@reventlessdev/reventless-aws 3.0.0-alpha.176 → 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.
- package/CHANGELOG.md +9 -0
- package/package.json +6 -6
- package/src/Platform.res +12 -0
- package/src/Platform.res.mjs +9 -0
- package/src/adapter/QueryDb/PgQueryResolver_Builder.res +281 -0
- package/src/adapter/QueryDb/PgQueryResolver_Builder.res.mjs +189 -0
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res +152 -9
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res.mjs +115 -4
- package/src/adapter/QueryDb/QueryDbResolvers.res +6 -5
- package/src/adapter/QueryDb/QueryDbResolvers.res.mjs +6 -3
- package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res +264 -0
- package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res.mjs +164 -0
- package/src/adapter/QueryDb/QueryDbStorage_Postgres.res +5 -1
- package/src/adapter/QueryDb/QueryDbStorage_Postgres.res.mjs +3 -3
- package/src/adapter/Runtime/PgQueryResolverEntryPoint.mjs +88 -0
- package/tests/PgQueryResolver_LambdaTest.res +104 -2
- package/tests/PgQueryResolver_LambdaTest.res.mjs +144 -11
|
@@ -19,12 +19,28 @@
|
|
|
19
19
|
|
|
20
20
|
let log = ReventlessCore.Logger.fromEnv()
|
|
21
21
|
|
|
22
|
+
@val external atob: string => string = "atob"
|
|
23
|
+
|
|
24
|
+
// Where a cross-table resolver reads the target's sort-key value from.
|
|
25
|
+
// `kind`: "field" (the parent object) | "arg" (the GraphQL arguments).
|
|
26
|
+
type subIdSource = {kind: string, name: string}
|
|
27
|
+
|
|
22
28
|
// Wire payload from the resolver template. `arguments` mirrors the field name the
|
|
23
29
|
// other Invoke templates use (QueryInterceptor_Lambda, invokeCommandGenerator).
|
|
30
|
+
// The cross-table fields (B3.2c) are present only for resolveOne/resolveMany:
|
|
24
31
|
type payload = {
|
|
25
32
|
readModelName: string,
|
|
26
33
|
kind: string,
|
|
27
34
|
index?: string,
|
|
35
|
+
// Cross-table (@resolves/@resolvesMany) — see B3.2c dispatch below.
|
|
36
|
+
target?: string,
|
|
37
|
+
source?: JSON.t,
|
|
38
|
+
sourceIdField?: string,
|
|
39
|
+
sourceIdsField?: string,
|
|
40
|
+
sourceSubId?: subIdSource,
|
|
41
|
+
targetIndex?: string,
|
|
42
|
+
targetIndexIdField?: string,
|
|
43
|
+
multi?: bool,
|
|
28
44
|
arguments: JSON.t,
|
|
29
45
|
identity: Reventless.Identity.t,
|
|
30
46
|
}
|
|
@@ -40,6 +56,13 @@ type pushdowns = {
|
|
|
40
56
|
~capability: ReventlessCore.GraphQL_FragmentGenerator.serverCapability,
|
|
41
57
|
~labelField: string,
|
|
42
58
|
) => promise<option<JSON.t>>,
|
|
59
|
+
// Sub-id connection ({single}Items) — keyset over sub_key within a partition.
|
|
60
|
+
itemsPage: (
|
|
61
|
+
~readModelName: string,
|
|
62
|
+
~subIdField: string,
|
|
63
|
+
~id: string,
|
|
64
|
+
~argsDict: dict<JSON.t>,
|
|
65
|
+
) => promise<JSON.t>,
|
|
43
66
|
// Full materialisation for the list fallback (shapes listPage declines).
|
|
44
67
|
scanAll: (~readModelName: string) => promise<array<JSON.t>>,
|
|
45
68
|
}
|
|
@@ -111,10 +134,12 @@ let dispatch = async (~binding: binding, ~payload: payload): JSON.t => {
|
|
|
111
134
|
let rm = payload.readModelName
|
|
112
135
|
switch await runInterceptor(~binding, ~payload) {
|
|
113
136
|
| Deny(_) =>
|
|
114
|
-
// Empty shape per kind (null for single,
|
|
137
|
+
// Empty shape per kind (null for single, connection for list/items, [] else).
|
|
138
|
+
let isMulti = payload.multi->Option.getOr(false)
|
|
115
139
|
switch payload.kind {
|
|
116
140
|
| "getById" => JSON.Encode.null
|
|
117
|
-
| "
|
|
141
|
+
| "resolveOne" if !isMulti => JSON.Encode.null
|
|
142
|
+
| "list" | "items" => emptyConnection()
|
|
118
143
|
| _ => JSON.Encode.array([])
|
|
119
144
|
}
|
|
120
145
|
| Allow =>
|
|
@@ -134,6 +159,21 @@ let dispatch = async (~binding: binding, ~payload: payload): JSON.t => {
|
|
|
134
159
|
let ids = payload.arguments->argStrs("ids")
|
|
135
160
|
JSON.Encode.array(await binding.pushdowns.byIds(~readModelName=rm, ids))
|
|
136
161
|
|
|
162
|
+
| "items" =>
|
|
163
|
+
// Sub-id connection: {single}Items(id, filter, first/after/last/before).
|
|
164
|
+
// Requires a subIdField; without one it's an empty connection.
|
|
165
|
+
switch binding.subIdField {
|
|
166
|
+
| Some(subIdField) =>
|
|
167
|
+
let id = payload.arguments->argStr("id")->Option.getOr("")
|
|
168
|
+
await binding.pushdowns.itemsPage(
|
|
169
|
+
~readModelName=rm,
|
|
170
|
+
~subIdField,
|
|
171
|
+
~id,
|
|
172
|
+
~argsDict=payload.arguments->argObj,
|
|
173
|
+
)
|
|
174
|
+
| None => emptyConnection()
|
|
175
|
+
}
|
|
176
|
+
|
|
137
177
|
| "index" =>
|
|
138
178
|
// The index arg name is the index; its stored field is idField ?? index.
|
|
139
179
|
let indexName = payload.index->Option.getOr("")
|
|
@@ -169,6 +209,55 @@ let dispatch = async (~binding: binding, ~payload: payload): JSON.t => {
|
|
|
169
209
|
)
|
|
170
210
|
}
|
|
171
211
|
|
|
212
|
+
// Cross-table single-ID field resolver (@resolves). `binding` is the TARGET
|
|
213
|
+
// binding (the handler looks it up by payload.target); the key comes from the
|
|
214
|
+
// parent object (payload.source[sourceIdField]).
|
|
215
|
+
| "resolveOne" =>
|
|
216
|
+
let target = payload.target->Option.getOr(rm)
|
|
217
|
+
let source = payload.source->Option.getOr(JSON.Encode.null)
|
|
218
|
+
let key = source->argStr(payload.sourceIdField->Option.getOr(""))->Option.getOr("")
|
|
219
|
+
let items = switch payload.targetIndex {
|
|
220
|
+
| Some(ix) =>
|
|
221
|
+
await binding.pushdowns.indexLookup(
|
|
222
|
+
~readModelName=target,
|
|
223
|
+
payload.targetIndexIdField->Option.getOr(ix),
|
|
224
|
+
key,
|
|
225
|
+
)
|
|
226
|
+
| None =>
|
|
227
|
+
switch await binding.ops.load(key) {
|
|
228
|
+
| Ok(items) => items
|
|
229
|
+
| Error(_) => []
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// Optional target sort-key filter (source field or GraphQL arg).
|
|
233
|
+
let filtered = switch (payload.sourceSubId, binding.subIdField) {
|
|
234
|
+
| (Some({kind, name}), Some(subField)) =>
|
|
235
|
+
let subVal =
|
|
236
|
+
switch kind {
|
|
237
|
+
| "arg" => payload.arguments->argStr(name)
|
|
238
|
+
| _ => source->argStr(name)
|
|
239
|
+
}->Option.getOr("")
|
|
240
|
+
items->Array.filter(it => it->argStr(subField)->Option.getOr("") == subVal)
|
|
241
|
+
| _ => items
|
|
242
|
+
}
|
|
243
|
+
if payload.multi->Option.getOr(false) {
|
|
244
|
+
JSON.Encode.array(filtered)
|
|
245
|
+
} else {
|
|
246
|
+
switch filtered->Array.get(0) {
|
|
247
|
+
| Some(item) => item
|
|
248
|
+
| None => JSON.Encode.null
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Cross-table batch field resolver (@resolvesMany). ids come from the parent
|
|
253
|
+
// object (payload.source[sourceIdsField]); BatchGet the target by partition
|
|
254
|
+
// key (missing ids drop out).
|
|
255
|
+
| "resolveMany" =>
|
|
256
|
+
let target = payload.target->Option.getOr(rm)
|
|
257
|
+
let source = payload.source->Option.getOr(JSON.Encode.null)
|
|
258
|
+
let ids = source->argStrs(payload.sourceIdsField->Option.getOr(""))
|
|
259
|
+
JSON.Encode.array(await binding.pushdowns.byIds(~readModelName=target, ids))
|
|
260
|
+
|
|
172
261
|
| other =>
|
|
173
262
|
// Fail loudly on unmapped kinds (feature-parity guard, per the B3.2 plan).
|
|
174
263
|
log.error(~comp="PgQueryResolver_Lambda", `unmapped resolver kind '${other}' for ${rm}`)
|
|
@@ -182,11 +271,65 @@ let bindings: dict<binding> = Dict.make()
|
|
|
182
271
|
let register = (~readModelName: string, binding: binding): unit =>
|
|
183
272
|
bindings->Dict.set(readModelName, binding)
|
|
184
273
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
274
|
+
// Relay node type → read-model-name map (B3.2c), populated at Lambda init from
|
|
275
|
+
// the deploy-time registerNodeType calls.
|
|
276
|
+
let nodeTypeMap: dict<string> = Dict.make()
|
|
277
|
+
let registerNodeType = (~typeName: string, ~readModelName: string): unit =>
|
|
278
|
+
nodeTypeMap->Dict.set(typeName, readModelName)
|
|
279
|
+
|
|
280
|
+
// node(id: ID!) — decode the global id (base64 of `typeName:localId`, matching
|
|
281
|
+
// AppSync's nodeDecodeGlobalId), map typeName → read model, load by localId,
|
|
282
|
+
// return the item tagged with __typename and the original global id. Runs the
|
|
283
|
+
// target's authorization/interceptor. Self-contained (getById returns raw ids,
|
|
284
|
+
// so no encoding elsewhere is affected).
|
|
285
|
+
let handleNode = async (~payload: payload): JSON.t => {
|
|
286
|
+
let globalId = payload.arguments->argStr("id")->Option.getOr("")
|
|
287
|
+
let decoded = try atob(globalId) catch {
|
|
288
|
+
| _ => ""
|
|
289
|
+
}
|
|
290
|
+
let colonIdx = decoded->String.indexOf(":")
|
|
291
|
+
if colonIdx <= 0 {
|
|
292
|
+
JSON.Encode.null
|
|
293
|
+
} else {
|
|
294
|
+
let typeName = decoded->String.slice(~start=0, ~end=colonIdx)
|
|
295
|
+
let localId = decoded->String.slice(~start=colonIdx + 1, ~end=decoded->String.length)
|
|
296
|
+
switch nodeTypeMap->Dict.get(typeName)->Option.flatMap(rm => bindings->Dict.get(rm)) {
|
|
297
|
+
| Some(binding) =>
|
|
298
|
+
switch await runInterceptor(~binding, ~payload) {
|
|
299
|
+
| Deny(_) => JSON.Encode.null
|
|
300
|
+
| Allow =>
|
|
301
|
+
switch await binding.ops.load(localId) {
|
|
302
|
+
| Ok(items) =>
|
|
303
|
+
switch items->Array.get(0) {
|
|
304
|
+
| Some(item) =>
|
|
305
|
+
let obj = item->JSON.Decode.object->Option.getOr(Dict.make())->Dict.copy
|
|
306
|
+
obj->Dict.set("__typename", JSON.Encode.string(typeName))
|
|
307
|
+
obj->Dict.set("id", JSON.Encode.string(globalId))
|
|
308
|
+
JSON.Encode.object(obj)
|
|
309
|
+
| None => JSON.Encode.null
|
|
310
|
+
}
|
|
311
|
+
| Error(_) => JSON.Encode.null
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
| None => JSON.Encode.null
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
let handler = async (payload: payload, _context) => {
|
|
320
|
+
// node decodes its own target; the cross-table field resolvers dispatch against
|
|
321
|
+
// the TARGET binding; everything else against the payload's own read model.
|
|
322
|
+
let bindingKey = switch payload.kind {
|
|
323
|
+
| "resolveOne" | "resolveMany" => payload.target->Option.getOr(payload.readModelName)
|
|
324
|
+
| _ => payload.readModelName
|
|
192
325
|
}
|
|
326
|
+
switch payload.kind {
|
|
327
|
+
| "node" => await handleNode(~payload)
|
|
328
|
+
| _ =>
|
|
329
|
+
switch bindings->Dict.get(bindingKey) {
|
|
330
|
+
| Some(binding) => await dispatch(~binding, ~payload)
|
|
331
|
+
| None =>
|
|
332
|
+
JsError.throwWithMessage("PgQueryResolver: no binding registered for read model " ++ bindingKey)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
@@ -85,6 +85,13 @@ async function dispatch(binding, payload) {
|
|
|
85
85
|
let field = Stdlib_Option.getOr(Stdlib_Option.flatMap(binding.indexes.find(ic => ic.index === indexName), ic => ic.idField), indexName);
|
|
86
86
|
let value = Stdlib_Option.getOr(argStr(payload.arguments, indexName), "");
|
|
87
87
|
return await binding.pushdowns.indexLookup(rm, field, value);
|
|
88
|
+
case "items" :
|
|
89
|
+
let subIdField = binding.subIdField;
|
|
90
|
+
if (subIdField === undefined) {
|
|
91
|
+
return emptyConnection();
|
|
92
|
+
}
|
|
93
|
+
let id$1 = Stdlib_Option.getOr(argStr(payload.arguments, "id"), "");
|
|
94
|
+
return await binding.pushdowns.itemsPage(rm, subIdField, id$1, argObj(payload.arguments));
|
|
88
95
|
case "list" :
|
|
89
96
|
let argsDict = argObj(payload.arguments);
|
|
90
97
|
let conn = await binding.pushdowns.listPage(rm, argsDict, binding.capability, binding.labelField);
|
|
@@ -93,17 +100,62 @@ async function dispatch(binding, payload) {
|
|
|
93
100
|
}
|
|
94
101
|
let items$1 = await binding.pushdowns.scanAll(rm);
|
|
95
102
|
return QueryDbListQuery$ReventlessCore.run(items$1, argsDict, binding.capability, binding.labelField, param => {});
|
|
103
|
+
case "resolveMany" :
|
|
104
|
+
let target = Stdlib_Option.getOr(payload.target, rm);
|
|
105
|
+
let source = Stdlib_Option.getOr(payload.source, null);
|
|
106
|
+
let ids$1 = argStrs(source, Stdlib_Option.getOr(payload.sourceIdsField, ""));
|
|
107
|
+
return await binding.pushdowns.byIds(target, ids$1);
|
|
108
|
+
case "resolveOne" :
|
|
109
|
+
let target$1 = Stdlib_Option.getOr(payload.target, rm);
|
|
110
|
+
let source$1 = Stdlib_Option.getOr(payload.source, null);
|
|
111
|
+
let key = Stdlib_Option.getOr(argStr(source$1, Stdlib_Option.getOr(payload.sourceIdField, "")), "");
|
|
112
|
+
let ix = payload.targetIndex;
|
|
113
|
+
let items$2;
|
|
114
|
+
if (ix !== undefined) {
|
|
115
|
+
items$2 = await binding.pushdowns.indexLookup(target$1, Stdlib_Option.getOr(payload.targetIndexIdField, ix), key);
|
|
116
|
+
} else {
|
|
117
|
+
let items$3 = await binding.ops.load(key);
|
|
118
|
+
items$2 = items$3.TAG === "Ok" ? items$3._0 : [];
|
|
119
|
+
}
|
|
120
|
+
let match$1 = payload.sourceSubId;
|
|
121
|
+
let match$2 = binding.subIdField;
|
|
122
|
+
let filtered;
|
|
123
|
+
if (match$1 !== undefined && match$2 !== undefined) {
|
|
124
|
+
let name = match$1.name;
|
|
125
|
+
let tmp = match$1.kind === "arg" ? argStr(payload.arguments, name) : argStr(source$1, name);
|
|
126
|
+
let subVal = Stdlib_Option.getOr(tmp, "");
|
|
127
|
+
filtered = items$2.filter(it => Stdlib_Option.getOr(argStr(it, match$2), "") === subVal);
|
|
128
|
+
} else {
|
|
129
|
+
filtered = items$2;
|
|
130
|
+
}
|
|
131
|
+
if (Stdlib_Option.getOr(payload.multi, false)) {
|
|
132
|
+
return filtered;
|
|
133
|
+
}
|
|
134
|
+
let item$1 = filtered[0];
|
|
135
|
+
if (item$1 !== undefined) {
|
|
136
|
+
return item$1;
|
|
137
|
+
} else {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
96
140
|
default:
|
|
97
141
|
log.error("PgQueryResolver_Lambda", undefined, `unmapped resolver kind '` + other + `' for ` + rm);
|
|
98
142
|
return Stdlib_JsError.throwWithMessage(`PgQueryResolver: unmapped resolver kind '` + other + `' for ` + rm);
|
|
99
143
|
}
|
|
100
144
|
} else {
|
|
101
|
-
let
|
|
102
|
-
|
|
145
|
+
let isMulti = Stdlib_Option.getOr(payload.multi, false);
|
|
146
|
+
let match$3 = payload.kind;
|
|
147
|
+
switch (match$3) {
|
|
103
148
|
case "getById" :
|
|
104
149
|
return null;
|
|
150
|
+
case "items" :
|
|
105
151
|
case "list" :
|
|
106
152
|
return emptyConnection();
|
|
153
|
+
case "resolveOne" :
|
|
154
|
+
if (isMulti) {
|
|
155
|
+
return [];
|
|
156
|
+
} else {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
107
159
|
default:
|
|
108
160
|
return [];
|
|
109
161
|
}
|
|
@@ -116,12 +168,68 @@ function register(readModelName, binding) {
|
|
|
116
168
|
bindings[readModelName] = binding;
|
|
117
169
|
}
|
|
118
170
|
|
|
171
|
+
let nodeTypeMap = {};
|
|
172
|
+
|
|
173
|
+
function registerNodeType(typeName, readModelName) {
|
|
174
|
+
nodeTypeMap[typeName] = readModelName;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function handleNode(payload) {
|
|
178
|
+
let globalId = Stdlib_Option.getOr(argStr(payload.arguments, "id"), "");
|
|
179
|
+
let decoded;
|
|
180
|
+
try {
|
|
181
|
+
decoded = atob(globalId);
|
|
182
|
+
} catch (exn) {
|
|
183
|
+
decoded = "";
|
|
184
|
+
}
|
|
185
|
+
let colonIdx = decoded.indexOf(":");
|
|
186
|
+
if (colonIdx <= 0) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
let typeName = decoded.slice(0, colonIdx);
|
|
190
|
+
let localId = decoded.slice(colonIdx + 1 | 0, decoded.length);
|
|
191
|
+
let binding = Stdlib_Option.flatMap(nodeTypeMap[typeName], rm => bindings[rm]);
|
|
192
|
+
if (binding === undefined) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
let match = await runInterceptor(binding, payload);
|
|
196
|
+
if (typeof match === "object") {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
let items = await binding.ops.load(localId);
|
|
200
|
+
if (items.TAG !== "Ok") {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
let item = items._0[0];
|
|
204
|
+
if (item === undefined) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
let obj = Object.assign({}, Stdlib_Option.getOr(Stdlib_JSON.Decode.object(item), {}));
|
|
208
|
+
obj["__typename"] = typeName;
|
|
209
|
+
obj["id"] = globalId;
|
|
210
|
+
return obj;
|
|
211
|
+
}
|
|
212
|
+
|
|
119
213
|
async function handler(payload, _context) {
|
|
120
|
-
let
|
|
214
|
+
let match = payload.kind;
|
|
215
|
+
let bindingKey;
|
|
216
|
+
switch (match) {
|
|
217
|
+
case "resolveMany" :
|
|
218
|
+
case "resolveOne" :
|
|
219
|
+
bindingKey = Stdlib_Option.getOr(payload.target, payload.readModelName);
|
|
220
|
+
break;
|
|
221
|
+
default:
|
|
222
|
+
bindingKey = payload.readModelName;
|
|
223
|
+
}
|
|
224
|
+
let match$1 = payload.kind;
|
|
225
|
+
if (match$1 === "node") {
|
|
226
|
+
return await handleNode(payload);
|
|
227
|
+
}
|
|
228
|
+
let binding = bindings[bindingKey];
|
|
121
229
|
if (binding !== undefined) {
|
|
122
230
|
return await dispatch(binding, payload);
|
|
123
231
|
} else {
|
|
124
|
-
return Stdlib_JsError.throwWithMessage("PgQueryResolver: no binding registered for read model " +
|
|
232
|
+
return Stdlib_JsError.throwWithMessage("PgQueryResolver: no binding registered for read model " + bindingKey);
|
|
125
233
|
}
|
|
126
234
|
}
|
|
127
235
|
|
|
@@ -136,6 +244,9 @@ export {
|
|
|
136
244
|
dispatch,
|
|
137
245
|
bindings,
|
|
138
246
|
register,
|
|
247
|
+
nodeTypeMap,
|
|
248
|
+
registerNodeType,
|
|
249
|
+
handleNode,
|
|
139
250
|
handler,
|
|
140
251
|
}
|
|
141
252
|
/* log Not a pure module */
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
module AppSync = QueryDbResolvers_AppSync
|
|
2
2
|
module NoOp = QueryDbResolvers_NoOp
|
|
3
|
+
module Lambda = QueryDbResolvers_Lambda
|
|
3
4
|
|
|
4
|
-
// B3.
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// B3.2b: Postgres-backed read models have no DynamoDB data source — their
|
|
6
|
+
// GraphQL Query fields are served by the shared PgQueryResolver Lambda data
|
|
7
|
+
// source (`QueryDbResolvers_Lambda`, Invoke templates → PgQueryResolver_Lambda).
|
|
8
|
+
// DynamoDB-backed (incl. admin-exempt) read models keep the direct resolver set.
|
|
8
9
|
module Selectable = {
|
|
9
10
|
type api = QueryDbResolvers_AppSync_NoOp.api
|
|
10
11
|
type role = QueryDbResolvers_AppSync_NoOp.role
|
|
@@ -21,7 +22,7 @@ module Selectable = {
|
|
|
21
22
|
~opts,
|
|
22
23
|
) =>
|
|
23
24
|
if QueryDbBackend.isPostgresFor(name) {
|
|
24
|
-
|
|
25
|
+
QueryDbResolvers_Lambda.make(
|
|
25
26
|
~name,
|
|
26
27
|
~api,
|
|
27
28
|
~apiRole,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as QueryDbBackend$ReventlessAws from "./QueryDbBackend.res.mjs";
|
|
4
|
+
import * as QueryDbResolvers_Lambda$ReventlessAws from "./QueryDbResolvers_Lambda.res.mjs";
|
|
4
5
|
import * as QueryDbResolvers_AppSync$ReventlessAws from "./QueryDbResolvers_AppSync.res.mjs";
|
|
5
|
-
import * as QueryDbResolvers_AppSync_NoOp$ReventlessAws from "./QueryDbResolvers_AppSync_NoOp.res.mjs";
|
|
6
6
|
|
|
7
7
|
function make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolverConfigs, idsResolverConfigs, authorization, opts) {
|
|
8
8
|
if (QueryDbBackend$ReventlessAws.isPostgresFor(name)) {
|
|
9
|
-
return
|
|
9
|
+
return QueryDbResolvers_Lambda$ReventlessAws.make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolverConfigs, idsResolverConfigs, authorization, opts);
|
|
10
10
|
} else {
|
|
11
11
|
return QueryDbResolvers_AppSync$ReventlessAws.make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolverConfigs, idsResolverConfigs, authorization, opts);
|
|
12
12
|
}
|
|
@@ -20,9 +20,12 @@ let AppSync;
|
|
|
20
20
|
|
|
21
21
|
let NoOp;
|
|
22
22
|
|
|
23
|
+
let Lambda;
|
|
24
|
+
|
|
23
25
|
export {
|
|
24
26
|
AppSync,
|
|
25
27
|
NoOp,
|
|
28
|
+
Lambda,
|
|
26
29
|
Selectable,
|
|
27
30
|
}
|
|
28
|
-
/*
|
|
31
|
+
/* QueryDbResolvers_Lambda-ReventlessAws Not a pure module */
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// AppSync resolvers for Postgres-backed read models (B3.2b) — the Lambda-data-
|
|
2
|
+
// source parallel to QueryDbResolvers_AppSync's direct DynamoDB resolvers.
|
|
3
|
+
//
|
|
4
|
+
// Every Query field becomes a thin APPSYNC_JS unit resolver whose `Invoke`
|
|
5
|
+
// template carries { readModelName, kind, index?, arguments, identity } to the
|
|
6
|
+
// ONE shared PgQueryResolver Lambda (PgQueryResolver_Builder), which dispatches
|
|
7
|
+
// via PgQueryResolver_Lambda.dispatch. `dataSourceName` is the shared data
|
|
8
|
+
// source's (deferred) name, the same for every Postgres read model.
|
|
9
|
+
//
|
|
10
|
+
// Field names / includeIdParam / connectionSpec come from the same registry the
|
|
11
|
+
// AppSync path reads, so the SDL emitted by GraphQL_FragmentGenerator stays in
|
|
12
|
+
// lockstep. Kinds covered: getById, list (connection), index, byIds. Deferred to
|
|
13
|
+
// B3.2c: items (sub-id connection), @resolves/@resolvesMany, node, auth-table.
|
|
14
|
+
|
|
15
|
+
module Resolver = AppSync_Resolver_Retrying
|
|
16
|
+
open Reventless.ReadModel
|
|
17
|
+
|
|
18
|
+
type api = Types.AppSync.api
|
|
19
|
+
type role = Types.AppSync.role
|
|
20
|
+
|
|
21
|
+
// Identity block shared by every Invoke template (Cognito vs IAM), matching the
|
|
22
|
+
// shape PgQueryResolver_Lambda decodes (Reventless.Identity.t).
|
|
23
|
+
let identityBlock = `id != null && id.sub != null
|
|
24
|
+
? { userId: id.sub, username: id.username, groups: id.claims?.['cognito:groups'] ?? [], claims: id.claims, provider: 'Cognito' }
|
|
25
|
+
: id != null
|
|
26
|
+
? { userArn: id.userArn ?? null, accountId: id.accountId ?? null, username: id.username ?? null, provider: 'IAM' }
|
|
27
|
+
: null`
|
|
28
|
+
|
|
29
|
+
let invokeTemplate = (~readModelName: string, ~kind: string, ~index: option<string>=?) => {
|
|
30
|
+
let indexFrag = switch index {
|
|
31
|
+
| Some(ix) => `\n index: '${ix}',`
|
|
32
|
+
| None => ""
|
|
33
|
+
}
|
|
34
|
+
`import { util } from '@aws-appsync/utils';
|
|
35
|
+
export function request(ctx) {
|
|
36
|
+
const id = ctx.identity;
|
|
37
|
+
return {
|
|
38
|
+
operation: 'Invoke',
|
|
39
|
+
payload: {
|
|
40
|
+
readModelName: '${readModelName}',
|
|
41
|
+
kind: '${kind}',${indexFrag}
|
|
42
|
+
arguments: ctx.args,
|
|
43
|
+
identity: ${identityBlock}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function response(ctx) {
|
|
48
|
+
if (ctx.error) util.error(ctx.error.message, ctx.error.type);
|
|
49
|
+
return ctx.result;
|
|
50
|
+
}
|
|
51
|
+
`->Pulumi.Input.make
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Cross-table field resolver (@resolves/@resolvesMany, B3.2c): on a parent
|
|
55
|
+
// entity type's field, carrying the parent object (ctx.source) + baked target
|
|
56
|
+
// metadata (`extra`). `sourceType` is the parent read model (used only for auth
|
|
57
|
+
// scoping in dispatch); `target` is the target read model's binding key.
|
|
58
|
+
let invokeFieldTemplate = (~sourceType: string, ~kind: string, ~target: string, ~extra: string) =>
|
|
59
|
+
`import { util } from '@aws-appsync/utils';
|
|
60
|
+
export function request(ctx) {
|
|
61
|
+
const id = ctx.identity;
|
|
62
|
+
return {
|
|
63
|
+
operation: 'Invoke',
|
|
64
|
+
payload: {
|
|
65
|
+
readModelName: '${sourceType}',
|
|
66
|
+
kind: '${kind}',
|
|
67
|
+
target: '${target}',
|
|
68
|
+
source: ctx.source,
|
|
69
|
+
arguments: ctx.args,${extra}
|
|
70
|
+
identity: ${identityBlock}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function response(ctx) {
|
|
75
|
+
if (ctx.error) util.error(ctx.error.message, ctx.error.type);
|
|
76
|
+
return ctx.result;
|
|
77
|
+
}
|
|
78
|
+
`->Pulumi.Input.make
|
|
79
|
+
|
|
80
|
+
let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
81
|
+
~name: string,
|
|
82
|
+
~api: api,
|
|
83
|
+
~apiRole as _: role,
|
|
84
|
+
~dataSourceName,
|
|
85
|
+
~indexes: array<indexConfig>,
|
|
86
|
+
~subIdField,
|
|
87
|
+
~idResolverConfigs: array<idResolverConfig>,
|
|
88
|
+
~idsResolverConfigs: array<idsResolverConfig>,
|
|
89
|
+
~authorization as _: Reventless.Authorization.permission,
|
|
90
|
+
~opts,
|
|
91
|
+
) => {
|
|
92
|
+
let dataSourceName = dataSourceName->Pulumi.Output.asInput
|
|
93
|
+
let name = name->String.capitalize
|
|
94
|
+
let registryEntry = ReventlessCore.Plugin_Helpers.queryFieldNamesRegistry->Dict.get(name)
|
|
95
|
+
|
|
96
|
+
let fieldNameForSingle = switch registryEntry {
|
|
97
|
+
| Some({singleFieldName}) => singleFieldName
|
|
98
|
+
| None => name->Resolver.Functions.uncapitalize
|
|
99
|
+
}
|
|
100
|
+
let includeIdParam = switch registryEntry {
|
|
101
|
+
| Some({includeIdParam}) => includeIdParam
|
|
102
|
+
| None => true
|
|
103
|
+
}
|
|
104
|
+
let connectionSpec = switch registryEntry {
|
|
105
|
+
| Some({connectionSpec}) => connectionSpec
|
|
106
|
+
| None => true
|
|
107
|
+
}
|
|
108
|
+
let fieldNameForAll = switch registryEntry {
|
|
109
|
+
| Some({listFieldName}) => listFieldName
|
|
110
|
+
| None => name ++ "s"
|
|
111
|
+
}
|
|
112
|
+
let labelField = switch registryEntry {
|
|
113
|
+
| Some({labelField: ?lf}) => lf->Option.getOr("id")
|
|
114
|
+
| None => "id"
|
|
115
|
+
}
|
|
116
|
+
let returnTypeName = switch registryEntry {
|
|
117
|
+
| Some({returnTypeName: rt}) => rt
|
|
118
|
+
| None => name
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Register the binding info the shared Lambda's env config needs (the entry
|
|
122
|
+
// point gets indexes/subIdField/schema from the spec module; labelField and
|
|
123
|
+
// includeIdParam come from this deploy-time registry). Keyed by spec name.
|
|
124
|
+
PgQueryResolver_Builder.register({readModelName: name, labelField, includeIdParam})
|
|
125
|
+
|
|
126
|
+
// Relay node type → this read model (for the shared node(id) resolver, B3.2c).
|
|
127
|
+
// Only entities addressable by id participate in node resolution.
|
|
128
|
+
if includeIdParam {
|
|
129
|
+
PgQueryResolver_Builder.registerNodeType(~typeName=returnTypeName, ~readModelName=name)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let mkResolver = (~resolverName, ~field, ~kind, ~index=?) =>
|
|
133
|
+
Resolver.makeUnitJsResolver(
|
|
134
|
+
~name=resolverName,
|
|
135
|
+
~api,
|
|
136
|
+
~dataSourceName,
|
|
137
|
+
~type_="Query"->Pulumi.Input.make,
|
|
138
|
+
~field=field->Pulumi.Input.make,
|
|
139
|
+
~code=invokeTemplate(~readModelName=name, ~kind, ~index?),
|
|
140
|
+
~opts,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
let stripLeadingBy = s =>
|
|
144
|
+
if s->String.startsWith("by") && s->String.length > 2 {
|
|
145
|
+
s->String.slice(~start=2, ~end=s->String.length)
|
|
146
|
+
} else {
|
|
147
|
+
s
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Resolvers are deferred into resourcesMaker (created inside the schema-pushed
|
|
151
|
+
// builderOutputs.apply, like the AppSync path) — so they only exist after the
|
|
152
|
+
// fields are ACTIVE and after PgQueryResolver_Builder.provision has resolved
|
|
153
|
+
// the shared data source name.
|
|
154
|
+
let resourcesMaker: ReventlessCore.QueryDb.resolversResourcesMaker = _allQueryDbs => {
|
|
155
|
+
// getById (or listAll when the read model has no id param, e.g. singletons).
|
|
156
|
+
let byId = mkResolver(
|
|
157
|
+
~resolverName=fieldNameForSingle->String.capitalize,
|
|
158
|
+
~field=fieldNameForSingle,
|
|
159
|
+
~kind=includeIdParam ? "getById" : "list",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
// Main list — Relay connection (or legacy list when connectionSpec=false).
|
|
163
|
+
let all = mkResolver(
|
|
164
|
+
~resolverName=fieldNameForAll->String.capitalize,
|
|
165
|
+
~field=fieldNameForAll,
|
|
166
|
+
~kind=connectionSpec ? "list" : "list",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
// Sub-id connection — {single}Items(id, filter, …); only when subId configured.
|
|
170
|
+
let items = switch subIdField {
|
|
171
|
+
| Some(_) => [
|
|
172
|
+
mkResolver(
|
|
173
|
+
~resolverName=fieldNameForSingle->String.capitalize ++ "Items",
|
|
174
|
+
~field=fieldNameForSingle ++ "Items",
|
|
175
|
+
~kind="items",
|
|
176
|
+
),
|
|
177
|
+
]
|
|
178
|
+
| None => []
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Batched-by-ids — single-key projections only (mirrors the SDL).
|
|
182
|
+
let byIds = if includeIdParam && subIdField === None {
|
|
183
|
+
let byIdsField = fieldNameForAll ++ "ByIds"
|
|
184
|
+
[
|
|
185
|
+
mkResolver(
|
|
186
|
+
~resolverName=byIdsField->String.capitalize,
|
|
187
|
+
~field=byIdsField,
|
|
188
|
+
~kind="byIds",
|
|
189
|
+
),
|
|
190
|
+
]
|
|
191
|
+
} else {
|
|
192
|
+
[]
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Per-index equality queries: {single}By{Index}.
|
|
196
|
+
let byIndex = indexes->Array.map(({index}) => {
|
|
197
|
+
let resolverName =
|
|
198
|
+
fieldNameForSingle->String.capitalize ++ "By" ++ index->stripLeadingBy->String.capitalize
|
|
199
|
+
let field = fieldNameForSingle ++ "By" ++ index->stripLeadingBy->String.capitalize
|
|
200
|
+
mkResolver(~resolverName, ~field, ~kind="index", ~index)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
// @resolves — single cross-table field resolver on this (parent) type. The
|
|
204
|
+
// target's binding key is its capitalized spec name (mirrors how the binding
|
|
205
|
+
// registry is keyed); the parent object flows via ctx.source.
|
|
206
|
+
let idResolvers = idResolverConfigs->Array.map(config => {
|
|
207
|
+
let {source: {idField, subId, resolvedField}, target} = config
|
|
208
|
+
let targetKey = target.tableName->String.capitalize
|
|
209
|
+
let (field, multi) = switch resolvedField {
|
|
210
|
+
| Single(f) => (f, false)
|
|
211
|
+
| Multi(f) => (f, true)
|
|
212
|
+
}
|
|
213
|
+
let targetFrag = switch target.idField {
|
|
214
|
+
| Index(ix) => `\n targetIndex: '${ix}',\n targetIndexIdField: '${ix}',`
|
|
215
|
+
| IndexWithId(ix, idf) => `\n targetIndex: '${ix}',\n targetIndexIdField: '${idf}',`
|
|
216
|
+
| Id => ""
|
|
217
|
+
}
|
|
218
|
+
let subIdFrag = switch subId {
|
|
219
|
+
| Field(f) => `\n sourceSubId: { kind: 'field', name: '${f}' },`
|
|
220
|
+
| Argument(a) => `\n sourceSubId: { kind: 'arg', name: '${a}' },`
|
|
221
|
+
| NoSubId => ""
|
|
222
|
+
}
|
|
223
|
+
let extra =
|
|
224
|
+
`\n sourceIdField: '${idField}',\n multi: ${multi ? "true" : "false"},` ++
|
|
225
|
+
targetFrag ++
|
|
226
|
+
subIdFrag
|
|
227
|
+
Resolver.makeUnitJsResolver(
|
|
228
|
+
~name=name ++ field->String.capitalize,
|
|
229
|
+
~api,
|
|
230
|
+
~dataSourceName,
|
|
231
|
+
~type_=name->Pulumi.Input.make,
|
|
232
|
+
~field=field->Pulumi.Input.make,
|
|
233
|
+
~code=invokeFieldTemplate(~sourceType=name, ~kind="resolveOne", ~target=targetKey, ~extra),
|
|
234
|
+
~opts,
|
|
235
|
+
)
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
// @resolvesMany — batch cross-table field resolver (BatchGet by ids).
|
|
239
|
+
let idsResolvers = idsResolverConfigs->Array.map(config => {
|
|
240
|
+
let {source: {idsField, resolvedField}, target} = config
|
|
241
|
+
let targetKey = target.tableName->String.capitalize
|
|
242
|
+
let extra = `\n sourceIdsField: '${idsField}',`
|
|
243
|
+
Resolver.makeUnitJsResolver(
|
|
244
|
+
~name=name ++ resolvedField->String.capitalize,
|
|
245
|
+
~api,
|
|
246
|
+
~dataSourceName,
|
|
247
|
+
~type_=name->Pulumi.Input.make,
|
|
248
|
+
~field=resolvedField->Pulumi.Input.make,
|
|
249
|
+
~code=invokeFieldTemplate(~sourceType=name, ~kind="resolveMany", ~target=targetKey, ~extra),
|
|
250
|
+
~opts,
|
|
251
|
+
)
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
[byId, all]
|
|
255
|
+
->Array.concat(items)
|
|
256
|
+
->Array.concat(byIds)
|
|
257
|
+
->Array.concat(byIndex)
|
|
258
|
+
->Array.concat(idResolvers)
|
|
259
|
+
->Array.concat(idsResolvers)
|
|
260
|
+
->Array.map(Util.AppSync.toResourceNative)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
{ReventlessCore.QueryDb_Adapter.resources: [], resourcesMaker}
|
|
264
|
+
}
|