@reventlessdev/reventless-aws 3.0.0-alpha.400 → 3.0.0-alpha.402
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 +33 -0
- package/package.json +9 -9
- package/src/Platform.res.mjs +2 -2
- package/src/adapter/Mcp/MCP_Lambda.res +1 -1
- package/src/adapter/Mcp/MCP_Lambda.res.mjs +0 -1
- package/src/adapter/Runtime/EventCollectorEntryPoint.mjs +6 -14
- package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res +13 -123
- package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res.mjs +8 -74
- package/src/adapter/Runtime/HeartbeatEntryPoint.res +0 -1
- package/src/adapter/Runtime/HeartbeatEntryPoint.res.mjs +1 -1
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +5 -5
- package/src/adapter/Upload/Upload_Presign_S3_Ops.res +13 -3
- package/src/adapter/Upload/Upload_Presign_S3_Ops.res.mjs +7 -2
- package/src/plugin/cloner/ClonerRunner_Fargate.res +1 -2
- package/src/plugin/cloner/ClonerRunner_Fargate.res.mjs +1 -2
- package/src/plugin/runtime/PluginRuntime_Builder.res +13 -49
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +2 -15
- package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res.mjs +1 -1
- package/tests/EventCollectorEntryPoint_OpsTest.res +27 -32
- package/tests/EventCollectorEntryPoint_OpsTest.res.mjs +28 -46
- package/tests/EventMapperEntryPoint_OpsTest.res +1 -1
- package/tests/EventMapperEntryPoint_OpsTest.res.mjs +1 -1
- package/tests/PgChangeFeedRelay_IntegrationTest.res.mjs +3 -3
- package/tests/PgPipeline_IntegrationTest.res.mjs +1 -1
- package/tests/Upload_ReleaseRuleTest.res +24 -0
- package/tests/Upload_ReleaseRuleTest.res.mjs +15 -0
- package/tests/integration/DcbEventLogStorage_DynamoDb_IntegrationTest.res.mjs +1 -1
- package/src/components/ForeignReadModel_Builder.res +0 -19
- package/src/components/ForeignReadModel_Builder.res.mjs +0 -54
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// surface — the earlier `decodeJwtSub` path is gone.
|
|
12
12
|
//
|
|
13
13
|
// Two operations dispatch on the `operation` field the resolver bakes in:
|
|
14
|
-
// presign → derive `{servedPrefix}/{sub}/{uuid}
|
|
14
|
+
// presign → derive `{servedPrefix}/{sub}/{uuid}.{ext}`, presign a PUT to the
|
|
15
15
|
// store's bucket (expires 300s), return `{uploadUrl, storageRef}`.
|
|
16
16
|
// release → apply the release rule (scope + age) to a `storageRef` and, if it
|
|
17
17
|
// passes, `DeleteObject`; return `{released, reason}`.
|
|
@@ -211,6 +211,10 @@ let headLastModifiedMs = async (~client: s3Client, ~bucket: string, ~key: string
|
|
|
211
211
|
|
|
212
212
|
// ── Operations ──────────────────────────────────────────────────────────────
|
|
213
213
|
|
|
214
|
+
// The object key a presign mints, pure so a test can pin its shape.
|
|
215
|
+
let presignKey = (~servedPrefix, ~sub, ~uuid, ~contentType) =>
|
|
216
|
+
`${servedPrefix}/${sub}/${Reventless.StorageRef.objectName(~uuid, ~contentType)}`
|
|
217
|
+
|
|
214
218
|
let handlePresign = async (
|
|
215
219
|
~client: s3Client,
|
|
216
220
|
~bucket: string,
|
|
@@ -221,11 +225,17 @@ let handlePresign = async (
|
|
|
221
225
|
if sub == "" {
|
|
222
226
|
JsError.throwWithMessage("unauthenticated")
|
|
223
227
|
}
|
|
224
|
-
let fileName = args.fileName->Option.getOr("upload")
|
|
225
228
|
// The object key doubles as the served path segment: rooted at the store's
|
|
226
229
|
// served prefix so the CloudFront `{prefix}/*` behavior fronts it, and namespaced
|
|
227
230
|
// by the verified `sub` so the release rule can tell one caller's objects apart.
|
|
228
|
-
|
|
231
|
+
// The `fileName` argument is not used: the key ends in an opaque name, because a
|
|
232
|
+
// ref is stored in an event and a file name is often personal data.
|
|
233
|
+
let key = presignKey(
|
|
234
|
+
~servedPrefix,
|
|
235
|
+
~sub,
|
|
236
|
+
~uuid=NodeCrypto.randomUUID(),
|
|
237
|
+
~contentType=args.contentType,
|
|
238
|
+
)
|
|
229
239
|
// The object is provisional from the moment the bytes land: it carries the
|
|
230
240
|
// pending tag until a committed event referencing it strips it (see
|
|
231
241
|
// `Upload_Claim_S3_Ops`). Written into the signature here, not asked of the
|
|
@@ -9,6 +9,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
9
9
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
10
10
|
import * as ClientS3 from "@aws-sdk/client-s3";
|
|
11
11
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
12
|
+
import * as StorageRef$Reventless from "@reventlessdev/reventless-spec/src/semantic/StorageRef.res.mjs";
|
|
12
13
|
import * as S3RequestPresigner from "@aws-sdk/s3-request-presigner";
|
|
13
14
|
import * as Upload_PendingTag$ReventlessAws from "./Upload_PendingTag.res.mjs";
|
|
14
15
|
|
|
@@ -167,12 +168,15 @@ async function headLastModifiedMs(client, bucket, key) {
|
|
|
167
168
|
}
|
|
168
169
|
}
|
|
169
170
|
|
|
171
|
+
function presignKey(servedPrefix, sub, uuid, contentType) {
|
|
172
|
+
return servedPrefix + `/` + sub + `/` + StorageRef$Reventless.objectName(uuid, contentType);
|
|
173
|
+
}
|
|
174
|
+
|
|
170
175
|
async function handlePresign(client, bucket, servedPrefix, sub, args) {
|
|
171
176
|
if (sub === "") {
|
|
172
177
|
Stdlib_JsError.throwWithMessage("unauthenticated");
|
|
173
178
|
}
|
|
174
|
-
let
|
|
175
|
-
let key = servedPrefix + `/` + sub + `/` + Nodecrypto.randomUUID() + `/` + fileName;
|
|
179
|
+
let key = presignKey(servedPrefix, sub, Nodecrypto.randomUUID(), args.contentType);
|
|
176
180
|
let command = new ClientS3.PutObjectCommand({
|
|
177
181
|
Bucket: bucket,
|
|
178
182
|
Key: key,
|
|
@@ -235,6 +239,7 @@ export {
|
|
|
235
239
|
releaseResult,
|
|
236
240
|
keyOfRef,
|
|
237
241
|
headLastModifiedMs,
|
|
242
|
+
presignKey,
|
|
238
243
|
handlePresign,
|
|
239
244
|
handleRelease,
|
|
240
245
|
handler,
|
|
@@ -114,7 +114,7 @@ export const handler = async (event) => {
|
|
|
114
114
|
["REVENTLESS_CORE_STACK", process.env.STACK_ORG + "/" + process.env.STACK_PROJECT + "/" + process.env.STACK_STACK],
|
|
115
115
|
["RESTORE_DATE_TIME", event.restoreDateTime],
|
|
116
116
|
]);
|
|
117
|
-
console.log("clone: requested by user " + event.meta.user
|
|
117
|
+
console.log("clone: requested by user " + event.meta.user);
|
|
118
118
|
await client.send(new RunTaskCommand({
|
|
119
119
|
taskDefinition: process.env.TASK_DEFINITION_ARN,
|
|
120
120
|
cluster: process.env.CLUSTER_ARN,
|
|
@@ -358,7 +358,6 @@ export function request(ctx) {
|
|
|
358
358
|
payload: {
|
|
359
359
|
restoreDateTime: ctx.args.restoreDateTime,
|
|
360
360
|
meta: {
|
|
361
|
-
ip: ctx.identity.sourceIp,
|
|
362
361
|
user: ctx.identity.username
|
|
363
362
|
}
|
|
364
363
|
}
|
|
@@ -67,7 +67,7 @@ export const handler = async (event) => {
|
|
|
67
67
|
["REVENTLESS_CORE_STACK", process.env.STACK_ORG + "/" + process.env.STACK_PROJECT + "/" + process.env.STACK_STACK],
|
|
68
68
|
["RESTORE_DATE_TIME", event.restoreDateTime],
|
|
69
69
|
]);
|
|
70
|
-
console.log("clone: requested by user " + event.meta.user
|
|
70
|
+
console.log("clone: requested by user " + event.meta.user);
|
|
71
71
|
await client.send(new RunTaskCommand({
|
|
72
72
|
taskDefinition: process.env.TASK_DEFINITION_ARN,
|
|
73
73
|
cluster: process.env.CLUSTER_ARN,
|
|
@@ -235,7 +235,6 @@ export function request(ctx) {
|
|
|
235
235
|
payload: {
|
|
236
236
|
restoreDateTime: ctx.args.restoreDateTime,
|
|
237
237
|
meta: {
|
|
238
|
-
ip: ctx.identity.sourceIp,
|
|
239
238
|
user: ctx.identity.username
|
|
240
239
|
}
|
|
241
240
|
}
|
|
@@ -346,8 +346,6 @@ module Make = (
|
|
|
346
346
|
extensions: [],
|
|
347
347
|
pluginExtensionPointCmdTopicUrl: Pulumi.Output.make(""),
|
|
348
348
|
publishToAggregates: Dict.make(),
|
|
349
|
-
readModelQueueUrls: Dict.make(),
|
|
350
|
-
readModelNamesForSourceName: Dict.make(),
|
|
351
349
|
}
|
|
352
350
|
}
|
|
353
351
|
|
|
@@ -427,18 +425,6 @@ module Make = (
|
|
|
427
425
|
envVars->Dict.set(envVarName, queueUrlOutput->Pulumi.Output.asInput)
|
|
428
426
|
})
|
|
429
427
|
|
|
430
|
-
// Same indirection for ReadModel EventCollector SQS URLs — only populated
|
|
431
|
-
// when user extensions enqueue directly into RMs (Extension_Operations
|
|
432
|
-
// publishToReadModels path). Empty for the admin Lambda and for the
|
|
433
|
-
// current Orders_Extension fixture (its RM is fed via the slice EventLog →
|
|
434
|
-
// RM EventColl subscription chain instead).
|
|
435
|
-
context.readModelQueueUrls
|
|
436
|
-
->Dict.toArray
|
|
437
|
-
->Array.forEach(((_rmName, queueUrlOutput)) => {
|
|
438
|
-
let envVarName = `PRM_${_rmName}_QUEUE_URL`
|
|
439
|
-
envVars->Dict.set(envVarName, queueUrlOutput->Pulumi.Output.asInput)
|
|
440
|
-
})
|
|
441
|
-
|
|
442
428
|
// Outgoing arrays first — resolve EP eventTopicArns, then bundle everything
|
|
443
429
|
// into one Output.apply that builds the JSON.
|
|
444
430
|
let epEventTopicArnsOutput =
|
|
@@ -523,8 +509,7 @@ module Make = (
|
|
|
523
509
|
// User-declared extensions — one entry per merged extension group.
|
|
524
510
|
// Each entry carries enough metadata for the bundled handler to
|
|
525
511
|
// dynamic-import the spec / mapping modules and filter the top-level
|
|
526
|
-
// publishToAggregates
|
|
527
|
-
// this extension actually uses.
|
|
512
|
+
// publishToAggregates map down to the subset this extension actually uses.
|
|
528
513
|
// Sort by extension name so the serialized JSON is stable across deploys.
|
|
529
514
|
let extensionsArr =
|
|
530
515
|
context.extensions
|
|
@@ -540,43 +525,26 @@ module Make = (
|
|
|
540
525
|
"aggregateNames",
|
|
541
526
|
ext.aggregateNames->Array.map(JSON.Encode.string)->JSON.Encode.array,
|
|
542
527
|
)
|
|
543
|
-
entryDict->Dict.set(
|
|
544
|
-
"readModelNames",
|
|
545
|
-
ext.readModelNames->Array.map(JSON.Encode.string)->JSON.Encode.array,
|
|
546
|
-
)
|
|
547
528
|
JSON.Encode.object(entryDict)
|
|
548
529
|
})
|
|
549
530
|
dict->Dict.set("extensions", JSON.Encode.array(extensionsArr))
|
|
550
531
|
|
|
551
|
-
// publishToAggregates
|
|
552
|
-
//
|
|
553
|
-
//
|
|
554
|
-
//
|
|
555
|
-
//
|
|
556
|
-
//
|
|
557
|
-
//
|
|
558
|
-
|
|
559
|
-
let rmNamesForSourceDict = Dict.make()
|
|
560
|
-
context.readModelNamesForSourceName
|
|
561
|
-
->Dict.toArray
|
|
562
|
-
->Array.forEach(((sourceName, rmNames)) =>
|
|
563
|
-
rmNamesForSourceDict->Dict.set(
|
|
564
|
-
sourceName,
|
|
565
|
-
rmNames->Array.map(JSON.Encode.string)->JSON.Encode.array,
|
|
566
|
-
)
|
|
567
|
-
)
|
|
568
|
-
dict->Dict.set("readModelNamesForSourceName", JSON.Encode.object(rmNamesForSourceDict))
|
|
532
|
+
// publishToAggregates lives in queueUrls.json (see the asset bundle below).
|
|
533
|
+
// It is one entry per target, so it grows with the plugin, and together with
|
|
534
|
+
// the PTA_ vars it names it carried the catalog EventCollector past Lambda's
|
|
535
|
+
// 4KB environment ceiling. Only the map moves: it is built from
|
|
536
|
+
// Dict.keysToArray, so it holds no Pulumi Output and can be serialized here.
|
|
537
|
+
// The URLs themselves cannot follow — see the note on the aggregate walk
|
|
538
|
+
// further down.
|
|
569
539
|
|
|
570
540
|
let json = JSON.Encode.object(dict)->JSON.stringify
|
|
571
|
-
// The PTA_
|
|
572
|
-
//
|
|
573
|
-
//
|
|
574
|
-
let queueUrlKeys =
|
|
541
|
+
// The PTA_ vars are the rest of this Lambda's environment and the half
|
|
542
|
+
// that grows with the plugin. Their names are known here; their values
|
|
543
|
+
// are still Outputs, so the budget check estimates those.
|
|
544
|
+
let queueUrlKeys =
|
|
575
545
|
context.publishToAggregates
|
|
576
546
|
->Dict.keysToArray
|
|
577
|
-
->Array.map(n => `PTA_${n}_QUEUE_URL`)
|
|
578
|
-
context.readModelQueueUrls->Dict.keysToArray->Array.map(n => `PRM_${n}_QUEUE_URL`),
|
|
579
|
-
)
|
|
547
|
+
->Array.map(n => `PTA_${n}_QUEUE_URL`)
|
|
580
548
|
Util_LambdaEnvBudget.check(
|
|
581
549
|
~lambdaName=name,
|
|
582
550
|
~handlerConfigJson=json,
|
|
@@ -660,10 +628,6 @@ module Make = (
|
|
|
660
628
|
"publishToAggregates",
|
|
661
629
|
nameMap(context.publishToAggregates->Dict.keysToArray, "PTA"),
|
|
662
630
|
)
|
|
663
|
-
queueUrlsDict->Dict.set(
|
|
664
|
-
"readModelQueueUrls",
|
|
665
|
-
nameMap(context.readModelQueueUrls->Dict.keysToArray, "PRM"),
|
|
666
|
-
)
|
|
667
631
|
extraStringAssets->Dict.set(
|
|
668
632
|
"queueUrls.json",
|
|
669
633
|
JSON.Encode.object(queueUrlsDict)->JSON.stringify,
|
|
@@ -268,9 +268,7 @@ function Make(EventCollectorChannel) {
|
|
|
268
268
|
}],
|
|
269
269
|
extensions: [],
|
|
270
270
|
pluginExtensionPointCmdTopicUrl: Pulumi.output(""),
|
|
271
|
-
publishToAggregates: {}
|
|
272
|
-
readModelQueueUrls: {},
|
|
273
|
-
readModelNamesForSourceName: {}
|
|
271
|
+
publishToAggregates: {}
|
|
274
272
|
};
|
|
275
273
|
};
|
|
276
274
|
let forPluginEventCollector = (param, eventTopics, resources, memorySizeOpt, timeoutOpt, eventCollector) => {
|
|
@@ -303,10 +301,6 @@ function Make(EventCollectorChannel) {
|
|
|
303
301
|
let envVarName = `PTA_` + param[0] + `_QUEUE_URL`;
|
|
304
302
|
envVars[envVarName] = param[1];
|
|
305
303
|
});
|
|
306
|
-
Object.entries(context.readModelQueueUrls).forEach(param => {
|
|
307
|
-
let envVarName = `PRM_` + param[0] + `_QUEUE_URL`;
|
|
308
|
-
envVars[envVarName] = param[1];
|
|
309
|
-
});
|
|
310
304
|
let epEventTopicArnsOutput = Pulumi.all(context.extensionPoints.map(ep => ep.eventTopicArn));
|
|
311
305
|
let handlerConfigJson = Pulumi.all([
|
|
312
306
|
queue.id,
|
|
@@ -371,17 +365,11 @@ function Make(EventCollectorChannel) {
|
|
|
371
365
|
entryDict["delegateModule"] = ext.delegateModule;
|
|
372
366
|
entryDict["extensionPointName"] = ext.extensionPointName;
|
|
373
367
|
entryDict["aggregateNames"] = ext.aggregateNames.map(prim => prim);
|
|
374
|
-
entryDict["readModelNames"] = ext.readModelNames.map(prim => prim);
|
|
375
368
|
return entryDict;
|
|
376
369
|
});
|
|
377
370
|
dict["extensions"] = extensionsArr;
|
|
378
|
-
let rmNamesForSourceDict = {};
|
|
379
|
-
Object.entries(context.readModelNamesForSourceName).forEach(param => {
|
|
380
|
-
rmNamesForSourceDict[param[0]] = param[1].map(prim => prim);
|
|
381
|
-
});
|
|
382
|
-
dict["readModelNamesForSourceName"] = rmNamesForSourceDict;
|
|
383
371
|
let json = JSON.stringify(dict);
|
|
384
|
-
let queueUrlKeys = Object.keys(context.publishToAggregates).map(n => `PTA_` + n + `_QUEUE_URL`)
|
|
372
|
+
let queueUrlKeys = Object.keys(context.publishToAggregates).map(n => `PTA_` + n + `_QUEUE_URL`);
|
|
385
373
|
Util_LambdaEnvBudget$ReventlessAws.check(name, json, queueUrlKeys);
|
|
386
374
|
return json;
|
|
387
375
|
});
|
|
@@ -428,7 +416,6 @@ function Make(EventCollectorChannel) {
|
|
|
428
416
|
};
|
|
429
417
|
let queueUrlsDict = {};
|
|
430
418
|
queueUrlsDict["publishToAggregates"] = nameMap(Object.keys(context.publishToAggregates), "PTA");
|
|
431
|
-
queueUrlsDict["readModelQueueUrls"] = nameMap(Object.keys(context.readModelQueueUrls), "PRM");
|
|
432
419
|
extraStringAssets["queueUrls.json"] = JSON.stringify(queueUrlsDict);
|
|
433
420
|
extraStringAssets["uiFragments.json"] = param[1];
|
|
434
421
|
return Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/EventCollectorEntryPoint.mjs", packageDirs, extraStringAssets, undefined);
|
|
@@ -17,7 +17,7 @@ function tag(key, value) {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
function testMeta() {
|
|
20
|
-
return Message$ReventlessCore.generateMeta("test", undefined, undefined, undefined, undefined, undefined, undefined
|
|
20
|
+
return Message$ReventlessCore.generateMeta("test", undefined, undefined, undefined, undefined, undefined, undefined);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
let table = {
|
|
@@ -32,9 +32,10 @@ describe("EventCollectorEntryPoint_Ops.projectPluginRow", () => {
|
|
|
32
32
|
expect(p.eventCollector)->toBe("") // defaulted when absent
|
|
33
33
|
expect(p.extensions->Array.length)->toBe(0)
|
|
34
34
|
expect(p.extensionPoints->Array.length)->toBe(0)
|
|
35
|
-
expect(p.dcbEventLog->Option.isNone)->toBe(true)
|
|
36
35
|
})
|
|
37
36
|
|
|
37
|
+
// A row written before `dcbSources` left the extension definition still
|
|
38
|
+
// carries the key; the projection reads past it.
|
|
38
39
|
testSync("decodes extensions, dropping entries without extensionPointName", () => {
|
|
39
40
|
let row = obj([
|
|
40
41
|
("id", str("Ordering@2")),
|
|
@@ -46,7 +47,7 @@ describe("EventCollectorEntryPoint_Ops.projectPluginRow", () => {
|
|
|
46
47
|
obj([
|
|
47
48
|
("name", str("OrdersExt")),
|
|
48
49
|
("extensionPointName", str("Catalog.Products")),
|
|
49
|
-
("dcbSources", JSON.Encode.array([
|
|
50
|
+
("dcbSources", JSON.Encode.array([])),
|
|
50
51
|
]),
|
|
51
52
|
obj([("name", str("Malformed"))]), // no extensionPointName -> dropped
|
|
52
53
|
]),
|
|
@@ -58,11 +59,9 @@ describe("EventCollectorEntryPoint_Ops.projectPluginRow", () => {
|
|
|
58
59
|
let e = p.extensions->Array.getUnsafe(0)
|
|
59
60
|
expect(e.name)->toBe("OrdersExt")
|
|
60
61
|
expect(e.extensionPointName)->toBe("Catalog.Products")
|
|
61
|
-
// Non-string dcbSources elements are filtered out.
|
|
62
|
-
expect(e.dcbSources)->toEqual(["prod"])
|
|
63
62
|
})
|
|
64
63
|
|
|
65
|
-
testSync("decodes extensionPoints
|
|
64
|
+
testSync("decodes extensionPoints; drops EPs without name/eventTopic", () => {
|
|
66
65
|
let row = obj([
|
|
67
66
|
("id", str("Catalog@1")),
|
|
68
67
|
("status", str("Connected")),
|
|
@@ -73,6 +72,7 @@ describe("EventCollectorEntryPoint_Ops.projectPluginRow", () => {
|
|
|
73
72
|
obj([("name", str("NoTopic"))]), // no eventTopic -> dropped
|
|
74
73
|
]),
|
|
75
74
|
),
|
|
75
|
+
// Still on older rows; the subscription manager no longer reads it.
|
|
76
76
|
("dcbEventLog", obj([("name", str("catalog-dcb")), ("eventTopicArn", str("dcb-arn"))])),
|
|
77
77
|
])
|
|
78
78
|
let p = project(row)->Option.getOrThrow
|
|
@@ -81,18 +81,6 @@ describe("EventCollectorEntryPoint_Ops.projectPluginRow", () => {
|
|
|
81
81
|
expect(ep.name)->toBe("Catalog.Products")
|
|
82
82
|
expect(ep.commandTopic)->toBe("") // defaulted
|
|
83
83
|
expect(ep.eventTopic)->toBe("t-arn")
|
|
84
|
-
let dcb = p.dcbEventLog->Option.getOrThrow
|
|
85
|
-
expect(dcb.name)->toBe("catalog-dcb")
|
|
86
|
-
expect(dcb.eventTopicArn)->toBe("dcb-arn")
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
testSync("drops a dcbEventLog missing eventTopicArn", () => {
|
|
90
|
-
let row = obj([
|
|
91
|
-
("id", str("Catalog@1")),
|
|
92
|
-
("status", str("Connected")),
|
|
93
|
-
("dcbEventLog", obj([("name", str("catalog-dcb"))])),
|
|
94
|
-
])
|
|
95
|
-
expect((project(row)->Option.getOrThrow).dcbEventLog->Option.isNone)->toBe(true)
|
|
96
84
|
})
|
|
97
85
|
})
|
|
98
86
|
|
|
@@ -146,7 +134,7 @@ describe("EventCollectorEntryPoint_Ops.parseHandlerConfig", () => {
|
|
|
146
134
|
let c = EventCollectorEntryPoint_Ops.parseHandlerConfig(minimalConfig([]))
|
|
147
135
|
expect(c.queueUrl)->toBe("q-url")
|
|
148
136
|
expect(c.connectExtension->Option.isNone)->toBe(true) // absent → None
|
|
149
|
-
expect(c.
|
|
137
|
+
expect(c.extensions->Array.length)->toBe(0)
|
|
150
138
|
})
|
|
151
139
|
|
|
152
140
|
testSync("connectExtension: JSON null (admin) maps to None; a record decodes", () => {
|
|
@@ -177,7 +165,7 @@ describe("EventCollectorEntryPoint_Ops.parseHandlerConfig", () => {
|
|
|
177
165
|
(
|
|
178
166
|
"extensions",
|
|
179
167
|
JSON.Encode.array([
|
|
180
|
-
// no name, no module specifiers, no aggregate
|
|
168
|
+
// no name, no module specifiers, no aggregate list
|
|
181
169
|
obj([("extensionPointName", str("Ordering.Orders"))]),
|
|
182
170
|
]),
|
|
183
171
|
),
|
|
@@ -187,10 +175,9 @@ describe("EventCollectorEntryPoint_Ops.parseHandlerConfig", () => {
|
|
|
187
175
|
expect(ext.name)->toBe("Ordering.Orders") // name falls back to the EP name
|
|
188
176
|
expect(ext.specModule)->toBe("")
|
|
189
177
|
expect(ext.aggregateNames)->toEqual([])
|
|
190
|
-
expect(ext.readModelNames)->toEqual([])
|
|
191
178
|
})
|
|
192
179
|
|
|
193
|
-
testSync("decodes extensionPoints entries
|
|
180
|
+
testSync("decodes extensionPoints entries", () => {
|
|
194
181
|
let c = EventCollectorEntryPoint_Ops.parseHandlerConfig(
|
|
195
182
|
minimalConfig([
|
|
196
183
|
(
|
|
@@ -204,30 +191,38 @@ describe("EventCollectorEntryPoint_Ops.parseHandlerConfig", () => {
|
|
|
204
191
|
]),
|
|
205
192
|
]),
|
|
206
193
|
),
|
|
207
|
-
(
|
|
208
|
-
"readModelNamesForSourceName",
|
|
209
|
-
obj([("Ordering.Orders", JSON.Encode.array([str("ProductDemands")]))]),
|
|
210
|
-
),
|
|
211
194
|
]),
|
|
212
195
|
)
|
|
213
196
|
let ep = c.extensionPoints->Array.getUnsafe(0)
|
|
214
197
|
expect(ep.eventTopicArn)->toBe("arn:ep")
|
|
215
198
|
expect(ep.aggregateNames)->toEqual(["Plugin"])
|
|
216
|
-
expect(c.readModelNamesForSourceName->Dict.get("Ordering.Orders"))->toEqual(
|
|
217
|
-
Some(["ProductDemands"]),
|
|
218
|
-
)
|
|
219
199
|
})
|
|
220
200
|
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
|
|
201
|
+
// Keys HANDLER_CONFIG no longer carries — the target→env-var maps that left for
|
|
202
|
+
// queueUrls.json, and the read-model forwarding index with the per-extension
|
|
203
|
+
// `readModelNames` — must parse rather than trip the required-field check: that
|
|
204
|
+
// is what a Lambda running an older archive sends.
|
|
205
|
+
testSync("ignores keys an older HANDLER_CONFIG still carries", () => {
|
|
225
206
|
let c = EventCollectorEntryPoint_Ops.parseHandlerConfig(
|
|
226
207
|
minimalConfig([
|
|
227
208
|
("publishToAggregates", obj([("Plugin", str("PTA_Plugin_QUEUE_URL"))])),
|
|
228
209
|
("readModelQueueUrls", obj([("ProductDemands", str("PRM_ProductDemands_QUEUE_URL"))])),
|
|
210
|
+
(
|
|
211
|
+
"readModelNamesForSourceName",
|
|
212
|
+
obj([("Ordering.Orders", JSON.Encode.array([str("ProductDemands")]))]),
|
|
213
|
+
),
|
|
214
|
+
(
|
|
215
|
+
"extensions",
|
|
216
|
+
JSON.Encode.array([
|
|
217
|
+
obj([
|
|
218
|
+
("extensionPointName", str("Ordering.Orders")),
|
|
219
|
+
("readModelNames", JSON.Encode.array([str("ProductDemands")])),
|
|
220
|
+
]),
|
|
221
|
+
]),
|
|
222
|
+
),
|
|
229
223
|
]),
|
|
230
224
|
)
|
|
231
225
|
expect(c.queueUrl)->toBe("q-url")
|
|
226
|
+
expect((c.extensions->Array.getUnsafe(0)).extensionPointName)->toBe("Ordering.Orders")
|
|
232
227
|
})
|
|
233
228
|
})
|
|
@@ -43,7 +43,6 @@ globalThis.describe("EventCollectorEntryPoint_Ops.projectPluginRow", () => {
|
|
|
43
43
|
globalThis.expect(p.eventCollector).toBe("");
|
|
44
44
|
globalThis.expect(p.extensions.length).toBe(0);
|
|
45
45
|
globalThis.expect(p.extensionPoints.length).toBe(0);
|
|
46
|
-
globalThis.expect(Stdlib_Option.isNone(p.dcbEventLog)).toBe(true);
|
|
47
46
|
});
|
|
48
47
|
globalThis.test("decodes extensions, dropping entries without extensionPointName", () => {
|
|
49
48
|
let row = Object.fromEntries([
|
|
@@ -73,10 +72,7 @@ globalThis.describe("EventCollectorEntryPoint_Ops.projectPluginRow", () => {
|
|
|
73
72
|
],
|
|
74
73
|
[
|
|
75
74
|
"dcbSources",
|
|
76
|
-
[
|
|
77
|
-
"prod",
|
|
78
|
-
7
|
|
79
|
-
]
|
|
75
|
+
[]
|
|
80
76
|
]
|
|
81
77
|
]),
|
|
82
78
|
Object.fromEntries([[
|
|
@@ -92,9 +88,8 @@ globalThis.describe("EventCollectorEntryPoint_Ops.projectPluginRow", () => {
|
|
|
92
88
|
let e = p.extensions[0];
|
|
93
89
|
globalThis.expect(e.name).toBe("OrdersExt");
|
|
94
90
|
globalThis.expect(e.extensionPointName).toBe("Catalog.Products");
|
|
95
|
-
globalThis.expect(e.dcbSources).toEqual(["prod"]);
|
|
96
91
|
});
|
|
97
|
-
globalThis.test("decodes extensionPoints
|
|
92
|
+
globalThis.test("decodes extensionPoints; drops EPs without name/eventTopic", () => {
|
|
98
93
|
let row = Object.fromEntries([
|
|
99
94
|
[
|
|
100
95
|
"id",
|
|
@@ -143,29 +138,6 @@ globalThis.describe("EventCollectorEntryPoint_Ops.projectPluginRow", () => {
|
|
|
143
138
|
globalThis.expect(ep.name).toBe("Catalog.Products");
|
|
144
139
|
globalThis.expect(ep.commandTopic).toBe("");
|
|
145
140
|
globalThis.expect(ep.eventTopic).toBe("t-arn");
|
|
146
|
-
let dcb = Stdlib_Option.getOrThrow(p.dcbEventLog, undefined);
|
|
147
|
-
globalThis.expect(dcb.name).toBe("catalog-dcb");
|
|
148
|
-
globalThis.expect(dcb.eventTopicArn).toBe("dcb-arn");
|
|
149
|
-
});
|
|
150
|
-
globalThis.test("drops a dcbEventLog missing eventTopicArn", () => {
|
|
151
|
-
let row = Object.fromEntries([
|
|
152
|
-
[
|
|
153
|
-
"id",
|
|
154
|
-
"Catalog@1"
|
|
155
|
-
],
|
|
156
|
-
[
|
|
157
|
-
"status",
|
|
158
|
-
"Connected"
|
|
159
|
-
],
|
|
160
|
-
[
|
|
161
|
-
"dcbEventLog",
|
|
162
|
-
Object.fromEntries([[
|
|
163
|
-
"name",
|
|
164
|
-
"catalog-dcb"
|
|
165
|
-
]])
|
|
166
|
-
]
|
|
167
|
-
]);
|
|
168
|
-
globalThis.expect(Stdlib_Option.isNone(Stdlib_Option.getOrThrow(EventCollectorEntryPoint_Ops$ReventlessAws.projectPluginRow(row), undefined).dcbEventLog)).toBe(true);
|
|
169
141
|
});
|
|
170
142
|
});
|
|
171
143
|
|
|
@@ -234,7 +206,7 @@ globalThis.describe("EventCollectorEntryPoint_Ops.parseHandlerConfig", () => {
|
|
|
234
206
|
let c = EventCollectorEntryPoint_Ops$ReventlessAws.parseHandlerConfig(minimalConfig([]));
|
|
235
207
|
globalThis.expect(c.queueUrl).toBe("q-url");
|
|
236
208
|
globalThis.expect(Stdlib_Option.isNone(c.connectExtension)).toBe(true);
|
|
237
|
-
globalThis.expect(
|
|
209
|
+
globalThis.expect(c.extensions.length).toBe(0);
|
|
238
210
|
});
|
|
239
211
|
globalThis.test("connectExtension: JSON null (admin) maps to None; a record decodes", () => {
|
|
240
212
|
let asNull = EventCollectorEntryPoint_Ops$ReventlessAws.parseHandlerConfig(minimalConfig([[
|
|
@@ -274,11 +246,9 @@ globalThis.describe("EventCollectorEntryPoint_Ops.parseHandlerConfig", () => {
|
|
|
274
246
|
globalThis.expect(ext.name).toBe("Ordering.Orders");
|
|
275
247
|
globalThis.expect(ext.specModule).toBe("");
|
|
276
248
|
globalThis.expect(ext.aggregateNames).toEqual([]);
|
|
277
|
-
globalThis.expect(ext.readModelNames).toEqual([]);
|
|
278
249
|
});
|
|
279
|
-
globalThis.test("decodes extensionPoints entries
|
|
280
|
-
let c = EventCollectorEntryPoint_Ops$ReventlessAws.parseHandlerConfig(minimalConfig([
|
|
281
|
-
[
|
|
250
|
+
globalThis.test("decodes extensionPoints entries", () => {
|
|
251
|
+
let c = EventCollectorEntryPoint_Ops$ReventlessAws.parseHandlerConfig(minimalConfig([[
|
|
282
252
|
"extensionPoints",
|
|
283
253
|
[Object.fromEntries([
|
|
284
254
|
[
|
|
@@ -298,21 +268,12 @@ globalThis.describe("EventCollectorEntryPoint_Ops.parseHandlerConfig", () => {
|
|
|
298
268
|
["Plugin"]
|
|
299
269
|
]
|
|
300
270
|
])]
|
|
301
|
-
]
|
|
302
|
-
[
|
|
303
|
-
"readModelNamesForSourceName",
|
|
304
|
-
Object.fromEntries([[
|
|
305
|
-
"Ordering.Orders",
|
|
306
|
-
["ProductDemands"]
|
|
307
|
-
]])
|
|
308
|
-
]
|
|
309
|
-
]));
|
|
271
|
+
]]));
|
|
310
272
|
let ep = c.extensionPoints[0];
|
|
311
273
|
globalThis.expect(ep.eventTopicArn).toBe("arn:ep");
|
|
312
274
|
globalThis.expect(ep.aggregateNames).toEqual(["Plugin"]);
|
|
313
|
-
globalThis.expect(c.readModelNamesForSourceName["Ordering.Orders"]).toEqual(["ProductDemands"]);
|
|
314
275
|
});
|
|
315
|
-
globalThis.test("ignores
|
|
276
|
+
globalThis.test("ignores keys an older HANDLER_CONFIG still carries", () => {
|
|
316
277
|
let c = EventCollectorEntryPoint_Ops$ReventlessAws.parseHandlerConfig(minimalConfig([
|
|
317
278
|
[
|
|
318
279
|
"publishToAggregates",
|
|
@@ -327,9 +288,30 @@ globalThis.describe("EventCollectorEntryPoint_Ops.parseHandlerConfig", () => {
|
|
|
327
288
|
"ProductDemands",
|
|
328
289
|
"PRM_ProductDemands_QUEUE_URL"
|
|
329
290
|
]])
|
|
291
|
+
],
|
|
292
|
+
[
|
|
293
|
+
"readModelNamesForSourceName",
|
|
294
|
+
Object.fromEntries([[
|
|
295
|
+
"Ordering.Orders",
|
|
296
|
+
["ProductDemands"]
|
|
297
|
+
]])
|
|
298
|
+
],
|
|
299
|
+
[
|
|
300
|
+
"extensions",
|
|
301
|
+
[Object.fromEntries([
|
|
302
|
+
[
|
|
303
|
+
"extensionPointName",
|
|
304
|
+
"Ordering.Orders"
|
|
305
|
+
],
|
|
306
|
+
[
|
|
307
|
+
"readModelNames",
|
|
308
|
+
["ProductDemands"]
|
|
309
|
+
]
|
|
310
|
+
])]
|
|
330
311
|
]
|
|
331
312
|
]));
|
|
332
313
|
globalThis.expect(c.queueUrl).toBe("q-url");
|
|
314
|
+
globalThis.expect(c.extensions[0].extensionPointName).toBe("Ordering.Orders");
|
|
333
315
|
});
|
|
334
316
|
});
|
|
335
317
|
|
|
@@ -78,7 +78,7 @@ describe("EventMapperEntryPoint_Ops.makeStreamHandler", () => {
|
|
|
78
78
|
}
|
|
79
79
|
let command: ReventlessCore.Message.commandJson = {
|
|
80
80
|
id: "prd-1",
|
|
81
|
-
meta: ReventlessCore.Message.generateMeta(~service="Product", ~
|
|
81
|
+
meta: ReventlessCore.Message.generateMeta(~service="Product", ~user="test"),
|
|
82
82
|
commandJson: JSON.Encode.string("Add"),
|
|
83
83
|
}
|
|
84
84
|
let commonEventsHandler: EventMapperEntryPoint_Ops.commonEventsHandler = async jsons => {
|
|
@@ -70,7 +70,7 @@ globalThis.describe("EventMapperEntryPoint_Ops.makeStreamHandler", () => {
|
|
|
70
70
|
publishJsons: ops_publishJsons,
|
|
71
71
|
queryEngine: ops_queryEngine
|
|
72
72
|
};
|
|
73
|
-
let command_meta = Message$ReventlessCore.generateMeta("Product", "
|
|
73
|
+
let command_meta = Message$ReventlessCore.generateMeta("Product", "test", undefined, undefined, undefined, undefined, undefined);
|
|
74
74
|
let command = {
|
|
75
75
|
id: "prd-1",
|
|
76
76
|
meta: command_meta,
|
|
@@ -22,7 +22,7 @@ function stored(eventType, tags, data) {
|
|
|
22
22
|
eventType: eventType,
|
|
23
23
|
data: data,
|
|
24
24
|
tags: tags,
|
|
25
|
-
meta: Message$ReventlessCore.generateMeta("relay-it", undefined, undefined, undefined, undefined, undefined, undefined
|
|
25
|
+
meta: Message$ReventlessCore.generateMeta("relay-it", undefined, undefined, undefined, undefined, undefined, undefined)
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -124,7 +124,7 @@ if (url !== undefined) {
|
|
|
124
124
|
"data",
|
|
125
125
|
data
|
|
126
126
|
]
|
|
127
|
-
].concat(Message$ReventlessCore.decomposeMeta(Message$ReventlessCore.generateMeta("relay-it", undefined, undefined, undefined, undefined, undefined, undefined
|
|
127
|
+
].concat(Message$ReventlessCore.decomposeMeta(Message$ReventlessCore.generateMeta("relay-it", undefined, undefined, undefined, undefined, undefined, undefined))));
|
|
128
128
|
await match[1].append(0, "agg-1", [
|
|
129
129
|
flatItem("agg-1", 0, "NameUpdated", Object.fromEntries([[
|
|
130
130
|
"n",
|
|
@@ -155,7 +155,7 @@ if (url !== undefined) {
|
|
|
155
155
|
let mkOps = name => EventLogStorage_Postgres$ReventlessPostgres.makeStorage(pool, name, undefined, undefined)[1];
|
|
156
156
|
let opsB = mkOps("ClassicRelayIsoBEventLog");
|
|
157
157
|
let opsA = mkOps("ClassicRelayIsoAEventLog");
|
|
158
|
-
let meta = () => Message$ReventlessCore.decomposeMeta(Message$ReventlessCore.generateMeta("relay-it", undefined, undefined, undefined, undefined, undefined, undefined
|
|
158
|
+
let meta = () => Message$ReventlessCore.decomposeMeta(Message$ReventlessCore.generateMeta("relay-it", undefined, undefined, undefined, undefined, undefined, undefined));
|
|
159
159
|
let item = (id, eventType) => Object.fromEntries([
|
|
160
160
|
[
|
|
161
161
|
"id",
|
|
@@ -132,7 +132,7 @@ function flatItem(id, seq, service, event) {
|
|
|
132
132
|
"data",
|
|
133
133
|
match[1]
|
|
134
134
|
]
|
|
135
|
-
].concat(Message$ReventlessCore.decomposeMeta(Message$ReventlessCore.generateMeta(service, undefined, undefined, undefined, undefined, undefined, undefined
|
|
135
|
+
].concat(Message$ReventlessCore.decomposeMeta(Message$ReventlessCore.generateMeta(service, undefined, undefined, undefined, undefined, undefined, undefined))));
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
function asSqsEvent(bodies) {
|
|
@@ -88,3 +88,27 @@ describe("Upload_Presign_S3_Ops.decideRelease", () => {
|
|
|
88
88
|
)
|
|
89
89
|
)
|
|
90
90
|
})
|
|
91
|
+
|
|
92
|
+
describe("Upload_Presign_S3_Ops.presignKey", () => {
|
|
93
|
+
testSync("is {prefix}/{sub}/{uuid}.{ext}, with nothing the uploader typed", () =>
|
|
94
|
+
expect(
|
|
95
|
+
Ops.presignKey(~servedPrefix=prefix, ~sub, ~uuid="0b1c", ~contentType=Some("image/png")),
|
|
96
|
+
)->toEqual("uploads/user-1/0b1c.png")
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
testSync("an undeclared content type is stored as bin", () =>
|
|
100
|
+
expect(Ops.presignKey(~servedPrefix=prefix, ~sub, ~uuid="0b1c", ~contentType=None))->toEqual(
|
|
101
|
+
"uploads/user-1/0b1c.bin",
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
testSync("a minted key passes the release rule's scope check for its own caller", () =>
|
|
106
|
+
expect(
|
|
107
|
+
Ops.scopeCheck(
|
|
108
|
+
~key=Ops.presignKey(~servedPrefix=prefix, ~sub, ~uuid="0b1c", ~contentType=None),
|
|
109
|
+
~sub,
|
|
110
|
+
~servedPrefix=prefix,
|
|
111
|
+
),
|
|
112
|
+
)->toEqual(Ok())
|
|
113
|
+
)
|
|
114
|
+
})
|
|
@@ -89,6 +89,21 @@ globalThis.describe("Upload_Presign_S3_Ops.decideRelease", () => {
|
|
|
89
89
|
});
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
+
globalThis.describe("Upload_Presign_S3_Ops.presignKey", () => {
|
|
93
|
+
globalThis.test("is {prefix}/{sub}/{uuid}.{ext}, with nothing the uploader typed", () => {
|
|
94
|
+
globalThis.expect(Upload_Presign_S3_Ops$ReventlessAws.presignKey(prefix, sub, "0b1c", "image/png")).toEqual("uploads/user-1/0b1c.png");
|
|
95
|
+
});
|
|
96
|
+
globalThis.test("an undeclared content type is stored as bin", () => {
|
|
97
|
+
globalThis.expect(Upload_Presign_S3_Ops$ReventlessAws.presignKey(prefix, sub, "0b1c", undefined)).toEqual("uploads/user-1/0b1c.bin");
|
|
98
|
+
});
|
|
99
|
+
globalThis.test("a minted key passes the release rule's scope check for its own caller", () => {
|
|
100
|
+
globalThis.expect(Upload_Presign_S3_Ops$ReventlessAws.scopeCheck(Upload_Presign_S3_Ops$ReventlessAws.presignKey(prefix, sub, "0b1c", undefined), sub, prefix)).toEqual({
|
|
101
|
+
TAG: "Ok",
|
|
102
|
+
_0: undefined
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
92
107
|
let Ops;
|
|
93
108
|
|
|
94
109
|
let nowMs = 1000000.0;
|