@reventlessdev/reventless-core 3.0.0-alpha.257 → 3.0.0-alpha.259
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 +18 -0
- package/package.json +5 -5
- package/src/Projection.res +158 -40
- package/src/Projection.res.mjs +107 -28
- package/src/admin/UiFragmentRegistry/{StateChangeSlice → StateChange}/UiFragmentRegistry.res.mjs +1 -1
- package/src/admin/UiFragmentRegistry/{StateChangeSlice → StateChange}/UiFragmentRegistry_Behavior.res.mjs +1 -1
- package/src/admin/UiFragmentRegistry/{StateViewSlice → StateView}/UiFragments.res.mjs +1 -1
- package/src/admin/UiFragmentRegistry/{StateViewSlice → StateView}/UiFragments_Projection.res.mjs +1 -1
- package/src/components/Api/GraphQL_FragmentGenerator.res +12 -1
- package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +2 -1
- package/src/components/Api/SchemaType.res +66 -1
- package/src/components/Api/SchemaType.res.mjs +66 -3
- package/src/components/Api/SuryToJsonSchema.res +1 -0
- package/src/components/Api/SuryToJsonSchema.res.mjs +11 -0
- package/src/components/ReadModel/ReadModel_Callback.res +5 -2
- package/src/components/ReadModel/ReadModel_Callback.res.mjs +2 -5
- package/src/components/StateViewSlice/StateViewSlice_Builder.res +9 -3
- package/src/components/StateViewSlice/StateViewSlice_Builder.res.mjs +1 -1
- package/src/components/StateViewSlice/StateViewSlice_Callback.res +5 -1
- package/src/components/StateViewSlice/StateViewSlice_Callback.res.mjs +1 -1
- package/src/plugin/component/Plugin_Structure.res +9 -46
- package/src/plugin/component/Plugin_Structure.res.mjs +8 -40
- package/src/plugin/connect/PluginExtensionPoint_UiFragment.res.mjs +1 -1
- package/tests/admin/AdminApiSchemaDriftTest.res.mjs +1 -1
- package/tests/api/BrandedMarkerCompositionTest.res +74 -0
- package/tests/api/BrandedMarkerCompositionTest.res.mjs +74 -0
- package/tests/api/PsBrandedMarkersReadModel.res +24 -0
- package/tests/api/PsBrandedMarkersReadModel.res.mjs +87 -0
- package/tests/api/SuryToJsonSchemaTest.res +35 -3
- package/tests/api/SuryToJsonSchemaTest.res.mjs +35 -4
- package/tests/plugin/PluginStructureAccessTest.res.mjs +3 -3
- package/tests/plugin/PluginStructureTest.res +5 -5
- package/tests/plugin/PluginStructureTest.res.mjs +26 -26
- package/tests/plugin/{StateChangeSlice → StateChange}/PsAttachInvoice.res.mjs +1 -1
- package/tests/plugin/{StateChangeSlice → StateChange}/PsChangePhoto.res.mjs +1 -1
- package/tests/plugin/{StateChangeSlice → StateChange}/PsDispatchShipment.res.mjs +1 -1
- package/tests/plugin/{StateChangeSlice → StateChange}/PsGatedCommands.res.mjs +1 -1
- package/tests/plugin/{StateChangeSlice → StateChange}/PsPlaceOrder.res.mjs +1 -1
- package/tests/plugin/{StateChangeSlice → StateChange}/PsReserveStock.res.mjs +1 -1
- package/tests/plugin/{StateChangeSlice → StateChange}/PsShipOrder.res.mjs +1 -1
- package/tests/plugin/{StateChangeSlice → StateChange}/PsTransitionSwitch.res.mjs +1 -1
- package/tests/plugin/{StateChangeSlice → StateChange}/PsTypoStore.res.mjs +1 -1
- package/tests/plugin/{StateChangeSlice → StateChange}/PsUploadAvatar.res.mjs +1 -1
- package/tests/plugin/{StateChangeSlice → StateChange}/PsUploadImages.res.mjs +1 -1
- package/tests/plugin/{StateViewSlice → StateView}/PsAnnotatedView.res.mjs +1 -1
- package/tests/plugin/{StateViewSlice → StateView}/PsAvailableProductsView.res.mjs +1 -1
- package/tests/plugin/{StateViewSlice → StateView}/PsCategoriesView.res.mjs +1 -1
- package/tests/plugin/{StateViewSlice → StateView}/PsCustomersView.res.mjs +1 -1
- package/tests/plugin/{StateViewSlice → StateView}/PsGatedView.res.mjs +1 -1
- package/tests/plugin/{StateViewSlice → StateView}/PsOrdersView.res.mjs +1 -1
- package/tests/plugin/{StateViewSlice → StateView}/PsShipmentsView.res.mjs +1 -1
- package/tests/projection/LifecycleTrailTest.res +139 -0
- package/tests/projection/LifecycleTrailTest.res.mjs +280 -0
- /package/src/admin/UiFragmentRegistry/{StateChangeSlice → StateChange}/UiFragmentRegistry.res +0 -0
- /package/src/admin/UiFragmentRegistry/{StateChangeSlice → StateChange}/UiFragmentRegistry_Behavior.res +0 -0
- /package/src/admin/UiFragmentRegistry/{StateViewSlice → StateView}/UiFragments.res +0 -0
- /package/src/admin/UiFragmentRegistry/{StateViewSlice → StateView}/UiFragments_Projection.res +0 -0
- /package/tests/plugin/{StateChangeSlice → StateChange}/PsAttachInvoice.res +0 -0
- /package/tests/plugin/{StateChangeSlice → StateChange}/PsChangePhoto.res +0 -0
- /package/tests/plugin/{StateChangeSlice → StateChange}/PsDispatchShipment.res +0 -0
- /package/tests/plugin/{StateChangeSlice → StateChange}/PsGatedCommands.res +0 -0
- /package/tests/plugin/{StateChangeSlice → StateChange}/PsPlaceOrder.res +0 -0
- /package/tests/plugin/{StateChangeSlice → StateChange}/PsReserveStock.res +0 -0
- /package/tests/plugin/{StateChangeSlice → StateChange}/PsShipOrder.res +0 -0
- /package/tests/plugin/{StateChangeSlice → StateChange}/PsTransitionSwitch.res +0 -0
- /package/tests/plugin/{StateChangeSlice → StateChange}/PsTypoStore.res +0 -0
- /package/tests/plugin/{StateChangeSlice → StateChange}/PsUploadAvatar.res +0 -0
- /package/tests/plugin/{StateChangeSlice → StateChange}/PsUploadImages.res +0 -0
- /package/tests/plugin/{StateViewSlice → StateView}/PsAnnotatedView.res +0 -0
- /package/tests/plugin/{StateViewSlice → StateView}/PsAvailableProductsView.res +0 -0
- /package/tests/plugin/{StateViewSlice → StateView}/PsCategoriesView.res +0 -0
- /package/tests/plugin/{StateViewSlice → StateView}/PsCustomersView.res +0 -0
- /package/tests/plugin/{StateViewSlice → StateView}/PsGatedView.res +0 -0
- /package/tests/plugin/{StateViewSlice → StateView}/PsOrdersView.res +0 -0
- /package/tests/plugin/{StateViewSlice → StateView}/PsShipmentsView.res +0 -0
|
@@ -10,6 +10,13 @@ type rec schemaType =
|
|
|
10
10
|
| ScalarBigInt
|
|
11
11
|
| EntityId
|
|
12
12
|
| DateTime
|
|
13
|
+
| /** A calendar day, with no instant in it. A shape of its own rather than a
|
|
14
|
+
plain string so `format: "date"` reaches the JSON Schema — but, unlike
|
|
15
|
+
`DateTime`, it is not excluded from the generic semantic wrapper below,
|
|
16
|
+
so it carries `x-reventless-semantic` as well. `DateTime`'s exclusion
|
|
17
|
+
exists because its bare-`format` output predates the marker and is a
|
|
18
|
+
published contract; a new semantic has no such history. */
|
|
19
|
+
CalendarDate
|
|
13
20
|
| Nullable(schemaType)
|
|
14
21
|
| ArrayOf(schemaType)
|
|
15
22
|
| ObjectRef(string, dict<schemaType>)
|
|
@@ -32,6 +39,7 @@ type rec schemaType =
|
|
|
32
39
|
|
|
33
40
|
let isTagged = Reventless.DcbTag.isTagged
|
|
34
41
|
let isDateTime = Reventless.DateTime.isDateTime
|
|
42
|
+
let isCalendarDate = Reventless.CalendarDate.isCalendarDate
|
|
35
43
|
|
|
36
44
|
let isIdFieldName = (name: string): bool => {
|
|
37
45
|
let lower = String.toLowerCase(name)
|
|
@@ -107,6 +115,7 @@ and shapeOf = (~parentName: string, ~fieldName: string, schema: S.t<unknown>): s
|
|
|
107
115
|
switch schema {
|
|
108
116
|
| String({const: ?Some(_)}) => ScalarString
|
|
109
117
|
| String(_) if isDateTime(schema) => DateTime
|
|
118
|
+
| String(_) if isCalendarDate(schema) => CalendarDate
|
|
110
119
|
| String(_) => isIdFieldName(fieldName) ? EntityId : ScalarString
|
|
111
120
|
// `Int32` and `Port` are both whole-number formats; anything else (or no
|
|
112
121
|
// format at all) is a `float` as far as the schema is concerned.
|
|
@@ -348,15 +357,71 @@ let optionalFieldNames = (schema: S.t<unknown>): array<string> =>
|
|
|
348
357
|
| _ => []
|
|
349
358
|
}
|
|
350
359
|
|
|
360
|
+
// A lifecycle trail's entries are `{state, at}`, and `state` holds the record's
|
|
361
|
+
// own lifecycle. Walked under the lifecycle field's name so it resolves to the
|
|
362
|
+
// enum that field already emits, rather than a second copy under the trail's
|
|
363
|
+
// path — the trail and the current state must read as one vocabulary.
|
|
364
|
+
let trailShape = (
|
|
365
|
+
~parentName: string,
|
|
366
|
+
~trailField: string,
|
|
367
|
+
~lifecycleField: string,
|
|
368
|
+
schema: S.t<unknown>,
|
|
369
|
+
): schemaType => {
|
|
370
|
+
let entryName =
|
|
371
|
+
parentName ++
|
|
372
|
+
trailField->String.charAt(0)->String.toUpperCase ++
|
|
373
|
+
trailField->String.slice(~start=1, ~end=trailField->String.length)
|
|
374
|
+
let entrySchema = switch schema {
|
|
375
|
+
| Array({additionalItems: Schema(item)}) => Some(item)
|
|
376
|
+
| _ => None
|
|
377
|
+
}
|
|
378
|
+
switch entrySchema {
|
|
379
|
+
| Some(Object({properties})) =>
|
|
380
|
+
let fields = Dict.make()
|
|
381
|
+
properties
|
|
382
|
+
->Dict.toArray
|
|
383
|
+
->Array.forEach(((propName, propSchema)) =>
|
|
384
|
+
fields->Dict.set(
|
|
385
|
+
propName,
|
|
386
|
+
propName === "state"
|
|
387
|
+
? fromSury(~parentName, ~fieldName=lifecycleField, propSchema)
|
|
388
|
+
: fromSury(~parentName=entryName, ~fieldName=propName, propSchema),
|
|
389
|
+
)
|
|
390
|
+
)
|
|
391
|
+
let shape = ArrayOf(ObjectRef(entryName, fields))
|
|
392
|
+
// Walking the entries by shape skips the semantic dispatch `fromSury` does,
|
|
393
|
+
// so the marker is re-applied here. Without it the trail reaches a consumer
|
|
394
|
+
// as an unremarkable array of objects, and recognising it would come down to
|
|
395
|
+
// the field's name.
|
|
396
|
+
switch Reventless.Semantic.get(schema) {
|
|
397
|
+
| Some(sem) => Semantic(sem, shape)
|
|
398
|
+
| None => shape
|
|
399
|
+
}
|
|
400
|
+
| _ => fromSury(~parentName, ~fieldName=trailField, schema)
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
351
404
|
let fromSuryObject = (~typeName: string, schema: S.t<unknown>): option<dict<schemaType>> =>
|
|
352
405
|
switch schema {
|
|
353
406
|
| Object({properties}) =>
|
|
407
|
+
let trail = switch (
|
|
408
|
+
Reventless.Lifecycle.Trail.fieldName(schema),
|
|
409
|
+
Reventless.Lifecycle.fieldName(schema),
|
|
410
|
+
) {
|
|
411
|
+
| (Some(trailField), Some(lifecycleField)) => Some((trailField, lifecycleField))
|
|
412
|
+
| _ => None
|
|
413
|
+
}
|
|
354
414
|
let fields = Dict.make()
|
|
355
415
|
properties
|
|
356
416
|
->Dict.toArray
|
|
357
417
|
->Array.forEach(((propName, propSchema)) => {
|
|
358
418
|
if propName !== "TAG" {
|
|
359
|
-
|
|
419
|
+
let shape = switch trail {
|
|
420
|
+
| Some((trailField, lifecycleField)) if trailField === propName =>
|
|
421
|
+
trailShape(~parentName=typeName, ~trailField, ~lifecycleField, propSchema)
|
|
422
|
+
| _ => fromSury(~parentName=typeName, ~fieldName=propName, propSchema)
|
|
423
|
+
}
|
|
424
|
+
fields->Dict.set(propName, shape)
|
|
360
425
|
}
|
|
361
426
|
})
|
|
362
427
|
Some(fields)
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
4
4
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
5
|
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
6
|
-
import * as DateTime$Reventless from "@reventlessdev/reventless-spec/src/
|
|
6
|
+
import * as DateTime$Reventless from "@reventlessdev/reventless-spec/src/semantic/DateTime.res.mjs";
|
|
7
7
|
import * as Semantic$Reventless from "@reventlessdev/reventless-spec/src/semantic/Semantic.res.mjs";
|
|
8
|
+
import * as Lifecycle$Reventless from "@reventlessdev/reventless-spec/src/types/Lifecycle.res.mjs";
|
|
8
9
|
import * as Reference$Reventless from "@reventlessdev/reventless-spec/src/components/Reference.res.mjs";
|
|
9
10
|
import * as TaggedUnion$Reventless from "@reventlessdev/reventless-spec/src/components/TaggedUnion.res.mjs";
|
|
11
|
+
import * as CalendarDate$Reventless from "@reventlessdev/reventless-spec/src/semantic/CalendarDate.res.mjs";
|
|
10
12
|
|
|
11
13
|
function isIdFieldName(name) {
|
|
12
14
|
let lower = name.toLowerCase();
|
|
@@ -79,6 +81,8 @@ function shapeOf(parentName, fieldName, schema) {
|
|
|
79
81
|
return "ScalarString";
|
|
80
82
|
} else if (DateTime$Reventless.isDateTime(schema)) {
|
|
81
83
|
return "DateTime";
|
|
84
|
+
} else if (CalendarDate$Reventless.isCalendarDate(schema)) {
|
|
85
|
+
return "CalendarDate";
|
|
82
86
|
} else if (isIdFieldName(fieldName)) {
|
|
83
87
|
return "EntityId";
|
|
84
88
|
} else {
|
|
@@ -324,17 +328,72 @@ function optionalFieldNames(schema) {
|
|
|
324
328
|
}
|
|
325
329
|
}
|
|
326
330
|
|
|
331
|
+
function trailShape(parentName, trailField, lifecycleField, schema) {
|
|
332
|
+
let entryName = parentName + trailField.charAt(0).toUpperCase() + trailField.slice(1, trailField.length);
|
|
333
|
+
let entrySchema;
|
|
334
|
+
if (schema.type === "array") {
|
|
335
|
+
let item = schema.additionalItems;
|
|
336
|
+
entrySchema = item === "strip" || item === "strict" ? undefined : item;
|
|
337
|
+
} else {
|
|
338
|
+
entrySchema = undefined;
|
|
339
|
+
}
|
|
340
|
+
if (entrySchema === undefined) {
|
|
341
|
+
return fromSury(parentName, trailField, schema);
|
|
342
|
+
}
|
|
343
|
+
if (entrySchema.type !== "object") {
|
|
344
|
+
return fromSury(parentName, trailField, schema);
|
|
345
|
+
}
|
|
346
|
+
let fields = {};
|
|
347
|
+
Object.entries(entrySchema.properties).forEach(param => {
|
|
348
|
+
let propSchema = param[1];
|
|
349
|
+
let propName = param[0];
|
|
350
|
+
fields[propName] = propName === "state" ? fromSury(parentName, lifecycleField, propSchema) : fromSury(entryName, propName, propSchema);
|
|
351
|
+
});
|
|
352
|
+
let shape = {
|
|
353
|
+
TAG: "ArrayOf",
|
|
354
|
+
_0: {
|
|
355
|
+
TAG: "ObjectRef",
|
|
356
|
+
_0: entryName,
|
|
357
|
+
_1: fields
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
let sem = Semantic$Reventless.get(schema);
|
|
361
|
+
if (sem !== undefined) {
|
|
362
|
+
return {
|
|
363
|
+
TAG: "Semantic",
|
|
364
|
+
_0: sem,
|
|
365
|
+
_1: shape
|
|
366
|
+
};
|
|
367
|
+
} else {
|
|
368
|
+
return shape;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
327
372
|
function fromSuryObject(typeName, schema) {
|
|
328
373
|
if (schema.type !== "object") {
|
|
329
374
|
return;
|
|
330
375
|
}
|
|
376
|
+
let match = Lifecycle$Reventless.Trail.fieldName(schema);
|
|
377
|
+
let match$1 = Lifecycle$Reventless.fieldName(schema);
|
|
378
|
+
let trail = match !== undefined && match$1 !== undefined ? [
|
|
379
|
+
match,
|
|
380
|
+
match$1
|
|
381
|
+
] : undefined;
|
|
331
382
|
let fields = {};
|
|
332
383
|
Object.entries(schema.properties).forEach(param => {
|
|
333
384
|
let propName = param[0];
|
|
334
|
-
if (propName
|
|
335
|
-
fields[propName] = fromSury(typeName, propName, param[1]);
|
|
385
|
+
if (propName === "TAG") {
|
|
336
386
|
return;
|
|
337
387
|
}
|
|
388
|
+
let propSchema = param[1];
|
|
389
|
+
let shape;
|
|
390
|
+
if (trail !== undefined) {
|
|
391
|
+
let trailField = trail[0];
|
|
392
|
+
shape = trailField === propName ? trailShape(typeName, trailField, trail[1], propSchema) : fromSury(typeName, propName, propSchema);
|
|
393
|
+
} else {
|
|
394
|
+
shape = fromSury(typeName, propName, propSchema);
|
|
395
|
+
}
|
|
396
|
+
fields[propName] = shape;
|
|
338
397
|
});
|
|
339
398
|
return fields;
|
|
340
399
|
}
|
|
@@ -343,11 +402,14 @@ let isTagged = DcbTag$Reventless.isTagged;
|
|
|
343
402
|
|
|
344
403
|
let isDateTime = DateTime$Reventless.isDateTime;
|
|
345
404
|
|
|
405
|
+
let isCalendarDate = CalendarDate$Reventless.isCalendarDate;
|
|
406
|
+
|
|
346
407
|
let maxReportDepth = 12;
|
|
347
408
|
|
|
348
409
|
export {
|
|
349
410
|
isTagged,
|
|
350
411
|
isDateTime,
|
|
412
|
+
isCalendarDate,
|
|
351
413
|
isIdFieldName,
|
|
352
414
|
isIdsFieldName,
|
|
353
415
|
semanticCompositeNames,
|
|
@@ -358,6 +420,7 @@ export {
|
|
|
358
420
|
collectUnclassifiedUnions,
|
|
359
421
|
unclassifiedUnions,
|
|
360
422
|
optionalFieldNames,
|
|
423
|
+
trailShape,
|
|
361
424
|
fromSuryObject,
|
|
362
425
|
}
|
|
363
426
|
/* DcbTag-Reventless Not a pure module */
|
|
@@ -207,6 +207,7 @@ let rec fromSchemaType = (~inputNames: bool=false, st: SchemaType.schemaType): J
|
|
|
207
207
|
| ScalarBigInt => jsonObject([("type", str("integer"))])
|
|
208
208
|
| EntityId => jsonObject([("type", str("string")), ("format", str("uuid"))])
|
|
209
209
|
| DateTime => jsonObject([("type", str("string")), ("format", str("date-time"))])
|
|
210
|
+
| CalendarDate => jsonObject([("type", str("string")), ("format", str("date"))])
|
|
210
211
|
| Nullable(inner) =>
|
|
211
212
|
let innerSchema = fromSchemaType(~inputNames, inner)
|
|
212
213
|
jsonObject([("oneOf", JSON.Encode.array([innerSchema, jsonObject([("type", str("null"))])]))])
|
|
@@ -212,6 +212,17 @@ function fromSchemaType(inputNamesOpt, st) {
|
|
|
212
212
|
"date-time"
|
|
213
213
|
]
|
|
214
214
|
]);
|
|
215
|
+
case "CalendarDate" :
|
|
216
|
+
return Object.fromEntries([
|
|
217
|
+
[
|
|
218
|
+
"type",
|
|
219
|
+
"string"
|
|
220
|
+
],
|
|
221
|
+
[
|
|
222
|
+
"format",
|
|
223
|
+
"date"
|
|
224
|
+
]
|
|
225
|
+
]);
|
|
215
226
|
case "ScalarString" :
|
|
216
227
|
case "Unknown" :
|
|
217
228
|
return Object.fromEntries([[
|
|
@@ -31,13 +31,16 @@ module Make = (
|
|
|
31
31
|
json,
|
|
32
32
|
)} actions:${actionsStr}`,
|
|
33
33
|
)->Effect.runSync
|
|
34
|
-
|
|
34
|
+
// Rewritten here rather than at the apply below: a trail entry is
|
|
35
|
+
// stamped with this envelope's own time.
|
|
36
|
+
actions->Array.map(
|
|
37
|
+
Projection.rewriteAction(_, ~at=context.meta.time, ReadModelSpec.stateSchema),
|
|
38
|
+
)
|
|
35
39
|
})
|
|
36
40
|
->Array.flat
|
|
37
41
|
->Array.reduce(Effect.succeed(), (acc, action) =>
|
|
38
42
|
acc->Effect.flatMap(
|
|
39
43
|
_ => {
|
|
40
|
-
let action = Projection.rewriteAction(action, ReadModelSpec.stateSchema)
|
|
41
44
|
Effect.promise(
|
|
42
45
|
() => Projection.handleAction(~comp, action, Spec.operations, ReadModelSpec.subIdConfig),
|
|
43
46
|
)->Effect.map(_ => ())
|
|
@@ -28,11 +28,8 @@ function Make(ReadModelSpec) {
|
|
|
28
28
|
let actionsStr = LogFormat$ReventlessCore.actionNames(actions);
|
|
29
29
|
let idxStr = (idx + 1 | 0).toString();
|
|
30
30
|
Effect.runSync(EffectLogger$ReventlessCore.logInfo(comp, json, `handling event ` + idxStr + `/` + total + ` from ` + sourceName + `: ` + LogFormat$ReventlessCore.eventDetail(json) + ` actions:` + actionsStr));
|
|
31
|
-
return actions;
|
|
32
|
-
}).flat(), Effect.succeed(), (acc, action) => Effect.flatMap(acc, () => {
|
|
33
|
-
let action$1 = Projection$ReventlessCore.rewriteAction(action, ReadModelSpec.stateSchema);
|
|
34
|
-
return Effect.map(Effect.promise(() => Projection$ReventlessCore.handleAction(comp, action$1, Spec.operations, ReadModelSpec.subIdConfig)), param => {});
|
|
35
|
-
}));
|
|
31
|
+
return actions.map(__x => Projection$ReventlessCore.rewriteAction(__x, context.meta.time, ReadModelSpec.stateSchema));
|
|
32
|
+
}).flat(), Effect.succeed(), (acc, action) => Effect.flatMap(acc, () => Effect.map(Effect.promise(() => Projection$ReventlessCore.handleAction(comp, action, Spec.operations, ReadModelSpec.subIdConfig)), param => {})));
|
|
36
33
|
});
|
|
37
34
|
return {
|
|
38
35
|
EventProjector: EventProjector,
|
|
@@ -148,17 +148,23 @@ module Make = (
|
|
|
148
148
|
~detail=eventData,
|
|
149
149
|
`handling event ${idxStr}/${total}: ${LogFormat.bold(eventType)}${fieldsStr} ${actionsStr}`,
|
|
150
150
|
)->Effect.runSync
|
|
151
|
-
|
|
151
|
+
// A slice's state carries the same synthetic `displayName` the
|
|
152
|
+
// ppx injects for a read model's, and the same lifecycle trail.
|
|
153
|
+
// Rewritten here rather than at the apply below because the
|
|
154
|
+
// trail entry is stamped with this envelope's own time.
|
|
155
|
+
actions->Array.map(
|
|
156
|
+
FrameworkProjection.rewriteAction(_, ~at=meta.time, Spec.stateSchema),
|
|
157
|
+
)
|
|
152
158
|
| None => []
|
|
153
159
|
}
|
|
154
160
|
})
|
|
155
161
|
->Array.flat
|
|
156
162
|
->Array.reduce(Effect.succeed(), (acc, action) =>
|
|
157
|
-
acc->Effect.flatMap(_ =>
|
|
163
|
+
acc->Effect.flatMap(_ => {
|
|
158
164
|
Effect.promise(() =>
|
|
159
165
|
FrameworkProjection.handleAction(~comp, action, projectionOps, Spec.subIdConfig)
|
|
160
166
|
)->Effect.map(_ => ())
|
|
161
|
-
)
|
|
167
|
+
})
|
|
162
168
|
)
|
|
163
169
|
})
|
|
164
170
|
|
|
@@ -120,7 +120,7 @@ function Make(RuntimeEnvironment) {
|
|
|
120
120
|
let f = Object.entries(dataDict).map(param => param[0] + `:` + JSON.stringify(param[1])).join(",");
|
|
121
121
|
let fieldsStr = f === "" ? "" : `({` + f + `})`;
|
|
122
122
|
Effect.runSync(EffectLogger$ReventlessCore.logInfo(comp, dataDict, `handling event ` + idxStr + `/` + total + `: ` + LogFormat$ReventlessCore.bold(eventType) + fieldsStr + ` ` + actionsStr));
|
|
123
|
-
return actions;
|
|
123
|
+
return actions.map(__x => Projection$ReventlessCore.rewriteAction(__x, meta.time, Spec.stateSchema));
|
|
124
124
|
}).flat(), Effect.succeed(), (acc, action) => Effect.flatMap(acc, () => Effect.map(Effect.promise(() => Projection$ReventlessCore.handleAction(comp, action, projectionOps, Spec.subIdConfig)), param => {})));
|
|
125
125
|
});
|
|
126
126
|
let handler = SpecificEventCollector.makeHandler(ec, jsonEventsHandler);
|
|
@@ -47,7 +47,11 @@ module Make = (
|
|
|
47
47
|
~detail=raw.data,
|
|
48
48
|
`handling event ${idx.contents->Int.toString}/${count}: ${LogFormat.bold(raw.eventType)}(${id}) ${actionsStr}`,
|
|
49
49
|
)->Effect.runSync
|
|
50
|
-
allActions->Array.pushMany(
|
|
50
|
+
allActions->Array.pushMany(
|
|
51
|
+
actions->Array.map(
|
|
52
|
+
FrameworkProjection.rewriteAction(_, ~at=raw.meta.time, Spec.stateSchema),
|
|
53
|
+
),
|
|
54
|
+
)
|
|
51
55
|
Some(event)
|
|
52
56
|
| None => None
|
|
53
57
|
}
|
|
@@ -35,7 +35,7 @@ function Make(Spec) {
|
|
|
35
35
|
});
|
|
36
36
|
let actionsStr = LogFormat$ReventlessCore.actionNames(actions);
|
|
37
37
|
Effect.runSync(EffectLogger$ReventlessCore.logInfo(comp, raw.data, `handling event ` + idx.contents.toString() + `/` + count + `: ` + LogFormat$ReventlessCore.bold(raw.eventType) + `(` + id + `) ` + actionsStr));
|
|
38
|
-
allActions.push(...actions);
|
|
38
|
+
allActions.push(...actions.map(__x => Projection$ReventlessCore.rewriteAction(__x, raw.meta.time, Spec.stateSchema)));
|
|
39
39
|
return Primitive_option.some(event);
|
|
40
40
|
});
|
|
41
41
|
let skipped = rawEvents.length - events.length | 0;
|
|
@@ -21,20 +21,6 @@ let rec isLabelShape = (t: SchemaType.schemaType): bool =>
|
|
|
21
21
|
| _ => false
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
// Whether a field can hold a lifecycle: a closed set of values, or an optional
|
|
25
|
-
// one. A free-text `lifecycle: string` is not a lifecycle — filtering a command
|
|
26
|
-
// menu against `allowedStates` needs states to compare with.
|
|
27
|
-
let rec isLifecycleShape = (t: SchemaType.schemaType): bool =>
|
|
28
|
-
switch t {
|
|
29
|
-
| Enum(_, _) => true
|
|
30
|
-
| Nullable(inner) => isLifecycleShape(inner)
|
|
31
|
-
// A union is a closed set of *shapes*, not of values, and `allowedStates`
|
|
32
|
-
// compares values. The same answer the ppx gives `@lifecycle` on a union
|
|
33
|
-
// field, which is a compile error — this is the convention rung agreeing.
|
|
34
|
-
| TaggedUnion(_, _) => false
|
|
35
|
-
| _ => false
|
|
36
|
-
}
|
|
37
|
-
|
|
38
24
|
// Field names that say "this one is the record's name" in the only way
|
|
39
25
|
// available short of the `@displayName` annotation. Matched case-insensitively
|
|
40
26
|
// and *exactly*: `customerName` holds a customer's name, not this record's.
|
|
@@ -43,33 +29,10 @@ let conventionalLabelNames = ["name", "title", "label", "displayname"]
|
|
|
43
29
|
let shapeOfField = (~entityName: string, ~name: string, schema: S.t<unknown>): SchemaType.schemaType =>
|
|
44
30
|
SchemaType.fromSury(~parentName=entityName, ~fieldName=name, schema)
|
|
45
31
|
|
|
46
|
-
// The field holding the entity's lifecycle, for filtering a per-row command
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
let lifecycleFieldFromStateSchema =
|
|
50
|
-
~entityName: string,
|
|
51
|
-
stateSchema: S.t<unknown>,
|
|
52
|
-
): option<string> => {
|
|
53
|
-
let annotated = switch Reventless.StateAnnotations.getSpec(stateSchema) {
|
|
54
|
-
| Some(spec) => spec.lifecycle
|
|
55
|
-
| None => None
|
|
56
|
-
}
|
|
57
|
-
switch annotated {
|
|
58
|
-
| Some(_) as some => some
|
|
59
|
-
| None =>
|
|
60
|
-
switch stateSchema {
|
|
61
|
-
| Object({properties}) =>
|
|
62
|
-
properties
|
|
63
|
-
->Dict.get("lifecycle")
|
|
64
|
-
->Option.flatMap(schema =>
|
|
65
|
-
isLifecycleShape(shapeOfField(~entityName, ~name="lifecycle", schema))
|
|
66
|
-
? Some("lifecycle")
|
|
67
|
-
: None
|
|
68
|
-
)
|
|
69
|
-
| _ => None
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
32
|
+
// The field holding the entity's lifecycle, for filtering a per-row command
|
|
33
|
+
// menu. One rule, stated in `Reventless.Lifecycle` — the projection machinery
|
|
34
|
+
// resolves the field a trail entry records against from the same place.
|
|
35
|
+
let lifecycleFieldFromStateSchema = Reventless.Lifecycle.fieldName
|
|
73
36
|
|
|
74
37
|
// The field whose truth withdraws a row from ordinary reads. Annotation-only —
|
|
75
38
|
// no convention rung, since guessing wrong here makes rows vanish.
|
|
@@ -113,7 +76,7 @@ let checkRetiredValue = (~entityName: string, stateSchema: S.t<unknown>): retire
|
|
|
113
76
|
| None | Some({values: None}) => NotDeclared
|
|
114
77
|
| Some({field, values: Some(values)}) =>
|
|
115
78
|
let named = values->Array.join(", ")
|
|
116
|
-
let lifecycle = lifecycleFieldFromStateSchema(
|
|
79
|
+
let lifecycle = lifecycleFieldFromStateSchema(stateSchema)
|
|
117
80
|
if lifecycle != Some(field) {
|
|
118
81
|
log.warn(
|
|
119
82
|
~comp="Plugin_Structure",
|
|
@@ -190,7 +153,7 @@ let lifecycleStatesFromStateSchema = (
|
|
|
190
153
|
~entityName: string,
|
|
191
154
|
stateSchema: S.t<unknown>,
|
|
192
155
|
): option<array<string>> =>
|
|
193
|
-
lifecycleFieldFromStateSchema(
|
|
156
|
+
lifecycleFieldFromStateSchema(stateSchema)->Option.flatMap(field =>
|
|
194
157
|
switch stateSchema {
|
|
195
158
|
| Object({properties}) =>
|
|
196
159
|
properties
|
|
@@ -965,7 +928,7 @@ let queryableDefFromSpec = (
|
|
|
965
928
|
labelField: label.field,
|
|
966
929
|
searchableFields: label.searchableFields,
|
|
967
930
|
labelFieldSource: Some(labelFieldSourceToString(label.source)),
|
|
968
|
-
lifecycleField: lifecycleFieldFromStateSchema(
|
|
931
|
+
lifecycleField: lifecycleFieldFromStateSchema(stateSchema),
|
|
969
932
|
ownerField: Reventless.Owner.fieldNames(stateSchema)->Array.get(0),
|
|
970
933
|
retiredField: retiredFieldFromStateSchema(stateSchema),
|
|
971
934
|
retiredValues: retiredValuesFromStateSchema(stateSchema),
|
|
@@ -1410,7 +1373,7 @@ let make = (
|
|
|
1410
1373
|
labelField: label.field,
|
|
1411
1374
|
searchableFields: label.searchableFields,
|
|
1412
1375
|
labelFieldSource: Some(labelFieldSourceToString(label.source)),
|
|
1413
|
-
lifecycleField: lifecycleFieldFromStateSchema(
|
|
1376
|
+
lifecycleField: lifecycleFieldFromStateSchema(stateSchema),
|
|
1414
1377
|
// Same schema `lifecycleField` reads, so the two cannot disagree about which
|
|
1415
1378
|
// fields this view has.
|
|
1416
1379
|
ownerField: Reventless.Owner.fieldNames(stateSchema)->Array.get(0),
|
|
@@ -1451,7 +1414,7 @@ let make = (
|
|
|
1451
1414
|
labelField: label.field,
|
|
1452
1415
|
searchableFields: label.searchableFields,
|
|
1453
1416
|
labelFieldSource: Some(labelFieldSourceToString(label.source)),
|
|
1454
|
-
lifecycleField: lifecycleFieldFromStateSchema(
|
|
1417
|
+
lifecycleField: lifecycleFieldFromStateSchema(stateSchema),
|
|
1455
1418
|
ownerField: Reventless.Owner.fieldNames(stateSchema)->Array.get(0),
|
|
1456
1419
|
retiredField: retiredFieldFromStateSchema(stateSchema),
|
|
1457
1420
|
retiredValues: retiredValuesFromStateSchema(stateSchema),
|
|
@@ -12,6 +12,7 @@ import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js
|
|
|
12
12
|
import * as Trait$Reventless from "@reventlessdev/reventless-spec/src/types/Trait.res.mjs";
|
|
13
13
|
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
14
14
|
import * as Message$Reventless from "@reventlessdev/reventless-spec/src/types/Message.res.mjs";
|
|
15
|
+
import * as Lifecycle$Reventless from "@reventlessdev/reventless-spec/src/types/Lifecycle.res.mjs";
|
|
15
16
|
import * as Reference$Reventless from "@reventlessdev/reventless-spec/src/components/Reference.res.mjs";
|
|
16
17
|
import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
|
|
17
18
|
import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
|
|
@@ -47,24 +48,6 @@ function isLabelShape(_t) {
|
|
|
47
48
|
};
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
function isLifecycleShape(_t) {
|
|
51
|
-
while (true) {
|
|
52
|
-
let t = _t;
|
|
53
|
-
if (typeof t !== "object") {
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
switch (t.TAG) {
|
|
57
|
-
case "Nullable" :
|
|
58
|
-
_t = t._0;
|
|
59
|
-
continue;
|
|
60
|
-
case "Enum" :
|
|
61
|
-
return true;
|
|
62
|
-
default:
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
51
|
let conventionalLabelNames = [
|
|
69
52
|
"name",
|
|
70
53
|
"title",
|
|
@@ -74,22 +57,6 @@ let conventionalLabelNames = [
|
|
|
74
57
|
|
|
75
58
|
let shapeOfField = SchemaType$ReventlessCore.fromSury;
|
|
76
59
|
|
|
77
|
-
function lifecycleFieldFromStateSchema(entityName, stateSchema) {
|
|
78
|
-
let spec = StateAnnotations$Reventless.getSpec(stateSchema);
|
|
79
|
-
let annotated = spec !== undefined ? spec.lifecycle : undefined;
|
|
80
|
-
if (annotated !== undefined) {
|
|
81
|
-
return annotated;
|
|
82
|
-
} else if (stateSchema.type === "object") {
|
|
83
|
-
return Stdlib_Option.flatMap(stateSchema.properties["lifecycle"], schema => {
|
|
84
|
-
if (isLifecycleShape(SchemaType$ReventlessCore.fromSury(entityName, "lifecycle", schema))) {
|
|
85
|
-
return "lifecycle";
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
} else {
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
60
|
function retiredFromStateSchema(stateSchema) {
|
|
94
61
|
let spec = StateAnnotations$Reventless.getSpec(stateSchema);
|
|
95
62
|
if (spec !== undefined) {
|
|
@@ -120,7 +87,7 @@ function checkRetiredValue(entityName, stateSchema) {
|
|
|
120
87
|
}
|
|
121
88
|
let field = match.field;
|
|
122
89
|
let named = values.join(", ");
|
|
123
|
-
let lifecycle =
|
|
90
|
+
let lifecycle = Lifecycle$Reventless.fieldName(stateSchema);
|
|
124
91
|
if (Primitive_object.notequal(lifecycle, field)) {
|
|
125
92
|
log.warn("Plugin_Structure", undefined, entityName + `: @retired(` + named + `) is on "` + field + `", which is not this record's lifecycle field` + Stdlib_Option.getOr(Stdlib_Option.map(lifecycle, f => ` (that is "` + f + `")`), " (it declares none)") + `. A retirement state no command's declared edge can name loses the command filtering the state form exists for.`);
|
|
126
93
|
}
|
|
@@ -169,7 +136,7 @@ function reportRetiredStates(pluginName, failures, unchecked) {
|
|
|
169
136
|
}
|
|
170
137
|
|
|
171
138
|
function lifecycleStatesFromStateSchema(entityName, stateSchema) {
|
|
172
|
-
return Stdlib_Option.flatMap(
|
|
139
|
+
return Stdlib_Option.flatMap(Lifecycle$Reventless.fieldName(stateSchema), field => {
|
|
173
140
|
if (stateSchema.type === "object") {
|
|
174
141
|
return Stdlib_Option.map(stateSchema.properties[field], schema => {
|
|
175
142
|
let match = SchemaType$ReventlessCore.fromSury(entityName, field, schema);
|
|
@@ -715,7 +682,7 @@ function queryableDefFromSpec(plugin, name, stateSchema, authorization, visibili
|
|
|
715
682
|
labelField: label.field,
|
|
716
683
|
searchableFields: label.searchableFields,
|
|
717
684
|
labelFieldSource: labelFieldSourceToString(label.source),
|
|
718
|
-
lifecycleField:
|
|
685
|
+
lifecycleField: Lifecycle$Reventless.fieldName(stateSchema),
|
|
719
686
|
ownerField: Owner$Reventless.fieldNames(stateSchema)[0],
|
|
720
687
|
retiredField: retiredFieldFromStateSchema(stateSchema),
|
|
721
688
|
retiredValues: retiredValuesFromStateSchema(stateSchema),
|
|
@@ -1024,7 +991,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
1024
991
|
labelField: label.field,
|
|
1025
992
|
searchableFields: label.searchableFields,
|
|
1026
993
|
labelFieldSource: labelFieldSourceToString(label.source),
|
|
1027
|
-
lifecycleField:
|
|
994
|
+
lifecycleField: Lifecycle$Reventless.fieldName(stateSchema),
|
|
1028
995
|
ownerField: Owner$Reventless.fieldNames(stateSchema)[0],
|
|
1029
996
|
retiredField: retiredFieldFromStateSchema(stateSchema),
|
|
1030
997
|
retiredValues: retiredValuesFromStateSchema(stateSchema),
|
|
@@ -1056,7 +1023,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
1056
1023
|
labelField: label.field,
|
|
1057
1024
|
searchableFields: label.searchableFields,
|
|
1058
1025
|
labelFieldSource: labelFieldSourceToString(label.source),
|
|
1059
|
-
lifecycleField:
|
|
1026
|
+
lifecycleField: Lifecycle$Reventless.fieldName(stateSchema),
|
|
1060
1027
|
ownerField: Owner$Reventless.fieldNames(stateSchema)[0],
|
|
1061
1028
|
retiredField: retiredFieldFromStateSchema(stateSchema),
|
|
1062
1029
|
retiredValues: retiredValuesFromStateSchema(stateSchema),
|
|
@@ -1383,11 +1350,12 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
1383
1350
|
};
|
|
1384
1351
|
}
|
|
1385
1352
|
|
|
1353
|
+
let lifecycleFieldFromStateSchema = Lifecycle$Reventless.fieldName;
|
|
1354
|
+
|
|
1386
1355
|
export {
|
|
1387
1356
|
log,
|
|
1388
1357
|
declaredTransitionsOnly,
|
|
1389
1358
|
isLabelShape,
|
|
1390
|
-
isLifecycleShape,
|
|
1391
1359
|
conventionalLabelNames,
|
|
1392
1360
|
shapeOfField,
|
|
1393
1361
|
lifecycleFieldFromStateSchema,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
|
|
4
4
|
import * as Plugin$ReventlessCore from "../component/Plugin.res.mjs";
|
|
5
|
-
import * as UiFragmentRegistry$ReventlessCore from "../../admin/UiFragmentRegistry/
|
|
5
|
+
import * as UiFragmentRegistry$ReventlessCore from "../../admin/UiFragmentRegistry/StateChange/UiFragmentRegistry.res.mjs";
|
|
6
6
|
import * as ExtensionPointMapping$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/ExtensionPointMapping.res.mjs";
|
|
7
7
|
import * as PluginExtensionPointSpec$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/PluginExtensionPointSpec.res.mjs";
|
|
8
8
|
|
|
@@ -4,7 +4,7 @@ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
|
4
4
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
5
|
import * as Plugin$Reventless from "@reventlessdev/reventless-spec/src/components/Plugin.res.mjs";
|
|
6
6
|
import * as SchemaType$ReventlessCore from "../../src/components/Api/SchemaType.res.mjs";
|
|
7
|
-
import * as UiFragments$ReventlessCore from "../../src/admin/UiFragmentRegistry/
|
|
7
|
+
import * as UiFragments$ReventlessCore from "../../src/admin/UiFragmentRegistry/StateView/UiFragments.res.mjs";
|
|
8
8
|
import * as Platform_UIFragmentsApi$ReventlessCore from "../../src/admin/Platform_UIFragmentsApi.res.mjs";
|
|
9
9
|
import * as Platform_PluginStructuresApi$ReventlessCore from "../../src/admin/Platform_PluginStructuresApi.res.mjs";
|
|
10
10
|
import * as Platform_ComponentDefinitionsApi$ReventlessCore from "../../src/admin/Platform_ComponentDefinitionsApi.res.mjs";
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// A field marker and a branded semantic type on the same field. Both markers
|
|
4
|
+
// used to test for the literal `string` keyword, so both saw the brand instead
|
|
5
|
+
// of the string and refused a field they should have taken — which would have
|
|
6
|
+
// made declaring what a field *is* cost it whatever the marker gave.
|
|
7
|
+
//
|
|
8
|
+
// The composition assertion is the one that matters. `@owner` cannot see the
|
|
9
|
+
// schema sury-ppx derives from `Email.t`, so the tempting fix is to inject
|
|
10
|
+
// `Owner.string` over it: that type-checks, passes an "is it marked" test, and
|
|
11
|
+
// silently leaves the field a plain string with the address grammar gone.
|
|
12
|
+
// Asserting the marker is there proves nothing on its own — the rejection below
|
|
13
|
+
// is what says the brand survived.
|
|
14
|
+
describe("a field marker on a branded semantic:", () => {
|
|
15
|
+
let stateSchema = PsBrandedMarkersReadModel.stateSchema->S.castToUnknown
|
|
16
|
+
|
|
17
|
+
let fieldOf = name =>
|
|
18
|
+
switch stateSchema {
|
|
19
|
+
| Object({properties}) => properties->Dict.get(name)
|
|
20
|
+
| _ => None
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("@owner on an Email field:", () => {
|
|
24
|
+
testSync("marks the field as the owner", () =>
|
|
25
|
+
expect(fieldOf("email")->Option.mapOr(false, Reventless.Owner.isFieldOwner))->toBe(true)
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
testSync("keeps the email semantic", () =>
|
|
29
|
+
expect(
|
|
30
|
+
fieldOf("email")
|
|
31
|
+
->Option.flatMap(Reventless.Semantic.getFrom)
|
|
32
|
+
->Option.map(s => s.id),
|
|
33
|
+
)->toEqual(Some("email"))
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
// The regression a substituting fix would land, and it lands in silence:
|
|
37
|
+
// the row still scopes to its owner, and the field accepts anything.
|
|
38
|
+
testSync("still rejects what is not an address", () => {
|
|
39
|
+
let parses = raw =>
|
|
40
|
+
switch fieldOf("email") {
|
|
41
|
+
| Some(schema) =>
|
|
42
|
+
switch raw->JSON.Encode.string->S.parseOrThrow(~to=schema) {
|
|
43
|
+
| _ => true
|
|
44
|
+
| exception _ => false
|
|
45
|
+
}
|
|
46
|
+
| None => false
|
|
47
|
+
}
|
|
48
|
+
expect((parses("buyer@example.com"), parses("buyer")))->toEqual((true, false))
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe("@displayName on a DateTime field:", () => {
|
|
53
|
+
testSync("names the row by it", () =>
|
|
54
|
+
expect(
|
|
55
|
+
S.Metadata.get(stateSchema, ~id=Reventless.DisplayName.displayNameId)->Option.map(d =>
|
|
56
|
+
d.fields
|
|
57
|
+
),
|
|
58
|
+
)->toEqual(Some(["openedAt"]))
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
testSync("and the instant grammar survives", () => {
|
|
62
|
+
let parses = raw =>
|
|
63
|
+
switch fieldOf("openedAt") {
|
|
64
|
+
| Some(schema) =>
|
|
65
|
+
switch raw->JSON.Encode.string->S.parseOrThrow(~to=schema) {
|
|
66
|
+
| _ => true
|
|
67
|
+
| exception _ => false
|
|
68
|
+
}
|
|
69
|
+
| None => false
|
|
70
|
+
}
|
|
71
|
+
expect((parses("2026-03-02T09:00:00Z"), parses("tomorrow")))->toEqual((true, false))
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
})
|