@reventlessdev/reventless-aws 3.0.0-alpha.175 → 3.0.0-alpha.177

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 (76) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/package.json +10 -10
  3. package/src/Platform.res +127 -31
  4. package/src/Platform.res.mjs +226 -44
  5. package/src/adapter/DcbEventLog/DcbBackend.res +2 -2
  6. package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream.res +4 -1
  7. package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream.res.mjs +1 -1
  8. package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream_Runtime.res +14 -4
  9. package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream_Runtime.res.mjs +19 -14
  10. package/src/adapter/EventLog/EventLogBackend.res +82 -0
  11. package/src/adapter/EventLog/EventLogBackend.res.mjs +53 -0
  12. package/src/adapter/EventLog/EventLogStorage.res +15 -0
  13. package/src/adapter/EventLog/EventLogStorage.res.mjs +20 -1
  14. package/src/adapter/EventLog/EventLogStorage_Postgres.res +37 -0
  15. package/src/adapter/EventLog/EventLogStorage_Postgres.res.mjs +21 -0
  16. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +26 -11
  17. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +15 -6
  18. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +54 -0
  19. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs +23 -0
  20. package/src/adapter/Postgres/PgProjectionFeed.res +100 -0
  21. package/src/adapter/Postgres/PgProjectionFeed.res.mjs +54 -0
  22. package/src/adapter/QueryDb/PgQueryResolver_Builder.res +281 -0
  23. package/src/adapter/QueryDb/PgQueryResolver_Builder.res.mjs +189 -0
  24. package/src/adapter/QueryDb/PgQueryResolver_Lambda.res +335 -0
  25. package/src/adapter/QueryDb/PgQueryResolver_Lambda.res.mjs +252 -0
  26. package/src/adapter/QueryDb/QueryDbBackend.res +52 -0
  27. package/src/adapter/QueryDb/QueryDbBackend.res.mjs +44 -0
  28. package/src/adapter/QueryDb/QueryDbResolvers.res +49 -0
  29. package/src/adapter/QueryDb/QueryDbResolvers.res.mjs +20 -1
  30. package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res +264 -0
  31. package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res.mjs +164 -0
  32. package/src/adapter/QueryDb/QueryDbStorage.res +44 -0
  33. package/src/adapter/QueryDb/QueryDbStorage.res.mjs +34 -1
  34. package/src/adapter/QueryDb/QueryDbStorage_Postgres.res +47 -0
  35. package/src/adapter/QueryDb/QueryDbStorage_Postgres.res.mjs +21 -0
  36. package/src/adapter/QueryDb/QueryDbStorage_Postgres_Runtime.res +19 -0
  37. package/src/adapter/QueryDb/QueryDbStorage_Postgres_Runtime.res.mjs +15 -0
  38. package/src/adapter/QueryEngine/QueryEngine_DynamoDb.res +12 -1
  39. package/src/adapter/QueryEngine/QueryEngine_DynamoDb.res.mjs +2 -1
  40. package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res +83 -3
  41. package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res.mjs +46 -3
  42. package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res +83 -3
  43. package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res.mjs +48 -3
  44. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +130 -5
  45. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res.mjs +66 -6
  46. package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.mjs +15 -10
  47. package/src/adapter/Runtime/PgQueryResolverEntryPoint.mjs +88 -0
  48. package/src/adapter/Runtime/ReadModelEntryPoint.mjs +32 -15
  49. package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +26 -12
  50. package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res +138 -8
  51. package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res.mjs +76 -11
  52. package/src/components/Aggregate_Builder_Single.res +12 -5
  53. package/src/components/Aggregate_Builder_Single.res.mjs +12 -6
  54. package/src/components/Aggregate_Builder_Single_Async.res +12 -5
  55. package/src/components/Aggregate_Builder_Single_Async.res.mjs +12 -6
  56. package/src/components/ReadModel_Builder_NoResolver.res.mjs +1 -1
  57. package/src/components/ReadModel_Builder_NoResolver_Stream.res +13 -4
  58. package/src/components/ReadModel_Builder_NoResolver_Stream.res.mjs +14 -9
  59. package/src/components/ReadModel_Builder_Single.res +14 -5
  60. package/src/components/ReadModel_Builder_Single.res.mjs +15 -12
  61. package/src/components/ReadModel_Builder_Single_Stream.res +14 -5
  62. package/src/components/ReadModel_Builder_Single_Stream.res.mjs +15 -12
  63. package/src/components/StateViewSlice_Builder.res +10 -4
  64. package/src/components/StateViewSlice_Builder.res.mjs +8 -10
  65. package/src/components/StateViewSlice_Builder_Stream.res +10 -4
  66. package/src/components/StateViewSlice_Builder_Stream.res.mjs +8 -10
  67. package/src/plugin/runtime/PluginRuntime_Builder.res +12 -0
  68. package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +4 -0
  69. package/tests/EventCollectorChannelStreamRuntimeTest.res +55 -0
  70. package/tests/EventCollectorChannelStreamRuntimeTest.res.mjs +78 -0
  71. package/tests/PgChangeFeedRelay_IntegrationTest.res +115 -4
  72. package/tests/PgChangeFeedRelay_IntegrationTest.res.mjs +84 -0
  73. package/tests/PgPipeline_IntegrationTest.res +208 -0
  74. package/tests/PgPipeline_IntegrationTest.res.mjs +260 -0
  75. package/tests/PgQueryResolver_LambdaTest.res +308 -0
  76. package/tests/PgQueryResolver_LambdaTest.res.mjs +392 -0
@@ -0,0 +1,308 @@
1
+ // Unit tests for the Postgres GraphQL-read Lambda dispatcher (B3.2a-2).
2
+ //
3
+ // `PgQueryResolver_Lambda.dispatch` is pure over a `binding`, so this exercises
4
+ // kind routing, arg extraction, spec-level authorization, the queryInterceptor
5
+ // hook, and the list push-down/fallback split with an IN-MEMORY mock binding —
6
+ // no Postgres. SQL correctness lives in QueryEnginePostgres and is covered by
7
+ // its PG_URL-gated parity test.
8
+
9
+ open JestGlobals
10
+ open ReventlessCore
11
+
12
+ @val external btoa: string => string = "btoa"
13
+
14
+ let mk = (id, status, name) => {
15
+ let o = Dict.make()
16
+ o->Dict.set("id", JSON.Encode.string(id))
17
+ o->Dict.set("status", JSON.Encode.string(status))
18
+ o->Dict.set("name", JSON.Encode.string(name))
19
+ JSON.Encode.object(o)
20
+ }
21
+
22
+ let store = Dict.fromArray([
23
+ ("p-1", mk("p-1", "active", "Alpha")),
24
+ ("p-2", mk("p-2", "inactive", "Bravo")),
25
+ ("p-3", mk("p-3", "active", "Charlie")),
26
+ ])
27
+ let allItems = () => store->Dict.valuesToArray
28
+
29
+ let fieldEq = (item, field, value) =>
30
+ item
31
+ ->JSON.Decode.object
32
+ ->Option.flatMap(d => d->Dict.get(field))
33
+ ->Option.flatMap(JSON.Decode.string)
34
+ ->Option.mapOr(false, v => v == value)
35
+
36
+ // Records whether listPage was consulted, and what it returns (sentinel or None).
37
+ let listPageReturn: ref<option<JSON.t>> = ref(None)
38
+
39
+ let ops: QueryDb_Adapter.operations = {
40
+ load: async id => Ok(store->Dict.get(id)->Option.mapOr([], i => [i])),
41
+ loadStream: Obj.magic(0), // never called by dispatch
42
+ save: Obj.magic(0),
43
+ saveBatch: Obj.magic(0),
44
+ count: Obj.magic(0),
45
+ delete: Obj.magic(0),
46
+ deleteBatch: Obj.magic(0),
47
+ }
48
+
49
+ let pushdowns: PgQueryResolver_Lambda.pushdowns = {
50
+ indexLookup: async (~readModelName as _, field, value) =>
51
+ allItems()->Array.filter(i => fieldEq(i, field, value)),
52
+ byIds: async (~readModelName as _, ids) =>
53
+ ids->Array.filterMap(id => store->Dict.get(id)),
54
+ listPage: async (~readModelName as _, ~argsDict as _, ~capability as _, ~labelField as _) =>
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))])),
59
+ scanAll: async (~readModelName as _) => allItems(),
60
+ }
61
+
62
+ let capability: GraphQL_FragmentGenerator.serverCapability = {
63
+ filterFields: [{name: "status", gqlType: "String", range: false}],
64
+ sortFields: ["name", "status"],
65
+ }
66
+
67
+ let makeBinding = (
68
+ ~authorization=Reventless.Authorization.AllowAnonymous,
69
+ ~subIdField=None,
70
+ (),
71
+ ): PgQueryResolver_Lambda.binding => {
72
+ ops,
73
+ pushdowns,
74
+ // "byStatus" index stored on the `status` field.
75
+ indexes: [
76
+ {index: "byStatus", type_: "S", idField: "status", projectionType: Reventless.ReadModel.ALL},
77
+ ],
78
+ subIdField,
79
+ capability,
80
+ labelField: "name",
81
+ includeIdParam: true,
82
+ authorization,
83
+ }
84
+
85
+ let mkPayload = (~kind, ~index=?, ~args=JSON.Encode.object(Dict.make()), ()): PgQueryResolver_Lambda.payload => {
86
+ readModelName: "Things",
87
+ kind,
88
+ ?index,
89
+ arguments: args,
90
+ identity: Reventless.Identity.anonymous,
91
+ }
92
+
93
+ let objArgs = pairs => JSON.Encode.object(Dict.fromArray(pairs))
94
+ let field = (j, k) => j->JSON.Decode.object->Option.flatMap(d => d->Dict.get(k))
95
+ let str = (j, k) => j->field(k)->Option.flatMap(JSON.Decode.string)
96
+ let ids = (arr: JSON.t) =>
97
+ arr
98
+ ->JSON.Decode.array
99
+ ->Option.getOr([])
100
+ ->Array.filterMap(i => i->str("id"))
101
+ ->Array.toSorted(String.compare)
102
+
103
+ describe("PgQueryResolver_Lambda.dispatch", () => {
104
+ testPromise("getById returns the item (id-carrying)", async () => {
105
+ let r = await PgQueryResolver_Lambda.dispatch(
106
+ ~binding=makeBinding(),
107
+ ~payload=mkPayload(~kind="getById", ~args=objArgs([("id", JSON.Encode.string("p-2"))]), ()),
108
+ )
109
+ expect(r->str("id"))->toEqual(Some("p-2"))
110
+ expect(r->str("name"))->toEqual(Some("Bravo"))
111
+ })
112
+
113
+ testPromise("getById miss → null", async () => {
114
+ let r = await PgQueryResolver_Lambda.dispatch(
115
+ ~binding=makeBinding(),
116
+ ~payload=mkPayload(~kind="getById", ~args=objArgs([("id", JSON.Encode.string("absent"))]), ()),
117
+ )
118
+ expect(r)->toBe(JSON.Encode.null)
119
+ })
120
+
121
+ testPromise("byIds returns matches, drops missing", async () => {
122
+ let r = await PgQueryResolver_Lambda.dispatch(
123
+ ~binding=makeBinding(),
124
+ ~payload=mkPayload(
125
+ ~kind="byIds",
126
+ ~args=objArgs([
127
+ ("ids", JSON.Encode.array(["p-1", "absent", "p-3"]->Array.map(JSON.Encode.string))),
128
+ ]),
129
+ (),
130
+ ),
131
+ )
132
+ expect(r->ids)->toEqual(["p-1", "p-3"])
133
+ })
134
+
135
+ testPromise("index routes to indexLookup on the idField", async () => {
136
+ let r = await PgQueryResolver_Lambda.dispatch(
137
+ ~binding=makeBinding(),
138
+ ~payload=mkPayload(
139
+ ~kind="index",
140
+ ~index="byStatus",
141
+ // arg name is the index name; value looked up against idField `status`
142
+ ~args=objArgs([("byStatus", JSON.Encode.string("active"))]),
143
+ (),
144
+ ),
145
+ )
146
+ expect(r->ids)->toEqual(["p-1", "p-3"])
147
+ })
148
+
149
+ testPromise("list with push-down → returns the connection listPage gives", async () => {
150
+ listPageReturn := Some(JSON.Encode.string("PUSHED"))
151
+ let r = await PgQueryResolver_Lambda.dispatch(
152
+ ~binding=makeBinding(),
153
+ ~payload=mkPayload(~kind="list", ()),
154
+ )
155
+ listPageReturn := None
156
+ expect(r)->toEqual(JSON.Encode.string("PUSHED"))
157
+ })
158
+
159
+ testPromise("list without push-down → falls back to the shared spec", async () => {
160
+ listPageReturn := None
161
+ let r = await PgQueryResolver_Lambda.dispatch(
162
+ ~binding=makeBinding(),
163
+ ~payload=mkPayload(~kind="list", ()),
164
+ )
165
+ // A connection object: edges present, all 3 items, sorted by id.
166
+ let edgeIds =
167
+ r
168
+ ->field("edges")
169
+ ->Option.flatMap(JSON.Decode.array)
170
+ ->Option.getOr([])
171
+ ->Array.filterMap(e => e->field("node")->Option.flatMap(n => n->str("id")))
172
+ expect(edgeIds)->toEqual(["p-1", "p-2", "p-3"])
173
+ })
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
+
191
+ testPromise("authorization DenyAll → empty shape per kind", async () => {
192
+ let b = makeBinding(~authorization=DenyAll, ())
193
+ let single = await PgQueryResolver_Lambda.dispatch(
194
+ ~binding=b,
195
+ ~payload=mkPayload(~kind="getById", ~args=objArgs([("id", JSON.Encode.string("p-1"))]), ()),
196
+ )
197
+ expect(single)->toBe(JSON.Encode.null)
198
+ let list = await PgQueryResolver_Lambda.dispatch(~binding=b, ~payload=mkPayload(~kind="list", ()))
199
+ expect(list->field("edges")->Option.flatMap(JSON.Decode.array)->Option.getOr([])->Array.length)->toBe(0)
200
+ let byIds = await PgQueryResolver_Lambda.dispatch(
201
+ ~binding=b,
202
+ ~payload=mkPayload(~kind="byIds", ~args=objArgs([("ids", JSON.Encode.array([]))]), ()),
203
+ )
204
+ expect(byIds->JSON.Decode.array->Option.getOr([])->Array.length)->toBe(0)
205
+ })
206
+
207
+ testPromise("queryInterceptor hook Deny → empty shape (auth allowed)", async () => {
208
+ QueryDb_Callback.registerQueryInterceptor(async (~identity as _, ~readModelName as _, ~args as _) =>
209
+ Deny("nope")
210
+ )
211
+ let r = await PgQueryResolver_Lambda.dispatch(
212
+ ~binding=makeBinding(),
213
+ ~payload=mkPayload(~kind="getById", ~args=objArgs([("id", JSON.Encode.string("p-1"))]), ()),
214
+ )
215
+ QueryDb_Callback.clearQueryInterceptor()
216
+ expect(r)->toBe(JSON.Encode.null)
217
+ })
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
+ testPromise("unmapped kind throws", async () => {
297
+ let threw = try {
298
+ let _ = await PgQueryResolver_Lambda.dispatch(
299
+ ~binding=makeBinding(),
300
+ ~payload=mkPayload(~kind="frobnicate", ()),
301
+ )
302
+ false
303
+ } catch {
304
+ | _ => true
305
+ }
306
+ expect(threw)->toBe(true)
307
+ })
308
+ })
@@ -0,0 +1,392 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
4
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
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";
7
+ import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
8
+ import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
9
+ import * as QueryDb_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/QueryDb/QueryDb_Callback.res.mjs";
10
+ import * as PgQueryResolver_Lambda$ReventlessAws from "../src/adapter/QueryDb/PgQueryResolver_Lambda.res.mjs";
11
+
12
+ function mk(id, status, name) {
13
+ let o = {};
14
+ o["id"] = id;
15
+ o["status"] = status;
16
+ o["name"] = name;
17
+ return o;
18
+ }
19
+
20
+ let store = Object.fromEntries([
21
+ [
22
+ "p-1",
23
+ mk("p-1", "active", "Alpha")
24
+ ],
25
+ [
26
+ "p-2",
27
+ mk("p-2", "inactive", "Bravo")
28
+ ],
29
+ [
30
+ "p-3",
31
+ mk("p-3", "active", "Charlie")
32
+ ]
33
+ ]);
34
+
35
+ function allItems() {
36
+ return Object.values(store);
37
+ }
38
+
39
+ function fieldEq(item, field, value) {
40
+ return Stdlib_Option.mapOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[field]), Stdlib_JSON.Decode.string), false, v => v === value);
41
+ }
42
+
43
+ let listPageReturn = {
44
+ contents: undefined
45
+ };
46
+
47
+ async function ops_load(id) {
48
+ return {
49
+ TAG: "Ok",
50
+ _0: Stdlib_Option.mapOr(store[id], [], i => [i])
51
+ };
52
+ }
53
+
54
+ let ops = {
55
+ load: ops_load,
56
+ loadStream: 0,
57
+ save: 0,
58
+ saveBatch: 0,
59
+ count: 0,
60
+ delete: 0,
61
+ deleteBatch: 0
62
+ };
63
+
64
+ async function pushdowns_indexLookup(param, field, value) {
65
+ return Object.values(store).filter(i => fieldEq(i, field, value));
66
+ }
67
+
68
+ async function pushdowns_byIds(param, ids) {
69
+ return Stdlib_Array.filterMap(ids, id => store[id]);
70
+ }
71
+
72
+ async function pushdowns_listPage(param, param$1, param$2, param$3) {
73
+ return listPageReturn.contents;
74
+ }
75
+
76
+ async function pushdowns_itemsPage(param, param$1, id, param$2) {
77
+ return Object.fromEntries([[
78
+ "itemsFor",
79
+ id
80
+ ]]);
81
+ }
82
+
83
+ async function pushdowns_scanAll(param) {
84
+ return Object.values(store);
85
+ }
86
+
87
+ let pushdowns = {
88
+ indexLookup: pushdowns_indexLookup,
89
+ byIds: pushdowns_byIds,
90
+ listPage: pushdowns_listPage,
91
+ itemsPage: pushdowns_itemsPage,
92
+ scanAll: pushdowns_scanAll
93
+ };
94
+
95
+ let capability_filterFields = [{
96
+ name: "status",
97
+ gqlType: "String",
98
+ range: false
99
+ }];
100
+
101
+ let capability_sortFields = [
102
+ "name",
103
+ "status"
104
+ ];
105
+
106
+ let capability = {
107
+ filterFields: capability_filterFields,
108
+ sortFields: capability_sortFields
109
+ };
110
+
111
+ function makeBinding(authorizationOpt, subIdFieldOpt, param) {
112
+ let authorization = authorizationOpt !== undefined ? authorizationOpt : "AllowAnonymous";
113
+ let subIdField = subIdFieldOpt !== undefined ? Primitive_option.valFromOption(subIdFieldOpt) : undefined;
114
+ return {
115
+ ops: ops,
116
+ pushdowns: pushdowns,
117
+ indexes: [{
118
+ index: "byStatus",
119
+ type_: "S",
120
+ idField: "status",
121
+ projectionType: "ALL"
122
+ }],
123
+ subIdField: subIdField,
124
+ capability: capability,
125
+ labelField: "name",
126
+ includeIdParam: true,
127
+ authorization: authorization
128
+ };
129
+ }
130
+
131
+ function mkPayload(kind, index, argsOpt, param) {
132
+ let args = argsOpt !== undefined ? argsOpt : ({});
133
+ return {
134
+ readModelName: "Things",
135
+ kind: kind,
136
+ index: index,
137
+ arguments: args,
138
+ identity: Identity$Reventless.anonymous
139
+ };
140
+ }
141
+
142
+ function objArgs(pairs) {
143
+ return Object.fromEntries(pairs);
144
+ }
145
+
146
+ function field(j, k) {
147
+ return Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(j), d => d[k]);
148
+ }
149
+
150
+ function str(j, k) {
151
+ return Stdlib_Option.flatMap(field(j, k), Stdlib_JSON.Decode.string);
152
+ }
153
+
154
+ function ids(arr) {
155
+ return Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_JSON.Decode.array(arr), []), i => str(i, "id")).toSorted(Primitive_string.compare);
156
+ }
157
+
158
+ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
159
+ globalThis.test("getById returns the item (id-carrying)", async () => {
160
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), mkPayload("getById", undefined, Object.fromEntries([[
161
+ "id",
162
+ "p-2"
163
+ ]]), undefined));
164
+ globalThis.expect(str(r, "id")).toEqual("p-2");
165
+ globalThis.expect(str(r, "name")).toEqual("Bravo");
166
+ });
167
+ globalThis.test("getById miss → null", async () => {
168
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), mkPayload("getById", undefined, Object.fromEntries([[
169
+ "id",
170
+ "absent"
171
+ ]]), undefined));
172
+ globalThis.expect(r).toBe(null);
173
+ });
174
+ globalThis.test("byIds returns matches, drops missing", async () => {
175
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), mkPayload("byIds", undefined, Object.fromEntries([[
176
+ "ids",
177
+ [
178
+ "p-1",
179
+ "absent",
180
+ "p-3"
181
+ ].map(prim => prim)
182
+ ]]), undefined));
183
+ globalThis.expect(ids(r)).toEqual([
184
+ "p-1",
185
+ "p-3"
186
+ ]);
187
+ });
188
+ globalThis.test("index routes to indexLookup on the idField", async () => {
189
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), mkPayload("index", "byStatus", Object.fromEntries([[
190
+ "byStatus",
191
+ "active"
192
+ ]]), undefined));
193
+ globalThis.expect(ids(r)).toEqual([
194
+ "p-1",
195
+ "p-3"
196
+ ]);
197
+ });
198
+ globalThis.test("list with push-down → returns the connection listPage gives", async () => {
199
+ listPageReturn.contents = "PUSHED";
200
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), mkPayload("list", undefined, undefined, undefined));
201
+ listPageReturn.contents = undefined;
202
+ globalThis.expect(r).toEqual("PUSHED");
203
+ });
204
+ globalThis.test("list without push-down → falls back to the shared spec", async () => {
205
+ listPageReturn.contents = undefined;
206
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), mkPayload("list", undefined, undefined, undefined));
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")));
208
+ globalThis.expect(edgeIds).toEqual([
209
+ "p-1",
210
+ "p-2",
211
+ "p-3"
212
+ ]);
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), 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), 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
+ });
228
+ globalThis.test("authorization DenyAll → empty shape per kind", async () => {
229
+ let b = makeBinding("DenyAll", undefined, undefined);
230
+ let single = await PgQueryResolver_Lambda$ReventlessAws.dispatch(b, mkPayload("getById", undefined, Object.fromEntries([[
231
+ "id",
232
+ "p-1"
233
+ ]]), undefined));
234
+ globalThis.expect(single).toBe(null);
235
+ let list = await PgQueryResolver_Lambda$ReventlessAws.dispatch(b, mkPayload("list", undefined, undefined, undefined));
236
+ globalThis.expect(Stdlib_Option.getOr(Stdlib_Option.flatMap(field(list, "edges"), Stdlib_JSON.Decode.array), []).length).toBe(0);
237
+ let byIds = await PgQueryResolver_Lambda$ReventlessAws.dispatch(b, mkPayload("byIds", undefined, Object.fromEntries([[
238
+ "ids",
239
+ []
240
+ ]]), undefined));
241
+ globalThis.expect(Stdlib_Option.getOr(Stdlib_JSON.Decode.array(byIds), []).length).toBe(0);
242
+ });
243
+ globalThis.test("queryInterceptor hook Deny → empty shape (auth allowed)", async () => {
244
+ QueryDb_Callback$ReventlessCore.registerQueryInterceptor(async (param, param$1, param$2) => ({
245
+ TAG: "Deny",
246
+ _0: "nope"
247
+ }));
248
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), mkPayload("getById", undefined, Object.fromEntries([[
249
+ "id",
250
+ "p-1"
251
+ ]]), undefined));
252
+ QueryDb_Callback$ReventlessCore.clearQueryInterceptor();
253
+ globalThis.expect(r).toBe(null);
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), 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), 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), 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
+ globalThis.test("unmapped kind throws", async () => {
365
+ let threw;
366
+ try {
367
+ await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined), mkPayload("frobnicate", undefined, undefined, undefined));
368
+ threw = false;
369
+ } catch (exn) {
370
+ threw = true;
371
+ }
372
+ globalThis.expect(threw).toBe(true);
373
+ });
374
+ });
375
+
376
+ export {
377
+ mk,
378
+ store,
379
+ allItems,
380
+ fieldEq,
381
+ listPageReturn,
382
+ ops,
383
+ pushdowns,
384
+ capability,
385
+ makeBinding,
386
+ mkPayload,
387
+ objArgs,
388
+ field,
389
+ str,
390
+ ids,
391
+ }
392
+ /* store Not a pure module */