naro-cli 0.11.0 → 0.12.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
@@ -91,9 +91,11 @@ naro db dump --output backup.sql SQL dump, streamed from Cloudflare (stdout wi
91
91
  naro db restore --bookmark <id> Time Travel, last 30 days (or --timestamp <time>);
92
92
  asks first, prints the command that undoes it
93
93
  naro db pull Existing database, no migrations yet: writes its schema
94
- as migrations/0001_remote_schema.sql, recorded as applied
94
+ and policies as migrations/0001_remote_schema.sql,
95
+ recorded as applied
95
96
  naro db reset [--dry-run] Drop every table, view and trigger, clear the ledger and
96
- apply ./migrations again (asks first; --yes in CI)
97
+ the policies, and apply ./migrations again (asks first;
98
+ --yes in CI; refuses files the server would refuse)
97
99
  naro db lint [--strict] Check the schema for what D1 actually trips on; exits 1
98
100
  on an error, and on a warning too with --strict
99
101
  naro db table create posts --column "id:integer:pk:autoincrement" --column "title:text:notnull" --column "created_at:datetime:notnull:default=current_timestamp" --owner user_id
@@ -191,12 +193,43 @@ reads the bookmark from just before the drops and prints it, so
191
193
  `naro db restore --bookmark <that>` puts the data back. `--dry-run` lists what
192
194
  it would drop and re-apply without touching anything.
193
195
 
196
+ The backend's accounts, sessions, settings and the other `_naro_` tables are
197
+ kept — except its policies: they are cleared with the schema and the migration
198
+ files create them again. Before it reads anything, `db reset` checks the files
199
+ as `naro db push --dry-run` does: when the control plane would refuse one, the
200
+ re-apply would stop there with the schema already dropped, so the reset is
201
+ refused with `VALIDATION_FAILED`, each file as `<file>: Statement <i> of <n>:
202
+ …` (`--dry-run` prints them and exits 1). A policy that exists only in the
203
+ database would be lost — one made with `naro policy create` or
204
+ `naro policy set`, and the owner policy that `naro db table create --owner`,
205
+ MCP's `create_table` and the table editor add, which no migration file holds.
206
+ So before it asks, `db reset` lists the policies your files do not make, each
207
+ with the SQL that makes it again — its `CREATE POLICY` and, for an owner rule,
208
+ the `COMMENT ON POLICY … IS 'naro:owner <column>'` that keeps its column
209
+ (`--json`: `policiesNotInFiles[].sql`); add it to a migration first to keep
210
+ them. The policies of a table your files do not create go with that table and
211
+ are named apart, with no SQL (`--json`: `policiesOfTablesNotInFiles`): a
212
+ migration that made them would stop at a table that is not there. A file the
213
+ re-apply would stop at that the check above does not catch (one whose SQL
214
+ cannot be split) is named instead — `<file> would be refused: <reason>`
215
+ (`--json`: `wouldBeRefused`) — and then no policy is called lost. A policy row
216
+ that cannot be read is listed as a `-- Policy … cannot be read` comment
217
+ instead: copying it keeps nothing, so drop that policy and create it again. A
218
+ backend whose worker predates named policies is refused with
219
+ `409 BACKEND_OUTDATED`: run `naro backend enable` first.
220
+
194
221
  `naro db lint` reads the schema and runs the checks D1 allows. Its rules are
195
222
  the ones D1 actually trips on — orphaned rows, a failed `quick_check`, a
196
223
  missing or nullable primary key, a foreign key no index starts with, a trigger
197
224
  the REST API cannot recreate, a virtual table that makes `db dump` refuse the
198
- whole database. There is no "unused index" guess: D1 keeps no execution
199
- statistics to base one on. An error exits 1, a warning exits 0 — `--strict`
225
+ whole database. On a project with a backend it also checks the policies:
226
+ `rls_no_policy` (a table the Data API refuses entirely),
227
+ `rls_policy_text_default` (a number column a policy reads as a boolean whose
228
+ `DEFAULT` is text, such as `BOOLEAN DEFAULT 'true'`) and
229
+ `rls_policy_unindexed` (warnings), and `rls_policy_invalid`, a policy the
230
+ backend cannot apply — one that reads a text column as true or false among
231
+ them (an error). There is no "unused index" guess: D1 keeps
232
+ no execution statistics to base one on. An error exits 1, a warning exits 0 — `--strict`
200
233
  makes warnings fail too, so CI can gate on it:
201
234
 
202
235
  ```bash
@@ -217,7 +250,9 @@ from a fixed list — and applies it as one migration, recorded in
217
250
  `d1_migrations` and written to `./migrations` as `NNNN_schema_<op>_<table>.sql`,
218
251
  numbered after your local files and the ledger. `naro migration list`,
219
252
  `db pull` and `db reset` see it like any other migration; with `--no-write`
220
- there is no file, so `db reset` will not re-apply it. `--dry-run` prints the
253
+ there is no file, so `db reset` will not re-apply it. The owner policy
254
+ `--owner` adds is not in that file either: `db reset` lists it, with its SQL,
255
+ before it removes it — add that SQL to a migration to keep it. `--dry-run` prints the
221
256
  SQL and changes nothing. While a local migration is not in the ledger yet, a
222
257
  change is refused — `naro db push` it first, or the change would run before it.
223
258
 
@@ -233,15 +268,17 @@ Every primary key but a single `integer` one is `NOT NULL`; a foreign key must
233
268
  point at its table's primary key or a unique column. `--owner user_id` adds
234
269
  `user_id TEXT NOT NULL` and, when the backend is on and ready, an owner policy for all
235
270
  four actions; any other new table is closed to the Data API until you give it
236
- a policy (`naro policy create` or `naro policy set`; the command says so).
271
+ a policy (`CREATE POLICY` in a migration, or `naro policy create` or
272
+ `naro policy set`; the command says so).
237
273
  Policies are kept by table name: a drop removes the table's policies whenever
238
274
  the project has a backend, on or off (the command says so). A rename leaves
239
275
  them under the old name, where they apply to no table — the command prints
240
276
  what to run to move them. Move them then: the next time `naro db table`
241
277
  creates a table as that name or renames one to it, renaming the table back
242
278
  included, the policies stored under the name are removed first, so a new
243
- table never inherits an earlier one's (a table made by `naro db push` or
244
- `naro db query` gets them as they are). Whatever a command removes it prints
279
+ table never inherits an earlier one's (except, on a project without a
280
+ backend, one changed through `naro db table`, the table editor or MCP) — and
281
+ neither does a table made by `naro db push` or `naro db query`. Whatever a command removes it prints
245
282
  so it can be made again: as `CREATE POLICY` statements, which
246
283
  `naro policy create` takes back, and — for owner rules, whose owner column
247
284
  `CREATE POLICY` cannot say — as the `naro policy set <table> … --owner <column>`
@@ -439,8 +476,112 @@ project's backend, then run the command again.
439
476
 
440
477
  Policies are Postgres's row-level security, as in Supabase. A table without a
441
478
  policy refuses every anon and signed-in request; the service key skips
442
- policies. Write them as `CREATE POLICY`, a change is live within about 10
443
- seconds, and `naro policy list` prints them back as SQL:
479
+ policies. Write a table's policies in the migration that creates it —
480
+ `CREATE POLICY` after the `CREATE TABLE`, in the same file. `naro db push`,
481
+ `naro migration up`, `naro db query` and the MCP tools `apply_migration` and
482
+ `execute_sql` apply them together with the table, all or nothing. Three shapes
483
+ cover most apps:
484
+
485
+ ```sql
486
+ -- migrations/0002_todos.sql: each signed-in user reads and changes their own rows.
487
+ CREATE TABLE todos (
488
+ id integer PRIMARY KEY,
489
+ user_id uuid NOT NULL,
490
+ title text NOT NULL
491
+ );
492
+ CREATE POLICY "own rows" ON todos FOR ALL TO authenticated
493
+ USING ((select auth.uid()) = user_id)
494
+ WITH CHECK ((select auth.uid()) = user_id);
495
+
496
+ -- migrations/0003_posts.sql: anyone reads, signed in or not.
497
+ CREATE TABLE posts (id integer PRIMARY KEY, title text NOT NULL);
498
+ CREATE POLICY "read" ON posts FOR SELECT TO anon, authenticated USING (true);
499
+
500
+ -- migrations/0004_reports.sql: an app only its admins sign in to. First run
501
+ -- naro backend auth signups off and create the admin accounts: authenticated
502
+ -- then means exactly those accounts.
503
+ CREATE TABLE reports (id integer PRIMARY KEY, body text NOT NULL);
504
+ CREATE POLICY "admins" ON reports FOR ALL TO authenticated USING (true) WITH CHECK (true);
505
+ ```
506
+
507
+ A migration or a query takes `CREATE POLICY`; `DROP POLICY [IF EXISTS]` (`IF
508
+ EXISTS` skips a missing table too, as in Postgres); `COMMENT ON POLICY p ON t
509
+ IS 'naro:owner <column>'`, which makes `p` the owner rule `naro policy set
510
+ --owner` writes (any other comment is ignored); and `ALTER TABLE t ENABLE |
511
+ FORCE | NO FORCE ROW LEVEL SECURITY`, which changes nothing because
512
+ row-level security is always on. Before anything runs, naro refuses these with
513
+ `400 VALIDATION_FAILED` "Statement <i> of <n>: …":
514
+
515
+ - `ALTER TABLE … DISABLE ROW LEVEL SECURITY` — it is always on; to let
516
+ everyone through, create a policy: `CREATE POLICY "open" ON t USING (true)
517
+ WITH CHECK (true);`
518
+ - `GRANT` and `REVOKE` — every table is behind row-level security, so write a
519
+ policy instead;
520
+ - `ALTER POLICY` — `DROP POLICY` and `CREATE POLICY` again;
521
+ - a table named `public.x` in `CREATE TABLE`, `DROP TABLE` or a rename: `SQLite
522
+ has no schema "public": write the table name without "public."`
523
+ - a `CREATE POLICY` that reads a column inside a subquery with no table in
524
+ front of it, when a later statement of the same file creates, drops, renames
525
+ or alters the policy's table or a table its subqueries read: `a later
526
+ statement changes a table, so name the table of every column inside this
527
+ policy's subqueries (for example members.user_id).` Postgres decides whose
528
+ column it is when the policy is created; naro binds it once the file is in,
529
+ when the later statement may have added that name to the subquery's table.
530
+ A later statement that changes only other tables — the next table of a file
531
+ grouped table by table — leaves the policy alone.
532
+
533
+ The statements of one request apply all-or-nothing, the policies with the
534
+ tables: a policy on a table that does not exist
535
+ (`relation "nope" does not exist`), a name already taken, or a 33rd policy on
536
+ one table fails the whole request with `400 DATABASE_QUERY_FAILED`, names the
537
+ statement, and applies nothing — not even the `CREATE TABLE` before it. Policy
538
+ statements, and statements that create, drop or rename a table, take no
539
+ `params` (`400 VALIDATION_FAILED`): write the values into the SQL.
540
+
541
+ Policies go with their table, as in Postgres: `DROP TABLE` drops them, and a
542
+ new table never inherits the leftovers of an earlier one with the same name
543
+ (except tables changed through the table editor on a project without a backend).
544
+ SQLite changes a column by building the table again — `CREATE TABLE
545
+ __new_x`, copy the rows, `DROP TABLE x`, `ALTER TABLE __new_x RENAME TO x` — so
546
+ the drop removes `x`'s policies and the rename does not bring them back. Write
547
+ the `CREATE POLICY` statements again after the rename, in the same migration.
548
+ A plain `ALTER TABLE a RENAME TO b` does not move them either: they stay under
549
+ `a`, and `b` has none until you create them again.
550
+ The answer's `warnings` lists the policies a migration removed, with SQL you
551
+ can paste; it also says when the project has no backend yet (the policies are
552
+ saved and apply once `naro backend enable` turns it on) and when a policy was
553
+ applied that the Data API refuses, with the reason. `naro db push` and
554
+ `naro db query` print each warning as `⚠ …` on stderr (`--json` has it whole
555
+ in `warnings`, a query's in `result.warnings`).
556
+
557
+ `naro db push --dry-run` parses the policy statements of the pending files on
558
+ your machine and prints what it would refuse as `<file>: Statement <i> of <n>:
559
+ …` (exit 1); whether a table or a policy exists is known only when the file is
560
+ applied. `naro db pull` writes the policies that exist at the end of the
561
+ baseline file — `ALTER TABLE … ENABLE ROW LEVEL SECURITY;` for each table,
562
+ then each `CREATE POLICY …;`, an owner rule's column as `COMMENT ON POLICY …
563
+ IS 'naro:owner <column>';` (a database without policies pulls as plain
564
+ SQLite). `naro db reset` clears the policies with the schema and creates them
565
+ again from the migration files, so a policy that exists only in the database
566
+ is lost; it lists those before it asks, each with the SQL to add to a
567
+ migration to keep it (an owner rule's column as its `COMMENT ON POLICY`; a
568
+ row that cannot be read is only a comment — drop it and create it again), and
569
+ names apart, without SQL, the policies of tables your files do not create. It
570
+ refuses to start when the control plane would refuse one of the files.
571
+ `naro db lint`, on a
572
+ project with a backend, reports `rls_no_policy` (a table the Data API refuses
573
+ entirely), `rls_policy_text_default` and `rls_policy_unindexed` as warnings,
574
+ and `rls_policy_invalid` — a
575
+ policy the backend cannot apply, such as one that reads a text column as true
576
+ or false — as an error.
577
+
578
+ `naro policy create` sends the same statements to the database directly: one
579
+ in quotes, or `--file` with several, `DROP POLICY` included. Nothing records
580
+ them in `./migrations`, so `naro db reset` removes what your files do not
581
+ make; use it for a table that already exists. A change is live within about 10
582
+ seconds, and `naro policy list` prints the policies back as SQL. More complete
583
+ policies — they give each user their own todos, everyone the published posts
584
+ and team members their team's docs, and hide deleted docs from all of them:
444
585
 
445
586
  ```sql
446
587
  -- Each signed-in user reads and changes their own todos.
@@ -560,7 +701,9 @@ policy — `naro policy create`, `naro policy drop` and `naro policy set` — wi
560
701
  `naro backend enable` uploads the current backend and turns the stored rules
561
702
  into named policies; it also turns a backend that is off back on, so run
562
703
  `naro backend disable` again afterwards if it should stay off. `naro policy
563
- list` and `naro policy remove` work in the meantime.
704
+ list` and `naro policy remove` work in the meantime. A migration or a query
705
+ that holds a `CREATE POLICY` or `DROP POLICY` gets the same answer before
706
+ anything runs; SQL without one runs as before.
564
707
 
565
708
  `naro policy set` refuses with `409 POLICY_EXISTS` when the table's `naro_<action>` policy is not one it wrote (for example one made with `naro policy create`): `Policy <name> on <table> is not one naro policy set wrote. Change it with naro policy create, or drop it first: naro policy drop <name> --table <table>.` A `naro_<action>` policy that cannot be read as stored (the table answers `500 POLICY_INVALID`) is not refused: setting a rule replaces it, and `none` drops it.
566
709
 
@@ -584,7 +727,21 @@ pass — reads a row as it will be stored and compares it with the column's
584
727
  collation (`COLLATE NOCASE`, `RTRIM`). So:
585
728
 
586
729
  - a number column it reads takes numbers, booleans and null, not strings, and
587
- cannot be compared with `like` or `ilike`;
730
+ cannot be compared with `like` or `ilike` — the Data API refuses a string
731
+ for a column that any policy of any table reads (an owner rule's own id
732
+ column excepted) with `400 INVALID_QUERY`. For a column whose declared type
733
+ has `INT`, `REAL`, `FLOA`, `DOUB`, `NUM`, `DEC`, `BOOL` or `BIT` in it, that
734
+ is every string — and even when no policy reads it, a string SQLite would
735
+ keep as text (`'abc'`, `'true'`; `'7'` is stored as 7), which Postgres
736
+ refuses as a type error and a policy written later would read inside out. A
737
+ column declared another type (`uuid`, `date`, `timestamptz`, `json`) keeps a
738
+ string as text, so Supabase's `user_id uuid` with `auth.uid() = user_id`
739
+ works; there the Data API refuses only a string SQLite would store as a
740
+ number (`'12'`), a string with a NUL character, and any string when a
741
+ policy reads the column as true or false or compares it with a number. On such a
742
+ column a `WITH CHECK` that orders it against a number (`<`, `>`, `BETWEEN`)
743
+ can judge a new text differently from the row it becomes, and `LIKE` or a
744
+ text function on it is refused as on a number column;
588
745
  - a primary key column it reads must be sent, and not as null;
589
746
  - any other column it reads may be left out only when it has no default or a
590
747
  plain one — `NULL`, `TRUE`, `FALSE`, a number, or a quoted string on a column
@@ -609,6 +766,39 @@ An `--update-check` you give replaces the default: without
609
766
  `user_id`. Write
610
767
  `--update-when 'user_id.eq.auth.uid()' --update-check 'user_id.eq.auth.uid(),status.eq.draft'`.
611
768
 
769
+ ## Release notes
770
+
771
+ ### 0.12.0
772
+
773
+ - Migrations and `naro db query` take Postgres's `CREATE POLICY`,
774
+ `DROP POLICY` and `COMMENT ON POLICY … IS 'naro:owner <column>'` next to
775
+ the tables they are for (see Row-level security). `naro db reset` clears
776
+ the policies with the schema and creates them again from your migration
777
+ files; before it drops anything it checks the files as
778
+ `naro db push --dry-run` does, and lists the policies your files do not
779
+ make, with the SQL to keep them.
780
+ - A failed `db push` keeps the server's `details.warnings` of the file that
781
+ failed (the policies a table rebuild took, with their SQL) and prints them
782
+ as `⚠` lines; the warnings of the files before it are
783
+ `details.appliedWarnings`.
784
+ - **Update the CLI before you run `naro db reset`.** `naro-cli` 0.11.0
785
+ (published on 2026-10-05 from main `98bf538b`, PR #82) and every older
786
+ version know nothing of policies in migrations. On a project whose backend
787
+ has named policies, the control plane now drops a table's policies with
788
+ the table, so their `db reset` takes every policy along — without listing
789
+ first the ones your files do not make again, and without reading the
790
+ warning that names them. A table left without a policy refuses every
791
+ Data API request until you create one again.
792
+ - **Check your clients before the backend updates.** The Data API now
793
+ refuses a string that SQLite would keep as text in a column declared with
794
+ INT, REAL, FLOA, DOUB, NUM, DEC, BOOL or BIT (a form's `"true"` for a
795
+ BOOLEAN, `"high"` for an INTEGER), even when no policy reads the column.
796
+ The backend worker takes this on at the project's next deploy or
797
+ `naro backend enable`, not when naro itself updates. Before that, look for
798
+ such rows: `naro db query "select count(*) from t where typeof(c) = 'text'"`
799
+ for each number or boolean column — a count above 0 means a client writes
800
+ strings there and needs fixing first.
801
+
612
802
  ## Machine-readable output (CI / AI agents)
613
803
 
614
804
  Every command supports `--json`: stdout carries exactly one JSON object,