@reventlessdev/reventless-spec 3.0.0-alpha.100
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 +931 -0
- package/LICENSE +202 -0
- package/README.md +109 -0
- package/package.json +49 -0
- package/rescript.json +32 -0
- package/run-generator.mjs +2 -0
- package/run-platform-generator.mjs +2 -0
- package/scripts/generate-currency.mjs +215 -0
- package/scripts/iso-4217-list-one.xml +1956 -0
- package/src/AnsiStyle.res +40 -0
- package/src/AnsiStyle.res.mjs +54 -0
- package/src/LogPrefix.res +192 -0
- package/src/LogPrefix.res.mjs +159 -0
- package/src/PackageVersion.res +67 -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/CapabilityManifest.res +74 -0
- package/src/components/CapabilityManifest.res.mjs +61 -0
- package/src/components/ComponentKind.res +99 -0
- package/src/components/ComponentKind.res.mjs +125 -0
- package/src/components/Counter.res +24 -0
- package/src/components/Counter.res.mjs +2 -0
- package/src/components/DcbDecode.res +118 -0
- package/src/components/DcbDecode.res.mjs +100 -0
- package/src/components/DcbScopeInference.res +244 -0
- package/src/components/DcbScopeInference.res.mjs +177 -0
- package/src/components/DcbTag.res +1335 -0
- package/src/components/DcbTag.res.mjs +898 -0
- package/src/components/DcbValidation.res +427 -0
- package/src/components/DcbValidation.res.mjs +423 -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 +85 -0
- package/src/components/InboundTranslationSlice.res.mjs +2 -0
- package/src/components/OutboundTranslationSlice.res +153 -0
- package/src/components/OutboundTranslationSlice.res.mjs +2 -0
- package/src/components/Plugin.res +538 -0
- package/src/components/Plugin.res.mjs +264 -0
- package/src/components/PluginName.res +39 -0
- package/src/components/PluginName.res.mjs +45 -0
- package/src/components/ReadModel.res +199 -0
- package/src/components/ReadModel.res.mjs +18 -0
- package/src/components/Reference.res +55 -0
- package/src/components/Reference.res.mjs +50 -0
- package/src/components/Snapshot.res +26 -0
- package/src/components/Snapshot.res.mjs +2 -0
- package/src/components/StateAnnotations.res +97 -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 +123 -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 +842 -0
- package/src/generator/Codegen.res.mjs +565 -0
- package/src/generator/Config.res +106 -0
- package/src/generator/Config.res.mjs +69 -0
- package/src/generator/Discovery.res +230 -0
- package/src/generator/Discovery.res.mjs +198 -0
- package/src/generator/Generator_Node.res +14 -0
- package/src/generator/Generator_Node.res.mjs +18 -0
- package/src/generator/Pairing.res +460 -0
- package/src/generator/Pairing.res.mjs +415 -0
- package/src/generator/PlatformCodegen.res +207 -0
- package/src/generator/PlatformCodegen.res.mjs +154 -0
- package/src/generator/PlatformGenerator.res +126 -0
- package/src/generator/PlatformGenerator.res.mjs +114 -0
- package/src/generator/PlatformManifests.res +203 -0
- package/src/generator/PlatformManifests.res.mjs +212 -0
- package/src/generator/PluginGenerator.res +57 -0
- package/src/generator/PluginGenerator.res.mjs +73 -0
- package/src/semantic/Bytes.res +54 -0
- package/src/semantic/Bytes.res.mjs +38 -0
- package/src/semantic/Capabilities.res +43 -0
- package/src/semantic/Capabilities.res.mjs +17 -0
- package/src/semantic/Color.res +51 -0
- package/src/semantic/Color.res.mjs +29 -0
- package/src/semantic/Currency.res +598 -0
- package/src/semantic/Currency.res.mjs +743 -0
- package/src/semantic/DateRange.res +148 -0
- package/src/semantic/DateRange.res.mjs +74 -0
- package/src/semantic/Duration.res +53 -0
- package/src/semantic/Duration.res.mjs +26 -0
- package/src/semantic/Email.res +51 -0
- package/src/semantic/Email.res.mjs +31 -0
- package/src/semantic/GeoPoint.res +226 -0
- package/src/semantic/GeoPoint.res.mjs +190 -0
- package/src/semantic/Geocoding.res +127 -0
- package/src/semantic/Geocoding.res.mjs +36 -0
- package/src/semantic/Money.res +196 -0
- package/src/semantic/Money.res.mjs +138 -0
- package/src/semantic/Offload.res +294 -0
- package/src/semantic/Offload.res.mjs +191 -0
- package/src/semantic/Percent.res +53 -0
- package/src/semantic/Percent.res.mjs +33 -0
- package/src/semantic/Phone.res +55 -0
- package/src/semantic/Phone.res.mjs +29 -0
- package/src/semantic/Semantic.res +162 -0
- package/src/semantic/Semantic.res.mjs +95 -0
- package/src/semantic/StorageRef.res +164 -0
- package/src/semantic/StorageRef.res.mjs +111 -0
- package/src/semantic/Url.res +66 -0
- package/src/semantic/Url.res.mjs +48 -0
- package/src/types/Authorization.res +23 -0
- package/src/types/Authorization.res.mjs +33 -0
- package/src/types/Behavior.res +86 -0
- package/src/types/Behavior.res.mjs +2 -0
- package/src/types/DateTime.res +29 -0
- package/src/types/DateTime.res.mjs +16 -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 +326 -0
- package/src/types/Message.res.mjs +186 -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,97 @@
|
|
|
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`), a server-query opt-in annotation
|
|
8
|
+
(`@scan`, `@scanSort`), or a UI-list annotation (`@status`, `@groupBy`).
|
|
9
|
+
Downstream consumers (UI, MCP, codegen) read the
|
|
10
|
+
spec to surface field roles in JSON Schema as `x-reventless-*` extension
|
|
11
|
+
properties.
|
|
12
|
+
|
|
13
|
+
Each list contains the source field names; `indexes` carries `(fieldName,
|
|
14
|
+
indexName)` pairs where `indexName` is `""` for unnamed `@index` annotations.
|
|
15
|
+
`hidden` lists fields the UI should suppress from summary/list views;
|
|
16
|
+
`summary` lists fields the UI should always include in summary/list views.
|
|
17
|
+
`drillTargets` carries `(fieldName, sliceName)` pairs naming the slice/view
|
|
18
|
+
the UI should navigate to instead of expanding the field inline;
|
|
19
|
+
`drillTargetKeys` carries `(fieldName, keyPath)` pairs for fields whose
|
|
20
|
+
drill-down target is keyed by a sub-path within the array element;
|
|
21
|
+
`collapsed` lists object-typed fields the UI should render as an inline
|
|
22
|
+
summary rather than expanding. `scan` lists fields the type author opted
|
|
23
|
+
into server-side equality filtering on (without a backing index); `scanSort`
|
|
24
|
+
lists fields the type author opted into server-side sorting on. The cost is
|
|
25
|
+
free on the in-memory adapter but is `O(n)` Scan + FilterExpression on
|
|
26
|
+
DynamoDB-backed adapters — the annotation is the explicit signal that the
|
|
27
|
+
read model is small enough or the cost is acceptable.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
Declared aggregation for a `@metric`-annotated numeric field. `aggregate` is one
|
|
31
|
+
of `"count"`, `"sum"`, `"avg"` (the UI's dashboard vocabulary); `label` is the
|
|
32
|
+
KPI label, or `""` to let the UI derive one from the field name.
|
|
33
|
+
*/
|
|
34
|
+
type metricSpec = {aggregate: string, label: string}
|
|
35
|
+
|
|
36
|
+
type stateAnnotationSpec = {
|
|
37
|
+
ids: array<string>,
|
|
38
|
+
compositeIds: array<string>,
|
|
39
|
+
subIds: array<string>,
|
|
40
|
+
compositeSubIds: array<string>,
|
|
41
|
+
indexes: array<(string, string)>,
|
|
42
|
+
hidden: array<string>,
|
|
43
|
+
summary: array<string>,
|
|
44
|
+
drillTargets: array<(string, string)>,
|
|
45
|
+
drillTargetKeys: array<(string, string)>,
|
|
46
|
+
collapsed: array<string>,
|
|
47
|
+
scan: array<string>,
|
|
48
|
+
scanSort: array<string>,
|
|
49
|
+
/**
|
|
50
|
+
Fields annotated `@semantic("<id>")` — `(fieldName, semanticId)` pairs. The
|
|
51
|
+
semantic id is the UI's `AutoSemantics` vocabulary (e.g. `"currency"`,
|
|
52
|
+
`"geo-lat"`); the PPX can't validate it (the UI owns that vocabulary).
|
|
53
|
+
`SuryToJsonSchema` emits `x-reventless-semantic` on the named field, which
|
|
54
|
+
AutoUI reads at `#Annotation` provenance — above its heuristic, below a
|
|
55
|
+
`ui-hints.json` `fields:` override.
|
|
56
|
+
*/
|
|
57
|
+
semantic: array<(string, string)>,
|
|
58
|
+
/**
|
|
59
|
+
Fields annotated `@metric(...)` — `(fieldName, metricSpec)` pairs.
|
|
60
|
+
`SuryToJsonSchema` emits `x-reventless-metric: {aggregate, label}` on the named
|
|
61
|
+
field, which AutoUI folds into the dashboard metric list it already builds from
|
|
62
|
+
hints — so a plugin gets a Dashboard page from its schema alone.
|
|
63
|
+
*/
|
|
64
|
+
metric: array<(string, metricSpec)>,
|
|
65
|
+
/**
|
|
66
|
+
Field annotated `@status` on the state record (PPX-emitted). `Some(name)`
|
|
67
|
+
when one such annotation exists; the PPX errors on duplicate `@status`
|
|
68
|
+
annotations within the same record. Codegen consumes this to populate
|
|
69
|
+
`queryableDef.statusField` (with a fallback to a field literally named
|
|
70
|
+
`"status"` when this annotation is absent).
|
|
71
|
+
*/
|
|
72
|
+
status: option<string>,
|
|
73
|
+
/**
|
|
74
|
+
Field annotated `@groupBy` on the state record (PPX-emitted). `Some(name)`
|
|
75
|
+
when one such annotation exists; the PPX errors on duplicate `@groupBy`
|
|
76
|
+
annotations within the same record. `SuryToJsonSchema.deriveObjectSchema`
|
|
77
|
+
emits `x-reventless-group-by: true` on the named field, which the UI's
|
|
78
|
+
list view reads to render rows in sections keyed on that field.
|
|
79
|
+
*/
|
|
80
|
+
groupBy: option<string>,
|
|
81
|
+
/**
|
|
82
|
+
Component-level visibility hint from `@@reventless.visibility(...)`.
|
|
83
|
+
`Some("Internal")` when the file-level attribute is `Internal`; `None`
|
|
84
|
+
(omitted) for the default `Public`. `SuryToJsonSchema.deriveObjectSchema`
|
|
85
|
+
emits `x-reventless-visibility: "Internal"` on the schema when present —
|
|
86
|
+
the default case is omitted to keep schemas compact.
|
|
87
|
+
*/
|
|
88
|
+
visibility: option<string>,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Sury metadata ID used to attach a `stateAnnotationSpec` to a state schema. */
|
|
92
|
+
let stateAnnotationsId: S.Metadata.Id.t<stateAnnotationSpec> =
|
|
93
|
+
S.Metadata.Id.make(~namespace="reventless", ~name="stateAnnotations")
|
|
94
|
+
|
|
95
|
+
/** Returns the spec attached to a state schema, if any. */
|
|
96
|
+
let getSpec = (schema: S.t<unknown>): option<stateAnnotationSpec> =>
|
|
97
|
+
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,123 @@
|
|
|
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 envelope a projection receives for each consumed event.
|
|
86
|
+
|
|
87
|
+
Carries the decoded event alongside the metadata the storage layer already
|
|
88
|
+
holds, so a projection can persist producer time or the acting user without the
|
|
89
|
+
command author having to duplicate that framework state into the event payload.
|
|
90
|
+
|
|
91
|
+
`meta.time` and `recordedAt` are **different clocks** — pick deliberately:
|
|
92
|
+
|
|
93
|
+
- `meta.time` — *producer* time, stamped when the command handler created the
|
|
94
|
+
event. This is the domain-meaningful timestamp (when the order was placed,
|
|
95
|
+
when it shipped); it is identical across every path.
|
|
96
|
+
- `recordedAt` — *storage* time, when the event was appended to the DCB log. On
|
|
97
|
+
the AWS (DynamoDB-stream) path this is the authoritative stored column; on the
|
|
98
|
+
local (topic) path it is stamped at publish, so it may sit a few milliseconds
|
|
99
|
+
after the stored value. Use it for storage-lag diagnostics, not domain dates.
|
|
100
|
+
*/
|
|
101
|
+
type consumed<'e> = {
|
|
102
|
+
event: 'e,
|
|
103
|
+
meta: Message.meta,
|
|
104
|
+
recordedAt: string,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
The Projection — the pure projection function from consumed events to actions.
|
|
109
|
+
*/
|
|
110
|
+
module type Projection = {
|
|
111
|
+
module Spec: Spec
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
Projects one consumed event into read model actions.
|
|
115
|
+
Receives the event wrapped in a `consumed` envelope (event + `meta` +
|
|
116
|
+
`recordedAt`); only events declared in `Spec.consumedEvent` — no wildcard needed.
|
|
117
|
+
*/
|
|
118
|
+
let project: consumed<Spec.consumedEvent> => array<Projection.action<string, Spec.state>>
|
|
119
|
+
|
|
120
|
+
/** File URL of this Projection module (`import.meta.url`). */
|
|
121
|
+
let moduleUrl: string
|
|
122
|
+
}
|
|
123
|
+
|
|
@@ -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
|
+
|