allez-orm 1.1.0 → 1.2.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
@@ -1,7 +1,471 @@
1
- # AllezORM
2
- browser SQLite ORM + schema generator.
3
-
4
- ## Install
5
-
6
- npm i allez-orm
7
-
1
+ # AllezORM
2
+
3
+ **Ship a real SQLite database — in the browser — in under five minutes.**
4
+ Zero servers. Zero migrations. Zero "your data is gone because the user refreshed."
5
+
6
+ > Built for the next era of software: client-first apps, AI-generated code,
7
+ > and teams that ship faster than infrastructure can keep up.
8
+
9
+ [![npm](https://img.shields.io/npm/v/allez-orm.svg)](https://www.npmjs.com/package/allez-orm)
10
+ [![license](https://img.shields.io/badge/license-ISC-blue.svg)](LICENSE)
11
+ [![tests](https://img.shields.io/badge/tests-67%20passing-brightgreen.svg)](#tests)
12
+ [![node](https://img.shields.io/badge/node-%3E=18-success.svg)](package.json)
13
+
14
+ ---
15
+
16
+ ## Why this exists (read this part)
17
+
18
+ You've felt all three of these. Recently.
19
+
20
+ 1. **Your "simple" app needs a real query layer in the browser**, and you're
21
+ one library choice away from `localStorage.setItem(JSON.stringify(...))`
22
+ creeping into production. Again.
23
+ 2. **Your AI agent confidently rewrote your schema** on Tuesday and you
24
+ didn't notice until staging. The spec said one thing. The code did
25
+ another. Nobody owned the diff.
26
+ 3. **You shipped a "client-side prototype"** that became the product, and
27
+ now you're trying to graft IndexedDB persistence, foreign keys, and
28
+ migrations onto something that was never designed for them.
29
+
30
+ AllezORM is a small, sharp answer to all three.
31
+
32
+ Open it, point it at a schema, and you have a real SQLite database
33
+ running in the browser tab — persisted to IndexedDB on every write,
34
+ queried with prepared statements, protected by inline foreign keys.
35
+ The schema generator writes idiomatic, version-pinned files that your
36
+ agents **cannot** silently drift away from. Because every file is
37
+ anchored — by SHA-256 — back to the spec it came from.
38
+
39
+ That's the whole pitch. The rest of this README shows you the receipts.
40
+
41
+ ---
42
+
43
+ ## What you get in 60 seconds
44
+
45
+ ```bash
46
+ npm install allez-orm
47
+ ```
48
+
49
+ ```js
50
+ // app.js
51
+ import { AllezORM } from "allez-orm";
52
+ import users from "./schemas/users.schema.js";
53
+ import posts from "./schemas/posts.schema.js";
54
+
55
+ const orm = await AllezORM.init({ dbName: "myapp.db", schemas: [users, posts] });
56
+
57
+ // Real prepared statements. Real foreign keys. Real persistence.
58
+ await orm.table("users").upsert({ id: 1, email: "a@b.co", display_name: "Ada" });
59
+ await orm.table("posts").insert({ user_id: 1, title: "Hello world" });
60
+
61
+ const recent = await orm.query(
62
+ "SELECT u.email, p.title FROM posts p JOIN users u ON u.id = p.user_id ORDER BY p.id DESC LIMIT 10"
63
+ );
64
+ ```
65
+
66
+ That's it. The database is alive in the tab. Refresh the page — it's still there.
67
+ Open DevTools → Application → IndexedDB → `myapp.db`. The whole SQLite file is sitting there as a `Uint8Array`.
68
+
69
+ No backend. No service worker. No build step. No "but does it work in Safari" anxiety.
70
+
71
+ ---
72
+
73
+ ## The five things that make it different
74
+
75
+ ### 1. **Spec-anchored schemas your agents can't silently break**
76
+
77
+ Here's what a generated schema file looks like:
78
+
79
+ ```js
80
+ // users.schema.js (generated by allez-orm)
81
+ // DO NOT EDIT — regenerate by editing the spec, then running:
82
+ // allez-orm from-json ../spec.json
83
+ // SPEC ../spec.json
84
+ // SPEC_SHA256 0f1ab429adc01ddeaa093a4b18a87836547f437e1bc31f5d1babddf9935c47dc
85
+ // TABLE_SHA256 531b42a728116ec1332467a5fb83b93f230afaf5ea75f4b4019ef2cf5d997adc
86
+ const usersSchema = {
87
+ table: "users",
88
+ version: 1,
89
+ createSQL: `
90
+ CREATE TABLE IF NOT EXISTS users (
91
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
92
+ email TEXT UNIQUE NOT NULL,
93
+ created_at TEXT NOT NULL,
94
+ updated_at TEXT NOT NULL,
95
+ deleted_at TEXT
96
+ );`
97
+ };
98
+ export default usersSchema;
99
+ ```
100
+
101
+ Every generated file carries two cryptographic anchors back to the spec it came from.
102
+ When an agent (or a junior dev, or future-you at 2am) opens this file, the very first
103
+ thing they see is *"DO NOT EDIT — regenerate from the spec."*
104
+
105
+ Don't trust the comment? Don't have to. Run drift detection:
106
+
107
+ ```bash
108
+ $ allez-orm verify schemas.spec.json --dir=./schemas
109
+ ✔ verify: 4 table(s) match spec schemas.spec.json
110
+ ```
111
+
112
+ Hand-edit a generated file:
113
+
114
+ ```bash
115
+ $ allez-orm verify schemas.spec.json --dir=./schemas
116
+ ✗ Drift detected in 1 file(s):
117
+ - ./schemas/users.schema.js
118
+
119
+ To resync, run: allez-orm from-json schemas.spec.json --dir=./schemas -f
120
+ ```
121
+
122
+ Wire that into CI. Wire it into your pre-commit hook. **Your agents do not get to
123
+ quietly mutate your contract anymore.**
124
+
125
+ You also get an `AGENTS.md` auto-emitted next to the schemas — picked up
126
+ natively by Claude Code, Cursor, and every IDE that respects the convention —
127
+ telling future agents exactly where the source of truth lives and how to
128
+ regenerate.
129
+
130
+ > This is the part Luca asked about. Now it ships.
131
+
132
+ ### 2. **A SQLite database in the browser, persisted, with FKs that actually enforce**
133
+
134
+ This is not a Promise-flavored wrapper around `localStorage`. AllezORM runs
135
+ **real SQLite via sql.js (WASM)** — the same engine in your phone, your browser, every
136
+ plane in the air, and most cars sold this decade.
137
+
138
+ - `PRAGMA foreign_keys = ON` — and stays on, even across saves. (We found and
139
+ fixed a subtle sql.js bug where `db.export()` resets connection PRAGMAs.
140
+ Now they don't.)
141
+ - Prepared statements for every helper — no string concatenation, no
142
+ injection holes. SQL identifier validation rejects anything that
143
+ doesn't match `/^[A-Za-z_][A-Za-z0-9_]*$/`, and we test it against
144
+ the OWASP injection corpus.
145
+ - Debounced auto-save to IndexedDB after every write. Configurable.
146
+ - Versioned `onUpgrade(db, from, to)` hook per schema, so you can ship
147
+ v2 of your app and migrate users in-place.
148
+
149
+ ### 3. **A CLI that generates schemas the way you'd write them by hand**
150
+
151
+ ```bash
152
+ # Ad-hoc — one table, fast:
153
+ allez-orm create table users email:text!+ display_name:text! --stamps
154
+
155
+ # Or declarative — the source-of-truth path:
156
+ allez-orm from-json schemas.spec.json
157
+ ```
158
+
159
+ Compact field syntax that fits in your head: `name:type` with flag suffixes
160
+ (`!` = NOT NULL, `+` = UNIQUE, `->target` = inline foreign key, `--onDelete=`
161
+ for every FK at once). One mental model. No DSL to memorize.
162
+
163
+ Spec a referenced table that doesn't exist yet? The CLI auto-generates an
164
+ ID-only stub for it so your DDL compiles immediately and you can fill it in later.
165
+
166
+ ### 4. **AllezORM Studio — a visual workbench for your in-browser DB**
167
+
168
+ `npm run dev` opens [Studio](index.html): a clean, single-page workbench
169
+ with searchable tables, sortable columns, a SQL scratchpad, and a
170
+ **Create table** / **Add column** dialog that calls the CLI for you
171
+ and live-reloads the ORM.
172
+
173
+ It's the kind of tool you wish you had during the first week of a new
174
+ project. Now you do.
175
+
176
+ ### 5. **Tested with the seriousness of a database**
177
+
178
+ This repo currently ships with **67 automated tests across six suites**:
179
+
180
+ | Suite | What it covers |
181
+ |---|---|
182
+ | [test-cli.mjs](tests/test-cli.mjs) | CLI smoke tests, force/overwrite, FK stubs |
183
+ | [test-security.mjs](tests/test-security.mjs) | SQL identifier injection corpus, prepared-statement integration |
184
+ | [test-spec-anchor.mjs](tests/test-spec-anchor.mjs) | Spec-anchor headers, AGENTS.md, drift detection |
185
+ | [test-generator-thorough.mjs](tests/test-generator-thorough.mjs) | 21 edge-case generator scenarios, real sql.js compile-and-run, idempotency |
186
+ | [test-fk-persistence.mjs](tests/test-fk-persistence.mjs) | Regression: FK pragma survives `db.export()` and repeated saves |
187
+ | [test-e2e-studio.mjs](tests/test-e2e-studio.mjs) | Playwright E2E: full Studio flow including IndexedDB reload, FK violations |
188
+
189
+ ```bash
190
+ npm test # everything except the browser E2E
191
+ npm run test:e2e # browser E2E (requires playwright + chromium)
192
+ npm run test:all # both
193
+ ```
194
+
195
+ ---
196
+
197
+ ## Install
198
+
199
+ ```bash
200
+ npm install allez-orm
201
+ ```
202
+
203
+ You don't need a bundler. You don't need a framework. You don't need a server.
204
+ The package is **pure ESM**, runs in browsers natively, and exposes a single
205
+ default entry plus a CLI:
206
+
207
+ - `import { AllezORM } from "allez-orm"` — the ORM
208
+ - `npx allez-orm …` — the schema generator
209
+ - `import "allez-orm/cli"` — programmatic CLI access
210
+
211
+ WASM is loaded from [sql.js's CDN](https://sql.js.org) by default. Want to
212
+ self-host the WASM? Pass `wasmLocateFile` to `AllezORM.init`.
213
+
214
+ ---
215
+
216
+ ## Quickstart: from zero to a real database in 5 commands
217
+
218
+ ```bash
219
+ # 1. Init a project
220
+ npm init -y && npm install allez-orm
221
+
222
+ # 2. Generate a schema declaratively
223
+ cat > schemas.spec.json <<'JSON'
224
+ {
225
+ "outDir": "./schemas",
226
+ "defaultOnDelete": "cascade",
227
+ "tables": [
228
+ { "name": "users", "stamps": true,
229
+ "fields": [
230
+ { "name": "email", "type": "text", "unique": true, "notnull": true },
231
+ { "name": "display_name", "type": "text", "notnull": true }
232
+ ] },
233
+ { "name": "posts", "stamps": true,
234
+ "fields": [
235
+ { "name": "title", "type": "text", "notnull": true },
236
+ { "name": "user_id", "type": "integer", "fk": { "table": "users" } }
237
+ ] }
238
+ ]
239
+ }
240
+ JSON
241
+
242
+ # 3. Generate the schema files (and AGENTS.md)
243
+ npx allez-orm from-json schemas.spec.json
244
+
245
+ # 4. Verify alignment any time you like
246
+ npx allez-orm verify schemas.spec.json
247
+
248
+ # 5. Use it from your app
249
+ echo 'import { AllezORM } from "allez-orm";
250
+ import users from "./schemas/users.schema.js";
251
+ import posts from "./schemas/posts.schema.js";
252
+ const orm = await AllezORM.init({ schemas: [users, posts] });
253
+ await orm.table("users").upsert({ id: 1, email: "a@b.co", display_name: "Ada", created_at: new Date().toISOString(), updated_at: new Date().toISOString() });
254
+ console.log(await orm.query("SELECT * FROM users"));
255
+ ' > app.mjs
256
+ ```
257
+
258
+ You now have a typed, persistent, foreign-key-enforced database
259
+ running in any modern browser, generated from a single source-of-truth file.
260
+
261
+ ---
262
+
263
+ ## The CLI
264
+
265
+ ```
266
+ allez-orm create table <name> [fields...] [options]
267
+ allez-orm from-json <config.json> [--dir=<outDir>] [-f|--force]
268
+ allez-orm verify <config.json> [--dir=<outDir>]
269
+ allez-orm --print-json-schema
270
+ ```
271
+
272
+ ### Field syntax (CLI form)
273
+
274
+ ```
275
+ col[:type][flags][->target]
276
+
277
+ Examples:
278
+ email:text!+ email TEXT UNIQUE NOT NULL
279
+ user_id:integer->users user_id INTEGER REFERENCES users(id)
280
+ parent_id:integer->nodes self-referencing FK; no stub generated
281
+ body body TEXT (type defaults to TEXT)
282
+ ```
283
+
284
+ Flags: `!` = NOT NULL, `+` = UNIQUE.
285
+ Type aliases (case-insensitive): `text|string`, `int|integer`, `real|float`, `numeric|number`, `blob`, `bool`.
286
+
287
+ ### Options
288
+
289
+ | Flag | What it does |
290
+ |---|---|
291
+ | `--dir=<outDir>` | Output directory (defaults to `schemas_cli`, or `outDir` from spec) |
292
+ | `--stamps` | Add `created_at` (NOT NULL), `updated_at` (NOT NULL), `deleted_at` (nullable) |
293
+ | `--onDelete=<mode>` | Apply ON DELETE behavior to every FK: `cascade\|restrict\|setnull\|noaction` |
294
+ | `-f, --force` | Overwrite existing files (also: `ALLEZ_FORCE=1`) |
295
+ | `--help` | Show help |
296
+
297
+ ### `from-json` config shape
298
+
299
+ Print the canonical JSON Schema:
300
+
301
+ ```bash
302
+ npx allez-orm --print-json-schema
303
+ ```
304
+
305
+ The config supports both **rich field objects** and **compact string tokens** in the same array — pick whichever fits the table:
306
+
307
+ ```json
308
+ {
309
+ "tables": [
310
+ { "name": "items", "fields": ["sku:text!+", "qty:integer!"] },
311
+ { "name": "memberships", "fields": [
312
+ { "name": "user_id", "type": "integer", "fk": { "table": "users" } },
313
+ { "name": "org_id", "type": "integer", "fk": { "table": "orgs" } }
314
+ ]}
315
+ ]
316
+ }
317
+ ```
318
+
319
+ ### `verify` — your CI safety net
320
+
321
+ `verify` re-runs generation **in memory** and byte-compares against what's on disk.
322
+ It exits non-zero on:
323
+
324
+ - **Drift** — a generated file was hand-edited.
325
+ - **Stale files** — the spec moved forward but files weren't regenerated.
326
+ - **Missing files** — the spec references a table whose file isn't there.
327
+
328
+ ```yaml
329
+ # .github/workflows/ci.yml
330
+ - run: npx allez-orm verify schemas.spec.json
331
+ ```
332
+
333
+ One line. No more 2am Slack messages about schema drift.
334
+
335
+ ---
336
+
337
+ ## The ORM API
338
+
339
+ ### Init
340
+
341
+ ```js
342
+ const orm = await AllezORM.init({
343
+ dbName: "myapp.db", // IndexedDB key; defaults to "allez.db"
344
+ autoSaveMs: 1500, // debounce window for IndexedDB writes
345
+ wasmLocateFile: f => `/wasm/${f}`, // self-host sql-wasm.wasm
346
+ schemas: [usersSchema, postsSchema]
347
+ });
348
+ ```
349
+
350
+ ### Per-table helpers (parameterized, FK-aware)
351
+
352
+ ```js
353
+ const users = orm.table("users");
354
+
355
+ await users.insert({ email: "a@b.co", display_name: "Ada" });
356
+ await users.upsert({ id: 1, email: "a@b.co", display_name: "Ada" });
357
+ await users.update(1, { display_name: "Ada Lovelace" });
358
+ await users.findById(1); // single row
359
+ await users.searchLike("ada", ["display_name"]); // LIKE %ada% across columns
360
+ await users.deleteSoft(1); // sets deleted_at / deletedAt
361
+ await users.remove(1); // hard delete
362
+ ```
363
+
364
+ ### Raw SQL — when you need it
365
+
366
+ ```js
367
+ const rows = await orm.query("SELECT * FROM users WHERE id IN (?, ?)", [1, 2]);
368
+ const one = await orm.get("SELECT * FROM users LIMIT 1");
369
+ await orm.execute("UPDATE users SET display_name = ? WHERE id = ?", ["X", 1]);
370
+ ```
371
+
372
+ ### Schema versioning
373
+
374
+ ```js
375
+ const usersSchema = {
376
+ table: "users",
377
+ version: 2,
378
+ createSQL: `CREATE TABLE IF NOT EXISTS users (...);`,
379
+ async onUpgrade(db, from, to) {
380
+ if (from < 2) db.exec(`ALTER TABLE users ADD COLUMN avatar_url TEXT`);
381
+ }
382
+ };
383
+ ```
384
+
385
+ ### Manual save
386
+
387
+ ```js
388
+ await orm.saveNow(); // force a write to IndexedDB right now
389
+ ```
390
+
391
+ ---
392
+
393
+ ## Security
394
+
395
+ This package treats SQL identifiers as an attack surface. Every table and
396
+ column name passed to the `table()` helpers goes through `safeIdent()`,
397
+ which validates against a strict character class and rejects every
398
+ payload in the OWASP injection corpus. You can audit the test for yourself
399
+ at [tests/test-security.mjs](tests/test-security.mjs) (13 assertions).
400
+
401
+ All values are bound via prepared statements. There is no string
402
+ concatenation of user input into SQL anywhere in the runtime.
403
+
404
+ If you find a security issue, please open a private advisory in the
405
+ GitHub repo. We respond.
406
+
407
+ ---
408
+
409
+ ## What you avoid by using this
410
+
411
+ This is the part that's hard to feel until you've been burned. So picture it:
412
+
413
+ - **No "the IndexedDB schema is one thing, the SQL schema is another" debugging at midnight.** One file is the truth. One command verifies it.
414
+ - **No agents quietly mutating your contract.** The header on every generated file is unmissable. The verify command is automatic.
415
+ - **No `localStorage.setItem(JSON.stringify(...))`** sprawling across your codebase as you slowly reinvent indexes. The Studio is the indexed database you wanted on day one.
416
+ - **No "we'll add a real backend later"** technical debt. You may discover you never need one. Plenty of apps don't.
417
+ - **No surprise FK behavior.** We tested the pragma persistence so you don't have to.
418
+
419
+ ---
420
+
421
+ ## Roadmap & status
422
+
423
+ AllezORM is **production-shape for client-only apps**: tests gate every
424
+ release, the API surface is small and stable, and the schema generator's
425
+ output format is now SHA-anchored (so we'll bump major if we ever change it).
426
+
427
+ Coming soon:
428
+
429
+ - WAL-style append journaling for crash-safe writes
430
+ - Optional Web Worker mode for off-main-thread queries
431
+ - An `allez-orm sync` command for two-way reconciliation with a remote SQL database (so the same schema works client-side and server-side)
432
+
433
+ Want one of those sooner? Open an issue and say so.
434
+
435
+ ---
436
+
437
+ ## Contributing
438
+
439
+ ```bash
440
+ git clone https://github.com/AllezMarketing/allez-orm.git
441
+ cd allez-orm
442
+ npm install
443
+ npm test # unit + integration
444
+ npx playwright install chromium
445
+ npm run test:e2e # browser E2E
446
+ npm run dev # launch Studio at http://localhost:5174
447
+ ```
448
+
449
+ PRs that touch generator output **must** keep the existing schema files
450
+ byte-stable (or bump version and update the SHA anchors deliberately).
451
+ `npm test` will tell you immediately.
452
+
453
+ ---
454
+
455
+ ## License
456
+
457
+ ISC © [Allez Marketing LLC](https://github.com/AllezMarketing)
458
+
459
+ ---
460
+
461
+ ## One last thing
462
+
463
+ If you read this far, you already know what to do. The install is one
464
+ command. The first table is one config. The verify is one CI line.
465
+
466
+ The friction between you and a working database is **smaller than it has
467
+ ever been.** Use the next ten minutes well.
468
+
469
+ ```bash
470
+ npm install allez-orm
471
+ ```
package/allez-orm.mjs CHANGED
@@ -282,6 +282,10 @@ export class AllezORM {
282
282
 
283
283
  async saveNow() {
284
284
  const data = this.db.export(); // Uint8Array
285
+ // sql.js's db.export() resets connection-scoped PRAGMAs (including
286
+ // foreign_keys) to 0. Re-enable FK enforcement so subsequent writes
287
+ // stay constraint-checked.
288
+ this.db.exec("PRAGMA foreign_keys = ON;");
285
289
  if (isBrowser) await idbSet(this.dbName, data);
286
290
  }
287
291
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "allez-orm",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "AllezORM: lightweight browser SQLite ORM (sql.js) + schema generator CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,8 +34,13 @@
34
34
  "test:cli": "node tests/test-cli.mjs",
35
35
  "ddl:audit": "node tools/ddl-audit.mjs",
36
36
  "test:security": "node tests/test-security.mjs",
37
- "test": "node tests/test-cli.mjs && node tests/test-security.mjs && node tools/ddl-audit.mjs",
38
- "prepublishOnly": "node tests/test-cli.mjs && node tests/test-security.mjs && node tools/ddl-audit.mjs"
37
+ "test:anchor": "node tests/test-spec-anchor.mjs",
38
+ "test:thorough": "node tests/test-generator-thorough.mjs",
39
+ "test:fk": "node tests/test-fk-persistence.mjs",
40
+ "test:e2e": "node tests/test-e2e-studio.mjs",
41
+ "test": "node tests/test-cli.mjs && node tests/test-security.mjs && node tests/test-spec-anchor.mjs && node tests/test-generator-thorough.mjs && node tests/test-fk-persistence.mjs && node tools/ddl-audit.mjs",
42
+ "test:all": "npm test && npm run test:e2e",
43
+ "prepublishOnly": "node tests/test-cli.mjs && node tests/test-security.mjs && node tests/test-spec-anchor.mjs && node tests/test-generator-thorough.mjs && node tests/test-fk-persistence.mjs && node tools/ddl-audit.mjs"
39
44
  },
40
45
  "files": [
41
46
  "allez-orm.mjs",
@@ -68,6 +73,7 @@
68
73
  "sql.js": "^1.13.0"
69
74
  },
70
75
  "devDependencies": {
76
+ "playwright": "^1.60.0",
71
77
  "serve": "^14.2.5"
72
78
  },
73
79
  "engines": {