@omnigraph/mysql
Version:
447 lines (446 loc) • 16.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.loadGraphQLSchemaFromMySQL = loadGraphQLSchemaFromMySQL;
const graphql_compose_1 = require("graphql-compose");
const graphql_scalars_1 = require("graphql-scalars");
const mysql_1 = require("mysql");
const mysql_utilities_1 = require("mysql-utilities");
const cross_helpers_1 = require("@graphql-mesh/cross-helpers");
const transport_mysql_1 = require("@graphql-mesh/transport-mysql");
const utils_1 = require("@graphql-mesh/utils");
const directives_js_1 = require("./directives.js");
async function loadGraphQLSchemaFromMySQL(subgraphName, opts) {
const connectionOpts = (0, transport_mysql_1.getConnectionOptsFromEndpointUri)(opts.endpoint);
let sslOpts;
if (connectionOpts.protocol === 'mysqls:') {
sslOpts = {
rejectUnauthorized: opts.ssl?.rejectUnauthorized || cross_helpers_1.process.env.NODE_TLS_REJECT_UNAUTHORIZED === '1',
ca: opts.ssl?.caPath ? cross_helpers_1.fs.readFileSync(opts.ssl?.caPath) : opts.ssl?.ca,
};
}
connectionOpts.ssl = sslOpts;
const introspectionConnection = (0, mysql_1.createConnection)(connectionOpts);
(0, mysql_utilities_1.upgrade)(introspectionConnection);
(0, mysql_utilities_1.introspection)(introspectionConnection);
const getDatabaseTables = cross_helpers_1.util.promisify(introspectionConnection.databaseTables.bind(introspectionConnection));
const schemaComposer = new graphql_compose_1.SchemaComposer();
schemaComposer.add(graphql_scalars_1.GraphQLBigInt);
schemaComposer.add(graphql_scalars_1.GraphQLJSON);
schemaComposer.add(graphql_scalars_1.GraphQLDate);
schemaComposer.add(graphql_scalars_1.GraphQLTime);
schemaComposer.add(graphql_scalars_1.GraphQLDateTime);
schemaComposer.add(graphql_scalars_1.GraphQLTimestamp);
schemaComposer.add(graphql_scalars_1.GraphQLUnsignedInt);
schemaComposer.add(graphql_scalars_1.GraphQLUnsignedFloat);
schemaComposer.addDirective(directives_js_1.TransportDirective);
schemaComposer.addDirective(directives_js_1.MySQLSelectDirective);
schemaComposer.addDirective(directives_js_1.MySQLInsertDirective);
schemaComposer.addDirective(directives_js_1.MySQLUpdateDirective);
schemaComposer.addDirective(directives_js_1.MySQLDeleteDirective);
schemaComposer.addDirective(directives_js_1.MySQLTableForeignDirective);
schemaComposer.addDirective(directives_js_1.MySQLCountDirective);
schemaComposer.createEnumTC({
name: 'OrderBy',
values: {
asc: {
value: 'asc',
},
desc: {
value: 'desc',
},
},
});
const tables = await getDatabaseTables(connectionOpts.database);
const tableNames = opts.tables || Object.keys(tables);
const autoIncrementedColumns = await getAutoIncrementFields(introspectionConnection);
await Promise.all(tableNames.map(async (tableName) => {
await handleTableName({
subgraphName,
tableName,
tables,
tableFieldsConfig: opts.tableFields,
schemaComposer,
introspectionConnection,
autoIncrementedColumns,
});
}));
const endConnection$ = cross_helpers_1.util.promisify(introspectionConnection.end.bind(introspectionConnection));
await endConnection$(undefined);
const schema = schemaComposer.buildSchema();
const extensions = (schema.extensions ||= {});
extensions.directives ||= {};
extensions.directives.transport = {
subgraph: subgraphName,
kind: 'mysql',
location: opts.endpoint,
};
return schema;
}
const SCALARS = {
bigint: 'BigInt',
'bigint unsigned': 'BigInt',
binary: 'String',
bit: 'Int',
blob: 'String',
bool: 'Boolean',
boolean: 'Boolean',
char: 'String',
date: 'Date',
datetime: 'DateTime',
dec: 'Float',
'dec unsigned': 'UnsignedFloat',
decimal: 'Float',
'decimal unsigned': 'UnsignedFloat',
double: 'Float',
'double unsigned': 'UnsignedFloat',
float: 'Float',
'float unsigned': 'UnsignedFloat',
int: 'Int',
'int unsigned': 'UnsignedInt',
integer: 'Int',
'integer unsigned': 'UnsignedInt',
json: 'JSON',
longblob: 'String',
longtext: 'String',
mediumblob: 'String',
mediumint: 'Int',
'mediumint unsigned': 'UnsignedInt',
mediumtext: 'String',
numeric: 'Float',
'numeric unsigned': 'UnsignedFloat',
smallint: 'Int',
'smallint unsigned': 'UnsignedInt',
text: 'String',
time: 'Time',
timestamp: 'Timestamp',
tinyblob: 'String',
tinyint: 'Int',
'tinyint unsigned': 'UnsignedInt',
tinytext: 'String',
varbinary: 'String',
varchar: 'String',
year: 'Int',
};
function getAutoIncrementFields(connection) {
const queryKeyValue$ = cross_helpers_1.util.promisify(connection.queryKeyValue.bind(connection));
return queryKeyValue$('SELECT TABLE_NAME, COLUMN_NAME FROM information_schema.columns WHERE EXTRA LIKE "%auto_increment%"', []);
}
async function handleTableName({ subgraphName, tableName, tables, schemaComposer, introspectionConnection, autoIncrementedColumns, tableFieldsConfig, filteredTables, }) {
if (filteredTables && !filteredTables.includes(tableName)) {
return;
}
const table = tables[tableName];
const objectTypeName = (0, utils_1.sanitizeNameForGraphQL)(table.TABLE_NAME);
const insertInputName = (0, utils_1.sanitizeNameForGraphQL)(table.TABLE_NAME + '_InsertInput');
const updateInputName = (0, utils_1.sanitizeNameForGraphQL)(table.TABLE_NAME + '_UpdateInput');
const whereInputName = (0, utils_1.sanitizeNameForGraphQL)(table.TABLE_NAME + '_WhereInput');
const orderByInputName = (0, utils_1.sanitizeNameForGraphQL)(table.TABLE_NAME + '_OrderByInput');
const tableTC = schemaComposer.createObjectTC({
name: objectTypeName,
description: table.TABLE_COMMENT || undefined,
extensions: table,
fields: {},
});
const tableInsertIC = schemaComposer.createInputTC({
name: insertInputName,
description: table.TABLE_COMMENT || undefined,
extensions: table,
fields: {},
});
const tableUpdateIC = schemaComposer.createInputTC({
name: updateInputName,
description: table.TABLE_COMMENT || undefined,
extensions: table,
fields: {},
});
const tableWhereIC = schemaComposer.createInputTC({
name: whereInputName,
description: table.TABLE_COMMENT || undefined,
extensions: table,
fields: {},
});
const tableOrderByIC = schemaComposer.createInputTC({
name: orderByInputName,
description: table.TABLE_COMMENT || undefined,
extensions: table,
fields: {},
});
const primaryKeys = new Set();
const getTableFields$ = cross_helpers_1.util.promisify(introspectionConnection.fields.bind(introspectionConnection));
const fields = await getTableFields$(tableName);
const fieldNames = tableFieldsConfig?.find(({ table }) => table === tableName)?.fields || Object.keys(fields);
await Promise.all(fieldNames.map(fieldName => handleFieldName({
fields,
primaryKeys,
schemaComposer,
tableName,
fieldName,
autoIncrementedColumns,
tableTC,
tableInsertIC,
tableUpdateIC,
tableWhereIC,
tableOrderByIC,
})));
const getTableForeigns$ = cross_helpers_1.util.promisify(introspectionConnection.foreign.bind(introspectionConnection));
const tableForeigns = await getTableForeigns$(tableName);
const tableForeignNames = Object.keys(tableForeigns);
await Promise.all(tableForeignNames.map(foreignName => handleTableForeignName({
subgraphName,
foreignName,
tableForeigns,
schemaComposer,
tableTC,
fieldNames,
tableName,
whereInputName,
orderByInputName,
objectTypeName,
})));
schemaComposer.Query.addFields({
[tableName]: {
type: '[' + objectTypeName + ']',
args: {
limit: {
type: 'Int',
},
offset: {
type: 'Int',
},
where: {
type: whereInputName,
},
orderBy: {
type: orderByInputName,
},
},
directives: [
{
name: 'mysqlSelect',
args: {
subgraph: subgraphName,
table: tableName,
},
},
],
},
});
schemaComposer.Query.addFields({
[`count_${tableName}`]: {
type: 'Int',
args: {
where: {
type: whereInputName,
},
},
directives: [
{
name: 'mysqlCount',
args: {
subgraph: subgraphName,
table: tableName,
},
},
],
},
});
schemaComposer.Mutation.addFields({
[`insert_${tableName}`]: {
type: objectTypeName,
args: {
[tableName]: {
type: insertInputName + '!',
},
},
directives: [
{
name: 'mysqlInsert',
args: {
subgraph: subgraphName,
table: tableName,
primaryKeys: Array.from(primaryKeys),
},
},
],
},
[`update_${tableName}`]: {
type: objectTypeName,
args: {
[tableName]: {
type: updateInputName + '!',
},
where: {
type: whereInputName,
},
},
directives: [
{
name: 'mysqlUpdate',
args: {
subgraph: subgraphName,
table: tableName,
},
},
],
},
[`delete_${tableName}`]: {
type: 'Boolean',
args: {
where: {
type: whereInputName,
},
},
directives: [
{
name: 'mysqlDelete',
args: {
subgraph: subgraphName,
table: tableName,
},
},
],
},
});
}
async function handleFieldName({ fields, primaryKeys, schemaComposer, tableName, fieldName, autoIncrementedColumns, tableTC, tableInsertIC, tableUpdateIC, tableWhereIC, tableOrderByIC, }) {
const tableField = fields[fieldName];
if (tableField.Key === 'PRI') {
primaryKeys.add(fieldName);
}
const typePattern = tableField.Type;
const [realTypeNameCased, restTypePattern] = typePattern.split('(');
const [typeDetails] = restTypePattern?.split(')') || [];
const realTypeName = realTypeNameCased.toLowerCase();
let type = SCALARS[realTypeName];
if (realTypeName === 'enum' || realTypeName === 'set') {
const enumValues = typeDetails.split(`'`).join('').split(',');
const enumTypeName = (0, utils_1.sanitizeNameForGraphQL)(tableName + '_' + fieldName);
schemaComposer.createEnumTC({
name: enumTypeName,
values: enumValues.reduce((prev, curr) => {
const enumKey = (0, utils_1.sanitizeNameForGraphQL)(curr);
return {
...prev,
[enumKey]: {
value: curr,
},
};
}, {}),
});
type = enumTypeName;
}
if (!type) {
console.warn(`${realTypeName} couldn't be mapped to a type. It will be mapped to JSON as a fallback.`);
type = 'JSON';
}
const isNullable = tableField.Null.toLowerCase() === 'yes';
const isRequired = !isNullable && tableField.Default === null && autoIncrementedColumns[tableName] !== fieldName;
tableTC.addFields({
[fieldName]: {
type: isNullable ? type : type + '!',
description: tableField.Comment || undefined,
},
});
tableInsertIC.addFields({
[fieldName]: {
type: isRequired ? type + '!' : type,
description: tableField.Comment || undefined,
},
});
tableUpdateIC.addFields({
[fieldName]: {
type,
description: tableField.Comment || undefined,
},
});
tableWhereIC.addFields({
[fieldName]: {
type: 'String',
description: tableField.Comment || undefined,
},
});
tableOrderByIC.addFields({
[fieldName]: {
type: 'OrderBy',
description: tableField.Comment || undefined,
},
});
}
async function handleTableForeignName({ subgraphName, foreignName, tableForeigns, schemaComposer, tableTC, fieldNames, tableName, whereInputName, orderByInputName, objectTypeName, }) {
const tableForeign = tableForeigns[foreignName];
const columnName = tableForeign.COLUMN_NAME;
if (!fieldNames.includes(columnName)) {
return;
}
const foreignTableName = tableForeign.REFERENCED_TABLE_NAME;
const foreignColumnName = tableForeign.REFERENCED_COLUMN_NAME;
const foreignObjectTypeName = (0, utils_1.sanitizeNameForGraphQL)(foreignTableName);
const foreignWhereInputName = (0, utils_1.sanitizeNameForGraphQL)(foreignTableName + '_WhereInput');
const foreignOrderByInputName = (0, utils_1.sanitizeNameForGraphQL)(foreignTableName + '_OrderByInput');
tableTC.addFields({
[foreignTableName]: {
type: '[' + foreignObjectTypeName + ']',
args: {
where: {
type: foreignWhereInputName,
},
orderBy: {
type: foreignOrderByInputName,
},
limit: {
type: 'Int',
},
offset: {
type: 'Int',
},
},
directives: [
{
name: 'mysqlSelect',
args: {
subgraph: subgraphName,
table: foreignTableName,
columnMap: [[foreignColumnName, columnName]],
},
},
{
name: 'mysqlTableForeign',
args: {
subgraph: subgraphName,
columnName: tableForeign.COLUMN_NAME,
},
},
],
},
});
const foreignOTC = schemaComposer.getOTC(foreignObjectTypeName);
foreignOTC.addFields({
[tableName]: {
type: '[' + objectTypeName + ']',
args: {
limit: {
type: 'Int',
},
offset: {
type: 'Int',
},
where: {
type: whereInputName,
},
orderBy: {
type: orderByInputName,
},
},
directives: [
{
name: 'mysqlSelect',
args: {
subgraph: subgraphName,
table: tableName,
columnMap: [[columnName, foreignColumnName]],
},
},
],
},
});
}