@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 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.183",
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-fast-csv": "1.2.0-alpha.14",
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.105",
41
- "@reventlessdev/reventless-spec": "3.0.0-alpha.81"
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 field->String.includes("(") {
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
- mutations->Array.push(` ${fieldName}(id: ID!): CommandResult!`)
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 = field.includes("(") ? field.replace(fieldName + `(`, fieldName + `(id: ID!, `) : field.replace(fieldName + `:`, fieldName + `(id: ID!):`);
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
- mutations.push(` ` + fieldName + `(id: ID!): CommandResult!`);
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 */