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

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 (180) hide show
  1. package/dist/{BranchService-DGPL6G_C.js → BranchService-Bsn2wfaj.js} +36 -18
  2. package/dist/BranchService-Bsn2wfaj.js.map +1 -0
  3. package/dist/PostgresBackendDriver.d.ts +166 -13
  4. package/dist/auth/ensure-tables.d.ts +12 -0
  5. package/dist/auth/services.d.ts +78 -14
  6. package/dist/{auth-users-columns-D2LBFrMH.js → auth-users-columns-C72EMoDJ.js} +17 -1
  7. package/dist/{auth-users-columns-D2LBFrMH.js.map → auth-users-columns-C72EMoDJ.js.map} +1 -1
  8. package/dist/backup/backup-cli.d.ts +14 -0
  9. package/dist/backup/backup-service.d.ts +2 -2
  10. package/dist/backup/pg-binary.d.ts +5 -0
  11. package/dist/{backup-cli-24v4OlSp.js → backup-cli-aUfRv16h.js} +37 -29
  12. package/dist/backup-cli-aUfRv16h.js.map +1 -0
  13. package/dist/{backup-service-HQ9GC4tN.js → backup-service-BTBB7RXn.js} +60 -28
  14. package/dist/backup-service-BTBB7RXn.js.map +1 -0
  15. package/dist/boot-ddl-DVpsFN6r.js +105 -0
  16. package/dist/boot-ddl-DVpsFN6r.js.map +1 -0
  17. package/dist/cli-errors-CaI4ntyD.js +1919 -0
  18. package/dist/cli-errors-CaI4ntyD.js.map +1 -0
  19. package/dist/cli-errors.d.ts +93 -5
  20. package/dist/{cli-flags-BglvpjHv.js → cli-flags-B-Dawocl.js} +5 -2
  21. package/dist/cli-flags-B-Dawocl.js.map +1 -0
  22. package/dist/cli-helpers.d.ts +52 -12
  23. package/dist/cli-output.d.ts +16 -28
  24. package/dist/cli-push.d.ts +87 -0
  25. package/dist/cli.js +625 -190
  26. package/dist/cli.js.map +1 -1
  27. package/dist/{column-plan-helpers-DF-8dTVa.js → column-plan-helpers-drZIsSNf.js} +42 -26
  28. package/dist/column-plan-helpers-drZIsSNf.js.map +1 -0
  29. package/dist/{databasePoolManager-Bj5FbeAs.js → connection-BPoNILnD.js} +19 -110
  30. package/dist/connection-BPoNILnD.js.map +1 -0
  31. package/dist/connection.d.ts +9 -1
  32. package/dist/data-transformer.d.ts +0 -8
  33. package/dist/databasePoolManager-BkEy2w18.js +119 -0
  34. package/dist/databasePoolManager-BkEy2w18.js.map +1 -0
  35. package/dist/deferred-index-build-DP1_SNFo.js +1051 -0
  36. package/dist/deferred-index-build-DP1_SNFo.js.map +1 -0
  37. package/dist/{doctor-Cb2thZ8s.js → doctor-C4WiftHm.js} +279 -87
  38. package/dist/doctor-C4WiftHm.js.map +1 -0
  39. package/dist/ensure-collection-policies-CXpbeNag.js +541 -0
  40. package/dist/ensure-collection-policies-CXpbeNag.js.map +1 -0
  41. package/dist/ensure-collection-tables-BhsU6qUE.js +532 -0
  42. package/dist/ensure-collection-tables-BhsU6qUE.js.map +1 -0
  43. package/dist/{ensure-tables-Dhn9KM3B.js → ensure-tables-CHsgyLqg.js} +178 -75
  44. package/dist/ensure-tables-CHsgyLqg.js.map +1 -0
  45. package/dist/enum-labels-kept-D8fI1MoX.js +56 -0
  46. package/dist/enum-labels-kept-D8fI1MoX.js.map +1 -0
  47. package/dist/generate-drizzle-schema-Bz52jH2h.js +57 -0
  48. package/dist/generate-drizzle-schema-Bz52jH2h.js.map +1 -0
  49. package/dist/{generate-drizzle-schema-logic-BfzK7UQd.js → generate-drizzle-schema-logic-B74WN37S.js} +28 -10
  50. package/dist/generate-drizzle-schema-logic-B74WN37S.js.map +1 -0
  51. package/dist/generate-files-CdEckX6c.js +199 -0
  52. package/dist/generate-files-CdEckX6c.js.map +1 -0
  53. package/dist/generate-postgres-ddl-logic-B64O2oZF.js +152 -0
  54. package/dist/generate-postgres-ddl-logic-B64O2oZF.js.map +1 -0
  55. package/dist/generated-sql.d.ts +34 -0
  56. package/dist/history/HistoryService.d.ts +38 -6
  57. package/dist/history/ensure-history-table.d.ts +42 -4
  58. package/dist/index.es.js +5557 -7612
  59. package/dist/index.es.js.map +1 -1
  60. package/dist/{introspect-db-logic-WMuAfxvw.js → introspect-db-logic-C-Di8dA9.js} +562 -350
  61. package/dist/introspect-db-logic-C-Di8dA9.js.map +1 -0
  62. package/dist/introspect-db-queries-D6kAqReI.js +339 -0
  63. package/dist/introspect-db-queries-D6kAqReI.js.map +1 -0
  64. package/dist/pg-error-utils-vJezlXdI.js +388 -0
  65. package/dist/pg-error-utils-vJezlXdI.js.map +1 -0
  66. package/dist/{plan-schema-C0fxM8dY.js → plan-schema-B1YF_tYq.js} +278 -533
  67. package/dist/plan-schema-B1YF_tYq.js.map +1 -0
  68. package/dist/{policy-drift-DljYdrpW.js → policy-drift-Dm8i-s1J.js} +24 -7
  69. package/dist/policy-drift-Dm8i-s1J.js.map +1 -0
  70. package/dist/push-index-prebuild-Db4AQF4i.js +45 -0
  71. package/dist/push-index-prebuild-Db4AQF4i.js.map +1 -0
  72. package/dist/push-legacy-renames-BK6ilQq2.js +34 -0
  73. package/dist/push-legacy-renames-BK6ilQq2.js.map +1 -0
  74. package/dist/{generate-postgres-ddl-logic-Bt2d2mRH.js → render-ddl-CtC3y9Ol.js} +17 -151
  75. package/dist/render-ddl-CtC3y9Ol.js.map +1 -0
  76. package/dist/{rls-bootstrap-sql-B8EclDyM.js → rls-bootstrap-sql-RQUljnJ1.js} +125 -1046
  77. package/dist/rls-bootstrap-sql-RQUljnJ1.js.map +1 -0
  78. package/dist/{rls-enforcement-C6Xk0lA6.js → rls-enforcement-DvzqlaL1.js} +137 -17
  79. package/dist/rls-enforcement-DvzqlaL1.js.map +1 -0
  80. package/dist/schema/auth-schema.d.ts +172 -2
  81. package/dist/schema/boot-ddl.d.ts +47 -0
  82. package/dist/schema/classify-change.d.ts +29 -1
  83. package/dist/schema/collection-index.d.ts +45 -0
  84. package/dist/schema/column-plan-helpers.d.ts +38 -21
  85. package/dist/schema/deferred-index-build.d.ts +131 -0
  86. package/dist/schema/destructive-sql.d.ts +27 -1
  87. package/dist/schema/doctor-cli.js +110 -6
  88. package/dist/schema/doctor-cli.js.map +1 -1
  89. package/dist/schema/doctor-search-scale.d.ts +45 -0
  90. package/dist/schema/doctor.d.ts +53 -1
  91. package/dist/schema/ensure-collection-policies.d.ts +95 -35
  92. package/dist/schema/ensure-collection-tables.d.ts +56 -3
  93. package/dist/schema/enum-labels-kept.d.ts +26 -0
  94. package/dist/schema/generate-drizzle-schema.js +1 -1
  95. package/dist/schema/generate-files.d.ts +44 -0
  96. package/dist/schema/generate-next-step.d.ts +0 -13
  97. package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -5
  98. package/dist/schema/generate-postgres-ddl.js +10 -81
  99. package/dist/schema/generate-postgres-ddl.js.map +1 -1
  100. package/dist/schema/generate-schema-commit.d.ts +12 -0
  101. package/dist/schema/introspect-db-logic.d.ts +43 -0
  102. package/dist/schema/introspect-db-queries.d.ts +12 -2
  103. package/dist/schema/introspect-db-search.d.ts +22 -0
  104. package/dist/schema/introspect-db-storage.d.ts +83 -0
  105. package/dist/schema/introspect-db-structure.d.ts +10 -0
  106. package/dist/schema/introspect-db.js +17 -6
  107. package/dist/schema/introspect-db.js.map +1 -1
  108. package/dist/schema/migration-post-process.d.ts +34 -0
  109. package/dist/schema/plan/diff-plan.d.ts +127 -1
  110. package/dist/schema/plan/plan-schema.d.ts +16 -8
  111. package/dist/schema/plan/render-ddl.d.ts +6 -0
  112. package/dist/schema/plan/types.d.ts +45 -11
  113. package/dist/schema/push-index-prebuild.d.ts +25 -0
  114. package/dist/schema/push-legacy-renames.d.ts +20 -0
  115. package/dist/schema/rls-bootstrap-sql.d.ts +15 -0
  116. package/dist/search-column-BM-GV6vH.js +442 -0
  117. package/dist/search-column-BM-GV6vH.js.map +1 -0
  118. package/dist/security/policy-drift.d.ts +1 -1
  119. package/dist/security/rls-enforcement.d.ts +39 -4
  120. package/dist/services/BranchService.d.ts +22 -1
  121. package/dist/services/FetchService.d.ts +63 -12
  122. package/dist/services/PersistService.d.ts +15 -0
  123. package/dist/services/RelationService.d.ts +106 -3
  124. package/dist/services/cdc/CdcListener.d.ts +26 -10
  125. package/dist/services/cdc/identity-columns.d.ts +19 -0
  126. package/dist/services/cdc/trigger-cdc.d.ts +180 -18
  127. package/dist/services/channel-bus/PostgresChannelBus.d.ts +12 -7
  128. package/dist/services/channel-history.d.ts +32 -5
  129. package/dist/services/collection-helpers.d.ts +26 -1
  130. package/dist/services/field-viewer.d.ts +15 -1
  131. package/dist/services/pg-notify-listener.d.ts +95 -6
  132. package/dist/services/realtimeService.d.ts +467 -53
  133. package/dist/services/row-pipeline.d.ts +16 -9
  134. package/dist/services/socket-liveness.d.ts +85 -0
  135. package/dist/services/soft-delete.d.ts +12 -0
  136. package/dist/services/sql-script.d.ts +275 -0
  137. package/dist/services/vector-iterative-scan.d.ts +40 -0
  138. package/dist/services/write-depth.d.ts +22 -0
  139. package/dist/services/write-transaction-scope.d.ts +15 -3
  140. package/dist/socket-liveness-D3UD5_BW.js +7570 -0
  141. package/dist/socket-liveness-D3UD5_BW.js.map +1 -0
  142. package/dist/socket-opt-ins.d.ts +64 -0
  143. package/dist/source-DBDbBTL4.js +811 -0
  144. package/dist/source-DBDbBTL4.js.map +1 -0
  145. package/dist/tsx-loader.d.ts +14 -0
  146. package/dist/utils/drizzle-conditions.d.ts +63 -3
  147. package/dist/utils/error-cause.d.ts +7 -0
  148. package/dist/utils/pg-error-utils.d.ts +2 -4
  149. package/dist/utils/result-rows.d.ts +10 -0
  150. package/dist/websocket.d.ts +63 -18
  151. package/package.json +7 -7
  152. package/dist/BranchService-DGPL6G_C.js.map +0 -1
  153. package/dist/backup-cli-24v4OlSp.js.map +0 -1
  154. package/dist/backup-service-HQ9GC4tN.js.map +0 -1
  155. package/dist/cli-errors-B8qHg02P.js +0 -798
  156. package/dist/cli-errors-B8qHg02P.js.map +0 -1
  157. package/dist/cli-flags-BglvpjHv.js.map +0 -1
  158. package/dist/cli-output-CNdMql-L.js +0 -47
  159. package/dist/cli-output-CNdMql-L.js.map +0 -1
  160. package/dist/collection-index-DdnsxiJ_.js +0 -286
  161. package/dist/collection-index-DdnsxiJ_.js.map +0 -1
  162. package/dist/column-plan-helpers-DF-8dTVa.js.map +0 -1
  163. package/dist/databasePoolManager-Bj5FbeAs.js.map +0 -1
  164. package/dist/doctor-Cb2thZ8s.js.map +0 -1
  165. package/dist/ensure-collection-policies-RHUcEp4v.js +0 -105
  166. package/dist/ensure-collection-policies-RHUcEp4v.js.map +0 -1
  167. package/dist/ensure-collection-tables-BEEjn5cn.js +0 -857
  168. package/dist/ensure-collection-tables-BEEjn5cn.js.map +0 -1
  169. package/dist/ensure-tables-Dhn9KM3B.js.map +0 -1
  170. package/dist/generate-drizzle-schema-logic-BfzK7UQd.js.map +0 -1
  171. package/dist/generate-drizzle-schema-yzY_BLhr.js +0 -123
  172. package/dist/generate-drizzle-schema-yzY_BLhr.js.map +0 -1
  173. package/dist/generate-postgres-ddl-logic-Bt2d2mRH.js.map +0 -1
  174. package/dist/introspect-db-logic-WMuAfxvw.js.map +0 -1
  175. package/dist/plan-schema-C0fxM8dY.js.map +0 -1
  176. package/dist/policy-drift-DljYdrpW.js.map +0 -1
  177. package/dist/rls-bootstrap-sql-B8EclDyM.js.map +0 -1
  178. package/dist/rls-enforcement-C6Xk0lA6.js.map +0 -1
  179. package/dist/source-Br7L7GOI.js +0 -410
  180. package/dist/source-Br7L7GOI.js.map +0 -1
@@ -1,54 +1,20 @@
1
1
  import { createRequire as __createRequire } from "module";
2
2
  __createRequire(import.meta.url);
3
3
  import { r as __exportAll } from "./__vite-browser-external-BnuHet1e.js";
4
- import { t as columnPgType } from "./plan-schema-C0fxM8dY.js";
5
- import { i as diagnoseDbError, n as deepestErrorMessage, s as parseHostInfo } from "./cli-errors-B8qHg02P.js";
6
- import { t as generateSchema } from "./generate-drizzle-schema-logic-BfzK7UQd.js";
7
- import { n as outError, t as out } from "./cli-output-CNdMql-L.js";
8
- import { t as chalk } from "./source-Br7L7GOI.js";
4
+ import { i as diagnoseDbError, n as deepestErrorMessage, s as parseHostInfo } from "./cli-errors-CaI4ntyD.js";
5
+ import { i as outError, r as out, t as chalk } from "./source-DBDbBTL4.js";
6
+ import { n as planSchema, t as columnPgType } from "./plan-schema-B1YF_tYq.js";
7
+ import { t as generateSchema } from "./generate-drizzle-schema-logic-B74WN37S.js";
8
+ import { r as renderPgType } from "./render-ddl-CtC3y9Ol.js";
9
9
  import { n as describeDeclarationDifferences, t as compareGeneratedDeclarations } from "./generated-schema-staleness-Di9c4ZxN.js";
10
10
  import { loadCollectionsFromDirectory } from "@rebasepro/server";
11
11
  import { computeSchemaVersion, getDeclaredSubcollections, isManyToMany, isPostgresCollectionConfig } from "@rebasepro/types";
12
- import { effectiveAccess, fieldKeyForColumn, findRelation, getTableName, isRelationRequired, relationalCollections, resolveCollectionRelations, resolveJunctionSpecs, sortCollectionsBySlug } from "@rebasepro/common";
13
- import { toSnakeCase } from "@rebasepro/utils";
12
+ import { effectiveAccess, fieldKeyForColumn, findRelation, getTableName, isRelationRequired, relationalCollections, resolveCollectionRelations, resolveJunctionSpecs, sortCollectionsBySlug, toCollectionAccessor, toSafeIdentifier } from "@rebasepro/common";
13
+ import { toPostgresIdentifier, toSnakeCase } from "@rebasepro/utils";
14
14
  import * as fs$1 from "fs";
15
15
  import { promises } from "fs";
16
16
  //#region ../codegen/dist/index.es.js
17
17
  /**
18
- * Utility functions for the SDK generator
19
- */
20
- /**
21
- * Convert a slug/snake_case string to PascalCase
22
- * e.g. "private_notes" → "PrivateNotes"
23
- *
24
- * Capitals already inside a word are meaningful and are kept: lowercasing the
25
- * tail of every chunk turned "TestEntities" into "Testentities", which is what
26
- * ended up in the generated type names. SHOUTING_CASE is the one shape where
27
- * the tail is not meaningful, so it is folded down.
28
- */
29
- function toPascalCase(str) {
30
- return str.split(/[_\-\s]+/).filter(Boolean).map((word) => {
31
- const rest = /^[A-Z0-9]+$/.test(word) ? word.slice(1).toLowerCase() : word.slice(1);
32
- return word.charAt(0).toUpperCase() + rest;
33
- }).join("");
34
- }
35
- /**
36
- * Convert a slug/snake_case string to camelCase
37
- * e.g. "private_notes" → "privateNotes"
38
- */
39
- function toCamelCase(str) {
40
- if (!/[_\-\s]/.test(str)) return str.charAt(0).toLowerCase() + str.slice(1);
41
- const pascal = toPascalCase(str);
42
- return pascal.charAt(0).toLowerCase() + pascal.slice(1);
43
- }
44
- /**
45
- * Convert a slug to a safe JS identifier
46
- * e.g. "private-notes" → "privateNotes"
47
- */
48
- function toSafeIdentifier(str) {
49
- return toCamelCase(str.replace(/[^a-zA-Z0-9_]/g, "_"));
50
- }
51
- /**
52
18
  * A schema that cannot be expressed as a valid TypeScript file.
53
19
  *
54
20
  * Thrown rather than emitted. The generator used to concatenate whatever it was
@@ -201,7 +167,9 @@ function includedRelationType(relation, accessors) {
201
167
  const slug = resolveTargetCollection(relation)?.slug ?? relation.targetSlug;
202
168
  const accessor = slug ? accessors.get(slug) : void 0;
203
169
  const rowType = accessor ? `Database[${emitString(accessor)}]["Row"]` : "Record<string, unknown>";
204
- return relation.cardinality === "many" ? `Array<${rowType}>` : rowType;
170
+ const pivot = relation.kind === "manyToMany" ? pivotType(relation.through.properties, "read") : void 0;
171
+ const elementType = pivot ? `${rowType} & { _pivot?: ${pivot} }` : rowType;
172
+ return relation.cardinality === "many" ? `Array<${elementType}>` : elementType;
205
173
  }
206
174
  /**
207
175
  * Map every slug to the property name it is reachable under on `client.data`.
@@ -221,13 +189,29 @@ function buildAccessors(collections) {
221
189
  if (typeof slug !== "string" || slug.length === 0) throw new CodegenError("A collection has no slug, so it has no name to generate a type for. Every collection needs a unique `slug`.");
222
190
  const safe = toSafeIdentifier(slug);
223
191
  if (safe.length === 0) throw new CodegenError(`The slug ${emitString(slug)} has no characters that can form a property name, so it cannot be reached as \`client.data.<name>\`. Use a slug containing letters, digits, underscores or dashes.`);
224
- const existing = bySafeName.get(safe);
225
- if (existing !== void 0) throw new CodegenError(`The collections ${emitString(existing)} and ${emitString(slug)} both generate the accessor "${safe}", so only one of them could be reached from the generated client and the other's reads would silently go to the wrong table. Rename one of the slugs.`);
226
- bySafeName.set(safe, slug);
227
- accessors.set(slug, safe);
192
+ const accessor = toCollectionAccessor(slug);
193
+ if (accessor !== safe) console.warn(`[codegen] The collection ${emitString(slug)} would be \`client.data.${safe}\`, a name the client reserves, so its typed accessor is \`client.data.${accessor}\`. \`client.data.collection(${emitString(slug)})\` reaches it too.`);
194
+ const existing = bySafeName.get(accessor);
195
+ if (existing !== void 0) throw new CodegenError(`The collections ${emitString(existing)} and ${emitString(slug)} both generate the accessor "${accessor}", so only one of them could be reached from the generated client and the other's reads would silently go to the wrong table. Rename one of the slugs.`);
196
+ bySafeName.set(accessor, slug);
197
+ accessors.set(slug, accessor);
228
198
  }
229
199
  return accessors;
230
200
  }
201
+ /**
202
+ * A column's type on a write: its own type (a date also as a `Date`), or
203
+ * `null` too when it is not required.
204
+ *
205
+ * `Row` has typed a non-required column `T | null` for a while; its write twins
206
+ * did not, so the one way to clear a value — `update(id, { publishedAt: null })`,
207
+ * `{ authorId: null }` to unassign, the edit-form round trip of a row that came
208
+ * back with nulls in it — was a compile error the server would have accepted.
209
+ * A primary key is never null, required or not.
210
+ */
211
+ function writableType(prop, tsType) {
212
+ const written = prop.type === "date" ? `${tsType} | Date` : tsType;
213
+ return prop.validation?.required || isPrimaryKey(prop) ? written : `${written} | null`;
214
+ }
231
215
  /** One emitted `key: type;` line, already indented. */
232
216
  function line(key, type, optional) {
233
217
  return ` ${emitKey(key)}${optional ? "?" : ""}: ${type};`;
@@ -239,9 +223,9 @@ function line(key, type, optional) {
239
223
  * property. `Row` already honoured that; `Insert` and `Update` deliberately did
240
224
  * not, on the reading that the column is stripped from responses rather than
241
225
  * from writes. That left the generated types as the one place a password hash
242
- * was still named, and it invited a client to send one. The server still
243
- * *accepts* such a field on a write — this describes the surface, it does not
244
- * add an enforcement point — but nothing generated advertises it.
226
+ * was still named, and it invited a client to send one. The server refuses
227
+ * such a field on a write (`VALIDATION_EXCLUDED_FIELDS`), so nothing generated
228
+ * advertises it.
245
229
  *
246
230
  * Read through `effectiveAccess`, so the flag and its longhand
247
231
  * `access: { read: [], write: [] }` produce the same file. A *role* rule is
@@ -265,6 +249,28 @@ function excludedApiKeys(properties) {
265
249
  }
266
250
  return excluded;
267
251
  }
252
+ /**
253
+ * The keys nobody may write: `access: { write: [] }`, readable or not.
254
+ *
255
+ * Off `Insert` and `Update` for the same reason `excludedApiKeys` takes a
256
+ * hidden property off all three — except that a field everyone can read but
257
+ * nobody can write stays on `Row`. The server refuses one on any write with
258
+ * `VALIDATION_EXCLUDED_FIELDS`, for every caller, `admin` included, so a
259
+ * generated `create({ computed })` compiled and could only ever be a 400.
260
+ *
261
+ * A write rule naming *roles* is not this: some caller can write it, and one
262
+ * generated shape cannot say which.
263
+ */
264
+ function unwritableKeys(properties) {
265
+ const unwritable = /* @__PURE__ */ new Set();
266
+ for (const [key, rawProp] of Object.entries(properties)) {
267
+ const prop = rawProp;
268
+ if (effectiveAccess(prop)?.write?.length !== 0) continue;
269
+ unwritable.add(key);
270
+ if (prop.columnName) unwritable.add(prop.columnName);
271
+ }
272
+ return unwritable;
273
+ }
268
274
  function generateTypedefs(input) {
269
275
  const collections = sortCollectionsBySlug(input);
270
276
  const accessors = buildAccessors(collections);
@@ -329,39 +335,42 @@ function generateTypedefs(input) {
329
335
  }
330
336
  lines.push(" };");
331
337
  lines.push(" Insert: {");
338
+ const notWritable = /* @__PURE__ */ new Set([...excluded, ...unwritableKeys(properties)]);
332
339
  emittedKeys.clear();
333
- for (const key of excluded) emittedKeys.add(key);
340
+ for (const key of notWritable) emittedKeys.add(key);
334
341
  for (const [key, rawProp] of Object.entries(properties)) {
335
342
  const prop = rawProp;
336
343
  if (prop.type === "relation") continue;
337
- if (excluded.has(key)) continue;
344
+ if (notWritable.has(key)) continue;
338
345
  const tsType = propertyToTypeScriptType(prop);
339
- const isOptional = !prop.validation?.required || isAutoAssignedId(prop);
340
- lines.push(line(key, tsType, isOptional));
346
+ const isOptional = !prop.validation?.required || isAutoAssignedId(prop) || Boolean(prop.databaseDefault);
347
+ lines.push(line(key, writableType(prop, tsType), isOptional));
341
348
  emittedKeys.add(key);
342
349
  }
343
350
  emitWritableRelations(lines, collection, properties, resolvedRelations, emittedKeys, false);
344
351
  lines.push(" };");
345
352
  lines.push(" Update: {");
346
353
  emittedKeys.clear();
347
- for (const key of excluded) emittedKeys.add(key);
354
+ for (const key of notWritable) emittedKeys.add(key);
348
355
  for (const [key, rawProp] of Object.entries(properties)) {
349
356
  const prop = rawProp;
350
357
  if (prop.type === "relation") continue;
351
358
  if (isPrimaryKey(prop)) continue;
352
- if (excluded.has(key)) continue;
353
- lines.push(line(key, propertyToTypeScriptType(prop), true));
359
+ if (notWritable.has(key)) continue;
360
+ lines.push(line(key, writableType(prop, propertyToTypeScriptType(prop)), true));
354
361
  emittedKeys.add(key);
355
362
  }
356
363
  emitWritableRelations(lines, collection, properties, resolvedRelations, emittedKeys, true);
357
364
  lines.push(" };");
358
- lines.push(" Relations: {");
365
+ if (Object.keys(resolvedRelations).length === 0) lines.push(" Relations: Record<never, never>;");
366
+ else lines.push(" Relations: {");
359
367
  for (const [key, relation] of Object.entries(resolvedRelations)) {
360
368
  const slug = resolveTargetCollection(relation)?.slug ?? relation.targetSlug;
361
369
  const accessor = slug ? accessors.get(slug) : void 0;
362
370
  lines.push(` ${emitKey(key)}: ${accessor ? emitString(accessor) : "never"};`);
363
371
  }
364
- lines.push(" };");
372
+ if (Object.keys(resolvedRelations).length > 0) lines.push(" };");
373
+ lines.push(` Slug: ${emitString(collection.slug)};`);
365
374
  lines.push(" };");
366
375
  }
367
376
  lines.push("}");
@@ -484,8 +493,9 @@ function includeHelperLines() {
484
493
  function emitWritableRelations(lines, collection, properties, resolvedRelations, emittedKeys, allOptional) {
485
494
  const emit = (key, relation) => {
486
495
  if (emittedKeys.has(key)) return;
487
- const optional = allOptional || !isRelationRequired(collection, relation);
488
- lines.push(line(key, foreignKeyType(relation), optional));
496
+ const required = isRelationRequired(collection, relation);
497
+ const type = required ? foreignKeyType(relation) : `${foreignKeyType(relation)} | null`;
498
+ lines.push(line(key, type, allOptional || !required));
489
499
  emittedKeys.add(key);
490
500
  };
491
501
  for (const relation of Object.values(resolvedRelations)) if (relation.kind === "belongsTo" && relation.localKey) emit(fieldKeyForColumn(collection, relation.localKey), relation);
@@ -494,6 +504,31 @@ function emitWritableRelations(lines, collection, properties, resolvedRelations,
494
504
  const relation = findRelation(resolvedRelations, key);
495
505
  if (relation?.kind === "belongsTo" && relation.localKey) emit(key, relation);
496
506
  }
507
+ for (const [key, relation] of Object.entries(resolvedRelations)) {
508
+ if (relation.cardinality !== "many" || !relation.writable) continue;
509
+ if (emittedKeys.has(key)) continue;
510
+ const fk = foreignKeyType(relation);
511
+ const pivot = relation.kind === "manyToMany" ? pivotType(relation.through.properties, "write") : void 0;
512
+ const element = `${fk} | { id: ${fk}${pivot ? `; _pivot?: ${pivot}` : ""} }`;
513
+ lines.push(line(key, `Array<${element}>`, true));
514
+ emittedKeys.add(key);
515
+ }
516
+ }
517
+ /**
518
+ * The `_pivot` a many-to-many link carries: the junction's payload columns,
519
+ * typed as a row's columns are on a read and as a write's on a write. `undefined`
520
+ * when the junction declares none, so there is no `_pivot` to name.
521
+ */
522
+ function pivotType(pivotProperties, direction) {
523
+ const entries = Object.entries(pivotProperties);
524
+ if (entries.length === 0) return void 0;
525
+ return `{ ${entries.map(([key, rawProp]) => {
526
+ const prop = rawProp;
527
+ const tsType = propertyToTypeScriptType(prop);
528
+ const required = Boolean(prop.validation?.required);
529
+ const type = direction === "write" ? writableType(prop, tsType) : required ? tsType : `${tsType} | null`;
530
+ return `${emitKey(key)}${required && direction === "read" ? "" : "?"}: ${type};`;
531
+ }).join(" ")} }`;
497
532
  }
498
533
  //#endregion
499
534
  //#region src/schema/doctor.ts
@@ -511,11 +546,15 @@ var doctor_exports = /* @__PURE__ */ __exportAll({
511
546
  checkCollectionsVsSchema: () => checkCollectionsVsSchema,
512
547
  checkCollectionsVsSdk: () => checkCollectionsVsSdk,
513
548
  getExpectedColumnType: () => getExpectedColumnType,
549
+ junctionKeyIssues: () => junctionKeyIssues,
514
550
  loadCollections: () => loadCollections,
515
551
  missingExtensionIssues: () => missingExtensionIssues,
552
+ orphanedJunctionIssues: () => orphanedJunctionIssues,
553
+ primaryKeyIssues: () => primaryKeyIssues,
516
554
  renderReport: () => renderReport,
517
555
  runDoctor: () => runDoctor,
518
- schemaStampIssues: () => schemaStampIssues
556
+ schemaStampIssues: () => schemaStampIssues,
557
+ typeMismatchFix: () => typeMismatchFix
519
558
  });
520
559
  /**
521
560
  * Resolve the SQL column name for a property.
@@ -736,6 +775,91 @@ async function schemaStampIssues(pool, collections) {
736
775
  fix: "A hash says the two disagree, never which is ahead. Check you are pointed at the database you think you are; then `rebase db push` to bring it to these collections."
737
776
  }];
738
777
  }
778
+ /** `"public"."posts"` — quoted, for a statement the reader will paste. */
779
+ var quotedTable = (schema, table) => `"${schema}"."${table}"`;
780
+ /**
781
+ * What to do about a column whose type is not the one the collection plans.
782
+ *
783
+ * Both ways out, because the doctor cannot know which side is right: the
784
+ * property moved, or the database did. Putting the property back always clears
785
+ * the finding; converting the column clears it when every value converts.
786
+ */
787
+ function typeMismatchFix(schema, table, column, actual, planned) {
788
+ const back = `Either put the property back to the column's type (${actual}), or`;
789
+ if (!planned) return `${back} run a migration that converts the column.`;
790
+ const type = renderPgType(planned);
791
+ return `${back} convert the column when you can watch it (every value must convert): ALTER TABLE ${quotedTable(schema, table)} ALTER COLUMN "${column}" TYPE ${type} USING "${column}"::${type};`;
792
+ }
793
+ /**
794
+ * The table's primary key against the one the collection reads it by.
795
+ *
796
+ * Rebase addresses a row by the key the plan names — the `isId` property, or
797
+ * the implicit `id` — so a table keyed on other columns, or on none, serves
798
+ * reads and writes against a key that does not hold. An error, because that is
799
+ * what it is at runtime.
800
+ */
801
+ function primaryKeyIssues(tablePlan, dbPk, displayName) {
802
+ const expected = tablePlan.primaryKey;
803
+ if (dbPk.length === expected.length && [...dbPk].sort().join("\0") === [...expected].sort().join("\0")) return [];
804
+ const table = quotedTable(tablePlan.schema, tablePlan.table);
805
+ const keyList = expected.map((c) => `"${c}"`).join(", ");
806
+ if (dbPk.length === 0) return [{
807
+ severity: "error",
808
+ category: "primary_key_mismatch",
809
+ table: displayName,
810
+ expected: expected.join(", "),
811
+ actual: "(none)",
812
+ message: `Table "${displayName}" has no primary key; the collection reads its rows by ${keyList}.`,
813
+ fix: `ALTER TABLE ${table} ADD PRIMARY KEY (${keyList});`
814
+ }];
815
+ const only = expected.some((column) => !dbPk.includes(column)) ? "only " : "";
816
+ const fix = dbPk.length === 1 ? `Mark ${only}the property for column "${dbPk[0]}" with \`isId\` — the table is keyed on it — or re-key the table on ${keyList}.` : `Mark ${only}the properties for columns ${dbPk.map((c) => `"${c}"`).join(", ")} with \`isId\` — together they are the table's composite key — or re-key the table on ${keyList}.`;
817
+ return [{
818
+ severity: "error",
819
+ category: "primary_key_mismatch",
820
+ table: displayName,
821
+ expected: expected.join(", "),
822
+ actual: dbPk.join(", "),
823
+ message: `Table "${displayName}" is keyed on (${dbPk.join(", ")}), but the collection reads its rows by ${keyList}.`,
824
+ fix
825
+ }];
826
+ }
827
+ /**
828
+ * A junction's two key columns: present, typed like the keys they point at,
829
+ * and the table keyed on the pair. Order is not compared — boot and `db push`
830
+ * have built the key in both orders, and either one serves the same rows.
831
+ */
832
+ function junctionKeyIssues(junctionPlan, dbColumns, dbPk, displayName) {
833
+ const issues = [];
834
+ const byName = new Map(dbColumns.map((c) => [c.column_name, c]));
835
+ for (const column of junctionPlan.columns) {
836
+ const dbCol = byName.get(column.column);
837
+ if (!dbCol) {
838
+ issues.push({
839
+ severity: "error",
840
+ category: "missing_column",
841
+ table: displayName,
842
+ column: column.column,
843
+ message: `Junction table "${displayName}" has no "${column.column}" column.`,
844
+ fix: "Run `rebase db push` or `rebase db generate && rebase db migrate`"
845
+ });
846
+ continue;
847
+ }
848
+ const expected = INFORMATION_SCHEMA_NAMES[column.type.kind];
849
+ if (dbCol.data_type !== expected) issues.push({
850
+ severity: "warning",
851
+ category: "type_mismatch",
852
+ table: displayName,
853
+ column: column.column,
854
+ expected,
855
+ actual: dbCol.data_type,
856
+ message: `Junction column "${column.column}" in "${displayName}" is ${dbCol.data_type}, but the key it points at is ${expected}.`,
857
+ fix: typeMismatchFix(junctionPlan.schema, junctionPlan.table, column.column, dbCol.data_type, column.type)
858
+ });
859
+ }
860
+ issues.push(...primaryKeyIssues(junctionPlan, dbPk, displayName));
861
+ return issues;
862
+ }
739
863
  async function checkCollectionsVsDatabase(collections, databaseUrl) {
740
864
  const issues = [];
741
865
  const pgModule = await import("pg");
@@ -767,15 +891,27 @@ async function checkCollectionsVsDatabase(collections, databaseUrl) {
767
891
  if (!columnsByTable.has(key)) columnsByTable.set(key, []);
768
892
  columnsByTable.get(key).push(row);
769
893
  }
770
- const enumsResult = await pool.query(`SELECT t.typname as enum_name, e.enumlabel as enum_value
894
+ const enumsResult = await pool.query(`SELECT n.nspname AS enum_schema, t.typname AS enum_name, e.enumlabel AS enum_value
771
895
  FROM pg_type t
896
+ JOIN pg_namespace n ON n.oid = t.typnamespace
772
897
  JOIN pg_enum e ON t.oid = e.enumtypid
773
- ORDER BY t.typname, e.enumsortorder`);
898
+ ORDER BY n.nspname, t.typname, e.enumsortorder`);
774
899
  const enumsByName = /* @__PURE__ */ new Map();
775
900
  for (const row of enumsResult.rows) {
776
- if (!enumsByName.has(row.enum_name)) enumsByName.set(row.enum_name, []);
777
- enumsByName.get(row.enum_name).push(row.enum_value);
901
+ const key = `${row.enum_schema}.${row.enum_name}`;
902
+ if (!enumsByName.has(key)) enumsByName.set(key, []);
903
+ enumsByName.get(key).push(row.enum_value);
778
904
  }
905
+ const pksResult = await pool.query(`SELECT n.nspname AS table_schema, c.relname AS table_name,
906
+ array_agg(a.attname::text ORDER BY array_position(i.indkey::int2[], a.attnum)) AS columns
907
+ FROM pg_index i
908
+ JOIN pg_class c ON c.oid = i.indrelid
909
+ JOIN pg_namespace n ON n.oid = c.relnamespace
910
+ JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = ANY(i.indkey::int2[])
911
+ WHERE i.indisprimary AND n.nspname = ANY($1)
912
+ GROUP BY n.nspname, c.relname`, [schemas]);
913
+ const pksByTable = /* @__PURE__ */ new Map();
914
+ for (const row of pksResult.rows) pksByTable.set(row.table_schema === "public" ? row.table_name : `${row.table_schema}.${row.table_name}`, row.columns);
779
915
  const fksResult = await pool.query(`SELECT
780
916
  tc.constraint_name,
781
917
  tc.table_schema,
@@ -800,6 +936,14 @@ async function checkCollectionsVsDatabase(collections, databaseUrl) {
800
936
  }
801
937
  const postgresCollections = relationalCollections(collections);
802
938
  const junctionSpecs = resolveJunctionSpecs(postgresCollections);
939
+ let plan;
940
+ try {
941
+ plan = planSchema(postgresCollections);
942
+ } catch {
943
+ plan = void 0;
944
+ }
945
+ const tablePlans = new Map((plan?.tables ?? []).map((t) => [t.qualified, t]));
946
+ const checkedJunctions = /* @__PURE__ */ new Set();
803
947
  for (const collection of postgresCollections) {
804
948
  const tableName = getTableName(collection);
805
949
  const schemaName = collection.schema || "public";
@@ -816,11 +960,8 @@ async function checkCollectionsVsDatabase(collections, databaseUrl) {
816
960
  }
817
961
  const dbColumns = columnsByTable.get(fullTableName) ?? [];
818
962
  const dbColumnMap = new Map(dbColumns.map((c) => [c.column_name, c]));
819
- const systemColumns = /* @__PURE__ */ new Set([
820
- "id",
821
- "created_on",
822
- "updated_on"
823
- ]);
963
+ const tablePlan = tablePlans.get(`${schemaName}.${tableName}`);
964
+ if (tablePlan) issues.push(...primaryKeyIssues(tablePlan, pksByTable.get(fullTableName) ?? [], fullTableName));
824
965
  for (const [propName, prop] of Object.entries(collection.properties ?? {})) {
825
966
  if (prop.type === "relation") {
826
967
  const relation = findRelation(resolveCollectionRelations(collection), prop.relation?.relationName ?? propName);
@@ -855,7 +996,6 @@ async function checkCollectionsVsDatabase(collections, databaseUrl) {
855
996
  continue;
856
997
  }
857
998
  const colName = resolveColumnName(propName, prop);
858
- if (systemColumns.has(colName)) continue;
859
999
  const dbCol = dbColumnMap.get(colName);
860
1000
  if (!dbCol) {
861
1001
  issues.push({
@@ -888,29 +1028,32 @@ async function checkCollectionsVsDatabase(collections, databaseUrl) {
888
1028
  if (expectedUdt && dbCol.udt_name !== expectedUdt) isMismatch = true;
889
1029
  }
890
1030
  }
891
- if (isMismatch) issues.push({
892
- severity: "warning",
893
- category: "type_mismatch",
894
- table: fullTableName,
895
- column: colName,
896
- expected: prop.type === "vector" ? "vector" : expectedType,
897
- actual: dbCol.udt_name === "vector" ? "vector" : actualType,
898
- message: `Column "${colName}" in table "${fullTableName}": expected type "${prop.type === "vector" ? "vector" : expectedType}" but found "${dbCol.udt_name === "vector" ? "vector" : actualType}".`,
899
- fix: "Review collection property type or run a migration"
900
- });
1031
+ if (isMismatch) {
1032
+ const planned = tablePlan?.columns.find((c) => c.column === colName)?.type;
1033
+ issues.push({
1034
+ severity: "warning",
1035
+ category: "type_mismatch",
1036
+ table: fullTableName,
1037
+ column: colName,
1038
+ expected: prop.type === "vector" ? "vector" : expectedType,
1039
+ actual: dbCol.udt_name === "vector" ? "vector" : actualType,
1040
+ message: `Column "${colName}" in table "${fullTableName}": expected type "${prop.type === "vector" ? "vector" : expectedType}" but found "${dbCol.udt_name === "vector" ? "vector" : actualType}".`,
1041
+ fix: typeMismatchFix(schemaName, tableName, colName, actualType, planned)
1042
+ });
1043
+ }
901
1044
  }
902
1045
  if (prop.type === "string" && prop.enum) {
903
1046
  const enumValues = prop.enum;
904
1047
  if (enumValues) {
905
- const enumName = `${tableName}_${colName}`;
906
- const dbEnumValues = enumsByName.get(enumName);
1048
+ const enumName = toPostgresIdentifier(`${tableName}_${colName}`);
1049
+ const dbEnumValues = enumsByName.get(`${schemaName}.${enumName}`);
907
1050
  if (!dbEnumValues) issues.push({
908
1051
  severity: "warning",
909
1052
  category: "missing_enum",
910
1053
  table: fullTableName,
911
1054
  column: colName,
912
1055
  expected: enumName,
913
- message: `Enum type "${enumName}" is defined in collection but not found in the database.`,
1056
+ message: `Enum type "${schemaName}"."${enumName}" is defined in collection but not found in the database.`,
914
1057
  fix: "Run `rebase db push` or `rebase db generate && rebase db migrate`"
915
1058
  });
916
1059
  else {
@@ -929,7 +1072,7 @@ async function checkCollectionsVsDatabase(collections, databaseUrl) {
929
1072
  expected: expectedValues.join(", "),
930
1073
  actual: dbEnumValues.join(", "),
931
1074
  message: `Enum values for "${colName}" in table "${fullTableName}" are out of sync (${parts.join("; ")}).`,
932
- fix: "Run `rebase db push` to update the enum"
1075
+ fix: extra.length > 0 && missing.length === 0 ? `PostgreSQL cannot drop an enum label, so \`rebase db push\` leaves ${extra.join(", ")} in place. Harmless unless something still writes it; to retire it, UPDATE its rows to another value, then rewrite the type in a migration (\`rebase db generate\`: CREATE TYPE …, ALTER TABLE … TYPE … USING, DROP TYPE) and \`rebase db migrate\`` : "Run `rebase db push` to add the missing labels" + (extra.length > 0 ? `; it cannot remove ${extra.join(", ")} (PostgreSQL has no ALTER TYPE … DROP VALUE) — retire those with a migration that rewrites the type` : "")
933
1076
  });
934
1077
  }
935
1078
  }
@@ -948,16 +1091,63 @@ async function checkCollectionsVsDatabase(collections, databaseUrl) {
948
1091
  message: `Junction table "${fullJunctionTable}" for many-to-many relation "${relation.relationName}" is missing.`,
949
1092
  fix: "Run `rebase db push` or `rebase db generate && rebase db migrate`"
950
1093
  });
1094
+ else if (!checkedJunctions.has(fullJunctionTable)) {
1095
+ checkedJunctions.add(fullJunctionTable);
1096
+ const junctionPlan = tablePlans.get(`${junctionSchema}.${junctionTable}`);
1097
+ if (junctionPlan) issues.push(...junctionKeyIssues(junctionPlan, columnsByTable.get(fullJunctionTable) ?? [], pksByTable.get(fullJunctionTable) ?? [], fullJunctionTable));
1098
+ }
951
1099
  }
952
1100
  }
1101
+ if (plan) issues.push(...orphanedJunctionIssues(plan, columnsByTable, pksByTable, fksByTable));
953
1102
  } finally {
954
1103
  await pool.end();
955
1104
  }
956
1105
  return {
957
- passed: issues.length === 0,
1106
+ passed: !issues.some((issue) => issue.severity !== "info"),
958
1107
  issues
959
1108
  };
960
1109
  }
1110
+ /**
1111
+ * A `public` table in the junction shape that no collection declares.
1112
+ *
1113
+ * The shape is the planner's: exactly two columns, together the primary key,
1114
+ * each a single-column foreign key to a collection's table. Renaming or
1115
+ * removing a many-to-many leaves such a table behind — push and boot only
1116
+ * create — with every link row still in it and nothing reading it.
1117
+ *
1118
+ * Info, never more: the table may be data somebody still wants. The remedy is
1119
+ * shown, never run.
1120
+ */
1121
+ function orphanedJunctionIssues(plan, columnsByTable, pksByTable, fksByTable) {
1122
+ const declared = new Set(plan.tables.map((table) => table.qualified));
1123
+ const collectionTables = new Set(plan.tables.filter((table) => table.kind === "collection").map((table) => table.qualified));
1124
+ const issues = [];
1125
+ for (const [table, columns] of columnsByTable) {
1126
+ if (table.includes(".") || declared.has(`public.${table}`)) continue;
1127
+ if (columns.length !== 2 || (pksByTable.get(table) ?? []).length !== 2) continue;
1128
+ const byConstraint = /* @__PURE__ */ new Map();
1129
+ for (const fk of fksByTable.get(table) ?? []) {
1130
+ const target = `${fk.foreign_table_schema}.${fk.foreign_table_name}`;
1131
+ byConstraint.set(fk.constraint_name, [...byConstraint.get(fk.constraint_name) ?? [], {
1132
+ column: fk.column_name,
1133
+ target
1134
+ }]);
1135
+ }
1136
+ const links = [...byConstraint.values()];
1137
+ if (links.length !== 2 || links.some((link) => link.length !== 1)) continue;
1138
+ if (new Set(links.map(([link]) => link.column)).size !== 2) continue;
1139
+ if (!links.every(([link]) => collectionTables.has(link.target))) continue;
1140
+ const targets = links.map(([link]) => link.target.replace(/^public\./, ""));
1141
+ issues.push({
1142
+ severity: "info",
1143
+ category: "orphaned_junction",
1144
+ table,
1145
+ message: `Table "${table}" links "${targets[0]}" and "${targets[1]}" like a many-to-many junction, but no collection declares it — left behind by a renamed or removed relation?`,
1146
+ fix: `DROP TABLE "public"."${table}";`
1147
+ });
1148
+ }
1149
+ return issues;
1150
+ }
961
1151
  function renderReport(report) {
962
1152
  out();
963
1153
  out(chalk.bold(" 🩺 Rebase Schema Doctor"));
@@ -1032,10 +1222,12 @@ function formatCategory(cat) {
1032
1222
  missing_column: "Missing Column",
1033
1223
  type_mismatch: "Type Mismatch",
1034
1224
  missing_constraint: "Missing Constraint",
1225
+ primary_key_mismatch: "Primary Key Mismatch",
1035
1226
  schema_stale: "Stale Schema",
1036
1227
  missing_enum: "Missing Enum",
1037
1228
  enum_value_mismatch: "Enum Value Mismatch",
1038
1229
  missing_foreign_key: "Missing Foreign Key",
1230
+ orphaned_junction: "Orphaned Junction Table",
1039
1231
  sdk_stale: "Stale SDK Types",
1040
1232
  sdk_not_generated: "SDK Types Not Generated",
1041
1233
  sdk_ungeneratable: "SDK Cannot Be Generated"
@@ -1105,4 +1297,4 @@ async function runDoctor(options) {
1105
1297
  //#endregion
1106
1298
  export { loadCollections as n, runDoctor as r, doctor_exports as t };
1107
1299
 
1108
- //# sourceMappingURL=doctor-Cb2thZ8s.js.map
1300
+ //# sourceMappingURL=doctor-C4WiftHm.js.map