UNPKG

@paratco/goose-js

Version:

JavaScript implementation of goose database migration tool

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