diff --git a/src/Workflow.Api/Configuration/JwtAuthConfiguration.cs b/src/Workflow.Api/Configuration/JwtAuthConfiguration.cs new file mode 100644 index 0000000..b8cf99c --- /dev/null +++ b/src/Workflow.Api/Configuration/JwtAuthConfiguration.cs @@ -0,0 +1,17 @@ +using FastEndpoints.Security; +using Workflow.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; + +namespace Workflow.Api.Configuration; + +public static class JwtAuthConfiguration +{ + public static void AddJwtAuthentication(this IServiceCollection services, IConfiguration configuration) + { + var jwtConfig = configuration.GetSection("Jwt"); + + services.AddJWTBearerAuth(jwtConfig["SigningKey"]!, bearerEvents: null); + + services.AddAuthorizationBuilder(); + } +} diff --git a/src/Workflow.Api/Endpoints/Form/CreateFormDefinitionEndpoint.cs b/src/Workflow.Api/Endpoints/Form/CreateFormDefinitionEndpoint.cs new file mode 100644 index 0000000..c4a178f --- /dev/null +++ b/src/Workflow.Api/Endpoints/Form/CreateFormDefinitionEndpoint.cs @@ -0,0 +1,38 @@ +using FastEndpoints; +using MediatR; +using Workflow.Application.Form.DTOs; +using Workflow.Application.Form.FormDefinition.Commands; + +namespace Workflow.Api.Endpoints.Form; + +public class CreateFormDefinitionEndpoint : Endpoint +{ + private readonly IMediator _mediator; + + public CreateFormDefinitionEndpoint(IMediator mediator) => _mediator = mediator; + + public override void Configure() + { + Post("/forms"); + AllowAnonymous(); + Summary(s => + { + s.Summary = "Create a new form definition"; + }); + } + + public override async Task HandleAsync(CreateFormDefinitionRequest req, CancellationToken ct) + { + var command = new CreateFormDefinitionCommand(req.Name, req.Code, req.Description, req.Fields); + var result = await _mediator.Send(command, ct); + await SendAsync(result, 200, ct); + } +} + +public class CreateFormDefinitionRequest +{ + public string Name { get; set; } = string.Empty; + public string Code { get; set; } = string.Empty; + public string? Description { get; set; } + public List Fields { get; set; } = new(); +} diff --git a/src/Workflow.Api/Endpoints/Form/DeleteFormDefinitionEndpoint.cs b/src/Workflow.Api/Endpoints/Form/DeleteFormDefinitionEndpoint.cs new file mode 100644 index 0000000..d8465b2 --- /dev/null +++ b/src/Workflow.Api/Endpoints/Form/DeleteFormDefinitionEndpoint.cs @@ -0,0 +1,34 @@ +using FastEndpoints; +using MediatR; +using Workflow.Application.Form.FormDefinition.Commands; + +namespace Workflow.Api.Endpoints.Form; + +public class DeleteFormDefinitionEndpoint : Endpoint +{ + private readonly IMediator _mediator; + + public DeleteFormDefinitionEndpoint(IMediator mediator) => _mediator = mediator; + + public override void Configure() + { + Delete("/forms/{Id}"); + AllowAnonymous(); + Summary(s => + { + s.Summary = "Delete a form definition (soft delete)"; + }); + } + + public override async Task HandleAsync(DeleteFormDefinitionRequest req, CancellationToken ct) + { + var command = new DeleteFormDefinitionCommand(req.Id); + await _mediator.Send(command, ct); + await SendOkAsync(ct); + } +} + +public class DeleteFormDefinitionRequest +{ + public Guid Id { get; set; } +} diff --git a/src/Workflow.Api/Endpoints/Form/GetFormDataByInstanceEndpoint.cs b/src/Workflow.Api/Endpoints/Form/GetFormDataByInstanceEndpoint.cs new file mode 100644 index 0000000..0ba67ba --- /dev/null +++ b/src/Workflow.Api/Endpoints/Form/GetFormDataByInstanceEndpoint.cs @@ -0,0 +1,35 @@ +using FastEndpoints; +using MediatR; +using Workflow.Application.Form.DTOs; +using Workflow.Application.Form.FormData.Queries; + +namespace Workflow.Api.Endpoints.Form; + +public class GetFormDataByInstanceEndpoint : Endpoint +{ + private readonly IMediator _mediator; + + public GetFormDataByInstanceEndpoint(IMediator mediator) => _mediator = mediator; + + public override void Configure() + { + Get("/forms/data/{InstanceId}"); + AllowAnonymous(); + Summary(s => + { + s.Summary = "Get form data by workflow instance id"; + }); + } + + public override async Task HandleAsync(GetFormDataByInstanceRequest req, CancellationToken ct) + { + var query = new GetFormDataByInstanceQuery(req.InstanceId); + var result = await _mediator.Send(query, ct); + await SendAsync(result, 200, ct); + } +} + +public class GetFormDataByInstanceRequest +{ + public Guid InstanceId { get; set; } +} diff --git a/src/Workflow.Api/Endpoints/Form/GetFormDefinitionByIdEndpoint.cs b/src/Workflow.Api/Endpoints/Form/GetFormDefinitionByIdEndpoint.cs new file mode 100644 index 0000000..a12fbeb --- /dev/null +++ b/src/Workflow.Api/Endpoints/Form/GetFormDefinitionByIdEndpoint.cs @@ -0,0 +1,35 @@ +using FastEndpoints; +using MediatR; +using Workflow.Application.Form.DTOs; +using Workflow.Application.Form.FormDefinition.Queries; + +namespace Workflow.Api.Endpoints.Form; + +public class GetFormDefinitionByIdEndpoint : Endpoint +{ + private readonly IMediator _mediator; + + public GetFormDefinitionByIdEndpoint(IMediator mediator) => _mediator = mediator; + + public override void Configure() + { + Get("/forms/{Id}"); + AllowAnonymous(); + Summary(s => + { + s.Summary = "Get form definition detail by id (includes fields)"; + }); + } + + public override async Task HandleAsync(GetFormDefinitionByIdRequest req, CancellationToken ct) + { + var query = new GetFormDefinitionByIdQuery(req.Id); + var result = await _mediator.Send(query, ct); + await SendAsync(result, 200, ct); + } +} + +public class GetFormDefinitionByIdRequest +{ + public Guid Id { get; set; } +} diff --git a/src/Workflow.Api/Endpoints/Form/GetFormDefinitionListEndpoint.cs b/src/Workflow.Api/Endpoints/Form/GetFormDefinitionListEndpoint.cs new file mode 100644 index 0000000..63d0e61 --- /dev/null +++ b/src/Workflow.Api/Endpoints/Form/GetFormDefinitionListEndpoint.cs @@ -0,0 +1,38 @@ +using FastEndpoints; +using MediatR; +using Workflow.Application.Form.DTOs; +using Workflow.Application.Form.FormDefinition.Queries; +using Workflow.Domain.Enums; + +namespace Workflow.Api.Endpoints.Form; + +public class GetFormDefinitionListEndpoint : Endpoint> +{ + private readonly IMediator _mediator; + + public GetFormDefinitionListEndpoint(IMediator mediator) => _mediator = mediator; + + public override void Configure() + { + Get("/forms"); + AllowAnonymous(); + Summary(s => + { + s.Summary = "Get paginated list of form definitions"; + }); + } + + public override async Task HandleAsync(GetFormDefinitionListRequest req, CancellationToken ct) + { + var query = new GetFormDefinitionListQuery(req.PageIndex, req.PageSize, req.Status); + var result = await _mediator.Send(query, ct); + await SendAsync(result, 200, ct); + } +} + +public class GetFormDefinitionListRequest +{ + public int PageIndex { get; set; } = 1; + public int PageSize { get; set; } = 20; + public FormStatus? Status { get; set; } +} diff --git a/src/Workflow.Api/Endpoints/Form/PublishFormDefinitionEndpoint.cs b/src/Workflow.Api/Endpoints/Form/PublishFormDefinitionEndpoint.cs new file mode 100644 index 0000000..686471b --- /dev/null +++ b/src/Workflow.Api/Endpoints/Form/PublishFormDefinitionEndpoint.cs @@ -0,0 +1,34 @@ +using FastEndpoints; +using MediatR; +using Workflow.Application.Form.FormDefinition.Commands; + +namespace Workflow.Api.Endpoints.Form; + +public class PublishFormDefinitionEndpoint : Endpoint +{ + private readonly IMediator _mediator; + + public PublishFormDefinitionEndpoint(IMediator mediator) => _mediator = mediator; + + public override void Configure() + { + Post("/forms/{Id}/publish"); + AllowAnonymous(); + Summary(s => + { + s.Summary = "Publish a form definition"; + }); + } + + public override async Task HandleAsync(PublishFormDefinitionRequest req, CancellationToken ct) + { + var command = new PublishFormDefinitionCommand(req.Id); + await _mediator.Send(command, ct); + await SendOkAsync(ct); + } +} + +public class PublishFormDefinitionRequest +{ + public Guid Id { get; set; } +} diff --git a/src/Workflow.Api/Endpoints/Form/SubmitFormDataEndpoint.cs b/src/Workflow.Api/Endpoints/Form/SubmitFormDataEndpoint.cs new file mode 100644 index 0000000..3c65931 --- /dev/null +++ b/src/Workflow.Api/Endpoints/Form/SubmitFormDataEndpoint.cs @@ -0,0 +1,36 @@ +using FastEndpoints; +using MediatR; +using Workflow.Application.Form.FormData.Commands; + +namespace Workflow.Api.Endpoints.Form; + +public class SubmitFormDataEndpoint : Endpoint +{ + private readonly IMediator _mediator; + + public SubmitFormDataEndpoint(IMediator mediator) => _mediator = mediator; + + public override void Configure() + { + Post("/forms/data"); + AllowAnonymous(); + Summary(s => + { + s.Summary = "Submit form data for a workflow instance"; + }); + } + + public override async Task HandleAsync(SubmitFormDataRequest req, CancellationToken ct) + { + var command = new SubmitFormDataCommand(req.FormDefinitionId, req.InstanceId, req.DataJson); + var result = await _mediator.Send(command, ct); + await SendAsync(result, 200, ct); + } +} + +public class SubmitFormDataRequest +{ + public Guid FormDefinitionId { get; set; } + public Guid InstanceId { get; set; } + public string DataJson { get; set; } = string.Empty; +} diff --git a/src/Workflow.Api/Endpoints/Form/UpdateFormDefinitionEndpoint.cs b/src/Workflow.Api/Endpoints/Form/UpdateFormDefinitionEndpoint.cs new file mode 100644 index 0000000..4413b3c --- /dev/null +++ b/src/Workflow.Api/Endpoints/Form/UpdateFormDefinitionEndpoint.cs @@ -0,0 +1,38 @@ +using FastEndpoints; +using MediatR; +using Workflow.Application.Form.DTOs; +using Workflow.Application.Form.FormDefinition.Commands; + +namespace Workflow.Api.Endpoints.Form; + +public class UpdateFormDefinitionEndpoint : Endpoint +{ + private readonly IMediator _mediator; + + public UpdateFormDefinitionEndpoint(IMediator mediator) => _mediator = mediator; + + public override void Configure() + { + Put("/forms/{Id}"); + AllowAnonymous(); + Summary(s => + { + s.Summary = "Update a form definition"; + }); + } + + public override async Task HandleAsync(UpdateFormDefinitionRequest req, CancellationToken ct) + { + var command = new UpdateFormDefinitionCommand(req.Id, req.Name, req.Description, req.Fields); + var result = await _mediator.Send(command, ct); + await SendAsync(result, 200, ct); + } +} + +public class UpdateFormDefinitionRequest +{ + public Guid Id { get; set; } + public string Name { get; set; } = string.Empty; + public string? Description { get; set; } + public List Fields { get; set; } = new(); +} diff --git a/src/Workflow.Api/Endpoints/WorkflowDefinition/CreateWorkflowDefinitionEndpoint.cs b/src/Workflow.Api/Endpoints/WorkflowDefinition/CreateWorkflowDefinitionEndpoint.cs new file mode 100644 index 0000000..02a9c2b --- /dev/null +++ b/src/Workflow.Api/Endpoints/WorkflowDefinition/CreateWorkflowDefinitionEndpoint.cs @@ -0,0 +1,37 @@ +using FastEndpoints; +using MediatR; +using Workflow.Application.Features.WorkflowDefinitions.Commands; +using Workflow.Application.Features.WorkflowDefinitions.DTOs; + +namespace Workflow.Api.Endpoints.WorkflowDefinition; + +public class CreateWorkflowDefinitionEndpoint : Endpoint +{ + private readonly IMediator _mediator; + + public CreateWorkflowDefinitionEndpoint(IMediator mediator) => _mediator = mediator; + + public override void Configure() + { + Post("/workflow-definitions"); + AllowAnonymous(); + Summary(s => + { + s.Summary = "Create a new workflow definition"; + }); + } + + public override async Task HandleAsync(CreateWorkflowDefinitionRequest req, CancellationToken ct) + { + var command = new CreateWorkflowDefinitionCommand(req.Name, req.Code, req.Description); + var result = await _mediator.Send(command, ct); + await SendAsync(result, 200, ct); + } +} + +public class CreateWorkflowDefinitionRequest +{ + public string Name { get; set; } = string.Empty; + public string Code { get; set; } = string.Empty; + public string? Description { get; set; } +} diff --git a/src/Workflow.Api/Endpoints/WorkflowDefinition/DeleteWorkflowDefinitionEndpoint.cs b/src/Workflow.Api/Endpoints/WorkflowDefinition/DeleteWorkflowDefinitionEndpoint.cs new file mode 100644 index 0000000..f825181 --- /dev/null +++ b/src/Workflow.Api/Endpoints/WorkflowDefinition/DeleteWorkflowDefinitionEndpoint.cs @@ -0,0 +1,34 @@ +using FastEndpoints; +using MediatR; +using Workflow.Application.Features.WorkflowDefinitions.Commands; + +namespace Workflow.Api.Endpoints.WorkflowDefinition; + +public class DeleteWorkflowDefinitionEndpoint : Endpoint +{ + private readonly IMediator _mediator; + + public DeleteWorkflowDefinitionEndpoint(IMediator mediator) => _mediator = mediator; + + public override void Configure() + { + Delete("/workflow-definitions/{Id}"); + AllowAnonymous(); + Summary(s => + { + s.Summary = "Delete a workflow definition (soft delete)"; + }); + } + + public override async Task HandleAsync(DeleteWorkflowDefinitionRequest req, CancellationToken ct) + { + var command = new DeleteWorkflowDefinitionCommand(req.Id); + await _mediator.Send(command, ct); + await SendOkAsync(ct); + } +} + +public class DeleteWorkflowDefinitionRequest +{ + public Guid Id { get; set; } +} diff --git a/src/Workflow.Api/Endpoints/WorkflowDefinition/DisableWorkflowDefinitionEndpoint.cs b/src/Workflow.Api/Endpoints/WorkflowDefinition/DisableWorkflowDefinitionEndpoint.cs new file mode 100644 index 0000000..e8abbac --- /dev/null +++ b/src/Workflow.Api/Endpoints/WorkflowDefinition/DisableWorkflowDefinitionEndpoint.cs @@ -0,0 +1,34 @@ +using FastEndpoints; +using MediatR; +using Workflow.Application.Features.WorkflowDefinitions.Commands; + +namespace Workflow.Api.Endpoints.WorkflowDefinition; + +public class DisableWorkflowDefinitionEndpoint : Endpoint +{ + private readonly IMediator _mediator; + + public DisableWorkflowDefinitionEndpoint(IMediator mediator) => _mediator = mediator; + + public override void Configure() + { + Post("/workflow-definitions/{Id}/disable"); + AllowAnonymous(); + Summary(s => + { + s.Summary = "Disable a workflow definition"; + }); + } + + public override async Task HandleAsync(DisableWorkflowDefinitionRequest req, CancellationToken ct) + { + var command = new DisableWorkflowDefinitionCommand(req.Id); + await _mediator.Send(command, ct); + await SendOkAsync(ct); + } +} + +public class DisableWorkflowDefinitionRequest +{ + public Guid Id { get; set; } +} diff --git a/src/Workflow.Api/Endpoints/WorkflowDefinition/GetWorkflowDefinitionByIdEndpoint.cs b/src/Workflow.Api/Endpoints/WorkflowDefinition/GetWorkflowDefinitionByIdEndpoint.cs new file mode 100644 index 0000000..5ca6772 --- /dev/null +++ b/src/Workflow.Api/Endpoints/WorkflowDefinition/GetWorkflowDefinitionByIdEndpoint.cs @@ -0,0 +1,35 @@ +using FastEndpoints; +using MediatR; +using Workflow.Application.Features.WorkflowDefinitions.DTOs; +using Workflow.Application.Features.WorkflowDefinitions.Queries; + +namespace Workflow.Api.Endpoints.WorkflowDefinition; + +public class GetWorkflowDefinitionByIdEndpoint : Endpoint +{ + private readonly IMediator _mediator; + + public GetWorkflowDefinitionByIdEndpoint(IMediator mediator) => _mediator = mediator; + + public override void Configure() + { + Get("/workflow-definitions/{Id}"); + AllowAnonymous(); + Summary(s => + { + s.Summary = "Get workflow definition detail by id (includes nodes and edges)"; + }); + } + + public override async Task HandleAsync(GetWorkflowDefinitionByIdRequest req, CancellationToken ct) + { + var query = new GetWorkflowDefinitionByIdQuery(req.Id); + var result = await _mediator.Send(query, ct); + await SendAsync(result, 200, ct); + } +} + +public class GetWorkflowDefinitionByIdRequest +{ + public Guid Id { get; set; } +} diff --git a/src/Workflow.Api/Endpoints/WorkflowDefinition/GetWorkflowDefinitionListEndpoint.cs b/src/Workflow.Api/Endpoints/WorkflowDefinition/GetWorkflowDefinitionListEndpoint.cs new file mode 100644 index 0000000..3e23d10 --- /dev/null +++ b/src/Workflow.Api/Endpoints/WorkflowDefinition/GetWorkflowDefinitionListEndpoint.cs @@ -0,0 +1,39 @@ +using FastEndpoints; +using MediatR; +using Workflow.Application.Common; +using Workflow.Application.Features.WorkflowDefinitions.DTOs; +using Workflow.Application.Features.WorkflowDefinitions.Queries; +using Workflow.Domain.Enums; + +namespace Workflow.Api.Endpoints.WorkflowDefinition; + +public class GetWorkflowDefinitionListEndpoint : Endpoint> +{ + private readonly IMediator _mediator; + + public GetWorkflowDefinitionListEndpoint(IMediator mediator) => _mediator = mediator; + + public override void Configure() + { + Get("/workflow-definitions"); + AllowAnonymous(); + Summary(s => + { + s.Summary = "Get paginated list of workflow definitions"; + }); + } + + public override async Task HandleAsync(GetWorkflowDefinitionListRequest req, CancellationToken ct) + { + var query = new GetWorkflowDefinitionListQuery(req.PageIndex, req.PageSize, req.Status); + var result = await _mediator.Send(query, ct); + await SendAsync(result, 200, ct); + } +} + +public class GetWorkflowDefinitionListRequest +{ + public int PageIndex { get; set; } = 1; + public int PageSize { get; set; } = 20; + public DefinitionStatus? Status { get; set; } +} diff --git a/src/Workflow.Api/Endpoints/WorkflowDefinition/PublishWorkflowDefinitionEndpoint.cs b/src/Workflow.Api/Endpoints/WorkflowDefinition/PublishWorkflowDefinitionEndpoint.cs new file mode 100644 index 0000000..e2b1f14 --- /dev/null +++ b/src/Workflow.Api/Endpoints/WorkflowDefinition/PublishWorkflowDefinitionEndpoint.cs @@ -0,0 +1,34 @@ +using FastEndpoints; +using MediatR; +using Workflow.Application.Features.WorkflowDefinitions.Commands; + +namespace Workflow.Api.Endpoints.WorkflowDefinition; + +public class PublishWorkflowDefinitionEndpoint : Endpoint +{ + private readonly IMediator _mediator; + + public PublishWorkflowDefinitionEndpoint(IMediator mediator) => _mediator = mediator; + + public override void Configure() + { + Post("/workflow-definitions/{Id}/publish"); + AllowAnonymous(); + Summary(s => + { + s.Summary = "Publish a workflow definition"; + }); + } + + public override async Task HandleAsync(PublishWorkflowDefinitionRequest req, CancellationToken ct) + { + var command = new PublishWorkflowDefinitionCommand(req.Id); + await _mediator.Send(command, ct); + await SendOkAsync(ct); + } +} + +public class PublishWorkflowDefinitionRequest +{ + public Guid Id { get; set; } +} diff --git a/src/Workflow.Api/Endpoints/WorkflowDefinition/UpdateWorkflowDefinitionEndpoint.cs b/src/Workflow.Api/Endpoints/WorkflowDefinition/UpdateWorkflowDefinitionEndpoint.cs new file mode 100644 index 0000000..6527aa9 --- /dev/null +++ b/src/Workflow.Api/Endpoints/WorkflowDefinition/UpdateWorkflowDefinitionEndpoint.cs @@ -0,0 +1,38 @@ +using FastEndpoints; +using MediatR; +using Workflow.Application.Features.WorkflowDefinitions.Commands; +using Workflow.Application.Features.WorkflowDefinitions.DTOs; + +namespace Workflow.Api.Endpoints.WorkflowDefinition; + +public class UpdateWorkflowDefinitionEndpoint : Endpoint +{ + private readonly IMediator _mediator; + + public UpdateWorkflowDefinitionEndpoint(IMediator mediator) => _mediator = mediator; + + public override void Configure() + { + Put("/workflow-definitions/{Id}"); + AllowAnonymous(); + Summary(s => + { + s.Summary = "Update a workflow definition"; + }); + } + + public override async Task HandleAsync(UpdateWorkflowDefinitionRequest req, CancellationToken ct) + { + var command = new UpdateWorkflowDefinitionCommand(req.Id, req.Name, req.Description, req.DefinitionJson); + var result = await _mediator.Send(command, ct); + await SendAsync(result, 200, ct); + } +} + +public class UpdateWorkflowDefinitionRequest +{ + public Guid Id { get; set; } + public string Name { get; set; } = string.Empty; + public string? Description { get; set; } + public string? DefinitionJson { get; set; } +} diff --git a/src/Workflow.Api/Endpoints/WorkflowInstance/GetWorkflowInstanceByIdEndpoint.cs b/src/Workflow.Api/Endpoints/WorkflowInstance/GetWorkflowInstanceByIdEndpoint.cs new file mode 100644 index 0000000..f914706 --- /dev/null +++ b/src/Workflow.Api/Endpoints/WorkflowInstance/GetWorkflowInstanceByIdEndpoint.cs @@ -0,0 +1,35 @@ +using FastEndpoints; +using MediatR; +using Workflow.Application.Features.WorkflowInstances.DTOs; +using Workflow.Application.Features.WorkflowInstances.Queries; + +namespace Workflow.Api.Endpoints.WorkflowInstance; + +public class GetWorkflowInstanceByIdEndpoint : Endpoint +{ + private readonly IMediator _mediator; + + public GetWorkflowInstanceByIdEndpoint(IMediator mediator) => _mediator = mediator; + + public override void Configure() + { + Get("/workflow-instances/{Id}"); + AllowAnonymous(); + Summary(s => + { + s.Summary = "Get workflow instance detail by id (includes tokens and tasks)"; + }); + } + + public override async Task HandleAsync(GetWorkflowInstanceByIdRequest req, CancellationToken ct) + { + var query = new GetWorkflowInstanceByIdQuery(req.Id); + var result = await _mediator.Send(query, ct); + await SendAsync(result, 200, ct); + } +} + +public class GetWorkflowInstanceByIdRequest +{ + public Guid Id { get; set; } +} diff --git a/src/Workflow.Api/Endpoints/WorkflowInstance/GetWorkflowInstanceListEndpoint.cs b/src/Workflow.Api/Endpoints/WorkflowInstance/GetWorkflowInstanceListEndpoint.cs new file mode 100644 index 0000000..ec377f4 --- /dev/null +++ b/src/Workflow.Api/Endpoints/WorkflowInstance/GetWorkflowInstanceListEndpoint.cs @@ -0,0 +1,40 @@ +using FastEndpoints; +using MediatR; +using Workflow.Application.Common; +using Workflow.Application.Features.WorkflowInstances.DTOs; +using Workflow.Application.Features.WorkflowInstances.Queries; +using Workflow.Domain.Enums; + +namespace Workflow.Api.Endpoints.WorkflowInstance; + +public class GetWorkflowInstanceListEndpoint : Endpoint> +{ + private readonly IMediator _mediator; + + public GetWorkflowInstanceListEndpoint(IMediator mediator) => _mediator = mediator; + + public override void Configure() + { + Get("/workflow-instances"); + AllowAnonymous(); + Summary(s => + { + s.Summary = "Get paginated list of workflow instances"; + }); + } + + public override async Task HandleAsync(GetWorkflowInstanceListRequest req, CancellationToken ct) + { + var query = new GetWorkflowInstanceListQuery(req.PageIndex, req.PageSize, req.Status, req.InitiatorId); + var result = await _mediator.Send(query, ct); + await SendAsync(result, 200, ct); + } +} + +public class GetWorkflowInstanceListRequest +{ + public int PageIndex { get; set; } = 1; + public int PageSize { get; set; } = 20; + public InstanceStatus? Status { get; set; } + public Guid? InitiatorId { get; set; } +} diff --git a/src/Workflow.Api/Endpoints/WorkflowInstance/MonitorWorkflowInstancesEndpoint.cs b/src/Workflow.Api/Endpoints/WorkflowInstance/MonitorWorkflowInstancesEndpoint.cs new file mode 100644 index 0000000..ee24d42 --- /dev/null +++ b/src/Workflow.Api/Endpoints/WorkflowInstance/MonitorWorkflowInstancesEndpoint.cs @@ -0,0 +1,30 @@ +using FastEndpoints; +using MediatR; +using Workflow.Application.Features.WorkflowInstances.DTOs; +using Workflow.Application.Features.WorkflowInstances.Queries; + +namespace Workflow.Api.Endpoints.WorkflowInstance; + +public class MonitorWorkflowInstancesEndpoint : EndpointWithoutRequest +{ + private readonly IMediator _mediator; + + public MonitorWorkflowInstancesEndpoint(IMediator mediator) => _mediator = mediator; + + public override void Configure() + { + Get("/workflow-instances/monitor"); + AllowAnonymous(); + Summary(s => + { + s.Summary = "Get workflow monitoring statistics"; + }); + } + + public override async Task HandleAsync(CancellationToken ct) + { + var query = new MonitorWorkflowInstancesQuery(); + var result = await _mediator.Send(query, ct); + await SendAsync(result, 200, ct); + } +} diff --git a/src/Workflow.Api/Endpoints/WorkflowInstance/ResumeWorkflowInstanceEndpoint.cs b/src/Workflow.Api/Endpoints/WorkflowInstance/ResumeWorkflowInstanceEndpoint.cs new file mode 100644 index 0000000..aae69d6 --- /dev/null +++ b/src/Workflow.Api/Endpoints/WorkflowInstance/ResumeWorkflowInstanceEndpoint.cs @@ -0,0 +1,34 @@ +using FastEndpoints; +using MediatR; +using Workflow.Application.Features.WorkflowInstances.Commands; + +namespace Workflow.Api.Endpoints.WorkflowInstance; + +public class ResumeWorkflowInstanceEndpoint : Endpoint +{ + private readonly IMediator _mediator; + + public ResumeWorkflowInstanceEndpoint(IMediator mediator) => _mediator = mediator; + + public override void Configure() + { + Post("/workflow-instances/{Id}/resume"); + AllowAnonymous(); + Summary(s => + { + s.Summary = "Resume a suspended workflow instance"; + }); + } + + public override async Task HandleAsync(ResumeWorkflowInstanceRequest req, CancellationToken ct) + { + var command = new ResumeWorkflowInstanceCommand(req.Id); + await _mediator.Send(command, ct); + await SendOkAsync(ct); + } +} + +public class ResumeWorkflowInstanceRequest +{ + public Guid Id { get; set; } +} diff --git a/src/Workflow.Api/Endpoints/WorkflowInstance/StartWorkflowInstanceEndpoint.cs b/src/Workflow.Api/Endpoints/WorkflowInstance/StartWorkflowInstanceEndpoint.cs new file mode 100644 index 0000000..693b290 --- /dev/null +++ b/src/Workflow.Api/Endpoints/WorkflowInstance/StartWorkflowInstanceEndpoint.cs @@ -0,0 +1,36 @@ +using FastEndpoints; +using MediatR; +using Workflow.Application.Features.WorkflowInstances.Commands; + +namespace Workflow.Api.Endpoints.WorkflowInstance; + +public class StartWorkflowInstanceEndpoint : Endpoint +{ + private readonly IMediator _mediator; + + public StartWorkflowInstanceEndpoint(IMediator mediator) => _mediator = mediator; + + public override void Configure() + { + Post("/workflow-instances"); + AllowAnonymous(); + Summary(s => + { + s.Summary = "Start a new workflow instance"; + }); + } + + public override async Task HandleAsync(StartWorkflowInstanceRequest req, CancellationToken ct) + { + var command = new StartWorkflowInstanceCommand(req.DefinitionCode, req.Title, req.Variables); + var result = await _mediator.Send(command, ct); + await SendAsync(result, 200, ct); + } +} + +public class StartWorkflowInstanceRequest +{ + public string DefinitionCode { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public string? Variables { get; set; } +} diff --git a/src/Workflow.Api/Endpoints/WorkflowInstance/SuspendWorkflowInstanceEndpoint.cs b/src/Workflow.Api/Endpoints/WorkflowInstance/SuspendWorkflowInstanceEndpoint.cs new file mode 100644 index 0000000..82bef7f --- /dev/null +++ b/src/Workflow.Api/Endpoints/WorkflowInstance/SuspendWorkflowInstanceEndpoint.cs @@ -0,0 +1,34 @@ +using FastEndpoints; +using MediatR; +using Workflow.Application.Features.WorkflowInstances.Commands; + +namespace Workflow.Api.Endpoints.WorkflowInstance; + +public class SuspendWorkflowInstanceEndpoint : Endpoint +{ + private readonly IMediator _mediator; + + public SuspendWorkflowInstanceEndpoint(IMediator mediator) => _mediator = mediator; + + public override void Configure() + { + Post("/workflow-instances/{Id}/suspend"); + AllowAnonymous(); + Summary(s => + { + s.Summary = "Suspend a running workflow instance"; + }); + } + + public override async Task HandleAsync(SuspendWorkflowInstanceRequest req, CancellationToken ct) + { + var command = new SuspendWorkflowInstanceCommand(req.Id); + await _mediator.Send(command, ct); + await SendOkAsync(ct); + } +} + +public class SuspendWorkflowInstanceRequest +{ + public Guid Id { get; set; } +} diff --git a/src/Workflow.Api/Endpoints/WorkflowInstance/WithdrawWorkflowInstanceEndpoint.cs b/src/Workflow.Api/Endpoints/WorkflowInstance/WithdrawWorkflowInstanceEndpoint.cs new file mode 100644 index 0000000..7dbdbc0 --- /dev/null +++ b/src/Workflow.Api/Endpoints/WorkflowInstance/WithdrawWorkflowInstanceEndpoint.cs @@ -0,0 +1,35 @@ +using FastEndpoints; +using MediatR; +using Workflow.Application.Features.WorkflowInstances.Commands; + +namespace Workflow.Api.Endpoints.WorkflowInstance; + +public class WithdrawWorkflowInstanceEndpoint : Endpoint +{ + private readonly IMediator _mediator; + + public WithdrawWorkflowInstanceEndpoint(IMediator mediator) => _mediator = mediator; + + public override void Configure() + { + Post("/workflow-instances/{Id}/withdraw"); + AllowAnonymous(); + Summary(s => + { + s.Summary = "Withdraw a workflow instance (initiator only)"; + }); + } + + public override async Task HandleAsync(WithdrawWorkflowInstanceRequest req, CancellationToken ct) + { + var command = new WithdrawWorkflowInstanceCommand(req.Id, req.UserId); + await _mediator.Send(command, ct); + await SendOkAsync(ct); + } +} + +public class WithdrawWorkflowInstanceRequest +{ + public Guid Id { get; set; } + public Guid UserId { get; set; } +} diff --git a/src/Workflow.Api/Endpoints/WorkflowTask/ApproveTaskEndpoint.cs b/src/Workflow.Api/Endpoints/WorkflowTask/ApproveTaskEndpoint.cs new file mode 100644 index 0000000..7138d1e --- /dev/null +++ b/src/Workflow.Api/Endpoints/WorkflowTask/ApproveTaskEndpoint.cs @@ -0,0 +1,36 @@ +using FastEndpoints; +using MediatR; +using Workflow.Application.Features.WorkflowTasks.Commands; + +namespace Workflow.Api.Endpoints.WorkflowTask; + +public class ApproveTaskEndpoint : Endpoint +{ + private readonly IMediator _mediator; + + public ApproveTaskEndpoint(IMediator mediator) => _mediator = mediator; + + public override void Configure() + { + Post("/workflow-tasks/{Id}/approve"); + AllowAnonymous(); + Summary(s => + { + s.Summary = "Approve a pending task"; + }); + } + + public override async Task HandleAsync(ApproveTaskRequest req, CancellationToken ct) + { + var command = new ApproveTaskCommand(req.Id, req.UserId, req.Comment); + await _mediator.Send(command, ct); + await SendOkAsync(ct); + } +} + +public class ApproveTaskRequest +{ + public Guid Id { get; set; } + public Guid UserId { get; set; } + public string? Comment { get; set; } +} diff --git a/src/Workflow.Api/Endpoints/WorkflowTask/DelegateTaskEndpoint.cs b/src/Workflow.Api/Endpoints/WorkflowTask/DelegateTaskEndpoint.cs new file mode 100644 index 0000000..7312f83 --- /dev/null +++ b/src/Workflow.Api/Endpoints/WorkflowTask/DelegateTaskEndpoint.cs @@ -0,0 +1,36 @@ +using FastEndpoints; +using MediatR; +using Workflow.Application.Features.WorkflowTasks.Commands; + +namespace Workflow.Api.Endpoints.WorkflowTask; + +public class DelegateTaskEndpoint : Endpoint +{ + private readonly IMediator _mediator; + + public DelegateTaskEndpoint(IMediator mediator) => _mediator = mediator; + + public override void Configure() + { + Post("/workflow-tasks/{Id}/delegate"); + AllowAnonymous(); + Summary(s => + { + s.Summary = "Delegate a task to another user"; + }); + } + + public override async Task HandleAsync(DelegateTaskRequest req, CancellationToken ct) + { + var command = new DelegateTaskCommand(req.Id, req.FromUserId, req.ToUserId); + await _mediator.Send(command, ct); + await SendOkAsync(ct); + } +} + +public class DelegateTaskRequest +{ + public Guid Id { get; set; } + public Guid FromUserId { get; set; } + public Guid ToUserId { get; set; } +} diff --git a/src/Workflow.Api/Endpoints/WorkflowTask/GetCcTasksEndpoint.cs b/src/Workflow.Api/Endpoints/WorkflowTask/GetCcTasksEndpoint.cs new file mode 100644 index 0000000..ff9c6a9 --- /dev/null +++ b/src/Workflow.Api/Endpoints/WorkflowTask/GetCcTasksEndpoint.cs @@ -0,0 +1,38 @@ +using FastEndpoints; +using MediatR; +using Workflow.Application.Common; +using Workflow.Application.Features.WorkflowTasks.DTOs; +using Workflow.Application.Features.WorkflowTasks.Queries; + +namespace Workflow.Api.Endpoints.WorkflowTask; + +public class GetCcTasksEndpoint : Endpoint> +{ + private readonly IMediator _mediator; + + public GetCcTasksEndpoint(IMediator mediator) => _mediator = mediator; + + public override void Configure() + { + Get("/workflow-tasks/cc"); + AllowAnonymous(); + Summary(s => + { + s.Summary = "Get CC (carbon copy) tasks for a user"; + }); + } + + public override async Task HandleAsync(GetCcTasksRequest req, CancellationToken ct) + { + var query = new GetCcTasksQuery(req.UserId, req.PageIndex, req.PageSize); + var result = await _mediator.Send(query, ct); + await SendAsync(result, 200, ct); + } +} + +public class GetCcTasksRequest +{ + public Guid UserId { get; set; } + public int PageIndex { get; set; } = 1; + public int PageSize { get; set; } = 20; +} diff --git a/src/Workflow.Api/Endpoints/WorkflowTask/GetHistoryTasksEndpoint.cs b/src/Workflow.Api/Endpoints/WorkflowTask/GetHistoryTasksEndpoint.cs new file mode 100644 index 0000000..abd9ac8 --- /dev/null +++ b/src/Workflow.Api/Endpoints/WorkflowTask/GetHistoryTasksEndpoint.cs @@ -0,0 +1,38 @@ +using FastEndpoints; +using MediatR; +using Workflow.Application.Common; +using Workflow.Application.Features.WorkflowTasks.DTOs; +using Workflow.Application.Features.WorkflowTasks.Queries; + +namespace Workflow.Api.Endpoints.WorkflowTask; + +public class GetHistoryTasksEndpoint : Endpoint> +{ + private readonly IMediator _mediator; + + public GetHistoryTasksEndpoint(IMediator mediator) => _mediator = mediator; + + public override void Configure() + { + Get("/workflow-tasks/history"); + AllowAnonymous(); + Summary(s => + { + s.Summary = "Get completed (approved/rejected) tasks for a user"; + }); + } + + public override async Task HandleAsync(GetHistoryTasksRequest req, CancellationToken ct) + { + var query = new GetHistoryTasksQuery(req.UserId, req.PageIndex, req.PageSize); + var result = await _mediator.Send(query, ct); + await SendAsync(result, 200, ct); + } +} + +public class GetHistoryTasksRequest +{ + public Guid UserId { get; set; } + public int PageIndex { get; set; } = 1; + public int PageSize { get; set; } = 20; +} diff --git a/src/Workflow.Api/Endpoints/WorkflowTask/GetOverdueTasksEndpoint.cs b/src/Workflow.Api/Endpoints/WorkflowTask/GetOverdueTasksEndpoint.cs new file mode 100644 index 0000000..25bc3a7 --- /dev/null +++ b/src/Workflow.Api/Endpoints/WorkflowTask/GetOverdueTasksEndpoint.cs @@ -0,0 +1,38 @@ +using FastEndpoints; +using MediatR; +using Workflow.Application.Common; +using Workflow.Application.Features.WorkflowTasks.DTOs; +using Workflow.Application.Features.WorkflowTasks.Queries; + +namespace Workflow.Api.Endpoints.WorkflowTask; + +public class GetOverdueTasksEndpoint : Endpoint> +{ + private readonly IMediator _mediator; + + public GetOverdueTasksEndpoint(IMediator mediator) => _mediator = mediator; + + public override void Configure() + { + Get("/workflow-tasks/overdue"); + AllowAnonymous(); + Summary(s => + { + s.Summary = "Get overdue tasks (past due date, still pending)"; + }); + } + + public override async Task HandleAsync(GetOverdueTasksRequest req, CancellationToken ct) + { + var query = new GetOverdueTasksQuery(req.UserId, req.PageIndex, req.PageSize); + var result = await _mediator.Send(query, ct); + await SendAsync(result, 200, ct); + } +} + +public class GetOverdueTasksRequest +{ + public Guid? UserId { get; set; } + public int PageIndex { get; set; } = 1; + public int PageSize { get; set; } = 20; +} diff --git a/src/Workflow.Api/Endpoints/WorkflowTask/GetPendingTasksEndpoint.cs b/src/Workflow.Api/Endpoints/WorkflowTask/GetPendingTasksEndpoint.cs new file mode 100644 index 0000000..04ff64c --- /dev/null +++ b/src/Workflow.Api/Endpoints/WorkflowTask/GetPendingTasksEndpoint.cs @@ -0,0 +1,38 @@ +using FastEndpoints; +using MediatR; +using Workflow.Application.Common; +using Workflow.Application.Features.WorkflowTasks.DTOs; +using Workflow.Application.Features.WorkflowTasks.Queries; + +namespace Workflow.Api.Endpoints.WorkflowTask; + +public class GetPendingTasksEndpoint : Endpoint> +{ + private readonly IMediator _mediator; + + public GetPendingTasksEndpoint(IMediator mediator) => _mediator = mediator; + + public override void Configure() + { + Get("/workflow-tasks/pending"); + AllowAnonymous(); + Summary(s => + { + s.Summary = "Get pending tasks for a user"; + }); + } + + public override async Task HandleAsync(GetPendingTasksRequest req, CancellationToken ct) + { + var query = new GetPendingTasksQuery(req.UserId, req.PageIndex, req.PageSize); + var result = await _mediator.Send(query, ct); + await SendAsync(result, 200, ct); + } +} + +public class GetPendingTasksRequest +{ + public Guid UserId { get; set; } + public int PageIndex { get; set; } = 1; + public int PageSize { get; set; } = 20; +} diff --git a/src/Workflow.Api/Endpoints/WorkflowTask/GetTaskByIdEndpoint.cs b/src/Workflow.Api/Endpoints/WorkflowTask/GetTaskByIdEndpoint.cs new file mode 100644 index 0000000..d377737 --- /dev/null +++ b/src/Workflow.Api/Endpoints/WorkflowTask/GetTaskByIdEndpoint.cs @@ -0,0 +1,35 @@ +using FastEndpoints; +using MediatR; +using Workflow.Application.Features.WorkflowTasks.DTOs; +using Workflow.Application.Features.WorkflowTasks.Queries; + +namespace Workflow.Api.Endpoints.WorkflowTask; + +public class GetTaskByIdEndpoint : Endpoint +{ + private readonly IMediator _mediator; + + public GetTaskByIdEndpoint(IMediator mediator) => _mediator = mediator; + + public override void Configure() + { + Get("/workflow-tasks/{Id}"); + AllowAnonymous(); + Summary(s => + { + s.Summary = "Get task detail by id"; + }); + } + + public override async Task HandleAsync(GetTaskByIdRequest req, CancellationToken ct) + { + var query = new GetTaskByIdQuery(req.Id); + var result = await _mediator.Send(query, ct); + await SendAsync(result, 200, ct); + } +} + +public class GetTaskByIdRequest +{ + public Guid Id { get; set; } +} diff --git a/src/Workflow.Api/Endpoints/WorkflowTask/RejectTaskEndpoint.cs b/src/Workflow.Api/Endpoints/WorkflowTask/RejectTaskEndpoint.cs new file mode 100644 index 0000000..d76da03 --- /dev/null +++ b/src/Workflow.Api/Endpoints/WorkflowTask/RejectTaskEndpoint.cs @@ -0,0 +1,36 @@ +using FastEndpoints; +using MediatR; +using Workflow.Application.Features.WorkflowTasks.Commands; + +namespace Workflow.Api.Endpoints.WorkflowTask; + +public class RejectTaskEndpoint : Endpoint +{ + private readonly IMediator _mediator; + + public RejectTaskEndpoint(IMediator mediator) => _mediator = mediator; + + public override void Configure() + { + Post("/workflow-tasks/{Id}/reject"); + AllowAnonymous(); + Summary(s => + { + s.Summary = "Reject a pending task"; + }); + } + + public override async Task HandleAsync(RejectTaskRequest req, CancellationToken ct) + { + var command = new RejectTaskCommand(req.Id, req.UserId, req.Comment); + await _mediator.Send(command, ct); + await SendOkAsync(ct); + } +} + +public class RejectTaskRequest +{ + public Guid Id { get; set; } + public Guid UserId { get; set; } + public string? Comment { get; set; } +} diff --git a/src/Workflow.Api/Endpoints/WorkflowTask/TransferTaskEndpoint.cs b/src/Workflow.Api/Endpoints/WorkflowTask/TransferTaskEndpoint.cs new file mode 100644 index 0000000..d1a0a85 --- /dev/null +++ b/src/Workflow.Api/Endpoints/WorkflowTask/TransferTaskEndpoint.cs @@ -0,0 +1,36 @@ +using FastEndpoints; +using MediatR; +using Workflow.Application.Features.WorkflowTasks.Commands; + +namespace Workflow.Api.Endpoints.WorkflowTask; + +public class TransferTaskEndpoint : Endpoint +{ + private readonly IMediator _mediator; + + public TransferTaskEndpoint(IMediator mediator) => _mediator = mediator; + + public override void Configure() + { + Post("/workflow-tasks/{Id}/transfer"); + AllowAnonymous(); + Summary(s => + { + s.Summary = "Transfer a task to another user"; + }); + } + + public override async Task HandleAsync(TransferTaskRequest req, CancellationToken ct) + { + var command = new TransferTaskCommand(req.Id, req.FromUserId, req.ToUserId); + await _mediator.Send(command, ct); + await SendOkAsync(ct); + } +} + +public class TransferTaskRequest +{ + public Guid Id { get; set; } + public Guid FromUserId { get; set; } + public Guid ToUserId { get; set; } +} diff --git a/src/Workflow.Api/Endpoints/WorkflowTask/UrgeTaskEndpoint.cs b/src/Workflow.Api/Endpoints/WorkflowTask/UrgeTaskEndpoint.cs new file mode 100644 index 0000000..d8400f7 --- /dev/null +++ b/src/Workflow.Api/Endpoints/WorkflowTask/UrgeTaskEndpoint.cs @@ -0,0 +1,35 @@ +using FastEndpoints; +using MediatR; +using Workflow.Application.Features.WorkflowTasks.Commands; + +namespace Workflow.Api.Endpoints.WorkflowTask; + +public class UrgeTaskEndpoint : Endpoint +{ + private readonly IMediator _mediator; + + public UrgeTaskEndpoint(IMediator mediator) => _mediator = mediator; + + public override void Configure() + { + Post("/workflow-tasks/{Id}/urge"); + AllowAnonymous(); + Summary(s => + { + s.Summary = "Urge a pending task (send notification to assignee)"; + }); + } + + public override async Task HandleAsync(UrgeTaskRequest req, CancellationToken ct) + { + var command = new UrgeTaskCommand(req.Id, req.UserId); + await _mediator.Send(command, ct); + await SendOkAsync(ct); + } +} + +public class UrgeTaskRequest +{ + public Guid Id { get; set; } + public Guid UserId { get; set; } +} diff --git a/src/Workflow.Api/Middleware/ApiResponseMiddleware.cs b/src/Workflow.Api/Middleware/ApiResponseMiddleware.cs new file mode 100644 index 0000000..b163213 --- /dev/null +++ b/src/Workflow.Api/Middleware/ApiResponseMiddleware.cs @@ -0,0 +1,71 @@ +using System.Text.Json; + +namespace Workflow.Api.Middleware; + +public class ApiResponseMiddleware +{ + private readonly RequestDelegate _next; + + public ApiResponseMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext context) + { + var originalBodyStream = context.Response.Body; + + using var responseBody = new MemoryStream(); + context.Response.Body = responseBody; + + await _next(context); + + context.Response.Body = originalBodyStream; + + responseBody.Seek(0, SeekOrigin.Begin); + var responseText = await new StreamReader(responseBody).ReadToEndAsync(); + + // Skip wrapping for non-JSON responses, error responses, or Swagger + if (context.Response.StatusCode >= 400 || + context.Response.ContentType?.Contains("application/json") != true || + context.Request.Path.StartsWithSegments("/swagger")) + { + responseBody.Seek(0, SeekOrigin.Begin); + await responseBody.CopyToAsync(originalBodyStream); + return; + } + + // Parse the original response and wrap it in the standard envelope + object? data; + if (string.IsNullOrWhiteSpace(responseText)) + { + data = null; + } + else + { + try + { + data = JsonSerializer.Deserialize(responseText); + } + catch + { + data = responseText; + } + } + + var wrappedResponse = new + { + code = 0, + data, + message = "ok" + }; + + var json = JsonSerializer.Serialize(wrappedResponse, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + + context.Response.ContentLength = System.Text.Encoding.UTF8.GetByteCount(json); + await context.Response.WriteAsync(json); + } +} diff --git a/src/Workflow.Api/Middleware/GlobalExceptionMiddleware.cs b/src/Workflow.Api/Middleware/GlobalExceptionMiddleware.cs new file mode 100644 index 0000000..7f47e5d --- /dev/null +++ b/src/Workflow.Api/Middleware/GlobalExceptionMiddleware.cs @@ -0,0 +1,67 @@ +using System.Net; +using System.Text.Json; +using Workflow.Domain.Exceptions; + +namespace Workflow.Api.Middleware; + +public class GlobalExceptionMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public GlobalExceptionMiddleware(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + public async Task InvokeAsync(HttpContext context) + { + try + { + await _next(context); + } + catch (Exception ex) + { + await HandleExceptionAsync(context, ex); + } + } + + private async Task HandleExceptionAsync(HttpContext context, Exception exception) + { + var (statusCode, message) = exception switch + { + BusinessException ex => ((int)HttpStatusCode.BadRequest, ex.Message), + NotFoundException ex => ((int)HttpStatusCode.NotFound, ex.Message), + UnauthorizedException ex => ((int)HttpStatusCode.Unauthorized, ex.Message), + InvalidStateTransitionException ex => ((int)HttpStatusCode.BadRequest, ex.Message), + _ => ((int)HttpStatusCode.InternalServerError, "An unexpected error occurred.") + }; + + if (statusCode == (int)HttpStatusCode.InternalServerError) + { + _logger.LogError(exception, "Unhandled exception: {Message}", exception.Message); + } + else + { + _logger.LogWarning(exception, "Business exception: {Message}", exception.Message); + } + + context.Response.StatusCode = statusCode; + context.Response.ContentType = "application/json"; + + var response = new + { + code = statusCode, + message, + data = (object?)null + }; + + var json = JsonSerializer.Serialize(response, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + + await context.Response.WriteAsync(json); + } +} diff --git a/src/Workflow.Api/Program.cs b/src/Workflow.Api/Program.cs new file mode 100644 index 0000000..52ccdcf --- /dev/null +++ b/src/Workflow.Api/Program.cs @@ -0,0 +1,59 @@ +using System.Text.Json; +using FastEndpoints; +using FastEndpoints.Swagger; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Workflow.Api.Configuration; +using Workflow.Api.Middleware; +using Workflow.Application.Form.FormDefinition.Commands; +using Workflow.Infrastructure.Persistence; + +var builder = WebApplication.CreateBuilder(args); + +// DbContext +builder.Services.AddDbContext(options => +{ + options.UseNpgsql(builder.Configuration.GetConnectionString("Default"), npgsql => + { + npgsql.MigrationsAssembly(typeof(WorkflowDbContext).Assembly.FullName); + }); +}); + +// MediatR +builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(CreateFormDefinitionCommand).Assembly)); + +// FastEndpoints +builder.Services.AddFastEndpoints(); +builder.Services.SwaggerDocument(); + +// JWT Authentication +builder.Services.AddJwtAuthentication(builder.Configuration); + +// CORS +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy(policy => + { + policy.WithOrigins("http://localhost:5173", "http://localhost:5174", "http://localhost:3000", "http://localhost:5666") + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials(); + }); +}); + +var app = builder.Build(); + +// Middleware pipeline (order matters) +app.UseCors(); +app.UseMiddleware(); +app.UseMiddleware(); +app.UseAuthentication(); +app.UseAuthorization(); +app.UseFastEndpoints(config => +{ + config.Endpoints.RoutePrefix = "api"; + config.Serializer.Options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; +}); +app.UseSwaggerGen(); + +app.Run(); diff --git a/src/Workflow.Api/Workflow.Api.csproj.lscache b/src/Workflow.Api/Workflow.Api.csproj.lscache index 984fc8f..168c324 100644 --- a/src/Workflow.Api/Workflow.Api.csproj.lscache +++ b/src/Workflow.Api/Workflow.Api.csproj.lscache @@ -46,24 +46,208 @@ TemporaryDependencyNodeTargetIdentifier=net10.0 /optimize- /out:obj/Debug/net10.0/Workflow.Api.dll /refout:obj/Debug/net10.0/refint/Workflow.Api.dll -/target:library +/target:exe /warnaserror+ /utf8output /deterministic+ /langversion:14.0 +/features:use-roslyn-tokenizer=true /warnaserror+:NU1605,SYSLIB0011 [sourceFiles] +Configuration/JwtAuthConfiguration.cs +Endpoints/Form/ + CreateFormDefinitionEndpoint.cs + DeleteFormDefinitionEndpoint.cs + GetFormDataByInstanceEndpoint.cs + GetFormDefinitionByIdEndpoint.cs + GetFormDefinitionListEndpoint.cs + PublishFormDefinitionEndpoint.cs + SubmitFormDataEndpoint.cs + UpdateFormDefinitionEndpoint.cs +Endpoints/WorkflowDefinition/ + CreateWorkflowDefinitionEndpoint.cs + DeleteWorkflowDefinitionEndpoint.cs + DisableWorkflowDefinitionEndpoint.cs + GetWorkflowDefinitionByIdEndpoint.cs + GetWorkflowDefinitionListEndpoint.cs + PublishWorkflowDefinitionEndpoint.cs + UpdateWorkflowDefinitionEndpoint.cs +Endpoints/WorkflowInstance/ + GetWorkflowInstanceByIdEndpoint.cs + GetWorkflowInstanceListEndpoint.cs + MonitorWorkflowInstancesEndpoint.cs + ResumeWorkflowInstanceEndpoint.cs + StartWorkflowInstanceEndpoint.cs + SuspendWorkflowInstanceEndpoint.cs + WithdrawWorkflowInstanceEndpoint.cs +Endpoints/WorkflowTask/ + ApproveTaskEndpoint.cs + DelegateTaskEndpoint.cs + GetCcTasksEndpoint.cs + GetHistoryTasksEndpoint.cs + GetOverdueTasksEndpoint.cs + GetPendingTasksEndpoint.cs + GetTaskByIdEndpoint.cs + RejectTaskEndpoint.cs + TransferTaskEndpoint.cs + UrgeTaskEndpoint.cs +Middleware/ + ApiResponseMiddleware.cs + GlobalExceptionMiddleware.cs obj/Debug/net10.0/ .NETCoreApp,Version=v10.0.AssemblyAttributes.cs + SwaggerExportPathInitializer.g.cs Workflow.Api.AssemblyInfo.cs Workflow.Api.GlobalUsings.g.cs +Program.cs [metadataReferences] ../ Workflow.Application/obj/Debug/net10.0/ref/Workflow.Application.dll Workflow.Domain/obj/Debug/net10.0/ref/Workflow.Domain.dll Workflow.Infrastructure/obj/Debug/net10.0/ref/Workflow.Infrastructure.dll +/packs/Microsoft.AspNetCore.App.Ref/10.0.7/ref/net10.0/ + Microsoft.AspNetCore.Antiforgery.dll + Microsoft.AspNetCore.Authentication.Abstractions.dll + Microsoft.AspNetCore.Authentication.BearerToken.dll + Microsoft.AspNetCore.Authentication.Cookies.dll + Microsoft.AspNetCore.Authentication.Core.dll + Microsoft.AspNetCore.Authentication.dll + Microsoft.AspNetCore.Authentication.OAuth.dll + Microsoft.AspNetCore.Authorization.dll + Microsoft.AspNetCore.Authorization.Policy.dll + Microsoft.AspNetCore.Components.Authorization.dll + Microsoft.AspNetCore.Components.dll + Microsoft.AspNetCore.Components.Endpoints.dll + Microsoft.AspNetCore.Components.Forms.dll + Microsoft.AspNetCore.Components.Server.dll + Microsoft.AspNetCore.Components.Web.dll + Microsoft.AspNetCore.Connections.Abstractions.dll + Microsoft.AspNetCore.CookiePolicy.dll + Microsoft.AspNetCore.Cors.dll + Microsoft.AspNetCore.Cryptography.Internal.dll + Microsoft.AspNetCore.Cryptography.KeyDerivation.dll + Microsoft.AspNetCore.DataProtection.Abstractions.dll + Microsoft.AspNetCore.DataProtection.dll + Microsoft.AspNetCore.DataProtection.Extensions.dll + Microsoft.AspNetCore.Diagnostics.Abstractions.dll + Microsoft.AspNetCore.Diagnostics.dll + Microsoft.AspNetCore.Diagnostics.HealthChecks.dll + Microsoft.AspNetCore.dll + Microsoft.AspNetCore.HostFiltering.dll + Microsoft.AspNetCore.Hosting.Abstractions.dll + Microsoft.AspNetCore.Hosting.dll + Microsoft.AspNetCore.Hosting.Server.Abstractions.dll + Microsoft.AspNetCore.Html.Abstractions.dll + Microsoft.AspNetCore.Http.Abstractions.dll + Microsoft.AspNetCore.Http.Connections.Common.dll + Microsoft.AspNetCore.Http.Connections.dll + Microsoft.AspNetCore.Http.dll + Microsoft.AspNetCore.Http.Extensions.dll + Microsoft.AspNetCore.Http.Features.dll + Microsoft.AspNetCore.Http.Results.dll + Microsoft.AspNetCore.HttpLogging.dll + Microsoft.AspNetCore.HttpOverrides.dll + Microsoft.AspNetCore.HttpsPolicy.dll + Microsoft.AspNetCore.Identity.dll + Microsoft.AspNetCore.Localization.dll + Microsoft.AspNetCore.Localization.Routing.dll + Microsoft.AspNetCore.Metadata.dll + Microsoft.AspNetCore.Mvc.Abstractions.dll + Microsoft.AspNetCore.Mvc.ApiExplorer.dll + Microsoft.AspNetCore.Mvc.Core.dll + Microsoft.AspNetCore.Mvc.Cors.dll + Microsoft.AspNetCore.Mvc.DataAnnotations.dll + Microsoft.AspNetCore.Mvc.dll + Microsoft.AspNetCore.Mvc.Formatters.Json.dll + Microsoft.AspNetCore.Mvc.Formatters.Xml.dll + Microsoft.AspNetCore.Mvc.Localization.dll + Microsoft.AspNetCore.Mvc.Razor.dll + Microsoft.AspNetCore.Mvc.RazorPages.dll + Microsoft.AspNetCore.Mvc.TagHelpers.dll + Microsoft.AspNetCore.Mvc.ViewFeatures.dll + Microsoft.AspNetCore.OutputCaching.dll + Microsoft.AspNetCore.RateLimiting.dll + Microsoft.AspNetCore.Razor.dll + Microsoft.AspNetCore.Razor.Runtime.dll + Microsoft.AspNetCore.RequestDecompression.dll + Microsoft.AspNetCore.ResponseCaching.Abstractions.dll + Microsoft.AspNetCore.ResponseCaching.dll + Microsoft.AspNetCore.ResponseCompression.dll + Microsoft.AspNetCore.Rewrite.dll + Microsoft.AspNetCore.Routing.Abstractions.dll + Microsoft.AspNetCore.Routing.dll + Microsoft.AspNetCore.Server.HttpSys.dll + Microsoft.AspNetCore.Server.IIS.dll + Microsoft.AspNetCore.Server.IISIntegration.dll + Microsoft.AspNetCore.Server.Kestrel.Core.dll + Microsoft.AspNetCore.Server.Kestrel.dll + Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.dll + Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.dll + Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.dll + Microsoft.AspNetCore.Session.dll + Microsoft.AspNetCore.SignalR.Common.dll + Microsoft.AspNetCore.SignalR.Core.dll + Microsoft.AspNetCore.SignalR.dll + Microsoft.AspNetCore.SignalR.Protocols.Json.dll + Microsoft.AspNetCore.StaticAssets.dll + Microsoft.AspNetCore.StaticFiles.dll + Microsoft.AspNetCore.WebSockets.dll + Microsoft.AspNetCore.WebUtilities.dll + Microsoft.Extensions.Caching.Abstractions.dll + Microsoft.Extensions.Caching.Memory.dll + Microsoft.Extensions.Configuration.Abstractions.dll + Microsoft.Extensions.Configuration.Binder.dll + Microsoft.Extensions.Configuration.CommandLine.dll + Microsoft.Extensions.Configuration.dll + Microsoft.Extensions.Configuration.EnvironmentVariables.dll + Microsoft.Extensions.Configuration.FileExtensions.dll + Microsoft.Extensions.Configuration.Ini.dll + Microsoft.Extensions.Configuration.Json.dll + Microsoft.Extensions.Configuration.KeyPerFile.dll + Microsoft.Extensions.Configuration.UserSecrets.dll + Microsoft.Extensions.Configuration.Xml.dll + Microsoft.Extensions.DependencyInjection.Abstractions.dll + Microsoft.Extensions.DependencyInjection.dll + Microsoft.Extensions.Diagnostics.Abstractions.dll + Microsoft.Extensions.Diagnostics.dll + Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.dll + Microsoft.Extensions.Diagnostics.HealthChecks.dll + Microsoft.Extensions.Features.dll + Microsoft.Extensions.FileProviders.Abstractions.dll + Microsoft.Extensions.FileProviders.Composite.dll + Microsoft.Extensions.FileProviders.Embedded.dll + Microsoft.Extensions.FileProviders.Physical.dll + Microsoft.Extensions.FileSystemGlobbing.dll + Microsoft.Extensions.Hosting.Abstractions.dll + Microsoft.Extensions.Hosting.dll + Microsoft.Extensions.Http.dll + Microsoft.Extensions.Identity.Core.dll + Microsoft.Extensions.Identity.Stores.dll + Microsoft.Extensions.Localization.Abstractions.dll + Microsoft.Extensions.Localization.dll + Microsoft.Extensions.Logging.Abstractions.dll + Microsoft.Extensions.Logging.Configuration.dll + Microsoft.Extensions.Logging.Console.dll + Microsoft.Extensions.Logging.Debug.dll + Microsoft.Extensions.Logging.dll + Microsoft.Extensions.Logging.EventLog.dll + Microsoft.Extensions.Logging.EventSource.dll + Microsoft.Extensions.Logging.TraceSource.dll + Microsoft.Extensions.ObjectPool.dll + Microsoft.Extensions.Options.ConfigurationExtensions.dll + Microsoft.Extensions.Options.DataAnnotations.dll + Microsoft.Extensions.Options.dll + Microsoft.Extensions.Primitives.dll + Microsoft.Extensions.Validation.dll + Microsoft.Extensions.WebEncoders.dll + Microsoft.JSInterop.dll + Microsoft.Net.Http.Headers.dll + System.Diagnostics.EventLog.dll + System.Formats.Cbor.dll + System.Security.Cryptography.Xml.dll + System.Threading.RateLimiting.dll /packs/Microsoft.NETCore.App.Ref/10.0.7/ref/net10.0/ Microsoft.CSharp.dll Microsoft.VisualBasic.Core.dll @@ -232,8 +416,74 @@ obj/Debug/net10.0/ System.Xml.XPath.dll System.Xml.XPath.XDocument.dll WindowsBase.dll +/ + efcore.namingconventions/10.0.1/lib/net10.0/EFCore.NamingConventions.dll + fastendpoints.attributes/8.1.0/lib/net10.0/FastEndpoints.Attributes.dll + fastendpoints.core/8.1.0/lib/net10.0/FastEndpoints.Core.dll + fastendpoints.jobqueues/8.1.0/lib/net10.0/FastEndpoints.JobQueues.dll + fastendpoints.messaging.core/8.1.0/lib/net10.0/FastEndpoints.Messaging.Core.dll + fastendpoints.messaging/8.1.0/lib/net10.0/FastEndpoints.Messaging.dll + fastendpoints.security/8.1.0/lib/net10.0/FastEndpoints.Security.dll + fastendpoints.swagger/8.1.0/lib/net10.0/FastEndpoints.Swagger.dll + fastendpoints/8.1.0/lib/net10.0/FastEndpoints.dll + fluentvalidation/12.1.1/lib/net8.0/FluentValidation.dll + humanizer.core/2.14.1/lib/net6.0/Humanizer.dll + mediatr.contracts/2.0.1/lib/netstandard2.0/MediatR.Contracts.dll + mediatr/14.1.0/lib/net10.0/MediatR.dll + microsoft.aspnetcore.authentication.jwtbearer/10.0.5/lib/net10.0/Microsoft.AspNetCore.Authentication.JwtBearer.dll + microsoft.build.framework/18.0.2/ref/net10.0/Microsoft.Build.Framework.dll + microsoft.codeanalysis.common/5.0.0/lib/net9.0/Microsoft.CodeAnalysis.dll + microsoft.codeanalysis.csharp.workspaces/5.0.0/lib/net9.0/Microsoft.CodeAnalysis.CSharp.Workspaces.dll + microsoft.codeanalysis.csharp/5.0.0/lib/net9.0/Microsoft.CodeAnalysis.CSharp.dll + microsoft.codeanalysis.workspaces.common/5.0.0/lib/net9.0/Microsoft.CodeAnalysis.Workspaces.dll + microsoft.codeanalysis.workspaces.msbuild/5.0.0/lib/net9.0/ + Microsoft.CodeAnalysis.ExternalAccess.RazorCompiler.dll + Microsoft.CodeAnalysis.Workspaces.MSBuild.dll + microsoft.entityframeworkcore.abstractions/10.0.7/lib/net10.0/Microsoft.EntityFrameworkCore.Abstractions.dll + microsoft.entityframeworkcore.design/10.0.7/lib/net10.0/Microsoft.EntityFrameworkCore.Design.dll + microsoft.entityframeworkcore.relational/10.0.7/lib/net10.0/Microsoft.EntityFrameworkCore.Relational.dll + microsoft.entityframeworkcore/10.0.7/lib/net10.0/Microsoft.EntityFrameworkCore.dll + microsoft.extensions.dependencymodel/10.0.7/lib/net10.0/Microsoft.Extensions.DependencyModel.dll + microsoft.identitymodel.abstractions/8.14.0/lib/net9.0/Microsoft.IdentityModel.Abstractions.dll + microsoft.identitymodel.jsonwebtokens/8.14.0/lib/net9.0/Microsoft.IdentityModel.JsonWebTokens.dll + microsoft.identitymodel.logging/8.14.0/lib/net9.0/Microsoft.IdentityModel.Logging.dll + microsoft.identitymodel.protocols.openidconnect/8.0.1/lib/net9.0/Microsoft.IdentityModel.Protocols.OpenIdConnect.dll + microsoft.identitymodel.protocols/8.0.1/lib/net9.0/Microsoft.IdentityModel.Protocols.dll + microsoft.identitymodel.tokens/8.14.0/lib/net9.0/Microsoft.IdentityModel.Tokens.dll + microsoft.visualstudio.solutionpersistence/1.0.52/lib/net8.0/Microsoft.VisualStudio.SolutionPersistence.dll + mono.texttemplating/3.0.0/lib/net6.0/Mono.TextTemplating.dll + namotion.reflection/3.4.3/lib/net8.0/Namotion.Reflection.dll + newtonsoft.json/13.0.3/lib/net6.0/Newtonsoft.Json.dll + njsonschema.annotations/11.5.2/lib/netstandard2.0/NJsonSchema.Annotations.dll + njsonschema.newtonsoftjson/11.5.2/lib/net8.0/NJsonSchema.NewtonsoftJson.dll + njsonschema.yaml/11.5.2/lib/net8.0/NJsonSchema.Yaml.dll + njsonschema/11.5.2/lib/net8.0/NJsonSchema.dll + npgsql.entityframeworkcore.postgresql/10.0.1/lib/net10.0/Npgsql.EntityFrameworkCore.PostgreSQL.dll + npgsql/10.0.2/lib/net10.0/Npgsql.dll + nswag.annotations/14.6.3/lib/netstandard2.0/NSwag.Annotations.dll + nswag.aspnetcore/14.6.3/lib/net10.0/NSwag.AspNetCore.dll + nswag.core.yaml/14.6.3/lib/net8.0/NSwag.Core.Yaml.dll + nswag.core/14.6.3/lib/net8.0/NSwag.Core.dll + nswag.generation.aspnetcore/14.6.3/lib/net10.0/NSwag.Generation.AspNetCore.dll + nswag.generation/14.6.3/lib/net8.0/NSwag.Generation.dll + system.codedom/6.0.0/lib/net6.0/System.CodeDom.dll + system.composition.attributedmodel/9.0.0/lib/net9.0/System.Composition.AttributedModel.dll + system.composition.convention/9.0.0/lib/net9.0/System.Composition.Convention.dll + system.composition.hosting/9.0.0/lib/net9.0/System.Composition.Hosting.dll + system.composition.runtime/9.0.0/lib/net9.0/System.Composition.Runtime.dll + system.composition.typedparts/9.0.0/lib/net9.0/System.Composition.TypedParts.dll + system.identitymodel.tokens.jwt/8.0.1/lib/net9.0/System.IdentityModel.Tokens.Jwt.dll + yamldotnet/16.3.0/lib/net8.0/YamlDotNet.dll [analyzerReferences] +/packs/Microsoft.AspNetCore.App.Ref/10.0.7/analyzers/dotnet/cs/ + Microsoft.AspNetCore.App.Analyzers.dll + Microsoft.AspNetCore.App.CodeFixes.dll + Microsoft.AspNetCore.App.SourceGenerators.dll + Microsoft.AspNetCore.Components.Analyzers.dll + Microsoft.Extensions.Logging.Generators.dll + Microsoft.Extensions.Options.SourceGeneration.dll + Microsoft.Extensions.Validation.ValidationsGenerator.dll /packs/Microsoft.NETCore.App.Ref/10.0.7/analyzers/dotnet/cs/ Microsoft.Interop.ComInterfaceGenerator.dll Microsoft.Interop.JavaScript.JSImportGenerator.dll @@ -241,9 +491,20 @@ obj/Debug/net10.0/ Microsoft.Interop.SourceGeneration.dll System.Text.Json.SourceGeneration.dll System.Text.RegularExpressions.Generator.dll +/sdk/10.0.203/Sdks/Microsoft.NET.Sdk.Razor/source-generators/ + Microsoft.AspNetCore.Razor.Utilities.Shared.dll + Microsoft.CodeAnalysis.Razor.Compiler.dll + Microsoft.Extensions.ObjectPool.dll +/sdk/10.0.203/Sdks/Microsoft.NET.Sdk.Web/analyzers/cs/ + Microsoft.AspNetCore.Analyzers.dll + Microsoft.AspNetCore.Mvc.Analyzers.dll /sdk/10.0.203/Sdks/Microsoft.NET.Sdk/analyzers/ Microsoft.CodeAnalysis.CSharp.NetAnalyzers.dll Microsoft.CodeAnalysis.NetAnalyzers.dll +/microsoft.codeanalysis.analyzers/3.11.0/analyzers/dotnet/cs/ + Microsoft.CodeAnalysis.Analyzers.dll + Microsoft.CodeAnalysis.CSharp.Analyzers.dll +/microsoft.entityframeworkcore.analyzers/10.0.7/analyzers/dotnet/cs/Microsoft.EntityFrameworkCore.Analyzers.dll [analyzerConfigFiles] /sdk/10.0.203/Sdks/Microsoft.NET.Sdk/analyzers/build/config/analysislevel_10_default.globalconfig diff --git a/src/Workflow.Api/appsettings.json b/src/Workflow.Api/appsettings.json new file mode 100644 index 0000000..69b2e87 --- /dev/null +++ b/src/Workflow.Api/appsettings.json @@ -0,0 +1,17 @@ +{ + "ConnectionStrings": { + "Default": "Host=localhost;Port=5432;Database=workflow;Username=postgres;Password=postgres" + }, + "Jwt": { + "Issuer": "workflow-api", + "Audience": "workflow-api", + "SigningKey": "workflow-engine-secret-key-at-least-32-characters-long" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/Workflow.Application/Features/WorkflowInstances/Queries/MonitorWorkflowInstancesQuery.cs b/src/Workflow.Application/Features/WorkflowInstances/Queries/MonitorWorkflowInstancesQuery.cs index 7c71291..8e54bad 100644 --- a/src/Workflow.Application/Features/WorkflowInstances/Queries/MonitorWorkflowInstancesQuery.cs +++ b/src/Workflow.Application/Features/WorkflowInstances/Queries/MonitorWorkflowInstancesQuery.cs @@ -1,8 +1,11 @@ using MediatR; using Microsoft.EntityFrameworkCore; using Workflow.Application.Features.WorkflowInstances.DTOs; +using Workflow.Domain.Enums; using Workflow.Infrastructure.Persistence; +using TaskStatus = Workflow.Domain.Enums.TaskStatus; + namespace Workflow.Application.Features.WorkflowInstances.Queries; public record MonitorWorkflowInstancesQuery : IRequest; diff --git a/src/Workflow.Application/Features/WorkflowTasks/Commands/UrgeTaskCommand.cs b/src/Workflow.Application/Features/WorkflowTasks/Commands/UrgeTaskCommand.cs index f820942..dbadb0c 100644 --- a/src/Workflow.Application/Features/WorkflowTasks/Commands/UrgeTaskCommand.cs +++ b/src/Workflow.Application/Features/WorkflowTasks/Commands/UrgeTaskCommand.cs @@ -3,6 +3,8 @@ using Workflow.Domain.Enums; using Workflow.Domain.Exceptions; using Workflow.Infrastructure.Persistence; +using TaskStatus = Workflow.Domain.Enums.TaskStatus; + namespace Workflow.Application.Features.WorkflowTasks.Commands; public record UrgeTaskCommand( diff --git a/src/Workflow.Application/Features/WorkflowTasks/Queries/GetOverdueTasksQuery.cs b/src/Workflow.Application/Features/WorkflowTasks/Queries/GetOverdueTasksQuery.cs index 78717e2..7e00625 100644 --- a/src/Workflow.Application/Features/WorkflowTasks/Queries/GetOverdueTasksQuery.cs +++ b/src/Workflow.Application/Features/WorkflowTasks/Queries/GetOverdueTasksQuery.cs @@ -5,6 +5,8 @@ using Workflow.Application.Features.WorkflowTasks.DTOs; using Workflow.Domain.Enums; using Workflow.Infrastructure.Persistence; +using TaskStatus = Workflow.Domain.Enums.TaskStatus; + namespace Workflow.Application.Features.WorkflowTasks.Queries; public record GetOverdueTasksQuery( diff --git a/tests/Workflow.Tests/Engine/ProcessEngineTests.cs b/tests/Workflow.Tests/Engine/ProcessEngineTests.cs index e521bb8..2e7ca02 100644 --- a/tests/Workflow.Tests/Engine/ProcessEngineTests.cs +++ b/tests/Workflow.Tests/Engine/ProcessEngineTests.cs @@ -8,6 +8,7 @@ using Workflow.Domain.Enums; using Workflow.Domain.Exceptions; using Workflow.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; +using Xunit; public class ProcessEngineTests { @@ -86,9 +87,9 @@ public class ProcessEngineTests .ToListAsync(); tasks.Should().HaveCount(1); - tasks[0].AssigneeId.Should().Be("user-001"); + tasks[0].AssigneeRole.Should().Be("user-001"); tasks[0].Type.Should().Be(TaskType.Approval); - tasks[0].Status.Should().Be(Enums.TaskStatus.Pending); + tasks[0].Status.Should().Be(TaskStatus.Pending); } [Fact] @@ -110,7 +111,7 @@ public class ProcessEngineTests tasks.Should().HaveCount(1); tasks[0].AssigneeRole.Should().Be("manager"); tasks[0].Type.Should().Be(TaskType.Approval); - tasks[0].Status.Should().Be(Enums.TaskStatus.Pending); + tasks[0].Status.Should().Be(TaskStatus.Pending); } [Fact] @@ -131,10 +132,10 @@ public class ProcessEngineTests Id = Guid.NewGuid(), InstanceId = instance.Id, TokenId = token.Id, - NodeId = approvalNode.Id.ToString(), - AssigneeId = "user-001", + NodeId = approvalNode.Id, + AssigneeRole = "user-001", Type = TaskType.Approval, - Status = Enums.TaskStatus.Pending, + Status = TaskStatus.Pending, }; _dbContext.WorkflowTasks.Add(task); await _dbContext.SaveChangesAsync(); @@ -167,10 +168,10 @@ public class ProcessEngineTests Id = Guid.NewGuid(), InstanceId = instance.Id, TokenId = token.Id, - NodeId = approvalNode.Id.ToString(), - AssigneeId = "user-001", + NodeId = approvalNode.Id, + AssigneeRole = "user-001", Type = TaskType.Approval, - Status = Enums.TaskStatus.Pending, + Status = TaskStatus.Pending, }; _dbContext.WorkflowTasks.Add(task); await _dbContext.SaveChangesAsync(); @@ -203,10 +204,10 @@ public class ProcessEngineTests Id = Guid.NewGuid(), InstanceId = instance.Id, TokenId = token.Id, - NodeId = approvalNode.Id.ToString(), - AssigneeId = "user-001", + NodeId = approvalNode.Id, + AssigneeRole = "user-001", Type = TaskType.Approval, - Status = Enums.TaskStatus.Pending, + Status = TaskStatus.Pending, }; _dbContext.WorkflowTasks.Add(task); await _dbContext.SaveChangesAsync(); @@ -533,7 +534,7 @@ public class ProcessEngineTests { var subDefId = Guid.NewGuid(); var subProcessNode = CreateNode(NodeType.SubProcess, "sub-1"); - subProcessNode.Config = $"{{ "definitionId": "{subDefId}" }}"; + subProcessNode.Config = $"{{ \"definitionId\": \"{subDefId}\" }}"; var definition = CreateDefinition(subProcessNode); var instance = CreateInstance(definition); diff --git a/tests/Workflow.Tests/Workflow.Tests.csproj.lscache b/tests/Workflow.Tests/Workflow.Tests.csproj.lscache index d5e88ae..226d8e0 100644 --- a/tests/Workflow.Tests/Workflow.Tests.csproj.lscache +++ b/tests/Workflow.Tests/Workflow.Tests.csproj.lscache @@ -57,6 +57,7 @@ TemporaryDependencyNodeTargetIdentifier=net10.0 /microsoft.net.test.sdk/18.5.1/build/net8.0/Microsoft.NET.Test.Sdk.Program.cs @folderNames=..,..,..,..,..,.nuget,packages,microsoft.net.test.sdk,18.5.1,build,net8.0 Condition/ConditionEvaluatorTests.cs +Engine/ProcessEngineTests.cs obj/Debug/net10.0/ .NETCoreApp,Version=v10.0.AssemblyAttributes.cs Workflow.Tests.AssemblyInfo.cs