@reventlessdev/reventless-spec 3.0.0-alpha.61

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.
Files changed (86) hide show
  1. package/CHANGELOG.md +625 -0
  2. package/LICENSE +202 -0
  3. package/README.md +114 -0
  4. package/package.json +34 -0
  5. package/rescript.json +25 -0
  6. package/run-generator.mjs +2 -0
  7. package/src/AnsiStyle.res +40 -0
  8. package/src/AnsiStyle.res.mjs +54 -0
  9. package/src/LogPrefix.res +168 -0
  10. package/src/LogPrefix.res.mjs +148 -0
  11. package/src/PackageVersion.res +73 -0
  12. package/src/PackageVersion.res.mjs +81 -0
  13. package/src/components/Aggregate.res +64 -0
  14. package/src/components/Aggregate.res.mjs +2 -0
  15. package/src/components/AutomationSlice.res +279 -0
  16. package/src/components/AutomationSlice.res.mjs +30 -0
  17. package/src/components/Counter.res +24 -0
  18. package/src/components/Counter.res.mjs +2 -0
  19. package/src/components/DcbDecode.res +128 -0
  20. package/src/components/DcbDecode.res.mjs +117 -0
  21. package/src/components/DcbTag.res +1256 -0
  22. package/src/components/DcbTag.res.mjs +869 -0
  23. package/src/components/DcbValidation.res +362 -0
  24. package/src/components/DcbValidation.res.mjs +360 -0
  25. package/src/components/DisplayName.res +40 -0
  26. package/src/components/DisplayName.res.mjs +26 -0
  27. package/src/components/ExtensionPoint.res +27 -0
  28. package/src/components/ExtensionPoint.res.mjs +2 -0
  29. package/src/components/InboundTranslationSlice.res +79 -0
  30. package/src/components/InboundTranslationSlice.res.mjs +2 -0
  31. package/src/components/OutboundTranslationSlice.res +108 -0
  32. package/src/components/OutboundTranslationSlice.res.mjs +2 -0
  33. package/src/components/Plugin.res +373 -0
  34. package/src/components/Plugin.res.mjs +249 -0
  35. package/src/components/ReadModel.res +199 -0
  36. package/src/components/ReadModel.res.mjs +18 -0
  37. package/src/components/Reference.res +51 -0
  38. package/src/components/Reference.res.mjs +37 -0
  39. package/src/components/StateAnnotations.res +65 -0
  40. package/src/components/StateAnnotations.res.mjs +15 -0
  41. package/src/components/StateChangeSlice.res +131 -0
  42. package/src/components/StateChangeSlice.res.mjs +2 -0
  43. package/src/components/StateViewSlice.res +99 -0
  44. package/src/components/StateViewSlice.res.mjs +2 -0
  45. package/src/components/Task.res +62 -0
  46. package/src/components/Task.res.mjs +2 -0
  47. package/src/generator/Codegen.res +703 -0
  48. package/src/generator/Codegen.res.mjs +485 -0
  49. package/src/generator/Config.res +85 -0
  50. package/src/generator/Config.res.mjs +78 -0
  51. package/src/generator/Discovery.res +204 -0
  52. package/src/generator/Discovery.res.mjs +213 -0
  53. package/src/generator/Generator_Node.res +49 -0
  54. package/src/generator/Generator_Node.res.mjs +18 -0
  55. package/src/generator/Pairing.res +386 -0
  56. package/src/generator/Pairing.res.mjs +353 -0
  57. package/src/generator/PluginGenerator.res +53 -0
  58. package/src/generator/PluginGenerator.res.mjs +70 -0
  59. package/src/types/Authorization.res +23 -0
  60. package/src/types/Authorization.res.mjs +33 -0
  61. package/src/types/Behavior.res +78 -0
  62. package/src/types/Behavior.res.mjs +2 -0
  63. package/src/types/EventMapping.res +100 -0
  64. package/src/types/EventMapping.res.mjs +15 -0
  65. package/src/types/Handler.res +30 -0
  66. package/src/types/Handler.res.mjs +2 -0
  67. package/src/types/Id.res +75 -0
  68. package/src/types/Id.res.mjs +37 -0
  69. package/src/types/Identity.res +46 -0
  70. package/src/types/Identity.res.mjs +51 -0
  71. package/src/types/Message.res +197 -0
  72. package/src/types/Message.res.mjs +108 -0
  73. package/src/types/Projection.res +220 -0
  74. package/src/types/Projection.res.mjs +44 -0
  75. package/src/types/QueryEngine.res +123 -0
  76. package/src/types/QueryEngine.res.mjs +12 -0
  77. package/src/types/ReadConsistency.res +38 -0
  78. package/src/types/ReadConsistency.res.mjs +29 -0
  79. package/src/types/Schedule.res +65 -0
  80. package/src/types/Schedule.res.mjs +68 -0
  81. package/src/types/SideEffect.res +45 -0
  82. package/src/types/SideEffect.res.mjs +2 -0
  83. package/src/types/StoredEvent.res +46 -0
  84. package/src/types/StoredEvent.res.mjs +32 -0
  85. package/src/types/Visibility.res +24 -0
  86. package/src/types/Visibility.res.mjs +25 -0
@@ -0,0 +1,199 @@
1
+ /**
2
+ Specifies how the sub-ID of a projected state row is supplied at query time.
3
+
4
+ - `Field(name)` — the sub-ID comes from a field in the query response
5
+ - `Argument(name)` — the sub-ID is passed as a GraphQL resolver argument
6
+ - `NoSubId` — the read model has no sub-ID dimension
7
+ */
8
+ type subId =
9
+ | Field(string)
10
+ | Argument(string)
11
+ | NoSubId
12
+
13
+ /**
14
+ Whether the GraphQL resolver field resolves to a single value or to a list.
15
+
16
+ - `Single(fieldName)` — returns one item by ID
17
+ - `Multi(fieldName)` — returns an array of items
18
+ */
19
+ type resolvedField =
20
+ | Single(string)
21
+ | Multi(string)
22
+
23
+ /** Source configuration for a single-ID GraphQL resolver. */
24
+ type idResolverSourceConfig = {
25
+ idField: string,
26
+ subId: subId,
27
+ resolvedField: resolvedField,
28
+ }
29
+
30
+ /**
31
+ Specifies which DynamoDB table field serves as the primary key in the resolver target.
32
+
33
+ - `Index(indexName)` — resolve against an index using only the index ID
34
+ - `IndexWithId(indexName, idField)` — resolve against an index with an explicit ID field
35
+ - `Id` — resolve against the table's primary key
36
+ */
37
+ type targetIdField =
38
+ | Index(string)
39
+ | IndexWithId(string, string)
40
+ | Id
41
+
42
+ /** Target configuration for a single-ID GraphQL resolver. */
43
+ type idResolverTargetConfig = {
44
+ pluginName?: string,
45
+ tableName: string,
46
+ idField: targetIdField,
47
+ subIdField?: string,
48
+ }
49
+
50
+ type resolveConfig<'source, 'target> = {
51
+ source: 'source,
52
+ target: 'target,
53
+ }
54
+
55
+ /** Configuration for a GraphQL resolver that resolves a single ID field. */
56
+ type idResolverConfig = resolveConfig<idResolverSourceConfig, idResolverTargetConfig>
57
+
58
+ /** Source configuration for a multi-ID (array) GraphQL resolver. */
59
+ type idsResolverSourceConfig = {
60
+ idsField: string,
61
+ resolvedField: string,
62
+ }
63
+
64
+ /** Target configuration for a multi-ID (array) GraphQL resolver. */
65
+ type idsResolverTargetConfig = {
66
+ pluginName?: string,
67
+ tableName: string,
68
+ subIdField?: string,
69
+ }
70
+
71
+ /** Configuration for a GraphQL resolver that resolves an array of ID fields. */
72
+ type idsResolverConfig = resolveConfig<idsResolverSourceConfig, idsResolverTargetConfig>
73
+
74
+ /** AppSync authorization rule associating a DynamoDB table with a Cognito group. */
75
+ type authorization = {
76
+ tableName: string,
77
+ group: string,
78
+ }
79
+
80
+ /** How much of a DynamoDB global secondary index is projected into the index. */
81
+ type projectionType = KEYS_ONLY | ALL | INCLUDE(array<string>)
82
+
83
+ /**
84
+ Configuration for a DynamoDB Global Secondary Index on a read model table.
85
+
86
+ - `index` — the index name
87
+ - `type_` — the DynamoDB attribute type of the index key (`"S"`, `"N"`, etc.)
88
+ - `idField` / `subIdField` — index key field overrides (set to synthetic `_name_pk`/`_name_sk` for composite keys)
89
+ - `pkFields` / `skFields` — source state field names for composite pk/sk; runtime uses these to compute synthetic attribute values
90
+ - `pkSep` / `skSep` — separator for composite pk/sk concatenation (default `"/"`)
91
+ - `projectionType` — which attributes are projected into the index
92
+ - `authorization` — optional AppSync authorization rule
93
+ */
94
+ type indexConfig = {
95
+ index: string,
96
+ type_: string,
97
+ idField?: string,
98
+ subIdField?: string,
99
+ pkFields?: array<string>,
100
+ pkSep?: string,
101
+ skFields?: array<string>,
102
+ skSep?: string,
103
+ projectionType: projectionType,
104
+ authorization?: authorization,
105
+ }
106
+
107
+ /**
108
+ Composite-key configuration for a read model that has both a primary ID and a sub-ID.
109
+
110
+ - `subIdField` — the DynamoDB range-key attribute name
111
+ - `getSubId` — extracts the sub-ID string from a projected state value
112
+ */
113
+ type subIdConfig<'state> = {
114
+ subIdField: string,
115
+ getSubId: 'state => string,
116
+ }
117
+
118
+ /**
119
+ Infrastructure configuration for a read model.
120
+
121
+ - `idResolvers` — GraphQL resolvers for single-ID lookups
122
+ - `idsResolvers` — GraphQL resolvers for multi-ID (array) lookups
123
+ - `indexes` — additional DynamoDB global secondary indexes
124
+
125
+ Use the `config` factory function to build this with defaults.
126
+ */
127
+ type config = {
128
+ idResolvers: array<idResolverConfig>,
129
+ idsResolvers: array<idsResolverConfig>,
130
+ indexes: array<indexConfig>,
131
+ }
132
+
133
+ /**
134
+ Builds a `ReadModel.config` with all optional fields defaulting to empty arrays.
135
+
136
+ @example
137
+ ```rescript
138
+ // CategoriesReadModel.res
139
+ let config = ReadModel.config()
140
+ ```
141
+ */
142
+ let config = (~idResolvers=[], ~idsResolvers=[], ~indexes=[]) => {
143
+ idResolvers,
144
+ idsResolvers,
145
+ indexes,
146
+ }
147
+
148
+ /**
149
+ Module type for a read model's identity and schema specification.
150
+
151
+ @example
152
+ ```rescript
153
+ // CategoriesReadModel.res
154
+ module Id = Id.String
155
+ let name = "Categories"
156
+
157
+ @schema
158
+ type state = {categoryId: string, name: string, archived: bool}
159
+
160
+ let config = ReadModel.config()
161
+ let subIdConfig = None
162
+ ```
163
+ */
164
+ module type Spec = {
165
+ module Id: Id.T
166
+
167
+ /** Logical read model name, used as the DynamoDB table-name prefix. */
168
+ let name: string
169
+ let moduleUrl: string
170
+
171
+ /** The projected state type stored in the read model. Must carry `@schema`. */
172
+ @schema
173
+ type state
174
+
175
+ /** Sury schema for the state type — generated automatically by `@schema`. */
176
+ let stateSchema: S.t<state>
177
+
178
+ /** Infrastructure configuration (indexes, resolvers). */
179
+ let config: config
180
+
181
+ /** Optional composite-key configuration. `None` for single-key tables. */
182
+ let subIdConfig: option<subIdConfig<state>>
183
+
184
+ /** Authorization rule evaluated at the GraphQL resolver entry before any
185
+ query is resolved. Auto-injected by `@@reventless.spec` and on
186
+ structurally-detected inline spec modules — defaults to
187
+ `AllowAuthenticated`; override at the file/module level with
188
+ `@@reventless.authorize(<rule>)`. */
189
+ let authorization: Authorization.permission
190
+
191
+ /** AutoUI visibility hint. Auto-injected by `@@reventless.spec` and on
192
+ structurally-detected inline spec modules — defaults to
193
+ `Visibility.Public`; override at the file/module level with
194
+ `@@reventless.visibility(Internal)` to hide from the AutoUI manifest.
195
+ Does not affect GraphQL exposure, authorization, or resolver
196
+ provisioning. */
197
+ let visibility: Visibility.t
198
+ }
199
+
@@ -0,0 +1,18 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+
4
+ function config(idResolversOpt, idsResolversOpt, indexesOpt) {
5
+ let idResolvers = idResolversOpt !== undefined ? idResolversOpt : [];
6
+ let idsResolvers = idsResolversOpt !== undefined ? idsResolversOpt : [];
7
+ let indexes = indexesOpt !== undefined ? indexesOpt : [];
8
+ return {
9
+ idResolvers: idResolvers,
10
+ idsResolvers: idsResolvers,
11
+ indexes: indexes
12
+ };
13
+ }
14
+
15
+ export {
16
+ config,
17
+ }
18
+ /* No side effect */
@@ -0,0 +1,51 @@
1
+ /** Identifies which entity a reference field points to. */
2
+ type target = {entity: string, plugin: option<string>}
3
+
4
+ /** Sury metadata ID for entity reference annotation. */
5
+ let referenceId: S.Metadata.Id.t<target> = S.Metadata.Id.make(~namespace="reventless", ~name="reference")
6
+
7
+ /**
8
+ A sury string schema annotated as an entity reference field.
9
+
10
+ Use with `@s.matches(Reference.to_("EntityName"))` on command/event fields that
11
+ carry a foreign-entity ID. Also implies DCB tag semantics so the field is
12
+ automatically queryable in DCB event logs.
13
+
14
+ Pass `~key` to override the DCB tag key (defaults to the field name). The
15
+ `@ref` ppx shorthand supplies it automatically for plural `*Ids: array<string>`
16
+ fields (singularizing, e.g. `productIds` → tag key `productId`) so they share a
17
+ tag key with their singular-named producer events.
18
+
19
+ Prefer the `@ref("EntityName")` ppx shorthand over writing `@s.matches(...)` by hand.
20
+
21
+ @example
22
+ ```rescript
23
+ @schema type command =
24
+ | PlaceOrder({
25
+ orderId: @s.matches(DcbTag.string) string,
26
+ customerId: @s.matches(Reference.to_("Customer")) string,
27
+ })
28
+ ```
29
+ */
30
+ let to_ = (~plugin=?, ~key=?, entity: string): S.t<string> => {
31
+ let base =
32
+ S.string
33
+ ->S.Metadata.set(~id=DcbTag.dcbTagId, true)
34
+ ->S.Metadata.set(~id=referenceId, {entity, plugin})
35
+ switch key {
36
+ | Some(k) => base->S.Metadata.set(~id=DcbTag.dcbTagKeyOverrideId, k)
37
+ | None => base
38
+ }
39
+ }
40
+
41
+ /** Returns the reference target if the schema carries `Reference.to_(...)` metadata. */
42
+ let getTarget = (schema: S.t<unknown>): option<target> =>
43
+ S.Metadata.get(schema, ~id=referenceId)
44
+
45
+ /**
46
+ Like `to_` but does not imply DCB tag semantics.
47
+ Use with `@ref("Entity") @noDcbTag` when the field references another entity
48
+ but should not participate in content-based event routing.
49
+ */
50
+ let toWithoutDcbTag = (~plugin=?, entity: string): S.t<string> =>
51
+ S.string->S.Metadata.set(~id=referenceId, {entity, plugin})
@@ -0,0 +1,37 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as DcbTag$Reventless from "./DcbTag.res.mjs";
5
+
6
+ let referenceId = S.Metadata.Id.make("reventless", "reference");
7
+
8
+ function to_(plugin, key, entity) {
9
+ let base = S.Metadata.set(S.Metadata.set(S.string, DcbTag$Reventless.dcbTagId, true), referenceId, {
10
+ entity: entity,
11
+ plugin: plugin
12
+ });
13
+ if (key !== undefined) {
14
+ return S.Metadata.set(base, DcbTag$Reventless.dcbTagKeyOverrideId, key);
15
+ } else {
16
+ return base;
17
+ }
18
+ }
19
+
20
+ function getTarget(schema) {
21
+ return S.Metadata.get(schema, referenceId);
22
+ }
23
+
24
+ function toWithoutDcbTag(plugin, entity) {
25
+ return S.Metadata.set(S.string, referenceId, {
26
+ entity: entity,
27
+ plugin: plugin
28
+ });
29
+ }
30
+
31
+ export {
32
+ referenceId,
33
+ to_,
34
+ getTarget,
35
+ toWithoutDcbTag,
36
+ }
37
+ /* referenceId Not a pure module */
@@ -0,0 +1,65 @@
1
+ /**
2
+ Spec describing the structural annotations declared on the fields of an
3
+ `@schema type state` record. The ppx attaches one of these to the generated
4
+ `stateSchema` whenever a field carries a structural annotation (`@id`,
5
+ `@compositeId`, `@subId`, `@compositeSubId`, `@index`), a visibility
6
+ annotation (`@hidden`, `@summary`), a hierarchical-rendering annotation
7
+ (`@drillTarget`, `@collapsed`), or a server-query opt-in annotation
8
+ (`@scan`, `@scanSort`). Downstream consumers (UI, MCP, codegen) read the
9
+ spec to surface field roles in JSON Schema as `x-reventless-*` extension
10
+ properties.
11
+
12
+ Each list contains the source field names; `indexes` carries `(fieldName,
13
+ indexName)` pairs where `indexName` is `""` for unnamed `@index` annotations.
14
+ `hidden` lists fields the UI should suppress from summary/list views;
15
+ `summary` lists fields the UI should always include in summary/list views.
16
+ `drillTargets` carries `(fieldName, sliceName)` pairs naming the slice/view
17
+ the UI should navigate to instead of expanding the field inline;
18
+ `drillTargetKeys` carries `(fieldName, keyPath)` pairs for fields whose
19
+ drill-down target is keyed by a sub-path within the array element;
20
+ `collapsed` lists object-typed fields the UI should render as an inline
21
+ summary rather than expanding. `scan` lists fields the type author opted
22
+ into server-side equality filtering on (without a backing index); `scanSort`
23
+ lists fields the type author opted into server-side sorting on. The cost is
24
+ free on the in-memory adapter but is `O(n)` Scan + FilterExpression on
25
+ DynamoDB-backed adapters — the annotation is the explicit signal that the
26
+ read model is small enough or the cost is acceptable.
27
+ */
28
+ type stateAnnotationSpec = {
29
+ ids: array<string>,
30
+ compositeIds: array<string>,
31
+ subIds: array<string>,
32
+ compositeSubIds: array<string>,
33
+ indexes: array<(string, string)>,
34
+ hidden: array<string>,
35
+ summary: array<string>,
36
+ drillTargets: array<(string, string)>,
37
+ drillTargetKeys: array<(string, string)>,
38
+ collapsed: array<string>,
39
+ scan: array<string>,
40
+ scanSort: array<string>,
41
+ /**
42
+ Field annotated `@status` on the state record (PPX-emitted). `Some(name)`
43
+ when one such annotation exists; the PPX errors on duplicate `@status`
44
+ annotations within the same record. Codegen consumes this to populate
45
+ `queryableDef.statusField` (with a fallback to a field literally named
46
+ `"status"` when this annotation is absent).
47
+ */
48
+ status: option<string>,
49
+ /**
50
+ Component-level visibility hint from `@@reventless.visibility(...)`.
51
+ `Some("Internal")` when the file-level attribute is `Internal`; `None`
52
+ (omitted) for the default `Public`. `SuryToJsonSchema.deriveObjectSchema`
53
+ emits `x-reventless-visibility: "Internal"` on the schema when present —
54
+ the default case is omitted to keep schemas compact.
55
+ */
56
+ visibility: option<string>,
57
+ }
58
+
59
+ /** Sury metadata ID used to attach a `stateAnnotationSpec` to a state schema. */
60
+ let stateAnnotationsId: S.Metadata.Id.t<stateAnnotationSpec> =
61
+ S.Metadata.Id.make(~namespace="reventless", ~name="stateAnnotations")
62
+
63
+ /** Returns the spec attached to a state schema, if any. */
64
+ let getSpec = (schema: S.t<unknown>): option<stateAnnotationSpec> =>
65
+ S.Metadata.get(schema, ~id=stateAnnotationsId)
@@ -0,0 +1,15 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+
5
+ let stateAnnotationsId = S.Metadata.Id.make("reventless", "stateAnnotations");
6
+
7
+ function getSpec(schema) {
8
+ return S.Metadata.get(schema, stateAnnotationsId);
9
+ }
10
+
11
+ export {
12
+ stateAnnotationsId,
13
+ getSpec,
14
+ }
15
+ /* stateAnnotationsId Not a pure module */
@@ -0,0 +1,131 @@
1
+ /**
2
+ Module types for a DCB write-side state change slice.
3
+
4
+ A `StateChangeSlice` is the DCB equivalent of an aggregate: it processes
5
+ commands by reading the relevant events from a shared `DcbEventLog`, building
6
+ a `state` (ephemeral read model), and appending new events conditioned
7
+ on no concurrent changes to the same entities.
8
+
9
+ Plan 02 splits the merged spec into two module types:
10
+
11
+ - `Spec` — types, identity, schemas. The structural contract.
12
+ - `Behavior` — `state`, `initialState`, `evolve`, `decide`. The state machine.
13
+
14
+ @example
15
+ ```rescript
16
+ // AddCategory.res
17
+ let name = "AddCategory"
18
+
19
+ type state = {exists: bool, archived: bool}
20
+ let initialState = {exists: false, archived: false}
21
+
22
+ @schema type consumedEvent =
23
+ | CategoryAdded
24
+ | CategoryArchived
25
+
26
+ let evolve = (state, event) => switch event {
27
+ | CategoryAdded => {exists: true, archived: false}
28
+ | CategoryArchived => {...state, archived: true}
29
+ }
30
+
31
+ @schema type command = AddCategory({categoryId: @s.matches(DcbTag.string) string, name: string})
32
+ @schema type error = CategoryAlreadyExists
33
+
34
+ @schema type event =
35
+ | CategoryAdded({categoryId: @s.matches(DcbTag.string) string, name: string})
36
+
37
+ let decide = (state, command) => switch command {
38
+ | AddCategory({categoryId, name}) =>
39
+ if state.exists { Error(CategoryAlreadyExists) }
40
+ else { Ok([CategoryAdded({categoryId, name})]) }
41
+ }
42
+ ```
43
+ */
44
+
45
+ /**
46
+ The lean Spec for a StateChangeSlice — types, identity, schemas. State and
47
+ state-evolution functions live in the sibling `Behavior` module type.
48
+ */
49
+ module type Spec = {
50
+ /** Logical name of this slice (used as a command topic prefix). */
51
+ let name: string
52
+ let moduleUrl: string
53
+
54
+ /** Identity type — always `Id.String` for DCB slices. */
55
+ module Id: Id.T
56
+
57
+ /**
58
+ Events this slice reads to build its decision model (in `evolve`).
59
+ Only needs the fields required for the decision — no tag annotations needed.
60
+ May be payload-less for events where only existence matters.
61
+ Must carry `@schema`.
62
+ */
63
+ @schema
64
+ type consumedEvent
65
+
66
+ /** Commands this slice handles. Must carry `@schema`. */
67
+ @schema
68
+ type command
69
+
70
+ /** Business rule violation errors. Must carry `@schema`. */
71
+ @schema
72
+ type error
73
+
74
+ /** Events this slice emits (from `decide`). Must carry `@schema` and include tag annotations. */
75
+ @schema
76
+ type event
77
+
78
+ /** Schema for the command type — used to extract DCB tags for the conditional read. */
79
+ let commandSchema: S.t<command>
80
+
81
+ /** Authorization rule evaluated at the GraphQL resolver entry before any
82
+ command is dispatched. Auto-injected by `@@reventless.spec` and on
83
+ structurally-detected inline spec modules — defaults to
84
+ `AllowAuthenticated`; override at the file/module level with
85
+ `@@reventless.authorize(<rule>)`. */
86
+ let commandAuthorization: command => Authorization.permission
87
+
88
+ /** Decision-read consistency mode for this slice's optimistic-concurrency
89
+ retry loop. Auto-injected by `@@reventless.spec` and on
90
+ structurally-detected inline spec modules — defaults to
91
+ `EscalateOnRetry` (eventual first, strong on retry); override at the
92
+ file/module level with `@@reventless.consistency(AlwaysStrong)` (or
93
+ `AlwaysEventual`). Affects RCU/latency only, never correctness — the
94
+ conditional append's fence is always evaluated strongly. */
95
+ let readConsistency: ReadConsistency.t
96
+ }
97
+
98
+ /**
99
+ The Behavior — pure state machine that decides commands and folds events.
100
+
101
+ `module Spec: Spec` shares the lean Spec's types so `evolve` references
102
+ `Spec.consumedEvent`, `decide` references `Spec.command`/`Spec.event`/`Spec.error`.
103
+ */
104
+ module type Behavior = {
105
+ module Spec: Spec
106
+
107
+ /**
108
+ The ephemeral state built by replaying relevant events.
109
+ Not persisted — reconstructed for each command by reading from the DCB log.
110
+ */
111
+ type state
112
+
113
+ /** The initial (empty) state before any events have been applied. */
114
+ let initialState: state
115
+
116
+ /**
117
+ Folds one consumed event into the state during the read phase.
118
+ Must be a pure function — no side effects.
119
+ */
120
+ let evolve: (state, Spec.consumedEvent) => state
121
+
122
+ /**
123
+ Decides what events to append given the current state and the command.
124
+ Return `Ok(events)` to append, or `Error(error)` to reject the command.
125
+ */
126
+ let decide: (state, Spec.command) => result<array<Spec.event>, Spec.error>
127
+
128
+ /** File URL of this Behavior module (`import.meta.url`). */
129
+ let moduleUrl: string
130
+ }
131
+
@@ -0,0 +1,2 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+ /* This output is empty. Its source's type definitions, externals and/or unused code got optimized away. */
@@ -0,0 +1,99 @@
1
+ /**
2
+ Module types for a DCB read-side state view slice.
3
+
4
+ A `StateViewSlice` is the DCB equivalent of a `ReadModel`: it listens to the
5
+ shared `DcbEventLog` event topic and projects events into a queryable state table
6
+ using the `Projection.action` algebra.
7
+
8
+ Plan 02 splits the merged spec into two module types:
9
+
10
+ - `Spec` — types, identity, schemas, infrastructure config. The persisted
11
+ state contract (matches the ReadModel convention where `state` is in Spec).
12
+ - `Projection` — the single `project` function (and any future projection
13
+ helpers).
14
+
15
+ @example
16
+ ```rescript
17
+ // CategoriesView.res
18
+ let name = "CategoriesView"
19
+
20
+ @schema type state = {categoryId: string, name: string, archived: bool}
21
+
22
+ @schema type consumedEvent =
23
+ | CategoryAdded({categoryId: string, name: string})
24
+ | CategoryRenamed({categoryId: string, name: string})
25
+ | CategoryArchived({categoryId: string})
26
+
27
+ let project = event => switch event {
28
+ | CategoryAdded({categoryId, name}) =>
29
+ [Set(categoryId, {categoryId, name, archived: false})]
30
+ | CategoryRenamed({categoryId, name}) =>
31
+ [Update(categoryId, state => {...state, name})]
32
+ | CategoryArchived({categoryId}) =>
33
+ [Update(categoryId, state => {...state, archived: true})]
34
+ }
35
+ ```
36
+ */
37
+
38
+ /**
39
+ The lean Spec for a StateViewSlice — types, identity, schemas, infra config.
40
+ Per D2, `state` lives here (not in `Projection`) because the projected state
41
+ is the externally-observable contract — schema queried by GraphQL resolvers.
42
+ */
43
+ module type Spec = {
44
+ /** Logical name of this view slice (used as a DynamoDB table prefix). */
45
+ let name: string
46
+ let moduleUrl: string
47
+
48
+ /** The projected state type stored in the view table. Must carry `@schema`. */
49
+ @schema
50
+ type state
51
+
52
+ /** Sury schema for the state type — generated automatically by `@schema`. */
53
+ let stateSchema: S.t<state>
54
+
55
+ /**
56
+ Events this view slice projects. Only needs the fields required for the projection —
57
+ no tag annotations needed. May be payload-less where only existence matters.
58
+ Must carry `@schema`.
59
+ */
60
+ @schema
61
+ type consumedEvent
62
+
63
+ /** Infrastructure configuration (indexes, resolvers). */
64
+ let config: ReadModel.config
65
+
66
+ /** Optional composite-key configuration. `None` for single-key tables. */
67
+ let subIdConfig: option<ReadModel.subIdConfig<state>>
68
+
69
+ /** Authorization rule evaluated at the GraphQL resolver entry before any
70
+ query is resolved. Auto-injected by `@@reventless.spec` and on
71
+ structurally-detected inline spec modules — defaults to
72
+ `AllowAuthenticated`. */
73
+ let authorization: Authorization.permission
74
+
75
+ /** AutoUI visibility hint. Auto-injected by `@@reventless.spec` and on
76
+ structurally-detected inline spec modules — defaults to
77
+ `Visibility.Public`; override at the file/module level with
78
+ `@@reventless.visibility(Internal)` to hide from the AutoUI manifest.
79
+ Does not affect GraphQL exposure, authorization, or resolver
80
+ provisioning. */
81
+ let visibility: Visibility.t
82
+ }
83
+
84
+ /**
85
+ The Projection — the pure projection function from consumed events to actions.
86
+ */
87
+ module type Projection = {
88
+ module Spec: Spec
89
+
90
+ /**
91
+ Projects one consumed event into read model actions.
92
+ Receives only events declared in `Spec.consumedEvent` — no wildcard needed.
93
+ */
94
+ let project: Spec.consumedEvent => array<Projection.action<string, Spec.state>>
95
+
96
+ /** File URL of this Projection module (`import.meta.url`). */
97
+ let moduleUrl: string
98
+ }
99
+
@@ -0,0 +1,2 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+ /* This output is empty. Its source's type definitions, externals and/or unused code got optimized away. */
@@ -0,0 +1,62 @@
1
+ /**
2
+ Derives the name used to query a task's S3 bucket.
3
+
4
+ - `~taskName` — the task's logical name
5
+ - `~bucketName` — optional override; defaults to the task's configured bucket name
6
+ */
7
+ type queryBucketName = (~taskName: string, ~bucketName: string=?) => string
8
+
9
+ /**
10
+ An action that a task handler can return after processing a trigger.
11
+
12
+ - `PublishCommands(aggregateName, commands)` — publish commands to an aggregate
13
+ - `CreateSchedule(schedule)` — create a new recurring or one-time schedule
14
+ - `DeleteSchedule(name)` — delete a schedule by name
15
+ */
16
+ type taskAction =
17
+ | PublishCommands(string, array<Message.commandJson>)
18
+ | CreateSchedule(Schedule.schedule)
19
+ | DeleteSchedule(string)
20
+
21
+ /**
22
+ A callback invoked when an S3 object event occurs on a task bucket.
23
+
24
+ - `~eventName` — the S3 event type (e.g. `"ObjectCreated:Put"`)
25
+ - `~key` — the S3 object key that triggered the event
26
+
27
+ Returns an array of `taskAction` values to execute after the callback completes.
28
+ */
29
+ type bucketCallback = (~eventName: string, ~key: string) => promise<array<taskAction>>
30
+
31
+ /**
32
+ The access mode a task needs for one of its S3 buckets.
33
+
34
+ - `Read` — task reads from the bucket (e.g. to import catalog data)
35
+ - `Write` — task writes to the bucket (e.g. to export a report)
36
+ - `ReadWrite` — task reads and writes
37
+ */
38
+ type bucketMode = Read | Write | ReadWrite
39
+
40
+ /**
41
+ Configuration for one S3 bucket used by a task.
42
+
43
+ - `bucketName` — optional override; if absent, the framework derives a name
44
+ - `bucketMode` — the required access level
45
+ - `callback` — optional handler triggered by S3 object events
46
+ */
47
+ type bucketSpec = {bucketName?: string, bucketMode: bucketMode, callback?: bucketCallback}
48
+
49
+ /** An array of `SideEffect.T` modules executed when this task fires. */
50
+ type sideEffects = array<module(SideEffect.T)>
51
+
52
+ /**
53
+ The runtime configuration returned by a task's `setup` function.
54
+
55
+ - `buckets` — optional list of S3 bucket specifications
56
+ - `sideEffects` — optional list of side effect modules to execute
57
+ */
58
+ type config = {
59
+ buckets?: array<bucketSpec>,
60
+ sideEffects?: sideEffects,
61
+ }
62
+
@@ -0,0 +1,2 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+ /* This output is empty. Its source's type definitions, externals and/or unused code got optimized away. */