"use strict"; // TODO: Make this its own independent package once the last bits have been polished (eg. consistent type names and onUpdate/onDelete values) const assert = require("assert"); module.exports = async function knexIntrospect(knex, tableName) { let clientType = knex.client.config.client; assert(clientType === "sqlite3" || clientType === "pg"); assert(/^[a-zA_Z_]+$/.test(tableName)); let safeTableName = tableName; // FIXME: Sanitize instead of assert? let tableLayout = {}; if (clientType === "sqlite3") { let [ columns, foreignKeys ] = await Promise.all([ knex.raw(`PRAGMA table_info('${safeTableName}')`), knex.raw(`PRAGMA foreign_key_list('${safeTableName}')`) ]); for (let column of columns) { tableLayout[column.name] = { name: column.name, type: column.type, notNull: (column.notnull === 1), isPrimaryKey: (column.pk === 1), foreignKey: null }; } for (let fk of foreignKeys) { tableLayout[fk.from].foreignKey = { table: fk.table, column: fk.to, onUpdate: fk.on_update, onDelete: fk.on_delete }; } } else if (clientType === "pg") { // Yes, this is a lot of queries. Yes, I "should" have used a single query with JOINs. If I did that, I would now have 1) a headache and 2) no working code. If this is something you care about, feel free to submit a PR. let tables = await knex("pg_class").limit(1).where({ relname: tableName }); assert(tables.length === 1); let table = tables[0]; let columns = await knex("pg_attribute") .where({ attrelid: table.oid }) .where("attnum", ">", 0); for (let column of columns) { let constraints = await knex("pg_constraint") .where({ conrelid: table.oid }) .whereRaw(`conkey = ARRAY[?::SMALLINT]`, [ column.attnum ]); let primaryKeyConstraint, foreignKey; for (let constraint of constraints) { if (constraint.contype === "p") { primaryKeyConstraint = constraint; } else if (constraint.contype === "f") { // TODO: Can there be multiple foreign key constraints? let foreignTable = await knex("pg_class").limit(1).where({ oid: constraint.confrelid }); // TODO: Support composite keys? assert(constraint.confkey.length === 1); let foreignColumn = await knex("pg_attribute") .limit(1) .where({ attrelid: constraint.confrelid }) .whereRaw(`attnum = ?::SMALLINT`, [ constraint.confkey[0] ]); foreignKey = { table: foreignTable[0].relname, column: foreignColumn[0].attname, // TODO: Convert possible values for onUpdate/onDelete into something consistent with SQLite, using match-value onUpdate: (constraint.confupdtype === "a") ? null : constraint.confupdtype, onDelete: (constraint.confdeltype === "a") ? null : constraint.confdeltype }; } } tableLayout[column.attname] = { name: column.attname, type: (await knex("pg_type").limit(1).where({ oid: column.atttypid }))[0].typname, notNull: column.attnotnull, isPrimaryKey: (primaryKeyConstraint != null), foreignKey: foreignKey }; } } return tableLayout; };