Skip to content

Commit 2086ca9

Browse files
committed
Fix persistence
1 parent bb27161 commit 2086ca9

11 files changed

Lines changed: 946 additions & 4 deletions

packages/core/PERSISTENCE.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,42 @@ const db = new DeclarativeDatabase({ adapter, schema });
8181

8282
## Quick Start
8383

84+
### Recommended: Initialize Storage First
85+
86+
For browser environments, it's recommended to initialize storage and request permissions before creating the adapter:
87+
88+
```typescript
89+
import {
90+
initializeStorage,
91+
AdapterFactory,
92+
DeclarativeDatabase
93+
} from 'declarative-sqlite';
94+
95+
// Initialize storage and request permissions
96+
const storageResult = await initializeStorage({
97+
requestPersistence: true,
98+
preferredBackend: StorageBackend.OPFS,
99+
verbose: true,
100+
});
101+
102+
console.log('Using backend:', storageResult.backend);
103+
console.log('Persistent storage:', storageResult.isPersistent);
104+
105+
if (storageResult.warnings.length > 0) {
106+
console.warn('Storage warnings:', storageResult.warnings);
107+
}
108+
109+
// Create adapter with the detected backend
110+
const adapter = await AdapterFactory.create({
111+
backend: storageResult.backend,
112+
name: 'myapp.db',
113+
enableWAL: true,
114+
});
115+
116+
const db = new DeclarativeDatabase({ adapter, schema, autoMigrate: true });
117+
await db.initialize();
118+
```
119+
84120
### Using AdapterFactory
85121

86122
The simplest way to create a configured adapter:

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "declarative-sqlite",
3-
"version": "1.15.0",
3+
"version": "1.16.0",
44
"description": "TypeScript port of declarative_sqlite for PWA and Capacitor applications - Zero code generation, type-safe SQLite with automatic migration",
55
"type": "module",
66
"main": "./dist/index.cjs",

packages/core/src/adapters/adapter.interface.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,10 @@ export interface SQLiteAdapter {
7575
* Check if database is currently open
7676
*/
7777
isOpen(): boolean;
78+
79+
/**
80+
* Export the database as a Uint8Array
81+
* Useful for downloading/saving the database file
82+
*/
83+
exportDatabase(): Promise<Uint8Array>;
7884
}

packages/core/src/database/better-sqlite3-adapter.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,17 @@ export class BetterSqlite3Adapter implements SQLiteAdapter {
7777
return this.db !== null && this.db !== undefined;
7878
}
7979

80+
/**
81+
* Export the database as a Uint8Array
82+
*/
83+
async exportDatabase(): Promise<Uint8Array> {
84+
this.ensureOpen();
85+
86+
// better-sqlite3 provides a serialize method
87+
const buffer = this.db.serialize();
88+
return new Uint8Array(buffer);
89+
}
90+
8091
/**
8192
* Get the underlying better-sqlite3 database instance
8293
*/

packages/core/src/database/declarative-database.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,14 @@ export class DeclarativeDatabase {
363363
this.isInitialized = false;
364364
}
365365

366+
/**
367+
* Export the database as a Uint8Array
368+
* Useful for downloading or backing up the database
369+
*/
370+
async exportDatabase(): Promise<Uint8Array> {
371+
return await this.adapter.exportDatabase();
372+
}
373+
366374
/**
367375
* Get the underlying adapter
368376
*/
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
/**
2+
* Example: Exporting and downloading SQLite database
3+
*
4+
* This demonstrates how to export the database and download it in a browser
5+
*/
6+
7+
import {
8+
SchemaBuilder,
9+
DeclarativeDatabase,
10+
AdapterFactory,
11+
StorageBackend,
12+
} from '../index';
13+
14+
const schema = new SchemaBuilder()
15+
.table('users', t => {
16+
t.guid('id').notNull('');
17+
t.text('name').notNull('');
18+
t.text('email').notNull('');
19+
t.key('id').primary();
20+
})
21+
.build();
22+
23+
/**
24+
* Export database to a file (browser download)
25+
*/
26+
export async function exportDatabaseToDownload() {
27+
// Create and populate database
28+
const adapter = await AdapterFactory.create({
29+
backend: StorageBackend.Memory,
30+
name: 'myapp.db',
31+
});
32+
33+
const db = new DeclarativeDatabase({ adapter, schema, autoMigrate: true });
34+
await db.initialize();
35+
36+
// Add some data
37+
await db.insert('users', { id: 'u1', name: 'Alice', email: 'alice@example.com' });
38+
await db.insert('users', { id: 'u2', name: 'Bob', email: 'bob@example.com' });
39+
40+
// Export database
41+
const dbBytes = await db.exportDatabase();
42+
console.log('Database size:', dbBytes.length, 'bytes');
43+
44+
// In browser: trigger download
45+
if (typeof window !== 'undefined') {
46+
const blob = new Blob([new Uint8Array(dbBytes)], { type: 'application/x-sqlite3' });
47+
const url = URL.createObjectURL(blob);
48+
const a = document.createElement('a');
49+
a.href = url;
50+
a.download = 'myapp.db';
51+
a.click();
52+
URL.revokeObjectURL(url);
53+
console.log('Database download triggered');
54+
}
55+
56+
await db.close();
57+
}
58+
59+
/**
60+
* Export database to file system (Node.js)
61+
*/
62+
export async function exportDatabaseToFile() {
63+
const adapter = await AdapterFactory.create({
64+
backend: StorageBackend.Memory,
65+
name: 'myapp.db',
66+
});
67+
68+
const db = new DeclarativeDatabase({ adapter, schema, autoMigrate: true });
69+
await db.initialize();
70+
71+
// Add some data
72+
await db.insert('users', { id: 'u1', name: 'Alice', email: 'alice@example.com' });
73+
74+
// Export database
75+
const dbBytes = await db.exportDatabase();
76+
77+
// In Node.js: write to file
78+
if (typeof process !== 'undefined' && process.versions?.node) {
79+
const fs = await import('fs/promises');
80+
await fs.writeFile('exported-database.db', dbBytes);
81+
console.log('Database exported to exported-database.db');
82+
}
83+
84+
await db.close();
85+
}
86+
87+
/**
88+
* Create a backup of the database
89+
*/
90+
export async function createDatabaseBackup() {
91+
const adapter = await AdapterFactory.create({
92+
backend: StorageBackend.Auto,
93+
name: 'myapp.db',
94+
});
95+
96+
const db = new DeclarativeDatabase({ adapter, schema, autoMigrate: true });
97+
await db.initialize();
98+
99+
// Regular operations...
100+
await db.insert('users', { id: 'u1', name: 'Alice', email: 'alice@example.com' });
101+
102+
// Create backup
103+
const backup = await db.exportDatabase();
104+
105+
// Store backup (could be to IndexedDB, localStorage, server, etc.)
106+
if (typeof localStorage !== 'undefined') {
107+
// Convert to base64 for localStorage
108+
const base64 = btoa(String.fromCharCode(...backup));
109+
localStorage.setItem('db-backup', base64);
110+
console.log('Backup stored in localStorage');
111+
}
112+
113+
await db.close();
114+
}
115+
116+
/**
117+
* Restore database from backup
118+
*/
119+
export async function restoreDatabaseFromBackup() {
120+
if (typeof localStorage === 'undefined') {
121+
console.log('localStorage not available');
122+
return;
123+
}
124+
125+
// Retrieve backup
126+
const base64 = localStorage.getItem('db-backup');
127+
if (!base64) {
128+
console.log('No backup found');
129+
return;
130+
}
131+
132+
// Convert from base64
133+
const binaryString = atob(base64);
134+
const bytes = new Uint8Array(binaryString.length);
135+
for (let i = 0; i < binaryString.length; i++) {
136+
bytes[i] = binaryString.charCodeAt(i);
137+
}
138+
139+
// Note: To restore, you would need to use the database's import functionality
140+
// This varies by adapter - sqlite-wasm might support importing via constructor
141+
console.log('Backup data retrieved:', bytes.length, 'bytes');
142+
}
143+
144+
/**
145+
* Export database periodically (auto-backup)
146+
*/
147+
export async function setupAutoBackup(intervalMs: number = 60000) {
148+
const adapter = await AdapterFactory.create({
149+
backend: StorageBackend.Auto,
150+
name: 'myapp.db',
151+
});
152+
153+
const db = new DeclarativeDatabase({ adapter, schema, autoMigrate: true });
154+
await db.initialize();
155+
156+
// Setup periodic backup
157+
const backupInterval = setInterval(async () => {
158+
try {
159+
const backup = await db.exportDatabase();
160+
const timestamp = new Date().toISOString();
161+
162+
// Store with timestamp
163+
if (typeof localStorage !== 'undefined') {
164+
const base64 = btoa(String.fromCharCode(...backup));
165+
localStorage.setItem(`db-backup-${timestamp}`, base64);
166+
console.log(`Backup created at ${timestamp}`);
167+
168+
// Keep only last 5 backups
169+
const keys = Object.keys(localStorage).filter(k => k.startsWith('db-backup-'));
170+
if (keys.length > 5) {
171+
keys.sort();
172+
for (let i = 0; i < keys.length - 5; i++) {
173+
const key = keys[i];
174+
if (key) localStorage.removeItem(key);
175+
}
176+
}
177+
}
178+
} catch (error) {
179+
console.error('Backup failed:', error);
180+
}
181+
}, intervalMs);
182+
183+
// Return cleanup function
184+
return () => {
185+
clearInterval(backupInterval);
186+
db.close();
187+
};
188+
}
189+
190+
/**
191+
* Compare two database exports
192+
*/
193+
export async function compareDatabases() {
194+
// Create two databases
195+
const adapter1 = await AdapterFactory.create({ backend: StorageBackend.Memory });
196+
const db1 = new DeclarativeDatabase({ adapter: adapter1, schema, autoMigrate: true });
197+
await db1.initialize();
198+
await db1.insert('users', { id: 'u1', name: 'Alice', email: 'alice@example.com' });
199+
200+
const adapter2 = await AdapterFactory.create({ backend: StorageBackend.Memory });
201+
const db2 = new DeclarativeDatabase({ adapter: adapter2, schema, autoMigrate: true });
202+
await db2.initialize();
203+
await db2.insert('users', { id: 'u1', name: 'Alice', email: 'alice@example.com' });
204+
205+
// Export both
206+
const export1 = await db1.exportDatabase();
207+
const export2 = await db2.exportDatabase();
208+
209+
// Compare
210+
const areEqual = export1.length === export2.length &&
211+
export1.every((byte, i) => byte === export2[i]);
212+
213+
console.log('Databases are equal:', areEqual);
214+
console.log('Database 1 size:', export1.length);
215+
console.log('Database 2 size:', export2.length);
216+
217+
await db1.close();
218+
await db2.close();
219+
}
220+
221+
// Run examples if executed directly
222+
if (require.main === module) {
223+
(async () => {
224+
console.log('=== Database Export Examples ===\n');
225+
226+
await exportDatabaseToDownload();
227+
await exportDatabaseToFile();
228+
await createDatabaseBackup();
229+
await compareDatabases();
230+
231+
console.log('\n=== Examples completed ===');
232+
})().catch(console.error);
233+
}

0 commit comments

Comments
 (0)