mongolite-ts 0.4.0 → 0.6.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/README.md +223 -0
- package/dist/bin/mongolite-debug.js +2 -2
- package/dist/bin/mongolite-debug.js.map +1 -1
- package/dist/changeStream.d.ts +114 -0
- package/dist/changeStream.js +338 -0
- package/dist/changeStream.js.map +1 -0
- package/dist/collection.d.ts +26 -0
- package/dist/collection.js +188 -9
- package/dist/collection.js.map +1 -1
- package/dist/cursors/findCursor.js +112 -5
- package/dist/cursors/findCursor.js.map +1 -1
- package/dist/database-manager.d.ts +1 -0
- package/dist/database-manager.js +2 -0
- package/dist/database-manager.js.map +1 -0
- package/dist/debugger/query-debugger.js +2 -2
- package/dist/debugger/query-debugger.js.map +1 -1
- package/dist/document-utils.d.ts +34 -0
- package/dist/document-utils.js +101 -0
- package/dist/document-utils.js.map +1 -0
- package/dist/find-cursor.d.ts +51 -0
- package/dist/find-cursor.js +204 -0
- package/dist/find-cursor.js.map +1 -0
- package/dist/index.d.ts +10 -3
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/plugins/mongodbSync.d.ts +128 -0
- package/dist/plugins/mongodbSync.js +339 -0
- package/dist/plugins/mongodbSync.js.map +1 -0
- package/dist/query-builder.d.ts +18 -0
- package/dist/query-builder.js +358 -0
- package/dist/query-builder.js.map +1 -0
- package/dist/types.d.ts +1 -0
- package/dist/update-operations.d.ts +17 -0
- package/dist/update-operations.js +147 -0
- package/dist/update-operations.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
export class ChangeStream extends EventEmitter {
|
|
3
|
+
constructor(db, collectionName, options = {}) {
|
|
4
|
+
super();
|
|
5
|
+
this.db = db;
|
|
6
|
+
this.collectionName = collectionName;
|
|
7
|
+
this.options = options;
|
|
8
|
+
this.closed = false;
|
|
9
|
+
this.pollInterval = null;
|
|
10
|
+
this.lastProcessedId = 0;
|
|
11
|
+
this.buffer = [];
|
|
12
|
+
this.maxBufferSize = options.maxBufferSize || 1000;
|
|
13
|
+
this.setupChangeTracking();
|
|
14
|
+
this.startPolling();
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Sets up the change tracking infrastructure
|
|
18
|
+
*/
|
|
19
|
+
async setupChangeTracking() {
|
|
20
|
+
await this.ensureChangeLogTable();
|
|
21
|
+
await this.setupTriggers();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Creates the change log table if it doesn't exist
|
|
25
|
+
*/
|
|
26
|
+
async ensureChangeLogTable() {
|
|
27
|
+
const createTableSQL = `
|
|
28
|
+
CREATE TABLE IF NOT EXISTS __mongolite_changes__ (
|
|
29
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
30
|
+
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
31
|
+
operation_type TEXT NOT NULL,
|
|
32
|
+
collection_name TEXT NOT NULL,
|
|
33
|
+
document_id TEXT NOT NULL,
|
|
34
|
+
full_document TEXT,
|
|
35
|
+
full_document_before TEXT,
|
|
36
|
+
updated_fields TEXT,
|
|
37
|
+
removed_fields TEXT,
|
|
38
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
39
|
+
);
|
|
40
|
+
`;
|
|
41
|
+
const createIndexSQL = `
|
|
42
|
+
CREATE INDEX IF NOT EXISTS idx_mongolite_changes_collection_timestamp
|
|
43
|
+
ON __mongolite_changes__ (collection_name, timestamp);
|
|
44
|
+
`;
|
|
45
|
+
await this.db.exec(createTableSQL);
|
|
46
|
+
await this.db.exec(createIndexSQL);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Sets up triggers for tracking changes on the collection
|
|
50
|
+
*/
|
|
51
|
+
async setupTriggers() {
|
|
52
|
+
const collectionName = this.collectionName;
|
|
53
|
+
// Drop existing triggers for this collection
|
|
54
|
+
await this.db.exec(`DROP TRIGGER IF EXISTS "${collectionName}_insert_trigger"`);
|
|
55
|
+
await this.db.exec(`DROP TRIGGER IF EXISTS "${collectionName}_update_trigger"`);
|
|
56
|
+
await this.db.exec(`DROP TRIGGER IF EXISTS "${collectionName}_delete_trigger"`);
|
|
57
|
+
// Insert trigger
|
|
58
|
+
const insertTriggerSQL = `
|
|
59
|
+
CREATE TRIGGER "${collectionName}_insert_trigger"
|
|
60
|
+
AFTER INSERT ON "${collectionName}"
|
|
61
|
+
FOR EACH ROW
|
|
62
|
+
BEGIN
|
|
63
|
+
INSERT INTO __mongolite_changes__ (
|
|
64
|
+
operation_type, collection_name, document_id, full_document
|
|
65
|
+
) VALUES (
|
|
66
|
+
'insert', '${collectionName}', NEW._id, NEW.data
|
|
67
|
+
);
|
|
68
|
+
END;
|
|
69
|
+
`;
|
|
70
|
+
// Update trigger
|
|
71
|
+
const updateTriggerSQL = `
|
|
72
|
+
CREATE TRIGGER "${collectionName}_update_trigger"
|
|
73
|
+
AFTER UPDATE ON "${collectionName}"
|
|
74
|
+
FOR EACH ROW
|
|
75
|
+
BEGIN
|
|
76
|
+
INSERT INTO __mongolite_changes__ (
|
|
77
|
+
operation_type, collection_name, document_id,
|
|
78
|
+
full_document, full_document_before
|
|
79
|
+
) VALUES (
|
|
80
|
+
'update', '${collectionName}', NEW._id,
|
|
81
|
+
NEW.data, OLD.data
|
|
82
|
+
);
|
|
83
|
+
END;
|
|
84
|
+
`;
|
|
85
|
+
// Delete trigger
|
|
86
|
+
const deleteTriggerSQL = `
|
|
87
|
+
CREATE TRIGGER "${collectionName}_delete_trigger"
|
|
88
|
+
AFTER DELETE ON "${collectionName}"
|
|
89
|
+
FOR EACH ROW
|
|
90
|
+
BEGIN
|
|
91
|
+
INSERT INTO __mongolite_changes__ (
|
|
92
|
+
operation_type, collection_name, document_id, full_document_before
|
|
93
|
+
) VALUES (
|
|
94
|
+
'delete', '${collectionName}', OLD._id, OLD.data
|
|
95
|
+
);
|
|
96
|
+
END;
|
|
97
|
+
`;
|
|
98
|
+
await this.db.exec(insertTriggerSQL);
|
|
99
|
+
await this.db.exec(updateTriggerSQL);
|
|
100
|
+
await this.db.exec(deleteTriggerSQL);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Starts polling for new changes
|
|
104
|
+
*/
|
|
105
|
+
startPolling() {
|
|
106
|
+
if (this.closed)
|
|
107
|
+
return;
|
|
108
|
+
this.pollInterval = setInterval(async () => {
|
|
109
|
+
try {
|
|
110
|
+
await this.pollForChanges();
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
this.emit('error', error);
|
|
114
|
+
}
|
|
115
|
+
}, 100); // Poll every 100ms
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Polls for new changes from the change log
|
|
119
|
+
*/
|
|
120
|
+
async pollForChanges() {
|
|
121
|
+
if (this.closed)
|
|
122
|
+
return;
|
|
123
|
+
const changes = await this.db.all(`SELECT * FROM __mongolite_changes__
|
|
124
|
+
WHERE collection_name = ? AND id > ?
|
|
125
|
+
ORDER BY id ASC LIMIT 100`, [this.collectionName, this.lastProcessedId]);
|
|
126
|
+
for (const change of changes) {
|
|
127
|
+
if (this.closed)
|
|
128
|
+
break;
|
|
129
|
+
const changeDoc = await this.transformChangeEvent(change);
|
|
130
|
+
if (this.passesFilter(changeDoc)) {
|
|
131
|
+
this.buffer.push(changeDoc);
|
|
132
|
+
// Emit the change event
|
|
133
|
+
this.emit('change', changeDoc);
|
|
134
|
+
// Clean buffer if it gets too large
|
|
135
|
+
if (this.buffer.length > this.maxBufferSize) {
|
|
136
|
+
this.buffer = this.buffer.slice(-this.maxBufferSize);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
this.lastProcessedId = change.id;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Transforms a raw change event from the database into a ChangeStreamDocument
|
|
144
|
+
*/
|
|
145
|
+
async transformChangeEvent(change) {
|
|
146
|
+
const fullDocument = change.full_document ? JSON.parse(change.full_document) : undefined;
|
|
147
|
+
const fullDocumentBefore = change.full_document_before
|
|
148
|
+
? JSON.parse(change.full_document_before)
|
|
149
|
+
: undefined;
|
|
150
|
+
let updateDescription;
|
|
151
|
+
if (change.operation_type === 'update' && fullDocument && fullDocumentBefore) {
|
|
152
|
+
updateDescription = this.computeUpdateDescription(fullDocumentBefore, fullDocument);
|
|
153
|
+
}
|
|
154
|
+
const changeDoc = {
|
|
155
|
+
_id: `${change.id}`, // Use the change log ID as the change stream document ID
|
|
156
|
+
operationType: change.operation_type,
|
|
157
|
+
clusterTime: new Date(change.timestamp),
|
|
158
|
+
documentKey: { _id: change.document_id },
|
|
159
|
+
ns: {
|
|
160
|
+
db: 'mongolite', // You could make this configurable
|
|
161
|
+
coll: change.collection_name,
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
// Add full document based on options
|
|
165
|
+
if (this.shouldIncludeFullDocument(change.operation_type)) {
|
|
166
|
+
changeDoc.fullDocument = fullDocument
|
|
167
|
+
? { _id: change.document_id, ...fullDocument }
|
|
168
|
+
: undefined;
|
|
169
|
+
}
|
|
170
|
+
// Add full document before change based on options
|
|
171
|
+
if (this.shouldIncludeFullDocumentBefore(change.operation_type)) {
|
|
172
|
+
changeDoc.fullDocumentBeforeChange = fullDocumentBefore
|
|
173
|
+
? { _id: change.document_id, ...fullDocumentBefore }
|
|
174
|
+
: undefined;
|
|
175
|
+
}
|
|
176
|
+
if (updateDescription) {
|
|
177
|
+
changeDoc.updateDescription = updateDescription;
|
|
178
|
+
}
|
|
179
|
+
return changeDoc;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Computes the update description by comparing before and after documents
|
|
183
|
+
*/
|
|
184
|
+
computeUpdateDescription(before, after) {
|
|
185
|
+
const updatedFields = {};
|
|
186
|
+
const removedFields = [];
|
|
187
|
+
// Type guard to ensure we have objects to compare
|
|
188
|
+
if (typeof before !== 'object' ||
|
|
189
|
+
before === null ||
|
|
190
|
+
typeof after !== 'object' ||
|
|
191
|
+
after === null) {
|
|
192
|
+
return { updatedFields, removedFields };
|
|
193
|
+
}
|
|
194
|
+
// Simple comparison - this could be made more sophisticated
|
|
195
|
+
const beforeObj = before;
|
|
196
|
+
const afterObj = after;
|
|
197
|
+
const allKeys = new Set([...Object.keys(beforeObj), ...Object.keys(afterObj)]);
|
|
198
|
+
for (const key of allKeys) {
|
|
199
|
+
if (!(key in afterObj)) {
|
|
200
|
+
removedFields.push(key);
|
|
201
|
+
}
|
|
202
|
+
else if (!(key in beforeObj) ||
|
|
203
|
+
JSON.stringify(beforeObj[key]) !== JSON.stringify(afterObj[key])) {
|
|
204
|
+
updatedFields[key] = afterObj[key];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return { updatedFields, removedFields };
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Determines if the full document should be included
|
|
211
|
+
*/
|
|
212
|
+
shouldIncludeFullDocument(operationType) {
|
|
213
|
+
const option = this.options.fullDocument || 'default';
|
|
214
|
+
switch (option) {
|
|
215
|
+
case 'default':
|
|
216
|
+
return operationType === 'insert';
|
|
217
|
+
case 'updateLookup':
|
|
218
|
+
case 'whenAvailable':
|
|
219
|
+
case 'required':
|
|
220
|
+
return (operationType === 'insert' || operationType === 'update' || operationType === 'replace');
|
|
221
|
+
default:
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Determines if the full document before change should be included
|
|
227
|
+
*/
|
|
228
|
+
shouldIncludeFullDocumentBefore(operationType) {
|
|
229
|
+
const option = this.options.fullDocumentBeforeChange || 'off';
|
|
230
|
+
if (option === 'off')
|
|
231
|
+
return false;
|
|
232
|
+
return operationType === 'update' || operationType === 'replace' || operationType === 'delete';
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Checks if a change document passes the filter
|
|
236
|
+
*/
|
|
237
|
+
passesFilter(changeDoc) {
|
|
238
|
+
if (!this.options.filter)
|
|
239
|
+
return true;
|
|
240
|
+
// Simple filter implementation - could be enhanced
|
|
241
|
+
// For now, just check basic equality filters
|
|
242
|
+
for (const [key, value] of Object.entries(this.options.filter)) {
|
|
243
|
+
const docValue = changeDoc[key];
|
|
244
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
245
|
+
// Handle operators like $in, $eq, etc.
|
|
246
|
+
// This is a simplified implementation
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
else if (docValue !== value) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Returns an async iterator for the change stream
|
|
257
|
+
*/
|
|
258
|
+
async *[Symbol.asyncIterator]() {
|
|
259
|
+
while (!this.closed) {
|
|
260
|
+
if (this.buffer.length > 0) {
|
|
261
|
+
yield this.buffer.shift();
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
// Wait for next change
|
|
265
|
+
await new Promise((resolve) => {
|
|
266
|
+
const onChange = () => {
|
|
267
|
+
this.removeListener('change', onChange);
|
|
268
|
+
this.removeListener('close', onClose);
|
|
269
|
+
resolve();
|
|
270
|
+
};
|
|
271
|
+
const onClose = () => {
|
|
272
|
+
this.removeListener('change', onChange);
|
|
273
|
+
this.removeListener('close', onClose);
|
|
274
|
+
resolve();
|
|
275
|
+
};
|
|
276
|
+
this.once('change', onChange);
|
|
277
|
+
this.once('close', onClose);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Closes the change stream
|
|
284
|
+
*/
|
|
285
|
+
close() {
|
|
286
|
+
if (this.closed)
|
|
287
|
+
return;
|
|
288
|
+
this.closed = true;
|
|
289
|
+
if (this.pollInterval) {
|
|
290
|
+
clearInterval(this.pollInterval);
|
|
291
|
+
this.pollInterval = null;
|
|
292
|
+
}
|
|
293
|
+
this.emit('close');
|
|
294
|
+
this.removeAllListeners();
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Returns the next change document
|
|
298
|
+
*/
|
|
299
|
+
async next() {
|
|
300
|
+
if (this.closed) {
|
|
301
|
+
return { value: {}, done: true };
|
|
302
|
+
}
|
|
303
|
+
if (this.buffer.length > 0) {
|
|
304
|
+
return { value: this.buffer.shift(), done: false };
|
|
305
|
+
}
|
|
306
|
+
// Wait for next change
|
|
307
|
+
return new Promise((resolve) => {
|
|
308
|
+
const onChange = (changeDoc) => {
|
|
309
|
+
this.removeListener('change', onChange);
|
|
310
|
+
this.removeListener('close', onClose);
|
|
311
|
+
resolve({ value: changeDoc, done: false });
|
|
312
|
+
};
|
|
313
|
+
const onClose = () => {
|
|
314
|
+
this.removeListener('change', onChange);
|
|
315
|
+
this.removeListener('close', onClose);
|
|
316
|
+
resolve({ value: {}, done: true });
|
|
317
|
+
};
|
|
318
|
+
this.once('change', onChange);
|
|
319
|
+
this.once('close', onClose);
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Cleanup triggers when the change stream is destroyed
|
|
324
|
+
*/
|
|
325
|
+
async cleanup() {
|
|
326
|
+
const collectionName = this.collectionName;
|
|
327
|
+
try {
|
|
328
|
+
await this.db.exec(`DROP TRIGGER IF EXISTS "${collectionName}_insert_trigger"`);
|
|
329
|
+
await this.db.exec(`DROP TRIGGER IF EXISTS "${collectionName}_update_trigger"`);
|
|
330
|
+
await this.db.exec(`DROP TRIGGER IF EXISTS "${collectionName}_delete_trigger"`);
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
// Ignore errors during cleanup
|
|
334
|
+
console.warn('Error during change stream cleanup:', error);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
//# sourceMappingURL=changeStream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changeStream.js","sourceRoot":"","sources":["../src/changeStream.ts"],"names":[],"mappings":"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"}
|
package/dist/collection.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SQLiteDB } from './db.js';
|
|
2
2
|
import { DocumentWithId, Filter, UpdateFilter, InsertOneResult, UpdateResult, DeleteResult, Projection, IndexSpecification, CreateIndexOptions, CreateIndexResult, DropIndexResult, IndexInfo } from './types.js';
|
|
3
3
|
import { FindCursor } from './cursors/findCursor.js';
|
|
4
|
+
import { ChangeStream, ChangeStreamOptions } from './changeStream.js';
|
|
4
5
|
/**
|
|
5
6
|
* MongoLiteCollection provides methods to interact with a specific SQLite table
|
|
6
7
|
* as if it were a MongoDB collection.
|
|
@@ -147,6 +148,31 @@ export declare class MongoLiteCollection<T extends DocumentWithId> {
|
|
|
147
148
|
* @returns {Promise<number>} The count of matching documents.
|
|
148
149
|
*/
|
|
149
150
|
countDocuments(filter?: Filter<T>): Promise<number>;
|
|
151
|
+
/**
|
|
152
|
+
* Opens a change stream to watch for changes on this collection.
|
|
153
|
+
* Returns a ChangeStream that emits events when documents are inserted, updated, or deleted.
|
|
154
|
+
*
|
|
155
|
+
* @param options Options for the change stream
|
|
156
|
+
* @returns A ChangeStream instance
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* ```typescript
|
|
160
|
+
* const changeStream = collection.watch();
|
|
161
|
+
*
|
|
162
|
+
* changeStream.on('change', (change) => {
|
|
163
|
+
* console.log('Change detected:', change);
|
|
164
|
+
* });
|
|
165
|
+
*
|
|
166
|
+
* // Or use async iteration
|
|
167
|
+
* for await (const change of changeStream) {
|
|
168
|
+
* console.log('Change detected:', change);
|
|
169
|
+
* }
|
|
170
|
+
*
|
|
171
|
+
* // Close the change stream when done
|
|
172
|
+
* changeStream.close();
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
watch(options?: ChangeStreamOptions<T>): ChangeStream<T>;
|
|
150
176
|
/**
|
|
151
177
|
* Retries an operation with exponential backoff when SQLITE_BUSY errors occur.
|
|
152
178
|
* @param operation The operation function to retry.
|
package/dist/collection.js
CHANGED
|
@@ -1,6 +1,138 @@
|
|
|
1
1
|
import { ObjectId } from 'bson';
|
|
2
2
|
import { FindCursor } from './cursors/findCursor.js';
|
|
3
3
|
import { extractRawIndexColumns } from './utils/indexing.js';
|
|
4
|
+
import { ChangeStream } from './changeStream.js';
|
|
5
|
+
/**
|
|
6
|
+
* Safely stringifies JSON data with validation and error handling.
|
|
7
|
+
* Prevents storing malformed JSON that could cause parsing issues later.
|
|
8
|
+
* @param data The data to stringify
|
|
9
|
+
* @param context Optional context for error messages
|
|
10
|
+
* @returns The JSON string
|
|
11
|
+
* @throws Error if the data cannot be safely stringified
|
|
12
|
+
*/
|
|
13
|
+
function safeJsonStringify(data, context) {
|
|
14
|
+
try {
|
|
15
|
+
// First, validate that the data is JSON-serializable by attempting to stringify it
|
|
16
|
+
const jsonString = JSON.stringify(data);
|
|
17
|
+
// Verify that the stringified data can be parsed back (round-trip test)
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(jsonString);
|
|
20
|
+
// Basic validation that the round-trip preserved the data structure
|
|
21
|
+
if (typeof data === 'object' && data !== null && typeof parsed !== 'object') {
|
|
22
|
+
throw new Error('Round-trip JSON validation failed: type mismatch');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (parseError) {
|
|
26
|
+
throw new Error(`Round-trip JSON validation failed: ${parseError instanceof Error ? parseError.message : 'Unknown parse error'}`);
|
|
27
|
+
}
|
|
28
|
+
return jsonString;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
const contextMsg = context ? ` in ${context}` : '';
|
|
32
|
+
if (error instanceof Error) {
|
|
33
|
+
throw new Error(`Failed to safely stringify JSON data${contextMsg}: ${error.message}`);
|
|
34
|
+
}
|
|
35
|
+
throw new Error(`Failed to safely stringify JSON data${contextMsg}: Unknown error`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Safely parses JSON data with fallback mechanisms for malformed JSON.
|
|
40
|
+
* @param jsonString The JSON string to parse
|
|
41
|
+
* @param context Optional context for error messages and recovery
|
|
42
|
+
* @returns The parsed object or a fallback object
|
|
43
|
+
*/
|
|
44
|
+
function safeJsonParse(jsonString, context) {
|
|
45
|
+
if (!jsonString || typeof jsonString !== 'string') {
|
|
46
|
+
console.warn(`Invalid JSON string${context ? ` in ${context}` : ''}: not a string or empty`);
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(jsonString);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
// Log the error for debugging
|
|
54
|
+
console.error(`JSON parse error${context ? ` in ${context}` : ''}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
55
|
+
console.error(`Malformed JSON string: ${jsonString.substring(0, 500)}${jsonString.length > 500 ? '...' : ''}`);
|
|
56
|
+
// Attempt to recover from common JSON corruption issues
|
|
57
|
+
try {
|
|
58
|
+
// Try to fix common escaping issues
|
|
59
|
+
let fixedJson = jsonString;
|
|
60
|
+
// Fix double-escaped quotes
|
|
61
|
+
fixedJson = fixedJson.replace(/\\"/g, '"');
|
|
62
|
+
// Fix improperly escaped backslashes
|
|
63
|
+
fixedJson = fixedJson.replace(/\\\\/g, '\\');
|
|
64
|
+
// Try parsing the fixed JSON
|
|
65
|
+
const recovered = JSON.parse(fixedJson);
|
|
66
|
+
console.warn(`Successfully recovered malformed JSON${context ? ` in ${context}` : ''}`);
|
|
67
|
+
return recovered;
|
|
68
|
+
}
|
|
69
|
+
catch (recoveryError) {
|
|
70
|
+
console.error(`JSON recovery failed${context ? ` in ${context}` : ''}: ${recoveryError instanceof Error ? recoveryError.message : 'Unknown error'}`);
|
|
71
|
+
// Last resort: return an empty object with a special marker
|
|
72
|
+
return {
|
|
73
|
+
__mongoLiteCorrupted: true,
|
|
74
|
+
__originalData: jsonString,
|
|
75
|
+
__error: error instanceof Error ? error.message : 'Unknown JSON parse error',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Validates that a document is safe to store (no functions, circular references, etc.)
|
|
82
|
+
* @param doc The document to validate
|
|
83
|
+
* @param path Current path for error reporting
|
|
84
|
+
* @throws Error if the document contains invalid data
|
|
85
|
+
*/
|
|
86
|
+
function validateDocumentForStorage(doc, path = 'root') {
|
|
87
|
+
if (doc === null || doc === undefined) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (typeof doc === 'function') {
|
|
91
|
+
throw new Error(`Document validation failed: Functions are not allowed in documents (found at ${path})`);
|
|
92
|
+
}
|
|
93
|
+
if (typeof doc === 'symbol') {
|
|
94
|
+
throw new Error(`Document validation failed: Symbols are not allowed in documents (found at ${path})`);
|
|
95
|
+
}
|
|
96
|
+
if (typeof doc === 'bigint') {
|
|
97
|
+
throw new Error(`Document validation failed: BigInt values are not supported, use regular numbers or strings (found at ${path})`);
|
|
98
|
+
}
|
|
99
|
+
if (doc instanceof RegExp) {
|
|
100
|
+
throw new Error(`Document validation failed: RegExp objects are not supported in documents (found at ${path})`);
|
|
101
|
+
}
|
|
102
|
+
if (Array.isArray(doc)) {
|
|
103
|
+
doc.forEach((item, index) => {
|
|
104
|
+
validateDocumentForStorage(item, `${path}[${index}]`);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
else if (typeof doc === 'object') {
|
|
108
|
+
// Check for circular references by maintaining a Set of visited objects
|
|
109
|
+
const visited = new Set();
|
|
110
|
+
function checkCircular(obj, currentPath) {
|
|
111
|
+
if (visited.has(obj)) {
|
|
112
|
+
throw new Error(`Document validation failed: Circular reference detected at ${currentPath}`);
|
|
113
|
+
}
|
|
114
|
+
visited.add(obj);
|
|
115
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
116
|
+
if (typeof value === 'object' && value !== null) {
|
|
117
|
+
checkCircular(value, `${currentPath}.${key}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
visited.delete(obj);
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
checkCircular(doc, path);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (error instanceof Error && error.message.includes('Circular reference')) {
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Validate each property
|
|
131
|
+
for (const [key, value] of Object.entries(doc)) {
|
|
132
|
+
validateDocumentForStorage(value, `${path}.${key}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
4
136
|
/**
|
|
5
137
|
* MongoLiteCollection provides methods to interact with a specific SQLite table
|
|
6
138
|
* as if it were a MongoDB collection.
|
|
@@ -54,7 +186,14 @@ export class MongoLiteCollection {
|
|
|
54
186
|
const docId = doc._id || new ObjectId().toString();
|
|
55
187
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
56
188
|
const { _id, ...dataToStore } = { ...doc, _id: docId }; // Ensure _id is part of the internal structure
|
|
57
|
-
|
|
189
|
+
// Validate the document before storing
|
|
190
|
+
try {
|
|
191
|
+
validateDocumentForStorage(dataToStore);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
throw new Error(`Cannot insert document: ${error instanceof Error ? error.message : 'Unknown validation error'}`);
|
|
195
|
+
}
|
|
196
|
+
const jsonData = safeJsonStringify(dataToStore, `insertOne for document ${docId}`);
|
|
58
197
|
const sql = `INSERT INTO "${this.name}" (_id, data) VALUES (?, ?)`;
|
|
59
198
|
try {
|
|
60
199
|
await this.db.run(sql, [docId, jsonData]);
|
|
@@ -103,13 +242,20 @@ export class MongoLiteCollection {
|
|
|
103
242
|
async insertBatch(docs) {
|
|
104
243
|
const results = [];
|
|
105
244
|
// Prepare all documents and their data upfront
|
|
106
|
-
const preparedDocs = docs.map(doc => {
|
|
245
|
+
const preparedDocs = docs.map((doc) => {
|
|
107
246
|
const docId = doc._id || new ObjectId().toString();
|
|
108
247
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
109
248
|
const { _id, ...dataToStore } = { ...doc, _id: docId };
|
|
249
|
+
// Validate the document before storing
|
|
250
|
+
try {
|
|
251
|
+
validateDocumentForStorage(dataToStore);
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
throw new Error(`Cannot insert document ${docId}: ${error instanceof Error ? error.message : 'Unknown validation error'}`);
|
|
255
|
+
}
|
|
110
256
|
return {
|
|
111
257
|
id: docId,
|
|
112
|
-
data:
|
|
258
|
+
data: safeJsonStringify(dataToStore, `insertBatch for document ${docId}`),
|
|
113
259
|
};
|
|
114
260
|
});
|
|
115
261
|
const insertBatchOperation = async () => {
|
|
@@ -139,7 +285,7 @@ export class MongoLiteCollection {
|
|
|
139
285
|
if (error.code === 'SQLITE_CONSTRAINT') {
|
|
140
286
|
// Find which document caused the constraint violation
|
|
141
287
|
const errorMessage = error.message;
|
|
142
|
-
const duplicateId = preparedDocs.find(doc => errorMessage.includes(doc.id))?.id || 'unknown';
|
|
288
|
+
const duplicateId = preparedDocs.find((doc) => errorMessage.includes(doc.id))?.id || 'unknown';
|
|
143
289
|
throw new Error(`Duplicate _id: ${duplicateId}`);
|
|
144
290
|
}
|
|
145
291
|
throw error;
|
|
@@ -211,7 +357,7 @@ export class MongoLiteCollection {
|
|
|
211
357
|
const newDocId = update.$set?._id || new ObjectId().toString();
|
|
212
358
|
const newDoc = { _id: newDocId, ...update.$set }; // Assuming $set is used for upsert
|
|
213
359
|
// Ensure _id is set
|
|
214
|
-
const jsonData =
|
|
360
|
+
const jsonData = safeJsonStringify(newDoc, `upsert for document ${newDocId}`);
|
|
215
361
|
const insertSql = `INSERT INTO "${this.name}" (_id, data) VALUES (?, ?)`;
|
|
216
362
|
try {
|
|
217
363
|
await this.db.run(insertSql, [newDocId, jsonData]);
|
|
@@ -233,7 +379,7 @@ export class MongoLiteCollection {
|
|
|
233
379
|
if (!rowToUpdate) {
|
|
234
380
|
return { acknowledged: true, matchedCount: 0, modifiedCount: 0, upsertedId: null };
|
|
235
381
|
}
|
|
236
|
-
const currentDoc =
|
|
382
|
+
const currentDoc = safeJsonParse(rowToUpdate.data, `updateOne for document ${rowToUpdate._id}`);
|
|
237
383
|
let modified = false;
|
|
238
384
|
// Process update operators
|
|
239
385
|
for (const operator in update) {
|
|
@@ -322,7 +468,10 @@ export class MongoLiteCollection {
|
|
|
322
468
|
if (modified) {
|
|
323
469
|
// Update the document in SQLite
|
|
324
470
|
const updateSql = `UPDATE "${this.name}" SET data = ? WHERE _id = ?`;
|
|
325
|
-
const updateParams = [
|
|
471
|
+
const updateParams = [
|
|
472
|
+
safeJsonStringify(currentDoc, `updateOne for document ${rowToUpdate._id}`),
|
|
473
|
+
rowToUpdate._id,
|
|
474
|
+
];
|
|
326
475
|
try {
|
|
327
476
|
await this.db.run(updateSql, updateParams);
|
|
328
477
|
}
|
|
@@ -366,7 +515,7 @@ export class MongoLiteCollection {
|
|
|
366
515
|
let modifiedCount = 0;
|
|
367
516
|
const updatedIds = [];
|
|
368
517
|
for (const rowToUpdate of rowsToUpdate) {
|
|
369
|
-
const currentDoc =
|
|
518
|
+
const currentDoc = safeJsonParse(rowToUpdate.data, `updateMany for document ${rowToUpdate._id}`);
|
|
370
519
|
let modified = false;
|
|
371
520
|
// Process update operators
|
|
372
521
|
for (const operator in update) {
|
|
@@ -458,7 +607,10 @@ export class MongoLiteCollection {
|
|
|
458
607
|
if (modified) {
|
|
459
608
|
// Update the document in SQLite
|
|
460
609
|
const updateSql = `UPDATE "${this.name}" SET data = ? WHERE _id = ?`;
|
|
461
|
-
const updateParams = [
|
|
610
|
+
const updateParams = [
|
|
611
|
+
safeJsonStringify(currentDoc, `updateMany for document ${rowToUpdate._id}`),
|
|
612
|
+
rowToUpdate._id,
|
|
613
|
+
];
|
|
462
614
|
try {
|
|
463
615
|
await this.db.run(updateSql, updateParams);
|
|
464
616
|
modifiedCount++;
|
|
@@ -760,6 +912,33 @@ export class MongoLiteCollection {
|
|
|
760
912
|
const result = await this.db.get(countSql, paramsForCount);
|
|
761
913
|
return result?.count || 0;
|
|
762
914
|
}
|
|
915
|
+
/**
|
|
916
|
+
* Opens a change stream to watch for changes on this collection.
|
|
917
|
+
* Returns a ChangeStream that emits events when documents are inserted, updated, or deleted.
|
|
918
|
+
*
|
|
919
|
+
* @param options Options for the change stream
|
|
920
|
+
* @returns A ChangeStream instance
|
|
921
|
+
*
|
|
922
|
+
* @example
|
|
923
|
+
* ```typescript
|
|
924
|
+
* const changeStream = collection.watch();
|
|
925
|
+
*
|
|
926
|
+
* changeStream.on('change', (change) => {
|
|
927
|
+
* console.log('Change detected:', change);
|
|
928
|
+
* });
|
|
929
|
+
*
|
|
930
|
+
* // Or use async iteration
|
|
931
|
+
* for await (const change of changeStream) {
|
|
932
|
+
* console.log('Change detected:', change);
|
|
933
|
+
* }
|
|
934
|
+
*
|
|
935
|
+
* // Close the change stream when done
|
|
936
|
+
* changeStream.close();
|
|
937
|
+
* ```
|
|
938
|
+
*/
|
|
939
|
+
watch(options = {}) {
|
|
940
|
+
return new ChangeStream(this.db, this.name, options);
|
|
941
|
+
}
|
|
763
942
|
/**
|
|
764
943
|
* Retries an operation with exponential backoff when SQLITE_BUSY errors occur.
|
|
765
944
|
* @param operation The operation function to retry.
|