Skip to content

Commit 80a367b

Browse files
authored
Merge pull request #51 from graknol/copilot/redesign-db-interaction
2 parents da0616b + e24566b commit 80a367b

File tree

11 files changed

+451
-917
lines changed

11 files changed

+451
-917
lines changed
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
# Plain Objects API - New Design
2+
3+
This document demonstrates the new plain objects API for declarative-sqlite, designed for simplicity and perfect Comlink compatibility.
4+
5+
## Overview
6+
7+
The new design eliminates the Proxy-based DbRecord system in favor of plain JavaScript objects. This makes records:
8+
- ✅ Fully serializable through Comlink
9+
- ✅ Compatible with structured clone
10+
- ✅ JSON serializable
11+
- ✅ Simpler to use and understand
12+
13+
## Basic Usage
14+
15+
### Creating and Saving Records
16+
17+
```typescript
18+
import { SchemaBuilder, DeclarativeDatabase, AdapterFactory } from 'declarative-sqlite';
19+
20+
// Define schema
21+
const schema = new SchemaBuilder()
22+
.table('users', t => {
23+
t.guid('id').notNull('');
24+
t.text('name').notNull('');
25+
t.text('email').notNull('');
26+
t.integer('age').notNull(0);
27+
t.key('id').primary();
28+
})
29+
.build();
30+
31+
// Create database
32+
const adapter = await AdapterFactory.create({ backend: 'memory' });
33+
const db = new DeclarativeDatabase({ adapter, schema });
34+
await db.initialize();
35+
36+
// Create a new record
37+
const user = db.createRecord('users');
38+
user.id = 'user-1';
39+
user.name = 'Alice';
40+
user.email = 'alice@example.com';
41+
user.age = 30;
42+
43+
// Save (INSERT)
44+
await db.save(user);
45+
46+
// Update
47+
user.age = 31;
48+
await db.save(user); // UPDATE - only changed fields
49+
50+
// Delete
51+
await db.deleteRecord(user);
52+
```
53+
54+
### Querying Records
55+
56+
```typescript
57+
// Query returns plain objects
58+
const users = await db.query('users', {
59+
where: 'age >= ?',
60+
whereArgs: [21],
61+
orderBy: 'name'
62+
});
63+
64+
// Access data directly
65+
console.log(users[0].name); // "Alice"
66+
67+
// Modify and save
68+
users[0].age = 32;
69+
await db.save(users[0]);
70+
```
71+
72+
### Streaming Queries
73+
74+
```typescript
75+
// Stream returns plain objects
76+
const users$ = db.stream('users', {
77+
where: 'age >= ?',
78+
whereArgs: [21]
79+
});
80+
81+
users$.subscribe(users => {
82+
console.log(`Found ${users.length} users`);
83+
users.forEach(user => {
84+
console.log(`${user.name} - ${user.age}`);
85+
});
86+
});
87+
88+
// Any changes trigger refresh
89+
await db.insert('users', { id: 'user-2', name: 'Bob', age: 25 });
90+
// Stream subscribers automatically receive updated data
91+
```
92+
93+
## Change Tracking with xRec
94+
95+
Each record has a hidden `xRec` property that stores the original database values:
96+
97+
```typescript
98+
const user = await db.queryOne('users', { where: 'id = ?', whereArgs: ['user-1'] });
99+
100+
// xRec stores original values (non-enumerable)
101+
const xRec = user.xRec;
102+
console.log(xRec.name); // Original name from database
103+
104+
// Modify the record
105+
user.name = 'Alice Smith';
106+
107+
// Compare with original
108+
console.log(user.name !== user.xRec.name); // true - changed
109+
110+
// Save updates xRec to reflect new database state
111+
await db.save(user);
112+
console.log(user.name === user.xRec.name); // true - synced
113+
```
114+
115+
## Comlink Integration
116+
117+
The new design works seamlessly with Comlink for web workers:
118+
119+
### Worker Setup (worker.ts)
120+
121+
```typescript
122+
import { expose } from 'comlink';
123+
import { DeclarativeDatabase, SchemaBuilder, AdapterFactory } from 'declarative-sqlite';
124+
125+
const schema = new SchemaBuilder()
126+
.table('users', t => {
127+
t.guid('id').notNull('');
128+
t.text('name').notNull('');
129+
t.integer('age').notNull(0);
130+
t.key('id').primary();
131+
})
132+
.build();
133+
134+
const adapter = await AdapterFactory.create({ name: 'mydb.db' });
135+
const db = new DeclarativeDatabase({ adapter, schema });
136+
await db.initialize();
137+
138+
// Expose database methods
139+
expose({
140+
query: (table: string, options?: any) => db.query(table, options),
141+
save: (record: any) => db.save(record),
142+
deleteRecord: (record: any) => db.deleteRecord(record),
143+
createRecord: (table: string) => db.createRecord(table),
144+
});
145+
```
146+
147+
### Main Thread (main.ts)
148+
149+
```typescript
150+
import { wrap } from 'comlink';
151+
152+
const worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' });
153+
const db = wrap(worker);
154+
155+
// Query from worker - returns plain objects automatically
156+
const users = await db.query('users');
157+
console.log(users[0].name); // Works!
158+
159+
// Modify and save back through worker
160+
users[0].age = 31;
161+
await db.save(users[0]); // Serializes automatically!
162+
163+
// Create new record in worker
164+
const newUser = await db.createRecord('users');
165+
newUser.id = 'user-2';
166+
newUser.name = 'Bob';
167+
await db.save(newUser); // Works seamlessly!
168+
```
169+
170+
## Migration from Old API
171+
172+
### Old DbRecord API (Deprecated)
173+
174+
```typescript
175+
// ❌ Old way with Proxy objects
176+
const user = db.createRecord('users');
177+
user.name = 'Alice';
178+
await user.save(); // Method on record
179+
180+
await user.delete(); // Method on record
181+
```
182+
183+
### New Plain Objects API
184+
185+
```typescript
186+
// ✅ New way with plain objects
187+
const user = db.createRecord('users');
188+
user.name = 'Alice';
189+
await db.save(user); // Method on database
190+
191+
await db.deleteRecord(user); // Method on database
192+
```
193+
194+
## Best Practices
195+
196+
### 1. Always use db.save() for modifications
197+
198+
```typescript
199+
const user = await db.queryOne('users', { where: 'id = ?', whereArgs: ['user-1'] });
200+
user.age = 31;
201+
await db.save(user); // Automatically determines INSERT vs UPDATE
202+
```
203+
204+
### 2. Use streaming queries for reactive UI
205+
206+
```typescript
207+
const users$ = db.stream('users');
208+
users$.subscribe(users => updateUI(users));
209+
```
210+
211+
### 3. Leverage xRec for optimistic updates
212+
213+
```typescript
214+
const user = await db.queryOne('users', { where: 'id = ?', whereArgs: ['user-1'] });
215+
const originalName = user.xRec.name;
216+
217+
user.name = 'New Name';
218+
try {
219+
await db.save(user);
220+
} catch (error) {
221+
// Rollback to original
222+
user.name = originalName;
223+
}
224+
```
225+
226+
### 4. Records work across Comlink automatically
227+
228+
```typescript
229+
// No special serialization needed!
230+
const users = await workerDb.query('users');
231+
users[0].age = 32;
232+
await workerDb.save(users[0]); // Just works!
233+
```
234+
235+
## Summary
236+
237+
The new plain objects API provides:
238+
239+
-**Simplicity**: No Proxy magic, just plain objects
240+
-**Comlink Compatible**: Serializes automatically through workers
241+
-**Change Tracking**: xRec property for detecting modifications
242+
-**Type Safe**: Full TypeScript support
243+
-**HLC Integration**: Automatic timestamp management
244+
-**Streaming Support**: RxJS observables for reactive updates
245+
246+
Perfect for offline-first PWAs with web worker databases!

packages/core/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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.16.0",
3+
"version": "2.0.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",

0 commit comments

Comments
 (0)