@monlite/core 1.3.0 → 2.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.
package/README.md CHANGED
@@ -137,6 +137,17 @@ interface User {
137
137
  const users = db.collection<User>("users");
138
138
  ```
139
139
 
140
+ When you type a collection, queries are **type-checked** (since 2.0): `where`/
141
+ `orderBy` reject unknown fields, and `select` **narrows the return type** to the
142
+ fields you ask for. Prefer schema-free? Use `db.collection("users")` (untyped) —
143
+ it accepts any field, exactly as before.
144
+
145
+ ```ts
146
+ const u = await users.findMany({ select: { name: true } });
147
+ u[0].name; // string ✅ u[0].age // ✗ not selected
148
+ await users.findMany({ where: { naem: "x" } }); // ✗ 'naem' is not a field
149
+ ```
150
+
140
151
  Every stored document gains three system fields:
141
152
 
142
153
  | Field | Type | Notes |
@@ -317,6 +328,28 @@ await users.distinct("age", { role: "admin" }); // [28, 31]
317
328
  await users.distinct("tags"); // ["a", "b", "c"]
318
329
  ```
319
330
 
331
+ ### Joins (`$lookup` / `$unwind`)
332
+
333
+ Pull in related documents from another collection with a `lookup` on `findMany`
334
+ — a left join, run as **two queries (no N+1)**, in either storage mode:
335
+
336
+ ```ts
337
+ // Attach each user's orders as an array ($lookup):
338
+ await db.collection("users").findMany({
339
+ lookup: { from: "orders", localField: "_id", foreignField: "user_id", as: "orders" },
340
+ });
341
+ // → [{ _id: "u1", name: "Ali", orders: [ {…}, {…} ] }, …]
342
+
343
+ // Flatten to one row per match with `unwind` ($unwind); use "preserve" to keep
344
+ // rows that have no match (left-outer):
345
+ await db.collection("orders").findMany({
346
+ lookup: { from: "users", localField: "user_id", foreignField: "_id", as: "user", unwind: true },
347
+ });
348
+ // → [{ _id: "o1", user_id: "u1", user: { _id: "u1", name: "Ali" } }, …]
349
+ ```
350
+
351
+ Pass an array of specs to join several collections at once.
352
+
320
353
  ---
321
354
 
322
355
  ## Live queries (reactivity)
@@ -597,6 +630,10 @@ These target **local / edge / desktop** runtimes — not a distributed cloud-sca
597
630
  Redis/Mongo/Qdrant replacement. For scale, keep the real services and
598
631
  [`@monlite/sync`](https://www.npmjs.com/package/@monlite/sync) to them.
599
632
 
633
+ **Building an Electron app?** [`@monlite/electron`](https://www.npmjs.com/package/@monlite/electron)
634
+ keeps the database in the main process and shares it with renderer windows over
635
+ IPC, with cross-window reactivity.
636
+
600
637
  ---
601
638
 
602
639
  ## Drivers & zero dependencies
@@ -686,8 +723,28 @@ future-proofing.
686
723
  ## Examples
687
724
 
688
725
  Runnable demos live in [`examples/`](examples/): a notes app (CRUD + full-text
689
- search + live queries), AI-agent memory (vector + hybrid search), and local-first
690
- sync. `cd examples && npm install && node notes.mjs`.
726
+ search + live queries), AI-agent memory (vector + hybrid search), local-first
727
+ sync, the cache/queue/cron harness, `$lookup`/`$unwind` joins, and the WASM
728
+ browser backend. `cd examples && npm install && node notes.mjs`.
729
+
730
+ ## Guides
731
+
732
+ - [Schema & migrations](docs/guides/migrations.md) — auto-additive changes and
733
+ `$migrate()` for drop/rename/type-change.
734
+ - [Custom adapters & drivers](docs/guides/custom-adapter.md) — add a sync backend
735
+ or a new SQLite binding/environment.
736
+ - [Migrating to 2.0](docs/guides/v2-migration.md) — the typed-query / select
737
+ changes (types only; runtime unchanged).
738
+
739
+ ## Studio (inspector)
740
+
741
+ Browse a database in your browser — collections, documents, filter queries:
742
+
743
+ ```bash
744
+ npx @monlite/studio app.db
745
+ ```
746
+
747
+ See [`@monlite/studio`](https://www.npmjs.com/package/@monlite/studio).
691
748
 
692
749
  ## Benchmarks
693
750
 
@@ -697,6 +754,12 @@ ops/sec, roughly 2× the raw-driver overhead for the full document API, and it
697
754
  **stays flat on indexed reads where JSON-file stores degrade** (lowdb point reads
698
755
  are ~15× slower at 10k docs).
699
756
 
757
+ ## On-disk format (cross-language)
758
+
759
+ A monlite database is **just a SQLite file** with documented conventions, so any
760
+ language with a SQLite library can read/write it — no port required. The contract
761
+ is in [`docs/FORMAT.md`](docs/FORMAT.md).
762
+
700
763
  ---
701
764
 
702
765
  ## License
package/dist/index.cjs CHANGED
@@ -1153,7 +1153,59 @@ 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
+ const a = args;
1157
+ if (!a.lookup) {
1158
+ return this.findManyCore(a);
1159
+ }
1160
+ const specs = Array.isArray(a.lookup) ? a.lookup : [a.lookup];
1161
+ let rows = this.findManyCore({
1162
+ ...a,
1163
+ select: void 0,
1164
+ lookup: void 0
1165
+ });
1166
+ for (const spec of specs) rows = await this.applyLookup(rows, spec);
1167
+ if (a.select) {
1168
+ rows = rows.map((r) => {
1169
+ const projected = project(r, a.select);
1170
+ for (const spec of specs) projected[spec.as] = r[spec.as];
1171
+ return projected;
1172
+ });
1173
+ }
1174
+ return rows;
1175
+ }
1176
+ /** Resolve one `$lookup` spec against already-fetched rows (2 queries, no N+1). */
1177
+ async applyLookup(rows, spec) {
1178
+ const localValues = [
1179
+ ...new Set(
1180
+ rows.map((r) => r[spec.localField]).filter((v) => v !== void 0 && v !== null)
1181
+ )
1182
+ ];
1183
+ const foreign = localValues.length ? await this.mon.collection(spec.from).findMany({
1184
+ where: { [spec.foreignField]: { in: localValues } }
1185
+ }) : [];
1186
+ const byKey = /* @__PURE__ */ new Map();
1187
+ for (const f of foreign) {
1188
+ const key = f[spec.foreignField];
1189
+ const list = byKey.get(key);
1190
+ if (list) list.push(f);
1191
+ else byKey.set(key, [f]);
1192
+ }
1193
+ if (spec.unwind) {
1194
+ const out = [];
1195
+ for (const r of rows) {
1196
+ const matches = byKey.get(r[spec.localField]) ?? [];
1197
+ if (matches.length === 0) {
1198
+ if (spec.unwind === "preserve") out.push({ ...r, [spec.as]: null });
1199
+ } else {
1200
+ for (const m of matches) out.push({ ...r, [spec.as]: m });
1201
+ }
1202
+ }
1203
+ return out;
1204
+ }
1205
+ return rows.map((r) => ({
1206
+ ...r,
1207
+ [spec.as]: byKey.get(r[spec.localField]) ?? []
1208
+ }));
1157
1209
  }
1158
1210
  async findFirst(args = {}) {
1159
1211
  const rows = await this.findMany({ ...args, take: 1 });