@reventlessdev/reventless-spec 3.0.0-alpha.137 → 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 +22 -0
- package/package.json +1 -1
- package/src/components/DcbScopeInference.res +27 -4
- package/src/components/DcbScopeInference.res.mjs +3 -1
- package/src/components/DcbTag.res +89 -61
- package/src/components/DcbTag.res.mjs +93 -43
- package/src/components/DcbValidation.res +51 -18
- package/src/components/DcbValidation.res.mjs +16 -0
- 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,28 @@
|
|
|
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
|
+
|
|
14
|
+
# 3.0.0-alpha.138 (2026-09-20)
|
|
15
|
+
|
|
16
|
+
* fix(spec)!: let an explicit partition tag declare an identity the name cannot ([01d82c0](https://github.com/ReventlessDev/reventless-core/commit/01d82c0714a31f31d55c466eae9100cf4dcc85f0))
|
|
17
|
+
|
|
18
|
+
### BREAKING CHANGES
|
|
19
|
+
|
|
20
|
+
* a @partitionTag on a field whose name is not *Id / *Ids was
|
|
21
|
+
inert and now takes effect, so such a slice's partition key — and with it its
|
|
22
|
+
storage key, fence and read scope — changes to the annotated field. Nothing else
|
|
23
|
+
moves: the annotation was doing nothing before, so no slice that resolved a key
|
|
24
|
+
resolves a different one.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
6
28
|
# 3.0.0-alpha.137 (2026-09-20)
|
|
7
29
|
|
|
8
30
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -34,8 +34,28 @@ The three rules (over the representation):
|
|
|
34
34
|
on `ProductAdded`) are payload ⇒ not indexed ⇒ the sibling-leak GSI write never
|
|
35
35
|
happens.
|
|
36
36
|
*/
|
|
37
|
-
/** A
|
|
38
|
-
|
|
37
|
+
/** A field the slice graph treats as an entity identity.
|
|
38
|
+
|
|
39
|
+
Normally that is a `*Id` / `*Ids`-shaped name — the convention is the signal,
|
|
40
|
+
and this module stays schema-agnostic by taking the name alone.
|
|
41
|
+
|
|
42
|
+
`byTag` marks the exception: an identity the *name* does not declare, which an
|
|
43
|
+
adapter recognised from an explicit `@partitionTag`. A domain's own identifier
|
|
44
|
+
is often not suffixed — `sku`, `isbn`, `vin` — and without this the annotation
|
|
45
|
+
naming one would be extracted as a hint and then dropped, because `seedOf`
|
|
46
|
+
only honours a hint already among the produced keys. So the escape hatch would
|
|
47
|
+
have worked for every field except the ones that need it.
|
|
48
|
+
|
|
49
|
+
It is carried rather than folded in because removing the annotation removes
|
|
50
|
+
the identity, which is exactly what the redundancy check has to know: a hint
|
|
51
|
+
inference cannot reach without it is never redundant. */
|
|
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
|
+
}
|
|
39
59
|
|
|
40
60
|
/** One variant arm: its constructor name and the `*Id` fields it carries. */
|
|
41
61
|
type eventShape = {eventType: string, idFields: array<idField>}
|
|
@@ -83,10 +103,13 @@ type derived = {
|
|
|
83
103
|
/**
|
|
84
104
|
The tag key for a `*Id`-shaped field. A plural `*Ids: array<string>` shares the
|
|
85
105
|
singular producer's key (trailing `s` stripped — `productIds` -> `productId`);
|
|
86
|
-
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.
|
|
87
108
|
*/
|
|
88
109
|
let tagKeyOf = (f: idField): string =>
|
|
89
|
-
if f.
|
|
110
|
+
if f.key->Option.isSome {
|
|
111
|
+
f.key->Option.getUnsafe
|
|
112
|
+
} else if f.isList && f.name->String.endsWith("s") {
|
|
90
113
|
f.name->String.slice(~start=0, ~end=f.name->String.length - 1)
|
|
91
114
|
} else {
|
|
92
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
|
/**
|
|
@@ -1084,29 +1084,56 @@ which is this module's half of the split.
|
|
|
1084
1084
|
*/
|
|
1085
1085
|
let idFieldsOfProperties = (properties: dict<S.t<unknown>>): array<DcbScopeInference.idField> => {
|
|
1086
1086
|
let isIdName = (name: string) => name->String.endsWith("Ids") || name->String.endsWith("Id")
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1087
|
+
// An explicit `@partitionTag` declares an identity the naming convention cannot
|
|
1088
|
+
// express — a domain's own identifier is often unsuffixed (`sku`, `isbn`).
|
|
1089
|
+
// Without this the hint is extracted and then ignored, since `seedOf` honours
|
|
1090
|
+
// only a hint already among the produced keys, so the escape hatch would work
|
|
1091
|
+
// for every field except the ones that need it. Flagged, not folded in: taking
|
|
1092
|
+
// the annotation away takes the identity with it, which is what the redundancy
|
|
1093
|
+
// check has to be able to tell.
|
|
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) =>
|
|
1090
1104
|
if isIdName(name) {
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
}
|
|
1095
|
-
[{DcbScopeInference.name, isList}]
|
|
1105
|
+
Some({DcbScopeInference.name, isList})
|
|
1106
|
+
} else if isPartitionTag(fieldSchema) {
|
|
1107
|
+
Some({DcbScopeInference.name, isList, byTag: true})
|
|
1096
1108
|
} else {
|
|
1109
|
+
None
|
|
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
|
+
}
|
|
1116
|
+
properties
|
|
1117
|
+
->Dict.toArray
|
|
1118
|
+
->Array.flatMap(((name, fieldSchema)) => {
|
|
1119
|
+
let isList = switch fieldSchema {
|
|
1120
|
+
| Array(_) => true
|
|
1121
|
+
| _ => false
|
|
1122
|
+
}
|
|
1123
|
+
switch identity(name, fieldSchema, isList) {
|
|
1124
|
+
| Some(f) => [f]
|
|
1125
|
+
| None =>
|
|
1097
1126
|
switch nestedRecordProperties(fieldSchema) {
|
|
1098
1127
|
| Some((nested, nestedIsList)) =>
|
|
1099
1128
|
nested
|
|
1100
1129
|
->Dict.toArray
|
|
1101
|
-
->Array.filterMap(((nestedName,
|
|
1102
|
-
|
|
1103
|
-
? Some({DcbScopeInference.name: nestedName, isList: nestedIsList})
|
|
1104
|
-
: None
|
|
1130
|
+
->Array.filterMap(((nestedName, nestedSchema)) =>
|
|
1131
|
+
identity(nestedName, nestedSchema, nestedIsList)
|
|
1105
1132
|
)
|
|
1106
1133
|
| None => []
|
|
1107
1134
|
}
|
|
1108
1135
|
}
|
|
1109
|
-
)
|
|
1136
|
+
})
|
|
1110
1137
|
}
|
|
1111
1138
|
|
|
1112
1139
|
/**
|
|
@@ -1135,7 +1162,8 @@ let eventShapesOfSchema = (schema: S.t<'a>): array<DcbScopeInference.eventShape>
|
|
|
1135
1162
|
// --- Partition tag derivation ---
|
|
1136
1163
|
|
|
1137
1164
|
/**
|
|
1138
|
-
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.
|
|
1139
1167
|
*/
|
|
1140
1168
|
let extractPartitionTagFields = (schema: S.t<'event>): array<string> => {
|
|
1141
1169
|
switch schema->toUnknownSchema {
|
|
@@ -1147,7 +1175,7 @@ let extractPartitionTagFields = (schema: S.t<'event>): array<string> => {
|
|
|
1147
1175
|
->Dict.toArray
|
|
1148
1176
|
->Array.filterMap(((fieldName, fieldSchema)) =>
|
|
1149
1177
|
if isPartitionTag(fieldSchema) {
|
|
1150
|
-
Some(fieldName)
|
|
1178
|
+
Some(resolveTagKey(fieldName, fieldSchema))
|
|
1151
1179
|
} else {
|
|
1152
1180
|
None
|
|
1153
1181
|
}
|
|
@@ -1164,7 +1192,7 @@ let extractPartitionTagFields = (schema: S.t<'event>): array<string> => {
|
|
|
1164
1192
|
->Dict.toArray
|
|
1165
1193
|
->Array.filterMap(((fieldName, fieldSchema)) =>
|
|
1166
1194
|
if isPartitionTag(fieldSchema) {
|
|
1167
|
-
Some(fieldName)
|
|
1195
|
+
Some(resolveTagKey(fieldName, fieldSchema))
|
|
1168
1196
|
} else {
|
|
1169
1197
|
None
|
|
1170
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,31 +725,53 @@ 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
|
+
};
|
|
738
|
+
let identity = (name, fieldSchema, isList) => {
|
|
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)) {
|
|
747
|
+
return {
|
|
748
|
+
name: name,
|
|
749
|
+
isList: isList
|
|
750
|
+
};
|
|
751
|
+
} else if (Stdlib_Option.isSome(Sury.$Metadata_get(fieldSchema, dcbPartitionTagId))) {
|
|
752
|
+
return {
|
|
753
|
+
name: name,
|
|
754
|
+
isList: isList,
|
|
755
|
+
byTag: true
|
|
756
|
+
};
|
|
757
|
+
} else {
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
};
|
|
706
761
|
return Object.entries(properties).flatMap(param => {
|
|
707
762
|
let fieldSchema = param[1];
|
|
708
|
-
let
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
return [
|
|
713
|
-
name: name,
|
|
714
|
-
isList: isList
|
|
715
|
-
}];
|
|
763
|
+
let isList;
|
|
764
|
+
isList = fieldSchema.type === "array";
|
|
765
|
+
let f = identity(param[0], fieldSchema, isList);
|
|
766
|
+
if (f !== undefined) {
|
|
767
|
+
return [f];
|
|
716
768
|
}
|
|
717
769
|
let match = nestedRecordProperties(fieldSchema);
|
|
718
770
|
if (match === undefined) {
|
|
719
771
|
return [];
|
|
720
772
|
}
|
|
721
773
|
let nestedIsList = match[1];
|
|
722
|
-
return Stdlib_Array.filterMap(Object.entries(match[0]), param =>
|
|
723
|
-
let nestedName = param[0];
|
|
724
|
-
if (isIdName(nestedName)) {
|
|
725
|
-
return {
|
|
726
|
-
name: nestedName,
|
|
727
|
-
isList: nestedIsList
|
|
728
|
-
};
|
|
729
|
-
}
|
|
730
|
-
});
|
|
774
|
+
return Stdlib_Array.filterMap(Object.entries(match[0]), param => identity(param[0], param[1], nestedIsList));
|
|
731
775
|
});
|
|
732
776
|
}
|
|
733
777
|
|
|
@@ -768,16 +812,18 @@ function extractPartitionTagFields(schema) {
|
|
|
768
812
|
switch (schema.type) {
|
|
769
813
|
case "object" :
|
|
770
814
|
return Stdlib_Array.filterMap(Object.entries(schema.properties), param => {
|
|
771
|
-
|
|
772
|
-
|
|
815
|
+
let fieldSchema = param[1];
|
|
816
|
+
if (Stdlib_Option.isSome(Sury.$Metadata_get(fieldSchema, dcbPartitionTagId))) {
|
|
817
|
+
return resolveTagKey(param[0], fieldSchema);
|
|
773
818
|
}
|
|
774
819
|
});
|
|
775
820
|
case "anyOf" :
|
|
776
821
|
let allFields = schema.anyOf.flatMap(variantSchema => {
|
|
777
822
|
if (variantSchema.type === "object") {
|
|
778
823
|
return Stdlib_Array.filterMap(Object.entries(variantSchema.properties), param => {
|
|
779
|
-
|
|
780
|
-
|
|
824
|
+
let fieldSchema = param[1];
|
|
825
|
+
if (Stdlib_Option.isSome(Sury.$Metadata_get(fieldSchema, dcbPartitionTagId))) {
|
|
826
|
+
return resolveTagKey(param[0], fieldSchema);
|
|
781
827
|
}
|
|
782
828
|
});
|
|
783
829
|
} else {
|
|
@@ -1038,6 +1084,10 @@ export {
|
|
|
1038
1084
|
dcbCrossPartitionId,
|
|
1039
1085
|
dcbCompositePartitionMemberId,
|
|
1040
1086
|
dcbTagKeyOverrideId,
|
|
1087
|
+
mark,
|
|
1088
|
+
markForKey,
|
|
1089
|
+
markPartition,
|
|
1090
|
+
markCrossPartition,
|
|
1041
1091
|
string,
|
|
1042
1092
|
stringForKey,
|
|
1043
1093
|
int,
|
|
@@ -501,29 +501,62 @@ let validatePartitionHintsVsInference = (
|
|
|
501
501
|
shapes->Array.forEach(s =>
|
|
502
502
|
switch s.partitionHint {
|
|
503
503
|
| Some(hint) if DcbScopeInference.producedKeys(s)->Array.includes(hint) =>
|
|
504
|
+
// "Without the annotation" has to mean without everything the annotation
|
|
505
|
+
// brought. A field that is an identity only because it carries the tag
|
|
506
|
+
// (`byTag`) stops being one when the tag goes, so dropping only the hint
|
|
507
|
+
// would ask whether inference reaches a key it can no longer see — and
|
|
508
|
+
// answer yes, reporting a load-bearing annotation as removable.
|
|
509
|
+
let withoutTag = (e: DcbScopeInference.eventShape) => {
|
|
510
|
+
...e,
|
|
511
|
+
idFields: e.idFields->Array.filter(f => f.byTag != Some(true)),
|
|
512
|
+
}
|
|
513
|
+
// A hint naming an identity that exists only because of the annotation is
|
|
514
|
+
// **necessary**, and fits neither verdict this check was built to give.
|
|
515
|
+
// Called redundant it would be removed and the key would vanish; called
|
|
516
|
+
// contradictory it would be "corrected" to whatever name-shaped field
|
|
517
|
+
// happens to sit beside it — which is the author's choice overruled, not a
|
|
518
|
+
// mistake found. Both readings come from assuming the field is an identity
|
|
519
|
+
// either way, which is true only when the name says so.
|
|
520
|
+
let declaredByTag =
|
|
521
|
+
s.produced->Array.some(e =>
|
|
522
|
+
e.idFields->Array.some(f => f.name == hint && f.byTag == Some(true))
|
|
523
|
+
)
|
|
504
524
|
let unaided = DcbScopeInference.resolvePartitions(
|
|
505
|
-
shapes->Array.map(o =>
|
|
525
|
+
shapes->Array.map(o =>
|
|
526
|
+
o.sliceName == s.sliceName
|
|
527
|
+
? {
|
|
528
|
+
...o,
|
|
529
|
+
partitionHint: None,
|
|
530
|
+
produced: o.produced->Array.map(withoutTag),
|
|
531
|
+
consumed: o.consumed->Array.map(withoutTag),
|
|
532
|
+
}
|
|
533
|
+
: o
|
|
534
|
+
),
|
|
506
535
|
)
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
})
|
|
518
|
-
| None =>
|
|
519
|
-
let candidates = unaided.candidatesBySlice->Dict.get(s.sliceName)->Option.getOr([])
|
|
520
|
-
if !(candidates->Array.includes(hint)) {
|
|
536
|
+
if declaredByTag {
|
|
537
|
+
()
|
|
538
|
+
} else {
|
|
539
|
+
switch unaided.partitionBySlice->Dict.get(s.sliceName) {
|
|
540
|
+
| Some(inferred) if inferred == hint =>
|
|
541
|
+
redundancies->Array.push({
|
|
542
|
+
sliceName: s.sliceName,
|
|
543
|
+
message: `@partitionTag ${hint} is what inference derives without it — the annotation is redundant and can be removed.`,
|
|
544
|
+
})
|
|
545
|
+
| Some(inferred) =>
|
|
521
546
|
contradictions->Array.push({
|
|
522
547
|
sliceName: s.sliceName,
|
|
523
|
-
message: `@partitionTag names ${hint},
|
|
524
|
-
", ",
|
|
525
|
-
)}). Move the annotation to the slice's own key.`,
|
|
548
|
+
message: `@partitionTag names ${hint}, but inference derives ${inferred} from the slice graph — ${hint} is read from another entity. Remove the annotation, or move it to ${inferred}.`,
|
|
526
549
|
})
|
|
550
|
+
| None =>
|
|
551
|
+
let candidates = unaided.candidatesBySlice->Dict.get(s.sliceName)->Option.getOr([])
|
|
552
|
+
if !(candidates->Array.includes(hint)) {
|
|
553
|
+
contradictions->Array.push({
|
|
554
|
+
sliceName: s.sliceName,
|
|
555
|
+
message: `@partitionTag names ${hint}, which this slice only reads as a reference to another entity (candidates: ${candidates->Array.join(
|
|
556
|
+
", ",
|
|
557
|
+
)}). Move the annotation to the slice's own key.`,
|
|
558
|
+
})
|
|
559
|
+
}
|
|
527
560
|
}
|
|
528
561
|
}
|
|
529
562
|
| _ => ()
|
|
@@ -437,14 +437,30 @@ function validatePartitionHintsVsInference(shapes) {
|
|
|
437
437
|
if (!DcbScopeInference$Reventless.producedKeys(s).includes(hint)) {
|
|
438
438
|
return;
|
|
439
439
|
}
|
|
440
|
+
let withoutTag = e => ({
|
|
441
|
+
eventType: e.eventType,
|
|
442
|
+
idFields: e.idFields.filter(f => Primitive_object.notequal(f.byTag, true))
|
|
443
|
+
});
|
|
444
|
+
let declaredByTag = s.produced.some(e => e.idFields.some(f => {
|
|
445
|
+
if (f.name === hint) {
|
|
446
|
+
return Primitive_object.equal(f.byTag, true);
|
|
447
|
+
} else {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
}));
|
|
440
451
|
let unaided = DcbScopeInference$Reventless.resolvePartitions(shapes.map(o => {
|
|
441
452
|
if (o.sliceName !== s.sliceName) {
|
|
442
453
|
return o;
|
|
443
454
|
}
|
|
444
455
|
let newrecord = {...o};
|
|
445
456
|
newrecord.partitionHint = undefined;
|
|
457
|
+
newrecord.produced = o.produced.map(withoutTag);
|
|
458
|
+
newrecord.consumed = o.consumed.map(withoutTag);
|
|
446
459
|
return newrecord;
|
|
447
460
|
}));
|
|
461
|
+
if (declaredByTag) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
448
464
|
let inferred = unaided.partitionBySlice[s.sliceName];
|
|
449
465
|
if (inferred !== undefined) {
|
|
450
466
|
if (inferred === hint) {
|
|
@@ -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 */
|