diff options
author | vnugent <public@vaughnnugent.com> | 2024-01-20 23:49:29 -0500 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2024-01-20 23:49:29 -0500 |
commit | 6cb7da37824d02a1898d08d0f9495c77fde4dd1d (patch) | |
tree | 95e37ea3c20f416d6a205ee4ab050c307b18eafe /back-end/src/Model |
inital commit
Diffstat (limited to 'back-end/src/Model')
-rw-r--r-- | back-end/src/Model/BookmarkEntry.cs | 93 | ||||
-rw-r--r-- | back-end/src/Model/BookmarkStore.cs | 140 | ||||
-rw-r--r-- | back-end/src/Model/BookmarkStoreConfig.cs | 42 | ||||
-rw-r--r-- | back-end/src/Model/SimpleBookmarkContext.cs | 78 | ||||
-rw-r--r-- | back-end/src/Model/UserSettingsDbStore.cs | 77 | ||||
-rw-r--r-- | back-end/src/Model/UserSettingsEntry.cs | 39 |
6 files changed, 469 insertions, 0 deletions
diff --git a/back-end/src/Model/BookmarkEntry.cs b/back-end/src/Model/BookmarkEntry.cs new file mode 100644 index 0000000..dca7f87 --- /dev/null +++ b/back-end/src/Model/BookmarkEntry.cs @@ -0,0 +1,93 @@ +// Copyright (C) 2024 Vaughn Nugent +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +using System; +using System.Text.RegularExpressions; +using System.Text.Json.Serialization; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +using FluentValidation; + +using Microsoft.EntityFrameworkCore; + +using VNLib.Plugins.Extensions.Data; +using VNLib.Plugins.Extensions.Data.Abstractions; + +namespace SimpleBookmark.Model +{ + [Index(nameof(Url))] + internal sealed partial class BookmarkEntry : DbModelBase, IUserEntity + { + [Key] + public override string Id { get; set; } + + public override DateTime Created { get; set; } + + public override DateTime LastModified { get; set; } + + [JsonIgnore] + public string? UserId { get; set; } + + [MaxLength(100)] + public string? Name { get; set; } + + [MaxLength(200)] + public string? Url { get; set; } + + [MaxLength(500)] + public string? Description { get; set; } + + //Json flavor + [NotMapped] + [JsonPropertyName("Tags")] + public string[]? JsonTags + { + get => Tags?.Split(','); + set => Tags = value is null ? null : string.Join(',', value); + } + + //Database flavor as string + [JsonIgnore] + [MaxLength(100)] + public string? Tags { get; set; } + + public static IValidator<BookmarkEntry> GetValidator() + { + InlineValidator<BookmarkEntry> validator = new(); + + validator.RuleFor(p => p.Name) + .NotEmpty() + .Matches(@"^[a-zA-Z0-9_\-\|\. ]+$", RegexOptions.Compiled) + .MaximumLength(100); + + validator.RuleFor(p => p.Url) + .NotEmpty() + .Matches(@"^https?://", RegexOptions.Compiled) + .MaximumLength(200); + + validator.RuleFor(p => p.Description) + .MaximumLength(500); + + //Tags must be non-empty and alphanumeric only, no spaces + validator.RuleForEach(p => p.JsonTags) + .NotNull() + .NotEmpty() + .Matches(@"^[a-zA-Z0-9]+$", RegexOptions.Compiled); + + return validator; + } + } +} diff --git a/back-end/src/Model/BookmarkStore.cs b/back-end/src/Model/BookmarkStore.cs new file mode 100644 index 0000000..fbd3213 --- /dev/null +++ b/back-end/src/Model/BookmarkStore.cs @@ -0,0 +1,140 @@ +// Copyright (C) 2024 Vaughn Nugent +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +using Microsoft.EntityFrameworkCore; + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using VNLib.Plugins.Extensions.Data; +using VNLib.Plugins.Extensions.Data.Abstractions; +using VNLib.Plugins.Extensions.Loading; + +namespace SimpleBookmark.Model +{ + internal sealed class BookmarkStore(IAsyncLazy<DbContextOptions> dbOptions) : DbStore<BookmarkEntry> + { + ///<inheritdoc/> + public override IDbQueryLookup<BookmarkEntry> QueryTable { get; } = new BookmarkQueryLookup(); + + ///<inheritdoc/> + public override IDbContextHandle GetNewContext() => new SimpleBookmarkContext(dbOptions.Value); + + ///<inheritdoc/> + public override string GetNewRecordId() => Guid.NewGuid().ToString("n"); + + ///<inheritdoc/> + public override void OnRecordUpdate(BookmarkEntry newRecord, BookmarkEntry existing) + { + //Update existing record + existing.Name = newRecord.Name; + existing.Url = newRecord.Url; + existing.Description = newRecord.Description; + existing.JsonTags = newRecord.JsonTags; + } + + public async Task<BookmarkEntry[]> SearchBookmarksAsync(string userId, string? query, string[] tags, int limit, int page, CancellationToken cancellation) + { + ArgumentNullException.ThrowIfNull(userId); + ArgumentNullException.ThrowIfNull(tags); + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(limit, 0); + ArgumentOutOfRangeException.ThrowIfNegative(page); + + //Init new db connection + await using SimpleBookmarkContext context = new(dbOptions.Value); + await context.OpenTransactionAsync(cancellation); + + //Start with userid + IQueryable<BookmarkEntry> q = context.Bookmarks.Where(b => b.UserId == userId); + + if (tags.Length > 0) + { + //if tags are set, only return bookmarks that match the tags + q = q.Where(b => b.Tags != null && tags.All(t => b.Tags!.Contains(t))); + } + + if (!string.IsNullOrWhiteSpace(query)) + { + //if query is set, only return bookmarks that match the query + q = q.Where(b => EF.Functions.Like(b.Name, $"%{query}%") || EF.Functions.Like(b.Description, $"%{query}%")); + } + + //return bookmarks in descending order of creation + q = q.OrderByDescending(static b => b.Created); + + //return only the requested page + q = q.Skip(page * limit).Take(limit); + + //execute query + BookmarkEntry[] results = await q.ToArrayAsync(cancellation); + + //Close db and commit transaction + await context.SaveAndCloseAsync(true, cancellation); + + return results; + } + + public async Task<string[]> GetAllTagsForUserAsync(string userId, CancellationToken cancellation) + { + ArgumentNullException.ThrowIfNull(userId); + + //Init new db connection + await using SimpleBookmarkContext context = new(dbOptions.Value); + await context.OpenTransactionAsync(cancellation); + + //Get all tags for the user + string[] tags = await context.Bookmarks + .Where(b => b.UserId == userId) + .Select(static b => b.Tags!) + .ToArrayAsync(cancellation); + + //Close db and commit transaction + await context.SaveAndCloseAsync(true, cancellation); + + //Split tags into individual strings + return tags + .Where(static t => !string.IsNullOrWhiteSpace(t)) + .SelectMany(static t => t!.Split(',')) + .Distinct() + .ToArray(); + } + + private sealed class BookmarkQueryLookup : IDbQueryLookup<BookmarkEntry> + { + public IQueryable<BookmarkEntry> GetCollectionQueryBuilder(IDbContextHandle context, params string[] constraints) + { + string userId = constraints[0]; + + return from b in context.Set<BookmarkEntry>() + where b.UserId == userId + orderby b.Created descending + select b; + } + + public IQueryable<BookmarkEntry> GetSingleQueryBuilder(IDbContextHandle context, params string[] constraints) + { + string bookmarkId = constraints[0]; + string userId = constraints[1]; + + + return from b in context.Set<BookmarkEntry>() + where b.UserId == userId && b.Id == bookmarkId + select b; + } + } + } +} diff --git a/back-end/src/Model/BookmarkStoreConfig.cs b/back-end/src/Model/BookmarkStoreConfig.cs new file mode 100644 index 0000000..95d835a --- /dev/null +++ b/back-end/src/Model/BookmarkStoreConfig.cs @@ -0,0 +1,42 @@ +// Copyright (C) 2024 Vaughn Nugent +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +using System.Text.Json.Serialization; + + +namespace SimpleBookmark.Model +{ + internal sealed class BookmarkStoreConfig + { + /// <summary> + /// The maximum number of results that can be returned + /// in a single query. + /// </summary> + [JsonPropertyName("max_limit")] + public uint MaxLimit { get; set; } = 100; + + /// <summary> + /// The default number of results that will be returned in a single query. + /// </summary> + [JsonPropertyName("default_limit")] + public uint DefaultLimit { get; set; } = 10; + + /// <summary> + /// The maximum number of bookmarks that can be stored per user account. + /// </summary> + [JsonPropertyName("user_quota")] + public uint PerPersonQuota { get; set; } = 5000; + } +} diff --git a/back-end/src/Model/SimpleBookmarkContext.cs b/back-end/src/Model/SimpleBookmarkContext.cs new file mode 100644 index 0000000..ed734b3 --- /dev/null +++ b/back-end/src/Model/SimpleBookmarkContext.cs @@ -0,0 +1,78 @@ +// Copyright (C) 2024 Vaughn Nugent +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +using Microsoft.EntityFrameworkCore; + +using VNLib.Plugins.Extensions.Data; +using VNLib.Plugins.Extensions.Loading.Sql; + + +namespace SimpleBookmark.Model +{ + + internal sealed class SimpleBookmarkContext : TransactionalDbContext, IDbTableDefinition + { + + public DbSet<BookmarkEntry> Bookmarks { get; set; } + + public DbSet<UserSettingsEntry> BmSettings { get; set; } + + public SimpleBookmarkContext(DbContextOptions options) : base(options) + { } + + public SimpleBookmarkContext() : base() + { } + + public void OnDatabaseCreating(IDbContextBuilder builder, object? userState) + { + builder.DefineTable<BookmarkEntry>(nameof(Bookmarks)) + .WithColumn(p => p.Id) + .SetIsKey() + .Next() + + .WithColumn(p => p.Created) + .AllowNull(false) + .Next() + + .WithColumn(p => p.LastModified) + .AllowNull(false) + .Next() + + .WithColumn(p => p.UserId) + .AllowNull(false) + .Next() + + .WithColumn(p => p.Name) + .AllowNull(true) + .MaxLength(100) + .Next() + + .WithColumn(p => p.Url) + .AllowNull(true) + .Next() + + .WithColumn(p => p.Description) + .AllowNull(true) + .MaxLength(500) + .Next() + + .WithColumn(p => p.Tags) + .AllowNull(true) + .MaxLength(100) + .Next(); + } + + } +} diff --git a/back-end/src/Model/UserSettingsDbStore.cs b/back-end/src/Model/UserSettingsDbStore.cs new file mode 100644 index 0000000..aa44fa2 --- /dev/null +++ b/back-end/src/Model/UserSettingsDbStore.cs @@ -0,0 +1,77 @@ +// Copyright (C) 2024 Vaughn Nugent +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +using System; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.EntityFrameworkCore; + +using VNLib.Utils; +using VNLib.Plugins.Extensions.Loading; + +namespace SimpleBookmark.Model +{ + internal sealed class UserSettingsDbStore(IAsyncLazy<DbContextOptions> dbOptions) + { + + public async Task<UserSettingsEntry?> GetSettingsForUserAsync(string userId, CancellationToken cancellation) + { + ArgumentNullException.ThrowIfNull(userId); + + //Init new db connection + await using SimpleBookmarkContext context = new(dbOptions.Value); + await context.OpenTransactionAsync(cancellation); + + UserSettingsEntry? settings = await context.BmSettings.FirstOrDefaultAsync(p => p.UserId == userId, cancellation); + + //Close db and commit transaction + await context.SaveAndCloseAsync(true, cancellation); + + return settings; + } + + public async Task<ERRNO> SetSettingsForUser(string userId, UserSettingsEntry settings, CancellationToken cancellation) + { + ArgumentNullException.ThrowIfNull(userId); + ArgumentNullException.ThrowIfNull(settings); + + //Init new db connection + await using SimpleBookmarkContext context = new(dbOptions.Value); + await context.OpenTransactionAsync(cancellation); + + //Search for existing settings entry + UserSettingsEntry? existing = await context.BmSettings.FirstOrDefaultAsync(p => p.UserId == userId, cancellation); + + if (existing is null) + { + //Add a new entry + settings.UserId = userId; + settings.LastModified = DateTime.UtcNow; + context.Add(settings); + } + else + { + //Update existing entry + existing.SettingsData = settings.SettingsData; + existing.LastModified = DateTime.UtcNow; + context.Update(existing); + } + + //Close db and commit transaction + return await context.SaveAndCloseAsync(true, cancellation); + } + } +} diff --git a/back-end/src/Model/UserSettingsEntry.cs b/back-end/src/Model/UserSettingsEntry.cs new file mode 100644 index 0000000..c27af8a --- /dev/null +++ b/back-end/src/Model/UserSettingsEntry.cs @@ -0,0 +1,39 @@ +// Copyright (C) 2024 Vaughn Nugent +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +using System; +using System.Text.Json.Serialization; +using System.ComponentModel.DataAnnotations; + +using VNLib.Plugins.Extensions.Data.Abstractions; + +namespace SimpleBookmark.Model +{ + internal sealed class UserSettingsEntry : IUserEntity + { + public DateTime LastModified { get; set; } + + [Timestamp] + [JsonIgnore] + public byte[]? Version { get; set; } + + [Key] + [JsonIgnore] + public string? UserId { get; set; } + + [MaxLength(5000)] + public byte[]? SettingsData { get; set; } + } +} |