// 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 . using Microsoft.EntityFrameworkCore; using System; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Collections.Generic; using VNLib.Utils; using VNLib.Plugins.Extensions.Data; using VNLib.Plugins.Extensions.Loading; using VNLib.Plugins.Extensions.Data.Abstractions; namespace SimpleBookmark.Model { internal sealed class BookmarkStore(IAsyncLazy dbOptions) : DbStore { /// public override IDbQueryLookup QueryTable { get; } = new BookmarkQueryLookup(); /// public override IDbContextHandle GetNewContext() => new SimpleBookmarkContext(dbOptions.Value); /// public override string GetNewRecordId() => Guid.NewGuid().ToString("n"); /// 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 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 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 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(); } public async Task AddBulkAsync(IEnumerable bookmarks, string userId, DateTimeOffset now, CancellationToken cancellation) { //Init new db connection await using SimpleBookmarkContext context = new(dbOptions.Value); await context.OpenTransactionAsync(cancellation); //Setup clean bookmark instances bookmarks = bookmarks.Select(b => new BookmarkEntry { Id = GetNewRecordId(), //new uuid UserId = userId, //Set userid LastModified = now.DateTime, //Allow reuse of created time Created = b.Created, Description = b.Description, Name = b.Name, Tags = b.Tags, Url = b.Url, }); //Filter out bookmarks that already exist bookmarks = bookmarks.Where(b => !context.Bookmarks.Any(b2 => b2.UserId == userId && b2.Url == b.Url)); //Add bookmarks to db context.AddRange(bookmarks); //Commit transaction return await context.SaveAndCloseAsync(true, cancellation); } private sealed class BookmarkQueryLookup : IDbQueryLookup { public IQueryable GetCollectionQueryBuilder(IDbContextHandle context, params string[] constraints) { string userId = constraints[0]; return from b in context.Set() where b.UserId == userId orderby b.Created descending select b; } public IQueryable GetSingleQueryBuilder(IDbContextHandle context, params string[] constraints) { string bookmarkId = constraints[0]; string userId = constraints[1]; return from b in context.Set() where b.UserId == userId && b.Id == bookmarkId select b; } } } }