@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.
Files changed (47) hide show
  1. package/dist/argumentCompositionStrategies.d.ts +8 -14
  2. package/dist/argumentCompositionStrategies.d.ts.map +1 -1
  3. package/dist/argumentCompositionStrategies.js +48 -48
  4. package/dist/argumentCompositionStrategies.js.map +1 -1
  5. package/dist/definitions.d.ts +1 -0
  6. package/dist/definitions.d.ts.map +1 -1
  7. package/dist/definitions.js +12 -10
  8. package/dist/definitions.js.map +1 -1
  9. package/dist/error.d.ts +5 -0
  10. package/dist/error.d.ts.map +1 -1
  11. package/dist/error.js +10 -0
  12. package/dist/error.js.map +1 -1
  13. package/dist/extractSubgraphsFromSupergraph.js +68 -63
  14. package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
  15. package/dist/federation.d.ts.map +1 -1
  16. package/dist/federation.js +72 -1
  17. package/dist/federation.js.map +1 -1
  18. package/dist/operations.d.ts +6 -4
  19. package/dist/operations.d.ts.map +1 -1
  20. package/dist/operations.js +28 -18
  21. package/dist/operations.js.map +1 -1
  22. package/dist/specs/coreSpec.d.ts +2 -2
  23. package/dist/specs/coreSpec.d.ts.map +1 -1
  24. package/dist/specs/coreSpec.js +15 -20
  25. package/dist/specs/coreSpec.js.map +1 -1
  26. package/dist/specs/costSpec.d.ts +3 -0
  27. package/dist/specs/costSpec.d.ts.map +1 -1
  28. package/dist/specs/costSpec.js +6 -0
  29. package/dist/specs/costSpec.js.map +1 -1
  30. package/dist/supergraphs.d.ts +8 -1
  31. package/dist/supergraphs.d.ts.map +1 -1
  32. package/dist/supergraphs.js +20 -2
  33. package/dist/supergraphs.js.map +1 -1
  34. package/dist/values.d.ts.map +1 -1
  35. package/dist/values.js +14 -5
  36. package/dist/values.js.map +1 -1
  37. package/package.json +1 -1
  38. package/src/argumentCompositionStrategies.ts +68 -50
  39. package/src/definitions.ts +25 -9
  40. package/src/error.ts +36 -0
  41. package/src/extractSubgraphsFromSupergraph.ts +73 -92
  42. package/src/federation.ts +140 -1
  43. package/src/operations.ts +36 -19
  44. package/src/specs/coreSpec.ts +27 -26
  45. package/src/specs/costSpec.ts +9 -1
  46. package/src/supergraphs.ts +31 -3
  47. package/src/values.ts +13 -5
@@ -984,12 +984,28 @@ export class CoreFeature {
984
984
  }
985
985
 
986
986
  directiveNameInSchema(name: string): string {
987
- const elementImport = this.imports.find((i) => i.name.charAt(0) === '@' && i.name.slice(1) === name);
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) ?? name)
990
- : (name === this.url.name
991
- ? this.nameInSchema
992
- : this.nameInSchema + '__' + name
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[1],
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: directFeature.imports.find(imp => imp.as === `@${element.name}`)?.name.slice(1) ?? element.name,
1092
- isImported: true,
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 { CONTEXT_VERSIONS, ContextSpecDefinition, DirectiveDefinition, FeatureUrl, FederationDirectiveName, SchemaElement, errorCauses, isFederationDirectiveDefinedInSchema, printErrors } from ".";
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
- propagateDemandControlDirectives(type, subgraphType, subgraph, args.originalDirectiveNames);
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 coreFeatures = supergraph.coreFeatures;
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
- for (const { type: subgraphType, subgraph } of subgraphsInfo.values()) {
454
- propagateDemandControlDirectives(type, subgraphType, subgraph, args.originalDirectiveNames);
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({ field, type: subgraphType, subgraph, isShareable, originalDirectiveNames });
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({ field, type: subgraphType, subgraph, isShareable, joinFieldArgs, originalDirectiveNames });
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({ field, type: subgraphType, subgraph, originalDirectiveNames });
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 args = application.arguments();
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 (!args.graph) {
515
+ if (!joinFieldArgs.graph) {
548
516
  continue;
549
517
  }
550
518
 
551
- const { type: subgraphType, subgraph } = subgraphsInfo.get(args.graph)!;
552
- addSubgraphInputField({ field, type: subgraphType, subgraph, joinFieldArgs: args, originalDirectiveNames });
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
- for (const { type: subgraphType, subgraph } of subgraphsInfo.values()) {
566
- propagateDemandControlDirectives(type, subgraphType, subgraph, originalDirectiveNames);
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, originalDirectiveNames?: Record<string, string>) {
682
- const costDirectiveName = originalDirectiveNames?.[FederationDirectiveName.COST];
683
- if (costDirectiveName) {
684
- const costDirective = source.appliedDirectivesOf(costDirectiveName).pop();
685
- if (costDirective) {
686
- dest.applyDirective(subgraph.metadata().costDirective().name, costDirective.arguments());
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 listSizeDirectiveName = originalDirectiveNames?.[FederationDirectiveName.LIST_SIZE];
691
- if (listSizeDirectiveName) {
692
- const listSizeDirective = source.appliedDirectivesOf(listSizeDirectiveName).pop();
693
- if (listSizeDirective) {
694
- dest.applyDirective(subgraph.metadata().listSizeDirective().name, listSizeDirective.arguments());
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
- originalDirectiveNames,
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
- originalDirectiveNames?: Record<string, string>,
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
- propagateDemandControlDirectives(arg, argDef, subgraph, originalDirectiveNames)
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
- propagateDemandControlDirectives(field, subgraphField, subgraph, originalDirectiveNames);
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
- originalDirectiveNames,
761
+ costSpec,
783
762
  }: {
784
763
  field: InputFieldDefinition,
785
764
  type: InputObjectType,
786
765
  subgraph: Subgraph,
787
766
  joinFieldArgs?: JoinFieldDirectiveArguments,
788
- originalDirectiveNames?: Record<string, string>
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
- propagateDemandControlDirectives(field, inputField, subgraph, originalDirectiveNames);
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