@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.
- package/dist/definitions.d.ts +2 -1
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +5 -0
- package/dist/definitions.js.map +1 -1
- package/dist/error.d.ts +1 -0
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +2 -0
- package/dist/error.js.map +1 -1
- package/dist/federation.d.ts +2 -1
- package/dist/federation.d.ts.map +1 -1
- package/dist/federation.js +21 -12
- package/dist/federation.js.map +1 -1
- package/dist/operations.d.ts +3 -1
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +49 -12
- package/dist/operations.js.map +1 -1
- package/dist/specs/coreSpec.d.ts +1 -0
- package/dist/specs/coreSpec.d.ts.map +1 -1
- package/dist/specs/coreSpec.js +9 -0
- package/dist/specs/coreSpec.js.map +1 -1
- package/dist/specs/sourceSpec.d.ts +2 -1
- package/dist/specs/sourceSpec.d.ts.map +1 -1
- package/dist/specs/sourceSpec.js +78 -78
- package/dist/specs/sourceSpec.js.map +1 -1
- package/package.json +1 -1
- package/src/definitions.ts +6 -0
- package/src/error.ts +7 -0
- package/src/federation.ts +31 -13
- package/src/operations.ts +95 -23
- package/src/specs/coreSpec.ts +22 -0
- package/src/specs/sourceSpec.ts +125 -116
package/src/specs/sourceSpec.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DirectiveLocation, GraphQLError,
|
|
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
|
|
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
|
-
|
|
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 (
|
|
220
|
+
if (!isValidSourceAPIName(name)) {
|
|
220
221
|
errors.push(ERRORS.SOURCE_API_NAME_INVALID.err(
|
|
221
|
-
`${sourceAPI}
|
|
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
|
-
|
|
227
|
-
assertName(name);
|
|
228
|
-
} catch (e) {
|
|
229
|
+
if (apiNameToProtocol.has(name)) {
|
|
229
230
|
errors.push(ERRORS.SOURCE_API_NAME_INVALID.err(
|
|
230
|
-
`${sourceAPI}
|
|
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 ${
|
|
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
|
|
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
|
-
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
302
|
-
const { GET, POST, headers, body } = protocolValue as HTTPSourceType;
|
|
324
|
+
validateHTTPHeaders(headers, errors, sourceType.name);
|
|
303
325
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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 !==
|
|
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 !==
|
|
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[
|
|
508
|
-
|
|
509
|
-
|
|
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 (
|
|
522
|
+
if (as && value) {
|
|
514
523
|
errors.push(ERRORS.SOURCE_HTTP_HEADERS_INVALID.err(
|
|
515
|
-
`${directiveName} headers[
|
|
524
|
+
`${directiveName} header ${JSON.stringify(headers[i])} should specify at most one of 'as' or 'value'`,
|
|
516
525
|
));
|
|
517
526
|
}
|
|
518
527
|
|