@reventlessdev/reventless-aws 3.0.0-alpha.232 → 3.0.0-alpha.233

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.
Files changed (104) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +8 -8
  3. package/src/adapter/Api/AppSyncEventsSigner_Ops.res +137 -0
  4. package/src/adapter/Api/AppSyncEventsSigner_Ops.res.mjs +121 -0
  5. package/src/adapter/Api/Platform_AdminScan_Ops.res +136 -0
  6. package/src/adapter/Api/Platform_AdminScan_Ops.res.mjs +98 -0
  7. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +12 -90
  8. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +11 -84
  9. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res +86 -0
  10. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res.mjs +79 -0
  11. package/src/adapter/Api/Platform_UIFragments_Lambda.res +9 -79
  12. package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +7 -143
  13. package/src/adapter/Api/Platform_UIFragments_Lambda_Ops.res +48 -0
  14. package/src/adapter/Api/Platform_UIFragments_Lambda_Ops.res.mjs +63 -0
  15. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res +21 -80
  16. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res.mjs +23 -68
  17. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync_Ops.res +70 -0
  18. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync_Ops.res.mjs +68 -0
  19. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +1 -1
  20. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +1 -1
  21. package/src/adapter/Postgres/PgMigration_Builder.res +1 -1
  22. package/src/adapter/Postgres/PgMigration_Builder.res.mjs +1 -1
  23. package/src/adapter/Runtime/AutomationSliceEntryPoint.mjs +42 -126
  24. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +246 -0
  25. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs +145 -0
  26. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +27 -2
  27. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +10 -3
  28. package/src/adapter/Runtime/CounterEntryPoint.mjs +19 -73
  29. package/src/adapter/Runtime/CounterEntryPoint_Ops.res +145 -0
  30. package/src/adapter/Runtime/CounterEntryPoint_Ops.res.mjs +129 -0
  31. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +11 -1
  32. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +9 -3
  33. package/src/adapter/Runtime/EventCollectorEntryPoint.mjs +52 -510
  34. package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res +816 -0
  35. package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res.mjs +651 -0
  36. package/src/adapter/Runtime/EventMapperEntryPoint.mjs +23 -67
  37. package/src/adapter/Runtime/EventMapperEntryPoint_Ops.res +126 -0
  38. package/src/adapter/Runtime/EventMapperEntryPoint_Ops.res.mjs +83 -0
  39. package/src/adapter/Runtime/ExtensionPointEntryPoint.mjs +16 -63
  40. package/src/adapter/Runtime/ExtensionPointEntryPoint_Ops.res +150 -0
  41. package/src/adapter/Runtime/ExtensionPointEntryPoint_Ops.res.mjs +111 -0
  42. package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.res +96 -0
  43. package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.res.mjs +70 -0
  44. package/src/adapter/Runtime/PgMigrationEntryPoint.res +54 -0
  45. package/src/adapter/Runtime/PgMigrationEntryPoint.res.mjs +37 -0
  46. package/src/adapter/Runtime/PgQueryResolverEntryPoint.mjs +20 -55
  47. package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res +155 -0
  48. package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res.mjs +94 -0
  49. package/src/adapter/Runtime/ProjectionEntryPoint_Ops.res +80 -0
  50. package/src/adapter/Runtime/ProjectionEntryPoint_Ops.res.mjs +39 -0
  51. package/src/adapter/Runtime/QueryDbEntryPoint_Ops.res +4 -4
  52. package/src/adapter/Runtime/ReadModelEntryPoint.mjs +34 -153
  53. package/src/adapter/Runtime/ReadModelEntryPoint_Ops.res +114 -0
  54. package/src/adapter/Runtime/ReadModelEntryPoint_Ops.res.mjs +83 -0
  55. package/src/adapter/Runtime/SideEffectEntryPoint.mjs +23 -64
  56. package/src/adapter/Runtime/SideEffectEntryPoint_Ops.res +71 -0
  57. package/src/adapter/Runtime/SideEffectEntryPoint_Ops.res.mjs +57 -0
  58. package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +34 -165
  59. package/src/adapter/Runtime/StateViewSliceEntryPoint_Ops.res +206 -0
  60. package/src/adapter/Runtime/StateViewSliceEntryPoint_Ops.res.mjs +105 -0
  61. package/src/adapter/Runtime/StreamRoutedEntryPoint_Ops.res +156 -0
  62. package/src/adapter/Runtime/StreamRoutedEntryPoint_Ops.res.mjs +123 -0
  63. package/src/adapter/Runtime/TaskBucketEntryPoint.mjs +18 -138
  64. package/src/adapter/Runtime/TaskBucketEntryPoint_Ops.res +198 -0
  65. package/src/adapter/Runtime/TaskBucketEntryPoint_Ops.res.mjs +155 -0
  66. package/src/adapter/StateTopic/StateTopic_AppSync.res +12 -170
  67. package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +7 -167
  68. package/src/adapter/StateTopic/StateTopic_AppSync_Ops.res +208 -0
  69. package/src/adapter/StateTopic/StateTopic_AppSync_Ops.res.mjs +183 -0
  70. package/src/components/AutomationSlice_Builder.res +2 -0
  71. package/src/components/AutomationSlice_Builder.res.mjs +1 -1
  72. package/src/components/OutboundTranslationSlice_Builder.res +1 -0
  73. package/src/components/OutboundTranslationSlice_Builder.res.mjs +1 -1
  74. package/tests/AppSyncEventsSigner_OpsTest.res +75 -0
  75. package/tests/AppSyncEventsSigner_OpsTest.res.mjs +44 -0
  76. package/tests/AppSyncEventsSigner_ParityFixture.mjs +36 -0
  77. package/tests/AutomationSliceEntryPoint_OpsTest.res +165 -0
  78. package/tests/AutomationSliceEntryPoint_OpsTest.res.mjs +280 -0
  79. package/tests/CounterEntryPoint_OpsTest.res +115 -0
  80. package/tests/CounterEntryPoint_OpsTest.res.mjs +150 -0
  81. package/tests/EventCollectorEntryPoint_OpsTest.res +216 -0
  82. package/tests/EventCollectorEntryPoint_OpsTest.res.mjs +339 -0
  83. package/tests/EventMapperEntryPoint_OpsTest.res +103 -0
  84. package/tests/EventMapperEntryPoint_OpsTest.res.mjs +120 -0
  85. package/tests/ExtensionPointEntryPoint_OpsTest.res +57 -0
  86. package/tests/ExtensionPointEntryPoint_OpsTest.res.mjs +54 -0
  87. package/tests/PgMigrationEntryPoint_IntegrationTest.res +22 -31
  88. package/tests/PgMigrationEntryPoint_IntegrationTest.res.mjs +20 -23
  89. package/tests/PgQueryResolverEntryPoint_OpsTest.res +38 -0
  90. package/tests/PgQueryResolverEntryPoint_OpsTest.res.mjs +31 -0
  91. package/tests/Platform_AdminScan_OpsTest.res +80 -0
  92. package/tests/Platform_AdminScan_OpsTest.res.mjs +127 -0
  93. package/tests/ReadModelEntryPoint_OpsTest.res +141 -0
  94. package/tests/ReadModelEntryPoint_OpsTest.res.mjs +267 -0
  95. package/tests/SideEffectEntryPoint_OpsTest.res +79 -0
  96. package/tests/SideEffectEntryPoint_OpsTest.res.mjs +105 -0
  97. package/tests/StateViewSliceEntryPoint_OpsTest.res +167 -0
  98. package/tests/StateViewSliceEntryPoint_OpsTest.res.mjs +296 -0
  99. package/tests/TaskBucketEntryPoint_OpsTest.res +117 -0
  100. package/tests/TaskBucketEntryPoint_OpsTest.res.mjs +165 -0
  101. package/tests/integration/StateViewSliceEntryPoint_IntegrationTest.res +42 -13
  102. package/tests/integration/StateViewSliceEntryPoint_IntegrationTest.res.mjs +29 -6
  103. package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.mjs +0 -57
  104. package/src/adapter/Runtime/PgMigrationEntryPoint.mjs +0 -40
@@ -0,0 +1,94 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
4
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as PgRuntime$ReventlessAws from "../Postgres/PgRuntime.res.mjs";
7
+ import * as HandlerFactoryHelpersMjs from "./HandlerFactoryHelpers.mjs";
8
+ import * as PgQueryResolver_Lambda$ReventlessAws from "../QueryDb/PgQueryResolver_Lambda.res.mjs";
9
+ import * as ProjectionEntryPoint_Ops$ReventlessAws from "./ProjectionEntryPoint_Ops.res.mjs";
10
+ import * as QueryEnginePostgres$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/QueryEnginePostgres.res.mjs";
11
+ import * as GraphQL_FragmentGenerator$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_FragmentGenerator.res.mjs";
12
+ import * as QueryDbStorage_Postgres_Runtime$ReventlessAws from "../QueryDb/QueryDbStorage_Postgres_Runtime.res.mjs";
13
+
14
+ function logDebug(prim0, prim1) {
15
+ HandlerFactoryHelpersMjs.log.debug(prim0, prim1);
16
+ }
17
+
18
+ function strOf(obj, key) {
19
+ return Stdlib_Option.flatMap(obj[key], Stdlib_JSON.Decode.string);
20
+ }
21
+
22
+ function decodeEntry(json) {
23
+ return Stdlib_Option.map(Stdlib_JSON.Decode.object(json), h => ({
24
+ readModelName: Stdlib_Option.getOr(strOf(h, "readModelName"), ""),
25
+ specModule: Stdlib_Option.getOr(strOf(h, "specModule"), ""),
26
+ labelField: Stdlib_Option.getOr(strOf(h, "labelField"), ""),
27
+ includeIdParam: Stdlib_Option.getOr(Stdlib_Option.flatMap(h["includeIdParam"], Stdlib_JSON.Decode.bool), false)
28
+ }));
29
+ }
30
+
31
+ function parseResolverConfig(rawJson) {
32
+ let obj = Stdlib_Option.getOr(Stdlib_JSON.Decode.object(JSON.parse(rawJson === "" ? `{"handlers":[]}` : rawJson)), {});
33
+ return {
34
+ pgConnection: Stdlib_Option.map(Stdlib_Option.filter(obj["pgConnection"], j => j !== null), prim => prim),
35
+ handlers: Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_Option.flatMap(obj["handlers"], Stdlib_JSON.Decode.array), []), decodeEntry),
36
+ nodeTypes: Object.fromEntries(Stdlib_Array.filterMap(Object.entries(Stdlib_Option.getOr(Stdlib_Option.flatMap(obj["nodeTypes"], Stdlib_JSON.Decode.object), {})), param => {
37
+ let k = param[0];
38
+ return Stdlib_Option.map(Stdlib_JSON.Decode.string(param[1]), s => [
39
+ k,
40
+ s
41
+ ]);
42
+ }))
43
+ };
44
+ }
45
+
46
+ function registerNodeTypes(config) {
47
+ Object.entries(config.nodeTypes).forEach(param => PgQueryResolver_Lambda$ReventlessAws.registerNodeType(param[0], param[1]));
48
+ }
49
+
50
+ function makePushdowns(pgConnection) {
51
+ let pool = PgRuntime$ReventlessAws.poolFor(pgConnection);
52
+ let Engine = QueryEnginePostgres$ReventlessPostgres.Make({
53
+ pool: pool
54
+ });
55
+ return {
56
+ indexLookup: Engine.indexLookup,
57
+ byIds: Engine.byIds,
58
+ listPage: Engine.listPage,
59
+ itemsPage: Engine.itemsPage,
60
+ scanAll: readModelName => Engine.scan(readModelName, [], 100000)
61
+ };
62
+ }
63
+
64
+ function registerBinding(pushdowns, pgConnection, entry, spec) {
65
+ let indexes = ProjectionEntryPoint_Ops$ReventlessAws.indexesOf(spec.config);
66
+ let subIdField = ProjectionEntryPoint_Ops$ReventlessAws.subIdFieldOf(spec.subIdConfig);
67
+ PgQueryResolver_Lambda$ReventlessAws.register(entry.readModelName, {
68
+ ops: QueryDbStorage_Postgres_Runtime$ReventlessAws.opsFor(pgConnection, entry.readModelName, indexes, subIdField),
69
+ pushdowns: pushdowns,
70
+ indexes: indexes,
71
+ subIdField: subIdField,
72
+ capability: GraphQL_FragmentGenerator$ReventlessCore.deriveServerCapability(spec.stateSchema),
73
+ labelField: entry.labelField,
74
+ includeIdParam: entry.includeIdParam,
75
+ authorization: spec.authorization
76
+ });
77
+ HandlerFactoryHelpersMjs.log.debug("registered resolver binding for " + entry.readModelName, {
78
+ comp: "PgQueryResolver"
79
+ });
80
+ }
81
+
82
+ let scanAllLimit = 100000;
83
+
84
+ export {
85
+ logDebug,
86
+ strOf,
87
+ decodeEntry,
88
+ parseResolverConfig,
89
+ registerNodeTypes,
90
+ scanAllLimit,
91
+ makePushdowns,
92
+ registerBinding,
93
+ }
94
+ /* PgRuntime-ReventlessAws Not a pure module */
@@ -0,0 +1,80 @@
1
+ // Typed QueryDb-assembly core shared by the two projection Lambda entry points
2
+ // (ReadModelEntryPoint.mjs, StateViewSliceEntryPoint.mjs).
3
+ //
4
+ // The "typed core, thin shell" split (docs/plans/minimize-lambda-entrypoint-mjs-shell.md):
5
+ // both shells build the QueryDb operation set (DynamoDB or Postgres, live-update
6
+ // wrapped) for each projection from spec-module reads; that machinery lives
7
+ // here, fully type-checked. The routed dispatch boundary they share (with the
8
+ // AutomationSlice entry point, which has no Postgres backend) lives in
9
+ // StreamRoutedEntryPoint_Ops — kept separate so this module's `pg`/PgRuntime
10
+ // imports stay out of the graphs that only need dispatch.
11
+ //
12
+ // Deliberately NOT folded into QueryDbEntryPoint_Ops: that module is also
13
+ // imported by DcbCommandTopicEntryPoint.mjs, and the Postgres branch here
14
+ // would drag `pg`/PgRuntime into the DCB command Lambda's cold-start graph.
15
+ //
16
+ // Runtime-pure: no `open PulumiAws` values — Pulumi appears in type positions
17
+ // only (erased).
18
+
19
+ @val @scope("process") external processEnv: dict<string> = "env"
20
+
21
+ // ── Runtime-loaded spec-module reads (typed at the seam) ────────────────────
22
+ // The shells read `config` / `subIdConfig` off dynamically imported spec
23
+ // modules; these are the shapes those reads are typed against. `indexes` is
24
+ // optional so a module whose `config` lacks the field degrades to `[]` exactly
25
+ // as the former JS `(config && config.indexes) || []` did.
26
+
27
+ type specConfig = {indexes?: array<Reventless.ReadModel.indexConfig>}
28
+
29
+ let indexesOf = (config: option<specConfig>): array<Reventless.ReadModel.indexConfig> =>
30
+ config->Option.flatMap(c => c.indexes)->Option.getOr([])
31
+
32
+ let subIdFieldOf = (
33
+ subIdConfig: option<Reventless.ReadModel.subIdConfig<JSON.t>>,
34
+ ): option<string> => subIdConfig->Option.map(c => c.subIdField)
35
+
36
+ // ── QueryDb operations (DynamoDB / Postgres backend branch) ─────────────────
37
+ // `pgConnection`, when present, selects the Postgres QueryDb runtime for the
38
+ // projection's view table (`queryDbTableName` is then the spec name, the shared
39
+ // `qdb_<name>` discriminator). Absent → the DynamoDB operation set.
40
+ //
41
+ // On a subscription-enabled (Stream) Postgres projection, every save/delete
42
+ // also publishes a live-update descriptor (no DynamoDB stream exists).
43
+ // `stateTopicName` (present only for stream projections) + APPSYNC_ENDPOINT
44
+ // gate it; absent → withLiveUpdates returns the ops unchanged.
45
+
46
+ type liveConfig = {
47
+ endpoint?: string,
48
+ region?: string,
49
+ topicName?: string,
50
+ subIdField?: string,
51
+ }
52
+
53
+ @module("./StateTopicPublish.mjs")
54
+ external withLiveUpdates: (
55
+ ReventlessCore.QueryDb_Adapter.operations,
56
+ liveConfig,
57
+ ) => ReventlessCore.QueryDb_Adapter.operations = "withLiveUpdates"
58
+
59
+ let makeQueryDbOps = (
60
+ ~queryDbTableName: string,
61
+ ~pgConnection: option<PgConnection.connectionConfig>,
62
+ ~stateTopicName: option<string>,
63
+ ~indexes: array<Reventless.ReadModel.indexConfig>,
64
+ ~subIdField: option<string>,
65
+ ): ReventlessCore.QueryDb_Adapter.operations =>
66
+ switch pgConnection {
67
+ | Some(connection) =>
68
+ QueryDbStorage_Postgres_Runtime.opsFor(
69
+ connection,
70
+ ~name=queryDbTableName,
71
+ ~indexes,
72
+ ~subIdField?,
73
+ )->withLiveUpdates({
74
+ endpoint: ?(processEnv->Dict.get("APPSYNC_ENDPOINT")),
75
+ region: ?(processEnv->Dict.get("AWS_REGION")),
76
+ topicName: ?stateTopicName,
77
+ subIdField: ?subIdField,
78
+ })
79
+ | None => QueryDbEntryPoint_Ops.makeDynamoQueryDbOps(~tableName=queryDbTableName)
80
+ }
@@ -0,0 +1,39 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
+ import * as StateTopicPublishMjs from "./StateTopicPublish.mjs";
5
+ import * as QueryDbEntryPoint_Ops$ReventlessAws from "./QueryDbEntryPoint_Ops.res.mjs";
6
+ import * as QueryDbStorage_Postgres_Runtime$ReventlessAws from "../QueryDb/QueryDbStorage_Postgres_Runtime.res.mjs";
7
+
8
+ function indexesOf(config) {
9
+ return Stdlib_Option.getOr(Stdlib_Option.flatMap(config, c => c.indexes), []);
10
+ }
11
+
12
+ function subIdFieldOf(subIdConfig) {
13
+ return Stdlib_Option.map(subIdConfig, c => c.subIdField);
14
+ }
15
+
16
+ function withLiveUpdates(prim0, prim1) {
17
+ return StateTopicPublishMjs.withLiveUpdates(prim0, prim1);
18
+ }
19
+
20
+ function makeQueryDbOps(queryDbTableName, pgConnection, stateTopicName, indexes, subIdField) {
21
+ if (pgConnection !== undefined) {
22
+ return StateTopicPublishMjs.withLiveUpdates(QueryDbStorage_Postgres_Runtime$ReventlessAws.opsFor(pgConnection, queryDbTableName, indexes, subIdField), {
23
+ endpoint: process.env["APPSYNC_ENDPOINT"],
24
+ region: process.env["AWS_REGION"],
25
+ topicName: stateTopicName,
26
+ subIdField: subIdField
27
+ });
28
+ } else {
29
+ return QueryDbEntryPoint_Ops$ReventlessAws.makeDynamoQueryDbOps(queryDbTableName);
30
+ }
31
+ }
32
+
33
+ export {
34
+ indexesOf,
35
+ subIdFieldOf,
36
+ withLiveUpdates,
37
+ makeQueryDbOps,
38
+ }
39
+ /* ./StateTopicPublish.mjs Not a pure module */
@@ -9,10 +9,10 @@
9
9
  // compiler-checked: a field rename/reorder or a `QueryDbStorage_DynamoDb_Runtime`
10
10
  // signature change is a build error, not a silent runtime break.
11
11
  //
12
- // The Postgres branch (`pgQdbOpsFor` + env-gated `withLiveUpdates`) and the
13
- // id-injection wrappers (`mkInjectIdSave`) stay in the shell: `pgQdbOpsFor` is
14
- // already a single typed call, and the wrappers/live-update publishing are
15
- // env-driven shell business logic, not framework-call drift.
12
+ // The Postgres branch and the id-injection wrappers live in
13
+ // ProjectionEntryPoint_Ops / ReadModelEntryPoint_Ops NOT here, because this
14
+ // module is also imported by DcbCommandTopicEntryPoint.mjs, which must not
15
+ // carry `pg`/PgRuntime in its cold-start graph.
16
16
 
17
17
  let makeDynamoQueryDbOps = (~tableName: string): ReventlessCore.QueryDb_Adapter.operations => {
18
18
  let table: Util_DynamoDb_Runtime.resolvedTable = {
@@ -1,47 +1,25 @@
1
1
  // ReadModel Lambda entry point.
2
2
  // At cold start: reads HANDLER_CONFIG, dynamically imports Spec/Mappings modules,
3
3
  // wires ReadModel_Callback.Make, builds handler map keyed by source URN.
4
-
5
- import {
6
- patchSpecId,
7
- log,
8
- runEffect,
9
- setRequestId,
10
- extractMetaField,
11
- extractSentTimestamp,
12
- extractRetryCount,
13
- } from "./HandlerFactoryHelpers.mjs";
4
+ //
5
+ // Thin untyped shell ("typed core, thin shell" —
6
+ // docs/plans/minimize-lambda-entrypoint-mjs-shell.md): this file owns ONLY the
7
+ // seams that are inherently untyped — the dynamic `import()` of spec/mappings
8
+ // modules named in HANDLER_CONFIG, the shape fix-ups on those runtime-loaded
9
+ // modules (patchSpecId / fixMappingsModule), and the ReadModel_Callback.Make
10
+ // functor application consuming them. Everything else (HANDLER_CONFIG parsing,
11
+ // the QueryDb operation assembly incl. the id-injection wrap and the
12
+ // Postgres / live-update branch, handler registration, the routed dispatch
13
+ // boundary) lives type-checked in ReadModelEntryPoint_Ops.res /
14
+ // ProjectionEntryPoint_Ops.res / StreamRoutedEntryPoint_Ops.res.
15
+
16
+ import { patchSpecId } from "./HandlerFactoryHelpers.mjs";
14
17
  import { Make as readModelCallbackMake } from "@reventlessdev/reventless-core/src/components/ReadModel/ReadModel_Callback.res.mjs";
15
- // Typed cold-start core DynamoDB QueryDb ops, compiler-checked against the
16
- // framework signatures (see the module header and
17
- // docs/plans/minimize-lambda-entrypoint-mjs-shell.md).
18
- import { makeDynamoQueryDbOps } from "./QueryDbEntryPoint_Ops.res.mjs";
19
- import { opsFor as pgQdbOpsFor } from "@reventlessdev/reventless-aws/src/adapter/QueryDb/QueryDbStorage_Postgres_Runtime.res.mjs";
20
- import { withLiveUpdates } from "./StateTopicPublish.mjs";
21
- import { handleStreamEvent } from "@reventlessdev/reventless-aws/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream_Runtime.res.mjs";
18
+ import * as StreamOps from "./StreamRoutedEntryPoint_Ops.res.mjs";
19
+ import * as Ops from "./ReadModelEntryPoint_Ops.res.mjs";
22
20
 
23
21
  const dynamicImport = (specifier) => import('/var/task/node_modules/' + specifier);
24
22
 
25
- // Inject 'id' key into saved state objects
26
- function mkInjectIdSave(rawSave) {
27
- return (id, state, saveMode, ttl) => {
28
- const injected = (state && typeof state === "object" && !Array.isArray(state))
29
- ? { ...state, id }
30
- : state;
31
- return rawSave(id, injected, saveMode, ttl);
32
- };
33
- }
34
-
35
- function mkInjectIdSaveBatch(rawSaveBatch) {
36
- return (items) =>
37
- rawSaveBatch(items.map(([id, state, ttl]) => {
38
- const injected = (state && typeof state === "object" && !Array.isArray(state))
39
- ? { ...state, id }
40
- : state;
41
- return [id, injected, ttl];
42
- }));
43
- }
44
-
45
23
  // Fix mappings module: ensure it has a `mappings` array
46
24
  function fixMappingsModule(mod) {
47
25
  if (mod.mappings) return mod;
@@ -54,126 +32,29 @@ function fixMappingsModule(mod) {
54
32
  return mod;
55
33
  }
56
34
 
57
- function groupBySource(records) {
58
- const dict = {};
59
- records.forEach(record => {
60
- const arn = record.eventSourceARN;
61
- const existing = dict[arn] || [];
62
- dict[arn] = existing.concat([record]);
63
- });
64
- return dict;
65
- }
66
-
67
- // `pgConnection`, when present, is the resolved PgConnection.connectionConfig the
68
- // runtime builder injected for a Postgres-backed read model: bind the Postgres
69
- // operation set (queryDbTableName is then the read-model spec name, the shared
70
- // `qdb_<name>` discriminator). Absent → the DynamoDB path is byte-identical.
71
- function buildReadModelHandler(specModule, mappingsModule, queryDbTableName, pgConnection, stateTopicName) {
72
- const patchedSpec = patchSpecId(specModule);
73
- let operations;
74
- if (pgConnection) {
75
- const indexes = (patchedSpec.config && patchedSpec.config.indexes) || [];
76
- const subIdField = patchedSpec.subIdConfig ? patchedSpec.subIdConfig.subIdField : undefined;
77
- const pgOps = pgQdbOpsFor(pgConnection, queryDbTableName, indexes, subIdField);
78
- // B3.3: on a subscription-enabled (Stream) Postgres read model, publish a
79
- // live-update descriptor after each save/delete (no DynamoDB stream exists).
80
- // `stateTopicName` (present only for stream RMs) + APPSYNC_ENDPOINT gate it;
81
- // absent → withLiveUpdates returns pgOps unchanged. Wrap BEFORE mkInjectIdSave
82
- // so the publish sees the id-injected state (id, subId, updatedAt).
83
- const livePgOps = withLiveUpdates(pgOps, {
84
- endpoint: process.env.APPSYNC_ENDPOINT,
85
- region: process.env.AWS_REGION,
86
- topicName: stateTopicName,
87
- subIdField,
88
- });
89
- operations = {
90
- ...livePgOps,
91
- save: mkInjectIdSave(livePgOps.save),
92
- saveBatch: mkInjectIdSaveBatch(livePgOps.saveBatch),
93
- };
94
- } else {
95
- // Typed core builds the 7 DynamoDB QueryDb ops; the shell keeps the id-
96
- // injection wrap on save/saveBatch (business logic, not framework-call drift).
97
- const base = makeDynamoQueryDbOps(queryDbTableName);
98
- operations = {
99
- ...base,
100
- save: mkInjectIdSave(base.save),
101
- saveBatch: mkInjectIdSaveBatch(base.saveBatch),
102
- };
103
- }
104
-
105
- const effectiveMappings = fixMappingsModule(mappingsModule);
106
- const callback = readModelCallbackMake(patchedSpec)(effectiveMappings)({
107
- ReadModelSpec: patchedSpec,
108
- operations,
109
- });
110
-
111
- return (event, context) => handleStreamEvent(callback.handleJsonEvents, event, context);
112
- }
113
-
114
35
  async function buildAllHandlers() {
115
- const configStr = process.env["HANDLER_CONFIG"] || '{"handlers":[]}';
116
- const config = JSON.parse(configStr);
117
36
  const handlers = {};
118
-
119
- await Promise.all(config.handlers.map(async h => {
120
- const specModule = await dynamicImport(h.specModule);
121
- const mappingsModule = await dynamicImport(h.mappingsModule);
122
- const handler = buildReadModelHandler(specModule, mappingsModule, h.queryDbTableName, h.pgConnection, h.stateTopicName);
123
- // `comp` / `plugin` are resolved at deploy time and baked into
124
- // HANDLER_CONFIG (EventCollectorRuntime_Builder_Single) so the shell doesn't
125
- // re-derive component identity from module paths, and so the string matches
126
- // the ReScript dispatch boundary's `EventCollector(<Name>)` byte for byte.
127
- // Kept next to the handler: this Lambda hosts every read model, so the comp
128
- // is what separates their log lines.
129
- const registered = { handler, comp: h.comp, plugin: h.plugin };
130
- // Multiple read models can share one source stream — e.g. every admin read
131
- // model (Plugins, PluginHistory, PlatformEventGraph, UIFragmentRegistry)
132
- // projects the Plugin aggregate's EventLog stream. Accumulate ALL handlers
133
- // per source ARN; a plain `handlers[urn] = handler` collapses them to one
134
- // (whichever async builder wins the Promise.all race), silently dropping the
135
- // rest — which leaves their QueryDbs empty.
136
- (handlers[h.sourceUrn] ||= []).push(registered);
37
+ const entries = Ops.parseHandlerConfig(process.env["HANDLER_CONFIG"] || "");
38
+
39
+ await Promise.all(entries.map(async (entry) => {
40
+ const specModule = patchSpecId(await dynamicImport(entry.specModule));
41
+ const mappingsModule = fixMappingsModule(await dynamicImport(entry.mappingsModule));
42
+ const operations = Ops.buildOperations(entry, {
43
+ config: specModule.config,
44
+ subIdConfig: specModule.subIdConfig,
45
+ });
46
+ const callback = readModelCallbackMake(specModule)(mappingsModule)({
47
+ ReadModelSpec: specModule,
48
+ operations,
49
+ });
50
+ StreamOps.addToRegistry(
51
+ handlers,
52
+ entry.sourceUrn,
53
+ Ops.makeRegisteredHandler(entry, callback.handleJsonEvents)
54
+ );
137
55
  }));
138
56
 
139
57
  return handlers;
140
58
  }
141
59
 
142
- const initPromise = buildAllHandlers();
143
-
144
- export async function handler(event, context) {
145
- setRequestId(context?.awsRequestId);
146
- const handlers = await initPromise;
147
- const records = event.Records || [];
148
- const grouped = groupBySource(records);
149
-
150
- await Promise.all(Object.entries(grouped).map(async ([arn, subRecords]) => {
151
- const streamHandlers = handlers[arn];
152
- if (streamHandlers !== undefined && streamHandlers.length > 0) {
153
- log.debug("found " + streamHandlers.length + " handler(s) for " + arn, { comp: "ReadModelRuntime" });
154
- const correlationId = extractMetaField(subRecords, "correlationId");
155
- const causationId = extractMetaField(subRecords, "causationId");
156
- const timestamp = extractSentTimestamp(subRecords);
157
- const retryCount = extractRetryCount(subRecords);
158
- // Run every read model registered for this source stream (independent
159
- // QueryDbs, so concurrent is safe). Each carries its own comp so the
160
- // shared Lambda's log lines stay separable per read model.
161
- await Promise.all(
162
- streamHandlers.map(({ handler: streamHandler, comp, plugin }) =>
163
- runEffect(streamHandler({ Records: subRecords }, context), {
164
- correlationId,
165
- causationId,
166
- comp,
167
- plugin,
168
- timestamp,
169
- retryCount,
170
- })
171
- )
172
- );
173
- } else {
174
- log.warn("no handler found: " + arn, { comp: "ReadModelRuntime" });
175
- }
176
- }));
177
-
178
- return "";
179
- }
60
+ export const handler = StreamOps.makeRoutedHandler("ReadModelRuntime", buildAllHandlers());
@@ -0,0 +1,114 @@
1
+ // Typed cold-start core for the ReadModel Lambda entry point.
2
+ //
3
+ // The "typed core, thin shell" split (docs/plans/minimize-lambda-entrypoint-mjs-shell.md):
4
+ // ReadModelEntryPoint.mjs keeps only the untyped seams — the dynamic `import()`
5
+ // of spec/mappings modules named in HANDLER_CONFIG, the `patchSpecId` /
6
+ // `fixMappingsModule` shape fix-ups on those runtime-loaded modules, and the
7
+ // ReadModel_Callback.Make functor application consuming them. HANDLER_CONFIG
8
+ // parsing, the QueryDb operation assembly (including the id-injection wrap),
9
+ // and handler registration live here, fully type-checked; the QueryDb backend
10
+ // branch is shared with the StateViewSlice entry point in
11
+ // ProjectionEntryPoint_Ops and the routed dispatch boundary lives in
12
+ // StreamRoutedEntryPoint_Ops.
13
+
14
+ type handlerEntry = {
15
+ specModule: string,
16
+ mappingsModule: string,
17
+ queryDbTableName: string,
18
+ sourceUrn: string,
19
+ // Resolved at deploy time and baked into HANDLER_CONFIG
20
+ // (EventCollectorRuntime_Builder_Single) so the shell doesn't re-derive
21
+ // component identity from module paths, and so the string matches the
22
+ // ReScript dispatch boundary's `EventCollector(<Name>)` byte for byte. This
23
+ // Lambda hosts every read model, so the comp is what separates their log
24
+ // lines; the plugin beats the Lambda-name-derived fallback in runEffect.
25
+ comp: option<string>,
26
+ plugin: option<string>,
27
+ pgConnection: option<PgConnection.connectionConfig>,
28
+ stateTopicName: option<string>,
29
+ }
30
+
31
+ // The builder writes `pgConnection` via PgConnection.connectionConfigToJson, so
32
+ // the parsed object IS the record shape — cast at the documented JSON contract
33
+ // (the AggregateEntryPoint_Ops.asConnectionConfig pattern).
34
+ external asPgConnection: JSON.t => PgConnection.connectionConfig = "%identity"
35
+
36
+ let strOf = (obj: dict<JSON.t>, key: string): option<string> =>
37
+ obj->Dict.get(key)->Option.flatMap(JSON.Decode.string)
38
+
39
+ let decodeEntry = (json: JSON.t): option<handlerEntry> =>
40
+ json
41
+ ->JSON.Decode.object
42
+ ->Option.map(h => {
43
+ specModule: h->strOf("specModule")->Option.getOr(""),
44
+ mappingsModule: h->strOf("mappingsModule")->Option.getOr(""),
45
+ queryDbTableName: h->strOf("queryDbTableName")->Option.getOr(""),
46
+ sourceUrn: h->strOf("sourceUrn")->Option.getOr(""),
47
+ comp: h->strOf("comp"),
48
+ plugin: h->strOf("plugin"),
49
+ pgConnection: h
50
+ ->Dict.get("pgConnection")
51
+ ->Option.filter(j => j != JSON.Null)
52
+ ->Option.map(asPgConnection),
53
+ stateTopicName: h->strOf("stateTopicName"),
54
+ })
55
+
56
+ let parseHandlerConfig = (rawJson: string): array<handlerEntry> =>
57
+ rawJson == ""
58
+ ? []
59
+ : rawJson
60
+ ->JSON.parseOrThrow
61
+ ->JSON.Decode.object
62
+ ->Option.flatMap(obj => obj->Dict.get("handlers"))
63
+ ->Option.flatMap(JSON.Decode.array)
64
+ ->Option.getOr([])
65
+ ->Array.filterMap(decodeEntry)
66
+
67
+ // ── QueryDb operations with id injection ────────────────────────────────────
68
+ // Saved read-model states get an `id` key injected (query resolvers and the
69
+ // live-update publisher read it off the row). Applied OUTSIDE the live-update
70
+ // wrap so the publish sees the id-injected state (id, subId, updatedAt).
71
+
72
+ let injectId = (id: string, state: JSON.t): JSON.t =>
73
+ switch state {
74
+ | Object(fields) =>
75
+ let injected = fields->Dict.copy
76
+ injected->Dict.set("id", JSON.String(id))
77
+ JSON.Object(injected)
78
+ | other => other
79
+ }
80
+
81
+ let withInjectedId = (
82
+ base: ReventlessCore.QueryDb_Adapter.operations,
83
+ ): ReventlessCore.QueryDb_Adapter.operations => {
84
+ ...base,
85
+ save: (id, state, saveMode, ttl) => base.save(id, injectId(id, state), saveMode, ttl),
86
+ saveBatch: items =>
87
+ base.saveBatch(items->Array.map(((id, state, ttl)) => (id, injectId(id, state), ttl))),
88
+ }
89
+
90
+ type specInfo = {
91
+ config: option<ProjectionEntryPoint_Ops.specConfig>,
92
+ subIdConfig: option<Reventless.ReadModel.subIdConfig<JSON.t>>,
93
+ }
94
+
95
+ let buildOperations = (
96
+ entry: handlerEntry,
97
+ spec: specInfo,
98
+ ): ReventlessCore.QueryDb_Adapter.operations =>
99
+ ProjectionEntryPoint_Ops.makeQueryDbOps(
100
+ ~queryDbTableName=entry.queryDbTableName,
101
+ ~pgConnection=entry.pgConnection,
102
+ ~stateTopicName=entry.stateTopicName,
103
+ ~indexes=ProjectionEntryPoint_Ops.indexesOf(spec.config),
104
+ ~subIdField=ProjectionEntryPoint_Ops.subIdFieldOf(spec.subIdConfig),
105
+ )->withInjectedId
106
+
107
+ let makeRegisteredHandler = (
108
+ entry: handlerEntry,
109
+ handleJsonEvents: ReventlessCore.EventCollector.jsonEventsHandler,
110
+ ): StreamRoutedEntryPoint_Ops.registeredHandler => {
111
+ handler: StreamRoutedEntryPoint_Ops.toStreamHandler(handleJsonEvents),
112
+ comp: ?entry.comp,
113
+ plugin: ?entry.plugin,
114
+ }
@@ -0,0 +1,83 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
4
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as ProjectionEntryPoint_Ops$ReventlessAws from "./ProjectionEntryPoint_Ops.res.mjs";
7
+ import * as StreamRoutedEntryPoint_Ops$ReventlessAws from "./StreamRoutedEntryPoint_Ops.res.mjs";
8
+
9
+ function strOf(obj, key) {
10
+ return Stdlib_Option.flatMap(obj[key], Stdlib_JSON.Decode.string);
11
+ }
12
+
13
+ function decodeEntry(json) {
14
+ return Stdlib_Option.map(Stdlib_JSON.Decode.object(json), h => ({
15
+ specModule: Stdlib_Option.getOr(strOf(h, "specModule"), ""),
16
+ mappingsModule: Stdlib_Option.getOr(strOf(h, "mappingsModule"), ""),
17
+ queryDbTableName: Stdlib_Option.getOr(strOf(h, "queryDbTableName"), ""),
18
+ sourceUrn: Stdlib_Option.getOr(strOf(h, "sourceUrn"), ""),
19
+ comp: strOf(h, "comp"),
20
+ plugin: strOf(h, "plugin"),
21
+ pgConnection: Stdlib_Option.map(Stdlib_Option.filter(h["pgConnection"], j => j !== null), prim => prim),
22
+ stateTopicName: strOf(h, "stateTopicName")
23
+ }));
24
+ }
25
+
26
+ function parseHandlerConfig(rawJson) {
27
+ if (rawJson === "") {
28
+ return [];
29
+ } else {
30
+ return Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(JSON.parse(rawJson)), obj => obj["handlers"]), Stdlib_JSON.Decode.array), []), decodeEntry);
31
+ }
32
+ }
33
+
34
+ function injectId(id, state) {
35
+ if (typeof state !== "object" || state === null || Array.isArray(state)) {
36
+ return state;
37
+ }
38
+ let injected = Object.assign({}, state);
39
+ injected["id"] = id;
40
+ return injected;
41
+ }
42
+
43
+ function withInjectedId(base) {
44
+ return {
45
+ load: base.load,
46
+ loadStream: base.loadStream,
47
+ save: (id, state, saveMode, ttl) => base.save(id, injectId(id, state), saveMode, ttl),
48
+ saveBatch: items => base.saveBatch(items.map(param => {
49
+ let id = param[0];
50
+ return [
51
+ id,
52
+ injectId(id, param[1]),
53
+ param[2]
54
+ ];
55
+ })),
56
+ count: base.count,
57
+ delete: base.delete,
58
+ deleteBatch: base.deleteBatch
59
+ };
60
+ }
61
+
62
+ function buildOperations(entry, spec) {
63
+ return withInjectedId(ProjectionEntryPoint_Ops$ReventlessAws.makeQueryDbOps(entry.queryDbTableName, entry.pgConnection, entry.stateTopicName, ProjectionEntryPoint_Ops$ReventlessAws.indexesOf(spec.config), ProjectionEntryPoint_Ops$ReventlessAws.subIdFieldOf(spec.subIdConfig)));
64
+ }
65
+
66
+ function makeRegisteredHandler(entry, handleJsonEvents) {
67
+ return {
68
+ handler: StreamRoutedEntryPoint_Ops$ReventlessAws.toStreamHandler(handleJsonEvents),
69
+ comp: entry.comp,
70
+ plugin: entry.plugin
71
+ };
72
+ }
73
+
74
+ export {
75
+ strOf,
76
+ decodeEntry,
77
+ parseHandlerConfig,
78
+ injectId,
79
+ withInjectedId,
80
+ buildOperations,
81
+ makeRegisteredHandler,
82
+ }
83
+ /* ProjectionEntryPoint_Ops-ReventlessAws Not a pure module */