@reventlessdev/reventless-core 3.0.0-alpha.223 → 3.0.0-alpha.225
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 +36 -0
- package/package.json +6 -6
- package/src/admin/Platform_Admin_Structure.res +16 -0
- package/src/admin/Platform_Admin_Structure.res.mjs +14 -3
- package/src/admin/Platform_BakedManifest.res +154 -0
- package/src/admin/Platform_BakedManifest.res.mjs +156 -0
- package/src/admin/Platform_ComponentDefinitionsApi.res +19 -2
- package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +30 -2
- package/src/components/Api/Api_Ids.res +63 -0
- package/src/components/Api/Api_Ids.res.mjs +43 -0
- package/src/components/Api/GraphQL_FragmentGenerator.res +121 -28
- package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +97 -27
- package/src/components/Api/OwnerScopeDiagnostics.res +54 -0
- package/src/components/Api/OwnerScopeDiagnostics.res.mjs +27 -0
- package/src/components/Api/QueryDbListQuery.res +38 -6
- package/src/components/Api/QueryDbListQuery.res.mjs +14 -5
- package/src/components/Api/SchemaType.res +36 -2
- package/src/components/Api/SchemaType.res.mjs +31 -10
- package/src/components/Api/SuryToJsonSchema.res +19 -0
- package/src/components/Api/SuryToJsonSchema.res.mjs +18 -4
- package/src/components/CommandGenerator/CommandGenerator_Callback.res +48 -0
- package/src/components/CommandGenerator/CommandGenerator_Callback.res.mjs +27 -1
- package/src/plugin/component/Plugin_Structure.res +78 -3
- package/src/plugin/component/Plugin_Structure.res.mjs +38 -9
- package/tests/admin/Platform_BakedManifestTest.res +244 -0
- package/tests/admin/Platform_BakedManifestTest.res.mjs +294 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +73 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +135 -6
- package/tests/admin/Platform_PluginStructuresApiTest.res +12 -0
- package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +27 -2
- package/tests/api/Api_IdsTest.res +143 -0
- package/tests/api/Api_IdsTest.res.mjs +97 -0
- package/tests/api/GraphQL_FragmentGeneratorTest.res +169 -0
- package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +179 -0
- package/tests/api/OwnerScopeDiagnosticsTest.res +109 -0
- package/tests/api/OwnerScopeDiagnosticsTest.res.mjs +91 -0
- package/tests/api/SuryToJsonSchemaTest.res +45 -0
- package/tests/api/SuryToJsonSchemaTest.res.mjs +23 -0
- package/tests/commandgenerator/OwnerStampingTest.res +174 -0
- package/tests/commandgenerator/OwnerStampingTest.res.mjs +195 -0
- package/tests/plugin/PluginStructureAccessTest.res +110 -0
- package/tests/plugin/PluginStructureAccessTest.res.mjs +144 -0
- package/tests/plugin/PluginStructureTest.res +90 -0
- package/tests/plugin/PluginStructureTest.res.mjs +86 -8
- package/tests/plugin/StateChangeSlice/PsGatedCommands.res +28 -0
- package/tests/plugin/StateChangeSlice/PsGatedCommands.res.mjs +84 -0
- package/tests/plugin/StateViewSlice/PsCategoriesView.res +17 -0
- package/tests/plugin/StateViewSlice/PsCategoriesView.res.mjs +57 -0
- package/tests/plugin/StateViewSlice/PsGatedView.res +15 -0
- package/tests/plugin/StateViewSlice/PsGatedView.res.mjs +60 -0
- package/tests/plugin/pluginDefinitionRequiredScalars.txt +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,42 @@
|
|
|
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.225 (2026-08-12)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **commands:** stamp owner-marked fields from the authenticated caller ([5266655](https://github.com/ReventlessDev/reventless-core/commit/5266655b34de4d90c8148dec686cb0f336ac223d))
|
|
11
|
+
* **core:** warn when an owner-scoped view exempts nobody ([cafa7a8](https://github.com/ReventlessDev/reventless-core/commit/cafa7a8b2082aeb00e53a9ed9da5bb978158db2d))
|
|
12
|
+
* **platform:** bake a curated component manifest as a static asset ([4f30265](https://github.com/ReventlessDev/reventless-core/commit/4f30265a51fc2c59e69afd8074cf1b2534c06378))
|
|
13
|
+
* **plugin:** publish the access a component's authorization rule implies ([e0d0f09](https://github.com/ReventlessDev/reventless-core/commit/e0d0f096b72fe44b185ed28dc1c133364fa841b8))
|
|
14
|
+
* **plugin:** publish which field ties a component to its owner ([ca71289](https://github.com/ReventlessDev/reventless-core/commit/ca7128931ce525af0d7d3a4487b2b5c54b19bec0))
|
|
15
|
+
* **queries:** narrow owner-bearing reads to the caller ([ba9cc3d](https://github.com/ReventlessDev/reventless-core/commit/ba9cc3d58d7914a9e4827bda90a704a74b1b82dd))
|
|
16
|
+
* **spec:** let a record name the field that identifies its owner ([b69ee91](https://github.com/ReventlessDev/reventless-core/commit/b69ee9123e6beb38fcdd716519103ab9328213c6))
|
|
17
|
+
* **spec:** let the environment name the groups exempt from owner scoping ([2de5c51](https://github.com/ReventlessDev/reventless-core/commit/2de5c5118347a46998f9ab603308fca09addf00b))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# 3.0.0-alpha.224 (2026-08-11)
|
|
21
|
+
|
|
22
|
+
### Bug Fixes
|
|
23
|
+
|
|
24
|
+
* **api:** one id contract across the read-side query doors ([c52fac2](https://github.com/ReventlessDev/reventless-core/commit/c52fac2bee87b3dd1fa33b4970a74f3cd3866280))
|
|
25
|
+
* feat(core)!: name semantic composites once instead of once per field ([09536ba](https://github.com/ReventlessDev/reventless-core/commit/09536badf338f898b24d749270acd5c7df851ddb))
|
|
26
|
+
### Features
|
|
27
|
+
|
|
28
|
+
* **api:** infer a queryable's key field and publish its provenance ([c835a42](https://github.com/ReventlessDev/reventless-core/commit/c835a42a0da07cdc4a3f010212e1f340a4a0ca27))
|
|
29
|
+
* **plugin:** publish singleQueryField on queryableDef ([a724ab5](https://github.com/ReventlessDev/reventless-core/commit/a724ab573614792c0615d68b6486b94da14f9f82))
|
|
30
|
+
|
|
31
|
+
### BREAKING CHANGES
|
|
32
|
+
|
|
33
|
+
* generated GraphQL type names change. A price field is now
|
|
34
|
+
`MoneyInput!` in a command and `Money!` in a read model, where it was named
|
|
35
|
+
after the field that carried it (`Catalog_AddProductPriceCurrency`,
|
|
36
|
+
`Ordering_SyncNewProductPriceCurrency`, …). AutoUI builds its queries at
|
|
37
|
+
runtime from the component-definitions manifest and adapts on its own; a client
|
|
38
|
+
naming the old types in a hand-written query does not.
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
6
42
|
# 3.0.0-alpha.223 (2026-08-10)
|
|
7
43
|
|
|
8
44
|
### 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.225",
|
|
4
4
|
"description": "Core package for Reventless framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -29,16 +29,16 @@
|
|
|
29
29
|
"sury": "11.0.0-alpha.4",
|
|
30
30
|
"uuid": "^13.0.0",
|
|
31
31
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
32
|
-
"@reventlessdev/rescript-fast-csv": "2.0.0-alpha.3",
|
|
33
32
|
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
|
|
33
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.4",
|
|
34
|
+
"@reventlessdev/rescript-fast-csv": "2.0.0-alpha.4",
|
|
34
35
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
35
|
-
"@reventlessdev/rescript-node": "2.0.0-alpha.3",
|
|
36
|
-
"@reventlessdev/rescript-ssh2": "2.0.0-alpha.3",
|
|
37
36
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
|
|
38
37
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
38
|
+
"@reventlessdev/rescript-ssh2": "2.0.0-alpha.4",
|
|
39
39
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.30",
|
|
40
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.
|
|
41
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.
|
|
40
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.135",
|
|
41
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.109"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"rescript": "12.3.0",
|
|
@@ -76,6 +76,8 @@ let activateCommand: commandDef = {
|
|
|
76
76
|
allowedStates: Some(["Inactive"]),
|
|
77
77
|
targetState: None,
|
|
78
78
|
apiExposed: Some(true),
|
|
79
|
+
requiredAccess: None,
|
|
80
|
+
ownerField: None,
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
let deactivateCommand: commandDef = {
|
|
@@ -88,6 +90,8 @@ let deactivateCommand: commandDef = {
|
|
|
88
90
|
allowedStates: Some(["Connected", "Disconnected"]),
|
|
89
91
|
targetState: None,
|
|
90
92
|
apiExposed: Some(true),
|
|
93
|
+
requiredAccess: None,
|
|
94
|
+
ownerField: None,
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
let pluginAggregate: writableDef = {
|
|
@@ -121,6 +125,18 @@ let pluginReadModel: queryableDef = {
|
|
|
121
125
|
statusField: Some("status"),
|
|
122
126
|
visibility: None,
|
|
123
127
|
chapter: None,
|
|
128
|
+
// The admin fragment hand-declares its query names rather than deriving them from
|
|
129
|
+
// the read-model name, so the singular is taken from the same call
|
|
130
|
+
// `PluginBaseFragment.queryNames.singleFieldName` makes — not singularised here.
|
|
131
|
+
singleQueryField: Some(Api_Naming.adminField(~name="Plugin")),
|
|
132
|
+
// No key field to name: the row id is `name@version` (`Plugin.makeId`), and the
|
|
133
|
+
// state carries the two halves separately rather than the composed key. `None`
|
|
134
|
+
// is the honest answer — the same one the resolver ladder reaches for a state
|
|
135
|
+
// with no `*Id` field.
|
|
136
|
+
idField: None,
|
|
137
|
+
idFieldSource: None,
|
|
138
|
+
requiredAccess: None,
|
|
139
|
+
ownerField: None,
|
|
124
140
|
}
|
|
125
141
|
|
|
126
142
|
let structure: pluginStructure = {
|
|
@@ -64,7 +64,9 @@ let activateCommand = {
|
|
|
64
64
|
references: activateCommand_references,
|
|
65
65
|
allowedStates: activateCommand_allowedStates,
|
|
66
66
|
targetState: undefined,
|
|
67
|
-
apiExposed: activateCommand_apiExposed
|
|
67
|
+
apiExposed: activateCommand_apiExposed,
|
|
68
|
+
requiredAccess: undefined,
|
|
69
|
+
ownerField: undefined
|
|
68
70
|
};
|
|
69
71
|
|
|
70
72
|
let deactivateCommand_schema = encodeSchema(idArgsSchema);
|
|
@@ -91,7 +93,9 @@ let deactivateCommand = {
|
|
|
91
93
|
references: deactivateCommand_references,
|
|
92
94
|
allowedStates: deactivateCommand_allowedStates,
|
|
93
95
|
targetState: undefined,
|
|
94
|
-
apiExposed: deactivateCommand_apiExposed
|
|
96
|
+
apiExposed: deactivateCommand_apiExposed,
|
|
97
|
+
requiredAccess: undefined,
|
|
98
|
+
ownerField: undefined
|
|
95
99
|
};
|
|
96
100
|
|
|
97
101
|
let pluginAggregate_commands = [
|
|
@@ -135,6 +139,8 @@ let pluginReadModel_labelFieldSource = "convention";
|
|
|
135
139
|
|
|
136
140
|
let pluginReadModel_statusField = "status";
|
|
137
141
|
|
|
142
|
+
let pluginReadModel_singleQueryField = Api_Naming$ReventlessCore.adminField("Plugin");
|
|
143
|
+
|
|
138
144
|
let pluginReadModel = {
|
|
139
145
|
name: "Plugins",
|
|
140
146
|
queryField: pluginReadModel_queryField,
|
|
@@ -145,8 +151,13 @@ let pluginReadModel = {
|
|
|
145
151
|
searchableFields: pluginReadModel_searchableFields,
|
|
146
152
|
labelFieldSource: pluginReadModel_labelFieldSource,
|
|
147
153
|
statusField: pluginReadModel_statusField,
|
|
154
|
+
ownerField: undefined,
|
|
148
155
|
visibility: undefined,
|
|
149
|
-
chapter: undefined
|
|
156
|
+
chapter: undefined,
|
|
157
|
+
singleQueryField: pluginReadModel_singleQueryField,
|
|
158
|
+
idField: undefined,
|
|
159
|
+
idFieldSource: undefined,
|
|
160
|
+
requiredAccess: undefined
|
|
150
161
|
};
|
|
151
162
|
|
|
152
163
|
let structure_readModels = [pluginReadModel];
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Curation for the baked component manifest: a deployment's include-list applied
|
|
2
|
+
// to the registered plugin structures, encoded through the very same encoder
|
|
3
|
+
// `Platform_ComponentDefinitions` uses.
|
|
4
|
+
//
|
|
5
|
+
// Curation is not authorization. This module decides WHAT EXISTS for a
|
|
6
|
+
// deployment's audience — deployment data, settled before anyone logs in. What a
|
|
7
|
+
// caller may do stays a server-side decision, enforced per query and per
|
|
8
|
+
// mutation, and nothing here relaxes it. That split is the reason a curated file
|
|
9
|
+
// can be static at all.
|
|
10
|
+
//
|
|
11
|
+
// The filtering happens on the `pluginStructure` record, never on the encoded
|
|
12
|
+
// JSON: whatever survives is then handed to
|
|
13
|
+
// `Platform_ComponentDefinitionsApi.encodePluginStructureEntry`, so a baked entry
|
|
14
|
+
// and a served entry cannot drift in shape, and an include-list that names
|
|
15
|
+
// everything produces byte-identical output.
|
|
16
|
+
|
|
17
|
+
open Reventless.Plugin
|
|
18
|
+
|
|
19
|
+
type error =
|
|
20
|
+
| UnknownPlugin(string)
|
|
21
|
+
| UnknownView({plugin: string, view: string})
|
|
22
|
+
| UnknownCommand({plugin: string, command: string})
|
|
23
|
+
|
|
24
|
+
let describe = (e: error): string =>
|
|
25
|
+
switch e {
|
|
26
|
+
| UnknownPlugin(plugin) => `baked manifest: no registered plugin named "${plugin}"`
|
|
27
|
+
| UnknownView({plugin, view}) =>
|
|
28
|
+
`baked manifest: plugin "${plugin}" has no view named "${view}"`
|
|
29
|
+
| UnknownCommand({plugin, command}) =>
|
|
30
|
+
`baked manifest: plugin "${plugin}" has no command named "${command}"`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Views are named by component name — a view IS the surface. Commands are named
|
|
34
|
+
// by command name, not by the write-side component that carries them: the
|
|
35
|
+
// command is what a user invokes, and which slice or aggregate it lives on is an
|
|
36
|
+
// implementation detail the author should not have to know to curate a shop.
|
|
37
|
+
type selection = {
|
|
38
|
+
plugin: string,
|
|
39
|
+
views: option<array<string>>,
|
|
40
|
+
commands: option<array<string>>,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let publicViews = (def: pluginStructure): array<queryableDef> =>
|
|
44
|
+
Array.concat(def.readModels, def.stateViewSlices)->Array.filter(
|
|
45
|
+
Platform_ComponentDefinitionsApi.isPublicQueryable,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
let writeSides = (def: pluginStructure): array<writableDef> =>
|
|
49
|
+
Array.concat(def.stateChangeSlices, def.aggregates)
|
|
50
|
+
|
|
51
|
+
let commandNames = (def: pluginStructure): array<string> =>
|
|
52
|
+
def->writeSides->Array.flatMap(w => w.commands->Array.map(c => c.name))
|
|
53
|
+
|
|
54
|
+
// Every name in the selection has to resolve. A silent no-op here ships a shell
|
|
55
|
+
// with a page missing for a reason no log explains — the same class of failure as
|
|
56
|
+
// a malformed hints file, and it gets the same treatment: fail the deploy.
|
|
57
|
+
let validate = (~def: pluginStructure, sel: selection): result<unit, error> => {
|
|
58
|
+
let known = (names, candidates) =>
|
|
59
|
+
names
|
|
60
|
+
->Option.getOr([])
|
|
61
|
+
->Array.find(name => !(candidates->Array.includes(name)))
|
|
62
|
+
|
|
63
|
+
switch sel.views->known(def->publicViews->Array.map(q => q.name)) {
|
|
64
|
+
| Some(view) => Error(UnknownView({plugin: sel.plugin, view}))
|
|
65
|
+
| None =>
|
|
66
|
+
switch sel.commands->known(def->commandNames) {
|
|
67
|
+
| Some(command) => Error(UnknownCommand({plugin: sel.plugin, command}))
|
|
68
|
+
| None => Ok()
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let isSelected = (selected: option<array<string>>, name: string): bool =>
|
|
74
|
+
switch selected {
|
|
75
|
+
| None => true
|
|
76
|
+
| Some(names) => names->Array.includes(name)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// An Internal view survives the bake only when an included command references it.
|
|
80
|
+
// It is carried for one reason — to be a reference target — and a shell reading a
|
|
81
|
+
// baked manifest has no admin API to recover a missing one from, so leaving it out
|
|
82
|
+
// breaks the picker permanently in the one deployment shape with no fallback.
|
|
83
|
+
// Naming it discloses nothing: the domain API already serves that view to the same
|
|
84
|
+
// caller (Internal is a menu hint, not a boundary).
|
|
85
|
+
let referencedEntities = (~pluginName: string, kept: array<writableDef>): array<string> =>
|
|
86
|
+
kept
|
|
87
|
+
->Array.flatMap(w => w.commands)
|
|
88
|
+
->Array.flatMap(c => c.references)
|
|
89
|
+
->Array.filterMap(r =>
|
|
90
|
+
switch r.plugin {
|
|
91
|
+
| Some(p) if Plugin.name(p) !== pluginName => None
|
|
92
|
+
| _ => Some(r.entity)
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
let curateStructure = (
|
|
97
|
+
~pluginId: string,
|
|
98
|
+
~def: pluginStructure,
|
|
99
|
+
sel: selection,
|
|
100
|
+
): result<pluginStructure, error> =>
|
|
101
|
+
validate(~def, sel)->Result.map(() => {
|
|
102
|
+
let pluginName = Plugin.name(pluginId)
|
|
103
|
+
let keepCommands = (ws: array<writableDef>) =>
|
|
104
|
+
ws
|
|
105
|
+
->Array.map(w => {...w, commands: w.commands->Array.filter(c => sel.commands->isSelected(c.name))})
|
|
106
|
+
// A write side left with no command contributes no surface. Dropping it
|
|
107
|
+
// keeps the curated entry a description of what the app offers rather than
|
|
108
|
+
// a list of components whose commands were all removed.
|
|
109
|
+
->Array.filter(w => w.commands->Array.length > 0)
|
|
110
|
+
|
|
111
|
+
let stateChangeSlices = def.stateChangeSlices->keepCommands
|
|
112
|
+
let aggregates = def.aggregates->keepCommands
|
|
113
|
+
let referenced = referencedEntities(
|
|
114
|
+
~pluginName,
|
|
115
|
+
Array.concat(stateChangeSlices, aggregates),
|
|
116
|
+
)
|
|
117
|
+
let keepViews = (qs: array<queryableDef>) =>
|
|
118
|
+
qs->Array.filter(q =>
|
|
119
|
+
if Platform_ComponentDefinitionsApi.isPublicQueryable(q) {
|
|
120
|
+
sel.views->isSelected(q.name)
|
|
121
|
+
} else {
|
|
122
|
+
referenced->Array.includes(q.name)
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
{
|
|
127
|
+
...def,
|
|
128
|
+
readModels: def.readModels->keepViews,
|
|
129
|
+
stateViewSlices: def.stateViewSlices->keepViews,
|
|
130
|
+
stateChangeSlices,
|
|
131
|
+
aggregates,
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
// The whole file: one entry per selection, in the order the deployment declared
|
|
136
|
+
// them, so the include-list also states the order a consumer reads plugins in.
|
|
137
|
+
let curate = (
|
|
138
|
+
~structures: array<(string, pluginStructure)>,
|
|
139
|
+
~selections: array<selection>,
|
|
140
|
+
): result<JSON.t, error> =>
|
|
141
|
+
selections->Array.reduce(Ok([]), (acc, sel) =>
|
|
142
|
+
acc->Result.flatMap(entries =>
|
|
143
|
+
switch structures->Array.find(((pluginId, _)) => Plugin.name(pluginId) === sel.plugin) {
|
|
144
|
+
| None => Error(UnknownPlugin(sel.plugin))
|
|
145
|
+
| Some((pluginId, def)) =>
|
|
146
|
+
curateStructure(~pluginId, ~def, sel)->Result.map(curated => {
|
|
147
|
+
entries->Array.push(
|
|
148
|
+
Platform_ComponentDefinitionsApi.encodePluginStructureEntry(~pluginId, curated),
|
|
149
|
+
)
|
|
150
|
+
entries
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
)->Result.map(JSON.Encode.array)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
|
|
6
|
+
import * as Plugin$ReventlessCore from "../plugin/component/Plugin.res.mjs";
|
|
7
|
+
import * as Platform_ComponentDefinitionsApi$ReventlessCore from "./Platform_ComponentDefinitionsApi.res.mjs";
|
|
8
|
+
|
|
9
|
+
function describe(e) {
|
|
10
|
+
switch (e.TAG) {
|
|
11
|
+
case "UnknownPlugin" :
|
|
12
|
+
return `baked manifest: no registered plugin named "` + e._0 + `"`;
|
|
13
|
+
case "UnknownView" :
|
|
14
|
+
return `baked manifest: plugin "` + e.plugin + `" has no view named "` + e.view + `"`;
|
|
15
|
+
case "UnknownCommand" :
|
|
16
|
+
return `baked manifest: plugin "` + e.plugin + `" has no command named "` + e.command + `"`;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function publicViews(def) {
|
|
21
|
+
return def.readModels.concat(def.stateViewSlices).filter(Platform_ComponentDefinitionsApi$ReventlessCore.isPublicQueryable);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function writeSides(def) {
|
|
25
|
+
return def.stateChangeSlices.concat(def.aggregates);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function commandNames(def) {
|
|
29
|
+
return writeSides(def).flatMap(w => w.commands.map(c => c.name));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function validate(def, sel) {
|
|
33
|
+
let known = (names, candidates) => Stdlib_Option.getOr(names, []).find(name => !candidates.includes(name));
|
|
34
|
+
let view = known(sel.views, publicViews(def).map(q => q.name));
|
|
35
|
+
if (view !== undefined) {
|
|
36
|
+
return {
|
|
37
|
+
TAG: "Error",
|
|
38
|
+
_0: {
|
|
39
|
+
TAG: "UnknownView",
|
|
40
|
+
plugin: sel.plugin,
|
|
41
|
+
view: view
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
let command = known(sel.commands, commandNames(def));
|
|
46
|
+
if (command !== undefined) {
|
|
47
|
+
return {
|
|
48
|
+
TAG: "Error",
|
|
49
|
+
_0: {
|
|
50
|
+
TAG: "UnknownCommand",
|
|
51
|
+
plugin: sel.plugin,
|
|
52
|
+
command: command
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
} else {
|
|
56
|
+
return {
|
|
57
|
+
TAG: "Ok",
|
|
58
|
+
_0: undefined
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isSelected(selected, name) {
|
|
64
|
+
if (selected !== undefined) {
|
|
65
|
+
return selected.includes(name);
|
|
66
|
+
} else {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function referencedEntities(pluginName, kept) {
|
|
72
|
+
return Stdlib_Array.filterMap(kept.flatMap(w => w.commands).flatMap(c => c.references), r => {
|
|
73
|
+
let p = r.plugin;
|
|
74
|
+
if (p !== undefined && Plugin$ReventlessCore.name(p) !== pluginName) {
|
|
75
|
+
return;
|
|
76
|
+
} else {
|
|
77
|
+
return r.entity;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function curateStructure(pluginId, def, sel) {
|
|
83
|
+
return Stdlib_Result.map(validate(def, sel), () => {
|
|
84
|
+
let pluginName = Plugin$ReventlessCore.name(pluginId);
|
|
85
|
+
let keepCommands = ws => ws.map(w => ({
|
|
86
|
+
name: w.name,
|
|
87
|
+
commands: w.commands.filter(c => isSelected(sel.commands, c.name)),
|
|
88
|
+
producedEventTypes: w.producedEventTypes,
|
|
89
|
+
consumedEventTypes: w.consumedEventTypes,
|
|
90
|
+
linkedViews: w.linkedViews,
|
|
91
|
+
consistencyRead: w.consistencyRead,
|
|
92
|
+
events: w.events,
|
|
93
|
+
errors: w.errors,
|
|
94
|
+
chapter: w.chapter
|
|
95
|
+
})).filter(w => w.commands.length !== 0);
|
|
96
|
+
let stateChangeSlices = keepCommands(def.stateChangeSlices);
|
|
97
|
+
let aggregates = keepCommands(def.aggregates);
|
|
98
|
+
let referenced = referencedEntities(pluginName, stateChangeSlices.concat(aggregates));
|
|
99
|
+
let keepViews = qs => qs.filter(q => {
|
|
100
|
+
if (Platform_ComponentDefinitionsApi$ReventlessCore.isPublicQueryable(q)) {
|
|
101
|
+
return isSelected(sel.views, q.name);
|
|
102
|
+
} else {
|
|
103
|
+
return referenced.includes(q.name);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
return {
|
|
107
|
+
readModels: keepViews(def.readModels),
|
|
108
|
+
stateViewSlices: keepViews(def.stateViewSlices),
|
|
109
|
+
stateChangeSlices: stateChangeSlices,
|
|
110
|
+
aggregates: aggregates,
|
|
111
|
+
automationSlices: def.automationSlices,
|
|
112
|
+
outboundTranslationSlices: def.outboundTranslationSlices,
|
|
113
|
+
inboundTranslationSlices: def.inboundTranslationSlices,
|
|
114
|
+
extensions: def.extensions,
|
|
115
|
+
extensionPoints: def.extensionPoints,
|
|
116
|
+
requiredStores: def.requiredStores,
|
|
117
|
+
requiredStoreDeclarations: def.requiredStoreDeclarations
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function curate(structures, selections) {
|
|
123
|
+
return Stdlib_Result.map(Stdlib_Array.reduce(selections, {
|
|
124
|
+
TAG: "Ok",
|
|
125
|
+
_0: []
|
|
126
|
+
}, (acc, sel) => Stdlib_Result.flatMap(acc, entries => {
|
|
127
|
+
let match = structures.find(param => Plugin$ReventlessCore.name(param[0]) === sel.plugin);
|
|
128
|
+
if (match === undefined) {
|
|
129
|
+
return {
|
|
130
|
+
TAG: "Error",
|
|
131
|
+
_0: {
|
|
132
|
+
TAG: "UnknownPlugin",
|
|
133
|
+
_0: sel.plugin
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
let pluginId = match[0];
|
|
138
|
+
return Stdlib_Result.map(curateStructure(pluginId, match[1], sel), curated => {
|
|
139
|
+
entries.push(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry(pluginId, curated));
|
|
140
|
+
return entries;
|
|
141
|
+
});
|
|
142
|
+
})), prim => prim);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export {
|
|
146
|
+
describe,
|
|
147
|
+
publicViews,
|
|
148
|
+
writeSides,
|
|
149
|
+
commandNames,
|
|
150
|
+
validate,
|
|
151
|
+
isSelected,
|
|
152
|
+
referencedEntities,
|
|
153
|
+
curateStructure,
|
|
154
|
+
curate,
|
|
155
|
+
}
|
|
156
|
+
/* No side effect */
|
|
@@ -18,14 +18,18 @@ open Reventless.Plugin
|
|
|
18
18
|
// consumers long after they were being encoded.
|
|
19
19
|
let sdlTypes: array<string> = [
|
|
20
20
|
`type Platform_FieldReference {\n fieldName: String!\n entity: String!\n plugin: String\n}`,
|
|
21
|
-
`type Platform_CommandDef {\n name: String!\n schema: String!\n level: String!\n aggregateIdField: String\n mutationField: String!\n references: [Platform_FieldReference!]!\n allowedStates: [String!]\n targetState: String\n apiExposed: Boolean\n}`,
|
|
21
|
+
`type Platform_CommandDef {\n name: String!\n schema: String!\n level: String!\n aggregateIdField: String\n mutationField: String!\n references: [Platform_FieldReference!]!\n allowedStates: [String!]\n targetState: String\n apiExposed: Boolean\n requiredAccess: [String!]\n ownerField: String\n}`,
|
|
22
22
|
`type Platform_EventDef {\n name: String!\n schema: String!\n references: [Platform_FieldReference!]!\n}`,
|
|
23
23
|
// Same fields as Platform_EventDef, kept a distinct type because a refusal is not
|
|
24
24
|
// a fact: a caller selecting `errors` is asking what a command can be rejected
|
|
25
25
|
// with, and the two lists must stay independently evolvable.
|
|
26
26
|
`type Platform_ErrorDef {\n name: String!\n schema: String!\n references: [Platform_FieldReference!]!\n}`,
|
|
27
27
|
`type Platform_WriteSideDef {\n name: String!\n commands: [Platform_CommandDef!]!\n linkedViews: [String!]!\n consistencyRead: String\n producedEventTypes: [String!]!\n consumedEventTypes: [String!]!\n events: [Platform_EventDef!]!\n errors: [Platform_ErrorDef!]!\n chapter: String\n}`,
|
|
28
|
-
`
|
|
28
|
+
// `singleQueryField` / `idField` / `idFieldSource` are nullable, not `String!`:
|
|
29
|
+
// structures persisted before the fields existed decode as `None`, a hand-rolled
|
|
30
|
+
// def may decline to state them, and a state whose key cannot be resolved has no
|
|
31
|
+
// `idField` to report.
|
|
32
|
+
`type Platform_ReadSideDef {\n name: String!\n queryField: String!\n schema: String!\n consumedEventTypes: [String!]!\n linkedWriteSide: [String!]!\n labelField: String!\n searchableFields: [String!]!\n labelFieldSource: String\n statusField: String\n visibility: String\n chapter: String\n singleQueryField: String\n idField: String\n idFieldSource: String\n requiredAccess: [String!]\n ownerField: String\n}`,
|
|
29
33
|
`type Platform_AutomationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n producedCommandTypes: [String!]!\n targetName: String\n chapter: String\n}`,
|
|
30
34
|
`type Platform_OutboundTranslationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n inboundCommandTypes: [String!]!\n targetName: String\n externalSystem: String\n chapter: String\n}`,
|
|
31
35
|
`type Platform_InboundTranslationSliceDef {\n name: String!\n commandTypes: [String!]!\n targetName: String\n externalSystem: String\n chapter: String\n}`,
|
|
@@ -77,6 +81,11 @@ let encodeCommandDef = (c: commandDef): JSON.t =>
|
|
|
77
81
|
),
|
|
78
82
|
("targetState", c.targetState->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
|
|
79
83
|
("apiExposed", c.apiExposed->Option.mapOr(JSON.Encode.null, JSON.Encode.bool)),
|
|
84
|
+
// Null, not `[]`: a rule asking for nothing checkable and a def written
|
|
85
|
+
// before the field existed are the same answer to a client — "no key gates
|
|
86
|
+
// this" — and neither should read as "gated by an empty set".
|
|
87
|
+
("requiredAccess", c.requiredAccess->Option.mapOr(JSON.Encode.null, encodeStrings)),
|
|
88
|
+
("ownerField", c.ownerField->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
|
|
80
89
|
])->JSON.Encode.object
|
|
81
90
|
|
|
82
91
|
// Internal ReadModels / StateViewSlices are carried in pluginStructure for developer
|
|
@@ -111,6 +120,14 @@ let encodeQueryableDef = (r: queryableDef): JSON.t =>
|
|
|
111
120
|
// survives the filter anyway.
|
|
112
121
|
("visibility", r.visibility->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
|
|
113
122
|
("chapter", r.chapter->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
|
|
123
|
+
(
|
|
124
|
+
"singleQueryField",
|
|
125
|
+
r.singleQueryField->Option.mapOr(JSON.Encode.null, JSON.Encode.string),
|
|
126
|
+
),
|
|
127
|
+
("idField", r.idField->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
|
|
128
|
+
("idFieldSource", r.idFieldSource->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
|
|
129
|
+
("requiredAccess", r.requiredAccess->Option.mapOr(JSON.Encode.null, encodeStrings)),
|
|
130
|
+
("ownerField", r.ownerField->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
|
|
114
131
|
])->JSON.Encode.object
|
|
115
132
|
|
|
116
133
|
let encodeEventDef = (e: eventDef): JSON.t =>
|
|
@@ -6,11 +6,11 @@ import * as Plugin$ReventlessCore from "../plugin/component/Plugin.res.mjs";
|
|
|
6
6
|
|
|
7
7
|
let sdlTypes = [
|
|
8
8
|
`type Platform_FieldReference {\n fieldName: String!\n entity: String!\n plugin: String\n}`,
|
|
9
|
-
`type Platform_CommandDef {\n name: String!\n schema: String!\n level: String!\n aggregateIdField: String\n mutationField: String!\n references: [Platform_FieldReference!]!\n allowedStates: [String!]\n targetState: String\n apiExposed: Boolean\n}`,
|
|
9
|
+
`type Platform_CommandDef {\n name: String!\n schema: String!\n level: String!\n aggregateIdField: String\n mutationField: String!\n references: [Platform_FieldReference!]!\n allowedStates: [String!]\n targetState: String\n apiExposed: Boolean\n requiredAccess: [String!]\n ownerField: String\n}`,
|
|
10
10
|
`type Platform_EventDef {\n name: String!\n schema: String!\n references: [Platform_FieldReference!]!\n}`,
|
|
11
11
|
`type Platform_ErrorDef {\n name: String!\n schema: String!\n references: [Platform_FieldReference!]!\n}`,
|
|
12
12
|
`type Platform_WriteSideDef {\n name: String!\n commands: [Platform_CommandDef!]!\n linkedViews: [String!]!\n consistencyRead: String\n producedEventTypes: [String!]!\n consumedEventTypes: [String!]!\n events: [Platform_EventDef!]!\n errors: [Platform_ErrorDef!]!\n chapter: String\n}`,
|
|
13
|
-
`type Platform_ReadSideDef {\n name: String!\n queryField: String!\n schema: String!\n consumedEventTypes: [String!]!\n linkedWriteSide: [String!]!\n labelField: String!\n searchableFields: [String!]!\n labelFieldSource: String\n statusField: String\n visibility: String\n chapter: String\n}`,
|
|
13
|
+
`type Platform_ReadSideDef {\n name: String!\n queryField: String!\n schema: String!\n consumedEventTypes: [String!]!\n linkedWriteSide: [String!]!\n labelField: String!\n searchableFields: [String!]!\n labelFieldSource: String\n statusField: String\n visibility: String\n chapter: String\n singleQueryField: String\n idField: String\n idFieldSource: String\n requiredAccess: [String!]\n ownerField: String\n}`,
|
|
14
14
|
`type Platform_AutomationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n producedCommandTypes: [String!]!\n targetName: String\n chapter: String\n}`,
|
|
15
15
|
`type Platform_OutboundTranslationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n inboundCommandTypes: [String!]!\n targetName: String\n externalSystem: String\n chapter: String\n}`,
|
|
16
16
|
`type Platform_InboundTranslationSliceDef {\n name: String!\n commandTypes: [String!]!\n targetName: String\n externalSystem: String\n chapter: String\n}`,
|
|
@@ -79,6 +79,14 @@ function encodeCommandDef(c) {
|
|
|
79
79
|
[
|
|
80
80
|
"apiExposed",
|
|
81
81
|
Stdlib_Option.mapOr(c.apiExposed, null, prim => prim)
|
|
82
|
+
],
|
|
83
|
+
[
|
|
84
|
+
"requiredAccess",
|
|
85
|
+
Stdlib_Option.mapOr(c.requiredAccess, null, encodeStrings)
|
|
86
|
+
],
|
|
87
|
+
[
|
|
88
|
+
"ownerField",
|
|
89
|
+
Stdlib_Option.mapOr(c.ownerField, null, prim => prim)
|
|
82
90
|
]
|
|
83
91
|
]);
|
|
84
92
|
}
|
|
@@ -132,6 +140,26 @@ function encodeQueryableDef(r) {
|
|
|
132
140
|
[
|
|
133
141
|
"chapter",
|
|
134
142
|
Stdlib_Option.mapOr(r.chapter, null, prim => prim)
|
|
143
|
+
],
|
|
144
|
+
[
|
|
145
|
+
"singleQueryField",
|
|
146
|
+
Stdlib_Option.mapOr(r.singleQueryField, null, prim => prim)
|
|
147
|
+
],
|
|
148
|
+
[
|
|
149
|
+
"idField",
|
|
150
|
+
Stdlib_Option.mapOr(r.idField, null, prim => prim)
|
|
151
|
+
],
|
|
152
|
+
[
|
|
153
|
+
"idFieldSource",
|
|
154
|
+
Stdlib_Option.mapOr(r.idFieldSource, null, prim => prim)
|
|
155
|
+
],
|
|
156
|
+
[
|
|
157
|
+
"requiredAccess",
|
|
158
|
+
Stdlib_Option.mapOr(r.requiredAccess, null, encodeStrings)
|
|
159
|
+
],
|
|
160
|
+
[
|
|
161
|
+
"ownerField",
|
|
162
|
+
Stdlib_Option.mapOr(r.ownerField, null, prim => prim)
|
|
135
163
|
]
|
|
136
164
|
]);
|
|
137
165
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
The one place that knows how a row's GraphQL `id` relates to its storage key.
|
|
3
|
+
|
|
4
|
+
A read-side row is reachable through several id-accepting doors — the typed
|
|
5
|
+
`X(id: ID!)` query, `XsByIds(ids:)`, the list `filter.ids`, and Relay's
|
|
6
|
+
`node(id:)`. They used to disagree about which form they took, so the obvious
|
|
7
|
+
client call `X(id: row.id)` silently returned `null`: the door took the storage
|
|
8
|
+
key while the row advertised the Relay global id. The rule below is what
|
|
9
|
+
`QueryDbListQuery.passIds` already applied to `filter.ids` — lifted out so every
|
|
10
|
+
door applies it, and so provider adapters share one definition rather than three.
|
|
11
|
+
|
|
12
|
+
Provider-neutral on purpose: the contract belongs to neither the local platform
|
|
13
|
+
nor AWS.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
@val external btoa: string => string = "btoa"
|
|
17
|
+
@val external atob: string => string = "atob"
|
|
18
|
+
|
|
19
|
+
/** `btoa("<TypeName>:<localId>")` — the Relay global id for a row. */
|
|
20
|
+
let encode = (~typeName: string, ~localId: string): string => btoa(`${typeName}:${localId}`)
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
The `(typeName, localId)` pair inside a global id, or `None` when the string is
|
|
24
|
+
not one. `atob` throws on non-base64 input; a decoded value with no `:` (or a
|
|
25
|
+
leading one, which would mean an empty type name) is not a global id either.
|
|
26
|
+
*/
|
|
27
|
+
let decode = (globalId: string): option<(string, string)> =>
|
|
28
|
+
try {
|
|
29
|
+
let decoded = atob(globalId)
|
|
30
|
+
let idx = decoded->String.indexOf(":")
|
|
31
|
+
if idx > 0 {
|
|
32
|
+
Some((
|
|
33
|
+
decoded->String.slice(~start=0, ~end=idx),
|
|
34
|
+
decoded->String.slice(~start=idx + 1, ~end=decoded->String.length),
|
|
35
|
+
))
|
|
36
|
+
} else {
|
|
37
|
+
None
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
| _ => None
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
The storage key a global id wraps, or `None` if the string is not one.
|
|
45
|
+
|
|
46
|
+
Deliberately NOT a `string => string` that "normalises" an id: a raw key that
|
|
47
|
+
happens to be valid base64 would be silently rewritten into something that
|
|
48
|
+
matches nothing. Callers look the raw id up first and fall back to this — see
|
|
49
|
+
`alternateKey`.
|
|
50
|
+
*/
|
|
51
|
+
let toLocalId = (id: string): option<string> => decode(id)->Option.map(((_, localId)) => localId)
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
The other key worth trying when a lookup by `id` found nothing: the storage key
|
|
55
|
+
inside it, if it was a global id. Ordering matters — the raw id is always tried
|
|
56
|
+
first, so a key that merely looks like base64 keeps resolving to its own row and
|
|
57
|
+
this only ever runs on a miss.
|
|
58
|
+
*/
|
|
59
|
+
let alternateKey = (id: string): option<string> =>
|
|
60
|
+
switch toLocalId(id) {
|
|
61
|
+
| Some(localId) if localId != id => Some(localId)
|
|
62
|
+
| _ => None
|
|
63
|
+
}
|