@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.
Files changed (29) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/package.json +9 -9
  3. package/src/Platform.res.mjs +2 -2
  4. package/src/adapter/Mcp/MCP_Lambda.res +1 -1
  5. package/src/adapter/Mcp/MCP_Lambda.res.mjs +0 -1
  6. package/src/adapter/Runtime/EventCollectorEntryPoint.mjs +6 -14
  7. package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res +13 -123
  8. package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res.mjs +8 -74
  9. package/src/adapter/Runtime/HeartbeatEntryPoint.res +0 -1
  10. package/src/adapter/Runtime/HeartbeatEntryPoint.res.mjs +1 -1
  11. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +5 -5
  12. package/src/adapter/Upload/Upload_Presign_S3_Ops.res +13 -3
  13. package/src/adapter/Upload/Upload_Presign_S3_Ops.res.mjs +7 -2
  14. package/src/plugin/cloner/ClonerRunner_Fargate.res +1 -2
  15. package/src/plugin/cloner/ClonerRunner_Fargate.res.mjs +1 -2
  16. package/src/plugin/runtime/PluginRuntime_Builder.res +13 -49
  17. package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +2 -15
  18. package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res.mjs +1 -1
  19. package/tests/EventCollectorEntryPoint_OpsTest.res +27 -32
  20. package/tests/EventCollectorEntryPoint_OpsTest.res.mjs +28 -46
  21. package/tests/EventMapperEntryPoint_OpsTest.res +1 -1
  22. package/tests/EventMapperEntryPoint_OpsTest.res.mjs +1 -1
  23. package/tests/PgChangeFeedRelay_IntegrationTest.res.mjs +3 -3
  24. package/tests/PgPipeline_IntegrationTest.res.mjs +1 -1
  25. package/tests/Upload_ReleaseRuleTest.res +24 -0
  26. package/tests/Upload_ReleaseRuleTest.res.mjs +15 -0
  27. package/tests/integration/DcbEventLogStorage_DynamoDb_IntegrationTest.res.mjs +1 -1
  28. package/src/components/ForeignReadModel_Builder.res +0 -19
  29. 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}/{fileName}`, presign a PUT to the
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
- let key = `${servedPrefix}/${sub}/${NodeCrypto.randomUUID()}/${fileName}`
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 fileName = Stdlib_Option.getOr(args.fileName, "upload");
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 + " from ip " + event.meta.ip);
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 + " from ip " + event.meta.ip);
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 + readModelQueueUrls maps down to the subset
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 / readModelQueueUrls live in queueUrls.json (see the
552
- // asset bundle below). Both are one entry per target, so they grow with the
553
- // plugin, and together with the PTA_/PRM_ vars they name they carried the
554
- // catalog EventCollector past Lambda's 4KB environment ceiling. Only the
555
- // maps move: they are built from Dict.keysToArray, so they hold no Pulumi
556
- // Output and can be serialized here. The URLs themselves cannot follow —
557
- // see the note on the aggregate walk further down.
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_/PRM_ vars are the rest of this Lambda's environment and the
572
- // half that grows with the plugin. Their names are known here; their
573
- // values are still Outputs, so the budget check estimates those.
574
- let queueUrlKeys = Array.concat(
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`).concat(Object.keys(context.readModelQueueUrls).map(n => `PRM_` + 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, 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([str("prod"), JSON.Encode.int(7)])),
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 and dcbEventLog; drops EPs without name/eventTopic", () => {
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.readModelNamesForSourceName->Dict.keysToArray->Array.length)->toBe(0)
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/readModel lists
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 and the env-var maps", () => {
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
- // The target→env-var maps left HANDLER_CONFIG for queueUrls.json, so a config
222
- // still carrying them must parse rather than trip the required-field check —
223
- // that is what a Lambda running an older archive sends.
224
- testSync("ignores publishToAggregates left over in HANDLER_CONFIG", () => {
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 and dcbEventLog; drops EPs without name/eventTopic", () => {
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(Object.keys(c.readModelNamesForSourceName).length).toBe(0);
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 and the env-var maps", () => {
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 publishToAggregates left over in HANDLER_CONFIG", () => {
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", ~ip="", ~user="test"),
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", "", "test", undefined, undefined, undefined, undefined, undefined);
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, 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, 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, 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, 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;