Skip to content

Commit

Permalink
Merge pull request #9 from neuroglia-io/fix-projection-validation
Browse files Browse the repository at this point in the history
Move projection validation from the `IngestCloudEventCommandHandler` into the `Repository` service
  • Loading branch information
cdavernas authored Feb 12, 2025
2 parents 4d5dd21 + aa3101a commit 432a583
Show file tree
Hide file tree
Showing 5 changed files with 45 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,6 @@ public class IngestCloudEventCommandHandler(ILogger<IngestCloudEventCommandHandl
/// </summary>
protected IEnumerable<IPatchHandler> PatchHandlers { get; } = patchHandlers;

/// <summary>
/// Gets the service used to validate schemas
/// </summary>
protected ISchemaValidator SchemaValidator { get; } = schemaValidator;

/// <summary>
/// Gets the service used to serialize/deserialize data to/from JSON
/// </summary>
Expand Down Expand Up @@ -134,8 +129,6 @@ protected virtual async Task ProcessCreateTriggerAsync(ProjectionType projection
ArgumentNullException.ThrowIfNull(e);
ArgumentException.ThrowIfNullOrWhiteSpace(correlationId);
var projection = (await ExpressionEvaluator.EvaluateAsync(trigger.State, e, cancellationToken: cancellationToken).ConfigureAwait(false))!;
var validationResult = await SchemaValidator.ValidateAsync(projection, projectionType.Schema, cancellationToken).ConfigureAwait(false);
if (!validationResult.IsSuccess()) throw new Exception($"Failed to validate the projection of type '{projectionType.Name}':{Environment.NewLine}{string.Join(Environment.NewLine, validationResult.Errors!)}");
var document = BsonDocument.Create(projection);
document["_id"] = correlationId;
await DbContext.Set(projectionType).AddAsync(document, cancellationToken).ConfigureAwait(false);
Expand Down Expand Up @@ -167,8 +160,6 @@ protected virtual async Task ProcessUpdateTriggerAsync(ProjectionType projection
var patchHandler = PatchHandlers.FirstOrDefault(h => h.Supports(trigger.Patch.Type)) ?? throw new NullReferenceException($"Failed to find an handler for the specified patch type '{trigger.Patch.Type}'");
var toPatch = JsonSerializer.Deserialize<object>(projection.ToJson(new() { OutputMode = JsonOutputMode.RelaxedExtendedJson }))!;
var patched = patchHandler.ApplyPatchAsync(trigger.Patch.Document, toPatch, cancellationToken).ConfigureAwait(false);
var validationResult = await SchemaValidator.ValidateAsync(patched, projectionType.Schema, cancellationToken).ConfigureAwait(false);
if (!validationResult.IsSuccess()) throw new Exception($"Failed to validate the projection of type '{projectionType.Name}':{Environment.NewLine}{string.Join(Environment.NewLine, validationResult.Errors!)}");
projection = BsonDocument.Create(patched);
break;
case ProjectionUpdateStrategy.Replace:
Expand All @@ -180,8 +171,6 @@ protected virtual async Task ProcessUpdateTriggerAsync(ProjectionType projection
object updated;
if (trigger.State is string expression) updated = (await ExpressionEvaluator.EvaluateAsync(expression, e, arguments, cancellationToken: cancellationToken).ConfigureAwait(false))!;
else updated = (await ExpressionEvaluator.EvaluateAsync(trigger.State!, e, arguments, cancellationToken: cancellationToken).ConfigureAwait(false))!;
validationResult = await SchemaValidator.ValidateAsync(updated, projectionType.Schema, cancellationToken).ConfigureAwait(false);
if (!validationResult.IsSuccess()) throw new Exception($"Failed to validate the projection of type '{projectionType.Name}':{Environment.NewLine}{string.Join(Environment.NewLine, validationResult.Errors!)}");
projection = BsonDocument.Create(updated);
projection["_id"] = correlationId;
projection[DocumentMetadata.PropertyName] = metadata;
Expand All @@ -208,7 +197,6 @@ protected virtual async Task ProcessDeleteTriggerAsync(ProjectionType projection
ArgumentNullException.ThrowIfNull(e);
ArgumentException.ThrowIfNullOrWhiteSpace(correlationId);
await DbContext.Set(projectionType).DeleteAsync(correlationId, cancellationToken).ConfigureAwait(false);
//todo: cascading delete
}

}
2 changes: 1 addition & 1 deletion src/CloudShapes.Application/Services/DbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ namespace CloudShapes.Application.Services;
/// </summary>
/// <param name="serviceProvider">The current <see cref="IServiceProvider"/></param>
/// <param name="database">The current <see cref="IMongoDatabase"/></param>
/// ^<param name="projectionTypes">The <see cref="IMongoCollection{TDocument}"/> used to manage <see cref="ProjectionType"/>s</param>
/// <param name="projectionTypes">The <see cref="IMongoCollection{TDocument}"/> used to manage <see cref="ProjectionType"/>s</param>
/// <param name="pluralize">The service used to pluralize terms</param>
public class DbContext(IServiceProvider serviceProvider, IMongoDatabase database, IMongoCollection<ProjectionType> projectionTypes, IPluralize pluralize)
: IDbContext
Expand Down
15 changes: 14 additions & 1 deletion src/CloudShapes.Application/Services/Repository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using CloudShapes.Data.Models;
using Neuroglia.Data;

namespace CloudShapes.Application.Services;

/// <summary>
Expand All @@ -21,12 +24,13 @@ namespace CloudShapes.Application.Services;
/// <param name="database">The current <see cref="IMongoDatabase"/></param>
/// <param name="projectionTypes">The <see cref="IMongoCollection{TDocument}"/> used to persist <see cref="ProjectionType"/>s</param>
/// <param name="projections">The <see cref="IMongoCollection{TDocument}"/> used to store projections managed by the <see cref="IRepository"/></param>
/// <param name="schemaValidator">The service used to validate schemas</param>
/// <param name="dbContext">The current <see cref="IDbContext"/></param>
/// <param name="cloudEventBus">The service used to observe both inbound and outbound <see cref="CloudEvent"/>s</param>
/// <param name="jsonSerializer">The service used to serialize/deserialize data to/from JSON</param>
/// <param name="type">The type of projections managed by the <see cref="IRepository"/></param>
public class Repository(ILogger<Repository> logger, IOptions<ApplicationOptions> options, IMongoDatabase database, IMongoCollection<ProjectionType> projectionTypes,
IMongoCollection<BsonDocument> projections, IDbContext dbContext, ICloudEventBus cloudEventBus, IJsonSerializer jsonSerializer, ProjectionType type)
IMongoCollection<BsonDocument> projections, ISchemaValidator schemaValidator, IDbContext dbContext, ICloudEventBus cloudEventBus, IJsonSerializer jsonSerializer, ProjectionType type)
: IRepository
{

Expand Down Expand Up @@ -60,6 +64,11 @@ public class Repository(ILogger<Repository> logger, IOptions<ApplicationOptions>
/// </summary>
protected IDbContext DbContext { get; } = dbContext;

/// <summary>
/// Gets the service used to validate schemas
/// </summary>
protected ISchemaValidator SchemaValidator { get; } = schemaValidator;

/// <summary>
/// Gets the service used to observe both inbound and outbound <see cref="CloudEvent"/>s
/// </summary>
Expand Down Expand Up @@ -108,6 +117,8 @@ public virtual async Task<IAsyncCursor<BsonDocument>> FindAsync(FilterDefinition
public virtual async Task AddAsync(BsonDocument projection, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(projection);
var validationResult = await SchemaValidator.ValidateAsync(BsonTypeMapper.MapToDotNetValue(projection), Type.Schema, cancellationToken).ConfigureAwait(false);
if (!validationResult.IsSuccess()) throw new ProblemDetailsException(new(Problems.Types.ValidationFailed, Problems.Titles.ValidationFailed, Problems.Statuses.ValidationFailed, StringFormatter.Format(Problems.Details.ProjectionValidationFailed, Type.Name, string.Join(Environment.NewLine, validationResult.Errors!))));
var projectionId = projection["_id"].ToString()!;
projection = projection.InsertMetadata(new DocumentMetadata());
if (Type.Relationships != null)
Expand Down Expand Up @@ -167,6 +178,8 @@ public virtual async Task AddAsync(BsonDocument projection, CancellationToken ca
public virtual async Task UpdateAsync(BsonDocument projection, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(projection);
var validationResult = await SchemaValidator.ValidateAsync(BsonTypeMapper.MapToDotNetValue(projection), Type.Schema, cancellationToken).ConfigureAwait(false);
if (!validationResult.IsSuccess()) throw new ProblemDetailsException(new(Problems.Types.ValidationFailed, Problems.Titles.ValidationFailed, Problems.Statuses.ValidationFailed, StringFormatter.Format(Problems.Details.ProjectionValidationFailed, Type.Name, string.Join(Environment.NewLine, validationResult.Errors!))));
var metadata = BsonSerializer.Deserialize<DocumentMetadata>(projection[DocumentMetadata.PropertyName].AsBsonDocument);
metadata.Update();
projection = projection.InsertMetadata(metadata);
Expand Down
30 changes: 28 additions & 2 deletions src/CloudShapes.Data/Problems.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using System.Net;

namespace CloudShapes.Data;

/// <summary>
Expand Down Expand Up @@ -44,6 +42,10 @@ public static class Types
/// Gets the uri that describes the type of problems that occur when Cloud Shapes failed to find the specified entity
/// </summary>
public static readonly Uri NotFound = new(BaseUri, "/not-found");
/// <summary>
/// Gets the uri that describes the type of problems that occur when Cloud Shapes failed to validate data
/// </summary>
public static readonly Uri ValidationFailed = new(BaseUri, "/validation-failed");

/// <summary>
/// Gets an <see cref="IEnumerable{T}"/> that contains all Cloud Shapes problem types
Expand All @@ -54,6 +56,7 @@ public static IEnumerable<Uri> AsEnumerable()
yield return IndexCreationFailed;
yield return KeyAlreadyExists;
yield return NotFound;
yield return ValidationFailed;
}

}
Expand All @@ -76,6 +79,10 @@ public static class Titles
/// Gets the title of the problem that occurs when Cloud Shapes failed to find the specified entity
/// </summary>
public const string NotFound = "Not Found";
/// <summary>
/// Gets the title of the problem that occurs when Cloud Shapes failed to validate data
/// </summary>
public const string ValidationFailed = "Validation Failed";

/// <summary>
/// Gets an <see cref="IEnumerable{T}"/> that contains all Cloud Shapes problem titles
Expand All @@ -86,6 +93,7 @@ public static IEnumerable<string> AsEnumerable()
yield return IndexCreationFailed;
yield return KeyAlreadyExists;
yield return NotFound;
yield return ValidationFailed;
}

}
Expand All @@ -104,6 +112,10 @@ public static class Statuses
/// Gets the status for problems that describe an unprocessable operation
/// </summary>
public const int Unprocessable = (int)HttpStatusCode.UnprocessableContent;
/// <summary>
/// Gets the status for problems that describe validation failures
/// </summary>
public const int ValidationFailed = (int)HttpStatusCode.UnprocessableContent;

/// <summary>
/// Gets an <see cref="IEnumerable{T}"/> that contains all Cloud Shapes problem statuses
Expand All @@ -113,8 +125,22 @@ public static IEnumerable<int> AsEnumerable()
{
yield return NotFound;
yield return Unprocessable;
yield return ValidationFailed;
}

}

/// <summary>
/// Exposes constants about Cloud Shapes related problem details
/// </summary>
public static class Details
{

/// <summary>
/// Gets the details template of a problem due to a projection validation failure
/// </summary>
public const string ProjectionValidationFailed = "Failed to validate a projection of type '{type}':\r\n{errors}";

}

}
3 changes: 2 additions & 1 deletion src/CloudShapes.Data/Usings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
global using System;
global using System.Collections.Generic;
global using System.ComponentModel.DataAnnotations;
global using System.Net;
global using System.Text.Json;
global using System.Text.Json.Serialization;
global using System.Text.RegularExpressions;
global using YamlDotNet.Serialization;
global using YamlDotNet.Serialization;

0 comments on commit 432a583

Please sign in to comment.