@apollo/federation-internals 2.7.0 → 2.7.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.
@@ -1,4 +1,4 @@
1
- import { DirectiveLocation, GraphQLError, assertName } from 'graphql';
1
+ import { DirectiveLocation, GraphQLError, Kind } from 'graphql';
2
2
  import { FeatureDefinition, FeatureDefinitions, FeatureUrl, FeatureVersion, LinkDirectiveArgs } from "./coreSpec";
3
3
  import {
4
4
  Schema,
@@ -16,7 +16,7 @@ import { ERRORS } from '../error';
16
16
  export const sourceIdentity = 'https://specs.apollo.dev/source';
17
17
 
18
18
  export class SourceSpecDefinition extends FeatureDefinition {
19
- constructor(version: FeatureVersion, minimumFederationVersion?: FeatureVersion) {
19
+ constructor(version: FeatureVersion, readonly minimumFederationVersion: FeatureVersion) {
20
20
  super(new FeatureUrl(sourceIdentity, 'source', version), minimumFederationVersion);
21
21
 
22
22
  this.registerDirective(createDirectiveSpecification({
@@ -157,7 +157,8 @@ export class SourceSpecDefinition extends FeatureDefinition {
157
157
  schema.schemaDefinition.appliedDirectivesOf<LinkDirectiveArgs>('link')
158
158
  .forEach(linkDirective => {
159
159
  const { url, import: imports } = linkDirective.arguments();
160
- if (imports && FeatureUrl.maybeParse(url)?.identity === sourceIdentity) {
160
+ const featureUrl = FeatureUrl.maybeParse(url);
161
+ if (imports && featureUrl && featureUrl.identity === sourceIdentity) {
161
162
  imports.forEach(nameOrRename => {
162
163
  const originalName = typeof nameOrRename === 'string' ? nameOrRename : nameOrRename.name;
163
164
  const importedName = typeof nameOrRename === 'string' ? nameOrRename : nameOrRename.as || originalName;
@@ -178,6 +179,7 @@ export class SourceSpecDefinition extends FeatureDefinition {
178
179
  }
179
180
 
180
181
  override validateSubgraphSchema(schema: Schema): GraphQLError[] {
182
+ const errors = super.validateSubgraphSchema(schema);
181
183
  const {
182
184
  sourceAPI,
183
185
  sourceType,
@@ -191,7 +193,6 @@ export class SourceSpecDefinition extends FeatureDefinition {
191
193
  }
192
194
 
193
195
  const apiNameToProtocol = new Map<string, ProtocolName>();
194
- const errors: GraphQLError[] = [];
195
196
 
196
197
  if (sourceAPI) {
197
198
  this.validateSourceAPI(sourceAPI, apiNameToProtocol, errors);
@@ -216,20 +217,18 @@ export class SourceSpecDefinition extends FeatureDefinition {
216
217
  sourceAPI.applications().forEach(application => {
217
218
  const { name, ...rest } = application.arguments();
218
219
 
219
- if (apiNameToProtocol.has(name)) {
220
+ if (!isValidSourceAPIName(name)) {
220
221
  errors.push(ERRORS.SOURCE_API_NAME_INVALID.err(
221
- `${sourceAPI} must specify unique name`,
222
+ `${sourceAPI}(name: ${
223
+ JSON.stringify(name)
224
+ }) must specify name using only [a-zA-Z0-9-_] characters`,
222
225
  { nodes: application.sourceAST },
223
226
  ));
224
227
  }
225
228
 
226
- try {
227
- assertName(name);
228
- } catch (e) {
229
+ if (apiNameToProtocol.has(name)) {
229
230
  errors.push(ERRORS.SOURCE_API_NAME_INVALID.err(
230
- `${sourceAPI}(name: ${
231
- JSON.stringify(name)
232
- }) must specify valid GraphQL name`,
231
+ `${sourceAPI} must specify unique name (${JSON.stringify(name)} reused)`,
233
232
  { nodes: application.sourceAST },
234
233
  ));
235
234
  }
@@ -239,7 +238,9 @@ export class SourceSpecDefinition extends FeatureDefinition {
239
238
  if (rest[knownProtocol]) {
240
239
  if (protocol) {
241
240
  errors.push(ERRORS.SOURCE_API_PROTOCOL_INVALID.err(
242
- `${sourceAPI} must specify only one of ${KNOWN_SOURCE_PROTOCOLS.join(', ')}`,
241
+ `${sourceAPI} must specify only one of ${
242
+ KNOWN_SOURCE_PROTOCOLS.join(', ')
243
+ } but specified both ${protocol} and ${knownProtocol}`,
243
244
  { nodes: application.sourceAST },
244
245
  ));
245
246
  }
@@ -258,7 +259,7 @@ export class SourceSpecDefinition extends FeatureDefinition {
258
259
  new URL(baseURL);
259
260
  } catch (e) {
260
261
  errors.push(ERRORS.SOURCE_API_HTTP_BASE_URL_INVALID.err(
261
- `${sourceAPI} http.baseURL ${JSON.stringify(baseURL)} must be valid URL`,
262
+ `${sourceAPI} http.baseURL ${JSON.stringify(baseURL)} must be valid URL (error: ${e.message})`,
262
263
  { nodes: application.sourceAST },
263
264
  ));
264
265
  }
@@ -267,7 +268,7 @@ export class SourceSpecDefinition extends FeatureDefinition {
267
268
  }
268
269
  } else {
269
270
  errors.push(ERRORS.SOURCE_API_PROTOCOL_INVALID.err(
270
- `${sourceAPI} must specify one of ${KNOWN_SOURCE_PROTOCOLS.join(', ')}`,
271
+ `${sourceAPI} must specify one protocol from the set {${KNOWN_SOURCE_PROTOCOLS.join(',')}}`,
271
272
  { nodes: application.sourceAST },
272
273
  ));
273
274
  }
@@ -286,58 +287,58 @@ export class SourceSpecDefinition extends FeatureDefinition {
286
287
  `${sourceType} specifies unknown api ${api}`,
287
288
  { nodes: application.sourceAST },
288
289
  ));
289
- } else {
290
- const expectedProtocol = apiNameToProtocol.get(api);
291
- const protocolValue = expectedProtocol && rest[expectedProtocol];
292
- if (expectedProtocol && !protocolValue) {
293
- errors.push(ERRORS.SOURCE_TYPE_API_ERROR.err(
294
- `${sourceType} must specify same ${
295
- expectedProtocol
296
- } argument as corresponding @sourceAPI for api ${api}`,
290
+ }
291
+
292
+ const expectedProtocol = apiNameToProtocol.get(api) || HTTP_PROTOCOL;
293
+ const protocolValue = expectedProtocol && rest[expectedProtocol];
294
+ if (expectedProtocol && !protocolValue) {
295
+ errors.push(ERRORS.SOURCE_TYPE_PROTOCOL_INVALID.err(
296
+ `${sourceType} must specify same ${
297
+ expectedProtocol
298
+ } argument as corresponding @sourceAPI for api ${api}`,
299
+ { nodes: application.sourceAST },
300
+ ));
301
+ }
302
+
303
+ if (protocolValue && expectedProtocol === HTTP_PROTOCOL) {
304
+ const { GET, POST, headers, body } = protocolValue as HTTPSourceType;
305
+
306
+ if ([GET, POST].filter(Boolean).length !== 1) {
307
+ errors.push(ERRORS.SOURCE_TYPE_HTTP_METHOD_INVALID.err(
308
+ `${sourceType} must specify exactly one of http.GET or http.POST`,
297
309
  { nodes: application.sourceAST },
298
310
  ));
311
+ } else {
312
+ const urlPathTemplate = (GET || POST)!;
313
+ try {
314
+ // TODO Validate URL path template uses only available @key fields
315
+ // of the type.
316
+ parseURLPathTemplate(urlPathTemplate);
317
+ } catch (e) {
318
+ errors.push(ERRORS.SOURCE_TYPE_HTTP_PATH_INVALID.err(
319
+ `${sourceType} http.GET or http.POST must be valid URL path template (error: ${e.message})`
320
+ ));
321
+ }
299
322
  }
300
323
 
301
- if (protocolValue && expectedProtocol === HTTP_PROTOCOL) {
302
- const { GET, POST, headers, body } = protocolValue as HTTPSourceType;
324
+ validateHTTPHeaders(headers, errors, sourceType.name);
303
325
 
304
- if ([GET, POST].filter(Boolean).length !== 1) {
305
- errors.push(ERRORS.SOURCE_TYPE_HTTP_METHOD_INVALID.err(
306
- `${sourceType} must specify exactly one of http.GET or http.POST`,
326
+ if (body) {
327
+ if (GET) {
328
+ errors.push(ERRORS.SOURCE_TYPE_HTTP_BODY_INVALID.err(
329
+ `${sourceType} http.GET cannot specify http.body`,
307
330
  { nodes: application.sourceAST },
308
331
  ));
309
- } else {
310
- const urlPathTemplate = (GET || POST)!;
311
- try {
312
- // TODO Validate URL path template uses only available @key fields
313
- // of the type.
314
- parseURLPathTemplate(urlPathTemplate);
315
- } catch (e) {
316
- errors.push(ERRORS.SOURCE_TYPE_HTTP_PATH_INVALID.err(
317
- `${sourceType} http.GET or http.POST must be valid URL path template`
318
- ));
319
- }
320
332
  }
321
333
 
322
- validateHTTPHeaders(headers, errors, sourceType.name);
323
-
324
- if (body) {
325
- if (GET) {
326
- errors.push(ERRORS.SOURCE_TYPE_HTTP_BODY_INVALID.err(
327
- `${sourceType} http.GET cannot specify http.body`,
328
- { nodes: application.sourceAST },
329
- ));
330
- }
331
-
332
- try {
333
- parseJSONSelection(body);
334
- // TODO Validate body selection matches the available fields.
335
- } catch (e) {
336
- errors.push(ERRORS.SOURCE_TYPE_HTTP_BODY_INVALID.err(
337
- `${sourceType} http.body not valid JSONSelection: ${e.message}`,
338
- { nodes: application.sourceAST },
339
- ));
340
- }
334
+ try {
335
+ parseJSONSelection(body);
336
+ // TODO Validate body selection matches the available fields.
337
+ } catch (e) {
338
+ errors.push(ERRORS.SOURCE_TYPE_HTTP_BODY_INVALID.err(
339
+ `${sourceType} http.body not valid JSONSelection (error: ${e.message})`,
340
+ { nodes: application.sourceAST },
341
+ ));
341
342
  }
342
343
  }
343
344
  }
@@ -357,7 +358,7 @@ export class SourceSpecDefinition extends FeatureDefinition {
357
358
  // TODO Validate selection is valid JSONSelection for type.
358
359
  } catch (e) {
359
360
  errors.push(ERRORS.SOURCE_TYPE_SELECTION_INVALID.err(
360
- `${sourceType} selection not valid JSONSelection: ${e.message}`,
361
+ `${sourceType} selection not valid JSONSelection (error: ${e.message})`,
361
362
  { nodes: application.sourceAST },
362
363
  ));
363
364
  }
@@ -383,59 +384,59 @@ export class SourceSpecDefinition extends FeatureDefinition {
383
384
  `${sourceField} specifies unknown api ${api}`,
384
385
  { nodes: application.sourceAST },
385
386
  ));
386
- } else {
387
- const expectedProtocol = apiNameToProtocol.get(api);
388
- const protocolValue = expectedProtocol && rest[expectedProtocol];
389
- if (protocolValue && expectedProtocol === HTTP_PROTOCOL) {
390
- const {
391
- GET, POST, PUT, PATCH, DELETE,
392
- headers,
393
- body,
394
- } = protocolValue as HTTPSourceField;
395
-
396
- const usedMethods = [GET, POST, PUT, PATCH, DELETE].filter(Boolean);
397
- if (usedMethods.length > 1) {
398
- errors.push(ERRORS.SOURCE_FIELD_HTTP_METHOD_INVALID.err(
399
- `${sourceField} allows at most one of http.{GET,POST,PUT,PATCH,DELETE}`,
387
+ }
388
+
389
+ const expectedProtocol = apiNameToProtocol.get(api) || HTTP_PROTOCOL;
390
+ const protocolValue = expectedProtocol && rest[expectedProtocol];
391
+ if (protocolValue && expectedProtocol === HTTP_PROTOCOL) {
392
+ const {
393
+ GET, POST, PUT, PATCH, DELETE,
394
+ headers,
395
+ body,
396
+ } = protocolValue as HTTPSourceField;
397
+
398
+ const usedMethods = [GET, POST, PUT, PATCH, DELETE].filter(Boolean);
399
+ if (usedMethods.length > 1) {
400
+ errors.push(ERRORS.SOURCE_FIELD_HTTP_METHOD_INVALID.err(
401
+ `${sourceField} allows at most one of http.{GET,POST,PUT,PATCH,DELETE}`,
402
+ ));
403
+ } else if (usedMethods.length === 1) {
404
+ const urlPathTemplate = usedMethods[0]!;
405
+ try {
406
+ // TODO Validate URL path template uses only available fields of
407
+ // the type and/or argument names of the field.
408
+ parseURLPathTemplate(urlPathTemplate);
409
+ } catch (e) {
410
+ errors.push(ERRORS.SOURCE_FIELD_HTTP_PATH_INVALID.err(
411
+ `${sourceField} http.{GET,POST,PUT,PATCH,DELETE} must be valid URL path template (error: ${e.message})`
400
412
  ));
401
- } else if (usedMethods.length === 1) {
402
- const urlPathTemplate = usedMethods[0]!;
403
- try {
404
- // TODO Validate URL path template uses only available fields of
405
- // the type and/or argument names of the field.
406
- parseURLPathTemplate(urlPathTemplate);
407
- } catch (e) {
408
- errors.push(ERRORS.SOURCE_FIELD_HTTP_PATH_INVALID.err(
409
- `${sourceField} http.{GET,POST,PUT,PATCH,DELETE} must be valid URL path template`
410
- ));
411
- }
412
413
  }
414
+ }
413
415
 
414
- validateHTTPHeaders(headers, errors, sourceField.name);
415
-
416
- if (body) {
417
- if (GET) {
418
- errors.push(ERRORS.SOURCE_FIELD_HTTP_BODY_INVALID.err(
419
- `${sourceField} http.GET cannot specify http.body`,
420
- { nodes: application.sourceAST },
421
- ));
422
- } else if (DELETE) {
423
- errors.push(ERRORS.SOURCE_FIELD_HTTP_BODY_INVALID.err(
424
- `${sourceField} http.DELETE cannot specify http.body`,
425
- { nodes: application.sourceAST },
426
- ));
427
- }
416
+ validateHTTPHeaders(headers, errors, sourceField.name);
428
417
 
429
- try {
430
- parseJSONSelection(body);
431
- // TODO Validate body string matches the available fields of the
432
- // parent type and/or argument names of the field.
433
- } catch (e) {
434
- errors.push(ERRORS.SOURCE_FIELD_HTTP_BODY_INVALID.err(
435
- `${sourceField} http.body not valid JSONSelection: ${e.message}`,
436
- { nodes: application.sourceAST },
437
- ));
438
- }
418
+ if (body) {
419
+ if (GET) {
420
+ errors.push(ERRORS.SOURCE_FIELD_HTTP_BODY_INVALID.err(
421
+ `${sourceField} http.GET cannot specify http.body`,
422
+ { nodes: application.sourceAST },
423
+ ));
424
+ } else if (DELETE) {
425
+ errors.push(ERRORS.SOURCE_FIELD_HTTP_BODY_INVALID.err(
426
+ `${sourceField} http.DELETE cannot specify http.body`,
427
+ { nodes: application.sourceAST },
428
+ ));
429
+ }
430
+
431
+ try {
432
+ parseJSONSelection(body);
433
+ // TODO Validate body string matches the available fields of the
434
+ // parent type and/or argument names of the field.
435
+ } catch (e) {
436
+ errors.push(ERRORS.SOURCE_FIELD_HTTP_BODY_INVALID.err(
437
+ `${sourceField} http.body not valid JSONSelection (error: ${e.message})`,
438
+ { nodes: application.sourceAST },
439
+ ));
439
440
  }
440
441
  }
441
442
  }
@@ -447,7 +448,7 @@ export class SourceSpecDefinition extends FeatureDefinition {
447
448
  // the parent type and/or argument names of the field.
448
449
  } catch (e) {
449
450
  errors.push(ERRORS.SOURCE_FIELD_SELECTION_INVALID.err(
450
- `${sourceField} selection not valid JSONSelection: ${e.message}`,
451
+ `${sourceField} selection not valid JSONSelection (error: ${e.message})`,
451
452
  { nodes: application.sourceAST },
452
453
  ));
453
454
  }
@@ -456,14 +457,14 @@ export class SourceSpecDefinition extends FeatureDefinition {
456
457
  // @sourceField is allowed only on root Query and Mutation fields or
457
458
  // fields of entity object types.
458
459
  const fieldParent = application.parent;
459
- if (fieldParent.sourceAST?.kind !== "FieldDefinition") {
460
+ if (fieldParent.sourceAST?.kind !== Kind.FIELD_DEFINITION) {
460
461
  errors.push(ERRORS.SOURCE_FIELD_NOT_ON_ROOT_OR_ENTITY_FIELD.err(
461
462
  `${sourceField} must be applied to field`,
462
463
  { nodes: application.sourceAST },
463
464
  ));
464
465
  } else {
465
466
  const typeGrandparent = fieldParent.parent as SchemaElement<any, any>;
466
- if (typeGrandparent.sourceAST?.kind !== "ObjectTypeDefinition") {
467
+ if (typeGrandparent.sourceAST?.kind !== Kind.OBJECT_TYPE_DEFINITION) {
467
468
  errors.push(ERRORS.SOURCE_FIELD_NOT_ON_ROOT_OR_ENTITY_FIELD.err(
468
469
  `${sourceField} must be applied to field of object type`,
469
470
  { nodes: application.sourceAST },
@@ -486,6 +487,10 @@ export class SourceSpecDefinition extends FeatureDefinition {
486
487
  }
487
488
  }
488
489
 
490
+ function isValidSourceAPIName(name: string): boolean {
491
+ return /^[a-z-_][a-z0-9-_]*$/i.test(name);
492
+ }
493
+
489
494
  function isValidHTTPHeaderName(name: string): boolean {
490
495
  // https://developers.cloudflare.com/rules/transform/request-header-modification/reference/header-format/
491
496
  return /^[a-zA-Z0-9-_]+$/.test(name);
@@ -504,15 +509,19 @@ function validateHTTPHeaders(
504
509
  // Ensure name is a valid HTTP header name.
505
510
  if (!isValidHTTPHeaderName(name)) {
506
511
  errors.push(ERRORS.SOURCE_HTTP_HEADERS_INVALID.err(
507
- `${directiveName} headers[${i}].name == ${
508
- JSON.stringify(name)
509
- } is not valid HTTP header name`,
512
+ `${directiveName} header ${JSON.stringify(headers[i])} specifies invalid name`,
513
+ ));
514
+ }
515
+
516
+ if (as && !isValidHTTPHeaderName(as)) {
517
+ errors.push(ERRORS.SOURCE_HTTP_HEADERS_INVALID.err(
518
+ `${directiveName} header ${JSON.stringify(headers[i])} specifies invalid 'as' name`,
510
519
  ));
511
520
  }
512
521
 
513
- if (!as === !value) {
522
+ if (as && value) {
514
523
  errors.push(ERRORS.SOURCE_HTTP_HEADERS_INVALID.err(
515
- `${directiveName} headers[${i}] must specify exactly one of as or value`,
524
+ `${directiveName} header ${JSON.stringify(headers[i])} should specify at most one of 'as' or 'value'`,
516
525
  ));
517
526
  }
518
527