diff --git a/LATEST_CHANGE.md b/LATEST_CHANGE.md index 0ab4830..136c07d 100644 --- a/LATEST_CHANGE.md +++ b/LATEST_CHANGE.md @@ -1,6 +1,8 @@ ## Features -* **Users"**: New `ChangeUserPassword` mathod for `ICouchDatabase`. +* **Indexes"**: Ability to create indexes. ([#102](https://github.com/matteobortolazzo/couchdb-net/issues/102)) +* **Null values"**: New `SetNullValueHandling` method for `CouchOptionsBuilder` to set how to handle null values. ([#101](https://github.com/matteobortolazzo/couchdb-net/issues/101)) +* **Query"**: New `Select` and `Convert` methods to select specific fields. ## Bug Fixes -* **IsMatch**: Back to public instead of internal; -* **AddOrUpdate**: Added `Async` postfix. \ No newline at end of file +* **Conflicts**: Fix the query parameter value to get conflicts. ([#100](https://github.com/matteobortolazzo/couchdb-net/issues/100)) +* **Query**: Fix queries when variables are used. ([#104](https://github.com/matteobortolazzo/couchdb-net/issues/104)) \ No newline at end of file diff --git a/README.md b/README.md index eab7424..c73b623 100644 --- a/README.md +++ b/README.md @@ -37,9 +37,9 @@ var skywalkers = await context.Rebels .OrderByDescending(r => r.Name) .ThenByDescending(r => r.Age) .Take(2) - .Select(r => new { - r.Name, - r.Age + .Select( + r => r.Name, + r => r.Age }) .ToListAsync(); ``` @@ -101,6 +101,9 @@ The produced Mango JSON: * [DB Changes Feed](#db-changes-feed) * [Feed Options](#feed-options) * [Feed Filter](#feed-filter) +* [Indexing](#indexing) + * [Index Options](#index-options) + * [Partial Indexes](#partial-indexes) * [Local (non-replicating) Documents](#local-(non-replicating)-documents) * [Bookmark and Execution stats](#bookmark-and-execution-stats) * [Users](#users) @@ -192,7 +195,8 @@ If the Where method is not called in the expression, it will at an empty selecto | sort | OrderBy(..).ThenBy() | | sort | OrderByDescending(..) | | sort | OrderByDescending(..).ThenByDescending() | -| fields | Select(x => new { }) | +| fields | Select(x => x.Prop1, x => x.Prop2) | +| fields | Convert() | | use_index | UseIndex("design_document") | | use_index | UseIndex(new [] { "design_document", "index_name" }) | | r | WithReadQuorum(n) | @@ -331,6 +335,7 @@ var client = new CouchClient("http://localhost:5984", builder => builder | DisableDocumentPluralization | Disables documents pluralization in requests. | | SetDocumentCase | Sets the format case for documents. | | SetPropertyCase | Sets the format case for properties. | +| SetNullValueHandling | Sets how to handle null values. | | DisableLogOutOnDispose | Disables log out on client dispose. | - **DocumentCaseTypes**: None, UnderscoreCase *(default)*, DashCase, KebabCase. @@ -426,6 +431,81 @@ var filter = ChangesFeedFilter.View(view); ChangesFeedResponse changes = await GetChangesAsync(options: null, filter); ``` +## Indexing + +It is possible to create indexes to use when querying. + +```csharp +// Basic index creation +await _rebels.CreateIndexAsync("rebels_index", b => b + .IndexBy(r => r.Surname)) + .ThenBy(r => r.Name)); + +// Descending index creation +await _rebels.CreateIndexAsync("rebels_index", b => b + .IndexByDescending(r => r.Surname)) + .ThenByDescending(r => r.Name)); +``` + +### Index Options + +```csharp +// Specifies the design document and/or whether a JSON index is partitioned or global +await _rebels.CreateIndexAsync("rebels_index", b => b + .IndexBy(r => r.Surname), + new IndexOptions() + { + DesignDocument = "surnames_ddoc", + Partitioned = true + }); +``` + +### Partial Indexes +```csharp +// Create an index which excludes documents at index time +await _rebels.CreateIndexAsync("skywalkers_index", b => b + .IndexBy(r => r.Name) + .Where(r => r.Surname == "Skywalker"); +``` + +### Indexes operations +```csharp +// Get the list of indexes +var indexes = await _rebels.GetIndexesAsync(); + +// Delete an indexes +await _rebels.DeleteIndexAsync(indexes[0]); +// or +await _rebels.DeleteIndexAsync("surnames_ddoc", name: "surnames"); +``` + +### CouchContext Index Configuration + +Finally it's possible to configure indexes on the `CouchContext`. +```csharp +public class MyDeathStarContext : CouchContext +{ + public CouchDatabase Rebels { get; set; } + + protected override void OnConfiguring(CouchOptionsBuilder optionsBuilder) + { + optionsBuilder + .UseEndpoint("http://localhost:5984/") + .UseBasicAuthentication("admin", "admin") + // If it finds a index with the same name and ddoc (or null) + // but with different fields and/or sort order, + // it will override the index + .OverrideExistingIndexes(); + } + + protected override void OnDatabaseCreating(CouchDatabaseBuilder databaseBuilder) + { + databaseBuilder.Document() + .HasIndex("rebel_surnames_index", b => b.IndexBy(b => b.Surname)); + } +} +``` + ## Local (non-replicating) Documents The Local (non-replicating) document interface allows you to create local documents that are not replicated to other databases. @@ -548,3 +628,5 @@ Also, the configurator has `ConfigureFlurlClient` to set custom HTTP client opti Thanks to [Ben Origas](https://github.com/borigas) for features, ideas and tests like SSL custom validation, multi queryable, async deadlock, cookie authenication and many others. Thanks to [n9](https://github.com/n9) for proxy authentication, some bug fixes, suggestions and the great feedback on the changes feed feature! + +Thanks to [Marc](https://github.com/bender-ristone) for NullValueHandling, bug fixes and suggestions! diff --git a/src/.editorconfig b/src/.editorconfig index 671c781..d2a67dc 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -147,3 +147,6 @@ csharp_preserve_single_line_statements = true # CA1303: Do not pass literals as localized parameters dotnet_diagnostic.CA1303.severity = silent + +# CA1308: Normalize strings to uppercase +dotnet_diagnostic.CA1308.severity = suggestion \ No newline at end of file diff --git a/src/CouchDB.Driver/ChangesFeed/ChangesFeedFilterExtensions.cs b/src/CouchDB.Driver/ChangesFeed/ChangesFeedFilterExtensions.cs index 31671b4..0d006fc 100644 --- a/src/CouchDB.Driver/ChangesFeed/ChangesFeedFilterExtensions.cs +++ b/src/CouchDB.Driver/ChangesFeed/ChangesFeedFilterExtensions.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using System.Linq; using System.Linq.Expressions; using System.Net.Http; using System.Threading; @@ -8,7 +7,6 @@ using CouchDB.Driver.ChangesFeed.Filters; using CouchDB.Driver.ChangesFeed.Responses; using CouchDB.Driver.Extensions; -using CouchDB.Driver.Options; using CouchDB.Driver.Query; using CouchDB.Driver.Types; using Flurl.Http; @@ -17,7 +15,7 @@ namespace CouchDB.Driver.ChangesFeed { internal static class ChangesFeedFilterExtensions { - public static async Task> QueryWithFilterAsync(this IFlurlRequest request, CouchOptions options, ChangesFeedFilter filter, + public static async Task> QueryWithFilterAsync(this IFlurlRequest request, IAsyncQueryProvider queryProvider, ChangesFeedFilter filter, CancellationToken cancellationToken) where TSource : CouchDocument { @@ -32,12 +30,9 @@ public static async Task> QueryWithFilterAsync selectorFilter) { - MethodCallExpression whereExpression = Expression.Call(typeof(Queryable), nameof(Queryable.Where), - new[] { typeof(TSource) }, Expression.Constant(Array.Empty().AsQueryable()), selectorFilter.Value); + MethodCallExpression whereExpression = selectorFilter.Value.WrapInWhereExpression(); + var jsonSelector = queryProvider.ToString(whereExpression); - var optimizer = new QueryOptimizer(); - Expression optimizedQuery = optimizer.Optimize(whereExpression); - var jsonSelector = new QueryTranslator(options).Translate(optimizedQuery); return await request .WithHeader("Content-Type", "application/json") .SetQueryParam("filter", "_selector") @@ -65,7 +60,7 @@ public static async Task> QueryWithFilterAsync QueryContinuousWithFilterAsync(this IFlurlRequest request, CouchOptions options, ChangesFeedFilter filter, CancellationToken cancellationToken) + public static async Task QueryContinuousWithFilterAsync(this IFlurlRequest request, IAsyncQueryProvider queryProvider, ChangesFeedFilter filter, CancellationToken cancellationToken) where TSource: CouchDocument { if (filter is DocumentIdsChangesFeedFilter documentIdsFilter) @@ -78,12 +73,9 @@ public static async Task QueryContinuousWithFilterAsync(this IF if (filter is SelectorChangesFeedFilter selectorFilter) { - MethodCallExpression whereExpression = Expression.Call(typeof(Queryable), nameof(Queryable.Where), - new[] { typeof(TSource) }, Expression.Constant(Array.Empty().AsQueryable()), selectorFilter.Value); + MethodCallExpression whereExpression = selectorFilter.Value.WrapInWhereExpression(); + var jsonSelector = queryProvider.ToString(whereExpression); - var optimizer = new QueryOptimizer(); - Expression optimizedQuery = optimizer.Optimize(whereExpression); - var jsonSelector = new QueryTranslator(options).Translate(optimizedQuery); return await request .WithHeader("Content-Type", "application/json") .SetQueryParam("filter", "_selector") diff --git a/src/CouchDB.Driver/CouchClient.cs b/src/CouchDB.Driver/CouchClient.cs index afe054a..e5357cd 100644 --- a/src/CouchDB.Driver/CouchClient.cs +++ b/src/CouchDB.Driver/CouchClient.cs @@ -90,7 +90,8 @@ private IFlurlClient GetConfiguredClient() => { s.JsonSerializer = new NewtonsoftJsonSerializer(new JsonSerializerSettings { - ContractResolver = new CouchContractResolver(_options.PropertiesCase) + ContractResolver = new CouchContractResolver(_options.PropertiesCase), + NullValueHandling = _options.NullValueHandling ?? NullValueHandling.Include }); s.BeforeCallAsync = OnBeforeCallAsync; if (_options.ServerCertificateCustomValidationCallback != null) diff --git a/src/CouchDB.Driver/CouchContext.cs b/src/CouchDB.Driver/CouchContext.cs index 417131e..49cef49 100644 --- a/src/CouchDB.Driver/CouchContext.cs +++ b/src/CouchDB.Driver/CouchContext.cs @@ -1,10 +1,12 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Threading; using System.Threading.Tasks; using CouchDB.Driver.Helpers; +using CouchDB.Driver.Indexes; using CouchDB.Driver.Options; +using CouchDB.Driver.Types; namespace CouchDB.Driver { @@ -12,14 +14,15 @@ public abstract class CouchContext : IAsyncDisposable { public ICouchClient Client { get; } protected virtual void OnConfiguring(CouchOptionsBuilder optionsBuilder) { } + protected virtual void OnDatabaseCreating(CouchDatabaseBuilder databaseBuilder) { } - private static readonly MethodInfo GetDatabaseGenericMethod - = typeof(CouchClient).GetMethods(BindingFlags.Public | BindingFlags.Instance) - .Single(mi => mi.Name == nameof(CouchClient.GetDatabase) && mi.GetParameters().Length == 0); + private static readonly MethodInfo InitDatabasesGenericMethod + = typeof(CouchContext).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance) + .Single(mi => mi.Name == nameof(InitDatabasesAsync)); - private static readonly MethodInfo GetOrCreateDatabaseAsyncGenericMethod - = typeof(CouchClient).GetMethods(BindingFlags.Public | BindingFlags.Instance) - .Single(mi => mi.Name == nameof(CouchClient.GetOrCreateDatabaseAsync) && mi.GetParameters().Length == 3); + private static readonly MethodInfo ApplyDatabaseChangesGenericMethod + = typeof(CouchContext).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance) + .Single(mi => mi.Name == nameof(ApplyDatabaseChangesAsync)); protected CouchContext() : this(new CouchOptions()) { } @@ -27,37 +30,28 @@ protected CouchContext(CouchOptions options) { Check.NotNull(options, nameof(options)); - var builder = new CouchOptionsBuilder(options); + var optionsBuilder = new CouchOptionsBuilder(options); + var databaseBuilder = new CouchDatabaseBuilder(); #pragma warning disable CA2214 // Do not call overridable methods in constructors - OnConfiguring(builder); + OnConfiguring(optionsBuilder); + OnDatabaseCreating(databaseBuilder); #pragma warning restore CA2214 // Do not call overridable methods in constructors Client = new CouchClient(options); + IEnumerable databasePropertyInfos = GetDatabaseProperties(); - PropertyInfo[] databasePropertyInfos = GetDatabaseProperties(); - - foreach (PropertyInfo propertyInfo in databasePropertyInfos) + foreach (PropertyInfo dbProperty in databasePropertyInfos) { - Type documentType = propertyInfo.PropertyType.GetGenericArguments()[0]; - object? database; - if (options.CheckDatabaseExists) - { - MethodInfo getOrCreateDatabaseMethod = GetOrCreateDatabaseAsyncGenericMethod.MakeGenericMethod(documentType); -#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. - var parameters = new[] {(object)null, null, default(CancellationToken)}; -#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. - var task = (Task)getOrCreateDatabaseMethod.Invoke(Client, parameters); - task.ConfigureAwait(false).GetAwaiter().GetResult(); - PropertyInfo resultProperty = task.GetType().GetProperty(nameof(Task.Result)); - database = resultProperty.GetValue(task); - } - else - { - MethodInfo getDatabaseMethod = GetDatabaseGenericMethod.MakeGenericMethod(documentType); - database = getDatabaseMethod.Invoke(Client, Array.Empty()); - } - propertyInfo.SetValue(this, database); + Type documentType = dbProperty.PropertyType.GetGenericArguments()[0]; + + var initDatabasesTask = (Task)InitDatabasesGenericMethod.MakeGenericMethod(documentType) + .Invoke(this, new object[] {dbProperty, options}); + initDatabasesTask.ConfigureAwait(false).GetAwaiter().GetResult(); + + var applyDatabaseChangesTask = (Task)ApplyDatabaseChangesGenericMethod.MakeGenericMethod(documentType) + .Invoke(this, new object[] { dbProperty, options, databaseBuilder }); + applyDatabaseChangesTask.ConfigureAwait(false).GetAwaiter().GetResult(); } } @@ -66,11 +60,107 @@ public ValueTask DisposeAsync() return Client.DisposeAsync(); } - private PropertyInfo[] GetDatabaseProperties() => + private async Task InitDatabasesAsync(PropertyInfo propertyInfo, CouchOptions options) + where TSource : CouchDocument + { + ICouchDatabase database = options.CheckDatabaseExists + ? await Client.GetOrCreateDatabaseAsync().ConfigureAwait(false) + : Client.GetDatabase(); + + propertyInfo.SetValue(this, database); + } + + private async Task ApplyDatabaseChangesAsync(PropertyInfo propertyInfo, CouchOptions options, CouchDatabaseBuilder databaseBuilder) + where TSource: CouchDocument + { + if (!databaseBuilder.DocumentBuilders.ContainsKey(typeof(TSource))) + { + return; + } + + var database = (CouchDatabase)propertyInfo.GetValue(this); + var documentBuilder = (CouchDocumentBuilder)databaseBuilder.DocumentBuilders[typeof(TSource)]; + + List indexes = await database.GetIndexesAsync().ConfigureAwait(false); + + foreach (IndexSetupDefinition indexSetup in documentBuilder.IndexDefinitions) + { + await TryCreateOrUpdateIndexAsync(options, indexes, indexSetup, database) + .ConfigureAwait(false); + } + } + + private static async Task TryCreateOrUpdateIndexAsync( + CouchOptions options, + IEnumerable indexes, + IndexSetupDefinition indexSetup, + CouchDatabase database) + where TSource : CouchDocument + { + IndexInfo? currentIndex = TryFindIndex( + indexes, + indexSetup.Name, + indexSetup.Options?.DesignDocument); + + if (currentIndex == null) + { + await database.CreateIndexAsync( + indexSetup.Name, + indexSetup.IndexBuilderAction, + indexSetup.Options) + .ConfigureAwait(false); + return; + } + + if (!options.OverrideExistingIndexes) + { + return; + } + + IndexDefinition indexDefinition = database.NewIndexBuilder(indexSetup.IndexBuilderAction).Build(); + if (!AreFieldsEqual(currentIndex.Fields, indexDefinition.Fields)) + { + await database.DeleteIndexAsync(currentIndex) + .ConfigureAwait(false); + await database.CreateIndexAsync(indexSetup.Name, indexDefinition, indexSetup.Options) + .ConfigureAwait(false); + } + } + + private static IndexInfo? TryFindIndex(IEnumerable indexes, string name, string? designDocument) + { + return indexes.SingleOrDefault(current => + current.Name == name && + (designDocument == null || current.DesignDocument == designDocument)); + } + + private static bool AreFieldsEqual(Dictionary current, + Dictionary requested) + { + if (current.Count != requested.Count) + { + return false; + } + + for (var i = 0; i < current.Count; i++) + { + (var currentField, IndexFieldDirection currentDirection) = current.ElementAt(i); + (var requestedField, IndexFieldDirection requestedDirection) = requested.ElementAt(i); + + if (currentField != requestedField || currentDirection != requestedDirection) + { + return false; + } + } + + return true; + } + + private IEnumerable GetDatabaseProperties() => GetType() .GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == typeof(CouchDatabase<>)) .ToArray(); } -} +} \ No newline at end of file diff --git a/src/CouchDB.Driver/CouchDB.Driver.csproj b/src/CouchDB.Driver/CouchDB.Driver.csproj index c663495..1ab8f96 100644 --- a/src/CouchDB.Driver/CouchDB.Driver.csproj +++ b/src/CouchDB.Driver/CouchDB.Driver.csproj @@ -35,6 +35,7 @@ + diff --git a/src/CouchDB.Driver/CouchDatabase.cs b/src/CouchDB.Driver/CouchDatabase.cs index e45e06e..3ecb4ac 100644 --- a/src/CouchDB.Driver/CouchDatabase.cs +++ b/src/CouchDB.Driver/CouchDatabase.cs @@ -11,13 +11,14 @@ using System.IO; using System.Linq; using System.Linq.Expressions; -using System.Net; using System.Net.Http; using System.Runtime.CompilerServices; +using System.Text; using System.Threading; using System.Threading.Tasks; using CouchDB.Driver.ChangesFeed; using CouchDB.Driver.ChangesFeed.Responses; +using CouchDB.Driver.Indexes; using CouchDB.Driver.Local; using CouchDB.Driver.Options; using CouchDB.Driver.Query; @@ -75,7 +76,7 @@ internal CouchDatabase(IFlurlClient flurlClient, CouchOptions options, QueryCont if (withConflicts) { - request = request.SetQueryParam("conflicts", true); + request = request.SetQueryParam("conflicts", "true"); } TSource document = await request @@ -374,7 +375,7 @@ public async Task> GetChangesAsync(ChangesFeedOptio return filter == null ? await request.GetJsonAsync>(cancellationToken) .ConfigureAwait(false) - : await request.QueryWithFilterAsync(_options, filter, cancellationToken) + : await request.QueryWithFilterAsync(_queryProvider, filter, cancellationToken) .ConfigureAwait(false); } @@ -396,7 +397,7 @@ public async IAsyncEnumerable> GetContinuousC await using Stream stream = filter == null ? await request.GetStreamAsync(cancellationToken, HttpCompletionOption.ResponseHeadersRead) .ConfigureAwait(false) - : await request.QueryContinuousWithFilterAsync(_options, filter, cancellationToken) + : await request.QueryContinuousWithFilterAsync(_queryProvider, filter, cancellationToken) .ConfigureAwait(false); await foreach (var line in stream.ReadLinesAsync(cancellationToken)) @@ -413,6 +414,92 @@ public async IAsyncEnumerable> GetContinuousC #endregion + #region Index + + /// + public async Task> GetIndexesAsync(CancellationToken cancellationToken = default) + { + GetIndexesResult response = await NewRequest() + .AppendPathSegment("_index") + .GetJsonAsync(cancellationToken) + .SendRequestAsync() + .ConfigureAwait(false); + return response.Indexes; + } + + /// + public Task CreateIndexAsync(string name, + Action> indexBuilderAction, + IndexOptions? options = null, + CancellationToken cancellationToken = default) + { + Check.NotNull(name, nameof(name)); + Check.NotNull(indexBuilderAction, nameof(indexBuilderAction)); + + IndexDefinition indexDefinition = NewIndexBuilder(indexBuilderAction).Build(); + return CreateIndexAsync(name, indexDefinition, options, cancellationToken); + } + + internal async Task CreateIndexAsync(string name, + IndexDefinition indexDefinition, + IndexOptions? options = null, + CancellationToken cancellationToken = default) + { + var indexJson = indexDefinition.ToString(); + + var sb = new StringBuilder(); + sb.Append("{") + .Append($"\"index\":{indexJson},") + .Append($"\"name\":\"{name}\",") + .Append("\"type\":\"json\""); + + if (options?.DesignDocument != null) + { + sb.Append($",\"ddoc\":\"{options.DesignDocument}\""); + } + if (options?.Partitioned != null) + { + sb.Append($",\"partitioned\":{options.Partitioned.ToString().ToLowerInvariant()}"); + } + + sb.Append("}"); + + var request = sb.ToString(); + + CreateIndexResult result = await NewRequest() + .WithHeader("Content-Type", "application/json") + .AppendPathSegment("_index") + .PostStringAsync(request, cancellationToken) + .ReceiveJson() + .SendRequestAsync() + .ConfigureAwait(false); + + return result.Id; + } + + /// + public async Task DeleteIndexAsync(string designDocument, string name, CancellationToken cancellationToken = default) + { + Check.NotNull(designDocument, nameof(designDocument)); + Check.NotNull(name, nameof(name)); + + _ = await NewRequest() + .AppendPathSegments("_index", designDocument, "json", name) + .DeleteAsync(cancellationToken) + .SendRequestAsync() + .ConfigureAwait(false); + } + + /// + public Task DeleteIndexAsync(IndexInfo indexInfo, CancellationToken cancellationToken = default) + { + Check.NotNull(indexInfo, nameof(indexInfo)); + + return DeleteIndexAsync(indexInfo.DesignDocument, indexInfo.Name, cancellationToken); + } + + #endregion + #region Utils /// @@ -493,6 +580,14 @@ internal CouchQueryable AsQueryable() return new CouchQueryable(_queryProvider); } + internal IndexBuilder NewIndexBuilder( + Action> indexBuilderAction) + { + var builder = new IndexBuilder(_options, _queryProvider); + indexBuilderAction(builder); + return builder; + } + #endregion } -} +} \ No newline at end of file diff --git a/src/CouchDB.Driver/DTOs/BulkGetResultItem.cs b/src/CouchDB.Driver/DTOs/BulkGetResultItem.cs index 7f50722..af71871 100644 --- a/src/CouchDB.Driver/DTOs/BulkGetResultItem.cs +++ b/src/CouchDB.Driver/DTOs/BulkGetResultItem.cs @@ -11,4 +11,4 @@ internal class BulkGetResultItem where TSource : CouchDocument public TSource Item { get; set; } } } -#nullable enable \ No newline at end of file +#nullable restore \ No newline at end of file diff --git a/src/CouchDB.Driver/DTOs/CreateIndexResult.cs b/src/CouchDB.Driver/DTOs/CreateIndexResult.cs new file mode 100644 index 0000000..ba014b5 --- /dev/null +++ b/src/CouchDB.Driver/DTOs/CreateIndexResult.cs @@ -0,0 +1,18 @@ +#nullable disable +using Newtonsoft.Json; + +namespace CouchDB.Driver.DTOs +{ + internal class CreateIndexResult + { + [JsonProperty("result")] + public string Result { get; set; } + + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + } +} +#nullable restore \ No newline at end of file diff --git a/src/CouchDB.Driver/DTOs/GetIndexesResult.cs b/src/CouchDB.Driver/DTOs/GetIndexesResult.cs new file mode 100644 index 0000000..bd04b39 --- /dev/null +++ b/src/CouchDB.Driver/DTOs/GetIndexesResult.cs @@ -0,0 +1,14 @@ +#nullable disable +using System.Collections.Generic; +using CouchDB.Driver.Types; +using Newtonsoft.Json; + +namespace CouchDB.Driver.DTOs +{ + internal class GetIndexesResult + { + [JsonProperty("indexes")] + public List Indexes { get; set; } + } +} +#nullable restore \ No newline at end of file diff --git a/src/CouchDB.Driver/DTOs/IndexDefinitionInfo.cs b/src/CouchDB.Driver/DTOs/IndexDefinitionInfo.cs new file mode 100644 index 0000000..792409b --- /dev/null +++ b/src/CouchDB.Driver/DTOs/IndexDefinitionInfo.cs @@ -0,0 +1,13 @@ +#nullable disable +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace CouchDB.Driver.DTOs +{ + internal class IndexDefinitionInfo + { + [JsonProperty("fields")] + public Dictionary[] Fields { get; set; } + } +} +#nullable enable \ No newline at end of file diff --git a/src/CouchDB.Driver/Extensions/ExpressionExtensions.cs b/src/CouchDB.Driver/Extensions/ExpressionExtensions.cs index 5e3cd08..0a21209 100644 --- a/src/CouchDB.Driver/Extensions/ExpressionExtensions.cs +++ b/src/CouchDB.Driver/Extensions/ExpressionExtensions.cs @@ -1,11 +1,43 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; +using CouchDB.Driver.Options; namespace CouchDB.Driver.Extensions { internal static class ExpressionExtensions { + public static MemberExpression ToMemberExpression(this Expression selector) + { + if (!(selector is LambdaExpression l) || !(l.Body is MemberExpression m)) + { + throw new ArgumentException("The given expression does not select a property.", nameof(selector)); + } + + return m; + } + + public static string GetPropertyName(this MemberExpression m, CouchOptions options) + { + PropertyCaseType caseType = options.PropertiesCase; + + var members = new List { m.Member.GetCouchPropertyName(caseType) }; + + Expression currentExpression = m.Expression; + + while (currentExpression is MemberExpression cm) + { + members.Add(cm.Member.GetCouchPropertyName(caseType)); + currentExpression = cm.Expression; + } + + members.Reverse(); + var propName = string.Join(".", members.ToArray()); + + return propName; + } + public static bool ContainsSelector(this Expression expression) => expression is MethodCallExpression m && m.Arguments.Count == 2 && m.Arguments[1].IsSelectorExpression(); @@ -39,6 +71,13 @@ public static Expression WrapInLambda(this Expression body, IReadOnlyCollection< return Expression.Quote(lambdaExpression); } + public static MethodCallExpression WrapInWhereExpression(this Expression> selector) + { + MethodCallExpression whereExpression = Expression.Call(typeof(Queryable), nameof(Queryable.Where), + new[] { typeof(TSource) }, Expression.Constant(Array.Empty().AsQueryable()), selector); + return whereExpression; + } + private static Expression StripQuotes(this Expression e) { while (e.NodeType == ExpressionType.Quote) diff --git a/src/CouchDB.Driver/Extensions/FlurlRequestExtensions.cs b/src/CouchDB.Driver/Extensions/FlurlRequestExtensions.cs index eae6e2a..f83a5a4 100644 --- a/src/CouchDB.Driver/Extensions/FlurlRequestExtensions.cs +++ b/src/CouchDB.Driver/Extensions/FlurlRequestExtensions.cs @@ -1,4 +1,5 @@ -using System.IO; +using System.Collections.Generic; +using System.IO; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -46,10 +47,14 @@ public static Task PostStringStreamAsync( public static IFlurlRequest ApplyQueryParametersOptions(this IFlurlRequest request, object options) { - var queryParameters = OptionsHelper.ToQueryParameters(options); - foreach (var (name, value) in queryParameters) + IEnumerable<(string Name, object? Value)> queryParameters = OptionsHelper.ToQueryParameters(options); + foreach ((var name, object? value) in queryParameters) { - request = request.SetQueryParam(name, value); + object? finalValue = value?.GetType() == typeof(bool) + ? value.ToString().ToLowerInvariant() + : value; + + request = request.SetQueryParam(name, finalValue); } return request; diff --git a/src/CouchDB.Driver/ICouchDatabase.cs b/src/CouchDB.Driver/ICouchDatabase.cs index 7c08239..73379bd 100644 --- a/src/CouchDB.Driver/ICouchDatabase.cs +++ b/src/CouchDB.Driver/ICouchDatabase.cs @@ -1,9 +1,11 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using CouchDB.Driver.ChangesFeed; using CouchDB.Driver.ChangesFeed.Responses; +using CouchDB.Driver.Indexes; using CouchDB.Driver.Local; using CouchDB.Driver.Security; using CouchDB.Driver.Types; @@ -124,7 +126,43 @@ IAsyncEnumerable> GetContinuousChangesAsync( CancellationToken cancellationToken); /// - /// Asynchronously downloads a specific attachment. + /// Gets the list of indexes in the database. + /// + /// A to observe while waiting for the task to complete. + /// A Task that represents the asynchronous operation. The task result contains the list of indexes. + Task> GetIndexesAsync(CancellationToken cancellationToken = default); + + /// + /// Creates an index for the current database with the given configuration. + /// + /// The name of the index. + /// The action to configure the index builder. + /// The index options. + /// A to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. The task result contains the ID of the design document. + Task CreateIndexAsync(string name, Action> indexBuilderAction, + IndexOptions? options = null, + CancellationToken cancellationToken = default); + + /// + /// Deletes a specific index. + /// + /// The design document name. + /// The index name. + /// A to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. + Task DeleteIndexAsync(string designDocument, string name, CancellationToken cancellationToken = default); + + /// + /// Deletes a specific index. + /// + /// The index info. + /// A to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. + Task DeleteIndexAsync(IndexInfo indexInfo, CancellationToken cancellationToken = default); + + /// + /// Asynchronously downloads a specific attachment. /// /// The attachment to download. /// Path of local folder where file is to be downloaded. diff --git a/src/CouchDB.Driver/Indexes/IIndexBuilder.cs b/src/CouchDB.Driver/Indexes/IIndexBuilder.cs new file mode 100644 index 0000000..c88f565 --- /dev/null +++ b/src/CouchDB.Driver/Indexes/IIndexBuilder.cs @@ -0,0 +1,30 @@ +using System; +using System.Linq.Expressions; +using CouchDB.Driver.Types; + +namespace CouchDB.Driver.Indexes +{ + /// + /// Builder to configure CouchDB indexes. + /// + /// The type of the document. + public interface IIndexBuilder + where TSource : CouchDocument + { + /// + /// Select a field for the index sort in ascending order. + /// + /// The type of the selected property. + /// Function to select a property. + /// Returns the current instance to chain calls. + IOrderedIndexBuilder IndexBy(Expression> selector); + + /// + /// Select a field for the index sort in descending order. + /// + /// The type of the selected property. + /// Function to select a property. + /// Returns the current instance to chain calls. + IOrderedDescendingIndexBuilder IndexByDescending(Expression> selector); + } +} \ No newline at end of file diff --git a/src/CouchDB.Driver/Indexes/IIndexBuilderBase.cs b/src/CouchDB.Driver/Indexes/IIndexBuilderBase.cs new file mode 100644 index 0000000..770ecb4 --- /dev/null +++ b/src/CouchDB.Driver/Indexes/IIndexBuilderBase.cs @@ -0,0 +1,21 @@ +using System; +using System.Linq.Expressions; +using CouchDB.Driver.Types; + +namespace CouchDB.Driver.Indexes +{ + /// + /// Builder to configure CouchDB indexes. + /// + /// The type of the document. + public interface IIndexBuilderBase + where TSource : CouchDocument + { + /// + /// Creates a partial index which excludes documents based on the predicate at index time. + /// + /// Function to filter documents. + /// Returns the current instance to chain calls. + void Where(Expression> predicate); + } +} \ No newline at end of file diff --git a/src/CouchDB.Driver/Indexes/IOrderedDescendingIndexBuilder.cs b/src/CouchDB.Driver/Indexes/IOrderedDescendingIndexBuilder.cs new file mode 100644 index 0000000..7a58e8f --- /dev/null +++ b/src/CouchDB.Driver/Indexes/IOrderedDescendingIndexBuilder.cs @@ -0,0 +1,22 @@ +using System; +using System.Linq.Expressions; +using CouchDB.Driver.Types; + +namespace CouchDB.Driver.Indexes +{ + /// + /// Builder to configure CouchDB indexes. + /// + /// The type of the document. + public interface IOrderedDescendingIndexBuilder : IIndexBuilderBase + where TSource : CouchDocument + { + /// + /// Adds a field for the index sort in descending order. + /// + /// The type of the selected property. + /// Function to select a property. + /// Returns the current instance to chain calls. + IOrderedDescendingIndexBuilder ThenByDescending(Expression> selector); + } +} \ No newline at end of file diff --git a/src/CouchDB.Driver/Indexes/IOrderedIndexBuilder.cs b/src/CouchDB.Driver/Indexes/IOrderedIndexBuilder.cs new file mode 100644 index 0000000..efd41e7 --- /dev/null +++ b/src/CouchDB.Driver/Indexes/IOrderedIndexBuilder.cs @@ -0,0 +1,22 @@ +using System; +using System.Linq.Expressions; +using CouchDB.Driver.Types; + +namespace CouchDB.Driver.Indexes +{ + /// + /// Builder to configure CouchDB indexes. + /// + /// The type of the document. + public interface IOrderedIndexBuilder : IIndexBuilderBase + where TSource : CouchDocument + { + /// + /// Adds a field for the index sort in ascending order. + /// + /// The type of the selected property. + /// Function to select a property. + /// Returns the current instance to chain calls. + IOrderedIndexBuilder ThenBy(Expression> selector); + } +} \ No newline at end of file diff --git a/src/CouchDB.Driver/Indexes/IndexBuilder.cs b/src/CouchDB.Driver/Indexes/IndexBuilder.cs new file mode 100644 index 0000000..a4ec575 --- /dev/null +++ b/src/CouchDB.Driver/Indexes/IndexBuilder.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using CouchDB.Driver.Extensions; +using CouchDB.Driver.Options; +using CouchDB.Driver.Query; +using CouchDB.Driver.Types; + +namespace CouchDB.Driver.Indexes +{ + internal class IndexBuilder: IIndexBuilder, IOrderedIndexBuilder, IOrderedDescendingIndexBuilder + where TSource : CouchDocument + { + private readonly CouchOptions _options; + private readonly IAsyncQueryProvider _queryProvider; + + private bool _ascending = true; + private readonly List _fields; + private string? _partialSelector; + + public IndexBuilder(CouchOptions options, IAsyncQueryProvider queryProvider) + { + _options = options; + _queryProvider = queryProvider; + _fields = new List(); + } + + public IOrderedIndexBuilder IndexBy(Expression> selector) + { + AddField(selector); + return this; + } + + public IOrderedDescendingIndexBuilder IndexByDescending(Expression> selector) + { + _ascending = false; + AddField(selector); + return this; + } + + public IOrderedIndexBuilder ThenBy(Expression> selector) + { + return IndexBy(selector); + } + + public IOrderedDescendingIndexBuilder ThenByDescending(Expression> selector) + { + return IndexByDescending(selector); + } + + public void Where(Expression> predicate) + { + MethodCallExpression whereExpression = predicate.WrapInWhereExpression(); + var jsonSelector = _queryProvider.ToString(whereExpression); + _partialSelector = jsonSelector + .Substring(1, jsonSelector.Length - 2) + .Replace("selector", "partial_filter_selector", StringComparison.CurrentCultureIgnoreCase); + } + + private void AddField(Expression> selector) + { + var memberExpression = selector.ToMemberExpression(); + _fields.Add(memberExpression.GetPropertyName(_options)); + } + + public IndexDefinition Build() + { + var fields = _fields.ToDictionary( + field => field, + _ => _ascending ? IndexFieldDirection.Ascending : IndexFieldDirection.Descending); + return new IndexDefinition(fields, _partialSelector); + } + } +} \ No newline at end of file diff --git a/src/CouchDB.Driver/Indexes/IndexDefinition.cs b/src/CouchDB.Driver/Indexes/IndexDefinition.cs new file mode 100644 index 0000000..660e5cb --- /dev/null +++ b/src/CouchDB.Driver/Indexes/IndexDefinition.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Text; +using CouchDB.Driver.Types; + +namespace CouchDB.Driver.Indexes +{ + internal class IndexDefinition + { + public IndexDefinition(Dictionary fields, string? partialSelector) + { + Fields = fields; + PartialSelector = partialSelector; + } + + public Dictionary Fields { get; } + public string? PartialSelector { get; } + + public override string ToString() + { + var sb = new StringBuilder(); + + sb.Append("{"); + + // Partial Selector + if (PartialSelector != null) + { + sb.Append(PartialSelector); + sb.Append(","); + } + + // Fields + sb.Append("\"fields\":["); + + foreach ((var fieldName, IndexFieldDirection fieldDirection) in Fields) + { + var fieldString = fieldDirection == IndexFieldDirection.Ascending + ? $"\"{fieldName}\"," + : $"{{\"{fieldName}\":\"desc\"}},"; + + sb.Append(fieldString); + } + + sb.Length--; + sb.Append("]}"); + + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/src/CouchDB.Driver/Indexes/IndexOptions.cs b/src/CouchDB.Driver/Indexes/IndexOptions.cs new file mode 100644 index 0000000..84a0217 --- /dev/null +++ b/src/CouchDB.Driver/Indexes/IndexOptions.cs @@ -0,0 +1,8 @@ +namespace CouchDB.Driver.Indexes +{ + public class IndexOptions + { + public string? DesignDocument { get; set; } + public bool? Partitioned { get; set; } + } +} \ No newline at end of file diff --git a/src/CouchDB.Driver/Options/CouchDatabaseBuilder.cs b/src/CouchDB.Driver/Options/CouchDatabaseBuilder.cs new file mode 100644 index 0000000..168344e --- /dev/null +++ b/src/CouchDB.Driver/Options/CouchDatabaseBuilder.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using CouchDB.Driver.Types; + +namespace CouchDB.Driver.Options +{ + public class CouchDatabaseBuilder + { + internal readonly Dictionary DocumentBuilders; + + internal CouchDatabaseBuilder() + { + DocumentBuilders = new Dictionary(); + } + + public CouchDocumentBuilder Document() + where TSource : CouchDocument + { + Type documentType = typeof(TSource); + if (!DocumentBuilders.ContainsKey(documentType)) + { + DocumentBuilders.Add(documentType, new CouchDocumentBuilder()); + } + + return (CouchDocumentBuilder)DocumentBuilders[documentType]; + } + } +} \ No newline at end of file diff --git a/src/CouchDB.Driver/Options/CouchDocumentBuilder.cs b/src/CouchDB.Driver/Options/CouchDocumentBuilder.cs new file mode 100644 index 0000000..3bc9de9 --- /dev/null +++ b/src/CouchDB.Driver/Options/CouchDocumentBuilder.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using CouchDB.Driver.Indexes; +using CouchDB.Driver.Types; + +namespace CouchDB.Driver.Options +{ + public class CouchDocumentBuilder + where TSource : CouchDocument + { + internal List> IndexDefinitions { get; } + + internal CouchDocumentBuilder() + { + IndexDefinitions = new List>(); + } + + public CouchDocumentBuilder HasIndex(string name, Action> indexBuilderAction, + IndexOptions? options = null) + { + var indexDefinition = new IndexSetupDefinition(name, indexBuilderAction, options); + IndexDefinitions.Add(indexDefinition); + return this; + } + } +} \ No newline at end of file diff --git a/src/CouchDB.Driver/Options/CouchOptions.cs b/src/CouchDB.Driver/Options/CouchOptions.cs index b5669c3..8415c31 100644 --- a/src/CouchDB.Driver/Options/CouchOptions.cs +++ b/src/CouchDB.Driver/Options/CouchOptions.cs @@ -5,6 +5,7 @@ using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using Flurl.Http.Configuration; +using Newtonsoft.Json; namespace CouchDB.Driver.Options { @@ -14,6 +15,7 @@ public abstract class CouchOptions internal Uri? Endpoint { get; set; } internal bool CheckDatabaseExists { get; set; } + internal bool OverrideExistingIndexes { get; set; } internal AuthenticationType AuthenticationType { get; set; } internal string? Username { get; set; } @@ -25,6 +27,7 @@ public abstract class CouchOptions internal bool PluralizeEntities { get; set; } internal DocumentCaseType DocumentsCaseType { get; set; } internal PropertyCaseType PropertiesCase { get; set; } + internal NullValueHandling? NullValueHandling { get; set; } internal bool LogOutOnDispose { get; set; } internal Func? ServerCertificateCustomValidationCallback { get; set; } diff --git a/src/CouchDB.Driver/Options/CouchOptionsBuilder.cs b/src/CouchDB.Driver/Options/CouchOptionsBuilder.cs index 7ff392b..e5bb18c 100644 --- a/src/CouchDB.Driver/Options/CouchOptionsBuilder.cs +++ b/src/CouchDB.Driver/Options/CouchOptionsBuilder.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using CouchDB.Driver.Helpers; using Flurl.Http.Configuration; +using Newtonsoft.Json; namespace CouchDB.Driver.Options { @@ -65,6 +66,17 @@ public virtual CouchOptionsBuilder EnsureDatabaseExists() return this; } + /// + /// If in a , overrides indexes with same design document and name. + /// Ignore in the . + /// + /// Return the current instance to chain calls. + public virtual CouchOptionsBuilder OverrideExistingIndexes() + { + Options.OverrideExistingIndexes = true; + return this; + } + /// /// Enables basic authentication. /// Basic authentication (RFC 2617) is a quick and simple way to authenticate with CouchDB. The main drawback is the need to send user credentials with each request which may be insecure and could hurt operation performance (since CouchDB must compute the password hash with every request). @@ -181,6 +193,17 @@ public virtual CouchOptionsBuilder SetPropertyCase(PropertyCaseType type) return this; } + /// + /// Sets how to handle null values during serialization. + /// + /// The type of null value handling. + /// Return the current instance to chain calls. + public virtual CouchOptionsBuilder SetJsonNullValueHandling(NullValueHandling nullValueHandling) + { + Options.NullValueHandling = nullValueHandling; + return this; + } + /// /// Disables log out on client dispose. /// diff --git a/src/CouchDB.Driver/Options/CouchOptionsBuilder`.cs b/src/CouchDB.Driver/Options/CouchOptionsBuilder`.cs index 25506f2..1d3bef0 100644 --- a/src/CouchDB.Driver/Options/CouchOptionsBuilder`.cs +++ b/src/CouchDB.Driver/Options/CouchOptionsBuilder`.cs @@ -5,6 +5,7 @@ using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using Flurl.Http.Configuration; +using Newtonsoft.Json; namespace CouchDB.Driver.Options { @@ -51,6 +52,14 @@ public CouchOptionsBuilder(CouchOptions options) : base(options) { } public new virtual CouchOptionsBuilder EnsureDatabaseExists() => (CouchOptionsBuilder)base.EnsureDatabaseExists(); + /// + /// If in a , overrides indexes with same design document and name. + /// Ignore in the . + /// + /// Return the current instance to chain calls. + public new virtual CouchOptionsBuilder OverrideExistingIndexes() + => (CouchOptionsBuilder)base.OverrideExistingIndexes(); + /// /// Enables basic authentication. /// Basic authentication (RFC 2617) is a quick and simple way to authenticate with CouchDB. The main drawback is the need to send user credentials with each request which may be insecure and could hurt operation performance (since CouchDB must compute the password hash with every request). @@ -121,6 +130,14 @@ public CouchOptionsBuilder(CouchOptions options) : base(options) { } public new virtual CouchOptionsBuilder SetPropertyCase(PropertyCaseType type) => (CouchOptionsBuilder)base.SetPropertyCase(type); + /// + /// Sets how to handle null values during serialization. + /// + /// The type of null value handling. + /// Return the current instance to chain calls. + public new virtual CouchOptionsBuilder SetJsonNullValueHandling(NullValueHandling nullValueHandling) + => (CouchOptionsBuilder)base.SetJsonNullValueHandling(nullValueHandling); + /// /// Disables log out on client dispose. /// diff --git a/src/CouchDB.Driver/Options/DocumentCaseType.cs b/src/CouchDB.Driver/Options/DocumentCaseType.cs index 5e5ef39..eea4bc6 100644 --- a/src/CouchDB.Driver/Options/DocumentCaseType.cs +++ b/src/CouchDB.Driver/Options/DocumentCaseType.cs @@ -24,7 +24,6 @@ private DocumentCaseType(string value) : base(value) { } /// public static readonly DocumentCaseType KebabCase = new DocumentCaseType("KebabCase"); -#pragma warning disable CA1308 // Normalize strings to uppercase internal override string Convert(string str) { if (Equals(this, None)) @@ -41,6 +40,5 @@ internal override string Convert(string str) } throw new NotSupportedException($"Value {Value} not supported."); } -#pragma warning restore CA1308 // Normalize strings to uppercase } } \ No newline at end of file diff --git a/src/CouchDB.Driver/Options/IndexSetupDefinition.cs b/src/CouchDB.Driver/Options/IndexSetupDefinition.cs new file mode 100644 index 0000000..fd3475f --- /dev/null +++ b/src/CouchDB.Driver/Options/IndexSetupDefinition.cs @@ -0,0 +1,21 @@ +using System; +using CouchDB.Driver.Indexes; +using CouchDB.Driver.Types; + +namespace CouchDB.Driver.Options +{ + internal class IndexSetupDefinition + where TSource : CouchDocument + { + public IndexSetupDefinition(string name, Action> indexBuilderAction, IndexOptions? options) + { + Name = name; + IndexBuilderAction = indexBuilderAction; + Options = options; + } + + public string Name { get; } + public Action> IndexBuilderAction { get; } + public IndexOptions? Options { get; } + } +} \ No newline at end of file diff --git a/src/CouchDB.Driver/Query/Extensions/QueryableQueryExtensions.cs b/src/CouchDB.Driver/Query/Extensions/QueryableQueryExtensions.cs index 841af2d..810d8c5 100644 --- a/src/CouchDB.Driver/Query/Extensions/QueryableQueryExtensions.cs +++ b/src/CouchDB.Driver/Query/Extensions/QueryableQueryExtensions.cs @@ -3,13 +3,14 @@ using System.Linq.Expressions; using System.Reflection; using CouchDB.Driver.Helpers; +using CouchDB.Driver.Types; namespace CouchDB.Driver.Query.Extensions { public static class QueryableQueryExtensions { #region Helper methods to obtain MethodInfo in a safe way - + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA1801:Review unused parameters")] private static MethodInfo GetMethodInfo(Func f, T1 unused1) { @@ -149,5 +150,46 @@ public static IQueryable IncludeConflicts(this IQueryable + /// Select specific fields to return in the result. + /// + /// The source of items. + /// List of functions to select fields. + /// An that contains the request specific fields when requesting elements from the sequence. + public static IQueryable Select(this IQueryable source, params Expression>[] selectFunctions) + { + Check.NotNull(source, nameof(source)); + + foreach (Expression> selectFunction in selectFunctions) + { + Check.NotNull(selectFunction, nameof(selectFunctions)); + } + + return source.Provider.CreateQuery( + Expression.Call( + null, + GetMethodInfo(Select, source, selectFunctions), + new[] { source.Expression, Expression.Constant(selectFunctions) })); + } + + /// + /// Select only the field defined in + /// + /// Type of source. + /// Type of output list. + /// The source of items. + /// An that contains the request specific fields when requesting elements from the sequence. + public static IQueryable Convert(this IQueryable source) + where TResult : CouchDocument + { + Check.NotNull(source, nameof(source)); + + return source.Provider.CreateQuery( + Expression.Call( + null, + GetMethodInfo(Convert, source), + new[] { source.Expression })); + } } } diff --git a/src/CouchDB.Driver/Query/QueryCompiler.cs b/src/CouchDB.Driver/Query/QueryCompiler.cs index 71ceb6e..a4b21b2 100644 --- a/src/CouchDB.Driver/Query/QueryCompiler.cs +++ b/src/CouchDB.Driver/Query/QueryCompiler.cs @@ -70,7 +70,8 @@ private TResult SendRequest(Expression query, bool async, CancellationT private TResult SendRequestWithoutFilter(Expression query, bool async, CancellationToken cancellationToken) { - var body = _queryTranslator.Translate(query); + Expression optimizedQuery = _queryOptimizer.Optimize(query); + var body = _queryTranslator.Translate(optimizedQuery); return _requestSender.Send(body, async, cancellationToken); } @@ -84,7 +85,7 @@ private TResult SendRequestWithFilter(MethodCallExpression methodCallEx throw new ArgumentException($"Expression of type {optimizedQuery.GetType().Name} is not valid."); } - var body = _queryTranslator.Translate(query); + var body = _queryTranslator.Translate(optimizedQuery); // If no operation must be done on the list return if (!methodCallExpression.Method.IsSupportedByComposition()) @@ -97,7 +98,7 @@ private TResult SendRequestWithFilter(MethodCallExpression methodCallEx Type returnType = GetReturnType(async); // Query database - var couchQueryable = RequestSendMethod + object couchQueryable = RequestSendMethod .MakeGenericMethod(couchListType) .Invoke(_requestSender, new object[]{ body, async, cancellationToken }); diff --git a/src/CouchDB.Driver/Query/QueryTranslator.cs b/src/CouchDB.Driver/Query/QueryTranslator.cs index 1ee355f..d4794b2 100644 --- a/src/CouchDB.Driver/Query/QueryTranslator.cs +++ b/src/CouchDB.Driver/Query/QueryTranslator.cs @@ -18,6 +18,7 @@ internal QueryTranslator(CouchOptions options) public string Translate(Expression e) { + _isSelectorSet = false; _sb.Clear(); _sb.Append("{"); Visit(e); diff --git a/src/CouchDB.Driver/Query/Translators/MemberExpressionTranslator.cs b/src/CouchDB.Driver/Query/Translators/MemberExpressionTranslator.cs index 257761b..4d7e7b7 100644 --- a/src/CouchDB.Driver/Query/Translators/MemberExpressionTranslator.cs +++ b/src/CouchDB.Driver/Query/Translators/MemberExpressionTranslator.cs @@ -1,7 +1,5 @@ -using Newtonsoft.Json; -using System.Collections.Generic; -using System.Linq.Expressions; -using System.Reflection; +using System.Linq.Expressions; +using CouchDB.Driver.Extensions; namespace CouchDB.Driver.Query { @@ -9,30 +7,7 @@ internal partial class QueryTranslator { protected override Expression VisitMember(MemberExpression m) { - string GetPropertyName(MemberInfo memberInfo) - { - var jsonPropertyAttributes = memberInfo.GetCustomAttributes(typeof(JsonPropertyAttribute), true); - JsonPropertyAttribute? jsonProperty = jsonPropertyAttributes.Length > 0 - ? jsonPropertyAttributes[0] as JsonPropertyAttribute - : null; - - return jsonProperty != null - ? jsonProperty.PropertyName - : _options.PropertiesCase.Convert(memberInfo.Name); - } - - var members = new List { GetPropertyName(m.Member) }; - - Expression currentExpression = m.Expression; - - while (currentExpression is MemberExpression cm) - { - members.Add(GetPropertyName(cm.Member)); - currentExpression = cm.Expression; - } - - members.Reverse(); - var propName = string.Join(".", members.ToArray()); + var propName = m.GetPropertyName(_options); _sb.Append($"\"{propName}\""); return m; diff --git a/src/CouchDB.Driver/Query/Translators/MethodCallExpressionTranslator.cs b/src/CouchDB.Driver/Query/Translators/MethodCallExpressionTranslator.cs index 66b557a..149245a 100644 --- a/src/CouchDB.Driver/Query/Translators/MethodCallExpressionTranslator.cs +++ b/src/CouchDB.Driver/Query/Translators/MethodCallExpressionTranslator.cs @@ -1,10 +1,12 @@ using CouchDB.Driver.Types; using System; using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Security.Authentication; using CouchDB.Driver.Extensions; +using CouchDB.Driver.Helpers; using CouchDB.Driver.Shared; #pragma warning disable IDE0058 // Expression value is never used @@ -120,6 +122,16 @@ protected override Expression VisitMethodCall(MethodCallExpression m) return VisitIncludeConflictsMethod(m); } + if (genericDefinition == SupportedQueryMethods.Select) + { + return VisitSelectFieldMethod(m); + } + + if (genericDefinition == SupportedQueryMethods.Convert) + { + return VisitConvertMethod(m); + } + // IEnumerable extensions if (genericDefinition == SupportedQueryMethods.EnumerableContains) @@ -376,6 +388,7 @@ public Expression VisitIncludeExecutionStatsMethod(MethodCallExpression m) _sb.Append(","); return m; } + public Expression VisitIncludeConflictsMethod(MethodCallExpression m) { Visit(m.Arguments[0]); @@ -384,6 +397,49 @@ public Expression VisitIncludeConflictsMethod(MethodCallExpression m) return m; } + public Expression VisitSelectFieldMethod(MethodCallExpression m) + { + Visit(m.Arguments[0]); + _sb.Append("\"fields\":["); + + if (!(((ConstantExpression)m.Arguments[1]).Value is Expression[] fieldExpressions)) + { + throw new InvalidOperationException(); + } + + foreach (Expression a in fieldExpressions) + { + Visit(a); + _sb.Append(","); + } + + _sb.Length--; + _sb.Append("],"); + return m; + } + + public Expression VisitConvertMethod(MethodCallExpression m) + { + Visit(m.Arguments[0]); + _sb.Append("\"fields\":["); + + Type returnType = m.Method.GetGenericArguments()[1]; + PropertyInfo[] properties = returnType + .GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(p => p.DeclaringType != typeof(CouchDocument)) + .ToArray(); + + foreach (PropertyInfo property in properties) + { + var field = property.GetCouchPropertyName(_options.PropertiesCase); + _sb.Append($"\"{field}\","); + } + + _sb.Length--; + _sb.Append("],"); + return m; + } + #endregion #region EnumerableExtensions diff --git a/src/CouchDB.Driver/Shared/SupportedMethodsProvider.cs b/src/CouchDB.Driver/Shared/SupportedMethodsProvider.cs index 48fd467..59069ba 100644 --- a/src/CouchDB.Driver/Shared/SupportedMethodsProvider.cs +++ b/src/CouchDB.Driver/Shared/SupportedMethodsProvider.cs @@ -50,6 +50,8 @@ static SupportedMethodsProvider() SupportedQueryMethods.UseIndex, SupportedQueryMethods.IncludeExecutionStats, SupportedQueryMethods.IncludeConflicts, + SupportedQueryMethods.Select, + SupportedQueryMethods.Convert, SupportedQueryMethods.EnumerableContains, SupportedQueryMethods.FieldExists, SupportedQueryMethods.IsCouchType, diff --git a/src/CouchDB.Driver/Shared/SupportedQueryMethods.cs b/src/CouchDB.Driver/Shared/SupportedQueryMethods.cs index f1bcada..0a28da7 100644 --- a/src/CouchDB.Driver/Shared/SupportedQueryMethods.cs +++ b/src/CouchDB.Driver/Shared/SupportedQueryMethods.cs @@ -1,8 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Linq; using System.Reflection; -using CouchDB.Driver.Extensions; using CouchDB.Driver.Query.Extensions; namespace CouchDB.Driver.Shared @@ -18,6 +15,8 @@ internal static class SupportedQueryMethods public static MethodInfo UseIndex { get; } public static MethodInfo IncludeExecutionStats { get; } public static MethodInfo IncludeConflicts { get; } + public static MethodInfo Select { get; } + public static MethodInfo Convert { get; } public static MethodInfo EnumerableContains { get; } public static MethodInfo FieldExists { get; } public static MethodInfo IsCouchType { get; } @@ -52,6 +51,8 @@ static SupportedQueryMethods() mi.Name == nameof(QueryableQueryExtensions.IncludeExecutionStats)); IncludeConflicts = queryableExtensionsMethods.Single(mi => mi.Name == nameof(QueryableQueryExtensions.IncludeConflicts)); + Select = queryableExtensionsMethods.Single(mi => mi.Name == nameof(QueryableQueryExtensions.Select)); + Convert = queryableExtensionsMethods.Single(mi => mi.Name == nameof(QueryableQueryExtensions.Convert)); EnumerableContains = typeof(EnumerableQueryExtensions) .GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) diff --git a/src/CouchDB.Driver/Types/CouchDocumentInfo.cs b/src/CouchDB.Driver/Types/CouchDocumentInfo.cs index 91bfde3..09b3da7 100644 --- a/src/CouchDB.Driver/Types/CouchDocumentInfo.cs +++ b/src/CouchDB.Driver/Types/CouchDocumentInfo.cs @@ -1,5 +1,4 @@ #nullable disable -using System.Collections.Generic; using System.Runtime.Serialization; using Newtonsoft.Json; diff --git a/src/CouchDB.Driver/Types/IndexFieldDirection.cs b/src/CouchDB.Driver/Types/IndexFieldDirection.cs new file mode 100644 index 0000000..1ccfdc7 --- /dev/null +++ b/src/CouchDB.Driver/Types/IndexFieldDirection.cs @@ -0,0 +1,11 @@ +namespace CouchDB.Driver.Types +{ + /// + /// Represent the direction of the index. + /// + public enum IndexFieldDirection + { + Ascending = 0, + Descending = 1, + } +} \ No newline at end of file diff --git a/src/CouchDB.Driver/Types/IndexInfo.cs b/src/CouchDB.Driver/Types/IndexInfo.cs new file mode 100644 index 0000000..b44352b --- /dev/null +++ b/src/CouchDB.Driver/Types/IndexInfo.cs @@ -0,0 +1,56 @@ +#nullable disable +using System.Collections.Generic; +using System.Linq; +using CouchDB.Driver.DTOs; +using Newtonsoft.Json; + +namespace CouchDB.Driver.Types +{ + /// + /// Represent info about the index. + /// + public class IndexInfo + { + public IndexInfo() + { + Fields = new Dictionary(); + } + + /// + /// ID of the design document the index belongs to. + /// + [JsonProperty("ddoc")] + public string DesignDocument { get; set; } + + /// + /// The name of the index. + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// The fields used in the index + /// + [JsonIgnore] + public Dictionary Fields { get; } + + [JsonProperty("def")] + internal IndexDefinitionInfo Definition + { + set + { + Fields.Clear(); + + foreach (Dictionary definitions in value.Fields) + { + var (name, direction) = definitions.First(); + IndexFieldDirection fieldDirection = direction == "asc" + ? IndexFieldDirection.Ascending + : IndexFieldDirection.Descending; + Fields.Add(name, fieldDirection); + } + } + } + } +} +#nullable restore \ No newline at end of file diff --git a/src/azure-pipelines.yaml b/src/azure-pipelines.yaml index 6debb59..f1639b7 100644 --- a/src/azure-pipelines.yaml +++ b/src/azure-pipelines.yaml @@ -1,6 +1,6 @@ variables: BuildConfiguration: Release - PackageVersion: '2.0.2' + PackageVersion: '2.1.0' trigger: branches: diff --git a/tests/CouchDB.Driver.E2ETests/Client_Tests.cs b/tests/CouchDB.Driver.E2ETests/Client_Tests.cs index a40090c..194963e 100644 --- a/tests/CouchDB.Driver.E2ETests/Client_Tests.cs +++ b/tests/CouchDB.Driver.E2ETests/Client_Tests.cs @@ -9,6 +9,7 @@ using CouchDB.Driver.E2ETests._Models; using CouchDB.Driver.Extensions; using CouchDB.Driver.Local; +using CouchDB.Driver.Query.Extensions; using Xunit; namespace CouchDB.Driver.E2E @@ -102,6 +103,30 @@ public async Task Crud_Context() Assert.NotEmpty(result); } + [Fact] + public async Task Crud_Index_Context() + { + // Create index + await using var context = new MyDeathStarContext(); + // Override index + await using var newContext = new MyDeathStarContext2(); + + var indexes = await context.Rebels.GetIndexesAsync(); + + Assert.Equal(2, indexes.Count); + + await context.Rebels.AddAsync(new Rebel { Name = "Han", Age = 30, Surname = "Solo" }); + await context.Rebels.AddAsync(new Rebel { Name = "Leia", Age = 19, Surname = "Skywalker" }); + await context.Rebels.AddAsync(new Rebel { Name = "Luke", Age = 19, Surname = "Skywalker" }); + + var rebels = await context.Rebels + .OrderBy(r => r.Surname) + .ThenBy(r => r.Name) + .ToListAsync(); + + Assert.NotEmpty(rebels); + } + [Fact] public async Task Crud_SpecialCharacters() { diff --git a/tests/CouchDB.Driver.E2ETests/MyDeathStarContext.cs b/tests/CouchDB.Driver.E2ETests/MyDeathStarContext.cs index 4ba363b..d78ae5d 100644 --- a/tests/CouchDB.Driver.E2ETests/MyDeathStarContext.cs +++ b/tests/CouchDB.Driver.E2ETests/MyDeathStarContext.cs @@ -14,5 +14,38 @@ protected override void OnConfiguring(CouchOptionsBuilder optionsBuilder) .EnsureDatabaseExists() .UseBasicAuthentication(username: "admin", password: "admin"); } + + protected override void OnDatabaseCreating(CouchDatabaseBuilder databaseBuilder) + { + databaseBuilder.Document() + .HasIndex("surnames_index", builder => builder + .IndexBy(r => r.Surname) + .ThenBy(r => r.Name)); + } + } + + /// + /// Different indexes + /// + public class MyDeathStarContext2 : CouchContext + { + public CouchDatabase Rebels { get; set; } + + protected override void OnConfiguring(CouchOptionsBuilder optionsBuilder) + { + optionsBuilder + .UseEndpoint("http://localhost:5984/") + .EnsureDatabaseExists() + .OverrideExistingIndexes() + .UseBasicAuthentication(username: "admin", password: "admin"); + } + + protected override void OnDatabaseCreating(CouchDatabaseBuilder databaseBuilder) + { + databaseBuilder.Document() + .HasIndex("surnames_index", builder => builder + .IndexByDescending(r => r.Surname) + .ThenByDescending(r => r.Name)); + } } } diff --git a/tests/CouchDB.Driver.UnitTests/CouchDbContext_Index_Tests.cs b/tests/CouchDB.Driver.UnitTests/CouchDbContext_Index_Tests.cs new file mode 100644 index 0000000..70f777f --- /dev/null +++ b/tests/CouchDB.Driver.UnitTests/CouchDbContext_Index_Tests.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using CouchDB.Driver.Extensions; +using CouchDB.Driver.Options; +using CouchDB.Driver.UnitTests.Models; +using Flurl.Http.Testing; +using Xunit; + +namespace CouchDB.Driver.UnitTests +{ + public class CouchDbContext_Index_Tests + { + private class MyDeathStarContext : CouchContext + { + public CouchDatabase Rebels { get; set; } + + protected override void OnConfiguring(CouchOptionsBuilder optionsBuilder) + { + optionsBuilder + .UseEndpoint("http://localhost:5984/") + .OverrideExistingIndexes() + .UseBasicAuthentication("admin", "admin"); + } + + protected override void OnDatabaseCreating(CouchDatabaseBuilder databaseBuilder) + { + databaseBuilder.Document() + .HasIndex("skywalkers", b => b.IndexBy(b => b.Surname)); + } + } + + [Fact] + public async Task Context_Index() + { + using var httpTest = new HttpTest(); + httpTest.RespondWithJson(new + { + Indexes = new string[0] + }); + httpTest.RespondWithJson(new + { + Result = "created" + }); + httpTest.RespondWithJson(new + { + docs = new object[] { + new { + Id = "176694", + Rev = "1-54f8e950cc338d2385d9b0cda2fd918e", + Name = "Luke" + } + } + }); + + await using var context = new MyDeathStarContext(); + var result = await context.Rebels.ToListAsync(); + Assert.NotEmpty(result); + Assert.Equal("Luke", result[0].Name); + } + + [Fact] + public async Task Context_Index_NotToOverride() + { + using var httpTest = new HttpTest(); + httpTest.RespondWithJson(new + { + Indexes = new [] + { + new { + ddoc = Guid.NewGuid().ToString(), + name = "skywalkers", + def = new { + fields = new[] { + new Dictionary{ { "surname", "asc" } } + } + } + } + } + }); + httpTest.RespondWithJson(new + { + docs = new object[] { + new { + Id = "176694", + Rev = "1-54f8e950cc338d2385d9b0cda2fd918e", + Name = "Luke" + } + } + }); + + await using var context = new MyDeathStarContext(); + var result = await context.Rebels.ToListAsync(); + Assert.NotEmpty(result); + Assert.Equal("Luke", result[0].Name); + } + + [Fact] + public async Task Context_Index_ToOverride() + { + using var httpTest = new HttpTest(); + httpTest.RespondWithJson(new + { + Indexes = new[] + { + new { + ddoc = Guid.NewGuid().ToString(), + name = "skywalkers", + def = new { + fields = new[] { + new Dictionary{ { "surname", "desc" } } + } + } + } + } + }); + // Delete + httpTest.RespondWithJson(new + { + ok = true + }); + // Create new + httpTest.RespondWithJson(new + { + Result = "created" + }); + // Query + httpTest.RespondWithJson(new + { + docs = new object[] { + new { + Id = "176694", + Rev = "1-54f8e950cc338d2385d9b0cda2fd918e", + Name = "Luke" + } + } + }); + await using var context = new MyDeathStarContext(); + var result = await context.Rebels.ToListAsync(); + Assert.NotEmpty(result); + Assert.Equal("Luke", result[0].Name); + } + } +} diff --git a/tests/CouchDB.Driver.UnitTests/Database_Tests.cs b/tests/CouchDB.Driver.UnitTests/Database_Tests.cs index d24a11e..4a83aba 100644 --- a/tests/CouchDB.Driver.UnitTests/Database_Tests.cs +++ b/tests/CouchDB.Driver.UnitTests/Database_Tests.cs @@ -58,7 +58,7 @@ public async Task FindWithConflicts() var newR = await _rebels.FindAsync("1", true); httpTest .ShouldHaveCalled("http://localhost/rebels/1") - .WithQueryParamValue("conflicts", true) + .WithQueryParamValue("conflicts", "true") .WithVerb(HttpMethod.Get); } diff --git a/tests/CouchDB.Driver.UnitTests/Feed/GetChanges_Tests.cs b/tests/CouchDB.Driver.UnitTests/Feed/GetChanges_Tests.cs index 90ad1e1..d98db93 100644 --- a/tests/CouchDB.Driver.UnitTests/Feed/GetChanges_Tests.cs +++ b/tests/CouchDB.Driver.UnitTests/Feed/GetChanges_Tests.cs @@ -61,7 +61,7 @@ public async Task GetChangesAsync_WithOptions() httpTest .ShouldHaveCalled("http://localhost/rebels/_changes") .WithQueryParamValue("feed", "longpoll") - .WithQueryParamValue("attachments", true) + .WithQueryParamValue("attachments", "true") .WithVerb(HttpMethod.Get); } diff --git a/tests/CouchDB.Driver.UnitTests/Feed/GetContinuousChangesAsync_Tests.cs b/tests/CouchDB.Driver.UnitTests/Feed/GetContinuousChangesAsync_Tests.cs index 06573fc..57f8830 100644 --- a/tests/CouchDB.Driver.UnitTests/Feed/GetContinuousChangesAsync_Tests.cs +++ b/tests/CouchDB.Driver.UnitTests/Feed/GetContinuousChangesAsync_Tests.cs @@ -75,7 +75,7 @@ public async Task GetContinuousChangesAsync_WithOptions() httpTest .ShouldHaveCalled("http://localhost/rebels/_changes") .WithQueryParamValue("feed", "continuous") - .WithQueryParamValue("attachments", true) + .WithQueryParamValue("attachments", "true") .WithVerb(HttpMethod.Get); } diff --git a/tests/CouchDB.Driver.UnitTests/Find/Find_Miscellaneous.cs b/tests/CouchDB.Driver.UnitTests/Find/Find_Miscellaneous.cs index 47a82fa..46ea435 100644 --- a/tests/CouchDB.Driver.UnitTests/Find/Find_Miscellaneous.cs +++ b/tests/CouchDB.Driver.UnitTests/Find/Find_Miscellaneous.cs @@ -2,7 +2,6 @@ using System; using System.Linq; using Xunit; -using CouchDB.Driver.Extensions; using CouchDB.Driver.Query.Extensions; namespace CouchDB.Driver.UnitTests.Find @@ -37,20 +36,42 @@ public void Field() var json = _rebels.Select(r => r.Age).ToString(); Assert.Equal(@"{""fields"":[""age""],""selector"":{}}", json); } - + [Fact] public void Fields() { - var json = _rebels.Select(r => new - { - r.Name, - r.Age - }).ToString(); + var json = _rebels.Select( + r => r.Name, + r => r.Age, + r => r.Vehicle.CanFly) + .ToString(); + Assert.Equal(@"{""fields"":[""name"",""age"",""vehicle.canFly""],""selector"":{}}", json); + } + + [Fact] + public void Fields_NewObject() + { + var json = _rebels.Select( + r => new + { + r.Name, + r.Age, + r.Vehicle.CanFly + }) + .ToString(); + Assert.Equal(@"{""fields"":[""name"",""age"",""vehicle.canFly""],""selector"":{}}", json); + } + + [Fact] + public void Convert() + { + var json = _rebels.Convert() + .ToString(); Assert.Equal(@"{""fields"":[""name"",""age""],""selector"":{}}", json); } [Fact] - public void Index_Parlial() + public void Index_Partial() { var json = _rebels.UseIndex("design_document").ToString(); Assert.Equal(@"{""use_index"":""design_document"",""selector"":{}}", json); diff --git a/tests/CouchDB.Driver.UnitTests/Find/Find_Selector_Combinations.cs b/tests/CouchDB.Driver.UnitTests/Find/Find_Selector_Combinations.cs index d4b6d0b..78b93b3 100644 --- a/tests/CouchDB.Driver.UnitTests/Find/Find_Selector_Combinations.cs +++ b/tests/CouchDB.Driver.UnitTests/Find/Find_Selector_Combinations.cs @@ -4,6 +4,7 @@ using CouchDB.Driver.Extensions; using System.Linq; using CouchDB.Driver.Query.Extensions; +using Flurl.Http.Testing; namespace CouchDB.Driver.UnitTests.Find { @@ -17,6 +18,34 @@ public Find_Selector_Combinations() _rebels = client.GetDatabase(); } + [Fact] + public void And_HttpCall() + { + using var httpTest = new HttpTest(); + httpTest.RespondWithJson(new + { + docs = new[] + { + new Rebel() + } + }); + var surname = "Skywalker"; + var result = _rebels.Where(r => r.Name == "Luke" && r.Surname == surname).ToList(); + Assert.NotEmpty(result); + + var actualBody = httpTest.CallLog[0].RequestBody; + var expectedBody = @"{""selector"":{""$and"":[{""name"":""Luke""},{""surname"":""Skywalker""}]}}"; + Assert.Equal(expectedBody, actualBody); + } + + [Fact] + public void And_WithVariable() + { + var surname = "Skywalker"; + var json = _rebels.Where(r => r.Name == "Luke" && r.Surname == surname).ToString(); + Assert.Equal(@"{""selector"":{""$and"":[{""name"":""Luke""},{""surname"":""Skywalker""}]}}", json); + } + [Fact] public void And() { diff --git a/tests/CouchDB.Driver.UnitTests/Index_Tests.cs b/tests/CouchDB.Driver.UnitTests/Index_Tests.cs new file mode 100644 index 0000000..4267444 --- /dev/null +++ b/tests/CouchDB.Driver.UnitTests/Index_Tests.cs @@ -0,0 +1,96 @@ +using System.Net.Http; +using System.Threading.Tasks; +using CouchDB.Driver.Indexes; +using CouchDB.Driver.UnitTests.Models; +using Flurl.Http.Testing; +using Xunit; + +namespace CouchDB.Driver.UnitTests +{ + public class Index_Tests + { + private readonly ICouchClient _client; + private readonly ICouchDatabase _rebels; + + public Index_Tests() + { + _client = new CouchClient("http://localhost"); + _rebels = _client.GetDatabase(); + } + + [Fact] + public async Task CreateIndex() + { + using var httpTest = new HttpTest(); + httpTest.RespondWithJson(new + { + result = "created" + }); + + await _rebels.CreateIndexAsync("skywalkers", b => b + .IndexBy(r => r.Surname)); + + + var expectedBody = + "{\"index\":{\"fields\":[\"surname\"]},\"name\":\"skywalkers\",\"type\":\"json\"}"; + httpTest + .ShouldHaveCalled("http://localhost/rebels/_index") + .WithRequestBody(expectedBody) + .WithVerb(HttpMethod.Post); + } + + [Fact] + public async Task CreateIndex_WithOptions() + { + using var httpTest = new HttpTest(); + httpTest.RespondWithJson(new + { + result = "created" + }); + + await _rebels.CreateIndexAsync("skywalkers", b => b + .IndexByDescending(r => r.Surname) + .ThenByDescending(r => r.Name), + new IndexOptions() + { + DesignDocument = "skywalkers_ddoc", + Partitioned = true + }); + + + var expectedBody = + "{\"index\":{\"fields\":[{\"surname\":\"desc\"},{\"name\":\"desc\"}]},\"name\":\"skywalkers\",\"type\":\"json\",\"ddoc\":\"skywalkers_ddoc\",\"partitioned\":true}"; + httpTest + .ShouldHaveCalled("http://localhost/rebels/_index") + .WithRequestBody(expectedBody) + .WithVerb(HttpMethod.Post); + } + + [Fact] + public async Task CreateIndex_Partial() + { + using var httpTest = new HttpTest(); + httpTest.RespondWithJson(new + { + result = "created" + }); + + await _rebels.CreateIndexAsync("skywalkers", b => b + .IndexBy(r => r.Surname) + .Where(r => r.Surname == "Skywalker"), + new IndexOptions() + { + DesignDocument = "skywalkers_ddoc", + Partitioned = true + }); + + + var expectedBody = + "{\"index\":{\"partial_filter_selector\":{\"surname\":\"Skywalker\"},\"fields\":[\"surname\"]},\"name\":\"skywalkers\",\"type\":\"json\",\"ddoc\":\"skywalkers_ddoc\",\"partitioned\":true}"; + httpTest + .ShouldHaveCalled("http://localhost/rebels/_index") + .WithRequestBody(expectedBody) + .WithVerb(HttpMethod.Post); + } + } +} diff --git a/tests/CouchDB.Driver.UnitTests/Settings_Tests.cs b/tests/CouchDB.Driver.UnitTests/Settings_Tests.cs index a037983..eecd192 100644 --- a/tests/CouchDB.Driver.UnitTests/Settings_Tests.cs +++ b/tests/CouchDB.Driver.UnitTests/Settings_Tests.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using CouchDB.Driver.Extensions; using CouchDB.Driver.Options; +using Newtonsoft.Json; using Xunit; namespace CouchDB.Driver.UnitTests @@ -189,5 +190,57 @@ public async Task DocumentName_JsonObject() } #endregion + + #region Null Value Handling + + [Fact] + public async Task PropertyNullValueHandling_NotSet() + { + using var httpTest = new HttpTest(); + // Logout + httpTest.RespondWithJson(new { ok = true }); + + await using var client = new CouchClient("http://localhost"); + var rebels = client.GetDatabase(); + await rebels.AddAsync(new Rebel()); + + var call = httpTest.CallLog.First(); + Assert.NotNull(call); + Assert.Equal(@"{""_conflicts"":[],""name"":null,""surname"":null,""age"":0,""isJedi"":false,""species"":0,""guid"":""00000000-0000-0000-0000-000000000000"",""skills"":null,""battles"":null,""vehicle"":null}", call.RequestBody); + } + + [Fact] + public async Task PropertyNullValueHandling_Includes() + { + using var httpTest = new HttpTest(); + // Logout + httpTest.RespondWithJson(new { ok = true }); + + await using var client = new CouchClient("http://localhost", x => x.SetJsonNullValueHandling(NullValueHandling.Include)); + var rebels = client.GetDatabase(); + await rebels.AddAsync(new Rebel()); + + var call = httpTest.CallLog.First(); + Assert.NotNull(call); + Assert.Equal(@"{""_conflicts"":[],""name"":null,""surname"":null,""age"":0,""isJedi"":false,""species"":0,""guid"":""00000000-0000-0000-0000-000000000000"",""skills"":null,""battles"":null,""vehicle"":null}", call.RequestBody); + } + + [Fact] + public async Task PropertyNullValueHandling_Ignore() + { + using var httpTest = new HttpTest(); + // Logout + httpTest.RespondWithJson(new { ok = true }); + + await using var client = new CouchClient("http://localhost", x => x.SetJsonNullValueHandling(NullValueHandling.Ignore)); + var rebels = client.GetDatabase(); + await rebels.AddAsync(new Rebel()); + + var call = httpTest.CallLog.First(); + Assert.NotNull(call); + Assert.Equal(@"{""_conflicts"":[],""age"":0,""isJedi"":false,""species"":0,""guid"":""00000000-0000-0000-0000-000000000000""}", call.RequestBody); + } + + #endregion } } diff --git a/tests/CouchDB.Driver.UnitTests/_Models/Rebel.cs b/tests/CouchDB.Driver.UnitTests/_Models/Rebel.cs index ff97273..1df64ba 100644 --- a/tests/CouchDB.Driver.UnitTests/_Models/Rebel.cs +++ b/tests/CouchDB.Driver.UnitTests/_Models/Rebel.cs @@ -1,10 +1,15 @@ using CouchDB.Driver.Types; using System; using System.Collections.Generic; -using System.Text; namespace CouchDB.Driver.UnitTests.Models { + public class SimpleRebel : CouchDocument + { + public string Name { get; set; } + public int Age { get; set; } + } + public class Rebel : CouchDocument { public string Name { get; set; } @@ -15,6 +20,7 @@ public class Rebel : CouchDocument public Guid Guid { get; set; } public List Skills { get; set; } public List Battles { get; set; } + public Vehicle Vehicle { get; set; } public override bool Equals(object obj) {