aboutsummaryrefslogtreecommitdiff
path: root/back-end/src/Model
diff options
context:
space:
mode:
Diffstat (limited to 'back-end/src/Model')
-rw-r--r--back-end/src/Model/BookmarkEntry.cs93
-rw-r--r--back-end/src/Model/BookmarkStore.cs140
-rw-r--r--back-end/src/Model/BookmarkStoreConfig.cs42
-rw-r--r--back-end/src/Model/SimpleBookmarkContext.cs78
-rw-r--r--back-end/src/Model/UserSettingsDbStore.cs77
-rw-r--r--back-end/src/Model/UserSettingsEntry.cs39
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; }
+ }
+}