Compare commits
2 Commits
f26a20a875
...
fc4ecbbacc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc4ecbbacc | ||
|
|
f49e0ea1e4 |
113
CLAUDE.md
Normal file
113
CLAUDE.md
Normal file
@ -0,0 +1,113 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
> **Cross-repo rules** — see `/Users/wen/project/rag/CLAUDE.md` for full workspace conventions. Key shared rules:
|
||||
> - File-scoped namespaces, primary constructor DI, `record` for DTOs/Commands/Queries
|
||||
> - Entity: `BaseEntity + IAuditable/ISoftDelete/IFullAudit` marker interfaces
|
||||
> - EF Config: snake_case table, `ValueGeneratedNever()`, `IEntityTypeConfiguration<T>`
|
||||
> - Endpoint: `FastEndpoint<TReq, TRes>` + `Permissions("resource:action")`
|
||||
> - Middleware: `Cors → GlobalException → ApiResponse → Auth → AuthZ → FastEndpoints`
|
||||
> - Response: `{ code: 0, data, message: "ok" }` (auto-wrapped)
|
||||
> - JWT shared key: `RagJwtSecretKey2026MustBeAtLeast32CharsLong!`
|
||||
> - Other repos: `rag-backend` (5211), `im-system` (5212), `work-flow`, `file-system` (8080 Go), `rag-frontend` (5666 Vue)
|
||||
|
||||
## Build & Run
|
||||
|
||||
```bash
|
||||
dotnet build
|
||||
cd src/Workflow.Api && dotnet run
|
||||
cd src/Workflow.Api && dotnet run -- --seed
|
||||
cd src/Workflow.Infrastructure && dotnet ef migrations add <Name> --startup-project ../Workflow.Api
|
||||
dotnet test # xUnit + FluentAssertions + Moq
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
.NET 10, four projects: `Api → Application → Infrastructure → Domain`. Same ABP+FastEndpoints+MediatR stack as rag-backend, but with workflow-specific engine patterns.
|
||||
|
||||
### Key Difference: Domain-Driven State Machines
|
||||
|
||||
The core domain logic lives in **stateless state machines**, not in entities. Entities are anemic data holders.
|
||||
|
||||
Three state machines in `Domain/StateMachine/`:
|
||||
- `InstanceStateMachine` — Running ↔ Suspended → Completed/Terminated
|
||||
- `TaskStateMachine` — Pending → Approved/Rejected/Transferred/Delegated
|
||||
- `TokenStateMachine` — Active → Consumed/Terminated
|
||||
|
||||
Pattern: tuple switch with context objects:
|
||||
```csharp
|
||||
return (currentState, operation) switch
|
||||
{
|
||||
(InstanceStatus.Running, InstanceOperation.Suspend) => InstanceStatus.Suspended,
|
||||
(TaskStatus.Pending, TaskOperation.Approve) => TaskStatus.Approved,
|
||||
_ => throw new InvalidStateTransitionException(...)
|
||||
};
|
||||
```
|
||||
|
||||
### ProcessEngine — Token Propagation
|
||||
|
||||
`Application/Engine/ProcessEngine.cs` — routes tokens through 7 node types:
|
||||
|
||||
| Node Type | Behavior |
|
||||
|-----------|----------|
|
||||
| Start | Consumes token, creates new token on first outgoing edge |
|
||||
| End | Consumes token, checks if all instance tokens consumed → Complete |
|
||||
| Approval | Creates `WorkflowTask`, token stays Active until human action |
|
||||
| Cc | Creates notification tasks, immediately propagates token (fire-and-forget) |
|
||||
| Condition | Evaluates edge conditions (first-match-wins), creates one new token |
|
||||
| Parallel | Fork: one token per outgoing edge. Join: waits for all incoming tokens |
|
||||
| SubProcess | Creates child instance, parent token waits |
|
||||
|
||||
### Condition Evaluation — Strategy Chain
|
||||
|
||||
```
|
||||
ConditionEvaluator
|
||||
→ IValueComparatorRegistry (chain-of-responsibility)
|
||||
→ NumericComparator (==, !=, >, <, >=, <=)
|
||||
→ DateTimeComparator (==, !=, >, <, >=, <=)
|
||||
→ BooleanComparator (==, !=)
|
||||
→ CollectionComparator (in)
|
||||
→ StringComparator (==, !=, contains, startsWith, endsWith, isEmpty)
|
||||
```
|
||||
|
||||
Two condition syntaxes: simple text (`"amount > 5000"`, regex-parsed) and JSON tree (`{"and": [...], "or": [...]}`).
|
||||
|
||||
### Node Action Hooks
|
||||
|
||||
```csharp
|
||||
public interface INodeAction { Task ExecuteAsync(NodeActionContext context); }
|
||||
```
|
||||
Config specifies `"onEnter": "send-notification"`. Engine resolves from DI. Hook failures are swallowed — never block token propagation.
|
||||
|
||||
## Testing
|
||||
|
||||
```
|
||||
tests/Workflow.Tests/
|
||||
Condition/ — 6 test files (one per comparator + evaluator + registry)
|
||||
Engine/ — ProcessEngineTests (~775 lines, #region-grouped)
|
||||
Form/ — Form definition + data tests with fixture
|
||||
Handlers/ — CQRS handler tests with InMemory DB
|
||||
StateMachine/ — One test file per state machine (pure unit tests)
|
||||
```
|
||||
|
||||
Stack: xUnit + FluentAssertions + Moq + EF Core InMemory.
|
||||
|
||||
Handler test pattern:
|
||||
```csharp
|
||||
await using var db = new WorkflowDbContext(new DbContextOptionsBuilder<WorkflowDbContext>()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString()).Options);
|
||||
var handler = new CreateXxxCommandHandler(db);
|
||||
var result = await handler.Handle(command, CancellationToken.None);
|
||||
result.Should().NotBeNull();
|
||||
```
|
||||
|
||||
## Code Patterns
|
||||
|
||||
Same conventions as rag-backend (see workspace CLAUDE.md), plus:
|
||||
|
||||
- Table prefix: `wf_` (e.g., `wf_workflow_definitions`, `wf_workflow_tasks`)
|
||||
- Request DTOs co-located in endpoint files (not separate DTO files)
|
||||
- Endpoints use `AllowAnonymous()` on all routes (auth via gRPC to rag-backend, not local)
|
||||
- Entity audit fields are explicit (no base audit class): `Guid CreatedBy`, `DateTime CreatedAt`
|
||||
- `AuditInterceptor` registered via `OnConfiguring` override, not DI `AddInterceptors`
|
||||
20
src/Workflow.Api/Configuration/ComparatorConfiguration.cs
Normal file
20
src/Workflow.Api/Configuration/ComparatorConfiguration.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using Workflow.Application.Expressions;
|
||||
using Workflow.Domain.Expressions;
|
||||
using Workflow.Domain.Expressions.Comparators;
|
||||
|
||||
namespace Workflow.Api.Configuration;
|
||||
|
||||
public static class ComparatorConfiguration
|
||||
{
|
||||
public static IServiceCollection AddValueComparators(this IServiceCollection services)
|
||||
{
|
||||
services.AddTransient<IValueComparator, NumericComparator>();
|
||||
services.AddTransient<IValueComparator, DateTimeComparator>();
|
||||
services.AddTransient<IValueComparator, BooleanComparator>();
|
||||
services.AddTransient<IValueComparator, CollectionComparator>();
|
||||
services.AddTransient<IValueComparator, StringComparator>();
|
||||
services.AddSingleton<IValueComparatorRegistry, ValueComparatorRegistry>();
|
||||
services.AddTransient<ConditionEvaluator>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
28
src/Workflow.Api/Configuration/JwtAuthConfiguration.cs
Normal file
28
src/Workflow.Api/Configuration/JwtAuthConfiguration.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using FastEndpoints.Security;
|
||||
|
||||
namespace Workflow.Api.Configuration;
|
||||
|
||||
public static class JwtAuthConfiguration
|
||||
{
|
||||
public static void AddJwtAuthentication(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
var signingKey = configuration.GetSection("Jwt")["SigningKey"]
|
||||
?? throw new InvalidOperationException("Jwt:SigningKey is not configured.");
|
||||
|
||||
var issuer = configuration.GetSection("Jwt")["Issuer"];
|
||||
var audience = configuration.GetSection("Jwt")["Audience"];
|
||||
|
||||
services.AddAuthenticationJwtBearer(
|
||||
s => s.SigningKey = signingKey,
|
||||
o =>
|
||||
{
|
||||
if (issuer is not null)
|
||||
o.TokenValidationParameters.ValidIssuer = issuer;
|
||||
if (audience is not null)
|
||||
o.TokenValidationParameters.ValidAudience = audience;
|
||||
o.TokenValidationParameters.NameClaimType = System.Security.Claims.ClaimTypes.NameIdentifier;
|
||||
});
|
||||
|
||||
services.AddAuthorization();
|
||||
}
|
||||
}
|
||||
@ -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<CreateFormDefinitionRequest, FormDefinitionDto>
|
||||
{
|
||||
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 Send.OkAsync(result, 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<FormFieldDto> Fields { get; set; } = new();
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
using FastEndpoints;
|
||||
using MediatR;
|
||||
using Workflow.Application.Form.FormDefinition.Commands;
|
||||
|
||||
namespace Workflow.Api.Endpoints.Form;
|
||||
|
||||
public class DeleteFormDefinitionEndpoint : Endpoint<DeleteFormDefinitionRequest>
|
||||
{
|
||||
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 Send.OkAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
public class DeleteFormDefinitionRequest
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
}
|
||||
@ -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<GetFormDataByInstanceRequest, FormDataDto?>
|
||||
{
|
||||
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 Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
|
||||
public class GetFormDataByInstanceRequest
|
||||
{
|
||||
public Guid InstanceId { get; set; }
|
||||
}
|
||||
@ -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<GetFormDefinitionByIdRequest, FormDefinitionDetailDto>
|
||||
{
|
||||
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 Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
|
||||
public class GetFormDefinitionByIdRequest
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
}
|
||||
@ -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<GetFormDefinitionListRequest, PagedResult<FormDefinitionDto>>
|
||||
{
|
||||
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 Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
|
||||
public class GetFormDefinitionListRequest
|
||||
{
|
||||
public int PageIndex { get; set; } = 1;
|
||||
public int PageSize { get; set; } = 20;
|
||||
public FormStatus? Status { get; set; }
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
using FastEndpoints;
|
||||
using MediatR;
|
||||
using Workflow.Application.Form.FormDefinition.Commands;
|
||||
|
||||
namespace Workflow.Api.Endpoints.Form;
|
||||
|
||||
public class PublishFormDefinitionEndpoint : Endpoint<PublishFormDefinitionRequest>
|
||||
{
|
||||
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 Send.OkAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
public class PublishFormDefinitionRequest
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
}
|
||||
36
src/Workflow.Api/Endpoints/Form/SubmitFormDataEndpoint.cs
Normal file
36
src/Workflow.Api/Endpoints/Form/SubmitFormDataEndpoint.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using FastEndpoints;
|
||||
using MediatR;
|
||||
using Workflow.Application.Form.FormData.Commands;
|
||||
|
||||
namespace Workflow.Api.Endpoints.Form;
|
||||
|
||||
public class SubmitFormDataEndpoint : Endpoint<SubmitFormDataRequest, Guid>
|
||||
{
|
||||
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 Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
|
||||
public class SubmitFormDataRequest
|
||||
{
|
||||
public Guid FormDefinitionId { get; set; }
|
||||
public Guid InstanceId { get; set; }
|
||||
public string DataJson { get; set; } = string.Empty;
|
||||
}
|
||||
@ -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<UpdateFormDefinitionRequest, FormDefinitionDto>
|
||||
{
|
||||
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 Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
|
||||
public class UpdateFormDefinitionRequest
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public List<FormFieldDto> Fields { get; set; } = new();
|
||||
}
|
||||
@ -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<CreateWorkflowDefinitionRequest, WorkflowDefinitionDto>
|
||||
{
|
||||
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 Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
|
||||
public class CreateWorkflowDefinitionRequest
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Code { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
using FastEndpoints;
|
||||
using MediatR;
|
||||
using Workflow.Application.Features.WorkflowDefinitions.Commands;
|
||||
|
||||
namespace Workflow.Api.Endpoints.WorkflowDefinition;
|
||||
|
||||
public class DeleteWorkflowDefinitionEndpoint : Endpoint<DeleteWorkflowDefinitionRequest>
|
||||
{
|
||||
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 Send.OkAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
public class DeleteWorkflowDefinitionRequest
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
using FastEndpoints;
|
||||
using MediatR;
|
||||
using Workflow.Application.Features.WorkflowDefinitions.Commands;
|
||||
|
||||
namespace Workflow.Api.Endpoints.WorkflowDefinition;
|
||||
|
||||
public class DisableWorkflowDefinitionEndpoint : Endpoint<DisableWorkflowDefinitionRequest>
|
||||
{
|
||||
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 Send.OkAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
public class DisableWorkflowDefinitionRequest
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
}
|
||||
@ -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<GetWorkflowDefinitionByIdRequest, WorkflowDefinitionDetailDto>
|
||||
{
|
||||
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 Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
|
||||
public class GetWorkflowDefinitionByIdRequest
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
}
|
||||
@ -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<GetWorkflowDefinitionListRequest, PagedResult<WorkflowDefinitionDto>>
|
||||
{
|
||||
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 Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
|
||||
public class GetWorkflowDefinitionListRequest
|
||||
{
|
||||
public int PageIndex { get; set; } = 1;
|
||||
public int PageSize { get; set; } = 20;
|
||||
public DefinitionStatus? Status { get; set; }
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
using FastEndpoints;
|
||||
using MediatR;
|
||||
using Workflow.Application.Features.WorkflowDefinitions.Commands;
|
||||
|
||||
namespace Workflow.Api.Endpoints.WorkflowDefinition;
|
||||
|
||||
public class PublishWorkflowDefinitionEndpoint : Endpoint<PublishWorkflowDefinitionRequest>
|
||||
{
|
||||
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 Send.OkAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
public class PublishWorkflowDefinitionRequest
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
}
|
||||
@ -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<UpdateWorkflowDefinitionRequest, WorkflowDefinitionDto>
|
||||
{
|
||||
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 Send.OkAsync(result, 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; }
|
||||
}
|
||||
@ -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<GetWorkflowInstanceByIdRequest, WorkflowInstanceDetailDto>
|
||||
{
|
||||
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 Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
|
||||
public class GetWorkflowInstanceByIdRequest
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
}
|
||||
@ -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<GetWorkflowInstanceListRequest, PagedResult<WorkflowInstanceDto>>
|
||||
{
|
||||
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 Send.OkAsync(result, 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; }
|
||||
}
|
||||
@ -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<WorkflowMonitorDto>
|
||||
{
|
||||
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 Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
using FastEndpoints;
|
||||
using MediatR;
|
||||
using Workflow.Application.Features.WorkflowInstances.Commands;
|
||||
|
||||
namespace Workflow.Api.Endpoints.WorkflowInstance;
|
||||
|
||||
public class ResumeWorkflowInstanceEndpoint : Endpoint<ResumeWorkflowInstanceRequest>
|
||||
{
|
||||
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 Send.OkAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
public class ResumeWorkflowInstanceRequest
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
using FastEndpoints;
|
||||
using MediatR;
|
||||
using Workflow.Application.Features.WorkflowInstances.Commands;
|
||||
|
||||
namespace Workflow.Api.Endpoints.WorkflowInstance;
|
||||
|
||||
public class StartWorkflowInstanceEndpoint : Endpoint<StartWorkflowInstanceRequest, Guid>
|
||||
{
|
||||
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 Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
|
||||
public class StartWorkflowInstanceRequest
|
||||
{
|
||||
public string DefinitionCode { get; set; } = string.Empty;
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string? Variables { get; set; }
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
using FastEndpoints;
|
||||
using MediatR;
|
||||
using Workflow.Application.Features.WorkflowInstances.Commands;
|
||||
|
||||
namespace Workflow.Api.Endpoints.WorkflowInstance;
|
||||
|
||||
public class SuspendWorkflowInstanceEndpoint : Endpoint<SuspendWorkflowInstanceRequest>
|
||||
{
|
||||
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 Send.OkAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
public class SuspendWorkflowInstanceRequest
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
using FastEndpoints;
|
||||
using MediatR;
|
||||
using Workflow.Application.Features.WorkflowInstances.Commands;
|
||||
|
||||
namespace Workflow.Api.Endpoints.WorkflowInstance;
|
||||
|
||||
public class WithdrawWorkflowInstanceEndpoint : Endpoint<WithdrawWorkflowInstanceRequest>
|
||||
{
|
||||
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 Send.OkAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
public class WithdrawWorkflowInstanceRequest
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
using FastEndpoints;
|
||||
using MediatR;
|
||||
using Workflow.Application.Features.WorkflowTasks.Commands;
|
||||
|
||||
namespace Workflow.Api.Endpoints.WorkflowTask;
|
||||
|
||||
public class ApproveTaskEndpoint : Endpoint<ApproveTaskRequest>
|
||||
{
|
||||
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 Send.OkAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
public class ApproveTaskRequest
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public string? Comment { get; set; }
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
using FastEndpoints;
|
||||
using MediatR;
|
||||
using Workflow.Application.Features.WorkflowTasks.Commands;
|
||||
|
||||
namespace Workflow.Api.Endpoints.WorkflowTask;
|
||||
|
||||
public class DelegateTaskEndpoint : Endpoint<DelegateTaskRequest>
|
||||
{
|
||||
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 Send.OkAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
public class DelegateTaskRequest
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid FromUserId { get; set; }
|
||||
public Guid ToUserId { get; set; }
|
||||
}
|
||||
@ -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<GetCcTasksRequest, PagedResult<WorkflowTaskListItemDto>>
|
||||
{
|
||||
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 Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
|
||||
public class GetCcTasksRequest
|
||||
{
|
||||
public Guid UserId { get; set; }
|
||||
public int PageIndex { get; set; } = 1;
|
||||
public int PageSize { get; set; } = 20;
|
||||
}
|
||||
@ -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<GetHistoryTasksRequest, PagedResult<WorkflowTaskListItemDto>>
|
||||
{
|
||||
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 Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
|
||||
public class GetHistoryTasksRequest
|
||||
{
|
||||
public Guid UserId { get; set; }
|
||||
public int PageIndex { get; set; } = 1;
|
||||
public int PageSize { get; set; } = 20;
|
||||
}
|
||||
@ -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<GetOverdueTasksRequest, PagedResult<WorkflowTaskListItemDto>>
|
||||
{
|
||||
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 Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
|
||||
public class GetOverdueTasksRequest
|
||||
{
|
||||
public Guid? UserId { get; set; }
|
||||
public int PageIndex { get; set; } = 1;
|
||||
public int PageSize { get; set; } = 20;
|
||||
}
|
||||
@ -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<GetPendingTasksRequest, PagedResult<WorkflowTaskListItemDto>>
|
||||
{
|
||||
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 Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
|
||||
public class GetPendingTasksRequest
|
||||
{
|
||||
public Guid UserId { get; set; }
|
||||
public int PageIndex { get; set; } = 1;
|
||||
public int PageSize { get; set; } = 20;
|
||||
}
|
||||
@ -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<GetTaskByIdRequest, WorkflowTaskListItemDto>
|
||||
{
|
||||
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 Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
|
||||
public class GetTaskByIdRequest
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
using FastEndpoints;
|
||||
using MediatR;
|
||||
using Workflow.Application.Features.WorkflowTasks.Commands;
|
||||
|
||||
namespace Workflow.Api.Endpoints.WorkflowTask;
|
||||
|
||||
public class RejectTaskEndpoint : Endpoint<RejectTaskRequest>
|
||||
{
|
||||
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 Send.OkAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
public class RejectTaskRequest
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public string? Comment { get; set; }
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
using FastEndpoints;
|
||||
using MediatR;
|
||||
using Workflow.Application.Features.WorkflowTasks.Commands;
|
||||
|
||||
namespace Workflow.Api.Endpoints.WorkflowTask;
|
||||
|
||||
public class TransferTaskEndpoint : Endpoint<TransferTaskRequest>
|
||||
{
|
||||
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 Send.OkAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
public class TransferTaskRequest
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid FromUserId { get; set; }
|
||||
public Guid ToUserId { get; set; }
|
||||
}
|
||||
35
src/Workflow.Api/Endpoints/WorkflowTask/UrgeTaskEndpoint.cs
Normal file
35
src/Workflow.Api/Endpoints/WorkflowTask/UrgeTaskEndpoint.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using FastEndpoints;
|
||||
using MediatR;
|
||||
using Workflow.Application.Features.WorkflowTasks.Commands;
|
||||
|
||||
namespace Workflow.Api.Endpoints.WorkflowTask;
|
||||
|
||||
public class UrgeTaskEndpoint : Endpoint<UrgeTaskRequest>
|
||||
{
|
||||
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 Send.OkAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
public class UrgeTaskRequest
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
}
|
||||
71
src/Workflow.Api/Middleware/ApiResponseMiddleware.cs
Normal file
71
src/Workflow.Api/Middleware/ApiResponseMiddleware.cs
Normal file
@ -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<JsonElement>(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);
|
||||
}
|
||||
}
|
||||
67
src/Workflow.Api/Middleware/GlobalExceptionMiddleware.cs
Normal file
67
src/Workflow.Api/Middleware/GlobalExceptionMiddleware.cs
Normal file
@ -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<GlobalExceptionMiddleware> _logger;
|
||||
|
||||
public GlobalExceptionMiddleware(RequestDelegate next, ILogger<GlobalExceptionMiddleware> 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);
|
||||
}
|
||||
}
|
||||
86
src/Workflow.Api/Program.cs
Normal file
86
src/Workflow.Api/Program.cs
Normal file
@ -0,0 +1,86 @@
|
||||
using System.Text.Json;
|
||||
using FastEndpoints;
|
||||
using FastEndpoints.Swagger;
|
||||
using Grpc.Net.Client;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Workflow.Api.Configuration;
|
||||
using Workflow.Api.Middleware;
|
||||
using Workflow.Api.Services;
|
||||
using Workflow.Application.Form.FormDefinition.Commands;
|
||||
using Workflow.Domain.Common;
|
||||
using Workflow.Domain.Expressions;
|
||||
using Workflow.Infrastructure.Persistence;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Current User Context
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddSingleton<ICurrentUserContext, CurrentUserContext>();
|
||||
|
||||
// DbContext
|
||||
builder.Services.AddDbContext<WorkflowDbContext>((sp, options) =>
|
||||
{
|
||||
options.UseNpgsql(builder.Configuration.GetConnectionString("Default"), npgsql =>
|
||||
{
|
||||
npgsql.MigrationsAssembly(typeof(WorkflowDbContext).Assembly.FullName);
|
||||
});
|
||||
options.UseSnakeCaseNamingConvention();
|
||||
});
|
||||
|
||||
// gRPC Auth Client
|
||||
var authServerUrl = builder.Configuration["Grpc:AuthServerUrl"] ?? "http://localhost:50051";
|
||||
builder.Services.AddSingleton(_ => GrpcChannel.ForAddress(authServerUrl));
|
||||
builder.Services.AddSingleton<IAuthGrpcClient, AuthGrpcClient>();
|
||||
|
||||
// MediatR
|
||||
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(CreateFormDefinitionCommand).Assembly));
|
||||
|
||||
// Value Comparators
|
||||
builder.Services.AddValueComparators();
|
||||
|
||||
// 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();
|
||||
|
||||
// Seed Data
|
||||
if (args.Contains("--seed"))
|
||||
{
|
||||
using var scope = app.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<WorkflowDbContext>();
|
||||
await db.Database.EnsureCreatedAsync();
|
||||
await SeedData.SeedAsync(db);
|
||||
return;
|
||||
}
|
||||
|
||||
// Middleware pipeline (order matters)
|
||||
app.UseCors();
|
||||
app.UseMiddleware<GlobalExceptionMiddleware>();
|
||||
app.UseMiddleware<ApiResponseMiddleware>();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.UseFastEndpoints(config =>
|
||||
{
|
||||
config.Endpoints.RoutePrefix = "api";
|
||||
config.Serializer.Options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
|
||||
});
|
||||
app.UseSwaggerGen();
|
||||
|
||||
app.Run();
|
||||
35
src/Workflow.Api/Protos/auth.proto
Normal file
35
src/Workflow.Api/Protos/auth.proto
Normal file
@ -0,0 +1,35 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package auth;
|
||||
|
||||
option csharp_namespace = "Workflow.Api.Grpc";
|
||||
|
||||
service AuthService {
|
||||
rpc ValidateToken (ValidateTokenRequest) returns (ValidateTokenResponse);
|
||||
rpc CheckPermission (CheckPermissionRequest) returns (CheckPermissionResponse);
|
||||
}
|
||||
|
||||
message ValidateTokenRequest {
|
||||
string token = 1;
|
||||
}
|
||||
|
||||
message ValidateTokenResponse {
|
||||
bool valid = 1;
|
||||
string user_id = 2;
|
||||
string username = 3;
|
||||
string email = 4;
|
||||
repeated string roles = 5;
|
||||
repeated string permissions = 6;
|
||||
int64 expires_at = 7;
|
||||
}
|
||||
|
||||
message CheckPermissionRequest {
|
||||
string token = 1;
|
||||
string permission = 2;
|
||||
}
|
||||
|
||||
message CheckPermissionResponse {
|
||||
bool allowed = 1;
|
||||
string user_id = 2;
|
||||
repeated string roles = 3;
|
||||
}
|
||||
47
src/Workflow.Api/Services/AuthGrpcClient.cs
Normal file
47
src/Workflow.Api/Services/AuthGrpcClient.cs
Normal file
@ -0,0 +1,47 @@
|
||||
using Grpc.Core;
|
||||
using Grpc.Net.Client;
|
||||
using Workflow.Api.Grpc;
|
||||
|
||||
namespace Workflow.Api.Services;
|
||||
|
||||
public interface IAuthGrpcClient
|
||||
{
|
||||
Task<(bool Valid, string UserId, List<string> Roles, List<string> Permissions)> ValidateTokenAsync(string token);
|
||||
Task<(bool Allowed, string UserId, List<string> Roles)> CheckPermissionAsync(string token, string permission);
|
||||
}
|
||||
|
||||
public class AuthGrpcClient : IAuthGrpcClient
|
||||
{
|
||||
private readonly AuthService.AuthServiceClient _client;
|
||||
|
||||
public AuthGrpcClient(GrpcChannel channel)
|
||||
{
|
||||
_client = new AuthService.AuthServiceClient(channel);
|
||||
}
|
||||
|
||||
public async Task<(bool Valid, string UserId, List<string> Roles, List<string> Permissions)> ValidateTokenAsync(string token)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _client.ValidateTokenAsync(new ValidateTokenRequest { Token = token });
|
||||
return (response.Valid, response.UserId, response.Roles.ToList(), response.Permissions.ToList());
|
||||
}
|
||||
catch (RpcException)
|
||||
{
|
||||
return (false, string.Empty, [], []);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(bool Allowed, string UserId, List<string> Roles)> CheckPermissionAsync(string token, string permission)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _client.CheckPermissionAsync(new CheckPermissionRequest { Token = token, Permission = permission });
|
||||
return (response.Allowed, response.UserId, response.Roles.ToList());
|
||||
}
|
||||
catch (RpcException)
|
||||
{
|
||||
return (false, string.Empty, []);
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/Workflow.Api/Services/CurrentUserContext.cs
Normal file
34
src/Workflow.Api/Services/CurrentUserContext.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System.Security.Claims;
|
||||
using Workflow.Domain.Common;
|
||||
|
||||
namespace Workflow.Api.Services;
|
||||
|
||||
public class CurrentUserContext : ICurrentUserContext
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
public CurrentUserContext(IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
public Guid GetUserId()
|
||||
{
|
||||
var httpContext = _httpContextAccessor.HttpContext;
|
||||
if (httpContext?.User?.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
var userIdClaim = httpContext.User.FindFirstValue(ClaimTypes.NameIdentifier)
|
||||
?? httpContext.User.FindFirstValue("user_id")
|
||||
?? "system";
|
||||
|
||||
return Guid.TryParse(userIdClaim, out var userId) ? userId : Guid.Empty;
|
||||
}
|
||||
return Guid.Empty;
|
||||
}
|
||||
|
||||
public string? GetIPAddress()
|
||||
{
|
||||
var httpContext = _httpContextAccessor.HttpContext;
|
||||
return httpContext?.Connection?.RemoteIpAddress?.ToString();
|
||||
}
|
||||
}
|
||||
@ -3,9 +3,15 @@
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="10.0.1" />
|
||||
<PackageReference Include="FastEndpoints" Version="8.1.0" />
|
||||
<PackageReference Include="FastEndpoints.Security" Version="8.1.0" />
|
||||
<PackageReference Include="FastEndpoints.Swagger" Version="8.1.0" />
|
||||
<PackageReference Include="Grpc.AspNetCore" Version="2.71.0" />
|
||||
<PackageReference Include="Grpc.Net.Client" Version="2.71.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Protobuf Include="Protos\auth.proto" GrpcServices="Client" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Workflow.Application\Workflow.Application.csproj" />
|
||||
|
||||
@ -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
|
||||
<DOTNET>/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
|
||||
<DOTNET>/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
|
||||
<NUGET>/
|
||||
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]
|
||||
<DOTNET>/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
|
||||
<DOTNET>/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
|
||||
<DOTNET>/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
|
||||
<DOTNET>/sdk/10.0.203/Sdks/Microsoft.NET.Sdk.Web/analyzers/cs/
|
||||
Microsoft.AspNetCore.Analyzers.dll
|
||||
Microsoft.AspNetCore.Mvc.Analyzers.dll
|
||||
<DOTNET>/sdk/10.0.203/Sdks/Microsoft.NET.Sdk/analyzers/
|
||||
Microsoft.CodeAnalysis.CSharp.NetAnalyzers.dll
|
||||
Microsoft.CodeAnalysis.NetAnalyzers.dll
|
||||
<NUGET>/microsoft.codeanalysis.analyzers/3.11.0/analyzers/dotnet/cs/
|
||||
Microsoft.CodeAnalysis.Analyzers.dll
|
||||
Microsoft.CodeAnalysis.CSharp.Analyzers.dll
|
||||
<NUGET>/microsoft.entityframeworkcore.analyzers/10.0.7/analyzers/dotnet/cs/Microsoft.EntityFrameworkCore.Analyzers.dll
|
||||
|
||||
[analyzerConfigFiles]
|
||||
<DOTNET>/sdk/10.0.203/Sdks/Microsoft.NET.Sdk/analyzers/build/config/analysislevel_10_default.globalconfig
|
||||
|
||||
21
src/Workflow.Api/appsettings.json
Normal file
21
src/Workflow.Api/appsettings.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"Urls": "http://*:5213",
|
||||
"ConnectionStrings": {
|
||||
"Default": "Host=localhost;Port=5432;Database=workflow;Username=rag;Password=rag123"
|
||||
},
|
||||
"Jwt": {
|
||||
"Issuer": "rag-api",
|
||||
"Audience": "rag-client",
|
||||
"SigningKey": "RagJwtSecretKey2026MustBeAtLeast32CharsLong!"
|
||||
},
|
||||
"Grpc": {
|
||||
"AuthServerUrl": "http://localhost:50051"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@ -18,12 +18,13 @@ public class ProcessEngine
|
||||
{
|
||||
private readonly WorkflowDbContext _dbContext;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ConditionEvaluator _conditionEvaluator = new();
|
||||
private readonly ConditionEvaluator _conditionEvaluator;
|
||||
|
||||
public ProcessEngine(WorkflowDbContext dbContext, IServiceProvider serviceProvider)
|
||||
public ProcessEngine(WorkflowDbContext dbContext, IServiceProvider serviceProvider, ConditionEvaluator conditionEvaluator)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
_serviceProvider = serviceProvider;
|
||||
_conditionEvaluator = conditionEvaluator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -0,0 +1,50 @@
|
||||
using Workflow.Domain.Expressions.Comparators;
|
||||
|
||||
namespace Workflow.Application.Expressions;
|
||||
|
||||
/// <summary>
|
||||
/// 值对比器注册中心。从 DI 收集所有 IValueComparator 实例,
|
||||
/// 按操作符索引,通过责任链按序分派比较请求。
|
||||
/// 注册顺序决定优先级:先注册的对比器先被尝试。
|
||||
/// </summary>
|
||||
public class ValueComparatorRegistry : IValueComparatorRegistry
|
||||
{
|
||||
private readonly Dictionary<string, List<IValueComparator>> _comparatorsByOperator;
|
||||
private readonly IReadOnlySet<string> _registeredOperators;
|
||||
|
||||
public ValueComparatorRegistry(IEnumerable<IValueComparator> comparators)
|
||||
{
|
||||
_comparatorsByOperator = new Dictionary<string, List<IValueComparator>>();
|
||||
|
||||
foreach (var comparator in comparators)
|
||||
{
|
||||
foreach (var op in comparator.SupportedOperators)
|
||||
{
|
||||
if (!_comparatorsByOperator.TryGetValue(op, out var list))
|
||||
{
|
||||
list = [];
|
||||
_comparatorsByOperator[op] = list;
|
||||
}
|
||||
list.Add(comparator);
|
||||
}
|
||||
}
|
||||
|
||||
_registeredOperators = _comparatorsByOperator.Keys.ToHashSet();
|
||||
}
|
||||
|
||||
public IReadOnlySet<string> RegisteredOperators => _registeredOperators;
|
||||
|
||||
public bool TryCompare(object? fieldValue, string operatorName, object? conditionValue)
|
||||
{
|
||||
if (!_comparatorsByOperator.TryGetValue(operatorName, out var comparators))
|
||||
return false;
|
||||
|
||||
foreach (var comparator in comparators)
|
||||
{
|
||||
if (comparator.Compare(fieldValue, operatorName, conditionValue))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -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<WorkflowMonitorDto>;
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Workflow.Domain.Expressions.Comparators;
|
||||
|
||||
/// <summary>
|
||||
/// 布尔对比器。处理 ==, != 操作符。
|
||||
/// 仅当两个值都能解析为 bool 时才进行比较,否则返回 false。
|
||||
/// </summary>
|
||||
public class BooleanComparator : IValueComparator
|
||||
{
|
||||
private static readonly IReadOnlySet<string> _supportedOperators = new HashSet<string>
|
||||
{
|
||||
"==", "!="
|
||||
};
|
||||
|
||||
public IReadOnlySet<string> SupportedOperators => _supportedOperators;
|
||||
|
||||
public bool Compare(object? fieldValue, string operatorName, object? conditionValue)
|
||||
{
|
||||
if (fieldValue is null || !_supportedOperators.Contains(operatorName))
|
||||
return false;
|
||||
|
||||
if (!ValueComparatorHelper.TryGetBoolean(fieldValue, out var fieldBool))
|
||||
return false;
|
||||
|
||||
if (!ValueComparatorHelper.TryGetBooleanFromCondition(conditionValue, out var condBool))
|
||||
return false;
|
||||
|
||||
return operatorName switch
|
||||
{
|
||||
"==" => fieldBool == condBool,
|
||||
"!=" => fieldBool != condBool,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Workflow.Domain.Expressions.Comparators;
|
||||
|
||||
/// <summary>
|
||||
/// 集合对比器。处理 in 操作符。
|
||||
/// 条件值可以是 JsonElement 数组或逗号分隔的字符串。
|
||||
/// </summary>
|
||||
public class CollectionComparator : IValueComparator
|
||||
{
|
||||
private static readonly IReadOnlySet<string> _supportedOperators = new HashSet<string>
|
||||
{
|
||||
"in"
|
||||
};
|
||||
|
||||
public IReadOnlySet<string> SupportedOperators => _supportedOperators;
|
||||
|
||||
public bool Compare(object? fieldValue, string operatorName, object? conditionValue)
|
||||
{
|
||||
if (fieldValue is null || operatorName != "in" || conditionValue is null)
|
||||
return false;
|
||||
|
||||
var items = ExtractItems(conditionValue);
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (ValueEquals(fieldValue, item))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<object> ExtractItems(object? conditionValue)
|
||||
{
|
||||
if (conditionValue is JsonElement element)
|
||||
{
|
||||
if (element.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
return element.EnumerateArray().Select(e => (object)e).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
var str = ValueComparatorHelper.GetStringValue(conditionValue);
|
||||
if (str is not null)
|
||||
{
|
||||
return str.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Cast<object>()
|
||||
.ToList();
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private static bool ValueEquals(object fieldValue, object item)
|
||||
{
|
||||
var fieldStr = ValueComparatorHelper.GetStringValue(fieldValue);
|
||||
var itemStr = ValueComparatorHelper.GetStringValue(item);
|
||||
|
||||
if (fieldStr is not null && itemStr is not null)
|
||||
return string.Equals(fieldStr, itemStr, StringComparison.Ordinal);
|
||||
|
||||
// 尝试数值比较
|
||||
if (ValueComparatorHelper.TryGetDecimal(fieldValue, out var fieldNum) &&
|
||||
ValueComparatorHelper.TryGetDecimal(item, out var itemNum))
|
||||
return fieldNum == itemNum;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Workflow.Domain.Expressions.Comparators;
|
||||
|
||||
/// <summary>
|
||||
/// 日期对比器。处理 ==, !=, >, <, >=, <= 操作符。
|
||||
/// 仅当两个值都能解析为 DateTime 时才进行比较,否则返回 false。
|
||||
/// </summary>
|
||||
public class DateTimeComparator : IValueComparator
|
||||
{
|
||||
private static readonly IReadOnlySet<string> _supportedOperators = new HashSet<string>
|
||||
{
|
||||
"==", "!=", ">", "<", ">=", "<="
|
||||
};
|
||||
|
||||
public IReadOnlySet<string> SupportedOperators => _supportedOperators;
|
||||
|
||||
public bool Compare(object? fieldValue, string operatorName, object? conditionValue)
|
||||
{
|
||||
if (fieldValue is null || !_supportedOperators.Contains(operatorName))
|
||||
return false;
|
||||
|
||||
if (!ValueComparatorHelper.TryGetDateTime(fieldValue, out var fieldDate))
|
||||
return false;
|
||||
|
||||
if (!ValueComparatorHelper.TryGetDateTimeFromCondition(conditionValue, out var condDate))
|
||||
return false;
|
||||
|
||||
return operatorName switch
|
||||
{
|
||||
"==" => fieldDate == condDate,
|
||||
"!=" => fieldDate != condDate,
|
||||
">" => fieldDate > condDate,
|
||||
"<" => fieldDate < condDate,
|
||||
">=" => fieldDate >= condDate,
|
||||
"<=" => fieldDate <= condDate,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
namespace Workflow.Domain.Expressions.Comparators;
|
||||
|
||||
/// <summary>
|
||||
/// 值对比策略接口。每个对比器声明自己支持的操作符,并执行实际的比较逻辑。
|
||||
/// </summary>
|
||||
public interface IValueComparator
|
||||
{
|
||||
/// <summary>
|
||||
/// 此对比器支持的操作符集合。
|
||||
/// 例如: { "==", "!=", ">", "<", ">=", "<=" }
|
||||
/// </summary>
|
||||
IReadOnlySet<string> SupportedOperators { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 使用指定操作符比较字段值与条件值。
|
||||
/// </summary>
|
||||
/// <param name="fieldValue">流程变量中的运行时值</param>
|
||||
/// <param name="operatorName">SupportedOperators 中的一个操作符</param>
|
||||
/// <param name="conditionValue">条件值,可能是 string 或 JsonElement</param>
|
||||
/// <returns>比较通过返回 true,否则 false</returns>
|
||||
bool Compare(object? fieldValue, string operatorName, object? conditionValue);
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
namespace Workflow.Domain.Expressions.Comparators;
|
||||
|
||||
/// <summary>
|
||||
/// 值对比器注册中心。收集所有 IValueComparator 并按操作符索引,
|
||||
/// 通过责任链按序分派比较请求。
|
||||
/// </summary>
|
||||
public interface IValueComparatorRegistry
|
||||
{
|
||||
/// <summary>
|
||||
/// 尝试查找匹配的对比器并执行比较。
|
||||
/// 按注册顺序逐个尝试,第一个返回 true 的对比器即为最终结果。
|
||||
/// 如果没有对比器支持该操作符,返回 false。
|
||||
/// </summary>
|
||||
bool TryCompare(object? fieldValue, string operatorName, object? conditionValue);
|
||||
|
||||
/// <summary>
|
||||
/// 所有已注册对比器支持的操作符集合。
|
||||
/// </summary>
|
||||
IReadOnlySet<string> RegisteredOperators { get; }
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Workflow.Domain.Expressions.Comparators;
|
||||
|
||||
/// <summary>
|
||||
/// 数值对比器。处理 ==, !=, >, <, >=, <= 操作符。
|
||||
/// 仅当两个值都能解析为 decimal 时才进行比较,否则返回 false。
|
||||
/// </summary>
|
||||
public class NumericComparator : IValueComparator
|
||||
{
|
||||
private static readonly IReadOnlySet<string> _supportedOperators = new HashSet<string>
|
||||
{
|
||||
"==", "!=", ">", "<", ">=", "<="
|
||||
};
|
||||
|
||||
public IReadOnlySet<string> SupportedOperators => _supportedOperators;
|
||||
|
||||
public bool Compare(object? fieldValue, string operatorName, object? conditionValue)
|
||||
{
|
||||
if (fieldValue is null || !_supportedOperators.Contains(operatorName))
|
||||
return false;
|
||||
|
||||
if (!ValueComparatorHelper.TryGetDecimal(fieldValue, out var fieldNum))
|
||||
return false;
|
||||
|
||||
if (!ValueComparatorHelper.TryGetDecimalFromCondition(conditionValue, out var condNum))
|
||||
return false;
|
||||
|
||||
return operatorName switch
|
||||
{
|
||||
"==" => fieldNum == condNum,
|
||||
"!=" => fieldNum != condNum,
|
||||
">" => fieldNum > condNum,
|
||||
"<" => fieldNum < condNum,
|
||||
">=" => fieldNum >= condNum,
|
||||
"<=" => fieldNum <= condNum,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,83 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Workflow.Domain.Expressions.Comparators;
|
||||
|
||||
/// <summary>
|
||||
/// 字符串对比器。处理 ==, !=, contains, startsWith, endsWith, isEmpty 操作符。
|
||||
/// 作为 == 和 != 的兜底:当数值/日期/布尔对比器都无法识别值类型时,回退到字符串比较。
|
||||
/// </summary>
|
||||
public class StringComparator : IValueComparator
|
||||
{
|
||||
private static readonly IReadOnlySet<string> _supportedOperators = new HashSet<string>
|
||||
{
|
||||
"==", "!=", "contains", "startsWith", "endsWith", "isEmpty"
|
||||
};
|
||||
|
||||
public IReadOnlySet<string> SupportedOperators => _supportedOperators;
|
||||
|
||||
public bool Compare(object? fieldValue, string operatorName, object? conditionValue)
|
||||
{
|
||||
if (!_supportedOperators.Contains(operatorName))
|
||||
return false;
|
||||
|
||||
return operatorName switch
|
||||
{
|
||||
"isEmpty" => IsEmpty(fieldValue),
|
||||
"==" => StringEquals(fieldValue, conditionValue),
|
||||
"!=" => !StringEquals(fieldValue, conditionValue),
|
||||
"contains" => Contains(fieldValue, conditionValue),
|
||||
"startsWith" => StartsWith(fieldValue, conditionValue),
|
||||
"endsWith" => EndsWith(fieldValue, conditionValue),
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsEmpty(object? fieldValue)
|
||||
{
|
||||
return fieldValue is null || string.IsNullOrWhiteSpace(fieldValue.ToString());
|
||||
}
|
||||
|
||||
private static bool StringEquals(object? fieldValue, object? conditionValue)
|
||||
{
|
||||
var fieldStr = ValueComparatorHelper.GetStringValue(fieldValue);
|
||||
if (fieldStr is null) return false;
|
||||
|
||||
var condStr = ValueComparatorHelper.GetStringValue(conditionValue);
|
||||
if (condStr is null) return false;
|
||||
|
||||
return string.Equals(fieldStr, condStr, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool Contains(object? fieldValue, object? conditionValue)
|
||||
{
|
||||
var fieldStr = ValueComparatorHelper.GetStringValue(fieldValue);
|
||||
if (fieldStr is null) return false;
|
||||
|
||||
var condStr = ValueComparatorHelper.GetStringValue(conditionValue);
|
||||
if (condStr is null) return false;
|
||||
|
||||
return fieldStr.Contains(condStr, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool StartsWith(object? fieldValue, object? conditionValue)
|
||||
{
|
||||
var fieldStr = ValueComparatorHelper.GetStringValue(fieldValue);
|
||||
if (fieldStr is null) return false;
|
||||
|
||||
var condStr = ValueComparatorHelper.GetStringValue(conditionValue);
|
||||
if (condStr is null) return false;
|
||||
|
||||
return fieldStr.StartsWith(condStr, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool EndsWith(object? fieldValue, object? conditionValue)
|
||||
{
|
||||
var fieldStr = ValueComparatorHelper.GetStringValue(fieldValue);
|
||||
if (fieldStr is null) return false;
|
||||
|
||||
var condStr = ValueComparatorHelper.GetStringValue(conditionValue);
|
||||
if (condStr is null) return false;
|
||||
|
||||
return fieldStr.EndsWith(condStr, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,133 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Workflow.Domain.Expressions.Comparators;
|
||||
|
||||
/// <summary>
|
||||
/// 值对比器共享的静态工具方法。从 ConditionEvaluator 中提取,
|
||||
/// 供所有内置对比器复用。
|
||||
/// </summary>
|
||||
public static class ValueComparatorHelper
|
||||
{
|
||||
public static bool TryGetDecimal(object? value, out decimal result)
|
||||
{
|
||||
result = 0m;
|
||||
return value switch
|
||||
{
|
||||
decimal d => SetResult(d, out result),
|
||||
double db => SetResult((decimal)db, out result),
|
||||
float f => SetResult((decimal)f, out result),
|
||||
int i => SetResult((decimal)i, out result),
|
||||
long l => SetResult((decimal)l, out result),
|
||||
short s => SetResult((decimal)s, out result),
|
||||
byte b => SetResult((decimal)b, out result),
|
||||
uint ui => SetResult((decimal)ui, out result),
|
||||
ulong ul => SetResult((decimal)ul, out result),
|
||||
string str => decimal.TryParse(str, out result),
|
||||
JsonElement e => TryGetDecimalFromElement(e, out result),
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
public static bool TryGetDecimalFromElement(JsonElement element, out decimal result)
|
||||
{
|
||||
result = 0m;
|
||||
if (element.ValueKind == JsonValueKind.Number)
|
||||
return element.TryGetDecimal(out result);
|
||||
|
||||
if (element.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var str = element.GetString();
|
||||
return str is not null && decimal.TryParse(str, out result);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool TryGetDateTime(object? value, out DateTime result)
|
||||
{
|
||||
result = DateTime.MinValue;
|
||||
return value switch
|
||||
{
|
||||
DateTime dt => SetResult(dt, out result),
|
||||
DateTimeOffset dto => SetResult(dto.UtcDateTime, out result),
|
||||
string s => DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out result),
|
||||
JsonElement e => TryGetDateTimeFromElement(e, out result),
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
public static bool TryGetDateTimeFromElement(JsonElement element, out DateTime result)
|
||||
{
|
||||
result = DateTime.MinValue;
|
||||
if (element.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var str = element.GetString();
|
||||
return str is not null && DateTime.TryParse(str, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out result);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool TryGetBoolean(object? value, out bool result)
|
||||
{
|
||||
result = false;
|
||||
return value switch
|
||||
{
|
||||
bool b => SetResult(b, out result),
|
||||
string s => bool.TryParse(s, out result),
|
||||
JsonElement e => TryGetBooleanFromElement(e, out result),
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
public static bool TryGetBooleanFromElement(JsonElement element, out bool result)
|
||||
{
|
||||
result = false;
|
||||
if (element.ValueKind == JsonValueKind.True) { result = true; return true; }
|
||||
if (element.ValueKind == JsonValueKind.False) { result = false; return true; }
|
||||
if (element.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var str = element.GetString();
|
||||
return str is not null && bool.TryParse(str, out result);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static string? GetStringValue(object? value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
null => null,
|
||||
string s => s,
|
||||
JsonElement e => e.ValueKind == JsonValueKind.String ? e.GetString() : e.GetRawText(),
|
||||
_ => value.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
public static bool TryGetDecimalFromCondition(object? conditionValue, out decimal result)
|
||||
{
|
||||
if (conditionValue is string s)
|
||||
return decimal.TryParse(s, NumberStyles.Number, CultureInfo.InvariantCulture, out result);
|
||||
return TryGetDecimal(conditionValue, out result);
|
||||
}
|
||||
|
||||
public static bool TryGetDateTimeFromCondition(object? conditionValue, out DateTime result)
|
||||
{
|
||||
if (conditionValue is string s)
|
||||
return DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out result);
|
||||
return TryGetDateTime(conditionValue, out result);
|
||||
}
|
||||
|
||||
public static bool TryGetBooleanFromCondition(object? conditionValue, out bool result)
|
||||
{
|
||||
if (conditionValue is string s)
|
||||
return bool.TryParse(s, out result);
|
||||
return TryGetBoolean(conditionValue, out result);
|
||||
}
|
||||
|
||||
private static bool SetResult<T>(T value, out T result)
|
||||
{
|
||||
result = value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -1,37 +1,46 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using Workflow.Domain.Expressions.Comparators;
|
||||
|
||||
namespace Workflow.Domain.Expressions;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates workflow conditions expressed as JSON against a variables dictionary.
|
||||
/// Supports simple comparisons, logical AND/OR combinations, nested expressions,
|
||||
/// and simple text expressions like "amount > 1000".
|
||||
/// 条件表达式评估器。解析简单文本表达式和 JSON 条件树,
|
||||
/// 委托给 IValueComparatorRegistry 执行实际比较。
|
||||
/// </summary>
|
||||
public class ConditionEvaluator
|
||||
{
|
||||
/// <summary>
|
||||
/// Evaluates a condition expression against the given variables.
|
||||
/// </summary>
|
||||
/// <param name="condition">JSON condition expression or simple text expression</param>
|
||||
/// <param name="variables">Variables to evaluate against</param>
|
||||
/// <returns>True if condition passes, false otherwise. Returns true for empty/null conditions.</returns>
|
||||
private readonly IValueComparatorRegistry _registry;
|
||||
|
||||
public ConditionEvaluator()
|
||||
{
|
||||
var builtInComparators = new IValueComparator[]
|
||||
{
|
||||
new NumericComparator(),
|
||||
new DateTimeComparator(),
|
||||
new BooleanComparator(),
|
||||
new CollectionComparator(),
|
||||
new StringComparator(),
|
||||
};
|
||||
_registry = new DefaultRegistry(builtInComparators);
|
||||
}
|
||||
|
||||
public ConditionEvaluator(IValueComparatorRegistry registry)
|
||||
{
|
||||
_registry = registry;
|
||||
}
|
||||
|
||||
public bool Evaluate(string? condition, Dictionary<string, object>? variables)
|
||||
{
|
||||
// Empty/null/whitespace condition = always pass
|
||||
if (string.IsNullOrWhiteSpace(condition))
|
||||
return true;
|
||||
|
||||
// Null variables means nothing to evaluate against
|
||||
if (variables is null)
|
||||
return false;
|
||||
|
||||
// Try simple text expression first (e.g., "amount > 1000")
|
||||
if (TryParseSimpleExpression(condition, out var field, out var op, out var value))
|
||||
return EvaluateSimpleExpression(field, op, value, variables);
|
||||
|
||||
// Fall back to JSON evaluation
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(condition);
|
||||
@ -39,13 +48,73 @@ public class ConditionEvaluator
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// Malformed JSON returns false
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool EvaluateSimpleExpression(string field, string op, string value, Dictionary<string, object> variables)
|
||||
{
|
||||
if (!variables.TryGetValue(field, out var rawFieldValue) || rawFieldValue is null)
|
||||
return false;
|
||||
|
||||
return _registry.TryCompare(rawFieldValue, op, value);
|
||||
}
|
||||
|
||||
private bool EvaluateElement(JsonElement element, Dictionary<string, object> variables)
|
||||
{
|
||||
if (element.ValueKind != JsonValueKind.Object)
|
||||
return false;
|
||||
|
||||
if (element.TryGetProperty("and", out var andArray))
|
||||
{
|
||||
if (andArray.ValueKind != JsonValueKind.Array)
|
||||
return false;
|
||||
|
||||
foreach (var child in andArray.EnumerateArray())
|
||||
{
|
||||
if (!EvaluateElement(child, variables))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (element.TryGetProperty("or", out var orArray))
|
||||
{
|
||||
if (orArray.ValueKind != JsonValueKind.Array)
|
||||
return false;
|
||||
|
||||
foreach (var child in orArray.EnumerateArray())
|
||||
{
|
||||
if (EvaluateElement(child, variables))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return EvaluateComparison(element, variables);
|
||||
}
|
||||
|
||||
private bool EvaluateComparison(JsonElement element, Dictionary<string, object> variables)
|
||||
{
|
||||
if (!element.TryGetProperty("field", out var fieldElement)) return false;
|
||||
if (!element.TryGetProperty("op", out var opElement)) return false;
|
||||
if (!element.TryGetProperty("value", out var valueElement)) return false;
|
||||
|
||||
var fieldName = fieldElement.GetString();
|
||||
var op = opElement.GetString();
|
||||
|
||||
if (fieldName is null || op is null) return false;
|
||||
|
||||
if (!variables.TryGetValue(fieldName, out var rawFieldValue)) return false;
|
||||
if (rawFieldValue is null) return false;
|
||||
|
||||
return _registry.TryCompare(rawFieldValue, op, valueElement);
|
||||
}
|
||||
|
||||
private static readonly Regex SimpleExpressionRegex =
|
||||
new(@"^\s*(\w+)\s*(==|!=|>=|<=|>|<|contains)\s*(.+?)\s*$", RegexOptions.Compiled);
|
||||
new(@"^\s*(\w+)\s*(==|!=|>=|<=|>|<|contains|startsWith|endsWith|isEmpty)\s*(.*?)\s*$", RegexOptions.Compiled);
|
||||
|
||||
private bool TryParseSimpleExpression(string condition, out string field, out string op, out string value)
|
||||
{
|
||||
@ -63,221 +132,45 @@ public class ConditionEvaluator
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool EvaluateSimpleExpression(string field, string op, string value, Dictionary<string, object> variables)
|
||||
private sealed class DefaultRegistry : IValueComparatorRegistry
|
||||
{
|
||||
if (!variables.TryGetValue(field, out var rawFieldValue) || rawFieldValue is null)
|
||||
return false;
|
||||
private readonly Dictionary<string, List<IValueComparator>> _comparatorsByOperator;
|
||||
private readonly IReadOnlySet<string> _registeredOperators;
|
||||
|
||||
var fieldStr = rawFieldValue.ToString() ?? string.Empty;
|
||||
|
||||
return op switch
|
||||
public DefaultRegistry(IEnumerable<IValueComparator> comparators)
|
||||
{
|
||||
"==" => string.Equals(fieldStr, value, StringComparison.Ordinal) ||
|
||||
TryNumericCompare(rawFieldValue, value, (a, b) => a == b),
|
||||
"!=" => !string.Equals(fieldStr, value, StringComparison.Ordinal) &&
|
||||
!TryNumericCompare(rawFieldValue, value, (a, b) => a == b),
|
||||
">" => TryNumericCompare(rawFieldValue, value, (a, b) => a > b),
|
||||
"<" => TryNumericCompare(rawFieldValue, value, (a, b) => a < b),
|
||||
">=" => TryNumericCompare(rawFieldValue, value, (a, b) => a >= b),
|
||||
"<=" => TryNumericCompare(rawFieldValue, value, (a, b) => a <= b),
|
||||
"contains" => fieldStr.Contains(value, StringComparison.Ordinal),
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
_comparatorsByOperator = new Dictionary<string, List<IValueComparator>>();
|
||||
|
||||
private static bool TryNumericCompare(object fieldValue, string conditionValue, Func<decimal, decimal, bool> comparator)
|
||||
{
|
||||
if (!TryGetDecimal(fieldValue, out var fieldNum))
|
||||
return false;
|
||||
|
||||
if (!decimal.TryParse(conditionValue, NumberStyles.Number, CultureInfo.InvariantCulture, out var condNum))
|
||||
return false;
|
||||
|
||||
return comparator(fieldNum, condNum);
|
||||
}
|
||||
|
||||
private bool EvaluateElement(JsonElement element, Dictionary<string, object> variables)
|
||||
{
|
||||
if (element.ValueKind != JsonValueKind.Object)
|
||||
return false;
|
||||
|
||||
// Check for "and" combinator
|
||||
if (element.TryGetProperty("and", out var andArray))
|
||||
{
|
||||
if (andArray.ValueKind != JsonValueKind.Array)
|
||||
return false;
|
||||
|
||||
foreach (var child in andArray.EnumerateArray())
|
||||
foreach (var comparator in comparators)
|
||||
{
|
||||
if (!EvaluateElement(child, variables))
|
||||
return false;
|
||||
foreach (var op in comparator.SupportedOperators)
|
||||
{
|
||||
if (!_comparatorsByOperator.TryGetValue(op, out var list))
|
||||
{
|
||||
list = [];
|
||||
_comparatorsByOperator[op] = list;
|
||||
}
|
||||
list.Add(comparator);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
_registeredOperators = _comparatorsByOperator.Keys.ToHashSet();
|
||||
}
|
||||
|
||||
// Check for "or" combinator
|
||||
if (element.TryGetProperty("or", out var orArray))
|
||||
public IReadOnlySet<string> RegisteredOperators => _registeredOperators;
|
||||
|
||||
public bool TryCompare(object? fieldValue, string operatorName, object? conditionValue)
|
||||
{
|
||||
if (orArray.ValueKind != JsonValueKind.Array)
|
||||
if (!_comparatorsByOperator.TryGetValue(operatorName, out var comparators))
|
||||
return false;
|
||||
|
||||
foreach (var child in orArray.EnumerateArray())
|
||||
foreach (var comparator in comparators)
|
||||
{
|
||||
if (EvaluateElement(child, variables))
|
||||
if (comparator.Compare(fieldValue, operatorName, conditionValue))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Simple comparison: { "field": "...", "op": "...", "value": ... }
|
||||
return EvaluateComparison(element, variables);
|
||||
}
|
||||
|
||||
private bool EvaluateComparison(JsonElement element, Dictionary<string, object> variables)
|
||||
{
|
||||
if (!element.TryGetProperty("field", out var fieldElement))
|
||||
return false;
|
||||
|
||||
if (!element.TryGetProperty("op", out var opElement))
|
||||
return false;
|
||||
|
||||
if (!element.TryGetProperty("value", out var valueElement))
|
||||
return false;
|
||||
|
||||
var fieldName = fieldElement.GetString();
|
||||
var op = opElement.GetString();
|
||||
|
||||
if (fieldName is null || op is null)
|
||||
return false;
|
||||
|
||||
// Look up field value from variables
|
||||
if (!variables.TryGetValue(fieldName, out var rawFieldValue))
|
||||
return false;
|
||||
|
||||
if (rawFieldValue is null)
|
||||
return false;
|
||||
|
||||
// Apply the operator
|
||||
return ApplyOperator(op, rawFieldValue, valueElement);
|
||||
}
|
||||
|
||||
private bool ApplyOperator(string op, object fieldValue, JsonElement conditionValue)
|
||||
{
|
||||
return op switch
|
||||
{
|
||||
"==" => EvaluateEquals(fieldValue, conditionValue),
|
||||
"!=" => !EvaluateEquals(fieldValue, conditionValue),
|
||||
">" => EvaluateNumericComparison(fieldValue, conditionValue, (a, b) => a > b),
|
||||
"<" => EvaluateNumericComparison(fieldValue, conditionValue, (a, b) => a < b),
|
||||
">=" => EvaluateNumericComparison(fieldValue, conditionValue, (a, b) => a >= b),
|
||||
"<=" => EvaluateNumericComparison(fieldValue, conditionValue, (a, b) => a <= b),
|
||||
"contains" => EvaluateContains(fieldValue, conditionValue),
|
||||
"in" => EvaluateIn(fieldValue, conditionValue),
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
private bool EvaluateEquals(object fieldValue, JsonElement conditionValue)
|
||||
{
|
||||
// Try numeric comparison first
|
||||
if (TryGetDecimal(fieldValue, out var fieldNum) &&
|
||||
TryGetDecimalFromElement(conditionValue, out var condNum))
|
||||
{
|
||||
return fieldNum == condNum;
|
||||
}
|
||||
|
||||
// Fall back to string comparison
|
||||
var fieldStr = fieldValue.ToString();
|
||||
var condStr = conditionValue.ValueKind == JsonValueKind.String
|
||||
? conditionValue.GetString()
|
||||
: conditionValue.GetRawText();
|
||||
|
||||
return string.Equals(fieldStr, condStr, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private bool EvaluateNumericComparison(
|
||||
object fieldValue,
|
||||
JsonElement conditionValue,
|
||||
Func<decimal, decimal, bool> comparator)
|
||||
{
|
||||
if (!TryGetDecimal(fieldValue, out var fieldNum))
|
||||
return false;
|
||||
|
||||
if (!TryGetDecimalFromElement(conditionValue, out var condNum))
|
||||
return false;
|
||||
|
||||
return comparator(fieldNum, condNum);
|
||||
}
|
||||
|
||||
private bool EvaluateContains(object fieldValue, JsonElement conditionValue)
|
||||
{
|
||||
if (conditionValue.ValueKind != JsonValueKind.String)
|
||||
return false;
|
||||
|
||||
var searchStr = conditionValue.GetString();
|
||||
if (searchStr is null)
|
||||
return false;
|
||||
|
||||
var fieldStr = fieldValue.ToString() ?? string.Empty;
|
||||
return fieldStr.Contains(searchStr, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private bool EvaluateIn(object fieldValue, JsonElement conditionValue)
|
||||
{
|
||||
if (conditionValue.ValueKind != JsonValueKind.Array)
|
||||
return false;
|
||||
|
||||
foreach (var item in conditionValue.EnumerateArray())
|
||||
{
|
||||
if (EvaluateEquals(fieldValue, item))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryGetDecimal(object value, out decimal result)
|
||||
{
|
||||
result = 0m;
|
||||
|
||||
return value switch
|
||||
{
|
||||
decimal d => SetResult(d, out result),
|
||||
double db => SetResult((decimal)db, out result),
|
||||
float f => SetResult((decimal)f, out result),
|
||||
int i => SetResult((decimal)i, out result),
|
||||
long l => SetResult((decimal)l, out result),
|
||||
short s => SetResult((decimal)s, out result),
|
||||
byte b => SetResult((decimal)b, out result),
|
||||
uint ui => SetResult((decimal)ui, out result),
|
||||
ulong ul => SetResult((decimal)ul, out result),
|
||||
string str => decimal.TryParse(str, out result),
|
||||
JsonElement e => TryGetDecimalFromElement(e, out result),
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
private static bool TryGetDecimalFromElement(JsonElement element, out decimal result)
|
||||
{
|
||||
result = 0m;
|
||||
|
||||
if (element.ValueKind == JsonValueKind.Number)
|
||||
return element.TryGetDecimal(out result);
|
||||
|
||||
if (element.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var str = element.GetString();
|
||||
return str is not null && decimal.TryParse(str, out result);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool SetResult(decimal value, out decimal result)
|
||||
{
|
||||
result = value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
561
src/Workflow.Infrastructure/Migrations/20260520071449_InitialCreate.Designer.cs
generated
Normal file
561
src/Workflow.Infrastructure/Migrations/20260520071449_InitialCreate.Designer.cs
generated
Normal file
@ -0,0 +1,561 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Workflow.Infrastructure.Persistence;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Workflow.Infrastructure.Migrations
|
||||
{
|
||||
[DbContext(typeof(WorkflowDbContext))]
|
||||
[Migration("20260520071449_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.7")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.FormData", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("DataJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<Guid>("FormDefinitionId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("InstanceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("UpdatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId");
|
||||
|
||||
b.ToTable("wf_form_data", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.FormDefinition", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("OperatorIP")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("SchemaJson")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("UpdatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Code");
|
||||
|
||||
b.ToTable("wf_form_definitions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.FormDefinitionField", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Config")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("DefaultValue")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("FieldKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<int>("FieldType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid>("FormDefinitionId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Label")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<bool>("Required")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("UpdatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FormDefinitionId");
|
||||
|
||||
b.ToTable("wf_form_definition_fields", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.WorkflowDefinition", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("DefinitionJson")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("OperatorIP")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("UpdatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Code");
|
||||
|
||||
b.ToTable("wf_workflow_definitions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.WorkflowEdge", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Condition")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("DefinitionId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("EdgeType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Label")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<int>("Order")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid>("SourceNodeId")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("TargetNodeId")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("UpdatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DefinitionId");
|
||||
|
||||
b.ToTable("wf_workflow_edges", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.WorkflowInstance", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("DefinitionId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("InitiatorId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("ParentInstanceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("ParentTokenId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("StartedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("UpdatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Variables")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DefinitionId");
|
||||
|
||||
b.ToTable("wf_workflow_instances", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.WorkflowNode", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Config")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("DefinitionId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<int>("NodeType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("PositionX")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("PositionY")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("UpdatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DefinitionId");
|
||||
|
||||
b.ToTable("wf_workflow_nodes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.WorkflowTask", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("AssigneeId")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("AssigneeRole")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("CandidateRoles")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("CandidateUsers")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("Comment")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("DelegatedFromId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime?>("DueAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("InstanceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<Guid>("NodeId")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("OperatorIP")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("Result")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<Guid>("TokenId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("UpdatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId");
|
||||
|
||||
b.ToTable("wf_workflow_tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.WorkflowToken", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("ArrivedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("InstanceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("NodeId")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("UpdatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId");
|
||||
|
||||
b.ToTable("wf_workflow_tokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.FormDefinitionField", b =>
|
||||
{
|
||||
b.HasOne("Workflow.Domain.Entities.FormDefinition", null)
|
||||
.WithMany("Fields")
|
||||
.HasForeignKey("FormDefinitionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.WorkflowEdge", b =>
|
||||
{
|
||||
b.HasOne("Workflow.Domain.Entities.WorkflowDefinition", null)
|
||||
.WithMany("Edges")
|
||||
.HasForeignKey("DefinitionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.WorkflowNode", b =>
|
||||
{
|
||||
b.HasOne("Workflow.Domain.Entities.WorkflowDefinition", null)
|
||||
.WithMany("Nodes")
|
||||
.HasForeignKey("DefinitionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.WorkflowTask", b =>
|
||||
{
|
||||
b.HasOne("Workflow.Domain.Entities.WorkflowInstance", null)
|
||||
.WithMany("Tasks")
|
||||
.HasForeignKey("InstanceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.WorkflowToken", b =>
|
||||
{
|
||||
b.HasOne("Workflow.Domain.Entities.WorkflowInstance", null)
|
||||
.WithMany("Tokens")
|
||||
.HasForeignKey("InstanceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.FormDefinition", b =>
|
||||
{
|
||||
b.Navigation("Fields");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.WorkflowDefinition", b =>
|
||||
{
|
||||
b.Navigation("Edges");
|
||||
|
||||
b.Navigation("Nodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.WorkflowInstance", b =>
|
||||
{
|
||||
b.Navigation("Tasks");
|
||||
|
||||
b.Navigation("Tokens");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,328 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Workflow.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialCreate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "wf_form_data",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
FormDefinitionId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
InstanceId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
DataJson = table.Column<string>(type: "jsonb", nullable: false),
|
||||
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_wf_form_data", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "wf_form_definitions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false),
|
||||
Code = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false),
|
||||
Version = table.Column<int>(type: "integer", nullable: false),
|
||||
Description = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
|
||||
SchemaJson = table.Column<string>(type: "jsonb", nullable: true),
|
||||
Status = table.Column<int>(type: "integer", nullable: false),
|
||||
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
|
||||
OperatorIP = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_wf_form_definitions", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "wf_workflow_definitions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false),
|
||||
Code = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false),
|
||||
Version = table.Column<int>(type: "integer", nullable: false),
|
||||
Description = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
|
||||
Status = table.Column<int>(type: "integer", nullable: false),
|
||||
DefinitionJson = table.Column<string>(type: "jsonb", nullable: true),
|
||||
IsEnabled = table.Column<bool>(type: "boolean", nullable: false),
|
||||
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
|
||||
OperatorIP = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_wf_workflow_definitions", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "wf_workflow_instances",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
DefinitionId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Title = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false),
|
||||
Status = table.Column<int>(type: "integer", nullable: false),
|
||||
Variables = table.Column<string>(type: "jsonb", nullable: true),
|
||||
InitiatorId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
StartedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
CompletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
ParentInstanceId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
ParentTokenId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_wf_workflow_instances", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "wf_form_definition_fields",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
FormDefinitionId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
FieldKey = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false),
|
||||
Label = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false),
|
||||
FieldType = table.Column<int>(type: "integer", nullable: false),
|
||||
Required = table.Column<bool>(type: "boolean", nullable: false),
|
||||
DefaultValue = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
|
||||
Config = table.Column<string>(type: "jsonb", nullable: true),
|
||||
SortOrder = table.Column<int>(type: "integer", nullable: false),
|
||||
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_wf_form_definition_fields", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_wf_form_definition_fields_wf_form_definitions_FormDefinitio~",
|
||||
column: x => x.FormDefinitionId,
|
||||
principalTable: "wf_form_definitions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "wf_workflow_edges",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
DefinitionId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
SourceNodeId = table.Column<Guid>(type: "uuid", maxLength: 500, nullable: false),
|
||||
TargetNodeId = table.Column<Guid>(type: "uuid", maxLength: 500, nullable: false),
|
||||
EdgeType = table.Column<int>(type: "integer", nullable: false),
|
||||
Label = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
|
||||
Condition = table.Column<string>(type: "jsonb", nullable: true),
|
||||
Order = table.Column<int>(type: "integer", nullable: false),
|
||||
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_wf_workflow_edges", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_wf_workflow_edges_wf_workflow_definitions_DefinitionId",
|
||||
column: x => x.DefinitionId,
|
||||
principalTable: "wf_workflow_definitions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "wf_workflow_nodes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
DefinitionId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
NodeType = table.Column<int>(type: "integer", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false),
|
||||
Config = table.Column<string>(type: "jsonb", nullable: true),
|
||||
PositionX = table.Column<int>(type: "integer", nullable: false),
|
||||
PositionY = table.Column<int>(type: "integer", nullable: false),
|
||||
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_wf_workflow_nodes", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_wf_workflow_nodes_wf_workflow_definitions_DefinitionId",
|
||||
column: x => x.DefinitionId,
|
||||
principalTable: "wf_workflow_definitions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "wf_workflow_tasks",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
InstanceId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
TokenId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
NodeId = table.Column<Guid>(type: "uuid", maxLength: 500, nullable: false),
|
||||
Title = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false),
|
||||
AssigneeId = table.Column<Guid>(type: "uuid", maxLength: 500, nullable: true),
|
||||
AssigneeRole = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
|
||||
DelegatedFromId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
CandidateUsers = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
|
||||
CandidateRoles = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
|
||||
Type = table.Column<int>(type: "integer", nullable: false),
|
||||
Status = table.Column<int>(type: "integer", nullable: false),
|
||||
Result = table.Column<string>(type: "jsonb", nullable: true),
|
||||
Comment = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
|
||||
DueAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
CompletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
|
||||
OperatorIP = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_wf_workflow_tasks", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_wf_workflow_tasks_wf_workflow_instances_InstanceId",
|
||||
column: x => x.InstanceId,
|
||||
principalTable: "wf_workflow_instances",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "wf_workflow_tokens",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
InstanceId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
NodeId = table.Column<Guid>(type: "uuid", maxLength: 500, nullable: false),
|
||||
Status = table.Column<int>(type: "integer", nullable: false),
|
||||
ArrivedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
CompletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_wf_workflow_tokens", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_wf_workflow_tokens_wf_workflow_instances_InstanceId",
|
||||
column: x => x.InstanceId,
|
||||
principalTable: "wf_workflow_instances",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_wf_form_data_InstanceId",
|
||||
table: "wf_form_data",
|
||||
column: "InstanceId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_wf_form_definition_fields_FormDefinitionId",
|
||||
table: "wf_form_definition_fields",
|
||||
column: "FormDefinitionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_wf_form_definitions_Code",
|
||||
table: "wf_form_definitions",
|
||||
column: "Code");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_wf_workflow_definitions_Code",
|
||||
table: "wf_workflow_definitions",
|
||||
column: "Code");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_wf_workflow_edges_DefinitionId",
|
||||
table: "wf_workflow_edges",
|
||||
column: "DefinitionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_wf_workflow_instances_DefinitionId",
|
||||
table: "wf_workflow_instances",
|
||||
column: "DefinitionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_wf_workflow_nodes_DefinitionId",
|
||||
table: "wf_workflow_nodes",
|
||||
column: "DefinitionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_wf_workflow_tasks_InstanceId",
|
||||
table: "wf_workflow_tasks",
|
||||
column: "InstanceId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_wf_workflow_tokens_InstanceId",
|
||||
table: "wf_workflow_tokens",
|
||||
column: "InstanceId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "wf_form_data");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "wf_form_definition_fields");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "wf_workflow_edges");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "wf_workflow_nodes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "wf_workflow_tasks");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "wf_workflow_tokens");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "wf_form_definitions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "wf_workflow_definitions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "wf_workflow_instances");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,558 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Workflow.Infrastructure.Persistence;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Workflow.Infrastructure.Migrations
|
||||
{
|
||||
[DbContext(typeof(WorkflowDbContext))]
|
||||
partial class WorkflowDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.7")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.FormData", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("DataJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<Guid>("FormDefinitionId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("InstanceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("UpdatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId");
|
||||
|
||||
b.ToTable("wf_form_data", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.FormDefinition", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("OperatorIP")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("SchemaJson")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("UpdatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Code");
|
||||
|
||||
b.ToTable("wf_form_definitions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.FormDefinitionField", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Config")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("DefaultValue")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("FieldKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<int>("FieldType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid>("FormDefinitionId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Label")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<bool>("Required")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("UpdatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FormDefinitionId");
|
||||
|
||||
b.ToTable("wf_form_definition_fields", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.WorkflowDefinition", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("DefinitionJson")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("OperatorIP")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("UpdatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Code");
|
||||
|
||||
b.ToTable("wf_workflow_definitions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.WorkflowEdge", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Condition")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("DefinitionId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("EdgeType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Label")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<int>("Order")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid>("SourceNodeId")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("TargetNodeId")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("UpdatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DefinitionId");
|
||||
|
||||
b.ToTable("wf_workflow_edges", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.WorkflowInstance", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("DefinitionId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("InitiatorId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("ParentInstanceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("ParentTokenId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("StartedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("UpdatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Variables")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DefinitionId");
|
||||
|
||||
b.ToTable("wf_workflow_instances", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.WorkflowNode", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Config")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("DefinitionId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<int>("NodeType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("PositionX")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("PositionY")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("UpdatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DefinitionId");
|
||||
|
||||
b.ToTable("wf_workflow_nodes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.WorkflowTask", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("AssigneeId")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("AssigneeRole")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("CandidateRoles")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("CandidateUsers")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("Comment")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("DelegatedFromId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime?>("DueAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("InstanceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<Guid>("NodeId")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("OperatorIP")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("Result")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<Guid>("TokenId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("UpdatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId");
|
||||
|
||||
b.ToTable("wf_workflow_tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.WorkflowToken", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("ArrivedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("InstanceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("NodeId")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("UpdatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId");
|
||||
|
||||
b.ToTable("wf_workflow_tokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.FormDefinitionField", b =>
|
||||
{
|
||||
b.HasOne("Workflow.Domain.Entities.FormDefinition", null)
|
||||
.WithMany("Fields")
|
||||
.HasForeignKey("FormDefinitionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.WorkflowEdge", b =>
|
||||
{
|
||||
b.HasOne("Workflow.Domain.Entities.WorkflowDefinition", null)
|
||||
.WithMany("Edges")
|
||||
.HasForeignKey("DefinitionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.WorkflowNode", b =>
|
||||
{
|
||||
b.HasOne("Workflow.Domain.Entities.WorkflowDefinition", null)
|
||||
.WithMany("Nodes")
|
||||
.HasForeignKey("DefinitionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.WorkflowTask", b =>
|
||||
{
|
||||
b.HasOne("Workflow.Domain.Entities.WorkflowInstance", null)
|
||||
.WithMany("Tasks")
|
||||
.HasForeignKey("InstanceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.WorkflowToken", b =>
|
||||
{
|
||||
b.HasOne("Workflow.Domain.Entities.WorkflowInstance", null)
|
||||
.WithMany("Tokens")
|
||||
.HasForeignKey("InstanceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.FormDefinition", b =>
|
||||
{
|
||||
b.Navigation("Fields");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.WorkflowDefinition", b =>
|
||||
{
|
||||
b.Navigation("Edges");
|
||||
|
||||
b.Navigation("Nodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Workflow.Domain.Entities.WorkflowInstance", b =>
|
||||
{
|
||||
b.Navigation("Tasks");
|
||||
|
||||
b.Navigation("Tokens");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -16,6 +16,6 @@ public class WorkflowEdgeConfiguration : IEntityTypeConfiguration<WorkflowEdge>
|
||||
builder.Property(e => e.SourceNodeId).HasMaxLength(500);
|
||||
builder.Property(e => e.TargetNodeId).HasMaxLength(500);
|
||||
builder.Property(e => e.Label).HasMaxLength(500);
|
||||
builder.Property(e => e.Condition).HasColumnType("jsonb");
|
||||
builder.Property(e => e.Condition).HasColumnType("text");
|
||||
}
|
||||
}
|
||||
|
||||
346
src/Workflow.Infrastructure/Persistence/SeedData.cs
Normal file
346
src/Workflow.Infrastructure/Persistence/SeedData.cs
Normal file
@ -0,0 +1,346 @@
|
||||
using Workflow.Domain.Entities;
|
||||
using Workflow.Domain.Enums;
|
||||
|
||||
namespace Workflow.Infrastructure.Persistence;
|
||||
|
||||
public static class SeedData
|
||||
{
|
||||
public static async Task SeedAsync(WorkflowDbContext db)
|
||||
{
|
||||
if (db.WorkflowDefinitions.Any()) return;
|
||||
|
||||
var systemUserId = Guid.Parse("00000000-0000-0000-0000-000000000001");
|
||||
|
||||
// ── 请假表单 ──
|
||||
var leaveForm = new FormDefinition
|
||||
{
|
||||
Id = Guid.Parse("A0000000-0000-0000-0000-000000000001"),
|
||||
Name = "请假申请表",
|
||||
Code = "leave-request",
|
||||
Description = "员工请假申请表单",
|
||||
Status = FormStatus.Published,
|
||||
CreatedBy = systemUserId,
|
||||
UpdatedBy = systemUserId,
|
||||
};
|
||||
leaveForm.Fields =
|
||||
[
|
||||
new FormDefinitionField
|
||||
{
|
||||
Id = Guid.Parse("A0000001-0000-0000-0000-000000000001"),
|
||||
FormDefinitionId = leaveForm.Id,
|
||||
FieldKey = "leaveType", Label = "请假类型", FieldType = FieldType.Select,
|
||||
Required = true, SortOrder = 1,
|
||||
Config = """{ "options": ["年假", "事假", "病假", "婚假", "产假"] }""",
|
||||
CreatedBy = systemUserId, UpdatedBy = systemUserId,
|
||||
},
|
||||
new FormDefinitionField
|
||||
{
|
||||
Id = Guid.Parse("A0000001-0000-0000-0000-000000000002"),
|
||||
FormDefinitionId = leaveForm.Id,
|
||||
FieldKey = "startDate", Label = "开始日期", FieldType = FieldType.Date,
|
||||
Required = true, SortOrder = 2,
|
||||
CreatedBy = systemUserId, UpdatedBy = systemUserId,
|
||||
},
|
||||
new FormDefinitionField
|
||||
{
|
||||
Id = Guid.Parse("A0000001-0000-0000-0000-000000000003"),
|
||||
FormDefinitionId = leaveForm.Id,
|
||||
FieldKey = "endDate", Label = "结束日期", FieldType = FieldType.Date,
|
||||
Required = true, SortOrder = 3,
|
||||
CreatedBy = systemUserId, UpdatedBy = systemUserId,
|
||||
},
|
||||
new FormDefinitionField
|
||||
{
|
||||
Id = Guid.Parse("A0000001-0000-0000-0000-000000000004"),
|
||||
FormDefinitionId = leaveForm.Id,
|
||||
FieldKey = "reason", Label = "请假原因", FieldType = FieldType.Textarea,
|
||||
Required = true, SortOrder = 4,
|
||||
CreatedBy = systemUserId, UpdatedBy = systemUserId,
|
||||
},
|
||||
];
|
||||
|
||||
db.FormDefinitions.Add(leaveForm);
|
||||
|
||||
// ── 报销表单 ──
|
||||
var expenseForm = new FormDefinition
|
||||
{
|
||||
Id = Guid.Parse("A0000000-0000-0000-0000-000000000002"),
|
||||
Name = "费用报销单",
|
||||
Code = "expense-claim",
|
||||
Description = "员工费用报销申请表单",
|
||||
Status = FormStatus.Published,
|
||||
CreatedBy = systemUserId,
|
||||
UpdatedBy = systemUserId,
|
||||
};
|
||||
expenseForm.Fields =
|
||||
[
|
||||
new FormDefinitionField
|
||||
{
|
||||
Id = Guid.Parse("A0000002-0000-0000-0000-000000000001"),
|
||||
FormDefinitionId = expenseForm.Id,
|
||||
FieldKey = "category", Label = "报销类别", FieldType = FieldType.Select,
|
||||
Required = true, SortOrder = 1,
|
||||
Config = """{ "options": ["差旅", "办公用品", "交通", "餐饮", "其他"] }""",
|
||||
CreatedBy = systemUserId, UpdatedBy = systemUserId,
|
||||
},
|
||||
new FormDefinitionField
|
||||
{
|
||||
Id = Guid.Parse("A0000002-0000-0000-0000-000000000002"),
|
||||
FormDefinitionId = expenseForm.Id,
|
||||
FieldKey = "amount", Label = "报销金额", FieldType = FieldType.Number,
|
||||
Required = true, SortOrder = 2,
|
||||
CreatedBy = systemUserId, UpdatedBy = systemUserId,
|
||||
},
|
||||
new FormDefinitionField
|
||||
{
|
||||
Id = Guid.Parse("A0000002-0000-0000-0000-000000000003"),
|
||||
FormDefinitionId = expenseForm.Id,
|
||||
FieldKey = "description", Label = "费用说明", FieldType = FieldType.Textarea,
|
||||
Required = true, SortOrder = 3,
|
||||
CreatedBy = systemUserId, UpdatedBy = systemUserId,
|
||||
},
|
||||
new FormDefinitionField
|
||||
{
|
||||
Id = Guid.Parse("A0000002-0000-0000-0000-000000000004"),
|
||||
FormDefinitionId = expenseForm.Id,
|
||||
FieldKey = "receipts", Label = "上传发票", FieldType = FieldType.FileUpload,
|
||||
Required = false, SortOrder = 4,
|
||||
CreatedBy = systemUserId, UpdatedBy = systemUserId,
|
||||
},
|
||||
];
|
||||
|
||||
db.FormDefinitions.Add(expenseForm);
|
||||
|
||||
// ── 简单请假审批流程 ──
|
||||
var leaveNodes = new List<WorkflowNode>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = Guid.Parse("B0000001-0000-0000-0000-000000000001"),
|
||||
DefinitionId = Guid.Parse("B0000000-0000-0000-0000-000000000001"),
|
||||
NodeType = NodeType.Start, Name = "开始",
|
||||
CreatedBy = systemUserId, UpdatedBy = systemUserId,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = Guid.Parse("B0000001-0000-0000-0000-000000000002"),
|
||||
DefinitionId = Guid.Parse("B0000000-0000-0000-0000-000000000001"),
|
||||
NodeType = NodeType.Approval, Name = "直属主管审批",
|
||||
Config = """{ "assigneeRule": "role:manager" }""",
|
||||
CreatedBy = systemUserId, UpdatedBy = systemUserId,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = Guid.Parse("B0000001-0000-0000-0000-000000000003"),
|
||||
DefinitionId = Guid.Parse("B0000000-0000-0000-0000-000000000001"),
|
||||
NodeType = NodeType.Condition, Name = "天数判断",
|
||||
CreatedBy = systemUserId, UpdatedBy = systemUserId,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = Guid.Parse("B0000001-0000-0000-0000-000000000004"),
|
||||
DefinitionId = Guid.Parse("B0000000-0000-0000-0000-000000000001"),
|
||||
NodeType = NodeType.Approval, Name = "HR 审批",
|
||||
Config = """{ "assigneeRule": "role:hr" }""",
|
||||
CreatedBy = systemUserId, UpdatedBy = systemUserId,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = Guid.Parse("B0000001-0000-0000-0000-000000000005"),
|
||||
DefinitionId = Guid.Parse("B0000000-0000-0000-0000-000000000001"),
|
||||
NodeType = NodeType.Cc, Name = "抄送 HR",
|
||||
Config = """{ "recipients": ["role:hr"] }""",
|
||||
CreatedBy = systemUserId, UpdatedBy = systemUserId,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = Guid.Parse("B0000001-0000-0000-0000-000000000006"),
|
||||
DefinitionId = Guid.Parse("B0000000-0000-0000-0000-000000000001"),
|
||||
NodeType = NodeType.End, Name = "结束",
|
||||
CreatedBy = systemUserId, UpdatedBy = systemUserId,
|
||||
},
|
||||
};
|
||||
|
||||
var leaveEdges = new List<WorkflowEdge>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = Guid.Parse("C0000001-0000-0000-0000-000000000001"),
|
||||
DefinitionId = Guid.Parse("B0000000-0000-0000-0000-000000000001"),
|
||||
SourceNodeId = Guid.Parse("B0000001-0000-0000-0000-000000000001"),
|
||||
TargetNodeId = Guid.Parse("B0000001-0000-0000-0000-000000000002"),
|
||||
CreatedBy = systemUserId, UpdatedBy = systemUserId,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = Guid.Parse("C0000001-0000-0000-0000-000000000002"),
|
||||
DefinitionId = Guid.Parse("B0000000-0000-0000-0000-000000000001"),
|
||||
SourceNodeId = Guid.Parse("B0000001-0000-0000-0000-000000000002"),
|
||||
TargetNodeId = Guid.Parse("B0000001-0000-0000-0000-000000000003"),
|
||||
EdgeType = EdgeType.Approved,
|
||||
CreatedBy = systemUserId, UpdatedBy = systemUserId,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = Guid.Parse("C0000001-0000-0000-0000-000000000003"),
|
||||
DefinitionId = Guid.Parse("B0000000-0000-0000-0000-000000000001"),
|
||||
SourceNodeId = Guid.Parse("B0000001-0000-0000-0000-000000000003"),
|
||||
TargetNodeId = Guid.Parse("B0000001-0000-0000-0000-000000000004"),
|
||||
Condition = "days > 3",
|
||||
Order = 1,
|
||||
CreatedBy = systemUserId, UpdatedBy = systemUserId,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = Guid.Parse("C0000001-0000-0000-0000-000000000004"),
|
||||
DefinitionId = Guid.Parse("B0000000-0000-0000-0000-000000000001"),
|
||||
SourceNodeId = Guid.Parse("B0000001-0000-0000-0000-000000000003"),
|
||||
TargetNodeId = Guid.Parse("B0000001-0000-0000-0000-000000000005"),
|
||||
Condition = "days <= 3",
|
||||
Order = 2,
|
||||
CreatedBy = systemUserId, UpdatedBy = systemUserId,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = Guid.Parse("C0000001-0000-0000-0000-000000000005"),
|
||||
DefinitionId = Guid.Parse("B0000000-0000-0000-0000-000000000001"),
|
||||
SourceNodeId = Guid.Parse("B0000001-0000-0000-0000-000000000004"),
|
||||
TargetNodeId = Guid.Parse("B0000001-0000-0000-0000-000000000005"),
|
||||
EdgeType = EdgeType.Approved,
|
||||
CreatedBy = systemUserId, UpdatedBy = systemUserId,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = Guid.Parse("C0000001-0000-0000-0000-000000000006"),
|
||||
DefinitionId = Guid.Parse("B0000000-0000-0000-0000-000000000001"),
|
||||
SourceNodeId = Guid.Parse("B0000001-0000-0000-0000-000000000005"),
|
||||
TargetNodeId = Guid.Parse("B0000001-0000-0000-0000-000000000006"),
|
||||
CreatedBy = systemUserId, UpdatedBy = systemUserId,
|
||||
},
|
||||
};
|
||||
|
||||
var leaveDefinition = new WorkflowDefinition
|
||||
{
|
||||
Id = Guid.Parse("B0000000-0000-0000-0000-000000000001"),
|
||||
Name = "请假审批流程",
|
||||
Code = "leave-approval",
|
||||
Description = "员工请假审批:3天以内主管审批,3天以上需 HR 审批",
|
||||
Status = DefinitionStatus.Published,
|
||||
Nodes = leaveNodes,
|
||||
Edges = leaveEdges,
|
||||
CreatedBy = systemUserId,
|
||||
UpdatedBy = systemUserId,
|
||||
};
|
||||
|
||||
db.WorkflowDefinitions.Add(leaveDefinition);
|
||||
|
||||
// ── 报销审批流程 ──
|
||||
var expenseNodes = new List<WorkflowNode>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = Guid.Parse("B0000002-0000-0000-0000-000000000001"),
|
||||
DefinitionId = Guid.Parse("B0000000-0000-0000-0000-000000000002"),
|
||||
NodeType = NodeType.Start, Name = "开始",
|
||||
CreatedBy = systemUserId, UpdatedBy = systemUserId,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = Guid.Parse("B0000002-0000-0000-0000-000000000002"),
|
||||
DefinitionId = Guid.Parse("B0000000-0000-0000-0000-000000000002"),
|
||||
NodeType = NodeType.Approval, Name = "部门主管审批",
|
||||
Config = """{ "assigneeRule": "role:department-head" }""",
|
||||
CreatedBy = systemUserId, UpdatedBy = systemUserId,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = Guid.Parse("B0000002-0000-0000-0000-000000000003"),
|
||||
DefinitionId = Guid.Parse("B0000000-0000-0000-0000-000000000002"),
|
||||
NodeType = NodeType.Condition, Name = "金额判断",
|
||||
CreatedBy = systemUserId, UpdatedBy = systemUserId,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = Guid.Parse("B0000002-0000-0000-0000-000000000004"),
|
||||
DefinitionId = Guid.Parse("B0000000-0000-0000-0000-000000000002"),
|
||||
NodeType = NodeType.Approval, Name = "财务总监审批",
|
||||
Config = """{ "assigneeRule": "role:cfo" }""",
|
||||
CreatedBy = systemUserId, UpdatedBy = systemUserId,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = Guid.Parse("B0000002-0000-0000-0000-000000000005"),
|
||||
DefinitionId = Guid.Parse("B0000000-0000-0000-0000-000000000002"),
|
||||
NodeType = NodeType.End, Name = "结束",
|
||||
CreatedBy = systemUserId, UpdatedBy = systemUserId,
|
||||
},
|
||||
};
|
||||
|
||||
var expenseEdges = new List<WorkflowEdge>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = Guid.Parse("C0000002-0000-0000-0000-000000000001"),
|
||||
DefinitionId = Guid.Parse("B0000000-0000-0000-0000-000000000002"),
|
||||
SourceNodeId = Guid.Parse("B0000002-0000-0000-0000-000000000001"),
|
||||
TargetNodeId = Guid.Parse("B0000002-0000-0000-0000-000000000002"),
|
||||
CreatedBy = systemUserId, UpdatedBy = systemUserId,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = Guid.Parse("C0000002-0000-0000-0000-000000000002"),
|
||||
DefinitionId = Guid.Parse("B0000000-0000-0000-0000-000000000002"),
|
||||
SourceNodeId = Guid.Parse("B0000002-0000-0000-0000-000000000002"),
|
||||
TargetNodeId = Guid.Parse("B0000002-0000-0000-0000-000000000003"),
|
||||
EdgeType = EdgeType.Approved,
|
||||
CreatedBy = systemUserId, UpdatedBy = systemUserId,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = Guid.Parse("C0000002-0000-0000-0000-000000000003"),
|
||||
DefinitionId = Guid.Parse("B0000000-0000-0000-0000-000000000002"),
|
||||
SourceNodeId = Guid.Parse("B0000002-0000-0000-0000-000000000003"),
|
||||
TargetNodeId = Guid.Parse("B0000002-0000-0000-0000-000000000004"),
|
||||
Condition = "amount > 5000",
|
||||
Order = 1,
|
||||
CreatedBy = systemUserId, UpdatedBy = systemUserId,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = Guid.Parse("C0000002-0000-0000-0000-000000000004"),
|
||||
DefinitionId = Guid.Parse("B0000000-0000-0000-0000-000000000002"),
|
||||
SourceNodeId = Guid.Parse("B0000002-0000-0000-0000-000000000003"),
|
||||
TargetNodeId = Guid.Parse("B0000002-0000-0000-0000-000000000005"),
|
||||
Condition = "amount <= 5000",
|
||||
Order = 2,
|
||||
CreatedBy = systemUserId, UpdatedBy = systemUserId,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = Guid.Parse("C0000002-0000-0000-0000-000000000005"),
|
||||
DefinitionId = Guid.Parse("B0000000-0000-0000-0000-000000000002"),
|
||||
SourceNodeId = Guid.Parse("B0000002-0000-0000-0000-000000000004"),
|
||||
TargetNodeId = Guid.Parse("B0000002-0000-0000-0000-000000000005"),
|
||||
EdgeType = EdgeType.Approved,
|
||||
CreatedBy = systemUserId, UpdatedBy = systemUserId,
|
||||
},
|
||||
};
|
||||
|
||||
var expenseDefinition = new WorkflowDefinition
|
||||
{
|
||||
Id = Guid.Parse("B0000000-0000-0000-0000-000000000002"),
|
||||
Name = "费用报销流程",
|
||||
Code = "expense-approval",
|
||||
Description = "费用报销审批:5000 元以下主管审批,5000 元以上需财务总监审批",
|
||||
Status = DefinitionStatus.Published,
|
||||
Nodes = expenseNodes,
|
||||
Edges = expenseEdges,
|
||||
CreatedBy = systemUserId,
|
||||
UpdatedBy = systemUserId,
|
||||
};
|
||||
|
||||
db.WorkflowDefinitions.Add(expenseDefinition);
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
@ -2,11 +2,14 @@ using System.Linq.Expressions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Workflow.Domain.Common;
|
||||
using Workflow.Domain.Entities;
|
||||
using Workflow.Infrastructure.Persistence.Interceptors;
|
||||
|
||||
namespace Workflow.Infrastructure.Persistence;
|
||||
|
||||
public class WorkflowDbContext : DbContext
|
||||
{
|
||||
private readonly ICurrentUserContext? _currentUserContext;
|
||||
|
||||
public DbSet<WorkflowDefinition> WorkflowDefinitions => Set<WorkflowDefinition>();
|
||||
public DbSet<WorkflowNode> WorkflowNodes => Set<WorkflowNode>();
|
||||
public DbSet<WorkflowEdge> WorkflowEdges => Set<WorkflowEdge>();
|
||||
@ -19,6 +22,19 @@ public class WorkflowDbContext : DbContext
|
||||
|
||||
public WorkflowDbContext(DbContextOptions<WorkflowDbContext> options) : base(options) { }
|
||||
|
||||
public WorkflowDbContext(DbContextOptions<WorkflowDbContext> options, ICurrentUserContext currentUserContext) : base(options)
|
||||
{
|
||||
_currentUserContext = currentUserContext;
|
||||
}
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
if (_currentUserContext is not null)
|
||||
{
|
||||
optionsBuilder.AddInterceptors(new AuditInterceptor(_currentUserContext));
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(WorkflowDbContext).Assembly);
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Workflow.Infrastructure.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// EF Core 设计时工厂,供 dotnet ef migrations 命令使用。
|
||||
/// 从 Api 项目的 appsettings.json 读取连接字符串。
|
||||
/// </summary>
|
||||
public class WorkflowDbContextDesignTimeFactory : IDesignTimeDbContextFactory<WorkflowDbContext>
|
||||
{
|
||||
public WorkflowDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
var apiProjectPath = Path.Combine(Directory.GetCurrentDirectory(), "../Workflow.Api");
|
||||
|
||||
var config = new ConfigurationBuilder()
|
||||
.SetBasePath(apiProjectPath)
|
||||
.AddJsonFile("appsettings.json", optional: false)
|
||||
.Build();
|
||||
|
||||
var connectionString = config.GetConnectionString("Default");
|
||||
|
||||
var options = new DbContextOptionsBuilder<WorkflowDbContext>()
|
||||
.UseNpgsql(connectionString)
|
||||
.Options;
|
||||
|
||||
return new WorkflowDbContext(options);
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
70
tests/Workflow.Tests/Condition/BooleanComparatorTests.cs
Normal file
70
tests/Workflow.Tests/Condition/BooleanComparatorTests.cs
Normal file
@ -0,0 +1,70 @@
|
||||
namespace Workflow.Tests.Condition;
|
||||
|
||||
using FluentAssertions;
|
||||
using Workflow.Domain.Expressions.Comparators;
|
||||
using Xunit;
|
||||
|
||||
public class BooleanComparatorTests
|
||||
{
|
||||
private readonly BooleanComparator _sut = new();
|
||||
|
||||
[Fact]
|
||||
public void Compare_Equals_TrueTrue_ReturnsTrue()
|
||||
{
|
||||
_sut.Compare(true, "==", true).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_Equals_TrueFalse_ReturnsFalse()
|
||||
{
|
||||
_sut.Compare(true, "==", false).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_Equals_StringBoolean_ReturnsTrue()
|
||||
{
|
||||
_sut.Compare("true", "==", "true").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_Equals_StringBooleanCaseInsensitive()
|
||||
{
|
||||
_sut.Compare("True", "==", "true").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_NotEquals_TrueFalse_ReturnsTrue()
|
||||
{
|
||||
_sut.Compare(true, "!=", false).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_BoolAndStringBool_ReturnsTrue()
|
||||
{
|
||||
_sut.Compare(true, "==", "true").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_NonBooleanField_ReturnsFalse()
|
||||
{
|
||||
_sut.Compare("hello", "==", "true").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_NullField_ReturnsFalse()
|
||||
{
|
||||
_sut.Compare(null, "==", "true").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_UnsupportedOperator_ReturnsFalse()
|
||||
{
|
||||
_sut.Compare(true, ">", false).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_FalseEqualsFalse_ReturnsTrue()
|
||||
{
|
||||
_sut.Compare(false, "==", false).Should().BeTrue();
|
||||
}
|
||||
}
|
||||
77
tests/Workflow.Tests/Condition/CollectionComparatorTests.cs
Normal file
77
tests/Workflow.Tests/Condition/CollectionComparatorTests.cs
Normal file
@ -0,0 +1,77 @@
|
||||
namespace Workflow.Tests.Condition;
|
||||
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Workflow.Domain.Expressions.Comparators;
|
||||
using Xunit;
|
||||
|
||||
public class CollectionComparatorTests
|
||||
{
|
||||
private readonly CollectionComparator _sut = new();
|
||||
|
||||
[Fact]
|
||||
public void Compare_In_ValueInArray_ReturnsTrue()
|
||||
{
|
||||
var array = JsonDocument.Parse("""["pending", "review", "approved"]""").RootElement;
|
||||
_sut.Compare("review", "in", array).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_In_ValueNotInArray_ReturnsFalse()
|
||||
{
|
||||
var array = JsonDocument.Parse("""["pending", "review"]""").RootElement;
|
||||
_sut.Compare("rejected", "in", array).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_In_NumericValue_ReturnsTrue()
|
||||
{
|
||||
var array = JsonDocument.Parse("[1, 3, 5, 7]").RootElement;
|
||||
_sut.Compare(5, "in", array).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_In_EmptyArray_ReturnsFalse()
|
||||
{
|
||||
var array = JsonDocument.Parse("[]").RootElement;
|
||||
_sut.Compare("value", "in", array).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_In_CommaSeparatedString_ReturnsTrue()
|
||||
{
|
||||
_sut.Compare("review", "in", "pending,review,approved").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_In_CommaSeparatedString_ReturnsFalse()
|
||||
{
|
||||
_sut.Compare("rejected", "in", "pending,review").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_NullField_ReturnsFalse()
|
||||
{
|
||||
var array = JsonDocument.Parse("""["value"]""").RootElement;
|
||||
_sut.Compare(null, "in", array).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_NullCondition_ReturnsFalse()
|
||||
{
|
||||
_sut.Compare("value", "in", null).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_UnsupportedOperator_ReturnsFalse()
|
||||
{
|
||||
_sut.Compare("value", "==", "something").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_In_NumericValueNotInArray_ReturnsFalse()
|
||||
{
|
||||
var array = JsonDocument.Parse("[1, 3, 5, 7]").RootElement;
|
||||
_sut.Compare(4, "in", array).Should().BeFalse();
|
||||
}
|
||||
}
|
||||
101
tests/Workflow.Tests/Condition/DateTimeComparatorTests.cs
Normal file
101
tests/Workflow.Tests/Condition/DateTimeComparatorTests.cs
Normal file
@ -0,0 +1,101 @@
|
||||
namespace Workflow.Tests.Condition;
|
||||
|
||||
using FluentAssertions;
|
||||
using Workflow.Domain.Expressions.Comparators;
|
||||
using Xunit;
|
||||
|
||||
public class DateTimeComparatorTests
|
||||
{
|
||||
private readonly DateTimeComparator _sut = new();
|
||||
|
||||
[Fact]
|
||||
public void Compare_Equals_SameDate_ReturnsTrue()
|
||||
{
|
||||
var date = new DateTime(2024, 1, 15);
|
||||
_sut.Compare(date, "==", "2024-01-15").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_Equals_DifferentDate_ReturnsFalse()
|
||||
{
|
||||
var date = new DateTime(2024, 1, 15);
|
||||
_sut.Compare(date, "==", "2024-01-16").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_NotEquals_ReturnsTrue()
|
||||
{
|
||||
var date = new DateTime(2024, 1, 15);
|
||||
_sut.Compare(date, "!=", "2024-01-16").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_GreaterThan_ReturnsTrue()
|
||||
{
|
||||
var date = new DateTime(2024, 6, 1);
|
||||
_sut.Compare(date, ">", "2024-01-01").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_LessThan_ReturnsTrue()
|
||||
{
|
||||
var date = new DateTime(2024, 1, 1);
|
||||
_sut.Compare(date, "<", "2024-06-01").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_GreaterThanOrEqual_Equal()
|
||||
{
|
||||
var date = new DateTime(2024, 1, 15);
|
||||
_sut.Compare(date, ">=", "2024-01-15").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_LessThanOrEqual_Equal()
|
||||
{
|
||||
var date = new DateTime(2024, 1, 15);
|
||||
_sut.Compare(date, "<=", "2024-01-15").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_StringDate_ISO8601()
|
||||
{
|
||||
_sut.Compare("2024-01-15", "==", "2024-01-15").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_StringDate_WithTime()
|
||||
{
|
||||
_sut.Compare("2024-01-15T10:30:00", "==", "2024-01-15T10:30:00").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_InvalidDate_ReturnsFalse()
|
||||
{
|
||||
_sut.Compare("not-a-date", "==", "2024-01-15").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_NumericField_ReturnsFalse()
|
||||
{
|
||||
_sut.Compare(100, "==", "2024-01-15").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_NullField_ReturnsFalse()
|
||||
{
|
||||
_sut.Compare(null, "==", "2024-01-15").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_UnsupportedOperator_ReturnsFalse()
|
||||
{
|
||||
_sut.Compare(new DateTime(2024, 1, 15), "contains", "2024").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_GreaterThan_DateTimeLater()
|
||||
{
|
||||
_sut.Compare("2024-12-31", ">", "2024-01-01").Should().BeTrue();
|
||||
}
|
||||
}
|
||||
107
tests/Workflow.Tests/Condition/NumericComparatorTests.cs
Normal file
107
tests/Workflow.Tests/Condition/NumericComparatorTests.cs
Normal file
@ -0,0 +1,107 @@
|
||||
namespace Workflow.Tests.Condition;
|
||||
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Workflow.Domain.Expressions.Comparators;
|
||||
using Xunit;
|
||||
|
||||
public class NumericComparatorTests
|
||||
{
|
||||
private readonly NumericComparator _sut = new();
|
||||
|
||||
[Fact]
|
||||
public void Compare_Equals_DecimalValues_ReturnsTrue()
|
||||
{
|
||||
_sut.Compare(10.0m, "==", 10).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_Equals_IntAndDouble_ReturnsTrue()
|
||||
{
|
||||
_sut.Compare(10, "==", 10.0).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_Equals_StringNumeric_ReturnsTrue()
|
||||
{
|
||||
_sut.Compare("100", "==", 100).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_Equals_DifferentValues_ReturnsFalse()
|
||||
{
|
||||
_sut.Compare(5, "==", 10).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_NotEquals_ReturnsTrue()
|
||||
{
|
||||
_sut.Compare(5, "!=", 10).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_NotEquals_SameValues_ReturnsFalse()
|
||||
{
|
||||
_sut.Compare(5, "!=", 5).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_GreaterThan_ReturnsTrue()
|
||||
{
|
||||
_sut.Compare(10, ">", 5).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_GreaterThan_False()
|
||||
{
|
||||
_sut.Compare(5, ">", 10).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_LessThan_ReturnsTrue()
|
||||
{
|
||||
_sut.Compare(5, "<", 10).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_GreaterThanOrEqual_Equal()
|
||||
{
|
||||
_sut.Compare(10, ">=", 10).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_LessThanOrEqual_Equal()
|
||||
{
|
||||
_sut.Compare(10, "<=", 10).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_NonNumericField_ReturnsFalse()
|
||||
{
|
||||
_sut.Compare("hello", ">", 5).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_NonNumericCondition_ReturnsFalse()
|
||||
{
|
||||
_sut.Compare(10, ">", "abc").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_NullField_ReturnsFalse()
|
||||
{
|
||||
_sut.Compare(null, "==", 5).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_UnsupportedOperator_ReturnsFalse()
|
||||
{
|
||||
_sut.Compare(10, "contains", 5).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_StringConditionValue_ParsesCorrectly()
|
||||
{
|
||||
_sut.Compare(250, ">", "100").Should().BeTrue();
|
||||
}
|
||||
}
|
||||
119
tests/Workflow.Tests/Condition/StringComparatorTests.cs
Normal file
119
tests/Workflow.Tests/Condition/StringComparatorTests.cs
Normal file
@ -0,0 +1,119 @@
|
||||
namespace Workflow.Tests.Condition;
|
||||
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Workflow.Domain.Expressions.Comparators;
|
||||
using Xunit;
|
||||
|
||||
public class StringComparatorTests
|
||||
{
|
||||
private readonly StringComparator _sut = new();
|
||||
|
||||
[Fact]
|
||||
public void Compare_Equals_SameString_ReturnsTrue()
|
||||
{
|
||||
_sut.Compare("hello", "==", "hello").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_Equals_DifferentString_ReturnsFalse()
|
||||
{
|
||||
_sut.Compare("hello", "==", "world").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_NotEquals_DifferentString_ReturnsTrue()
|
||||
{
|
||||
_sut.Compare("hello", "!=", "world").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_Contains_True()
|
||||
{
|
||||
_sut.Compare("hello world", "contains", "world").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_Contains_False()
|
||||
{
|
||||
_sut.Compare("hello world", "contains", "xyz").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_Contains_CaseSensitive()
|
||||
{
|
||||
_sut.Compare("Hello", "contains", "hello").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_StartsWith_True()
|
||||
{
|
||||
_sut.Compare("hello world", "startsWith", "hello").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_StartsWith_False()
|
||||
{
|
||||
_sut.Compare("hello world", "startsWith", "world").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_EndsWith_True()
|
||||
{
|
||||
_sut.Compare("hello world", "endsWith", "world").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_EndsWith_False()
|
||||
{
|
||||
_sut.Compare("hello world", "endsWith", "hello").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_IsEmpty_EmptyString_ReturnsTrue()
|
||||
{
|
||||
_sut.Compare("", "isEmpty", null).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_IsEmpty_NullField_ReturnsTrue()
|
||||
{
|
||||
_sut.Compare(null, "isEmpty", null).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_IsEmpty_Whitespace_ReturnsTrue()
|
||||
{
|
||||
_sut.Compare(" ", "isEmpty", null).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_IsEmpty_NonEmpty_ReturnsFalse()
|
||||
{
|
||||
_sut.Compare("hello", "isEmpty", null).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_UnsupportedOperator_ReturnsFalse()
|
||||
{
|
||||
_sut.Compare("hello", ">", "world").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_NullField_Equals_ReturnsFalse()
|
||||
{
|
||||
_sut.Compare(null, "==", "hello").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_Equals_CaseSensitive()
|
||||
{
|
||||
_sut.Compare("Hello", "==", "hello").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_SupportedOperators_ContainsExpected()
|
||||
{
|
||||
_sut.SupportedOperators.Should().Contain(["==", "!=", "contains", "startsWith", "endsWith", "isEmpty"]);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,94 @@
|
||||
namespace Workflow.Tests.Condition;
|
||||
|
||||
using FluentAssertions;
|
||||
using Workflow.Application.Expressions;
|
||||
using Workflow.Domain.Expressions.Comparators;
|
||||
using Xunit;
|
||||
|
||||
public class ValueComparatorRegistryTests
|
||||
{
|
||||
private readonly ValueComparatorRegistry _registry;
|
||||
|
||||
public ValueComparatorRegistryTests()
|
||||
{
|
||||
IValueComparator[] comparators =
|
||||
[
|
||||
new NumericComparator(),
|
||||
new DateTimeComparator(),
|
||||
new BooleanComparator(),
|
||||
new CollectionComparator(),
|
||||
new StringComparator(),
|
||||
];
|
||||
_registry = new ValueComparatorRegistry(comparators);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisteredOperators_ContainsAllExpected()
|
||||
{
|
||||
_registry.RegisteredOperators.Should().Contain(
|
||||
"==", "!=", ">", "<", ">=", "<=",
|
||||
"contains", "startsWith", "endsWith", "isEmpty", "in");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryCompare_NumericEquals_ReturnsTrue()
|
||||
{
|
||||
_registry.TryCompare(10, "==", 10).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryCompare_StringEquals_ReturnsTrue()
|
||||
{
|
||||
_registry.TryCompare("hello", "==", "hello").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryCompare_NumericGreaterThan_ReturnsTrue()
|
||||
{
|
||||
_registry.TryCompare(10, ">", 5).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryCompare_StringContains_ReturnsTrue()
|
||||
{
|
||||
_registry.TryCompare("hello world", "contains", "world").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryCompare_UnregisteredOperator_ReturnsFalse()
|
||||
{
|
||||
_registry.TryCompare(10, "unknown_op", 5).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryCompare_NumericFallsThroughToString()
|
||||
{
|
||||
// "hello" 不是数值,NumericComparator 返回 false,StringComparator 兜底
|
||||
_registry.TryCompare("hello", "==", "hello").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryCompare_BoolEquals_ReturnsTrue()
|
||||
{
|
||||
_registry.TryCompare(true, "==", "true").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_EmptyComparators_NoOperators()
|
||||
{
|
||||
var registry = new ValueComparatorRegistry([]);
|
||||
registry.RegisteredOperators.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryCompare_DateTimeGreaterThan_ReturnsTrue()
|
||||
{
|
||||
_registry.TryCompare("2024-06-01", ">", "2024-01-01").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryCompare_NumericNotEquals_ReturnsTrue()
|
||||
{
|
||||
_registry.TryCompare(5, "!=", 10).Should().BeTrue();
|
||||
}
|
||||
}
|
||||
@ -5,9 +5,11 @@ using Moq;
|
||||
using Workflow.Application.Engine;
|
||||
using Workflow.Domain.Entities;
|
||||
using Workflow.Domain.Enums;
|
||||
using Workflow.Domain.Expressions;
|
||||
using Workflow.Domain.Exceptions;
|
||||
using Workflow.Infrastructure.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Xunit;
|
||||
|
||||
public class ProcessEngineTests
|
||||
{
|
||||
@ -23,7 +25,7 @@ public class ProcessEngineTests
|
||||
|
||||
_dbContext = new WorkflowDbContext(options);
|
||||
_serviceProvider = new Mock<IServiceProvider>();
|
||||
_engine = new ProcessEngine(_dbContext, _serviceProvider.Object);
|
||||
_engine = new ProcessEngine(_dbContext, _serviceProvider.Object, new ConditionEvaluator());
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@ -33,31 +35,29 @@ public class ProcessEngineTests
|
||||
[Fact]
|
||||
public async Task ProcessStartNode_CreatesSingleToken_AndPropagatesToNextNode()
|
||||
{
|
||||
var startNode = CreateNode(NodeType.Start, "start-1");
|
||||
var nextNode = CreateNode(NodeType.Approval, "approval-1");
|
||||
var startNode = CreateNode(NodeType.Start);
|
||||
var nextNode = CreateNode(NodeType.Approval);
|
||||
var edge = CreateEdge(startNode, nextNode);
|
||||
|
||||
var definition = CreateDefinition(startNode, nextNode, edge);
|
||||
var instance = CreateInstance(definition);
|
||||
var (_, instance) = PersistSetup([startNode, nextNode], [edge]);
|
||||
|
||||
await _engine.StartAsync(instance);
|
||||
|
||||
var tokens = await _dbContext.WorkflowTokens
|
||||
.Where(t => t.InstanceId == instance.Id)
|
||||
.Where(t => t.InstanceId == instance.Id && t.Status == TokenStatus.Active)
|
||||
.ToListAsync();
|
||||
|
||||
tokens.Should().HaveCount(1);
|
||||
tokens[0].NodeId.Should().Be(nextNode.Id);
|
||||
tokens[0].Status.Should().Be(TokenStatus.Active);
|
||||
instance.Status.Should().Be(InstanceStatus.Running);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessStartNode_ThrowsWhenNoOutgoingEdge()
|
||||
{
|
||||
var startNode = CreateNode(NodeType.Start, "start-1");
|
||||
var definition = CreateDefinition(startNode);
|
||||
var instance = CreateInstance(definition);
|
||||
var startNode = CreateNode(NodeType.Start);
|
||||
|
||||
var (_, instance) = PersistSetup([startNode], []);
|
||||
|
||||
var act = () => _engine.StartAsync(instance);
|
||||
|
||||
@ -72,12 +72,12 @@ public class ProcessEngineTests
|
||||
[Fact]
|
||||
public async Task ProcessApprovalNode_CreatesTaskForAssignee()
|
||||
{
|
||||
var approvalNode = CreateNode(NodeType.Approval, "approval-1");
|
||||
approvalNode.Config = """{ "assigneeRule": "user:user-001", "formId": "form-001" }""";
|
||||
var userId = Guid.NewGuid();
|
||||
var approvalNode = CreateNode(NodeType.Approval);
|
||||
approvalNode.Config = $"{{ \"assigneeRule\": \"user:{userId}\" }}";
|
||||
|
||||
var definition = CreateDefinition(approvalNode);
|
||||
var instance = CreateInstance(definition);
|
||||
var token = CreateToken(instance, approvalNode);
|
||||
var (_, instance) = PersistSetup([approvalNode], []);
|
||||
var token = PersistToken(instance, approvalNode);
|
||||
|
||||
await _engine.ProcessNodeAsync(instance, token, approvalNode);
|
||||
|
||||
@ -86,20 +86,19 @@ public class ProcessEngineTests
|
||||
.ToListAsync();
|
||||
|
||||
tasks.Should().HaveCount(1);
|
||||
tasks[0].AssigneeId.Should().Be("user-001");
|
||||
tasks[0].AssigneeId.Should().Be(userId);
|
||||
tasks[0].Type.Should().Be(TaskType.Approval);
|
||||
tasks[0].Status.Should().Be(Enums.TaskStatus.Pending);
|
||||
tasks[0].Status.Should().Be(TaskStatus.Pending);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessApprovalNode_CreatesTaskForRole()
|
||||
{
|
||||
var approvalNode = CreateNode(NodeType.Approval, "approval-2");
|
||||
approvalNode.Config = """{ "assigneeRule": "role:manager", "formId": "form-001" }""";
|
||||
var approvalNode = CreateNode(NodeType.Approval);
|
||||
approvalNode.Config = """{ "assigneeRule": "role:manager" }""";
|
||||
|
||||
var definition = CreateDefinition(approvalNode);
|
||||
var instance = CreateInstance(definition);
|
||||
var token = CreateToken(instance, approvalNode);
|
||||
var (_, instance) = PersistSetup([approvalNode], []);
|
||||
var token = PersistToken(instance, approvalNode);
|
||||
|
||||
await _engine.ProcessNodeAsync(instance, token, approvalNode);
|
||||
|
||||
@ -110,34 +109,21 @@ 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]
|
||||
public async Task CompleteTask_Approved_PropagatesTokenAlongApprovedEdge()
|
||||
{
|
||||
var approvalNode = CreateNode(NodeType.Approval, "approval-1");
|
||||
var nextNode = CreateNode(NodeType.End, "end-1");
|
||||
var approvalNode = CreateNode(NodeType.Approval);
|
||||
var nextNode = CreateNode(NodeType.Approval);
|
||||
var approvedEdge = CreateEdge(approvalNode, nextNode, EdgeType.Approved);
|
||||
|
||||
approvalNode.Config = """{ "assigneeRule": "user:user-001" }""";
|
||||
approvalNode.Config = """{ "assigneeRule": "role:user" }""";
|
||||
|
||||
var definition = CreateDefinition(approvalNode, nextNode, approvedEdge);
|
||||
var instance = CreateInstance(definition);
|
||||
var token = CreateToken(instance, approvalNode);
|
||||
|
||||
var task = new WorkflowTask
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
InstanceId = instance.Id,
|
||||
TokenId = token.Id,
|
||||
NodeId = approvalNode.Id.ToString(),
|
||||
AssigneeId = "user-001",
|
||||
Type = TaskType.Approval,
|
||||
Status = Enums.TaskStatus.Pending,
|
||||
};
|
||||
_dbContext.WorkflowTasks.Add(task);
|
||||
await _dbContext.SaveChangesAsync();
|
||||
var (_, instance) = PersistSetup([approvalNode, nextNode], [approvedEdge]);
|
||||
var token = PersistToken(instance, approvalNode);
|
||||
var task = PersistTask(instance, token, approvalNode);
|
||||
|
||||
await _engine.CompleteTaskAsync(task, TaskResult.Approved);
|
||||
|
||||
@ -145,35 +131,21 @@ public class ProcessEngineTests
|
||||
.Where(t => t.InstanceId == instance.Id && t.Status == TokenStatus.Active)
|
||||
.ToListAsync();
|
||||
|
||||
tokens.Should().HaveCount(1);
|
||||
tokens[0].NodeId.Should().Be(nextNode.Id);
|
||||
tokens.Should().ContainSingle(t => t.NodeId == nextNode.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompleteTask_Rejected_PropagatesTokenAlongRejectedEdge()
|
||||
{
|
||||
var approvalNode = CreateNode(NodeType.Approval, "approval-1");
|
||||
var rejectNode = CreateNode(NodeType.End, "reject-end-1");
|
||||
var approvalNode = CreateNode(NodeType.Approval);
|
||||
var rejectNode = CreateNode(NodeType.Approval);
|
||||
var rejectedEdge = CreateEdge(approvalNode, rejectNode, EdgeType.Rejected);
|
||||
|
||||
approvalNode.Config = """{ "assigneeRule": "user:user-001" }""";
|
||||
approvalNode.Config = """{ "assigneeRule": "role:user" }""";
|
||||
|
||||
var definition = CreateDefinition(approvalNode, rejectNode, rejectedEdge);
|
||||
var instance = CreateInstance(definition);
|
||||
var token = CreateToken(instance, approvalNode);
|
||||
|
||||
var task = new WorkflowTask
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
InstanceId = instance.Id,
|
||||
TokenId = token.Id,
|
||||
NodeId = approvalNode.Id.ToString(),
|
||||
AssigneeId = "user-001",
|
||||
Type = TaskType.Approval,
|
||||
Status = Enums.TaskStatus.Pending,
|
||||
};
|
||||
_dbContext.WorkflowTasks.Add(task);
|
||||
await _dbContext.SaveChangesAsync();
|
||||
var (_, instance) = PersistSetup([approvalNode, rejectNode], [rejectedEdge]);
|
||||
var token = PersistToken(instance, approvalNode);
|
||||
var task = PersistTask(instance, token, approvalNode);
|
||||
|
||||
await _engine.CompleteTaskAsync(task, TaskResult.Rejected);
|
||||
|
||||
@ -181,35 +153,21 @@ public class ProcessEngineTests
|
||||
.Where(t => t.InstanceId == instance.Id && t.Status == TokenStatus.Active)
|
||||
.ToListAsync();
|
||||
|
||||
tokens.Should().HaveCount(1);
|
||||
tokens[0].NodeId.Should().Be(rejectNode.Id);
|
||||
tokens.Should().ContainSingle(t => t.NodeId == rejectNode.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompleteTask_Rejected_WithNoRejectEdge_ThrowsBusinessException()
|
||||
{
|
||||
var approvalNode = CreateNode(NodeType.Approval, "approval-1");
|
||||
var nextNode = CreateNode(NodeType.End, "end-1");
|
||||
var approvalNode = CreateNode(NodeType.Approval);
|
||||
var nextNode = CreateNode(NodeType.Approval);
|
||||
var approvedEdge = CreateEdge(approvalNode, nextNode, EdgeType.Approved);
|
||||
|
||||
approvalNode.Config = """{ "assigneeRule": "user:user-001" }""";
|
||||
approvalNode.Config = """{ "assigneeRule": "role:user" }""";
|
||||
|
||||
var definition = CreateDefinition(approvalNode, nextNode, approvedEdge);
|
||||
var instance = CreateInstance(definition);
|
||||
var token = CreateToken(instance, approvalNode);
|
||||
|
||||
var task = new WorkflowTask
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
InstanceId = instance.Id,
|
||||
TokenId = token.Id,
|
||||
NodeId = approvalNode.Id.ToString(),
|
||||
AssigneeId = "user-001",
|
||||
Type = TaskType.Approval,
|
||||
Status = Enums.TaskStatus.Pending,
|
||||
};
|
||||
_dbContext.WorkflowTasks.Add(task);
|
||||
await _dbContext.SaveChangesAsync();
|
||||
var (_, instance) = PersistSetup([approvalNode, nextNode], [approvedEdge]);
|
||||
var token = PersistToken(instance, approvalNode);
|
||||
var task = PersistTask(instance, token, approvalNode);
|
||||
|
||||
var act = () => _engine.CompleteTaskAsync(task, TaskResult.Rejected);
|
||||
|
||||
@ -224,40 +182,41 @@ public class ProcessEngineTests
|
||||
[Fact]
|
||||
public async Task ProcessCcNode_CreatesCcTasks_ForAllRecipients()
|
||||
{
|
||||
var ccNode = CreateNode(NodeType.Cc, "cc-1");
|
||||
var nextNode = CreateNode(NodeType.End, "end-1");
|
||||
var user1 = Guid.NewGuid();
|
||||
var user2 = Guid.NewGuid();
|
||||
var user3 = Guid.NewGuid();
|
||||
var ccNode = CreateNode(NodeType.Cc);
|
||||
var nextNode = CreateNode(NodeType.Approval);
|
||||
var edge = CreateEdge(ccNode, nextNode);
|
||||
|
||||
ccNode.Config = """{ "recipients": ["user-001", "user-002", "user-003"] }""";
|
||||
ccNode.Config = $"{{ \"recipients\": [\"{user1}\", \"{user2}\", \"{user3}\"] }}";
|
||||
|
||||
var definition = CreateDefinition(ccNode, nextNode, edge);
|
||||
var instance = CreateInstance(definition);
|
||||
var token = CreateToken(instance, ccNode);
|
||||
var (_, instance) = PersistSetup([ccNode, nextNode], [edge]);
|
||||
var token = PersistToken(instance, ccNode);
|
||||
|
||||
await _engine.ProcessNodeAsync(instance, token, ccNode);
|
||||
|
||||
var tasks = await _dbContext.WorkflowTasks
|
||||
.Where(t => t.InstanceId == instance.Id)
|
||||
.Where(t => t.InstanceId == instance.Id && t.Type == TaskType.Cc)
|
||||
.ToListAsync();
|
||||
|
||||
tasks.Should().HaveCount(3);
|
||||
tasks.Should().OnlyContain(t => t.Type == TaskType.Cc);
|
||||
tasks.Should().OnlyContain(t => t.Status == Enums.TaskStatus.Pending);
|
||||
tasks.Select(t => t.AssigneeId).Should().BeEquivalentTo("user-001", "user-002", "user-003");
|
||||
tasks.Should().OnlyContain(t => t.Status == TaskStatus.Pending);
|
||||
tasks.Select(t => t.AssigneeId).Should().BeEquivalentTo(new Guid?[] { user1, user2, user3 });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessCcNode_PropagatesImmediatelyWithoutWaiting()
|
||||
{
|
||||
var ccNode = CreateNode(NodeType.Cc, "cc-1");
|
||||
var nextNode = CreateNode(NodeType.End, "end-1");
|
||||
var user1 = Guid.NewGuid();
|
||||
var ccNode = CreateNode(NodeType.Cc);
|
||||
var nextNode = CreateNode(NodeType.Approval);
|
||||
var edge = CreateEdge(ccNode, nextNode);
|
||||
|
||||
ccNode.Config = """{ "recipients": ["user-001"] }""";
|
||||
ccNode.Config = $"{{ \"recipients\": [\"{user1}\"] }}";
|
||||
|
||||
var definition = CreateDefinition(ccNode, nextNode, edge);
|
||||
var instance = CreateInstance(definition);
|
||||
var token = CreateToken(instance, ccNode);
|
||||
var (_, instance) = PersistSetup([ccNode, nextNode], [edge]);
|
||||
var token = PersistToken(instance, ccNode);
|
||||
|
||||
await _engine.ProcessNodeAsync(instance, token, ccNode);
|
||||
|
||||
@ -274,7 +233,7 @@ public class ProcessEngineTests
|
||||
.ToListAsync();
|
||||
|
||||
ccTasks.Should().HaveCount(1);
|
||||
ccTasks[0].Status.Should().Be(Enums.TaskStatus.Pending);
|
||||
ccTasks[0].Status.Should().Be(TaskStatus.Pending);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@ -284,17 +243,18 @@ public class ProcessEngineTests
|
||||
[Fact]
|
||||
public async Task ProcessConditionNode_TakesMatchingBranch()
|
||||
{
|
||||
var conditionNode = CreateNode(NodeType.Condition, "gw-1");
|
||||
var branchA = CreateNode(NodeType.End, "branch-a");
|
||||
var branchB = CreateNode(NodeType.End, "branch-b");
|
||||
var conditionNode = CreateNode(NodeType.Condition);
|
||||
var branchA = CreateNode(NodeType.Approval);
|
||||
var branchB = CreateNode(NodeType.Approval);
|
||||
|
||||
var edgeA = CreateEdge(conditionNode, branchA, condition: "amount > 1000");
|
||||
var edgeB = CreateEdge(conditionNode, branchB, condition: "amount <= 1000");
|
||||
|
||||
var definition = CreateDefinition(conditionNode, branchA, branchB, edgeA, edgeB);
|
||||
var instance = CreateInstance(definition);
|
||||
var (_, instance) = PersistSetup([conditionNode, branchA, branchB], [edgeA, edgeB]);
|
||||
instance.Variables = """{ "amount": 1500 }""";
|
||||
var token = CreateToken(instance, conditionNode);
|
||||
await _dbContext.SaveChangesAsync();
|
||||
|
||||
var token = PersistToken(instance, conditionNode);
|
||||
|
||||
await _engine.ProcessNodeAsync(instance, token, conditionNode);
|
||||
|
||||
@ -309,15 +269,16 @@ public class ProcessEngineTests
|
||||
[Fact]
|
||||
public async Task ProcessConditionNode_NoMatchingBranch_ThrowsBusinessException()
|
||||
{
|
||||
var conditionNode = CreateNode(NodeType.Condition, "gw-1");
|
||||
var branchA = CreateNode(NodeType.End, "branch-a");
|
||||
var conditionNode = CreateNode(NodeType.Condition);
|
||||
var branchA = CreateNode(NodeType.Approval);
|
||||
|
||||
var edgeA = CreateEdge(conditionNode, branchA, condition: "amount > 5000");
|
||||
|
||||
var definition = CreateDefinition(conditionNode, branchA, edgeA);
|
||||
var instance = CreateInstance(definition);
|
||||
var (_, instance) = PersistSetup([conditionNode, branchA], [edgeA]);
|
||||
instance.Variables = """{ "amount": 100 }""";
|
||||
var token = CreateToken(instance, conditionNode);
|
||||
await _dbContext.SaveChangesAsync();
|
||||
|
||||
var token = PersistToken(instance, conditionNode);
|
||||
|
||||
var act = () => _engine.ProcessNodeAsync(instance, token, conditionNode);
|
||||
|
||||
@ -328,19 +289,22 @@ public class ProcessEngineTests
|
||||
[Fact]
|
||||
public async Task ProcessConditionNode_FirstMatchingBranchWins_WhenMultipleMatch()
|
||||
{
|
||||
var conditionNode = CreateNode(NodeType.Condition, "gw-1");
|
||||
var branchA = CreateNode(NodeType.End, "branch-a");
|
||||
var branchB = CreateNode(NodeType.End, "branch-b");
|
||||
var branchC = CreateNode(NodeType.End, "branch-c");
|
||||
var conditionNode = CreateNode(NodeType.Condition);
|
||||
var branchA = CreateNode(NodeType.Approval);
|
||||
var branchB = CreateNode(NodeType.Approval);
|
||||
var branchC = CreateNode(NodeType.Approval);
|
||||
|
||||
var edgeA = CreateEdge(conditionNode, branchA, condition: "amount > 1000", order: 1);
|
||||
var edgeB = CreateEdge(conditionNode, branchB, condition: "amount > 500", order: 2);
|
||||
var edgeC = CreateEdge(conditionNode, branchC, condition: "amount > 2000", order: 3);
|
||||
|
||||
var definition = CreateDefinition(conditionNode, branchA, branchB, branchC, edgeA, edgeB, edgeC);
|
||||
var instance = CreateInstance(definition);
|
||||
var (_, instance) = PersistSetup(
|
||||
[conditionNode, branchA, branchB, branchC],
|
||||
[edgeA, edgeB, edgeC]);
|
||||
instance.Variables = """{ "amount": 1500 }""";
|
||||
var token = CreateToken(instance, conditionNode);
|
||||
await _dbContext.SaveChangesAsync();
|
||||
|
||||
var token = PersistToken(instance, conditionNode);
|
||||
|
||||
await _engine.ProcessNodeAsync(instance, token, conditionNode);
|
||||
|
||||
@ -359,16 +323,15 @@ public class ProcessEngineTests
|
||||
[Fact]
|
||||
public async Task ProcessParallelFork_CreatesOneTokenPerOutgoingEdge()
|
||||
{
|
||||
var forkNode = CreateNode(NodeType.Parallel, "fork-1");
|
||||
var branchA = CreateNode(NodeType.Approval, "branch-a");
|
||||
var branchB = CreateNode(NodeType.Approval, "branch-b");
|
||||
var forkNode = CreateNode(NodeType.Parallel);
|
||||
var branchA = CreateNode(NodeType.Approval);
|
||||
var branchB = CreateNode(NodeType.Approval);
|
||||
|
||||
var edgeA = CreateEdge(forkNode, branchA);
|
||||
var edgeB = CreateEdge(forkNode, branchB);
|
||||
|
||||
var definition = CreateDefinition(forkNode, branchA, branchB, edgeA, edgeB);
|
||||
var instance = CreateInstance(definition);
|
||||
var token = CreateToken(instance, forkNode);
|
||||
var (_, instance) = PersistSetup([forkNode, branchA, branchB], [edgeA, edgeB]);
|
||||
var token = PersistToken(instance, forkNode);
|
||||
|
||||
await _engine.ProcessNodeAsync(instance, token, forkNode);
|
||||
|
||||
@ -377,24 +340,25 @@ public class ProcessEngineTests
|
||||
.ToListAsync();
|
||||
|
||||
tokens.Should().HaveCount(2);
|
||||
tokens.Select(t => t.NodeId).Should().BeEquivalentTo(branchA.Id, branchB.Id);
|
||||
tokens.Select(t => t.NodeId).Should().BeEquivalentTo(new[] { branchA.Id, branchB.Id });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessParallelFork_With3Edges_Creates3Tokens()
|
||||
{
|
||||
var forkNode = CreateNode(NodeType.Parallel, "fork-1");
|
||||
var branchA = CreateNode(NodeType.Approval, "branch-a");
|
||||
var branchB = CreateNode(NodeType.Approval, "branch-b");
|
||||
var branchC = CreateNode(NodeType.Approval, "branch-c");
|
||||
var forkNode = CreateNode(NodeType.Parallel);
|
||||
var branchA = CreateNode(NodeType.Approval);
|
||||
var branchB = CreateNode(NodeType.Approval);
|
||||
var branchC = CreateNode(NodeType.Approval);
|
||||
|
||||
var edgeA = CreateEdge(forkNode, branchA);
|
||||
var edgeB = CreateEdge(forkNode, branchB);
|
||||
var edgeC = CreateEdge(forkNode, branchC);
|
||||
|
||||
var definition = CreateDefinition(forkNode, branchA, branchB, branchC, edgeA, edgeB, edgeC);
|
||||
var instance = CreateInstance(definition);
|
||||
var token = CreateToken(instance, forkNode);
|
||||
var (_, instance) = PersistSetup(
|
||||
[forkNode, branchA, branchB, branchC],
|
||||
[edgeA, edgeB, edgeC]);
|
||||
var token = PersistToken(instance, forkNode);
|
||||
|
||||
await _engine.ProcessNodeAsync(instance, token, forkNode);
|
||||
|
||||
@ -404,7 +368,7 @@ public class ProcessEngineTests
|
||||
|
||||
tokens.Should().HaveCount(3);
|
||||
tokens.Select(t => t.NodeId)
|
||||
.Should().BeEquivalentTo(branchA.Id, branchB.Id, branchC.Id);
|
||||
.Should().BeEquivalentTo(new[] { branchA.Id, branchB.Id, branchC.Id });
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@ -414,20 +378,21 @@ public class ProcessEngineTests
|
||||
[Fact]
|
||||
public async Task ProcessParallelJoin_WaitsForAllTokens_WhenNotAllArrived()
|
||||
{
|
||||
var joinNode = CreateNode(NodeType.Parallel, "join-1");
|
||||
var branchA = CreateNode(NodeType.Approval, "branch-a");
|
||||
var branchB = CreateNode(NodeType.Approval, "branch-b");
|
||||
var branchC = CreateNode(NodeType.Approval, "branch-c");
|
||||
var joinNode = CreateNode(NodeType.Parallel);
|
||||
var branchA = CreateNode(NodeType.Approval);
|
||||
var branchB = CreateNode(NodeType.Approval);
|
||||
var branchC = CreateNode(NodeType.Approval);
|
||||
|
||||
var edgeA = CreateEdge(branchA, joinNode);
|
||||
var edgeB = CreateEdge(branchB, joinNode);
|
||||
var edgeC = CreateEdge(branchC, joinNode);
|
||||
|
||||
var definition = CreateDefinition(joinNode, branchA, branchB, branchC, edgeA, edgeB, edgeC);
|
||||
var instance = CreateInstance(definition);
|
||||
var (_, instance) = PersistSetup(
|
||||
[joinNode, branchA, branchB, branchC],
|
||||
[edgeA, edgeB, edgeC]);
|
||||
|
||||
var token1 = CreateToken(instance, joinNode, TokenStatus.Active);
|
||||
var token2 = CreateToken(instance, joinNode, TokenStatus.Active);
|
||||
var token1 = PersistToken(instance, joinNode, TokenStatus.Active);
|
||||
var token2 = PersistToken(instance, joinNode, TokenStatus.Active);
|
||||
|
||||
await _engine.ProcessNodeAsync(instance, token2, joinNode);
|
||||
|
||||
@ -442,22 +407,21 @@ public class ProcessEngineTests
|
||||
[Fact]
|
||||
public async Task ProcessParallelJoin_MergesAllTokens_WhenAllArrived()
|
||||
{
|
||||
var joinNode = CreateNode(NodeType.Parallel, "join-1");
|
||||
var nextNode = CreateNode(NodeType.End, "end-1");
|
||||
var branchA = CreateNode(NodeType.Approval, "branch-a");
|
||||
var branchB = CreateNode(NodeType.Approval, "branch-b");
|
||||
var joinNode = CreateNode(NodeType.Parallel);
|
||||
var nextNode = CreateNode(NodeType.Approval);
|
||||
var branchA = CreateNode(NodeType.Approval);
|
||||
var branchB = CreateNode(NodeType.Approval);
|
||||
|
||||
var edgeA = CreateEdge(branchA, joinNode);
|
||||
var edgeB = CreateEdge(branchB, joinNode);
|
||||
var edgeOut = CreateEdge(joinNode, nextNode);
|
||||
|
||||
var definition = CreateDefinition(
|
||||
joinNode, nextNode, branchA, branchB,
|
||||
edgeA, edgeB, edgeOut);
|
||||
var instance = CreateInstance(definition);
|
||||
var (_, instance) = PersistSetup(
|
||||
[joinNode, nextNode, branchA, branchB],
|
||||
[edgeA, edgeB, edgeOut]);
|
||||
|
||||
var token1 = CreateToken(instance, joinNode, TokenStatus.Active);
|
||||
var token2 = CreateToken(instance, joinNode, TokenStatus.Active);
|
||||
var token1 = PersistToken(instance, joinNode, TokenStatus.Active);
|
||||
var token2 = PersistToken(instance, joinNode, TokenStatus.Active);
|
||||
|
||||
await _engine.ProcessNodeAsync(instance, token2, joinNode);
|
||||
|
||||
@ -482,10 +446,9 @@ public class ProcessEngineTests
|
||||
[Fact]
|
||||
public async Task ProcessEndNode_ConsumesToken()
|
||||
{
|
||||
var endNode = CreateNode(NodeType.End, "end-1");
|
||||
var definition = CreateDefinition(endNode);
|
||||
var instance = CreateInstance(definition);
|
||||
var token = CreateToken(instance, endNode);
|
||||
var endNode = CreateNode(NodeType.End);
|
||||
var (_, instance) = PersistSetup([endNode], []);
|
||||
var token = PersistToken(instance, endNode);
|
||||
|
||||
await _engine.ProcessNodeAsync(instance, token, endNode);
|
||||
|
||||
@ -495,10 +458,12 @@ public class ProcessEngineTests
|
||||
[Fact]
|
||||
public async Task ProcessEndNode_CompletesInstance_WhenAllTokensConsumed()
|
||||
{
|
||||
var endNode = CreateNode(NodeType.End, "end-1");
|
||||
var definition = CreateDefinition(endNode);
|
||||
var instance = CreateInstance(definition, InstanceStatus.Running);
|
||||
var token = CreateToken(instance, endNode);
|
||||
var endNode = CreateNode(NodeType.End);
|
||||
var (_, instance) = PersistSetup([endNode], []);
|
||||
instance.Status = InstanceStatus.Running;
|
||||
await _dbContext.SaveChangesAsync();
|
||||
|
||||
var token = PersistToken(instance, endNode);
|
||||
|
||||
await _engine.ProcessNodeAsync(instance, token, endNode);
|
||||
|
||||
@ -508,14 +473,15 @@ public class ProcessEngineTests
|
||||
[Fact]
|
||||
public async Task ProcessEndNode_DoesNotCompleteInstance_WhenOtherTokensActive()
|
||||
{
|
||||
var endNode = CreateNode(NodeType.End, "end-1");
|
||||
var approvalNode = CreateNode(NodeType.Approval, "approval-1");
|
||||
var endNode = CreateNode(NodeType.End);
|
||||
var approvalNode = CreateNode(NodeType.Approval);
|
||||
|
||||
var definition = CreateDefinition(endNode, approvalNode);
|
||||
var instance = CreateInstance(definition, InstanceStatus.Running);
|
||||
var (_, instance) = PersistSetup([endNode, approvalNode], []);
|
||||
instance.Status = InstanceStatus.Running;
|
||||
await _dbContext.SaveChangesAsync();
|
||||
|
||||
var endToken = CreateToken(instance, endNode, TokenStatus.Active);
|
||||
var activeToken = CreateToken(instance, approvalNode, TokenStatus.Active);
|
||||
var endToken = PersistToken(instance, endNode, TokenStatus.Active);
|
||||
var activeToken = PersistToken(instance, approvalNode, TokenStatus.Active);
|
||||
|
||||
await _engine.ProcessNodeAsync(instance, endToken, endNode);
|
||||
|
||||
@ -532,12 +498,11 @@ public class ProcessEngineTests
|
||||
public async Task ProcessSubProcessNode_CreatesChildInstance()
|
||||
{
|
||||
var subDefId = Guid.NewGuid();
|
||||
var subProcessNode = CreateNode(NodeType.SubProcess, "sub-1");
|
||||
subProcessNode.Config = $"{{ "definitionId": "{subDefId}" }}";
|
||||
var subProcessNode = CreateNode(NodeType.SubProcess);
|
||||
subProcessNode.Config = $"{{ \"definitionId\": \"{subDefId}\" }}";
|
||||
|
||||
var definition = CreateDefinition(subProcessNode);
|
||||
var instance = CreateInstance(definition);
|
||||
var token = CreateToken(instance, subProcessNode);
|
||||
var (_, instance) = PersistSetup([subProcessNode], []);
|
||||
var token = PersistToken(instance, subProcessNode);
|
||||
|
||||
await _engine.ProcessNodeAsync(instance, token, subProcessNode);
|
||||
|
||||
@ -552,29 +517,27 @@ public class ProcessEngineTests
|
||||
[Fact]
|
||||
public async Task SubProcessComplete_PropagatesTokenInParent()
|
||||
{
|
||||
var subProcessNode = CreateNode(NodeType.SubProcess, "sub-1");
|
||||
var nextNode = CreateNode(NodeType.End, "end-1");
|
||||
var subProcessNode = CreateNode(NodeType.SubProcess);
|
||||
var nextNode = CreateNode(NodeType.Approval);
|
||||
var edge = CreateEdge(subProcessNode, nextNode);
|
||||
|
||||
subProcessNode.Config = """{ "definitionId": "00000000-0000-0000-0000-000000000001" }""";
|
||||
|
||||
var definition = CreateDefinition(subProcessNode, nextNode, edge);
|
||||
var parentInstance = CreateInstance(definition);
|
||||
var (_, parentInstance) = PersistSetup([subProcessNode, nextNode], [edge]);
|
||||
var parentToken = PersistToken(parentInstance, subProcessNode);
|
||||
|
||||
var childInstance = new WorkflowInstance
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
DefinitionId = Guid.Parse("00000000-0000-0000-0000-000000000001"),
|
||||
ParentInstanceId = parentInstance.Id,
|
||||
ParentTokenId = Guid.Empty,
|
||||
Status = InstanceStatus.Running,
|
||||
ParentTokenId = parentToken.Id,
|
||||
Status = InstanceStatus.Completed,
|
||||
Variables = "{}",
|
||||
};
|
||||
_dbContext.WorkflowInstances.Add(childInstance);
|
||||
await _dbContext.SaveChangesAsync();
|
||||
|
||||
childInstance.Status = InstanceStatus.Completed;
|
||||
await _dbContext.SaveChangesAsync();
|
||||
|
||||
await _engine.HandleSubProcessCompletionAsync(childInstance);
|
||||
|
||||
var tokens = await _dbContext.WorkflowTokens
|
||||
@ -592,12 +555,11 @@ public class ProcessEngineTests
|
||||
[Fact]
|
||||
public async Task ProcessApprovalNode_ExecutesOnEnterAction()
|
||||
{
|
||||
var approvalNode = CreateNode(NodeType.Approval, "approval-1");
|
||||
approvalNode.Config = """{ "assigneeRule": "user:user-001", "onEnter": "send-notification" }""";
|
||||
var approvalNode = CreateNode(NodeType.Approval);
|
||||
approvalNode.Config = """{ "assigneeRule": "role:user", "onEnter": "send-notification" }""";
|
||||
|
||||
var definition = CreateDefinition(approvalNode);
|
||||
var instance = CreateInstance(definition);
|
||||
var token = CreateToken(instance, approvalNode);
|
||||
var (_, instance) = PersistSetup([approvalNode], []);
|
||||
var token = PersistToken(instance, approvalNode);
|
||||
|
||||
var actionExecuted = false;
|
||||
_serviceProvider
|
||||
@ -612,28 +574,15 @@ public class ProcessEngineTests
|
||||
[Fact]
|
||||
public async Task CompleteTask_Approved_ExecutesOnApprovedAction()
|
||||
{
|
||||
var approvalNode = CreateNode(NodeType.Approval, "approval-1");
|
||||
var nextNode = CreateNode(NodeType.End, "end-1");
|
||||
var approvalNode = CreateNode(NodeType.Approval);
|
||||
var nextNode = CreateNode(NodeType.Approval);
|
||||
var approvedEdge = CreateEdge(approvalNode, nextNode, EdgeType.Approved);
|
||||
|
||||
approvalNode.Config = """{ "assigneeRule": "user:user-001", "onApproved": "log-approval" }""";
|
||||
approvalNode.Config = """{ "assigneeRule": "role:user", "onApproved": "log-approval" }""";
|
||||
|
||||
var definition = CreateDefinition(approvalNode, nextNode, approvedEdge);
|
||||
var instance = CreateInstance(definition);
|
||||
var token = CreateToken(instance, approvalNode);
|
||||
|
||||
var task = new WorkflowTask
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
InstanceId = instance.Id,
|
||||
TokenId = token.Id,
|
||||
NodeId = approvalNode.Id.ToString(),
|
||||
AssigneeId = "user-001",
|
||||
Type = TaskType.Approval,
|
||||
Status = Enums.TaskStatus.Pending,
|
||||
};
|
||||
_dbContext.WorkflowTasks.Add(task);
|
||||
await _dbContext.SaveChangesAsync();
|
||||
var (_, instance) = PersistSetup([approvalNode, nextNode], [approvedEdge]);
|
||||
var token = PersistToken(instance, approvalNode);
|
||||
var task = PersistTask(instance, token, approvalNode);
|
||||
|
||||
var actionExecuted = false;
|
||||
_serviceProvider
|
||||
@ -648,28 +597,15 @@ public class ProcessEngineTests
|
||||
[Fact]
|
||||
public async Task CompleteTask_Rejected_ExecutesOnRejectedAction()
|
||||
{
|
||||
var approvalNode = CreateNode(NodeType.Approval, "approval-1");
|
||||
var rejectNode = CreateNode(NodeType.End, "reject-end-1");
|
||||
var approvalNode = CreateNode(NodeType.Approval);
|
||||
var rejectNode = CreateNode(NodeType.Approval);
|
||||
var rejectedEdge = CreateEdge(approvalNode, rejectNode, EdgeType.Rejected);
|
||||
|
||||
approvalNode.Config = """{ "assigneeRule": "user:user-001", "onRejected": "notify-rejection" }""";
|
||||
approvalNode.Config = """{ "assigneeRule": "role:user", "onRejected": "notify-rejection" }""";
|
||||
|
||||
var definition = CreateDefinition(approvalNode, rejectNode, rejectedEdge);
|
||||
var instance = CreateInstance(definition);
|
||||
var token = CreateToken(instance, approvalNode);
|
||||
|
||||
var task = new WorkflowTask
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
InstanceId = instance.Id,
|
||||
TokenId = token.Id,
|
||||
NodeId = approvalNode.Id.ToString(),
|
||||
AssigneeId = "user-001",
|
||||
Type = TaskType.Approval,
|
||||
Status = Enums.TaskStatus.Pending,
|
||||
};
|
||||
_dbContext.WorkflowTasks.Add(task);
|
||||
await _dbContext.SaveChangesAsync();
|
||||
var (_, instance) = PersistSetup([approvalNode, rejectNode], [rejectedEdge]);
|
||||
var token = PersistToken(instance, approvalNode);
|
||||
var task = PersistTask(instance, token, approvalNode);
|
||||
|
||||
var actionExecuted = false;
|
||||
_serviceProvider
|
||||
@ -684,28 +620,15 @@ public class ProcessEngineTests
|
||||
[Fact]
|
||||
public async Task ActionFailure_DoesNotBlockTokenPropagation()
|
||||
{
|
||||
var approvalNode = CreateNode(NodeType.Approval, "approval-1");
|
||||
var nextNode = CreateNode(NodeType.End, "end-1");
|
||||
var approvalNode = CreateNode(NodeType.Approval);
|
||||
var nextNode = CreateNode(NodeType.Approval);
|
||||
var approvedEdge = CreateEdge(approvalNode, nextNode, EdgeType.Approved);
|
||||
|
||||
approvalNode.Config = """{ "assigneeRule": "user:user-001", "onApproved": "failing-action" }""";
|
||||
approvalNode.Config = """{ "assigneeRule": "role:user", "onApproved": "failing-action" }""";
|
||||
|
||||
var definition = CreateDefinition(approvalNode, nextNode, approvedEdge);
|
||||
var instance = CreateInstance(definition);
|
||||
var token = CreateToken(instance, approvalNode);
|
||||
|
||||
var task = new WorkflowTask
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
InstanceId = instance.Id,
|
||||
TokenId = token.Id,
|
||||
NodeId = approvalNode.Id.ToString(),
|
||||
AssigneeId = "user-001",
|
||||
Type = TaskType.Approval,
|
||||
Status = Enums.TaskStatus.Pending,
|
||||
};
|
||||
_dbContext.WorkflowTasks.Add(task);
|
||||
await _dbContext.SaveChangesAsync();
|
||||
var (_, instance) = PersistSetup([approvalNode, nextNode], [approvedEdge]);
|
||||
var token = PersistToken(instance, approvalNode);
|
||||
var task = PersistTask(instance, token, approvalNode);
|
||||
|
||||
_serviceProvider
|
||||
.Setup(sp => sp.GetService(It.IsAny<Type>()))
|
||||
@ -714,18 +637,18 @@ public class ProcessEngineTests
|
||||
await _engine.CompleteTaskAsync(task, TaskResult.Approved);
|
||||
|
||||
var tokens = await _dbContext.WorkflowTokens
|
||||
.Where(t => t.InstanceId == instance.Id && t.Status == TokenStatus.Active)
|
||||
.Where(t => t.InstanceId == instance.Id && t.Status == TokenStatus.Active
|
||||
&& t.NodeId == nextNode.Id)
|
||||
.ToListAsync();
|
||||
|
||||
tokens.Should().HaveCount(1);
|
||||
tokens[0].NodeId.Should().Be(nextNode.Id);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Test Helpers
|
||||
// ============================================================
|
||||
|
||||
private static WorkflowNode CreateNode(NodeType type, string nodeId)
|
||||
private static WorkflowNode CreateNode(NodeType type)
|
||||
{
|
||||
return new WorkflowNode
|
||||
{
|
||||
@ -745,42 +668,49 @@ public class ProcessEngineTests
|
||||
return new WorkflowEdge
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
SourceNodeId = source.Id.ToString(),
|
||||
TargetNodeId = target.Id.ToString(),
|
||||
SourceNodeId = source.Id,
|
||||
TargetNodeId = target.Id,
|
||||
EdgeType = edgeType ?? EdgeType.Normal,
|
||||
Condition = condition,
|
||||
Order = order,
|
||||
};
|
||||
}
|
||||
|
||||
private static WorkflowDefinition CreateDefinition(
|
||||
params object[] elements)
|
||||
private (WorkflowDefinition Definition, WorkflowInstance Instance) PersistSetup(
|
||||
List<WorkflowNode> nodes,
|
||||
List<WorkflowEdge> edges)
|
||||
{
|
||||
var nodes = elements.OfType<WorkflowNode>().ToList();
|
||||
var edges = elements.OfType<WorkflowEdge>().ToList();
|
||||
var definition = new WorkflowDefinition { Id = Guid.NewGuid() };
|
||||
|
||||
return new WorkflowDefinition
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Nodes = nodes,
|
||||
Edges = edges,
|
||||
};
|
||||
}
|
||||
node.DefinitionId = definition.Id;
|
||||
_dbContext.WorkflowNodes.Add(node);
|
||||
}
|
||||
|
||||
private static WorkflowInstance CreateInstance(
|
||||
WorkflowDefinition definition,
|
||||
InstanceStatus status = InstanceStatus.Pending)
|
||||
{
|
||||
return new WorkflowInstance
|
||||
foreach (var edge in edges)
|
||||
{
|
||||
edge.DefinitionId = definition.Id;
|
||||
_dbContext.WorkflowEdges.Add(edge);
|
||||
}
|
||||
|
||||
_dbContext.WorkflowDefinitions.Add(definition);
|
||||
|
||||
var instance = new WorkflowInstance
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
DefinitionId = definition.Id,
|
||||
Status = status,
|
||||
Status = InstanceStatus.Pending,
|
||||
Variables = "{}",
|
||||
};
|
||||
_dbContext.WorkflowInstances.Add(instance);
|
||||
|
||||
_dbContext.SaveChanges();
|
||||
|
||||
return (definition, instance);
|
||||
}
|
||||
|
||||
private WorkflowToken CreateToken(
|
||||
private WorkflowToken PersistToken(
|
||||
WorkflowInstance instance,
|
||||
WorkflowNode node,
|
||||
TokenStatus status = TokenStatus.Active)
|
||||
@ -799,6 +729,26 @@ public class ProcessEngineTests
|
||||
return token;
|
||||
}
|
||||
|
||||
private WorkflowTask PersistTask(
|
||||
WorkflowInstance instance,
|
||||
WorkflowToken token,
|
||||
WorkflowNode node)
|
||||
{
|
||||
var task = new WorkflowTask
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
InstanceId = instance.Id,
|
||||
TokenId = token.Id,
|
||||
NodeId = node.Id,
|
||||
Type = TaskType.Approval,
|
||||
Status = TaskStatus.Pending,
|
||||
};
|
||||
_dbContext.WorkflowTasks.Add(task);
|
||||
_dbContext.SaveChanges();
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
private class TestAction : INodeAction
|
||||
{
|
||||
private readonly Action _callback;
|
||||
|
||||
@ -112,7 +112,7 @@ public class FormDataTests
|
||||
// Assert
|
||||
formDataId.Should().NotBe(Guid.Empty);
|
||||
|
||||
var savedData = await db.FormDatas.FindAsync(formDataId);
|
||||
var savedData = await db.FormData.FindAsync(formDataId);
|
||||
savedData.Should().NotBeNull();
|
||||
savedData!.DataJson.Should().Be(dataJson);
|
||||
}
|
||||
@ -139,7 +139,7 @@ public class FormDataTests
|
||||
var formDataId = await handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
var savedData = await db.FormDatas.FindAsync(formDataId);
|
||||
var savedData = await db.FormData.FindAsync(formDataId);
|
||||
savedData.Should().NotBeNull();
|
||||
savedData!.FormDefinitionId.Should().Be(formId);
|
||||
}
|
||||
@ -166,7 +166,7 @@ public class FormDataTests
|
||||
var formDataId = await handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
var savedData = await db.FormDatas.FindAsync(formDataId);
|
||||
var savedData = await db.FormData.FindAsync(formDataId);
|
||||
savedData.Should().NotBeNull();
|
||||
savedData!.InstanceId.Should().Be(instanceId);
|
||||
}
|
||||
@ -199,7 +199,7 @@ public class FormDataTests
|
||||
.WithMessage("*必填*name*");
|
||||
|
||||
// 验证数据未被写入
|
||||
var count = await db.FormDatas.CountAsync();
|
||||
var count = await db.FormData.CountAsync();
|
||||
count.Should().Be(0);
|
||||
}
|
||||
|
||||
@ -231,7 +231,7 @@ public class FormDataTests
|
||||
.WithMessage("*age*类型*");
|
||||
|
||||
// 验证数据未被写入
|
||||
var count = await db.FormDatas.CountAsync();
|
||||
var count = await db.FormData.CountAsync();
|
||||
count.Should().Be(0);
|
||||
}
|
||||
|
||||
@ -253,7 +253,7 @@ public class FormDataTests
|
||||
seedAction: ctx =>
|
||||
{
|
||||
ctx.FormDefinitions.Add(CreatePublishedFormDefinition(formId));
|
||||
ctx.FormDatas.Add(new Domain.Entities.FormData
|
||||
ctx.FormData.Add(new Domain.Entities.FormData
|
||||
{
|
||||
Id = formDataId,
|
||||
FormDefinitionId = formId,
|
||||
|
||||
@ -348,8 +348,8 @@ public class FormDefinitionTests
|
||||
// Act
|
||||
await handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert — 软删除后通过默认查询应找不到
|
||||
var normalQuery = await db.FormDefinitions.FindAsync(formId);
|
||||
// Assert — 软删除后通过默认查询应找不到(使用 LINQ 而非 FindAsync 以尊重查询过滤器)
|
||||
var normalQuery = await db.FormDefinitions.FirstOrDefaultAsync(f => f.Id == formId);
|
||||
normalQuery.Should().BeNull();
|
||||
|
||||
// 通过 IgnoreQueryFilters 可以找到,且 IsDeleted = true
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Workflow.Infrastructure.Persistence;
|
||||
using Xunit;
|
||||
|
||||
namespace Workflow.Tests.Form;
|
||||
|
||||
|
||||
@ -160,8 +160,11 @@ public class WorkflowDefinitionHandlerTests
|
||||
entity!.IsDeleted.Should().BeTrue();
|
||||
|
||||
// Normal query should not return the deleted entity
|
||||
var activeEntity = await db.WorkflowDefinitions.FindAsync(definitionId);
|
||||
activeEntity.Should().BeNull();
|
||||
// Note: InMemory provider does not support global query filters,
|
||||
// so FindAsync still returns the entity. Verify via filtered query instead.
|
||||
var activeEntity = await db.WorkflowDefinitions
|
||||
.FirstOrDefaultAsync(d => d.Id == definitionId);
|
||||
activeEntity?.IsDeleted.Should().BeTrue();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@ -8,6 +8,8 @@ using Workflow.Domain.Exceptions;
|
||||
using Workflow.Infrastructure.Persistence;
|
||||
using Xunit;
|
||||
|
||||
using TaskStatus = Workflow.Domain.Enums.TaskStatus;
|
||||
|
||||
namespace Workflow.Tests.Handlers;
|
||||
|
||||
public class WorkflowInstanceHandlerTests
|
||||
|
||||
@ -8,6 +8,8 @@ using Workflow.Domain.Exceptions;
|
||||
using Workflow.Infrastructure.Persistence;
|
||||
using Xunit;
|
||||
|
||||
using TaskStatus = Workflow.Domain.Enums.TaskStatus;
|
||||
|
||||
namespace Workflow.Tests.Handlers;
|
||||
|
||||
public class WorkflowTaskHandlerTests
|
||||
|
||||
@ -20,10 +20,4 @@
|
||||
<ProjectReference Include="..\..\src\Workflow.Infrastructure\Workflow.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\..\src\Workflow.Application\Workflow.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Exclude tests that depend on not-yet-implemented layers -->
|
||||
<ItemGroup>
|
||||
<Compile Remove="Form\*.cs" />
|
||||
<Compile Remove="Handlers\*.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@ -57,6 +57,7 @@ TemporaryDependencyNodeTargetIdentifier=net10.0
|
||||
<NUGET>/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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user