diff --git a/.eslintignore b/.eslintignore index d479a95c..5b1583d3 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,5 @@ -lib/migration-template.js -lib/migration-template.ts node_modules dist +lib +templates +src diff --git a/.gitignore b/.gitignore index 8a9d4927..af55a89f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ *.log /migrations /node_modules -/dist*/** \ No newline at end of file +/dist +/lib diff --git a/.npmignore b/.npmignore index e524ecfa..e64a4f40 100644 --- a/.npmignore +++ b/.npmignore @@ -2,5 +2,6 @@ *.log migrations/ test/ +src/ mocha* renovate.json diff --git a/.prettierignore b/.prettierignore index 63d3fde8..f8a26871 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1 @@ -lib/migration-template.js \ No newline at end of file +templates diff --git a/.prettierrc.js b/.prettierrc.js index 225da3f1..95ab40f5 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -10,6 +10,10 @@ module.exports = { { files: '*.md', options: { parser: 'markdown' } + }, + { + files: '*.ts', + options: { parser: 'typescript' } } ] }; diff --git a/bin/node-pg-migrate b/bin/node-pg-migrate index 31b89483..4d831acf 100755 --- a/bin/node-pg-migrate +++ b/bin/node-pg-migrate @@ -277,8 +277,8 @@ if (action === 'create') { MIGRATIONS_FILE_LANGUAGE, IGNORE_PATTERN ) - .then(migration => { - console.log(util.format('Created migration -- %s', migration.path)); + .then(migrationPath => { + console.log(util.format('Created migration -- %s', migrationPath)); process.exit(0); }) .catch(err => { diff --git a/docs/triggers.md b/docs/triggers.md index b80b4713..1e3bcc2e 100644 --- a/docs/triggers.md +++ b/docs/triggers.md @@ -13,7 +13,7 @@ - `operation` _[string or array of strings]_ - `INSERT`, `UPDATE[ OF ...]`, `DELETE` or `TRUNCATE` - `constraint` _[boolean]_ - creates constraint trigger - `function` _[[Name](migrations.md#type)]_ - the name of procedure to execute - - `functionArgs` _[array]_ - parameters of the procedure + - `functionParams` _[array]_ - parameters of the procedure - `level` _[string]_ - `STATEMENT`, or `ROW` - `condition` _[string]_ - condition to met to execute trigger - `deferrable` _[boolean]_ - flag for deferrable constraint trigger diff --git a/index.d.ts b/index.d.ts index 70455548..0170dc7b 100644 --- a/index.d.ts +++ b/index.d.ts @@ -32,7 +32,7 @@ export interface ForeignKeyOptions extends ReferencesOptions { export interface ConstraintOptions { check?: string | string[] - unique?: Name[] | Name[][] + unique?: Name | Name[] | Name[][] primaryKey?: Name | Name[] foreignKeys?: ForeignKeyOptions | ForeignKeyOptions[] exclude?: string @@ -513,7 +513,7 @@ export interface MigrationBuilder { createView(viewName: Name, options: CreateViewOptions, definition: string): void dropView(viewName: Name, options?: DropOptions): void alterView(viewName: Name, options: AlterViewOptions): void - alterViewColumn(viewName: Name, options: AlterViewColumnOptions): void + alterViewColumn(viewName: Name, columnName: string, options: AlterViewColumnOptions): void renameView(viewName: Name, newViewName: Name): void createMaterializedView(viewName: Name, options: CreateMaterializedViewOptions, definition: string): void diff --git a/lib/db.js b/lib/db.js deleted file mode 100644 index 794ab81b..00000000 --- a/lib/db.js +++ /dev/null @@ -1,89 +0,0 @@ -/* - This file just manages the database connection and provides a query method - */ - -const pg = require('pg'); -// or native libpq bindings -// const pg = require('pg/native'); - -module.exports = (connection, log = console.error) => { - const isExternalClient = connection instanceof pg.Client; - let clientActive = false; - - const client = isExternalClient ? connection : new pg.Client(connection); - - const beforeCloseListeners = []; - - const createConnection = () => - new Promise((resolve, reject) => - clientActive || isExternalClient - ? resolve() - : client.connect(err => { - if (err) { - log('could not connect to postgres', err); - return reject(err); - } - clientActive = true; - return resolve(); - }) - ); - - const query = async (...args) => { - await createConnection(); - try { - return await client.query(...args); - } catch (err) { - const { message, position } = err; - const string = args[0].text || args[0]; - if (message && position >= 1) { - const endLineWrapIndexOf = string.indexOf('\n', position); - const endLineWrapPos = - endLineWrapIndexOf >= 0 ? endLineWrapIndexOf : string.length; - const stringStart = string.substring(0, endLineWrapPos); - const stringEnd = string.substr(endLineWrapPos); - const startLineWrapPos = stringStart.lastIndexOf('\n') + 1; - const padding = ' '.repeat(position - startLineWrapPos - 1); - log(`Error executing: -${stringStart} -${padding}^^^^${stringEnd} - -${message} -`); - } else { - log(`Error executing: -${string} -${err} -`); - } - throw err; - } - }; - - const select = async (...args) => { - const { rows } = await query(...args); - return rows; - }; - const column = async (columnName, ...args) => - (await select(...args)).map(r => r[columnName]); - - return { - createConnection, - query, - select, - column, - - addBeforeCloseListener: listener => beforeCloseListeners.push(listener), - - close: async () => { - await beforeCloseListeners.reduce( - (promise, listener) => - promise.then(listener).catch(err => log(err.stack || err)), - Promise.resolve() - ); - if (!isExternalClient) { - clientActive = false; - client.end(); - } - } - }; -}; diff --git a/lib/migration-builder.js b/lib/migration-builder.js deleted file mode 100644 index b6f1d063..00000000 --- a/lib/migration-builder.js +++ /dev/null @@ -1,202 +0,0 @@ -/* - The migration builder is used to actually create a migration from instructions - - A new instance of MigrationBuilder is instantiated and passed to the up or down block - of each migration when it is being run. - - It makes the methods available via the pgm variable and stores up the sql commands. - This is what makes it possible to do this without making everything async - and it makes inference of down migrations possible. - */ - -const { PgLiteral, createSchemalize } = require('./utils'); - -const extensions = require('./operations/extensions'); -const indexes = require('./operations/indexes'); -const tables = require('./operations/tables'); -const types = require('./operations/types'); -const roles = require('./operations/roles'); -const functions = require('./operations/functions'); -const triggers = require('./operations/triggers'); -const schemas = require('./operations/schemas'); -const domains = require('./operations/domains'); -const sequences = require('./operations/sequences'); -const operators = require('./operations/operators'); -const policies = require('./operations/policies'); -const views = require('./operations/views'); -const mViews = require('./operations/viewsMaterialized'); -const other = require('./operations/other'); - -/* eslint-disable security/detect-non-literal-fs-filename */ -module.exports = class MigrationBuilder { - constructor(db, typeShorthands, shouldDecamelize) { - this._steps = []; - this._REVERSE_MODE = false; - // by default, all migrations are wrapped in a transaction - this._use_transaction = true; - - // this function wraps each operation within a function that either - // calls the operation or its reverse, and appends the result (array of sql statements) - // to the steps array - const wrap = operation => (...args) => { - if (this._REVERSE_MODE && typeof operation.reverse !== 'function') { - const name = `pgm.${operation.name}()`; - throw new Error( - `Impossible to automatically infer down migration for "${name}"` - ); - } - this._steps = this._steps.concat( - this._REVERSE_MODE ? operation.reverse(...args) : operation(...args) - ); - }; - - const options = { - typeShorthands, - schemalize: createSchemalize(shouldDecamelize, false), - literal: createSchemalize(shouldDecamelize, true) - }; - - // defines the methods that are accessible via pgm in each migrations - // there are some convenience aliases to make usage easier - this.createExtension = wrap(extensions.createExtension(options)); - this.dropExtension = wrap(extensions.dropExtension(options)); - this.addExtension = this.createExtension; - - this.createTable = wrap(tables.createTable(options)); - this.dropTable = wrap(tables.dropTable(options)); - this.renameTable = wrap(tables.renameTable(options)); - this.alterTable = wrap(tables.alterTable(options)); - - this.addColumns = wrap(tables.addColumns(options)); - this.dropColumns = wrap(tables.dropColumns(options)); - this.renameColumn = wrap(tables.renameColumn(options)); - this.alterColumn = wrap(tables.alterColumn(options)); - this.addColumn = this.addColumns; - this.dropColumn = this.dropColumns; - - this.addConstraint = wrap(tables.addConstraint(options)); - this.dropConstraint = wrap(tables.dropConstraint(options)); - this.renameConstraint = wrap(tables.renameConstraint(options)); - this.createConstraint = this.addConstraint; - - this.createType = wrap(types.createType(options)); - this.dropType = wrap(types.dropType(options)); - this.addType = this.createType; - this.renameType = wrap(types.renameType(options)); - this.renameTypeAttribute = wrap(types.renameTypeAttribute(options)); - this.renameTypeValue = wrap(types.renameTypeValue(options)); - this.addTypeAttribute = wrap(types.addTypeAttribute(options)); - this.dropTypeAttribute = wrap(types.dropTypeAttribute(options)); - this.setTypeAttribute = wrap(types.setTypeAttribute(options)); - this.addTypeValue = wrap(types.addTypeValue(options)); - - this.createIndex = wrap(indexes.createIndex(options)); - this.dropIndex = wrap(indexes.dropIndex(options)); - this.addIndex = this.createIndex; - - this.createRole = wrap(roles.createRole(options)); - this.dropRole = wrap(roles.dropRole(options)); - this.alterRole = wrap(roles.alterRole(options)); - this.renameRole = wrap(roles.renameRole(options)); - - this.createFunction = wrap(functions.createFunction(options)); - this.dropFunction = wrap(functions.dropFunction(options)); - this.renameFunction = wrap(functions.renameFunction(options)); - - this.createTrigger = wrap(triggers.createTrigger(options)); - this.dropTrigger = wrap(triggers.dropTrigger(options)); - this.renameTrigger = wrap(triggers.renameTrigger(options)); - - this.createSchema = wrap(schemas.createSchema(options)); - this.dropSchema = wrap(schemas.dropSchema(options)); - this.renameSchema = wrap(schemas.renameSchema(options)); - - this.createDomain = wrap(domains.createDomain(options)); - this.dropDomain = wrap(domains.dropDomain(options)); - this.alterDomain = wrap(domains.alterDomain(options)); - this.renameDomain = wrap(domains.renameDomain(options)); - - this.createSequence = wrap(sequences.createSequence(options)); - this.dropSequence = wrap(sequences.dropSequence(options)); - this.alterSequence = wrap(sequences.alterSequence(options)); - this.renameSequence = wrap(sequences.renameSequence(options)); - - this.createOperator = wrap(operators.createOperator(options)); - this.dropOperator = wrap(operators.dropOperator(options)); - this.createOperatorClass = wrap(operators.createOperatorClass(options)); - this.dropOperatorClass = wrap(operators.dropOperatorClass(options)); - this.renameOperatorClass = wrap(operators.renameOperatorClass(options)); - this.createOperatorFamily = wrap(operators.createOperatorFamily(options)); - this.dropOperatorFamily = wrap(operators.dropOperatorFamily(options)); - this.renameOperatorFamily = wrap(operators.renameOperatorFamily(options)); - this.addToOperatorFamily = wrap(operators.addToOperatorFamily(options)); - this.removeFromOperatorFamily = wrap( - operators.removeFromOperatorFamily(options) - ); - - this.createPolicy = wrap(policies.createPolicy(options)); - this.dropPolicy = wrap(policies.dropPolicy(options)); - this.alterPolicy = wrap(policies.alterPolicy(options)); - this.renamePolicy = wrap(policies.renamePolicy(options)); - - this.createView = wrap(views.createView(options)); - this.dropView = wrap(views.dropView(options)); - this.alterView = wrap(views.alterView(options)); - this.alterViewColumn = wrap(views.alterViewColumn(options)); - this.renameView = wrap(views.renameView(options)); - - this.createMaterializedView = wrap(mViews.createMaterializedView(options)); - this.dropMaterializedView = wrap(mViews.dropMaterializedView(options)); - this.alterMaterializedView = wrap(mViews.alterMaterializedView(options)); - this.renameMaterializedView = wrap(mViews.renameMaterializedView(options)); - this.renameMaterializedViewColumn = wrap( - mViews.renameMaterializedViewColumn(options) - ); - this.refreshMaterializedView = wrap( - mViews.refreshMaterializedView(options) - ); - - this.sql = wrap(other.sql(options)); - - // Other utilities which may be useful - // .func creates a string which will not be escaped - // common uses are for PG functions, ex: { ... default: pgm.func('NOW()') } - this.func = PgLiteral.create; - - // expose DB so we can access database within transaction - const wrapDB = operation => (...args) => { - if (this._REVERSE_MODE) { - throw new Error('Impossible to automatically infer down migration'); - } - return operation(...args); - }; - this.db = { - query: wrapDB(db.query), - select: wrapDB(db.select) - }; - } - - enableReverseMode() { - this._REVERSE_MODE = true; - return this; - } - - noTransaction() { - this._use_transaction = false; - return this; - } - - isUsingTransaction() { - return this._use_transaction; - } - - getSql() { - return `${this.getSqlSteps().join('\n')}\n`; - } - - getSqlSteps() { - // in reverse mode, we flip the order of the statements - return this._REVERSE_MODE ? this._steps.slice().reverse() : this._steps; - } -}; -/* eslint-enable security/detect-non-literal-fs-filename */ diff --git a/lib/operations/extensions.js b/lib/operations/extensions.js deleted file mode 100644 index 4fda4723..00000000 --- a/lib/operations/extensions.js +++ /dev/null @@ -1,33 +0,0 @@ -const _ = require('lodash'); - -function dropExtension(mOptions) { - const _drop = (extensions, { ifExists, cascade } = {}) => { - if (!_.isArray(extensions)) extensions = [extensions]; // eslint-disable-line no-param-reassign - const ifExistsStr = ifExists ? ' IF EXISTS' : ''; - const cascadeStr = cascade ? ' CASCADE' : ''; - return _.map(extensions, extension => { - const extensionStr = mOptions.literal(extension); - return `DROP EXTENSION${ifExistsStr} ${extensionStr}${cascadeStr};`; - }); - }; - return _drop; -} - -function createExtension(mOptions) { - const _create = (extensions, { ifNotExists, schema } = {}) => { - if (!_.isArray(extensions)) extensions = [extensions]; // eslint-disable-line no-param-reassign - const ifNotExistsStr = ifNotExists ? ' IF NOT EXISTS' : ''; - const schemaStr = schema ? ` SCHEMA ${mOptions.literal(schema)}` : ''; - return _.map(extensions, extension => { - const extensionStr = mOptions.literal(extension); - return `CREATE EXTENSION${ifNotExistsStr} ${extensionStr}${schemaStr};`; - }); - }; - _create.reverse = dropExtension(mOptions); - return _create; -} - -module.exports = { - createExtension, - dropExtension -}; diff --git a/lib/operations/other.js b/lib/operations/other.js deleted file mode 100644 index 0181acb9..00000000 --- a/lib/operations/other.js +++ /dev/null @@ -1,18 +0,0 @@ -const { createTransformer } = require('../utils'); - -function sql(mOptions) { - const t = createTransformer(mOptions.literal); - return (...args) => { - // applies some very basic templating using the utils.p - let s = t(...args); - // add trailing ; if not present - if (s.lastIndexOf(';') !== s.length - 1) { - s += ';'; - } - return s; - }; -} - -module.exports = { - sql -}; diff --git a/package.json b/package.json index 7c57b5d5..a741d3dc 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,8 @@ "@babel/core": "7.6.4", "@babel/plugin-proposal-object-rest-spread": "7.6.2", "@babel/preset-env": "7.6.3", + "@types/lodash": "4.14.144", + "@types/mkdirp": "0.5.2", "babel-eslint": "10.0.3", "chai": "4.2.0", "chai-as-promised": "7.1.1", @@ -75,7 +77,8 @@ "prettier": "1.18.2", "proxyquire": "2.1.3", "sinon": "7.5.0", - "sinon-chai": "3.3.0" + "sinon-chai": "3.3.0", + "typescript": "3.6.4" }, "peerDependencies": { "pg": ">=4.3.0 <8.0.0" @@ -85,12 +88,13 @@ "dotenv": ">=1.0.0" }, "scripts": { - "compile": "babel lib/ -d dist/ && cp lib/migration-template.* dist/", + "build": "tsc", + "compile": "babel lib/ -d dist/", "test": "cross-env NODE_ENV=test mocha --opts ./mocha.opts test", "migrate": "node bin/node-pg-migrate", "lint": "eslint . bin/*", "lintfix": "npm run lint -- --fix && prettier --write *.json *.md docs/*.md", - "prepare": "npm run compile" + "prepare": "npm run build && npm run compile" }, "husky": { "hooks": { diff --git a/src/db.ts b/src/db.ts new file mode 100644 index 00000000..e91657f8 --- /dev/null +++ b/src/db.ts @@ -0,0 +1,136 @@ +/* + This file just manages the database connection and provides a query method + */ + +import { + Client, + ClientConfig, + QueryArrayResult, + QueryResult, + QueryArrayConfig, + QueryConfig, +} from 'pg'; +// or native libpq bindings +// const pg = require('pg/native'); + +// see ClientBase in @types/pg +export interface DB { + createConnection(): Promise; + + query(queryConfig: QueryArrayConfig, values?: any[]): Promise; + query(queryConfig: QueryConfig): Promise; + query(queryTextOrConfig: string | QueryConfig, values?: any[]): Promise; + + select(queryConfig: QueryArrayConfig, values?: any[]): Promise; + select(queryConfig: QueryConfig): Promise; + select(queryTextOrConfig: string | QueryConfig, values?: any[]): Promise; + + column(columnName: 'name',queryConfig: QueryArrayConfig, values?: any[]): Promise; + column(columnName: 'name',queryConfig: QueryConfig): Promise; + column(columnName: 'name',queryTextOrConfig: string | QueryConfig, values?: any[]): Promise; + + addBeforeCloseListener: (listener: any) => number; + close(): Promise; +} + +const db = ( + connection: Client | string | ClientConfig, + log = console.error +): DB => { + const isExternalClient = connection instanceof Client; + let clientActive = false; + + const client: Client = isExternalClient + ? (connection as Client) + : new Client(connection as string | ClientConfig); + + const beforeCloseListeners: any[] = []; + + const createConnection: () => Promise = () => + new Promise((resolve, reject) => + clientActive || isExternalClient + ? resolve() + : client.connect(err => { + if (err) { + log('could not connect to postgres', err); + return reject(err); + } + clientActive = true; + return resolve(); + }) + ); + + const query: DB['query'] = async ( + queryTextOrConfig: string | QueryConfig | QueryArrayConfig, + values?: any[], + ): Promise => { + await createConnection(); + try { + return await client.query(queryTextOrConfig, values); + } catch (err) { + const { message, position }: { message: string; position: number } = err; + const string: string = typeof queryTextOrConfig === 'string' ? queryTextOrConfig : queryTextOrConfig.text; + if (message && position >= 1) { + const endLineWrapIndexOf = string.indexOf('\n', position); + const endLineWrapPos = + endLineWrapIndexOf >= 0 ? endLineWrapIndexOf : string.length; + const stringStart = string.substring(0, endLineWrapPos); + const stringEnd = string.substr(endLineWrapPos); + const startLineWrapPos = stringStart.lastIndexOf('\n') + 1; + const padding = ' '.repeat(position - startLineWrapPos - 1); + log(`Error executing: +${stringStart} +${padding}^^^^${stringEnd} + +${message} +`); + } else { + log(`Error executing: +${string} +${err} +`); + } + throw err; + } + }; + + const select: DB['select'] = async ( + queryTextOrConfig: string | QueryConfig | QueryArrayConfig, + values?: any[], + ) => { + const { rows } = await query(queryTextOrConfig, values); + return rows; + }; + const column: DB['column'] = async ( + columnName: string, + queryTextOrConfig: string | QueryConfig | QueryArrayConfig, + values?: any[], + ) => { + const rows = await select(queryTextOrConfig, values) + return rows.map((r: { [key: string]: any }) => r[columnName]); + } + + return { + createConnection, + query, + select, + column, + + addBeforeCloseListener: listener => beforeCloseListeners.push(listener), + + close: async () => { + await beforeCloseListeners.reduce( + (promise, listener) => + promise.then(listener).catch((err: any) => log(err.stack || err)), + Promise.resolve() + ); + if (!isExternalClient) { + clientActive = false; + client.end(); + } + } + }; +}; + +export default db; +module.exports = db; diff --git a/src/definitions.ts b/src/definitions.ts new file mode 100644 index 00000000..5a4d1f05 --- /dev/null +++ b/src/definitions.ts @@ -0,0 +1,144 @@ +import { PgLiteral } from './utils'; +import { SequenceOptions } from './operations/sequences'; + +export type LiteralUnion = + | T + | (U & { zz_IGNORE_ME?: never }); + +interface ValueArray extends Array {} + +export type Value = null | boolean | string | number | PgLiteral | ValueArray; + +export type Type = string | { type: string }; + +export type Name = string | { schema?: string; name: string }; + +export type Action = + | 'NO ACTION' + | 'RESTRICT' + | 'CASCADE' + | 'SET NULL' + | 'SET DEFAULT'; + +// Note these currently don't contain the parameterized types like +// bit(n), varchar(n) and so on, they have to be specified as strings +export enum PgType { + BIGINT = 'bigint', // signed eight-byte integer + INT8 = 'int8', // alias for bigint + BIGSERIAL = 'bigserial', // autoincrementing eight-byte integer + BIT_1 = 'bit', // fixed-length bit string + BIT_VARYING = 'bit varying', // variable-length bit string + VARBIT = 'varbit', // alias for bit varying + SERIAL8 = 'serial8', // alias for bigserial + BOOLEAN = 'boolean', // logical Boolean (true/false) + BOOL = 'bool', // alias for boolean + BOX = 'box', // rectangular box on a plane + BYTEA = 'bytea', // binary data ("byte array") + CHARACTER = 'character', // fixed-length character string + CHAR = 'char', // alias for character + CHARACTER_VARYING = 'character varying', // variable-length character string + VARCHAR = 'varchar', // alias for character varying + CIDR = 'cidr', // IPv4 or IPv6 network address + CIRCLE = 'circle', // circle on a plane + DATE = 'date', // calendar date (year, month, day) + DOUBLE_PRECISION = 'double precision', // float8 double precision floating-point number (8 bytes) + INET = 'inet', // IPv4 or IPv6 host address + INTEGER = 'integer', // signed four-byte integer + INT = 'int', // alias for int + INT4 = 'int4', // alias for int + INTERVAL = 'interval', // time span + JSON = 'json', // textual JSON data + JSONB = 'jsonb', // binary JSON data, decomposed + LINE = 'line', // infinite line on a plane + LSEG = 'lseg', // line segment on a plane + MACADDR = 'macaddr', // MAC (Media Access Control) address + MONEY = 'money', // currency amount + NUMERIC = 'numeric', // exact numeric of selectable precision + PATH = 'path', // geometric path on a plane + PG_LSN = 'pg_lsn', // PostgreSQL Log Sequence Number + POINT = 'point', // geometric point on a plane + POLYGON = 'polygon', // closed geometric path on a plane + REAL = 'real', // single precision floating-point number (4 bytes) + FLOAT4 = 'float4', // alias for REAL + SMALLINT = 'smallint', // signed two-byte integer + INT2 = 'int2', // alias for smallint + SMALLSERIAL = 'smallserial', // autoincrementing two-byte integer + SERIAL2 = 'serial2', // alias for smallserial + SERIAL = 'serial', // autoincrementing four-byte integer + SERIAL4 = 'serial4', // alias for serial + TEXT = 'text', // variable-length character string + TIME = 'time', // time of day (no time zone) + TIME_WITHOUT_TIME_ZONE = 'without time zone', // alias of time + TIME_WITH_TIME_ZONE = 'time with time zone', // time of day, including time zone + TIMETZ = 'timetz', // alias of time with time zone + TIMESTAMP = 'timestamp', // date and time (no time zone) + TIMESTAMP_WITHOUT_TIME_ZONE = 'timestamp without time zone', // alias of timestamp + TIMESTAMP_WITH_TIME_ZONE = 'timestamp with time zone', // date and time, including time zone + TIMESTAMPTZ = 'timestamptz', // alias of timestamp with time zone + TSQUERY = 'tsquery', // text search query + TSVECTOR = 'tsvector', // text search document + TXID_SNAPSHOT = 'txid_snapshot', // user-level transaction ID snapshot + UUID = 'uuid', // universally unique identifier + XML = 'xml' // XML data +} + +export interface ReferencesOptions { + referencesConstraintName?: string; + referencesConstraintComment?: string; + references?: Name; + onDelete?: Action; + onUpdate?: Action; + match?: 'FULL' | 'SIMPLE'; +} + +export interface ColumnDefinition extends ReferencesOptions { + type: string; + collation?: string; + unique?: boolean; + primaryKey?: boolean; + notNull?: boolean; + default?: Value; + check?: string; + deferrable?: boolean; + deferred?: boolean; + comment?: string | null; + generated?: { precedence: 'ALWAYS' | 'BY DEFAULT' } & SequenceOptions; +} + +export interface ColumnDefinitions { + [name: string]: ColumnDefinition | string; +} + +export interface ShorthandDefinitions { + [name: string]: ColumnDefinition +} + +export type Like = + | 'COMMENTS' + | 'CONSTRAINTS' + | 'DEFAULTS' + | 'IDENTITY' + | 'INDEXES' + | 'STATISTICS' + | 'STORAGE' + | 'ALL'; + +export interface LikeOptions { + including?: Like | Like[]; + excluding?: Like | Like[]; +} + +export interface IfNotExistsOption { + ifNotExists?: boolean; +} + +export interface IfExistsOption { + ifExists?: boolean; +} + +export interface CascadeOption { + cascade?: boolean; +} + +export type AddOptions = IfNotExistsOption; +export type DropOptions = IfExistsOption & CascadeOption; diff --git a/src/migration-builder.ts b/src/migration-builder.ts new file mode 100644 index 00000000..503cb29b --- /dev/null +++ b/src/migration-builder.ts @@ -0,0 +1,628 @@ +/* + The migration builder is used to actually create a migration from instructions + + A new instance of MigrationBuilder is instantiated and passed to the up or down block + of each migration when it is being run. + + It makes the methods available via the pgm variable and stores up the sql commands. + This is what makes it possible to do this without making everything async + and it makes inference of down migrations possible. + */ + +import { DB } from './db'; +import { + AddOptions, + ColumnDefinitions, + ShorthandDefinitions, + DropOptions, + IfExistsOption, + LiteralUnion, + Name, + Type, + Value +} from './definitions'; +import { DomainOptionsAlter, DomainOptionsCreate } from './operations/domains'; +import { CreateExtensionOptions, Extension } from './operations/extensions'; +import { FunctionOptions, FunctionParam } from './operations/functions'; +import { CreateIndexOptions, DropIndexOptions } from './operations/indexes'; +import { + CreateOperatorClassOptions, + CreateOperatorOptions, + DropOperatorOptions, + OperatorListDefinition +} from './operations/operators'; +import { CreatePolicyOptions, PolicyOptions } from './operations/policies'; +import { RoleOptions } from './operations/roles'; +import { CreateSchemaOptions } from './operations/schemas'; +import { + SequenceOptionsAlter, + SequenceOptionsCreate +} from './operations/sequences'; +import { + AlterColumnOptions, + AlterTableOptions, + ConstraintOptions, + TableOptions +} from './operations/tables'; +import { TriggerOptions } from './operations/triggers'; +import { + AlterViewColumnOptions, + AlterViewOptions, + CreateViewOptions +} from './operations/views'; +import { createSchemalize, PgLiteral } from './utils'; + +import * as domains from './operations/domains'; +import * as extensions from './operations/extensions'; +import * as functions from './operations/functions'; +import * as indexes from './operations/indexes'; +import * as operators from './operations/operators'; +import * as other from './operations/other'; +import * as policies from './operations/policies'; +import * as roles from './operations/roles'; +import * as schemas from './operations/schemas'; +import * as sequences from './operations/sequences'; +import * as tables from './operations/tables'; +import * as triggers from './operations/triggers'; +import * as types from './operations/types'; +import * as views from './operations/views'; +import * as mViews from './operations/viewsMaterialized'; + +export type MigrationAction = (pgm: MigrationBuilder, run?: () => void) => Promise | void + +export interface MigrationBuilderActions { + up?: MigrationAction; + down?: MigrationAction; + shorthands?: ShorthandDefinitions; +} + +export interface MigrationOptions { + typeShorthands: ShorthandDefinitions; + schemalize: (v: Name) => string; + literal: (v: Name) => string; +} + +/* eslint-disable security/detect-non-literal-fs-filename */ +export default class MigrationBuilder { + public readonly createExtension: ( + extension: LiteralUnion | Array>, + options?: CreateExtensionOptions + ) => void; + public readonly dropExtension: ( + extension: LiteralUnion | Array>, + dropOptions?: DropOptions + ) => void; + public readonly addExtension: ( + extension: LiteralUnion | Array>, + options?: CreateExtensionOptions + ) => void; + + public readonly createTable: ( + tableName: Name, + columns: ColumnDefinitions, + options?: TableOptions + ) => void; + public readonly dropTable: ( + tableName: Name, + dropOptions?: DropOptions + ) => void; + public readonly renameTable: (tableName: Name, newtableName: Name) => void; + public readonly alterTable: ( + tableName: Name, + alterOptions: AlterTableOptions + ) => void; + + public readonly addColumns: ( + tableName: Name, + newColumns: ColumnDefinitions, + addOptions?: AddOptions + ) => void; + public readonly dropColumns: ( + tableName: Name, + columns: string | string[] | { [name: string]: any }, + dropOptions?: DropOptions + ) => void; + public readonly renameColumn: ( + tableName: Name, + oldColumnName: string, + newColumnName: string + ) => void; + public readonly alterColumn: ( + tableName: Name, + columnName: string, + options: AlterColumnOptions + ) => void; + public readonly addColumn: ( + tableName: Name, + newColumns: ColumnDefinitions, + addOptions?: AddOptions + ) => void; + public readonly dropColumn: ( + tableName: Name, + columns: string | string[] | { [name: string]: any }, + dropOptions?: DropOptions + ) => void; + + public readonly addConstraint: ( + tableName: Name, + constraintName: string | null, + expression: string | ConstraintOptions + ) => void; + public readonly dropConstraint: ( + tableName: Name, + constraintName: string, + options?: DropOptions + ) => void; + public readonly renameConstraint: ( + tableName: Name, + oldConstraintName: string, + newConstraintName: string + ) => void; + public readonly createConstraint: ( + tableName: Name, + constraintName: string | null, + expression: string | ConstraintOptions + ) => void; + + public readonly createType: ( + typeName: Name, + values: Value[] | { [name: string]: Type } + ) => void; + public readonly dropType: (typeName: Name, dropOptions?: DropOptions) => void; + public readonly addType: ( + typeName: Name, + values: Value[] | { [name: string]: Type } + ) => void; + public readonly renameType: (typeName: Name, newTypeName: Name) => void; + public readonly renameTypeAttribute: ( + typeName: Name, + attributeName: string, + newAttributeName: string + ) => void; + public readonly renameTypeValue: ( + typeName: Name, + value: string, + newValue: string + ) => void; + public readonly addTypeAttribute: ( + typeName: Name, + attributeName: string, + attributeType: Type + ) => void; + public readonly dropTypeAttribute: ( + typeName: Name, + attributeName: string, + options: IfExistsOption + ) => void; + public readonly setTypeAttribute: ( + typeName: Name, + attributeName: string, + attributeType: Type + ) => void; + public readonly addTypeValue: ( + typeName: Name, + value: Value, + options?: { + ifNotExists?: boolean; + before?: string; + after?: string; + } + ) => void; + + public readonly createIndex: ( + tableName: Name, + columns: string | string[], + options?: CreateIndexOptions + ) => void; + public readonly dropIndex: ( + tableName: Name, + columns: string | string[], + options?: DropIndexOptions + ) => void; + public readonly addIndex: ( + tableName: Name, + columns: string | string[], + options?: CreateIndexOptions + ) => void; + + public readonly createRole: ( + roleName: Name, + roleOptions?: RoleOptions + ) => void; + public readonly dropRole: (roleName: Name, options?: IfExistsOption) => void; + public readonly alterRole: (roleName: Name, roleOptions: RoleOptions) => void; + public readonly renameRole: (oldRoleName: Name, newRoleName: Name) => void; + + public readonly createFunction: ( + functionName: Name, + functionParams: FunctionParam[], + functionOptions: FunctionOptions, + definition: Value + ) => void; + public readonly dropFunction: ( + functionName: Name, + functionParams: FunctionParam[], + dropOptions?: DropOptions + ) => void; + public readonly renameFunction: ( + oldFunctionName: Name, + functionParams: FunctionParam[], + newFunctionName: Name + ) => void; + + public readonly createTrigger: + | (( + tableName: Name, + triggerName: Name, + triggerOptions: TriggerOptions + ) => void) + | (( + tableName: Name, + triggerName: Name, + triggerOptions: TriggerOptions & FunctionOptions, + definition: Value + ) => void); + public readonly dropTrigger: ( + tableName: Name, + triggerName: Name, + dropOptions?: DropOptions + ) => void; + public readonly renameTrigger: ( + tableName: Name, + oldTriggerName: Name, + newTriggerName: Name + ) => void; + + public readonly createSchema: ( + schemaName: string, + schemaOptions?: CreateSchemaOptions + ) => void; + public readonly dropSchema: ( + schemaName: string, + dropOptions?: DropOptions + ) => void; + public readonly renameSchema: ( + oldSchemaName: string, + newSchemaName: string + ) => void; + + public readonly createDomain: ( + domainName: Name, + type: Type, + domainOptions?: DomainOptionsCreate + ) => void; + public readonly dropDomain: ( + domainName: Name, + dropOptions?: DropOptions + ) => void; + public readonly alterDomain: ( + domainName: Name, + domainOptions: DomainOptionsAlter + ) => void; + public readonly renameDomain: ( + oldDomainName: Name, + newDomainName: Name + ) => void; + + public readonly createSequence: ( + sequenceName: Name, + sequenceOptions?: SequenceOptionsCreate + ) => void; + public readonly dropSequence: ( + sequenceName: Name, + dropOptions?: DropOptions + ) => void; + public readonly alterSequence: ( + sequenceName: Name, + sequenceOptions: SequenceOptionsAlter + ) => void; + public readonly renameSequence: ( + oldSequenceName: Name, + newSequenceName: Name + ) => void; + + public readonly createOperator: ( + operatorName: Name, + options?: CreateOperatorOptions + ) => void; + public readonly dropOperator: ( + operatorName: Name, + dropOptions?: DropOperatorOptions + ) => void; + public readonly createOperatorClass: ( + operatorClassName: Name, + type: Type, + indexMethod: string, + operatorList: OperatorListDefinition[], + options: CreateOperatorClassOptions + ) => void; + public readonly dropOperatorClass: ( + operatorClassName: Name, + indexMethod: string, + dropOptions?: DropOptions + ) => void; + public readonly renameOperatorClass: ( + oldOperatorClassName: Name, + indexMethod: string, + newOperatorClassName: Name + ) => void; + public readonly createOperatorFamily: ( + operatorFamilyName: Name, + indexMethod: string + ) => void; + public readonly dropOperatorFamily: ( + operatorFamilyName: Name, + newSchemaName: Name, + dropOptions?: DropOptions + ) => void; + public readonly renameOperatorFamily: ( + oldOperatorFamilyName: Name, + indexMethod: string, + newOperatorFamilyName: Name + ) => void; + public readonly addToOperatorFamily: ( + operatorFamilyName: Name, + indexMethod: string, + operatorList: OperatorListDefinition[] + ) => void; + public readonly removeFromOperatorFamily: ( + operatorFamilyName: Name, + indexMethod: string, + operatorList: OperatorListDefinition[] + ) => void; + + public readonly createPolicy: ( + tableName: Name, + policyName: string, + options?: CreatePolicyOptions + ) => void; + public readonly dropPolicy: ( + tableName: Name, + policyName: string, + options?: IfExistsOption + ) => void; + public readonly alterPolicy: ( + tableName: Name, + policyName: string, + options: PolicyOptions + ) => void; + public readonly renamePolicy: ( + tableName: Name, + policyName: string, + newPolicyName: string + ) => void; + + public readonly createView: ( + viewName: Name, + options: CreateViewOptions, + definition: string + ) => void; + public readonly dropView: (viewName: Name, options?: DropOptions) => void; + public readonly alterView: ( + viewName: Name, + options: AlterViewOptions + ) => void; + public readonly alterViewColumn: ( + viewName: Name, + columnName: string, + options: AlterViewColumnOptions + ) => void; + public readonly renameView: (viewName: Name, newViewName: Name) => void; + + public readonly createMaterializedView: ( + viewName: Name, + options: mViews.CreateMaterializedViewOptions, + definition: string + ) => void; + public readonly dropMaterializedView: ( + viewName: Name, + options?: DropOptions + ) => void; + public readonly alterMaterializedView: ( + viewName: Name, + options: mViews.AlterMaterializedViewOptions + ) => void; + public readonly renameMaterializedView: ( + viewName: Name, + newViewName: Name + ) => void; + public readonly renameMaterializedViewColumn: ( + viewName: Name, + columnName: string, + newColumnName: string + ) => void; + public readonly refreshMaterializedView: ( + viewName: Name, + options?: mViews.RefreshMaterializedViewOptions + ) => void; + + public readonly sql: (sql: string, args?: object) => void; + public readonly func: (sql: string) => PgLiteral; + public readonly db: Partial; + + private _steps: string[]; + private _REVERSE_MODE: boolean; + private _use_transaction: boolean; + + constructor( + db: DB, + typeShorthands: ShorthandDefinitions, + shouldDecamelize: boolean + ) { + this._steps = []; + this._REVERSE_MODE = false; + // by default, all migrations are wrapped in a transaction + this._use_transaction = true; + + interface Operation { + (...args: any[]): string | string[] + reverse?: (...args: any[]) => string | string[] + } + + // this function wraps each operation within a function that either + // calls the operation or its reverse, and appends the result (array of sql statements) + // to the steps array + const wrap = (operation: T) => ( + ...args: Parameters + ) => { + if (this._REVERSE_MODE && typeof operation.reverse !== 'function') { + const name = `pgm.${operation.name}()`; + throw new Error( + `Impossible to automatically infer down migration for "${name}"` + ); + } + this._steps = this._steps.concat( + this._REVERSE_MODE ? operation.reverse(...args) : operation(...args) + ); + }; + + const options: MigrationOptions = { + typeShorthands, + schemalize: createSchemalize(shouldDecamelize, false), + literal: createSchemalize(shouldDecamelize, true) + }; + + // defines the methods that are accessible via pgm in each migrations + // there are some convenience aliases to make usage easier + this.createExtension = wrap(extensions.createExtension(options)); + this.dropExtension = wrap(extensions.dropExtension(options)); + this.addExtension = this.createExtension; + + this.createTable = wrap(tables.createTable(options)); + this.dropTable = wrap(tables.dropTable(options)); + this.renameTable = wrap(tables.renameTable(options)); + this.alterTable = wrap(tables.alterTable(options)); + + this.addColumns = wrap(tables.addColumns(options)); + this.dropColumns = wrap(tables.dropColumns(options)); + this.renameColumn = wrap(tables.renameColumn(options)); + this.alterColumn = wrap(tables.alterColumn(options)); + this.addColumn = this.addColumns; + this.dropColumn = this.dropColumns; + + this.addConstraint = wrap(tables.addConstraint(options)); + this.dropConstraint = wrap(tables.dropConstraint(options)); + this.renameConstraint = wrap(tables.renameConstraint(options)); + this.createConstraint = this.addConstraint; + + this.createType = wrap(types.createType(options)); + this.dropType = wrap(types.dropType(options)); + this.addType = this.createType; + this.renameType = wrap(types.renameType(options)); + this.renameTypeAttribute = wrap(types.renameTypeAttribute(options)); + this.renameTypeValue = wrap(types.renameTypeValue(options)); + this.addTypeAttribute = wrap(types.addTypeAttribute(options)); + this.dropTypeAttribute = wrap(types.dropTypeAttribute(options)); + this.setTypeAttribute = wrap(types.setTypeAttribute(options)); + this.addTypeValue = wrap(types.addTypeValue(options)); + + this.createIndex = wrap(indexes.createIndex(options)); + this.dropIndex = wrap(indexes.dropIndex(options)); + this.addIndex = this.createIndex; + + this.createRole = wrap(roles.createRole(options)); + this.dropRole = wrap(roles.dropRole(options)); + this.alterRole = wrap(roles.alterRole(options)); + this.renameRole = wrap(roles.renameRole(options)); + + this.createFunction = wrap(functions.createFunction(options)); + this.dropFunction = wrap(functions.dropFunction(options)); + this.renameFunction = wrap(functions.renameFunction(options)); + + this.createTrigger = wrap(triggers.createTrigger(options)); + this.dropTrigger = wrap(triggers.dropTrigger(options)); + this.renameTrigger = wrap(triggers.renameTrigger(options)); + + this.createSchema = wrap(schemas.createSchema(options)); + this.dropSchema = wrap(schemas.dropSchema(options)); + this.renameSchema = wrap(schemas.renameSchema(options)); + + this.createDomain = wrap(domains.createDomain(options)); + this.dropDomain = wrap(domains.dropDomain(options)); + this.alterDomain = wrap(domains.alterDomain(options)); + this.renameDomain = wrap(domains.renameDomain(options)); + + this.createSequence = wrap(sequences.createSequence(options)); + this.dropSequence = wrap(sequences.dropSequence(options)); + this.alterSequence = wrap(sequences.alterSequence(options)); + this.renameSequence = wrap(sequences.renameSequence(options)); + + this.createOperator = wrap(operators.createOperator(options)); + this.dropOperator = wrap(operators.dropOperator(options)); + this.createOperatorClass = wrap(operators.createOperatorClass(options)); + this.dropOperatorClass = wrap(operators.dropOperatorClass(options)); + this.renameOperatorClass = wrap(operators.renameOperatorClass(options)); + this.createOperatorFamily = wrap(operators.createOperatorFamily(options)); + this.dropOperatorFamily = wrap(operators.dropOperatorFamily(options)); + this.renameOperatorFamily = wrap(operators.renameOperatorFamily(options)); + this.addToOperatorFamily = wrap(operators.addToOperatorFamily(options)); + this.removeFromOperatorFamily = wrap( + operators.removeFromOperatorFamily(options) + ); + + this.createPolicy = wrap(policies.createPolicy(options)); + this.dropPolicy = wrap(policies.dropPolicy(options)); + this.alterPolicy = wrap(policies.alterPolicy(options)); + this.renamePolicy = wrap(policies.renamePolicy(options)); + + this.createView = wrap(views.createView(options)); + this.dropView = wrap(views.dropView(options)); + this.alterView = wrap(views.alterView(options)); + this.alterViewColumn = wrap(views.alterViewColumn(options)); + this.renameView = wrap(views.renameView(options)); + + this.createMaterializedView = wrap(mViews.createMaterializedView(options)); + this.dropMaterializedView = wrap(mViews.dropMaterializedView(options)); + this.alterMaterializedView = wrap(mViews.alterMaterializedView(options)); + this.renameMaterializedView = wrap(mViews.renameMaterializedView(options)); + this.renameMaterializedViewColumn = wrap( + mViews.renameMaterializedViewColumn(options) + ); + this.refreshMaterializedView = wrap( + mViews.refreshMaterializedView(options) + ); + + this.sql = wrap(other.sql(options)); + + // Other utilities which may be useful + // .func creates a string which will not be escaped + // common uses are for PG functions, ex: { ... default: pgm.func('NOW()') } + this.func = PgLiteral.create; + + // expose DB so we can access database within transaction + const wrapDB = (operation: (...args: T[]) => R) => (...args: T[]) => { + if (this._REVERSE_MODE) { + throw new Error('Impossible to automatically infer down migration'); + } + return operation(...args); + }; + this.db = { + query: wrapDB(db.query), + select: wrapDB(db.select) + }; + } + + enableReverseMode(): this { + this._REVERSE_MODE = true; + return this; + } + + public noTransaction(): this { + this._use_transaction = false; + return this; + } + + isUsingTransaction(): boolean { + return this._use_transaction; + } + + getSql(): string { + return `${this.getSqlSteps().join('\n')}\n`; + } + + getSqlSteps(): string[] { + // in reverse mode, we flip the order of the statements + return this._REVERSE_MODE ? this._steps.slice().reverse() : this._steps; + } +} +/* eslint-enable security/detect-non-literal-fs-filename */ diff --git a/lib/migration.js b/src/migration.ts similarity index 68% rename from lib/migration.js rename to src/migration.ts index 9d2ae0b5..b95b49ca 100644 --- a/lib/migration.js +++ b/src/migration.ts @@ -6,19 +6,24 @@ */ -const fs = require('fs'); -const mkdirp = require('mkdirp'); -const path = require('path'); - -const MigrationBuilder = require('./migration-builder'); -const { getMigrationTableSchema, promisify } = require('./utils'); - -const readdir = promisify(fs.readdir); // eslint-disable-line security/detect-non-literal-fs-filename -const lstat = promisify(fs.lstat); // eslint-disable-line security/detect-non-literal-fs-filename +import fs from 'fs'; +import mkdirp from 'mkdirp'; +import path from 'path'; +import { DB } from './db'; +import { ShorthandDefinitions } from './definitions'; +import MigrationBuilder, { MigrationAction, MigrationBuilderActions } from './migration-builder'; +import { MigrationDirection, RunnerOption } from './runner'; +import { getMigrationTableSchema, promisify } from './utils'; + +const readdir = promisify(fs.readdir); // eslint-disable-line security/detect-non-literal-fs-filename +const lstat = promisify(fs.lstat); // eslint-disable-line security/detect-non-literal-fs-filename const SEPARATOR = '_'; -const loadMigrationFiles = async (dir, ignorePattern) => { +export const loadMigrationFiles = async ( + dir: string, + ignorePattern: string +) => { const dirContent = await readdir(`${dir}/`); const files = await Promise.all( dirContent.map(async file => { @@ -30,7 +35,7 @@ const loadMigrationFiles = async (dir, ignorePattern) => { return files.filter(i => i && !filter.test(i)).sort(); }; -const getLastSuffix = async (dir, ignorePattern) => { +const getLastSuffix = async (dir: string, ignorePattern: string) => { try { const files = await loadMigrationFiles(dir, ignorePattern); return files.length > 0 @@ -41,9 +46,20 @@ const getLastSuffix = async (dir, ignorePattern) => { } }; -module.exports = class Migration { +export interface RunMigration { + readonly path: string; + readonly name: string; + readonly timestamp: number; +} + +export class Migration implements RunMigration { // class method that creates a new migration file by cloning the migration template - static async create(name, directory, language, ignorePattern) { + static async create( + name: string, + directory: string, + language: 'js' | 'ts' | 'sql', + ignorePattern: string + ) { // ensure the migrations directory exists mkdirp.sync(directory); @@ -57,22 +73,32 @@ module.exports = class Migration { await new Promise(resolve => { // eslint-disable-next-line security/detect-non-literal-fs-filename fs.createReadStream( - path.resolve(__dirname, `./migration-template.${suffix}`) + path.resolve(__dirname, `../templates/migration-template.${suffix}`) ) // eslint-disable-next-line security/detect-non-literal-fs-filename .pipe(fs.createWriteStream(newFile)) .on('end', resolve); }); - return new Migration(null, newFile); + return newFile; } + public readonly db: DB; + public readonly path: string; + public readonly name: string; + public readonly timestamp: number; + public readonly up?: MigrationAction; + public down?: false | MigrationAction; + public readonly options: RunnerOption; + public readonly typeShorthands: ShorthandDefinitions; + public readonly log: typeof console.log; + constructor( - db, - migrationPath, - { up, down } = {}, - options = {}, - typeShorthands, + db: DB, + migrationPath: string, + { up, down }: MigrationBuilderActions, + options: RunnerOption, + typeShorthands?: ShorthandDefinitions, log = console.log ) { this.db = db; @@ -86,7 +112,7 @@ module.exports = class Migration { this.log = log; } - _getMarkAsRun(action) { + _getMarkAsRun(action: MigrationAction) { const schema = getMigrationTableSchema(this.options); const { migrationsTable } = this.options; const { name } = this; @@ -102,7 +128,7 @@ module.exports = class Migration { } } - async _apply(action, pgm) { + async _apply(action: MigrationAction, pgm: MigrationBuilder) { if (action.length === 2) { await new Promise(resolve => action(pgm, resolve)); } else { @@ -130,12 +156,12 @@ module.exports = class Migration { return sqlSteps.reduce( (promise, sql) => - promise.then(() => this.options.dryRun || this.db.query(sql)), + promise.then((): unknown => this.options.dryRun || this.db.query(sql)), Promise.resolve() ); } - _getAction(direction) { + _getAction(direction: MigrationDirection) { if (direction === 'down') { if (this.down === false) { throw new Error( @@ -148,7 +174,7 @@ module.exports = class Migration { } } - const action = this[direction]; + const action: MigrationAction | false = this[direction]; if (typeof action !== 'function') { throw new Error(`Unknown value for direction: ${direction}`); @@ -157,7 +183,7 @@ module.exports = class Migration { return action; } - apply(direction) { + apply(direction: MigrationDirection) { const pgm = new MigrationBuilder( this.db, this.typeShorthands, @@ -173,9 +199,11 @@ module.exports = class Migration { return this._apply(action, pgm); } - markAsRun(direction) { + markAsRun(direction: MigrationDirection) { return this.db.query(this._getMarkAsRun(this._getAction(direction))); } -}; +} +export default Migration; +module.exports = Migration; module.exports.loadMigrationFiles = loadMigrationFiles; diff --git a/lib/operations/domains.js b/src/operations/domains.ts similarity index 65% rename from lib/operations/domains.js rename to src/operations/domains.ts index 77c7d2a6..ea66841b 100644 --- a/lib/operations/domains.js +++ b/src/operations/domains.ts @@ -1,7 +1,28 @@ -const { applyType, escapeValue } = require('../utils'); +import { DropOptions, Name, Type, Value } from '../definitions'; +import { MigrationOptions } from '../migration-builder'; +import { applyType, escapeValue } from '../utils'; -function dropDomain(mOptions) { - const _drop = (domainName, { ifExists, cascade } = {}) => { +export interface DomainOptions { + default?: Value; + notNull?: boolean; + check?: string; + constraintName?: string; +} + +export interface DomainOptionsCreateEn { + collation?: string; +} + +export type DomainOptionsCreate = DomainOptionsCreateEn & DomainOptions; + +export interface DomainOptionsAlterEn { + allowNull?: boolean; +} + +export type DomainOptionsAlter = DomainOptionsAlterEn & DomainOptions; + +export function dropDomain(mOptions: MigrationOptions) { + const _drop = (domainName: Name, { ifExists, cascade }: DropOptions = {}) => { const ifExistsStr = ifExists ? ' IF EXISTS' : ''; const cascadeStr = cascade ? ' CASCADE' : ''; const domainNameStr = mOptions.literal(domainName); @@ -10,8 +31,12 @@ function dropDomain(mOptions) { return _drop; } -function createDomain(mOptions) { - const _create = (domainName, type, options = {}) => { +export function createDomain(mOptions: MigrationOptions) { + const _create = ( + domainName: Name, + type: Type, + options: DomainOptionsCreate = {} + ) => { const { default: defaultValue, collation, @@ -48,13 +73,13 @@ function createDomain(mOptions) { return `CREATE DOMAIN ${domainNameStr} AS ${typeStr}${constraintsStr};`; }; - _create.reverse = (domainName, type, options) => + _create.reverse = (domainName: Name, type: Type, options: DropOptions) => dropDomain(mOptions)(domainName, options); return _create; } -function alterDomain(mOptions) { - const _alter = (domainName, options) => { +export function alterDomain(mOptions: MigrationOptions) { + const _alter = (domainName: Name, options: DomainOptionsAlter) => { const { default: defaultValue, notNull, @@ -90,20 +115,13 @@ function alterDomain(mOptions) { return _alter; } -function renameDomain(mOptions) { - const _rename = (domainName, newDomainName) => { +export function renameDomain(mOptions: MigrationOptions) { + const _rename = (domainName: Name, newDomainName: Name) => { const domainNameStr = mOptions.literal(domainName); const newDomainNameStr = mOptions.literal(newDomainName); return `ALTER DOMAIN ${domainNameStr} RENAME TO ${newDomainNameStr};`; }; - _rename.reverse = (domainName, newDomainName) => + _rename.reverse = (domainName: Name, newDomainName: Name) => _rename(newDomainName, domainName); return _rename; } - -module.exports = { - dropDomain, - createDomain, - alterDomain, - renameDomain -}; diff --git a/src/operations/extensions.ts b/src/operations/extensions.ts new file mode 100644 index 00000000..7846d3f1 --- /dev/null +++ b/src/operations/extensions.ts @@ -0,0 +1,88 @@ +import _ from 'lodash'; +import { DropOptions, LiteralUnion } from '../definitions'; +import { MigrationOptions } from '../migration-builder'; + +export type Extension = + | 'adminpack' + | 'amcheck' + | 'auth_delay' + | 'auto_explain' + | 'bloom' + | 'btree_gin' + | 'btree_gist' + | 'citext' + | 'cube' + | 'dblink' + | 'dict_int' + | 'dict_xsyn' + | 'earthdistance' + | 'file_fdw' + | 'fuzzystrmatch' + | 'hstore' + | 'intagg' + | 'intarray' + | 'isn' + | 'lo' + | 'ltree' + | 'pageinspect' + | 'passwordcheck' + | 'pg_buffercache' + | 'pgcrypto' + | 'pg_freespacemap' + | 'pg_prewarm' + | 'pgrowlocks' + | 'pg_stat_statements' + | 'pgstattuple' + | 'pg_trgm' + | 'pg_visibility' + | 'postgres_fdw' + | 'seg' + | 'sepgsql' + | 'spi' + | 'sslinfo' + | 'tablefunc' + | 'tcn' + | 'test_decoding' + | 'tsm_system_rows' + | 'tsm_system_time' + | 'unaccent' + | 'uuid-ossp' + | 'xml2'; + +export interface CreateExtensionOptions { + ifNotExists?: boolean; + schema?: string; +} + +export function dropExtension(mOptions: MigrationOptions) { + const _drop = ( + extensions: LiteralUnion | Array>, + { ifExists, cascade }: DropOptions = {} + ) => { + if (!_.isArray(extensions)) extensions = [extensions]; // eslint-disable-line no-param-reassign + const ifExistsStr = ifExists ? ' IF EXISTS' : ''; + const cascadeStr = cascade ? ' CASCADE' : ''; + return _.map(extensions, extension => { + const extensionStr = mOptions.literal(extension); + return `DROP EXTENSION${ifExistsStr} ${extensionStr}${cascadeStr};`; + }); + }; + return _drop; +} + +export function createExtension(mOptions: MigrationOptions) { + const _create = ( + extensions: LiteralUnion | Array>, + { ifNotExists, schema }: CreateExtensionOptions = {} + ) => { + if (!_.isArray(extensions)) extensions = [extensions]; // eslint-disable-line no-param-reassign + const ifNotExistsStr = ifNotExists ? ' IF NOT EXISTS' : ''; + const schemaStr = schema ? ` SCHEMA ${mOptions.literal(schema)}` : ''; + return _.map(extensions, extension => { + const extensionStr = mOptions.literal(extension); + return `CREATE EXTENSION${ifNotExistsStr} ${extensionStr}${schemaStr};`; + }); + }; + _create.reverse = dropExtension(mOptions); + return _create; +} diff --git a/lib/operations/functions.js b/src/operations/functions.ts similarity index 56% rename from lib/operations/functions.js rename to src/operations/functions.ts index 33783d83..967b82f9 100644 --- a/lib/operations/functions.js +++ b/src/operations/functions.ts @@ -1,10 +1,31 @@ -const { escapeValue, formatParams } = require('../utils'); +import { DropOptions, Name, Value } from '../definitions'; +import { MigrationOptions } from '../migration-builder'; +import { escapeValue, formatParams } from '../utils'; -function dropFunction(mOptions) { +export interface FunctionParamType { + mode?: 'IN' | 'OUT' | 'INOUT' | 'VARIADIC'; + name?: string; + type: string; + default?: Value; +} + +export type FunctionParam = string | FunctionParamType; + +export interface FunctionOptions { + returns?: string; + language: string; + replace?: boolean; + window?: boolean; + behavior?: 'IMMUTABLE' | 'STABLE' | 'VOLATILE'; + onNull?: boolean; + parallel?: 'UNSAFE' | 'RESTRICTED' | 'SAFE'; +} + +export function dropFunction(mOptions: MigrationOptions) { const _drop = ( - functionName, - functionParams = [], - { ifExists, cascade } = {} + functionName: Name, + functionParams: FunctionParam[] = [], + { ifExists, cascade }: DropOptions = {} ) => { const ifExistsStr = ifExists ? ' IF EXISTS' : ''; const cascadeStr = cascade ? ' CASCADE' : ''; @@ -15,12 +36,12 @@ function dropFunction(mOptions) { return _drop; } -function createFunction(mOptions) { +export function createFunction(mOptions: MigrationOptions) { const _create = ( - functionName, - functionParams = [], - functionOptions = {}, - definition + functionName: Name, + functionParams: FunctionParam[] = [], + functionOptions: Partial = {}, + definition: Value ) => { const { replace, @@ -65,20 +86,21 @@ function createFunction(mOptions) { return _create; } -function renameFunction(mOptions) { - const _rename = (oldFunctionName, functionParams = [], newFunctionName) => { +export function renameFunction(mOptions: MigrationOptions) { + const _rename = ( + oldFunctionName: Name, + functionParams: FunctionParam[] = [], + newFunctionName: Name + ) => { const paramsStr = formatParams(functionParams, mOptions); const oldFunctionNameStr = mOptions.literal(oldFunctionName); const newFunctionNameStr = mOptions.literal(newFunctionName); return `ALTER FUNCTION ${oldFunctionNameStr}${paramsStr} RENAME TO ${newFunctionNameStr};`; }; - _rename.reverse = (oldFunctionName, functionParams, newFunctionName) => - _rename(newFunctionName, functionParams, oldFunctionName); + _rename.reverse = ( + oldFunctionName: Name, + functionParams: FunctionParam[], + newFunctionName: Name + ) => _rename(newFunctionName, functionParams, oldFunctionName); return _rename; } - -module.exports = { - createFunction, - dropFunction, - renameFunction -}; diff --git a/lib/operations/indexes.js b/src/operations/indexes.ts similarity index 67% rename from lib/operations/indexes.js rename to src/operations/indexes.ts index b33fce47..87995a17 100644 --- a/lib/operations/indexes.js +++ b/src/operations/indexes.ts @@ -1,13 +1,35 @@ -const _ = require('lodash'); +import _ from 'lodash'; +import { DropOptions, Name } from '../definitions'; +import { MigrationOptions } from '../migration-builder'; -function generateIndexName(table, columns, options) { +export interface CreateIndexOptions { + name?: string; + unique?: boolean; + where?: string; + concurrently?: boolean; + opclass?: string; + method?: 'btree' | 'hash' | 'gist' | 'spgist' | 'gin'; +} + +export interface DropIndexOptionsEn { + name?: string; + concurrently?: boolean; +} + +export type DropIndexOptions = DropIndexOptionsEn & DropOptions; + +function generateIndexName( + table: Name, + columns: string | string[], + options: CreateIndexOptions | DropIndexOptions +) { if (options.name) { return typeof table === 'object' ? { schema: table.schema, name: options.name } : options.name; } const cols = _.isArray(columns) ? columns.join('_') : columns; - const uniq = options.unique ? '_unique' : ''; + const uniq = 'unique' in options && options.unique ? '_unique' : ''; return typeof table === 'object' ? { schema: table.schema, @@ -16,7 +38,7 @@ function generateIndexName(table, columns, options) { : `${table}_${cols}${uniq}_index`; } -function generateColumnString(column, literal) { +function generateColumnString(column: string, literal: (v: Name) => string) { const openingBracketPos = column.indexOf('('); const closingBracketPos = column.indexOf(')'); const isFunction = @@ -26,14 +48,21 @@ function generateColumnString(column, literal) { : literal(column); // single column } -function generateColumnsString(columns, literal) { +function generateColumnsString( + columns: string | string[], + literal: (v: Name) => string +) { return _.isArray(columns) ? columns.map(column => generateColumnString(column, literal)).join(', ') : generateColumnString(columns, literal); } -function dropIndex(mOptions) { - const _drop = (tableName, columns, options = {}) => { +export function dropIndex(mOptions: MigrationOptions) { + const _drop = ( + tableName: Name, + columns: string | string[], + options: DropIndexOptions = {} + ) => { const { concurrently, ifExists, cascade } = options; const concurrentlyStr = concurrently ? ' CONCURRENTLY' : ''; const ifExistsStr = ifExists ? ' IF EXISTS' : ''; @@ -46,8 +75,12 @@ function dropIndex(mOptions) { return _drop; } -function createIndex(mOptions) { - const _create = (tableName, columns, options = {}) => { +export function createIndex(mOptions: MigrationOptions) { + const _create = ( + tableName: Name, + columns: string | string[], + options: CreateIndexOptions = {} + ) => { /* columns - the column, columns, or expression to create the index on @@ -80,8 +113,3 @@ function createIndex(mOptions) { _create.reverse = dropIndex(mOptions); return _create; } - -module.exports = { - createIndex, - dropIndex -}; diff --git a/lib/operations/operators.js b/src/operations/operators.ts similarity index 61% rename from lib/operations/operators.js rename to src/operations/operators.ts index a985de84..97eabc14 100644 --- a/lib/operations/operators.js +++ b/src/operations/operators.ts @@ -1,7 +1,41 @@ -const { formatParams, applyType } = require('../utils'); +import { Name, DropOptions, Type } from '../definitions'; +import { MigrationOptions } from '../migration-builder'; +import { applyType, formatParams } from '../utils'; +import { FunctionParam } from './functions'; -function dropOperator(mOptions) { - const _drop = (operatorName, options = {}) => { +export interface CreateOperatorOptions { + procedure: Name; + left?: Name; + right?: Name; + commutator?: Name; + negator?: Name; + restrict?: Name; + join?: Name; + hashes?: boolean; + merges?: boolean; +} + +export interface DropOperatorOptions { + left?: Name; + right?: Name; + ifExists?: boolean; + cascade?: boolean; +} + +export interface CreateOperatorClassOptions { + default?: boolean; + family?: string; +} + +export interface OperatorListDefinition { + type: 'function' | 'operator'; + number: number; + name: Name; + params?: FunctionParam[]; +} + +export function dropOperator(mOptions: MigrationOptions) { + const _drop = (operatorName: Name, options: DropOperatorOptions = {}) => { const { ifExists, cascade, left, right } = options; const operatorNameStr = mOptions.schemalize(operatorName); @@ -16,8 +50,8 @@ function dropOperator(mOptions) { return _drop; } -function createOperator(mOptions) { - const _create = (operatorName, options = {}) => { +export function createOperator(mOptions: MigrationOptions) { + const _create = (operatorName: Name, options: Partial = {}) => { const { procedure, left, @@ -63,11 +97,11 @@ function createOperator(mOptions) { return _create; } -function dropOperatorFamily(mOptions) { +export function dropOperatorFamily(mOptions: MigrationOptions) { const _drop = ( - operatorFamilyName, - indexMethod, - { ifExists, cascade } = {} + operatorFamilyName: Name, + indexMethod: string, + { ifExists, cascade }: DropOptions = {} ) => { const operatorFamilyNameStr = mOptions.literal(operatorFamilyName); const ifExistsStr = ifExists ? ' IF EXISTS' : ''; @@ -77,8 +111,8 @@ function dropOperatorFamily(mOptions) { return _drop; } -function createOperatorFamily(mOptions) { - const _create = (operatorFamilyName, indexMethod) => { +export function createOperatorFamily(mOptions: MigrationOptions) { + const _create = (operatorFamilyName: Name, indexMethod: string) => { const operatorFamilyNameStr = mOptions.literal(operatorFamilyName); return `CREATE OPERATOR FAMILY ${operatorFamilyNameStr} USING ${indexMethod};`; }; @@ -86,7 +120,12 @@ function createOperatorFamily(mOptions) { return _create; } -const operatorMap = mOptions => ({ type = '', number, name, params = [] }) => { +const operatorMap = (mOptions: MigrationOptions) => ({ + type, + number, + name, + params = [] +}: OperatorListDefinition) => { const nameStr = mOptions.literal(name); if (String(type).toLowerCase() === 'function') { if (params.length > 2) { @@ -105,8 +144,15 @@ const operatorMap = mOptions => ({ type = '', number, name, params = [] }) => { throw new Error('Operator "type" must be either "function" or "operator"'); }; -const changeOperatorFamily = (op, reverse) => mOptions => { - const method = (operatorFamilyName, indexMethod, operatorList) => { +const changeOperatorFamily = ( + op: 'ADD' | 'DROP', + reverse?: (mOptions: MigrationOptions) => any +) => (mOptions: MigrationOptions) => { + const method = ( + operatorFamilyName: Name, + indexMethod: string, + operatorList: OperatorListDefinition[] + ) => { const operatorFamilyNameStr = mOptions.literal(operatorFamilyName); const operatorListStr = operatorList .map(operatorMap(mOptions)) @@ -121,17 +167,17 @@ const changeOperatorFamily = (op, reverse) => mOptions => { return method; }; -const removeFromOperatorFamily = changeOperatorFamily('DROP'); -const addToOperatorFamily = changeOperatorFamily( +export const removeFromOperatorFamily = changeOperatorFamily('DROP'); +export const addToOperatorFamily = changeOperatorFamily( 'ADD', removeFromOperatorFamily ); -function renameOperatorFamily(mOptions) { +export function renameOperatorFamily(mOptions: MigrationOptions) { const _rename = ( - oldOperatorFamilyName, - indexMethod, - newOperatorFamilyName + oldOperatorFamilyName: Name, + indexMethod: string, + newOperatorFamilyName: Name ) => { const oldOperatorFamilyNameStr = mOptions.literal(oldOperatorFamilyName); const newOperatorFamilyNameStr = mOptions.literal(newOperatorFamilyName); @@ -139,18 +185,18 @@ function renameOperatorFamily(mOptions) { return `ALTER OPERATOR FAMILY ${oldOperatorFamilyNameStr} USING ${indexMethod} RENAME TO ${newOperatorFamilyNameStr};`; }; _rename.reverse = ( - oldOperatorFamilyName, - indexMethod, - newOperatorFamilyName + oldOperatorFamilyName: Name, + indexMethod: string, + newOperatorFamilyName: Name ) => _rename(newOperatorFamilyName, indexMethod, oldOperatorFamilyName); return _rename; } -function dropOperatorClass(mOptions) { +export function dropOperatorClass(mOptions: MigrationOptions) { const _drop = ( - operatorClassName, - indexMethod, - { ifExists, cascade } = {} + operatorClassName: Name, + indexMethod: string, + { ifExists, cascade }: DropOptions = {} ) => { const operatorClassNameStr = mOptions.literal(operatorClassName); const ifExistsStr = ifExists ? ' IF EXISTS' : ''; @@ -161,13 +207,13 @@ function dropOperatorClass(mOptions) { return _drop; } -function createOperatorClass(mOptions) { +export function createOperatorClass(mOptions: MigrationOptions) { const _create = ( - operatorClassName, - type, - indexMethod, - operatorList, - options + operatorClassName: Name, + type: Type, + indexMethod: string, + operatorList: OperatorListDefinition[], + options: CreateOperatorClassOptions ) => { const { default: isDefault, family } = options; const operatorClassNameStr = mOptions.literal(operatorClassName); @@ -183,36 +229,30 @@ function createOperatorClass(mOptions) { ${operatorListStr};`; }; _create.reverse = ( - operatorClassName, - type, - indexMethod, - operatorList, - options + operatorClassName: Name, + type: Type, + indexMethod: string, + operatorList: OperatorListDefinition[], + options: DropOptions ) => dropOperatorClass(mOptions)(operatorClassName, indexMethod, options); return _create; } -function renameOperatorClass(mOptions) { - const _rename = (oldOperatorClassName, indexMethod, newOperatorClassName) => { +export function renameOperatorClass(mOptions: MigrationOptions) { + const _rename = ( + oldOperatorClassName: Name, + indexMethod: string, + newOperatorClassName: Name + ) => { const oldOperatorClassNameStr = mOptions.literal(oldOperatorClassName); const newOperatorClassNameStr = mOptions.literal(newOperatorClassName); return `ALTER OPERATOR CLASS ${oldOperatorClassNameStr} USING ${indexMethod} RENAME TO ${newOperatorClassNameStr};`; }; - _rename.reverse = (oldOperatorClassName, indexMethod, newOperatorClassName) => - _rename(newOperatorClassName, indexMethod, oldOperatorClassName); + _rename.reverse = ( + oldOperatorClassName: Name, + indexMethod: string, + newOperatorClassName: Name + ) => _rename(newOperatorClassName, indexMethod, oldOperatorClassName); return _rename; } - -module.exports = { - createOperator, - dropOperator, - createOperatorFamily, - dropOperatorFamily, - removeFromOperatorFamily, - addToOperatorFamily, - renameOperatorFamily, - dropOperatorClass, - createOperatorClass, - renameOperatorClass -}; diff --git a/src/operations/other.ts b/src/operations/other.ts new file mode 100644 index 00000000..d91825e6 --- /dev/null +++ b/src/operations/other.ts @@ -0,0 +1,16 @@ +import { MigrationOptions } from '../migration-builder'; +import { createTransformer } from '../utils'; +import { Name } from '../definitions'; + +export function sql(mOptions: MigrationOptions) { + const t = createTransformer(mOptions.literal); + return (sql: string, args?: { [key: string]: Name }) => { + // applies some very basic templating using the utils.p + let s: string = t(sql, args); + // add trailing ; if not present + if (s.lastIndexOf(';') !== s.length - 1) { + s += ';'; + } + return s; + }; +} diff --git a/lib/operations/policies.js b/src/operations/policies.ts similarity index 52% rename from lib/operations/policies.js rename to src/operations/policies.ts index f268a801..b2f962fa 100644 --- a/lib/operations/policies.js +++ b/src/operations/policies.ts @@ -1,6 +1,21 @@ -const makeClauses = ({ role, using, check }) => { +import { IfExistsOption, Name } from '../definitions'; +import { MigrationOptions } from '../migration-builder'; + +export interface PolicyOptions { + role: string | string[]; + using: string; + check: string; +} + +export interface CreatePolicyOptionsEn { + command: 'ALL' | 'SELECT' | 'INSERT' | 'UPDATE' | 'DELETE'; +} + +export type CreatePolicyOptions = CreatePolicyOptionsEn & PolicyOptions; + +const makeClauses = ({ role, using, check }: Partial) => { const roles = (Array.isArray(role) ? role : [role]).join(', '); - const clauses = []; + const clauses: string[] = []; if (roles) { clauses.push(`TO ${roles}`); } @@ -13,8 +28,12 @@ const makeClauses = ({ role, using, check }) => { return clauses; }; -function dropPolicy(mOptions) { - const _drop = (tableName, policyName, { ifExists } = {}) => { +export function dropPolicy(mOptions: MigrationOptions) { + const _drop = ( + tableName: Name, + policyName: string, + { ifExists }: IfExistsOption = {} + ) => { const ifExistsStr = ifExists ? ' IF EXISTS' : ''; const policyNameStr = mOptions.literal(policyName); const tableNameStr = mOptions.literal(tableName); @@ -23,9 +42,13 @@ function dropPolicy(mOptions) { return _drop; } -function createPolicy(mOptions) { - const _create = (tableName, policyName, options = {}) => { - const createOptions = { +export function createPolicy(mOptions: MigrationOptions) { + const _create = ( + tableName: Name, + policyName: string, + options: Partial = {} + ) => { + const createOptions: Partial = { ...options, role: options.role || 'PUBLIC' }; @@ -42,8 +65,12 @@ function createPolicy(mOptions) { return _create; } -function alterPolicy(mOptions) { - const _alter = (tableName, policyName, options = {}) => { +export function alterPolicy(mOptions: MigrationOptions) { + const _alter = ( + tableName: Name, + policyName: string, + options: Partial = {} + ) => { const clausesStr = makeClauses(options).join(' '); const policyNameStr = mOptions.literal(policyName); const tableNameStr = mOptions.literal(tableName); @@ -52,21 +79,21 @@ function alterPolicy(mOptions) { return _alter; } -function renamePolicy(mOptions) { - const _rename = (tableName, policyName, newPolicyName) => { +export function renamePolicy(mOptions: MigrationOptions) { + const _rename = ( + tableName: Name, + policyName: string, + newPolicyName: string + ) => { const policyNameStr = mOptions.literal(policyName); const newPolicyNameStr = mOptions.literal(newPolicyName); const tableNameStr = mOptions.literal(tableName); return `ALTER POLICY ${policyNameStr} ON ${tableNameStr} RENAME TO ${newPolicyNameStr};`; }; - _rename.reverse = (tableName, policyName, newPolicyName) => - _rename(tableName, newPolicyName, policyName); + _rename.reverse = ( + tableName: Name, + policyName: string, + newPolicyName: string + ) => _rename(tableName, newPolicyName, policyName); return _rename; } - -module.exports = { - createPolicy, - dropPolicy, - alterPolicy, - renamePolicy -}; diff --git a/lib/operations/roles.js b/src/operations/roles.ts similarity index 72% rename from lib/operations/roles.js rename to src/operations/roles.ts index 37ad5c57..d1da5dcb 100644 --- a/lib/operations/roles.js +++ b/src/operations/roles.ts @@ -1,7 +1,26 @@ -const { isArray } = require('lodash'); -const { escapeValue } = require('../utils'); +import { isArray } from 'lodash'; +import { IfExistsOption, Name, Value } from '../definitions'; +import { MigrationOptions } from '../migration-builder'; +import { escapeValue } from '../utils'; -const formatRoleOptions = (roleOptions = {}) => { +export interface RoleOptions { + superuser?: boolean; + createdb?: boolean; + createrole?: boolean; + inherit?: boolean; + login?: boolean; + replication?: boolean; + bypassrls?: boolean; + limit?: number; + password?: Value; + encrypted?: boolean; + valid?: Value; + inRole?: string | string[]; + role?: string | string[]; + admin?: string | string[]; +} + +const formatRoleOptions = (roleOptions: RoleOptions = {}) => { const options = []; if (roleOptions.superuser !== undefined) { options.push(roleOptions.superuser ? 'SUPERUSER' : 'NOSUPERUSER'); @@ -60,8 +79,8 @@ const formatRoleOptions = (roleOptions = {}) => { return options.join(' '); }; -function dropRole(mOptions) { - const _drop = (roleName, { ifExists } = {}) => { +export function dropRole(mOptions: MigrationOptions) { + const _drop = (roleName: Name, { ifExists }: IfExistsOption = {}) => { const ifExistsStr = ifExists ? ' IF EXISTS' : ''; const roleNameStr = mOptions.literal(roleName); return `DROP ROLE${ifExistsStr} ${roleNameStr};`; @@ -69,8 +88,8 @@ function dropRole(mOptions) { return _drop; } -function createRole(mOptions) { - const _create = (roleName, roleOptions = {}) => { +export function createRole(mOptions: MigrationOptions) { + const _create = (roleName: Name, roleOptions: RoleOptions = {}) => { const options = formatRoleOptions({ ...roleOptions, superuser: roleOptions.superuser || false, @@ -87,8 +106,8 @@ function createRole(mOptions) { return _create; } -function alterRole(mOptions) { - const _alter = (roleName, roleOptions = {}) => { +export function alterRole(mOptions: MigrationOptions) { + const _alter = (roleName: Name, roleOptions: RoleOptions = {}) => { const options = formatRoleOptions(roleOptions); return options ? `ALTER ROLE ${mOptions.literal(roleName)} WITH ${options};` @@ -97,20 +116,13 @@ function alterRole(mOptions) { return _alter; } -function renameRole(mOptions) { - const _rename = (oldRoleName, newRoleName) => { +export function renameRole(mOptions: MigrationOptions) { + const _rename = (oldRoleName: Name, newRoleName: Name) => { const oldRoleNameStr = mOptions.literal(oldRoleName); const newRoleNameStr = mOptions.literal(newRoleName); return `ALTER ROLE ${oldRoleNameStr} RENAME TO ${newRoleNameStr};`; }; - _rename.reverse = (oldRoleName, newRoleName) => + _rename.reverse = (oldRoleName: Name, newRoleName: Name) => _rename(newRoleName, oldRoleName); return _rename; } - -module.exports = { - createRole, - dropRole, - alterRole, - renameRole -}; diff --git a/lib/operations/schemas.js b/src/operations/schemas.ts similarity index 54% rename from lib/operations/schemas.js rename to src/operations/schemas.ts index f632daab..bf487f64 100644 --- a/lib/operations/schemas.js +++ b/src/operations/schemas.ts @@ -1,5 +1,15 @@ -function dropSchema(mOptions) { - const _drop = (schemaName, { ifExists, cascade } = {}) => { +import { DropOptions, IfNotExistsOption, Name } from '../definitions'; +import { MigrationOptions } from '../migration-builder'; + +export interface CreateSchemaOptions extends IfNotExistsOption { + authorization?: string; +} + +export function dropSchema(mOptions: MigrationOptions) { + const _drop = ( + schemaName: string, + { ifExists, cascade }: DropOptions = {} + ) => { const ifExistsStr = ifExists ? ' IF EXISTS' : ''; const cascadeStr = cascade ? ' CASCADE' : ''; const schemaNameStr = mOptions.literal(schemaName); @@ -8,8 +18,11 @@ function dropSchema(mOptions) { return _drop; } -function createSchema(mOptions) { - const _create = (schemaName, { ifNotExists, authorization } = {}) => { +export function createSchema(mOptions: MigrationOptions) { + const _create = ( + schemaName: string, + { ifNotExists, authorization }: CreateSchemaOptions = {} + ) => { const ifNotExistsStr = ifNotExists ? ' IF NOT EXISTS' : ''; const schemaNameStr = mOptions.literal(schemaName); const authorizationStr = authorization @@ -21,19 +34,13 @@ function createSchema(mOptions) { return _create; } -function renameSchema(mOptions) { - const _rename = (schemaName, newSchemaName) => { +export function renameSchema(mOptions: MigrationOptions) { + const _rename = (schemaName: Name, newSchemaName: Name) => { const schemaNameStr = mOptions.literal(schemaName); const newSchemaNameStr = mOptions.literal(newSchemaName); return `ALTER SCHEMA ${schemaNameStr} RENAME TO ${newSchemaNameStr};`; }; - _rename.reverse = (schemaName, newSchemaName) => + _rename.reverse = (schemaName: Name, newSchemaName: Name) => _rename(newSchemaName, schemaName); return _rename; } - -module.exports = { - createSchema, - dropSchema, - renameSchema -}; diff --git a/lib/operations/sequences.js b/src/operations/sequences.ts similarity index 61% rename from lib/operations/sequences.js rename to src/operations/sequences.ts index 106b4c1b..cb8b4f3c 100644 --- a/lib/operations/sequences.js +++ b/src/operations/sequences.ts @@ -1,6 +1,35 @@ -const { applyType } = require('../utils'); +import { ShorthandDefinitions, DropOptions, Name, Type } from '../definitions'; +import { MigrationOptions } from '../migration-builder'; +import { applyType } from '../utils'; -const parseSequenceOptions = (typeShorthands, options) => { +export interface SequenceOptions { + type?: Type; + increment?: number; + minvalue?: number | null | false; + maxvalue?: number | null | false; + start?: number; + cache?: number; + cycle?: boolean; + owner?: string | null | false; +} + +export interface SequenceOptionsCreateEn { + temporary?: boolean; + ifNotExists?: boolean; +} + +export interface SequenceOptionsAlterEn { + restart?: number | true; +} + +export type SequenceOptionsCreate = SequenceOptionsCreateEn & SequenceOptions; + +export type SequenceOptionsAlter = SequenceOptionsAlterEn & SequenceOptions; + +export const parseSequenceOptions = ( + typeShorthands: ShorthandDefinitions, + options: SequenceOptions +) => { const { type, increment, @@ -11,7 +40,7 @@ const parseSequenceOptions = (typeShorthands, options) => { cycle, owner } = options; - const clauses = []; + const clauses: string[] = []; if (type) { clauses.push(`AS ${applyType(type, typeShorthands).type}`); } @@ -47,8 +76,11 @@ const parseSequenceOptions = (typeShorthands, options) => { return clauses; }; -function dropSequence(mOptions) { - const _drop = (sequenceName, { ifExists, cascade } = {}) => { +export function dropSequence(mOptions: MigrationOptions) { + const _drop = ( + sequenceName: Name, + { ifExists, cascade }: DropOptions = {} + ) => { const ifExistsStr = ifExists ? ' IF EXISTS' : ''; const cascadeStr = cascade ? ' CASCADE' : ''; const sequenceNameStr = mOptions.literal(sequenceName); @@ -57,8 +89,8 @@ function dropSequence(mOptions) { return _drop; } -function createSequence(mOptions) { - const _create = (sequenceName, options = {}) => { +export function createSequence(mOptions: MigrationOptions) { + const _create = (sequenceName: Name, options: SequenceOptionsCreate = {}) => { const { temporary, ifNotExists } = options; const temporaryStr = temporary ? ' TEMPORARY' : ''; const ifNotExistsStr = ifNotExists ? ' IF NOT EXISTS' : ''; @@ -74,8 +106,8 @@ function createSequence(mOptions) { return _create; } -function alterSequence(mOptions) { - return (sequenceName, options) => { +export function alterSequence(mOptions: MigrationOptions) { + return (sequenceName: Name, options: SequenceOptionsAlter) => { const { restart } = options; const clauses = parseSequenceOptions(mOptions.typeShorthands, options); if (restart) { @@ -90,21 +122,13 @@ function alterSequence(mOptions) { }; } -function renameSequence(mOptions) { - const _rename = (sequenceName, newSequenceName) => { +export function renameSequence(mOptions: MigrationOptions) { + const _rename = (sequenceName: Name, newSequenceName: Name) => { const sequenceNameStr = mOptions.literal(sequenceName); const newSequenceNameStr = mOptions.literal(newSequenceName); return `ALTER SEQUENCE ${sequenceNameStr} RENAME TO ${newSequenceNameStr};`; }; - _rename.reverse = (sequenceName, newSequenceName) => + _rename.reverse = (sequenceName: Name, newSequenceName: Name) => _rename(newSequenceName, sequenceName); return _rename; } - -module.exports = { - createSequence, - dropSequence, - alterSequence, - renameSequence, - parseSequenceOptions -}; diff --git a/lib/operations/tables.js b/src/operations/tables.ts similarity index 61% rename from lib/operations/tables.js rename to src/operations/tables.ts index bf56a700..824fdef6 100644 --- a/lib/operations/tables.js +++ b/src/operations/tables.ts @@ -1,16 +1,59 @@ -const _ = require('lodash'); -const { - escapeValue, +import _ from 'lodash'; +import { + AddOptions, + ColumnDefinition, + ColumnDefinitions, + DropOptions, + Like, + LikeOptions, + Name, + ReferencesOptions, + Value +} from '../definitions'; +import { MigrationOptions } from '../migration-builder'; +import { applyType, applyTypeAdapters, comment, + escapeValue, formatLines -} = require('../utils'); -const { parseSequenceOptions } = require('./sequences'); +} from '../utils'; +import { parseSequenceOptions, SequenceOptions } from './sequences'; + +export interface ForeignKeyOptions extends ReferencesOptions { + columns: Name | Name[]; +} + +export interface ConstraintOptions { + check?: string | string[]; + unique?: Name | Name[] | Name[][]; + primaryKey?: Name | Name[]; + foreignKeys?: ForeignKeyOptions | ForeignKeyOptions[]; + exclude?: string; + deferrable?: boolean; + deferred?: boolean; + comment?: string; +} -const parseReferences = (options, literal) => { +export interface TableOptions { + temporary?: boolean; + ifNotExists?: boolean; + inherits?: Name; + like?: Name | { table: Name; options?: LikeOptions }; + constraints?: ConstraintOptions; + comment?: string | null; +} + +export interface AlterTableOptions { + levelSecurity: 'DISABLE' | 'ENABLE' | 'FORCE' | 'NO FORCE'; +} + +const parseReferences = ( + options: ReferencesOptions, + literal: (v: Name) => string +) => { const { references, match, onDelete, onUpdate } = options; - const clauses = []; + const clauses: string[] = []; clauses.push( typeof references === 'string' && (references.startsWith('"') || references.endsWith(')')) @@ -29,17 +72,27 @@ const parseReferences = (options, literal) => { return clauses.join(' '); }; -const parseDeferrable = options => +const parseDeferrable = (options: { deferred?: boolean }) => `DEFERRABLE INITIALLY ${options.deferred ? 'DEFERRED' : 'IMMEDIATE'}`; -const parseColumns = (tableName, columns, mOptions) => { +const parseColumns = ( + tableName: Name, + columns: ColumnDefinitions, + mOptions: MigrationOptions +): { + columns: string[]; + constraints: { primaryKey?: string[] }; + comments: string[]; +} => { const extendingTypeShorthands = mOptions.typeShorthands; let columnsWithOptions = _.mapValues(columns, column => applyType(column, extendingTypeShorthands) ); const primaryColumns = _.chain(columnsWithOptions) - .map((options, columnName) => (options.primaryKey ? columnName : null)) + .map((options: ColumnDefinition, columnName) => + options.primaryKey ? columnName : null + ) .filter() .value(); const multiplePrimaryColumns = primaryColumns.length > 1; @@ -51,9 +104,9 @@ const parseColumns = (tableName, columns, mOptions) => { })); } - const comments = _.chain(columnsWithOptions) + const comments: string[] = _.chain(columnsWithOptions) .map( - (options, columnName) => + (options: ColumnDefinition, columnName) => typeof options.comment !== 'undefined' && comment( 'COLUMN', @@ -65,89 +118,99 @@ const parseColumns = (tableName, columns, mOptions) => { .value(); return { - columns: _.map(columnsWithOptions, (options, columnName) => { - const { - type, - collation, - default: defaultValue, - unique, - primaryKey, - notNull, - check, - references, - referencesConstraintName, - referencesConstraintComment, - deferrable, - generated - } = options; - const constraints = []; - if (collation) { - constraints.push(`COLLATE ${collation}`); - } - if (defaultValue !== undefined) { - constraints.push(`DEFAULT ${escapeValue(defaultValue)}`); - } - if (unique) { - constraints.push('UNIQUE'); - } - if (primaryKey) { - constraints.push('PRIMARY KEY'); - } - if (notNull) { - constraints.push('NOT NULL'); - } - if (check) { - constraints.push(`CHECK (${check})`); - } - if (references) { - const name = - referencesConstraintName || - (referencesConstraintComment ? `${tableName}_fk_${columnName}` : ''); - const constraintName = name - ? `CONSTRAINT ${mOptions.literal(name)} ` - : ''; - constraints.push( - `${constraintName}${parseReferences(options, mOptions.literal)}` - ); - if (referencesConstraintComment) { - comments.push( - comment( - `CONSTRAINT ${mOptions.literal(name)} ON`, - mOptions.literal(tableName), - referencesConstraintComment - ) + columns: _.map( + columnsWithOptions, + (options: ColumnDefinition, columnName) => { + const { + type, + collation, + default: defaultValue, + unique, + primaryKey, + notNull, + check, + references, + referencesConstraintName, + referencesConstraintComment, + deferrable, + generated + }: ColumnDefinition = options; + const constraints: string[] = []; + if (collation) { + constraints.push(`COLLATE ${collation}`); + } + if (defaultValue !== undefined) { + constraints.push(`DEFAULT ${escapeValue(defaultValue)}`); + } + if (unique) { + constraints.push('UNIQUE'); + } + if (primaryKey) { + constraints.push('PRIMARY KEY'); + } + if (notNull) { + constraints.push('NOT NULL'); + } + if (check) { + constraints.push(`CHECK (${check})`); + } + if (references) { + const name = + referencesConstraintName || + (referencesConstraintComment + ? `${tableName}_fk_${columnName}` + : ''); + const constraintName = name + ? `CONSTRAINT ${mOptions.literal(name)} ` + : ''; + constraints.push( + `${constraintName}${parseReferences(options, mOptions.literal)}` + ); + if (referencesConstraintComment) { + comments.push( + comment( + `CONSTRAINT ${mOptions.literal(name)} ON`, + mOptions.literal(tableName), + referencesConstraintComment + ) + ); + } + } + if (deferrable) { + constraints.push(parseDeferrable(options)); + } + if (generated) { + const sequenceOptions = parseSequenceOptions( + extendingTypeShorthands, + generated + ).join(' '); + constraints.push( + `GENERATED ${generated.precedence} AS IDENTITY${ + sequenceOptions ? ` (${sequenceOptions})` : '' + }` ); } - } - if (deferrable) { - constraints.push(parseDeferrable(options)); - } - if (generated) { - const sequenceOptions = parseSequenceOptions( - extendingTypeShorthands, - generated - ).join(' '); - constraints.push( - `GENERATED ${generated.precedence} AS IDENTITY${ - sequenceOptions ? ` (${sequenceOptions})` : '' - }` - ); - } - const constraintsStr = constraints.length - ? ` ${constraints.join(' ')}` - : ''; + const constraintsStr = constraints.length + ? ` ${constraints.join(' ')}` + : ''; - const sType = typeof type === 'object' ? mOptions.literal(type) : type; + const sType = typeof type === 'object' ? mOptions.literal(type) : type; - return `${mOptions.literal(columnName)} ${sType}${constraintsStr}`; - }), + return `${mOptions.literal(columnName)} ${sType}${constraintsStr}`; + } + ), constraints: multiplePrimaryColumns ? { primaryKey: primaryColumns } : {}, comments }; }; -const parseConstraints = (table, options, optionName, literal) => { +const parseConstraints = ( + table: Name, + options: ConstraintOptions, + optionName: string, + literal: (v: Name) => string +) => { const { check, unique, @@ -156,7 +219,7 @@ const parseConstraints = (table, options, optionName, literal) => { exclude, deferrable, comment: optionComment - } = options; + }: ConstraintOptions = options; const tableName = typeof table === 'object' ? table.name : table; let constraints = []; const comments = []; @@ -172,9 +235,9 @@ const parseConstraints = (table, options, optionName, literal) => { } } if (unique) { - const uniqueArray = _.isArray(unique) ? unique : [unique]; + const uniqueArray: Array = _.isArray(unique) ? unique : [unique]; const isArrayOfArrays = uniqueArray.some(uniqueSet => _.isArray(uniqueSet)); - (isArrayOfArrays ? uniqueArray : [uniqueArray]).forEach(uniqueSet => { + ((isArrayOfArrays ? uniqueArray : [uniqueArray]) as Array).forEach(uniqueSet => { const cols = _.isArray(uniqueSet) ? uniqueSet : [uniqueSet]; const name = literal(optionName || `${tableName}_uniq_${cols.join('_')}`); constraints.push( @@ -245,25 +308,31 @@ const parseConstraints = (table, options, optionName, literal) => { }; }; -const parseLike = (like, literal) => { - const formatOptions = (name, options) => +const parseLike = ( + like: Name | { table: Name; options?: LikeOptions }, + literal: (v: Name) => string +) => { + const formatOptions = ( + name: 'INCLUDING' | 'EXCLUDING', + options: Like | Like[] + ) => (_.isArray(options) ? options : [options]) .map(option => ` ${name} ${option}`) .join(''); - const table = typeof like === 'string' || !like.table ? like : like.table; - const options = like.options - ? [ + const table = typeof like === 'string' || !('table' in like) ? like : like.table; + const options = typeof like === 'string' || !('options' in like) + ? '' + : [ formatOptions('INCLUDING', like.options.including), formatOptions('EXCLUDING', like.options.excluding) - ].join('') - : ''; + ].join(''); return `LIKE ${literal(table)}${options}`; }; // TABLE -function dropTable(mOptions) { - const _drop = (tableName, { ifExists, cascade } = {}) => { +export function dropTable(mOptions: MigrationOptions) { + const _drop = (tableName: Name, { ifExists, cascade }: DropOptions = {}) => { const ifExistsStr = ifExists ? ' IF EXISTS' : ''; const cascadeStr = cascade ? ' CASCADE' : ''; const tableNameStr = mOptions.literal(tableName); @@ -272,8 +341,12 @@ function dropTable(mOptions) { return _drop; } -function createTable(mOptions) { - const _create = (tableName, columns, options = {}) => { +export function createTable(mOptions: MigrationOptions) { + const _create = ( + tableName: Name, + columns: ColumnDefinitions, + options: TableOptions = {} + ) => { const { temporary, ifNotExists, @@ -281,7 +354,7 @@ function createTable(mOptions) { like, constraints: optionsConstraints = {}, comment: tableComment - } = options; + }: TableOptions = options; const { columns: columnLines, constraints: crossColumnConstraints, @@ -298,7 +371,10 @@ function createTable(mOptions) { ); } - const constraints = { ...optionsConstraints, ...crossColumnConstraints }; + const constraints: ConstraintOptions = { + ...optionsConstraints, + ...crossColumnConstraints + }; const { constraints: constraintLines, comments: constraintComments @@ -331,8 +407,8 @@ ${formatLines(tableDefinition)} return _create; } -function alterTable(mOptions) { - const _alter = (tableName, options) => { +export function alterTable(mOptions: MigrationOptions) { + const _alter = (tableName: Name, options: AlterTableOptions) => { const alterDefinition = []; if (options.levelSecurity) { alterDefinition.push(`${options.levelSecurity} ROW LEVEL SECURITY`); @@ -344,8 +420,26 @@ function alterTable(mOptions) { } // COLUMNS -function dropColumns(mOptions) { - const _drop = (tableName, columns, { ifExists, cascade } = {}) => { +export interface AlterColumnOptions { + type?: string; + default?: Value; + notNull?: boolean; + allowNull?: boolean; + collation?: string; + using?: string; + comment?: string | null; + generated?: + | null + | false + | ({ precedence: 'ALWAYS' | 'BY DEFAULT' } & SequenceOptions); +} + +export function dropColumns(mOptions: MigrationOptions) { + const _drop = ( + tableName: Name, + columns: string | string[] | ColumnDefinitions, + { ifExists, cascade }: DropOptions = {} + ) => { if (typeof columns === 'string') { columns = [columns]; // eslint-disable-line no-param-reassign } else if (!_.isArray(columns) && typeof columns === 'object') { @@ -362,8 +456,12 @@ ${columnsStr};`; return _drop; } -function addColumns(mOptions) { - const _add = (tableName, columns, { ifNotExists } = {}) => { +export function addColumns(mOptions: MigrationOptions) { + const _add = ( + tableName: Name, + columns: ColumnDefinitions, + { ifNotExists }: AddOptions = {} + ) => { const { columns: columnLines, comments: columnComments = [] @@ -382,8 +480,8 @@ function addColumns(mOptions) { return _add; } -function alterColumn(mOptions) { - return (tableName, columnName, options) => { +export function alterColumn(mOptions: MigrationOptions) { + return (tableName: Name, columnName: string, options: AlterColumnOptions) => { const { default: defaultValue, type, @@ -394,7 +492,7 @@ function alterColumn(mOptions) { comment: columnComment, generated } = options; - const actions = []; + const actions: string[] = []; if (defaultValue === null) { actions.push('DROP DEFAULT'); } else if (defaultValue !== undefined) { @@ -427,7 +525,7 @@ function alterColumn(mOptions) { } } - const queries = []; + const queries: string[] = []; if (actions.length > 0) { const columnsStr = formatLines( actions, @@ -450,42 +548,54 @@ function alterColumn(mOptions) { }; } -function renameTable(mOptions) { - const _rename = (tableName, newName) => { +export function renameTable(mOptions: MigrationOptions) { + const _rename = (tableName: Name, newName: Name) => { const tableNameStr = mOptions.literal(tableName); const newNameStr = mOptions.literal(newName); return `ALTER TABLE ${tableNameStr} RENAME TO ${newNameStr};`; }; - _rename.reverse = (tableName, newName) => _rename(newName, tableName); + _rename.reverse = (tableName: Name, newName: Name) => + _rename(newName, tableName); return _rename; } -function renameColumn(mOptions) { - const _rename = (tableName, columnName, newName) => { +export function renameColumn(mOptions: MigrationOptions) { + const _rename = (tableName: Name, columnName: string, newName: string) => { const tableNameStr = mOptions.literal(tableName); const columnNameStr = mOptions.literal(columnName); const newNameStr = mOptions.literal(newName); return `ALTER TABLE ${tableNameStr} RENAME ${columnNameStr} TO ${newNameStr};`; }; - _rename.reverse = (tableName, columnName, newName) => + _rename.reverse = (tableName: Name, columnName: string, newName: string) => _rename(tableName, newName, columnName); return _rename; } -function renameConstraint(mOptions) { - const _rename = (tableName, constraintName, newName) => { +export function renameConstraint(mOptions: MigrationOptions) { + const _rename = ( + tableName: Name, + constraintName: string, + newName: string + ) => { const tableNameStr = mOptions.literal(tableName); const constraintNameStr = mOptions.literal(constraintName); const newNameStr = mOptions.literal(newName); return `ALTER TABLE ${tableNameStr} RENAME CONSTRAINT ${constraintNameStr} TO ${newNameStr};`; }; - _rename.reverse = (tableName, constraintName, newName) => - _rename(tableName, newName, constraintName); + _rename.reverse = ( + tableName: Name, + constraintName: string, + newName: string + ) => _rename(tableName, newName, constraintName); return _rename; } -function dropConstraint(mOptions) { - const _drop = (tableName, constraintName, { ifExists, cascade } = {}) => { +export function dropConstraint(mOptions: MigrationOptions) { + const _drop = ( + tableName: Name, + constraintName: string, + { ifExists, cascade }: DropOptions = {} + ) => { const ifExistsStr = ifExists ? ' IF EXISTS' : ''; const cascadeStr = cascade ? ' CASCADE' : ''; const tableNameStr = mOptions.literal(tableName); @@ -494,8 +604,12 @@ function dropConstraint(mOptions) { }; return _drop; } -function addConstraint(mOptions) { - const _add = (tableName, constraintName, expression) => { +export function addConstraint(mOptions: MigrationOptions) { + const _add = ( + tableName: Name, + constraintName: string | null, + expression: string | ConstraintOptions + ) => { const { constraints, comments } = typeof expression === 'string' ? { @@ -523,17 +637,3 @@ function addConstraint(mOptions) { _add.reverse = dropConstraint(mOptions); return _add; } - -module.exports = { - createTable, - dropTable, - alterTable, - renameTable, - addColumns, - dropColumns, - alterColumn, - renameColumn, - addConstraint, - dropConstraint, - renameConstraint -}; diff --git a/lib/operations/triggers.js b/src/operations/triggers.ts similarity index 65% rename from lib/operations/triggers.js rename to src/operations/triggers.ts index a6bdef58..83db7465 100644 --- a/lib/operations/triggers.js +++ b/src/operations/triggers.ts @@ -1,9 +1,27 @@ -const { isArray } = require('lodash'); -const { escapeValue } = require('../utils'); -const { createFunction, dropFunction } = require('./functions'); +import { isArray } from 'lodash'; +import { DropOptions, Name, Value } from '../definitions'; +import { MigrationOptions } from '../migration-builder'; +import { escapeValue } from '../utils'; +import { createFunction, dropFunction, FunctionOptions } from './functions'; -function dropTrigger(mOptions) { - const _drop = (tableName, triggerName, { ifExists, cascade } = {}) => { +export interface TriggerOptions { + when?: 'BEFORE' | 'AFTER' | 'INSTEAD OF'; + operation: string | string[]; + constraint?: boolean; + function?: Name; + functionParams?: Value[]; + level?: 'STATEMENT' | 'ROW'; + condition?: string; + deferrable?: boolean; + deferred?: boolean; +} + +export function dropTrigger(mOptions: MigrationOptions) { + const _drop = ( + tableName: Name, + triggerName: Name, + { ifExists, cascade }: DropOptions = {} + ) => { const ifExistsStr = ifExists ? ' IF EXISTS' : ''; const cascadeStr = cascade ? ' CASCADE' : ''; const triggerNameStr = mOptions.literal(triggerName); @@ -13,15 +31,20 @@ function dropTrigger(mOptions) { return _drop; } -function createTrigger(mOptions) { - const _create = (tableName, triggerName, triggerOptions = {}, definition) => { +export function createTrigger(mOptions: MigrationOptions) { + const _create = ( + tableName: Name, + triggerName: Name, + triggerOptions: Partial = {}, + definition?: Value + ) => { const { constraint, condition, operation, deferrable, deferred, - functionArgs = [] + functionParams = [] } = triggerOptions; let { when, level = 'STATEMENT', function: functionName } = triggerOptions; const operations = isArray(operation) ? operation.join(' OR ') : operation; @@ -56,7 +79,7 @@ function createTrigger(mOptions) { : ''; const conditionClause = condition ? `WHEN (${condition})\n ` : ''; const constraintStr = constraint ? ' CONSTRAINT' : ''; - const paramsStr = functionArgs.map(escapeValue).join(', '); + const paramsStr = functionParams.map(escapeValue).join(', '); const triggerNameStr = mOptions.literal(triggerName); const tableNameStr = mOptions.literal(tableName); const functionNameStr = mOptions.literal(functionName); @@ -78,10 +101,10 @@ function createTrigger(mOptions) { }; _create.reverse = ( - tableName, - triggerName, - triggerOptions = {}, - definition + tableName: Name, + triggerName: Name, + triggerOptions: Partial & DropOptions = {}, + definition?: Value ) => { const triggerSQL = dropTrigger(mOptions)( tableName, @@ -101,20 +124,21 @@ function createTrigger(mOptions) { return _create; } -function renameTrigger(mOptions) { - const _rename = (tableName, oldTriggerName, newTriggerName) => { +export function renameTrigger(mOptions: MigrationOptions) { + const _rename = ( + tableName: Name, + oldTriggerName: Name, + newTriggerName: Name + ) => { const oldTriggerNameStr = mOptions.literal(oldTriggerName); const tableNameStr = mOptions.literal(tableName); const newTriggerNameStr = mOptions.literal(newTriggerName); return `ALTER TRIGGER ${oldTriggerNameStr} ON ${tableNameStr} RENAME TO ${newTriggerNameStr};`; }; - _rename.reverse = (tableName, oldTriggerName, newTriggerName) => - _rename(tableName, newTriggerName, oldTriggerName); + _rename.reverse = ( + tableName: Name, + oldTriggerName: Name, + newTriggerName: Name + ) => _rename(tableName, newTriggerName, oldTriggerName); return _rename; } - -module.exports = { - createTrigger, - dropTrigger, - renameTrigger -}; diff --git a/lib/operations/types.js b/src/operations/types.ts similarity index 62% rename from lib/operations/types.js rename to src/operations/types.ts index 7f2748d2..662809e7 100644 --- a/lib/operations/types.js +++ b/src/operations/types.ts @@ -1,8 +1,10 @@ -const _ = require('lodash'); -const { applyType, escapeValue } = require('../utils'); +import _ from 'lodash'; +import { DropOptions, IfExistsOption, Name, Type, Value } from '../definitions'; +import { MigrationOptions } from '../migration-builder'; +import { applyType, escapeValue } from '../utils'; -function dropType(mOptions) { - const _drop = (typeName, { ifExists, cascade } = {}) => { +export function dropType(mOptions: MigrationOptions) { + const _drop = (typeName: Name, { ifExists, cascade }: DropOptions = {}) => { const ifExistsStr = ifExists ? ' IF EXISTS' : ''; const cascadeStr = cascade ? ' CASCADE' : ''; const typeNameStr = mOptions.literal(typeName); @@ -11,8 +13,11 @@ function dropType(mOptions) { return _drop; } -function createType(mOptions) { - const _create = (typeName, options) => { +export function createType(mOptions: MigrationOptions) { + const _create = ( + typeName: Name, + options: Value[] | { [name: string]: Type } + ) => { if (_.isArray(options)) { const optionsStr = options.map(escapeValue).join(', '); const typeNameStr = mOptions.literal(typeName); @@ -28,8 +33,12 @@ function createType(mOptions) { return _create; } -function dropTypeAttribute(mOptions) { - const _drop = (typeName, attributeName, { ifExists } = {}) => { +export function dropTypeAttribute(mOptions: MigrationOptions) { + const _drop = ( + typeName: Name, + attributeName: string, + { ifExists }: IfExistsOption = {} + ) => { const ifExistsStr = ifExists ? ' IF EXISTS' : ''; const typeNameStr = mOptions.literal(typeName); const attributeNameStr = mOptions.literal(attributeName); @@ -38,8 +47,12 @@ function dropTypeAttribute(mOptions) { return _drop; } -function addTypeAttribute(mOptions) { - const _alterAttributeAdd = (typeName, attributeName, attributeType) => { +export function addTypeAttribute(mOptions: MigrationOptions) { + const _alterAttributeAdd = ( + typeName: Name, + attributeName: string, + attributeType: Type + ) => { const typeStr = applyType(attributeType, mOptions.typeShorthands).type; const typeNameStr = mOptions.literal(typeName); const attributeNameStr = mOptions.literal(attributeName); @@ -50,8 +63,8 @@ function addTypeAttribute(mOptions) { return _alterAttributeAdd; } -function setTypeAttribute(mOptions) { - return (typeName, attributeName, attributeType) => { +export function setTypeAttribute(mOptions: MigrationOptions) { + return (typeName: Name, attributeName: string, attributeType: Type) => { const typeStr = applyType(attributeType, mOptions.typeShorthands).type; const typeNameStr = mOptions.literal(typeName); const attributeNameStr = mOptions.literal(attributeName); @@ -60,8 +73,16 @@ function setTypeAttribute(mOptions) { }; } -function addTypeValue(mOptions) { - const _add = (typeName, value, options = {}) => { +export function addTypeValue(mOptions: MigrationOptions) { + const _add = ( + typeName: Name, + value: Value, + options: { + ifNotExists?: boolean; + before?: string; + after?: string; + } = {} + ) => { const { ifNotExists, before, after } = options; if (before && after) { @@ -78,48 +99,44 @@ function addTypeValue(mOptions) { return _add; } -function renameType(mOptions) { - const _rename = (typeName, newTypeName) => { +export function renameType(mOptions: MigrationOptions) { + const _rename = (typeName: Name, newTypeName: Name) => { const typeNameStr = mOptions.literal(typeName); const newTypeNameStr = mOptions.literal(newTypeName); return `ALTER TYPE ${typeNameStr} RENAME TO ${newTypeNameStr};`; }; - _rename.reverse = (typeName, newTypeName) => _rename(newTypeName, typeName); + _rename.reverse = (typeName: Name, newTypeName: Name) => + _rename(newTypeName, typeName); return _rename; } -function renameTypeAttribute(mOptions) { - const _rename = (typeName, attributeName, newAttributeName) => { +export function renameTypeAttribute(mOptions: MigrationOptions) { + const _rename = ( + typeName: Name, + attributeName: string, + newAttributeName: string + ) => { const typeNameStr = mOptions.literal(typeName); const attributeNameStr = mOptions.literal(attributeName); const newAttributeNameStr = mOptions.literal(newAttributeName); return `ALTER TYPE ${typeNameStr} RENAME ATTRIBUTE ${attributeNameStr} TO ${newAttributeNameStr};`; }; - _rename.reverse = (typeName, attributeName, newAttributeName) => - _rename(typeName, newAttributeName, attributeName); + _rename.reverse = ( + typeName: Name, + attributeName: string, + newAttributeName: string + ) => _rename(typeName, newAttributeName, attributeName); return _rename; } -function renameTypeValue(mOptions) { - const _rename = (typeName, value, newValue) => { +export function renameTypeValue(mOptions: MigrationOptions) { + const _rename = (typeName: Name, value: string, newValue: string) => { const valueStr = escapeValue(value); const newValueStr = escapeValue(newValue); const typeNameStr = mOptions.literal(typeName); return `ALTER TYPE ${typeNameStr} RENAME VALUE ${valueStr} TO ${newValueStr};`; }; - _rename.reverse = (typeName, value, newValue) => + _rename.reverse = (typeName: Name, value: string, newValue: string) => _rename(typeName, newValue, value); return _rename; } - -module.exports = { - createType, - dropType, - renameType, - addTypeAttribute, - dropTypeAttribute, - setTypeAttribute, - renameTypeAttribute, - renameTypeValue, - addTypeValue -}; diff --git a/lib/operations/views.js b/src/operations/views.ts similarity index 63% rename from lib/operations/views.js rename to src/operations/views.ts index d0b6233c..4236eaf6 100644 --- a/lib/operations/views.js +++ b/src/operations/views.ts @@ -1,7 +1,25 @@ -const { escapeValue } = require('../utils'); +import { DropOptions, Name, Value } from '../definitions'; +import { MigrationOptions } from '../migration-builder'; +import { escapeValue } from '../utils'; -function dropView(mOptions) { - const _drop = (viewName, { ifExists, cascade } = {}) => { +export interface CreateViewOptions { + temporary?: boolean; + replace?: boolean; + recursive?: boolean; + columns?: string | string[]; + checkOption?: 'CASCADED' | 'LOCAL'; +} + +export interface AlterViewOptions { + checkOption?: null | false | 'CASCADED' | 'LOCAL'; +} + +export interface AlterViewColumnOptions { + default?: Value; +} + +export function dropView(mOptions: MigrationOptions) { + const _drop = (viewName: Name, { ifExists, cascade }: DropOptions = {}) => { const ifExistsStr = ifExists ? ' IF EXISTS' : ''; const cascadeStr = cascade ? ' CASCADE' : ''; const viewNameStr = mOptions.literal(viewName); @@ -10,8 +28,12 @@ function dropView(mOptions) { return _drop; } -function createView(mOptions) { - const _create = (viewName, options, definition) => { +export function createView(mOptions: MigrationOptions) { + const _create = ( + viewName: Name, + options: CreateViewOptions, + definition: string + ) => { const { temporary, replace, @@ -36,8 +58,8 @@ function createView(mOptions) { return _create; } -function alterView(mOptions) { - const _alter = (viewName, options) => { +export function alterView(mOptions: MigrationOptions) { + const _alter = (viewName: Name, options: AlterViewOptions) => { const { checkOption } = options; const clauses = []; if (checkOption !== undefined) { @@ -54,8 +76,12 @@ function alterView(mOptions) { return _alter; } -function alterViewColumn(mOptions) { - const _alter = (viewName, columnName, options) => { +export function alterViewColumn(mOptions: MigrationOptions) { + const _alter = ( + viewName: Name, + columnName: Name, + options: AlterViewColumnOptions + ) => { const { default: defaultValue } = options; const actions = []; if (defaultValue === null) { @@ -75,20 +101,13 @@ function alterViewColumn(mOptions) { return _alter; } -function renameView(mOptions) { - const _rename = (viewName, newViewName) => { +export function renameView(mOptions: MigrationOptions) { + const _rename = (viewName: Name, newViewName: Name) => { const viewNameStr = mOptions.literal(viewName); const newViewNameStr = mOptions.literal(newViewName); return `ALTER VIEW ${viewNameStr} RENAME TO ${newViewNameStr};`; }; - _rename.reverse = (viewName, newViewName) => _rename(newViewName, viewName); + _rename.reverse = (viewName: Name, newViewName: Name) => + _rename(newViewName, viewName); return _rename; } - -module.exports = { - createView, - dropView, - alterView, - alterViewColumn, - renameView -}; diff --git a/lib/operations/viewsMaterialized.js b/src/operations/viewsMaterialized.ts similarity index 60% rename from lib/operations/viewsMaterialized.js rename to src/operations/viewsMaterialized.ts index 2d0ee044..b26bbfce 100644 --- a/lib/operations/viewsMaterialized.js +++ b/src/operations/viewsMaterialized.ts @@ -1,15 +1,41 @@ -const { formatLines } = require('../utils'); +import { DropOptions, Name } from '../definitions'; +import { MigrationOptions } from '../migration-builder'; +import { formatLines } from '../utils'; -const dataClause = data => +export interface CreateMaterializedViewOptions { + ifNotExists?: boolean; + columns?: string | string[]; + tablespace?: string; + storageParameters?: { [key: string]: any }; + data?: boolean; +} + +export interface AlterMaterializedViewOptions { + cluster?: null | false | string; + extension?: string; + storageParameters?: { [key: string]: any }; +} + +export interface RefreshMaterializedViewOptions { + concurrently?: boolean; + data?: boolean; +} + +const dataClause = (data?: boolean) => data !== undefined ? ` WITH${data ? '' : ' NO'} DATA` : ''; -const storageParameterStr = storageParameters => key => { +const storageParameterStr = < + T extends { [key: string]: any }, + K extends keyof T +>( + storageParameters: T +) => (key: K) => { const value = storageParameters[key] === true ? '' : ` = ${storageParameters[key]}`; return `${key}${value}`; }; -function dropMaterializedView(mOptions) { - const _drop = (viewName, { ifExists, cascade } = {}) => { +export function dropMaterializedView(mOptions: MigrationOptions) { + const _drop = (viewName: Name, { ifExists, cascade }: DropOptions = {}) => { const ifExistsStr = ifExists ? ' IF EXISTS' : ''; const cascadeStr = cascade ? ' CASCADE' : ''; const viewNameStr = mOptions.literal(viewName); @@ -18,8 +44,12 @@ function dropMaterializedView(mOptions) { return _drop; } -function createMaterializedView(mOptions) { - const _create = (viewName, options, definition) => { +export function createMaterializedView(mOptions: MigrationOptions) { + const _create = ( + viewName: Name, + options: CreateMaterializedViewOptions, + definition: string + ) => { const { ifNotExists, columns = [], @@ -48,9 +78,13 @@ function createMaterializedView(mOptions) { return _create; } -function alterMaterializedView(mOptions) { - const _alter = (viewName, options) => { - const { cluster, extension, storageParameters = {} } = options; +export function alterMaterializedView(mOptions: MigrationOptions) { + const _alter = (viewName: Name, options: AlterMaterializedViewOptions) => { + const { + cluster, + extension, + storageParameters = {} + }: AlterMaterializedViewOptions = options; const clauses = []; if (cluster !== undefined) { if (cluster) { @@ -82,30 +116,41 @@ function alterMaterializedView(mOptions) { return _alter; } -function renameMaterializedView(mOptions) { - const _rename = (viewName, newViewName) => { +export function renameMaterializedView(mOptions: MigrationOptions) { + const _rename = (viewName: Name, newViewName: Name) => { const viewNameStr = mOptions.literal(viewName); const newViewNameStr = mOptions.literal(newViewName); return `ALTER MATERIALIZED VIEW ${viewNameStr} RENAME TO ${newViewNameStr};`; }; - _rename.reverse = (viewName, newViewName) => _rename(newViewName, viewName); + _rename.reverse = (viewName: Name, newViewName: Name) => + _rename(newViewName, viewName); return _rename; } -function renameMaterializedViewColumn(mOptions) { - const _rename = (viewName, columnName, newColumnName) => { +export function renameMaterializedViewColumn(mOptions: MigrationOptions) { + const _rename = ( + viewName: Name, + columnName: string, + newColumnName: string + ) => { const viewNameStr = mOptions.literal(viewName); const columnNameStr = mOptions.literal(columnName); const newColumnNameStr = mOptions.literal(newColumnName); return `ALTER MATERIALIZED VIEW ${viewNameStr} RENAME COLUMN ${columnNameStr} TO ${newColumnNameStr};`; }; - _rename.reverse = (viewName, columnName, newColumnName) => - _rename(viewName, newColumnName, columnName); + _rename.reverse = ( + viewName: Name, + columnName: string, + newColumnName: string + ) => _rename(viewName, newColumnName, columnName); return _rename; } -function refreshMaterializedView(mOptions) { - const _refresh = (viewName, { concurrently, data } = {}) => { +export function refreshMaterializedView(mOptions: MigrationOptions) { + const _refresh = ( + viewName: Name, + { concurrently, data }: RefreshMaterializedViewOptions = {} + ) => { const concurrentlyStr = concurrently ? ' CONCURRENTLY' : ''; const dataStr = dataClause(data); const viewNameStr = mOptions.literal(viewName); @@ -114,12 +159,3 @@ function refreshMaterializedView(mOptions) { _refresh.reverse = _refresh; return _refresh; } - -module.exports = { - createMaterializedView, - dropMaterializedView, - alterMaterializedView, - renameMaterializedView, - renameMaterializedViewColumn, - refreshMaterializedView -}; diff --git a/lib/runner.js b/src/runner.ts similarity index 67% rename from lib/runner.js rename to src/runner.ts index b84277b5..68e3c854 100644 --- a/lib/runner.js +++ b/src/runner.ts @@ -1,34 +1,39 @@ -const path = require('path'); -const fs = require('fs'); -const Db = require('./db'); -const Migration = require('./migration'); -const { - getSchemas, +import fs from 'fs'; +import path from 'path'; +import { Client } from 'pg'; +import { TlsOptions } from 'tls'; +import Db, { DB } from './db'; +import { ShorthandDefinitions } from './definitions'; +import Migration, { loadMigrationFiles, RunMigration } from './migration'; +import { MigrationBuilderActions } from './migration-builder'; +import { + createSchemalize, getMigrationTableSchema, - promisify, + getSchemas, PgLiteral, - createSchemalize -} = require('./utils'); + promisify +} from './utils'; // Random but well-known identifier shared by all instances of node-pg-migrate const PG_MIGRATE_LOCK_ID = 7241865325823964; -const readFile = promisify(fs.readFile); // eslint-disable-line security/detect-non-literal-fs-filename +const readFile = promisify(fs.readFile); // eslint-disable-line security/detect-non-literal-fs-filename const idColumn = 'id'; const nameColumn = 'name'; const runOnColumn = 'run_on'; -const loadMigrations = async (db, options, log) => { +const loadMigrations = async ( + db: DB, + options: RunnerOption, + log: typeof console.log +) => { try { - let shorthands = {}; - const files = await Migration.loadMigrationFiles( - options.dir, - options.ignorePattern - ); + let shorthands: ShorthandDefinitions = {}; + const files = await loadMigrationFiles(options.dir, options.ignorePattern); return files.map(file => { const filePath = `${options.dir}/${file}`; - const actions = + const actions: MigrationBuilderActions = path.extname(filePath) === '.sql' ? // eslint-disable-next-line security/detect-non-literal-fs-filename { up: async pgm => pgm.sql(await readFile(filePath, 'utf8')) } @@ -51,10 +56,8 @@ const loadMigrations = async (db, options, log) => { } }; -const lock = async db => { - const { - rows: [lockObtained] - } = await db.query( +const lock = async (db: DB): Promise => { + const [lockObtained] = await db.select( `select pg_try_advisory_lock(${PG_MIGRATE_LOCK_ID}) as "lockObtained"` ); if (!lockObtained) { @@ -62,7 +65,10 @@ const lock = async db => { } }; -const ensureMigrationsTable = async (db, options) => { +const ensureMigrationsTable = async ( + db: DB, + options: RunnerOption +): Promise => { try { const schema = getMigrationTableSchema(options); const { migrationsTable } = options; @@ -94,7 +100,7 @@ const ensureMigrationsTable = async (db, options) => { } }; -const getRunMigrations = async (db, options) => { +const getRunMigrations = async (db: DB, options: RunnerOption) => { const schema = getMigrationTableSchema(options); const { migrationsTable } = options; const fullTableName = createSchemalize(options.decamelize, true)({ @@ -107,22 +113,26 @@ const getRunMigrations = async (db, options) => { ); }; -const getMigrationsToRun = (options, runNames, migrations) => { +const getMigrationsToRun = ( + options: RunnerOption, + runNames: string[], + migrations: Migration[] +): Migration[] => { if (options.direction === 'down') { - const downMigrations = runNames + const downMigrations: Array = runNames .filter(migrationName => !options.file || options.file === migrationName) .map( migrationName => migrations.find(({ name }) => name === migrationName) || migrationName ); const toRun = (options.timestamp - ? downMigrations.filter(({ timestamp }) => timestamp >= options.count) + ? downMigrations.filter((migration) => typeof migration === 'object' && migration.timestamp >= options.count) : downMigrations.slice( -Math.abs(options.count === undefined ? 1 : options.count) ) ).reverse(); const deletedMigrations = toRun.filter( - migration => typeof migration === 'string' + (migration): migration is string => typeof migration === 'string' ); if (deletedMigrations.length) { const deletedMigrationsStr = deletedMigrations.join(', '); @@ -130,7 +140,7 @@ const getMigrationsToRun = (options, runNames, migrations) => { `Definitions of migrations ${deletedMigrationsStr} have been deleted.` ); } - return toRun; + return toRun as Migration[]; } const upMigrations = migrations.filter( ({ name }) => @@ -144,7 +154,7 @@ const getMigrationsToRun = (options, runNames, migrations) => { ); }; -const checkOrder = (runNames, migrations) => { +const checkOrder = (runNames: string[], migrations: Migration[]) => { const len = Math.min(runNames.length, migrations.length); for (let i = 0; i < len; i += 1) { const runName = runNames[i]; @@ -157,15 +167,70 @@ const checkOrder = (runNames, migrations) => { } }; -const runMigrations = (toRun, method, direction) => +export type MigrationDirection = 'up' | 'down'; + +const runMigrations = ( + toRun: Migration[], + method: 'markAsRun' | 'apply', + direction: MigrationDirection +) => toRun.reduce( (promise, migration) => promise.then(() => migration[method](direction)), Promise.resolve() ); -const runner = async options => { +export interface RunnerOptionConfig { + migrationsTable: string; + migrationsSchema?: string; + schema?: string | string[]; + dir: string; + checkOrder?: boolean; + direction: MigrationDirection; + count: number; + timestamp?: boolean; + ignorePattern: string; + file?: string; + dryRun?: boolean; + createSchema?: boolean; + createMigrationsSchema?: boolean; + singleTransaction?: boolean; + noLock?: boolean; + fake?: boolean; + decamelize?: boolean; + log?: (msg: string) => void; +} + +export interface ConnectionConfig { + user?: string; + database?: string; + password?: string; + port?: number; + host?: string; + connectionString?: string; +} + +export interface ClientConfig extends ConnectionConfig { + ssl?: boolean | TlsOptions; +} + +export interface RunnerOptionUrl { + databaseUrl: string | ClientConfig; +} + +export interface RunnerOptionClient { + dbClient: Client; +} + +export type RunnerOption = RunnerOptionConfig & + (RunnerOptionClient | RunnerOptionUrl); + +const runner = async (options: RunnerOption): Promise => { const log = options.log || console.log; - const db = Db(options.dbClient || options.databaseUrl, log); + const db = Db( + (options as RunnerOptionClient).dbClient || + (options as RunnerOptionUrl).databaseUrl, + log + ); try { await db.createConnection(); if (options.schema) { @@ -190,7 +255,7 @@ const runner = async options => { await ensureMigrationsTable(db, options); if (!options.noLock) { - await lock(db, options); + await lock(db); } const [migrations, runNames] = await Promise.all([ @@ -202,7 +267,11 @@ const runner = async options => { checkOrder(runNames, migrations); } - const toRun = getMigrationsToRun(options, runNames, migrations); + const toRun: Migration[] = getMigrationsToRun( + options, + runNames, + migrations + ); if (!toRun.length) { log('No migrations to run!'); @@ -245,4 +314,5 @@ runner.default = runner; // workaround for transpilers runner.PgLiteral = PgLiteral; runner.Migration = Migration; +export default runner; module.exports = runner; diff --git a/lib/utils.js b/src/utils.ts similarity index 52% rename from lib/utils.js rename to src/utils.ts index 684f539b..3436daab 100644 --- a/lib/utils.js +++ b/src/utils.ts @@ -1,30 +1,45 @@ -const decamelize = require('decamelize'); +import decamelize from 'decamelize'; +import { + ColumnDefinition, + ShorthandDefinitions, + Name, + Type, + Value +} from './definitions'; +import { MigrationOptions } from './migration-builder'; +import { FunctionParam, FunctionParamType } from './operations/functions'; +import { RunnerOption } from './runner'; // This is used to create unescaped strings // exposed in the migrations via pgm.func -class PgLiteral { - static create(str) { +export class PgLiteral { + static create(str: string): PgLiteral { return new PgLiteral(str); } - constructor(str) { + private readonly _str: string; + + constructor(str: string) { this._str = str; } - toString() { + toString(): string { return this._str; } } -const identity = v => v; -const quote = str => `"${str}"`; +const identity = (v: T) => v; +const quote = (str: string) => `"${str}"`; -const createSchemalize = (shouldDecamelize, shouldQuote) => { - const transform = [ +export const createSchemalize = ( + shouldDecamelize: boolean, + shouldQuote: boolean +) => { + const transform: (v: Name) => string = [ shouldDecamelize ? decamelize : identity, shouldQuote ? quote : identity ].reduce((acc, fn) => (fn === identity ? acc : x => acc(fn(x)))); - return v => { + return (v: Name) => { if (typeof v === 'object') { const { schema, name } = v; return (schema ? `${transform(schema)}.` : '') + transform(name); @@ -33,13 +48,16 @@ const createSchemalize = (shouldDecamelize, shouldQuote) => { }; }; -const createTransformer = literal => (s, d) => +export const createTransformer = (literal: (v: Name) => string) => ( + s: string, + d?: { [key: string]: Name } +) => Object.keys(d || {}).reduce( - (str, p) => str.replace(new RegExp(`{${p}}`, 'g'), literal(d[p])), // eslint-disable-line security/detect-non-literal-regexp + (str: string, p) => str.replace(new RegExp(`{${p}}`, 'g'), literal(d[p])), // eslint-disable-line security/detect-non-literal-regexp s ); -const escapeValue = val => { +export const escapeValue = (val: Value): string | number => { if (val === null) { return 'NULL'; } @@ -47,7 +65,7 @@ const escapeValue = val => { return val.toString(); } if (typeof val === 'string') { - let dollars; + let dollars: string; let index = 0; do { index += 1; @@ -71,14 +89,14 @@ const escapeValue = val => { return ''; }; -const getSchemas = schema => { +export const getSchemas = (schema: string | string[]): string[] => { const schemas = (Array.isArray(schema) ? schema : [schema]).filter( s => typeof s === 'string' && s.length > 0 ); return schemas.length > 0 ? schemas : ['public']; }; -const getMigrationTableSchema = options => +export const getMigrationTableSchema = (options: RunnerOption): string => options.migrationsSchema !== undefined ? options.migrationsSchema : getSchemas(options.schema)[0]; @@ -90,24 +108,27 @@ const typeAdapters = { double: 'double precision', datetime: 'timestamp', bool: 'boolean' -}; +} as const; -const defaultTypeShorthands = { +const defaultTypeShorthands: ShorthandDefinitions = { id: { type: 'serial', primaryKey: true } // convenience type for serial primary keys }; // some convenience adapters -- see above -const applyTypeAdapters = type => - typeAdapters[type] ? typeAdapters[type] : type; - -const applyType = (type, extendingTypeShorthands = {}) => { - const typeShorthands = { +export const applyTypeAdapters = (type: string): string => + type in typeAdapters ? typeAdapters[type as keyof typeof typeAdapters] : type; + +export const applyType = ( + type: Type, + extendingTypeShorthands: ShorthandDefinitions = {} +): ColumnDefinition & FunctionParamType => { + const typeShorthands: ShorthandDefinitions = { ...defaultTypeShorthands, ...extendingTypeShorthands }; const options = typeof type === 'string' ? { type } : type; - let ext = null; - const types = [options.type]; + let ext: { type?: string } | null = null; + const types: string[] = [options.type]; while (typeShorthands[types[types.length - 1]]) { if (ext) { delete ext.type; @@ -131,12 +152,16 @@ const applyType = (type, extendingTypeShorthands = {}) => { }; }; -const formatParam = mOptions => param => { - const { mode, name, type, default: defaultValue } = applyType( - param, - mOptions.typeShorthands - ); - const options = []; +const formatParam = (mOptions: MigrationOptions) => ( + param: FunctionParamType +) => { + const { + mode, + name, + type, + default: defaultValue + }: FunctionParamType = applyType(param, mOptions.typeShorthands); + const options: string[] = []; if (mode) { options.push(mode); } @@ -152,38 +177,33 @@ const formatParam = mOptions => param => { return options.join(' '); }; -const formatParams = (params = [], mOptions) => - `(${params.map(formatParam(mOptions)).join(', ')})`; +export const formatParams = ( + params: FunctionParam[] = [], + mOptions: MigrationOptions +) => `(${params.map(formatParam(mOptions)).join(', ')})`; -const comment = (object, name, text) => { +export const comment = (object: string, name: string, text?: string) => { const cmt = escapeValue(text || null); return `COMMENT ON ${object} ${name} IS ${cmt};`; }; -const formatLines = (lines, replace = ' ', separator = ',') => +export const formatLines = ( + lines: string[], + replace: string = ' ', + separator: string = ',' +) => lines .map(line => line.replace(/(?:\r\n|\r|\n)+/g, ' ')) .join(`${separator}\n`) .replace(/^/gm, replace); -const promisify = fn => (...args) => - new Promise((resolve, reject) => - fn.call(this, ...args, (err, ...result) => - err ? reject(err) : resolve(...result) - ) - ); - -module.exports = { - PgLiteral, - createSchemalize, - createTransformer, - escapeValue, - getSchemas, - getMigrationTableSchema, - applyTypeAdapters, - applyType, - formatParams, - comment, - formatLines, - promisify -}; +export function promisify( + fn: (...args: any[]) => any +): (...args: any[]) => Promise { + return (...args) => + new Promise((resolve, reject) => + fn.call(this, ...args, (err: any, ...result: any[]) => + err ? reject(err) : resolve(...result) + ) + ); +} diff --git a/lib/migration-template.js b/templates/migration-template.js similarity index 53% rename from lib/migration-template.js rename to templates/migration-template.js index 5dffd627..fbce4d40 100644 --- a/lib/migration-template.js +++ b/templates/migration-template.js @@ -2,10 +2,6 @@ exports.shorthands = undefined; -exports.up = (pgm) => { +exports.up = pgm => {}; -}; - -exports.down = (pgm) => { - -}; +exports.down = pgm => {}; diff --git a/lib/migration-template.sql b/templates/migration-template.sql similarity index 100% rename from lib/migration-template.sql rename to templates/migration-template.sql diff --git a/lib/migration-template.ts b/templates/migration-template.ts similarity index 100% rename from lib/migration-template.ts rename to templates/migration-template.ts diff --git a/test/tables-test.js b/test/tables-test.js index 63c945b6..1899f003 100644 --- a/test/tables-test.js +++ b/test/tables-test.js @@ -194,6 +194,33 @@ describe('lib/operations/tables', () => { );`); }); + it('check table unique constraint work correctly for string', () => { + const args = [ + 'myTableName', + { + colA: { type: 'integer' }, + colB: { type: 'varchar' } + }, + { + constraints: { + unique: 'colA' + } + } + ]; + const sql1 = Tables.createTable(options1)(...args); + const sql2 = Tables.createTable(options2)(...args); + expect(sql1).to.equal(`CREATE TABLE "myTableName" ( + "colA" integer, + "colB" varchar, + CONSTRAINT "myTableName_uniq_colA" UNIQUE ("colA") +);`); + expect(sql2).to.equal(`CREATE TABLE "my_table_name" ( + "col_a" integer, + "col_b" varchar, + CONSTRAINT "my_table_name_uniq_col_a" UNIQUE ("col_a") +);`); + }); + it('check table unique constraint work correctly for array of arrays', () => { const args = [ 'myTableName', diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..b6017b4b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "es5", + "moduleResolution": "node", + "rootDir": "src", + "outDir": "lib", + "lib": ["es2017"], + "esModuleInterop": true, + "declaration": true, + "sourceMap": false, + "removeComments": true, + "strict": false, + "noImplicitAny": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "templates", "lib", "dist", "bin"] +}