@reventlessdev/reventless-core 3.0.0-alpha.259 → 3.0.0-alpha.260

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 CHANGED
@@ -3,6 +3,13 @@
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.260 (2026-09-08)
7
+
8
+ ### Features
9
+
10
+ * **spec:** a field declares the value a form opens it on ([1d541fe](https://github.com/ReventlessDev/reventless-core/commit/1d541fe497e1df2ff48b72e26a06a1b8fd3c9e06))
11
+
12
+
6
13
  # 3.0.0-alpha.259 (2026-09-08)
7
14
 
8
15
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-core",
3
- "version": "3.0.0-alpha.259",
3
+ "version": "3.0.0-alpha.260",
4
4
  "description": "Core package for Reventless framework",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -29,21 +29,21 @@
29
29
  "sury": "11.0.0-rc.2",
30
30
  "uuid": "^13.0.0",
31
31
  "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
32
- "@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
33
- "@reventlessdev/rescript-node": "2.0.0-alpha.9",
34
- "@reventlessdev/rescript-fast-csv": "2.0.0-alpha.9",
32
+ "@reventlessdev/rescript-fast-csv": "2.0.0-alpha.10",
35
33
  "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
34
+ "@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
35
+ "@reventlessdev/rescript-node": "2.0.0-alpha.10",
36
36
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
37
- "@reventlessdev/rescript-ssh2": "2.0.0-alpha.9",
37
+ "@reventlessdev/rescript-ssh2": "2.0.0-alpha.10",
38
38
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
39
- "@reventlessdev/reventless-infra": "3.0.0-alpha.160",
39
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.161",
40
40
  "@reventlessdev/reventless-interop": "3.0.0-alpha.35",
41
- "@reventlessdev/reventless-spec": "3.0.0-alpha.132"
41
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.133"
42
42
  },
43
43
  "devDependencies": {
44
44
  "rescript": "12.3.0",
45
45
  "sury-ppx": "11.0.0-rc.2",
46
- "@reventlessdev/reventless-ppx": "1.0.0-alpha.78"
46
+ "@reventlessdev/reventless-ppx": "1.0.0-alpha.80"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "rescript": "12.3.0"
@@ -89,6 +89,9 @@ let rec fromSchemaType = (
89
89
  // reaches the UI through the field's JSON Schema, which is the channel that
90
90
  // can express it.
91
91
  | Semantic(_, inner) => fromSchemaType(~required, ~asInput, inner, collectedTypes, seenTypes)
92
+ // The SDL type is the wrapped shape's. A GraphQL default value here instead
93
+ // would make the field optional on the wire — a different claim.
94
+ | Defaulted(_, inner) => fromSchemaType(~required, ~asInput, inner, collectedTypes, seenTypes)
92
95
  // A union declares one object type per arm and a `union` naming them. Both
93
96
  // names come from the schema — see `Reventless.TaggedUnion` — so the same union
94
97
  // is the same type wherever it appears, which is what lets AppSync merge each
@@ -292,7 +295,7 @@ let rec scalarOfSchemaType = (st: SchemaType.schemaType): string =>
292
295
  | ScalarBoolean => "Boolean"
293
296
  | ScalarBigInt => "String"
294
297
  | EntityId => "ID"
295
- | Semantic(_, inner) => scalarOfSchemaType(inner)
298
+ | Semantic(_, inner) | Defaulted(_, inner) => scalarOfSchemaType(inner)
296
299
  | _ => "String"
297
300
  }
298
301
 
@@ -99,6 +99,10 @@ function fromSchemaType(_required, _asInputOpt, _st, collectedTypes, seenTypes)
99
99
  _st = inner;
100
100
  _asInputOpt = asInput;
101
101
  continue;
102
+ case "Defaulted" :
103
+ _st = st._1;
104
+ _asInputOpt = asInput;
105
+ continue;
102
106
  case "TaggedUnion" :
103
107
  let name$3 = st._0;
104
108
  if (asInput) {
@@ -232,11 +236,14 @@ function scalarOfSchemaType(_st) {
232
236
  return "String";
233
237
  }
234
238
  } else {
235
- if (st.TAG !== "Semantic") {
236
- return "String";
239
+ switch (st.TAG) {
240
+ case "Semantic" :
241
+ case "Defaulted" :
242
+ _st = st._1;
243
+ continue;
244
+ default:
245
+ return "String";
237
246
  }
238
- _st = st._1;
239
- continue;
240
247
  }
241
248
  };
242
249
  }
@@ -27,6 +27,10 @@ type rec schemaType =
27
27
  are *not* expressed this way: they long predate the generic marker and
28
28
  their JSON Schema `format` output is a published contract. */
29
29
  Semantic(Reventless.Semantic.t, schemaType)
30
+ | /** A field whose type declares the value it opens with. Wraps the shape the
31
+ value has, as `Semantic` does, so a consumer that cares only about shape
32
+ unwraps: a default changes where a form starts, never what the field is. */
33
+ Defaulted(JSON.t, schemaType)
30
34
  | /** A variant used as a field: the union's name, and one arm per constructor
31
35
  keyed by the `TAG` sury discriminates on. Each arm is the `ObjectRef` its
32
36
  member type is emitted from, so the arm's own name travels with it and
@@ -82,6 +86,17 @@ let canonicalName = (id: string): option<string> =>
82
86
  ->Option.map(((_, name)) => name)
83
87
 
84
88
  let rec fromSury = (~parentName: string, ~fieldName: string, schema: S.t<unknown>): schemaType => {
89
+ // Outermost, so it wraps whatever the field turns out to be, a semantic
90
+ // composite included — the other order buries the marker under a shape whose
91
+ // consumers unwrap it.
92
+ let shape = withoutDefault(~parentName, ~fieldName, schema)
93
+ switch Reventless.FieldDefault.getFrom(schema) {
94
+ | Some(value) => Defaulted(value, shape)
95
+ | None => shape
96
+ }
97
+ }
98
+
99
+ and withoutDefault = (~parentName: string, ~fieldName: string, schema: S.t<unknown>): schemaType => {
85
100
  // Semantics the IR already has a dedicated shape for keep it: `dateTime` and
86
101
  // `reference` are read below via `isDateTime` / `getTarget`, both of which now
87
102
  // consult the generic marker, and their `format` output is a published
@@ -9,6 +9,7 @@ import * as Lifecycle$Reventless from "@reventlessdev/reventless-spec/src/types/
9
9
  import * as Reference$Reventless from "@reventlessdev/reventless-spec/src/components/Reference.res.mjs";
10
10
  import * as TaggedUnion$Reventless from "@reventlessdev/reventless-spec/src/components/TaggedUnion.res.mjs";
11
11
  import * as CalendarDate$Reventless from "@reventlessdev/reventless-spec/src/semantic/CalendarDate.res.mjs";
12
+ import * as FieldDefault$Reventless from "@reventlessdev/reventless-spec/src/components/FieldDefault.res.mjs";
12
13
 
13
14
  function isIdFieldName(name) {
14
15
  let lower = name.toLowerCase();
@@ -54,6 +55,20 @@ function canonicalName(id) {
54
55
  }
55
56
 
56
57
  function fromSury(parentName, fieldName, schema) {
58
+ let shape = withoutDefault(parentName, fieldName, schema);
59
+ let value = FieldDefault$Reventless.getFrom(schema);
60
+ if (value !== undefined) {
61
+ return {
62
+ TAG: "Defaulted",
63
+ _0: value,
64
+ _1: shape
65
+ };
66
+ } else {
67
+ return shape;
68
+ }
69
+ }
70
+
71
+ function withoutDefault(parentName, fieldName, schema) {
57
72
  let sem = Semantic$Reventless.get(schema);
58
73
  if (sem === undefined) {
59
74
  return shapeOf(parentName, fieldName, schema);
@@ -415,6 +430,7 @@ export {
415
430
  semanticCompositeNames,
416
431
  canonicalName,
417
432
  fromSury,
433
+ withoutDefault,
418
434
  shapeOf,
419
435
  maxReportDepth,
420
436
  collectUnclassifiedUnions,
@@ -234,6 +234,15 @@ let rec fromSchemaType = (~inputNames: bool=false, st: SchemaType.schemaType): J
234
234
  | _ => base
235
235
  }
236
236
  base->withSemantic(sem)
237
+ // JSON Schema's own keyword, not an `x-reventless-` extension: the vocabulary
238
+ // already has a word for this one, so any client reads it.
239
+ | Defaulted(value, inner) =>
240
+ switch fromSchemaType(~inputNames, inner)->JSON.Decode.object {
241
+ | Some(obj) =>
242
+ obj->Dict.set("default", value)
243
+ JSON.Encode.object(obj)
244
+ | None => fromSchemaType(~inputNames, inner)
245
+ }
237
246
  // A union has no input form — see the fragment generator, which emits `String`
238
247
  // and says so — hence no name to publish on either arm.
239
248
  // A union is `oneOf` its arms, each an object carrying the `TAG` const it is
@@ -170,130 +170,144 @@ function withGraphqlInput(schema, name) {
170
170
  }
171
171
  }
172
172
 
173
- function fromSchemaType(inputNamesOpt, st) {
174
- let inputNames = inputNamesOpt !== undefined ? inputNamesOpt : false;
175
- if (typeof st !== "object") {
176
- switch (st) {
177
- case "ScalarNumber" :
178
- return Object.fromEntries([[
179
- "type",
180
- "number"
181
- ]]);
182
- case "ScalarBoolean" :
183
- return Object.fromEntries([[
184
- "type",
185
- "boolean"
186
- ]]);
187
- case "ScalarInt" :
188
- case "ScalarBigInt" :
189
- return Object.fromEntries([[
190
- "type",
191
- "integer"
192
- ]]);
193
- case "EntityId" :
194
- return Object.fromEntries([
195
- [
196
- "type",
197
- "string"
198
- ],
199
- [
200
- "format",
201
- "uuid"
202
- ]
203
- ]);
204
- case "DateTime" :
205
- return Object.fromEntries([
206
- [
207
- "type",
208
- "string"
209
- ],
210
- [
211
- "format",
212
- "date-time"
213
- ]
214
- ]);
215
- case "CalendarDate" :
216
- return Object.fromEntries([
217
- [
218
- "type",
219
- "string"
220
- ],
221
- [
222
- "format",
223
- "date"
224
- ]
225
- ]);
226
- case "ScalarString" :
227
- case "Unknown" :
228
- return Object.fromEntries([[
229
- "type",
230
- "string"
231
- ]]);
232
- }
233
- } else {
234
- switch (st.TAG) {
235
- case "Nullable" :
236
- let innerSchema = fromSchemaType(inputNames, st._0);
237
- return Object.fromEntries([[
238
- "oneOf",
173
+ function fromSchemaType(_inputNamesOpt, _st) {
174
+ while (true) {
175
+ let inputNamesOpt = _inputNamesOpt;
176
+ let st = _st;
177
+ let inputNames = inputNamesOpt !== undefined ? inputNamesOpt : false;
178
+ if (typeof st !== "object") {
179
+ switch (st) {
180
+ case "ScalarNumber" :
181
+ return Object.fromEntries([[
182
+ "type",
183
+ "number"
184
+ ]]);
185
+ case "ScalarBoolean" :
186
+ return Object.fromEntries([[
187
+ "type",
188
+ "boolean"
189
+ ]]);
190
+ case "ScalarInt" :
191
+ case "ScalarBigInt" :
192
+ return Object.fromEntries([[
193
+ "type",
194
+ "integer"
195
+ ]]);
196
+ case "EntityId" :
197
+ return Object.fromEntries([
198
+ [
199
+ "type",
200
+ "string"
201
+ ],
239
202
  [
240
- innerSchema,
241
- Object.fromEntries([[
242
- "type",
243
- "null"
244
- ]])
203
+ "format",
204
+ "uuid"
245
205
  ]
246
- ]]);
247
- case "ArrayOf" :
248
- return Object.fromEntries([
249
- [
250
- "type",
251
- "array"
252
- ],
253
- [
254
- "items",
255
- fromSchemaType(inputNames, st._0)
256
- ]
257
- ]);
258
- case "ObjectRef" :
259
- let base = objectRefToJsonSchema(undefined, undefined, undefined, undefined, inputNames, st._1);
260
- if (inputNames) {
261
- return withGraphqlInput(base, st._0);
262
- } else {
263
- return base;
264
- }
265
- case "Enum" :
266
- return Object.fromEntries([
267
- [
268
- "type",
269
- "string"
270
- ],
271
- [
272
- "enum",
273
- st._1.map(prim => prim)
274
- ]
275
- ]);
276
- case "Semantic" :
277
- let inner = st._1;
278
- let sem = st._0;
279
- let base$1 = fromSchemaType(inputNames, inner);
280
- let match = SchemaType$ReventlessCore.canonicalName(sem.id);
281
- let base$2 = inputNames && match !== undefined && typeof inner === "object" && inner.TAG === "ObjectRef" ? withGraphqlInput(base$1, match + "Input") : base$1;
282
- return withSemantic(base$2, sem);
283
- case "TaggedUnion" :
284
- let members = st._1.map(param => armToJsonSchema(inputNames, param[0], param[1]));
285
- return Object.fromEntries([
286
- [
287
- "oneOf",
288
- members
289
- ],
290
- [
291
- "x-reventless-union",
292
- st._0
293
- ]
294
- ]);
206
+ ]);
207
+ case "DateTime" :
208
+ return Object.fromEntries([
209
+ [
210
+ "type",
211
+ "string"
212
+ ],
213
+ [
214
+ "format",
215
+ "date-time"
216
+ ]
217
+ ]);
218
+ case "CalendarDate" :
219
+ return Object.fromEntries([
220
+ [
221
+ "type",
222
+ "string"
223
+ ],
224
+ [
225
+ "format",
226
+ "date"
227
+ ]
228
+ ]);
229
+ case "ScalarString" :
230
+ case "Unknown" :
231
+ return Object.fromEntries([[
232
+ "type",
233
+ "string"
234
+ ]]);
235
+ }
236
+ } else {
237
+ switch (st.TAG) {
238
+ case "Nullable" :
239
+ let innerSchema = fromSchemaType(inputNames, st._0);
240
+ return Object.fromEntries([[
241
+ "oneOf",
242
+ [
243
+ innerSchema,
244
+ Object.fromEntries([[
245
+ "type",
246
+ "null"
247
+ ]])
248
+ ]
249
+ ]]);
250
+ case "ArrayOf" :
251
+ return Object.fromEntries([
252
+ [
253
+ "type",
254
+ "array"
255
+ ],
256
+ [
257
+ "items",
258
+ fromSchemaType(inputNames, st._0)
259
+ ]
260
+ ]);
261
+ case "ObjectRef" :
262
+ let base = objectRefToJsonSchema(undefined, undefined, undefined, undefined, inputNames, st._1);
263
+ if (inputNames) {
264
+ return withGraphqlInput(base, st._0);
265
+ } else {
266
+ return base;
267
+ }
268
+ case "Enum" :
269
+ return Object.fromEntries([
270
+ [
271
+ "type",
272
+ "string"
273
+ ],
274
+ [
275
+ "enum",
276
+ st._1.map(prim => prim)
277
+ ]
278
+ ]);
279
+ case "Semantic" :
280
+ let inner = st._1;
281
+ let sem = st._0;
282
+ let base$1 = fromSchemaType(inputNames, inner);
283
+ let match = SchemaType$ReventlessCore.canonicalName(sem.id);
284
+ let base$2 = inputNames && match !== undefined && typeof inner === "object" && inner.TAG === "ObjectRef" ? withGraphqlInput(base$1, match + "Input") : base$1;
285
+ return withSemantic(base$2, sem);
286
+ case "Defaulted" :
287
+ let inner$1 = st._1;
288
+ let obj = Stdlib_JSON.Decode.object(fromSchemaType(inputNames, inner$1));
289
+ if (obj !== undefined) {
290
+ obj["default"] = st._0;
291
+ return obj;
292
+ }
293
+ _st = inner$1;
294
+ _inputNamesOpt = inputNames;
295
+ continue;
296
+ case "TaggedUnion" :
297
+ let members = st._1.map(param => armToJsonSchema(inputNames, param[0], param[1]));
298
+ return Object.fromEntries([
299
+ [
300
+ "oneOf",
301
+ members
302
+ ],
303
+ [
304
+ "x-reventless-union",
305
+ st._0
306
+ ]
307
+ ]);
308
+ }
295
309
  }
296
- }
310
+ };
297
311
  }
298
312
 
299
313
  function armToJsonSchema(inputNamesOpt, tag, armType) {
@@ -1356,4 +1356,60 @@ describe("SuryToJsonSchema:", () => {
1356
1356
  )
1357
1357
  })
1358
1358
 
1359
+ // The value a field opens with is JSON Schema's own keyword, not an
1360
+ // `x-reventless-` extension — and it rides on the field's schema, so it
1361
+ // survives the one place a name-keyed annotation spec cannot reach: a record
1362
+ // nested inside a command.
1363
+ describe("a field declaring a default:", () => {
1364
+ let defaultOf = (json, ~field) =>
1365
+ getPropertyOf(json, field)->Option.flatMap(s => getProperty(s, "default"))
1366
+
1367
+ let json = SuryToJsonSchema.deriveObjectSchema(
1368
+ S.schema(s =>
1369
+ {
1370
+ "quantity": s.matches(S.int->Reventless.FieldDefault.int(1)),
1371
+ "shippingMethod": s.matches(S.string->Reventless.FieldDefault.string("Standard")),
1372
+ "plain": s.matches(S.int),
1373
+ }
1374
+ )->S.castToUnknown,
1375
+ )
1376
+
1377
+ testSync("carries it as `default`", () =>
1378
+ expect(defaultOf(json, ~field="quantity"))->toEqual(Some(JSON.Encode.int(1)))
1379
+ )
1380
+
1381
+ testSync("keeps the shape the value actually has", () =>
1382
+ expect(
1383
+ getPropertyOf(json, "quantity")->Option.flatMap(s => getProperty(s, "type")),
1384
+ )->toEqual(Some(JSON.Encode.string("integer")))
1385
+ )
1386
+
1387
+ testSync("carries a string default the same way", () =>
1388
+ expect(defaultOf(json, ~field="shippingMethod"))
1389
+ ->toEqual(Some(JSON.Encode.string("Standard")))
1390
+ )
1391
+
1392
+ testSync("while a field declaring none says nothing", () =>
1393
+ expect(defaultOf(json, ~field="plain"))->toBe(None)
1394
+ )
1395
+
1396
+ // The case the whole marker exists for: a name-keyed annotation spec speaks
1397
+ // for a record's own fields, and an order's line items are one level in.
1398
+ testSync("and reaches a field of a nested record", () => {
1399
+ let lineItem = S.schema(s =>
1400
+ {
1401
+ "productId": s.matches(S.string),
1402
+ "quantity": s.matches(S.int->Reventless.FieldDefault.int(1)),
1403
+ }
1404
+ )
1405
+ let nested = SuryToJsonSchema.deriveObjectSchema(
1406
+ S.schema(s => {"lineItems": s.matches(S.array(lineItem))})->S.castToUnknown,
1407
+ )
1408
+ expect(
1409
+ getPropertyOf(nested, "lineItems")
1410
+ ->Option.flatMap(a => getProperty(a, "items"))
1411
+ ->Option.flatMap(i => defaultOf(i, ~field="quantity")),
1412
+ )->toEqual(Some(JSON.Encode.int(1)))
1413
+ })
1414
+ })
1359
1415
  })
@@ -20,6 +20,7 @@ import * as Reference$Reventless from "@reventlessdev/reventless-spec/src/compon
20
20
  import * as Sensitive$Reventless from "@reventlessdev/reventless-spec/src/components/Sensitive.res.mjs";
21
21
  import * as StorageRef$Reventless from "@reventlessdev/reventless-spec/src/semantic/StorageRef.res.mjs";
22
22
  import * as CalendarDate$Reventless from "@reventlessdev/reventless-spec/src/semantic/CalendarDate.res.mjs";
23
+ import * as FieldDefault$Reventless from "@reventlessdev/reventless-spec/src/components/FieldDefault.res.mjs";
23
24
  import * as StateAnnotations$Reventless from "@reventlessdev/reventless-spec/src/components/StateAnnotations.res.mjs";
24
25
  import * as SuryToJsonSchema$ReventlessCore from "../../src/components/Api/SuryToJsonSchema.res.mjs";
25
26
 
@@ -1163,6 +1164,36 @@ globalThis.describe("SuryToJsonSchema:", () => {
1163
1164
  globalThis.expect(nestedRequired.includes("street")).toBe(true);
1164
1165
  });
1165
1166
  });
1167
+ globalThis.describe("a field declaring a default:", () => {
1168
+ let defaultOf = (json, field) => Stdlib_Option.flatMap(getPropertyOf(json, field), s => getProperty(s, "default"));
1169
+ let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(undefined, undefined, Sury.$schema(s => ({
1170
+ quantity: s.m(FieldDefault$Reventless.int(Sury.int, 1)),
1171
+ shippingMethod: s.m(FieldDefault$Reventless.string(Sury.string, "Standard")),
1172
+ plain: s.m(Sury.int)
1173
+ })));
1174
+ globalThis.test("carries it as `default`", () => {
1175
+ globalThis.expect(defaultOf(json, "quantity")).toEqual(1);
1176
+ });
1177
+ globalThis.test("keeps the shape the value actually has", () => {
1178
+ globalThis.expect(Stdlib_Option.flatMap(getPropertyOf(json, "quantity"), s => getProperty(s, "type"))).toEqual("integer");
1179
+ });
1180
+ globalThis.test("carries a string default the same way", () => {
1181
+ globalThis.expect(defaultOf(json, "shippingMethod")).toEqual("Standard");
1182
+ });
1183
+ globalThis.test("while a field declaring none says nothing", () => {
1184
+ globalThis.expect(defaultOf(json, "plain")).toBe(undefined);
1185
+ });
1186
+ globalThis.test("and reaches a field of a nested record", () => {
1187
+ let lineItem = Sury.$schema(s => ({
1188
+ productId: s.m(Sury.string),
1189
+ quantity: s.m(FieldDefault$Reventless.int(Sury.int, 1))
1190
+ }));
1191
+ let nested = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(undefined, undefined, Sury.$schema(s => ({
1192
+ lineItems: s.m(Sury.array(lineItem))
1193
+ })));
1194
+ globalThis.expect(Stdlib_Option.flatMap(Stdlib_Option.flatMap(getPropertyOf(nested, "lineItems"), a => getProperty(a, "items")), i => defaultOf(i, "quantity"))).toEqual(1);
1195
+ });
1196
+ });
1166
1197
  });
1167
1198
 
1168
1199
  export {
@@ -1458,6 +1458,25 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1458
1458
  )->toEqual(["contact"])
1459
1459
  )
1460
1460
 
1461
+ // JSON Schema's own keyword rather than an `x-reventless-` one, so a client
1462
+ // that knows nothing about Reventless reads it.
1463
+ testSync("@default flows through the PPX to JSON Schema's `default`", () => {
1464
+ expect(
1465
+ annotatedSchema
1466
+ ->getPropertyOf("quantity")
1467
+ ->Option.flatMap(s => getProperty(s, "default")),
1468
+ )->toEqual(Some(JSON.Encode.int(1)))
1469
+ })
1470
+
1471
+ testSync("@default leaves the field's own shape alone", () => {
1472
+ expect(
1473
+ annotatedSchema
1474
+ ->getPropertyOf("quantity")
1475
+ ->Option.flatMap(s => getProperty(s, "type"))
1476
+ ->Option.flatMap(JSON.Decode.string),
1477
+ )->toBe(Some("integer"))
1478
+ })
1479
+
1461
1480
  testSync("@metric flows through the PPX to x-reventless-metric {aggregate,label}", () => {
1462
1481
  let metricObj =
1463
1482
  annotatedSchema
@@ -1555,6 +1555,12 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1555
1555
  globalThis.test("@sensitive reaches an event variant's payload, not only state", () => {
1556
1556
  globalThis.expect(Sensitive$Reventless.variantFieldNames(PsAnnotatedView$ReventlessCore.consumedEventSchema, "ItemRecorded")).toEqual(["contact"]);
1557
1557
  });
1558
+ globalThis.test("@default flows through the PPX to JSON Schema's `default`", () => {
1559
+ globalThis.expect(Stdlib_Option.flatMap(getPropertyOf(annotatedSchema, "quantity"), s => getProperty(s, "default"))).toEqual(1);
1560
+ });
1561
+ globalThis.test("@default leaves the field's own shape alone", () => {
1562
+ globalThis.expect(Stdlib_Option.flatMap(Stdlib_Option.flatMap(getPropertyOf(annotatedSchema, "quantity"), s => getProperty(s, "type")), Stdlib_JSON.Decode.string)).toBe("integer");
1563
+ });
1558
1564
  globalThis.test("@metric flows through the PPX to x-reventless-metric {aggregate,label}", () => {
1559
1565
  let metricObj = Stdlib_Option.flatMap(getPropertyOf(annotatedSchema, "total"), s => getProperty(s, "x-reventless-metric"));
1560
1566
  globalThis.expect([
@@ -16,6 +16,7 @@ type consumedEvent =
16
16
  // On the event, not just the state: a renderer composes from an event
17
17
  // payload, so this is the case the marker has to reach.
18
18
  @sensitive contact: string,
19
+ @default(1) quantity: int,
19
20
  })
20
21
 
21
22
  @live(false)
@@ -27,10 +28,11 @@ type state = {
27
28
  name: string,
28
29
  @semantic("currency") @metric({aggregate: "sum", label: "Revenue"}) total: float,
29
30
  @sensitive contact: string,
31
+ @default(1) quantity: int,
30
32
  }
31
33
 
32
34
  let project = ({event}: Reventless.StateViewSlice.consumed<consumedEvent>) =>
33
35
  switch event {
34
- | ItemRecorded({itemId, ownerId, version, name, total, contact}) =>
35
- [Set(itemId, {itemId, ownerId, version, name, total, contact})]
36
+ | ItemRecorded({itemId, ownerId, version, name, total, contact, quantity}) =>
37
+ [Set(itemId, {itemId, ownerId, version, name, total, contact, quantity})]
36
38
  }
@@ -4,6 +4,7 @@ import * as Sury from "sury";
4
4
  import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
5
5
  import * as ReadModel$Reventless from "@reventlessdev/reventless-spec/src/components/ReadModel.res.mjs";
6
6
  import * as Sensitive$Reventless from "@reventlessdev/reventless-spec/src/components/Sensitive.res.mjs";
7
+ import * as FieldDefault$Reventless from "@reventlessdev/reventless-spec/src/components/FieldDefault.res.mjs";
7
8
  import * as StateAnnotations$Reventless from "@reventlessdev/reventless-spec/src/components/StateAnnotations.res.mjs";
8
9
 
9
10
  let consumedEventSchema = Sury.$schema(s => ({
@@ -13,7 +14,8 @@ let consumedEventSchema = Sury.$schema(s => ({
13
14
  version: s.m(Sury.string),
14
15
  name: s.m(Sury.string),
15
16
  total: s.m(Sury.float),
16
- contact: s.m(Sensitive$Reventless.string)
17
+ contact: s.m(Sensitive$Reventless.string),
18
+ quantity: s.m(FieldDefault$Reventless.int(Sury.int, 1))
17
19
  }));
18
20
 
19
21
  let stateSchema = Sury.$schema(s => ({
@@ -22,7 +24,8 @@ let stateSchema = Sury.$schema(s => ({
22
24
  ownerId: s.m(Sury.string),
23
25
  name: s.m(Sury.string),
24
26
  total: s.m(Sury.float),
25
- contact: s.m(Sensitive$Reventless.string)
27
+ contact: s.m(Sensitive$Reventless.string),
28
+ quantity: s.m(FieldDefault$Reventless.int(Sury.int, 1))
26
29
  }));
27
30
 
28
31
  function project(param) {
@@ -37,7 +40,8 @@ function project(param) {
37
40
  ownerId: event.ownerId,
38
41
  name: event.name,
39
42
  total: event.total,
40
- contact: event.contact
43
+ contact: event.contact,
44
+ quantity: event.quantity
41
45
  }
42
46
  }];
43
47
  }