@semics-tech/mongolite 1.0.0 → 1.2.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.
Files changed (44) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +78 -2
  3. package/dist/adapters/node-sqlite.d.ts +65 -0
  4. package/dist/adapters/node-sqlite.js +241 -0
  5. package/dist/adapters/node-sqlite.js.map +1 -0
  6. package/dist/better-sqlite3.d.ts +49 -0
  7. package/dist/better-sqlite3.js +60 -0
  8. package/dist/better-sqlite3.js.map +1 -0
  9. package/dist/bin/mongolite-debug.js +0 -0
  10. package/dist/cjs/adapters/browser.js +106 -0
  11. package/dist/cjs/adapters/cloudflare.js +97 -0
  12. package/dist/cjs/adapters/node-sqlite.js +248 -0
  13. package/dist/cjs/better-sqlite3.js +80 -0
  14. package/dist/cjs/bin/mongolite-debug.js +152 -0
  15. package/dist/cjs/changeStream.js +341 -0
  16. package/dist/cjs/cloudflare.js +59 -0
  17. package/dist/cjs/collection.js +1748 -0
  18. package/dist/cjs/cursors/findCursor.js +837 -0
  19. package/dist/cjs/db.js +307 -0
  20. package/dist/cjs/debugger/query-debugger.js +297 -0
  21. package/dist/cjs/index.js +65 -0
  22. package/dist/cjs/mongo-client.js +58 -0
  23. package/dist/cjs/package.json +3 -0
  24. package/dist/cjs/types.js +2 -0
  25. package/dist/cjs/utils/connection-registry.js +122 -0
  26. package/dist/cjs/utils/indexing.js +45 -0
  27. package/dist/cjs/utils/projection.js +101 -0
  28. package/dist/cjs/utils/queryOperators.js +142 -0
  29. package/dist/collection.d.ts +25 -0
  30. package/dist/collection.js +53 -14
  31. package/dist/collection.js.map +1 -1
  32. package/dist/cursors/findCursor.d.ts +58 -0
  33. package/dist/cursors/findCursor.js +88 -11
  34. package/dist/cursors/findCursor.js.map +1 -1
  35. package/dist/db.d.ts +23 -0
  36. package/dist/db.js +85 -39
  37. package/dist/db.js.map +1 -1
  38. package/dist/index.d.ts +5 -2
  39. package/dist/index.js +6 -3
  40. package/dist/index.js.map +1 -1
  41. package/dist/utils/connection-registry.d.ts +74 -0
  42. package/dist/utils/connection-registry.js +117 -0
  43. package/dist/utils/connection-registry.js.map +1 -0
  44. package/package.json +20 -10
@@ -0,0 +1,341 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ChangeStream = void 0;
4
+ const events_1 = require("events");
5
+ class ChangeStream extends events_1.EventEmitter {
6
+ constructor(db, collectionName, options = {}) {
7
+ super();
8
+ this.db = db;
9
+ this.collectionName = collectionName;
10
+ this.options = options;
11
+ this.closed = false;
12
+ this.pollInterval = null;
13
+ this.lastProcessedId = 0;
14
+ this.buffer = [];
15
+ this.maxBufferSize = options.maxBufferSize || 1000;
16
+ this.setupChangeTracking();
17
+ this.startPolling();
18
+ }
19
+ /**
20
+ * Sets up the change tracking infrastructure
21
+ */
22
+ async setupChangeTracking() {
23
+ await this.ensureChangeLogTable();
24
+ await this.setupTriggers();
25
+ }
26
+ /**
27
+ * Creates the change log table if it doesn't exist
28
+ */
29
+ async ensureChangeLogTable() {
30
+ const createTableSQL = `
31
+ CREATE TABLE IF NOT EXISTS __mongolite_changes__ (
32
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
33
+ timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
34
+ operation_type TEXT NOT NULL,
35
+ collection_name TEXT NOT NULL,
36
+ document_id TEXT NOT NULL,
37
+ full_document TEXT,
38
+ full_document_before TEXT,
39
+ updated_fields TEXT,
40
+ removed_fields TEXT,
41
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
42
+ );
43
+ `;
44
+ const createIndexSQL = `
45
+ CREATE INDEX IF NOT EXISTS idx_mongolite_changes_collection_timestamp
46
+ ON __mongolite_changes__ (collection_name, timestamp);
47
+ `;
48
+ await this.db.exec(createTableSQL);
49
+ await this.db.exec(createIndexSQL);
50
+ }
51
+ /**
52
+ * Sets up triggers for tracking changes on the collection
53
+ */
54
+ async setupTriggers() {
55
+ const collectionName = this.collectionName;
56
+ // Drop existing triggers for this collection
57
+ await this.db.exec(`DROP TRIGGER IF EXISTS "${collectionName}_insert_trigger"`);
58
+ await this.db.exec(`DROP TRIGGER IF EXISTS "${collectionName}_update_trigger"`);
59
+ await this.db.exec(`DROP TRIGGER IF EXISTS "${collectionName}_delete_trigger"`);
60
+ // Insert trigger
61
+ const insertTriggerSQL = `
62
+ CREATE TRIGGER "${collectionName}_insert_trigger"
63
+ AFTER INSERT ON "${collectionName}"
64
+ FOR EACH ROW
65
+ BEGIN
66
+ INSERT INTO __mongolite_changes__ (
67
+ operation_type, collection_name, document_id, full_document
68
+ ) VALUES (
69
+ 'insert', '${collectionName}', NEW._id, NEW.data
70
+ );
71
+ END;
72
+ `;
73
+ // Update trigger
74
+ const updateTriggerSQL = `
75
+ CREATE TRIGGER "${collectionName}_update_trigger"
76
+ AFTER UPDATE ON "${collectionName}"
77
+ FOR EACH ROW
78
+ BEGIN
79
+ INSERT INTO __mongolite_changes__ (
80
+ operation_type, collection_name, document_id,
81
+ full_document, full_document_before
82
+ ) VALUES (
83
+ 'update', '${collectionName}', NEW._id,
84
+ NEW.data, OLD.data
85
+ );
86
+ END;
87
+ `;
88
+ // Delete trigger
89
+ const deleteTriggerSQL = `
90
+ CREATE TRIGGER "${collectionName}_delete_trigger"
91
+ AFTER DELETE ON "${collectionName}"
92
+ FOR EACH ROW
93
+ BEGIN
94
+ INSERT INTO __mongolite_changes__ (
95
+ operation_type, collection_name, document_id, full_document_before
96
+ ) VALUES (
97
+ 'delete', '${collectionName}', OLD._id, OLD.data
98
+ );
99
+ END;
100
+ `;
101
+ await this.db.exec(insertTriggerSQL);
102
+ await this.db.exec(updateTriggerSQL);
103
+ await this.db.exec(deleteTriggerSQL);
104
+ }
105
+ /**
106
+ * Starts polling for new changes
107
+ */
108
+ startPolling() {
109
+ if (this.closed)
110
+ return;
111
+ this.pollInterval = setInterval(async () => {
112
+ try {
113
+ await this.pollForChanges();
114
+ }
115
+ catch (error) {
116
+ this.emit('error', error);
117
+ }
118
+ }, 100); // Poll every 100ms
119
+ }
120
+ /**
121
+ * Polls for new changes from the change log
122
+ */
123
+ async pollForChanges() {
124
+ if (this.closed)
125
+ return;
126
+ const changes = await this.db.all(`SELECT * FROM __mongolite_changes__
127
+ WHERE collection_name = ? AND id > ?
128
+ ORDER BY id ASC LIMIT 100`, [this.collectionName, this.lastProcessedId]);
129
+ for (const change of changes) {
130
+ if (this.closed)
131
+ break;
132
+ const changeDoc = await this.transformChangeEvent(change);
133
+ if (this.passesFilter(changeDoc)) {
134
+ this.buffer.push(changeDoc);
135
+ // Emit the change event
136
+ this.emit('change', changeDoc);
137
+ // Clean buffer if it gets too large
138
+ if (this.buffer.length > this.maxBufferSize) {
139
+ this.buffer = this.buffer.slice(-this.maxBufferSize);
140
+ }
141
+ }
142
+ this.lastProcessedId = change.id;
143
+ }
144
+ }
145
+ /**
146
+ * Transforms a raw change event from the database into a ChangeStreamDocument
147
+ */
148
+ async transformChangeEvent(change) {
149
+ const fullDocument = change.full_document ? JSON.parse(change.full_document) : undefined;
150
+ const fullDocumentBefore = change.full_document_before
151
+ ? JSON.parse(change.full_document_before)
152
+ : undefined;
153
+ let updateDescription;
154
+ if (change.operation_type === 'update' && fullDocument && fullDocumentBefore) {
155
+ updateDescription = this.computeUpdateDescription(fullDocumentBefore, fullDocument);
156
+ }
157
+ const changeDoc = {
158
+ _id: `${change.id}`, // Use the change log ID as the change stream document ID
159
+ operationType: change.operation_type,
160
+ clusterTime: new Date(change.timestamp),
161
+ documentKey: { _id: change.document_id },
162
+ ns: {
163
+ db: 'mongolite', // You could make this configurable
164
+ coll: change.collection_name,
165
+ },
166
+ };
167
+ // Add full document based on options
168
+ if (this.shouldIncludeFullDocument(change.operation_type)) {
169
+ changeDoc.fullDocument = fullDocument
170
+ ? { _id: change.document_id, ...fullDocument }
171
+ : undefined;
172
+ }
173
+ // Add full document before change based on options
174
+ if (this.shouldIncludeFullDocumentBefore(change.operation_type)) {
175
+ changeDoc.fullDocumentBeforeChange = fullDocumentBefore
176
+ ? { _id: change.document_id, ...fullDocumentBefore }
177
+ : undefined;
178
+ }
179
+ if (updateDescription) {
180
+ changeDoc.updateDescription = updateDescription;
181
+ }
182
+ return changeDoc;
183
+ }
184
+ /**
185
+ * Computes the update description by comparing before and after documents
186
+ */
187
+ computeUpdateDescription(before, after) {
188
+ const updatedFields = {};
189
+ const removedFields = [];
190
+ // Type guard to ensure we have objects to compare
191
+ if (typeof before !== 'object' ||
192
+ before === null ||
193
+ typeof after !== 'object' ||
194
+ after === null) {
195
+ return { updatedFields, removedFields };
196
+ }
197
+ // Simple comparison - this could be made more sophisticated
198
+ const beforeObj = before;
199
+ const afterObj = after;
200
+ const allKeys = new Set([...Object.keys(beforeObj), ...Object.keys(afterObj)]);
201
+ for (const key of allKeys) {
202
+ if (!(key in afterObj)) {
203
+ removedFields.push(key);
204
+ }
205
+ else if (!(key in beforeObj) ||
206
+ JSON.stringify(beforeObj[key]) !== JSON.stringify(afterObj[key])) {
207
+ updatedFields[key] = afterObj[key];
208
+ }
209
+ }
210
+ return { updatedFields, removedFields };
211
+ }
212
+ /**
213
+ * Determines if the full document should be included
214
+ */
215
+ shouldIncludeFullDocument(operationType) {
216
+ const option = this.options.fullDocument || 'default';
217
+ switch (option) {
218
+ case 'default':
219
+ return operationType === 'insert';
220
+ case 'updateLookup':
221
+ case 'whenAvailable':
222
+ case 'required':
223
+ return (operationType === 'insert' || operationType === 'update' || operationType === 'replace');
224
+ default:
225
+ return false;
226
+ }
227
+ }
228
+ /**
229
+ * Determines if the full document before change should be included
230
+ */
231
+ shouldIncludeFullDocumentBefore(operationType) {
232
+ const option = this.options.fullDocumentBeforeChange || 'off';
233
+ if (option === 'off')
234
+ return false;
235
+ return operationType === 'update' || operationType === 'replace' || operationType === 'delete';
236
+ }
237
+ /**
238
+ * Checks if a change document passes the filter
239
+ */
240
+ passesFilter(changeDoc) {
241
+ if (!this.options.filter)
242
+ return true;
243
+ // Simple filter implementation - could be enhanced
244
+ // For now, just check basic equality filters
245
+ for (const [key, value] of Object.entries(this.options.filter)) {
246
+ const docValue = changeDoc[key];
247
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
248
+ // Handle operators like $in, $eq, etc.
249
+ // This is a simplified implementation
250
+ continue;
251
+ }
252
+ else if (docValue !== value) {
253
+ return false;
254
+ }
255
+ }
256
+ return true;
257
+ }
258
+ /**
259
+ * Returns an async iterator for the change stream
260
+ */
261
+ async *[Symbol.asyncIterator]() {
262
+ while (!this.closed) {
263
+ if (this.buffer.length > 0) {
264
+ yield this.buffer.shift();
265
+ }
266
+ else {
267
+ // Wait for next change
268
+ await new Promise((resolve) => {
269
+ const onChange = () => {
270
+ this.removeListener('change', onChange);
271
+ this.removeListener('close', onClose);
272
+ resolve();
273
+ };
274
+ const onClose = () => {
275
+ this.removeListener('change', onChange);
276
+ this.removeListener('close', onClose);
277
+ resolve();
278
+ };
279
+ this.once('change', onChange);
280
+ this.once('close', onClose);
281
+ });
282
+ }
283
+ }
284
+ }
285
+ /**
286
+ * Closes the change stream
287
+ */
288
+ close() {
289
+ if (this.closed)
290
+ return;
291
+ this.closed = true;
292
+ if (this.pollInterval) {
293
+ clearInterval(this.pollInterval);
294
+ this.pollInterval = null;
295
+ }
296
+ this.emit('close');
297
+ this.removeAllListeners();
298
+ }
299
+ /**
300
+ * Returns the next change document
301
+ */
302
+ async next() {
303
+ if (this.closed) {
304
+ return { value: {}, done: true };
305
+ }
306
+ if (this.buffer.length > 0) {
307
+ return { value: this.buffer.shift(), done: false };
308
+ }
309
+ // Wait for next change
310
+ return new Promise((resolve) => {
311
+ const onChange = (changeDoc) => {
312
+ this.removeListener('change', onChange);
313
+ this.removeListener('close', onClose);
314
+ resolve({ value: changeDoc, done: false });
315
+ };
316
+ const onClose = () => {
317
+ this.removeListener('change', onChange);
318
+ this.removeListener('close', onClose);
319
+ resolve({ value: {}, done: true });
320
+ };
321
+ this.once('change', onChange);
322
+ this.once('close', onClose);
323
+ });
324
+ }
325
+ /**
326
+ * Cleanup triggers when the change stream is destroyed
327
+ */
328
+ async cleanup() {
329
+ const collectionName = this.collectionName;
330
+ try {
331
+ await this.db.exec(`DROP TRIGGER IF EXISTS "${collectionName}_insert_trigger"`);
332
+ await this.db.exec(`DROP TRIGGER IF EXISTS "${collectionName}_update_trigger"`);
333
+ await this.db.exec(`DROP TRIGGER IF EXISTS "${collectionName}_delete_trigger"`);
334
+ }
335
+ catch (error) {
336
+ // Ignore errors during cleanup
337
+ console.warn('Error during change stream cleanup:', error);
338
+ }
339
+ }
340
+ }
341
+ exports.ChangeStream = ChangeStream;
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ /**
3
+ * Cloudflare-safe entry point for MongoLite.
4
+ *
5
+ * Import from `@semics-tech/mongolite/cloudflare` inside Cloudflare Workers / Durable Objects.
6
+ * This module does **not** import `better-sqlite3`, so it bundles cleanly with `wrangler`.
7
+ *
8
+ * ## Requirements
9
+ *
10
+ * - Enable **Node.js compatibility** in your Worker configuration (e.g. `nodejs_compat`
11
+ * flag in `wrangler.toml`/`wrangler.jsonc`). The `ChangeStream` class depends on
12
+ * Node's built-in `events` module, which requires this flag even if you don't use
13
+ * change streams directly.
14
+ *
15
+ * ## Usage
16
+ *
17
+ * ```ts
18
+ * import { DurableObject } from 'cloudflare:workers';
19
+ * import { MongoLite, CloudflareDurableObjectAdapter } from '@semics-tech/mongolite/cloudflare';
20
+ *
21
+ * export class MyDurableObject extends DurableObject {
22
+ * private client: MongoLite;
23
+ *
24
+ * constructor(ctx: DurableObjectState, env: Env) {
25
+ * super(ctx, env);
26
+ * this.client = new MongoLite(new CloudflareDurableObjectAdapter(ctx.storage.sql));
27
+ * ctx.blockConcurrencyWhile(() => this.client.collection('users').ensureTable());
28
+ * }
29
+ * }
30
+ * ```
31
+ */
32
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
33
+ if (k2 === undefined) k2 = k;
34
+ var desc = Object.getOwnPropertyDescriptor(m, k);
35
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
36
+ desc = { enumerable: true, get: function() { return m[k]; } };
37
+ }
38
+ Object.defineProperty(o, k2, desc);
39
+ }) : (function(o, m, k, k2) {
40
+ if (k2 === undefined) k2 = k;
41
+ o[k2] = m[k];
42
+ }));
43
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
44
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
45
+ };
46
+ Object.defineProperty(exports, "__esModule", { value: true });
47
+ exports.ChangeStream = exports.MongoLiteCollection = exports.CloudflareDurableObjectAdapter = exports.MongoLite = void 0;
48
+ // Core MongoLite class — accepts any IDatabaseAdapter, no better-sqlite3 dependency.
49
+ var mongo_client_js_1 = require("./mongo-client.js");
50
+ Object.defineProperty(exports, "MongoLite", { enumerable: true, get: function () { return mongo_client_js_1.MongoLite; } });
51
+ // Cloudflare-specific adapter
52
+ var cloudflare_js_1 = require("./adapters/cloudflare.js");
53
+ Object.defineProperty(exports, "CloudflareDurableObjectAdapter", { enumerable: true, get: function () { return cloudflare_js_1.CloudflareDurableObjectAdapter; } });
54
+ var collection_js_1 = require("./collection.js");
55
+ Object.defineProperty(exports, "MongoLiteCollection", { enumerable: true, get: function () { return collection_js_1.MongoLiteCollection; } });
56
+ __exportStar(require("./types.js"), exports);
57
+ // Change streams (requires nodejs_compat in Workers)
58
+ var changeStream_js_1 = require("./changeStream.js");
59
+ Object.defineProperty(exports, "ChangeStream", { enumerable: true, get: function () { return changeStream_js_1.ChangeStream; } });