From ddccb560d1058550240d8dfe4a2a06b30e8786a8 Mon Sep 17 00:00:00 2001 From: uasan Date: Sun, 10 Sep 2023 20:14:13 +0200 Subject: [PATCH] WIP --- bin/dev.mjs | 2 +- bin/migrate.mjs | 2 +- package.json | 1 + src/compiler/worker/hooks.js | 9 +-- src/runtime/migration/actions/help.js | 6 ++ src/runtime/migration/actions/status.js | 9 +++ src/runtime/migration/actions/up.js | 56 ++++++++++++++++ src/runtime/migration/app.js | 28 +++++++- src/runtime/migration/constants.js | 5 ++ src/runtime/migration/context.js | 10 ++- .../{database.js => internals/connect.js} | 17 ++--- src/runtime/migration/internals/report.js | 28 ++++++++ src/runtime/migration/internals/state.js | 67 +++++++++++++++++++ src/runtime/postgres/context.js | 4 ++ src/runtime/postgres/transaction.js | 22 ++++++ src/runtime/server/app.js | 2 +- .../{console/colors.js => utils/console.js} | 2 + 17 files changed, 249 insertions(+), 21 deletions(-) create mode 100644 src/runtime/migration/actions/help.js create mode 100644 src/runtime/migration/actions/status.js create mode 100644 src/runtime/migration/actions/up.js create mode 100644 src/runtime/migration/constants.js rename src/runtime/migration/{database.js => internals/connect.js} (67%) create mode 100644 src/runtime/migration/internals/report.js create mode 100644 src/runtime/migration/internals/state.js create mode 100644 src/runtime/postgres/transaction.js rename src/runtime/{console/colors.js => utils/console.js} (97%) diff --git a/bin/dev.mjs b/bin/dev.mjs index 872aa5e..3137982 100755 --- a/bin/dev.mjs +++ b/bin/dev.mjs @@ -1,4 +1,4 @@ -#!/usr/bin/env node --watch --experimental-import-meta-resolve +#!/usr/bin/env node --watch import { createWatchHost } from '../src/compiler/worker/watch.js'; diff --git a/bin/migrate.mjs b/bin/migrate.mjs index daaf20c..03b3a52 100755 --- a/bin/migrate.mjs +++ b/bin/migrate.mjs @@ -1,4 +1,4 @@ -#!/usr/bin/env node --experimental-import-meta-resolve +#!/usr/bin/env node import { createBuilderHost } from '../src/compiler/worker/watch.js'; diff --git a/package.json b/package.json index 7bbd112..91078e9 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ }, "imports": { "#config": "./src/config.js", + "#utils/*": "./src/runtime/utils/*", "#runtime/*": "./src/runtime/*", "#compiler/*": "./src/compiler/*" }, diff --git a/src/compiler/worker/hooks.js b/src/compiler/worker/hooks.js index 951f5bf..c7bda25 100644 --- a/src/compiler/worker/hooks.js +++ b/src/compiler/worker/hooks.js @@ -10,6 +10,7 @@ export class BuilderHooks { worker = { filename: '', instance: null, + options: { argv: process.argv.slice(2) }, close: () => { if (this.worker.instance) { this.worker.instance.once('exit', process.exit.bind(process)); @@ -60,10 +61,10 @@ export class BuilderHooks { afterEmit() { if (this.worker.filename) { this.worker.instance?.terminate(); - this.worker.instance = new Worker(this.worker.filename).on( - 'error', - console.error - ); + this.worker.instance = new Worker( + this.worker.filename, + this.worker.options + ).on('error', console.error); } } diff --git a/src/runtime/migration/actions/help.js b/src/runtime/migration/actions/help.js new file mode 100644 index 0000000..5f0fadb --- /dev/null +++ b/src/runtime/migration/actions/help.js @@ -0,0 +1,6 @@ +export function help() { + console.log(`These are common Migrate commands: + npm run migrate up Run up method for all new and updated migrations + npm run migrate down [filename] Run down method of [filename] + npm run migrate status Check status of migrations table`); +} diff --git a/src/runtime/migration/actions/status.js b/src/runtime/migration/actions/status.js new file mode 100644 index 0000000..e79b244 --- /dev/null +++ b/src/runtime/migration/actions/status.js @@ -0,0 +1,9 @@ +import { reportStatusMigrate } from '../internals/report.js'; + +export function status(ctx, migrations) { + for (let index = 0; index < migrations.length; ) { + const migration = migrations[index]; + + reportStatusMigrate(migration, ++index); + } +} diff --git a/src/runtime/migration/actions/up.js b/src/runtime/migration/actions/up.js new file mode 100644 index 0000000..99827fb --- /dev/null +++ b/src/runtime/migration/actions/up.js @@ -0,0 +1,56 @@ +import { lockMigrate, unlockMigrate } from '../internals/connect.js'; +import { reportUpMigrate } from '../internals/report.js'; +import { saveMigrations } from '../internals/state.js'; +import { STATUS_DONE, STATUS_NEW, STATUS_UPDATED } from '../constants.js'; + +async function upMigration(ctx, migrations) { + await lockMigrate(ctx); + + for (let index = 0; index < migrations.length; ) { + const migration = migrations[index]; + + reportUpMigrate(migration, ++index); + await migration.up(); + } + + await saveMigrations(ctx, migrations); + await unlockMigrate(ctx); +} + +export async function up(ctx, migrations) { + const upMigrations = []; + const wasDoneMigrations = []; + const afterCommitMigrations = []; + + for (const migration of migrations) + switch (migration.status) { + case STATUS_NEW: + case STATUS_UPDATED: { + upMigrations.push(migration); + + if (Object.hasOwn(migration, 'onAfterCommit')) + afterCommitMigrations.push(migration); + + break; + } + + case STATUS_DONE: { + if (Object.hasOwn(migration, 'onWasDone')) + wasDoneMigrations.push(migration); + + break; + } + } + + if (upMigrations.length) { + await ctx.startTransaction(upMigration, upMigrations); + } + + for (const migration of afterCommitMigrations) { + await migration.onAfterCommit(); + } + + for (const migration of wasDoneMigrations) { + await migration.onWasDone(); + } +} diff --git a/src/runtime/migration/app.js b/src/runtime/migration/app.js index 342b962..e067604 100644 --- a/src/runtime/migration/app.js +++ b/src/runtime/migration/app.js @@ -1,7 +1,29 @@ -import { connect, disconnect } from './database.js'; +import { argv } from 'node:process'; + +import { setMigrations } from './internals/state.js'; +import { connect, disconnect } from './internals/connect.js'; + +import { up } from './actions/up.js'; +import { help } from './actions/help.js'; +import { status } from './actions/status.js'; + +const actions = { + up, + status, +}; export async function migrate(context, migrations) { - const ctx = await connect(context); + const action = actions[argv[2] ?? 'up']; + + if (action) { + const ctx = await connect(context); - await disconnect(ctx); + try { + await action(ctx, await setMigrations(ctx, migrations), ...argv.slice(3)); + } finally { + await disconnect(ctx); + } + } else { + help(); + } } diff --git a/src/runtime/migration/constants.js b/src/runtime/migration/constants.js new file mode 100644 index 0000000..52b7a2a --- /dev/null +++ b/src/runtime/migration/constants.js @@ -0,0 +1,5 @@ +export const LOCK_ID = 0; + +export const STATUS_NEW = 'new'; +export const STATUS_UPDATED = 'updated'; +export const STATUS_DONE = 'done'; diff --git a/src/runtime/migration/context.js b/src/runtime/migration/context.js index 8094725..24762dc 100644 --- a/src/runtime/migration/context.js +++ b/src/runtime/migration/context.js @@ -1,3 +1,11 @@ import { Context } from '../context.js'; +import { STATUS_NEW } from './constants.js'; -export class MigrationContext extends Context {} +export class MigrationContext extends Context { + static hash = null; + static version = null; + static wasDone = false; + + status = STATUS_NEW; + isBootStrap = true; +} diff --git a/src/runtime/migration/database.js b/src/runtime/migration/internals/connect.js similarity index 67% rename from src/runtime/migration/database.js rename to src/runtime/migration/internals/connect.js index 6d709ed..72f8cbf 100644 --- a/src/runtime/migration/database.js +++ b/src/runtime/migration/internals/connect.js @@ -1,21 +1,21 @@ -import { green, yellow, red } from '../console/colors.js'; +import { yellow, red } from '#utils/console.js'; +import { ERRORS } from '@uah/postgres/src/constants.js'; +import { LOCK_ID } from '../constants.js'; -export const LOCK_ID = 0; export const DEFAULT_DATABASE = 'postgres'; -export const ERROR_DATABASE_NOT_EXIST = '3D000'; export async function createDatabase(ctx) { const { options } = ctx.postgres; - await ctx.postgres.setOptions({ ...options, database: DEFAULT_DATABASE }); + await ctx.postgres.reset({ ...options, database: DEFAULT_DATABASE }); await ctx.postgres.query(`CREATE DATABASE "${options.database}"`); - await ctx.postgres.setOptions(options); + await ctx.postgres.reset(options); } export async function dropDatabase(ctx) { const { options } = ctx.postgres; - await ctx.postgres.setOptions({ ...options, database: DEFAULT_DATABASE }); + await ctx.postgres.reset({ ...options, database: DEFAULT_DATABASE }); await ctx.postgres.query(`DROP DATABASE IF EXISTS "${options.database}"`); } @@ -28,8 +28,6 @@ export async function lockMigrate(ctx) { await ctx.sql`SELECT pg_advisory_lock(${LOCK_ID}::bigint)`; } - - console.log(green(`Migrate: start lock ${LOCK_ID}`)); } export async function unlockMigrate(ctx) { @@ -42,11 +40,10 @@ export async function connect(context) { try { await ctx.postgres.connect(); } catch (error) { - if (error.code === ERROR_DATABASE_NOT_EXIST) await createDatabase(ctx); + if (error.code === ERRORS.DATABASE_NOT_EXIST) await createDatabase(ctx); else throw error; } - await lockMigrate(ctx); return ctx; } diff --git a/src/runtime/migration/internals/report.js b/src/runtime/migration/internals/report.js new file mode 100644 index 0000000..2d9c9f5 --- /dev/null +++ b/src/runtime/migration/internals/report.js @@ -0,0 +1,28 @@ +import { green, blue, red } from '#utils/console.js'; + +function report(migration, command, index) { + console.log( + green('Migrate: ') + + index + + ' ' + + command + + ' ' + + migration.constructor.path + ); +} + +export function reportUpMigrate(migration, index) { + report(migration, green('up'), index); +} + +export function reportDownMigrate(migration, index) { + report(migration, red('down'), index); +} + +export function reportStatusMigrate(migration, index) { + const status = migration.status.toUpperCase(); + + console.log( + index + ' ' + blue(status) + ' ' + green(migration.constructor.path) + ); +} diff --git a/src/runtime/migration/internals/state.js b/src/runtime/migration/internals/state.js new file mode 100644 index 0000000..4c24e3e --- /dev/null +++ b/src/runtime/migration/internals/state.js @@ -0,0 +1,67 @@ +import { ERRORS } from '@uah/postgres/src/constants.js'; +import { STATUS_DONE, STATUS_NEW, STATUS_UPDATED } from '../constants.js'; + +async function createTableMigrations(ctx) { + await ctx.sql` + CREATE TABLE public.migrations ( + name text COLLATE "C" PRIMARY KEY, + hash bytea, + updated_at timestamptz not null default CURRENT_TIMESTAMP, + created_at timestamptz not null default CURRENT_TIMESTAMP + )`; +} + +async function getStateMigrations(ctx, migrations) { + const names = migrations.map(({ path }) => path); + const hashes = migrations.map(({ hash }) => hash); + + const query = ctx.sql` + SELECT json_object_agg(files.name, CASE + WHEN migrations.name IS NULL THEN ${STATUS_NEW} + WHEN migrations.hash IS DISTINCT FROM files.hash THEN ${STATUS_UPDATED} + ELSE ${STATUS_DONE} + END) + FROM unnest(${names}::text[], ${hashes}::bytea[]) AS files(name, hash) + LEFT JOIN public.migrations USING(name)`.asValue(); + + try { + return await query; + } catch (error) { + if (error.code === ERRORS.RELATION_NOT_EXIST) { + await createTableMigrations(ctx); + return await query; + } + throw error; + } +} + +export async function setMigrations(ctx, migrations) { + const state = await getStateMigrations(ctx, migrations); + + ctx.isBootStrap = Object.values(state).every(status => status === STATUS_NEW); + + for (let i = 0; i < migrations.length; i++) { + const migration = migrations[i]; + const status = state[migration.path]; + + migration.wasDone = status === STATUS_DONE; + + migrations[i] = new migration(); + migrations[i].status = status; + } + + return migrations; +} + +export async function saveMigrations(ctx, migrations) { + const names = migrations.map(m => m.constructor.path); + const hashes = migrations.map(m => m.constructor.hash); + + await ctx.sql` + INSERT INTO public.migrations (name, hash) + SELECT name, hash + FROM unnest(${names}::text[], ${hashes}::bytea[]) AS files(name, hash) + ON CONFLICT (name) DO UPDATE SET + hash = EXCLUDED.hash, + updated_at = CURRENT_TIMESTAMP`; +} diff --git a/src/runtime/postgres/context.js b/src/runtime/postgres/context.js index 0fdff7c..8c73b69 100644 --- a/src/runtime/postgres/context.js +++ b/src/runtime/postgres/context.js @@ -1,8 +1,12 @@ import { Pool } from '@uah/postgres/src/pool.js'; import { Client } from '@uah/postgres/src/client.js'; + import { signal } from '../process.js'; +import { startTransaction } from './transaction.js'; export function initPostgres({ prototype }, options) { + prototype.startTransaction = startTransaction; + prototype.postgres = options.maxConnections > 1 ? new Pool({ signal, ...options }) diff --git a/src/runtime/postgres/transaction.js b/src/runtime/postgres/transaction.js new file mode 100644 index 0000000..462c4e5 --- /dev/null +++ b/src/runtime/postgres/transaction.js @@ -0,0 +1,22 @@ +import { + TRANSACTION_ACTIVE, + TRANSACTION_INACTIVE, +} from '@uah/postgres/src/constants.js'; + +export async function startTransaction(action, payload) { + try { + await this.postgres.query('BEGIN'); + const result = await action(this, payload); + + if (this.postgres.state === TRANSACTION_ACTIVE) { + await this.postgres.query('COMMIT'); + } + + return result; + } catch (error) { + if (this.postgres.state !== TRANSACTION_INACTIVE) { + await this.postgres.query('ROLLBACK'); + } + throw error; + } +} diff --git a/src/runtime/server/app.js b/src/runtime/server/app.js index 48eee9c..591306b 100644 --- a/src/runtime/server/app.js +++ b/src/runtime/server/app.js @@ -6,7 +6,7 @@ import { import { signal } from '../process.js'; import { Router } from './router.js'; -import { blue, green, red } from '../console/colors.js'; +import { blue, green, red } from '#utils/console.js'; export const Server = { url: '', diff --git a/src/runtime/console/colors.js b/src/runtime/utils/console.js similarity index 97% rename from src/runtime/console/colors.js rename to src/runtime/utils/console.js index 827eb7f..235ca85 100644 --- a/src/runtime/console/colors.js +++ b/src/runtime/utils/console.js @@ -1,3 +1,5 @@ +import process from 'node:process'; + let FORCE_COLOR, NODE_DISABLE_COLORS, NO_COLOR,