@reventlessdev/reventless-aws 3.0.0-alpha.231 → 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 +23 -0
  2. package/package.json +7 -7
  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 +16 -3
  32. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +11 -6
  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
@@ -16,42 +16,33 @@ open JestGlobals
16
16
 
17
17
  @val external processEnv: dict<string> = "process.env"
18
18
 
19
- // Import the .mjs entry point and call runMigration with the pool injected.
20
- // Returns "ok" on success. A minimal pgConnection satisfies the guard; the real
21
- // fields are ignored because `makePool` is injected.
22
- let runMigrationWithPool: (
23
- ReventlessPostgres.PgDriver.pool,
24
- ) => promise<string> = %raw(`
25
- async (pool) => {
26
- const { runMigration } = await import(
27
- "@reventlessdev/reventless-aws/src/adapter/Runtime/PgMigrationEntryPoint.mjs"
28
- );
29
- const config = {
19
+ // Drive the real ReScript `runMigration` with the pool injected — the guard
20
+ // rejects a HANDLER_CONFIG with no pgConnection; the real fields below are
21
+ // ignored because `~makePool` is injected. Returns "ok" on success.
22
+ let runMigrationWithPool = (pool: ReventlessPostgres.PgDriver.pool): promise<string> =>
23
+ PgMigrationEntryPoint.runMigration(
24
+ {
30
25
  pgConnection: {
31
- host: "ignored", port: 5432, database: "ignored",
32
- username: "ignored", secretArn: "ignored",
26
+ host: "ignored",
27
+ port: 5432,
28
+ database: "ignored",
29
+ username: "ignored",
30
+ secretArn: "ignored",
33
31
  },
34
- };
35
- return await runMigration(config, { makePool: () => pool });
36
- }
37
- `)
38
-
39
- // runMigration with an empty config — the guard must reject a HANDLER_CONFIG
40
- // that carries no pgConnection (a misconfigured deploy), rather than silently
41
- // building a bad pool.
42
- let runMigrationMissingConfigThrows: unit => promise<bool> = %raw(`
43
- async () => {
44
- const { runMigration } = await import(
45
- "@reventlessdev/reventless-aws/src/adapter/Runtime/PgMigrationEntryPoint.mjs"
46
- );
47
- try { await runMigration({}, {}); return false; }
48
- catch { return true; }
49
- }
50
- `)
32
+ },
33
+ ~makePool=_ => pool,
34
+ )
51
35
 
52
36
  describe("PgMigrationEntryPoint.runMigration", () => {
53
37
  testPromise("rejects a HANDLER_CONFIG with no pgConnection", async () => {
54
- let threw = await runMigrationMissingConfigThrows()
38
+ // An empty config carries no pgConnection (a misconfigured deploy); the
39
+ // guard must reject it rather than silently building a bad pool.
40
+ let threw = try {
41
+ let _ = await PgMigrationEntryPoint.runMigration({pgConnection: ?None})
42
+ false
43
+ } catch {
44
+ | _ => true
45
+ }
55
46
  expect(threw)->toBe(true)
56
47
  })
57
48
 
@@ -1,33 +1,31 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as PgDriver$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/PgDriver.res.mjs";
4
+ import * as PgMigrationEntryPoint$ReventlessAws from "../src/adapter/Runtime/PgMigrationEntryPoint.res.mjs";
4
5
  import * as EventLogStorage_Postgres$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/EventLogStorage_Postgres.res.mjs";
5
6
  import * as DcbEventLogStorage_Postgres$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/DcbEventLogStorage_Postgres.res.mjs";
6
7
 
7
- let runMigrationWithPool = (async (pool) => {
8
- const { runMigration } = await import(
9
- "@reventlessdev/reventless-aws/src/adapter/Runtime/PgMigrationEntryPoint.mjs"
10
- );
11
- const config = {
12
- pgConnection: {
13
- host: "ignored", port: 5432, database: "ignored",
14
- username: "ignored", secretArn: "ignored",
15
- },
16
- };
17
- return await runMigration(config, { makePool: () => pool });
18
- });
19
-
20
- let runMigrationMissingConfigThrows = (async () => {
21
- const { runMigration } = await import(
22
- "@reventlessdev/reventless-aws/src/adapter/Runtime/PgMigrationEntryPoint.mjs"
23
- );
24
- try { await runMigration({}, {}); return false; }
25
- catch { return true; }
26
- });
8
+ function runMigrationWithPool(pool) {
9
+ return PgMigrationEntryPoint$ReventlessAws.runMigration({
10
+ pgConnection: {
11
+ host: "ignored",
12
+ port: 5432,
13
+ database: "ignored",
14
+ username: "ignored",
15
+ secretArn: "ignored"
16
+ }
17
+ }, param => pool);
18
+ }
27
19
 
28
20
  globalThis.describe("PgMigrationEntryPoint.runMigration", () => {
29
21
  globalThis.test("rejects a HANDLER_CONFIG with no pgConnection", async () => {
30
- let threw = await runMigrationMissingConfigThrows();
22
+ let threw;
23
+ try {
24
+ await PgMigrationEntryPoint$ReventlessAws.runMigration({}, undefined);
25
+ threw = false;
26
+ } catch (exn) {
27
+ threw = true;
28
+ }
31
29
  globalThis.expect(threw).toBe(true);
32
30
  });
33
31
  let url = process.env["PG_URL"];
@@ -58,6 +56,5 @@ globalThis.describe("PgMigrationEntryPoint.runMigration", () => {
58
56
 
59
57
  export {
60
58
  runMigrationWithPool,
61
- runMigrationMissingConfigThrows,
62
59
  }
63
- /* runMigrationWithPool Not a pure module */
60
+ /* Not a pure module */
@@ -0,0 +1,38 @@
1
+ // Guards the typed cold-start core hoisted out of PgQueryResolverEntryPoint.mjs:
2
+ // - parseResolverConfig — the {pgConnection, handlers, nodeTypes} shape
3
+ // written by PgQueryResolver_Builder; a drift here silently drops a
4
+ // Postgres read model's GraphQL bindings (or its Relay node types) at
5
+ // cold start.
6
+
7
+ open JestGlobals
8
+
9
+ describe("PgQueryResolverEntryPoint_Ops.parseResolverConfig", () => {
10
+ testSync("empty raw config yields no connection and no handlers", () => {
11
+ let config = PgQueryResolverEntryPoint_Ops.parseResolverConfig("")
12
+ expect(config.pgConnection->Option.isNone)->toBe(true)
13
+ expect(config.handlers->Array.length)->toBe(0)
14
+ expect(config.nodeTypes->Dict.keysToArray->Array.length)->toBe(0)
15
+ })
16
+
17
+ testSync("null pgConnection decodes to None", () => {
18
+ let config = PgQueryResolverEntryPoint_Ops.parseResolverConfig(
19
+ `{"pgConnection":null,"handlers":[]}`,
20
+ )
21
+ expect(config.pgConnection->Option.isNone)->toBe(true)
22
+ })
23
+
24
+ testSync("decodes connection, handlers, and node types", () => {
25
+ let config = PgQueryResolverEntryPoint_Ops.parseResolverConfig(
26
+ `{"pgConnection":{"host":"db.local","port":5432,"database":"app","username":"master","secretArn":"arn:secret"},"handlers":[{"readModelName":"Products","specModule":"@x/p/src/ReadModel/Products.res.mjs","labelField":"name","includeIdParam":true},{"readModelName":"Orders","specModule":"@x/p/src/ReadModel/Orders.res.mjs","labelField":"id","includeIdParam":false}],"nodeTypes":{"Product":"Products"}}`,
27
+ )
28
+ expect(config.pgConnection->Option.map(cc => cc.host))->toEqual(Some("db.local"))
29
+ expect(config.handlers->Array.length)->toBe(2)
30
+ let first = config.handlers->Array.getUnsafe(0)
31
+ expect(first.readModelName)->toBe("Products")
32
+ expect(first.specModule)->toBe("@x/p/src/ReadModel/Products.res.mjs")
33
+ expect(first.labelField)->toBe("name")
34
+ expect(first.includeIdParam)->toBe(true)
35
+ expect((config.handlers->Array.getUnsafe(1)).includeIdParam)->toBe(false)
36
+ expect(config.nodeTypes->Dict.get("Product"))->toEqual(Some("Products"))
37
+ })
38
+ })
@@ -0,0 +1,31 @@
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 PgQueryResolverEntryPoint_Ops$ReventlessAws from "../src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res.mjs";
5
+
6
+ globalThis.describe("PgQueryResolverEntryPoint_Ops.parseResolverConfig", () => {
7
+ globalThis.test("empty raw config yields no connection and no handlers", () => {
8
+ let config = PgQueryResolverEntryPoint_Ops$ReventlessAws.parseResolverConfig("");
9
+ globalThis.expect(Stdlib_Option.isNone(config.pgConnection)).toBe(true);
10
+ globalThis.expect(config.handlers.length).toBe(0);
11
+ globalThis.expect(Object.keys(config.nodeTypes).length).toBe(0);
12
+ });
13
+ globalThis.test("null pgConnection decodes to None", () => {
14
+ let config = PgQueryResolverEntryPoint_Ops$ReventlessAws.parseResolverConfig(`{"pgConnection":null,"handlers":[]}`);
15
+ globalThis.expect(Stdlib_Option.isNone(config.pgConnection)).toBe(true);
16
+ });
17
+ globalThis.test("decodes connection, handlers, and node types", () => {
18
+ let config = PgQueryResolverEntryPoint_Ops$ReventlessAws.parseResolverConfig(`{"pgConnection":{"host":"db.local","port":5432,"database":"app","username":"master","secretArn":"arn:secret"},"handlers":[{"readModelName":"Products","specModule":"@x/p/src/ReadModel/Products.res.mjs","labelField":"name","includeIdParam":true},{"readModelName":"Orders","specModule":"@x/p/src/ReadModel/Orders.res.mjs","labelField":"id","includeIdParam":false}],"nodeTypes":{"Product":"Products"}}`);
19
+ globalThis.expect(Stdlib_Option.map(config.pgConnection, cc => cc.host)).toEqual("db.local");
20
+ globalThis.expect(config.handlers.length).toBe(2);
21
+ let first = config.handlers[0];
22
+ globalThis.expect(first.readModelName).toBe("Products");
23
+ globalThis.expect(first.specModule).toBe("@x/p/src/ReadModel/Products.res.mjs");
24
+ globalThis.expect(first.labelField).toBe("name");
25
+ globalThis.expect(first.includeIdParam).toBe(true);
26
+ globalThis.expect(config.handlers[1].includeIdParam).toBe(false);
27
+ globalThis.expect(config.nodeTypes["Product"]).toEqual("Products");
28
+ });
29
+ });
30
+
31
+ /* Not a pure module */
@@ -0,0 +1,80 @@
1
+ // Guards the pure logic hoisted out of the two admin-query resolver Lambdas'
2
+ // former inline JS strings (Platform_UIFragments, Platform_ComponentDefinitions)
3
+ // into the shared, type-checked Platform_AdminScan_Ops:
4
+ // - `compareVersions` — a byte-for-byte port of the JS `cmpVer` comparator.
5
+ // - `latestByName` — the highest-version-per-plugin-name collapse.
6
+ // The DynamoDB scan itself is an SDK call, not unit-tested here.
7
+
8
+ open JestGlobals
9
+
10
+ let cmp = Platform_AdminScan_Ops.compareVersions
11
+
12
+ describe("Platform_AdminScan_Ops.compareVersions", () => {
13
+ testSync("orders numeric segments as numbers, not lexically", () => {
14
+ // Lexical order would put "10" < "9"; numeric order is the opposite.
15
+ expect(cmp("1.10.0", "1.9.0"))->toBe(1)
16
+ expect(cmp("1.9.0", "1.10.0"))->toBe(-1)
17
+ })
18
+
19
+ testSync("equal versions compare to 0", () => {
20
+ expect(cmp("2.3.4", "2.3.4"))->toBe(0)
21
+ })
22
+
23
+ testSync("a longer version outranks its prefix", () => {
24
+ // "1.2" normalises to ["1","2"]; "1.2.1" adds a segment (1 > missing "").
25
+ expect(cmp("1.2.1", "1.2"))->toBe(1)
26
+ expect(cmp("1.2", "1.2.1"))->toBe(-1)
27
+ })
28
+
29
+ testSync("normalises pre-release `-`/`+` separators to `.`", () => {
30
+ // "1.2.0-1" -> ["1","2","0","1"] outranks "1.2.0" -> ["1","2","0"].
31
+ expect(cmp("1.2.0-1", "1.2.0"))->toBe(1)
32
+ })
33
+
34
+ testSync("falls back to lexical order for non-numeric segments", () => {
35
+ expect(cmp("1.2.beta", "1.2.alpha"))->toBe(1)
36
+ expect(cmp("1.2.alpha", "1.2.beta"))->toBe(-1)
37
+ })
38
+ })
39
+
40
+ // A minimal DDB-row (attribute -> JSON value) for the collapse tests.
41
+ let row = pairs => Dict.fromArray(pairs)
42
+ let s = JSON.Encode.string
43
+
44
+ describe("Platform_AdminScan_Ops.latestByName", () => {
45
+ // A trivial entry builder that echoes the resolved bare name; drops rows with
46
+ // no `keep` attribute (exercises the toEntry=None skip path).
47
+ let nameVersionOf = item => item->Dict.get("name")->Option.flatMap(JSON.Decode.string)
48
+ let toEntry = (item, ~name) =>
49
+ switch item->Dict.get("keep") {
50
+ | Some(_) => Some(JSON.Encode.object(Dict.fromArray([("pluginId", s(name))])))
51
+ | None => None
52
+ }
53
+
54
+ let pluginIdOf = json =>
55
+ json->JSON.Decode.object->Option.flatMap(o => o->Dict.get("pluginId"))->Option.flatMap(JSON.Decode.string)
56
+
57
+ testSync("keeps only the highest version per bare plugin name", () => {
58
+ let items = [
59
+ row([("name", s("Catalog@1.9.0")), ("keep", s("y"))]),
60
+ row([("name", s("Catalog@1.10.0")), ("keep", s("y"))]),
61
+ row([("name", s("Catalog@1.2.0")), ("keep", s("y"))]),
62
+ row([("name", s("Ordering@2.0.0")), ("keep", s("y"))]),
63
+ ]
64
+ let out = Platform_AdminScan_Ops.latestByName(items, ~nameVersionOf, ~toEntry)
65
+ let names = out->Array.filterMap(pluginIdOf)
66
+ // One entry per bare name.
67
+ expect(out->Array.length)->toBe(2)
68
+ expect(names->Array.includes("Catalog"))->toBe(true)
69
+ expect(names->Array.includes("Ordering"))->toBe(true)
70
+ })
71
+
72
+ testSync("drops rows the entry builder rejects", () => {
73
+ let items = [
74
+ row([("name", s("Catalog@1.0.0"))]), // no `keep` -> dropped
75
+ row([("name", s("Ordering@1.0.0")), ("keep", s("y"))]),
76
+ ]
77
+ let out = Platform_AdminScan_Ops.latestByName(items, ~nameVersionOf, ~toEntry)
78
+ expect(out->Array.filterMap(pluginIdOf))->toEqual(["Ordering"])
79
+ })
80
+ })
@@ -0,0 +1,127 @@
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 Platform_AdminScan_Ops$ReventlessAws from "../src/adapter/Api/Platform_AdminScan_Ops.res.mjs";
7
+
8
+ globalThis.describe("Platform_AdminScan_Ops.compareVersions", () => {
9
+ globalThis.test("orders numeric segments as numbers, not lexically", () => {
10
+ globalThis.expect(Platform_AdminScan_Ops$ReventlessAws.compareVersions("1.10.0", "1.9.0")).toBe(1);
11
+ globalThis.expect(Platform_AdminScan_Ops$ReventlessAws.compareVersions("1.9.0", "1.10.0")).toBe(-1);
12
+ });
13
+ globalThis.test("equal versions compare to 0", () => {
14
+ globalThis.expect(Platform_AdminScan_Ops$ReventlessAws.compareVersions("2.3.4", "2.3.4")).toBe(0);
15
+ });
16
+ globalThis.test("a longer version outranks its prefix", () => {
17
+ globalThis.expect(Platform_AdminScan_Ops$ReventlessAws.compareVersions("1.2.1", "1.2")).toBe(1);
18
+ globalThis.expect(Platform_AdminScan_Ops$ReventlessAws.compareVersions("1.2", "1.2.1")).toBe(-1);
19
+ });
20
+ globalThis.test("normalises pre-release `-`/`+` separators to `.`", () => {
21
+ globalThis.expect(Platform_AdminScan_Ops$ReventlessAws.compareVersions("1.2.0-1", "1.2.0")).toBe(1);
22
+ });
23
+ globalThis.test("falls back to lexical order for non-numeric segments", () => {
24
+ globalThis.expect(Platform_AdminScan_Ops$ReventlessAws.compareVersions("1.2.beta", "1.2.alpha")).toBe(1);
25
+ globalThis.expect(Platform_AdminScan_Ops$ReventlessAws.compareVersions("1.2.alpha", "1.2.beta")).toBe(-1);
26
+ });
27
+ });
28
+
29
+ function row(pairs) {
30
+ return Object.fromEntries(pairs);
31
+ }
32
+
33
+ function s(prim) {
34
+ return prim;
35
+ }
36
+
37
+ globalThis.describe("Platform_AdminScan_Ops.latestByName", () => {
38
+ let nameVersionOf = item => Stdlib_Option.flatMap(item["name"], Stdlib_JSON.Decode.string);
39
+ let toEntry = (item, name) => {
40
+ let match = item["keep"];
41
+ if (match !== undefined) {
42
+ return Object.fromEntries([[
43
+ "pluginId",
44
+ name
45
+ ]]);
46
+ }
47
+ };
48
+ let pluginIdOf = json => Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), o => o["pluginId"]), Stdlib_JSON.Decode.string);
49
+ globalThis.test("keeps only the highest version per bare plugin name", () => {
50
+ let items = [
51
+ Object.fromEntries([
52
+ [
53
+ "name",
54
+ "Catalog@1.9.0"
55
+ ],
56
+ [
57
+ "keep",
58
+ "y"
59
+ ]
60
+ ]),
61
+ Object.fromEntries([
62
+ [
63
+ "name",
64
+ "Catalog@1.10.0"
65
+ ],
66
+ [
67
+ "keep",
68
+ "y"
69
+ ]
70
+ ]),
71
+ Object.fromEntries([
72
+ [
73
+ "name",
74
+ "Catalog@1.2.0"
75
+ ],
76
+ [
77
+ "keep",
78
+ "y"
79
+ ]
80
+ ]),
81
+ Object.fromEntries([
82
+ [
83
+ "name",
84
+ "Ordering@2.0.0"
85
+ ],
86
+ [
87
+ "keep",
88
+ "y"
89
+ ]
90
+ ])
91
+ ];
92
+ let out = Platform_AdminScan_Ops$ReventlessAws.latestByName(items, nameVersionOf, toEntry);
93
+ let names = Stdlib_Array.filterMap(out, pluginIdOf);
94
+ globalThis.expect(out.length).toBe(2);
95
+ globalThis.expect(names.includes("Catalog")).toBe(true);
96
+ globalThis.expect(names.includes("Ordering")).toBe(true);
97
+ });
98
+ globalThis.test("drops rows the entry builder rejects", () => {
99
+ let items = [
100
+ Object.fromEntries([[
101
+ "name",
102
+ "Catalog@1.0.0"
103
+ ]]),
104
+ Object.fromEntries([
105
+ [
106
+ "name",
107
+ "Ordering@1.0.0"
108
+ ],
109
+ [
110
+ "keep",
111
+ "y"
112
+ ]
113
+ ])
114
+ ];
115
+ let out = Platform_AdminScan_Ops$ReventlessAws.latestByName(items, nameVersionOf, toEntry);
116
+ globalThis.expect(Stdlib_Array.filterMap(out, pluginIdOf)).toEqual(["Ordering"]);
117
+ });
118
+ });
119
+
120
+ let cmp = Platform_AdminScan_Ops$ReventlessAws.compareVersions;
121
+
122
+ export {
123
+ cmp,
124
+ row,
125
+ s,
126
+ }
127
+ /* Not a pure module */
@@ -0,0 +1,141 @@
1
+ // Guards the typed cold-start core hoisted out of ReadModelEntryPoint.mjs:
2
+ // - parseHandlerConfig — the HANDLER_CONFIG contract every read-model Lambda
3
+ // cold-starts on, incl. the optional attribution (comp/plugin) and backend
4
+ // (pgConnection/stateTopicName) fields.
5
+ // - injectId / withInjectedId — the id-injection wrap on save/saveBatch;
6
+ // query resolvers and the live-update publisher read `id` off the row, so
7
+ // silently dropping the injection would strand every list query.
8
+
9
+ open JestGlobals
10
+
11
+ let str = JSON.Encode.string
12
+ let obj = pairs => JSON.Encode.object(Dict.fromArray(pairs))
13
+
14
+ describe("ReadModelEntryPoint_Ops.parseHandlerConfig", () => {
15
+ testSync("empty raw config yields no handlers", () => {
16
+ expect(ReadModelEntryPoint_Ops.parseHandlerConfig("")->Array.length)->toBe(0)
17
+ expect(ReadModelEntryPoint_Ops.parseHandlerConfig(`{"handlers":[]}`)->Array.length)->toBe(0)
18
+ })
19
+
20
+ testSync("decodes a full entry incl. attribution and Postgres fields", () => {
21
+ let config = obj([
22
+ (
23
+ "handlers",
24
+ JSON.Encode.array([
25
+ obj([
26
+ ("specModule", str("@x/spec/src/ReadModel/Products.res.mjs")),
27
+ ("mappingsModule", str("@x/spec/src/ReadModel/Products_Projections.res.mjs")),
28
+ ("queryDbTableName", str("Products-abc")),
29
+ ("sourceUrn", str("arn:aws:sqs:eu-west-1:1:q")),
30
+ ("comp", str("EventCollector(ProductsEC)")),
31
+ ("plugin", str("Catalog")),
32
+ ("stateTopicName", str("catalogProducts")),
33
+ (
34
+ "pgConnection",
35
+ obj([
36
+ ("host", str("db.local")),
37
+ ("port", JSON.Encode.int(5432)),
38
+ ("database", str("app")),
39
+ ("username", str("master")),
40
+ ("secretArn", str("arn:secret")),
41
+ ]),
42
+ ),
43
+ ]),
44
+ ]),
45
+ ),
46
+ ])->JSON.stringify
47
+ let entries = ReadModelEntryPoint_Ops.parseHandlerConfig(config)
48
+ expect(entries->Array.length)->toBe(1)
49
+ let e = entries->Array.getUnsafe(0)
50
+ expect(e.specModule)->toBe("@x/spec/src/ReadModel/Products.res.mjs")
51
+ expect(e.mappingsModule)->toBe("@x/spec/src/ReadModel/Products_Projections.res.mjs")
52
+ expect(e.queryDbTableName)->toBe("Products-abc")
53
+ expect(e.sourceUrn)->toBe("arn:aws:sqs:eu-west-1:1:q")
54
+ expect(e.comp)->toEqual(Some("EventCollector(ProductsEC)"))
55
+ expect(e.plugin)->toEqual(Some("Catalog"))
56
+ expect(e.stateTopicName)->toEqual(Some("catalogProducts"))
57
+ switch e.pgConnection {
58
+ | Some(cc) => {
59
+ expect(cc.host)->toBe("db.local")
60
+ expect(cc.port)->toBe(5432)
61
+ expect(cc.database)->toBe("app")
62
+ }
63
+ | None => JsError.throwWithMessage("expected Some(pgConnection)")
64
+ }
65
+ })
66
+
67
+ testSync("optional fields default to None; null pgConnection maps to None", () => {
68
+ let config = obj([
69
+ (
70
+ "handlers",
71
+ JSON.Encode.array([
72
+ obj([
73
+ ("specModule", str("a.res.mjs")),
74
+ ("mappingsModule", str("b.res.mjs")),
75
+ ("queryDbTableName", str("t")),
76
+ ("sourceUrn", str("u")),
77
+ ("pgConnection", JSON.Null),
78
+ ]),
79
+ ]),
80
+ ),
81
+ ])->JSON.stringify
82
+ let e = ReadModelEntryPoint_Ops.parseHandlerConfig(config)->Array.getUnsafe(0)
83
+ expect(e.comp->Option.isNone)->toBe(true)
84
+ expect(e.plugin->Option.isNone)->toBe(true)
85
+ expect(e.stateTopicName->Option.isNone)->toBe(true)
86
+ expect(e.pgConnection->Option.isNone)->toBe(true)
87
+ })
88
+ })
89
+
90
+ describe("ReadModelEntryPoint_Ops.injectId", () => {
91
+ testSync("injects (and overwrites) id on object states", () => {
92
+ expect(ReadModelEntryPoint_Ops.injectId("p-1", obj([("name", str("Widget"))])))->toEqual(
93
+ obj([("name", str("Widget")), ("id", str("p-1"))]),
94
+ )
95
+ expect(ReadModelEntryPoint_Ops.injectId("p-1", obj([("id", str("stale"))])))->toEqual(
96
+ obj([("id", str("p-1"))]),
97
+ )
98
+ })
99
+
100
+ testSync("passes non-object states through unchanged", () => {
101
+ expect(ReadModelEntryPoint_Ops.injectId("p-1", str("scalar")))->toEqual(str("scalar"))
102
+ expect(
103
+ ReadModelEntryPoint_Ops.injectId("p-1", JSON.Encode.array([str("a")])),
104
+ )->toEqual(JSON.Encode.array([str("a")]))
105
+ expect(ReadModelEntryPoint_Ops.injectId("p-1", JSON.Null))->toEqual(JSON.Null)
106
+ })
107
+ })
108
+
109
+ describe("ReadModelEntryPoint_Ops.withInjectedId", () => {
110
+ // Capturing stub operation set — only save/saveBatch matter for the wrap.
111
+ let makeStub = (saved: array<(string, JSON.t)>): ReventlessCore.QueryDb_Adapter.operations => {
112
+ load: async _ => Ok([]),
113
+ loadStream: _ => Stream.fromIterable([]),
114
+ save: async (id, state, _mode, _ttl) => {
115
+ saved->Array.push((id, state))
116
+ Ok()
117
+ },
118
+ saveBatch: async items => {
119
+ items->Array.forEach(((id, state, _ttl)) => saved->Array.push((id, state)))
120
+ Ok()
121
+ },
122
+ count: async (_, _, _) => Ok(0),
123
+ delete: async (_, _) => Ok(),
124
+ deleteBatch: async _ => Ok(),
125
+ }
126
+
127
+ test("save and saveBatch write id-injected states", async () => {
128
+ let saved = []
129
+ let ops = ReadModelEntryPoint_Ops.withInjectedId(makeStub(saved))
130
+ let _ = await ops.save("p-1", obj([("name", str("A"))]), ReventlessCore.QueryDb.Any, None)
131
+ let _ = await ops.saveBatch([
132
+ ("p-2", obj([("name", str("B"))]), None),
133
+ ("p-3", str("scalar"), None),
134
+ ])
135
+ expect(saved)->toEqual([
136
+ ("p-1", obj([("name", str("A")), ("id", str("p-1"))])),
137
+ ("p-2", obj([("name", str("B")), ("id", str("p-2"))])),
138
+ ("p-3", str("scalar")),
139
+ ])
140
+ })
141
+ })