@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.
Files changed (71) hide show
  1. package/LICENSE +22 -6
  2. package/dist/common/src/data/query_builder.d.ts +51 -0
  3. package/dist/common/src/index.d.ts +1 -0
  4. package/dist/common/src/util/entities.d.ts +2 -2
  5. package/dist/common/src/util/relations.d.ts +1 -1
  6. package/dist/index.es.js +1435 -738
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/index.umd.js +1433 -736
  9. package/dist/index.umd.js.map +1 -1
  10. package/dist/server-postgresql/src/PostgresAdapter.d.ts +6 -0
  11. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +2 -1
  12. package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +0 -5
  13. package/dist/server-postgresql/src/auth/ensure-tables.d.ts +2 -1
  14. package/dist/server-postgresql/src/auth/services.d.ts +37 -15
  15. package/dist/server-postgresql/src/index.d.ts +1 -0
  16. package/dist/server-postgresql/src/schema/auth-schema.d.ts +43 -856
  17. package/dist/server-postgresql/src/schema/default-collections.d.ts +2 -0
  18. package/dist/server-postgresql/src/schema/doctor.d.ts +10 -1
  19. package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +1 -0
  20. package/dist/server-postgresql/src/services/entity-helpers.d.ts +1 -1
  21. package/dist/server-postgresql/src/services/realtimeService.d.ts +12 -0
  22. package/dist/server-postgresql/src/websocket.d.ts +2 -1
  23. package/dist/types/src/controllers/auth.d.ts +9 -8
  24. package/dist/types/src/controllers/client.d.ts +3 -0
  25. package/dist/types/src/controllers/data.d.ts +21 -0
  26. package/dist/types/src/types/auth_adapter.d.ts +356 -0
  27. package/dist/types/src/types/collections.d.ts +67 -2
  28. package/dist/types/src/types/database_adapter.d.ts +94 -0
  29. package/dist/types/src/types/entity_actions.d.ts +7 -1
  30. package/dist/types/src/types/entity_callbacks.d.ts +1 -1
  31. package/dist/types/src/types/entity_views.d.ts +36 -1
  32. package/dist/types/src/types/index.d.ts +2 -0
  33. package/dist/types/src/types/plugins.d.ts +1 -1
  34. package/dist/types/src/types/properties.d.ts +24 -5
  35. package/dist/types/src/types/property_config.d.ts +6 -2
  36. package/dist/types/src/types/relations.d.ts +1 -1
  37. package/dist/types/src/types/translations.d.ts +8 -0
  38. package/dist/types/src/users/user.d.ts +5 -0
  39. package/package.json +22 -15
  40. package/src/PostgresAdapter.ts +59 -0
  41. package/src/PostgresBackendDriver.ts +66 -13
  42. package/src/PostgresBootstrapper.ts +35 -15
  43. package/src/auth/ensure-tables.ts +82 -189
  44. package/src/auth/services.ts +421 -170
  45. package/src/cli.ts +49 -13
  46. package/src/data-transformer.ts +78 -8
  47. package/src/history/HistoryService.ts +25 -2
  48. package/src/index.ts +1 -0
  49. package/src/schema/auth-schema.ts +130 -98
  50. package/src/schema/default-collections.ts +69 -0
  51. package/src/schema/doctor-cli.ts +5 -1
  52. package/src/schema/doctor.ts +166 -48
  53. package/src/schema/generate-drizzle-schema-logic.ts +74 -27
  54. package/src/schema/generate-drizzle-schema.ts +13 -3
  55. package/src/schema/introspect-db-inference.ts +5 -5
  56. package/src/schema/introspect-db-logic.ts +9 -2
  57. package/src/schema/introspect-db.ts +14 -3
  58. package/src/services/EntityFetchService.ts +5 -5
  59. package/src/services/RelationService.ts +2 -2
  60. package/src/services/entity-helpers.ts +1 -1
  61. package/src/services/realtimeService.ts +145 -136
  62. package/src/utils/drizzle-conditions.ts +16 -2
  63. package/src/websocket.ts +113 -37
  64. package/test/auth-services.test.ts +163 -74
  65. package/test/data-transformer-hardening.test.ts +57 -0
  66. package/test/data-transformer.test.ts +43 -0
  67. package/test/generate-drizzle-schema.test.ts +7 -5
  68. package/test/introspect-db-utils.test.ts +4 -1
  69. package/test/postgresDataDriver.test.ts +147 -1
  70. package/test/realtimeService.test.ts +7 -7
  71. 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
+ };
@@ -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("..", "shared", "collections");
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
 
@@ -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 public schema
234
- const tablesResult = await pool.query<{ table_name: string }>(
235
- "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE'"
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) => r.table_name));
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 = 'public'
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 tableName = (row as unknown as Record<string, string>).table_name;
249
- if (!columnsByTable.has(tableName)) {
250
- columnsByTable.set(tableName, []);
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(tableName)!.push(row);
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 = 'public'`
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
- if (!fksByTable.has(row.table_name)) {
294
- fksByTable.set(row.table_name, []);
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(row.table_name)!.push(row);
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(tableName)) {
396
+ if (!existingTables.has(fullTableName)) {
308
397
  issues.push({
309
398
  severity: "error",
310
399
  category: "missing_table",
311
- table: tableName,
312
- message: `Table "${tableName}" does not exist in the database.`,
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(tableName) ?? [];
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: tableName,
425
+ table: fullTableName,
337
426
  column: fkColName,
338
- message: `Foreign key column "${fkColName}" for relation "${propName}" is missing from table "${tableName}".`,
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(tableName) ?? [];
345
- const hasFk = tableFks.some((fk) => fk.column_name === fkColName);
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: tableName,
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: tableName,
472
+ table: fullTableName,
375
473
  column: colName,
376
- message: `Column "${colName}" is defined in collection "${collection.slug}" but missing from table "${tableName}".`,
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
- if (actualType !== expectedType) {
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: tableName,
492
+ table: fullTableName,
391
493
  column: colName,
392
- expected: expectedType,
393
- actual: actualType,
394
- message: `Column "${colName}" in table "${tableName}": expected type "${expectedType}" but found "${actualType}".`,
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: tableName,
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: tableName,
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 "${tableName}" are out of sync (${parts.join("; ")}).`,
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
- if (!existingTables.has(junctionTable)) {
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: junctionTable,
455
- message: `Junction table "${junctionTable}" for many-to-many relation "${relation.relationName}" is missing.`,
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
- const allIssues = [...collectionsToSchema.issues, ...schemaToDatabase.issues];
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);