@graphql-mesh/transport-mysql
Version:
169 lines (168 loc) • 9.24 kB
JavaScript
import { getNamedType, isObjectType } from 'graphql';
import graphqlFields from 'graphql-fields';
import { createPool } from 'mysql';
import { introspection, upgrade } from 'mysql-utilities';
import { util } from '@graphql-mesh/cross-helpers';
import { getDefDirectives, makeAsyncDisposable } from '@graphql-mesh/utils';
import { createDefaultExecutor } from '@graphql-tools/delegate';
import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils';
import { getConnectionOptsFromEndpointUri } from './parseEndpointUri.js';
function getFieldsFromResolveInfo(info) {
const fieldMap = graphqlFields(info);
return Object.keys(fieldMap).filter(fieldName => Object.keys(fieldMap[fieldName]).length === 0 && fieldName !== '__typename');
}
export function getMySQLExecutor({ subgraph, pool }) {
const mysqlConnectionByContext = new WeakMap();
subgraph = mapSchema(subgraph, {
[MapperKind.OBJECT_FIELD](fieldConfig, fieldName) {
const directives = getDefDirectives(subgraph, fieldConfig);
for (const directive of directives) {
switch (directive.name) {
case 'mysqlSelect': {
const { table, columnMap: columnMapEntries } = directive.args;
const columnMap = new Map(columnMapEntries);
fieldConfig.resolve = function mysqlSelectResolver(root, args, context, info) {
const where = {
...args.where,
};
columnMap.forEach((foreignColumn, localColumn) => {
where[foreignColumn] = root[localColumn];
});
const limit = [args.limit, args.offset].filter(Boolean);
const fieldMap = graphqlFields(info);
const fields = [];
for (const fieldName in fieldMap) {
if (fieldName !== '__typename') {
const subFieldMap = fieldMap[fieldName];
if (Object.keys(subFieldMap).length === 0) {
fields.push(fieldName);
}
else {
const returnType = getNamedType(fieldConfig.type);
if (isObjectType(returnType)) {
const returnTypeFields = returnType.getFields();
const foreignField = returnTypeFields[fieldName];
const foreignDirective = getDirective(subgraph, foreignField, 'mysqlTableForeign');
const foreignDirectiveArgs = foreignDirective?.[0];
const columnName = foreignDirectiveArgs?.columnName;
if (columnName) {
fields.push(columnName);
}
}
else {
throw new Error(`Invalid type for field ${fieldName}`);
}
}
}
}
const mysqlConnection = mysqlConnectionByContext.get(context);
if (limit.length) {
const selectLimit$ = util.promisify(mysqlConnection.selectLimit.bind(mysqlConnection));
return selectLimit$(table, fields, limit, where, args?.orderBy);
}
else {
const select$ = util.promisify(mysqlConnection.select.bind(mysqlConnection));
return select$(table, fields, where, args?.orderBy);
}
};
break;
}
case 'mysqlCount': {
const { table } = directive.args;
fieldConfig.resolve = function mysqlCountResolver(root, args, context, info) {
const mysqlConnection = mysqlConnectionByContext.get(context);
const count$ = util.promisify(mysqlConnection.count.bind(mysqlConnection));
return count$(table, args.where);
};
break;
}
case 'mysqlInsert': {
const { table, primaryKeys } = directive.args;
fieldConfig.resolve = async function mysqlInsertResolver(root, args, context, info) {
const mysqlConnection = mysqlConnectionByContext.get(context);
const select$ = util.promisify(mysqlConnection.select.bind(mysqlConnection));
const insert$ = util.promisify(mysqlConnection.insert.bind(mysqlConnection));
const input = args[table];
const { recordId } = await insert$(table, input);
const fields = getFieldsFromResolveInfo(info);
const where = {};
for (const primaryColumnName of primaryKeys) {
where[primaryColumnName] = input[primaryColumnName] || recordId;
}
const result = await select$(table, fields, where, {});
return result[0];
};
break;
}
case 'mysqlUpdate': {
const { table } = directive.args;
fieldConfig.resolve = async function mysqlUpdateResolver(root, args, context, info) {
const mysqlConnection = mysqlConnectionByContext.get(context);
const update$ = util.promisify(mysqlConnection.update.bind(mysqlConnection));
await update$(table, args[table], args.where);
const fields = getFieldsFromResolveInfo(info);
const select$ = util.promisify(mysqlConnection.select.bind(mysqlConnection));
const result = await select$(table, fields, args.where, {});
return result[0];
};
break;
}
case 'mysqlDelete': {
const { table } = directive.args;
fieldConfig.resolve = async function mysqlDeleteResolver(root, args, context, info) {
const mysqlConnection = mysqlConnectionByContext.get(context);
const delete$ = util.promisify(mysqlConnection.delete.bind(mysqlConnection));
const res = await delete$(table, args.where);
return !!res.affectedRows;
};
break;
}
}
}
return fieldConfig;
},
});
const transportDirectives = getDirective(subgraph, subgraph, 'transport');
if (!transportDirectives?.length) {
throw new Error(`No transport directives found in the schema`);
}
const transportDirective = transportDirectives[0];
const { location } = transportDirective;
const connectionOpts = getConnectionOptsFromEndpointUri(location);
const isDebug = globalThis.process?.env?.DEBUG?.includes('mysql') ||
globalThis.process?.env?.DEBUG?.toString() === '1' ||
globalThis.process?.env?.DEBUG?.toLowerCase() === 'true';
pool ||= createPool({
...connectionOpts,
supportBigNumbers: true,
bigNumberStrings: true,
debug: !!isDebug,
trace: !!isDebug,
});
pool.on('connection', connection => {
introspection(connection);
upgrade(connection);
});
const defaultExecutor = createDefaultExecutor(subgraph);
const getConnection$ = util.promisify(pool.getConnection.bind(pool));
return makeAsyncDisposable(async function mysqlExecutor(executionRequest) {
const mysqlConnection = await getConnection$();
mysqlConnectionByContext.set(executionRequest.context, mysqlConnection);
try {
return await defaultExecutor(executionRequest);
}
finally {
mysqlConnectionByContext.delete(executionRequest.context);
mysqlConnection.release();
}
}, () => new Promise((resolve, reject) => {
pool.end(err => {
if (err) {
reject(err);
}
else {
resolve();
}
});
}));
}