@reventlessdev/reventless-aws 3.0.0-alpha.176 → 3.0.0-alpha.178

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 (71) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/docker-compose.postgres.yml +21 -0
  3. package/package.json +7 -6
  4. package/scripts/run-pg-integration-tests.sh +58 -0
  5. package/src/Platform.res +49 -0
  6. package/src/Platform.res.mjs +48 -2
  7. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +7 -1
  8. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +10 -1
  9. package/src/adapter/Api/Platform_UIFragments_Lambda.res +7 -1
  10. package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +10 -1
  11. package/src/adapter/Counter/CounterHandler_DynamoDbStream.res +6 -21
  12. package/src/adapter/Counter/CounterHandler_DynamoDbStream.res.mjs +2 -17
  13. package/src/adapter/DcbEventLog/DcbBackend.res +4 -0
  14. package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres.res +2 -2
  15. package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres.res.mjs +7 -1
  16. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +1 -11
  17. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +2 -24
  18. package/src/adapter/Postgres/PgConnection.res +70 -10
  19. package/src/adapter/Postgres/PgConnection.res.mjs +48 -3
  20. package/src/adapter/Postgres/PgMigration_Builder.res +127 -0
  21. package/src/adapter/Postgres/PgMigration_Builder.res.mjs +51 -0
  22. package/src/adapter/QueryDb/PgQueryResolver_Builder.res +279 -0
  23. package/src/adapter/QueryDb/PgQueryResolver_Builder.res.mjs +170 -0
  24. package/src/adapter/QueryDb/PgQueryResolver_Lambda.res +202 -11
  25. package/src/adapter/QueryDb/PgQueryResolver_Lambda.res.mjs +150 -9
  26. package/src/adapter/QueryDb/QueryDbBackend.res +8 -0
  27. package/src/adapter/QueryDb/QueryDbBackend.res.mjs +4 -1
  28. package/src/adapter/QueryDb/QueryDbResolvers.res +6 -5
  29. package/src/adapter/QueryDb/QueryDbResolvers.res.mjs +6 -3
  30. package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res +288 -0
  31. package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res.mjs +170 -0
  32. package/src/adapter/QueryDb/QueryDbStorage.res +6 -1
  33. package/src/adapter/QueryDb/QueryDbStorage.res.mjs +2 -1
  34. package/src/adapter/QueryDb/QueryDbStorage_Postgres.res +5 -1
  35. package/src/adapter/QueryDb/QueryDbStorage_Postgres.res.mjs +3 -3
  36. package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res +3 -14
  37. package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res.mjs +2 -25
  38. package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res +3 -14
  39. package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res.mjs +2 -25
  40. package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +7 -1
  41. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +78 -15
  42. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res.mjs +43 -26
  43. package/src/adapter/Runtime/PgMigrationEntryPoint.mjs +40 -0
  44. package/src/adapter/Runtime/PgQueryResolverEntryPoint.mjs +88 -0
  45. package/src/adapter/Runtime/ReadModelEntryPoint.mjs +17 -5
  46. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +10 -0
  47. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +3 -0
  48. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +9 -14
  49. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +2 -25
  50. package/src/adapter/Runtime/StateTopicPublish.mjs +204 -0
  51. package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +17 -3
  52. package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res +86 -17
  53. package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res.mjs +50 -33
  54. package/src/adapter/StateTopic/StateTopic_AppSync.res +7 -1
  55. package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +10 -1
  56. package/src/plugin/cloner/ClonerRunner_Fargate.res +7 -1
  57. package/src/plugin/cloner/ClonerRunner_Fargate.res.mjs +10 -1
  58. package/src/util/Util_Bundle.res +71 -1
  59. package/src/util/Util_Bundle.res.mjs +49 -1
  60. package/tests/PgChangeFeedRelay_IntegrationTest.res +3 -3
  61. package/tests/PgConnectionJsonTest.res +84 -0
  62. package/tests/PgConnectionJsonTest.res.mjs +68 -0
  63. package/tests/PgMigrationEntryPoint_IntegrationTest.res +82 -0
  64. package/tests/PgMigrationEntryPoint_IntegrationTest.res.mjs +63 -0
  65. package/tests/PgPipeline_IntegrationTest.res +3 -1
  66. package/tests/PgQueryResolver_LambdaTest.res +163 -2
  67. package/tests/PgQueryResolver_LambdaTest.res.mjs +226 -14
  68. package/tests/StateTopicPublishTest.res +162 -0
  69. package/tests/StateTopicPublishTest.res.mjs +158 -0
  70. package/tests/Util_BundleEsmLoaderTest.res +57 -0
  71. package/tests/Util_BundleEsmLoaderTest.res.mjs +44 -0
@@ -9,6 +9,8 @@
9
9
  open JestGlobals
10
10
  open ReventlessCore
11
11
 
12
+ @val external btoa: string => string = "btoa"
13
+
12
14
  let mk = (id, status, name) => {
13
15
  let o = Dict.make()
14
16
  o->Dict.set("id", JSON.Encode.string(id))
@@ -51,6 +53,9 @@ let pushdowns: PgQueryResolver_Lambda.pushdowns = {
51
53
  ids->Array.filterMap(id => store->Dict.get(id)),
52
54
  listPage: async (~readModelName as _, ~argsDict as _, ~capability as _, ~labelField as _) =>
53
55
  listPageReturn.contents,
56
+ itemsPage: async (~readModelName as _, ~subIdField as _, ~id, ~argsDict as _) =>
57
+ // Sentinel echoing the requested id, so the dispatch routing is observable.
58
+ JSON.Encode.object(Dict.fromArray([("itemsFor", JSON.Encode.string(id))])),
54
59
  scanAll: async (~readModelName as _) => allItems(),
55
60
  }
56
61
 
@@ -59,14 +64,18 @@ let capability: GraphQL_FragmentGenerator.serverCapability = {
59
64
  sortFields: ["name", "status"],
60
65
  }
61
66
 
62
- let makeBinding = (~authorization=Reventless.Authorization.AllowAnonymous, ()): PgQueryResolver_Lambda.binding => {
67
+ let makeBinding = (
68
+ ~authorization=Reventless.Authorization.AllowAnonymous,
69
+ ~subIdField=None,
70
+ (),
71
+ ): PgQueryResolver_Lambda.binding => {
63
72
  ops,
64
73
  pushdowns,
65
74
  // "byStatus" index stored on the `status` field.
66
75
  indexes: [
67
76
  {index: "byStatus", type_: "S", idField: "status", projectionType: Reventless.ReadModel.ALL},
68
77
  ],
69
- subIdField: None,
78
+ subIdField,
70
79
  capability,
71
80
  labelField: "name",
72
81
  includeIdParam: true,
@@ -163,6 +172,22 @@ describe("PgQueryResolver_Lambda.dispatch", () => {
163
172
  expect(edgeIds)->toEqual(["p-1", "p-2", "p-3"])
164
173
  })
165
174
 
175
+ testPromise("items with subId routes to itemsPage (id echoed)", async () => {
176
+ let r = await PgQueryResolver_Lambda.dispatch(
177
+ ~binding=makeBinding(~subIdField=Some("seq"), ()),
178
+ ~payload=mkPayload(~kind="items", ~args=objArgs([("id", JSON.Encode.string("p-2"))]), ()),
179
+ )
180
+ expect(r->str("itemsFor"))->toEqual(Some("p-2"))
181
+ })
182
+
183
+ testPromise("items without subId → empty connection", async () => {
184
+ let r = await PgQueryResolver_Lambda.dispatch(
185
+ ~binding=makeBinding(),
186
+ ~payload=mkPayload(~kind="items", ~args=objArgs([("id", JSON.Encode.string("p-2"))]), ()),
187
+ )
188
+ expect(r->field("edges")->Option.flatMap(JSON.Decode.array)->Option.getOr([])->Array.length)->toBe(0)
189
+ })
190
+
166
191
  testPromise("authorization DenyAll → empty shape per kind", async () => {
167
192
  let b = makeBinding(~authorization=DenyAll, ())
168
193
  let single = await PgQueryResolver_Lambda.dispatch(
@@ -191,6 +216,142 @@ describe("PgQueryResolver_Lambda.dispatch", () => {
191
216
  expect(r)->toBe(JSON.Encode.null)
192
217
  })
193
218
 
219
+ // ── cross-table (@resolves / @resolvesMany / node) ────────────────────────
220
+ // For resolveOne/resolveMany the dispatch binding IS the target binding
221
+ // (the handler selects it); source carries the parent object's key(s).
222
+ testPromise("resolveOne loads target by parent's id field", async () => {
223
+ let payload: PgQueryResolver_Lambda.payload = {
224
+ readModelName: "Things",
225
+ kind: "resolveOne",
226
+ target: "Things",
227
+ source: objArgs([("ref", JSON.Encode.string("p-2"))]),
228
+ sourceIdField: "ref",
229
+ multi: false,
230
+ arguments: objArgs([]),
231
+ identity: Reventless.Identity.anonymous,
232
+ }
233
+ let r = await PgQueryResolver_Lambda.dispatch(~binding=makeBinding(), ~payload)
234
+ expect(r->str("id"))->toEqual(Some("p-2"))
235
+ })
236
+
237
+ testPromise("resolveOne multi via target index → array", async () => {
238
+ let payload: PgQueryResolver_Lambda.payload = {
239
+ readModelName: "Things",
240
+ kind: "resolveOne",
241
+ target: "Things",
242
+ source: objArgs([("st", JSON.Encode.string("active"))]),
243
+ sourceIdField: "st",
244
+ targetIndex: "byStatus",
245
+ targetIndexIdField: "status",
246
+ multi: true,
247
+ arguments: objArgs([]),
248
+ identity: Reventless.Identity.anonymous,
249
+ }
250
+ let r = await PgQueryResolver_Lambda.dispatch(~binding=makeBinding(), ~payload)
251
+ expect(r->ids)->toEqual(["p-1", "p-3"])
252
+ })
253
+
254
+ testPromise("resolveMany batch-loads target by parent's ids field", async () => {
255
+ let payload: PgQueryResolver_Lambda.payload = {
256
+ readModelName: "Things",
257
+ kind: "resolveMany",
258
+ target: "Things",
259
+ source: objArgs([
260
+ ("refs", JSON.Encode.array(["p-1", "x", "p-3"]->Array.map(JSON.Encode.string))),
261
+ ]),
262
+ sourceIdsField: "refs",
263
+ arguments: objArgs([]),
264
+ identity: Reventless.Identity.anonymous,
265
+ }
266
+ let r = await PgQueryResolver_Lambda.dispatch(~binding=makeBinding(), ~payload)
267
+ expect(r->ids)->toEqual(["p-1", "p-3"])
268
+ })
269
+
270
+ testPromise("node decodes global id → __typename + item", async () => {
271
+ // Register a binding + node type in the module-level registries.
272
+ PgQueryResolver_Lambda.register(~readModelName="Things", makeBinding())
273
+ PgQueryResolver_Lambda.registerNodeType(~typeName="Thing", ~readModelName="Things")
274
+ let payload: PgQueryResolver_Lambda.payload = {
275
+ readModelName: "",
276
+ kind: "node",
277
+ arguments: objArgs([("id", JSON.Encode.string(btoa("Thing:p-2")))]),
278
+ identity: Reventless.Identity.anonymous,
279
+ }
280
+ let r = await PgQueryResolver_Lambda.handler(payload, Obj.magic(Nullable.null))
281
+ expect(r->str("__typename"))->toEqual(Some("Thing"))
282
+ expect(r->str("name"))->toEqual(Some("Bravo"))
283
+ })
284
+
285
+ testPromise("node with unknown type → null", async () => {
286
+ let payload: PgQueryResolver_Lambda.payload = {
287
+ readModelName: "",
288
+ kind: "node",
289
+ arguments: objArgs([("id", JSON.Encode.string(btoa("Nope:p-1")))]),
290
+ identity: Reventless.Identity.anonymous,
291
+ }
292
+ let r = await PgQueryResolver_Lambda.handler(payload, Obj.magic(Nullable.null))
293
+ expect(r)->toBe(JSON.Encode.null)
294
+ })
295
+
296
+ // ── auth-table pipeline (group-restricted index) ──────────────────────────
297
+ // Auth table (another read model) maps index value → { ownerId: <username> }.
298
+ let authStore = Dict.fromArray([
299
+ ("active", JSON.Encode.object(Dict.fromArray([("ownerId", JSON.Encode.string("alice"))]))),
300
+ ])
301
+ let authOps: QueryDb_Adapter.operations = {
302
+ load: async id => Ok(authStore->Dict.get(id)->Option.mapOr([], r => [r])),
303
+ loadStream: Obj.magic(0),
304
+ save: Obj.magic(0),
305
+ saveBatch: Obj.magic(0),
306
+ count: Obj.magic(0),
307
+ delete: Obj.magic(0),
308
+ deleteBatch: Obj.magic(0),
309
+ }
310
+ let authBinding = {...makeBinding(), PgQueryResolver_Lambda.ops: authOps}
311
+ let lookupAuth = name => name == "AuthTable" ? Some(authBinding) : None
312
+ let ident = (~username, ~groups): Reventless.Identity.t => {
313
+ userId: username,
314
+ username,
315
+ groups,
316
+ provider: InMemory,
317
+ }
318
+ let authIndexPayload = (~username, ~groups): PgQueryResolver_Lambda.payload => {
319
+ readModelName: "Things",
320
+ kind: "index",
321
+ index: "byStatus",
322
+ authTable: "AuthTable",
323
+ authGroup: "Owner",
324
+ arguments: objArgs([("byStatus", JSON.Encode.string("active"))]),
325
+ identity: ident(~username, ~groups),
326
+ }
327
+
328
+ testPromise("auth index: group member who owns → results", async () => {
329
+ let r = await PgQueryResolver_Lambda.dispatch(
330
+ ~binding=makeBinding(),
331
+ ~lookupBinding=lookupAuth,
332
+ ~payload=authIndexPayload(~username="alice", ~groups=["Owner"]),
333
+ )
334
+ expect(r->ids)->toEqual(["p-1", "p-3"])
335
+ })
336
+
337
+ testPromise("auth index: group member who does NOT own → empty", async () => {
338
+ let r = await PgQueryResolver_Lambda.dispatch(
339
+ ~binding=makeBinding(),
340
+ ~lookupBinding=lookupAuth,
341
+ ~payload=authIndexPayload(~username="bob", ~groups=["Owner"]),
342
+ )
343
+ expect(r->JSON.Decode.array->Option.getOr([])->Array.length)->toBe(0)
344
+ })
345
+
346
+ testPromise("auth index: non-member passes through → results", async () => {
347
+ let r = await PgQueryResolver_Lambda.dispatch(
348
+ ~binding=makeBinding(),
349
+ ~lookupBinding=lookupAuth,
350
+ ~payload=authIndexPayload(~username="bob", ~groups=[]),
351
+ )
352
+ expect(r->ids)->toEqual(["p-1", "p-3"])
353
+ })
354
+
194
355
  testPromise("unmapped kind throws", async () => {
195
356
  let threw = try {
196
357
  let _ = await PgQueryResolver_Lambda.dispatch(
@@ -3,6 +3,7 @@
3
3
  import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
4
4
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
5
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
6
7
  import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
7
8
  import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
8
9
  import * as QueryDb_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/QueryDb/QueryDb_Callback.res.mjs";
@@ -72,6 +73,13 @@ async function pushdowns_listPage(param, param$1, param$2, param$3) {
72
73
  return listPageReturn.contents;
73
74
  }
74
75
 
76
+ async function pushdowns_itemsPage(param, param$1, id, param$2) {
77
+ return Object.fromEntries([[
78
+ "itemsFor",
79
+ id
80
+ ]]);
81
+ }
82
+
75
83
  async function pushdowns_scanAll(param) {
76
84
  return Object.values(store);
77
85
  }
@@ -80,6 +88,7 @@ let pushdowns = {
80
88
  indexLookup: pushdowns_indexLookup,
81
89
  byIds: pushdowns_byIds,
82
90
  listPage: pushdowns_listPage,
91
+ itemsPage: pushdowns_itemsPage,
83
92
  scanAll: pushdowns_scanAll
84
93
  };
85
94
 
@@ -99,8 +108,9 @@ let capability = {
99
108
  sortFields: capability_sortFields
100
109
  };
101
110
 
102
- function makeBinding(authorizationOpt, param) {
111
+ function makeBinding(authorizationOpt, subIdFieldOpt, param) {
103
112
  let authorization = authorizationOpt !== undefined ? authorizationOpt : "AllowAnonymous";
113
+ let subIdField = subIdFieldOpt !== undefined ? Primitive_option.valFromOption(subIdFieldOpt) : undefined;
104
114
  return {
105
115
  ops: ops,
106
116
  pushdowns: pushdowns,
@@ -110,7 +120,7 @@ function makeBinding(authorizationOpt, param) {
110
120
  idField: "status",
111
121
  projectionType: "ALL"
112
122
  }],
113
- subIdField: undefined,
123
+ subIdField: subIdField,
114
124
  capability: capability,
115
125
  labelField: "name",
116
126
  includeIdParam: true,
@@ -147,7 +157,7 @@ function ids(arr) {
147
157
 
148
158
  globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
149
159
  globalThis.test("getById returns the item (id-carrying)", async () => {
150
- let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined), mkPayload("getById", undefined, Object.fromEntries([[
160
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
151
161
  "id",
152
162
  "p-2"
153
163
  ]]), undefined));
@@ -155,14 +165,14 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
155
165
  globalThis.expect(str(r, "name")).toEqual("Bravo");
156
166
  });
157
167
  globalThis.test("getById miss → null", async () => {
158
- let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined), mkPayload("getById", undefined, Object.fromEntries([[
168
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
159
169
  "id",
160
170
  "absent"
161
171
  ]]), undefined));
162
172
  globalThis.expect(r).toBe(null);
163
173
  });
164
174
  globalThis.test("byIds returns matches, drops missing", async () => {
165
- let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined), mkPayload("byIds", undefined, Object.fromEntries([[
175
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), undefined, mkPayload("byIds", undefined, Object.fromEntries([[
166
176
  "ids",
167
177
  [
168
178
  "p-1",
@@ -176,7 +186,7 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
176
186
  ]);
177
187
  });
178
188
  globalThis.test("index routes to indexLookup on the idField", async () => {
179
- let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined), mkPayload("index", "byStatus", Object.fromEntries([[
189
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), undefined, mkPayload("index", "byStatus", Object.fromEntries([[
180
190
  "byStatus",
181
191
  "active"
182
192
  ]]), undefined));
@@ -187,13 +197,13 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
187
197
  });
188
198
  globalThis.test("list with push-down → returns the connection listPage gives", async () => {
189
199
  listPageReturn.contents = "PUSHED";
190
- let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined), mkPayload("list", undefined, undefined, undefined));
200
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), undefined, mkPayload("list", undefined, undefined, undefined));
191
201
  listPageReturn.contents = undefined;
192
202
  globalThis.expect(r).toEqual("PUSHED");
193
203
  });
194
204
  globalThis.test("list without push-down → falls back to the shared spec", async () => {
195
205
  listPageReturn.contents = undefined;
196
- let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined), mkPayload("list", undefined, undefined, undefined));
206
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), undefined, mkPayload("list", undefined, undefined, undefined));
197
207
  let edgeIds = Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_Option.flatMap(field(r, "edges"), Stdlib_JSON.Decode.array), []), e => Stdlib_Option.flatMap(field(e, "node"), n => str(n, "id")));
198
208
  globalThis.expect(edgeIds).toEqual([
199
209
  "p-1",
@@ -201,16 +211,30 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
201
211
  "p-3"
202
212
  ]);
203
213
  });
214
+ globalThis.test("items with subId routes to itemsPage (id echoed)", async () => {
215
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, "seq", undefined), undefined, mkPayload("items", undefined, Object.fromEntries([[
216
+ "id",
217
+ "p-2"
218
+ ]]), undefined));
219
+ globalThis.expect(str(r, "itemsFor")).toEqual("p-2");
220
+ });
221
+ globalThis.test("items without subId → empty connection", async () => {
222
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), undefined, mkPayload("items", undefined, Object.fromEntries([[
223
+ "id",
224
+ "p-2"
225
+ ]]), undefined));
226
+ globalThis.expect(Stdlib_Option.getOr(Stdlib_Option.flatMap(field(r, "edges"), Stdlib_JSON.Decode.array), []).length).toBe(0);
227
+ });
204
228
  globalThis.test("authorization DenyAll → empty shape per kind", async () => {
205
- let b = makeBinding("DenyAll", undefined);
206
- let single = await PgQueryResolver_Lambda$ReventlessAws.dispatch(b, mkPayload("getById", undefined, Object.fromEntries([[
229
+ let b = makeBinding("DenyAll", undefined, undefined);
230
+ let single = await PgQueryResolver_Lambda$ReventlessAws.dispatch(b, undefined, mkPayload("getById", undefined, Object.fromEntries([[
207
231
  "id",
208
232
  "p-1"
209
233
  ]]), undefined));
210
234
  globalThis.expect(single).toBe(null);
211
- let list = await PgQueryResolver_Lambda$ReventlessAws.dispatch(b, mkPayload("list", undefined, undefined, undefined));
235
+ let list = await PgQueryResolver_Lambda$ReventlessAws.dispatch(b, undefined, mkPayload("list", undefined, undefined, undefined));
212
236
  globalThis.expect(Stdlib_Option.getOr(Stdlib_Option.flatMap(field(list, "edges"), Stdlib_JSON.Decode.array), []).length).toBe(0);
213
- let byIds = await PgQueryResolver_Lambda$ReventlessAws.dispatch(b, mkPayload("byIds", undefined, Object.fromEntries([[
237
+ let byIds = await PgQueryResolver_Lambda$ReventlessAws.dispatch(b, undefined, mkPayload("byIds", undefined, Object.fromEntries([[
214
238
  "ids",
215
239
  []
216
240
  ]]), undefined));
@@ -221,17 +245,205 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
221
245
  TAG: "Deny",
222
246
  _0: "nope"
223
247
  }));
224
- let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined), mkPayload("getById", undefined, Object.fromEntries([[
248
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
225
249
  "id",
226
250
  "p-1"
227
251
  ]]), undefined));
228
252
  QueryDb_Callback$ReventlessCore.clearQueryInterceptor();
229
253
  globalThis.expect(r).toBe(null);
230
254
  });
255
+ globalThis.test("resolveOne loads target by parent's id field", async () => {
256
+ let payload_target = "Things";
257
+ let payload_source = Object.fromEntries([[
258
+ "ref",
259
+ "p-2"
260
+ ]]);
261
+ let payload_sourceIdField = "ref";
262
+ let payload_multi = false;
263
+ let payload_arguments = Object.fromEntries([]);
264
+ let payload = {
265
+ readModelName: "Things",
266
+ kind: "resolveOne",
267
+ target: payload_target,
268
+ source: payload_source,
269
+ sourceIdField: payload_sourceIdField,
270
+ multi: payload_multi,
271
+ arguments: payload_arguments,
272
+ identity: Identity$Reventless.anonymous
273
+ };
274
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), undefined, payload);
275
+ globalThis.expect(str(r, "id")).toEqual("p-2");
276
+ });
277
+ globalThis.test("resolveOne multi via target index → array", async () => {
278
+ let payload_target = "Things";
279
+ let payload_source = Object.fromEntries([[
280
+ "st",
281
+ "active"
282
+ ]]);
283
+ let payload_sourceIdField = "st";
284
+ let payload_targetIndex = "byStatus";
285
+ let payload_targetIndexIdField = "status";
286
+ let payload_multi = true;
287
+ let payload_arguments = Object.fromEntries([]);
288
+ let payload = {
289
+ readModelName: "Things",
290
+ kind: "resolveOne",
291
+ target: payload_target,
292
+ source: payload_source,
293
+ sourceIdField: payload_sourceIdField,
294
+ targetIndex: payload_targetIndex,
295
+ targetIndexIdField: payload_targetIndexIdField,
296
+ multi: payload_multi,
297
+ arguments: payload_arguments,
298
+ identity: Identity$Reventless.anonymous
299
+ };
300
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), undefined, payload);
301
+ globalThis.expect(ids(r)).toEqual([
302
+ "p-1",
303
+ "p-3"
304
+ ]);
305
+ });
306
+ globalThis.test("resolveMany batch-loads target by parent's ids field", async () => {
307
+ let payload_target = "Things";
308
+ let payload_source = Object.fromEntries([[
309
+ "refs",
310
+ [
311
+ "p-1",
312
+ "x",
313
+ "p-3"
314
+ ].map(prim => prim)
315
+ ]]);
316
+ let payload_sourceIdsField = "refs";
317
+ let payload_arguments = Object.fromEntries([]);
318
+ let payload = {
319
+ readModelName: "Things",
320
+ kind: "resolveMany",
321
+ target: payload_target,
322
+ source: payload_source,
323
+ sourceIdsField: payload_sourceIdsField,
324
+ arguments: payload_arguments,
325
+ identity: Identity$Reventless.anonymous
326
+ };
327
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), undefined, payload);
328
+ globalThis.expect(ids(r)).toEqual([
329
+ "p-1",
330
+ "p-3"
331
+ ]);
332
+ });
333
+ globalThis.test("node decodes global id → __typename + item", async () => {
334
+ PgQueryResolver_Lambda$ReventlessAws.register("Things", makeBinding(undefined, undefined, undefined));
335
+ PgQueryResolver_Lambda$ReventlessAws.registerNodeType("Thing", "Things");
336
+ let payload_arguments = Object.fromEntries([[
337
+ "id",
338
+ btoa("Thing:p-2")
339
+ ]]);
340
+ let payload = {
341
+ readModelName: "",
342
+ kind: "node",
343
+ arguments: payload_arguments,
344
+ identity: Identity$Reventless.anonymous
345
+ };
346
+ let r = await PgQueryResolver_Lambda$ReventlessAws.handler(payload, null);
347
+ globalThis.expect(str(r, "__typename")).toEqual("Thing");
348
+ globalThis.expect(str(r, "name")).toEqual("Bravo");
349
+ });
350
+ globalThis.test("node with unknown type → null", async () => {
351
+ let payload_arguments = Object.fromEntries([[
352
+ "id",
353
+ btoa("Nope:p-1")
354
+ ]]);
355
+ let payload = {
356
+ readModelName: "",
357
+ kind: "node",
358
+ arguments: payload_arguments,
359
+ identity: Identity$Reventless.anonymous
360
+ };
361
+ let r = await PgQueryResolver_Lambda$ReventlessAws.handler(payload, null);
362
+ globalThis.expect(r).toBe(null);
363
+ });
364
+ let authStore = Object.fromEntries([[
365
+ "active",
366
+ Object.fromEntries([[
367
+ "ownerId",
368
+ "alice"
369
+ ]])
370
+ ]]);
371
+ let authOps_load = async id => ({
372
+ TAG: "Ok",
373
+ _0: Stdlib_Option.mapOr(authStore[id], [], r => [r])
374
+ });
375
+ let authOps = {
376
+ load: authOps_load,
377
+ loadStream: 0,
378
+ save: 0,
379
+ saveBatch: 0,
380
+ count: 0,
381
+ delete: 0,
382
+ deleteBatch: 0
383
+ };
384
+ let init = makeBinding(undefined, undefined, undefined);
385
+ let authBinding_pushdowns = init.pushdowns;
386
+ let authBinding_indexes = init.indexes;
387
+ let authBinding_subIdField = init.subIdField;
388
+ let authBinding_capability = init.capability;
389
+ let authBinding_labelField = init.labelField;
390
+ let authBinding_includeIdParam = init.includeIdParam;
391
+ let authBinding_authorization = init.authorization;
392
+ let authBinding = {
393
+ ops: authOps,
394
+ pushdowns: authBinding_pushdowns,
395
+ indexes: authBinding_indexes,
396
+ subIdField: authBinding_subIdField,
397
+ capability: authBinding_capability,
398
+ labelField: authBinding_labelField,
399
+ includeIdParam: authBinding_includeIdParam,
400
+ authorization: authBinding_authorization
401
+ };
402
+ let lookupAuth = name => {
403
+ if (name === "AuthTable") {
404
+ return authBinding;
405
+ }
406
+ };
407
+ let ident = (username, groups) => ({
408
+ userId: username,
409
+ username: username,
410
+ groups: groups,
411
+ provider: "InMemory"
412
+ });
413
+ let authIndexPayload = (username, groups) => ({
414
+ readModelName: "Things",
415
+ kind: "index",
416
+ index: "byStatus",
417
+ authTable: "AuthTable",
418
+ authGroup: "Owner",
419
+ arguments: Object.fromEntries([[
420
+ "byStatus",
421
+ "active"
422
+ ]]),
423
+ identity: ident(username, groups)
424
+ });
425
+ globalThis.test("auth index: group member who owns → results", async () => {
426
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), lookupAuth, authIndexPayload("alice", ["Owner"]));
427
+ globalThis.expect(ids(r)).toEqual([
428
+ "p-1",
429
+ "p-3"
430
+ ]);
431
+ });
432
+ globalThis.test("auth index: group member who does NOT own → empty", async () => {
433
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), lookupAuth, authIndexPayload("bob", ["Owner"]));
434
+ globalThis.expect(Stdlib_Option.getOr(Stdlib_JSON.Decode.array(r), []).length).toBe(0);
435
+ });
436
+ globalThis.test("auth index: non-member passes through → results", async () => {
437
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), lookupAuth, authIndexPayload("bob", []));
438
+ globalThis.expect(ids(r)).toEqual([
439
+ "p-1",
440
+ "p-3"
441
+ ]);
442
+ });
231
443
  globalThis.test("unmapped kind throws", async () => {
232
444
  let threw;
233
445
  try {
234
- await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined), mkPayload("frobnicate", undefined, undefined, undefined));
446
+ await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), undefined, mkPayload("frobnicate", undefined, undefined, undefined));
235
447
  threw = false;
236
448
  } catch (exn) {
237
449
  threw = true;