boundlessdb 0.1.0
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/CHANGELOG.md +89 -0
- package/LICENSE +21 -0
- package/README.md +545 -0
- package/dist/better-sqlite3-shim.d.ts +12 -0
- package/dist/better-sqlite3-shim.d.ts.map +1 -0
- package/dist/better-sqlite3-shim.js +18 -0
- package/dist/better-sqlite3-shim.js.map +1 -0
- package/dist/browser.d.ts +14 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +14 -0
- package/dist/browser.js.map +1 -0
- package/dist/config/extractor.d.ts +22 -0
- package/dist/config/extractor.d.ts.map +1 -0
- package/dist/config/extractor.js +132 -0
- package/dist/config/extractor.js.map +1 -0
- package/dist/config/validator.d.ts +14 -0
- package/dist/config/validator.d.ts.map +1 -0
- package/dist/config/validator.js +94 -0
- package/dist/config/validator.js.map +1 -0
- package/dist/event-store.browser.d.ts +61 -0
- package/dist/event-store.browser.d.ts.map +1 -0
- package/dist/event-store.browser.js +323 -0
- package/dist/event-store.browser.js.map +1 -0
- package/dist/event-store.d.ts +101 -0
- package/dist/event-store.d.ts.map +1 -0
- package/dist/event-store.js +249 -0
- package/dist/event-store.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/query-builder.d.ts +72 -0
- package/dist/query-builder.d.ts.map +1 -0
- package/dist/query-builder.js +84 -0
- package/dist/query-builder.js.map +1 -0
- package/dist/storage/interface.d.ts +48 -0
- package/dist/storage/interface.d.ts.map +1 -0
- package/dist/storage/interface.js +5 -0
- package/dist/storage/interface.js.map +1 -0
- package/dist/storage/memory.d.ts +27 -0
- package/dist/storage/memory.d.ts.map +1 -0
- package/dist/storage/memory.js +94 -0
- package/dist/storage/memory.js.map +1 -0
- package/dist/storage/postgres.d.ts +76 -0
- package/dist/storage/postgres.d.ts.map +1 -0
- package/dist/storage/postgres.js +346 -0
- package/dist/storage/postgres.js.map +1 -0
- package/dist/storage/sqlite.d.ts +47 -0
- package/dist/storage/sqlite.d.ts.map +1 -0
- package/dist/storage/sqlite.js +249 -0
- package/dist/storage/sqlite.js.map +1 -0
- package/dist/storage/sqljs.d.ts +60 -0
- package/dist/storage/sqljs.d.ts.map +1 -0
- package/dist/storage/sqljs.js +354 -0
- package/dist/storage/sqljs.js.map +1 -0
- package/dist/types.d.ts +172 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +52 -0
- package/dist/types.js.map +1 -0
- package/package.json +75 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite Storage implementation using better-sqlite3
|
|
3
|
+
*/
|
|
4
|
+
import Database from 'better-sqlite3';
|
|
5
|
+
import { isConstrainedCondition } from '../types.js';
|
|
6
|
+
const SCHEMA = `
|
|
7
|
+
-- Events (Append-Only Log)
|
|
8
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
9
|
+
position INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
10
|
+
event_id TEXT NOT NULL UNIQUE,
|
|
11
|
+
event_type TEXT NOT NULL,
|
|
12
|
+
data TEXT NOT NULL,
|
|
13
|
+
metadata TEXT,
|
|
14
|
+
timestamp TEXT NOT NULL,
|
|
15
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
-- Consistency Key Index (populated by store on write)
|
|
19
|
+
CREATE TABLE IF NOT EXISTS event_keys (
|
|
20
|
+
position INTEGER NOT NULL REFERENCES events(position),
|
|
21
|
+
key_name TEXT NOT NULL,
|
|
22
|
+
key_value TEXT NOT NULL,
|
|
23
|
+
PRIMARY KEY (position, key_name, key_value)
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
-- Metadata (for config hash, etc.)
|
|
27
|
+
CREATE TABLE IF NOT EXISTS metadata (
|
|
28
|
+
key TEXT PRIMARY KEY,
|
|
29
|
+
value TEXT NOT NULL
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
-- Indices for fast queries
|
|
33
|
+
CREATE INDEX IF NOT EXISTS idx_event_type ON events(event_type);
|
|
34
|
+
CREATE INDEX IF NOT EXISTS idx_key ON event_keys(key_name, key_value);
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_key_position ON event_keys(key_name, key_value, position);
|
|
36
|
+
`;
|
|
37
|
+
/**
|
|
38
|
+
* SQLite-backed event storage
|
|
39
|
+
*/
|
|
40
|
+
export class SqliteStorage {
|
|
41
|
+
db;
|
|
42
|
+
constructor(path = ':memory:') {
|
|
43
|
+
this.db = new Database(path);
|
|
44
|
+
this.db.pragma('journal_mode = WAL');
|
|
45
|
+
this.db.pragma('foreign_keys = ON');
|
|
46
|
+
this.db.exec(SCHEMA);
|
|
47
|
+
}
|
|
48
|
+
async append(eventsToStore, keys) {
|
|
49
|
+
if (eventsToStore.length !== keys.length) {
|
|
50
|
+
throw new Error('Events and keys arrays must have the same length');
|
|
51
|
+
}
|
|
52
|
+
if (eventsToStore.length === 0) {
|
|
53
|
+
return this.getLatestPosition();
|
|
54
|
+
}
|
|
55
|
+
const insertEvent = this.db.prepare(`
|
|
56
|
+
INSERT INTO events (event_id, event_type, data, metadata, timestamp)
|
|
57
|
+
VALUES (?, ?, ?, ?, ?)
|
|
58
|
+
`);
|
|
59
|
+
const insertKey = this.db.prepare(`
|
|
60
|
+
INSERT INTO event_keys (position, key_name, key_value)
|
|
61
|
+
VALUES (?, ?, ?)
|
|
62
|
+
`);
|
|
63
|
+
let lastPosition = 0n;
|
|
64
|
+
// Everything in one transaction for atomicity
|
|
65
|
+
const transaction = this.db.transaction(() => {
|
|
66
|
+
for (let i = 0; i < eventsToStore.length; i++) {
|
|
67
|
+
const event = eventsToStore[i];
|
|
68
|
+
const eventKeys = keys[i];
|
|
69
|
+
// Insert event
|
|
70
|
+
const result = insertEvent.run(event.id, event.type, JSON.stringify(event.data), event.metadata ? JSON.stringify(event.metadata) : null, event.timestamp.toISOString());
|
|
71
|
+
const position = BigInt(result.lastInsertRowid);
|
|
72
|
+
lastPosition = position;
|
|
73
|
+
// Insert keys
|
|
74
|
+
for (const key of eventKeys) {
|
|
75
|
+
insertKey.run(Number(position), key.name, key.value);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
transaction();
|
|
80
|
+
return lastPosition;
|
|
81
|
+
}
|
|
82
|
+
async query(conditions, fromPosition, limit) {
|
|
83
|
+
if (conditions.length === 0) {
|
|
84
|
+
// No conditions = return all events
|
|
85
|
+
let sql = `
|
|
86
|
+
SELECT position, event_id, event_type, data, metadata, timestamp
|
|
87
|
+
FROM events
|
|
88
|
+
`;
|
|
89
|
+
const params = [];
|
|
90
|
+
if (fromPosition !== undefined) {
|
|
91
|
+
sql += ' WHERE position > ?';
|
|
92
|
+
params.push(Number(fromPosition));
|
|
93
|
+
}
|
|
94
|
+
sql += ' ORDER BY position';
|
|
95
|
+
if (limit !== undefined) {
|
|
96
|
+
sql += ' LIMIT ?';
|
|
97
|
+
params.push(limit);
|
|
98
|
+
}
|
|
99
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
100
|
+
return rows.map(row => this.rowToEvent(row));
|
|
101
|
+
}
|
|
102
|
+
// Separate conditions: constrained (with key/value) vs unconstrained (type only)
|
|
103
|
+
const constrained = conditions.filter(isConstrainedCondition);
|
|
104
|
+
const unconstrained = conditions.filter(c => !isConstrainedCondition(c));
|
|
105
|
+
const whereClauses = [];
|
|
106
|
+
const whereParams = [];
|
|
107
|
+
// Unconstrained: match by type only (no join needed)
|
|
108
|
+
if (unconstrained.length > 0) {
|
|
109
|
+
const typePlaceholders = unconstrained.map(() => '?').join(', ');
|
|
110
|
+
whereClauses.push(`e.event_type IN (${typePlaceholders})`);
|
|
111
|
+
whereParams.push(...unconstrained.map(c => c.type));
|
|
112
|
+
}
|
|
113
|
+
// Constrained: match by type + key + value
|
|
114
|
+
if (constrained.length > 0) {
|
|
115
|
+
const constrainedClauses = constrained.map(() => '(e.event_type = ? AND k.key_name = ? AND k.key_value = ?)');
|
|
116
|
+
whereClauses.push(`(${constrainedClauses.join(' OR ')})`);
|
|
117
|
+
whereParams.push(...constrained.flatMap(c => [c.type, c.key, c.value]));
|
|
118
|
+
}
|
|
119
|
+
// Build SQL
|
|
120
|
+
let sql;
|
|
121
|
+
if (constrained.length > 0) {
|
|
122
|
+
// Need JOIN for constrained conditions
|
|
123
|
+
sql = `
|
|
124
|
+
SELECT DISTINCT
|
|
125
|
+
e.position,
|
|
126
|
+
e.event_id,
|
|
127
|
+
e.event_type,
|
|
128
|
+
e.data,
|
|
129
|
+
e.metadata,
|
|
130
|
+
e.timestamp
|
|
131
|
+
FROM events e
|
|
132
|
+
LEFT JOIN event_keys k ON e.position = k.position
|
|
133
|
+
WHERE (${whereClauses.join(' OR ')})
|
|
134
|
+
`;
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
// No constrained conditions, no JOIN needed
|
|
138
|
+
sql = `
|
|
139
|
+
SELECT
|
|
140
|
+
position,
|
|
141
|
+
event_id,
|
|
142
|
+
event_type,
|
|
143
|
+
data,
|
|
144
|
+
metadata,
|
|
145
|
+
timestamp
|
|
146
|
+
FROM events e
|
|
147
|
+
WHERE (${whereClauses.join(' OR ')})
|
|
148
|
+
`;
|
|
149
|
+
}
|
|
150
|
+
const params = [...whereParams];
|
|
151
|
+
if (fromPosition !== undefined) {
|
|
152
|
+
sql += ' AND e.position > ?';
|
|
153
|
+
params.push(Number(fromPosition));
|
|
154
|
+
}
|
|
155
|
+
sql += ' ORDER BY e.position';
|
|
156
|
+
if (limit !== undefined) {
|
|
157
|
+
sql += ' LIMIT ?';
|
|
158
|
+
params.push(limit);
|
|
159
|
+
}
|
|
160
|
+
const stmt = this.db.prepare(sql);
|
|
161
|
+
const rows = stmt.all(...params);
|
|
162
|
+
return rows.map(row => this.rowToEvent(row));
|
|
163
|
+
}
|
|
164
|
+
async getEventsSince(conditions, sincePosition) {
|
|
165
|
+
return this.query(conditions, sincePosition);
|
|
166
|
+
}
|
|
167
|
+
async getLatestPosition() {
|
|
168
|
+
const row = this.db.prepare('SELECT MAX(position) as pos FROM events').get();
|
|
169
|
+
return row.pos ? BigInt(row.pos) : 0n;
|
|
170
|
+
}
|
|
171
|
+
async close() {
|
|
172
|
+
this.db.close();
|
|
173
|
+
}
|
|
174
|
+
// --- UI Helper Methods (not part of core DCB API) ---
|
|
175
|
+
/**
|
|
176
|
+
* Get all events (for debugging/UI)
|
|
177
|
+
*/
|
|
178
|
+
getAllEvents() {
|
|
179
|
+
const rows = this.db.prepare(`
|
|
180
|
+
SELECT position, event_id, event_type, data, metadata, timestamp
|
|
181
|
+
FROM events
|
|
182
|
+
ORDER BY position ASC
|
|
183
|
+
`).all();
|
|
184
|
+
return rows.map(row => this.rowToEvent(row));
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Get all keys (for debugging/UI)
|
|
188
|
+
*/
|
|
189
|
+
getAllKeys() {
|
|
190
|
+
return this.db.prepare(`
|
|
191
|
+
SELECT position, key_name, key_value
|
|
192
|
+
FROM event_keys
|
|
193
|
+
ORDER BY position ASC
|
|
194
|
+
`).all();
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Clear all data (for testing)
|
|
198
|
+
*/
|
|
199
|
+
clear() {
|
|
200
|
+
this.db.exec('DELETE FROM event_keys');
|
|
201
|
+
this.db.exec('DELETE FROM events');
|
|
202
|
+
this.db.exec("DELETE FROM sqlite_sequence WHERE name IN ('events', 'event_keys')");
|
|
203
|
+
}
|
|
204
|
+
// --- Metadata Methods ---
|
|
205
|
+
/**
|
|
206
|
+
* Get stored config hash
|
|
207
|
+
*/
|
|
208
|
+
getConfigHash() {
|
|
209
|
+
const row = this.db.prepare("SELECT value FROM metadata WHERE key = 'config_hash'").get();
|
|
210
|
+
return row?.value ?? null;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Set config hash
|
|
214
|
+
*/
|
|
215
|
+
setConfigHash(hash) {
|
|
216
|
+
this.db.prepare("INSERT OR REPLACE INTO metadata (key, value) VALUES ('config_hash', ?)").run(hash);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Reindex all events with new keys
|
|
220
|
+
*/
|
|
221
|
+
reindex(extractKeys) {
|
|
222
|
+
const events = this.getAllEvents();
|
|
223
|
+
const deleteKeys = this.db.prepare('DELETE FROM event_keys');
|
|
224
|
+
const insertKey = this.db.prepare('INSERT INTO event_keys (position, key_name, key_value) VALUES (?, ?, ?)');
|
|
225
|
+
const transaction = this.db.transaction(() => {
|
|
226
|
+
// Clear all keys
|
|
227
|
+
deleteKeys.run();
|
|
228
|
+
// Re-extract and insert keys for all events
|
|
229
|
+
for (const event of events) {
|
|
230
|
+
const keys = extractKeys(event);
|
|
231
|
+
for (const key of keys) {
|
|
232
|
+
insertKey.run(Number(event.position), key.name, key.value);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
transaction();
|
|
237
|
+
}
|
|
238
|
+
rowToEvent(row) {
|
|
239
|
+
return {
|
|
240
|
+
id: row.event_id,
|
|
241
|
+
type: row.event_type,
|
|
242
|
+
data: JSON.parse(row.data),
|
|
243
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
|
|
244
|
+
timestamp: new Date(row.timestamp),
|
|
245
|
+
position: BigInt(row.position),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
//# sourceMappingURL=sqlite.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite.js","sourceRoot":"","sources":["../../src/storage/sqlite.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,sBAAsB,EAA4D,MAAM,aAAa,CAAC;AAG/G,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8Bd,CAAC;AAWF;;GAEG;AACH,MAAM,OAAO,aAAa;IAChB,EAAE,CAAoB;IAE9B,YAAY,OAAe,UAAU;QACnC,IAAI,CAAC,EAAE,GAAG,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC7B,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;QACrC,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC;QACpC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACvB,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,aAA6B,EAAE,IAAsB;QAChE,IAAI,aAAa,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC;YACzC,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;QACtE,CAAC;QAED,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC/B,OAAO,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAClC,CAAC;QAED,MAAM,WAAW,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC;;;KAGnC,CAAC,CAAC;QAEH,MAAM,SAAS,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC;;;KAGjC,CAAC,CAAC;QAEH,IAAI,YAAY,GAAW,EAAE,CAAC;QAE9B,8CAA8C;QAC9C,MAAM,WAAW,GAAG,IAAI,CAAC,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE;YAC3C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,aAAa,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBAC9C,MAAM,KAAK,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC;gBAC/B,MAAM,SAAS,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;gBAE1B,eAAe;gBACf,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAC5B,KAAK,CAAC,EAAE,EACR,KAAK,CAAC,IAAI,EACV,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,EAC1B,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,EACtD,KAAK,CAAC,SAAS,CAAC,WAAW,EAAE,CAC9B,CAAC;gBAEF,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;gBAChD,YAAY,GAAG,QAAQ,CAAC;gBAExB,cAAc;gBACd,KAAK,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;oBAC5B,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC;gBACvD,CAAC;YACH,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,WAAW,EAAE,CAAC;QAEd,OAAO,YAAY,CAAC;IACtB,CAAC;IAED,KAAK,CAAC,KAAK,CACT,UAA4B,EAC5B,YAAqB,EACrB,KAAc;QAEd,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC5B,oCAAoC;YACpC,IAAI,GAAG,GAAG;;;OAGT,CAAC;YACF,MAAM,MAAM,GAAwB,EAAE,CAAC;YAEvC,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;gBAC/B,GAAG,IAAI,qBAAqB,CAAC;gBAC7B,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC;YACpC,CAAC;YAED,GAAG,IAAI,oBAAoB,CAAC;YAE5B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACxB,GAAG,IAAI,UAAU,CAAC;gBAClB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACrB,CAAC;YAED,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,MAAM,CAAe,CAAC;YAC/D,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;QAC/C,CAAC;QAED,iFAAiF;QACjF,MAAM,WAAW,GAAG,UAAU,CAAC,MAAM,CAAC,sBAAsB,CAAC,CAAC;QAC9D,MAAM,aAAa,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,CAAC;QAEzE,MAAM,YAAY,GAAa,EAAE,CAAC;QAClC,MAAM,WAAW,GAAwB,EAAE,CAAC;QAE5C,qDAAqD;QACrD,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC7B,MAAM,gBAAgB,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACjE,YAAY,CAAC,IAAI,CAAC,oBAAoB,gBAAgB,GAAG,CAAC,CAAC;YAC3D,WAAW,CAAC,IAAI,CAAC,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QACtD,CAAC;QAED,2CAA2C;QAC3C,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,MAAM,kBAAkB,GAAG,WAAW,CAAC,GAAG,CACxC,GAAG,EAAE,CAAC,2DAA2D,CAClE,CAAC;YACF,YAAY,CAAC,IAAI,CAAC,IAAI,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC1D,WAAW,CAAC,IAAI,CAAC,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAC1E,CAAC;QAED,YAAY;QACZ,IAAI,GAAW,CAAC;QAChB,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,uCAAuC;YACvC,GAAG,GAAG;;;;;;;;;;iBAUK,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC;OACnC,CAAC;QACJ,CAAC;aAAM,CAAC;YACN,4CAA4C;YAC5C,GAAG,GAAG;;;;;;;;;iBASK,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC;OACnC,CAAC;QACJ,CAAC;QAED,MAAM,MAAM,GAAwB,CAAC,GAAG,WAAW,CAAC,CAAC;QAErD,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;YAC/B,GAAG,IAAI,qBAAqB,CAAC;YAC7B,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC;QACpC,CAAC;QAED,GAAG,IAAI,sBAAsB,CAAC;QAE9B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,GAAG,IAAI,UAAU,CAAC;YAClB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACrB,CAAC;QAED,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAClC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,MAAM,CAAe,CAAC;QAE/C,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/C,CAAC;IAED,KAAK,CAAC,cAAc,CAClB,UAA4B,EAC5B,aAAqB;QAErB,OAAO,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;IAC/C,CAAC;IAED,KAAK,CAAC,iBAAiB;QACrB,MAAM,GAAG,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,yCAAyC,CAAC,CAAC,GAAG,EAA4B,CAAC;QACvG,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACxC,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC;IAClB,CAAC;IAED,uDAAuD;IAEvD;;OAEG;IACH,YAAY;QACV,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC;;;;KAI5B,CAAC,CAAC,GAAG,EAAgB,CAAC;QAEvB,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/C,CAAC;IAED;;OAEG;IACH,UAAU;QACR,OAAO,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC;;;;KAItB,CAAC,CAAC,GAAG,EAAsE,CAAC;IAC/E,CAAC;IAED;;OAEG;IACH,KAAK;QACH,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAC;QACvC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;QACnC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,oEAAoE,CAAC,CAAC;IACrF,CAAC;IAED,2BAA2B;IAE3B;;OAEG;IACH,aAAa;QACX,MAAM,GAAG,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CACzB,sDAAsD,CACvD,CAAC,GAAG,EAAmC,CAAC;QACzC,OAAO,GAAG,EAAE,KAAK,IAAI,IAAI,CAAC;IAC5B,CAAC;IAED;;OAEG;IACH,aAAa,CAAC,IAAY;QACxB,IAAI,CAAC,EAAE,CAAC,OAAO,CACb,wEAAwE,CACzE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACd,CAAC;IAED;;OAEG;IACH,OAAO,CAAC,WAAmD;QACzD,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;QAEnC,MAAM,UAAU,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,wBAAwB,CAAC,CAAC;QAC7D,MAAM,SAAS,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAC/B,yEAAyE,CAC1E,CAAC;QAEF,MAAM,WAAW,GAAG,IAAI,CAAC,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE;YAC3C,iBAAiB;YACjB,UAAU,CAAC,GAAG,EAAE,CAAC;YAEjB,4CAA4C;YAC5C,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;gBAC3B,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;gBAChC,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;oBACvB,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC;gBAC7D,CAAC;YACH,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,WAAW,EAAE,CAAC;IAChB,CAAC;IAEO,UAAU,CAAC,GAAa;QAC9B,OAAO;YACL,EAAE,EAAE,GAAG,CAAC,QAAQ;YAChB,IAAI,EAAE,GAAG,CAAC,UAAU;YACpB,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC;YAC1B,QAAQ,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS;YAC7D,SAAS,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC;YAClC,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;SAC/B,CAAC;IACJ,CAAC;CACF"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sql.js Storage implementation for browser environments
|
|
3
|
+
*/
|
|
4
|
+
import { type ExtractedKey, type QueryCondition, type StoredEvent } from '../types.js';
|
|
5
|
+
import type { EventStorage, EventToStore } from './interface.js';
|
|
6
|
+
export interface SqlJsStorageOptions {
|
|
7
|
+
/** URL to load sql.js WASM from (optional, uses CDN by default) */
|
|
8
|
+
wasmUrl?: string;
|
|
9
|
+
/** Existing database bytes to restore from */
|
|
10
|
+
data?: ArrayLike<number>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* sql.js-backed event storage for browser environments
|
|
14
|
+
*/
|
|
15
|
+
export declare class SqlJsStorage implements EventStorage {
|
|
16
|
+
private db;
|
|
17
|
+
private initPromise;
|
|
18
|
+
constructor(options?: SqlJsStorageOptions);
|
|
19
|
+
private initialize;
|
|
20
|
+
private ensureInitialized;
|
|
21
|
+
append(eventsToStore: EventToStore[], keys: ExtractedKey[][]): Promise<bigint>;
|
|
22
|
+
query(conditions: QueryCondition[], fromPosition?: bigint, limit?: number): Promise<StoredEvent[]>;
|
|
23
|
+
getEventsSince(conditions: QueryCondition[], sincePosition: bigint): Promise<StoredEvent[]>;
|
|
24
|
+
getLatestPosition(): Promise<bigint>;
|
|
25
|
+
close(): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Get all events (for debugging/UI)
|
|
28
|
+
*/
|
|
29
|
+
getAllEvents(): Promise<StoredEvent[]>;
|
|
30
|
+
/**
|
|
31
|
+
* Get all keys (for debugging/UI)
|
|
32
|
+
*/
|
|
33
|
+
getAllKeys(): Promise<Array<{
|
|
34
|
+
position: number;
|
|
35
|
+
key_name: string;
|
|
36
|
+
key_value: string;
|
|
37
|
+
}>>;
|
|
38
|
+
/**
|
|
39
|
+
* Clear all data (for testing)
|
|
40
|
+
*/
|
|
41
|
+
clear(): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Export database as Uint8Array for persistence
|
|
44
|
+
*/
|
|
45
|
+
export(): Promise<Uint8Array>;
|
|
46
|
+
/**
|
|
47
|
+
* Get stored config hash
|
|
48
|
+
*/
|
|
49
|
+
getConfigHash(): Promise<string | null>;
|
|
50
|
+
/**
|
|
51
|
+
* Set config hash
|
|
52
|
+
*/
|
|
53
|
+
setConfigHash(hash: string): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Reindex all events with new keys
|
|
56
|
+
*/
|
|
57
|
+
reindex(extractKeys: (event: StoredEvent) => ExtractedKey[]): Promise<void>;
|
|
58
|
+
private rowToEvent;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=sqljs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqljs.d.ts","sourceRoot":"","sources":["../../src/storage/sqljs.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAA0B,KAAK,YAAY,EAAE,KAAK,cAAc,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/G,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AA4CjE,MAAM,WAAW,mBAAmB;IAClC,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8CAA8C;IAC9C,IAAI,CAAC,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC;CAC1B;AAED;;GAEG;AACH,qBAAa,YAAa,YAAW,YAAY;IAC/C,OAAO,CAAC,EAAE,CAA8B;IACxC,OAAO,CAAC,WAAW,CAAgB;gBAEvB,OAAO,GAAE,mBAAwB;YAI/B,UAAU;YAkBV,iBAAiB;IAQzB,MAAM,CAAC,aAAa,EAAE,YAAY,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAgE9E,KAAK,CACT,UAAU,EAAE,cAAc,EAAE,EAC5B,YAAY,CAAC,EAAE,MAAM,EACrB,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,WAAW,EAAE,CAAC;IAmHnB,cAAc,CAClB,UAAU,EAAE,cAAc,EAAE,EAC5B,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,WAAW,EAAE,CAAC;IAInB,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC;IASpC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ5B;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAyB5C;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAiB7F;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAO5B;;OAEG;IACG,MAAM,IAAI,OAAO,CAAC,UAAU,CAAC;IAOnC;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAS7C;;OAEG;IACG,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAahD;;OAEG;IACG,OAAO,CAAC,WAAW,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,YAAY,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IA4BjF,OAAO,CAAC,UAAU;CAUnB"}
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sql.js Storage implementation for browser environments
|
|
3
|
+
*/
|
|
4
|
+
import initSqlJs from 'sql.js';
|
|
5
|
+
import { isConstrainedCondition } from '../types.js';
|
|
6
|
+
const SCHEMA = `
|
|
7
|
+
-- Events (Append-Only Log)
|
|
8
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
9
|
+
position INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
10
|
+
event_id TEXT NOT NULL UNIQUE,
|
|
11
|
+
event_type TEXT NOT NULL,
|
|
12
|
+
data TEXT NOT NULL,
|
|
13
|
+
metadata TEXT,
|
|
14
|
+
timestamp TEXT NOT NULL,
|
|
15
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
-- Consistency Key Index (populated by store on write)
|
|
19
|
+
CREATE TABLE IF NOT EXISTS event_keys (
|
|
20
|
+
position INTEGER NOT NULL,
|
|
21
|
+
key_name TEXT NOT NULL,
|
|
22
|
+
key_value TEXT NOT NULL,
|
|
23
|
+
PRIMARY KEY (position, key_name, key_value),
|
|
24
|
+
FOREIGN KEY (position) REFERENCES events(position)
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
-- Metadata (for config hash, etc.)
|
|
28
|
+
CREATE TABLE IF NOT EXISTS metadata (
|
|
29
|
+
key TEXT PRIMARY KEY,
|
|
30
|
+
value TEXT NOT NULL
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
-- Indices for fast queries
|
|
34
|
+
CREATE INDEX IF NOT EXISTS idx_event_type ON events(event_type);
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_key ON event_keys(key_name, key_value);
|
|
36
|
+
CREATE INDEX IF NOT EXISTS idx_key_position ON event_keys(key_name, key_value, position);
|
|
37
|
+
`;
|
|
38
|
+
/**
|
|
39
|
+
* sql.js-backed event storage for browser environments
|
|
40
|
+
*/
|
|
41
|
+
export class SqlJsStorage {
|
|
42
|
+
db = null;
|
|
43
|
+
initPromise;
|
|
44
|
+
constructor(options = {}) {
|
|
45
|
+
this.initPromise = this.initialize(options);
|
|
46
|
+
}
|
|
47
|
+
async initialize(options) {
|
|
48
|
+
const SQL = await initSqlJs({
|
|
49
|
+
locateFile: (file) => {
|
|
50
|
+
if (options.wasmUrl)
|
|
51
|
+
return options.wasmUrl;
|
|
52
|
+
// Use CDN by default
|
|
53
|
+
return `https://sql.js.org/dist/${file}`;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
if (options.data) {
|
|
57
|
+
this.db = new SQL.Database(new Uint8Array(options.data));
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
this.db = new SQL.Database();
|
|
61
|
+
}
|
|
62
|
+
this.db.run(SCHEMA);
|
|
63
|
+
}
|
|
64
|
+
async ensureInitialized() {
|
|
65
|
+
await this.initPromise;
|
|
66
|
+
if (!this.db) {
|
|
67
|
+
throw new Error('Database not initialized');
|
|
68
|
+
}
|
|
69
|
+
return this.db;
|
|
70
|
+
}
|
|
71
|
+
async append(eventsToStore, keys) {
|
|
72
|
+
const db = await this.ensureInitialized();
|
|
73
|
+
if (eventsToStore.length !== keys.length) {
|
|
74
|
+
throw new Error('Events and keys arrays must have the same length');
|
|
75
|
+
}
|
|
76
|
+
if (eventsToStore.length === 0) {
|
|
77
|
+
return this.getLatestPosition();
|
|
78
|
+
}
|
|
79
|
+
let lastPosition = 0n;
|
|
80
|
+
// sql.js doesn't have built-in transactions, but we can use BEGIN/COMMIT
|
|
81
|
+
db.run('BEGIN TRANSACTION');
|
|
82
|
+
try {
|
|
83
|
+
// sql.js db.run() doesn't support params properly - use escaped SQL
|
|
84
|
+
const escapeSql = (s) => {
|
|
85
|
+
if (s === null)
|
|
86
|
+
return 'NULL';
|
|
87
|
+
return "'" + s.replace(/'/g, "''") + "'";
|
|
88
|
+
};
|
|
89
|
+
for (let i = 0; i < eventsToStore.length; i++) {
|
|
90
|
+
const event = eventsToStore[i];
|
|
91
|
+
const eventKeys = keys[i];
|
|
92
|
+
// Insert event
|
|
93
|
+
const eventId = String(event.id);
|
|
94
|
+
const eventType = String(event.type);
|
|
95
|
+
const eventData = JSON.stringify(event.data);
|
|
96
|
+
const eventMeta = event.metadata ? JSON.stringify(event.metadata) : null;
|
|
97
|
+
const eventTime = event.timestamp.toISOString();
|
|
98
|
+
if (!eventId || eventId === 'undefined' || eventId === 'null') {
|
|
99
|
+
throw new Error(`[SqlJsStorage] Invalid event ID: "${eventId}"`);
|
|
100
|
+
}
|
|
101
|
+
const sql = `INSERT INTO events (event_id, event_type, data, metadata, timestamp)
|
|
102
|
+
VALUES (${escapeSql(eventId)}, ${escapeSql(eventType)}, ${escapeSql(eventData)}, ${eventMeta === null ? 'NULL' : escapeSql(eventMeta)}, ${escapeSql(eventTime)})`;
|
|
103
|
+
db.run(sql);
|
|
104
|
+
// Get the last inserted position
|
|
105
|
+
const result = db.exec('SELECT last_insert_rowid() as position');
|
|
106
|
+
const position = BigInt(result[0].values[0][0]);
|
|
107
|
+
lastPosition = position;
|
|
108
|
+
// Insert keys
|
|
109
|
+
for (const key of eventKeys) {
|
|
110
|
+
const keySql = `INSERT INTO event_keys (position, key_name, key_value) VALUES (${Number(position)}, ${escapeSql(key.name)}, ${escapeSql(key.value)})`;
|
|
111
|
+
db.run(keySql);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
db.run('COMMIT');
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
db.run('ROLLBACK');
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
return lastPosition;
|
|
121
|
+
}
|
|
122
|
+
async query(conditions, fromPosition, limit) {
|
|
123
|
+
const db = await this.ensureInitialized();
|
|
124
|
+
// sql.js doesn't support params properly - use escaped SQL
|
|
125
|
+
const escapeSql = (s) => "'" + s.replace(/'/g, "''") + "'";
|
|
126
|
+
if (conditions.length === 0) {
|
|
127
|
+
// No conditions = return all events
|
|
128
|
+
let sql = `
|
|
129
|
+
SELECT position, event_id, event_type, data, metadata, timestamp
|
|
130
|
+
FROM events
|
|
131
|
+
`;
|
|
132
|
+
if (fromPosition !== undefined) {
|
|
133
|
+
sql += ` WHERE position > ${Number(fromPosition)}`;
|
|
134
|
+
}
|
|
135
|
+
sql += ' ORDER BY position';
|
|
136
|
+
if (limit !== undefined) {
|
|
137
|
+
sql += ` LIMIT ${Number(limit)}`;
|
|
138
|
+
}
|
|
139
|
+
const result = db.exec(sql);
|
|
140
|
+
if (result.length === 0)
|
|
141
|
+
return [];
|
|
142
|
+
const columns = result[0].columns || result[0].lc;
|
|
143
|
+
const rows = result[0].values;
|
|
144
|
+
return rows.map((row) => {
|
|
145
|
+
const obj = {};
|
|
146
|
+
columns.forEach((col, i) => {
|
|
147
|
+
obj[col] = row[i];
|
|
148
|
+
});
|
|
149
|
+
return this.rowToEvent(obj);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
// Separate conditions: constrained (with key/value) vs unconstrained (type only)
|
|
153
|
+
const constrained = conditions.filter(isConstrainedCondition);
|
|
154
|
+
const unconstrained = conditions.filter(c => !isConstrainedCondition(c));
|
|
155
|
+
const whereClauses = [];
|
|
156
|
+
// Unconstrained: match by type only
|
|
157
|
+
if (unconstrained.length > 0) {
|
|
158
|
+
const typeList = unconstrained.map(c => escapeSql(c.type)).join(', ');
|
|
159
|
+
whereClauses.push(`e.event_type IN (${typeList})`);
|
|
160
|
+
}
|
|
161
|
+
// Constrained: match by type + key + value
|
|
162
|
+
if (constrained.length > 0) {
|
|
163
|
+
const constrainedClauses = constrained.map(c => `(e.event_type = ${escapeSql(c.type)} AND k.key_name = ${escapeSql(c.key)} AND k.key_value = ${escapeSql(c.value)})`);
|
|
164
|
+
whereClauses.push(`(${constrainedClauses.join(' OR ')})`);
|
|
165
|
+
}
|
|
166
|
+
// Build SQL
|
|
167
|
+
let sql;
|
|
168
|
+
if (constrained.length > 0) {
|
|
169
|
+
// Need JOIN for constrained conditions
|
|
170
|
+
sql = `
|
|
171
|
+
SELECT DISTINCT
|
|
172
|
+
e.position,
|
|
173
|
+
e.event_id,
|
|
174
|
+
e.event_type,
|
|
175
|
+
e.data,
|
|
176
|
+
e.metadata,
|
|
177
|
+
e.timestamp
|
|
178
|
+
FROM events e
|
|
179
|
+
LEFT JOIN event_keys k ON e.position = k.position
|
|
180
|
+
WHERE (${whereClauses.join(' OR ')})
|
|
181
|
+
`;
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
// No constrained conditions, no JOIN needed
|
|
185
|
+
sql = `
|
|
186
|
+
SELECT
|
|
187
|
+
position,
|
|
188
|
+
event_id,
|
|
189
|
+
event_type,
|
|
190
|
+
data,
|
|
191
|
+
metadata,
|
|
192
|
+
timestamp
|
|
193
|
+
FROM events e
|
|
194
|
+
WHERE (${whereClauses.join(' OR ')})
|
|
195
|
+
`;
|
|
196
|
+
}
|
|
197
|
+
if (fromPosition !== undefined) {
|
|
198
|
+
sql += ` AND e.position > ${Number(fromPosition)}`;
|
|
199
|
+
}
|
|
200
|
+
sql += ' ORDER BY e.position';
|
|
201
|
+
if (limit !== undefined) {
|
|
202
|
+
sql += ` LIMIT ${Number(limit)}`;
|
|
203
|
+
}
|
|
204
|
+
const result = db.exec(sql);
|
|
205
|
+
if (result.length === 0)
|
|
206
|
+
return [];
|
|
207
|
+
// sql.js uses 'columns' or 'lc' depending on version
|
|
208
|
+
const columns = result[0].columns || result[0].lc;
|
|
209
|
+
const rows = result[0].values;
|
|
210
|
+
return rows.map((row) => {
|
|
211
|
+
const obj = {};
|
|
212
|
+
columns.forEach((col, i) => {
|
|
213
|
+
obj[col] = row[i];
|
|
214
|
+
});
|
|
215
|
+
return this.rowToEvent(obj);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
async getEventsSince(conditions, sincePosition) {
|
|
219
|
+
return this.query(conditions, sincePosition);
|
|
220
|
+
}
|
|
221
|
+
async getLatestPosition() {
|
|
222
|
+
const db = await this.ensureInitialized();
|
|
223
|
+
const result = db.exec('SELECT MAX(position) as pos FROM events');
|
|
224
|
+
if (result.length === 0 || result[0].values[0][0] === null) {
|
|
225
|
+
return 0n;
|
|
226
|
+
}
|
|
227
|
+
return BigInt(result[0].values[0][0]);
|
|
228
|
+
}
|
|
229
|
+
async close() {
|
|
230
|
+
const db = await this.ensureInitialized();
|
|
231
|
+
db.close();
|
|
232
|
+
this.db = null;
|
|
233
|
+
}
|
|
234
|
+
// --- UI Helper Methods (not part of core DCB API) ---
|
|
235
|
+
/**
|
|
236
|
+
* Get all events (for debugging/UI)
|
|
237
|
+
*/
|
|
238
|
+
async getAllEvents() {
|
|
239
|
+
const db = await this.ensureInitialized();
|
|
240
|
+
const result = db.exec(`
|
|
241
|
+
SELECT position, event_id, event_type, data, metadata, timestamp
|
|
242
|
+
FROM events
|
|
243
|
+
ORDER BY position ASC
|
|
244
|
+
`);
|
|
245
|
+
if (result.length === 0) {
|
|
246
|
+
return [];
|
|
247
|
+
}
|
|
248
|
+
// sql.js uses 'columns' or 'lc' depending on version
|
|
249
|
+
const columns = result[0].columns || result[0].lc;
|
|
250
|
+
const rows = result[0].values;
|
|
251
|
+
return rows.map((row) => {
|
|
252
|
+
const obj = {};
|
|
253
|
+
columns.forEach((col, i) => {
|
|
254
|
+
obj[col] = row[i];
|
|
255
|
+
});
|
|
256
|
+
return this.rowToEvent(obj);
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Get all keys (for debugging/UI)
|
|
261
|
+
*/
|
|
262
|
+
async getAllKeys() {
|
|
263
|
+
const db = await this.ensureInitialized();
|
|
264
|
+
const result = db.exec(`
|
|
265
|
+
SELECT position, key_name, key_value
|
|
266
|
+
FROM event_keys
|
|
267
|
+
ORDER BY position ASC
|
|
268
|
+
`);
|
|
269
|
+
if (result.length === 0)
|
|
270
|
+
return [];
|
|
271
|
+
return result[0].values.map((row) => ({
|
|
272
|
+
position: row[0],
|
|
273
|
+
key_name: row[1],
|
|
274
|
+
key_value: row[2]
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Clear all data (for testing)
|
|
279
|
+
*/
|
|
280
|
+
async clear() {
|
|
281
|
+
const db = await this.ensureInitialized();
|
|
282
|
+
db.run('DELETE FROM event_keys');
|
|
283
|
+
db.run('DELETE FROM events');
|
|
284
|
+
db.run("DELETE FROM sqlite_sequence WHERE name IN ('events', 'event_keys')");
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Export database as Uint8Array for persistence
|
|
288
|
+
*/
|
|
289
|
+
async export() {
|
|
290
|
+
const db = await this.ensureInitialized();
|
|
291
|
+
return db.export();
|
|
292
|
+
}
|
|
293
|
+
// --- Metadata Methods ---
|
|
294
|
+
/**
|
|
295
|
+
* Get stored config hash
|
|
296
|
+
*/
|
|
297
|
+
async getConfigHash() {
|
|
298
|
+
const db = await this.ensureInitialized();
|
|
299
|
+
const result = db.exec("SELECT value FROM metadata WHERE key = 'config_hash'");
|
|
300
|
+
if (result.length === 0 || result[0].values.length === 0) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
return result[0].values[0][0];
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Set config hash
|
|
307
|
+
*/
|
|
308
|
+
async setConfigHash(hash) {
|
|
309
|
+
if (!hash) {
|
|
310
|
+
console.warn('[SqlJsStorage] setConfigHash called with empty hash, skipping');
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const db = await this.ensureInitialized();
|
|
314
|
+
// sql.js ignores params - use escaped SQL
|
|
315
|
+
const escapedHash = hash.replace(/'/g, "''");
|
|
316
|
+
db.run(`INSERT OR REPLACE INTO metadata (key, value) VALUES ('config_hash', '${escapedHash}')`);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Reindex all events with new keys
|
|
320
|
+
*/
|
|
321
|
+
async reindex(extractKeys) {
|
|
322
|
+
const db = await this.ensureInitialized();
|
|
323
|
+
const events = await this.getAllEvents();
|
|
324
|
+
db.run('BEGIN TRANSACTION');
|
|
325
|
+
try {
|
|
326
|
+
// Clear all keys
|
|
327
|
+
db.run('DELETE FROM event_keys');
|
|
328
|
+
// Re-extract and insert keys for all events
|
|
329
|
+
const escapeSql = (s) => "'" + s.replace(/'/g, "''") + "'";
|
|
330
|
+
for (const event of events) {
|
|
331
|
+
const keys = extractKeys(event);
|
|
332
|
+
for (const key of keys) {
|
|
333
|
+
db.run(`INSERT INTO event_keys (position, key_name, key_value) VALUES (${Number(event.position)}, ${escapeSql(key.name)}, ${escapeSql(key.value)})`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
db.run('COMMIT');
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
db.run('ROLLBACK');
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
rowToEvent(row) {
|
|
344
|
+
return {
|
|
345
|
+
id: row.event_id,
|
|
346
|
+
type: row.event_type,
|
|
347
|
+
data: JSON.parse(row.data),
|
|
348
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
|
|
349
|
+
timestamp: new Date(row.timestamp),
|
|
350
|
+
position: BigInt(row.position),
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
//# sourceMappingURL=sqljs.js.map
|