@apollo/federation-internals 2.1.0-alpha.2 → 2.1.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 +5 -4
- package/dist/buildSchema.d.ts.map +1 -1
- package/dist/buildSchema.js +14 -4
- package/dist/buildSchema.js.map +1 -1
- package/dist/coreSpec.d.ts +1 -4
- package/dist/coreSpec.d.ts.map +1 -1
- package/dist/coreSpec.js +1 -5
- package/dist/coreSpec.js.map +1 -1
- package/dist/definitions.d.ts +66 -34
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +309 -165
- package/dist/definitions.js.map +1 -1
- package/dist/error.d.ts +5 -0
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +47 -1
- package/dist/error.js.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.js +135 -5
- package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
- package/dist/federation.d.ts +3 -2
- package/dist/federation.d.ts.map +1 -1
- package/dist/federation.js +12 -7
- package/dist/federation.js.map +1 -1
- package/dist/inaccessibleSpec.js +1 -2
- package/dist/inaccessibleSpec.js.map +1 -1
- package/dist/operations.d.ts +44 -20
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +287 -27
- 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.d.ts.map +1 -1
- package/dist/schemaUpgrader.js +6 -6
- package/dist/schemaUpgrader.js.map +1 -1
- package/dist/supergraphs.d.ts +1 -3
- package/dist/supergraphs.d.ts.map +1 -1
- package/dist/supergraphs.js +9 -22
- package/dist/supergraphs.js.map +1 -1
- package/dist/utils.d.ts +10 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +40 -1
- package/dist/utils.js.map +1 -1
- package/package.json +3 -4
- package/src/__tests__/coreSpec.test.ts +1 -1
- package/src/__tests__/definitions.test.ts +27 -0
- package/src/__tests__/extractSubgraphsFromSupergraph.test.ts +9 -5
- package/src/__tests__/operations.test.ts +36 -0
- package/src/__tests__/removeInaccessibleElements.test.ts +1 -3
- package/src/__tests__/schemaUpgrader.test.ts +0 -1
- package/src/__tests__/subgraphValidation.test.ts +1 -2
- package/src/buildSchema.ts +20 -7
- package/src/coreSpec.ts +2 -7
- package/src/definitions.ts +355 -155
- package/src/error.ts +62 -0
- package/src/extractSubgraphsFromSupergraph.ts +198 -7
- package/src/federation.ts +11 -4
- package/src/inaccessibleSpec.ts +2 -5
- package/src/operations.ts +428 -40
- package/src/print.ts +3 -3
- package/src/schemaUpgrader.ts +7 -6
- package/src/supergraphs.ts +16 -25
- package/src/utils.ts +49 -0
- package/tsconfig.test.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
package/src/operations.ts
CHANGED
|
@@ -43,10 +43,12 @@ import {
|
|
|
43
43
|
isConditionalDirective,
|
|
44
44
|
isDirectiveApplicationsSubset,
|
|
45
45
|
isAbstractType,
|
|
46
|
+
DeferDirectiveArgs,
|
|
47
|
+
Variable,
|
|
46
48
|
} from "./definitions";
|
|
47
49
|
import { ERRORS } from "./error";
|
|
48
50
|
import { isDirectSubtype, sameType } from "./types";
|
|
49
|
-
import { assert, mapEntries, MapWithCachedArrays, MultiMap } from "./utils";
|
|
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;
|
|
@@ -408,6 +536,61 @@ export class Operation {
|
|
|
408
536
|
);
|
|
409
537
|
}
|
|
410
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
|
+
|
|
411
594
|
toString(expandFragments: boolean = false, prettyPrint: boolean = true): string {
|
|
412
595
|
return this.selectionSet.toOperationString(this.rootKind, this.variableDefinitions, this.name, expandFragments, prettyPrint);
|
|
413
596
|
}
|
|
@@ -621,6 +804,67 @@ abstract class Freezable<T> {
|
|
|
621
804
|
abstract clone(): T;
|
|
622
805
|
}
|
|
623
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
|
+
|
|
624
868
|
export class SelectionSet extends Freezable<SelectionSet> {
|
|
625
869
|
// The argument is either the responseName (for fields), or the type name (for fragments), with the empty string being used as a special
|
|
626
870
|
// case for a fragment with no type condition.
|
|
@@ -707,7 +951,6 @@ export class SelectionSet extends Freezable<SelectionSet> {
|
|
|
707
951
|
if (names && names.length === 0) {
|
|
708
952
|
return this;
|
|
709
953
|
}
|
|
710
|
-
|
|
711
954
|
const newFragments = updateSelectionSetFragments
|
|
712
955
|
? (names ? this.fragments?.without(names) : undefined)
|
|
713
956
|
: this.fragments;
|
|
@@ -723,6 +966,50 @@ export class SelectionSet extends Freezable<SelectionSet> {
|
|
|
723
966
|
return withExpanded;
|
|
724
967
|
}
|
|
725
968
|
|
|
969
|
+
/**
|
|
970
|
+
* Returns the result of mapping the provided `mapper` to all the selection of this selection set.
|
|
971
|
+
*
|
|
972
|
+
* This method assumes that the `mapper` may often return it's argument directly, meaning that only
|
|
973
|
+
* a small subset of selection actually need any modifications, and will avoid re-creating new
|
|
974
|
+
* objects when that is the case. This does mean that the resulting selection set may be `this`
|
|
975
|
+
* directly, or may alias some of the sub-selection in `this`.
|
|
976
|
+
*/
|
|
977
|
+
private lazyMap(mapper: (selection: Selection) => Selection | SelectionSet | undefined): SelectionSet {
|
|
978
|
+
let updatedSelections: Selection[] | undefined = undefined;
|
|
979
|
+
const selections = this.selections();
|
|
980
|
+
for (let i = 0; i < selections.length; i++) {
|
|
981
|
+
const selection = selections[i];
|
|
982
|
+
const updated = mapper(selection);
|
|
983
|
+
if (updated !== selection && !updatedSelections) {
|
|
984
|
+
updatedSelections = [];
|
|
985
|
+
for (let j = 0; j < i; j++) {
|
|
986
|
+
updatedSelections.push(selections[j]);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
if (!!updated && updatedSelections) {
|
|
990
|
+
if (updated instanceof SelectionSet) {
|
|
991
|
+
updated.selections().forEach((s) => updatedSelections!.push(s));
|
|
992
|
+
} else {
|
|
993
|
+
updatedSelections.push(updated);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
if (!updatedSelections) {
|
|
998
|
+
return this;
|
|
999
|
+
}
|
|
1000
|
+
return new SelectionSet(this.parentType, this.fragments).addAll(updatedSelections)
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
withoutDefer(labelsToRemove?: Set<string>): SelectionSet {
|
|
1004
|
+
assert(!this.fragments, 'Not yet supported');
|
|
1005
|
+
return this.lazyMap((selection) => selection.withoutDefer(labelsToRemove));
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
withNormalizedDefer(normalizer: DeferNormalizer): SelectionSet {
|
|
1009
|
+
assert(!this.fragments, 'Not yet supported');
|
|
1010
|
+
return this.lazyMap((selection) => selection.withNormalizedDefer(normalizer));
|
|
1011
|
+
}
|
|
1012
|
+
|
|
726
1013
|
/**
|
|
727
1014
|
* Returns the selection select from filtering out any selection that does not match the provided predicate.
|
|
728
1015
|
*
|
|
@@ -730,14 +1017,12 @@ export class SelectionSet extends Freezable<SelectionSet> {
|
|
|
730
1017
|
* call `optimize` on the result if you want to re-apply some fragments.
|
|
731
1018
|
*/
|
|
732
1019
|
filter(predicate: (selection: Selection) => boolean): SelectionSet {
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
}
|
|
740
|
-
return filtered;
|
|
1020
|
+
return this.lazyMap((selection) => selection.filter(predicate));
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
withoutEmptyBranches(): SelectionSet | undefined {
|
|
1024
|
+
const updated = this.filter((selection) => selection.selectionSet?.isEmpty() !== true);
|
|
1025
|
+
return updated.isEmpty() ? undefined : updated;
|
|
741
1026
|
}
|
|
742
1027
|
|
|
743
1028
|
protected freezeInternals(): void {
|
|
@@ -766,7 +1051,7 @@ export class SelectionSet extends Freezable<SelectionSet> {
|
|
|
766
1051
|
* This is very similar to `mergeIn` except that it takes a direct array of selection, and the direct aliasing
|
|
767
1052
|
* remarks from `mergeInd` applies here too.
|
|
768
1053
|
*/
|
|
769
|
-
addAll(selections: Selection[]): SelectionSet {
|
|
1054
|
+
addAll(selections: readonly Selection[]): SelectionSet {
|
|
770
1055
|
selections.forEach(s => this.add(s));
|
|
771
1056
|
return this;
|
|
772
1057
|
}
|
|
@@ -1155,7 +1440,11 @@ export class FieldSelection extends Freezable<FieldSelection> {
|
|
|
1155
1440
|
if (!this.selectionSet) {
|
|
1156
1441
|
return this;
|
|
1157
1442
|
}
|
|
1158
|
-
|
|
1443
|
+
|
|
1444
|
+
const updatedSelectionSet = this.selectionSet.filter(predicate);
|
|
1445
|
+
return this.selectionSet === updatedSelectionSet
|
|
1446
|
+
? this
|
|
1447
|
+
: new FieldSelection(this.field, updatedSelectionSet);
|
|
1159
1448
|
}
|
|
1160
1449
|
|
|
1161
1450
|
protected freezeInternals(): void {
|
|
@@ -1264,6 +1553,20 @@ export class FieldSelection extends Freezable<FieldSelection> {
|
|
|
1264
1553
|
return this.selectionSet?.fragments;
|
|
1265
1554
|
}
|
|
1266
1555
|
|
|
1556
|
+
withoutDefer(labelsToRemove?: Set<string>): FieldSelection {
|
|
1557
|
+
const updatedSubSelections = this.selectionSet?.withoutDefer(labelsToRemove);
|
|
1558
|
+
return updatedSubSelections === this.selectionSet
|
|
1559
|
+
? this
|
|
1560
|
+
: new FieldSelection(this.field, updatedSubSelections);
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
withNormalizedDefer(normalizer: DeferNormalizer): FieldSelection {
|
|
1564
|
+
const updatedSubSelections = this.selectionSet?.withNormalizedDefer(normalizer);
|
|
1565
|
+
return updatedSubSelections === this.selectionSet
|
|
1566
|
+
? this
|
|
1567
|
+
: new FieldSelection(this.field, updatedSubSelections);
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1267
1570
|
clone(): FieldSelection {
|
|
1268
1571
|
if (!this.selectionSet) {
|
|
1269
1572
|
return this;
|
|
@@ -1297,27 +1600,41 @@ export abstract class FragmentSelection extends Freezable<FragmentSelection> {
|
|
|
1297
1600
|
|
|
1298
1601
|
abstract validate(): void;
|
|
1299
1602
|
|
|
1603
|
+
abstract withoutDefer(labelsToRemove?: Set<string>): FragmentSelection | SelectionSet;
|
|
1604
|
+
|
|
1605
|
+
abstract withNormalizedDefer(normalizer: DeferNormalizer): FragmentSelection | SelectionSet;
|
|
1606
|
+
|
|
1607
|
+
abstract updateForAddingTo(selectionSet: SelectionSet): FragmentSelection;
|
|
1608
|
+
|
|
1300
1609
|
protected us(): FragmentSelection {
|
|
1301
1610
|
return this;
|
|
1302
1611
|
}
|
|
1303
1612
|
|
|
1304
|
-
|
|
1305
|
-
|
|
1613
|
+
protected validateDeferAndStream() {
|
|
1614
|
+
if (this.element().hasDefer() || this.element().hasStream()) {
|
|
1615
|
+
const schemaDef = this.element().schema().schemaDefinition;
|
|
1616
|
+
const parentType = this.element().parentType;
|
|
1617
|
+
validate(
|
|
1618
|
+
schemaDef.rootType('mutation') !== parentType && schemaDef.rootType('subscription') !== parentType,
|
|
1619
|
+
() => `The @defer and @stream directives cannot be used on ${schemaDef.roots().filter((t) => t.type === parentType).pop()?.rootKind} root type "${parentType}"`,
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1306
1622
|
}
|
|
1307
1623
|
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
return this.element() === updatedFragment
|
|
1311
|
-
? this.cloneIfFrozen()
|
|
1312
|
-
: new InlineFragmentSelection(updatedFragment, this.selectionSet.cloneIfFrozen());
|
|
1624
|
+
usedVariables(): Variables {
|
|
1625
|
+
return mergeVariables(this.element().variables(), this.selectionSet.usedVariables());
|
|
1313
1626
|
}
|
|
1314
1627
|
|
|
1315
|
-
filter(predicate: (selection: Selection) => boolean):
|
|
1628
|
+
filter(predicate: (selection: Selection) => boolean): FragmentSelection | undefined {
|
|
1316
1629
|
if (!predicate(this)) {
|
|
1317
1630
|
return undefined;
|
|
1318
1631
|
}
|
|
1319
1632
|
// Note that we essentially expand all fragments as part of this.
|
|
1320
|
-
|
|
1633
|
+
const selectionSet = this.selectionSet;
|
|
1634
|
+
const updatedSelectionSet = selectionSet.filter(predicate);
|
|
1635
|
+
return updatedSelectionSet === selectionSet
|
|
1636
|
+
? this
|
|
1637
|
+
: new InlineFragmentSelection(this.element(), updatedSelectionSet);
|
|
1321
1638
|
}
|
|
1322
1639
|
|
|
1323
1640
|
protected freezeInternals() {
|
|
@@ -1363,6 +1680,7 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
1363
1680
|
}
|
|
1364
1681
|
|
|
1365
1682
|
validate() {
|
|
1683
|
+
this.validateDeferAndStream();
|
|
1366
1684
|
// Note that validation is kind of redundant since `this.selectionSet.validate()` will check that it isn't empty. But doing it
|
|
1367
1685
|
// allow to provide much better error messages.
|
|
1368
1686
|
validate(
|
|
@@ -1372,6 +1690,31 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
1372
1690
|
this.selectionSet.validate();
|
|
1373
1691
|
}
|
|
1374
1692
|
|
|
1693
|
+
updateForAddingTo(selectionSet: SelectionSet): FragmentSelection {
|
|
1694
|
+
const updatedFragment = this.element().updateForAddingTo(selectionSet);
|
|
1695
|
+
if (this.element() === updatedFragment) {
|
|
1696
|
+
return this.cloneIfFrozen();
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
// Like for fields, we create a new selection that not only uses the updated fragment, but also ensures
|
|
1700
|
+
// the underlying selection set uses the updated type as parent type.
|
|
1701
|
+
const updatedCastedType = updatedFragment.castedType();
|
|
1702
|
+
let updatedSelectionSet : SelectionSet | undefined;
|
|
1703
|
+
if (this.selectionSet.parentType !== updatedCastedType) {
|
|
1704
|
+
updatedSelectionSet = new SelectionSet(updatedCastedType);
|
|
1705
|
+
// Note that re-adding every selection ensures that anything frozen will be cloned as needed, on top of handling any knock-down
|
|
1706
|
+
// effect of the type change.
|
|
1707
|
+
for (const selection of this.selectionSet.selections()) {
|
|
1708
|
+
updatedSelectionSet.add(selection);
|
|
1709
|
+
}
|
|
1710
|
+
} else {
|
|
1711
|
+
updatedSelectionSet = this.selectionSet?.cloneIfFrozen();
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
return new InlineFragmentSelection(updatedFragment, updatedSelectionSet);
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
|
|
1375
1718
|
get selectionSet(): SelectionSet {
|
|
1376
1719
|
return this._selectionSet;
|
|
1377
1720
|
}
|
|
@@ -1437,6 +1780,31 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
1437
1780
|
this.selectionSet.collectUsedFragmentNames(collector);
|
|
1438
1781
|
}
|
|
1439
1782
|
|
|
1783
|
+
withoutDefer(labelsToRemove?: Set<string>): FragmentSelection | SelectionSet {
|
|
1784
|
+
const updatedSubSelections = this.selectionSet.withoutDefer(labelsToRemove);
|
|
1785
|
+
const deferArgs = this.fragmentElement.deferDirectiveArgs();
|
|
1786
|
+
const hasDeferToRemove = deferArgs && (!labelsToRemove || (deferArgs.label && labelsToRemove.has(deferArgs.label)));
|
|
1787
|
+
if (updatedSubSelections === this.selectionSet && !hasDeferToRemove) {
|
|
1788
|
+
return this;
|
|
1789
|
+
}
|
|
1790
|
+
const newFragment = hasDeferToRemove ? this.fragmentElement.withoutDefer() : this.fragmentElement;
|
|
1791
|
+
if (!newFragment) {
|
|
1792
|
+
return updatedSubSelections;
|
|
1793
|
+
}
|
|
1794
|
+
return new InlineFragmentSelection(newFragment, updatedSubSelections);
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
withNormalizedDefer(normalizer: DeferNormalizer): InlineFragmentSelection | SelectionSet {
|
|
1798
|
+
const newFragment = this.fragmentElement.withNormalizedDefer(normalizer);
|
|
1799
|
+
const updatedSubSelections = this.selectionSet.withNormalizedDefer(normalizer);
|
|
1800
|
+
if (!newFragment) {
|
|
1801
|
+
return updatedSubSelections;
|
|
1802
|
+
}
|
|
1803
|
+
return newFragment === this.fragmentElement && updatedSubSelections === this.selectionSet
|
|
1804
|
+
? this
|
|
1805
|
+
: new InlineFragmentSelection(newFragment, updatedSubSelections);
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1440
1808
|
toString(expandFragments: boolean = true, indent?: string): string {
|
|
1441
1809
|
return (indent ?? '') + this.fragmentElement + ' ' + this.selectionSet.toString(expandFragments, true, indent);
|
|
1442
1810
|
}
|
|
@@ -1480,7 +1848,9 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
1480
1848
|
}
|
|
1481
1849
|
|
|
1482
1850
|
validate(): void {
|
|
1483
|
-
|
|
1851
|
+
this.validateDeferAndStream();
|
|
1852
|
+
|
|
1853
|
+
// We don't do anything else because fragment definition are validated when created.
|
|
1484
1854
|
}
|
|
1485
1855
|
|
|
1486
1856
|
toSelectionNode(): FragmentSpreadNode {
|
|
@@ -1508,13 +1878,23 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
1508
1878
|
return this;
|
|
1509
1879
|
}
|
|
1510
1880
|
|
|
1881
|
+
updateForAddingTo(_selectionSet: SelectionSet): FragmentSelection {
|
|
1882
|
+
// This is a little bit iffy, because the fragment could link to a schema (typically the supergraph API one)
|
|
1883
|
+
// that is different from the one of `_selectionSet` (say, a subgraph fetch selection in which we're trying to
|
|
1884
|
+
// reuse a user fragment). But in practice, we expand all fragments when we do query planning and only re-add
|
|
1885
|
+
// fragments back at the very end, so this should be fine. Importantly, we don't want this method to mistakenly
|
|
1886
|
+
// expand the spread, as that would compromise the code that optimize subgraph fetches to re-use named
|
|
1887
|
+
// fragments.
|
|
1888
|
+
return this;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1511
1891
|
expandFragments(names?: string[], updateSelectionSetFragments: boolean = true): FragmentSelection | readonly Selection[] {
|
|
1512
1892
|
if (names && !names.includes(this.namedFragment.name)) {
|
|
1513
1893
|
return this;
|
|
1514
1894
|
}
|
|
1515
1895
|
|
|
1516
1896
|
const expandedSubSelections = this.selectionSet.expandFragments(names, updateSelectionSetFragments);
|
|
1517
|
-
return sameType(this._element.parentType, this.namedFragment.typeCondition)
|
|
1897
|
+
return sameType(this._element.parentType, this.namedFragment.typeCondition) && this._element.appliedDirectives.length === 0
|
|
1518
1898
|
? expandedSubSelections.selections()
|
|
1519
1899
|
: new InlineFragmentSelection(this._element, expandedSubSelections);
|
|
1520
1900
|
}
|
|
@@ -1525,6 +1905,14 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
1525
1905
|
collector.set(this.namedFragment.name, usageCount === undefined ? 1 : usageCount + 1);
|
|
1526
1906
|
}
|
|
1527
1907
|
|
|
1908
|
+
withoutDefer(_labelsToRemove?: Set<string>): FragmentSelection {
|
|
1909
|
+
assert(false, 'Unsupported, see `Operation.withoutDefer`');
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
withNormalizedDefer(_normalizezr: DeferNormalizer): FragmentSelection {
|
|
1913
|
+
assert(false, 'Unsupported, see `Operation.withAllDeferLabelled`');
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1528
1916
|
private spreadDirectives(): Directive<FragmentElement>[] {
|
|
1529
1917
|
return this._element.appliedDirectives.slice(this.namedFragment.appliedDirectives.length);
|
|
1530
1918
|
}
|
package/src/print.ts
CHANGED
|
@@ -94,7 +94,7 @@ export function printSchema(schema: Schema, options: PrintOptions = defaultPrint
|
|
|
94
94
|
return definitions.flat().join('\n\n');
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
function definitionAndExtensions<T extends ExtendableElement>(element: {extensions():
|
|
97
|
+
function definitionAndExtensions<T extends ExtendableElement>(element: {extensions(): readonly Extension<T>[]}, options: PrintOptions): (Extension<any> | null | undefined)[] {
|
|
98
98
|
return options.mergeTypesAndExtensions ? [undefined] : [null, ...element.extensions()];
|
|
99
99
|
}
|
|
100
100
|
|
|
@@ -102,7 +102,7 @@ function printSchemaDefinitionAndExtensions(schemaDefinition: SchemaDefinition,
|
|
|
102
102
|
return printDefinitionAndExtensions(schemaDefinition, options, printSchemaDefinitionOrExtension);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
function printDefinitionAndExtensions<T extends {extensions():
|
|
105
|
+
function printDefinitionAndExtensions<T extends {extensions(): readonly Extension<any>[]}>(
|
|
106
106
|
t: T,
|
|
107
107
|
options: PrintOptions,
|
|
108
108
|
printer: (t: T, options: PrintOptions, extension?: Extension<any> | null) => string | undefined
|
|
@@ -200,7 +200,7 @@ export function printTypeDefinitionAndExtensions(type: NamedType, options: Print
|
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
export function printDirectiveDefinition(directive: DirectiveDefinition, options: PrintOptions): string {
|
|
203
|
+
export function printDirectiveDefinition(directive: DirectiveDefinition, options: PrintOptions = defaultPrintOptions): string {
|
|
204
204
|
const locations = directive.locations.join(' | ');
|
|
205
205
|
return `${printDescription(directive, options, null)}directive ${directive}${printArgs(directive.arguments(), options)}${directive.repeatable ? ' repeatable' : ''} on ${locations}`;
|
|
206
206
|
}
|