okmodel 0.5.1 → 0.7.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 (233) hide show
  1. package/README.md +100 -1116
  2. package/dist/adapters/capabilities.d.ts +3 -0
  3. package/dist/adapters/failure.d.ts +13 -2
  4. package/dist/adapters/lazy-errors.d.ts +22 -0
  5. package/dist/adapters/pg/bunsql-extra.d.ts +54 -3
  6. package/dist/adapters/pg/bunsql.d.ts +14 -1
  7. package/dist/adapters/pg/cursor.d.ts +17 -0
  8. package/dist/adapters/pg/no-inflight-timeout.d.ts +20 -0
  9. package/dist/adapters/pg/nodepostgres-extra.d.ts +22 -1
  10. package/dist/adapters/pg/nodepostgres.d.ts +12 -0
  11. package/dist/adapters/pg/pglite-wire.d.ts +24 -0
  12. package/dist/adapters/pg/pglite.d.ts +10 -0
  13. package/dist/adapters/pg/postgres-extra.d.ts +41 -3
  14. package/dist/adapters/pg/reserved.d.ts +42 -0
  15. package/dist/adapters/pg/session.d.ts +0 -18
  16. package/dist/contracts/catalog/document.d.ts +19 -5
  17. package/dist/contracts/catalog/object.d.ts +2 -1
  18. package/dist/contracts/catalog/policy.d.ts +48 -0
  19. package/dist/contracts/catalog/types.d.ts +25 -1
  20. package/dist/contracts/catalog/view.d.ts +2 -0
  21. package/dist/contracts/connection.d.ts +7 -6
  22. package/dist/contracts/internal.js +1 -1
  23. package/dist/contracts/location.d.ts +10 -0
  24. package/dist/dialects/pg/column.d.ts +13 -2
  25. package/dist/dialects/pg/ddl.d.ts +37 -1
  26. package/dist/dialects/pg/emit.d.ts +19 -1
  27. package/dist/dialects/pg/errors.d.ts +32 -1
  28. package/dist/dialects/pg/ext/citext.js +1 -1
  29. package/dist/dialects/pg/ext/index.js +1 -1
  30. package/dist/dialects/pg/ext/pg-trgm.js +1 -1
  31. package/dist/dialects/pg/filter-parse.d.ts +5 -4
  32. package/dist/dialects/pg/floor.d.ts +68 -0
  33. package/dist/dialects/pg/fn/index.js +2 -2
  34. package/dist/dialects/pg/index.d.ts +8 -2
  35. package/dist/dialects/pg/index.js +67 -171
  36. package/dist/dialects/pg/introspect.d.ts +7 -0
  37. package/dist/dialects/pg/keys.d.ts +11 -8
  38. package/dist/dialects/pg/model.d.ts +47 -2
  39. package/dist/dialects/pg/morph.d.ts +43 -0
  40. package/dist/dialects/pg/operator-fit.d.ts +2 -2
  41. package/dist/dialects/pg/operators.d.ts +14 -7
  42. package/dist/dialects/pg/ops/bucket.d.ts +65 -0
  43. package/dist/dialects/pg/ops/inc.d.ts +2 -2
  44. package/dist/dialects/pg/ops/matches.d.ts +9 -12
  45. package/dist/dialects/pg/ops/relevance.d.ts +13 -0
  46. package/dist/dialects/pg/ops/search-query.d.ts +22 -0
  47. package/dist/dialects/pg/ops/window.d.ts +103 -0
  48. package/dist/dialects/pg/owner.d.ts +42 -0
  49. package/dist/dialects/pg/path-endpoint.d.ts +20 -0
  50. package/dist/dialects/pg/preset.d.ts +1 -1
  51. package/dist/dialects/pg/relations.d.ts +100 -3
  52. package/dist/dialects/pg/role/check.d.ts +3 -0
  53. package/dist/dialects/pg/role/index.d.ts +71 -6
  54. package/dist/dialects/pg/role/sql.d.ts +11 -0
  55. package/dist/dialects/pg/schema.d.ts +33 -3
  56. package/dist/dialects/pg/table.d.ts +25 -3
  57. package/dist/dialects/pg/tenancy.d.ts +45 -3
  58. package/dist/dialects/pg/view/client.d.ts +19 -1
  59. package/dist/dialects/pg/view/index.d.ts +4 -0
  60. package/dist/dialects/pg/view/index.js +21 -178
  61. package/dist/dialects/pg/view/refresh.d.ts +24 -0
  62. package/dist/runtime/aggregate.d.ts +8 -2
  63. package/dist/runtime/batch.d.ts +1 -1
  64. package/dist/runtime/bucket-group.d.ts +70 -0
  65. package/dist/runtime/conflict.d.ts +24 -6
  66. package/dist/runtime/drift.d.ts +9 -0
  67. package/dist/runtime/include.d.ts +0 -8
  68. package/dist/runtime/inflight.d.ts +14 -0
  69. package/dist/runtime/page.d.ts +4 -1
  70. package/dist/runtime/pg/bun.js +12 -5
  71. package/dist/runtime/pg/pg.js +5 -5
  72. package/dist/runtime/pg/pglite.d.ts +1 -0
  73. package/dist/runtime/pg/pglite.js +9 -8
  74. package/dist/runtime/pg/postgresjs.js +5 -5
  75. package/dist/runtime/plan.d.ts +113 -15
  76. package/dist/runtime/predicate.d.ts +19 -0
  77. package/dist/runtime/read-stream.d.ts +13 -16
  78. package/dist/runtime/result-name.d.ts +17 -0
  79. package/dist/runtime/safety/index.js +1 -1
  80. package/dist/runtime/server-check.d.ts +3 -3
  81. package/dist/runtime/sort-bind.d.ts +21 -0
  82. package/dist/runtime/sort.d.ts +56 -0
  83. package/dist/runtime/sync.d.ts +70 -0
  84. package/dist/runtime/tenancy/composite.d.ts +26 -0
  85. package/dist/runtime/tenancy/context.d.ts +16 -0
  86. package/dist/runtime/tenancy/index.d.ts +9 -5
  87. package/dist/runtime/tenancy/index.js +1511 -38
  88. package/dist/runtime/tenancy/path.d.ts +87 -0
  89. package/dist/runtime/tenancy/rls.d.ts +67 -0
  90. package/dist/runtime/tenancy/via.d.ts +22 -0
  91. package/dist/runtime/text-search.d.ts +20 -0
  92. package/dist/runtime/traits/index.d.ts +80 -0
  93. package/dist/runtime/traits/index.js +187 -17
  94. package/dist/runtime/tree.d.ts +38 -0
  95. package/dist/runtime/types.d.ts +231 -23
  96. package/dist/runtime/validate/index.js +4 -4
  97. package/dist/runtime/version.d.ts +39 -0
  98. package/dist/runtime/window.d.ts +21 -0
  99. package/dist/runtime/write.d.ts +55 -3
  100. package/dist/shared/{krxx77g0.js → 14vx95q4.js} +12 -18
  101. package/dist/shared/1est4ezg.js +313 -0
  102. package/dist/shared/{4az561fn.js → 1qa0qajz.js} +8 -23
  103. package/dist/shared/1w508jbr.js +1611 -0
  104. package/dist/shared/2c13q96h.js +18 -0
  105. package/dist/shared/2d3xq4ps.js +9 -0
  106. package/dist/shared/2shdbn8q.js +23 -0
  107. package/dist/shared/{t4tbsy9h.js → 358zsv4r.js} +1 -1
  108. package/dist/shared/3v7t1yrv.js +208 -0
  109. package/dist/shared/5e3tf63j.js +220 -0
  110. package/dist/shared/5fe3kbsh.js +10 -0
  111. package/dist/shared/5v3xf80k.js +37 -0
  112. package/dist/shared/{51mvgj8w.js → 63qtkrca.js} +130 -3
  113. package/dist/shared/{gbtcc2d7.js → 67txcrcw.js} +62 -66
  114. package/dist/shared/79dt02vc.js +12 -0
  115. package/dist/shared/{v36c5g6p.js → 7k9grvf6.js} +407 -14
  116. package/dist/shared/7kms6fvj.js +20 -0
  117. package/dist/shared/7mnme5tn.js +8 -0
  118. package/dist/shared/7n7ddwt4.js +6 -0
  119. package/dist/shared/{87jvvhnt.js → 81ebdx9p.js} +48 -68
  120. package/dist/shared/{szm0cksa.js → 8skf0cam.js} +34 -6
  121. package/dist/shared/{gjthds9j.js → 9628a9xt.js} +1 -1
  122. package/dist/shared/9a4ae4p8.js +31 -0
  123. package/dist/shared/{8xsbagad.js → 9s0ypp87.js} +88 -7
  124. package/dist/shared/9y3mmmh1.js +436 -0
  125. package/dist/shared/ab9gmbwy.js +213 -0
  126. package/dist/shared/{sycnx8gt.js → ax7qv55k.js} +18 -8
  127. package/dist/shared/{f5cw5q5r.js → aytqh5y2.js} +6 -4
  128. package/dist/shared/{fancm5pt.js → b5pftnn2.js} +138 -15
  129. package/dist/shared/{efmy2gcg.js → b76b8d95.js} +2 -2
  130. package/dist/shared/bdk4sty6.js +58 -0
  131. package/dist/shared/{32zqc1dw.js → bg3rym3g.js} +63 -32
  132. package/dist/shared/{0t22wwh1.js → bm22fvzr.js} +16 -14
  133. package/dist/shared/bz84tkzm.js +342 -0
  134. package/dist/shared/{31vm914h.js → cm3w0ywp.js} +15 -16
  135. package/dist/shared/cn957f8d.js +179 -0
  136. package/dist/shared/{pf0d8f77.js → cnkky55j.js} +25 -14
  137. package/dist/shared/{5fqx9yh4.js → dk8v79p3.js} +3 -1
  138. package/dist/shared/{nkaxvt9q.js → dkxgzaqd.js} +224 -19
  139. package/dist/shared/dzj09cye.js +10 -0
  140. package/dist/shared/{4m9fn52z.js → ed0b8k4s.js} +1 -1
  141. package/dist/shared/{z2rnngj4.js → ek2wt2dz.js} +8 -5
  142. package/dist/shared/exn3sd90.js +359 -0
  143. package/dist/shared/g6evj1j3.js +66 -0
  144. package/dist/shared/ga9j92qj.js +25 -0
  145. package/dist/shared/{qv2ydck9.js → gs74dbj5.js} +651 -76
  146. package/dist/shared/gzaveazc.js +285 -0
  147. package/dist/shared/{8rsy3hxz.js → h15jx9bc.js} +1 -1
  148. package/dist/shared/{dyj86cmj.js → hyh73qk6.js} +3 -3
  149. package/dist/shared/je4axnm8.js +22 -0
  150. package/dist/shared/{ycatkxmq.js → jgqj11w4.js} +1 -1
  151. package/dist/shared/{6qd27mx0.js → jvd0mqag.js} +5 -5
  152. package/dist/shared/jznfax0y.js +155 -0
  153. package/dist/shared/{cgbamx3e.js → kb2sdhfb.js} +7 -4
  154. package/dist/shared/{ps7a6h14.js → kr0jzbw2.js} +12 -8
  155. package/dist/shared/{janx09m5.js → ktz42yez.js} +20 -8
  156. package/dist/shared/{gde73es6.js → m6p12kbr.js} +56 -55
  157. package/dist/shared/{r6nwkxnt.js → m7n9h2a5.js} +4 -4
  158. package/dist/shared/{672t5vym.js → mss6ctgv.js} +2 -2
  159. package/dist/shared/{nxdp2ze3.js → mtaszv5a.js} +144 -12
  160. package/dist/shared/n816peh1.js +45 -0
  161. package/dist/shared/nbw1z4yd.js +18 -0
  162. package/dist/shared/{cctw17hj.js → p6qc5gcp.js} +38 -34
  163. package/dist/shared/{6rk2gen5.js → pbp1ss85.js} +20 -17
  164. package/dist/shared/pt94kz7f.js +6 -0
  165. package/dist/shared/{wq9n6pwr.js → pz6rj8ay.js} +82 -9
  166. package/dist/shared/q5c6ngn3.js +13 -0
  167. package/dist/shared/{8bwbwy8h.js → qq3899ht.js} +128 -18
  168. package/dist/shared/{8ktwd3rh.js → r3j8s5y0.js} +1 -1
  169. package/dist/shared/rhs3xe4k.js +7 -0
  170. package/dist/shared/{ntneex2a.js → rpycarcr.js} +6 -6
  171. package/dist/shared/{sdc7r2ww.js → rs73fca9.js} +8 -8
  172. package/dist/shared/{tm5ab8we.js → sfz1ksn6.js} +7 -4
  173. package/dist/shared/{6mnp99hx.js → svncdeh5.js} +16 -12
  174. package/dist/shared/{hf22e0wt.js → tbrvgw6z.js} +62 -42
  175. package/dist/shared/te5as7z7.js +622 -0
  176. package/dist/shared/{ga3sfy93.js → tsvww22q.js} +535 -200
  177. package/dist/shared/{47x8x9ze.js → ttsqgp4p.js} +20 -13
  178. package/dist/shared/{x72jb7ad.js → v8g1vgv8.js} +1 -1
  179. package/dist/shared/vb6qfyve.js +152 -0
  180. package/dist/shared/{r2sp8hrc.js → vczs8gd4.js} +22 -13
  181. package/dist/shared/{1g0drakv.js → vqrjpcqn.js} +145 -3
  182. package/dist/shared/{2pqx26h8.js → w72kgg4q.js} +31 -22
  183. package/dist/shared/{96qcj21c.js → w79hsckn.js} +16 -51
  184. package/dist/shared/wa07eas9.js +730 -0
  185. package/dist/shared/{k8v5v5wj.js → x933k9a3.js} +75 -34
  186. package/dist/shared/{cr74e96e.js → xhfsfkgk.js} +62 -28
  187. package/dist/shared/xjk7hnn5.js +272 -0
  188. package/dist/shared/{5enne4ma.js → xnxsed62.js} +66 -113
  189. package/dist/shared/xqve9598.js +53 -0
  190. package/dist/shared/{9x25fxhc.js → y34a8714.js} +1 -1
  191. package/dist/shared/{k7dwkhvq.js → y6jdds7g.js} +4 -1
  192. package/dist/shared/y6njgyda.js +249 -0
  193. package/dist/shared/{c9z5g0fm.js → ywr8hmey.js} +5 -2
  194. package/dist/shared/{g7ez901n.js → ywspezzx.js} +1 -1
  195. package/dist/shared/z7s6fpxv.js +62 -0
  196. package/dist/shared/{n7mcc1gk.js → zchaghkf.js} +4 -3
  197. package/dist/shared/{9m80mkac.js → ze4hxsmy.js} +1 -1
  198. package/dist/shared/zm0ptry0.js +40 -0
  199. package/dist/shared/{nsva2a6b.js → znm0bpp5.js} +3 -0
  200. package/dist/shared/zp9fmend.js +145 -0
  201. package/dist/shared/{y005b761.js → zy8r7qvz.js} +6 -2
  202. package/dist/tooling/cli.js +41 -15
  203. package/dist/tooling/migrate/apply.d.ts +14 -0
  204. package/dist/tooling/migrate/check.d.ts +3 -4
  205. package/dist/tooling/migrate/classify.d.ts +4 -0
  206. package/dist/tooling/migrate/config.d.ts +3 -2
  207. package/dist/tooling/migrate/engine.d.ts +3 -2
  208. package/dist/tooling/migrate/files.d.ts +11 -1
  209. package/dist/tooling/migrate/index.js +14 -14
  210. package/dist/tooling/migrate/lint.d.ts +1 -1
  211. package/dist/tooling/migrate/lost.d.ts +28 -0
  212. package/dist/tooling/migrate/paths.d.ts +14 -0
  213. package/dist/tooling/migrate/plan.d.ts +6 -3
  214. package/dist/tooling/migrate/runner.d.ts +25 -0
  215. package/dist/tooling/migrate/sandbox.d.ts +57 -0
  216. package/dist/tooling/migrate/sql-lex.d.ts +20 -3
  217. package/dist/tooling/migrate/text.d.ts +42 -0
  218. package/dist/tooling/node-version.d.ts +21 -0
  219. package/dist/tooling/testing/facts.d.ts +33 -5
  220. package/dist/tooling/testing/index.d.ts +1 -1
  221. package/dist/tooling/testing/index.js +3 -3
  222. package/dist/tooling/testing/isolation.d.ts +11 -5
  223. package/package.json +8 -3
  224. package/dist/shared/7m88x3e9.js +0 -45
  225. package/dist/shared/7zwwyh0v.js +0 -448
  226. package/dist/shared/aj2qxyhj.js +0 -90
  227. package/dist/shared/caw8b9d8.js +0 -204
  228. package/dist/shared/jwtxdj06.js +0 -76
  229. package/dist/shared/kfjp0gck.js +0 -212
  230. package/dist/shared/q58y17hy.js +0 -21
  231. package/dist/shared/qm7cdtvr.js +0 -912
  232. package/dist/shared/r8x5qhd7.js +0 -86
  233. package/dist/shared/zqrnhwsq.js +0 -31
package/README.md CHANGED
@@ -1,172 +1,93 @@
1
- # OKModel
2
-
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
-
5
- Version 0.5.0. Apache-2.0.
6
-
7
- ## Contents
8
-
9
- - [Install](#install)
10
- - [Quickstart](#quickstart)
11
- - [Configure](#configure)
12
- - [Schema](#schema)
13
- - [Push](#push)
14
- - [Reviewed migrations](#reviewed-migrations)
15
- - [One client](#one-client)
16
- - [Write and read](#write-and-read)
17
- - [Core](#core)
18
- - [Keys](#keys)
19
- - [Identity](#identity)
20
- - [Number](#number)
21
- - [Uuid](#uuid)
22
- - [Generated in the app](#generated-in-the-app)
23
- - [Slug](#slug)
24
- - [Two columns](#two-columns)
25
- - [Insert](#insert)
26
- - [On conflict](#on-conflict)
27
- - [Find](#find)
28
- - [One](#one)
29
- - [Count](#count)
30
- - [Exists](#exists)
31
- - [Update](#update)
32
- - [Delete](#delete)
33
- - [Errors](#errors)
34
- - [What is in the box](#what-is-in-the-box)
35
- - [Columns](#columns)
36
- - [Hidden](#hidden)
37
- - [Guarded](#guarded)
38
- - [Sensitive](#sensitive)
39
- - [Relations](#relations)
40
- - [Operators](#operators)
41
- - [Page and aggregate](#page-and-aggregate)
42
- - [Request filters](#request-filters)
43
- - [Presets](#presets)
44
- - [Validation](#validation)
45
- - [Transactions](#transactions)
46
- - [Batch and locks](#batch-and-locks)
47
- - [Traits](#traits)
48
- - [Archive](#archive)
49
- - [Tenancy](#tenancy)
50
- - [Extensions](#extensions)
51
- - [Domains](#domains)
52
- - [Functions](#functions)
53
- - [Triggers](#triggers)
54
- - [Views](#views)
55
- - [Roles and grants](#roles-and-grants)
56
- - [okm ext and okm doctor](#okm-ext-and-okm-doctor)
57
- - [Linter](#linter)
58
- - [Reference data](#reference-data)
59
- - [Backfill](#backfill)
60
- - [Testing](#testing)
61
- - [Topology](#topology)
62
- - [Reference app](#reference-app)
63
- - [Startup](#startup)
64
- - [Protected targets](#protected-targets)
65
- - [Commands](#commands)
66
- - [Check a history in CI](#check-a-history-in-ci)
67
- - [Roadmap](#roadmap)
68
- - [Size](#size)
69
- - [Docs](#docs)
1
+ <p align="center">
2
+ <picture>
3
+ <source
4
+ media="(prefers-color-scheme: dark)"
5
+ srcset="https://raw.githubusercontent.com/omqkhafi/okmodel/main/site/public/okm-white.svg"
6
+ />
7
+ <img
8
+ alt="OKModel"
9
+ src="https://raw.githubusercontent.com/omqkhafi/okmodel/main/site/public/okm.svg"
10
+ width="240"
11
+ />
12
+ </picture>
13
+ </p>
14
+
15
+ <p align="center">
16
+ <em>okmodel is a catalog-first TypeScript ORM, PostgreSQL first. The schema is the single source. Migrations and queries come from it.</em>
17
+ </p>
18
+
19
+ <p align="center">
20
+ <a href="https://www.npmjs.com/package/okmodel"><img alt="npm" src="https://img.shields.io/npm/v/okmodel?style=flat-square" /></a>
21
+ <a href="https://www.apache.org/licenses/LICENSE-2.0"><img alt="Apache-2.0" src="https://img.shields.io/npm/l/okmodel?style=flat-square" /></a>
22
+ <a href="https://www.postgresql.org"><img alt="PostgreSQL 15 to 18" src="https://img.shields.io/badge/PostgreSQL-15%E2%80%9318-336791?style=flat-square&logo=postgresql&logoColor=white" /></a>
23
+ <a href="https://github.com/omqkhafi/okmodel/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/omqkhafi/okmodel/ci.yml?branch=main&label=CI&style=flat-square" /></a>
24
+ <a href="https://github.com/omqkhafi/okmodel/commits"><img alt="last commit" src="https://img.shields.io/github/last-commit/omqkhafi/okmodel?style=flat-square" /></a>
25
+ </p>
26
+
27
+ <p align="center">
28
+ <a href="https://okm.omqkhafi.dev"><strong>Docs</strong></a> ·
29
+ <a href="https://okm.omqkhafi.dev/docs"><strong>Handbook</strong></a> ·
30
+ <a href="https://okm.omqkhafi.dev/llms.txt"><code>llms.txt</code></a> ·
31
+ <a href="https://www.npmjs.com/package/okmodel"><code>okmodel</code></a>
32
+ </p>
33
+
34
+ CI runs PostgreSQL 15 to 18. Other SQL databases are the direction. Apache-2.0.
70
35
 
71
36
  ## Install
72
37
 
73
- Four Postgres drivers. Pick the one the runtime already has. postgres.js is the default, on Node and Bun. node-postgres is the other choice on Node and Bun when `pg` is already installed. Bun.sql is built into Bun and runs only there. PGlite is in-process, on Node and Bun.
38
+ `okmodel` has no runtime dependencies. Each driver is an optional peer. Pick the one the runtime already has.
74
39
 
75
40
  ```sh
76
41
  bun add okmodel postgres
77
- bun add okmodel pg
78
- bun add okmodel @electric-sql/pglite
79
42
  ```
80
43
 
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
-
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.
44
+ The other drivers:
84
45
 
85
- ## Quickstart
46
+ ```sh
47
+ bun add okmodel pg
48
+ bun add -D @types/pg
49
+ bun add okmodel @electric-sql/pglite
50
+ bun add okmodel
51
+ ```
86
52
 
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.
53
+ postgres.js is the default on Node and Bun. node-postgres is for a project that already has `pg`. PGlite runs Postgres in the process. The last line is Bun.sql, built into Bun, and it runs only there.
88
54
 
89
- ### Configure
55
+ ## Quick start
90
56
 
91
- Point the CLI at the schema and at that URL.
57
+ Define the table, compile the schema, point the config at it, push, then connect. The same steps are on [Let's start](https://okm.omqkhafi.dev/docs/start).
92
58
 
93
- `okmodel.config.ts`:
59
+ `schema.ts`:
94
60
 
95
61
  ```ts
96
- import { defineConfig } from "okmodel/migrate";
97
-
98
- const url = process.env.DATABASE_URL;
99
- if (url === undefined || url.length === 0) {
100
- throw new Error("DATABASE_URL is not set");
101
- }
62
+ import { schema, table, t } from "okmodel/pg";
102
63
 
103
- // schema is the file. database is DATABASE_URL.
104
- export default defineConfig({
105
- schema: "./schema.ts",
106
- database: url,
64
+ export const notes = table("notes", {
65
+ id: t.id(),
66
+ title: t.text(),
107
67
  });
108
- ```
109
68
 
110
- ### Schema
111
-
112
- `authors` has a unique name. `notes` stores a title and points at an author. `t.identity()` is the primary key. An insert omits the id, and the row that comes back carries it. Identity ids come back as strings by default. `t.identity({ as: "number" })` returns numbers. Push and reviewed migrations are two ways to do the same job: pick one per database.
69
+ export default schema({ tables: [notes] });
70
+ ```
113
71
 
114
- Tested on PostgreSQL 15 to 18.
72
+ `t.id()` stores `gen_random_uuid()`, which works on Postgres 15 to 18. For a large write-heavy table, `defaults: { id: "uuidv7" }` keeps the ids time-ordered and the index local. That choice needs Postgres 18 and `minPostgres: 18`. Omit `minPostgres` unless you need a newer major. `requires: { postgres: ">=18" }` is the same floor and is deprecated.
115
73
 
116
- `schema.ts`:
74
+ `okmodel.config.ts`:
117
75
 
118
76
  ```ts
119
- import { one, schema, table, t } from "okmodel/pg";
77
+ import { defineConfig } from "okmodel/migrate";
120
78
 
121
- const authors = table("authors", {
122
- id: t.identity(),
123
- name: t.text().unique(),
79
+ export default defineConfig({
80
+ schema: "./schema.ts",
81
+ database: process.env.DATABASE_URL!,
124
82
  });
125
-
126
- const notes = table(
127
- "notes",
128
- {
129
- id: t.identity(),
130
- title: t.text(),
131
- // Points at the authors primary key.
132
- authorId: t.bigint().references("authors"),
133
- },
134
- {
135
- // find({ include: { author: true } }) loads this.
136
- relations: { author: one("authors") },
137
- },
138
- );
139
-
140
- const tables = [authors, notes];
141
-
142
- export default schema({ tables });
143
83
  ```
144
84
 
145
- ### Push
146
-
147
- Create the tables with push, which is allowed because this target is not protected.
85
+ `okm push` connects and creates the tables. It does not write a SQL file. A protected target refuses it (OKM1850).
148
86
 
149
87
  ```sh
150
88
  bunx okm push
151
89
  ```
152
90
 
153
- A protected target refuses push. Production settings are in the [production checklist](https://github.com/omqkhafi/okmodel/blob/main/docs/production.md).
154
-
155
- ### Reviewed migrations
156
-
157
- Generate writes a SQL file and `.okm`. On an empty database, apply installs that schema and any reference rows. A database that already has history runs the new file, so you can read it first.
158
-
159
- ```sh
160
- bunx okm generate init
161
- bunx okm migrate apply
162
- ```
163
-
164
- Each step is `expand`, `contract`, or unclassified SQL. Expand is additive. Contract removes or tightens something. On an existing table the planner writes the safe form: a concurrent index, a check or foreign key as `NOT VALID` then `VALIDATE`, and `SET NOT NULL` through a validated check. A backfill step updates rows in batches and resumes after a failure. See [Backfill](#backfill).
165
-
166
- ### One client
167
-
168
- Import `db` from this file wherever the server handles a request. The process keeps one client.
169
-
170
91
  `db.ts`:
171
92
 
172
93
  ```ts
@@ -174,1006 +95,69 @@ import { connect } from "okmodel/pg/postgresjs";
174
95
 
175
96
  import schema from "./schema.ts";
176
97
 
177
- const url = process.env.DATABASE_URL;
178
- if (url === undefined || url.length === 0) throw new Error("DATABASE_URL is not set");
179
-
180
- // Import this from the server. The process keeps one client.
181
- export const db = connect(url, { schema });
98
+ export const db = connect(process.env.DATABASE_URL!, { schema });
182
99
  ```
183
100
 
184
- A server keeps this client for the life of the process. A script can use `await using`, which closes the pool at the end of the block. `db.close()` is that call on a line you choose. A pool opened with `ssl` still waits out the driver's 30 second idle timer.
185
-
186
- ### Write and read
101
+ ## Basic usage
187
102
 
188
- The script writes one author and one note, then reads the note with the author attached. `await using` closes the pool when the block ends.
103
+ Each call on `db`. [Basic Usage](https://okm.omqkhafi.dev/docs/start/basic-usage) has one example per call.
189
104
 
190
- `script.ts`:
191
-
192
- ```ts
193
- import { connect } from "okmodel/pg/postgresjs";
194
-
195
- import schema from "./schema.ts";
196
-
197
- const url = process.env.DATABASE_URL;
198
- if (url === undefined || url.length === 0) throw new Error("DATABASE_URL is not set");
199
-
200
- await using db = connect(url, { schema });
201
- const author = await db.authors.insert({ name: "Lin" });
202
- const note = await db.notes.insert({ title: "hello", authorId: author.id });
203
- // include loads the author named on the note.
204
- const found = await db.notes.find({
205
- where: { id: note.id },
206
- limit: 1,
207
- include: { author: true },
208
- });
209
- const row = found[0];
210
- if (author.name !== "Lin" || row?.title !== "hello" || row.author?.name !== "Lin") {
211
- throw new Error("insert did not return the row");
212
- }
213
- ```
214
-
215
- ## Core
216
-
217
- The calls below use the same schema and the same client. Read them in order. Each one uses the row the previous call wrote. The last one closes the client.
218
-
219
- ### Keys
220
-
221
- Each kind of key is its own table.
222
-
223
- #### Identity
224
-
225
- The quickstart uses this. The database assigns a bigint, the insert leaves the id out, and the row comes back with that id as a string. It works on every supported Postgres version.
226
-
227
- `identity.ts`:
228
-
229
- ```ts
230
- import { schema, table, t } from "okmodel/pg";
231
-
232
- // The insert leaves id out. The row comes back with it as a string.
233
- const authors = table("authors", { id: t.identity(), name: t.text() });
234
- export const app = schema({ tables: [authors] });
235
- ```
236
-
237
- #### Number
238
-
239
- `t.identity({ as: "number" })` is the same key, and the id comes back as a number.
240
-
241
- `number.ts`:
242
-
243
- ```ts
244
- import { schema, table, t } from "okmodel/pg";
245
-
246
- // Same identity key. The id comes back as a number.
247
- const counts = table("counts", { id: t.identity({ as: "number" }), name: t.text() });
248
- export const app = schema({ tables: [counts] });
249
- ```
250
-
251
- #### Uuid
252
-
253
- `t.id()` is a uuid primary key. The default is `uuidv7()`, which needs Postgres 18. On an older server, `okm migrate apply` stops with OKM1812 before it runs any statement. `t.id({ default: "uuidv4" })` uses `gen_random_uuid()`, which Postgres has included since 13. `t.id({ default: "none" })` means the insert supplies the id.
254
-
255
- #### Generated in the app
256
-
257
- Import `uuidv4`, `uuidv7`, or `okid` from `okmodel/ids` and pass it to `.default()`. Or set `defaults.id` on `schema()` for every id column that does not choose its own. A choice on the column wins. A generator is not a database default, so changing it does not create a migration, and an insert that bypasses okmodel has to supply the value.
258
-
259
- `generated.ts`:
260
-
261
- ```ts
262
- import { uuidv4 } from "okmodel/ids";
263
- import { schema, table, t } from "okmodel/pg";
264
-
265
- // The client fills id when the insert omits it. The database stores no default.
266
- const sessions = table("sessions", { id: t.text().primaryKey().default(uuidv4) });
267
- export const app = schema({ tables: [sessions] });
268
- ```
269
-
270
- #### Slug
271
-
272
- `.primaryKey()` on one column is a key you supply, such as a slug. The insert includes it, and an update cannot change it.
273
-
274
- `slug.ts`:
275
-
276
- ```ts
277
- import { schema, table, t } from "okmodel/pg";
278
-
279
- // The insert supplies slug. An update cannot change it.
280
- const slugs = table("slugs", { slug: t.text().primaryKey() });
281
- export const app = schema({ tables: [slugs] });
282
- ```
283
-
284
- #### Two columns
285
-
286
- A key of two columns is the `primaryKey` option on the table. The names are the columns, in order.
287
-
288
- `members.ts`:
289
-
290
- ```ts
291
- import { schema, table, t } from "okmodel/pg";
292
-
293
- // Both columns together are the key. The insert supplies both.
294
- const members = table(
295
- "members",
296
- { userId: t.integer(), orgId: t.integer() },
297
- { primaryKey: ["userId", "orgId"] },
298
- );
299
- export const app = schema({ tables: [members] });
300
- ```
301
-
302
- ### Insert
303
-
304
- Insert an author, then a note that points at the author's id.
105
+ `notes.ts`:
305
106
 
306
107
  ```ts
307
108
  import { db } from "./db.ts";
308
109
 
309
- const author = await db.authors.insert({ name: "Ada" });
310
- // authorId is the id the insert just returned.
311
- const note = await db.notes.insert({
312
- title: "hello",
313
- authorId: author.id,
314
- });
315
- if (author.name !== "Ada" || note.title !== "hello" || author.id.length === 0) {
316
- throw new Error("insert did not return the row");
110
+ // insert omits id. The row that comes back has it.
111
+ export async function createNote(title: string) {
112
+ return db.notes.insert({ title });
317
113
  }
318
- ```
319
-
320
- ### On conflict
321
114
 
322
- The name is unique, so inserting Ada again would fail. `onConflict: "ignore"` skips that second insert. The other form updates the named columns when the row is already there.
323
-
324
- ```ts
325
- // Ada is already there. ignore skips this insert.
326
- await db.authors.insert({ name: "Ada" }, { onConflict: "ignore" });
327
- ```
328
-
329
- ### Find
330
-
331
- `find` returns a list, so it needs a `limit` (OKM1101). `include` loads the author on each note. When the relation is itself a list, that include needs its own limit (OKM1105).
332
-
333
- ```ts
334
- // limit is required. include loads the author.
335
- const found = await db.notes.find({
336
- where: { id: note.id },
337
- limit: 5,
338
- include: { author: true },
339
- });
340
- const row = found[0];
341
- if (row?.title !== "hello" || row.author?.name !== "Ada") {
342
- throw new Error("find did not return the note and its author");
115
+ // find returns a list and needs a limit. [0] is undefined when nothing matched.
116
+ export async function readNote(id: string) {
117
+ const found = await db.notes.find({ where: { id }, limit: 1 });
118
+ return found[0];
343
119
  }
344
- ```
345
-
346
- ### One
347
120
 
348
- `one` returns the single note for that id. More than one match throws `not_unique`.
349
-
350
- ```ts
351
- // One row. More than one match throws.
352
- const single = await db.notes.one({ where: { id: note.id } });
353
- if (single.title !== "hello") throw new Error("one did not return the note");
354
- ```
355
-
356
- ### Count
357
-
358
- `count` returns how many notes match. It does not return the rows.
359
-
360
- ```ts
361
- // How many notes, not the rows themselves.
362
- if ((await db.notes.count()) < 1) throw new Error("count did not see the note");
363
- ```
364
-
365
- ### Exists
366
-
367
- `exists` returns true when a note matches. It does not return the row.
368
-
369
- ```ts
370
- // True or false. The row stays in the database.
371
- if (!(await db.notes.exists({ where: { id: note.id } }))) {
372
- throw new Error("exists did not see the note");
121
+ // one returns the row, or null. More than one match throws not_unique.
122
+ export async function readOne(id: string) {
123
+ return db.notes.one({ where: { id } });
373
124
  }
374
- ```
375
-
376
- ### Update
377
-
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.
379
-
380
- ```ts
381
- // where picks the row. set names the new title.
382
- await db.notes.update({ where: { id: note.id }, set: { title: "next" } });
383
- ```
384
-
385
- ### Delete
386
-
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.
388
-
389
- ```ts
390
- // where picks the row. Without it the call is refused.
391
- await db.notes.delete({ where: { id: note.id } });
392
- ```
393
-
394
- ### Errors
395
-
396
- Inserting Ada again fails because the name is unique. `safe` returns that `OkmError` instead of throwing. `kind` is `unique`.
397
-
398
- ```ts
399
- import { OkmError, safe } from "okmodel";
400
-
401
- // safe returns the unique error instead of throwing.
402
- const duplicate = await safe(db.authors.insert({ name: "Ada" }));
403
- if (duplicate.ok || !(duplicate.error instanceof OkmError) || duplicate.error.kind !== "unique") {
404
- throw new Error("expected an OkmError of kind unique");
405
- }
406
- await db.close();
407
- ```
408
-
409
- ## What is in the box
410
-
411
- Each section below is one feature you can add to the schema above.
412
-
413
- ### Columns
414
-
415
- A column can allow only a fixed list of strings. `.picklist()` adds that check to a string column. `t.enum()` creates a Postgres enum, and every column that uses the same name shares the list. A value outside the list is OKM1210. See [known limits](https://github.com/omqkhafi/okmodel/blob/main/docs/known-limits.md).
416
-
417
- `columns.ts`:
418
-
419
- ```ts
420
- import { schema, table, t } from "okmodel/pg";
421
-
422
- const tasks = table("tasks", {
423
- id: t.identity(),
424
- // Only these three strings. Anything else is rejected.
425
- status: t.varchar(20).picklist(["draft", "active", "done"]),
426
- // A Postgres enum. Other columns named color share this list.
427
- color: t.enum("color", ["red", "blue"]),
428
- });
429
- export const app = schema({ tables: [tasks] });
430
- ```
431
-
432
- ### Hidden
433
-
434
- 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.
435
-
436
- `hidden.ts`:
437
-
438
- ```ts
439
- import { schema, table, t } from "okmodel/pg";
440
-
441
- const users = table("users", {
442
- id: t.identity(),
443
- email: t.text(),
444
- // Left out of find and include. A named select still returns it.
445
- passwordHash: t.text().hidden(),
446
- });
447
- export const app = schema({ tables: [users] });
448
- ```
449
-
450
- ### Guarded
451
-
452
- A role should not be set by whatever the caller sent. `.guarded()` drops the column from insert and update. The call that is allowed to set it passes `{ allow: ["role"] }`. Any other call is OKM1190.
453
-
454
- `guarded.ts`:
455
125
 
456
- ```ts
457
- import { schema, table, t } from "okmodel/pg";
458
- import { connect } from "okmodel/pg/postgresjs";
459
-
460
- // role is dropped from insert and update unless the call names it in allow.
461
- const users = table("users", { id: t.identity(), email: t.text(), role: t.text().guarded() });
462
- const app = schema({ tables: [users] });
463
-
464
- export async function grant(url: string): Promise<void> {
465
- await using db = connect(url, { schema: app });
466
- await db.users.insert({ email: "a@b.c", role: "admin" }, { allow: ["role"] });
126
+ // where picks the row. A write with no where is OKM1102.
127
+ export async function renameNote(id: string, title: string) {
128
+ await db.notes.update({ where: { id }, set: { title } });
467
129
  }
468
- ```
469
-
470
- ### Sensitive
471
-
472
- A token can be stored and returned, and it still must not appear in a log or an error. `.sensitive()` shows `[redacted]` in logs, errors, and `inspect()`. The value in the database stays the same.
473
-
474
- `sensitive.ts`:
475
-
476
- ```ts
477
- import { schema, table, t } from "okmodel/pg";
478
-
479
- const sessions = table("sessions", {
480
- id: t.identity(),
481
- // Stored as written. Logs, errors, and inspect show [redacted].
482
- token: t.text().sensitive(),
483
- });
484
- export const app = schema({ tables: [sessions] });
485
- ```
486
-
487
- ### Relations
488
-
489
- The quickstart note points at one author. `one` is that author on the note. `many` is the list of notes on the author. `find({ include })` loads the related rows. `manyThrough` is the case with a third table in between, such as labels on a task through `taskLabels`.
490
-
491
- `relations.ts`:
492
-
493
- ```ts
494
- import { many, one, schema, table, t } from "okmodel/pg";
495
130
 
496
- // notes is the list of notes on this author.
497
- const authors = table(
498
- "authors",
499
- { id: t.identity() },
500
- { relations: { notes: many("notes", "authorId") } },
501
- );
502
- // author is the one author this note points at.
503
- const notes = table(
504
- "notes",
505
- { id: t.identity(), authorId: t.bigint().references("authors") },
506
- { relations: { author: one("authors", "authorId") } },
507
- );
508
- export const app = schema({ tables: [authors, notes] });
509
- ```
510
-
511
- ### Operators
512
-
513
- `where: { title: "hello" }` matches that exact title. A helper matches something else. `ilike` is a case-insensitive pattern, `contains` looks inside JSON, and `overlaps` looks for a shared array value. On update, `inc` adds to a number, `json.set` writes one JSON field, and `arr.append` adds one array value. The helper is a query parameter, so a JSON body cannot pretend to be one. `iStartsWith`, `iContains`, and `iEndsWith` are not in this version. See [known limits](https://github.com/omqkhafi/okmodel/blob/main/docs/known-limits.md).
514
-
515
- `operators.ts`:
516
-
517
- ```ts
518
- import { arr, contains, eq, ilike, inc, json, overlaps, schema, table, t } from "okmodel/pg";
519
-
520
- const docs = table("docs", {
521
- id: t.identity(),
522
- title: t.text(),
523
- n: t.integer(),
524
- tags: t.text().array(),
525
- meta: t.jsonb<{ readonly published: boolean }>(),
526
- });
527
- export const app = schema({ tables: [docs] });
528
- // Read filters. A plain value would match exactly.
529
- export const where = {
530
- title: ilike("%hip%"),
531
- n: eq(1),
532
- meta: contains({ published: true }),
533
- tags: overlaps(["news"]),
534
- };
535
- // Write helpers, passed inside update.
536
- export const set = { n: inc(1), meta: json.set(["published"], true), tags: arr.append("news") };
537
- ```
538
-
539
- ### Page and aggregate
540
-
541
- `page` returns one page of rows and a `next` cursor for the page after it. Pass that cursor back as `after`, with the same `orderBy`. A cursor from a different `orderBy` is OKM1130. `aggregate` counts or groups rows instead of returning them. A group needs a `limit`, or `.all(reason)`. There is no `having`.
542
-
543
- `page.ts`:
544
-
545
- ```ts
546
- import { schema, table, t } from "okmodel/pg";
547
- import { connect } from "okmodel/pg/postgresjs";
548
-
549
- const notes = table("notes", { id: t.identity(), title: t.text() });
550
- const app = schema({ tables: [notes] });
551
-
552
- export async function report(url: string): Promise<void> {
553
- await using db = connect(url, { schema: app });
554
- // next is the cursor for the following page. Pass it back as after.
555
- const page = await db.notes.page({ orderBy: { title: "asc" }, limit: 20 });
556
- // One row per title, with a count. groupBy needs a limit.
557
- await db.notes.aggregate({ count: true, groupBy: ["title"], limit: 10 });
558
- void page.next;
131
+ export async function removeNote(id: string) {
132
+ await db.notes.delete({ where: { id } });
559
133
  }
560
134
  ```
561
135
 
562
- ### Request filters
563
-
564
- A request should not be able to filter on every column. `filters` names the columns and the comparisons you accept, and the columns a caller may sort by. `parse` turns that request into `where` and `orderBy`. Naming a hidden column there is OKM1123.
565
-
566
- `filters.ts`:
567
-
568
- ```ts
569
- import { schema, table, t } from "okmodel/pg";
570
-
571
- const users = table("users", { id: t.identity(), email: t.text() });
572
- export const app = schema({ tables: [users] });
573
- // A request may filter email with eq, and may sort by email. Nothing else.
574
- export const userFilters = users.filters({ allow: { email: ["eq"] }, sort: ["email"] });
575
- ```
576
-
577
- ### Presets
578
-
579
- A filter you use often can live on the table under a name. Here `pending` means the status is `"pending"`. `db.tasks.pending().find({ limit: 20 })` applies it. It is added on top of the tenant and the archive filter, and it cannot remove either.
580
-
581
- `presets.ts`:
582
-
583
- ```ts
584
- import { schema, table, t } from "okmodel/pg";
585
-
586
- const tasks = table(
587
- "tasks",
588
- { id: t.identity(), title: t.text(), status: t.text() },
589
- {
590
- // db.tasks.pending().find({ limit: 20 }) adds this filter.
591
- presets: { pending: (q) => q.where({ status: "pending" }) },
592
- },
593
- );
594
- export const app = schema({ tables: [tasks] });
595
- ```
596
-
597
- ### Validation
598
-
599
- A title can be checked before it is written. `.validate()` stores the rules on the column. They run on insert and update when the schema sets `validation: true` and the file imports `okmodel/validate`. A failed check is OKM1200. `onRead` is stored and is not applied. See [known limits](https://github.com/omqkhafi/okmodel/blob/main/docs/known-limits.md).
600
-
601
- `validation.ts`:
602
-
603
- ```ts
604
- import { schema, table, t } from "okmodel/pg";
605
- import { v } from "okmodel/validate";
606
-
607
- const tasks = table("tasks", {
608
- id: t.identity(),
609
- // Trim, then require one character. A failure is OKM1200.
610
- title: t.varchar(8).validate([v.trim(), v.min(1, "title_required")]),
611
- });
612
- export const app = schema({ tables: [tasks], validation: true });
613
- ```
614
-
615
- ### Transactions
616
-
617
- Two writes that must succeed or fail together go in `tx()`. The callback gets its own client, and every call on it shares the transaction. A `tx()` inside that callback is a savepoint. `retry` runs the callback again after a serialization failure or a deadlock.
618
-
619
- `transactions.ts`:
620
-
621
- ```ts
622
- import { schema, table, t } from "okmodel/pg";
623
- import { connect } from "okmodel/pg/postgresjs";
624
-
625
- const notes = table("notes", { id: t.identity(), title: t.text() });
626
- const app = schema({ tables: [notes] });
627
-
628
- export async function save(url: string, title: string): Promise<void> {
629
- await using db = connect(url, { schema: app });
630
- // Both calls share one transaction. tx is the client inside it.
631
- await db.tx(async (tx) => {
632
- await tx.notes.insert({ title });
633
- });
634
- }
635
- ```
636
-
637
- ### Batch and locks
638
-
639
- `batch` sends several writes as one unit. It will not run `restore`, and it will not run a write that uses `expect` (OKM1121). Inside `tx()`, `lock: "update"` makes other transactions wait to change that row, and `advisoryLock` holds a named lock until the transaction ends. A row lock outside `tx()` is OKM1830. See [known limits](https://github.com/omqkhafi/okmodel/blob/main/docs/known-limits.md).
640
-
641
- `batch.ts`:
642
-
643
- ```ts
644
- import { schema, table, t } from "okmodel/pg";
645
- import { connect } from "okmodel/pg/postgresjs";
646
-
647
- const notes = table("notes", { id: t.identity(), title: t.text() });
648
- const app = schema({ tables: [notes] });
649
-
650
- export async function run(url: string, id: string): Promise<void> {
651
- await using db = connect(url, { schema: app });
652
- // One unit. The delete runs with the update.
653
- await db.batch([
654
- db.notes.update({ where: { id }, set: { title: "next" } }),
655
- db.notes.delete({ where: { title: "old" } }),
656
- ]);
657
- await db.tx(async (tx) => {
658
- // lock waits inside the transaction only.
659
- await tx.notes.find({ where: { id }, limit: 1, lock: "update", wait: "skip" });
660
- await tx.advisoryLock("notes");
661
- });
662
- }
663
- ```
664
-
665
- ### Traits
666
-
667
- `timestamps()` adds `createdAt` and `updatedAt` for you. Insert fills both with the current time. Update changes `updatedAt` and leaves `createdAt`. `{ enforce: "trigger" }` does that in the database as well, so a write that skips the client still updates the time.
668
-
669
- `traits.ts`:
670
-
671
- ```ts
672
- import { schema, table, t } from "okmodel/pg";
673
- import { timestamps } from "okmodel/traits";
674
-
675
- // Adds createdAt and updatedAt. The insert does not set them.
676
- const notes = table("notes", { id: t.identity(), title: t.text() }, { traits: [timestamps()] });
677
- export const app = schema({ tables: [notes] });
678
- ```
679
-
680
- ### Archive
681
-
682
- `archivable()` keeps the row and hides it from ordinary reads. `archive()` marks the matching rows and returns an `archiveId`. `restore({ archiveId })` brings that set back. `withArchived()` includes hidden rows. `onlyArchived()` reads only those rows. Put `cascade` on the parent and name the child tables that should archive with it. One level of children. See [known limits](https://github.com/omqkhafi/okmodel/blob/main/docs/known-limits.md).
683
-
684
- `archive.ts`:
685
-
686
- ```ts
687
- import { schema, table, t } from "okmodel/pg";
688
- import { connect } from "okmodel/pg/postgresjs";
689
- import { archivable } from "okmodel/traits";
690
-
691
- // cascade is on the parent. tasks archive and restore with the list.
692
- const lists = table(
693
- "lists",
694
- { id: t.identity() },
695
- { traits: [archivable({ cascade: ["tasks"] })] },
696
- );
697
- const tasks = table(
698
- "tasks",
699
- { id: t.identity(), listId: t.bigint().references("lists") },
700
- { traits: [archivable()] },
701
- );
702
- const app = schema({ tables: [lists, tasks] });
703
-
704
- export async function hide(url: string, id: string): Promise<void> {
705
- await using db = connect(url, { schema: app });
706
- // archive hides the row and returns the id restore uses.
707
- const archived = await db.lists.archive({ where: { id } });
708
- await db.lists.onlyArchived().restore({ archiveId: archived.archiveId });
709
- }
710
- ```
711
-
712
- ### Tenancy
713
-
714
- 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).
715
-
716
- `tenancy.ts`:
717
-
718
- ```ts
719
- import { schema, table, t } from "okmodel/pg";
720
- import { columnTenancy, global } from "okmodel/tenancy";
721
-
722
- // Every read and write adds tenantId.
723
- const notes = table("notes", { id: t.identity(), title: t.text() });
724
- // Shared by every tenant. The reason is recorded on the table.
725
- const countries = table("countries", { id: t.identity() }, { tenancy: global("shared") });
726
- export const app = schema({
727
- tenancy: columnTenancy({ key: "tenantId", type: "uuid" }),
728
- tables: [notes, countries],
729
- });
730
- ```
731
-
732
- ### Extensions
733
-
734
- `citext` is case-insensitive text. `pg_trgm` searches by similar chunks of text. Declare the extension on the schema and the migration creates it before the tables that use it. `gin` stores the trigram index. `okm ext check` compares the declaration with the server. A missing extension is OKM1811, and that check runs before any statement.
735
-
736
- `extensions.ts`:
737
-
738
- ```ts
739
- import { schema, table, t } from "okmodel/pg";
740
- import { citext as citextExtension } from "okmodel/pg/citext";
741
- import { pgTrgm } from "okmodel/pg/pg_trgm";
742
-
743
- const trigram = pgTrgm();
744
- const people = table(
745
- "people",
746
- {
747
- email: t.citext(),
748
- title: t.text(),
749
- },
750
- // gin index for similar-text search on title.
751
- { indexes: (column) => [trigram.gin(column.title.name)] },
752
- );
753
- export const app = schema({ extensions: [citextExtension(), trigram], tables: [people] });
754
- ```
755
-
756
- ### Domains
757
-
758
- A domain is a Postgres type with its own check. Here `pos` is an integer that must be greater than zero. TypeScript still sees a number. Changing the base type later is OKM1020. Write the check the way Postgres prints it, including the parentheses. A different spelling is a different check. See [known limits](https://github.com/omqkhafi/okmodel/blob/main/docs/known-limits.md).
759
-
760
- `domains.ts`:
761
-
762
- ```ts
763
- import { schema, table, t } from "okmodel/pg";
764
-
765
- export const app = schema({
766
- // pos is an integer that must be greater than zero.
767
- tables: [table("people", { n: t.domain("pos", t.integer(), "((VALUE > 0))") })],
768
- });
769
- ```
770
-
771
- ### Functions
772
-
773
- `fn()` is a function that lives in the database. This one takes a title and returns text. A plpgsql function names the tables it reads in `dependsOn`, or the declaration is OKM1824. `security: "definer"` also needs a `searchPath`, or it is OKM1823. A query cannot call it as `fn.slugify(col)` yet. See [known limits](https://github.com/omqkhafi/okmodel/blob/main/docs/known-limits.md).
774
-
775
- `functions.ts`:
776
-
777
- ```ts
778
- import { fn } from "okmodel/fn";
779
- import { schema, table, t } from "okmodel/pg";
780
-
781
- const tasks = table("tasks", { id: t.text().primaryKey(), title: t.text() });
782
- export const app = schema({
783
- tables: [tasks],
784
- functions: [
785
- // plpgsql must name the tables it uses.
786
- fn("slug", {
787
- arguments: [{ name: "title", type: "text" }],
788
- returns: "text",
789
- language: "plpgsql",
790
- body: "begin return title; end",
791
- dependsOn: [tasks],
792
- }),
793
- ],
794
- });
795
- ```
796
-
797
- ### Triggers
798
-
799
- A trigger runs a function when a row changes. This one runs `touch` before each update of `tasks` and returns the new row. The function has to be listed on the schema. `timestamps({ enforce: "trigger" })` is the built-in form of this for `updatedAt`. See [known limits](https://github.com/omqkhafi/okmodel/blob/main/docs/known-limits.md).
800
-
801
- `triggers.ts`:
802
-
803
- ```ts
804
- import { fn, trigger } from "okmodel/fn";
805
- import { schema, table, t } from "okmodel/pg";
806
-
807
- const tasks = table("tasks", { id: t.text().primaryKey(), title: t.text() });
808
- const touch = fn("touch", {
809
- returns: "trigger",
810
- language: "plpgsql",
811
- volatility: "volatile",
812
- body: "begin return new; end",
813
- dependsOn: [tasks],
814
- });
815
- export const app = schema({
816
- tables: [tasks],
817
- functions: [touch],
818
- triggers: [
819
- // Runs touch before each row update and returns the new row.
820
- trigger("tasks_touch", {
821
- on: tasks,
822
- timing: "before",
823
- events: ["update"],
824
- level: "row",
825
- calls: touch,
826
- }),
827
- ],
828
- });
829
- ```
830
-
831
- ### Views
832
-
833
- 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).
834
-
835
- `views.ts`:
836
-
837
- ```ts
838
- import { schema, table, t } from "okmodel/pg";
839
- import { materializedView, view } from "okmodel/view";
840
-
841
- const tasks = table("tasks", { id: t.text().primaryKey(), title: t.text() });
842
- const columns = [
843
- { name: "id", type: "text" },
844
- { name: "title", type: "text" },
845
- ];
846
- export const app = schema({
847
- tables: [tasks],
848
- views: [
849
- // A saved query. find reads it. The handle has no insert.
850
- view("active", {
851
- columns,
852
- query: "SELECT id,\n title\n FROM tasks\n WHERE title IS NOT NULL",
853
- }),
854
- // Concurrent refresh needs the unique index.
855
- materializedView("sums", {
856
- columns,
857
- query: "SELECT id,\n title\n FROM tasks",
858
- refresh: "concurrently",
859
- indexes: [{ columns: ["id"], unique: true }],
860
- }),
861
- ],
862
- });
863
- ```
864
-
865
- ### Roles and grants
866
-
867
- 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).
868
-
869
- `roles.ts`:
870
-
871
- ```ts
872
- import { defineConfig } from "okmodel/migrate";
873
-
874
- const url = process.env.DATABASE_URL;
875
- if (url === undefined || url.length === 0) throw new Error("DATABASE_URL is not set");
876
-
877
- export default defineConfig({
878
- schema: "./schema.ts",
879
- database: url,
880
- // Apply uses the migration role. The app uses the app role.
881
- roles: { migration: "okm_migrate", app: "okm_app", managed: [{ name: "okm_migrate" }] },
882
- });
883
- ```
884
-
885
- ### okm ext and okm doctor
886
-
887
- `okm ext list` prints the extensions this server can install, and the version that is already installed. `okm ext check` compares that list with the schema. `okm doctor` lists the triggers on each table. `okm doctor OKM1811` prints what that error code means. `okm ext test` and `okm ext scaffold` are not in this version. See [known limits](https://github.com/omqkhafi/okmodel/blob/main/docs/known-limits.md).
888
-
889
- ```text
890
- okm ext list
891
- okm ext check
892
- okm doctor
893
- okm doctor OKM1811
894
- ```
895
-
896
- ### Linter
897
-
898
- A migration plan is checked before it runs. Dropping a table, renaming a column, or adding a unique constraint on a table that already exists is an error. On an existing table the planner writes the safe form: a concurrent index, a check or foreign key as `NOT VALID` then `VALIDATE`, and `SET NOT NULL` through a validated check. The same statements written by hand, without that form, are errors. A type change that rewrites the table stays a warning. `okm generate` prints the finding and still writes the file. `okm migrate plan` and `okm check` exit non-zero on an error. `okm migrate apply` refuses with OKM1510 before any statement. The linter reads the plan and the catalogs. It does not connect. See [the linter](https://github.com/omqkhafi/okmodel/blob/main/docs/linter.md).
899
-
900
- ```text
901
- error OKM1511 step 1: drops a table -- fix: Stop reading the table in an expand migration, then drop it in a later contract. Or allow OKM1511 with a reason.
902
- ```
903
-
904
- ```sql
905
- -- okm-allow OKM1511: the table is empty and nothing reads it
906
- drop table "public"."notes";
907
- ```
908
-
909
- ### Reference data
910
-
911
- Rows the application needs are declared on the table. Apply on an empty database inserts a missing key. It does not update or delete. The rows are not part of the catalog hash, so a new key does not need a DDL migration.
912
-
913
- `reference.ts`:
914
-
915
- ```ts
916
- import { schema, table, t } from "okmodel/pg";
917
-
918
- const roles = table(
919
- "roles",
920
- { code: t.text().primaryKey(), label: t.text() },
921
- {
922
- reference: {
923
- key: "code",
924
- rows: [{ code: "admin", label: "Admin" }],
925
- },
926
- },
927
- );
928
-
929
- export default schema({ tables: [roles] });
930
- ```
931
-
932
- ### Backfill
933
-
934
- A backfill is one idempotent `UPDATE` in the migration file. `$1` is the exclusive lower bound and `$2` is the inclusive upper bound. Null opens that side. Apply commits one batch at a time and resumes from `okm_backfill`. There is no `okm backfill` command. A protected target refuses the step unless that invocation passes `--allow-protected`.
935
-
936
- ```sql
937
- -- name: fill
938
- -- class: expand
939
-
940
- -- class: expand
941
- -- action: backfill
942
- -- lock: ROW EXCLUSIVE
943
- -- transactional: false
944
- -- backfill table="public"."notes" key="id" batch=10
945
- update "public"."notes" set "title" = 'noted' where "title" = 'hello' and ("id" > $1 or $1 is null) and ("id" <= $2 or $2 is null);
946
- ```
947
-
948
- ### Testing
949
-
950
- `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. Each call uses a new pair of tenant ids, deletes what it inserted, and names a factory that is missing. `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`.
951
-
952
- `factory.ts`:
953
-
954
- ```ts
955
- import { schema, table, t } from "okmodel/pg";
956
- import { open } from "okmodel/pg/pglite";
957
- import { testing } from "okmodel/testing";
958
-
959
- const notes = table("notes", { id: t.identity(), title: t.text() });
960
- const app = schema({ tables: [notes] });
961
- const harness = await testing(app, { driver: open() });
962
- const factories = harness.factories({
963
- notes: (x) => ({ title: x.words(2) }),
964
- });
965
- await harness.expectQueries(1, () => factories.notes.create());
966
- const rows = await harness.db.notes.find({ limit: 5 });
967
- if (rows.length !== 1) throw new Error("expected one note");
968
- await harness.close();
969
- ```
970
-
971
- `seed.ts`:
972
-
973
- ```ts
974
- export default async function seed(t: {
975
- factories(definitions: {
976
- authors(x: { words(count: number): string }): { name: string };
977
- notes(x: { words(count: number): string; ref(table: string): unknown }): {
978
- title: string;
979
- authorId: unknown;
980
- };
981
- }): {
982
- notes: { create(): Promise<unknown> };
983
- };
984
- }): Promise<void> {
985
- const factories = t.factories({
986
- authors: (x) => ({ name: x.words(2) }),
987
- notes: (x) => ({ title: x.words(2), authorId: x.ref("authors") }),
988
- });
989
- await factories.notes.create();
990
- }
991
- ```
992
-
993
- ```sh
994
- bunx okm seed seed.ts
995
- ```
996
-
997
- ### Topology
998
-
999
- `connect({ primary, replicas }, options)` opens one pool per endpoint. An automatic read chooses a replica that has replayed at least as far as this client's last committed write. `routing.consistency` is `"session"` by default. `"eventual"` does not keep that watermark. `routing.maxLag` is a duration (`"5s"`, `"500ms"`, `"2m"`) or a size (`"16MB"`, `"512KB"`, `"1GB"`, `"4096B"`). A bare number is rejected. `find({ route: "primary" })` reads the primary. `find({ route: "replica" })` requires a replica that satisfies the same watermark and lag, and it does not fall back to the primary. `db.using("primary")` or `db.using("replica")` returns a client that forces that route. That client has no `close` and no `using`. `onRoute` receives `{ op, endpoint, reason }`. A throw from it is ignored. A string or an existing pool is one endpoint and serves either `route` from it, and it has no `using`.
1000
-
1001
- The watermark is one value for the whole `connect()`. The root, `for()`, `unscoped()`, `using()`, and `reserve()` share it. A write through any of them moves it for the others. A watermark per `for()` client is not in this version.
1002
-
1003
- When replicas are configured and consistency is `"session"`, a committed write reads `pg_current_wal_insert_lsn()` before its promise resolves. That is one extra round trip: on the primary pool after an autocommit `execute` or `batch`, and on the reserved connection after a successful `COMMIT`. A rollback does not read it. `"eventual"`, and a connect with no replicas, do not read it either.
1004
-
1005
- `routing.select` picks among the replicas that passed health, the watermark, and `maxLag`. The default is `"weighted"`. `weighted` is smooth weighted round-robin. Equal weights take turns. A weight of `0` is rejected. `roundRobin` ignores weights and rotates through the eligible replicas. `leastConnections` uses the replica with the fewest statements already in flight on this client. A tie follows configuration order. `latencyAware` uses the replica with the lowest moving average of round-trip time. A function receives each candidate as `{ name, weight, inflight, latencyMs, lag }` and `{ op: "read" }`. `lag` is the number of bytes the replica is behind the primary, or `null` when that distance is unknown. The function returns one of those objects or its `name`. It runs only when an automatic read has two or more candidates.
1006
-
1007
- A write, a batch, a transaction, or a locking read on a replica route is OKM1840. `route: "replica"` or `using("replica")` with no eligible replica is OKM1843, including a topology with no replicas. An automatic read with `routing.fallback: "error"` and no eligible replica is OKM1844. A migrate target that carries `primary`, `replicas`, `weight`, or `pool` is OKM1845. Waiting for a connection past `timeouts.acquire` is OKM1846, and that wait stays on the endpoint's own pool. The rules are in [topology](https://github.com/omqkhafi/okmodel/blob/main/docs/topology.md).
1008
-
1009
- A replica whose pool is at its maximum, with no idle connection and callers waiting, is skipped. If every replica that can serve the read is in that state, the read uses the primary and `onRoute` reports `fallback:saturated`. When healthy replicas exist and none has caught up, or none is inside `maxLag`, the reason is `fallback:behind`. `route: "replica"` still uses a saturated replica, and it still requires the watermark and `maxLag`.
1010
-
1011
- `replicas.ts`:
1012
-
1013
- ```ts
1014
- import { connect } from "okmodel/pg/postgresjs";
1015
-
1016
- import schema from "./schema.ts";
1017
-
1018
- const url = process.env.DATABASE_URL;
1019
- if (url === undefined || url.length === 0) throw new Error("DATABASE_URL is not set");
1020
- const east = process.env.REPLICA_URL;
1021
- if (east === undefined || east.length === 0) throw new Error("REPLICA_URL is not set");
1022
- const west = process.env.REPLICA_URL_WEST ?? east;
1023
-
1024
- const endpoints = {
1025
- primary: url,
1026
- replicas: [{ url: east, weight: 2, name: "east" }, west],
1027
- };
1028
-
1029
- const db = await connect(endpoints, {
1030
- schema,
1031
- routing: { probe: "1s", fallback: "primary", consistency: "session", select: "weighted" },
1032
- onRoute(event) {
1033
- // { op: "read", endpoint: "east", reason: "auto:east" }
1034
- void event;
1035
- },
1036
- });
1037
-
1038
- await db.notes.find({ limit: 50 });
1039
- await db.notes.find({ limit: 50, route: "primary" });
1040
- await db.notes.find({ limit: 50, route: "replica" });
1041
- const replica = db.using("replica");
1042
- await replica.notes.find({ limit: 50 });
1043
- await db.close();
1044
-
1045
- const eventual = await connect(endpoints, {
1046
- schema,
1047
- routing: { consistency: "eventual" },
1048
- });
1049
- await eventual.notes.find({ limit: 50 });
1050
- await eventual.close();
1051
-
1052
- for (const select of ["roundRobin", "leastConnections", "latencyAware"] as const) {
1053
- const client = await connect(endpoints, { schema, routing: { select } });
1054
- await client.notes.find({ limit: 1 });
1055
- await client.close();
1056
- }
1057
-
1058
- const custom = await connect(endpoints, {
1059
- schema,
1060
- routing: {
1061
- select(candidates) {
1062
- return candidates[0] ?? "east";
1063
- },
1064
- },
1065
- });
1066
- await custom.notes.find({ limit: 1 });
1067
- await custom.close();
1068
- ```
1069
-
1070
- ### Reference app
1071
-
1072
- The worked example is a project tracker: workspaces as tenants, archive, a function, two views, three generated migrations, and this topology. It is a private package in the repository, not on npm. CI runs it on the primary and two standbys. Board reads go to a replica. A read just after a write, with replay paused, comes back from the primary. The report uses `route: "replica"`. See the [example app](https://github.com/omqkhafi/okmodel/blob/main/docs/example-app.md) and the [reference app notes](https://github.com/omqkhafi/okmodel/blob/main/packages/reference-app/README.md).
1073
-
1074
- ### Startup
1075
-
1076
- `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.
1077
-
1078
- ### Protected targets
1079
-
1080
- `protected: true` is a flag on the target, not on the name and not on `NODE_ENV`. Plan, status, check, ext check, and drift stay allowed. Expand and reference rows stay allowed. An empty protected database can be provisioned. Contract, unclassified SQL, push, backfill, and seed are OKM1850 unless that invocation passes `--allow-protected`. `okm migrate check` writes a scratch schema, so it stays refused, and the flag does not apply. Point it at a throwaway Postgres. Protection does not affect `connect()`.
1081
-
1082
- ## Commands
1083
-
1084
- | Command | What it does |
1085
- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1086
- | `okm build` | Validates the schema and writes `.okm/`. Does not connect. |
1087
- | `okm check` | Lints the schema and, after `okm_meta` exists, compares the database. Drift is OKM1520. Allowed on a protected target. |
1088
- | `okm generate [name]` | Writes a SQL migration and `.okm/`. Prints lint findings and still writes the file. Offline. |
1089
- | `okm dev` | Opens a PGlite database in `.okm/dev-db`, or a target named `dev`. It does not apply migrations. |
1090
- | `okm push` | Applies the schema directly. OKM1850 on a protected target unless `--allow-protected`. |
1091
- | `okm migrate plan <name>` | Prints each step's class and lock, plus a row estimate when the target answers. Read-only on a protected target. An error finding exits non-zero. |
1092
- | `okm migrate apply` | Installs the head snapshot on an empty database, or runs pending files. Expand is allowed on a protected target. Contract, unclassified SQL, and backfill need `--allow-protected`. |
1093
- | `okm migrate check` | Replays the history in a scratch schema and checks the previous catalog, the head, and the linter. Prints `ok N migrations`. Refused on a protected target, and `--allow-protected` does not apply. |
1094
- | `okm migrate status` | Prints version, catalog hash, and state. Read-only on a protected target. An unfinished backfill is listed under the table. |
1095
- | `okm seed <file>` | Runs the file's default export against the migrated target. OKM1850 on a protected target unless `--allow-protected`. |
1096
- | `okm ext list` | Prints extensions the server can install. Read-only on a protected target. |
1097
- | `okm ext check` | Compares installed extensions with the schema. OKM1811 if one is missing, OKM1812 if the pin is not met. Read-only on a protected target. `okm ext test` and `okm ext scaffold` are not in this version. |
1098
- | `okm doctor [code]` | Lists triggers, or explains one error code. A role check is read-only on a protected target. |
1099
- | `okm --version` | Prints the package version. |
1100
-
1101
- `okmodel` and `okm` are the same command.
1102
-
1103
- ## Check a history in CI
1104
-
1105
- `okm migrate check` is the command for a pipeline. It applies every migration into a scratch schema, plans each result back to that file's catalog, checks the previous catalog, requires the last catalog to match the schema, and lints the history. Success prints `ok N migrations`. A failure exits non-zero.
1106
-
1107
- Point it at a throwaway Postgres. A protected target is refused, and `--allow-protected` does not apply. `defineConfig({ lintFrom: "<migration id>" })` skips lint for files before that id, which is how a project adopts the linter without failing on old migrations. Apply still lints pending migrations only.
1108
-
1109
- The previous-catalog check is schema-level. It does not prove application behaviour. Tenant targets are not checked.
1110
-
1111
- ```yaml
1112
- name: migrate
1113
- on: [push, pull_request]
1114
- jobs:
1115
- check:
1116
- runs-on: ubuntu-latest
1117
- services:
1118
- postgres:
1119
- image: postgres:17
1120
- env:
1121
- POSTGRES_PASSWORD: postgres
1122
- ports:
1123
- - 5432:5432
1124
- options: >-
1125
- --health-cmd "pg_isready -U postgres"
1126
- --health-interval 10s
1127
- --health-timeout 5s
1128
- --health-retries 5
1129
- steps:
1130
- - uses: actions/checkout@v5
1131
- - uses: oven-sh/setup-bun@v2
1132
- - run: bun install --frozen-lockfile
1133
- - run: bunx okm migrate check
1134
- env:
1135
- DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres
1136
- ```
1137
-
1138
- The config reads `DATABASE_URL`, and that target is not protected.
1139
-
1140
- ## Roadmap
1141
-
1142
- Status is on the [board](https://github.com/users/omqkhafi/projects/1). Each release is a milestone.
1143
-
1144
- - [x] [0.1](https://github.com/omqkhafi/okmodel/milestone/1) — Schema, queries, and migrations on PostgreSQL, with postgres.js and PGlite.
1145
- - [x] [0.2](https://github.com/omqkhafi/okmodel/milestone/2) — Hidden and sensitive fields, validation, traits, tenancy, archive and restore, richer relations, presets, transactions, and operators for JSON, arrays, ranges, and search.
1146
- - [x] [0.3](https://github.com/omqkhafi/okmodel/milestone/3) — Extensions, domains, functions, triggers, views, roles, and grants.
1147
- - [x] [0.4](https://github.com/omqkhafi/okmodel/milestone/4) — Safer migration plans, backfill, drift checks, provisioning, reference data, and a testing package.
1148
- - [x] [0.5](https://github.com/omqkhafi/okmodel/milestone/5) — A primary with replicas, read routing, and a reference app.
1149
- - [ ] [0.5.1](https://github.com/omqkhafi/okmodel/milestone/7) — QA fixes for 0.5.0.
1150
-
1151
- `okmodel/internal` has no stability promise. Names on that subpath can change or disappear in any release.
1152
-
1153
- Each limit, with the version that lifts it, is in [known limits](https://github.com/omqkhafi/okmodel/blob/main/docs/known-limits.md).
1154
-
1155
- ## Size
136
+ `okm push` is for a database you can recreate. A shared database uses reviewed migrations: `okm generate init`, then `okm migrate apply`.
1156
137
 
1157
- Measured on this release.
138
+ ## Next
1158
139
 
1159
- | | Minified | Gzip | Cold import |
1160
- | -------------------------------------------- | -------: | -----: | ----------: |
1161
- | Runtime entry | 5,288 | 2,026 | 2.347 ms |
1162
- | App startup (10 tables, one find) | 89,751 | 29,801 | 15.061 ms |
1163
- | App startup, every 0.2 feature in use (full) | 120,132 | 39,162 | 15.956 ms |
140
+ | Want… | Go to |
141
+ | --------------------------- | ---------------------------------------------------------------------------------------- |
142
+ | One example per client call | [Basic usage](https://okm.omqkhafi.dev/docs/start/basic-usage) |
143
+ | Tables, keys, and relations | [Define](https://okm.omqkhafi.dev/docs/define) |
144
+ | Every feature in one file | [Guide](https://github.com/omqkhafi/okmodel/blob/main/docs/guide.md) |
145
+ | Run it in production | [Production checklist](https://github.com/omqkhafi/okmodel/blob/main/docs/production.md) |
146
+ | What is not built yet | [Known limits](https://github.com/omqkhafi/okmodel/blob/main/docs/known-limits.md) |
1164
147
 
1165
- 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.
148
+ ## Documentation
1166
149
 
1167
- The rest of the measurements are in [size](https://github.com/omqkhafi/okmodel/blob/main/docs/size.md).
150
+ | Resource | Link |
151
+ | ------------ | ---------------------------------------------------------------------------------- |
152
+ | Docs site | [okm.omqkhafi.dev](https://okm.omqkhafi.dev) |
153
+ | Get started | [/docs/start](https://okm.omqkhafi.dev/docs/start) |
154
+ | Handbook | [/docs](https://okm.omqkhafi.dev/docs) |
155
+ | Changelog | [/changelog](https://okm.omqkhafi.dev/changelog) |
156
+ | Agents index | [/llms.txt](https://okm.omqkhafi.dev/llms.txt) |
157
+ | Size | [`docs/size.md`](https://github.com/omqkhafi/okmodel/blob/main/docs/size.md) |
158
+ | Security | [`SECURITY.md`](https://github.com/omqkhafi/okmodel/blob/main/SECURITY.md) |
159
+ | Contributing | [`CONTRIBUTING.md`](https://github.com/omqkhafi/okmodel/blob/main/CONTRIBUTING.md) |
1168
160
 
1169
- ## Docs
161
+ ## License
1170
162
 
1171
- - [Quickstart](https://github.com/omqkhafi/okmodel/blob/main/docs/quickstart.md)
1172
- - [Production checklist](https://github.com/omqkhafi/okmodel/blob/main/docs/production.md)
1173
- - [Linter](https://github.com/omqkhafi/okmodel/blob/main/docs/linter.md)
1174
- - [Backfill](https://github.com/omqkhafi/okmodel/blob/main/docs/backfill.md)
1175
- - [Provisioning](https://github.com/omqkhafi/okmodel/blob/main/docs/provisioning.md)
1176
- - [Testing](https://github.com/omqkhafi/okmodel/blob/main/docs/testing.md)
1177
- - [Topology](https://github.com/omqkhafi/okmodel/blob/main/docs/topology.md)
1178
- - [Known limits](https://github.com/omqkhafi/okmodel/blob/main/docs/known-limits.md)
1179
- - [Changelog](https://github.com/omqkhafi/okmodel/blob/main/changelog.md)
163
+ Published on [npm](https://www.npmjs.com/package/okmodel). [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0).