@apollo/federation-internals 2.0.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.
- package/CHANGELOG.md +16 -0
- package/dist/buildSchema.d.ts.map +1 -1
- package/dist/buildSchema.js +17 -6
- package/dist/buildSchema.js.map +1 -1
- package/dist/coreSpec.d.ts +1 -0
- package/dist/coreSpec.d.ts.map +1 -1
- package/dist/coreSpec.js +3 -0
- package/dist/coreSpec.js.map +1 -1
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +9 -5
- package/dist/definitions.js.map +1 -1
- package/dist/error.js +7 -7
- package/dist/error.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/federation.d.ts.map +1 -1
- package/dist/federation.js +15 -0
- package/dist/federation.js.map +1 -1
- package/dist/genErrorCodeDoc.js +12 -6
- package/dist/genErrorCodeDoc.js.map +1 -1
- package/dist/inaccessibleSpec.d.ts +2 -1
- package/dist/inaccessibleSpec.d.ts.map +1 -1
- package/dist/inaccessibleSpec.js +4 -1
- package/dist/inaccessibleSpec.js.map +1 -1
- package/dist/joinSpec.d.ts +2 -1
- package/dist/joinSpec.d.ts.map +1 -1
- package/dist/joinSpec.js +3 -0
- package/dist/joinSpec.js.map +1 -1
- package/dist/operations.d.ts +38 -4
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +107 -8
- package/dist/operations.js.map +1 -1
- package/dist/schemaUpgrader.d.ts +8 -1
- package/dist/schemaUpgrader.d.ts.map +1 -1
- package/dist/schemaUpgrader.js +40 -1
- package/dist/schemaUpgrader.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/definitions.test.ts +66 -0
- package/src/__tests__/extractSubgraphsFromSupergraph.test.ts +83 -0
- package/src/__tests__/operations.test.ts +119 -1
- package/src/__tests__/removeInaccessibleElements.test.ts +84 -1
- package/src/__tests__/schemaUpgrader.test.ts +38 -0
- package/src/__tests__/subgraphValidation.test.ts +74 -0
- package/src/buildSchema.ts +32 -5
- package/src/coreSpec.ts +4 -0
- package/src/definitions.ts +15 -6
- package/src/error.ts +7 -7
- package/src/extractSubgraphsFromSupergraph.ts +229 -204
- package/src/federation.ts +50 -0
- package/src/genErrorCodeDoc.ts +13 -7
- package/src/inaccessibleSpec.ts +17 -3
- package/src/joinSpec.ts +5 -1
- package/src/operations.ts +179 -8
- package/src/schemaUpgrader.ts +45 -0
- package/tsconfig.test.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -590,6 +590,72 @@ test('default arguments for directives', () => {
|
|
|
590
590
|
expect(d3.arguments(true)).toEqual({ inputObject: { number: 3 }});
|
|
591
591
|
});
|
|
592
592
|
|
|
593
|
+
describe('clone', () => {
|
|
594
|
+
it('should allow directive application before definition', () => {
|
|
595
|
+
const schema = buildSchema(`
|
|
596
|
+
directive @foo(arg: String @wizz(arg: "foo")) on FIELD_DEFINITION
|
|
597
|
+
directive @wizz(arg: String @fuzz(arg: "wizz")) on ARGUMENT_DEFINITION
|
|
598
|
+
directive @fuzz(arg: String @buzz(arg: "fuzz")) on ARGUMENT_DEFINITION
|
|
599
|
+
directive @buzz(arg: String @baz(arg: "buzz")) on ARGUMENT_DEFINITION
|
|
600
|
+
directive @baz(arg: String @bar) on ARGUMENT_DEFINITION
|
|
601
|
+
directive @bar on ARGUMENT_DEFINITION
|
|
602
|
+
|
|
603
|
+
type Query {
|
|
604
|
+
foo: String! @foo(arg: "query")
|
|
605
|
+
}
|
|
606
|
+
`).clone();
|
|
607
|
+
|
|
608
|
+
expect(schema.elementByCoordinate("@foo")).toBeDefined();
|
|
609
|
+
expect(schema.elementByCoordinate("@wizz")).toBeDefined();
|
|
610
|
+
expect(schema.elementByCoordinate("@fuzz")).toBeDefined();
|
|
611
|
+
expect(schema.elementByCoordinate("@buzz")).toBeDefined();
|
|
612
|
+
expect(schema.elementByCoordinate("@baz")).toBeDefined();
|
|
613
|
+
expect(schema.elementByCoordinate("@bar")).toBeDefined();
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
// https://github.com/apollographql/federation/issues/1794
|
|
617
|
+
it('should allow using an imported federation diretive in another directive', () => {
|
|
618
|
+
const schema = buildSubgraph('foo', "", `
|
|
619
|
+
extend schema
|
|
620
|
+
@link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@tag"])
|
|
621
|
+
|
|
622
|
+
directive @foo(arg: String @tag(name: "tag")) on FIELD_DEFINITION
|
|
623
|
+
|
|
624
|
+
type Query {
|
|
625
|
+
hi: String! @foo
|
|
626
|
+
}
|
|
627
|
+
`).schema.clone();
|
|
628
|
+
expect(schema.elementByCoordinate("@foo")).toBeDefined();
|
|
629
|
+
expect(schema.elementByCoordinate("@tag")).toBeDefined();
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
it('should allow type use in directives', () => {
|
|
633
|
+
const schema = buildSchema(`
|
|
634
|
+
directive @foo(arg: Thing!) on FIELD_DEFINITION
|
|
635
|
+
scalar Thing
|
|
636
|
+
|
|
637
|
+
type Query {
|
|
638
|
+
foo: String! @foo(arg: "sunshine")
|
|
639
|
+
}
|
|
640
|
+
`).clone();
|
|
641
|
+
|
|
642
|
+
expect(schema.elementByCoordinate("@foo")).toBeDefined();
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
it('should allow recursive directive definitions', () => {
|
|
646
|
+
const schema = buildSchema(`
|
|
647
|
+
directive @foo(a: Int @bar) on ARGUMENT_DEFINITION
|
|
648
|
+
directive @bar(b: Int @foo) on ARGUMENT_DEFINITION
|
|
649
|
+
|
|
650
|
+
type Query {
|
|
651
|
+
getData(arg: String @foo): String!
|
|
652
|
+
}
|
|
653
|
+
`).clone();
|
|
654
|
+
expect(schema.elementByCoordinate("@foo")).toBeDefined();
|
|
655
|
+
expect(schema.elementByCoordinate("@bar")).toBeDefined();
|
|
656
|
+
});
|
|
657
|
+
});
|
|
658
|
+
|
|
593
659
|
test('correctly convert to a graphQL-js schema', () => {
|
|
594
660
|
const sdl = `
|
|
595
661
|
schema {
|
|
@@ -597,3 +597,86 @@ test('throw meaningful error for invalid federation directive fieldSet', () => {
|
|
|
597
597
|
+ '[serviceB] On field "A.a", for @requires(fields: "b { x }"): Cannot query field "b" on type "A" (if the field is defined in another subgraph, you need to add it to this subgraph with @external).'
|
|
598
598
|
);
|
|
599
599
|
})
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
test('throw meaningful error for type erased from supergraph due to extending an entity without a key', () => {
|
|
603
|
+
// Supergraph generated by fed1 composition from:
|
|
604
|
+
// ServiceA:
|
|
605
|
+
// type Query {
|
|
606
|
+
// t: T
|
|
607
|
+
// }
|
|
608
|
+
//
|
|
609
|
+
// type T @key(fields: "id") {
|
|
610
|
+
// id: ID!
|
|
611
|
+
// x: Int!
|
|
612
|
+
// }
|
|
613
|
+
//
|
|
614
|
+
// ServiceB
|
|
615
|
+
// extend type T {
|
|
616
|
+
// x: Int! @external
|
|
617
|
+
// }
|
|
618
|
+
//
|
|
619
|
+
// type Other {
|
|
620
|
+
// f: T @provides(fields: "x")
|
|
621
|
+
// }
|
|
622
|
+
//
|
|
623
|
+
// The issue of that schema is that `T` is referenced in `ServiceB`, but because it extends an entity type
|
|
624
|
+
// without a key and has noly external field, ther is no remaining traces of it's definition in `ServiceB`
|
|
625
|
+
// in the supergraph. As extract cannot make up the original definition out of thin air, it ends up erroring
|
|
626
|
+
// when extracting `Other.t` due to not knowing that type.
|
|
627
|
+
const supergraph = `
|
|
628
|
+
schema
|
|
629
|
+
@core(feature: "https://specs.apollo.dev/core/v0.2"),
|
|
630
|
+
@core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION)
|
|
631
|
+
{
|
|
632
|
+
query: Query
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA
|
|
636
|
+
|
|
637
|
+
directive @join__field(graph: join__Graph, provides: join__FieldSet, requires: join__FieldSet) on FIELD_DEFINITION
|
|
638
|
+
|
|
639
|
+
directive @join__graph(name: String!, url: String!) on ENUM_VALUE
|
|
640
|
+
|
|
641
|
+
directive @join__owner(graph: join__Graph!) on INTERFACE | OBJECT
|
|
642
|
+
|
|
643
|
+
directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on INTERFACE | OBJECT
|
|
644
|
+
|
|
645
|
+
type Other {
|
|
646
|
+
f: T @join__field(graph: SERVICEB, provides: "x")
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
type Query {
|
|
650
|
+
t: T @join__field(graph: SERVICEA)
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
type T
|
|
654
|
+
@join__owner(graph: SERVICEA)
|
|
655
|
+
@join__type(graph: SERVICEA, key: "id")
|
|
656
|
+
{
|
|
657
|
+
id: ID! @join__field(graph: SERVICEA)
|
|
658
|
+
x: Int! @join__field(graph: SERVICEA)
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
enum core__Purpose {
|
|
662
|
+
EXECUTION
|
|
663
|
+
SECURITY
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
scalar join__FieldSet
|
|
667
|
+
|
|
668
|
+
enum join__Graph {
|
|
669
|
+
SERVICEA @join__graph(name: "serviceA" url: "")
|
|
670
|
+
SERVICEB @join__graph(name: "serviceB" url: "")
|
|
671
|
+
}
|
|
672
|
+
`;
|
|
673
|
+
|
|
674
|
+
const schema = buildSupergraphSchema(supergraph)[0];
|
|
675
|
+
expect(() => extractSubgraphsFromSupergraph(schema)).toThrow(
|
|
676
|
+
'Error extracting subgraphs from the supergraph: this might be due to errors in subgraphs that were mistakenly ignored by federation 0.x versions but are rejected by federation 2.\n'
|
|
677
|
+
+ 'Please try composing your subgraphs with federation 2: this should help precisely pinpoint the problems and, once fixed, generate a correct federation 2 supergraph.\n'
|
|
678
|
+
+ '\n'
|
|
679
|
+
+ 'Details:\n'
|
|
680
|
+
+ 'Error: Cannot find type "T" in subgraph "serviceB"'
|
|
681
|
+
);
|
|
682
|
+
})
|
|
@@ -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
|
+
});
|
|
@@ -178,7 +178,7 @@ describe("removeInaccessibleElements", () => {
|
|
|
178
178
|
`);
|
|
179
179
|
});
|
|
180
180
|
|
|
181
|
-
it(`handles renames of @inaccessible`, () => {
|
|
181
|
+
it(`handles renames of @inaccessible via core "as"`, () => {
|
|
182
182
|
const schema = buildSchema(`
|
|
183
183
|
directive @core(feature: String!, as: String, for: core__Purpose) repeatable on SCHEMA
|
|
184
184
|
|
|
@@ -212,6 +212,42 @@ describe("removeInaccessibleElements", () => {
|
|
|
212
212
|
expect(schema.elementByCoordinate("Query.privateField")).toBeUndefined();
|
|
213
213
|
});
|
|
214
214
|
|
|
215
|
+
it(`handles renames of @inaccessible via import "as"`, () => {
|
|
216
|
+
const schema = buildSchema(`
|
|
217
|
+
directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
|
|
218
|
+
|
|
219
|
+
enum link__Purpose {
|
|
220
|
+
EXECUTION
|
|
221
|
+
SECURITY
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
scalar link__Import
|
|
225
|
+
|
|
226
|
+
directive @foo on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
|
|
227
|
+
|
|
228
|
+
schema
|
|
229
|
+
@link(url: "https://specs.apollo.dev/link/v1.0")
|
|
230
|
+
@link(url: "https://specs.apollo.dev/inaccessible/v0.2", import: [{name: "@inaccessible", as: "@foo"}])
|
|
231
|
+
{
|
|
232
|
+
query: Query
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
type Query {
|
|
236
|
+
someField: Bar @inaccessible
|
|
237
|
+
privateField: String @foo
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
scalar Bar
|
|
241
|
+
|
|
242
|
+
directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
|
|
243
|
+
`);
|
|
244
|
+
|
|
245
|
+
removeInaccessibleElements(schema);
|
|
246
|
+
schema.validate();
|
|
247
|
+
expect(schema.elementByCoordinate("Query.someField")).toBeDefined();
|
|
248
|
+
expect(schema.elementByCoordinate("Query.privateField")).toBeUndefined();
|
|
249
|
+
});
|
|
250
|
+
|
|
215
251
|
it(`fails for @inaccessible built-ins`, () => {
|
|
216
252
|
const schema = buildSchema(`
|
|
217
253
|
${INACCESSIBLE_V02_HEADER}
|
|
@@ -2387,4 +2423,51 @@ describe("removeInaccessibleElements", () => {
|
|
|
2387
2423
|
]
|
|
2388
2424
|
`);
|
|
2389
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
|
+
});
|
|
2390
2473
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FEDERATION2_LINK_WTH_FULL_IMPORTS } from '..';
|
|
2
|
+
import { ObjectType } from '../definitions';
|
|
2
3
|
import { buildSubgraph, Subgraphs } from '../federation';
|
|
3
4
|
import { UpgradeChangeID, UpgradeResult, upgradeSubgraphsIfNecessary } from '../schemaUpgrader';
|
|
4
5
|
import './matchers';
|
|
@@ -167,3 +168,40 @@ test('update federation directive non-string arguments', () => {
|
|
|
167
168
|
}
|
|
168
169
|
`);
|
|
169
170
|
})
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
test('remove tag on external field if found on definition', () => {
|
|
174
|
+
const s1 = `
|
|
175
|
+
type Query {
|
|
176
|
+
a: A @provides(fields: "y")
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
type A @key(fields: "id") {
|
|
180
|
+
id: String
|
|
181
|
+
x: Int
|
|
182
|
+
y: Int @external @tag(name: "a tag")
|
|
183
|
+
}
|
|
184
|
+
`;
|
|
185
|
+
|
|
186
|
+
const s2 = `
|
|
187
|
+
type A @key(fields: "id") {
|
|
188
|
+
id: String
|
|
189
|
+
y: Int @tag(name: "a tag")
|
|
190
|
+
}
|
|
191
|
+
`;
|
|
192
|
+
|
|
193
|
+
const subgraphs = new Subgraphs();
|
|
194
|
+
subgraphs.add(buildSubgraph('s1', 'http://s1', s1));
|
|
195
|
+
subgraphs.add(buildSubgraph('s2', 'http://s2', s2));
|
|
196
|
+
const res = upgradeSubgraphsIfNecessary(subgraphs);
|
|
197
|
+
expect(res.errors).toBeUndefined();
|
|
198
|
+
|
|
199
|
+
expect(changeMessages(res, 's1', 'REMOVED_TAG_ON_EXTERNAL')).toStrictEqual([
|
|
200
|
+
'Removed @tag(name: "a tag") application on @external "A.y" as the @tag application is on another definition',
|
|
201
|
+
]);
|
|
202
|
+
|
|
203
|
+
const typeAInS1 = res.subgraphs?.get('s1')?.schema.type("A") as ObjectType;
|
|
204
|
+
const typeAInS2 = res.subgraphs?.get('s2')?.schema.type("A") as ObjectType;
|
|
205
|
+
expect(typeAInS1.field("y")?.appliedDirectivesOf('tag').map((d) => d.toString())).toStrictEqual([]);
|
|
206
|
+
expect(typeAInS2.field("y")?.appliedDirectivesOf('tag').map((d) => d.toString())).toStrictEqual([ '@tag(name: "a tag")' ]);
|
|
207
|
+
})
|
|
@@ -879,3 +879,77 @@ describe('@core/@link handling', () => {
|
|
|
879
879
|
buildAndValidate(doc);
|
|
880
880
|
});
|
|
881
881
|
});
|
|
882
|
+
|
|
883
|
+
describe('federation 1 schema', () => {
|
|
884
|
+
it('accepts federation directive definitions without arguments', () => {
|
|
885
|
+
const doc = gql`
|
|
886
|
+
type Query {
|
|
887
|
+
a: Int
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
directive @key on OBJECT | INTERFACE
|
|
891
|
+
directive @requires on FIELD_DEFINITION
|
|
892
|
+
`;
|
|
893
|
+
|
|
894
|
+
buildAndValidate(doc);
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
it('accepts federation directive definitions with nullable arguments', () => {
|
|
898
|
+
const doc = gql`
|
|
899
|
+
type Query {
|
|
900
|
+
a: Int
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
type T @key(fields: "id") {
|
|
904
|
+
id: ID! @requires(fields: "x")
|
|
905
|
+
x: Int @external
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
# Tests with the _FieldSet argument non-nullable
|
|
909
|
+
scalar _FieldSet
|
|
910
|
+
directive @key(fields: _FieldSet) on OBJECT | INTERFACE
|
|
911
|
+
|
|
912
|
+
# Tests with the argument as String and non-nullable
|
|
913
|
+
directive @requires(fields: String) on FIELD_DEFINITION
|
|
914
|
+
`;
|
|
915
|
+
|
|
916
|
+
buildAndValidate(doc);
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
it('accepts federation directive definitions with "FieldSet" type instead of "_FieldSet"', () => {
|
|
920
|
+
const doc = gql`
|
|
921
|
+
type Query {
|
|
922
|
+
a: Int
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
type T @key(fields: "id") {
|
|
926
|
+
id: ID!
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
scalar FieldSet
|
|
930
|
+
directive @key(fields: FieldSet) on OBJECT | INTERFACE
|
|
931
|
+
`;
|
|
932
|
+
|
|
933
|
+
buildAndValidate(doc);
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
it('rejects federation directive definition with unknown arguments', () => {
|
|
937
|
+
const doc = gql`
|
|
938
|
+
type Query {
|
|
939
|
+
a: Int
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
type T @key(fields: "id", unknown: 42) {
|
|
943
|
+
id: ID!
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
scalar _FieldSet
|
|
947
|
+
directive @key(fields: _FieldSet!, unknown: Int) on OBJECT | INTERFACE
|
|
948
|
+
`;
|
|
949
|
+
|
|
950
|
+
expect(buildForErrors(doc, { asFed2: false })).toStrictEqual([[
|
|
951
|
+
'DIRECTIVE_DEFINITION_INVALID',
|
|
952
|
+
'[S] Invalid definition for directive "@key": unknown/unsupported argument "unknown"'
|
|
953
|
+
]]);
|
|
954
|
+
});
|
|
955
|
+
})
|
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/coreSpec.ts
CHANGED
|
@@ -131,6 +131,10 @@ export abstract class FeatureDefinition {
|
|
|
131
131
|
return features.getByIdentity(this.identity);
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
get defaultCorePurpose(): CorePurpose | undefined {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
134
138
|
toString(): string {
|
|
135
139
|
return `${this.identity}/${this.version}`
|
|
136
140
|
}
|
package/src/definitions.ts
CHANGED
|
@@ -946,11 +946,13 @@ export class CoreFeature {
|
|
|
946
946
|
}
|
|
947
947
|
|
|
948
948
|
directiveNameInSchema(name: string): string {
|
|
949
|
-
if (name === this.url.name) {
|
|
950
|
-
return this.nameInSchema;
|
|
951
|
-
}
|
|
952
949
|
const elementImport = this.imports.find((i) => i.name.charAt(0) === '@' && i.name.slice(1) === name);
|
|
953
|
-
return elementImport
|
|
950
|
+
return elementImport
|
|
951
|
+
? (elementImport.as?.slice(1) ?? name)
|
|
952
|
+
: (name === this.url.name
|
|
953
|
+
? this.nameInSchema
|
|
954
|
+
: this.nameInSchema + '__' + name
|
|
955
|
+
);
|
|
954
956
|
}
|
|
955
957
|
|
|
956
958
|
typeNameInSchema(name: string): string {
|
|
@@ -2762,7 +2764,7 @@ export class DirectiveDefinition<TApplicationArgs extends {[key: string]: any} =
|
|
|
2762
2764
|
assert(false, `Directive definition ${this} can't reference other types (it's arguments can); shouldn't be asked to remove reference to ${type}`);
|
|
2763
2765
|
}
|
|
2764
2766
|
|
|
2765
|
-
|
|
2767
|
+
/**
|
|
2766
2768
|
* Removes this directive definition from its parent schema.
|
|
2767
2769
|
*
|
|
2768
2770
|
* After calling this method, this directive definition will be "detached": it will have no parent, schema, or
|
|
@@ -3213,9 +3215,16 @@ function copy(source: Schema, dest: Schema) {
|
|
|
3213
3215
|
for (const type of typesToCopy(source, dest)) {
|
|
3214
3216
|
dest.addType(newNamedType(type.kind, type.name));
|
|
3215
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.
|
|
3216
3221
|
for (const directive of directivesToCopy(source, dest)) {
|
|
3217
|
-
|
|
3222
|
+
dest.addDirectiveDefinition(directive.name);
|
|
3218
3223
|
}
|
|
3224
|
+
for (const directive of directivesToCopy(source, dest)) {
|
|
3225
|
+
copyDirectiveDefinitionInner(directive, dest.directive(directive.name)!);
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3219
3228
|
copySchemaDefinitionInner(source.schemaDefinition, dest.schemaDefinition);
|
|
3220
3229
|
for (const type of typesToCopy(source, dest)) {
|
|
3221
3230
|
copyNamedTypeInner(type, dest.type(type.name)!);
|