@monlite/core 0.1.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/LICENSE +21 -0
- package/README.md +343 -0
- package/dist/index.cjs +814 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +271 -0
- package/dist/index.d.ts +271 -0
- package/dist/index.js +802 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Emad Jumaah
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
# 🌙 monlite
|
|
2
|
+
|
|
3
|
+
> An embedded document database for TypeScript apps.
|
|
4
|
+
> MongoDB-like API. Prisma-like DX. SQLite under the hood. Zero config.
|
|
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`.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { createDb } from "@monlite/core";
|
|
13
|
+
|
|
14
|
+
const db = createDb("./app.db");
|
|
15
|
+
const users = db.collection("users");
|
|
16
|
+
|
|
17
|
+
await users.create({ data: { name: "Ali", age: 28 } });
|
|
18
|
+
await users.findMany({ where: { age: { gte: 18 } } });
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
That's it. No setup. No config. Your data is in `app.db`.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install @monlite/core
|
|
29
|
+
# or: pnpm add @monlite/core / yarn add @monlite/core
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
monlite uses [`better-sqlite3`](https://github.com/WiseLibs/better-sqlite3) under
|
|
33
|
+
the hood (its only runtime dependency). Node 18+ is required.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## When to use monlite
|
|
38
|
+
|
|
39
|
+
| Situation | Use |
|
|
40
|
+
|---|---|
|
|
41
|
+
| Desktop / Electron / Tauri app needing local data | ✅ monlite |
|
|
42
|
+
| CLI tool with persistent state | ✅ monlite |
|
|
43
|
+
| Local-first app syncing with a cloud MongoDB | ✅ monlite |
|
|
44
|
+
| Prototype / MVP needing fast iteration, flexible schema | ✅ monlite |
|
|
45
|
+
| Server app with Postgres/MySQL | ❌ use your DB directly |
|
|
46
|
+
| Strictly relational, known, stable schema | ❌ use SQLite directly |
|
|
47
|
+
| Production cloud database | ❌ use MongoDB / a managed DB |
|
|
48
|
+
|
|
49
|
+
If your data is structured and you already know your schema, plain SQLite adds
|
|
50
|
+
nothing on top of monlite — use it directly. monlite earns its keep when your
|
|
51
|
+
documents are dynamic, schema-free, or mirror a cloud NoSQL store.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Setup
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { createDb } from "@monlite/core";
|
|
59
|
+
|
|
60
|
+
const db = createDb("./app.db"); // creates the file if missing
|
|
61
|
+
const mem = createDb(":memory:"); // in-memory database
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Options
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
const db = createDb("./app.db", {
|
|
68
|
+
autoIndex: true, // auto-create indexes on hot JSON paths (default: true)
|
|
69
|
+
autoIndexAfter: 10, // create an index after a path is queried N times (default: 10)
|
|
70
|
+
readonly: false, // open read-only (default: false)
|
|
71
|
+
wal: true, // use WAL journal mode (default: true)
|
|
72
|
+
verbose: (sql) => console.log(sql), // log every executed SQL statement
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Collections
|
|
79
|
+
|
|
80
|
+
Collections are created automatically on first access — no schema, no migration,
|
|
81
|
+
no definition needed. Pass a type for full inference.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
interface User {
|
|
85
|
+
name: string;
|
|
86
|
+
age?: number;
|
|
87
|
+
address?: { city: string };
|
|
88
|
+
tags?: string[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const users = db.collection<User>("users");
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Every stored document gains three system fields:
|
|
95
|
+
|
|
96
|
+
| Field | Type | Notes |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| `_id` | `string` | Auto-generated, ObjectId-compatible (24 hex chars), time-sortable. Provide your own to override. |
|
|
99
|
+
| `created_at` | `number` | Unix epoch milliseconds, set on insert. |
|
|
100
|
+
| `updated_at` | `number` | Unix epoch milliseconds, bumped on every update. |
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## CRUD
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
// create
|
|
108
|
+
const user = await users.create({
|
|
109
|
+
data: { name: "Ali", age: 28, address: { city: "Riyadh" } },
|
|
110
|
+
});
|
|
111
|
+
// user._id, user.created_at, user.updated_at are populated
|
|
112
|
+
|
|
113
|
+
// createMany (single transaction)
|
|
114
|
+
await users.createMany({ data: [{ name: "Sara" }, { name: "Omar" }] });
|
|
115
|
+
|
|
116
|
+
// read
|
|
117
|
+
await users.findById("…"); // doc | null
|
|
118
|
+
await users.findFirst({ where: { name: "Ali" } }); // doc | null
|
|
119
|
+
await users.findMany({
|
|
120
|
+
where: { age: { gte: 18 } },
|
|
121
|
+
orderBy: { age: "desc" },
|
|
122
|
+
select: { name: true, age: true },
|
|
123
|
+
skip: 0,
|
|
124
|
+
take: 10,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// update (first match) — returns the updated doc or null
|
|
128
|
+
await users.update({ where: { _id: "…" }, data: { age: 29 } });
|
|
129
|
+
await users.updateMany({ where: { role: "admin" }, data: { active: true } }); // { count }
|
|
130
|
+
|
|
131
|
+
// upsert
|
|
132
|
+
await users.upsert({
|
|
133
|
+
where: { name: "Ali" },
|
|
134
|
+
create: { name: "Ali", age: 1 },
|
|
135
|
+
update: { age: 2 },
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// delete — returns the deleted doc or null
|
|
139
|
+
await users.delete({ where: { _id: "…" } });
|
|
140
|
+
await users.deleteMany({ where: { active: false } }); // { count }
|
|
141
|
+
await users.deleteMany(); // delete all → { count }
|
|
142
|
+
|
|
143
|
+
// count
|
|
144
|
+
await users.count({ where: { role: "admin" } });
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Where operators
|
|
150
|
+
|
|
151
|
+
Prisma-style, no `$` prefix. A bare value is shorthand for `equals`.
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
// Comparison
|
|
155
|
+
where: { age: 28 } // shorthand equals
|
|
156
|
+
where: { age: { equals: 28 } }
|
|
157
|
+
where: { age: { not: 28 } } // also matches docs missing the field
|
|
158
|
+
where: { age: { gt: 18 } } // gt, gte, lt, lte
|
|
159
|
+
where: { role: { in: ["admin", "editor"] } }
|
|
160
|
+
where: { role: { notIn: ["guest"] } }
|
|
161
|
+
|
|
162
|
+
// String (case-sensitive; wildcards are matched literally)
|
|
163
|
+
where: { name: { contains: "li" } }
|
|
164
|
+
where: { name: { startsWith: "A" } }
|
|
165
|
+
where: { name: { endsWith: "i" } }
|
|
166
|
+
|
|
167
|
+
// Arrays
|
|
168
|
+
where: { tags: { contains: "admin" } } // element membership
|
|
169
|
+
where: { tags: { has: "admin" } } // explicit element membership
|
|
170
|
+
|
|
171
|
+
// Existence
|
|
172
|
+
where: { phone: { exists: true } } // field present (even if null)
|
|
173
|
+
where: { phone: { exists: false } }
|
|
174
|
+
|
|
175
|
+
// Nested paths (dot notation)
|
|
176
|
+
where: { "address.city": { equals: "Riyadh" } }
|
|
177
|
+
where: { "meta.score": { gte: 9 } }
|
|
178
|
+
|
|
179
|
+
// Logical
|
|
180
|
+
where: { AND: [{ age: { gte: 18 } }, { active: true }] }
|
|
181
|
+
where: { OR: [{ role: "admin" }, { role: "editor" }] }
|
|
182
|
+
where: { NOT: { role: "guest" } }
|
|
183
|
+
where: { role: "admin", age: { gt: 30 } } // multiple fields => implicit AND
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
> `contains`/`startsWith`/`endsWith` are **case-sensitive** (implemented with
|
|
187
|
+
> SQLite's `instr`/`substr`, so `%` and `_` are literal). On an array field,
|
|
188
|
+
> `contains` checks element membership.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Update operators
|
|
193
|
+
|
|
194
|
+
The `data` payload is either a plain object (shallow-merged) or update operators.
|
|
195
|
+
The two forms cannot be mixed.
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
// Default — shallow merge
|
|
199
|
+
await c.update({ where: { _id }, data: { age: 29, name: "Ali Updated" } });
|
|
200
|
+
|
|
201
|
+
// $set — set fields, including nested dot paths
|
|
202
|
+
await c.update({ where: { _id }, data: { $set: { "address.city": "Jeddah" } } });
|
|
203
|
+
|
|
204
|
+
// $inc — increment (missing field starts at 0)
|
|
205
|
+
await c.update({ where: { _id }, data: { $inc: { score: 1 } } });
|
|
206
|
+
|
|
207
|
+
// $push — append to an array ($each pushes many)
|
|
208
|
+
await c.update({ where: { _id }, data: { $push: { tags: "moderator" } } });
|
|
209
|
+
await c.update({ where: { _id }, data: { $push: { tags: { $each: ["a", "b"] } } } });
|
|
210
|
+
|
|
211
|
+
// $pull — remove matching elements from an array
|
|
212
|
+
await c.update({ where: { _id }, data: { $pull: { tags: "guest" } } });
|
|
213
|
+
|
|
214
|
+
// $unset — remove a field
|
|
215
|
+
await c.update({ where: { _id }, data: { $unset: { temporaryField: true } } });
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`_id` is immutable — attempts to set it via update data are ignored.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Aggregation
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
// aggregate
|
|
226
|
+
const stats = await users.aggregate({
|
|
227
|
+
where: { active: true },
|
|
228
|
+
_count: true,
|
|
229
|
+
_sum: { age: true },
|
|
230
|
+
_avg: { age: true },
|
|
231
|
+
_min: { age: true },
|
|
232
|
+
_max: { age: true },
|
|
233
|
+
});
|
|
234
|
+
// { _count: 42, _sum: { age: 1200 }, _avg: { age: 28.5 }, _min: { age: 18 }, _max: { age: 64 } }
|
|
235
|
+
|
|
236
|
+
// groupBy
|
|
237
|
+
const grouped = await users.groupBy({
|
|
238
|
+
by: ["role"],
|
|
239
|
+
where: { active: true },
|
|
240
|
+
_count: true,
|
|
241
|
+
_sum: { age: true },
|
|
242
|
+
orderBy: { _count: "desc" },
|
|
243
|
+
});
|
|
244
|
+
// [ { role: "admin", _count: 5, _sum: { age: 140 } }, … ]
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## SQL escape hatch
|
|
250
|
+
|
|
251
|
+
When you need full SQL power — complex joins, analytics, cross-collection
|
|
252
|
+
queries — drop to raw SQL. Documents live in a `data` JSON column, queryable
|
|
253
|
+
with SQLite's `json_extract`.
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
// Tagged template — values are safely parameterized
|
|
257
|
+
const report = await db.$queryRaw`
|
|
258
|
+
SELECT json_extract(u.data, '$.name') AS customer,
|
|
259
|
+
SUM(json_extract(o.data, '$.amount')) AS revenue
|
|
260
|
+
FROM users u
|
|
261
|
+
JOIN orders o ON json_extract(o.data, '$.userId') = u._id
|
|
262
|
+
WHERE json_extract(u.data, '$.role') = 'admin'
|
|
263
|
+
GROUP BY u._id
|
|
264
|
+
`;
|
|
265
|
+
|
|
266
|
+
// Execute (returns affected row count)
|
|
267
|
+
await db.$executeRaw`UPDATE users SET updated_at = ${Date.now()} WHERE _id = ${id}`;
|
|
268
|
+
|
|
269
|
+
// String form with positional params
|
|
270
|
+
await db.$queryRawUnsafe(`SELECT * FROM users WHERE _id = ?`, id);
|
|
271
|
+
await db.$executeRawUnsafe(`DELETE FROM users WHERE _id = ?`, id);
|
|
272
|
+
|
|
273
|
+
// Synchronous transaction (the callback must not be async)
|
|
274
|
+
await db.$transaction((tx) => {
|
|
275
|
+
// ...use tx.collection(...) or tx.sqlite...
|
|
276
|
+
});
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Need the raw driver? `db.sqlite` is the underlying `better-sqlite3` instance.
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## Auto-indexing
|
|
284
|
+
|
|
285
|
+
monlite tracks which JSON paths your `where`/`orderBy`/aggregation clauses touch.
|
|
286
|
+
Once a path crosses the threshold (default 10 queries), an expression index is
|
|
287
|
+
created silently:
|
|
288
|
+
|
|
289
|
+
```sql
|
|
290
|
+
CREATE INDEX IF NOT EXISTS idx_users_address_city
|
|
291
|
+
ON users(json_extract(data, '$.address.city'));
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
You never think about indexes. Disable with `createDb("./app.db", { autoIndex: false })`.
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Database management
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
await db.$collections(); // string[] of collection names
|
|
302
|
+
await db.$drop("users"); // drop a collection and its data
|
|
303
|
+
await db.$dropAll(); // drop everything
|
|
304
|
+
await db.$disconnect(); // close the connection
|
|
305
|
+
db.sqlite; // the underlying better-sqlite3 instance
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## How it works
|
|
311
|
+
|
|
312
|
+
Every collection is a single SQLite table:
|
|
313
|
+
|
|
314
|
+
```sql
|
|
315
|
+
CREATE TABLE IF NOT EXISTS "users" (
|
|
316
|
+
_id TEXT PRIMARY KEY,
|
|
317
|
+
data TEXT NOT NULL, -- your document as JSON
|
|
318
|
+
created_at INTEGER NOT NULL,
|
|
319
|
+
updated_at INTEGER NOT NULL
|
|
320
|
+
);
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Your entire document lives in the `data` column as JSON; `_id`, `created_at`
|
|
324
|
+
and `updated_at` are real columns. SQLite's built-in `json_extract` /
|
|
325
|
+
`json_each` power all document queries. No columns are added per field, so
|
|
326
|
+
there is no schema and no migration — ever.
|
|
327
|
+
|
|
328
|
+
All operations are synchronous under the hood (better-sqlite3 is sync) but are
|
|
329
|
+
exposed as `async` (they return Promises) for API consistency and future-proofing.
|
|
330
|
+
|
|
331
|
+
### Notes & limitations
|
|
332
|
+
|
|
333
|
+
- `_id`, `created_at`, `updated_at` are reserved; document fields with those
|
|
334
|
+
names are managed by monlite and won't round-trip as ordinary data.
|
|
335
|
+
- `contains`/`startsWith`/`endsWith` are case-sensitive (see above).
|
|
336
|
+
- `$transaction` callbacks run synchronously and must not be `async`.
|
|
337
|
+
- Collection names must be identifier-like (`[A-Za-z_][A-Za-z0-9_]*`).
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## License
|
|
342
|
+
|
|
343
|
+
MIT 🌙
|