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
@@ -23,6 +23,9 @@ export declare function assertRoleHealth(runner: Runner, roles: RolesInput, obje
23
23
  /**
24
24
  * Refuses to apply role changes when the acting role lacks `CREATEROLE`.
25
25
  *
26
+ * A superuser can create roles when `rolcreaterole` is false, so `rolsuper`
27
+ * satisfies the same check.
28
+ *
26
29
  * @param runner - Database connection
27
30
  * @param role - The role that will run `CREATE ROLE` or `ALTER ROLE`
28
31
  */
@@ -1,11 +1,12 @@
1
1
  /**
2
- * `roles: { migration, app }`.
2
+ * `roles: { migration, app, reporting? }`.
3
3
  *
4
4
  * The object contributes role, grant, and default-privilege records. `schema()`
5
5
  * does not import this module. Tooling calls {@link attachRoles} when the
6
6
  * config names roles.
7
7
  */
8
8
  import type { Catalog, CatalogObject } from "../../../contracts/catalog/types.js";
9
+ import { OkmError } from "../../../contracts/error.js";
9
10
  /** A role the plan creates and alters. It is never dropped. */
10
11
  export type ManagedRole = {
11
12
  readonly name: string;
@@ -14,37 +15,101 @@ export type ManagedRole = {
14
15
  /** `INHERIT` when not `false`. */
15
16
  readonly inherit?: boolean;
16
17
  };
18
+ /**
19
+ * One column grant.
20
+ *
21
+ * `on` is the table, view, or materialized view name. `select` and `update`
22
+ * name columns. A hidden column is included only when it is listed here.
23
+ */
24
+ export type ColumnGrantInput = {
25
+ /** Role that receives the privilege. */
26
+ readonly role: string;
27
+ /** Table, view, or materialized view. */
28
+ readonly on: string;
29
+ /** Columns granted `SELECT`. */
30
+ readonly select?: readonly string[];
31
+ /** Columns granted `UPDATE`. */
32
+ readonly update?: readonly string[];
33
+ };
34
+ /**
35
+ * Why a `roles` declaration was refused.
36
+ *
37
+ * `reporting_distinct` means the reporting role is the migration or
38
+ * application role. `reporting_write` means that role was granted a write.
39
+ */
40
+ export type RoleDeclarationReason = "reporting_distinct" | "reporting_write";
41
+ /**
42
+ * A `roles` declaration this version refuses.
43
+ *
44
+ * The kind stays `invalid`. No new OKM code: OKM1825 is a missing privilege
45
+ * at doctor time, and OKM1060 is a column definition.
46
+ */
47
+ export declare class RoleDeclarationError extends OkmError {
48
+ /** Which rule refused the declaration. */
49
+ readonly reason: RoleDeclarationReason;
50
+ /**
51
+ * @param reason - Which rule failed
52
+ * @param message - What the author should change
53
+ */
54
+ constructor(reason: RoleDeclarationReason, message: string);
55
+ }
56
+ /**
57
+ * Columns of one table, for the reporting role's hidden-column rule.
58
+ *
59
+ * `hidden` is true for `.hidden()` and for `hidden({ filterable: true })`.
60
+ */
61
+ export type TableExposure = {
62
+ /** SQL table name. */
63
+ readonly table: string;
64
+ /** Field name and SQL name. `hidden` is left out of the reporting role. */
65
+ readonly columns: readonly {
66
+ readonly field: string;
67
+ readonly sql: string;
68
+ readonly hidden: boolean;
69
+ }[];
70
+ };
17
71
  /**
18
72
  * Roles named by `defineConfig`.
19
73
  *
20
74
  * A string is external: it must already exist. A name listed in `managed` is
21
- * created when `pg_roles` does not have it.
75
+ * created when `pg_roles` does not have it. `reporting` receives select only.
22
76
  */
23
77
  export type RolesInput = {
24
78
  /** Role that runs migrations. */
25
79
  readonly migration: string;
26
80
  /** Role the application connects as. */
27
81
  readonly app: string;
82
+ /**
83
+ * Read-only role.
84
+ *
85
+ * Select on tables, views, and materialized views, usage on schemas, and
86
+ * default privileges for later tables. No write and no execute.
87
+ */
88
+ readonly reporting?: string;
28
89
  /** Roles this catalog creates and alters. */
29
90
  readonly managed?: readonly ManagedRole[];
91
+ /** Column `SELECT` or `UPDATE` grants. */
92
+ readonly columns?: readonly ColumnGrantInput[];
30
93
  };
31
94
  /**
32
95
  * Adds role, grant, and default-privilege objects to a catalog.
33
96
  *
34
97
  * Grants and default privileges are for {@link RolesInput.app} on every
35
- * managed table, view, materialized view, sequence, and function. Fine-grained
36
- * grants are not accepted here.
98
+ * managed table, view, materialized view, sequence, and function. Column
99
+ * grants and the reporting role are added when `input` names them.
37
100
  *
38
101
  * @param source - Catalog from `schema()`
39
102
  * @param input - Migration role, application role, and managed roles
103
+ * @param exposure - Hidden columns. Omitted when no table hides one
40
104
  * @returns A catalog that includes those objects
41
105
  */
42
- export declare function attachRoles(source: Catalog, input: RolesInput): Catalog;
106
+ export declare function attachRoles(source: Catalog, input: RolesInput, exposure?: readonly TableExposure[]): Catalog;
43
107
  /**
44
108
  * Catalog records for one `roles` configuration.
45
109
  *
46
110
  * @param input - Migration role, application role, and managed roles
47
111
  * @param built - Objects already in the catalog
112
+ * @param exposure - Hidden columns. Omitted when no table hides one
48
113
  * @returns Role, grant, and default-privilege records
49
114
  */
50
- export declare function contributeRoles(input: RolesInput, built: readonly CatalogObject[]): readonly CatalogObject[];
115
+ export declare function contributeRoles(input: RolesInput, built: readonly CatalogObject[], exposure?: readonly TableExposure[]): readonly CatalogObject[];
@@ -19,6 +19,17 @@ export type PrivilegeStatement = {
19
19
  readonly sql: string;
20
20
  readonly kind: "revoke" | "revoke-default" | "create-role" | "alter-role" | "grant-default" | "grant";
21
21
  };
22
+ /**
23
+ * Privilege name for a column grant.
24
+ *
25
+ * Columns are sorted so the identity is stable. The catalog stores this
26
+ * string. It does not grow the grant type.
27
+ *
28
+ * @param name - `SELECT` or `UPDATE`
29
+ * @param columns - SQL column names
30
+ * @returns `SELECT(email,name)`
31
+ */
32
+ export declare function columnPrivilege(name: string, columns: readonly string[]): string;
22
33
  /**
23
34
  * Diffs role, grant, and default-privilege objects into SQL.
24
35
  *
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import type { Catalog } from "../../contracts/catalog/types.js";
7
7
  import { type ClientGenerator } from "../../contracts/generator.js";
8
+ import { type PostgresMajor } from "./floor.js";
8
9
  import { type SchemaHook, type TableModel } from "./model.js";
9
10
  import type { FieldsOfList, HasArchive, PresetsOfList, Trait } from "./trait.js";
10
11
  import { type ColumnTenancy, type TenantFields } from "./tenancy.js";
@@ -15,8 +16,16 @@ export type SchemaCodecs = {
15
16
  readonly numeric: "string" | "number";
16
17
  readonly timestamps: "temporal";
17
18
  };
18
- /** Engine requirement recorded for connect. */
19
+ /**
20
+ * Engine requirement recorded for connect.
21
+ *
22
+ * The stored value is `>=` and a major. `minPostgres` and the deprecated
23
+ * string form normalise to the same object.
24
+ */
19
25
  export type SchemaRequires = {
26
+ /**
27
+ * @deprecated Use `minPostgres`. `">=17"` is the same floor as `minPostgres: 17`.
28
+ */
20
29
  readonly postgres?: string;
21
30
  };
22
31
  /**
@@ -28,6 +37,15 @@ export type SchemaRequires = {
28
37
  */
29
38
  export type SchemaInput<TTables extends readonly AnyTable[]> = {
30
39
  readonly tables: TTables;
40
+ /**
41
+ * Oldest Postgres major this schema runs on.
42
+ *
43
+ * Omitted means 15. A feature that needs a newer major is refused here.
44
+ */
45
+ readonly minPostgres?: PostgresMajor;
46
+ /**
47
+ * @deprecated Use {@link SchemaInput.minPostgres}. `">=17"` is the same floor as `minPostgres: 17`.
48
+ */
31
49
  readonly requires?: SchemaRequires;
32
50
  readonly casing?: "snake";
33
51
  readonly codecs?: {
@@ -39,8 +57,13 @@ export type SchemaInput<TTables extends readonly AnyTable[]> = {
39
57
  /**
40
58
  * What a bare `t.id()` means.
41
59
  *
42
- * `"uuidv7"` and `"uuidv4"` are database defaults. A client generator is
43
- * filled on insert and stays out of the catalog. A column option wins.
60
+ * Omitted, the database default is `gen_random_uuid()`. `"uuidv4"` stores
61
+ * the same function. `"uuidv7"` stores `uuidv7()` and needs Postgres 18
62
+ * (`minPostgres: 18`).
63
+ * Pick `uuidv7` for a large write-heavy table: the ids are time-ordered
64
+ * and the index stays local. Random ids are fine elsewhere. A client
65
+ * generator is filled on insert and stays out of the catalog. A column
66
+ * option wins.
44
67
  */
45
68
  readonly defaults?: {
46
69
  readonly id?: "uuidv7" | "uuidv4" | ClientGenerator<string>;
@@ -80,6 +103,11 @@ export type BuiltSchema<TTables extends readonly AnyTable[]> = {
80
103
  readonly types: "emitted" | "inferred";
81
104
  readonly casing: "snake" | undefined;
82
105
  readonly codecs: SchemaCodecs;
106
+ /**
107
+ * Declared floor, normalised to `>=` and a major.
108
+ *
109
+ * Absent when the schema names no floor. Connect then uses 15.
110
+ */
83
111
  readonly requires: SchemaRequires | undefined;
84
112
  readonly tables: TTables;
85
113
  /**
@@ -114,6 +142,8 @@ export type TenancySchema<S, TTables extends readonly AnyTable[], TTenancy> = TT
114
142
  readonly "~byName": {
115
143
  readonly [T in TTables[number] as T["~name"]]: T extends {
116
144
  readonly "~global": string;
145
+ } ? T : T extends {
146
+ readonly "~path": string;
117
147
  } ? T : T & {
118
148
  readonly "~row": RowFrom<TenantFields<TTenancy["key"]>>;
119
149
  readonly "~insert": InsertFrom<TenantFields<TTenancy["key"]>>;
@@ -154,7 +154,8 @@ export type Table<TName extends string, TColumns, TRelations extends Readonly<Re
154
154
  * Allowlisted client filters.
155
155
  *
156
156
  * A hidden field in `allow`, `sort`, or `relations` throws OKM1123 here.
157
- * `parse` loads the parser on first use.
157
+ * `parse` loads the parser on first use. An operator outside
158
+ * {@link FilterOperator} is not assignable.
158
159
  *
159
160
  * @param spec - Fields, operators, and sorts the caller may send
160
161
  * @returns A parser for one request object
@@ -163,10 +164,23 @@ export type Table<TName extends string, TColumns, TRelations extends Readonly<Re
163
164
  parse(query: unknown): Promise<ParsedFilters>;
164
165
  };
165
166
  };
167
+ /**
168
+ * Operators `filters().parse` applies.
169
+ *
170
+ * `path`, `matches`, `similar`, `wordSimilar`, boolean trees, relation
171
+ * quantifiers, and write operators are not in this list. A caller cannot
172
+ * name them in `allow`.
173
+ */
174
+ export type FilterOperator = "eq" | "in" | "inList" | "notIn" | "lt" | "lte" | "gt" | "gte" | "between" | "startsWith" | "endsWith" | "contains" | "like" | "ilike" | "not" | "containedBy" | "overlaps" | "hasKey" | "hasAnyKey";
166
175
  /** What {@link Table.filters} accepts. */
167
176
  export type FilterSpec = {
168
- readonly allow?: Readonly<Record<string, readonly string[]>>;
177
+ readonly allow?: Readonly<Record<string, readonly FilterOperator[]>>;
169
178
  readonly sort?: readonly string[];
179
+ /**
180
+ * Relation name to the fields a caller may filter.
181
+ *
182
+ * `parse` turns `{ list: { name: { startsWith: "a" } } }` into `has`.
183
+ */
170
184
  readonly relations?: Readonly<Record<string, readonly string[]>>;
171
185
  };
172
186
  /** `where` and `orderBy` a caller can spread into `find`. */
@@ -266,7 +280,7 @@ type PrimaryOf<TOptions> = TOptions extends {
266
280
  * @typeParam TColumns - Column builders
267
281
  * @typeParam TOptions - Options argument
268
282
  */
269
- type DeclaredTable<TName extends string, TColumns, TOptions> = Table<TName, TColumns, RelationsOf<TOptions>, PrimaryOf<TOptions>, NoInfer<TOptions>> & TraitShapes<TColumns, TOptions> & ArchiveFlag<TOptions> & PresetFlag<TOptions> & OmitFlag<TOptions> & GlobalFlag<TOptions>;
283
+ type DeclaredTable<TName extends string, TColumns, TOptions> = Table<TName, TColumns, RelationsOf<TOptions>, PrimaryOf<TOptions>, NoInfer<TOptions>> & TraitShapes<TColumns, TOptions> & ArchiveFlag<TOptions> & PresetFlag<TOptions> & OmitFlag<TOptions> & GlobalFlag<TOptions> & PathFlag<TOptions>;
270
284
  type ArchiveFlag<TOptions> = TOptions extends {
271
285
  readonly traits: infer TTraits;
272
286
  } ? HasArchive<TTraits> : unknown;
@@ -291,6 +305,14 @@ type GlobalFlag<TOptions> = TOptions extends {
291
305
  } ? {
292
306
  readonly "~global": TReason;
293
307
  } : unknown;
308
+ /** A path table keeps its own columns. The schema type does not add the tenant key. */
309
+ type PathFlag<TOptions> = TOptions extends {
310
+ readonly tenancy: {
311
+ readonly via: infer TVia extends string;
312
+ };
313
+ } ? {
314
+ readonly "~path": TVia;
315
+ } : unknown;
294
316
  type TraitShapes<TColumns, TOptions> = TOptions extends {
295
317
  readonly traits: infer TTraits;
296
318
  } ? TTraits extends readonly {
@@ -16,9 +16,15 @@ import type { AnyTable } from "./table.js";
16
16
  export type TenantFields<Key extends string> = {
17
17
  readonly [K in Key]: ColumnBuilder<string, FlagTrue<PlainFlags, "guarded">>;
18
18
  };
19
- /** One tenant value, or a reason for leaving the scope. The value is not a cache key. */
19
+ /**
20
+ * One tenant value, or a reason for leaving the scope. The value is not a cache key.
21
+ *
22
+ * `values` is set when the tenant is several keys. `value` stays the first key,
23
+ * which is what a single-key scope already stored.
24
+ */
20
25
  export type TenantCall = {
21
26
  readonly value: string;
27
+ readonly values?: Readonly<Record<string, string>>;
22
28
  } | {
23
29
  readonly unscoped: string;
24
30
  };
@@ -49,10 +55,45 @@ export type TenancyRule = {
49
55
  */
50
56
  export type ColumnTenancy = {
51
57
  readonly key: string;
58
+ /** Every tenant field, in declaration order. One entry for column tenancy. */
59
+ readonly keys?: readonly string[];
52
60
  readonly type: "uuid";
53
- readonly strategy: "column";
61
+ readonly strategy: string;
54
62
  /** Tables with the tenant column, widened uniques, and composite references. */
55
63
  rewrite(tables: readonly AnyTable[]): readonly AnyTable[];
64
+ /** Path table: isolation follows a relation instead of a tenant column. */
65
+ isPath?(table: string): boolean;
66
+ /**
67
+ * Steps from a path table to the tenant table.
68
+ *
69
+ * Absent when `table` has its own tenant column.
70
+ */
71
+ pathOf?(table: string): readonly {
72
+ readonly child: string;
73
+ readonly parent: string;
74
+ readonly localField: string;
75
+ readonly remoteField: string;
76
+ }[] | undefined;
77
+ /**
78
+ * Writes a locking `EXISTS` for an insert into a path table.
79
+ *
80
+ * @returns `false` when `table` is not a path table
81
+ */
82
+ lockPredicate?(input: {
83
+ readonly table: string;
84
+ readonly fieldSql: (field: string) => string | undefined;
85
+ readonly encode: ((value: unknown) => string) | undefined;
86
+ readonly alias: string;
87
+ readonly appended: boolean;
88
+ readonly scope: TenantCall | undefined;
89
+ readonly sink: TenancyText;
90
+ }): boolean;
91
+ /**
92
+ * Checks a path update's new parent in the same statement.
93
+ *
94
+ * A column-tenant table ignores it.
95
+ */
96
+ noteParent?(table: string, set: Readonly<Record<string, unknown>>, sink: TenancyText, scope: TenantCall | undefined, fieldSql: (field: string) => string | undefined): void;
56
97
  /** The guarded uuid column, for emitted row types. */
57
98
  column(): object;
58
99
  predicate(input: {
@@ -95,7 +136,8 @@ export type ColumnTenancy = {
95
136
  /**
96
137
  * Reads `schema({ tenancy })`.
97
138
  *
98
- * Omitted stays `undefined`. Anything else must be `columnTenancy()`.
139
+ * Omitted stays `undefined`. Anything else must carry `rewrite`. The strategy
140
+ * name stays on the object from `okmodel/tenancy`. This file does not list them.
99
141
  *
100
142
  * @param value - The option the caller passed
101
143
  * @returns The tenancy object, or `undefined`
@@ -7,6 +7,23 @@
7
7
  import type { ColumnTenancy } from "../tenancy.js";
8
8
  import type { SchemaHook, TableModel } from "../model.js";
9
9
  import { type AnyTable } from "../table.js";
10
+ /**
11
+ * Reason from `global("reason")` on a view, when the view opted out.
12
+ *
13
+ * The reason is not a catalog field, so the catalog hash does not change.
14
+ *
15
+ * @param model - The view's query model
16
+ * @returns The reason, or `undefined` when the view is not global
17
+ */
18
+ export declare function viewGlobalReason(model: object): string | undefined;
19
+ /**
20
+ * Whether `query` names `name` as an identifier.
21
+ *
22
+ * @param query - Author SQL
23
+ * @param name - Table name, SQL or client
24
+ * @returns Whether the text mentions that name
25
+ */
26
+ export declare function viewMentions(query: string, name: string): boolean;
10
27
  /** What `schema()` passes when it binds a view. */
11
28
  export type ViewInstall = (model: {
12
29
  [name: string]: TableModel;
@@ -18,12 +35,13 @@ export type ViewInstall = (model: {
18
35
  * @param columns - Declared output columns
19
36
  * @param query - Author SQL
20
37
  * @param tenancy - `global("reason")`, when the view opts out
38
+ * @param refresh - `plain` stores no `refresh`. A materialized view is `blocking` or `concurrently`
21
39
  * @returns Methods stored on the declaration
22
40
  */
23
41
  export declare function viewClient(name: string, columns: readonly {
24
42
  readonly name: string;
25
43
  readonly type: string;
26
- }[], query: string, tenancy: unknown): {
44
+ }[], query: string, tenancy: unknown, refresh?: "plain" | "blocking" | "concurrently"): {
27
45
  readonly field: string;
28
46
  readonly install: ViewInstall;
29
47
  readonly hook: SchemaHook;
@@ -43,6 +43,8 @@ export type ViewDeclaration<N extends string = string, C extends readonly ViewCo
43
43
  readonly schema: string;
44
44
  /** Declared columns, for types only. `db.views` reads them. */
45
45
  readonly "~columns"?: C;
46
+ /** `"view"`. A materialized view is `"materialized"`, which adds `refresh()`. */
47
+ readonly "~kind": "view";
46
48
  /**
47
49
  * Catalog record for this declaration.
48
50
  *
@@ -87,6 +89,8 @@ export type MaterializedViewDeclaration<N extends string = string, C extends rea
87
89
  readonly schema: string;
88
90
  /** Declared columns, for types only. `db.views` reads them. */
89
91
  readonly "~columns"?: C;
92
+ /** `"materialized"`. `db.views` adds `refresh()` for this kind only. */
93
+ readonly "~kind": "materialized";
90
94
  /**
91
95
  * Catalog records for this declaration.
92
96
  *
@@ -1,197 +1,33 @@
1
+ import {
2
+ viewClient
3
+ } from "../../../shared/5e3tf63j.js";
1
4
  import {
2
5
  viewObject,
3
6
  materializedViewObject,
4
7
  materializedViewIndex
5
- } from "../../../shared/y005b761.js";
6
- import {
7
- encodeUuid,
8
- decodeUuid
9
- } from "../../../shared/dyj86cmj.js";
10
- import {
11
- snakeCase
12
- } from "../../../shared/ntneex2a.js";
13
- import {
14
- encodeText,
15
- decodeText
16
- } from "../../../shared/4m9fn52z.js";
8
+ } from "../../../shared/zy8r7qvz.js";
17
9
  import {
18
10
  staticNamespace2
19
- } from "../../../shared/t4tbsy9h.js";
11
+ } from "../../../shared/358zsv4r.js";
20
12
  import {
21
13
  OkmError2
22
14
  } from "../../../shared/52szzakm.js";
23
15
 
24
- // src/dialects/pg/view/client.ts
25
- function viewClient(name, columns, query, tenancy) {
26
- const field = camelCase(name);
27
- const marked = readGlobal(name, tenancy);
28
- return {
29
- field,
30
- install(model, scope, tables, casing, scoped) {
31
- if (Object.hasOwn(model, field)) {
32
- throw new OkmError2("OKM1023", `View ${name} uses the client name ${field}.`, {
33
- fix: { summary: "Give the view a name that no table uses." }
34
- });
35
- }
36
- const exposes = exposesKey(columns, scope);
37
- if (scope !== undefined && readsTenant(query, tables, casing, scoped)) {
38
- if (!exposes && marked === undefined) {
39
- throw new OkmError2("OKM1820", `View ${name} reads a tenant table and does not expose ${scope.key}.`, {
40
- fix: {
41
- summary: "Expose the tenant key so the predicate can be applied, or declare global(reason)."
42
- }
43
- });
44
- }
45
- if (exposes) {
46
- scope.scopeView(field);
47
- scoped.add(name);
48
- }
49
- }
50
- model[field] = {
51
- name: field,
52
- sql: name,
53
- primary: [],
54
- uniques: [],
55
- columns: columnModels(columns, scope),
56
- relations: []
57
- };
58
- },
59
- hook(target, ctx) {
60
- publish(field, target, ctx);
61
- }
62
- };
63
- }
64
- function publish(field, target, ctx) {
65
- if (ctx.tables === undefined || ctx.table !== undefined)
66
- return;
67
- const handle = ctx.tables[field];
68
- if (typeof handle !== "object" || handle === null)
69
- return;
70
- const source = handle;
71
- const views = target.views;
72
- const map = typeof views === "object" && views !== null ? views : {};
73
- map[field] = {
74
- find: source.find,
75
- one: source.one,
76
- count: source.count,
77
- exists: source.exists
78
- };
79
- target.views = map;
80
- delete ctx.tables[field];
81
- delete target[field];
82
- }
83
- function columnModels(columns, tenancy) {
84
- const seen = new Set;
85
- const models = [];
86
- for (const column of columns) {
87
- const field = fieldName(column.name, tenancy);
88
- if (seen.has(field)) {
89
- throw new OkmError2("OKM1020", `View column ${column.name} repeats ${field}.`, {
90
- fix: { summary: "Declare each output column once." }
91
- });
92
- }
93
- seen.add(field);
94
- const uuid = column.type.toLowerCase() === "uuid";
95
- const key = tenancy !== undefined && field === tenancy.key;
96
- models.push({
97
- field,
98
- sql: column.name,
99
- dataType: column.type,
100
- encode: uuid ? (value) => encodeUuid(textValue(value)) : (value) => encodeText(textValue(value)),
101
- decode: uuid ? decodeUuid : decodeText,
102
- hidden: false,
103
- sensitive: false,
104
- guarded: key,
105
- writable: false,
106
- guardUpdate: false
107
- });
108
- }
109
- return models;
110
- }
111
- function exposesKey(columns, tenancy) {
112
- if (tenancy === undefined)
113
- return false;
114
- return columns.some((column) => fieldName(column.name, tenancy) === tenancy.key);
115
- }
116
- function readsTenant(query, tables, casing, scoped) {
117
- for (const name of scoped) {
118
- if (mentions(query, name))
119
- return true;
120
- }
121
- for (const table of tables) {
122
- const mark = table.options?.tenancy;
123
- if (isGlobalMark(mark))
124
- continue;
125
- const sql = casing === "snake" ? snakeCase(table.name) : table.name;
126
- if (mentions(query, sql) || mentions(query, table.name))
127
- return true;
128
- }
129
- return false;
130
- }
131
- function readGlobal(name, tenancy) {
132
- if (tenancy === undefined)
133
- return;
134
- if (!isGlobalMark(tenancy)) {
135
- throw new OkmError2("OKM1020", `View ${name} tenancy must be global("reason").`, {
136
- fix: { summary: 'Pass global("reason") from okmodel/tenancy, or omit tenancy.' }
137
- });
138
- }
139
- return tenancy.reason;
140
- }
141
- function isGlobalMark(value) {
142
- if (typeof value !== "object" || value === null)
143
- return false;
144
- const record = value;
145
- return record.kind === "global" && typeof record.reason === "string" && record.reason.trim().length > 0;
146
- }
147
- function fieldName(column, tenancy) {
148
- const field = camelCase(column);
149
- if (tenancy !== undefined && (column === tenancy.key || field === tenancy.key))
150
- return tenancy.key;
151
- return field;
152
- }
153
- function camelCase(name) {
154
- let out = "";
155
- let upper = false;
156
- for (const char of name) {
157
- if (char === "_") {
158
- upper = out.length > 0;
159
- continue;
160
- }
161
- out += upper ? char.toUpperCase() : char;
162
- upper = false;
163
- }
164
- return out;
165
- }
166
- function mentions(query, name) {
167
- let pattern = '(^|[^A-Za-z0-9_])"?';
168
- for (const char of name) {
169
- if ("\\^$[]().*+?|{}".includes(char))
170
- pattern += "\\";
171
- pattern += char;
172
- }
173
- pattern += '"?(?![A-Za-z0-9_])';
174
- return new RegExp(pattern).test(query);
175
- }
176
- function textValue(value) {
177
- if (typeof value !== "string") {
178
- throw new OkmError2("OKM1121", "A view column value must be text.", {
179
- fix: { summary: "Pass a string." }
180
- });
181
- }
182
- return value;
183
- }
184
-
185
16
  // src/dialects/pg/view/index.ts
186
17
  function view(name, options) {
187
18
  const schema = options.schema ?? "public";
188
19
  const columns = options.columns.map((column) => ({ name: column.name, dataType: column.type }));
189
20
  const query = bodyText(options.query);
190
- const bound = viewClient(name, options.columns, query, options.tenancy);
21
+ const bound = viewClient(name, options.columns, query, options.tenancy, "plain");
22
+ let securityInvoker = false;
191
23
  return {
192
24
  name,
193
25
  schema,
194
- install: bound.install,
26
+ "~kind": "view",
27
+ install(model, tenancy, tables, casing, scoped) {
28
+ bound.install(model, tenancy, tables, casing, scoped);
29
+ securityInvoker = tenancy?.strategy === "rls" && scoped.has(name);
30
+ },
195
31
  hook: bound.hook,
196
32
  contribute(peers, built) {
197
33
  assertPeers(peers);
@@ -203,7 +39,8 @@ function view(name, options) {
203
39
  name,
204
40
  columns,
205
41
  query,
206
- provenance
42
+ provenance,
43
+ ...securityInvoker ? { securityInvoker: true } : {}
207
44
  })
208
45
  ];
209
46
  }
@@ -213,7 +50,12 @@ function materializedView(name, options) {
213
50
  const schema = options.schema ?? "public";
214
51
  const columns = options.columns.map((column) => ({ name: column.name, dataType: column.type }));
215
52
  const query = bodyText(options.query);
216
- const bound = viewClient(name, options.columns, query, options.tenancy);
53
+ if (options.refresh !== undefined && options.refresh !== "concurrently") {
54
+ throw new OkmError2("OKM1120", `Materialized view ${name} refresh must be "concurrently".`, {
55
+ fix: { summary: 'Omit refresh, or pass refresh: "concurrently" with a unique index.' }
56
+ });
57
+ }
58
+ const bound = viewClient(name, options.columns, query, options.tenancy, options.refresh === "concurrently" ? "concurrently" : "blocking");
217
59
  const indexes = options.indexes ?? [];
218
60
  if (options.refresh === "concurrently" && !indexes.some((index) => index.unique === true)) {
219
61
  throw new OkmError2("OKM1822", `Materialized view ${name} refreshes concurrently and has no unique index.`, { fix: { summary: "Add a unique index, or refresh without CONCURRENTLY." } });
@@ -229,6 +71,7 @@ function materializedView(name, options) {
229
71
  return {
230
72
  name,
231
73
  schema,
74
+ "~kind": "materialized",
232
75
  install: bound.install,
233
76
  hook: bound.hook,
234
77
  contribute(peers, built) {
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `REFRESH MATERIALIZED VIEW` for one view handle.
3
+ *
4
+ * Loaded the first time `refresh()` runs. A featureless app, and an app that
5
+ * never calls it, do not import this module. The name is unqualified so the
6
+ * session `search_path` selects the schema, matching a view `find`.
7
+ */
8
+ /**
9
+ * Refreshes one materialized view on the primary.
10
+ *
11
+ * `CONCURRENTLY` when the declaration asked for it. The statement timeout is
12
+ * the client's, the same ceiling a write uses. A database failure is mapped
13
+ * and logged like any other statement.
14
+ *
15
+ * The refresh runs on a reserved primary connection with `app.unscoped` set
16
+ * in that same transaction. Under `FORCE ROW LEVEL SECURITY` the owner would
17
+ * otherwise refresh zero rows. `CONCURRENTLY` cannot run inside a transaction,
18
+ * so that form sets the session value and clears it before release.
19
+ *
20
+ * @param session - Pool, timeout, and logger from the client
21
+ * @param name - SQL name of the materialized view
22
+ * @param concurrently - True when the view declares `refresh: "concurrently"`
23
+ */
24
+ export declare function refreshView(session: object, name: string, concurrently: boolean): Promise<void>;