@rebasepro/server-postgres 0.17.3 → 0.18.1

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 (204) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +31 -0
  3. package/dist/BranchService-ByLfM_Tt.js +360 -0
  4. package/dist/BranchService-ByLfM_Tt.js.map +1 -0
  5. package/dist/__vite-browser-external-BnuHet1e.js +46 -0
  6. package/dist/__vite-browser-external-BnuHet1e.js.map +1 -0
  7. package/dist/arg-Dni7MzLB.js +87 -0
  8. package/dist/arg-Dni7MzLB.js.map +1 -0
  9. package/dist/auth-users-columns-D2LBFrMH.js +116 -0
  10. package/dist/auth-users-columns-D2LBFrMH.js.map +1 -0
  11. package/dist/backup-argv.d.ts +19 -0
  12. package/dist/backup-cli-oM9WQVlV.js +408 -0
  13. package/dist/backup-cli-oM9WQVlV.js.map +1 -0
  14. package/dist/backup-service-Br3tqwsy.js +715 -0
  15. package/dist/backup-service-Br3tqwsy.js.map +1 -0
  16. package/dist/branch-argv.d.ts +28 -0
  17. package/dist/branch-prune.d.ts +77 -0
  18. package/dist/cli-collections-path.d.ts +31 -0
  19. package/dist/cli-errors-DxN4yzdH.js +657 -0
  20. package/dist/cli-errors-DxN4yzdH.js.map +1 -0
  21. package/dist/cli-errors.d.ts +123 -0
  22. package/dist/cli-flags.d.ts +101 -0
  23. package/dist/cli-helpers.d.ts +94 -10
  24. package/dist/cli-output-CNdMql-L.js +47 -0
  25. package/dist/cli-output-CNdMql-L.js.map +1 -0
  26. package/dist/cli.js +2020 -0
  27. package/dist/cli.js.map +1 -0
  28. package/dist/collection-index-BHgT_TKd.js +276 -0
  29. package/dist/collection-index-BHgT_TKd.js.map +1 -0
  30. package/dist/{collections-schema-version-BMeu3cgv.js → collections-schema-version-T6cGvrDi.js} +1 -2
  31. package/dist/{collections-schema-version-BMeu3cgv.js.map → collections-schema-version-T6cGvrDi.js.map} +1 -1
  32. package/dist/{connection-GOKU3Hu5.js → databasePoolManager-Bj5FbeAs.js} +112 -40
  33. package/dist/databasePoolManager-Bj5FbeAs.js.map +1 -0
  34. package/dist/databasePoolManager.d.ts +27 -0
  35. package/dist/doctor-D7HWmGzs.js +974 -0
  36. package/dist/doctor-D7HWmGzs.js.map +1 -0
  37. package/dist/{ensure-collection-policies-UCqgv_8c.js → ensure-collection-policies-2c2NV8zX.js} +7 -27
  38. package/dist/ensure-collection-policies-2c2NV8zX.js.map +1 -0
  39. package/dist/ensure-collection-tables-D4wJzM4y.js +820 -0
  40. package/dist/ensure-collection-tables-D4wJzM4y.js.map +1 -0
  41. package/dist/ensure-tables-CIojK-Rx.js +650 -0
  42. package/dist/ensure-tables-CIojK-Rx.js.map +1 -0
  43. package/dist/generate-drizzle-schema-CwEgVyjO.js +123 -0
  44. package/dist/generate-drizzle-schema-CwEgVyjO.js.map +1 -0
  45. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js +528 -0
  46. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js.map +1 -0
  47. package/dist/{ensure-collection-tables-DgVixhX3.js → generate-postgres-ddl-logic-CtnpIBVv.js} +126 -984
  48. package/dist/generate-postgres-ddl-logic-CtnpIBVv.js.map +1 -0
  49. package/dist/generated-schema-staleness-DQdfJl2A.js +157 -0
  50. package/dist/generated-schema-staleness-DQdfJl2A.js.map +1 -0
  51. package/dist/index.d.ts +12 -0
  52. package/dist/index.es.js +7423 -1133
  53. package/dist/index.es.js.map +1 -1
  54. package/dist/introspect-db-logic-C6LQdTxj.js +1378 -0
  55. package/dist/introspect-db-logic-C6LQdTxj.js.map +1 -0
  56. package/dist/policy-drift-kNJ6X2Uj.js +313 -0
  57. package/dist/policy-drift-kNJ6X2Uj.js.map +1 -0
  58. package/dist/{backup-service-DCk7KhhL.js → rls-bootstrap-sql-DLDnUjtx.js} +737 -2689
  59. package/dist/rls-bootstrap-sql-DLDnUjtx.js.map +1 -0
  60. package/dist/{rls-enforcement-DvAbL9YJ.js → rls-enforcement-DeYEjYLt.js} +65 -190
  61. package/dist/rls-enforcement-DeYEjYLt.js.map +1 -0
  62. package/dist/schema/bin/generate-drizzle-schema.d.ts +1 -0
  63. package/dist/schema/bin/generate-postgres-ddl.d.ts +1 -0
  64. package/dist/schema/column-type-drift.d.ts +79 -0
  65. package/dist/schema/doctor-cli.js +129 -0
  66. package/dist/schema/doctor-cli.js.map +1 -0
  67. package/dist/schema/doctor.d.ts +42 -0
  68. package/dist/schema/ensure-collection-tables.d.ts +90 -0
  69. package/dist/schema/generate-drizzle-schema.d.ts +1 -1
  70. package/dist/schema/generate-drizzle-schema.js +24 -0
  71. package/dist/schema/generate-drizzle-schema.js.map +1 -0
  72. package/dist/schema/generate-next-step.d.ts +30 -0
  73. package/dist/schema/generate-postgres-ddl-logic.d.ts +28 -0
  74. package/dist/schema/generate-postgres-ddl.d.ts +1 -1
  75. package/dist/schema/generate-postgres-ddl.js +138 -0
  76. package/dist/schema/generate-postgres-ddl.js.map +1 -0
  77. package/dist/schema/generated-schema-staleness.d.ts +37 -0
  78. package/dist/schema/introspect-db-queries.d.ts +34 -0
  79. package/dist/schema/introspect-db.js +866 -0
  80. package/dist/schema/introspect-db.js.map +1 -0
  81. package/dist/security/rls-enforcement.d.ts +63 -2
  82. package/dist/services/BranchService.d.ts +86 -1
  83. package/dist/services/collection-helpers.d.ts +1 -1
  84. package/dist/services/realtimeService.d.ts +30 -2
  85. package/dist/services/row-pipeline.d.ts +12 -1
  86. package/dist/source-Br7L7GOI.js +410 -0
  87. package/dist/source-Br7L7GOI.js.map +1 -0
  88. package/dist/utils/pg-error-utils.d.ts +3 -1
  89. package/dist/utils/pg-numeric-number-patch.d.ts +31 -0
  90. package/dist/vector-index-DkWxm2de.js +309 -0
  91. package/dist/vector-index-DkWxm2de.js.map +1 -0
  92. package/package.json +32 -27
  93. package/dist/backup-service-DCk7KhhL.js.map +0 -1
  94. package/dist/collection-index-BRUg10H5.js +0 -5690
  95. package/dist/collection-index-BRUg10H5.js.map +0 -1
  96. package/dist/connection-GOKU3Hu5.js.map +0 -1
  97. package/dist/ensure-collection-policies-UCqgv_8c.js.map +0 -1
  98. package/dist/ensure-collection-tables-DgVixhX3.js.map +0 -1
  99. package/dist/rls-bootstrap-sql-B5C9LoJ6.js +0 -244
  100. package/dist/rls-bootstrap-sql-B5C9LoJ6.js.map +0 -1
  101. package/dist/rls-enforcement-DvAbL9YJ.js.map +0 -1
  102. package/dist/src-DiB5RP2Z.js +0 -541
  103. package/dist/src-DiB5RP2Z.js.map +0 -1
  104. package/dist/websocket-BZ4H5wUz.js +0 -8198
  105. package/dist/websocket-BZ4H5wUz.js.map +0 -1
  106. package/src/PostgresAdapter.ts +0 -91
  107. package/src/PostgresBackendDriver.ts +0 -1857
  108. package/src/PostgresBootstrapper.ts +0 -1157
  109. package/src/auth/ensure-tables.ts +0 -896
  110. package/src/auth/schema-version.ts +0 -260
  111. package/src/auth/services.ts +0 -1610
  112. package/src/backup/backup-cli.ts +0 -520
  113. package/src/backup/backup-cron.ts +0 -212
  114. package/src/backup/backup-logic.ts +0 -93
  115. package/src/backup/backup-service.ts +0 -485
  116. package/src/backup/index.ts +0 -12
  117. package/src/backup/pg-tools.ts +0 -403
  118. package/src/backup/retention.ts +0 -86
  119. package/src/cli-errors.ts +0 -265
  120. package/src/cli-helpers.ts +0 -569
  121. package/src/cli-output.ts +0 -43
  122. package/src/cli.ts +0 -1339
  123. package/src/collections/PostgresCollectionRegistry.ts +0 -108
  124. package/src/collections/buildRegistry.ts +0 -68
  125. package/src/collections/validate-relations.ts +0 -402
  126. package/src/connection.ts +0 -333
  127. package/src/data-transformer.ts +0 -845
  128. package/src/databasePoolManager.ts +0 -95
  129. package/src/history/HistoryService.ts +0 -254
  130. package/src/history/ensure-history-table.ts +0 -53
  131. package/src/index.ts +0 -19
  132. package/src/interfaces.ts +0 -60
  133. package/src/module-dir.ts +0 -7
  134. package/src/schema/atlas-argv.ts +0 -94
  135. package/src/schema/auth-schema.ts +0 -298
  136. package/src/schema/auth-users-columns.ts +0 -131
  137. package/src/schema/carved-out-migration.ts +0 -404
  138. package/src/schema/classify-change.ts +0 -436
  139. package/src/schema/collection-index.ts +0 -427
  140. package/src/schema/collections-schema-version.ts +0 -103
  141. package/src/schema/destructive-sql.ts +0 -94
  142. package/src/schema/doctor-cli.ts +0 -66
  143. package/src/schema/doctor-policy-checks.ts +0 -105
  144. package/src/schema/doctor.ts +0 -837
  145. package/src/schema/drizzle-ddl.ts +0 -33
  146. package/src/schema/dynamic-tables.test.ts +0 -302
  147. package/src/schema/dynamic-tables.ts +0 -293
  148. package/src/schema/ensure-collection-policies.ts +0 -198
  149. package/src/schema/ensure-collection-tables.test.ts +0 -518
  150. package/src/schema/ensure-collection-tables.ts +0 -1267
  151. package/src/schema/generate-drizzle-schema-logic.ts +0 -957
  152. package/src/schema/generate-drizzle-schema.ts +0 -143
  153. package/src/schema/generate-postgres-ddl-logic.ts +0 -1339
  154. package/src/schema/generate-postgres-ddl.ts +0 -180
  155. package/src/schema/generate-schema-commit.ts +0 -267
  156. package/src/schema/generated-schema-staleness.ts +0 -284
  157. package/src/schema/introspect-db-constraints.ts +0 -385
  158. package/src/schema/introspect-db-inference.ts +0 -255
  159. package/src/schema/introspect-db-logic.ts +0 -1387
  160. package/src/schema/introspect-db-naming.ts +0 -15
  161. package/src/schema/introspect-db-project.ts +0 -78
  162. package/src/schema/introspect-db-queries.ts +0 -326
  163. package/src/schema/introspect-db-structure.ts +0 -670
  164. package/src/schema/introspect-db-types.ts +0 -56
  165. package/src/schema/introspect-db.ts +0 -257
  166. package/src/schema/introspect-runtime.test.ts +0 -260
  167. package/src/schema/introspect-runtime.ts +0 -327
  168. package/src/schema/non-sql-collections.test.ts +0 -131
  169. package/src/schema/rls-bootstrap-sql.ts +0 -288
  170. package/src/schema/search-column.ts +0 -643
  171. package/src/schema/test-schema.ts +0 -11
  172. package/src/schema/vector-index.ts +0 -491
  173. package/src/security/anonymous-grants.test.ts +0 -73
  174. package/src/security/policy-drift.test.ts +0 -435
  175. package/src/security/policy-drift.ts +0 -473
  176. package/src/security/rls-enforcement.ts +0 -505
  177. package/src/services/BranchService.ts +0 -326
  178. package/src/services/FetchService.ts +0 -2115
  179. package/src/services/PersistService.ts +0 -557
  180. package/src/services/RelationService.ts +0 -978
  181. package/src/services/RelationWriteService.ts +0 -653
  182. package/src/services/cdc/CdcListener.ts +0 -103
  183. package/src/services/cdc/junction-tables.ts +0 -91
  184. package/src/services/cdc/trigger-cdc.ts +0 -173
  185. package/src/services/channel-bus/ChannelBus.ts +0 -44
  186. package/src/services/channel-bus/PostgresChannelBus.ts +0 -299
  187. package/src/services/channel-bus/index.ts +0 -114
  188. package/src/services/channel-history.ts +0 -424
  189. package/src/services/channel-presence.ts +0 -185
  190. package/src/services/collection-helpers.ts +0 -420
  191. package/src/services/dataService.ts +0 -256
  192. package/src/services/index.ts +0 -14
  193. package/src/services/junction-writes.ts +0 -295
  194. package/src/services/nested-path.ts +0 -145
  195. package/src/services/pg-notify-listener.ts +0 -151
  196. package/src/services/realtimeService.ts +0 -2545
  197. package/src/services/row-pipeline.ts +0 -240
  198. package/src/services/write-denial.ts +0 -55
  199. package/src/types.ts +0 -4
  200. package/src/utils/connection-string.ts +0 -58
  201. package/src/utils/drizzle-conditions.ts +0 -2743
  202. package/src/utils/pg-array-null-patch.ts +0 -42
  203. package/src/utils/pg-error-utils.ts +0 -391
  204. package/src/websocket.ts +0 -855
@@ -1,240 +0,0 @@
1
- import { CollectionConfig, Property, ResolvedRelation, isManyToMany, type ResolvedVia } from "@rebasepro/types";
2
- import { resolveCollectionRelations, findRelation, createRelationRefWithData } from "@rebasepro/common";
3
- import { normalizeDbValues } from "../data-transformer";
4
- import { deriveRowAddress } from "./collection-helpers";
5
- import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
6
-
7
- /**
8
- * Turning a drizzle result into a row we serve.
9
- *
10
- * There are two shapes, and they are the same walk. Both take a row whose
11
- * relation fields hold nested objects, both unwrap junction rows to reach the
12
- * target behind them, and both leave every other column alone. They differ only
13
- * in what they put where the relation was:
14
- *
15
- * - `"ref"` — a `{ id, path, __type: "relation" }` reference carrying the
16
- * target's values. This is what the admin renders.
17
- * - `"inline"` — the target's own columns, flat. This is what REST serves, and
18
- * — since the in-process SDK reads through the same pipeline — what
19
- * `rebase.data` / `context.data` serve too. A developer never sees a ref.
20
- *
21
- * They used to be two functions that happened to agree, and the agreement was
22
- * not enforced by anything: the row-identity bug had to be fixed five times
23
- * across differently-shaped copies of this walk, and one of the copies was
24
- * dead code nobody had noticed. Whatever the next cross-cutting change is, it
25
- * is one edit here.
26
- */
27
- export type RelationStyle = "ref" | "inline";
28
-
29
- /**
30
- * Whether a many-relation reaches its target through a junction table.
31
- *
32
- * Also used to build the drizzle `with` config, which is why it is exported:
33
- * the query has to nest one level deeper for a junction, and the row walk has
34
- * to unwrap that same level back out.
35
- */
36
- export function isJunctionRelation(relation: ResolvedRelation): boolean {
37
- // An explicit `through` says so outright.
38
- if (isManyToMany(relation)) return true;
39
- // A multi-hop join path is the same thing spelled longhand.
40
- return relation.kind === "via" && relation.joinPath.length > 1;
41
- }
42
-
43
- /**
44
- * Reach the target row inside a junction row.
45
- *
46
- * A junction row looks like `{ post_id: 1, tag_id: { id: 5, name: "ts" } }` —
47
- * the foreign keys, and the target nested under one of them. The target is the
48
- * only object among them, so that is how it is found. A junction row that has
49
- * not been nested (no `with` on the join) has no object and is returned as-is.
50
- */
51
- function unwrapJunctionRow(item: Record<string, unknown>): Record<string, unknown> {
52
- const nestedKey = Object.keys(item).find(
53
- key => typeof item[key] === "object" && item[key] !== null && !Array.isArray(item[key])
54
- );
55
- return nestedKey ? item[nestedKey] as Record<string, unknown> : item;
56
- }
57
-
58
- /**
59
- * Give back the number a `number` property was declared to be.
60
- *
61
- * Postgres returns NUMERIC as a string and drizzle keeps it that way, since a
62
- * numeric can hold more precision than a double. But the collection declared
63
- * this property `number` and the OpenAPI spec this server publishes says
64
- * `type: number`, so serving `"9.99"` breaks its own contract — and breaks it
65
- * asymmetrically, because a create answers with the number and the read that
66
- * follows answers with the string. Any client that multiplies a price works
67
- * until the first refresh.
68
- *
69
- * Declaring a property `number` already accepts double precision — that is what
70
- * the admin has always parsed it to. Columns REST serves that no property
71
- * declares are left exactly as the database returned them.
72
- */
73
- function coerceDeclaredNumber(value: unknown, property: Property | undefined): unknown {
74
- if (property?.type !== "number" || typeof value !== "string") return value;
75
- const parsed = parseFloat(value);
76
- return isNaN(parsed) ? null : parsed;
77
- }
78
-
79
- /** Apply {@link coerceDeclaredNumber} across a row, leaving every other column alone. */
80
- function coerceDeclaredNumbers(
81
- row: Record<string, unknown>,
82
- collection: CollectionConfig
83
- ): Record<string, unknown> {
84
- const properties = collection.properties;
85
- if (!properties) return row;
86
-
87
- const out: Record<string, unknown> = {};
88
- for (const [key, value] of Object.entries(row)) {
89
- out[key] = coerceDeclaredNumber(value, properties[key] as Property | undefined);
90
- }
91
- return out;
92
- }
93
-
94
- /** Render one target row in the requested style. */
95
- /**
96
- * Drop every column the collection marked `excludeFromApi`.
97
- *
98
- * Password hashes and verification tokens have to be readable server-side but
99
- * must never reach a client — and "never" has to mean every exit from this
100
- * pipeline, including relation targets, or a secret leaks through whichever
101
- * path was overlooked. Keyed by both the property name and its column name,
102
- * since a row can arrive keyed either way depending on the caller.
103
- */
104
- function stripExcluded(
105
- row: Record<string, unknown>,
106
- collection: CollectionConfig
107
- ): Record<string, unknown> {
108
- const properties = collection.properties as Record<string, Property> | undefined;
109
- if (!properties) return row;
110
-
111
- for (const [key, property] of Object.entries(properties)) {
112
- if (!property?.excludeFromApi) continue;
113
- delete row[key];
114
- if (property.columnName) delete row[property.columnName];
115
- }
116
- return row;
117
- }
118
-
119
- function renderTarget(
120
- targetRow: Record<string, unknown>,
121
- targetCollection: CollectionConfig,
122
- style: RelationStyle,
123
- registry: PostgresCollectionRegistry
124
- ): unknown {
125
- if (style === "inline") {
126
- // The target's columns, and only those: its address is the consumer's
127
- // to derive, and merging one in overwrites a real `id` column.
128
- return stripExcluded(coerceDeclaredNumbers({ ...targetRow }, targetCollection), targetCollection);
129
- }
130
-
131
- const address = relationTargetAddress(targetRow, targetCollection, registry);
132
- const path = targetCollection.slug;
133
- return createRelationRefWithData(address, path, {
134
- id: address,
135
- path,
136
- values: normalizeDbValues(targetRow, targetCollection)
137
- });
138
- }
139
-
140
- /**
141
- * The address a relation ref points at.
142
- *
143
- * The whole key, not its first column: a composite-keyed target addressed by
144
- * `tenant_id` alone points at every row that shares it. A target whose key
145
- * cannot be resolved at all used to throw here — reading `[0]` of an empty
146
- * array — taking down the parent's fetch over a relation it may not even have
147
- * asked for. The first column is a guess, but a ref that resolves to nothing
148
- * beats no rows at all.
149
- */
150
- export function relationTargetAddress(
151
- targetRow: Record<string, unknown>,
152
- targetCollection: CollectionConfig,
153
- registry: PostgresCollectionRegistry
154
- ): string {
155
- const address = deriveRowAddress(targetRow, targetCollection, registry);
156
- if (address) return address;
157
- return String(targetRow[Object.keys(targetRow)[0]] ?? "");
158
- }
159
-
160
- /**
161
- * The row the admin renders: every column, with relations as references.
162
- *
163
- * Values are normalized (dates, numbers, NaN) because the admin's view-model
164
- * expects real types. The row's own address is *not* among the columns — it is
165
- * derived by the consumer from the collection's primary keys.
166
- */
167
- export function toFlatRow(
168
- row: Record<string, unknown>,
169
- collection: CollectionConfig,
170
- registry: PostgresCollectionRegistry
171
- ): Record<string, unknown> {
172
- const resolvedRelations = resolveCollectionRelations(collection);
173
- const normalized = normalizeDbValues(row, collection) as Record<string, unknown>;
174
-
175
- // Keyed by relation, reading the drizzle relation name off the raw row: the
176
- // relation's property key and the name drizzle nested it under are not
177
- // always the same, and the value is written back under the property key.
178
- for (const [key, relation] of Object.entries(resolvedRelations)) {
179
- const relData = row[relation.relationName || key];
180
- if (relData === undefined || relData === null) continue;
181
-
182
- if (relation.cardinality === "many" && Array.isArray(relData)) {
183
- const targetCollection = relation.target();
184
- normalized[key] = relData.map((item: Record<string, unknown>) =>
185
- renderTarget(
186
- isJunctionRelation(relation) ? unwrapJunctionRow(item) : item,
187
- targetCollection,
188
- "ref",
189
- registry
190
- ));
191
- } else if (relation.cardinality === "one" && typeof relData === "object" && !Array.isArray(relData)) {
192
- normalized[key] = renderTarget(relData as Record<string, unknown>, relation.target(), "ref", registry);
193
- }
194
- }
195
-
196
- return stripExcluded(normalized, collection);
197
- }
198
-
199
- /**
200
- * The row REST serves: every column under its own name, with the value Postgres
201
- * returned, and relations inlined as the target's columns.
202
- *
203
- * Values are the ones the database returned, except where that contradicts the
204
- * declared type: a `number` property is served as a number (see
205
- * {@link coerceDeclaredNumber}). Dates stay as the database returned them —
206
- * JSON has its own opinions about dates that the admin's view-model does not
207
- * share.
208
- *
209
- * Keyed by the row rather than by the relation list — a REST fetch only loads
210
- * the relations `include` asked for, so the row is the authority on which are
211
- * actually there.
212
- */
213
- export function toRestRow(
214
- row: Record<string, unknown>,
215
- collection: CollectionConfig,
216
- registry: PostgresCollectionRegistry
217
- ): Record<string, unknown> {
218
- const resolvedRelations = resolveCollectionRelations(collection);
219
- const flat: Record<string, unknown> = {};
220
-
221
- for (const [key, value] of Object.entries(row)) {
222
- const relation = findRelation(resolvedRelations, key);
223
-
224
- if (relation && Array.isArray(value)) {
225
- flat[key] = value.map((item: Record<string, unknown>) =>
226
- renderTarget(
227
- isJunctionRelation(relation) ? unwrapJunctionRow(item) : item,
228
- relation.target(),
229
- "inline",
230
- registry
231
- ));
232
- } else if (relation && typeof value === "object" && value !== null) {
233
- flat[key] = renderTarget(value as Record<string, unknown>, relation.target(), "inline", registry);
234
- } else {
235
- flat[key] = coerceDeclaredNumber(value, collection.properties?.[key] as Property | undefined);
236
- }
237
- }
238
-
239
- return stripExcluded(flat, collection);
240
- }
@@ -1,55 +0,0 @@
1
- import { and, sql, SQL } from "drizzle-orm";
2
- import { PgTable } from "drizzle-orm/pg-core";
3
- import { ApiError } from "@rebasepro/server";
4
- import { DrizzleClient } from "../interfaces";
5
-
6
- /**
7
- * Explain a write that matched no rows.
8
- *
9
- * Row-level security filters UPDATE and DELETE through the policy's USING
10
- * clause instead of raising: a denied write is reported by Postgres exactly
11
- * like a successful one that happened to match nothing. Left unchecked, a
12
- * caller cannot tell "denied" from "done" — the write returns 200/204 and the
13
- * row is untouched. An agent handed a key with `orders:delete` and no delete
14
- * policy is the case that makes it concrete: it deletes nothing, forever, and
15
- * is told it worked every time.
16
- *
17
- * Re-reading the target over the *same* RLS-scoped handle separates the two
18
- * cases. A visible row means the policy rejected the write (403); an invisible
19
- * one means there is nothing there to write for this caller (404, matching what
20
- * a GET would say). The re-read is bound by the caller's own policies, so it
21
- * discloses nothing a plain read wouldn't.
22
- *
23
- * Only reached when zero rows matched, so the happy path pays nothing.
24
- *
25
- * It lives here, rather than beside its first caller, because every zero-row
26
- * write has to answer the same question and answer it identically: the rule
27
- * that a readable-but-unwritable target is a 403 is the contract, and a second
28
- * copy of it is a second chance to get it wrong.
29
- *
30
- * @param handle The RLS-scoped connection the write ran on — not a fresh
31
- * one, or the re-read would answer for a different caller.
32
- * @param table The table the write targeted (the junction, for a link).
33
- * @param conditions The write's own WHERE terms, reused verbatim.
34
- * @param denied Message for the 403: the target is there and was refused.
35
- * @param missing Message for the 404: there is nothing there for this caller.
36
- */
37
- export async function explainZeroRowWrite(
38
- handle: DrizzleClient,
39
- table: PgTable,
40
- conditions: SQL[],
41
- denied: string,
42
- missing: string
43
- ): Promise<ApiError> {
44
- const visible = await handle
45
- .select({ present: sql<number>`1` })
46
- .from(table)
47
- .where(and(...conditions))
48
- .limit(1);
49
-
50
- if (visible.length > 0) {
51
- return ApiError.forbidden(denied, "WRITE_DENIED");
52
- }
53
-
54
- return ApiError.notFound(missing);
55
- }
package/src/types.ts DELETED
@@ -1,4 +0,0 @@
1
- import type { PgTable, AnyPgColumn } from "drizzle-orm/pg-core";
2
-
3
- /** Drizzle PgTable with column access by name. Runtime Drizzle tables satisfy this shape. */
4
- export type RebasePgTable = PgTable & Record<string, AnyPgColumn>;
@@ -1,58 +0,0 @@
1
- /**
2
- * Connection-string rewrites for the tools Rebase shells out to.
3
- *
4
- * Pure and dependency-free: `pg-tools` builds argument vectors without a live
5
- * server, and these have to be usable from there.
6
- */
7
-
8
- /**
9
- * The `sslmode` values libpq accepts. `no-verify` is conspicuously not one.
10
- */
11
- const LIBPQ_SSLMODES = new Set(["disable", "allow", "prefer", "require", "verify-ca", "verify-full"]);
12
-
13
- /**
14
- * Make a connection string safe to hand to a libpq program.
15
- *
16
- * `sslmode=no-verify` is a node-postgres convention — encrypt, but do not check
17
- * the certificate — and libpq does not have it. It does not degrade or warn:
18
- *
19
- * psql: error: invalid sslmode value: "no-verify"
20
- *
21
- * which means a `DATABASE_URL` that works perfectly for the app makes `psql`,
22
- * `pg_dump`, `pg_restore` and Atlas all refuse to start, with an error that
23
- * points at the value rather than at the convention it comes from. It has cost
24
- * this project time more than once.
25
- *
26
- * `require` is the honest translation: libpq's `require` encrypts and does not
27
- * verify the certificate either, which is exactly what `no-verify` asks for.
28
- * Nothing is relaxed by the rewrite — `verify-ca` and `verify-full` are left
29
- * alone, so a connection string that asked for verification still gets it.
30
- *
31
- * Only `sslmode` is touched, and only when it is a value libpq would reject.
32
- * Anything unparseable is returned as given: a connection that works
33
- * unverified beats one this corrupted.
34
- */
35
- export function forLibpq(connectionString: string): string {
36
- let url: URL;
37
- try {
38
- url = new URL(connectionString);
39
- } catch {
40
- // Key/value DSNs and anything else we cannot parse. A `sslmode` in one
41
- // of those is space-separated rather than a query parameter, and
42
- // rewriting it with a regex is how a password containing "sslmode="
43
- // gets mangled.
44
- return connectionString;
45
- }
46
- if (url.protocol !== "postgres:" && url.protocol !== "postgresql:") return connectionString;
47
-
48
- const sslmode = url.searchParams.get("sslmode");
49
- if (sslmode === null || LIBPQ_SSLMODES.has(sslmode)) return connectionString;
50
-
51
- url.searchParams.set("sslmode", "require");
52
- // Re-serialize by hand for the reason `pinSearchPath` does: URLSearchParams
53
- // writes a space as `+`, which node-postgres decodes and libpq does not.
54
- url.search = Array.from(url.searchParams.entries())
55
- .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
56
- .join("&");
57
- return url.toString();
58
- }