@monlite/core 0.3.0 β 0.6.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 +153 -9
- package/dist/index.cjs +914 -138
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +260 -11
- package/dist/index.d.ts +260 -11
- package/dist/index.js +906 -139
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
# π monlite
|
|
2
2
|
|
|
3
|
-
>
|
|
4
|
-
>
|
|
5
|
-
|
|
6
|
-
monlite is a local-first document database that lives inside your app as a
|
|
7
|
-
single `.db` file. No server to run, no schema to define, no migrations to
|
|
8
|
-
manage. You get the flexibility of MongoDB, the familiarity of a Prisma-style
|
|
9
|
-
API, and the reliability of SQLite β all in one `npm install`.
|
|
3
|
+
> Your app's local database. A MongoDB-like API and Prisma-style DX in a single
|
|
4
|
+
> file. Zero config, zero migrations, zero server.
|
|
10
5
|
|
|
11
6
|
```ts
|
|
12
7
|
import { createDb } from "@monlite/core";
|
|
@@ -18,7 +13,36 @@ await users.create({ data: { name: "Ali", age: 28 } });
|
|
|
18
13
|
await users.findMany({ where: { age: { gte: 18 } } });
|
|
19
14
|
```
|
|
20
15
|
|
|
21
|
-
That's
|
|
16
|
+
That's the whole setup. Your data is in `app.db`.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Mental model (read this first)
|
|
21
|
+
|
|
22
|
+
monlite is **one database with one query API.** You never have to choose "SQL or
|
|
23
|
+
NoSQL." You only choose, per collection, **where each field is stored:**
|
|
24
|
+
|
|
25
|
+
- **Document mode** (default) β the whole document is stored as JSON. Flexible
|
|
26
|
+
and schema-free, like MongoDB.
|
|
27
|
+
- **Structured mode** (`db.collection(name, { schema })`) β the fields you
|
|
28
|
+
declare become real SQL columns (typed, indexed, joinable). Anything else
|
|
29
|
+
overflows into JSON automatically.
|
|
30
|
+
|
|
31
|
+
> **A schema changes the _storage_, never the _syntax_.**
|
|
32
|
+
> `create`, `find`, `where`, `orderBy`, `groupBy` are identical in both modes and
|
|
33
|
+
> return identical results β structured mode is just faster and SQL-native underneath.
|
|
34
|
+
|
|
35
|
+
Raw SQL is the one optional place SQL becomes visible: the `$queryRaw` escape
|
|
36
|
+
hatch, for joins/CTEs/window functions the document API doesn't cover.
|
|
37
|
+
|
|
38
|
+
| You decide⦠| Document (default) | Structured (`{ schema }`) |
|
|
39
|
+
| --- | --- | --- |
|
|
40
|
+
| **How you query** | `find` / `where` / `orderBy` / `groupBy` | **identical** |
|
|
41
|
+
| **Where a field lives** | JSON `data` blob | a native column (declared) β the rest overflow to JSON |
|
|
42
|
+
| **Pick it when** | the shape is unknown or varies per record | the shape is stable and you want joins, FKs, reporting, or fast native indexes |
|
|
43
|
+
|
|
44
|
+
You can mix both in the same `.db`, and move a collection from document to
|
|
45
|
+
structured later without changing a single query.
|
|
22
46
|
|
|
23
47
|
---
|
|
24
48
|
|
|
@@ -57,6 +81,20 @@ If your data is structured and you already know your schema, plain SQLite adds
|
|
|
57
81
|
nothing on top of monlite β use it directly. monlite earns its keep when your
|
|
58
82
|
documents are dynamic, schema-free, or mirror a cloud NoSQL store.
|
|
59
83
|
|
|
84
|
+
### How monlite compares
|
|
85
|
+
|
|
86
|
+
| | monlite | MongoDB | better-sqlite3 | Prisma + SQLite |
|
|
87
|
+
|---|---|---|---|---|
|
|
88
|
+
| Schema-free documents | β
| β
| β οΈ manual JSON | β |
|
|
89
|
+
| Native typed columns | β
(opt-in) | β | β
| β
|
|
|
90
|
+
| Same API for both | β
| β | β | β |
|
|
91
|
+
| Raw SQL escape hatch | β
| β | β
| β
(`$queryRaw`) |
|
|
92
|
+
| No server / single file | β
| β | β
| β
|
|
|
93
|
+
| No migrations / codegen | β
| β
| β
| β |
|
|
94
|
+
| Aggregation API | β
| β
| β οΈ manual | β οΈ limited |
|
|
95
|
+
| Local-first sync | β
(`@monlite/sync`) | β οΈ Atlas/Realm | β | β |
|
|
96
|
+
| Runtime dependencies | **0** (Node 22.5+) | server | 1 (native) | several |
|
|
97
|
+
|
|
60
98
|
---
|
|
61
99
|
|
|
62
100
|
## Setup
|
|
@@ -124,6 +162,9 @@ await users.createMany({ data: [{ name: "Sara" }, { name: "Omar" }] });
|
|
|
124
162
|
// read
|
|
125
163
|
await users.findById("β¦"); // doc | null
|
|
126
164
|
await users.findFirst({ where: { name: "Ali" } }); // doc | null
|
|
165
|
+
await users.findUnique({ where: { email: "a@x.com" } }); // alias of findFirst
|
|
166
|
+
await users.findFirstOrThrow({ where: { name: "Ali" } }); // throws if missing
|
|
167
|
+
await users.exists({ role: "admin" }); // boolean
|
|
127
168
|
await users.findMany({
|
|
128
169
|
where: { age: { gte: 18 } },
|
|
129
170
|
orderBy: { age: "desc" },
|
|
@@ -167,10 +208,11 @@ where: { age: { gt: 18 } } // gt, gte, lt, lte
|
|
|
167
208
|
where: { role: { in: ["admin", "editor"] } }
|
|
168
209
|
where: { role: { notIn: ["guest"] } }
|
|
169
210
|
|
|
170
|
-
// String (case-sensitive; wildcards are matched literally)
|
|
211
|
+
// String (case-sensitive by default; wildcards are matched literally)
|
|
171
212
|
where: { name: { contains: "li" } }
|
|
172
213
|
where: { name: { startsWith: "A" } }
|
|
173
214
|
where: { name: { endsWith: "i" } }
|
|
215
|
+
where: { name: { contains: "ALI", mode: "insensitive" } } // case-insensitive (ASCII)
|
|
174
216
|
|
|
175
217
|
// Arrays
|
|
176
218
|
where: { tags: { contains: "admin" } } // element membership
|
|
@@ -277,6 +319,108 @@ await users.distinct("tags"); // ["a", "b", "c"]
|
|
|
277
319
|
|
|
278
320
|
---
|
|
279
321
|
|
|
322
|
+
## Structured collections (native SQL columns)
|
|
323
|
+
|
|
324
|
+
By default a collection is **document mode** β schema-free, every field stored
|
|
325
|
+
as JSON. Pass a `schema` to make it a **structured collection**: the declared
|
|
326
|
+
fields become real, typed SQL columns (fast, indexable, joinable, constrainable)
|
|
327
|
+
and any *other* fields overflow into a JSON column. **The CRUD/query API is
|
|
328
|
+
identical** β `find`, `where`, `orderBy`, `groupBy`, `distinct`, updates. As the
|
|
329
|
+
[mental model](#mental-model-read-this-first) says: a schema changes the storage,
|
|
330
|
+
not the syntax.
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
const orders = db.collection("orders", {
|
|
334
|
+
schema: {
|
|
335
|
+
user_id: { type: "TEXT", index: true, references: "users(_id)" },
|
|
336
|
+
amount: "REAL",
|
|
337
|
+
status: { type: "TEXT", notNull: true, default: "pending" },
|
|
338
|
+
meta: "JSON", // objects/arrays, transparently (de)serialized
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// Same API as document collections β but `amount`/`status` are real columns:
|
|
343
|
+
await orders.create({ data: { user_id: "u1", amount: 100, status: "paid", note: "rush" } });
|
|
344
|
+
await orders.findMany({ where: { amount: { gte: 50 }, status: "paid" } });
|
|
345
|
+
await orders.groupBy({ by: ["status"], _sum: { amount: true } });
|
|
346
|
+
|
|
347
|
+
// Undeclared fields (like `note`) still work β they overflow into JSON.
|
|
348
|
+
await orders.findMany({ where: { note: { contains: "rush" } } });
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Because the columns are native, they join, constrain, and index like any SQL
|
|
352
|
+
table β including from the raw SQL hatch with no `json_extract`:
|
|
353
|
+
|
|
354
|
+
```ts
|
|
355
|
+
await db.$queryRaw`
|
|
356
|
+
SELECT u.name, SUM(o.amount) AS revenue
|
|
357
|
+
FROM users u JOIN orders o ON o.user_id = u._id
|
|
358
|
+
GROUP BY u._id
|
|
359
|
+
`;
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Column types: `"TEXT" | "INTEGER" | "REAL" | "BLOB" | "JSON"`. A full column
|
|
363
|
+
definition supports `index`, `unique`, `notNull`, `default`, and `references`.
|
|
364
|
+
|
|
365
|
+
### Do I have to care: JSON vs native columns?
|
|
366
|
+
|
|
367
|
+
- **For correctness β no.** Both modes return identical results through the same API.
|
|
368
|
+
- **For performance & SQL interop β a little.** Native columns + native indexes are
|
|
369
|
+
faster and join/constrain cleanly; JSON is for when the shape is unknown or varies.
|
|
370
|
+
|
|
371
|
+
monlite never hides which is which:
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
orders.mode; // "structured" | "document"
|
|
375
|
+
await db.$schema("orders"); // physical columns: [{ name, type, notNull, primaryKey }, β¦]
|
|
376
|
+
createDb("./app.db", { verbose: (sql) => console.log(sql) }); // see json_extract vs bare columns
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
> **Rule of thumb:** unknown/flexible shape β document (JSON); known/stable shape
|
|
380
|
+
> with heavy joins, reporting, or external SQL tooling β structured (native columns).
|
|
381
|
+
|
|
382
|
+
> Note: structured collections are not yet covered by `@monlite/sync` (document
|
|
383
|
+
> collections are) β that's planned follow-up work.
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## Sync & local-first
|
|
388
|
+
|
|
389
|
+
The companion package [`@monlite/sync`](https://www.npmjs.com/package/@monlite/sync)
|
|
390
|
+
replicates a local monlite database with a remote source of truth β MongoDB
|
|
391
|
+
first β so apps can work offline and converge when reconnected.
|
|
392
|
+
|
|
393
|
+
Opt in with `{ sync: true }` (adds a change feed + tombstones + versioning; zero
|
|
394
|
+
overhead when off), then drive an engine:
|
|
395
|
+
|
|
396
|
+
```ts
|
|
397
|
+
import { createDb } from "@monlite/core";
|
|
398
|
+
import { sync, MongoAdapter } from "@monlite/sync";
|
|
399
|
+
import { MongoClient } from "mongodb";
|
|
400
|
+
|
|
401
|
+
const db = createDb("./app.db", { sync: true });
|
|
402
|
+
const mongo = new MongoClient(uri);
|
|
403
|
+
await mongo.connect();
|
|
404
|
+
|
|
405
|
+
const engine = sync(db, {
|
|
406
|
+
adapter: new MongoAdapter({ client: mongo, db: "app" }),
|
|
407
|
+
collections: "*",
|
|
408
|
+
mode: "two-way", // "pull" | "push" | "two-way"
|
|
409
|
+
conflict: "lww", // or a custom resolver
|
|
410
|
+
interval: 5000,
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
await engine.start();
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Pull / push / two-way replication, last-write-wins (or custom) conflict
|
|
417
|
+
resolution, and pluggable adapters (`MongoAdapter`, `MonliteAdapter` for
|
|
418
|
+
monlite-to-monlite, `MemoryAdapter` for tests). monlite's ObjectId-compatible
|
|
419
|
+
`_id`s map 1:1 to Mongo `_id`s. See the
|
|
420
|
+
[`@monlite/sync` README](https://www.npmjs.com/package/@monlite/sync) for details.
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
280
424
|
## SQL escape hatch
|
|
281
425
|
|
|
282
426
|
When you need full SQL power β complex joins, analytics, cross-collection
|