schema-idb 0.0.6 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -332,14 +332,18 @@ When a store is removed from the schema, you can choose how to handle it:
332
332
  const db = openDB({
333
333
  name: "MyApp",
334
334
  versionStrategy: "auto",
335
- // 'error' (default): Throws an error when stores are removed
336
- // 'preserve': Renames removed stores to __storeName_deleted_v{version}__ as backup.
337
- // Preserved stores are isolated from the typed API to avoid future name collisions.
338
- removedStoreStrategy: "preserve",
335
+ removedStoreStrategy: "preserve", // See options below
339
336
  stores: [usersStore] as const,
340
337
  });
341
338
  ```
342
339
 
340
+ | Strategy | Behavior |
341
+ | -------- | -------- |
342
+ | `'error'` | Throws an error (default) |
343
+ | `'preserve'` | Renames to `__storeName_deleted_v{version}__` as backup |
344
+ | `'drop'` | Deletes the store and its data |
345
+ | `'ignore'` | Keeps the store as-is (no changes) |
346
+
343
347
  #### Behavior with explicit versioning
344
348
 
345
349
  When `versionStrategy` is `"explicit"`:
@@ -398,7 +402,7 @@ function openDB<T extends readonly SchemaStoreDefinition[]>(options: {
398
402
  stores: T;
399
403
  versionStrategy?: "auto" | "explicit";
400
404
  version?: number;
401
- removedStoreStrategy?: "error" | "preserve";
405
+ removedStoreStrategy?: "error" | "preserve" | "drop" | "ignore";
402
406
  }): SchemaDatabase<T>;
403
407
  ```
404
408
 
@@ -408,7 +412,7 @@ function openDB<T extends readonly SchemaStoreDefinition[]>(options: {
408
412
  | `stores` | `readonly SchemaStoreDefinition[]` | Store definitions created with `defineStore` |
409
413
  | `versionStrategy` | `"auto" \| "explicit"` | `"auto"` detects schema changes automatically. Default: `"explicit"` (recommended for production control) |
410
414
  | `version` | `number` | Required when `versionStrategy` is `"explicit"` |
411
- | `removedStoreStrategy` | `"error" \| "preserve"` | How to handle removed stores. Default: `"error"` |
415
+ | `removedStoreStrategy` | `"error" \| "preserve" \| "drop" \| "ignore"` | How to handle removed stores. Default: `"error"` |
412
416
 
413
417
  ### SchemaDatabase
414
418
 
@@ -21,7 +21,7 @@ export interface SchemaDBConfig<TStores extends readonly AnySchemaStore[]> {
21
21
  name: string;
22
22
  version?: number;
23
23
  versionStrategy?: 'explicit' | 'auto';
24
- removedStoreStrategy?: 'error' | 'preserve';
24
+ removedStoreStrategy?: 'error' | 'preserve' | 'drop' | 'ignore';
25
25
  stores: TStores;
26
26
  onBlocked?: () => void;
27
27
  onVersionChange?: () => void;
@@ -1,9 +1,9 @@
1
- "use strict";var E=Object.defineProperty;var d=(e,r)=>E(e,"name",{value:r,configurable:!0});import{openDatabase as T}from"./utils.js";import{createStoreAccessor as D}from"./storeAccessor.js";import{createStartTransaction as A}from"./transaction.js";import{determineAutoVersion as k,applySafeChanges as R,openDatabaseForSchemaRead as $,readExistingSchema as M,toDesiredSchema as z,detectSchemaChanges as j}from"./schemaDetection.js";import{ensureSchemaHistoryStore as B,getAppliedMigrations as x,recordMigrationApplied as C,initializeSchemaHistory as q}from"./migrationHistory.js";function Y(e){return e}d(Y,"getKeyPathString");function U(e,r,a){const n=d(async()=>{if(await e.readyPromise,!e.idb)throw new Error("Database initialization failed");return D(e.idb,r,a)},"getAccessor");return new Proxy({},{get(t,o){return o==="query"?c=>c?n().then(u=>u.query(c)):F(e,r,a):async(...c)=>(await n())[o](...c)}})}d(U,"createLazyStoreAccessor");function F(e,r,a){const n=d(async()=>{if(await e.readyPromise,!e.idb)throw new Error("Database initialization failed");return D(e.idb,r,a).query()},"getQueryBuilder"),t=d(o=>new Proxy({},{get(c,u){return u==="findAll"||u==="find"||u==="count"?()=>o().then(s=>s[u]()):(...s)=>t(d(()=>o().then(l=>l[u](...s)),"newGetBuilder"))}}),"createBuilderProxy");return t(n)}d(F,"createLazyQueryBuilder");function K(e,r){const a={get name(){return e.idb?.name??r},get version(){return e.idb?.version??0},get raw(){if(!e.idb)throw new Error("Database not ready. Call waitForReady() first or check ready property.");return e.idb},get ready(){return e.ready},waitForReady(){return e.readyPromise},close(){e.idb?.close()},startTransaction(...n){if(!e.startTransaction){const[t,o]=n,c=Array.isArray(t)?t:[t];return L(e,c,o)}return e.startTransaction(...n)}};for(const n of e.stores)Object.defineProperty(a,n.name,{get(){return U(e,n.name,n.defaults)},enumerable:!0});return a}d(K,"buildSchemaDatabase");function L(e,r,a){const n={get raw(){throw new Error("Transaction raw is not available before ready state. Use await db.waitForReady() before starting transactions.")},async commit(){if(await e.readyPromise,!e.startTransaction)throw new Error("Database initialization failed");return e.startTransaction(r,a).commit()},abort(){}};for(const t of r)e.stores.find(c=>c.name===t)&&Object.defineProperty(n,t,{get(){throw new Error("Transaction operations before ready state are not yet supported. Use await db.waitForReady() before starting transactions.")},enumerable:!0});return n}d(L,"createLazyTransaction");function O(e){const r=[],a=new Set;for(const n of e)for(const t of n.migrations){if(a.has(t.name))throw new Error(`Duplicate migration name "${t.name}" found across stores`);a.add(t.name),r.push(t)}return r.sort((n,t)=>n.name.localeCompare(t.name))}d(O,"collectMigrations");function N(e,r){const a=new Set(r);return e.filter(n=>!a.has(n.name)).sort((n,t)=>n.name.localeCompare(t.name))}d(N,"filterPendingMigrations");function V(e,r,a,n,t,o,c){if(B(e),a===0){for(const s of n){const i=e.createObjectStore(s.name,{keyPath:s.keyPath});for(const l of s.indexes)i.createIndex(l.name,l.keyPath,{unique:l.unique??!1,multiEntry:l.multiEntry??!1})}q(r)}else c&&c.safe.length>0&&R(e,r,c.safe,n);let u=[...o];for(const s of t)try{const i=s.up(e,r);i instanceof Promise&&i.catch(l=>{console.error(`Migration "${s.name}" failed:`,l),r.abort()}),C(r,s.name,u),u=[...u,s.name].sort()}catch(i){throw console.error(`Migration "${s.name}" failed:`,i),r.abort(),i}}d(V,"handleUpgrade");export function openDB(e){const{name:r,version:a,versionStrategy:n="explicit",removedStoreStrategy:t="error",stores:o,onBlocked:c,onVersionChange:u}=e,s=new Set;for(const f of o){if(s.has(f.name))throw new Error(`Duplicate store name: "${f.name}"`);s.add(f.name)}const i=O(o);let l=d(()=>{},"readyResolve"),g=d(()=>{},"readyReject");const p={idb:null,ready:!1,error:null,readyPromise:new Promise((f,y)=>{l=f,g=y}),readyResolve:l,readyReject:g,stores:o,startTransaction:null},m=K(p,r);return G(p,r,a,n,t,o,i,c,u),m}d(openDB,"openDB");async function G(e,r,a,n,t,o,c,u,s){try{let i=[],l=null,g;if(n==="auto"){const m=await k(r,o,{removedStoreStrategy:t});if(g=m.version,l=m.changes,m.version>1){const y=await $(r);y&&(i=await x(y),y.close())}N(c,i).length>0&&!m.needsUpgrade&&(g=m.version+1)}else{if(a===void 0)throw new Error('Version is required when versionStrategy is "explicit"');g=a;const m=await $(r);if(m){const f=m.version;i=await x(m);const y=M(m),_=z(o);m.close();const b=j(y,_);if(b.hasChanges){const S=[];for(const w of b.dangerous)w.type==="store_delete"&&t==="preserve"?b.safe.push({type:"store_rename",oldName:w.storeName,newName:`__${w.storeName}_deleted_v${f}__`}):S.push(w);if(S.length>0){const h=`Dangerous schema changes detected:
2
- ${S.map(v=>{switch(v.type){case"store_delete":return`Store "${v.storeName}" would be deleted. Use removedStoreStrategy: 'preserve' to backup, or add a migration to explicitly delete it.`;case"keypath_change":return`Store "${v.storeName}" keyPath changed from "${v.oldKeyPath}" to "${v.newKeyPath}". This requires recreating the store with a manual migration.`;default:return"Unknown dangerous change"}}).join(`
1
+ "use strict";var E=Object.defineProperty;var d=(e,r)=>E(e,"name",{value:r,configurable:!0});import{openDatabase as T}from"./utils.js";import{createStoreAccessor as P}from"./storeAccessor.js";import{createStartTransaction as k}from"./transaction.js";import{determineAutoVersion as A,applySafeChanges as R,openDatabaseForSchemaRead as $,readExistingSchema as M,toDesiredSchema as z,detectSchemaChanges as j}from"./schemaDetection.js";import{ensureSchemaHistoryStore as B,getAppliedMigrations as x,recordMigrationApplied as C,initializeSchemaHistory as q}from"./migrationHistory.js";function Y(e){return e}d(Y,"getKeyPathString");function U(e,r,a){const n=d(async()=>{if(await e.readyPromise,!e.idb)throw new Error("Database initialization failed");return P(e.idb,r,a)},"getAccessor");return new Proxy({},{get(t,o){return o==="query"?c=>c?n().then(u=>u.query(c)):F(e,r,a):async(...c)=>(await n())[o](...c)}})}d(U,"createLazyStoreAccessor");function F(e,r,a){const n=d(async()=>{if(await e.readyPromise,!e.idb)throw new Error("Database initialization failed");return P(e.idb,r,a).query()},"getQueryBuilder"),t=d(o=>new Proxy({},{get(c,u){return u==="findAll"||u==="find"||u==="count"?()=>o().then(s=>s[u]()):(...s)=>t(d(()=>o().then(l=>l[u](...s)),"newGetBuilder"))}}),"createBuilderProxy");return t(n)}d(F,"createLazyQueryBuilder");function K(e,r){const a={get name(){return e.idb?.name??r},get version(){return e.idb?.version??0},get raw(){if(!e.idb)throw new Error("Database not ready. Call waitForReady() first or check ready property.");return e.idb},get ready(){return e.ready},waitForReady(){return e.readyPromise},close(){e.idb?.close()},startTransaction(...n){if(!e.startTransaction){const[t,o]=n,c=Array.isArray(t)?t:[t];return L(e,c,o)}return e.startTransaction(...n)}};for(const n of e.stores)Object.defineProperty(a,n.name,{get(){return U(e,n.name,n.defaults)},enumerable:!0});return a}d(K,"buildSchemaDatabase");function L(e,r,a){const n={get raw(){throw new Error("Transaction raw is not available before ready state. Use await db.waitForReady() before starting transactions.")},async commit(){if(await e.readyPromise,!e.startTransaction)throw new Error("Database initialization failed");return e.startTransaction(r,a).commit()},abort(){}};for(const t of r)e.stores.find(c=>c.name===t)&&Object.defineProperty(n,t,{get(){throw new Error("Transaction operations before ready state are not yet supported. Use await db.waitForReady() before starting transactions.")},enumerable:!0});return n}d(L,"createLazyTransaction");function O(e){const r=[],a=new Set;for(const n of e)for(const t of n.migrations){if(a.has(t.name))throw new Error(`Duplicate migration name "${t.name}" found across stores`);a.add(t.name),r.push(t)}return r.sort((n,t)=>n.name.localeCompare(t.name))}d(O,"collectMigrations");function N(e,r){const a=new Set(r);return e.filter(n=>!a.has(n.name)).sort((n,t)=>n.name.localeCompare(t.name))}d(N,"filterPendingMigrations");function V(e,r,a,n,t,o,c){if(B(e),a===0){for(const s of n){const i=e.createObjectStore(s.name,{keyPath:s.keyPath});for(const l of s.indexes)i.createIndex(l.name,l.keyPath,{unique:l.unique??!1,multiEntry:l.multiEntry??!1})}q(r)}else c&&c.safe.length>0&&R(e,r,c.safe,n);let u=[...o];for(const s of t)try{const i=s.up(e,r);i instanceof Promise&&i.catch(l=>{console.error(`Migration "${s.name}" failed:`,l),r.abort()}),C(r,s.name,u),u=[...u,s.name].sort()}catch(i){throw console.error(`Migration "${s.name}" failed:`,i),r.abort(),i}}d(V,"handleUpgrade");export function openDB(e){const{name:r,version:a,versionStrategy:n="explicit",removedStoreStrategy:t="error",stores:o,onBlocked:c,onVersionChange:u}=e,s=new Set;for(const f of o){if(s.has(f.name))throw new Error(`Duplicate store name: "${f.name}"`);s.add(f.name)}const i=O(o);let l=d(()=>{},"readyResolve"),g=d(()=>{},"readyReject");const b={idb:null,ready:!1,error:null,readyPromise:new Promise((f,y)=>{l=f,g=y}),readyResolve:l,readyReject:g,stores:o,startTransaction:null},m=K(b,r);return G(b,r,a,n,t,o,i,c,u),m}d(openDB,"openDB");async function G(e,r,a,n,t,o,c,u,s){try{let i=[],l=null,g;if(n==="auto"){const m=await A(r,o,{removedStoreStrategy:t});if(g=m.version,l=m.changes,m.version>1){const y=await $(r);y&&(i=await x(y),y.close())}N(c,i).length>0&&!m.needsUpgrade&&(g=m.version+1)}else{if(a===void 0)throw new Error('Version is required when versionStrategy is "explicit"');g=a;const m=await $(r);if(m){const f=m.version;i=await x(m);const y=M(m),_=z(o);m.close();const p=j(y,_);if(p.hasChanges){const S=[];for(const w of p.dangerous)if(w.type==="store_delete")switch(t){case"preserve":p.safe.push({type:"store_rename",oldName:w.storeName,newName:`__${w.storeName}_deleted_v${f}__`});break;case"drop":p.safe.push(w);break;case"ignore":break;default:S.push(w);break}else S.push(w);if(S.length>0){const h=`Dangerous schema changes detected:
2
+ ${S.map(v=>{switch(v.type){case"store_delete":return`Store "${v.storeName}" would be deleted. Use removedStoreStrategy: 'preserve', 'drop', or 'ignore' to handle this.`;case"keypath_change":return`Store "${v.storeName}" keyPath changed from "${v.oldKeyPath}" to "${v.newKeyPath}". This requires recreating the store with a manual migration.`;default:return"Unknown dangerous change"}}).join(`
3
3
  `)}
4
4
 
5
- Add explicit migrations to handle these changes safely.`;throw console.error("[schema-idb]",h),new Error(h)}if(b.dangerous=S,l=b,g<=f){const w=b.safe.map(h=>{switch(h.type){case"store_add":return`- Add store "${h.storeName}"`;case"store_rename":return`- Rename store "${h.oldName}" to "${h.newName}"`;case"index_add":return`- Add index "${h.indexName}" on "${h.storeName}"`;case"index_delete":return`- Delete index "${h.indexName}" from "${h.storeName}"`;default:return"- Schema change"}});console.warn(`[schema-idb] Schema changes detected but version not bumped:
5
+ Add explicit migrations to handle these changes safely.`;throw console.error("[schema-idb]",h),new Error(h)}if(p.dangerous=S,l=p,g<=f){const w=p.safe.map(h=>{switch(h.type){case"store_add":return`- Add store "${h.storeName}"`;case"store_delete":return`- Delete store "${h.storeName}"`;case"store_rename":return`- Rename store "${h.oldName}" to "${h.newName}"`;case"index_add":return`- Add index "${h.indexName}" on "${h.storeName}"`;case"index_delete":return`- Delete index "${h.indexName}" from "${h.storeName}"`;default:return"- Schema change"}});console.warn(`[schema-idb] Schema changes detected but version not bumped:
6
6
  ${w.join(`
7
7
  `)}
8
8
  Current DB version: ${f}, Provided version: ${g}
9
- Bump the version to apply these changes.`)}}}}const P=N(c,i),p=await T(r,g,(m,f,y)=>{V(m,f,y,o,P,i,l)},u);s&&(p.onversionchange=s),e.idb=p,e.startTransaction=A(p,o),e.ready=!0,e.readyResolve()}catch(i){e.error=i instanceof Error?i:new Error(String(i)),e.readyReject(e.error)}}d(G,"initializeDatabase");
9
+ Bump the version to apply these changes.`)}}}}const D=N(c,i),b=await T(r,g,(m,f,y)=>{V(m,f,y,o,D,i,l)},u);s&&(b.onversionchange=s),e.idb=b,e.startTransaction=k(b,o),e.ready=!0,e.readyResolve()}catch(i){e.error=i instanceof Error?i:new Error(String(i)),e.readyReject(e.error)}}d(G,"initializeDatabase");
@@ -68,7 +68,7 @@ export declare function applySafeChanges(db: IDBDatabase, tx: IDBTransaction, ch
68
68
  export declare function getCurrentDatabaseVersion(dbName: string): Promise<number>;
69
69
  export declare function openDatabaseForSchemaRead(dbName: string): Promise<IDBDatabase | null>;
70
70
  export interface AutoVersionOptions {
71
- removedStoreStrategy?: 'error' | 'preserve';
71
+ removedStoreStrategy?: 'error' | 'preserve' | 'drop' | 'ignore';
72
72
  }
73
73
  export declare function determineAutoVersion(dbName: string, stores: readonly {
74
74
  name: string;
@@ -1,5 +1,5 @@
1
- "use strict";var g=Object.defineProperty;var l=(r,t)=>g(r,"name",{value:t,configurable:!0});function p(r){return r.startsWith("__")}l(p,"isInternalStore");export function readExistingSchema(r){const t=new Map,n=Array.from({length:r.objectStoreNames.length},(a,o)=>r.objectStoreNames.item(o)).filter(a=>!p(a));for(const a of n){const o=r.transaction(a,"readonly"),u=o.objectStore(a),s=new Map,c=Array.from({length:u.indexNames.length},(e,i)=>u.indexNames.item(i));for(const e of c){const i=u.index(e);s.set(e,{keyPath:i.keyPath,unique:i.unique,multiEntry:i.multiEntry})}t.set(a,{name:a,keyPath:u.keyPath,indexes:s}),o.abort()}return t}l(readExistingSchema,"readExistingSchema");export function toDesiredSchema(r){const t=new Map;for(const n of r)t.set(n.name,{name:n.name,keyPath:n.keyPath,indexes:n.indexes});return t}l(toDesiredSchema,"toDesiredSchema");function x(r,t){return r===t||r==null&&t==null?!0:r==null||t==null?!1:Array.isArray(r)&&Array.isArray(t)?r.length!==t.length?!1:r.every((n,a)=>n===t[a]):r===t}l(x,"keyPathEquals");export function detectSchemaChanges(r,t){const n=[],a=[];for(const[o,u]of t){const s=r.get(o);if(!s){n.push({type:"store_add",storeName:o});continue}if(!x(s.keyPath,u.keyPath)){a.push({type:"keypath_change",storeName:o,oldKeyPath:s.keyPath,newKeyPath:u.keyPath});continue}for(const e of u.indexes){const i=s.indexes.get(e.name);if(!i)n.push({type:"index_add",storeName:o,indexName:e.name,index:e});else{const m=!x(i.keyPath,e.keyPath),h=i.unique!==(e.unique??!1),d=i.multiEntry!==(e.multiEntry??!1);(m||h||d)&&(n.push({type:"index_delete",storeName:o,indexName:e.name}),n.push({type:"index_add",storeName:o,indexName:e.name,index:e}))}}const c=new Set(u.indexes.map(e=>e.name));for(const e of s.indexes.keys())c.has(e)||n.push({type:"index_delete",storeName:o,indexName:e})}for(const o of r.keys())t.has(o)||a.push({type:"store_delete",storeName:o});return{safe:n,dangerous:a,hasChanges:n.length>0||a.length>0}}l(detectSchemaChanges,"detectSchemaChanges");export function applySafeChanges(r,t,n,a){const o=n.filter(s=>s.type==="store_rename"),u=new Map;for(const s of o){const c=t.objectStore(s.oldName),e=[],i=Array.from({length:c.indexNames.length},(h,d)=>c.indexNames.item(d));for(const h of i){const d=c.index(h);e.push({name:h,keyPath:d.keyPath,unique:d.unique,multiEntry:d.multiEntry})}u.set(s.oldName,{keyPath:c.keyPath,indexes:e,newName:s.newName});const m=c.getAll();m.onsuccess=()=>{const h=m.result,d=u.get(s.oldName),y=r.createObjectStore(d.newName,{keyPath:d.keyPath});for(const f of d.indexes)y.createIndex(f.name,f.keyPath,{unique:f.unique,multiEntry:f.multiEntry});for(const f of h)y.put(f)},r.deleteObjectStore(s.oldName)}for(const s of n)switch(s.type){case"store_add":{const c=a.find(e=>e.name===s.storeName);if(c){const e=r.createObjectStore(c.name,{keyPath:c.keyPath});for(const i of c.indexes)e.createIndex(i.name,i.keyPath,{unique:i.unique??!1,multiEntry:i.multiEntry??!1})}break}case"index_add":{t.objectStore(s.storeName).createIndex(s.indexName,s.index.keyPath,{unique:s.index.unique??!1,multiEntry:s.index.multiEntry??!1});break}case"index_delete":{t.objectStore(s.storeName).deleteIndex(s.indexName);break}}}l(applySafeChanges,"applySafeChanges");export async function getCurrentDatabaseVersion(r){return new Promise(t=>{const n=indexedDB.open(r);n.onsuccess=()=>{const a=n.result,o=a.version;a.close(),t(o)},n.onerror=()=>{t(0)}})}l(getCurrentDatabaseVersion,"getCurrentDatabaseVersion");export async function openDatabaseForSchemaRead(r){return new Promise(t=>{const n=indexedDB.open(r);n.onsuccess=()=>{t(n.result)},n.onerror=()=>{t(null)},n.onupgradeneeded=()=>{n.transaction?.abort(),t(null)}})}l(openDatabaseForSchemaRead,"openDatabaseForSchemaRead");export async function determineAutoVersion(r,t,n={}){const{removedStoreStrategy:a="error"}=n,o=await openDatabaseForSchemaRead(r);if(!o)return{version:1,changes:null,needsUpgrade:!0};const u=o.version,s=readExistingSchema(o),c=toDesiredSchema(t);o.close();const e=detectSchemaChanges(s,c);if(!e.hasChanges)return{version:u,changes:null,needsUpgrade:!1};const i=[];for(const m of e.dangerous)m.type==="store_delete"&&a==="preserve"?e.safe.push({type:"store_rename",oldName:m.storeName,newName:`__${m.storeName}_deleted_v${u}__`}):i.push(m);if(e.dangerous=i,e.dangerous.length>0){const h=`Dangerous schema changes detected:
2
- ${e.dangerous.map(d=>{switch(d.type){case"store_delete":return`Store "${d.storeName}" would be deleted. Use removedStoreStrategy: 'preserve' to backup, or add a migration to explicitly delete it.`;case"keypath_change":return`Store "${d.storeName}" keyPath changed from "${d.oldKeyPath}" to "${d.newKeyPath}". This requires recreating the store with a manual migration.`;default:return"Unknown dangerous change"}}).join(`
1
+ "use strict";var x=Object.defineProperty;var l=(t,n)=>x(t,"name",{value:n,configurable:!0});function p(t){return t.startsWith("__")}l(p,"isInternalStore");export function readExistingSchema(t){const n=new Map,r=Array.from({length:t.objectStoreNames.length},(a,o)=>t.objectStoreNames.item(o)).filter(a=>!p(a));for(const a of r){const o=t.transaction(a,"readonly"),d=o.objectStore(a),s=new Map,c=Array.from({length:d.indexNames.length},(e,i)=>d.indexNames.item(i));for(const e of c){const i=d.index(e);s.set(e,{keyPath:i.keyPath,unique:i.unique,multiEntry:i.multiEntry})}n.set(a,{name:a,keyPath:d.keyPath,indexes:s}),o.abort()}return n}l(readExistingSchema,"readExistingSchema");export function toDesiredSchema(t){const n=new Map;for(const r of t)n.set(r.name,{name:r.name,keyPath:r.keyPath,indexes:r.indexes});return n}l(toDesiredSchema,"toDesiredSchema");function g(t,n){return t===n||t==null&&n==null?!0:t==null||n==null?!1:Array.isArray(t)&&Array.isArray(n)?t.length!==n.length?!1:t.every((r,a)=>r===n[a]):t===n}l(g,"keyPathEquals");export function detectSchemaChanges(t,n){const r=[],a=[];for(const[o,d]of n){const s=t.get(o);if(!s){r.push({type:"store_add",storeName:o});continue}if(!g(s.keyPath,d.keyPath)){a.push({type:"keypath_change",storeName:o,oldKeyPath:s.keyPath,newKeyPath:d.keyPath});continue}for(const e of d.indexes){const i=s.indexes.get(e.name);if(!i)r.push({type:"index_add",storeName:o,indexName:e.name,index:e});else{const h=!g(i.keyPath,e.keyPath),m=i.unique!==(e.unique??!1),u=i.multiEntry!==(e.multiEntry??!1);(h||m||u)&&(r.push({type:"index_delete",storeName:o,indexName:e.name}),r.push({type:"index_add",storeName:o,indexName:e.name,index:e}))}}const c=new Set(d.indexes.map(e=>e.name));for(const e of s.indexes.keys())c.has(e)||r.push({type:"index_delete",storeName:o,indexName:e})}for(const o of t.keys())n.has(o)||a.push({type:"store_delete",storeName:o});return{safe:r,dangerous:a,hasChanges:r.length>0||a.length>0}}l(detectSchemaChanges,"detectSchemaChanges");export function applySafeChanges(t,n,r,a){const o=r.filter(s=>s.type==="store_rename"),d=new Map;for(const s of o){const c=n.objectStore(s.oldName),e=[],i=Array.from({length:c.indexNames.length},(m,u)=>c.indexNames.item(u));for(const m of i){const u=c.index(m);e.push({name:m,keyPath:u.keyPath,unique:u.unique,multiEntry:u.multiEntry})}d.set(s.oldName,{keyPath:c.keyPath,indexes:e,newName:s.newName});const h=c.getAll();h.onsuccess=()=>{const m=h.result,u=d.get(s.oldName),y=t.createObjectStore(u.newName,{keyPath:u.keyPath});for(const f of u.indexes)y.createIndex(f.name,f.keyPath,{unique:f.unique,multiEntry:f.multiEntry});for(const f of m)y.put(f)},t.deleteObjectStore(s.oldName)}for(const s of r)switch(s.type){case"store_add":{const c=a.find(e=>e.name===s.storeName);if(c){const e=t.createObjectStore(c.name,{keyPath:c.keyPath});for(const i of c.indexes)e.createIndex(i.name,i.keyPath,{unique:i.unique??!1,multiEntry:i.multiEntry??!1})}break}case"index_add":{n.objectStore(s.storeName).createIndex(s.indexName,s.index.keyPath,{unique:s.index.unique??!1,multiEntry:s.index.multiEntry??!1});break}case"index_delete":{n.objectStore(s.storeName).deleteIndex(s.indexName);break}case"store_delete":{t.deleteObjectStore(s.storeName);break}}}l(applySafeChanges,"applySafeChanges");export async function getCurrentDatabaseVersion(t){return new Promise(n=>{const r=indexedDB.open(t);r.onsuccess=()=>{const a=r.result,o=a.version;a.close(),n(o)},r.onerror=()=>{n(0)}})}l(getCurrentDatabaseVersion,"getCurrentDatabaseVersion");export async function openDatabaseForSchemaRead(t){return new Promise(n=>{const r=indexedDB.open(t);r.onsuccess=()=>{n(r.result)},r.onerror=()=>{n(null)},r.onupgradeneeded=()=>{r.transaction?.abort(),n(null)}})}l(openDatabaseForSchemaRead,"openDatabaseForSchemaRead");export async function determineAutoVersion(t,n,r={}){const{removedStoreStrategy:a="error"}=r,o=await openDatabaseForSchemaRead(t);if(!o)return{version:1,changes:null,needsUpgrade:!0};const d=o.version,s=readExistingSchema(o),c=toDesiredSchema(n);o.close();const e=detectSchemaChanges(s,c);if(!e.hasChanges)return{version:d,changes:null,needsUpgrade:!1};const i=[];for(const h of e.dangerous)if(h.type==="store_delete")switch(a){case"preserve":e.safe.push({type:"store_rename",oldName:h.storeName,newName:`__${h.storeName}_deleted_v${d}__`});break;case"drop":e.safe.push(h);break;case"ignore":break;default:i.push(h);break}else i.push(h);if(e.dangerous=i,e.dangerous.length>0){const m=`Dangerous schema changes detected:
2
+ ${e.dangerous.map(u=>{switch(u.type){case"store_delete":return`Store "${u.storeName}" would be deleted. Use removedStoreStrategy: 'preserve', 'drop', or 'ignore' to handle this.`;case"keypath_change":return`Store "${u.storeName}" keyPath changed from "${u.oldKeyPath}" to "${u.newKeyPath}". This requires recreating the store with a manual migration.`;default:return"Unknown dangerous change"}}).join(`
3
3
  `)}
4
4
 
5
- Add explicit migrations to handle these changes safely.`;throw console.error("[schema-idb]",h),new Error(h)}return{version:u+1,changes:e,needsUpgrade:!0}}l(determineAutoVersion,"determineAutoVersion");
5
+ Add explicit migrations to handle these changes safely.`;throw console.error("[schema-idb]",m),new Error(m)}return e.safe.length===0?{version:d,changes:null,needsUpgrade:!1}:{version:d+1,changes:e,needsUpgrade:!0}}l(determineAutoVersion,"determineAutoVersion");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "schema-idb",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "Type-safe IndexedDB wrapper with chainable transactions",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",