@delali/sirannon-db 0.1.3 → 0.1.4
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 +276 -77
- package/dist/backup-scheduler/index.d.ts +3 -0
- package/dist/backup-scheduler/index.mjs +2 -0
- package/dist/chunk-74UN4DIE.mjs +14 -0
- package/dist/{chunk-VI4UP4RR.mjs → chunk-AX66KWBR.mjs} +74 -139
- package/dist/chunk-FB2U2Q3Y.mjs +21 -0
- package/dist/chunk-O7BHI3CF.mjs +90 -0
- package/dist/chunk-PXKAKK2V.mjs +124 -0
- package/dist/client/index.d.ts +38 -2
- package/dist/core/index.d.ts +30 -142
- package/dist/core/index.mjs +229 -469
- package/dist/driver/better-sqlite3.d.ts +8 -0
- package/dist/driver/better-sqlite3.mjs +63 -0
- package/dist/driver/bun.mjs +61 -0
- package/dist/driver/expo.mjs +55 -0
- package/dist/driver/node.d.ts +8 -0
- package/dist/driver/node.mjs +60 -0
- package/dist/driver/wa-sqlite.d.ts +34 -0
- package/dist/driver/wa-sqlite.mjs +141 -0
- package/dist/file-migrations/index.d.ts +16 -0
- package/dist/file-migrations/index.mjs +128 -0
- package/dist/index-hXiis3N-.d.ts +16 -0
- package/dist/server/index.d.ts +110 -54
- package/dist/server/index.mjs +107 -92
- package/dist/{sirannon-BJ8Yd1Uf.d.ts → sirannon-B1oTfebD.d.ts} +30 -58
- package/dist/types-BFSsG77t.d.ts +29 -0
- package/dist/types-DRkJlqex.d.ts +38 -0
- package/dist/{types-DArCObcu.d.ts → types-DtDutWRU.d.ts} +4 -1
- package/dist/vfs-INWQ5DTE.mjs +2 -0
- package/package.json +58 -7
- package/dist/protocol-BX1H-_Mz.d.ts +0 -104
package/README.md
CHANGED
|
@@ -16,29 +16,146 @@ Turn any SQLite database into a networked data layer with real-time subscription
|
|
|
16
16
|
pnpm add @delali/sirannon-db
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Then install the SQLite driver for your platform:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pnpm add better-sqlite3 # Node.js
|
|
23
|
+
pnpm add wa-sqlite # Browser (IndexedDB persistence)
|
|
24
|
+
pnpm add expo-sqlite # React Native (Expo)
|
|
25
|
+
# Node 22+ built-in sqlite and Bun need no extra package
|
|
26
|
+
```
|
|
20
27
|
|
|
21
28
|
## Quick start
|
|
22
29
|
|
|
30
|
+
### Node.js
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pnpm add @delali/sirannon-db better-sqlite3
|
|
34
|
+
```
|
|
35
|
+
|
|
23
36
|
```ts
|
|
24
37
|
import { Sirannon } from '@delali/sirannon-db'
|
|
38
|
+
import { betterSqlite3 } from '@delali/sirannon-db/driver/better-sqlite3'
|
|
39
|
+
|
|
40
|
+
const driver = betterSqlite3()
|
|
41
|
+
const sirannon = new Sirannon({ driver })
|
|
42
|
+
const db = await sirannon.open('app', './data/app.db')
|
|
43
|
+
|
|
44
|
+
await db.execute('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)')
|
|
45
|
+
await db.execute('INSERT INTO users (name, email) VALUES (?, ?)', ['Ada', 'ada@example.com'])
|
|
46
|
+
|
|
47
|
+
const users = await db.query<{ id: number; name: string }>('SELECT * FROM users')
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Node.js 22+ users can skip the extra dependency by using the built-in `node:sqlite` module (requires the `--experimental-sqlite` flag):
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { nodeSqlite } from '@delali/sirannon-db/driver/node'
|
|
54
|
+
|
|
55
|
+
const driver = nodeSqlite()
|
|
56
|
+
```
|
|
25
57
|
|
|
26
|
-
|
|
27
|
-
const db = sirannon.open('app', './data/app.db')
|
|
58
|
+
### Browser
|
|
28
59
|
|
|
29
|
-
|
|
30
|
-
|
|
60
|
+
```bash
|
|
61
|
+
pnpm add @delali/sirannon-db wa-sqlite
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The browser driver persists data to IndexedDB through a WebAssembly SQLite build. Use `Database.create` directly since `Sirannon` registries are designed for server-side use.
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { Database } from '@delali/sirannon-db'
|
|
68
|
+
import { waSqlite } from '@delali/sirannon-db/driver/wa-sqlite'
|
|
31
69
|
|
|
32
|
-
const
|
|
70
|
+
const driver = waSqlite({ vfs: 'IDBBatchAtomicVFS' })
|
|
71
|
+
const db = await Database.create('app', '/app.db', driver, {
|
|
72
|
+
readPoolSize: 1,
|
|
73
|
+
walMode: false,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
await db.execute('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)')
|
|
77
|
+
await db.execute('INSERT INTO users (name, email) VALUES (?, ?)', ['Ada', 'ada@example.com'])
|
|
78
|
+
|
|
79
|
+
const users = await db.query<{ id: number; name: string }>('SELECT * FROM users')
|
|
33
80
|
```
|
|
34
81
|
|
|
35
|
-
|
|
82
|
+
### React Native (Expo)
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pnpm add @delali/sirannon-db expo-sqlite
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { Sirannon } from '@delali/sirannon-db'
|
|
90
|
+
import { expoSqlite } from '@delali/sirannon-db/driver/expo'
|
|
91
|
+
|
|
92
|
+
const driver = expoSqlite()
|
|
93
|
+
const sirannon = new Sirannon({ driver })
|
|
94
|
+
const db = await sirannon.open('app', 'app.db', {
|
|
95
|
+
readPoolSize: 1,
|
|
96
|
+
})
|
|
36
97
|
|
|
37
|
-
|
|
98
|
+
await db.execute('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)')
|
|
99
|
+
await db.execute('INSERT INTO users (name, email) VALUES (?, ?)', ['Ada', 'ada@example.com'])
|
|
100
|
+
|
|
101
|
+
const users = await db.query<{ id: number; name: string }>('SELECT * FROM users')
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Bun
|
|
105
|
+
|
|
106
|
+
No extra dependency needed since Bun ships `bun:sqlite` built in.
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import { Sirannon } from '@delali/sirannon-db'
|
|
110
|
+
import { bunSqlite } from '@delali/sirannon-db/driver/bun'
|
|
111
|
+
|
|
112
|
+
const driver = bunSqlite()
|
|
113
|
+
const sirannon = new Sirannon({ driver })
|
|
114
|
+
const db = await sirannon.open('app', './data/app.db')
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Standalone databases
|
|
118
|
+
|
|
119
|
+
You can create databases without a `Sirannon` registry on any platform:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
const db = await Database.create('app', './data/app.db', driver)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Pluggable drivers
|
|
126
|
+
|
|
127
|
+
Sirannon-db separates the database engine from the library. You pick the driver that fits your runtime, and the rest of the API stays the same.
|
|
128
|
+
|
|
129
|
+
| Driver | Import | Runtime | Install |
|
|
130
|
+
| --- | --- | --- | --- |
|
|
131
|
+
| better-sqlite3 | `@delali/sirannon-db/driver/better-sqlite3` | Node.js | `pnpm add better-sqlite3` |
|
|
132
|
+
| Node built-in | `@delali/sirannon-db/driver/node` | Node.js >= 22 | None (use `--experimental-sqlite` flag) |
|
|
133
|
+
| wa-sqlite | `@delali/sirannon-db/driver/wa-sqlite` | Browser | `pnpm add wa-sqlite` |
|
|
134
|
+
| Bun | `@delali/sirannon-db/driver/bun` | Bun | None (uses `bun:sqlite`) |
|
|
135
|
+
| Expo | `@delali/sirannon-db/driver/expo` | React Native | `pnpm add expo-sqlite` |
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { betterSqlite3 } from '@delali/sirannon-db/driver/better-sqlite3'
|
|
139
|
+
const driver = betterSqlite3()
|
|
140
|
+
|
|
141
|
+
// or for Node 22's built-in sqlite:
|
|
142
|
+
import { nodeSqlite } from '@delali/sirannon-db/driver/node'
|
|
143
|
+
const driver = nodeSqlite()
|
|
144
|
+
|
|
145
|
+
// or for browser with IndexedDB persistence:
|
|
146
|
+
import { waSqlite } from '@delali/sirannon-db/driver/wa-sqlite'
|
|
147
|
+
const driver = waSqlite({ vfs: 'IDBBatchAtomicVFS' })
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Package exports
|
|
151
|
+
|
|
152
|
+
The package ships independent exports so you only bundle what you need:
|
|
38
153
|
|
|
39
154
|
| Import | What you get |
|
|
40
|
-
|
|
155
|
+
| --- | --- |
|
|
41
156
|
| `@delali/sirannon-db` | Core library: queries, transactions, CDC, migrations, backups, hooks, metrics, lifecycle |
|
|
157
|
+
| `@delali/sirannon-db/driver/*` | SQLite driver adapters (see table above) |
|
|
158
|
+
| `@delali/sirannon-db/file-migrations` | Load `.up.sql` / `.down.sql` files from a directory |
|
|
42
159
|
| `@delali/sirannon-db/server` | HTTP + WebSocket server powered by uWebSockets.js |
|
|
43
160
|
| `@delali/sirannon-db/client` | Browser/Node.js client SDK with auto-reconnect and subscription restore |
|
|
44
161
|
|
|
@@ -47,36 +164,34 @@ The package ships three independent exports so you only bundle what you need:
|
|
|
47
164
|
### Queries and transactions
|
|
48
165
|
|
|
49
166
|
```ts
|
|
50
|
-
const row = db.queryOne<{ count: number }>('SELECT count(*) as count FROM users')
|
|
167
|
+
const row = await db.queryOne<{ count: number }>('SELECT count(*) as count FROM users')
|
|
51
168
|
|
|
52
|
-
const result = db.execute(
|
|
169
|
+
const result = await db.execute(
|
|
53
170
|
'INSERT INTO users (name, email) VALUES (?, ?)',
|
|
54
171
|
['Grace', 'grace@example.com'],
|
|
55
172
|
)
|
|
56
173
|
// result.changes === 1, result.lastInsertRowId === 2
|
|
57
174
|
|
|
58
|
-
db.executeBatch('INSERT INTO tags (label) VALUES (?)', [
|
|
175
|
+
await db.executeBatch('INSERT INTO tags (label) VALUES (?)', [
|
|
59
176
|
['typescript'],
|
|
60
177
|
['sqlite'],
|
|
61
178
|
['realtime'],
|
|
62
179
|
])
|
|
63
180
|
|
|
64
|
-
const total = db.transaction(tx => {
|
|
65
|
-
tx.execute('UPDATE accounts SET balance = balance - 100 WHERE id = ?', [1])
|
|
66
|
-
tx.execute('UPDATE accounts SET balance = balance + 100 WHERE id = ?', [2])
|
|
67
|
-
const [row] = tx.query<{ balance: number }>('SELECT balance FROM accounts WHERE id = ?', [2])
|
|
181
|
+
const total = await db.transaction(async tx => {
|
|
182
|
+
await tx.execute('UPDATE accounts SET balance = balance - 100 WHERE id = ?', [1])
|
|
183
|
+
await tx.execute('UPDATE accounts SET balance = balance + 100 WHERE id = ?', [2])
|
|
184
|
+
const [row] = await tx.query<{ balance: number }>('SELECT balance FROM accounts WHERE id = ?', [2])
|
|
68
185
|
return row
|
|
69
186
|
})
|
|
70
187
|
```
|
|
71
188
|
|
|
72
|
-
Statements are cached in an LRU pool (capacity 128) so repeated queries skip the prepare step.
|
|
73
|
-
|
|
74
189
|
### Connection pooling
|
|
75
190
|
|
|
76
191
|
Every database opens with 1 dedicated write connection and N read connections (default 4). WAL mode is enabled by default, allowing concurrent reads during writes.
|
|
77
192
|
|
|
78
193
|
```ts
|
|
79
|
-
const db = sirannon.open('analytics', './data/analytics.db', {
|
|
194
|
+
const db = await sirannon.open('analytics', './data/analytics.db', {
|
|
80
195
|
readPoolSize: 8,
|
|
81
196
|
walMode: true,
|
|
82
197
|
})
|
|
@@ -87,7 +202,7 @@ const db = sirannon.open('analytics', './data/analytics.db', {
|
|
|
87
202
|
Watch tables for INSERT, UPDATE, and DELETE events in real time. The CDC system installs SQLite triggers that record changes into a tracking table, then polls at a configurable interval.
|
|
88
203
|
|
|
89
204
|
```ts
|
|
90
|
-
db.watch('orders')
|
|
205
|
+
await db.watch('orders')
|
|
91
206
|
|
|
92
207
|
const subscription = db
|
|
93
208
|
.on('orders')
|
|
@@ -104,7 +219,7 @@ const subscription = db
|
|
|
104
219
|
subscription.unsubscribe()
|
|
105
220
|
|
|
106
221
|
// Stop tracking entirely:
|
|
107
|
-
db.unwatch('orders')
|
|
222
|
+
await db.unwatch('orders')
|
|
108
223
|
```
|
|
109
224
|
|
|
110
225
|
### Migrations
|
|
@@ -131,7 +246,10 @@ migrations/
|
|
|
131
246
|
#### File-based migrations
|
|
132
247
|
|
|
133
248
|
```ts
|
|
134
|
-
|
|
249
|
+
import { loadMigrations } from '@delali/sirannon-db/file-migrations'
|
|
250
|
+
|
|
251
|
+
const migrations = loadMigrations('./migrations')
|
|
252
|
+
const result = await db.migrate(migrations)
|
|
135
253
|
// result.applied: entries that ran this time
|
|
136
254
|
// result.skipped: number of entries already applied
|
|
137
255
|
```
|
|
@@ -139,14 +257,15 @@ const result = db.migrate('./migrations')
|
|
|
139
257
|
#### Rollback
|
|
140
258
|
|
|
141
259
|
```ts
|
|
142
|
-
|
|
143
|
-
db.rollback(
|
|
144
|
-
db.rollback(
|
|
260
|
+
const migrations = loadMigrations('./migrations')
|
|
261
|
+
await db.rollback(migrations) // undo the last applied migration
|
|
262
|
+
await db.rollback(migrations, 2) // undo all migrations after version 2
|
|
263
|
+
await db.rollback(migrations, 0) // undo everything
|
|
145
264
|
```
|
|
146
265
|
|
|
147
266
|
#### Programmatic migrations
|
|
148
267
|
|
|
149
|
-
Pass an array of migration objects instead of
|
|
268
|
+
Pass an array of migration objects instead of loading from files:
|
|
150
269
|
|
|
151
270
|
```ts
|
|
152
271
|
const migrations = [
|
|
@@ -156,22 +275,11 @@ const migrations = [
|
|
|
156
275
|
up: 'CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)',
|
|
157
276
|
down: 'DROP TABLE users',
|
|
158
277
|
},
|
|
159
|
-
{
|
|
160
|
-
version: 2,
|
|
161
|
-
name: 'seed_data',
|
|
162
|
-
up: (tx) => {
|
|
163
|
-
// You can run any code here
|
|
164
|
-
tx.execute("INSERT INTO users (name) VALUES (?)", ['Alice'])
|
|
165
|
-
},
|
|
166
|
-
down: (tx) => {
|
|
167
|
-
tx.execute("DELETE FROM users WHERE name = ?", ['Alice'])
|
|
168
|
-
},
|
|
169
|
-
},
|
|
170
278
|
]
|
|
171
279
|
|
|
172
|
-
db.migrate(migrations)
|
|
173
|
-
db.rollback(migrations) // undo last migration
|
|
174
|
-
db.rollback(migrations, 0) // undo everything
|
|
280
|
+
await db.migrate(migrations)
|
|
281
|
+
await db.rollback(migrations) // undo last migration
|
|
282
|
+
await db.rollback(migrations, 0) // undo everything
|
|
175
283
|
```
|
|
176
284
|
|
|
177
285
|
### Backups
|
|
@@ -179,7 +287,7 @@ db.rollback(migrations, 0) // undo everything
|
|
|
179
287
|
One-shot backups use `VACUUM INTO` for a consistent snapshot. Scheduled backups run on a cron expression with automatic file rotation.
|
|
180
288
|
|
|
181
289
|
```ts
|
|
182
|
-
db.backup('./backups/snapshot.db')
|
|
290
|
+
await db.backup('./backups/snapshot.db')
|
|
183
291
|
|
|
184
292
|
db.scheduleBackup({
|
|
185
293
|
cron: '0 */6 * * *', // every 6 hours
|
|
@@ -219,6 +327,7 @@ Plug in callbacks to collect query timing, connection events, and CDC activity.
|
|
|
219
327
|
|
|
220
328
|
```ts
|
|
221
329
|
const sirannon = new Sirannon({
|
|
330
|
+
driver,
|
|
222
331
|
metrics: {
|
|
223
332
|
onQueryComplete: m => histogram.observe(m.durationMs),
|
|
224
333
|
onConnectionOpen: m => gauge.inc({ db: m.databaseId }),
|
|
@@ -234,6 +343,7 @@ For multi-tenant setups, the lifecycle manager handles auto-opening, idle timeou
|
|
|
234
343
|
|
|
235
344
|
```ts
|
|
236
345
|
const sirannon = new Sirannon({
|
|
346
|
+
driver,
|
|
237
347
|
lifecycle: {
|
|
238
348
|
autoOpen: {
|
|
239
349
|
resolver: id => ({ path: `/data/tenants/${id}.db` }),
|
|
@@ -244,7 +354,7 @@ const sirannon = new Sirannon({
|
|
|
244
354
|
})
|
|
245
355
|
|
|
246
356
|
// Databases resolve on first access:
|
|
247
|
-
const db = sirannon.
|
|
357
|
+
const db = await sirannon.resolve('tenant-42') // opens /data/tenants/tenant-42.db
|
|
248
358
|
```
|
|
249
359
|
|
|
250
360
|
## Server
|
|
@@ -253,32 +363,23 @@ Expose any `Sirannon` instance over HTTP and WebSocket with a single function ca
|
|
|
253
363
|
|
|
254
364
|
```ts
|
|
255
365
|
import { Sirannon } from '@delali/sirannon-db'
|
|
366
|
+
import { betterSqlite3 } from '@delali/sirannon-db/driver/better-sqlite3'
|
|
256
367
|
import { createServer } from '@delali/sirannon-db/server'
|
|
257
368
|
|
|
258
|
-
const
|
|
259
|
-
sirannon
|
|
260
|
-
|
|
261
|
-
const server = createServer(sirannon, {
|
|
262
|
-
port: 9876,
|
|
263
|
-
cors: true,
|
|
264
|
-
onRequest: ({ headers, path, method, remoteAddress }) => {
|
|
265
|
-
if (headers.authorization !== `Bearer ${process.env.API_TOKEN}`) {
|
|
266
|
-
return { status: 401, code: 'UNAUTHORIZED', message: 'Invalid or missing token' }
|
|
267
|
-
}
|
|
268
|
-
},
|
|
269
|
-
})
|
|
369
|
+
const driver = betterSqlite3()
|
|
370
|
+
const sirannon = new Sirannon({ driver })
|
|
371
|
+
await sirannon.open('app', './data/app.db')
|
|
270
372
|
|
|
373
|
+
const server = createServer(sirannon, { port: 9876 })
|
|
271
374
|
await server.listen()
|
|
272
375
|
```
|
|
273
376
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
**Important:** The server accepts arbitrary SQL from clients. When exposed beyond localhost, always use `onRequest` to authenticate and authorize requests.
|
|
377
|
+
See the [Security](#security) section for authentication, TLS, and CORS configuration.
|
|
277
378
|
|
|
278
379
|
### HTTP routes
|
|
279
380
|
|
|
280
381
|
| Method | Path | Description |
|
|
281
|
-
|
|
382
|
+
| --- | --- | --- |
|
|
282
383
|
| `POST` | `/db/:id/query` | Execute a SELECT, returns `{ rows }` |
|
|
283
384
|
| `POST` | `/db/:id/execute` | Execute a mutation, returns `{ changes, lastInsertRowId }` |
|
|
284
385
|
| `POST` | `/db/:id/transaction` | Execute a batch of statements atomically, returns `{ results }` |
|
|
@@ -308,17 +409,16 @@ const users = await db.query<{ id: number; name: string }>('SELECT * FROM users'
|
|
|
308
409
|
|
|
309
410
|
await db.execute('INSERT INTO users (name) VALUES (?)', ['Turing'])
|
|
310
411
|
|
|
311
|
-
const sub =
|
|
312
|
-
.
|
|
313
|
-
|
|
314
|
-
.subscribe(event => console.log('Admin changed:', event))
|
|
412
|
+
const sub = db.subscribe('users', event => {
|
|
413
|
+
console.log('User changed:', event)
|
|
414
|
+
})
|
|
315
415
|
|
|
316
416
|
// Cleanup:
|
|
317
417
|
sub.unsubscribe()
|
|
318
418
|
client.close()
|
|
319
419
|
```
|
|
320
420
|
|
|
321
|
-
Transactions
|
|
421
|
+
Transactions use the HTTP transport:
|
|
322
422
|
|
|
323
423
|
```ts
|
|
324
424
|
const httpClient = new SirannonClient('http://localhost:9876', {
|
|
@@ -335,12 +435,70 @@ await httpDb.transaction([
|
|
|
335
435
|
httpClient.close()
|
|
336
436
|
```
|
|
337
437
|
|
|
438
|
+
## Security
|
|
439
|
+
|
|
440
|
+
Sirannon-db is designed to be secure by default in its core operations. This section covers what the library handles for you and what you're responsible for when deploying to production.
|
|
441
|
+
|
|
442
|
+
### Built-in protections
|
|
443
|
+
|
|
444
|
+
- **Parameterized queries** - All SQL execution uses parameter binding through the driver layer, preventing SQL injection. User input never touches query strings directly.
|
|
445
|
+
- **Identifier validation** - CDC table and column names are validated against a strict allowlist regex (`/^[a-zA-Z_][a-zA-Z0-9_]*$/`), and identifiers are escaped with double-quote wrapping.
|
|
446
|
+
- **Path traversal prevention** - Migration and backup paths reject null bytes, `..` segments, and control characters before any filesystem access.
|
|
447
|
+
- **Request size limits** - HTTP bodies and WebSocket payloads are capped at 1 MB, preventing memory exhaustion from oversized requests.
|
|
448
|
+
- **Error isolation** - Errors returned to clients contain a machine-readable code and message. Stack traces and internal details are never leaked.
|
|
449
|
+
- **Connection isolation** - Read and write operations use separate connection pools. Read-only databases enforce immutability at the connection level.
|
|
450
|
+
|
|
451
|
+
### Authentication and authorization
|
|
452
|
+
|
|
453
|
+
The server accepts arbitrary SQL from clients. When you expose it beyond localhost, always use the `onRequest` hook to authenticate and authorize requests.
|
|
454
|
+
|
|
455
|
+
```ts
|
|
456
|
+
const server = createServer(sirannon, {
|
|
457
|
+
port: 9876,
|
|
458
|
+
onRequest: ({ headers, path, method, remoteAddress }) => {
|
|
459
|
+
if (headers.authorization !== `Bearer ${process.env.API_TOKEN}`) {
|
|
460
|
+
return { status: 401, code: 'UNAUTHORIZED', message: 'Invalid or missing token' }
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
})
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
The hook runs before every database route (HTTP and WebSocket upgrade). Return `void` to allow the request, or return a `{ status, code, message }` object to deny it. Health endpoints (`/health`, `/health/ready`) bypass this hook.
|
|
467
|
+
|
|
468
|
+
### TLS and transport security
|
|
469
|
+
|
|
470
|
+
> **Warning:** The built-in server binds plain HTTP and WebSocket without TLS. When you serve traffic outside a trusted network, terminate TLS upstream with a reverse proxy (nginx, Caddy, a cloud load balancer) or your clients' bearer tokens and query payloads will travel in cleartext.
|
|
471
|
+
|
|
472
|
+
Once TLS is in place, update your client URLs to use `https://` and `wss://`:
|
|
473
|
+
|
|
474
|
+
```ts
|
|
475
|
+
const client = new SirannonClient('https://db.example.com', {
|
|
476
|
+
transport: 'websocket',
|
|
477
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
478
|
+
})
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### CORS
|
|
482
|
+
|
|
483
|
+
CORS is disabled by default. Enable it only if browser clients need direct access, and restrict origins to trusted domains:
|
|
484
|
+
|
|
485
|
+
```ts
|
|
486
|
+
const server = createServer(sirannon, {
|
|
487
|
+
port: 9876,
|
|
488
|
+
cors: {
|
|
489
|
+
origin: ['https://app.example.com'],
|
|
490
|
+
},
|
|
491
|
+
})
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
Passing `cors: true` allows all origins, which is fine for local development but should be avoided in production.
|
|
495
|
+
|
|
338
496
|
## Error handling
|
|
339
497
|
|
|
340
498
|
All errors extend `SirannonError` with a machine-readable `code` property:
|
|
341
499
|
|
|
342
500
|
| Error | Code | When |
|
|
343
|
-
|
|
501
|
+
| --- | --- | --- |
|
|
344
502
|
| `DatabaseNotFoundError` | `DATABASE_NOT_FOUND` | Database ID not in registry |
|
|
345
503
|
| `DatabaseAlreadyExistsError` | `DATABASE_ALREADY_EXISTS` | Duplicate database ID |
|
|
346
504
|
| `ReadOnlyError` | `READ_ONLY` | Write attempted on read-only database |
|
|
@@ -358,7 +516,7 @@ All errors extend `SirannonError` with a machine-readable `code` property:
|
|
|
358
516
|
import { QueryError } from '@delali/sirannon-db'
|
|
359
517
|
|
|
360
518
|
try {
|
|
361
|
-
db.execute('INSERT INTO users (id) VALUES (?)', [1])
|
|
519
|
+
await db.execute('INSERT INTO users (id) VALUES (?)', [1])
|
|
362
520
|
} catch (err) {
|
|
363
521
|
if (err instanceof QueryError) {
|
|
364
522
|
console.error(`SQL failed [${err.code}]: ${err.message}`)
|
|
@@ -369,28 +527,29 @@ try {
|
|
|
369
527
|
|
|
370
528
|
## Configuration reference
|
|
371
529
|
|
|
530
|
+
### `SirannonOptions`
|
|
531
|
+
|
|
532
|
+
| Option | Type | Required | Description |
|
|
533
|
+
| --- | --- | --- | --- |
|
|
534
|
+
| `driver` | `SQLiteDriver` | Yes | The SQLite driver adapter to use |
|
|
535
|
+
| `hooks` | `HookConfig` | No | Before/after hooks for queries, connections, subscriptions |
|
|
536
|
+
| `metrics` | `MetricsConfig` | No | Callbacks for query timing, connection events, CDC activity |
|
|
537
|
+
| `lifecycle` | `LifecycleConfig` | No | Auto-open resolver, idle timeout, max open databases |
|
|
538
|
+
|
|
372
539
|
### `DatabaseOptions`
|
|
373
540
|
|
|
374
541
|
| Option | Type | Default | Description |
|
|
375
|
-
|
|
542
|
+
| --- | --- | --- | --- |
|
|
376
543
|
| `readOnly` | `boolean` | `false` | Open in read-only mode |
|
|
377
544
|
| `readPoolSize` | `number` | `4` | Number of read connections |
|
|
378
545
|
| `walMode` | `boolean` | `true` | Enable WAL mode |
|
|
379
546
|
| `cdcPollInterval` | `number` | `50` | CDC polling interval in ms |
|
|
380
547
|
| `cdcRetention` | `number` | `3_600_000` | CDC retention period in ms (1 hour) |
|
|
381
548
|
|
|
382
|
-
### `SirannonOptions`
|
|
383
|
-
|
|
384
|
-
| Option | Type | Description |
|
|
385
|
-
|---|---|---|
|
|
386
|
-
| `hooks` | `HookConfig` | Before/after hooks for queries, connections, subscriptions |
|
|
387
|
-
| `metrics` | `MetricsConfig` | Callbacks for query timing, connection events, CDC activity |
|
|
388
|
-
| `lifecycle` | `LifecycleConfig` | Auto-open resolver, idle timeout, max open databases |
|
|
389
|
-
|
|
390
549
|
### `ServerOptions`
|
|
391
550
|
|
|
392
551
|
| Option | Type | Default | Description |
|
|
393
|
-
|
|
552
|
+
| --- | --- | --- | --- |
|
|
394
553
|
| `host` | `string` | `'127.0.0.1'` | Bind address |
|
|
395
554
|
| `port` | `number` | `9876` | Listen port |
|
|
396
555
|
| `cors` | `boolean \| CorsOptions` | `false` | CORS configuration |
|
|
@@ -399,15 +558,55 @@ try {
|
|
|
399
558
|
### `ClientOptions`
|
|
400
559
|
|
|
401
560
|
| Option | Type | Default | Description |
|
|
402
|
-
|
|
561
|
+
| --- | --- | --- | --- |
|
|
403
562
|
| `transport` | `'websocket' \| 'http'` | `'websocket'` | Transport protocol |
|
|
404
563
|
| `headers` | `Record<string, string>` | - | Custom HTTP headers |
|
|
405
564
|
| `autoReconnect` | `boolean` | `true` | Reconnect on WebSocket disconnect |
|
|
406
565
|
| `reconnectInterval` | `number` | `1000` | Reconnect delay in ms |
|
|
407
566
|
|
|
567
|
+
## Examples
|
|
568
|
+
|
|
569
|
+
Self-contained example projects live in [`examples/`](examples/) and cover every runtime target:
|
|
570
|
+
|
|
571
|
+
| Example | Runtime | Driver | What it demonstrates |
|
|
572
|
+
| --- | --- | --- | --- |
|
|
573
|
+
| [`node-better-sqlite3`](examples/node-better-sqlite3/) | Node.js | better-sqlite3 | All core features: schema, migrations, CRUD, transactions, CDC, connection pools, metrics, multi-tenant, hooks, backup, shutdown |
|
|
574
|
+
| [`node-native`](examples/node-native/) | Node.js >= 22 | built-in `node:sqlite` | Same features as above using the zero-dependency Node driver |
|
|
575
|
+
| [`web-wa-sqlite`](examples/web-wa-sqlite/) | Browser (Vite) | wa-sqlite + IndexedDB | CRUD, transactions, CDC subscriptions in the browser |
|
|
576
|
+
| [`web-client`](examples/web-client/) | Browser + Node.js | better-sqlite3 (server) | Client SDK connecting to a Sirannon server over HTTP and WebSocket |
|
|
577
|
+
|
|
578
|
+
### Running the examples
|
|
579
|
+
|
|
580
|
+
From the repository root:
|
|
581
|
+
|
|
582
|
+
```bash
|
|
583
|
+
pnpm install
|
|
584
|
+
pnpm --filter @delali/sirannon-db build
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
Then pick an example:
|
|
588
|
+
|
|
589
|
+
```bash
|
|
590
|
+
# Node.js with better-sqlite3
|
|
591
|
+
cd packages/ts/examples/node-better-sqlite3
|
|
592
|
+
pnpm start
|
|
593
|
+
|
|
594
|
+
# Node.js with built-in sqlite
|
|
595
|
+
cd packages/ts/examples/node-native
|
|
596
|
+
pnpm start
|
|
597
|
+
|
|
598
|
+
# Browser with wa-sqlite (opens Vite dev server)
|
|
599
|
+
cd packages/ts/examples/web-wa-sqlite
|
|
600
|
+
pnpm dev
|
|
601
|
+
|
|
602
|
+
# Client-server (starts both Sirannon server and Vite client)
|
|
603
|
+
cd packages/ts/examples/web-client
|
|
604
|
+
pnpm start
|
|
605
|
+
```
|
|
606
|
+
|
|
408
607
|
## Benchmarks
|
|
409
608
|
|
|
410
|
-
The benchmark suite compares Sirannon's embedded SQLite performance against Postgres 17 across micro-operations, YCSB, TPC-C, and concurrency scaling. See [`
|
|
609
|
+
The benchmark suite compares Sirannon's embedded SQLite performance against Postgres 17 across micro-operations, YCSB, TPC-C, and concurrency scaling. All benchmarks support driver switching via the `BENCH_DRIVER` environment variable (`better-sqlite3` or `node`). See [`benchmarks/BENCHMARKS.md`](benchmarks/BENCHMARKS.md) for setup instructions, configuration, Docker-based fair comparisons, and statistical analysis methodology.
|
|
411
610
|
|
|
412
611
|
## Development
|
|
413
612
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { SirannonError } from './chunk-O7BHI3CF.mjs';
|
|
2
|
+
|
|
3
|
+
// src/core/driver/define.ts
|
|
4
|
+
function defineDriver(config) {
|
|
5
|
+
if (!config.capabilities || typeof config.open !== "function") {
|
|
6
|
+
throw new SirannonError("Driver must define capabilities and open()", "INVALID_DRIVER");
|
|
7
|
+
}
|
|
8
|
+
return Object.freeze({
|
|
9
|
+
capabilities: Object.freeze({ ...config.capabilities }),
|
|
10
|
+
open: config.open
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export { defineDriver };
|