@rebasepro/server-postgresql 0.1.2 → 0.2.3
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 +22 -6
- package/dist/common/src/data/query_builder.d.ts +51 -0
- package/dist/common/src/index.d.ts +1 -0
- package/dist/common/src/util/entities.d.ts +2 -2
- package/dist/common/src/util/relations.d.ts +1 -1
- package/dist/index.es.js +1435 -738
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1433 -736
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresAdapter.d.ts +6 -0
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +2 -1
- package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +0 -5
- package/dist/server-postgresql/src/auth/ensure-tables.d.ts +2 -1
- package/dist/server-postgresql/src/auth/services.d.ts +37 -15
- package/dist/server-postgresql/src/index.d.ts +1 -0
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +43 -856
- package/dist/server-postgresql/src/schema/default-collections.d.ts +2 -0
- package/dist/server-postgresql/src/schema/doctor.d.ts +10 -1
- package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +1 -0
- package/dist/server-postgresql/src/services/entity-helpers.d.ts +1 -1
- package/dist/server-postgresql/src/services/realtimeService.d.ts +12 -0
- package/dist/server-postgresql/src/websocket.d.ts +2 -1
- package/dist/types/src/controllers/auth.d.ts +9 -8
- package/dist/types/src/controllers/client.d.ts +3 -0
- package/dist/types/src/controllers/data.d.ts +21 -0
- package/dist/types/src/types/auth_adapter.d.ts +356 -0
- package/dist/types/src/types/collections.d.ts +67 -2
- package/dist/types/src/types/database_adapter.d.ts +94 -0
- package/dist/types/src/types/entity_actions.d.ts +7 -1
- package/dist/types/src/types/entity_callbacks.d.ts +1 -1
- package/dist/types/src/types/entity_views.d.ts +36 -1
- package/dist/types/src/types/index.d.ts +2 -0
- package/dist/types/src/types/plugins.d.ts +1 -1
- package/dist/types/src/types/properties.d.ts +24 -5
- package/dist/types/src/types/property_config.d.ts +6 -2
- package/dist/types/src/types/relations.d.ts +1 -1
- package/dist/types/src/types/translations.d.ts +8 -0
- package/dist/types/src/users/user.d.ts +5 -0
- package/package.json +22 -15
- package/src/PostgresAdapter.ts +59 -0
- package/src/PostgresBackendDriver.ts +66 -13
- package/src/PostgresBootstrapper.ts +35 -15
- package/src/auth/ensure-tables.ts +82 -189
- package/src/auth/services.ts +421 -170
- package/src/cli.ts +49 -13
- package/src/data-transformer.ts +78 -8
- package/src/history/HistoryService.ts +25 -2
- package/src/index.ts +1 -0
- package/src/schema/auth-schema.ts +130 -98
- package/src/schema/default-collections.ts +69 -0
- package/src/schema/doctor-cli.ts +5 -1
- package/src/schema/doctor.ts +166 -48
- package/src/schema/generate-drizzle-schema-logic.ts +74 -27
- package/src/schema/generate-drizzle-schema.ts +13 -3
- package/src/schema/introspect-db-inference.ts +5 -5
- package/src/schema/introspect-db-logic.ts +9 -2
- package/src/schema/introspect-db.ts +14 -3
- package/src/services/EntityFetchService.ts +5 -5
- package/src/services/RelationService.ts +2 -2
- package/src/services/entity-helpers.ts +1 -1
- package/src/services/realtimeService.ts +145 -136
- package/src/utils/drizzle-conditions.ts +16 -2
- package/src/websocket.ts +113 -37
- package/test/auth-services.test.ts +163 -74
- package/test/data-transformer-hardening.test.ts +57 -0
- package/test/data-transformer.test.ts +43 -0
- package/test/generate-drizzle-schema.test.ts +7 -5
- package/test/introspect-db-utils.test.ts +4 -1
- package/test/postgresDataDriver.test.ts +147 -1
- package/test/realtimeService.test.ts +7 -7
- package/test/websocket.test.ts +139 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { PostgresCollection } from "@rebasepro/types";
|
|
2
|
+
|
|
3
|
+
export const defaultUsersCollection: PostgresCollection = {
|
|
4
|
+
name: "Users",
|
|
5
|
+
singularName: "User",
|
|
6
|
+
slug: "users",
|
|
7
|
+
table: "users",
|
|
8
|
+
schema: "rebase",
|
|
9
|
+
icon: "Users",
|
|
10
|
+
group: "Settings",
|
|
11
|
+
properties: {
|
|
12
|
+
id: {
|
|
13
|
+
name: "ID",
|
|
14
|
+
type: "string",
|
|
15
|
+
isId: "uuid"
|
|
16
|
+
},
|
|
17
|
+
email: {
|
|
18
|
+
name: "Email",
|
|
19
|
+
type: "string",
|
|
20
|
+
validation: { required: true, unique: true }
|
|
21
|
+
},
|
|
22
|
+
password_hash: {
|
|
23
|
+
name: "Password Hash",
|
|
24
|
+
type: "string",
|
|
25
|
+
ui: { hideFromCollection: true }
|
|
26
|
+
},
|
|
27
|
+
display_name: {
|
|
28
|
+
name: "Display Name",
|
|
29
|
+
type: "string"
|
|
30
|
+
},
|
|
31
|
+
photo_url: {
|
|
32
|
+
name: "Photo URL",
|
|
33
|
+
type: "string"
|
|
34
|
+
},
|
|
35
|
+
email_verified: {
|
|
36
|
+
name: "Email Verified",
|
|
37
|
+
type: "boolean",
|
|
38
|
+
defaultValue: false
|
|
39
|
+
},
|
|
40
|
+
email_verification_token: {
|
|
41
|
+
name: "Email Verification Token",
|
|
42
|
+
type: "string",
|
|
43
|
+
ui: { hideFromCollection: true }
|
|
44
|
+
},
|
|
45
|
+
email_verification_sent_at: {
|
|
46
|
+
name: "Email Verification Sent At",
|
|
47
|
+
type: "date",
|
|
48
|
+
ui: { hideFromCollection: true }
|
|
49
|
+
},
|
|
50
|
+
metadata: {
|
|
51
|
+
name: "Metadata",
|
|
52
|
+
type: "map",
|
|
53
|
+
defaultValue: {},
|
|
54
|
+
ui: { hideFromCollection: true }
|
|
55
|
+
},
|
|
56
|
+
created_at: {
|
|
57
|
+
name: "Created At",
|
|
58
|
+
type: "date",
|
|
59
|
+
autoValue: "on_create",
|
|
60
|
+
ui: { readOnly: true, hideFromCollection: true }
|
|
61
|
+
},
|
|
62
|
+
updated_at: {
|
|
63
|
+
name: "Updated At",
|
|
64
|
+
type: "date",
|
|
65
|
+
autoValue: "on_update",
|
|
66
|
+
ui: { readOnly: true, hideFromCollection: true }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
package/src/schema/doctor-cli.ts
CHANGED
|
@@ -5,14 +5,17 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import path from "path";
|
|
7
7
|
import chalk from "chalk";
|
|
8
|
+
import fs from "fs";
|
|
8
9
|
import { runDoctor } from "./doctor";
|
|
9
10
|
|
|
10
11
|
async function main() {
|
|
11
12
|
const collectionsArg = process.argv.find((a) => a.startsWith("--collections="));
|
|
12
13
|
const schemaArg = process.argv.find((a) => a.startsWith("--schema="));
|
|
14
|
+
const sdkArg = process.argv.find((a) => a.startsWith("--sdk="));
|
|
13
15
|
|
|
14
|
-
const collectionsPath = collectionsArg?.split("=")[1] ?? path.join("..", "
|
|
16
|
+
const collectionsPath = collectionsArg?.split("=")[1] ?? path.join("..", "config", "collections");
|
|
15
17
|
const schemaPath = schemaArg?.split("=")[1] ?? path.join("src", "schema.generated.ts");
|
|
18
|
+
const sdkPath = sdkArg?.split("=")[1] ?? path.join("..", "generated", "sdk", "database.types.ts");
|
|
16
19
|
|
|
17
20
|
// Load .env
|
|
18
21
|
try {
|
|
@@ -32,6 +35,7 @@ async function main() {
|
|
|
32
35
|
const report = await runDoctor({
|
|
33
36
|
collectionsPath: path.resolve(process.cwd(), collectionsPath),
|
|
34
37
|
schemaPath: path.resolve(process.cwd(), schemaPath),
|
|
38
|
+
sdkPath: path.resolve(process.cwd(), sdkPath),
|
|
35
39
|
databaseUrl: databaseUrl ?? undefined
|
|
36
40
|
});
|
|
37
41
|
|
package/src/schema/doctor.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { pathToFileURL } from "url";
|
|
|
14
14
|
import chalk from "chalk";
|
|
15
15
|
import { EntityCollection, isPostgresCollection, Property, NumberProperty, StringProperty, DateProperty, ArrayProperty, MapProperty, RelationProperty } from "@rebasepro/types";
|
|
16
16
|
import { generateSchema } from "./generate-drizzle-schema-logic";
|
|
17
|
+
import { generateTypedefs } from "@rebasepro/sdk-generator";
|
|
17
18
|
import { getTableName, resolveCollectionRelations, findRelation } from "@rebasepro/common";
|
|
18
19
|
import { toSnakeCase } from "@rebasepro/utils";
|
|
19
20
|
|
|
@@ -35,7 +36,7 @@ export type IssueSeverity = "error" | "warning" | "info";
|
|
|
35
36
|
|
|
36
37
|
export interface DoctorIssue {
|
|
37
38
|
severity: IssueSeverity;
|
|
38
|
-
category: "missing_table" | "missing_column" | "type_mismatch" | "missing_constraint" | "schema_stale" | "missing_enum" | "enum_value_mismatch" | "missing_foreign_key";
|
|
39
|
+
category: "missing_table" | "missing_column" | "type_mismatch" | "missing_constraint" | "schema_stale" | "missing_enum" | "enum_value_mismatch" | "missing_foreign_key" | "sdk_stale";
|
|
39
40
|
table?: string;
|
|
40
41
|
column?: string;
|
|
41
42
|
expected?: string;
|
|
@@ -46,6 +47,7 @@ export interface DoctorIssue {
|
|
|
46
47
|
|
|
47
48
|
export interface DoctorReport {
|
|
48
49
|
collectionsToSchema: { passed: boolean; issues: DoctorIssue[] };
|
|
50
|
+
collectionsToSdk: { passed: boolean; issues: DoctorIssue[] };
|
|
49
51
|
schemaToDatabase: { passed: boolean; issues: DoctorIssue[] };
|
|
50
52
|
summary: { passed: number; warnings: number; errors: number };
|
|
51
53
|
}
|
|
@@ -92,6 +94,10 @@ export function getExpectedColumnType(prop: Property): string | null {
|
|
|
92
94
|
return null; // FK columns are derived from the relation, not from the property
|
|
93
95
|
case "reference":
|
|
94
96
|
return "character varying"; // References default to varchar FK
|
|
97
|
+
case "vector":
|
|
98
|
+
return "USER-DEFINED";
|
|
99
|
+
case "binary":
|
|
100
|
+
return "bytea";
|
|
95
101
|
default:
|
|
96
102
|
return null;
|
|
97
103
|
}
|
|
@@ -139,6 +145,9 @@ export async function loadCollections(collectionsPath: string): Promise<EntityCo
|
|
|
139
145
|
}
|
|
140
146
|
}
|
|
141
147
|
|
|
148
|
+
// Sort collections by slug alphabetically to ensure deterministic comparison
|
|
149
|
+
collections.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
150
|
+
|
|
142
151
|
return collections;
|
|
143
152
|
}
|
|
144
153
|
|
|
@@ -203,9 +212,61 @@ issues };
|
|
|
203
212
|
issues };
|
|
204
213
|
}
|
|
205
214
|
|
|
215
|
+
export async function checkCollectionsVsSdk(
|
|
216
|
+
collections: EntityCollection[],
|
|
217
|
+
sdkFilePath: string
|
|
218
|
+
): Promise<{ passed: boolean; issues: DoctorIssue[] }> {
|
|
219
|
+
const issues: DoctorIssue[] = [];
|
|
220
|
+
|
|
221
|
+
// Check if SDK file exists
|
|
222
|
+
if (!fs.existsSync(sdkFilePath)) {
|
|
223
|
+
issues.push({
|
|
224
|
+
severity: "warning",
|
|
225
|
+
category: "sdk_stale",
|
|
226
|
+
message: `Generated SDK typedefs file does not exist at "${sdkFilePath}".`,
|
|
227
|
+
fix: "Run `rebase generate-sdk`"
|
|
228
|
+
});
|
|
229
|
+
return { passed: false, issues };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const expectedSdk = generateTypedefs(collections);
|
|
234
|
+
const actualSdk = await fsPromises.readFile(sdkFilePath, "utf-8");
|
|
235
|
+
|
|
236
|
+
// Normalize whitespace for comparison
|
|
237
|
+
const normalize = (s: string) =>
|
|
238
|
+
s
|
|
239
|
+
.replace(/\/\/.*$/gm, "") // strip single-line comments
|
|
240
|
+
.replace(/\/\*[\s\S]*?\*\//g, "") // strip multi-line comments
|
|
241
|
+
.replace(/\s+/g, " ")
|
|
242
|
+
.trim();
|
|
243
|
+
|
|
244
|
+
if (normalize(expectedSdk) !== normalize(actualSdk)) {
|
|
245
|
+
issues.push({
|
|
246
|
+
severity: "warning",
|
|
247
|
+
category: "sdk_stale",
|
|
248
|
+
message: "Generated SDK types are out of date — collection definitions have changed since last SDK generation.",
|
|
249
|
+
fix: "Run `rebase generate-sdk`"
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
} catch (err: unknown) {
|
|
253
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
254
|
+
issues.push({
|
|
255
|
+
severity: "warning",
|
|
256
|
+
category: "sdk_stale",
|
|
257
|
+
message: `Could not regenerate SDK types for comparison: ${message}`,
|
|
258
|
+
fix: "Run `rebase generate-sdk` to verify"
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return { passed: issues.length === 0, issues };
|
|
263
|
+
}
|
|
264
|
+
|
|
206
265
|
// ── Phase 2: Collections ↔ Database ──────────────────────────────────────
|
|
207
266
|
|
|
208
267
|
interface DbColumn {
|
|
268
|
+
table_schema: string;
|
|
269
|
+
table_name: string;
|
|
209
270
|
column_name: string;
|
|
210
271
|
data_type: string;
|
|
211
272
|
is_nullable: string;
|
|
@@ -229,27 +290,45 @@ export async function checkCollectionsVsDatabase(
|
|
|
229
290
|
const { Pool } = pgModule.default ?? pgModule;
|
|
230
291
|
const pool = new Pool({ connectionString: databaseUrl });
|
|
231
292
|
|
|
293
|
+
// Determine all schemas defined by the collections, plus public and rebase
|
|
294
|
+
const schemas = Array.from(new Set([
|
|
295
|
+
"public",
|
|
296
|
+
"rebase",
|
|
297
|
+
...collections
|
|
298
|
+
.filter(isPostgresCollection)
|
|
299
|
+
.map(c => c.schema)
|
|
300
|
+
.filter((s): s is string => !!s)
|
|
301
|
+
]));
|
|
302
|
+
|
|
232
303
|
try {
|
|
233
|
-
// Fetch all tables in the
|
|
234
|
-
const tablesResult = await pool.query<{ table_name: string }>(
|
|
235
|
-
|
|
304
|
+
// Fetch all tables in the defined schemas
|
|
305
|
+
const tablesResult = await pool.query<{ table_schema: string; table_name: string }>(
|
|
306
|
+
`SELECT table_schema, table_name
|
|
307
|
+
FROM information_schema.tables
|
|
308
|
+
WHERE table_schema = ANY($1) AND table_type = 'BASE TABLE'`,
|
|
309
|
+
[schemas]
|
|
236
310
|
);
|
|
237
|
-
const existingTables = new Set(tablesResult.rows.map((r) =>
|
|
311
|
+
const existingTables = new Set(tablesResult.rows.map((r) =>
|
|
312
|
+
r.table_schema === "public" ? r.table_name : `${r.table_schema}.${r.table_name}`
|
|
313
|
+
));
|
|
238
314
|
|
|
239
|
-
// Fetch all columns
|
|
315
|
+
// Fetch all columns in the defined schemas
|
|
240
316
|
const columnsResult = await pool.query<DbColumn>(
|
|
241
|
-
`SELECT table_name, column_name, data_type, is_nullable, udt_name
|
|
317
|
+
`SELECT table_schema, table_name, column_name, data_type, is_nullable, udt_name
|
|
242
318
|
FROM information_schema.columns
|
|
243
|
-
WHERE table_schema =
|
|
244
|
-
ORDER BY table_name, ordinal_position
|
|
319
|
+
WHERE table_schema = ANY($1)
|
|
320
|
+
ORDER BY table_schema, table_name, ordinal_position`,
|
|
321
|
+
[schemas]
|
|
245
322
|
);
|
|
246
323
|
const columnsByTable = new Map<string, DbColumn[]>();
|
|
247
324
|
for (const row of columnsResult.rows) {
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
325
|
+
const tableSchema = row.table_schema;
|
|
326
|
+
const tableName = row.table_name;
|
|
327
|
+
const key = tableSchema === "public" ? tableName : `${tableSchema}.${tableName}`;
|
|
328
|
+
if (!columnsByTable.has(key)) {
|
|
329
|
+
columnsByTable.set(key, []);
|
|
251
330
|
}
|
|
252
|
-
columnsByTable.get(
|
|
331
|
+
columnsByTable.get(key)!.push(row);
|
|
253
332
|
}
|
|
254
333
|
|
|
255
334
|
// Fetch enums
|
|
@@ -267,18 +346,22 @@ export async function checkCollectionsVsDatabase(
|
|
|
267
346
|
enumsByName.get(row.enum_name)!.push(row.enum_value);
|
|
268
347
|
}
|
|
269
348
|
|
|
270
|
-
// Fetch foreign key constraints
|
|
349
|
+
// Fetch foreign key constraints in the defined schemas
|
|
271
350
|
const fksResult = await pool.query<{
|
|
272
351
|
constraint_name: string;
|
|
352
|
+
table_schema: string;
|
|
273
353
|
table_name: string;
|
|
274
354
|
column_name: string;
|
|
355
|
+
foreign_table_schema: string;
|
|
275
356
|
foreign_table_name: string;
|
|
276
357
|
foreign_column_name: string;
|
|
277
358
|
}>(
|
|
278
359
|
`SELECT
|
|
279
360
|
tc.constraint_name,
|
|
361
|
+
tc.table_schema,
|
|
280
362
|
tc.table_name,
|
|
281
363
|
kcu.column_name,
|
|
364
|
+
ccu.table_schema AS foreign_table_schema,
|
|
282
365
|
ccu.table_name AS foreign_table_name,
|
|
283
366
|
ccu.column_name AS foreign_column_name
|
|
284
367
|
FROM information_schema.table_constraints AS tc
|
|
@@ -286,14 +369,18 @@ export async function checkCollectionsVsDatabase(
|
|
|
286
369
|
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
|
|
287
370
|
JOIN information_schema.constraint_column_usage AS ccu
|
|
288
371
|
ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema
|
|
289
|
-
WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema =
|
|
372
|
+
WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = ANY($1)`,
|
|
373
|
+
[schemas]
|
|
290
374
|
);
|
|
291
375
|
const fksByTable = new Map<string, typeof fksResult.rows>();
|
|
292
376
|
for (const row of fksResult.rows) {
|
|
293
|
-
|
|
294
|
-
|
|
377
|
+
const tableSchema = row.table_schema;
|
|
378
|
+
const tableName = row.table_name;
|
|
379
|
+
const key = tableSchema === "public" ? tableName : `${tableSchema}.${tableName}`;
|
|
380
|
+
if (!fksByTable.has(key)) {
|
|
381
|
+
fksByTable.set(key, []);
|
|
295
382
|
}
|
|
296
|
-
fksByTable.get(
|
|
383
|
+
fksByTable.get(key)!.push(row);
|
|
297
384
|
}
|
|
298
385
|
|
|
299
386
|
// ── Compare each collection against the database ─────────────────
|
|
@@ -302,20 +389,22 @@ export async function checkCollectionsVsDatabase(
|
|
|
302
389
|
|
|
303
390
|
for (const collection of postgresCollections) {
|
|
304
391
|
const tableName = getTableName(collection);
|
|
392
|
+
const schemaName = collection.schema || "public";
|
|
393
|
+
const fullTableName = schemaName === "public" ? tableName : `${schemaName}.${tableName}`;
|
|
305
394
|
|
|
306
395
|
// Check table existence
|
|
307
|
-
if (!existingTables.has(
|
|
396
|
+
if (!existingTables.has(fullTableName)) {
|
|
308
397
|
issues.push({
|
|
309
398
|
severity: "error",
|
|
310
399
|
category: "missing_table",
|
|
311
|
-
table:
|
|
312
|
-
message: `Table "${
|
|
400
|
+
table: fullTableName,
|
|
401
|
+
message: `Table "${fullTableName}" does not exist in the database.`,
|
|
313
402
|
fix: "Run `rebase db push` or `rebase db generate && rebase db migrate`"
|
|
314
403
|
});
|
|
315
404
|
continue; // Skip column checks for missing tables
|
|
316
405
|
}
|
|
317
406
|
|
|
318
|
-
const dbColumns = columnsByTable.get(
|
|
407
|
+
const dbColumns = columnsByTable.get(fullTableName) ?? [];
|
|
319
408
|
const dbColumnMap = new Map(dbColumns.map((c) => [c.column_name, c]));
|
|
320
409
|
|
|
321
410
|
// System columns that Rebase always creates
|
|
@@ -333,27 +422,36 @@ export async function checkCollectionsVsDatabase(
|
|
|
333
422
|
issues.push({
|
|
334
423
|
severity: "error",
|
|
335
424
|
category: "missing_column",
|
|
336
|
-
table:
|
|
425
|
+
table: fullTableName,
|
|
337
426
|
column: fkColName,
|
|
338
|
-
message: `Foreign key column "${fkColName}" for relation "${propName}" is missing from table "${
|
|
427
|
+
message: `Foreign key column "${fkColName}" for relation "${propName}" is missing from table "${fullTableName}".`,
|
|
339
428
|
fix: "Run `rebase db push` or `rebase db generate && rebase db migrate`"
|
|
340
429
|
});
|
|
341
430
|
}
|
|
342
431
|
|
|
343
432
|
// Check FK constraint exists
|
|
344
|
-
const tableFks = fksByTable.get(
|
|
345
|
-
|
|
433
|
+
const tableFks = fksByTable.get(fullTableName) ?? [];
|
|
434
|
+
let targetTableName = "unknown";
|
|
435
|
+
let targetSchemaName = "public";
|
|
436
|
+
try {
|
|
437
|
+
const targetColl = relation.target();
|
|
438
|
+
targetTableName = getTableName(targetColl);
|
|
439
|
+
targetSchemaName = targetColl.schema || "public";
|
|
440
|
+
} catch { /* ignore */ }
|
|
441
|
+
|
|
442
|
+
const hasFk = tableFks.some((fk) =>
|
|
443
|
+
fk.column_name === fkColName &&
|
|
444
|
+
fk.foreign_table_name === targetTableName &&
|
|
445
|
+
fk.foreign_table_schema === targetSchemaName
|
|
446
|
+
);
|
|
447
|
+
|
|
346
448
|
if (dbColumnMap.has(fkColName) && !hasFk) {
|
|
347
|
-
let targetTableName = "unknown";
|
|
348
|
-
try {
|
|
349
|
-
targetTableName = getTableName(relation.target());
|
|
350
|
-
} catch { /* ignore */ }
|
|
351
449
|
issues.push({
|
|
352
450
|
severity: "warning",
|
|
353
451
|
category: "missing_foreign_key",
|
|
354
|
-
table:
|
|
452
|
+
table: fullTableName,
|
|
355
453
|
column: fkColName,
|
|
356
|
-
message: `Column "${fkColName}" exists but has no FOREIGN KEY constraint referencing "${targetTableName}".`,
|
|
454
|
+
message: `Column "${fkColName}" exists but has no FOREIGN KEY constraint referencing "${targetSchemaName === "public" ? targetTableName : `${targetSchemaName}.${targetTableName}`}".`,
|
|
357
455
|
fix: "Run `rebase db push` or add the constraint manually"
|
|
358
456
|
});
|
|
359
457
|
}
|
|
@@ -371,9 +469,9 @@ export async function checkCollectionsVsDatabase(
|
|
|
371
469
|
issues.push({
|
|
372
470
|
severity: "error",
|
|
373
471
|
category: "missing_column",
|
|
374
|
-
table:
|
|
472
|
+
table: fullTableName,
|
|
375
473
|
column: colName,
|
|
376
|
-
message: `Column "${colName}" is defined in collection "${collection.slug}" but missing from table "${
|
|
474
|
+
message: `Column "${colName}" is defined in collection "${collection.slug}" but missing from table "${fullTableName}".`,
|
|
377
475
|
fix: "Run `rebase db push` or `rebase db generate && rebase db migrate`"
|
|
378
476
|
});
|
|
379
477
|
continue;
|
|
@@ -383,15 +481,19 @@ export async function checkCollectionsVsDatabase(
|
|
|
383
481
|
const expectedType = getExpectedColumnType(prop);
|
|
384
482
|
if (expectedType) {
|
|
385
483
|
const actualType = dbCol.data_type;
|
|
386
|
-
|
|
484
|
+
let isMismatch = actualType !== expectedType;
|
|
485
|
+
if (prop.type === "vector" && dbCol.udt_name !== "vector") {
|
|
486
|
+
isMismatch = true;
|
|
487
|
+
}
|
|
488
|
+
if (isMismatch) {
|
|
387
489
|
issues.push({
|
|
388
490
|
severity: "warning",
|
|
389
491
|
category: "type_mismatch",
|
|
390
|
-
table:
|
|
492
|
+
table: fullTableName,
|
|
391
493
|
column: colName,
|
|
392
|
-
expected: expectedType,
|
|
393
|
-
actual: actualType,
|
|
394
|
-
message: `Column "${colName}" in table "${
|
|
494
|
+
expected: prop.type === "vector" ? "vector" : expectedType,
|
|
495
|
+
actual: dbCol.udt_name === "vector" ? "vector" : actualType,
|
|
496
|
+
message: `Column "${colName}" in table "${fullTableName}": expected type "${prop.type === "vector" ? "vector" : expectedType}" but found "${dbCol.udt_name === "vector" ? "vector" : actualType}".`,
|
|
395
497
|
fix: "Review collection property type or run a migration"
|
|
396
498
|
});
|
|
397
499
|
}
|
|
@@ -407,7 +509,7 @@ export async function checkCollectionsVsDatabase(
|
|
|
407
509
|
issues.push({
|
|
408
510
|
severity: "warning",
|
|
409
511
|
category: "missing_enum",
|
|
410
|
-
table:
|
|
512
|
+
table: fullTableName,
|
|
411
513
|
column: colName,
|
|
412
514
|
expected: enumName,
|
|
413
515
|
message: `Enum type "${enumName}" is defined in collection but not found in the database.`,
|
|
@@ -429,11 +531,11 @@ export async function checkCollectionsVsDatabase(
|
|
|
429
531
|
issues.push({
|
|
430
532
|
severity: "warning",
|
|
431
533
|
category: "enum_value_mismatch",
|
|
432
|
-
table:
|
|
534
|
+
table: fullTableName,
|
|
433
535
|
column: colName,
|
|
434
536
|
expected: expectedValues.join(", "),
|
|
435
537
|
actual: dbEnumValues.join(", "),
|
|
436
|
-
message: `Enum values for "${colName}" in table "${
|
|
538
|
+
message: `Enum values for "${colName}" in table "${fullTableName}" are out of sync (${parts.join("; ")}).`,
|
|
437
539
|
fix: "Run `rebase db push` to update the enum"
|
|
438
540
|
});
|
|
439
541
|
}
|
|
@@ -447,12 +549,14 @@ export async function checkCollectionsVsDatabase(
|
|
|
447
549
|
for (const relation of Object.values(resolvedRelations)) {
|
|
448
550
|
if (relation.cardinality === "many" && relation.direction === "owning" && relation.through) {
|
|
449
551
|
const junctionTable = relation.through.table;
|
|
450
|
-
|
|
552
|
+
const junctionSchema = collection.schema || "public";
|
|
553
|
+
const fullJunctionTable = junctionSchema === "public" ? junctionTable : `${junctionSchema}.${junctionTable}`;
|
|
554
|
+
if (!existingTables.has(fullJunctionTable)) {
|
|
451
555
|
issues.push({
|
|
452
556
|
severity: "error",
|
|
453
557
|
category: "missing_table",
|
|
454
|
-
table:
|
|
455
|
-
message: `Junction table "${
|
|
558
|
+
table: fullJunctionTable,
|
|
559
|
+
message: `Junction table "${fullJunctionTable}" for many-to-many relation "${relation.relationName}" is missing.`,
|
|
456
560
|
fix: "Run `rebase db push` or `rebase db generate && rebase db migrate`"
|
|
457
561
|
});
|
|
458
562
|
}
|
|
@@ -489,6 +593,13 @@ export function renderReport(report: DoctorReport): void {
|
|
|
489
593
|
report.schemaToDatabase.issues
|
|
490
594
|
);
|
|
491
595
|
|
|
596
|
+
// Phase 3
|
|
597
|
+
renderPhase(
|
|
598
|
+
"Collections → SDK Types",
|
|
599
|
+
report.collectionsToSdk.passed,
|
|
600
|
+
report.collectionsToSdk.issues
|
|
601
|
+
);
|
|
602
|
+
|
|
492
603
|
// Summary
|
|
493
604
|
console.log(chalk.gray(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
|
|
494
605
|
const { passed, warnings, errors } = report.summary;
|
|
@@ -554,7 +665,8 @@ function formatCategory(cat: DoctorIssue["category"]): string {
|
|
|
554
665
|
schema_stale: "Stale Schema",
|
|
555
666
|
missing_enum: "Missing Enum",
|
|
556
667
|
enum_value_mismatch: "Enum Value Mismatch",
|
|
557
|
-
missing_foreign_key: "Missing Foreign Key"
|
|
668
|
+
missing_foreign_key: "Missing Foreign Key",
|
|
669
|
+
sdk_stale: "Stale SDK Types"
|
|
558
670
|
};
|
|
559
671
|
return labels[cat];
|
|
560
672
|
}
|
|
@@ -564,6 +676,7 @@ function formatCategory(cat: DoctorIssue["category"]): string {
|
|
|
564
676
|
export async function runDoctor(options: {
|
|
565
677
|
collectionsPath: string;
|
|
566
678
|
schemaPath: string;
|
|
679
|
+
sdkPath: string;
|
|
567
680
|
databaseUrl?: string;
|
|
568
681
|
}): Promise<DoctorReport> {
|
|
569
682
|
console.log("");
|
|
@@ -591,14 +704,19 @@ issues: [] };
|
|
|
591
704
|
console.log(chalk.gray(" Set DATABASE_URL in your .env to enable full drift detection."));
|
|
592
705
|
}
|
|
593
706
|
|
|
594
|
-
|
|
707
|
+
// Phase 3: Collections ↔ SDK Types
|
|
708
|
+
console.log(chalk.gray(" Checking Collections → SDK Types..."));
|
|
709
|
+
const collectionsToSdk = await checkCollectionsVsSdk(collections, options.sdkPath);
|
|
710
|
+
|
|
711
|
+
const allIssues = [...collectionsToSchema.issues, ...schemaToDatabase.issues, ...collectionsToSdk.issues];
|
|
595
712
|
const summary = {
|
|
596
|
-
passed: [collectionsToSchema, schemaToDatabase].filter((p) => p.passed).length,
|
|
713
|
+
passed: [collectionsToSchema, schemaToDatabase, collectionsToSdk].filter((p) => p.passed).length,
|
|
597
714
|
warnings: allIssues.filter((i) => i.severity === "warning").length,
|
|
598
715
|
errors: allIssues.filter((i) => i.severity === "error").length
|
|
599
716
|
};
|
|
600
717
|
|
|
601
718
|
const report: DoctorReport = { collectionsToSchema,
|
|
719
|
+
collectionsToSdk,
|
|
602
720
|
schemaToDatabase,
|
|
603
721
|
summary };
|
|
604
722
|
renderReport(report);
|