@monlite/core 1.3.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)
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 });