@apollo/federation-internals 2.0.0-preview.4 → 2.0.0-preview.8
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 +13 -3
- package/dist/buildSchema.d.ts.map +1 -1
- package/dist/buildSchema.js +50 -40
- package/dist/buildSchema.js.map +1 -1
- package/dist/coreSpec.d.ts +4 -2
- package/dist/coreSpec.d.ts.map +1 -1
- package/dist/coreSpec.js +107 -19
- package/dist/coreSpec.js.map +1 -1
- package/dist/definitions.d.ts +3 -0
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +72 -6
- 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 +29 -1
- package/dist/error.js.map +1 -1
- package/dist/federation.d.ts +4 -0
- package/dist/federation.d.ts.map +1 -1
- package/dist/federation.js +41 -11
- package/dist/federation.js.map +1 -1
- package/dist/federationSpec.d.ts +4 -0
- package/dist/federationSpec.d.ts.map +1 -1
- package/dist/federationSpec.js +20 -2
- package/dist/federationSpec.js.map +1 -1
- package/dist/inaccessibleSpec.d.ts +3 -0
- package/dist/inaccessibleSpec.d.ts.map +1 -1
- package/dist/inaccessibleSpec.js +22 -3
- package/dist/inaccessibleSpec.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/joinSpec.d.ts +1 -0
- package/dist/joinSpec.d.ts.map +1 -1
- package/dist/joinSpec.js +18 -0
- package/dist/joinSpec.js.map +1 -1
- package/dist/knownCoreFeatures.d.ts +4 -0
- package/dist/knownCoreFeatures.d.ts.map +1 -0
- package/dist/knownCoreFeatures.js +16 -0
- package/dist/knownCoreFeatures.js.map +1 -0
- package/dist/tagSpec.d.ts +1 -0
- package/dist/tagSpec.d.ts.map +1 -1
- package/dist/tagSpec.js +6 -0
- package/dist/tagSpec.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/coreSpec.test.ts +100 -0
- package/src/__tests__/definitions.test.ts +75 -0
- package/src/__tests__/removeInaccessibleElements.test.ts +1 -1
- package/src/__tests__/schemaUpgrader.test.ts +3 -2
- package/src/__tests__/subgraphValidation.test.ts +63 -4
- package/src/buildSchema.ts +97 -50
- package/src/coreSpec.ts +124 -21
- package/src/definitions.ts +99 -7
- package/src/error.ts +46 -0
- package/src/federation.ts +67 -12
- package/src/federationSpec.ts +27 -1
- package/src/inaccessibleSpec.ts +26 -10
- package/src/index.ts +1 -0
- package/src/joinSpec.ts +19 -0
- package/src/knownCoreFeatures.ts +13 -0
- package/src/tagSpec.ts +8 -0
- package/tsconfig.test.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
package/src/definitions.ts
CHANGED
|
@@ -39,6 +39,8 @@ import { SDLValidationRule } from "graphql/validation/ValidationContext";
|
|
|
39
39
|
import { specifiedSDLRules } from "graphql/validation/specifiedRules";
|
|
40
40
|
import { validateSchema } from "./validate";
|
|
41
41
|
import { createDirectiveSpecification, createScalarTypeSpecification, DirectiveSpecification, TypeSpecification } from "./directiveAndTypeSpecification";
|
|
42
|
+
import { didYouMean, suggestionList } from "./suggestions";
|
|
43
|
+
import { withModifiedErrorMessage } from "./error";
|
|
42
44
|
|
|
43
45
|
const validationErrorCode = 'GraphQLValidationFailed';
|
|
44
46
|
|
|
@@ -505,7 +507,10 @@ export abstract class SchemaElement<TOwnType extends SchemaElement<any, TParent>
|
|
|
505
507
|
this.checkUpdate();
|
|
506
508
|
const def = this.schema().directive(nameOrDefOrDirective) ?? this.schema().blueprint.onMissingDirectiveDefinition(this.schema(), nameOrDefOrDirective);
|
|
507
509
|
if (!def) {
|
|
508
|
-
throw
|
|
510
|
+
throw this.schema().blueprint.onGraphQLJSValidationError(
|
|
511
|
+
this.schema(),
|
|
512
|
+
new GraphQLError(`Unknown directive "@${nameOrDefOrDirective}".`)
|
|
513
|
+
);
|
|
509
514
|
}
|
|
510
515
|
name = nameOrDefOrDirective;
|
|
511
516
|
} else {
|
|
@@ -857,6 +862,38 @@ export class SchemaBlueprint {
|
|
|
857
862
|
validationRules(): readonly SDLValidationRule[] {
|
|
858
863
|
return specifiedSDLRules;
|
|
859
864
|
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Allows to intercept some graphQL-js error messages when we can provide additional guidance to users.
|
|
868
|
+
*/
|
|
869
|
+
onGraphQLJSValidationError(schema: Schema, error: GraphQLError): GraphQLError {
|
|
870
|
+
// For now, the main additional guidance we provide is around directives, where we could provide additional help in 2 main ways:
|
|
871
|
+
// - if a directive name is likely misspelled (somehow, graphQL-js has methods to offer suggestions on likely mispelling, but don't use this (at the
|
|
872
|
+
// time of this writting) for directive names).
|
|
873
|
+
// - for fed 2 schema, if a federation directive is refered under it's "default" naming but is not properly imported (not enforced
|
|
874
|
+
// in the method but rather in the `FederationBlueprint`).
|
|
875
|
+
//
|
|
876
|
+
// Note that intercepting/parsing error messages to modify them is never ideal, but pragmatically, it's probably better than rewriting the relevant
|
|
877
|
+
// rules entirely (in that later case, our "copied" rule would stop getting any potential graphQL-js made improvements for instance). And while such
|
|
878
|
+
// parsing is fragile, in that it'll break if the original message change, we have unit tests to surface any such breakage so it's not really a risk.
|
|
879
|
+
const matcher = /^Unknown directive "@(?<directive>[_A-Za-z][_0-9A-Za-z]*)"\.$/.exec(error.message);
|
|
880
|
+
const name = matcher?.groups?.directive;
|
|
881
|
+
if (!name) {
|
|
882
|
+
return error;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const allDefinedDirectiveNames = schema.allDirectives().map((d) => d.name);
|
|
886
|
+
const suggestions = suggestionList(name, allDefinedDirectiveNames);
|
|
887
|
+
if (suggestions.length === 0) {
|
|
888
|
+
return this.onUnknownDirectiveValidationError(schema, name, error);
|
|
889
|
+
} else {
|
|
890
|
+
return withModifiedErrorMessage(error, `${error.message}${didYouMean(suggestions.map((s) => '@' + s))}`);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
onUnknownDirectiveValidationError(_schema: Schema, _unknownDirectiveName: string, error: GraphQLError): GraphQLError {
|
|
895
|
+
return error;
|
|
896
|
+
}
|
|
860
897
|
}
|
|
861
898
|
|
|
862
899
|
export const defaultSchemaBlueprint = new SchemaBlueprint();
|
|
@@ -873,7 +910,8 @@ export class CoreFeature {
|
|
|
873
910
|
|
|
874
911
|
isFeatureDefinition(element: NamedType | DirectiveDefinition): boolean {
|
|
875
912
|
return element.name.startsWith(this.nameInSchema + '__')
|
|
876
|
-
|| (element.kind === 'DirectiveDefinition' && element.name === this.nameInSchema)
|
|
913
|
+
|| (element.kind === 'DirectiveDefinition' && element.name === this.nameInSchema)
|
|
914
|
+
|| !!this.imports.find((i) => element.name === (i.as ?? i.name));
|
|
877
915
|
}
|
|
878
916
|
|
|
879
917
|
directiveNameInSchema(name: string): string {
|
|
@@ -931,7 +969,7 @@ export class CoreFeatures {
|
|
|
931
969
|
if (existing) {
|
|
932
970
|
throw error(`Duplicate inclusion of feature ${url.identity}`);
|
|
933
971
|
}
|
|
934
|
-
const imports = extractCoreFeatureImports(typedDirective);
|
|
972
|
+
const imports = extractCoreFeatureImports(url, typedDirective);
|
|
935
973
|
const feature = new CoreFeature(url, args.as ?? url.name, directive, imports, args.for);
|
|
936
974
|
this.add(feature);
|
|
937
975
|
directive.schema().blueprint.onAddedCoreFeature(directive.schema(), feature);
|
|
@@ -995,6 +1033,10 @@ const graphQLBuiltInDirectivesSpecifications: readonly DirectiveSpecification[]
|
|
|
995
1033
|
}),
|
|
996
1034
|
];
|
|
997
1035
|
|
|
1036
|
+
|
|
1037
|
+
// A coordinate is up to 3 "graphQL name" ([_A-Za-z][_0-9A-Za-z]*).
|
|
1038
|
+
const coordinateRegexp = /^@?[_A-Za-z][_0-9A-Za-z]*(\.[_A-Za-z][_0-9A-Za-z]*)?(\([_A-Za-z][_0-9A-Za-z]*:\))?$/;
|
|
1039
|
+
|
|
998
1040
|
export class Schema {
|
|
999
1041
|
private _schemaDefinition: SchemaDefinition;
|
|
1000
1042
|
private readonly _builtInTypes = new MapWithCachedArrays<string, NamedType>();
|
|
@@ -1311,7 +1353,7 @@ export class Schema {
|
|
|
1311
1353
|
|
|
1312
1354
|
// TODO: we check that all types are properly set (aren't undefined) in `validateSchema`, but `validateSDL` will error out beforehand. We should
|
|
1313
1355
|
// probably extract that part of `validateSchema` and run `validateSDL` conditionally on that first check.
|
|
1314
|
-
let errors = validateSDL(this.toAST(), undefined, this.blueprint.validationRules());
|
|
1356
|
+
let errors = validateSDL(this.toAST(), undefined, this.blueprint.validationRules()).map((e) => this.blueprint.onGraphQLJSValidationError(this, e));
|
|
1315
1357
|
errors = errors.concat(validateSchema(this));
|
|
1316
1358
|
|
|
1317
1359
|
// We avoid adding federation-specific validations if the base schema is not proper graphQL as the later can easily trigger
|
|
@@ -1364,6 +1406,56 @@ export class Schema {
|
|
|
1364
1406
|
specifiedByDirective(schema: Schema): DirectiveDefinition<{url: string}> {
|
|
1365
1407
|
return this.getBuiltInDirective(schema, 'specifiedBy');
|
|
1366
1408
|
}
|
|
1409
|
+
|
|
1410
|
+
/**
|
|
1411
|
+
* Gets an element of the schema given its "schema coordinate".
|
|
1412
|
+
*
|
|
1413
|
+
* Note that the syntax for schema coordinates is the one from the upcoming GraphQL spec: https://github.com/graphql/graphql-spec/pull/794.
|
|
1414
|
+
*/
|
|
1415
|
+
elementByCoordinate(coordinate: string): NamedSchemaElement<any, any, any> | undefined {
|
|
1416
|
+
if (!coordinate.match(coordinateRegexp)) {
|
|
1417
|
+
throw error(`Invalid argument "${coordinate}: it is not a syntactically valid graphQL coordinate."`);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
const argStartIdx = coordinate.indexOf('(');
|
|
1421
|
+
const start = argStartIdx < 0 ? coordinate : coordinate.slice(0, argStartIdx);
|
|
1422
|
+
// Argument syntax is `foo(argName:)`, so the arg name start after the open parenthesis and go until the final ':)'.
|
|
1423
|
+
const argName = argStartIdx < 0 ? undefined : coordinate.slice(argStartIdx + 1, coordinate.length - 2);
|
|
1424
|
+
const splittedStart = start.split('.');
|
|
1425
|
+
const typeOrDirectiveName = splittedStart[0];
|
|
1426
|
+
const fieldOrEnumName = splittedStart[1];
|
|
1427
|
+
const isDirective = typeOrDirectiveName.startsWith('@');
|
|
1428
|
+
if (isDirective) {
|
|
1429
|
+
if (fieldOrEnumName) {
|
|
1430
|
+
throw error(`Invalid argument "${coordinate}: it is not a syntactically valid graphQL coordinate."`);
|
|
1431
|
+
}
|
|
1432
|
+
const directive = this.directive(typeOrDirectiveName.slice(1));
|
|
1433
|
+
return argName ? directive?.argument(argName) : directive;
|
|
1434
|
+
} else {
|
|
1435
|
+
const type = this.type(typeOrDirectiveName);
|
|
1436
|
+
if (!type || !fieldOrEnumName) {
|
|
1437
|
+
return type;
|
|
1438
|
+
}
|
|
1439
|
+
switch (type.kind) {
|
|
1440
|
+
case 'ObjectType':
|
|
1441
|
+
case 'InterfaceType':
|
|
1442
|
+
const field = type.field(fieldOrEnumName);
|
|
1443
|
+
return argName ? field?.argument(argName) : field;
|
|
1444
|
+
case 'InputObjectType':
|
|
1445
|
+
if (argName) {
|
|
1446
|
+
throw error(`Invalid argument "${coordinate}: it is not a syntactically valid graphQL coordinate."`);
|
|
1447
|
+
}
|
|
1448
|
+
return type.field(fieldOrEnumName);
|
|
1449
|
+
case 'EnumType':
|
|
1450
|
+
if (argName) {
|
|
1451
|
+
throw error(`Invalid argument "${coordinate}: it is not a syntactically valid graphQL coordinate."`);
|
|
1452
|
+
}
|
|
1453
|
+
return type.value(fieldOrEnumName);
|
|
1454
|
+
default:
|
|
1455
|
+
throw error(`Invalid argument "${coordinate}: it is not a syntactically valid graphQL coordinate."`);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1367
1459
|
}
|
|
1368
1460
|
|
|
1369
1461
|
export class RootType extends BaseExtensionMember<SchemaDefinition> {
|
|
@@ -1403,7 +1495,7 @@ export class SchemaDefinition extends SchemaElement<SchemaDefinition, Schema> {
|
|
|
1403
1495
|
const schemaDirective = applied as Directive<SchemaDefinition, CoreOrLinkDirectiveArgs>;
|
|
1404
1496
|
const args = schemaDirective.arguments();
|
|
1405
1497
|
const url = FeatureUrl.parse((args.url ?? args.feature)!);
|
|
1406
|
-
const imports = extractCoreFeatureImports(schemaDirective);
|
|
1498
|
+
const imports = extractCoreFeatureImports(url, schemaDirective);
|
|
1407
1499
|
const core = new CoreFeature(url, args.as ?? url.name, schemaDirective, imports, args.for);
|
|
1408
1500
|
Schema.prototype['markAsCoreSchema'].call(schema, core);
|
|
1409
1501
|
} else if (coreFeatures) {
|
|
@@ -2711,9 +2803,9 @@ export class Directive<
|
|
|
2711
2803
|
this.onModification();
|
|
2712
2804
|
const coreFeatures = this.schema().coreFeatures;
|
|
2713
2805
|
if (coreFeatures && this.name === coreFeatures.coreItself.nameInSchema) {
|
|
2714
|
-
// We're removing a @core directive application, so we remove it from the list of core features. And
|
|
2806
|
+
// We're removing a @core/@link directive application, so we remove it from the list of core features. And
|
|
2715
2807
|
// if it is @core itself, we clean all features (to avoid having things too inconsistent).
|
|
2716
|
-
const url = FeatureUrl.parse(this._args[
|
|
2808
|
+
const url = FeatureUrl.parse(this._args[coreFeatures.coreDefinition.urlArgName()]!);
|
|
2717
2809
|
if (url.identity === coreFeatures.coreItself.url.identity) {
|
|
2718
2810
|
// Note that we unmark first because the loop after that will nuke our parent.
|
|
2719
2811
|
Schema.prototype['unmarkAsCoreSchema'].call(this.schema());
|
package/src/error.ts
CHANGED
|
@@ -105,6 +105,34 @@ export function errorCodeDef(e: GraphQLError | string): ErrorCodeDefinition | un
|
|
|
105
105
|
return code ? codeDefByCode[code] : undefined;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
export function withModifiedErrorMessage(e: GraphQLError, newMessage: string): GraphQLError {
|
|
109
|
+
return new GraphQLError(
|
|
110
|
+
newMessage,
|
|
111
|
+
{
|
|
112
|
+
nodes: e.nodes,
|
|
113
|
+
source: e.source,
|
|
114
|
+
positions: e.positions,
|
|
115
|
+
path: e.path,
|
|
116
|
+
originalError: e.originalError,
|
|
117
|
+
extensions: e.extensions
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function withModifiedErrorNodes(e: GraphQLError, newNodes: readonly ASTNode[] | ASTNode | undefined): GraphQLError {
|
|
123
|
+
return new GraphQLError(
|
|
124
|
+
e.message,
|
|
125
|
+
{
|
|
126
|
+
nodes: newNodes,
|
|
127
|
+
source: e.source,
|
|
128
|
+
positions: e.positions,
|
|
129
|
+
path: e.path,
|
|
130
|
+
originalError: e.originalError,
|
|
131
|
+
extensions: e.extensions
|
|
132
|
+
}
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
108
136
|
const INVALID_GRAPHQL = makeCodeDefinition(
|
|
109
137
|
'INVALID_GRAPHQL',
|
|
110
138
|
'A schema is invalid GraphQL: it violates one of the rule of the specification.'
|
|
@@ -252,6 +280,11 @@ const EXTERNAL_ON_INTERFACE = makeCodeDefinition(
|
|
|
252
280
|
'The field of an interface type is marked with `@external`: as external is about marking field not resolved by the subgraph and as interface field are not resolved (only implementations of those fields are), an "external" interface field is nonsensical',
|
|
253
281
|
);
|
|
254
282
|
|
|
283
|
+
const MERGED_DIRECTIVE_APPLICATION_ON_EXTERNAL = makeCodeDefinition(
|
|
284
|
+
'MERGED_DIRECTIVE_APPLICATION_ON_EXTERNAL',
|
|
285
|
+
'In a subgraph, a field is both marked @external and has a merged directive applied to it',
|
|
286
|
+
);
|
|
287
|
+
|
|
255
288
|
const FIELD_TYPE_MISMATCH = makeCodeDefinition(
|
|
256
289
|
'FIELD_TYPE_MISMATCH',
|
|
257
290
|
'A field has a type that is incompatible with other declarations of that field in other subgraphs.',
|
|
@@ -306,6 +339,16 @@ const INVALID_LINK_DIRECTIVE_USAGE = makeCodeDefinition(
|
|
|
306
339
|
'An application of the @link directive is invalid/does not respect the specification.'
|
|
307
340
|
);
|
|
308
341
|
|
|
342
|
+
const LINK_IMPORT_NAME_MISMATCH = makeCodeDefinition(
|
|
343
|
+
'LINK_IMPORT_NAME_MISMATCH',
|
|
344
|
+
'The import name for a merged directive (as declared by the relevant `@link(import:)` argument) is inconsistent between subgraphs.'
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
const REFERENCED_INACCESSIBLE = makeCodeDefinition(
|
|
348
|
+
'REFERENCED_INACCESSIBLE',
|
|
349
|
+
'An element is marked as @inaccessible but is referenced by a non-inaccessible element.'
|
|
350
|
+
);
|
|
351
|
+
|
|
309
352
|
const REQUIRED_ARGUMENT_MISSING_IN_SOME_SUBGRAPH = makeCodeDefinition(
|
|
310
353
|
'REQUIRED_ARGUMENT_MISSING_IN_SOME_SUBGRAPH',
|
|
311
354
|
'An argument of a field or directive definition is mandatory in some subgraphs, but the argument is not defined in all subgraphs that define the field or directive definition.'
|
|
@@ -360,6 +403,7 @@ export const ERRORS = {
|
|
|
360
403
|
EXTERNAL_ARGUMENT_TYPE_MISMATCH,
|
|
361
404
|
EXTERNAL_ARGUMENT_DEFAULT_MISMATCH,
|
|
362
405
|
EXTERNAL_ON_INTERFACE,
|
|
406
|
+
MERGED_DIRECTIVE_APPLICATION_ON_EXTERNAL,
|
|
363
407
|
FIELD_TYPE_MISMATCH,
|
|
364
408
|
ARGUMENT_TYPE_MISMATCH,
|
|
365
409
|
INPUT_FIELD_DEFAULT_MISMATCH,
|
|
@@ -370,6 +414,8 @@ export const ERRORS = {
|
|
|
370
414
|
INTERFACE_FIELD_IMPLEM_TYPE_MISMATCH,
|
|
371
415
|
INVALID_FIELD_SHARING,
|
|
372
416
|
INVALID_LINK_DIRECTIVE_USAGE,
|
|
417
|
+
LINK_IMPORT_NAME_MISMATCH,
|
|
418
|
+
REFERENCED_INACCESSIBLE,
|
|
373
419
|
REQUIRED_ARGUMENT_MISSING_IN_SOME_SUBGRAPH,
|
|
374
420
|
SATISFIABILITY_ERROR,
|
|
375
421
|
};
|
package/src/federation.ts
CHANGED
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
ErrorCodeDefinition,
|
|
51
51
|
ERROR_CATEGORIES,
|
|
52
52
|
ERRORS,
|
|
53
|
+
withModifiedErrorMessage,
|
|
53
54
|
} from "./error";
|
|
54
55
|
import { computeShareables } from "./sharing";
|
|
55
56
|
import {
|
|
@@ -71,10 +72,14 @@ import {
|
|
|
71
72
|
extendsDirectiveSpec,
|
|
72
73
|
shareableDirectiveSpec,
|
|
73
74
|
tagDirectiveSpec,
|
|
75
|
+
inaccessibleDirectiveSpec,
|
|
74
76
|
FEDERATION2_SPEC_DIRECTIVES,
|
|
77
|
+
ALL_FEDERATION_DIRECTIVES_DEFAULT_NAMES,
|
|
78
|
+
FEDERATION2_ONLY_SPEC_DIRECTIVES,
|
|
75
79
|
} from "./federationSpec";
|
|
76
80
|
import { defaultPrintOptions, PrintOptions as PrintOptions, printSchema } from "./print";
|
|
77
81
|
import { createObjectTypeSpecification, createScalarTypeSpecification, createUnionTypeSpecification } from "./directiveAndTypeSpecification";
|
|
82
|
+
import { didYouMean, suggestionList } from "./suggestions";
|
|
78
83
|
|
|
79
84
|
const linkSpec = LINK_VERSIONS.latest();
|
|
80
85
|
const tagSpec = TAG_VERSIONS.latest();
|
|
@@ -102,6 +107,7 @@ const FEDERATION_SPECIFIC_VALIDATION_RULES = [
|
|
|
102
107
|
|
|
103
108
|
const FEDERATION_VALIDATION_RULES = specifiedSDLRules.filter(rule => !FEDERATION_OMITTED_VALIDATION_RULES.includes(rule)).concat(FEDERATION_SPECIFIC_VALIDATION_RULES);
|
|
104
109
|
|
|
110
|
+
|
|
105
111
|
// Returns a list of the coordinate of all the fields in the selection that are marked external.
|
|
106
112
|
function validateFieldSetSelections(
|
|
107
113
|
directiveName: string,
|
|
@@ -417,16 +423,6 @@ function formatFieldsToReturnType([type, implems]: [string, FieldDefinition<Obje
|
|
|
417
423
|
return `${joinStrings(implems.map(printFieldCoordinate))} ${implems.length == 1 ? 'has' : 'have'} type "${type}"`;
|
|
418
424
|
}
|
|
419
425
|
|
|
420
|
-
function checkIfFed2Schema(schema: Schema): boolean {
|
|
421
|
-
const core = schema.coreFeatures;
|
|
422
|
-
if (!core) {
|
|
423
|
-
return false
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
const federationFeature = core.getByIdentity(federationSpec.identity);
|
|
427
|
-
return !!federationFeature && federationFeature.url.version.satisfies(new FeatureVersion(2, 0));
|
|
428
|
-
}
|
|
429
|
-
|
|
430
426
|
export class FederationMetadata {
|
|
431
427
|
private _externalTester?: ExternalTester;
|
|
432
428
|
private _sharingPredicate?: (field: FieldDefinition<CompositeType>) => boolean;
|
|
@@ -443,11 +439,16 @@ export class FederationMetadata {
|
|
|
443
439
|
|
|
444
440
|
isFed2Schema(): boolean {
|
|
445
441
|
if (!this._isFed2Schema) {
|
|
446
|
-
|
|
442
|
+
const feature = this.federationFeature();
|
|
443
|
+
this._isFed2Schema = !!feature && feature.url.version.satisfies(new FeatureVersion(2, 0))
|
|
447
444
|
}
|
|
448
445
|
return this._isFed2Schema;
|
|
449
446
|
}
|
|
450
447
|
|
|
448
|
+
federationFeature(): CoreFeature | undefined {
|
|
449
|
+
return this.schema.coreFeatures?.getByIdentity(federationSpec.identity);
|
|
450
|
+
}
|
|
451
|
+
|
|
451
452
|
private externalTester(): ExternalTester {
|
|
452
453
|
if (!this._externalTester) {
|
|
453
454
|
this._externalTester = new ExternalTester(this.schema);
|
|
@@ -557,6 +558,10 @@ export class FederationMetadata {
|
|
|
557
558
|
return this.getFederationDirective(tagDirectiveSpec.name);
|
|
558
559
|
}
|
|
559
560
|
|
|
561
|
+
inaccessibleDirective(): DirectiveDefinition<{}> {
|
|
562
|
+
return this.getFederationDirective(inaccessibleDirectiveSpec.name);
|
|
563
|
+
}
|
|
564
|
+
|
|
560
565
|
allFederationDirectives(): DirectiveDefinition[] {
|
|
561
566
|
const baseDirectives = [
|
|
562
567
|
this.keyDirective(),
|
|
@@ -567,7 +572,7 @@ export class FederationMetadata {
|
|
|
567
572
|
this.extendsDirective(),
|
|
568
573
|
];
|
|
569
574
|
return this.isFed2Schema()
|
|
570
|
-
? baseDirectives.concat(this.shareableDirective())
|
|
575
|
+
? baseDirectives.concat(this.shareableDirective(), this.inaccessibleDirective())
|
|
571
576
|
: baseDirectives;
|
|
572
577
|
}
|
|
573
578
|
|
|
@@ -769,6 +774,52 @@ export class FederationBlueprint extends SchemaBlueprint {
|
|
|
769
774
|
validationRules(): readonly SDLValidationRule[] {
|
|
770
775
|
return FEDERATION_VALIDATION_RULES;
|
|
771
776
|
}
|
|
777
|
+
|
|
778
|
+
onUnknownDirectiveValidationError(schema: Schema, unknownDirectiveName: string, error: GraphQLError): GraphQLError {
|
|
779
|
+
const metadata = federationMetadata(schema);
|
|
780
|
+
assert(metadata, `This method should only have been called on a subgraph schema`)
|
|
781
|
+
if (ALL_FEDERATION_DIRECTIVES_DEFAULT_NAMES.includes(unknownDirectiveName)) {
|
|
782
|
+
// The directive name is "unknown" but it is a default federation directive name. So it means one of a few things
|
|
783
|
+
// happened:
|
|
784
|
+
// 1. it's a fed1 schema but the directive is a fed2 only one (only possible case for fed1 schema).
|
|
785
|
+
// 2. the directive has not been imported at all (so needs to be prefixed for it to work).
|
|
786
|
+
// 3. the directive has an `import`, but it's been aliased to another name.
|
|
787
|
+
if (metadata.isFed2Schema()) {
|
|
788
|
+
const federationFeature = metadata.federationFeature();
|
|
789
|
+
assert(federationFeature, 'Fed2 subgraph _must_ link to the federation feature')
|
|
790
|
+
const directiveNameInSchema = federationFeature.directiveNameInSchema(unknownDirectiveName);
|
|
791
|
+
console.log(`For ${unknownDirectiveName}, name in schema = ${directiveNameInSchema}`);
|
|
792
|
+
if (directiveNameInSchema.startsWith(federationFeature.nameInSchema + '__')) {
|
|
793
|
+
// There is no import for that directive
|
|
794
|
+
return withModifiedErrorMessage(
|
|
795
|
+
error,
|
|
796
|
+
`${error.message} If you meant the "@${unknownDirectiveName}" federation directive, you should use fully-qualified name "@${directiveNameInSchema}" or add "@${unknownDirectiveName}" to the \`import\` argument of the @link to the federation specification.`
|
|
797
|
+
);
|
|
798
|
+
} else {
|
|
799
|
+
// There's an import, but it's renamed
|
|
800
|
+
return withModifiedErrorMessage(
|
|
801
|
+
error,
|
|
802
|
+
`${error.message} If you meant the "@${unknownDirectiveName}" federation directive, you should use "@${directiveNameInSchema}" as it is imported under that name in the @link to the federation specification of this schema.`
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
} else {
|
|
806
|
+
return withModifiedErrorMessage(
|
|
807
|
+
error,
|
|
808
|
+
`${error.message} If you meant the "@${unknownDirectiveName}" federation 2 directive, note that this schema is a federation 1 schema. To be a federation 2 schema, it needs to @link to the federation specifcation v2.`
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
} else if (!metadata.isFed2Schema()) {
|
|
812
|
+
// We could get here in the case where a fed1 schema has tried to use a fed2 directive but mispelled it.
|
|
813
|
+
const suggestions = suggestionList(unknownDirectiveName, FEDERATION2_ONLY_SPEC_DIRECTIVES.map((spec) => spec.name));
|
|
814
|
+
if (suggestions.length > 0) {
|
|
815
|
+
return withModifiedErrorMessage(
|
|
816
|
+
error,
|
|
817
|
+
`${error.message}${didYouMean(suggestions.map((s) => '@' + s))} If so, note that ${suggestions.length === 1 ? 'it is a federation 2 directive' : 'they are federation 2 directives'} but this schema is a federation 1 one. To be a federation 2 schema, it needs to @link to the federation specifcation v2.`
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return error;
|
|
822
|
+
}
|
|
772
823
|
}
|
|
773
824
|
|
|
774
825
|
const federationBlueprint = new FederationBlueprint();
|
|
@@ -817,6 +868,10 @@ export function setSchemaAsFed2Subgraph(schema: Schema) {
|
|
|
817
868
|
completeSubgraphSchema(schema);
|
|
818
869
|
}
|
|
819
870
|
|
|
871
|
+
// This is the full @link declaration as added by `asFed2SubgraphDocument`. It's here primarily for uses by tests that print and match
|
|
872
|
+
// subgraph schema to avoid having to update 20+ tests every time we use a new directive or the order of import changes ...
|
|
873
|
+
export const FEDERATION2_LINK_WTH_FULL_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible"])';
|
|
874
|
+
|
|
820
875
|
export function asFed2SubgraphDocument(document: DocumentNode): DocumentNode {
|
|
821
876
|
const fed2LinkExtension: SchemaExtensionNode = {
|
|
822
877
|
kind: Kind.SCHEMA_EXTENSION,
|
package/src/federationSpec.ts
CHANGED
|
@@ -13,6 +13,8 @@ import { DirectiveLocation } from "graphql";
|
|
|
13
13
|
import { assert } from "./utils";
|
|
14
14
|
import { tagLocations } from "./tagSpec";
|
|
15
15
|
import { federationMetadata } from "./federation";
|
|
16
|
+
import { registerKnownFeature } from "./knownCoreFeatures";
|
|
17
|
+
import { inaccessibleLocations } from "./inaccessibleSpec";
|
|
16
18
|
|
|
17
19
|
export const federationIdentity = 'https://specs.apollo.dev/federation';
|
|
18
20
|
|
|
@@ -62,11 +64,17 @@ export const shareableDirectiveSpec = createDirectiveSpecification({
|
|
|
62
64
|
export const tagDirectiveSpec = createDirectiveSpecification({
|
|
63
65
|
name:'tag',
|
|
64
66
|
locations: tagLocations,
|
|
67
|
+
repeatable: true,
|
|
65
68
|
argumentFct: (schema) => {
|
|
66
69
|
return [{ name: 'name', type: new NonNullType(schema.stringType()) }];
|
|
67
70
|
}
|
|
68
71
|
});
|
|
69
72
|
|
|
73
|
+
export const inaccessibleDirectiveSpec = createDirectiveSpecification({
|
|
74
|
+
name:'inaccessible',
|
|
75
|
+
locations: inaccessibleLocations,
|
|
76
|
+
});
|
|
77
|
+
|
|
70
78
|
function fieldsArgument(schema: Schema): ArgumentSpecification {
|
|
71
79
|
return { name: 'fields', type: fieldSetType(schema) };
|
|
72
80
|
}
|
|
@@ -77,17 +85,27 @@ function fieldSetType(schema: Schema): InputType {
|
|
|
77
85
|
return new NonNullType(metadata.fieldSetType());
|
|
78
86
|
}
|
|
79
87
|
|
|
88
|
+
export const FEDERATION2_ONLY_SPEC_DIRECTIVES = [
|
|
89
|
+
shareableDirectiveSpec,
|
|
90
|
+
inaccessibleDirectiveSpec,
|
|
91
|
+
];
|
|
92
|
+
|
|
80
93
|
// Note that this is only used for federation 2+ (federation 1 adds the same directive, but not through a core spec).
|
|
81
94
|
export const FEDERATION2_SPEC_DIRECTIVES = [
|
|
82
95
|
keyDirectiveSpec,
|
|
83
96
|
requiresDirectiveSpec,
|
|
84
97
|
providesDirectiveSpec,
|
|
85
98
|
externalDirectiveSpec,
|
|
86
|
-
shareableDirectiveSpec,
|
|
87
99
|
tagDirectiveSpec,
|
|
88
100
|
extendsDirectiveSpec, // TODO: should we stop supporting that?
|
|
101
|
+
...FEDERATION2_ONLY_SPEC_DIRECTIVES,
|
|
89
102
|
];
|
|
90
103
|
|
|
104
|
+
// Note that this is meant to contain _all_ federation directive names ever supported, regardless of which version.
|
|
105
|
+
// But currently, fed2 directives are a superset of fed1's so ... (but this may change if we stop supporting `@extends`
|
|
106
|
+
// in fed2).
|
|
107
|
+
export const ALL_FEDERATION_DIRECTIVES_DEFAULT_NAMES = FEDERATION2_SPEC_DIRECTIVES.map((spec) => spec.name);
|
|
108
|
+
|
|
91
109
|
export const FEDERATION_SPEC_TYPES = [
|
|
92
110
|
fieldSetTypeSpec,
|
|
93
111
|
]
|
|
@@ -107,7 +125,15 @@ export class FederationSpecDefinition extends FeatureDefinition {
|
|
|
107
125
|
directive.checkOrAdd(schema, feature.directiveNameInSchema(directive.name));
|
|
108
126
|
}
|
|
109
127
|
}
|
|
128
|
+
|
|
129
|
+
allElementNames(): string[] {
|
|
130
|
+
return FEDERATION2_SPEC_DIRECTIVES.map((spec) => `@${spec.name}`).concat([
|
|
131
|
+
fieldSetTypeSpec.name,
|
|
132
|
+
])
|
|
133
|
+
}
|
|
110
134
|
}
|
|
111
135
|
|
|
112
136
|
export const FEDERATION_VERSIONS = new FeatureDefinitions<FederationSpecDefinition>(federationIdentity)
|
|
113
137
|
.add(new FederationSpecDefinition(new FeatureVersion(2, 0)));
|
|
138
|
+
|
|
139
|
+
registerKnownFeature(FEDERATION_VERSIONS);
|
package/src/inaccessibleSpec.ts
CHANGED
|
@@ -8,31 +8,41 @@ import {
|
|
|
8
8
|
Schema,
|
|
9
9
|
} from "./definitions";
|
|
10
10
|
import { GraphQLError, DirectiveLocation } from "graphql";
|
|
11
|
+
import { registerKnownFeature } from "./knownCoreFeatures";
|
|
12
|
+
import { ERRORS } from "./error";
|
|
11
13
|
|
|
12
14
|
export const inaccessibleIdentity = 'https://specs.apollo.dev/inaccessible';
|
|
13
15
|
|
|
16
|
+
export const inaccessibleLocations = [
|
|
17
|
+
DirectiveLocation.FIELD_DEFINITION,
|
|
18
|
+
DirectiveLocation.OBJECT,
|
|
19
|
+
DirectiveLocation.INTERFACE,
|
|
20
|
+
DirectiveLocation.UNION,
|
|
21
|
+
];
|
|
22
|
+
|
|
14
23
|
export class InaccessibleSpecDefinition extends FeatureDefinition {
|
|
15
24
|
constructor(version: FeatureVersion) {
|
|
16
25
|
super(new FeatureUrl(inaccessibleIdentity, 'inaccessible', version));
|
|
17
26
|
}
|
|
18
27
|
|
|
19
28
|
addElementsToSchema(schema: Schema) {
|
|
20
|
-
this.addDirective(schema, 'inaccessible').addLocations(
|
|
21
|
-
DirectiveLocation.FIELD_DEFINITION,
|
|
22
|
-
DirectiveLocation.OBJECT,
|
|
23
|
-
DirectiveLocation.INTERFACE,
|
|
24
|
-
DirectiveLocation.UNION,
|
|
25
|
-
);
|
|
29
|
+
this.addDirective(schema, 'inaccessible').addLocations(...inaccessibleLocations);
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
inaccessibleDirective(schema: Schema): DirectiveDefinition<Record<string, never>> {
|
|
29
33
|
return this.directive(schema, 'inaccessible')!;
|
|
30
34
|
}
|
|
35
|
+
|
|
36
|
+
allElementNames(): string[] {
|
|
37
|
+
return ['@inaccessible'];
|
|
38
|
+
}
|
|
31
39
|
}
|
|
32
40
|
|
|
33
41
|
export const INACCESSIBLE_VERSIONS = new FeatureDefinitions<InaccessibleSpecDefinition>(inaccessibleIdentity)
|
|
34
42
|
.add(new InaccessibleSpecDefinition(new FeatureVersion(0, 1)));
|
|
35
43
|
|
|
44
|
+
registerKnownFeature(INACCESSIBLE_VERSIONS);
|
|
45
|
+
|
|
36
46
|
export function removeInaccessibleElements(schema: Schema) {
|
|
37
47
|
const coreFeatures = schema.coreFeatures;
|
|
38
48
|
if (!coreFeatures) {
|
|
@@ -69,10 +79,16 @@ export function removeInaccessibleElements(schema: Schema) {
|
|
|
69
79
|
// field type will be `undefined`).
|
|
70
80
|
if (reference.kind === 'FieldDefinition') {
|
|
71
81
|
if (!reference.hasAppliedDirective(inaccessibleDirective)) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
82
|
+
// We ship the inaccessible type and it's invalid reference in the extensions so composition can extract those and add proper links
|
|
83
|
+
// in the subgraphs for those elements.
|
|
84
|
+
throw ERRORS.REFERENCED_INACCESSIBLE.err({
|
|
85
|
+
message: `Field "${reference.coordinate}" returns @inaccessible type "${type}" without being marked @inaccessible itself.`,
|
|
86
|
+
nodes: reference.sourceAST,
|
|
87
|
+
extensions: {
|
|
88
|
+
"inaccessible_element": type.coordinate,
|
|
89
|
+
"inaccessible_reference": reference.coordinate,
|
|
90
|
+
}
|
|
91
|
+
});
|
|
76
92
|
}
|
|
77
93
|
}
|
|
78
94
|
// Other references can be:
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ export * from './debug';
|
|
|
10
10
|
export * from './coreSpec';
|
|
11
11
|
export * from './joinSpec';
|
|
12
12
|
export * from './tagSpec';
|
|
13
|
+
export * from './inaccessibleSpec';
|
|
13
14
|
export * from './federationSpec';
|
|
14
15
|
export * from './supergraphs';
|
|
15
16
|
export * from './extractSubgraphsFromSupergraph';
|
package/src/joinSpec.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
NonNullType,
|
|
9
9
|
} from "./definitions";
|
|
10
10
|
import { Subgraph, Subgraphs } from "./federation";
|
|
11
|
+
import { registerKnownFeature } from './knownCoreFeatures';
|
|
11
12
|
import { MultiMap } from "./utils";
|
|
12
13
|
|
|
13
14
|
export const joinIdentity = 'https://specs.apollo.dev/join';
|
|
@@ -90,6 +91,22 @@ export class JoinSpecDefinition extends FeatureDefinition {
|
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
allElementNames(): string[] {
|
|
95
|
+
const names = [
|
|
96
|
+
'graph',
|
|
97
|
+
'Graph',
|
|
98
|
+
'FieldSet',
|
|
99
|
+
'@type',
|
|
100
|
+
'@field',
|
|
101
|
+
];
|
|
102
|
+
if (this.isV01()) {
|
|
103
|
+
names.push('@owner');
|
|
104
|
+
} else {
|
|
105
|
+
names.push('@implements');
|
|
106
|
+
}
|
|
107
|
+
return names;
|
|
108
|
+
}
|
|
109
|
+
|
|
93
110
|
populateGraphEnum(schema: Schema, subgraphs: Subgraphs): Map<string, string> {
|
|
94
111
|
// Duplicate enum values can occur due to sanitization and must be accounted for
|
|
95
112
|
// collect the duplicates in an array so we can uniquify them in a second pass.
|
|
@@ -160,3 +177,5 @@ export class JoinSpecDefinition extends FeatureDefinition {
|
|
|
160
177
|
export const JOIN_VERSIONS = new FeatureDefinitions<JoinSpecDefinition>(joinIdentity)
|
|
161
178
|
.add(new JoinSpecDefinition(new FeatureVersion(0, 1)))
|
|
162
179
|
.add(new JoinSpecDefinition(new FeatureVersion(0, 2)));
|
|
180
|
+
|
|
181
|
+
registerKnownFeature(JOIN_VERSIONS);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { FeatureDefinition, FeatureDefinitions, FeatureUrl } from "./coreSpec";
|
|
2
|
+
|
|
3
|
+
const registeredFeatures: Map<string, FeatureDefinitions> = new Map();
|
|
4
|
+
|
|
5
|
+
export function registerKnownFeature(definitions: FeatureDefinitions) {
|
|
6
|
+
if (!registeredFeatures.has(definitions.identity)) {
|
|
7
|
+
registeredFeatures.set(definitions.identity, definitions);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function coreFeatureDefinitionIfKnown(url: FeatureUrl): FeatureDefinition | undefined {
|
|
12
|
+
return registeredFeatures.get(url.identity)?.find(url.version);
|
|
13
|
+
}
|
package/src/tagSpec.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { DirectiveLocation, GraphQLError } from "graphql";
|
|
|
2
2
|
import { FeatureDefinition, FeatureDefinitions, FeatureUrl, FeatureVersion } from "./coreSpec";
|
|
3
3
|
import { DirectiveDefinition, NonNullType, Schema } from "./definitions";
|
|
4
4
|
import { ERRORS } from "./error";
|
|
5
|
+
import { registerKnownFeature } from "./knownCoreFeatures";
|
|
5
6
|
import { sameType } from "./types";
|
|
6
7
|
|
|
7
8
|
export const tagIdentity = 'https://specs.apollo.dev/tag';
|
|
@@ -22,6 +23,7 @@ export class TagSpecDefinition extends FeatureDefinition {
|
|
|
22
23
|
|
|
23
24
|
addElementsToSchema(schema: Schema) {
|
|
24
25
|
const directive = this.addDirective(schema, 'tag').addLocations(...tagLocations);
|
|
26
|
+
directive.repeatable = true;
|
|
25
27
|
directive.addArgument("name", new NonNullType(schema.stringType()));
|
|
26
28
|
}
|
|
27
29
|
|
|
@@ -42,7 +44,13 @@ export class TagSpecDefinition extends FeatureDefinition {
|
|
|
42
44
|
}
|
|
43
45
|
return undefined;
|
|
44
46
|
}
|
|
47
|
+
|
|
48
|
+
allElementNames(): string[] {
|
|
49
|
+
return ["@tag"];
|
|
50
|
+
}
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
export const TAG_VERSIONS = new FeatureDefinitions<TagSpecDefinition>(tagIdentity)
|
|
48
54
|
.add(new TagSpecDefinition(new FeatureVersion(0, 1)));
|
|
55
|
+
|
|
56
|
+
registerKnownFeature(TAG_VERSIONS);
|