UNPKG

knex-automigrate

Version:

Table schema based database migration tool, built on top of the knex.js

363 lines 15.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const fs_1 = __importDefault(require("fs")); const knex_1 = __importDefault(require("knex")); const helper = __importStar(require("./index/helper")); const index_1 = require("./index/index"); const APPENDED_KEY = '__appended__'; function createSchemaRecorder() { const appended = []; return new Proxy({}, { get(_target, method) { if (method === APPENDED_KEY) { return appended; } if (typeof method !== 'string' || method === 'constructor') { return undefined; } return function recordedCall(a, b, c) { const child = createSchemaRecorder(); appended.push([child, method, [a, b, c]]); return child; }; }, }); } async function applyFulltextIndexesWithParser(db, tableName, indexes) { await Promise.all(indexes.map(async (index) => { const cols = Array.isArray(index.columns) ? index.columns : [index.columns]; const bindings = [tableName, index.name, ...cols]; const placeholders = cols.map(() => '??').join(', '); let sql = `ALTER TABLE ?? ADD FULLTEXT INDEX ?? (${placeholders})`; if (index.options.parser) { sql += ' WITH PARSER ??'; bindings.push(index.options.parser); } await db.raw(sql, bindings); })); } function resolveMethodAction(method, args, depth, context) { const { existIndexes, schemaIndexes, fulltextIndexesWithParser, columnName, } = context; if ((method === 'increments' || method === 'bigIncrements') && existIndexes.pk.length > 0) { return 'skip'; } if (method === 'index') { schemaIndexes.key.push(typeof args[0] === 'string' ? [args[0]] : args[0]); if (existIndexes.isIndexExists(args[0], args[1])) { return 'skip'; } if (args.length > 2 && args[2] && args[2].indexType === 'FULLTEXT' && args[2].parser) { fulltextIndexesWithParser.push({ columns: args[0], name: args[1], options: args[2] }); return 'skip'; } } if (method === 'references') { schemaIndexes.fk.push([columnName, typeof args[0] === 'string' ? [args[0]] : args[0]]); if (existIndexes.isForeignKeyExists(columnName, args[0])) { return 'no-apply'; } } if (method === 'unique') { const keys = args[0] || columnName; schemaIndexes.uk.push(typeof keys === 'string' ? [keys] : keys); if (existIndexes.isUniqueExists(args[0] || columnName)) { return depth === 0 ? 'skip' : 'no-apply'; } } if (method === 'primary') { const keys = args[0] || columnName; schemaIndexes.pk.push(typeof keys === 'string' ? [keys] : keys); if (existIndexes.isPrimaryKeyExists(args[0] || columnName)) { return depth === 0 ? 'skip' : 'no-apply'; } } return 'apply'; } function processSchemaEntry(builder, entry, depth, context) { if (!entry || !entry[0]) return null; const [childProxy, method, args] = entry; if (depth === 0) { context.columnName = args[0]; context.schemaColumns[args[0]] = true; } const action = resolveMethodAction(method, args, depth, context); if (action === 'skip') return builder; let result = builder; if (action === 'apply') { result = result[method](...args); } childProxy[APPENDED_KEY].forEach((childEntry) => { result = processSchemaEntry(result, childEntry, depth + 1, context); }); if (depth === 0) { if (method !== 'foreign' && method !== 'index' && method !== 'unique') { if (context.existColumns[context.columnName]) { result.alter(); } else if (context.prevColumnName) { result = result.after(context.prevColumnName); } else { result = result.first(); } } context.prevColumnName = context.columnName; } return result; } async function Automigrate(opts) { const db = (0, knex_1.default)(opts.config); try { // TODO: Migration PK (Y,N) // TODO: Migration Indexes (Y,N) // TODO: Migration Unique Attr. (Y,N) // TODO: Migration Reference Attr. (Y,N) const migrateTable = async (tableName, fn, initRows) => { const exists = await db.schema.hasTable(tableName); if (!exists) { const fulltextIndexesWithParser = []; await db.schema.createTable(tableName, (table) => { const customTable = Object.create(table); customTable.index = function indexWithParser(columns, name, indexOptions) { if (indexOptions && indexOptions.indexType === 'FULLTEXT' && indexOptions.parser) { fulltextIndexesWithParser.push({ columns, name, options: indexOptions }); return this; } return table.index.call(this, columns, name, indexOptions); }; fn(customTable); }); await applyFulltextIndexesWithParser(db, tableName, fulltextIndexesWithParser); } else { const existColumns = await db.from(tableName).columnInfo(); const existIndexes = await (0, index_1.loadIndexes)(db, tableName); const schemaColumns = {}; const schemaIndexes = { pk: [], uk: [], key: [], fk: [], }; const fulltextIndexesWithParser = []; await db.schema.alterTable(tableName, (table) => { const recorder = createSchemaRecorder(); fn(recorder); const context = { schemaColumns, schemaIndexes, existColumns, existIndexes, fulltextIndexesWithParser, columnName: undefined, prevColumnName: undefined, }; recorder[APPENDED_KEY].forEach((entry) => { processSchemaEntry(table, entry, 0, context); }); // Drop unused columns. const dropColumns = []; Object.keys(existColumns).forEach((col) => { if (!schemaColumns[col]) { Object.keys(existIndexes.fk).forEach((key) => { if (helper.isArrayEqual([col], existIndexes.fk[key].key)) { table.dropForeign([], key); } }); dropColumns.push(col); } }); if (dropColumns.length > 0) { if (opts.config.safe) { if (opts.verbose !== false) { console.warn(`* [Skip Drop Column${dropColumns.length > 1 ? 's' : ''}] \`${tableName}\``); console.warn(` ${'-'.repeat(20)}`); console.warn(` ALTER TABLE \`${tableName}\` \n ${dropColumns.map((c) => `DROP COLUMN \`${c}\``).join(',\n ')};`); console.warn(` ${'-'.repeat(20)}`); } } else { table.dropColumns(...dropColumns); } } // Drop unused indexes. Object.keys(existIndexes.key).forEach((key) => { const existKey = existIndexes.key[key]; let found = schemaIndexes.key.some((sKey) => helper.isArrayEqual(existKey, sKey)); if (!found) { found = schemaIndexes.fk.some((sKey) => { const normalized = typeof sKey[0] === 'string' ? [sKey[0]] : sKey[0]; return helper.isArrayEqual(existKey, normalized); }); } if (!found) { table.dropIndex([], key); } }); Object.keys(existIndexes.uk).forEach((key) => { const found = schemaIndexes.uk.some((sKey) => helper.isArrayEqual(existIndexes.uk[key], sKey)); if (!found) { table.dropUnique([], key); } }); }); await applyFulltextIndexesWithParser(db, tableName, fulltextIndexesWithParser); } if (typeof initRows === 'function') { const result = await db.count('* AS cnt').from(tableName); const existRows = result[0].cnt; if (!existRows) { const rows = initRows(opts.config); if (rows && rows.length) { await db.batchInsert(tableName, rows); } } } return tableName; }; const migrateView = async (viewName, fn) => { const connection = opts.config.connection; const exists = (await db('INFORMATION_SCHEMA.TABLES').select('TABLE_SCHEMA', 'TABLE_TYPE').where({ TABLE_SCHEMA: connection.database, TABLE_NAME: viewName, }))[0]; if (exists && exists.TABLE_TYPE === 'BASE TABLE') { throw new Error(`* ${viewName} must be a view, but a table already exists with this name.`); } if (!exists) { await db.schema.createView(viewName, (view) => { fn(view); }); } else { const recorder = createSchemaRecorder(); fn(recorder); const { schemaColumns } = recorder[APPENDED_KEY].reduce((acc, cur) => { if (cur[1] === 'columns') { if (acc.schemaColumns) throw new Error('Only one "columns" can be used.'); acc.schemaColumns = cur[2][0]; } if (cur[1] === 'as') { if (acc.schemaAs) throw new Error('Only one "as" can be used.'); acc.schemaAs = cur[2][0]; } return acc; }, { schemaColumns: null, schemaAs: null }); const existColumns = await db.from(viewName).columnInfo(); const dropColumns = Object.keys(existColumns) .filter((existColumn) => schemaColumns && !schemaColumns.includes(existColumn)); if (dropColumns.length > 0) { if (opts.verbose !== false) { console.warn(`* [Drop Column${dropColumns.length > 1 ? 's' : ''}] \`${viewName}\``); console.warn(` ${'-'.repeat(20)}`); console.warn(` ALTER VIEW \`${viewName}\` \n ${dropColumns.map((c) => `DROP COLUMN \`${c}\``).join(',\n ')};`); console.warn(` ${'-'.repeat(20)}`); } } await db.schema.createViewOrReplace(viewName, (view) => { fn(view); }); } return viewName; }; const migrations = { tables: [], views: [], }; const migrationPath = opts.cwd || process.cwd(); const tableMigrator = (a, b, c) => ({ call: async () => migrateTable(a, b, c), }); const viewMigrator = (a, b) => ({ call: async () => migrateView(a, b), }); fs_1.default.readdirSync(migrationPath).forEach((name) => { if (name.slice(0, 6) !== 'table_' && name.slice(0, 5) !== 'view_') return; const isTable = name.slice(0, 6) === 'table_'; // eslint-disable-next-line @typescript-eslint/no-var-requires const migration = require(`${migrationPath}/${name}`); migration.auto(isTable ? tableMigrator : viewMigrator, db).forEach((entry) => { if (isTable) { migrations.tables.push([name, entry]); } else { migrations.views.push([name, entry]); } }); }); if (opts.tables) { migrations.tables.push(...opts.tables(tableMigrator, db).map((table) => ['table_on_demand', table])); } if (opts.views) { migrations.views.push(...opts.views(viewMigrator, db).map((view) => ['view_on_demand', view])); } const convert = (type) => type.charAt(0).toUpperCase() + type.slice(1, -1); for (const type of Object.keys(migrations)) { if (migrations[type].length > 0) { for (let i = 0; i < migrations[type].length; i += 1) { try { const res = await migrations[type][i][1].call(); if (opts.verbose !== false) { console.info(`* ${convert(type)} \`${res}\` has been migrated.`); } } catch (err) { if (opts.verbose !== false) { console.error(`* ${convert(type)} \`${migrations[type][i][0]}\` migration failed.`); } throw err; } } } else { console.info(`* No ${convert(type)} schema exist.`); } } } finally { await db.destroy(); } } exports.default = Automigrate; //# sourceMappingURL=automigrate.js.map