@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.
- package/CHANGELOG.md +17 -0
- package/package.json +6 -6
- package/src/admin/Platform_Admin.res +2 -2
- package/src/admin/Platform_Admin.res.mjs +2 -2
- package/src/admin/Platform_Admin_Structure.res +4 -0
- package/src/admin/Platform_Admin_Structure.res.mjs +5 -1
- package/src/admin/Platform_ComponentDefinitionsApi.res +5 -1
- package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +5 -1
- package/src/components/Dcb/Dcb_Builder.res +2 -2
- package/src/components/Dcb/Dcb_Builder.res.mjs +4 -4
- package/src/plugin/component/Plugin_Builder.res +2 -2
- package/src/plugin/component/Plugin_Builder.res.mjs +4 -4
- package/src/plugin/component/Plugin_Structure.res +192 -45
- package/src/plugin/component/Plugin_Structure.res.mjs +139 -39
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +5 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +15 -4
- package/tests/message/MessageTest.res +1 -0
- package/tests/message/MessageTest.res.mjs +2 -1
- package/tests/plugin/PluginStructureTest.res +292 -0
- package/tests/plugin/PluginStructureTest.res.mjs +268 -0
- package/tests/plugin/StateChangeSlice/PsAttachInvoice.res +39 -0
- package/tests/plugin/StateChangeSlice/PsAttachInvoice.res.mjs +63 -0
|
@@ -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([])
|