@reventlessdev/reventless-core 3.0.0-alpha.212 → 3.0.0-alpha.214
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 +19 -0
- package/package.json +6 -6
- package/src/Projection.res +5 -2
- package/src/Projection.res.mjs +2 -1
- package/src/adapter/EventLogProvisioning/EventLogProvisioning.res +165 -0
- package/src/adapter/EventLogProvisioning/EventLogProvisioning.res.mjs +49 -0
- package/src/components/AutomationSlice/AutomationSlice_Callback.res +30 -0
- package/src/components/AutomationSlice/AutomationSlice_Callback.res.mjs +12 -0
- package/src/components/Dcb/Dcb_Builder.res +6 -2
- package/src/components/Dcb/Dcb_Builder.res.mjs +2 -2
- package/src/components/DcbEventLog/DcbEventLog_Builder.res +17 -4
- package/src/components/DcbEventLog/DcbEventLog_Builder.res.mjs +9 -5
- package/src/components/EventLog/EventLog_Builder.res +10 -2
- package/src/components/EventLog/EventLog_Builder.res.mjs +4 -1
- package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Callback.res +32 -0
- package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Callback.res.mjs +13 -0
- package/src/plugin/component/Plugin_Builder.res +6 -13
- package/src/plugin/component/Plugin_Builder.res.mjs +2 -16
- package/src/plugin/component/Plugin_Helpers.res +21 -3
- package/src/plugin/component/Plugin_Helpers.res.mjs +16 -0
- package/src/util/LogFormat.res +52 -1
- package/src/util/LogFormat.res.mjs +21 -2
- package/src/util/StoreLayout.res +38 -0
- package/src/util/StoreLayout.res.mjs +22 -0
- package/tests/adapter/EventLogProvisioningTest.res +207 -0
- package/tests/adapter/EventLogProvisioningTest.res.mjs +150 -0
- package/tests/api/GraphQL_FragmentGeneratorTest.res +40 -0
- package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +35 -0
- package/tests/logger/LogFormatTest.res +42 -0
- package/tests/logger/LogFormatTest.res.mjs +37 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,25 @@
|
|
|
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.214 (2026-08-07)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **core,aws:** add the EventLogProvisioning seam for out-of-tree consumers ([82477b3](https://github.com/ReventlessDev/reventless-core/commit/82477b38c1ef90fbe3a6f4a114a9798770329470))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.213 (2026-08-05)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **core:** describe slice TODO items by their own schema in the API ([d863056](https://github.com/ReventlessDev/reventless-core/commit/d863056ba734ba3dc76721c32bfb766d777ceb9a))
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* **core:** cap rendered values in command, event and action logs ([8e07752](https://github.com/ReventlessDev/reventless-core/commit/8e077527ad3c8258582951544ee53d2722876191))
|
|
21
|
+
* **local:** offload large pluginDefinition fields to the object store ([84c5fab](https://github.com/ReventlessDev/reventless-core/commit/84c5fab339856b5d64f0c05110b1fa7f38646f5e))
|
|
22
|
+
* **local:** scoped seed:reset for the local platform ([a4dd003](https://github.com/ReventlessDev/reventless-core/commit/a4dd0033b7d01be23be91bac3f01d38ebeab7d45))
|
|
23
|
+
|
|
24
|
+
|
|
6
25
|
# 3.0.0-alpha.212 (2026-08-04)
|
|
7
26
|
|
|
8
27
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-core",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.214",
|
|
4
4
|
"description": "Core package for Reventless framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -28,17 +28,17 @@
|
|
|
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.1",
|
|
32
31
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
33
32
|
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
|
|
34
|
-
"@reventlessdev/rescript-
|
|
33
|
+
"@reventlessdev/rescript-fast-csv": "2.0.0-alpha.2",
|
|
35
34
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
36
35
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
|
|
37
|
-
"@reventlessdev/rescript-ssh2": "2.0.0-alpha.
|
|
36
|
+
"@reventlessdev/rescript-ssh2": "2.0.0-alpha.2",
|
|
38
37
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
39
|
-
"@reventlessdev/
|
|
38
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.2",
|
|
39
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.127",
|
|
40
40
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.30",
|
|
41
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.
|
|
41
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.101"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"rescript": "12.3.0",
|
package/src/Projection.res
CHANGED
|
@@ -126,9 +126,12 @@ let applyChanges = async (
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
// Best-effort: a state that can't be JSON-serialized (e.g. carries a function)
|
|
129
|
-
// must not crash the projection just to produce a debug line.
|
|
129
|
+
// must not crash the projection just to produce a debug line. Capped at the
|
|
130
|
+
// shared log budget — an action line names which row changed, and a read model
|
|
131
|
+
// holding a large field (a plugin structure, an inline offload payload) would
|
|
132
|
+
// otherwise print the whole row twice, once per side of the `=>`.
|
|
130
133
|
let stateToString: 'a => string = state =>
|
|
131
|
-
state->JSON.stringifyAny->Option.getOr("<unserializable>")
|
|
134
|
+
state->JSON.stringifyAny->Option.getOr("<unserializable>")->LogFormat.truncate
|
|
132
135
|
let statesToString: array<'a> => string = states =>
|
|
133
136
|
states->Array.map(stateToString)->Array.joinUnsafe(", ")
|
|
134
137
|
|
package/src/Projection.res.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import * as DisplayName$Reventless from "@reventlessdev/reventless-spec/src/comp
|
|
|
16
16
|
import * as Message$ReventlessCore from "./Message.res.mjs";
|
|
17
17
|
import * as QueryDb$ReventlessCore from "./components/QueryDb/QueryDb.res.mjs";
|
|
18
18
|
import * as QueryDb$ReventlessInfra from "@reventlessdev/reventless-infra/src/components/QueryDb.res.mjs";
|
|
19
|
+
import * as LogFormat$ReventlessCore from "./util/LogFormat.res.mjs";
|
|
19
20
|
|
|
20
21
|
let log = Logger$ReventlessCore.fromEnv();
|
|
21
22
|
|
|
@@ -195,7 +196,7 @@ async function applyChanges(action, id, beforeStates, afterStates, param, param$
|
|
|
195
196
|
}
|
|
196
197
|
|
|
197
198
|
function stateToString(state) {
|
|
198
|
-
return Stdlib_Option.getOr(JSON.stringify(state), "<unserializable>");
|
|
199
|
+
return LogFormat$ReventlessCore.truncate(Stdlib_Option.getOr(JSON.stringify(state), "<unserializable>"));
|
|
199
200
|
}
|
|
200
201
|
|
|
201
202
|
function statesToString(states) {
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Deploy-time hook: every event log announces itself as it is provisioned, so an
|
|
3
|
+
extension can attach behaviour to it inside the program that owns it.
|
|
4
|
+
|
|
5
|
+
No-op by default. Zero behavioral change unless an extension registers a backend
|
|
6
|
+
via `use` before the platform/plugin build. The seam fires once per log at
|
|
7
|
+
provisioning time (inside the deploy program) — never at invocation, and it runs
|
|
8
|
+
no code in production.
|
|
9
|
+
|
|
10
|
+
What a backend does with a log is deliberately NOT a framework concern.
|
|
11
|
+
Aggregation, CDC export, external indexing, replication and archival all want to
|
|
12
|
+
react to an event log's changes; none of them belong in core, and each one added
|
|
13
|
+
to core is maintenance its users pay for whether or not they use it. This only
|
|
14
|
+
exposes the choke point through which the framework says "I just provisioned an
|
|
15
|
+
event log, here is everything I know about it" and leaves the reaction to the
|
|
16
|
+
listener.
|
|
17
|
+
|
|
18
|
+
The alternative — discovering a log's resources from outside the owning program —
|
|
19
|
+
is impractical on AWS: a DynamoDB stream ARN ends in a creation-time stream
|
|
20
|
+
label, so it cannot be synthesised, and exporting it per plugin pushes framework
|
|
21
|
+
internals into every deployment's stack outputs.
|
|
22
|
+
|
|
23
|
+
See `docs/plans/done/event-log-provisioned-seam.md`.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
Which event-log family provisioned the log. The two differ in what a reader finds
|
|
28
|
+
on the wire — a classic log is per-aggregate and its rows carry an aggregate id;
|
|
29
|
+
a DCB log is per-plugin and its rows carry tags — so a backend that decodes
|
|
30
|
+
records needs to know which it is looking at before it looks.
|
|
31
|
+
*/
|
|
32
|
+
type logStyle =
|
|
33
|
+
| /** per-aggregate log behind `EventLog_Builder` */ Classic
|
|
34
|
+
| /** per-plugin tagged log behind `DcbEventLog_Builder` */ Dcb
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
An event-log backend, registered by an extension (a deploy program) before the
|
|
38
|
+
platform builds. `onProvisioned` is called once per event log.
|
|
39
|
+
|
|
40
|
+
`~name` is the log's STATIC logical name — the suffixed component name the
|
|
41
|
+
builder passed to its storage maker (`ProductEventLog`, `CatalogDcbEventLog`). A
|
|
42
|
+
Pulumi-based backend derives logical resource names from it, which must be stable
|
|
43
|
+
and known at graph-construction time.
|
|
44
|
+
|
|
45
|
+
`~owner` is the model element the log belongs to: `{kind: Aggregate, name:
|
|
46
|
+
"Product"}` for a classic log, `{kind: Plugin, name: "Catalog"}` for a DCB log
|
|
47
|
+
(the plugin owns it, not any one slice). `~plugin` / `~platform` come from the
|
|
48
|
+
ambient `ResourceAttribution` context the plugin builder publishes during
|
|
49
|
+
construction — the same source `AWS_Tags` and `Monitoring.notify` use. Both are
|
|
50
|
+
`None` for a log provisioned outside any plugin construct.
|
|
51
|
+
|
|
52
|
+
`~resources` is the adapter's deploy-time resources, not a stream: a stream is a
|
|
53
|
+
DynamoDB-shaped idea and SQL-backed adapters have none. A backend derives
|
|
54
|
+
whatever its store supports and does nothing for the rest — a Postgres log
|
|
55
|
+
arrives with `resources: []`, and an in-memory one with nothing attachable. The
|
|
56
|
+
contract is uniform on purpose; filtering belongs in the backend.
|
|
57
|
+
|
|
58
|
+
`~opts` carries the log component's parent options. A backend that provisions
|
|
59
|
+
anything needs them, or its resources land outside the component tree.
|
|
60
|
+
|
|
61
|
+
Because `onProvisioned` fires during resource creation inside the deploy program,
|
|
62
|
+
a Pulumi-based backend can create its resources in the same stack, parented
|
|
63
|
+
naturally.
|
|
64
|
+
|
|
65
|
+
**The reader budget.** A DynamoDB table has exactly one stream, and DynamoDB
|
|
66
|
+
Streams throttles beyond two processes reading the same shard. Core already
|
|
67
|
+
spends one slot: `EventCollectorChannel_Helpers` maps every event-log stream onto
|
|
68
|
+
the plugin's event-collector Lambda. A backend that subscribes through this seam
|
|
69
|
+
is therefore reader #2 — the ceiling. A second stream consumer would throttle the
|
|
70
|
+
first, which is why registration is single-slot and loud (see `use`). If several
|
|
71
|
+
consumers are ever needed the answer is a fan-out — routing through the existing
|
|
72
|
+
collector, or a stream type that supports more readers — not more subscriptions.
|
|
73
|
+
|
|
74
|
+
**Snapshots share the log's table.** Classic aggregate snapshots are written to
|
|
75
|
+
the event-log table under `position = "SNAPSHOT"`, so a stream consumer sees them
|
|
76
|
+
alongside events. The seam does not filter; a backend that only wants events must
|
|
77
|
+
skip those rows itself.
|
|
78
|
+
|
|
79
|
+
**No ordering guarantee against other seams.** This and `Monitoring.notify` both
|
|
80
|
+
fire during provisioning, from different builders, in whatever order the graph is
|
|
81
|
+
constructed. A backend must not depend on having seen one before the other.
|
|
82
|
+
*/
|
|
83
|
+
module type Backend = {
|
|
84
|
+
let onProvisioned: (
|
|
85
|
+
~logStyle: logStyle,
|
|
86
|
+
~name: string,
|
|
87
|
+
~owner: option<ResourceAttribution.owner>,
|
|
88
|
+
~plugin: option<string>,
|
|
89
|
+
~platform: option<string>,
|
|
90
|
+
~resources: array<ReventlessInfra.Adapter.resource>,
|
|
91
|
+
~opts: Pulumi.CustomResourceOptions.t,
|
|
92
|
+
) => unit
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Default backend: does nothing. Active unless an extension calls `use`. */
|
|
96
|
+
module Noop: Backend = {
|
|
97
|
+
let onProvisioned = (
|
|
98
|
+
~logStyle as _,
|
|
99
|
+
~name as _,
|
|
100
|
+
~owner as _,
|
|
101
|
+
~plugin as _,
|
|
102
|
+
~platform as _,
|
|
103
|
+
~resources as _,
|
|
104
|
+
~opts as _,
|
|
105
|
+
) => ()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let backend: ref<module(Backend)> = ref(module(Noop: Backend))
|
|
109
|
+
let registered = ref(false)
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
Register the event-log backend. Must run before the platform/plugin build in the
|
|
113
|
+
deploy program (plain statement order — the registry is consulted lazily at each
|
|
114
|
+
provisioning site). Called once, by the extension, first.
|
|
115
|
+
|
|
116
|
+
Single-slot and loud: a second `use` raises rather than silently winning, because
|
|
117
|
+
the reader budget documented on `Backend` means two registered backends is not a
|
|
118
|
+
last-writer-wins configuration question but two consumers competing for one
|
|
119
|
+
stream slot — a defect that otherwise surfaces in production as iterator-age
|
|
120
|
+
alarms on whichever one lost. `reset` clears the slot for tests.
|
|
121
|
+
*/
|
|
122
|
+
let use = (b: module(Backend)) => {
|
|
123
|
+
if registered.contents {
|
|
124
|
+
JsError.throwWithMessage(
|
|
125
|
+
"EventLogProvisioning.use called twice: only one event-log backend can be registered. " ++
|
|
126
|
+
"A DynamoDB stream supports two readers per shard and core's event collector already holds one, " ++
|
|
127
|
+
"so a second backend would throttle the first. Compose the two backends into one, or fan out " ++
|
|
128
|
+
"behind a single registration.",
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
registered := true
|
|
132
|
+
backend := b
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
Restore the default `Noop` backend and clear the registration slot. Test-support
|
|
137
|
+
only — deploy programs never call this.
|
|
138
|
+
*/
|
|
139
|
+
let reset = () => {
|
|
140
|
+
backend := module(Noop: Backend)
|
|
141
|
+
registered := false
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
Announce a provisioned event log to the registered backend. Called by the
|
|
146
|
+
`EventLog` and `DcbEventLog` builders at their storage-provisioning sites (once
|
|
147
|
+
per log). Firing from the builders rather than from each storage adapter is what
|
|
148
|
+
makes the contract uniform: every adapter — DynamoDB, Postgres, SQLite,
|
|
149
|
+
in-memory, and any future one — is covered by construction, with nothing to
|
|
150
|
+
remember when a new one is written. No-op until an extension registers a backend.
|
|
151
|
+
*/
|
|
152
|
+
let notify = (
|
|
153
|
+
~logStyle: logStyle,
|
|
154
|
+
~name: string,
|
|
155
|
+
~owner: option<ResourceAttribution.owner>,
|
|
156
|
+
~resources: array<ReventlessInfra.Adapter.resource>,
|
|
157
|
+
~opts: Pulumi.CustomResourceOptions.t,
|
|
158
|
+
) => {
|
|
159
|
+
module B = unpack(backend.contents)
|
|
160
|
+
// The owning plugin/platform come from the ambient context the plugin builder publishes
|
|
161
|
+
// (ResourceAttribution.enter) — populated exactly when this fires, so the call sites need not
|
|
162
|
+
// thread the names. `None` outside a plugin construct.
|
|
163
|
+
let {ResourceAttribution.plugin: plugin, platform} = ResourceAttribution.current.contents
|
|
164
|
+
B.onProvisioned(~logStyle, ~name, ~owner, ~plugin, ~platform, ~resources, ~opts)
|
|
165
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
4
|
+
import * as ResourceAttribution$ReventlessCore from "../../ResourceAttribution.res.mjs";
|
|
5
|
+
|
|
6
|
+
function onProvisioned(param, param$1, param$2, param$3, param$4, param$5, param$6) {
|
|
7
|
+
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let Noop = {
|
|
11
|
+
onProvisioned: onProvisioned
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
let backend = {
|
|
15
|
+
contents: Noop
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
let registered = {
|
|
19
|
+
contents: false
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function use(b) {
|
|
23
|
+
if (registered.contents) {
|
|
24
|
+
Stdlib_JsError.throwWithMessage("EventLogProvisioning.use called twice: only one event-log backend can be registered. A DynamoDB stream supports two readers per shard and core's event collector already holds one, so a second backend would throttle the first. Compose the two backends into one, or fan out behind a single registration.");
|
|
25
|
+
}
|
|
26
|
+
registered.contents = true;
|
|
27
|
+
backend.contents = b;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function reset() {
|
|
31
|
+
backend.contents = Noop;
|
|
32
|
+
registered.contents = false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function notify(logStyle, name, owner, resources, opts) {
|
|
36
|
+
let B = backend.contents;
|
|
37
|
+
let match = ResourceAttribution$ReventlessCore.current.contents;
|
|
38
|
+
B.onProvisioned(logStyle, name, owner, match.plugin, match.platform, resources, opts);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export {
|
|
42
|
+
Noop,
|
|
43
|
+
backend,
|
|
44
|
+
registered,
|
|
45
|
+
use,
|
|
46
|
+
reset,
|
|
47
|
+
notify,
|
|
48
|
+
}
|
|
49
|
+
/* No side effect */
|
|
@@ -31,6 +31,36 @@ type todoRow = {
|
|
|
31
31
|
retryCount: int,
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/** The item schema seen through the `JSON.t` the row actually stores.
|
|
35
|
+
|
|
36
|
+
`item` holds `Spec.todoItemSchema`-encoded JSON, so the item schema already
|
|
37
|
+
describes the stored value exactly — only the ReScript types disagree. */
|
|
38
|
+
external itemAsJson: S.t<'item> => S.t<JSON.t> = "%identity"
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
The row schema as the API describes it: `item` carried by the slice's own item
|
|
42
|
+
schema instead of as opaque JSON.
|
|
43
|
+
|
|
44
|
+
`todoRowSchema` types `item` as `JSON.t`, which the schema→GraphQL IR can only
|
|
45
|
+
classify as `Unknown` — and `Unknown` renders as `String!`. The field then
|
|
46
|
+
serves a stored object through a String, which fails at execution ("String
|
|
47
|
+
cannot represent value: { … }") the moment a client selects it. Describing the
|
|
48
|
+
item shape gives the field a real object type instead.
|
|
49
|
+
|
|
50
|
+
Shape only: never encode or parse a row with this. The runtime field is `JSON.t`
|
|
51
|
+
and the identity cast above would have `S.parseOrThrow` hand back an item where
|
|
52
|
+
the row's own code expects JSON.
|
|
53
|
+
*/
|
|
54
|
+
let todoRowSchemaFor = (itemSchema: S.t<'item>): S.t<todoRow> =>
|
|
55
|
+
S.schema(s => {
|
|
56
|
+
item: s.matches(itemSchema->itemAsJson),
|
|
57
|
+
status: s.matches(todoStatusSchema),
|
|
58
|
+
createdAt: s.matches(S.string),
|
|
59
|
+
processedAt: ?s.matches(S.option(S.string)),
|
|
60
|
+
completedAt: ?s.matches(S.option(S.string)),
|
|
61
|
+
retryCount: s.matches(S.int),
|
|
62
|
+
})
|
|
63
|
+
|
|
34
64
|
/**
|
|
35
65
|
The runtime protocol between a built callback and whoever drives it — the same
|
|
36
66
|
arrangement as `OutboundTranslationSlice_Callback`, and for the same reason. The
|
|
@@ -30,6 +30,17 @@ let todoRowSchema = S.schema(s => ({
|
|
|
30
30
|
retryCount: s.m(S.int)
|
|
31
31
|
}));
|
|
32
32
|
|
|
33
|
+
function todoRowSchemaFor(itemSchema) {
|
|
34
|
+
return S.schema(s => ({
|
|
35
|
+
item: s.m(itemSchema),
|
|
36
|
+
status: s.m(todoStatusSchema),
|
|
37
|
+
createdAt: s.m(S.string),
|
|
38
|
+
processedAt: s.m(S.option(S.string)),
|
|
39
|
+
completedAt: s.m(S.option(S.string)),
|
|
40
|
+
retryCount: s.m(S.int)
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
|
|
33
44
|
function Make(Spec) {
|
|
34
45
|
return Automation => {
|
|
35
46
|
let todoItems = {};
|
|
@@ -184,6 +195,7 @@ function Make(Spec) {
|
|
|
184
195
|
export {
|
|
185
196
|
todoStatusSchema,
|
|
186
197
|
todoRowSchema,
|
|
198
|
+
todoRowSchemaFor,
|
|
187
199
|
Make,
|
|
188
200
|
}
|
|
189
201
|
/* Not a pure module */
|
|
@@ -1009,7 +1009,9 @@ module Make = (
|
|
|
1009
1009
|
ReventlessInfra.Api.singleFieldName: qn.singleFieldName,
|
|
1010
1010
|
listFieldName: qn.listFieldName,
|
|
1011
1011
|
returnTypeName: qn.returnTypeName,
|
|
1012
|
-
stateSchema: AutomationSlice_Callback.
|
|
1012
|
+
stateSchema: AutomationSlice_Callback.todoRowSchemaFor(
|
|
1013
|
+
A.Spec.todoItemSchema,
|
|
1014
|
+
)->S.castToUnknown,
|
|
1013
1015
|
authorization: None,
|
|
1014
1016
|
connectionSpec: true,
|
|
1015
1017
|
}
|
|
@@ -1023,7 +1025,9 @@ module Make = (
|
|
|
1023
1025
|
ReventlessInfra.Api.singleFieldName: qn.singleFieldName,
|
|
1024
1026
|
listFieldName: qn.listFieldName,
|
|
1025
1027
|
returnTypeName: qn.returnTypeName,
|
|
1026
|
-
stateSchema: OutboundTranslationSlice_Callback.
|
|
1028
|
+
stateSchema: OutboundTranslationSlice_Callback.todoRowSchemaFor(
|
|
1029
|
+
O.Spec.outboundItemSchema,
|
|
1030
|
+
)->S.castToUnknown,
|
|
1027
1031
|
authorization: None,
|
|
1028
1032
|
connectionSpec: true,
|
|
1029
1033
|
}
|
|
@@ -531,7 +531,7 @@ function Make(DcbEventLogStorage) {
|
|
|
531
531
|
singleFieldName: qn.singleFieldName,
|
|
532
532
|
listFieldName: qn.listFieldName,
|
|
533
533
|
returnTypeName: qn.returnTypeName,
|
|
534
|
-
stateSchema: AutomationSlice_Callback$ReventlessCore.
|
|
534
|
+
stateSchema: AutomationSlice_Callback$ReventlessCore.todoRowSchemaFor(A.Spec.todoItemSchema),
|
|
535
535
|
authorization: undefined,
|
|
536
536
|
connectionSpec: true
|
|
537
537
|
};
|
|
@@ -542,7 +542,7 @@ function Make(DcbEventLogStorage) {
|
|
|
542
542
|
singleFieldName: qn.singleFieldName,
|
|
543
543
|
listFieldName: qn.listFieldName,
|
|
544
544
|
returnTypeName: qn.returnTypeName,
|
|
545
|
-
stateSchema: OutboundTranslationSlice_Callback$ReventlessCore.
|
|
545
|
+
stateSchema: OutboundTranslationSlice_Callback$ReventlessCore.todoRowSchemaFor(O.Spec.outboundItemSchema),
|
|
546
546
|
authorization: undefined,
|
|
547
547
|
connectionSpec: true
|
|
548
548
|
};
|
|
@@ -16,21 +16,34 @@ module Make = (
|
|
|
16
16
|
let construct = (indexes, partitionTag, crossPartitionTagKeys, self, name) => {
|
|
17
17
|
let opts = {Pulumi.CustomResourceOptions.parent: self->Component.toPulumiResource}
|
|
18
18
|
|
|
19
|
+
let logName = name->ComponentType.name(DcbEventLog.componentType)
|
|
20
|
+
// The DCB event log is shared across the plugin's slices, so the plugin owns
|
|
21
|
+
// it — not any one component.
|
|
22
|
+
let owner: ResourceAttribution.owner = {kind: ComponentType.Plugin, name}
|
|
19
23
|
let storage = Storage.make(
|
|
20
|
-
~name=
|
|
24
|
+
~name=logName,
|
|
21
25
|
~indexes,
|
|
22
26
|
~partitionTag,
|
|
23
27
|
~crossPartitionTagKeys,
|
|
24
28
|
~opts,
|
|
25
29
|
)
|
|
26
30
|
|
|
31
|
+
// Announce the log to any registered extension. No-op unless a deploy program
|
|
32
|
+
// registered a backend via EventLogProvisioning.use — see
|
|
33
|
+
// src/adapter/EventLogProvisioning/EventLogProvisioning.res.
|
|
34
|
+
EventLogProvisioning.notify(
|
|
35
|
+
~logStyle=Dcb,
|
|
36
|
+
~name=logName,
|
|
37
|
+
~owner=Some(owner),
|
|
38
|
+
~resources=storage.resources,
|
|
39
|
+
~opts,
|
|
40
|
+
)
|
|
41
|
+
|
|
27
42
|
module SpecificEventTopic = EventTopic_Builder.Make(EventTopicSpec, EventTopicPublisher)
|
|
28
43
|
let eventTopic = SpecificEventTopic.make(
|
|
29
44
|
~name,
|
|
30
45
|
~storageResources=storage.resources,
|
|
31
|
-
|
|
32
|
-
// owns it — not any one component.
|
|
33
|
-
~owner={kind: ComponentType.Plugin, name},
|
|
46
|
+
~owner,
|
|
34
47
|
~opts=opts->Util.Pulumi.ComponentResourceOptions.ofCustomResourceOptions,
|
|
35
48
|
)
|
|
36
49
|
|
|
@@ -8,6 +8,7 @@ import * as DcbEventLog$ReventlessCore from "./DcbEventLog.res.mjs";
|
|
|
8
8
|
import * as Util_Pulumi$ReventlessCore from "../../util/Util_Pulumi.res.mjs";
|
|
9
9
|
import * as ComponentType$ReventlessCore from "../../ComponentType.res.mjs";
|
|
10
10
|
import * as EventTopic_Builder$ReventlessCore from "../EventTopic/EventTopic_Builder.res.mjs";
|
|
11
|
+
import * as EventLogProvisioning$ReventlessCore from "../../adapter/EventLogProvisioning/EventLogProvisioning.res.mjs";
|
|
11
12
|
import * as DcbEventLog_Operations$ReventlessCore from "./DcbEventLog_Operations.res.mjs";
|
|
12
13
|
|
|
13
14
|
function Make(Storage) {
|
|
@@ -20,16 +21,19 @@ function Make(Storage) {
|
|
|
20
21
|
let opts = {
|
|
21
22
|
parent: opts_parent
|
|
22
23
|
};
|
|
23
|
-
let
|
|
24
|
+
let logName = ComponentType$ReventlessCore.name(extra$1, DcbEventLog$ReventlessCore.componentType);
|
|
25
|
+
let owner = {
|
|
26
|
+
kind: "Plugin",
|
|
27
|
+
name: extra$1
|
|
28
|
+
};
|
|
29
|
+
let storage = Storage.make(logName, indexes, partitionTag, crossPartitionTagKeys, opts);
|
|
30
|
+
EventLogProvisioning$ReventlessCore.notify("Dcb", logName, owner, storage.resources, opts);
|
|
24
31
|
let SpecificEventTopic = EventTopic_Builder$ReventlessCore.Make({
|
|
25
32
|
Id: Id$Reventless.$$String,
|
|
26
33
|
name: "DcbEventLog",
|
|
27
34
|
eventSchema: S.json
|
|
28
35
|
})(EventTopicPublisher);
|
|
29
|
-
let eventTopic = SpecificEventTopic.make(extra$1, storage.resources,
|
|
30
|
-
kind: "Plugin",
|
|
31
|
-
name: extra$1
|
|
32
|
-
}, Util_Pulumi$ReventlessCore.ComponentResourceOptions.ofCustomResourceOptions(opts));
|
|
36
|
+
let eventTopic = SpecificEventTopic.make(extra$1, storage.resources, owner, Util_Pulumi$ReventlessCore.ComponentResourceOptions.ofCustomResourceOptions(opts));
|
|
33
37
|
Component$ReventlessCore.setOperations(extra, Pulumi.all([
|
|
34
38
|
storage.operations,
|
|
35
39
|
Component$ReventlessCore.operations(eventTopic)
|
|
@@ -18,9 +18,17 @@ module Make = (
|
|
|
18
18
|
let construct = (~owner=?, self, name) => {
|
|
19
19
|
let opts = {Pulumi.CustomResourceOptions.parent: self->Component.toPulumiResource}
|
|
20
20
|
|
|
21
|
-
let
|
|
22
|
-
|
|
21
|
+
let logName = name->ComponentType.name(EventLog.componentType)
|
|
22
|
+
let storage = Storage.make(~name=logName, ~owner, ~opts)
|
|
23
|
+
|
|
24
|
+
// Announce the log to any registered extension. No-op unless a deploy program
|
|
25
|
+
// registered a backend via EventLogProvisioning.use — see
|
|
26
|
+
// src/adapter/EventLogProvisioning/EventLogProvisioning.res.
|
|
27
|
+
EventLogProvisioning.notify(
|
|
28
|
+
~logStyle=Classic,
|
|
29
|
+
~name=logName,
|
|
23
30
|
~owner,
|
|
31
|
+
~resources=storage.resources,
|
|
24
32
|
~opts,
|
|
25
33
|
)
|
|
26
34
|
|
|
@@ -7,6 +7,7 @@ import * as Util_Pulumi$ReventlessCore from "../../util/Util_Pulumi.res.mjs";
|
|
|
7
7
|
import * as ComponentType$ReventlessCore from "../../ComponentType.res.mjs";
|
|
8
8
|
import * as EventTopic_Builder$ReventlessCore from "../EventTopic/EventTopic_Builder.res.mjs";
|
|
9
9
|
import * as EventLog_Operations$ReventlessCore from "./EventLog_Operations.res.mjs";
|
|
10
|
+
import * as EventLogProvisioning$ReventlessCore from "../../adapter/EventLogProvisioning/EventLogProvisioning.res.mjs";
|
|
10
11
|
|
|
11
12
|
function Make(Spec) {
|
|
12
13
|
return Storage => (EventTopicPublisher => {
|
|
@@ -15,7 +16,9 @@ function Make(Spec) {
|
|
|
15
16
|
let opts = {
|
|
16
17
|
parent: opts_parent
|
|
17
18
|
};
|
|
18
|
-
let
|
|
19
|
+
let logName = ComponentType$ReventlessCore.name(extra$1, EventLog$ReventlessCore.componentType);
|
|
20
|
+
let storage = Storage.make(logName, owner, opts);
|
|
21
|
+
EventLogProvisioning$ReventlessCore.notify("Classic", logName, owner, storage.resources, opts);
|
|
19
22
|
let SpecificEventTopic = EventTopic_Builder$ReventlessCore.Make(Spec)(EventTopicPublisher);
|
|
20
23
|
let eventTopic = SpecificEventTopic.make(extra$1, storage.resources, owner, Util_Pulumi$ReventlessCore.ComponentResourceOptions.ofCustomResourceOptions(opts));
|
|
21
24
|
Component$ReventlessCore.setOperations(extra, Pulumi.all([
|
|
@@ -22,6 +22,38 @@ type todoRow = {
|
|
|
22
22
|
lastError?: string,
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/** The item schema seen through the `JSON.t` the row actually stores.
|
|
26
|
+
|
|
27
|
+
`item` holds `Spec.outboundItemSchema`-encoded JSON, so the item schema
|
|
28
|
+
already describes the stored value exactly — only the ReScript types
|
|
29
|
+
disagree. */
|
|
30
|
+
external itemAsJson: S.t<'item> => S.t<JSON.t> = "%identity"
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
The row schema as the API describes it: `item` carried by the slice's own item
|
|
34
|
+
schema instead of as opaque JSON.
|
|
35
|
+
|
|
36
|
+
`todoRowSchema` types `item` as `JSON.t`, which the schema→GraphQL IR can only
|
|
37
|
+
classify as `Unknown` — and `Unknown` renders as `String!`. The field then
|
|
38
|
+
serves a stored object through a String, which fails at execution ("String
|
|
39
|
+
cannot represent value: { … }") the moment a client selects it. Describing the
|
|
40
|
+
item shape gives the field a real object type instead.
|
|
41
|
+
|
|
42
|
+
Shape only: never encode or parse a row with this. The runtime field is `JSON.t`
|
|
43
|
+
and the identity cast above would have `S.parseOrThrow` hand back an item where
|
|
44
|
+
the row's own code expects JSON.
|
|
45
|
+
*/
|
|
46
|
+
let todoRowSchemaFor = (itemSchema: S.t<'item>): S.t<todoRow> =>
|
|
47
|
+
S.schema(s => {
|
|
48
|
+
item: s.matches(itemSchema->itemAsJson),
|
|
49
|
+
status: s.matches(todoStatusSchema),
|
|
50
|
+
createdAt: s.matches(S.string),
|
|
51
|
+
processedAt: ?s.matches(S.option(S.string)),
|
|
52
|
+
completedAt: ?s.matches(S.option(S.string)),
|
|
53
|
+
retryCount: s.matches(S.int),
|
|
54
|
+
lastError: ?s.matches(S.option(S.string)),
|
|
55
|
+
})
|
|
56
|
+
|
|
25
57
|
/**
|
|
26
58
|
The runtime protocol between a built callback and whoever drives it.
|
|
27
59
|
|
|
@@ -29,6 +29,18 @@ let todoRowSchema = S.schema(s => ({
|
|
|
29
29
|
lastError: s.m(S.option(S.string))
|
|
30
30
|
}));
|
|
31
31
|
|
|
32
|
+
function todoRowSchemaFor(itemSchema) {
|
|
33
|
+
return S.schema(s => ({
|
|
34
|
+
item: s.m(itemSchema),
|
|
35
|
+
status: s.m(todoStatusSchema),
|
|
36
|
+
createdAt: s.m(S.string),
|
|
37
|
+
processedAt: s.m(S.option(S.string)),
|
|
38
|
+
completedAt: s.m(S.option(S.string)),
|
|
39
|
+
retryCount: s.m(S.int),
|
|
40
|
+
lastError: s.m(S.option(S.string))
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
function Make(Spec) {
|
|
33
45
|
return Translation => {
|
|
34
46
|
let todoItems = {};
|
|
@@ -168,6 +180,7 @@ function Make(Spec) {
|
|
|
168
180
|
export {
|
|
169
181
|
todoStatusSchema,
|
|
170
182
|
todoRowSchema,
|
|
183
|
+
todoRowSchemaFor,
|
|
171
184
|
Make,
|
|
172
185
|
}
|
|
173
186
|
/* Not a pure module */
|
|
@@ -621,20 +621,13 @@ module Make = (
|
|
|
621
621
|
|
|
622
622
|
// Offload the two large pluginDefinition fields at deploy time when a
|
|
623
623
|
// provider offers an object store (AWS writes a content-addressed object
|
|
624
|
-
// and returns a reference)
|
|
625
|
-
//
|
|
626
|
-
//
|
|
627
|
-
//
|
|
628
|
-
//
|
|
624
|
+
// and returns a reference). Computed HERE, at graph-construction time —
|
|
625
|
+
// both values are concrete and the hook (which creates the object resource)
|
|
626
|
+
// MUST run outside the Pulumi.Output.apply below: a resource cannot be
|
|
627
|
+
// created inside .apply, and an apply callback is skipped entirely on
|
|
628
|
+
// `pulumi preview` when its inputs are still unknown.
|
|
629
629
|
// Unconditional: these fields are always large enough to be worth offloading.
|
|
630
|
-
let toPayload =
|
|
631
|
-
switch Plugin_Helpers.offloadHook.contents {
|
|
632
|
-
| Some(hook) =>
|
|
633
|
-
Reventless.Offload.Offloaded(
|
|
634
|
-
hook(~store, ~bytes=value->S.reverseConvertToJsonStringOrThrow(schema)),
|
|
635
|
-
)
|
|
636
|
-
| None => Reventless.Offload.Inline(value)
|
|
637
|
-
}
|
|
630
|
+
let toPayload = Plugin_Helpers.offloadPayload
|
|
638
631
|
let apiSchemaFragmentPayload = toPayload(
|
|
639
632
|
apiSchemaFragment,
|
|
640
633
|
~schema=Reventless.Plugin.apiSchemaFragmentSchema,
|
|
@@ -425,22 +425,8 @@ function Make(Spec) {
|
|
|
425
425
|
});
|
|
426
426
|
let pushSchema = Spec.hooks.preResolversSchemaHook;
|
|
427
427
|
let schemaPushed = pushSchema !== undefined ? pushSchema(extra$1, version, apiSchemaFragment) : Pulumi.output();
|
|
428
|
-
let
|
|
429
|
-
|
|
430
|
-
if (hook !== undefined) {
|
|
431
|
-
return {
|
|
432
|
-
TAG: "Offloaded",
|
|
433
|
-
_0: hook(store, S.reverseConvertToJsonStringOrThrow(value, schema, undefined))
|
|
434
|
-
};
|
|
435
|
-
} else {
|
|
436
|
-
return {
|
|
437
|
-
TAG: "Inline",
|
|
438
|
-
_0: value
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
};
|
|
442
|
-
let apiSchemaFragmentPayload = toPayload(apiSchemaFragment, Plugin$Reventless.apiSchemaFragmentSchema, "pluginApiFragments");
|
|
443
|
-
let structurePayload = Stdlib_Option.map(pluginStructure, s => toPayload(s, Plugin$Reventless.pluginStructureSchema, "pluginStructures"));
|
|
428
|
+
let apiSchemaFragmentPayload = Plugin_Helpers$ReventlessCore.offloadPayload(apiSchemaFragment, Plugin$Reventless.apiSchemaFragmentSchema, "pluginApiFragments");
|
|
429
|
+
let structurePayload = Stdlib_Option.map(pluginStructure, s => Plugin_Helpers$ReventlessCore.offloadPayload(s, Plugin$Reventless.pluginStructureSchema, "pluginStructures"));
|
|
444
430
|
let builderOutputs = Pulumi.all([
|
|
445
431
|
Pulumi.all([
|
|
446
432
|
interstackAdminExtensionPoints,
|