@reventlessdev/reventless-core 3.0.0-alpha.189 → 3.0.0-alpha.191

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.
@@ -79,6 +79,20 @@ module PsAnnotatedViewSlice: ReventlessInfra.StateViewSlice.T = {
79
79
  let make = (~dcbEventLog as _, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
80
80
  }
81
81
 
82
+ module PsAttachInvoiceSlice: ReventlessInfra.StateChangeSlice.T = {
83
+ module Spec = PsAttachInvoice
84
+ module Behavior = {
85
+ type state = PsAttachInvoice.state
86
+ let initialState = PsAttachInvoice.initialState
87
+ let evolve = PsAttachInvoice.evolve
88
+ let decide = PsAttachInvoice.decide
89
+ let moduleUrl = PsAttachInvoice.moduleUrl
90
+ }
91
+ let isAsync = false
92
+ type component = scsComponent
93
+ let make = (~dcbEventLog as _, ~publishJsons as _, ~tagKeysByEventType as _=?, ~crossPartitionTagKeys as _=?, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
94
+ }
95
+
82
96
  let structure = Plugin_Structure.make(
83
97
  ~name="TestPlugin",
84
98
  ~stateChangeSlices=[module(PsPlaceOrderSlice), module(PsShipOrderSlice)],
@@ -261,6 +275,252 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
261
275
  })
262
276
  })
263
277
 
278
+ // What a field *is* comes from `SchemaType`, the IR every other schema
279
+ // consumer reads, rather than from a `String(_)` match here. These pin the
280
+ // shapes that separates the two: a date, a reference, a semantic-carrying
281
+ // string and an optional one all match `String(_)` loosely or not at all, and
282
+ // each was answered wrong before.
283
+ describe("labelField — the shape rule", () => {
284
+ let labelOf = (~entityName="Test", schema) => {
285
+ let r = Plugin_Structure.labelFieldsFromStateSchema(~entityName, schema->S.castToUnknown)
286
+ (r.field, r.searchableFields)
287
+ }
288
+
289
+ testSync("a DateTime is not a name — a state whose only string is one falls to id", () => {
290
+ // The shipped `Orders` case: `placedAt` was added so date views have a
291
+ // field to key off, and a positional rule let it rename the entity.
292
+ let schema = S.schema(s =>
293
+ {
294
+ "orderId": s.matches(Reventless.Reference.to_("Order")),
295
+ "placedAt": s.matches(Reventless.DateTime.string),
296
+ "shippedAt": s.matches(Reventless.DateTime.string),
297
+ }
298
+ )
299
+ expect(labelOf(schema))->toEqual(("id", []))
300
+ })
301
+
302
+ testSync("a DateTime is skipped, not fatal — a later plain string still wins", () => {
303
+ let schema = S.schema(s =>
304
+ {
305
+ "placedAt": s.matches(Reventless.DateTime.string),
306
+ "reference": s.matches(S.string),
307
+ }
308
+ )
309
+ expect(labelOf(schema))->toEqual(("reference", ["reference"]))
310
+ })
311
+
312
+ testSync("an optional string is still the entity's name", () => {
313
+ let schema = S.schema(s =>
314
+ {
315
+ "nickname": s.matches(S.option(S.string)),
316
+ }
317
+ )
318
+ expect(labelOf(schema))->toEqual(("nickname", ["nickname"]))
319
+ })
320
+
321
+ testSync("a reference is not a name even when its field is not called `*Id`", () => {
322
+ let schema = S.schema(s =>
323
+ {
324
+ "customer": s.matches(Reventless.Reference.to_("Customer")),
325
+ "note": s.matches(S.string),
326
+ }
327
+ )
328
+ expect(labelOf(schema))->toEqual(("note", ["note"]))
329
+ })
330
+
331
+ testSync("a DCB-tagged string is not a name", () => {
332
+ let schema = S.schema(s =>
333
+ {
334
+ "partition": s.matches(Reventless.DcbTag.string),
335
+ "note": s.matches(S.string),
336
+ }
337
+ )
338
+ expect(labelOf(schema))->toEqual(("note", ["note"]))
339
+ })
340
+
341
+ testSync("the `*Id` test is case-insensitive — `productID` is a reference", () => {
342
+ let schema = S.schema(s =>
343
+ {
344
+ "productID": s.matches(S.string),
345
+ "note": s.matches(S.string),
346
+ }
347
+ )
348
+ expect(labelOf(schema))->toEqual(("note", ["note"]))
349
+ })
350
+
351
+ testSync("a semantic-carrying string is a value, not a name", () => {
352
+ // `imageUrl` names a stored object. A bucket key read as a product's name
353
+ // is what the previous rule did whenever no plain string preceded it.
354
+ let schema = S.schema(s =>
355
+ {
356
+ "imageUrl": s.matches(Reventless.StorageRef.forStore(~store="productImages")),
357
+ "note": s.matches(S.string),
358
+ }
359
+ )
360
+ expect(labelOf(schema))->toEqual(("note", ["note"]))
361
+ })
362
+
363
+ testSync("a state whose only string is a semantic falls to id", () => {
364
+ let schema = S.schema(s =>
365
+ {
366
+ "imageUrl": s.matches(Reventless.StorageRef.forStore(~store="productImages")),
367
+ }
368
+ )
369
+ expect(labelOf(schema))->toEqual(("id", []))
370
+ })
371
+
372
+ testSync("`id` itself is still excluded by name", () => {
373
+ // `SchemaType` reads a name that short as an ordinary string, so the
374
+ // exclusion cannot come from the IR.
375
+ let schema = S.schema(s =>
376
+ {
377
+ "id": s.matches(S.string),
378
+ "note": s.matches(S.string),
379
+ }
380
+ )
381
+ expect(labelOf(schema))->toEqual(("note", ["note"]))
382
+ })
383
+ })
384
+
385
+ describe("labelField — the conventional-name rung", () => {
386
+ let labelOf = schema => {
387
+ let r = Plugin_Structure.labelFieldsFromStateSchema(~entityName="Test", schema->S.castToUnknown)
388
+ (r.field, r.searchableFields)
389
+ }
390
+
391
+ testSync("a field named `name` beats an earlier string", () => {
392
+ let schema = S.schema(s =>
393
+ {
394
+ "sku": s.matches(S.string),
395
+ "name": s.matches(S.string),
396
+ }
397
+ )
398
+ expect(labelOf(schema))->toEqual(("name", ["name"]))
399
+ })
400
+
401
+ testSync("`title` counts too, case-insensitively", () => {
402
+ let schema = S.schema(s =>
403
+ {
404
+ "body": s.matches(S.string),
405
+ "Title": s.matches(S.string),
406
+ }
407
+ )
408
+ expect(labelOf(schema))->toEqual(("Title", ["Title"]))
409
+ })
410
+
411
+ testSync("the match is exact — `customerName` is a customer's name, not this record's", () => {
412
+ // The shipped `OrdersView` fixture depends on this: it has exactly one
413
+ // eligible field and must be picked by position, not by convention.
414
+ let schema = S.schema(s =>
415
+ {
416
+ "customerName": s.matches(S.string),
417
+ "note": s.matches(S.string),
418
+ }
419
+ )
420
+ expect(labelOf(schema))->toEqual(("customerName", ["customerName"]))
421
+ })
422
+
423
+ testSync("an optional `name` still outranks an earlier string", () => {
424
+ let schema = S.schema(s =>
425
+ {
426
+ "sku": s.matches(S.string),
427
+ "name": s.matches(S.option(S.string)),
428
+ }
429
+ )
430
+ expect(labelOf(schema))->toEqual(("name", ["name"]))
431
+ })
432
+ })
433
+
434
+ // The rung is published because the four are not equally believable: a
435
+ // consumer with a name rule of its own has to know whether it is ranking
436
+ // against a declaration or against a guess.
437
+ describe("labelFieldSource — which rung answered", () => {
438
+ let sourceOf = schema =>
439
+ Plugin_Structure.labelFieldsFromStateSchema(
440
+ ~entityName="Test",
441
+ schema->S.castToUnknown,
442
+ ).source->Plugin_Structure.labelFieldSourceToString
443
+
444
+ testSync("a @displayName spec is a declaration", () => {
445
+ let customers = structure.stateViewSlices->Array.getUnsafe(2)
446
+ expect((customers.labelField, customers.labelFieldSource))->toEqual((
447
+ "displayName",
448
+ Some("annotation"),
449
+ ))
450
+ })
451
+
452
+ testSync("a field named `name` is a guess the consumer can also make", () => {
453
+ let schema = S.schema(s =>
454
+ {
455
+ "sku": s.matches(S.string),
456
+ "name": s.matches(S.string),
457
+ }
458
+ )
459
+ expect(sourceOf(schema))->toEqual("convention")
460
+ })
461
+
462
+ testSync("declaration order is a guess only this side can make", () => {
463
+ // `customerName` is eligible and unconventional, so it wins by position —
464
+ // and a consumer told only "customerName" could not tell that from a
465
+ // declaration.
466
+ let schema = S.schema(s =>
467
+ {
468
+ "customerName": s.matches(S.string),
469
+ "note": s.matches(S.string),
470
+ }
471
+ )
472
+ expect(sourceOf(schema))->toEqual("position")
473
+ })
474
+
475
+ testSync("no candidate at all is the state saying it has no human field", () => {
476
+ let schema = S.schema(s =>
477
+ {
478
+ "orderId": s.matches(Reventless.Reference.to_("Order")),
479
+ "placedAt": s.matches(Reventless.DateTime.string),
480
+ }
481
+ )
482
+ expect(sourceOf(schema))->toEqual("fallback")
483
+ })
484
+
485
+ testSync("every built def states one", () => {
486
+ let stated =
487
+ Array.concat(structure.readModels, structure.stateViewSlices)->Array.every(q =>
488
+ q.labelFieldSource->Option.isSome
489
+ )
490
+ expect(stated)->toEqual(true)
491
+ })
492
+ })
493
+
494
+ describe("statusField — the shape rule", () => {
495
+ let statusOf = schema =>
496
+ Plugin_Structure.statusFieldFromStateSchema(~entityName="Test", schema->S.castToUnknown)
497
+
498
+ testSync("a `status` field holding a closed set of values is the lifecycle field", () => {
499
+ let schema = S.schema(s =>
500
+ {
501
+ "status": s.matches(S.union([S.literal("Placed"), S.literal("Shipped")])),
502
+ }
503
+ )
504
+ expect(statusOf(schema))->toEqual(Some("status"))
505
+ })
506
+
507
+ testSync("an optional one counts too", () => {
508
+ let schema = S.schema(s =>
509
+ {
510
+ "status": s.matches(S.option(S.union([S.literal("Placed"), S.literal("Shipped")]))),
511
+ }
512
+ )
513
+ expect(statusOf(schema))->toEqual(Some("status"))
514
+ })
515
+
516
+ testSync("free text named `status` is not a lifecycle", () => {
517
+ // `allowedStates` filtering needs states to compare against; a string
518
+ // field named `status` gives a command menu nothing to match.
519
+ let schema = S.schema(s => {"status": s.matches(S.string)})
520
+ expect(statusOf(schema))->toEqual(None)
521
+ })
522
+ })
523
+
264
524
  // Phase 4: queryableDef.schema must carry x-reventless-* extension keys for
265
525
  // annotated state types. Plugin_Structure now uses SuryToJsonSchema.deriveObjectSchema
266
526
  // (annotation-aware) instead of S.toJSONSchema (metadata-blind).
@@ -384,4 +644,36 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
384
644
  expect(placeOrder.chapter)->toEqual(None)
385
645
  })
386
646
  })
647
+
648
+ // A field typed as a storage ref states that the deployment needs that store
649
+ // to exist. Collecting the requirement onto the structure is what lets the
650
+ // deploy read it without re-walking every component's schema.
651
+ describe("requiredStores", () => {
652
+ let withStores = Plugin_Structure.make(
653
+ ~name="TestPlugin",
654
+ ~stateChangeSlices=[module(PsAttachInvoiceSlice)],
655
+ )
656
+ let stores = withStores.requiredStores->Option.getOr([])
657
+
658
+ testSync("an unqualified store resolves against the declaring plugin", () =>
659
+ expect(stores->Array.includes("TestPlugin.documents"))->toBe(true)
660
+ )
661
+
662
+ testSync("a qualified store keeps its own plugin", () =>
663
+ expect(stores->Array.includes("branding.logos"))->toBe(true)
664
+ )
665
+
666
+ // `documents` is declared on both a command field and an event field.
667
+ testSync("a store declared by several fields is collected once", () =>
668
+ expect(stores->Array.filter(s => s == "TestPlugin.documents")->Array.length)->toBe(1)
669
+ )
670
+
671
+ testSync("only the declared stores are collected", () =>
672
+ expect(stores->Array.length)->toBe(2)
673
+ )
674
+
675
+ testSync("a plugin declaring no stores collects none — not None", () =>
676
+ expect(structure.requiredStores)->toEqual(Some([]))
677
+ )
678
+ })
387
679
  })
@@ -1,12 +1,18 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as S from "sury/src/S.res.mjs";
3
4
  import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
4
5
  import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
5
6
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
+ import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
8
+ import * as DateTime$Reventless from "@reventlessdev/reventless-spec/src/types/DateTime.res.mjs";
9
+ import * as Reference$Reventless from "@reventlessdev/reventless-spec/src/components/Reference.res.mjs";
10
+ import * as StorageRef$Reventless from "@reventlessdev/reventless-spec/src/semantic/StorageRef.res.mjs";
6
11
  import * as PsShipOrder$ReventlessCore from "./StateChangeSlice/PsShipOrder.res.mjs";
7
12
  import * as PsOrdersView$ReventlessCore from "./StateViewSlice/PsOrdersView.res.mjs";
8
13
  import * as PsPlaceOrder$ReventlessCore from "./StateChangeSlice/PsPlaceOrder.res.mjs";
9
14
  import * as PsAnnotatedView$ReventlessCore from "./StateViewSlice/PsAnnotatedView.res.mjs";
15
+ import * as PsAttachInvoice$ReventlessCore from "./StateChangeSlice/PsAttachInvoice.res.mjs";
10
16
  import * as PsCustomersView$ReventlessCore from "./StateViewSlice/PsCustomersView.res.mjs";
11
17
  import * as Plugin_Structure$ReventlessCore from "../../src/plugin/component/Plugin_Structure.res.mjs";
12
18
  import * as PsAvailableProductsView$ReventlessCore from "./StateViewSlice/PsAvailableProductsView.res.mjs";
@@ -175,6 +181,36 @@ let PsAnnotatedViewSlice = {
175
181
  make: make$5
176
182
  };
177
183
 
184
+ let Behavior$2 = {
185
+ initialState: false,
186
+ evolve: PsAttachInvoice$ReventlessCore.evolve,
187
+ decide: PsAttachInvoice$ReventlessCore.decide,
188
+ moduleUrl: PsAttachInvoice$ReventlessCore.moduleUrl
189
+ };
190
+
191
+ function make$6(param, param$1, param$2, param$3, param$4, param$5) {
192
+ return 0;
193
+ }
194
+
195
+ let PsAttachInvoiceSlice_Spec = {
196
+ name: PsAttachInvoice$ReventlessCore.name,
197
+ moduleUrl: PsAttachInvoice$ReventlessCore.moduleUrl,
198
+ Id: Id$Reventless.$$String,
199
+ consumedEventSchema: PsAttachInvoice$ReventlessCore.consumedEventSchema,
200
+ errorSchema: PsAttachInvoice$ReventlessCore.errorSchema,
201
+ eventSchema: PsAttachInvoice$ReventlessCore.eventSchema,
202
+ commandSchema: PsAttachInvoice$ReventlessCore.commandSchema,
203
+ commandAuthorization: PsAttachInvoice$ReventlessCore.commandAuthorization,
204
+ readConsistency: PsAttachInvoice$ReventlessCore.readConsistency
205
+ };
206
+
207
+ let PsAttachInvoiceSlice = {
208
+ Spec: PsAttachInvoiceSlice_Spec,
209
+ Behavior: Behavior$2,
210
+ isAsync: false,
211
+ make: make$6
212
+ };
213
+
178
214
  let structure = Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, [
179
215
  PsOrdersViewSlice,
180
216
  PsAvailableProductsViewSlice,
@@ -364,6 +400,218 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
364
400
  ]);
365
401
  });
366
402
  });
403
+ globalThis.describe("labelField — the shape rule", () => {
404
+ let labelOf = (entityNameOpt, schema) => {
405
+ let entityName = entityNameOpt !== undefined ? entityNameOpt : "Test";
406
+ let r = Plugin_Structure$ReventlessCore.labelFieldsFromStateSchema(entityName, schema);
407
+ return [
408
+ r.field,
409
+ r.searchableFields
410
+ ];
411
+ };
412
+ globalThis.test("a DateTime is not a name — a state whose only string is one falls to id", () => {
413
+ let schema = S.schema(s => ({
414
+ orderId: s.m(Reference$Reventless.to_(undefined, undefined, "Order")),
415
+ placedAt: s.m(DateTime$Reventless.string),
416
+ shippedAt: s.m(DateTime$Reventless.string)
417
+ }));
418
+ globalThis.expect(labelOf(undefined, schema)).toEqual([
419
+ "id",
420
+ []
421
+ ]);
422
+ });
423
+ globalThis.test("a DateTime is skipped, not fatal — a later plain string still wins", () => {
424
+ let schema = S.schema(s => ({
425
+ placedAt: s.m(DateTime$Reventless.string),
426
+ reference: s.m(S.string)
427
+ }));
428
+ globalThis.expect(labelOf(undefined, schema)).toEqual([
429
+ "reference",
430
+ ["reference"]
431
+ ]);
432
+ });
433
+ globalThis.test("an optional string is still the entity's name", () => {
434
+ let schema = S.schema(s => ({
435
+ nickname: s.m(S.option(S.string))
436
+ }));
437
+ globalThis.expect(labelOf(undefined, schema)).toEqual([
438
+ "nickname",
439
+ ["nickname"]
440
+ ]);
441
+ });
442
+ globalThis.test("a reference is not a name even when its field is not called `*Id`", () => {
443
+ let schema = S.schema(s => ({
444
+ customer: s.m(Reference$Reventless.to_(undefined, undefined, "Customer")),
445
+ note: s.m(S.string)
446
+ }));
447
+ globalThis.expect(labelOf(undefined, schema)).toEqual([
448
+ "note",
449
+ ["note"]
450
+ ]);
451
+ });
452
+ globalThis.test("a DCB-tagged string is not a name", () => {
453
+ let schema = S.schema(s => ({
454
+ partition: s.m(DcbTag$Reventless.string),
455
+ note: s.m(S.string)
456
+ }));
457
+ globalThis.expect(labelOf(undefined, schema)).toEqual([
458
+ "note",
459
+ ["note"]
460
+ ]);
461
+ });
462
+ globalThis.test("the `*Id` test is case-insensitive — `productID` is a reference", () => {
463
+ let schema = S.schema(s => ({
464
+ productID: s.m(S.string),
465
+ note: s.m(S.string)
466
+ }));
467
+ globalThis.expect(labelOf(undefined, schema)).toEqual([
468
+ "note",
469
+ ["note"]
470
+ ]);
471
+ });
472
+ globalThis.test("a semantic-carrying string is a value, not a name", () => {
473
+ let schema = S.schema(s => ({
474
+ imageUrl: s.m(StorageRef$Reventless.forStore(undefined, "productImages")),
475
+ note: s.m(S.string)
476
+ }));
477
+ globalThis.expect(labelOf(undefined, schema)).toEqual([
478
+ "note",
479
+ ["note"]
480
+ ]);
481
+ });
482
+ globalThis.test("a state whose only string is a semantic falls to id", () => {
483
+ let schema = S.schema(s => ({
484
+ imageUrl: s.m(StorageRef$Reventless.forStore(undefined, "productImages"))
485
+ }));
486
+ globalThis.expect(labelOf(undefined, schema)).toEqual([
487
+ "id",
488
+ []
489
+ ]);
490
+ });
491
+ globalThis.test("`id` itself is still excluded by name", () => {
492
+ let schema = S.schema(s => ({
493
+ id: s.m(S.string),
494
+ note: s.m(S.string)
495
+ }));
496
+ globalThis.expect(labelOf(undefined, schema)).toEqual([
497
+ "note",
498
+ ["note"]
499
+ ]);
500
+ });
501
+ });
502
+ globalThis.describe("labelField — the conventional-name rung", () => {
503
+ let labelOf = schema => {
504
+ let r = Plugin_Structure$ReventlessCore.labelFieldsFromStateSchema("Test", schema);
505
+ return [
506
+ r.field,
507
+ r.searchableFields
508
+ ];
509
+ };
510
+ globalThis.test("a field named `name` beats an earlier string", () => {
511
+ let schema = S.schema(s => ({
512
+ sku: s.m(S.string),
513
+ name: s.m(S.string)
514
+ }));
515
+ globalThis.expect(labelOf(schema)).toEqual([
516
+ "name",
517
+ ["name"]
518
+ ]);
519
+ });
520
+ globalThis.test("`title` counts too, case-insensitively", () => {
521
+ let schema = S.schema(s => ({
522
+ body: s.m(S.string),
523
+ Title: s.m(S.string)
524
+ }));
525
+ globalThis.expect(labelOf(schema)).toEqual([
526
+ "Title",
527
+ ["Title"]
528
+ ]);
529
+ });
530
+ globalThis.test("the match is exact — `customerName` is a customer's name, not this record's", () => {
531
+ let schema = S.schema(s => ({
532
+ customerName: s.m(S.string),
533
+ note: s.m(S.string)
534
+ }));
535
+ globalThis.expect(labelOf(schema)).toEqual([
536
+ "customerName",
537
+ ["customerName"]
538
+ ]);
539
+ });
540
+ globalThis.test("an optional `name` still outranks an earlier string", () => {
541
+ let schema = S.schema(s => ({
542
+ sku: s.m(S.string),
543
+ name: s.m(S.option(S.string))
544
+ }));
545
+ globalThis.expect(labelOf(schema)).toEqual([
546
+ "name",
547
+ ["name"]
548
+ ]);
549
+ });
550
+ });
551
+ globalThis.describe("labelFieldSource — which rung answered", () => {
552
+ let sourceOf = schema => Plugin_Structure$ReventlessCore.labelFieldSourceToString(Plugin_Structure$ReventlessCore.labelFieldsFromStateSchema("Test", schema).source);
553
+ globalThis.test("a @displayName spec is a declaration", () => {
554
+ let customers = structure.stateViewSlices[2];
555
+ globalThis.expect([
556
+ customers.labelField,
557
+ customers.labelFieldSource
558
+ ]).toEqual([
559
+ "displayName",
560
+ "annotation"
561
+ ]);
562
+ });
563
+ globalThis.test("a field named `name` is a guess the consumer can also make", () => {
564
+ let schema = S.schema(s => ({
565
+ sku: s.m(S.string),
566
+ name: s.m(S.string)
567
+ }));
568
+ globalThis.expect(sourceOf(schema)).toEqual("convention");
569
+ });
570
+ globalThis.test("declaration order is a guess only this side can make", () => {
571
+ let schema = S.schema(s => ({
572
+ customerName: s.m(S.string),
573
+ note: s.m(S.string)
574
+ }));
575
+ globalThis.expect(sourceOf(schema)).toEqual("position");
576
+ });
577
+ globalThis.test("no candidate at all is the state saying it has no human field", () => {
578
+ let schema = S.schema(s => ({
579
+ orderId: s.m(Reference$Reventless.to_(undefined, undefined, "Order")),
580
+ placedAt: s.m(DateTime$Reventless.string)
581
+ }));
582
+ globalThis.expect(sourceOf(schema)).toEqual("fallback");
583
+ });
584
+ globalThis.test("every built def states one", () => {
585
+ let stated = structure.readModels.concat(structure.stateViewSlices).every(q => Stdlib_Option.isSome(q.labelFieldSource));
586
+ globalThis.expect(stated).toEqual(true);
587
+ });
588
+ });
589
+ globalThis.describe("statusField — the shape rule", () => {
590
+ globalThis.test("a `status` field holding a closed set of values is the lifecycle field", () => {
591
+ let schema = S.schema(s => ({
592
+ status: s.m(S.union([
593
+ S.literal("Placed"),
594
+ S.literal("Shipped")
595
+ ]))
596
+ }));
597
+ globalThis.expect(Plugin_Structure$ReventlessCore.statusFieldFromStateSchema("Test", schema)).toEqual("status");
598
+ });
599
+ globalThis.test("an optional one counts too", () => {
600
+ let schema = S.schema(s => ({
601
+ status: s.m(S.option(S.union([
602
+ S.literal("Placed"),
603
+ S.literal("Shipped")
604
+ ])))
605
+ }));
606
+ globalThis.expect(Plugin_Structure$ReventlessCore.statusFieldFromStateSchema("Test", schema)).toEqual("status");
607
+ });
608
+ globalThis.test("free text named `status` is not a lifecycle", () => {
609
+ let schema = S.schema(s => ({
610
+ status: s.m(S.string)
611
+ }));
612
+ globalThis.expect(Plugin_Structure$ReventlessCore.statusFieldFromStateSchema("Test", schema)).toEqual(undefined);
613
+ });
614
+ });
367
615
  globalThis.describe("queryableDef.schema propagates x-reventless-* annotations", () => {
368
616
  let getProperty = (json, key) => {
369
617
  let obj = Stdlib_JSON.Decode.object(json);
@@ -453,6 +701,25 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
453
701
  globalThis.expect(placeOrder.chapter).toEqual(undefined);
454
702
  });
455
703
  });
704
+ globalThis.describe("requiredStores", () => {
705
+ let withStores = Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, undefined, [PsAttachInvoiceSlice], undefined, undefined, undefined, undefined, undefined, undefined);
706
+ let stores = Stdlib_Option.getOr(withStores.requiredStores, []);
707
+ globalThis.test("an unqualified store resolves against the declaring plugin", () => {
708
+ globalThis.expect(stores.includes("TestPlugin.documents")).toBe(true);
709
+ });
710
+ globalThis.test("a qualified store keeps its own plugin", () => {
711
+ globalThis.expect(stores.includes("branding.logos")).toBe(true);
712
+ });
713
+ globalThis.test("a store declared by several fields is collected once", () => {
714
+ globalThis.expect(stores.filter(s => s === "TestPlugin.documents").length).toBe(1);
715
+ });
716
+ globalThis.test("only the declared stores are collected", () => {
717
+ globalThis.expect(stores.length).toBe(2);
718
+ });
719
+ globalThis.test("a plugin declaring no stores collects none — not None", () => {
720
+ globalThis.expect(structure.requiredStores).toEqual([]);
721
+ });
722
+ });
456
723
  });
457
724
 
458
725
  export {
@@ -462,6 +729,7 @@ export {
462
729
  PsAvailableProductsViewSlice,
463
730
  PsCustomersViewSlice,
464
731
  PsAnnotatedViewSlice,
732
+ PsAttachInvoiceSlice,
465
733
  structure,
466
734
  }
467
735
  /* structure Not a pure module */
@@ -0,0 +1,39 @@
1
+ // Test fixture spec for declared-object-store collection.
2
+ //
3
+ // Carries `@storageRef` on a command field, on an event field, and once in the
4
+ // qualified `"<plugin>.<store>"` form — the three shapes Plugin_Structure has to
5
+ // resolve into one qualified identity per store. `documents` is declared twice
6
+ // on purpose so the deduplication is exercised by data rather than asserted in
7
+ // the abstract.
8
+
9
+ @@reventless.spec("AttachInvoice")
10
+
11
+ type state = bool
12
+ let initialState = false
13
+
14
+ @schema
15
+ type consumedEvent = OrderPlaced
16
+
17
+ let evolve = (_state, _event) => true
18
+
19
+ // Note the attribute position: `@storageRef(…)` precedes the field NAME. Placed
20
+ // after the colon it attaches to the type expression instead of the field, and
21
+ // the ppx ignores it silently — a declaration that compiles, deploys green, and
22
+ // provisions nothing.
23
+ @schema
24
+ type command =
25
+ | AttachInvoice({
26
+ orderId: string,
27
+ @storageRef("documents") documentUrl: string,
28
+ // Another plugin's store — qualified, so it must NOT resolve to this
29
+ // plugin.
30
+ @storageRef("branding.logos") logoUrl: string,
31
+ })
32
+
33
+ @schema
34
+ type error = OrderNotFound
35
+
36
+ @schema
37
+ type event = InvoiceAttached({orderId: string, @storageRef("documents") documentUrl: string})
38
+
39
+ let decide = (_state, _command): result<array<event>, error> => Ok([])