@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 +65 -2
- package/dist/index.cjs +53 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +60 -13
- package/dist/index.d.ts +60 -13
- package/dist/index.js +53 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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),
|
|
690
|
-
sync
|
|
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
|
-
|
|
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 });
|