UNPKG

@graphql-mesh/config

Version:
531 lines (530 loc) • 23.4 kB
import { camelCase } from 'camel-case'; import { parse, print, visit } from 'graphql'; import { pascalCase } from 'pascal-case'; import { useMaskedErrors } from '@envelop/core'; import { path as pathModule, process } from '@graphql-mesh/cross-helpers'; import { FsStoreStorageAdapter, InMemoryStoreStorageAdapter, MeshStore } from '@graphql-mesh/store'; import { defaultImportFn, parseWithCache, printWithCache, resolveAdditionalResolvers, } from '@graphql-mesh/utils'; import { usePersistedOperations } from '@graphql-yoga/plugin-persisted-operations'; import { getAdditionalResolversFromTypeDefs } from './getAdditionalResolversFromTypeDefs.js'; import { getPackage, hashSHA256, resolveAdditionalTypeDefs, resolveCache, resolveCustomFetch, resolveDocuments, resolveLogger, resolvePubSub, } from './utils.js'; const ENVELOP_CORE_PLUGINS_MAP = { maskedErrors: { moduleName: '@envelop/core', importName: 'useMaskedErrors', pluginFactory: useMaskedErrors, }, }; function getDefaultMeshStore(dir, importFn, artifactsDir) { const isProd = process.env.NODE_ENV?.toLowerCase() === 'production'; const storeStorageAdapter = isProd ? new FsStoreStorageAdapter({ cwd: dir, importFn, fileType: 'ts', }) : new InMemoryStoreStorageAdapter(); return new MeshStore(pathModule.resolve(dir, artifactsDir), storeStorageAdapter, { /** * TODO: * `mesh start` => { readonly: true, validate: false } * `mesh dev` => { readonly: false, validate: true } => validation error should show a prompt for confirmation * `mesh validate` => { readonly: true, validate: true } => should fetch from remote and try to update * readonly */ readonly: isProd, validate: false, }); } export async function processConfig(config, options) { if (config.skipSSLValidation) { process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; } const importCodes = new Set([ `import type { GetMeshOptions } from '@graphql-mesh/runtime';`, `import type { YamlConfig } from '@graphql-mesh/types';`, ]); const codes = new Set([ `export const rawServeConfig: YamlConfig.Config['serve'] = ${JSON.stringify(config.serve)} as any`, `export async function getMeshOptions(): Promise<GetMeshOptions> {`, ]); const { dir, importFn = defaultImportFn, store: providedStore, artifactsDir, additionalPackagePrefixes = [], } = options || {}; if (config.require) { await Promise.all(config.require.map(mod => importFn(mod))); for (const mod of config.require) { importCodes.add(`import '${mod}';`); } } const rootStore = providedStore || getDefaultMeshStore(dir, importFn, artifactsDir || '.mesh'); const { pubsub, importCode: pubsubImportCode, code: pubsubCode, } = await resolvePubSub(config.pubsub, importFn, dir, additionalPackagePrefixes); importCodes.add(pubsubImportCode); codes.add(pubsubCode); const sourcesStore = rootStore.child('sources'); codes.add(`const sourcesStore = rootStore.child('sources');`); const { logger, importCode: loggerImportCode, code: loggerCode, } = await resolveLogger(config.logger, importFn, dir, additionalPackagePrefixes, options?.initialLoggerPrefix); importCodes.add(loggerImportCode); codes.add(loggerCode); const { cache, importCode: cacheImportCode, code: cacheCode, } = await resolveCache(config.cache, importFn, rootStore, dir, pubsub, logger, additionalPackagePrefixes); importCodes.add(cacheImportCode); codes.add(cacheCode); const { fetchFn, importCode: fetchFnImportCode, code: fetchFnCode, } = await resolveCustomFetch({ fetchConfig: config.customFetch, cache, importFn, cwd: dir, additionalPackagePrefixes, }); importCodes.add(fetchFnImportCode); codes.add(fetchFnCode); importCodes.add(`import { MeshResolvedSource } from '@graphql-mesh/runtime';`); codes.add(`const sources: MeshResolvedSource[] = [];`); importCodes.add(`import { MeshTransform, MeshPlugin } from '@graphql-mesh/types';`); codes.add(`const transforms: MeshTransform[] = [];`); codes.add(`const additionalEnvelopPlugins: MeshPlugin<any>[] = [];`); const [sources, transforms, additionalEnvelopPlugins, additionalTypeDefs, additionalResolvers, documents,] = await Promise.all([ Promise.all(config.sources.map(async (source, sourceIndex) => { const handlerName = Object.keys(source.handler)[0].toString(); const handlerConfig = source.handler[handlerName]; const handlerVariableName = camelCase(`${source.name}_Handler`); const transformsVariableName = camelCase(`${source.name}_Transforms`); codes.add(`const ${transformsVariableName} = [];`); const [handler, transforms] = await Promise.all([ await getPackage({ name: handlerName, type: 'handler', importFn, cwd: dir, additionalPrefixes: additionalPackagePrefixes, }).then(({ resolved: HandlerCtor, moduleName }) => { if (options.generateCode) { const handlerImportName = pascalCase(handlerName + '_Handler'); importCodes.add(`import ${handlerImportName} from ${JSON.stringify(moduleName)}`); codes.add(`const ${handlerVariableName} = new ${handlerImportName}({ name: ${JSON.stringify(source.name)}, config: ${JSON.stringify(handlerConfig)}, baseDir, cache, pubsub, store: sourcesStore.child(${JSON.stringify(source.name)}), logger: logger.child(${JSON.stringify(source.name)}), importFn, });`); } return new HandlerCtor({ name: source.name, config: handlerConfig, baseDir: dir, cache, pubsub, store: sourcesStore.child(source.name), logger: logger.child(source.name), importFn, }); }), Promise.all((source.transforms || []).map(async (t, transformIndex) => { const transformName = Object.keys(t)[0].toString(); const transformConfig = t[transformName]; const { resolved: TransformCtor, moduleName } = await getPackage({ name: transformName, type: 'transform', importFn, cwd: dir, additionalPrefixes: additionalPackagePrefixes, }); if (options.generateCode) { const transformImportName = pascalCase(transformName + '_Transform'); importCodes.add(`import ${transformImportName} from ${JSON.stringify(moduleName)};`); codes.add(`${transformsVariableName}[${transformIndex}] = new ${transformImportName}({ apiName: ${JSON.stringify(source.name)}, config: ${JSON.stringify(transformConfig)}, baseDir, cache, pubsub, importFn, logger, });`); } return new TransformCtor({ apiName: source.name, config: transformConfig, baseDir: dir, cache, pubsub, importFn, logger, }); })), ]); if (options.generateCode) { codes.add(`sources[${sourceIndex}] = { name: '${source.name}', handler: ${handlerVariableName}, transforms: ${transformsVariableName} }`); } return { name: source.name, handler, transforms, }; })), Promise.all(config.transforms?.map(async (t, transformIndex) => { const transformName = Object.keys(t)[0].toString(); const transformConfig = t[transformName]; const { resolved: TransformLibrary, moduleName } = await getPackage({ name: transformName, type: 'transform', importFn, cwd: dir, additionalPrefixes: additionalPackagePrefixes, }); if (options.generateCode) { const transformImportName = pascalCase(transformName + '_Transform'); importCodes.add(`import ${transformImportName} from ${JSON.stringify(moduleName)};`); codes.add(`transforms[${transformIndex}] = new (${transformImportName} as any)({ apiName: '', config: ${JSON.stringify(transformConfig)}, baseDir, cache, pubsub, importFn, logger, })`); } return new TransformLibrary({ apiName: '', config: transformConfig, baseDir: dir, cache, pubsub, importFn, logger, }); }) || []), Promise.all(config.plugins?.map(async (p, pluginIndex) => { const [pluginName, maybeImportName] = Object.keys(p)[0].toString().split('#'); const pluginConfig = p[pluginName]; if (ENVELOP_CORE_PLUGINS_MAP[pluginName] != null) { const { importName, moduleName, pluginFactory } = ENVELOP_CORE_PLUGINS_MAP[pluginName]; if (options.generateCode) { importCodes.add(`import { ${importName} } from ${JSON.stringify(moduleName)};`); codes.add(`additionalEnvelopPlugins[${pluginIndex}] = await ${importName}(${JSON.stringify(pluginConfig, null, 2)});`); } return pluginFactory(pluginConfig); } const { resolved: possiblePluginFactory, moduleName } = await getPackage({ name: pluginName, type: 'plugin', importFn, cwd: dir, additionalPrefixes: [ ...additionalPackagePrefixes, '@envelop/', '@graphql-yoga/plugin-', '@escape.tech/graphql-armor-', ], }); let pluginFactory; if (typeof possiblePluginFactory === 'function') { pluginFactory = possiblePluginFactory; if (options.generateCode) { const importName = pascalCase('use_' + pluginName); importCodes.add(`import ${importName} from ${JSON.stringify(moduleName)};`); codes.add(`additionalEnvelopPlugins[${pluginIndex}] = await ${importName}({ ...(${JSON.stringify(pluginConfig, null, 2)}), logger: logger.child(${JSON.stringify(pluginName)}), cache, pubsub, baseDir, importFn, })`); } } else { const importName = maybeImportName || Object.keys(possiblePluginFactory) .find(iName => (iName.toString().startsWith('use') || iName.toString().endsWith('Plugin')) && typeof possiblePluginFactory[iName] === 'function') .toString(); if (importName) { pluginFactory = possiblePluginFactory[importName]; if (options.generateCode) { importCodes.add(`import { ${importName} } from ${JSON.stringify(moduleName)};`); codes.add(`additionalEnvelopPlugins[${pluginIndex}] = await ${importName}({ ...(${JSON.stringify(pluginConfig, null, 2)}), logger: logger.child(${JSON.stringify(pluginName)}), cache, pubsub, baseDir, importFn, });`); } } } return pluginFactory({ ...pluginConfig, logger: logger.child(pluginName), cache, pubsub, baseDir: dir, importFn, }); }) || []), resolveAdditionalTypeDefs(dir, config.additionalTypeDefs).then(additionalTypeDefs => { if (options.generateCode) { codes.add(`const additionalTypeDefs = [${(additionalTypeDefs || []).map(parsedTypeDefs => `parse(${JSON.stringify(print(parsedTypeDefs))}),`)}] as any[];`); if (additionalTypeDefs?.length) { importCodes.add(`import { parse } from 'graphql';`); } } return additionalTypeDefs; }), options?.ignoreAdditionalResolvers ? [] : resolveAdditionalResolvers(dir, config.additionalResolvers, importFn, pubsub), resolveDocuments(config.documents, dir), ]); if (options.generateCode) { if (config.additionalResolvers?.length) { codes.add(`const additionalResolvers = await Promise.all([ ${config.additionalResolvers .map(additionalResolverDefinition => { if (typeof additionalResolverDefinition === 'string') { return `import(${JSON.stringify(pathModule.join('..', additionalResolverDefinition).split('\\').join('/'))}) .then(m => m.resolvers || m.default || m)`; } else { importCodes.add(`import { resolveAdditionalResolversWithoutImport } from '@graphql-mesh/utils';`); return `resolveAdditionalResolversWithoutImport( ${JSON.stringify(additionalResolverDefinition, null, 2)} )`; } }) .join(',\n')} ]);`); } else { codes.add(`const additionalResolvers = [] as any[]`); } } if (additionalTypeDefs?.length) { const additionalResolversConfigFromTypeDefs = getAdditionalResolversFromTypeDefs(additionalTypeDefs); if (additionalResolversConfigFromTypeDefs?.length) { const resolveToDirectiveDefinition = /* GraphQL */ ` scalar ResolveToSourceArgs directive @resolveTo( requiredSelectionSet: String sourceName: String! sourceTypeName: String! sourceFieldName: String! sourceSelectionSet: String sourceArgs: ResolveToSourceArgs keyField: String keysArg: String pubsubTopic: String filterBy: String additionalArgs: ResolveToSourceArgs result: String resultType: String ) on FIELD_DEFINITION `; const resolvedAdditionalResolvers = await resolveAdditionalResolvers(dir, additionalResolversConfigFromTypeDefs, importFn, pubsub); additionalTypeDefs.unshift(parse(resolveToDirectiveDefinition)); additionalResolvers.push(...resolvedAdditionalResolvers); if (options.generateCode && resolvedAdditionalResolvers.length) { importCodes.add(`import { resolveAdditionalResolvers } from '@graphql-mesh/utils';`); codes.add(`additionalTypeDefs.unshift(parse(/* GraphQL */\`${resolveToDirectiveDefinition}\`))`); codes.add(`const additionalResolversFromTypeDefs = await resolveAdditionalResolvers( baseDir, ${JSON.stringify(additionalResolversConfigFromTypeDefs)}, importFn, pubsub );`); codes.add(`additionalResolvers.push(...additionalResolversFromTypeDefs)`); } } } let mergerName = config.merger; // Decide what is the default merger if (!mergerName) { if (config.sources.length > 1) { mergerName = 'stitching'; } else { // eslint-disable-next-line no-labels resolversLoop: for (const resolversObj of additionalResolvers || []) { for (const typeName in resolversObj || {}) { const fieldResolvers = resolversObj[typeName]; if (typeof fieldResolvers === 'object') { for (const fieldName in fieldResolvers) { const fieldResolveObj = fieldResolvers[fieldName]; if (typeof fieldResolveObj === 'object') { // selectionSet needs stitching merger even if there is a single source if (fieldResolveObj.selectionSet != null) { mergerName = 'stitching'; // eslint-disable-next-line no-labels break resolversLoop; } } } } } } if (!mergerName) { mergerName = 'bare'; } } } const { resolved: Merger, moduleName: mergerModuleName } = await getPackage({ name: mergerName, type: 'merger', importFn, cwd: dir, additionalPrefixes: additionalPackagePrefixes, }); if (options.generateCode) { const mergerImportName = pascalCase(`${mergerName}Merger`); importCodes.add(`import ${mergerImportName} from ${JSON.stringify(mergerModuleName)};`); codes.add(`const merger = new(${mergerImportName} as any)({ cache, pubsub, logger: logger.child('${mergerName}Merger'), store: rootStore.child('${mergerName}Merger') })`); } const merger = new Merger({ cache, pubsub, logger: logger.child(`${mergerName}Merger`), store: rootStore.child(`${mergerName}Merger`), }); if (config.additionalEnvelopPlugins) { codes.add(`const importedAdditionalEnvelopPlugins = await import(${JSON.stringify(pathModule.join('..', config.additionalEnvelopPlugins).split('\\').join('/'))}).then(m => m.default || m);`); const importedAdditionalEnvelopPlugins = await importFn(pathModule.isAbsolute(config.additionalEnvelopPlugins) ? config.additionalEnvelopPlugins : pathModule.join(dir, config.additionalEnvelopPlugins)); if (typeof importedAdditionalEnvelopPlugins === 'function') { const factoryResult = await importedAdditionalEnvelopPlugins(config); if (Array.isArray(factoryResult)) { if (options.generateCode) { codes.add(`additionalEnvelopPlugins.push(...(await importedAdditionalEnvelopPlugins()));`); } additionalEnvelopPlugins.push(...factoryResult); } else { if (options.generateCode) { codes.add(`additionalEnvelopPlugins.push(await importedAdditionalEnvelopPlugins());`); } additionalEnvelopPlugins.push(factoryResult); } } else { if (Array.isArray(importedAdditionalEnvelopPlugins)) { if (options.generateCode) { codes.add(`additionalEnvelopPlugins.push(...importedAdditionalEnvelopPlugins)`); } additionalEnvelopPlugins.push(...importedAdditionalEnvelopPlugins); } else { if (options.generateCode) { codes.add(`additionalEnvelopPlugins.push(importedAdditionalEnvelopPlugins)`); } additionalEnvelopPlugins.push(importedAdditionalEnvelopPlugins); } } } const documentVariableNames = []; const documentVariableHashCodeMap = new Map(); if (documents?.length) { const documentHashMap = new Map(); const documentHashMapCodes = new Set(); if (options.generateCode) { importCodes.add(`import { printWithCache } from '@graphql-mesh/utils';`); } await Promise.all(documents.map(async (documentSource) => { const documentStr = documentSource.rawSDL || printWithCache(documentSource.document); const sha256Hash = await hashSHA256(documentStr); documentHashMap.set(sha256Hash, documentStr); documentSource.sha256Hash = sha256Hash; if (options.generateCode) { visit(documentSource.document || parseWithCache(documentStr), { OperationDefinition(node) { const variableName = pascalCase(node.name.value + '_Document'); documentVariableNames.push(variableName); documentVariableHashCodeMap.set(variableName, sha256Hash); if (options.generateCode) { documentHashMapCodes.add(`${JSON.stringify(sha256Hash)}: ${variableName}`); } }, }); } })); additionalEnvelopPlugins.push(usePersistedOperations({ getPersistedOperation(key) { return documentHashMap.get(key); }, skipDocumentValidation: true, allowArbitraryOperations: true, ...config.persistedOperations, })); if (options.generateCode) { importCodes.add(`import { usePersistedOperations } from '@graphql-yoga/plugin-persisted-operations';`); codes.add(`const documentHashMap = { ${[...documentHashMapCodes].join(',\n')} }`); codes.add(`additionalEnvelopPlugins.push(usePersistedOperations({ getPersistedOperation(key) { return documentHashMap[key]; }, ...${JSON.stringify(config.persistedOperations ?? {}, null, 2)} }))`); } } if (options.generateCode) { codes.add(` return { sources, transforms, additionalTypeDefs, additionalResolvers, cache, pubsub, merger, logger, additionalEnvelopPlugins, get documents() { return [ ${documentVariableNames .map(documentVarName => `{ document: ${documentVarName}, get rawSDL() { return printWithCache(${documentVarName}); }, location: '${documentVarName}.graphql', sha256Hash: '${documentVariableHashCodeMap.get(documentVarName)}' }`) .join(',')} ]; }, fetchFn, }; }`); } return { sources, transforms, additionalTypeDefs, additionalResolvers, cache, merger, pubsub, config, documents, logger, store: rootStore, additionalEnvelopPlugins, importCodes, codes, fetchFn, }; }