@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.
- package/CHANGELOG.md +625 -0
- package/LICENSE +202 -0
- package/README.md +114 -0
- package/package.json +34 -0
- package/rescript.json +25 -0
- package/run-generator.mjs +2 -0
- package/src/AnsiStyle.res +40 -0
- package/src/AnsiStyle.res.mjs +54 -0
- package/src/LogPrefix.res +168 -0
- package/src/LogPrefix.res.mjs +148 -0
- package/src/PackageVersion.res +73 -0
- package/src/PackageVersion.res.mjs +81 -0
- package/src/components/Aggregate.res +64 -0
- package/src/components/Aggregate.res.mjs +2 -0
- package/src/components/AutomationSlice.res +279 -0
- package/src/components/AutomationSlice.res.mjs +30 -0
- package/src/components/Counter.res +24 -0
- package/src/components/Counter.res.mjs +2 -0
- package/src/components/DcbDecode.res +128 -0
- package/src/components/DcbDecode.res.mjs +117 -0
- package/src/components/DcbTag.res +1256 -0
- package/src/components/DcbTag.res.mjs +869 -0
- package/src/components/DcbValidation.res +362 -0
- package/src/components/DcbValidation.res.mjs +360 -0
- package/src/components/DisplayName.res +40 -0
- package/src/components/DisplayName.res.mjs +26 -0
- package/src/components/ExtensionPoint.res +27 -0
- package/src/components/ExtensionPoint.res.mjs +2 -0
- package/src/components/InboundTranslationSlice.res +79 -0
- package/src/components/InboundTranslationSlice.res.mjs +2 -0
- package/src/components/OutboundTranslationSlice.res +108 -0
- package/src/components/OutboundTranslationSlice.res.mjs +2 -0
- package/src/components/Plugin.res +373 -0
- package/src/components/Plugin.res.mjs +249 -0
- package/src/components/ReadModel.res +199 -0
- package/src/components/ReadModel.res.mjs +18 -0
- package/src/components/Reference.res +51 -0
- package/src/components/Reference.res.mjs +37 -0
- package/src/components/StateAnnotations.res +65 -0
- package/src/components/StateAnnotations.res.mjs +15 -0
- package/src/components/StateChangeSlice.res +131 -0
- package/src/components/StateChangeSlice.res.mjs +2 -0
- package/src/components/StateViewSlice.res +99 -0
- package/src/components/StateViewSlice.res.mjs +2 -0
- package/src/components/Task.res +62 -0
- package/src/components/Task.res.mjs +2 -0
- package/src/generator/Codegen.res +703 -0
- package/src/generator/Codegen.res.mjs +485 -0
- package/src/generator/Config.res +85 -0
- package/src/generator/Config.res.mjs +78 -0
- package/src/generator/Discovery.res +204 -0
- package/src/generator/Discovery.res.mjs +213 -0
- package/src/generator/Generator_Node.res +49 -0
- package/src/generator/Generator_Node.res.mjs +18 -0
- package/src/generator/Pairing.res +386 -0
- package/src/generator/Pairing.res.mjs +353 -0
- package/src/generator/PluginGenerator.res +53 -0
- package/src/generator/PluginGenerator.res.mjs +70 -0
- package/src/types/Authorization.res +23 -0
- package/src/types/Authorization.res.mjs +33 -0
- package/src/types/Behavior.res +78 -0
- package/src/types/Behavior.res.mjs +2 -0
- package/src/types/EventMapping.res +100 -0
- package/src/types/EventMapping.res.mjs +15 -0
- package/src/types/Handler.res +30 -0
- package/src/types/Handler.res.mjs +2 -0
- package/src/types/Id.res +75 -0
- package/src/types/Id.res.mjs +37 -0
- package/src/types/Identity.res +46 -0
- package/src/types/Identity.res.mjs +51 -0
- package/src/types/Message.res +197 -0
- package/src/types/Message.res.mjs +108 -0
- package/src/types/Projection.res +220 -0
- package/src/types/Projection.res.mjs +44 -0
- package/src/types/QueryEngine.res +123 -0
- package/src/types/QueryEngine.res.mjs +12 -0
- package/src/types/ReadConsistency.res +38 -0
- package/src/types/ReadConsistency.res.mjs +29 -0
- package/src/types/Schedule.res +65 -0
- package/src/types/Schedule.res.mjs +68 -0
- package/src/types/SideEffect.res +45 -0
- package/src/types/SideEffect.res.mjs +2 -0
- package/src/types/StoredEvent.res +46 -0
- package/src/types/StoredEvent.res.mjs +32 -0
- package/src/types/Visibility.res +24 -0
- 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,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,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
|
+
|