@rebasepro/server-postgres 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/dist/{BranchService-iXXZeVYD.js → BranchService-CWBnDhcv.js} +88 -12
  2. package/dist/BranchService-CWBnDhcv.js.map +1 -0
  3. package/dist/PostgresBackendDriver.d.ts +13 -0
  4. package/dist/PostgresBootstrapper.d.ts +7 -0
  5. package/dist/auth/services.d.ts +8 -1
  6. package/dist/auth/verify-auth-tables.d.ts +21 -0
  7. package/dist/backup/backup-cli.d.ts +40 -3
  8. package/dist/backup/backup-service.d.ts +11 -0
  9. package/dist/{backup-service-Bb39plam.js → backup-cli-PoRG-zEq.js} +777 -10
  10. package/dist/backup-cli-PoRG-zEq.js.map +1 -0
  11. package/dist/{boot-ddl-s_3Hkz94.js → boot-ddl-EsgqDkx0.js} +3 -3
  12. package/dist/{boot-ddl-s_3Hkz94.js.map → boot-ddl-EsgqDkx0.js.map} +1 -1
  13. package/dist/branch-argv.d.ts +15 -0
  14. package/dist/branch-diff-CxE4VlxT.js +1396 -0
  15. package/dist/branch-diff-CxE4VlxT.js.map +1 -0
  16. package/dist/branch-diff-report-DpvVf7hR.js +48 -0
  17. package/dist/branch-diff-report-DpvVf7hR.js.map +1 -0
  18. package/dist/branching/branch-diff-report.d.ts +7 -0
  19. package/dist/branching/branch-diff.d.ts +115 -0
  20. package/dist/{cli-console-DQ_K-sdM.js → cli-console-f0k6_TsJ.js} +41 -9
  21. package/dist/cli-console-f0k6_TsJ.js.map +1 -0
  22. package/dist/cli-console.d.ts +17 -1
  23. package/dist/{cli-errors-DwQnNATs.js → cli-errors-DsYyNYzd.js} +90 -11
  24. package/dist/cli-errors-DsYyNYzd.js.map +1 -0
  25. package/dist/cli-errors.d.ts +14 -0
  26. package/dist/cli-flags.d.ts +51 -0
  27. package/dist/{cli-prompt-egtNKkZl.js → cli-prompt-DXG0yk-h.js} +39 -4
  28. package/dist/{cli-prompt-egtNKkZl.js.map → cli-prompt-DXG0yk-h.js.map} +1 -1
  29. package/dist/cli.js +121 -271
  30. package/dist/cli.js.map +1 -1
  31. package/dist/{collections-schema-version-T6cGvrDi.js → collections-schema-version-D7bDI0PJ.js} +13 -9
  32. package/dist/collections-schema-version-D7bDI0PJ.js.map +1 -0
  33. package/dist/{column-plan-helpers-D_V4oaZh.js → column-plan-helpers-IG6uFb1s.js} +2 -2
  34. package/dist/{column-plan-helpers-D_V4oaZh.js.map → column-plan-helpers-IG6uFb1s.js.map} +1 -1
  35. package/dist/{connection-C938nR3E.js → connection-czvI7TZL.js} +57 -10
  36. package/dist/connection-czvI7TZL.js.map +1 -0
  37. package/dist/{databasePoolManager-Yqdw14wY.js → databasePoolManager-3stMNFL-.js} +4 -6
  38. package/dist/databasePoolManager-3stMNFL-.js.map +1 -0
  39. package/dist/db-command-line.d.ts +14 -0
  40. package/dist/db-subcommands.d.ts +11 -0
  41. package/dist/{deferred-index-build-Cis_HoGn.js → deferred-index-build-CqaOv1Xx.js} +8 -8
  42. package/dist/{deferred-index-build-Cis_HoGn.js.map → deferred-index-build-CqaOv1Xx.js.map} +1 -1
  43. package/dist/{doctor-DvJH-IQG.js → doctor-DW_mlTzz.js} +186 -42
  44. package/dist/doctor-DW_mlTzz.js.map +1 -0
  45. package/dist/{ensure-collection-policies-CCqayoUz.js → ensure-collection-policies-DLWgabQ2.js} +13 -10
  46. package/dist/ensure-collection-policies-DLWgabQ2.js.map +1 -0
  47. package/dist/{ensure-collection-tables-DvvImXDe.js → ensure-collection-tables-CFIRq8ni.js} +9 -9
  48. package/dist/{ensure-collection-tables-DvvImXDe.js.map → ensure-collection-tables-CFIRq8ni.js.map} +1 -1
  49. package/dist/{ensure-tables-Dpgwx5sy.js → ensure-tables-4nA-tfwi.js} +5 -5
  50. package/dist/{ensure-tables-Dpgwx5sy.js.map → ensure-tables-4nA-tfwi.js.map} +1 -1
  51. package/dist/{enum-labels-kept-CUlUlRkO.js → enum-labels-kept-PcPW65xy.js} +3 -3
  52. package/dist/{enum-labels-kept-CUlUlRkO.js.map → enum-labels-kept-PcPW65xy.js.map} +1 -1
  53. package/dist/{generate-drizzle-schema-C3EAP9HE.js → generate-drizzle-schema-DC0BOUpn.js} +3 -3
  54. package/dist/{generate-drizzle-schema-C3EAP9HE.js.map → generate-drizzle-schema-DC0BOUpn.js.map} +1 -1
  55. package/dist/{generate-drizzle-schema-logic-DA3zM8zc.js → generate-drizzle-schema-logic-CPzCRD-8.js} +4 -4
  56. package/dist/{generate-drizzle-schema-logic-DA3zM8zc.js.map → generate-drizzle-schema-logic-CPzCRD-8.js.map} +1 -1
  57. package/dist/{generate-files-ClmOUHXj.js → generate-files-BP-4W_gm.js} +6 -6
  58. package/dist/{generate-files-ClmOUHXj.js.map → generate-files-BP-4W_gm.js.map} +1 -1
  59. package/dist/{generate-next-step-DCeS4LrJ.js → generate-next-step-DgK5BlBn.js} +2 -2
  60. package/dist/{generate-next-step-DCeS4LrJ.js.map → generate-next-step-DgK5BlBn.js.map} +1 -1
  61. package/dist/{generate-postgres-ddl-logic-4eUP4t8w.js → generate-postgres-ddl-logic-DPl1oH_z.js} +4 -4
  62. package/dist/{generate-postgres-ddl-logic-4eUP4t8w.js.map → generate-postgres-ddl-logic-DPl1oH_z.js.map} +1 -1
  63. package/dist/generated-schema-diff-eZCklRZQ.js +1025 -0
  64. package/dist/generated-schema-diff-eZCklRZQ.js.map +1 -0
  65. package/dist/{generated-schema-staleness-FlxM2i4i.js → generated-schema-staleness-BvNF3i-s.js} +2 -2
  66. package/dist/{generated-schema-staleness-FlxM2i4i.js.map → generated-schema-staleness-BvNF3i-s.js.map} +1 -1
  67. package/dist/history/HistoryService.d.ts +7 -0
  68. package/dist/index.d.ts +21 -0
  69. package/dist/index.es.js +288 -11
  70. package/dist/index.es.js.map +1 -1
  71. package/dist/{introspect-db-logic-CI5CU_n0.js → introspect-db-logic-DgQVzccW.js} +2 -2
  72. package/dist/{introspect-db-logic-CI5CU_n0.js.map → introspect-db-logic-DgQVzccW.js.map} +1 -1
  73. package/dist/{rls-bootstrap-sql-Bd-hJk08.js → migrate-baseline-B-2Ef4Mv.js} +718 -41
  74. package/dist/migrate-baseline-B-2Ef4Mv.js.map +1 -0
  75. package/dist/migrate-policy-stamps-BQtFHApx.js +89 -0
  76. package/dist/migrate-policy-stamps-BQtFHApx.js.map +1 -0
  77. package/dist/{open-rebase-BKA7Nyoe.js → open-rebase-C8jQ6s5a.js} +324 -1047
  78. package/dist/open-rebase-C8jQ6s5a.js.map +1 -0
  79. package/dist/open-rebase.d.ts +9 -1
  80. package/dist/{pg-error-utils-Bv5n2PLn.js → pg-error-utils-DhiUTlVx.js} +2 -2
  81. package/dist/{pg-error-utils-Bv5n2PLn.js.map → pg-error-utils-DhiUTlVx.js.map} +1 -1
  82. package/dist/{plan-schema-CXOucCw-.js → plan-schema-Czf3q4-Q.js} +4 -4
  83. package/dist/{plan-schema-CXOucCw-.js.map → plan-schema-Czf3q4-Q.js.map} +1 -1
  84. package/dist/{policy-drift-vm0BcvcE.js → policy-drift-X4A-xAzv.js} +3 -3
  85. package/dist/{policy-drift-vm0BcvcE.js.map → policy-drift-X4A-xAzv.js.map} +1 -1
  86. package/dist/pool-disconnects.d.ts +28 -0
  87. package/dist/{push-index-prebuild-DIequ4bV.js → push-index-prebuild-UTvB5pqH.js} +4 -4
  88. package/dist/{push-index-prebuild-DIequ4bV.js.map → push-index-prebuild-UTvB5pqH.js.map} +1 -1
  89. package/dist/push-kept-defaults-BGOlOqRQ.js +137 -0
  90. package/dist/push-kept-defaults-BGOlOqRQ.js.map +1 -0
  91. package/dist/{push-legacy-renames-BqJut4F3.js → push-legacy-renames-BwaKi8xF.js} +4 -4
  92. package/dist/{push-legacy-renames-BqJut4F3.js.map → push-legacy-renames-BwaKi8xF.js.map} +1 -1
  93. package/dist/{push-policy-plan-CvX8hiPb.js → push-policy-plan-DQ2tWceD.js} +5 -5
  94. package/dist/{push-policy-plan-CvX8hiPb.js.map → push-policy-plan-DQ2tWceD.js.map} +1 -1
  95. package/dist/{render-ddl-CQNwZdse.js → render-ddl-4v88y-Gv.js} +4 -4
  96. package/dist/{render-ddl-CQNwZdse.js.map → render-ddl-4v88y-Gv.js.map} +1 -1
  97. package/dist/{rls-enforcement-D7P4-ZYX.js → rls-enforcement-BOHuHVlc.js} +43 -4
  98. package/dist/rls-enforcement-BOHuHVlc.js.map +1 -0
  99. package/dist/schema/destructive-sql.d.ts +16 -0
  100. package/dist/schema/doctor-cli.js +5 -5
  101. package/dist/schema/doctor.d.ts +20 -2
  102. package/dist/schema/ensure-collection-policies.d.ts +4 -1
  103. package/dist/schema/generate-drizzle-schema.js +1 -1
  104. package/dist/schema/generate-postgres-ddl.js +2 -2
  105. package/dist/schema/generated-schema-diff.d.ts +21 -0
  106. package/dist/schema/introspect-db.js +5 -6
  107. package/dist/schema/introspect-db.js.map +1 -1
  108. package/dist/schema/migrate-policy-stamps.d.ts +23 -0
  109. package/dist/schema/migration-freshness.d.ts +69 -0
  110. package/dist/schema/push-kept-defaults.d.ts +57 -0
  111. package/dist/schema/push-policy-plan.d.ts +5 -0
  112. package/dist/schema/rls-bootstrap-sql.d.ts +47 -10
  113. package/dist/security/rls-enforcement.d.ts +18 -0
  114. package/dist/services/BranchService.d.ts +20 -2
  115. package/dist/services/FetchService.d.ts +12 -0
  116. package/dist/services/channel-presence.d.ts +3 -1
  117. package/dist/services/nested-path.d.ts +16 -0
  118. package/dist/services/pg-notify-listener.d.ts +2 -0
  119. package/dist/services/realtimeService.d.ts +18 -1
  120. package/dist/{source-Bv5KPeIx.js → source-fZb92pN8.js} +2 -2
  121. package/dist/{source-Bv5KPeIx.js.map → source-fZb92pN8.js.map} +1 -1
  122. package/dist/{vector-index-BFhyEaiz.js → vector-index-DsQ3V2d2.js} +2 -2
  123. package/dist/{vector-index-BFhyEaiz.js.map → vector-index-DsQ3V2d2.js.map} +1 -1
  124. package/dist/{websocket-MC0zTBar.js → websocket-CKYc3V4N.js} +56 -14
  125. package/dist/websocket-CKYc3V4N.js.map +1 -0
  126. package/package.json +7 -7
  127. package/dist/BranchService-iXXZeVYD.js.map +0 -1
  128. package/dist/backup-cli-B2CAn9fR.js +0 -647
  129. package/dist/backup-cli-B2CAn9fR.js.map +0 -1
  130. package/dist/backup-service-Bb39plam.js.map +0 -1
  131. package/dist/cli-console-DQ_K-sdM.js.map +0 -1
  132. package/dist/cli-errors-DwQnNATs.js.map +0 -1
  133. package/dist/cli-flags-D-b0qL9T.js +0 -206
  134. package/dist/cli-flags-D-b0qL9T.js.map +0 -1
  135. package/dist/collections-schema-version-T6cGvrDi.js.map +0 -1
  136. package/dist/connection-C938nR3E.js.map +0 -1
  137. package/dist/databasePoolManager-Yqdw14wY.js.map +0 -1
  138. package/dist/doctor-DvJH-IQG.js.map +0 -1
  139. package/dist/ensure-collection-policies-CCqayoUz.js.map +0 -1
  140. package/dist/open-rebase-BKA7Nyoe.js.map +0 -1
  141. package/dist/rls-bootstrap-sql-Bd-hJk08.js.map +0 -1
  142. package/dist/rls-enforcement-D7P4-ZYX.js.map +0 -1
  143. package/dist/rolldown-runtime-V7rFbMRe.js +0 -39
  144. package/dist/websocket-MC0zTBar.js.map +0 -1
@@ -1,18 +1,20 @@
1
1
  import { createRequire as __createRequire } from "module";
2
2
  __createRequire(import.meta.url);
3
- import { n as __exportAll } from "./rolldown-runtime-V7rFbMRe.js";
4
- import { a as diagnoseDbError, r as deepestErrorMessage, u as parseHostInfo } from "./cli-errors-DwQnNATs.js";
5
- import { i as outError, r as out, t as chalk } from "./source-Bv5KPeIx.js";
6
- import { n as planSchema, t as columnPgType } from "./plan-schema-CXOucCw-.js";
7
- import { t as generateSchema } from "./generate-drizzle-schema-logic-DA3zM8zc.js";
8
- import { r as renderPgType } from "./render-ddl-CQNwZdse.js";
9
- import { n as describeDeclarationDifferences, t as compareGeneratedDeclarations } from "./generated-schema-staleness-FlxM2i4i.js";
3
+ import { i as __exportAll } from "./cli-prompt-DXG0yk-h.js";
4
+ import { a as diagnoseDbError, r as deepestErrorMessage, u as parseHostInfo } from "./cli-errors-DsYyNYzd.js";
5
+ import { i as outError, r as out, t as chalk } from "./source-fZb92pN8.js";
6
+ import { n as planSchema, t as columnPgType } from "./plan-schema-Czf3q4-Q.js";
7
+ import { t as generateSchema } from "./generate-drizzle-schema-logic-CPzCRD-8.js";
8
+ import { r as renderPgType } from "./render-ddl-4v88y-Gv.js";
9
+ import { r as readGeneratedSchemaDrift, t as describeSchemaDifference } from "./generated-schema-diff-eZCklRZQ.js";
10
+ import { n as describeDeclarationDifferences, t as compareGeneratedDeclarations } from "./generated-schema-staleness-BvNF3i-s.js";
10
11
  import { loadCollectionsFromDirectory } from "@rebasepro/server";
11
- import { computeSchemaVersion, getDeclaredSubcollections, isManyToMany, isPostgresCollectionConfig } from "@rebasepro/types";
12
- import { effectiveAccess, fieldKeyForColumn, findRelation, getTableName, isRelationRequired, relationalCollections, resolveCollectionRelations, resolveJunctionSpecs, sortCollectionsBySlug, toCollectionAccessor, toSafeIdentifier } from "@rebasepro/common";
12
+ import { DEFAULT_DATA_SOURCE_KEY, computeSchemaVersion, getDeclaredSubcollections, isManyToMany, isPostgresCollectionConfig } from "@rebasepro/types";
13
+ import { effectiveAccess, fieldKeyForColumn, findRelation, getTableName, isRelationRequired, relationalCollections, resolveCollectionRelations, resolveJunctionSpecs, restrictedFieldNames, sortCollectionsBySlug, toCollectionAccessor, toSafeIdentifier } from "@rebasepro/common";
13
14
  import { toPostgresIdentifier, toSnakeCase } from "@rebasepro/utils";
14
15
  import * as fs$1 from "fs";
15
16
  import { promises } from "fs";
17
+ import { pathToFileURL } from "url";
16
18
  //#region ../codegen/dist/index.es.js
17
19
  /**
18
20
  * A schema that cannot be expressed as a valid TypeScript file.
@@ -234,20 +236,40 @@ function line(key, type, optional) {
234
236
  * reader who holds `hr` and one who does not. The server is the enforcement
235
237
  * point; the types describe the surface a caller may name.
236
238
  *
237
- * Keyed by the property name *and* by its column name, the same pair the
238
- * server's `stripUnreadable` deletes, so a foreign key or a relation addressed
239
- * under the column name cannot put the property back.
239
+ * Every spelling the server restricts, read from the server's own function
240
+ * (`restrictedFieldNames`): the property name, its column name, and — for a
241
+ * closed to-one relation — the foreign key that names the same row. Keying on
242
+ * the property and column alone, which this used to, kept `authorId` on all
243
+ * three types for an `excludeFromApi` `author`, though the server strips it
244
+ * from every read and refuses it on every write.
240
245
  */
241
- function excludedApiKeys(properties) {
242
- const excluded = /* @__PURE__ */ new Set();
243
- for (const [key, rawProp] of Object.entries(properties)) {
244
- const prop = rawProp;
245
- const access = effectiveAccess(prop);
246
- if (access?.read?.length !== 0 || access?.write?.length !== 0) continue;
247
- excluded.add(key);
248
- if (prop.columnName) excluded.add(prop.columnName);
246
+ function excludedApiKeys(collection) {
247
+ const unreadable = closedNames(collection, "read");
248
+ const unwritable = closedNames(collection, "write");
249
+ return new Set([...unreadable].filter((name) => unwritable.has(name)));
250
+ }
251
+ /**
252
+ * The names nobody may read (or write), as the server judges them: no viewer
253
+ * is the trusted plane, which every role-naming rule admits, so what is left
254
+ * is exactly the empty rules.
255
+ *
256
+ * `restrictedFieldNames` resolves the relations of a closed relation property
257
+ * to find its foreign key. When those cannot be resolved — already warned
258
+ * about by the caller — the property and column names are still closed.
259
+ */
260
+ function closedNames(collection, kind) {
261
+ try {
262
+ return restrictedFieldNames(collection, void 0, kind).refused;
263
+ } catch {
264
+ const names = /* @__PURE__ */ new Set();
265
+ for (const [key, rawProp] of Object.entries(collection.properties ?? {})) {
266
+ const prop = rawProp;
267
+ if (effectiveAccess(prop)?.[kind]?.length !== 0) continue;
268
+ names.add(key);
269
+ if (prop.columnName) names.add(prop.columnName);
270
+ }
271
+ return names;
249
272
  }
250
- return excluded;
251
273
  }
252
274
  /**
253
275
  * The keys nobody may write: `access: { write: [] }`, readable or not.
@@ -259,17 +281,11 @@ function excludedApiKeys(properties) {
259
281
  * generated `create({ computed })` compiled and could only ever be a 400.
260
282
  *
261
283
  * A write rule naming *roles* is not this: some caller can write it, and one
262
- * generated shape cannot say which.
284
+ * generated shape cannot say which. A closed to-one relation's foreign key is:
285
+ * the server refuses `reviewerId` exactly as it refuses `reviewer`.
263
286
  */
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;
287
+ function unwritableKeys(collection) {
288
+ return closedNames(collection, "write");
273
289
  }
274
290
  function generateTypedefs(input) {
275
291
  const collections = sortCollectionsBySlug(input);
@@ -301,7 +317,7 @@ function generateTypedefs(input) {
301
317
  lines.push(` ${emitKey(accessors.get(collection.slug))}: {`);
302
318
  lines.push(" Row: {");
303
319
  const emittedKeys = /* @__PURE__ */ new Set();
304
- const excluded = excludedApiKeys(properties);
320
+ const excluded = excludedApiKeys(collection);
305
321
  for (const key of excluded) emittedKeys.add(key);
306
322
  for (const [key, rawProp] of Object.entries(properties)) {
307
323
  const prop = rawProp;
@@ -335,7 +351,7 @@ function generateTypedefs(input) {
335
351
  }
336
352
  lines.push(" };");
337
353
  lines.push(" Insert: {");
338
- const notWritable = /* @__PURE__ */ new Set([...excluded, ...unwritableKeys(properties)]);
354
+ const notWritable = /* @__PURE__ */ new Set([...excluded, ...unwritableKeys(collection)]);
339
355
  emittedKeys.clear();
340
356
  for (const key of notWritable) emittedKeys.add(key);
341
357
  for (const [key, rawProp] of Object.entries(properties)) {
@@ -347,8 +363,8 @@ function generateTypedefs(input) {
347
363
  lines.push(line(key, writableType(prop, tsType), isOptional));
348
364
  emittedKeys.add(key);
349
365
  }
350
- emitWritableRelations(lines, collection, properties, resolvedRelations, emittedKeys, false);
351
- lines.push(" };");
366
+ const eitherSpelling = emitWritableRelations(lines, collection, properties, resolvedRelations, emittedKeys, false);
367
+ lines.push(eitherSpelling.length === 0 ? " };" : ` } & ${eitherSpelling.join(" & ")};`);
352
368
  lines.push(" Update: {");
353
369
  emittedKeys.clear();
354
370
  for (const key of notWritable) emittedKeys.add(key);
@@ -362,14 +378,15 @@ function generateTypedefs(input) {
362
378
  }
363
379
  emitWritableRelations(lines, collection, properties, resolvedRelations, emittedKeys, true);
364
380
  lines.push(" };");
365
- if (Object.keys(resolvedRelations).length === 0) lines.push(" Relations: Record<never, never>;");
381
+ const includable = Object.entries(resolvedRelations).filter(([key]) => !excluded.has(key));
382
+ if (includable.length === 0) lines.push(" Relations: Record<never, never>;");
366
383
  else lines.push(" Relations: {");
367
- for (const [key, relation] of Object.entries(resolvedRelations)) {
384
+ for (const [key, relation] of includable) {
368
385
  const slug = resolveTargetCollection(relation)?.slug ?? relation.targetSlug;
369
386
  const accessor = slug ? accessors.get(slug) : void 0;
370
387
  lines.push(` ${emitKey(key)}: ${accessor ? emitString(accessor) : "never"};`);
371
388
  }
372
- if (Object.keys(resolvedRelations).length > 0) lines.push(" };");
389
+ if (includable.length > 0) lines.push(" };");
373
390
  lines.push(` Slug: ${emitString(collection.slug)};`);
374
391
  lines.push(" };");
375
392
  }
@@ -489,14 +506,38 @@ function includeHelperLines() {
489
506
  * finds there is one. A relation whose `relationName` differs from its property
490
507
  * key is reachable as the property and not as the name, so emitting the name
491
508
  * would have offered a key that writes to a column that does not exist.
509
+ *
510
+ * A **required** `belongsTo` is satisfied by either spelling — the server's
511
+ * required check reads both (`write-validation.ts`). Emitting each name as a
512
+ * required key made a create name the same row twice to compile. So on an
513
+ * `Insert` both names go in optional, and the requirement comes back as one
514
+ * returned `({ authorId: T } | { author: T })` for the caller to intersect
515
+ * with the object. Both stay in the object so `keyof Insert` still names them.
492
516
  */
493
517
  function emitWritableRelations(lines, collection, properties, resolvedRelations, emittedKeys, allOptional) {
518
+ const spellings = /* @__PURE__ */ new Map();
519
+ const addSpelling = (relation, key) => {
520
+ if (emittedKeys.has(key)) return;
521
+ const keys = spellings.get(relation) ?? [];
522
+ if (!keys.includes(key)) keys.push(key);
523
+ spellings.set(relation, keys);
524
+ };
525
+ for (const relation of Object.values(resolvedRelations)) if (relation.kind === "belongsTo" && relation.localKey) addSpelling(relation, fieldKeyForColumn(collection, relation.localKey));
526
+ for (const [key, rawProp] of Object.entries(properties)) {
527
+ if (rawProp.type !== "relation") continue;
528
+ const relation = findRelation(resolvedRelations, key);
529
+ if (relation?.kind === "belongsTo" && relation.localKey) addSpelling(relation, key);
530
+ }
531
+ const eitherSpelling = [];
494
532
  const emit = (key, relation) => {
495
533
  if (emittedKeys.has(key)) return;
496
534
  const required = isRelationRequired(collection, relation);
497
535
  const type = required ? foreignKeyType(relation) : `${foreignKeyType(relation)} | null`;
498
- lines.push(line(key, type, allOptional || !required));
536
+ const names = spellings.get(relation) ?? [key];
537
+ const oneOfSeveral = required && !allOptional && names.length > 1;
538
+ lines.push(line(key, type, allOptional || !required || oneOfSeveral));
499
539
  emittedKeys.add(key);
540
+ if (oneOfSeveral && names[0] === key) eitherSpelling.push(`(${names.map((name) => `{ ${emitKey(name)}: ${type} }`).join(" | ")})`);
500
541
  };
501
542
  for (const relation of Object.values(resolvedRelations)) if (relation.kind === "belongsTo" && relation.localKey) emit(fieldKeyForColumn(collection, relation.localKey), relation);
502
543
  for (const [key, rawProp] of Object.entries(properties)) {
@@ -504,6 +545,14 @@ function emitWritableRelations(lines, collection, properties, resolvedRelations,
504
545
  const relation = findRelation(resolvedRelations, key);
505
546
  if (relation?.kind === "belongsTo" && relation.localKey) emit(key, relation);
506
547
  }
548
+ for (const [key, rawProp] of Object.entries(properties)) {
549
+ if (rawProp.type !== "relation" || emittedKeys.has(key)) continue;
550
+ const relation = findRelation(resolvedRelations, key);
551
+ const toOneWritable = relation?.kind === "hasOne" || relation?.kind === "via" && relation.cardinality === "one" && relation.writable;
552
+ if (!relation || !toOneWritable) continue;
553
+ lines.push(line(key, `${foreignKeyType(relation)} | null`, true));
554
+ emittedKeys.add(key);
555
+ }
507
556
  for (const [key, relation] of Object.entries(resolvedRelations)) {
508
557
  if (relation.cardinality !== "many" || !relation.writable) continue;
509
558
  if (emittedKeys.has(key)) continue;
@@ -513,6 +562,7 @@ function emitWritableRelations(lines, collection, properties, resolvedRelations,
513
562
  lines.push(line(key, `Array<${element}>`, true));
514
563
  emittedKeys.add(key);
515
564
  }
565
+ return eitherSpelling;
516
566
  }
517
567
  /**
518
568
  * The `_pivot` a many-to-many link carries: the junction's payload columns,
@@ -545,6 +595,7 @@ var doctor_exports = /* @__PURE__ */ __exportAll({
545
595
  checkCollectionsVsDatabase: () => checkCollectionsVsDatabase,
546
596
  checkCollectionsVsSchema: () => checkCollectionsVsSchema,
547
597
  checkCollectionsVsSdk: () => checkCollectionsVsSdk,
598
+ generatedSchemaDriftIssues: () => generatedSchemaDriftIssues,
548
599
  getExpectedColumnType: () => getExpectedColumnType,
549
600
  junctionKeyIssues: () => junctionKeyIssues,
550
601
  loadCollections: () => loadCollections,
@@ -860,7 +911,63 @@ function junctionKeyIssues(junctionPlan, dbColumns, dbPk, displayName) {
860
911
  issues.push(...primaryKeyIssues(junctionPlan, dbPk, displayName));
861
912
  return issues;
862
913
  }
863
- async function checkCollectionsVsDatabase(collections, databaseUrl) {
914
+ /** A table as the other issues name it: bare in `public`, `schema.table` elsewhere. */
915
+ function displayTableOf(difference, collections) {
916
+ const collection = difference.collection ? collections.find((c) => c.slug === difference.collection) : void 0;
917
+ const schema = collection && isPostgresCollectionConfig(collection) ? collection.schema : void 0;
918
+ return schema && schema !== "public" ? `${schema}.${difference.table}` : difference.table;
919
+ }
920
+ /** Which of the doctor's own categories already says what a boot difference says. */
921
+ var COVERING_CATEGORY = {
922
+ "missing-table": "missing_table",
923
+ "missing-column": "missing_column",
924
+ type: "type_mismatch",
925
+ nullability: "nullability_mismatch",
926
+ "extra-column": "extra_column"
927
+ };
928
+ /** What to do about one boot difference, said the way the boot warning says it. */
929
+ function generatedDriftFix(difference) {
930
+ switch (difference.kind) {
931
+ case "nullability": return difference.generated === "NOT NULL" ? "`rebase db push` applies it; a NOT NULL needs every row filled first (UPDATE the empty ones, or give the property a default). Boot adds one only with a new column on an empty table. If the collections changed since schema.generated.ts was written, run `rebase schema generate` first." : "`rebase db push` drops the NOT NULL, which boot never does. If the column should stay required, mark the property `validation: { required: true }`. If the collections changed since schema.generated.ts was written, run `rebase schema generate` first.";
932
+ case "extra-column": return "Boot never drops a column, so its data is still there. `rebase db push` removes it (it asks first, since that deletes the data), or a migration does (`rebase db generate`). If a property should still read it, declare it again. If the collections changed since schema.generated.ts was written, run `rebase schema generate` first.";
933
+ case "type": return "`rebase db push` converts it (it asks first when values may not convert), or a migration does. If the collections changed since schema.generated.ts was written, run `rebase schema generate` first.";
934
+ case "missing-column": return "`rebase db push` adds it. If the collections changed since schema.generated.ts was written, run `rebase schema generate` first.";
935
+ case "missing-table": return "Run `rebase db push` or `rebase db generate && rebase db migrate`";
936
+ }
937
+ }
938
+ /**
939
+ * Boot's drift warnings, as doctor issues — one per difference, in boot's own
940
+ * words ({@link describeSchemaDifference}), and a warning, as boot raises them.
941
+ *
942
+ * Except where an issue the collections-side checks above already raised says
943
+ * the same thing: a missing table, a missing column or a type mismatch on the
944
+ * same column. Nullability and columns no collection declares are boot's alone;
945
+ * the doctor never compared them, which is how it came to say "In sync" beside
946
+ * a boot that had just warned about both.
947
+ */
948
+ function generatedSchemaDriftIssues(differences, already, collections) {
949
+ const bare = (table) => table?.split(".").pop();
950
+ const covered = (difference) => already.some((issue) => issue.category === COVERING_CATEGORY[difference.kind] && bare(issue.table) === difference.table && (difference.kind === "missing-table" || issue.column === difference.column));
951
+ return differences.filter((difference) => !covered(difference)).map((difference) => ({
952
+ severity: "warning",
953
+ category: COVERING_CATEGORY[difference.kind],
954
+ table: displayTableOf(difference, collections),
955
+ ...difference.column ? { column: difference.column } : {},
956
+ ...difference.generated ? { expected: difference.generated } : {},
957
+ ...difference.live ? { actual: difference.live } : {},
958
+ message: describeSchemaDifference(difference),
959
+ fix: generatedDriftFix(difference)
960
+ }));
961
+ }
962
+ /**
963
+ * The collections whose tables the default database holds — the filter boot
964
+ * applies before it reads the catalogue, for the same reason: a collection
965
+ * routed to `database("analytics")` has no table here.
966
+ */
967
+ function defaultSourceCollections(collections) {
968
+ return collections.filter((c) => !c.dataSource || c.dataSource === DEFAULT_DATA_SOURCE_KEY);
969
+ }
970
+ async function checkCollectionsVsDatabase(collections, databaseUrl, options = {}) {
864
971
  const issues = [];
865
972
  const pgModule = await import("pg");
866
973
  const { Pool } = pgModule.default ?? pgModule;
@@ -1099,6 +1206,15 @@ async function checkCollectionsVsDatabase(collections, databaseUrl) {
1099
1206
  }
1100
1207
  }
1101
1208
  if (plan) issues.push(...orphanedJunctionIssues(plan, columnsByTable, pksByTable, fksByTable));
1209
+ if (options.generated) {
1210
+ const differences = await readGeneratedSchemaDrift({
1211
+ client: pool,
1212
+ generated: options.generated,
1213
+ collections: defaultSourceCollections(collections),
1214
+ defaultSchema: "public"
1215
+ });
1216
+ issues.push(...generatedSchemaDriftIssues(differences, issues, collections));
1217
+ }
1102
1218
  } finally {
1103
1219
  await pool.end();
1104
1220
  }
@@ -1221,6 +1337,8 @@ function formatCategory(cat) {
1221
1337
  missing_table: "Missing Table",
1222
1338
  missing_column: "Missing Column",
1223
1339
  type_mismatch: "Type Mismatch",
1340
+ nullability_mismatch: "Nullability Mismatch",
1341
+ extra_column: "Extra Column",
1224
1342
  missing_constraint: "Missing Constraint",
1225
1343
  primary_key_mismatch: "Primary Key Mismatch",
1226
1344
  schema_stale: "Stale Schema",
@@ -1233,6 +1351,22 @@ function formatCategory(cat) {
1233
1351
  sdk_ungeneratable: "SDK Cannot Be Generated"
1234
1352
  }[cat];
1235
1353
  }
1354
+ var isRecord = (value) => typeof value === "object" && value !== null;
1355
+ /**
1356
+ * The `tables` export of `schema.generated.ts`, imported as boot imports it.
1357
+ *
1358
+ * Nothing, and no error, when the file is not there: phase 1 reports that, and
1359
+ * boot then has nothing to compare either.
1360
+ */
1361
+ async function loadGeneratedTables(schemaPath) {
1362
+ if (!fs$1.existsSync(schemaPath)) return {};
1363
+ try {
1364
+ const module = await import(pathToFileURL(schemaPath).href);
1365
+ return isRecord(module) && isRecord(module.tables) ? { tables: module.tables } : {};
1366
+ } catch (err) {
1367
+ return { error: err instanceof Error ? err.message : String(err) };
1368
+ }
1369
+ }
1236
1370
  async function runDoctor(options) {
1237
1371
  out();
1238
1372
  out(chalk.bold(" 🩺 Loading collections..."));
@@ -1249,7 +1383,17 @@ async function runDoctor(options) {
1249
1383
  if (options.databaseUrl) {
1250
1384
  out(chalk.gray(" Checking Collections → Database..."));
1251
1385
  try {
1252
- schemaToDatabase = await checkCollectionsVsDatabase(collections, options.databaseUrl);
1386
+ const generated = await loadGeneratedTables(options.schemaPath);
1387
+ schemaToDatabase = await checkCollectionsVsDatabase(collections, options.databaseUrl, { generated: generated.tables });
1388
+ if (generated.error) {
1389
+ schemaToDatabase.issues.push({
1390
+ severity: "warning",
1391
+ category: "schema_stale",
1392
+ message: `schema.generated.ts could not be loaded, so it was not compared against the database as boot compares it: ${generated.error}`,
1393
+ fix: "Run `rebase schema generate`, then `rebase doctor` again"
1394
+ });
1395
+ schemaToDatabase.passed = false;
1396
+ }
1253
1397
  } catch (err) {
1254
1398
  const banner = diagnoseDbError(err, options.databaseUrl);
1255
1399
  if (banner) outError(banner);
@@ -1297,4 +1441,4 @@ async function runDoctor(options) {
1297
1441
  //#endregion
1298
1442
  export { loadCollections as n, runDoctor as r, doctor_exports as t };
1299
1443
 
1300
- //# sourceMappingURL=doctor-DvJH-IQG.js.map
1444
+ //# sourceMappingURL=doctor-DW_mlTzz.js.map