UNPKG

yq-store

Version:

A high-performance, persistent and in-memory key-value store with TTL support, built for Node.js applications.

84 lines (68 loc) 10.1 kB
#!/usr/bin/env node #!/usr/bin/env node import N from"node:path";import b from"node:fs";var S="_default";var T=["namespace","key","value","created_at","expires_at","is_deleted","last_accessed"];function v(){return typeof Bun<"u"&&!!Bun?.version}async function k(e){if(v()){let{Database:t}=await import("bun:sqlite");return new t(e)}else{let{DatabaseSync:t}=await import("node:sqlite");return new t(e)}}function p(e,t){e.exec(t)}function h(e,t){return v()?e.query(t).get():e.prepare(t).get()}function R(e,t){return v()?e.query(t).all():e.prepare(t).all()}function C(e){e.close()}async function y(e){if(!b.existsSync(e))return{needsMigration:!1,validation:{isValid:!0,expectedVersion:1,missingColumns:[],extraColumns:[]},dbPath:e,exists:!1};let s=await k(e);try{if(!h(s,"SELECT name FROM sqlite_master WHERE type='table' AND name='kv_store'"))return{needsMigration:!1,validation:{isValid:!0,expectedVersion:1,missingColumns:[],extraColumns:[]},dbPath:e,exists:!0};let a=R(s,"PRAGMA table_info(kv_store)").map(d=>d.name),o;try{if(h(s,"SELECT name FROM sqlite_master WHERE type='table' AND name='yq_meta'")){let f=h(s,"SELECT value FROM yq_meta WHERE key = 'schema_version'");f&&(o=parseInt(f.value,10))}}catch{}let l=[...T],i=l.filter(d=>!a.includes(d)),g=a.filter(d=>!l.includes(d)),m=i.length===0&&(o===void 0||o===1);return{needsMigration:!m,validation:{isValid:m,currentVersion:o,expectedVersion:1,missingColumns:i,extraColumns:g,message:m?void 0:"Schema mismatch detected"},dbPath:e,exists:!0}}finally{C(s)}}async function D(e){let{dbPath:t,backup:s=!0,dryRun:n=!1,verbose:r=!1}=e,a=[],o=u=>{r&&console.log(`[migrate] ${u}`),a.push(u)};if(!b.existsSync(t))return{success:!1,fromVersion:null,toVersion:1,changes:["Database file does not exist"],error:`Database file not found: ${t}`};let l=await y(t);if(!l.needsMigration)return o("Schema is already up to date"),{success:!0,fromVersion:l.validation.currentVersion??1,toVersion:1,changes:a};let i=l.validation.currentVersion??0,g;if(s&&!n&&(g=`${t}.backup.${Date.now()}`,b.copyFileSync(t,g),o(`Created backup at: ${g}`)),n)return o("DRY RUN - No changes will be made"),o(`Would migrate from v${i} to v${1}`),l.validation.missingColumns.length>0&&o(`Would add columns: ${l.validation.missingColumns.join(", ")}`),{success:!0,fromVersion:i,toVersion:1,changes:a};let m=await k(t);try{p(m,"BEGIN TRANSACTION");for(let u of l.validation.missingColumns){let d=A(u),f=M(u);o(`Adding column: ${u} (${f})`),p(m,`ALTER TABLE kv_store ADD COLUMN ${u} ${f} ${d}`)}return o("Creating/updating indexes"),p(m,` CREATE INDEX IF NOT EXISTS idx_kv_ns_active ON kv_store(namespace, is_deleted, expires_at, key) WHERE is_deleted = 0; CREATE INDEX IF NOT EXISTS idx_kv_global_active ON kv_store(is_deleted, expires_at, namespace, key) WHERE is_deleted = 0; CREATE INDEX IF NOT EXISTS idx_kv_ns_lru ON kv_store(namespace, last_accessed) WHERE is_deleted = 0; CREATE INDEX IF NOT EXISTS idx_kv_expiry ON kv_store(expires_at) WHERE is_deleted = 0 AND expires_at < ${9007199254740991}; CREATE INDEX IF NOT EXISTS idx_kv_ns_stats ON kv_store(namespace, is_deleted); `),o("Updating schema version"),p(m,` CREATE TABLE IF NOT EXISTS yq_meta ( key TEXT PRIMARY KEY, value TEXT NOT NULL ); INSERT OR REPLACE INTO yq_meta (key, value) VALUES ('schema_version', '${1}'); `),p(m,"COMMIT"),o(`Migration complete: v${i} -> v${1}`),{success:!0,fromVersion:i,toVersion:1,changes:a,backupPath:g}}catch(u){try{p(m,"ROLLBACK")}catch{}let d=u instanceof Error?u.message:String(u);return o(`Migration failed: ${d}`),{success:!1,fromVersion:i,toVersion:1,changes:a,backupPath:g,error:d}}finally{C(m)}}function M(e){switch(e){case"namespace":case"key":return"TEXT NOT NULL";case"value":return"BLOB NOT NULL";case"created_at":case"expires_at":case"last_accessed":return"INTEGER NOT NULL";case"is_deleted":return"INTEGER NOT NULL";default:return"TEXT"}}function A(e){switch(e){case"namespace":return`DEFAULT '${S}'`;case"expires_at":return`DEFAULT ${9007199254740991}`;case"is_deleted":return"DEFAULT 0";case"last_accessed":return"DEFAULT 0";case"created_at":return`DEFAULT ${Date.now()}`;default:return""}}function O(e,t="*.yqs"){let s=[];if(!b.existsSync(e))return s;let n=b.readdirSync(e),r=new RegExp("^"+t.replace(/\*/g,".*").replace(/\?/g,".")+"$");for(let a of n)r.test(a)&&s.push(N.join(e,a));return s}import E from"node:path";import L from"node:os";import P from"node:crypto";var $="1.0.0";function I(){let e=L.tmpdir(),t=P.createHash("md5").update(process.cwd()).digest("hex")+"_store_storage";return E.join(e,`${t}.yqs`)}var V=` yqstore - yq-store database management CLI Usage: yqstore <command> [options] Commands: migrate Migrate database schema to the latest version check Check if database needs migration version Show version information help Show this help message Options for 'migrate': --db <path> Path to database file (uses default if not specified) --dir <path> Directory containing database files --pattern <glob> Pattern for finding databases (default: *.yqs) --backup Create backup before migration (default: true) --no-backup Skip backup creation --dry-run Preview changes without applying them --verbose, -v Show detailed output --yes, -y Skip confirmation prompts Options for 'check': --db <path> Path to database file (uses default if not specified) --dir <path> Directory containing database files --pattern <glob> Pattern for finding databases (default: *.yqs) --json Output result as JSON Examples: # Check the default database for current project yqstore check # Migrate the default database for current project yqstore migrate # Check a specific database yqstore check --db ./data/store.yqs # Migrate a specific database with backup yqstore migrate --db ./data/store.yqs --verbose # Dry run migration yqstore migrate --db ./data/store.yqs --dry-run # Migrate all databases in a directory yqstore migrate --dir ./data --pattern "*.yqs" --verbose # Skip confirmation yqstore migrate --db ./data/store.yqs -y `;function w(e){let t={command:"",pattern:"*.yqs",backup:!0,dryRun:!1,verbose:!1,yes:!1,json:!1},s=0;for(;s<e.length;){let n=e[s];!n.startsWith("-")&&!t.command?t.command=n:n==="--db"&&s+1<e.length?t.db=e[++s]:n==="--dir"&&s+1<e.length?t.dir=e[++s]:n==="--pattern"&&s+1<e.length?t.pattern=e[++s]:n==="--backup"?t.backup=!0:n==="--no-backup"?t.backup=!1:n==="--dry-run"?t.dryRun=!0:n==="--verbose"||n==="-v"?t.verbose=!0:n==="--yes"||n==="-y"?t.yes=!0:n==="--json"?t.json=!0:n==="--help"||n==="-h"?t.command="help":(n==="--version"||n==="-V")&&(t.command="version"),s++}return t}async function q(e){let s=(await import("node:readline")).createInterface({input:process.stdin,output:process.stdout});return new Promise(n=>{s.question(`${e} [y/N] `,r=>{s.close(),n(r.toLowerCase()==="y"||r.toLowerCase()==="yes")})})}async function U(e){let t=_(e);!e.db&&!e.dir&&!e.json&&console.log(`Using default database path: ${t[0]}`);let n=[];for(let r of t){let a=await y(r);if(n.push(a),!e.json){if(console.log(""),console.log(`Database: ${r}`),console.log(` Exists: ${a.exists}`),!a.exists){console.log(" Status: File does not exist");continue}console.log(` Current Version: ${a.validation.currentVersion??"unknown"}`),console.log(` Expected Version: ${a.validation.expectedVersion}`),console.log(` Needs Migration: ${a.needsMigration?"YES":"no"}`),a.validation.missingColumns.length>0&&console.log(` Missing Columns: ${a.validation.missingColumns.join(", ")}`),a.validation.extraColumns.length>0&&console.log(` Extra Columns: ${a.validation.extraColumns.join(", ")}`)}}if(e.json)console.log(JSON.stringify(n,null,2));else{console.log("");let r=n.filter(a=>a.needsMigration).length;r>0?(console.log(` ${r} database(s) need migration.`),console.log("Run: yqstore migrate --db <path>")):console.log(` All databases are up to date.`)}}async function F(e){let t=_(e);!e.db&&!e.dir&&console.log(`Using default database path: ${t[0]}`);let n=[];for(let o of t){let l=await y(o);l.needsMigration?n.push({path:o,validation:l.validation}):e.verbose&&console.log(`[skip] ${o} - already up to date`)}if(n.length===0){console.log("All databases are already up to date.");return}console.log(` Databases to migrate: ${n.length}`);for(let o of n)console.log(` - ${o.path}`),o.validation.missingColumns.length>0&&console.log(` Missing columns: ${o.validation.missingColumns.join(", ")}`);if(!e.yes&&!e.dryRun&&!await q(` Proceed with migration?`)){console.log("Migration cancelled.");return}let r=0,a=0;for(let o of n){let l={dbPath:o.path,backup:e.backup,dryRun:e.dryRun,verbose:e.verbose};console.log(` Migrating: ${o.path}`);let i=await D(l);if(i.success){if(r++,e.dryRun?console.log(" [DRY RUN] Would apply changes:"):console.log(` \u2713 Migrated v${i.fromVersion} -> v${i.toVersion}`),i.backupPath&&console.log(` Backup: ${i.backupPath}`),e.verbose)for(let g of i.changes)console.log(` - ${g}`)}else a++,console.log(` \u2717 Migration failed: ${i.error}`)}console.log(` `+"\u2500".repeat(50)),console.log(`Migration complete: ${r} succeeded, ${a} failed`)}function _(e){let t=[];if(e.db){let s=E.resolve(e.db);t.push(s)}if(e.dir){let s=E.resolve(e.dir),n=O(s,e.pattern);t.push(...n)}if(!e.db&&!e.dir){let s=I();t.push(s)}return t}async function B(){let e=process.argv.slice(2),t=w(e);switch(t.command){case"help":case"":console.log(V);break;case"version":console.log(`yqstore CLI v${$}`),console.log(`Schema version: ${1}`);break;case"check":await U(t);break;case"migrate":await F(t);break;default:console.error(`Unknown command: ${t.command}`),console.error('Run "yqstore help" for usage information.'),process.exit(1)}}B().catch(e=>{console.error("Error:",e.message),process.exit(1)});