@reventlessdev/reventless-spec 3.0.0-alpha.138 → 3.0.0-alpha.139
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 +8 -0
- package/package.json +1 -1
- package/src/components/DcbScopeInference.res +12 -3
- package/src/components/DcbScopeInference.res.mjs +3 -1
- package/src/components/DcbTag.res +64 -49
- package/src/components/DcbTag.res.mjs +72 -27
- package/src/components/Reference.res +26 -0
- package/src/components/Reference.res.mjs +31 -0
- package/src/semantic/Semantic.res +17 -1
- package/src/semantic/Semantic.res.mjs +22 -1
- package/src/types/Id.res +34 -4
- package/src/types/Id.res.mjs +17 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
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.139 (2026-09-21)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **spec:** one Id type per identity ([203be38](https://github.com/ReventlessDev/reventless-core/commit/203be389b9047db0a7a0294fe3c1c04ba5119965))
|
|
11
|
+
* **spec:** the runtime reads identities ([417c5f6](https://github.com/ReventlessDev/reventless-core/commit/417c5f613b031a08c7849a81c75de6a8bbeb841b))
|
|
12
|
+
|
|
13
|
+
|
|
6
14
|
# 3.0.0-alpha.138 (2026-09-20)
|
|
7
15
|
|
|
8
16
|
* fix(spec)!: let an explicit partition tag declare an identity the name cannot ([01d82c0](https://github.com/ReventlessDev/reventless-core/commit/01d82c0714a31f31d55c466eae9100cf4dcc85f0))
|
package/package.json
CHANGED
|
@@ -49,7 +49,13 @@ The three rules (over the representation):
|
|
|
49
49
|
It is carried rather than folded in because removing the annotation removes
|
|
50
50
|
the identity, which is exactly what the redundancy check has to know: a hint
|
|
51
51
|
inference cannot reach without it is never redundant. */
|
|
52
|
-
type idField = {
|
|
52
|
+
type idField = {
|
|
53
|
+
name: string,
|
|
54
|
+
isList: bool,
|
|
55
|
+
byTag?: bool,
|
|
56
|
+
/** The tag key, when the field's type says it (an identity) rather than its name. */
|
|
57
|
+
key?: string,
|
|
58
|
+
}
|
|
53
59
|
|
|
54
60
|
/** One variant arm: its constructor name and the `*Id` fields it carries. */
|
|
55
61
|
type eventShape = {eventType: string, idFields: array<idField>}
|
|
@@ -97,10 +103,13 @@ type derived = {
|
|
|
97
103
|
/**
|
|
98
104
|
The tag key for a `*Id`-shaped field. A plural `*Ids: array<string>` shares the
|
|
99
105
|
singular producer's key (trailing `s` stripped — `productIds` -> `productId`);
|
|
100
|
-
a scalar `*Id` uses the field name verbatim. Mirrors the PPX's `*Ids` rule.
|
|
106
|
+
a scalar `*Id` uses the field name verbatim. Mirrors the PPX's `*Ids` rule. An
|
|
107
|
+
identity-typed field carries its key, whatever it is called.
|
|
101
108
|
*/
|
|
102
109
|
let tagKeyOf = (f: idField): string =>
|
|
103
|
-
if f.
|
|
110
|
+
if f.key->Option.isSome {
|
|
111
|
+
f.key->Option.getUnsafe
|
|
112
|
+
} else if f.isList && f.name->String.endsWith("s") {
|
|
104
113
|
f.name->String.slice(~start=0, ~end=f.name->String.length - 1)
|
|
105
114
|
} else {
|
|
106
115
|
f.name
|
|
@@ -6,7 +6,9 @@ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js
|
|
|
6
6
|
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
7
7
|
|
|
8
8
|
function tagKeyOf(f) {
|
|
9
|
-
if (
|
|
9
|
+
if (Stdlib_Option.isSome(f.key)) {
|
|
10
|
+
return f.key;
|
|
11
|
+
} else if (f.isList && f.name.endsWith("s")) {
|
|
10
12
|
return f.name.slice(0, f.name.length - 1 | 0);
|
|
11
13
|
} else {
|
|
12
14
|
return f.name;
|
|
@@ -126,6 +126,23 @@ let dcbTagKeyOverrideId: S.Metadata.Id.t<string> = S.Metadata.Id.make(
|
|
|
126
126
|
~name="tagKeyOverride",
|
|
127
127
|
)
|
|
128
128
|
|
|
129
|
+
/**
|
|
130
|
+
Type-preserving forms of the markers below, for a field whose schema is not a
|
|
131
|
+
bare `S.string` — an identity (`OrderId.schema->DcbTag.mark`) keeps its type and
|
|
132
|
+
its semantic, and the tag key follows the identity unless `markForKey` says
|
|
133
|
+
otherwise.
|
|
134
|
+
*/
|
|
135
|
+
let mark = (schema: S.t<'a>): S.t<'a> => schema->S.Metadata.set(~id=dcbTagId, true)
|
|
136
|
+
|
|
137
|
+
let markForKey = (schema: S.t<'a>, ~key: string): S.t<'a> =>
|
|
138
|
+
schema->mark->S.Metadata.set(~id=dcbTagKeyOverrideId, key)
|
|
139
|
+
|
|
140
|
+
let markPartition = (schema: S.t<'a>): S.t<'a> =>
|
|
141
|
+
schema->mark->S.Metadata.set(~id=dcbPartitionTagId, true)
|
|
142
|
+
|
|
143
|
+
let markCrossPartition = (schema: S.t<'a>): S.t<'a> =>
|
|
144
|
+
schema->mark->S.Metadata.set(~id=dcbCrossPartitionId, true)
|
|
145
|
+
|
|
129
146
|
/**
|
|
130
147
|
A sury string schema annotated as a DCB tag field.
|
|
131
148
|
|
|
@@ -148,7 +165,7 @@ and array element types.
|
|
|
148
165
|
```
|
|
149
166
|
*/
|
|
150
167
|
let string: S.t<string> =
|
|
151
|
-
S.string->
|
|
168
|
+
S.string->mark
|
|
152
169
|
|
|
153
170
|
/**
|
|
154
171
|
A sury string schema annotated as a DCB tag field with an explicit tag-key override.
|
|
@@ -171,8 +188,7 @@ annotations carrying a string payload.
|
|
|
171
188
|
// productIds: ["p1", "p2"] → tags [{key: "productId", value: "p1"}, {key: "productId", value: "p2"}]
|
|
172
189
|
```
|
|
173
190
|
*/
|
|
174
|
-
let stringForKey = (~key: string): S.t<string> =>
|
|
175
|
-
S.string->S.Metadata.set(~id=dcbTagId, true)->S.Metadata.set(~id=dcbTagKeyOverrideId, key)
|
|
191
|
+
let stringForKey = (~key: string): S.t<string> => S.string->markForKey(~key)
|
|
176
192
|
|
|
177
193
|
/**
|
|
178
194
|
A sury int schema annotated as a DCB tag field.
|
|
@@ -198,7 +214,7 @@ optional (auto-selected) when only one tagged field exists.
|
|
|
198
214
|
```
|
|
199
215
|
*/
|
|
200
216
|
let partition: S.t<string> =
|
|
201
|
-
S.string->
|
|
217
|
+
S.string->markPartition
|
|
202
218
|
|
|
203
219
|
/**
|
|
204
220
|
A sury string schema annotated as a DCB tag field with *cross-partition* read
|
|
@@ -224,7 +240,7 @@ entities but can be partitioned by only one (course-subscription capacity,
|
|
|
224
240
|
```
|
|
225
241
|
*/
|
|
226
242
|
let crossPartition: S.t<string> =
|
|
227
|
-
S.string->
|
|
243
|
+
S.string->markCrossPartition
|
|
228
244
|
|
|
229
245
|
/**
|
|
230
246
|
A sury string schema marking a field as a composite partition key member.
|
|
@@ -355,19 +371,21 @@ let isCrossPartitionTaggedArray = (fieldSchema: S.t<unknown>) =>
|
|
|
355
371
|
|
|
356
372
|
/**
|
|
357
373
|
Resolves the tag key for a scalar tagged field: the explicit override metadata if
|
|
358
|
-
present, otherwise the field name.
|
|
374
|
+
present, else the field's identity, otherwise the field name.
|
|
359
375
|
*/
|
|
360
376
|
let resolveTagKey = (fieldName: string, fieldSchema: S.t<unknown>): string =>
|
|
361
|
-
S.Metadata.get(fieldSchema, ~id=dcbTagKeyOverrideId)
|
|
377
|
+
switch S.Metadata.get(fieldSchema, ~id=dcbTagKeyOverrideId) {
|
|
378
|
+
| Some(key) => key
|
|
379
|
+
| None => fieldSchema->Semantic.identityKey->Option.getOr(fieldName)
|
|
380
|
+
}
|
|
362
381
|
|
|
363
382
|
/**
|
|
364
|
-
Resolves the tag key for an array tagged field. The override metadata
|
|
365
|
-
inner element schema; falls back to the field name
|
|
383
|
+
Resolves the tag key for an array tagged field. The override metadata and the
|
|
384
|
+
identity sit on the inner element schema; falls back to the field name.
|
|
366
385
|
*/
|
|
367
386
|
let resolveArrayTagKey = (fieldName: string, fieldSchema: S.t<unknown>): string =>
|
|
368
387
|
switch fieldSchema {
|
|
369
|
-
| Array({additionalItems: Schema(itemSchema)}) =>
|
|
370
|
-
S.Metadata.get(itemSchema, ~id=dcbTagKeyOverrideId)->Option.getOr(fieldName)
|
|
388
|
+
| Array({additionalItems: Schema(itemSchema)}) => resolveTagKey(fieldName, itemSchema)
|
|
371
389
|
| _ => fieldName
|
|
372
390
|
}
|
|
373
391
|
|
|
@@ -970,51 +988,33 @@ let buildQueryFromCommand = (
|
|
|
970
988
|
// --- Extract tagged field names from event schema ---
|
|
971
989
|
|
|
972
990
|
/**
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
`@s.matches(DcbTag.string)` or `@s.matches(DcbTag.int)`.
|
|
991
|
+
The tag keys of all DCB-tagged fields across all variants of an event schema,
|
|
992
|
+
resolved as the tags themselves are, so an index is named after the key its tags
|
|
993
|
+
carry. Sorted and deduplicated.
|
|
977
994
|
|
|
978
995
|
For `CatalogEventLog.event` returns `["categoryId", "productId"]`.
|
|
979
996
|
*/
|
|
980
997
|
let extractTaggedFields = (schema: S.t<'event>): array<string> => {
|
|
981
|
-
|
|
982
|
-
| AnyOf({anyOf}) =>
|
|
983
|
-
// For union types, collect tagged fields from all variants
|
|
984
|
-
let allFields = anyOf->Array.flatMap(variantSchema =>
|
|
985
|
-
switch variantSchema {
|
|
986
|
-
| Object({properties}) =>
|
|
987
|
-
properties
|
|
988
|
-
->Dict.toArray
|
|
989
|
-
->Array.filterMap(((fieldName, fieldSchema)) =>
|
|
990
|
-
if isTagged(fieldSchema) {
|
|
991
|
-
Some(fieldName)
|
|
992
|
-
} else {
|
|
993
|
-
None
|
|
994
|
-
}
|
|
995
|
-
)
|
|
996
|
-
| _ => []
|
|
997
|
-
}
|
|
998
|
-
)
|
|
999
|
-
// Deduplicate field names using Set
|
|
1000
|
-
let fieldSet = Set.make()
|
|
1001
|
-
allFields->Array.forEach(field => fieldSet->Set.add(field))
|
|
1002
|
-
Array.fromIterator(fieldSet->Set.values)->Array.toSorted((a, b) => String.compare(a, b))
|
|
1003
|
-
|
|
1004
|
-
| Object({properties}) =>
|
|
998
|
+
let ofProperties = properties =>
|
|
1005
999
|
properties
|
|
1006
1000
|
->Dict.toArray
|
|
1007
1001
|
->Array.filterMap(((fieldName, fieldSchema)) =>
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1002
|
+
isTagged(fieldSchema) ? Some(resolveTagKey(fieldName, fieldSchema)) : None
|
|
1003
|
+
)
|
|
1004
|
+
let keys = switch schema->toUnknownSchema {
|
|
1005
|
+
| AnyOf({anyOf}) =>
|
|
1006
|
+
anyOf->Array.flatMap(variantSchema =>
|
|
1007
|
+
switch variantSchema {
|
|
1008
|
+
| Object({properties}) => ofProperties(properties)
|
|
1009
|
+
| _ => []
|
|
1012
1010
|
}
|
|
1013
1011
|
)
|
|
1014
|
-
|
|
1015
|
-
|
|
1012
|
+
| Object({properties}) => ofProperties(properties)
|
|
1016
1013
|
| _ => []
|
|
1017
1014
|
}
|
|
1015
|
+
let seen = Set.make()
|
|
1016
|
+
keys->Array.forEach(k => seen->Set.add(k))
|
|
1017
|
+
Array.fromIterator(seen->Set.values)->Array.toSorted((a, b) => String.compare(a, b))
|
|
1018
1018
|
}
|
|
1019
1019
|
|
|
1020
1020
|
/**
|
|
@@ -1091,7 +1091,16 @@ let idFieldsOfProperties = (properties: dict<S.t<unknown>>): array<DcbScopeInfer
|
|
|
1091
1091
|
// for every field except the ones that need it. Flagged, not folded in: taking
|
|
1092
1092
|
// the annotation away takes the identity with it, which is what the redundancy
|
|
1093
1093
|
// check has to be able to tell.
|
|
1094
|
-
|
|
1094
|
+
// A field's type outranks its name: `buyer: CustomerId.t` is keyed `customerId`,
|
|
1095
|
+
// and so is `orderId: CustomerId.t`. An explicit `@dcbTag("k")` still wins.
|
|
1096
|
+
let typedKey = (fieldSchema: S.t<unknown>, isList) => {
|
|
1097
|
+
let valueSchema = switch (isList, fieldSchema) {
|
|
1098
|
+
| (true, Array({additionalItems: Schema(item)})) => item
|
|
1099
|
+
| _ => fieldSchema
|
|
1100
|
+
}
|
|
1101
|
+
valueSchema->Semantic.identityKey->Option.map(_ => resolveTagKey("", valueSchema))
|
|
1102
|
+
}
|
|
1103
|
+
let untypedIdentity = (name, fieldSchema, isList) =>
|
|
1095
1104
|
if isIdName(name) {
|
|
1096
1105
|
Some({DcbScopeInference.name, isList})
|
|
1097
1106
|
} else if isPartitionTag(fieldSchema) {
|
|
@@ -1099,6 +1108,11 @@ let idFieldsOfProperties = (properties: dict<S.t<unknown>>): array<DcbScopeInfer
|
|
|
1099
1108
|
} else {
|
|
1100
1109
|
None
|
|
1101
1110
|
}
|
|
1111
|
+
let identity = (name, fieldSchema, isList) =>
|
|
1112
|
+
switch typedKey(fieldSchema, isList) {
|
|
1113
|
+
| Some(key) => Some({DcbScopeInference.name, isList, key})
|
|
1114
|
+
| None => untypedIdentity(name, fieldSchema, isList)
|
|
1115
|
+
}
|
|
1102
1116
|
properties
|
|
1103
1117
|
->Dict.toArray
|
|
1104
1118
|
->Array.flatMap(((name, fieldSchema)) => {
|
|
@@ -1148,7 +1162,8 @@ let eventShapesOfSchema = (schema: S.t<'a>): array<DcbScopeInference.eventShape>
|
|
|
1148
1162
|
// --- Partition tag derivation ---
|
|
1149
1163
|
|
|
1150
1164
|
/**
|
|
1151
|
-
Extracts
|
|
1165
|
+
Extracts the tag keys of fields annotated with `@s.matches(DcbTag.partition)` from an
|
|
1166
|
+
event schema: the field name, unless an override or an identity says otherwise.
|
|
1152
1167
|
*/
|
|
1153
1168
|
let extractPartitionTagFields = (schema: S.t<'event>): array<string> => {
|
|
1154
1169
|
switch schema->toUnknownSchema {
|
|
@@ -1160,7 +1175,7 @@ let extractPartitionTagFields = (schema: S.t<'event>): array<string> => {
|
|
|
1160
1175
|
->Dict.toArray
|
|
1161
1176
|
->Array.filterMap(((fieldName, fieldSchema)) =>
|
|
1162
1177
|
if isPartitionTag(fieldSchema) {
|
|
1163
|
-
Some(fieldName)
|
|
1178
|
+
Some(resolveTagKey(fieldName, fieldSchema))
|
|
1164
1179
|
} else {
|
|
1165
1180
|
None
|
|
1166
1181
|
}
|
|
@@ -1177,7 +1192,7 @@ let extractPartitionTagFields = (schema: S.t<'event>): array<string> => {
|
|
|
1177
1192
|
->Dict.toArray
|
|
1178
1193
|
->Array.filterMap(((fieldName, fieldSchema)) =>
|
|
1179
1194
|
if isPartitionTag(fieldSchema) {
|
|
1180
|
-
Some(fieldName)
|
|
1195
|
+
Some(resolveTagKey(fieldName, fieldSchema))
|
|
1181
1196
|
} else {
|
|
1182
1197
|
None
|
|
1183
1198
|
}
|
|
@@ -32,17 +32,33 @@ let dcbCompositePartitionMemberId = Sury.$Metadata_Id_make("dcb", "compositePart
|
|
|
32
32
|
|
|
33
33
|
let dcbTagKeyOverrideId = Sury.$Metadata_Id_make("dcb", "tagKeyOverride");
|
|
34
34
|
|
|
35
|
+
function mark(schema) {
|
|
36
|
+
return Sury.$Metadata_set(schema, dcbTagId, true);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function markForKey(schema, key) {
|
|
40
|
+
return Sury.$Metadata_set(Sury.$Metadata_set(schema, dcbTagId, true), dcbTagKeyOverrideId, key);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function markPartition(schema) {
|
|
44
|
+
return Sury.$Metadata_set(Sury.$Metadata_set(schema, dcbTagId, true), dcbPartitionTagId, true);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function markCrossPartition(schema) {
|
|
48
|
+
return Sury.$Metadata_set(Sury.$Metadata_set(schema, dcbTagId, true), dcbCrossPartitionId, true);
|
|
49
|
+
}
|
|
50
|
+
|
|
35
51
|
let string = Sury.$Metadata_set(Sury.string, dcbTagId, true);
|
|
36
52
|
|
|
37
53
|
function stringForKey(key) {
|
|
38
|
-
return
|
|
54
|
+
return markForKey(Sury.string, key);
|
|
39
55
|
}
|
|
40
56
|
|
|
41
57
|
let int = Sury.$Metadata_set(Sury.int, dcbTagId, true);
|
|
42
58
|
|
|
43
|
-
let partition =
|
|
59
|
+
let partition = markPartition(Sury.string);
|
|
44
60
|
|
|
45
|
-
let crossPartition =
|
|
61
|
+
let crossPartition = markCrossPartition(Sury.string);
|
|
46
62
|
|
|
47
63
|
function compositePartitionMember(position, sepOpt) {
|
|
48
64
|
let sep = sepOpt !== undefined ? sepOpt : "/";
|
|
@@ -169,7 +185,12 @@ function isCrossPartitionTaggedArray(fieldSchema) {
|
|
|
169
185
|
}
|
|
170
186
|
|
|
171
187
|
function resolveTagKey(fieldName, fieldSchema) {
|
|
172
|
-
|
|
188
|
+
let key = Sury.$Metadata_get(fieldSchema, dcbTagKeyOverrideId);
|
|
189
|
+
if (key !== undefined) {
|
|
190
|
+
return key;
|
|
191
|
+
} else {
|
|
192
|
+
return Stdlib_Option.getOr(Semantic$Reventless.identityKey(fieldSchema), fieldName);
|
|
193
|
+
}
|
|
173
194
|
}
|
|
174
195
|
|
|
175
196
|
function resolveArrayTagKey(fieldName, fieldSchema) {
|
|
@@ -180,7 +201,7 @@ function resolveArrayTagKey(fieldName, fieldSchema) {
|
|
|
180
201
|
if (itemSchema === "strip" || itemSchema === "strict") {
|
|
181
202
|
return fieldName;
|
|
182
203
|
} else {
|
|
183
|
-
return
|
|
204
|
+
return resolveTagKey(fieldName, itemSchema);
|
|
184
205
|
}
|
|
185
206
|
}
|
|
186
207
|
|
|
@@ -627,33 +648,34 @@ function buildQueryFromCommand(eventTypes, schema, value, tagKeysByEventTypeOpt,
|
|
|
627
648
|
}
|
|
628
649
|
|
|
629
650
|
function extractTaggedFields(schema) {
|
|
651
|
+
let ofProperties = properties => Stdlib_Array.filterMap(Object.entries(properties), param => {
|
|
652
|
+
let fieldSchema = param[1];
|
|
653
|
+
if (Stdlib_Option.isSome(Sury.$Metadata_get(fieldSchema, dcbTagId))) {
|
|
654
|
+
return resolveTagKey(param[0], fieldSchema);
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
let keys;
|
|
630
658
|
switch (schema.type) {
|
|
631
659
|
case "object" :
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
return param[0];
|
|
635
|
-
}
|
|
636
|
-
}).toSorted(Primitive_string.compare);
|
|
660
|
+
keys = ofProperties(schema.properties);
|
|
661
|
+
break;
|
|
637
662
|
case "anyOf" :
|
|
638
|
-
|
|
663
|
+
keys = schema.anyOf.flatMap(variantSchema => {
|
|
639
664
|
if (variantSchema.type === "object") {
|
|
640
|
-
return
|
|
641
|
-
if (Stdlib_Option.isSome(Sury.$Metadata_get(param[1], dcbTagId))) {
|
|
642
|
-
return param[0];
|
|
643
|
-
}
|
|
644
|
-
});
|
|
665
|
+
return ofProperties(variantSchema.properties);
|
|
645
666
|
} else {
|
|
646
667
|
return [];
|
|
647
668
|
}
|
|
648
669
|
});
|
|
649
|
-
|
|
650
|
-
allFields.forEach(field => {
|
|
651
|
-
fieldSet.add(field);
|
|
652
|
-
});
|
|
653
|
-
return Array.from(fieldSet.values()).toSorted(Primitive_string.compare);
|
|
670
|
+
break;
|
|
654
671
|
default:
|
|
655
|
-
|
|
672
|
+
keys = [];
|
|
656
673
|
}
|
|
674
|
+
let seen = new Set();
|
|
675
|
+
keys.forEach(k => {
|
|
676
|
+
seen.add(k);
|
|
677
|
+
});
|
|
678
|
+
return Array.from(seen.values()).toSorted(Primitive_string.compare);
|
|
657
679
|
}
|
|
658
680
|
|
|
659
681
|
function crossPartitionKeysOfProperties(properties) {
|
|
@@ -703,8 +725,25 @@ function idFieldsOfProperties(properties) {
|
|
|
703
725
|
return name.endsWith("Id");
|
|
704
726
|
}
|
|
705
727
|
};
|
|
728
|
+
let typedKey = (fieldSchema, isList) => {
|
|
729
|
+
let valueSchema;
|
|
730
|
+
if (isList && fieldSchema.type === "array") {
|
|
731
|
+
let item = fieldSchema.additionalItems;
|
|
732
|
+
valueSchema = item === "strip" || item === "strict" ? fieldSchema : item;
|
|
733
|
+
} else {
|
|
734
|
+
valueSchema = fieldSchema;
|
|
735
|
+
}
|
|
736
|
+
return Stdlib_Option.map(Semantic$Reventless.identityKey(valueSchema), param => resolveTagKey("", valueSchema));
|
|
737
|
+
};
|
|
706
738
|
let identity = (name, fieldSchema, isList) => {
|
|
707
|
-
|
|
739
|
+
let key = typedKey(fieldSchema, isList);
|
|
740
|
+
if (key !== undefined) {
|
|
741
|
+
return {
|
|
742
|
+
name: name,
|
|
743
|
+
isList: isList,
|
|
744
|
+
key: key
|
|
745
|
+
};
|
|
746
|
+
} else if (isIdName(name)) {
|
|
708
747
|
return {
|
|
709
748
|
name: name,
|
|
710
749
|
isList: isList
|
|
@@ -773,16 +812,18 @@ function extractPartitionTagFields(schema) {
|
|
|
773
812
|
switch (schema.type) {
|
|
774
813
|
case "object" :
|
|
775
814
|
return Stdlib_Array.filterMap(Object.entries(schema.properties), param => {
|
|
776
|
-
|
|
777
|
-
|
|
815
|
+
let fieldSchema = param[1];
|
|
816
|
+
if (Stdlib_Option.isSome(Sury.$Metadata_get(fieldSchema, dcbPartitionTagId))) {
|
|
817
|
+
return resolveTagKey(param[0], fieldSchema);
|
|
778
818
|
}
|
|
779
819
|
});
|
|
780
820
|
case "anyOf" :
|
|
781
821
|
let allFields = schema.anyOf.flatMap(variantSchema => {
|
|
782
822
|
if (variantSchema.type === "object") {
|
|
783
823
|
return Stdlib_Array.filterMap(Object.entries(variantSchema.properties), param => {
|
|
784
|
-
|
|
785
|
-
|
|
824
|
+
let fieldSchema = param[1];
|
|
825
|
+
if (Stdlib_Option.isSome(Sury.$Metadata_get(fieldSchema, dcbPartitionTagId))) {
|
|
826
|
+
return resolveTagKey(param[0], fieldSchema);
|
|
786
827
|
}
|
|
787
828
|
});
|
|
788
829
|
} else {
|
|
@@ -1043,6 +1084,10 @@ export {
|
|
|
1043
1084
|
dcbCrossPartitionId,
|
|
1044
1085
|
dcbCompositePartitionMemberId,
|
|
1045
1086
|
dcbTagKeyOverrideId,
|
|
1087
|
+
mark,
|
|
1088
|
+
markForKey,
|
|
1089
|
+
markPartition,
|
|
1090
|
+
markCrossPartition,
|
|
1046
1091
|
string,
|
|
1047
1092
|
stringForKey,
|
|
1048
1093
|
int,
|
|
@@ -39,6 +39,32 @@ let to_ = (~plugin=?, ~key=?, entity: string): S.t<string> => {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
`to_` for a field whose schema is not a bare `S.string`: the schema keeps its
|
|
44
|
+
type, and an identity it carries moves onto the reference's target rather than
|
|
45
|
+
being overwritten.
|
|
46
|
+
*/
|
|
47
|
+
let mark = (schema: S.t<'a>, ~plugin=?, ~key=?, entity: string): S.t<'a> => {
|
|
48
|
+
let identity = Semantic.identityKey(schema)
|
|
49
|
+
let base =
|
|
50
|
+
schema
|
|
51
|
+
->S.Metadata.set(~id=DcbTag.dcbTagId, true)
|
|
52
|
+
->Semantic.mark(~id=Semantic.Id.reference, ~payload=ReferenceTo({entity, plugin, ?identity}))
|
|
53
|
+
switch key {
|
|
54
|
+
| Some(k) => base->S.Metadata.set(~id=DcbTag.dcbTagKeyOverrideId, k)
|
|
55
|
+
| None => base
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** `toWithoutDcbTag` for a field whose schema is not a bare `S.string`. */
|
|
60
|
+
let markWithoutDcbTag = (schema: S.t<'a>, ~plugin=?, entity: string): S.t<'a> => {
|
|
61
|
+
let identity = Semantic.identityKey(schema)
|
|
62
|
+
schema->Semantic.mark(
|
|
63
|
+
~id=Semantic.Id.reference,
|
|
64
|
+
~payload=ReferenceTo({entity, plugin, ?identity}),
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
42
68
|
/** Returns the reference target if the schema carries `Reference.to_(...)` metadata. */
|
|
43
69
|
let getTarget = (schema: S.t<unknown>): option<target> =>
|
|
44
70
|
switch Semantic.get(schema) {
|
|
@@ -21,6 +21,35 @@ function to_(plugin, key, entity) {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function mark(schema, plugin, key, entity) {
|
|
25
|
+
let identity = Semantic$Reventless.identityKey(schema);
|
|
26
|
+
let base = Semantic$Reventless.mark(Sury.$Metadata_set(schema, DcbTag$Reventless.dcbTagId, true), Semantic$Reventless.Id.reference, {
|
|
27
|
+
TAG: "ReferenceTo",
|
|
28
|
+
_0: {
|
|
29
|
+
entity: entity,
|
|
30
|
+
plugin: plugin,
|
|
31
|
+
identity: identity
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
if (key !== undefined) {
|
|
35
|
+
return Sury.$Metadata_set(base, DcbTag$Reventless.dcbTagKeyOverrideId, key);
|
|
36
|
+
} else {
|
|
37
|
+
return base;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function markWithoutDcbTag(schema, plugin, entity) {
|
|
42
|
+
let identity = Semantic$Reventless.identityKey(schema);
|
|
43
|
+
return Semantic$Reventless.mark(schema, Semantic$Reventless.Id.reference, {
|
|
44
|
+
TAG: "ReferenceTo",
|
|
45
|
+
_0: {
|
|
46
|
+
entity: entity,
|
|
47
|
+
plugin: plugin,
|
|
48
|
+
identity: identity
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
24
53
|
function getTarget(schema) {
|
|
25
54
|
let match = Semantic$Reventless.get(schema);
|
|
26
55
|
if (match === undefined) {
|
|
@@ -92,6 +121,8 @@ function toWithoutDcbTag(plugin, entity) {
|
|
|
92
121
|
|
|
93
122
|
export {
|
|
94
123
|
to_,
|
|
124
|
+
mark,
|
|
125
|
+
markWithoutDcbTag,
|
|
95
126
|
getTarget,
|
|
96
127
|
getFieldTarget,
|
|
97
128
|
collectFieldTargets,
|
|
@@ -10,7 +10,7 @@ The payload is a typed variant because the vocabulary is framework-owned and
|
|
|
10
10
|
closed — which also keeps `Reference.getTarget` total.
|
|
11
11
|
*/
|
|
12
12
|
/** Which entity a reference field points to. */
|
|
13
|
-
type referenceTarget = {entity: string, plugin: option<string
|
|
13
|
+
type referenceTarget = {entity: string, plugin: option<string>, identity?: string}
|
|
14
14
|
|
|
15
15
|
/** Which object store the value lives in. `plugin` is absent for the declaring
|
|
16
16
|
plugin's own store; `threshold` is `@offload`'s per-field byte cut, `None`
|
|
@@ -50,6 +50,8 @@ type payload =
|
|
|
50
50
|
| ReferenceTo(referenceTarget)
|
|
51
51
|
| StoredIn(storeTarget)
|
|
52
52
|
| MemberOf(memberTarget)
|
|
53
|
+
/** Which identity an id is: named by its key (`orderId`). */
|
|
54
|
+
| IdentityOf({key: string})
|
|
53
55
|
|
|
54
56
|
/** A field's semantic: the vocabulary id, plus its detail. */
|
|
55
57
|
type t = {id: string, payload: payload}
|
|
@@ -111,6 +113,10 @@ module Id = {
|
|
|
111
113
|
// The first composite that is a union rather than an object. Collapses fields,
|
|
112
114
|
// so adopting it changes the wire and rebuilds a derived view.
|
|
113
115
|
let geolocation = "geolocation"
|
|
116
|
+
|
|
117
|
+
// Which entity an id names, carried by the type `Id.Make` produces. A string on
|
|
118
|
+
// the wire, so adopting one changes nothing stored.
|
|
119
|
+
let identity = "identity"
|
|
114
120
|
}
|
|
115
121
|
|
|
116
122
|
/** One transparent-string semantic: a `semantic/` module whose `type t` is a
|
|
@@ -242,6 +248,16 @@ let rec getFrom = (schema: S.t<unknown>): option<t> =>
|
|
|
242
248
|
|
|
243
249
|
let get = (fieldSchema: S.t<'a>): option<t> => fieldSchema->S.castToUnknown->getFrom
|
|
244
250
|
|
|
251
|
+
/** The identity a field's value is (`orderId`), read through `option<…>`. A
|
|
252
|
+
reference to an identity carries it on its target, since a schema holds one
|
|
253
|
+
semantic. */
|
|
254
|
+
let identityKey = (fieldSchema: S.t<'a>): option<string> =>
|
|
255
|
+
switch get(fieldSchema) {
|
|
256
|
+
| Some({payload: IdentityOf({key})}) => Some(key)
|
|
257
|
+
| Some({payload: ReferenceTo({?identity})}) => identity
|
|
258
|
+
| _ => None
|
|
259
|
+
}
|
|
260
|
+
|
|
245
261
|
/** Whether a field's schema carries this specific semantic. */
|
|
246
262
|
let has = (fieldSchema: S.t<'a>, ~id: string): bool =>
|
|
247
263
|
switch get(fieldSchema) {
|
|
@@ -51,7 +51,8 @@ let Id = {
|
|
|
51
51
|
dateRange: "dateRange",
|
|
52
52
|
geoPoint: "geoPoint",
|
|
53
53
|
lifecycleTrail: "lifecycleTrail",
|
|
54
|
-
geolocation: "geolocation"
|
|
54
|
+
geolocation: "geolocation",
|
|
55
|
+
identity: "identity"
|
|
55
56
|
};
|
|
56
57
|
|
|
57
58
|
let brandedStrings = [
|
|
@@ -205,6 +206,25 @@ function getFrom(schema) {
|
|
|
205
206
|
|
|
206
207
|
let get = getFrom;
|
|
207
208
|
|
|
209
|
+
function identityKey(fieldSchema) {
|
|
210
|
+
let match = getFrom(fieldSchema);
|
|
211
|
+
if (match === undefined) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
let match$1 = match.payload;
|
|
215
|
+
if (typeof match$1 !== "object") {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
switch (match$1.TAG) {
|
|
219
|
+
case "ReferenceTo" :
|
|
220
|
+
return match$1._0.identity;
|
|
221
|
+
case "IdentityOf" :
|
|
222
|
+
return match$1.key;
|
|
223
|
+
default:
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
208
228
|
function has(fieldSchema, id) {
|
|
209
229
|
let s = getFrom(fieldSchema);
|
|
210
230
|
if (s !== undefined) {
|
|
@@ -225,6 +245,7 @@ export {
|
|
|
225
245
|
unionVariant,
|
|
226
246
|
getFrom,
|
|
227
247
|
get,
|
|
248
|
+
identityKey,
|
|
228
249
|
has,
|
|
229
250
|
}
|
|
230
251
|
/* semanticId Not a pure module */
|
package/src/types/Id.res
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
Module type for aggregate and read model identifiers.
|
|
3
3
|
|
|
4
4
|
Every Reventless component declares its own `Id` module satisfying this type.
|
|
5
|
-
The abstract `type t`
|
|
6
|
-
|
|
5
|
+
The abstract `type t` keeps ids apart from plain strings, but not from each
|
|
6
|
+
other: every alias of `Id.String` (which is what the ppx injects) is the same
|
|
7
|
+
type. Only `Id.Make` gives an entity an id type of its own.
|
|
7
8
|
|
|
8
9
|
@example
|
|
9
10
|
```rescript
|
|
@@ -62,8 +63,9 @@ module StringPure = {
|
|
|
62
63
|
/**
|
|
63
64
|
A sealed string-based `Id.T` implementation for use in production aggregate specs.
|
|
64
65
|
|
|
65
|
-
Unlike `StringPure`, the `t` type is abstract,
|
|
66
|
-
|
|
66
|
+
Unlike `StringPure`, the `t` type is abstract, so an id is not a plain string. It is
|
|
67
|
+
sealed once, so every alias shares one type; use `Id.Make` for an id type per
|
|
68
|
+
entity. Use `Id.StringPure` in tests when string literals are needed.
|
|
67
69
|
|
|
68
70
|
@example
|
|
69
71
|
```rescript
|
|
@@ -73,3 +75,31 @@ let name = "Category"
|
|
|
73
75
|
```
|
|
74
76
|
*/
|
|
75
77
|
module String: T = StringPure
|
|
78
|
+
|
|
79
|
+
/** An `Id.T` that names one entity, by its key (`orderId`). */
|
|
80
|
+
module type Identity = {
|
|
81
|
+
include T with type input = string
|
|
82
|
+
let key: string
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
One distinct type per identity. Each application yields a fresh abstract `t`, so
|
|
87
|
+
an `OrderId.t` cannot be passed where a `CustomerId.t` is expected. A string on
|
|
88
|
+
the wire; the schema carries the `identity` semantic, which is how the runtime
|
|
89
|
+
knows which entity a field names whatever the field is called.
|
|
90
|
+
|
|
91
|
+
@example
|
|
92
|
+
```rescript
|
|
93
|
+
// src/Order/OrderId.res
|
|
94
|
+
include Reventless.Id.Make({let key = "orderId"})
|
|
95
|
+
```
|
|
96
|
+
*/
|
|
97
|
+
module Make = (
|
|
98
|
+
K: {
|
|
99
|
+
let key: string
|
|
100
|
+
},
|
|
101
|
+
): Identity => {
|
|
102
|
+
include StringPure
|
|
103
|
+
let key = K.key
|
|
104
|
+
let schema = schema->Semantic.mark(~id=Semantic.Id.identity, ~payload=IdentityOf({key: K.key}))
|
|
105
|
+
}
|
package/src/types/Id.res.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as Sury from "sury";
|
|
4
4
|
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
5
|
+
import * as Semantic$Reventless from "../semantic/Semantic.res.mjs";
|
|
5
6
|
|
|
6
7
|
let schema = Sury.string;
|
|
7
8
|
|
|
@@ -12,6 +13,21 @@ let StringPure = {
|
|
|
12
13
|
cmp: cmp
|
|
13
14
|
};
|
|
14
15
|
|
|
16
|
+
function Make(K) {
|
|
17
|
+
let schema$1 = Semantic$Reventless.mark(schema, Semantic$Reventless.Id.identity, {
|
|
18
|
+
TAG: "IdentityOf",
|
|
19
|
+
key: K.key
|
|
20
|
+
});
|
|
21
|
+
return {
|
|
22
|
+
schema: schema$1,
|
|
23
|
+
make: prim => prim,
|
|
24
|
+
makeFromString: prim => prim,
|
|
25
|
+
toString: prim => prim,
|
|
26
|
+
cmp: cmp,
|
|
27
|
+
key: K.key
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
15
31
|
function String_make(prim) {
|
|
16
32
|
return prim;
|
|
17
33
|
}
|
|
@@ -35,5 +51,6 @@ let $$String = {
|
|
|
35
51
|
export {
|
|
36
52
|
StringPure,
|
|
37
53
|
$$String,
|
|
54
|
+
Make,
|
|
38
55
|
}
|
|
39
56
|
/* schema Not a pure module */
|