@reventlessdev/reventless-core 3.0.0-alpha.185 → 3.0.0-alpha.187
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 +15 -0
- package/package.json +6 -6
- package/src/components/Api/GraphQL_FragmentGenerator.res +7 -1
- package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +26 -14
- package/src/components/Api/SchemaType.res +26 -0
- package/src/components/Api/SchemaType.res.mjs +20 -0
- package/src/components/Api/SuryToJsonSchema.res +70 -1
- package/src/components/Api/SuryToJsonSchema.res.mjs +55 -2
- package/src/plugin/component/Plugin_Builder.res +11 -1
- package/src/plugin/component/Plugin_Builder.res.mjs +12 -2
- package/src/plugin/component/Plugin_EventQuerySchema.res +121 -0
- package/src/plugin/component/Plugin_EventQuerySchema.res.mjs +102 -0
- package/src/plugin/component/Plugin_Helpers.res +16 -0
- package/tests/api/SuryToJsonSchemaTest.res +129 -0
- package/tests/api/SuryToJsonSchemaTest.res.mjs +104 -0
- package/tests/plugin/Plugin_EventQuerySchemaTest.res +98 -0
- package/tests/plugin/Plugin_EventQuerySchemaTest.res.mjs +113 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
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.187 (2026-07-28)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **spec:** one semantic marker every typed semantic marks itself with ([aa18afc](https://github.com/ReventlessDev/reventless-core/commit/aa18afcf04c8edad9afe27e6fa4261d01e184da7))
|
|
11
|
+
* **spec:** StorageRef — the first semantic type, declared on the field's type ([44f15c3](https://github.com/ReventlessDev/reventless-core/commit/44f15c37de71261d701d18a9f1ada6f481c4a8dc))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# 3.0.0-alpha.186 (2026-07-27)
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* **api:** event-history query — the Source A read counterpart ([9268b33](https://github.com/ReventlessDev/reventless-core/commit/9268b33a4835d4cd4bc79b38f19bc2b974853fa4))
|
|
19
|
+
|
|
20
|
+
|
|
6
21
|
# 3.0.0-alpha.185 (2026-07-27)
|
|
7
22
|
|
|
8
23
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-core",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.187",
|
|
4
4
|
"description": "Core package for Reventless framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -30,20 +30,20 @@
|
|
|
30
30
|
"uuid": "^13.0.0",
|
|
31
31
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.31",
|
|
32
32
|
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.13",
|
|
33
|
-
"@reventlessdev/rescript-jest": "1.0.0-alpha.9",
|
|
34
33
|
"@reventlessdev/rescript-fast-csv": "1.2.0-alpha.14",
|
|
35
34
|
"@reventlessdev/rescript-node-streams": "1.1.0-alpha.13",
|
|
35
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.9",
|
|
36
36
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.17",
|
|
37
37
|
"@reventlessdev/rescript-ssh2": "1.1.0-alpha.14",
|
|
38
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.106",
|
|
39
38
|
"@reventlessdev/rescript-uuid": "1.1.0-alpha.17",
|
|
40
|
-
"@reventlessdev/reventless-
|
|
41
|
-
"@reventlessdev/reventless-
|
|
39
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.29",
|
|
40
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.107",
|
|
41
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.82"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"rescript": "12.3.0",
|
|
45
45
|
"sury-ppx": "11.0.0-alpha.2",
|
|
46
|
-
"@reventlessdev/reventless-ppx": "1.0.0-alpha.
|
|
46
|
+
"@reventlessdev/reventless-ppx": "1.0.0-alpha.57"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"rescript": "12.3.0"
|
|
@@ -62,6 +62,11 @@ let rec fromSchemaType = (
|
|
|
62
62
|
collectedTypes->Array.push(`enum ${name} {\n ${valuesStr}\n}`)
|
|
63
63
|
}
|
|
64
64
|
`${name}${bang}`
|
|
65
|
+
// GraphQL carries the value's shape, not its meaning: a storage ref is a
|
|
66
|
+
// String on the wire exactly as it is in the event log. The semantic reaches
|
|
67
|
+
// the UI through the field's JSON Schema, which is the channel that can
|
|
68
|
+
// express it.
|
|
69
|
+
| Semantic(_, inner) => fromSchemaType(~required, ~asInput, inner, collectedTypes, seenTypes)
|
|
65
70
|
| Unknown => `String${bang}`
|
|
66
71
|
}
|
|
67
72
|
}
|
|
@@ -171,13 +176,14 @@ let emptyCapability: serverCapability = {filterFields: [], sortFields: []}
|
|
|
171
176
|
// Convert a SchemaType.schemaType to its GraphQL scalar name (input position).
|
|
172
177
|
// Mirrors the scalar branches of fromSchemaType — kept inline because we don't
|
|
173
178
|
// emit `!`/list wrappers for filter inputs.
|
|
174
|
-
let scalarOfSchemaType = (st: SchemaType.schemaType): string =>
|
|
179
|
+
let rec scalarOfSchemaType = (st: SchemaType.schemaType): string =>
|
|
175
180
|
switch st {
|
|
176
181
|
| ScalarString => "String"
|
|
177
182
|
| ScalarNumber => "Float"
|
|
178
183
|
| ScalarBoolean => "Boolean"
|
|
179
184
|
| ScalarBigInt => "String"
|
|
180
185
|
| EntityId => "ID"
|
|
186
|
+
| Semantic(_, inner) => scalarOfSchemaType(inner)
|
|
181
187
|
| _ => "String"
|
|
182
188
|
}
|
|
183
189
|
|
|
@@ -71,6 +71,10 @@ function fromSchemaType(_required, _asInputOpt, _st, collectedTypes, seenTypes)
|
|
|
71
71
|
collectedTypes.push(`enum ` + name$1 + ` {\n ` + valuesStr + `\n}`);
|
|
72
72
|
}
|
|
73
73
|
return name$1 + bang;
|
|
74
|
+
case "Semantic" :
|
|
75
|
+
_st = st._1;
|
|
76
|
+
_asInputOpt = asInput;
|
|
77
|
+
continue;
|
|
74
78
|
}
|
|
75
79
|
}
|
|
76
80
|
};
|
|
@@ -144,20 +148,28 @@ let emptyCapability = {
|
|
|
144
148
|
sortFields: emptyCapability_sortFields
|
|
145
149
|
};
|
|
146
150
|
|
|
147
|
-
function scalarOfSchemaType(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
151
|
+
function scalarOfSchemaType(_st) {
|
|
152
|
+
while (true) {
|
|
153
|
+
let st = _st;
|
|
154
|
+
if (typeof st !== "object") {
|
|
155
|
+
switch (st) {
|
|
156
|
+
case "ScalarNumber" :
|
|
157
|
+
return "Float";
|
|
158
|
+
case "ScalarBoolean" :
|
|
159
|
+
return "Boolean";
|
|
160
|
+
case "EntityId" :
|
|
161
|
+
return "ID";
|
|
162
|
+
default:
|
|
163
|
+
return "String";
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
if (st.TAG !== "Semantic") {
|
|
167
|
+
return "String";
|
|
168
|
+
}
|
|
169
|
+
_st = st._1;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
161
173
|
}
|
|
162
174
|
|
|
163
175
|
function deriveServerCapability(schema) {
|
|
@@ -9,6 +9,12 @@ type rec schemaType =
|
|
|
9
9
|
| ArrayOf(schemaType)
|
|
10
10
|
| ObjectRef(string, dict<schemaType>)
|
|
11
11
|
| Enum(string, array<string>)
|
|
12
|
+
| /** A field whose type carries a semantic the IR has no dedicated shape for.
|
|
13
|
+
Wraps the shape the value actually has, so every consumer that only cares
|
|
14
|
+
about shape unwraps and is otherwise unaffected. `DateTime` and `EntityId`
|
|
15
|
+
are *not* expressed this way: they long predate the generic marker and
|
|
16
|
+
their JSON Schema `format` output is a published contract. */
|
|
17
|
+
Semantic(Reventless.Semantic.t, schemaType)
|
|
12
18
|
| Unknown
|
|
13
19
|
|
|
14
20
|
let isTagged = Reventless.DcbTag.isTagged
|
|
@@ -27,6 +33,26 @@ let isIdsFieldName = (name: string): bool => {
|
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
let rec fromSury = (~parentName: string, ~fieldName: string, schema: S.t<unknown>): schemaType => {
|
|
36
|
+
let shape = shapeOf(~parentName, ~fieldName, schema)
|
|
37
|
+
// Semantics the IR already has a dedicated shape for keep it: `dateTime` and
|
|
38
|
+
// `reference` are read below via `isDateTime` / `getTarget`, both of which now
|
|
39
|
+
// consult the generic marker, and their `format` output is a published
|
|
40
|
+
// contract. Every other semantic rides along as a wrapper, so surfacing a new
|
|
41
|
+
// one costs a value rather than a branch.
|
|
42
|
+
switch Reventless.Semantic.get(schema) {
|
|
43
|
+
| Some({id} as sem)
|
|
44
|
+
if id !== Reventless.Semantic.Id.dateTime && id !== Reventless.Semantic.Id.reference =>
|
|
45
|
+
Semantic(sem, shape)
|
|
46
|
+
| _ => shape
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
and shapeOf = (~parentName: string, ~fieldName: string, schema: S.t<unknown>): schemaType => {
|
|
51
|
+
// Two independent facts, checked independently. A DCB-tagged field is an
|
|
52
|
+
// entity id because it routes; a reference field is one because it points at
|
|
53
|
+
// an entity. Neither implies the other — a `@partitionTag` field carries no
|
|
54
|
+
// reference, and `Reference.toWithoutDcbTag` carries no tag — so collapsing
|
|
55
|
+
// these into one test would silently reclassify whichever case it dropped.
|
|
30
56
|
if isTagged(schema) || Reventless.Reference.getTarget(schema)->Option.isSome {
|
|
31
57
|
EntityId
|
|
32
58
|
} else {
|
|
@@ -4,6 +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 DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
6
6
|
import * as DateTime$Reventless from "@reventlessdev/reventless-spec/src/types/DateTime.res.mjs";
|
|
7
|
+
import * as Semantic$Reventless from "@reventlessdev/reventless-spec/src/semantic/Semantic.res.mjs";
|
|
7
8
|
import * as Reference$Reventless from "@reventlessdev/reventless-spec/src/components/Reference.res.mjs";
|
|
8
9
|
|
|
9
10
|
function isIdFieldName(name) {
|
|
@@ -27,6 +28,24 @@ function isIdsFieldName(name) {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
function fromSury(parentName, fieldName, schema) {
|
|
31
|
+
let shape = shapeOf(parentName, fieldName, schema);
|
|
32
|
+
let sem = Semantic$Reventless.get(schema);
|
|
33
|
+
if (sem === undefined) {
|
|
34
|
+
return shape;
|
|
35
|
+
}
|
|
36
|
+
let id = sem.id;
|
|
37
|
+
if (id !== Semantic$Reventless.Id.dateTime && id !== Semantic$Reventless.Id.reference) {
|
|
38
|
+
return {
|
|
39
|
+
TAG: "Semantic",
|
|
40
|
+
_0: sem,
|
|
41
|
+
_1: shape
|
|
42
|
+
};
|
|
43
|
+
} else {
|
|
44
|
+
return shape;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function shapeOf(parentName, fieldName, schema) {
|
|
30
49
|
if (DcbTag$Reventless.isTagged(schema) || Stdlib_Option.isSome(Reference$Reventless.getTarget(schema))) {
|
|
31
50
|
return "EntityId";
|
|
32
51
|
}
|
|
@@ -156,6 +175,7 @@ export {
|
|
|
156
175
|
isIdFieldName,
|
|
157
176
|
isIdsFieldName,
|
|
158
177
|
fromSury,
|
|
178
|
+
shapeOf,
|
|
159
179
|
fromSuryObject,
|
|
160
180
|
}
|
|
161
181
|
/* DcbTag-Reventless Not a pure module */
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Shared utility: convert sury S.t<'a> schemas to JSON Schema objects.
|
|
2
2
|
// Uses SchemaType as the shared intermediate representation.
|
|
3
3
|
|
|
4
|
+
let log = Logger.fromEnv()
|
|
5
|
+
|
|
4
6
|
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
5
7
|
|
|
6
8
|
let str = JSON.Encode.string
|
|
@@ -8,6 +10,18 @@ let str = JSON.Encode.string
|
|
|
8
10
|
let jsonObject = (entries: array<(string, JSON.t)>): JSON.t =>
|
|
9
11
|
JSON.Encode.object(Dict.fromArray(entries))
|
|
10
12
|
|
|
13
|
+
// An absent plugin means "the declaring plugin's own", which the key's absence
|
|
14
|
+
// says exactly. Writing an empty string instead would make "mine" and "unnamed"
|
|
15
|
+
// indistinguishable to every reader.
|
|
16
|
+
let withOptionalPlugin = (
|
|
17
|
+
entries: array<(string, JSON.t)>,
|
|
18
|
+
plugin: option<string>,
|
|
19
|
+
): array<(string, JSON.t)> =>
|
|
20
|
+
switch plugin {
|
|
21
|
+
| Some(p) => entries->Array.concat([("plugin", JSON.Encode.string(p))])
|
|
22
|
+
| None => entries
|
|
23
|
+
}
|
|
24
|
+
|
|
11
25
|
// Merge x-reventless-* extension properties from a Reventless.StateAnnotations.stateAnnotationSpec
|
|
12
26
|
// into a property schema (a JSON Schema object). Returns the schema unchanged when the field
|
|
13
27
|
// has no annotations or when the schema is not a JSON object.
|
|
@@ -70,7 +84,21 @@ let mergeAnnotations = (
|
|
|
70
84
|
}
|
|
71
85
|
switch spec.semantic->Array.find(((field, _)) => field === fieldName) {
|
|
72
86
|
| Some((_, semanticId)) =>
|
|
73
|
-
|
|
87
|
+
// The type wins. A typed declaration is better-sourced than an annotation
|
|
88
|
+
// — the compiler checked it and it cannot drift from the field's shape —
|
|
89
|
+
// so an annotation never overwrites one. A field carrying both is the
|
|
90
|
+
// domain saying two things about one value, which is worth hearing about
|
|
91
|
+
// rather than resolving in silence.
|
|
92
|
+
switch obj->Dict.get("x-reventless-semantic") {
|
|
93
|
+
| Some(fromType) =>
|
|
94
|
+
if fromType !== JSON.Encode.string(semanticId) {
|
|
95
|
+
log.warn(
|
|
96
|
+
~comp="SuryToJsonSchema",
|
|
97
|
+
`field "${fieldName}" declares semantic "${semanticId}" by annotation but its type already carries ${fromType->JSON.stringify}; the type wins`,
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
| None => obj->Dict.set("x-reventless-semantic", JSON.Encode.string(semanticId))
|
|
101
|
+
}
|
|
74
102
|
| None => ()
|
|
75
103
|
}
|
|
76
104
|
switch spec.metric->Array.find(((field, _)) => field === fieldName) {
|
|
@@ -107,9 +135,50 @@ let rec fromSchemaType = (st: SchemaType.schemaType): JSON.t =>
|
|
|
107
135
|
("type", str("string")),
|
|
108
136
|
("enum", JSON.Encode.array(values->Array.map(JSON.Encode.string))),
|
|
109
137
|
])
|
|
138
|
+
| Semantic(sem, inner) => fromSchemaType(inner)->withSemantic(sem)
|
|
110
139
|
| Unknown => jsonObject([("type", str("string"))])
|
|
111
140
|
}
|
|
112
141
|
|
|
142
|
+
// Attach a type-carried semantic to a field's JSON Schema.
|
|
143
|
+
//
|
|
144
|
+
// This is the type path's emission point, and it is deliberately *not*
|
|
145
|
+
// `mergeAnnotations`: that one is fed by the PPX-collected annotation spec,
|
|
146
|
+
// which the PPX only ever collects on read-model `state` records. This walk is
|
|
147
|
+
// schema-shape-driven and runs over every schema, commands and events included —
|
|
148
|
+
// which is where a declaration like a storage ref has to live, since that is
|
|
149
|
+
// where the value is first accepted.
|
|
150
|
+
//
|
|
151
|
+
// Both paths write the same `x-reventless-semantic` key, on purpose: one wire
|
|
152
|
+
// format, whatever the source. The payload rides in sibling keys so a reader
|
|
153
|
+
// that knows only the bare string still reads it correctly.
|
|
154
|
+
and withSemantic = (fieldSchema: JSON.t, sem: Reventless.Semantic.t): JSON.t =>
|
|
155
|
+
switch fieldSchema->JSON.Decode.object {
|
|
156
|
+
| None => fieldSchema
|
|
157
|
+
| Some(obj) =>
|
|
158
|
+
obj->Dict.set("x-reventless-semantic", str(sem.id))
|
|
159
|
+
// The `type` discriminator is what lets a reader tell a type-carried
|
|
160
|
+
// semantic from an annotated one, and rank it above the annotation
|
|
161
|
+
// accordingly: the compiler checked this one, and it cannot drift from the
|
|
162
|
+
// field's shape.
|
|
163
|
+
obj->Dict.set("x-reventless-semantic-source", str("type"))
|
|
164
|
+
switch sem.payload {
|
|
165
|
+
| Plain => ()
|
|
166
|
+
| ReferenceTo({entity, plugin}) =>
|
|
167
|
+
obj->Dict.set(
|
|
168
|
+
"x-reventless-semantic-target",
|
|
169
|
+
jsonObject(withOptionalPlugin([("entity", str(entity))], plugin)),
|
|
170
|
+
)
|
|
171
|
+
| StoredIn({plugin, store}) =>
|
|
172
|
+
// An absent plugin means "this plugin's own store" — omit the key rather
|
|
173
|
+
// than writing an empty string, so a reader never has to tell those apart.
|
|
174
|
+
obj->Dict.set(
|
|
175
|
+
"x-reventless-semantic-target",
|
|
176
|
+
jsonObject(withOptionalPlugin([("store", str(store))], plugin)),
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
JSON.Encode.object(obj)
|
|
180
|
+
}
|
|
181
|
+
|
|
113
182
|
and objectRefToJsonSchema = (
|
|
114
183
|
~annotations: option<Reventless.StateAnnotations.stateAnnotationSpec>=?,
|
|
115
184
|
fields: dict<SchemaType.schemaType>,
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
4
|
+
import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
|
|
4
5
|
import * as SchemaType$ReventlessCore from "./SchemaType.res.mjs";
|
|
5
6
|
import * as StateAnnotations$Reventless from "@reventlessdev/reventless-spec/src/components/StateAnnotations.res.mjs";
|
|
6
7
|
|
|
8
|
+
let log = Logger$ReventlessCore.fromEnv();
|
|
9
|
+
|
|
7
10
|
function str(prim) {
|
|
8
11
|
return prim;
|
|
9
12
|
}
|
|
@@ -12,6 +15,17 @@ function jsonObject(entries) {
|
|
|
12
15
|
return Object.fromEntries(entries);
|
|
13
16
|
}
|
|
14
17
|
|
|
18
|
+
function withOptionalPlugin(entries, plugin) {
|
|
19
|
+
if (plugin !== undefined) {
|
|
20
|
+
return entries.concat([[
|
|
21
|
+
"plugin",
|
|
22
|
+
plugin
|
|
23
|
+
]]);
|
|
24
|
+
} else {
|
|
25
|
+
return entries;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
function mergeAnnotations(fieldSchema, fieldName, spec) {
|
|
16
30
|
let obj = Stdlib_JSON.Decode.object(fieldSchema);
|
|
17
31
|
if (obj === undefined) {
|
|
@@ -64,7 +78,15 @@ function mergeAnnotations(fieldSchema, fieldName, spec) {
|
|
|
64
78
|
}
|
|
65
79
|
let match$3 = spec.semantic.find(param => param[0] === fieldName);
|
|
66
80
|
if (match$3 !== undefined) {
|
|
67
|
-
|
|
81
|
+
let semanticId = match$3[1];
|
|
82
|
+
let fromType = obj["x-reventless-semantic"];
|
|
83
|
+
if (fromType !== undefined) {
|
|
84
|
+
if (fromType !== semanticId) {
|
|
85
|
+
log.warn("SuryToJsonSchema", undefined, `field "` + fieldName + `" declares semantic "` + semanticId + `" by annotation but its type already carries ` + JSON.stringify(fromType) + `; the type wins`);
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
obj["x-reventless-semantic"] = semanticId;
|
|
89
|
+
}
|
|
68
90
|
}
|
|
69
91
|
let match$4 = spec.metric.find(param => param[0] === fieldName);
|
|
70
92
|
if (match$4 !== undefined) {
|
|
@@ -167,8 +189,36 @@ function fromSchemaType(st) {
|
|
|
167
189
|
st._1.map(prim => prim)
|
|
168
190
|
]
|
|
169
191
|
]);
|
|
192
|
+
case "Semantic" :
|
|
193
|
+
return withSemantic(fromSchemaType(st._1), st._0);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function withSemantic(fieldSchema, sem) {
|
|
199
|
+
let obj = Stdlib_JSON.Decode.object(fieldSchema);
|
|
200
|
+
if (obj === undefined) {
|
|
201
|
+
return fieldSchema;
|
|
202
|
+
}
|
|
203
|
+
obj["x-reventless-semantic"] = sem.id;
|
|
204
|
+
obj["x-reventless-semantic-source"] = "type";
|
|
205
|
+
let match = sem.payload;
|
|
206
|
+
if (typeof match === "object") {
|
|
207
|
+
if (match.TAG === "ReferenceTo") {
|
|
208
|
+
let match$1 = match._0;
|
|
209
|
+
obj["x-reventless-semantic-target"] = Object.fromEntries(withOptionalPlugin([[
|
|
210
|
+
"entity",
|
|
211
|
+
match$1.entity
|
|
212
|
+
]], match$1.plugin));
|
|
213
|
+
} else {
|
|
214
|
+
let match$2 = match._0;
|
|
215
|
+
obj["x-reventless-semantic-target"] = Object.fromEntries(withOptionalPlugin([[
|
|
216
|
+
"store",
|
|
217
|
+
match$2.store
|
|
218
|
+
]], match$2.plugin));
|
|
170
219
|
}
|
|
171
220
|
}
|
|
221
|
+
return obj;
|
|
172
222
|
}
|
|
173
223
|
|
|
174
224
|
function objectRefToJsonSchema(annotations, fields) {
|
|
@@ -231,12 +281,15 @@ function toJsonSchema(schema) {
|
|
|
231
281
|
}
|
|
232
282
|
|
|
233
283
|
export {
|
|
284
|
+
log,
|
|
234
285
|
str,
|
|
235
286
|
jsonObject,
|
|
287
|
+
withOptionalPlugin,
|
|
236
288
|
mergeAnnotations,
|
|
237
289
|
fromSchemaType,
|
|
290
|
+
withSemantic,
|
|
238
291
|
objectRefToJsonSchema,
|
|
239
292
|
deriveObjectSchema,
|
|
240
293
|
toJsonSchema,
|
|
241
294
|
}
|
|
242
|
-
/*
|
|
295
|
+
/* log Not a pure module */
|
|
@@ -298,10 +298,15 @@ module Make = (
|
|
|
298
298
|
~mutationEntries,
|
|
299
299
|
~eventLogEntries,
|
|
300
300
|
)
|
|
301
|
+
// The historical read counterpart of the Source A subscription — same
|
|
302
|
+
// event logs, same generation point, so the two cannot disagree about
|
|
303
|
+
// which streams exist.
|
|
304
|
+
let evtResult = Plugin_EventQuerySchema.generate(~plugin=name, ~eventLogEntries)
|
|
301
305
|
let parts = GraphQL_Stitcher.decode(baseFragment)
|
|
302
306
|
GraphQL_Stitcher.encode({
|
|
303
307
|
...parts,
|
|
304
|
-
types: Array.
|
|
308
|
+
types: Array.flat([parts.types, subResult.extraTypes, evtResult.extraTypes]),
|
|
309
|
+
queries: Array.concat(parts.queries, evtResult.queryFields),
|
|
305
310
|
subscriptions: subResult.subscriptionFields,
|
|
306
311
|
subscriptionSources: subResult.subscriptionSources,
|
|
307
312
|
})
|
|
@@ -324,6 +329,11 @@ module Make = (
|
|
|
324
329
|
})
|
|
325
330
|
)
|
|
326
331
|
|
|
332
|
+
// Register the event-history query resolvers matching the SDL emitted above.
|
|
333
|
+
Spec.hooks.eventQueryResolverHook->Option.forEach(registerEventQueries =>
|
|
334
|
+
registerEventQueries({pluginName: name, eventLogEntries})
|
|
335
|
+
)
|
|
336
|
+
|
|
327
337
|
// Register DCB StateChangeSlice publish functions in publishToAggregates so that
|
|
328
338
|
// extensions whose Delegate is a DCB slice can dispatch commands to them.
|
|
329
339
|
// All slices in a plugin share the same DCB command topic (same publishJsons).
|
|
@@ -32,6 +32,7 @@ import * as ExtensionMapping$ReventlessInfra from "@reventlessdev/reventless-inf
|
|
|
32
32
|
import * as ExtensionPoint$ReventlessInterop from "@reventlessdev/reventless-interop/src/components/ExtensionPoint.res.mjs";
|
|
33
33
|
import * as Heartbeat_Builder$ReventlessCore from "../../components/Heartbeat/Heartbeat_Builder.res.mjs";
|
|
34
34
|
import * as ResourceAttribution$ReventlessCore from "../../ResourceAttribution.res.mjs";
|
|
35
|
+
import * as Plugin_EventQuerySchema$ReventlessCore from "./Plugin_EventQuerySchema.res.mjs";
|
|
35
36
|
import * as PluginExtensionPointSpec$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/PluginExtensionPointSpec.res.mjs";
|
|
36
37
|
import * as Plugin_SubscriptionSchema$ReventlessCore from "./Plugin_SubscriptionSchema.res.mjs";
|
|
37
38
|
|
|
@@ -226,11 +227,16 @@ function Make(Spec) {
|
|
|
226
227
|
});
|
|
227
228
|
let baseFragment = FragmentProvider.generateFragment(mutationEntries, queryEntries);
|
|
228
229
|
let subResult = Plugin_SubscriptionSchema$ReventlessCore.generate(mutationEntries, eventLogEntries);
|
|
230
|
+
let evtResult = Plugin_EventQuerySchema$ReventlessCore.generate(extra$1, eventLogEntries);
|
|
229
231
|
let parts = GraphQL_Stitcher$ReventlessCore.decode(baseFragment);
|
|
230
232
|
let apiSchemaFragment = GraphQL_Stitcher$ReventlessCore.encode({
|
|
231
|
-
types:
|
|
233
|
+
types: [
|
|
234
|
+
parts.types,
|
|
235
|
+
subResult.extraTypes,
|
|
236
|
+
evtResult.extraTypes
|
|
237
|
+
].flat(),
|
|
232
238
|
mutations: parts.mutations,
|
|
233
|
-
queries: parts.queries,
|
|
239
|
+
queries: parts.queries.concat(evtResult.queryFields),
|
|
234
240
|
subscriptions: subResult.subscriptionFields,
|
|
235
241
|
subscriptionSources: subResult.subscriptionSources
|
|
236
242
|
});
|
|
@@ -245,6 +251,10 @@ function Make(Spec) {
|
|
|
245
251
|
eventLogEntries: eventLogEntries,
|
|
246
252
|
subscriptionFields: GraphQL_Stitcher$ReventlessCore.decode(apiSchemaFragment).subscriptions
|
|
247
253
|
}));
|
|
254
|
+
Stdlib_Option.forEach(Spec.hooks.eventQueryResolverHook, registerEventQueries => registerEventQueries({
|
|
255
|
+
pluginName: extra$1,
|
|
256
|
+
eventLogEntries: eventLogEntries
|
|
257
|
+
}));
|
|
248
258
|
let slicePublishJsons = dcbResult.dcbPublishJsons;
|
|
249
259
|
if (slicePublishJsons !== undefined) {
|
|
250
260
|
stateChangeSlices.forEach(Sc => {
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Plugin_EventQuerySchema.res
|
|
2
|
+
// Generates the query counterpart of the Source A raw-event subscription:
|
|
3
|
+
// one paginated `…EventHistory` connection per event log, so a client can ask
|
|
4
|
+
// "what happened to this entity, in order, with who and when".
|
|
5
|
+
//
|
|
6
|
+
// Called by Plugin_Builder alongside Plugin_SubscriptionSchema, from the same
|
|
7
|
+
// `eventLogEntries` array — so the query and the subscription can never drift
|
|
8
|
+
// about which logs exist.
|
|
9
|
+
//
|
|
10
|
+
// The subscription (Plugin_SubscriptionSchema.sourceAFieldsAndTypes) answers
|
|
11
|
+
// "something was just appended" and deliberately carries no envelope metadata.
|
|
12
|
+
// This answers a historical question, so it carries the audit-relevant subset
|
|
13
|
+
// of `Message.meta`: who (`user`), when (`time`), and the correlation /
|
|
14
|
+
// causation chain. Transport and tracing fields (`ip`, `traceparent`,
|
|
15
|
+
// `schemaVersion`) and the `headers` context bag are NOT exposed — `headers`
|
|
16
|
+
// in particular carries cross-cutting context (tenant ids, feature flags) that
|
|
17
|
+
// should not leave the server.
|
|
18
|
+
|
|
19
|
+
open ReventlessInfra.Api
|
|
20
|
+
|
|
21
|
+
// Shared supporting types, emitted once per fragment. The stitcher dedupes by
|
|
22
|
+
// type name across fragments, so concatenating plugin + admin fragments stays
|
|
23
|
+
// safe — the same contract `commandResultSdlTypes` relies on.
|
|
24
|
+
let sharedSdlTypes: array<string> = [
|
|
25
|
+
`type EventMeta {
|
|
26
|
+
service: String!
|
|
27
|
+
time: String!
|
|
28
|
+
user: String
|
|
29
|
+
msgId: String!
|
|
30
|
+
correlationId: String!
|
|
31
|
+
causationId: String
|
|
32
|
+
}`,
|
|
33
|
+
`type EventTag {
|
|
34
|
+
key: String!
|
|
35
|
+
value: String!
|
|
36
|
+
}`,
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
let recordTypeName = (~plugin: string, ~displayName: string) =>
|
|
40
|
+
`${plugin}_${displayName}EventRecord`
|
|
41
|
+
|
|
42
|
+
let historyFieldName = (~plugin: string, ~displayName: string) =>
|
|
43
|
+
`${plugin}_${displayName}EventHistory`
|
|
44
|
+
|
|
45
|
+
let filterTypeName = (~plugin: string, ~displayName: string) =>
|
|
46
|
+
`${plugin}_${displayName}EventHistoryFilter`
|
|
47
|
+
|
|
48
|
+
let recordSdl = (~typeName: string): string =>
|
|
49
|
+
`type ${typeName} {
|
|
50
|
+
position: String!
|
|
51
|
+
eventType: String!
|
|
52
|
+
payload: AWSJSON!
|
|
53
|
+
tags: [EventTag!]!
|
|
54
|
+
meta: EventMeta!
|
|
55
|
+
recordedAt: String!
|
|
56
|
+
}`
|
|
57
|
+
|
|
58
|
+
let connectionSdlTypes = (~typeName: string): array<string> => [
|
|
59
|
+
`type ${typeName}Edge {\n node: ${typeName}!\n cursor: String!\n}`,
|
|
60
|
+
`type ${typeName}Connection {\n edges: [${typeName}Edge!]!\n pageInfo: PageInfo!\n}`,
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
// `entityId` is the ergonomic filter and the audit-trail case: for an
|
|
64
|
+
// aggregate log it matches the aggregate id, for a DCB log it matches ANY tag
|
|
65
|
+
// value (the same semantics the MCP event-history handler uses).
|
|
66
|
+
// `tagKey` + `tagValue` is the precise form, for a DCB event carrying several
|
|
67
|
+
// ids where you mean one specific one.
|
|
68
|
+
let filterSdl = (~typeName: string): string =>
|
|
69
|
+
`input ${typeName} {
|
|
70
|
+
entityId: ID
|
|
71
|
+
tagKey: String
|
|
72
|
+
tagValue: String
|
|
73
|
+
eventTypes: [String!]
|
|
74
|
+
user: String
|
|
75
|
+
timeFrom: String
|
|
76
|
+
timeTo: String
|
|
77
|
+
}`
|
|
78
|
+
|
|
79
|
+
type result = {
|
|
80
|
+
queryFields: array<string>,
|
|
81
|
+
extraTypes: array<string>,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
Emit the event-history query field + supporting types for every distinct event
|
|
86
|
+
log in `eventLogEntries`.
|
|
87
|
+
|
|
88
|
+
Deduped by `displayName` exactly as the Source A subscription is: an aggregate
|
|
89
|
+
and a DCB log that project the same display name describe one logical stream.
|
|
90
|
+
*/
|
|
91
|
+
let generate = (~plugin: string, ~eventLogEntries: array<eventLogSchemaEntry>): result => {
|
|
92
|
+
let seen: Set.t<string> = Set.make()
|
|
93
|
+
let queryFields: array<string> = []
|
|
94
|
+
let extraTypes: array<string> = []
|
|
95
|
+
|
|
96
|
+
eventLogEntries->Array.forEach(entry => {
|
|
97
|
+
let displayName = entry.displayName
|
|
98
|
+
if !(seen->Set.has(displayName)) {
|
|
99
|
+
seen->Set.add(displayName)
|
|
100
|
+
let typeName = recordTypeName(~plugin, ~displayName)
|
|
101
|
+
let filterName = filterTypeName(~plugin, ~displayName)
|
|
102
|
+
let fieldName = historyFieldName(~plugin, ~displayName)
|
|
103
|
+
|
|
104
|
+
extraTypes->Array.push(recordSdl(~typeName))
|
|
105
|
+
connectionSdlTypes(~typeName)->Array.forEach(t => extraTypes->Array.push(t))
|
|
106
|
+
extraTypes->Array.push(filterSdl(~typeName=filterName))
|
|
107
|
+
|
|
108
|
+
queryFields->Array.push(
|
|
109
|
+
` ${fieldName}(filter: ${filterName}, first: Int, after: String, last: Int, before: String): ${typeName}Connection!`,
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// Only pull in the shared types when this fragment actually emits a record
|
|
115
|
+
// type that references them — a plugin with no event logs stays untouched.
|
|
116
|
+
if queryFields->Array.length > 0 {
|
|
117
|
+
sharedSdlTypes->Array.forEach(t => extraTypes->Array.push(t))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
{queryFields, extraTypes}
|
|
121
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
let sharedSdlTypes = [
|
|
5
|
+
`type EventMeta {
|
|
6
|
+
service: String!
|
|
7
|
+
time: String!
|
|
8
|
+
user: String
|
|
9
|
+
msgId: String!
|
|
10
|
+
correlationId: String!
|
|
11
|
+
causationId: String
|
|
12
|
+
}`,
|
|
13
|
+
`type EventTag {
|
|
14
|
+
key: String!
|
|
15
|
+
value: String!
|
|
16
|
+
}`
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function recordTypeName(plugin, displayName) {
|
|
20
|
+
return plugin + `_` + displayName + `EventRecord`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function historyFieldName(plugin, displayName) {
|
|
24
|
+
return plugin + `_` + displayName + `EventHistory`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function filterTypeName(plugin, displayName) {
|
|
28
|
+
return plugin + `_` + displayName + `EventHistoryFilter`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function recordSdl(typeName) {
|
|
32
|
+
return `type ` + typeName + ` {
|
|
33
|
+
position: String!
|
|
34
|
+
eventType: String!
|
|
35
|
+
payload: AWSJSON!
|
|
36
|
+
tags: [EventTag!]!
|
|
37
|
+
meta: EventMeta!
|
|
38
|
+
recordedAt: String!
|
|
39
|
+
}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function connectionSdlTypes(typeName) {
|
|
43
|
+
return [
|
|
44
|
+
`type ` + typeName + `Edge {\n node: ` + typeName + `!\n cursor: String!\n}`,
|
|
45
|
+
`type ` + typeName + `Connection {\n edges: [` + typeName + `Edge!]!\n pageInfo: PageInfo!\n}`
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function filterSdl(typeName) {
|
|
50
|
+
return `input ` + typeName + ` {
|
|
51
|
+
entityId: ID
|
|
52
|
+
tagKey: String
|
|
53
|
+
tagValue: String
|
|
54
|
+
eventTypes: [String!]
|
|
55
|
+
user: String
|
|
56
|
+
timeFrom: String
|
|
57
|
+
timeTo: String
|
|
58
|
+
}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function generate(plugin, eventLogEntries) {
|
|
62
|
+
let seen = new Set();
|
|
63
|
+
let queryFields = [];
|
|
64
|
+
let extraTypes = [];
|
|
65
|
+
eventLogEntries.forEach(entry => {
|
|
66
|
+
let displayName = entry.displayName;
|
|
67
|
+
if (seen.has(displayName)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
seen.add(displayName);
|
|
71
|
+
let typeName = recordTypeName(plugin, displayName);
|
|
72
|
+
let filterName = filterTypeName(plugin, displayName);
|
|
73
|
+
let fieldName = historyFieldName(plugin, displayName);
|
|
74
|
+
extraTypes.push(recordSdl(typeName));
|
|
75
|
+
connectionSdlTypes(typeName).forEach(t => {
|
|
76
|
+
extraTypes.push(t);
|
|
77
|
+
});
|
|
78
|
+
extraTypes.push(filterSdl(filterName));
|
|
79
|
+
queryFields.push(` ` + fieldName + `(filter: ` + filterName + `, first: Int, after: String, last: Int, before: String): ` + typeName + `Connection!`);
|
|
80
|
+
});
|
|
81
|
+
if (queryFields.length !== 0) {
|
|
82
|
+
sharedSdlTypes.forEach(t => {
|
|
83
|
+
extraTypes.push(t);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
queryFields: queryFields,
|
|
88
|
+
extraTypes: extraTypes
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export {
|
|
93
|
+
sharedSdlTypes,
|
|
94
|
+
recordTypeName,
|
|
95
|
+
historyFieldName,
|
|
96
|
+
filterTypeName,
|
|
97
|
+
recordSdl,
|
|
98
|
+
connectionSdlTypes,
|
|
99
|
+
filterSdl,
|
|
100
|
+
generate,
|
|
101
|
+
}
|
|
102
|
+
/* No side effect */
|
|
@@ -892,6 +892,19 @@ type mcpRegistrationParams = {
|
|
|
892
892
|
subscriptionFields: array<string>,
|
|
893
893
|
}
|
|
894
894
|
|
|
895
|
+
// ---------------------------------------------------------------------------
|
|
896
|
+
// Event-history query registration params. The SDL for these fields is emitted
|
|
897
|
+
// by Plugin_EventQuerySchema from this same `eventLogEntries` array; this hook
|
|
898
|
+
// hands a platform what it needs to register the matching resolvers. Kept apart
|
|
899
|
+
// from mcpRegistrationParams (which carries the same two fields) because the
|
|
900
|
+
// two consumers are unrelated: one registers GraphQL resolvers, the other MCP
|
|
901
|
+
// resources, and either can exist without the other.
|
|
902
|
+
// ---------------------------------------------------------------------------
|
|
903
|
+
type eventQueryRegistrationParams = {
|
|
904
|
+
pluginName: string,
|
|
905
|
+
eventLogEntries: array<ReventlessInfra.Api.eventLogSchemaEntry>,
|
|
906
|
+
}
|
|
907
|
+
|
|
895
908
|
// Subscription infrastructure hook params — fired after allQueryDbs and allEventTopics
|
|
896
909
|
// are assembled inside builderOutputs. Lets the AWS platform wire StateTopic and
|
|
897
910
|
// EventLogSubscription Lambdas without touching reventless-core.
|
|
@@ -943,6 +956,9 @@ type platformHooks = {
|
|
|
943
956
|
subscriptionInfraHook?: subscriptionInfraParams => unit,
|
|
944
957
|
// MCP tools and resources.
|
|
945
958
|
mcpSchemaRegistrationHook?: mcpRegistrationParams => unit,
|
|
959
|
+
// Event-history query resolvers — the read counterpart of the Source A
|
|
960
|
+
// subscription (see Plugin_EventQuerySchema).
|
|
961
|
+
eventQueryResolverHook?: eventQueryRegistrationParams => unit,
|
|
946
962
|
// ── AppSync resolver creation (AWS) ───────────────────────────────────
|
|
947
963
|
preResolversSchemaHook?: (~name: string, ~version: string, Reventless.Plugin.apiSchemaFragment) => Pulumi.Output.t<unit>,
|
|
948
964
|
// Admin equivalent of preResolversSchemaHook. Set by AWS platforms to push
|
|
@@ -490,4 +490,133 @@ describe("SuryToJsonSchema:", () => {
|
|
|
490
490
|
)->toBe(None)
|
|
491
491
|
})
|
|
492
492
|
})
|
|
493
|
+
|
|
494
|
+
// Reference-ness and DCB-tagged-ness are separate facts that happen to
|
|
495
|
+
// co-occur on `Reference.to_`. The schema walk tests them independently, and
|
|
496
|
+
// must keep doing so: a field can carry either alone, and a semantic type that
|
|
497
|
+
// is marked but deliberately *not* DCB-tagged (a storage ref, say) must not be
|
|
498
|
+
// dragged into entity-id classification by the other fact. Routing failures do
|
|
499
|
+
// not show up in a schema diff, so the distinction is pinned here executably.
|
|
500
|
+
describe("reference / DCB-tag decoupling:", () => {
|
|
501
|
+
let formatOf = (schema: JSON.t, fieldName: string): option<JSON.t> =>
|
|
502
|
+
getPropertyOf(schema, fieldName)->Option.flatMap(s => getProperty(s, "format"))
|
|
503
|
+
|
|
504
|
+
testSync("a DCB-tagged field with no reference is still an entity id", () => {
|
|
505
|
+
let schema = S.schema(s =>
|
|
506
|
+
{
|
|
507
|
+
"orderId": s.matches(Reventless.DcbTag.string),
|
|
508
|
+
}
|
|
509
|
+
)->S.castToUnknown
|
|
510
|
+
let json = SuryToJsonSchema.deriveObjectSchema(schema)
|
|
511
|
+
expect(formatOf(json, "orderId"))->toEqual(Some(JSON.Encode.string("uuid")))
|
|
512
|
+
expect(Reventless.Reference.getTarget(schema))->toBe(None)
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
testSync("a reference without a DCB tag is still an entity id", () => {
|
|
516
|
+
let field = Reventless.Reference.toWithoutDcbTag("Customer")
|
|
517
|
+
let schema = S.schema(s => {"customerId": s.matches(field)})->S.castToUnknown
|
|
518
|
+
let json = SuryToJsonSchema.deriveObjectSchema(schema)
|
|
519
|
+
expect(formatOf(json, "customerId"))->toEqual(Some(JSON.Encode.string("uuid")))
|
|
520
|
+
expect(Reventless.DcbTag.isTagged(field->S.castToUnknown))->toBe(false)
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
testSync("Reference.to_ carries both facts", () => {
|
|
524
|
+
let field = Reventless.Reference.to_("Customer")
|
|
525
|
+
expect(Reventless.DcbTag.isTagged(field->S.castToUnknown))->toBe(true)
|
|
526
|
+
expect(
|
|
527
|
+
Reventless.Reference.getTarget(field->S.castToUnknown)->Option.map(t => t.entity),
|
|
528
|
+
)->toEqual(Some("Customer"))
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
testSync("neither reference nor tag emits x-reventless-semantic on the wire", () => {
|
|
532
|
+
// `dateTime` and `reference` predate the generic marker and keep emitting
|
|
533
|
+
// `format`; adding the semantic key for them would change a published
|
|
534
|
+
// contract. Only semantics without a dedicated shape surface as the key.
|
|
535
|
+
let schema = S.schema(s =>
|
|
536
|
+
{
|
|
537
|
+
"customerId": s.matches(Reventless.Reference.to_("Customer")),
|
|
538
|
+
"placedAt": s.matches(Reventless.DateTime.string),
|
|
539
|
+
}
|
|
540
|
+
)->S.castToUnknown
|
|
541
|
+
let json = SuryToJsonSchema.deriveObjectSchema(schema)
|
|
542
|
+
let semanticOf = fieldName =>
|
|
543
|
+
getPropertyOf(json, fieldName)->Option.flatMap(s =>
|
|
544
|
+
getProperty(s, "x-reventless-semantic")
|
|
545
|
+
)
|
|
546
|
+
expect(semanticOf("customerId"))->toBe(None)
|
|
547
|
+
expect(semanticOf("placedAt"))->toBe(None)
|
|
548
|
+
})
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
// The reason the generic marker exists. `x-reventless-semantic` used to be
|
|
552
|
+
// emitted only by the annotation merge, which is fed by a PPX pass that only
|
|
553
|
+
// ever runs on read-model `state` records — so a command field could not carry
|
|
554
|
+
// a semantic at all. This walk is shape-driven and runs over every schema, so
|
|
555
|
+
// the declaration reaches the place the value is first accepted.
|
|
556
|
+
describe("type-carried semantics on a command field:", () => {
|
|
557
|
+
let semanticOf = (json, fieldName) =>
|
|
558
|
+
getPropertyOf(json, fieldName)->Option.flatMap(s => getProperty(s, "x-reventless-semantic"))
|
|
559
|
+
|
|
560
|
+
testSync("a storage-ref command field emits the semantic id", () => {
|
|
561
|
+
let schema = S.schema(s =>
|
|
562
|
+
{
|
|
563
|
+
"productId": s.matches(S.string),
|
|
564
|
+
"imageUrl": s.matches(Reventless.StorageRef.forStore(~store="productImages")),
|
|
565
|
+
}
|
|
566
|
+
)->S.castToUnknown
|
|
567
|
+
let json = SuryToJsonSchema.deriveObjectSchema(schema)
|
|
568
|
+
expect(semanticOf(json, "imageUrl"))->toEqual(Some(JSON.Encode.string("storageRef")))
|
|
569
|
+
expect(semanticOf(json, "productId"))->toBe(None)
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
testSync("it carries the store identity and marks the type as its source", () => {
|
|
573
|
+
let schema = S.schema(s =>
|
|
574
|
+
{
|
|
575
|
+
"imageUrl": s.matches(
|
|
576
|
+
Reventless.StorageRef.forStore(~plugin="catalog", ~store="productImages"),
|
|
577
|
+
),
|
|
578
|
+
}
|
|
579
|
+
)->S.castToUnknown
|
|
580
|
+
let json = SuryToJsonSchema.deriveObjectSchema(schema)
|
|
581
|
+
let field = getPropertyOf(json, "imageUrl")
|
|
582
|
+
expect(field->Option.flatMap(s => getProperty(s, "x-reventless-semantic-source")))->toEqual(
|
|
583
|
+
Some(JSON.Encode.string("type")),
|
|
584
|
+
)
|
|
585
|
+
expect(field->Option.flatMap(s => getProperty(s, "x-reventless-semantic-target")))->toEqual(
|
|
586
|
+
Some(
|
|
587
|
+
JSON.Encode.object(
|
|
588
|
+
Dict.fromArray([
|
|
589
|
+
("store", JSON.Encode.string("productImages")),
|
|
590
|
+
("plugin", JSON.Encode.string("catalog")),
|
|
591
|
+
]),
|
|
592
|
+
),
|
|
593
|
+
),
|
|
594
|
+
)
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
testSync("the underlying string shape is unchanged", () => {
|
|
598
|
+
// The marker refines an existing `string` field; nothing about what is
|
|
599
|
+
// stored changes, which is why it can be retrofitted onto a live schema.
|
|
600
|
+
let schema = S.schema(s =>
|
|
601
|
+
{"imageUrl": s.matches(Reventless.StorageRef.forStore(~store="productImages"))}
|
|
602
|
+
)->S.castToUnknown
|
|
603
|
+
let json = SuryToJsonSchema.deriveObjectSchema(schema)
|
|
604
|
+
expect(
|
|
605
|
+
getPropertyOf(json, "imageUrl")->Option.flatMap(s => getProperty(s, "type")),
|
|
606
|
+
)->toEqual(Some(JSON.Encode.string("string")))
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
testSync("a type-carried semantic beats an annotation naming the same field", () => {
|
|
610
|
+
let withSpec = (schema, spec) =>
|
|
611
|
+
schema->S.Metadata.set(~id=Reventless.StateAnnotations.stateAnnotationsId, spec)
|
|
612
|
+
let schema =
|
|
613
|
+
S.schema(s =>
|
|
614
|
+
{"imageUrl": s.matches(Reventless.StorageRef.forStore(~store="productImages"))}
|
|
615
|
+
)
|
|
616
|
+
->S.castToUnknown
|
|
617
|
+
->withSpec({...emptySpec, semantic: [("imageUrl", "image")]})
|
|
618
|
+
let json = SuryToJsonSchema.deriveObjectSchema(schema)
|
|
619
|
+
expect(semanticOf(json, "imageUrl"))->toEqual(Some(JSON.Encode.string("storageRef")))
|
|
620
|
+
})
|
|
621
|
+
})
|
|
493
622
|
})
|
|
@@ -5,7 +5,10 @@ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
|
5
5
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
6
6
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
7
|
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
8
|
+
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
8
9
|
import * as DateTime$Reventless from "@reventlessdev/reventless-spec/src/types/DateTime.res.mjs";
|
|
10
|
+
import * as Reference$Reventless from "@reventlessdev/reventless-spec/src/components/Reference.res.mjs";
|
|
11
|
+
import * as StorageRef$Reventless from "@reventlessdev/reventless-spec/src/semantic/StorageRef.res.mjs";
|
|
9
12
|
import * as StateAnnotations$Reventless from "@reventlessdev/reventless-spec/src/components/StateAnnotations.res.mjs";
|
|
10
13
|
import * as SuryToJsonSchema$ReventlessCore from "../../src/components/Api/SuryToJsonSchema.res.mjs";
|
|
11
14
|
|
|
@@ -833,6 +836,107 @@ globalThis.describe("SuryToJsonSchema:", () => {
|
|
|
833
836
|
globalThis.expect(Stdlib_Option.flatMap(entityIdSchema, s => getProperty(s, "x-reventless-id"))).toBe(undefined);
|
|
834
837
|
});
|
|
835
838
|
});
|
|
839
|
+
globalThis.describe("reference / DCB-tag decoupling:", () => {
|
|
840
|
+
let formatOf = (schema, fieldName) => Stdlib_Option.flatMap(getPropertyOf(schema, fieldName), s => getProperty(s, "format"));
|
|
841
|
+
globalThis.test("a DCB-tagged field with no reference is still an entity id", () => {
|
|
842
|
+
let schema = S.schema(s => ({
|
|
843
|
+
orderId: s.m(DcbTag$Reventless.string)
|
|
844
|
+
}));
|
|
845
|
+
let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(schema);
|
|
846
|
+
globalThis.expect(formatOf(json, "orderId")).toEqual("uuid");
|
|
847
|
+
globalThis.expect(Reference$Reventless.getTarget(schema)).toBe(undefined);
|
|
848
|
+
});
|
|
849
|
+
globalThis.test("a reference without a DCB tag is still an entity id", () => {
|
|
850
|
+
let field = Reference$Reventless.toWithoutDcbTag(undefined, "Customer");
|
|
851
|
+
let schema = S.schema(s => ({
|
|
852
|
+
customerId: s.m(field)
|
|
853
|
+
}));
|
|
854
|
+
let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(schema);
|
|
855
|
+
globalThis.expect(formatOf(json, "customerId")).toEqual("uuid");
|
|
856
|
+
globalThis.expect(DcbTag$Reventless.isTagged(field)).toBe(false);
|
|
857
|
+
});
|
|
858
|
+
globalThis.test("Reference.to_ carries both facts", () => {
|
|
859
|
+
let field = Reference$Reventless.to_(undefined, undefined, "Customer");
|
|
860
|
+
globalThis.expect(DcbTag$Reventless.isTagged(field)).toBe(true);
|
|
861
|
+
globalThis.expect(Stdlib_Option.map(Reference$Reventless.getTarget(field), t => t.entity)).toEqual("Customer");
|
|
862
|
+
});
|
|
863
|
+
globalThis.test("neither reference nor tag emits x-reventless-semantic on the wire", () => {
|
|
864
|
+
let schema = S.schema(s => ({
|
|
865
|
+
customerId: s.m(Reference$Reventless.to_(undefined, undefined, "Customer")),
|
|
866
|
+
placedAt: s.m(DateTime$Reventless.string)
|
|
867
|
+
}));
|
|
868
|
+
let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(schema);
|
|
869
|
+
let semanticOf = fieldName => Stdlib_Option.flatMap(getPropertyOf(json, fieldName), s => getProperty(s, "x-reventless-semantic"));
|
|
870
|
+
globalThis.expect(semanticOf("customerId")).toBe(undefined);
|
|
871
|
+
globalThis.expect(semanticOf("placedAt")).toBe(undefined);
|
|
872
|
+
});
|
|
873
|
+
});
|
|
874
|
+
globalThis.describe("type-carried semantics on a command field:", () => {
|
|
875
|
+
let semanticOf = (json, fieldName) => Stdlib_Option.flatMap(getPropertyOf(json, fieldName), s => getProperty(s, "x-reventless-semantic"));
|
|
876
|
+
globalThis.test("a storage-ref command field emits the semantic id", () => {
|
|
877
|
+
let schema = S.schema(s => ({
|
|
878
|
+
productId: s.m(S.string),
|
|
879
|
+
imageUrl: s.m(StorageRef$Reventless.forStore(undefined, "productImages"))
|
|
880
|
+
}));
|
|
881
|
+
let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(schema);
|
|
882
|
+
globalThis.expect(semanticOf(json, "imageUrl")).toEqual("storageRef");
|
|
883
|
+
globalThis.expect(semanticOf(json, "productId")).toBe(undefined);
|
|
884
|
+
});
|
|
885
|
+
globalThis.test("it carries the store identity and marks the type as its source", () => {
|
|
886
|
+
let schema = S.schema(s => ({
|
|
887
|
+
imageUrl: s.m(StorageRef$Reventless.forStore("catalog", "productImages"))
|
|
888
|
+
}));
|
|
889
|
+
let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(schema);
|
|
890
|
+
let field = getPropertyOf(json, "imageUrl");
|
|
891
|
+
globalThis.expect(Stdlib_Option.flatMap(field, s => getProperty(s, "x-reventless-semantic-source"))).toEqual("type");
|
|
892
|
+
globalThis.expect(Stdlib_Option.flatMap(field, s => getProperty(s, "x-reventless-semantic-target"))).toEqual(Object.fromEntries([
|
|
893
|
+
[
|
|
894
|
+
"store",
|
|
895
|
+
"productImages"
|
|
896
|
+
],
|
|
897
|
+
[
|
|
898
|
+
"plugin",
|
|
899
|
+
"catalog"
|
|
900
|
+
]
|
|
901
|
+
]));
|
|
902
|
+
});
|
|
903
|
+
globalThis.test("the underlying string shape is unchanged", () => {
|
|
904
|
+
let schema = S.schema(s => ({
|
|
905
|
+
imageUrl: s.m(StorageRef$Reventless.forStore(undefined, "productImages"))
|
|
906
|
+
}));
|
|
907
|
+
let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(schema);
|
|
908
|
+
globalThis.expect(Stdlib_Option.flatMap(getPropertyOf(json, "imageUrl"), s => getProperty(s, "type"))).toEqual("string");
|
|
909
|
+
});
|
|
910
|
+
globalThis.test("a type-carried semantic beats an annotation naming the same field", () => {
|
|
911
|
+
let withSpec = (schema, spec) => S.Metadata.set(schema, StateAnnotations$Reventless.stateAnnotationsId, spec);
|
|
912
|
+
let schema = withSpec(S.schema(s => ({
|
|
913
|
+
imageUrl: s.m(StorageRef$Reventless.forStore(undefined, "productImages"))
|
|
914
|
+
})), {
|
|
915
|
+
ids: emptySpec_ids,
|
|
916
|
+
compositeIds: emptySpec_compositeIds,
|
|
917
|
+
subIds: emptySpec_subIds,
|
|
918
|
+
compositeSubIds: emptySpec_compositeSubIds,
|
|
919
|
+
indexes: emptySpec_indexes,
|
|
920
|
+
hidden: emptySpec_hidden,
|
|
921
|
+
summary: emptySpec_summary,
|
|
922
|
+
drillTargets: emptySpec_drillTargets,
|
|
923
|
+
drillTargetKeys: emptySpec_drillTargetKeys,
|
|
924
|
+
collapsed: emptySpec_collapsed,
|
|
925
|
+
scan: emptySpec_scan,
|
|
926
|
+
scanSort: emptySpec_scanSort,
|
|
927
|
+
semantic: [[
|
|
928
|
+
"imageUrl",
|
|
929
|
+
"image"
|
|
930
|
+
]],
|
|
931
|
+
metric: emptySpec_metric,
|
|
932
|
+
status: undefined,
|
|
933
|
+
groupBy: undefined,
|
|
934
|
+
visibility: undefined
|
|
935
|
+
});
|
|
936
|
+
let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(schema);
|
|
937
|
+
globalThis.expect(semanticOf(json, "imageUrl")).toEqual("storageRef");
|
|
938
|
+
});
|
|
939
|
+
});
|
|
836
940
|
});
|
|
837
941
|
|
|
838
942
|
export {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// The event-history query is the read counterpart of the Source A raw-event
|
|
4
|
+
// subscription. These tests pin the two properties that make it safe to expose:
|
|
5
|
+
// it is deduped per event log exactly as the subscription is, and it leaks none
|
|
6
|
+
// of the transport / tenant-context fields of `Message.meta`.
|
|
7
|
+
|
|
8
|
+
module SomeEvent = {
|
|
9
|
+
@schema
|
|
10
|
+
type event = Happened({thingId: string})
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let entry = (~displayName): ReventlessInfra.Api.eventLogSchemaEntry => {
|
|
14
|
+
busKey: displayName ++ "AggrEventLog",
|
|
15
|
+
displayName,
|
|
16
|
+
eventSchema: SomeEvent.eventSchema->S.castToUnknown,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let allTypes = (r: Plugin_EventQuerySchema.result) => r.extraTypes->Array.join("\n")
|
|
20
|
+
|
|
21
|
+
describe("Plugin_EventQuerySchema.generate", () => {
|
|
22
|
+
testSync("emits one history query per event log, plugin-prefixed", () => {
|
|
23
|
+
let r = Plugin_EventQuerySchema.generate(
|
|
24
|
+
~plugin="Ordering",
|
|
25
|
+
~eventLogEntries=[entry(~displayName="Order"), entry(~displayName="Shipment")],
|
|
26
|
+
)
|
|
27
|
+
expect(r.queryFields->Array.length)->toBe(2)
|
|
28
|
+
let joined = r.queryFields->Array.join("\n")
|
|
29
|
+
expect(joined->String.includes("Ordering_OrderEventHistory("))->toBe(true)
|
|
30
|
+
expect(joined->String.includes("Ordering_ShipmentEventHistory("))->toBe(true)
|
|
31
|
+
// Relay connection args + return type, identical in shape to every other
|
|
32
|
+
// list query — existing client pagination applies unchanged.
|
|
33
|
+
expect(joined->String.includes("first: Int, after: String, last: Int, before: String"))->toBe(
|
|
34
|
+
true,
|
|
35
|
+
)
|
|
36
|
+
expect(joined->String.includes(": Ordering_OrderEventRecordConnection!"))->toBe(true)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
testSync("dedupes by display name — one logical stream, one field", () => {
|
|
40
|
+
let r = Plugin_EventQuerySchema.generate(
|
|
41
|
+
~plugin="Ordering",
|
|
42
|
+
~eventLogEntries=[entry(~displayName="Order"), entry(~displayName="Order")],
|
|
43
|
+
)
|
|
44
|
+
expect(r.queryFields->Array.length)->toBe(1)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
testSync("record carries the audit-relevant meta subset and nothing else", () => {
|
|
48
|
+
let sdl = allTypes(
|
|
49
|
+
Plugin_EventQuerySchema.generate(
|
|
50
|
+
~plugin="Ordering",
|
|
51
|
+
~eventLogEntries=[entry(~displayName="Order")],
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
expect(sdl->String.includes("type EventMeta {"))->toBe(true)
|
|
55
|
+
["user", "time", "service", "msgId", "correlationId", "causationId"]->Array.forEach(f =>
|
|
56
|
+
expect(sdl->String.includes(f))->toBe(true)
|
|
57
|
+
)
|
|
58
|
+
// Transport / tracing detail and the cross-cutting context bag must not
|
|
59
|
+
// leave the server — see the module comment.
|
|
60
|
+
["ip:", "traceparent", "schemaVersion", "headers"]->Array.forEach(f =>
|
|
61
|
+
expect(sdl->String.includes(f))->toBe(false)
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
testSync("record exposes position, payload, tags and both timestamps", () => {
|
|
66
|
+
let sdl = allTypes(
|
|
67
|
+
Plugin_EventQuerySchema.generate(
|
|
68
|
+
~plugin="Ordering",
|
|
69
|
+
~eventLogEntries=[entry(~displayName="Order")],
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
expect(sdl->String.includes("type Ordering_OrderEventRecord {"))->toBe(true)
|
|
73
|
+
["position: String!", "eventType: String!", "payload: AWSJSON!", "tags: [EventTag!]!", "meta: EventMeta!", "recordedAt: String!"]->Array.forEach(
|
|
74
|
+
f => expect(sdl->String.includes(f))->toBe(true),
|
|
75
|
+
)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
testSync("filter offers the entity shortcut and the precise tag pair", () => {
|
|
79
|
+
let sdl = allTypes(
|
|
80
|
+
Plugin_EventQuerySchema.generate(
|
|
81
|
+
~plugin="Ordering",
|
|
82
|
+
~eventLogEntries=[entry(~displayName="Order")],
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
expect(sdl->String.includes("input Ordering_OrderEventHistoryFilter {"))->toBe(true)
|
|
86
|
+
["entityId: ID", "tagKey: String", "tagValue: String", "eventTypes: [String!]", "user: String", "timeFrom: String", "timeTo: String"]->Array.forEach(
|
|
87
|
+
f => expect(sdl->String.includes(f))->toBe(true),
|
|
88
|
+
)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
testSync("a plugin with no event logs emits nothing at all", () => {
|
|
92
|
+
let r = Plugin_EventQuerySchema.generate(~plugin="Ordering", ~eventLogEntries=[])
|
|
93
|
+
expect(r.queryFields->Array.length)->toBe(0)
|
|
94
|
+
// Not even the shared types — an empty fragment stays byte-identical to
|
|
95
|
+
// what it was before this generator existed.
|
|
96
|
+
expect(r.extraTypes->Array.length)->toBe(0)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Plugin_EventQuerySchema$ReventlessCore from "../../src/plugin/component/Plugin_EventQuerySchema.res.mjs";
|
|
5
|
+
|
|
6
|
+
let eventSchema = S.schema(s => ({
|
|
7
|
+
TAG: "Happened",
|
|
8
|
+
thingId: s.m(S.string)
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
let SomeEvent = {
|
|
12
|
+
eventSchema: eventSchema
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function entry(displayName) {
|
|
16
|
+
return {
|
|
17
|
+
busKey: displayName + "AggrEventLog",
|
|
18
|
+
displayName: displayName,
|
|
19
|
+
eventSchema: eventSchema
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function allTypes(r) {
|
|
24
|
+
return r.extraTypes.join("\n");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
globalThis.describe("Plugin_EventQuerySchema.generate", () => {
|
|
28
|
+
globalThis.test("emits one history query per event log, plugin-prefixed", () => {
|
|
29
|
+
let r = Plugin_EventQuerySchema$ReventlessCore.generate("Ordering", [
|
|
30
|
+
entry("Order"),
|
|
31
|
+
entry("Shipment")
|
|
32
|
+
]);
|
|
33
|
+
globalThis.expect(r.queryFields.length).toBe(2);
|
|
34
|
+
let joined = r.queryFields.join("\n");
|
|
35
|
+
globalThis.expect(joined.includes("Ordering_OrderEventHistory(")).toBe(true);
|
|
36
|
+
globalThis.expect(joined.includes("Ordering_ShipmentEventHistory(")).toBe(true);
|
|
37
|
+
globalThis.expect(joined.includes("first: Int, after: String, last: Int, before: String")).toBe(true);
|
|
38
|
+
globalThis.expect(joined.includes(": Ordering_OrderEventRecordConnection!")).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
globalThis.test("dedupes by display name — one logical stream, one field", () => {
|
|
41
|
+
let r = Plugin_EventQuerySchema$ReventlessCore.generate("Ordering", [
|
|
42
|
+
entry("Order"),
|
|
43
|
+
entry("Order")
|
|
44
|
+
]);
|
|
45
|
+
globalThis.expect(r.queryFields.length).toBe(1);
|
|
46
|
+
});
|
|
47
|
+
globalThis.test("record carries the audit-relevant meta subset and nothing else", () => {
|
|
48
|
+
let r = Plugin_EventQuerySchema$ReventlessCore.generate("Ordering", [entry("Order")]);
|
|
49
|
+
let sdl = r.extraTypes.join("\n");
|
|
50
|
+
globalThis.expect(sdl.includes("type EventMeta {")).toBe(true);
|
|
51
|
+
[
|
|
52
|
+
"user",
|
|
53
|
+
"time",
|
|
54
|
+
"service",
|
|
55
|
+
"msgId",
|
|
56
|
+
"correlationId",
|
|
57
|
+
"causationId"
|
|
58
|
+
].forEach(f => {
|
|
59
|
+
globalThis.expect(sdl.includes(f)).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
[
|
|
62
|
+
"ip:",
|
|
63
|
+
"traceparent",
|
|
64
|
+
"schemaVersion",
|
|
65
|
+
"headers"
|
|
66
|
+
].forEach(f => {
|
|
67
|
+
globalThis.expect(sdl.includes(f)).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
globalThis.test("record exposes position, payload, tags and both timestamps", () => {
|
|
71
|
+
let r = Plugin_EventQuerySchema$ReventlessCore.generate("Ordering", [entry("Order")]);
|
|
72
|
+
let sdl = r.extraTypes.join("\n");
|
|
73
|
+
globalThis.expect(sdl.includes("type Ordering_OrderEventRecord {")).toBe(true);
|
|
74
|
+
[
|
|
75
|
+
"position: String!",
|
|
76
|
+
"eventType: String!",
|
|
77
|
+
"payload: AWSJSON!",
|
|
78
|
+
"tags: [EventTag!]!",
|
|
79
|
+
"meta: EventMeta!",
|
|
80
|
+
"recordedAt: String!"
|
|
81
|
+
].forEach(f => {
|
|
82
|
+
globalThis.expect(sdl.includes(f)).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
globalThis.test("filter offers the entity shortcut and the precise tag pair", () => {
|
|
86
|
+
let r = Plugin_EventQuerySchema$ReventlessCore.generate("Ordering", [entry("Order")]);
|
|
87
|
+
let sdl = r.extraTypes.join("\n");
|
|
88
|
+
globalThis.expect(sdl.includes("input Ordering_OrderEventHistoryFilter {")).toBe(true);
|
|
89
|
+
[
|
|
90
|
+
"entityId: ID",
|
|
91
|
+
"tagKey: String",
|
|
92
|
+
"tagValue: String",
|
|
93
|
+
"eventTypes: [String!]",
|
|
94
|
+
"user: String",
|
|
95
|
+
"timeFrom: String",
|
|
96
|
+
"timeTo: String"
|
|
97
|
+
].forEach(f => {
|
|
98
|
+
globalThis.expect(sdl.includes(f)).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
globalThis.test("a plugin with no event logs emits nothing at all", () => {
|
|
102
|
+
let r = Plugin_EventQuerySchema$ReventlessCore.generate("Ordering", []);
|
|
103
|
+
globalThis.expect(r.queryFields.length).toBe(0);
|
|
104
|
+
globalThis.expect(r.extraTypes.length).toBe(0);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
export {
|
|
109
|
+
SomeEvent,
|
|
110
|
+
entry,
|
|
111
|
+
allTypes,
|
|
112
|
+
}
|
|
113
|
+
/* eventSchema Not a pure module */
|