@apollo/federation-internals 2.0.2-alpha.0 → 2.0.2-alpha.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.
@@ -2,7 +2,7 @@ import {
2
2
  Schema,
3
3
  } from '../../dist/definitions';
4
4
  import { buildSchema } from '../../dist/buildSchema';
5
- import { parseOperation } from '../../dist/operations';
5
+ import { Field, FieldSelection, parseOperation, SelectionSet } from '../../dist/operations';
6
6
  import './matchers';
7
7
 
8
8
  function parseSchema(schema: string): Schema {
@@ -141,3 +141,121 @@ test('fragments optimization of selection sets', () => {
141
141
  }
142
142
  `);
143
143
  });
144
+
145
+ describe('selection set freezing', () => {
146
+ const schema = parseSchema(`
147
+ type Query {
148
+ t: T
149
+ }
150
+
151
+ type T {
152
+ a: Int
153
+ b: Int
154
+ }
155
+ `);
156
+
157
+ const tField = schema.schemaDefinition.rootType('query')!.field('t')!;
158
+
159
+ it('throws if one tries to modify a frozen selection set', () => {
160
+ // Note that we use parseOperation to help us build selection/selection sets because it's more readable/convenient
161
+ // thant to build the object "programmatically".
162
+ const s1 = parseOperation(schema, `{ t { a } }`).selectionSet;
163
+ const s2 = parseOperation(schema, `{ t { b } }`).selectionSet;
164
+
165
+ const s = new SelectionSet(schema.schemaDefinition.rootType('query')!);
166
+
167
+ // Control: check we can add to the selection set while not yet frozen
168
+ expect(s.isFrozen()).toBeFalsy();
169
+ expect(() => s.mergeIn(s1)).not.toThrow();
170
+
171
+ s.freeze();
172
+ expect(s.isFrozen()).toBeTruthy();
173
+ expect(() => s.mergeIn(s2)).toThrowError('Cannot add to frozen selection: { t { a } }');
174
+ });
175
+
176
+ it('it does not clone non-frozen selections when adding to another one', () => {
177
+ // This test is a bit debatable because what it tests is not so much a behaviour
178
+ // we *need* absolutely to preserve, but rather test how things happens to
179
+ // behave currently and illustrate the current contrast between frozen and
180
+ // non-frozen selection set.
181
+ // That is, this test show what happens if the test
182
+ // "it automaticaly clones frozen selections when adding to another one"
183
+ // is done without freezing.
184
+
185
+ const s1 = parseOperation(schema, `{ t { a } }`).selectionSet;
186
+ const s2 = parseOperation(schema, `{ t { b } }`).selectionSet;
187
+ const s = new SelectionSet(schema.schemaDefinition.rootType('query')!);
188
+
189
+ s.mergeIn(s1);
190
+ s.mergeIn(s2);
191
+
192
+ expect(s.toString()).toBe('{ t { a b } }');
193
+
194
+ // This next assertion is where differs from the case where `s1` is frozen. Namely,
195
+ // when we do `s.mergeIn(s1)`, then `s` directly references `s1` without cloning
196
+ // and thus the next modification (`s.mergeIn(s2)`) ends up modifying both `s` and `s1`.
197
+ // Note that we don't mean by this test that the fact that `s.mergeIn(s1)` does
198
+ // not clone `s1` is a behaviour one should *rely* on, but it currently done for
199
+ // efficiencies sake: query planning does a lot of selection set building through
200
+ // `SelectionSet::mergeIn` and `SelectionSet::add` and we often pass to those method
201
+ // newly constructed selections as input, so cloning them would wast CPU and early
202
+ // query planning benchmarking showed that this could add up on the more expansive
203
+ // plan computations. This is why freezing exists: it allows us to save cloning
204
+ // in general, but to protect those selection set we know should be immutable
205
+ // so they do get cloned in such situation.
206
+ expect(s1.toString()).toBe('{ t { a b } }');
207
+ expect(s2.toString()).toBe('{ t { b } }');
208
+ });
209
+
210
+ it('it automaticaly clones frozen field selections when merging to another one', () => {
211
+ const s1 = parseOperation(schema, `{ t { a } }`).selectionSet.freeze();
212
+ const s2 = parseOperation(schema, `{ t { b } }`).selectionSet.freeze();
213
+ const s = new SelectionSet(schema.schemaDefinition.rootType('query')!);
214
+
215
+ s.mergeIn(s1);
216
+ s.mergeIn(s2);
217
+
218
+ // We check S is what we expect...
219
+ expect(s.toString()).toBe('{ t { a b } }');
220
+
221
+ // ... but more importantly for this test, that s1/s2 were not modified.
222
+ expect(s1.toString()).toBe('{ t { a } }');
223
+ expect(s2.toString()).toBe('{ t { b } }');
224
+ });
225
+
226
+ it('it automaticaly clones frozen fragment selections when merging to another one', () => {
227
+ // Note: needlessly complex queries, but we're just ensuring the cloning story works when fragments are involved
228
+ const s1 = parseOperation(schema, `{ ... on Query { t { ... on T { a } } } }`).selectionSet.freeze();
229
+ const s2 = parseOperation(schema, `{ ... on Query { t { ... on T { b } } } }`).selectionSet.freeze();
230
+ const s = new SelectionSet(schema.schemaDefinition.rootType('query')!);
231
+
232
+ s.mergeIn(s1);
233
+ s.mergeIn(s2);
234
+
235
+ expect(s.toString()).toBe('{ ... on Query { t { ... on T { a b } } } }');
236
+
237
+ expect(s1.toString()).toBe('{ ... on Query { t { ... on T { a } } } }');
238
+ expect(s2.toString()).toBe('{ ... on Query { t { ... on T { b } } } }');
239
+ });
240
+
241
+ it('it automaticaly clones frozen field selections when adding to another one', () => {
242
+ const s1 = parseOperation(schema, `{ t { a } }`).selectionSet.freeze();
243
+ const s2 = parseOperation(schema, `{ t { b } }`).selectionSet.freeze();
244
+ const s = new SelectionSet(schema.schemaDefinition.rootType('query')!);
245
+ const tSelection = new FieldSelection(new Field(tField));
246
+ s.add(tSelection);
247
+
248
+ // Note that this test both checks the auto-cloning for the `add` method, but
249
+ // also shows that freezing dose apply deeply (since we freeze the whole `s1`/`s2`
250
+ // but only add some sub-selection of it).
251
+ tSelection.selectionSet!.add(s1.selections()[0].selectionSet!.selections()[0]);
252
+ tSelection.selectionSet!.add(s2.selections()[0].selectionSet!.selections()[0]);
253
+
254
+ // We check S is what we expect...
255
+ expect(s.toString()).toBe('{ t { a b } }');
256
+
257
+ // ... but more importantly for this test, that s1/s2 were not modified.
258
+ expect(s1.toString()).toBe('{ t { a } }');
259
+ expect(s2.toString()).toBe('{ t { b } }');
260
+ });
261
+ });
@@ -2423,4 +2423,51 @@ describe("removeInaccessibleElements", () => {
2423
2423
  ]
2424
2424
  `);
2425
2425
  });
2426
+
2427
+ it(`fails to remove @inaccessible non-core feature elements referenced by core feature elements`, () => {
2428
+ const schema = buildSchema(`
2429
+ directive @core(feature: String!, as: String, for: core__Purpose) repeatable on SCHEMA
2430
+
2431
+ enum core__Purpose {
2432
+ EXECUTION
2433
+ SECURITY
2434
+ }
2435
+
2436
+ directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
2437
+
2438
+ schema
2439
+ @core(feature: "https://specs.apollo.dev/core/v0.2")
2440
+ @core(feature: "https://specs.apollo.dev/inaccessible/v0.2")
2441
+ @core(feature: "http://localhost/foo/v1.0")
2442
+ {
2443
+ query: Query
2444
+ }
2445
+
2446
+ type Query {
2447
+ someField: String
2448
+ }
2449
+
2450
+ # Inaccessible input object field
2451
+ input InputObject {
2452
+ someField: String
2453
+ privateField: String @inaccessible
2454
+ }
2455
+
2456
+ # Inaccessible input object field can't be referenced by default value of
2457
+ # directive argument of a core feature element
2458
+ directive @foo__referencer(
2459
+ someArg: InputObject = { privateField: "" }
2460
+ ) on FIELD
2461
+ `);
2462
+
2463
+ const errorMessages = expectErrors(1, () => {
2464
+ removeInaccessibleElements(schema);
2465
+ });
2466
+
2467
+ expect(errorMessages).toMatchInlineSnapshot(`
2468
+ Array [
2469
+ "Input field \\"InputObject.privateField\\" is @inaccessible but is used in the default value of \\"@foo__referencer(someArg:)\\", which is in the API schema.",
2470
+ ]
2471
+ `);
2472
+ });
2426
2473
  });
@@ -79,8 +79,13 @@ export function buildSchemaFromAST(
79
79
  // to the schema element. But that detection necessitates that the corresponding directive definition has been fully
80
80
  // populated (and at this point, we don't really know the name of the `@core` directive since it can be renamed, so
81
81
  // we just handle all directives).
82
+ // Note that one subtlety is that we skip, for now, directive _applications_ within those directive definitions (we can
83
+ // have such applications on the arguments). The reason is again core schema related: we haven't yet properly detected
84
+ // if the schema if a core-schema yet, and for federation subgraphs, we haven't yet "imported" federation definitions.
85
+ // So if one of those directive application was relying on that "importing", it would fail at this point. Which is why
86
+ // directive application is delayed to later in that method.
82
87
  for (const directiveDefinitionNode of directiveDefinitions) {
83
- buildDirectiveDefinitionInner(directiveDefinitionNode, schema.directive(directiveDefinitionNode.name.value)!, errors);
88
+ buildDirectiveDefinitionInnerWithoutDirectiveApplications(directiveDefinitionNode, schema.directive(directiveDefinitionNode.name.value)!, errors);
84
89
  }
85
90
  for (const schemaDefinition of schemaDefinitions) {
86
91
  buildSchemaDefinitionInner(schemaDefinition, schema.schemaDefinition, errors);
@@ -89,8 +94,17 @@ export function buildSchemaFromAST(
89
94
  buildSchemaDefinitionInner(schemaExtension, schema.schemaDefinition, errors, schema.schemaDefinition.newExtension());
90
95
  }
91
96
 
97
+ // The following is a no-op for "standard" schema, but for federation subgraphs, this is where we handle the auto-addition
98
+ // of imported federation directive definitions. That is why we have avoid looking at directive applications within
99
+ // directive definition earlier: if one of those application was of an imported federation directive, the definition
100
+ // wouldn't be presence before this point and we'd have triggered an error. After this, we can handle any directive
101
+ // application safely.
92
102
  errors.push(...schema.blueprint.onDirectiveDefinitionAndSchemaParsed(schema));
93
103
 
104
+ for (const directiveDefinitionNode of directiveDefinitions) {
105
+ buildDirectiveApplicationsInDirectiveDefinition(directiveDefinitionNode, schema.directive(directiveDefinitionNode.name.value)!, errors);
106
+ }
107
+
94
108
  for (const definitionNode of documentNode.definitions) {
95
109
  switch (definitionNode.kind) {
96
110
  case 'OperationDefinition':
@@ -360,7 +374,7 @@ function buildFieldDefinitionInner(
360
374
  const type = buildTypeReferenceFromAST(fieldNode.type, field.schema());
361
375
  field.type = validateOutputType(type, field.coordinate, fieldNode, errors);
362
376
  for (const inputValueDef of fieldNode.arguments ?? []) {
363
- buildArgumentDefinitionInner(inputValueDef, field.addArgument(inputValueDef.name.value), errors);
377
+ buildArgumentDefinitionInner(inputValueDef, field.addArgument(inputValueDef.name.value), errors, true);
364
378
  }
365
379
  buildAppliedDirectives(fieldNode, field, errors);
366
380
  field.description = fieldNode.description?.value;
@@ -408,11 +422,14 @@ function buildArgumentDefinitionInner(
408
422
  inputNode: InputValueDefinitionNode,
409
423
  arg: ArgumentDefinition<any>,
410
424
  errors: GraphQLError[],
425
+ includeDirectiveApplication: boolean,
411
426
  ) {
412
427
  const type = buildTypeReferenceFromAST(inputNode.type, arg.schema());
413
428
  arg.type = validateInputType(type, arg.coordinate, inputNode, errors);
414
429
  arg.defaultValue = buildValue(inputNode.defaultValue);
415
- buildAppliedDirectives(inputNode, arg, errors);
430
+ if (includeDirectiveApplication) {
431
+ buildAppliedDirectives(inputNode, arg, errors);
432
+ }
416
433
  arg.description = inputNode.description?.value;
417
434
  arg.sourceAST = inputNode;
418
435
  }
@@ -430,13 +447,13 @@ function buildInputFieldDefinitionInner(
430
447
  field.sourceAST = fieldNode;
431
448
  }
432
449
 
433
- function buildDirectiveDefinitionInner(
450
+ function buildDirectiveDefinitionInnerWithoutDirectiveApplications(
434
451
  directiveNode: DirectiveDefinitionNode,
435
452
  directive: DirectiveDefinition,
436
453
  errors: GraphQLError[],
437
454
  ) {
438
455
  for (const inputValueDef of directiveNode.arguments ?? []) {
439
- buildArgumentDefinitionInner(inputValueDef, directive.addArgument(inputValueDef.name.value), errors);
456
+ buildArgumentDefinitionInner(inputValueDef, directive.addArgument(inputValueDef.name.value), errors, false);
440
457
  }
441
458
  directive.repeatable = directiveNode.repeatable;
442
459
  const locations = directiveNode.locations.map(({ value }) => value as DirectiveLocation);
@@ -444,3 +461,13 @@ function buildDirectiveDefinitionInner(
444
461
  directive.description = directiveNode.description?.value;
445
462
  directive.sourceAST = directiveNode;
446
463
  }
464
+
465
+ function buildDirectiveApplicationsInDirectiveDefinition(
466
+ directiveNode: DirectiveDefinitionNode,
467
+ directive: DirectiveDefinition,
468
+ errors: GraphQLError[],
469
+ ) {
470
+ for (const inputValueDef of directiveNode.arguments ?? []) {
471
+ buildAppliedDirectives(inputValueDef, directive.argument(inputValueDef.name.value)!, errors);
472
+ }
473
+ }
@@ -3215,9 +3215,16 @@ function copy(source: Schema, dest: Schema) {
3215
3215
  for (const type of typesToCopy(source, dest)) {
3216
3216
  dest.addType(newNamedType(type.kind, type.name));
3217
3217
  }
3218
+ // Directives can use other directives in their arguments. So, like types, we first shallow copy
3219
+ // directives so future references to any of them can be dereferenced. We'll copy the actual
3220
+ // definition later after all directives are defined.
3218
3221
  for (const directive of directivesToCopy(source, dest)) {
3219
- copyDirectiveDefinitionInner(directive, dest.addDirectiveDefinition(directive.name));
3222
+ dest.addDirectiveDefinition(directive.name);
3220
3223
  }
3224
+ for (const directive of directivesToCopy(source, dest)) {
3225
+ copyDirectiveDefinitionInner(directive, dest.directive(directive.name)!);
3226
+ }
3227
+
3221
3228
  copySchemaDefinitionInner(source.schemaDefinition, dest.schemaDefinition);
3222
3229
  for (const type of typesToCopy(source, dest)) {
3223
3230
  copyNamedTypeInner(type, dest.type(type.name)!);