@reventlessdev/reventless-core 3.0.0-alpha.183 → 3.0.0-alpha.185
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 +14 -0
- package/package.json +7 -7
- package/src/components/Api/GraphQL_FragmentGenerator.res +13 -2
- package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +6 -2
- package/src/components/Dcb/Dcb_Builder.res +14 -2
- package/src/components/Dcb/Dcb_Builder.res.mjs +4 -2
- package/tests/api/GraphQL_FragmentGeneratorTest.res +49 -0
- package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +57 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
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.185 (2026-07-27)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **api:** don't inject id: ID! into multi-variant DCB slice mutations ([03cfaad](https://github.com/ReventlessDev/reventless-core/commit/03cfaadce3bee3d7010835edc10f57ab45ddfa5b))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.184 (2026-07-27)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **aws:** grant DCB command Lambda write access to InboundTranslationSlice audit tables ([2c445fa](https://github.com/ReventlessDev/reventless-core/commit/2c445faf9692573d0eab2f0d4a456e8c5d723b41))
|
|
18
|
+
|
|
19
|
+
|
|
6
20
|
# 3.0.0-alpha.183 (2026-07-27)
|
|
7
21
|
|
|
8
22
|
### 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.185",
|
|
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.31",
|
|
32
|
-
"@reventlessdev/rescript-
|
|
32
|
+
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.13",
|
|
33
33
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.9",
|
|
34
|
+
"@reventlessdev/rescript-fast-csv": "1.2.0-alpha.14",
|
|
34
35
|
"@reventlessdev/rescript-node-streams": "1.1.0-alpha.13",
|
|
35
|
-
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.13",
|
|
36
36
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.17",
|
|
37
|
-
"@reventlessdev/rescript-uuid": "1.1.0-alpha.17",
|
|
38
|
-
"@reventlessdev/reventless-interop": "3.0.0-alpha.29",
|
|
39
37
|
"@reventlessdev/rescript-ssh2": "1.1.0-alpha.14",
|
|
40
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.
|
|
41
|
-
"@reventlessdev/
|
|
38
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.106",
|
|
39
|
+
"@reventlessdev/rescript-uuid": "1.1.0-alpha.17",
|
|
40
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.81",
|
|
41
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.29"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"rescript": "12.3.0",
|
|
@@ -396,6 +396,12 @@ let generate = (
|
|
|
396
396
|
|
|
397
397
|
mutationEntries->Array.forEach(entry => {
|
|
398
398
|
let schema = entry.commandSchema
|
|
399
|
+
// Aggregates carry the instance id as a separate `id: ID!` argument ahead of
|
|
400
|
+
// the command payload; DCB slices don't (their key field is part of the
|
|
401
|
+
// payload). A multi-variant slice command is a sury `Union` just like an
|
|
402
|
+
// aggregate command, so the injection must key off the entry flag, not the
|
|
403
|
+
// schema shape — see `mutationSchemaEntry.injectIdArg`.
|
|
404
|
+
let injectIdArg = entry.injectIdArg->Option.getOr(true)
|
|
399
405
|
switch schema {
|
|
400
406
|
| Union({anyOf}) =>
|
|
401
407
|
// Map each fieldName to its variant in anyOf by matching the constructor
|
|
@@ -433,7 +439,9 @@ let generate = (
|
|
|
433
439
|
variantSchema,
|
|
434
440
|
) {
|
|
435
441
|
| Some(field) =>
|
|
436
|
-
let withId = if
|
|
442
|
+
let withId = if !injectIdArg {
|
|
443
|
+
field
|
|
444
|
+
} else if field->String.includes("(") {
|
|
437
445
|
field->String.replace(`${fieldName}(`, `${fieldName}(id: ID!, `)
|
|
438
446
|
} else {
|
|
439
447
|
field->String.replace(`${fieldName}:`, `${fieldName}(id: ID!):`)
|
|
@@ -442,7 +450,10 @@ let generate = (
|
|
|
442
450
|
| None =>
|
|
443
451
|
// Payload-less variant (S.literal("Ctor")) — no Object fields to
|
|
444
452
|
// derive args from, but still a valid mutation.
|
|
445
|
-
|
|
453
|
+
let field = injectIdArg
|
|
454
|
+
? ` ${fieldName}(id: ID!): CommandResult!`
|
|
455
|
+
: ` ${fieldName}: CommandResult!`
|
|
456
|
+
mutations->Array.push(field)
|
|
446
457
|
}
|
|
447
458
|
})
|
|
448
459
|
| Object(_) =>
|
|
@@ -319,6 +319,7 @@ function generate(mutationEntries, queryEntries) {
|
|
|
319
319
|
let constructorNameOf = fieldName => Stdlib_Option.getOr(fieldName.split("_")[fieldName.split("_").length - 1 | 0], fieldName);
|
|
320
320
|
mutationEntries.forEach(entry => {
|
|
321
321
|
let schema = entry.commandSchema;
|
|
322
|
+
let injectIdArg = Stdlib_Option.getOr(entry.injectIdArg, true);
|
|
322
323
|
switch (schema.type) {
|
|
323
324
|
case "object" :
|
|
324
325
|
let fieldName = Stdlib_Option.getOr(entry.fieldNames[0], "");
|
|
@@ -361,11 +362,14 @@ function generate(mutationEntries, queryEntries) {
|
|
|
361
362
|
let variantSchema = variantIndex >= 0 ? Stdlib_Option.getOr(anyOf[variantIndex], schema) : schema;
|
|
362
363
|
let field = deriveMutationFieldFromObject(fieldName, types, seenTypes, variantSchema);
|
|
363
364
|
if (field !== undefined) {
|
|
364
|
-
let withId =
|
|
365
|
+
let withId = injectIdArg ? (
|
|
366
|
+
field.includes("(") ? field.replace(fieldName + `(`, fieldName + `(id: ID!, `) : field.replace(fieldName + `:`, fieldName + `(id: ID!):`)
|
|
367
|
+
) : field;
|
|
365
368
|
mutations.push(withId);
|
|
366
369
|
return;
|
|
367
370
|
}
|
|
368
|
-
|
|
371
|
+
let field$1 = injectIdArg ? ` ` + fieldName + `(id: ID!): CommandResult!` : ` ` + fieldName + `: CommandResult!`;
|
|
372
|
+
mutations.push(field$1);
|
|
369
373
|
});
|
|
370
374
|
return;
|
|
371
375
|
default:
|
|
@@ -739,12 +739,19 @@ module Make = (
|
|
|
739
739
|
composite->Obj.magic
|
|
740
740
|
})
|
|
741
741
|
|
|
742
|
-
// Resources the sync Lambda needs access to
|
|
742
|
+
// Resources the sync Lambda needs access to. An InboundTranslationSlice
|
|
743
|
+
// exposes its audit table under `queryDb.resources` — its top-level
|
|
744
|
+
// `resources` is empty — and the sync DCB command Lambda is the Route 0
|
|
745
|
+
// target that writes those audit rows. Granting only `outputs.resources`
|
|
746
|
+
// left the Lambda role without `PutItem` on the audit table, so every
|
|
747
|
+
// audit write failed with AccessDenied (swallowed at the write site,
|
|
748
|
+
// surfacing only as a permanently empty audit view). Include the audit
|
|
749
|
+
// table's resources so the role can write it.
|
|
743
750
|
let dcbResources = Array.concat(
|
|
744
751
|
stateChangeSlicesOutputs->Dict.valuesToArray->Array.flatMap(outputs => outputs.resources),
|
|
745
752
|
inboundTranslationSlicesOutputs
|
|
746
753
|
->Dict.valuesToArray
|
|
747
|
-
->Array.flatMap(outputs => outputs.resources),
|
|
754
|
+
->Array.flatMap(outputs => Array.concat(outputs.resources, outputs.queryDb.resources)),
|
|
748
755
|
)
|
|
749
756
|
|
|
750
757
|
// Resources the async Lambda needs access to
|
|
@@ -907,6 +914,10 @@ module Make = (
|
|
|
907
914
|
ReventlessInfra.Api.fieldNames: fieldSpecs->Array.map(((f, _)) => f),
|
|
908
915
|
commandSchema,
|
|
909
916
|
fieldPermissions,
|
|
917
|
+
// Slice key fields (e.g. `orderId`) are payload args; there is no
|
|
918
|
+
// separate aggregate `id`. A multi-variant slice command is a sury
|
|
919
|
+
// `Union`, so without this the generator would inject `id: ID!`.
|
|
920
|
+
injectIdArg: false,
|
|
910
921
|
systemCallable: systemCallableComponents->Array.includes(S.Spec.name),
|
|
911
922
|
linkedViews: ?sliceDef->Option.map(d => d.linkedViews),
|
|
912
923
|
consistencyRead: ?sliceDef->Option.flatMap(d => d.consistencyRead),
|
|
@@ -931,6 +942,7 @@ module Make = (
|
|
|
931
942
|
ReventlessInfra.Api.fieldNames: [fieldName],
|
|
932
943
|
commandSchema: ITS.Spec.externalInputSchema->S.castToUnknown,
|
|
933
944
|
fieldPermissions,
|
|
945
|
+
injectIdArg: false,
|
|
934
946
|
}
|
|
935
947
|
})
|
|
936
948
|
|
|
@@ -398,7 +398,7 @@ function Make(DcbEventLogStorage) {
|
|
|
398
398
|
}
|
|
399
399
|
};
|
|
400
400
|
});
|
|
401
|
-
let dcbResources = Object.values(stateChangeSlicesOutputs).flatMap(outputs => outputs.resources).concat(Object.values(inboundTranslationSlicesOutputs).flatMap(outputs => outputs.resources));
|
|
401
|
+
let dcbResources = Object.values(stateChangeSlicesOutputs).flatMap(outputs => outputs.resources).concat(Object.values(inboundTranslationSlicesOutputs).flatMap(outputs => outputs.resources.concat(outputs.queryDb.resources)));
|
|
402
402
|
let asyncDcbResources = Object.values(asyncStateChangeSlicesOutputs).flatMap(outputs => outputs.resources);
|
|
403
403
|
let inboundFieldNames = inboundTranslationSliceData.map(param => param[1]);
|
|
404
404
|
let inboundSchemas = inboundTranslationSliceData.map(param => param[3]);
|
|
@@ -484,6 +484,7 @@ function Make(DcbEventLogStorage) {
|
|
|
484
484
|
commandSchema: commandSchema,
|
|
485
485
|
fieldPermissions: fieldPermissions,
|
|
486
486
|
systemCallable: systemCallableComponents.includes(S.Spec.name),
|
|
487
|
+
injectIdArg: false,
|
|
487
488
|
linkedViews: Stdlib_Option.map(sliceDef, d => d.linkedViews),
|
|
488
489
|
consistencyRead: Stdlib_Option.flatMap(sliceDef, d => d.consistencyRead)
|
|
489
490
|
};
|
|
@@ -498,7 +499,8 @@ function Make(DcbEventLogStorage) {
|
|
|
498
499
|
return {
|
|
499
500
|
fieldNames: [fieldName],
|
|
500
501
|
commandSchema: ITS.Spec.externalInputSchema,
|
|
501
|
-
fieldPermissions: fieldPermissions
|
|
502
|
+
fieldPermissions: fieldPermissions,
|
|
503
|
+
injectIdArg: false
|
|
502
504
|
};
|
|
503
505
|
});
|
|
504
506
|
let stateViewEntries = stateViewSlices.map(V => {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// A multi-variant command union serializes as a sury `Union`, the same shape an
|
|
4
|
+
// aggregate command produces. The generator must decide whether to inject the
|
|
5
|
+
// aggregate-style `id: ID!` argument from the entry's `injectIdArg` flag, not
|
|
6
|
+
// from the schema shape — otherwise a multi-variant DCB slice (e.g. CancelOrder
|
|
7
|
+
// + @noApi ReopenOrder) wrongly inherits a spurious `id: ID!` and callers that
|
|
8
|
+
// send only the slice's own key field get `MissingFieldArgument: id`.
|
|
9
|
+
module OrderSliceCmd = {
|
|
10
|
+
@schema
|
|
11
|
+
type command =
|
|
12
|
+
| CancelOrder({orderId: string})
|
|
13
|
+
| ReopenOrder({orderId: string})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let mutationFor = (fragment, needle) =>
|
|
17
|
+
GraphQL_Stitcher.decode(fragment).mutations->Array.find(m => m->String.includes(needle))
|
|
18
|
+
|
|
19
|
+
describe("GraphQL_FragmentGenerator.generate mutation id injection", () => {
|
|
20
|
+
testSync("multi-variant slice entry (injectIdArg: false) omits the id argument", () => {
|
|
21
|
+
let fragment = GraphQL_FragmentGenerator.generate(
|
|
22
|
+
~mutationEntries=[
|
|
23
|
+
{
|
|
24
|
+
ReventlessInfra.Api.fieldNames: ["Ordering_CancelOrder"],
|
|
25
|
+
commandSchema: OrderSliceCmd.commandSchema->S.castToUnknown,
|
|
26
|
+
injectIdArg: false,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
~queryEntries=[],
|
|
30
|
+
)
|
|
31
|
+
let field = mutationFor(fragment, "Ordering_CancelOrder")->Option.getOr("")
|
|
32
|
+
expect(field->String.includes("orderId"))->toBe(true)
|
|
33
|
+
expect(field->String.includes("id: ID!"))->toBe(false)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
testSync("aggregate-style entry (default) injects id: ID! ahead of the payload", () => {
|
|
37
|
+
let fragment = GraphQL_FragmentGenerator.generate(
|
|
38
|
+
~mutationEntries=[
|
|
39
|
+
{
|
|
40
|
+
ReventlessInfra.Api.fieldNames: ["Ordering_CancelOrder"],
|
|
41
|
+
commandSchema: OrderSliceCmd.commandSchema->S.castToUnknown,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
~queryEntries=[],
|
|
45
|
+
)
|
|
46
|
+
let field = mutationFor(fragment, "Ordering_CancelOrder")->Option.getOr("")
|
|
47
|
+
expect(field->String.includes("id: ID!, orderId"))->toBe(true)
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as GraphQL_Stitcher$ReventlessCore from "../../src/components/Api/GraphQL_Stitcher.res.mjs";
|
|
6
|
+
import * as GraphQL_FragmentGenerator$ReventlessCore from "../../src/components/Api/GraphQL_FragmentGenerator.res.mjs";
|
|
7
|
+
|
|
8
|
+
let commandSchema = S.union([
|
|
9
|
+
S.schema(s => ({
|
|
10
|
+
TAG: "CancelOrder",
|
|
11
|
+
orderId: s.m(S.string)
|
|
12
|
+
})),
|
|
13
|
+
S.schema(s => ({
|
|
14
|
+
TAG: "ReopenOrder",
|
|
15
|
+
orderId: s.m(S.string)
|
|
16
|
+
}))
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
function commandAuthorization(param) {
|
|
20
|
+
return "AllowAuthenticated";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let OrderSliceCmd = {
|
|
24
|
+
commandSchema: commandSchema,
|
|
25
|
+
commandAuthorization: commandAuthorization
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function mutationFor(fragment, needle) {
|
|
29
|
+
return GraphQL_Stitcher$ReventlessCore.decode(fragment).mutations.find(m => m.includes(needle));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
globalThis.describe("GraphQL_FragmentGenerator.generate mutation id injection", () => {
|
|
33
|
+
globalThis.test("multi-variant slice entry (injectIdArg: false) omits the id argument", () => {
|
|
34
|
+
let fragment = GraphQL_FragmentGenerator$ReventlessCore.generate([{
|
|
35
|
+
fieldNames: ["Ordering_CancelOrder"],
|
|
36
|
+
commandSchema: commandSchema,
|
|
37
|
+
injectIdArg: false
|
|
38
|
+
}], []);
|
|
39
|
+
let field = Stdlib_Option.getOr(mutationFor(fragment, "Ordering_CancelOrder"), "");
|
|
40
|
+
globalThis.expect(field.includes("orderId")).toBe(true);
|
|
41
|
+
globalThis.expect(field.includes("id: ID!")).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
globalThis.test("aggregate-style entry (default) injects id: ID! ahead of the payload", () => {
|
|
44
|
+
let fragment = GraphQL_FragmentGenerator$ReventlessCore.generate([{
|
|
45
|
+
fieldNames: ["Ordering_CancelOrder"],
|
|
46
|
+
commandSchema: commandSchema
|
|
47
|
+
}], []);
|
|
48
|
+
let field = Stdlib_Option.getOr(mutationFor(fragment, "Ordering_CancelOrder"), "");
|
|
49
|
+
globalThis.expect(field.includes("id: ID!, orderId")).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export {
|
|
54
|
+
OrderSliceCmd,
|
|
55
|
+
mutationFor,
|
|
56
|
+
}
|
|
57
|
+
/* commandSchema Not a pure module */
|