@apollo/federation-internals 2.0.2-alpha.0 → 2.0.2
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 +11 -0
- package/dist/buildSchema.d.ts.map +1 -1
- package/dist/buildSchema.js +17 -6
- package/dist/buildSchema.js.map +1 -1
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +8 -2
- package/dist/definitions.js.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.js +170 -152
- package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
- package/dist/inaccessibleSpec.js +1 -1
- package/dist/inaccessibleSpec.js.map +1 -1
- package/dist/operations.d.ts +18 -3
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +55 -7
- package/dist/operations.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/definitions.test.ts +87 -0
- package/src/__tests__/extractSubgraphsFromSupergraph.test.ts +83 -0
- package/src/__tests__/operations.test.ts +119 -1
- package/src/__tests__/removeInaccessibleElements.test.ts +47 -0
- package/src/buildSchema.ts +32 -5
- package/src/definitions.ts +13 -2
- package/src/extractSubgraphsFromSupergraph.ts +229 -204
- package/src/inaccessibleSpec.ts +12 -2
- package/src/operations.ts +112 -7
- package/tsconfig.test.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -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
|
});
|
package/src/buildSchema.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|
package/src/definitions.ts
CHANGED
|
@@ -2106,7 +2106,11 @@ export class EnumType extends BaseNamedType<OutputTypeReferencer, EnumType> {
|
|
|
2106
2106
|
}
|
|
2107
2107
|
|
|
2108
2108
|
protected removeInnerElements(): void {
|
|
2109
|
-
|
|
2109
|
+
// Make a copy, since EnumValue.remove() will modify this._values.
|
|
2110
|
+
const values = Array.from(this._values);
|
|
2111
|
+
for (const value of values) {
|
|
2112
|
+
value.remove();
|
|
2113
|
+
}
|
|
2110
2114
|
}
|
|
2111
2115
|
|
|
2112
2116
|
protected hasNonExtensionInnerElements(): boolean {
|
|
@@ -3215,9 +3219,16 @@ function copy(source: Schema, dest: Schema) {
|
|
|
3215
3219
|
for (const type of typesToCopy(source, dest)) {
|
|
3216
3220
|
dest.addType(newNamedType(type.kind, type.name));
|
|
3217
3221
|
}
|
|
3222
|
+
// Directives can use other directives in their arguments. So, like types, we first shallow copy
|
|
3223
|
+
// directives so future references to any of them can be dereferenced. We'll copy the actual
|
|
3224
|
+
// definition later after all directives are defined.
|
|
3225
|
+
for (const directive of directivesToCopy(source, dest)) {
|
|
3226
|
+
dest.addDirectiveDefinition(directive.name);
|
|
3227
|
+
}
|
|
3218
3228
|
for (const directive of directivesToCopy(source, dest)) {
|
|
3219
|
-
copyDirectiveDefinitionInner(directive, dest.
|
|
3229
|
+
copyDirectiveDefinitionInner(directive, dest.directive(directive.name)!);
|
|
3220
3230
|
}
|
|
3231
|
+
|
|
3221
3232
|
copySchemaDefinitionInner(source.schemaDefinition, dest.schemaDefinition);
|
|
3222
3233
|
for (const type of typesToCopy(source, dest)) {
|
|
3223
3234
|
copyNamedTypeInner(type, dest.type(type.name)!);
|