@rebasepro/server-postgres 0.23.0 → 0.24.1-canary.g19596ef

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 (208) hide show
  1. package/README.md +16 -0
  2. package/dist/{BranchService-DGPL6G_C.js → BranchService-UgKa416x.js} +47 -19
  3. package/dist/BranchService-UgKa416x.js.map +1 -0
  4. package/dist/PostgresBackendDriver.d.ts +187 -13
  5. package/dist/PostgresBootstrapper.d.ts +101 -2
  6. package/dist/auth/ensure-tables.d.ts +12 -0
  7. package/dist/auth/services.d.ts +78 -14
  8. package/dist/{auth-users-columns-D2LBFrMH.js → auth-users-columns-C72EMoDJ.js} +17 -1
  9. package/dist/{auth-users-columns-D2LBFrMH.js.map → auth-users-columns-C72EMoDJ.js.map} +1 -1
  10. package/dist/backup/backup-cli.d.ts +75 -2
  11. package/dist/backup/backup-service.d.ts +2 -2
  12. package/dist/backup/pg-binary.d.ts +5 -0
  13. package/dist/{backup-cli-24v4OlSp.js → backup-cli-yiKd1GsC.js} +168 -75
  14. package/dist/backup-cli-yiKd1GsC.js.map +1 -0
  15. package/dist/{backup-service-HQ9GC4tN.js → backup-service-AZWc2wvI.js} +104 -35
  16. package/dist/backup-service-AZWc2wvI.js.map +1 -0
  17. package/dist/boot-ddl-B9ZmBJ75.js +105 -0
  18. package/dist/boot-ddl-B9ZmBJ75.js.map +1 -0
  19. package/dist/cli-console-DGee3Zl7.js +225 -0
  20. package/dist/cli-console-DGee3Zl7.js.map +1 -0
  21. package/dist/cli-console.d.ts +39 -0
  22. package/dist/cli-errors-oVUuD8k0.js +2180 -0
  23. package/dist/cli-errors-oVUuD8k0.js.map +1 -0
  24. package/dist/cli-errors.d.ts +133 -9
  25. package/dist/{cli-flags-BglvpjHv.js → cli-flags-DnPHXjRH.js} +56 -6
  26. package/dist/cli-flags-DnPHXjRH.js.map +1 -0
  27. package/dist/cli-flags.d.ts +15 -0
  28. package/dist/cli-helpers.d.ts +88 -34
  29. package/dist/cli-output.d.ts +16 -28
  30. package/dist/{arg-Dni7MzLB.js → cli-prompt-egtNKkZl.js} +49 -3
  31. package/dist/cli-prompt-egtNKkZl.js.map +1 -0
  32. package/dist/cli-prompt.d.ts +19 -0
  33. package/dist/cli-push.d.ts +87 -0
  34. package/dist/cli-scratch-database.d.ts +33 -8
  35. package/dist/cli.js +826 -230
  36. package/dist/cli.js.map +1 -1
  37. package/dist/{column-plan-helpers-DF-8dTVa.js → column-plan-helpers-D_V4oaZh.js} +43 -27
  38. package/dist/column-plan-helpers-D_V4oaZh.js.map +1 -0
  39. package/dist/{databasePoolManager-Bj5FbeAs.js → connection-C938nR3E.js} +25 -113
  40. package/dist/connection-C938nR3E.js.map +1 -0
  41. package/dist/connection.d.ts +13 -2
  42. package/dist/data-transformer.d.ts +0 -8
  43. package/dist/databasePoolManager-DZgnBOgj.js +119 -0
  44. package/dist/databasePoolManager-DZgnBOgj.js.map +1 -0
  45. package/dist/deferred-index-build-BqmEHGaD.js +1051 -0
  46. package/dist/deferred-index-build-BqmEHGaD.js.map +1 -0
  47. package/dist/{doctor-Cb2thZ8s.js → doctor-CUAAOFZm.js} +281 -89
  48. package/dist/doctor-CUAAOFZm.js.map +1 -0
  49. package/dist/ensure-collection-policies-Bb51V2OV.js +541 -0
  50. package/dist/ensure-collection-policies-Bb51V2OV.js.map +1 -0
  51. package/dist/ensure-collection-tables-egjGBjhv.js +532 -0
  52. package/dist/ensure-collection-tables-egjGBjhv.js.map +1 -0
  53. package/dist/{ensure-tables-Dhn9KM3B.js → ensure-tables-DMPWhJ6R.js} +180 -77
  54. package/dist/ensure-tables-DMPWhJ6R.js.map +1 -0
  55. package/dist/enum-labels-kept-Msq5gMcb.js +56 -0
  56. package/dist/enum-labels-kept-Msq5gMcb.js.map +1 -0
  57. package/dist/generate-drizzle-schema-DrA-YgXv.js +57 -0
  58. package/dist/generate-drizzle-schema-DrA-YgXv.js.map +1 -0
  59. package/dist/{generate-drizzle-schema-logic-BfzK7UQd.js → generate-drizzle-schema-logic-BaT0H7Gq.js} +29 -11
  60. package/dist/generate-drizzle-schema-logic-BaT0H7Gq.js.map +1 -0
  61. package/dist/generate-files-OoRJLEdk.js +199 -0
  62. package/dist/generate-files-OoRJLEdk.js.map +1 -0
  63. package/dist/generate-postgres-ddl-logic-B4k9djAV.js +152 -0
  64. package/dist/generate-postgres-ddl-logic-B4k9djAV.js.map +1 -0
  65. package/dist/{generated-schema-staleness-Di9c4ZxN.js → generated-schema-staleness-BWN_PsMi.js} +2 -2
  66. package/dist/{generated-schema-staleness-Di9c4ZxN.js.map → generated-schema-staleness-BWN_PsMi.js.map} +1 -1
  67. package/dist/generated-sql.d.ts +34 -0
  68. package/dist/history/HistoryService.d.ts +64 -5
  69. package/dist/history/ensure-history-table.d.ts +63 -4
  70. package/dist/index.d.ts +1 -0
  71. package/dist/index.es.js +139 -16738
  72. package/dist/index.es.js.map +1 -1
  73. package/dist/{introspect-db-logic-WMuAfxvw.js → introspect-db-logic-BCc1Jym2.js} +654 -353
  74. package/dist/introspect-db-logic-BCc1Jym2.js.map +1 -0
  75. package/dist/introspect-db-queries-BhhJg0b8.js +364 -0
  76. package/dist/introspect-db-queries-BhhJg0b8.js.map +1 -0
  77. package/dist/open-rebase-CAjWpPjs.js +20895 -0
  78. package/dist/open-rebase-CAjWpPjs.js.map +1 -0
  79. package/dist/open-rebase.d.ts +94 -0
  80. package/dist/pg-error-utils-Bs_Ue4wn.js +395 -0
  81. package/dist/pg-error-utils-Bs_Ue4wn.js.map +1 -0
  82. package/dist/{plan-schema-C0fxM8dY.js → plan-schema-CsNgK0qi.js} +279 -534
  83. package/dist/plan-schema-CsNgK0qi.js.map +1 -0
  84. package/dist/{policy-drift-DljYdrpW.js → policy-drift-BKC50y2p.js} +33 -9
  85. package/dist/policy-drift-BKC50y2p.js.map +1 -0
  86. package/dist/push-index-prebuild-C_qhyoJa.js +45 -0
  87. package/dist/push-index-prebuild-C_qhyoJa.js.map +1 -0
  88. package/dist/push-legacy-renames-5g9h3HOa.js +34 -0
  89. package/dist/push-legacy-renames-5g9h3HOa.js.map +1 -0
  90. package/dist/{generate-postgres-ddl-logic-Bt2d2mRH.js → render-ddl-CrfEe__B.js} +24 -157
  91. package/dist/render-ddl-CrfEe__B.js.map +1 -0
  92. package/dist/{rls-bootstrap-sql-B8EclDyM.js → rls-bootstrap-sql-Bd-hJk08.js} +125 -1101
  93. package/dist/rls-bootstrap-sql-Bd-hJk08.js.map +1 -0
  94. package/dist/{rls-enforcement-C6Xk0lA6.js → rls-enforcement-D7P4-ZYX.js} +138 -18
  95. package/dist/rls-enforcement-D7P4-ZYX.js.map +1 -0
  96. package/dist/{__vite-browser-external-BnuHet1e.js → rolldown-runtime-V7rFbMRe.js} +1 -8
  97. package/dist/schema/atlas-exclude.d.ts +42 -0
  98. package/dist/schema/auth-schema.d.ts +172 -2
  99. package/dist/schema/boot-ddl.d.ts +47 -0
  100. package/dist/schema/carved-out-migration.d.ts +5 -4
  101. package/dist/schema/classify-change.d.ts +29 -1
  102. package/dist/schema/collection-index.d.ts +45 -0
  103. package/dist/schema/column-plan-helpers.d.ts +38 -21
  104. package/dist/schema/deferred-index-build.d.ts +131 -0
  105. package/dist/schema/destructive-sql.d.ts +50 -2
  106. package/dist/schema/doctor-cli.js +110 -6
  107. package/dist/schema/doctor-cli.js.map +1 -1
  108. package/dist/schema/doctor-search-scale.d.ts +45 -0
  109. package/dist/schema/doctor.d.ts +53 -1
  110. package/dist/schema/ensure-collection-policies.d.ts +95 -35
  111. package/dist/schema/ensure-collection-tables.d.ts +56 -3
  112. package/dist/schema/enum-labels-kept.d.ts +26 -0
  113. package/dist/schema/generate-drizzle-schema.js +1 -1
  114. package/dist/schema/generate-files.d.ts +44 -0
  115. package/dist/schema/generate-next-step.d.ts +0 -13
  116. package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -5
  117. package/dist/schema/generate-postgres-ddl.js +10 -81
  118. package/dist/schema/generate-postgres-ddl.js.map +1 -1
  119. package/dist/schema/generate-schema-commit.d.ts +12 -0
  120. package/dist/schema/generated-schema-diff.d.ts +7 -0
  121. package/dist/schema/introspect-db-logic.d.ts +63 -0
  122. package/dist/schema/introspect-db-queries.d.ts +21 -2
  123. package/dist/schema/introspect-db-search.d.ts +22 -0
  124. package/dist/schema/introspect-db-storage.d.ts +83 -0
  125. package/dist/schema/introspect-db-structure.d.ts +10 -0
  126. package/dist/schema/introspect-db.js +29 -21
  127. package/dist/schema/introspect-db.js.map +1 -1
  128. package/dist/schema/migrate-baseline.d.ts +33 -0
  129. package/dist/schema/migration-post-process.d.ts +34 -0
  130. package/dist/schema/plan/diff-plan.d.ts +127 -1
  131. package/dist/schema/plan/plan-schema.d.ts +16 -8
  132. package/dist/schema/plan/render-ddl.d.ts +8 -1
  133. package/dist/schema/plan/types.d.ts +45 -11
  134. package/dist/schema/push-index-prebuild.d.ts +25 -0
  135. package/dist/schema/push-legacy-renames.d.ts +20 -0
  136. package/dist/schema/rls-bootstrap-sql.d.ts +15 -0
  137. package/dist/schema/undeclarable-constraints.d.ts +59 -0
  138. package/dist/search-column-BM-GV6vH.js +442 -0
  139. package/dist/search-column-BM-GV6vH.js.map +1 -0
  140. package/dist/security/policy-drift.d.ts +1 -1
  141. package/dist/security/rls-enforcement.d.ts +39 -4
  142. package/dist/services/BranchService.d.ts +29 -1
  143. package/dist/services/FetchService.d.ts +63 -12
  144. package/dist/services/PersistService.d.ts +15 -0
  145. package/dist/services/RelationService.d.ts +106 -3
  146. package/dist/services/cdc/CdcListener.d.ts +26 -10
  147. package/dist/services/cdc/identity-columns.d.ts +19 -0
  148. package/dist/services/cdc/trigger-cdc.d.ts +180 -18
  149. package/dist/services/channel-bus/PostgresChannelBus.d.ts +12 -7
  150. package/dist/services/channel-bus/index.d.ts +1 -1
  151. package/dist/services/channel-history.d.ts +32 -5
  152. package/dist/services/collection-helpers.d.ts +26 -1
  153. package/dist/services/field-viewer.d.ts +15 -1
  154. package/dist/services/pg-notify-listener.d.ts +95 -6
  155. package/dist/services/realtimeService.d.ts +467 -53
  156. package/dist/services/row-pipeline.d.ts +16 -9
  157. package/dist/services/socket-liveness.d.ts +85 -0
  158. package/dist/services/soft-delete.d.ts +12 -0
  159. package/dist/services/sql-script.d.ts +275 -0
  160. package/dist/services/vector-iterative-scan.d.ts +40 -0
  161. package/dist/services/write-depth.d.ts +22 -0
  162. package/dist/services/write-transaction-scope.d.ts +15 -3
  163. package/dist/socket-opt-ins.d.ts +64 -0
  164. package/dist/source-DB6xJv3p.js +873 -0
  165. package/dist/source-DB6xJv3p.js.map +1 -0
  166. package/dist/tsx-loader.d.ts +14 -0
  167. package/dist/utils/connection-string.d.ts +31 -0
  168. package/dist/utils/drizzle-conditions.d.ts +63 -3
  169. package/dist/utils/error-cause.d.ts +7 -0
  170. package/dist/utils/pg-error-utils.d.ts +2 -4
  171. package/dist/utils/result-rows.d.ts +10 -0
  172. package/dist/{vector-index-CMJKb3C9.js → vector-index-BFhyEaiz.js} +2 -2
  173. package/dist/{vector-index-CMJKb3C9.js.map → vector-index-BFhyEaiz.js.map} +1 -1
  174. package/dist/websocket-DUiiwQd2.js +1632 -0
  175. package/dist/websocket-DUiiwQd2.js.map +1 -0
  176. package/dist/websocket.d.ts +63 -18
  177. package/package.json +7 -7
  178. package/dist/BranchService-DGPL6G_C.js.map +0 -1
  179. package/dist/__vite-browser-external-BnuHet1e.js.map +0 -1
  180. package/dist/arg-Dni7MzLB.js.map +0 -1
  181. package/dist/backup-cli-24v4OlSp.js.map +0 -1
  182. package/dist/backup-service-HQ9GC4tN.js.map +0 -1
  183. package/dist/cli-errors-B8qHg02P.js +0 -798
  184. package/dist/cli-errors-B8qHg02P.js.map +0 -1
  185. package/dist/cli-flags-BglvpjHv.js.map +0 -1
  186. package/dist/cli-output-CNdMql-L.js +0 -47
  187. package/dist/cli-output-CNdMql-L.js.map +0 -1
  188. package/dist/collection-index-DdnsxiJ_.js +0 -286
  189. package/dist/collection-index-DdnsxiJ_.js.map +0 -1
  190. package/dist/column-plan-helpers-DF-8dTVa.js.map +0 -1
  191. package/dist/databasePoolManager-Bj5FbeAs.js.map +0 -1
  192. package/dist/doctor-Cb2thZ8s.js.map +0 -1
  193. package/dist/ensure-collection-policies-RHUcEp4v.js +0 -105
  194. package/dist/ensure-collection-policies-RHUcEp4v.js.map +0 -1
  195. package/dist/ensure-collection-tables-BEEjn5cn.js +0 -857
  196. package/dist/ensure-collection-tables-BEEjn5cn.js.map +0 -1
  197. package/dist/ensure-tables-Dhn9KM3B.js.map +0 -1
  198. package/dist/generate-drizzle-schema-logic-BfzK7UQd.js.map +0 -1
  199. package/dist/generate-drizzle-schema-yzY_BLhr.js +0 -123
  200. package/dist/generate-drizzle-schema-yzY_BLhr.js.map +0 -1
  201. package/dist/generate-postgres-ddl-logic-Bt2d2mRH.js.map +0 -1
  202. package/dist/introspect-db-logic-WMuAfxvw.js.map +0 -1
  203. package/dist/plan-schema-C0fxM8dY.js.map +0 -1
  204. package/dist/policy-drift-DljYdrpW.js.map +0 -1
  205. package/dist/rls-bootstrap-sql-B8EclDyM.js.map +0 -1
  206. package/dist/rls-enforcement-C6Xk0lA6.js.map +0 -1
  207. package/dist/source-Br7L7GOI.js +0 -410
  208. package/dist/source-Br7L7GOI.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-column-BM-GV6vH.js","names":[],"sources":["../src/schema/search-column.ts"],"sourcesContent":["/**\n * The one place a collection's `search` block becomes SQL.\n *\n * Four things describe a Postgres table in this codebase — the DDL generator,\n * the Drizzle schema generator, the runtime table builder for BaaS mode, and\n * the boot-time schema ensure — and each of them has, at some point, described\n * a column differently from the others. The `varchar(255)` note in\n * `generate-postgres-ddl-logic` is one such scar: the same property produced a\n * capped column down one path and an uncapped one down the other, and nothing\n * failed until a user hit the cap.\n *\n * So the search column is not implemented four times. It is computed once,\n * here, and every generator renders the same {@link SearchColumnSpec}. There is\n * a test asserting exactly that (`search-column-contract.test.ts`); the point of\n * this module is that the test has something to assert *about*.\n *\n * ## Why the expressions look the way they do\n *\n * A `GENERATED ALWAYS AS … STORED` expression must be strictly IMMUTABLE, and\n * Postgres is stricter here than intuition. Verified against PostgreSQL 18:\n *\n * | expression | immutable |\n * |-----------------------------------------|-----------|\n * | `to_tsvector('spanish', col)` | yes |\n * | `to_tsvector(col)` (1-arg) | **no** — depends on `default_text_search_config` |\n * | `array_to_string(col, ' ')` | **no** |\n * | `col::text` on `text[]` | **no** |\n * | `to_jsonb(col)` | **no** |\n * | `unaccent(col)` | **no** — dictionary lookup is STABLE |\n * | `jsonb_to_tsvector('spanish', j, '[\"string\"]')` | yes |\n * | `setweight(...) || setweight(...)` | yes |\n *\n * Three of the four things a real search column needs are therefore unavailable\n * directly, which is why {@link searchHelperFunctions} exists: each wraps a\n * stable built-in in an SQL function declared IMMUTABLE. That declaration is a\n * promise, and it is a true one for these three — array joining, JSON string\n * extraction and accent folding are all deterministic for a given input; the\n * built-ins are marked stable only because they must account for element types\n * and dictionaries in general.\n *\n * The alternative was to skip `unaccent` and text arrays entirely. That is not\n * a real option in an accented language: Postgres stems `auditoría` to\n * `auditor` and `auditoria` to `auditori` — *different lexemes* — so a query\n * typed without accents misses every row that carries them.\n */\nimport {\n CollectionConfig,\n Property,\n StringProperty,\n ArrayProperty,\n MapProperty,\n SearchConfig,\n SearchField,\n SearchMode,\n SearchWeight,\n isPostgresCollectionConfig,\n DEFAULT_SEARCH_COLUMN,\n DEFAULT_SEARCH_MODE,\n DEFAULT_SEARCH_LANGUAGE,\n DEFAULT_SEARCH_WEIGHT,\n DEFAULT_FUZZY_THRESHOLD\n} from \"@rebasepro/types\";\nimport { createHash } from \"node:crypto\";\nimport { getTableName } from \"@rebasepro/common\";\nimport { toSnakeCase, toPostgresIdentifier } from \"@rebasepro/utils\";\n\n/** Schema-qualified so a collection outside `public` still resolves them. */\nconst HELPER_SCHEMA = \"public\";\n\n/**\n * Names of the helper functions. Frozen: they are recorded in the stored\n * generation expression of every search column ever created, so renaming one\n * orphans every table that already has a search column.\n */\nexport const SEARCH_TEXT_FN = `${HELPER_SCHEMA}.rebase_search_text`;\nexport const SEARCH_UNACCENT_FN = `${HELPER_SCHEMA}.rebase_search_unaccent`;\n\n/** How a declared path reaches text, which decides the SQL that extracts it. */\ntype FieldKind = \"text\" | \"text_array\" | \"jsonb\";\n\n/** One resolved field: where it lives, how to read it, what it is worth. */\nexport interface ResolvedSearchField {\n /** The path exactly as the author wrote it, for error messages. */\n path: string;\n /** The physical column the path starts at. */\n column: string;\n /** Dotted remainder addressed inside a JSONB column, if any. */\n jsonPath: string[];\n kind: FieldKind;\n weight: SearchWeight;\n /** The `setweight(to_tsvector(…), 'X')` term this field contributes. */\n sql: string;\n /** The plain-text term this field contributes, for the fuzzy column. */\n textSql: string;\n /**\n * The same text with accents folded **unconditionally**, for the substring\n * half of {@link SearchMode} `\"hybrid\"`.\n *\n * Separate from {@link textSql} rather than replacing it, and that\n * separation is the whole migration story for `mode`. `textSql` feeds the\n * *stored* generated columns, so changing it changes their generation\n * expression, which changes the fingerprint, which makes the next boot\n * refuse (see `searchStampGuards`). This one is only ever interpolated into\n * a WHERE clause, so a collection can switch to `\"hybrid\"` — and gain\n * accent folding on the substring half — without rebuilding a column or\n * taking an ACCESS EXCLUSIVE lock.\n */\n foldedTextSql: string;\n}\n\n/** Everything the generators need to render one collection's search column. */\nexport interface SearchColumnSpec {\n schema: string;\n table: string;\n /** The generated `tsvector` column. */\n column: string;\n language: string;\n unaccent: boolean;\n /**\n * How the query side matches. Deliberately absent from\n * {@link SearchColumnSpec.expression} and from every fingerprint: it\n * describes the WHERE clause, not the column.\n */\n mode: SearchMode;\n fields: ResolvedSearchField[];\n /** Body of `GENERATED ALWAYS AS ( … ) STORED` for the tsvector column. */\n expression: string;\n indexName: string;\n /** Extensions that must exist before the column can be created. */\n extensions: string[];\n fuzzy?: {\n column: string;\n expression: string;\n indexName: string;\n threshold: number;\n };\n}\n\n/** Raised when a `search` block names something that cannot be searched. */\nexport class SearchConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"SearchConfigError\";\n }\n}\n\n/** The `search` block of a collection, or undefined when it has none. */\nexport const getSearchConfig = (collection: CollectionConfig): SearchConfig | undefined =>\n isPostgresCollectionConfig(collection) ? collection.search : undefined;\n\n/**\n * Refuse a `search` block on a collection this engine does not store.\n *\n * The type only permits one on a `PostgresCollectionConfig`, so TypeScript\n * already stops the ordinary case. This catches the rest — a JS config, a cast,\n * a collection whose `engine` was changed after the block was written — because\n * the alternative is the exact failure the block exists to prevent: a developer\n * who declared what to index, saw no error, and got the substring fallback.\n *\n * Called with *every* collection, before the Postgres ones are filtered out.\n */\nexport const assertSearchIsPostgresOnly = (collections: CollectionConfig[]): void => {\n for (const collection of collections) {\n if (isPostgresCollectionConfig(collection)) continue;\n if (!(collection as { search?: unknown }).search) continue;\n const engine = (collection as { engine?: string }).engine ?? \"non-postgres\";\n throw new SearchConfigError(\n `${collection.slug}.search: full-text search is a Postgres feature, and this collection is served by \\`${engine}\\`. ` +\n \"Remove the block — it would otherwise look configured while `.search()` kept using the default substring match.\"\n );\n }\n};\n\nconst columnNameOf = (propName: string, prop?: Property | null): string =>\n prop && \"columnName\" in prop && typeof prop.columnName === \"string\" ? prop.columnName : toSnakeCase(propName);\n\n/**\n * Classify a property for search purposes.\n *\n * Deliberately narrower than the schema plan's `PgType`: search only cares\n * whether a value reaches text, and the mapping from property to *physical*\n * type is asserted against the plan in the contract test rather than duplicated\n * here.\n *\n * Returns null for anything that is not text-bearing, which the caller turns\n * into a boot error naming the property.\n */\nconst classify = (prop: Property): { kind: FieldKind; reason?: string } | null => {\n switch (prop.type) {\n case \"string\": {\n const sp = prop as StringProperty;\n if (sp.enum) {\n return { kind: \"text\", reason: \"enum\" };\n }\n if (sp.isId === \"uuid\" || sp.columnType === \"uuid\") {\n return { kind: \"text\", reason: \"uuid\" };\n }\n return { kind: \"text\" };\n }\n case \"map\": {\n const mp = prop as MapProperty;\n // A `json` column is not `jsonb`, and the cast between them is not\n // immutable. Declaring `columnType: \"json\"` puts the value out of\n // reach of a generated column.\n if (mp.columnType === \"json\") return { kind: \"jsonb\", reason: \"json\" };\n return { kind: \"jsonb\" };\n }\n case \"array\": {\n const ap = prop as ArrayProperty;\n let colType = ap.columnType;\n if (!colType && ap.of && !Array.isArray(ap.of)) {\n const of = ap.of as Property;\n if (of.type === \"string\") colType = \"text[]\";\n else if (of.type === \"number\") colType = of.validation?.integer ? \"integer[]\" : \"numeric[]\";\n else if (of.type === \"boolean\") colType = \"boolean[]\";\n }\n if (colType === \"text[]\") return { kind: \"text_array\" };\n if (colType === \"json\") return { kind: \"jsonb\", reason: \"json\" };\n if (colType === \"integer[]\" || colType === \"boolean[]\" || colType === \"numeric[]\") {\n return { kind: \"text_array\", reason: \"non_text_array\" };\n }\n // Everything else lands in JSONB, which the JSON extractor handles.\n return { kind: \"jsonb\" };\n }\n default:\n return null;\n }\n};\n\nconst normalize = (inner: string, unaccent: boolean): string =>\n unaccent ? `${SEARCH_UNACCENT_FN}(${inner})` : inner;\n\n/** SQL reading one field as plain text, before normalization. */\nconst rawTextSql = (field: { column: string; jsonPath: string[]; kind: FieldKind }): string => {\n const col = `\"${field.column}\"`;\n if (field.kind === \"text\") return `coalesce(${col}, '')`;\n if (field.kind === \"text_array\") return `${SEARCH_TEXT_FN}(coalesce(${col}, '{}'::text[]))`;\n // JSONB, optionally addressed at a path inside the document.\n const target = field.jsonPath.length === 0\n ? col\n : field.jsonPath.length === 1\n ? `${col} -> ${quote(field.jsonPath[0])}`\n : `${col} #> ${quote(`{${field.jsonPath.join(\",\")}}`)}`;\n return `${SEARCH_TEXT_FN}(coalesce(${target}, '{}'::jsonb))`;\n};\n\nconst quote = (v: string): string => `'${v.replace(/'/g, \"''\")}'`;\n\n/**\n * Resolve and validate one declared field path.\n *\n * A path that does not resolve throws. The whole point of an explicit block is\n * that the author knows what is indexed; a silently dropped field would make it\n * a guess again, and the failure — a search that returns nothing for content\n * that is plainly in the row — is invisible from the outside.\n */\nconst resolveField = (\n entry: string | SearchField,\n collection: CollectionConfig,\n cfg: SearchConfig\n): ResolvedSearchField => {\n const path = typeof entry === \"string\" ? entry : entry.path;\n const weight = (typeof entry === \"string\" ? undefined : entry.weight) ?? DEFAULT_SEARCH_WEIGHT;\n const where = `${collection.slug}.search`;\n\n if (!path || typeof path !== \"string\") {\n throw new SearchConfigError(`${where}: every entry in \\`fields\\` needs a property path.`);\n }\n\n const [head, ...rest] = path.split(\".\");\n const prop = collection.properties?.[head] as Property | undefined;\n if (!prop) {\n const known = Object.keys(collection.properties ?? {}).join(\", \");\n throw new SearchConfigError(\n `${where}: \"${path}\" starts at property \"${head}\", which this collection does not declare. Known properties: ${known}.`\n );\n }\n\n const classified = classify(prop);\n if (!classified) {\n throw new SearchConfigError(\n `${where}: \"${path}\" is a \\`${prop.type}\\` property, which holds no text to search. ` +\n `Searchable kinds are \\`string\\`, \\`string[]\\` and \\`map\\` (or a path inside one).`\n );\n }\n if (classified.reason === \"enum\") {\n throw new SearchConfigError(\n `${where}: \"${path}\" is an enum. Enums are a fixed vocabulary — filter on them with \\`where\\` instead, which is exact and uses an index.`\n );\n }\n if (classified.reason === \"uuid\") {\n throw new SearchConfigError(\n `${where}: \"${path}\" is a UUID column. Look it up by id rather than searching it.`\n );\n }\n if (classified.reason === \"json\") {\n throw new SearchConfigError(\n `${where}: \"${path}\" is a \\`json\\` column, and the cast from \\`json\\` to \\`jsonb\\` is not immutable, so it cannot feed a generated column. Declare the property as \\`jsonb\\` (the default) to search it.`\n );\n }\n if (classified.reason === \"non_text_array\") {\n throw new SearchConfigError(\n `${where}: \"${path}\" is an array of numbers or booleans. Only \\`string[]\\` carries text to search.`\n );\n }\n\n if (rest.length > 0 && classified.kind !== \"jsonb\") {\n throw new SearchConfigError(\n `${where}: \"${path}\" addresses a path inside \"${head}\", but \"${head}\" is a \\`${prop.type}\\` property, not a \\`map\\`. Only map properties have paths inside them.`\n );\n }\n\n const column = columnNameOf(head, prop);\n const field = { column, jsonPath: rest, kind: classified.kind };\n const raw = rawTextSql(field);\n const textSql = normalize(raw, cfg.unaccent === true);\n const language = cfg.language ?? DEFAULT_SEARCH_LANGUAGE;\n\n return {\n path,\n column,\n jsonPath: rest,\n kind: classified.kind,\n weight,\n sql: `setweight(to_tsvector(${quote(language)}, ${textSql}), ${quote(weight)})`,\n textSql,\n foldedTextSql: normalize(raw, true)\n };\n};\n\n/**\n * Build the full spec for a collection, or undefined when it has not opted in.\n *\n * Throws {@link SearchConfigError} on a config that cannot be honoured. Callers\n * at boot surface that as a startup failure — a search block that half-works is\n * worse than one that refuses.\n */\nexport const buildSearchColumnSpec = (collection: CollectionConfig): SearchColumnSpec | undefined => {\n const cfg = getSearchConfig(collection);\n if (!cfg) return undefined;\n\n if (!Array.isArray(cfg.fields) || cfg.fields.length === 0) {\n throw new SearchConfigError(\n `${collection.slug}.search: \\`fields\\` is empty. Name the properties to index, or remove the \\`search\\` block to keep the default ILIKE behaviour.`\n );\n }\n\n const table = getTableName(collection);\n const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : \"public\";\n const column = cfg.column ?? DEFAULT_SEARCH_COLUMN;\n\n if (collection.properties?.[column]) {\n throw new SearchConfigError(\n `${collection.slug}.search: the generated column \"${column}\" collides with a declared property of the same name. Set \\`search.column\\` to something else.`\n );\n }\n\n const fields = cfg.fields.map(entry => resolveField(entry, collection, cfg));\n\n const seen = new Set<string>();\n for (const f of fields) {\n if (seen.has(f.path)) {\n throw new SearchConfigError(`${collection.slug}.search: \"${f.path}\" is listed twice.`);\n }\n seen.add(f.path);\n }\n\n const mode = cfg.mode ?? DEFAULT_SEARCH_MODE;\n\n const extensions: string[] = [];\n // `hybrid` folds accents on its substring half whatever `unaccent` says, so\n // it needs the dictionary and the helper even on a block that never asked\n // for folding in the stored column. Both statements are idempotent, which\n // is what keeps turning the mode on out of the column-rebuild business.\n if (cfg.unaccent || mode === \"hybrid\") extensions.push(\"unaccent\");\n if (cfg.fuzzy) extensions.push(\"pg_trgm\");\n\n const spec: SearchColumnSpec = {\n schema,\n table,\n column,\n language: cfg.language ?? DEFAULT_SEARCH_LANGUAGE,\n unaccent: cfg.unaccent === true,\n mode,\n fields,\n expression: fields.map(f => f.sql).join(\" || \"),\n indexName: toPostgresIdentifier(`${table}_${column}_gin`),\n extensions\n };\n\n if (cfg.fuzzy) {\n const fuzzyColumn = `${column}_text`;\n if (collection.properties?.[fuzzyColumn]) {\n throw new SearchConfigError(\n `${collection.slug}.search: \\`fuzzy\\` needs the column \"${fuzzyColumn}\", which collides with a declared property. Set \\`search.column\\` to something else.`\n );\n }\n spec.fuzzy = {\n column: fuzzyColumn,\n // Concatenated with spaces so a trigram never spans two fields.\n expression: fields.map(f => f.textSql).join(\" || ' ' || \"),\n indexName: toPostgresIdentifier(`${table}_${fuzzyColumn}_trgm`),\n threshold: cfg.fuzzyThreshold ?? DEFAULT_FUZZY_THRESHOLD\n };\n }\n\n return spec;\n};\n\n/**\n * The IMMUTABLE wrappers the generated expressions call.\n *\n * `CREATE OR REPLACE` so a boot against an existing database is a no-op rather\n * than an error, and idempotent for the same reason every other boot-time DDL\n * statement here is.\n *\n * The bodies are stable built-ins wrapped in an immutable promise — see the\n * module comment for why that promise is sound. `STRICT` matters: it makes NULL\n * in mean NULL out without executing the body, which is what the `coalesce` at\n * each call site then absorbs.\n */\nexport const searchHelperFunctions = (spec: SearchColumnSpec): string[] => {\n const statements: string[] = [\n // text[] → \" \"-joined text.\n `CREATE OR REPLACE FUNCTION ${SEARCH_TEXT_FN}(text[]) RETURNS text\\n` +\n ` LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS\\n` +\n ` $$ SELECT array_to_string($1, ' ') $$;`,\n // jsonb → every string value at or below the node, space-joined. Keys\n // are not values: indexing them would make `certifications` itself a\n // search term on every row that has the field at all.\n `CREATE OR REPLACE FUNCTION ${SEARCH_TEXT_FN}(jsonb) RETURNS text\\n` +\n ` LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS\\n` +\n ` $$ SELECT coalesce(string_agg(v, ' '), '')\\n` +\n ` FROM jsonb_array_elements_text(jsonb_path_query_array($1, 'strict $.**?(@.type() == \"string\")')) AS v $$;`\n ];\n if (spec.unaccent || spec.mode === \"hybrid\") {\n // The two-argument form with an explicit dictionary is the one that can\n // honestly be called immutable: the single-argument form resolves the\n // dictionary through the current search_path at call time.\n statements.push(\n `CREATE OR REPLACE FUNCTION ${SEARCH_UNACCENT_FN}(text) RETURNS text\\n` +\n ` LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS\\n` +\n ` $$ SELECT ${HELPER_SCHEMA}.unaccent('${HELPER_SCHEMA}.unaccent'::regdictionary, $1) $$;`\n );\n }\n return statements;\n};\n\n/**\n * `CREATE EXTENSION` statements the spec's expressions depend on.\n *\n * `WITH SCHEMA public` is load-bearing, not tidiness. An unqualified\n * `CREATE EXTENSION` installs into the first schema on `search_path`, which\n * defaults to `\"$user\", public` — and the scaffold's database role is named\n * `rebase`, the same as the schema the generator creates one statement earlier.\n * So the moment that schema exists, `CREATE EXTENSION unaccent` puts the\n * dictionary in `rebase`, and every reference to `public.unaccent` below fails\n * with \"text search dictionary does not exist\". Observed, not theorised.\n */\nexport const searchExtensionStatements = (spec: SearchColumnSpec): string[] =>\n spec.extensions.map(e => `CREATE EXTENSION IF NOT EXISTS ${e} WITH SCHEMA ${HELPER_SCHEMA};`);\n\n/**\n * Everything after the column name — the type and the generation expression.\n *\n * Split out because four emitters need it and only two of them have a place to\n * put the name: `CREATE TABLE` and `ADD COLUMN` write `\"col\" <this>`, while the\n * schema plan carries it as the column's SQL definition and the boot-time\n * rebuild statement interpolates it on its own.\n */\nexport const searchColumnTypeSql = (expression: string, kind: \"tsvector\" | \"text\"): string =>\n `${kind} GENERATED ALWAYS AS (${expression}) STORED`;\n\n/** The column definition as it appears inside `CREATE TABLE`. */\nexport const searchColumnDefinition = (spec: SearchColumnSpec): string =>\n `\"${spec.column}\" ${searchColumnTypeSql(spec.expression, \"tsvector\")}`;\n\n/** The fuzzy column definition, when the spec asks for one. */\nexport const fuzzyColumnDefinition = (spec: SearchColumnSpec): string | undefined =>\n spec.fuzzy ? `\"${spec.fuzzy.column}\" ${searchColumnTypeSql(spec.fuzzy.expression, \"text\")}` : undefined;\n\n/**\n * Index statements for the spec.\n *\n * `CONCURRENTLY` is deliberately *not* used here. This form is emitted into a\n * SQL file replayed as one unit — a migration, or `search.sql` — where a\n * concurrent build is not allowed. The boot-time ensure path runs statement by\n * statement against tables that are live and populated, and uses the\n * concurrent form instead; see `ensureSearchColumns`.\n */\nexport const searchIndexStatements = (spec: SearchColumnSpec): string[] => {\n const statements = [\n `CREATE INDEX IF NOT EXISTS \"${spec.indexName}\" ON \"${spec.schema}\".\"${spec.table}\" USING GIN (\"${spec.column}\");`\n ];\n if (spec.fuzzy) {\n statements.push(\n // The operator class is resolved through `search_path` like any\n // other object, so it is qualified for the same reason the\n // extension is installed explicitly.\n `CREATE INDEX IF NOT EXISTS \"${spec.fuzzy.indexName}\" ON \"${spec.schema}\".\"${spec.table}\" USING GIN (\"${spec.fuzzy.column}\" ${HELPER_SCHEMA}.gin_trgm_ops);`\n );\n }\n return statements;\n};\n\n// ── Telling a changed `search` block from an unchanged one ──────────────────\n\n/**\n * Marker on the comment of every generated search column this module creates.\n *\n * Versioned because the fingerprint below is only comparable against itself: a\n * future change to how it is computed has to read as \"not stamped by this\n * version\" rather than as drift on every existing column.\n */\nexport const SEARCH_STAMP_PREFIX = \"rebase:search:v1:\";\n\n/**\n * A stable fingerprint of one generated column's expression.\n *\n * Why a stamp rather than reading the expression back: Postgres stores a\n * generated column's expression *parsed*, and hands it back deparsed — casts\n * made explicit, identifiers requoted, schema qualifications added or dropped\n * according to `search_path`. Comparing that text to the text we generated\n * would report drift on wording, and this comparison decides whether a boot\n * refuses, so a false positive is an outage. The stamp is written by the same\n * code that writes the column, so equality means what it says.\n */\nexport const searchExpressionFingerprint = (expression: string): string =>\n `${SEARCH_STAMP_PREFIX}${createHash(\"sha256\").update(expression).digest(\"hex\").slice(0, 16)}`;\n\n/** One generated column, with the fingerprint that identifies its expression. */\nexport interface SearchColumnStamp {\n column: string;\n /** The expression the column is generated from. */\n expression: string;\n fingerprint: string;\n /** `COMMENT ON COLUMN …`, which is where the fingerprint is recorded. */\n sql: string;\n}\n\n/**\n * The stamps for a spec's generated columns — one per column, never shared.\n *\n * Per column on purpose: turning `fuzzy` on adds a second column and changes\n * nothing about the first, and a spec-wide fingerprint would report the\n * untouched `tsvector` column as drifted and refuse a boot over a change that\n * is purely additive.\n */\nexport const searchColumnStamps = (spec: SearchColumnSpec): SearchColumnStamp[] => {\n const stamp = (column: string, expression: string): SearchColumnStamp => {\n const fingerprint = searchExpressionFingerprint(expression);\n return {\n column,\n expression,\n fingerprint,\n sql: `COMMENT ON COLUMN \"${spec.schema}\".\"${spec.table}\".\"${column}\" IS ${quote(fingerprint)};`\n };\n };\n const stamps = [stamp(spec.column, spec.expression)];\n if (spec.fuzzy) stamps.push(stamp(spec.fuzzy.column, spec.fuzzy.expression));\n return stamps;\n};\n\n/**\n * The same drift check as the boot ensure, for the SQL file.\n *\n * Needed because {@link searchColumnStamps} would otherwise *launder* drift on\n * the migration path: `ADD COLUMN IF NOT EXISTS` does nothing to a column that\n * exists, so a re-generated `search.sql` would stamp a stale column with the\n * new block's fingerprint and the next boot would find them in agreement.\n * Guarding first means the file refuses instead — `rebase db push` is attended,\n * and the operator reading the failure is the person who changed the block.\n */\nexport const searchStampGuards = (spec: SearchColumnSpec): string[] =>\n searchColumnStamps(spec).map(stamp => {\n const relation = quote(`\"${spec.schema}\".\"${spec.table}\"`);\n return `DO $rebase_search$\nDECLARE recorded text;\nBEGIN\n SELECT col_description(a.attrelid, a.attnum) INTO recorded\n FROM pg_attribute a\n WHERE a.attrelid = ${relation}::regclass AND a.attname = ${quote(stamp.column)} AND NOT a.attisdropped;\n IF recorded LIKE ${quote(`${SEARCH_STAMP_PREFIX}%`)} AND recorded <> ${quote(stamp.fingerprint)} THEN\n RAISE EXCEPTION 'Rebase: the search block for ${spec.schema}.${spec.table} changed after the generated column \"${stamp.column}\" was built (recorded %, expected ${stamp.fingerprint}). Postgres cannot alter a generated expression in place. Drop the column and re-apply this file — it rewrites the table and rebuilds the index: ALTER TABLE ${relation.slice(1, -1)} DROP COLUMN \"${stamp.column}\";', recorded;\n END IF;\nEND\n$rebase_search$;`;\n });\n\n/**\n * The index names the spec creates.\n *\n * Needed by name, not just by statement, so Atlas can be told to exclude them\n * from its diff — see `searchExcludePatterns`.\n */\nexport const searchIndexNames = (spec: SearchColumnSpec): string[] =>\n spec.fuzzy ? [spec.indexName, spec.fuzzy.indexName] : [spec.indexName];\n\n// ── Keeping the generated columns out of responses ──────────────────────────\n\n/**\n * The generated column names a collection's search block adds, if any.\n *\n * These are physical columns on the table, so `SELECT *` returns them. They are\n * an index in column form — a list of lexeme positions, or a concatenation of\n * every searchable field on the row — and nothing outside the query planner has\n * any use for them. Left in, every list response carries a second, larger copy\n * of the row's text.\n */\nexport const searchColumnNames = (collection: CollectionConfig): string[] => {\n let spec: SearchColumnSpec | undefined;\n try {\n spec = buildSearchColumnSpec(collection);\n } catch {\n // A malformed block is reported at boot, loudly. A read is the wrong\n // place to raise it a second time, and returning the row without the\n // exclusion would be worse than returning it with.\n return [];\n }\n if (!spec) return [];\n return spec.fuzzy ? [spec.column, spec.fuzzy.column] : [spec.column];\n};\n\n/**\n * True for a column whose type only ever holds a search index.\n *\n * Independent of any collection config on purpose: an introspected database\n * (BaaS mode) can carry a `tsvector` column this framework never created —\n * Pagila's `film.fulltext` is the canonical one — and it should not be returned\n * to callers either. `isDerivedIndexColumn` already keeps such a column out of\n * the *properties*; this keeps it out of the *rows*.\n */\nexport const isSearchIndexColumn = (column: { getSQLType?: () => string }): boolean => {\n const sqlType = typeof column?.getSQLType === \"function\" ? column.getSQLType().toLowerCase() : \"\";\n return sqlType === \"tsvector\" || sqlType === \"tsquery\";\n};\n\n/**\n * A drizzle select projection over `table` with the search columns dropped.\n *\n * Returns undefined when nothing needs dropping, so the common case keeps using\n * a plain `select()` and this stays invisible in the generated SQL.\n */\nexport const visibleColumnProjection = (\n tableColumns: Record<string, { getSQLType?: () => string }> | undefined,\n collection?: CollectionConfig\n): Record<string, unknown> | undefined => {\n const excluded = excludedColumnNames(tableColumns, collection);\n if (!tableColumns || excluded.length === 0) return undefined;\n const projection: Record<string, unknown> = {};\n for (const [name, column] of Object.entries(tableColumns)) {\n if (!excluded.includes(name)) projection[name] = column;\n }\n return projection;\n};\n\n/** The same exclusion as a drizzle `db.query` `columns` denylist. */\nexport const hiddenColumnsOption = (\n tableColumns: Record<string, { getSQLType?: () => string }> | undefined,\n collection?: CollectionConfig\n): Record<string, false> | undefined => {\n const excluded = excludedColumnNames(tableColumns, collection);\n if (excluded.length === 0) return undefined;\n return Object.fromEntries(excluded.map(name => [name, false as const]));\n};\n\n/**\n * The columns to keep out of a response, by name.\n *\n * `tableColumns` is whatever `getTableColumns` returned, which is `undefined`\n * for anything that is not a real drizzle table — a stub in a test, a derived\n * or nested path with no table behind it. Nothing to exclude is the right\n * answer there, and it has to be an answer rather than a throw: this runs on\n * the read path of every collection, opted in or not.\n */\nconst excludedColumnNames = (\n tableColumns: Record<string, { getSQLType?: () => string }> | undefined,\n collection?: CollectionConfig\n): string[] => {\n if (!tableColumns || typeof tableColumns !== \"object\") return [];\n const byName = new Set(collection ? searchColumnNames(collection) : []);\n return Object.keys(tableColumns).filter(\n name => byName.has(name) || isSearchIndexColumn(tableColumns[name])\n );\n};\n"],"mappings":"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"}
@@ -10,7 +10,7 @@
10
10
  * table is indistinguishable from a table with no data.
11
11
  *
12
12
  * Expected policies are parsed from `generatePostgresPoliciesDdl`, the same
13
- * function `db push` uses to write `drizzle/policies.sql`, so this compares
13
+ * function `db push` uses to write `.rebase/sql/policies.sql`, so this compares
14
14
  * against exactly what would be applied rather than a reimplementation.
15
15
  */
16
16
  import { type CollectionConfig } from "@rebasepro/types";
@@ -31,8 +31,9 @@ import { REBASE_USER_ROLE } from "@rebasepro/common";
31
31
  * SELECT/INSERT/UPDATE/DELETE grants (+ default privileges so future
32
32
  * tables stay covered).
33
33
  * 3. {@link applyAuthContext} — per-transaction: set the `app.*` GUCs the
34
- * policies read (`rebase.uid()` etc.) and `SET LOCAL ROLE rebase_user` so
35
- * RLS binds. Transaction-scoped, so it composes with poolers.
34
+ * policies read (`rebase.uid()` etc.) and the transaction-local role
35
+ * `rebase_user` so RLS binds, in one statement. Transaction-scoped, so it
36
+ * composes with poolers.
36
37
  *
37
38
  * Provisioning runs from the framework's own bootstrap/migrate (which already
38
39
  * self-creates the `auth` schema and functions) — enforcement is default-on,
@@ -115,6 +116,13 @@ export interface AuthContext {
115
116
  */
116
117
  claims?: Record<string, unknown>;
117
118
  }
119
+ /**
120
+ * Take every privilege the user role holds on one table back — what a table
121
+ * gets when RLS cannot be switched on for it, so it is unreachable rather than
122
+ * unprotected. Quoted exactly: a mixed-case adopted table (`"User"`) folded to
123
+ * lower case names a table that does not exist.
124
+ */
125
+ export declare const revokeUserRoleSql: (schema: string, table: string) => string;
118
126
  /**
119
127
  * Warn when the connection role shares its name with an existing schema.
120
128
  *
@@ -196,11 +204,38 @@ export declare function appRoleSetupInstructions(connectionRole: string, schemas
196
204
  * connection cannot create it.
197
205
  */
198
206
  export declare function ensureAppRole(run: RawSqlRunner, schemas: string[]): Promise<void>;
207
+ /**
208
+ * The grant tier of {@link ensureAppRole} on its own: `USAGE` on each existing
209
+ * schema in `schemas`, DML on its tables and `USAGE, SELECT` on its
210
+ * sequences, the default privileges that cover what this role creates later,
211
+ * and Rebase's own tables taken back — for a role and membership already
212
+ * provisioned.
213
+ *
214
+ * Every statement is issued only when the catalogue says it would change
215
+ * something. They used to run unconditionally, twice per boot (at connect, and
216
+ * again in the final posture check): a `GRANT … ON ALL TABLES` rewrites every
217
+ * table's `pg_class` row whether or not the ACL changes, which invalidates
218
+ * every table's cached plans on every connection, and the internal-table sweep
219
+ * ran 23 `REVOKE`s per schema. A boot against a database whose grants already
220
+ * hold now reads one catalogue query and writes nothing; one that is missing
221
+ * any of them runs exactly the statements it always ran.
222
+ *
223
+ * Table owners (the expected non-superuser posture) can always grant on their
224
+ * own objects, so this needs no extra privilege.
225
+ */
226
+ export declare function ensureAppRoleGrants(run: RawSqlRunner, schemas: string[]): Promise<void>;
227
+ /**
228
+ * Rebase's own RLS-off tables in `schema` that a `revokeInternalTableAccess`
229
+ * sweep would change — the user role holds a privilege on them, or their ACL
230
+ * has never been written. One read; the sweep it narrows is one `DO` per
231
+ * table name.
232
+ */
233
+ export declare function internalTablesGrantedToUserRole(run: RawSqlRunner, schema: string): Promise<string[]>;
199
234
  /**
200
235
  * Apply the authenticated context to a transaction: the `app.*` GUCs that RLS
201
236
  * policies read via `rebase.uid()` / `rebase.roles()` / `rebase.jwt()`, and — when
202
- * `userRole` is set — `SET LOCAL ROLE` so RLS binds every statement in this
203
- * transaction (reads *and* writes).
237
+ * `userRole` is set — the `LOCAL` role switch, in the same statement, so RLS
238
+ * binds every statement in this transaction (reads *and* writes).
204
239
  *
205
240
  * GUCs are set with `is_local = true` and the role switch is `LOCAL`: both
206
241
  * reset at commit/rollback, so pooled connections are never polluted.
@@ -23,10 +23,31 @@ export declare class BranchingUnsupportedError extends Error {
23
23
  readonly code = "BRANCHING_UNSUPPORTED";
24
24
  constructor(message: string);
25
25
  }
26
+ /**
27
+ * Where a server's branches are kept: the main database — what a branch is
28
+ * copied from unless told otherwise, and what may never be dropped — and the
29
+ * connection whose `rebase.branches` records them.
30
+ */
31
+ export interface BranchHome {
32
+ /** The main database's name. */
33
+ database(): Promise<string>;
34
+ /**
35
+ * A connection on the database the registry is in. Asked for at each use:
36
+ * a pool to the main database is closed before it is copied.
37
+ */
38
+ registry(): Promise<DrizzleClient>;
39
+ }
26
40
  export declare class BranchService {
27
41
  private db;
28
42
  private poolManager;
29
- constructor(db: DrizzleClient, poolManager: DatabasePoolManager);
43
+ private readonly home;
44
+ /**
45
+ * @param db A connection on the server the branches are made on. Branch
46
+ * DDL runs here.
47
+ * @param home Where branches are kept. Without one, the database the
48
+ * pool manager's connection string names, and `db`'s registry.
49
+ */
50
+ constructor(db: DrizzleClient, poolManager: DatabasePoolManager, home?: BranchHome);
30
51
  /**
31
52
  * Refuse a branch mutation the connected server cannot honour.
32
53
  *
@@ -40,6 +61,13 @@ export declare class BranchService {
40
61
  * pane into a failure with a "Try again" that can never succeed.
41
62
  */
42
63
  private assertBranchingSupported;
64
+ /**
65
+ * Whether the `rebase.branches` registry exists. A database that has never
66
+ * had a branch made on it may not have it, and reading it there is an
67
+ * empty answer, not an error — nor a reason to create it. The CLI's `list`
68
+ * and `info` ask this rather than running the DDL that creates it.
69
+ */
70
+ hasBranchRegistry(): Promise<boolean>;
43
71
  /**
44
72
  * Ensure the `rebase.branches` metadata table exists in the default database.
45
73
  * Idempotent — safe to call on every startup.
@@ -1,5 +1,5 @@
1
1
  import { SQL } from "drizzle-orm";
2
- import { PgTable } from "drizzle-orm/pg-core";
2
+ import { AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
3
3
  import { FilterValues, OrderByTuple, LogicalCondition } from "@rebasepro/types";
4
4
  import type { IncludeSpec, VectorSearchParams } from "@rebasepro/types";
5
5
  import { RelationService } from "./RelationService.js";
@@ -218,6 +218,18 @@ export declare class FetchService {
218
218
  * no related row, so a typo in an `include` looked like empty data.
219
219
  */
220
220
  private resolveIncludeLevel;
221
+ /**
222
+ * Resolve every level of an include tree against the collections, without
223
+ * loading anything — the 400 for an unknown relation, decided before the
224
+ * read rather than by it.
225
+ *
226
+ * {@link loadIncludes} resolves a level only once it has rows to load it
227
+ * onto. On its own that made the refusal depend on the data: a typo in
228
+ * `include` against an empty table, under a filter that matched nothing,
229
+ * or below a relation that loaded no rows answered 200, and the same
230
+ * request became a 400 the first time a row matched.
231
+ */
232
+ private assertIncludeResolvable;
221
233
  /**
222
234
  * The extra `WHERE` an include's own `where`/`logical` puts on the target.
223
235
  *
@@ -227,14 +239,25 @@ export declare class FetchService {
227
239
  */
228
240
  private includeNarrowing;
229
241
  /**
230
- * Sort, cap and project the rows of ONE to-many relation, per parent.
242
+ * Refuse a `count` the loader cannot serve honestly, before anything runs.
231
243
  *
232
- * `orderBy` and `limit` are applied here rather than in SQL because a limit
233
- * on a batched relation load is *per parent*, and expressing that needs a
234
- * LATERAL or a window function per relation kind — four of them, each with
235
- * its own join shape. The rows were already narrowed by
236
- * {@link includeNarrowing} in SQL, so what this sorts and cuts is the set
237
- * the caller asked for and not the table.
244
+ * - On a to-one relation: there is nothing to count past a row or none,
245
+ * and answering 0 or 1 under `_count` would invent a shape for it.
246
+ * - On a collection with a property named `_count`: the total would be
247
+ * written over that column's value. A silent overwrite answers 200 with
248
+ * the column gone, so it is a 400 naming why.
249
+ */
250
+ private assertCountsAllowed;
251
+ /**
252
+ * Cap and project the rows of ONE relation, per parent.
253
+ *
254
+ * The order and the limit are Postgres's: the batch loader ranks each
255
+ * parent's related rows by the include's `orderBy` and reads back only the
256
+ * ones within its `limit` (see
257
+ * {@link RelationService.batchFetchRelatedEntitiesMany}), so `rows`
258
+ * arrives sorted and already cut. The slice here is what holds the limit
259
+ * where a `via` chain reaches one target along two paths — those rows share
260
+ * a rank — and it costs nothing where nothing is left to cut.
238
261
  */
239
262
  private shapeRelatedRows;
240
263
  /**
@@ -313,9 +336,10 @@ export declare class FetchService {
313
336
  * Extract cursor pagination conditions from startAfter options.
314
337
  *
315
338
  * "Every row that sorts after this one", written out as a comparison over
316
- * the same keys the `ORDER BY` uses and ending on the same `id DESC`. With
317
- * one key that is the familiar `k > v OR (k = v AND id < cursorId)`; with
318
- * several it nests, each key's tie handing the decision to the next.
339
+ * the same keys the `ORDER BY` uses and ending on the same primary key,
340
+ * descending. With one sort key that is the familiar
341
+ * `k > v OR (k = v AND id < cursorId)`; with several it nests, each key's
342
+ * tie handing the decision to the next.
319
343
  */
320
344
  private buildCursorConditions;
321
345
  /** The table's primary key columns, in key order. */
@@ -359,7 +383,7 @@ export declare class FetchService {
359
383
  */
360
384
  private preciseCursorValue;
361
385
  /**
362
- * "Sorts strictly after the cursor row", over `keys` and then the id.
386
+ * "Sorts strictly after the cursor row", over `keys` and then the primary key.
363
387
  *
364
388
  * Built by recursion rather than as a row-value comparison — `(a, b) > (x, y)`
365
389
  * would be shorter, but it is only correct when every key runs the same
@@ -645,6 +669,33 @@ export declare class FetchService {
645
669
  /** See `FetchCollectionProps.withDeleted`. */
646
670
  withDeleted?: WithDeleted;
647
671
  }): Promise<Record<string, unknown> | null>;
672
+ /**
673
+ * {@link fetchOneForRest}'s read of a row an INSERT just wrote, taken from
674
+ * the INSERT's own `RETURNING` instead of a second statement — or
675
+ * `undefined` when the two could answer differently.
676
+ *
677
+ * A write answers with `fetchOneForRest(slug, id, undefined, …, { withDeleted: true })`
678
+ * (see `PersistService.save`): the row's visible columns, rendered by
679
+ * `toRestRow`. For a row the statement itself wrote, with nothing written
680
+ * after it, `RETURNING` those same columns is that row — the same values
681
+ * through the same column decoders, in the same order, rendered the same
682
+ * way. Two things narrow the read and not the statement, and either one
683
+ * keeps the read:
684
+ *
685
+ * - a `beforeQuery` adds conditions to the read (and runs application
686
+ * code): a row outside them is no answer today, and would be one from
687
+ * `RETURNING`;
688
+ * - per-field read access, which projects the read for the viewer.
689
+ *
690
+ * Row-level security is the same either way. `INSERT … RETURNING` holds
691
+ * the new row to the table's SELECT policies and refuses one they hide
692
+ * (42501) — which the INSERT's `RETURNING <key>` already did, so returning
693
+ * more columns changes nothing about who may write or read what.
694
+ */
695
+ insertReturning(collectionPath: string): {
696
+ columns: Record<string, AnyPgColumn>;
697
+ render: (returned: Record<string, unknown>) => Record<string, unknown>;
698
+ } | undefined;
648
699
  /**
649
700
  * Fetch raw rows without any relation processing (for REST fast path)
650
701
  */
@@ -75,6 +75,21 @@ export declare class PersistService {
75
75
  * for why a zero-row write cannot be reported as success.
76
76
  */
77
77
  private explainZeroRowWrite;
78
+ /**
79
+ * Lock one row against every other write to it until this transaction
80
+ * ends: `FOR NO KEY UPDATE`, the lock an UPDATE of it takes anyway.
81
+ *
82
+ * For a conditional write (`If-Match`): taken before the row is read and
83
+ * compared, so a write that lands meanwhile is either already committed and
84
+ * read, or waits for this one. Without it two writes carrying the same tag
85
+ * both read the old row, both passed, and the second overwrote the first.
86
+ *
87
+ * Under row-level security the lock reads through the caller's UPDATE
88
+ * policies as well as their SELECT ones, so a row they may read but not
89
+ * update is simply not locked — and their write is refused by the database
90
+ * all the same. A nested address locks the row it ends in.
91
+ */
92
+ lockRow(collectionPath: string, id: string | number): Promise<void>;
78
93
  /**
79
94
  * Delete an row by ID
80
95
  */
@@ -1,5 +1,5 @@
1
1
  import { SQL } from "drizzle-orm";
2
- import { PgTable } from "drizzle-orm/pg-core";
2
+ import { AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
3
3
  import { DrizzleClient } from "../interfaces.js";
4
4
  import { CollectionConfig, FilterValues, OrderByTuple, ResolvedRelation, ResolvedHasMany, ResolvedHasOne } from "@rebasepro/types";
5
5
  import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry.js";
@@ -16,6 +16,70 @@ import { type ReadCallContextProvider } from "./read-scope.js";
16
16
  * the same claim about the same drizzle behaviour in a second spelling.
17
17
  */
18
18
  export declare function applyDynamicJoin<T>(query: T, joinTable: PgTable, condition: SQL): T;
19
+ /**
20
+ * The ON condition of one `joinPath` step: every `from` column equal to its
21
+ * `to` column, or `undefined` naming the first column either table lacks.
22
+ *
23
+ * `on.from`/`on.to` are a column or a tuple, and a tuple is how a step joins on
24
+ * a composite key. Comparing the first pair alone joined each row to every row
25
+ * sharing that column — every locale of a translation, not the one addressed.
26
+ */
27
+ export declare function joinStepCondition(currentTable: PgTable, joinTable: PgTable, step: {
28
+ on: {
29
+ from: string | string[];
30
+ to: string | string[];
31
+ };
32
+ }): {
33
+ condition: SQL;
34
+ } | {
35
+ missing: string;
36
+ };
37
+ /**
38
+ * What an include asks of ONE to-many relation's rows, per parent: how many,
39
+ * and in what order. See {@link RelationService.batchFetchRelatedEntitiesMany}.
40
+ */
41
+ export interface RelatedRowWindow {
42
+ /** Rows to keep per parent. */
43
+ limit?: number;
44
+ /** Their order, by the target's own columns. */
45
+ orderBy?: OrderByTuple[];
46
+ /**
47
+ * Filled, when given and a `limit` is set, with each parent's number of
48
+ * matching related rows *before* the limit — what `include: { x: { limit,
49
+ * count: true } }` serves under `_count`. Counted in the ranking subquery,
50
+ * so it is the same statement, the same conditions and the same RLS
51
+ * context as the rows. A parent with no related row is absent (zero).
52
+ * Without a limit nothing is cut, and the caller counts what it got.
53
+ */
54
+ totals?: Map<string, number>;
55
+ }
56
+ /**
57
+ * How the batch loaders render a target row.
58
+ *
59
+ * - `"admin"` — the view model the admin panel reads: declared types parsed
60
+ * (`parseDataFromServer`), the target's own to-one relations as
61
+ * `{ __type: "relation" }` refs, undeclared columns dropped.
62
+ * - `"rest"` — the row the target's own route serves (`toRestRow`): what an
63
+ * `include` embeds. The REST contract is that a developer never sees a ref,
64
+ * and an included row is rendered exactly as the same row is at the top
65
+ * level.
66
+ */
67
+ export type RelatedRowStyle = "admin" | "rest";
68
+ /**
69
+ * The order one parent's related rows come in: the include's own keys, then
70
+ * the target's key ascending.
71
+ *
72
+ * The key ascending is the order an unordered batch read handed rows back in
73
+ * on an append-only table, and so the rows a `limit` with no `orderBy` kept —
74
+ * stated now rather than left to the plan. A key that names no column of the
75
+ * target sorts nothing, as it sorted nothing when the sort ran over the
76
+ * returned rows in JavaScript; the caller's field access on it was already
77
+ * judged at the route.
78
+ *
79
+ * NULLs go where a top-level `orderBy` puts them: the stated placement, else
80
+ * Postgres's own (last ascending, first descending).
81
+ */
82
+ export declare function relatedRowOrder(targetTable: PgTable, targetKeys: AnyPgColumn[], orderBy: OrderByTuple[] | undefined): SQL[];
19
83
  /**
20
84
  * Service for handling all relation-related operations.
21
85
  * Handles fetching, updating, and managing row relations.
@@ -101,6 +165,21 @@ export declare class RelationService {
101
165
  * nobody was comparing.
102
166
  */
103
167
  private toRelatedRow;
168
+ /**
169
+ * A target row as its own REST route serves it, for the `"rest"` style.
170
+ *
171
+ * The loaders read the target with `SELECT *`, so the columns a REST read
172
+ * never selects — a generated search vector — are dropped first; then
173
+ * `toRestRow` renders the rest, which applies the target's field access.
174
+ * Built once per batch: the hidden set is a property of the table.
175
+ *
176
+ * It replaces a second pass. An include used to render each target row as
177
+ * the admin's view model (`parseDataFromServer`, which also minted a
178
+ * `{ __type: "relation" }` ref for the target's own to-one relations) and
179
+ * then re-render that for REST: 3.4 µs a row against 1.4 µs for the row
180
+ * itself, and an envelope in a response whose contract has none.
181
+ */
182
+ private restRendering;
104
183
  /**
105
184
  * The junction row's own columns, as the caller may see them.
106
185
  *
@@ -224,13 +303,37 @@ export declare class RelationService {
224
303
  * the rows that come back, because filtering afterwards reads every related
225
304
  * row of every parent in order to discard most of them.
226
305
  */
227
- batchFetchRelatedEntities(parentCollectionPath: string, parentIds: (string | number)[], _relationKey: string, relation: ResolvedRelation, narrow?: SQL): Promise<Map<string, RelatedRow<Record<string, unknown>>>>;
306
+ batchFetchRelatedEntities(parentCollectionPath: string, parentIds: (string | number)[], _relationKey: string, relation: ResolvedRelation, narrow?: SQL, options?: {
307
+ /** How each target row is rendered; see {@link RelatedRowStyle}. */
308
+ style?: RelatedRowStyle;
309
+ /**
310
+ * The parent rows themselves, in `parentIds` order, when the caller
311
+ * holds them. A `belongsTo` reads each one's foreign key off its
312
+ * row rather than selecting it again.
313
+ */
314
+ parentRows?: Record<string, unknown>[];
315
+ }): Promise<Map<string, RelatedRow<Record<string, unknown>>>>;
228
316
  /**
229
317
  * Batch fetch many-cardinality related rows for multiple parent rows.
230
318
  * Returns a Map<parentId, RelatedRow[]> instead of Map<parentId, RelatedRow>.
231
319
  * Uses a single SQL query with IN clause to avoid N+1.
320
+ *
321
+ * `window` is an include's own `limit` and `orderBy`, and both are applied
322
+ * by Postgres. A limit is *per parent*, so it is a rank within each
323
+ * parent's rows rather than a `LIMIT`: a subquery ranks the keys of every
324
+ * matching row — under the same relation condition, `narrow`, soft delete
325
+ * and `beforeQuery` the unlimited read applies — and the read joins back to
326
+ * the ones ranked within the limit. It used to read every related row of
327
+ * every parent and cut the list in JavaScript: 200 rows served out of
328
+ * 200,000 read, parsed and rendered for `{ messages: { limit: 2 } }` on a
329
+ * page of 100 threads. Rows come back in the window's order, so the
330
+ * per-parent lists are already sorted.
232
331
  */
233
332
  batchFetchRelatedEntitiesMany(parentCollectionPath: string, parentIds: (string | number)[], _relationKey: string, relation: ResolvedRelation,
234
333
  /** An extra condition on the target — see {@link batchFetchRelatedEntities}. */
235
- narrow?: SQL): Promise<Map<string, RelatedRow<Record<string, unknown>>[]>>;
334
+ narrow?: SQL, window?: RelatedRowWindow,
335
+ /** `style` — how each target row is rendered; see {@link RelatedRowStyle}. */
336
+ options?: {
337
+ style?: RelatedRowStyle;
338
+ }): Promise<Map<string, RelatedRow<Record<string, unknown>>[]>>;
236
339
  }
@@ -1,25 +1,38 @@
1
+ import { type PgNotifyListenerStatus } from "../pg-notify-listener.js";
1
2
  /**
2
3
  * A single database change captured by the CDC triggers and delivered over the
3
- * `rebase_cdc` NOTIFY channel.
4
+ * `rebase_cdc` NOTIFY channel — one row of a notification, which may name many.
4
5
  */
5
6
  export interface CdcChangeEvent {
6
7
  schema: string;
7
8
  table: string;
8
- op: "INSERT" | "UPDATE" | "DELETE";
9
+ /** `TRUNCATE` changed every row, and `row` is empty. */
10
+ op: "INSERT" | "UPDATE" | "DELETE" | "TRUNCATE";
9
11
  /**
10
- * The changed tuple (NEW for insert/update, OLD for delete). May be a
11
- * partial identity-only object when the full row overflowed the pg_notify
12
- * size cap — see {@link truncated}.
12
+ * The changed row's identity, by column: its key, and for a junction table
13
+ * the two ids naming the child list (from NEW for insert/update, OLD for
14
+ * delete). Never the row's other values — any login can LISTEN; see
15
+ * `buildCdcFunctionSql`.
13
16
  */
14
17
  row: Record<string, unknown>;
15
- /** True when the row was reduced to its identity because it was too large to notify. */
18
+ /** True when even the key was too large to notify, so `row` is empty. */
16
19
  truncated?: boolean;
17
20
  }
18
21
  /**
19
- * Parse a `rebase_cdc` NOTIFY payload. Returns `null` for anything malformed so
20
- * a single bad message can never crash the listener.
22
+ * Parse a `rebase_cdc` NOTIFY payload into the changes it names, one per row.
23
+ * Returns `null` for anything malformed so a single bad message can never
24
+ * crash the listener.
25
+ *
26
+ * Reads every shape a capture trigger sends, whichever version installed it —
27
+ * during a rolling deploy the database's triggers and this instance need not
28
+ * be the same version:
29
+ * - `{ schema, table, op, row }` — one row: the row trigger, and a statement
30
+ * that changed one row;
31
+ * - `{ schema, table, op, cols, keys }` — a statement's rows in a batch. With
32
+ * one column a key is the value; with several, an array in `cols` order;
33
+ * - `{ schema, table, op: "TRUNCATE" }`.
21
34
  */
22
- export declare function parseCdcPayload(payload: string): CdcChangeEvent | null;
35
+ export declare function parseCdcPayload(payload: string): CdcChangeEvent[] | null;
23
36
  /**
24
37
  * Dedicated Postgres LISTEN client for database-level CDC.
25
38
  *
@@ -36,7 +49,7 @@ export declare class CdcListener {
36
49
  * drop. Every change committed in the gap was notified to nobody, so
37
50
  * this is where subscribers are told to look again.
38
51
  */
39
- constructor(connectionString: string, onEvent: (event: CdcChangeEvent) => void | Promise<void>, onReconnect?: () => void);
52
+ constructor(connectionString: string, onEvents: (events: CdcChangeEvent[]) => void | Promise<void>, onReconnect?: () => void, applicationName?: string);
40
53
  /**
41
54
  * Connect and begin listening. Idempotent.
42
55
  *
@@ -49,4 +62,7 @@ export declare class CdcListener {
49
62
  start(): Promise<void>;
50
63
  /** Stop listening and release the connection. */
51
64
  stop(): Promise<void>;
65
+ /** Listening on a connection that answered its last heartbeat — see {@link PgNotifyListener}. */
66
+ get connected(): boolean;
67
+ status(): PgNotifyListenerStatus;
52
68
  }
@@ -0,0 +1,19 @@
1
+ import type { CollectionConfig } from "@rebasepro/types";
2
+ import type { PostgresCollectionRegistry } from "../../collections/PostgresCollectionRegistry.js";
3
+ /** One key field of a collection, and the column it is stored in. */
4
+ export interface KeyColumn {
5
+ fieldName: string;
6
+ columnName: string;
7
+ }
8
+ /**
9
+ * The columns a collection's rows are addressed by.
10
+ *
11
+ * A change notification names its row by column — that is all a trigger sees —
12
+ * and an address is built from the key's *field* names. Both ends read the
13
+ * mapping from here: the provisioner, to attach the trigger with exactly these
14
+ * columns (and nothing else leaves the database), and the consumer, to turn the
15
+ * captured columns back into an address. A key declared as `userId` over
16
+ * `user_id` used to be looked up by field name on the captured row, never
17
+ * found, and every external write reached no single-row subscriber.
18
+ */
19
+ export declare function collectionKeyColumns(collection: CollectionConfig, registry: PostgresCollectionRegistry): KeyColumn[];