diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..3726ee1 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1 @@ +module.exports = require("@addbots/prettier-config") diff --git a/package-lock.json b/package-lock.json index 0f61e1c..9136d14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,9 +1,21 @@ { "name": "postgres-schema-builder", - "version": "1.1.0-beta.2", + "version": "1.2.0-beta.1", "lockfileVersion": 1, "requires": true, "dependencies": { + "@addbots/eslint-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@addbots/eslint-config/-/eslint-config-1.1.0.tgz", + "integrity": "sha512-6Jm6p/7H9dGK+cMx0XzKoDvUfsvDbuxqFXbngEV0TNrI91xf6Pa9RIslrx/LnxD8NQJd4/Wcr3P/nOfajeqjSg==", + "dev": true + }, + "@addbots/prettier-config": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@addbots/prettier-config/-/prettier-config-1.0.0.tgz", + "integrity": "sha512-M21sKAAUWw5/DpysCrKnF36t4VHuP5QdNWTvYVylQBDqG0zZ0vuUMAgLwfjCMm+EtTiacxGzDBCTN9+ZyQW/Uw==", + "dev": true + }, "@babel/code-frame": { "version": "7.5.5", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", @@ -1980,6 +1992,21 @@ "integrity": "sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==", "dev": true }, + "@types/lodash": { + "version": "4.14.161", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.161.tgz", + "integrity": "sha512-EP6O3Jkr7bXvZZSZYlsgt5DIjiGr0dXP1/jVEwVLTFgg0d+3lWVQkRavYVQszV7dYUwvg0B8R0MBDpcmXg7XIA==", + "dev": true + }, + "@types/lodash.max": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@types/lodash.max/-/lodash.max-4.0.6.tgz", + "integrity": "sha512-zl9Z4eoGcsvziXLCxPV9AcKRn2xhM2BVTl/TiPWh1M7ek3TSjMx8QRBi3IZuVuJZzxIfbuI5lFoteba7qsZ0jA==", + "dev": true, + "requires": { + "@types/lodash": "*" + } + }, "@types/node": { "version": "14.0.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.6.tgz", @@ -7226,6 +7253,11 @@ "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", "dev": true }, + "lodash.max": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.max/-/lodash.max-4.0.1.tgz", + "integrity": "sha1-hzVWbGGLNan3YFILSHrnllivE2o=" + }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", diff --git a/package.json b/package.json index a296a80..1798852 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "postgres-schema-builder", - "version": "1.1.0-beta.2", + "version": "1.2.0-beta.1", "description": "Simple postgres schema builder leveraging Typescript's type system to enable typesafe queries", "keywords": [ "postgres", @@ -38,13 +38,17 @@ "dependencies": { "@types/pg": "^7.11.2", "@types/pg-escape": "^0.2.0", + "lodash.max": "^4.0.1", "moment": "^2.24.0", "pg": "^8.0.3", "pg-escape": "^0.2.0" }, "devDependencies": { + "@addbots/eslint-config": "^1.1.0", + "@addbots/prettier-config": "^1.0.0", "@types/dotenv": "^8.2.0", "@types/jest": "^26.0.0", + "@types/lodash.max": "^4.0.6", "@types/node": "^14.0.0", "@types/uuid": "^8.0.0", "@typescript-eslint/eslint-plugin": "^2.8.0", diff --git a/src/__test__/database-schema.test.ts b/src/__test__/database-schema.test.ts index 5fa4b19..540b286 100644 --- a/src/__test__/database-schema.test.ts +++ b/src/__test__/database-schema.test.ts @@ -1,370 +1,489 @@ -import { makeTestDatabase, makeMockDatabase } from "./utils/make-test-database"; -import { TestTables, TestTableA, TestTableB, TestTableAllTypes, TestTableAllTypesV2 } from "./fixtures/test-tables"; -import { DatabaseSchema, IMigration, Migration, IDatabaseSchemaArgs } from "../database-schema"; -import { composeCreateTableStatements } from "../sql-utils"; -import { ColumnType, ForeignKeyUpdateDeleteRule } from "../table"; -import { SQL } from "../sql"; +import { makeTestDatabase, makeMockDatabase } from "./utils/make-test-database" +import { TestTables, TestTableA, TestTableB, TestTableAllTypes, TestTableAllTypesV2 } from "./fixtures/test-tables" +import { DatabaseSchema, IMigration, Migration, IDatabaseSchemaArgs } from "../database-schema" +import { composeCreateTableStatements } from "../sql-utils" +import { ColumnType, ForeignKeyUpdateDeleteRule } from "../table" +import { SQL } from "../sql" -const cleanupHooks: (() => Promise)[] = []; +const cleanupHooks: (() => Promise)[] = [] afterAll(async () => { - await Promise.all(cleanupHooks.map(hook => hook())); -}); + await Promise.all(cleanupHooks.map((hook) => hook())) +}) const setupTest = async () => { - const { database, cleanupHook } = await makeTestDatabase(); - cleanupHooks.push(cleanupHook); + const { database, cleanupHook } = await makeTestDatabase() + cleanupHooks.push(cleanupHook) - return { database } + return { database } } -describe('init', () => { - test('initializes correctly', async () => { - const { database } = await setupTest() - - const databaseSchema = DatabaseSchema({ - name: 'TestSchema', - client: database, - createStatements: composeCreateTableStatements(TestTables), - migrations: new Map(), - }) - - await databaseSchema.init() - - expect(databaseSchema.name).toBe('TestSchema') - expect(databaseSchema.getVersion()).toBe(1) - }) - - test('initialize twice with restart succeeds', async () => { - const { database } = await setupTest() - - const databaseSchemaConfig: IDatabaseSchemaArgs = { - name: 'TestSchema', - client: database, - createStatements: composeCreateTableStatements(TestTables), - migrations: new Map(), - } - const databaseSchema = DatabaseSchema(databaseSchemaConfig) - await databaseSchema.init() - - // simulate restart - const databaseSchemaOnSecondStart = DatabaseSchema(databaseSchemaConfig) - await databaseSchemaOnSecondStart.init() - - expect(databaseSchemaOnSecondStart.getVersion()).toBe(1) - }) - - test('initialize twice without restart fails', async () => { - const { database } = await setupTest() - - const databaseSchema = DatabaseSchema({ - name: 'TestSchema', - client: database, - createStatements: composeCreateTableStatements(TestTables), - migrations: new Map(), - }) - - await databaseSchema.init() - - await expect(databaseSchema.init()) - .rejects.toThrowError(`Database schema ${databaseSchema.name} has already been initialized.`) - }) +describe("init", () => { + test("initializes correctly", async () => { + const { database } = await setupTest() + + const databaseSchema = DatabaseSchema({ + name: "TestSchema", + client: database, + createStatements: composeCreateTableStatements(TestTables), + migrations: new Map(), + }) + + await databaseSchema.init() + + expect(databaseSchema.name).toBe("TestSchema") + expect(databaseSchema.getVersion()).toBe(1) + }) + + test("initialize twice with restart succeeds", async () => { + const { database } = await setupTest() + + const databaseSchemaConfig: IDatabaseSchemaArgs = { + name: "TestSchema", + client: database, + createStatements: composeCreateTableStatements(TestTables), + migrations: new Map(), + } + const databaseSchema = DatabaseSchema(databaseSchemaConfig) + await databaseSchema.init() + + // simulate restart + const databaseSchemaOnSecondStart = DatabaseSchema(databaseSchemaConfig) + await databaseSchemaOnSecondStart.init() + + expect(databaseSchemaOnSecondStart.getVersion()).toBe(1) + }) + + test("initialize twice without restart fails", async () => { + const { database } = await setupTest() + + const databaseSchema = DatabaseSchema({ + name: "TestSchema", + client: database, + createStatements: composeCreateTableStatements(TestTables), + migrations: new Map(), + }) + + await databaseSchema.init() + + await expect(databaseSchema.init()).rejects.toThrowError( + `Database schema ${databaseSchema.name} has already been initialized.`, + ) + }) }) -describe('migrate latest', () => { - test('valid migrations succeed', async () => { - const { database } = await setupTest() - - const migrations = new Map() - migrations.set(2, Migration(async (transaction) => { - await transaction.query(TestTableB.drop()) - await new Promise(resolve => setTimeout(() => resolve(), 200)) - await transaction.query(TestTableA.addColumns({ - some_newly_added_col_string: { type: ColumnType.Varchar, nullable: false, defaultValue: 'Hello World' }, - some_newly_added_col_boolean: { type: ColumnType.Boolean, nullable: true }, - })) - })) - const columnsToBeRemoved = ['some_newly_added_col_string', 'some_str'] - const newColumnsWithFKConstraints = { - some_new_fk: { - type: ColumnType.Integer, nullable: false, createIndex: true, foreignKeys: [{ - targetTable: TestTableA.name, - targetColumn: 'id', - onDelete: ForeignKeyUpdateDeleteRule.Cascade, - onUpdate: ForeignKeyUpdateDeleteRule.NoAction, - }], - }, - some_new_fk_same_target: { - type: ColumnType.Integer, nullable: false, createIndex: true, foreignKeys: [{ - targetTable: TestTableA.name, - targetColumn: 'id', - onDelete: ForeignKeyUpdateDeleteRule.Cascade, - onUpdate: ForeignKeyUpdateDeleteRule.NoAction, - }], - }, - } - migrations.set(3, Migration(async (transaction) => { - await transaction.query(SQL.raw(SQL.dropColumns(TestTableA.name, columnsToBeRemoved))) - await transaction.query(TestTableAllTypes.addColumns(newColumnsWithFKConstraints)) - })) - migrations.set(4, Migration(async (transaction) => { - await transaction.query(TestTableAllTypesV2.dropColumns(['some_new_fk', 'some_new_fk_same_target'])) - })) - - const databaseSchema = DatabaseSchema({ - name: 'TestSchema', - client: database, - createStatements: composeCreateTableStatements(TestTables), - migrations, - }) - - await databaseSchema.init() - await databaseSchema.migrateLatest() - - expect(databaseSchema.getVersion()).toBe(4) - - const tableAColumnsResults = await database.query(SQL.raw(` +describe("migrate latest", () => { + test("valid migrations succeed", async () => { + const { database } = await setupTest() + + const migrations = new Map() + migrations.set( + 2, + Migration(async ({ transaction }) => { + await transaction.query(TestTableB.drop()) + await new Promise((resolve) => setTimeout(() => resolve(), 200)) + await transaction.query( + TestTableA.addColumns({ + some_newly_added_col_string: { + type: ColumnType.Varchar, + nullable: false, + defaultValue: "Hello World", + }, + some_newly_added_col_boolean: { type: ColumnType.Boolean, nullable: true }, + }), + ) + }), + ) + const columnsToBeRemoved = ["some_newly_added_col_string", "some_str"] + const newColumnsWithFKConstraints = { + some_new_fk: { + type: ColumnType.Integer, + nullable: false, + createIndex: true, + foreignKeys: [ + { + targetTable: TestTableA.name, + targetColumn: "id", + onDelete: ForeignKeyUpdateDeleteRule.Cascade, + onUpdate: ForeignKeyUpdateDeleteRule.NoAction, + }, + ], + }, + some_new_fk_same_target: { + type: ColumnType.Integer, + nullable: false, + createIndex: true, + foreignKeys: [ + { + targetTable: TestTableA.name, + targetColumn: "id", + onDelete: ForeignKeyUpdateDeleteRule.Cascade, + onUpdate: ForeignKeyUpdateDeleteRule.NoAction, + }, + ], + }, + } + migrations.set( + 3, + Migration(async ({ transaction }) => { + await transaction.query(SQL.raw(SQL.dropColumns(TestTableA.name, columnsToBeRemoved))) + await transaction.query(TestTableAllTypes.addColumns(newColumnsWithFKConstraints)) + }), + ) + migrations.set( + 4, + Migration(async ({ transaction }) => { + await transaction.query(TestTableAllTypesV2.dropColumns(["some_new_fk", "some_new_fk_same_target"])) + }), + ) + + const databaseSchema = DatabaseSchema({ + name: "TestSchema", + client: database, + createStatements: composeCreateTableStatements(TestTables), + migrations, + }) + + await databaseSchema.init() + await databaseSchema.migrateLatest() + + expect(databaseSchema.getVersion()).toBe(4) + + const tableAColumnsResults = await database.query( + SQL.raw(` SELECT column_name FROM information_schema.columns WHERE table_name='${TestTableA.name}' AND (column_name='${columnsToBeRemoved[0]}' OR column_name='${columnsToBeRemoved[1]}'); - `)) - - expect(tableAColumnsResults.length).toBe(0) - }) - - test('no schema initialization fails', async () => { - const database = makeMockDatabase() - - const databaseSchema = DatabaseSchema({ - name: 'TestSchema', - client: database, - createStatements: composeCreateTableStatements(TestTables), - migrations: new Map(), - }) - - await expect(databaseSchema.migrateLatest()) - .rejects.toThrowError('Migration failed, database schema is not initialized. Please call init() first on your database schema.') - expect(databaseSchema.getVersion()).toBe(0) - }) - - test('sql error aborts migration', async () => { - const { database } = await setupTest() - - const migrations = new Map() - migrations.set(2, Migration(async (transaction) => { - await transaction.query(TestTableA.dropColumns(['id'])) - })) - - const databaseSchema = DatabaseSchema({ - name: 'TestSchema', - client: database, - createStatements: composeCreateTableStatements(TestTables), - migrations, - }) - - await databaseSchema.init() - - await expect(databaseSchema.migrateLatest()) - .rejects.toThrowError('cannot drop table test_table_a column id because other objects depend on it') - expect(databaseSchema.getVersion()).toBe(1) - }) - - test('missing migration aborts', async () => { - const { database } = await setupTest() - - const migrations = new Map() - migrations.set(2, Migration(async (transaction) => { - await transaction.query(TestTableB.drop()) - })) - migrations.set(4, Migration(async (transaction) => undefined)) - - const databaseSchema = DatabaseSchema({ - name: 'TestSchema', - client: database, - createStatements: composeCreateTableStatements(TestTables), - migrations, - }) - - await databaseSchema.init() - - await expect(databaseSchema.migrateLatest()) - .rejects.toThrowError(`Migration with version 3 not found. Aborting migration process...`) - - expect(databaseSchema.getVersion()).toBe(2) - }) + `), + ) + + expect(tableAColumnsResults.length).toBe(0) + }) + + test("no schema initialization fails", async () => { + const database = makeMockDatabase() + + const databaseSchema = DatabaseSchema({ + name: "TestSchema", + client: database, + createStatements: composeCreateTableStatements(TestTables), + migrations: new Map(), + }) + + await expect(databaseSchema.migrateLatest()).rejects.toThrowError( + "Migration failed, database schema is not initialized. Please call init() first on your database schema.", + ) + expect(databaseSchema.getVersion()).toBe(0) + }) + + test("sql error aborts migration", async () => { + const { database } = await setupTest() + + const migrations = new Map() + migrations.set( + 2, + Migration(async ({ transaction }) => { + await transaction.query(TestTableA.dropColumns(["id"])) + }), + ) + + const databaseSchema = DatabaseSchema({ + name: "TestSchema", + client: database, + createStatements: composeCreateTableStatements(TestTables), + migrations, + }) + + await databaseSchema.init() + + await expect(databaseSchema.migrateLatest()).rejects.toThrow() + expect(databaseSchema.getVersion()).toBe(1) + }) + + test("missing migration aborts", async () => { + const { database } = await setupTest() + + const migrations = new Map() + migrations.set( + 2, + Migration(async ({ transaction }) => { + await transaction.query(TestTableB.drop()) + }), + ) + migrations.set( + 4, + Migration(async () => undefined), + ) + + const databaseSchema = DatabaseSchema({ + name: "TestSchema", + client: database, + createStatements: composeCreateTableStatements(TestTables), + migrations, + }) + + await databaseSchema.init() + + await expect(databaseSchema.migrateLatest()).rejects.toThrowError( + `Migration with version 3 not found. Aborting migration process...`, + ) + + expect(databaseSchema.getVersion()).toBe(2) + }) +}) + +describe("migrate to version", () => { + test("migrate to specific version succeeds", async () => { + const { database } = await setupTest() + + const migration2 = jest.fn() + const migration3 = jest.fn() + const migration4 = jest.fn() + const migration5 = jest.fn() + + const migrations = new Map() + migrations.set(2, Migration(migration2)) + migrations.set(3, Migration(migration3)) + migrations.set(4, Migration(migration4)) + migrations.set(5, Migration(migration5)) + + const databaseSchema = DatabaseSchema({ + name: "TestSchema", + client: database, + createStatements: composeCreateTableStatements(TestTables), + migrations, + }) + + await databaseSchema.init() + await databaseSchema.migrateToVersion(4) + + expect(databaseSchema.getVersion()).toBe(4) + expect(migration2).toBeCalled() + expect(migration3).toBeCalled() + expect(migration4).toBeCalled() + expect(migration5).not.toBeCalled() + }) + + test("migrate to specific version multiple steps succeeds", async () => { + const { database } = await setupTest() + + const migration2 = jest.fn() + const migration3 = jest.fn() + const migration4 = jest.fn() + const migration5 = jest.fn() + + const migrations = new Map() + migrations.set(2, Migration(migration2)) + migrations.set(3, Migration(migration3)) + migrations.set(4, Migration(migration4)) + migrations.set(5, Migration(migration5)) + + const databaseSchema = DatabaseSchema({ + name: "TestSchema", + client: database, + createStatements: composeCreateTableStatements(TestTables), + migrations, + }) + + await databaseSchema.init() + + await databaseSchema.migrateToVersion(2) + expect(databaseSchema.getVersion()).toBe(2) + expect(migration2).toBeCalled() + + await databaseSchema.migrateToVersion(4) + expect(databaseSchema.getVersion()).toBe(4) + expect(migration3).toBeCalled() + expect(migration4).toBeCalled() + + await databaseSchema.migrateToVersion(5) + expect(databaseSchema.getVersion()).toBe(5) + expect(migration5).toBeCalled() + + expect(migration2).toHaveBeenCalledTimes(1) + expect(migration3).toHaveBeenCalledTimes(1) + expect(migration4).toHaveBeenCalledTimes(1) + expect(migration5).toHaveBeenCalledTimes(1) + }) + + test("migrate to lower version than current version does nothing", async () => { + const { database } = await setupTest() + + const migration2 = jest.fn() + const migration3 = jest.fn() + const migration4 = jest.fn() + const migration5 = jest.fn() + + const migrations = new Map() + migrations.set(5, Migration(migration5)) + migrations.set(2, Migration(migration2)) + migrations.set(3, Migration(migration3)) + migrations.set(4, Migration(migration4)) + + const databaseSchema = DatabaseSchema({ + name: "TestSchema", + client: database, + createStatements: composeCreateTableStatements(TestTables), + migrations, + }) + + await databaseSchema.init() + await databaseSchema.migrateToVersion(4) + await databaseSchema.migrateToVersion(2) + + expect(databaseSchema.getVersion()).toBe(4) + expect(migration2).toHaveBeenCalledTimes(1) + expect(migration3).toHaveBeenCalledTimes(1) + expect(migration4).toHaveBeenCalledTimes(1) + expect(migration5).not.toBeCalled() + }) + + test("migrate to version lower 2 fails", async () => { + const { database } = await setupTest() + + const migrations = new Map() + const databaseSchema = DatabaseSchema({ + name: "TestSchema", + client: database, + createStatements: composeCreateTableStatements(TestTables), + migrations, + }) + + await databaseSchema.init() + await expect(databaseSchema.migrateToVersion(1)).rejects.toThrowError( + "Target version of migrateToVersion() has to be greater 1", + ) + await expect(databaseSchema.migrateToVersion(0)).rejects.toThrowError( + "Target version of migrateToVersion() has to be greater 1", + ) + await expect(databaseSchema.migrateToVersion(-5)).rejects.toThrowError( + "Target version of migrateToVersion() has to be greater 1", + ) + }) }) -describe('migrate to version', () => { - test('migrate to specific version succeeds', async () => { - const { database } = await setupTest() - - const migration2 = jest.fn() - const migration3 = jest.fn() - const migration4 = jest.fn() - const migration5 = jest.fn() - - const migrations = new Map() - migrations.set(2, Migration(migration2)) - migrations.set(3, Migration(migration3)) - migrations.set(4, Migration(migration4)) - migrations.set(5, Migration(migration5)) - - const databaseSchema = DatabaseSchema({ - name: 'TestSchema', - client: database, - createStatements: composeCreateTableStatements(TestTables), - migrations, - }) - - await databaseSchema.init() - await databaseSchema.migrateToVersion(4) - - expect(databaseSchema.getVersion()).toBe(4) - expect(migration2).toBeCalled() - expect(migration3).toBeCalled() - expect(migration4).toBeCalled() - expect(migration5).not.toBeCalled() - }) - - test('migrate to specific version multiple steps succeeds', async () => { - const { database } = await setupTest() - - const migration2 = jest.fn() - const migration3 = jest.fn() - const migration4 = jest.fn() - const migration5 = jest.fn() - - const migrations = new Map() - migrations.set(2, Migration(migration2)) - migrations.set(3, Migration(migration3)) - migrations.set(4, Migration(migration4)) - migrations.set(5, Migration(migration5)) - - const databaseSchema = DatabaseSchema({ - name: 'TestSchema', - client: database, - createStatements: composeCreateTableStatements(TestTables), - migrations, - }) - - await databaseSchema.init() - - await databaseSchema.migrateToVersion(2) - expect(databaseSchema.getVersion()).toBe(2) - expect(migration2).toBeCalled() - - await databaseSchema.migrateToVersion(4) - expect(databaseSchema.getVersion()).toBe(4) - expect(migration3).toBeCalled() - expect(migration4).toBeCalled() - - await databaseSchema.migrateToVersion(5) - expect(databaseSchema.getVersion()).toBe(5) - expect(migration5).toBeCalled() - - expect(migration2).toHaveBeenCalledTimes(1) - expect(migration3).toHaveBeenCalledTimes(1) - expect(migration4).toHaveBeenCalledTimes(1) - expect(migration5).toHaveBeenCalledTimes(1) - }) - - test('migrate to lower version than current version does nothing', async () => { - const { database } = await setupTest() - - const migration2 = jest.fn() - const migration3 = jest.fn() - const migration4 = jest.fn() - const migration5 = jest.fn() - - const migrations = new Map() - migrations.set(5, Migration(migration5)) - migrations.set(2, Migration(migration2)) - migrations.set(3, Migration(migration3)) - migrations.set(4, Migration(migration4)) - - const databaseSchema = DatabaseSchema({ - name: 'TestSchema', - client: database, - createStatements: composeCreateTableStatements(TestTables), - migrations, - }) - - await databaseSchema.init() - await databaseSchema.migrateToVersion(4) - await databaseSchema.migrateToVersion(2) - - expect(databaseSchema.getVersion()).toBe(4) - expect(migration2).toHaveBeenCalledTimes(1) - expect(migration3).toHaveBeenCalledTimes(1) - expect(migration4).toHaveBeenCalledTimes(1) - expect(migration5).not.toBeCalled() - }) - - test('migrate to version lower 2 fails', async () => { - const { database } = await setupTest() - - const migrations = new Map() - const databaseSchema = DatabaseSchema({ - name: 'TestSchema', - client: database, - createStatements: composeCreateTableStatements(TestTables), - migrations, - }) - - await databaseSchema.init() - await expect(databaseSchema.migrateToVersion(1)) - .rejects.toThrowError('Target version of migrateToVersion() has to be greater 1') - await expect(databaseSchema.migrateToVersion(0)) - .rejects.toThrowError('Target version of migrateToVersion() has to be greater 1') - await expect(databaseSchema.migrateToVersion(-5)) - .rejects.toThrowError('Target version of migrateToVersion() has to be greater 1') - }) +describe("multi-node environment", () => { + const simulateNode = async (nodeOprations: (...args: any[]) => Promise, nodeName: string) => { + await nodeOprations(nodeName) + } + + test("test", async () => { + const { database } = await setupTest() + + const migration2 = jest.fn() + const migration3 = jest.fn() + const migration4 = jest.fn() + const migration5 = jest.fn() + + const migrations = new Map() + migrations.set(5, Migration(migration5)) + migrations.set(2, Migration(migration2)) + migrations.set(3, Migration(migration3)) + migrations.set(4, Migration(migration4)) + + const operations = async (nodeName: string) => { + const databaseSchema = DatabaseSchema({ + name: "TestSchema", + client: database, + createStatements: composeCreateTableStatements(TestTables), + migrations, + }) + + await databaseSchema.init() + await databaseSchema.migrateLatest() + + expect(databaseSchema.getVersion()).toBe(5) + } + + await Promise.all([ + simulateNode(operations, "Node1"), + simulateNode(operations, "Node2"), + simulateNode(operations, "Node3"), + simulateNode(operations, "Node4"), + ]) + + expect(migration2).toHaveBeenCalledTimes(1) + expect(migration3).toHaveBeenCalledTimes(1) + expect(migration4).toHaveBeenCalledTimes(1) + expect(migration5).toHaveBeenCalledTimes(1) + }) }) -describe('multi-node environment', () => { - const simulateNode = async (nodeOprations: (...args: any[]) => Promise, nodeName: string) => { - await nodeOprations(nodeName) - } - - test('test', async () => { - const { database } = await setupTest() - - const migration2 = jest.fn() - const migration3 = jest.fn() - const migration4 = jest.fn() - const migration5 = jest.fn() - - const migrations = new Map() - migrations.set(5, Migration(migration5)) - migrations.set(2, Migration(migration2)) - migrations.set(3, Migration(migration3)) - migrations.set(4, Migration(migration4)) - - const operations = async (nodeName: string) => { - const databaseSchema = DatabaseSchema({ - name: 'TestSchema', - client: database, - createStatements: composeCreateTableStatements(TestTables), - migrations, - }) - - await databaseSchema.init() - await databaseSchema.migrateLatest() - - expect(databaseSchema.getVersion()).toBe(5) - } - - await Promise.all([ - simulateNode(operations, 'Node1'), - simulateNode(operations, 'Node2'), - simulateNode(operations, 'Node3'), - simulateNode(operations, 'Node4'), - ]) - - expect(migration2).toHaveBeenCalledTimes(1) - expect(migration3).toHaveBeenCalledTimes(1) - expect(migration4).toHaveBeenCalledTimes(1) - expect(migration5).toHaveBeenCalledTimes(1) - }) +describe("read access during migration", () => { + test("usage of database client is non-blocking", async () => { + const { database } = await setupTest() + + const migrations = new Map() + migrations.set( + 2, + Migration(async ({ database }) => { + const statements = [TestTableB.drop()] + + const data = await database.query(TestTableB.selectAll("*")) + + expect(data).toBeDefined() + + return statements + }), + ) + + const databaseSchema = DatabaseSchema({ + name: "TestSchema", + client: database, + createStatements: composeCreateTableStatements(TestTables), + migrations, + }) + + await databaseSchema.init() + await databaseSchema.migrateLatest() + + expect(databaseSchema.getVersion()).toBe(2) + + const tableBColumnsResults = await database.query( + SQL.raw(` + SELECT * + FROM information_schema.tables + WHERE table_name='${TestTableB.name}'; + `), + ) + + expect(tableBColumnsResults.length).toBe(0) + }) + + // this doesn't work right now :() + test.skip("usage of transaction client is blocking", async (done) => { + const { database } = await setupTest() + + const migrations = new Map() + migrations.set( + 2, + Migration(async ({ transaction, database }) => { + await transaction.query(TestTableB.drop()) + + await database.query(TestTableB.selectAll("*")) + }), + ) + + const databaseSchema = DatabaseSchema({ + name: "TestSchema", + client: database, + createStatements: composeCreateTableStatements(TestTables), + migrations, + }) + + await databaseSchema.init() + + const promise = new Promise(async (resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error("Migration got stuck")) + }, 2000) + await databaseSchema.migrateLatest() + + clearTimeout(timeout) + resolve() + }) + + await expect(promise).rejects.toThrowError("Migration got stuck") + }) }) diff --git a/src/database-schema.ts b/src/database-schema.ts index 5800545..0b611d9 100644 --- a/src/database-schema.ts +++ b/src/database-schema.ts @@ -1,161 +1,202 @@ -import { IDatabaseClient, IDatabaseBaseClient } from './database-client' -import { TableSchema, ColumnType, NativeFunction, Table } from './table'; -import { SQL } from './sql'; +import { IDatabaseClient, IDatabaseBaseClient } from "./database-client" +import { TableSchema, ColumnType, NativeFunction, Table, IQuery } from "./table" +import { SQL } from "./sql" +import max from "lodash.max" const schema_management = TableSchema({ - name: { type: ColumnType.Varchar, primaryKey: true, nullable: false, unique: true }, - version: { type: ColumnType.Integer, nullable: false }, - date_added: { type: ColumnType.TimestampTZ, nullable: false, defaultValue: { func: NativeFunction.Now } }, - locked: { type: ColumnType.Boolean, nullable: false, defaultValue: false }, + name: { + type: ColumnType.Varchar, + primaryKey: true, + nullable: false, + unique: true, + }, + version: { type: ColumnType.Integer, nullable: false }, + date_added: { + type: ColumnType.TimestampTZ, + nullable: false, + defaultValue: { func: NativeFunction.Now }, + }, + locked: { type: ColumnType.Boolean, nullable: false, defaultValue: false }, }) -const SchemaManagementTable = Table({ schema_management }, 'schema_management') +const SchemaManagementTable = Table({ schema_management }, "schema_management") -const selectVersionQuery = (name: string) => SchemaManagementTable.select('*', ['name'])([name]) +const selectVersionQuery = (name: string) => SchemaManagementTable.select("*", ["name"])([name]) const insertSchemaQuery = (name: string, version: number) => SchemaManagementTable.insertFromObj({ name, version }) -const updateSchemaVersionQuery = (name: string, newVersion: number) => SchemaManagementTable.update(['version'], ['name'])([newVersion], [name]) +const updateSchemaVersionQuery = (name: string, newVersion: number) => + SchemaManagementTable.update(["version"], ["name"])([newVersion], [name]) export type IDatabaseSchema = ReturnType +export interface IUpDownArgs { + transaction: IDatabaseBaseClient + database: IDatabaseBaseClient +} + export interface IMigration { - up: (client: IDatabaseBaseClient) => Promise; + up: (args: IUpDownArgs) => Promise[]> } -export const Migration = (up: (client: IDatabaseBaseClient) => Promise): IMigration => ({ up }) +export const Migration = (up: (args: IUpDownArgs) => Promise[]>): IMigration => ({ up }) export type CreateStatement = string export interface IDatabaseSchemaArgs { - name: string; - client: IDatabaseClient; - createStatements: CreateStatement[]; - migrations: Map; - logMigrations?: boolean; + name: string + client: IDatabaseClient + createStatements: CreateStatement[] + migrations: Map + logMigrations?: boolean } export const DatabaseSchema = ({ client, createStatements, name, migrations, logMigrations }: IDatabaseSchemaArgs) => { - let version = 0 - let isInitialized = false - - const init = async () => { - if (isInitialized) { - throw new Error(`Database schema ${name} has already been initialized.`) - } - - try { - await client.transaction(async (transaction) => { - await transaction.query(SchemaManagementTable.create()) - - const versionDBResults = await transaction.query(selectVersionQuery(name)) - - if (versionDBResults.length === 0) { - await transaction.query({ - sql: createStatements.join('\n'), - }) - await transaction.query(insertSchemaQuery(name, 1)) - - version = 1 - } else { - version = versionDBResults[0].version - } - }) - } catch (err) { - if (err.message.indexOf('duplicate key value violates unique constraint') === -1) { - throw err - } - } - - isInitialized = true - } - - const throwNotInitialized = () => { - throw new Error(`Migration failed, database schema is not initialized. Please call init() first on your database schema.`) - } - - const lockSchemaTableQuery = SQL.raw(` + let version = 0 + let isInitialized = false + + const getLatestMigrationVersion = () => { + return parseInt(max(Object.keys(migrations)) || "1") + } + + const init = async () => { + if (isInitialized) { + throw new Error(`Database schema ${name} has already been initialized.`) + } + + try { + await client.transaction(async (transaction) => { + await transaction.query(SchemaManagementTable.create()) + + const versionDBResults = await transaction.query(selectVersionQuery(name)) + + if (versionDBResults.length === 0) { + const initialVersion = getLatestMigrationVersion() + + await transaction.query({ + sql: createStatements.join("\n"), + }) + await transaction.query(insertSchemaQuery(name, initialVersion)) + + version = initialVersion + } else { + version = versionDBResults[0].version + } + }) + } catch (err) { + if (err.message.indexOf("duplicate key value violates unique constraint") === -1) { + throw err + } + } + + isInitialized = true + } + + const throwNotInitialized = () => { + throw new Error( + `Migration failed, database schema is not initialized. Please call init() first on your database schema.`, + ) + } + + const lockSchemaTableQuery = SQL.raw( + ` LOCK TABLE ${SchemaManagementTable.name} IN ACCESS EXCLUSIVE MODE; - `, []) - const getSchemaVersionQuery = (awaitLock: boolean) => SQL.raw(` + `, + [], + ) + const getSchemaVersionQuery = (awaitLock: boolean) => + SQL.raw( + ` SELECT * FROM ${SchemaManagementTable.name} - WHERE name = $1 ${!awaitLock ? 'FOR UPDATE NOWAIT' : ''}; - `, [name]) - const setSchemaLockQuery = (locked: boolean) => SQL.raw(` + WHERE name = $1 ${!awaitLock ? "FOR UPDATE NOWAIT" : ""}; + `, + [name], + ) + const setSchemaLockQuery = (locked: boolean) => + SQL.raw( + ` UPDATE ${SchemaManagementTable.name} SET locked = $1 WHERE name=$2; - `, [locked, name]) + `, + [locked, name], + ) - /* + /* Locks schema_management table for given transaction and retrievs current schema version If table is already locked, the postgres client is advised to await execution until lock is released This ensures, that in a multi-node environment all starting nodes proceed code execution after all migrations are done */ - const getCurrentVersionAndLockSchema = async (client: IDatabaseBaseClient, awaitLock: boolean) => { - await client.query(lockSchemaTableQuery) - const dbResults = await client.query(getSchemaVersionQuery(awaitLock)) + const getCurrentVersionAndLockSchema = async (client: IDatabaseBaseClient, awaitLock: boolean) => { + await client.query(lockSchemaTableQuery) + const dbResults = await client.query(getSchemaVersionQuery(awaitLock)) - if (dbResults.length === 1 && dbResults[0].locked === false) { - await client.query(setSchemaLockQuery(true)) + if (dbResults.length === 1 && dbResults[0].locked === false) { + await client.query(setSchemaLockQuery(true)) - return dbResults[0].version - } + return dbResults[0].version + } - return null - } + return null + } - const migrateToVersion = async (targetVersion: number) => { - if (!isInitialized) throwNotInitialized() + const migrateToVersion = async (targetVersion: number) => { + if (!isInitialized) throwNotInitialized() - if (targetVersion <= 1) { - throw new Error('Target version of migrateToVersion() has to be greater 1') - } + if (targetVersion <= 1) { + throw new Error("Target version of migrateToVersion() has to be greater 1") + } - for (let newVersion = version; newVersion <= targetVersion; newVersion++) { - await client.transaction(async (transaction) => { - const currentVersion = await getCurrentVersionAndLockSchema(transaction, true) + for (let newVersion = version; newVersion <= targetVersion; newVersion++) { + await client.transaction(async (transaction) => { + const currentVersion = await getCurrentVersionAndLockSchema(transaction, true) - if (currentVersion === null || currentVersion >= newVersion) { - if (currentVersion) { - await transaction.query(setSchemaLockQuery(false)) - } + if (currentVersion === null || currentVersion >= newVersion) { + if (currentVersion) { + await transaction.query(setSchemaLockQuery(false)) + } - return - } + return + } - const migration = migrations.get(newVersion) + const migration = migrations.get(newVersion) - if (!migration) { - await transaction.query(setSchemaLockQuery(false)) + if (!migration) { + await transaction.query(setSchemaLockQuery(false)) - throw new Error(`Migration with version ${newVersion} not found. Aborting migration process...`) - } + throw new Error(`Migration with version ${newVersion} not found. Aborting migration process...`) + } - await migration.up(transaction) - await transaction.query(updateSchemaVersionQuery(name, newVersion)) - await transaction.query(setSchemaLockQuery(false)) + const migrationQueries = await migration.up({ transaction, database: client }) - // istanbul ignore next - if (!(logMigrations === false)) { - console.info(`Successfully migrated ${name} from version ${version} to ${newVersion}`) - } - }) + if (Array.isArray(migrationQueries)) { + for (const migrationQuery of migrationQueries) { + await transaction.query(migrationQuery) + } + } - version = newVersion - } - } + await transaction.query(updateSchemaVersionQuery(name, newVersion)) + await transaction.query(setSchemaLockQuery(false)) - const migrateLatest = async () => { - const sortedMigrationKeys = Array.from(migrations.keys()).sort() - const latestVersion = sortedMigrationKeys[sortedMigrationKeys.length - 1] + // istanbul ignore next + if (!(logMigrations === false)) { + console.info(`Successfully migrated ${name} from version ${version} to ${newVersion}`) + } + }) - await migrateToVersion(latestVersion) - } + version = newVersion + } + } - const getVersion = () => version + const migrateLatest = async () => { + const sortedMigrationKeys = Array.from(migrations.keys()).sort() + const latestVersion = sortedMigrationKeys[sortedMigrationKeys.length - 1] - return { - name, - getVersion, - init, - migrateLatest, - migrateToVersion, - } -} + await migrateToVersion(latestVersion) + } + const getVersion = () => version + + return { + name, + getVersion, + init, + migrateLatest, + migrateToVersion, + } +} diff --git a/src/sql.ts b/src/sql.ts index 0a1436b..f990cd8 100644 --- a/src/sql.ts +++ b/src/sql.ts @@ -1,70 +1,90 @@ -import { Columns, Column, IReferenceConstraintInternal, isCollection, isSQLFunction, ForeignKeyUpdateDeleteRule, ICreateIndexStatement, IQuery, isJSONType, IWhereConditionColumned, ISQLArg } from "./table"; -import * as pgEscape from 'pg-escape'; -import { dateToSQLUTCFormat } from "./sql-utils"; -import * as moment from 'moment' -import { flatten } from './utils' - -const isStringArray = (arr: any): arr is string[] => Array.isArray(arr) && arr.every(item => typeof item === 'string') +import { + Columns, + Column, + IReferenceConstraintInternal, + isCollection, + isSQLFunction, + ForeignKeyUpdateDeleteRule, + ICreateIndexStatement, + IQuery, + isJSONType, + IWhereConditionColumned, + ISQLArg, +} from "./table" +import pgEscape from "pg-escape" +import { dateToSQLUTCFormat } from "./sql-utils" +import moment from "moment" +import { flatten } from "./utils" + +const isStringArray = (arr: any): arr is string[] => Array.isArray(arr) && arr.every((item) => typeof item === "string") export namespace SQL { - export const createDatabase = (name: string) => `CREATE DATABASE ${name};`; + export const createDatabase = (name: string) => `CREATE DATABASE ${name};` export const createTable = (name: string, columns: Columns): string => { - const entries = Object.entries(columns).map(([name, column]) => ({ name, ...column })); - const foreignKeyConstraints: IReferenceConstraintInternal[] = collectForeignKeyConstraints(entries); + const entries = Object.entries(columns).map(([name, column]) => ({ name, ...column })) + const foreignKeyConstraints: IReferenceConstraintInternal[] = collectForeignKeyConstraints(entries) - const primaryKeyColoumns = entries.filter(col => { - return col.primaryKey !== undefined && col.primaryKey; - }); + const primaryKeyColoumns = entries.filter((col) => { + return col.primaryKey !== undefined && col.primaryKey + }) if (primaryKeyColoumns.length === 0) { - throw new Error(`Primary Key(s) missing. Cannot create table ${name}.`); + throw new Error(`Primary Key(s) missing. Cannot create table ${name}.`) } const createTableQuery = ` CREATE TABLE IF NOT EXISTS ${name} ( ${entries - .map(prepareCreateColumnStatement) - .concat([ - `CONSTRAINT PK_${name}_${primaryKeyColoumns.map(pkc => pkc.name).join('_')} PRIMARY KEY (${primaryKeyColoumns.map(pkc => `"${pkc.name}"`).join(',')})`, - ]) - .concat( - prepareForeignKeyConstraintStatements(name, foreignKeyConstraints) - .map(stmt => `CONSTRAINT ${stmt}`) - ) - .join(',\n')} + .map(prepareCreateColumnStatement) + .concat([ + `CONSTRAINT PK_${name}_${primaryKeyColoumns + .map((pkc) => pkc.name) + .join("_")} PRIMARY KEY (${primaryKeyColoumns.map((pkc) => `"${pkc.name}"`).join(",")})`, + ]) + .concat( + prepareForeignKeyConstraintStatements(name, foreignKeyConstraints).map( + (stmt) => `CONSTRAINT ${stmt}`, + ), + ) + .join(",\n")} ); - `; - - const indexStatements: ICreateIndexStatement[] = entries.filter((col): boolean => { - return (col.unique !== undefined && col.unique) || (col.createIndex !== undefined && col.createIndex); - }).map((col) => { - return { - column: col.name, - unique: col.unique !== undefined && col.unique, - } - }); + ` - const createIndexQueries = indexStatements.map(indexStatement => createIndex(indexStatement.unique, name, indexStatement.column)); + const indexStatements: ICreateIndexStatement[] = entries + .filter((col): boolean => { + return (col.unique !== undefined && col.unique) || (col.createIndex !== undefined && col.createIndex) + }) + .map((col) => { + return { + column: col.name, + unique: col.unique !== undefined && col.unique, + } + }) + + const createIndexQueries = indexStatements.map((indexStatement) => + createIndex(indexStatement.unique, name, indexStatement.column), + ) return ` ${createTableQuery} - ${createIndexQueries.join('\n')} - `; + ${createIndexQueries.join("\n")} + ` } export const addColumns = (tableName: string, columns: Columns): string => { - const entries = Object.entries(columns).map(([name, column]) => ({ name, ...column })); - const foreignKeyConstraints: IReferenceConstraintInternal[] = collectForeignKeyConstraints(entries); + const entries = Object.entries(columns).map(([name, column]) => ({ name, ...column })) + const foreignKeyConstraints: IReferenceConstraintInternal[] = collectForeignKeyConstraints(entries) const addForeignKeyConstraintsStatements = ` ${prepareForeignKeyConstraintStatements(tableName, foreignKeyConstraints) - .map(constraint => `ALTER TABLE ${tableName} ADD CONSTRAINT ${constraint}`).join(';\n')} + .map((constraint) => `ALTER TABLE ${tableName} ADD CONSTRAINT ${constraint}`) + .join(";\n")} ` const addTableColumnStatement = ` ALTER TABLE ${tableName} - ${entries.map(entry => `ADD COLUMN ${prepareCreateColumnStatement(entry)}`).join(',\n')}; + ${entries.map((entry) => `ADD COLUMN ${prepareCreateColumnStatement(entry)}`).join(",\n")}; ` return ` @@ -74,33 +94,35 @@ export namespace SQL { } type DropColumns = { - (tableName: string, columns: Columns): string; - (tableName: string, columns: string[], constraints?: string[]): string; + (tableName: string, columns: Columns): string + (tableName: string, columns: string[], constraints?: string[]): string } - export const dropColumns: DropColumns = (tableName: string, columns: Columns | string[], constraints?: string[]): string => { - const columnNames = isStringArray(columns) - ? columns - : Object.keys(columns) + export const dropColumns: DropColumns = ( + tableName: string, + columns: Columns | string[], + constraints?: string[], + ): string => { + const columnNames = isStringArray(columns) ? columns : Object.keys(columns) let constraintNames: string[] = [] if (constraints && constraints.length > 0) { constraintNames = constraints } else if (!isStringArray(columns)) { - const entries = Object.entries(columns).map(([name, column]) => ({ name, ...column })); + const entries = Object.entries(columns).map(([name, column]) => ({ name, ...column })) const foreignKeyConstraints = collectForeignKeyConstraints(entries) - constraintNames = foreignKeyConstraints.map(fkc => `${tableName}_${fkc.column}_fkey`) + constraintNames = foreignKeyConstraints.map((fkc) => `${tableName}_${fkc.column}_fkey`) } const dropTableColumnsStatement = ` ALTER TABLE ${tableName} - ${columnNames.map(column => `DROP COLUMN ${column}`).join(',\n')}; + ${columnNames.map((column) => `DROP COLUMN ${column}`).join(",\n")}; ` const dropConstraintsStatement = constraintNames - .map(constraint => `ALTER TABLE ${tableName} DROP CONSTRAINT ${constraint};`) - .join('\n') + .map((constraint) => `ALTER TABLE ${tableName} DROP CONSTRAINT ${constraint};`) + .join("\n") return ` ${dropConstraintsStatement} @@ -109,30 +131,31 @@ export namespace SQL { } export const insert = (tableName: string, subset: string[]) => { - const cql = `INSERT INTO ${tableName}` - + ` ( ${subset.map(column => `"${column}"`).join(", ")} )` - + ` VALUES ( ${subset.map((_, idx) => `$${idx + 1}`).join(', ')} );`; + const cql = + `INSERT INTO ${tableName}` + + ` ( ${subset.map((column) => `"${column}"`).join(", ")} )` + + ` VALUES ( ${subset.map((_, idx) => `$${idx + 1}`).join(", ")} );` - return cql; + return cql } export const update = (tableName: string, subset: string[], where: string[]) => { - const cql = `UPDATE ${tableName} ` - + `SET ${subset.map((col, idx) => `"${col}" = $${idx + 1}`).join(', ')} ` - + `WHERE ${where.map((col, idx) => `"${col}" = $${subset.length + idx + 1}`).join(' AND ')};` + const cql = + `UPDATE ${tableName} ` + + `SET ${subset.map((col, idx) => `"${col}" = $${idx + 1}`).join(", ")} ` + + `WHERE ${where.map((col, idx) => `"${col}" = $${subset.length + idx + 1}`).join(" AND ")};` - return cql; + return cql } - export const selectAll = (tableName: string, subset: string[] | '*') => { - const cql = `SELECT ${subset === '*' ? '*' : subset.join(', ')} ` - + `FROM ${tableName};`; + export const selectAll = (tableName: string, subset: string[] | "*") => { + const cql = `SELECT ${subset === "*" ? "*" : subset.join(", ")} ` + `FROM ${tableName};` - return cql; + return cql } const whereConditionToString = (cond: string | ISQLArg, idx: number) => { - if (typeof cond === 'string') { + if (typeof cond === "string") { return `("${cond}" = $${idx + 1})` } @@ -140,109 +163,127 @@ export namespace SQL { } type Select = { - (tableName: string, subset: string[] | '*', where: string[]): string; - (tableName: string, subset: string[] | '*', where: ISQLArg[]): string; + (tableName: string, subset: string[] | "*", where: string[]): string + (tableName: string, subset: string[] | "*", where: ISQLArg[]): string } - export const select: Select = (tableName: string, subset: string[] | '*', where: (string | IWhereConditionColumned)[]) => { - const cql = `SELECT ${subset === "*" ? "*" : subset.map(column => `"${column}"`).join(", ")}` - + ` FROM ${tableName}` - + ` WHERE ${where.map(whereConditionToString).join(' AND ')}` - + `;`; - - return cql; + export const select: Select = ( + tableName: string, + subset: string[] | "*", + where: (string | IWhereConditionColumned)[], + ) => { + const cql = + `SELECT ${subset === "*" ? "*" : subset.map((column) => `"${column}"`).join(", ")}` + + ` FROM ${tableName}` + + ` WHERE ${where.map(whereConditionToString).join(" AND ")}` + + `;` + + return cql } export const deleteEntry = (tableName: string, where: string[]) => { - const cql = `DELETE` - + ` FROM ${tableName}` - + ` WHERE ${where.map((column, i) => `("${column}" = $${i + 1})`).join(' AND ')}` - + `;`; + const cql = + `DELETE` + + ` FROM ${tableName}` + + ` WHERE ${where.map((column, i) => `("${column}" = $${i + 1})`).join(" AND ")}` + + `;` - return cql; + return cql } - export const dropTable = (tableName: string) => `DROP TABLE ${tableName};`; + export const dropTable = (tableName: string) => `DROP TABLE ${tableName};` export const createIndex = (unique: boolean, name: string, column: string): string => { - const sql = `CREATE ${unique ? 'UNIQUE ' : ''}INDEX IF NOT EXISTS ${name}_${column}_${unique ? 'u' : ''}index ON ${name} (${column});` + const sql = `CREATE ${unique ? "UNIQUE " : ""}INDEX IF NOT EXISTS ${name}_${column}_${ + unique ? "u" : "" + }index ON ${name} (${column});` - return sql; + return sql } export const raw = (sql: string, values: unknown[] = []): IQuery => ({ sql, values, - }); + }) } const collectForeignKeyConstraints = (columns: ({ name: string } & Column)[]): IReferenceConstraintInternal[] => { return flatten( - columns.map(col => col.foreignKeys - ? col.foreignKeys.map(fkc => ({ ...fkc, column: col.name })) - : [] - ) + columns.map((col) => (col.foreignKeys ? col.foreignKeys.map((fkc) => ({ ...fkc, column: col.name })) : [])), ) } -const prepareCreateColumnStatement = (col: ({ name: string } & Column)): string => { - const replaceArr: any[] = []; +const prepareCreateColumnStatement = (col: { name: string } & Column): string => { + const replaceArr: any[] = [] if (col.defaultValue !== undefined) { - replaceArr.push(col.defaultValue); + replaceArr.push(col.defaultValue) } - return `"${col.name}" ${!col.autoIncrement ? mapColumnType(col) : ''} ` + - `${col.autoIncrement ? 'SERIAL ' : ''}` + - `${col.nullable !== undefined && !col.nullable ? 'NOT NULL ' : ''}` + - `${col.defaultValue !== undefined ? `DEFAULT ${isSQLFunction(col.defaultValue) ? - col.defaultValue.func : mapValues(col.defaultValue)}` : ''}`; + return ( + `"${col.name}" ${!col.autoIncrement ? mapColumnType(col) : ""} ` + + `${col.autoIncrement ? "SERIAL " : ""}` + + `${col.nullable !== undefined && !col.nullable ? "NOT NULL " : ""}` + + `${ + col.defaultValue !== undefined + ? `DEFAULT ${isSQLFunction(col.defaultValue) ? col.defaultValue.func : mapValues(col.defaultValue)}` + : "" + }` + ) } -const prepareForeignKeyConstraintStatements = (tableName: string, foreignKeyConstraints: IReferenceConstraintInternal[]): string[] => { - return foreignKeyConstraints - .map(fkc => +const prepareForeignKeyConstraintStatements = ( + tableName: string, + foreignKeyConstraints: IReferenceConstraintInternal[], +): string[] => { + return foreignKeyConstraints.map( + (fkc) => `${tableName}_${fkc.column}_fkey FOREIGN KEY (${fkc.column}) REFERENCES ${fkc.targetTable} (${fkc.targetColumn}) - ${fkc.onDelete !== undefined ? mapUpdateDeleteRule(fkc.onDelete, false) : ''} - ${fkc.onUpdate !== undefined ? mapUpdateDeleteRule(fkc.onUpdate, true) : ''}` - ); + ${fkc.onDelete !== undefined ? mapUpdateDeleteRule(fkc.onDelete, false) : ""} + ${fkc.onUpdate !== undefined ? mapUpdateDeleteRule(fkc.onUpdate, true) : ""}`, + ) } const mapColumnType = (col: Column) => { if (isJSONType(col.type)) { - return 'JSON'; + return "JSON" } else if (isCollection(col.type)) { - return col.type.type.toUpperCase() + '[]'; + return col.type.type.toUpperCase() + "[]" } else { - return col.type.toUpperCase(); + return col.type.toUpperCase() } } const mapValues = (val: any): any => { if (val === undefined || val === null) { - return 'NULL'; - } else if (typeof val === 'string') { - return pgEscape('%L', val); + return "NULL" + } else if (typeof val === "string") { + return pgEscape("%L", val) } else if (moment.isMoment(val)) { - return `'${dateToSQLUTCFormat(val.utc().toDate())}'`; + return `'${dateToSQLUTCFormat(val.utc().toDate())}'` } else if (val instanceof Date) { - return `'${dateToSQLUTCFormat(val)}'`; - } else if (typeof val === 'object') { - return mapValues(JSON.stringify(val)); + return `'${dateToSQLUTCFormat(val)}'` + } else if (typeof val === "object") { + return mapValues(JSON.stringify(val)) } else { - return val; + return val } } const mapUpdateDeleteRule = (rule: ForeignKeyUpdateDeleteRule, isUpdate: boolean): string => { - const prefix = isUpdate ? 'UPDATE' : 'DELETE'; + const prefix = isUpdate ? "UPDATE" : "DELETE" switch (rule) { - case ForeignKeyUpdateDeleteRule.Cascade: return `ON ${prefix} CASCADE`; - case ForeignKeyUpdateDeleteRule.NoAction: return ''; - case ForeignKeyUpdateDeleteRule.Restrict: return ''; - case ForeignKeyUpdateDeleteRule.SetDefault: return `ON ${prefix} SET DEFAULT`; - case ForeignKeyUpdateDeleteRule.SetNull: return `ON ${prefix} SET NULL`; + case ForeignKeyUpdateDeleteRule.Cascade: + return `ON ${prefix} CASCADE` + case ForeignKeyUpdateDeleteRule.NoAction: + return "" + case ForeignKeyUpdateDeleteRule.Restrict: + return "" + case ForeignKeyUpdateDeleteRule.SetDefault: + return `ON ${prefix} SET DEFAULT` + case ForeignKeyUpdateDeleteRule.SetNull: + return `ON ${prefix} SET NULL` } } diff --git a/tsconfig.json b/tsconfig.json index 175e823..a7d22bb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,12 +14,8 @@ "strictNullChecks": true, "skipDefaultLibCheck": true, "sourceMap": true, + "esModuleInterop": true }, - "include": [ - "src/index.ts", - ], - "exclude": [ - "node_modules", - "build" - ] -} \ No newline at end of file + "include": ["src/index.ts"], + "exclude": ["node_modules", "build"] +}