@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 +24 -20
- package/dist/bin/mongolite.js +3 -3
- package/dist/bin/mongolite.js.map +1 -1
- package/dist/cjs/bin/mongolite.js +3 -3
- package/dist/cjs/collection.js +215 -135
- package/dist/cjs/cursors/findCursor.js +58 -5
- package/dist/cjs/utils/aggregation.js +272 -0
- package/dist/cjs/utils/queryOperators.js +49 -0
- package/dist/collection.d.ts +9 -1
- package/dist/collection.js +215 -135
- package/dist/collection.js.map +1 -1
- package/dist/cursors/findCursor.d.ts +7 -1
- package/dist/cursors/findCursor.js +58 -5
- package/dist/cursors/findCursor.js.map +1 -1
- package/dist/utils/aggregation.d.ts +22 -0
- package/dist/utils/aggregation.js +265 -0
- package/dist/utils/aggregation.js.map +1 -0
- package/dist/utils/queryOperators.d.ts +1 -1
- package/dist/utils/queryOperators.js +49 -0
- package/dist/utils/queryOperators.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
|
89
|
-
|
|
|
90
|
-
| MongoDB query API (`$set`, `$elemMatch`,
|
|
91
|
-
| Runs in the browser
|
|
92
|
-
| Runs on the edge (Cloudflare Durable Objects)
|
|
93
|
-
| Zero infrastructure (no server process)
|
|
94
|
-
| TypeScript, strict mode
|
|
95
|
-
| Actively maintained
|
|
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
|
-
| [
|
|
105
|
-
| [
|
|
106
|
-
| [
|
|
107
|
-
| [
|
|
108
|
-
| [
|
|
109
|
-
| [
|
|
110
|
-
| [
|
|
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
|
|
package/dist/bin/mongolite.js
CHANGED
|
@@ -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":";AACA;;;;;;;GAOG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAanD,MAAM,UAAU,SAAS,CAAC,IAAc;IACtC,MAAM,OAAO,GAAe,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IACzF,MAAM,UAAU,GAAa,EAAE,CAAC;IAEhC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QACpD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;QACxB,QAAQ,GAAG,EAAE,CAAC;YACZ,KAAK,YAAY,CAAC;YAClB,KAAK,IAAI;gBACP,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,EAAE,KAAK,CAAC,CAAC;gBACjC,MAAM;YACR,KAAK,cAAc,CAAC;YACpB,KAAK,IAAI;gBACP,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,EAAE,KAAK,CAAC,CAAC;gBACnC,MAAM;YACR,KAAK,WAAW,CAAC;YACjB,KAAK,IAAI;gBACP,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;gBACvB,MAAM;YACR,KAAK,QAAQ,CAAC;YACd,KAAK,UAAU;gBACb,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;gBACpB,MAAM;YACR,KAAK,UAAU;gBACb,OAAO,CAAC,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;gBAC3C,MAAM;YACR,KAAK,aAAa;gBAChB,OAAO,CAAC,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;gBACzC,MAAM;YACR,KAAK,QAAQ,CAAC;YACd,KAAK,IAAI;gBACP,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;gBACpB,MAAM;YACR,KAAK,WAAW,CAAC;YACjB,KAAK,IAAI;gBACP,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;gBACvB,MAAM;YACR;gBACE,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;oBACxB,MAAM,IAAI,KAAK,CAAC,mBAAmB,GAAG,EAAE,CAAC,CAAC;gBAC5C,CAAC;gBACD,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBACrB,MAAM;QACV,CAAC;IACH,CAAC;IAED,2EAA2E;IAC3E,IAAI,CAAC,OAAO,CAAC,QAAQ,IAAI,UAAU,CAAC,CAAC,CAAC;QAAE,OAAO,CAAC,QAAQ,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;IACzE,IAAI,CAAC,OAAO,CAAC,UAAU,IAAI,UAAU,CAAC,CAAC,CAAC;QAAE,OAAO,CAAC,UAAU,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;IAE7E,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,QAAQ;IACf,MAAM,KAAK,GAAG;QACZ,GAAG,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,iDAAiD;QAC3E,EAAE;QACF,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC;QACnB,
|
|
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)',
|
package/dist/cjs/collection.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1401
|
-
|
|
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
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
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 =
|
|
1450
|
-
if (Array.isArray(arrayVal)) {
|
|
1451
|
-
|
|
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
|
|
1458
|
-
|
|
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
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
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
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
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
|
-
|
|
1496
|
-
|
|
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:
|
|
1507
|
-
for (const [field,
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
if (
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
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] =
|
|
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,
|
|
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 (
|
|
1621
|
-
|
|
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);
|