@graphql-mesh/transport-mysql
Version:
170 lines (169 loc) • 8.96 kB
JavaScript
import { getNamedType, isObjectType } from 'graphql';
import graphqlFields from 'graphql-fields';
import { createPool } from 'mysql';
import { upgrade } from 'mysql-utilities';
import { util } from '@graphql-mesh/cross-helpers';
import { createDefaultExecutor } from '@graphql-tools/delegate';
import { getDirective, getDirectives, 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, pubsub, logger, }) {
subgraph = mapSchema(subgraph, {
[MapperKind.OBJECT_FIELD](fieldConfig, fieldName) {
const directives = getDirectives(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}`);
}
}
}
}
if (limit.length) {
const selectLimit$ = util.promisify(context.mysqlConnection.selectLimit.bind(context.mysqlConnection));
return selectLimit$(table, fields, limit, where, args?.orderBy);
}
else {
const select$ = util.promisify(context.mysqlConnection.select.bind(context.mysqlConnection));
return select$(table, fields, where, args?.orderBy);
}
};
break;
}
case 'mysqlCount': {
const { table } = directive.args;
fieldConfig.resolve = function mysqlCountResolver(root, args, context, info) {
const count$ = util.promisify(context.mysqlConnection.count.bind(context.mysqlConnection));
return count$(table, args.where);
};
break;
}
case 'mysqlInsert': {
const { table, primaryKeys } = directive.args;
fieldConfig.resolve = async function mysqlInsertResolver(root, args, context, info) {
const select$ = util.promisify(context.mysqlConnection.select.bind(context.mysqlConnection));
const insert$ = util.promisify(context.mysqlConnection.insert.bind(context.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 update$ = util.promisify(context.mysqlConnection.update.bind(context.mysqlConnection));
await update$(table, args[table], args.where);
const fields = getFieldsFromResolveInfo(info);
const select$ = util.promisify(context.mysqlConnection.select.bind(context.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 delete$ = util.promisify(context.mysqlConnection.delete.bind(context.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 => {
upgrade(connection);
});
const defaultExecutor = createDefaultExecutor(subgraph);
const getConnection$ = util.promisify(pool.getConnection.bind(pool));
if (pubsub) {
const id = pubsub.subscribe('destroy', () => {
pool.end(err => {
if (err) {
console.error(err);
}
pubsub.unsubscribe(id);
});
});
}
else {
logger?.warn(`FIXME: No pubsub provided for mysql executor, so the connection pool will never be closed`);
}
return async function mysqlExecutor(executionRequest) {
const mysqlConnection = await getConnection$();
const mysqlContext = {
mysqlConnection,
...executionRequest.context,
};
try {
return await defaultExecutor({
...executionRequest,
context: mysqlContext,
});
}
finally {
mysqlConnection.release();
}
};
}