perf: reuse prepared form schemas
This commit is contained in:
parent
178b441b8a
commit
943ae02859
@ -0,0 +1,7 @@
|
|||||||
|
namespace Workflow.Application.BuildFlow.Common.Services;
|
||||||
|
|
||||||
|
/// <summary>解析后的 Excel 行。</summary>
|
||||||
|
public sealed record ExcelParsedRow(
|
||||||
|
int RowNumber,
|
||||||
|
string JsonObject,
|
||||||
|
IReadOnlyList<ExcelCellError> Errors);
|
||||||
@ -53,7 +53,7 @@ public class FormSchemaExcelService : IFormSchemaExcelService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IReadOnlyList<ExcelParsedRow> Parse(string schemaJson, byte[] xlsxBytes)
|
public IEnumerable<ExcelParsedRow> Parse(string schemaJson, byte[] xlsxBytes)
|
||||||
{
|
{
|
||||||
var fields = ExtractFields(schemaJson);
|
var fields = ExtractFields(schemaJson);
|
||||||
using var ms = new MemoryStream(xlsxBytes);
|
using var ms = new MemoryStream(xlsxBytes);
|
||||||
@ -62,19 +62,16 @@ public class FormSchemaExcelService : IFormSchemaExcelService
|
|||||||
var lastRow = ws.LastRowUsed();
|
var lastRow = ws.LastRowUsed();
|
||||||
|
|
||||||
var headerMap = BuildHeaderMap(ws, fields);
|
var headerMap = BuildHeaderMap(ws, fields);
|
||||||
var result = new List<ExcelParsedRow>();
|
|
||||||
if (lastRow is null)
|
if (lastRow is null)
|
||||||
{
|
{
|
||||||
return result;
|
yield break;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var r = 2; r <= lastRow.RowNumber(); r++)
|
for (var r = 2; r <= lastRow.RowNumber(); r++)
|
||||||
{
|
{
|
||||||
var (json, errors) = BuildRowJson(ws, r, headerMap, fields);
|
var (json, errors) = BuildRowJson(ws, r, headerMap, fields);
|
||||||
result.Add(new ExcelParsedRow(r, json, errors));
|
yield return new ExcelParsedRow(r, json, errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IReadOnlyList<FieldSummary> ExtractFields(string schemaJson)
|
private static IReadOnlyList<FieldSummary> ExtractFields(string schemaJson)
|
||||||
|
|||||||
@ -12,12 +12,6 @@ public interface IFormSchemaExcelService
|
|||||||
/// <summary>根据 SchemaJson 字段顺序导出已有 FormData 列表为 xlsx 字节流。</summary>
|
/// <summary>根据 SchemaJson 字段顺序导出已有 FormData 列表为 xlsx 字节流。</summary>
|
||||||
byte[] BuildExport(string schemaJson, string formName, IEnumerable<string> dataJsonRows);
|
byte[] BuildExport(string schemaJson, string formName, IEnumerable<string> dataJsonRows);
|
||||||
|
|
||||||
/// <summary>解析上传 xlsx 字节流,按字段顺序返回每行的 JSON 对象字符串。</summary>
|
/// <summary>解析上传 xlsx 字节流,按字段顺序惰性返回每行的 JSON 对象字符串。</summary>
|
||||||
IReadOnlyList<ExcelParsedRow> Parse(string schemaJson, byte[] xlsxBytes);
|
IEnumerable<ExcelParsedRow> Parse(string schemaJson, byte[] xlsxBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>解析后的 Excel 行。</summary>
|
|
||||||
public sealed record ExcelParsedRow(
|
|
||||||
int RowNumber,
|
|
||||||
string JsonObject,
|
|
||||||
IReadOnlyList<ExcelCellError> Errors);
|
|
||||||
|
|||||||
@ -62,25 +62,24 @@ public class ImportJobProcessor(
|
|||||||
jobId,
|
jobId,
|
||||||
leaseOwner,
|
leaseOwner,
|
||||||
leaseDuration,
|
leaseDuration,
|
||||||
rows.Count,
|
|
||||||
ct);
|
ct);
|
||||||
if (nextRowIndex is null) return;
|
if (nextRowIndex is null) return;
|
||||||
if (nextRowIndex.Value > rows.Count)
|
|
||||||
{
|
|
||||||
throw new BusinessException(
|
|
||||||
ErrorCode.Business,
|
|
||||||
$"导入任务进度游标 {nextRowIndex.Value} 超过总行数 {rows.Count}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var submitHandler = new SubmitFormDataCommandHandler(
|
var submitHandler = new SubmitFormDataCommandHandler(
|
||||||
db,
|
db,
|
||||||
new FormValueDerivationService([], [], db),
|
new FormValueDerivationService([], [], db),
|
||||||
new FormAggregateRefreshService(db));
|
new FormAggregateRefreshService(db));
|
||||||
|
|
||||||
for (var i = nextRowIndex.Value; i < rows.Count; i++)
|
var rowIndex = 0;
|
||||||
|
foreach (var row in rows)
|
||||||
{
|
{
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
var row = rows[i];
|
if (rowIndex < nextRowIndex.Value)
|
||||||
|
{
|
||||||
|
rowIndex++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (row.Errors.Count > 0)
|
if (row.Errors.Count > 0)
|
||||||
{
|
{
|
||||||
var recorded = await CommitFailedRowAsync(
|
var recorded = await CommitFailedRowAsync(
|
||||||
@ -89,8 +88,10 @@ public class ImportJobProcessor(
|
|||||||
leaseDuration,
|
leaseDuration,
|
||||||
row.RowNumber,
|
row.RowNumber,
|
||||||
row.Errors.Select(e => e.Message).ToList(),
|
row.Errors.Select(e => e.Message).ToList(),
|
||||||
|
rowIndex + 1,
|
||||||
ct);
|
ct);
|
||||||
if (!recorded) return;
|
if (!recorded) return;
|
||||||
|
rowIndex++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,7 +102,7 @@ public class ImportJobProcessor(
|
|||||||
leaseOwner,
|
leaseOwner,
|
||||||
leaseDuration,
|
leaseDuration,
|
||||||
job.FormDefinitionId,
|
job.FormDefinitionId,
|
||||||
i,
|
rowIndex,
|
||||||
row.JsonObject,
|
row.JsonObject,
|
||||||
submitHandler,
|
submitHandler,
|
||||||
ct);
|
ct);
|
||||||
@ -119,12 +120,22 @@ public class ImportJobProcessor(
|
|||||||
leaseDuration,
|
leaseDuration,
|
||||||
row.RowNumber,
|
row.RowNumber,
|
||||||
[ex.Message],
|
[ex.Message],
|
||||||
|
rowIndex + 1,
|
||||||
ct);
|
ct);
|
||||||
if (!recorded) return;
|
if (!recorded) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rowIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
await CompleteJobAsync(jobId, leaseOwner, ct);
|
if (nextRowIndex.Value > rowIndex)
|
||||||
|
{
|
||||||
|
throw new BusinessException(
|
||||||
|
ErrorCode.Business,
|
||||||
|
$"导入任务进度游标 {nextRowIndex.Value} 超过总行数 {rowIndex}");
|
||||||
|
}
|
||||||
|
|
||||||
|
await CompleteJobAsync(jobId, leaseOwner, rowIndex, ct);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
@ -141,7 +152,6 @@ public class ImportJobProcessor(
|
|||||||
Guid jobId,
|
Guid jobId,
|
||||||
string leaseOwner,
|
string leaseOwner,
|
||||||
TimeSpan leaseDuration,
|
TimeSpan leaseDuration,
|
||||||
int totalRows,
|
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
db.ChangeTracker.Clear();
|
db.ChangeTracker.Clear();
|
||||||
@ -153,7 +163,6 @@ public class ImportJobProcessor(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
job.TotalRows = totalRows;
|
|
||||||
RenewLease(job, leaseDuration);
|
RenewLease(job, leaseDuration);
|
||||||
await db.SaveChangesAsync(ct);
|
await db.SaveChangesAsync(ct);
|
||||||
await transaction.CommitAsync(ct);
|
await transaction.CommitAsync(ct);
|
||||||
@ -184,6 +193,7 @@ public class ImportJobProcessor(
|
|||||||
new SubmitFormDataCommand(formDefinitionId, instanceId, dataJson),
|
new SubmitFormDataCommand(formDefinitionId, instanceId, dataJson),
|
||||||
ct);
|
ct);
|
||||||
job.SuccessRows++;
|
job.SuccessRows++;
|
||||||
|
job.TotalRows = Math.Max(job.TotalRows, rowIndex + 1);
|
||||||
RenewLease(job, leaseDuration);
|
RenewLease(job, leaseDuration);
|
||||||
await db.SaveChangesAsync(ct);
|
await db.SaveChangesAsync(ct);
|
||||||
await transaction.CommitAsync(ct);
|
await transaction.CommitAsync(ct);
|
||||||
@ -196,6 +206,7 @@ public class ImportJobProcessor(
|
|||||||
TimeSpan leaseDuration,
|
TimeSpan leaseDuration,
|
||||||
int rowNumber,
|
int rowNumber,
|
||||||
IReadOnlyList<string> messages,
|
IReadOnlyList<string> messages,
|
||||||
|
int processedRows,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
db.ChangeTracker.Clear();
|
db.ChangeTracker.Clear();
|
||||||
@ -210,6 +221,7 @@ public class ImportJobProcessor(
|
|||||||
var errors = DeserializeErrors(job.ErrorJson);
|
var errors = DeserializeErrors(job.ErrorJson);
|
||||||
errors.Add(new ImportRowError(rowNumber, messages));
|
errors.Add(new ImportRowError(rowNumber, messages));
|
||||||
job.FailedRows++;
|
job.FailedRows++;
|
||||||
|
job.TotalRows = Math.Max(job.TotalRows, processedRows);
|
||||||
job.ErrorJson = JsonSerializer.Serialize(errors, JsonOptions);
|
job.ErrorJson = JsonSerializer.Serialize(errors, JsonOptions);
|
||||||
RenewLease(job, leaseDuration);
|
RenewLease(job, leaseDuration);
|
||||||
await db.SaveChangesAsync(ct);
|
await db.SaveChangesAsync(ct);
|
||||||
@ -220,6 +232,7 @@ public class ImportJobProcessor(
|
|||||||
private async Task CompleteJobAsync(
|
private async Task CompleteJobAsync(
|
||||||
Guid jobId,
|
Guid jobId,
|
||||||
string leaseOwner,
|
string leaseOwner,
|
||||||
|
int totalRows,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
db.ChangeTracker.Clear();
|
db.ChangeTracker.Clear();
|
||||||
@ -237,6 +250,7 @@ public class ImportJobProcessor(
|
|||||||
? BuildFlowImportJobStatus.Failed
|
? BuildFlowImportJobStatus.Failed
|
||||||
: BuildFlowImportJobStatus.PartialSuccess;
|
: BuildFlowImportJobStatus.PartialSuccess;
|
||||||
job.ErrorMessage = null;
|
job.ErrorMessage = null;
|
||||||
|
job.TotalRows = totalRows;
|
||||||
job.FinishedAt = DateTime.UtcNow;
|
job.FinishedAt = DateTime.UtcNow;
|
||||||
job.LastHeartbeatAt = DateTime.UtcNow;
|
job.LastHeartbeatAt = DateTime.UtcNow;
|
||||||
job.LeaseOwner = null;
|
job.LeaseOwner = null;
|
||||||
|
|||||||
@ -2,6 +2,7 @@ using System.Text.Encodings.Web;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
using Workflow.Application.Form.Relation;
|
using Workflow.Application.Form.Relation;
|
||||||
|
using Workflow.Application.Form.Schema;
|
||||||
using Workflow.Domain.Enums;
|
using Workflow.Domain.Enums;
|
||||||
using Workflow.Infrastructure.Persistence;
|
using Workflow.Infrastructure.Persistence;
|
||||||
|
|
||||||
@ -33,15 +34,22 @@ public sealed class FormValueDerivationService : IFormValueDerivationService
|
|||||||
_db = db;
|
_db = db;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<FormDerivationResult> DeriveAsync(
|
public Task<FormDerivationResult> DeriveAsync(
|
||||||
string schemaJson,
|
string schemaJson,
|
||||||
string dataJson,
|
string dataJson,
|
||||||
FormDerivationContext context,
|
FormDerivationContext context,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
|
=> DeriveAsync(PreparedFormSchema.Create(schemaJson), dataJson, context, ct);
|
||||||
|
|
||||||
|
public async Task<FormDerivationResult> DeriveAsync(
|
||||||
|
PreparedFormSchema preparedSchema,
|
||||||
|
string dataJson,
|
||||||
|
FormDerivationContext context,
|
||||||
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var schema = JsonNode.Parse(schemaJson) as JsonObject ?? new JsonObject();
|
var schema = preparedSchema.Schema.DeepClone().AsObject();
|
||||||
var data = JsonNode.Parse(dataJson) as JsonObject ?? new JsonObject();
|
var data = JsonNode.Parse(dataJson) as JsonObject ?? new JsonObject();
|
||||||
var derivedPaths = new List<string>();
|
var derivedPaths = new List<string>();
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
using Workflow.Application.Form.Schema;
|
||||||
|
|
||||||
namespace Workflow.Application.Form.Derivation;
|
namespace Workflow.Application.Form.Derivation;
|
||||||
|
|
||||||
/// <summary>统一表单提交前派生值管线:计算字段、关联快照、统计字段都在这里收口。</summary>
|
/// <summary>统一表单提交前派生值管线:计算字段、关联快照、统计字段都在这里收口。</summary>
|
||||||
@ -8,4 +10,11 @@ public interface IFormValueDerivationService
|
|||||||
string dataJson,
|
string dataJson,
|
||||||
FormDerivationContext context,
|
FormDerivationContext context,
|
||||||
CancellationToken ct);
|
CancellationToken ct);
|
||||||
|
|
||||||
|
/// <summary>使用预解析 Schema 执行派生,供批量场景复用字段元数据。</summary>
|
||||||
|
Task<FormDerivationResult> DeriveAsync(
|
||||||
|
PreparedFormSchema schema,
|
||||||
|
string dataJson,
|
||||||
|
FormDerivationContext context,
|
||||||
|
CancellationToken ct);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,6 +33,31 @@ public static class FormDataValidator
|
|||||||
IReadOnlyDictionary<string, string>? nodeFieldPermissions = null)
|
IReadOnlyDictionary<string, string>? nodeFieldPermissions = null)
|
||||||
{
|
{
|
||||||
var schemaValidation = SchemaValidator.Validate(schemaJson, allowedComponents);
|
var schemaValidation = SchemaValidator.Validate(schemaJson, allowedComponents);
|
||||||
|
return Validate(
|
||||||
|
schemaValidation,
|
||||||
|
dataJson,
|
||||||
|
currentNodeKey,
|
||||||
|
nodeFieldPermissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>使用预解析 Schema 校验表单数据,避免批量场景逐行重复解析。</summary>
|
||||||
|
public static FormDataValidationResult Validate(
|
||||||
|
PreparedFormSchema schema,
|
||||||
|
string dataJson,
|
||||||
|
string? currentNodeKey = null,
|
||||||
|
IReadOnlyDictionary<string, string>? nodeFieldPermissions = null)
|
||||||
|
=> Validate(
|
||||||
|
schema.Validation,
|
||||||
|
dataJson,
|
||||||
|
currentNodeKey,
|
||||||
|
nodeFieldPermissions);
|
||||||
|
|
||||||
|
private static FormDataValidationResult Validate(
|
||||||
|
SchemaValidationResult schemaValidation,
|
||||||
|
string dataJson,
|
||||||
|
string? currentNodeKey,
|
||||||
|
IReadOnlyDictionary<string, string>? nodeFieldPermissions)
|
||||||
|
{
|
||||||
if (!schemaValidation.IsValid)
|
if (!schemaValidation.IsValid)
|
||||||
{
|
{
|
||||||
return FormDataValidationResult.Invalid(schemaValidation.Errors);
|
return FormDataValidationResult.Invalid(schemaValidation.Errors);
|
||||||
|
|||||||
@ -0,0 +1,43 @@
|
|||||||
|
using System.Text.Json.Nodes;
|
||||||
|
|
||||||
|
namespace Workflow.Application.Form.Schema;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 预解析表单 Schema。批量用例复用字段校验结果和 JSON 结构,避免逐行重复解析。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PreparedFormSchema
|
||||||
|
{
|
||||||
|
private PreparedFormSchema(
|
||||||
|
string schemaJson,
|
||||||
|
JsonObject schema,
|
||||||
|
SchemaValidationResult validation)
|
||||||
|
{
|
||||||
|
SchemaJson = schemaJson;
|
||||||
|
Schema = schema;
|
||||||
|
Validation = validation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>原始 Schema JSON。</summary>
|
||||||
|
public string SchemaJson { get; }
|
||||||
|
|
||||||
|
/// <summary>解析后的 Schema JSON 对象。</summary>
|
||||||
|
public JsonObject Schema { get; }
|
||||||
|
|
||||||
|
/// <summary>已提取字段元数据的 Schema 校验结果。</summary>
|
||||||
|
public SchemaValidationResult Validation { get; }
|
||||||
|
|
||||||
|
/// <summary>校验并准备一个可复用的表单 Schema。</summary>
|
||||||
|
public static PreparedFormSchema Create(string schemaJson)
|
||||||
|
{
|
||||||
|
var validation = SchemaValidator.Validate(schemaJson);
|
||||||
|
if (!validation.IsValid)
|
||||||
|
{
|
||||||
|
throw new BusinessException(
|
||||||
|
ErrorCode.InvalidParam,
|
||||||
|
$"表单 Schema 无效: {string.Join("; ", validation.Errors)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var schema = JsonNode.Parse(schemaJson) as JsonObject ?? new JsonObject();
|
||||||
|
return new PreparedFormSchema(schemaJson, schema, validation);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -49,7 +49,7 @@ public class FormSchemaExcelServiceTests
|
|||||||
using var outMs = new MemoryStream();
|
using var outMs = new MemoryStream();
|
||||||
wb.SaveAs(outMs);
|
wb.SaveAs(outMs);
|
||||||
|
|
||||||
var rows = _service.Parse(SampleSchema, outMs.ToArray());
|
var rows = _service.Parse(SampleSchema, outMs.ToArray()).ToList();
|
||||||
|
|
||||||
rows.Should().HaveCount(1);
|
rows.Should().HaveCount(1);
|
||||||
var row = rows[0];
|
var row = rows[0];
|
||||||
@ -73,7 +73,7 @@ public class FormSchemaExcelServiceTests
|
|||||||
using var outMs = new MemoryStream();
|
using var outMs = new MemoryStream();
|
||||||
wb.SaveAs(outMs);
|
wb.SaveAs(outMs);
|
||||||
|
|
||||||
var rows = _service.Parse(SampleSchema, outMs.ToArray());
|
var rows = _service.Parse(SampleSchema, outMs.ToArray()).ToList();
|
||||||
|
|
||||||
rows.Should().HaveCount(1);
|
rows.Should().HaveCount(1);
|
||||||
rows[0].Errors.Should().ContainSingle(e => e.Message.Contains("必填"));
|
rows[0].Errors.Should().ContainSingle(e => e.Message.Contains("必填"));
|
||||||
|
|||||||
@ -0,0 +1,42 @@
|
|||||||
|
using ClosedXML.Excel;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Workflow.Application.BuildFlow.Common.Services;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace BuildFlow.Workflow.Tests.Performance;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Excel 导入行惰性枚举契约测试。
|
||||||
|
/// </summary>
|
||||||
|
public class LazyExcelRowTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ParseReturnsLazyEnumerableInsteadOfMaterializedList()
|
||||||
|
{
|
||||||
|
var service = new FormSchemaExcelService();
|
||||||
|
var template = service.BuildTemplate(Schema, "惰性导入");
|
||||||
|
using var input = new MemoryStream(template);
|
||||||
|
using var workbook = new XLWorkbook(input);
|
||||||
|
var worksheet = workbook.Worksheets.First();
|
||||||
|
worksheet.Cell(2, 1).Value = "first";
|
||||||
|
worksheet.Cell(3, 1).Value = "second";
|
||||||
|
worksheet.Cell(4, 1).Value = "third";
|
||||||
|
using var output = new MemoryStream();
|
||||||
|
workbook.SaveAs(output);
|
||||||
|
|
||||||
|
var rows = service.Parse(Schema, output.ToArray());
|
||||||
|
|
||||||
|
rows.Should().NotBeAssignableTo<IReadOnlyList<ExcelParsedRow>>();
|
||||||
|
rows.Take(1).Should().ContainSingle(x =>
|
||||||
|
x.RowNumber == 2 && x.JsonObject.Contains("first", StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
|
private const string Schema = """
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string", "title": "名称", "required": true, "x-component": "Input" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
}
|
||||||
@ -0,0 +1,89 @@
|
|||||||
|
using BuildFlow.Shared.Domain.Errors;
|
||||||
|
using BuildFlow.Shared.Domain.Exceptions;
|
||||||
|
using FluentAssertions;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using Workflow.Application.Form.Derivation;
|
||||||
|
using Workflow.Application.Form.Schema;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace BuildFlow.Workflow.Tests.Performance;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 预解析表单 Schema 在批量校验和派生中的复用测试。
|
||||||
|
/// </summary>
|
||||||
|
public class PreparedFormSchemaTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task PreparedSchemaCanBeReusedForValidationAndDerivation()
|
||||||
|
{
|
||||||
|
var prepared = PreparedFormSchema.Create(ComputedSchema);
|
||||||
|
var derivation = new FormValueDerivationService([], []);
|
||||||
|
|
||||||
|
var firstValidation = FormDataValidator.Validate(
|
||||||
|
prepared,
|
||||||
|
"""{"quantity":2,"unitPrice":10}""");
|
||||||
|
var first = await derivation.DeriveAsync(
|
||||||
|
prepared,
|
||||||
|
"""{"quantity":2,"unitPrice":10}""",
|
||||||
|
new FormDerivationContext(Guid.NewGuid(), Guid.NewGuid(), null),
|
||||||
|
CancellationToken.None);
|
||||||
|
var second = await derivation.DeriveAsync(
|
||||||
|
prepared,
|
||||||
|
"""{"quantity":3,"unitPrice":5}""",
|
||||||
|
new FormDerivationContext(Guid.NewGuid(), Guid.NewGuid(), null),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
firstValidation.IsValid.Should().BeTrue();
|
||||||
|
ReadTotal(first.DataJson).Should().Be(20);
|
||||||
|
ReadTotal(second.DataJson).Should().Be(15);
|
||||||
|
prepared.Validation.Fields.Should().HaveCount(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidSchemaCannotBePrepared()
|
||||||
|
{
|
||||||
|
var act = () => PreparedFormSchema.Create("""
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"supplier": {
|
||||||
|
"type": "object",
|
||||||
|
"x-component": "RelationPicker",
|
||||||
|
"x-field-kind": "relation",
|
||||||
|
"x-relation": { "idField": "id" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
var exception = act.Should().Throw<BusinessException>().Which;
|
||||||
|
exception.Code.Should().Be(ErrorCode.InvalidParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static decimal ReadTotal(string dataJson)
|
||||||
|
=> JsonNode.Parse(dataJson)!["total"]!.GetValue<decimal>();
|
||||||
|
|
||||||
|
private const string ComputedSchema = """
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"quantity": { "type": "number", "x-component": "InputNumber" },
|
||||||
|
"unitPrice": { "type": "number", "x-component": "InputNumber" },
|
||||||
|
"total": {
|
||||||
|
"type": "number",
|
||||||
|
"x-component": "InputNumber",
|
||||||
|
"x-field-kind": "computed",
|
||||||
|
"x-derived": {
|
||||||
|
"mode": "computed",
|
||||||
|
"writePolicy": "overwrite",
|
||||||
|
"expression": {
|
||||||
|
"operator": "multiply",
|
||||||
|
"left": { "type": "field", "path": "quantity" },
|
||||||
|
"right": { "type": "field", "path": "unitPrice" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
}
|
||||||
@ -201,7 +201,7 @@ public class ImportJobRecoveryTests(PostgresWorkflowFixture fixture)
|
|||||||
string formName,
|
string formName,
|
||||||
IEnumerable<string> dataJsonRows) => [];
|
IEnumerable<string> dataJsonRows) => [];
|
||||||
|
|
||||||
public IReadOnlyList<ExcelParsedRow> Parse(string schemaJson, byte[] xlsxBytes) => rows;
|
public IEnumerable<ExcelParsedRow> Parse(string schemaJson, byte[] xlsxBytes) => rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class CancellingRows(
|
private sealed class CancellingRows(
|
||||||
@ -220,7 +220,14 @@ public class ImportJobRecoveryTests(PostgresWorkflowFixture fixture)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerator<ExcelParsedRow> GetEnumerator() => rows.GetEnumerator();
|
public IEnumerator<ExcelParsedRow> GetEnumerator()
|
||||||
|
{
|
||||||
|
for (var index = 0; index < rows.Count; index++)
|
||||||
|
{
|
||||||
|
if (index == cancelAtIndex) cancel();
|
||||||
|
yield return rows[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
|
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
|
||||||
=> GetEnumerator();
|
=> GetEnumerator();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user