@paratco/goose-js
Version:
JavaScript implementation of goose database migration tool
50 lines (45 loc) • 11.3 kB
JavaScript
import{Command as e}from"@commander-js/extra-typings";import t from"node:fs/promises";import n from"node:path";import r from"knex";async function i(e,r,i){await t.mkdir(i,{recursive:!0});let a=new Date().toISOString().replaceAll(/[-:]/g,``).replace(`T`,``).split(`.`)[0],o=e.toLowerCase().replaceAll(/\s+/g,`_`),s=`${a}_${o}.${r}`,c=n.join(i,s),l=`/**
* Migration: ${e}
* Created at: ${new Date().toISOString()}
*/
/**
* Up migration
* @param {import('knex').Knex} db - The database connection
*/
export async function up(db) {
// Write your migration code here
// Example:
// await db.schema.createTable('users', (table) => {
// table.increments('id').primary();
// table.string('name').notNullable();
// table.string('email').notNullable().unique();
// table.timestamps(true, true);
// });
}
/**
* Down migration
* optional
* @param {import('knex').Knex} db - The database connection
*/
export async function down(db) {
// Write your rollback code here
// Example:
// await db.schema.dropTable('users');
}
// optionally export a flag to indicate that this migration does not require a transaction (default is false)
//export const noTransaction = true;
// optionally export a flag to indicate that this migration is irreversible (default is false)
//export const irreversible = true;
`,u=`-- +goose Up
-- +goose StatementBegin
SELECT 'up SQL query';
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
SELECT 'down SQL query';
-- +goose StatementEnd
`;return await t.writeFile(c,r===`sql`?u:l,`utf8`),s}const a=[`sqlite3`,`better-sqlite3`],o=[`pg`,`pg-native`,`mysql`,`mysql2`,`oracledb`,`tedious`],s=new Set([...a,...o]);function c(e,t){if(e===void 0||e.trim()===``)throw Error(`db string is required`);e=e.trim();let n=/^([a-z0-9-]+):\/\//i.exec(e);if(n===null){if(t===void 0)throw Error(`Driver is required when db string does not specify one`);if(!s.has(t))throw Error(`Unsupported driver: ${t}`);e=`${t}://${e}`}let r;try{r=new URL(e.trim())}catch{throw Error(`Invalid db string format: ${e}`)}if(t=r.protocol.slice(0,-1),!s.has(t))throw Error(`Unsupported driver: ${t}`);return a.includes(t)?{driver:t,filename:decodeURI(`${r.host}${r.pathname}`),params:Object.fromEntries(r.searchParams)}:{driver:t,user:r.username===``?void 0:decodeURI(r.username),password:r.password===``?void 0:decodeURI(r.password),host:decodeURI(r.hostname),port:r.port===``?void 0:Number.parseInt(r.port),database:decodeURI(r.pathname).slice(1),params:Object.fromEntries(r.searchParams)}}let l=null;async function u(e){if(l===null){let{driver:t,params:n,...i}=c(e.dbString,e.driver);e.verbose&&console.log(`Connecting to database with ${t}`),l=r({client:t,connection:{...i,...n,supportBigNumbers:!0,bigNumberStrings:!0}})}let t=await l.schema.hasTable(e.table);if(!t){e.verbose&&console.log(`Creating migrations table: ${e.table}`),await l.schema.createTable(e.table,e=>{e.bigIncrements(`id`).notNullable().primary(),e.bigint(`version_id`).notNullable().unique(),e.tinyint(`is_applied`,1).notNullable(),e.timestamp(`tstamp`).defaultTo(l.fn.now())});let t=await l.table(e.table).select(`id`).where(`version_id`,0).first();t===void 0&&(e.verbose&&console.log(`Adding version 0 as initial state`),await l(e.table).insert({version_id:0,is_applied:1}))}return l}function d(){if(l===null)throw Error(`Database not initialized. Call initializeDb first.`);return l}function f(e){let t=/^(\d+)_/.exec(e);if(t===null)throw Error(`Invalid migration filename: ${e}`);return t[1]}function p(e){return String(e).padStart(2,`0`)}function m(e){let t=e.getFullYear(),n=p(e.getMonth()+1),r=p(e.getDate()),i=p(e.getHours()),a=p(e.getMinutes()),o=p(e.getSeconds());return`${t}/${n}/${r} ${i}:${a}:${o}`}function h(e){let t=e.split(/\r?\n/),n=`none`,r=!1,i=``,a=[],o=[];for(let e of t){let t=e.trim();if(t===`-- +goose Up`){n=`up`;continue}else if(t===`-- +goose Down`){n=`down`;continue}if(!r&&(t===``||t.startsWith(`--`)))continue;if(t===`-- +goose StatementBegin`){r=!0,i=``;continue}else if(t===`-- +goose StatementEnd`){r=!1,n===`up`?a.push(i):n===`down`&&o.push(i);continue}r?i+=e+`
`:n!==`none`&&t!==``&&!t.startsWith(`--`)&&(i+=e+`
`,t.endsWith(`;`)&&(n===`up`?a.push(i):o.push(i),i=``))}return{up:async e=>{for(let t of a)await e.raw(t)},down:o.length>0?async e=>{for(let t of o)await e.raw(t)}:void 0,noTransaction:e.includes(`-- +goose NO TRANSACTION`),irreversible:e.includes(`-- +goose IRREVERSIBLE`)}}async function g(e){let r=await t.readdir(e),i=r.filter(e=>(e.endsWith(`.js`)||e.endsWith(`.sql`))&&/^\d+_/.test(e)).sort((e,t)=>{let n=f(e),r=f(t);return Number(BigInt(n)-BigInt(r))}),a=[];for(let r of i){let i=f(r),o=n.join(e,r),s=r.endsWith(`.sql`);try{let e;if(s){let n=await t.readFile(o,`utf8`);e=h(n)}else{let t=await import(`file:${n.resolve(o)}`);if(typeof t.up!=`function`)throw TypeError(`Migration ${r} must export up function`);if(t.down!==void 0&&typeof t.down!=`function`)throw TypeError(`Migration ${r} down export must be a function`);if(t.noTransaction!==void 0&&typeof t.noTransaction!=`boolean`)throw TypeError(`Migration ${r} noTransaction export must be a boolean`);if(t.irreversible!==void 0&&typeof t.irreversible!=`boolean`)throw TypeError(`Migration ${r} irreversible export must be a boolean`);e={up:t.up,down:t.down,noTransaction:!!t.noTransaction,irreversible:!!t.irreversible}}a.push({version:i,filename:r,filepath:o,module:e})}catch(e){throw console.error(`Error loading migration ${r}: ${e.message}`),e}}return a}async function _(e){let t=d();return t(e).select(`*`).orderBy(`id`,`asc`)}async function v(e,t){let n=d();console.log(`Applying ${e.length} migrations`);for(let r of e){t.verbose&&console.log(`Applying migration: ${r.filename}`);try{r.module.noTransaction?(t.verbose&&console.log(`Running migration without transaction: ${r.filename}`),await r.module.up(n),await n(t.table).insert({version_id:r.version,is_applied:1})):await n.transaction(async e=>{await r.module.up(e),await e(t.table).insert({version_id:r.version,is_applied:1})}),console.log(m(new Date),`Applied migration:`,r.filename)}catch(e){throw console.error(m(new Date),`Error applying migration ${r.filename}: ${e.message}`),e}}}async function y(e,t){let n=await g(e.dir),r=await _(e.table),i=n.filter(e=>!r.some(t=>t.version_id===e.version));if(t!==void 0&&i.length>0){let e=BigInt(t);i=i.filter(t=>BigInt(t.version)<=e)}if(i.length===0){console.log(`No pending migrations`);return}await v(i,e)}async function b(e){let t=await g(e.dir),n=await _(e.table),r=t.filter(e=>!n.some(t=>t.version_id===e.version));if(r.length===0){console.log(`No pending migrations`);return}await v([r[0]],e)}async function x(e,t){let n=d(),r=await g(e.dir),i=await _(e.table);if(i.length===0){console.log(`No migrations to roll back`);return}let a=[...i].sort((e,t)=>Number(BigInt(t.version_id)-BigInt(e.version_id))),o=[];if(t===void 0?o.push(a[0]):o=a.filter(e=>BigInt(e.version_id)>BigInt(t)),o.length===0){console.log(`No migrations to roll back`);return}console.log(`Rolling back ${o.length} migrations to version ${o[o.length-1].version_id}`);for(let t of o){let i=r.find(e=>e.version===t.version_id);if(i===void 0){console.error(`Migration file not found for version ${t.version_id}`);continue}e.verbose&&console.log(`Rolling back migration: ${i.filename}`);try{if(i.module.irreversible){console.log(`irreversible migration: ${i.filename} (finishing rollback)`);break}i.module.noTransaction?(e.verbose&&console.log(`Rolling back migration without transaction: ${i.filename}`),i.module.down!==void 0&&await i.module.down(n),await n(e.table).where(`version_id`,i.version).delete()):await n.transaction(async t=>{i.module.down!==void 0&&await i.module.down(t),await t(e.table).where(`version_id`,i.version).delete()}),console.log(`Rolled back migration: ${i.filename}`)}catch(e){throw console.error(`Error rolling back migration ${i.filename}: ${e.message}`),e}}}async function S(e){let t=await g(e.dir),n=await _(e.table);if(console.log(`Migration Status:`),console.log(`=================`),t.length===0){console.log(`No migration files found`);return}for(let e of t){let t=n.find(t=>t.version_id===e.version),r=t===void 0?`Pending`:`Applied`,i=t===void 0?``:new Date(t.tstamp).toISOString();console.log(`[${r}] ${e.filename}${i===``?``:` (${i})`}`)}}const C=new e(`goose-js`).description(`JavaScript implementation of goose database migration tool`).version(process.env.GOOSE_JS_VERSION??`DEV`).option(`--dir <string>`,`directory with migration files, (GOOSE_MIGRATION_DIR env variable supported)`,process.env.GOOSE_MIGRATION_DIR??`./`).option(`--table <string>`,`migrations table name`,process.env.GOOSE_TABLE??`goose_db_version`).option(`-v, --verbose`,`enable verbose mode`,!1);C.command(`create <name> [sql|js]`).description(`Creates new migration file with the current timestamp`).action(async(e,t)=>{t!==`js`&&(t=`sql`);try{let n=await i(e,t,C.opts().dir);console.log(m(new Date),`Created new file:`,n),process.exit(0)}catch(e){console.error(`Error creating migration: ${e.message}`),process.exit(1)}}),C.command(`up`).description(`Migrate the DB to the most recent version available`).action(async()=>{let e={...C.opts(),driver:process.env.GOOSE_DRIVER,dbString:process.env.GOOSE_DBSTRING};try{await u(e),await y(e),process.exit(0)}catch(e){console.error(`Error running migrations: ${e.message}`),process.exit(1)}}),C.command(`down`).description(`Roll back the version by 1`).action(async()=>{let e={...C.opts(),driver:process.env.GOOSE_DRIVER,dbString:process.env.GOOSE_DBSTRING};try{await u(e),await x(e),process.exit(0)}catch(e){console.error(`Error rolling back migrations: ${e.message}`),process.exit(1)}}),C.command(`status`).description(`Dump the migration status for the current DB`).action(async()=>{let e={...C.opts(),driver:process.env.GOOSE_DRIVER,dbString:process.env.GOOSE_DBSTRING};try{await u(e),await S(e),process.exit(0)}catch(e){console.error(`Error checking migration status: ${e.message}`),process.exit(1)}}),C.command(`up-to <version>`).description(`Migrate the DB to a specific version`).action(async e=>{let t={...C.opts(),driver:process.env.GOOSE_DRIVER,dbString:process.env.GOOSE_DBSTRING};try{await u(t),await y(t,e),process.exit(0)}catch(t){console.error(`Error migrating up to version ${e}: ${t.message}`),process.exit(1)}}),C.command(`up-by-one`).description(`Migrate the DB up by 1`).action(async()=>{let e={...C.opts(),driver:process.env.GOOSE_DRIVER,dbString:process.env.GOOSE_DBSTRING};try{await u(e),await b(e),process.exit(0)}catch(e){console.error(`Error migrating up by one: ${e.message}`),process.exit(1)}}),C.command(`down-to <version>`).description(`Roll back to a specific version`).action(async e=>{let t={...C.opts(),driver:process.env.GOOSE_DRIVER,dbString:process.env.GOOSE_DBSTRING};try{await u(t),await x(t,e),process.exit(0)}catch(t){console.error(`Error rolling back to version ${e}: ${t.message}`),process.exit(1)}}),C.command(`reset`).description(`Roll back all migrations`).action(async()=>{let e={...C.opts(),driver:process.env.GOOSE_DRIVER,dbString:process.env.GOOSE_DBSTRING};try{await u(e),await x(e,`0`),process.exit(0)}catch(e){console.error(`Error rolling back all migration : ${e.message}`),process.exit(1)}}),C.parse();
//# sourceMappingURL=cli.js.map