@delali/sirannon-db 0.1.1 → 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 +282 -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 +78 -27
- package/dist/protocol-BX1H-_Mz.d.ts +0 -104
package/README.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# sirannon-db
|
|
2
2
|
|
|
3
|
+
[](https://github.com/assetcorp/sirannon-db/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@delali/sirannon-db)
|
|
5
|
+
[](https://www.npmjs.com/package/@delali/sirannon-db)
|
|
6
|
+
[](https://www.npmjs.com/package/@delali/sirannon-db)
|
|
7
|
+
[](https://github.com/assetcorp/sirannon-db/blob/main/LICENSE)
|
|
8
|
+
|
|
3
9
|
Turn any SQLite database into a networked data layer with real-time subscriptions. One library gives you connection pooling, change data capture, migrations, scheduled backups, and a client SDK that talks over HTTP or WebSocket.
|
|
4
10
|
|
|
5
11
|
> *sirannon* means 'gate-stream' in Sindarin.
|
|
@@ -10,29 +16,146 @@ Turn any SQLite database into a networked data layer with real-time subscription
|
|
|
10
16
|
pnpm add @delali/sirannon-db
|
|
11
17
|
```
|
|
12
18
|
|
|
13
|
-
|
|
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
|
+
```
|
|
14
27
|
|
|
15
28
|
## Quick start
|
|
16
29
|
|
|
30
|
+
### Node.js
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pnpm add @delali/sirannon-db better-sqlite3
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```ts
|
|
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
|
+
```
|
|
57
|
+
|
|
58
|
+
### Browser
|
|
59
|
+
|
|
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'
|
|
69
|
+
|
|
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')
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### React Native (Expo)
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pnpm add @delali/sirannon-db expo-sqlite
|
|
86
|
+
```
|
|
87
|
+
|
|
17
88
|
```ts
|
|
18
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
|
+
})
|
|
97
|
+
|
|
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
|
+
```
|
|
19
116
|
|
|
20
|
-
|
|
21
|
-
const db = sirannon.open('app', './data/app.db')
|
|
117
|
+
### Standalone databases
|
|
22
118
|
|
|
23
|
-
|
|
24
|
-
db.execute('INSERT INTO users (name, email) VALUES (?, ?)', ['Ada', 'ada@example.com'])
|
|
119
|
+
You can create databases without a `Sirannon` registry on any platform:
|
|
25
120
|
|
|
26
|
-
|
|
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' })
|
|
27
148
|
```
|
|
28
149
|
|
|
29
|
-
##
|
|
150
|
+
## Package exports
|
|
30
151
|
|
|
31
|
-
The package ships
|
|
152
|
+
The package ships independent exports so you only bundle what you need:
|
|
32
153
|
|
|
33
154
|
| Import | What you get |
|
|
34
|
-
|
|
155
|
+
| --- | --- |
|
|
35
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 |
|
|
36
159
|
| `@delali/sirannon-db/server` | HTTP + WebSocket server powered by uWebSockets.js |
|
|
37
160
|
| `@delali/sirannon-db/client` | Browser/Node.js client SDK with auto-reconnect and subscription restore |
|
|
38
161
|
|
|
@@ -41,36 +164,34 @@ The package ships three independent exports so you only bundle what you need:
|
|
|
41
164
|
### Queries and transactions
|
|
42
165
|
|
|
43
166
|
```ts
|
|
44
|
-
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')
|
|
45
168
|
|
|
46
|
-
const result = db.execute(
|
|
169
|
+
const result = await db.execute(
|
|
47
170
|
'INSERT INTO users (name, email) VALUES (?, ?)',
|
|
48
171
|
['Grace', 'grace@example.com'],
|
|
49
172
|
)
|
|
50
173
|
// result.changes === 1, result.lastInsertRowId === 2
|
|
51
174
|
|
|
52
|
-
db.executeBatch('INSERT INTO tags (label) VALUES (?)', [
|
|
175
|
+
await db.executeBatch('INSERT INTO tags (label) VALUES (?)', [
|
|
53
176
|
['typescript'],
|
|
54
177
|
['sqlite'],
|
|
55
178
|
['realtime'],
|
|
56
179
|
])
|
|
57
180
|
|
|
58
|
-
const total = db.transaction(tx => {
|
|
59
|
-
tx.execute('UPDATE accounts SET balance = balance - 100 WHERE id = ?', [1])
|
|
60
|
-
tx.execute('UPDATE accounts SET balance = balance + 100 WHERE id = ?', [2])
|
|
61
|
-
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])
|
|
62
185
|
return row
|
|
63
186
|
})
|
|
64
187
|
```
|
|
65
188
|
|
|
66
|
-
Statements are cached in an LRU pool (capacity 128) so repeated queries skip the prepare step.
|
|
67
|
-
|
|
68
189
|
### Connection pooling
|
|
69
190
|
|
|
70
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.
|
|
71
192
|
|
|
72
193
|
```ts
|
|
73
|
-
const db = sirannon.open('analytics', './data/analytics.db', {
|
|
194
|
+
const db = await sirannon.open('analytics', './data/analytics.db', {
|
|
74
195
|
readPoolSize: 8,
|
|
75
196
|
walMode: true,
|
|
76
197
|
})
|
|
@@ -81,7 +202,7 @@ const db = sirannon.open('analytics', './data/analytics.db', {
|
|
|
81
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.
|
|
82
203
|
|
|
83
204
|
```ts
|
|
84
|
-
db.watch('orders')
|
|
205
|
+
await db.watch('orders')
|
|
85
206
|
|
|
86
207
|
const subscription = db
|
|
87
208
|
.on('orders')
|
|
@@ -98,7 +219,7 @@ const subscription = db
|
|
|
98
219
|
subscription.unsubscribe()
|
|
99
220
|
|
|
100
221
|
// Stop tracking entirely:
|
|
101
|
-
db.unwatch('orders')
|
|
222
|
+
await db.unwatch('orders')
|
|
102
223
|
```
|
|
103
224
|
|
|
104
225
|
### Migrations
|
|
@@ -125,7 +246,10 @@ migrations/
|
|
|
125
246
|
#### File-based migrations
|
|
126
247
|
|
|
127
248
|
```ts
|
|
128
|
-
|
|
249
|
+
import { loadMigrations } from '@delali/sirannon-db/file-migrations'
|
|
250
|
+
|
|
251
|
+
const migrations = loadMigrations('./migrations')
|
|
252
|
+
const result = await db.migrate(migrations)
|
|
129
253
|
// result.applied: entries that ran this time
|
|
130
254
|
// result.skipped: number of entries already applied
|
|
131
255
|
```
|
|
@@ -133,14 +257,15 @@ const result = db.migrate('./migrations')
|
|
|
133
257
|
#### Rollback
|
|
134
258
|
|
|
135
259
|
```ts
|
|
136
|
-
|
|
137
|
-
db.rollback(
|
|
138
|
-
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
|
|
139
264
|
```
|
|
140
265
|
|
|
141
266
|
#### Programmatic migrations
|
|
142
267
|
|
|
143
|
-
Pass an array of migration objects instead of
|
|
268
|
+
Pass an array of migration objects instead of loading from files:
|
|
144
269
|
|
|
145
270
|
```ts
|
|
146
271
|
const migrations = [
|
|
@@ -150,22 +275,11 @@ const migrations = [
|
|
|
150
275
|
up: 'CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)',
|
|
151
276
|
down: 'DROP TABLE users',
|
|
152
277
|
},
|
|
153
|
-
{
|
|
154
|
-
version: 2,
|
|
155
|
-
name: 'seed_data',
|
|
156
|
-
up: (tx) => {
|
|
157
|
-
// You can run any code here
|
|
158
|
-
tx.execute("INSERT INTO users (name) VALUES (?)", ['Alice'])
|
|
159
|
-
},
|
|
160
|
-
down: (tx) => {
|
|
161
|
-
tx.execute("DELETE FROM users WHERE name = ?", ['Alice'])
|
|
162
|
-
},
|
|
163
|
-
},
|
|
164
278
|
]
|
|
165
279
|
|
|
166
|
-
db.migrate(migrations)
|
|
167
|
-
db.rollback(migrations) // undo last migration
|
|
168
|
-
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
|
|
169
283
|
```
|
|
170
284
|
|
|
171
285
|
### Backups
|
|
@@ -173,7 +287,7 @@ db.rollback(migrations, 0) // undo everything
|
|
|
173
287
|
One-shot backups use `VACUUM INTO` for a consistent snapshot. Scheduled backups run on a cron expression with automatic file rotation.
|
|
174
288
|
|
|
175
289
|
```ts
|
|
176
|
-
db.backup('./backups/snapshot.db')
|
|
290
|
+
await db.backup('./backups/snapshot.db')
|
|
177
291
|
|
|
178
292
|
db.scheduleBackup({
|
|
179
293
|
cron: '0 */6 * * *', // every 6 hours
|
|
@@ -213,6 +327,7 @@ Plug in callbacks to collect query timing, connection events, and CDC activity.
|
|
|
213
327
|
|
|
214
328
|
```ts
|
|
215
329
|
const sirannon = new Sirannon({
|
|
330
|
+
driver,
|
|
216
331
|
metrics: {
|
|
217
332
|
onQueryComplete: m => histogram.observe(m.durationMs),
|
|
218
333
|
onConnectionOpen: m => gauge.inc({ db: m.databaseId }),
|
|
@@ -228,6 +343,7 @@ For multi-tenant setups, the lifecycle manager handles auto-opening, idle timeou
|
|
|
228
343
|
|
|
229
344
|
```ts
|
|
230
345
|
const sirannon = new Sirannon({
|
|
346
|
+
driver,
|
|
231
347
|
lifecycle: {
|
|
232
348
|
autoOpen: {
|
|
233
349
|
resolver: id => ({ path: `/data/tenants/${id}.db` }),
|
|
@@ -238,7 +354,7 @@ const sirannon = new Sirannon({
|
|
|
238
354
|
})
|
|
239
355
|
|
|
240
356
|
// Databases resolve on first access:
|
|
241
|
-
const db = sirannon.
|
|
357
|
+
const db = await sirannon.resolve('tenant-42') // opens /data/tenants/tenant-42.db
|
|
242
358
|
```
|
|
243
359
|
|
|
244
360
|
## Server
|
|
@@ -247,32 +363,23 @@ Expose any `Sirannon` instance over HTTP and WebSocket with a single function ca
|
|
|
247
363
|
|
|
248
364
|
```ts
|
|
249
365
|
import { Sirannon } from '@delali/sirannon-db'
|
|
366
|
+
import { betterSqlite3 } from '@delali/sirannon-db/driver/better-sqlite3'
|
|
250
367
|
import { createServer } from '@delali/sirannon-db/server'
|
|
251
368
|
|
|
252
|
-
const
|
|
253
|
-
sirannon
|
|
254
|
-
|
|
255
|
-
const server = createServer(sirannon, {
|
|
256
|
-
port: 9876,
|
|
257
|
-
cors: true,
|
|
258
|
-
onRequest: ({ headers, path, method, remoteAddress }) => {
|
|
259
|
-
if (headers.authorization !== `Bearer ${process.env.API_TOKEN}`) {
|
|
260
|
-
return { status: 401, code: 'UNAUTHORIZED', message: 'Invalid or missing token' }
|
|
261
|
-
}
|
|
262
|
-
},
|
|
263
|
-
})
|
|
369
|
+
const driver = betterSqlite3()
|
|
370
|
+
const sirannon = new Sirannon({ driver })
|
|
371
|
+
await sirannon.open('app', './data/app.db')
|
|
264
372
|
|
|
373
|
+
const server = createServer(sirannon, { port: 9876 })
|
|
265
374
|
await server.listen()
|
|
266
375
|
```
|
|
267
376
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
**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.
|
|
271
378
|
|
|
272
379
|
### HTTP routes
|
|
273
380
|
|
|
274
381
|
| Method | Path | Description |
|
|
275
|
-
|
|
382
|
+
| --- | --- | --- |
|
|
276
383
|
| `POST` | `/db/:id/query` | Execute a SELECT, returns `{ rows }` |
|
|
277
384
|
| `POST` | `/db/:id/execute` | Execute a mutation, returns `{ changes, lastInsertRowId }` |
|
|
278
385
|
| `POST` | `/db/:id/transaction` | Execute a batch of statements atomically, returns `{ results }` |
|
|
@@ -302,17 +409,16 @@ const users = await db.query<{ id: number; name: string }>('SELECT * FROM users'
|
|
|
302
409
|
|
|
303
410
|
await db.execute('INSERT INTO users (name) VALUES (?)', ['Turing'])
|
|
304
411
|
|
|
305
|
-
const sub =
|
|
306
|
-
.
|
|
307
|
-
|
|
308
|
-
.subscribe(event => console.log('Admin changed:', event))
|
|
412
|
+
const sub = db.subscribe('users', event => {
|
|
413
|
+
console.log('User changed:', event)
|
|
414
|
+
})
|
|
309
415
|
|
|
310
416
|
// Cleanup:
|
|
311
417
|
sub.unsubscribe()
|
|
312
418
|
client.close()
|
|
313
419
|
```
|
|
314
420
|
|
|
315
|
-
Transactions
|
|
421
|
+
Transactions use the HTTP transport:
|
|
316
422
|
|
|
317
423
|
```ts
|
|
318
424
|
const httpClient = new SirannonClient('http://localhost:9876', {
|
|
@@ -329,12 +435,70 @@ await httpDb.transaction([
|
|
|
329
435
|
httpClient.close()
|
|
330
436
|
```
|
|
331
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
|
+
|
|
332
496
|
## Error handling
|
|
333
497
|
|
|
334
498
|
All errors extend `SirannonError` with a machine-readable `code` property:
|
|
335
499
|
|
|
336
500
|
| Error | Code | When |
|
|
337
|
-
|
|
501
|
+
| --- | --- | --- |
|
|
338
502
|
| `DatabaseNotFoundError` | `DATABASE_NOT_FOUND` | Database ID not in registry |
|
|
339
503
|
| `DatabaseAlreadyExistsError` | `DATABASE_ALREADY_EXISTS` | Duplicate database ID |
|
|
340
504
|
| `ReadOnlyError` | `READ_ONLY` | Write attempted on read-only database |
|
|
@@ -352,7 +516,7 @@ All errors extend `SirannonError` with a machine-readable `code` property:
|
|
|
352
516
|
import { QueryError } from '@delali/sirannon-db'
|
|
353
517
|
|
|
354
518
|
try {
|
|
355
|
-
db.execute('INSERT INTO users (id) VALUES (?)', [1])
|
|
519
|
+
await db.execute('INSERT INTO users (id) VALUES (?)', [1])
|
|
356
520
|
} catch (err) {
|
|
357
521
|
if (err instanceof QueryError) {
|
|
358
522
|
console.error(`SQL failed [${err.code}]: ${err.message}`)
|
|
@@ -363,28 +527,29 @@ try {
|
|
|
363
527
|
|
|
364
528
|
## Configuration reference
|
|
365
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
|
+
|
|
366
539
|
### `DatabaseOptions`
|
|
367
540
|
|
|
368
541
|
| Option | Type | Default | Description |
|
|
369
|
-
|
|
542
|
+
| --- | --- | --- | --- |
|
|
370
543
|
| `readOnly` | `boolean` | `false` | Open in read-only mode |
|
|
371
544
|
| `readPoolSize` | `number` | `4` | Number of read connections |
|
|
372
545
|
| `walMode` | `boolean` | `true` | Enable WAL mode |
|
|
373
546
|
| `cdcPollInterval` | `number` | `50` | CDC polling interval in ms |
|
|
374
547
|
| `cdcRetention` | `number` | `3_600_000` | CDC retention period in ms (1 hour) |
|
|
375
548
|
|
|
376
|
-
### `SirannonOptions`
|
|
377
|
-
|
|
378
|
-
| Option | Type | Description |
|
|
379
|
-
|---|---|---|
|
|
380
|
-
| `hooks` | `HookConfig` | Before/after hooks for queries, connections, subscriptions |
|
|
381
|
-
| `metrics` | `MetricsConfig` | Callbacks for query timing, connection events, CDC activity |
|
|
382
|
-
| `lifecycle` | `LifecycleConfig` | Auto-open resolver, idle timeout, max open databases |
|
|
383
|
-
|
|
384
549
|
### `ServerOptions`
|
|
385
550
|
|
|
386
551
|
| Option | Type | Default | Description |
|
|
387
|
-
|
|
552
|
+
| --- | --- | --- | --- |
|
|
388
553
|
| `host` | `string` | `'127.0.0.1'` | Bind address |
|
|
389
554
|
| `port` | `number` | `9876` | Listen port |
|
|
390
555
|
| `cors` | `boolean \| CorsOptions` | `false` | CORS configuration |
|
|
@@ -393,15 +558,55 @@ try {
|
|
|
393
558
|
### `ClientOptions`
|
|
394
559
|
|
|
395
560
|
| Option | Type | Default | Description |
|
|
396
|
-
|
|
561
|
+
| --- | --- | --- | --- |
|
|
397
562
|
| `transport` | `'websocket' \| 'http'` | `'websocket'` | Transport protocol |
|
|
398
563
|
| `headers` | `Record<string, string>` | - | Custom HTTP headers |
|
|
399
564
|
| `autoReconnect` | `boolean` | `true` | Reconnect on WebSocket disconnect |
|
|
400
565
|
| `reconnectInterval` | `number` | `1000` | Reconnect delay in ms |
|
|
401
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
|
+
|
|
402
607
|
## Benchmarks
|
|
403
608
|
|
|
404
|
-
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.
|
|
405
610
|
|
|
406
611
|
## Development
|
|
407
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 };
|