Skip to content

Commit

Permalink
Merge pull request #616 from PinguApps/feature/better-doc-serialization
Browse files Browse the repository at this point in the history
Better Document serialization
  • Loading branch information
pingu2k4 authored Jan 11, 2025
2 parents ed4e4b2 + ae4c2fb commit 8109773
Show file tree
Hide file tree
Showing 23 changed files with 959 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,14 @@ public async Task<AppwriteResult<Document>> CreateDocument(CreateDocumentRequest
}

/// <inheritdoc/>
public async Task<AppwriteResult<Document<TData>>> CreateDocument<TData>(CreateDocumentRequest request)
public async Task<AppwriteResult<Document<TData>>> CreateDocument<TData>(CreateDocumentRequest<TData> request)
where TData : class, new()
{
try
{
request.Validate(true);

var result = await _databasesApi.CreateDocument<TData>(GetCurrentSession(), request.DatabaseId, request.CollectionId, request);
var result = await _databasesApi.CreateDocument(GetCurrentSession(), request.DatabaseId, request.CollectionId, request);

return result.GetApiResponse();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public interface IClientDatabasesClient
/// <typeparam name="TData">The type of the document data</typeparam>
/// <param name="request">The request content</param>
/// <returns>The document</returns>
Task<AppwriteResult<Document<TData>>> CreateDocument<TData>(CreateDocumentRequest request) where TData : class, new();
Task<AppwriteResult<Document<TData>>> CreateDocument<TData>(CreateDocumentRequest<TData> request) where TData : class, new();

/// <summary>
/// Delete a document by its unique ID.
Expand Down
2 changes: 1 addition & 1 deletion src/PinguApps.Appwrite.Client/Internals/IDatabasesApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ internal interface IDatabasesApi : IBaseApi
Task<IApiResponse<Document>> CreateDocument([Header("x-appwrite-session")] string? session, string databaseId, string collectionId, CreateDocumentRequest request);

[Post("/databases/{databaseId}/collections/{collectionId}/documents")]
Task<IApiResponse<Document<TData>>> CreateDocument<TData>([Header("x-appwrite-session")] string? session, string databaseId, string collectionId, CreateDocumentRequest request) where TData : class, new();
Task<IApiResponse<Document<TData>>> CreateDocument<TData>([Header("x-appwrite-session")] string? session, string databaseId, string collectionId, CreateDocumentRequest<TData> request) where TData : class, new();

[Delete("/databases/{databaseId}/collections/{collectionId}/documents/{documentId}")]
Task<IApiResponse> DeleteDocument([Header("x-appwrite-session")] string? session, string databaseId, string collectionId, string documentId);
Expand Down
6 changes: 6 additions & 0 deletions src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ public interface IServerDatabasesClient
/// <typeparam name="TData">The data type for your document</typeparam>
/// <param name="request">The request content</param>
/// <returns>The document</returns>
Task<AppwriteResult<Document<TData>>> CreateDocument<TData>(CreateDocumentRequest request) where TData : class, new();
Task<AppwriteResult<Document<TData>>> CreateDocument<TData>(CreateDocumentRequest<TData> request) where TData : class, new();

/// <summary>
/// Delete a document by its unique ID.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -634,7 +634,7 @@ public async Task<AppwriteResult<Document>> CreateDocument(CreateDocumentRequest
}

/// <inheritdoc/>
public async Task<AppwriteResult<Document<TData>>> CreateDocument<TData>(CreateDocumentRequest request)
public async Task<AppwriteResult<Document<TData>>> CreateDocument<TData>(CreateDocumentRequest<TData> request)
where TData : class, new()
{
try
Expand Down
2 changes: 1 addition & 1 deletion src/PinguApps.Appwrite.Server/Internals/IDatabasesApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ internal interface IDatabasesApi : IBaseApi
Task<IApiResponse<Document>> CreateDocument(string databaseId, string collectionId, CreateDocumentRequest request);

[Post("/databases/{databaseId}/collections/{collectionId}/documents")]
Task<IApiResponse<Document<TData>>> CreateDocument<TData>(string databaseId, string collectionId, CreateDocumentRequest request) where TData : class, new();
Task<IApiResponse<Document<TData>>> CreateDocument<TData>(string databaseId, string collectionId, CreateDocumentRequest<TData> request) where TData : class, new();

[Delete("/databases/{databaseId}/collections/{collectionId}/documents/{documentId}")]
Task<IApiResponse> DeleteDocument(string databaseId, string collectionId, string documentId);
Expand Down
6 changes: 6 additions & 0 deletions src/PinguApps.Appwrite.Server/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
2 changes: 1 addition & 1 deletion src/PinguApps.Appwrite.Shared/Constants.cs
Original file line number Diff line number Diff line change
@@ -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";
}
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,12 @@ public override void Write(Utf8JsonWriter writer, Document<TData> 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<>);
}

Expand Down
14 changes: 14 additions & 0 deletions src/PinguApps.Appwrite.Shared/Converters/SdkMarkerConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace PinguApps.Appwrite.Shared.Converters;
public class SdkMarkerConverter : JsonConverter<object>
{
// 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();
}
Original file line number Diff line number Diff line change
@@ -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<UpdateDocumentRequest>
{
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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace PinguApps.Appwrite.Shared.Requests.Databases;
/// <summary>
/// The request to update a document. Can only be created with <see cref="CreateBuilder"/>
/// </summary>
[JsonConverter(typeof(UpdateDocumentRequestConverter))]
public class UpdateDocumentRequest : DatabaseCollectionDocumentIdBaseRequest<UpdateDocumentRequest, UpdateDocumentRequestValidator>
{
internal UpdateDocumentRequest() { }
Expand All @@ -24,7 +25,7 @@ internal UpdateDocumentRequest() { }
/// </summary>
[JsonPropertyName("permissions")]
[JsonConverter(typeof(PermissionListConverter))]
public List<Permission> Permissions { get; set; } = [];
public List<Permission>? Permissions { get; set; }

/// <summary>
/// Creates a new builder for creating a document request
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -38,13 +39,62 @@ public IUpdateDocumentRequestBuilder WithPermissions(List<Permission> 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<T> (but not string, which is IEnumerable<char>)
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;
}

Expand All @@ -59,6 +109,13 @@ public IUpdateDocumentRequestBuilder WithChanges<T>(T before, T after) where T :
throw new ArgumentNullException(nameof(after));
}

// Check if T is Document<TData>
if (IsDocumentType(typeof(T)))
{
HandleDocumentChanges(before, after);
return this;
}

var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);

foreach (var property in properties)
Expand Down Expand Up @@ -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>(T before, T after) where T : class
{
var documentType = typeof(T);
var idProperty = documentType.GetProperty(nameof(Document<object>.Id));
var dataProperty = documentType.GetProperty(nameof(Document<object>.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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,8 @@ public UpdateDocumentRequestValidator()
{
Include(new DatabaseCollectionDocumentIdBaseRequestValidator<UpdateDocumentRequest, UpdateDocumentRequestValidator>());

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.");
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
Loading

0 comments on commit 8109773

Please sign in to comment.