@reventlessdev/reventless-spec 3.0.0-alpha.144 → 3.0.0-alpha.145
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/components/DcbScopeInference.res +39 -1
- package/src/components/DcbScopeInference.res.mjs +20 -0
- package/src/components/DcbTag.res +68 -3
- package/src/components/DcbTag.res.mjs +76 -12
- package/src/components/DcbValidation.res +42 -1
- package/src/components/DcbValidation.res.mjs +19 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.0.0-alpha.145 (2026-09-24)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **dcb:** a command reference no decision reads by stays out of the query ([3ae149a](https://github.com/ReventlessDev/reventless-core/commit/3ae149ae91ebab63413be57df5827c5fd4390160))
|
|
11
|
+
|
|
12
|
+
|
|
6
13
|
# 3.0.0-alpha.144 (2026-09-23)
|
|
7
14
|
|
|
8
15
|
* feat(node)!: every command-line tool reads its arguments through parseArgs ([2e41979](https://github.com/ReventlessDev/reventless-core/commit/2e41979b73646c46356e197e17eb4a3da0154c73))
|
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@ a tag-metadata flag. That is precisely what we are replacing — so both the run
|
|
|
10
10
|
VS Code tooling (building shapes from parsed `.res` source) can feed the same
|
|
11
11
|
`infer`. See `docs/plans/done/dcb-tag-scope-inference.md` § "Phase 1 design".
|
|
12
12
|
|
|
13
|
-
The
|
|
13
|
+
The four rules (over the representation):
|
|
14
14
|
|
|
15
15
|
1. **Owner / partition.** A slice's partition key is the key its *own* emitted
|
|
16
16
|
events are identified by — computed as `producedKeys(S)` minus the keys `S`
|
|
@@ -33,6 +33,12 @@ The three rules (over the representation):
|
|
|
33
33
|
producing slice's own partition key. Foreign reference keys (e.g. `categoryId`
|
|
34
34
|
on `ProductAdded`) are payload ⇒ not indexed ⇒ the sibling-leak GSI write never
|
|
35
35
|
happens.
|
|
36
|
+
|
|
37
|
+
4. **Command scope.** A scalar key on a *command* is a query tag iff it is the
|
|
38
|
+
slice's partition or a key the slice reads off a foreign event (rule 2's
|
|
39
|
+
candidates). `ShipOrder({orderId, carrierId})` names the carrier as information:
|
|
40
|
+
AND-ing it into the clause would hide every order event that does not carry it,
|
|
41
|
+
so it is payload. See `commandPayloadKeys`.
|
|
36
42
|
*/
|
|
37
43
|
/** A field the slice graph treats as an entity identity.
|
|
38
44
|
|
|
@@ -55,6 +61,9 @@ type idField = {
|
|
|
55
61
|
byTag?: bool,
|
|
56
62
|
/** The tag key, when the field's type says it (an identity) rather than its name. */
|
|
57
63
|
key?: string,
|
|
64
|
+
/** The author asked for the tag (`@dcbTag`, `@partitionTag`, `@crossPartition`,
|
|
65
|
+
`@compositePartitionTag`), so a command keeps it in its query (rule 4). */
|
|
66
|
+
declared?: bool,
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
/** One variant arm: its constructor name and the `*Id` fields it carries. */
|
|
@@ -397,6 +406,35 @@ let crossPartitionForSlice = (s: sliceShape): array<string> => {
|
|
|
397
406
|
foreign->Array.filter(k => Some(k) != partition && scalar->Array.includes(k))
|
|
398
407
|
}
|
|
399
408
|
|
|
409
|
+
/**
|
|
410
|
+
Rule 4: the scalar command keys that stay out of the slice's decision query.
|
|
411
|
+
|
|
412
|
+
A key stays in when it is the partition, when the slice reads it off a foreign
|
|
413
|
+
event, when the boundary reads it across partitions, when the author declared it
|
|
414
|
+
on the command or on one of the slice's own arms (an M:N capacity read's
|
|
415
|
+
`@crossPartition` sits on the event), or when the command also carries it as a
|
|
416
|
+
list. Anything else only names another thing, and the fence is the partition.
|
|
417
|
+
*/
|
|
418
|
+
let commandPayloadKeys = (s: sliceShape, ~partition: string, ~crossPartition: array<string>): array<
|
|
419
|
+
string,
|
|
420
|
+
> => {
|
|
421
|
+
let foreign = foreignConsumedKeys(s)
|
|
422
|
+
let declared = (f: idField) => f.declared->Option.getOr(false)
|
|
423
|
+
let pinned =
|
|
424
|
+
s.command
|
|
425
|
+
->Array.filter(f => f.isList || declared(f))
|
|
426
|
+
->Array.concat(
|
|
427
|
+
Array.concat(s.consumed, s.produced)->Array.flatMap(e => e.idFields->Array.filter(declared)),
|
|
428
|
+
)
|
|
429
|
+
->Array.map(tagKeyOf)
|
|
430
|
+
commandScalarKeys(s)->Array.filter(k =>
|
|
431
|
+
k != partition &&
|
|
432
|
+
!(foreign->Array.includes(k)) &&
|
|
433
|
+
!(crossPartition->Array.includes(k)) &&
|
|
434
|
+
!(pinned->Array.includes(k))
|
|
435
|
+
)
|
|
436
|
+
}
|
|
437
|
+
|
|
400
438
|
let infer = (slices: array<sliceShape>): derived => {
|
|
401
439
|
let {partitionBySlice, ambiguities} = resolvePartitions(slices)
|
|
402
440
|
|
|
@@ -258,6 +258,25 @@ function crossPartitionForSlice(s) {
|
|
|
258
258
|
});
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
function commandPayloadKeys(s, partition, crossPartition) {
|
|
262
|
+
let foreign = foreignConsumedKeys(s);
|
|
263
|
+
let declared = f => Stdlib_Option.getOr(f.declared, false);
|
|
264
|
+
let pinned = s.command.filter(f => {
|
|
265
|
+
if (f.isList) {
|
|
266
|
+
return true;
|
|
267
|
+
} else {
|
|
268
|
+
return Stdlib_Option.getOr(f.declared, false);
|
|
269
|
+
}
|
|
270
|
+
}).concat(s.consumed.concat(s.produced).flatMap(e => e.idFields.filter(declared))).map(tagKeyOf);
|
|
271
|
+
return commandScalarKeys(s).filter(k => {
|
|
272
|
+
if (k !== partition && !foreign.includes(k) && !crossPartition.includes(k)) {
|
|
273
|
+
return !pinned.includes(k);
|
|
274
|
+
} else {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
261
280
|
function infer(slices) {
|
|
262
281
|
let match = resolvePartitions(slices);
|
|
263
282
|
let partitionBySlice = match.partitionBySlice;
|
|
@@ -332,6 +351,7 @@ export {
|
|
|
332
351
|
resolvePartitions,
|
|
333
352
|
partitionBlockers,
|
|
334
353
|
crossPartitionForSlice,
|
|
354
|
+
commandPayloadKeys,
|
|
335
355
|
infer,
|
|
336
356
|
}
|
|
337
357
|
/* No side effect */
|
|
@@ -126,6 +126,17 @@ let dcbTagKeyOverrideId: S.Metadata.Id.t<string> = S.Metadata.Id.make(
|
|
|
126
126
|
~name="tagKeyOverride",
|
|
127
127
|
)
|
|
128
128
|
|
|
129
|
+
/**
|
|
130
|
+
Internal sury metadata ID marking a tag the author wrote (`@dcbTag`) rather than
|
|
131
|
+
one the naming convention or the field's identity implied. A declared command tag
|
|
132
|
+
always stays in the decision query; an implied one the slice does not decide by
|
|
133
|
+
is payload (`DcbScopeInference.commandPayloadKeys`).
|
|
134
|
+
*/
|
|
135
|
+
let dcbDeclaredTagId: S.Metadata.Id.t<bool> = S.Metadata.Id.make(
|
|
136
|
+
~namespace="dcb",
|
|
137
|
+
~name="declaredTag",
|
|
138
|
+
)
|
|
139
|
+
|
|
129
140
|
/**
|
|
130
141
|
Type-preserving forms of the markers below, for a field whose schema is not a
|
|
131
142
|
bare `S.string` — an identity (`OrderId.schema->DcbTag.mark`) keeps its type and
|
|
@@ -143,6 +154,12 @@ let markPartition = (schema: S.t<'a>): S.t<'a> =>
|
|
|
143
154
|
let markCrossPartition = (schema: S.t<'a>): S.t<'a> =>
|
|
144
155
|
schema->mark->S.Metadata.set(~id=dcbCrossPartitionId, true)
|
|
145
156
|
|
|
157
|
+
let markDeclared = (schema: S.t<'a>): S.t<'a> =>
|
|
158
|
+
schema->mark->S.Metadata.set(~id=dcbDeclaredTagId, true)
|
|
159
|
+
|
|
160
|
+
let markDeclaredForKey = (schema: S.t<'a>, ~key: string): S.t<'a> =>
|
|
161
|
+
schema->markForKey(~key)->S.Metadata.set(~id=dcbDeclaredTagId, true)
|
|
162
|
+
|
|
146
163
|
/**
|
|
147
164
|
A sury string schema annotated as a DCB tag field.
|
|
148
165
|
|
|
@@ -190,6 +207,11 @@ annotations carrying a string payload.
|
|
|
190
207
|
*/
|
|
191
208
|
let stringForKey = (~key: string): S.t<string> => S.string->markForKey(~key)
|
|
192
209
|
|
|
210
|
+
/** What `@dcbTag` emits: `string` / `stringForKey`, marked as written by the author. */
|
|
211
|
+
let declared: S.t<string> = S.string->markDeclared
|
|
212
|
+
|
|
213
|
+
let declaredForKey = (~key: string): S.t<string> => S.string->markDeclaredForKey(~key)
|
|
214
|
+
|
|
193
215
|
/**
|
|
194
216
|
A sury int schema annotated as a DCB tag field.
|
|
195
217
|
Use with `@s.matches(DcbTag.int)` on integer fields that should be extracted as tags.
|
|
@@ -359,6 +381,14 @@ let isPartitionTag = (fieldSchema: S.t<unknown>) =>
|
|
|
359
381
|
let isCrossPartitionTag = (fieldSchema: S.t<unknown>) =>
|
|
360
382
|
S.Metadata.get(fieldSchema, ~id=dcbCrossPartitionId)->Option.isSome
|
|
361
383
|
|
|
384
|
+
/** Returns `true` if the author asked for the tag: `@dcbTag`, `@partitionTag`,
|
|
385
|
+
`@crossPartition` or `@compositePartitionTag`. */
|
|
386
|
+
let isDeclaredTag = (fieldSchema: S.t<unknown>) =>
|
|
387
|
+
S.Metadata.get(fieldSchema, ~id=dcbDeclaredTagId)->Option.isSome ||
|
|
388
|
+
isPartitionTag(fieldSchema) ||
|
|
389
|
+
isCrossPartitionTag(fieldSchema) ||
|
|
390
|
+
S.Metadata.get(fieldSchema, ~id=dcbCompositePartitionMemberId)->Option.isSome
|
|
391
|
+
|
|
362
392
|
/**
|
|
363
393
|
Returns `true` if the schema is an array whose item schema is a cross-partition
|
|
364
394
|
DCB tag (`array<@s.matches(DcbTag.crossPartition) string>`).
|
|
@@ -926,6 +956,10 @@ pairing matches nothing, so results are unchanged. A type that carries the tag
|
|
|
926
956
|
as a *secondary* tag is retained (a legitimate cross-partition read), and a type
|
|
927
957
|
absent from the map is kept (cannot be proven vacuous).
|
|
928
958
|
|
|
959
|
+
`~payloadTagKeys` (`commandPayloadTagKeys`) are command keys the slice does not
|
|
960
|
+
decide by; they are left out of every clause, so they narrow neither the read nor
|
|
961
|
+
the append condition.
|
|
962
|
+
|
|
929
963
|
@example
|
|
930
964
|
```rescript
|
|
931
965
|
// Single-entity command → single AND clause
|
|
@@ -953,20 +987,23 @@ let buildQueryFromCommand = (
|
|
|
953
987
|
~value: 'a,
|
|
954
988
|
~tagKeysByEventType: dict<array<string>>=Dict.make(),
|
|
955
989
|
~crossPartitionTagKeys: array<string>=[],
|
|
990
|
+
~payloadTagKeys: array<string>=[],
|
|
956
991
|
): query => {
|
|
957
992
|
let typesForTags = clauseTags =>
|
|
958
993
|
narrowEventTypesForTags(eventTypes, clauseTags, tagKeysByEventType)
|
|
994
|
+
let decidedBy = (tags: array<tag>) =>
|
|
995
|
+
tags->Array.filter(tag => !(payloadTagKeys->Array.includes(tag.key)))
|
|
959
996
|
if hasTaggedArrayFields(schema) {
|
|
960
997
|
// Array fields already fan out per element into single-tag clauses, so a
|
|
961
998
|
// cross-partition array tag is already its own clause (the adapter routes it
|
|
962
999
|
// by `crossPartitionTagKeys`).
|
|
963
|
-
let tags = extractTagsExpanded(schema, value)
|
|
1000
|
+
let tags = extractTagsExpanded(schema, value)->decidedBy
|
|
964
1001
|
tags->Array.map(tag => {
|
|
965
1002
|
let clauseTags = [{key: tag.key, value: tag.value}]
|
|
966
1003
|
{eventTypes: typesForTags(clauseTags), tags: clauseTags}
|
|
967
1004
|
})
|
|
968
1005
|
} else {
|
|
969
|
-
let tags = extractTags(schema, value)
|
|
1006
|
+
let tags = extractTags(schema, value)->decidedBy
|
|
970
1007
|
// When any command tag is cross-partition, fan every scalar tag out into its
|
|
971
1008
|
// own single-tag clause instead of AND-ing them into one composite
|
|
972
1009
|
// (exact-pair) clause. An M:N command (`SubscribeStudent({courseId, studentId})`)
|
|
@@ -1108,11 +1145,18 @@ let idFieldsOfProperties = (properties: dict<S.t<unknown>>): array<DcbScopeInfer
|
|
|
1108
1145
|
} else {
|
|
1109
1146
|
None
|
|
1110
1147
|
}
|
|
1148
|
+
let isDeclared = (fieldSchema: S.t<unknown>, isList) => {
|
|
1149
|
+
let valueSchema = switch (isList, fieldSchema) {
|
|
1150
|
+
| (true, Array({additionalItems: Schema(item)})) => item
|
|
1151
|
+
| _ => fieldSchema
|
|
1152
|
+
}
|
|
1153
|
+
valueSchema->Semantic.unwrapOptional->Option.getOr(valueSchema)->isDeclaredTag
|
|
1154
|
+
}
|
|
1111
1155
|
let identity = (name, fieldSchema, isList) =>
|
|
1112
1156
|
switch typedKey(fieldSchema, isList) {
|
|
1113
1157
|
| Some(key) => Some({DcbScopeInference.name, isList, key})
|
|
1114
1158
|
| None => untypedIdentity(name, fieldSchema, isList)
|
|
1115
|
-
}
|
|
1159
|
+
}->Option.map(f => isDeclared(fieldSchema, isList) ? {...f, declared: true} : f)
|
|
1116
1160
|
properties
|
|
1117
1161
|
->Dict.toArray
|
|
1118
1162
|
->Array.flatMap(((name, fieldSchema)) => {
|
|
@@ -1337,6 +1381,27 @@ let deriveEffectiveScope = (slices: array<sliceSchemas>): effectiveScope => {
|
|
|
1337
1381
|
}
|
|
1338
1382
|
}
|
|
1339
1383
|
|
|
1384
|
+
/**
|
|
1385
|
+
The command tag keys a slice's decision query leaves out (rule 4 of
|
|
1386
|
+
`DcbScopeInference`), given the slice's partition within its boundary and the
|
|
1387
|
+
boundary's cross-partition keys. Empty unless the partition is a single key: a
|
|
1388
|
+
composite boundary keeps every tag it has.
|
|
1389
|
+
*/
|
|
1390
|
+
let commandPayloadTagKeys = (
|
|
1391
|
+
shape: DcbScopeInference.sliceShape,
|
|
1392
|
+
~partitionTag: option<derivedPartitionTag>,
|
|
1393
|
+
~crossPartitionTagKeys: array<string>,
|
|
1394
|
+
): array<string> =>
|
|
1395
|
+
switch partitionTag {
|
|
1396
|
+
| Some(Simple({key})) =>
|
|
1397
|
+
DcbScopeInference.commandPayloadKeys(
|
|
1398
|
+
shape,
|
|
1399
|
+
~partition=key,
|
|
1400
|
+
~crossPartition=crossPartitionTagKeys,
|
|
1401
|
+
)
|
|
1402
|
+
| _ => []
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1340
1405
|
// --- Composite partition key helpers ---
|
|
1341
1406
|
|
|
1342
1407
|
type compositePartitionFieldInfo = {name: string, position: int, sep: string}
|
|
@@ -32,6 +32,8 @@ let dcbCompositePartitionMemberId = Sury.$Metadata_Id_make("dcb", "compositePart
|
|
|
32
32
|
|
|
33
33
|
let dcbTagKeyOverrideId = Sury.$Metadata_Id_make("dcb", "tagKeyOverride");
|
|
34
34
|
|
|
35
|
+
let dcbDeclaredTagId = Sury.$Metadata_Id_make("dcb", "declaredTag");
|
|
36
|
+
|
|
35
37
|
function mark(schema) {
|
|
36
38
|
return Sury.$Metadata_set(schema, dcbTagId, true);
|
|
37
39
|
}
|
|
@@ -48,12 +50,26 @@ function markCrossPartition(schema) {
|
|
|
48
50
|
return Sury.$Metadata_set(Sury.$Metadata_set(schema, dcbTagId, true), dcbCrossPartitionId, true);
|
|
49
51
|
}
|
|
50
52
|
|
|
53
|
+
function markDeclared(schema) {
|
|
54
|
+
return Sury.$Metadata_set(Sury.$Metadata_set(schema, dcbTagId, true), dcbDeclaredTagId, true);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function markDeclaredForKey(schema, key) {
|
|
58
|
+
return Sury.$Metadata_set(markForKey(schema, key), dcbDeclaredTagId, true);
|
|
59
|
+
}
|
|
60
|
+
|
|
51
61
|
let string = Sury.$Metadata_set(Sury.string, dcbTagId, true);
|
|
52
62
|
|
|
53
63
|
function stringForKey(key) {
|
|
54
64
|
return markForKey(Sury.string, key);
|
|
55
65
|
}
|
|
56
66
|
|
|
67
|
+
let declared = markDeclared(Sury.string);
|
|
68
|
+
|
|
69
|
+
function declaredForKey(key) {
|
|
70
|
+
return markDeclaredForKey(Sury.string, key);
|
|
71
|
+
}
|
|
72
|
+
|
|
57
73
|
let int = Sury.$Metadata_set(Sury.int32, dcbTagId, true);
|
|
58
74
|
|
|
59
75
|
let partition = markPartition(Sury.string);
|
|
@@ -172,6 +188,14 @@ function isCrossPartitionTag(fieldSchema) {
|
|
|
172
188
|
return Stdlib_Option.isSome(Sury.$Metadata_get(fieldSchema, dcbCrossPartitionId));
|
|
173
189
|
}
|
|
174
190
|
|
|
191
|
+
function isDeclaredTag(fieldSchema) {
|
|
192
|
+
if (Stdlib_Option.isSome(Sury.$Metadata_get(fieldSchema, dcbDeclaredTagId)) || Stdlib_Option.isSome(Sury.$Metadata_get(fieldSchema, dcbPartitionTagId)) || Stdlib_Option.isSome(Sury.$Metadata_get(fieldSchema, dcbCrossPartitionId))) {
|
|
193
|
+
return true;
|
|
194
|
+
} else {
|
|
195
|
+
return Stdlib_Option.isSome(Sury.$Metadata_get(fieldSchema, dcbCompositePartitionMemberId));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
175
199
|
function isCrossPartitionTaggedArray(fieldSchema) {
|
|
176
200
|
if (fieldSchema.type !== "array") {
|
|
177
201
|
return false;
|
|
@@ -610,11 +634,13 @@ function narrowEventTypesForTags(eventTypes, tags, tagKeysByEventType) {
|
|
|
610
634
|
});
|
|
611
635
|
}
|
|
612
636
|
|
|
613
|
-
function buildQueryFromCommand(eventTypes, schema, value, tagKeysByEventTypeOpt, crossPartitionTagKeysOpt) {
|
|
637
|
+
function buildQueryFromCommand(eventTypes, schema, value, tagKeysByEventTypeOpt, crossPartitionTagKeysOpt, payloadTagKeysOpt) {
|
|
614
638
|
let tagKeysByEventType = tagKeysByEventTypeOpt !== undefined ? tagKeysByEventTypeOpt : ({});
|
|
615
639
|
let crossPartitionTagKeys = crossPartitionTagKeysOpt !== undefined ? crossPartitionTagKeysOpt : [];
|
|
640
|
+
let payloadTagKeys = payloadTagKeysOpt !== undefined ? payloadTagKeysOpt : [];
|
|
641
|
+
let decidedBy = tags => tags.filter(tag => !payloadTagKeys.includes(tag.key));
|
|
616
642
|
if (hasTaggedArrayFields(schema)) {
|
|
617
|
-
let tags = extractTagsExpanded(schema, value);
|
|
643
|
+
let tags = decidedBy(extractTagsExpanded(schema, value));
|
|
618
644
|
return tags.map(tag => {
|
|
619
645
|
let clauseTags = [{
|
|
620
646
|
key: tag.key,
|
|
@@ -626,7 +652,7 @@ function buildQueryFromCommand(eventTypes, schema, value, tagKeysByEventTypeOpt,
|
|
|
626
652
|
};
|
|
627
653
|
});
|
|
628
654
|
}
|
|
629
|
-
let tags$1 = extractTags(schema, value);
|
|
655
|
+
let tags$1 = decidedBy(extractTags(schema, value));
|
|
630
656
|
let hasCrossPartition = tags$1.some(tag => crossPartitionTagKeys.includes(tag.key));
|
|
631
657
|
if (hasCrossPartition && tags$1.length > 1) {
|
|
632
658
|
return tags$1.map(tag => {
|
|
@@ -735,15 +761,8 @@ function idFieldsOfProperties(properties) {
|
|
|
735
761
|
}
|
|
736
762
|
return Stdlib_Option.map(Semantic$Reventless.identityKey(valueSchema), param => resolveTagKey("", valueSchema));
|
|
737
763
|
};
|
|
738
|
-
let
|
|
739
|
-
|
|
740
|
-
if (key !== undefined) {
|
|
741
|
-
return {
|
|
742
|
-
name: name,
|
|
743
|
-
isList: isList,
|
|
744
|
-
key: key
|
|
745
|
-
};
|
|
746
|
-
} else if (isIdName(name)) {
|
|
764
|
+
let untypedIdentity = (name, fieldSchema, isList) => {
|
|
765
|
+
if (isIdName(name)) {
|
|
747
766
|
return {
|
|
748
767
|
name: name,
|
|
749
768
|
isList: isList
|
|
@@ -758,6 +777,31 @@ function idFieldsOfProperties(properties) {
|
|
|
758
777
|
return;
|
|
759
778
|
}
|
|
760
779
|
};
|
|
780
|
+
let isDeclared = (fieldSchema, isList) => {
|
|
781
|
+
let valueSchema;
|
|
782
|
+
if (isList && fieldSchema.type === "array") {
|
|
783
|
+
let item = fieldSchema.additionalItems;
|
|
784
|
+
valueSchema = item === "strip" || item === "strict" ? fieldSchema : item;
|
|
785
|
+
} else {
|
|
786
|
+
valueSchema = fieldSchema;
|
|
787
|
+
}
|
|
788
|
+
return isDeclaredTag(Stdlib_Option.getOr(Semantic$Reventless.unwrapOptional(valueSchema), valueSchema));
|
|
789
|
+
};
|
|
790
|
+
let identity = (name, fieldSchema, isList) => {
|
|
791
|
+
let key = typedKey(fieldSchema, isList);
|
|
792
|
+
return Stdlib_Option.map(key !== undefined ? ({
|
|
793
|
+
name: name,
|
|
794
|
+
isList: isList,
|
|
795
|
+
key: key
|
|
796
|
+
}) : untypedIdentity(name, fieldSchema, isList), f => {
|
|
797
|
+
if (!isDeclared(fieldSchema, isList)) {
|
|
798
|
+
return f;
|
|
799
|
+
}
|
|
800
|
+
let newrecord = {...f};
|
|
801
|
+
newrecord.declared = true;
|
|
802
|
+
return newrecord;
|
|
803
|
+
});
|
|
804
|
+
};
|
|
761
805
|
return Object.entries(properties).flatMap(param => {
|
|
762
806
|
let fieldSchema = param[1];
|
|
763
807
|
let isList;
|
|
@@ -895,6 +939,19 @@ function deriveEffectiveScope(slices) {
|
|
|
895
939
|
};
|
|
896
940
|
}
|
|
897
941
|
|
|
942
|
+
function commandPayloadTagKeys(shape, partitionTag, crossPartitionTagKeys) {
|
|
943
|
+
if (partitionTag === undefined) {
|
|
944
|
+
return [];
|
|
945
|
+
}
|
|
946
|
+
switch (partitionTag.TAG) {
|
|
947
|
+
case "Simple" :
|
|
948
|
+
return DcbScopeInference$Reventless.commandPayloadKeys(shape, partitionTag._0.key, crossPartitionTagKeys);
|
|
949
|
+
case "Composite" :
|
|
950
|
+
case "ByEventType" :
|
|
951
|
+
return [];
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
898
955
|
function extractCompositePartitionFieldsFromProperties(properties) {
|
|
899
956
|
return Stdlib_Array.filterMap(Object.entries(properties), param => {
|
|
900
957
|
let meta = Sury.$Metadata_get(param[1], dcbCompositePartitionMemberId);
|
|
@@ -1084,12 +1141,17 @@ export {
|
|
|
1084
1141
|
dcbCrossPartitionId,
|
|
1085
1142
|
dcbCompositePartitionMemberId,
|
|
1086
1143
|
dcbTagKeyOverrideId,
|
|
1144
|
+
dcbDeclaredTagId,
|
|
1087
1145
|
mark,
|
|
1088
1146
|
markForKey,
|
|
1089
1147
|
markPartition,
|
|
1090
1148
|
markCrossPartition,
|
|
1149
|
+
markDeclared,
|
|
1150
|
+
markDeclaredForKey,
|
|
1091
1151
|
string,
|
|
1092
1152
|
stringForKey,
|
|
1153
|
+
declared,
|
|
1154
|
+
declaredForKey,
|
|
1093
1155
|
int,
|
|
1094
1156
|
partition,
|
|
1095
1157
|
crossPartition,
|
|
@@ -1105,6 +1167,7 @@ export {
|
|
|
1105
1167
|
hasNestedTags,
|
|
1106
1168
|
isPartitionTag,
|
|
1107
1169
|
isCrossPartitionTag,
|
|
1170
|
+
isDeclaredTag,
|
|
1108
1171
|
isCrossPartitionTaggedArray,
|
|
1109
1172
|
resolveTagKey,
|
|
1110
1173
|
resolveArrayTagKey,
|
|
@@ -1140,6 +1203,7 @@ export {
|
|
|
1140
1203
|
sliceShapeFromSchemas,
|
|
1141
1204
|
sliceShape,
|
|
1142
1205
|
deriveEffectiveScope,
|
|
1206
|
+
commandPayloadTagKeys,
|
|
1143
1207
|
extractCompositePartitionFieldsFromProperties,
|
|
1144
1208
|
extractCompositePartitionFields,
|
|
1145
1209
|
getCompositePartitionKeyValue,
|
|
@@ -170,20 +170,24 @@ exact-match trap. Returns one `validationError` (warning) per offending
|
|
|
170
170
|
|
|
171
171
|
@param slices `(sliceName, commandSchema, consumedEventSchema)` per consumer.
|
|
172
172
|
@param producedTagKeys event type → its produced tag-key set (from the event-log schema).
|
|
173
|
+
@param payloadTagKeysBySlice slice → the command keys its query leaves out
|
|
174
|
+
(`DcbTag.commandPayloadTagKeys`), which take no part in a composite read.
|
|
173
175
|
*/
|
|
174
176
|
let validateCompositeReads = (
|
|
175
177
|
~slices: array<(string, S.t<unknown>, S.t<unknown>)>,
|
|
176
178
|
~producedTagKeys: dict<array<string>>,
|
|
179
|
+
~payloadTagKeysBySlice: dict<array<string>>=Dict.make(),
|
|
177
180
|
): array<validationError> => {
|
|
178
181
|
let warnings: array<validationError> = []
|
|
179
182
|
slices->Array.forEach(((sliceName, commandSchema, consumedSchema)) =>
|
|
180
183
|
if !DcbTag.hasTaggedArrayFields(commandSchema) {
|
|
181
184
|
let commandTagKeysByVariant = DcbTag.extractTagKeysByEventType(commandSchema)
|
|
182
185
|
let consumedTypes = DcbTag.extractVariantNames(consumedSchema)
|
|
186
|
+
let payload = payloadTagKeysBySlice->Dict.get(sliceName)->Option.getOr([])
|
|
183
187
|
commandTagKeysByVariant
|
|
184
188
|
->Dict.valuesToArray
|
|
185
189
|
->Array.forEach(cmdKeys => {
|
|
186
|
-
let querySet = dedupeKeys(cmdKeys)
|
|
190
|
+
let querySet = dedupeKeys(cmdKeys)->Array.filter(k => !(payload->Array.includes(k)))
|
|
187
191
|
if querySet->Array.length >= 2 {
|
|
188
192
|
consumedTypes->Array.forEach(
|
|
189
193
|
consumedType =>
|
|
@@ -564,3 +568,40 @@ let validatePartitionHintsVsInference = (
|
|
|
564
568
|
)
|
|
565
569
|
{contradictions, redundancies}
|
|
566
570
|
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
Finds `@noDcbTag` on command fields whose key the slice's query leaves out anyway
|
|
574
|
+
(rule 4 of `DcbScopeInference`). Such a reference is payload without the
|
|
575
|
+
annotation, so the annotation can go.
|
|
576
|
+
|
|
577
|
+
The PPX consumes the attribute, so the caller reads the suppressed fields off the
|
|
578
|
+
source. A field the shape does not know (a name that is no identity) is skipped.
|
|
579
|
+
|
|
580
|
+
@param suppressedBySlice slice → the command fields written `@noDcbTag`.
|
|
581
|
+
@param payloadTagKeysBySlice slice → `DcbTag.commandPayloadTagKeys`.
|
|
582
|
+
*/
|
|
583
|
+
let validateCommandTagSuppressions = (
|
|
584
|
+
~shapes: array<DcbScopeInference.sliceShape>,
|
|
585
|
+
~suppressedBySlice: dict<array<string>>,
|
|
586
|
+
~payloadTagKeysBySlice: dict<array<string>>,
|
|
587
|
+
): array<validationError> =>
|
|
588
|
+
shapes->Array.flatMap(s => {
|
|
589
|
+
let payload = payloadTagKeysBySlice->Dict.get(s.sliceName)->Option.getOr([])
|
|
590
|
+
suppressedBySlice
|
|
591
|
+
->Dict.get(s.sliceName)
|
|
592
|
+
->Option.getOr([])
|
|
593
|
+
->Array.filterMap(field =>
|
|
594
|
+
s.command
|
|
595
|
+
->Array.find(f => f.name == field)
|
|
596
|
+
->Option.map(DcbScopeInference.tagKeyOf)
|
|
597
|
+
->Option.flatMap(
|
|
598
|
+
key =>
|
|
599
|
+
payload->Array.includes(key)
|
|
600
|
+
? Some({
|
|
601
|
+
sliceName: s.sliceName,
|
|
602
|
+
message: `@noDcbTag ${field} is what inference derives without it — ${s.sliceName} decides nothing by ${key}, so it stays out of the query anyway. The annotation is redundant and can be removed.`,
|
|
603
|
+
})
|
|
604
|
+
: None,
|
|
605
|
+
)
|
|
606
|
+
)
|
|
607
|
+
})
|
|
@@ -191,7 +191,8 @@ function dedupeKeys(keys) {
|
|
|
191
191
|
});
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
function validateCompositeReads(slices, producedTagKeys) {
|
|
194
|
+
function validateCompositeReads(slices, producedTagKeys, payloadTagKeysBySliceOpt) {
|
|
195
|
+
let payloadTagKeysBySlice = payloadTagKeysBySliceOpt !== undefined ? payloadTagKeysBySliceOpt : ({});
|
|
195
196
|
let warnings = [];
|
|
196
197
|
slices.forEach(param => {
|
|
197
198
|
let commandSchema = param[1];
|
|
@@ -201,8 +202,9 @@ function validateCompositeReads(slices, producedTagKeys) {
|
|
|
201
202
|
}
|
|
202
203
|
let commandTagKeysByVariant = DcbTag$Reventless.extractTagKeysByEventType(commandSchema);
|
|
203
204
|
let consumedTypes = DcbTag$Reventless.extractVariantNames(param[2]);
|
|
205
|
+
let payload = Stdlib_Option.getOr(payloadTagKeysBySlice[sliceName], []);
|
|
204
206
|
Object.values(commandTagKeysByVariant).forEach(cmdKeys => {
|
|
205
|
-
let querySet = dedupeKeys(cmdKeys);
|
|
207
|
+
let querySet = dedupeKeys(cmdKeys).filter(k => !payload.includes(k));
|
|
206
208
|
if (querySet.length >= 2) {
|
|
207
209
|
consumedTypes.forEach(consumedType => {
|
|
208
210
|
let producedKeys = producedTagKeys[consumedType];
|
|
@@ -491,6 +493,20 @@ function validatePartitionHintsVsInference(shapes) {
|
|
|
491
493
|
};
|
|
492
494
|
}
|
|
493
495
|
|
|
496
|
+
function validateCommandTagSuppressions(shapes, suppressedBySlice, payloadTagKeysBySlice) {
|
|
497
|
+
return shapes.flatMap(s => {
|
|
498
|
+
let payload = Stdlib_Option.getOr(payloadTagKeysBySlice[s.sliceName], []);
|
|
499
|
+
return Stdlib_Array.filterMap(Stdlib_Option.getOr(suppressedBySlice[s.sliceName], []), field => Stdlib_Option.flatMap(Stdlib_Option.map(s.command.find(f => f.name === field), DcbScopeInference$Reventless.tagKeyOf), key => {
|
|
500
|
+
if (payload.includes(key)) {
|
|
501
|
+
return {
|
|
502
|
+
sliceName: s.sliceName,
|
|
503
|
+
message: `@noDcbTag ` + field + ` is what inference derives without it — ` + s.sliceName + ` decides nothing by ` + key + `, so it stays out of the query anyway. The annotation is redundant and can be removed.`
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
}));
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
494
510
|
export {
|
|
495
511
|
extractVariantInfo,
|
|
496
512
|
extractAllVariants,
|
|
@@ -505,5 +521,6 @@ export {
|
|
|
505
521
|
validateCrossPartitionScope,
|
|
506
522
|
validateScopeVsInference,
|
|
507
523
|
validatePartitionHintsVsInference,
|
|
524
|
+
validateCommandTagSuppressions,
|
|
508
525
|
}
|
|
509
526
|
/* DcbTag-Reventless Not a pure module */
|