UNPKG

knex-schema-inspector

Version:

Utility for extracting information about existing DB schema

267 lines (229 loc) 7.62 kB
import { Knex } from 'knex'; import flatten from 'lodash.flatten'; import { SchemaInspector } from '../types/schema-inspector'; import { Table } from '../types/table'; import { Column } from '../types/column'; import extractMaxLength from '../utils/extract-max-length'; import extractType from '../utils/extract-type'; import { ForeignKey } from '../types/foreign-key'; import { stripQuotes } from '../utils/strip-quotes'; type RawColumn = { cid: number; name: string; type: string; notnull: 0 | 1; unique: 0 | 1; dflt_value: any; pk: 0 | 1; hidden: number; }; type RawForeignKey = { id: number; seq: number; table: string; from: string; to: string; on_update: ForeignKey['on_update']; on_delete: ForeignKey['on_delete']; match: string; }; export function parseDefaultValue(value: string | null): string | null { if (value === null || value.trim().toLowerCase() === 'null') return null; return stripQuotes(value); } export default class SQLite implements SchemaInspector { knex: Knex; constructor(knex: Knex) { this.knex = knex; } // Tables // =============================================================================================== /** * List all existing tables in the current schema/database */ async tables(): Promise<string[]> { const records = await this.knex .select('name') .from('sqlite_master') .whereRaw(`type = 'table' AND name NOT LIKE 'sqlite_%'`); return records.map(({ name }) => name) as string[]; } /** * Get the table info for a given table. If table parameter is undefined, it will return all tables * in the current schema/database */ tableInfo(): Promise<Table[]>; tableInfo(table: string): Promise<Table>; async tableInfo(table?: string) { const query = this.knex .select('name', 'sql') .from('sqlite_master') .where({ type: 'table' }) .andWhereRaw(`name NOT LIKE 'sqlite_%'`); if (table) { query.andWhere({ name: table }); } let records = await query; records = records.map((table) => ({ name: table.name, sql: table.sql, })); if (table) { return records[0]; } return records; } /** * Check if a table exists in the current schema/database */ async hasTable(table: string): Promise<boolean> { const results = await this.knex .select(1) .from('sqlite_master') .where({ type: 'table', name: table }); return results.length > 0; } // Columns // =============================================================================================== /** * Get all the available columns in the current schema/database. Can be filtered to a specific table */ async columns(table?: string): Promise<{ table: string; column: string }[]> { if (table) { const columns = await this.knex.raw<RawColumn[]>( `PRAGMA table_xinfo(??)`, table ); return columns.map((column) => ({ table, column: column.name, })); } const tables = await this.tables(); const columnsPerTable = await Promise.all( tables.map(async (table) => await this.columns(table)) ); return flatten(columnsPerTable); } /** * Get the column info for all columns, columns in a given table, or a specific column. */ columnInfo(): Promise<Column[]>; columnInfo(table: string): Promise<Column[]>; columnInfo(table: string, column: string): Promise<Column>; async columnInfo(table?: string, column?: string) { const getColumnsForTable = async (table: string): Promise<Column[]> => { const tablesWithAutoIncrementPrimaryKeys = ( await this.knex .select('name') .from('sqlite_master') .whereRaw(`sql LIKE '%AUTOINCREMENT%'`) ).map(({ name }) => name); const columns: RawColumn[] = await this.knex.raw( `PRAGMA table_xinfo(??)`, table ); const foreignKeys = await this.knex.raw< { table: string; from: string; to: string }[] >(`PRAGMA foreign_key_list(??)`, table); const indexList = await this.knex.raw< { name: string; unique: boolean }[] >(`PRAGMA index_list(??)`, table); const indexInfoList = await Promise.all( indexList.map((index) => this.knex.raw<{ seqno: number; cid: number; name: string }[]>( `PRAGMA index_info(??)`, index.name ) ) ); return columns.map((raw): Column => { const foreignKey = foreignKeys.find((fk) => fk.from === raw.name); const indexIndex = indexInfoList.findIndex((list) => list.find((fk) => fk.name === raw.name) ); const index = indexList[indexIndex]; const indexInfo = indexInfoList[indexIndex]; return { name: raw.name, table: table, data_type: extractType(raw.type), default_value: parseDefaultValue(raw.dflt_value), max_length: extractMaxLength(raw.type), /** @NOTE SQLite3 doesn't support precision/scale */ numeric_precision: null, numeric_scale: null, is_generated: raw.hidden !== 0, generation_expression: null, is_nullable: raw.notnull === 0, is_unique: !!index?.unique && indexInfo?.length === 1, is_primary_key: raw.pk === 1, has_auto_increment: raw.pk === 1 && tablesWithAutoIncrementPrimaryKeys.includes(table), foreign_key_column: foreignKey?.to || null, foreign_key_table: foreignKey?.table || null, }; }); }; if (!table) { const tables = await this.tables(); const columnsPerTable = await Promise.all( tables.map(async (table) => await getColumnsForTable(table)) ); return flatten(columnsPerTable); } if (table && !column) { return await getColumnsForTable(table); } const columns = await getColumnsForTable(table); return columns.find((columnInfo) => columnInfo.name === column); } /** * Check if a table exists in the current schema/database */ async hasColumn(table: string, column: string): Promise<boolean> { let isColumn = false; const results = await this.knex.raw( `SELECT COUNT(*) AS ct FROM pragma_table_xinfo('${table}') WHERE name='${column}'` ); const resultsVal = results[0]['ct']; if (resultsVal !== 0) { isColumn = true; } return isColumn; } /** * Get the primary key column for the given table */ async primary(table: string) { const columns = await this.knex.raw<RawColumn[]>( `PRAGMA table_xinfo(??)`, table ); const pkColumn = columns.find((col) => col.pk !== 0); return pkColumn?.name || null; } // Foreign Keys // =============================================================================================== async foreignKeys(table?: string): Promise<ForeignKey[]> { if (table) { const keys = await this.knex.raw(`PRAGMA foreign_key_list(??)`, table); return keys.map( (key: RawForeignKey): ForeignKey => ({ table, column: key.from, foreign_key_table: key.table, foreign_key_column: key.to, on_update: key.on_update, on_delete: key.on_delete, constraint_name: null, }) ); } const tables = await this.tables(); const keysPerTable = await Promise.all( tables.map(async (table) => await this.foreignKeys(table)) ); return flatten(keysPerTable); } }