@semics-tech/mongolite 1.4.0 → 1.4.2

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 CHANGED
@@ -12,10 +12,11 @@ A MongoDB-like client backed by SQLite. Use a familiar MongoDB API with the simp
12
12
  - You want a MongoDB-style API without running a MongoDB server
13
13
  - You need a lightweight, embedded database for local apps, CLIs, or testing
14
14
  - You want simple file-based persistence with zero infrastructure overhead
15
+ - You want a persistent, high-performance local cache in front of a remote MongoDB, one that survives restarts and network interruptions, without giving up Mongo's query semantics (including things that are natural in a document model but awkward in SQL, like layered/override queries)
15
16
 
16
17
  ## Features
17
18
 
18
- - **MongoDB-compatible API** — `insertOne`, `findOne`, `updateOne`, `deleteOne`, `find`, `aggregate`, and more
19
+ - **MongoDB-compatible API** — `insertOne`, `findOne`, `updateOne`, `deleteOne`, `find`, `aggregate` (a subset of stages: `$match`, `$project`, `$addFields`, `$sort`, `$limit`, `$skip`, `$count`, `$group`, `$unwind`, `$lookup`, `$replaceRoot`; unknown stages throw), and more
19
20
  - **SQLite persistence** — single file, zero configuration, works offline
20
21
  - **Automatic `_id` generation** — UUID assigned on insert if not provided
21
22
  - **WAL mode** — Write-Ahead Logging for better concurrent read access, on by default
@@ -74,7 +75,7 @@ main();
74
75
  ## Explore a database from the terminal
75
76
 
76
77
  ```bash
77
- npx mongolite
78
+ npx @semics-tech/mongolite
78
79
  ```
79
80
 
80
81
  Picks up the SQLite files around you, lists the collections it finds inside,
@@ -85,29 +86,32 @@ raw SQL are a menu item away when you want them. See [the CLI docs](./docs/CLI.m
85
86
 
86
87
  ## How does it compare?
87
88
 
88
- | | MongoLite | lowdb | better-sqlite3 (raw) | NeDB | PouchDB | MongoDB |
89
- | -------------------------------------------------------- | ----------- | ------------------------ | -------------------- | -------------------- | ------------------------ | ------- |
90
- | MongoDB query API (`$set`, `$elemMatch`, aggregation...) | ✅ | ❌ (plain object access) | ❌ (raw SQL) | ✅ | ❌ (Mango/CouchDB-style) | ✅ |
91
- | Runs in the browser | ✅ (sql.js) | ✅ | ❌ (native binding) | ✅ | ✅ (IndexedDB) | ❌ |
92
- | Runs on the edge (Cloudflare Durable Objects) | ✅ | ❌ | ❌ (native binding) | ❌ | ❌ | ❌ |
93
- | Zero infrastructure (no server process) | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
94
- | TypeScript, strict mode | ✅ | ✅ | ✅ | ⚠️ (community types) | ⚠️ (community types) | ✅ |
95
- | Actively maintained | ✅ | ✅ | ✅ | ❌ (unmaintained) | ⚠️ (slow-moving) | ✅ |
89
+ | | MongoLite | lowdb | better-sqlite3 (raw) | NeDB | PouchDB | MongoDB |
90
+ | --------------------------------------------------- | ----------- | ------------------------ | -------------------- | --------------------------- | --------------------------- | ------- |
91
+ | MongoDB-style query API (`$set`, `$elemMatch`, ...) | ✅ | ❌ (plain object access) | ❌ (raw SQL) | ✅ (no aggregation) | ❌ (Mango/CouchDB-style) | ✅ |
92
+ | Runs in the browser | ✅ (sql.js) | ✅ | ❌ (native binding) | ✅ | ✅ (IndexedDB) | ❌ |
93
+ | Runs on the edge (Cloudflare Durable Objects) | ✅ | ❌ | ❌ (native binding) | ❌ | ❌ | ❌ |
94
+ | Zero infrastructure (no server process) | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
95
+ | TypeScript, strict mode | ✅ | ✅ | ✅ | ⚠️ (community types) | ⚠️ (community types) | ✅ |
96
+ | Actively maintained | ✅ | ✅ | ✅ | ⚠️ (little recent activity) | ⚠️ (varies; check the repo) | ✅ |
97
+
98
+ _Comparison reflects our understanding of each project's documentation at the time of writing and may be out of date; check each project for current details. MongoLite supports a subset of MongoDB's aggregation stages and has no multi-document transaction API ([API reference](./docs/COMPATIBILITY.md))._
96
99
 
97
100
  MongoLite's niche: the MongoDB query API you already know, running anywhere SQLite runs — a local file, an in-memory test database, the browser, or a Cloudflare Durable Object — without standing up a MongoDB server.
98
101
 
99
102
  ## Documentation
100
103
 
101
- | Topic | Description |
102
- | -------------------------------------------------- | ---------------------------------------------------------------------- |
103
- | [API Reference](./docs/API.md) | Full API docs: methods, query operators, update operators |
104
- | [Change Streams](./docs/CHANGE_STREAMS.md) | Real-time change tracking with `collection.watch()` |
105
- | [Syncing to MongoDB](./docs/SYNC.md) | Replication to an upstream MongoDB, directly or through an HTTP API |
106
- | [JSON Safety](./docs/JSON_SAFETY.md) | Document validation and corrupted data recovery |
107
- | [CLI](./docs/CLI.md) | `npx mongolite` — guided explorer for browsing and querying a database |
108
- | [Query Debugger](./docs/DEBUGGER.md) | The older command-driven REPL (`npx mongolite --repl`) |
109
- | [Benchmarks](./docs/BENCHMARKS.md) | Performance benchmarks and storage characteristics |
110
- | [Cloudflare Durable Objects](./docs/CLOUDFLARE.md) | Using MongoLite inside a Cloudflare Durable Object |
104
+ | Topic | Description |
105
+ | -------------------------------------------------- | ----------------------------------------------------------------------------------- |
106
+ | [API Reference](./docs/API.md) | Full API docs: methods, query operators, update operators |
107
+ | [MongoDB Compatibility](./docs/COMPATIBILITY.md) | Supported, partial and unsupported operators, stages and APIs |
108
+ | [Change Streams](./docs/CHANGE_STREAMS.md) | Real-time change tracking with `collection.watch()` |
109
+ | [Syncing to MongoDB](./docs/SYNC.md) | Replication to an upstream MongoDB, directly or through an HTTP API |
110
+ | [JSON Safety](./docs/JSON_SAFETY.md) | Document validation and corrupted data recovery |
111
+ | [CLI](./docs/CLI.md) | `npx @semics-tech/mongolite` — guided explorer for browsing and querying a database |
112
+ | [Query Debugger](./docs/DEBUGGER.md) | The older command-driven REPL (`npx @semics-tech/mongolite --repl`) |
113
+ | [Benchmarks](./docs/BENCHMARKS.md) | Performance benchmarks and storage characteristics |
114
+ | [Cloudflare Durable Objects](./docs/CLOUDFLARE.md) | Using MongoLite inside a Cloudflare Durable Object |
111
115
 
112
116
  ## Backend Examples
113
117
 
@@ -66,9 +66,9 @@ function showHelp() {
66
66
  `${style.bold('mongolite')} — explore a MongoLite (SQLite-backed) database`,
67
67
  '',
68
68
  style.bold('Usage'),
69
- ' npx mongolite Start the guided explorer',
70
- ' npx mongolite ./app.db Open a specific database',
71
- ' npx mongolite ./app.db users Open a specific collection',
69
+ ' npx @semics-tech/mongolite Start the guided explorer',
70
+ ' npx @semics-tech/mongolite ./app.db Open a specific database',
71
+ ' npx @semics-tech/mongolite ./app.db users Open a specific collection',
72
72
  '',
73
73
  style.bold('Options'),
74
74
  ' -d, --database <path> Database file to open (otherwise you get a list to pick from)',
@@ -1 +1 @@
1
- {"version":3,"file":"mongolite.js","sourceRoot":"","sources":["../../src/bin/mongolite.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mongolite.js","sourceRoot":"","sources":["../../src/bin/mongolite.ts"],"names":[],"mappings":"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"}
@@ -102,9 +102,9 @@ function showHelp() {
102
102
  `${ui_js_1.style.bold('mongolite')} — explore a MongoLite (SQLite-backed) database`,
103
103
  '',
104
104
  ui_js_1.style.bold('Usage'),
105
- ' npx mongolite Start the guided explorer',
106
- ' npx mongolite ./app.db Open a specific database',
107
- ' npx mongolite ./app.db users Open a specific collection',
105
+ ' npx @semics-tech/mongolite Start the guided explorer',
106
+ ' npx @semics-tech/mongolite ./app.db Open a specific database',
107
+ ' npx @semics-tech/mongolite ./app.db users Open a specific collection',
108
108
  '',
109
109
  ui_js_1.style.bold('Options'),
110
110
  ' -d, --database <path> Database file to open (otherwise you get a list to pick from)',
@@ -7,6 +7,7 @@ const indexing_js_1 = require("./utils/indexing.js");
7
7
  const changeStream_js_1 = require("./changeStream.js");
8
8
  const projection_js_1 = require("./utils/projection.js");
9
9
  const queryOperators_js_1 = require("./utils/queryOperators.js");
10
+ const aggregation_js_1 = require("./utils/aggregation.js");
10
11
  /**
11
12
  * Safely stringifies JSON data with validation and error handling.
12
13
  * Prevents storing malformed JSON that could cause parsing issues later.
@@ -138,6 +139,10 @@ function validateDocumentForStorage(doc, path = 'root') {
138
139
  }
139
140
  }
140
141
  }
142
+ /** A $project value that is a computed expression rather than an inclusion/exclusion flag. */
143
+ function isComputedProjection(value) {
144
+ return (typeof value === 'string' && value.startsWith('$')) || (0, aggregation_js_1.isOperatorExpression)(value);
145
+ }
141
146
  /**
142
147
  * MongoLiteCollection provides methods to interact with a specific SQLite table
143
148
  * as if it were a MongoDB collection.
@@ -1046,25 +1051,26 @@ class MongoLiteCollection {
1046
1051
  await this.ensureTable();
1047
1052
  const paramsForDelete = [];
1048
1053
  const whereClause = new findCursor_js_1.FindCursor(this.db, this.name, filter)['buildGuardedWhereClause'](filter, paramsForDelete);
1049
- // Get the number of documents that would be deleted
1050
- const countSql = `SELECT COUNT(*) as count FROM "${this.name}" WHERE ${whereClause} LIMIT 1`;
1051
- const countResult = await this.db.get(countSql, paramsForDelete);
1052
1054
  const deleteSql = `
1053
1055
  DELETE FROM "${this.name}"
1054
1056
  WHERE ROWID IN (SELECT ROWID FROM "${this.name}" WHERE ${whereClause} LIMIT 1);
1055
1057
  `;
1058
+ // Use the statement's own change count rather than a separate COUNT(*):
1059
+ // `LIMIT 1` on an aggregate does not stop the scan, so the pre-count read every
1060
+ // matching row (a full json_extract table scan on unindexed filters).
1061
+ let result;
1056
1062
  try {
1057
- await this.db.run(deleteSql, paramsForDelete);
1063
+ result = await this.db.run(deleteSql, paramsForDelete);
1058
1064
  }
1059
1065
  catch (error) {
1060
1066
  if (error.code === 'SQLITE_BUSY') {
1061
- await this.retryWithBackoff(async () => this.db.run(deleteSql, paramsForDelete), `delete for filter ${JSON.stringify(filter)}`);
1067
+ result = await this.retryWithBackoff(async () => this.db.run(deleteSql, paramsForDelete), `delete for filter ${JSON.stringify(filter)}`);
1062
1068
  }
1063
1069
  else {
1064
1070
  throw error;
1065
1071
  }
1066
1072
  }
1067
- return { acknowledged: true, deletedCount: countResult?.count ? 1 : 0 };
1073
+ return { acknowledged: true, deletedCount: result.changes ? 1 : 0 };
1068
1074
  }
1069
1075
  /**
1070
1076
  * Deletes multiple documents matching the filter.
@@ -1099,11 +1105,7 @@ class MongoLiteCollection {
1099
1105
  */
1100
1106
  async countDocuments(filter = {}) {
1101
1107
  await this.ensureTable();
1102
- const paramsForCount = [];
1103
- const whereClause = new findCursor_js_1.FindCursor(this.db, this.name, filter)['buildGuardedWhereClause'](filter, paramsForCount);
1104
- const countSql = `SELECT COUNT(*) as count FROM "${this.name}" WHERE ${whereClause}`;
1105
- const result = await this.db.get(countSql, paramsForCount);
1106
- return result?.count || 0;
1108
+ return this.find(filter).count();
1107
1109
  }
1108
1110
  /**
1109
1111
  * Finds rows whose stored `data` column is not valid JSON.
@@ -1346,7 +1348,10 @@ class MongoLiteCollection {
1346
1348
  }
1347
1349
  /**
1348
1350
  * Executes an aggregation pipeline on the collection.
1349
- * Supports: $match, $project, $sort, $limit, $skip, $count, $group, $unwind, $addFields.
1351
+ * Supports: $match (incl. $expr), $project, $sort, $limit, $skip, $count, $group
1352
+ * ($sum, $avg, $min, $max, $push, $addToSet, $first, $last, $count), $unwind, $addFields/$set,
1353
+ * $replaceRoot/$replaceWith and $lookup (equality form, local collections).
1354
+ * Unknown stages and accumulators throw.
1350
1355
  * @param pipeline An array of pipeline stage documents.
1351
1356
  * @returns An object with a toArray() method that returns the aggregation result.
1352
1357
  */
@@ -1367,8 +1372,20 @@ class MongoLiteCollection {
1367
1372
  // Only push the initial $match down to SQL when it is the very first stage.
1368
1373
  // Applying a $match that appears after other stages (e.g. after $unwind/$group) too
1369
1374
  // early would change pipeline semantics.
1375
+ for (const stage of pipeline) {
1376
+ const keys = Object.keys(stage);
1377
+ if (keys.length !== 1) {
1378
+ throw new Error('A pipeline stage specification object must contain exactly one field.');
1379
+ }
1380
+ if (!aggregation_js_1.SUPPORTED_STAGES.includes(keys[0])) {
1381
+ throw new Error(`Unrecognized or unsupported aggregation pipeline stage: ${keys[0]}`);
1382
+ }
1383
+ }
1370
1384
  const firstStage = pipeline[0];
1371
- const firstIsMatch = firstStage !== undefined && '$match' in firstStage;
1385
+ // $expr can't be pushed to SQL; it's evaluated in memory instead.
1386
+ const firstIsMatch = firstStage !== undefined &&
1387
+ '$match' in firstStage &&
1388
+ !(0, aggregation_js_1.filterUsesExpr)(firstStage.$match);
1372
1389
  if (firstIsMatch) {
1373
1390
  const matchFilter = firstStage.$match;
1374
1391
  const docs = await this.find(matchFilter).toArray();
@@ -1396,9 +1413,43 @@ class MongoLiteCollection {
1396
1413
  results = results.map((doc) => this.applyAggregateProjection(doc, projection));
1397
1414
  break;
1398
1415
  }
1399
- case '$addFields': {
1400
- const fields = stage['$addFields'];
1401
- results = results.map((doc) => ({ ...doc, ...fields }));
1416
+ case '$addFields':
1417
+ case '$set': {
1418
+ const fields = stage[stageKey];
1419
+ results = results.map((doc) => {
1420
+ const out = structuredClone(doc);
1421
+ for (const [path, expr] of Object.entries(fields)) {
1422
+ const val = (0, aggregation_js_1.evaluateExpression)(doc, expr);
1423
+ if (val === undefined)
1424
+ continue;
1425
+ if (path.includes('.'))
1426
+ this.setNestedValue(out, path, val);
1427
+ else
1428
+ out[path] = val;
1429
+ }
1430
+ return out;
1431
+ });
1432
+ break;
1433
+ }
1434
+ case '$replaceRoot':
1435
+ case '$replaceWith': {
1436
+ const expr = stageKey === '$replaceRoot'
1437
+ ? stage['$replaceRoot'].newRoot
1438
+ : stage['$replaceWith'];
1439
+ if (expr === undefined) {
1440
+ throw new Error('$replaceRoot requires a newRoot expression');
1441
+ }
1442
+ results = results.map((doc) => {
1443
+ const root = (0, aggregation_js_1.evaluateExpression)(doc, expr);
1444
+ if (typeof root !== 'object' || root === null || Array.isArray(root)) {
1445
+ throw new Error("'newRoot' expression must evaluate to an object");
1446
+ }
1447
+ return root;
1448
+ });
1449
+ break;
1450
+ }
1451
+ case '$lookup': {
1452
+ results = await this.applyLookupStage(results, stage['$lookup']);
1402
1453
  break;
1403
1454
  }
1404
1455
  case '$sort': {
@@ -1440,22 +1491,43 @@ class MongoLiteCollection {
1440
1491
  break;
1441
1492
  }
1442
1493
  case '$unwind': {
1443
- const path = typeof stage['$unwind'] === 'string'
1444
- ? stage['$unwind']
1445
- : stage['$unwind'].path;
1446
- const fieldName = path.startsWith('$') ? path.slice(1) : path;
1494
+ const spec = stage['$unwind'];
1495
+ const opts = typeof spec === 'string' ? { path: spec } : spec;
1496
+ const path = opts.path;
1497
+ if (typeof path !== 'string' || !path.startsWith('$')) {
1498
+ throw new Error('$unwind requires a path starting with "$"');
1499
+ }
1500
+ const fieldName = path.slice(1);
1501
+ const preserve = opts.preserveNullAndEmptyArrays === true;
1502
+ const indexField = typeof opts.includeArrayIndex === 'string' ? opts.includeArrayIndex : undefined;
1447
1503
  const unwound = [];
1448
1504
  for (const doc of results) {
1449
- const arrayVal = this.getNestedValue(doc, fieldName);
1450
- if (Array.isArray(arrayVal)) {
1451
- for (const item of arrayVal) {
1452
- const newDoc = { ...doc };
1505
+ const arrayVal = (0, aggregation_js_1.getPath)(doc, fieldName);
1506
+ if (Array.isArray(arrayVal) && arrayVal.length > 0) {
1507
+ arrayVal.forEach((item, idx) => {
1508
+ const newDoc = fieldName.includes('.') ? structuredClone(doc) : { ...doc };
1453
1509
  this.setNestedValue(newDoc, fieldName, item);
1510
+ if (indexField)
1511
+ newDoc[indexField] = idx;
1512
+ unwound.push(newDoc);
1513
+ });
1514
+ }
1515
+ else if (Array.isArray(arrayVal) || arrayVal === null || arrayVal === undefined) {
1516
+ if (preserve) {
1517
+ const newDoc = fieldName.includes('.') ? structuredClone(doc) : { ...doc };
1518
+ if (Array.isArray(arrayVal))
1519
+ this.unsetNestedValue(newDoc, fieldName);
1520
+ if (indexField)
1521
+ newDoc[indexField] = null;
1454
1522
  unwound.push(newDoc);
1455
1523
  }
1456
1524
  }
1457
- else if (arrayVal !== undefined && arrayVal !== null) {
1458
- unwound.push(doc);
1525
+ else {
1526
+ // Non-array operand is treated as a single-element array
1527
+ const newDoc = { ...doc };
1528
+ if (indexField)
1529
+ newDoc[indexField] = null;
1530
+ unwound.push(newDoc);
1459
1531
  }
1460
1532
  }
1461
1533
  results = unwound;
@@ -1472,130 +1544,131 @@ class MongoLiteCollection {
1472
1544
  applyGroupStage(docs, groupSpec) {
1473
1545
  const groups = new Map();
1474
1546
  const idSpec = groupSpec['_id'];
1475
- for (const doc of docs) {
1476
- let groupKey;
1477
- if (idSpec === null) {
1478
- groupKey = null;
1479
- }
1480
- else if (typeof idSpec === 'string' && idSpec.startsWith('$')) {
1481
- groupKey = this.getNestedValue(doc, idSpec.slice(1));
1547
+ // Validate accumulators up front so an unknown one throws even for empty input.
1548
+ const accumulators = [];
1549
+ for (const [field, expr] of Object.entries(groupSpec)) {
1550
+ if (field === '_id')
1551
+ continue;
1552
+ if (typeof expr !== 'object' || expr === null || Array.isArray(expr)) {
1553
+ throw new Error(`The field '${field}' must be an accumulator object`);
1482
1554
  }
1483
- else if (typeof idSpec === 'object' && idSpec !== null) {
1484
- const keyObj = {};
1485
- for (const [k, expr] of Object.entries(idSpec)) {
1486
- if (typeof expr === 'string' && expr.startsWith('$')) {
1487
- keyObj[k] = this.getNestedValue(doc, expr.slice(1));
1488
- }
1489
- else {
1490
- keyObj[k] = expr;
1491
- }
1492
- }
1493
- groupKey = keyObj;
1555
+ const keys = Object.keys(expr);
1556
+ if (keys.length !== 1) {
1557
+ throw new Error(`The field '${field}' must specify exactly one accumulator`);
1494
1558
  }
1495
- else {
1496
- groupKey = idSpec;
1559
+ if (!aggregation_js_1.SUPPORTED_ACCUMULATORS.includes(keys[0])) {
1560
+ throw new Error(`Unknown or unsupported $group accumulator operator: ${keys[0]}`);
1497
1561
  }
1562
+ accumulators.push([field, keys[0], expr[keys[0]]]);
1563
+ }
1564
+ const keyValues = new Map();
1565
+ for (const doc of docs) {
1566
+ let groupKey = idSpec === undefined ? null : (0, aggregation_js_1.evaluateExpression)(doc, idSpec);
1567
+ if (groupKey === undefined)
1568
+ groupKey = null;
1498
1569
  const keyStr = JSON.stringify(groupKey);
1499
1570
  if (!groups.has(keyStr)) {
1500
1571
  groups.set(keyStr, []);
1572
+ keyValues.set(keyStr, groupKey);
1501
1573
  }
1502
1574
  groups.get(keyStr).push(doc);
1503
1575
  }
1504
1576
  const results = [];
1505
1577
  for (const [keyStr, groupDocs] of groups) {
1506
- const groupResult = { _id: JSON.parse(keyStr) };
1507
- for (const [field, expr] of Object.entries(groupSpec)) {
1508
- if (field === '_id')
1509
- continue;
1510
- if (typeof expr === 'object' && expr !== null) {
1511
- const aggOp = Object.keys(expr)[0];
1512
- const aggArg = expr[aggOp];
1513
- switch (aggOp) {
1514
- case '$sum': {
1515
- if (typeof aggArg === 'number') {
1516
- groupResult[field] = groupDocs.length * aggArg;
1517
- }
1518
- else if (typeof aggArg === 'string' && aggArg.startsWith('$')) {
1519
- const fieldName = aggArg.slice(1);
1520
- groupResult[field] = groupDocs.reduce((sum, doc) => {
1521
- const val = this.getNestedValue(doc, fieldName);
1522
- return sum + (typeof val === 'number' ? val : 0);
1523
- }, 0);
1524
- }
1525
- break;
1526
- }
1527
- case '$avg': {
1528
- if (typeof aggArg === 'string' && aggArg.startsWith('$')) {
1529
- const fieldName = aggArg.slice(1);
1530
- const nums = groupDocs
1531
- .map((doc) => this.getNestedValue(doc, fieldName))
1532
- .filter((v) => typeof v === 'number');
1533
- groupResult[field] =
1534
- nums.length > 0 ? nums.reduce((a, b) => a + b, 0) / nums.length : null;
1535
- }
1536
- break;
1537
- }
1538
- case '$min': {
1539
- if (typeof aggArg === 'string' && aggArg.startsWith('$')) {
1540
- const fieldName = aggArg.slice(1);
1541
- const vals = groupDocs
1542
- .map((doc) => this.getNestedValue(doc, fieldName))
1543
- .filter((v) => v !== undefined && v !== null);
1544
- groupResult[field] =
1545
- vals.length > 0 ? vals.reduce((a, b) => (a < b ? a : b)) : null;
1546
- }
1547
- break;
1548
- }
1549
- case '$max': {
1550
- if (typeof aggArg === 'string' && aggArg.startsWith('$')) {
1551
- const fieldName = aggArg.slice(1);
1552
- const vals = groupDocs
1553
- .map((doc) => this.getNestedValue(doc, fieldName))
1554
- .filter((v) => v !== undefined && v !== null);
1555
- groupResult[field] =
1556
- vals.length > 0 ? vals.reduce((a, b) => (a > b ? a : b)) : null;
1557
- }
1558
- break;
1559
- }
1560
- case '$push': {
1561
- if (typeof aggArg === 'string' && aggArg.startsWith('$')) {
1562
- const fieldName = aggArg.slice(1);
1563
- groupResult[field] = groupDocs.map((doc) => this.getNestedValue(doc, fieldName));
1564
- }
1565
- else {
1566
- groupResult[field] = groupDocs.map(() => aggArg);
1567
- }
1568
- break;
1569
- }
1570
- case '$first': {
1571
- if (typeof aggArg === 'string' && aggArg.startsWith('$')) {
1572
- const fieldName = aggArg.slice(1);
1573
- groupResult[field] =
1574
- groupDocs.length > 0 ? this.getNestedValue(groupDocs[0], fieldName) : null;
1575
- }
1576
- break;
1577
- }
1578
- case '$last': {
1579
- if (typeof aggArg === 'string' && aggArg.startsWith('$')) {
1580
- const fieldName = aggArg.slice(1);
1581
- groupResult[field] =
1582
- groupDocs.length > 0
1583
- ? this.getNestedValue(groupDocs[groupDocs.length - 1], fieldName)
1584
- : null;
1585
- }
1586
- break;
1587
- }
1588
- case '$count': {
1589
- groupResult[field] = groupDocs.length;
1590
- break;
1578
+ const groupResult = { _id: keyValues.get(keyStr) };
1579
+ for (const [field, aggOp, aggArg] of accumulators) {
1580
+ const vals = () => groupDocs.map((doc) => (0, aggregation_js_1.evaluateExpression)(doc, aggArg));
1581
+ switch (aggOp) {
1582
+ case '$sum':
1583
+ groupResult[field] = vals().reduce((sum, v) => sum + (typeof v === 'number' ? v : 0), 0);
1584
+ break;
1585
+ case '$avg': {
1586
+ const nums = vals().filter((v) => typeof v === 'number');
1587
+ groupResult[field] =
1588
+ nums.length > 0 ? nums.reduce((a, b) => a + b, 0) / nums.length : null;
1589
+ break;
1590
+ }
1591
+ case '$min': {
1592
+ const v = vals().filter((x) => x !== undefined && x !== null);
1593
+ groupResult[field] = v.length > 0 ? v.reduce((a, b) => (a < b ? a : b)) : null;
1594
+ break;
1595
+ }
1596
+ case '$max': {
1597
+ const v = vals().filter((x) => x !== undefined && x !== null);
1598
+ groupResult[field] = v.length > 0 ? v.reduce((a, b) => (a > b ? a : b)) : null;
1599
+ break;
1600
+ }
1601
+ case '$push':
1602
+ groupResult[field] = vals().filter((v) => v !== undefined);
1603
+ break;
1604
+ case '$addToSet': {
1605
+ const seen = new Set();
1606
+ const out = [];
1607
+ for (const v of vals()) {
1608
+ if (v === undefined)
1609
+ continue;
1610
+ const k = v instanceof Date ? `d:${v.getTime()}` : JSON.stringify(v);
1611
+ if (seen.has(k))
1612
+ continue;
1613
+ seen.add(k);
1614
+ out.push(v);
1591
1615
  }
1616
+ groupResult[field] = out;
1617
+ break;
1618
+ }
1619
+ case '$first': {
1620
+ const v = groupDocs.length > 0 ? (0, aggregation_js_1.evaluateExpression)(groupDocs[0], aggArg) : null;
1621
+ groupResult[field] = v === undefined ? null : v;
1622
+ break;
1623
+ }
1624
+ case '$last': {
1625
+ const v = groupDocs.length > 0
1626
+ ? (0, aggregation_js_1.evaluateExpression)(groupDocs[groupDocs.length - 1], aggArg)
1627
+ : null;
1628
+ groupResult[field] = v === undefined ? null : v;
1629
+ break;
1592
1630
  }
1631
+ case '$count':
1632
+ groupResult[field] = groupDocs.length;
1633
+ break;
1593
1634
  }
1594
1635
  }
1595
1636
  results.push(groupResult);
1596
1637
  }
1597
1638
  return results;
1598
1639
  }
1640
+ /**
1641
+ * Applies a $lookup stage (equality match against another collection in the same database).
1642
+ * @private
1643
+ */
1644
+ async applyLookupStage(docs, spec) {
1645
+ const { from, localField, foreignField, as } = spec;
1646
+ if ('pipeline' in spec || 'let' in spec) {
1647
+ throw new Error('$lookup with pipeline/let is not supported; use localField/foreignField');
1648
+ }
1649
+ if (typeof from !== 'string' ||
1650
+ typeof localField !== 'string' ||
1651
+ typeof foreignField !== 'string' ||
1652
+ typeof as !== 'string') {
1653
+ throw new Error('$lookup requires string from, localField, foreignField and as');
1654
+ }
1655
+ const foreign = (await new MongoLiteCollection(this.db, from, this.options)
1656
+ .find({})
1657
+ .toArray());
1658
+ const key = (v) => JSON.stringify(v === undefined ? null : v);
1659
+ return docs.map((doc) => {
1660
+ const local = (0, aggregation_js_1.getPath)(doc, localField);
1661
+ const localKeys = new Set((Array.isArray(local) ? (local.length > 0 ? local : [null]) : [local]).map(key));
1662
+ const matches = foreign.filter((f) => {
1663
+ const fv = (0, aggregation_js_1.getPath)(f, foreignField);
1664
+ const fvs = Array.isArray(fv) ? [...fv, fv] : [fv];
1665
+ return fvs.some((x) => localKeys.has(key(x)));
1666
+ });
1667
+ const out = { ...doc };
1668
+ this.setNestedValue(out, as, matches);
1669
+ return out;
1670
+ });
1671
+ }
1599
1672
  /**
1600
1673
  * Applies a $project stage to a document.
1601
1674
  * @private
@@ -1609,16 +1682,18 @@ class MongoLiteCollection {
1609
1682
  // for mode-determination purposes, matching real $project behavior.
1610
1683
  const modeInput = {};
1611
1684
  for (const [key, value] of Object.entries(projection)) {
1612
- modeInput[key] = typeof value === 'string' && value.startsWith('$') ? 1 : value;
1685
+ modeInput[key] = isComputedProjection(value) ? 1 : value;
1613
1686
  }
1614
1687
  const mode = (0, projection_js_1.resolveProjectionMode)(modeInput);
1615
- const base = (0, projection_js_1.applyProjectionToDocument)(doc, projection);
1688
+ const base = (0, projection_js_1.applyProjectionToDocument)(doc, modeInput);
1616
1689
  if (mode !== 'include')
1617
1690
  return base;
1618
1691
  const result = { ...base };
1619
1692
  for (const [field, val] of Object.entries(projection)) {
1620
- if (typeof val === 'string' && val.startsWith('$')) {
1621
- result[field] = this.getNestedValue(doc, val.slice(1));
1693
+ if (isComputedProjection(val)) {
1694
+ const computed = (0, aggregation_js_1.evaluateExpression)(doc, val);
1695
+ if (computed !== undefined)
1696
+ this.setNestedValue(result, field, computed);
1622
1697
  }
1623
1698
  }
1624
1699
  return result;
@@ -1646,6 +1721,11 @@ class MongoLiteCollection {
1646
1721
  return false;
1647
1722
  continue;
1648
1723
  }
1724
+ if (key === '$expr') {
1725
+ if (!(0, aggregation_js_1.evaluateExprFilter)(doc, value))
1726
+ return false;
1727
+ continue;
1728
+ }
1649
1729
  if (key.startsWith('$'))
1650
1730
  continue;
1651
1731
  const docValue = this.getNestedValue(doc, key);