knex-schema-inspector
Version:
Utility for extracting information about existing DB schema
267 lines (229 loc) • 7.62 kB
text/typescript
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);
}
}