@monlite/core 1.2.0 → 1.4.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/README.md CHANGED
@@ -317,6 +317,28 @@ await users.distinct("age", { role: "admin" }); // [28, 31]
317
317
  await users.distinct("tags"); // ["a", "b", "c"]
318
318
  ```
319
319
 
320
+ ### Joins (`$lookup` / `$unwind`)
321
+
322
+ Pull in related documents from another collection with a `lookup` on `findMany`
323
+ — a left join, run as **two queries (no N+1)**, in either storage mode:
324
+
325
+ ```ts
326
+ // Attach each user's orders as an array ($lookup):
327
+ await db.collection("users").findMany({
328
+ lookup: { from: "orders", localField: "_id", foreignField: "user_id", as: "orders" },
329
+ });
330
+ // → [{ _id: "u1", name: "Ali", orders: [ {…}, {…} ] }, …]
331
+
332
+ // Flatten to one row per match with `unwind` ($unwind); use "preserve" to keep
333
+ // rows that have no match (left-outer):
334
+ await db.collection("orders").findMany({
335
+ lookup: { from: "users", localField: "user_id", foreignField: "_id", as: "user", unwind: true },
336
+ });
337
+ // → [{ _id: "o1", user_id: "u1", user: { _id: "u1", name: "Ali" } }, …]
338
+ ```
339
+
340
+ Pass an array of specs to join several collections at once.
341
+
320
342
  ---
321
343
 
322
344
  ## Live queries (reactivity)
@@ -459,9 +481,10 @@ await engine.start();
459
481
  ```
460
482
 
461
483
  Pull / push / two-way replication, last-write-wins (or custom) conflict
462
- resolution, and pluggable adapters (`MongoAdapter`, `MonliteAdapter` for
463
- monlite-to-monlite, `MemoryAdapter` for tests). monlite's ObjectId-compatible
464
- `_id`s map 1:1 to Mongo `_id`s. See the
484
+ resolution, and pluggable adapters (`MongoAdapter`, `PostgresAdapter`,
485
+ `MySqlAdapter`, `MonliteAdapter` for monlite-to-monlite, `MemoryAdapter` for
486
+ tests) keep local monlite as the embedded runtime and a server DB as the cloud
487
+ of record. See the
465
488
  [`@monlite/sync` README](https://www.npmjs.com/package/@monlite/sync) for details.
466
489
 
467
490
  ---
@@ -600,13 +623,14 @@ Redis/Mongo/Qdrant replacement. For scale, keep the real services and
600
623
 
601
624
  ## Drivers & zero dependencies
602
625
 
603
- monlite talks to SQLite through a tiny driver adapter, so it runs on two
626
+ monlite talks to SQLite through a tiny driver adapter, so it runs on
604
627
  interchangeable backends:
605
628
 
606
629
  | Backend | When it's used | Notes |
607
630
  |---|---|---|
608
631
  | **`node:sqlite`** | Built into Node **22.5+** | **Zero dependencies.** Still flagged experimental by Node, so it prints a one-time `ExperimentalWarning`. |
609
632
  | **`better-sqlite3`** | When the package is installed | Battle-tested native driver. Works on Node 18/20/22, no warning. Install it yourself: `npm i better-sqlite3`. |
633
+ | **WASM (browser)** | Via [`@monlite/wasm`](https://www.npmjs.com/package/@monlite/wasm) | Runs monlite **in the browser** on SQLite-WASM (sql.js); pass `driver: wasmDriver(SQL)`. Snapshot persistence to IndexedDB/OPFS. |
610
634
 
611
635
  By default (`driver: "auto"`) monlite uses `better-sqlite3` if it's installed,
612
636
  otherwise falls back to the built-in `node:sqlite`. Force one explicitly:
package/dist/index.cjs CHANGED
@@ -1153,7 +1153,56 @@ var Collection = class {
1153
1153
  return this.db.prepare(`SELECT 1 FROM "${this.name}" WHERE ${clause} LIMIT 1`).get(...params) != null;
1154
1154
  }
1155
1155
  async findMany(args = {}) {
1156
- return this.findManyCore(args);
1156
+ if (!args.lookup) return this.findManyCore(args);
1157
+ const specs = Array.isArray(args.lookup) ? args.lookup : [args.lookup];
1158
+ let rows = this.findManyCore({
1159
+ ...args,
1160
+ select: void 0,
1161
+ lookup: void 0
1162
+ });
1163
+ for (const spec of specs) rows = await this.applyLookup(rows, spec);
1164
+ if (args.select) {
1165
+ rows = rows.map((r) => {
1166
+ const projected = project(r, args.select);
1167
+ for (const spec of specs) projected[spec.as] = r[spec.as];
1168
+ return projected;
1169
+ });
1170
+ }
1171
+ return rows;
1172
+ }
1173
+ /** Resolve one `$lookup` spec against already-fetched rows (2 queries, no N+1). */
1174
+ async applyLookup(rows, spec) {
1175
+ const localValues = [
1176
+ ...new Set(
1177
+ rows.map((r) => r[spec.localField]).filter((v) => v !== void 0 && v !== null)
1178
+ )
1179
+ ];
1180
+ const foreign = localValues.length ? await this.mon.collection(spec.from).findMany({
1181
+ where: { [spec.foreignField]: { in: localValues } }
1182
+ }) : [];
1183
+ const byKey = /* @__PURE__ */ new Map();
1184
+ for (const f of foreign) {
1185
+ const key = f[spec.foreignField];
1186
+ const list = byKey.get(key);
1187
+ if (list) list.push(f);
1188
+ else byKey.set(key, [f]);
1189
+ }
1190
+ if (spec.unwind) {
1191
+ const out = [];
1192
+ for (const r of rows) {
1193
+ const matches = byKey.get(r[spec.localField]) ?? [];
1194
+ if (matches.length === 0) {
1195
+ if (spec.unwind === "preserve") out.push({ ...r, [spec.as]: null });
1196
+ } else {
1197
+ for (const m of matches) out.push({ ...r, [spec.as]: m });
1198
+ }
1199
+ }
1200
+ return out;
1201
+ }
1202
+ return rows.map((r) => ({
1203
+ ...r,
1204
+ [spec.as]: byKey.get(r[spec.localField]) ?? []
1205
+ }));
1157
1206
  }
1158
1207
  async findFirst(args = {}) {
1159
1208
  const rows = await this.findMany({ ...args, take: 1 });
@@ -1615,7 +1664,11 @@ function loadNodeSqlite() {
1615
1664
  return null;
1616
1665
  }
1617
1666
  }
1667
+ function isDriverInstance(d) {
1668
+ return typeof d === "object" && d !== null && typeof d.prepare === "function" && typeof d.exec === "function";
1669
+ }
1618
1670
  function createDriver(filename, options = {}) {
1671
+ if (isDriverInstance(options.driver)) return options.driver;
1619
1672
  const choice = options.driver ?? "auto";
1620
1673
  if (options.encryption) {
1621
1674
  if (choice === "node:sqlite") {