okmodel 0.5.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.
Files changed (169) hide show
  1. package/README.md +36 -18
  2. package/dist/adapters/pg/no-inflight-timeout.d.ts +17 -0
  3. package/dist/adapters/pg/postgres-extra.d.ts +22 -0
  4. package/dist/contracts/catalog/document.d.ts +19 -5
  5. package/dist/contracts/catalog/object.d.ts +2 -1
  6. package/dist/contracts/catalog/policy.d.ts +48 -0
  7. package/dist/contracts/catalog/types.d.ts +25 -1
  8. package/dist/contracts/catalog/view.d.ts +2 -0
  9. package/dist/contracts/internal.js +1 -1
  10. package/dist/contracts/location.d.ts +10 -0
  11. package/dist/dialects/pg/column.d.ts +13 -2
  12. package/dist/dialects/pg/ddl.d.ts +37 -1
  13. package/dist/dialects/pg/emit.d.ts +19 -1
  14. package/dist/dialects/pg/errors.d.ts +32 -1
  15. package/dist/dialects/pg/ext/citext.js +1 -1
  16. package/dist/dialects/pg/ext/index.js +1 -1
  17. package/dist/dialects/pg/ext/pg-trgm.js +1 -1
  18. package/dist/dialects/pg/filter-parse.d.ts +10 -3
  19. package/dist/dialects/pg/fn/index.js +10 -2
  20. package/dist/dialects/pg/index.d.ts +2 -1
  21. package/dist/dialects/pg/index.js +29 -17
  22. package/dist/dialects/pg/introspect.d.ts +7 -0
  23. package/dist/dialects/pg/model.d.ts +7 -2
  24. package/dist/dialects/pg/ops/or.d.ts +1 -1
  25. package/dist/dialects/pg/owner.d.ts +42 -0
  26. package/dist/dialects/pg/path-endpoint.d.ts +20 -0
  27. package/dist/dialects/pg/role/check.d.ts +3 -0
  28. package/dist/dialects/pg/role/index.d.ts +71 -6
  29. package/dist/dialects/pg/role/sql.d.ts +11 -0
  30. package/dist/dialects/pg/schema.d.ts +2 -0
  31. package/dist/dialects/pg/table.d.ts +25 -3
  32. package/dist/dialects/pg/temporal.d.ts +5 -76
  33. package/dist/dialects/pg/tenancy.d.ts +45 -3
  34. package/dist/dialects/pg/view/client.d.ts +19 -1
  35. package/dist/dialects/pg/view/index.d.ts +4 -0
  36. package/dist/dialects/pg/view/index.js +21 -178
  37. package/dist/dialects/pg/view/refresh.d.ts +24 -0
  38. package/dist/runtime/batch.d.ts +1 -1
  39. package/dist/runtime/client.d.ts +6 -1
  40. package/dist/runtime/conflict.d.ts +2 -1
  41. package/dist/runtime/drift.d.ts +9 -0
  42. package/dist/runtime/inflight.d.ts +14 -0
  43. package/dist/runtime/pg/bun.js +6 -5
  44. package/dist/runtime/pg/pg.js +5 -5
  45. package/dist/runtime/pg/pglite.js +6 -5
  46. package/dist/runtime/pg/postgresjs.js +5 -5
  47. package/dist/runtime/plan.d.ts +22 -1
  48. package/dist/runtime/predicate.d.ts +19 -0
  49. package/dist/runtime/safety/index.js +1 -1
  50. package/dist/runtime/tenancy/composite.d.ts +26 -0
  51. package/dist/runtime/tenancy/context.d.ts +16 -0
  52. package/dist/runtime/tenancy/index.d.ts +9 -5
  53. package/dist/runtime/tenancy/index.js +1508 -37
  54. package/dist/runtime/tenancy/path.d.ts +87 -0
  55. package/dist/runtime/tenancy/rls.d.ts +67 -0
  56. package/dist/runtime/tenancy/via.d.ts +22 -0
  57. package/dist/runtime/traits/index.js +17 -11
  58. package/dist/runtime/types.d.ts +13 -1
  59. package/dist/runtime/validate/index.js +4 -4
  60. package/dist/runtime/write.d.ts +1 -1
  61. package/dist/shared/09p049xa.js +702 -0
  62. package/dist/shared/{ykv7wh5b.js → 1f0xa3cb.js} +5 -5
  63. package/dist/shared/1nw1f3vs.js +23 -0
  64. package/dist/shared/{6p5k20sm.js → 1z04fa85.js} +21 -11
  65. package/dist/shared/{p7pfx624.js → 2cg3wnxc.js} +314 -65
  66. package/dist/shared/{dyj86cmj.js → 2jd1sn0e.js} +2 -2
  67. package/dist/shared/{qgvt8nx1.js → 2z7wv5tj.js} +21 -5
  68. package/dist/shared/{kyxb62mc.js → 31vm914h.js} +9 -1
  69. package/dist/shared/{sf8m6tfy.js → 32cnm7kn.js} +8 -3
  70. package/dist/shared/{t4tbsy9h.js → 358zsv4r.js} +1 -1
  71. package/dist/shared/{jdgqqx31.js → 3fqwmr09.js} +38 -14
  72. package/dist/shared/{922qbgnw.js → 3qnhc7w5.js} +16 -9
  73. package/dist/shared/{zvbjc949.js → 4az561fn.js} +1 -1
  74. package/dist/shared/{k3ted12y.js → 54tqkxja.js} +20 -2
  75. package/dist/shared/{k7cc9nzk.js → 5d8t84hg.js} +15 -3
  76. package/dist/shared/{fa9vt245.js → 5j36z8dn.js} +17 -6
  77. package/dist/shared/{1g0drakv.js → 5yv4k6a6.js} +1 -1
  78. package/dist/shared/8dqhth8c.js +12 -0
  79. package/dist/shared/8etn7d8w.js +62 -0
  80. package/dist/shared/{g516zdsb.js → 8x72s86k.js} +476 -30
  81. package/dist/shared/{nkc9sfqb.js → 929h19xp.js} +145 -7
  82. package/dist/shared/{gjthds9j.js → 9628a9xt.js} +1 -1
  83. package/dist/shared/{8xsbagad.js → 9s0ypp87.js} +88 -7
  84. package/dist/shared/{b066g80h.js → ag4c7516.js} +2 -2
  85. package/dist/shared/b3awxn7h.js +45 -0
  86. package/dist/shared/b4v57v3x.js +9 -0
  87. package/dist/shared/bdk4sty6.js +58 -0
  88. package/dist/shared/{s6gqfn5j.js → c58secgj.js} +125 -8
  89. package/dist/shared/{rrstp3kb.js → cgbamx3e.js} +2 -2
  90. package/dist/shared/d5azex6s.js +6 -0
  91. package/dist/shared/{5fqx9yh4.js → dk8v79p3.js} +3 -1
  92. package/dist/shared/{4m9fn52z.js → ed0b8k4s.js} +1 -1
  93. package/dist/shared/{z2rnngj4.js → ek2wt2dz.js} +8 -5
  94. package/dist/shared/{wxn4b7pp.js → f0f1n281.js} +59 -12
  95. package/dist/shared/{gdh9mrtp.js → g90m9b0g.js} +3 -2
  96. package/dist/shared/{tbszx8mz.js → gwn6v667.js} +2 -2
  97. package/dist/shared/{c8aghh05.js → h15jx9bc.js} +8 -2
  98. package/dist/shared/{3p4b1fg0.js → hazqqecq.js} +17 -93
  99. package/dist/shared/{4ef7mmqf.js → j3rkk835.js} +34 -11
  100. package/dist/shared/{bt9sfq0z.js → j60w0dvr.js} +56 -21
  101. package/dist/shared/{ycatkxmq.js → jgqj11w4.js} +1 -1
  102. package/dist/shared/k68eedmy.js +85 -0
  103. package/dist/shared/{szm0cksa.js → knmes2kh.js} +34 -6
  104. package/dist/shared/{ckjbyrt9.js → mb2ycs1p.js} +7 -9
  105. package/dist/shared/{tq9afp8d.js → n076ydsc.js} +1 -1
  106. package/dist/shared/{xhh1srw3.js → nx9hh281.js} +50 -32
  107. package/dist/shared/{kdkxh3kb.js → p248q79y.js} +58 -59
  108. package/dist/shared/{990a291j.js → p6bjzq9q.js} +14 -9
  109. package/dist/shared/{h4nqvjkp.js → pjr6xrm5.js} +2 -2
  110. package/dist/shared/pz6rj8ay.js +212 -0
  111. package/dist/shared/{5vdm4sgp.js → q3j1tq7a.js} +36 -23
  112. package/dist/shared/{8bwbwy8h.js → qq3899ht.js} +128 -18
  113. package/dist/shared/{dxjp14rk.js → r11n8grd.js} +2 -2
  114. package/dist/shared/r1fdd0f6.js +19 -0
  115. package/dist/shared/{8ktwd3rh.js → r3j8s5y0.js} +1 -1
  116. package/dist/shared/rhs3xe4k.js +7 -0
  117. package/dist/shared/{jsd07prz.js → rpycarcr.js} +6 -6
  118. package/dist/shared/{xsq7v6wp.js → sbdme3vy.js} +14 -2
  119. package/dist/shared/{xdpj5kkm.js → t3g0k88x.js} +775 -264
  120. package/dist/shared/{ayz64vzj.js → ttmg09vd.js} +708 -19
  121. package/dist/shared/{r6pj4fq2.js → venqrjna.js} +8 -4
  122. package/dist/shared/{fancm5pt.js → vjpnzwjz.js} +138 -15
  123. package/dist/shared/{9tgmr1gh.js → vmfkhszw.js} +44 -9
  124. package/dist/shared/{wqem4jed.js → wkme6tep.js} +11 -9
  125. package/dist/shared/{qy9rdq0g.js → wt5rzv1h.js} +15 -11
  126. package/dist/shared/wxr8cwns.js +6 -0
  127. package/dist/shared/{bzsjtd7c.js → wynzeggs.js} +19 -14
  128. package/dist/shared/x3jc53kg.js +220 -0
  129. package/dist/shared/{7p2wbz7f.js → x72jb7ad.js} +1 -1
  130. package/dist/shared/{fyqkk1m0.js → xq0mvhts.js} +10 -9
  131. package/dist/shared/xqve9598.js +53 -0
  132. package/dist/shared/{47xzb9fc.js → y2t3jktd.js} +2 -2
  133. package/dist/shared/{9x25fxhc.js → y34a8714.js} +1 -1
  134. package/dist/shared/{k7dwkhvq.js → y6jdds7g.js} +4 -1
  135. package/dist/shared/{h8ne16fq.js → ys0xzyaj.js} +1 -1
  136. package/dist/shared/{c9z5g0fm.js → ywr8hmey.js} +5 -2
  137. package/dist/shared/{g7ez901n.js → ywspezzx.js} +1 -1
  138. package/dist/shared/{9m80mkac.js → ze4hxsmy.js} +1 -1
  139. package/dist/shared/{fb00y8fp.js → zm0mafk4.js} +12 -6
  140. package/dist/shared/{y005b761.js → zy8r7qvz.js} +6 -2
  141. package/dist/tooling/cli.js +97 -24
  142. package/dist/tooling/migrate/apply.d.ts +14 -0
  143. package/dist/tooling/migrate/check.d.ts +3 -4
  144. package/dist/tooling/migrate/classify.d.ts +4 -0
  145. package/dist/tooling/migrate/commands.d.ts +1 -2
  146. package/dist/tooling/migrate/config.d.ts +3 -2
  147. package/dist/tooling/migrate/drift.d.ts +1 -1
  148. package/dist/tooling/migrate/estimate.d.ts +14 -1
  149. package/dist/tooling/migrate/files.d.ts +11 -1
  150. package/dist/tooling/migrate/index.js +17 -16
  151. package/dist/tooling/migrate/lint.d.ts +4 -4
  152. package/dist/tooling/migrate/lost.d.ts +28 -0
  153. package/dist/tooling/migrate/paths.d.ts +14 -0
  154. package/dist/tooling/migrate/plan.d.ts +10 -3
  155. package/dist/tooling/migrate/policy.d.ts +11 -0
  156. package/dist/tooling/migrate/runner.d.ts +25 -0
  157. package/dist/tooling/migrate/sandbox.d.ts +57 -0
  158. package/dist/tooling/migrate/sql-lex.d.ts +39 -0
  159. package/dist/tooling/migrate/text.d.ts +42 -0
  160. package/dist/tooling/node-version.d.ts +21 -0
  161. package/dist/tooling/testing/factories.d.ts +10 -0
  162. package/dist/tooling/testing/facts.d.ts +33 -5
  163. package/dist/tooling/testing/index.d.ts +1 -1
  164. package/dist/tooling/testing/index.js +3 -3
  165. package/dist/tooling/testing/isolation.d.ts +15 -8
  166. package/package.json +10 -6
  167. package/dist/shared/0v1g424p.js +0 -26
  168. package/dist/shared/r73pt594.js +0 -426
  169. package/dist/shared/rasvvzgz.js +0 -21
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  okmodel is a catalog-first TypeScript ORM, PostgreSQL first. Other SQL databases are the direction. CI runs PostgreSQL 15 to 18. The schema is the single source. Migrations and queries come from it.
4
4
 
5
- Version 0.5.0. Apache-2.0.
5
+ Apache-2.0. The published version is [okmodel on npm](https://www.npmjs.com/package/okmodel).
6
6
 
7
7
  ## Contents
8
8
 
@@ -80,9 +80,11 @@ bun add okmodel @electric-sql/pglite
80
80
 
81
81
  Bun.sql needs no extra package: `import { connect } from "okmodel/pg/bun"`. okmodel has no runtime dependencies. Each installed driver is an optional peer.
82
82
 
83
+ The `okm` bin's shebang is node, so `bunx okm` runs it with Node even inside a Bun project. Run `bun --bun okm` when the command must use the Bun runtime (Bun.sql runs only there). A `.ts` config file under plain Node needs Node 22.18 or newer for type stripping.
84
+
83
85
  ## Quickstart
84
86
 
85
- This builds two tables, applies them, and runs one script. The script writes an author and a note, then reads the note with its author. `DATABASE_URL` is a direct Postgres URL, not a pooler.
87
+ This builds two tables, applies them, and runs one script. The script writes an author and a note, then reads the note with its author. `DATABASE_URL` is a direct Postgres URL, not a pooler. Port `6432` and port `6543` count as a pooler. Apply, seed, and check also refuse when the backend pid changes between two statements (OKM1854), unless `--allow-pooler` is set.
86
88
 
87
89
  ### Configure
88
90
 
@@ -343,12 +345,12 @@ if (row?.title !== "hello" || row.author?.name !== "Ada") {
343
345
 
344
346
  ### One
345
347
 
346
- `one` returns the single note for that id. More than one match throws `not_unique`.
348
+ `one` returns the note for that id, or null when none matches. More than one match throws `not_unique`.
347
349
 
348
350
  ```ts
349
- // One row. More than one match throws.
351
+ // One row, or null. More than one match throws.
350
352
  const single = await db.notes.one({ where: { id: note.id } });
351
- if (single.title !== "hello") throw new Error("one did not return the note");
353
+ if (single === null || single.title !== "hello") throw new Error("one did not return the note");
352
354
  ```
353
355
 
354
356
  ### Count
@@ -373,7 +375,7 @@ if (!(await db.notes.exists({ where: { id: note.id } }))) {
373
375
 
374
376
  ### Update
375
377
 
376
- `update` changes the columns in `set` on the rows that match `where`. A call with no `where` is OKM1102. `.all(reason)` is the form that means every row.
378
+ `update` changes the columns in `set` on the rows that match `where`. A write with no effective predicate, including undefined values, is OKM1102. An empty `or()` branch is OKM1121. `.all(reason)` is the form that means every row.
377
379
 
378
380
  ```ts
379
381
  // where picks the row. set names the new title.
@@ -382,7 +384,7 @@ await db.notes.update({ where: { id: note.id }, set: { title: "next" } });
382
384
 
383
385
  ### Delete
384
386
 
385
- `delete` removes the rows that match `where`. A call with no `where` is OKM1102. `.all(reason)` is the form that means every row.
387
+ `delete` removes the rows that match `where`. A write with no effective predicate, including undefined values, is OKM1102. An empty `or()` branch is OKM1121. `.all(reason)` is the form that means every row.
386
388
 
387
389
  ```ts
388
390
  // where picks the row. Without it the call is refused.
@@ -396,6 +398,8 @@ Inserting Ada again fails because the name is unique. `safe` returns that `OkmEr
396
398
  ```ts
397
399
  import { OkmError, safe } from "okmodel";
398
400
 
401
+ import { db } from "./db.ts";
402
+
399
403
  // safe returns the unique error instead of throwing.
400
404
  const duplicate = await safe(db.authors.insert({ name: "Ada" }));
401
405
  if (duplicate.ok || !(duplicate.error instanceof OkmError) || duplicate.error.kind !== "unique") {
@@ -429,7 +433,7 @@ export const app = schema({ tables: [tasks] });
429
433
 
430
434
  ### Hidden
431
435
 
432
- A password hash should not come back on an ordinary read. `.hidden()` leaves the column out of `find` and out of `include`. Name it in `select` when that call needs it.
436
+ A password hash should not come back on an ordinary read, and a caller should not be able to filter on it. `.hidden()` leaves the column out of `find` and out of `include`, and `where` and `orderBy` refuse it (OKM1120). Name it in `select` when that call needs it. `hidden({ filterable: true })` allows `where` and `orderBy` and still leaves it out of the default row. `filters()` still refuses a hidden column.
433
437
 
434
438
  `hidden.ts`:
435
439
 
@@ -709,7 +713,7 @@ export async function hide(url: string, id: string): Promise<void> {
709
713
 
710
714
  ### Tenancy
711
715
 
712
- Every note belongs to one tenant. Column tenancy adds `tenantId` and puts that tenant on every read and write. `db.for({ tenantId }).notes.find({ limit: 20 })` reads one tenant. `global("shared")` marks a table every tenant can read, such as a list of countries. Row-level security is not in this version. See [known limits](https://github.com/omqkhafi/okmodel/blob/main/docs/known-limits.md).
716
+ Every note belongs to one tenant. Column tenancy adds `tenantId` and puts that tenant on every read and write. `db.for({ tenantId }).notes.find({ limit: 20 })` reads one tenant. Composite tenancy passes every key, `compositeTenancy({ key: ["organizationId", "workspaceId"], type: "uuid" })` and `db.for({ organizationId, workspaceId })`. A table without its own key uses `tenancy: via("project.organization")`, up to three relations, and reads and writes follow that path. `global("shared")` marks a table every tenant can read, such as a list of countries. `rlsTenancy({ key: "tenantId", type: "uuid" })` keeps that predicate and adds row-level security policies. A role that owns the tables, a superuser, or `BYPASSRLS` is refused on the first scoped call. See [known limits](https://github.com/omqkhafi/okmodel/blob/main/docs/known-limits.md).
713
717
 
714
718
  `tenancy.ts`:
715
719
 
@@ -828,7 +832,7 @@ export const app = schema({
828
832
 
829
833
  ### Views
830
834
 
831
- A view is a saved query. You give it the SQL and the columns that query returns. `db.views.active.find({ limit: 20 })` reads `active`. That handle has no insert or update. There is no query-builder form yet. `okm check` reprints the query on the connected server before it compares, so a difference that is only how that server prints the query is not drift. A materialized view that refreshes while people are reading it needs a unique index, or the declaration is OKM1822. See [known limits](https://github.com/omqkhafi/okmodel/blob/main/docs/known-limits.md).
835
+ A view is a saved query. You give it the SQL and the columns that query returns. `db.views.active.find({ limit: 20 })` reads `active`. That handle has no insert or update. View handles are camelCased: a view named `active_projects` reads as `db.views.activeProjects`. There is no query-builder form yet. `okm check` reprints the query on the connected server before it compares, so a difference that is only how that server prints the query is not drift. A materialized view refreshes with `REFRESH MATERIALIZED VIEW`: the plan populates it with a plain refresh first (Postgres rejects `CONCURRENTLY` on an empty view), then refreshes concurrently when the declaration sets `refresh: "concurrently"`. A materialized view that refreshes while people are reading it needs a unique index, or the declaration is OKM1822. See [known limits](https://github.com/omqkhafi/okmodel/blob/main/docs/known-limits.md).
832
836
 
833
837
  `views.ts`:
834
838
 
@@ -862,7 +866,13 @@ export const app = schema({
862
866
 
863
867
  ### Roles and grants
864
868
 
865
- The migration connects as one role and the app connects as another. `roles` names both. The app role receives select, insert, update, and delete on tables and views, select on materialized views, execute on functions, and usage plus select on sequences. Apply runs as the migration role, and switches to it with `SET ROLE` when the connection is someone else. The app role is a role you already created, unless you list it in `managed`. Grants on one column, and row-level security, are not in this version. See [known limits](https://github.com/omqkhafi/okmodel/blob/main/docs/known-limits.md).
869
+ The migration connects as one role and the app connects as another. `roles` names both. The app role receives select, insert, update, and delete on tables and views, select on materialized views, execute on functions, and usage plus select on sequences. Apply also grants that role select on `okm_meta`, and nothing else there, so `connect()` can read the stored hash. Apply runs as the migration role, and switches to it with `SET ROLE` when the connection is someone else. A role you already created is external, unless you list it in `managed`.
870
+
871
+ `roles.reporting` is a third role that can only select. It gets select on tables, views, and materialized views, usage on schemas, and the same select on tables created later. It does not get insert, update, delete, or execute. A hidden column is left out unless `roles.columns` names it. Under `rlsTenancy()` this role is not `BYPASSRLS` and it is not tenant-safe against its own SQL: it sees no tenant rows until the session sets `app.tenant`, and it can set `app.unscoped` itself and then read every tenant. `unscoped()` is the application client's read escape and is not given to this role.
872
+
873
+ `roles.columns` grants select or update on named columns: `{ role, on, select, update }`. `on` is the table or view. A listed column grant on a view replaces select on the whole view for the reporting role.
874
+
875
+ `withOwner(table(...), "ignored")` keeps a table out of diffs, plans, and `okm check`. `"external"` is known and never created, altered, or dropped. A managed table that becomes either stays in the database. Drop it while it is still managed if it should go away. See [known limits](https://github.com/omqkhafi/okmodel/blob/main/docs/known-limits.md).
866
876
 
867
877
  `roles.ts`:
868
878
 
@@ -876,7 +886,13 @@ export default defineConfig({
876
886
  schema: "./schema.ts",
877
887
  database: url,
878
888
  // Apply uses the migration role. The app uses the app role.
879
- roles: { migration: "okm_migrate", app: "okm_app", managed: [{ name: "okm_migrate" }] },
889
+ roles: {
890
+ migration: "okm_migrate",
891
+ app: "okm_app",
892
+ reporting: "okm_report",
893
+ managed: [{ name: "okm_migrate" }, { name: "okm_report", login: true }],
894
+ columns: [{ role: "okm_report", on: "tasks", select: ["title"] }],
895
+ },
880
896
  });
881
897
  ```
882
898
 
@@ -945,7 +961,7 @@ update "public"."notes" set "title" = 'noted' where "title" = 'hello' and ("id"
945
961
 
946
962
  ### Testing
947
963
 
948
- `okmodel/testing` opens a real pool. `factories` insert rows, `expectQueries` counts statements, and `isolation()` checks that tenant A cannot see tenant B. `okm seed <file>` calls the file's default export with that harness. The target must already be migrated. A protected target is refused unless `--allow-protected`.
964
+ `okmodel/testing` opens a real pool. `factories` insert rows, `expectQueries` counts statements, and `isolation()` checks that tenant A cannot see the row it inserted for tenant B, including through a tenant view or materialized view. Each call uses a new pair of tenant ids, deletes what it inserted, and names a factory that is missing. A global table or view is skipped and listed. `okm seed <file>` calls the file's default export with that harness. The target must already be migrated. A protected target is refused unless `--allow-protected`.
949
965
 
950
966
  `factory.ts`:
951
967
 
@@ -1071,7 +1087,7 @@ The worked example is a project tracker: workspaces as tenants, archive, a funct
1071
1087
 
1072
1088
  ### Startup
1073
1089
 
1074
- `connect()` compares the app with `okm_history`. A database that is ahead by expand migrations still opens. Ahead by a contract migration, or behind the app, is OKM1520 and names the migration. `okm migrate status` prints the same states, plus `failed at step N (resume with okm migrate apply)`.
1090
+ `connect()` compares the app with `okm_history`. A database that is ahead by expand migrations still opens. Ahead by a contract migration, or behind the app, is OKM1520 and names the migration. `okm migrate status` prints the same states, plus `failed at step N (resume with okm migrate apply)`, where N is the stored step index. Apply's own `failed at step N` counts from 1, the same number the linter prints.
1075
1091
 
1076
1092
  ### Protected targets
1077
1093
 
@@ -1144,6 +1160,8 @@ Status is on the [board](https://github.com/users/omqkhafi/projects/1). Each rel
1144
1160
  - [x] [0.3](https://github.com/omqkhafi/okmodel/milestone/3) — Extensions, domains, functions, triggers, views, roles, and grants.
1145
1161
  - [x] [0.4](https://github.com/omqkhafi/okmodel/milestone/4) — Safer migration plans, backfill, drift checks, provisioning, reference data, and a testing package.
1146
1162
  - [x] [0.5](https://github.com/omqkhafi/okmodel/milestone/5) — A primary with replicas, read routing, and a reference app.
1163
+ - [x] [0.5.1](https://github.com/omqkhafi/okmodel/milestone/7) — QA fixes for 0.5.0.
1164
+ - [x] [0.6](https://github.com/omqkhafi/okmodel/milestone/8) — Composite, path, and row-level tenancy, column grants, a reporting role, and SQL constraint names.
1147
1165
 
1148
1166
  `okmodel/internal` has no stability promise. Names on that subpath can change or disappear in any release.
1149
1167
 
@@ -1155,11 +1173,11 @@ Measured on this release.
1155
1173
 
1156
1174
  | | Minified | Gzip | Cold import |
1157
1175
  | -------------------------------------------- | -------: | -----: | ----------: |
1158
- | Runtime entry | 5,288 | 2,026 | 2.347 ms |
1159
- | App startup (10 tables, one find) | 89,751 | 29,801 | 15.061 ms |
1160
- | App startup, every 0.2 feature in use (full) | 120,132 | 39,162 | 15.956 ms |
1176
+ | Runtime entry | 5,288 | 2,026 | 1.790 ms |
1177
+ | App startup (10 tables, one find) | 90,762 | 30,139 | 10.490 ms |
1178
+ | App startup, every 0.2 feature in use (full) | 121,506 | 39,741 | 12.455 ms |
1161
1179
 
1162
- The full app has column tenancy, `archivable()`, `timestamps()`, validation rules, `one`, `many` and `manyThrough` relations, presets, and calls `include`, `page`, `aggregate`, `tx` and `batch`. A feature costs bytes only in an app that uses it: the full app is 30,381 minified and 9,361 gzip bytes above the plain one. The runtime entry and the plain app are gated. The full app is printed, not gated. The plain app's cold import on this sample is above the 15 ms local reference. That figure is printed and is not a gate.
1180
+ The full app has column tenancy, `archivable()`, `timestamps()`, validation rules, `one`, `many` and `manyThrough` relations, presets, and calls `include`, `page`, `aggregate`, `tx` and `batch`. A feature costs bytes only in an app that uses it: the full app is 30,744 minified and 9,602 gzip bytes above the plain one. The runtime entry and the plain app are gated. The full app is printed, not gated. The plain app's cold import on this sample is under the 15 ms local reference. That figure is printed and is not a gate.
1163
1181
 
1164
1182
  The rest of the measurements are in [size](https://github.com/omqkhafi/okmodel/blob/main/docs/size.md).
1165
1183
 
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Refusal for a driver that cannot cancel an in-flight statement (D224).
3
+ *
4
+ * Bun.sql and PGlite throw OKM1120 for a positive `timeout`. The message
5
+ * stays `timeout` because the PGlite connect graph has two gzip bytes left.
6
+ * Kind is `invalid`, which separates it from kind `timeout`. `timeout: 0`
7
+ * still fails immediately as kind `timeout`. `signal` is unchanged.
8
+ * postgres.js and node-postgres do not import this.
9
+ */
10
+ /**
11
+ * Throws OKM1120 when `timeout` is a positive number.
12
+ *
13
+ * @param options - Per-call options. Absent, and a non-positive timeout, pass through
14
+ */
15
+ export declare function noTimeout(options: {
16
+ readonly timeout?: number;
17
+ } | undefined): void;
@@ -42,6 +42,28 @@ export declare function describe(sql: Sql, text: string, params?: readonly WireV
42
42
  * @returns Chunks of wire rows
43
43
  */
44
44
  export declare function stream(sql: Sql, text: string, params?: readonly WireValue[]): AsyncIterable<readonly (readonly WireValue[])[]>;
45
+ type Lost = {
46
+ on: boolean;
47
+ };
48
+ /**
49
+ * Rejects later writes once postgres.js reports the reserved socket closed.
50
+ *
51
+ * The first statement installs `onclose`. The checkout guard then refuses
52
+ * the next statement, savepoint, or COMMIT before `socket.write`.
53
+ *
54
+ * @param sql - Reserved connection
55
+ * @param lost - Set when that socket closes
56
+ */
57
+ /**
58
+ * Sets `lost` when the reserved socket closes.
59
+ *
60
+ * The first statement installs `onclose`. Later statements see the flag
61
+ * before they write.
62
+ *
63
+ * @param sql - Reserved connection
64
+ * @param lost - Set when that socket closes
65
+ */
66
+ export declare function watchReserved(sql: Sql, lost: Lost): void;
45
67
  /** Refcount a checkout or a listener borrows from the pool. */
46
68
  type Gate = {
47
69
  enter(): void;
@@ -67,11 +67,13 @@ export declare function catalogHash(source: Catalog): string;
67
67
  *
68
68
  * Roles, grants, and default privileges come from the migration config, not
69
69
  * the schema, so they are left out. A `schema()` catalog never has them, so
70
- * `connect()` hashes it unchanged. `okm check` still verifies them. A
71
- * catalog without them is returned as is.
70
+ * `connect()` hashes it unchanged. `okm check` still verifies them. An
71
+ * `external` or `ignored` owner is planning state. The startup copy writes
72
+ * it back to `managed`, which is what `schema()` stored. A catalog with
73
+ * neither privileges nor those owners is returned as is.
72
74
  *
73
75
  * @param source - Catalog document
74
- * @returns The catalog without role, grant, and default-privilege objects
76
+ * @returns The catalog `connect()` hashes
75
77
  */
76
78
  export declare function startupCatalog(source: Catalog): Catalog;
77
79
  /**
@@ -89,11 +91,23 @@ export declare function startupHash(source: Catalog): string;
89
91
  * stored hash and the version, then trusts the objects (D133). `okm check`
90
92
  * and dev keep {@link parseCatalog}.
91
93
  *
92
- * @param text - Canonical catalog JSON
93
- * @param hash - SHA-256 of `text`, stored beside the artifact
94
+ * @param text - Catalog JSON. A BOM is stripped and `\r` is removed before the hash check
95
+ * @param hash - SHA-256 of that normalized text, stored beside the artifact
94
96
  * @returns The catalog document
95
97
  */
96
98
  export declare function loadTrustedCatalog(text: string, hash: string): Catalog;
99
+ /**
100
+ * Catalog file bytes the hash covers.
101
+ *
102
+ * A leading UTF-8 BOM is dropped. Carriage returns are removed, so a CRLF
103
+ * checkout hashes like the LF file the writer stored. An LF file is unchanged.
104
+ * UTF-16 is OKM1027.
105
+ *
106
+ * @param text - Raw catalog JSON or hash file
107
+ * @param label - Name used when the text is UTF-16
108
+ * @returns Text to hash and parse
109
+ */
110
+ export declare function catalogFileText(text: string, label?: string): string;
97
111
  /**
98
112
  * Reads canonical catalog JSON.
99
113
  *
@@ -106,7 +106,8 @@ export declare function index(input: IndexInput): IndexObject;
106
106
  * Builds a constraint object.
107
107
  *
108
108
  * Primary key names are `{parent}_pkey` and do not include column names.
109
- * Other names use `nameKey`, which stays fixed when a field is renamed.
109
+ * Unique and foreign-key names use every SQL column. Checks use `nameKey`,
110
+ * which stays fixed when a field is renamed. `nameKey` is always stored.
110
111
  *
111
112
  * @param input - Parent, kind, columns, and the stable name key
112
113
  * @returns A constraint envelope
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Row-level security policy catalog objects.
3
+ *
4
+ * The factory stores the record. It does not plan SQL. Postgres DDL stays in
5
+ * the dialect. A schema that never imports `rlsTenancy` does not load this.
6
+ */
7
+ import type { ObjectIdentity, ObjectRef, Owner, PolicyCommand, PolicyObject, Provenance } from "./types.js";
8
+ /** Input for {@link policyObject}. */
9
+ export type PolicyInput = {
10
+ readonly parent: ObjectRef;
11
+ readonly name: string;
12
+ readonly command: PolicyCommand;
13
+ readonly expression: string;
14
+ /** True when the table is `FORCE ROW LEVEL SECURITY`. */
15
+ readonly force: boolean;
16
+ readonly owner?: Owner;
17
+ readonly provenance: Provenance;
18
+ /** Extra edges. The parent table is always included. */
19
+ readonly dependencies?: readonly ObjectIdentity[];
20
+ };
21
+ /**
22
+ * Predicate stored for `{table}_tenant`.
23
+ *
24
+ * The text is what `pg_get_expr` prints, so a replayed catalog matches.
25
+ * An unset `app.tenant` becomes null and the comparison matches no row.
26
+ *
27
+ * @param column - SQL name of the tenant column
28
+ * @param dataType - Postgres type of that column
29
+ * @returns The `USING` and `WITH CHECK` expression
30
+ */
31
+ export declare function tenantPolicyExpression(column: string, dataType: string): string;
32
+ /**
33
+ * Predicate stored for `{table}_unscoped_select`.
34
+ *
35
+ * `SELECT` only. Writes stay on the tenant policy.
36
+ *
37
+ * @returns The `USING` expression
38
+ */
39
+ export declare function unscopedPolicyExpression(): string;
40
+ /**
41
+ * Builds a policy record.
42
+ *
43
+ * Identity is `(parent, name)`. A name past the dialect limit is fitted.
44
+ *
45
+ * @param input - Parent table, command, and expression
46
+ * @returns The catalog object
47
+ */
48
+ export declare function policyObject(input: PolicyInput): PolicyObject;
@@ -393,6 +393,13 @@ export type ViewColumn = {
393
393
  export type ViewDefinition = {
394
394
  readonly columns: readonly ViewColumn[];
395
395
  readonly query: string;
396
+ /**
397
+ * `WITH (security_invoker = true)`.
398
+ *
399
+ * Set only for a view over tenant tables under `rlsTenancy`. Omitted
400
+ * otherwise, so a view under another strategy keeps the same catalog bytes.
401
+ */
402
+ readonly securityInvoker?: true;
396
403
  };
397
404
  /**
398
405
  * Materialized view definition.
@@ -440,10 +447,27 @@ export type RoleObject = CatalogEnvelope<"role", RoleIdentity, RoleDefinition>;
440
447
  export type GrantObject = CatalogEnvelope<"grant", GrantIdentity, GrantDefinition>;
441
448
  /** A default privilege. Identity is `(forRole, namespace, objectKind, grantee, privilege)`. */
442
449
  export type DefaultPrivilegeObject = CatalogEnvelope<"defaultPrivilege", DefaultPrivilegeIdentity, DefaultPrivilegeDefinition>;
450
+ /** Which statements a policy covers. `all` is every command. `select` is reads. */
451
+ export type PolicyCommand = "all" | "select";
452
+ /**
453
+ * One row-level security policy.
454
+ *
455
+ * `expression` is the `USING` text, and the `WITH CHECK` text when `command`
456
+ * is `all`. `force` is true when the table is `FORCE ROW LEVEL SECURITY`.
457
+ */
458
+ export type PolicyDefinition = {
459
+ readonly command: PolicyCommand;
460
+ readonly expression: string;
461
+ readonly force: boolean;
462
+ };
463
+ /** A policy. Identity is `(parent, name)`. */
464
+ export type PolicyObject = CatalogEnvelope<"policy", AnchoredIdentity & {
465
+ readonly kind: "policy";
466
+ }, PolicyDefinition>;
443
467
  /**
444
468
  * One built catalog object.
445
469
  */
446
- export type CatalogObject = TableObject | ColumnObject | IndexObject | ConstraintObject | SequenceObject | TypeObject | ExtensionObject | FunctionObject | TriggerObject | ViewObject | MaterializedViewObject | RoleObject | GrantObject | DefaultPrivilegeObject;
470
+ export type CatalogObject = TableObject | ColumnObject | IndexObject | ConstraintObject | SequenceObject | TypeObject | ExtensionObject | FunctionObject | TriggerObject | ViewObject | MaterializedViewObject | RoleObject | GrantObject | DefaultPrivilegeObject | PolicyObject;
447
471
  /** Format version stored in the serialized catalog. */
448
472
  export declare const CATALOG_VERSION = 1;
449
473
  /** A catalog document. Object order is identity-key order. */
@@ -11,6 +11,8 @@ export type ViewInput = {
11
11
  readonly name: string;
12
12
  readonly columns: readonly ViewColumn[];
13
13
  readonly query: string;
14
+ /** `WITH (security_invoker = true)`. Omitted from the record when unset. */
15
+ readonly securityInvoker?: true;
14
16
  readonly owner?: Owner;
15
17
  readonly provenance: Provenance;
16
18
  readonly dependencies?: readonly ObjectIdentity[];
@@ -13,7 +13,7 @@ import {
13
13
  index3,
14
14
  constraint2,
15
15
  sequence2
16
- } from "../shared/t4tbsy9h.js";
16
+ } from "../shared/358zsv4r.js";
17
17
  import {
18
18
  OWNERS2,
19
19
  OBJECT_KINDS2,
@@ -14,6 +14,16 @@
14
14
  * @returns `file.ts:line`, or `undefined` when the runtime has no stack
15
15
  */
16
16
  export declare function callerLocation(skip: number): string | undefined;
17
+ /**
18
+ * File and line from stack lines, skipping `location.ts` frames.
19
+ *
20
+ * A frame may use `/` or `\`.
21
+ *
22
+ * @param lines - `Error.stack` split on `\n`
23
+ * @param skip - Callers to pass before the frame that is recorded
24
+ * @returns `file.ts:line`, or `undefined` when no frame remains
25
+ */
26
+ export declare function locationFromLines(lines: readonly string[], skip: number): string | undefined;
17
27
  /**
18
28
  * Appends a source location to an error message.
19
29
  *
@@ -207,6 +207,12 @@ export type ColumnState<TValue> = {
207
207
  } | undefined;
208
208
  readonly guarded: boolean;
209
209
  readonly hidden: boolean;
210
+ /**
211
+ * Hidden, and allowed in `where` and `orderBy`.
212
+ *
213
+ * Absent otherwise. Not part of the catalog or its hash.
214
+ */
215
+ readonly filterable?: true;
210
216
  /** Redacted in logs, errors, and `inspect()`. Not part of the catalog hash. */
211
217
  readonly sensitive: boolean;
212
218
  readonly omitWrite: boolean;
@@ -383,11 +389,16 @@ export declare class ColumnBuilder<TValue, TFlags extends ColumnFlags> {
383
389
  /**
384
390
  * Excludes the column from the default row.
385
391
  *
386
- * A named `select` still returns it. Includes never do.
392
+ * A named `select` still returns it. Includes never do. `where` and
393
+ * `orderBy` refuse it (OKM1120) unless `filterable` is set. `filters()`
394
+ * still refuses it.
387
395
  *
396
+ * @param options - `filterable` allows the column in `where` and `orderBy`
388
397
  * @returns The same column, hidden
389
398
  */
390
- hidden(): ColumnBuilder<TValue, FlagTrue<TFlags, "hidden">>;
399
+ hidden(options?: {
400
+ readonly filterable?: boolean;
401
+ }): ColumnBuilder<TValue, FlagTrue<TFlags, "hidden">>;
391
402
  /**
392
403
  * Redacts the value in logs, errors, and `inspect()`.
393
404
  *
@@ -4,7 +4,7 @@
4
4
  * Planning and scratch comparison both render through here, so a statement
5
5
  * has one spelling.
6
6
  */
7
- import type { Catalog, CatalogObject, ColumnObject, FunctionObject, MaterializedViewObject, SequenceObject, TableObject, ViewObject } from "../../contracts/catalog/types.js";
7
+ import type { Catalog, CatalogObject, ColumnObject, FunctionObject, MaterializedViewObject, PolicyObject, SequenceObject, TableObject, ViewObject } from "../../contracts/catalog/types.js";
8
8
  /**
9
9
  * Quotes one identifier.
10
10
  *
@@ -23,6 +23,18 @@ export declare function quoteIdent(name: string): string;
23
23
  * @returns SQL statements
24
24
  */
25
25
  export declare function renderCatalog(source: Catalog, schema: string): readonly string[];
26
+ /**
27
+ * Pulls a unique constraint in front of a foreign key that references it.
28
+ *
29
+ * The primary key is created with the table. A foreign key aimed at another
30
+ * unique has to wait for that unique. Create order ties those two by name, so
31
+ * the foreign key can otherwise come first. The planner uses this for creates
32
+ * and for the list it reverses when it drops.
33
+ *
34
+ * @param objects - Create order
35
+ * @returns The same objects, with those uniques moved forward
36
+ */
37
+ export declare function uniqueBeforeForeignKey(objects: readonly CatalogObject[]): readonly CatalogObject[];
26
38
  /**
27
39
  * Reports whether an index belongs to a view or a materialized view.
28
40
  *
@@ -74,6 +86,30 @@ export declare function functionSql(object: FunctionObject, schema: string, repl
74
86
  * @returns The statement
75
87
  */
76
88
  export declare function viewSql(object: ViewObject, schema: string, replace: boolean): string;
89
+ /** One policy statement and the planner kind that classifies it. */
90
+ export type PolicyPlanStep = {
91
+ readonly sql: string;
92
+ readonly kind: "enable-rls" | "create-policy" | "drop-policy" | "disable-rls";
93
+ };
94
+ /**
95
+ * `ENABLE`, `FORCE`, and `CREATE POLICY` for one policy.
96
+ *
97
+ * The `all` command policy also enables and forces row-level security.
98
+ * The `select` policy is the unscoped read. Names are the stored identity.
99
+ *
100
+ * @param object - Policy to create
101
+ * @param schema - Concrete schema name
102
+ * @returns Statements in apply order
103
+ */
104
+ export declare function policyCreatePlan(object: PolicyObject, schema: string): readonly PolicyPlanStep[];
105
+ /**
106
+ * `DROP POLICY`, and for the `all` command policy `NO FORCE` and `DISABLE`.
107
+ *
108
+ * @param object - Policy to drop
109
+ * @param schema - Concrete schema name
110
+ * @returns Statements in apply order
111
+ */
112
+ export declare function policyDropPlan(object: PolicyObject, schema: string): readonly PolicyPlanStep[];
77
113
  /**
78
114
  * `CREATE MATERIALIZED VIEW ... WITH NO DATA`.
79
115
  *
@@ -6,6 +6,14 @@
6
6
  */
7
7
  import { type BuiltSchema } from "./schema.js";
8
8
  import { type AnyTable } from "./table.js";
9
+ /**
10
+ * Type arguments read from `t.json<T>()` and `t.jsonb<T>()` in schema source.
11
+ *
12
+ * The key is `table.field`. QA-L3 kept the wrapper for a bare `t.jsonb()`,
13
+ * which has no argument and stays `jsonb<unknown>`. The documented call
14
+ * `t.jsonb<{ published?: boolean }>()` is this map.
15
+ */
16
+ export type JsonColumnTypes = ReadonlyMap<string, string>;
9
17
  /**
10
18
  * Emits row, insert, and update interfaces for every table.
11
19
  *
@@ -13,6 +21,16 @@ import { type AnyTable } from "./table.js";
13
21
  * and `TasksUpdate`.
14
22
  *
15
23
  * @param source - Schema from {@link schema}
24
+ * @param jsonTypes - Type arguments from the schema source. Omitted, json and jsonb stay `unknown`
16
25
  * @returns Declaration text
17
26
  */
18
- export declare function emitRowTypes(source: BuiltSchema<readonly AnyTable[]>): string;
27
+ export declare function emitRowTypes(source: BuiltSchema<readonly AnyTable[]>, jsonTypes?: JsonColumnTypes): string;
28
+ /**
29
+ * Reads `t.json<T>()` and `t.jsonb<T>()` type arguments from schema source.
30
+ *
31
+ * A bare `t.jsonb()` has no argument. The key is the table name and the field.
32
+ *
33
+ * @param source - Schema source, comments removed by the caller or still present
34
+ * @returns `table.field` to the type argument text
35
+ */
36
+ export declare function jsonColumnTypes(source: string): JsonColumnTypes;
@@ -2,9 +2,30 @@
2
2
  * Maps a Postgres {@link DriverError} to an {@link OkmError} (spec §4.2, §14).
3
3
  *
4
4
  * Adapters normalise the driver into `DriverError`. They do not construct
5
- * `OkmError`. Constraint names follow the catalog: `{table}_{nameKey}_{tag}`.
5
+ * `OkmError`. `connect()` passes its schema. A reason then comes from the
6
+ * error's columns matched to a catalog object. With no catalog, the
7
+ * constraint name is parsed.
6
8
  */
9
+ import type { Catalog } from "../../contracts/catalog/types.js";
7
10
  import { OkmError, type ErrorStatuses } from "../../contracts/error.js";
11
+ /**
12
+ * Catalog and model used to name a unique, foreign key, or check.
13
+ *
14
+ * `connect()` passes the schema it already holds. The reference is enough:
15
+ * matching stays in this module.
16
+ */
17
+ export type MapPostgresErrorSchema = {
18
+ readonly catalog: Catalog;
19
+ readonly model: Readonly<Record<string, {
20
+ readonly sql: string;
21
+ readonly columns: readonly SqlField[];
22
+ }>>;
23
+ };
24
+ /** One column the reason mapper can turn back into a TypeScript field. */
25
+ type SqlField = {
26
+ readonly field: string;
27
+ readonly sql: string;
28
+ };
8
29
  /** Options for {@link mapPostgresError}. */
9
30
  export type MapPostgresErrorOptions = {
10
31
  /**
@@ -15,6 +36,15 @@ export type MapPostgresErrorOptions = {
15
36
  readonly includeValues?: boolean;
16
37
  /** Category overrides from `connect({ errors })`. */
17
38
  readonly http?: ErrorStatuses;
39
+ /**
40
+ * Schema whose catalog still has the stored name key.
41
+ *
42
+ * Columns in the error select the object. The stored name key is the
43
+ * reason. A composite key that is only the SQL column list becomes the
44
+ * TypeScript fields. With no columns, the stored name key is the reason.
45
+ * An index keeps its full name.
46
+ */
47
+ readonly schema?: MapPostgresErrorSchema;
18
48
  };
19
49
  /**
20
50
  * Maps a thrown driver error into an {@link OkmError}.
@@ -28,3 +58,4 @@ export type MapPostgresErrorOptions = {
28
58
  * @returns A categorised error
29
59
  */
30
60
  export declare function mapPostgresError(error: unknown, options?: MapPostgresErrorOptions): OkmError;
61
+ export {};
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  extension2
3
- } from "../../../shared/ycatkxmq.js";
3
+ } from "../../../shared/jgqj11w4.js";
4
4
 
5
5
  // src/dialects/pg/ext/citext.ts
6
6
  function citext(options = {}) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  extension2
3
- } from "../../../shared/ycatkxmq.js";
3
+ } from "../../../shared/jgqj11w4.js";
4
4
  export {
5
5
  extension2 as extension
6
6
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  extension2
3
- } from "../../../shared/ycatkxmq.js";
3
+ } from "../../../shared/jgqj11w4.js";
4
4
  import {
5
5
  tag
6
6
  } from "../../../shared/cn0n68ft.js";
@@ -3,17 +3,24 @@
3
3
  *
4
4
  * Loaded the first time `parse()` is called. `filters()` validates the spec
5
5
  * before this module exists.
6
+ *
7
+ * A request key is equality when the value is a scalar and `eq` is allowed.
8
+ * An object value names operators `parse` applies. `sort` is a field name
9
+ * from the sort list. A relation key becomes `has` over the allowlisted fields.
10
+ * A column key wins when the same name is in `allow` and `relations`.
6
11
  */
7
12
  import type { ParsedFilters } from "./table.js";
8
13
  /**
9
14
  * Reads one request object.
10
15
  *
11
- * A plain value is equality when `eq` is allowlisted. `sort` is a field name
12
- * from the sort list. JSON cannot add an operator.
16
+ * Lookups use own properties, so `constructor`, `__proto__`, `toString`, and
17
+ * `hasOwnProperty` are ordinary keys. A key that is not in `allow` is OKM1123.
18
+ * `sort` is the exception.
13
19
  *
14
20
  * @param allow - Fields and the operators each one accepts
15
21
  * @param sort - Fields a caller may sort by
16
22
  * @param query - One request object
23
+ * @param relations - Relation name to fields, when `filters` passed any
17
24
  * @returns Filters a caller can spread into `find`
18
25
  */
19
- export declare function parseQuery(allow: Readonly<Record<string, readonly string[]>>, sort: readonly string[], query: unknown): ParsedFilters;
26
+ export declare function parseQuery(allow: Readonly<Record<string, readonly string[]>>, sort: readonly string[], query: unknown, relations?: Readonly<Record<string, readonly string[]>>): ParsedFilters;