@apollo/federation-internals 2.9.0 → 2.10.0-alpha.1
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/error.d.ts +0 -19
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +0 -38
- package/dist/error.js.map +1 -1
- package/dist/federation.d.ts +2 -6
- package/dist/federation.d.ts.map +1 -1
- package/dist/federation.js +2 -24
- package/dist/federation.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/knownCoreFeatures.d.ts +0 -3
- package/dist/knownCoreFeatures.d.ts.map +1 -1
- package/dist/knownCoreFeatures.js +1 -12
- package/dist/knownCoreFeatures.js.map +1 -1
- package/dist/specs/connectSpec.d.ts +12 -0
- package/dist/specs/connectSpec.d.ts.map +1 -0
- package/dist/specs/connectSpec.js +83 -0
- package/dist/specs/connectSpec.js.map +1 -0
- package/dist/specs/coreSpec.d.ts +0 -1
- package/dist/specs/coreSpec.d.ts.map +1 -1
- package/dist/specs/coreSpec.js +2 -5
- package/dist/specs/coreSpec.js.map +1 -1
- package/dist/specs/federationSpec.d.ts +0 -3
- package/dist/specs/federationSpec.d.ts.map +1 -1
- package/dist/specs/federationSpec.js +3 -9
- package/dist/specs/federationSpec.js.map +1 -1
- package/dist/supergraphs.d.ts +1 -1
- package/dist/supergraphs.d.ts.map +1 -1
- package/dist/supergraphs.js +1 -0
- package/dist/supergraphs.js.map +1 -1
- package/package.json +1 -1
- package/src/error.ts +4 -137
- package/src/federation.ts +24 -59
- package/src/index.ts +1 -1
- package/src/knownCoreFeatures.ts +0 -15
- package/src/specs/connectSpec.ts +148 -0
- package/src/specs/coreSpec.ts +2 -7
- package/src/specs/federationSpec.ts +3 -10
- package/src/supergraphs.ts +2 -1
- package/dist/specs/sourceSpec.d.ts +0 -69
- package/dist/specs/sourceSpec.d.ts.map +0 -1
- package/dist/specs/sourceSpec.js +0 -345
- package/dist/specs/sourceSpec.js.map +0 -1
- package/src/specs/sourceSpec.ts +0 -607
package/src/specs/sourceSpec.ts
DELETED
|
@@ -1,607 +0,0 @@
|
|
|
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);
|