@reventlessdev/reventless-aws 3.0.0-alpha.176 → 3.0.0-alpha.178
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 +23 -0
- package/docker-compose.postgres.yml +21 -0
- package/package.json +7 -6
- package/scripts/run-pg-integration-tests.sh +58 -0
- package/src/Platform.res +49 -0
- package/src/Platform.res.mjs +48 -2
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +7 -1
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +10 -1
- package/src/adapter/Api/Platform_UIFragments_Lambda.res +7 -1
- package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +10 -1
- package/src/adapter/Counter/CounterHandler_DynamoDbStream.res +6 -21
- package/src/adapter/Counter/CounterHandler_DynamoDbStream.res.mjs +2 -17
- package/src/adapter/DcbEventLog/DcbBackend.res +4 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres.res +2 -2
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres.res.mjs +7 -1
- package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +1 -11
- package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +2 -24
- package/src/adapter/Postgres/PgConnection.res +70 -10
- package/src/adapter/Postgres/PgConnection.res.mjs +48 -3
- package/src/adapter/Postgres/PgMigration_Builder.res +127 -0
- package/src/adapter/Postgres/PgMigration_Builder.res.mjs +51 -0
- package/src/adapter/QueryDb/PgQueryResolver_Builder.res +279 -0
- package/src/adapter/QueryDb/PgQueryResolver_Builder.res.mjs +170 -0
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res +202 -11
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res.mjs +150 -9
- package/src/adapter/QueryDb/QueryDbBackend.res +8 -0
- package/src/adapter/QueryDb/QueryDbBackend.res.mjs +4 -1
- package/src/adapter/QueryDb/QueryDbResolvers.res +6 -5
- package/src/adapter/QueryDb/QueryDbResolvers.res.mjs +6 -3
- package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res +288 -0
- package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res.mjs +170 -0
- package/src/adapter/QueryDb/QueryDbStorage.res +6 -1
- package/src/adapter/QueryDb/QueryDbStorage.res.mjs +2 -1
- package/src/adapter/QueryDb/QueryDbStorage_Postgres.res +5 -1
- package/src/adapter/QueryDb/QueryDbStorage_Postgres.res.mjs +3 -3
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res +3 -14
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res.mjs +2 -25
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res +3 -14
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res.mjs +2 -25
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +7 -1
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +78 -15
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res.mjs +43 -26
- package/src/adapter/Runtime/PgMigrationEntryPoint.mjs +40 -0
- package/src/adapter/Runtime/PgQueryResolverEntryPoint.mjs +88 -0
- package/src/adapter/Runtime/ReadModelEntryPoint.mjs +17 -5
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +10 -0
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +3 -0
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +9 -14
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +2 -25
- package/src/adapter/Runtime/StateTopicPublish.mjs +204 -0
- package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +17 -3
- package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res +86 -17
- package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res.mjs +50 -33
- package/src/adapter/StateTopic/StateTopic_AppSync.res +7 -1
- package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +10 -1
- package/src/plugin/cloner/ClonerRunner_Fargate.res +7 -1
- package/src/plugin/cloner/ClonerRunner_Fargate.res.mjs +10 -1
- package/src/util/Util_Bundle.res +71 -1
- package/src/util/Util_Bundle.res.mjs +49 -1
- package/tests/PgChangeFeedRelay_IntegrationTest.res +3 -3
- package/tests/PgConnectionJsonTest.res +84 -0
- package/tests/PgConnectionJsonTest.res.mjs +68 -0
- package/tests/PgMigrationEntryPoint_IntegrationTest.res +82 -0
- package/tests/PgMigrationEntryPoint_IntegrationTest.res.mjs +63 -0
- package/tests/PgPipeline_IntegrationTest.res +3 -1
- package/tests/PgQueryResolver_LambdaTest.res +163 -2
- package/tests/PgQueryResolver_LambdaTest.res.mjs +226 -14
- package/tests/StateTopicPublishTest.res +162 -0
- package/tests/StateTopicPublishTest.res.mjs +158 -0
- package/tests/Util_BundleEsmLoaderTest.res +57 -0
- package/tests/Util_BundleEsmLoaderTest.res.mjs +44 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// Unit tests for the Postgres live-update publisher (B3.3a).
|
|
2
|
+
//
|
|
3
|
+
// `withLiveUpdates` wraps a Postgres QueryDb ops set so save/delete also publish
|
|
4
|
+
// an AppSync-Events change descriptor. The publish itself (SigV4 POST) is
|
|
5
|
+
// injected here as a capturing stub, so this exercises the WRAPPING logic
|
|
6
|
+
// headlessly — entity-key composition, changeKind, sortKeyValue extraction, the
|
|
7
|
+
// `option<(field,subValue)>` delete signature, and composite partition-wide
|
|
8
|
+
// delete enumeration via ops.load. The real SigV4/channel wire format matches
|
|
9
|
+
// StateTopic_AppSync (the DynamoDB path) by construction and is not re-asserted
|
|
10
|
+
// here.
|
|
11
|
+
|
|
12
|
+
open JestGlobals
|
|
13
|
+
|
|
14
|
+
// Drive a scenario against withLiveUpdates with a mock ops + capturing publish,
|
|
15
|
+
// entirely in JS (the mock ops are plain JS objects). Returns the captured
|
|
16
|
+
// publish-call array as JSON for assertions.
|
|
17
|
+
let run: string => promise<JSON.t> = %raw(`
|
|
18
|
+
async function(scenario) {
|
|
19
|
+
const mod = await import("../src/adapter/Runtime/StateTopicPublish.mjs");
|
|
20
|
+
const { withLiveUpdates } = mod;
|
|
21
|
+
const captured = [];
|
|
22
|
+
const publish = async (call) => { captured.push(call); };
|
|
23
|
+
const ok = async () => ({ TAG: "Ok", _0: undefined });
|
|
24
|
+
|
|
25
|
+
// Mock ops. load returns the composite partition's rows for the enumerate case.
|
|
26
|
+
const partitionRows = { TAG: "Ok", _0: [
|
|
27
|
+
{ id: "o1", lineId: "L1" },
|
|
28
|
+
{ id: "o1", lineId: "L2" },
|
|
29
|
+
]};
|
|
30
|
+
const ops = {
|
|
31
|
+
load: async (id) => partitionRows,
|
|
32
|
+
loadStream: () => {},
|
|
33
|
+
save: ok, saveBatch: ok, count: ok, delete: ok, deleteBatch: ok,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const cfg = (subIdField) => ({
|
|
37
|
+
endpoint: "https://x.appsync-api.eu-west-1.amazonaws.com",
|
|
38
|
+
region: "eu-west-1",
|
|
39
|
+
topicName: "Shop_Orders",
|
|
40
|
+
subIdField,
|
|
41
|
+
publish,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const w = (subIdField) => withLiveUpdates(ops, cfg(subIdField));
|
|
45
|
+
|
|
46
|
+
switch (scenario) {
|
|
47
|
+
case "save-single":
|
|
48
|
+
await w(undefined).save("p1", { id: "p1", updatedAt: "2024-01-02" }, "Insert", undefined);
|
|
49
|
+
break;
|
|
50
|
+
case "save-composite":
|
|
51
|
+
await w("lineId").save("o1", { id: "o1", lineId: "L2", createdAt: "2024-03-04" }, "Insert", undefined);
|
|
52
|
+
break;
|
|
53
|
+
case "save-no-sortkey":
|
|
54
|
+
await w(undefined).save("p1", { id: "p1" }, "Insert", undefined);
|
|
55
|
+
break;
|
|
56
|
+
case "saveBatch":
|
|
57
|
+
await w(undefined).saveBatch([
|
|
58
|
+
["a", { id: "a", updatedAt: "t1" }, undefined],
|
|
59
|
+
["b", { id: "b", updatedAt: "t2" }, undefined],
|
|
60
|
+
]);
|
|
61
|
+
break;
|
|
62
|
+
case "delete-single":
|
|
63
|
+
await w(undefined).delete("p1", undefined);
|
|
64
|
+
break;
|
|
65
|
+
case "delete-composite-row":
|
|
66
|
+
await w("lineId").delete("o1", ["lineId", "L2"]);
|
|
67
|
+
break;
|
|
68
|
+
case "delete-composite-partition":
|
|
69
|
+
await w("lineId").delete("o1", undefined);
|
|
70
|
+
break;
|
|
71
|
+
case "deleteBatch":
|
|
72
|
+
await w(undefined).deleteBatch([["a", undefined], ["b", undefined]]);
|
|
73
|
+
break;
|
|
74
|
+
case "gated-no-topic": {
|
|
75
|
+
const gatedOps = withLiveUpdates(ops, { endpoint: "https://x", region: "r", topicName: undefined, publish });
|
|
76
|
+
await gatedOps.save("p1", { id: "p1" }, "Insert", undefined);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return captured;
|
|
81
|
+
}
|
|
82
|
+
`)
|
|
83
|
+
|
|
84
|
+
let getField = (call: JSON.t, key: string): option<JSON.t> =>
|
|
85
|
+
call->JSON.Decode.object->Option.flatMap(d => d->Dict.get(key))
|
|
86
|
+
|
|
87
|
+
let str = (call, key) => getField(call, key)->Option.flatMap(JSON.Decode.string)
|
|
88
|
+
|
|
89
|
+
let calls = (j: JSON.t): array<JSON.t> => j->JSON.Decode.array->Option.getOr([])
|
|
90
|
+
|
|
91
|
+
describe("withLiveUpdates (B3.3a Postgres live updates)", () => {
|
|
92
|
+
testAsync("save on a single-key table publishes Updated with sortKeyValue", async () => {
|
|
93
|
+
let c = await run("save-single")
|
|
94
|
+
let arr = calls(c)
|
|
95
|
+
expect(arr->Array.length)->toBe(1)
|
|
96
|
+
let call = arr->Array.getUnsafe(0)
|
|
97
|
+
expect(str(call, "entityKey"))->toEqual(Some("p1"))
|
|
98
|
+
expect(str(call, "changeKind"))->toEqual(Some("Updated"))
|
|
99
|
+
expect(str(call, "sortKeyValue"))->toEqual(Some("2024-01-02"))
|
|
100
|
+
expect(str(call, "topicName"))->toEqual(Some("Shop_Orders"))
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
testAsync("save on a composite table uses id-subKey and createdAt fallback", async () => {
|
|
104
|
+
let c = await run("save-composite")
|
|
105
|
+
let call = calls(c)->Array.getUnsafe(0)
|
|
106
|
+
expect(str(call, "entityKey"))->toEqual(Some("o1-L2"))
|
|
107
|
+
expect(str(call, "changeKind"))->toEqual(Some("Updated"))
|
|
108
|
+
expect(str(call, "sortKeyValue"))->toEqual(Some("2024-03-04"))
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
testAsync("save without updatedAt/createdAt omits sortKeyValue", async () => {
|
|
112
|
+
let c = await run("save-no-sortkey")
|
|
113
|
+
let call = calls(c)->Array.getUnsafe(0)
|
|
114
|
+
expect(getField(call, "sortKeyValue"))->toEqual(None)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
testAsync("saveBatch publishes one Updated per item", async () => {
|
|
118
|
+
let c = await run("saveBatch")
|
|
119
|
+
let arr = calls(c)
|
|
120
|
+
expect(arr->Array.length)->toBe(2)
|
|
121
|
+
expect(str(arr->Array.getUnsafe(0), "entityKey"))->toEqual(Some("a"))
|
|
122
|
+
expect(str(arr->Array.getUnsafe(1), "entityKey"))->toEqual(Some("b"))
|
|
123
|
+
arr->Array.forEach(call => expect(str(call, "changeKind"))->toEqual(Some("Updated")))
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
testAsync("delete on a single-key table publishes Removed for the id", async () => {
|
|
127
|
+
let c = await run("delete-single")
|
|
128
|
+
let call = calls(c)->Array.getUnsafe(0)
|
|
129
|
+
expect(str(call, "entityKey"))->toEqual(Some("p1"))
|
|
130
|
+
expect(str(call, "changeKind"))->toEqual(Some("Removed"))
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
testAsync("delete of a specific composite row uses id-subValue", async () => {
|
|
134
|
+
let c = await run("delete-composite-row")
|
|
135
|
+
let arr = calls(c)
|
|
136
|
+
expect(arr->Array.length)->toBe(1)
|
|
137
|
+
expect(str(arr->Array.getUnsafe(0), "entityKey"))->toEqual(Some("o1-L2"))
|
|
138
|
+
expect(str(arr->Array.getUnsafe(0), "changeKind"))->toEqual(Some("Removed"))
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
testAsync("partition-wide composite delete enumerates rows via load", async () => {
|
|
142
|
+
let c = await run("delete-composite-partition")
|
|
143
|
+
let arr = calls(c)
|
|
144
|
+
expect(arr->Array.length)->toBe(2)
|
|
145
|
+
expect(str(arr->Array.getUnsafe(0), "entityKey"))->toEqual(Some("o1-L1"))
|
|
146
|
+
expect(str(arr->Array.getUnsafe(1), "entityKey"))->toEqual(Some("o1-L2"))
|
|
147
|
+
arr->Array.forEach(call => expect(str(call, "changeKind"))->toEqual(Some("Removed")))
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
testAsync("deleteBatch publishes one Removed per entry", async () => {
|
|
151
|
+
let c = await run("deleteBatch")
|
|
152
|
+
let arr = calls(c)
|
|
153
|
+
expect(arr->Array.length)->toBe(2)
|
|
154
|
+
expect(str(arr->Array.getUnsafe(0), "entityKey"))->toEqual(Some("a"))
|
|
155
|
+
expect(str(arr->Array.getUnsafe(1), "entityKey"))->toEqual(Some("b"))
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
testAsync("no topicName → ops pass through, nothing published", async () => {
|
|
159
|
+
let c = await run("gated-no-topic")
|
|
160
|
+
expect(calls(c)->Array.length)->toBe(0)
|
|
161
|
+
})
|
|
162
|
+
})
|
|
@@ -0,0 +1,158 @@
|
|
|
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_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
|
|
6
|
+
let run = (async function(scenario) {
|
|
7
|
+
const mod = await import("../src/adapter/Runtime/StateTopicPublish.mjs");
|
|
8
|
+
const { withLiveUpdates } = mod;
|
|
9
|
+
const captured = [];
|
|
10
|
+
const publish = async (call) => { captured.push(call); };
|
|
11
|
+
const ok = async () => ({ TAG: "Ok", _0: undefined });
|
|
12
|
+
|
|
13
|
+
// Mock ops. load returns the composite partition's rows for the enumerate case.
|
|
14
|
+
const partitionRows = { TAG: "Ok", _0: [
|
|
15
|
+
{ id: "o1", lineId: "L1" },
|
|
16
|
+
{ id: "o1", lineId: "L2" },
|
|
17
|
+
]};
|
|
18
|
+
const ops = {
|
|
19
|
+
load: async (id) => partitionRows,
|
|
20
|
+
loadStream: () => {},
|
|
21
|
+
save: ok, saveBatch: ok, count: ok, delete: ok, deleteBatch: ok,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const cfg = (subIdField) => ({
|
|
25
|
+
endpoint: "https://x.appsync-api.eu-west-1.amazonaws.com",
|
|
26
|
+
region: "eu-west-1",
|
|
27
|
+
topicName: "Shop_Orders",
|
|
28
|
+
subIdField,
|
|
29
|
+
publish,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const w = (subIdField) => withLiveUpdates(ops, cfg(subIdField));
|
|
33
|
+
|
|
34
|
+
switch (scenario) {
|
|
35
|
+
case "save-single":
|
|
36
|
+
await w(undefined).save("p1", { id: "p1", updatedAt: "2024-01-02" }, "Insert", undefined);
|
|
37
|
+
break;
|
|
38
|
+
case "save-composite":
|
|
39
|
+
await w("lineId").save("o1", { id: "o1", lineId: "L2", createdAt: "2024-03-04" }, "Insert", undefined);
|
|
40
|
+
break;
|
|
41
|
+
case "save-no-sortkey":
|
|
42
|
+
await w(undefined).save("p1", { id: "p1" }, "Insert", undefined);
|
|
43
|
+
break;
|
|
44
|
+
case "saveBatch":
|
|
45
|
+
await w(undefined).saveBatch([
|
|
46
|
+
["a", { id: "a", updatedAt: "t1" }, undefined],
|
|
47
|
+
["b", { id: "b", updatedAt: "t2" }, undefined],
|
|
48
|
+
]);
|
|
49
|
+
break;
|
|
50
|
+
case "delete-single":
|
|
51
|
+
await w(undefined).delete("p1", undefined);
|
|
52
|
+
break;
|
|
53
|
+
case "delete-composite-row":
|
|
54
|
+
await w("lineId").delete("o1", ["lineId", "L2"]);
|
|
55
|
+
break;
|
|
56
|
+
case "delete-composite-partition":
|
|
57
|
+
await w("lineId").delete("o1", undefined);
|
|
58
|
+
break;
|
|
59
|
+
case "deleteBatch":
|
|
60
|
+
await w(undefined).deleteBatch([["a", undefined], ["b", undefined]]);
|
|
61
|
+
break;
|
|
62
|
+
case "gated-no-topic": {
|
|
63
|
+
const gatedOps = withLiveUpdates(ops, { endpoint: "https://x", region: "r", topicName: undefined, publish });
|
|
64
|
+
await gatedOps.save("p1", { id: "p1" }, "Insert", undefined);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return captured;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
function getField(call, key) {
|
|
72
|
+
return Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(call), d => d[key]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function str(call, key) {
|
|
76
|
+
return Stdlib_Option.flatMap(getField(call, key), Stdlib_JSON.Decode.string);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function calls(j) {
|
|
80
|
+
return Stdlib_Option.getOr(Stdlib_JSON.Decode.array(j), []);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
globalThis.describe("withLiveUpdates (B3.3a Postgres live updates)", () => {
|
|
84
|
+
globalThis.test("save on a single-key table publishes Updated with sortKeyValue", async () => {
|
|
85
|
+
let c = await run("save-single");
|
|
86
|
+
let arr = calls(c);
|
|
87
|
+
globalThis.expect(arr.length).toBe(1);
|
|
88
|
+
let call = arr[0];
|
|
89
|
+
globalThis.expect(str(call, "entityKey")).toEqual("p1");
|
|
90
|
+
globalThis.expect(str(call, "changeKind")).toEqual("Updated");
|
|
91
|
+
globalThis.expect(str(call, "sortKeyValue")).toEqual("2024-01-02");
|
|
92
|
+
globalThis.expect(str(call, "topicName")).toEqual("Shop_Orders");
|
|
93
|
+
});
|
|
94
|
+
globalThis.test("save on a composite table uses id-subKey and createdAt fallback", async () => {
|
|
95
|
+
let c = await run("save-composite");
|
|
96
|
+
let call = calls(c)[0];
|
|
97
|
+
globalThis.expect(str(call, "entityKey")).toEqual("o1-L2");
|
|
98
|
+
globalThis.expect(str(call, "changeKind")).toEqual("Updated");
|
|
99
|
+
globalThis.expect(str(call, "sortKeyValue")).toEqual("2024-03-04");
|
|
100
|
+
});
|
|
101
|
+
globalThis.test("save without updatedAt/createdAt omits sortKeyValue", async () => {
|
|
102
|
+
let c = await run("save-no-sortkey");
|
|
103
|
+
let call = calls(c)[0];
|
|
104
|
+
globalThis.expect(getField(call, "sortKeyValue")).toEqual(undefined);
|
|
105
|
+
});
|
|
106
|
+
globalThis.test("saveBatch publishes one Updated per item", async () => {
|
|
107
|
+
let c = await run("saveBatch");
|
|
108
|
+
let arr = calls(c);
|
|
109
|
+
globalThis.expect(arr.length).toBe(2);
|
|
110
|
+
globalThis.expect(str(arr[0], "entityKey")).toEqual("a");
|
|
111
|
+
globalThis.expect(str(arr[1], "entityKey")).toEqual("b");
|
|
112
|
+
arr.forEach(call => {
|
|
113
|
+
globalThis.expect(str(call, "changeKind")).toEqual("Updated");
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
globalThis.test("delete on a single-key table publishes Removed for the id", async () => {
|
|
117
|
+
let c = await run("delete-single");
|
|
118
|
+
let call = calls(c)[0];
|
|
119
|
+
globalThis.expect(str(call, "entityKey")).toEqual("p1");
|
|
120
|
+
globalThis.expect(str(call, "changeKind")).toEqual("Removed");
|
|
121
|
+
});
|
|
122
|
+
globalThis.test("delete of a specific composite row uses id-subValue", async () => {
|
|
123
|
+
let c = await run("delete-composite-row");
|
|
124
|
+
let arr = calls(c);
|
|
125
|
+
globalThis.expect(arr.length).toBe(1);
|
|
126
|
+
globalThis.expect(str(arr[0], "entityKey")).toEqual("o1-L2");
|
|
127
|
+
globalThis.expect(str(arr[0], "changeKind")).toEqual("Removed");
|
|
128
|
+
});
|
|
129
|
+
globalThis.test("partition-wide composite delete enumerates rows via load", async () => {
|
|
130
|
+
let c = await run("delete-composite-partition");
|
|
131
|
+
let arr = calls(c);
|
|
132
|
+
globalThis.expect(arr.length).toBe(2);
|
|
133
|
+
globalThis.expect(str(arr[0], "entityKey")).toEqual("o1-L1");
|
|
134
|
+
globalThis.expect(str(arr[1], "entityKey")).toEqual("o1-L2");
|
|
135
|
+
arr.forEach(call => {
|
|
136
|
+
globalThis.expect(str(call, "changeKind")).toEqual("Removed");
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
globalThis.test("deleteBatch publishes one Removed per entry", async () => {
|
|
140
|
+
let c = await run("deleteBatch");
|
|
141
|
+
let arr = calls(c);
|
|
142
|
+
globalThis.expect(arr.length).toBe(2);
|
|
143
|
+
globalThis.expect(str(arr[0], "entityKey")).toEqual("a");
|
|
144
|
+
globalThis.expect(str(arr[1], "entityKey")).toEqual("b");
|
|
145
|
+
});
|
|
146
|
+
globalThis.test("no topicName → ops pass through, nothing published", async () => {
|
|
147
|
+
let c = await run("gated-no-topic");
|
|
148
|
+
globalThis.expect(calls(c).length).toBe(0);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
export {
|
|
153
|
+
run,
|
|
154
|
+
getField,
|
|
155
|
+
str,
|
|
156
|
+
calls,
|
|
157
|
+
}
|
|
158
|
+
/* run Not a pure module */
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// Guards the ESM self-containment loader (Option C) against silent drift. The
|
|
4
|
+
// load-bearing contract: every code archive that ships the loader files MUST set
|
|
5
|
+
// NODE_OPTIONS=--import pointing at the shipped register-hook.mjs, and the two
|
|
6
|
+
// files must keep cross-referencing each other by the exact filenames used here.
|
|
7
|
+
// See docs/plans/deployed-lambda-esm-self-containment.md.
|
|
8
|
+
|
|
9
|
+
describe("Util_Bundle ESM loader — env-var contract", () => {
|
|
10
|
+
testSync("NODE_OPTIONS --imports the shipped register-hook file by name", () => {
|
|
11
|
+
expect(Util_Bundle.esmLoaderNodeOptions)->toBe(
|
|
12
|
+
`--import file:///var/task/${Util_Bundle.registerHookFileName}`,
|
|
13
|
+
)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
testSync("fallback dirs cover both the layer and the runtime SDK dir", () => {
|
|
17
|
+
// @aws-sdk/* lives in the runtime dir; @reventlessdev/*, effect, sury in the layer.
|
|
18
|
+
expect(Util_Bundle.esmFallbackDirs->String.includes("/opt/nodejs/node_modules"))->toBe(true)
|
|
19
|
+
expect(Util_Bundle.esmFallbackDirs->String.includes("/var/runtime/node_modules"))->toBe(true)
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe("Util_Bundle ESM loader — file cross-references", () => {
|
|
24
|
+
testSync("register-hook registers exactly the layer-resolver filename", () => {
|
|
25
|
+
expect(Util_Bundle.registerHookSource->String.includes(Util_Bundle.layerResolverFileName))->toBe(
|
|
26
|
+
true,
|
|
27
|
+
)
|
|
28
|
+
expect(Util_Bundle.registerHookSource->String.includes("register("))->toBe(true)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
testSync("layer-resolver retries only bare specifiers on ERR_MODULE_NOT_FOUND", () => {
|
|
32
|
+
expect(Util_Bundle.layerResolverSource->String.includes("ESM_FALLBACK_DIRS"))->toBe(true)
|
|
33
|
+
expect(Util_Bundle.layerResolverSource->String.includes("ERR_MODULE_NOT_FOUND"))->toBe(true)
|
|
34
|
+
// The resolve hook is the exported entry point Node's loader calls.
|
|
35
|
+
expect(Util_Bundle.layerResolverSource->String.includes("export async function resolve"))->toBe(
|
|
36
|
+
true,
|
|
37
|
+
)
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe("Util_Bundle.addEsmLoaderAssets", () => {
|
|
42
|
+
testSync("injects both loader files at the archive root", () => {
|
|
43
|
+
let contents: dict<Pulumi.Archive.assetOrArchive> = Dict.make()
|
|
44
|
+
let _ = Util_Bundle.addEsmLoaderAssets(contents)
|
|
45
|
+
expect(contents->Dict.has(Util_Bundle.registerHookFileName))->toBe(true)
|
|
46
|
+
expect(contents->Dict.has(Util_Bundle.layerResolverFileName))->toBe(true)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
testSync("returns a stable, non-empty hash fragment", () => {
|
|
50
|
+
let a: dict<Pulumi.Archive.assetOrArchive> = Dict.make()
|
|
51
|
+
let b: dict<Pulumi.Archive.assetOrArchive> = Dict.make()
|
|
52
|
+
let hashA = Util_Bundle.addEsmLoaderAssets(a)
|
|
53
|
+
let hashB = Util_Bundle.addEsmLoaderAssets(b)
|
|
54
|
+
expect(hashA->String.length > 0)->toBe(true)
|
|
55
|
+
expect(hashA)->toBe(hashB)
|
|
56
|
+
})
|
|
57
|
+
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Util_Bundle$ReventlessAws from "../src/util/Util_Bundle.res.mjs";
|
|
4
|
+
|
|
5
|
+
globalThis.describe("Util_Bundle ESM loader — env-var contract", () => {
|
|
6
|
+
globalThis.test("NODE_OPTIONS --imports the shipped register-hook file by name", () => {
|
|
7
|
+
globalThis.expect(Util_Bundle$ReventlessAws.esmLoaderNodeOptions).toBe(`--import file:///var/task/` + Util_Bundle$ReventlessAws.registerHookFileName);
|
|
8
|
+
});
|
|
9
|
+
globalThis.test("fallback dirs cover both the layer and the runtime SDK dir", () => {
|
|
10
|
+
globalThis.expect(Util_Bundle$ReventlessAws.esmFallbackDirs.includes("/opt/nodejs/node_modules")).toBe(true);
|
|
11
|
+
globalThis.expect(Util_Bundle$ReventlessAws.esmFallbackDirs.includes("/var/runtime/node_modules")).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
globalThis.describe("Util_Bundle ESM loader — file cross-references", () => {
|
|
16
|
+
globalThis.test("register-hook registers exactly the layer-resolver filename", () => {
|
|
17
|
+
globalThis.expect(Util_Bundle$ReventlessAws.registerHookSource.includes(Util_Bundle$ReventlessAws.layerResolverFileName)).toBe(true);
|
|
18
|
+
globalThis.expect(Util_Bundle$ReventlessAws.registerHookSource.includes("register(")).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
globalThis.test("layer-resolver retries only bare specifiers on ERR_MODULE_NOT_FOUND", () => {
|
|
21
|
+
globalThis.expect(Util_Bundle$ReventlessAws.layerResolverSource.includes("ESM_FALLBACK_DIRS")).toBe(true);
|
|
22
|
+
globalThis.expect(Util_Bundle$ReventlessAws.layerResolverSource.includes("ERR_MODULE_NOT_FOUND")).toBe(true);
|
|
23
|
+
globalThis.expect(Util_Bundle$ReventlessAws.layerResolverSource.includes("export async function resolve")).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
globalThis.describe("Util_Bundle.addEsmLoaderAssets", () => {
|
|
28
|
+
globalThis.test("injects both loader files at the archive root", () => {
|
|
29
|
+
let contents = {};
|
|
30
|
+
Util_Bundle$ReventlessAws.addEsmLoaderAssets(contents);
|
|
31
|
+
globalThis.expect(Util_Bundle$ReventlessAws.registerHookFileName in contents).toBe(true);
|
|
32
|
+
globalThis.expect(Util_Bundle$ReventlessAws.layerResolverFileName in contents).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
globalThis.test("returns a stable, non-empty hash fragment", () => {
|
|
35
|
+
let a = {};
|
|
36
|
+
let b = {};
|
|
37
|
+
let hashA = Util_Bundle$ReventlessAws.addEsmLoaderAssets(a);
|
|
38
|
+
let hashB = Util_Bundle$ReventlessAws.addEsmLoaderAssets(b);
|
|
39
|
+
globalThis.expect(hashA.length > 0).toBe(true);
|
|
40
|
+
globalThis.expect(hashA).toBe(hashB);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/* Not a pure module */
|