mongolite-ts 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/LICENSE +21 -0
- package/README.md +256 -0
- package/dist/collection.d.ts +114 -0
- package/dist/collection.js +643 -0
- package/dist/collection.js.map +1 -0
- package/dist/db.d.ts +74 -0
- package/dist/db.js +183 -0
- package/dist/db.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +118 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MongoLiteCollection = exports.FindCursor = void 0;
|
|
4
|
+
const uuid_1 = require("uuid");
|
|
5
|
+
/**
|
|
6
|
+
* Represents a cursor for find operations, allowing chaining of limit, skip, and sort.
|
|
7
|
+
*/
|
|
8
|
+
class FindCursor {
|
|
9
|
+
constructor(db, collectionName, initialFilter) {
|
|
10
|
+
this.db = db;
|
|
11
|
+
this.collectionName = collectionName;
|
|
12
|
+
this.limitCount = null;
|
|
13
|
+
this.skipCount = null;
|
|
14
|
+
this.sortCriteria = null;
|
|
15
|
+
this.projectionFields = null;
|
|
16
|
+
this.queryParts = this.buildSelectQuery(initialFilter);
|
|
17
|
+
}
|
|
18
|
+
parseJsonPath(path) {
|
|
19
|
+
return `'$.${path.replace(/\./g, '.')}'`;
|
|
20
|
+
}
|
|
21
|
+
buildWhereClause(filter, params) {
|
|
22
|
+
const conditions = [];
|
|
23
|
+
// Handle $and, $or, $nor logical operators at the top level
|
|
24
|
+
if (filter.$and) {
|
|
25
|
+
const andConditions = filter.$and
|
|
26
|
+
.map((subFilter) => `(${this.buildWhereClause(subFilter, params)})`)
|
|
27
|
+
.join(' AND ');
|
|
28
|
+
conditions.push(`(${andConditions})`);
|
|
29
|
+
}
|
|
30
|
+
else if (filter.$or) {
|
|
31
|
+
const orConditions = filter.$or
|
|
32
|
+
.map((subFilter) => `(${this.buildWhereClause(subFilter, params)})`)
|
|
33
|
+
.join(' OR ');
|
|
34
|
+
conditions.push(`(${orConditions})`);
|
|
35
|
+
}
|
|
36
|
+
else if (filter.$nor) {
|
|
37
|
+
const norConditions = filter.$nor
|
|
38
|
+
.map((subFilter) => `(${this.buildWhereClause(subFilter, params)})`)
|
|
39
|
+
.join(' OR ');
|
|
40
|
+
conditions.push(`NOT (${norConditions})`);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
// Handle field conditions
|
|
44
|
+
for (const key in filter) {
|
|
45
|
+
if (key.startsWith('$'))
|
|
46
|
+
continue; // Skip logical operators already handled
|
|
47
|
+
const value = filter[key];
|
|
48
|
+
if (key === '_id') {
|
|
49
|
+
if (typeof value === 'string') {
|
|
50
|
+
conditions.push('_id = ?');
|
|
51
|
+
params.push(value);
|
|
52
|
+
}
|
|
53
|
+
else if (typeof value === 'object' &&
|
|
54
|
+
value !== null &&
|
|
55
|
+
value.$in) {
|
|
56
|
+
const inValues = value.$in;
|
|
57
|
+
if (inValues.length > 0) {
|
|
58
|
+
conditions.push(`_id IN (${inValues.map(() => '?').join(',')})`);
|
|
59
|
+
params.push(...inValues);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
conditions.push('1=0'); // No values in $in means nothing matches
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else if (typeof value === 'object' &&
|
|
66
|
+
value !== null &&
|
|
67
|
+
value.$ne) {
|
|
68
|
+
conditions.push('_id <> ?');
|
|
69
|
+
params.push(value.$ne);
|
|
70
|
+
}
|
|
71
|
+
// Add other _id specific operators if needed
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
const jsonPath = this.parseJsonPath(key);
|
|
75
|
+
if (typeof value === 'object' && value !== null) {
|
|
76
|
+
// Handle operators like $gt, $lt, $in, $exists, $not etc.
|
|
77
|
+
for (const op in value) {
|
|
78
|
+
const opValue = value[op];
|
|
79
|
+
switch (op) {
|
|
80
|
+
case '$eq':
|
|
81
|
+
conditions.push(`json_extract(data, ${jsonPath}) = json(?)`);
|
|
82
|
+
params.push(JSON.stringify(opValue));
|
|
83
|
+
break;
|
|
84
|
+
case '$ne':
|
|
85
|
+
conditions.push(`json_extract(data, ${jsonPath}) != json(?)`);
|
|
86
|
+
params.push(JSON.stringify(opValue));
|
|
87
|
+
break;
|
|
88
|
+
case '$gt':
|
|
89
|
+
conditions.push(`json_extract(data, ${jsonPath}) > json(?)`);
|
|
90
|
+
params.push(JSON.stringify(opValue));
|
|
91
|
+
break;
|
|
92
|
+
case '$gte':
|
|
93
|
+
conditions.push(`json_extract(data, ${jsonPath}) >= json(?)`);
|
|
94
|
+
params.push(JSON.stringify(opValue));
|
|
95
|
+
break;
|
|
96
|
+
case '$lt':
|
|
97
|
+
conditions.push(`json_extract(data, ${jsonPath}) < json(?)`);
|
|
98
|
+
params.push(JSON.stringify(opValue));
|
|
99
|
+
break;
|
|
100
|
+
case '$lte':
|
|
101
|
+
conditions.push(`json_extract(data, ${jsonPath}) <= json(?)`);
|
|
102
|
+
params.push(JSON.stringify(opValue));
|
|
103
|
+
break;
|
|
104
|
+
case '$in':
|
|
105
|
+
if (Array.isArray(opValue) && opValue.length > 0) {
|
|
106
|
+
const inConditions = opValue
|
|
107
|
+
.map(() => `json_extract(data, ${jsonPath}) = json(?)`)
|
|
108
|
+
.join(' OR ');
|
|
109
|
+
conditions.push(`(${inConditions})`);
|
|
110
|
+
opValue.forEach((val) => params.push(JSON.stringify(val)));
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
conditions.push('1=0'); // Empty $in array, nothing will match
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
case '$nin':
|
|
117
|
+
if (Array.isArray(opValue) && opValue.length > 0) {
|
|
118
|
+
const ninConditions = opValue
|
|
119
|
+
.map(() => `json_extract(data, ${jsonPath}) != json(?)`)
|
|
120
|
+
.join(' AND ');
|
|
121
|
+
conditions.push(`(${ninConditions})`);
|
|
122
|
+
opValue.forEach((val) => params.push(JSON.stringify(val)));
|
|
123
|
+
}
|
|
124
|
+
// Empty $nin array means match everything, so no condition needed
|
|
125
|
+
break;
|
|
126
|
+
case '$exists':
|
|
127
|
+
if (opValue === true) {
|
|
128
|
+
conditions.push(`json_extract(data, ${jsonPath}) IS NOT NULL`);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
conditions.push(`json_extract(data, ${jsonPath}) IS NULL`);
|
|
132
|
+
}
|
|
133
|
+
break;
|
|
134
|
+
// Add other operators as needed
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
// Direct equality for non-object values
|
|
140
|
+
conditions.push(`json_extract(data, ${jsonPath}) = json(?)`);
|
|
141
|
+
params.push(JSON.stringify(value));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return conditions.length > 0 ? conditions.join(' AND ') : '1=1';
|
|
147
|
+
}
|
|
148
|
+
buildSelectQuery(filter) {
|
|
149
|
+
const params = [];
|
|
150
|
+
const whereClause = this.buildWhereClause(filter, params);
|
|
151
|
+
const sql = `SELECT _id, data FROM "${this.collectionName}" WHERE ${whereClause}`;
|
|
152
|
+
return { sql, params };
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Specifies the maximum number of documents the cursor will return.
|
|
156
|
+
* @param count The number of documents to limit to.
|
|
157
|
+
* @returns The `FindCursor` instance for chaining.
|
|
158
|
+
*/
|
|
159
|
+
limit(count) {
|
|
160
|
+
if (count < 0)
|
|
161
|
+
throw new Error('Limit must be a non-negative number.');
|
|
162
|
+
this.limitCount = count;
|
|
163
|
+
return this;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Specifies the number of documents to skip.
|
|
167
|
+
* @param count The number of documents to skip.
|
|
168
|
+
* @returns The `FindCursor` instance for chaining.
|
|
169
|
+
*/
|
|
170
|
+
skip(count) {
|
|
171
|
+
if (count < 0)
|
|
172
|
+
throw new Error('Skip must be a non-negative number.');
|
|
173
|
+
this.skipCount = count;
|
|
174
|
+
return this;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Specifies the sorting order for the documents.
|
|
178
|
+
* @param sortCriteria An object defining sort order (e.g., `{ age: -1, name: 1 }`).
|
|
179
|
+
* @returns The `FindCursor` instance for chaining.
|
|
180
|
+
*/
|
|
181
|
+
sort(sortCriteria) {
|
|
182
|
+
this.sortCriteria = sortCriteria;
|
|
183
|
+
return this;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Specifies the fields to return (projection).
|
|
187
|
+
* @param projection An object where keys are field names and values are 1 (include) or 0 (exclude).
|
|
188
|
+
* `_id` is included by default unless explicitly excluded.
|
|
189
|
+
* @returns The `FindCursor` instance for chaining.
|
|
190
|
+
*/
|
|
191
|
+
project(projection) {
|
|
192
|
+
this.projectionFields = projection;
|
|
193
|
+
return this;
|
|
194
|
+
}
|
|
195
|
+
applyProjection(doc) {
|
|
196
|
+
if (!this.projectionFields)
|
|
197
|
+
return doc;
|
|
198
|
+
const projectedDoc = {};
|
|
199
|
+
let includeMode = true; // true if any field is 1, false if any field is 0 (excluding _id)
|
|
200
|
+
let hasExplicitInclusion = false;
|
|
201
|
+
// Determine if it's an inclusion or exclusion projection
|
|
202
|
+
for (const key in this.projectionFields) {
|
|
203
|
+
if (key === '_id')
|
|
204
|
+
continue;
|
|
205
|
+
if (this.projectionFields[key] === 1) {
|
|
206
|
+
hasExplicitInclusion = true;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
if (this.projectionFields[key] === 0) {
|
|
210
|
+
includeMode = false;
|
|
211
|
+
// No break here, need to check all for explicit inclusions if _id is also 0
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (this.projectionFields._id === 0 && !hasExplicitInclusion) {
|
|
215
|
+
// If _id is excluded and no other fields are explicitly included,
|
|
216
|
+
// it's an exclusion projection where other fields are implicitly included.
|
|
217
|
+
includeMode = false;
|
|
218
|
+
}
|
|
219
|
+
else if (hasExplicitInclusion) {
|
|
220
|
+
includeMode = true;
|
|
221
|
+
}
|
|
222
|
+
if (includeMode) {
|
|
223
|
+
// Inclusion mode
|
|
224
|
+
for (const key in this.projectionFields) {
|
|
225
|
+
if (this.projectionFields[key] === 1) {
|
|
226
|
+
if (key.includes('.')) {
|
|
227
|
+
// Handle nested paths for inclusion (basic implementation)
|
|
228
|
+
const path = key.split('.');
|
|
229
|
+
let current = doc;
|
|
230
|
+
let target = projectedDoc;
|
|
231
|
+
// Navigate to the last parent in the path
|
|
232
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
233
|
+
const segment = path[i];
|
|
234
|
+
if (current[segment] === undefined)
|
|
235
|
+
break;
|
|
236
|
+
if (target[segment] === undefined) {
|
|
237
|
+
target[segment] = {};
|
|
238
|
+
}
|
|
239
|
+
current = current[segment];
|
|
240
|
+
target = target[segment];
|
|
241
|
+
}
|
|
242
|
+
// Set the final property if we reached it
|
|
243
|
+
const lastSegment = path[path.length - 1];
|
|
244
|
+
if (current && current[lastSegment] !== undefined) {
|
|
245
|
+
target[lastSegment] = current[lastSegment];
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
else if (key in doc) {
|
|
249
|
+
projectedDoc[key] = doc[key];
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// _id is included by default in inclusion mode, unless explicitly excluded
|
|
254
|
+
if (this.projectionFields._id !== 0 && '_id' in doc) {
|
|
255
|
+
projectedDoc._id = doc._id;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
// Exclusion mode
|
|
260
|
+
Object.assign(projectedDoc, doc);
|
|
261
|
+
for (const key in this.projectionFields) {
|
|
262
|
+
if (this.projectionFields[key] === 0) {
|
|
263
|
+
if (key.includes('.')) {
|
|
264
|
+
// Handle nested paths for exclusion (basic implementation)
|
|
265
|
+
const path = key.split('.');
|
|
266
|
+
let current = projectedDoc;
|
|
267
|
+
// Navigate to the parent of the property to exclude
|
|
268
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
269
|
+
const segment = path[i];
|
|
270
|
+
if (current[segment] === undefined)
|
|
271
|
+
break;
|
|
272
|
+
current = current[segment];
|
|
273
|
+
}
|
|
274
|
+
// Delete the final property if we reached its parent
|
|
275
|
+
const lastSegment = path[path.length - 1];
|
|
276
|
+
if (current && current[lastSegment] !== undefined) {
|
|
277
|
+
delete current[lastSegment];
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
delete projectedDoc[key];
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return projectedDoc;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Executes the query and returns all matching documents as an array.
|
|
290
|
+
* @returns A promise that resolves to an array of documents.
|
|
291
|
+
*/
|
|
292
|
+
async toArray() {
|
|
293
|
+
let finalSql = this.queryParts.sql;
|
|
294
|
+
const finalParams = [...this.queryParts.params];
|
|
295
|
+
if (this.sortCriteria) {
|
|
296
|
+
const sortClauses = Object.entries(this.sortCriteria).map(([field, order]) => {
|
|
297
|
+
if (field === '_id') {
|
|
298
|
+
return `_id ${order === 1 ? 'ASC' : 'DESC'}`;
|
|
299
|
+
}
|
|
300
|
+
return `json_extract(data, ${this.parseJsonPath(field)}) ${order === 1 ? 'ASC' : 'DESC'}`;
|
|
301
|
+
});
|
|
302
|
+
if (sortClauses.length > 0) {
|
|
303
|
+
finalSql += ` ORDER BY ${sortClauses.join(', ')}`;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (this.limitCount !== null) {
|
|
307
|
+
finalSql += ` LIMIT ?`;
|
|
308
|
+
finalParams.push(this.limitCount);
|
|
309
|
+
}
|
|
310
|
+
if (this.skipCount !== null) {
|
|
311
|
+
if (this.limitCount === null) {
|
|
312
|
+
// SQLite requires a LIMIT if OFFSET is used.
|
|
313
|
+
// Use a very large number if no limit is specified.
|
|
314
|
+
finalSql += ` LIMIT -1`; // Or a large number like 999999999
|
|
315
|
+
}
|
|
316
|
+
finalSql += ` OFFSET ?`;
|
|
317
|
+
finalParams.push(this.skipCount);
|
|
318
|
+
}
|
|
319
|
+
const rows = await this.db.all(finalSql, finalParams);
|
|
320
|
+
return rows.map((row) => {
|
|
321
|
+
const doc = { _id: row._id, ...JSON.parse(row.data) };
|
|
322
|
+
return this.applyProjection(doc);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
exports.FindCursor = FindCursor;
|
|
327
|
+
/**
|
|
328
|
+
* MongoLiteCollection provides methods to interact with a specific SQLite table
|
|
329
|
+
* as if it were a MongoDB collection.
|
|
330
|
+
*/
|
|
331
|
+
class MongoLiteCollection {
|
|
332
|
+
constructor(db, name) {
|
|
333
|
+
this.db = db;
|
|
334
|
+
this.name = name;
|
|
335
|
+
this.ensureTable().catch((err) => {
|
|
336
|
+
// This error should be handled or logged appropriately.
|
|
337
|
+
// For now, console.error is used. In a real app, a more robust
|
|
338
|
+
// error handling mechanism would be needed, potentially failing
|
|
339
|
+
// the collection initialization or notifying the user.
|
|
340
|
+
console.error(`Failed to ensure table ${this.name} exists:`, err);
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Ensures the SQLite table for this collection exists, creating it if necessary.
|
|
345
|
+
* The table will have an `_id` column (indexed) and a `data` column for JSON.
|
|
346
|
+
* @private
|
|
347
|
+
*/
|
|
348
|
+
async ensureTable() {
|
|
349
|
+
// Using " " around table name to handle names with special characters or keywords
|
|
350
|
+
const createTableSQL = `
|
|
351
|
+
CREATE TABLE IF NOT EXISTS "${this.name}" (
|
|
352
|
+
_id TEXT PRIMARY KEY,
|
|
353
|
+
data TEXT
|
|
354
|
+
);
|
|
355
|
+
`;
|
|
356
|
+
// It's generally good practice to create an index on _id, but TEXT PRIMARY KEY often implies an index.
|
|
357
|
+
// Explicitly creating an index can be done if performance dictates.
|
|
358
|
+
// const createIndexSQL = `CREATE UNIQUE INDEX IF NOT EXISTS idx_${this.name}__id ON "${this.name}"(_id);`;
|
|
359
|
+
try {
|
|
360
|
+
await this.db.exec(createTableSQL);
|
|
361
|
+
// await this.db.exec(createIndexSQL); // If explicit index is desired
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
console.error(`Error ensuring table "${this.name}":`, error);
|
|
365
|
+
throw error; // Re-throw to allow caller to handle
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Inserts a single document into the collection.
|
|
370
|
+
* If `_id` is not provided, a UUID will be generated.
|
|
371
|
+
* @param doc The document to insert.
|
|
372
|
+
* @returns {Promise<InsertOneResult>} An object containing the outcome of the insert operation.
|
|
373
|
+
*/
|
|
374
|
+
async insertOne(doc) {
|
|
375
|
+
await this.ensureTable(); // Ensure table exists before insert
|
|
376
|
+
const docId = doc._id || (0, uuid_1.v4)();
|
|
377
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
378
|
+
const { _id, ...dataToStore } = { ...doc, _id: docId }; // Ensure _id is part of the internal structure
|
|
379
|
+
const jsonData = JSON.stringify(dataToStore);
|
|
380
|
+
const sql = `INSERT INTO "${this.name}" (_id, data) VALUES (?, ?)`;
|
|
381
|
+
try {
|
|
382
|
+
await this.db.run(sql, [docId, jsonData]);
|
|
383
|
+
return { acknowledged: true, insertedId: docId };
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
// Handle potential errors, e.g., unique constraint violation if _id already exists
|
|
387
|
+
console.error(`Error inserting document into ${this.name}:`, error);
|
|
388
|
+
// Check for unique constraint error (SQLite specific error code)
|
|
389
|
+
if (error.code === 'SQLITE_CONSTRAINT') {
|
|
390
|
+
throw new Error(`Duplicate _id: ${docId}`);
|
|
391
|
+
}
|
|
392
|
+
throw error; // Re-throw other errors
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Finds a single document matching the filter.
|
|
397
|
+
* @param filter The query criteria.
|
|
398
|
+
* @param projection Optional. Specifies the fields to return.
|
|
399
|
+
* @returns {Promise<T | null>} The found document or `null`.
|
|
400
|
+
*/
|
|
401
|
+
async findOne(filter, projection) {
|
|
402
|
+
await this.ensureTable();
|
|
403
|
+
const cursor = this.find(filter).limit(1);
|
|
404
|
+
if (projection) {
|
|
405
|
+
cursor.project(projection);
|
|
406
|
+
}
|
|
407
|
+
const results = await cursor.toArray();
|
|
408
|
+
return results.length > 0 ? results[0] : null;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Finds multiple documents matching the filter and returns a cursor.
|
|
412
|
+
* @param filter The query criteria.
|
|
413
|
+
* @returns {FindCursor<T>} A `FindCursor` instance.
|
|
414
|
+
*/
|
|
415
|
+
find(filter = {}) {
|
|
416
|
+
// ensureTable is called by operations on the cursor or by constructor
|
|
417
|
+
return new FindCursor(this.db, this.name, filter);
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Updates a single document matching the filter.
|
|
421
|
+
* @param filter The selection criteria for the update.
|
|
422
|
+
* @param update The modifications to apply.
|
|
423
|
+
* @returns {Promise<UpdateResult>} An object describing the outcome.
|
|
424
|
+
*/
|
|
425
|
+
async updateOne(filter, update) {
|
|
426
|
+
await this.ensureTable();
|
|
427
|
+
// Find the document first (only one)
|
|
428
|
+
const paramsForSelect = [];
|
|
429
|
+
const whereClause = new FindCursor(this.db, this.name, filter)['buildWhereClause'](filter, paramsForSelect);
|
|
430
|
+
const selectSql = `SELECT _id, data FROM "${this.name}" WHERE ${whereClause} LIMIT 1`;
|
|
431
|
+
const rowToUpdate = await this.db.get(selectSql, paramsForSelect);
|
|
432
|
+
if (!rowToUpdate) {
|
|
433
|
+
return { acknowledged: true, matchedCount: 0, modifiedCount: 0, upsertedId: null };
|
|
434
|
+
}
|
|
435
|
+
let currentDoc = JSON.parse(rowToUpdate.data);
|
|
436
|
+
let modified = false;
|
|
437
|
+
// Process update operators
|
|
438
|
+
for (const operator in update) {
|
|
439
|
+
const opArgs = update[operator];
|
|
440
|
+
if (!opArgs)
|
|
441
|
+
continue;
|
|
442
|
+
switch (operator) {
|
|
443
|
+
case '$set':
|
|
444
|
+
for (const path in opArgs) {
|
|
445
|
+
const value = opArgs[path];
|
|
446
|
+
this.setNestedValue(currentDoc, path, value);
|
|
447
|
+
modified = true;
|
|
448
|
+
}
|
|
449
|
+
break;
|
|
450
|
+
case '$unset':
|
|
451
|
+
for (const path in opArgs) {
|
|
452
|
+
this.unsetNestedValue(currentDoc, path);
|
|
453
|
+
modified = true;
|
|
454
|
+
}
|
|
455
|
+
break;
|
|
456
|
+
case '$inc':
|
|
457
|
+
for (const path in opArgs) {
|
|
458
|
+
const value = opArgs[path];
|
|
459
|
+
if (typeof value === 'number') {
|
|
460
|
+
const currentValue = this.getNestedValue(currentDoc, path) || 0;
|
|
461
|
+
if (typeof currentValue === 'number') {
|
|
462
|
+
this.setNestedValue(currentDoc, path, currentValue + value);
|
|
463
|
+
modified = true;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
break;
|
|
468
|
+
case '$push':
|
|
469
|
+
for (const path in opArgs) {
|
|
470
|
+
const value = opArgs[path];
|
|
471
|
+
const currentValue = this.getNestedValue(currentDoc, path);
|
|
472
|
+
if (Array.isArray(currentValue)) {
|
|
473
|
+
if (typeof value === 'object' && value !== null && '$each' in value) {
|
|
474
|
+
if (Array.isArray(value.$each)) {
|
|
475
|
+
currentValue.push(...value.$each);
|
|
476
|
+
modified = true;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
currentValue.push(value);
|
|
481
|
+
modified = true;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
else if (currentValue === undefined) {
|
|
485
|
+
// If the field doesn't exist, create it as an array
|
|
486
|
+
if (typeof value === 'object' && value !== null && '$each' in value) {
|
|
487
|
+
if (Array.isArray(value.$each)) {
|
|
488
|
+
this.setNestedValue(currentDoc, path, [...value.$each]);
|
|
489
|
+
modified = true;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
this.setNestedValue(currentDoc, path, [value]);
|
|
494
|
+
modified = true;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
break;
|
|
499
|
+
case '$pull':
|
|
500
|
+
for (const path in opArgs) {
|
|
501
|
+
const value = opArgs[path];
|
|
502
|
+
const currentValue = this.getNestedValue(currentDoc, path);
|
|
503
|
+
if (Array.isArray(currentValue)) {
|
|
504
|
+
// Simple equality pull
|
|
505
|
+
const newArray = currentValue.filter((item) => {
|
|
506
|
+
if (typeof item === 'object' && typeof value === 'object') {
|
|
507
|
+
// For objects, do a deep comparison (simplified)
|
|
508
|
+
return JSON.stringify(item) !== JSON.stringify(value);
|
|
509
|
+
}
|
|
510
|
+
return item !== value;
|
|
511
|
+
});
|
|
512
|
+
if (newArray.length !== currentValue.length) {
|
|
513
|
+
this.setNestedValue(currentDoc, path, newArray);
|
|
514
|
+
modified = true;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if (modified) {
|
|
522
|
+
// Get the modifiedCount
|
|
523
|
+
const matchedRecordSql = `SELECT COUNT(*) as count FROM "${this.name}" WHERE ${whereClause}`;
|
|
524
|
+
const matchedRecord = await this.db.get(matchedRecordSql, paramsForSelect);
|
|
525
|
+
const matchedCount = matchedRecord?.count || 0;
|
|
526
|
+
// Update the document in SQLite
|
|
527
|
+
const updateSql = `UPDATE "${this.name}" SET data = ? WHERE _id = ?`;
|
|
528
|
+
const updateParams = [JSON.stringify(currentDoc), rowToUpdate._id];
|
|
529
|
+
await this.db.run(updateSql, updateParams);
|
|
530
|
+
return {
|
|
531
|
+
acknowledged: true,
|
|
532
|
+
matchedCount: 1,
|
|
533
|
+
modifiedCount: matchedCount,
|
|
534
|
+
upsertedId: null, // We don't support upsert yet
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
return {
|
|
538
|
+
acknowledged: true,
|
|
539
|
+
matchedCount: 1,
|
|
540
|
+
modifiedCount: 0, // No changes were made
|
|
541
|
+
upsertedId: null,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
// Helper for $set, $inc to handle dot notation
|
|
545
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
546
|
+
setNestedValue(obj, path, value) {
|
|
547
|
+
const keys = path.split('.');
|
|
548
|
+
let current = obj;
|
|
549
|
+
// Navigate to the last parent in the path
|
|
550
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
551
|
+
const key = keys[i];
|
|
552
|
+
// Create nested objects if they don't exist
|
|
553
|
+
if (current[key] === undefined || current[key] === null) {
|
|
554
|
+
current[key] = {};
|
|
555
|
+
}
|
|
556
|
+
else if (typeof current[key] !== 'object') {
|
|
557
|
+
// If it's not an object but we need to go deeper, replace it with an object
|
|
558
|
+
current[key] = {};
|
|
559
|
+
}
|
|
560
|
+
current = current[key];
|
|
561
|
+
}
|
|
562
|
+
// Set the value at the final key
|
|
563
|
+
current[keys[keys.length - 1]] = value;
|
|
564
|
+
}
|
|
565
|
+
// Helper for $unset
|
|
566
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
567
|
+
unsetNestedValue(obj, path) {
|
|
568
|
+
const keys = path.split('.');
|
|
569
|
+
let current = obj;
|
|
570
|
+
// Navigate to the last parent in the path
|
|
571
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
572
|
+
const key = keys[i];
|
|
573
|
+
if (current[key] === undefined || current[key] === null || typeof current[key] !== 'object') {
|
|
574
|
+
return; // Path doesn't exist, nothing to unset
|
|
575
|
+
}
|
|
576
|
+
current = current[key];
|
|
577
|
+
}
|
|
578
|
+
// Delete the property at the final key
|
|
579
|
+
delete current[keys[keys.length - 1]];
|
|
580
|
+
}
|
|
581
|
+
// Helper to get nested value
|
|
582
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
583
|
+
getNestedValue(obj, path) {
|
|
584
|
+
const keys = path.split('.');
|
|
585
|
+
let current = obj;
|
|
586
|
+
// Navigate through the path
|
|
587
|
+
for (let i = 0; i < keys.length; i++) {
|
|
588
|
+
const key = keys[i];
|
|
589
|
+
if (current[key] === undefined) {
|
|
590
|
+
return undefined; // Path doesn't exist
|
|
591
|
+
}
|
|
592
|
+
current = current[key];
|
|
593
|
+
}
|
|
594
|
+
return current;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Deletes a single document matching the filter.
|
|
598
|
+
* @param filter The criteria to select the document to delete.
|
|
599
|
+
* @returns {Promise<DeleteResult>} An object describing the outcome.
|
|
600
|
+
*/
|
|
601
|
+
async deleteOne(filter) {
|
|
602
|
+
await this.ensureTable();
|
|
603
|
+
const paramsForDelete = [];
|
|
604
|
+
const whereClause = new FindCursor(this.db, this.name, filter)['buildWhereClause'](filter, paramsForDelete);
|
|
605
|
+
// Get the number of documents that would be deleted
|
|
606
|
+
const countSql = `SELECT COUNT(*) as count FROM "${this.name}" WHERE ${whereClause}`;
|
|
607
|
+
const countResult = await this.db.get(countSql, paramsForDelete);
|
|
608
|
+
const deleteSql = `
|
|
609
|
+
DELETE FROM "${this.name}"
|
|
610
|
+
WHERE ROWID IN (SELECT ROWID FROM "${this.name}" WHERE ${whereClause} LIMIT 1);
|
|
611
|
+
`;
|
|
612
|
+
await this.db.run(deleteSql, paramsForDelete);
|
|
613
|
+
return { acknowledged: true, deletedCount: countResult ? 1 : 0 };
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Deletes multiple documents matching the filter.
|
|
617
|
+
* @param filter The criteria to select documents to delete.
|
|
618
|
+
* @returns {Promise<DeleteResult>} An object describing the outcome.
|
|
619
|
+
*/
|
|
620
|
+
async deleteMany(filter) {
|
|
621
|
+
await this.ensureTable();
|
|
622
|
+
const paramsForDelete = [];
|
|
623
|
+
const whereClause = new FindCursor(this.db, this.name, filter)['buildWhereClause'](filter, paramsForDelete);
|
|
624
|
+
const deleteSql = `DELETE FROM "${this.name}" WHERE ${whereClause}`;
|
|
625
|
+
const result = await this.db.run(deleteSql, paramsForDelete);
|
|
626
|
+
return { acknowledged: true, deletedCount: result.changes || 0 };
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Counts the number of documents matching the filter.
|
|
630
|
+
* @param filter The criteria to select documents to count.
|
|
631
|
+
* @returns {Promise<number>} The count of matching documents.
|
|
632
|
+
*/
|
|
633
|
+
async countDocuments(filter = {}) {
|
|
634
|
+
await this.ensureTable();
|
|
635
|
+
const paramsForCount = [];
|
|
636
|
+
const whereClause = new FindCursor(this.db, this.name, filter)['buildWhereClause'](filter, paramsForCount);
|
|
637
|
+
const countSql = `SELECT COUNT(*) as count FROM "${this.name}" WHERE ${whereClause}`;
|
|
638
|
+
const result = await this.db.get(countSql, paramsForCount);
|
|
639
|
+
return result?.count || 0;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
exports.MongoLiteCollection = MongoLiteCollection;
|
|
643
|
+
//# sourceMappingURL=collection.js.map
|