@reventlessdev/reventless-aws 3.0.0-alpha.305 → 3.0.0-alpha.307

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 (67) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/package.json +8 -8
  3. package/src/Platform.res +1 -1
  4. package/src/adapter/Api/AppSync_Resolver_Native.res +1 -1
  5. package/src/adapter/Api/AppSync_Resolver_Retrying.res +2 -2
  6. package/src/adapter/Auth/Auth_ActiveRolePoolAttachment.res +69 -11
  7. package/src/adapter/Auth/Auth_ActiveRolePoolAttachment.res.mjs +19 -1
  8. package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res +2 -2
  9. package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres.res +1 -1
  10. package/src/adapter/EventLog/EventLogStorage_DynamoDb_Runtime.res +1 -1
  11. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +1 -1
  12. package/src/adapter/Postgres/PgProjectionFeed.res +1 -1
  13. package/src/adapter/QueryDb/PgQueryResolver_Lambda.res +64 -0
  14. package/src/adapter/QueryDb/PgQueryResolver_Lambda.res.mjs +54 -2
  15. package/src/adapter/QueryDb/QueryDbBackend.res +1 -1
  16. package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res +138 -46
  17. package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs +25 -21
  18. package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res +19 -11
  19. package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res.mjs +11 -10
  20. package/src/adapter/QueryEngine/QueryEngine_DynamoDb.res +1 -1
  21. package/src/adapter/Runtime/AggregateEntryPoint.mjs +1 -1
  22. package/src/adapter/Runtime/AggregateEntryPoint_Ops.res +1 -1
  23. package/src/adapter/Runtime/AutomationSliceEntryPoint.mjs +1 -1
  24. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +1 -1
  25. package/src/adapter/Runtime/CounterEntryPoint.mjs +1 -1
  26. package/src/adapter/Runtime/CounterEntryPoint_Ops.res +1 -1
  27. package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +44 -6
  28. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +1 -1
  29. package/src/adapter/Runtime/EventCollectorEntryPoint.mjs +1 -1
  30. package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res +1 -1
  31. package/src/adapter/Runtime/EventMapperEntryPoint.mjs +1 -1
  32. package/src/adapter/Runtime/EventMapperEntryPoint_Ops.res +1 -1
  33. package/src/adapter/Runtime/ExtensionPointEntryPoint.mjs +1 -1
  34. package/src/adapter/Runtime/ExtensionPointEntryPoint_Ops.res +1 -1
  35. package/src/adapter/Runtime/PgQueryResolverEntryPoint.mjs +1 -1
  36. package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res +7 -1
  37. package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res.mjs +2 -1
  38. package/src/adapter/Runtime/PluginExtensionPointEntryPoint.res +1 -1
  39. package/src/adapter/Runtime/ProjectionEntryPoint_Ops.res +1 -1
  40. package/src/adapter/Runtime/QueryDbEntryPoint_Ops.res +1 -1
  41. package/src/adapter/Runtime/ReadModelEntryPoint.mjs +1 -1
  42. package/src/adapter/Runtime/ReadModelEntryPoint_Ops.res +1 -1
  43. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +1 -1
  44. package/src/adapter/Runtime/SideEffectEntryPoint.mjs +1 -1
  45. package/src/adapter/Runtime/SideEffectEntryPoint_Ops.res +1 -1
  46. package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +1 -1
  47. package/src/adapter/Runtime/StateViewSliceEntryPoint_Ops.res +1 -1
  48. package/src/adapter/Runtime/StreamRoutedEntryPoint_Ops.res +1 -1
  49. package/src/adapter/Runtime/TaskBucketEntryPoint.mjs +1 -1
  50. package/src/adapter/Runtime/TaskBucketEntryPoint_Ops.res +1 -1
  51. package/src/util/Util_Bundle.res +1 -1
  52. package/src/util/Util_DynamoDbStream_Runtime.res +1 -1
  53. package/tests/AWS_TagsTest.res +2 -2
  54. package/tests/AppSync_RetirementNarrowingTest.res +272 -0
  55. package/tests/AppSync_RetirementNarrowingTest.res.mjs +193 -0
  56. package/tests/Auth_ActiveRolePoolAttachmentTest.res +140 -1
  57. package/tests/Auth_ActiveRolePoolAttachmentTest.res.mjs +117 -2
  58. package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res +2 -2
  59. package/tests/EventLogStorage_DynamoDb_RuntimeTest.res +1 -1
  60. package/tests/PgQueryResolver_LambdaTest.res +2 -0
  61. package/tests/PgQueryResolver_LambdaTest.res.mjs +34 -30
  62. package/tests/QueryDbResolvers_AppSyncTest.res +2 -2
  63. package/tests/Util_BundleEsmLoaderTest.res +1 -1
  64. package/tests/Util_BundleRuntimeExtensionTest.res +1 -1
  65. package/tests/integration/AggregateEntryPoint_IntegrationTest.res +1 -1
  66. package/tests/integration/DcbCommandTopicEntryPoint_IntegrationTest.res +1 -1
  67. package/tests/integration/DcbEventLogStorage_DynamoDb_IntegrationTest.res +2 -2
package/CHANGELOG.md CHANGED
@@ -3,6 +3,27 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 3.0.0-alpha.307 (2026-08-18)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **api:** make the by-index door answer, and let an elevated caller widen it ([0fe0c6f](https://github.com/ReventlessDev/reventless-core/commit/0fe0c6f8dec6228ecaba39577e28d780b4f79c83))
11
+ * **aws:** apply [@owner](https://github.com/owner) on the DynamoDB by-ids and by-index doors ([a6b5afc](https://github.com/ReventlessDev/reventless-core/commit/a6b5afc2923bfa4d3e0b2990367f67e8ffdd8877))
12
+ * **aws:** narrow retirement on every DynamoDB door, not only the list ([d6a799b](https://github.com/ReventlessDev/reventless-core/commit/d6a799b287b28e9c1f75e193adbf3f6328a6bf2d))
13
+ ### Features
14
+
15
+ * **core:** let a reference name a retired row, and let an elevated caller open one ([9e2623a](https://github.com/ReventlessDev/reventless-core/commit/9e2623a4b22487561607fcc0ca19d51726069ee4))
16
+
17
+
18
+ # 3.0.0-alpha.306 (2026-08-16)
19
+
20
+ ### Bug Fixes
21
+
22
+ * **aws:** attach the pre-token trigger through both fields Cognito keeps ([6d6e2e9](https://github.com/ReventlessDev/reventless-core/commit/6d6e2e90e1d6cb3defabaab8ae7f3c9d1955a5df))
23
+ * **aws:** scope by-key reads to the owner, not only lists ([8232fd4](https://github.com/ReventlessDev/reventless-core/commit/8232fd4c09c1098c7265e4a17882ee44884f3bec))
24
+ * **core:** stamp the owner from each DCB slice's own command schema ([6edbdf4](https://github.com/ReventlessDev/reventless-core/commit/6edbdf4680f2535597c3b560215aa514ceed27eb))
25
+
26
+
6
27
  # 3.0.0-alpha.305 (2026-08-16)
7
28
 
8
29
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.305",
3
+ "version": "3.0.0-alpha.307",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "dependencies": {
@@ -12,18 +12,18 @@
12
12
  "@aws-sdk/s3-request-presigner": "3.970.0",
13
13
  "sury": "11.0.0-alpha.4",
14
14
  "uuid": "^13.0.0",
15
- "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.11",
15
+ "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.12",
16
16
  "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
17
17
  "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
18
- "@reventlessdev/rescript-node": "2.0.0-alpha.7",
19
- "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.77",
18
+ "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.79",
19
+ "@reventlessdev/rescript-node": "2.0.0-alpha.8",
20
20
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
21
21
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
22
- "@reventlessdev/reventless-core": "3.0.0-alpha.237",
23
- "@reventlessdev/reventless-infra": "3.0.0-alpha.143",
22
+ "@reventlessdev/reventless-core": "3.0.0-alpha.239",
24
23
  "@reventlessdev/reventless-interop": "3.0.0-alpha.31",
25
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.101",
26
- "@reventlessdev/reventless-spec": "3.0.0-alpha.115"
24
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.103",
25
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.145",
26
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.117"
27
27
  },
28
28
  "devDependencies": {
29
29
  "rescript": "12.3.0",
package/src/Platform.res CHANGED
@@ -656,7 +656,7 @@ module MakeWithConfig = (
656
656
  }
657
657
  module InboundTranslationSlice = InboundTranslationSlice_Builder.Make(ApiConfig)
658
658
 
659
- // Admin UI-fragment registry slices (docs/plans/event-sourced-fragment-registries.md): the
659
+ // Admin UI-fragment registry slices (docs/plans/done/event-sourced-fragment-registries.md): the
660
660
  // platform UI-fragment registry hosted as admin DCB slices sharing the admin DcbEventLog.
661
661
  // Built once at platform-module level and passed into Admin.construct below (registers the
662
662
  // slice's command handler exactly once).
@@ -1,7 +1,7 @@
1
1
  /** AppSync_Resolver_Native — drop-in replacement for
2
2
  `PulumiAws.AppSync.Resolver` that uses `@pulumi/aws-native` (Cloud Control
3
3
  API) so the CFN handler can internally wait out schema -> resolver
4
- propagation. See docs/plans/appsync-resolver-aws-native.md. */
4
+ propagation. See docs/plans/done/appsync-resolver-aws-native.md. */
5
5
 
6
6
  module Native = PulumiAws.AwsNative.AppSync.Resolver
7
7
  module Functions = PulumiAws.AppSync.Resolver.Functions
@@ -16,7 +16,7 @@
16
16
  `aws-native:appsync/resolver:Resolver` type so existing resources are
17
17
  adopted in-place on first deploy (no delete+recreate).
18
18
 
19
- See docs/plans/appsync-resolver-aws-native-retry.md. */
19
+ See docs/plans/done/appsync-resolver-aws-native-retry.md. */
20
20
 
21
21
  // ── AWS SDK bindings ─────────────────────────────────────────────────────────
22
22
  //
@@ -198,7 +198,7 @@ let jsThrow: JsExn.t => 'a = %raw(`e => { throw e }`)
198
198
 
199
199
  - `NotFoundException / No field named` — schema-propagation race after a
200
200
  successful `StartSchemaCreation`. Backstop for the per-plugin dedup
201
- flow (see `docs/plans/appsync-schema-push-dedup.md`).
201
+ flow (see `docs/plans/done/appsync-schema-push-dedup.md`).
202
202
  - `ConcurrentModificationException / Schema is currently being altered` —
203
203
  API-level lock contention with an in-flight `StartSchemaCreation` on
204
204
  the same API (admin resolvers racing the platform schema push from
@@ -62,8 +62,23 @@ let readOnlyKeys = [
62
62
  "EmailConfigurationFailure",
63
63
  ]
64
64
 
65
- /** The pool's own configuration, sent back whole with `PreTokenGeneration` set
66
- (or, when `preTokenGenerationArn` is `None`, removed).
65
+ /** The version of the pre-token-generation contract this resource attaches at.
66
+
67
+ [Auth_ActiveRoleTrigger_Ops] implements `V1_0` — it answers with
68
+ `claimsOverrideDetails` — and does so deliberately, because `V2_0`/`V3_0` buy
69
+ access-token customisation it does not need and are gated behind the
70
+ Essentials and Plus feature plans.
71
+
72
+ 🚨 **A pool left at a version the handler does not implement does not fail —
73
+ it silently does nothing.** Cognito sends a `V2_0` event, the handler answers
74
+ in `V1_0` shape, and the service ignores the answer. Sign-in succeeds, tokens
75
+ mint, and the role narrowing never happens. So the version is not the pool's
76
+ to keep: the trigger and the contract it speaks are one unit, and attaching
77
+ the trigger means attaching its version too. */
78
+ let triggerVersion = "V1_0"
79
+
80
+ /** The pool's own configuration, sent back whole with the pre-token-generation
81
+ trigger set (or, when `preTokenGenerationArn` is `None`, removed).
67
82
 
68
83
  Pure and total so the property that matters — a pool carrying non-default
69
84
  settings still carries them afterwards — is checkable without a pool. */
@@ -89,21 +104,64 @@ let mergedUpdateInput = (
89
104
  ->Dict.get("LambdaConfig")
90
105
  ->Option.flatMap(JSON.Decode.object)
91
106
  ->Option.mapOr(Dict.make(), existing => Dict.fromArray(existing->Dict.toArray))
107
+ // 🚨 Cognito carries this one trigger in TWO fields: the legacy
108
+ // `PreTokenGeneration` string and the `PreTokenGenerationConfig` object.
109
+ // `UpdateUserPool` rejects the call outright when both are present naming
110
+ // different functions, so both are written, together, naming the same one.
111
+ //
112
+ // Writing only one and letting the other ride through on the copy above cannot
113
+ // work, and not merely because a customer might have set it: **AWS
114
+ // materialises the field we did not write.** Attach via the legacy field alone
115
+ // and the next `DescribeUserPool` reports a `PreTokenGenerationConfig` the
116
+ // service populated, which the merge would then feed back on the following
117
+ // deploy — so an attachment that succeeded once fails the next time with
118
+ // nothing changed but AWS normalising its own record. Writing the pair in
119
+ // agreement is the only form stable under repeated describe → merge → update.
92
120
  switch preTokenGenerationArn {
93
- | Some(arn) => lambdaConfig->Dict.set("PreTokenGeneration", JSON.Encode.string(arn))
94
- | None => lambdaConfig->Dict.delete("PreTokenGeneration")
121
+ | Some(arn) =>
122
+ let config = Dict.make()
123
+ config->Dict.set("LambdaArn", JSON.Encode.string(arn))
124
+ config->Dict.set("LambdaVersion", JSON.Encode.string(triggerVersion))
125
+ lambdaConfig->Dict.set("PreTokenGeneration", JSON.Encode.string(arn))
126
+ lambdaConfig->Dict.set("PreTokenGenerationConfig", JSON.Encode.object(config))
127
+ | None =>
128
+ // Both, or the destroyed function stays attached through whichever field was
129
+ // left behind — the "every sign-in fails on a pool nothing here would fix"
130
+ // hazard [delete_] exists to prevent.
131
+ lambdaConfig->Dict.delete("PreTokenGeneration")
132
+ lambdaConfig->Dict.delete("PreTokenGenerationConfig")
95
133
  }
96
134
  out->Dict.set("LambdaConfig", lambdaConfig->JSON.Encode.object)
97
135
  out
98
136
  }
99
137
 
100
- /** The trigger currently attached to a described pool, if any. */
101
- let attachedTrigger = (~described: dict<JSON.t>): option<string> =>
102
- described
103
- ->Dict.get("LambdaConfig")
104
- ->Option.flatMap(JSON.Decode.object)
105
- ->Option.flatMap(c => c->Dict.get("PreTokenGeneration"))
106
- ->Option.flatMap(JSON.Decode.string)
138
+ /** The trigger currently attached to a described pool, if any.
139
+
140
+ Prefers `PreTokenGenerationConfig`, since that is the field AWS populates and
141
+ the one that carries the version.
142
+
143
+ A pool whose `LambdaVersion` names a contract [triggerVersion] does not
144
+ implement reports `None` — *not attached* — even when it points at our own
145
+ function. That is what turns the silent-no-op case into ordinary drift:
146
+ refresh records nothing attached, and the next `up` rewrites the pair at the
147
+ right version. Reporting the ARN instead would leave a pool that mints
148
+ un-narrowed tokens looking correct forever. */
149
+ let attachedTrigger = (~described: dict<JSON.t>): option<string> => {
150
+ let lambdaConfig = described->Dict.get("LambdaConfig")->Option.flatMap(JSON.Decode.object)
151
+ switch lambdaConfig
152
+ ->Option.flatMap(c => c->Dict.get("PreTokenGenerationConfig"))
153
+ ->Option.flatMap(JSON.Decode.object) {
154
+ | Some(config) =>
155
+ switch config->Dict.get("LambdaVersion")->Option.flatMap(JSON.Decode.string) {
156
+ | Some(version) if version != triggerVersion => None
157
+ | _ => config->Dict.get("LambdaArn")->Option.flatMap(JSON.Decode.string)
158
+ }
159
+ | None =>
160
+ lambdaConfig
161
+ ->Option.flatMap(c => c->Dict.get("PreTokenGeneration"))
162
+ ->Option.flatMap(JSON.Decode.string)
163
+ }
164
+ }
107
165
 
108
166
  // ── AWS SDK bindings (lazily imported — see AppSync_SourceApiAssociation_Retrying) ──
109
167
 
@@ -30,6 +30,8 @@ let readOnlyKeys = [
30
30
  "EmailConfigurationFailure"
31
31
  ];
32
32
 
33
+ let triggerVersion = "V1_0";
34
+
33
35
  function mergedUpdateInput(described, userPoolId, preTokenGenerationArn) {
34
36
  let out = {};
35
37
  Stdlib_Dict.forEachWithKey(described, (value, key) => {
@@ -44,16 +46,31 @@ function mergedUpdateInput(described, userPoolId, preTokenGenerationArn) {
44
46
  out["UserPoolId"] = userPoolId;
45
47
  let lambdaConfig = Stdlib_Option.mapOr(Stdlib_Option.flatMap(described["LambdaConfig"], Stdlib_JSON.Decode.object), {}, existing => Object.fromEntries(Object.entries(existing)));
46
48
  if (preTokenGenerationArn !== undefined) {
49
+ let config = {};
50
+ config["LambdaArn"] = preTokenGenerationArn;
51
+ config["LambdaVersion"] = triggerVersion;
47
52
  lambdaConfig["PreTokenGeneration"] = preTokenGenerationArn;
53
+ lambdaConfig["PreTokenGenerationConfig"] = config;
48
54
  } else {
49
55
  Stdlib_Dict.$$delete(lambdaConfig, "PreTokenGeneration");
56
+ Stdlib_Dict.$$delete(lambdaConfig, "PreTokenGenerationConfig");
50
57
  }
51
58
  out["LambdaConfig"] = lambdaConfig;
52
59
  return out;
53
60
  }
54
61
 
55
62
  function attachedTrigger(described) {
56
- return Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(described["LambdaConfig"], Stdlib_JSON.Decode.object), c => c["PreTokenGeneration"]), Stdlib_JSON.Decode.string);
63
+ let lambdaConfig = Stdlib_Option.flatMap(described["LambdaConfig"], Stdlib_JSON.Decode.object);
64
+ let config = Stdlib_Option.flatMap(Stdlib_Option.flatMap(lambdaConfig, c => c["PreTokenGenerationConfig"]), Stdlib_JSON.Decode.object);
65
+ if (config === undefined) {
66
+ return Stdlib_Option.flatMap(Stdlib_Option.flatMap(lambdaConfig, c => c["PreTokenGeneration"]), Stdlib_JSON.Decode.string);
67
+ }
68
+ let version = Stdlib_Option.flatMap(config["LambdaVersion"], Stdlib_JSON.Decode.string);
69
+ if (version !== undefined && version !== triggerVersion) {
70
+ return;
71
+ } else {
72
+ return Stdlib_Option.flatMap(config["LambdaArn"], Stdlib_JSON.Decode.string);
73
+ }
57
74
  }
58
75
 
59
76
  let newOf1 = ((C, x) => new C(x));
@@ -310,6 +327,7 @@ function make(nameOpt, props, opts) {
310
327
  export {
311
328
  log,
312
329
  readOnlyKeys,
330
+ triggerVersion,
313
331
  mergedUpdateInput,
314
332
  attachedTrigger,
315
333
  newOf1,
@@ -704,7 +704,7 @@ let collectEventTags = (
704
704
  // local backends' true DCB query semantics (and fixing analysis Issue 4 — the
705
705
  // one-event-type-per-attribute deadlock). It also folds the create guard into the
706
706
  // fence (see `buildConditionalFenceUpdate`), retiring the `create#` rows.
707
- // Plan: docs/plans/dcb-fence-event-type-granularity.md.
707
+ // Plan: docs/plans/done/dcb-fence-event-type-granularity.md.
708
708
  let fenceTypeAttr = (eventType: string) => `pos#${eventType}`
709
709
 
710
710
  let dedupStrings = (xs: array<string>): array<string> => {
@@ -975,7 +975,7 @@ let appendUnconditional = async (
975
975
  // fan-out sharing those prefixes turns into a hot partition → `TransactionConflict`
976
976
  // → `retries exhausted`; it also over-fenced (two DISTINCT composite entities
977
977
  // sharing a member value serialized needlessly). Plan:
978
- // docs/plans/Backlog/dcb-hot-tag-fence-contention.md § "Root-cause correction".
978
+ // docs/plans/done/dcb-hot-tag-fence-contention.md § "Root-cause correction".
979
979
  let compositeFenceTagKey = "__dcb_composite__"
980
980
 
981
981
  let makeCompositeFenceTag = (
@@ -11,7 +11,7 @@
11
11
  // The pool is lazy (`PgRuntime.poolFor`), so binding ops inside the deploy-time
12
12
  // `Output.apply` opens no connection during `pulumi up` — a real query only runs
13
13
  // inside the DcbCommandTopic Lambda at runtime. See
14
- // docs/plans/aws-postgres-change-feed-bridge.md.
14
+ // docs/plans/done/aws-postgres-change-feed-bridge.md.
15
15
 
16
16
  let make: ReventlessCore.DcbEventLog_Adapter.storageMaker = (
17
17
  ~name,
@@ -71,7 +71,7 @@ let append = table =>
71
71
  )
72
72
  ->Effect.runPromise
73
73
 
74
- // ─── Aggregate snapshots (docs/plans/aggregate-snapshotting.md) ───
74
+ // ─── Aggregate snapshots (docs/plans/done/aggregate-snapshotting.md) ───
75
75
  // A snapshot lives in the SAME table as the events, on the reserved sort key
76
76
  // `position = "SNAPSHOT"`. Event positions are 9-digit zero-padded numbers, so
77
77
  // bounding the replay query to the all-digit range excludes the snapshot row
@@ -5,7 +5,7 @@
5
5
  // consumes — onto that queue. The plugin EventCollector (`EventCollectorEntryPoint`)
6
6
  // then drives the full fan-out: read-model projections, aggregate command topics,
7
7
  // and the cross-plugin SNS EventTopic. See
8
- // docs/plans/aws-postgres-change-feed-bridge.md.
8
+ // docs/plans/done/aws-postgres-change-feed-bridge.md.
9
9
  //
10
10
  // The transform reuses the canonical DynamoDB shape producers
11
11
  // (`derivePartitionKey` + `buildJsonEvent'`) so the emitted body is byte-identical
@@ -1,5 +1,5 @@
1
1
  // B3.0 — projection delivery on Postgres platforms
2
- // (docs/plans/aws-postgres-querydb-adapter.md).
2
+ // (docs/plans/done/aws-postgres-querydb-adapter.md).
3
3
  //
4
4
  // On DynamoDB platforms the ReadModel / StateViewSlice Lambdas consume their
5
5
  // source event logs via DynamoDB-stream event-source mappings. Postgres logs have
@@ -99,6 +99,10 @@ type binding = {
99
99
  ownerField: option<string>,
100
100
  retiredField: option<string>,
101
101
  retiredValues: option<array<string>>,
102
+ /** Whether a reference to a retired row of this read model still resolves its
103
+ name — the `@namedWhenRetired` opt-in. Read only by the reference door;
104
+ every other door narrows as it always did. */
105
+ namedWhenRetired: bool,
102
106
  }
103
107
 
104
108
  // -- arg helpers -------------------------------------------------------------
@@ -253,6 +257,66 @@ let dispatch = async (
253
257
  Array.concat(found, extra)->Array.filter(item => ownerAllows(item) && retiredAllows(item)),
254
258
  )
255
259
 
260
+ | "refs" =>
261
+ // The reference door. The same by-ids read, projected on the way out to
262
+ // what naming a row needs, with the retirement lifted for a view that said
263
+ // its retired rows keep their names — and the owner rule left exactly where
264
+ // it was, so a row that is somebody's is still only theirs.
265
+ let ids = payload.arguments->argStrs("ids")
266
+ let found = await binding.pushdowns.byIds(~readModelName=rm, ids)
267
+ let missing =
268
+ found->Array.length < ids->Array.length
269
+ ? ids->Array.filterMap(ReventlessCore.Api_Ids.alternateKey)
270
+ : []
271
+ let extra = missing->Array.length > 0
272
+ ? await binding.pushdowns.byIds(~readModelName=rm, missing)
273
+ : []
274
+ let retirementOf = (item: JSON.t) =>
275
+ switch binding.retiredField {
276
+ | None => (false, None)
277
+ | Some(field) =>
278
+ let cell = item->JSON.Decode.object->Option.flatMap(d => d->Dict.get(field))
279
+ let scope: Reventless.OwnerScope.retiredScope = {
280
+ field,
281
+ values: binding.retiredValues,
282
+ }
283
+ let retired = scope->Reventless.OwnerScope.isRetiredValue(cell)
284
+ (
285
+ retired,
286
+ retired && binding.retiredValues->Option.isSome
287
+ ? cell->Option.flatMap(JSON.Decode.string)
288
+ : None,
289
+ )
290
+ }
291
+ JSON.Encode.array(
292
+ Array.concat(found, extra)
293
+ ->Array.filter(ownerAllows)
294
+ ->Array.filterMap(item => {
295
+ let (retired, state) = retirementOf(item)
296
+ if retired && !binding.namedWhenRetired {
297
+ None
298
+ } else {
299
+ let get = key => item->JSON.Decode.object->Option.flatMap(d => d->Dict.get(key))
300
+ let id = get("id")->Option.flatMap(JSON.Decode.string)->Option.getOr("")
301
+ Some(
302
+ Dict.fromArray([
303
+ ("id", JSON.Encode.string(id)),
304
+ (
305
+ "label",
306
+ JSON.Encode.string(
307
+ get(binding.labelField)
308
+ ->Option.flatMap(JSON.Decode.string)
309
+ ->Option.getOr(id),
310
+ ),
311
+ ),
312
+ ("retired", JSON.Encode.bool(retired)),
313
+ ("retiredState", state->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
314
+ ])->JSON.Encode.object,
315
+ )
316
+ }
317
+ }),
318
+ )
319
+
256
320
  | "items" =>
257
321
  // Sub-id connection: {single}Items(id, filter, first/after/last/before).
258
322
  // Requires a subIdField; without one it's an empty connection.
@@ -185,11 +185,63 @@ async function dispatch(binding, lookupBindingOpt, payload) {
185
185
  let items$1 = await binding.pushdowns.scanAll(rm);
186
186
  return QueryDbListQuery$ReventlessCore.run(items$1, argsDict, binding.capability, binding.labelField, undefined, ownerScope, retiredScope);
187
187
  break;
188
+ case "refs" :
189
+ let ids$1 = argStrs(payload.arguments, "ids");
190
+ let found$2 = await binding.pushdowns.byIds(rm, ids$1);
191
+ let missing$1 = found$2.length < ids$1.length ? Stdlib_Array.filterMap(ids$1, Api_Ids$ReventlessCore.alternateKey) : [];
192
+ let extra$1 = missing$1.length !== 0 ? await binding.pushdowns.byIds(rm, missing$1) : [];
193
+ let retirementOf = item => {
194
+ let field = binding.retiredField;
195
+ if (field === undefined) {
196
+ return [
197
+ false,
198
+ undefined
199
+ ];
200
+ }
201
+ let cell = Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[field]);
202
+ let scope_values = binding.retiredValues;
203
+ let scope = {
204
+ field: field,
205
+ values: scope_values
206
+ };
207
+ let retired = OwnerScope$Reventless.isRetiredValue(scope, cell);
208
+ return [
209
+ retired,
210
+ retired && Stdlib_Option.isSome(binding.retiredValues) ? Stdlib_Option.flatMap(cell, Stdlib_JSON.Decode.string) : undefined
211
+ ];
212
+ };
213
+ return Stdlib_Array.filterMap(found$2.concat(extra$1).filter(ownerAllows), item => {
214
+ let match = retirementOf(item);
215
+ let retired = match[0];
216
+ if (retired && !binding.namedWhenRetired) {
217
+ return;
218
+ }
219
+ let get = key => Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[key]);
220
+ let id = Stdlib_Option.getOr(Stdlib_Option.flatMap(get("id"), Stdlib_JSON.Decode.string), "");
221
+ return Object.fromEntries([
222
+ [
223
+ "id",
224
+ id
225
+ ],
226
+ [
227
+ "label",
228
+ Stdlib_Option.getOr(Stdlib_Option.flatMap(get(binding.labelField), Stdlib_JSON.Decode.string), id)
229
+ ],
230
+ [
231
+ "retired",
232
+ retired
233
+ ],
234
+ [
235
+ "retiredState",
236
+ Stdlib_Option.mapOr(match[1], null, prim => prim)
237
+ ]
238
+ ]);
239
+ });
188
240
  case "resolveMany" :
189
241
  let target = Stdlib_Option.getOr(payload.target, rm);
190
242
  let source = Stdlib_Option.getOr(payload.source, null);
191
- let ids$1 = argStrs(source, Stdlib_Option.getOr(payload.sourceIdsField, ""));
192
- return await binding.pushdowns.byIds(target, ids$1);
243
+ let ids$2 = argStrs(source, Stdlib_Option.getOr(payload.sourceIdsField, ""));
244
+ return await binding.pushdowns.byIds(target, ids$2);
193
245
  case "resolveOne" :
194
246
  let target$1 = Stdlib_Option.getOr(payload.target, rm);
195
247
  let source$1 = Stdlib_Option.getOr(payload.source, null);
@@ -1,6 +1,6 @@
1
1
  // AWS-side selection for the QueryDb (read model) storage backend — the QueryDb
2
2
  // analogue of `EventLogBackend` / `DcbBackend` (B3.1,
3
- // docs/plans/aws-postgres-querydb-adapter.md).
3
+ // docs/plans/done/aws-postgres-querydb-adapter.md).
4
4
  //
5
5
  // When `Platform.MakeWithConfig` is given a `~pgConnection`, it records the
6
6
  // resolved selection here BEFORE any admin/plugin `construct` runs. Read by: