@reventlessdev/reventless-aws 3.0.0-alpha.177 → 3.0.0-alpha.179
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 +21 -0
- package/docker-compose.postgres.yml +21 -0
- package/package.json +6 -5
- package/scripts/run-pg-integration-tests.sh +58 -0
- package/src/Platform.res +37 -0
- package/src/Platform.res.mjs +41 -4
- 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/DcbEventLog/DcbEventLogStorage_Postgres_Runtime.res +4 -2
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres_Runtime.res.mjs +2 -2
- package/src/adapter/EventLog/EventLogStorage_Postgres_Runtime.res +3 -1
- package/src/adapter/EventLog/EventLogStorage_Postgres_Runtime.res.mjs +2 -2
- 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 +11 -13
- package/src/adapter/QueryDb/PgQueryResolver_Builder.res.mjs +5 -24
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res +51 -3
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res.mjs +48 -18
- package/src/adapter/QueryDb/QueryDbBackend.res +8 -0
- package/src/adapter/QueryDb/QueryDbBackend.res.mjs +4 -1
- package/src/adapter/QueryDb/QueryDbResolvers.res.mjs +1 -1
- package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res +31 -7
- package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res.mjs +16 -10
- package/src/adapter/QueryDb/QueryDbStorage.res +6 -1
- package/src/adapter/QueryDb/QueryDbStorage.res.mjs +2 -1
- package/src/adapter/QueryDb/QueryDbStorage_Postgres.res.mjs +1 -1
- package/src/adapter/QueryDb/QueryDbStorage_Postgres_Runtime.res +3 -1
- package/src/adapter/QueryDb/QueryDbStorage_Postgres_Runtime.res.mjs +2 -2
- 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/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 +59 -0
- package/tests/PgQueryResolver_LambdaTest.res.mjs +95 -16
- 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
|
@@ -157,7 +157,7 @@ function ids(arr) {
|
|
|
157
157
|
|
|
158
158
|
globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
|
|
159
159
|
globalThis.test("getById returns the item (id-carrying)", async () => {
|
|
160
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), mkPayload("getById", undefined, Object.fromEntries([[
|
|
160
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
|
|
161
161
|
"id",
|
|
162
162
|
"p-2"
|
|
163
163
|
]]), undefined));
|
|
@@ -165,14 +165,14 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
|
|
|
165
165
|
globalThis.expect(str(r, "name")).toEqual("Bravo");
|
|
166
166
|
});
|
|
167
167
|
globalThis.test("getById miss → null", async () => {
|
|
168
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), mkPayload("getById", undefined, Object.fromEntries([[
|
|
168
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
|
|
169
169
|
"id",
|
|
170
170
|
"absent"
|
|
171
171
|
]]), undefined));
|
|
172
172
|
globalThis.expect(r).toBe(null);
|
|
173
173
|
});
|
|
174
174
|
globalThis.test("byIds returns matches, drops missing", async () => {
|
|
175
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), mkPayload("byIds", undefined, Object.fromEntries([[
|
|
175
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), undefined, mkPayload("byIds", undefined, Object.fromEntries([[
|
|
176
176
|
"ids",
|
|
177
177
|
[
|
|
178
178
|
"p-1",
|
|
@@ -186,7 +186,7 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
|
|
|
186
186
|
]);
|
|
187
187
|
});
|
|
188
188
|
globalThis.test("index routes to indexLookup on the idField", async () => {
|
|
189
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), mkPayload("index", "byStatus", Object.fromEntries([[
|
|
189
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), undefined, mkPayload("index", "byStatus", Object.fromEntries([[
|
|
190
190
|
"byStatus",
|
|
191
191
|
"active"
|
|
192
192
|
]]), undefined));
|
|
@@ -197,13 +197,13 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
|
|
|
197
197
|
});
|
|
198
198
|
globalThis.test("list with push-down → returns the connection listPage gives", async () => {
|
|
199
199
|
listPageReturn.contents = "PUSHED";
|
|
200
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), mkPayload("list", undefined, undefined, undefined));
|
|
200
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), undefined, mkPayload("list", undefined, undefined, undefined));
|
|
201
201
|
listPageReturn.contents = undefined;
|
|
202
202
|
globalThis.expect(r).toEqual("PUSHED");
|
|
203
203
|
});
|
|
204
204
|
globalThis.test("list without push-down → falls back to the shared spec", async () => {
|
|
205
205
|
listPageReturn.contents = undefined;
|
|
206
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), mkPayload("list", undefined, undefined, undefined));
|
|
206
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), undefined, mkPayload("list", undefined, undefined, undefined));
|
|
207
207
|
let edgeIds = Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_Option.flatMap(field(r, "edges"), Stdlib_JSON.Decode.array), []), e => Stdlib_Option.flatMap(field(e, "node"), n => str(n, "id")));
|
|
208
208
|
globalThis.expect(edgeIds).toEqual([
|
|
209
209
|
"p-1",
|
|
@@ -212,14 +212,14 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
|
|
|
212
212
|
]);
|
|
213
213
|
});
|
|
214
214
|
globalThis.test("items with subId routes to itemsPage (id echoed)", async () => {
|
|
215
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, "seq", undefined), mkPayload("items", undefined, Object.fromEntries([[
|
|
215
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, "seq", undefined), undefined, mkPayload("items", undefined, Object.fromEntries([[
|
|
216
216
|
"id",
|
|
217
217
|
"p-2"
|
|
218
218
|
]]), undefined));
|
|
219
219
|
globalThis.expect(str(r, "itemsFor")).toEqual("p-2");
|
|
220
220
|
});
|
|
221
221
|
globalThis.test("items without subId → empty connection", async () => {
|
|
222
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), mkPayload("items", undefined, Object.fromEntries([[
|
|
222
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), undefined, mkPayload("items", undefined, Object.fromEntries([[
|
|
223
223
|
"id",
|
|
224
224
|
"p-2"
|
|
225
225
|
]]), undefined));
|
|
@@ -227,14 +227,14 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
|
|
|
227
227
|
});
|
|
228
228
|
globalThis.test("authorization DenyAll → empty shape per kind", async () => {
|
|
229
229
|
let b = makeBinding("DenyAll", undefined, undefined);
|
|
230
|
-
let single = await PgQueryResolver_Lambda$ReventlessAws.dispatch(b, mkPayload("getById", undefined, Object.fromEntries([[
|
|
230
|
+
let single = await PgQueryResolver_Lambda$ReventlessAws.dispatch(b, undefined, mkPayload("getById", undefined, Object.fromEntries([[
|
|
231
231
|
"id",
|
|
232
232
|
"p-1"
|
|
233
233
|
]]), undefined));
|
|
234
234
|
globalThis.expect(single).toBe(null);
|
|
235
|
-
let list = await PgQueryResolver_Lambda$ReventlessAws.dispatch(b, mkPayload("list", undefined, undefined, undefined));
|
|
235
|
+
let list = await PgQueryResolver_Lambda$ReventlessAws.dispatch(b, undefined, mkPayload("list", undefined, undefined, undefined));
|
|
236
236
|
globalThis.expect(Stdlib_Option.getOr(Stdlib_Option.flatMap(field(list, "edges"), Stdlib_JSON.Decode.array), []).length).toBe(0);
|
|
237
|
-
let byIds = await PgQueryResolver_Lambda$ReventlessAws.dispatch(b, mkPayload("byIds", undefined, Object.fromEntries([[
|
|
237
|
+
let byIds = await PgQueryResolver_Lambda$ReventlessAws.dispatch(b, undefined, mkPayload("byIds", undefined, Object.fromEntries([[
|
|
238
238
|
"ids",
|
|
239
239
|
[]
|
|
240
240
|
]]), undefined));
|
|
@@ -245,7 +245,7 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
|
|
|
245
245
|
TAG: "Deny",
|
|
246
246
|
_0: "nope"
|
|
247
247
|
}));
|
|
248
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), mkPayload("getById", undefined, Object.fromEntries([[
|
|
248
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
|
|
249
249
|
"id",
|
|
250
250
|
"p-1"
|
|
251
251
|
]]), undefined));
|
|
@@ -271,7 +271,7 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
|
|
|
271
271
|
arguments: payload_arguments,
|
|
272
272
|
identity: Identity$Reventless.anonymous
|
|
273
273
|
};
|
|
274
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), payload);
|
|
274
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), undefined, payload);
|
|
275
275
|
globalThis.expect(str(r, "id")).toEqual("p-2");
|
|
276
276
|
});
|
|
277
277
|
globalThis.test("resolveOne multi via target index → array", async () => {
|
|
@@ -297,7 +297,7 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
|
|
|
297
297
|
arguments: payload_arguments,
|
|
298
298
|
identity: Identity$Reventless.anonymous
|
|
299
299
|
};
|
|
300
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), payload);
|
|
300
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), undefined, payload);
|
|
301
301
|
globalThis.expect(ids(r)).toEqual([
|
|
302
302
|
"p-1",
|
|
303
303
|
"p-3"
|
|
@@ -324,7 +324,7 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
|
|
|
324
324
|
arguments: payload_arguments,
|
|
325
325
|
identity: Identity$Reventless.anonymous
|
|
326
326
|
};
|
|
327
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), payload);
|
|
327
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), undefined, payload);
|
|
328
328
|
globalThis.expect(ids(r)).toEqual([
|
|
329
329
|
"p-1",
|
|
330
330
|
"p-3"
|
|
@@ -361,10 +361,89 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
|
|
|
361
361
|
let r = await PgQueryResolver_Lambda$ReventlessAws.handler(payload, null);
|
|
362
362
|
globalThis.expect(r).toBe(null);
|
|
363
363
|
});
|
|
364
|
+
let authStore = Object.fromEntries([[
|
|
365
|
+
"active",
|
|
366
|
+
Object.fromEntries([[
|
|
367
|
+
"ownerId",
|
|
368
|
+
"alice"
|
|
369
|
+
]])
|
|
370
|
+
]]);
|
|
371
|
+
let authOps_load = async id => ({
|
|
372
|
+
TAG: "Ok",
|
|
373
|
+
_0: Stdlib_Option.mapOr(authStore[id], [], r => [r])
|
|
374
|
+
});
|
|
375
|
+
let authOps = {
|
|
376
|
+
load: authOps_load,
|
|
377
|
+
loadStream: 0,
|
|
378
|
+
save: 0,
|
|
379
|
+
saveBatch: 0,
|
|
380
|
+
count: 0,
|
|
381
|
+
delete: 0,
|
|
382
|
+
deleteBatch: 0
|
|
383
|
+
};
|
|
384
|
+
let init = makeBinding(undefined, undefined, undefined);
|
|
385
|
+
let authBinding_pushdowns = init.pushdowns;
|
|
386
|
+
let authBinding_indexes = init.indexes;
|
|
387
|
+
let authBinding_subIdField = init.subIdField;
|
|
388
|
+
let authBinding_capability = init.capability;
|
|
389
|
+
let authBinding_labelField = init.labelField;
|
|
390
|
+
let authBinding_includeIdParam = init.includeIdParam;
|
|
391
|
+
let authBinding_authorization = init.authorization;
|
|
392
|
+
let authBinding = {
|
|
393
|
+
ops: authOps,
|
|
394
|
+
pushdowns: authBinding_pushdowns,
|
|
395
|
+
indexes: authBinding_indexes,
|
|
396
|
+
subIdField: authBinding_subIdField,
|
|
397
|
+
capability: authBinding_capability,
|
|
398
|
+
labelField: authBinding_labelField,
|
|
399
|
+
includeIdParam: authBinding_includeIdParam,
|
|
400
|
+
authorization: authBinding_authorization
|
|
401
|
+
};
|
|
402
|
+
let lookupAuth = name => {
|
|
403
|
+
if (name === "AuthTable") {
|
|
404
|
+
return authBinding;
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
let ident = (username, groups) => ({
|
|
408
|
+
userId: username,
|
|
409
|
+
username: username,
|
|
410
|
+
groups: groups,
|
|
411
|
+
provider: "InMemory"
|
|
412
|
+
});
|
|
413
|
+
let authIndexPayload = (username, groups) => ({
|
|
414
|
+
readModelName: "Things",
|
|
415
|
+
kind: "index",
|
|
416
|
+
index: "byStatus",
|
|
417
|
+
authTable: "AuthTable",
|
|
418
|
+
authGroup: "Owner",
|
|
419
|
+
arguments: Object.fromEntries([[
|
|
420
|
+
"byStatus",
|
|
421
|
+
"active"
|
|
422
|
+
]]),
|
|
423
|
+
identity: ident(username, groups)
|
|
424
|
+
});
|
|
425
|
+
globalThis.test("auth index: group member who owns → results", async () => {
|
|
426
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), lookupAuth, authIndexPayload("alice", ["Owner"]));
|
|
427
|
+
globalThis.expect(ids(r)).toEqual([
|
|
428
|
+
"p-1",
|
|
429
|
+
"p-3"
|
|
430
|
+
]);
|
|
431
|
+
});
|
|
432
|
+
globalThis.test("auth index: group member who does NOT own → empty", async () => {
|
|
433
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), lookupAuth, authIndexPayload("bob", ["Owner"]));
|
|
434
|
+
globalThis.expect(Stdlib_Option.getOr(Stdlib_JSON.Decode.array(r), []).length).toBe(0);
|
|
435
|
+
});
|
|
436
|
+
globalThis.test("auth index: non-member passes through → results", async () => {
|
|
437
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), lookupAuth, authIndexPayload("bob", []));
|
|
438
|
+
globalThis.expect(ids(r)).toEqual([
|
|
439
|
+
"p-1",
|
|
440
|
+
"p-3"
|
|
441
|
+
]);
|
|
442
|
+
});
|
|
364
443
|
globalThis.test("unmapped kind throws", async () => {
|
|
365
444
|
let threw;
|
|
366
445
|
try {
|
|
367
|
-
await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), mkPayload("frobnicate", undefined, undefined, undefined));
|
|
446
|
+
await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), undefined, mkPayload("frobnicate", undefined, undefined, undefined));
|
|
368
447
|
threw = false;
|
|
369
448
|
} catch (exn) {
|
|
370
449
|
threw = true;
|
|
@@ -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 */
|