@reventlessdev/reventless-core 3.0.0-alpha.200 → 3.0.0-alpha.201

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,14 @@
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.201 (2026-08-02)
7
+
8
+ ### Features
9
+
10
+ * **aws,core,doc:** environment-tiered CloudWatch log retention and levels ([8032023](https://github.com/ReventlessDev/reventless-core/commit/803202305a7d352484b8df5e58df03c92ca58b5f))
11
+ * **aws,core,spec,seed-aws:** expire uploads nobody committed a reference to ([f63e84c](https://github.com/ReventlessDev/reventless-core/commit/f63e84c1a11cc350b799a6f69a2e7427cf1ea6e9))
12
+
13
+
6
14
  # 3.0.0-alpha.200 (2026-08-01)
7
15
 
8
16
  * feat(aws,core,spec)!: qualify store prefixes by plugin and refuse name collisions ([da39405](https://github.com/ReventlessDev/reventless-core/commit/da394059d9f8f981bf7adc79e2c1ce2b429e0267))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-core",
3
- "version": "3.0.0-alpha.200",
3
+ "version": "3.0.0-alpha.201",
4
4
  "description": "Core package for Reventless framework",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -28,22 +28,22 @@
28
28
  "dependencies": {
29
29
  "sury": "11.0.0-alpha.4",
30
30
  "uuid": "^13.0.0",
31
+ "@reventlessdev/rescript-fast-csv": "2.0.0-alpha.0",
31
32
  "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
32
33
  "@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
33
34
  "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
34
- "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
35
35
  "@reventlessdev/rescript-node": "2.0.0-alpha.0",
36
- "@reventlessdev/rescript-fast-csv": "2.0.0-alpha.0",
36
+ "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
37
37
  "@reventlessdev/rescript-ssh2": "2.0.0-alpha.0",
38
- "@reventlessdev/reventless-infra": "3.0.0-alpha.117",
38
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.118",
39
39
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
40
- "@reventlessdev/reventless-spec": "3.0.0-alpha.92",
41
- "@reventlessdev/reventless-interop": "3.0.0-alpha.30"
40
+ "@reventlessdev/reventless-interop": "3.0.0-alpha.30",
41
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.93"
42
42
  },
43
43
  "devDependencies": {
44
44
  "rescript": "12.3.0",
45
45
  "sury-ppx": "11.0.0-alpha.2",
46
- "@reventlessdev/reventless-ppx": "1.0.0-alpha.59"
46
+ "@reventlessdev/reventless-ppx": "1.0.0-alpha.60"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "rescript": "12.3.0"
@@ -200,6 +200,11 @@ type commandHandlerConfig = {
200
200
  sqsBatchSize?: int,
201
201
  ephemeralStorageMb?: int,
202
202
  logRetentionDays?: int,
203
+ // Minimum log level for this handler ("silent" | "error" | "warn" | "info" |
204
+ // "debug"), mapped onto the `LOG_LEVEL` env var the logger reads. Overrides the
205
+ // per-environment tier default. Transport-neutral: the in-memory platform reads
206
+ // it the same way via the env var.
207
+ logLevel?: string,
203
208
  envVars?: dict<string>,
204
209
  }
205
210
 
@@ -220,5 +225,8 @@ module CommandHandlerDefaults = {
220
225
  let timeout = 30
221
226
  let sqsBatchSize = 10
222
227
  let ephemeralStorageMb = 512
223
- let logRetentionDays = 7
228
+ // No `logRetentionDays` / `logLevel` default here: both are resolved per
229
+ // environment tier by the AWS platform (`Util_LogRetention`), not by a flat
230
+ // framework constant. An unset field means "follow the stack's tier", which a
231
+ // single number could not express.
224
232
  }
@@ -48,8 +48,7 @@ let CommandHandlerDefaults = {
48
48
  memorySize: 1024,
49
49
  timeout: 30,
50
50
  sqsBatchSize: 10,
51
- ephemeralStorageMb: 512,
52
- logRetentionDays: 7
51
+ ephemeralStorageMb: 512
53
52
  };
54
53
 
55
54
  export {
@@ -309,7 +309,7 @@ module Make = (
309
309
  })
310
310
  let eventLogEntries = Array.concat(eventLogEntriesFromAggregates, dcbResult.eventLogEntries)
311
311
  Config.hooks.subscriptionInfraHook->Option.forEach(hook =>
312
- hook({allQueryDbs, allEventTopics, eventLogEntries, opts})
312
+ hook({pluginName: name, allQueryDbs, allEventTopics, eventLogEntries, opts})
313
313
  )
314
314
 
315
315
  let queryEngine = QueryEngineAdapter.make(allQueryDbs)
@@ -150,6 +150,7 @@ function Make(RuntimeEnvironment) {
150
150
  }));
151
151
  let eventLogEntries = eventLogEntriesFromAggregates.concat(dcbResult.eventLogEntries);
152
152
  Stdlib_Option.forEach(Config.hooks.subscriptionInfraHook, hook => hook({
153
+ pluginName: Platform_Admin_Structure$ReventlessCore.pluginId,
153
154
  allQueryDbs: allQueryDbs,
154
155
  allEventTopics: aggregateEventTopics,
155
156
  eventLogEntries: eventLogEntries,
@@ -89,7 +89,7 @@ let pluginStatusSubscriptionSource: GraphQL_Stitcher.subscriptionSource = {
89
89
  // (`injectAwsAuthAll(~group="Admin")`), and uploads are a regular authenticated-user
90
90
  // operation. They are added instead to the **domain** API's platform-owned base
91
91
  // (`domainBaseFragment` in the AWS/local Platform), which takes the default
92
- // `AllowAuthenticated` auth. See [docs/plans/upload-release-path.md] § "Which API".
92
+ // `AllowAuthenticated` auth. See [docs/plans/done/upload-release-path.md] § "Which API".
93
93
  let uploadTypes = [
94
94
  `type Upload_Ticket {\n uploadUrl: String!\n storageRef: String!\n}`,
95
95
  `type Upload_ReleaseResult {\n released: Boolean!\n reason: String\n}`,
@@ -0,0 +1,124 @@
1
+ // Which fields of which committed events can hold a storage ref — read off the
2
+ // declarations rather than hand-listed.
3
+ //
4
+ // This is the deploy-time half of the claim mechanism (see
5
+ // [docs/plans/done/upload-pending-claim-and-expiry.md]). An uploaded object is minted
6
+ // carrying a "pending" tag and expires while it still has one, so something has
7
+ // to notice when an event finally references it. That something needs to know
8
+ // three things, and a `@storageRef` declaration already says all three:
9
+ //
10
+ // which events to observe only variants with at least one declared field
11
+ // where the refs sit the field name, and whether it holds one or many
12
+ // which store they're in the target, qualified to `{plugin}.{store}`
13
+ //
14
+ // Deriving it here rather than in the claimer means the runtime carries a flat
15
+ // lookup table and no schema machinery: the Lambda reads `{eventType → fields}`
16
+ // and never sees a sury schema. It also means an annotation added, moved or
17
+ // deleted changes what gets claimed on the next deploy, with nothing to keep in
18
+ // sync by hand.
19
+ //
20
+ // Read the same way `Plugin_Structure` reads the same declarations — top-level
21
+ // properties of each variant, via `StorageRef.getFieldStore`. A store nested
22
+ // inside an object is not provisioned there and so is not claimed here: one
23
+ // reading, consistently narrow, rather than two that could disagree about which
24
+ // fields exist.
25
+
26
+ /** One declared ref-bearing field of one event variant. */
27
+ type refField = {
28
+ /** The field's name on the event's payload (`data` on the wire). */
29
+ field: string,
30
+ /** Whether the field holds one ref or an array of them. */
31
+ arity: Reventless.StorageRef.arity,
32
+ /** The store the refs live in, qualified `{plugin}.{store}` — already
33
+ resolved against the declaring plugin, so a reader never has to know
34
+ whose plugin it came from. */
35
+ store: string,
36
+ }
37
+
38
+ /** The ref-bearing fields of one event variant. Only variants with at least
39
+ one are produced, so an empty result means "this event log carries no
40
+ storage refs" and the caller can skip it entirely. */
41
+ type eventRefFields = {
42
+ eventType: string,
43
+ fields: array<refField>,
44
+ }
45
+
46
+ // A variant is `{TAG: "<Name>", …fields}` (payload-less variants are a bare
47
+ // string const and can hold nothing, so they drop out here).
48
+ let fromVariant = (~plugin: string, variant: S.t<unknown>): option<eventRefFields> =>
49
+ switch variant {
50
+ | S.Object({properties}) =>
51
+ switch properties->Dict.get("TAG") {
52
+ | Some(String({const: ?Some(eventType)})) =>
53
+ let fields =
54
+ properties
55
+ ->Dict.toArray
56
+ ->Array.filterMap(((field, fieldSchema)) =>
57
+ Reventless.StorageRef.getFieldStore(fieldSchema)->Option.map(((target, arity)) => {
58
+ field,
59
+ arity,
60
+ // An unqualified store belongs to the declaring plugin — the same
61
+ // resolution `Plugin_Structure` makes, so the key the claimer looks
62
+ // up is the key the deploy provisioned.
63
+ store: target.plugin->Option.getOr(plugin) ++ "." ++ target.store,
64
+ })
65
+ )
66
+ ->Array.toSorted((a, b) => String.compare(a.field, b.field))
67
+ fields->Array.length == 0 ? None : Some({eventType, fields})
68
+ | _ => None
69
+ }
70
+ | _ => None
71
+ }
72
+
73
+ /**
74
+ The ref-bearing fields of every variant in an event log's event schema.
75
+
76
+ `~plugin` is the registered name of the plugin the event log belongs to, used
77
+ to qualify a store the annotation left unqualified.
78
+ */
79
+ let fromEventSchema = (~plugin: string, eventSchema: S.t<unknown>): array<eventRefFields> =>
80
+ switch eventSchema {
81
+ | Union({anyOf}) => anyOf->Array.filterMap(v => fromVariant(~plugin, v))
82
+ | other => fromVariant(~plugin, other)->Option.mapOr([], e => [e])
83
+ }
84
+
85
+ /**
86
+ The wire form the claimer reads: `{"<eventType>": [{"field", "arity", "store"}]}`.
87
+
88
+ A flat JSON object rather than a schema, because it crosses into a Lambda's
89
+ environment: the runtime side does a dictionary lookup and a field read, and
90
+ carries no sury dependency to do it.
91
+ */
92
+ let toJson = (entries: array<eventRefFields>): JSON.t =>
93
+ entries
94
+ ->Array.map(({eventType, fields}) => (
95
+ eventType,
96
+ fields
97
+ ->Array.map(({field, arity, store}) =>
98
+ Dict.fromArray([
99
+ ("field", JSON.Encode.string(field)),
100
+ (
101
+ "arity",
102
+ JSON.Encode.string(
103
+ switch arity {
104
+ | Single => "one"
105
+ | Multiple => "many"
106
+ },
107
+ ),
108
+ ),
109
+ ("store", JSON.Encode.string(store)),
110
+ ])->JSON.Encode.object
111
+ )
112
+ ->JSON.Encode.array,
113
+ ))
114
+ ->Dict.fromArray
115
+ ->JSON.Encode.object
116
+
117
+ /** The distinct stores an event log's declarations point at. What the claimer's
118
+ IAM has to cover for that log, and what a coverage check compares against
119
+ the set of stores the deploy actually provisioned. */
120
+ let storesOf = (entries: array<eventRefFields>): array<string> =>
121
+ entries
122
+ ->Array.flatMap(({fields}) => fields->Array.map(({store}) => store))
123
+ ->Belt.Set.String.fromArray
124
+ ->Belt.Set.String.toArray
@@ -0,0 +1,88 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as Belt_SetString from "@rescript/runtime/lib/es6/Belt_SetString.js";
6
+ import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
7
+ import * as StorageRef$Reventless from "@reventlessdev/reventless-spec/src/semantic/StorageRef.res.mjs";
8
+
9
+ function fromVariant(plugin, variant) {
10
+ if (variant.type !== "object") {
11
+ return;
12
+ }
13
+ let properties = variant.properties;
14
+ let match = properties["TAG"];
15
+ if (match === undefined) {
16
+ return;
17
+ }
18
+ if (match.type !== "string") {
19
+ return;
20
+ }
21
+ let eventType = match.const;
22
+ if (eventType === undefined) {
23
+ return;
24
+ }
25
+ let fields = Stdlib_Array.filterMap(Object.entries(properties), param => {
26
+ let field = param[0];
27
+ return Stdlib_Option.map(StorageRef$Reventless.getFieldStore(param[1]), param => {
28
+ let target = param[0];
29
+ return {
30
+ field: field,
31
+ arity: param[1],
32
+ store: Stdlib_Option.getOr(target.plugin, plugin) + "." + target.store
33
+ };
34
+ });
35
+ }).toSorted((a, b) => Primitive_string.compare(a.field, b.field));
36
+ if (fields.length === 0) {
37
+ return;
38
+ } else {
39
+ return {
40
+ eventType: eventType,
41
+ fields: fields
42
+ };
43
+ }
44
+ }
45
+
46
+ function fromEventSchema(plugin, eventSchema) {
47
+ if (eventSchema.type === "union") {
48
+ return Stdlib_Array.filterMap(eventSchema.anyOf, v => fromVariant(plugin, v));
49
+ } else {
50
+ return Stdlib_Option.mapOr(fromVariant(plugin, eventSchema), [], e => [e]);
51
+ }
52
+ }
53
+
54
+ function toJson(entries) {
55
+ return Object.fromEntries(entries.map(param => [
56
+ param.eventType,
57
+ param.fields.map(param => {
58
+ let tmp;
59
+ tmp = param.arity === "Single" ? "one" : "many";
60
+ return Object.fromEntries([
61
+ [
62
+ "field",
63
+ param.field
64
+ ],
65
+ [
66
+ "arity",
67
+ tmp
68
+ ],
69
+ [
70
+ "store",
71
+ param.store
72
+ ]
73
+ ]);
74
+ })
75
+ ]));
76
+ }
77
+
78
+ function storesOf(entries) {
79
+ return Belt_SetString.toArray(Belt_SetString.fromArray(entries.flatMap(param => param.fields.map(param => param.store))));
80
+ }
81
+
82
+ export {
83
+ fromVariant,
84
+ fromEventSchema,
85
+ toJson,
86
+ storesOf,
87
+ }
88
+ /* StorageRef-Reventless Not a pure module */
@@ -888,7 +888,7 @@ module Make = (
888
888
  // Gives the AWS platform access to allQueryDbs, allEventTopics, and eventLogEntries
889
889
  // without coupling reventless-core to reventless-aws.
890
890
  Spec.hooks.subscriptionInfraHook->Option.forEach(hook =>
891
- hook({allQueryDbs, allEventTopics, eventLogEntries, opts})
891
+ hook({pluginName: name, allQueryDbs, allEventTopics, eventLogEntries, opts})
892
892
  )
893
893
 
894
894
  module SpecificHeartbeat = Heartbeat_Builder.Make(HeartbeatRunner)
@@ -547,6 +547,7 @@ function Make(Spec) {
547
547
  let tasksOutputs = Plugin_Helpers$ReventlessCore.createTasks(tasks, aggregatesOutputs, scheduler, schedulerRoleUrn, publishToAggregates, queryEngine, Spec.resourceNaming, componentRuntime, opts);
548
548
  let resolvers = Plugin_Helpers$ReventlessCore.createResolvers(allQueryDbs);
549
549
  Stdlib_Option.forEach(Spec.hooks.subscriptionInfraHook, hook => hook({
550
+ pluginName: extra$1,
550
551
  allQueryDbs: allQueryDbs,
551
552
  allEventTopics: aggregateEventTopics,
552
553
  eventLogEntries: eventLogEntries,
@@ -911,6 +911,11 @@ type eventQueryRegistrationParams = {
911
911
  // are assembled inside builderOutputs. Lets the AWS platform wire StateTopic and
912
912
  // EventLogSubscription Lambdas without touching reventless-core.
913
913
  type subscriptionInfraParams = {
914
+ /** The plugin's registered name. Carried because infrastructure wired from
915
+ here can be per plugin rather than per component — and because a
916
+ declaration that names a store without qualifying it means "this plugin's
917
+ own", which is unreadable without knowing whose plugin it is. */
918
+ pluginName: string,
914
919
  allQueryDbs: ReventlessInfra.QueryDb.allOutputs,
915
920
  allEventTopics: dict<ReventlessInfra.EventTopic.outputs>,
916
921
  eventLogEntries: array<ReventlessInfra.Api.eventLogSchemaEntry>,
@@ -503,7 +503,11 @@ let make = (
503
503
  properties
504
504
  ->Dict.toArray
505
505
  ->Array.filterMap(((field, fieldSchema)) =>
506
- Reventless.StorageRef.getStore(fieldSchema)->Option.map(target => {
506
+ // `getFieldStore`, not `getStore`: a `@storageRef("s") urls: array<string>`
507
+ // field carries the marker on its *element*, so reading the field schema
508
+ // directly answered `None` and the store went unprovisioned — the same
509
+ // silence as never having written the annotation.
510
+ Reventless.StorageRef.getFieldStore(fieldSchema)->Option.map(((target, _arity)) => {
507
511
  Reventless.Plugin.store: target.plugin->Option.getOr(name) ++ "." ++ target.store,
508
512
  component,
509
513
  field,
@@ -446,7 +446,8 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
446
446
  let properties = v.properties;
447
447
  return Stdlib_Array.filterMap(Object.entries(properties), param => {
448
448
  let field = param[0];
449
- return Stdlib_Option.map(StorageRef$Reventless.getStore(param[1]), target => {
449
+ return Stdlib_Option.map(StorageRef$Reventless.getFieldStore(param[1]), param => {
450
+ let target = param[0];
450
451
  let plugin = target.plugin;
451
452
  return {
452
453
  store: Stdlib_Option.getOr(target.plugin, name) + "." + target.store,
@@ -0,0 +1,148 @@
1
+ // What the claim component ever gets to see, decided here.
2
+ //
3
+ // `StorageRefFields.fromEventSchema` is the deploy-time reading of the
4
+ // `@storageRef` declarations that becomes the claimer's entire input: an event
5
+ // variant it does not produce is an event the claimer never reads a record for,
6
+ // and a field it does not list is a ref that never gets claimed — so the object
7
+ // keeps its pending tag and, on a store with expiry enabled, is eventually
8
+ // deleted while something still references it.
9
+ //
10
+ // Which makes the interesting assertions the negative ones: what is *absent*
11
+ // from this output is what silently rots.
12
+
13
+ open JestGlobals
14
+
15
+ module Fields = StorageRefFields
16
+
17
+ // An event union, shaped the way sury encodes a ReScript variant: one object per
18
+ // variant, discriminated by a `TAG` string constant.
19
+ let variant = (~tag: string, fields: array<(string, S.t<unknown>)>): S.t<unknown> =>
20
+ S.object(s => {
21
+ let _ = s.field("TAG", S.literal(tag)->S.castToUnknown)
22
+ fields->Array.forEach(((name, schema)) => {
23
+ let _ = s.field(name, schema)
24
+ })
25
+ ()
26
+ })->S.castToUnknown
27
+
28
+ let ref_ = (~plugin: option<string>=?, ~store: string) =>
29
+ Reventless.StorageRef.forStore(~plugin?, ~store)->S.castToUnknown
30
+
31
+ describe("StorageRefFields.fromEventSchema", () => {
32
+ testSync("finds a declared single-valued ref field and qualifies its store", () => {
33
+ let schema = S.union([variant(~tag="ProductImageChanged", [("imageUrl", ref_(~store="productImages"))])])->S.castToUnknown
34
+ expect(Fields.fromEventSchema(~plugin="Catalog", schema))->toEqual([
35
+ {
36
+ Fields.eventType: "ProductImageChanged",
37
+ fields: [
38
+ {Fields.field: "imageUrl", arity: Single, store: "Catalog.productImages"},
39
+ ],
40
+ },
41
+ ])
42
+ })
43
+
44
+ // The annotation's own qualification wins: a field pointing at another
45
+ // plugin's store must not be re-attributed to the plugin that wrote it, or the
46
+ // claimer resolves the wrong bucket and refuses the ref as out-of-store.
47
+ testSync("a qualified declaration keeps the store it names", () => {
48
+ let schema =
49
+ S.union([
50
+ variant(~tag="OrderPhotoAttached", [("photo", ref_(~plugin="Catalog", ~store="productImages"))]),
51
+ ])->S.castToUnknown
52
+ expect(
53
+ Fields.fromEventSchema(~plugin="Ordering", schema)
54
+ ->Array.flatMap(e => e.fields->Array.map(f => f.store)),
55
+ )->toEqual(["Catalog.productImages"])
56
+ })
57
+
58
+ // `@storageRef` on an `array<string>` attaches the marker to the ELEMENT, so
59
+ // reading the field schema directly answers None. The claimer needs the arity
60
+ // to know whether to read one value or iterate.
61
+ testSync("finds a multi-valued ref field and records its arity", () => {
62
+ let schema =
63
+ S.union([
64
+ variant(~tag="GalleryReplaced", [("imageUrls", S.array(ref_(~store="productImages"))->S.castToUnknown)]),
65
+ ])->S.castToUnknown
66
+ expect(
67
+ Fields.fromEventSchema(~plugin="Catalog", schema)->Array.flatMap(e => e.fields),
68
+ )->toEqual([{Fields.field: "imageUrls", arity: Multiple, store: "Catalog.productImages"}])
69
+ })
70
+
71
+ // Narrow input by construction: a platform whose plugins declare no store
72
+ // provisions no claimer at all, and an event log carrying no declaration is
73
+ // never subscribed to.
74
+ testSync("a variant with no declared field is omitted entirely", () => {
75
+ let schema =
76
+ S.union([
77
+ variant(~tag="ProductRenamed", [("name", S.string->S.castToUnknown)]),
78
+ variant(~tag="ProductImageChanged", [("imageUrl", ref_(~store="productImages"))]),
79
+ ])->S.castToUnknown
80
+ expect(Fields.fromEventSchema(~plugin="Catalog", schema)->Array.map(e => e.eventType))->toEqual([
81
+ "ProductImageChanged",
82
+ ])
83
+ })
84
+
85
+ testSync("an event schema with no declaration anywhere yields nothing", () => {
86
+ let schema = S.union([variant(~tag="ProductRenamed", [("name", S.string->S.castToUnknown)])])->S.castToUnknown
87
+ expect(Fields.fromEventSchema(~plugin="Catalog", schema))->toEqual([])
88
+ })
89
+
90
+ // A string field that merely looks like a ref is not one. Declaration
91
+ // outranks inference everywhere else in the framework, and this is the reading
92
+ // that has to agree with `Plugin_Structure`'s — the claimer can only reach a
93
+ // store the deploy actually provisioned.
94
+ testSync("an unannotated string field is not treated as a ref", () => {
95
+ let schema =
96
+ S.union([variant(~tag="ProductImported", [("imageUrl", S.string->S.castToUnknown)])])->S.castToUnknown
97
+ expect(Fields.fromEventSchema(~plugin="Catalog", schema))->toEqual([])
98
+ })
99
+
100
+ testSync("a single-variant (non-union) event schema is read the same way", () => {
101
+ let schema = variant(~tag="ProductImageChanged", [("imageUrl", ref_(~store="productImages"))])
102
+ expect(Fields.fromEventSchema(~plugin="Catalog", schema)->Array.map(e => e.eventType))->toEqual([
103
+ "ProductImageChanged",
104
+ ])
105
+ })
106
+ })
107
+
108
+ describe("StorageRefFields.toJson", () => {
109
+ // The wire form crosses into a Lambda's environment, where the handler decodes
110
+ // it with plain JSON reads. Both ends are pinned here because a drift is
111
+ // silent: an unreadable entry is indistinguishable from an absent one, and an
112
+ // absent one means "nothing to claim".
113
+ testSync("emits field, arity and store per event type", () => {
114
+ let entries: array<Fields.eventRefFields> = [
115
+ {
116
+ eventType: "ProductImageChanged",
117
+ fields: [{field: "imageUrl", arity: Single, store: "Catalog.productImages"}],
118
+ },
119
+ {
120
+ eventType: "GalleryReplaced",
121
+ fields: [{field: "imageUrls", arity: Multiple, store: "Catalog.productImages"}],
122
+ },
123
+ ]
124
+ expect(Fields.toJson(entries)->JSON.stringify)->toBe(
125
+ `{"ProductImageChanged":[{"field":"imageUrl","arity":"one","store":"Catalog.productImages"}],` ++
126
+ `"GalleryReplaced":[{"field":"imageUrls","arity":"many","store":"Catalog.productImages"}]}`,
127
+ )
128
+ })
129
+ })
130
+
131
+ describe("StorageRefFields.storesOf", () => {
132
+ testSync("collects the distinct stores an event log's declarations reach", () => {
133
+ let entries: array<Fields.eventRefFields> = [
134
+ {
135
+ eventType: "A",
136
+ fields: [{field: "a", arity: Single, store: "Catalog.productImages"}],
137
+ },
138
+ {
139
+ eventType: "B",
140
+ fields: [
141
+ {field: "b", arity: Single, store: "Catalog.productImages"},
142
+ {field: "c", arity: Multiple, store: "Ordering.receipts"},
143
+ ],
144
+ },
145
+ ]
146
+ expect(Fields.storesOf(entries))->toEqual(["Catalog.productImages", "Ordering.receipts"])
147
+ })
148
+ })
@@ -0,0 +1,152 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as StorageRef$Reventless from "@reventlessdev/reventless-spec/src/semantic/StorageRef.res.mjs";
5
+ import * as StorageRefFields$ReventlessCore from "../../src/components/Api/StorageRefFields.res.mjs";
6
+
7
+ function variant(tag, fields) {
8
+ return S.object(s => {
9
+ s.f("TAG", S.literal(tag));
10
+ fields.forEach(param => {
11
+ s.f(param[0], param[1]);
12
+ });
13
+ });
14
+ }
15
+
16
+ let ref_ = StorageRef$Reventless.forStore;
17
+
18
+ globalThis.describe("StorageRefFields.fromEventSchema", () => {
19
+ globalThis.test("finds a declared single-valued ref field and qualifies its store", () => {
20
+ let schema = S.union([variant("ProductImageChanged", [[
21
+ "imageUrl",
22
+ StorageRef$Reventless.forStore(undefined, "productImages")
23
+ ]])]);
24
+ globalThis.expect(StorageRefFields$ReventlessCore.fromEventSchema("Catalog", schema)).toEqual([{
25
+ eventType: "ProductImageChanged",
26
+ fields: [{
27
+ field: "imageUrl",
28
+ arity: "Single",
29
+ store: "Catalog.productImages"
30
+ }]
31
+ }]);
32
+ });
33
+ globalThis.test("a qualified declaration keeps the store it names", () => {
34
+ let schema = S.union([variant("OrderPhotoAttached", [[
35
+ "photo",
36
+ StorageRef$Reventless.forStore("Catalog", "productImages")
37
+ ]])]);
38
+ globalThis.expect(StorageRefFields$ReventlessCore.fromEventSchema("Ordering", schema).flatMap(e => e.fields.map(f => f.store))).toEqual(["Catalog.productImages"]);
39
+ });
40
+ globalThis.test("finds a multi-valued ref field and records its arity", () => {
41
+ let schema = S.union([variant("GalleryReplaced", [[
42
+ "imageUrls",
43
+ S.array(StorageRef$Reventless.forStore(undefined, "productImages"))
44
+ ]])]);
45
+ globalThis.expect(StorageRefFields$ReventlessCore.fromEventSchema("Catalog", schema).flatMap(e => e.fields)).toEqual([{
46
+ field: "imageUrls",
47
+ arity: "Multiple",
48
+ store: "Catalog.productImages"
49
+ }]);
50
+ });
51
+ globalThis.test("a variant with no declared field is omitted entirely", () => {
52
+ let schema = S.union([
53
+ variant("ProductRenamed", [[
54
+ "name",
55
+ S.string
56
+ ]]),
57
+ variant("ProductImageChanged", [[
58
+ "imageUrl",
59
+ StorageRef$Reventless.forStore(undefined, "productImages")
60
+ ]])
61
+ ]);
62
+ globalThis.expect(StorageRefFields$ReventlessCore.fromEventSchema("Catalog", schema).map(e => e.eventType)).toEqual(["ProductImageChanged"]);
63
+ });
64
+ globalThis.test("an event schema with no declaration anywhere yields nothing", () => {
65
+ let schema = S.union([variant("ProductRenamed", [[
66
+ "name",
67
+ S.string
68
+ ]])]);
69
+ globalThis.expect(StorageRefFields$ReventlessCore.fromEventSchema("Catalog", schema)).toEqual([]);
70
+ });
71
+ globalThis.test("an unannotated string field is not treated as a ref", () => {
72
+ let schema = S.union([variant("ProductImported", [[
73
+ "imageUrl",
74
+ S.string
75
+ ]])]);
76
+ globalThis.expect(StorageRefFields$ReventlessCore.fromEventSchema("Catalog", schema)).toEqual([]);
77
+ });
78
+ globalThis.test("a single-variant (non-union) event schema is read the same way", () => {
79
+ let schema = variant("ProductImageChanged", [[
80
+ "imageUrl",
81
+ StorageRef$Reventless.forStore(undefined, "productImages")
82
+ ]]);
83
+ globalThis.expect(StorageRefFields$ReventlessCore.fromEventSchema("Catalog", schema).map(e => e.eventType)).toEqual(["ProductImageChanged"]);
84
+ });
85
+ });
86
+
87
+ globalThis.describe("StorageRefFields.toJson", () => {
88
+ globalThis.test("emits field, arity and store per event type", () => {
89
+ let entries = [
90
+ {
91
+ eventType: "ProductImageChanged",
92
+ fields: [{
93
+ field: "imageUrl",
94
+ arity: "Single",
95
+ store: "Catalog.productImages"
96
+ }]
97
+ },
98
+ {
99
+ eventType: "GalleryReplaced",
100
+ fields: [{
101
+ field: "imageUrls",
102
+ arity: "Multiple",
103
+ store: "Catalog.productImages"
104
+ }]
105
+ }
106
+ ];
107
+ globalThis.expect(JSON.stringify(StorageRefFields$ReventlessCore.toJson(entries))).toBe(`{"ProductImageChanged":[{"field":"imageUrl","arity":"one","store":"Catalog.productImages"}],"GalleryReplaced":[{"field":"imageUrls","arity":"many","store":"Catalog.productImages"}]}`);
108
+ });
109
+ });
110
+
111
+ globalThis.describe("StorageRefFields.storesOf", () => {
112
+ globalThis.test("collects the distinct stores an event log's declarations reach", () => {
113
+ let entries = [
114
+ {
115
+ eventType: "A",
116
+ fields: [{
117
+ field: "a",
118
+ arity: "Single",
119
+ store: "Catalog.productImages"
120
+ }]
121
+ },
122
+ {
123
+ eventType: "B",
124
+ fields: [
125
+ {
126
+ field: "b",
127
+ arity: "Single",
128
+ store: "Catalog.productImages"
129
+ },
130
+ {
131
+ field: "c",
132
+ arity: "Multiple",
133
+ store: "Ordering.receipts"
134
+ }
135
+ ]
136
+ }
137
+ ];
138
+ globalThis.expect(StorageRefFields$ReventlessCore.storesOf(entries)).toEqual([
139
+ "Catalog.productImages",
140
+ "Ordering.receipts"
141
+ ]);
142
+ });
143
+ });
144
+
145
+ let Fields;
146
+
147
+ export {
148
+ Fields,
149
+ variant,
150
+ ref_,
151
+ }
152
+ /* Not a pure module */