diff --git a/.github/workflows/dotnet-ci.yml b/.github/workflows/dotnet-ci.yml index 6cec436..2f4bbd0 100644 --- a/.github/workflows/dotnet-ci.yml +++ b/.github/workflows/dotnet-ci.yml @@ -9,7 +9,7 @@ jobs: - name: Setup .NET Core SDK uses: actions/setup-dotnet@v1 with: - dotnet-version: '6.0.x' + dotnet-version: '7.x' include-prerelease: true - name: Restore working-directory: ./code/ @@ -22,7 +22,7 @@ jobs: - name: Setup .NET Core SDK uses: actions/setup-dotnet@v1 with: - dotnet-version: '6.0.x' + dotnet-version: '7.x' include-prerelease: true - name: Build working-directory: ./code/ diff --git a/VERSION b/VERSION index bf7112c..ed50cc5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.4.0 \ No newline at end of file +2.0.0 \ No newline at end of file diff --git a/code/DarkMatter/DarkMatter.csproj b/code/DarkMatter/DarkMatter.csproj index 13b08bb..64303cf 100644 --- a/code/DarkMatter/DarkMatter.csproj +++ b/code/DarkMatter/DarkMatter.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 enable disable diff --git a/code/DarkMatter/Program.cs b/code/DarkMatter/Program.cs index 5fd4f5e..65a72c0 100644 --- a/code/DarkMatter/Program.cs +++ b/code/DarkMatter/Program.cs @@ -31,10 +31,18 @@ // actual use of UniverseQuery (Gravity g, IList T) = await galaxy.Paged( page: new Q.Page(50), - catalysts: new List + clusters: new List() { - new(nameof(MyObject.Links), "", Operator: Q.Operator.In), - new(nameof(MyObject.Code), "", Where: Q.Where.Or) + new(Catalysts: new List + { + new(nameof(MyObject.Links), "", Operator: Q.Operator.In), + new(nameof(MyObject.Code), "", Where: Q.Where.Or) + }, Where: Q.Where.And), + new(Catalysts: new List + { + new(nameof(MyObject.Name), Operator: Q.Operator.Defined), + new(nameof(MyObject.Description), Operator: Q.Operator.Defined) + }, Where: Q.Where.And) }, columnOptions: new( Names: new List @@ -67,8 +75,7 @@ class MyObject : ICosmicEntity public DateTime AddedOn { get; set; } public DateTime? ModifiedOn { get; set; } - [JsonIgnore] - public string PartitionKey => Code; + [JsonIgnore] public string PartitionKey => Code; public string Code { get; set; } diff --git a/code/Universe/Galaxy.cs b/code/Universe/Galaxy.cs index f3eb587..5407324 100644 --- a/code/Universe/Galaxy.cs +++ b/code/Universe/Galaxy.cs @@ -4,13 +4,13 @@ namespace Universe; /// Inherit repositories to implement Universe -public abstract class Galaxy : IDisposable, IGalaxy where T : ICosmicEntity +public abstract class Galaxy : IDisposable, IGalaxy where T : class, ICosmicEntity { - private readonly Container Container; - private bool DisposedValue; + private readonly Container _container; + private bool _disposedValue; - private readonly bool RecordQuery; - private readonly bool AllowBulk; + private readonly bool _recordQuery; + private readonly bool _allowBulk; /// protected Galaxy(CosmosClient client, string database, string container, string partitionKey, bool recordQueries = false) @@ -18,39 +18,31 @@ protected Galaxy(CosmosClient client, string database, string container, string if (string.IsNullOrWhiteSpace(container) || string.IsNullOrWhiteSpace(partitionKey)) throw new UniverseException("Container name and PartitionKey are required"); - RecordQuery = recordQueries; + _recordQuery = recordQueries; if (client.ClientOptions is not null) - AllowBulk = client.ClientOptions.AllowBulkExecution; - Container = client.GetDatabase(database).CreateContainerIfNotExistsAsync(container, partitionKey).GetAwaiter().GetResult(); + _allowBulk = client.ClientOptions.AllowBulkExecution; + _container = client.GetDatabase(database).CreateContainerIfNotExistsAsync(container, partitionKey).GetAwaiter().GetResult(); } - private static QueryDefinition CreateQuery(IList catalysts, ColumnOptions? columnOptions = null, IList sorting = null, IList groups = null) + private static QueryDefinition CreateQuery(IList clusters, ColumnOptions? columnOptions = null, IList sorting = null, IList groups = null) { - // Validate Catalysts - if (catalysts is not null && catalysts.Any() && catalysts.Any(c => c.RuleViolations().Any())) - { - List> violationsPerCatalyst = catalysts.Select(c => c.RuleViolations()).ToList(); - List violations = violationsPerCatalyst.SelectMany(v => v).ToList().Distinct().ToList(); - throw new UniverseException(string.Join(Environment.NewLine, violations)); - } - // Column Options Builder string columnsInQuery = "*"; if (columnOptions is not null) { - if (columnOptions?.Names is not null && columnOptions?.Names.Count > 0) - columnsInQuery = string.Join(", ", columnOptions?.Names.Select(c => $"c.{c}").ToList()); + if (columnOptions.Value.Names is not null && columnOptions.Value.Names.Count > 0) + columnsInQuery = string.Join(", ", columnOptions.Value.Names.Select(c => $"c.{c}").ToList()); - if ((columnOptions?.Top ?? 0) > 0) - columnsInQuery = $"TOP {columnOptions?.Top ?? 1} {columnsInQuery}"; + if ((columnOptions.Value.Top) > 0) + columnsInQuery = $"TOP {columnOptions.Value.Top} {columnsInQuery}"; - if (columnOptions?.IsDistinct ?? false) + if (columnOptions.Value.IsDistinct) columnsInQuery = $"DISTINCT {columnsInQuery}"; - if (columnOptions?.Count ?? false) + if (columnOptions.Value.Count) { groups ??= new List(); - groups = groups.Concat(columnOptions?.Names ?? new List()).Distinct().ToList(); + groups = groups.Concat(columnOptions.Value.Names ?? new List()).Distinct().ToList(); columnsInQuery = $"{columnsInQuery}, COUNT(1) Count"; } } @@ -60,16 +52,43 @@ private static QueryDefinition CreateQuery(IList catalysts, ColumnOpti throw new UniverseException("ORDER BY is not supported in presence of GROUP BY"); // Update Columns Builder with Group By - if (columnsInQuery.Contains("*") && groups is not null && groups.Any()) - columnsInQuery.Replace("*", string.Join(", ", groups.Select(c => $"c.{c}").ToList())); + if (columnsInQuery.Contains('*') && groups is not null && groups.Any()) + _ = columnsInQuery.Replace("*", string.Join(", ", groups.Select(c => $"c.{c}").ToList())); - // Where Clause Builder StringBuilder queryBuilder = new($"SELECT {columnsInQuery} FROM c"); - if (catalysts.Any()) + + // Validate Clusters + if (clusters is not null && clusters.Any(c => c.Catalysts is null || !c.Catalysts.Any())) + throw new UniverseException("Catalysts inside of a Cluster must not be null or empty."); + + // Construct Where Clause by Clusters + if (clusters is not null) { - queryBuilder.Append($" WHERE {WhereClauseBuilder(catalysts[0])}"); - foreach (Catalyst catalyst in catalysts.Where(p => p.Column != catalysts[0].Column).ToList()) - queryBuilder.Append($" {catalyst.Where.Value()} {WhereClauseBuilder(catalyst)}"); + foreach (Cluster cluster in clusters) + { + // Validate Catalysts + if (cluster.Catalysts.Any(c => c.RuleViolations().Any())) + { + List> violationsPerCatalyst = cluster.Catalysts.Select(c => c.RuleViolations()).ToList(); + List violations = violationsPerCatalyst.SelectMany(v => v).ToList().Distinct().ToList(); + throw new UniverseException(string.Join(Environment.NewLine, violations)); + } + + // Add the where statement if not yet present + if (clusters.IndexOf(cluster) == 0) + queryBuilder.Append(" WHERE ("); + else queryBuilder.Append($" {cluster.Where.Value()} ("); + + // Where Clause Builder + foreach (Catalyst catalyst in cluster.Catalysts) + { + if (cluster.Catalysts.IndexOf(catalyst) == 0) + queryBuilder.Append(WhereClauseBuilder(catalyst)); + else queryBuilder.Append($" {catalyst.Where.Value()} {WhereClauseBuilder(catalyst)}"); + } + + queryBuilder.Append(')'); + } } // Sorting Builder @@ -90,12 +109,13 @@ private static QueryDefinition CreateQuery(IList catalysts, ColumnOpti // Parameters Builder QueryDefinition query = new(queryBuilder.ToString()); - if (!catalysts.Any()) - return query; - query = query.WithParameter($"@{catalysts[0].ParameterName()}", catalysts[0].Value); - foreach (Catalyst catalyst in catalysts.Where(p => p.Column != catalysts[0].Column).ToList()) - query = query.WithParameter($"@{catalyst.ParameterName()}", catalyst.Value); + if (clusters is not null && clusters.Any()) + { + query = clusters.SelectMany(cluster => cluster.Catalysts) + .Aggregate(query, (current, catalyst) => + current.WithParameter($"@{catalyst.ParameterName()}", catalyst.Value)); + } return query; @@ -115,37 +135,30 @@ private static QueryDefinition CreateQuery(IList catalysts, ColumnOpti model.id = Guid.NewGuid().ToString(); model.AddedOn = DateTime.UtcNow; - ItemResponse response = await Container.CreateItemAsync(model, new PartitionKey(model.PartitionKey)); + ItemResponse response = await _container.CreateItemAsync(model, new PartitionKey(model.PartitionKey)); return (new(response.RequestCharge, null), model.id); } async Task IGalaxy.Create(IList models) { - try - { - if (!AllowBulk) - throw new UniverseException("Bulk create of documents is not configured properly."); - - Gravity gravity = new(0, string.Empty); - List tasks = new(models.Count); + if (!_allowBulk) + throw new UniverseException("Bulk create of documents is not configured properly."); - foreach (T model in models) - { - if (string.IsNullOrWhiteSpace(model.id)) - model.id = Guid.NewGuid().ToString(); - model.AddedOn = DateTime.UtcNow; + Gravity gravity = new(0, string.Empty); + List tasks = new(models.Count); - tasks.Add(Container.CreateItemAsync(model, new PartitionKey(model.PartitionKey)) - .ContinueWith(response => gravity = new(gravity.RU + response.Result.RequestCharge, string.Empty))); - } - - await Task.WhenAll(tasks); - return gravity; - } - catch + foreach (T model in models) { - throw; + if (string.IsNullOrWhiteSpace(model.id)) + model.id = Guid.NewGuid().ToString(); + model.AddedOn = DateTime.UtcNow; + + tasks.Add(_container.CreateItemAsync(model, new PartitionKey(model.PartitionKey)) + .ContinueWith(response => gravity = new(gravity.RU + response.Result.RequestCharge, string.Empty))); } + + await Task.WhenAll(tasks); + return gravity; } async Task<(Gravity, T)> IGalaxy.Modify(T model) @@ -154,7 +167,7 @@ async Task IGalaxy.Create(IList models) { model.ModifiedOn = DateTime.UtcNow; - ItemResponse response = await Container.ReplaceItemAsync(model, model.id, new PartitionKey(model.PartitionKey)); + ItemResponse response = await _container.ReplaceItemAsync(model, model.id, new PartitionKey(model.PartitionKey)); return (new(response.RequestCharge, null), response.Resource); } catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound) @@ -171,7 +184,7 @@ async Task IGalaxy.Modify(IList models) { try { - if (!AllowBulk) + if (!_allowBulk) throw new UniverseException("Bulk modify of documents is not configured properly."); Gravity gravity = new(0, string.Empty); @@ -181,11 +194,11 @@ async Task IGalaxy.Modify(IList models) { model.ModifiedOn = DateTime.UtcNow; - tasks.Add(Container.ReplaceItemAsync(model, model.id, new PartitionKey(model.PartitionKey)) + tasks.Add(_container.ReplaceItemAsync(model, model.id, new PartitionKey(model.PartitionKey)) .ContinueWith(response => { if (!response.IsCompletedSuccessfully) - throw new UniverseException(response.Exception.Flatten().InnerException.Message); + throw new UniverseException(response.Exception?.Flatten().InnerException?.Message ?? "Oops! Something went wrong!"); gravity = new(gravity.RU + response.Result.RequestCharge, string.Empty); })); @@ -194,7 +207,11 @@ async Task IGalaxy.Modify(IList models) await Task.WhenAll(tasks); return gravity; } - catch + catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound) + { + throw new UniverseException($"Something went wrong doing the bulk operation. See error: {ex.Message}"); + } + catch (CosmosException ex) when (ex.StatusCode != HttpStatusCode.NotFound) { throw; } @@ -204,7 +221,7 @@ async Task IGalaxy.Remove(string id, string partitionKey) { try { - ItemResponse response = await Container.DeleteItemAsync(id, new PartitionKey(partitionKey)); + ItemResponse response = await _container.DeleteItemAsync(id, new PartitionKey(partitionKey)); return new(response.RequestCharge, null); } catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound) @@ -221,7 +238,7 @@ async Task IGalaxy.Remove(string id, string partitionKey) { try { - ItemResponse response = await Container.ReadItemAsync(id, new PartitionKey(partitionKey)); + ItemResponse response = await _container.ReadItemAsync(id, new PartitionKey(partitionKey)); return (new(response.RequestCharge, null), response.Resource); } catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound) @@ -236,37 +253,20 @@ async Task IGalaxy.Remove(string id, string partitionKey) async Task<(Gravity, T)> GetOneFromQuery(QueryDefinition query) { - using FeedIterator queryResponse = Container.GetItemQueryIterator(query); + using FeedIterator queryResponse = _container.GetItemQueryIterator(query); if (queryResponse.HasMoreResults) { FeedResponse next = await queryResponse.ReadNextAsync(); - return (new(next.RequestCharge, null, RecordQuery ? (query.QueryText, query.GetQueryParameters()) : default), next.Any() ? next.Resource.FirstOrDefault() : default); + return (new(next.RequestCharge, null, _recordQuery ? (query.QueryText, query.GetQueryParameters()) : default), next.Any() ? next.Resource.FirstOrDefault() : default); } else return new(new(0, null), default); } - async Task<(Gravity, T)> IGalaxy.Get(Catalyst catalyst, IList columns) - { - try - { - QueryDefinition query = CreateQuery(catalysts: new[] { catalyst }, columnOptions: columns is null || !columns.Any() ? null : new(columns)); - return await GetOneFromQuery(query); - } - catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound) - { - throw new UniverseException($"{typeof(T).Name} does not exist."); - } - catch (CosmosException ex) when (ex.StatusCode != HttpStatusCode.NotFound) - { - throw; - } - } - - async Task<(Gravity, T)> IGalaxy.Get(IList catalysts, IList columns) + async Task<(Gravity, T)> IGalaxy.Get(IList clusters, IList columns) { try { - QueryDefinition query = CreateQuery(catalysts: catalysts, columnOptions: columns is null || !columns.Any() ? null : new(columns)); + QueryDefinition query = CreateQuery(clusters, columnOptions: columns is null || !columns.Any() ? null : new(columns)); return await GetOneFromQuery(query); } catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound) @@ -283,7 +283,7 @@ async Task IGalaxy.Remove(string id, string partitionKey) { double requestCharge = 0; List collection = new(); - using FeedIterator queryResponse = Container.GetItemQueryIterator(query); + using FeedIterator queryResponse = _container.GetItemQueryIterator(query); while (queryResponse.HasMoreResults) { FeedResponse next = await queryResponse.ReadNextAsync(); @@ -291,27 +291,14 @@ async Task IGalaxy.Remove(string id, string partitionKey) requestCharge += next.RequestCharge; } - return (new(requestCharge, null, RecordQuery ? (query.QueryText, query.GetQueryParameters()) : default), collection); - } - - async Task<(Gravity, IList)> IGalaxy.List(Catalyst catalyst, ColumnOptions? columnOptions, IList sorting, IList group) - { - try - { - QueryDefinition query = CreateQuery(catalysts: new[] { catalyst }, columnOptions: columnOptions, sorting: sorting, groups: group); - return await GetListFromQuery(query); - } - catch (CosmosException ex) when (ex.StatusCode != HttpStatusCode.NotFound) - { - throw; - } + return (new(requestCharge, null, _recordQuery ? (query.QueryText, query.GetQueryParameters()) : default), collection); } - async Task<(Gravity, IList)> IGalaxy.List(IList catalysts, ColumnOptions? columnOptions, IList sorting, IList group) + async Task<(Gravity, IList)> IGalaxy.List(IList clusters, ColumnOptions? columnOptions, IList sorting, IList group) { try { - QueryDefinition query = CreateQuery(catalysts: catalysts, columnOptions: columnOptions, sorting: sorting, groups: group); + QueryDefinition query = CreateQuery(clusters: clusters, columnOptions: columnOptions, sorting: sorting, groups: group); return await GetListFromQuery(query); } catch (CosmosException ex) when (ex.StatusCode != HttpStatusCode.NotFound) @@ -320,16 +307,16 @@ async Task IGalaxy.Remove(string id, string partitionKey) } } - async Task<(Gravity, IList)> IGalaxy.Paged(Q.Page page, IList catalysts, ColumnOptions? columnOptions, IList sorting, IList group) + async Task<(Gravity, IList)> IGalaxy.Paged(Q.Page page, IList clusters, ColumnOptions? columnOptions, IList sorting, IList group) { try { - QueryDefinition query = CreateQuery(catalysts: catalysts, columnOptions: columnOptions, sorting: sorting, groups: group); + QueryDefinition query = CreateQuery(clusters: clusters, columnOptions: columnOptions, sorting: sorting, groups: group); double requestUnit = 0; string continuationToken = string.Empty; List collection = new(); - using FeedIterator queryResponse = Container.GetItemQueryIterator(query, + using FeedIterator queryResponse = _container.GetItemQueryIterator(query, requestOptions: new() { MaxItemCount = page.Size }, continuationToken: string.IsNullOrWhiteSpace(page.ContinuationToken) ? null : page.ContinuationToken ); @@ -345,7 +332,7 @@ async Task IGalaxy.Remove(string id, string partitionKey) } } - return (new(requestUnit, continuationToken, RecordQuery ? (query.QueryText, query.GetQueryParameters()) : default), collection); + return (new(requestUnit, continuationToken, _recordQuery ? (query.QueryText, query.GetQueryParameters()) : default), collection); } catch (CosmosException ex) when (ex.StatusCode != HttpStatusCode.NotFound) { @@ -356,12 +343,12 @@ async Task IGalaxy.Remove(string id, string partitionKey) /// protected virtual void Dispose(bool disposing) { - if (DisposedValue) return; + if (_disposedValue) return; if (disposing) { } - DisposedValue = true; + _disposedValue = true; } /// diff --git a/code/Universe/Interfaces/IGalaxy.cs b/code/Universe/Interfaces/IGalaxy.cs index c4f7ba1..b2bb62c 100644 --- a/code/Universe/Interfaces/IGalaxy.cs +++ b/code/Universe/Interfaces/IGalaxy.cs @@ -38,25 +38,15 @@ public interface IGalaxy where T : ICosmicEntity /// /// Get one model from the database /// - Task<(Gravity g, T T)> Get(Catalyst catalyst, IList columns = null); + Task<(Gravity g, T T)> Get(IList clusters, IList columns = null); /// - /// Get one model from the database - /// - Task<(Gravity g, T T)> Get(IList catalysts, IList columns = null); - - /// - /// Get a paginated list from the database - /// - Task<(Gravity g, IList T)> List(Catalyst catalyst, ColumnOptions? columnOptions = null, IList sorting = null, IList group = null); - - /// - /// Get a paginated list from the database + /// Get list from the database /// - Task<(Gravity g, IList T)> List(IList catalysts, ColumnOptions? columnOptions = null, IList sorting = null, IList group = null); + Task<(Gravity g, IList T)> List(IList clusters, ColumnOptions? columnOptions = null, IList sorting = null, IList group = null); /// /// Get a paginated list from the database /// - Task<(Gravity g, IList T)> Paged(Q.Page page, IList catalysts, ColumnOptions? columnOptions = null, IList sorting = null, IList group = null); + Task<(Gravity g, IList T)> Paged(Q.Page page, IList clusters, ColumnOptions? columnOptions = null, IList sorting = null, IList group = null); } diff --git a/code/Universe/Options/SortingOptions.cs b/code/Universe/Options/SortingOptions.cs index abd7641..8f2967f 100644 --- a/code/Universe/Options/SortingOptions.cs +++ b/code/Universe/Options/SortingOptions.cs @@ -14,7 +14,7 @@ public enum Direction } /// - public record Option(string Column, Direction Direction = Direction.ASC); + public readonly record struct Option(string Column, Direction Direction = Direction.ASC); } /// diff --git a/code/Universe/Options/WhereClause.cs b/code/Universe/Options/WhereClause.cs new file mode 100644 index 0000000..14dfe9b --- /dev/null +++ b/code/Universe/Options/WhereClause.cs @@ -0,0 +1,8 @@ +namespace Universe.Options.Query; + +/// +/// Clusters are group of Catalysts that are joined by a Where operator (eg AND / OR). This will divide the where clause into multiple groups. +/// +/// Catalysts under one group / cluster +/// Where operator (eg AND / OR) +public readonly record struct Cluster(IList Catalysts, Q.Where Where = Q.Where.And); \ No newline at end of file diff --git a/code/Universe/UniverseQuery.csproj b/code/Universe/UniverseQuery.csproj index 8e1247c..1161164 100644 --- a/code/Universe/UniverseQuery.csproj +++ b/code/Universe/UniverseQuery.csproj @@ -1,9 +1,9 @@ - net6.0 + net7.0 disable - 10.0 + 11.0 A simpler way of querying a CosmosDb Namespace Nor Gelera 2022 Nor Gelera @@ -15,16 +15,16 @@ LICENSE A simpler way of querying a CosmosDb Namespace - Nor Gelera 2022 + Nor Gelera 2023 https://github.com/kuromukira/universe cosmos simple query Git Nor Gelera Universe - 1.4.1 + 2.0.0 View release on https://github.com/kuromukira/universe/releases - 1.4.1.0 - 1.4.1.0 + 2.0.0.0 + 2.0.0.0 @@ -37,8 +37,8 @@ - - + + diff --git a/code/Universe/UniverseQuery.xml b/code/Universe/UniverseQuery.xml index cfa9ca5..04c4e4f 100644 --- a/code/Universe/UniverseQuery.xml +++ b/code/Universe/UniverseQuery.xml @@ -82,27 +82,17 @@ Get one model from the database - + Get one model from the database - + - Get one model from the database - - - - - Get a paginated list from the database + Get list from the database - - - Get a paginated list from the database - - - + Get a paginated list from the database @@ -274,6 +264,26 @@ + + + Clusters are group of Catalysts that are joined by a Where operator (eg AND / OR). This will divide the where clause into multiple groups. + + Catalysts under one group / cluster + Where operator (eg AND / OR) + + + + Clusters are group of Catalysts that are joined by a Where operator (eg AND / OR). This will divide the where clause into multiple groups. + + Catalysts under one group / cluster + Where operator (eg AND / OR) + + + Catalysts under one group / cluster + + + Where operator (eg AND / OR) +