Skip to content

Commit

Permalink
Added UmzeptionStorage + pg context and storage
Browse files Browse the repository at this point in the history
  • Loading branch information
voxpelli committed Aug 6, 2023
1 parent ea5b856 commit 626c733
Show file tree
Hide file tree
Showing 11 changed files with 301 additions and 10 deletions.
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,19 @@ const umzeptionSetup = umzeption({
### Use with Umzug

```javascript
import { Sequelize } from 'sequelize';
import { Umzug, SequelizeStorage } from 'umzug';
import pg from 'pg';
import { UmzeptionPgStorage, createUmzeptionPgContext } from 'umzeption';
import { Umzug } from 'umzug';

// TODO: add example for how to create umzeptionContexts
const pool = new Pool({
host: 'localhost',
user: 'database-user',
});

const umzug = new Umzug({
migrations: umzeptionSetup,
context: umzeptionContext,
storage: new SequelizeStorage({ sequelize }),
context: createUmzeptionPgContext(pool),
storage: new UmzeptionPgStorage(),
logger: console,
});

Expand Down
15 changes: 13 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
export type {
UmzeptionDependency,
UmzeptionLookupOptions,
AnyUmzeptionContext,
DefineUmzeptionContexts,
FastifyPostgresStyleDb,
UmzeptionContext,
UmzeptionDependency,
UmzeptionLookupOptions,
UmzeptionStorage,
} from './lib/advanced-types.d.ts';

export {
createUmzeptionPgContext,
UmzeptionPgStorage,
} from './lib/context-pg/main.js';

export {
createUmzeptionContext,
} from './lib/context.js';

export {
umzeption,
} from './lib/main.js';

export {
BaseUmzeptionStorage,
} from './lib/storage.js';
10 changes: 10 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
// TODO: Extract pg context into a "umzeption-pg" module
export {
createUmzeptionPgContext,
UmzeptionPgStorage,
} from './lib/context-pg/main.js';

export {
createUmzeptionContext,
} from './lib/context.js';

export {
umzeption,
} from './lib/main.js';

export {
BaseUmzeptionStorage,
} from './lib/storage.js';
35 changes: 33 additions & 2 deletions lib/advanced-types.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
// @ts-ignore Avoid strict dependency on 'pg'
import type { Pool as PgPool, PoolClient as PgPoolClient } from 'pg';
import type { PluginDefinition } from 'plugin-importer';
import type { MigrationParams, UmzugStorage } from 'umzug';

// What a dependency should provide
export interface UmzeptionDependency<T extends AnyUmzeptionContext = AnyUmzeptionContext> extends PluginDefinition {
Expand All @@ -24,10 +27,24 @@ export interface UmzeptionLookupOptions<T extends AnyUmzeptionContext> extends
noop?: boolean
}

// *** Storage ***

export abstract class UmzeptionStorage<T extends AnyUmzeptionContext> implements UmzugStorage<T> {
// From UmzugStorage
logMigration: (params: MigrationParams<T>) => Promise<void>;
unlogMigration: (params: MigrationParams<T>) => Promise<void>;
executed: (meta: Pick<MigrationParams<T>, 'context'>) => Promise<string[]>;

// Extensions
ensureTable (context: T): Promise<void>
query (context: T, query: string, ...values: string[]): Promise<{ rows: Array<{ [column: string]: unknown }> }>
}

// *** Context definitions ***

export interface DefineUmzeptionContexts {
unknown: UmzeptionContext<'unknown'>,
pg: UmzeptionContext<'pg', FastifyPostgresStyleDb>,
unknown: UmzeptionContext<'unknown', unknown>,
}

type ValidUmzeptionContexts = {
Expand All @@ -46,11 +63,25 @@ interface BaseUmzeptionContext {
value: unknown
}

export interface UmzeptionContext<T extends UmzeptionContextTypes, V = unknown> extends BaseUmzeptionContext {
export interface UmzeptionContext<T extends UmzeptionContextTypes, V> extends BaseUmzeptionContext {
type: string extends T ? never : (T extends string ? T : never);
value: V
}

// *** Postgres context **

// TODO: Extract pg context into a "umzeption-pg" module

type FastifyPostgresStyleTransactCallback = (client: PgPoolClient) => void;
type FastifyPostgresStyleTransact = (callback: FastifyPostgresStyleTransactCallback) => void;

export type FastifyPostgresStyleDb = {
pool: PgPool;
query: PgPool["query"];
connect: PgPool["connect"];
transact: FastifyPostgresStyleTransact;
};

// *** Helpers ***

type PartialKeys<T, Keys extends keyof T> = Omit<T, Keys> & Partial<Pick<T, Keys>>;
Expand Down
16 changes: 16 additions & 0 deletions lib/context-pg/context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { createUmzeptionContext } from '../context.js';
import { createFastifyPostgresStyleDb } from './utils.js';

// TODO: Extract pg context into a "umzeption-pg" module

/** @typedef {import('../advanced-types.js').FastifyPostgresStyleDb} FastifyPostgresStyleDb */

/**
* @param {FastifyPostgresStyleDb["pool"]} pool
* @returns {import('../advanced-types.js').UmzeptionContext<'pg', FastifyPostgresStyleDb>}
*/
export function createUmzeptionPgContext (pool) {
const db = createFastifyPostgresStyleDb(pool);

return createUmzeptionContext('pg', db);
}
2 changes: 2 additions & 0 deletions lib/context-pg/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { createUmzeptionPgContext } from './context.js';
export { UmzeptionPgStorage } from './storage.js';
17 changes: 17 additions & 0 deletions lib/context-pg/storage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { BaseUmzeptionStorage } from '../storage.js';

/** @typedef {import('../advanced-types.js').DefineUmzeptionContexts['pg']} UmzeptionPgContext */

/** @augments {BaseUmzeptionStorage<UmzeptionPgContext>} */
export class UmzeptionPgStorage extends BaseUmzeptionStorage {
/**
* @override
* @type {BaseUmzeptionStorage<UmzeptionPgContext>["query"]}
*/
async query (context, query, ...values) {
if (context.type === 'pg') {
return context.value.query(query, values);
}
throw new Error(`Unsupported context type: ${context.type}`);
}
}
32 changes: 32 additions & 0 deletions lib/context-pg/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/** @typedef {import('../advanced-types.js').FastifyPostgresStyleDb} FastifyPostgresStyleDb */

/**
* @this {FastifyPostgresStyleDb["pool"]}
* @param {Parameters<FastifyPostgresStyleDb["transact"]>[0]} fn
*/
async function transact (fn) {
const client = await this.connect();

try {
await client.query('BEGIN');
await fn(client);
await client.query('COMMIT');
} catch {
await client.query('ROLLBACK');
} finally {
client.release();
}
}

/**
* @param {FastifyPostgresStyleDb["pool"]} pool
* @returns {FastifyPostgresStyleDb}
*/
export function createFastifyPostgresStyleDb (pool) {
return {
connect: pool.connect.bind(pool),
pool,
query: pool.query.bind(pool),
transact: transact.bind(pool),
};
}
52 changes: 52 additions & 0 deletions lib/storage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/** @typedef {import('./advanced-types.js').AnyUmzeptionContext} AnyUmzeptionContext */

/**
* @template {AnyUmzeptionContext} T
* @typedef {import('./advanced-types.js').UmzeptionStorage<T>} UmzeptionStorage
*/

/**
* @template {AnyUmzeptionContext} T
* @augments {UmzeptionStorage<T>}
*/
export class BaseUmzeptionStorage {
#tableEnsured = false;

/** @type {UmzeptionStorage<T>["logMigration"]} */
async logMigration ({ context, name }) {
await this.ensureTable(context);
await this.query(context, 'INSERT INTO umzeption_migrations (name) VALUES ($1)', name);
}

/** @type {UmzeptionStorage<T>["unlogMigration"]} */
async unlogMigration ({ context, name }) {
await this.ensureTable(context);
await this.query(context, 'DELETE FROM umzeption_migrations WHERE name = $1', name);
}

/** @type {UmzeptionStorage<T>["executed"]} */
async executed ({ context }) {
await this.ensureTable(context);
const { rows } = await this.query(context, 'SELECT name FROM umzeption_migrations');
return rows.map(row => typeof row['name'] === 'string' ? row['name'] : '');
}

/** @type {UmzeptionStorage<T>["query"]} */
async query (context, _query, ..._values) {
throw new Error(`Unsupported context type: ${context.type}`);
}

/** @type {UmzeptionStorage<T>["ensureTable"]} */
async ensureTable (context) {
if (this.#tableEnsured) return;

this.#tableEnsured = true;

await this.query(context, `
CREATE TABLE IF NOT EXISTS umzeption_migrations (
name VARCHAR(255) PRIMARY KEY,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
)
`);
}
}
14 changes: 13 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,25 @@
"test": "run-s check test:*"
},
"peerDependencies": {
"@types/pg": "^8.10.2",
"pg": "^8.11.2",
"umzug": "^3.2.1"
},
"peerDependenciesMeta": {
"@types/pg": {
"optional": true
},
"pg": {
"optional": true
}
},
"dependencies": {
"globby": "^13.2.2",
"plugin-importer": "^0.1.1"
},
"devDependencies": {
"@types/node": "^18.17.1",
"@types/pg": "^8.10.2",
"@types/sinon": "^10.0.16",
"@voxpelli/eslint-config": "^19.0.0",
"@voxpelli/tsconfig": "^8.0.0",
Expand All @@ -75,8 +86,9 @@
"eslint-plugin-unicorn": "^48.0.1",
"husky": "^8.0.3",
"installed-check": "^8.0.0",
"knip": "^2.17.2",
"knip": "^2.19.0",
"npm-run-all2": "^6.0.6",
"pg": "^8.11.2",
"sinon": "^15.2.0",
"type-coverage": "^2.26.0",
"typescript": "~5.1.6",
Expand Down
104 changes: 104 additions & 0 deletions test/pg-integration.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { describe, it, afterEach } from 'node:test';
import assert from 'node:assert/strict';

import sinon from 'sinon';

import pg from 'pg';
import { Umzug } from 'umzug';

import { UmzeptionPgStorage, createUmzeptionPgContext, umzeption } from '../index.js';

import { down as downMain, up as upMain } from './fixtures/migrations/foo-01.js';
import { installSchema as installSchemaTestDependency } from './fixtures/test-dependency/index.js';
import { down as downTestDependency, up as upTestDependency } from './fixtures/test-dependency/migrations/foo-01.js';

function getDependencyStubCallCount ({
down = 0,
installSchema = 0,
up = 0,
} = {}) {
return {
downMain: downMain.callCount + down,
downTestDependency: downTestDependency.callCount + down,
installSchemaTestDependency: installSchemaTestDependency.callCount + installSchema,
upMain: upMain.callCount + up,
upTestDependency: upTestDependency.callCount + up,
};
}

describe('PG Integration', () => {
afterEach(() => {
sinon.restore();
});

it('should resolve dependencies and create proper migrations', async () => {
const queryStub = sinon.stub(pg.Pool.prototype, 'query').resolves({ rows: [] });

const context = createUmzeptionPgContext(new pg.Pool({
connectionString: 'postgresql://dbuser:secretpassword@localhost:3211/mydb',
}));

const storage = new UmzeptionPgStorage();

const expectedCallCount = getDependencyStubCallCount({ up: 1 });

const umzug = new Umzug({
migrations: umzeption({
dependencies: ['./fixtures/test-dependency'],
glob: ['fixtures/migrations/*.js'],
meta: import.meta,
}),
context,
storage,
logger: sinon.stub(console),
});

await umzug.up();

assert.deepEqual(
getDependencyStubCallCount(),
expectedCallCount,
'Unexpected call count of dependency stubs'
);

assert.deepStrictEqual(queryStub.args, [
[
'\n' +
' CREATE TABLE IF NOT EXISTS umzeption_migrations (\n' +
' name VARCHAR(255) PRIMARY KEY,\n' +
' created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP\n' +
' )\n' +
' ',
[],
],
[
'SELECT name FROM umzeption_migrations',
[],
],
[
'INSERT INTO umzeption_migrations (name) VALUES ($1)',
[
'test-dependency:install',
],
],
[
'INSERT INTO umzeption_migrations (name) VALUES ($1)',
[
'main:install',
],
],
[
'INSERT INTO umzeption_migrations (name) VALUES ($1)',
[
'test-dependency|foo-01.js',
],
],
[
'INSERT INTO umzeption_migrations (name) VALUES ($1)',
[
'main|foo-01.js',
],
],
]);
});
});

0 comments on commit 626c733

Please sign in to comment.