@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 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 🌙