diff --git a/src/PinguApps.Appwrite.Client/Clients/ClientDatabasesClient.cs b/src/PinguApps.Appwrite.Client/Clients/ClientDatabasesClient.cs index 69ccfd57..96c4b126 100644 --- a/src/PinguApps.Appwrite.Client/Clients/ClientDatabasesClient.cs +++ b/src/PinguApps.Appwrite.Client/Clients/ClientDatabasesClient.cs @@ -71,14 +71,14 @@ public async Task> CreateDocument(CreateDocumentRequest } /// - public async Task>> CreateDocument(CreateDocumentRequest request) + public async Task>> CreateDocument(CreateDocumentRequest request) where TData : class, new() { try { request.Validate(true); - var result = await _databasesApi.CreateDocument(GetCurrentSession(), request.DatabaseId, request.CollectionId, request); + var result = await _databasesApi.CreateDocument(GetCurrentSession(), request.DatabaseId, request.CollectionId, request); return result.GetApiResponse(); } diff --git a/src/PinguApps.Appwrite.Client/Clients/IClientDatabasesClient.cs b/src/PinguApps.Appwrite.Client/Clients/IClientDatabasesClient.cs index b8a140f6..3cd3aac1 100644 --- a/src/PinguApps.Appwrite.Client/Clients/IClientDatabasesClient.cs +++ b/src/PinguApps.Appwrite.Client/Clients/IClientDatabasesClient.cs @@ -46,7 +46,7 @@ public interface IClientDatabasesClient /// The type of the document data /// The request content /// The document - Task>> CreateDocument(CreateDocumentRequest request) where TData : class, new(); + Task>> CreateDocument(CreateDocumentRequest request) where TData : class, new(); /// /// Delete a document by its unique ID. diff --git a/src/PinguApps.Appwrite.Client/Internals/IDatabasesApi.cs b/src/PinguApps.Appwrite.Client/Internals/IDatabasesApi.cs index 9c635be8..931df0e3 100644 --- a/src/PinguApps.Appwrite.Client/Internals/IDatabasesApi.cs +++ b/src/PinguApps.Appwrite.Client/Internals/IDatabasesApi.cs @@ -20,7 +20,7 @@ internal interface IDatabasesApi : IBaseApi Task> CreateDocument([Header("x-appwrite-session")] string? session, string databaseId, string collectionId, CreateDocumentRequest request); [Post("/databases/{databaseId}/collections/{collectionId}/documents")] - Task>> CreateDocument([Header("x-appwrite-session")] string? session, string databaseId, string collectionId, CreateDocumentRequest request) where TData : class, new(); + Task>> CreateDocument([Header("x-appwrite-session")] string? session, string databaseId, string collectionId, CreateDocumentRequest request) where TData : class, new(); [Delete("/databases/{databaseId}/collections/{collectionId}/documents/{documentId}")] Task DeleteDocument([Header("x-appwrite-session")] string? session, string databaseId, string collectionId, string documentId); diff --git a/src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs b/src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs index 16e8a8be..3f066d65 100644 --- a/src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs +++ b/src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs @@ -148,7 +148,13 @@ private static RefitSettings AddSerializationConfigToRefitSettings(RefitSettings PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + options.Converters.Add(new SdkMarkerConverter()); + options.Converters.Add(new UpdateDocumentRequestConverter()); options.Converters.Add(new IgnoreSdkExcludedPropertiesConverterFactory()); + options.Converters.Add(new DocumentConverter()); + options.Converters.Add(new DocumentGenericConverterFactory()); + options.Converters.Add(new DocumentListConverter()); + options.Converters.Add(new DocumentListGenericConverter()); settings.ContentSerializer = new SystemTextJsonContentSerializer(options); diff --git a/src/PinguApps.Appwrite.Server/Clients/IServerDatabasesClient.cs b/src/PinguApps.Appwrite.Server/Clients/IServerDatabasesClient.cs index 0dfd9273..04694820 100644 --- a/src/PinguApps.Appwrite.Server/Clients/IServerDatabasesClient.cs +++ b/src/PinguApps.Appwrite.Server/Clients/IServerDatabasesClient.cs @@ -313,7 +313,7 @@ public interface IServerDatabasesClient /// The data type for your document /// The request content /// The document - Task>> CreateDocument(CreateDocumentRequest request) where TData : class, new(); + Task>> CreateDocument(CreateDocumentRequest request) where TData : class, new(); /// /// Delete a document by its unique ID. diff --git a/src/PinguApps.Appwrite.Server/Clients/ServerDatabasesClient.cs b/src/PinguApps.Appwrite.Server/Clients/ServerDatabasesClient.cs index 772b69e7..52a4c9df 100644 --- a/src/PinguApps.Appwrite.Server/Clients/ServerDatabasesClient.cs +++ b/src/PinguApps.Appwrite.Server/Clients/ServerDatabasesClient.cs @@ -634,7 +634,7 @@ public async Task> CreateDocument(CreateDocumentRequest } /// - public async Task>> CreateDocument(CreateDocumentRequest request) + public async Task>> CreateDocument(CreateDocumentRequest request) where TData : class, new() { try diff --git a/src/PinguApps.Appwrite.Server/Internals/IDatabasesApi.cs b/src/PinguApps.Appwrite.Server/Internals/IDatabasesApi.cs index 97c1a22b..3ad30324 100644 --- a/src/PinguApps.Appwrite.Server/Internals/IDatabasesApi.cs +++ b/src/PinguApps.Appwrite.Server/Internals/IDatabasesApi.cs @@ -129,7 +129,7 @@ internal interface IDatabasesApi : IBaseApi Task> CreateDocument(string databaseId, string collectionId, CreateDocumentRequest request); [Post("/databases/{databaseId}/collections/{collectionId}/documents")] - Task>> CreateDocument(string databaseId, string collectionId, CreateDocumentRequest request) where TData : class, new(); + Task>> CreateDocument(string databaseId, string collectionId, CreateDocumentRequest request) where TData : class, new(); [Delete("/databases/{databaseId}/collections/{collectionId}/documents/{documentId}")] Task DeleteDocument(string databaseId, string collectionId, string documentId); diff --git a/src/PinguApps.Appwrite.Server/ServiceCollectionExtensions.cs b/src/PinguApps.Appwrite.Server/ServiceCollectionExtensions.cs index 671951b5..dda7d740 100644 --- a/src/PinguApps.Appwrite.Server/ServiceCollectionExtensions.cs +++ b/src/PinguApps.Appwrite.Server/ServiceCollectionExtensions.cs @@ -106,7 +106,13 @@ private static RefitSettings AddSerializationConfigToRefitSettings(RefitSettings PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + options.Converters.Add(new SdkMarkerConverter()); + options.Converters.Add(new UpdateDocumentRequestConverter()); options.Converters.Add(new IgnoreSdkExcludedPropertiesConverterFactory()); + options.Converters.Add(new DocumentConverter()); + options.Converters.Add(new DocumentGenericConverterFactory()); + options.Converters.Add(new DocumentListConverter()); + options.Converters.Add(new DocumentListGenericConverter()); settings.ContentSerializer = new SystemTextJsonContentSerializer(options); diff --git a/src/PinguApps.Appwrite.Shared/Constants.cs b/src/PinguApps.Appwrite.Shared/Constants.cs index 62038dcd..75946e50 100644 --- a/src/PinguApps.Appwrite.Shared/Constants.cs +++ b/src/PinguApps.Appwrite.Shared/Constants.cs @@ -1,5 +1,5 @@ namespace PinguApps.Appwrite.Shared; public static class Constants { - public const string Version = "1.0.4"; + public const string Version = "1.1.1"; } diff --git a/src/PinguApps.Appwrite.Shared/Converters/DocumentGenericConverter.cs b/src/PinguApps.Appwrite.Shared/Converters/DocumentGenericConverter.cs index e8b41404..33de89f0 100644 --- a/src/PinguApps.Appwrite.Shared/Converters/DocumentGenericConverter.cs +++ b/src/PinguApps.Appwrite.Shared/Converters/DocumentGenericConverter.cs @@ -185,8 +185,12 @@ public override void Write(Utf8JsonWriter writer, Document value, JsonSer writer.WriteStartObject(); writer.WriteString("$id", value.Id); - writer.WriteString("$collectionId", value.CollectionId); - writer.WriteString("$databaseId", value.DatabaseId); + + if (!options.IsInsideSdk()) + { + writer.WriteString("$collectionId", value.CollectionId); + writer.WriteString("$databaseId", value.DatabaseId); + } // Use MultiFormatDateTimeConverter for DateTime properties var dateTimeConverter = new NullableDateTimeConverter(); diff --git a/src/PinguApps.Appwrite.Shared/Converters/DocumentListGenericConverter.cs b/src/PinguApps.Appwrite.Shared/Converters/DocumentListGenericConverter.cs index a68b8214..0fe60639 100644 --- a/src/PinguApps.Appwrite.Shared/Converters/DocumentListGenericConverter.cs +++ b/src/PinguApps.Appwrite.Shared/Converters/DocumentListGenericConverter.cs @@ -14,6 +14,12 @@ public override bool CanConvert(Type typeToConvert) return false; } + var documentType = typeToConvert.GetGenericArguments()[0]; + if (!documentType.IsGenericType || documentType.GetGenericTypeDefinition() != typeof(Document<>)) + { + return false; + } + return typeToConvert.GetGenericTypeDefinition() == typeof(IReadOnlyList<>); } diff --git a/src/PinguApps.Appwrite.Shared/Converters/SdkMarkerConverter.cs b/src/PinguApps.Appwrite.Shared/Converters/SdkMarkerConverter.cs new file mode 100644 index 00000000..13c90835 --- /dev/null +++ b/src/PinguApps.Appwrite.Shared/Converters/SdkMarkerConverter.cs @@ -0,0 +1,14 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace PinguApps.Appwrite.Shared.Converters; +public class SdkMarkerConverter : JsonConverter +{ + // Never actually converts anything + public override bool CanConvert(Type typeToConvert) => false; + + public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => throw new NotImplementedException(); + + public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) => throw new NotImplementedException(); +} diff --git a/src/PinguApps.Appwrite.Shared/Converters/UpdateDocumentRequestConverter.cs b/src/PinguApps.Appwrite.Shared/Converters/UpdateDocumentRequestConverter.cs new file mode 100644 index 00000000..369a3575 --- /dev/null +++ b/src/PinguApps.Appwrite.Shared/Converters/UpdateDocumentRequestConverter.cs @@ -0,0 +1,47 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using PinguApps.Appwrite.Shared.Requests.Databases; +using PinguApps.Appwrite.Shared.Utils; + +namespace PinguApps.Appwrite.Shared.Converters; +public class UpdateDocumentRequestConverter : JsonConverter +{ + public override UpdateDocumentRequest Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + + public override void Write(Utf8JsonWriter writer, UpdateDocumentRequest value, JsonSerializerOptions options) + { + var permissionsListConverter = new PermissionListConverter(); + + writer.WriteStartObject(); + + if (!options.IsInsideSdk()) + { + writer.WriteString("$collectionId", value.CollectionId); + writer.WriteString("$databaseId", value.DatabaseId); + } + + if (value.Permissions is not null) + { + writer.WritePropertyName("permissions"); + permissionsListConverter.Write(writer, [.. value.Permissions], options); + } + + if (value.Data.Count > 0) + { + writer.WritePropertyName("data"); + writer.WriteStartObject(); + foreach (var kvp in value.Data) + { + writer.WritePropertyName(kvp.Key); + JsonSerializer.Serialize(writer, kvp.Value, options); + } + writer.WriteEndObject(); + } + + writer.WriteEndObject(); + } +} diff --git a/src/PinguApps.Appwrite.Shared/Requests/Databases/UpdateDocumentRequest.cs b/src/PinguApps.Appwrite.Shared/Requests/Databases/UpdateDocumentRequest.cs index ff940f18..7dad5952 100644 --- a/src/PinguApps.Appwrite.Shared/Requests/Databases/UpdateDocumentRequest.cs +++ b/src/PinguApps.Appwrite.Shared/Requests/Databases/UpdateDocumentRequest.cs @@ -9,6 +9,7 @@ namespace PinguApps.Appwrite.Shared.Requests.Databases; /// /// The request to update a document. Can only be created with /// +[JsonConverter(typeof(UpdateDocumentRequestConverter))] public class UpdateDocumentRequest : DatabaseCollectionDocumentIdBaseRequest { internal UpdateDocumentRequest() { } @@ -24,7 +25,7 @@ internal UpdateDocumentRequest() { } /// [JsonPropertyName("permissions")] [JsonConverter(typeof(PermissionListConverter))] - public List Permissions { get; set; } = []; + public List? Permissions { get; set; } /// /// Creates a new builder for creating a document request diff --git a/src/PinguApps.Appwrite.Shared/Requests/Databases/UpdateDocumentRequestBuilder.cs b/src/PinguApps.Appwrite.Shared/Requests/Databases/UpdateDocumentRequestBuilder.cs index f89d195d..76010f3e 100644 --- a/src/PinguApps.Appwrite.Shared/Requests/Databases/UpdateDocumentRequestBuilder.cs +++ b/src/PinguApps.Appwrite.Shared/Requests/Databases/UpdateDocumentRequestBuilder.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Reflection; using System.Text.Json.Serialization; +using PinguApps.Appwrite.Shared.Responses; using PinguApps.Appwrite.Shared.Utils; namespace PinguApps.Appwrite.Shared.Requests.Databases; @@ -38,13 +39,62 @@ public IUpdateDocumentRequestBuilder WithPermissions(List permission public IUpdateDocumentRequestBuilder AddPermission(Permission permission) { + _request.Permissions ??= []; _request.Permissions.Add(permission); return this; } public IUpdateDocumentRequestBuilder AddField(string name, object? value) { - _data[name] = value; + if (value == null) + { + _data[name] = null; + return this; + } + + var valueType = value.GetType(); + + if (valueType.IsEnum) + { + _data[name] = value.ToString(); + return this; + } + + bool IsStandardType(Type type) => + type.IsPrimitive || + type == typeof(string) || + type == typeof(DateTime) || + type == typeof(DateTimeOffset) || + type == typeof(decimal) || + type.IsEnum; + + // Check if it's an IEnumerable (but not string, which is IEnumerable) + if (valueType != typeof(string)) + { + var enumerableInterface = valueType + .GetInterfaces() + .Concat([valueType]) + .FirstOrDefault(i => + i.IsGenericType && + i.GetGenericTypeDefinition() == typeof(IEnumerable<>)); + + if (enumerableInterface is not null) + { + var elementType = enumerableInterface.GetGenericArguments()[0]; + + if (IsStandardType(elementType)) + { + _data[name] = value; + return this; + } + } + } + + if (IsStandardType(valueType)) + { + _data[name] = value; + } + return this; } @@ -59,6 +109,13 @@ public IUpdateDocumentRequestBuilder WithChanges(T before, T after) where T : throw new ArgumentNullException(nameof(after)); } + // Check if T is Document + if (IsDocumentType(typeof(T))) + { + HandleDocumentChanges(before, after); + return this; + } + var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance); foreach (var property in properties) @@ -110,4 +167,66 @@ private static bool AreValuesEqual(object? value1, object? value2) return value1.Equals(value2); } + + private bool IsDocumentType(Type type) + { + if (!type.IsGenericType) + { + return false; + } + + return type.GetGenericTypeDefinition() == typeof(Document<>); + } + + private void HandleDocumentChanges(T before, T after) where T : class + { + var documentType = typeof(T); + var idProperty = documentType.GetProperty(nameof(Document.Id)); + var dataProperty = documentType.GetProperty(nameof(Document.Data)); + + var beforeId = idProperty.GetValue(before) as string; + var afterId = idProperty.GetValue(after) as string; + + var beforeData = dataProperty.GetValue(before); + var afterData = dataProperty.GetValue(after); + + if (beforeData is null || afterData is null) + { + return; + } + + // If IDs match, compare the Data properties + if (!string.IsNullOrEmpty(beforeId) && beforeId == afterId) + { + var dataProperties = afterData.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); + + foreach (var property in dataProperties) + { + if (!property.CanRead) continue; + + var beforeValue = property.GetValue(beforeData); + var afterValue = property.GetValue(afterData); + + if (!AreValuesEqual(beforeValue, afterValue)) + { + var jsonPropertyName = GetJsonPropertyName(property); + AddField(jsonPropertyName, afterValue); + } + } + } + // If IDs don't match, add all properties from after.Data + else + { + var dataProperties = afterData.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); + + foreach (var property in dataProperties) + { + if (!property.CanRead) continue; + + var afterValue = property.GetValue(afterData); + var jsonPropertyName = GetJsonPropertyName(property); + AddField(jsonPropertyName, afterValue); + } + } + } } diff --git a/src/PinguApps.Appwrite.Shared/Requests/Databases/Validators/UpdateDocumentRequestValidator.cs b/src/PinguApps.Appwrite.Shared/Requests/Databases/Validators/UpdateDocumentRequestValidator.cs index bf1a5baa..8788ecb9 100644 --- a/src/PinguApps.Appwrite.Shared/Requests/Databases/Validators/UpdateDocumentRequestValidator.cs +++ b/src/PinguApps.Appwrite.Shared/Requests/Databases/Validators/UpdateDocumentRequestValidator.cs @@ -7,12 +7,8 @@ public UpdateDocumentRequestValidator() { Include(new DatabaseCollectionDocumentIdBaseRequestValidator()); - RuleFor(x => x.Data) - .NotNull() - .WithMessage("Data is required."); - - RuleFor(x => x.Permissions) - .NotNull() - .WithMessage("Permissions cannot be null."); + RuleFor(x => x) + .Must(x => (x.Data?.Count > 0) || x.Permissions != null) + .WithMessage("Either Data must contain at least one item or Permissions must be provided."); } } diff --git a/src/PinguApps.Appwrite.Shared/Utils/JsonSerializerOptionsExtensions.cs b/src/PinguApps.Appwrite.Shared/Utils/JsonSerializerOptionsExtensions.cs new file mode 100644 index 00000000..c5a61575 --- /dev/null +++ b/src/PinguApps.Appwrite.Shared/Utils/JsonSerializerOptionsExtensions.cs @@ -0,0 +1,9 @@ +using System.Linq; +using System.Text.Json; +using PinguApps.Appwrite.Shared.Converters; + +namespace PinguApps.Appwrite.Shared.Utils; +public static class JsonSerializerOptionsExtensions +{ + public static bool IsInsideSdk(this JsonSerializerOptions options) => options.Converters.Any(c => c is SdkMarkerConverter); +} diff --git a/tests/PinguApps.Appwrite.Client.Tests/Clients/Databases/DatabasesClientTests.CreateDocumentGeneric.cs b/tests/PinguApps.Appwrite.Client.Tests/Clients/Databases/DatabasesClientTests.CreateDocumentGeneric.cs index 0c51eecc..4ffc71df 100644 --- a/tests/PinguApps.Appwrite.Client.Tests/Clients/Databases/DatabasesClientTests.CreateDocumentGeneric.cs +++ b/tests/PinguApps.Appwrite.Client.Tests/Clients/Databases/DatabasesClientTests.CreateDocumentGeneric.cs @@ -11,11 +11,16 @@ public partial class DatabasesClientTests public async Task CreateDocumentGeneric_ShouldReturnSuccess_WhenApiCallSucceeds() { // Arrange - var request = CreateDocumentRequest.CreateBuilder() - .WithDatabaseId(IdUtils.GenerateUniqueId()) - .WithCollectionId(IdUtils.GenerateUniqueId()) - .AddField("AttributeName", "MyValue") - .Build(); + var request = new CreateDocumentRequest() + { + DatabaseId = IdUtils.GenerateUniqueId(), + CollectionId = IdUtils.GenerateUniqueId(), + Data = new TestData() + { + Name = "Test", + Age = 25 + } + }; _mockHttp.Expect(HttpMethod.Post, $"{TestConstants.Endpoint}/databases/{request.DatabaseId}/collections/{request.CollectionId}/documents") .ExpectedHeaders() @@ -35,11 +40,16 @@ public async Task CreateDocumentGeneric_ShouldReturnSuccess_WhenApiCallSucceeds( public async Task CreateDocumentGeneric_ShouldIncludeSessionHeaders_WhenProvided() { // Arrange - var request = CreateDocumentRequest.CreateBuilder() - .WithDatabaseId(IdUtils.GenerateUniqueId()) - .WithCollectionId(IdUtils.GenerateUniqueId()) - .AddField("AttributeName", "MyValue") - .Build(); + var request = new CreateDocumentRequest() + { + DatabaseId = IdUtils.GenerateUniqueId(), + CollectionId = IdUtils.GenerateUniqueId(), + Data = new TestData() + { + Name = "Test", + Age = 25 + } + }; _mockHttp.Expect(HttpMethod.Post, $"{TestConstants.Endpoint}/databases/{request.DatabaseId}/collections/{request.CollectionId}/documents") .ExpectedHeaders() @@ -59,11 +69,16 @@ public async Task CreateDocumentGeneric_ShouldIncludeSessionHeaders_WhenProvided public async Task CreateDocumentGeneric_ShouldHandleException_WhenApiCallFails() { // Arrange - var request = CreateDocumentRequest.CreateBuilder() - .WithDatabaseId(IdUtils.GenerateUniqueId()) - .WithCollectionId(IdUtils.GenerateUniqueId()) - .AddField("AttributeName", "MyValue") - .Build(); + var request = new CreateDocumentRequest() + { + DatabaseId = IdUtils.GenerateUniqueId(), + CollectionId = IdUtils.GenerateUniqueId(), + Data = new TestData() + { + Name = "Test", + Age = 25 + } + }; _mockHttp.Expect(HttpMethod.Post, $"{TestConstants.Endpoint}/databases/{request.DatabaseId}/collections/{request.CollectionId}/documents") .ExpectedHeaders() @@ -82,11 +97,16 @@ public async Task CreateDocumentGeneric_ShouldHandleException_WhenApiCallFails() public async Task CreateDocumentGeneric_ShouldReturnErrorResponse_WhenExceptionOccurs() { // Arrange - var request = CreateDocumentRequest.CreateBuilder() - .WithDatabaseId(IdUtils.GenerateUniqueId()) - .WithCollectionId(IdUtils.GenerateUniqueId()) - .AddField("AttributeName", "MyValue") - .Build(); + var request = new CreateDocumentRequest() + { + DatabaseId = IdUtils.GenerateUniqueId(), + CollectionId = IdUtils.GenerateUniqueId(), + Data = new TestData() + { + Name = "Test", + Age = 25 + } + }; _mockHttp.Expect(HttpMethod.Post, $"{TestConstants.Endpoint}/databases/{request.DatabaseId}/collections/{request.CollectionId}/documents") .ExpectedHeaders() diff --git a/tests/PinguApps.Appwrite.Server.Tests/Clients/Databases/DatabasesClientTests.CreateDocumentGeneric.cs b/tests/PinguApps.Appwrite.Server.Tests/Clients/Databases/DatabasesClientTests.CreateDocumentGeneric.cs index 5fea49b9..0873e13e 100644 --- a/tests/PinguApps.Appwrite.Server.Tests/Clients/Databases/DatabasesClientTests.CreateDocumentGeneric.cs +++ b/tests/PinguApps.Appwrite.Server.Tests/Clients/Databases/DatabasesClientTests.CreateDocumentGeneric.cs @@ -11,11 +11,16 @@ public partial class DatabasesClientTests public async Task CreateDocumentGeneric_ShouldReturnSuccess_WhenApiCallSucceeds() { // Arrange - var request = CreateDocumentRequest.CreateBuilder() - .WithDatabaseId(IdUtils.GenerateUniqueId()) - .WithCollectionId(IdUtils.GenerateUniqueId()) - .AddField("AttributeName", "MyValue") - .Build(); + var request = new CreateDocumentRequest() + { + DatabaseId = IdUtils.GenerateUniqueId(), + CollectionId = IdUtils.GenerateUniqueId(), + Data = new TestData() + { + Name = "Test", + Age = 25 + } + }; _mockHttp.Expect(HttpMethod.Post, $"{TestConstants.Endpoint}/databases/{request.DatabaseId}/collections/{request.CollectionId}/documents") .ExpectedHeaders() @@ -35,11 +40,16 @@ public async Task CreateDocumentGeneric_ShouldReturnSuccess_WhenApiCallSucceeds( public async Task CreateDocumentGeneric_ShouldHandleException_WhenApiCallFails() { // Arrange - var request = CreateDocumentRequest.CreateBuilder() - .WithDatabaseId(IdUtils.GenerateUniqueId()) - .WithCollectionId(IdUtils.GenerateUniqueId()) - .AddField("AttributeName", "MyValue") - .Build(); + var request = new CreateDocumentRequest() + { + DatabaseId = IdUtils.GenerateUniqueId(), + CollectionId = IdUtils.GenerateUniqueId(), + Data = new TestData() + { + Name = "Test", + Age = 25 + } + }; _mockHttp.Expect(HttpMethod.Post, $"{TestConstants.Endpoint}/databases/{request.DatabaseId}/collections/{request.CollectionId}/documents") .ExpectedHeaders() @@ -58,11 +68,16 @@ public async Task CreateDocumentGeneric_ShouldHandleException_WhenApiCallFails() public async Task CreateDocumentGeneric_ShouldReturnErrorResponse_WhenExceptionOccurs() { // Arrange - var request = CreateDocumentRequest.CreateBuilder() - .WithDatabaseId(IdUtils.GenerateUniqueId()) - .WithCollectionId(IdUtils.GenerateUniqueId()) - .AddField("AttributeName", "MyValue") - .Build(); + var request = new CreateDocumentRequest() + { + DatabaseId = IdUtils.GenerateUniqueId(), + CollectionId = IdUtils.GenerateUniqueId(), + Data = new TestData() + { + Name = "Test", + Age = 25 + } + }; _mockHttp.Expect(HttpMethod.Post, $"{TestConstants.Endpoint}/databases/{request.DatabaseId}/collections/{request.CollectionId}/documents") .ExpectedHeaders() diff --git a/tests/PinguApps.Appwrite.Shared.Tests/Converters/SdkMarkerConverterTests.cs b/tests/PinguApps.Appwrite.Shared.Tests/Converters/SdkMarkerConverterTests.cs new file mode 100644 index 00000000..fdb3b25a --- /dev/null +++ b/tests/PinguApps.Appwrite.Shared.Tests/Converters/SdkMarkerConverterTests.cs @@ -0,0 +1,68 @@ +using System.Text.Json; +using PinguApps.Appwrite.Shared.Converters; + +namespace PinguApps.Appwrite.Shared.Tests.Converters; +public class SdkMarkerConverterTests +{ + private readonly SdkMarkerConverter _converter; + + public SdkMarkerConverterTests() + { + _converter = new SdkMarkerConverter(); + } + + [Fact] + public void CanConvert_AlwaysReturnsFalse() + { + // Arrange + var types = new[] + { + typeof(string), + typeof(int), + typeof(object), + typeof(SdkMarkerConverter) + }; + + // Act & Assert + foreach (var type in types) + { + Assert.False(_converter.CanConvert(type)); + } + } + + [Fact] + public void Read_ThrowsNotImplementedException() + { + // Arrange + var json = "{}"; + var reader = new Utf8JsonReader(System.Text.Encoding.UTF8.GetBytes(json)); + var options = new JsonSerializerOptions(); + var exceptionThrown = false; + + // Act + try + { + _converter.Read(ref reader, typeof(object), options); + } + catch (NotImplementedException) + { + exceptionThrown = true; + } + + // Assert + Assert.True(exceptionThrown); + } + + [Fact] + public void Write_ThrowsNotImplementedException() + { + // Arrange + using var stream = new System.IO.MemoryStream(); + using var writer = new Utf8JsonWriter(stream); + var options = new JsonSerializerOptions(); + + // Act & Assert + Assert.Throws(() => + _converter.Write(writer, new object(), options)); + } +} diff --git a/tests/PinguApps.Appwrite.Shared.Tests/Converters/UpdateDocumentRequestConverterTests.cs b/tests/PinguApps.Appwrite.Shared.Tests/Converters/UpdateDocumentRequestConverterTests.cs new file mode 100644 index 00000000..3b87f689 --- /dev/null +++ b/tests/PinguApps.Appwrite.Shared.Tests/Converters/UpdateDocumentRequestConverterTests.cs @@ -0,0 +1,217 @@ +using System.Text.Json; +using PinguApps.Appwrite.Shared.Converters; +using PinguApps.Appwrite.Shared.Requests.Databases; +using PinguApps.Appwrite.Shared.Utils; + +namespace PinguApps.Appwrite.Shared.Tests.Converters; +public class UpdateDocumentRequestConverterTests +{ + private readonly UpdateDocumentRequestConverter _converter; + private readonly JsonSerializerOptions _defaultOptions; + private readonly JsonSerializerOptions _sdkOptions; + + public UpdateDocumentRequestConverterTests() + { + _converter = new UpdateDocumentRequestConverter(); + + // Setup default options without SDK marker + _defaultOptions = new JsonSerializerOptions(); + + // Setup options with SDK marker + _sdkOptions = new JsonSerializerOptions(); + _sdkOptions.Converters.Add(new SdkMarkerConverter()); + } + + [Fact] + public void Read_ThrowsNotImplementedException() + { + // Arrange + var json = "{}"; + var reader = new Utf8JsonReader(System.Text.Encoding.UTF8.GetBytes(json)); + var threw = false; + + // Act + try + { + _converter.Read(ref reader, typeof(UpdateDocumentRequest), _defaultOptions); + } + catch (NotImplementedException) + { + threw = true; + } + + // Assert + Assert.True(threw, "Expected NotImplementedException was not thrown"); + } + + [Fact] + public void Write_WithoutSdk_WritesCollectionAndDatabaseIds() + { + // Arrange + var request = new UpdateDocumentRequest + { + CollectionId = "col123", + DatabaseId = "db456", + Data = [] + }; + + // Act + var json = JsonSerializer.Serialize(request, _defaultOptions); + + // Assert + var jsonDoc = JsonDocument.Parse(json); + Assert.Equal("col123", jsonDoc.RootElement.GetProperty("$collectionId").GetString()); + Assert.Equal("db456", jsonDoc.RootElement.GetProperty("$databaseId").GetString()); + } + + [Fact] + public void Write_WithSdk_DoesNotWriteCollectionAndDatabaseIds() + { + // Arrange + var request = new UpdateDocumentRequest + { + CollectionId = "col123", + DatabaseId = "db456", + Data = [] + }; + + // Act + var json = JsonSerializer.Serialize(request, _sdkOptions); + + // Assert + var jsonDoc = JsonDocument.Parse(json); + Assert.False(jsonDoc.RootElement.TryGetProperty("$collectionId", out _)); + Assert.False(jsonDoc.RootElement.TryGetProperty("$databaseId", out _)); + } + + [Fact] + public void Write_WithPermissions_WritesPermissionsProperty() + { + // Arrange + var request = new UpdateDocumentRequest + { + Permissions = [Permission.Read().Any()], + Data = [] + }; + + // Act + var json = JsonSerializer.Serialize(request, _defaultOptions); + + // Assert + var jsonDoc = JsonDocument.Parse(json); + Assert.True(jsonDoc.RootElement.TryGetProperty("permissions", out var permissionsElement)); + Assert.Equal(JsonValueKind.Array, permissionsElement.ValueKind); + } + + [Fact] + public void Write_WithoutPermissions_DoesNotWritePermissionsProperty() + { + // Arrange + var request = new UpdateDocumentRequest + { + Permissions = null, + Data = [] + }; + + // Act + var json = JsonSerializer.Serialize(request, _defaultOptions); + + // Assert + var jsonDoc = JsonDocument.Parse(json); + Assert.False(jsonDoc.RootElement.TryGetProperty("permissions", out _)); + } + + [Fact] + public void Write_WithData_WritesDataProperty() + { + // Arrange + var request = new UpdateDocumentRequest + { + Data = new Dictionary + { + { "name", "John" }, + { "age", 30 } + } + }; + + // Act + var json = JsonSerializer.Serialize(request, _defaultOptions); + + // Assert + var jsonDoc = JsonDocument.Parse(json); + Assert.True(jsonDoc.RootElement.TryGetProperty("data", out var dataElement)); + Assert.Equal("John", dataElement.GetProperty("name").GetString()); + Assert.Equal(30, dataElement.GetProperty("age").GetInt32()); + } + + [Fact] + public void Write_WithEmptyData_DoesNotWriteDataProperty() + { + // Arrange + var request = new UpdateDocumentRequest + { + Data = [] + }; + + // Act + var json = JsonSerializer.Serialize(request, _defaultOptions); + + // Assert + var jsonDoc = JsonDocument.Parse(json); + Assert.False(jsonDoc.RootElement.TryGetProperty("data", out _)); + } + + [Fact] + public void Write_WithComplexDataTypes_SerializesCorrectly() + { + // Arrange + var request = new UpdateDocumentRequest + { + Data = new Dictionary + { + { "array", new[] { 1, 2, 3 } }, + { "nested", new Dictionary + { + { "key", "value" } + } + } + } + }; + + // Act + var json = JsonSerializer.Serialize(request, _defaultOptions); + + // Assert + var jsonDoc = JsonDocument.Parse(json); + var dataElement = jsonDoc.RootElement.GetProperty("data"); + Assert.Equal(JsonValueKind.Array, dataElement.GetProperty("array").ValueKind); + Assert.Equal(JsonValueKind.Object, dataElement.GetProperty("nested").ValueKind); + } + + [Fact] + public void Write_WithAllProperties_GeneratesCorrectJson() + { + // Arrange + var request = new UpdateDocumentRequest + { + CollectionId = "col123", + DatabaseId = "db456", + Permissions = [Permission.Read().Any()], + Data = new Dictionary + { + { "name", "John" } + } + }; + + // Act + var json = JsonSerializer.Serialize(request, _defaultOptions); + + // Assert + var jsonDoc = JsonDocument.Parse(json); + Assert.Equal("col123", jsonDoc.RootElement.GetProperty("$collectionId").GetString()); + Assert.Equal("db456", jsonDoc.RootElement.GetProperty("$databaseId").GetString()); + Assert.True(jsonDoc.RootElement.TryGetProperty("permissions", out _)); + Assert.True(jsonDoc.RootElement.TryGetProperty("data", out var dataElement)); + Assert.Equal("John", dataElement.GetProperty("name").GetString()); + } +} diff --git a/tests/PinguApps.Appwrite.Shared.Tests/Requests/Databases/UpdateDocumentRequestBuilderTests.cs b/tests/PinguApps.Appwrite.Shared.Tests/Requests/Databases/UpdateDocumentRequestBuilderTests.cs index ed67a3dc..f9e5523a 100644 --- a/tests/PinguApps.Appwrite.Shared.Tests/Requests/Databases/UpdateDocumentRequestBuilderTests.cs +++ b/tests/PinguApps.Appwrite.Shared.Tests/Requests/Databases/UpdateDocumentRequestBuilderTests.cs @@ -1,5 +1,6 @@ using System.Text.Json.Serialization; using PinguApps.Appwrite.Shared.Requests.Databases; +using PinguApps.Appwrite.Shared.Responses; using PinguApps.Appwrite.Shared.Utils; namespace PinguApps.Appwrite.Shared.Tests.Requests.Databases; @@ -207,7 +208,12 @@ public string WriteOnlyProperty set { } } - public IEnumerable? CollectionProperty { get; set; } + public IEnumerable? CollectionProperty { get; set; } + } + public enum TestEnum + { + FirstValue, + SecondValue } [Fact] @@ -287,8 +293,8 @@ public void WithChanges_CollectionPropertyChanged_AddsToData() { // Arrange var builder = UpdateDocumentRequest.CreateBuilder(); - var before = new TestModel { CollectionProperty = [new object(), new object()] }; - var after = new TestModel { CollectionProperty = [new object()] }; + var before = new TestModel { CollectionProperty = [1, 2] }; + var after = new TestModel { CollectionProperty = [1] }; // Act var request = builder.WithChanges(before, after).Build(); @@ -303,7 +309,7 @@ public void WithChanges_CollectionPropertySameReference_NoChange() { // Arrange var builder = UpdateDocumentRequest.CreateBuilder(); - var collection = new[] { new object() }; + var collection = new[] { 1 }; var before = new TestModel { CollectionProperty = collection }; var after = new TestModel { CollectionProperty = collection }; @@ -345,4 +351,370 @@ public void WithChanges_PropertyChangedFromValueToNull_AddsToData() Assert.True(request.Data.ContainsKey("RegularProperty")); Assert.Null(request.Data["RegularProperty"]); } + + [Fact] + public void AddField_WhenValueIsEnum_StoresEnumAsString() + { + // Arrange + var builder = new UpdateDocumentRequestBuilder(); + var enumValue = TestEnum.FirstValue; + + // Act + builder.AddField("enumField", enumValue); + + // Assert + var request = builder.Build(); + var data = request.Data; + + Assert.True(data.ContainsKey("enumField")); + Assert.Equal("FirstValue", data["enumField"]); + } + + [Fact] + public void AddField_WhenValueIsPrimitive_StoresValue() + { + // Arrange + var builder = new UpdateDocumentRequestBuilder(); + sbyte primitiveValue = 42; + + // Act + builder.AddField("field", primitiveValue); + + // Assert + var request = builder.Build(); + Assert.Equal(primitiveValue, request.Data["field"]); + } + + [Fact] + public void AddField_WhenValueIsString_StoresValue() + { + // Arrange + var builder = new UpdateDocumentRequestBuilder(); + string stringValue = "test"; + + // Act + builder.AddField("field", stringValue); + + // Assert + var request = builder.Build(); + Assert.Equal(stringValue, request.Data["field"]); + } + + [Fact] + public void AddField_WhenValueIsDateTime_StoresValue() + { + // Arrange + var builder = new UpdateDocumentRequestBuilder(); + DateTime dateValue = new DateTime(2024, 1, 1); + + // Act + builder.AddField("field", dateValue); + + // Assert + var request = builder.Build(); + Assert.Equal(dateValue, request.Data["field"]); + } + + [Fact] + public void AddField_WhenValueIsDateTimeOffset_StoresValue() + { + // Arrange + var builder = new UpdateDocumentRequestBuilder(); + DateTimeOffset dateOffsetValue = new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero); + + // Act + builder.AddField("field", dateOffsetValue); + + // Assert + var request = builder.Build(); + Assert.Equal(dateOffsetValue, request.Data["field"]); + } + + [Fact] + public void AddField_WhenValueIsDecimal_StoresValue() + { + // Arrange + var builder = new UpdateDocumentRequestBuilder(); + decimal decimalValue = 42.42m; + + // Act + builder.AddField("field", decimalValue); + + // Assert + var request = builder.Build(); + Assert.Equal(decimalValue, request.Data["field"]); + } + + [Fact] + public void AddField_WhenValueIsEnum_StoresStringValue() + { + // Arrange + var builder = new UpdateDocumentRequestBuilder(); + TestEnum enumValue = TestEnum.FirstValue; + + // Act + builder.AddField("field", enumValue); + + // Assert + var request = builder.Build(); + Assert.Equal("FirstValue", request.Data["field"]); + } + + [Fact] + public void AddField_WhenValueIsNonStandardType_DoesNotStoreValue() + { + // Arrange + var builder = new UpdateDocumentRequestBuilder(); + var complexValue = new TestModel { RegularProperty = "test" }; + + // Act + builder.AddField("field", complexValue); + + // Assert + var request = builder.Build(); + Assert.False(request.Data.ContainsKey("field")); + } + + [Fact] + public void AddField_WhenValueIsEnumerableOfNonStandardType_DoesNotStoreValue() + { + // Arrange + var builder = new UpdateDocumentRequestBuilder(); + var complexList = new List + { + new() { RegularProperty = "test" }, + new() { RegularProperty = "test2" } + }; + + // Act + builder.AddField("field", complexList); + + // Assert + var request = builder.Build(); + Assert.False(request.Data.ContainsKey("field")); + } + + [Fact] + public void WithChanges_WhenTypeIsDocument_HandlesDocumentChanges() + { + // Arrange + var builder = new UpdateDocumentRequestBuilder(); + + var beforeDoc = new Document( + Id: "doc1", + CollectionId: "col1", + DatabaseId: "db1", + CreatedAt: DateTime.UtcNow.AddHours(-1), + UpdatedAt: DateTime.UtcNow.AddHours(-1), + Permissions: null, + Data: new TestData + { + Name = "Before", + Value = 1 + } + ); + + var afterDoc = new Document( + Id: "doc1", + CollectionId: "col1", + DatabaseId: "db1", + CreatedAt: beforeDoc.CreatedAt, + UpdatedAt: DateTime.UtcNow, + Permissions: null, + Data: new TestData + { + Name = "After", + Value = 2 + } + ); + + // Act + builder.WithChanges(beforeDoc, afterDoc); + + // Assert + var request = builder.Build(); + Assert.True(request.Data.ContainsKey("name")); + Assert.Equal("After", request.Data["name"]); + Assert.True(request.Data.ContainsKey("value")); + Assert.Equal(2, request.Data["value"]); + } + + private class TestData + { + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("value")] + public int Value { get; set; } + } + + [Fact] + public void WithChanges_WhenEnumerableValuesAreDifferent_DetectsChange() + { + // Arrange + var builder = new UpdateDocumentRequestBuilder(); + + var beforeDoc = new Document( + Id: "doc1", + CollectionId: "col1", + DatabaseId: "db1", + CreatedAt: DateTime.UtcNow.AddHours(-1), + UpdatedAt: DateTime.UtcNow.AddHours(-1), + Permissions: null, + Data: new TestDataWithList + { + Items = new List { "item1", "item2" } + } + ); + + var afterDoc = new Document( + Id: "doc1", + CollectionId: "col1", + DatabaseId: "db1", + CreatedAt: beforeDoc.CreatedAt, + UpdatedAt: DateTime.UtcNow, + Permissions: null, + Data: new TestDataWithList + { + Items = new List { "item1", "item3" } // Changed item2 to item3 + } + ); + + // Act + builder.WithChanges(beforeDoc, afterDoc); + + // Assert + var request = builder.Build(); + Assert.True(request.Data.ContainsKey("items")); + var items = Assert.IsType>(request.Data["items"]); + Assert.Equal(new List { "item1", "item3" }, items); + } + + private class TestDataWithList + { + [JsonPropertyName("items")] + public List Items { get; set; } = new(); + } + + [Fact] + public void WithChanges_WhenDocumentDataIsNull_DoesNothing() + { + // Arrange + var builder = new UpdateDocumentRequestBuilder(); + + var before = new Document( + Id: "doc1", + CollectionId: "col1", + DatabaseId: "db1", + CreatedAt: DateTime.UtcNow.AddHours(-1), + UpdatedAt: DateTime.UtcNow.AddHours(-1), + Permissions: null, + Data: null! // Force null despite non-nullable + ); + + var after = new Document( + Id: "doc1", + CollectionId: "col1", + DatabaseId: "db1", + CreatedAt: before.CreatedAt, + UpdatedAt: DateTime.UtcNow, + Permissions: null, + Data: null! // Force null despite non-nullable + ); + + // Act + builder.WithChanges(before, after); + + // Assert + var request = builder.Build(); + Assert.Empty(request.Data); + } + + [Fact] + public void WithChanges_WhenDocumentIdsAreDifferent_AddsAllProperties() + { + // Arrange + var builder = new UpdateDocumentRequestBuilder(); + + var before = new Document( + Id: "doc1", // Not null/empty + CollectionId: "col1", + DatabaseId: "db1", + CreatedAt: DateTime.UtcNow.AddHours(-1), + UpdatedAt: DateTime.UtcNow.AddHours(-1), + Permissions: null, + Data: new TestData + { + Name = "Before", + Value = 1 + } + ); + + var after = new Document( + Id: "doc2", // Different ID + CollectionId: "col1", + DatabaseId: "db1", + CreatedAt: before.CreatedAt, + UpdatedAt: DateTime.UtcNow, + Permissions: null, + Data: new TestData + { + Name = "After", + Value = 1 // Same value, but should still be added because IDs don't match + } + ); + + // Act + builder.WithChanges(before, after); + + // Assert + var request = builder.Build(); + // Should add all properties, even if they haven't changed + Assert.True(request.Data.ContainsKey("name")); + Assert.Equal("After", request.Data["name"]); + Assert.True(request.Data.ContainsKey("value")); + Assert.Equal(1, request.Data["value"]); + } + + [Fact] + public void WithChanges_WhenBothIdsAreNull_DoesNotCompareProperties() + { + // Arrange + var builder = new UpdateDocumentRequestBuilder(); + + var before = new Document( + Id: "", + CollectionId: "col1", + DatabaseId: "db1", + CreatedAt: DateTime.UtcNow.AddHours(-1), + UpdatedAt: DateTime.UtcNow.AddHours(-1), + Permissions: null, + Data: new TestData + { + Name = "Before", + Value = 1 + } + ); + + var after = new Document( + Id: "", + CollectionId: "col1", + DatabaseId: "db1", + CreatedAt: before.CreatedAt, + UpdatedAt: DateTime.UtcNow, + Permissions: null, + Data: new TestData + { + Name = "After", + Value = 2 + } + ); + + // Act + builder.WithChanges(before, after); + + // Assert + var request = builder.Build(); + Assert.NotEmpty(request.Data); + } } diff --git a/tests/PinguApps.Appwrite.Shared.Tests/Requests/Databases/UpdateDocumentRequestTests.cs b/tests/PinguApps.Appwrite.Shared.Tests/Requests/Databases/UpdateDocumentRequestTests.cs index fbd6a62f..b5becdb9 100644 --- a/tests/PinguApps.Appwrite.Shared.Tests/Requests/Databases/UpdateDocumentRequestTests.cs +++ b/tests/PinguApps.Appwrite.Shared.Tests/Requests/Databases/UpdateDocumentRequestTests.cs @@ -24,8 +24,7 @@ public void Constructor_InitializesWithExpectedValues() // Assert Assert.NotNull(request.Data); Assert.Empty(request.Data); - Assert.NotNull(request.Permissions); - Assert.Empty(request.Permissions); + Assert.Null(request.Permissions); } [Fact]