@apollo/federation-internals 2.8.0-connectors.4 → 2.8.0
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/directiveAndTypeSpecification.d.ts +13 -1
- package/dist/directiveAndTypeSpecification.d.ts.map +1 -1
- package/dist/directiveAndTypeSpecification.js +2 -2
- package/dist/directiveAndTypeSpecification.js.map +1 -1
- package/dist/error.d.ts +25 -0
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +50 -0
- package/dist/error.js.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.d.ts +1 -1
- package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.js +62 -7
- package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
- package/dist/federation.d.ts +19 -2
- package/dist/federation.d.ts.map +1 -1
- package/dist/federation.js +420 -9
- package/dist/federation.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/knownCoreFeatures.d.ts +3 -0
- package/dist/knownCoreFeatures.d.ts.map +1 -1
- package/dist/knownCoreFeatures.js +12 -1
- package/dist/knownCoreFeatures.js.map +1 -1
- package/dist/operations.d.ts +10 -8
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +48 -16
- package/dist/operations.js.map +1 -1
- package/dist/specs/contextSpec.d.ts +20 -0
- package/dist/specs/contextSpec.d.ts.map +1 -0
- package/dist/specs/contextSpec.js +62 -0
- package/dist/specs/contextSpec.js.map +1 -0
- package/dist/specs/coreSpec.d.ts +1 -0
- package/dist/specs/coreSpec.d.ts.map +1 -1
- package/dist/specs/coreSpec.js +3 -0
- package/dist/specs/coreSpec.js.map +1 -1
- package/dist/specs/federationSpec.d.ts +8 -2
- package/dist/specs/federationSpec.d.ts.map +1 -1
- package/dist/specs/federationSpec.js +16 -1
- package/dist/specs/federationSpec.js.map +1 -1
- package/dist/specs/joinSpec.d.ts +6 -0
- package/dist/specs/joinSpec.d.ts.map +1 -1
- package/dist/specs/joinSpec.js +11 -1
- package/dist/specs/joinSpec.js.map +1 -1
- package/dist/specs/sourceSpec.d.ts +69 -0
- package/dist/specs/sourceSpec.d.ts.map +1 -0
- package/dist/specs/sourceSpec.js +345 -0
- package/dist/specs/sourceSpec.js.map +1 -0
- package/dist/supergraphs.d.ts +4 -0
- package/dist/supergraphs.d.ts.map +1 -1
- package/dist/supergraphs.js +35 -2
- package/dist/supergraphs.js.map +1 -1
- package/dist/utils.d.ts +3 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +39 -1
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/directiveAndTypeSpecification.ts +8 -1
- package/src/error.ts +175 -0
- package/src/extractSubgraphsFromSupergraph.ts +76 -14
- package/src/federation.ts +636 -17
- package/src/index.ts +2 -1
- package/src/knownCoreFeatures.ts +15 -0
- package/src/operations.ts +63 -23
- package/src/specs/contextSpec.ts +87 -0
- package/src/specs/coreSpec.ts +5 -0
- package/src/specs/federationSpec.ts +18 -1
- package/src/specs/joinSpec.ts +27 -3
- package/src/specs/sourceSpec.ts +607 -0
- package/src/supergraphs.ts +37 -1
- package/src/utils.ts +38 -0
- package/dist/specs/connectSpec.d.ts +0 -42
- package/dist/specs/connectSpec.d.ts.map +0 -1
- package/dist/specs/connectSpec.js +0 -82
- package/dist/specs/connectSpec.js.map +0 -1
- package/dist/wasm.d.ts +0 -4
- package/dist/wasm.d.ts.map +0 -1
- package/dist/wasm.js +0 -83
- package/dist/wasm.js.map +0 -1
- package/src/specs/connectSpec.ts +0 -181
- package/src/wasm.ts +0 -111
- package/wasm/node/.gitignore +0 -1
- package/wasm/node/.npmignore +0 -2
- package/wasm/node/federation_internals_wasm.d.ts +0 -49
- package/wasm/node/federation_internals_wasm.js +0 -367
- package/wasm/node/federation_internals_wasm_bg.wasm +0 -0
- package/wasm/node/federation_internals_wasm_bg.wasm.d.ts +0 -19
- package/wasm/node/package.json +0 -15
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
import { DirectiveLocation, GraphQLError, Kind } from 'graphql';
|
|
2
|
+
import { FeatureDefinition, FeatureDefinitions, FeatureUrl, FeatureVersion, LinkDirectiveArgs } from "./coreSpec";
|
|
3
|
+
import {
|
|
4
|
+
Schema,
|
|
5
|
+
NonNullType,
|
|
6
|
+
InputObjectType,
|
|
7
|
+
InputFieldDefinition,
|
|
8
|
+
ListType,
|
|
9
|
+
DirectiveDefinition,
|
|
10
|
+
SchemaElement,
|
|
11
|
+
} from '../definitions';
|
|
12
|
+
import { registerKnownFeature } from '../knownCoreFeatures';
|
|
13
|
+
import { createDirectiveSpecification } from '../directiveAndTypeSpecification';
|
|
14
|
+
import { ERRORS } from '../error';
|
|
15
|
+
|
|
16
|
+
export const sourceIdentity = 'https://specs.apollo.dev/source';
|
|
17
|
+
|
|
18
|
+
export class SourceSpecDefinition extends FeatureDefinition {
|
|
19
|
+
constructor(version: FeatureVersion, readonly minimumFederationVersion: FeatureVersion) {
|
|
20
|
+
super(new FeatureUrl(sourceIdentity, 'source', version), minimumFederationVersion);
|
|
21
|
+
|
|
22
|
+
this.registerDirective(createDirectiveSpecification({
|
|
23
|
+
name: 'sourceAPI',
|
|
24
|
+
locations: [DirectiveLocation.SCHEMA],
|
|
25
|
+
repeatable: true,
|
|
26
|
+
// We "compose" these `@source{API,Type,Field}` directives using the
|
|
27
|
+
// `@join__directive` mechanism, so they do not need to be composed in the
|
|
28
|
+
// way passing `composes: true` here implies.
|
|
29
|
+
composes: false,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
this.registerDirective(createDirectiveSpecification({
|
|
33
|
+
name: 'sourceType',
|
|
34
|
+
locations: [DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE],
|
|
35
|
+
repeatable: true,
|
|
36
|
+
composes: false,
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
this.registerDirective(createDirectiveSpecification({
|
|
40
|
+
name: 'sourceField',
|
|
41
|
+
locations: [DirectiveLocation.FIELD_DEFINITION],
|
|
42
|
+
repeatable: true,
|
|
43
|
+
composes: false,
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
addElementsToSchema(schema: Schema): GraphQLError[] {
|
|
48
|
+
const sourceAPI = this.addDirective(schema, 'sourceAPI').addLocations(DirectiveLocation.SCHEMA);
|
|
49
|
+
sourceAPI.repeatable = true;
|
|
50
|
+
|
|
51
|
+
sourceAPI.addArgument('name', new NonNullType(schema.stringType()));
|
|
52
|
+
|
|
53
|
+
const HTTPHeaderMapping = schema.addType(new InputObjectType('HTTPHeaderMapping'));
|
|
54
|
+
HTTPHeaderMapping.addField(new InputFieldDefinition('name')).type =
|
|
55
|
+
new NonNullType(schema.stringType());
|
|
56
|
+
HTTPHeaderMapping.addField(new InputFieldDefinition('as')).type =
|
|
57
|
+
schema.stringType();
|
|
58
|
+
HTTPHeaderMapping.addField(new InputFieldDefinition('value')).type =
|
|
59
|
+
schema.stringType();
|
|
60
|
+
|
|
61
|
+
const HTTPSourceAPI = schema.addType(new InputObjectType('HTTPSourceAPI'));
|
|
62
|
+
HTTPSourceAPI.addField(new InputFieldDefinition('baseURL')).type =
|
|
63
|
+
new NonNullType(schema.stringType());
|
|
64
|
+
HTTPSourceAPI.addField(new InputFieldDefinition('headers')).type =
|
|
65
|
+
new ListType(new NonNullType(HTTPHeaderMapping));
|
|
66
|
+
sourceAPI.addArgument('http', HTTPSourceAPI);
|
|
67
|
+
|
|
68
|
+
const sourceType = this.addDirective(schema, 'sourceType').addLocations(
|
|
69
|
+
DirectiveLocation.OBJECT,
|
|
70
|
+
DirectiveLocation.INTERFACE,
|
|
71
|
+
// TODO Allow @sourceType on unions, similar to interfaces?
|
|
72
|
+
// DirectiveLocation.UNION,
|
|
73
|
+
);
|
|
74
|
+
sourceType.repeatable = true;
|
|
75
|
+
sourceType.addArgument('api', new NonNullType(schema.stringType()));
|
|
76
|
+
|
|
77
|
+
const URLPathTemplate = this.addScalarType(schema, 'URLPathTemplate');
|
|
78
|
+
const JSONSelection = this.addScalarType(schema, 'JSONSelection');
|
|
79
|
+
|
|
80
|
+
const HTTPSourceType = schema.addType(new InputObjectType('HTTPSourceType'));
|
|
81
|
+
HTTPSourceType.addField(new InputFieldDefinition('GET')).type = URLPathTemplate;
|
|
82
|
+
HTTPSourceType.addField(new InputFieldDefinition('POST')).type = URLPathTemplate;
|
|
83
|
+
HTTPSourceType.addField(new InputFieldDefinition('headers')).type =
|
|
84
|
+
new ListType(new NonNullType(HTTPHeaderMapping));
|
|
85
|
+
// Note that this body selection can only use @key fields of the type,
|
|
86
|
+
// because there are no field arguments to consume with @sourceType.
|
|
87
|
+
HTTPSourceType.addField(new InputFieldDefinition('body')).type = JSONSelection;
|
|
88
|
+
sourceType.addArgument('http', HTTPSourceType);
|
|
89
|
+
|
|
90
|
+
sourceType.addArgument('selection', new NonNullType(JSONSelection));
|
|
91
|
+
|
|
92
|
+
const KeyTypeMap = schema.addType(new InputObjectType('KeyTypeMap'));
|
|
93
|
+
KeyTypeMap.addField(new InputFieldDefinition('key')).type = new NonNullType(schema.stringType());
|
|
94
|
+
KeyTypeMap.addField(new InputFieldDefinition('typeMap')).type =
|
|
95
|
+
// TypenameKeyMap is a scalar type similar to a JSON dictionary, where the
|
|
96
|
+
// keys are __typename strings and the values are values of the key field.
|
|
97
|
+
this.addScalarType(schema, 'TypenameKeyMap');
|
|
98
|
+
sourceType.addArgument('keyTypeMap', KeyTypeMap);
|
|
99
|
+
|
|
100
|
+
const sourceField = this.addDirective(schema, 'sourceField').addLocations(
|
|
101
|
+
DirectiveLocation.FIELD_DEFINITION,
|
|
102
|
+
);
|
|
103
|
+
sourceField.repeatable = true;
|
|
104
|
+
sourceField.addArgument('api', new NonNullType(schema.stringType()));
|
|
105
|
+
sourceField.addArgument('selection', JSONSelection);
|
|
106
|
+
sourceField.addArgument('keyTypeMap', KeyTypeMap);
|
|
107
|
+
|
|
108
|
+
const HTTPSourceField = schema.addType(new InputObjectType('HTTPSourceField'));
|
|
109
|
+
HTTPSourceField.addField(new InputFieldDefinition('GET')).type = URLPathTemplate;
|
|
110
|
+
HTTPSourceField.addField(new InputFieldDefinition('POST')).type = URLPathTemplate;
|
|
111
|
+
HTTPSourceField.addField(new InputFieldDefinition('PUT')).type = URLPathTemplate;
|
|
112
|
+
HTTPSourceField.addField(new InputFieldDefinition('PATCH')).type = URLPathTemplate;
|
|
113
|
+
HTTPSourceField.addField(new InputFieldDefinition('DELETE')).type = URLPathTemplate;
|
|
114
|
+
HTTPSourceField.addField(new InputFieldDefinition('body')).type = JSONSelection;
|
|
115
|
+
HTTPSourceField.addField(new InputFieldDefinition('headers')).type =
|
|
116
|
+
new ListType(new NonNullType(HTTPHeaderMapping));
|
|
117
|
+
sourceField.addArgument('http', HTTPSourceField);
|
|
118
|
+
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
allElementNames(): string[] {
|
|
123
|
+
return [
|
|
124
|
+
'@sourceAPI',
|
|
125
|
+
'@sourceType',
|
|
126
|
+
'@sourceField',
|
|
127
|
+
// 'JSONSelection',
|
|
128
|
+
// 'URLPathTemplate',
|
|
129
|
+
// 'JSON',
|
|
130
|
+
// 'HTTPHeaderMapping',
|
|
131
|
+
// 'HTTPSourceAPI',
|
|
132
|
+
// 'HTTPSourceType',
|
|
133
|
+
// 'HTTPSourceField',
|
|
134
|
+
// 'KeyTypeMap',
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
sourceAPIDirective(schema: Schema) {
|
|
139
|
+
return this.directive<SourceAPIDirectiveArgs>(schema, 'sourceAPI')!;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
sourceTypeDirective(schema: Schema) {
|
|
143
|
+
return this.directive<SourceTypeDirectiveArgs>(schema, 'sourceType')!;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
sourceFieldDirective(schema: Schema) {
|
|
147
|
+
return this.directive<SourceFieldDirectiveArgs>(schema, 'sourceField')!;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private getSourceDirectives(schema: Schema) {
|
|
151
|
+
const result: {
|
|
152
|
+
sourceAPI?: DirectiveDefinition<SourceAPIDirectiveArgs>;
|
|
153
|
+
sourceType?: DirectiveDefinition<SourceTypeDirectiveArgs>;
|
|
154
|
+
sourceField?: DirectiveDefinition<SourceFieldDirectiveArgs>;
|
|
155
|
+
} = {};
|
|
156
|
+
|
|
157
|
+
schema.schemaDefinition.appliedDirectivesOf<LinkDirectiveArgs>('link')
|
|
158
|
+
.forEach(linkDirective => {
|
|
159
|
+
const { url, import: imports } = linkDirective.arguments();
|
|
160
|
+
const featureUrl = FeatureUrl.maybeParse(url);
|
|
161
|
+
if (imports && featureUrl && featureUrl.identity === sourceIdentity) {
|
|
162
|
+
imports.forEach(nameOrRename => {
|
|
163
|
+
const originalName = typeof nameOrRename === 'string' ? nameOrRename : nameOrRename.name;
|
|
164
|
+
const importedName = typeof nameOrRename === 'string' ? nameOrRename : nameOrRename.as || originalName;
|
|
165
|
+
const importedNameWithoutAt = importedName.replace(/^@/, '');
|
|
166
|
+
|
|
167
|
+
if (originalName === '@sourceAPI') {
|
|
168
|
+
result.sourceAPI = schema.directive(importedNameWithoutAt) as DirectiveDefinition<SourceAPIDirectiveArgs>;
|
|
169
|
+
} else if (originalName === '@sourceType') {
|
|
170
|
+
result.sourceType = schema.directive(importedNameWithoutAt) as DirectiveDefinition<SourceTypeDirectiveArgs>;
|
|
171
|
+
} else if (originalName === '@sourceField') {
|
|
172
|
+
result.sourceField = schema.directive(importedNameWithoutAt) as DirectiveDefinition<SourceFieldDirectiveArgs>;
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
override validateSubgraphSchema(schema: Schema): GraphQLError[] {
|
|
182
|
+
const errors = super.validateSubgraphSchema(schema);
|
|
183
|
+
const {
|
|
184
|
+
sourceAPI,
|
|
185
|
+
sourceType,
|
|
186
|
+
sourceField,
|
|
187
|
+
} = this.getSourceDirectives(schema);
|
|
188
|
+
|
|
189
|
+
if (!(sourceAPI || sourceType || sourceField)) {
|
|
190
|
+
// If none of the @source* directives are present, nothing needs
|
|
191
|
+
// validating.
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const apiNameToProtocol = new Map<string, ProtocolName>();
|
|
196
|
+
|
|
197
|
+
if (sourceAPI) {
|
|
198
|
+
this.validateSourceAPI(sourceAPI, apiNameToProtocol, errors);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (sourceType) {
|
|
202
|
+
this.validateSourceType(sourceType, apiNameToProtocol, errors);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (sourceField) {
|
|
206
|
+
this.validateSourceField(sourceField, apiNameToProtocol, errors);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return errors;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private validateSourceAPI(
|
|
213
|
+
sourceAPI: DirectiveDefinition<SourceAPIDirectiveArgs>,
|
|
214
|
+
apiNameToProtocol: Map<string, ProtocolName>,
|
|
215
|
+
errors: GraphQLError[],
|
|
216
|
+
) {
|
|
217
|
+
sourceAPI.applications().forEach(application => {
|
|
218
|
+
const { name, ...rest } = application.arguments();
|
|
219
|
+
|
|
220
|
+
if (!isValidSourceAPIName(name)) {
|
|
221
|
+
errors.push(ERRORS.SOURCE_API_NAME_INVALID.err(
|
|
222
|
+
`${sourceAPI}(name: ${
|
|
223
|
+
JSON.stringify(name)
|
|
224
|
+
}) must specify name using only [a-zA-Z0-9-_] characters`,
|
|
225
|
+
{ nodes: application.sourceAST },
|
|
226
|
+
));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (apiNameToProtocol.has(name)) {
|
|
230
|
+
errors.push(ERRORS.SOURCE_API_NAME_INVALID.err(
|
|
231
|
+
`${sourceAPI} must specify unique name (${JSON.stringify(name)} reused)`,
|
|
232
|
+
{ nodes: application.sourceAST },
|
|
233
|
+
));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let protocol: ProtocolName | undefined;
|
|
237
|
+
KNOWN_SOURCE_PROTOCOLS.forEach(knownProtocol => {
|
|
238
|
+
if (rest[knownProtocol]) {
|
|
239
|
+
if (protocol) {
|
|
240
|
+
errors.push(ERRORS.SOURCE_API_PROTOCOL_INVALID.err(
|
|
241
|
+
`${sourceAPI} must specify only one of ${
|
|
242
|
+
KNOWN_SOURCE_PROTOCOLS.join(', ')
|
|
243
|
+
} but specified both ${protocol} and ${knownProtocol}`,
|
|
244
|
+
{ nodes: application.sourceAST },
|
|
245
|
+
));
|
|
246
|
+
}
|
|
247
|
+
protocol = knownProtocol;
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if (protocol) {
|
|
252
|
+
apiNameToProtocol.set(name, protocol);
|
|
253
|
+
|
|
254
|
+
const protocolValue = rest[protocol];
|
|
255
|
+
if (protocolValue && protocol === HTTP_PROTOCOL) {
|
|
256
|
+
const { baseURL, headers } = protocolValue as HTTPSourceAPI;
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
new URL(baseURL);
|
|
260
|
+
} catch (e) {
|
|
261
|
+
errors.push(ERRORS.SOURCE_API_HTTP_BASE_URL_INVALID.err(
|
|
262
|
+
`${sourceAPI} http.baseURL ${JSON.stringify(baseURL)} must be valid URL (error: ${e.message})`,
|
|
263
|
+
{ nodes: application.sourceAST },
|
|
264
|
+
));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
validateHTTPHeaders(headers, errors, sourceAPI.name);
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
errors.push(ERRORS.SOURCE_API_PROTOCOL_INVALID.err(
|
|
271
|
+
`${sourceAPI} must specify one protocol from the set {${KNOWN_SOURCE_PROTOCOLS.join(',')}}`,
|
|
272
|
+
{ nodes: application.sourceAST },
|
|
273
|
+
));
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private validateSourceType(
|
|
279
|
+
sourceType: DirectiveDefinition<SourceTypeDirectiveArgs>,
|
|
280
|
+
apiNameToProtocol: Map<string, ProtocolName>,
|
|
281
|
+
errors: GraphQLError[],
|
|
282
|
+
) {
|
|
283
|
+
sourceType.applications().forEach(application => {
|
|
284
|
+
const { api, selection, ...rest } = application.arguments();
|
|
285
|
+
if (!api || !apiNameToProtocol.has(api)) {
|
|
286
|
+
errors.push(ERRORS.SOURCE_TYPE_API_ERROR.err(
|
|
287
|
+
`${sourceType} specifies unknown api ${api}`,
|
|
288
|
+
{ nodes: application.sourceAST },
|
|
289
|
+
));
|
|
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`,
|
|
309
|
+
{ nodes: application.sourceAST },
|
|
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
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
validateHTTPHeaders(headers, errors, sourceType.name);
|
|
325
|
+
|
|
326
|
+
if (body) {
|
|
327
|
+
if (GET) {
|
|
328
|
+
errors.push(ERRORS.SOURCE_TYPE_HTTP_BODY_INVALID.err(
|
|
329
|
+
`${sourceType} http.GET cannot specify http.body`,
|
|
330
|
+
{ nodes: application.sourceAST },
|
|
331
|
+
));
|
|
332
|
+
}
|
|
333
|
+
|
|
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
|
+
));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const ast = application.parent.sourceAST;
|
|
347
|
+
switch (ast?.kind) {
|
|
348
|
+
case "ObjectTypeDefinition":
|
|
349
|
+
case "InterfaceTypeDefinition":
|
|
350
|
+
if (!ast.directives?.some(directive => directive.name.value === "key")) {
|
|
351
|
+
errors.push(ERRORS.SOURCE_TYPE_ON_NON_OBJECT_OR_NON_ENTITY.err(
|
|
352
|
+
`${sourceType} must be applied to an entity type that also has a @key directive`,
|
|
353
|
+
{ nodes: application.sourceAST },
|
|
354
|
+
));
|
|
355
|
+
}
|
|
356
|
+
try {
|
|
357
|
+
parseJSONSelection(selection);
|
|
358
|
+
// TODO Validate selection is valid JSONSelection for type.
|
|
359
|
+
} catch (e) {
|
|
360
|
+
errors.push(ERRORS.SOURCE_TYPE_SELECTION_INVALID.err(
|
|
361
|
+
`${sourceType} selection not valid JSONSelection (error: ${e.message})`,
|
|
362
|
+
{ nodes: application.sourceAST },
|
|
363
|
+
));
|
|
364
|
+
}
|
|
365
|
+
break;
|
|
366
|
+
default:
|
|
367
|
+
errors.push(ERRORS.SOURCE_TYPE_ON_NON_OBJECT_OR_NON_ENTITY.err(
|
|
368
|
+
`${sourceType} must be applied to object or interface type`,
|
|
369
|
+
{ nodes: application.sourceAST },
|
|
370
|
+
));
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private validateSourceField(
|
|
376
|
+
sourceField: DirectiveDefinition<SourceFieldDirectiveArgs>,
|
|
377
|
+
apiNameToProtocol: Map<string, ProtocolName>,
|
|
378
|
+
errors: GraphQLError[],
|
|
379
|
+
) {
|
|
380
|
+
sourceField.applications().forEach(application => {
|
|
381
|
+
const { api, selection, ...rest } = application.arguments();
|
|
382
|
+
if (!api || !apiNameToProtocol.has(api)) {
|
|
383
|
+
errors.push(ERRORS.SOURCE_FIELD_API_ERROR.err(
|
|
384
|
+
`${sourceField} specifies unknown api ${api}`,
|
|
385
|
+
{ nodes: application.sourceAST },
|
|
386
|
+
));
|
|
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})`
|
|
412
|
+
));
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
validateHTTPHeaders(headers, errors, sourceField.name);
|
|
417
|
+
|
|
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
|
+
));
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (selection) {
|
|
445
|
+
try {
|
|
446
|
+
parseJSONSelection(selection);
|
|
447
|
+
// TODO Validate selection string matches the available fields of
|
|
448
|
+
// the parent type and/or argument names of the field.
|
|
449
|
+
} catch (e) {
|
|
450
|
+
errors.push(ERRORS.SOURCE_FIELD_SELECTION_INVALID.err(
|
|
451
|
+
`${sourceField} selection not valid JSONSelection (error: ${e.message})`,
|
|
452
|
+
{ nodes: application.sourceAST },
|
|
453
|
+
));
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// @sourceField is allowed only on root Query and Mutation fields or
|
|
458
|
+
// fields of entity object types.
|
|
459
|
+
const fieldParent = application.parent;
|
|
460
|
+
if (fieldParent.sourceAST?.kind !== Kind.FIELD_DEFINITION) {
|
|
461
|
+
errors.push(ERRORS.SOURCE_FIELD_NOT_ON_ROOT_OR_ENTITY_FIELD.err(
|
|
462
|
+
`${sourceField} must be applied to field`,
|
|
463
|
+
{ nodes: application.sourceAST },
|
|
464
|
+
));
|
|
465
|
+
} else {
|
|
466
|
+
const typeGrandparent = fieldParent.parent as SchemaElement<any, any>;
|
|
467
|
+
if (typeGrandparent.sourceAST?.kind !== Kind.OBJECT_TYPE_DEFINITION) {
|
|
468
|
+
errors.push(ERRORS.SOURCE_FIELD_NOT_ON_ROOT_OR_ENTITY_FIELD.err(
|
|
469
|
+
`${sourceField} must be applied to field of object type`,
|
|
470
|
+
{ nodes: application.sourceAST },
|
|
471
|
+
));
|
|
472
|
+
} else {
|
|
473
|
+
const typeGrandparentName = typeGrandparent.sourceAST?.name.value;
|
|
474
|
+
if (
|
|
475
|
+
typeGrandparentName !== "Query" &&
|
|
476
|
+
typeGrandparentName !== "Mutation" &&
|
|
477
|
+
typeGrandparent.appliedDirectivesOf("key").length === 0
|
|
478
|
+
) {
|
|
479
|
+
errors.push(ERRORS.SOURCE_FIELD_NOT_ON_ROOT_OR_ENTITY_FIELD.err(
|
|
480
|
+
`${sourceField} must be applied to root Query or Mutation field or field of entity type`,
|
|
481
|
+
{ nodes: application.sourceAST },
|
|
482
|
+
));
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function isValidSourceAPIName(name: string): boolean {
|
|
491
|
+
return /^[a-z-_][a-z0-9-_]*$/i.test(name);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function isValidHTTPHeaderName(name: string): boolean {
|
|
495
|
+
// https://developers.cloudflare.com/rules/transform/request-header-modification/reference/header-format/
|
|
496
|
+
return /^[a-zA-Z0-9-_]+$/.test(name);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function validateHTTPHeaders(
|
|
500
|
+
headers: HTTPHeaderMapping[] | undefined,
|
|
501
|
+
errors: GraphQLError[],
|
|
502
|
+
directiveName: string,
|
|
503
|
+
) {
|
|
504
|
+
if (!directiveName.startsWith('@')) {
|
|
505
|
+
directiveName = '@' + directiveName;
|
|
506
|
+
}
|
|
507
|
+
if (headers) {
|
|
508
|
+
headers.forEach(({ name, as, value }, i) => {
|
|
509
|
+
// Ensure name is a valid HTTP header name.
|
|
510
|
+
if (!isValidHTTPHeaderName(name)) {
|
|
511
|
+
errors.push(ERRORS.SOURCE_HTTP_HEADERS_INVALID.err(
|
|
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`,
|
|
519
|
+
));
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (as && value) {
|
|
523
|
+
errors.push(ERRORS.SOURCE_HTTP_HEADERS_INVALID.err(
|
|
524
|
+
`${directiveName} header ${JSON.stringify(headers[i])} should specify at most one of 'as' or 'value'`,
|
|
525
|
+
));
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// TODO Validate value is valid HTTP header value?
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function parseJSONSelection(_selection: string): any {
|
|
534
|
+
// TODO
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function parseURLPathTemplate(_template: string): any {
|
|
538
|
+
// TODO
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const HTTP_PROTOCOL = "http";
|
|
542
|
+
const KNOWN_SOURCE_PROTOCOLS = [
|
|
543
|
+
HTTP_PROTOCOL,
|
|
544
|
+
] as const;
|
|
545
|
+
type ProtocolName = (typeof KNOWN_SOURCE_PROTOCOLS)[number];
|
|
546
|
+
|
|
547
|
+
export type SourceAPIDirectiveArgs = {
|
|
548
|
+
name: string;
|
|
549
|
+
http?: HTTPSourceAPI;
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
export type HTTPSourceAPI = {
|
|
553
|
+
baseURL: string;
|
|
554
|
+
headers?: HTTPHeaderMapping[];
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
export type HTTPHeaderMapping = {
|
|
558
|
+
name: string;
|
|
559
|
+
as?: string;
|
|
560
|
+
value?: string;
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
export type SourceTypeDirectiveArgs = {
|
|
564
|
+
api: string;
|
|
565
|
+
http?: HTTPSourceType;
|
|
566
|
+
selection: JSONSelection;
|
|
567
|
+
keyTypeMap?: KeyTypeMap;
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
export type HTTPSourceType = {
|
|
571
|
+
GET?: URLPathTemplate;
|
|
572
|
+
POST?: URLPathTemplate;
|
|
573
|
+
headers?: HTTPHeaderMapping[];
|
|
574
|
+
body?: JSONSelection;
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
type URLPathTemplate = string;
|
|
578
|
+
type JSONSelection = string;
|
|
579
|
+
|
|
580
|
+
type KeyTypeMap = {
|
|
581
|
+
key: string;
|
|
582
|
+
typeMap: {
|
|
583
|
+
[__typename: string]: string;
|
|
584
|
+
};
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
export type SourceFieldDirectiveArgs = {
|
|
588
|
+
api: string;
|
|
589
|
+
http?: HTTPSourceField;
|
|
590
|
+
selection?: JSONSelection;
|
|
591
|
+
keyTypeMap?: KeyTypeMap;
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
export type HTTPSourceField = {
|
|
595
|
+
GET?: URLPathTemplate;
|
|
596
|
+
POST?: URLPathTemplate;
|
|
597
|
+
PUT?: URLPathTemplate;
|
|
598
|
+
PATCH?: URLPathTemplate;
|
|
599
|
+
DELETE?: URLPathTemplate;
|
|
600
|
+
body?: JSONSelection;
|
|
601
|
+
headers?: HTTPHeaderMapping[];
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
export const SOURCE_VERSIONS = new FeatureDefinitions<SourceSpecDefinition>(sourceIdentity)
|
|
605
|
+
.add(new SourceSpecDefinition(new FeatureVersion(0, 1), new FeatureVersion(2, 7)));
|
|
606
|
+
|
|
607
|
+
registerKnownFeature(SOURCE_VERSIONS);
|
package/src/supergraphs.ts
CHANGED
|
@@ -14,6 +14,7 @@ export const DEFAULT_SUPPORTED_SUPERGRAPH_FEATURES = new Set([
|
|
|
14
14
|
'https://specs.apollo.dev/join/v0.2',
|
|
15
15
|
'https://specs.apollo.dev/join/v0.3',
|
|
16
16
|
'https://specs.apollo.dev/join/v0.4',
|
|
17
|
+
'https://specs.apollo.dev/join/v0.5',
|
|
17
18
|
'https://specs.apollo.dev/tag/v0.1',
|
|
18
19
|
'https://specs.apollo.dev/tag/v0.2',
|
|
19
20
|
'https://specs.apollo.dev/tag/v0.3',
|
|
@@ -21,6 +22,26 @@ export const DEFAULT_SUPPORTED_SUPERGRAPH_FEATURES = new Set([
|
|
|
21
22
|
'https://specs.apollo.dev/inaccessible/v0.2',
|
|
22
23
|
]);
|
|
23
24
|
|
|
25
|
+
export const ROUTER_SUPPORTED_SUPERGRAPH_FEATURES = new Set([
|
|
26
|
+
'https://specs.apollo.dev/core/v0.1',
|
|
27
|
+
'https://specs.apollo.dev/core/v0.2',
|
|
28
|
+
'https://specs.apollo.dev/join/v0.1',
|
|
29
|
+
'https://specs.apollo.dev/join/v0.2',
|
|
30
|
+
'https://specs.apollo.dev/join/v0.3',
|
|
31
|
+
'https://specs.apollo.dev/join/v0.4',
|
|
32
|
+
'https://specs.apollo.dev/join/v0.5',
|
|
33
|
+
'https://specs.apollo.dev/tag/v0.1',
|
|
34
|
+
'https://specs.apollo.dev/tag/v0.2',
|
|
35
|
+
'https://specs.apollo.dev/tag/v0.3',
|
|
36
|
+
'https://specs.apollo.dev/inaccessible/v0.1',
|
|
37
|
+
'https://specs.apollo.dev/inaccessible/v0.2',
|
|
38
|
+
'https://specs.apollo.dev/authenticated/v0.1',
|
|
39
|
+
'https://specs.apollo.dev/requiresScopes/v0.1',
|
|
40
|
+
'https://specs.apollo.dev/policy/v0.1',
|
|
41
|
+
'https://specs.apollo.dev/source/v0.1',
|
|
42
|
+
'https://specs.apollo.dev/context/v0.1',
|
|
43
|
+
]);
|
|
44
|
+
|
|
24
45
|
const coreVersionZeroDotOneUrl = FeatureUrl.parse('https://specs.apollo.dev/core/v0.1');
|
|
25
46
|
|
|
26
47
|
/**
|
|
@@ -84,6 +105,7 @@ export class Supergraph {
|
|
|
84
105
|
private readonly containedSubgraphs: readonly {name: string, url: string}[];
|
|
85
106
|
// Lazily computed as that requires a bit of work.
|
|
86
107
|
private _subgraphs?: Subgraphs;
|
|
108
|
+
private _subgraphNameToGraphEnumValue?: Map<string, string>;
|
|
87
109
|
|
|
88
110
|
constructor(
|
|
89
111
|
readonly schema: Schema,
|
|
@@ -114,6 +136,9 @@ export class Supergraph {
|
|
|
114
136
|
return new Supergraph(schema, options?.supportedFeatures, options?.validateSupergraph);
|
|
115
137
|
}
|
|
116
138
|
|
|
139
|
+
static buildForTests(supergraphSdl: string | DocumentNode, validateSupergraph?: boolean) {
|
|
140
|
+
return Supergraph.build(supergraphSdl, { supportedFeatures: ROUTER_SUPPORTED_SUPERGRAPH_FEATURES, validateSupergraph });
|
|
141
|
+
}
|
|
117
142
|
/**
|
|
118
143
|
* The list of names/urls of the subgraphs contained in this subgraph.
|
|
119
144
|
*
|
|
@@ -129,11 +154,22 @@ export class Supergraph {
|
|
|
129
154
|
// Note that `extractSubgraphsFromSupergraph` redo a little bit of work we're already one, like validating
|
|
130
155
|
// the supergraph. We could refactor things to avoid it, but it's completely negligible in practice so we
|
|
131
156
|
// can leave that to "some day, maybe".
|
|
132
|
-
|
|
157
|
+
const extractionResults = extractSubgraphsFromSupergraph(this.schema, this.shouldValidate);
|
|
158
|
+
this._subgraphs = extractionResults[0];
|
|
159
|
+
this._subgraphNameToGraphEnumValue = extractionResults[1];
|
|
133
160
|
}
|
|
134
161
|
return this._subgraphs;
|
|
135
162
|
}
|
|
136
163
|
|
|
164
|
+
subgraphNameToGraphEnumValue(): Map<string, string> {
|
|
165
|
+
if (!this._subgraphNameToGraphEnumValue) {
|
|
166
|
+
const extractionResults = extractSubgraphsFromSupergraph(this.schema, this.shouldValidate);
|
|
167
|
+
this._subgraphs = extractionResults[0];
|
|
168
|
+
this._subgraphNameToGraphEnumValue = extractionResults[1];
|
|
169
|
+
}
|
|
170
|
+
return new Map([...this._subgraphNameToGraphEnumValue]);
|
|
171
|
+
}
|
|
172
|
+
|
|
137
173
|
apiSchema(): Schema {
|
|
138
174
|
return this.schema.toAPISchema();
|
|
139
175
|
}
|