@reventlessdev/reventless-aws 3.0.0-alpha.172 → 3.0.0-alpha.174
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 +20 -0
- package/package.json +6 -6
- package/src/Platform.res +77 -7
- package/src/Platform.res.mjs +82 -8
- package/src/adapter/Counter/CounterHandler_DynamoDbStream.res.mjs +1 -1
- package/src/adapter/DcbEventLog/DcbBackend.res +78 -0
- package/src/adapter/DcbEventLog/DcbBackend.res.mjs +53 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage.res +20 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage.res.mjs +21 -1
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres.res +43 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres.res.mjs +21 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres_Runtime.res +26 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres_Runtime.res.mjs +15 -0
- package/src/adapter/EventLog/EventLogStorage_Postgres_Runtime.res +23 -0
- package/src/adapter/EventLog/EventLogStorage_Postgres_Runtime.res.mjs +14 -0
- package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +188 -0
- package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +141 -0
- package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +72 -0
- package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs +40 -0
- package/src/adapter/Runtime/AggregateEntryPoint.mjs +27 -9
- package/src/adapter/Runtime/AggregateRuntime_Builder_Micro.res.mjs +3 -3
- package/src/adapter/Runtime/AggregateRuntime_Builder_Micro_Async.res.mjs +3 -3
- package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate.res.mjs +1 -1
- package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate_Async.res.mjs +1 -1
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res.mjs +1 -1
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res.mjs +1 -1
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +1 -1
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +20 -6
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_PerEventCollector.res.mjs +1 -1
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res.mjs +1 -1
- package/src/adapter/Runtime/ExtensionPointRuntime_Builder_PerExtensionPoint.res.mjs +1 -1
- package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.mjs +52 -0
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +38 -0
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +20 -2
- package/src/adapter/Runtime/SideEffectHandlerRuntime_Builder_Single.res.mjs +1 -1
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +84 -6
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +49 -4
- package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res.mjs +1 -1
- package/src/adapter/Runtime/TaskRuntime_Builder_PerBucket.res.mjs +1 -1
- package/src/components/Api/AppSync_Adapter.res +72 -8
- package/src/components/Api/AppSync_Adapter.res.mjs +45 -6
- package/src/components/Plugin.res +1 -1
- package/src/components/Plugin.res.mjs +2 -2
- package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res.mjs +1 -1
- package/src/plugin/runtime/PluginRuntime_Builder.res +20 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +10 -2
- package/tests/AppSync_AdapterTest.res +140 -0
- package/tests/AppSync_AdapterTest.res.mjs +182 -6
- package/tests/PgChangeFeedRelay_RuntimeTest.res +75 -0
- package/tests/PgChangeFeedRelay_RuntimeTest.res.mjs +135 -0
|
@@ -27,14 +27,14 @@ let decodeFragment = GraphQL_Stitcher$ReventlessCore.decode;
|
|
|
27
27
|
globalThis.describe("AppSync_Adapter.injectAwsAuthAll", () => {
|
|
28
28
|
let baseFragment = AdminApi$ReventlessCore.baseFragment(true);
|
|
29
29
|
globalThis.test("adds @aws_auth directive to all mutation fields", () => {
|
|
30
|
-
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin");
|
|
30
|
+
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin", undefined);
|
|
31
31
|
let parts = GraphQL_Stitcher$ReventlessCore.decode(augmented);
|
|
32
32
|
parts.mutations.forEach(field => {
|
|
33
33
|
globalThis.expect(field).toContain(`@aws_auth(cognito_groups: ["Admin"])`);
|
|
34
34
|
});
|
|
35
35
|
});
|
|
36
36
|
globalThis.test("adds @aws_auth directive to all query fields", () => {
|
|
37
|
-
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin");
|
|
37
|
+
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin", undefined);
|
|
38
38
|
let parts = GraphQL_Stitcher$ReventlessCore.decode(augmented);
|
|
39
39
|
parts.queries.forEach(field => {
|
|
40
40
|
globalThis.expect(field).toContain(`@aws_auth(cognito_groups: ["Admin"])`);
|
|
@@ -42,12 +42,12 @@ globalThis.describe("AppSync_Adapter.injectAwsAuthAll", () => {
|
|
|
42
42
|
});
|
|
43
43
|
globalThis.test("preserves type definitions unchanged", () => {
|
|
44
44
|
let original = GraphQL_Stitcher$ReventlessCore.decode(baseFragment);
|
|
45
|
-
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin");
|
|
45
|
+
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin", undefined);
|
|
46
46
|
let parts = GraphQL_Stitcher$ReventlessCore.decode(augmented);
|
|
47
47
|
globalThis.expect(parts.types).toEqual(original.types);
|
|
48
48
|
});
|
|
49
49
|
globalThis.test("uses specified group name", () => {
|
|
50
|
-
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "SuperAdmin");
|
|
50
|
+
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "SuperAdmin", undefined);
|
|
51
51
|
let parts = GraphQL_Stitcher$ReventlessCore.decode(augmented);
|
|
52
52
|
parts.mutations.forEach(field => {
|
|
53
53
|
globalThis.expect(field).toContain(`cognito_groups: ["SuperAdmin"]`);
|
|
@@ -55,7 +55,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuthAll", () => {
|
|
|
55
55
|
});
|
|
56
56
|
globalThis.test("preserves subscription fields with @aws_auth directive", () => {
|
|
57
57
|
let original = GraphQL_Stitcher$ReventlessCore.decode(baseFragment);
|
|
58
|
-
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin");
|
|
58
|
+
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin", undefined);
|
|
59
59
|
let parts = GraphQL_Stitcher$ReventlessCore.decode(augmented);
|
|
60
60
|
globalThis.expect(parts.subscriptions.length).toBe(original.subscriptions.length);
|
|
61
61
|
globalThis.expect(parts.subscriptions.length).toBeGreaterThan(0);
|
|
@@ -64,7 +64,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuthAll", () => {
|
|
|
64
64
|
});
|
|
65
65
|
});
|
|
66
66
|
globalThis.test("admin Plugin aggregate subscription fields survive the round-trip", () => {
|
|
67
|
-
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin");
|
|
67
|
+
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin", undefined);
|
|
68
68
|
let parts = GraphQL_Stitcher$ReventlessCore.decode(augmented);
|
|
69
69
|
let joined = parts.subscriptions.join("\n");
|
|
70
70
|
globalThis.expect(joined).toContain("onPlatform_Plugin_Activate");
|
|
@@ -282,6 +282,182 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
|
|
|
282
282
|
});
|
|
283
283
|
});
|
|
284
284
|
|
|
285
|
+
globalThis.describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth", () => {
|
|
286
|
+
let makeFragment = (mutationFields, queryFields) => GraphQL_Stitcher$ReventlessCore.encode({
|
|
287
|
+
types: [],
|
|
288
|
+
mutations: mutationFields,
|
|
289
|
+
queries: queryFields,
|
|
290
|
+
subscriptions: []
|
|
291
|
+
});
|
|
292
|
+
globalThis.test("systemCallable mutation with a group emits @aws_cognito_user_pools + @aws_iam", () => {
|
|
293
|
+
let entry_fieldNames = ["p_Sync"];
|
|
294
|
+
let entry_fieldPermissions = Object.fromEntries([[
|
|
295
|
+
"p_Sync",
|
|
296
|
+
{
|
|
297
|
+
TAG: "AllowGroups",
|
|
298
|
+
_0: ["Admin"]
|
|
299
|
+
}
|
|
300
|
+
]]);
|
|
301
|
+
let entry_systemCallable = true;
|
|
302
|
+
let entry = {
|
|
303
|
+
fieldNames: entry_fieldNames,
|
|
304
|
+
commandSchema: S.unknown,
|
|
305
|
+
fieldPermissions: entry_fieldPermissions,
|
|
306
|
+
systemCallable: entry_systemCallable
|
|
307
|
+
};
|
|
308
|
+
let frag = makeFragment(["p_Sync(id: ID!): String"], []);
|
|
309
|
+
let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [entry], []);
|
|
310
|
+
let m = GraphQL_Stitcher$ReventlessCore.decode(aug).mutations[0];
|
|
311
|
+
globalThis.expect(m).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`);
|
|
312
|
+
globalThis.expect(m).toContain("@aws_iam");
|
|
313
|
+
globalThis.expect(m).not.toContain("@aws_auth");
|
|
314
|
+
});
|
|
315
|
+
globalThis.test("systemCallable mutation without a group restriction stays open to Cognito + IAM", () => {
|
|
316
|
+
let entry_fieldNames = ["p_Sync"];
|
|
317
|
+
let entry_fieldPermissions = Object.fromEntries([[
|
|
318
|
+
"p_Sync",
|
|
319
|
+
"AllowAuthenticated"
|
|
320
|
+
]]);
|
|
321
|
+
let entry_systemCallable = true;
|
|
322
|
+
let entry = {
|
|
323
|
+
fieldNames: entry_fieldNames,
|
|
324
|
+
commandSchema: S.unknown,
|
|
325
|
+
fieldPermissions: entry_fieldPermissions,
|
|
326
|
+
systemCallable: entry_systemCallable
|
|
327
|
+
};
|
|
328
|
+
let frag = makeFragment(["p_Sync(id: ID!): String"], []);
|
|
329
|
+
let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [entry], []);
|
|
330
|
+
let m = GraphQL_Stitcher$ReventlessCore.decode(aug).mutations[0];
|
|
331
|
+
globalThis.expect(m).toContain("@aws_cognito_user_pools @aws_iam");
|
|
332
|
+
globalThis.expect(m).not.toContain("cognito_groups");
|
|
333
|
+
globalThis.expect(m).not.toContain("@aws_auth");
|
|
334
|
+
});
|
|
335
|
+
globalThis.test("a non-systemCallable sibling keeps single-mode @aws_auth (no @aws_iam)", () => {
|
|
336
|
+
let entry_fieldNames = [
|
|
337
|
+
"p_Sync",
|
|
338
|
+
"p_Other"
|
|
339
|
+
];
|
|
340
|
+
let entry_fieldPermissions = Object.fromEntries([
|
|
341
|
+
[
|
|
342
|
+
"p_Sync",
|
|
343
|
+
{
|
|
344
|
+
TAG: "AllowGroups",
|
|
345
|
+
_0: ["Admin"]
|
|
346
|
+
}
|
|
347
|
+
],
|
|
348
|
+
[
|
|
349
|
+
"p_Other",
|
|
350
|
+
{
|
|
351
|
+
TAG: "AllowGroups",
|
|
352
|
+
_0: ["Admin"]
|
|
353
|
+
}
|
|
354
|
+
]
|
|
355
|
+
]);
|
|
356
|
+
let entry_systemCallable = false;
|
|
357
|
+
let entry = {
|
|
358
|
+
fieldNames: entry_fieldNames,
|
|
359
|
+
commandSchema: S.unknown,
|
|
360
|
+
fieldPermissions: entry_fieldPermissions,
|
|
361
|
+
systemCallable: entry_systemCallable
|
|
362
|
+
};
|
|
363
|
+
let iamEntry_fieldNames = ["p_Sync"];
|
|
364
|
+
let iamEntry_fieldPermissions = Object.fromEntries([[
|
|
365
|
+
"p_Sync",
|
|
366
|
+
{
|
|
367
|
+
TAG: "AllowGroups",
|
|
368
|
+
_0: ["Admin"]
|
|
369
|
+
}
|
|
370
|
+
]]);
|
|
371
|
+
let iamEntry_systemCallable = true;
|
|
372
|
+
let iamEntry = {
|
|
373
|
+
fieldNames: iamEntry_fieldNames,
|
|
374
|
+
commandSchema: S.unknown,
|
|
375
|
+
fieldPermissions: iamEntry_fieldPermissions,
|
|
376
|
+
systemCallable: iamEntry_systemCallable
|
|
377
|
+
};
|
|
378
|
+
let frag = makeFragment([
|
|
379
|
+
"p_Sync(id: ID!): String",
|
|
380
|
+
"p_Other(id: ID!): String"
|
|
381
|
+
], []);
|
|
382
|
+
let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [
|
|
383
|
+
entry,
|
|
384
|
+
iamEntry
|
|
385
|
+
], []);
|
|
386
|
+
let parts = GraphQL_Stitcher$ReventlessCore.decode(aug);
|
|
387
|
+
let other = parts.mutations.find(f => f.includes("p_Other"));
|
|
388
|
+
if (other !== undefined) {
|
|
389
|
+
globalThis.expect(other).toContain(`@aws_auth(cognito_groups: ["Admin"])`);
|
|
390
|
+
globalThis.expect(other).not.toContain("@aws_iam");
|
|
391
|
+
return;
|
|
392
|
+
} else {
|
|
393
|
+
return Stdlib_JsError.throwWithMessage("missing p_Other field");
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
globalThis.test("systemCallable query applies dual-auth to BOTH single and list fields", () => {
|
|
397
|
+
let entry_permission = {
|
|
398
|
+
TAG: "AllowGroups",
|
|
399
|
+
_0: ["Admin"]
|
|
400
|
+
};
|
|
401
|
+
let entry_systemCallable = true;
|
|
402
|
+
let entry = {
|
|
403
|
+
singleFieldName: "p_Item",
|
|
404
|
+
listFieldName: "p_Items",
|
|
405
|
+
returnTypeName: "p_Item",
|
|
406
|
+
stateSchema: S.unknown,
|
|
407
|
+
authorization: undefined,
|
|
408
|
+
permission: entry_permission,
|
|
409
|
+
systemCallable: entry_systemCallable
|
|
410
|
+
};
|
|
411
|
+
let frag = makeFragment([], [
|
|
412
|
+
"p_Item(id: ID!): Item",
|
|
413
|
+
"p_Items(first: Int, after: String): ItemConnection!"
|
|
414
|
+
]);
|
|
415
|
+
let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [], [entry]);
|
|
416
|
+
let parts = GraphQL_Stitcher$ReventlessCore.decode(aug);
|
|
417
|
+
let match = parts.queries.find(f => f.includes("p_Item("));
|
|
418
|
+
let match$1 = parts.queries.find(f => f.includes("p_Items("));
|
|
419
|
+
if (match !== undefined && match$1 !== undefined) {
|
|
420
|
+
globalThis.expect(match).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`);
|
|
421
|
+
globalThis.expect(match).toContain("@aws_iam");
|
|
422
|
+
globalThis.expect(match$1).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`);
|
|
423
|
+
globalThis.expect(match$1).toContain("@aws_iam");
|
|
424
|
+
return;
|
|
425
|
+
} else {
|
|
426
|
+
return Stdlib_JsError.throwWithMessage("missing query fields");
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
globalThis.describe("AppSync_Adapter.injectAwsAuthAll — ~iamFieldNames", () => {
|
|
432
|
+
let baseFragment = AdminApi$ReventlessCore.baseFragment(true);
|
|
433
|
+
globalThis.test("marks only the named field dual-auth, leaving others single-mode", () => {
|
|
434
|
+
let firstMutationName = GraphQL_Stitcher$ReventlessCore.extractLeadingName(GraphQL_Stitcher$ReventlessCore.decode(baseFragment).mutations[0]);
|
|
435
|
+
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin", [firstMutationName]);
|
|
436
|
+
let parts = GraphQL_Stitcher$ReventlessCore.decode(augmented);
|
|
437
|
+
parts.mutations.forEach(field => {
|
|
438
|
+
let name = GraphQL_Stitcher$ReventlessCore.extractLeadingName(field);
|
|
439
|
+
if (name === firstMutationName) {
|
|
440
|
+
globalThis.expect(field).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`);
|
|
441
|
+
globalThis.expect(field).toContain("@aws_iam");
|
|
442
|
+
globalThis.expect(field).not.toContain("@aws_auth");
|
|
443
|
+
} else {
|
|
444
|
+
globalThis.expect(field).toContain(`@aws_auth(cognito_groups: ["Admin"])`);
|
|
445
|
+
globalThis.expect(field).not.toContain("@aws_iam");
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
globalThis.test("default (no ~iamFieldNames) leaves every field single-mode @aws_auth", () => {
|
|
450
|
+
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin", undefined);
|
|
451
|
+
let parts = GraphQL_Stitcher$ReventlessCore.decode(augmented);
|
|
452
|
+
parts.mutations.forEach(field => {
|
|
453
|
+
globalThis.expect(field).not.toContain("@aws_iam");
|
|
454
|
+
});
|
|
455
|
+
parts.queries.forEach(field => {
|
|
456
|
+
globalThis.expect(field).not.toContain("@aws_iam");
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
285
461
|
globalThis.describe("Split mode — empty base fragment", () => {
|
|
286
462
|
globalThis.test("empty stitcher encode produces fragment with no fields", () => {
|
|
287
463
|
let emptyFragment = GraphQL_Stitcher$ReventlessCore.encode({
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// A realistic DCB feed event as PgChangeFeed.readBatch would surface it.
|
|
4
|
+
let sampleEvent: ReventlessCore.DcbEventLog_Adapter.rawSequencedEvent = {
|
|
5
|
+
position: "00000000000000000042:00000000000000000007",
|
|
6
|
+
eventType: "OrderCreated",
|
|
7
|
+
data: [("customerId", JSON.Encode.string("c-1"))]->Dict.fromArray->JSON.Encode.object,
|
|
8
|
+
tags: [{key: "orderId", value: "o-1"}],
|
|
9
|
+
meta: {
|
|
10
|
+
service: "Orders",
|
|
11
|
+
time: "2026-07-05T10:00:00.000Z",
|
|
12
|
+
msgId: "m-1",
|
|
13
|
+
correlationId: "m-1",
|
|
14
|
+
},
|
|
15
|
+
recordedAt: "2026-07-05T10:00:00.000Z",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("PgChangeFeedRelay_Runtime.toEventCollectorJson", () => {
|
|
19
|
+
testSync("produces the EventCollector {id, meta, event} body from a feed event", () => {
|
|
20
|
+
let json = PgChangeFeedRelay_Runtime.toEventCollectorJson(sampleEvent)->Option.getOrThrow
|
|
21
|
+
let obj = json->JSON.Decode.object->Option.getOrThrow
|
|
22
|
+
|
|
23
|
+
// id = derivePartitionKey with no partitionTag → "<firstTag.key>:<value>"
|
|
24
|
+
expect(obj->Dict.get("id"))->toEqual(Some(JSON.Encode.string("orderId:o-1")))
|
|
25
|
+
|
|
26
|
+
// event carries the event type + payload (combineMessage encodes a
|
|
27
|
+
// payload-bearing variant as an object, e.g. {"OrderCreated": {...}})
|
|
28
|
+
let eventJson = obj->Dict.get("event")->Option.getOrThrow
|
|
29
|
+
expect(eventJson->JSON.stringify->String.includes("OrderCreated"))->toBe(true)
|
|
30
|
+
expect(eventJson->JSON.stringify->String.includes("c-1"))->toBe(true)
|
|
31
|
+
|
|
32
|
+
// meta round-trips the producing service
|
|
33
|
+
let metaObj =
|
|
34
|
+
obj->Dict.get("meta")->Option.flatMap(JSON.Decode.object)->Option.getOrThrow
|
|
35
|
+
expect(metaObj->Dict.get("service"))->toEqual(Some(JSON.Encode.string("Orders")))
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
testSync("derives the 'dcb' id when the event carries no tags", () => {
|
|
39
|
+
let noTags = {...sampleEvent, tags: []}
|
|
40
|
+
let json = PgChangeFeedRelay_Runtime.toEventCollectorJson(noTags)->Option.getOrThrow
|
|
41
|
+
let obj = json->JSON.Decode.object->Option.getOrThrow
|
|
42
|
+
expect(obj->Dict.get("id"))->toEqual(Some(JSON.Encode.string("dcb")))
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe("PgChangeFeedRelay_Runtime partitionTag (B2.3d)", () => {
|
|
47
|
+
let multiTagEvent = {
|
|
48
|
+
...sampleEvent,
|
|
49
|
+
tags: [{key: "orderId", value: "o-1"}, {key: "customerId", value: "c-9"}],
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
testSync("a Simple partitionTag selects its tag for the id, not just the first", () => {
|
|
53
|
+
let pt = Reventless.DcbTag.Simple({key: "customerId"})
|
|
54
|
+
let json =
|
|
55
|
+
PgChangeFeedRelay_Runtime.toEventCollectorJson(multiTagEvent, ~partitionTag=pt)->Option.getOrThrow
|
|
56
|
+
let obj = json->JSON.Decode.object->Option.getOrThrow
|
|
57
|
+
// Without partitionTag the id would be "orderId:o-1" (first tag); the tag
|
|
58
|
+
// pins it to "customerId:c-9".
|
|
59
|
+
expect(obj->Dict.get("id"))->toEqual(Some(JSON.Encode.string("customerId:c-9")))
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
testSync("derivedPartitionTag round-trips through the sury schema (relay wire-format)", () => {
|
|
63
|
+
// The relay builder serialises partitionTag with this schema into HANDLER_CONFIG
|
|
64
|
+
// and PgChangeFeedRelay_Runtime.relay parses it back the same way. Guard the
|
|
65
|
+
// round-trip so a schema change can't silently break the id derivation.
|
|
66
|
+
let simple = Reventless.DcbTag.Simple({key: "courseId"})
|
|
67
|
+
let composite =
|
|
68
|
+
Reventless.DcbTag.Composite({keys: ["studentId", "courseId"], seps: [":"]})
|
|
69
|
+
[simple, composite]->Array.forEach(pt => {
|
|
70
|
+
let wire = pt->S.reverseConvertToJsonOrThrow(Reventless.DcbTag.derivedPartitionTagSchema)
|
|
71
|
+
let back = wire->S.parseJsonOrThrow(Reventless.DcbTag.derivedPartitionTagSchema)
|
|
72
|
+
expect(back)->toEqual(pt)
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
})
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
5
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
7
|
+
import * as PgChangeFeedRelay_Runtime$ReventlessAws from "../src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs";
|
|
8
|
+
|
|
9
|
+
let sampleEvent_data = Object.fromEntries([[
|
|
10
|
+
"customerId",
|
|
11
|
+
"c-1"
|
|
12
|
+
]]);
|
|
13
|
+
|
|
14
|
+
let sampleEvent_tags = [{
|
|
15
|
+
key: "orderId",
|
|
16
|
+
value: "o-1"
|
|
17
|
+
}];
|
|
18
|
+
|
|
19
|
+
let sampleEvent_meta = {
|
|
20
|
+
service: "Orders",
|
|
21
|
+
time: "2026-07-05T10:00:00.000Z",
|
|
22
|
+
msgId: "m-1",
|
|
23
|
+
correlationId: "m-1"
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
let sampleEvent = {
|
|
27
|
+
position: "00000000000000000042:00000000000000000007",
|
|
28
|
+
eventType: "OrderCreated",
|
|
29
|
+
data: sampleEvent_data,
|
|
30
|
+
tags: sampleEvent_tags,
|
|
31
|
+
meta: sampleEvent_meta,
|
|
32
|
+
recordedAt: "2026-07-05T10:00:00.000Z"
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
globalThis.describe("PgChangeFeedRelay_Runtime.toEventCollectorJson", () => {
|
|
36
|
+
globalThis.test("produces the EventCollector {id, meta, event} body from a feed event", () => {
|
|
37
|
+
let json = Stdlib_Option.getOrThrow(PgChangeFeedRelay_Runtime$ReventlessAws.toEventCollectorJson(sampleEvent, undefined), undefined);
|
|
38
|
+
let obj = Stdlib_Option.getOrThrow(Stdlib_JSON.Decode.object(json), undefined);
|
|
39
|
+
globalThis.expect(obj["id"]).toEqual("orderId:o-1");
|
|
40
|
+
let eventJson = Stdlib_Option.getOrThrow(obj["event"], undefined);
|
|
41
|
+
globalThis.expect(JSON.stringify(eventJson).includes("OrderCreated")).toBe(true);
|
|
42
|
+
globalThis.expect(JSON.stringify(eventJson).includes("c-1")).toBe(true);
|
|
43
|
+
let metaObj = Stdlib_Option.getOrThrow(Stdlib_Option.flatMap(obj["meta"], Stdlib_JSON.Decode.object), undefined);
|
|
44
|
+
globalThis.expect(metaObj["service"]).toEqual("Orders");
|
|
45
|
+
});
|
|
46
|
+
globalThis.test("derives the 'dcb' id when the event carries no tags", () => {
|
|
47
|
+
let noTags_data = sampleEvent_data;
|
|
48
|
+
let noTags_tags = [];
|
|
49
|
+
let noTags_meta = {
|
|
50
|
+
service: "Orders",
|
|
51
|
+
time: "2026-07-05T10:00:00.000Z",
|
|
52
|
+
msgId: "m-1",
|
|
53
|
+
correlationId: "m-1"
|
|
54
|
+
};
|
|
55
|
+
let noTags = {
|
|
56
|
+
position: "00000000000000000042:00000000000000000007",
|
|
57
|
+
eventType: "OrderCreated",
|
|
58
|
+
data: noTags_data,
|
|
59
|
+
tags: noTags_tags,
|
|
60
|
+
meta: noTags_meta,
|
|
61
|
+
recordedAt: "2026-07-05T10:00:00.000Z"
|
|
62
|
+
};
|
|
63
|
+
let json = Stdlib_Option.getOrThrow(PgChangeFeedRelay_Runtime$ReventlessAws.toEventCollectorJson(noTags, undefined), undefined);
|
|
64
|
+
let obj = Stdlib_Option.getOrThrow(Stdlib_JSON.Decode.object(json), undefined);
|
|
65
|
+
globalThis.expect(obj["id"]).toEqual("dcb");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
globalThis.describe("PgChangeFeedRelay_Runtime partitionTag (B2.3d)", () => {
|
|
70
|
+
let multiTagEvent_data = sampleEvent_data;
|
|
71
|
+
let multiTagEvent_tags = [
|
|
72
|
+
{
|
|
73
|
+
key: "orderId",
|
|
74
|
+
value: "o-1"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
key: "customerId",
|
|
78
|
+
value: "c-9"
|
|
79
|
+
}
|
|
80
|
+
];
|
|
81
|
+
let multiTagEvent_meta = {
|
|
82
|
+
service: "Orders",
|
|
83
|
+
time: "2026-07-05T10:00:00.000Z",
|
|
84
|
+
msgId: "m-1",
|
|
85
|
+
correlationId: "m-1"
|
|
86
|
+
};
|
|
87
|
+
let multiTagEvent = {
|
|
88
|
+
position: "00000000000000000042:00000000000000000007",
|
|
89
|
+
eventType: "OrderCreated",
|
|
90
|
+
data: multiTagEvent_data,
|
|
91
|
+
tags: multiTagEvent_tags,
|
|
92
|
+
meta: multiTagEvent_meta,
|
|
93
|
+
recordedAt: "2026-07-05T10:00:00.000Z"
|
|
94
|
+
};
|
|
95
|
+
globalThis.test("a Simple partitionTag selects its tag for the id, not just the first", () => {
|
|
96
|
+
let json = Stdlib_Option.getOrThrow(PgChangeFeedRelay_Runtime$ReventlessAws.toEventCollectorJson(multiTagEvent, {
|
|
97
|
+
TAG: "Simple",
|
|
98
|
+
_0: {
|
|
99
|
+
key: "customerId"
|
|
100
|
+
}
|
|
101
|
+
}), undefined);
|
|
102
|
+
let obj = Stdlib_Option.getOrThrow(Stdlib_JSON.Decode.object(json), undefined);
|
|
103
|
+
globalThis.expect(obj["id"]).toEqual("customerId:c-9");
|
|
104
|
+
});
|
|
105
|
+
globalThis.test("derivedPartitionTag round-trips through the sury schema (relay wire-format)", () => {
|
|
106
|
+
let composite = {
|
|
107
|
+
TAG: "Composite",
|
|
108
|
+
_0: {
|
|
109
|
+
keys: [
|
|
110
|
+
"studentId",
|
|
111
|
+
"courseId"
|
|
112
|
+
],
|
|
113
|
+
seps: [":"]
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
[
|
|
117
|
+
{
|
|
118
|
+
TAG: "Simple",
|
|
119
|
+
_0: {
|
|
120
|
+
key: "courseId"
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
composite
|
|
124
|
+
].forEach(pt => {
|
|
125
|
+
let wire = S.reverseConvertToJsonOrThrow(pt, DcbTag$Reventless.derivedPartitionTagSchema);
|
|
126
|
+
let back = S.parseJsonOrThrow(wire, DcbTag$Reventless.derivedPartitionTagSchema);
|
|
127
|
+
globalThis.expect(back).toEqual(pt);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
export {
|
|
133
|
+
sampleEvent,
|
|
134
|
+
}
|
|
135
|
+
/* sampleEvent Not a pure module */
|