@apollo/federation-internals 2.1.0-alpha.1 → 2.1.0-alpha.4
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 +13 -0
- package/dist/buildSchema.d.ts.map +1 -1
- package/dist/buildSchema.js +13 -3
- package/dist/buildSchema.js.map +1 -1
- package/dist/definitions.d.ts +47 -8
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +137 -23
- package/dist/definitions.js.map +1 -1
- package/dist/error.d.ts +1 -0
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +2 -0
- package/dist/error.js.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.js +9 -0
- package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
- package/dist/federation.d.ts +5 -1
- package/dist/federation.d.ts.map +1 -1
- package/dist/federation.js +13 -5
- package/dist/federation.js.map +1 -1
- package/dist/federationSpec.d.ts +13 -9
- package/dist/federationSpec.d.ts.map +1 -1
- package/dist/federationSpec.js +27 -5
- package/dist/federationSpec.js.map +1 -1
- package/dist/inaccessibleSpec.js +1 -2
- package/dist/inaccessibleSpec.js.map +1 -1
- package/dist/operations.d.ts +48 -21
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +329 -48
- package/dist/operations.js.map +1 -1
- package/dist/print.d.ts +1 -1
- package/dist/print.d.ts.map +1 -1
- package/dist/print.js +1 -1
- package/dist/print.js.map +1 -1
- package/dist/schemaUpgrader.js +2 -2
- package/dist/schemaUpgrader.js.map +1 -1
- package/dist/utils.d.ts +9 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +31 -1
- package/dist/utils.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/definitions.test.ts +18 -0
- package/src/__tests__/operations.test.ts +217 -99
- package/src/__tests__/subgraphValidation.test.ts +2 -0
- package/src/buildSchema.ts +19 -5
- package/src/definitions.ts +217 -29
- package/src/error.ts +7 -0
- package/src/extractSubgraphsFromSupergraph.ts +20 -0
- package/src/federation.ts +16 -5
- package/src/federationSpec.ts +32 -5
- package/src/inaccessibleSpec.ts +2 -5
- package/src/operations.ts +520 -71
- package/src/print.ts +1 -1
- package/src/schemaUpgrader.ts +2 -2
- package/src/utils.ts +40 -0
- package/tsconfig.test.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
package/src/operations.ts
CHANGED
|
@@ -39,14 +39,16 @@ import {
|
|
|
39
39
|
variableDefinitionsFromAST,
|
|
40
40
|
CompositeType,
|
|
41
41
|
typenameFieldName,
|
|
42
|
-
NamedType,
|
|
43
42
|
sameDirectiveApplications,
|
|
44
43
|
isConditionalDirective,
|
|
45
44
|
isDirectiveApplicationsSubset,
|
|
45
|
+
isAbstractType,
|
|
46
|
+
DeferDirectiveArgs,
|
|
47
|
+
Variable,
|
|
46
48
|
} from "./definitions";
|
|
47
49
|
import { ERRORS } from "./error";
|
|
48
|
-
import { sameType } from "./types";
|
|
49
|
-
import { assert, mapEntries, MapWithCachedArrays, MultiMap } from "./utils";
|
|
50
|
+
import { isDirectSubtype, sameType } from "./types";
|
|
51
|
+
import { assert, mapEntries, MapWithCachedArrays, MultiMap, SetMultiMap } from "./utils";
|
|
50
52
|
import { argumentsEquals, argumentsFromAST, isValidValue, valueToAST, valueToString } from "./values";
|
|
51
53
|
|
|
52
54
|
function validate(condition: any, message: () => string, sourceAST?: ASTNode): asserts condition {
|
|
@@ -178,27 +180,46 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
|
|
|
178
180
|
updateForAddingTo(selectionSet: SelectionSet): Field<TArgs> {
|
|
179
181
|
const selectionParent = selectionSet.parentType;
|
|
180
182
|
const fieldParent = this.definition.parent;
|
|
181
|
-
if (selectionParent
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
183
|
+
if (selectionParent === fieldParent) {
|
|
184
|
+
return this;
|
|
185
|
+
}
|
|
185
186
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
187
|
+
if (this.name === typenameFieldName) {
|
|
188
|
+
return this.withUpdatedDefinition(selectionParent.typenameField()!);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// We accept adding a selection of an interface field to a selection of one of its subtype. But otherwise, it's invalid.
|
|
192
|
+
// Do note that the field might come from a supergraph while the selection is on a subgraph, so we avoid relying on isDirectSubtype (because
|
|
193
|
+
// isDirectSubtype relies on the subtype knowing which interface it implements, but the one of the subgraph might not declare implementing
|
|
194
|
+
// the supergraph interface, even if it does in the subgraph).
|
|
195
|
+
validate(
|
|
196
|
+
selectionParent.name == fieldParent.name
|
|
197
|
+
|| (
|
|
191
198
|
!isUnionType(selectionParent)
|
|
192
199
|
&& (
|
|
193
200
|
(isInterfaceType(fieldParent) && fieldParent.allImplementations().some(i => i.name == selectionParent.name))
|
|
194
201
|
|| (isObjectType(fieldParent) && fieldParent.name == selectionParent.name)
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
+
)
|
|
203
|
+
),
|
|
204
|
+
() => `Cannot add selection of field "${this.definition.coordinate}" to selection set of parent type "${selectionSet.parentType}"`
|
|
205
|
+
);
|
|
206
|
+
const fieldDef = selectionParent.field(this.name);
|
|
207
|
+
validate(fieldDef, () => `Cannot add selection of field "${this.definition.coordinate}" to selection set of parent type "${selectionParent}" (that does not declare that field)`);
|
|
208
|
+
return this.withUpdatedDefinition(fieldDef);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
hasDefer(): boolean {
|
|
212
|
+
// @defer cannot be on field at the moment
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
deferDirectiveArgs(): undefined {
|
|
217
|
+
// @defer cannot be on field at the moment (but exists so we can call this method on any `OperationElement` conveniently)
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
withoutDefer(): Field<TArgs> {
|
|
222
|
+
// @defer cannot be on field at the moment
|
|
202
223
|
return this;
|
|
203
224
|
}
|
|
204
225
|
|
|
@@ -243,8 +264,15 @@ export class FragmentElement extends AbstractOperationElement<FragmentElement> {
|
|
|
243
264
|
return this.sourceType;
|
|
244
265
|
}
|
|
245
266
|
|
|
267
|
+
castedType(): CompositeType {
|
|
268
|
+
return this.typeCondition ? this.typeCondition : this.sourceType;
|
|
269
|
+
}
|
|
270
|
+
|
|
246
271
|
withUpdatedSourceType(newSourceType: CompositeType): FragmentElement {
|
|
247
|
-
|
|
272
|
+
// Note that we pass the type-condition name instead of the type itself, to ensure that if `newSourceType` was from a different
|
|
273
|
+
// schema (typically, the supergraph) than `this.sourceType` (typically, a subgraph), then the new condition uses the
|
|
274
|
+
// definition of the proper schema (the supergraph in such cases, instead of the subgraph).
|
|
275
|
+
const newFragment = new FragmentElement(newSourceType, this.typeCondition?.name);
|
|
248
276
|
for (const directive of this.appliedDirectives) {
|
|
249
277
|
newFragment.applyDirective(directive.definition!, directive.arguments());
|
|
250
278
|
}
|
|
@@ -266,6 +294,106 @@ export class FragmentElement extends AbstractOperationElement<FragmentElement> {
|
|
|
266
294
|
return this;
|
|
267
295
|
}
|
|
268
296
|
|
|
297
|
+
hasDefer(): boolean {
|
|
298
|
+
return this.hasAppliedDirective('defer');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
hasStream(): boolean {
|
|
302
|
+
return this.hasAppliedDirective('stream');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
deferDirectiveArgs(): DeferDirectiveArgs | undefined {
|
|
306
|
+
// Note: @defer is not repeatable, so the return array below is either empty, or has a single value.
|
|
307
|
+
return this.appliedDirectivesOf(this.schema().deferDirective())[0]?.arguments();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Returns this fragment element but with any @defer directive on it removed.
|
|
312
|
+
*
|
|
313
|
+
* This method will return `undefined` if, upon removing @defer, the fragment has no conditions nor
|
|
314
|
+
* any remaining applied directives (meaning that it carries no information whatsoever and can be
|
|
315
|
+
* ignored).
|
|
316
|
+
*/
|
|
317
|
+
withoutDefer(): FragmentElement | undefined {
|
|
318
|
+
const deferName = this.schema().deferDirective().name;
|
|
319
|
+
const updatedDirectives = this.appliedDirectives.filter((d) => d.name !== deferName);
|
|
320
|
+
if (!this.typeCondition && updatedDirectives.length === 0) {
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (updatedDirectives.length === this.appliedDirectives.length) {
|
|
325
|
+
return this;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const updated = new FragmentElement(this.sourceType, this.typeCondition);
|
|
329
|
+
updatedDirectives.forEach((d) => updated.applyDirective(d.definition!, d.arguments()));
|
|
330
|
+
return updated;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Returns this fragment element, but it is has a @defer directive, the element is returned with
|
|
335
|
+
* the @defer "normalized".
|
|
336
|
+
*
|
|
337
|
+
* See `Operation.withNormalizedDefer` for details on our so-called @defer normalization.
|
|
338
|
+
*/
|
|
339
|
+
withNormalizedDefer(normalizer: DeferNormalizer): FragmentElement | undefined {
|
|
340
|
+
const deferArgs = this.deferDirectiveArgs();
|
|
341
|
+
if (!deferArgs) {
|
|
342
|
+
return this;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
let newDeferArgs: DeferDirectiveArgs | undefined = undefined;
|
|
346
|
+
let conditionVariable: Variable | undefined = undefined;
|
|
347
|
+
if (deferArgs.if !== undefined) {
|
|
348
|
+
if (typeof deferArgs.if === 'boolean') {
|
|
349
|
+
if (deferArgs.if) {
|
|
350
|
+
// Harcoded `if: true`, remove the `if`
|
|
351
|
+
newDeferArgs = {
|
|
352
|
+
...deferArgs,
|
|
353
|
+
if: undefined,
|
|
354
|
+
}
|
|
355
|
+
} else {
|
|
356
|
+
// Harcoded `if: false`, remove the @defer altogether
|
|
357
|
+
return this.withoutDefer();
|
|
358
|
+
}
|
|
359
|
+
} else {
|
|
360
|
+
// `if` on a variable
|
|
361
|
+
conditionVariable = deferArgs.if;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
let label = deferArgs.label;
|
|
366
|
+
if (!label) {
|
|
367
|
+
label = normalizer.newLabel();
|
|
368
|
+
if (newDeferArgs) {
|
|
369
|
+
newDeferArgs.label = label;
|
|
370
|
+
} else {
|
|
371
|
+
newDeferArgs = {
|
|
372
|
+
...deferArgs,
|
|
373
|
+
label,
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Now that we are sure to have a label, if we had a (non-trivial) condition,
|
|
379
|
+
// associate it to that label.
|
|
380
|
+
if (conditionVariable) {
|
|
381
|
+
normalizer.registerCondition(label, conditionVariable);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (!newDeferArgs) {
|
|
385
|
+
return this;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const updated = new FragmentElement(this.sourceType, this.typeCondition);
|
|
389
|
+
const deferDirective = this.schema().deferDirective();
|
|
390
|
+
// Re-apply all the non-defer directives
|
|
391
|
+
this.appliedDirectives.filter((d) => d.name !== deferDirective.name).forEach((d) => updated.applyDirective(d.definition!, d.arguments()));
|
|
392
|
+
// And then re-apply the @defer with the new label.
|
|
393
|
+
updated.applyDirective(this.schema().deferDirective(), newDeferArgs);
|
|
394
|
+
return updated;
|
|
395
|
+
}
|
|
396
|
+
|
|
269
397
|
equals(that: OperationElement): boolean {
|
|
270
398
|
if (this === that) {
|
|
271
399
|
return true;
|
|
@@ -360,30 +488,37 @@ export class Operation {
|
|
|
360
488
|
}
|
|
361
489
|
|
|
362
490
|
optimize(fragments?: NamedFragments, minUsagesToOptimize: number = 2): Operation {
|
|
363
|
-
|
|
491
|
+
assert(minUsagesToOptimize >= 1, `Expected 'minUsagesToOptimize' to be at least 1, but got ${minUsagesToOptimize}`)
|
|
492
|
+
if (!fragments || fragments.isEmpty()) {
|
|
364
493
|
return this;
|
|
365
494
|
}
|
|
495
|
+
|
|
366
496
|
let optimizedSelection = this.selectionSet.optimize(fragments);
|
|
367
497
|
if (optimizedSelection === this.selectionSet) {
|
|
368
498
|
return this;
|
|
369
499
|
}
|
|
370
500
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
if (minUsagesToOptimize > 1) {
|
|
377
|
-
const usages = new Map<string, number>();
|
|
378
|
-
optimizedSelection.collectUsedFragmentNames(usages);
|
|
379
|
-
for (const fragment of fragments.names()) {
|
|
380
|
-
if (!usages.has(fragment)) {
|
|
381
|
-
usages.set(fragment, 0);
|
|
382
|
-
}
|
|
501
|
+
const usages = new Map<string, number>();
|
|
502
|
+
optimizedSelection.collectUsedFragmentNames(usages);
|
|
503
|
+
for (const fragment of fragments.names()) {
|
|
504
|
+
if (!usages.has(fragment)) {
|
|
505
|
+
usages.set(fragment, 0);
|
|
383
506
|
}
|
|
384
|
-
const toDeoptimize = mapEntries(usages).filter(([_, count]) => count < minUsagesToOptimize).map(([name]) => name);
|
|
385
|
-
optimizedSelection = optimizedSelection.expandFragments(toDeoptimize);
|
|
386
507
|
}
|
|
508
|
+
|
|
509
|
+
// We re-expand any fragments that is used less than our minimum. Optimizing all fragments to potentially
|
|
510
|
+
// re-expand some is not entirely optimal, but it's simple and probably don't matter too much in practice
|
|
511
|
+
// (we only call this optimization on the final computed query plan, so not a very hot path; plus in most
|
|
512
|
+
// cases we won't even reach that point either because there is no fragment, or none will have been
|
|
513
|
+
// optimized away so we'll exit above). We can optimize later if this show up in profiling though.
|
|
514
|
+
//
|
|
515
|
+
// Also note `toDeoptimize` will always contains the unused fragments, which will allow `expandFragments`
|
|
516
|
+
// to remove them from the listed fragments in `optimizedSelection` (here again, this could make use call
|
|
517
|
+
// `expandFragments` on _only_ unused fragments and that case could be dealt with more efficiently, but
|
|
518
|
+
// probably not noticeable in practice so ...).
|
|
519
|
+
const toDeoptimize = mapEntries(usages).filter(([_, count]) => count < minUsagesToOptimize).map(([name]) => name);
|
|
520
|
+
optimizedSelection = optimizedSelection.expandFragments(toDeoptimize);
|
|
521
|
+
|
|
387
522
|
return new Operation(this.rootKind, optimizedSelection, this.variableDefinitions, this.name);
|
|
388
523
|
}
|
|
389
524
|
|
|
@@ -401,6 +536,61 @@ export class Operation {
|
|
|
401
536
|
);
|
|
402
537
|
}
|
|
403
538
|
|
|
539
|
+
/**
|
|
540
|
+
* Returns this operation but potentially modified so all/some of the @defer applications have been removed.
|
|
541
|
+
*
|
|
542
|
+
* @param labelsToRemove - If provided, then only the `@defer` applications with labels in the provided
|
|
543
|
+
* set will be remove. Other `@defer` applications will be untouched. If `undefined`, then all `@defer`
|
|
544
|
+
* applications are removed.
|
|
545
|
+
*/
|
|
546
|
+
withoutDefer(labelsToRemove?: Set<string>): Operation {
|
|
547
|
+
// If we have named fragments, we should be looking inside those and either expand those having @defer or,
|
|
548
|
+
// probably better, replace them with a verison without @defer. But as we currently only call this method
|
|
549
|
+
// after `expandAllFragments`, we'll implement this when/if we need it.
|
|
550
|
+
assert(!this.selectionSet.fragments || this.selectionSet.fragments.isEmpty(), 'Removing @defer currently only work on "expanded" selections (no named fragments)');
|
|
551
|
+
const updated = this.selectionSet.withoutDefer(labelsToRemove);
|
|
552
|
+
return updated == this.selectionSet
|
|
553
|
+
? this
|
|
554
|
+
: new Operation(this.rootKind, updated, this.variableDefinitions, this.name);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Returns this operation but modified to "normalize" all the @defer applications.
|
|
559
|
+
*
|
|
560
|
+
* "Normalized" in this context means that all the `@defer` application in the
|
|
561
|
+
* resulting operation will:
|
|
562
|
+
* - have a (unique) label. Which imply that this method generates label for
|
|
563
|
+
* any `@defer` not having a label.
|
|
564
|
+
* - have a non-trivial `if` condition, if any. By non-trivial, we mean that
|
|
565
|
+
* the condition will be a variable and not an hard-coded `true` or `false`.
|
|
566
|
+
* To do this, this method will remove the condition of any `@defer` that
|
|
567
|
+
* has `if: true`, and will completely remove any `@defer` application that
|
|
568
|
+
* has `if: false`.
|
|
569
|
+
*/
|
|
570
|
+
withNormalizedDefer(): {
|
|
571
|
+
operation: Operation,
|
|
572
|
+
hasDefers: boolean,
|
|
573
|
+
assignedDeferLabels: Set<string>,
|
|
574
|
+
deferConditions: SetMultiMap<string, string>,
|
|
575
|
+
} {
|
|
576
|
+
// Similar comment than in `withoutDefer`
|
|
577
|
+
assert(!this.selectionSet.fragments || this.selectionSet.fragments.isEmpty(), 'Assigning @defer lables currently only work on "expanded" selections (no named fragments)');
|
|
578
|
+
|
|
579
|
+
const normalizer = new DeferNormalizer();
|
|
580
|
+
const { hasDefers, hasNonLabelledOrConditionalDefers } = normalizer.init(this.selectionSet);
|
|
581
|
+
let updatedOperation: Operation = this;
|
|
582
|
+
if (hasNonLabelledOrConditionalDefers) {
|
|
583
|
+
const updated = this.selectionSet.withNormalizedDefer(normalizer);
|
|
584
|
+
updatedOperation = new Operation(this.rootKind, updated, this.variableDefinitions, this.name);
|
|
585
|
+
}
|
|
586
|
+
return {
|
|
587
|
+
operation: updatedOperation,
|
|
588
|
+
hasDefers,
|
|
589
|
+
assignedDeferLabels: normalizer.assignedLabels,
|
|
590
|
+
deferConditions: normalizer.deferConditions,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
404
594
|
toString(expandFragments: boolean = false, prettyPrint: boolean = true): string {
|
|
405
595
|
return this.selectionSet.toOperationString(this.rootKind, this.variableDefinitions, this.name, expandFragments, prettyPrint);
|
|
406
596
|
}
|
|
@@ -434,6 +624,10 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
|
|
|
434
624
|
super(schema);
|
|
435
625
|
}
|
|
436
626
|
|
|
627
|
+
withUpdatedSelectionSet(newSelectionSet: SelectionSet): NamedFragmentDefinition {
|
|
628
|
+
return new NamedFragmentDefinition(this.schema(), this.name, this.typeCondition, newSelectionSet);
|
|
629
|
+
}
|
|
630
|
+
|
|
437
631
|
variables(): Variables {
|
|
438
632
|
return mergeVariables(this.variablesInAppliedDirectives(), this.selectionSet.usedVariables());
|
|
439
633
|
}
|
|
@@ -460,6 +654,19 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
|
|
|
460
654
|
};
|
|
461
655
|
}
|
|
462
656
|
|
|
657
|
+
/**
|
|
658
|
+
* Whether this fragment may apply at the provided type, that is if its type condition matches the type
|
|
659
|
+
* or is a supertype of it.
|
|
660
|
+
*
|
|
661
|
+
* @param type - the type at which we're looking at applying the fragment
|
|
662
|
+
*/
|
|
663
|
+
canApplyAtType(type: CompositeType): boolean {
|
|
664
|
+
return (
|
|
665
|
+
sameType(this.typeCondition, type)
|
|
666
|
+
|| (isAbstractType(this.typeCondition) && !isUnionType(type) && isDirectSubtype(this.typeCondition, type))
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
|
|
463
670
|
toString(indent?: string): string {
|
|
464
671
|
return (indent ?? '') + `fragment ${this.name} on ${this.typeCondition}${this.appliedDirectivesToString()} ${this.selectionSet.toString(false, true, indent)}`;
|
|
465
672
|
}
|
|
@@ -497,8 +704,8 @@ export class NamedFragments {
|
|
|
497
704
|
}
|
|
498
705
|
}
|
|
499
706
|
|
|
500
|
-
|
|
501
|
-
return this.fragments.values().filter(f => f.
|
|
707
|
+
maybeApplyingAtType(type: CompositeType): NamedFragmentDefinition[] {
|
|
708
|
+
return this.fragments.values().filter(f => f.canApplyAtType(type));
|
|
502
709
|
}
|
|
503
710
|
|
|
504
711
|
without(names: string[]): NamedFragments {
|
|
@@ -525,6 +732,10 @@ export class NamedFragments {
|
|
|
525
732
|
return this.fragments.get(name);
|
|
526
733
|
}
|
|
527
734
|
|
|
735
|
+
has(name: string): boolean {
|
|
736
|
+
return this.fragments.has(name);
|
|
737
|
+
}
|
|
738
|
+
|
|
528
739
|
definitions(): readonly NamedFragmentDefinition[] {
|
|
529
740
|
return this.fragments.values();
|
|
530
741
|
}
|
|
@@ -593,6 +804,67 @@ abstract class Freezable<T> {
|
|
|
593
804
|
abstract clone(): T;
|
|
594
805
|
}
|
|
595
806
|
|
|
807
|
+
/**
|
|
808
|
+
* Utility class used to handle "normalizing" the @defer in an operation.
|
|
809
|
+
*
|
|
810
|
+
* See `Operation.withNormalizedDefer` for details on what we mean by normalizing in
|
|
811
|
+
* this context.
|
|
812
|
+
*/
|
|
813
|
+
class DeferNormalizer {
|
|
814
|
+
private index = 0;
|
|
815
|
+
readonly assignedLabels = new Set<string>();
|
|
816
|
+
readonly deferConditions = new SetMultiMap<string, string>();
|
|
817
|
+
private readonly usedLabels = new Set<string>();
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Initializes the "labeller" with all the labels used in the provided selections set.
|
|
821
|
+
*
|
|
822
|
+
* @return - whether `selectionSet` has any non-labeled @defer.
|
|
823
|
+
*/
|
|
824
|
+
init(selectionSet: SelectionSet): { hasDefers: boolean, hasNonLabelledOrConditionalDefers: boolean } {
|
|
825
|
+
let hasNonLabelledOrConditionalDefers = false;
|
|
826
|
+
let hasDefers = false;
|
|
827
|
+
const stack: Selection[] = selectionSet.selections().concat();
|
|
828
|
+
while (stack.length > 0) {
|
|
829
|
+
const selection = stack.pop()!;
|
|
830
|
+
if (selection.kind === 'FragmentSelection') {
|
|
831
|
+
const deferArgs = selection.element().deferDirectiveArgs();
|
|
832
|
+
if (deferArgs) {
|
|
833
|
+
hasDefers = true;
|
|
834
|
+
if (deferArgs.label) {
|
|
835
|
+
this.usedLabels.add(deferArgs.label);
|
|
836
|
+
} else {
|
|
837
|
+
hasNonLabelledOrConditionalDefers = true;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
if (selection.selectionSet) {
|
|
842
|
+
selection.selectionSet.selections().forEach((s) => stack.push(s));
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return { hasDefers, hasNonLabelledOrConditionalDefers };
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
private nextLabel(): string {
|
|
849
|
+
return `qp__${this.index++}`;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
newLabel(): string {
|
|
853
|
+
let candidate = this.nextLabel();
|
|
854
|
+
// It's unlikely that auto-generated label would conflict an existing one, but
|
|
855
|
+
// not taking any chances.
|
|
856
|
+
while (this.usedLabels.has(candidate)) {
|
|
857
|
+
candidate = this.nextLabel();
|
|
858
|
+
}
|
|
859
|
+
this.assignedLabels.add(candidate);
|
|
860
|
+
return candidate;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
registerCondition(label: string, condition: Variable): void {
|
|
864
|
+
this.deferConditions.add(condition.name, label);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
596
868
|
export class SelectionSet extends Freezable<SelectionSet> {
|
|
597
869
|
// The argument is either the responseName (for fields), or the type name (for fragments), with the empty string being used as a special
|
|
598
870
|
// case for a fragment with no type condition.
|
|
@@ -648,10 +920,6 @@ export class SelectionSet extends Freezable<SelectionSet> {
|
|
|
648
920
|
}
|
|
649
921
|
|
|
650
922
|
collectUsedFragmentNames(collector: Map<string, number>) {
|
|
651
|
-
if (!this.fragments) {
|
|
652
|
-
return;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
923
|
for (const byResponseName of this._selections.values()) {
|
|
656
924
|
for (const selection of byResponseName) {
|
|
657
925
|
selection.collectUsedFragmentNames(collector);
|
|
@@ -680,10 +948,6 @@ export class SelectionSet extends Freezable<SelectionSet> {
|
|
|
680
948
|
}
|
|
681
949
|
|
|
682
950
|
expandFragments(names?: string[], updateSelectionSetFragments: boolean = true): SelectionSet {
|
|
683
|
-
if (!this.fragments) {
|
|
684
|
-
return this;
|
|
685
|
-
}
|
|
686
|
-
|
|
687
951
|
if (names && names.length === 0) {
|
|
688
952
|
return this;
|
|
689
953
|
}
|
|
@@ -693,11 +957,60 @@ export class SelectionSet extends Freezable<SelectionSet> {
|
|
|
693
957
|
: this.fragments;
|
|
694
958
|
const withExpanded = new SelectionSet(this.parentType, newFragments);
|
|
695
959
|
for (const selection of this.selections()) {
|
|
696
|
-
|
|
960
|
+
const expanded = selection.expandFragments(names, updateSelectionSetFragments);
|
|
961
|
+
if (Array.isArray(expanded)) {
|
|
962
|
+
withExpanded.addAll(expanded);
|
|
963
|
+
} else {
|
|
964
|
+
withExpanded.add(expanded as Selection);
|
|
965
|
+
}
|
|
697
966
|
}
|
|
698
967
|
return withExpanded;
|
|
699
968
|
}
|
|
700
969
|
|
|
970
|
+
/**
|
|
971
|
+
* Returns the result of mapping the provided `mapper` to all the selection of this selection set.
|
|
972
|
+
*
|
|
973
|
+
* This method assumes that the `mapper` may often return it's argument directly, meaning that only
|
|
974
|
+
* a small subset of selection actually need any modifications, and will avoid re-creating new
|
|
975
|
+
* objects when that is the case. This does mean that the resulting selection set may be `this`
|
|
976
|
+
* directly, or may alias some of the sub-selection in `this`.
|
|
977
|
+
*/
|
|
978
|
+
private lazyMap(mapper: (selection: Selection) => Selection | SelectionSet | undefined): SelectionSet {
|
|
979
|
+
let updatedSelections: Selection[] | undefined = undefined;
|
|
980
|
+
const selections = this.selections();
|
|
981
|
+
for (let i = 0; i < selections.length; i++) {
|
|
982
|
+
const selection = selections[i];
|
|
983
|
+
const updated = mapper(selection);
|
|
984
|
+
if (updated !== selection && !updatedSelections) {
|
|
985
|
+
updatedSelections = [];
|
|
986
|
+
for (let j = 0; j < i; j++) {
|
|
987
|
+
updatedSelections.push(selections[j]);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
if (!!updated && updatedSelections) {
|
|
991
|
+
if (updated instanceof SelectionSet) {
|
|
992
|
+
updated.selections().forEach((s) => updatedSelections!.push(s));
|
|
993
|
+
} else {
|
|
994
|
+
updatedSelections.push(updated);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
if (!updatedSelections) {
|
|
999
|
+
return this;
|
|
1000
|
+
}
|
|
1001
|
+
return new SelectionSet(this.parentType, this.fragments).addAll(updatedSelections)
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
withoutDefer(labelsToRemove?: Set<string>): SelectionSet {
|
|
1005
|
+
assert(!this.fragments, 'Not yet supported');
|
|
1006
|
+
return this.lazyMap((selection) => selection.withoutDefer(labelsToRemove));
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
withNormalizedDefer(normalizer: DeferNormalizer): SelectionSet {
|
|
1010
|
+
assert(!this.fragments, 'Not yet supported');
|
|
1011
|
+
return this.lazyMap((selection) => selection.withNormalizedDefer(normalizer));
|
|
1012
|
+
}
|
|
1013
|
+
|
|
701
1014
|
/**
|
|
702
1015
|
* Returns the selection select from filtering out any selection that does not match the provided predicate.
|
|
703
1016
|
*
|
|
@@ -705,14 +1018,12 @@ export class SelectionSet extends Freezable<SelectionSet> {
|
|
|
705
1018
|
* call `optimize` on the result if you want to re-apply some fragments.
|
|
706
1019
|
*/
|
|
707
1020
|
filter(predicate: (selection: Selection) => boolean): SelectionSet {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
}
|
|
715
|
-
return filtered;
|
|
1021
|
+
return this.lazyMap((selection) => selection.filter(predicate));
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
withoutEmptyBranches(): SelectionSet | undefined {
|
|
1025
|
+
const updated = this.filter((selection) => selection.selectionSet?.isEmpty() !== true);
|
|
1026
|
+
return updated.isEmpty() ? undefined : updated;
|
|
716
1027
|
}
|
|
717
1028
|
|
|
718
1029
|
protected freezeInternals(): void {
|
|
@@ -1080,8 +1391,44 @@ export class FieldSelection extends Freezable<FieldSelection> {
|
|
|
1080
1391
|
}
|
|
1081
1392
|
}
|
|
1082
1393
|
|
|
1083
|
-
optimize(fragments: NamedFragments):
|
|
1394
|
+
optimize(fragments: NamedFragments): Selection {
|
|
1084
1395
|
const optimizedSelection = this.selectionSet ? this.selectionSet.optimize(fragments) : undefined;
|
|
1396
|
+
const fieldBaseType = baseType(this.field.definition.type!);
|
|
1397
|
+
if (isCompositeType(fieldBaseType) && optimizedSelection) {
|
|
1398
|
+
for (const candidate of fragments.maybeApplyingAtType(fieldBaseType)) {
|
|
1399
|
+
// TODO: Checking `equals` here is very simple, but somewhat restrictive in theory. That is, if a query
|
|
1400
|
+
// is:
|
|
1401
|
+
// {
|
|
1402
|
+
// t {
|
|
1403
|
+
// a
|
|
1404
|
+
// b
|
|
1405
|
+
// c
|
|
1406
|
+
// }
|
|
1407
|
+
// }
|
|
1408
|
+
// and we have:
|
|
1409
|
+
// fragment X on T {
|
|
1410
|
+
// t {
|
|
1411
|
+
// a
|
|
1412
|
+
// b
|
|
1413
|
+
// }
|
|
1414
|
+
// }
|
|
1415
|
+
// then the current code will not use the fragment because `c` is not in the fragment, but in relatity,
|
|
1416
|
+
// we could use it and make the result be:
|
|
1417
|
+
// {
|
|
1418
|
+
// ...X
|
|
1419
|
+
// t {
|
|
1420
|
+
// c
|
|
1421
|
+
// }
|
|
1422
|
+
// }
|
|
1423
|
+
// To do that, we can change that `equals` to `contains`, but then we should also "extract" the remainder
|
|
1424
|
+
// of `optimizedSelection` that isn't covered by the fragment, and that is the part slighly more involved.
|
|
1425
|
+
if (optimizedSelection.equals(candidate.selectionSet)) {
|
|
1426
|
+
const fragmentSelection = new FragmentSpreadSelection(fieldBaseType, fragments, candidate.name);
|
|
1427
|
+
return new FieldSelection(this.field, selectionSetOf(fieldBaseType, fragmentSelection));
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1085
1432
|
return this.selectionSet === optimizedSelection
|
|
1086
1433
|
? this
|
|
1087
1434
|
: new FieldSelection(this.field, optimizedSelection);
|
|
@@ -1094,7 +1441,11 @@ export class FieldSelection extends Freezable<FieldSelection> {
|
|
|
1094
1441
|
if (!this.selectionSet) {
|
|
1095
1442
|
return this;
|
|
1096
1443
|
}
|
|
1097
|
-
|
|
1444
|
+
|
|
1445
|
+
const updatedSelectionSet = this.selectionSet.filter(predicate);
|
|
1446
|
+
return this.selectionSet === updatedSelectionSet
|
|
1447
|
+
? this
|
|
1448
|
+
: new FieldSelection(this.field, updatedSelectionSet);
|
|
1098
1449
|
}
|
|
1099
1450
|
|
|
1100
1451
|
protected freezeInternals(): void {
|
|
@@ -1203,6 +1554,20 @@ export class FieldSelection extends Freezable<FieldSelection> {
|
|
|
1203
1554
|
return this.selectionSet?.fragments;
|
|
1204
1555
|
}
|
|
1205
1556
|
|
|
1557
|
+
withoutDefer(labelsToRemove?: Set<string>): FieldSelection {
|
|
1558
|
+
const updatedSubSelections = this.selectionSet?.withoutDefer(labelsToRemove);
|
|
1559
|
+
return updatedSubSelections === this.selectionSet
|
|
1560
|
+
? this
|
|
1561
|
+
: new FieldSelection(this.field, updatedSubSelections);
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
withNormalizedDefer(normalizer: DeferNormalizer): FieldSelection {
|
|
1565
|
+
const updatedSubSelections = this.selectionSet?.withNormalizedDefer(normalizer);
|
|
1566
|
+
return updatedSubSelections === this.selectionSet
|
|
1567
|
+
? this
|
|
1568
|
+
: new FieldSelection(this.field, updatedSubSelections);
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1206
1571
|
clone(): FieldSelection {
|
|
1207
1572
|
if (!this.selectionSet) {
|
|
1208
1573
|
return this;
|
|
@@ -1230,33 +1595,69 @@ export abstract class FragmentSelection extends Freezable<FragmentSelection> {
|
|
|
1230
1595
|
|
|
1231
1596
|
abstract optimize(fragments: NamedFragments): FragmentSelection;
|
|
1232
1597
|
|
|
1233
|
-
abstract expandFragments(names?: string[]):
|
|
1598
|
+
abstract expandFragments(names?: string[]): Selection | readonly Selection[];
|
|
1234
1599
|
|
|
1235
1600
|
abstract toSelectionNode(): SelectionNode;
|
|
1236
1601
|
|
|
1237
1602
|
abstract validate(): void;
|
|
1238
1603
|
|
|
1604
|
+
abstract withoutDefer(labelsToRemove?: Set<string>): FragmentSelection | SelectionSet;
|
|
1605
|
+
|
|
1606
|
+
abstract withNormalizedDefer(normalizer: DeferNormalizer): FragmentSelection | SelectionSet;
|
|
1607
|
+
|
|
1239
1608
|
protected us(): FragmentSelection {
|
|
1240
1609
|
return this;
|
|
1241
1610
|
}
|
|
1242
1611
|
|
|
1612
|
+
protected validateDeferAndStream() {
|
|
1613
|
+
if (this.element().hasDefer() || this.element().hasStream()) {
|
|
1614
|
+
const schemaDef = this.element().schema().schemaDefinition;
|
|
1615
|
+
const parentType = this.element().parentType;
|
|
1616
|
+
validate(
|
|
1617
|
+
schemaDef.rootType('mutation') !== parentType && schemaDef.rootType('subscription') !== parentType,
|
|
1618
|
+
() => `The @defer and @stream directives cannot be used on ${schemaDef.roots().filter((t) => t.type === parentType).pop()?.rootKind} root type "${parentType}"`,
|
|
1619
|
+
);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1243
1623
|
usedVariables(): Variables {
|
|
1244
1624
|
return mergeVariables(this.element().variables(), this.selectionSet.usedVariables());
|
|
1245
1625
|
}
|
|
1246
1626
|
|
|
1247
1627
|
updateForAddingTo(selectionSet: SelectionSet): FragmentSelection {
|
|
1248
1628
|
const updatedFragment = this.element().updateForAddingTo(selectionSet);
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1629
|
+
if (this.element() === updatedFragment) {
|
|
1630
|
+
return this.cloneIfFrozen();
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
// Like for fields, we create a new selection that not only uses the updated fragment, but also ensures
|
|
1634
|
+
// the underlying selection set uses the updated type as parent type.
|
|
1635
|
+
const updatedCastedType = updatedFragment.castedType();
|
|
1636
|
+
let updatedSelectionSet : SelectionSet | undefined;
|
|
1637
|
+
if (this.selectionSet.parentType !== updatedCastedType) {
|
|
1638
|
+
updatedSelectionSet = new SelectionSet(updatedCastedType);
|
|
1639
|
+
// Note that re-adding every selection ensures that anything frozen will be cloned as needed, on top of handling any knock-down
|
|
1640
|
+
// effect of the type change.
|
|
1641
|
+
for (const selection of this.selectionSet.selections()) {
|
|
1642
|
+
updatedSelectionSet.add(selection);
|
|
1643
|
+
}
|
|
1644
|
+
} else {
|
|
1645
|
+
updatedSelectionSet = this.selectionSet?.cloneIfFrozen();
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
return new InlineFragmentSelection(updatedFragment, updatedSelectionSet);
|
|
1252
1649
|
}
|
|
1253
1650
|
|
|
1254
|
-
filter(predicate: (selection: Selection) => boolean):
|
|
1651
|
+
filter(predicate: (selection: Selection) => boolean): FragmentSelection | undefined {
|
|
1255
1652
|
if (!predicate(this)) {
|
|
1256
1653
|
return undefined;
|
|
1257
1654
|
}
|
|
1258
1655
|
// Note that we essentially expand all fragments as part of this.
|
|
1259
|
-
|
|
1656
|
+
const selectionSet = this.selectionSet;
|
|
1657
|
+
const updatedSelectionSet = selectionSet.filter(predicate);
|
|
1658
|
+
return updatedSelectionSet === selectionSet
|
|
1659
|
+
? this
|
|
1660
|
+
: new InlineFragmentSelection(this.element(), updatedSelectionSet);
|
|
1260
1661
|
}
|
|
1261
1662
|
|
|
1262
1663
|
protected freezeInternals() {
|
|
@@ -1302,6 +1703,7 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
1302
1703
|
}
|
|
1303
1704
|
|
|
1304
1705
|
validate() {
|
|
1706
|
+
this.validateDeferAndStream();
|
|
1305
1707
|
// Note that validation is kind of redundant since `this.selectionSet.validate()` will check that it isn't empty. But doing it
|
|
1306
1708
|
// allow to provide much better error messages.
|
|
1307
1709
|
validate(
|
|
@@ -1311,7 +1713,6 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
1311
1713
|
this.selectionSet.validate();
|
|
1312
1714
|
}
|
|
1313
1715
|
|
|
1314
|
-
|
|
1315
1716
|
get selectionSet(): SelectionSet {
|
|
1316
1717
|
return this._selectionSet;
|
|
1317
1718
|
}
|
|
@@ -1343,13 +1744,21 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
1343
1744
|
}
|
|
1344
1745
|
|
|
1345
1746
|
optimize(fragments: NamedFragments): FragmentSelection {
|
|
1346
|
-
|
|
1747
|
+
let optimizedSelection = this.selectionSet.optimize(fragments);
|
|
1347
1748
|
const typeCondition = this.element().typeCondition;
|
|
1348
1749
|
if (typeCondition) {
|
|
1349
|
-
for (const candidate of fragments.
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1750
|
+
for (const candidate of fragments.maybeApplyingAtType(typeCondition)) {
|
|
1751
|
+
// See comment in `FieldSelection.optimize` about the `equals`: this fully apply here too.
|
|
1752
|
+
if (optimizedSelection.equals(candidate.selectionSet)) {
|
|
1753
|
+
const spread = new FragmentSpreadSelection(this.element().parentType, fragments, candidate.name);
|
|
1754
|
+
// We use the fragment when the fragments condition is either the same, or a supertype of our current condition.
|
|
1755
|
+
// If it's the same type, then we don't really want to preserve the current condition, it is included in the
|
|
1756
|
+
// spread and we can return it directive. But if the fragment condition is a superset, then we should preserve
|
|
1757
|
+
// our current condition since it restricts the selection more than the fragment actual does.
|
|
1758
|
+
if (sameType(typeCondition, candidate.typeCondition)) {
|
|
1759
|
+
return spread;
|
|
1760
|
+
}
|
|
1761
|
+
optimizedSelection = selectionSetOf(spread.element().parentType, spread);
|
|
1353
1762
|
}
|
|
1354
1763
|
}
|
|
1355
1764
|
}
|
|
@@ -1369,6 +1778,32 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
1369
1778
|
this.selectionSet.collectUsedFragmentNames(collector);
|
|
1370
1779
|
}
|
|
1371
1780
|
|
|
1781
|
+
withoutDefer(labelsToRemove?: Set<string>): FragmentSelection | SelectionSet {
|
|
1782
|
+
const updatedSubSelections = this.selectionSet.withoutDefer(labelsToRemove);
|
|
1783
|
+
const deferArgs = this.fragmentElement.deferDirectiveArgs();
|
|
1784
|
+
const hasDeferToRemove = deferArgs && (!labelsToRemove || (deferArgs.label && labelsToRemove.has(deferArgs.label)));
|
|
1785
|
+
if (updatedSubSelections === this.selectionSet && !hasDeferToRemove) {
|
|
1786
|
+
return this;
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
const newFragment = hasDeferToRemove ? this.fragmentElement.withoutDefer() : this.fragmentElement;
|
|
1790
|
+
if (!newFragment) {
|
|
1791
|
+
return updatedSubSelections;
|
|
1792
|
+
}
|
|
1793
|
+
return new InlineFragmentSelection(newFragment, updatedSubSelections);
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
withNormalizedDefer(normalizer: DeferNormalizer): InlineFragmentSelection | SelectionSet {
|
|
1797
|
+
const newFragment = this.fragmentElement.withNormalizedDefer(normalizer);
|
|
1798
|
+
const updatedSubSelections = this.selectionSet.withNormalizedDefer(normalizer);
|
|
1799
|
+
if (!newFragment) {
|
|
1800
|
+
return updatedSubSelections;
|
|
1801
|
+
}
|
|
1802
|
+
return newFragment === this.fragmentElement && updatedSubSelections === this.selectionSet
|
|
1803
|
+
? this
|
|
1804
|
+
: new InlineFragmentSelection(newFragment, updatedSubSelections);
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1372
1807
|
toString(expandFragments: boolean = true, indent?: string): string {
|
|
1373
1808
|
return (indent ?? '') + this.fragmentElement + ' ' + this.selectionSet.toString(expandFragments, true, indent);
|
|
1374
1809
|
}
|
|
@@ -1412,7 +1847,9 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
1412
1847
|
}
|
|
1413
1848
|
|
|
1414
1849
|
validate(): void {
|
|
1415
|
-
|
|
1850
|
+
this.validateDeferAndStream();
|
|
1851
|
+
|
|
1852
|
+
// We don't do anything else because fragment definition are validated when created.
|
|
1416
1853
|
}
|
|
1417
1854
|
|
|
1418
1855
|
toSelectionNode(): FragmentSpreadNode {
|
|
@@ -1440,11 +1877,15 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
1440
1877
|
return this;
|
|
1441
1878
|
}
|
|
1442
1879
|
|
|
1443
|
-
expandFragments(names?: string[], updateSelectionSetFragments: boolean = true): FragmentSelection {
|
|
1880
|
+
expandFragments(names?: string[], updateSelectionSetFragments: boolean = true): FragmentSelection | readonly Selection[] {
|
|
1444
1881
|
if (names && !names.includes(this.namedFragment.name)) {
|
|
1445
1882
|
return this;
|
|
1446
1883
|
}
|
|
1447
|
-
|
|
1884
|
+
|
|
1885
|
+
const expandedSubSelections = this.selectionSet.expandFragments(names, updateSelectionSetFragments);
|
|
1886
|
+
return sameType(this._element.parentType, this.namedFragment.typeCondition)
|
|
1887
|
+
? expandedSubSelections.selections()
|
|
1888
|
+
: new InlineFragmentSelection(this._element, expandedSubSelections);
|
|
1448
1889
|
}
|
|
1449
1890
|
|
|
1450
1891
|
collectUsedFragmentNames(collector: Map<string, number>): void {
|
|
@@ -1453,6 +1894,14 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
1453
1894
|
collector.set(this.namedFragment.name, usageCount === undefined ? 1 : usageCount + 1);
|
|
1454
1895
|
}
|
|
1455
1896
|
|
|
1897
|
+
withoutDefer(_labelsToRemove?: Set<string>): FragmentSelection {
|
|
1898
|
+
assert(false, 'Unsupported, see `Operation.withoutDefer`');
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
withNormalizedDefer(_normalizezr: DeferNormalizer): FragmentSelection {
|
|
1902
|
+
assert(false, 'Unsupported, see `Operation.withAllDeferLabelled`');
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1456
1905
|
private spreadDirectives(): Directive<FragmentElement>[] {
|
|
1457
1906
|
return this._element.appliedDirectives.slice(this.namedFragment.appliedDirectives.length);
|
|
1458
1907
|
}
|