@apollo/federation-internals 2.4.0-alpha.1 → 2.4.1

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.
@@ -212,7 +212,7 @@ test('reject @interfaceObject usage if not all subgraphs are fed2', () => {
212
212
 
213
213
  const s1 = `
214
214
  extend schema
215
- @link(url: "https://specs.apollo.dev/federation/v2.4", import: [ "@key", "@interfaceObject"])
215
+ @link(url: "https://specs.apollo.dev/federation/v2.3", import: [ "@key", "@interfaceObject"])
216
216
 
217
217
  type Query {
218
218
  a: A
@@ -32,7 +32,16 @@ import {
32
32
  removeAllCoreFeatures,
33
33
  } from "./coreSpec";
34
34
  import { assert, mapValues, MapWithCachedArrays, removeArrayElement } from "./utils";
35
- import { withDefaultValues, valueEquals, valueToString, valueToAST, variablesInValue, valueFromAST, valueNodeToConstValueNode, argumentsEquals } from "./values";
35
+ import {
36
+ withDefaultValues,
37
+ valueEquals,
38
+ valueToString,
39
+ valueToAST,
40
+ valueFromAST,
41
+ valueNodeToConstValueNode,
42
+ argumentsEquals,
43
+ collectVariablesInValue
44
+ } from "./values";
36
45
  import { removeInaccessibleElements } from "./inaccessibleSpec";
37
46
  import { printDirectiveDefinition, printSchema } from './print';
38
47
  import { sameType } from './types';
@@ -343,54 +352,39 @@ export interface Named {
343
352
  export type ExtendableElement = SchemaDefinition | NamedType;
344
353
 
345
354
  export class DirectiveTargetElement<T extends DirectiveTargetElement<T>> {
346
- private _appliedDirectives: Directive<T>[] | undefined;
355
+ readonly appliedDirectives: Directive<T>[];
347
356
 
348
- constructor(private readonly _schema: Schema) {}
357
+ constructor(
358
+ private readonly _schema: Schema,
359
+ directives: readonly Directive<any>[] = [],
360
+ ) {
361
+ this.appliedDirectives = directives.map((d) => this.attachDirective(d));
362
+ }
349
363
 
350
364
  schema(): Schema {
351
365
  return this._schema;
352
366
  }
353
367
 
368
+ private attachDirective(directive: Directive<any>): Directive<T> {
369
+ // if the directive is not attached, we can assume we're fine just attaching it to use. Otherwise, we're "copying" it.
370
+ const toAdd = directive.isAttached()
371
+ ? new Directive(directive.name, directive.arguments())
372
+ : directive;
373
+
374
+ Element.prototype['setParent'].call(toAdd, this);
375
+ return toAdd;
376
+ }
377
+
354
378
  appliedDirectivesOf<TApplicationArgs extends {[key: string]: any} = {[key: string]: any}>(nameOrDefinition: string | DirectiveDefinition<TApplicationArgs>): Directive<T, TApplicationArgs>[] {
355
379
  const directiveName = typeof nameOrDefinition === 'string' ? nameOrDefinition : nameOrDefinition.name;
356
380
  return this.appliedDirectives.filter(d => d.name == directiveName) as Directive<T, TApplicationArgs>[];
357
381
  }
358
382
 
359
- get appliedDirectives(): readonly Directive<T>[] {
360
- return this._appliedDirectives ?? [];
361
- }
362
-
363
383
  hasAppliedDirective(nameOrDefinition: string | DirectiveDefinition): boolean {
364
384
  const directiveName = typeof nameOrDefinition === 'string' ? nameOrDefinition : nameOrDefinition.name;
365
385
  return this.appliedDirectives.some(d => d.name == directiveName);
366
386
  }
367
387
 
368
- applyDirective<TApplicationArgs extends {[key: string]: any} = {[key: string]: any}>(
369
- defOrDirective: Directive<T, TApplicationArgs> | DirectiveDefinition<TApplicationArgs>,
370
- args?: TApplicationArgs
371
- ): Directive<T, TApplicationArgs> {
372
- let toAdd: Directive<T, TApplicationArgs>;
373
- if (defOrDirective instanceof Directive) {
374
- if (defOrDirective.schema() != this.schema()) {
375
- throw new Error(`Cannot add directive ${defOrDirective} to ${this} as it is attached to another schema`);
376
- }
377
- toAdd = defOrDirective;
378
- if (args) {
379
- toAdd.setArguments(args);
380
- }
381
- } else {
382
- toAdd = new Directive<T, TApplicationArgs>(defOrDirective.name, args ?? Object.create(null));
383
- }
384
- Element.prototype['setParent'].call(toAdd, this);
385
- // TODO: we should typecheck arguments or our TApplicationArgs business is just a lie.
386
- if (this._appliedDirectives) {
387
- this._appliedDirectives.push(toAdd);
388
- } else {
389
- this._appliedDirectives = [ toAdd ];
390
- }
391
- return toAdd;
392
- }
393
-
394
388
  appliedDirectivesToDirectiveNodes() : ConstDirectiveNode[] | undefined {
395
389
  if (this.appliedDirectives.length == 0) {
396
390
  return undefined;
@@ -414,8 +408,10 @@ export class DirectiveTargetElement<T extends DirectiveTargetElement<T>> {
414
408
  : ' ' + this.appliedDirectives.join(' ');
415
409
  }
416
410
 
417
- variablesInAppliedDirectives(): Variables {
418
- return this.appliedDirectives.reduce((acc: Variables, d) => mergeVariables(acc, variablesInArguments(d.arguments())), []);
411
+ collectVariablesInAppliedDirectives(collector: VariableCollector) {
412
+ for (const applied of this.appliedDirectives) {
413
+ collector.collectInArguments(applied.arguments());
414
+ }
419
415
  }
420
416
  }
421
417
 
@@ -3069,7 +3065,7 @@ export class Directive<
3069
3065
  // applied to a field that is part of an extension, the field will have its extension set, but not the underlying directive.
3070
3066
  private _extension?: Extension<any>;
3071
3067
 
3072
- constructor(readonly name: string, private _args: TArgs) {
3068
+ constructor(readonly name: string, private _args: TArgs = Object.create(null)) {
3073
3069
  super();
3074
3070
  }
3075
3071
 
@@ -3314,38 +3310,38 @@ export class Variable {
3314
3310
 
3315
3311
  export type Variables = readonly Variable[];
3316
3312
 
3317
- export function mergeVariables(v1s: Variables, v2s: Variables): Variables {
3318
- if (v1s.length == 0) {
3319
- return v2s;
3313
+ export class VariableCollector {
3314
+ private readonly _variables = new Map<string, Variable>();
3315
+
3316
+ add(variable: Variable) {
3317
+ this._variables.set(variable.name, variable);
3320
3318
  }
3321
- if (v2s.length == 0) {
3322
- return v1s;
3319
+
3320
+ addAll(variables: Variables) {
3321
+ for (const variable of variables) {
3322
+ this.add(variable);
3323
+ }
3323
3324
  }
3324
- const res: Variable[] = v1s.concat();
3325
- for (const v of v2s) {
3326
- if (!containsVariable(v1s, v)) {
3327
- res.push(v);
3325
+
3326
+ collectInArguments(args: {[key: string]: any}) {
3327
+ for (const value of Object.values(args)) {
3328
+ collectVariablesInValue(value, this);
3328
3329
  }
3329
3330
  }
3330
- return res;
3331
- }
3332
3331
 
3333
- export function containsVariable(variables: Variables, toCheck: Variable): boolean {
3334
- return variables.some(v => v.name == toCheck.name);
3332
+ variables() {
3333
+ return mapValues(this._variables);
3334
+ }
3335
+
3336
+ toString(): string {
3337
+ return this.variables().toString();
3338
+ }
3335
3339
  }
3336
3340
 
3337
3341
  export function isVariable(v: any): v is Variable {
3338
3342
  return v instanceof Variable;
3339
3343
  }
3340
3344
 
3341
- export function variablesInArguments(args: {[key: string]: any}): Variables {
3342
- let variables: Variables = [];
3343
- for (const value of Object.values(args)) {
3344
- variables = mergeVariables(variables, variablesInValue(value));
3345
- }
3346
- return variables;
3347
- }
3348
-
3349
3345
  export class VariableDefinition extends DirectiveTargetElement<VariableDefinition> {
3350
3346
  constructor(
3351
3347
  schema: Schema,
package/src/federation.ts CHANGED
@@ -48,7 +48,7 @@ import {
48
48
  } from "graphql";
49
49
  import { KnownTypeNamesInFederationRule } from "./validation/KnownTypeNamesInFederationRule";
50
50
  import { buildSchema, buildSchemaFromAST } from "./buildSchema";
51
- import { parseSelectionSet, selectionOfElement, SelectionSet } from './operations';
51
+ import { parseSelectionSet, SelectionSet } from './operations';
52
52
  import { TAG_VERSIONS } from "./tagSpec";
53
53
  import {
54
54
  errorCodeDef,
@@ -129,7 +129,7 @@ function validateFieldSetSelections({
129
129
  allowFieldsWithArguments: boolean,
130
130
  }): void {
131
131
  for (const selection of selectionSet.selections()) {
132
- const appliedDirectives = selection.element().appliedDirectives;
132
+ const appliedDirectives = selection.element.appliedDirectives;
133
133
  if (appliedDirectives.length > 0) {
134
134
  onError(ERROR_CATEGORIES.DIRECTIVE_IN_FIELDS_ARG.get(directiveName).err(
135
135
  `cannot have directive applications in the @${directiveName}(fields:) argument but found ${appliedDirectives.join(', ')}.`,
@@ -137,7 +137,7 @@ function validateFieldSetSelections({
137
137
  }
138
138
 
139
139
  if (selection.kind === 'FieldSelection') {
140
- const field = selection.element().definition;
140
+ const field = selection.element.definition;
141
141
  const isExternal = metadata.isFieldExternal(field);
142
142
  if (!allowFieldsWithArguments && field.hasArguments()) {
143
143
  onError(ERROR_CATEGORIES.FIELDS_HAS_ARGS.get(directiveName).err(
@@ -1817,12 +1817,17 @@ class ExternalTester {
1817
1817
  private collectProvidedFields() {
1818
1818
  for (const provides of this.metadata().providesDirective().applications()) {
1819
1819
  const parent = provides.parent as FieldDefinition<CompositeType>;
1820
- collectTargetFields({
1821
- parentType: baseType(parent.type!) as CompositeType,
1822
- directive: provides as Directive<any, {fields: any}>,
1823
- includeInterfaceFieldsImplementations: true,
1824
- validate: false,
1825
- }).forEach((f) => this.providedFields.add(f.coordinate));
1820
+ const parentType = baseType(parent.type!);
1821
+ // If `parentType` is not a composite, that means an invalid @provides, but we ignore such errors
1822
+ // for now (also why we pass 'validate: false'). Proper errors will be thrown later during validation.
1823
+ if (isCompositeType(parentType)) {
1824
+ collectTargetFields({
1825
+ parentType,
1826
+ directive: provides as Directive<any, {fields: any}>,
1827
+ includeInterfaceFieldsImplementations: true,
1828
+ validate: false,
1829
+ }).forEach((f) => this.providedFields.add(f.coordinate));
1830
+ }
1826
1831
  }
1827
1832
  }
1828
1833
 
@@ -1854,7 +1859,7 @@ class ExternalTester {
1854
1859
 
1855
1860
  selectsAnyExternalField(selectionSet: SelectionSet): boolean {
1856
1861
  for (const selection of selectionSet.selections()) {
1857
- if (selection.kind === 'FieldSelection' && this.isExternal(selection.element().definition)) {
1862
+ if (selection.kind === 'FieldSelection' && this.isExternal(selection.element.definition)) {
1858
1863
  return true;
1859
1864
  }
1860
1865
  if (selection.selectionSet) {
@@ -1966,7 +1971,7 @@ function selectsNonExternalLeafField(selection: SelectionSet): boolean {
1966
1971
  return selection.selections().some(s => {
1967
1972
  if (s.kind === 'FieldSelection') {
1968
1973
  // If it's external, we're good and don't need to recurse.
1969
- if (isExternalOrHasExternalImplementations(s.field.definition)) {
1974
+ if (isExternalOrHasExternalImplementations(s.element.definition)) {
1970
1975
  return false;
1971
1976
  }
1972
1977
  // Otherwise, we select a non-external if it's a leaf, or the sub-selection does.
@@ -1978,25 +1983,24 @@ function selectsNonExternalLeafField(selection: SelectionSet): boolean {
1978
1983
  }
1979
1984
 
1980
1985
  function withoutNonExternalLeafFields(selectionSet: SelectionSet): SelectionSet {
1981
- const newSelectionSet = new SelectionSet(selectionSet.parentType);
1982
- for (const selection of selectionSet.selections()) {
1986
+ return selectionSet.lazyMap((selection) => {
1983
1987
  if (selection.kind === 'FieldSelection') {
1984
- if (isExternalOrHasExternalImplementations(selection.field.definition)) {
1988
+ if (isExternalOrHasExternalImplementations(selection.element.definition)) {
1985
1989
  // That field is external, so we can add the selection back entirely.
1986
- newSelectionSet.add(selection);
1987
- continue;
1990
+ return selection;
1988
1991
  }
1989
1992
  }
1990
- // Note that for fragments will always be true (and we just recurse), while
1991
- // for fields, we'll only get here if the field is not external, and so
1992
- // we want to add the selection only if it's not a leaf and even then, only
1993
- // the part where we've recursed.
1994
1993
  if (selection.selectionSet) {
1994
+ // Note that for fragments this will always be true (and we just recurse), while
1995
+ // for fields, we'll only get here if the field is not external, and so
1996
+ // we want to add the selection only if it's not a leaf and even then, only
1997
+ // the part where we've recursed.
1995
1998
  const updated = withoutNonExternalLeafFields(selection.selectionSet);
1996
1999
  if (!updated.isEmpty()) {
1997
- newSelectionSet.add(selectionOfElement(selection.element(), updated));
2000
+ return selection.withUpdatedSelectionSet(updated);
1998
2001
  }
1999
2002
  }
2000
- }
2001
- return newSelectionSet;
2003
+ // We skip that selection.
2004
+ return undefined;
2005
+ });
2002
2006
  }