@semics-tech/mongolite 1.0.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 (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +179 -0
  3. package/dist/adapters/browser.d.ts +97 -0
  4. package/dist/adapters/browser.js +103 -0
  5. package/dist/adapters/browser.js.map +1 -0
  6. package/dist/adapters/cloudflare.d.ts +106 -0
  7. package/dist/adapters/cloudflare.js +94 -0
  8. package/dist/adapters/cloudflare.js.map +1 -0
  9. package/dist/bin/mongolite-debug.d.ts +2 -0
  10. package/dist/bin/mongolite-debug.js +115 -0
  11. package/dist/bin/mongolite-debug.js.map +1 -0
  12. package/dist/changeStream.d.ts +114 -0
  13. package/dist/changeStream.js +338 -0
  14. package/dist/changeStream.js.map +1 -0
  15. package/dist/cloudflare.d.ts +39 -0
  16. package/dist/cloudflare.js +39 -0
  17. package/dist/cloudflare.js.map +1 -0
  18. package/dist/collection.d.ts +274 -0
  19. package/dist/collection.js +1706 -0
  20. package/dist/collection.js.map +1 -0
  21. package/dist/cursors/findCursor.d.ts +101 -0
  22. package/dist/cursors/findCursor.js +757 -0
  23. package/dist/cursors/findCursor.js.map +1 -0
  24. package/dist/db.d.ts +116 -0
  25. package/dist/db.js +254 -0
  26. package/dist/db.js.map +1 -0
  27. package/dist/debugger/query-debugger.d.ts +22 -0
  28. package/dist/debugger/query-debugger.js +258 -0
  29. package/dist/debugger/query-debugger.js.map +1 -0
  30. package/dist/index.d.ts +30 -0
  31. package/dist/index.js +40 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/mongo-client.d.ts +56 -0
  34. package/dist/mongo-client.js +55 -0
  35. package/dist/mongo-client.js.map +1 -0
  36. package/dist/types.d.ts +318 -0
  37. package/dist/types.js +2 -0
  38. package/dist/types.js.map +1 -0
  39. package/dist/utils/indexing.d.ts +4 -0
  40. package/dist/utils/indexing.js +43 -0
  41. package/dist/utils/indexing.js.map +1 -0
  42. package/dist/utils/projection.d.ts +16 -0
  43. package/dist/utils/projection.js +98 -0
  44. package/dist/utils/projection.js.map +1 -0
  45. package/dist/utils/queryOperators.d.ts +16 -0
  46. package/dist/utils/queryOperators.js +138 -0
  47. package/dist/utils/queryOperators.js.map +1 -0
  48. package/package.json +92 -0
@@ -0,0 +1,757 @@
1
+ import { applyProjectionToDocument } from '../utils/projection.js';
2
+ /**
3
+ * Safely parses JSON data with fallback mechanisms for malformed JSON.
4
+ * @param jsonString The JSON string to parse
5
+ * @param context Optional context for error messages and recovery
6
+ * @returns The parsed object or a fallback object
7
+ */
8
+ function safeJsonParse(jsonString, context) {
9
+ if (!jsonString || typeof jsonString !== 'string') {
10
+ console.warn(`Invalid JSON string${context ? ` in ${context}` : ''}: not a string or empty`);
11
+ return {};
12
+ }
13
+ try {
14
+ return JSON.parse(jsonString);
15
+ }
16
+ catch (error) {
17
+ // Log the error for debugging
18
+ console.error(`JSON parse error${context ? ` in ${context}` : ''}: ${error instanceof Error ? error.message : 'Unknown error'}`);
19
+ console.error(`Malformed JSON string: ${jsonString.substring(0, 500)}${jsonString.length > 500 ? '...' : ''}`);
20
+ // Attempt to recover from common JSON corruption issues
21
+ try {
22
+ // Try to fix common escaping issues
23
+ let fixedJson = jsonString;
24
+ // Fix double-escaped quotes
25
+ fixedJson = fixedJson.replace(/\\"/g, '"');
26
+ // Fix improperly escaped backslashes
27
+ fixedJson = fixedJson.replace(/\\\\/g, '\\');
28
+ // Try parsing the fixed JSON
29
+ const recovered = JSON.parse(fixedJson);
30
+ console.warn(`Successfully recovered malformed JSON${context ? ` in ${context}` : ''}`);
31
+ return recovered;
32
+ }
33
+ catch (recoveryError) {
34
+ console.error(`JSON recovery failed${context ? ` in ${context}` : ''}: ${recoveryError instanceof Error ? recoveryError.message : 'Unknown error'}`);
35
+ // Last resort: return an empty object with a special marker
36
+ return {
37
+ __mongoLiteCorrupted: true,
38
+ __originalData: jsonString,
39
+ __error: error instanceof Error ? error.message : 'Unknown JSON parse error',
40
+ };
41
+ }
42
+ }
43
+ }
44
+ /**
45
+ * Helper function to convert JavaScript values to SQLite-compatible values.
46
+ * This ensures that boolean values are converted to 0/1 integers that SQLite can handle.
47
+ * Date objects are converted to ISO strings for SQLite storage and comparison.
48
+ * @param value The value to convert
49
+ * @returns A SQLite-compatible value
50
+ */
51
+ function toSQLiteValue(value) {
52
+ if (typeof value === 'boolean') {
53
+ return value ? 1 : 0;
54
+ }
55
+ if (value instanceof Date) {
56
+ return value.toISOString();
57
+ }
58
+ return value;
59
+ }
60
+ /**
61
+ * Returns true for values that json_extract() returns as a nested JSON structure
62
+ * (plain objects/arrays) rather than as an unwrapped scalar. Date instances are
63
+ * scalars from SQLite's point of view (stored as ISO strings), not nested objects.
64
+ */
65
+ function isPlainObjectOrArray(value) {
66
+ return typeof value === 'object' && value !== null && !(value instanceof Date);
67
+ }
68
+ /**
69
+ * Builds a comparable value for a `json_extract(...) = ?` condition against an
70
+ * array element. json_extract() unwraps scalar leaf values (strings, numbers,
71
+ * booleans, ISO date strings) but returns nested objects/arrays as JSON text, so
72
+ * only those need to be JSON-stringified for comparison.
73
+ */
74
+ function toJsonExtractComparableValue(value) {
75
+ if (isPlainObjectOrArray(value)) {
76
+ return JSON.stringify(value);
77
+ }
78
+ return toSQLiteValue(value);
79
+ }
80
+ /**
81
+ * Helper function to recursively restore Date objects from JSON-parsed data.
82
+ * Converts ISO date strings back to Date objects.
83
+ * @param obj The object to process
84
+ * @returns The object with Date strings converted back to Date objects
85
+ */
86
+ function restoreDates(obj) {
87
+ if (obj === null || obj === undefined) {
88
+ return obj;
89
+ }
90
+ if (typeof obj === 'string') {
91
+ // Check if string is an ISO date string
92
+ const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/;
93
+ if (isoDateRegex.test(obj)) {
94
+ const date = new Date(obj);
95
+ // Verify it's a valid date and the string represents the same date
96
+ if (!isNaN(date.getTime()) && date.toISOString() === obj) {
97
+ return date;
98
+ }
99
+ }
100
+ return obj;
101
+ }
102
+ if (Array.isArray(obj)) {
103
+ return obj.map((item) => restoreDates(item));
104
+ }
105
+ if (typeof obj === 'object') {
106
+ const result = {};
107
+ for (const [key, value] of Object.entries(obj)) {
108
+ result[key] = restoreDates(value);
109
+ }
110
+ return result;
111
+ }
112
+ return obj;
113
+ }
114
+ /**
115
+ * Represents a cursor for find operations, allowing chaining of limit, skip, and sort.
116
+ */
117
+ export class FindCursor {
118
+ constructor(db, collectionName, initialFilter, options = {}) {
119
+ this.db = db;
120
+ this.collectionName = collectionName;
121
+ this.options = options;
122
+ this.limitCount = null;
123
+ this.skipCount = null;
124
+ this.sortCriteria = null;
125
+ this.projectionFields = null;
126
+ this.queryParts = this.buildSelectQuery(initialFilter);
127
+ }
128
+ parseJsonPath(path) {
129
+ return `'$.${path.replace(/\./g, '.')}'`;
130
+ }
131
+ /**
132
+ * Helper method to build $in and $nin conditions for both array and non-array fields
133
+ */
134
+ buildArrayComparisonCondition(field, value, operator, params, elementPrefix = 'data') {
135
+ const jsonPath = elementPrefix === 'data'
136
+ ? `json_extract(${elementPrefix}, ${this.parseJsonPath(field)})`
137
+ : `json_extract(${elementPrefix}.value, '$.${field}')`;
138
+ // For Date values, use simple comparison without json_each to avoid JSON serialization issues
139
+ const hasDate = value.some((v) => v instanceof Date);
140
+ if (hasDate) {
141
+ const operator_symbol = operator === '$in' ? '=' : '!=';
142
+ const joiner = operator === '$in' ? ' OR ' : ' AND ';
143
+ const conditions = value.map(() => `${jsonPath} ${operator_symbol} ?`).join(joiner);
144
+ params.push(...value.map((v) => toSQLiteValue(v)));
145
+ if (operator === '$in') {
146
+ return `(${conditions})`;
147
+ }
148
+ else {
149
+ // For $nin with non-array fields, also include NULL
150
+ return `(${jsonPath} IS NULL OR ${conditions})`;
151
+ }
152
+ }
153
+ // For non-Date values, handle both array and non-array fields
154
+ const arrayPath = this.parseJsonPath(field);
155
+ const isArrayCheck = `json_type(json_extract(${elementPrefix}, ${arrayPath})) = 'array'`;
156
+ const placeholders = value.map(() => '?').join(',');
157
+ let arrayCondition;
158
+ let nonArrayCondition;
159
+ if (operator === '$in') {
160
+ arrayCondition = `EXISTS (SELECT 1 FROM json_each(json_extract(${elementPrefix}, ${arrayPath})) WHERE json_each.value IN (${placeholders}))`;
161
+ nonArrayCondition = `(${jsonPath} IN (${placeholders}))`;
162
+ }
163
+ else {
164
+ // $nin
165
+ arrayCondition = `NOT EXISTS (SELECT 1 FROM json_each(json_extract(${elementPrefix}, ${arrayPath})) WHERE json_each.value IN (${placeholders}))`;
166
+ nonArrayCondition = `(${jsonPath} NOT IN (${placeholders}) OR ${jsonPath} IS NULL)`;
167
+ }
168
+ // Push parameters for both array and non-array conditions
169
+ params.push(...value.map((v) => toSQLiteValue(v))); // for array condition
170
+ params.push(...value.map((v) => toSQLiteValue(v))); // for non-array condition
171
+ return `(
172
+ (${isArrayCheck} AND ${arrayCondition}) OR
173
+ (json_type(json_extract(${elementPrefix}, ${arrayPath})) != 'array' AND ${nonArrayCondition})
174
+ )`;
175
+ }
176
+ /**
177
+ * Helper method to generate a comparison condition
178
+ */
179
+ buildComparisonCondition(field, operator, value, params, elementPrefix = 'data') {
180
+ // Handle _id specially as it's a column, not in the JSON
181
+ if (field === '_id' && elementPrefix === 'data') {
182
+ switch (operator) {
183
+ case '$eq':
184
+ params.push(toSQLiteValue(value));
185
+ return '_id = ?';
186
+ case '$ne':
187
+ params.push(toSQLiteValue(value));
188
+ return '_id <> ?';
189
+ case '$in':
190
+ if (Array.isArray(value) && value.length > 0) {
191
+ // Convert each value to SQLite-compatible type
192
+ params.push(...value.map((v) => toSQLiteValue(v)));
193
+ return `_id IN (${value.map(() => '?').join(',')})`;
194
+ }
195
+ return '1=0'; // Empty array, nothing matches
196
+ case '$nin':
197
+ if (Array.isArray(value) && value.length > 0) {
198
+ // Convert each value to SQLite-compatible type
199
+ params.push(...value.map((v) => toSQLiteValue(v)));
200
+ return `_id NOT IN (${value.map(() => '?').join(',')})`;
201
+ }
202
+ return '1=1'; // Empty array, everything matches
203
+ default:
204
+ // Handle other operators that might be applied to _id
205
+ return '1=0'; // Unsupported operator for _id
206
+ }
207
+ }
208
+ // For JSON fields
209
+ const jsonPath = elementPrefix === 'data'
210
+ ? `json_extract(${elementPrefix}, ${this.parseJsonPath(field)})`
211
+ : `json_extract(${elementPrefix}.value, '$.${field}')`;
212
+ switch (operator) {
213
+ case '$eq':
214
+ if (value === null) {
215
+ return `${jsonPath} IS NULL`;
216
+ }
217
+ // Handle Date objects with exact equality
218
+ if (value instanceof Date) {
219
+ params.push(toSQLiteValue(value));
220
+ return `${jsonPath} = ?`;
221
+ }
222
+ // Handle string matching for equality and array matching as par mongodb client
223
+ params.push(toSQLiteValue(value));
224
+ params.push(toSQLiteValue(value));
225
+ return `(${jsonPath} = ? OR ${jsonPath} LIKE '%' || ? || '%')`; // For string matching
226
+ case '$ne':
227
+ if (value === null) {
228
+ return `${jsonPath} IS NOT NULL`;
229
+ }
230
+ params.push(toSQLiteValue(value));
231
+ // In MongoDB, $ne matches documents where the field doesn't exist OR has a different value
232
+ // SQLite's NULL != ? returns NULL (falsy), so we need to explicitly include NULL
233
+ return `(${jsonPath} IS NULL OR ${jsonPath} != ?)`;
234
+ case '$gt':
235
+ params.push(toSQLiteValue(value));
236
+ return `${jsonPath} > ?`;
237
+ case '$gte':
238
+ params.push(toSQLiteValue(value));
239
+ return `${jsonPath} >= ?`;
240
+ case '$lt':
241
+ params.push(toSQLiteValue(value));
242
+ return `${jsonPath} < ?`;
243
+ case '$lte':
244
+ params.push(toSQLiteValue(value));
245
+ return `${jsonPath} <= ?`;
246
+ case '$exists':
247
+ return value ? `${jsonPath} IS NOT NULL` : `${jsonPath} IS NULL`;
248
+ case '$in':
249
+ if (Array.isArray(value) && value.length > 0) {
250
+ return this.buildArrayComparisonCondition(field, value, '$in', params, elementPrefix);
251
+ }
252
+ return '1=0'; // Empty array, nothing matches
253
+ case '$nin':
254
+ if (Array.isArray(value) && value.length > 0) {
255
+ return this.buildArrayComparisonCondition(field, value, '$nin', params, elementPrefix);
256
+ }
257
+ return '1=1'; // Empty array, everything matches
258
+ case '$options':
259
+ // Consumed by $regex handling in buildWhereClause; treat standalone $options as unsupported
260
+ return '1=0';
261
+ case '$regex': {
262
+ // When called from buildComparisonCondition directly (no $options context),
263
+ // extract flags from a RegExp value if provided
264
+ let src;
265
+ let flags = '';
266
+ if (value instanceof RegExp) {
267
+ src = value.source;
268
+ flags = value.flags;
269
+ }
270
+ else if (typeof value === 'string') {
271
+ src = value;
272
+ }
273
+ else {
274
+ return '1=0';
275
+ }
276
+ const textExpr = `CAST(${jsonPath} AS TEXT)`;
277
+ if (flags) {
278
+ params.push(src, flags);
279
+ return `regexp_flags(?, ?, ${textExpr}) = 1`;
280
+ }
281
+ else {
282
+ params.push(src);
283
+ return `regexp(?, ${textExpr}) = 1`;
284
+ }
285
+ }
286
+ case '$size':
287
+ // Matches arrays with exactly N elements
288
+ params.push(value);
289
+ return `json_array_length(${jsonPath}) = ?`;
290
+ case '$type': {
291
+ // Map BSON type numbers/names to SQLite json_type() values
292
+ const mapBsonType = (bsonType) => {
293
+ const typeMap = {
294
+ 1: ['real', 'integer'],
295
+ 2: ['text'],
296
+ 3: ['object'],
297
+ 4: ['array'],
298
+ 8: ['true', 'false'],
299
+ 9: ['text'],
300
+ 10: ['null'],
301
+ 16: ['integer'],
302
+ 18: ['integer'],
303
+ double: ['real', 'integer'],
304
+ string: ['text'],
305
+ object: ['object'],
306
+ array: ['array'],
307
+ bool: ['true', 'false'],
308
+ boolean: ['true', 'false'],
309
+ date: ['text'],
310
+ null: ['null'],
311
+ int: ['integer'],
312
+ long: ['integer'],
313
+ number: ['real', 'integer'],
314
+ };
315
+ return typeMap[bsonType] ?? [];
316
+ };
317
+ const typeArgs = Array.isArray(value) ? value : [value];
318
+ const allSqlTypes = typeArgs.flatMap((t) => mapBsonType(t));
319
+ const uniqueTypes = [...new Set(allSqlTypes)];
320
+ if (uniqueTypes.length === 0)
321
+ return '1=0';
322
+ // Use json_type(json, path) form which correctly identifies type of JSON values
323
+ const typePath = elementPrefix === 'data'
324
+ ? `json_type(${elementPrefix}, '$.${field}')`
325
+ : `json_type(${elementPrefix}.value, '$.${field}')`;
326
+ const typeConds = uniqueTypes.map(() => `${typePath} = ?`).join(' OR ');
327
+ params.push(...uniqueTypes);
328
+ return uniqueTypes.length === 1 ? `${typePath} = ?` : `(${typeConds})`;
329
+ }
330
+ case '$mod': {
331
+ // Matches where field % divisor === remainder
332
+ if (Array.isArray(value) && value.length === 2) {
333
+ params.push(value[0], value[1]);
334
+ return `(CAST(${jsonPath} AS INTEGER) % ? = ?)`;
335
+ }
336
+ return '1=0';
337
+ }
338
+ default:
339
+ return '1=0'; // Unsupported operator
340
+ }
341
+ }
342
+ /**
343
+ * Builds a regex condition with optional flags from $options.
344
+ */
345
+ buildRegexCondition(field, value, options, params, elementPrefix = 'data') {
346
+ let src;
347
+ let flags = typeof options === 'string' ? options : '';
348
+ if (value instanceof RegExp) {
349
+ src = value.source;
350
+ flags = value.flags || flags;
351
+ }
352
+ else if (typeof value === 'string') {
353
+ src = value;
354
+ }
355
+ else {
356
+ return '1=0';
357
+ }
358
+ const jsonPath = elementPrefix === 'data'
359
+ ? `json_extract(${elementPrefix}, ${this.parseJsonPath(field)})`
360
+ : `json_extract(${elementPrefix}.value, '$.${field}')`;
361
+ const textExpr = `CAST(${jsonPath} AS TEXT)`;
362
+ if (flags) {
363
+ params.push(src, flags);
364
+ return `regexp_flags(?, ?, ${textExpr}) = 1`;
365
+ }
366
+ else {
367
+ params.push(src);
368
+ return `regexp(?, ${textExpr}) = 1`;
369
+ }
370
+ }
371
+ /**
372
+ * Builds a subquery for array operations like $all and $elemMatch
373
+ */
374
+ buildArraySubquery(field, operator, value, params) {
375
+ const arrayPath = this.parseJsonPath(field);
376
+ const arrayTypeCheck = `json_type(json_extract(data, ${arrayPath})) = 'array'`;
377
+ if (operator === '$all') {
378
+ if (Array.isArray(value) && value.length > 0) {
379
+ const allConditions = value
380
+ .map((item) => {
381
+ if (typeof item === 'object' &&
382
+ item !== null &&
383
+ !Array.isArray(item) &&
384
+ !(item instanceof Date)) {
385
+ // For object elements, we need to check if any array element matches all properties
386
+ const objectConditions = [];
387
+ for (const [key, val] of Object.entries(item)) {
388
+ // For nested objects in arrays, we need to match the full object
389
+ params.push(toJsonExtractComparableValue(val));
390
+ objectConditions.push(`json_extract(json_each.value, '$.${key}') = ?`);
391
+ }
392
+ const condition = objectConditions.join(' AND ');
393
+ return `EXISTS (SELECT 1 FROM json_each(json_extract(data, ${arrayPath})) WHERE ${condition})`;
394
+ }
395
+ else {
396
+ // For simple values, use direct comparison
397
+ params.push(toSQLiteValue(item));
398
+ return `EXISTS (SELECT 1 FROM json_each(json_extract(data, ${arrayPath})) WHERE json_each.value = ?)`;
399
+ }
400
+ })
401
+ .join(' AND ');
402
+ return `(${arrayTypeCheck} AND ${allConditions})`;
403
+ }
404
+ return '1=0'; // Empty $all array, nothing matches
405
+ }
406
+ else if (operator === '$in') {
407
+ // Handle $in with array values that may contain objects or scalars
408
+ if (Array.isArray(value) && value.length > 0) {
409
+ const hasObjects = value.some((v) => typeof v === 'object' && v !== null && !Array.isArray(v) && !(v instanceof Date));
410
+ if (hasObjects) {
411
+ // When $in has objects, match if any array element matches any of the objects
412
+ const orConditions = value
413
+ .map((item) => {
414
+ if (typeof item === 'object' &&
415
+ item !== null &&
416
+ !Array.isArray(item) &&
417
+ !(item instanceof Date)) {
418
+ // For object elements in $in, check if array element matches all properties
419
+ const objectConditions = [];
420
+ for (const [key, val] of Object.entries(item)) {
421
+ params.push(toJsonExtractComparableValue(val));
422
+ objectConditions.push(`json_extract(json_each.value, '$.${key}') = ?`);
423
+ }
424
+ const condition = objectConditions.join(' AND ');
425
+ return `EXISTS (SELECT 1 FROM json_each(json_extract(data, ${arrayPath})) WHERE ${condition})`;
426
+ }
427
+ else {
428
+ // For scalar values
429
+ params.push(toSQLiteValue(item));
430
+ return `EXISTS (SELECT 1 FROM json_each(json_extract(data, ${arrayPath})) WHERE json_each.value = ?)`;
431
+ }
432
+ })
433
+ .join(' OR ');
434
+ return `(${arrayTypeCheck} AND (${orConditions}))`;
435
+ }
436
+ else {
437
+ // All scalar values - use the existing logic for scalar comparison
438
+ return this.buildArrayComparisonCondition(field, value, '$in', params);
439
+ }
440
+ }
441
+ return '1=0'; // Empty $in array, nothing matches
442
+ }
443
+ else if (operator === '$elemMatch') {
444
+ let elemMatchSubquery = `EXISTS (
445
+ SELECT 1
446
+ FROM json_each(json_extract(data, ${arrayPath})) as array_elements
447
+ WHERE `;
448
+ if (typeof value === 'object' && value !== null) {
449
+ const conditions = this.buildObjectConditions(value, params, 'array_elements');
450
+ elemMatchSubquery += conditions;
451
+ }
452
+ else {
453
+ // Simple equality check for the entire element
454
+ params.push(toSQLiteValue(value));
455
+ elemMatchSubquery += `array_elements.value = ?`;
456
+ }
457
+ elemMatchSubquery += ')';
458
+ return `${arrayTypeCheck} AND ${elemMatchSubquery}`;
459
+ }
460
+ return '1=0'; // Unsupported array operator
461
+ }
462
+ /**
463
+ * Builds conditions for an object with potentially nested operators
464
+ */
465
+ buildObjectConditions(obj, params, elementPrefix = 'data') {
466
+ const conditions = [];
467
+ for (const [key, value] of Object.entries(obj)) {
468
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
469
+ // Check if this is an operator object
470
+ const operatorKeys = Object.keys(value);
471
+ const isOperatorObject = operatorKeys.every((k) => k.startsWith('$'));
472
+ if (isOperatorObject) {
473
+ // This is an object with operators like { age: { $gt: 30 } }
474
+ const opConditions = operatorKeys.map((op) => this.buildComparisonCondition(key, op, value[op], params, elementPrefix));
475
+ conditions.push(`(${opConditions.join(' AND ')})`);
476
+ }
477
+ else {
478
+ // This is a nested object, handle recursively
479
+ // In this implementation, we'd need to handle dot notation
480
+ // which might require more complex logic
481
+ conditions.push('1=0'); // Placeholder for nested object handling
482
+ }
483
+ }
484
+ else {
485
+ // Simple equality
486
+ conditions.push(this.buildComparisonCondition(key, '$eq', value, params, elementPrefix));
487
+ }
488
+ }
489
+ return conditions.length > 0 ? conditions.join(' AND ') : '1=1';
490
+ }
491
+ /**
492
+ * Builds the SQL condition(s) for a set of operators applied to one field, e.g.
493
+ * `{ age: { $gt: 30, $lt: 50 } }`. Recurses for `$not`, so a negated operator set on the same
494
+ * field (`{ age: { $not: { $gt: 30 } } }`) gets wrapped in `NOT (...)` rather than falling
495
+ * through to buildComparisonCondition's unsupported-operator default.
496
+ */
497
+ buildFieldOperatorConditions(field, ops, params) {
498
+ const regexOptions = ops['$options'];
499
+ return Object.entries(ops)
500
+ .filter(([op]) => op !== '$options') // $options is consumed alongside $regex
501
+ .map(([op, opValue]) => {
502
+ if (op === '$not') {
503
+ return `NOT (${this.buildFieldOperatorConditions(field, opValue, params)})`;
504
+ }
505
+ if (op === '$elemMatch' || op === '$all' || op === '$in') {
506
+ // Check if $in has objects that need array subquery handling
507
+ if (op === '$in' &&
508
+ Array.isArray(opValue) &&
509
+ opValue.some((v) => typeof v === 'object' && v !== null && !Array.isArray(v) && !(v instanceof Date))) {
510
+ return this.buildArraySubquery(field, op, opValue, params);
511
+ }
512
+ if (op === '$elemMatch' || op === '$all') {
513
+ return this.buildArraySubquery(field, op, opValue, params);
514
+ }
515
+ }
516
+ if (op === '$regex') {
517
+ return this.buildRegexCondition(field, opValue, regexOptions, params);
518
+ }
519
+ return this.buildComparisonCondition(field, op, opValue, params);
520
+ })
521
+ .join(' AND ');
522
+ }
523
+ buildWhereClause(filter, params) {
524
+ const conditions = [];
525
+ for (const key of Object.keys(filter)) {
526
+ const value = filter[key];
527
+ // Handle logical operators
528
+ if (key === '$and' && filter.$and) {
529
+ const andConditions = filter.$and
530
+ .map((subFilter) => `(${this.buildWhereClause(subFilter, params)})`)
531
+ .join(' AND ');
532
+ conditions.push(`(${andConditions})`);
533
+ }
534
+ else if (key === '$or' && filter.$or) {
535
+ const orConditions = filter.$or
536
+ .map((subFilter) => `(${this.buildWhereClause(subFilter, params)})`)
537
+ .join(' OR ');
538
+ conditions.push(`(${orConditions})`);
539
+ }
540
+ else if (key === '$nor' && filter.$nor) {
541
+ const norConditions = filter.$nor
542
+ .map((subFilter) => `(${this.buildWhereClause(subFilter, params)})`)
543
+ .join(' OR ');
544
+ conditions.push(`NOT (${norConditions})`);
545
+ }
546
+ else if (key === '$not' && filter.$not) {
547
+ // Create a nested filter with the $not contents
548
+ const nestedFilter = {
549
+ [Object.keys(filter.$not)[0]]: filter.$not[Object.keys(filter.$not)[0]],
550
+ };
551
+ const notClause = this.buildWhereClause(nestedFilter, params);
552
+ conditions.push(`NOT (${notClause})`);
553
+ }
554
+ // Handle text search
555
+ else if (key === '$text' && filter.$text) {
556
+ const textSearch = filter.$text;
557
+ const search = textSearch.$search;
558
+ if (typeof search === 'string' && search.trim() !== '') {
559
+ conditions.push(`data LIKE ?`);
560
+ params.push(`%${search}%`); // Simple LIKE search
561
+ }
562
+ else {
563
+ conditions.push('1=0'); // No valid search term, nothing matches
564
+ }
565
+ }
566
+ // Handle field existence checks
567
+ else if (key === '$exists' && filter.$exists) {
568
+ const existsConditions = Object.entries(filter.$exists)
569
+ .map(([field, exists]) => this.buildComparisonCondition(field, '$exists', exists, params))
570
+ .join(' AND ');
571
+ conditions.push(`(${existsConditions})`);
572
+ }
573
+ // Handle array operations
574
+ else if ((key === '$all' || key === '$elemMatch') && filter[key]) {
575
+ const arrayConditions = Object.entries(filter[key])
576
+ .map(([field, value]) => this.buildArraySubquery(field, key, value, params))
577
+ .join(' AND ');
578
+ conditions.push(`(${arrayConditions})`);
579
+ }
580
+ // Handle regular field conditions
581
+ else if (!key.startsWith('$')) {
582
+ // It's a field path
583
+ if (key === '_id') {
584
+ if (typeof value === 'string') {
585
+ conditions.push(this.buildComparisonCondition('_id', '$eq', value, params));
586
+ }
587
+ else if (typeof value === 'object' && value !== null) {
588
+ // Handle operators on _id
589
+ const opConditions = Object.entries(value)
590
+ .map(([op, opValue]) => this.buildComparisonCondition('_id', op, opValue, params))
591
+ .join(' AND ');
592
+ conditions.push(`(${opConditions})`);
593
+ }
594
+ }
595
+ else {
596
+ // Regular field with JSON path
597
+ if (value instanceof Date) {
598
+ // Handle Date objects for simple equality
599
+ const jsonPath = this.parseJsonPath(key);
600
+ conditions.push(`json_extract(data, ${jsonPath}) = ?`);
601
+ params.push(toSQLiteValue(value));
602
+ }
603
+ else if (value instanceof RegExp) {
604
+ // Support bare RegExp values: { field: /pattern/i }
605
+ conditions.push(this.buildRegexCondition(key, value, value.flags, params));
606
+ }
607
+ else if (typeof value === 'object' && value !== null) {
608
+ // Check if any key is an operator
609
+ const hasOperators = Object.keys(value).some((k) => k.startsWith('$'));
610
+ if (hasOperators) {
611
+ conditions.push(`(${this.buildFieldOperatorConditions(key, value, params)})`);
612
+ }
613
+ else {
614
+ // This is an object equality check (exact match)
615
+ const jsonPath = this.parseJsonPath(key);
616
+ conditions.push(`json_extract(data, ${jsonPath}) = ?`);
617
+ params.push(JSON.stringify(value));
618
+ }
619
+ }
620
+ else {
621
+ // Simple equality check
622
+ conditions.push(this.buildComparisonCondition(key, '$eq', value, params));
623
+ }
624
+ }
625
+ }
626
+ }
627
+ return conditions.length > 0 ? conditions.join(' AND ') : '1=1';
628
+ }
629
+ buildSelectQuery(filter) {
630
+ const params = [];
631
+ const whereClause = this.buildWhereClause(filter, params);
632
+ const sql = `SELECT _id, data FROM "${this.collectionName}" WHERE ${whereClause}`;
633
+ if (this.options.verbose) {
634
+ console.log(`Building SQL query for collection "${this.collectionName}"`);
635
+ console.log(`Filter: ${JSON.stringify(filter)}`);
636
+ console.log(`Where Clause: ${whereClause}`);
637
+ console.log(`SQL Query: ${sql}`);
638
+ console.log(`Parameters: ${JSON.stringify(params)}`);
639
+ }
640
+ return { sql, params };
641
+ }
642
+ /**
643
+ * Specifies the maximum number of documents the cursor will return.
644
+ * @param count The number of documents to limit to.
645
+ * @returns The `FindCursor` instance for chaining.
646
+ */
647
+ limit(count) {
648
+ if (count < 0)
649
+ throw new Error('Limit must be a non-negative number.');
650
+ this.limitCount = count;
651
+ return this;
652
+ }
653
+ /**
654
+ * Specifies the number of documents to skip.
655
+ * @param count The number of documents to skip.
656
+ * @returns The `FindCursor` instance for chaining.
657
+ */
658
+ skip(count) {
659
+ if (count < 0)
660
+ throw new Error('Skip must be a non-negative number.');
661
+ this.skipCount = count;
662
+ return this;
663
+ }
664
+ /**
665
+ * Specifies the sorting order for the documents.
666
+ * @param sortCriteria An object defining sort order (e.g., `{ age: -1, name: 1 }`).
667
+ * @returns The `FindCursor` instance for chaining.
668
+ */
669
+ sort(sortCriteria) {
670
+ this.sortCriteria = sortCriteria;
671
+ return this;
672
+ }
673
+ /**
674
+ * Specifies the fields to return (projection).
675
+ * @param projection An object where keys are field names and values are 1 (include) or 0 (exclude).
676
+ * `_id` is included by default unless explicitly excluded.
677
+ * @returns The `FindCursor` instance for chaining.
678
+ * @remarks When using projections, the returned documents may have undefined fields at runtime,
679
+ * though the TypeScript type will still be `T`. Consumers should be aware that projected-out
680
+ * fields will be undefined.
681
+ */
682
+ project(projection) {
683
+ this.projectionFields = projection;
684
+ return this;
685
+ }
686
+ /**
687
+ * Applies projection to a document, returning only the specified fields.
688
+ * @param doc The full document to apply projection to
689
+ * @returns The projected document. When a projection is applied, some fields may be undefined
690
+ * at runtime despite the return type being `T`. This trade-off provides better developer
691
+ * experience for the common case (no projection) while maintaining type compatibility.
692
+ * @private
693
+ */
694
+ applyProjection(doc) {
695
+ return applyProjectionToDocument(doc, this.projectionFields);
696
+ }
697
+ /**
698
+ * Executes the query and returns all matching documents as an array.
699
+ * @returns A promise that resolves to an array of documents.
700
+ */
701
+ async toArray() {
702
+ let finalSql = this.queryParts.sql;
703
+ const finalParams = [...this.queryParts.params];
704
+ if (this.sortCriteria) {
705
+ const sortClauses = Object.entries(this.sortCriteria).map(([field, order]) => {
706
+ if (field === '_id') {
707
+ return `_id ${order === 1 ? 'ASC' : 'DESC'}`;
708
+ }
709
+ return `json_extract(data, ${this.parseJsonPath(field)}) ${order === 1 ? 'ASC' : 'DESC'}`;
710
+ });
711
+ if (sortClauses.length > 0) {
712
+ finalSql += ` ORDER BY ${sortClauses.join(', ')}`;
713
+ }
714
+ }
715
+ if (this.limitCount !== null) {
716
+ finalSql += ` LIMIT ?`;
717
+ finalParams.push(this.limitCount);
718
+ }
719
+ if (this.skipCount !== null) {
720
+ if (this.limitCount === null) {
721
+ // SQLite requires a LIMIT if OFFSET is used.
722
+ // Use a very large number if no limit is specified.
723
+ finalSql += ` LIMIT -1`; // Or a large number like 999999999
724
+ }
725
+ finalSql += ` OFFSET ?`;
726
+ finalParams.push(this.skipCount);
727
+ }
728
+ // Handle malformed json in the database
729
+ const rows = await this.db.all(finalSql, finalParams);
730
+ return rows.map((row) => {
731
+ const parsedData = safeJsonParse(row.data, `findCursor toArray for document ${row._id}`);
732
+ const restoredData = restoreDates(parsedData);
733
+ const doc = { _id: row._id, ...restoredData };
734
+ return this.applyProjection(doc);
735
+ });
736
+ }
737
+ /**
738
+ * Executes the query and returns the first matching document.
739
+ * @returns A promise that resolves to the first matching document or null if no matches are found.
740
+ * @throws Error if the query fails.
741
+ */
742
+ async first() {
743
+ const results = await this.limit(1).toArray();
744
+ return results.length > 0 ? results[0] : null;
745
+ }
746
+ /**
747
+ * Executes the query and returns a count of matching documents.
748
+ * @returns A promise that resolves to the count of matching documents.
749
+ */
750
+ async count() {
751
+ const whereClause = this.buildWhereClause({}, []);
752
+ const countSql = `SELECT COUNT(*) as count FROM "${this.collectionName}" WHERE ${whereClause}`;
753
+ const result = await this.db.get(countSql);
754
+ return result?.count ?? 0; // Return 0 if result is undefined
755
+ }
756
+ }
757
+ //# sourceMappingURL=findCursor.js.map