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.7 kB
#!/usr/bin/env node #!/usr/bin/env node "use strict";var P=Object.create;var T=Object.defineProperty;var $=Object.getOwnPropertyDescriptor;var I=Object.getOwnPropertyNames;var V=Object.getPrototypeOf,w=Object.prototype.hasOwnProperty;var q=(e,t,s,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of I(t))!w.call(e,r)&&r!==s&&T(e,r,{get:()=>t[r],enumerable:!(n=$(t,r))||n.enumerable});return e};var p=(e,t,s)=>(s=e!=null?P(V(e)):{},q(t||!e||!e.__esModule?T(s,"default",{value:e,enumerable:!0}):s,e));var D=p(require("node:path"),1),b=p(require("node:fs"),1);var x="_default";var k=["namespace","key","value","created_at","expires_at","is_deleted","last_accessed"];function S(){return typeof Bun<"u"&&!!Bun?.version}async function O(e){if(S()){let{Database:t}=await import("bun:sqlite");return new t(e)}else{let{DatabaseSync:t}=await import("node:sqlite");return new t(e)}}function f(e,t){e.exec(t)}function E(e,t){return S()?e.query(t).get():e.prepare(t).get()}function U(e,t){return S()?e.query(t).all():e.prepare(t).all()}function _(e){e.close()}async function h(e){if(!b.default.existsSync(e))return{needsMigration:!1,validation:{isValid:!0,expectedVersion:1,missingColumns:[],extraColumns:[]},dbPath:e,exists:!1};let s=await O(e);try{if(!E(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=U(s,"PRAGMA table_info(kv_store)").map(d=>d.name),o;try{if(E(s,"SELECT name FROM sqlite_master WHERE type='table' AND name='yq_meta'")){let y=E(s,"SELECT value FROM yq_meta WHERE key = 'schema_version'");y&&(o=parseInt(y.value,10))}}catch{}let l=[...k],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{_(s)}}async function N(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.default.existsSync(t))return{success:!1,fromVersion:null,toVersion:1,changes:["Database file does not exist"],error:`Database file not found: ${t}`};let l=await h(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.default.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 O(t);try{f(m,"BEGIN TRANSACTION");for(let u of l.validation.missingColumns){let d=B(u),y=F(u);o(`Adding column: ${u} (${y})`),f(m,`ALTER TABLE kv_store ADD COLUMN ${u} ${y} ${d}`)}return o("Creating/updating indexes"),f(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"),f(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}'); `),f(m,"COMMIT"),o(`Migration complete: v${i} -> v${1}`),{success:!0,fromVersion:i,toVersion:1,changes:a,backupPath:g}}catch(u){try{f(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{_(m)}}function F(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 B(e){switch(e){case"namespace":return`DEFAULT '${x}'`;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 R(e,t="*.yqs"){let s=[];if(!b.default.existsSync(e))return s;let n=b.default.readdirSync(e),r=new RegExp("^"+t.replace(/\*/g,".*").replace(/\?/g,".")+"$");for(let a of n)r.test(a)&&s.push(D.default.join(e,a));return s}var v=p(require("node:path"),1),M=p(require("node:os"),1),A=p(require("node:crypto"),1),X="1.0.0";function j(){let e=M.default.tmpdir(),t=A.default.createHash("md5").update(process.cwd()).digest("hex")+"_store_storage";return v.default.join(e,`${t}.yqs`)}var K=` 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 H(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 z(e){let t=L(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 h(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 Y(e){let t=L(e);!e.db&&!e.dir&&console.log(`Using default database path: ${t[0]}`);let n=[];for(let o of t){let l=await h(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 H(` 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 N(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 L(e){let t=[];if(e.db){let s=v.default.resolve(e.db);t.push(s)}if(e.dir){let s=v.default.resolve(e.dir),n=R(s,e.pattern);t.push(...n)}if(!e.db&&!e.dir){let s=j();t.push(s)}return t}async function G(){let e=process.argv.slice(2),t=W(e);switch(t.command){case"help":case"":console.log(K);break;case"version":console.log(`yqstore CLI v${X}`),console.log(`Schema version: ${1}`);break;case"check":await z(t);break;case"migrate":await Y(t);break;default:console.error(`Unknown command: ${t.command}`),console.error('Run "yqstore help" for usage information.'),process.exit(1)}}G().catch(e=>{console.error("Error:",e.message),process.exit(1)});