@reventlessdev/reventless-spec 3.0.0-alpha.164 → 3.0.0-alpha.166
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 +27 -0
- package/package.json +1 -1
- package/schema/platform-api.graphql +1 -0
- package/src/components/AutomationSlice.res +26 -0
- package/src/components/AutomationSlice.res.mjs +2 -1
- package/src/components/Clock.res +38 -0
- package/src/components/Clock.res.mjs +28 -0
- package/src/components/Plugin.res +3 -0
- package/src/components/Plugin.res.mjs +2 -1
- package/src/types/OwnerScope.res +24 -0
- package/src/types/OwnerScope.res.mjs +21 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,33 @@
|
|
|
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.166 (2026-10-16)
|
|
7
|
+
|
|
8
|
+
* fix(auth)!: only operators read the framework's own stores ([a2fd202](https://github.com/ReventlessDev/reventless-core/commit/a2fd2022b6876dae855b185df7f8263353fc971d))
|
|
9
|
+
|
|
10
|
+
### BREAKING CHANGES
|
|
11
|
+
|
|
12
|
+
* a caller outside the elevated groups is refused the
|
|
13
|
+
framework's to-do, schedule and audit stores. On AWS, a roles-gated
|
|
14
|
+
view's …ByIds, …Refs, …Items and …By<Index> fields refuse callers
|
|
15
|
+
outside its roles.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# 3.0.0-alpha.165 (2026-10-16)
|
|
20
|
+
|
|
21
|
+
* feat!(automation): schedules set by events ([53bc436](https://github.com/ReventlessDev/reventless-core/commit/53bc4363dffce81ef36475728bf0dae704716021))
|
|
22
|
+
* feat!(automation): declared schedules tick an automation ([420acd2](https://github.com/ReventlessDev/reventless-core/commit/420acd2233aadf700141a31d9d3e0fdc8074bef7))
|
|
23
|
+
|
|
24
|
+
### BREAKING CHANGES
|
|
25
|
+
|
|
26
|
+
* a hand-written AutomationSlice.Mapping.Make the PPX does
|
|
27
|
+
not see must define `let reschedule = _ => []`.
|
|
28
|
+
* a hand-written AutomationSlice spec the PPX does not see must
|
|
29
|
+
declare `let schedule = None`.
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
6
33
|
# 3.0.0-alpha.164 (2026-10-15)
|
|
7
34
|
|
|
8
35
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -72,6 +72,14 @@ module type Spec = {
|
|
|
72
72
|
|
|
73
73
|
/** Name of the aggregate or StateChangeSlice that receives the produced command. */
|
|
74
74
|
let targetName: string
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
The time this automation runs on, or `None` for one started by events only. A
|
|
78
|
+
declared time is the slice's schedule under `Clock.defaultKey`; each time it comes,
|
|
79
|
+
the slice's `Clock` mapping receives a `Tick`. The `@@reventless.spec` PPX injects
|
|
80
|
+
`None` when the spec declares none.
|
|
81
|
+
*/
|
|
82
|
+
let schedule: option<Schedule.time>
|
|
75
83
|
}
|
|
76
84
|
|
|
77
85
|
// ── Plan 04: Mixed-source mappings ───────────────────────────────────────────
|
|
@@ -117,6 +125,17 @@ type context = {
|
|
|
117
125
|
recordedAt?: float,
|
|
118
126
|
}
|
|
119
127
|
|
|
128
|
+
/**
|
|
129
|
+
What an event does to one of the slice's schedules. `key` names the schedule:
|
|
130
|
+
`Clock.defaultKey` for the slice-wide one (replacing a declared time), or an id for a
|
|
131
|
+
time per entity, which the tick carries back to `collect`. `Set` replaces the key's
|
|
132
|
+
time, the last one winning; `Clear` of a key with no schedule does nothing. An
|
|
133
|
+
`After` is counted from when the event was recorded.
|
|
134
|
+
*/
|
|
135
|
+
type scheduleChange =
|
|
136
|
+
| Set({key: string, time: Schedule.time})
|
|
137
|
+
| Clear({key: string})
|
|
138
|
+
|
|
120
139
|
/**
|
|
121
140
|
A compiled per-source-to-AutomationSlice mapping.
|
|
122
141
|
|
|
@@ -148,6 +167,9 @@ module type Mapping = {
|
|
|
148
167
|
*/
|
|
149
168
|
let collect: (sourceEvent, ~sourceId: string, context) => array<(string, todoItem)>
|
|
150
169
|
let resolve: sourceEvent => option<string>
|
|
170
|
+
/** The schedules the event sets or clears. The `@@reventless.automation` PPX adds
|
|
171
|
+
`let reschedule = _ => []` to a mapping written without it. */
|
|
172
|
+
let reschedule: sourceEvent => array<scheduleChange>
|
|
151
173
|
}
|
|
152
174
|
|
|
153
175
|
/**
|
|
@@ -233,6 +255,9 @@ module type MappingImpl = {
|
|
|
233
255
|
*/
|
|
234
256
|
let collect: (sourceEvent, ~sourceId: string, context) => array<(string, todoItem)>
|
|
235
257
|
let resolve: sourceEvent => option<string>
|
|
258
|
+
/** The schedules the event sets or clears. The `@@reventless.automation` PPX adds
|
|
259
|
+
`let reschedule = _ => []` to a mapping written without it. */
|
|
260
|
+
let reschedule: sourceEvent => array<scheduleChange>
|
|
236
261
|
}
|
|
237
262
|
|
|
238
263
|
/**
|
|
@@ -314,6 +339,7 @@ module Mapping = {
|
|
|
314
339
|
let sourceName = Source.name
|
|
315
340
|
let collect = Impl.collect
|
|
316
341
|
let resolve = Impl.resolve
|
|
342
|
+
let reschedule = Impl.reschedule
|
|
317
343
|
}
|
|
318
344
|
}
|
|
319
345
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
The framework source a scheduled automation maps. When one of the slice's
|
|
3
|
+
schedules comes due, the platform's Scheduler fires and the slice receives
|
|
4
|
+
`Tick({key, at})`: which of its schedules fired (`default` for the declared one)
|
|
5
|
+
and the time it was scheduled for, in milliseconds since 1970 (UTC). What the tick
|
|
6
|
+
means is the slice's own: a mapping's `collect` makes the item, `process` sends the
|
|
7
|
+
command. Never written by an application.
|
|
8
|
+
|
|
9
|
+
@example
|
|
10
|
+
```rescript
|
|
11
|
+
module FromClock = Mapping.Make(
|
|
12
|
+
Reventless.Clock,
|
|
13
|
+
DemandReport,
|
|
14
|
+
{
|
|
15
|
+
open Reventless.Clock
|
|
16
|
+
let collect = (Tick({key, at}), ~sourceId as _, _ctx) =>
|
|
17
|
+
[(itemId(key, at), ({day: at}: DemandReport.todoItem))]
|
|
18
|
+
let resolve = _ => None
|
|
19
|
+
},
|
|
20
|
+
)
|
|
21
|
+
```
|
|
22
|
+
*/
|
|
23
|
+
module Id = Id.String
|
|
24
|
+
|
|
25
|
+
let name = "Clock"
|
|
26
|
+
|
|
27
|
+
@schema
|
|
28
|
+
type event = Tick({key: string, at: float})
|
|
29
|
+
|
|
30
|
+
/** The key a declared `schedule` is held under. */
|
|
31
|
+
let defaultKey = "default"
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
The to-do item id for a tick: `<key>@<scheduled time in UTC, to the minute>`, such
|
|
35
|
+
as `default@2026-10-16T06:00Z`. The same tick delivered twice names the same item.
|
|
36
|
+
*/
|
|
37
|
+
let itemId = (key, at) =>
|
|
38
|
+
`${key}@${at->Date.fromTime->Date.toISOString->String.slice(~start=0, ~end=16)}Z`
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Sury from "sury";
|
|
4
|
+
|
|
5
|
+
let eventSchema = Sury.$schema(s => ({
|
|
6
|
+
TAG: "Tick",
|
|
7
|
+
key: s.m(Sury.string),
|
|
8
|
+
at: s.m(Sury.number)
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
function itemId(key, at) {
|
|
12
|
+
return key + `@` + new Date(at).toISOString().slice(0, 16) + `Z`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let Id;
|
|
16
|
+
|
|
17
|
+
let name = "Clock";
|
|
18
|
+
|
|
19
|
+
let defaultKey = "default";
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
Id,
|
|
23
|
+
name,
|
|
24
|
+
eventSchema,
|
|
25
|
+
defaultKey,
|
|
26
|
+
itemId,
|
|
27
|
+
}
|
|
28
|
+
/* eventSchema Not a pure module */
|
|
@@ -301,6 +301,9 @@ type automationSliceDef = {
|
|
|
301
301
|
targetName: string,
|
|
302
302
|
/** Chapter grouping band — see `queryableDef.chapter`. */
|
|
303
303
|
chapter: option<string>,
|
|
304
|
+
/** The declared `schedule`, as the JSON of `Schedule.timeSchema`; `None` for an
|
|
305
|
+
automation started by events only, and in a structure from before the field. */
|
|
306
|
+
schedule?: string,
|
|
304
307
|
}
|
|
305
308
|
|
|
306
309
|
@schema
|
|
@@ -158,7 +158,8 @@ let automationSliceDefSchema = Sury.$schema(s => ({
|
|
|
158
158
|
consumedEventTypes: s.m(Sury.array(Sury.string)),
|
|
159
159
|
producedCommandTypes: s.m(Sury.array(Sury.string)),
|
|
160
160
|
targetName: s.m(Sury.string),
|
|
161
|
-
chapter: s.m(Sury.$option(Sury.string))
|
|
161
|
+
chapter: s.m(Sury.$option(Sury.string)),
|
|
162
|
+
schedule: s.m(Sury.$option(Sury.string))
|
|
162
163
|
}));
|
|
163
164
|
|
|
164
165
|
let outboundTranslationSliceDefSchema = Sury.$schema(s => ({
|
package/src/types/OwnerScope.res
CHANGED
|
@@ -180,6 +180,30 @@ let elevatedGroups = (): array<string> =>
|
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
/** The role that stands for an elevated group: the one mapped to it, else the
|
|
184
|
+
role of the same name. Refused when that role was mapped elsewhere, since no
|
|
185
|
+
role then names the group and a rule would silently admit someone else. */
|
|
186
|
+
let roleForGroup = (group: string): Role.name =>
|
|
187
|
+
switch Role.renamed()->Array.find(((_, g)) => g == group) {
|
|
188
|
+
| Some((role, _)) => role
|
|
189
|
+
| None if Role.groupOf(Role.make(group)) == group => Role.make(group)
|
|
190
|
+
| None =>
|
|
191
|
+
JsError.throwWithMessage(
|
|
192
|
+
`Elevated group "${group}" has no role standing for it: role "${group}" stands for ` ++
|
|
193
|
+
`group "${Role.groupOf(Role.make(group))}". Map a role to "${group}" with roleGroups.`,
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
The deployment's operators as a rule: the roles standing for the elevated
|
|
199
|
+
groups. For what the framework keeps for itself, such as an automation's to-do
|
|
200
|
+
list. An empty list admits nobody. Read when called, after the root has stated
|
|
201
|
+
its operators and its role mapping.
|
|
202
|
+
*/
|
|
203
|
+
let operators = (): Authorization.permission => AllowRoles(
|
|
204
|
+
elevatedGroups()->Array.map(roleForGroup),
|
|
205
|
+
)
|
|
206
|
+
|
|
183
207
|
// Order matters: the provider is examined before `userId`, because the IAM
|
|
184
208
|
// caller fails the `userId` test for a reason that has nothing to do with being
|
|
185
209
|
// anonymous and must not be refused as though it did.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
4
4
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
5
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
6
7
|
import * as Role$Reventless from "./Role.res.mjs";
|
|
7
8
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
8
9
|
import * as Identity$Reventless from "./Identity.res.mjs";
|
|
@@ -80,6 +81,24 @@ function elevatedGroups() {
|
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
function roleForGroup(group) {
|
|
85
|
+
let match = Role$Reventless.renamed().find(param => param[1] === group);
|
|
86
|
+
if (match !== undefined) {
|
|
87
|
+
return match[0];
|
|
88
|
+
} else if (Role$Reventless.groupOf(group) === group) {
|
|
89
|
+
return group;
|
|
90
|
+
} else {
|
|
91
|
+
return Stdlib_JsError.throwWithMessage(`Elevated group "` + group + `" has no role standing for it: role "` + group + `" stands for ` + (`group "` + Role$Reventless.groupOf(group) + `". Map a role to "` + group + `" with roleGroups.`));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function operators() {
|
|
96
|
+
return {
|
|
97
|
+
TAG: "AllowRoles",
|
|
98
|
+
_0: elevatedGroups().map(roleForGroup)
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
83
102
|
function classify(identity, elevated) {
|
|
84
103
|
let provider = identity.provider;
|
|
85
104
|
let providerName = isJsString(provider) ? provider : undefined;
|
|
@@ -344,6 +363,8 @@ export {
|
|
|
344
363
|
defaultElevatedRoles,
|
|
345
364
|
parseElevatedGroups,
|
|
346
365
|
elevatedGroups,
|
|
366
|
+
roleForGroup,
|
|
367
|
+
operators,
|
|
347
368
|
classify,
|
|
348
369
|
resolve,
|
|
349
370
|
ownerId,
|