@rebasepro/server-postgres 0.17.3-canary.gdd23447 → 0.18.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.
- package/LICENSE +0 -1
- package/README.md +31 -0
- package/dist/backup-argv.d.ts +19 -0
- package/dist/branch-argv.d.ts +28 -0
- package/dist/branch-prune.d.ts +77 -0
- package/dist/cli-collections-path.d.ts +31 -0
- package/dist/cli-errors.d.ts +123 -0
- package/dist/cli-flags.d.ts +101 -0
- package/dist/cli-helpers.d.ts +94 -10
- package/dist/collection-index-C7LopMyX.js +247 -0
- package/dist/collection-index-C7LopMyX.js.map +1 -0
- package/dist/databasePoolManager.d.ts +27 -0
- package/dist/{ensure-collection-policies-UCqgv_8c.js → ensure-collection-policies-CagOzEPS.js} +6 -6
- package/dist/{ensure-collection-policies-UCqgv_8c.js.map → ensure-collection-policies-CagOzEPS.js.map} +1 -1
- package/dist/{ensure-collection-tables-DgVixhX3.js → ensure-collection-tables-DbAhrjnG.js} +614 -52
- package/dist/ensure-collection-tables-DbAhrjnG.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.es.js +19307 -3059
- package/dist/index.es.js.map +1 -1
- package/dist/{rls-enforcement-DvAbL9YJ.js → rls-enforcement-ClJoTboT.js} +53 -189
- package/dist/rls-enforcement-ClJoTboT.js.map +1 -0
- package/dist/schema/column-type-drift.d.ts +79 -0
- package/dist/schema/doctor.d.ts +42 -0
- package/dist/schema/ensure-collection-tables.d.ts +90 -0
- package/dist/schema/generate-next-step.d.ts +30 -0
- package/dist/schema/generate-postgres-ddl-logic.d.ts +28 -0
- package/dist/schema/generated-schema-staleness.d.ts +37 -0
- package/dist/schema/introspect-db-queries.d.ts +34 -0
- package/dist/security/rls-enforcement.d.ts +63 -2
- package/dist/services/BranchService.d.ts +86 -1
- package/dist/services/collection-helpers.d.ts +1 -1
- package/dist/services/realtimeService.d.ts +30 -2
- package/dist/services/row-pipeline.d.ts +12 -1
- package/dist/utils/pg-error-utils.d.ts +3 -1
- package/dist/utils/pg-numeric-number-patch.d.ts +31 -0
- package/package.json +32 -27
- package/dist/backup-service-DCk7KhhL.js +0 -9044
- package/dist/backup-service-DCk7KhhL.js.map +0 -1
- package/dist/collection-index-BRUg10H5.js +0 -5690
- package/dist/collection-index-BRUg10H5.js.map +0 -1
- package/dist/connection-GOKU3Hu5.js +0 -277
- package/dist/connection-GOKU3Hu5.js.map +0 -1
- package/dist/ensure-collection-tables-DgVixhX3.js.map +0 -1
- package/dist/rls-bootstrap-sql-B5C9LoJ6.js +0 -244
- package/dist/rls-bootstrap-sql-B5C9LoJ6.js.map +0 -1
- package/dist/rls-enforcement-DvAbL9YJ.js.map +0 -1
- package/dist/src-DiB5RP2Z.js +0 -541
- package/dist/src-DiB5RP2Z.js.map +0 -1
- package/dist/websocket-BZ4H5wUz.js +0 -8198
- package/dist/websocket-BZ4H5wUz.js.map +0 -1
- package/src/PostgresAdapter.ts +0 -91
- package/src/PostgresBackendDriver.ts +0 -1857
- package/src/PostgresBootstrapper.ts +0 -1157
- package/src/auth/ensure-tables.ts +0 -896
- package/src/auth/schema-version.ts +0 -260
- package/src/auth/services.ts +0 -1610
- package/src/backup/backup-cli.ts +0 -520
- package/src/backup/backup-cron.ts +0 -212
- package/src/backup/backup-logic.ts +0 -93
- package/src/backup/backup-service.ts +0 -485
- package/src/backup/index.ts +0 -12
- package/src/backup/pg-tools.ts +0 -403
- package/src/backup/retention.ts +0 -86
- package/src/cli-errors.ts +0 -265
- package/src/cli-helpers.ts +0 -569
- package/src/cli-output.ts +0 -43
- package/src/cli.ts +0 -1339
- package/src/collections/PostgresCollectionRegistry.ts +0 -108
- package/src/collections/buildRegistry.ts +0 -68
- package/src/collections/validate-relations.ts +0 -402
- package/src/connection.ts +0 -333
- package/src/data-transformer.ts +0 -845
- package/src/databasePoolManager.ts +0 -95
- package/src/history/HistoryService.ts +0 -254
- package/src/history/ensure-history-table.ts +0 -53
- package/src/index.ts +0 -19
- package/src/interfaces.ts +0 -60
- package/src/module-dir.ts +0 -7
- package/src/schema/atlas-argv.ts +0 -94
- package/src/schema/auth-schema.ts +0 -298
- package/src/schema/auth-users-columns.ts +0 -131
- package/src/schema/carved-out-migration.ts +0 -404
- package/src/schema/classify-change.ts +0 -436
- package/src/schema/collection-index.ts +0 -427
- package/src/schema/collections-schema-version.ts +0 -103
- package/src/schema/destructive-sql.ts +0 -94
- package/src/schema/doctor-cli.ts +0 -66
- package/src/schema/doctor-policy-checks.ts +0 -105
- package/src/schema/doctor.ts +0 -837
- package/src/schema/drizzle-ddl.ts +0 -33
- package/src/schema/dynamic-tables.test.ts +0 -302
- package/src/schema/dynamic-tables.ts +0 -293
- package/src/schema/ensure-collection-policies.ts +0 -198
- package/src/schema/ensure-collection-tables.test.ts +0 -518
- package/src/schema/ensure-collection-tables.ts +0 -1267
- package/src/schema/generate-drizzle-schema-logic.ts +0 -957
- package/src/schema/generate-drizzle-schema.ts +0 -143
- package/src/schema/generate-postgres-ddl-logic.ts +0 -1339
- package/src/schema/generate-postgres-ddl.ts +0 -180
- package/src/schema/generate-schema-commit.ts +0 -267
- package/src/schema/generated-schema-staleness.ts +0 -284
- package/src/schema/introspect-db-constraints.ts +0 -385
- package/src/schema/introspect-db-inference.ts +0 -255
- package/src/schema/introspect-db-logic.ts +0 -1387
- package/src/schema/introspect-db-naming.ts +0 -15
- package/src/schema/introspect-db-project.ts +0 -78
- package/src/schema/introspect-db-queries.ts +0 -326
- package/src/schema/introspect-db-structure.ts +0 -670
- package/src/schema/introspect-db-types.ts +0 -56
- package/src/schema/introspect-db.ts +0 -257
- package/src/schema/introspect-runtime.test.ts +0 -260
- package/src/schema/introspect-runtime.ts +0 -327
- package/src/schema/non-sql-collections.test.ts +0 -131
- package/src/schema/rls-bootstrap-sql.ts +0 -288
- package/src/schema/search-column.ts +0 -643
- package/src/schema/test-schema.ts +0 -11
- package/src/schema/vector-index.ts +0 -491
- package/src/security/anonymous-grants.test.ts +0 -73
- package/src/security/policy-drift.test.ts +0 -435
- package/src/security/policy-drift.ts +0 -473
- package/src/security/rls-enforcement.ts +0 -505
- package/src/services/BranchService.ts +0 -326
- package/src/services/FetchService.ts +0 -2115
- package/src/services/PersistService.ts +0 -557
- package/src/services/RelationService.ts +0 -978
- package/src/services/RelationWriteService.ts +0 -653
- package/src/services/cdc/CdcListener.ts +0 -103
- package/src/services/cdc/junction-tables.ts +0 -91
- package/src/services/cdc/trigger-cdc.ts +0 -173
- package/src/services/channel-bus/ChannelBus.ts +0 -44
- package/src/services/channel-bus/PostgresChannelBus.ts +0 -299
- package/src/services/channel-bus/index.ts +0 -114
- package/src/services/channel-history.ts +0 -424
- package/src/services/channel-presence.ts +0 -185
- package/src/services/collection-helpers.ts +0 -420
- package/src/services/dataService.ts +0 -256
- package/src/services/index.ts +0 -14
- package/src/services/junction-writes.ts +0 -295
- package/src/services/nested-path.ts +0 -145
- package/src/services/pg-notify-listener.ts +0 -151
- package/src/services/realtimeService.ts +0 -2545
- package/src/services/row-pipeline.ts +0 -240
- package/src/services/write-denial.ts +0 -55
- package/src/types.ts +0 -4
- package/src/utils/connection-string.ts +0 -58
- package/src/utils/drizzle-conditions.ts +0 -2743
- package/src/utils/pg-array-null-patch.ts +0 -42
- package/src/utils/pg-error-utils.ts +0 -391
- 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
|
-
}
|