diff options
32 files changed, 1105 insertions, 902 deletions
diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IBulkDataStore.cs b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IBulkDataStore.cs deleted file mode 100644 index 0a2e4a8..0000000 --- a/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IBulkDataStore.cs +++ /dev/null @@ -1,65 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Data -* File: IBulkDataStore.cs -* -* IBulkDataStore.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Extensions.Data 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. -* -* VNLib.Plugins.Extensions.Data 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.Collections.Generic; -using System.Threading.Tasks; - -using VNLib.Utils; - -namespace VNLib.Plugins.Extensions.Data.Abstractions -{ - /// <summary> - /// An abstraction that defines a Data-Store that supports - /// bulk data operations - /// </summary> - /// <typeparam name="T">The data-model type</typeparam> - public interface IBulkDataStore<T> - { - /// <summary> - /// Deletes a collection of records from the store - /// </summary> - /// <param name="records">A collection of records to delete</param> - ///<returns>A task the resolves the number of entires removed from the store</returns> - Task<ERRNO> DeleteBulkAsync(ICollection<T> records); - /// <summary> - /// Updates a collection of records - /// </summary> - /// <param name="records">The collection of records to update</param> - /// <returns>A task the resolves an error code (should evaluate to false on failure, and true on success)</returns> - Task<ERRNO> UpdateBulkAsync(ICollection<T> records); - /// <summary> - /// Creates a bulk collection of records as entries in the store - /// </summary> - /// <param name="records">The collection of records to add</param> - /// <returns>A task the resolves an error code (should evaluate to false on failure, and true on success)</returns> - Task<ERRNO> CreateBulkAsync(ICollection<T> records); - /// <summary> - /// Creates or updates individual records from a bulk collection of records - /// </summary> - /// <param name="records">The collection of records to add</param> - /// <returns>A task the resolves an error code (should evaluate to false on failure, and true on success)</returns> - Task<ERRNO> AddOrUpdateBulkAsync(ICollection<T> records); - } - -} diff --git a/lib/VNLib.Plugins.Extensions.Data/src/IConcurrentDbContext.cs b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IConcurrentDbContext.cs index 330b05a..f3308c5 100644 --- a/lib/VNLib.Plugins.Extensions.Data/src/IConcurrentDbContext.cs +++ b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IConcurrentDbContext.cs @@ -26,7 +26,7 @@ using System.Threading; using System.Threading.Tasks; using System.Transactions; -namespace VNLib.Plugins.Extensions.Data +namespace VNLib.Plugins.Extensions.Data.Abstractions { /// <summary> /// Represents a database context that can manage concurrency via transactions diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDataStore.cs b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDataStore.cs index 1c8174c..bdd1b6c 100644 --- a/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDataStore.cs +++ b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDataStore.cs @@ -22,14 +22,6 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -using System; -using System.Threading; -using System.Threading.Tasks; -using System.Collections.Generic; - -using VNLib.Utils; - - namespace VNLib.Plugins.Extensions.Data.Abstractions { /// <summary> @@ -37,99 +29,30 @@ namespace VNLib.Plugins.Extensions.Data.Abstractions /// operations that retrieve or manipulate records of data /// </summary> /// <typeparam name="T">The data-model type</typeparam> - public interface IDataStore<T> + public interface IDataStore<T> where T: class, IDbModel { /// <summary> - /// Gets the total number of records in the current store - /// </summary> - /// <param name="cancellation">A cancellation token to cancel the operation</param> - /// <returns>A task that resolves the number of records in the store</returns> - Task<long> GetCountAsync(CancellationToken cancellation = default); - /// <summary> - /// Gets the number of records that belong to the specified constraint - /// </summary> - /// <param name="specifier">A specifier to constrain the reults</param> - /// <param name="cancellation">A cancellation token to cancel the operation</param> - /// <returns>The number of records that belong to the specifier</returns> - Task<long> GetCountAsync(string specifier, CancellationToken cancellation = default); - /// <summary> - /// Gets a record from its key + /// Gets a unique ID for a new record being added to the store /// </summary> - /// <param name="key">The key identifying the unique record</param> - /// <param name="cancellation">A cancellation token to cancel the operation</param> - /// <returns>A promise that resolves the record identified by the specified key</returns> - Task<T?> GetSingleAsync(string key, CancellationToken cancellation = default); + string GetNewRecordId(); + /// <summary> - /// Gets a record from its key + /// Gets a new <see cref="TransactionalDbContext"/> ready for use /// </summary> - /// <param name="specifiers">A variable length specifier arguemnt array for retreiving a single application</param> /// <returns></returns> - Task<T?> GetSingleAsync(params string[] specifiers); - /// <summary> - /// Gets a record from the store with a partial model, intended to complete the model - /// </summary> - /// <param name="record">The partial model used to query the store</param> - /// <param name="cancellation">A cancellation token to cancel the operation</param> - /// <returns>A task the resolves the completed data-model</returns> - Task<T?> GetSingleAsync(T record, CancellationToken cancellation = default); - /// <summary> - /// Fills a collection with enires retireved from the store using the specifer - /// </summary> - /// <param name="collection">The collection to add entires to</param> - /// <param name="specifier">A specifier argument to constrain results</param> - /// <param name="limit">The maximum number of elements to retrieve</param> - /// <param name="cancellation">A cancellation token to cancel the operation</param> - /// <returns>A Task the resolves to the number of items added to the collection</returns> - Task<ERRNO> GetCollectionAsync(ICollection<T> collection, string specifier, int limit, CancellationToken cancellation = default); - /// <summary> - /// Fills a collection with enires retireved from the store using a variable length specifier - /// parameter - /// </summary> - /// <param name="collection">The collection to add entires to</param> - /// <param name="limit">The maximum number of elements to retrieve</param> - /// <param name="args"></param> - /// <returns>A Task the resolves to the number of items added to the collection</returns> - Task<ERRNO> GetCollectionAsync(ICollection<T> collection, int limit, params string[] args); - /// <summary> - /// Updates an entry in the store with the specified record - /// </summary> - /// <param name="record">The record to update</param> - /// <param name="cancellation">A cancellation token to cancel the operation</param> - /// <returns>A task the resolves an error code (should evaluate to false on failure, and true on success)</returns> - Task<ERRNO> UpdateAsync(T record, CancellationToken cancellation = default); - /// <summary> - /// Creates a new entry in the store representing the specified record - /// </summary> - /// <param name="record">The record to add to the store</param> - /// <param name="cancellation">A cancellation token to cancel the operation</param> - /// <returns>A task the resolves an error code (should evaluate to false on failure, and true on success)</returns> - Task<ERRNO> CreateAsync(T record, CancellationToken cancellation = default); - /// <summary> - /// Deletes one or more entrires from the store matching the specified record - /// </summary> - /// <param name="record">The record to remove from the store</param> - /// <param name="cancellation">A cancellation token to cancel the operation</param> - /// <returns>A task the resolves the number of records removed(should evaluate to false on failure, and deleted count on success)</returns> - Task<ERRNO> DeleteAsync(T record, CancellationToken cancellation = default); - /// <summary> - /// Deletes one or more entires from the store matching the specified unique key - /// </summary> - /// <param name="key">The unique key that identifies the record</param> - /// <param name="cancellation">A cancellation token to cancel the operation</param> - /// <returns>A task the resolves the number of records removed(should evaluate to false on failure, and deleted count on success)</returns> - Task<ERRNO> DeleteAsync(string key, CancellationToken cancellation = default); + IDbContextHandle GetNewContext(); + /// <summary> - /// Deletes one or more entires from the store matching the supplied specifiers + /// Represents a table of ef queryies that can be used to execute operations against a a database /// </summary> - /// <param name="specifiers">A variable length array of specifiers used to delete one or more entires</param> - /// <returns>A task the resolves the number of records removed(should evaluate to false on failure, and deleted count on success)</returns> - Task<ERRNO> DeleteAsync(params string[] specifiers); + IDbQueryLookup<T> QueryTable { get; } + /// <summary> - /// Updates an entry in the store if it exists, or creates a new entry if one does not already exist + /// Updates the current record (if found) to the new record before + /// storing the updates. /// </summary> - /// <param name="record">The record to add to the store</param> - /// <param name="cancellation">A cancellation token to cancel the operation</param> - /// <returns>A task the resolves the result of the operation</returns> - Task<ERRNO> AddOrUpdateAsync(T record, CancellationToken cancellation = default); + /// <param name="newRecord">The new record to capture data from</param> + /// <param name="existing">The current record to be updated</param> + void OnRecordUpdate(T newRecord, T existing); } } diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDbContextHandle.cs b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDbContextHandle.cs new file mode 100644 index 0000000..73734b5 --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDbContextHandle.cs @@ -0,0 +1,84 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: IDbContextHandle.cs +* +* IDbContextHandle.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.Data 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. +* +* VNLib.Plugins.Extensions.Data 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.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +using VNLib.Utils; + +namespace VNLib.Plugins.Extensions.Data.Abstractions +{ + /// <summary> + /// Represents an open database connection and interfaces with the database, + /// allows queries, and modifications of the set + /// </summary> + public interface IDbContextHandle : IAsyncDisposable + { + /// <summary> + /// Gets a supported set of the desired entity type within the context + /// </summary> + /// <typeparam name="T">The entity model type</typeparam> + /// <returns>A querriable instance to execute queries on</returns> + IQueryable<T> Set<T>() where T : class; + + /// <summary> + /// Adds a new entity to the set + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="entity">The entity instance to add to the set</param> + void Add<T>(T entity) where T : class; + + /// <summary> + /// Adds a range of entities to the set of the given type + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="entities">The range of entitites to add to the set</param> + void AddRange<T>(IEnumerable<T> entities) where T : class; + + /// <summary> + /// Removes an entity of a given type from the set + /// </summary> + /// <typeparam name="T">The entity type to remove</typeparam> + /// <param name="entity">The entity instance containing required information to remove</param> + void Remove<T>(T entity) where T : class; + + /// <summary> + /// Removes a range of entities of a given type from the set + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="entities">The range of entities to remove</param> + void RemoveRange<T>(IEnumerable<T> entities) where T : class; + + /// <summary> + /// Commits saves changes on the context and optionally commits changes to the database + /// </summary> + /// <param name="commit">A value that indicates whether the changes should be commited to the database</param> + /// <param name="cancellation">A token to cancel the operation</param> + /// <returns>The result of the database commit</returns> + Task<ERRNO> SaveAndCloseAsync(bool commit, CancellationToken cancellation = default); + } +} diff --git a/lib/VNLib.Plugins.Extensions.Data/src/IDbModel.cs b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDbModel.cs index f60dc27..2826c5a 100644 --- a/lib/VNLib.Plugins.Extensions.Data/src/IDbModel.cs +++ b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDbModel.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Data @@ -24,7 +24,7 @@ using System; -namespace VNLib.Plugins.Extensions.Data +namespace VNLib.Plugins.Extensions.Data.Abstractions { /// <summary> /// Represents a basic data model for an EFCore entity @@ -36,14 +36,17 @@ namespace VNLib.Plugins.Extensions.Data /// A unique id for the entity /// </summary> string Id { get; set; } + /// <summary> /// The <see cref="DateTime"/> the entity was created in the store /// </summary> DateTime Created { get; set; } + /// <summary> /// The <see cref="DateTime"/> the entity was last modified in the store /// </summary> DateTime LastModified { get; set; } + /// <summary> /// Entity concurrency token /// </summary> diff --git a/lib/VNLib.Plugins.Extensions.Data/src/DbStoreQueries.cs b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDbQueryLookup.cs index ff0319e..86c8bff 100644 --- a/lib/VNLib.Plugins.Extensions.Data/src/DbStoreQueries.cs +++ b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IDbQueryLookup.cs @@ -3,10 +3,10 @@ * * Library: VNLib * Package: VNLib.Plugins.Extensions.Data -* File: DbStoreQueries.cs +* File: IDbQueryLookup.cs * -* DbStoreQueries.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger -* VNLib collection of libraries and utilities. +* IDbQueryLookup.cs is part of VNLib.Plugins.Extensions.Data which is part +* of the larger VNLib collection of libraries and utilities. * * VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -22,15 +22,16 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -using System; using System.Linq; -namespace VNLib.Plugins.Extensions.Data +namespace VNLib.Plugins.Extensions.Data.Abstractions { - - public partial class DbStore<T> + /// <summary> + /// Represents a collection of queries that can be used to execute operations against a a database + /// </summary> + /// <typeparam name="T"></typeparam> + public interface IDbQueryLookup<T> where T : class, IDbModel { - /// <summary> /// Builds a query that attempts to get a single entry from the /// store based on the specified record if it does not have a @@ -39,7 +40,7 @@ namespace VNLib.Plugins.Extensions.Data /// <param name="context">The active context to query</param> /// <param name="record">The record to search for</param> /// <returns>A query that yields a single record if it exists in the store</returns> - protected virtual IQueryable<T> AddOrUpdateQueryBuilder(TransactionalDbContext context, T record) + virtual IQueryable<T> AddOrUpdateQueryBuilder(IDbContextHandle context, T record) { //default to get single of the specific record return GetSingleQueryBuilder(context, record); @@ -52,13 +53,12 @@ namespace VNLib.Plugins.Extensions.Data /// <param name="context">The active context to query</param> /// <param name="record">The record to search for</param> /// <returns>A query that yields a single record to update if it exists in the store</returns> - protected virtual IQueryable<T> UpdateQueryBuilder(TransactionalDbContext context, T record) + virtual IQueryable<T> UpdateQueryBuilder(IDbContextHandle context, T record) { //default to get single of the specific record return GetSingleQueryBuilder(context, record); } - /// <summary> /// Builds a query that results in a single entry to delete from the /// constraint arguments @@ -66,7 +66,7 @@ namespace VNLib.Plugins.Extensions.Data /// <param name="context">The active context</param> /// <param name="constraints">A variable length parameter array of query constraints</param> /// <returns>A query that yields a single record (or no record) to delete</returns> - protected virtual IQueryable<T> DeleteQueryBuilder(TransactionalDbContext context, params string[] constraints) + virtual IQueryable<T> DeleteQueryBuilder(IDbContextHandle context, params string[] constraints) { //default use the get-single method, as the implementation is usually identical return GetSingleQueryBuilder(context, constraints); @@ -78,19 +78,18 @@ namespace VNLib.Plugins.Extensions.Data /// <param name="context">The active context to run the query on</param> /// <param name="specifier">The specifier constrain</param> /// <returns>A query that can be counted</returns> - protected virtual IQueryable<T> GetCollectionQueryBuilder(TransactionalDbContext context, string specifier) + virtual IQueryable<T> GetCollectionQueryBuilder(IDbContextHandle context, string specifier) { return GetCollectionQueryBuilder(context, new string[] { specifier }); } - /// <summary> /// Builds a query to get a count of records constrained by the specifier /// </summary> /// <param name="context">The active context to run the query on</param> /// <param name="specifier">The specifier constrain</param> /// <returns>A query that can be counted</returns> - protected virtual IQueryable<T> GetCountQueryBuilder(TransactionalDbContext context, string specifier) + virtual IQueryable<T> GetCountQueryBuilder(IDbContextHandle context, string specifier) { //Default use the get collection and just call the count method return GetCollectionQueryBuilder(context, specifier); @@ -107,7 +106,7 @@ namespace VNLib.Plugins.Extensions.Data /// <param name="context">The context to execute query against</param> /// <param name="record">A record to referrence the lookup</param> /// <returns>A query that yields a single record</returns> - protected virtual IQueryable<T> GetSingleQueryBuilder(TransactionalDbContext context, T record) + virtual IQueryable<T> GetSingleQueryBuilder(IDbContextHandle context, T record) { return from entry in context.Set<T>() where entry.Id == record.Id @@ -120,27 +119,19 @@ namespace VNLib.Plugins.Extensions.Data /// <param name="context">The active context to run the query on</param> /// <param name="constraints">An arguments array to constrain the results of the query</param> /// <returns>A query that returns a paginated collection of records from the store</returns> - protected virtual IQueryable<T> GetPageQueryBuilder(TransactionalDbContext context, params string[] constraints) + virtual IQueryable<T> GetPageQueryBuilder(IDbContextHandle context, params string[] constraints) { //Default to getting the entire collection and just selecting a single page return GetCollectionQueryBuilder(context, constraints); } /// <summary> - /// Updates the current record (if found) to the new record before - /// storing the updates. - /// </summary> - /// <param name="newRecord">The new record to capture data from</param> - /// <param name="currentRecord">The current record to be updated</param> - protected abstract void OnRecordUpdate(T newRecord, T currentRecord); - - /// <summary> /// Builds a query to get a single record from the variable length parameter arguments /// </summary> /// <param name="context">The context to execute query against</param> /// <param name="constraints">Arguments to constrain the results of the query to a single record</param> /// <returns>A query that yields a single record</returns> - protected abstract IQueryable<T> GetSingleQueryBuilder(TransactionalDbContext context, params string[] constraints); + IQueryable<T> GetSingleQueryBuilder(IDbContextHandle context, params string[] constraints); /// <summary> /// Builds a query to get a collection of records based on an variable length array of parameters @@ -148,6 +139,6 @@ namespace VNLib.Plugins.Extensions.Data /// <param name="context">The active context to run the query on</param> /// <param name="constraints">An arguments array to constrain the results of the query</param> /// <returns>A query that returns a collection of records from the store</returns> - protected abstract IQueryable<T> GetCollectionQueryBuilder(TransactionalDbContext context, params string[] constraints); + IQueryable<T> GetCollectionQueryBuilder(IDbContextHandle context, params string[] constraints); } } diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IPaginatedDataStore.cs b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IPaginatedDataStore.cs deleted file mode 100644 index 7c271eb..0000000 --- a/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/IPaginatedDataStore.cs +++ /dev/null @@ -1,58 +0,0 @@ -/* -* Copyright (c) 2023 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Data -* File: IPaginatedDataStore.cs -* -* IPaginatedDataStore.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Extensions.Data 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. -* -* VNLib.Plugins.Extensions.Data 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.Threading; -using System.Threading.Tasks; -using System.Collections.Generic; - -namespace VNLib.Plugins.Extensions.Data.Abstractions -{ - /// <summary> - /// Defines a Data-Store that can retirieve and manipulate paginated - /// data - /// </summary> - /// <typeparam name="T">The data-model type</typeparam> - public interface IPaginatedDataStore<T> - { - /// <summary> - /// Gets a collection of records using a pagination style query, and adds the records to the collecion - /// </summary> - /// <param name="collection">The collection to add records to</param> - /// <param name="page">Pagination page to get records from</param> - /// <param name="limit">The maximum number of items to retrieve from the store</param> - /// <param name="cancellation">A cancellation token to cancel the operation</param> - /// <returns>A task that resolves the number of items added to the collection</returns> - Task<int> GetPageAsync(ICollection<T> collection, int page, int limit, CancellationToken cancellation = default); - /// <summary> - /// Gets a collection of records using a pagination style query with constraint arguments, and adds the records to the collecion - /// </summary> - /// <param name="collection">The collection to add records to</param> - /// <param name="page">Pagination page to get records from</param> - /// <param name="limit">The maximum number of items to retrieve from the store</param> - /// <param name="constraints">A params array of strings to constrain the result set from the db</param> - /// <returns>A task that resolves the number of items added to the collection</returns> - Task<int> GetPageAsync(ICollection<T> collection, int page, int limit, params string[] constraints); - } - -} diff --git a/lib/VNLib.Plugins.Extensions.Data/src/ITransactionalDbContext.cs b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/ITransactionalDbContext.cs index a699140..dd906d1 100644 --- a/lib/VNLib.Plugins.Extensions.Data/src/ITransactionalDbContext.cs +++ b/lib/VNLib.Plugins.Extensions.Data/src/Abstractions/ITransactionalDbContext.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Data @@ -27,7 +27,7 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.Storage; -namespace VNLib.Plugins.Extensions.Data +namespace VNLib.Plugins.Extensions.Data.Abstractions { /// <summary> /// Represents a database context that can manage concurrency via transactions diff --git a/lib/VNLib.Plugins.Extensions.Data/src/DbModelBase.cs b/lib/VNLib.Plugins.Extensions.Data/src/DbModelBase.cs index 5104e02..cdf763c 100644 --- a/lib/VNLib.Plugins.Extensions.Data/src/DbModelBase.cs +++ b/lib/VNLib.Plugins.Extensions.Data/src/DbModelBase.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Data @@ -26,6 +26,8 @@ using System; using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; +using VNLib.Plugins.Extensions.Data.Abstractions; + namespace VNLib.Plugins.Extensions.Data { /// <summary> diff --git a/lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs b/lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs index 761d78f..b5f327e 100644 --- a/lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs +++ b/lib/VNLib.Plugins.Extensions.Data/src/DbStore.cs @@ -22,16 +22,8 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -using System; -using System.Linq; -using System.Threading; -using System.Transactions; -using System.Threading.Tasks; using System.Collections.Generic; -using Microsoft.EntityFrameworkCore; - -using VNLib.Utils; using VNLib.Utils.Memory.Caching; using VNLib.Plugins.Extensions.Data.Abstractions; @@ -42,370 +34,28 @@ namespace VNLib.Plugins.Extensions.Data /// Implements basic data-store functionality with abstract query builders /// </summary> /// <typeparam name="T">A <see cref="DbModelBase"/> implemented type</typeparam> - public abstract partial class DbStore<T> : IDataStore<T>, IPaginatedDataStore<T> where T: class, IDbModel + public abstract class DbStore<T> : IDataStore<T> where T: class, IDbModel { - /// <summary> - /// Gets a unique ID for a new record being added to the store - /// </summary> - public abstract string RecordIdBuilder { get; } /// <summary> /// Gets a new <see cref="TransactionalDbContext"/> ready for use /// </summary> /// <returns></returns> - public abstract TransactionalDbContext NewContext(); - - /// <summary> - /// An object rental for entity collections - /// </summary> - public ObjectRental<List<T>> ListRental { get; } = ObjectRental.Create<List<T>>(null, static ret => ret.Clear()); - - #region Add Or Update - ///<inheritdoc/> - public virtual async Task<ERRNO> AddOrUpdateAsync(T record, CancellationToken cancellation = default) - { - //Open new db context - await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.ReadCommitted, cancellation); - - IQueryable<T> query; - - if (string.IsNullOrWhiteSpace(record.Id)) - { - //Get the application - query = AddOrUpdateQueryBuilder(ctx, record); - } - else - { - //Get the application - query = (from et in ctx.Set<T>() - where et.Id == record.Id - select et); - } - - //Using single - T? entry = await query.SingleOrDefaultAsync(cancellation); - - //Check if creted - if (entry == null) - { - //Create a new template id - record.Id = RecordIdBuilder; - //Set the created/lm times - record.Created = record.LastModified = DateTime.UtcNow; - //Add the new template to the ctx - ctx.Add(record); - } - else - { - OnRecordUpdate(record, entry); - } - - return await ctx.SaveAndCloseAsync(cancellation); - } - - ///<inheritdoc/> - public virtual async Task<ERRNO> UpdateAsync(T record, CancellationToken cancellation = default) - { - //Open new db context - await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.Serializable, cancellation); - - //Get the application - IQueryable<T> query = UpdateQueryBuilder(ctx, record); - - //Using single to make sure only one app is in the db (should never be an issue) - T? oldEntry = await query.SingleOrDefaultAsync(cancellation); - - if (oldEntry == null) - { - return false; - } - - //Update the template meta-data - OnRecordUpdate(record, oldEntry); - - //Only publish update if changes happened - if (!ctx.ChangeTracker.HasChanges()) - { - //commit transaction if no changes need to be made - await ctx.CommitTransactionAsync(cancellation); - return true; - } - - return await ctx.SaveAndCloseAsync(cancellation); - } - - ///<inheritdoc/> - public virtual async Task<ERRNO> CreateAsync(T record, CancellationToken cancellation = default) - { - //Open new db context - await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.ReadUncommitted, cancellation); - - //Create a new template id - record.Id = RecordIdBuilder; - - //Update the created/last modified time of the record - record.Created = record.LastModified = DateTime.UtcNow; - - //Add the new template - ctx.Add(record); - - return await ctx.SaveAndCloseAsync(cancellation); - } - - #endregion - - #region Delete + public abstract IDbContextHandle GetNewContext(); ///<inheritdoc/> - public virtual async Task<ERRNO> DeleteAsync(string key, CancellationToken cancellation = default) - { - //Open new db context - await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.RepeatableRead, cancellation); - - //Get the template by its id - IQueryable<T> query = (from temp in ctx.Set<T>() - where temp.Id == key - select temp); - - T? record = await query.SingleOrDefaultAsync(cancellation); - - if (record == null) - { - return false; - } - - //Add the new application - ctx.Remove(record); - - return await ctx.SaveAndCloseAsync(cancellation); - } + public abstract string GetNewRecordId(); ///<inheritdoc/> - public virtual async Task<ERRNO> DeleteAsync(T record, CancellationToken cancellation = default) - { - //Open new db context - await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.RepeatableRead, cancellation); - - //Get a query for a a single item - IQueryable<T> query = GetSingleQueryBuilder(ctx, record); - - //Get the entry - T? entry = await query.SingleOrDefaultAsync(cancellation); - - if (entry == null) - { - return false; - } - - //Add the new application - ctx.Remove(entry); - - return await ctx.SaveAndCloseAsync(cancellation); - } + public abstract void OnRecordUpdate(T newRecord, T existing); ///<inheritdoc/> - public virtual async Task<ERRNO> DeleteAsync(params string[] specifiers) - { - //Open new db context - await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.RepeatableRead); - - //Get the template by its id - IQueryable<T> query = DeleteQueryBuilder(ctx, specifiers); - - T? entry = await query.SingleOrDefaultAsync(); - - if (entry == null) - { - return false; - } - - //Add the new application - ctx.Remove(entry); - - return await ctx.SaveAndCloseAsync(); - } - - #endregion - - #region Get Collection - - ///<inheritdoc/> - public virtual async Task<ERRNO> GetCollectionAsync(ICollection<T> collection, string specifier, int limit, CancellationToken cancellation = default) - { - int previous = collection.Count; - - //Open new db context - await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.ReadUncommitted, cancellation); - - //Get the single template by its id - await GetCollectionQueryBuilder(ctx, specifier) - .Take(limit) - .Select(static e => e) - .ForEachAsync(collection.Add, cancellation); - - //close db and transaction - await ctx.CommitTransactionAsync(cancellation); + public abstract IDbQueryLookup<T> QueryTable { get; } - //Return the number of elements add to the collection - return collection.Count - previous; - } - - ///<inheritdoc/> - public virtual async Task<ERRNO> GetCollectionAsync(ICollection<T> collection, int limit, params string[] args) - { - int previous = collection.Count; - - //Open new db context - await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.ReadUncommitted); - - //Get the single template by the supplied user arguments - await GetCollectionQueryBuilder(ctx, args) - .Take(limit) - .Select(static e => e) - .ForEachAsync(collection.Add); - - //close db and transaction - await ctx.CommitTransactionAsync(); - - //Return the number of elements add to the collection - return collection.Count - previous; - } - - #endregion - - #region Get Count - - ///<inheritdoc/> - public virtual async Task<long> GetCountAsync(CancellationToken cancellation = default) - { - //Open db connection - await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.ReadUncommitted, cancellation); - - //Async get the number of records of the given entity type - long count = await ctx.Set<T>().LongCountAsync(cancellation); - - //close db and transaction - await ctx.CommitTransactionAsync(cancellation); - - return count; - } - - ///<inheritdoc/> - public virtual async Task<long> GetCountAsync(string specifier, CancellationToken cancellation) - { - await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.ReadUncommitted, cancellation); - - //Async get the number of records of the given entity type - long count = await GetCountQueryBuilder(ctx, specifier).LongCountAsync(cancellation); - - //close db and transaction - await ctx.CommitTransactionAsync(cancellation); - - return count; - } - - - #endregion - - #region Get Single - - ///<inheritdoc/> - public virtual async Task<T?> GetSingleAsync(string key, CancellationToken cancellation = default) - { - //Open db connection - await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.ReadUncommitted, cancellation); - - //Get the single template by its id - T? record = await (from entry in ctx.Set<T>() - where entry.Id == key - select entry) - .SingleOrDefaultAsync(cancellation); - - //close db and transaction - await ctx.CommitTransactionAsync(cancellation); - return record; - } - - ///<inheritdoc/> - public virtual async Task<T?> GetSingleAsync(T record, CancellationToken cancellation = default) - { - //Open db connection - await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.ReadUncommitted, cancellation); - - //Get the single template by its id - T? entry = await GetSingleQueryBuilder(ctx, record).SingleOrDefaultAsync(cancellation); - - //close db and transaction - await ctx.CommitTransactionAsync(cancellation); - - return record; - } - - ///<inheritdoc/> - public virtual async Task<T?> GetSingleAsync(params string[] specifiers) - { - //Open db connection - await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.ReadUncommitted); - - //Get the single template by its id - T? record = await GetSingleQueryBuilder(ctx, specifiers).SingleOrDefaultAsync(); - - //close db and transaction - await ctx.CommitTransactionAsync(); - - return record; - } - - #endregion - - #region Get Page - - ///<inheritdoc/> - public virtual async Task<int> GetPageAsync(ICollection<T> collection, int page, int limit, CancellationToken cancellation = default) - { - //Store preivous count - int previous = collection.Count; - - //Open db connection - await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.ReadUncommitted, cancellation); - - //Get a page offset and a limit for the - await ctx.Set<T>() - .Skip(page * limit) - .Take(limit) - .Select(static p => p) - .ForEachAsync(collection.Add, cancellation); - - //close db and transaction - await ctx.CommitTransactionAsync(cancellation); - - //Return the number of records added - return collection.Count - previous; - } - - ///<inheritdoc/> - public virtual async Task<int> GetPageAsync(ICollection<T> collection, int page, int limit, params string[] constraints) - { - //Store preivous count - int previous = collection.Count; - - //Open new db context - await using TransactionalDbContext ctx = await this.OpenAsync(IsolationLevel.ReadUncommitted); - - //Get a page of records constrained by the given arguments - await GetPageQueryBuilder(ctx, constraints) - .Skip(page * limit) - .Take(limit) - .Select(static e => e) - .ForEachAsync(collection.Add); - - //close db and transaction - await ctx.CommitTransactionAsync(); - - //Return the number of records added - return collection.Count - previous; - } - - #endregion + /// <summary> + /// An object rental for entity collections + /// </summary> + public ObjectRental<List<T>> ListRental { get; } = ObjectRental.Create<List<T>>(null, static ret => ret.Clear()); + } } diff --git a/lib/VNLib.Plugins.Extensions.Data/src/DbStoreHelperExtensions.cs b/lib/VNLib.Plugins.Extensions.Data/src/DbStoreHelperExtensions.cs deleted file mode 100644 index 5971307..0000000 --- a/lib/VNLib.Plugins.Extensions.Data/src/DbStoreHelperExtensions.cs +++ /dev/null @@ -1,82 +0,0 @@ -/* -* Copyright (c) 2023 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Data -* File: DbStore.cs -* -* DbStore.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Extensions.Data 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. -* -* VNLib.Plugins.Extensions.Data 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.Threading; -using System.Transactions; -using System.Threading.Tasks; - -using VNLib.Utils; - -namespace VNLib.Plugins.Extensions.Data -{ - internal static class DbStoreHelperExtensions - { - /// <summary> - /// Commits saves changes on the context and commits the transaction if the result - /// of the operation was successful - /// </summary> - /// <param name="ctx"></param> - /// <param name="cancellation">A token to cancel the operation</param> - /// <returns>A task that resolves the result of the operation</returns> - public static async Task<ERRNO> SaveAndCloseAsync(this TransactionalDbContext ctx, CancellationToken cancellation = default) - { - //Save changes - ERRNO result = await ctx.SaveChangesAsync(cancellation); - - if (result) - { - //commit transaction if update was successful - await ctx.CommitTransactionAsync(cancellation); - } - - return result; - } - - /// <summary> - /// Opens a new database connection and begins a transaction with the specified isolation level - /// </summary> - /// <typeparam name="T"></typeparam> - /// <param name="store"></param> - /// <param name="level">The transaction isolation level</param> - /// <param name="cancellation">A token to cancel the transaction operation</param> - /// <returns></returns> - public static async Task<TransactionalDbContext> OpenAsync<T>(this DbStore<T> store, IsolationLevel level, CancellationToken cancellation = default) - where T : class, IDbModel - { - //Open new db context - TransactionalDbContext ctx = store.NewContext(); - try - { - //Open transaction - await ctx.OpenTransactionAsync(level, cancellation); - return ctx; - } - catch - { - await ctx.DisposeAsync(); - throw; - } - } - } -} diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Extensions.cs b/lib/VNLib.Plugins.Extensions.Data/src/Extensions.cs deleted file mode 100644 index 0a956ed..0000000 --- a/lib/VNLib.Plugins.Extensions.Data/src/Extensions.cs +++ /dev/null @@ -1,81 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Data -* File: Extensions.cs -* -* Extensions.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Extensions.Data 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. -* -* VNLib.Plugins.Extensions.Data 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.Linq; -using System.Threading.Tasks; -using System.Collections.Generic; - -using Microsoft.EntityFrameworkCore; - -using VNLib.Utils; -using VNLib.Plugins.Extensions.Data.Abstractions; - -namespace VNLib.Plugins.Extensions.Data -{ - public static class Extensions - { - - public static int GetPageOrDefault(this IReadOnlyDictionary<string, string> queryArgs, int @default, int minClamp = 0, int maxClamp = int.MaxValue) - { - return queryArgs.TryGetValue("page", out string? pageStr) && int.TryParse(pageStr, out int page) ? Math.Clamp(page, minClamp, maxClamp) : @default; - } - - public static int GetLimitOrDefault(this IReadOnlyDictionary<string, string> queryArgs, int @default, int minClamp = 0, int maxClamp = int.MaxValue) - { - return queryArgs.TryGetValue("limit", out string? limitStr) && int.TryParse(limitStr, out int limit) ? Math.Clamp(limit, minClamp, maxClamp) : @default; - } - - public static async Task<ERRNO> AddBulkAsync<TEntity>(this DbStore<TEntity> store, IEnumerable<TEntity> records, string userId, bool overwriteTime = true) - where TEntity : class, IDbModel, IUserEntity - { - //Open context and transaction - await using TransactionalDbContext database = store.NewContext(); - await database.OpenTransactionAsync(); - //Get the entity set - DbSet<TEntity> set = database.Set<TEntity>(); - //Generate random ids for the feeds and set user-id - foreach (TEntity entity in records) - { - entity.Id = store.RecordIdBuilder; - //Explicitly assign the user-id - entity.UserId = userId; - //If the entity has the default created time, update it, otherwise leave it as is - if (overwriteTime || entity.Created == default) - { - entity.Created = DateTime.UtcNow; - } - //Update last-modified time - entity.LastModified = DateTime.UtcNow; - } - //Add feeds to database - set.AddRange(records); - //Commit changes - ERRNO count = await database.SaveChangesAsync(); - //Commit transaction and exit - await database.CommitTransactionAsync(); - return count; - } - } -} diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Extensions/BulkExtensions.cs b/lib/VNLib.Plugins.Extensions.Data/src/Extensions/BulkExtensions.cs new file mode 100644 index 0000000..f14fdc6 --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Data/src/Extensions/BulkExtensions.cs @@ -0,0 +1,44 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: Extensions.cs +* +* Extensions.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.Data 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. +* +* VNLib.Plugins.Extensions.Data 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.Collections.Generic; + + +namespace VNLib.Plugins.Extensions.Data.Extensions +{ + public static class BulkExtensions + { + + public static int GetPageOrDefault(this IReadOnlyDictionary<string, string> queryArgs, int @default, int minClamp = 0, int maxClamp = int.MaxValue) + { + return queryArgs.TryGetValue("page", out string? pageStr) && int.TryParse(pageStr, out int page) ? Math.Clamp(page, minClamp, maxClamp) : @default; + } + + public static int GetLimitOrDefault(this IReadOnlyDictionary<string, string> queryArgs, int @default, int minClamp = 0, int maxClamp = int.MaxValue) + { + return queryArgs.TryGetValue("limit", out string? limitStr) && int.TryParse(limitStr, out int limit) ? Math.Clamp(limit, minClamp, maxClamp) : @default; + } + } +} diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Extensions/DbStoreExtensions.cs b/lib/VNLib.Plugins.Extensions.Data/src/Extensions/DbStoreExtensions.cs new file mode 100644 index 0000000..e053900 --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Data/src/Extensions/DbStoreExtensions.cs @@ -0,0 +1,529 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: DbStore.cs +* +* DbStore.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.Data 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. +* +* VNLib.Plugins.Extensions.Data 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.Linq; +using System.Threading; +using System.Transactions; +using System.Threading.Tasks; +using System.Collections.Generic; + +using Microsoft.EntityFrameworkCore; + +using VNLib.Utils; +using VNLib.Plugins.Extensions.Data.Abstractions; + +namespace VNLib.Plugins.Extensions.Data.Extensions +{ + /// <summary> + /// Extension methods for <see cref="IDataStore{T}"/> to add additional functionality + /// </summary> + public static class DbStoreExtensions + { + /// <summary> + /// Updates an entry in the store if it exists, or creates a new entry if one does not already exist + /// </summary> + /// <param name="store"></param> + /// <param name="record">The record to add to the store</param> + /// <param name="cancellation">A cancellation token to cancel the operation</param> + /// <returns>A task the resolves the result of the operation</returns> + public static async Task<ERRNO> AddOrUpdateAsync<T>(this IDataStore<T> store, T record, CancellationToken cancellation = default) + where T : class, IDbModel + { + //Open new db context + await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadCommitted, cancellation); + + IQueryable<T> query; + + if (string.IsNullOrWhiteSpace(record.Id)) + { + //Get the application + query = store.QueryTable.AddOrUpdateQueryBuilder(ctx, record); + } + else + { + //Get the application + query = (from et in ctx.Set<T>() + where et.Id == record.Id + select et); + } + + //Using single + T? entry = await query.SingleOrDefaultAsync(cancellation); + + //Check if creted + if (entry == null) + { + //Create a new template id + record.Id = store.GetNewRecordId(); + //Set the created/lm times + record.Created = record.LastModified = DateTime.UtcNow; + //Add the new template to the ctx + ctx.Add(record); + } + else + { + store.OnRecordUpdate(record, entry); + } + + return await ctx.SaveAndCloseAsync(true, cancellation); + } + + /// <summary> + /// Updates an entry in the store with the specified record + /// </summary> + /// <param name="store"></param> + /// <param name="record">The record to update</param> + /// <param name="cancellation">A cancellation token to cancel the operation</param> + /// <returns>A task the resolves an error code (should evaluate to false on failure, and true on success)</returns> + public static async Task<ERRNO> UpdateAsync<T>(this IDataStore<T> store, T record, CancellationToken cancellation = default) + where T : class, IDbModel + { + //Open new db context + await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.Serializable, cancellation); + + //Get the application + IQueryable<T> query = store.QueryTable.UpdateQueryBuilder(ctx, record); + + //Using single to make sure only one app is in the db (should never be an issue) + T? oldEntry = await query.SingleOrDefaultAsync(cancellation); + + if (oldEntry == null) + { + return false; + } + + //Update the template meta-data + store.OnRecordUpdate(record, oldEntry); + + return await ctx.SaveAndCloseAsync(true, cancellation); + } + + /// <summary> + /// Creates a new entry in the store representing the specified record + /// </summary> + /// <param name="store"></param> + /// <param name="record">The record to add to the store</param> + /// <param name="cancellation">A cancellation token to cancel the operation</param> + /// <returns>A task the resolves an error code (should evaluate to false on failure, and true on success)</returns> + public static async Task<ERRNO> CreateAsync<T>(this IDataStore<T> store, T record, CancellationToken cancellation = default) + where T : class, IDbModel + { + //Open new db context + await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted, cancellation); + + //Create a new template id + record.Id = store.GetNewRecordId(); + + //Update the created/last modified time of the record + record.Created = record.LastModified = DateTime.UtcNow; + + //Add the new template + ctx.Add(record); + + return await ctx.SaveAndCloseAsync(true, cancellation); + } + + + /// <summary> + /// Gets the total number of records in the current store + /// </summary> + /// <param name="store"></param> + /// <param name="cancellation">A cancellation token to cancel the operation</param> + /// <returns>A task that resolves the number of records in the store</returns> + public static async Task<long> GetCountAsync<T>(this IDataStore<T> store, CancellationToken cancellation = default) + where T : class, IDbModel + { + //Open db connection + await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted, cancellation); + + //Async get the number of records of the given entity type + long count = await ctx.Set<T>().LongCountAsync(cancellation); + + //close db and transaction + await ctx.SaveAndCloseAsync(true, cancellation); + + return count; + } + + /// <summary> + /// Gets the number of records that belong to the specified constraint + /// </summary> + /// <param name="store"></param> + /// <param name="specifier">A specifier to constrain the reults</param> + /// <param name="cancellation">A cancellation token to cancel the operation</param> + /// <returns>The number of records that belong to the specifier</returns> + public static async Task<long> GetCountAsync<T>(this IDataStore<T> store, string specifier, CancellationToken cancellation = default) + where T : class, IDbModel + { + await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted, cancellation); + + //Async get the number of records of the given entity type + long count = await store.QueryTable.GetCountQueryBuilder(ctx, specifier).LongCountAsync(cancellation); + + //close db and transaction + await ctx.SaveAndCloseAsync(true, cancellation); + + return count; + } + + + /// <summary> + /// Gets a record from its key + /// </summary> + /// <param name="store"></param> + /// <param name="key">The key identifying the unique record</param> + /// <param name="cancellation">A cancellation token to cancel the operation</param> + /// <returns>A promise that resolves the record identified by the specified key</returns> + public static async Task<T?> GetSingleAsync<T>(this IDataStore<T> store, string key, CancellationToken cancellation = default) + where T : class, IDbModel + { + //Open db connection + await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted, cancellation); + + //Get the single template by its id + T? record = await (from entry in ctx.Set<T>() + where entry.Id == key + select entry) + .AsNoTracking() + .SingleOrDefaultAsync(cancellation); + + //close db and transaction + await ctx.SaveAndCloseAsync(true, cancellation); + return record; + } + + /// <summary> + /// Gets a record identified by it's id + /// </summary> + /// <param name="store"></param> + /// <param name="specifiers">A variable length specifier arguemnt array for retreiving a single application</param> + /// <returns>A task that resolves the entity if it exists</returns> + public static async Task<T?> GetSingleAsync<T>(this IDataStore<T> store, params string[] specifiers) + where T : class, IDbModel + { + //Open db connection + await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted); + + //Get the single item by specifiers + T? record = await store.QueryTable.GetSingleQueryBuilder(ctx, specifiers).SingleOrDefaultAsync(); + + //close db and transaction + await ctx.SaveAndCloseAsync(true); + + return record; + } + + /// <summary> + /// Gets a record from the store with a partial model, intended to complete the model + /// </summary> + /// <param name="store"></param> + /// <param name="record">The partial model used to query the store</param> + /// <param name="cancellation">A cancellation token to cancel the operation</param> + /// <returns>A task the resolves the completed data-model</returns> + public static async Task<T?> GetSingleAsync<T>(this IDataStore<T> store, T record, CancellationToken cancellation = default) + where T : class, IDbModel + { + //Open db connection + await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted, cancellation); + + //Get the single template by its id + T? entry = await store.QueryTable.GetSingleQueryBuilder(ctx, record).SingleOrDefaultAsync(cancellation); + + //close db and transaction + await ctx.SaveAndCloseAsync(true, cancellation); + + return record; + } + + + /// <summary> + /// Fills a collection with enires retireved from the store using the specifer + /// </summary> + /// <param name="store"></param> + /// <param name="collection">The collection to add entires to</param> + /// <param name="specifier">A specifier argument to constrain results</param> + /// <param name="limit">The maximum number of elements to retrieve</param> + /// <param name="cancellation">A cancellation token to cancel the operation</param> + /// <returns>A Task the resolves to the number of items added to the collection</returns> + public static async Task<ERRNO> GetCollectionAsync<T>(this IDataStore<T> store, ICollection<T> collection, string specifier, int limit, CancellationToken cancellation = default) + where T : class, IDbModel + { + int previous = collection.Count; + + //Open new db context + await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted, cancellation); + + //Get the single template by its id + await store.QueryTable.GetCollectionQueryBuilder(ctx, specifier) + .Take(limit) + .Select(static e => e) + .AsNoTracking() + .ForEachAsync(collection.Add, cancellation); + + //close db and transaction + _ = await ctx.SaveAndCloseAsync(true, cancellation); + + //Return the number of elements add to the collection + return collection.Count - previous; + } + + /// <summary> + /// Fills a collection with enires retireved from the store using a variable length specifier + /// parameter + /// </summary> + /// <param name="store"></param> + /// <param name="collection">The collection to add entires to</param> + /// <param name="limit">The maximum number of elements to retrieve</param> + /// <param name="args"></param> + /// <returns>A Task the resolves to the number of items added to the collection</returns> + public static async Task<ERRNO> GetCollectionAsync<T>(this IDataStore<T> store, ICollection<T> collection, int limit, params string[] args) + where T : class, IDbModel + { + int previous = collection.Count; + + //Open new db context + await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted); + + //Get the single template by its id + await store.QueryTable.GetCollectionQueryBuilder(ctx, args) + .Take(limit) + .Select(static e => e) + .AsNoTracking() + .ForEachAsync(collection.Add); + + //close db and transaction + _ = await ctx.SaveAndCloseAsync(true); + + //Return the number of elements add to the collection + return collection.Count - previous; + } + + + /// <summary> + /// Deletes one or more entrires from the store matching the specified record + /// </summary> + /// <param name="store"></param> + /// <param name="record">The record to remove from the store</param> + /// <param name="cancellation">A cancellation token to cancel the operation</param> + /// <returns>A task the resolves the number of records removed(should evaluate to false on failure, and deleted count on success)</returns> + public static async Task<ERRNO> DeleteAsync<T>(this IDataStore<T> store, T record, CancellationToken cancellation = default) + where T : class, IDbModel + { + //Open new db context + await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.RepeatableRead, cancellation); + + //Get a query for a a single item + IQueryable<T> query = store.QueryTable.GetSingleQueryBuilder(ctx, record); + + //Get the entry if it exists + T? entry = await query.SingleOrDefaultAsync(cancellation); + + if (entry == null) + { + await ctx.SaveAndCloseAsync(false, cancellation); + return false; + } + else + { + //Remove the entry + ctx.Remove(entry); + return await ctx.SaveAndCloseAsync(true, cancellation); + } + } + + /// <summary> + /// Deletes one or more entires from the store matching the specified unique key + /// </summary> + /// <param name="store"></param> + /// <param name="key">The unique key that identifies the record</param> + /// <param name="cancellation">A cancellation token to cancel the operation</param> + /// <returns>A task the resolves the number of records removed(should evaluate to false on failure, and deleted count on success)</returns> + public static async Task<ERRNO> DeleteAsync<T>(this IDataStore<T> store, string key, CancellationToken cancellation = default) + where T : class, IDbModel + { + //Open new db context + await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.RepeatableRead, cancellation); + + //Get a query for a a single item + IQueryable<T> query = store.QueryTable.GetSingleQueryBuilder(ctx, key); + + //Get the entry if it exists + T? entry = await query.SingleOrDefaultAsync(cancellation); + + if (entry == null) + { + await ctx.SaveAndCloseAsync(false, cancellation); + return false; + } + else + { + //Remove the entry + ctx.Remove(entry); + return await ctx.SaveAndCloseAsync(true, cancellation); + } + } + + /// <summary> + /// Deletes one or more entires from the store matching the supplied specifiers + /// </summary> + /// <param name="store"></param> + /// <param name="specifiers">A variable length array of specifiers used to delete one or more entires</param> + /// <returns>A task the resolves the number of records removed(should evaluate to false on failure, and deleted count on success)</returns> + public static async Task<ERRNO> DeleteAsync<T>(this IDataStore<T> store, params string[] specifiers) + where T : class, IDbModel + { + //Open new db context + await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.RepeatableRead); + + //Get the template by its id + IQueryable<T> query = store.QueryTable.DeleteQueryBuilder(ctx, specifiers); + + T? entry = await query.SingleOrDefaultAsync(); + + if (entry == null) + { + return false; + } + + //Add the new application + ctx.Remove(entry); + + return await ctx.SaveAndCloseAsync(true); + } + + + /// <summary> + /// Gets a collection of records using a pagination style query, and adds the records to the collecion + /// </summary> + /// <param name="store"></param> + /// <param name="collection">The collection to add records to</param> + /// <param name="page">Pagination page to get records from</param> + /// <param name="limit">The maximum number of items to retrieve from the store</param> + /// <param name="cancellation">A cancellation token to cancel the operation</param> + /// <returns>A task that resolves the number of items added to the collection</returns> + public static async Task<int> GetPageAsync<T>(this IDataStore<T> store, ICollection<T> collection, int page, int limit, CancellationToken cancellation = default) + where T : class, IDbModel + { + //Store preivous count + int previous = collection.Count; + + //Open db connection + await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted, cancellation); + + //Get a page offset and a limit for the + await ctx.Set<T>() + .Skip(page * limit) + .Take(limit) + .Select(static p => p) + .AsNoTracking() + .ForEachAsync(collection.Add, cancellation); + + //close db and transaction + await ctx.SaveAndCloseAsync(true, cancellation); + + //Return the number of records added + return collection.Count - previous; + } + + /// <summary> + /// Gets a collection of records using a pagination style query with constraint arguments, and adds the records to the collecion + /// </summary> + /// <param name="store"></param> + /// <param name="collection">The collection to add records to</param> + /// <param name="page">Pagination page to get records from</param> + /// <param name="limit">The maximum number of items to retrieve from the store</param> + /// <param name="constraints">A params array of strings to constrain the result set from the db</param> + /// <returns>A task that resolves the number of items added to the collection</returns> + public static async Task<int> GetPageAsync<T>(this IDataStore<T> store, ICollection<T> collection, int page, int limit, params string[] constraints) + where T : class, IDbModel + { + //Store preivous count + int previous = collection.Count; + + //Open new db context + await using IDbContextHandle ctx = await store.OpenAsync(IsolationLevel.ReadUncommitted); + + //Get a page of records constrained by the given arguments + await store.QueryTable.GetPageQueryBuilder(ctx, constraints) + .Skip(page * limit) + .Take(limit) + .Select(static e => e) + .AsNoTracking() + .ForEachAsync(collection.Add); + + //close db and transaction + await ctx.SaveAndCloseAsync(true); + + //Return the number of records added + return collection.Count - previous; + } + + + public static Task<ERRNO> AddBulkAsync<T>(this IDataStore<T> store, IEnumerable<T> records, string userId, bool overwriteTime = true, CancellationToken cancellation = default) + where T : class, IDbModel, IUserEntity + { + //Assign user-id when numerated + IEnumerable<T> withUserId = records.Select(p => + { + p.UserId = userId; + return p; + }); + + return store.AddBulkAsync(withUserId, overwriteTime, cancellation); + } + + public static async Task<ERRNO> AddBulkAsync<T>(this IDataStore<T> store, IEnumerable<T> records, bool overwriteTime = true, CancellationToken cancellation = default) + where T : class, IDbModel + { + DateTime now = DateTime.UtcNow; + + //Open context and transaction + await using IDbContextHandle database = await store.OpenAsync(IsolationLevel.ReadCommitted, cancellation); + + //Get the entity set + IQueryable<T> set = database.Set<T>(); + + //Generate random ids for the feeds and set user-id + foreach (T entity in records) + { + entity.Id = store.GetNewRecordId(); + + //If the entity has the default created time, update it, otherwise leave it as is + if (overwriteTime || entity.Created == default) + { + entity.Created = now; + } + + //Update last-modified time + entity.LastModified = now; + } + + //Add bulk items to database + database.AddRange(records); + return await database.SaveAndCloseAsync(true, cancellation); + } + + } +} diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Extensions/DbStoreHelperExtensions.cs b/lib/VNLib.Plugins.Extensions.Data/src/Extensions/DbStoreHelperExtensions.cs new file mode 100644 index 0000000..55230cf --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Data/src/Extensions/DbStoreHelperExtensions.cs @@ -0,0 +1,89 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: DbStoreHelperExtensions.cs +* +* DbStoreHelperExtensions.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.Data 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. +* +* VNLib.Plugins.Extensions.Data 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.Threading; +using System.Transactions; +using System.Threading.Tasks; + +using VNLib.Plugins.Extensions.Data.Abstractions; + +namespace VNLib.Plugins.Extensions.Data.Extensions +{ + internal static class DbStoreHelperExtensions + { + /// <summary> + /// If the current context instance inherits the <see cref="IConcurrentDbContext"/> interface, + /// attempts to open a transaction with the specified isolation level. + /// </summary> + /// <param name="tdb"></param> + /// <param name="isolationLevel">The transaction isolation level</param> + /// <param name="cancellationToken">A token to cancel the operation</param> + /// <returns></returns> + internal static Task OpenTransactionAsync(this ITransactionalDbContext tdb, IsolationLevel isolationLevel, CancellationToken cancellationToken = default) + { + if (tdb is IConcurrentDbContext ccdb) + { + return ccdb.OpenTransactionAsync(isolationLevel, cancellationToken); + } + else + { + //Just ignore the isolation level + return tdb.OpenTransactionAsync(cancellationToken); + } + } + + /// <summary> + /// Opens a new database connection. If the context supports transactions, it will + /// open a transaction with the specified isolation level. + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="store"></param> + /// <param name="level">The transaction isolation level</param> + /// <param name="cancellation">A token to cancel the transaction operation</param> + /// <returns>A task that resolves the new open <see cref="IDbContextHandle"/> </returns> + public static async Task<IDbContextHandle> OpenAsync<T>(this IDataStore<T> store, IsolationLevel level, CancellationToken cancellation = default) + where T : class, IDbModel + { + //Open new db context + IDbContextHandle ctx = store.GetNewContext(); + + //Support transactions and start them if the context supports it + if(ctx is ITransactionalDbContext tdb) + { + try + { + //Open transaction + await tdb.OpenTransactionAsync(level, cancellation); + } + catch + { + await ctx.DisposeAsync(); + throw; + } + } + + return ctx; + } + } +} diff --git a/lib/VNLib.Plugins.Extensions.Data/src/ProtectedEntityExtensions.cs b/lib/VNLib.Plugins.Extensions.Data/src/Extensions/ProtectedEntityExtensions.cs index ec7b4f5..ebb71cf 100644 --- a/lib/VNLib.Plugins.Extensions.Data/src/ProtectedEntityExtensions.cs +++ b/lib/VNLib.Plugins.Extensions.Data/src/Extensions/ProtectedEntityExtensions.cs @@ -22,17 +22,18 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -using System.Linq; using System.Threading; -using System.Transactions; using System.Threading.Tasks; using System.Collections.Generic; using VNLib.Utils; using VNLib.Plugins.Extensions.Data.Abstractions; -namespace VNLib.Plugins.Extensions.Data +namespace VNLib.Plugins.Extensions.Data.Extensions { + /// <summary> + /// Extension methods for <see cref="IDataStore{TEntity}"/> implementations that support user-protected entities + /// </summary> public static class ProtectedEntityExtensions { /// <summary> @@ -43,7 +44,7 @@ namespace VNLib.Plugins.Extensions.Data /// <param name="userId">The userid of the record owner</param> /// <param name="cancellation">A token to cancel the operation</param> /// <returns>A task that evaluates to the number of records modified</returns> - public static Task<ERRNO> UpdateUserRecordAsync<TEntity>(this IDataStore<TEntity> store, TEntity record, string userId, CancellationToken cancellation = default) + public static Task<ERRNO> UpdateUserRecordAsync<TEntity>(this IDataStore<TEntity> store, TEntity record, string userId, CancellationToken cancellation = default) where TEntity : class, IDbModel, IUserEntity { record.UserId = userId; @@ -58,7 +59,7 @@ namespace VNLib.Plugins.Extensions.Data /// <param name="userId">The userid of the record owner</param> /// <param name="cancellation">A token to cancel the operation</param> /// <returns>A task that evaluates to the number of records modified</returns> - public static Task<ERRNO> CreateUserRecordAsync<TEntity>(this IDataStore<TEntity> store, TEntity record, string userId, CancellationToken cancellation = default) + public static Task<ERRNO> CreateUserRecordAsync<TEntity>(this IDataStore<TEntity> store, TEntity record, string userId, CancellationToken cancellation = default) where TEntity : class, IDbModel, IUserEntity { record.UserId = userId; @@ -88,7 +89,7 @@ namespace VNLib.Plugins.Extensions.Data /// <param name="page">The page offset</param> /// <param name="limit">The record limit for the page</param> /// <returns>A task that resolves the number of entities added to the collection</returns> - public static Task<int> GetUserPageAsync<TEntity>(this IPaginatedDataStore<TEntity> store, ICollection<TEntity> collection, string userId, int page, int limit) + public static Task<int> GetUserPageAsync<TEntity>(this IDataStore<TEntity> store, ICollection<TEntity> collection, string userId, int page, int limit) where TEntity : class, IDbModel, IUserEntity { return store.GetPageAsync(collection, page, limit, userId); @@ -120,25 +121,5 @@ namespace VNLib.Plugins.Extensions.Data return store.GetCountAsync(userId, cancellation); } - /// <summary> - /// If the current context instance inherits the <see cref="IConcurrentDbContext"/> interface, - /// attempts to open a transaction with the specified isolation level. - /// </summary> - /// <param name="tdb"></param> - /// <param name="isolationLevel">The transaction isolation level</param> - /// <param name="cancellationToken">A token to cancel the operation</param> - /// <returns></returns> - internal static Task OpenTransactionAsync(this ITransactionalDbContext tdb, IsolationLevel isolationLevel, CancellationToken cancellationToken = default) - { - if(tdb is IConcurrentDbContext ccdb) - { - return ccdb.OpenTransactionAsync(isolationLevel, cancellationToken); - } - else - { - //Just ignore the isolation level - return tdb.OpenTransactionAsync(cancellationToken); - } - } } } diff --git a/lib/VNLib.Plugins.Extensions.Data/src/ProtectedDbStore.cs b/lib/VNLib.Plugins.Extensions.Data/src/ProtectedDbStore.cs deleted file mode 100644 index 8e85cbb..0000000 --- a/lib/VNLib.Plugins.Extensions.Data/src/ProtectedDbStore.cs +++ /dev/null @@ -1,70 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Data -* File: ProtectedDbStore.cs -* -* ProtectedDbStore.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Extensions.Data 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. -* -* VNLib.Plugins.Extensions.Data 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.Linq; - -using VNLib.Plugins.Extensions.Data.Abstractions; - -namespace VNLib.Plugins.Extensions.Data -{ -#nullable enable - /// <summary> - /// A data store that provides unique identities and protections based on an entity that has an owner <see cref="IUserEntity"/> - /// </summary> - public abstract class ProtectedDbStore<TEntity> : DbStore<TEntity> where TEntity : class, IDbModel, IUserEntity - { - ///<inheritdoc/> - protected override IQueryable<TEntity> GetCollectionQueryBuilder(TransactionalDbContext context, params string[] constraints) - { - string userId = constraints[0]; - //Query items for the user and its id - return from item in context.Set<TEntity>() - where item.UserId == userId - orderby item.Created descending - select item; - } - - /// <summary> - /// Gets a single item contrained by a given user-id and item id - /// </summary> - /// <param name="context"></param> - /// <param name="constraints"></param> - /// <returns></returns> - protected override IQueryable<TEntity> GetSingleQueryBuilder(TransactionalDbContext context, params string[] constraints) - { - string key = constraints[0]; - string userId = constraints[1]; - //Query items for the user and its id - return from item in context.Set<TEntity>() - where item.Id == key && item.UserId == userId - select item; - } - ///<inheritdoc/> - protected override IQueryable<TEntity> GetSingleQueryBuilder(TransactionalDbContext context, TEntity record) - { - return this.GetSingleQueryBuilder(context, record.Id, record.UserId); - } - } -} diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWDecriptorCreationException.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/LWDecriptorCreationException.cs index 7ea50d0..7ea50d0 100644 --- a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWDecriptorCreationException.cs +++ b/lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/LWDecriptorCreationException.cs diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageRemoveFailedException.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/LWStorageRemoveFailedException.cs index d91019c..d91019c 100644 --- a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageRemoveFailedException.cs +++ b/lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/LWStorageRemoveFailedException.cs diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageUpdateFailedException.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/LWStorageUpdateFailedException.cs index f13792d..f13792d 100644 --- a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageUpdateFailedException.cs +++ b/lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/LWStorageUpdateFailedException.cs diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/UndefinedBlobStateException.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/UndefinedBlobStateException.cs index 8b4d81b..8b4d81b 100644 --- a/lib/VNLib.Plugins.Extensions.Data/src/Storage/UndefinedBlobStateException.cs +++ b/lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/UndefinedBlobStateException.cs diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/FsExtensions.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/FsExtensions.cs new file mode 100644 index 0000000..8f5c099 --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Data/src/Storage/FsExtensions.cs @@ -0,0 +1,72 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: FsExtensions.cs +* +* FsExtensions.cs is part of VNLib.Plugins.Extensions.Data which is part +* of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.Data 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. +* +* VNLib.Plugins.Extensions.Data 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.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace VNLib.Plugins.Extensions.Data.Storage +{ + /// <summary> + /// Contains filesystem extension methods + /// </summary> + public static class FsExtensions + { + /// <summary> + /// Creates a new scope for the given filesystem. All operations will be offset by the given path + /// within the parent filesystem. + /// </summary> + /// <param name="fs"></param> + /// <param name="offsetPath">The base path to prepend to all requests</param> + /// <returns>A new <see cref="ISimpleFilesystem"/> with a new filesystem directory scope</returns> + public static ISimpleFilesystem CreateNewScope(this ISimpleFilesystem fs, string offsetPath) => new FsScope(fs, offsetPath); + + private sealed record class FsScope(ISimpleFilesystem Parent, string OffsetPath) : ISimpleFilesystem + { + public Task DeleteFileAsync(string filePath, CancellationToken cancellation) + { + string path = Path.Combine(OffsetPath, filePath); + return Parent.DeleteFileAsync(path, cancellation); + } + + public string GetExternalFilePath(string filePath) + { + string path = Path.Combine(OffsetPath, filePath); + return Parent.GetExternalFilePath(path); + } + + public Task<long> ReadFileAsync(string filePath, Stream output, CancellationToken cancellation) + { + string path = Path.Combine(OffsetPath, filePath); + return Parent.ReadFileAsync(path, output, cancellation); + } + + public Task SetFileAsync(string filePath, Stream data, string contentType, CancellationToken cancellation) + { + string path = Path.Combine(OffsetPath, filePath); + return Parent.SetFileAsync(path, data, contentType, cancellation); + } + } + } +} diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/ISimpleFilesystem.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/ISimpleFilesystem.cs new file mode 100644 index 0000000..d3dc431 --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Data/src/Storage/ISimpleFilesystem.cs @@ -0,0 +1,72 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: ISimpleFilesystem.cs +* +* ISimpleFilesystem.cs is part of VNLib.Plugins.Extensions.Data which is part +* of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.Data 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. +* +* VNLib.Plugins.Extensions.Data 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.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace VNLib.Plugins.Extensions.Data.Storage +{ + + /// <summary> + /// Represents an opaque storage interface that abstracts simple storage operations + /// ignorant of the underlying storage system. + /// </summary> + public interface ISimpleFilesystem + { + /// <summary> + /// Gets the full public file path for the given relative file path + /// </summary> + /// <param name="filePath">The relative file path of the item to get the full path for</param> + /// <returns>The full relative file path</returns> + string GetExternalFilePath(string filePath); + + /// <summary> + /// Deletes a file from the storage system asynchronously + /// </summary> + /// <param name="filePath">The path to the file to delete</param> + /// <param name="cancellation">A token to cancel the operation</param> + /// <returns>A task that represents and asynchronous work</returns> + Task DeleteFileAsync(string filePath, CancellationToken cancellation); + + /// <summary> + /// Writes a file from the stream to the given file location + /// </summary> + /// <param name="filePath">The path to the file to write to</param> + /// <param name="data">The file data to stream</param> + /// <param name="contentType">The content type of the file to write</param> + /// <param name="cancellation">A token to cancel the operation</param> + /// <returns>A task that represents and asynchronous work</returns> + Task SetFileAsync(string filePath, Stream data, string contentType, CancellationToken cancellation); + + /// <summary> + /// Reads a file from the storage system at the given path asynchronously + /// </summary> + /// <param name="filePath">The file to read</param> + /// <param name="output">The stream to write the file output to</param> + /// <param name="cancellation">A token to cancel the operation</param> + /// <returns>The number of bytes read, -1 if the operation failed</returns> + Task<long> ReadFileAsync(string filePath, Stream output, CancellationToken cancellation); + } +} diff --git a/lib/VNLib.Plugins.Extensions.Data/src/TransactionalDbContext.cs b/lib/VNLib.Plugins.Extensions.Data/src/TransactionalDbContext.cs index e06ac43..cbb9799 100644 --- a/lib/VNLib.Plugins.Extensions.Data/src/TransactionalDbContext.cs +++ b/lib/VNLib.Plugins.Extensions.Data/src/TransactionalDbContext.cs @@ -23,24 +23,31 @@ */ using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; +using System.Collections.Generic; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; +using VNLib.Utils; +using VNLib.Plugins.Extensions.Data.Abstractions; + + namespace VNLib.Plugins.Extensions.Data { /// <summary> /// Abstract implementation of <see cref="ITransactionalDbContext"/> that provides a transactional context for database operations /// </summary> - public abstract class TransactionalDbContext : DbContext, IAsyncDisposable, ITransactionalDbContext + public abstract class TransactionalDbContext : DbContext, IAsyncDisposable, ITransactionalDbContext, IDbContextHandle { /// <summary> /// <inheritdoc/> /// </summary> protected TransactionalDbContext() { } + /// <summary> /// <inheritdoc/> /// </summary> @@ -50,46 +57,83 @@ namespace VNLib.Plugins.Extensions.Data ///<inheritdoc/> public IDbContextTransaction? Transaction { get; set; } + ///<inheritdoc/> + public async Task OpenTransactionAsync(CancellationToken cancellationToken = default) + { + //open a new transaction on the current database + this.Transaction = await base.Database.BeginTransactionAsync(cancellationToken); + } -#pragma warning disable CA1816 // Dispose methods should call SuppressFinalize, ignore because base.Dispose() is called ///<inheritdoc/> - public sealed override void Dispose() + public Task CommitTransactionAsync(CancellationToken token = default) { - //dispose the transaction - Transaction?.Dispose(); - base.Dispose(); + return Transaction != null ? Transaction.CommitAsync(token) : Task.CompletedTask; } ///<inheritdoc/> - public override async ValueTask DisposeAsync() + public Task RollbackTransctionAsync(CancellationToken token = default) { - //If transaction has been created, dispose the transaction - if (Transaction != null) + return Transaction != null ? Transaction.RollbackAsync(token) : Task.CompletedTask; + } + + ///<inheritdoc/> + IQueryable<T> IDbContextHandle.Set<T>() => base.Set<T>(); + + ///<inheritdoc/> + void IDbContextHandle.Add<T>(T entity) => base.Add(entity); + + ///<inheritdoc/> + void IDbContextHandle.Remove<T>(T entity) => base.Remove<T>(entity); + + ///<inheritdoc/> + public virtual void AddRange<T>(IEnumerable<T> entities) where T : class + { + DbSet<T> set = base.Set<T>(); + set.AddRange(entities); + } + + ///<inheritdoc/> + public virtual async Task<ERRNO> SaveAndCloseAsync(bool commit, CancellationToken cancellation = default) + { + //Save db changes + ERRNO result = await base.SaveChangesAsync(cancellation); + if (commit) { - await Transaction.DisposeAsync(); + await CommitTransactionAsync(cancellation); } - await base.DisposeAsync(); + else + { + await RollbackTransctionAsync(cancellation); + } + return result; } -#pragma warning restore CA1816 // Dispose methods should call SuppressFinalize ///<inheritdoc/> - public async Task OpenTransactionAsync(CancellationToken cancellationToken = default) + public virtual void RemoveRange<T>(IEnumerable<T> entities) where T : class { - //open a new transaction on the current database - this.Transaction = await base.Database.BeginTransactionAsync(cancellationToken); + DbSet<T> set = base.Set<T>(); + set.RemoveRange(entities); } +#pragma warning disable CA1816 // Dispose methods should call SuppressFinalize, ignore because base.Dispose() is called ///<inheritdoc/> - public Task CommitTransactionAsync(CancellationToken token = default) + public sealed override void Dispose() { - return Transaction != null ? Transaction.CommitAsync(token) : Task.CompletedTask; + //dispose the transaction + Transaction?.Dispose(); + base.Dispose(); } ///<inheritdoc/> - public Task RollbackTransctionAsync(CancellationToken token = default) + public override async ValueTask DisposeAsync() { - return Transaction != null ? Transaction.RollbackAsync(token) : Task.CompletedTask; + //If transaction has been created, dispose the transaction + if (Transaction != null) + { + await Transaction.DisposeAsync(); + } + await base.DisposeAsync(); } - +#pragma warning restore CA1816 // Dispose methods should call SuppressFinalize } }
\ No newline at end of file diff --git a/lib/VNLib.Plugins.Extensions.Data/src/VNLib.Plugins.Extensions.Data.csproj b/lib/VNLib.Plugins.Extensions.Data/src/VNLib.Plugins.Extensions.Data.csproj index 05a6df2..f89bf00 100644 --- a/lib/VNLib.Plugins.Extensions.Data/src/VNLib.Plugins.Extensions.Data.csproj +++ b/lib/VNLib.Plugins.Extensions.Data/src/VNLib.Plugins.Extensions.Data.csproj @@ -23,14 +23,6 @@ <PackageProjectUrl>https://www.vaughnnugent.com/resources/software/modules/VNLib.Plugins.Extensions</PackageProjectUrl> <RepositoryUrl>https://github.com/VnUgE/VNLib.Plugins.Extensions/tree/master/lib/VNLib.Plugins.Extensions.Data</RepositoryUrl> </PropertyGroup> - - <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> - <Deterministic>False</Deterministic> - </PropertyGroup> - - <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> - <Deterministic>False</Deterministic> - </PropertyGroup> <ItemGroup> <PackageReference Include="ErrorProne.NET.CoreAnalyzers" Version="0.1.2"> @@ -41,7 +33,7 @@ <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.20" /> + <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.21" /> </ItemGroup> <ItemGroup> diff --git a/lib/VNLib.Plugins.Extensions.Loading.Sql/src/SqlDbConnectionLoader.cs b/lib/VNLib.Plugins.Extensions.Loading.Sql/src/SqlDbConnectionLoader.cs index 8775d54..0ea60d6 100644 --- a/lib/VNLib.Plugins.Extensions.Loading.Sql/src/SqlDbConnectionLoader.cs +++ b/lib/VNLib.Plugins.Extensions.Loading.Sql/src/SqlDbConnectionLoader.cs @@ -124,7 +124,7 @@ namespace VNLib.Plugins.Extensions.Loading.Sql sqlBuilder = new MySqlConnectionStringBuilder() { Server = sqlConf["hostname"].GetString(), - Database = sqlConf["database"].GetString(), + Database = sqlConf["catalog"].GetString(), UserID = sqlConf["username"].GetString(), Password = password?.Result.ToString(), Pooling = true, diff --git a/lib/VNLib.Plugins.Extensions.Loading.Sql/src/VNLib.Plugins.Extensions.Loading.Sql.csproj b/lib/VNLib.Plugins.Extensions.Loading.Sql/src/VNLib.Plugins.Extensions.Loading.Sql.csproj index abfbfb6..d8ab72e 100644 --- a/lib/VNLib.Plugins.Extensions.Loading.Sql/src/VNLib.Plugins.Extensions.Loading.Sql.csproj +++ b/lib/VNLib.Plugins.Extensions.Loading.Sql/src/VNLib.Plugins.Extensions.Loading.Sql.csproj @@ -33,8 +33,8 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.20" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.20" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.21" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.21" /> <PackageReference Include="MySqlConnector" Version="2.2.7" /> <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.2" /> </ItemGroup> diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs index adfd997..ccb2341 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs @@ -167,6 +167,52 @@ namespace VNLib.Plugins.Extensions.Loading } /// <summary> + /// Gets a required configuration property from the specified configuration scope + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="config"></param> + /// <param name="property">The name of the property to get</param> + /// <param name="getter">A function to get the value from the json type</param> + /// <returns>The property value</returns> + /// <exception cref="ArgumentNullException"></exception> + public static T? GetProperty<T>(this IConfigScope config, string property, Func<JsonElement, T> getter) + { + //Check null + _ = config ?? throw new ArgumentNullException(nameof(config)); + _ = property ?? throw new ArgumentNullException(nameof(property)); + _ = getter ?? throw new ArgumentNullException(nameof(getter)); + + return !config.TryGetValue(property, out JsonElement el) ? default : getter(el); + } + + /// <summary> + /// Gets a required configuration property from the specified configuration scope + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="config"></param> + /// <param name="property">The name of the property to get</param> + /// <param name="getter">A function to get the value from the json type</param> + /// <returns>The property value</returns> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="KeyNotFoundException"></exception> + public static T GetRequiredProperty<T>(this IConfigScope config, string property, Func<JsonElement, T> getter) + { + //Check null + _ = config ?? throw new ArgumentNullException(nameof(config)); + _ = property ?? throw new ArgumentNullException(nameof(property)); + _ = getter ?? throw new ArgumentNullException(nameof(getter)); + + //Get the property + if(!config.TryGetValue(property, out JsonElement el)) + { + throw new KeyNotFoundException($"Missing required configuration property '{property}'"); + } + + //Even if the getter returns null, throw + return getter(el) ?? throw new KeyNotFoundException($"Missing required configuration property '{property}'"); + } + + /// <summary> /// Gets the configuration property name for the type /// </summary> /// <param name="type">The type to get the configuration name for</param> diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Events/EventManagment.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Events/EventManagment.cs index 57e4a9c..40a52fc 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/Events/EventManagment.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Events/EventManagment.cs @@ -57,7 +57,7 @@ namespace VNLib.Plugins.Extensions.Loading.Events { plugin.ThrowIfUnloaded(); - plugin.Log.Verbose("Interval for {t} scheduled", interval); + plugin.Log.Verbose("Interval for {t} scheduled on type {rr}", interval, asyncCallback.Target); //Run interval on plugins bg scheduler _ = plugin.ObserveWork(() => RunIntervalOnPluginScheduler(plugin, asyncCallback, interval, immediate)); diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/SecretResult.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/SecretResult.cs index f2cbd28..c1e6b3d 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/SecretResult.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/SecretResult.cs @@ -42,7 +42,14 @@ namespace VNLib.Plugins.Extensions.Loading public ReadOnlySpan<char> Result => _secretChars; - internal SecretResult(ReadOnlySpan<char> value) => _secretChars = value.ToArray(); + internal SecretResult(ReadOnlySpan<char> value) : this(value.ToArray()) + { } + + private SecretResult(char[] secretChars) + { + _secretChars = secretChars; + } + ///<inheritdoc/> protected override void Free() @@ -56,5 +63,7 @@ namespace VNLib.Plugins.Extensions.Loading MemoryUtil.UnsafeZeroMemory<char>(result); return res; } + + internal static SecretResult ToSecret(char[] result) => new(result); } } diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/VaultSecrets.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/VaultSecrets.cs index 08af485..c2d830f 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/VaultSecrets.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/VaultSecrets.cs @@ -23,9 +23,11 @@ */ using System; +using System.IO; using System.Linq; using System.Text; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; @@ -62,6 +64,7 @@ namespace VNLib.Plugins.Extensions.Loading public const string VAULT_URL_SCHEME = "vault://"; public const string ENV_URL_SCHEME = "env://"; + public const string FILE_URL_SCHEME = "file://"; /// <summary> @@ -125,11 +128,36 @@ namespace VNLib.Plugins.Extensions.Loading return Task.FromResult<ISecretResult?>(envVal == null ? null : new SecretResult(envVal)); } + + //See if the secret is a file path + if (rawSecret.StartsWith(FILE_URL_SCHEME, StringComparison.OrdinalIgnoreCase)) + { + string filePath = rawSecret[FILE_URL_SCHEME.Length..]; + return GetSecretFromFileAsync(filePath, plugin.UnloadToken); + } //Finally, return the raw value return Task.FromResult<ISecretResult?>(new SecretResult(rawSecret.AsSpan())); } + private static async Task<ISecretResult?> GetSecretFromFileAsync(string filePath, CancellationToken ct) + { + //read the file data + byte[] secretFileData = await File.ReadAllBytesAsync(filePath, ct); + + //recover the character data from the file data + int chars = Encoding.UTF8.GetCharCount(secretFileData); + char[] secretFileChars = new char[chars]; + Encoding.UTF8.GetChars(secretFileData, secretFileChars); + + //Create secret from the file data + SecretResult sr = SecretResult.ToSecret(secretFileChars); + + //Clear file data buffer + MemoryUtil.InitializeBlock(secretFileData.AsSpan()); + return sr; + } + /// <summary> /// Gets a secret at the given vault url (in the form of "vault://[mount-name]/[secret-path]?secret=[secret_name]") /// </summary> diff --git a/lib/VNLib.Plugins.Extensions.Validation/src/VNLib.Plugins.Extensions.Validation.csproj b/lib/VNLib.Plugins.Extensions.Validation/src/VNLib.Plugins.Extensions.Validation.csproj index 1747a7d..8b9b6a1 100644 --- a/lib/VNLib.Plugins.Extensions.Validation/src/VNLib.Plugins.Extensions.Validation.csproj +++ b/lib/VNLib.Plugins.Extensions.Validation/src/VNLib.Plugins.Extensions.Validation.csproj @@ -34,7 +34,7 @@ <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="FluentValidation" Version="11.6.0" /> + <PackageReference Include="FluentValidation" Version="11.7.1" /> </ItemGroup> <ItemGroup> |