@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
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
|
+
import * as Stdlib_String from "@rescript/runtime/lib/es6/Stdlib_String.js";
|
|
5
|
+
import * as Util_AppSync$ReventlessAws from "../../util/Util_AppSync.res.mjs";
|
|
6
|
+
import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Helpers.res.mjs";
|
|
7
|
+
import * as AppSync_Resolver_Functions$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/AppSync/AppSync_Resolver_Functions.res.mjs";
|
|
8
|
+
import * as PgQueryResolver_Builder$ReventlessAws from "./PgQueryResolver_Builder.res.mjs";
|
|
9
|
+
import * as AppSync_Resolver_Retrying$ReventlessAws from "../Api/AppSync_Resolver_Retrying.res.mjs";
|
|
10
|
+
|
|
11
|
+
let identityBlock = `id != null && id.sub != null
|
|
12
|
+
? { userId: id.sub, username: id.username, groups: id.claims?.['cognito:groups'] ?? [], claims: id.claims, provider: 'Cognito' }
|
|
13
|
+
: id != null
|
|
14
|
+
? { userArn: id.userArn ?? null, accountId: id.accountId ?? null, username: id.username ?? null, provider: 'IAM' }
|
|
15
|
+
: null`;
|
|
16
|
+
|
|
17
|
+
function invokeTemplate(readModelName, kind, index) {
|
|
18
|
+
let indexFrag = index !== undefined ? `\n index: '` + index + `',` : "";
|
|
19
|
+
return `import { util } from '@aws-appsync/utils';
|
|
20
|
+
export function request(ctx) {
|
|
21
|
+
const id = ctx.identity;
|
|
22
|
+
return {
|
|
23
|
+
operation: 'Invoke',
|
|
24
|
+
payload: {
|
|
25
|
+
readModelName: '` + readModelName + `',
|
|
26
|
+
kind: '` + kind + `',` + indexFrag + `
|
|
27
|
+
arguments: ctx.args,
|
|
28
|
+
identity: ` + identityBlock + `
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function response(ctx) {
|
|
33
|
+
if (ctx.error) util.error(ctx.error.message, ctx.error.type);
|
|
34
|
+
return ctx.result;
|
|
35
|
+
}
|
|
36
|
+
`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function invokeFieldTemplate(sourceType, kind, target, extra) {
|
|
40
|
+
return `import { util } from '@aws-appsync/utils';
|
|
41
|
+
export function request(ctx) {
|
|
42
|
+
const id = ctx.identity;
|
|
43
|
+
return {
|
|
44
|
+
operation: 'Invoke',
|
|
45
|
+
payload: {
|
|
46
|
+
readModelName: '` + sourceType + `',
|
|
47
|
+
kind: '` + kind + `',
|
|
48
|
+
target: '` + target + `',
|
|
49
|
+
source: ctx.source,
|
|
50
|
+
arguments: ctx.args,` + extra + `
|
|
51
|
+
identity: ` + identityBlock + `
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function response(ctx) {
|
|
56
|
+
if (ctx.error) util.error(ctx.error.message, ctx.error.type);
|
|
57
|
+
return ctx.result;
|
|
58
|
+
}
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function make(name, api, param, dataSourceName, indexes, subIdField, idResolverConfigs, idsResolverConfigs, param$1, opts) {
|
|
63
|
+
let name$1 = Stdlib_String.capitalize(name);
|
|
64
|
+
let registryEntry = Plugin_Helpers$ReventlessCore.queryFieldNamesRegistry[name$1];
|
|
65
|
+
let fieldNameForSingle = registryEntry !== undefined ? registryEntry.singleFieldName : AppSync_Resolver_Functions$PulumiAws.uncapitalize(name$1);
|
|
66
|
+
let includeIdParam = registryEntry !== undefined ? registryEntry.includeIdParam : true;
|
|
67
|
+
let fieldNameForAll = registryEntry !== undefined ? registryEntry.listFieldName : name$1 + "s";
|
|
68
|
+
let labelField = registryEntry !== undefined ? Stdlib_Option.getOr(registryEntry.labelField, "id") : "id";
|
|
69
|
+
let returnTypeName = registryEntry !== undefined ? registryEntry.returnTypeName : name$1;
|
|
70
|
+
PgQueryResolver_Builder$ReventlessAws.register({
|
|
71
|
+
readModelName: name$1,
|
|
72
|
+
labelField: labelField,
|
|
73
|
+
includeIdParam: includeIdParam
|
|
74
|
+
});
|
|
75
|
+
if (includeIdParam) {
|
|
76
|
+
PgQueryResolver_Builder$ReventlessAws.registerNodeType(returnTypeName, name$1);
|
|
77
|
+
}
|
|
78
|
+
let mkResolver = (resolverName, field, kind, index) => AppSync_Resolver_Retrying$ReventlessAws.makeUnitJsResolver(resolverName, api, dataSourceName, "Query", field, invokeTemplate(name$1, kind, index), opts);
|
|
79
|
+
let stripLeadingBy = s => {
|
|
80
|
+
if (s.startsWith("by") && s.length > 2) {
|
|
81
|
+
return s.slice(2, s.length);
|
|
82
|
+
} else {
|
|
83
|
+
return s;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
let resourcesMaker = _allQueryDbs => {
|
|
87
|
+
let byId = mkResolver(Stdlib_String.capitalize(fieldNameForSingle), fieldNameForSingle, includeIdParam ? "getById" : "list", undefined);
|
|
88
|
+
let all = mkResolver(Stdlib_String.capitalize(fieldNameForAll), fieldNameForAll, "list", undefined);
|
|
89
|
+
let items = subIdField !== undefined ? [mkResolver(Stdlib_String.capitalize(fieldNameForSingle) + "Items", fieldNameForSingle + "Items", "items", undefined)] : [];
|
|
90
|
+
let byIds;
|
|
91
|
+
if (includeIdParam && subIdField === undefined) {
|
|
92
|
+
let byIdsField = fieldNameForAll + "ByIds";
|
|
93
|
+
byIds = [mkResolver(Stdlib_String.capitalize(byIdsField), byIdsField, "byIds", undefined)];
|
|
94
|
+
} else {
|
|
95
|
+
byIds = [];
|
|
96
|
+
}
|
|
97
|
+
let byIndex = indexes.map(param => {
|
|
98
|
+
let index = param.index;
|
|
99
|
+
let resolverName = Stdlib_String.capitalize(fieldNameForSingle) + "By" + Stdlib_String.capitalize(stripLeadingBy(index));
|
|
100
|
+
let field = fieldNameForSingle + "By" + Stdlib_String.capitalize(stripLeadingBy(index));
|
|
101
|
+
return mkResolver(resolverName, field, "index", index);
|
|
102
|
+
});
|
|
103
|
+
let idResolvers = idResolverConfigs.map(config => {
|
|
104
|
+
let target = config.target;
|
|
105
|
+
let match = config.source;
|
|
106
|
+
let resolvedField = match.resolvedField;
|
|
107
|
+
let subId = match.subId;
|
|
108
|
+
let targetKey = Stdlib_String.capitalize(target.tableName);
|
|
109
|
+
let match$1;
|
|
110
|
+
match$1 = resolvedField.TAG === "Single" ? [
|
|
111
|
+
resolvedField._0,
|
|
112
|
+
false
|
|
113
|
+
] : [
|
|
114
|
+
resolvedField._0,
|
|
115
|
+
true
|
|
116
|
+
];
|
|
117
|
+
let field = match$1[0];
|
|
118
|
+
let ix = target.idField;
|
|
119
|
+
let targetFrag;
|
|
120
|
+
if (typeof ix !== "object") {
|
|
121
|
+
targetFrag = "";
|
|
122
|
+
} else if (ix.TAG === "Index") {
|
|
123
|
+
let ix$1 = ix._0;
|
|
124
|
+
targetFrag = `\n targetIndex: '` + ix$1 + `',\n targetIndexIdField: '` + ix$1 + `',`;
|
|
125
|
+
} else {
|
|
126
|
+
targetFrag = `\n targetIndex: '` + ix._0 + `',\n targetIndexIdField: '` + ix._1 + `',`;
|
|
127
|
+
}
|
|
128
|
+
let subIdFrag;
|
|
129
|
+
subIdFrag = typeof subId !== "object" ? "" : (
|
|
130
|
+
subId.TAG === "Field" ? `\n sourceSubId: { kind: 'field', name: '` + subId._0 + `' },` : `\n sourceSubId: { kind: 'arg', name: '` + subId._0 + `' },`
|
|
131
|
+
);
|
|
132
|
+
let extra = `\n sourceIdField: '` + match.idField + `',\n multi: ` + (
|
|
133
|
+
match$1[1] ? "true" : "false"
|
|
134
|
+
) + `,` + targetFrag + subIdFrag;
|
|
135
|
+
return AppSync_Resolver_Retrying$ReventlessAws.makeUnitJsResolver(name$1 + Stdlib_String.capitalize(field), api, dataSourceName, name$1, field, invokeFieldTemplate(name$1, "resolveOne", targetKey, extra), opts);
|
|
136
|
+
});
|
|
137
|
+
let idsResolvers = idsResolverConfigs.map(config => {
|
|
138
|
+
let match = config.source;
|
|
139
|
+
let resolvedField = match.resolvedField;
|
|
140
|
+
let targetKey = Stdlib_String.capitalize(config.target.tableName);
|
|
141
|
+
let extra = `\n sourceIdsField: '` + match.idsField + `',`;
|
|
142
|
+
return AppSync_Resolver_Retrying$ReventlessAws.makeUnitJsResolver(name$1 + Stdlib_String.capitalize(resolvedField), api, dataSourceName, name$1, resolvedField, invokeFieldTemplate(name$1, "resolveMany", targetKey, extra), opts);
|
|
143
|
+
});
|
|
144
|
+
return [
|
|
145
|
+
byId,
|
|
146
|
+
all
|
|
147
|
+
].concat(items).concat(byIds).concat(byIndex).concat(idResolvers).concat(idsResolvers).map(Util_AppSync$ReventlessAws.toResourceNative);
|
|
148
|
+
};
|
|
149
|
+
return {
|
|
150
|
+
resources: [],
|
|
151
|
+
resourcesMaker: resourcesMaker
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let Resolver;
|
|
156
|
+
|
|
157
|
+
export {
|
|
158
|
+
Resolver,
|
|
159
|
+
identityBlock,
|
|
160
|
+
invokeTemplate,
|
|
161
|
+
invokeFieldTemplate,
|
|
162
|
+
make,
|
|
163
|
+
}
|
|
164
|
+
/* Util_AppSync-ReventlessAws Not a pure module */
|
|
@@ -37,7 +37,11 @@ let make: ReventlessCore.QueryDb_Adapter.storageMaker<api, role> = (
|
|
|
37
37
|
}
|
|
38
38
|
{
|
|
39
39
|
ReventlessCore.QueryDb_Adapter.resources: [],
|
|
40
|
-
|
|
40
|
+
// B3.2b: the shared PgQueryResolver Lambda data source. Deferred — resolved
|
|
41
|
+
// by PgQueryResolver_Builder.provision after every plugin is built (the
|
|
42
|
+
// resolvers that read it are created even later, inside the schema-pushed
|
|
43
|
+
// resourcesMaker). Same value for every Postgres read model.
|
|
44
|
+
dataSourceName: PgQueryResolver_Builder.dataSourceName,
|
|
41
45
|
operations,
|
|
42
46
|
}
|
|
43
47
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
-
import * as Pulumi from "@pulumi/pulumi";
|
|
4
3
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
5
4
|
import * as QueryDbBackend$ReventlessAws from "./QueryDbBackend.res.mjs";
|
|
5
|
+
import * as PgQueryResolver_Builder$ReventlessAws from "./PgQueryResolver_Builder.res.mjs";
|
|
6
6
|
import * as QueryDbStorage_Postgres_Runtime$ReventlessAws from "./QueryDbStorage_Postgres_Runtime.res.mjs";
|
|
7
7
|
|
|
8
8
|
function make(name, indexes, subIdField, param, param$1, param$2, param$3) {
|
|
@@ -10,7 +10,7 @@ function make(name, indexes, subIdField, param, param$1, param$2, param$3) {
|
|
|
10
10
|
let operations = match !== undefined ? match.connectionConfig.apply(config => QueryDbStorage_Postgres_Runtime$ReventlessAws.opsFor(config, name, indexes, subIdField)) : Stdlib_JsError.throwWithMessage("QueryDbStorage_Postgres.make called without a QueryDbBackend selection");
|
|
11
11
|
return {
|
|
12
12
|
resources: [],
|
|
13
|
-
dataSourceName:
|
|
13
|
+
dataSourceName: PgQueryResolver_Builder$ReventlessAws.dataSourceName,
|
|
14
14
|
operations: operations
|
|
15
15
|
};
|
|
16
16
|
}
|
|
@@ -18,4 +18,4 @@ function make(name, indexes, subIdField, param, param$1, param$2, param$3) {
|
|
|
18
18
|
export {
|
|
19
19
|
make,
|
|
20
20
|
}
|
|
21
|
-
/*
|
|
21
|
+
/* PgQueryResolver_Builder-ReventlessAws Not a pure module */
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Shared PgQueryResolver Lambda entry point (B3.2b).
|
|
2
|
+
//
|
|
3
|
+
// One in-VPC Lambda serves every Postgres-backed read model's GraphQL Query
|
|
4
|
+
// fields (they have no per-table AppSync data source). At cold start it reads
|
|
5
|
+
// QUERY_RESOLVER_CONFIG (one shared pgConnection + one handler per read model),
|
|
6
|
+
// dynamically imports each spec package, and registers a per-read-model
|
|
7
|
+
// `binding` (ops + QueryEnginePostgres push-downs + indexes/subIdField/capability
|
|
8
|
+
// + baked labelField/includeIdParam + authorization) into
|
|
9
|
+
// PgQueryResolver_Lambda. AppSync invokes `handler` with the resolver payload
|
|
10
|
+
// { readModelName, kind, index?, arguments, identity }, which
|
|
11
|
+
// PgQueryResolver_Lambda.dispatch routes.
|
|
12
|
+
//
|
|
13
|
+
// ReScript labeled args compile positionally, so the engine push-downs
|
|
14
|
+
// (indexLookup/byIds/listPage/scan) are assigned straight into the `pushdowns`
|
|
15
|
+
// record — same calling convention PgQueryResolver_Lambda.dispatch invokes them
|
|
16
|
+
// with (verified against the compiled QueryEnginePostgres output).
|
|
17
|
+
|
|
18
|
+
import { poolFor } from "@reventlessdev/reventless-aws/src/adapter/Postgres/PgRuntime.res.mjs";
|
|
19
|
+
import { Make as makeEngine } from "@reventlessdev/reventless-postgres/src/QueryEnginePostgres.res.mjs";
|
|
20
|
+
import { opsFor as pgQdbOpsFor } from "@reventlessdev/reventless-aws/src/adapter/QueryDb/QueryDbStorage_Postgres_Runtime.res.mjs";
|
|
21
|
+
import { deriveServerCapability } from "@reventlessdev/reventless-core/src/components/Api/GraphQL_FragmentGenerator.res.mjs";
|
|
22
|
+
import { register, registerNodeType, handler as dispatchHandler } from "@reventlessdev/reventless-aws/src/adapter/QueryDb/PgQueryResolver_Lambda.res.mjs";
|
|
23
|
+
import { patchSpecId, log } from "./HandlerFactoryHelpers.mjs";
|
|
24
|
+
|
|
25
|
+
const dynamicImport = (specifier) => import('/var/task/node_modules/' + specifier);
|
|
26
|
+
|
|
27
|
+
// A bounded full-materialisation for the list fallback (shapes listPage
|
|
28
|
+
// declines). Kept high; the fallback is only hit for search/searchPrefix/ids/
|
|
29
|
+
// backward pagination, which the AutoUI does not issue for large models.
|
|
30
|
+
const SCAN_ALL_LIMIT = 100000;
|
|
31
|
+
|
|
32
|
+
async function buildAllBindings() {
|
|
33
|
+
const configStr = process.env["QUERY_RESOLVER_CONFIG"] || '{"handlers":[]}';
|
|
34
|
+
const config = JSON.parse(configStr);
|
|
35
|
+
const pgConnection = config.pgConnection;
|
|
36
|
+
if (!pgConnection) {
|
|
37
|
+
log.warn("no pgConnection in QUERY_RESOLVER_CONFIG", { comp: "PgQueryResolver" });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// Container-lifetime pool (memoised by poolFor); the engine and the ops set
|
|
41
|
+
// share it, so a Lambda holds one pool regardless of read model count.
|
|
42
|
+
const pool = poolFor(pgConnection);
|
|
43
|
+
const engine = makeEngine({ pool });
|
|
44
|
+
|
|
45
|
+
// Relay node type → read-model map (B3.2c), baked into the config.
|
|
46
|
+
for (const [typeName, rm] of Object.entries(config.nodeTypes || {})) {
|
|
47
|
+
registerNodeType(typeName, rm);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const pushdowns = {
|
|
51
|
+
indexLookup: engine.indexLookup,
|
|
52
|
+
byIds: engine.byIds,
|
|
53
|
+
listPage: engine.listPage,
|
|
54
|
+
itemsPage: engine.itemsPage,
|
|
55
|
+
scanAll: (readModelName) => engine.scan(readModelName, [], SCAN_ALL_LIMIT),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
await Promise.all((config.handlers || []).map(async h => {
|
|
59
|
+
const specModule = patchSpecId(await dynamicImport(h.specModule));
|
|
60
|
+
const indexes = (specModule.config && specModule.config.indexes) || [];
|
|
61
|
+
const subIdField = specModule.subIdConfig ? specModule.subIdConfig.subIdField : undefined;
|
|
62
|
+
const capability = deriveServerCapability(specModule.stateSchema);
|
|
63
|
+
const ops = pgQdbOpsFor(pgConnection, h.readModelName, indexes, subIdField);
|
|
64
|
+
|
|
65
|
+
register(h.readModelName, {
|
|
66
|
+
ops,
|
|
67
|
+
pushdowns,
|
|
68
|
+
indexes,
|
|
69
|
+
subIdField,
|
|
70
|
+
capability,
|
|
71
|
+
labelField: h.labelField,
|
|
72
|
+
includeIdParam: h.includeIdParam,
|
|
73
|
+
// Compiled string variant (e.g. "AllowAuthenticated"), injected on the
|
|
74
|
+
// spec by @@reventless.spec — the shape Reventless.Authorization.isAllowed
|
|
75
|
+
// matches on.
|
|
76
|
+
authorization: specModule.authorization,
|
|
77
|
+
});
|
|
78
|
+
log.debug("registered resolver binding for " + h.readModelName, { comp: "PgQueryResolver" });
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const initPromise = buildAllBindings();
|
|
83
|
+
|
|
84
|
+
// AppSync Lambda data source: the invocation payload IS the event.
|
|
85
|
+
export async function handler(event, context) {
|
|
86
|
+
await initPromise;
|
|
87
|
+
return dispatchHandler(event, context);
|
|
88
|
+
}
|
|
@@ -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 = (
|
|
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
|
|
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,83 @@ 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
|
+
|
|
194
296
|
testPromise("unmapped kind throws", async () => {
|
|
195
297
|
let threw = try {
|
|
196
298
|
let _ = await PgQueryResolver_Lambda.dispatch(
|