Skip to content

Commit

Permalink
feat(generator): generate latest migration via pgdump
Browse files Browse the repository at this point in the history
  • Loading branch information
ThatOneBro committed Apr 18, 2024
1 parent a0a8b93 commit 011fe6d
Show file tree
Hide file tree
Showing 8 changed files with 35,786 additions and 16,467 deletions.
640 changes: 640 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@cyclonedx/cyclonedx-npm": "1.16.2",
"@microsoft/api-documenter": "7.24.2",
"@microsoft/api-extractor": "7.43.1",
"@testcontainers/postgresql": "10.8.2",
"@types/jest": "29.5.12",
"@types/node": "20.12.7",
"babel-jest": "29.7.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/generator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"fhirtypes": "ts-node src/index.ts",
"generate": "npm run fhirtypes && npm run jsonschema && npm run baseschema && npm run mockclient",
"jsonschema": "ts-node src/jsonschema.ts && npx prettier ../definitions/dist/fhir/r4/fhir.schema.json --write",
"migrate": "ts-node src/migrate.ts && npx prettier ../server/src/migrations/**/*.ts --write",
"migrate": "ts-node src/migrate.ts && ts-node src/latestmigration.ts && npx prettier ../server/src/migrations/**/*.ts --write",
"mockclient": "ts-node src/mockclient.ts && npx prettier ../mock/src/mocks/*.json --write",
"test": "jest"
},
Expand Down
93 changes: 93 additions & 0 deletions packages/generator/src/latestmigration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { PostgreSqlContainer } from '@testcontainers/postgresql';
import { spawn } from 'node:child_process';
import { writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { Pool } from 'pg';
import { migrate } from '../../server/src/migrations/migrations';
import { FileBuilder } from './filebuilder';

const SCHEMA_DIR = resolve(__dirname, '../../server/src/migrations/schema');

process.on('SIGINT', () => {
console.log('Gracefully quitting process...');
// @ts-expect-error Not a public method
const activeHandles = process._getActiveHandles() as any[];
if (activeHandles.length) {
console.log('Active handles:', activeHandles);
}
process.exit(1);
});

async function main(): Promise<void> {
// Start clean database...
console.info('Starting Postgres container...');
const container = await new PostgreSqlContainer().start();
const pool = new Pool({ connectionString: container.getConnectionUri() });
const client = await pool.connect();
console.info('Connected to pool... Running migrations...');

// Run migrations
await migrate(client, true);

// Dump resulting schema
const { stdout } = spawn(
'pg_dump',
[
'-h',
container.getHost(),
'-p',
container.getPort().toString(),
'-U',
container.getUsername(),
'--schema-only',
'--no-owner',
container.getDatabase(),
],
{
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, PGPASSWORD: 'test' },
}
);

let dump = '';
stdout.setEncoding('utf-8');
stdout.on('data', (data) => {
dump += data;
});

await new Promise<void>((resolve) => {
stdout.on('close', () => {
resolve();
});
});

console.info('Database dump successful. Writing dump to file...');

const builder = new FileBuilder();
buildLatestMigration(builder, dump);
writeFileSync(`${SCHEMA_DIR}/latest.ts`, builder.toString(), 'utf8');

console.info('Migration file successfully created.');

client.release();
await pool.end();
await container.stop();
}

function buildLatestMigration(b: FileBuilder, sqlDump: string): void {
b.append("import { PoolClient } from 'pg';");
b.newLine();
b.append('export async function run(client: PoolClient): Promise<void> {');
b.indentCount++;

b.newLine();
b.appendNoWrap('await client.query(`');
b.indentCount--;

b.appendNoWrap(sqlDump);

b.append('`);');
b.append('}');
}

main().catch(console.error);
11 changes: 4 additions & 7 deletions packages/generator/src/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,11 @@ export async function main(): Promise<void> {

const startDefinition = await buildStartDefinition();
const targetDefinition = buildTargetDefinition();
const b1 = new FileBuilder();
const b2 = new FileBuilder();
writeMigrations(b1, startDefinition, targetDefinition);
writeMigrations(b2, { tables: [] }, targetDefinition);
if (b1.getLineCount() > 20) {
writeFileSync(`${SCHEMA_DIR}/v${getNextSchemaVersion()}.ts`, b1.toString(), 'utf8');
const b = new FileBuilder();
writeMigrations(b, startDefinition, targetDefinition);
if (b.getLineCount() > 20) {
writeFileSync(`${SCHEMA_DIR}/v${getNextSchemaVersion()}.ts`, b.toString(), 'utf8');
}
writeFileSync(`${SCHEMA_DIR}/latest.ts`, b2.toString(), 'utf8');
rewriteMigrationExports();
}

Expand Down
40 changes: 1 addition & 39 deletions packages/server/src/database.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Pool, PoolClient } from 'pg';
import { MedplumServerConfig } from './config';
import { globalLogger } from './logger';
import * as migrations from './migrations/schema';
import { migrate } from './migrations/migrations';

let pool: Pool | undefined;

Expand Down Expand Up @@ -67,41 +67,3 @@ export async function closeDatabase(): Promise<void> {
pool = undefined;
}
}

async function migrate(client: PoolClient, forceAllSteps = false): Promise<void> {
await client.query(`CREATE TABLE IF NOT EXISTS "DatabaseMigration" (
"id" INTEGER NOT NULL PRIMARY KEY,
"version" INTEGER NOT NULL,
"dataVersion" INTEGER NOT NULL
)`);

const result = await client.query('SELECT "version" FROM "DatabaseMigration"');
const version = result.rows[0]?.version ?? 0; // First version starts at v1

const migrationKeys = Object.keys(migrations).filter((key) => key.startsWith('v'));
const migrationVersions = migrationKeys.map((key) => Number.parseInt(key.slice(1), 10)).sort((a, b) => a - b);
if (version === 0) {
await client.query('INSERT INTO "DatabaseMigration" ("id", "version", "dataVersion") VALUES (1, 0, 0)');
}
if (version === 0 && !forceAllSteps) {
const start = Date.now();
await migrations.latest.run(client);
const latestVersion = migrationVersions[migrationVersions.length - 1];
globalLogger.info('Database schema migration', {
version: `v${latestVersion}`,
duration: `${Date.now() - start} ms`,
});
await client.query('UPDATE "DatabaseMigration" SET "version"=$1', [latestVersion]);
} else {
globalLogger.info(`migrationKeys length: ${migrationKeys.length}`);
for (let i = version + 1; i <= migrationKeys.length; i++) {
const migration = (migrations as Record<string, migrations.Migration>)[`v${i}`];
if (migration) {
const start = Date.now();
await migration.run(client);
globalLogger.info('Database schema migration', { version: `v${i}`, duration: `${Date.now() - start} ms` });
await client.query('UPDATE "DatabaseMigration" SET "version"=$1', [i]);
}
}
}
}
61 changes: 61 additions & 0 deletions packages/server/src/migrations/migrations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { normalizeErrorString, sleep } from '@medplum/core';
import { PoolClient } from 'pg';
import { globalLogger } from '../logger';
import * as migrations from './schema/index';

export async function migrate(client: PoolClient, forceAllSteps = false): Promise<void> {
let version: number;
try {
const result = await client.query<{ id: number; version: number; dataVersion: number }>(
'SELECT "version" FROM "DatabaseMigration"'
);
version = result.rows[0]?.version ?? 0; // First version starts at v1
globalLogger.info(`Current schema version: v${version}`);
} catch (err) {
globalLogger.debug(`Error during version query: ${normalizeErrorString(err)}`);
globalLogger.info('Database not initialized. Initializing...');
version = 0;
}

const migrationKeys = Object.keys(migrations).filter((key) => key.startsWith('v'));
const migrationVersions = migrationKeys.map((key) => Number.parseInt(key.slice(1), 10)).sort((a, b) => a - b);

if (version === 0 && !forceAllSteps) {
const start = Date.now();
await migrations.latest.run(client);
const latestVersion = migrationVersions[migrationVersions.length - 1];
globalLogger.info('Database schema migration', {
version: `v${latestVersion}`,
duration: `${Date.now() - start} ms`,
});
for (let i = 0; i < 5; i++) {
await sleep(1000);
try {
await client.query('INSERT INTO "DatabaseMigration" ("id", "version", "dataVersion") VALUES (1, $1, 0)', [
latestVersion,
]);
break;
} catch (_err) {
console.log(`Retrying... Attempt #${i + 1}`);
}
}
} else {
if (version === 0) {
await client.query(`CREATE TABLE IF NOT EXISTS "DatabaseMigration" (
"id" INTEGER NOT NULL PRIMARY KEY,
"version" INTEGER NOT NULL,
"dataVersion" INTEGER NOT NULL
)`);
await client.query('INSERT INTO "DatabaseMigration" ("id", "version", "dataVersion") VALUES (1, 0, 0)');
}
for (let i = version + 1; i <= migrationKeys.length; i++) {
const migration = (migrations as Record<string, migrations.Migration>)[`v${i}`];
if (migration) {
const start = Date.now();
await migration.run(client);
globalLogger.info('Database schema migration', { version: `v${i}`, duration: `${Date.now() - start} ms` });
await client.query('UPDATE "DatabaseMigration" SET "version"=$1', [i]);
}
}
}
}

0 comments on commit 011fe6d

Please sign in to comment.