1 | import { GraphQLList, GraphQLBoolean, GraphQLInt, GraphQLFloat, GraphQLString, GraphQLEnumType, GraphQLNonNull, GraphQLInputObjectType, GraphQLObjectType, GraphQLSchema } from 'graphql';
|
2 | import { pascalCase } from 'pascal-case';
|
3 | import { JSONResolver } from 'graphql-scalars';
|
4 | import urlJoin from 'url-join';
|
5 | import { loadFromModuleExportExpression, readFileOrUrlWithCache, parseInterpolationStrings, stringInterpolator } from '@graphql-mesh/utils';
|
6 | import AggregateError from 'aggregate-error';
|
7 | import { Request, fetchache } from 'fetchache';
|
8 |
|
9 | const asArray = (maybeArray) => {
|
10 | if (Array.isArray(maybeArray)) {
|
11 | return maybeArray;
|
12 | }
|
13 | else if (maybeArray) {
|
14 | return [maybeArray];
|
15 | }
|
16 | else {
|
17 | return [];
|
18 | }
|
19 | };
|
20 | class JSONSchemaVisitorCache {
|
21 | constructor() {
|
22 | this.inputSpecificTypesByIdentifier = new Map();
|
23 | this.outputSpecificTypesByIdentifier = new Map();
|
24 | this.sharedTypesByIdentifier = new Map();
|
25 | this.typesByNames = new Map();
|
26 | this.prefixedNames = new Set();
|
27 | this.potentialPrefixes = new WeakMap();
|
28 | }
|
29 | }
|
30 | class JSONSchemaVisitor {
|
31 | constructor(cache = new JSONSchemaVisitorCache()) {
|
32 | this.cache = cache;
|
33 | this.warnedReferences = new Set();
|
34 | }
|
35 | getNameFromId(id) {
|
36 | const [actualTypePath, genericTypePart] = id.split('<');
|
37 | const actualTypePathParts = actualTypePath.split(':');
|
38 | const actualTypeName = actualTypePathParts[actualTypePathParts.length - 1];
|
39 | let finalTypeName = actualTypeName;
|
40 | if (genericTypePart) {
|
41 | const [genericTypePath] = genericTypePart.split('>');
|
42 | const genericTypeParts = genericTypePath.split(':');
|
43 | const genericTypeName = genericTypeParts[genericTypeParts.length - 1];
|
44 | finalTypeName = actualTypeName + '_' + genericTypeName;
|
45 | }
|
46 | return pascalCase(finalTypeName);
|
47 | }
|
48 | visit(def, propertyName, prefix, isInput) {
|
49 | if ('definitions' in def) {
|
50 | for (const propertyName in def.definitions) {
|
51 | const definition = def.definitions[propertyName];
|
52 | this.visit(definition, propertyName, prefix, isInput);
|
53 | }
|
54 | }
|
55 | if ('$defs' in def) {
|
56 | for (const propertyName in def.$defs) {
|
57 | const definition = def.$defs[propertyName];
|
58 | this.visit(definition, propertyName, prefix, isInput);
|
59 | }
|
60 | }
|
61 | switch (def.type) {
|
62 | case 'array':
|
63 | return this.visitArray(def, propertyName, prefix, isInput);
|
64 | case 'boolean':
|
65 | return this.visitBoolean();
|
66 | case 'integer':
|
67 | return this.visitInteger();
|
68 | case 'number':
|
69 | return this.visitNumber();
|
70 | case 'string':
|
71 | if ('enum' in def) {
|
72 | return this.visitEnum(def, propertyName, prefix);
|
73 | }
|
74 | else {
|
75 | return this.visitString();
|
76 | }
|
77 | case 'null':
|
78 | case 'any':
|
79 | return this.visitAny();
|
80 | case 'object':
|
81 | if ('$ref' in def) {
|
82 | return this.visitObjectReference(def, isInput);
|
83 | }
|
84 | else if ('name' in def || 'title' in def) {
|
85 | return this.visitTypedNamedObjectDefinition(def, prefix, isInput);
|
86 | }
|
87 | else if ('properties' in def) {
|
88 | return this.visitTypedUnnamedObjectDefinition(def, propertyName, prefix, isInput);
|
89 | }
|
90 | else if ('additionalProperties' in def && def.additionalProperties) {
|
91 | return this.visitAny();
|
92 | }
|
93 | break;
|
94 | }
|
95 | throw new Error(`Unexpected schema definition:
|
96 | ${JSON.stringify(def, null, 2)}`);
|
97 | }
|
98 | visitArray(arrayDef, propertyName, prefix, isInput) {
|
99 | const [itemsDef] = asArray(arrayDef.items);
|
100 | return new GraphQLList(itemsDef ? this.visit(itemsDef, propertyName, prefix, isInput) : this.visitAny());
|
101 | }
|
102 | visitBoolean() {
|
103 | return GraphQLBoolean;
|
104 | }
|
105 | visitInteger() {
|
106 | return GraphQLInt;
|
107 | }
|
108 | visitNumber() {
|
109 | return GraphQLFloat;
|
110 | }
|
111 | visitString() {
|
112 | return GraphQLString;
|
113 | }
|
114 | visitEnum(enumDef, propertyName, prefix) {
|
115 | const enumIdentifier = JSON.stringify(enumDef);
|
116 | if (!this.cache.sharedTypesByIdentifier.has(enumIdentifier)) {
|
117 |
|
118 |
|
119 | let name = pascalCase(propertyName);
|
120 | if (!this.cache.prefixedNames.has(name) && this.cache.typesByNames.has(name)) {
|
121 | const existingType = this.cache.typesByNames.get(name);
|
122 | if ('name' in existingType) {
|
123 | const prefix = this.cache.potentialPrefixes.get(existingType);
|
124 | existingType.name = pascalCase(prefix + '_' + name);
|
125 | this.cache.prefixedNames.add(name);
|
126 | this.cache.typesByNames.delete(name);
|
127 | if (this.cache.typesByNames.has(existingType.name)) {
|
128 | throw existingType.name;
|
129 | }
|
130 | this.cache.typesByNames.set(existingType.name, existingType);
|
131 | }
|
132 | }
|
133 | if (this.cache.prefixedNames.has(name)) {
|
134 | name = pascalCase(prefix + '_' + name);
|
135 | }
|
136 | const type = new GraphQLEnumType({
|
137 | name,
|
138 | description: enumDef.description,
|
139 | values: enumDef.enum.reduce((values, enumValue) => ({ ...values, [enumValue]: {} }), {}),
|
140 | });
|
141 | this.cache.potentialPrefixes.set(type, prefix);
|
142 | this.cache.sharedTypesByIdentifier.set(enumIdentifier, type);
|
143 | if (this.cache.typesByNames.has(name)) {
|
144 | throw name;
|
145 | }
|
146 | this.cache.typesByNames.set(name, type);
|
147 | }
|
148 | return this.cache.sharedTypesByIdentifier.get(enumIdentifier);
|
149 | }
|
150 | createFieldsMapFromProperties(objectDef, prefix, isInput) {
|
151 | var _a;
|
152 | const fieldMap = {};
|
153 | for (const propertyName in objectDef.properties) {
|
154 | const fieldName = propertyName.split(':').join('_');
|
155 | const property = objectDef.properties[propertyName];
|
156 | const type = this.visit(property, propertyName, prefix, isInput);
|
157 | const isRequired = 'required' in objectDef && ((_a = objectDef.required) === null || _a === void 0 ? void 0 : _a.includes(propertyName));
|
158 | fieldMap[fieldName] = {
|
159 | type: isRequired ? new GraphQLNonNull(type) : type,
|
160 | description: property.description,
|
161 | };
|
162 | if (fieldName !== propertyName) {
|
163 | fieldMap[fieldName].resolve = (root) => root[propertyName];
|
164 | }
|
165 | }
|
166 | return fieldMap;
|
167 | }
|
168 | getSpecificTypeByIdentifier(identifier, isInput) {
|
169 | return (this.cache.sharedTypesByIdentifier.get(identifier) ||
|
170 | (isInput
|
171 | ? this.cache.inputSpecificTypesByIdentifier.get(identifier)
|
172 | : this.cache.outputSpecificTypesByIdentifier.get(identifier)));
|
173 | }
|
174 | getGraphQLObjectTypeWithTypedObjectDef(objectDef, objectIdentifier, rawName, prefix, isInput) {
|
175 | const specificType = this.getSpecificTypeByIdentifier(objectIdentifier, isInput);
|
176 | if (!specificType) {
|
177 | let name = rawName;
|
178 |
|
179 |
|
180 | if (!this.cache.prefixedNames.has(name) && this.cache.typesByNames.has(name)) {
|
181 | const existingType = this.cache.typesByNames.get(name);
|
182 | if ('name' in existingType) {
|
183 | const existingTypePrefix = this.cache.potentialPrefixes.get(existingType);
|
184 | existingType.name = pascalCase(existingTypePrefix + '_' + name);
|
185 | this.cache.prefixedNames.add(name);
|
186 | this.cache.typesByNames.delete(name);
|
187 | if (this.cache.typesByNames.has(existingType.name)) {
|
188 | throw existingType.name;
|
189 | }
|
190 | this.cache.typesByNames.set(existingType.name, existingType);
|
191 | }
|
192 | }
|
193 |
|
194 | if (this.cache.prefixedNames.has(name)) {
|
195 | name = pascalCase(prefix + '_' + name);
|
196 | }
|
197 | if (this.cache.typesByNames.has(name)) {
|
198 | const suffix = isInput ? 'Input' : 'Output';
|
199 | name = pascalCase(name + '_' + suffix);
|
200 | }
|
201 | if (isInput) {
|
202 | const inputType = new GraphQLInputObjectType({
|
203 | name,
|
204 | description: objectDef.description,
|
205 | fields: this.createFieldsMapFromProperties(objectDef, name, true),
|
206 | });
|
207 | this.cache.inputSpecificTypesByIdentifier.set(objectIdentifier, inputType);
|
208 | this.cache.typesByNames.set(inputType.name, inputType);
|
209 | this.cache.potentialPrefixes.set(inputType, prefix);
|
210 | return inputType;
|
211 | }
|
212 | else {
|
213 | const outputType = new GraphQLObjectType({
|
214 | name,
|
215 | description: objectDef.description,
|
216 | fields: this.createFieldsMapFromProperties(objectDef, name, false),
|
217 | });
|
218 | this.cache.outputSpecificTypesByIdentifier.set(objectIdentifier, outputType);
|
219 | this.cache.typesByNames.set(outputType.name, outputType);
|
220 | this.cache.potentialPrefixes.set(outputType, prefix);
|
221 | return outputType;
|
222 | }
|
223 | }
|
224 | return specificType;
|
225 | }
|
226 | visitTypedUnnamedObjectDefinition(typedUnnamedObjectDef, propertyName, prefix, isInput) {
|
227 | const objectIdentifier = 'id' in typedUnnamedObjectDef
|
228 | ? typedUnnamedObjectDef.id
|
229 | : '$id' in typedUnnamedObjectDef
|
230 | ? typedUnnamedObjectDef.$id
|
231 | : `${prefix}_${propertyName}`;
|
232 | const name = this.getNameFromId(objectIdentifier);
|
233 | return this.getGraphQLObjectTypeWithTypedObjectDef(typedUnnamedObjectDef, objectIdentifier, name, prefix, isInput);
|
234 | }
|
235 | visitTypedNamedObjectDefinition(typedNamedObjectDef, prefix, isInput) {
|
236 | const objectIdentifier = 'name' in typedNamedObjectDef ? typedNamedObjectDef.name : typedNamedObjectDef.title;
|
237 | const name = pascalCase(objectIdentifier);
|
238 | return this.getGraphQLObjectTypeWithTypedObjectDef(typedNamedObjectDef, objectIdentifier, name, prefix, isInput);
|
239 | }
|
240 | visitObjectReference(objectRef, isInput) {
|
241 | const referenceParts = objectRef.$ref.split('/');
|
242 | const reference = referenceParts[referenceParts.length - 1];
|
243 | const specificType = this.getSpecificTypeByIdentifier(reference, isInput);
|
244 | if (!specificType) {
|
245 | if (!this.warnedReferences.has(reference) && !this.warnedReferences.has(reference)) {
|
246 | console.warn(`Missing JSON Schema reference: ${reference}. GraphQLJSON will be used instead!`);
|
247 | this.warnedReferences.add(reference);
|
248 | }
|
249 | return this.visitAny();
|
250 | }
|
251 | return specificType;
|
252 | }
|
253 | visitAny() {
|
254 | return JSONResolver;
|
255 | }
|
256 | }
|
257 |
|
258 | const handler = {
|
259 | async getMeshSource({ config, cache }) {
|
260 | var _a, _b;
|
261 | const visitorCache = new JSONSchemaVisitorCache();
|
262 | await Promise.all(((_a = config.typeReferences) === null || _a === void 0 ? void 0 : _a.map(typeReference => Promise.all(Object.keys(typeReference).map(async (key) => {
|
263 | switch (key) {
|
264 | case 'sharedType': {
|
265 | const sharedType = await loadFromModuleExportExpression(typeReference.sharedType);
|
266 | visitorCache.sharedTypesByIdentifier.set(typeReference.reference, sharedType);
|
267 | break;
|
268 | }
|
269 | case 'outputType': {
|
270 | const outputType = await loadFromModuleExportExpression(typeReference.outputType);
|
271 | visitorCache.outputSpecificTypesByIdentifier.set(typeReference.reference, outputType);
|
272 | break;
|
273 | }
|
274 | case 'inputType': {
|
275 | const inputType = await loadFromModuleExportExpression(typeReference.inputType);
|
276 | visitorCache.inputSpecificTypesByIdentifier.set(typeReference.reference, inputType);
|
277 | break;
|
278 | }
|
279 | case 'reference':
|
280 | break;
|
281 | default:
|
282 | throw new Error(`Unexpected type reference field: ${key}`);
|
283 | }
|
284 | })))) || []);
|
285 | const queryFields = {};
|
286 | const mutationFields = {};
|
287 | const schemaVisitor = new JSONSchemaVisitor(visitorCache);
|
288 | const contextVariables = [];
|
289 | await Promise.all(((_b = config.operations) === null || _b === void 0 ? void 0 : _b.map(async (operationConfig) => {
|
290 | const [requestSchema, responseSchema] = await Promise.all([
|
291 | operationConfig.requestSchema
|
292 | ? readFileOrUrlWithCache(operationConfig.requestSchema, cache, {
|
293 | headers: config.schemaHeaders,
|
294 | })
|
295 | : undefined,
|
296 | readFileOrUrlWithCache(operationConfig.responseSchema, cache, {
|
297 | headers: config.schemaHeaders,
|
298 | }),
|
299 | ]);
|
300 | operationConfig.method = operationConfig.method || (operationConfig.type === 'Mutation' ? 'POST' : 'GET');
|
301 | operationConfig.type = operationConfig.type || (operationConfig.method === 'GET' ? 'Query' : 'Mutation');
|
302 | const destination = operationConfig.type === 'Query' ? queryFields : mutationFields;
|
303 | const type = schemaVisitor.visit(responseSchema, 'Response', operationConfig.field, false);
|
304 | const { args, contextVariables: specificContextVariables } = parseInterpolationStrings([
|
305 | ...Object.values(config.operationHeaders || {}),
|
306 | ...Object.values(operationConfig.headers || {}),
|
307 | operationConfig.path,
|
308 | ]);
|
309 | contextVariables.push(...specificContextVariables);
|
310 | if (requestSchema) {
|
311 | args.input = {
|
312 | type: schemaVisitor.visit(requestSchema, 'Request', operationConfig.field, true),
|
313 | };
|
314 | }
|
315 | destination[operationConfig.field] = {
|
316 | description: operationConfig.description ||
|
317 | responseSchema.description ||
|
318 | `${operationConfig.method} ${operationConfig.path}`,
|
319 | type,
|
320 | args,
|
321 | resolve: async (root, args, context, info) => {
|
322 | const interpolationData = { root, args, context, info };
|
323 | const interpolatedPath = stringInterpolator.parse(operationConfig.path, interpolationData);
|
324 | const fullPath = urlJoin(config.baseUrl, interpolatedPath);
|
325 | const method = operationConfig.method;
|
326 | const headers = {
|
327 | ...config.operationHeaders,
|
328 | ...operationConfig === null || operationConfig === void 0 ? void 0 : operationConfig.headers,
|
329 | };
|
330 | for (const headerName in headers) {
|
331 | headers[headerName] = stringInterpolator.parse(headers[headerName], interpolationData);
|
332 | }
|
333 | const requestInit = {
|
334 | method,
|
335 | headers,
|
336 | };
|
337 | const urlObj = new URL(fullPath);
|
338 | const input = args.input;
|
339 | if (input) {
|
340 | switch (method) {
|
341 | case 'GET':
|
342 | case 'DELETE': {
|
343 | const newSearchParams = new URLSearchParams(input);
|
344 | newSearchParams.forEach((value, key) => {
|
345 | urlObj.searchParams.set(key, value);
|
346 | });
|
347 | break;
|
348 | }
|
349 | case 'POST':
|
350 | case 'PUT': {
|
351 | requestInit.body = JSON.stringify(input);
|
352 | break;
|
353 | }
|
354 | default:
|
355 | throw new Error(`Unknown method ${operationConfig.method}`);
|
356 | }
|
357 | }
|
358 | const request = new Request(urlObj.toString(), requestInit);
|
359 | const response = await fetchache(request, cache);
|
360 | const responseText = await response.text();
|
361 | let responseJson;
|
362 | try {
|
363 | responseJson = JSON.parse(responseText);
|
364 | }
|
365 | catch (e) {
|
366 | throw responseText;
|
367 | }
|
368 | if (responseJson.errors) {
|
369 | throw new AggregateError(responseJson.errors);
|
370 | }
|
371 | if (responseJson._errors) {
|
372 | throw new AggregateError(responseJson._errors);
|
373 | }
|
374 | if (responseJson.error) {
|
375 | throw responseJson.error;
|
376 | }
|
377 | return responseJson;
|
378 | },
|
379 | };
|
380 | })) || []);
|
381 | const schema = new GraphQLSchema({
|
382 | query: new GraphQLObjectType({
|
383 | name: 'Query',
|
384 | fields: Object.keys(queryFields).length > 0 ? queryFields : { ping: { type: GraphQLBoolean, resolve: () => true } },
|
385 | }),
|
386 | mutation: Object.keys(mutationFields).length > 0
|
387 | ? new GraphQLObjectType({
|
388 | name: 'Mutation',
|
389 | fields: mutationFields,
|
390 | })
|
391 | : undefined,
|
392 | });
|
393 | return {
|
394 | schema,
|
395 | contextVariables,
|
396 | };
|
397 | },
|
398 | };
|
399 |
|
400 | export default handler;
|
401 |
|