@quory/core
Version:
Quickly extract relationships from any database
691 lines (674 loc) • 24.9 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
ConditionOperator: () => ConditionOperator,
areQueriesEqual: () => areQueriesEqual,
booleanConditionOperators: () => booleanConditionOperators,
findTableFromSchemas: () => findTableFromSchemas,
getCountForQuery: () => getCountForQuery,
getEntitiesAndJunctions: () => getEntitiesAndJunctions,
getRelationsForTable: () => getRelationsForTable,
getSchemas: () => getSchemas,
isBooleanCondition: () => isBooleanCondition,
isConditionComplete: () => isConditionComplete,
isListCondition: () => isListCondition,
isValueCondition: () => isValueCondition,
listConditionOperators: () => listConditionOperators,
parseToCompleteCondition: () => parseToCompleteCondition,
runQuery: () => runQuery,
splitTableRef: () => splitTableRef,
valueConditionOperators: () => valueConditionOperators
});
module.exports = __toCommonJS(index_exports);
// src/getSchemas.ts
async function getSchemas(databaseDriver) {
const schemas = [];
const retrieveOrCreateTable = (schemaName, tableName) => {
let schemaInfo = schemas.find(({ name }) => name === schemaName);
if (!schemaInfo) {
schemaInfo = { name: schemaName, tables: [] };
schemas.push(schemaInfo);
}
let tableInfo = schemaInfo.tables.find(({ name }) => name === tableName);
if (!tableInfo) {
tableInfo = { name: tableName, columns: [] };
schemaInfo.tables.push(tableInfo);
}
return tableInfo;
};
const allColumns = await databaseDriver.getAllColumnsInDatabase();
const allForeignKeys = await databaseDriver.getAllForeignKeysInDatabase();
allColumns.forEach(({ tableName, schemaName, ...column }) => {
const tableInfo = retrieveOrCreateTable(schemaName, tableName);
tableInfo.columns.push({
...column,
foreignKeys: allForeignKeys.filter(
(key) => key.localSchema === schemaName && key.localTable === tableName && key.localColumn === column.name
).map((key) => ({
foreignColumnName: key.foreignColumn,
foreignSchemaName: key.foreignSchema,
foreignTableName: key.foreignTable,
hasForeignKeyConstraint: true,
confidence: 1
})),
foreignKeyReferences: allForeignKeys.filter(
(key) => key.foreignSchema === schemaName && key.foreignTable === tableName && key.foreignColumn === column.name
).map((key) => ({
localColumnName: key.localColumn,
localSchemaName: key.localSchema,
localTableName: key.localTable,
hasForeignKeyConstraint: true,
confidence: 1
}))
});
});
return schemas;
}
// util/makeGraphForDatabase.ts
var import_graph_data_structure = require("graph-data-structure");
function makeGraphForDatabase(databaseSchemas, ignoreTables = []) {
const links = databaseSchemas.flatMap(
({ name: schemaName, tables }) => tables.filter(
({ name: tableName }) => !ignoreTables.some(
(table) => table.schemaName === schemaName && table.tableName === tableName
)
).flatMap(({ name: tableName, columns }) => {
return [
...columns.flatMap(
(column) => column.foreignKeys.map((key) => ({
source: `${schemaName}.${tableName}`,
target: `${key.foreignSchemaName}.${key.foreignTableName}`
}))
),
...columns.flatMap(
(column) => column.foreignKeyReferences.map((key) => ({
source: `${schemaName}.${tableName}`,
target: `${key.localSchemaName}.${key.localTableName}`
}))
)
];
})
);
const nodes = databaseSchemas.flatMap(
({ name: schemaName, tables }) => tables.map(({ name: tableName }) => `${schemaName}.${tableName}`)
);
const graph = new import_graph_data_structure.Graph();
nodes.forEach((node) => graph.addNode(node));
links.forEach(({ source, target }) => graph.addEdge(source, target));
return graph;
}
// src/util/findTableFromSchemas.ts
function findTableFromSchemas(schemas, schemaName, tableName) {
const schema = schemas.find(({ name }) => name === schemaName);
if (!schema) {
throw new Error(`Could not find schema ${schemaName}`);
}
const table = schema.tables.find(({ name }) => name === tableName);
if (!table) {
throw new Error(`Could not find table ${schemaName}.${tableName}`);
}
return {
...table,
schemaName
};
}
// src/util/getWhereClauseFromConditions.ts
function getWhereClauseFromConditions(table, tableAlias, where) {
if (!tableAlias) {
tableAlias = `${table.schemaName}.${table.name}`;
}
function makeCondition(condition) {
if ("column" in condition && !table.columns.some((column) => column.name === condition.column)) {
throw new Error(
`Where clause references column ${table.schemaName}.${table.name}.${condition.column} which does not exist`
);
}
switch (condition.operator) {
case "and" /* AND */:
case "or" /* OR */:
return `(${condition.conditions.map(makeCondition).join(` ${condition.operator.toUpperCase()} `)})`;
default:
return `${tableAlias}.${condition.column} ${condition.operator.toUpperCase()} ${"value" in condition ? `'${condition.value}'` : `(${condition.values.map((value) => `'${value}'`).join(",")})`}`;
}
}
return makeCondition(where);
}
// src/util/splitTableRef.ts
function splitTableRef(tableRef) {
const [schemaName, tableName] = tableRef.split(".");
if (!schemaName || !tableName) {
throw new Error(`Invalid tableRef: ${tableRef}`);
}
return [schemaName, tableName];
}
// src/util/getSelectForColumn.ts
function getSelectForColumn(table, tableAlias, column) {
return {
name: column.name,
ref: `${tableAlias}.${column.name}`,
subQueryAlias: `${tableAlias}__${column.name}`,
outerAlias: `${table.schemaName}__${table.name}__${column.name}`,
includeInOuter: true
};
}
// src/util/getShortestPath.ts
var import_graph_data_structure2 = require("graph-data-structure");
function getShortestPath(graph, sourceRef, targetRef, via) {
if (new Set(via).size !== via.length) {
throw new Error(`Duplicate table refs in "via" path: ${via.join(", ")}`);
}
if (via.includes(sourceRef)) {
throw new Error(`Via path includes source table ref: ${sourceRef}`);
}
if (via[via.length - 1] === targetRef) {
throw new Error(`Via path ends with target table ref: ${targetRef}`);
}
const desiredRoute = [sourceRef, ...via, targetRef];
try {
return [
sourceRef,
...desiredRoute.flatMap((thisTable, i, arr) => {
const nextTable = arr[i + 1];
if (!nextTable) {
return [];
}
const { nodes } = (0, import_graph_data_structure2.shortestPath)(graph, thisTable, nextTable);
return nodes.slice(1);
})
];
} catch {
throw new Error(`Couldn't find a path from ${sourceRef} to ${targetRef}`);
}
}
// src/prepareQuery.ts
var ConditionOperator = /* @__PURE__ */ ((ConditionOperator2) => {
ConditionOperator2["AND"] = "and";
ConditionOperator2["OR"] = "or";
ConditionOperator2["LIKE"] = "like";
ConditionOperator2["EQUALS"] = "=";
ConditionOperator2["GREATER_THAN"] = ">";
ConditionOperator2["LESS_THAN"] = "<";
ConditionOperator2["GREATER_THAN_OR_EQUAL"] = ">=";
ConditionOperator2["LESS_THAN_OR_EQUAL"] = "<=";
ConditionOperator2["NOT_EQUALS"] = "<>";
ConditionOperator2["NOT_LIKE"] = "not like";
ConditionOperator2["NOT_IN"] = "not in";
ConditionOperator2["IN"] = "in";
return ConditionOperator2;
})(ConditionOperator || {});
var booleanConditionOperators = [
"and" /* AND */,
"or" /* OR */
];
var isBooleanCondition = (condition) => booleanConditionOperators.includes(condition.operator);
var valueConditionOperators = [
"=" /* EQUALS */,
">" /* GREATER_THAN */,
"<" /* LESS_THAN */,
">=" /* GREATER_THAN_OR_EQUAL */,
"<=" /* LESS_THAN_OR_EQUAL */,
"<>" /* NOT_EQUALS */,
"like" /* LIKE */,
"not like" /* NOT_LIKE */
];
var isValueCondition = (condition) => valueConditionOperators.includes(condition.operator);
var listConditionOperators = [
"in" /* IN */,
"not in" /* NOT_IN */
];
var isListCondition = (condition) => listConditionOperators.includes(condition.operator);
async function prepareQuery(databaseSchemas, query) {
const { base } = query;
const [baseSchemaName, baseTableName] = splitTableRef(base.tableRef);
const baseTable = findTableFromSchemas(
databaseSchemas,
baseSchemaName,
baseTableName
);
if (baseTable.columns.every(
(column) => column.foreignKeys.length === 0 && column.foreignKeyReferences.length === 0
)) {
throw new Error(`No relationships found for table ${baseTableName}`);
}
const tableSeenCounts = {};
const getAlias = (tableRef) => {
tableSeenCounts[tableRef] = tableSeenCounts[tableRef] ?? 0;
tableSeenCounts[tableRef] += 1;
const [tableSchema, tableName] = splitTableRef(tableRef);
return `${tableSchema}__${tableName}__${tableSeenCounts[tableRef]}`;
};
const joinDefsWithAliases = function addAlias(join) {
return {
...join,
joinAlias: getAlias(join.tableRef),
joins: join.joins?.map((childJoin) => addAlias(childJoin)) ?? []
};
}(base);
const graph = makeGraphForDatabase(databaseSchemas);
const flattenedJoinDefs = function flattenJoinDef(join, parent) {
if (join.via && join.via[join.via.length - 1] === join.tableRef) {
throw new Error(
`Via path (${join.via.join(", ")}) for join ${join.tableRef} ends with table ref ${join.tableRef}`
);
}
const pathFromBase = parent ? [
...parent.pathFromBase.slice(0, parent.pathFromBase.length - 1),
...getShortestPath(
graph,
parent.tableRef,
join.tableRef,
join.via ?? []
)
] : [join.tableRef];
const ret = {
...join,
pathFromBase
};
const { joins: _, ...rest } = ret;
if (join.joins) {
for (const [index, childJoin] of join.joins.entries()) {
const match = join.joins.some(
({ tableRef }, i) => index !== i && tableRef === childJoin.tableRef
);
if (match) {
throw new Error(
`Duplicate table ref ${childJoin.tableRef}. Path: ${pathFromBase.join(" > ")} > ${childJoin.tableRef}`
);
}
}
return [
rest,
...(join.joins ?? []).flatMap(
(childJoin) => flattenJoinDef(childJoin, ret)
)
];
} else {
return [rest];
}
}(joinDefsWithAliases);
const joinTree = function buildTree(join) {
const joinsInSubTreeOfThisJoin = flattenedJoinDefs.filter(({ joinAlias }) => joinAlias !== join.joinAlias).filter((otherJoin) => {
return join.pathFromBase.every(
(tableRef, i) => otherJoin.pathFromBase[i] === tableRef
);
});
const childJoins = joinsInSubTreeOfThisJoin.filter(
({ pathFromBase }) => pathFromBase.length === join.pathFromBase.length + 1
);
for (const joinInSubTree of joinsInSubTreeOfThisJoin) {
const childNodeWithConnection = childJoins.find(
({ pathFromBase }) => joinInSubTree.pathFromBase.join().includes(pathFromBase.join())
);
if (!childNodeWithConnection) {
const childNodeTableRef = joinInSubTree.pathFromBase[join.pathFromBase.length];
childJoins.push({
tableRef: childNodeTableRef,
select: [],
joinAlias: getAlias(childNodeTableRef),
pathFromBase: [...join.pathFromBase, childNodeTableRef]
});
}
}
let select2 = join.select;
if (select2 === "*") {
const [schemaName, tableName] = splitTableRef(join.tableRef);
const table = findTableFromSchemas(
databaseSchemas,
schemaName,
tableName
);
select2 = table.columns.map(({ name }) => name);
}
return {
...join,
select: select2,
joins: childJoins.map(buildTree)
};
}(flattenedJoinDefs[0]);
const select = function extractSelects(join) {
const [schemaName, tableName] = splitTableRef(join.tableRef);
const table = findTableFromSchemas(databaseSchemas, schemaName, tableName);
return [
...table.columns.map((column) => getSelectForColumn(table, join.joinAlias, column)).filter((column) => join.select.includes(column.name)),
...join.joins.flatMap(extractSelects)
];
}(joinTree);
const conditions = function extractConditions(join) {
const [schemaName, tableName] = splitTableRef(join.tableRef);
const table = findTableFromSchemas(databaseSchemas, schemaName, tableName);
return [
...join.where ? [getWhereClauseFromConditions(table, join.joinAlias, join.where)] : [],
...join.joins.flatMap(extractConditions)
];
}(joinTree);
const orderBy = function extractOrderBy(join, priorities) {
return [
...join.orderBy ? join.orderBy.map(({ column, direction, priority }) => {
if (priorities && priorities.length >= 1 && !priority) {
throw new Error(
"Missing 'priority' field for orderBy. 'priority' must be specified when multiple orderBy conditions exist in the query."
);
}
if (priority && priorities && priorities.includes(priority)) {
throw new Error(
`Duplicate 'priority' field for orderBy: ${priority}`
);
}
return {
joinAlias: join.joinAlias,
column,
order: direction,
priority: priority || 0
};
}) : [],
...join.joins.flatMap(
(childJoin) => extractOrderBy(childJoin, priorities)
)
];
}(joinTree);
let fromClause = `${joinTree.tableRef} AS ${joinTree.joinAlias}`;
const primaryKeyRefs = [];
function traverseTree(thisJoin) {
const [schemaName, tableName] = splitTableRef(thisJoin.tableRef);
const thisTable = findTableFromSchemas(
databaseSchemas,
schemaName,
tableName
);
primaryKeyRefs.push(
...thisTable.columns.filter((column) => column.includedInPrimaryKey).map((column) => `${thisJoin.joinAlias}.${column.name}`)
);
for (const childJoin of thisJoin.joins) {
const [childSchemaName, childTableName] = splitTableRef(
childJoin.tableRef
);
const childTable = findTableFromSchemas(
databaseSchemas,
childSchemaName,
childTableName
);
const referenceFromThisTableToChildTable = thisTable.columns.flatMap(
(column) => column.foreignKeys.map((ref) => ({
...ref,
columnName: column.name
}))
).find(
(reference) => reference.foreignSchemaName === childSchemaName && reference.foreignTableName === childTableName
);
if (referenceFromThisTableToChildTable) {
fromClause += ` INNER JOIN ${childSchemaName}.${childTableName} AS ${childJoin.joinAlias} ON ${childJoin.joinAlias}.${referenceFromThisTableToChildTable.foreignColumnName} = ${thisJoin.joinAlias}.${referenceFromThisTableToChildTable.columnName}`;
} else {
const referenceFromChildTableToThisTable = childTable.columns.flatMap(
(column) => column.foreignKeys.map((ref) => ({
...ref,
columnName: column.name
}))
).find(
(reference) => reference.foreignSchemaName === schemaName && reference.foreignTableName === tableName
);
if (!referenceFromChildTableToThisTable) {
throw new Error(
`Could not find reference from ${childSchemaName}.${childTableName} to ${schemaName}.${tableName}. This should never happen.`
);
}
fromClause += ` INNER JOIN ${childSchemaName}.${childTableName} AS ${childJoin.joinAlias} ON ${childJoin.joinAlias}.${referenceFromChildTableToThisTable.columnName} = ${thisJoin.joinAlias}.${referenceFromChildTableToThisTable.foreignColumnName}`;
}
traverseTree(childJoin);
}
}
traverseTree(joinTree);
const sql = `SELECT ${select.map(
(column) => `${column.ref} AS ${column.outerAlias}`
)} FROM ${fromClause} ${conditions.length >= 1 ? `WHERE ${conditions.join(" AND ")}` : ""} ${primaryKeyRefs.length >= 1 ? `GROUP BY ${primaryKeyRefs.join(", ")}` : ""} ${orderBy.length >= 1 ? `ORDER BY ${orderBy.map(
(column) => `${column.joinAlias}.${column.column} ${column.order === "asc" ? "ASC" : "DESC"}`
)}` : ""}`;
const joinsList = function flattenJoinTree(join, parent = null) {
const joinWithParent = {
...join,
parent,
childJoins: join.joins.map((child) => ({
tableRef: child.tableRef,
joinAlias: child.joinAlias
}))
};
return [
joinWithParent,
...join.joins.flatMap((child) => flattenJoinTree(child, joinWithParent))
];
}(joinTree);
const preparedQuery = {
...query,
base: joinTree
};
return {
preparedQuery,
joinsList,
sql
};
}
// src/runQuery.ts
async function runQuery(databaseDriver, databaseSchemas, query) {
const {
sql: preparedSql,
preparedQuery,
joinsList
} = await prepareQuery(databaseSchemas, query);
let sql = preparedSql;
if (query.limit) {
sql += ` LIMIT ${query.limit}`;
}
const execResult = await databaseDriver.exec(sql).catch((err) => {
console.error(`Error while executing SQL:
${sql}
. Error below`);
throw err;
});
return {
meta: {
preparedQuery,
joinsList
},
sql,
rows: execResult.map((row) => {
return joinsList.map((join) => {
const [schemaName, tableName] = splitTableRef(join.tableRef);
const table = findTableFromSchemas(
databaseSchemas,
schemaName,
tableName
);
const columns = table.columns.filter(
(column) => join.select === "*" ? true : join.select.includes(column.name)
).map((column) => ({
name: column.name,
alias: getSelectForColumn(table, "THIS_CAN_BE_ANYTHING", column).outerAlias
}));
return {
joinAlias: join.joinAlias,
tableRef: join.tableRef,
data: Object.fromEntries(
columns.map((column) => {
return [column.name, row[column.alias]];
})
)
};
});
})
};
}
// src/getCountForQuery.ts
async function getCountForQuery(databaseDriver, databaseSchemas, query) {
const { sql, preparedQuery } = await prepareQuery(databaseSchemas, query);
const rows = await databaseDriver.exec(
`SELECT COUNT(*) AS count FROM (${sql})`
);
const count = Number(rows[0].count);
return {
count,
meta: {
preparedQuery
}
};
}
// src/getRelationsForTable.ts
var import_graph_data_structure3 = require("graph-data-structure");
function getRelationsForTable(databaseSchemas, schemaName, tableName, maxJoins) {
const table = findTableFromSchemas(databaseSchemas, schemaName, tableName);
if (table.columns.every(
(column) => column.foreignKeys.length === 0 && column.foreignKeyReferences.length === 0
)) {
throw new Error(
`No relationships found for table ${schemaName}.${tableName}`
);
}
const graph = makeGraphForDatabase(databaseSchemas);
return databaseSchemas.flatMap(
(schema) => schema.tables.filter(({ name }) => name !== tableName).map((_table) => ({ ..._table, schemaName: schema.name }))
).map((foreignTable) => {
try {
const { weight: shortestJoinPath } = (0, import_graph_data_structure3.shortestPath)(
graph,
`${schemaName}.${tableName}`,
`${foreignTable.schemaName}.${foreignTable.name}`
);
if (maxJoins && shortestJoinPath > maxJoins) {
return null;
}
return {
...foreignTable,
schemaName: foreignTable.schemaName,
shortestJoinPath
};
} catch {
return null;
}
}).filter((_table) => _table !== null);
}
// src/getEntitiesAndJunctions.ts
var import_lodash_es = require("lodash-es");
var import_pluralize = __toESM(require("pluralize"), 1);
var getRef = (table) => `${table.schemaName}.${table.name}`;
function getEntitiesAndJunctions(schemaRelationships) {
const allTables = schemaRelationships.flatMap(
(schema) => schema.tables.map((table) => ({
...table,
schemaName: schema.name
}))
);
const junctions = allTables.filter((table) => {
const columnsWithForeignKeys = table.columns.filter((column) => {
const foreignKeysPointingAtOtherTables = column.foreignKeys.filter(
(foreignKey) => !(foreignKey.foreignSchemaName === table.schemaName && foreignKey.foreignTableName === table.name)
);
return foreignKeysPointingAtOtherTables.length > 0;
});
if (columnsWithForeignKeys.length < 2) {
return false;
}
const tableNameResemblesJunction = columnsWithForeignKeys.every(
(column) => {
const [assumedEntityName] = (0, import_lodash_es.snakeCase)(column.name).split("_");
return assumedEntityName && (table.name.includes(assumedEntityName) || table.name.includes((0, import_pluralize.default)(assumedEntityName)));
}
);
return tableNameResemblesJunction;
}).map(getRef);
return {
junctions,
entities: allTables.map(getRef).filter((ref) => !junctions.includes(ref))
};
}
// src/util/isConditionComplete.ts
function isConditionComplete(condition) {
if (isListCondition(condition)) {
return Boolean(
condition.column && condition.operator && condition.values.length >= 1
);
}
if (isValueCondition(condition)) {
return Boolean(condition.column && condition.operator && condition.value);
}
return condition.conditions.every(isConditionComplete);
}
// src/util/parseToCompleteCondition.ts
function parseToCompleteCondition(condition) {
if (isBooleanCondition(condition)) {
const completeConditions = condition.conditions.filter(isConditionComplete);
if (completeConditions.length === 0) {
return null;
}
return {
...condition,
conditions: completeConditions
};
}
return isConditionComplete(condition) ? condition : null;
}
// src/util/areQueriesEqual.ts
function areQueriesEqual(schemas, queryA, queryB) {
const convertSelect = (join) => {
if (typeof join.select === "string") {
const [schemaName, tableName] = splitTableRef(join.tableRef);
const table = findTableFromSchemas(schemas, schemaName, tableName);
return table.columns.map(({ name }) => name);
}
return join.select;
};
const areJoinsEqual = (joinA, joinB) => {
if (!joinB) {
return false;
}
return joinA.tableRef === joinB.tableRef && convertSelect(joinA).join(",") === convertSelect(joinB).join(",") && JSON.stringify(joinA.via || []) === JSON.stringify(joinB.via || []) && JSON.stringify(joinA.where || {}) === JSON.stringify(joinB.where || {}) && JSON.stringify(joinA.orderBy || []) === JSON.stringify(joinB.orderBy || []) && (joinA.joins || []).every(
(subJoin, i) => areJoinsEqual(subJoin, (joinB.joins || [])[i])
);
};
return areJoinsEqual(queryA.base, queryB.base);
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
ConditionOperator,
areQueriesEqual,
booleanConditionOperators,
findTableFromSchemas,
getCountForQuery,
getEntitiesAndJunctions,
getRelationsForTable,
getSchemas,
isBooleanCondition,
isConditionComplete,
isListCondition,
isValueCondition,
listConditionOperators,
parseToCompleteCondition,
runQuery,
splitTableRef,
valueConditionOperators
});