UNPKG

18.7 kBJavaScriptView Raw
1import { GraphQLList, GraphQLBoolean, GraphQLInt, GraphQLFloat, GraphQLString, GraphQLEnumType, GraphQLNonNull, GraphQLInputObjectType, GraphQLObjectType, GraphQLSchema } from 'graphql';
2import { pascalCase } from 'pascal-case';
3import { JSONResolver } from 'graphql-scalars';
4import urlJoin from 'url-join';
5import { loadFromModuleExportExpression, readFileOrUrlWithCache, parseInterpolationStrings, stringInterpolator } from '@graphql-mesh/utils';
6import AggregateError from 'aggregate-error';
7import { Request, fetchache } from 'fetchache';
8
9const 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};
20class 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}
30class 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 // If there is a different enum but with the same name,
118 // just change the existing one's name and use prefixes from now on
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 // If there is a different object but with the same name,
179 // just change the existing one's name and use prefixes from now on
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 // If this name should be prefixed
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
258const 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
400export default handler;
401//# sourceMappingURL=index.esm.js.map