@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,108 +0,0 @@
1
- import { CollectionRegistry, resolveCollectionRelations } from "@rebasepro/common";
2
- import { type CollectionConfig } from "@rebasepro/types";
3
- import { PgEnum, PgTable } from "drizzle-orm/pg-core";
4
- import { Relations } from "drizzle-orm";
5
- import { CollectionRegistryInterface } from "../interfaces";
6
- import { getTableName } from "@rebasepro/common";
7
-
8
- /**
9
- * PostgreSQL-specific collection registry.
10
- * Extends the base CollectionRegistry with support for Drizzle ORM tables, enums, and relations.
11
- *
12
- * Satisfies CollectionRegistryInterface through inheritance from CollectionRegistry.
13
- */
14
- export class PostgresCollectionRegistry extends CollectionRegistry implements CollectionRegistryInterface {
15
-
16
- private tables = new Map<string, PgTable>();
17
- private enums = new Map<string, PgEnum<[string, ...string[]]>>();
18
- private relations = new Map<string, Relations>();
19
-
20
- registerTable(table: PgTable, tableName: string) {
21
- this.tables.set(tableName, table);
22
- }
23
-
24
- getTable(tableName: string): PgTable | undefined {
25
- return this.tables.get(tableName);
26
- }
27
-
28
- /**
29
- * Checks if a specific collection has a registered table
30
- */
31
- hasTableForCollection(tableName: string): boolean {
32
- return this.tables.has(tableName);
33
- }
34
-
35
- /**
36
- * Returns all registered table names.
37
- */
38
- getTableNames(): string[] {
39
- return Array.from(this.tables.keys());
40
- }
41
-
42
- /**
43
- * Finds collections assigned to a specific data source that do not have a registered table.
44
- */
45
- getCollectionsWithoutTables(dataSourceKey = "(default)"): CollectionConfig[] {
46
- const collections = this.getCollections().filter(
47
- c => c.dataSource === dataSourceKey || (!c.dataSource && dataSourceKey === "(default)")
48
- );
49
- return collections.filter(c => !this.tables.has(getTableName(c)));
50
- }
51
-
52
- registerEnums(enums: Record<string, PgEnum<[string, ...string[]]>>) {
53
- Object.entries(enums).forEach(([name, value]) => this.enums.set(name, value));
54
- }
55
-
56
- registerRelations(relations: Record<string, Relations>) {
57
- Object.entries(relations).forEach(([name, value]) => this.relations.set(name, value));
58
- }
59
-
60
- getEnum(name: string): PgEnum<[string, ...string[]]> | undefined {
61
- return this.enums.get(name);
62
- }
63
-
64
- getRelation(name: string): Relations | undefined {
65
- return this.relations.get(name);
66
- }
67
-
68
- getAllEnums(): Record<string, PgEnum<[string, ...string[]]>> {
69
- return Object.fromEntries(this.enums.entries());
70
- }
71
-
72
- getAllRelations(): Record<string, Relations> {
73
- return Object.fromEntries(this.relations.entries());
74
- }
75
-
76
- /**
77
- * Get the merged schema object (tables + relations) for use with Drizzle's
78
- * relational query API (`db.query`).
79
- */
80
- getMergedSchema(): Record<string, unknown> {
81
- const result: Record<string, unknown> = {};
82
- for (const [name, table] of this.tables.entries()) {
83
- result[name] = table;
84
- }
85
- for (const [name, relation] of this.relations.entries()) {
86
- result[name] = relation;
87
- }
88
- return result;
89
- }
90
-
91
- /**
92
- * Get the available Drizzle relation keys for a given collection path.
93
- * Maps from the collection's relation property names to the Drizzle relation names
94
- * defined in the schema.
95
- */
96
- getRelationKeysForCollection(collectionPath: string): string[] {
97
- const collection = this.getCollectionByPath(collectionPath);
98
- if (!collection) return [];
99
- // Resolved, not authored. `relationName` is optional at the authoring
100
- // surface and defaults to the property key or the target's slug, so
101
- // reading the raw field dropped every relation that relied on the
102
- // default — and never saw relations declared inline on a property at
103
- // all, since those are not in the `relations` array.
104
- return Object.keys(resolveCollectionRelations(collection));
105
- }
106
-
107
- }
108
-
@@ -1,68 +0,0 @@
1
- import { isTable, getTableName, Relations } from "drizzle-orm";
2
- import { PgEnum, PgTable } from "drizzle-orm/pg-core";
3
- import { CollectionConfig } from "@rebasepro/types";
4
- import { logger } from "@rebasepro/server";
5
- import { PostgresCollectionRegistry } from "./PostgresCollectionRegistry";
6
- import { warnOnKeysTheAdminCannotResolve } from "../services/collection-helpers";
7
- import { assertRelationsResolve } from "./validate-relations";
8
-
9
- /**
10
- * Everything a registry is built from: the collections, and the drizzle schema
11
- * they are backed by. In BaaS mode all of it is introspected from the live
12
- * database; when collections are declared it comes from the config and the generated schema.
13
- */
14
- export interface RegistrySchema {
15
- collections?: CollectionConfig[];
16
- tables?: Record<string, unknown>;
17
- enums?: Record<string, PgEnum<[string, ...string[]]>>;
18
- relations?: Record<string, Relations>;
19
- }
20
-
21
- /**
22
- * Build the collection registry for a driver.
23
- *
24
- * The order matters and is the reason this is one function rather than a run of
25
- * statements in the bootstrapper. Keys are resolved from the drizzle schema, so
26
- * anything that inspects them has to run *after* the tables are registered —
27
- * and `warnOnKeysTheAdminCannotResolve` fails open if it does not, because a
28
- * collection whose table it cannot look up is one it has nothing to say about.
29
- * Warned too early, it would skip every collection and report nothing, which
30
- * reads exactly like having nothing to report.
31
- */
32
- export function buildCollectionRegistry(schema: RegistrySchema): PostgresCollectionRegistry {
33
- const registry = new PostgresCollectionRegistry();
34
-
35
- if (schema.collections) {
36
- registry.registerMultiple(schema.collections);
37
- // `Auto-discovered collections` already reports the count and the
38
- // directory they came from; this is the same fact with the names.
39
- logger.debug(
40
- `📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: ` +
41
- `[${registry.getCollections().map(c => c.slug).join(", ")}]`
42
- );
43
- }
44
-
45
- if (schema.tables) {
46
- Object.values(schema.tables).forEach((table) => {
47
- if (isTable(table)) {
48
- registry.registerTable(table as PgTable, getTableName(table));
49
- }
50
- });
51
- }
52
-
53
- if (schema.enums) registry.registerEnums(schema.enums);
54
- if (schema.relations) registry.registerRelations(schema.relations);
55
-
56
- // Now that the keys resolve: say which of them the admin cannot see. It
57
- // compiles the same collection files into its bundle but never the drizzle
58
- // schema, and nothing serves it one, so only an edit to the config fixes it.
59
- warnOnKeysTheAdminCannotResolve(registry.getCollections(), registry);
60
-
61
- // And now that the tables resolve: refuse to start on a relation whose
62
- // names do not exist. The union checks a relation's shape at compile time;
63
- // only here is there a schema to check its *names* against. Every one of
64
- // these used to surface as an empty result at query time and nothing else.
65
- assertRelationsResolve(registry.getCollections(), registry);
66
-
67
- return registry;
68
- }
@@ -1,402 +0,0 @@
1
- import { getTableColumns } from "drizzle-orm";
2
- import { PgTable } from "drizzle-orm/pg-core";
3
- import { CollectionConfig, ResolvedRelation } from "@rebasepro/types";
4
- import { getTableName, resolveCollectionRelations } from "@rebasepro/common";
5
- import { generateForeignKeyName, legacyForeignKeyName } from "@rebasepro/utils";
6
-
7
- import { PostgresCollectionRegistry } from "./PostgresCollectionRegistry";
8
-
9
- /**
10
- * Check every relation against the schema it actually runs on, at boot.
11
- *
12
- * The tagged union made the *shape* of a relation impossible to get wrong: a
13
- * `manyToMany` cannot carry a `foreignKeyOnTarget`, a to-many cannot carry a
14
- * `localKey`. What it cannot know is whether any of the names are real —
15
- * whether `posts_tags` is a table, whether `author_id` is a column, whether a
16
- * `joinPath` connects the tables it claims to. Those are facts about the
17
- * database, and the type system never sees them.
18
- *
19
- * Until now nothing checked them until a query ran, and the failures were the
20
- * quiet kind. A missing junction table logged a warning and returned no rows,
21
- * so `posts/1/tags` answered `[]` — indistinguishable from a post with no tags.
22
- * The relation looked configured, the admin drew the tab, the tab was empty,
23
- * and nothing anywhere said why.
24
- *
25
- * The junction default is the sharp edge this exists for. `through.table`
26
- * defaults to the two table names sorted and joined, so renaming a table
27
- * silently re-points the relation at a name that was never created. It is the
28
- * one default whose output changes when you edit something that looks
29
- * unrelated.
30
- */
31
- export interface RelationDefect {
32
- /** Slug of the collection declaring the relation. */
33
- collection: string;
34
- relationName: string;
35
- kind: ResolvedRelation["kind"];
36
- /** What is wrong, in terms of the schema. */
37
- problem: string;
38
- /** The edit that fixes it. */
39
- fix: string;
40
- }
41
-
42
- /**
43
- * Every name a column answers to: the key in the drizzle schema and the real
44
- * column name in Postgres. A relation may legitimately be written with either,
45
- * and reporting a working relation as broken is worse than not checking.
46
- */
47
- function columnNames(table: PgTable): Set<string> {
48
- const names = new Set<string>();
49
- for (const [key, col] of Object.entries(getTableColumns(table))) {
50
- names.add(key);
51
- const dbName = (col as { name?: string })?.name;
52
- if (dbName) names.add(dbName);
53
- }
54
- return names;
55
- }
56
-
57
- const quote = (xs: Iterable<string>) => Array.from(xs).map(s => `\`${s}\``).join(", ");
58
-
59
- /** `on.from` / `on.to` accept a single column or a composite tuple. */
60
- const asColumns = (value: string | string[]): string[] => Array.isArray(value) ? value : [value];
61
-
62
- /**
63
- * Distinguish "this column name is wrong" from "the generated schema is old".
64
- *
65
- * They present identically here — a relation asks for a column the registered
66
- * table does not have — but they are opposite problems with opposite fixes, and
67
- * getting them the wrong way round is how the 0.12 → 0.13 upgrade bricked
68
- * projects.
69
- *
70
- * The registered table is not the database. It comes from the project's
71
- * checked-in `backend/src/schema.generated.ts`, and 0.13 changed the rule that
72
- * derives foreign-key names: `categories` yields `category_id` where it used to
73
- * yield `categorie_id`. Boot-ensure renames the database column to match, so by
74
- * the time this runs the *database* is correct and the *generated module* is the
75
- * stale one. Reporting "not a column" then points at the wrong artifact, and the
76
- * generic fix — "set `through.targetColumn` to one of: …", listing the legacy
77
- * name because that is what the stale module still has — talks the reader into
78
- * pinning a column that no longer exists.
79
- *
80
- * So when the wanted name is what the current rule derives, and the table
81
- * carries what the *previous* rule would have derived from the same source, say
82
- * that instead.
83
- *
84
- * @param wanted the column the relation asks for
85
- * @param available every column the registered table has
86
- * @param sources names the default could have been derived from (a slug, a
87
- * relation name) — checking against these rather than guessing
88
- * backwards from `wanted` keeps the match exact
89
- */
90
- function staleCodegenRename(
91
- wanted: string,
92
- available: Set<string>,
93
- sources: string[]
94
- ): { legacy: string; current: string } | null {
95
- for (const source of sources) {
96
- if (!source) continue;
97
- const current = generateForeignKeyName(source);
98
- const legacy = legacyForeignKeyName(source);
99
- // Only a name that actually moved, and only when the table still has the
100
- // old spelling and not the new one.
101
- if (current !== wanted || legacy === current) continue;
102
- if (available.has(legacy) && !available.has(current)) return { legacy, current };
103
- }
104
- return null;
105
- }
106
-
107
- /** The shared explanation, so every relation kind reports it identically. */
108
- function staleCodegenDefect(
109
- table: string,
110
- { legacy, current }: { legacy: string; current: string }
111
- ): Pick<RelationDefect, "problem" | "fix"> {
112
- return {
113
- problem:
114
- `the generated Drizzle schema still declares \`${legacy}\` on \`${table}\`, but this ` +
115
- `release derives \`${current}\` — the generated schema predates the foreign-key ` +
116
- "naming fix and no longer describes the database",
117
- fix:
118
- "regenerate it with `rebase schema generate` (or `pnpm run schema:generate`). The " +
119
- "database column has already been renamed for you at boot, so nothing else is needed. " +
120
- `To keep \`${legacy}\` instead, name it explicitly on the relation and regenerate.`
121
- };
122
- }
123
-
124
- /**
125
- * Relations whose names do not resolve against the registered schema.
126
- *
127
- * Fails open wherever it cannot see enough to be sure — an unregistered source
128
- * table, a target belonging to another backend — because a false alarm here
129
- * costs more than a missed one: it would block boot on a working app.
130
- */
131
- export function findRelationDefects(
132
- collections: CollectionConfig[],
133
- registry: PostgresCollectionRegistry
134
- ): RelationDefect[] {
135
- const defects: RelationDefect[] = [];
136
- const registeredSlugs = new Set(registry.getCollections().map(c => c.slug));
137
-
138
- for (const collection of collections) {
139
- const sourceTableName = getTableName(collection);
140
- const sourceTable = registry.getTable(sourceTableName);
141
- // Nothing to check against. Another boot warning already covers this.
142
- if (!sourceTable) continue;
143
-
144
- const sourceColumns = columnNames(sourceTable);
145
- const relations = resolveCollectionRelations(collection);
146
-
147
- for (const relation of Object.values(relations)) {
148
- const at = { collection: collection.slug,
149
- relationName: relation.relationName,
150
- kind: relation.kind };
151
-
152
- let targetCollection: CollectionConfig;
153
- try {
154
- targetCollection = relation.target();
155
- } catch (e) {
156
- defects.push({
157
- ...at,
158
- problem: `its \`target()\` threw: ${e instanceof Error ? e.message : String(e)}`,
159
- fix: "a target thunk usually throws because of a circular import — make sure it is `() => otherCollection` and not evaluated at module load"
160
- });
161
- continue;
162
- }
163
-
164
- // A target this registry has never heard of belongs to another
165
- // backend; its tables are not ours to check.
166
- if (!registeredSlugs.has(targetCollection.slug)) continue;
167
-
168
- const targetTableName = getTableName(targetCollection);
169
- const targetTable = registry.getTable(targetTableName);
170
- if (!targetTable) {
171
- defects.push({
172
- ...at,
173
- problem: `it points at collection \`${targetCollection.slug}\`, which has no table \`${targetTableName}\` in the schema`,
174
- fix: `create the \`${targetTableName}\` table, or correct \`table\` on the \`${targetCollection.slug}\` collection`
175
- });
176
- continue;
177
- }
178
- const targetColumns = columnNames(targetTable);
179
-
180
- switch (relation.kind) {
181
- case "belongsTo": {
182
- if (!sourceColumns.has(relation.localKey)) {
183
- // `localKey` defaults to the relation name run through
184
- // the foreign-key rule, so it moves with that rule.
185
- const stale = staleCodegenRename(
186
- relation.localKey,
187
- sourceColumns,
188
- [relation.relationName, targetCollection.slug]
189
- );
190
- defects.push(stale
191
- ? { ...at, ...staleCodegenDefect(sourceTableName, stale) }
192
- : {
193
- ...at,
194
- problem: `\`localKey: "${relation.localKey}"\` is not a column on \`${sourceTableName}\``,
195
- fix: `add the column, or set \`localKey\` to one of: ${quote(sourceColumns)}`
196
- });
197
- }
198
- break;
199
- }
200
-
201
- case "hasOne":
202
- case "hasMany": {
203
- if (!targetColumns.has(relation.foreignKeyOnTarget)) {
204
- // The default is derived from *this* collection's slug —
205
- // the column on the target that points back here.
206
- const stale = staleCodegenRename(
207
- relation.foreignKeyOnTarget,
208
- targetColumns,
209
- [collection.slug]
210
- );
211
- defects.push(stale
212
- ? { ...at, ...staleCodegenDefect(targetTableName, stale) }
213
- : {
214
- ...at,
215
- problem: `\`foreignKeyOnTarget: "${relation.foreignKeyOnTarget}"\` is not a column on the target table \`${targetTableName}\``,
216
- fix: `add the column, or set \`foreignKeyOnTarget\` to one of: ${quote(targetColumns)}`
217
- });
218
- }
219
- // `sourceKey` is the easiest of the two to put on the wrong
220
- // side — it is the only column in a `hasMany` that lives
221
- // here rather than on the target, and naming a target column
222
- // reads perfectly well right next to `foreignKeyOnTarget`.
223
- if (relation.sourceKey && !sourceColumns.has(relation.sourceKey)) {
224
- defects.push({
225
- ...at,
226
- problem: `\`sourceKey: "${relation.sourceKey}"\` is not a column on \`${sourceTableName}\``,
227
- fix: targetColumns.has(relation.sourceKey)
228
- ? `it is a column on the *target* table \`${targetTableName}\` — \`sourceKey\` names ` +
229
- "the column on this collection that the target's foreign key points at, so it " +
230
- `must be one of: ${quote(sourceColumns)}`
231
- : `add the column, or set \`sourceKey\` to one of: ${quote(sourceColumns)}`
232
- });
233
- }
234
- break;
235
- }
236
-
237
- case "manyToMany": {
238
- const { table, sourceColumn, targetColumn } = relation.through;
239
- const junction = registry.getTable(table);
240
- if (!junction) {
241
- defects.push({
242
- ...at,
243
- problem: `its junction table \`${table}\` does not exist`,
244
- fix: `create \`${table}\`, or name the real one with \`through: { table: "..." }\`. ` +
245
- "Note that an omitted `through.table` is derived from the two table names sorted " +
246
- "and joined, so renaming a table changes it"
247
- });
248
- break;
249
- }
250
- const junctionColumns = columnNames(junction);
251
- // Junction columns are the ones that actually moved in 0.13:
252
- // each defaults to its endpoint collection's *slug* run
253
- // through the foreign-key rule, and slugs are plural.
254
- const derivedFrom = {
255
- sourceColumn: [collection.slug],
256
- targetColumn: [targetCollection.slug]
257
- } as const;
258
- for (const [label, column] of [["sourceColumn", sourceColumn], ["targetColumn", targetColumn]] as const) {
259
- if (!junctionColumns.has(column)) {
260
- const stale = staleCodegenRename(column, junctionColumns, [...derivedFrom[label]]);
261
- defects.push(stale
262
- ? { ...at, ...staleCodegenDefect(table, stale) }
263
- : {
264
- ...at,
265
- problem: `\`through.${label}: "${column}"\` is not a column on the junction table \`${table}\``,
266
- fix: `set \`through.${label}\` to one of: ${quote(junctionColumns)}` +
267
- (label === "sourceColumn" ? " — it is the column naming *this* collection" : "")
268
- });
269
- }
270
- }
271
- break;
272
- }
273
-
274
- case "via": {
275
- if (relation.joinPath.length === 0) {
276
- defects.push({
277
- ...at,
278
- problem: "its `joinPath` is empty, so it joins nothing",
279
- fix: "add at least one step, ending at the target's table"
280
- });
281
- break;
282
- }
283
-
284
- // Walk the chain: each step's `from` names columns on the
285
- // previous table, its `to` names columns on its own.
286
- let prevName = sourceTableName;
287
- let prevColumns = sourceColumns;
288
- let broken = false;
289
-
290
- for (const [i, step] of relation.joinPath.entries()) {
291
- const stepTable = registry.getTable(step.table);
292
- if (!stepTable) {
293
- defects.push({
294
- ...at,
295
- problem: `step ${i + 1} of its \`joinPath\` joins \`${step.table}\`, which is not a table in the schema`,
296
- fix: `correct \`joinPath[${i}].table\``
297
- });
298
- broken = true;
299
- break;
300
- }
301
- const stepColumns = columnNames(stepTable);
302
-
303
- for (const column of asColumns(step.on.from)) {
304
- if (!prevColumns.has(column)) {
305
- defects.push({
306
- ...at,
307
- problem: `step ${i + 1} joins \`${prevName}.${column}\` → \`${step.table}\`, but \`${column}\` is not a column on \`${prevName}\``,
308
- fix: `\`joinPath[${i}].on.from\` names columns on ${i === 0 ? "this collection's table" : `the previous step's table (\`${prevName}\`)`}: ${quote(prevColumns)}`
309
- });
310
- }
311
- }
312
- for (const column of asColumns(step.on.to)) {
313
- if (!stepColumns.has(column)) {
314
- defects.push({
315
- ...at,
316
- problem: `step ${i + 1} joins into \`${step.table}.${column}\`, but \`${column}\` is not a column on \`${step.table}\``,
317
- fix: `\`joinPath[${i}].on.to\` names columns on \`${step.table}\`: ${quote(stepColumns)}`
318
- });
319
- }
320
- }
321
-
322
- if (asColumns(step.on.from).length !== asColumns(step.on.to).length) {
323
- defects.push({
324
- ...at,
325
- problem: `step ${i + 1} compares ${asColumns(step.on.from).length} column(s) against ${asColumns(step.on.to).length}`,
326
- fix: `\`from\` and \`to\` must name the same number of columns in \`joinPath[${i}]\``
327
- });
328
- }
329
-
330
- prevName = step.table;
331
- prevColumns = stepColumns;
332
- }
333
-
334
- // The chain has to end where the relation says it points,
335
- // or the rows it returns are not the target's rows.
336
- if (!broken && prevName !== targetTableName) {
337
- defects.push({
338
- ...at,
339
- problem: `its \`joinPath\` ends at \`${prevName}\`, but it targets \`${targetCollection.slug}\` (table \`${targetTableName}\`)`,
340
- fix: `make the last step join \`${targetTableName}\`, or point \`target\` at the collection backed by \`${prevName}\``
341
- });
342
- }
343
- break;
344
- }
345
-
346
- default: {
347
- const exhaustive: never = relation;
348
- throw new Error(`Unhandled relation kind: ${JSON.stringify(exhaustive)}`);
349
- }
350
- }
351
- }
352
- }
353
-
354
- return defects;
355
- }
356
-
357
- /**
358
- * Fail boot on any relation that cannot resolve, listing all of them at once.
359
- *
360
- * Deliberately fatal rather than a warning. Every one of these produces an
361
- * empty result at query time and nothing else — an empty tab, an empty
362
- * `include`, a subcollection that looks like it has no rows. A server that
363
- * refuses to start is recoverable in a minute; a relation that quietly answers
364
- * "nothing" is the kind of bug found in production, weeks later, by a user
365
- * asking where their data went.
366
- */
367
- export function assertRelationsResolve(
368
- collections: CollectionConfig[],
369
- registry: PostgresCollectionRegistry
370
- ): void {
371
- const defects = findRelationDefects(collections, registry);
372
- if (defects.length === 0) return;
373
-
374
- const lines = defects.map(d =>
375
- ` • ${d.collection}.${d.relationName} (${d.kind})\n` +
376
- ` ${d.problem}\n` +
377
- ` fix: ${d.fix}`
378
- );
379
-
380
- throw new Error(
381
- `${defects.length} relation${defects.length === 1 ? "" : "s"} cannot resolve against ` +
382
- "`backend/src/schema.generated.ts`.\n\n" +
383
- "Each of these would return no rows at query time rather than reporting an error, " +
384
- "so they are fatal at boot instead.\n\n" +
385
- // This reads the *generated file*, not the database, and the difference
386
- // is the whole diagnosis after an upgrade. Boot-ensure renames columns
387
- // in the database — a 0.12 → 0.13 upgrade singularises a junction key,
388
- // `categorie_id` → `category_id` — and the checked-in file still
389
- // declares the old name. The config is then correct and the file is
390
- // stale, so the per-defect advice below, which lists the columns this
391
- // file has, names a column that no longer exists in the database.
392
- // Following it turns a recoverable state into a broken config.
393
- //
394
- // Hence the ordering: regenerate first, and only then consider that the
395
- // collection might be the thing that is wrong.
396
- "If the database was migrated recently — an upgrade, a `db push`, a restore — this file is\n" +
397
- "probably older than the schema it describes. Regenerate it before changing anything else:\n\n" +
398
- " rebase schema generate\n\n" +
399
- "If it is already current, then the collection is what disagrees with it:\n\n" +
400
- lines.join("\n\n") + "\n"
401
- );
402
- }