@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
|
@@ -27,22 +27,41 @@ function isIdsFieldName(name) {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
let semanticCompositeNames = [
|
|
31
|
+
[
|
|
32
|
+
Semantic$Reventless.Id.money,
|
|
33
|
+
"Money"
|
|
34
|
+
],
|
|
35
|
+
[
|
|
36
|
+
Semantic$Reventless.Id.dateRange,
|
|
37
|
+
"DateRange"
|
|
38
|
+
],
|
|
39
|
+
[
|
|
40
|
+
Semantic$Reventless.Id.geoPoint,
|
|
41
|
+
"GeoPoint"
|
|
42
|
+
]
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
function canonicalName(id) {
|
|
46
|
+
return Stdlib_Option.map(semanticCompositeNames.find(param => param[0] === id), param => param[1]);
|
|
47
|
+
}
|
|
48
|
+
|
|
30
49
|
function fromSury(parentName, fieldName, schema) {
|
|
31
|
-
let shape = shapeOf(parentName, fieldName, schema);
|
|
32
50
|
let sem = Semantic$Reventless.get(schema);
|
|
33
51
|
if (sem === undefined) {
|
|
34
|
-
return
|
|
52
|
+
return shapeOf(parentName, fieldName, schema);
|
|
35
53
|
}
|
|
36
54
|
let id = sem.id;
|
|
37
|
-
if (id
|
|
38
|
-
return
|
|
39
|
-
TAG: "Semantic",
|
|
40
|
-
_0: sem,
|
|
41
|
-
_1: shape
|
|
42
|
-
};
|
|
43
|
-
} else {
|
|
44
|
-
return shape;
|
|
55
|
+
if (id === Semantic$Reventless.Id.dateTime || id === Semantic$Reventless.Id.reference) {
|
|
56
|
+
return shapeOf(parentName, fieldName, schema);
|
|
45
57
|
}
|
|
58
|
+
let name = canonicalName(id);
|
|
59
|
+
let shape = name !== undefined ? shapeOf(name, "", schema) : shapeOf(parentName, fieldName, schema);
|
|
60
|
+
return {
|
|
61
|
+
TAG: "Semantic",
|
|
62
|
+
_0: sem,
|
|
63
|
+
_1: shape
|
|
64
|
+
};
|
|
46
65
|
}
|
|
47
66
|
|
|
48
67
|
function shapeOf(parentName, fieldName, schema) {
|
|
@@ -195,6 +214,8 @@ export {
|
|
|
195
214
|
isDateTime,
|
|
196
215
|
isIdFieldName,
|
|
197
216
|
isIdsFieldName,
|
|
217
|
+
semanticCompositeNames,
|
|
218
|
+
canonicalName,
|
|
198
219
|
fromSury,
|
|
199
220
|
shapeOf,
|
|
200
221
|
optionalFieldNames,
|
|
@@ -188,9 +188,17 @@ and withSemantic = (fieldSchema: JSON.t, sem: Reventless.Semantic.t): JSON.t =>
|
|
|
188
188
|
// through here: nothing validates a read schema. A command schema is the input
|
|
189
189
|
// of a form, and a field listed as required is one the form refuses to submit
|
|
190
190
|
// without — which turned `imageUrl?` into a picture every product had to have.
|
|
191
|
+
//
|
|
192
|
+
// `owners` arrives the same way `optional` does — a list of field names read off
|
|
193
|
+
// the sury schema by the caller — rather than as a case in the IR. The IR is
|
|
194
|
+
// shape-driven, and ownership is not a shape: it says which principal a record
|
|
195
|
+
// belongs to, leaving the field a plain string either way. Threading it here
|
|
196
|
+
// also keeps it available on command variants, which carry no annotation spec
|
|
197
|
+
// for `mergeAnnotations` to read.
|
|
191
198
|
and objectRefToJsonSchema = (
|
|
192
199
|
~annotations: option<Reventless.StateAnnotations.stateAnnotationSpec>=?,
|
|
193
200
|
~optional: array<string>=[],
|
|
201
|
+
~owners: array<string>=[],
|
|
194
202
|
fields: dict<SchemaType.schemaType>,
|
|
195
203
|
): JSON.t => {
|
|
196
204
|
let props = Dict.make()
|
|
@@ -203,6 +211,16 @@ and objectRefToJsonSchema = (
|
|
|
203
211
|
| Some(spec) => mergeAnnotations(baseSchema, fieldName, spec)
|
|
204
212
|
| None => baseSchema
|
|
205
213
|
}
|
|
214
|
+
let withAnnotations = if owners->Array.includes(fieldName) {
|
|
215
|
+
switch withAnnotations->JSON.Decode.object {
|
|
216
|
+
| Some(obj) =>
|
|
217
|
+
obj->Dict.set("x-reventless-owner", JSON.Encode.bool(true))
|
|
218
|
+
JSON.Encode.object(obj)
|
|
219
|
+
| None => withAnnotations
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
withAnnotations
|
|
223
|
+
}
|
|
206
224
|
props->Dict.set(fieldName, withAnnotations)
|
|
207
225
|
if !(optional->Array.includes(fieldName)) {
|
|
208
226
|
required->Array.push(fieldName)
|
|
@@ -227,6 +245,7 @@ let deriveObjectSchema = (schema: S.t<unknown>): JSON.t =>
|
|
|
227
245
|
let objSchema = objectRefToJsonSchema(
|
|
228
246
|
~annotations?,
|
|
229
247
|
~optional=SchemaType.optionalFieldNames(schema),
|
|
248
|
+
~owners=Reventless.Owner.fieldNames(schema),
|
|
230
249
|
fields,
|
|
231
250
|
)
|
|
232
251
|
// Surface component-level hints on the top-level object schema.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
4
4
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as Owner$Reventless from "@reventlessdev/reventless-spec/src/components/Owner.res.mjs";
|
|
5
6
|
import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
|
|
6
7
|
import * as SchemaType$ReventlessCore from "./SchemaType.res.mjs";
|
|
7
8
|
import * as StateAnnotations$Reventless from "@reventlessdev/reventless-spec/src/components/StateAnnotations.res.mjs";
|
|
@@ -178,7 +179,7 @@ function fromSchemaType(st) {
|
|
|
178
179
|
]
|
|
179
180
|
]);
|
|
180
181
|
case "ObjectRef" :
|
|
181
|
-
return objectRefToJsonSchema(undefined, undefined, st._1);
|
|
182
|
+
return objectRefToJsonSchema(undefined, undefined, undefined, st._1);
|
|
182
183
|
case "Enum" :
|
|
183
184
|
return Object.fromEntries([
|
|
184
185
|
[
|
|
@@ -222,15 +223,28 @@ function withSemantic(fieldSchema, sem) {
|
|
|
222
223
|
return obj;
|
|
223
224
|
}
|
|
224
225
|
|
|
225
|
-
function objectRefToJsonSchema(annotations, optionalOpt, fields) {
|
|
226
|
+
function objectRefToJsonSchema(annotations, optionalOpt, ownersOpt, fields) {
|
|
226
227
|
let optional = optionalOpt !== undefined ? optionalOpt : [];
|
|
228
|
+
let owners = ownersOpt !== undefined ? ownersOpt : [];
|
|
227
229
|
let props = {};
|
|
228
230
|
let required = [];
|
|
229
231
|
Object.entries(fields).forEach(param => {
|
|
230
232
|
let fieldName = param[0];
|
|
231
233
|
let baseSchema = fromSchemaType(param[1]);
|
|
232
234
|
let withAnnotations = annotations !== undefined ? mergeAnnotations(baseSchema, fieldName, annotations) : baseSchema;
|
|
233
|
-
|
|
235
|
+
let withAnnotations$1;
|
|
236
|
+
if (owners.includes(fieldName)) {
|
|
237
|
+
let obj = Stdlib_JSON.Decode.object(withAnnotations);
|
|
238
|
+
if (obj !== undefined) {
|
|
239
|
+
obj["x-reventless-owner"] = true;
|
|
240
|
+
withAnnotations$1 = obj;
|
|
241
|
+
} else {
|
|
242
|
+
withAnnotations$1 = withAnnotations;
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
withAnnotations$1 = withAnnotations;
|
|
246
|
+
}
|
|
247
|
+
props[fieldName] = withAnnotations$1;
|
|
234
248
|
if (!optional.includes(fieldName)) {
|
|
235
249
|
required.push(fieldName);
|
|
236
250
|
return;
|
|
@@ -264,7 +278,7 @@ function deriveObjectSchema(schema) {
|
|
|
264
278
|
]]);
|
|
265
279
|
}
|
|
266
280
|
let annotations = StateAnnotations$Reventless.getSpec(schema);
|
|
267
|
-
let objSchema = objectRefToJsonSchema(annotations, SchemaType$ReventlessCore.optionalFieldNames(schema), fields);
|
|
281
|
+
let objSchema = objectRefToJsonSchema(annotations, SchemaType$ReventlessCore.optionalFieldNames(schema), Owner$Reventless.fieldNames(schema), fields);
|
|
268
282
|
if (annotations === undefined) {
|
|
269
283
|
return objSchema;
|
|
270
284
|
}
|
|
@@ -30,6 +30,48 @@ module type T = {
|
|
|
30
30
|
let generateCommand: CommandGenerator.commandGenerator
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
Overwrite every `@owner`-marked field of the command being issued with the
|
|
35
|
+
authenticated caller's id.
|
|
36
|
+
|
|
37
|
+
Deliberately here rather than in a transport's resolver. The local GraphQL
|
|
38
|
+
resolver and the AppSync mutation template build the same `payload` and both
|
|
39
|
+
hand it to `makeGenerateCommand`, so this is the only place a single edit is
|
|
40
|
+
true on both — and a rule enforced on one transport and not the other is worse
|
|
41
|
+
than no rule, because it reads as enforced.
|
|
42
|
+
|
|
43
|
+
**Overwrite, not fill-if-absent.** A caller who omits the field and a caller who
|
|
44
|
+
sends someone else's id must produce the same row. Filling only the absent case
|
|
45
|
+
passes every test that does not forge the field, and forging it is the whole
|
|
46
|
+
attack.
|
|
47
|
+
|
|
48
|
+
An exempt caller — the platform's own service traffic, or an operator — is left
|
|
49
|
+
alone, so acting on another principal's behalf stays possible for those who may.
|
|
50
|
+
A caller that cannot be identified is refused outright, but only for commands
|
|
51
|
+
that actually carry an owner: an anonymous caller invoking an `AllowAnonymous`
|
|
52
|
+
command with no owner field has nothing to prove and is not this rule's business.
|
|
53
|
+
*/
|
|
54
|
+
let stampOwnerFields = (
|
|
55
|
+
obj: dict<JSON.t>,
|
|
56
|
+
~commandSchema: S.t<unknown>,
|
|
57
|
+
~command: string,
|
|
58
|
+
~identity: Reventless.Identity.t,
|
|
59
|
+
~serviceName: string,
|
|
60
|
+
) =>
|
|
61
|
+
switch Reventless.Owner.variantFieldNames(commandSchema, ~variant=command) {
|
|
62
|
+
| [] => ()
|
|
63
|
+
| ownerFields =>
|
|
64
|
+
switch identity->Reventless.OwnerScope.resolve {
|
|
65
|
+
| System | Elevated(_) => ()
|
|
66
|
+
| Owned({userId}) =>
|
|
67
|
+
ownerFields->Array.forEach(field => obj->Dict.set(field, JSON.Encode.string(userId)))
|
|
68
|
+
| Unidentified(why) =>
|
|
69
|
+
JsError.throwWithMessage(
|
|
70
|
+
`Forbidden: ${serviceName}.${command} records an owner, but the caller could not be identified (${why})`,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
33
75
|
let makeGenerateCommand = (
|
|
34
76
|
~publishJsons: CommandGenerator.publishJsons,
|
|
35
77
|
~publishJsonsAndWait: option<CommandTopic.publishJsonsAndWait>=?,
|
|
@@ -58,6 +100,12 @@ let makeGenerateCommand = (
|
|
|
58
100
|
if stripIdFromParams {
|
|
59
101
|
obj->Dict.delete("id")
|
|
60
102
|
}
|
|
103
|
+
obj->stampOwnerFields(
|
|
104
|
+
~commandSchema,
|
|
105
|
+
~command=payload.command,
|
|
106
|
+
~identity=payload.identity,
|
|
107
|
+
~serviceName,
|
|
108
|
+
)
|
|
61
109
|
obj->Dict.toArray
|
|
62
110
|
}
|
|
63
111
|
| None =>
|
|
@@ -5,8 +5,10 @@ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
|
5
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
6
|
import * as Effect from "effect/Effect";
|
|
7
7
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
8
|
+
import * as Owner$Reventless from "@reventlessdev/reventless-spec/src/components/Owner.res.mjs";
|
|
8
9
|
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
9
10
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
11
|
+
import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
|
|
10
12
|
import * as Message$ReventlessCore from "../../Message.res.mjs";
|
|
11
13
|
import * as LogFormat$ReventlessCore from "../../util/LogFormat.res.mjs";
|
|
12
14
|
import * as EffectLogger$ReventlessCore from "../../util/EffectLogger.res.mjs";
|
|
@@ -24,6 +26,29 @@ function clearCommandInterceptor() {
|
|
|
24
26
|
commandInterceptorHook.contents = undefined;
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
function stampOwnerFields(obj, commandSchema, command, identity, serviceName) {
|
|
30
|
+
let ownerFields = Owner$Reventless.variantFieldNames(commandSchema, command);
|
|
31
|
+
if (ownerFields.length === 0) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
let why = OwnerScope$Reventless.resolve(identity, undefined);
|
|
35
|
+
if (typeof why !== "object") {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
switch (why.TAG) {
|
|
39
|
+
case "Elevated" :
|
|
40
|
+
return;
|
|
41
|
+
case "Owned" :
|
|
42
|
+
let userId = why.userId;
|
|
43
|
+
ownerFields.forEach(field => {
|
|
44
|
+
obj[field] = userId;
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
case "Unidentified" :
|
|
48
|
+
return Stdlib_JsError.throwWithMessage(`Forbidden: ` + serviceName + `.` + command + ` records an owner, but the caller could not be identified (` + why._0 + `)`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
27
52
|
function makeGenerateCommand(publishJsons, publishJsonsAndWait, serviceName, commandSchema, componentKind, $staropt$star) {
|
|
28
53
|
return payload => {
|
|
29
54
|
let stripIdFromParams = $staropt$star !== undefined ? $staropt$star : true;
|
|
@@ -41,7 +66,7 @@ function makeGenerateCommand(publishJsons, publishJsonsAndWait, serviceName, com
|
|
|
41
66
|
correlationId: msgId
|
|
42
67
|
};
|
|
43
68
|
let obj = Stdlib_Option.flatMap(JSON.stringify(payload.arguments), jsonString => Stdlib_JSON.Decode.object(JSON.parse(jsonString)));
|
|
44
|
-
let params = obj !== undefined ? (stripIdFromParams ? Stdlib_Dict.$$delete(obj, "id") : undefined, Object.entries(obj)) : Stdlib_JsError.throwWithMessage("Couldn't decode:" + Stdlib_Option.getOr(JSON.stringify(payload.arguments), "<payload.arguments>"));
|
|
69
|
+
let params = obj !== undefined ? (stripIdFromParams ? Stdlib_Dict.$$delete(obj, "id") : undefined, stampOwnerFields(obj, commandSchema, payload.command, payload.identity, serviceName), Object.entries(obj)) : Stdlib_JsError.throwWithMessage("Couldn't decode:" + Stdlib_Option.getOr(JSON.stringify(payload.arguments), "<payload.arguments>"));
|
|
45
70
|
let commandStr = payload.command;
|
|
46
71
|
let match = params.length;
|
|
47
72
|
let commandJson = match !== 0 ? Object.fromEntries([[
|
|
@@ -155,6 +180,7 @@ export {
|
|
|
155
180
|
commandInterceptorHook,
|
|
156
181
|
registerCommandInterceptor,
|
|
157
182
|
clearCommandInterceptor,
|
|
183
|
+
stampOwnerFields,
|
|
158
184
|
makeGenerateCommand,
|
|
159
185
|
Make,
|
|
160
186
|
}
|
|
@@ -299,10 +299,33 @@ let make = (
|
|
|
299
299
|
}
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
+
// A rule the server enforces, expressed as the keys a client checks against
|
|
303
|
+
// `identity.groups ++ config.accessTiers`. `AllowGroups` is satisfied by ANY of
|
|
304
|
+
// its groups (`Authorization.isAllowed` is `some`, not `every`), so the array is
|
|
305
|
+
// an any-of and a client must read it that way.
|
|
306
|
+
//
|
|
307
|
+
// `AllowAuthenticated` / `AllowAnonymous` ask for nothing a client can check —
|
|
308
|
+
// anyone holding a session already satisfies them — so they publish no keys
|
|
309
|
+
// rather than a key everyone holds.
|
|
310
|
+
//
|
|
311
|
+
// `DenyAll` also publishes none, deliberately. An unsatisfiable key would render
|
|
312
|
+
// as locked-with-upsell in a tiered shell: a surface advertised as purchasable
|
|
313
|
+
// that no purchase unlocks. A component nobody may call belongs in no menu, and
|
|
314
|
+
// that is an omission for the enumerating side to make, not a key to invent here.
|
|
315
|
+
let accessKeysFor = (rule: Reventless.Authorization.permission): option<array<string>> =>
|
|
316
|
+
switch rule {
|
|
317
|
+
| AllowGroups(groups) if groups->Array.length > 0 => Some(groups)
|
|
318
|
+
| AllowGroups(_) | AllowAuthenticated | AllowAnonymous | DenyAll => None
|
|
319
|
+
}
|
|
320
|
+
|
|
302
321
|
let toCommandDef = (
|
|
303
322
|
~isAggregate,
|
|
304
323
|
~mutationFieldFor: string => string,
|
|
305
324
|
~parentSchema: S.t<unknown>,
|
|
325
|
+
// The PPX-generated `command => permission`. Per VARIANT, not per component:
|
|
326
|
+
// one aggregate carries commands with very different audiences, and a
|
|
327
|
+
// component-level shortcut would gate `AddProduct` and `PlaceOrder` alike.
|
|
328
|
+
~commandAuthorization: unknown => Reventless.Authorization.permission,
|
|
306
329
|
v: S.t<unknown>,
|
|
307
330
|
): option<Reventless.Plugin.commandDef> => {
|
|
308
331
|
// Build a commandDef for one variant. `properties` is the variant's field dict —
|
|
@@ -329,6 +352,14 @@ let make = (
|
|
|
329
352
|
| Some(excluded) => !(excluded->Set.has(variantName))
|
|
330
353
|
| None => true
|
|
331
354
|
}
|
|
355
|
+
// Evaluated against a synthetic value per constructor, the same shape the
|
|
356
|
+
// resolver builds at call time: a payload-bearing variant compiles to
|
|
357
|
+
// `{TAG, ...}`, a payload-less one to a bare string.
|
|
358
|
+
let syntheticCommand: unknown =
|
|
359
|
+
Reventless.DcbTag.isVariantPayloadBearing(parentSchema, variantName)
|
|
360
|
+
? {"TAG": variantName}->Obj.magic
|
|
361
|
+
: variantName->Obj.magic
|
|
362
|
+
let requiredAccess = accessKeysFor(commandAuthorization(syntheticCommand))
|
|
332
363
|
({
|
|
333
364
|
Reventless.Plugin.name: variantName,
|
|
334
365
|
// The derived schema, not sury's raw one: `S.toJSONSchema` carries the
|
|
@@ -355,6 +386,11 @@ let make = (
|
|
|
355
386
|
allowedStates,
|
|
356
387
|
targetState,
|
|
357
388
|
apiExposed: Some(apiExposed),
|
|
389
|
+
requiredAccess,
|
|
390
|
+
// Resolved from this constructor's own properties, not the union's: two
|
|
391
|
+
// commands in one slice can disagree about whether they record an owner,
|
|
392
|
+
// and the write path stamps per constructor for the same reason.
|
|
393
|
+
ownerField: Reventless.Owner.fieldNamesOfProperties(properties)->Array.get(0),
|
|
358
394
|
}: Reventless.Plugin.commandDef)
|
|
359
395
|
}
|
|
360
396
|
switch v {
|
|
@@ -379,17 +415,29 @@ let make = (
|
|
|
379
415
|
let extractCommandDefs = (
|
|
380
416
|
~isAggregate,
|
|
381
417
|
~mutationFieldFor: string => string,
|
|
418
|
+
~commandAuthorization: unknown => Reventless.Authorization.permission,
|
|
382
419
|
commandSchema: S.t<unknown>,
|
|
383
420
|
): array<Reventless.Plugin.commandDef> =>
|
|
384
421
|
switch commandSchema {
|
|
385
422
|
| Union({anyOf}) =>
|
|
386
423
|
anyOf->Array.filterMap(v =>
|
|
387
|
-
toCommandDef(
|
|
424
|
+
toCommandDef(
|
|
425
|
+
~isAggregate,
|
|
426
|
+
~mutationFieldFor,
|
|
427
|
+
~parentSchema=commandSchema,
|
|
428
|
+
~commandAuthorization,
|
|
429
|
+
v,
|
|
430
|
+
)
|
|
388
431
|
)
|
|
389
432
|
| _ =>
|
|
390
433
|
// Single-variant command types compile to a bare Object schema, not a Union.
|
|
391
|
-
toCommandDef(
|
|
392
|
-
|
|
434
|
+
toCommandDef(
|
|
435
|
+
~isAggregate,
|
|
436
|
+
~mutationFieldFor,
|
|
437
|
+
~parentSchema=commandSchema,
|
|
438
|
+
~commandAuthorization,
|
|
439
|
+
commandSchema,
|
|
440
|
+
)->Option.mapOr([], def => [def])
|
|
393
441
|
}
|
|
394
442
|
|
|
395
443
|
// ── Per-component event type extraction ────────────────────────────────────
|
|
@@ -650,6 +698,12 @@ let make = (
|
|
|
650
698
|
let qf = Api_Naming.queryFieldNamesForReadModel(~plugin=name, ~name=R.Spec.name)
|
|
651
699
|
let stateSchema = R.Spec.stateSchema->S.castToUnknown
|
|
652
700
|
let label = labelFieldsFromStateSchema(~entityName=R.Spec.name, stateSchema)
|
|
701
|
+
// The same call the capability deriver makes, so the published key and the
|
|
702
|
+
// key the generated filter/order-by is built from cannot disagree.
|
|
703
|
+
let keyField = GraphQL_FragmentGenerator.resolveKeyField(
|
|
704
|
+
~entityName=R.Spec.name,
|
|
705
|
+
stateSchema,
|
|
706
|
+
)
|
|
653
707
|
// The events this read model projects from, qualified to the plugin's event ids so
|
|
654
708
|
// they match the producers' nodes in the graph. Was empty — which dropped projection
|
|
655
709
|
// edges for any event reaching the read model via a DCB-log-sourced mapping (a classic
|
|
@@ -665,8 +719,18 @@ let make = (
|
|
|
665
719
|
searchableFields: label.searchableFields,
|
|
666
720
|
labelFieldSource: Some(labelFieldSourceToString(label.source)),
|
|
667
721
|
statusField: statusFieldFromStateSchema(~entityName=R.Spec.name, stateSchema),
|
|
722
|
+
// Same schema `statusField` reads, so the two cannot disagree about which
|
|
723
|
+
// fields this view has.
|
|
724
|
+
ownerField: Reventless.Owner.fieldNames(stateSchema)->Array.get(0),
|
|
668
725
|
visibility: visibilityTag(R.Spec.visibility),
|
|
669
726
|
chapter: chapterOf(R.Spec.name),
|
|
727
|
+
// Taken from the `qf` record, never re-derived: `Api_Naming` is the only
|
|
728
|
+
// place that decides how `Products`/`Categories` singularise, and the
|
|
729
|
+
// point of publishing the name is that a consumer stops guessing it.
|
|
730
|
+
singleQueryField: Some(qf.singleFieldName),
|
|
731
|
+
idField: keyField->Option.map(((f, _)) => f),
|
|
732
|
+
idFieldSource: keyField->Option.map(((_, rung)) => rung),
|
|
733
|
+
requiredAccess: accessKeysFor(R.Spec.authorization),
|
|
670
734
|
}: Reventless.Plugin.queryableDef)
|
|
671
735
|
})
|
|
672
736
|
|
|
@@ -676,6 +740,10 @@ let make = (
|
|
|
676
740
|
let (_, consumed) = svsConsumed->Array.getUnsafe(i)
|
|
677
741
|
let stateSchema = SVS.Spec.stateSchema->S.castToUnknown
|
|
678
742
|
let label = labelFieldsFromStateSchema(~entityName=SVS.Spec.name, stateSchema)
|
|
743
|
+
let keyField = GraphQL_FragmentGenerator.resolveKeyField(
|
|
744
|
+
~entityName=SVS.Spec.name,
|
|
745
|
+
stateSchema,
|
|
746
|
+
)
|
|
679
747
|
({
|
|
680
748
|
Reventless.Plugin.name: SVS.Spec.name,
|
|
681
749
|
queryField: qf.listFieldName,
|
|
@@ -686,8 +754,13 @@ let make = (
|
|
|
686
754
|
searchableFields: label.searchableFields,
|
|
687
755
|
labelFieldSource: Some(labelFieldSourceToString(label.source)),
|
|
688
756
|
statusField: statusFieldFromStateSchema(~entityName=SVS.Spec.name, stateSchema),
|
|
757
|
+
ownerField: Reventless.Owner.fieldNames(stateSchema)->Array.get(0),
|
|
689
758
|
visibility: visibilityTag(SVS.Spec.visibility),
|
|
690
759
|
chapter: chapterOf(SVS.Spec.name),
|
|
760
|
+
singleQueryField: Some(qf.singleFieldName),
|
|
761
|
+
idField: keyField->Option.map(((f, _)) => f),
|
|
762
|
+
idFieldSource: keyField->Option.map(((_, rung)) => rung),
|
|
763
|
+
requiredAccess: accessKeysFor(SVS.Spec.authorization),
|
|
691
764
|
}: Reventless.Plugin.queryableDef)
|
|
692
765
|
})
|
|
693
766
|
|
|
@@ -708,6 +781,7 @@ let make = (
|
|
|
708
781
|
~commandSchema=SCS.Spec.commandSchema->S.castToUnknown,
|
|
709
782
|
~variant=variantName,
|
|
710
783
|
),
|
|
784
|
+
~commandAuthorization=SCS.Spec.commandAuthorization->Obj.magic,
|
|
711
785
|
SCS.Spec.commandSchema->S.castToUnknown,
|
|
712
786
|
),
|
|
713
787
|
producedEventTypes: produced,
|
|
@@ -731,6 +805,7 @@ let make = (
|
|
|
731
805
|
commands: extractCommandDefs(
|
|
732
806
|
~isAggregate=true,
|
|
733
807
|
~mutationFieldFor=variantName => Api_Naming.aggregateMutationField(~plugin=name, ~aggregate=A.Spec.name, ~command=variantName),
|
|
808
|
+
~commandAuthorization=A.Spec.commandAuthorization->Obj.magic,
|
|
734
809
|
A.Spec.commandSchema->S.castToUnknown,
|
|
735
810
|
),
|
|
736
811
|
producedEventTypes: produced,
|
|
@@ -4,6 +4,7 @@ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
|
4
4
|
import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
|
|
5
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
6
|
import * as Belt_SetString from "@rescript/runtime/lib/es6/Belt_SetString.js";
|
|
7
|
+
import * as Owner$Reventless from "@reventlessdev/reventless-spec/src/components/Owner.res.mjs";
|
|
7
8
|
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
8
9
|
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
9
10
|
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
@@ -19,6 +20,7 @@ import * as SuryToJsonSchema$ReventlessCore from "../../components/Api/SuryToJso
|
|
|
19
20
|
import * as Capability_Inference$ReventlessCore from "./Capability_Inference.res.mjs";
|
|
20
21
|
import * as ApiTargetStateHelpers$ReventlessCore from "../../components/Api/ApiTargetStateHelpers.res.mjs";
|
|
21
22
|
import * as ApiAllowedStatesHelpers$ReventlessCore from "../../components/Api/ApiAllowedStatesHelpers.res.mjs";
|
|
23
|
+
import * as GraphQL_FragmentGenerator$ReventlessCore from "../../components/Api/GraphQL_FragmentGenerator.res.mjs";
|
|
22
24
|
|
|
23
25
|
let log = Logger$ReventlessCore.fromEnv();
|
|
24
26
|
|
|
@@ -268,7 +270,16 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
268
270
|
];
|
|
269
271
|
}
|
|
270
272
|
};
|
|
271
|
-
let
|
|
273
|
+
let accessKeysFor = rule => {
|
|
274
|
+
if (typeof rule !== "object") {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
let groups = rule._0;
|
|
278
|
+
if (groups.length !== 0) {
|
|
279
|
+
return groups;
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
let toCommandDef = (isAggregate, mutationFieldFor, parentSchema, commandAuthorization, v) => {
|
|
272
283
|
let mkDef = (variantName, properties) => {
|
|
273
284
|
let match = commandLevelAndId(isAggregate, variantName, properties);
|
|
274
285
|
let references = extractReferences(properties);
|
|
@@ -279,6 +290,10 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
279
290
|
let excluded = ApiNoApiHelpers$ReventlessCore.getExcludedVariants(parentSchema);
|
|
280
291
|
apiExposed = excluded !== undefined ? !excluded.has(variantName) : true;
|
|
281
292
|
}
|
|
293
|
+
let syntheticCommand = DcbTag$Reventless.isVariantPayloadBearing(parentSchema, variantName) ? ({
|
|
294
|
+
TAG: variantName
|
|
295
|
+
}) : variantName;
|
|
296
|
+
let requiredAccess = accessKeysFor(commandAuthorization(syntheticCommand));
|
|
282
297
|
return {
|
|
283
298
|
name: variantName,
|
|
284
299
|
schema: JSON.stringify(SuryToJsonSchema$ReventlessCore.deriveObjectSchema(v)),
|
|
@@ -288,7 +303,9 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
288
303
|
references: references,
|
|
289
304
|
allowedStates: allowedStates,
|
|
290
305
|
targetState: targetState,
|
|
291
|
-
apiExposed: apiExposed
|
|
306
|
+
apiExposed: apiExposed,
|
|
307
|
+
requiredAccess: requiredAccess,
|
|
308
|
+
ownerField: Owner$Reventless.fieldNamesOfProperties(properties)[0]
|
|
292
309
|
};
|
|
293
310
|
};
|
|
294
311
|
switch (v.type) {
|
|
@@ -314,11 +331,11 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
314
331
|
return;
|
|
315
332
|
}
|
|
316
333
|
};
|
|
317
|
-
let extractCommandDefs = (isAggregate, mutationFieldFor, commandSchema) => {
|
|
334
|
+
let extractCommandDefs = (isAggregate, mutationFieldFor, commandAuthorization, commandSchema) => {
|
|
318
335
|
if (commandSchema.type === "union") {
|
|
319
|
-
return Stdlib_Array.filterMap(commandSchema.anyOf, v => toCommandDef(isAggregate, mutationFieldFor, commandSchema, v));
|
|
336
|
+
return Stdlib_Array.filterMap(commandSchema.anyOf, v => toCommandDef(isAggregate, mutationFieldFor, commandSchema, commandAuthorization, v));
|
|
320
337
|
} else {
|
|
321
|
-
return Stdlib_Option.mapOr(toCommandDef(isAggregate, mutationFieldFor, commandSchema, commandSchema), [], def => [def]);
|
|
338
|
+
return Stdlib_Option.mapOr(toCommandDef(isAggregate, mutationFieldFor, commandSchema, commandAuthorization, commandSchema), [], def => [def]);
|
|
322
339
|
}
|
|
323
340
|
};
|
|
324
341
|
let scsProduced = stateChangeSlices.map(SCS => [
|
|
@@ -500,6 +517,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
500
517
|
let qf = Api_Naming$ReventlessCore.queryFieldNamesForReadModel(name, R.Spec.name, undefined);
|
|
501
518
|
let stateSchema = R.Spec.stateSchema;
|
|
502
519
|
let label = labelFieldsFromStateSchema(R.Spec.name, stateSchema);
|
|
520
|
+
let keyField = GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField(R.Spec.name, stateSchema);
|
|
503
521
|
let consumed = qualify(name, R.consumedEventNames);
|
|
504
522
|
return {
|
|
505
523
|
name: R.Spec.name,
|
|
@@ -511,8 +529,13 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
511
529
|
searchableFields: label.searchableFields,
|
|
512
530
|
labelFieldSource: labelFieldSourceToString(label.source),
|
|
513
531
|
statusField: statusFieldFromStateSchema(R.Spec.name, stateSchema),
|
|
532
|
+
ownerField: Owner$Reventless.fieldNames(stateSchema)[0],
|
|
514
533
|
visibility: visibilityTag(R.Spec.visibility),
|
|
515
|
-
chapter: componentChapters[R.Spec.name]
|
|
534
|
+
chapter: componentChapters[R.Spec.name],
|
|
535
|
+
singleQueryField: qf.singleFieldName,
|
|
536
|
+
idField: Stdlib_Option.map(keyField, param => param[0]),
|
|
537
|
+
idFieldSource: Stdlib_Option.map(keyField, param => param[1]),
|
|
538
|
+
requiredAccess: accessKeysFor(R.Spec.authorization)
|
|
516
539
|
};
|
|
517
540
|
});
|
|
518
541
|
let stateViewDefs = stateViewSlices.map((SVS, i) => {
|
|
@@ -521,6 +544,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
521
544
|
let consumed = match[1];
|
|
522
545
|
let stateSchema = SVS.Spec.stateSchema;
|
|
523
546
|
let label = labelFieldsFromStateSchema(SVS.Spec.name, stateSchema);
|
|
547
|
+
let keyField = GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField(SVS.Spec.name, stateSchema);
|
|
524
548
|
return {
|
|
525
549
|
name: SVS.Spec.name,
|
|
526
550
|
queryField: qf.listFieldName,
|
|
@@ -531,8 +555,13 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
531
555
|
searchableFields: label.searchableFields,
|
|
532
556
|
labelFieldSource: labelFieldSourceToString(label.source),
|
|
533
557
|
statusField: statusFieldFromStateSchema(SVS.Spec.name, stateSchema),
|
|
558
|
+
ownerField: Owner$Reventless.fieldNames(stateSchema)[0],
|
|
534
559
|
visibility: visibilityTag(SVS.Spec.visibility),
|
|
535
|
-
chapter: componentChapters[SVS.Spec.name]
|
|
560
|
+
chapter: componentChapters[SVS.Spec.name],
|
|
561
|
+
singleQueryField: qf.singleFieldName,
|
|
562
|
+
idField: Stdlib_Option.map(keyField, param => param[0]),
|
|
563
|
+
idFieldSource: Stdlib_Option.map(keyField, param => param[1]),
|
|
564
|
+
requiredAccess: accessKeysFor(SVS.Spec.authorization)
|
|
536
565
|
};
|
|
537
566
|
});
|
|
538
567
|
let stateChangeDefs = stateChangeSlices.map((SCS, i) => {
|
|
@@ -542,7 +571,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
542
571
|
let consumed = match$1[1];
|
|
543
572
|
return {
|
|
544
573
|
name: SCS.Spec.name,
|
|
545
|
-
commands: extractCommandDefs(false, variantName => Api_Naming$ReventlessCore.sliceMutationFieldFor(name, SCS.Spec.name, SCS.Spec.commandSchema, variantName), SCS.Spec.commandSchema),
|
|
574
|
+
commands: extractCommandDefs(false, variantName => Api_Naming$ReventlessCore.sliceMutationFieldFor(name, SCS.Spec.name, SCS.Spec.commandSchema, variantName), SCS.Spec.commandAuthorization, SCS.Spec.commandSchema),
|
|
546
575
|
producedEventTypes: produced,
|
|
547
576
|
consumedEventTypes: consumed,
|
|
548
577
|
linkedViews: linkedSvsFor(produced),
|
|
@@ -557,7 +586,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
557
586
|
let produced = match[1];
|
|
558
587
|
return {
|
|
559
588
|
name: A.Spec.name,
|
|
560
|
-
commands: extractCommandDefs(true, variantName => Api_Naming$ReventlessCore.aggregateMutationField(name, A.Spec.name, variantName), A.Spec.commandSchema),
|
|
589
|
+
commands: extractCommandDefs(true, variantName => Api_Naming$ReventlessCore.aggregateMutationField(name, A.Spec.name, variantName), A.Spec.commandAuthorization, A.Spec.commandSchema),
|
|
561
590
|
producedEventTypes: produced,
|
|
562
591
|
consumedEventTypes: [],
|
|
563
592
|
linkedViews: linkedSvsFor(produced).concat(linkedReadModelsFor(A.Spec.name)),
|