@defai.digital/sqlite-adapter 13.0.3
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/LICENSE +214 -0
- package/dist/checkpoint-store.d.ts +80 -0
- package/dist/checkpoint-store.d.ts.map +1 -0
- package/dist/checkpoint-store.js +264 -0
- package/dist/checkpoint-store.js.map +1 -0
- package/dist/dead-letter-store.d.ts +82 -0
- package/dist/dead-letter-store.d.ts.map +1 -0
- package/dist/dead-letter-store.js +307 -0
- package/dist/dead-letter-store.js.map +1 -0
- package/dist/event-store.d.ts +68 -0
- package/dist/event-store.d.ts.map +1 -0
- package/dist/event-store.js +227 -0
- package/dist/event-store.js.map +1 -0
- package/dist/fts-store.d.ts +137 -0
- package/dist/fts-store.d.ts.map +1 -0
- package/dist/fts-store.js +312 -0
- package/dist/fts-store.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/trace-store.d.ts +74 -0
- package/dist/trace-store.d.ts.map +1 -0
- package/dist/trace-store.js +305 -0
- package/dist/trace-store.js.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite Dead Letter Storage Implementation
|
|
3
|
+
*
|
|
4
|
+
* Persistent storage for dead letter queue entries.
|
|
5
|
+
*
|
|
6
|
+
* Invariants:
|
|
7
|
+
* - INV-DLQ-001: Failed events captured with full context
|
|
8
|
+
* - INV-DLQ-002: Retries respect maxRetries limit
|
|
9
|
+
* - INV-DLQ-003: Exhausted entries marked appropriately
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Error thrown by SQLite dead letter store operations
|
|
13
|
+
*/
|
|
14
|
+
export class SqliteDeadLetterStoreError extends Error {
|
|
15
|
+
code;
|
|
16
|
+
details;
|
|
17
|
+
constructor(code, message, details) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.details = details;
|
|
21
|
+
this.name = 'SqliteDeadLetterStoreError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Error codes for SQLite dead letter store
|
|
26
|
+
*/
|
|
27
|
+
export const SqliteDeadLetterStoreErrorCodes = {
|
|
28
|
+
DATABASE_ERROR: 'SQLITE_DLQ_DATABASE_ERROR',
|
|
29
|
+
INVALID_ENTRY: 'SQLITE_DLQ_INVALID_ENTRY',
|
|
30
|
+
SERIALIZATION_ERROR: 'SQLITE_DLQ_SERIALIZATION_ERROR',
|
|
31
|
+
INVALID_TABLE_NAME: 'SQLITE_DLQ_INVALID_TABLE_NAME',
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Validates a SQL table name to prevent SQL injection
|
|
35
|
+
* Only allows alphanumeric characters and underscores, must start with letter or underscore
|
|
36
|
+
*/
|
|
37
|
+
function isValidTableName(name) {
|
|
38
|
+
return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name) && name.length <= 64;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* SQLite implementation of DeadLetterStorage
|
|
42
|
+
*
|
|
43
|
+
* Invariants:
|
|
44
|
+
* - INV-DLQ-001: All entry data is serialized and stored
|
|
45
|
+
* - INV-DLQ-002: Status transitions tracked via updates
|
|
46
|
+
* - INV-DLQ-003: countByStatus provides accurate status breakdown
|
|
47
|
+
*/
|
|
48
|
+
export class SqliteDeadLetterStorage {
|
|
49
|
+
db;
|
|
50
|
+
tableName;
|
|
51
|
+
constructor(db, tableName = 'dead_letter_entries') {
|
|
52
|
+
// Validate table name to prevent SQL injection
|
|
53
|
+
if (!isValidTableName(tableName)) {
|
|
54
|
+
throw new SqliteDeadLetterStoreError(SqliteDeadLetterStoreErrorCodes.INVALID_TABLE_NAME, `Invalid table name: ${tableName}. Must start with letter or underscore, contain only alphanumeric and underscores, max 64 chars.`);
|
|
55
|
+
}
|
|
56
|
+
this.db = db;
|
|
57
|
+
this.tableName = tableName;
|
|
58
|
+
this.initialize();
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Initializes the database schema
|
|
62
|
+
*/
|
|
63
|
+
initialize() {
|
|
64
|
+
this.db.exec(`
|
|
65
|
+
CREATE TABLE IF NOT EXISTS ${this.tableName} (
|
|
66
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
67
|
+
entry_id TEXT NOT NULL UNIQUE,
|
|
68
|
+
original_event_id TEXT NOT NULL,
|
|
69
|
+
event_type TEXT NOT NULL,
|
|
70
|
+
event_payload TEXT,
|
|
71
|
+
error TEXT NOT NULL,
|
|
72
|
+
retry_count INTEGER NOT NULL DEFAULT 0,
|
|
73
|
+
max_retries INTEGER NOT NULL,
|
|
74
|
+
last_retry_at TEXT,
|
|
75
|
+
next_retry_at TEXT,
|
|
76
|
+
status TEXT NOT NULL,
|
|
77
|
+
created_at TEXT NOT NULL,
|
|
78
|
+
correlation_id TEXT,
|
|
79
|
+
source TEXT NOT NULL,
|
|
80
|
+
context TEXT,
|
|
81
|
+
resolution_notes TEXT,
|
|
82
|
+
resolved_by TEXT,
|
|
83
|
+
resolved_at TEXT
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_status
|
|
87
|
+
ON ${this.tableName}(status);
|
|
88
|
+
|
|
89
|
+
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_source
|
|
90
|
+
ON ${this.tableName}(source);
|
|
91
|
+
|
|
92
|
+
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_event_type
|
|
93
|
+
ON ${this.tableName}(event_type);
|
|
94
|
+
|
|
95
|
+
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_created_at
|
|
96
|
+
ON ${this.tableName}(created_at DESC);
|
|
97
|
+
|
|
98
|
+
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_next_retry_at
|
|
99
|
+
ON ${this.tableName}(next_retry_at);
|
|
100
|
+
|
|
101
|
+
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_status_next_retry
|
|
102
|
+
ON ${this.tableName}(status, next_retry_at);
|
|
103
|
+
`);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Add entry to DLQ
|
|
107
|
+
* INV-DLQ-001: All context captured
|
|
108
|
+
*/
|
|
109
|
+
async add(entry) {
|
|
110
|
+
try {
|
|
111
|
+
const stmt = this.db.prepare(`
|
|
112
|
+
INSERT INTO ${this.tableName} (
|
|
113
|
+
entry_id, original_event_id, event_type, event_payload,
|
|
114
|
+
error, retry_count, max_retries, last_retry_at, next_retry_at,
|
|
115
|
+
status, created_at, correlation_id, source, context,
|
|
116
|
+
resolution_notes, resolved_by, resolved_at
|
|
117
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
118
|
+
`);
|
|
119
|
+
stmt.run(entry.entryId, entry.originalEventId, entry.eventType, entry.eventPayload !== undefined ? JSON.stringify(entry.eventPayload) : null, JSON.stringify(entry.error), entry.retryCount, entry.maxRetries, entry.lastRetryAt ?? null, entry.nextRetryAt ?? null, entry.status, entry.createdAt, entry.correlationId ?? null, entry.source, entry.context !== undefined ? JSON.stringify(entry.context) : null, entry.resolutionNotes ?? null, entry.resolvedBy ?? null, entry.resolvedAt ?? null);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
throw new SqliteDeadLetterStoreError(SqliteDeadLetterStoreErrorCodes.DATABASE_ERROR, `Failed to add DLQ entry: ${error instanceof Error ? error.message : 'Unknown error'}`, { entryId: entry.entryId });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Get entry by ID
|
|
127
|
+
*/
|
|
128
|
+
async get(entryId) {
|
|
129
|
+
try {
|
|
130
|
+
const stmt = this.db.prepare(`
|
|
131
|
+
SELECT entry_id, original_event_id, event_type, event_payload,
|
|
132
|
+
error, retry_count, max_retries, last_retry_at, next_retry_at,
|
|
133
|
+
status, created_at, correlation_id, source, context,
|
|
134
|
+
resolution_notes, resolved_by, resolved_at
|
|
135
|
+
FROM ${this.tableName}
|
|
136
|
+
WHERE entry_id = ?
|
|
137
|
+
`);
|
|
138
|
+
const row = stmt.get(entryId);
|
|
139
|
+
return row !== undefined ? rowToDeadLetterEntry(row) : null;
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
throw new SqliteDeadLetterStoreError(SqliteDeadLetterStoreErrorCodes.DATABASE_ERROR, `Failed to get DLQ entry: ${error instanceof Error ? error.message : 'Unknown error'}`, { entryId });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* List entries with optional status filter
|
|
147
|
+
*/
|
|
148
|
+
async list(status) {
|
|
149
|
+
try {
|
|
150
|
+
let sql = `
|
|
151
|
+
SELECT entry_id, original_event_id, event_type, event_payload,
|
|
152
|
+
error, retry_count, max_retries, last_retry_at, next_retry_at,
|
|
153
|
+
status, created_at, correlation_id, source, context,
|
|
154
|
+
resolution_notes, resolved_by, resolved_at
|
|
155
|
+
FROM ${this.tableName}
|
|
156
|
+
`;
|
|
157
|
+
const params = [];
|
|
158
|
+
if (status !== undefined) {
|
|
159
|
+
sql += ' WHERE status = ?';
|
|
160
|
+
params.push(status);
|
|
161
|
+
}
|
|
162
|
+
sql += ' ORDER BY created_at DESC';
|
|
163
|
+
const stmt = this.db.prepare(sql);
|
|
164
|
+
const rows = (params.length > 0 ? stmt.all(...params) : stmt.all());
|
|
165
|
+
return rows.map(rowToDeadLetterEntry);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
throw new SqliteDeadLetterStoreError(SqliteDeadLetterStoreErrorCodes.DATABASE_ERROR, `Failed to list DLQ entries: ${error instanceof Error ? error.message : 'Unknown error'}`, { status });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Update entry
|
|
173
|
+
* INV-DLQ-002, INV-DLQ-003: Status transitions handled
|
|
174
|
+
*/
|
|
175
|
+
async update(entry) {
|
|
176
|
+
try {
|
|
177
|
+
const stmt = this.db.prepare(`
|
|
178
|
+
UPDATE ${this.tableName} SET
|
|
179
|
+
original_event_id = ?,
|
|
180
|
+
event_type = ?,
|
|
181
|
+
event_payload = ?,
|
|
182
|
+
error = ?,
|
|
183
|
+
retry_count = ?,
|
|
184
|
+
max_retries = ?,
|
|
185
|
+
last_retry_at = ?,
|
|
186
|
+
next_retry_at = ?,
|
|
187
|
+
status = ?,
|
|
188
|
+
correlation_id = ?,
|
|
189
|
+
source = ?,
|
|
190
|
+
context = ?,
|
|
191
|
+
resolution_notes = ?,
|
|
192
|
+
resolved_by = ?,
|
|
193
|
+
resolved_at = ?
|
|
194
|
+
WHERE entry_id = ?
|
|
195
|
+
`);
|
|
196
|
+
stmt.run(entry.originalEventId, entry.eventType, entry.eventPayload !== undefined ? JSON.stringify(entry.eventPayload) : null, JSON.stringify(entry.error), entry.retryCount, entry.maxRetries, entry.lastRetryAt ?? null, entry.nextRetryAt ?? null, entry.status, entry.correlationId ?? null, entry.source, entry.context !== undefined ? JSON.stringify(entry.context) : null, entry.resolutionNotes ?? null, entry.resolvedBy ?? null, entry.resolvedAt ?? null, entry.entryId);
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
throw new SqliteDeadLetterStoreError(SqliteDeadLetterStoreErrorCodes.DATABASE_ERROR, `Failed to update DLQ entry: ${error instanceof Error ? error.message : 'Unknown error'}`, { entryId: entry.entryId });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Delete entry
|
|
204
|
+
*/
|
|
205
|
+
async delete(entryId) {
|
|
206
|
+
try {
|
|
207
|
+
const stmt = this.db.prepare(`
|
|
208
|
+
DELETE FROM ${this.tableName} WHERE entry_id = ?
|
|
209
|
+
`);
|
|
210
|
+
const result = stmt.run(entryId);
|
|
211
|
+
return result.changes > 0;
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
throw new SqliteDeadLetterStoreError(SqliteDeadLetterStoreErrorCodes.DATABASE_ERROR, `Failed to delete DLQ entry: ${error instanceof Error ? error.message : 'Unknown error'}`, { entryId });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Count entries by status
|
|
219
|
+
*/
|
|
220
|
+
async countByStatus() {
|
|
221
|
+
try {
|
|
222
|
+
const stmt = this.db.prepare(`
|
|
223
|
+
SELECT status, COUNT(*) as count
|
|
224
|
+
FROM ${this.tableName}
|
|
225
|
+
GROUP BY status
|
|
226
|
+
`);
|
|
227
|
+
const rows = stmt.all();
|
|
228
|
+
const counts = {
|
|
229
|
+
pending: 0,
|
|
230
|
+
retrying: 0,
|
|
231
|
+
exhausted: 0,
|
|
232
|
+
resolved: 0,
|
|
233
|
+
discarded: 0,
|
|
234
|
+
};
|
|
235
|
+
for (const row of rows) {
|
|
236
|
+
const status = row.status;
|
|
237
|
+
if (status in counts) {
|
|
238
|
+
counts[status] = row.count;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return counts;
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
throw new SqliteDeadLetterStoreError(SqliteDeadLetterStoreErrorCodes.DATABASE_ERROR, `Failed to count DLQ entries by status: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Clears all entries (for testing only)
|
|
249
|
+
*/
|
|
250
|
+
clear() {
|
|
251
|
+
this.db.exec(`DELETE FROM ${this.tableName}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Safely parses JSON with error handling
|
|
256
|
+
*/
|
|
257
|
+
function safeJsonParse(json, fieldName, entryId) {
|
|
258
|
+
try {
|
|
259
|
+
return JSON.parse(json);
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
throw new SqliteDeadLetterStoreError(SqliteDeadLetterStoreErrorCodes.SERIALIZATION_ERROR, `Failed to parse ${fieldName} for entry ${entryId}: ${error instanceof Error ? error.message : 'Invalid JSON'}`, { entryId, fieldName });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function rowToDeadLetterEntry(row) {
|
|
266
|
+
const entry = {
|
|
267
|
+
entryId: row.entry_id,
|
|
268
|
+
originalEventId: row.original_event_id,
|
|
269
|
+
eventType: row.event_type,
|
|
270
|
+
eventPayload: row.event_payload !== null ? safeJsonParse(row.event_payload, 'eventPayload', row.entry_id) : undefined,
|
|
271
|
+
error: safeJsonParse(row.error, 'error', row.entry_id),
|
|
272
|
+
retryCount: row.retry_count,
|
|
273
|
+
maxRetries: row.max_retries,
|
|
274
|
+
status: row.status,
|
|
275
|
+
createdAt: row.created_at,
|
|
276
|
+
source: row.source,
|
|
277
|
+
};
|
|
278
|
+
if (row.last_retry_at !== null) {
|
|
279
|
+
entry.lastRetryAt = row.last_retry_at;
|
|
280
|
+
}
|
|
281
|
+
if (row.next_retry_at !== null) {
|
|
282
|
+
entry.nextRetryAt = row.next_retry_at;
|
|
283
|
+
}
|
|
284
|
+
if (row.correlation_id !== null) {
|
|
285
|
+
entry.correlationId = row.correlation_id;
|
|
286
|
+
}
|
|
287
|
+
if (row.context !== null) {
|
|
288
|
+
entry.context = safeJsonParse(row.context, 'context', row.entry_id);
|
|
289
|
+
}
|
|
290
|
+
if (row.resolution_notes !== null) {
|
|
291
|
+
entry.resolutionNotes = row.resolution_notes;
|
|
292
|
+
}
|
|
293
|
+
if (row.resolved_by !== null) {
|
|
294
|
+
entry.resolvedBy = row.resolved_by;
|
|
295
|
+
}
|
|
296
|
+
if (row.resolved_at !== null) {
|
|
297
|
+
entry.resolvedAt = row.resolved_at;
|
|
298
|
+
}
|
|
299
|
+
return entry;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Creates a SQLite dead letter storage
|
|
303
|
+
*/
|
|
304
|
+
export function createSqliteDeadLetterStorage(db, tableName) {
|
|
305
|
+
return new SqliteDeadLetterStorage(db, tableName);
|
|
306
|
+
}
|
|
307
|
+
//# sourceMappingURL=dead-letter-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dead-letter-store.js","sourceRoot":"","sources":["../src/dead-letter-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { MemoryEvent, MemoryEventType } from '@defai.digital/contracts';
|
|
2
|
+
import type { EventStore } from '@defai.digital/memory-domain';
|
|
3
|
+
import type Database from 'better-sqlite3';
|
|
4
|
+
/**
|
|
5
|
+
* Error thrown by SQLite event store operations
|
|
6
|
+
*/
|
|
7
|
+
export declare class SqliteEventStoreError extends Error {
|
|
8
|
+
readonly code: string;
|
|
9
|
+
readonly details?: Record<string, unknown> | undefined;
|
|
10
|
+
constructor(code: string, message: string, details?: Record<string, unknown> | undefined);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Error codes for SQLite event store
|
|
14
|
+
*/
|
|
15
|
+
export declare const SqliteEventStoreErrorCodes: {
|
|
16
|
+
readonly VERSION_CONFLICT: "SQLITE_VERSION_CONFLICT";
|
|
17
|
+
readonly INVALID_EVENT: "SQLITE_INVALID_EVENT";
|
|
18
|
+
readonly DATABASE_ERROR: "SQLITE_DATABASE_ERROR";
|
|
19
|
+
readonly INVALID_TABLE_NAME: "SQLITE_INVALID_TABLE_NAME";
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* SQLite implementation of EventStore
|
|
23
|
+
* INV-MEM-001: Events are immutable - stored as read-only rows
|
|
24
|
+
* INV-MEM-003: Adapter does not accept domain objects directly (uses MemoryEvent contract)
|
|
25
|
+
* INV-MEM-004: Events are ordered by version within aggregate
|
|
26
|
+
*/
|
|
27
|
+
export declare class SqliteEventStore implements EventStore {
|
|
28
|
+
private readonly db;
|
|
29
|
+
private readonly tableName;
|
|
30
|
+
constructor(db: Database.Database, tableName?: string);
|
|
31
|
+
/**
|
|
32
|
+
* Initializes the database schema
|
|
33
|
+
*/
|
|
34
|
+
private initialize;
|
|
35
|
+
/**
|
|
36
|
+
* Appends an event to the store
|
|
37
|
+
* INV-MEM-001: Events are immutable once stored
|
|
38
|
+
* INV-MEM-004: Version ordering enforced via constraint
|
|
39
|
+
*/
|
|
40
|
+
append(event: MemoryEvent): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Gets all events for an aggregate
|
|
43
|
+
* INV-MEM-004: Events returned in version order
|
|
44
|
+
*/
|
|
45
|
+
getEvents(aggregateId: string): Promise<MemoryEvent[]>;
|
|
46
|
+
/**
|
|
47
|
+
* Gets events by type
|
|
48
|
+
*/
|
|
49
|
+
getEventsByType(type: MemoryEventType): Promise<MemoryEvent[]>;
|
|
50
|
+
/**
|
|
51
|
+
* Gets events by correlation ID
|
|
52
|
+
* INV-MEM-005: Support correlation tracing
|
|
53
|
+
*/
|
|
54
|
+
getEventsByCorrelation(correlationId: string): Promise<MemoryEvent[]>;
|
|
55
|
+
/**
|
|
56
|
+
* Gets the current version for an aggregate
|
|
57
|
+
*/
|
|
58
|
+
getVersion(aggregateId: string): Promise<number>;
|
|
59
|
+
/**
|
|
60
|
+
* Clears all events (for testing only)
|
|
61
|
+
*/
|
|
62
|
+
clear(): void;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Creates a SQLite event store
|
|
66
|
+
*/
|
|
67
|
+
export declare function createSqliteEventStore(db: Database.Database, tableName?: string): SqliteEventStore;
|
|
68
|
+
//# sourceMappingURL=event-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-store.d.ts","sourceRoot":"","sources":["../src/event-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC7E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAC/D,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAE3C;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;aAE5B,IAAI,EAAE,MAAM;aAEZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;gBAFjC,IAAI,EAAE,MAAM,EAC5B,OAAO,EAAE,MAAM,EACC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,YAAA;CAKpD;AAED;;GAEG;AACH,eAAO,MAAM,0BAA0B;;;;;CAK7B,CAAC;AAUX;;;;;GAKG;AACH,qBAAa,gBAAiB,YAAW,UAAU;IACjD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAoB;IACvC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAEvB,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE,SAAS,SAAkB;IAa9D;;OAEG;IACH,OAAO,CAAC,UAAU;IA4BlB;;;;OAIG;IACG,MAAM,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAwE/C;;;OAGG;IACH,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAYtD;;OAEG;IACH,eAAe,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAY9D;;;OAGG;IACH,sBAAsB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAYrE;;OAEG;IACH,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAgBhD;;OAEG;IACH,KAAK,IAAI,IAAI;CAGd;AAsDD;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,SAAS,CAAC,EAAE,MAAM,GACjB,gBAAgB,CAElB"}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error thrown by SQLite event store operations
|
|
3
|
+
*/
|
|
4
|
+
export class SqliteEventStoreError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
details;
|
|
7
|
+
constructor(code, message, details) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.details = details;
|
|
11
|
+
this.name = 'SqliteEventStoreError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Error codes for SQLite event store
|
|
16
|
+
*/
|
|
17
|
+
export const SqliteEventStoreErrorCodes = {
|
|
18
|
+
VERSION_CONFLICT: 'SQLITE_VERSION_CONFLICT',
|
|
19
|
+
INVALID_EVENT: 'SQLITE_INVALID_EVENT',
|
|
20
|
+
DATABASE_ERROR: 'SQLITE_DATABASE_ERROR',
|
|
21
|
+
INVALID_TABLE_NAME: 'SQLITE_INVALID_TABLE_NAME',
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Validates a SQL table name to prevent SQL injection
|
|
25
|
+
* Only allows alphanumeric characters and underscores, must start with letter or underscore
|
|
26
|
+
*/
|
|
27
|
+
function isValidTableName(name) {
|
|
28
|
+
return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name) && name.length <= 64;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* SQLite implementation of EventStore
|
|
32
|
+
* INV-MEM-001: Events are immutable - stored as read-only rows
|
|
33
|
+
* INV-MEM-003: Adapter does not accept domain objects directly (uses MemoryEvent contract)
|
|
34
|
+
* INV-MEM-004: Events are ordered by version within aggregate
|
|
35
|
+
*/
|
|
36
|
+
export class SqliteEventStore {
|
|
37
|
+
db;
|
|
38
|
+
tableName;
|
|
39
|
+
constructor(db, tableName = 'memory_events') {
|
|
40
|
+
// Validate table name to prevent SQL injection
|
|
41
|
+
if (!isValidTableName(tableName)) {
|
|
42
|
+
throw new SqliteEventStoreError(SqliteEventStoreErrorCodes.INVALID_TABLE_NAME, `Invalid table name: ${tableName}. Must start with letter or underscore, contain only alphanumeric and underscores, max 64 chars.`);
|
|
43
|
+
}
|
|
44
|
+
this.db = db;
|
|
45
|
+
this.tableName = tableName;
|
|
46
|
+
this.initialize();
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Initializes the database schema
|
|
50
|
+
*/
|
|
51
|
+
initialize() {
|
|
52
|
+
this.db.exec(`
|
|
53
|
+
CREATE TABLE IF NOT EXISTS ${this.tableName} (
|
|
54
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
55
|
+
event_id TEXT NOT NULL UNIQUE,
|
|
56
|
+
type TEXT NOT NULL,
|
|
57
|
+
aggregate_id TEXT,
|
|
58
|
+
version INTEGER,
|
|
59
|
+
timestamp TEXT NOT NULL,
|
|
60
|
+
payload TEXT NOT NULL,
|
|
61
|
+
metadata TEXT,
|
|
62
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_aggregate_id
|
|
66
|
+
ON ${this.tableName}(aggregate_id);
|
|
67
|
+
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_type
|
|
69
|
+
ON ${this.tableName}(type);
|
|
70
|
+
|
|
71
|
+
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_correlation_id
|
|
72
|
+
ON ${this.tableName}(json_extract(metadata, '$.correlationId'));
|
|
73
|
+
|
|
74
|
+
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_aggregate_version
|
|
75
|
+
ON ${this.tableName}(aggregate_id, version);
|
|
76
|
+
`);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Appends an event to the store
|
|
80
|
+
* INV-MEM-001: Events are immutable once stored
|
|
81
|
+
* INV-MEM-004: Version ordering enforced via constraint
|
|
82
|
+
*/
|
|
83
|
+
async append(event) {
|
|
84
|
+
// Validate event
|
|
85
|
+
if (event.eventId === '') {
|
|
86
|
+
throw new SqliteEventStoreError(SqliteEventStoreErrorCodes.INVALID_EVENT, 'Event must have an eventId');
|
|
87
|
+
}
|
|
88
|
+
const aggregateId = event.aggregateId ?? 'global';
|
|
89
|
+
// Use a transaction to atomically check version and insert
|
|
90
|
+
// This prevents TOCTOU race conditions between version check and insert
|
|
91
|
+
const transaction = this.db.transaction(() => {
|
|
92
|
+
// Check version ordering if version is specified (INV-MEM-004)
|
|
93
|
+
if (event.version !== undefined) {
|
|
94
|
+
const versionStmt = this.db.prepare(`
|
|
95
|
+
SELECT MAX(version) as max_version, COUNT(*) as count
|
|
96
|
+
FROM ${this.tableName}
|
|
97
|
+
WHERE aggregate_id = ?
|
|
98
|
+
`);
|
|
99
|
+
const row = versionStmt.get(aggregateId);
|
|
100
|
+
const currentVersion = row.count === 0 ? 0 : (row.max_version ?? row.count);
|
|
101
|
+
if (event.version !== currentVersion + 1) {
|
|
102
|
+
throw new SqliteEventStoreError(SqliteEventStoreErrorCodes.VERSION_CONFLICT, `Version conflict: expected ${String(currentVersion + 1)}, got ${String(event.version)}`, { expected: currentVersion + 1, actual: event.version });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const stmt = this.db.prepare(`
|
|
106
|
+
INSERT INTO ${this.tableName} (
|
|
107
|
+
event_id, type, aggregate_id, version, timestamp, payload, metadata
|
|
108
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
109
|
+
`);
|
|
110
|
+
stmt.run(event.eventId, event.type, aggregateId, event.version ?? null, event.timestamp, JSON.stringify(event.payload), event.metadata !== undefined ? JSON.stringify(event.metadata) : null);
|
|
111
|
+
});
|
|
112
|
+
try {
|
|
113
|
+
transaction();
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (error instanceof SqliteEventStoreError) {
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
if (error instanceof Error &&
|
|
120
|
+
error.message.includes('UNIQUE constraint failed')) {
|
|
121
|
+
throw new SqliteEventStoreError(SqliteEventStoreErrorCodes.INVALID_EVENT, `Event with ID ${event.eventId} already exists`);
|
|
122
|
+
}
|
|
123
|
+
throw new SqliteEventStoreError(SqliteEventStoreErrorCodes.DATABASE_ERROR, `Failed to append event: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Gets all events for an aggregate
|
|
128
|
+
* INV-MEM-004: Events returned in version order
|
|
129
|
+
*/
|
|
130
|
+
getEvents(aggregateId) {
|
|
131
|
+
const stmt = this.db.prepare(`
|
|
132
|
+
SELECT event_id, type, aggregate_id, version, timestamp, payload, metadata
|
|
133
|
+
FROM ${this.tableName}
|
|
134
|
+
WHERE aggregate_id = ?
|
|
135
|
+
ORDER BY COALESCE(version, id)
|
|
136
|
+
`);
|
|
137
|
+
const rows = stmt.all(aggregateId);
|
|
138
|
+
return Promise.resolve(rows.map(rowToEvent));
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Gets events by type
|
|
142
|
+
*/
|
|
143
|
+
getEventsByType(type) {
|
|
144
|
+
const stmt = this.db.prepare(`
|
|
145
|
+
SELECT event_id, type, aggregate_id, version, timestamp, payload, metadata
|
|
146
|
+
FROM ${this.tableName}
|
|
147
|
+
WHERE type = ?
|
|
148
|
+
ORDER BY id
|
|
149
|
+
`);
|
|
150
|
+
const rows = stmt.all(type);
|
|
151
|
+
return Promise.resolve(rows.map(rowToEvent));
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Gets events by correlation ID
|
|
155
|
+
* INV-MEM-005: Support correlation tracing
|
|
156
|
+
*/
|
|
157
|
+
getEventsByCorrelation(correlationId) {
|
|
158
|
+
const stmt = this.db.prepare(`
|
|
159
|
+
SELECT event_id, type, aggregate_id, version, timestamp, payload, metadata
|
|
160
|
+
FROM ${this.tableName}
|
|
161
|
+
WHERE json_extract(metadata, '$.correlationId') = ?
|
|
162
|
+
ORDER BY id
|
|
163
|
+
`);
|
|
164
|
+
const rows = stmt.all(correlationId);
|
|
165
|
+
return Promise.resolve(rows.map(rowToEvent));
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Gets the current version for an aggregate
|
|
169
|
+
*/
|
|
170
|
+
getVersion(aggregateId) {
|
|
171
|
+
const stmt = this.db.prepare(`
|
|
172
|
+
SELECT MAX(version) as max_version, COUNT(*) as count
|
|
173
|
+
FROM ${this.tableName}
|
|
174
|
+
WHERE aggregate_id = ?
|
|
175
|
+
`);
|
|
176
|
+
const row = stmt.get(aggregateId);
|
|
177
|
+
if (row.count === 0) {
|
|
178
|
+
return Promise.resolve(0);
|
|
179
|
+
}
|
|
180
|
+
return Promise.resolve(row.max_version ?? row.count);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Clears all events (for testing only)
|
|
184
|
+
*/
|
|
185
|
+
clear() {
|
|
186
|
+
this.db.exec(`DELETE FROM ${this.tableName}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Safely parses JSON with error handling
|
|
191
|
+
*/
|
|
192
|
+
function safeJsonParse(json, fieldName, eventId) {
|
|
193
|
+
try {
|
|
194
|
+
return JSON.parse(json);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
throw new SqliteEventStoreError(SqliteEventStoreErrorCodes.DATABASE_ERROR, `Failed to parse ${fieldName} for event ${eventId}: ${error instanceof Error ? error.message : 'Invalid JSON'}`, { eventId, fieldName });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Converts a database row to a MemoryEvent
|
|
202
|
+
*/
|
|
203
|
+
function rowToEvent(row) {
|
|
204
|
+
const event = {
|
|
205
|
+
eventId: row.event_id,
|
|
206
|
+
type: row.type,
|
|
207
|
+
timestamp: row.timestamp,
|
|
208
|
+
payload: safeJsonParse(row.payload, 'payload', row.event_id),
|
|
209
|
+
};
|
|
210
|
+
if (row.aggregate_id !== null) {
|
|
211
|
+
event.aggregateId = row.aggregate_id;
|
|
212
|
+
}
|
|
213
|
+
if (row.version !== null) {
|
|
214
|
+
event.version = row.version;
|
|
215
|
+
}
|
|
216
|
+
if (row.metadata !== null) {
|
|
217
|
+
event.metadata = safeJsonParse(row.metadata, 'metadata', row.event_id);
|
|
218
|
+
}
|
|
219
|
+
return event;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Creates a SQLite event store
|
|
223
|
+
*/
|
|
224
|
+
export function createSqliteEventStore(db, tableName) {
|
|
225
|
+
return new SqliteEventStore(db, tableName);
|
|
226
|
+
}
|
|
227
|
+
//# sourceMappingURL=event-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-store.js","sourceRoot":"","sources":["../src/event-store.ts"],"names":[],"mappings":"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"}
|