@reventlessdev/reventless-core 3.0.0-alpha.193 → 3.0.0-alpha.195
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/SchemaType.res +36 -0
- package/src/components/Api/SchemaType.res.mjs +22 -0
- package/src/components/Api/SuryToJsonSchema.res +18 -2
- package/src/components/Api/SuryToJsonSchema.res.mjs +8 -4
- package/src/plugin/component/Plugin_Structure.res +19 -2
- package/src/plugin/component/Plugin_Structure.res.mjs +11 -8
- package/tests/api/SuryToJsonSchemaTest.res +16 -0
- package/tests/api/SuryToJsonSchemaTest.res.mjs +8 -0
- package/tests/plugin/PluginStructureTest.res +122 -21
- package/tests/plugin/PluginStructureTest.res.mjs +96 -24
- package/tests/plugin/StateChangeSlice/PsUploadAvatar.res +37 -0
- package/tests/plugin/StateChangeSlice/PsUploadAvatar.res.mjs +64 -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.195 (2026-07-30)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **core:** a command's field markers reach the wire, and its optional fields stay optional ([f1c1112](https://github.com/ReventlessDev/reventless-core/commit/f1c1112e9baa6b06e50097a5a618f49c9301cd0a))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.194 (2026-07-30)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **spec,core:** record the storageRef annotation instead of inferring it ([06fb5d6](https://github.com/ReventlessDev/reventless-core/commit/06fb5d671db91fb536acefd0c2db69d98671da39))
|
|
18
|
+
* **spec:** read a semantic marker through an optional field's wrapper ([abebaa9](https://github.com/ReventlessDev/reventless-core/commit/abebaa9e6c73986765bb3de589ba0414eb0d85da))
|
|
19
|
+
|
|
20
|
+
|
|
6
21
|
# 3.0.0-alpha.193 (2026-07-30)
|
|
7
22
|
|
|
8
23
|
### Features
|
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.195",
|
|
4
4
|
"description": "Core package for Reventless framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -28,17 +28,17 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"sury": "11.0.0-alpha.4",
|
|
30
30
|
"uuid": "^13.0.0",
|
|
31
|
-
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.13",
|
|
32
31
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.31",
|
|
32
|
+
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.13",
|
|
33
33
|
"@reventlessdev/rescript-fast-csv": "1.2.0-alpha.14",
|
|
34
34
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.9",
|
|
35
35
|
"@reventlessdev/rescript-node-streams": "1.1.0-alpha.13",
|
|
36
|
-
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.17",
|
|
37
36
|
"@reventlessdev/rescript-ssh2": "1.1.0-alpha.14",
|
|
38
37
|
"@reventlessdev/rescript-uuid": "1.1.0-alpha.17",
|
|
39
|
-
"@reventlessdev/
|
|
40
|
-
"@reventlessdev/reventless-
|
|
41
|
-
"@reventlessdev/reventless-
|
|
38
|
+
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.17",
|
|
39
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.29",
|
|
40
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.88",
|
|
41
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.113"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"rescript": "12.3.0",
|
|
@@ -141,6 +141,42 @@ and shapeOf = (~parentName: string, ~fieldName: string, schema: S.t<unknown>): s
|
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
/**
|
|
145
|
+
The names of an object's optional properties, read from sury rather than from
|
|
146
|
+
the IR.
|
|
147
|
+
|
|
148
|
+
The IR cannot answer this for every field. `shapeOf` classifies a DCB-tagged or
|
|
149
|
+
`@ref` field as `EntityId` before it ever looks at the union sury wraps an
|
|
150
|
+
`option<…>` in — correctly, since those markers now describe the field through
|
|
151
|
+
that wrapper — so an optional reference reaches the IR as a plain `EntityId`
|
|
152
|
+
with its optionality spent. Asking the source schema keeps the two questions
|
|
153
|
+
apart: what a field *is*, and whether it has to be there.
|
|
154
|
+
|
|
155
|
+
Top-level properties only. A nested object's own fields are emitted by the
|
|
156
|
+
`ObjectRef` branch, which has no `required` of its own today.
|
|
157
|
+
*/
|
|
158
|
+
let optionalFieldNames = (schema: S.t<unknown>): array<string> =>
|
|
159
|
+
switch schema {
|
|
160
|
+
| Object({properties}) =>
|
|
161
|
+
properties
|
|
162
|
+
->Dict.toArray
|
|
163
|
+
->Array.filterMap(((propName, propSchema)) =>
|
|
164
|
+
switch propSchema {
|
|
165
|
+
| Union({anyOf}) =>
|
|
166
|
+
anyOf->Array.some(v =>
|
|
167
|
+
switch v {
|
|
168
|
+
| Null(_) | Undefined(_) => true
|
|
169
|
+
| _ => false
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
? Some(propName)
|
|
173
|
+
: None
|
|
174
|
+
| _ => None
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
| _ => []
|
|
178
|
+
}
|
|
179
|
+
|
|
144
180
|
let fromSuryObject = (~typeName: string, schema: S.t<unknown>): option<dict<schemaType>> =>
|
|
145
181
|
switch schema {
|
|
146
182
|
| Object({properties}) =>
|
|
@@ -150,6 +150,27 @@ function shapeOf(parentName, fieldName, schema) {
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
function optionalFieldNames(schema) {
|
|
154
|
+
if (schema.type === "object") {
|
|
155
|
+
return Stdlib_Array.filterMap(Object.entries(schema.properties), param => {
|
|
156
|
+
let propSchema = param[1];
|
|
157
|
+
if (propSchema.type === "union" && propSchema.anyOf.some(v => {
|
|
158
|
+
switch (v.type) {
|
|
159
|
+
case "null" :
|
|
160
|
+
case "undefined" :
|
|
161
|
+
return true;
|
|
162
|
+
default:
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
})) {
|
|
166
|
+
return param[0];
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
} else {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
153
174
|
function fromSuryObject(typeName, schema) {
|
|
154
175
|
if (schema.type !== "object") {
|
|
155
176
|
return;
|
|
@@ -176,6 +197,7 @@ export {
|
|
|
176
197
|
isIdsFieldName,
|
|
177
198
|
fromSury,
|
|
178
199
|
shapeOf,
|
|
200
|
+
optionalFieldNames,
|
|
179
201
|
fromSuryObject,
|
|
180
202
|
}
|
|
181
203
|
/* DcbTag-Reventless Not a pure module */
|
|
@@ -179,8 +179,18 @@ and withSemantic = (fieldSchema: JSON.t, sem: Reventless.Semantic.t): JSON.t =>
|
|
|
179
179
|
JSON.Encode.object(obj)
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
// `optional` names the fields that may be absent; everything else is required.
|
|
183
|
+
// It comes from the sury schema (`SchemaType.optionalFieldNames`) rather than
|
|
184
|
+
// from the IR, which cannot answer for a reference or a DCB-tagged field — both
|
|
185
|
+
// classify as `EntityId` before the nullable wrapper is ever examined.
|
|
186
|
+
//
|
|
187
|
+
// Defaulting to "all required" mattered nowhere while only read-model state came
|
|
188
|
+
// through here: nothing validates a read schema. A command schema is the input
|
|
189
|
+
// of a form, and a field listed as required is one the form refuses to submit
|
|
190
|
+
// without — which turned `imageUrl?` into a picture every product had to have.
|
|
182
191
|
and objectRefToJsonSchema = (
|
|
183
192
|
~annotations: option<Reventless.StateAnnotations.stateAnnotationSpec>=?,
|
|
193
|
+
~optional: array<string>=[],
|
|
184
194
|
fields: dict<SchemaType.schemaType>,
|
|
185
195
|
): JSON.t => {
|
|
186
196
|
let props = Dict.make()
|
|
@@ -194,7 +204,9 @@ and objectRefToJsonSchema = (
|
|
|
194
204
|
| None => baseSchema
|
|
195
205
|
}
|
|
196
206
|
props->Dict.set(fieldName, withAnnotations)
|
|
197
|
-
|
|
207
|
+
if !(optional->Array.includes(fieldName)) {
|
|
208
|
+
required->Array.push(fieldName)
|
|
209
|
+
}
|
|
198
210
|
})
|
|
199
211
|
let entries: array<(string, JSON.t)> = [
|
|
200
212
|
("type", str("object")),
|
|
@@ -212,7 +224,11 @@ let deriveObjectSchema = (schema: S.t<unknown>): JSON.t =>
|
|
|
212
224
|
switch SchemaType.fromSuryObject(~typeName="", schema) {
|
|
213
225
|
| Some(fields) =>
|
|
214
226
|
let annotations = Reventless.StateAnnotations.getSpec(schema)
|
|
215
|
-
let objSchema = objectRefToJsonSchema(
|
|
227
|
+
let objSchema = objectRefToJsonSchema(
|
|
228
|
+
~annotations?,
|
|
229
|
+
~optional=SchemaType.optionalFieldNames(schema),
|
|
230
|
+
fields,
|
|
231
|
+
)
|
|
216
232
|
// Surface component-level visibility on the top-level object schema.
|
|
217
233
|
// Omitted entirely for the default `Public` (None) so schemas stay compact.
|
|
218
234
|
switch annotations {
|
|
@@ -177,7 +177,7 @@ function fromSchemaType(st) {
|
|
|
177
177
|
]
|
|
178
178
|
]);
|
|
179
179
|
case "ObjectRef" :
|
|
180
|
-
return objectRefToJsonSchema(undefined, st._1);
|
|
180
|
+
return objectRefToJsonSchema(undefined, undefined, st._1);
|
|
181
181
|
case "Enum" :
|
|
182
182
|
return Object.fromEntries([
|
|
183
183
|
[
|
|
@@ -221,7 +221,8 @@ function withSemantic(fieldSchema, sem) {
|
|
|
221
221
|
return obj;
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
-
function objectRefToJsonSchema(annotations, fields) {
|
|
224
|
+
function objectRefToJsonSchema(annotations, optionalOpt, fields) {
|
|
225
|
+
let optional = optionalOpt !== undefined ? optionalOpt : [];
|
|
225
226
|
let props = {};
|
|
226
227
|
let required = [];
|
|
227
228
|
Object.entries(fields).forEach(param => {
|
|
@@ -229,7 +230,10 @@ function objectRefToJsonSchema(annotations, fields) {
|
|
|
229
230
|
let baseSchema = fromSchemaType(param[1]);
|
|
230
231
|
let withAnnotations = annotations !== undefined ? mergeAnnotations(baseSchema, fieldName, annotations) : baseSchema;
|
|
231
232
|
props[fieldName] = withAnnotations;
|
|
232
|
-
|
|
233
|
+
if (!optional.includes(fieldName)) {
|
|
234
|
+
required.push(fieldName);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
233
237
|
});
|
|
234
238
|
let entries = [
|
|
235
239
|
[
|
|
@@ -259,7 +263,7 @@ function deriveObjectSchema(schema) {
|
|
|
259
263
|
]]);
|
|
260
264
|
}
|
|
261
265
|
let annotations = StateAnnotations$Reventless.getSpec(schema);
|
|
262
|
-
let objSchema = objectRefToJsonSchema(annotations, fields);
|
|
266
|
+
let objSchema = objectRefToJsonSchema(annotations, SchemaType$ReventlessCore.optionalFieldNames(schema), fields);
|
|
263
267
|
if (annotations === undefined) {
|
|
264
268
|
return objSchema;
|
|
265
269
|
}
|
|
@@ -269,7 +269,14 @@ let make = (
|
|
|
269
269
|
}
|
|
270
270
|
({
|
|
271
271
|
Reventless.Plugin.name: variantName,
|
|
272
|
-
schema:
|
|
272
|
+
// The derived schema, not sury's raw one: `S.toJSONSchema` carries the
|
|
273
|
+
// shape and drops every `x-reventless-*` marker the PPX put on the
|
|
274
|
+
// fields, so a command's `@storageRef`/`@semantic`/`@ref` reached the
|
|
275
|
+
// wire on the read side and nowhere on the write side. A reader that
|
|
276
|
+
// matches a field against its setter — or picks the upload endpoint of
|
|
277
|
+
// the store a command argument declares — then has nothing to match on.
|
|
278
|
+
// `MCP_SchemaGenerator` already derives these same variant schemas.
|
|
279
|
+
schema: v->SuryToJsonSchema.deriveObjectSchema->JSON.stringify,
|
|
273
280
|
level,
|
|
274
281
|
aggregateIdField,
|
|
275
282
|
// A non-exposed (`@noApi`) variant has no callable mutation field. For a
|
|
@@ -343,7 +350,9 @@ let make = (
|
|
|
343
350
|
)
|
|
344
351
|
({
|
|
345
352
|
Reventless.Plugin.name: variantName,
|
|
346
|
-
schema
|
|
353
|
+
// Derived for the same reason as `commandDef.schema` above: an event's
|
|
354
|
+
// field markers are the write side's half of the same vocabulary.
|
|
355
|
+
schema: v->SuryToJsonSchema.deriveObjectSchema->JSON.stringify,
|
|
347
356
|
references,
|
|
348
357
|
}: Reventless.Plugin.eventDef)
|
|
349
358
|
}
|
|
@@ -498,6 +507,14 @@ let make = (
|
|
|
498
507
|
Reventless.Plugin.store: target.plugin->Option.getOr(name) ++ "." ++ target.store,
|
|
499
508
|
component,
|
|
500
509
|
field,
|
|
510
|
+
// The annotation as its author wrote it. `target.plugin` is `None`
|
|
511
|
+
// exactly when the field left the store unqualified, so this is the
|
|
512
|
+
// source text rather than a guess at it — and this is the only place
|
|
513
|
+
// that distinction survives.
|
|
514
|
+
annotation: switch target.plugin {
|
|
515
|
+
| None => target.store
|
|
516
|
+
| Some(plugin) => plugin ++ "." ++ target.store
|
|
517
|
+
},
|
|
501
518
|
})
|
|
502
519
|
)
|
|
503
520
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
-
import * as S from "sury/src/S.res.mjs";
|
|
4
3
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
5
4
|
import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
|
|
6
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
@@ -229,7 +228,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
229
228
|
}
|
|
230
229
|
return {
|
|
231
230
|
name: variantName,
|
|
232
|
-
schema: JSON.stringify(
|
|
231
|
+
schema: JSON.stringify(SuryToJsonSchema$ReventlessCore.deriveObjectSchema(v)),
|
|
233
232
|
level: match[0],
|
|
234
233
|
aggregateIdField: match[1],
|
|
235
234
|
mutationField: apiExposed ? mutationFieldFor(variantName) : "",
|
|
@@ -281,7 +280,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
281
280
|
});
|
|
282
281
|
return {
|
|
283
282
|
name: variantName,
|
|
284
|
-
schema: JSON.stringify(
|
|
283
|
+
schema: JSON.stringify(SuryToJsonSchema$ReventlessCore.deriveObjectSchema(v)),
|
|
285
284
|
references: references
|
|
286
285
|
};
|
|
287
286
|
};
|
|
@@ -447,11 +446,15 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
447
446
|
let properties = v.properties;
|
|
448
447
|
return Stdlib_Array.filterMap(Object.entries(properties), param => {
|
|
449
448
|
let field = param[0];
|
|
450
|
-
return Stdlib_Option.map(StorageRef$Reventless.getStore(param[1]), target =>
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
449
|
+
return Stdlib_Option.map(StorageRef$Reventless.getStore(param[1]), target => {
|
|
450
|
+
let plugin = target.plugin;
|
|
451
|
+
return {
|
|
452
|
+
store: Stdlib_Option.getOr(target.plugin, name) + "." + target.store,
|
|
453
|
+
component: component,
|
|
454
|
+
field: field,
|
|
455
|
+
annotation: plugin !== undefined ? plugin + "." + target.store : target.store
|
|
456
|
+
};
|
|
457
|
+
});
|
|
455
458
|
});
|
|
456
459
|
} else {
|
|
457
460
|
return [];
|
|
@@ -606,6 +606,22 @@ describe("SuryToJsonSchema:", () => {
|
|
|
606
606
|
)->toEqual(Some(JSON.Encode.string("string")))
|
|
607
607
|
})
|
|
608
608
|
|
|
609
|
+
// An optional field's marker sits inside the wrapper sury-ppx builds around
|
|
610
|
+
// it, so this used to emit a plain nullable string: the field was a storage
|
|
611
|
+
// ref in the source and an untyped string on the wire. The semantic now
|
|
612
|
+
// survives, and it rides *beside* the nullable shape rather than replacing
|
|
613
|
+
// it — a reader still learns the value may be absent.
|
|
614
|
+
testSync("an optional storage-ref field keeps both its semantic and its nullability", () => {
|
|
615
|
+
let schema = S.schema(s =>
|
|
616
|
+
{"imageUrl": s.matches(S.option(Reventless.StorageRef.forStore(~store="productImages")))}
|
|
617
|
+
)->S.castToUnknown
|
|
618
|
+
let json = SuryToJsonSchema.deriveObjectSchema(schema)
|
|
619
|
+
expect(semanticOf(json, "imageUrl"))->toEqual(Some(JSON.Encode.string("storageRef")))
|
|
620
|
+
expect(
|
|
621
|
+
getPropertyOf(json, "imageUrl")->Option.flatMap(s => getProperty(s, "oneOf"))->Option.isSome,
|
|
622
|
+
)->toBe(true)
|
|
623
|
+
})
|
|
624
|
+
|
|
609
625
|
testSync("a type-carried semantic beats an annotation naming the same field", () => {
|
|
610
626
|
let withSpec = (schema, spec) =>
|
|
611
627
|
schema->S.Metadata.set(~id=Reventless.StateAnnotations.stateAnnotationsId, spec)
|
|
@@ -909,6 +909,14 @@ globalThis.describe("SuryToJsonSchema:", () => {
|
|
|
909
909
|
let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(schema);
|
|
910
910
|
globalThis.expect(Stdlib_Option.flatMap(getPropertyOf(json, "imageUrl"), s => getProperty(s, "type"))).toEqual("string");
|
|
911
911
|
});
|
|
912
|
+
globalThis.test("an optional storage-ref field keeps both its semantic and its nullability", () => {
|
|
913
|
+
let schema = S.schema(s => ({
|
|
914
|
+
imageUrl: s.m(S.option(StorageRef$Reventless.forStore(undefined, "productImages")))
|
|
915
|
+
}));
|
|
916
|
+
let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(schema);
|
|
917
|
+
globalThis.expect(semanticOf(json, "imageUrl")).toEqual("storageRef");
|
|
918
|
+
globalThis.expect(Stdlib_Option.isSome(Stdlib_Option.flatMap(getPropertyOf(json, "imageUrl"), s => getProperty(s, "oneOf")))).toBe(true);
|
|
919
|
+
});
|
|
912
920
|
globalThis.test("a type-carried semantic beats an annotation naming the same field", () => {
|
|
913
921
|
let withSpec = (schema, spec) => S.Metadata.set(schema, StateAnnotations$Reventless.stateAnnotationsId, spec);
|
|
914
922
|
let schema = withSpec(S.schema(s => ({
|
|
@@ -93,6 +93,20 @@ module PsAttachInvoiceSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
|
93
93
|
let make = (~dcbEventLog as _, ~publishJsons as _, ~tagKeysByEventType as _=?, ~crossPartitionTagKeys as _=?, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
module PsUploadAvatarSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
97
|
+
module Spec = PsUploadAvatar
|
|
98
|
+
module Behavior = {
|
|
99
|
+
type state = PsUploadAvatar.state
|
|
100
|
+
let initialState = PsUploadAvatar.initialState
|
|
101
|
+
let evolve = PsUploadAvatar.evolve
|
|
102
|
+
let decide = PsUploadAvatar.decide
|
|
103
|
+
let moduleUrl = PsUploadAvatar.moduleUrl
|
|
104
|
+
}
|
|
105
|
+
let isAsync = false
|
|
106
|
+
type component = scsComponent
|
|
107
|
+
let make = (~dcbEventLog as _, ~publishJsons as _, ~tagKeysByEventType as _=?, ~crossPartitionTagKeys as _=?, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
|
|
108
|
+
}
|
|
109
|
+
|
|
96
110
|
module PsChangePhotoSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
97
111
|
module Spec = PsChangePhoto
|
|
98
112
|
module Behavior = {
|
|
@@ -535,26 +549,28 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
535
549
|
})
|
|
536
550
|
})
|
|
537
551
|
|
|
538
|
-
//
|
|
539
|
-
//
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
| None => None
|
|
546
|
-
}
|
|
552
|
+
// Shared by the read-side and write-side schema cases below, which ask the
|
|
553
|
+
// same question of the same emitter.
|
|
554
|
+
let getProperty = (json: JSON.t, key: string): option<JSON.t> =>
|
|
555
|
+
switch json->JSON.Decode.object {
|
|
556
|
+
| Some(obj) => obj->Dict.get(key)
|
|
557
|
+
| None => None
|
|
558
|
+
}
|
|
547
559
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
| None => None
|
|
554
|
-
}
|
|
560
|
+
let getPropertyOf = (json: JSON.t, fieldName: string): option<JSON.t> =>
|
|
561
|
+
switch getProperty(json, "properties") {
|
|
562
|
+
| Some(props) =>
|
|
563
|
+
switch props->JSON.Decode.object {
|
|
564
|
+
| Some(obj) => obj->Dict.get(fieldName)
|
|
555
565
|
| None => None
|
|
556
566
|
}
|
|
567
|
+
| None => None
|
|
568
|
+
}
|
|
557
569
|
|
|
570
|
+
// Phase 4: queryableDef.schema must carry x-reventless-* extension keys for
|
|
571
|
+
// annotated state types. Plugin_Structure now uses SuryToJsonSchema.deriveObjectSchema
|
|
572
|
+
// (annotation-aware) instead of S.toJSONSchema (metadata-blind).
|
|
573
|
+
describe("queryableDef.schema propagates x-reventless-* annotations", () => {
|
|
558
574
|
let parseSchema = (svs: Reventless.Plugin.queryableDef): JSON.t =>
|
|
559
575
|
svs.schema->JSON.parseOrThrow
|
|
560
576
|
|
|
@@ -694,9 +710,22 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
694
710
|
// `documents` is declared on a command field AND an event field of the
|
|
695
711
|
// same component under the same field name — one declaration site, so
|
|
696
712
|
// the identical triples collapse to one entry.
|
|
713
|
+
// `annotation` is the source text, not the key: the own-plugin store is
|
|
714
|
+
// written bare and the foreign one qualified, which is exactly the
|
|
715
|
+
// distinction no downstream comparison can recover.
|
|
697
716
|
let expected: array<Reventless.Plugin.requiredStoreDeclaration> = [
|
|
698
|
-
{
|
|
699
|
-
|
|
717
|
+
{
|
|
718
|
+
store: "TestPlugin.documents",
|
|
719
|
+
component: "AttachInvoice",
|
|
720
|
+
field: "documentUrl",
|
|
721
|
+
annotation: "documents",
|
|
722
|
+
},
|
|
723
|
+
{
|
|
724
|
+
store: "branding.logos",
|
|
725
|
+
component: "AttachInvoice",
|
|
726
|
+
field: "logoUrl",
|
|
727
|
+
annotation: "branding.logos",
|
|
728
|
+
},
|
|
700
729
|
]
|
|
701
730
|
expect(withStores.requiredStoreDeclarations)->toEqual(Some(expected))
|
|
702
731
|
})
|
|
@@ -718,7 +747,7 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
718
747
|
let entry =
|
|
719
748
|
manifest.capabilities->Array.find(c => c.key == "TestPlugin.documents")->Option.getOrThrow
|
|
720
749
|
let expected: array<Reventless.CapabilityManifest.provenance> = [
|
|
721
|
-
{component: "AttachInvoice", field: "documentUrl"},
|
|
750
|
+
{component: "AttachInvoice", field: "documentUrl", annotation: "documents"},
|
|
722
751
|
]
|
|
723
752
|
expect(entry.declaredBy)->toEqual(expected)
|
|
724
753
|
})
|
|
@@ -738,7 +767,8 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
738
767
|
"declaredBy": [
|
|
739
768
|
{
|
|
740
769
|
"component": "AttachInvoice",
|
|
741
|
-
"field": "documentUrl"
|
|
770
|
+
"field": "documentUrl",
|
|
771
|
+
"annotation": "documents"
|
|
742
772
|
}
|
|
743
773
|
]
|
|
744
774
|
},
|
|
@@ -748,7 +778,8 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
748
778
|
"declaredBy": [
|
|
749
779
|
{
|
|
750
780
|
"component": "AttachInvoice",
|
|
751
|
-
"field": "logoUrl"
|
|
781
|
+
"field": "logoUrl",
|
|
782
|
+
"annotation": "branding.logos"
|
|
752
783
|
}
|
|
753
784
|
]
|
|
754
785
|
}
|
|
@@ -760,6 +791,76 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
760
791
|
})
|
|
761
792
|
})
|
|
762
793
|
|
|
794
|
+
// Making a field optional says the value may be absent. It does not say the
|
|
795
|
+
// field stopped being a storage ref or a reference — but both walks read the
|
|
796
|
+
// marker through `Semantic.get`, and an optional field's marker sits on the
|
|
797
|
+
// schema inside the wrapper sury-ppx builds. Until that unwrap existed, adding
|
|
798
|
+
// a `?` to a field silently un-declared its store: nothing failed, an S3
|
|
799
|
+
// bucket just stopped being provisioned.
|
|
800
|
+
describe("optional carriers keep their declarations", () => {
|
|
801
|
+
let withOptional = Plugin_Structure.make(
|
|
802
|
+
~name="TestPlugin",
|
|
803
|
+
~stateChangeSlices=[module(PsUploadAvatarSlice)],
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
testSync("an optional storage ref still requires its store", () =>
|
|
807
|
+
expect(withOptional.requiredStores)->toEqual(Some(["TestPlugin.avatars"]))
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
// Declared on the command and on the event under one field name — the same
|
|
811
|
+
// collapse the required case makes, so optionality does not change arity.
|
|
812
|
+
testSync("and still carries one declaring site", () => {
|
|
813
|
+
let expected: array<Reventless.Plugin.requiredStoreDeclaration> = [
|
|
814
|
+
{
|
|
815
|
+
store: "TestPlugin.avatars",
|
|
816
|
+
component: "UploadAvatar",
|
|
817
|
+
field: "avatarUrl",
|
|
818
|
+
annotation: "avatars",
|
|
819
|
+
},
|
|
820
|
+
]
|
|
821
|
+
expect(withOptional.requiredStoreDeclarations)->toEqual(Some(expected))
|
|
822
|
+
})
|
|
823
|
+
|
|
824
|
+
testSync("an optional reference is still collected", () => {
|
|
825
|
+
let cmd = (withOptional.stateChangeSlices->Array.getUnsafe(0)).commands->Array.getUnsafe(0)
|
|
826
|
+
let expected: array<Reventless.Plugin.fieldReference> = [
|
|
827
|
+
{fieldName: "referredBy", entity: "Customers", plugin: None},
|
|
828
|
+
]
|
|
829
|
+
expect(cmd.references)->toEqual(expected)
|
|
830
|
+
})
|
|
831
|
+
|
|
832
|
+
// What `requiredStores` knows, the wire has to carry. A reader deciding
|
|
833
|
+
// which command fills a storage-ref field, or which store's endpoint an
|
|
834
|
+
// upload goes to, has only the command schema to go on.
|
|
835
|
+
testSync("the command schema carries the field's storage-ref marker", () => {
|
|
836
|
+
let cmd = (withOptional.stateChangeSlices->Array.getUnsafe(0)).commands->Array.getUnsafe(0)
|
|
837
|
+
let target =
|
|
838
|
+
cmd.schema
|
|
839
|
+
->JSON.parseOrThrow
|
|
840
|
+
->getPropertyOf("avatarUrl")
|
|
841
|
+
->Option.flatMap(s => getProperty(s, "x-reventless-semantic-target"))
|
|
842
|
+
->Option.flatMap(t => getProperty(t, "store"))
|
|
843
|
+
->Option.flatMap(JSON.Decode.string)
|
|
844
|
+
expect(target)->toBe(Some("avatars"))
|
|
845
|
+
})
|
|
846
|
+
|
|
847
|
+
// A form submits what a schema says is required. `avatarUrl?` is optional in
|
|
848
|
+
// the spec, so listing it would make the picture mandatory in every UI that
|
|
849
|
+
// renders the command — the failure this pairs with, since carrying the
|
|
850
|
+
// marker is worth nothing if the field cannot be left empty.
|
|
851
|
+
testSync("an optional command argument is not required", () => {
|
|
852
|
+
let cmd = (withOptional.stateChangeSlices->Array.getUnsafe(0)).commands->Array.getUnsafe(0)
|
|
853
|
+
let required =
|
|
854
|
+
cmd.schema
|
|
855
|
+
->JSON.parseOrThrow
|
|
856
|
+
->getProperty("required")
|
|
857
|
+
->Option.flatMap(JSON.Decode.array)
|
|
858
|
+
->Option.getOr([])
|
|
859
|
+
->Array.filterMap(JSON.Decode.string)
|
|
860
|
+
expect(required)->toEqual(["customerId"])
|
|
861
|
+
})
|
|
862
|
+
})
|
|
863
|
+
|
|
763
864
|
// Declarations are authoritative; the name heuristics are a lint. A
|
|
764
865
|
// heuristic-only match warns and never reaches the manifest, so nothing is
|
|
765
866
|
// provisioned from a guess — `imageUrl` is genuinely ambiguous between an
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as S from "sury/src/S.res.mjs";
|
|
4
4
|
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
5
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
5
6
|
import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
|
|
6
7
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
8
|
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
@@ -13,6 +14,7 @@ import * as PsOrdersView$ReventlessCore from "./StateViewSlice/PsOrdersView.res.
|
|
|
13
14
|
import * as PsPlaceOrder$ReventlessCore from "./StateChangeSlice/PsPlaceOrder.res.mjs";
|
|
14
15
|
import * as PsChangePhoto$ReventlessCore from "./StateChangeSlice/PsChangePhoto.res.mjs";
|
|
15
16
|
import * as CapabilityManifest$Reventless from "@reventlessdev/reventless-spec/src/components/CapabilityManifest.res.mjs";
|
|
17
|
+
import * as PsUploadAvatar$ReventlessCore from "./StateChangeSlice/PsUploadAvatar.res.mjs";
|
|
16
18
|
import * as PsAnnotatedView$ReventlessCore from "./StateViewSlice/PsAnnotatedView.res.mjs";
|
|
17
19
|
import * as PsAttachInvoice$ReventlessCore from "./StateChangeSlice/PsAttachInvoice.res.mjs";
|
|
18
20
|
import * as PsCustomersView$ReventlessCore from "./StateViewSlice/PsCustomersView.res.mjs";
|
|
@@ -215,13 +217,43 @@ let PsAttachInvoiceSlice = {
|
|
|
215
217
|
};
|
|
216
218
|
|
|
217
219
|
let Behavior$3 = {
|
|
220
|
+
initialState: false,
|
|
221
|
+
evolve: PsUploadAvatar$ReventlessCore.evolve,
|
|
222
|
+
decide: PsUploadAvatar$ReventlessCore.decide,
|
|
223
|
+
moduleUrl: PsUploadAvatar$ReventlessCore.moduleUrl
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
function make$7(param, param$1, param$2, param$3, param$4, param$5) {
|
|
227
|
+
return 0;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let PsUploadAvatarSlice_Spec = {
|
|
231
|
+
name: PsUploadAvatar$ReventlessCore.name,
|
|
232
|
+
moduleUrl: PsUploadAvatar$ReventlessCore.moduleUrl,
|
|
233
|
+
Id: Id$Reventless.$$String,
|
|
234
|
+
consumedEventSchema: PsUploadAvatar$ReventlessCore.consumedEventSchema,
|
|
235
|
+
errorSchema: PsUploadAvatar$ReventlessCore.errorSchema,
|
|
236
|
+
eventSchema: PsUploadAvatar$ReventlessCore.eventSchema,
|
|
237
|
+
commandSchema: PsUploadAvatar$ReventlessCore.commandSchema,
|
|
238
|
+
commandAuthorization: PsUploadAvatar$ReventlessCore.commandAuthorization,
|
|
239
|
+
readConsistency: PsUploadAvatar$ReventlessCore.readConsistency
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
let PsUploadAvatarSlice = {
|
|
243
|
+
Spec: PsUploadAvatarSlice_Spec,
|
|
244
|
+
Behavior: Behavior$3,
|
|
245
|
+
isAsync: false,
|
|
246
|
+
make: make$7
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
let Behavior$4 = {
|
|
218
250
|
initialState: false,
|
|
219
251
|
evolve: PsChangePhoto$ReventlessCore.evolve,
|
|
220
252
|
decide: PsChangePhoto$ReventlessCore.decide,
|
|
221
253
|
moduleUrl: PsChangePhoto$ReventlessCore.moduleUrl
|
|
222
254
|
};
|
|
223
255
|
|
|
224
|
-
function make$
|
|
256
|
+
function make$8(param, param$1, param$2, param$3, param$4, param$5) {
|
|
225
257
|
return 0;
|
|
226
258
|
}
|
|
227
259
|
|
|
@@ -239,9 +271,9 @@ let PsChangePhotoSlice_Spec = {
|
|
|
239
271
|
|
|
240
272
|
let PsChangePhotoSlice = {
|
|
241
273
|
Spec: PsChangePhotoSlice_Spec,
|
|
242
|
-
Behavior: Behavior$
|
|
274
|
+
Behavior: Behavior$4,
|
|
243
275
|
isAsync: false,
|
|
244
|
-
make: make$
|
|
276
|
+
make: make$8
|
|
245
277
|
};
|
|
246
278
|
|
|
247
279
|
let structure = Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, [
|
|
@@ -645,23 +677,23 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
645
677
|
globalThis.expect(Plugin_Structure$ReventlessCore.statusFieldFromStateSchema("Test", schema)).toEqual(undefined);
|
|
646
678
|
});
|
|
647
679
|
});
|
|
680
|
+
let getProperty = (json, key) => {
|
|
681
|
+
let obj = Stdlib_JSON.Decode.object(json);
|
|
682
|
+
if (obj !== undefined) {
|
|
683
|
+
return obj[key];
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
let getPropertyOf = (json, fieldName) => {
|
|
687
|
+
let props = getProperty(json, "properties");
|
|
688
|
+
if (props === undefined) {
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
let obj = Stdlib_JSON.Decode.object(props);
|
|
692
|
+
if (obj !== undefined) {
|
|
693
|
+
return obj[fieldName];
|
|
694
|
+
}
|
|
695
|
+
};
|
|
648
696
|
globalThis.describe("queryableDef.schema propagates x-reventless-* annotations", () => {
|
|
649
|
-
let getProperty = (json, key) => {
|
|
650
|
-
let obj = Stdlib_JSON.Decode.object(json);
|
|
651
|
-
if (obj !== undefined) {
|
|
652
|
-
return obj[key];
|
|
653
|
-
}
|
|
654
|
-
};
|
|
655
|
-
let getPropertyOf = (json, fieldName) => {
|
|
656
|
-
let props = getProperty(json, "properties");
|
|
657
|
-
if (props === undefined) {
|
|
658
|
-
return;
|
|
659
|
-
}
|
|
660
|
-
let obj = Stdlib_JSON.Decode.object(props);
|
|
661
|
-
if (obj !== undefined) {
|
|
662
|
-
return obj[fieldName];
|
|
663
|
-
}
|
|
664
|
-
};
|
|
665
697
|
let annotatedSchema = JSON.parse(structure.stateViewSlices[3].schema);
|
|
666
698
|
globalThis.test("itemId carries x-reventless-id", () => {
|
|
667
699
|
globalThis.expect(Stdlib_Option.flatMap(Stdlib_Option.flatMap(getPropertyOf(annotatedSchema, "itemId"), s => getProperty(s, "x-reventless-id")), Stdlib_JSON.Decode.bool)).toBe(true);
|
|
@@ -757,12 +789,14 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
757
789
|
{
|
|
758
790
|
store: "TestPlugin.documents",
|
|
759
791
|
component: "AttachInvoice",
|
|
760
|
-
field: "documentUrl"
|
|
792
|
+
field: "documentUrl",
|
|
793
|
+
annotation: "documents"
|
|
761
794
|
},
|
|
762
795
|
{
|
|
763
796
|
store: "branding.logos",
|
|
764
797
|
component: "AttachInvoice",
|
|
765
|
-
field: "logoUrl"
|
|
798
|
+
field: "logoUrl",
|
|
799
|
+
annotation: "branding.logos"
|
|
766
800
|
}
|
|
767
801
|
];
|
|
768
802
|
globalThis.expect(withStores.requiredStoreDeclarations).toEqual(expected);
|
|
@@ -776,7 +810,8 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
776
810
|
let entry = Stdlib_Option.getOrThrow(manifest.capabilities.find(c => c.key === "TestPlugin.documents"), undefined);
|
|
777
811
|
let expected = [{
|
|
778
812
|
component: "AttachInvoice",
|
|
779
|
-
field: "documentUrl"
|
|
813
|
+
field: "documentUrl",
|
|
814
|
+
annotation: "documents"
|
|
780
815
|
}];
|
|
781
816
|
globalThis.expect(entry.declaredBy).toEqual(expected);
|
|
782
817
|
});
|
|
@@ -793,7 +828,8 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
793
828
|
"declaredBy": [
|
|
794
829
|
{
|
|
795
830
|
"component": "AttachInvoice",
|
|
796
|
-
"field": "documentUrl"
|
|
831
|
+
"field": "documentUrl",
|
|
832
|
+
"annotation": "documents"
|
|
797
833
|
}
|
|
798
834
|
]
|
|
799
835
|
},
|
|
@@ -803,7 +839,8 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
803
839
|
"declaredBy": [
|
|
804
840
|
{
|
|
805
841
|
"component": "AttachInvoice",
|
|
806
|
-
"field": "logoUrl"
|
|
842
|
+
"field": "logoUrl",
|
|
843
|
+
"annotation": "branding.logos"
|
|
807
844
|
}
|
|
808
845
|
]
|
|
809
846
|
}
|
|
@@ -813,6 +850,40 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
813
850
|
});
|
|
814
851
|
});
|
|
815
852
|
});
|
|
853
|
+
globalThis.describe("optional carriers keep their declarations", () => {
|
|
854
|
+
let withOptional = Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, undefined, [PsUploadAvatarSlice], undefined, undefined, undefined, undefined, undefined, undefined);
|
|
855
|
+
globalThis.test("an optional storage ref still requires its store", () => {
|
|
856
|
+
globalThis.expect(withOptional.requiredStores).toEqual(["TestPlugin.avatars"]);
|
|
857
|
+
});
|
|
858
|
+
globalThis.test("and still carries one declaring site", () => {
|
|
859
|
+
let expected = [{
|
|
860
|
+
store: "TestPlugin.avatars",
|
|
861
|
+
component: "UploadAvatar",
|
|
862
|
+
field: "avatarUrl",
|
|
863
|
+
annotation: "avatars"
|
|
864
|
+
}];
|
|
865
|
+
globalThis.expect(withOptional.requiredStoreDeclarations).toEqual(expected);
|
|
866
|
+
});
|
|
867
|
+
globalThis.test("an optional reference is still collected", () => {
|
|
868
|
+
let cmd = withOptional.stateChangeSlices[0].commands[0];
|
|
869
|
+
let expected = [{
|
|
870
|
+
fieldName: "referredBy",
|
|
871
|
+
entity: "Customers",
|
|
872
|
+
plugin: undefined
|
|
873
|
+
}];
|
|
874
|
+
globalThis.expect(cmd.references).toEqual(expected);
|
|
875
|
+
});
|
|
876
|
+
globalThis.test("the command schema carries the field's storage-ref marker", () => {
|
|
877
|
+
let cmd = withOptional.stateChangeSlices[0].commands[0];
|
|
878
|
+
let target = Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(getPropertyOf(JSON.parse(cmd.schema), "avatarUrl"), s => getProperty(s, "x-reventless-semantic-target")), t => getProperty(t, "store")), Stdlib_JSON.Decode.string);
|
|
879
|
+
globalThis.expect(target).toBe("avatars");
|
|
880
|
+
});
|
|
881
|
+
globalThis.test("an optional command argument is not required", () => {
|
|
882
|
+
let cmd = withOptional.stateChangeSlices[0].commands[0];
|
|
883
|
+
let required = Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_Option.flatMap(getProperty(JSON.parse(cmd.schema), "required"), Stdlib_JSON.Decode.array), []), Stdlib_JSON.Decode.string);
|
|
884
|
+
globalThis.expect(required).toEqual(["customerId"]);
|
|
885
|
+
});
|
|
886
|
+
});
|
|
816
887
|
globalThis.describe("capability inference", () => {
|
|
817
888
|
let commandWarnings = Capability_Inference$ReventlessCore.scanSchema("ChangePhoto", PsChangePhoto$ReventlessCore.commandSchema);
|
|
818
889
|
let eventWarnings = Capability_Inference$ReventlessCore.scanSchema("ChangePhoto", PsChangePhoto$ReventlessCore.eventSchema);
|
|
@@ -867,6 +938,7 @@ export {
|
|
|
867
938
|
PsCustomersViewSlice,
|
|
868
939
|
PsAnnotatedViewSlice,
|
|
869
940
|
PsAttachInvoiceSlice,
|
|
941
|
+
PsUploadAvatarSlice,
|
|
870
942
|
PsChangePhotoSlice,
|
|
871
943
|
structure,
|
|
872
944
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Test fixture spec where every semantic carrier is OPTIONAL.
|
|
2
|
+
//
|
|
3
|
+
// The marker of an optional field sits on the schema *inside* the
|
|
4
|
+
// undefined-union sury-ppx wraps it in, so a walk that reads only the outer
|
|
5
|
+
// schema finds nothing. That failure is silent in the worst way: the plugin
|
|
6
|
+
// compiles, deploys green, and provisions no object store — so `avatarUrl` is
|
|
7
|
+
// the only declarer of `avatars` here on purpose. If the unwrap regresses, the
|
|
8
|
+
// store requirement disappears entirely rather than merely losing a site.
|
|
9
|
+
//
|
|
10
|
+
// `referredBy` is the same trap for `@ref`: an optional reference that goes
|
|
11
|
+
// uncollected drops out of `commandDef.references`.
|
|
12
|
+
|
|
13
|
+
@@reventless.spec("UploadAvatar")
|
|
14
|
+
|
|
15
|
+
type state = bool
|
|
16
|
+
let initialState = false
|
|
17
|
+
|
|
18
|
+
@schema
|
|
19
|
+
type consumedEvent = OrderPlaced
|
|
20
|
+
|
|
21
|
+
let evolve = (_state, _event) => true
|
|
22
|
+
|
|
23
|
+
@schema
|
|
24
|
+
type command =
|
|
25
|
+
| UploadAvatar({
|
|
26
|
+
customerId: string,
|
|
27
|
+
@storageRef("avatars") avatarUrl?: string,
|
|
28
|
+
@ref("Customers") referredBy?: string,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
@schema
|
|
32
|
+
type error = CustomerNotFound
|
|
33
|
+
|
|
34
|
+
@schema
|
|
35
|
+
type event = AvatarUploaded({customerId: string, @storageRef("avatars") avatarUrl?: string})
|
|
36
|
+
|
|
37
|
+
let decide = (_state, _command): result<array<event>, error> => Ok([])
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
5
|
+
import * as Reference$Reventless from "@reventlessdev/reventless-spec/src/components/Reference.res.mjs";
|
|
6
|
+
import * as StorageRef$Reventless from "@reventlessdev/reventless-spec/src/semantic/StorageRef.res.mjs";
|
|
7
|
+
|
|
8
|
+
let consumedEventSchema = S.literal("OrderPlaced");
|
|
9
|
+
|
|
10
|
+
function evolve(_state, _event) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let commandSchema = S.schema(s => ({
|
|
15
|
+
TAG: "UploadAvatar",
|
|
16
|
+
customerId: s.m(DcbTag$Reventless.string),
|
|
17
|
+
avatarUrl: s.m(S.option(StorageRef$Reventless.forStore(undefined, "avatars"))),
|
|
18
|
+
referredBy: s.m(S.option(Reference$Reventless.to_(undefined, undefined, "Customers")))
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
let errorSchema = S.literal("CustomerNotFound");
|
|
22
|
+
|
|
23
|
+
let eventSchema = S.schema(s => ({
|
|
24
|
+
TAG: "AvatarUploaded",
|
|
25
|
+
customerId: s.m(DcbTag$Reventless.string),
|
|
26
|
+
avatarUrl: s.m(S.option(StorageRef$Reventless.forStore(undefined, "avatars")))
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
function decide(_state, _command) {
|
|
30
|
+
return {
|
|
31
|
+
TAG: "Ok",
|
|
32
|
+
_0: []
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function commandAuthorization(param) {
|
|
37
|
+
return "AllowAuthenticated";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let name = "UploadAvatar";
|
|
41
|
+
|
|
42
|
+
let Id;
|
|
43
|
+
|
|
44
|
+
let initialState = false;
|
|
45
|
+
|
|
46
|
+
let moduleUrl = "@reventlessdev/reventless-core/tests/plugin/StateChangeSlice/PsUploadAvatar.res.mjs";
|
|
47
|
+
|
|
48
|
+
let readConsistency = "EscalateOnRetry";
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
name,
|
|
52
|
+
Id,
|
|
53
|
+
initialState,
|
|
54
|
+
consumedEventSchema,
|
|
55
|
+
evolve,
|
|
56
|
+
commandSchema,
|
|
57
|
+
errorSchema,
|
|
58
|
+
eventSchema,
|
|
59
|
+
decide,
|
|
60
|
+
moduleUrl,
|
|
61
|
+
commandAuthorization,
|
|
62
|
+
readConsistency,
|
|
63
|
+
}
|
|
64
|
+
/* consumedEventSchema Not a pure module */
|