@apollo/federation-internals 2.9.0 → 2.9.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/dist/argumentCompositionStrategies.d.ts +8 -14
- package/dist/argumentCompositionStrategies.d.ts.map +1 -1
- package/dist/argumentCompositionStrategies.js +48 -48
- package/dist/argumentCompositionStrategies.js.map +1 -1
- package/dist/definitions.d.ts +1 -0
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +12 -10
- 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 +10 -0
- package/dist/error.js.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.js +68 -63
- package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
- package/dist/federation.d.ts.map +1 -1
- package/dist/federation.js +72 -1
- package/dist/federation.js.map +1 -1
- package/dist/operations.d.ts +6 -4
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +28 -18
- package/dist/operations.js.map +1 -1
- package/dist/specs/coreSpec.d.ts +2 -2
- package/dist/specs/coreSpec.d.ts.map +1 -1
- package/dist/specs/coreSpec.js +15 -20
- package/dist/specs/coreSpec.js.map +1 -1
- package/dist/specs/costSpec.d.ts +3 -0
- package/dist/specs/costSpec.d.ts.map +1 -1
- package/dist/specs/costSpec.js +6 -0
- package/dist/specs/costSpec.js.map +1 -1
- package/dist/supergraphs.d.ts +8 -1
- package/dist/supergraphs.d.ts.map +1 -1
- package/dist/supergraphs.js +20 -2
- package/dist/supergraphs.js.map +1 -1
- package/dist/values.d.ts.map +1 -1
- package/dist/values.js +14 -5
- package/dist/values.js.map +1 -1
- package/package.json +1 -1
- package/src/argumentCompositionStrategies.ts +68 -50
- package/src/definitions.ts +25 -9
- package/src/error.ts +36 -0
- package/src/extractSubgraphsFromSupergraph.ts +73 -92
- package/src/federation.ts +140 -1
- package/src/operations.ts +36 -19
- package/src/specs/coreSpec.ts +27 -26
- package/src/specs/costSpec.ts +9 -1
- package/src/supergraphs.ts +31 -3
- package/src/values.ts +13 -5
package/src/definitions.ts
CHANGED
|
@@ -984,12 +984,28 @@ export class CoreFeature {
|
|
|
984
984
|
}
|
|
985
985
|
|
|
986
986
|
directiveNameInSchema(name: string): string {
|
|
987
|
-
|
|
987
|
+
return CoreFeature.directiveNameInSchemaForCoreArguments(
|
|
988
|
+
this.url,
|
|
989
|
+
this.nameInSchema,
|
|
990
|
+
this.imports,
|
|
991
|
+
name,
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
static directiveNameInSchemaForCoreArguments(
|
|
996
|
+
specUrl: FeatureUrl,
|
|
997
|
+
specNameInSchema: string,
|
|
998
|
+
imports: CoreImport[],
|
|
999
|
+
directiveNameInSpec: string,
|
|
1000
|
+
): string {
|
|
1001
|
+
const elementImport = imports.find((i) =>
|
|
1002
|
+
i.name.charAt(0) === '@' && i.name.slice(1) === directiveNameInSpec
|
|
1003
|
+
);
|
|
988
1004
|
return elementImport
|
|
989
|
-
? (elementImport.as?.slice(1) ??
|
|
990
|
-
: (
|
|
991
|
-
?
|
|
992
|
-
:
|
|
1005
|
+
? (elementImport.as?.slice(1) ?? directiveNameInSpec)
|
|
1006
|
+
: (directiveNameInSpec === specUrl.name
|
|
1007
|
+
? specNameInSchema
|
|
1008
|
+
: specNameInSchema + '__' + directiveNameInSpec
|
|
993
1009
|
);
|
|
994
1010
|
}
|
|
995
1011
|
|
|
@@ -1064,7 +1080,7 @@ export class CoreFeatures {
|
|
|
1064
1080
|
const feature = this.byAlias.get(splitted[0]);
|
|
1065
1081
|
return feature ? {
|
|
1066
1082
|
feature,
|
|
1067
|
-
nameInFeature: splitted
|
|
1083
|
+
nameInFeature: splitted.slice(1).join('__'),
|
|
1068
1084
|
isImported: false,
|
|
1069
1085
|
} : undefined;
|
|
1070
1086
|
} else {
|
|
@@ -1076,7 +1092,7 @@ export class CoreFeatures {
|
|
|
1076
1092
|
if ((as ?? name) === importName) {
|
|
1077
1093
|
return {
|
|
1078
1094
|
feature,
|
|
1079
|
-
nameInFeature: name.slice(1),
|
|
1095
|
+
nameInFeature: isDirective ? name.slice(1) : name,
|
|
1080
1096
|
isImported: true,
|
|
1081
1097
|
};
|
|
1082
1098
|
}
|
|
@@ -1088,8 +1104,8 @@ export class CoreFeatures {
|
|
|
1088
1104
|
if (directFeature && isDirective) {
|
|
1089
1105
|
return {
|
|
1090
1106
|
feature: directFeature,
|
|
1091
|
-
nameInFeature:
|
|
1092
|
-
isImported:
|
|
1107
|
+
nameInFeature: element.name,
|
|
1108
|
+
isImported: false,
|
|
1093
1109
|
};
|
|
1094
1110
|
}
|
|
1095
1111
|
|
package/src/error.ts
CHANGED
|
@@ -710,6 +710,36 @@ const CONTEXTUAL_ARGUMENT_NOT_CONTEXTUAL_IN_ALL_SUBGRAPHS = makeCodeDefinition(
|
|
|
710
710
|
{ addedIn: '2.7.0' },
|
|
711
711
|
);
|
|
712
712
|
|
|
713
|
+
const COST_APPLIED_TO_INTERFACE_FIELD = makeCodeDefinition(
|
|
714
|
+
'COST_APPLIED_TO_INTERFACE_FIELD',
|
|
715
|
+
'The `@cost` directive must be applied to concrete types',
|
|
716
|
+
{ addedIn: '2.9.2' },
|
|
717
|
+
);
|
|
718
|
+
|
|
719
|
+
const LIST_SIZE_APPLIED_TO_NON_LIST = makeCodeDefinition(
|
|
720
|
+
'LIST_SIZE_APPLIED_TO_NON_LIST',
|
|
721
|
+
'The `@listSize` directive must be applied to list types',
|
|
722
|
+
{ addedIn: '2.9.2' },
|
|
723
|
+
);
|
|
724
|
+
|
|
725
|
+
const LIST_SIZE_INVALID_ASSUMED_SIZE = makeCodeDefinition(
|
|
726
|
+
'LIST_SIZE_INVALID_ASSUMED_SIZE',
|
|
727
|
+
'The `@listSize` directive assumed size cannot be negative',
|
|
728
|
+
{ addedIn: '2.9.2' },
|
|
729
|
+
);
|
|
730
|
+
|
|
731
|
+
const LIST_SIZE_INVALID_SLICING_ARGUMENT = makeCodeDefinition(
|
|
732
|
+
'LIST_SIZE_INVALID_SLICING_ARGUMENT',
|
|
733
|
+
'The `@listSize` directive must have existing integer slicing arguments',
|
|
734
|
+
{ addedIn: '2.9.2' },
|
|
735
|
+
);
|
|
736
|
+
|
|
737
|
+
const LIST_SIZE_INVALID_SIZED_FIELD = makeCodeDefinition(
|
|
738
|
+
'LIST_SIZE_INVALID_SIZED_FIELD',
|
|
739
|
+
'The `@listSize` directive must reference existing list fields as sized fields',
|
|
740
|
+
{ addedIn: '2.9.2' },
|
|
741
|
+
);
|
|
742
|
+
|
|
713
743
|
export const ERROR_CATEGORIES = {
|
|
714
744
|
DIRECTIVE_FIELDS_MISSING_EXTERNAL,
|
|
715
745
|
DIRECTIVE_UNSUPPORTED_ON_INTERFACE,
|
|
@@ -824,6 +854,12 @@ export const ERRORS = {
|
|
|
824
854
|
SOURCE_FIELD_SELECTION_INVALID,
|
|
825
855
|
SOURCE_FIELD_NOT_ON_ROOT_OR_ENTITY_FIELD,
|
|
826
856
|
CONTEXTUAL_ARGUMENT_NOT_CONTEXTUAL_IN_ALL_SUBGRAPHS,
|
|
857
|
+
// Errors related to demand control
|
|
858
|
+
COST_APPLIED_TO_INTERFACE_FIELD,
|
|
859
|
+
LIST_SIZE_APPLIED_TO_NON_LIST,
|
|
860
|
+
LIST_SIZE_INVALID_ASSUMED_SIZE,
|
|
861
|
+
LIST_SIZE_INVALID_SIZED_FIELD,
|
|
862
|
+
LIST_SIZE_INVALID_SLICING_ARGUMENT,
|
|
827
863
|
};
|
|
828
864
|
|
|
829
865
|
const codeDefByCode = Object.values(ERRORS).reduce((obj: {[code: string]: ErrorCodeDefinition}, codeDef: ErrorCodeDefinition) => { obj[codeDef.code] = codeDef; return obj; }, {});
|
|
@@ -40,7 +40,7 @@ import { parseSelectionSet } from "./operations";
|
|
|
40
40
|
import fs from 'fs';
|
|
41
41
|
import path from 'path';
|
|
42
42
|
import { validateStringContainsBoolean } from "./utils";
|
|
43
|
-
import {
|
|
43
|
+
import { ContextSpecDefinition, CostSpecDefinition, SchemaElement, errorCauses, isFederationDirectiveDefinedInSchema, printErrors } from ".";
|
|
44
44
|
|
|
45
45
|
function filteredTypes(
|
|
46
46
|
supergraph: Schema,
|
|
@@ -194,7 +194,7 @@ function typesUsedInFederationDirective(fieldSet: string | undefined, parentType
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
export function extractSubgraphsFromSupergraph(supergraph: Schema, validateExtractedSubgraphs: boolean = true): [Subgraphs, Map<string, string>] {
|
|
197
|
-
const [coreFeatures, joinSpec] = validateSupergraph(supergraph);
|
|
197
|
+
const [coreFeatures, joinSpec, contextSpec, costSpec] = validateSupergraph(supergraph);
|
|
198
198
|
const isFed1 = joinSpec.version.equals(new FeatureVersion(0, 1));
|
|
199
199
|
try {
|
|
200
200
|
// We first collect the subgraphs (creating an empty schema that we'll populate next for each).
|
|
@@ -224,13 +224,13 @@ export function extractSubgraphsFromSupergraph(supergraph: Schema, validateExtra
|
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
const types = filteredTypes(supergraph, joinSpec, coreFeatures.coreDefinition);
|
|
227
|
-
const originalDirectiveNames = getApolloDirectiveNames(supergraph);
|
|
228
227
|
const args: ExtractArguments = {
|
|
229
228
|
supergraph,
|
|
230
229
|
subgraphs,
|
|
231
230
|
joinSpec,
|
|
231
|
+
contextSpec,
|
|
232
|
+
costSpec,
|
|
232
233
|
filteredTypes: types,
|
|
233
|
-
originalDirectiveNames,
|
|
234
234
|
getSubgraph,
|
|
235
235
|
getSubgraphEnumValue,
|
|
236
236
|
};
|
|
@@ -293,8 +293,9 @@ type ExtractArguments = {
|
|
|
293
293
|
supergraph: Schema,
|
|
294
294
|
subgraphs: Subgraphs,
|
|
295
295
|
joinSpec: JoinSpecDefinition,
|
|
296
|
+
contextSpec: ContextSpecDefinition | undefined,
|
|
297
|
+
costSpec: CostSpecDefinition | undefined,
|
|
296
298
|
filteredTypes: NamedType[],
|
|
297
|
-
originalDirectiveNames: Record<string, string>,
|
|
298
299
|
getSubgraph: (application: Directive<any, { graph?: string }>) => Subgraph | undefined,
|
|
299
300
|
getSubgraphEnumValue: (subgraphName: string) => string
|
|
300
301
|
}
|
|
@@ -352,7 +353,9 @@ function addAllEmptySubgraphTypes(args: ExtractArguments): TypesInfo {
|
|
|
352
353
|
const subgraph = getSubgraph(application);
|
|
353
354
|
assert(subgraph, () => `Should have found the subgraph for ${application}`);
|
|
354
355
|
const subgraphType = subgraph.schema.addType(newNamedType(type.kind, type.name));
|
|
355
|
-
|
|
356
|
+
if (args.costSpec) {
|
|
357
|
+
propagateDemandControlDirectives(type, subgraphType, subgraph, args.costSpec);
|
|
358
|
+
}
|
|
356
359
|
}
|
|
357
360
|
break;
|
|
358
361
|
}
|
|
@@ -401,17 +404,8 @@ function addEmptyType<T extends NamedType>(
|
|
|
401
404
|
}
|
|
402
405
|
}
|
|
403
406
|
}
|
|
404
|
-
|
|
405
|
-
const
|
|
406
|
-
assert(coreFeatures, 'Should have core features');
|
|
407
|
-
const contextFeature = coreFeatures.getByIdentity(ContextSpecDefinition.identity);
|
|
408
|
-
let supergraphContextDirective: DirectiveDefinition<{ name: string}> | undefined;
|
|
409
|
-
if (contextFeature) {
|
|
410
|
-
const contextSpec = CONTEXT_VERSIONS.find(contextFeature.url.version);
|
|
411
|
-
assert(contextSpec, 'Should have context spec');
|
|
412
|
-
supergraphContextDirective = contextSpec.contextDirective(supergraph);
|
|
413
|
-
}
|
|
414
|
-
|
|
407
|
+
|
|
408
|
+
const supergraphContextDirective = args.contextSpec?.contextDirective(supergraph);
|
|
415
409
|
if (supergraphContextDirective) {
|
|
416
410
|
const contextApplications = type.appliedDirectivesOf(supergraphContextDirective);
|
|
417
411
|
// for every application, apply the context directive to the correct subgraph
|
|
@@ -438,8 +432,6 @@ function extractObjOrItfContent(args: ExtractArguments, info: TypeInfo<ObjectTyp
|
|
|
438
432
|
const implementsDirective = args.joinSpec.implementsDirective(args.supergraph);
|
|
439
433
|
assert(implementsDirective, '@join__implements should existing for a fed2 supergraph');
|
|
440
434
|
|
|
441
|
-
const originalDirectiveNames = args.originalDirectiveNames;
|
|
442
|
-
|
|
443
435
|
for (const { type, subgraphsInfo } of info) {
|
|
444
436
|
const implementsApplications = type.appliedDirectivesOf(implementsDirective);
|
|
445
437
|
for (const application of implementsApplications) {
|
|
@@ -450,8 +442,10 @@ function extractObjOrItfContent(args: ExtractArguments, info: TypeInfo<ObjectTyp
|
|
|
450
442
|
subgraphInfo.type.addImplementedInterface(args.interface);
|
|
451
443
|
}
|
|
452
444
|
|
|
453
|
-
|
|
454
|
-
|
|
445
|
+
if (args.costSpec) {
|
|
446
|
+
for (const { type: subgraphType, subgraph } of subgraphsInfo.values()) {
|
|
447
|
+
propagateDemandControlDirectives(type, subgraphType, subgraph, args.costSpec);
|
|
448
|
+
}
|
|
455
449
|
}
|
|
456
450
|
|
|
457
451
|
for (const field of type.fields()) {
|
|
@@ -460,7 +454,13 @@ function extractObjOrItfContent(args: ExtractArguments, info: TypeInfo<ObjectTyp
|
|
|
460
454
|
// In fed2 subgraph, no @join__field means that the field is in all the subgraphs in which the type is.
|
|
461
455
|
const isShareable = isObjectType(type) && subgraphsInfo.size > 1;
|
|
462
456
|
for (const { type: subgraphType, subgraph } of subgraphsInfo.values()) {
|
|
463
|
-
addSubgraphField({
|
|
457
|
+
addSubgraphField({
|
|
458
|
+
field,
|
|
459
|
+
type: subgraphType,
|
|
460
|
+
subgraph,
|
|
461
|
+
isShareable,
|
|
462
|
+
costSpec: args.costSpec
|
|
463
|
+
});
|
|
464
464
|
}
|
|
465
465
|
} else {
|
|
466
466
|
const isShareable = isObjectType(type)
|
|
@@ -478,58 +478,21 @@ function extractObjOrItfContent(args: ExtractArguments, info: TypeInfo<ObjectTyp
|
|
|
478
478
|
}
|
|
479
479
|
|
|
480
480
|
const { type: subgraphType, subgraph } = subgraphsInfo.get(joinFieldArgs.graph)!;
|
|
481
|
-
addSubgraphField({
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
/**
|
|
489
|
-
* Builds a map of original name to new name for Apollo feature directives. This is
|
|
490
|
-
* used to handle cases where a directive is renamed via an import statement. For
|
|
491
|
-
* example, importing a directive with a custom name like
|
|
492
|
-
* ```graphql
|
|
493
|
-
* @link(url: "https://specs.apollo.dev/cost/v0.1", import: [{ name: "@cost", as: "@renamedCost" }])
|
|
494
|
-
* ```
|
|
495
|
-
* results in a map entry of `cost -> renamedCost` with the `@` prefix removed.
|
|
496
|
-
*
|
|
497
|
-
* If the directive is imported under its default name, that also results in an entry. So,
|
|
498
|
-
* ```graphql
|
|
499
|
-
* @link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@cost"])
|
|
500
|
-
* ```
|
|
501
|
-
* results in a map entry of `cost -> cost`. This duals as a way to check if a directive
|
|
502
|
-
* is included in the supergraph schema.
|
|
503
|
-
*
|
|
504
|
-
* **Important:** This map does _not_ include directives imported from identities other
|
|
505
|
-
* than `specs.apollo.dev`. This helps us avoid extracting directives to subgraphs
|
|
506
|
-
* when a custom directive's name conflicts with that of a default one.
|
|
507
|
-
*/
|
|
508
|
-
function getApolloDirectiveNames(supergraph: Schema): Record<string, string> {
|
|
509
|
-
const originalDirectiveNames: Record<string, string> = {};
|
|
510
|
-
for (const linkDirective of supergraph.schemaDefinition.appliedDirectivesOf("link")) {
|
|
511
|
-
if (linkDirective.arguments().url && linkDirective.arguments().import) {
|
|
512
|
-
const url = FeatureUrl.maybeParse(linkDirective.arguments().url);
|
|
513
|
-
if (!url?.identity.includes("specs.apollo.dev")) {
|
|
514
|
-
continue;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
for (const importedDirective of linkDirective.arguments().import) {
|
|
518
|
-
if (importedDirective.name && importedDirective.as) {
|
|
519
|
-
originalDirectiveNames[importedDirective.name.replace('@', '')] = importedDirective.as.replace('@', '');
|
|
520
|
-
} else if (typeof importedDirective === 'string') {
|
|
521
|
-
originalDirectiveNames[importedDirective.replace('@', '')] = importedDirective.replace('@', '');
|
|
481
|
+
addSubgraphField({
|
|
482
|
+
field,
|
|
483
|
+
type: subgraphType,
|
|
484
|
+
subgraph, isShareable,
|
|
485
|
+
joinFieldArgs,
|
|
486
|
+
costSpec: args.costSpec
|
|
487
|
+
});
|
|
522
488
|
}
|
|
523
489
|
}
|
|
524
490
|
}
|
|
525
491
|
}
|
|
526
|
-
|
|
527
|
-
return originalDirectiveNames;
|
|
528
492
|
}
|
|
529
493
|
|
|
530
494
|
function extractInputObjContent(args: ExtractArguments, info: TypeInfo<InputObjectType>[]) {
|
|
531
495
|
const fieldDirective = args.joinSpec.fieldDirective(args.supergraph);
|
|
532
|
-
const originalDirectiveNames = args.originalDirectiveNames;
|
|
533
496
|
|
|
534
497
|
for (const { type, subgraphsInfo } of info) {
|
|
535
498
|
for (const field of type.fields()) {
|
|
@@ -537,19 +500,30 @@ function extractInputObjContent(args: ExtractArguments, info: TypeInfo<InputObje
|
|
|
537
500
|
if (fieldApplications.length === 0) {
|
|
538
501
|
// In fed2 subgraph, no @join__field means that the field is in all the subgraphs in which the type is.
|
|
539
502
|
for (const { type: subgraphType, subgraph } of subgraphsInfo.values()) {
|
|
540
|
-
addSubgraphInputField({
|
|
503
|
+
addSubgraphInputField({
|
|
504
|
+
field,
|
|
505
|
+
type: subgraphType,
|
|
506
|
+
subgraph,
|
|
507
|
+
costSpec: args.costSpec
|
|
508
|
+
});
|
|
541
509
|
}
|
|
542
510
|
} else {
|
|
543
511
|
for (const application of fieldApplications) {
|
|
544
|
-
const
|
|
512
|
+
const joinFieldArgs = application.arguments();
|
|
545
513
|
// We use a @join__field with no graph to indicates when a field in the supergraph does not come
|
|
546
514
|
// directly from any subgraph and there is thus nothing to do to "extract" it.
|
|
547
|
-
if (!
|
|
515
|
+
if (!joinFieldArgs.graph) {
|
|
548
516
|
continue;
|
|
549
517
|
}
|
|
550
518
|
|
|
551
|
-
const { type: subgraphType, subgraph } = subgraphsInfo.get(
|
|
552
|
-
addSubgraphInputField({
|
|
519
|
+
const { type: subgraphType, subgraph } = subgraphsInfo.get(joinFieldArgs.graph)!;
|
|
520
|
+
addSubgraphInputField({
|
|
521
|
+
field,
|
|
522
|
+
type: subgraphType,
|
|
523
|
+
subgraph,
|
|
524
|
+
joinFieldArgs,
|
|
525
|
+
costSpec: args.costSpec
|
|
526
|
+
});
|
|
553
527
|
}
|
|
554
528
|
}
|
|
555
529
|
}
|
|
@@ -559,11 +533,12 @@ function extractInputObjContent(args: ExtractArguments, info: TypeInfo<InputObje
|
|
|
559
533
|
function extractEnumTypeContent(args: ExtractArguments, info: TypeInfo<EnumType>[]) {
|
|
560
534
|
// This was added in join 0.3, so it can genuinely be undefined.
|
|
561
535
|
const enumValueDirective = args.joinSpec.enumValueDirective(args.supergraph);
|
|
562
|
-
const originalDirectiveNames = args.originalDirectiveNames;
|
|
563
536
|
|
|
564
537
|
for (const { type, subgraphsInfo } of info) {
|
|
565
|
-
|
|
566
|
-
|
|
538
|
+
if (args.costSpec) {
|
|
539
|
+
for (const { type: subgraphType, subgraph } of subgraphsInfo.values()) {
|
|
540
|
+
propagateDemandControlDirectives(type, subgraphType, subgraph, args.costSpec);
|
|
541
|
+
}
|
|
567
542
|
}
|
|
568
543
|
|
|
569
544
|
for (const value of type.values) {
|
|
@@ -678,20 +653,20 @@ function maybeDumpSubgraphSchema(subgraph: Subgraph): string {
|
|
|
678
653
|
}
|
|
679
654
|
}
|
|
680
655
|
|
|
681
|
-
function propagateDemandControlDirectives(source: SchemaElement<any, any>, dest: SchemaElement<any, any>, subgraph: Subgraph,
|
|
682
|
-
const
|
|
683
|
-
if (
|
|
684
|
-
const
|
|
685
|
-
if (
|
|
686
|
-
dest.applyDirective(subgraph.metadata().costDirective().name,
|
|
656
|
+
function propagateDemandControlDirectives(source: SchemaElement<any, any>, dest: SchemaElement<any, any>, subgraph: Subgraph, costSpec: CostSpecDefinition) {
|
|
657
|
+
const costDirective = costSpec.costDirective(source.schema());
|
|
658
|
+
if (costDirective) {
|
|
659
|
+
const application = source.appliedDirectivesOf(costDirective)[0];
|
|
660
|
+
if (application) {
|
|
661
|
+
dest.applyDirective(subgraph.metadata().costDirective().name, application.arguments());
|
|
687
662
|
}
|
|
688
663
|
}
|
|
689
664
|
|
|
690
|
-
const
|
|
691
|
-
if (
|
|
692
|
-
const
|
|
693
|
-
if (
|
|
694
|
-
dest.applyDirective(subgraph.metadata().listSizeDirective().name,
|
|
665
|
+
const listSizeDirective = costSpec.listSizeDirective(source.schema());
|
|
666
|
+
if (listSizeDirective) {
|
|
667
|
+
const application = source.appliedDirectivesOf(listSizeDirective)[0];
|
|
668
|
+
if (application) {
|
|
669
|
+
dest.applyDirective(subgraph.metadata().listSizeDirective().name, application.arguments());
|
|
695
670
|
}
|
|
696
671
|
}
|
|
697
672
|
}
|
|
@@ -707,14 +682,14 @@ function addSubgraphField({
|
|
|
707
682
|
subgraph,
|
|
708
683
|
isShareable,
|
|
709
684
|
joinFieldArgs,
|
|
710
|
-
|
|
685
|
+
costSpec,
|
|
711
686
|
}: {
|
|
712
687
|
field: FieldDefinition<ObjectType | InterfaceType>,
|
|
713
688
|
type: ObjectType | InterfaceType,
|
|
714
689
|
subgraph: Subgraph,
|
|
715
690
|
isShareable: boolean,
|
|
716
691
|
joinFieldArgs?: JoinFieldDirectiveArguments,
|
|
717
|
-
|
|
692
|
+
costSpec?: CostSpecDefinition,
|
|
718
693
|
}): FieldDefinition<ObjectType | InterfaceType> {
|
|
719
694
|
const copiedFieldType = joinFieldArgs?.type
|
|
720
695
|
? decodeType(joinFieldArgs.type, subgraph.schema, subgraph.name)
|
|
@@ -723,7 +698,9 @@ function addSubgraphField({
|
|
|
723
698
|
const subgraphField = type.addField(field.name, copiedFieldType);
|
|
724
699
|
for (const arg of field.arguments()) {
|
|
725
700
|
const argDef = subgraphField.addArgument(arg.name, copyType(arg.type!, subgraph.schema, subgraph.name), arg.defaultValue);
|
|
726
|
-
|
|
701
|
+
if (costSpec) {
|
|
702
|
+
propagateDemandControlDirectives(arg, argDef, subgraph, costSpec);
|
|
703
|
+
}
|
|
727
704
|
}
|
|
728
705
|
if (joinFieldArgs?.requires) {
|
|
729
706
|
subgraphField.applyDirective(subgraph.metadata().requiresDirective(), {'fields': joinFieldArgs.requires});
|
|
@@ -769,7 +746,9 @@ function addSubgraphField({
|
|
|
769
746
|
subgraphField.applyDirective(subgraph.metadata().shareableDirective());
|
|
770
747
|
}
|
|
771
748
|
|
|
772
|
-
|
|
749
|
+
if (costSpec) {
|
|
750
|
+
propagateDemandControlDirectives(field, subgraphField, subgraph, costSpec);
|
|
751
|
+
}
|
|
773
752
|
|
|
774
753
|
return subgraphField;
|
|
775
754
|
}
|
|
@@ -779,13 +758,13 @@ function addSubgraphInputField({
|
|
|
779
758
|
type,
|
|
780
759
|
subgraph,
|
|
781
760
|
joinFieldArgs,
|
|
782
|
-
|
|
761
|
+
costSpec,
|
|
783
762
|
}: {
|
|
784
763
|
field: InputFieldDefinition,
|
|
785
764
|
type: InputObjectType,
|
|
786
765
|
subgraph: Subgraph,
|
|
787
766
|
joinFieldArgs?: JoinFieldDirectiveArguments,
|
|
788
|
-
|
|
767
|
+
costSpec?: CostSpecDefinition,
|
|
789
768
|
}): InputFieldDefinition {
|
|
790
769
|
const copiedType = joinFieldArgs?.type
|
|
791
770
|
? decodeType(joinFieldArgs?.type, subgraph.schema, subgraph.name)
|
|
@@ -794,7 +773,9 @@ function addSubgraphInputField({
|
|
|
794
773
|
const inputField = type.addField(field.name, copiedType);
|
|
795
774
|
inputField.defaultValue = field.defaultValue
|
|
796
775
|
|
|
797
|
-
|
|
776
|
+
if (costSpec) {
|
|
777
|
+
propagateDemandControlDirectives(field, inputField, subgraph, costSpec);
|
|
778
|
+
}
|
|
798
779
|
|
|
799
780
|
return inputField;
|
|
800
781
|
}
|
package/src/federation.ts
CHANGED
|
@@ -36,6 +36,8 @@ import {
|
|
|
36
36
|
isListType,
|
|
37
37
|
isWrapperType,
|
|
38
38
|
possibleRuntimeTypes,
|
|
39
|
+
isIntType,
|
|
40
|
+
Type,
|
|
39
41
|
} from "./definitions";
|
|
40
42
|
import { assert, MultiMap, printHumanReadableList, OrderedMap, mapValues, assertUnreachable } from "./utils";
|
|
41
43
|
import { SDLValidationRule } from "graphql/validation/ValidationContext";
|
|
@@ -100,7 +102,7 @@ import {
|
|
|
100
102
|
SourceFieldDirectiveArgs,
|
|
101
103
|
SourceTypeDirectiveArgs,
|
|
102
104
|
} from "./specs/sourceSpec";
|
|
103
|
-
import { CostDirectiveArguments, ListSizeDirectiveArguments } from "./specs/costSpec";
|
|
105
|
+
import { COST_VERSIONS, CostDirectiveArguments, ListSizeDirectiveArguments, costIdentity } from "./specs/costSpec";
|
|
104
106
|
|
|
105
107
|
const linkSpec = LINK_VERSIONS.latest();
|
|
106
108
|
const tagSpec = TAG_VERSIONS.latest();
|
|
@@ -1055,6 +1057,123 @@ function validateShareableNotRepeatedOnSameDeclaration(
|
|
|
1055
1057
|
}
|
|
1056
1058
|
}
|
|
1057
1059
|
}
|
|
1060
|
+
|
|
1061
|
+
function validateCostNotAppliedToInterface(application: Directive<SchemaElement<any, any>, CostDirectiveArguments>, errorCollector: GraphQLError[]) {
|
|
1062
|
+
const parent = application.parent;
|
|
1063
|
+
// @cost cannot be used on interfaces https://ibm.github.io/graphql-specs/cost-spec.html#sec-No-Cost-on-Interface-Fields
|
|
1064
|
+
if (parent instanceof FieldDefinition && parent.parent instanceof InterfaceType) {
|
|
1065
|
+
errorCollector.push(ERRORS.COST_APPLIED_TO_INTERFACE_FIELD.err(
|
|
1066
|
+
`@cost cannot be applied to interface "${parent.coordinate}"`,
|
|
1067
|
+
{ nodes: sourceASTs(application, parent) }
|
|
1068
|
+
));
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function validateListSizeAppliedToList(
|
|
1073
|
+
application: Directive<SchemaElement<any, any>, ListSizeDirectiveArguments>,
|
|
1074
|
+
parent: FieldDefinition<CompositeType>,
|
|
1075
|
+
errorCollector: GraphQLError[],
|
|
1076
|
+
) {
|
|
1077
|
+
const { sizedFields = [] } = application.arguments();
|
|
1078
|
+
// @listSize must be applied to a list https://ibm.github.io/graphql-specs/cost-spec.html#sec-Valid-List-Size-Target
|
|
1079
|
+
if (!sizedFields.length && parent.type && !isListType(parent.type)) {
|
|
1080
|
+
errorCollector.push(ERRORS.LIST_SIZE_APPLIED_TO_NON_LIST.err(
|
|
1081
|
+
`"${parent.coordinate}" is not a list`,
|
|
1082
|
+
{ nodes: sourceASTs(application, parent) },
|
|
1083
|
+
));
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function validateAssumedSizeNotNegative(
|
|
1088
|
+
application: Directive<SchemaElement<any, any>, ListSizeDirectiveArguments>,
|
|
1089
|
+
parent: FieldDefinition<CompositeType>,
|
|
1090
|
+
errorCollector: GraphQLError[]
|
|
1091
|
+
) {
|
|
1092
|
+
const { assumedSize } = application.arguments();
|
|
1093
|
+
// Validate assumed size, but we differ from https://ibm.github.io/graphql-specs/cost-spec.html#sec-Valid-Assumed-Size.
|
|
1094
|
+
// Assumed size is used as a backup for slicing arguments in the event they are both specified.
|
|
1095
|
+
// The spec aims to rule out cases when the assumed size will never be used because there is always
|
|
1096
|
+
// a slicing argument. Two applications which are compliant with that validation rule can be merged
|
|
1097
|
+
// into an application which is not compliant, thus we need to handle this case gracefully at runtime regardless.
|
|
1098
|
+
// We omit this check to keep the validations to those that will otherwise cause runtime failures.
|
|
1099
|
+
//
|
|
1100
|
+
// With all that said, assumed size should not be negative.
|
|
1101
|
+
if (assumedSize !== undefined && assumedSize !== null && assumedSize < 0) {
|
|
1102
|
+
errorCollector.push(ERRORS.LIST_SIZE_INVALID_ASSUMED_SIZE.err(
|
|
1103
|
+
`Assumed size of "${parent.coordinate}" cannot be negative`,
|
|
1104
|
+
{ nodes: sourceASTs(application, parent) },
|
|
1105
|
+
));
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function isNonNullIntType(ty: Type): boolean {
|
|
1110
|
+
return isNonNullType(ty) && isIntType(ty.ofType)
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function validateSlicingArgumentsAreValidIntegers(
|
|
1114
|
+
application: Directive<SchemaElement<any, any>, ListSizeDirectiveArguments>,
|
|
1115
|
+
parent: FieldDefinition<CompositeType>,
|
|
1116
|
+
errorCollector: GraphQLError[]
|
|
1117
|
+
) {
|
|
1118
|
+
const { slicingArguments = [] } = application.arguments();
|
|
1119
|
+
// Validate slicingArguments https://ibm.github.io/graphql-specs/cost-spec.html#sec-Valid-Slicing-Arguments-Target
|
|
1120
|
+
for (const slicingArgumentName of slicingArguments) {
|
|
1121
|
+
const slicingArgument = parent.argument(slicingArgumentName);
|
|
1122
|
+
if (!slicingArgument?.type) {
|
|
1123
|
+
// Slicing arguments must be one of the field's arguments
|
|
1124
|
+
errorCollector.push(ERRORS.LIST_SIZE_INVALID_SLICING_ARGUMENT.err(
|
|
1125
|
+
`Slicing argument "${slicingArgumentName}" is not an argument of "${parent.coordinate}"`,
|
|
1126
|
+
{ nodes: sourceASTs(application, parent) }
|
|
1127
|
+
));
|
|
1128
|
+
} else if (!isIntType(slicingArgument.type) && !isNonNullIntType(slicingArgument.type)) {
|
|
1129
|
+
// Slicing arguments must be Int or Int!
|
|
1130
|
+
errorCollector.push(ERRORS.LIST_SIZE_INVALID_SLICING_ARGUMENT.err(
|
|
1131
|
+
`Slicing argument "${slicingArgument.coordinate}" must be Int or Int!`,
|
|
1132
|
+
{ nodes: sourceASTs(application, parent) }
|
|
1133
|
+
));
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function isNonNullListType(ty: Type): boolean {
|
|
1139
|
+
return isNonNullType(ty) && isListType(ty.ofType)
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function validateSizedFieldsAreValidLists(
|
|
1143
|
+
application: Directive<SchemaElement<any, any>, ListSizeDirectiveArguments>,
|
|
1144
|
+
parent: FieldDefinition<CompositeType>,
|
|
1145
|
+
errorCollector: GraphQLError[]
|
|
1146
|
+
) {
|
|
1147
|
+
const { sizedFields = [] } = application.arguments();
|
|
1148
|
+
// Validate sizedFields https://ibm.github.io/graphql-specs/cost-spec.html#sec-Valid-Sized-Fields-Target
|
|
1149
|
+
if (sizedFields.length) {
|
|
1150
|
+
if (!parent.type || !isCompositeType(parent.type)) {
|
|
1151
|
+
// The output type must have fields
|
|
1152
|
+
errorCollector.push(ERRORS.LIST_SIZE_INVALID_SIZED_FIELD.err(
|
|
1153
|
+
`Sized fields cannot be used because "${parent.type}" is not a composite type`,
|
|
1154
|
+
{ nodes: sourceASTs(application, parent)}
|
|
1155
|
+
));
|
|
1156
|
+
} else {
|
|
1157
|
+
for (const sizedFieldName of sizedFields) {
|
|
1158
|
+
const sizedField = parent.type.field(sizedFieldName);
|
|
1159
|
+
if (!sizedField) {
|
|
1160
|
+
// Sized fields must be present on the output type
|
|
1161
|
+
errorCollector.push(ERRORS.LIST_SIZE_INVALID_SIZED_FIELD.err(
|
|
1162
|
+
`Sized field "${sizedFieldName}" is not a field on type "${parent.type.coordinate}"`,
|
|
1163
|
+
{ nodes: sourceASTs(application, parent) }
|
|
1164
|
+
));
|
|
1165
|
+
} else if (!sizedField.type || !(isListType(sizedField.type) || isNonNullListType(sizedField.type))) {
|
|
1166
|
+
// Sized fields must be lists
|
|
1167
|
+
errorCollector.push(ERRORS.LIST_SIZE_APPLIED_TO_NON_LIST.err(
|
|
1168
|
+
`Sized field "${sizedField.coordinate}" is not a list`,
|
|
1169
|
+
{ nodes: sourceASTs(application, parent) },
|
|
1170
|
+
));
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1058
1177
|
export class FederationMetadata {
|
|
1059
1178
|
private _externalTester?: ExternalTester;
|
|
1060
1179
|
private _sharingPredicate?: (field: FieldDefinition<CompositeType>) => boolean;
|
|
@@ -1736,6 +1855,26 @@ export class FederationBlueprint extends SchemaBlueprint {
|
|
|
1736
1855
|
}
|
|
1737
1856
|
}
|
|
1738
1857
|
|
|
1858
|
+
const costFeature = schema.coreFeatures?.getByIdentity(costIdentity);
|
|
1859
|
+
const costSpec = costFeature && COST_VERSIONS.find(costFeature.url.version);
|
|
1860
|
+
const costDirective = costSpec?.costDirective(schema);
|
|
1861
|
+
const listSizeDirective = costSpec?.listSizeDirective(schema);
|
|
1862
|
+
|
|
1863
|
+
// Validate @cost
|
|
1864
|
+
for (const application of costDirective?.applications() ?? []) {
|
|
1865
|
+
validateCostNotAppliedToInterface(application, errorCollector);
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
// Validate @listSize
|
|
1869
|
+
for (const application of listSizeDirective?.applications() ?? []) {
|
|
1870
|
+
const parent = application.parent;
|
|
1871
|
+
assert(parent instanceof FieldDefinition, "@listSize can only be applied to FIELD_DEFINITION");
|
|
1872
|
+
validateListSizeAppliedToList(application, parent, errorCollector);
|
|
1873
|
+
validateAssumedSizeNotNegative(application, parent, errorCollector);
|
|
1874
|
+
validateSlicingArgumentsAreValidIntegers(application, parent, errorCollector);
|
|
1875
|
+
validateSizedFieldsAreValidLists(application, parent, errorCollector);
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1739
1878
|
return errorCollector;
|
|
1740
1879
|
}
|
|
1741
1880
|
|