forked from votrongdao/FlowX
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRollupCapabilities.cs
More file actions
104 lines (90 loc) · 4 KB
/
Copy pathRollupCapabilities.cs
File metadata and controls
104 lines (90 loc) · 4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
using FlowX;
namespace Crm;
/// <summary>
/// Declares a field whose value is an aggregate over a parent's children.
/// </summary>
/// <remarks>
/// <para>
/// <strong>Every check here is a publish-time check</strong>, and each one exists because the
/// alternative is an aggregate that is silently always zero. A roll-up whose field belongs to the
/// wrong object walks an edge no child of that parent is on; one whose source belongs to the
/// wrong object reads a key no child has. Both read exactly like a parent that genuinely has no
/// children, which is the worst kind of wrong number — nobody investigates a zero.
/// </para>
/// <para>
/// <strong><c>crm.admin</c>, and it also makes the field read-only.</strong> Declaring a roll-up
/// takes a field away from every writer, which is a bigger act than writing one.
/// </para>
/// </remarks>
[Capability("crm.custom.define_rollup", Version = "1.0.0",
Authorization = Authorization.Permission, Permission = "crm.admin",
Idempotent = true)]
public sealed class DefineCustomRollup : ICapability<DefineRollup, RollupDefined>
{
private readonly CustomSchemaStore _schema;
private readonly RollupStore _rollups;
/// <summary>Creates the capability.</summary>
/// <param name="schema">Reads the relationship's two ends.</param>
/// <param name="rollups">Reads what a field belongs to, and writes the declaration.</param>
/// <exception cref="ArgumentNullException">Any argument is null.</exception>
public DefineCustomRollup(CustomSchemaStore schema, RollupStore rollups)
{
ArgumentNullException.ThrowIfNull(schema);
ArgumentNullException.ThrowIfNull(rollups);
_schema = schema;
_rollups = rollups;
}
/// <inheritdoc />
public async ValueTask<Result<RollupDefined>> ExecuteAsync(
DefineRollup input,
CapabilityContext ctx,
CancellationToken ct)
{
ArgumentNullException.ThrowIfNull(input);
ArgumentNullException.ThrowIfNull(ctx);
if ((input.Aggregate == RollupAggregate.Count) != (input.SourceField is null))
{
return Result.Fail<RollupDefined>(
RollupErrors.SourceDoesNotMatchTheAggregate(input.Aggregate));
}
if (await _schema.ReadRelationshipAsync(ctx.TenantId, input.Relationship, ct)
.ConfigureAwait(false) is not { } edge)
{
return Result.Fail<RollupDefined>(
CustomSchemaErrors.RelationshipNotFound(input.Relationship));
}
if (await _rollups.FieldOwnerAsync(ctx.TenantId, input.Field, ct).ConfigureAwait(false)
is not { } field)
{
return Result.Fail<RollupDefined>(RollupErrors.FieldNotFound(input.Field));
}
if (field.IsComputed)
{
return Result.Fail<RollupDefined>(RollupErrors.FieldIsAlreadyComputed(input.Field));
}
// `from` is the parent. Migration 0006's cardinality trigger is what makes that true —
// it constrains the `to` end to one link — and the roll-up has to agree with it.
if (field.Object != edge.From)
{
return Result.Fail<RollupDefined>(RollupErrors.FieldIsNotOnTheParent(input.Field));
}
if (input.SourceField is { } source)
{
if (await _rollups.FieldOwnerAsync(ctx.TenantId, source, ct).ConfigureAwait(false)
is not { } sourceField)
{
return Result.Fail<RollupDefined>(RollupErrors.FieldNotFound(source));
}
if (sourceField.Object != edge.To)
{
return Result.Fail<RollupDefined>(RollupErrors.SourceIsNotOnTheChild(source));
}
}
var id = await _rollups
.DeclareAsync(ctx.TenantId, ctx.NewId(), input, ctx.UtcNow, ct)
.ConfigureAwait(false);
return id is null
? Result.Fail<RollupDefined>(RollupErrors.FieldIsAlreadyComputed(input.Field))
: Result.Ok(new RollupDefined(id.Value, input.Field));
}
}