quick-erd
Version:
quick and easy text-based ERD + code generator for migration, query, typescript types and orm entity
299 lines (298 loc) • 9.37 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateQuery = generateQuery;
const ts_type_1 = require("./ts-type");
function emptySelect() {
return {
fromTable: { name: '', fields: new Map() },
joins: [],
expandedSelectFields: [],
};
}
function generateQuery(columns, tableList) {
const schema = buildSchema(tableList);
const selection = makeJoinSelection(schema, columns);
function makeSelect() {
if (selection.selectedFields.length === 0)
return emptySelect();
markFieldAliases(selection.selectedFields);
const fromTable = makeFrom(selection);
const joins = makeJoins(fromTable, selection);
return makeExpandedSelectFields(fromTable, joins, selection.selectedFields);
}
const select = makeSelect();
return {
tsType: selectToTsType(select),
sql: selectToSQL(select),
knex: selectToKnex(select),
};
}
function getTable(schema, name) {
const table = schema.tables.get(name);
if (!table) {
throw new Error(`Table ${name} not found`);
}
return table;
}
function getField(table, name) {
const field = table.fields.get(name);
if (!field) {
throw new Error(`Field ${name} not found`);
}
return field;
}
function buildSchema(tableList) {
const tables = new Map();
for (const table of tableList) {
tables.set(table.name, buildTable(table));
}
return { tables };
}
function buildTable(ast_table) {
const fields = new Map();
const table = {
name: ast_table.name,
fields,
};
for (const field of ast_table.field_list) {
fields.set(field.name, buildField(table, field));
}
return table;
}
function buildField(table, field) {
let ts_type = (0, ts_type_1.toTsType)(field.type);
if (field.is_null) {
ts_type = 'null | ' + ts_type;
}
return {
table,
name: field.name,
ts_type,
reference: field.references,
alias: field.name.endsWith('_id')
? null
: makeFieldAlias(table.name, field.name),
};
}
function makeJoinSelection(schema, columns) {
const joins = [];
const selectedTables = new Set();
const selectedFields = [];
for (const column of columns) {
const table = getTable(schema, column.table);
const field = getField(table, column.field);
selectedTables.add(table);
selectedFields.push(field);
if (field.reference) {
const reference = makeReference(schema, field.reference);
joins.push({
left: field,
right: reference,
as: makeAsTableAlias(field, reference.table),
});
}
}
if (selectedTables.size == 1 && joins.length == 0) {
removeAllFieldAliases(selectedFields);
}
return {
joins: removeUnnecessaryJoins(joins, selectedTables),
selectedTables,
selectedFields,
};
}
function makeReference(schema, reference) {
const table = getTable(schema, reference.table);
const field = getField(table, reference.field);
return field;
}
function makeAsTableAlias(field, table) {
if (field.name == 'id') {
return null;
}
const asTable = field.name.replace(/_id$/, '');
if (field.reference?.table == field.table.name) {
return asTable == table.name ? asTable + '2' : asTable;
}
return asTable == table.name ? null : asTable;
}
function removeAllFieldAliases(fields) {
for (const field of fields) {
field.alias = null;
}
}
function removeUnnecessaryJoins(joins, selectedTables) {
return joins.filter(join => selectedTables.has(join.left.table) &&
selectedTables.has(join.right.table));
}
function markFieldAliases(selectedFields) {
const fieldsByName = countFieldNames(selectedFields);
for (const fields of fieldsByName.values()) {
if (fields.length > 1) {
for (const field of fields) {
field.alias = makeFieldAlias(field.table.name, field.name);
}
}
}
}
function countFieldNames(selectedFields) {
const fieldsByName = new Map();
for (const field of selectedFields) {
const fields = fieldsByName.get(field.name);
if (fields) {
fields.push(field);
}
else {
fieldsByName.set(field.name, [field]);
}
}
return fieldsByName;
}
function makeFieldAlias(tableName, fieldName) {
return tableName + '_' + fieldName;
}
function makeFrom(selection) {
const rightTables = new Set();
for (const join of selection.joins) {
if (join.left.table == join.right.table)
continue;
rightTables.add(join.right.table);
}
for (const table of selection.selectedTables) {
if (!rightTables.has(table)) {
return table;
}
}
throw new Error('Cannot determine left-most table to select from');
}
function makeJoins(fromTable, selection) {
const joins = [];
const pendingJoins = new Set(selection.joins);
const connectedTables = new Set();
connectedTables.add(fromTable);
for (;;) {
const oldPendingCount = pendingJoins.size;
for (const join of pendingJoins) {
if (connectedTables.has(join.left.table)) {
joins.push(join);
pendingJoins.delete(join);
connectedTables.add(join.right.table);
}
}
const newPendingCount = pendingJoins.size;
if (newPendingCount == 0)
break;
if (newPendingCount < oldPendingCount)
continue;
throw new JoinError(Array.from(pendingJoins));
}
return joins;
}
class JoinError extends Error {
pendingJoins;
constructor(pendingJoins) {
super(`Cannot determine join order, pending joined: ${Array.from(pendingJoins)
.map(join => {
const left = join.left.table.name;
const right = join.right.table.name;
return join.as
? `${left} join ${right} as ${join.as}`
: `${left} join ${right}`;
})
.join(', ')}`);
this.pendingJoins = pendingJoins;
}
}
function makeExpandedSelectFields(fromTable, joins, selectedFields) {
const expandedSelectFields = [];
const pickFromTable = (table, asTable) => {
for (const field of selectedFields) {
if (field.table == table) {
expandedSelectFields.push({
table: table.name,
field: field.name,
ts_type: field.ts_type,
table_alias: asTable,
field_alias: asTable
? makeFieldAlias(asTable, field.name)
: field.alias,
});
}
}
};
pickFromTable(fromTable, null);
for (const join of joins) {
pickFromTable(join.right.table, join.as);
}
return { fromTable, joins, expandedSelectFields };
}
function selectToTsType(select) {
let code = 'export type Row = {';
for (const field of select.expandedSelectFields) {
const name = field.field_alias || field.field;
code += `\n ${name}: ${field.ts_type}`;
}
code += '\n}';
return code;
}
function selectToSQL(select) {
if (select.expandedSelectFields.length == 0)
return '';
let sql = 'select';
for (const field of select.expandedSelectFields) {
const table_name = field.table_alias || field.table;
sql += '\n, ' + table_name + '.' + field.field;
if (field.field_alias) {
sql += ' as ' + field.field_alias;
}
}
// first select column don't need to start with comma
sql = sql.replace(',', ' '); // re
sql += '\nfrom ' + select.fromTable.name;
for (const join of select.joins) {
sql += '\n' + joinToSQL(join);
}
return sql;
}
function joinToSQL(join) {
let rightTable = join.right.table.name;
let sql = 'inner join ' + rightTable;
if (join.as) {
rightTable = join.as;
sql += ' as ' + rightTable;
}
sql += ' on ' + rightTable + '.' + join.right.name;
sql += ' = ' + join.left.table.name + '.' + join.left.name;
return sql;
}
function selectToKnex(select) {
if (select.expandedSelectFields.length == 0)
return '';
let knex = 'knex';
knex += `\n .from('${select.fromTable.name}')`;
for (const join of select.joins) {
knex += `\n ` + joinToKnex(join);
}
knex += '\n .select(';
for (const field of select.expandedSelectFields) {
const table_name = field.table_alias || field.table;
knex += "\n '" + table_name + '.' + field.field;
if (field.field_alias) {
knex += ' as ' + field.field_alias;
}
knex += "',";
}
knex += '\n )';
return knex;
}
function joinToKnex(join) {
let rightTable = join.right.table.name;
let knex = ".innerJoin('" + rightTable;
if (join.as) {
rightTable = join.as;
knex += ' as ' + rightTable;
}
knex += "', '" + rightTable + '.' + join.right.name;
knex += "', '" + join.left.table.name + '.' + join.left.name + "')";
return knex;
}
;