grafeo-ogm 2.5.4 → 2.6.0
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 +54 -0
- package/README.md +123 -0
- package/dist/cjs/compilers/fulltext.compiler.d.ts +17 -1
- package/dist/cjs/compilers/fulltext.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/fulltext.compiler.js +35 -3
- package/dist/cjs/compilers/fulltext.compiler.js.map +1 -1
- package/dist/cjs/compilers/mutation.compiler.d.ts +39 -0
- package/dist/cjs/compilers/mutation.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/mutation.compiler.js +161 -11
- package/dist/cjs/compilers/mutation.compiler.js.map +1 -1
- package/dist/cjs/compilers/selection.compiler.d.ts +65 -1
- package/dist/cjs/compilers/selection.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/selection.compiler.js +521 -25
- package/dist/cjs/compilers/selection.compiler.js.map +1 -1
- package/dist/cjs/compilers/where.compiler.d.ts +4 -0
- package/dist/cjs/compilers/where.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/where.compiler.js +23 -0
- package/dist/cjs/compilers/where.compiler.js.map +1 -1
- package/dist/cjs/interface-model.d.ts.map +1 -1
- package/dist/cjs/interface-model.js +8 -1
- package/dist/cjs/interface-model.js.map +1 -1
- package/dist/cjs/model.d.ts +7 -0
- package/dist/cjs/model.d.ts.map +1 -1
- package/dist/cjs/model.js +20 -4
- package/dist/cjs/model.js.map +1 -1
- package/dist/cjs/ogm.d.ts +14 -0
- package/dist/cjs/ogm.d.ts.map +1 -1
- package/dist/cjs/ogm.js +36 -2
- package/dist/cjs/ogm.js.map +1 -1
- package/dist/cjs/utils/strict-mode.d.ts +39 -0
- package/dist/cjs/utils/strict-mode.d.ts.map +1 -0
- package/dist/cjs/utils/strict-mode.js +59 -0
- package/dist/cjs/utils/strict-mode.js.map +1 -0
- package/dist/esm/compilers/fulltext.compiler.d.ts +17 -1
- package/dist/esm/compilers/fulltext.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/fulltext.compiler.js +35 -3
- package/dist/esm/compilers/fulltext.compiler.js.map +1 -1
- package/dist/esm/compilers/mutation.compiler.d.ts +39 -0
- package/dist/esm/compilers/mutation.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/mutation.compiler.js +161 -11
- package/dist/esm/compilers/mutation.compiler.js.map +1 -1
- package/dist/esm/compilers/selection.compiler.d.ts +65 -1
- package/dist/esm/compilers/selection.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/selection.compiler.js +521 -25
- package/dist/esm/compilers/selection.compiler.js.map +1 -1
- package/dist/esm/compilers/where.compiler.d.ts +4 -0
- package/dist/esm/compilers/where.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/where.compiler.js +23 -0
- package/dist/esm/compilers/where.compiler.js.map +1 -1
- package/dist/esm/interface-model.d.ts.map +1 -1
- package/dist/esm/interface-model.js +8 -1
- package/dist/esm/interface-model.js.map +1 -1
- package/dist/esm/model.d.ts +7 -0
- package/dist/esm/model.d.ts.map +1 -1
- package/dist/esm/model.js +20 -4
- package/dist/esm/model.js.map +1 -1
- package/dist/esm/ogm.d.ts +14 -0
- package/dist/esm/ogm.d.ts.map +1 -1
- package/dist/esm/ogm.js +36 -2
- package/dist/esm/ogm.js.map +1 -1
- package/dist/esm/utils/strict-mode.d.ts +39 -0
- package/dist/esm/utils/strict-mode.d.ts.map +1 -0
- package/dist/esm/utils/strict-mode.js +54 -0
- package/dist/esm/utils/strict-mode.js.map +1 -0
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@ const errors_1 = require("../errors");
|
|
|
7
7
|
const utils_1 = require("../schema/utils");
|
|
8
8
|
const cypher_field_projection_1 = require("../utils/cypher-field-projection");
|
|
9
9
|
const validation_1 = require("../utils/validation");
|
|
10
|
+
const strict_mode_1 = require("../utils/strict-mode");
|
|
10
11
|
const where_compiler_1 = require("./where.compiler");
|
|
11
12
|
/**
|
|
12
13
|
* v2.5.3 — the printed arguments of a parsed selectionSet field, so a
|
|
@@ -14,6 +15,52 @@ const where_compiler_1 = require("./where.compiler");
|
|
|
14
15
|
* selections carry the same arguments.
|
|
15
16
|
*/
|
|
16
17
|
const argumentKeys = new WeakMap();
|
|
18
|
+
/**
|
|
19
|
+
* v2.6.0 — the strict-mode warnings of each parsed selectionSet, replayed
|
|
20
|
+
* on cache hits (see `SelectionCompiler.replayParseWarnings`).
|
|
21
|
+
*/
|
|
22
|
+
const parseWarnings = new WeakMap();
|
|
23
|
+
/**
|
|
24
|
+
* v2.6.0 — whether two selections of one response key can apply to the
|
|
25
|
+
* same object. A selection outside any fragment applies to every object;
|
|
26
|
+
* fragment-only selections overlap when their type conditions share a
|
|
27
|
+
* possible concrete type. Only overlapping selections with different
|
|
28
|
+
* arguments conflict (GraphQL's "fields in set can merge" rule).
|
|
29
|
+
*/
|
|
30
|
+
function canApplyToSameObject(a, b, ctx) {
|
|
31
|
+
if (!a || !b)
|
|
32
|
+
return true;
|
|
33
|
+
const types = new Set(a.flatMap((t) => ctx.possibleTypes(t)));
|
|
34
|
+
return b.some((t) => ctx.possibleTypes(t).some((p) => types.has(p)));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* v2.6.0 — strict merge of a response key selected with different
|
|
38
|
+
* arguments: a conflict throws (as GraphQL validation does) when the two
|
|
39
|
+
* selections can apply to the same object; otherwise the incoming
|
|
40
|
+
* selection becomes (or joins) a per-type variant.
|
|
41
|
+
*/
|
|
42
|
+
function mergeTypeVariant(existing, incoming, key, ctx) {
|
|
43
|
+
const groups = [existing, ...(existing.typeVariants ?? [])];
|
|
44
|
+
for (const group of groups)
|
|
45
|
+
if (!sameArguments(group, incoming) &&
|
|
46
|
+
canApplyToSameObject(group.typeConditions, incoming.typeConditions, ctx))
|
|
47
|
+
throw new errors_1.OGMError(`Fields "${key}" conflict because they have differing arguments. ` +
|
|
48
|
+
`Use different aliases on the fields to fetch both.`);
|
|
49
|
+
const same = groups.find((g) => sameArguments(g, incoming));
|
|
50
|
+
if (!same) {
|
|
51
|
+
existing.typeVariants = [...(existing.typeVariants ?? []), incoming];
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (same.typeConditions && incoming.typeConditions)
|
|
55
|
+
same.typeConditions = [
|
|
56
|
+
...new Set([...same.typeConditions, ...incoming.typeConditions]),
|
|
57
|
+
];
|
|
58
|
+
mergeSameArguments(same, incoming, ctx);
|
|
59
|
+
}
|
|
60
|
+
function parseWarn(ctx, warning) {
|
|
61
|
+
if (!ctx.warnings.some((w) => w.code === warning.code && w.detail === warning.detail))
|
|
62
|
+
ctx.warnings.push(warning);
|
|
63
|
+
}
|
|
17
64
|
function sameArguments(a, b) {
|
|
18
65
|
return (argumentKeys.get(a) ?? '') === (argumentKeys.get(b) ?? '');
|
|
19
66
|
}
|
|
@@ -29,7 +76,7 @@ function sameArguments(a, b) {
|
|
|
29
76
|
* different types: recorded in `fieldByType` for a per-type projection.
|
|
30
77
|
* - A selection outside any fragment makes the field unconditional.
|
|
31
78
|
*/
|
|
32
|
-
function mergeSelection(list, seen, incoming) {
|
|
79
|
+
function mergeSelection(list, seen, incoming, ctx) {
|
|
33
80
|
const key = incoming.alias ?? incoming.fieldName;
|
|
34
81
|
const existing = seen.get(key);
|
|
35
82
|
if (!existing) {
|
|
@@ -41,6 +88,12 @@ function mergeSelection(list, seen, incoming) {
|
|
|
41
88
|
mergeFragmentField(existing, incoming);
|
|
42
89
|
return;
|
|
43
90
|
}
|
|
91
|
+
if (ctx.strict &&
|
|
92
|
+
(!sameArguments(existing, incoming) || existing.typeVariants)) {
|
|
93
|
+
mergeTypeVariant(existing, incoming, key, ctx);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const priorTypes = existing.typeConditions;
|
|
44
97
|
if (existing.typeConditions && incoming.typeConditions)
|
|
45
98
|
existing.typeConditions = [
|
|
46
99
|
...new Set([...existing.typeConditions, ...incoming.typeConditions]),
|
|
@@ -48,12 +101,51 @@ function mergeSelection(list, seen, incoming) {
|
|
|
48
101
|
// Also selected outside any fragment → unconditional.
|
|
49
102
|
else
|
|
50
103
|
delete existing.typeConditions;
|
|
51
|
-
if (!sameArguments(existing, incoming))
|
|
104
|
+
if (!sameArguments(existing, incoming)) {
|
|
105
|
+
// v2.6.0 — GraphQL rejects one response key selected with different
|
|
106
|
+
// arguments when both selections can apply to the same object; 2.x
|
|
107
|
+
// keeps the first selection and drops the second. Fragments on types
|
|
108
|
+
// that can never be the same object are valid GraphQL: no warning
|
|
109
|
+
// (strict projects each per type, see `mergeTypeVariant`).
|
|
110
|
+
if (!canApplyToSameObject(priorTypes, incoming.typeConditions, ctx))
|
|
111
|
+
return;
|
|
112
|
+
parseWarn(ctx, {
|
|
113
|
+
code: 'selection-key-arguments',
|
|
114
|
+
detail: key,
|
|
115
|
+
problem: `The selectionSet selects "${key}" twice with different arguments; ` +
|
|
116
|
+
`2.x keeps the first selection and drops the second.`,
|
|
117
|
+
fix: 'reject the conflict, as GraphQL validation does',
|
|
118
|
+
});
|
|
52
119
|
return;
|
|
120
|
+
}
|
|
121
|
+
mergeSameArguments(existing, incoming, ctx);
|
|
122
|
+
}
|
|
123
|
+
/** Merge a repeat of a response key with the same arguments (v2.5.3). */
|
|
124
|
+
function mergeSameArguments(existing, incoming, ctx) {
|
|
125
|
+
const key = incoming.alias ?? incoming.fieldName;
|
|
126
|
+
if (existing.isConnection) {
|
|
127
|
+
if (ctx.strict) {
|
|
128
|
+
mergeConnectionEdgeSets(existing, incoming, ctx);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (!existing.totalCountOnly &&
|
|
132
|
+
!incoming.totalCountOnly &&
|
|
133
|
+
edgesKeyOf(existing) !== edgesKeyOf(incoming))
|
|
134
|
+
warnAliasedEdges(ctx, key, edgesKeyOf(existing), edgesKeyOf(incoming));
|
|
135
|
+
else
|
|
136
|
+
for (const which of ['node', 'properties']) {
|
|
137
|
+
const a = which === 'node' ? existing.children : existing.edgeChildren;
|
|
138
|
+
const b = which === 'node' ? incoming.children : incoming.edgeChildren;
|
|
139
|
+
const ka = existing.connectionAliases?.[which] ?? which;
|
|
140
|
+
const kb = incoming.connectionAliases?.[which] ?? which;
|
|
141
|
+
if (a && b && ka !== kb)
|
|
142
|
+
warnAliasedEdgeKey(ctx, key, which, ka, kb);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
53
145
|
if (incoming.children)
|
|
54
|
-
existing.children = mergeSelectionLists(existing.children, incoming.children);
|
|
146
|
+
existing.children = mergeSelectionLists(existing.children, incoming.children, ctx);
|
|
55
147
|
if (incoming.edgeChildren)
|
|
56
|
-
existing.edgeChildren = mergeSelectionLists(existing.edgeChildren, incoming.edgeChildren);
|
|
148
|
+
existing.edgeChildren = mergeSelectionLists(existing.edgeChildren, incoming.edgeChildren, ctx);
|
|
57
149
|
if (existing.isConnection) {
|
|
58
150
|
if (incoming.totalCount)
|
|
59
151
|
existing.totalCount = true;
|
|
@@ -66,6 +158,168 @@ function mergeSelection(list, seen, incoming) {
|
|
|
66
158
|
};
|
|
67
159
|
}
|
|
68
160
|
}
|
|
161
|
+
/** The response key of a connection's (first) `edges` selection. */
|
|
162
|
+
function edgesKeyOf(node) {
|
|
163
|
+
return node.connectionAliases?.edges ?? 'edges';
|
|
164
|
+
}
|
|
165
|
+
/** v2.6.0 — 2.x merges differently aliased `edges` under the first alias. */
|
|
166
|
+
function warnAliasedEdges(ctx, connection, first, second) {
|
|
167
|
+
parseWarn(ctx, {
|
|
168
|
+
code: 'selection-aliased-edges',
|
|
169
|
+
detail: `${connection}:${first}:${second}`,
|
|
170
|
+
problem: `"${connection}" selects \`edges\` under two response keys ("${first}", "${second}"); ` +
|
|
171
|
+
`2.x returns only "${first}", with both selections merged into it.`,
|
|
172
|
+
fix: 'return each `edges` alias with its own selection',
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/** v2.6.0 — 2.x merges differently aliased `node` / `properties` keys. */
|
|
176
|
+
function warnAliasedEdgeKey(ctx, connection, which, first, second) {
|
|
177
|
+
parseWarn(ctx, {
|
|
178
|
+
code: 'selection-aliased-edge-key',
|
|
179
|
+
detail: `${connection}:${which}:${first}:${second}`,
|
|
180
|
+
problem: `"${connection}" selects \`${which}\` under two response keys ("${first}", "${second}") in its edges; ` +
|
|
181
|
+
`2.x returns only "${first}", with both selections merged into it.`,
|
|
182
|
+
fix: `return each \`${which}\` alias with its own selection`,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
/** The `node` (or `properties`) keys of an edge set, main key first. */
|
|
186
|
+
function edgeKeyEntries(set, which) {
|
|
187
|
+
const main = which === 'node' ? set.children : set.edgeChildren;
|
|
188
|
+
const extras = (which === 'node' ? set.extraNodes : set.extraProperties) ?? [];
|
|
189
|
+
return [
|
|
190
|
+
...(main
|
|
191
|
+
? [{ key: set.connectionAliases?.[which] ?? which, children: main }]
|
|
192
|
+
: []),
|
|
193
|
+
...extras,
|
|
194
|
+
];
|
|
195
|
+
}
|
|
196
|
+
/** Store `entries` (main key first) back on an edge set (strict). */
|
|
197
|
+
function setEdgeKeyEntries(set, which, entries) {
|
|
198
|
+
const [main, ...rest] = entries;
|
|
199
|
+
const extrasProp = which === 'node' ? 'extraNodes' : 'extraProperties';
|
|
200
|
+
if (which === 'node')
|
|
201
|
+
set.children = main?.children;
|
|
202
|
+
else
|
|
203
|
+
set.edgeChildren = main?.children;
|
|
204
|
+
if (!main)
|
|
205
|
+
if (which === 'node')
|
|
206
|
+
delete set.children;
|
|
207
|
+
else
|
|
208
|
+
delete set.edgeChildren;
|
|
209
|
+
if (main && main.key !== which)
|
|
210
|
+
set.connectionAliases = { ...set.connectionAliases, [which]: main.key };
|
|
211
|
+
else if (set.connectionAliases) {
|
|
212
|
+
delete set.connectionAliases[which];
|
|
213
|
+
if (Object.keys(set.connectionAliases).length === 0)
|
|
214
|
+
delete set.connectionAliases;
|
|
215
|
+
}
|
|
216
|
+
if (rest.length > 0)
|
|
217
|
+
set[extrasProp] = rest;
|
|
218
|
+
else
|
|
219
|
+
delete set[extrasProp];
|
|
220
|
+
}
|
|
221
|
+
/** Merge two edge sets' `node` / `properties` keys by response key (strict). */
|
|
222
|
+
function mergeEdgeKeys(into, from, ctx) {
|
|
223
|
+
for (const which of ['node', 'properties']) {
|
|
224
|
+
const merged = edgeKeyEntries(into, which).map((e) => ({ ...e }));
|
|
225
|
+
for (const entry of edgeKeyEntries(from, which)) {
|
|
226
|
+
const same = merged.find((m) => m.key === entry.key);
|
|
227
|
+
if (same)
|
|
228
|
+
same.children = mergeSelectionLists(same.children, entry.children, ctx);
|
|
229
|
+
else
|
|
230
|
+
merged.push({ ...entry });
|
|
231
|
+
}
|
|
232
|
+
setEdgeKeyEntries(into, which, merged);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/** A connection's edges selection as a standalone edge set (strict). */
|
|
236
|
+
function asEdgeSet(node) {
|
|
237
|
+
if (node.totalCountOnly)
|
|
238
|
+
return null;
|
|
239
|
+
const set = {
|
|
240
|
+
fieldName: node.fieldName,
|
|
241
|
+
isScalar: false,
|
|
242
|
+
isRelationship: false,
|
|
243
|
+
isConnection: true,
|
|
244
|
+
};
|
|
245
|
+
if (node.children)
|
|
246
|
+
set.children = node.children;
|
|
247
|
+
if (node.edgeChildren)
|
|
248
|
+
set.edgeChildren = node.edgeChildren;
|
|
249
|
+
if (node.extraNodes)
|
|
250
|
+
set.extraNodes = node.extraNodes;
|
|
251
|
+
if (node.extraProperties)
|
|
252
|
+
set.extraProperties = node.extraProperties;
|
|
253
|
+
const { totalCount: _total, ...aliases } = node.connectionAliases ?? {};
|
|
254
|
+
if (Object.keys(aliases).length > 0)
|
|
255
|
+
set.connectionAliases = aliases;
|
|
256
|
+
return set;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* v2.6.0 — strict merge of a connection selected twice (same arguments):
|
|
260
|
+
* edge sets merge by response key, so `a: edges` and `b: edges` stay apart.
|
|
261
|
+
*/
|
|
262
|
+
function mergeConnectionEdgeSets(existing, incoming, ctx) {
|
|
263
|
+
const sets = new Map();
|
|
264
|
+
const add = (set) => {
|
|
265
|
+
const k = edgesKeyOf(set);
|
|
266
|
+
const prev = sets.get(k);
|
|
267
|
+
if (!prev) {
|
|
268
|
+
sets.set(k, {
|
|
269
|
+
...set,
|
|
270
|
+
...(set.connectionAliases
|
|
271
|
+
? { connectionAliases: { ...set.connectionAliases } }
|
|
272
|
+
: {}),
|
|
273
|
+
});
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
// v2.6.0 — `node` / `properties` keys merge by response key too.
|
|
277
|
+
mergeEdgeKeys(prev, set, ctx);
|
|
278
|
+
};
|
|
279
|
+
for (const node of [existing, incoming]) {
|
|
280
|
+
const main = asEdgeSet(node);
|
|
281
|
+
if (main)
|
|
282
|
+
add(main);
|
|
283
|
+
for (const extra of node.extraEdges ?? [])
|
|
284
|
+
add(extra);
|
|
285
|
+
}
|
|
286
|
+
const totalAlias = existing.connectionAliases?.totalCount ??
|
|
287
|
+
incoming.connectionAliases?.totalCount;
|
|
288
|
+
if (incoming.totalCount)
|
|
289
|
+
existing.totalCount = true;
|
|
290
|
+
const [main, ...rest] = [...sets.values()];
|
|
291
|
+
if (main) {
|
|
292
|
+
delete existing.totalCountOnly;
|
|
293
|
+
if (main.children)
|
|
294
|
+
existing.children = main.children;
|
|
295
|
+
else
|
|
296
|
+
delete existing.children;
|
|
297
|
+
if (main.edgeChildren)
|
|
298
|
+
existing.edgeChildren = main.edgeChildren;
|
|
299
|
+
else
|
|
300
|
+
delete existing.edgeChildren;
|
|
301
|
+
if (main.extraNodes)
|
|
302
|
+
existing.extraNodes = main.extraNodes;
|
|
303
|
+
else
|
|
304
|
+
delete existing.extraNodes;
|
|
305
|
+
if (main.extraProperties)
|
|
306
|
+
existing.extraProperties = main.extraProperties;
|
|
307
|
+
else
|
|
308
|
+
delete existing.extraProperties;
|
|
309
|
+
}
|
|
310
|
+
const aliases = {
|
|
311
|
+
...(main?.connectionAliases ?? {}),
|
|
312
|
+
...(totalAlias !== undefined ? { totalCount: totalAlias } : {}),
|
|
313
|
+
};
|
|
314
|
+
if (Object.keys(aliases).length > 0)
|
|
315
|
+
existing.connectionAliases = aliases;
|
|
316
|
+
else
|
|
317
|
+
delete existing.connectionAliases;
|
|
318
|
+
if (rest.length > 0)
|
|
319
|
+
existing.extraEdges = rest;
|
|
320
|
+
else
|
|
321
|
+
delete existing.extraEdges;
|
|
322
|
+
}
|
|
69
323
|
/** Two scalars under one key, from fragments on different types (v2.5.3). */
|
|
70
324
|
function mergeFragmentField(existing, incoming) {
|
|
71
325
|
// Only scalars selected by inline fragments can differ per type; any
|
|
@@ -87,11 +341,11 @@ function mergeFragmentField(existing, incoming) {
|
|
|
87
341
|
...new Set([...existing.typeConditions, ...incoming.typeConditions]),
|
|
88
342
|
];
|
|
89
343
|
}
|
|
90
|
-
function mergeSelectionLists(base, extra) {
|
|
344
|
+
function mergeSelectionLists(base, extra, ctx) {
|
|
91
345
|
const list = [];
|
|
92
346
|
const seen = new Map();
|
|
93
347
|
for (const node of [...(base ?? []), ...extra])
|
|
94
|
-
mergeSelection(list, seen, node);
|
|
348
|
+
mergeSelection(list, seen, node, ctx);
|
|
95
349
|
return list;
|
|
96
350
|
}
|
|
97
351
|
/** selectionSet field arguments the OGM honours, by field kind (v2.5.0). */
|
|
@@ -110,6 +364,43 @@ function assertNoVariables(value, where) {
|
|
|
110
364
|
for (const f of value.fields)
|
|
111
365
|
assertNoVariables(f.value, where);
|
|
112
366
|
}
|
|
367
|
+
/**
|
|
368
|
+
* v2.6.0 — whether `@include` / `@skip` keep a selectionSet field or inline
|
|
369
|
+
* fragment. 2.x ignores both directives (the selection is always kept) and
|
|
370
|
+
* warns; strict honours a literal `if: true | false` and rejects anything
|
|
371
|
+
* else, `$variables` included (selectionSet strings have no variables).
|
|
372
|
+
*/
|
|
373
|
+
function keptByDirectives(directives, where, ctx) {
|
|
374
|
+
const relevant = (directives ?? []).filter((d) => d.name.value === 'include' || d.name.value === 'skip');
|
|
375
|
+
if (relevant.length === 0)
|
|
376
|
+
return true;
|
|
377
|
+
if (!ctx.strict) {
|
|
378
|
+
parseWarn(ctx, {
|
|
379
|
+
code: 'selection-include-skip',
|
|
380
|
+
detail: where,
|
|
381
|
+
problem: `@include / @skip on "${where}" in a selectionSet is ignored in 2.x: ` +
|
|
382
|
+
`the selection is always kept.`,
|
|
383
|
+
fix: 'honour @include / @skip with a literal `if: true | false`',
|
|
384
|
+
});
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
let kept = true;
|
|
388
|
+
for (const directive of relevant) {
|
|
389
|
+
const name = directive.name.value;
|
|
390
|
+
const args = directive.arguments ?? [];
|
|
391
|
+
const arg = args.length === 1 ? args[0] : undefined;
|
|
392
|
+
if (!arg || arg.name.value !== 'if')
|
|
393
|
+
throw new errors_1.OGMError(`@${name} on "${where}" takes exactly one argument: if: true | false.`);
|
|
394
|
+
if (arg.value.kind === graphql_1.Kind.VARIABLE)
|
|
395
|
+
throw new errors_1.OGMError(`GraphQL variables are not supported in selectionSet directives (@${name} on "${where}": ` +
|
|
396
|
+
`$${arg.value.name.value}). Use a literal true or false.`);
|
|
397
|
+
if (arg.value.kind !== graphql_1.Kind.BOOLEAN)
|
|
398
|
+
throw new errors_1.OGMError(`@${name}(if:) on "${where}" must be a literal true or false.`);
|
|
399
|
+
if (name === 'include' ? !arg.value.value : arg.value.value)
|
|
400
|
+
kept = false;
|
|
401
|
+
}
|
|
402
|
+
return kept;
|
|
403
|
+
}
|
|
113
404
|
function assertNoArguments(field, where) {
|
|
114
405
|
if (field.arguments && field.arguments.length > 0)
|
|
115
406
|
throw new errors_1.OGMError(`Unsupported argument "${field.arguments[0].name.value}" on "${where}". ` +
|
|
@@ -191,7 +482,7 @@ function applyFieldArguments(field, node) {
|
|
|
191
482
|
* connection fields with edge properties, and nested traversals with depth limiting.
|
|
192
483
|
*/
|
|
193
484
|
class SelectionCompiler {
|
|
194
|
-
constructor(schema, whereCompiler) {
|
|
485
|
+
constructor(schema, whereCompiler, strictMode) {
|
|
195
486
|
this.schema = schema;
|
|
196
487
|
/**
|
|
197
488
|
* Real LRU cache of parsed selection sets, capped at
|
|
@@ -208,6 +499,17 @@ class SelectionCompiler {
|
|
|
208
499
|
*/
|
|
209
500
|
this.parseCache = new Map();
|
|
210
501
|
this.whereCompiler = whereCompiler;
|
|
502
|
+
this.strictMode = strictMode ?? (0, strict_mode_1.lenientStrictMode)();
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* v2.6.0 — re-log (through the warn-once log) the strict-mode warnings
|
|
506
|
+
* found when `parsed` was first parsed. Parsed selections are cached —
|
|
507
|
+
* here and in `Model`'s cross-OGM cache — so a cache hit must still warn
|
|
508
|
+
* on an OGM instance that has not seen the shape yet.
|
|
509
|
+
*/
|
|
510
|
+
replayParseWarnings(parsed) {
|
|
511
|
+
for (const w of parseWarnings.get(parsed) ?? [])
|
|
512
|
+
this.strictMode.warn(w.code, w.detail, w.problem, w.fix);
|
|
211
513
|
}
|
|
212
514
|
/** Clear the parse cache. Useful in tests to prevent cross-test pollution. */
|
|
213
515
|
clearCache() {
|
|
@@ -315,6 +617,15 @@ class SelectionCompiler {
|
|
|
315
617
|
: `.${(0, validation_1.escapeIdentifier)(node.fieldName)}`);
|
|
316
618
|
continue;
|
|
317
619
|
}
|
|
620
|
+
// v2.6.0 — strict: one key selected with different arguments by
|
|
621
|
+
// fragments on disjoint types, projected per concrete type.
|
|
622
|
+
if (node.typeVariants && node.typeVariants.length > 0) {
|
|
623
|
+
const variantCypher = this.compileTypeVariants(node, nodeVar, maxDepth, currentDepth, params, paramCounter, policyContext);
|
|
624
|
+
if (variantCypher) {
|
|
625
|
+
parts.push(`${key}: ${variantCypher}`);
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
318
629
|
// v2.5.3 — a relationship / connection field selected through an
|
|
319
630
|
// inline fragment on an abstract type that doesn't declare it.
|
|
320
631
|
// (Union relationship fields keep their per-member CASE below.)
|
|
@@ -435,6 +746,7 @@ class SelectionCompiler {
|
|
|
435
746
|
// order is the LRU iteration order we rely on for eviction.
|
|
436
747
|
this.parseCache.delete(selectionSet);
|
|
437
748
|
this.parseCache.set(selectionSet, cached);
|
|
749
|
+
this.replayParseWarnings(cached);
|
|
438
750
|
return cached;
|
|
439
751
|
}
|
|
440
752
|
const trimmed = selectionSet.trim();
|
|
@@ -453,7 +765,16 @@ class SelectionCompiler {
|
|
|
453
765
|
const dummyField = opDef.selectionSet.selections[0];
|
|
454
766
|
if (dummyField.kind !== graphql_1.Kind.FIELD || !dummyField.selectionSet)
|
|
455
767
|
return [];
|
|
456
|
-
const
|
|
768
|
+
const ctx = {
|
|
769
|
+
strict: this.strictMode.strict,
|
|
770
|
+
warnings: [],
|
|
771
|
+
possibleTypes: (typeName) => this.possibleTypes(typeName),
|
|
772
|
+
};
|
|
773
|
+
const result = this.convertSelectionSet(dummyField.selectionSet, undefined, ctx);
|
|
774
|
+
if (ctx.warnings.length > 0) {
|
|
775
|
+
parseWarnings.set(result, ctx.warnings);
|
|
776
|
+
this.replayParseWarnings(result);
|
|
777
|
+
}
|
|
457
778
|
// Real-LRU eviction: drop the oldest entry when the cache is full,
|
|
458
779
|
// then insert. Pre-1.7.5 we just stopped caching at saturation,
|
|
459
780
|
// forcing every subsequent miss to re-parse — a silent perf cliff
|
|
@@ -466,12 +787,24 @@ class SelectionCompiler {
|
|
|
466
787
|
this.parseCache.set(selectionSet, result);
|
|
467
788
|
return result;
|
|
468
789
|
}
|
|
790
|
+
/**
|
|
791
|
+
* v2.6.0 — the concrete types a fragment on `typeName` can apply to (see
|
|
792
|
+
* `ParseContext.possibleTypes`). Unknown names stand for themselves.
|
|
793
|
+
*/
|
|
794
|
+
possibleTypes(typeName) {
|
|
795
|
+
if (this.schema.nodes.has(typeName))
|
|
796
|
+
return [typeName];
|
|
797
|
+
const iface = this.schema.interfaces?.get(typeName);
|
|
798
|
+
if (iface)
|
|
799
|
+
return iface.implementedBy;
|
|
800
|
+
return this.schema.unions?.get(typeName) ?? [typeName];
|
|
801
|
+
}
|
|
469
802
|
/**
|
|
470
803
|
* Convert a GraphQL SelectionSetNode into SelectionNode[].
|
|
471
804
|
*/
|
|
472
805
|
convertSelectionSet(selectionSet,
|
|
473
806
|
/** Type condition of the enclosing inline fragment, if any (v2.4.8). */
|
|
474
|
-
typeCondition) {
|
|
807
|
+
typeCondition, ctx) {
|
|
475
808
|
const nodes = [];
|
|
476
809
|
const seen = new Map();
|
|
477
810
|
for (const sel of selectionSet.selections) {
|
|
@@ -482,15 +815,19 @@ class SelectionCompiler {
|
|
|
482
815
|
if (sel.kind === graphql_1.Kind.INLINE_FRAGMENT) {
|
|
483
816
|
const fragment = sel;
|
|
484
817
|
const condition = fragment.typeCondition?.name.value ?? typeCondition;
|
|
818
|
+
if (!keptByDirectives(fragment.directives, `... on ${condition ?? '(inline fragment)'}`, ctx))
|
|
819
|
+
continue;
|
|
485
820
|
if (fragment.selectionSet)
|
|
486
|
-
for (const merged of this.convertSelectionSet(fragment.selectionSet, condition))
|
|
487
|
-
mergeSelection(nodes, seen, merged);
|
|
821
|
+
for (const merged of this.convertSelectionSet(fragment.selectionSet, condition, ctx))
|
|
822
|
+
mergeSelection(nodes, seen, merged, ctx);
|
|
488
823
|
continue;
|
|
489
824
|
}
|
|
490
825
|
if (sel.kind !== graphql_1.Kind.FIELD)
|
|
491
826
|
continue;
|
|
492
827
|
const field = sel;
|
|
493
828
|
const fieldName = field.name.value;
|
|
829
|
+
if (!keptByDirectives(field.directives, field.alias?.value ?? fieldName, ctx))
|
|
830
|
+
continue;
|
|
494
831
|
// v2.5.0 — fields are keyed by their OUTPUT name, so the same field
|
|
495
832
|
// under two aliases (`a: kids { .. } b: kids { .. }`) stays two
|
|
496
833
|
// selections. Pre-2.5.0 they collapsed into one.
|
|
@@ -516,30 +853,82 @@ class SelectionCompiler {
|
|
|
516
853
|
if (hasChildren && field.selectionSet)
|
|
517
854
|
if (isConnection)
|
|
518
855
|
// For connections, parse edges -> node and edges -> properties
|
|
519
|
-
this.parseConnectionChildren(field.selectionSet, node);
|
|
856
|
+
this.parseConnectionChildren(field.selectionSet, node, ctx);
|
|
520
857
|
else
|
|
521
|
-
node.children = this.convertSelectionSet(field.selectionSet);
|
|
522
|
-
mergeSelection(nodes, seen, node);
|
|
858
|
+
node.children = this.convertSelectionSet(field.selectionSet, undefined, ctx);
|
|
859
|
+
mergeSelection(nodes, seen, node, ctx);
|
|
523
860
|
}
|
|
524
861
|
return nodes;
|
|
525
862
|
}
|
|
526
863
|
/**
|
|
527
864
|
* Parse the children of a connection field, extracting node and edge children.
|
|
528
865
|
*/
|
|
529
|
-
parseConnectionChildren(selectionSet, parentNode) {
|
|
866
|
+
parseConnectionChildren(selectionSet, parentNode, ctx) {
|
|
530
867
|
let selectsEdges = false;
|
|
868
|
+
// v2.6.0 — the response key of the first `edges`, and (strict) the edge
|
|
869
|
+
// sets of `edges` selected under other keys.
|
|
870
|
+
let firstEdgesKey;
|
|
871
|
+
const extras = new Map();
|
|
872
|
+
// v2.6.0 — the first `node` / `properties` key of each edge set.
|
|
873
|
+
const firstKeys = new Map();
|
|
874
|
+
const connectionName = parentNode.alias ?? parentNode.fieldName;
|
|
875
|
+
/**
|
|
876
|
+
* Where a `node` / `properties` selection under `key` goes: the edge
|
|
877
|
+
* set's own (first) key, or — strict, another key — its own entry.
|
|
878
|
+
*/
|
|
879
|
+
const edgeKeyTarget = (set, which, key) => {
|
|
880
|
+
const firsts = firstKeys.get(set) ?? {};
|
|
881
|
+
firstKeys.set(set, firsts);
|
|
882
|
+
firsts[which] ?? (firsts[which] = key);
|
|
883
|
+
if (firsts[which] === key)
|
|
884
|
+
return null;
|
|
885
|
+
if (!ctx.strict) {
|
|
886
|
+
warnAliasedEdgeKey(ctx, connectionName, which, firsts[which], key);
|
|
887
|
+
return null;
|
|
888
|
+
}
|
|
889
|
+
const prop = which === 'node' ? 'extraNodes' : 'extraProperties';
|
|
890
|
+
const list = (set[prop] ?? (set[prop] = []));
|
|
891
|
+
let entry = list.find((e) => e.key === key);
|
|
892
|
+
if (!entry) {
|
|
893
|
+
(0, validation_1.assertSafeIdentifier)(key, 'selection alias');
|
|
894
|
+
entry = { key, children: [] };
|
|
895
|
+
list.push(entry);
|
|
896
|
+
}
|
|
897
|
+
return entry;
|
|
898
|
+
};
|
|
531
899
|
for (const sel of selectionSet.selections) {
|
|
532
900
|
if (sel.kind !== graphql_1.Kind.FIELD)
|
|
533
901
|
continue;
|
|
534
902
|
const field = sel;
|
|
535
903
|
assertNoArguments(field, `${parentNode.fieldName}.${field.name.value}`);
|
|
904
|
+
if (!keptByDirectives(field.directives, `${parentNode.fieldName}.${field.name.value}`, ctx))
|
|
905
|
+
continue;
|
|
536
906
|
if (field.name.value === 'totalCount') {
|
|
537
907
|
parentNode.totalCount = true;
|
|
538
908
|
this.recordConnectionAlias(parentNode, 'totalCount', field);
|
|
539
909
|
}
|
|
910
|
+
let target = parentNode;
|
|
540
911
|
if (field.name.value === 'edges') {
|
|
541
912
|
selectsEdges = true;
|
|
542
|
-
|
|
913
|
+
const edgesKey = field.alias?.value ?? 'edges';
|
|
914
|
+
firstEdgesKey ?? (firstEdgesKey = edgesKey);
|
|
915
|
+
if (edgesKey !== firstEdgesKey)
|
|
916
|
+
if (ctx.strict) {
|
|
917
|
+
let extra = extras.get(edgesKey);
|
|
918
|
+
if (!extra) {
|
|
919
|
+
extra = {
|
|
920
|
+
fieldName: parentNode.fieldName,
|
|
921
|
+
isScalar: false,
|
|
922
|
+
isRelationship: false,
|
|
923
|
+
isConnection: true,
|
|
924
|
+
};
|
|
925
|
+
extras.set(edgesKey, extra);
|
|
926
|
+
}
|
|
927
|
+
target = extra;
|
|
928
|
+
}
|
|
929
|
+
else
|
|
930
|
+
warnAliasedEdges(ctx, parentNode.alias ?? parentNode.fieldName, firstEdgesKey, edgesKey);
|
|
931
|
+
this.recordConnectionAlias(target, 'edges', field);
|
|
543
932
|
}
|
|
544
933
|
if (field.name.value === 'edges' && field.selectionSet)
|
|
545
934
|
for (const edgeSel of field.selectionSet.selections) {
|
|
@@ -547,20 +936,36 @@ class SelectionCompiler {
|
|
|
547
936
|
continue;
|
|
548
937
|
const edgeField = edgeSel;
|
|
549
938
|
assertNoArguments(edgeField, `${parentNode.fieldName}.edges.${edgeField.name.value}`);
|
|
939
|
+
if (!keptByDirectives(edgeField.directives, `${parentNode.fieldName}.edges.${edgeField.name.value}`, ctx))
|
|
940
|
+
continue;
|
|
550
941
|
// v2.5.3 — a repeated `edges { node { … } }` merges its
|
|
551
942
|
// sub-selections; pre-2.5.3 the last one replaced the others.
|
|
552
943
|
if (edgeField.name.value === 'node' && edgeField.selectionSet) {
|
|
553
|
-
|
|
554
|
-
|
|
944
|
+
const extra = edgeKeyTarget(target, 'node', edgeField.alias?.value ?? 'node');
|
|
945
|
+
const parsed = this.convertSelectionSet(edgeField.selectionSet, undefined, ctx);
|
|
946
|
+
if (extra)
|
|
947
|
+
extra.children = mergeSelectionLists(extra.children, parsed, ctx);
|
|
948
|
+
else {
|
|
949
|
+
this.recordConnectionAlias(target, 'node', edgeField);
|
|
950
|
+
target.children = mergeSelectionLists(target.children, parsed, ctx);
|
|
951
|
+
}
|
|
555
952
|
}
|
|
556
953
|
if (edgeField.name.value === 'properties' && edgeField.selectionSet) {
|
|
557
|
-
|
|
558
|
-
|
|
954
|
+
const extra = edgeKeyTarget(target, 'properties', edgeField.alias?.value ?? 'properties');
|
|
955
|
+
const parsed = this.convertSelectionSet(edgeField.selectionSet, undefined, ctx);
|
|
956
|
+
if (extra)
|
|
957
|
+
extra.children = mergeSelectionLists(extra.children, parsed, ctx);
|
|
958
|
+
else {
|
|
959
|
+
this.recordConnectionAlias(target, 'properties', edgeField);
|
|
960
|
+
target.edgeChildren = mergeSelectionLists(target.edgeChildren, parsed, ctx);
|
|
961
|
+
}
|
|
559
962
|
}
|
|
560
963
|
}
|
|
561
964
|
}
|
|
562
965
|
if (parentNode.totalCount && !selectsEdges)
|
|
563
966
|
parentNode.totalCountOnly = true;
|
|
967
|
+
if (extras.size > 0)
|
|
968
|
+
parentNode.extraEdges = [...extras.values()];
|
|
564
969
|
}
|
|
565
970
|
/**
|
|
566
971
|
* Keep the selectionSet alias of a connection key (v2.5.3). The first
|
|
@@ -586,12 +991,40 @@ class SelectionCompiler {
|
|
|
586
991
|
* Returns `null` (unchanged behaviour) when no fragment type declares it.
|
|
587
992
|
*/
|
|
588
993
|
compileFragmentRelationship(node, nodeVar, maxDepth, currentDepth, params, paramCounter, policyContext = null) {
|
|
994
|
+
const { branches, singular } = this.fragmentRelationshipBranches(node, node.typeConditions ?? [], nodeVar, maxDepth, currentDepth, params, paramCounter, policyContext);
|
|
995
|
+
if (branches.length === 0)
|
|
996
|
+
return null;
|
|
997
|
+
return `CASE ${branches.join(' ')} ELSE ${singular ? 'null' : '[]'} END`;
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* v2.6.0 — strict: a relationship / connection key with per-type
|
|
1001
|
+
* variants (`SelectionNode.typeVariants`): one `CASE` whose branches
|
|
1002
|
+
* compile each variant, with its own arguments and the target's read
|
|
1003
|
+
* policy, against the concrete types its fragments apply to.
|
|
1004
|
+
*/
|
|
1005
|
+
compileTypeVariants(node, nodeVar, maxDepth, currentDepth, params, paramCounter, policyContext = null) {
|
|
1006
|
+
const branches = [];
|
|
1007
|
+
let singular = false;
|
|
1008
|
+
for (const variant of [node, ...(node.typeVariants ?? [])]) {
|
|
1009
|
+
const types = [
|
|
1010
|
+
...new Set((variant.typeConditions ?? []).flatMap((t) => this.possibleTypes(t))),
|
|
1011
|
+
];
|
|
1012
|
+
const part = this.fragmentRelationshipBranches(variant, types, nodeVar, maxDepth, currentDepth, params, paramCounter, policyContext);
|
|
1013
|
+
branches.push(...part.branches);
|
|
1014
|
+
singular || (singular = part.singular);
|
|
1015
|
+
}
|
|
1016
|
+
if (branches.length === 0)
|
|
1017
|
+
return null;
|
|
1018
|
+
return `CASE ${branches.join(' ')} ELSE ${singular ? 'null' : '[]'} END`;
|
|
1019
|
+
}
|
|
1020
|
+
/** `WHEN n:T THEN <projection>` per type of `typeNames` declaring the field. */
|
|
1021
|
+
fragmentRelationshipBranches(node, typeNames, nodeVar, maxDepth, currentDepth, params, paramCounter, policyContext = null) {
|
|
589
1022
|
const relField = node.isConnection
|
|
590
1023
|
? node.fieldName.replace(/Connection$/, '')
|
|
591
1024
|
: node.fieldName;
|
|
592
1025
|
const branches = [];
|
|
593
1026
|
let singular = node.isConnection;
|
|
594
|
-
for (const typeName of
|
|
1027
|
+
for (const typeName of typeNames) {
|
|
595
1028
|
const def = this.schema.nodes.get(typeName);
|
|
596
1029
|
const relDef = def?.relationships.get(relField);
|
|
597
1030
|
if (!def || !relDef)
|
|
@@ -604,9 +1037,7 @@ class SelectionCompiler {
|
|
|
604
1037
|
if (expr)
|
|
605
1038
|
branches.push(`WHEN ${nodeVar}:${(0, validation_1.escapeIdentifier)(def.label)} THEN ${expr}`);
|
|
606
1039
|
}
|
|
607
|
-
|
|
608
|
-
return null;
|
|
609
|
-
return `CASE ${branches.join(' ')} ELSE ${singular ? 'null' : '[]'} END`;
|
|
1040
|
+
return { branches, singular };
|
|
610
1041
|
}
|
|
611
1042
|
/**
|
|
612
1043
|
* Compile a relationship field into a Cypher pattern comprehension.
|
|
@@ -839,6 +1270,11 @@ class SelectionCompiler {
|
|
|
839
1270
|
mapParts.push(`${propertiesKey}: ${edgeVar} { ${edgeFields} }`);
|
|
840
1271
|
projectionKeys.push(propertiesKey);
|
|
841
1272
|
}
|
|
1273
|
+
// v2.6.0 — strict: further `node` / `properties` keys of these edges.
|
|
1274
|
+
for (const [k, expr] of this.extraEdgeKeyParts(node, childVar, edgeVar, targetDef, maxDepth, currentDepth, params, paramCounter, policyContext)) {
|
|
1275
|
+
mapParts.push(`${k}: ${expr}`);
|
|
1276
|
+
projectionKeys.push(k);
|
|
1277
|
+
}
|
|
842
1278
|
// Inject synthetic sort scalars at the top level of the edge map so
|
|
843
1279
|
// apoc.coll.sortMulti (which only accepts top-level keys) can order the
|
|
844
1280
|
// collection. The outer list comprehension strips them on the way out.
|
|
@@ -985,7 +1421,12 @@ class SelectionCompiler {
|
|
|
985
1421
|
: '';
|
|
986
1422
|
if (node.totalCountOnly)
|
|
987
1423
|
return `{ ${totalCount} }`;
|
|
988
|
-
|
|
1424
|
+
// v2.6.0 — strict: `edges` selected under further response keys, each
|
|
1425
|
+
// its own list over the same pattern and filter. Empty otherwise.
|
|
1426
|
+
const extraEdges = (node.extraEdges ?? [])
|
|
1427
|
+
.map((extra) => `, ${(0, validation_1.escapeIdentifier)(edgesKeyOf(extra))}: [${pattern}${whereClause} | ${this.compileEdgeSetMap(extra, childVar, edgeVar, targetDef, maxDepth, currentDepth, params, paramCounter, policyContext)}]`)
|
|
1428
|
+
.join('');
|
|
1429
|
+
const withTotal = `${extraEdges}${totalCount ? `, ${totalCount}` : ''}`;
|
|
989
1430
|
if (sortKeyTokens.length === 0)
|
|
990
1431
|
return `{ ${edgesKey}: [${pattern}${whereClause} | ${innerMap}]${withTotal} }`;
|
|
991
1432
|
// Re-project only the user-facing keys so synthetic __skN scalars don't
|
|
@@ -995,6 +1436,61 @@ class SelectionCompiler {
|
|
|
995
1436
|
: `{}`;
|
|
996
1437
|
return `{ ${edgesKey}: [x IN apoc.coll.sortMulti([${pattern}${whereClause} | ${innerMap}], [${sortKeyTokens.join(', ')}]) | ${outerProjection}]${withTotal} }`;
|
|
997
1438
|
}
|
|
1439
|
+
/**
|
|
1440
|
+
* v2.6.0 — the `{ node: …, properties: … }` map of one strict-mode extra
|
|
1441
|
+
* edge set (see `SelectionNode.extraEdges`), mirroring `compileConnection`.
|
|
1442
|
+
*/
|
|
1443
|
+
compileEdgeSetMap(set, childVar, edgeVar, targetDef, maxDepth, currentDepth, params, paramCounter, policyContext = null) {
|
|
1444
|
+
const parts = [];
|
|
1445
|
+
const aliases = set.connectionAliases ?? {};
|
|
1446
|
+
if (set.children && set.children.length > 0)
|
|
1447
|
+
parts.push(`${aliases.node ? (0, validation_1.escapeIdentifier)(aliases.node) : 'node'}: ${this.compile(set.children, childVar, targetDef, maxDepth, currentDepth + 1, params, paramCounter, null, policyContext)}`);
|
|
1448
|
+
if (set.edgeChildren && set.edgeChildren.length > 0) {
|
|
1449
|
+
const edgeFields = set.edgeChildren
|
|
1450
|
+
.filter((c) => c.isScalar)
|
|
1451
|
+
.map((c) => {
|
|
1452
|
+
if (c.alias === undefined || c.alias === c.fieldName)
|
|
1453
|
+
return `.${(0, validation_1.escapeIdentifier)(c.fieldName)}`;
|
|
1454
|
+
(0, validation_1.assertSafeIdentifier)(c.alias, 'selection alias');
|
|
1455
|
+
return `${(0, validation_1.escapeIdentifier)(c.alias)}: ${edgeVar}.${(0, validation_1.escapeIdentifier)(c.fieldName)}`;
|
|
1456
|
+
})
|
|
1457
|
+
.join(', ');
|
|
1458
|
+
parts.push(`${aliases.properties ? (0, validation_1.escapeIdentifier)(aliases.properties) : 'properties'}: ${edgeVar} { ${edgeFields} }`);
|
|
1459
|
+
}
|
|
1460
|
+
for (const [k, expr] of this.extraEdgeKeyParts(set, childVar, edgeVar, targetDef, maxDepth, currentDepth, params, paramCounter, policyContext))
|
|
1461
|
+
parts.push(`${k}: ${expr}`);
|
|
1462
|
+
return `{ ${parts.join(', ')} }`;
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* v2.6.0 — strict: the projections of an edge set's further `node` /
|
|
1466
|
+
* `properties` keys (`SelectionNode.extraNodes` / `extraProperties`),
|
|
1467
|
+
* as `[escapedKey, expression]` pairs. Empty in 2.x default mode.
|
|
1468
|
+
*/
|
|
1469
|
+
extraEdgeKeyParts(set, childVar, edgeVar, targetDef, maxDepth, currentDepth, params, paramCounter, policyContext = null) {
|
|
1470
|
+
const out = [];
|
|
1471
|
+
for (const extra of set.extraNodes ?? [])
|
|
1472
|
+
if (extra.children.length > 0)
|
|
1473
|
+
out.push([
|
|
1474
|
+
(0, validation_1.escapeIdentifier)(extra.key),
|
|
1475
|
+
this.compile(extra.children, childVar, targetDef, maxDepth, currentDepth + 1, params, paramCounter, null, policyContext),
|
|
1476
|
+
]);
|
|
1477
|
+
for (const extra of set.extraProperties ?? []) {
|
|
1478
|
+
const fields = extra.children
|
|
1479
|
+
.filter((c) => c.isScalar)
|
|
1480
|
+
.map((c) => {
|
|
1481
|
+
if (c.alias === undefined || c.alias === c.fieldName)
|
|
1482
|
+
return `.${(0, validation_1.escapeIdentifier)(c.fieldName)}`;
|
|
1483
|
+
(0, validation_1.assertSafeIdentifier)(c.alias, 'selection alias');
|
|
1484
|
+
return `${(0, validation_1.escapeIdentifier)(c.alias)}: ${edgeVar}.${(0, validation_1.escapeIdentifier)(c.fieldName)}`;
|
|
1485
|
+
});
|
|
1486
|
+
if (fields.length > 0)
|
|
1487
|
+
out.push([
|
|
1488
|
+
(0, validation_1.escapeIdentifier)(extra.key),
|
|
1489
|
+
`${edgeVar} { ${fields.join(', ')} }`,
|
|
1490
|
+
]);
|
|
1491
|
+
}
|
|
1492
|
+
return out;
|
|
1493
|
+
}
|
|
998
1494
|
/**
|
|
999
1495
|
* AND-combine an optional user where (`select.where` / `connectionWhere`)
|
|
1000
1496
|
* with the target type's `'read'` policy and emit a `WHERE …` fragment
|