@rebasepro/server-postgresql 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/dist/{server-postgresql/src/PostgresAdapter.d.ts → PostgresAdapter.d.ts} +1 -1
  2. package/dist/{server-postgresql/src/PostgresBackendDriver.d.ts → PostgresBackendDriver.d.ts} +2 -2
  3. package/dist/{server-postgresql/src/PostgresBootstrapper.d.ts → PostgresBootstrapper.d.ts} +11 -1
  4. package/dist/{server-postgresql/src/collections → collections}/PostgresCollectionRegistry.d.ts +4 -0
  5. package/dist/index.es.js +10168 -11145
  6. package/dist/index.es.js.map +1 -1
  7. package/dist/index.umd.js +10735 -11429
  8. package/dist/index.umd.js.map +1 -1
  9. package/dist/{server-postgresql/src/services → services}/EntityPersistService.d.ts +0 -14
  10. package/dist/utils/pg-error-utils.d.ts +55 -0
  11. package/package.json +24 -21
  12. package/src/PostgresAdapter.ts +9 -10
  13. package/src/PostgresBackendDriver.ts +134 -121
  14. package/src/PostgresBootstrapper.ts +86 -13
  15. package/src/auth/ensure-tables.ts +28 -5
  16. package/src/auth/services.ts +28 -18
  17. package/src/cli.ts +99 -96
  18. package/src/collections/PostgresCollectionRegistry.ts +7 -0
  19. package/src/connection.ts +11 -6
  20. package/src/data-transformer.ts +16 -14
  21. package/src/databasePoolManager.ts +3 -2
  22. package/src/history/HistoryService.ts +3 -2
  23. package/src/history/ensure-history-table.ts +5 -4
  24. package/src/schema/auth-schema.ts +1 -2
  25. package/src/schema/doctor-cli.ts +2 -1
  26. package/src/schema/doctor.ts +40 -37
  27. package/src/schema/generate-drizzle-schema-logic.ts +56 -18
  28. package/src/schema/generate-drizzle-schema.ts +11 -11
  29. package/src/schema/introspect-db-inference.ts +25 -25
  30. package/src/schema/introspect-db-logic.ts +38 -38
  31. package/src/schema/introspect-db.ts +28 -27
  32. package/src/services/BranchService.ts +14 -0
  33. package/src/services/EntityFetchService.ts +28 -25
  34. package/src/services/EntityPersistService.ts +11 -141
  35. package/src/services/RelationService.ts +57 -37
  36. package/src/services/entity-helpers.ts +6 -2
  37. package/src/services/realtimeService.ts +45 -32
  38. package/src/utils/drizzle-conditions.ts +31 -15
  39. package/src/utils/pg-error-utils.ts +211 -0
  40. package/src/websocket.ts +15 -12
  41. package/test/auth-services.test.ts +36 -19
  42. package/test/batch-many-to-many-regression.test.ts +119 -39
  43. package/test/data-transformer-hardening.test.ts +67 -33
  44. package/test/data-transformer.test.ts +4 -2
  45. package/test/doctor.test.ts +10 -5
  46. package/test/drizzle-conditions.test.ts +59 -6
  47. package/test/generate-drizzle-schema.test.ts +65 -40
  48. package/test/introspect-db-generation.test.ts +179 -81
  49. package/test/introspect-db-utils.test.ts +92 -37
  50. package/test/mocks/chalk.cjs +7 -0
  51. package/test/pg-error-utils.test.ts +221 -0
  52. package/test/postgresDataDriver.test.ts +14 -5
  53. package/test/property-ordering.test.ts +126 -79
  54. package/test/realtimeService.test.ts +6 -2
  55. package/test/relation-pipeline-gaps.test.ts +84 -36
  56. package/test/relations.test.ts +247 -0
  57. package/test/unmapped-tables-safety.test.ts +14 -6
  58. package/test/websocket.test.ts +1 -1
  59. package/tsconfig.json +5 -0
  60. package/tsconfig.prod.json +3 -0
  61. package/vite.config.ts +5 -5
  62. package/dist/common/src/collections/CollectionRegistry.d.ts +0 -56
  63. package/dist/common/src/collections/default-collections.d.ts +0 -9
  64. package/dist/common/src/collections/index.d.ts +0 -2
  65. package/dist/common/src/data/buildRebaseData.d.ts +0 -14
  66. package/dist/common/src/data/query_builder.d.ts +0 -55
  67. package/dist/common/src/index.d.ts +0 -4
  68. package/dist/common/src/util/builders.d.ts +0 -57
  69. package/dist/common/src/util/callbacks.d.ts +0 -6
  70. package/dist/common/src/util/collections.d.ts +0 -11
  71. package/dist/common/src/util/common.d.ts +0 -2
  72. package/dist/common/src/util/conditions.d.ts +0 -26
  73. package/dist/common/src/util/entities.d.ts +0 -58
  74. package/dist/common/src/util/enums.d.ts +0 -3
  75. package/dist/common/src/util/index.d.ts +0 -16
  76. package/dist/common/src/util/navigation_from_path.d.ts +0 -34
  77. package/dist/common/src/util/navigation_utils.d.ts +0 -20
  78. package/dist/common/src/util/parent_references_from_path.d.ts +0 -6
  79. package/dist/common/src/util/paths.d.ts +0 -14
  80. package/dist/common/src/util/permissions.d.ts +0 -14
  81. package/dist/common/src/util/references.d.ts +0 -2
  82. package/dist/common/src/util/relations.d.ts +0 -22
  83. package/dist/common/src/util/resolutions.d.ts +0 -72
  84. package/dist/common/src/util/storage.d.ts +0 -24
  85. package/dist/types/src/controllers/analytics_controller.d.ts +0 -7
  86. package/dist/types/src/controllers/auth.d.ts +0 -104
  87. package/dist/types/src/controllers/client.d.ts +0 -168
  88. package/dist/types/src/controllers/collection_registry.d.ts +0 -46
  89. package/dist/types/src/controllers/customization_controller.d.ts +0 -60
  90. package/dist/types/src/controllers/data.d.ts +0 -207
  91. package/dist/types/src/controllers/data_driver.d.ts +0 -218
  92. package/dist/types/src/controllers/database_admin.d.ts +0 -11
  93. package/dist/types/src/controllers/dialogs_controller.d.ts +0 -36
  94. package/dist/types/src/controllers/effective_role.d.ts +0 -4
  95. package/dist/types/src/controllers/email.d.ts +0 -36
  96. package/dist/types/src/controllers/index.d.ts +0 -18
  97. package/dist/types/src/controllers/local_config_persistence.d.ts +0 -20
  98. package/dist/types/src/controllers/navigation.d.ts +0 -225
  99. package/dist/types/src/controllers/registry.d.ts +0 -63
  100. package/dist/types/src/controllers/side_dialogs_controller.d.ts +0 -67
  101. package/dist/types/src/controllers/side_entity_controller.d.ts +0 -97
  102. package/dist/types/src/controllers/snackbar.d.ts +0 -24
  103. package/dist/types/src/controllers/storage.d.ts +0 -171
  104. package/dist/types/src/index.d.ts +0 -4
  105. package/dist/types/src/rebase_context.d.ts +0 -122
  106. package/dist/types/src/types/auth_adapter.d.ts +0 -301
  107. package/dist/types/src/types/backend.d.ts +0 -571
  108. package/dist/types/src/types/backend_hooks.d.ts +0 -172
  109. package/dist/types/src/types/builders.d.ts +0 -15
  110. package/dist/types/src/types/chips.d.ts +0 -5
  111. package/dist/types/src/types/collections.d.ts +0 -961
  112. package/dist/types/src/types/component_ref.d.ts +0 -47
  113. package/dist/types/src/types/cron.d.ts +0 -102
  114. package/dist/types/src/types/data_source.d.ts +0 -64
  115. package/dist/types/src/types/database_adapter.d.ts +0 -94
  116. package/dist/types/src/types/entities.d.ts +0 -145
  117. package/dist/types/src/types/entity_actions.d.ts +0 -104
  118. package/dist/types/src/types/entity_callbacks.d.ts +0 -173
  119. package/dist/types/src/types/entity_link_builder.d.ts +0 -7
  120. package/dist/types/src/types/entity_overrides.d.ts +0 -10
  121. package/dist/types/src/types/entity_views.d.ts +0 -87
  122. package/dist/types/src/types/export_import.d.ts +0 -21
  123. package/dist/types/src/types/formex.d.ts +0 -40
  124. package/dist/types/src/types/index.d.ts +0 -28
  125. package/dist/types/src/types/locales.d.ts +0 -4
  126. package/dist/types/src/types/modify_collections.d.ts +0 -5
  127. package/dist/types/src/types/plugins.d.ts +0 -282
  128. package/dist/types/src/types/properties.d.ts +0 -1173
  129. package/dist/types/src/types/property_config.d.ts +0 -74
  130. package/dist/types/src/types/relations.d.ts +0 -336
  131. package/dist/types/src/types/slots.d.ts +0 -262
  132. package/dist/types/src/types/translations.d.ts +0 -900
  133. package/dist/types/src/types/user_management_delegate.d.ts +0 -86
  134. package/dist/types/src/types/websockets.d.ts +0 -78
  135. package/dist/types/src/users/index.d.ts +0 -1
  136. package/dist/types/src/users/user.d.ts +0 -50
  137. /package/dist/{server-postgresql/src/auth → auth}/ensure-tables.d.ts +0 -0
  138. /package/dist/{server-postgresql/src/auth → auth}/services.d.ts +0 -0
  139. /package/dist/{server-postgresql/src/cli.d.ts → cli.d.ts} +0 -0
  140. /package/dist/{server-postgresql/src/connection.d.ts → connection.d.ts} +0 -0
  141. /package/dist/{server-postgresql/src/data-transformer.d.ts → data-transformer.d.ts} +0 -0
  142. /package/dist/{server-postgresql/src/databasePoolManager.d.ts → databasePoolManager.d.ts} +0 -0
  143. /package/dist/{server-postgresql/src/history → history}/HistoryService.d.ts +0 -0
  144. /package/dist/{server-postgresql/src/history → history}/ensure-history-table.d.ts +0 -0
  145. /package/dist/{server-postgresql/src/index.d.ts → index.d.ts} +0 -0
  146. /package/dist/{server-postgresql/src/interfaces.d.ts → interfaces.d.ts} +0 -0
  147. /package/dist/{server-postgresql/src/schema → schema}/auth-schema.d.ts +0 -0
  148. /package/dist/{server-postgresql/src/schema → schema}/doctor-cli.d.ts +0 -0
  149. /package/dist/{server-postgresql/src/schema → schema}/doctor.d.ts +0 -0
  150. /package/dist/{server-postgresql/src/schema → schema}/generate-drizzle-schema-logic.d.ts +0 -0
  151. /package/dist/{server-postgresql/src/schema → schema}/generate-drizzle-schema.d.ts +0 -0
  152. /package/dist/{server-postgresql/src/schema → schema}/introspect-db-inference.d.ts +0 -0
  153. /package/dist/{server-postgresql/src/schema → schema}/introspect-db-logic.d.ts +0 -0
  154. /package/dist/{server-postgresql/src/schema → schema}/introspect-db.d.ts +0 -0
  155. /package/dist/{server-postgresql/src/schema → schema}/test-schema.d.ts +0 -0
  156. /package/dist/{server-postgresql/src/services → services}/BranchService.d.ts +0 -0
  157. /package/dist/{server-postgresql/src/services → services}/EntityFetchService.d.ts +0 -0
  158. /package/dist/{server-postgresql/src/services → services}/RelationService.d.ts +0 -0
  159. /package/dist/{server-postgresql/src/services → services}/entity-helpers.d.ts +0 -0
  160. /package/dist/{server-postgresql/src/services → services}/entityService.d.ts +0 -0
  161. /package/dist/{server-postgresql/src/services → services}/index.d.ts +0 -0
  162. /package/dist/{server-postgresql/src/services → services}/realtimeService.d.ts +0 -0
  163. /package/dist/{server-postgresql/src/types.d.ts → types.d.ts} +0 -0
  164. /package/dist/{server-postgresql/src/utils → utils}/drizzle-conditions.d.ts +0 -0
  165. /package/dist/{server-postgresql/src/websocket.d.ts → websocket.d.ts} +0 -0
@@ -6,6 +6,7 @@ import { getTableName, resolveCollectionRelations, findRelation, createRelationR
6
6
  import { PostgresCollectionRegistry } from "./collections/PostgresCollectionRegistry";
7
7
  import { DrizzleConditionBuilder } from "./utils/drizzle-conditions";
8
8
  import { getPrimaryKeys, buildCompositeId } from "./services/entity-helpers";
9
+ import { logger } from "@rebasepro/server-core";
9
10
 
10
11
  /**
11
12
  * Data transformation utilities for converting between frontend and database formats.
@@ -90,7 +91,9 @@ export function serializeDataToServer<M extends Record<string, unknown>>(
90
91
  collection?: EntityCollection,
91
92
  registry?: PostgresCollectionRegistry
92
93
  ): SerializedEntityData {
93
- if (!entity || !properties) return { scalarData: entity ?? {}, inverseRelationUpdates: [], joinPathRelationUpdates: [] };
94
+ if (!entity || !properties) return { scalarData: entity ?? {},
95
+ inverseRelationUpdates: [],
96
+ joinPathRelationUpdates: [] };
94
97
 
95
98
  const result: Record<string, unknown> = {};
96
99
 
@@ -128,7 +131,6 @@ export function serializeDataToServer<M extends Record<string, unknown>>(
128
131
  }
129
132
 
130
133
 
131
-
132
134
  // Handle relation properties specially
133
135
  if (property.type === "relation" && collection) {
134
136
  const relation = findRelation(resolvedRelations, key);
@@ -320,7 +322,7 @@ export async function parseDataFromServer<M extends Record<string, unknown>>(
320
322
  const targetCollection = relation.target();
321
323
  result[propKey] = createRelationRef(fkValue.toString(), targetCollection.slug);
322
324
  } catch (e) {
323
- console.warn(`Could not resolve target collection for relation property: ${propKey}`, e);
325
+ logger.warn(`Could not resolve target collection for relation property: ${propKey}`, { error: e });
324
326
  }
325
327
  }
326
328
  } else if (relation.direction === "inverse" && relation.foreignKeyOnTarget && db && registry) {
@@ -350,7 +352,7 @@ export async function parseDataFromServer<M extends Record<string, unknown>>(
350
352
  } else {
351
353
  // One-to-many: return array of relation objects
352
354
  const targetPks = getPrimaryKeys(targetCollection, registry!);
353
- result[propKey] = relatedEntities.map((entity: Record<string, unknown>) =>
355
+ result[propKey] = relatedEntities.map((entity: Record<string, unknown>) =>
354
356
  createRelationRef(buildCompositeId(entity, targetPks), targetCollection.slug)
355
357
  );
356
358
  }
@@ -358,7 +360,7 @@ export async function parseDataFromServer<M extends Record<string, unknown>>(
358
360
  }
359
361
  }
360
362
  } catch (e) {
361
- console.warn(`Could not resolve inverse relation property: ${propKey}`, e);
363
+ logger.warn(`Could not resolve inverse relation property: ${propKey}`, { error: e });
362
364
  }
363
365
  } else if (relation.direction === "inverse" && relation.joinPath && db && registry) {
364
366
  // Join path relation: Multi-hop relation using joins
@@ -371,7 +373,7 @@ export async function parseDataFromServer<M extends Record<string, unknown>>(
371
373
  // Build the join query following the join path
372
374
  const sourceTable = registry.getTable(getTableName(collection));
373
375
  if (!sourceTable) {
374
- console.warn(`Source table not found for collection: ${collection.slug}`);
376
+ logger.warn(`Source table not found for collection: ${collection.slug}`);
375
377
  continue;
376
378
  }
377
379
 
@@ -382,7 +384,7 @@ export async function parseDataFromServer<M extends Record<string, unknown>>(
382
384
  for (const join of relation.joinPath) {
383
385
  const joinTable = registry.getTable(join.table);
384
386
  if (!joinTable) {
385
- console.warn(`Join table not found: ${join.table}`);
387
+ logger.warn(`Join table not found: ${join.table}`);
386
388
  break;
387
389
  }
388
390
 
@@ -400,7 +402,7 @@ export async function parseDataFromServer<M extends Record<string, unknown>>(
400
402
  const toCol = joinTable[toColName as keyof typeof joinTable] as AnyPgColumn;
401
403
 
402
404
  if (!fromCol || !toCol) {
403
- console.warn(`Join columns not found: ${fromColumn} -> ${toColumn}`);
405
+ logger.warn(`Join columns not found: ${fromColumn} -> ${toColumn}`);
404
406
  break;
405
407
  }
406
408
 
@@ -414,7 +416,7 @@ export async function parseDataFromServer<M extends Record<string, unknown>>(
414
416
  query = query.where(eq(sourceIdField, currentEntityId)) as typeof query;
415
417
  } else {
416
418
  // For composite keys, we would need to map the split parts. For now log a warning.
417
- console.warn(`Join path resolution for composite primary keys is not yet fully supported: ${collection.slug}`);
419
+ logger.warn(`Join path resolution for composite primary keys is not yet fully supported: ${collection.slug}`);
418
420
  }
419
421
 
420
422
  // Build additional conditions array
@@ -453,7 +455,7 @@ export async function parseDataFromServer<M extends Record<string, unknown>>(
453
455
  }
454
456
  }
455
457
  } catch (e) {
456
- console.warn(`Could not resolve join path relation property: ${propKey}`, e);
458
+ logger.warn(`Could not resolve join path relation property: ${propKey}`, { error: e });
457
459
  }
458
460
  }
459
461
  }
@@ -519,13 +521,13 @@ export function parsePropertyFromServer(value: unknown, property: Property, coll
519
521
 
520
522
  case "string": {
521
523
  if (typeof value === "string") return value;
522
-
524
+
523
525
  // Handle Buffer objects (e.g. from PostgreSQL bytea columns)
524
526
  const buf = tryResolveBuffer(value);
525
527
  if (buf) {
526
528
  return bufferToStringOrBase64(buf);
527
529
  }
528
-
530
+
529
531
  if (typeof value === "object" && value !== null) {
530
532
  try {
531
533
  return JSON.stringify(value);
@@ -549,7 +551,7 @@ export function parsePropertyFromServer(value: unknown, property: Property, coll
549
551
  }
550
552
 
551
553
  if (!relationDef) {
552
- console.warn(`Relation not defined in property for key: ${propertyKey || "unknown"}`);
554
+ logger.warn(`Relation not defined in property for key: ${propertyKey || "unknown"}`);
553
555
  return value;
554
556
  }
555
557
 
@@ -557,7 +559,7 @@ export function parsePropertyFromServer(value: unknown, property: Property, coll
557
559
  const targetCollection = relationDef.target();
558
560
  return createRelationRef(value.toString(), targetCollection.slug);
559
561
  } catch (e) {
560
- console.warn(`Could not resolve target collection for relation property: ${propertyKey || "unknown"}`, e);
562
+ logger.warn(`Could not resolve target collection for relation property: ${propertyKey || "unknown"}`, { error: e });
561
563
  return value;
562
564
  }
563
565
  }
@@ -1,6 +1,7 @@
1
1
  import { Pool } from "pg";
2
2
  import { drizzle } from "drizzle-orm/node-postgres";
3
3
  import { NodePgDatabase } from "drizzle-orm/node-postgres";
4
+ import { logger } from "@rebasepro/server-core";
4
5
 
5
6
  export class DatabasePoolManager {
6
7
  private pools: Map<string, Pool> = new Map();
@@ -47,7 +48,7 @@ export class DatabasePoolManager {
47
48
 
48
49
  // Prevent idle client errors from crashing the Node.js process
49
50
  pool.on("error", (err) => {
50
- console.error(`[DatabasePoolManager] Unexpected error on idle client for db ${databaseName}`, err);
51
+ logger.error(`[DatabasePoolManager] Unexpected error on idle client for db ${databaseName}`, { error: err });
51
52
  });
52
53
 
53
54
  this.pools.set(databaseName, pool);
@@ -76,7 +77,7 @@ export class DatabasePoolManager {
76
77
  public async shutdown(): Promise<void> {
77
78
  const promises = [];
78
79
  for (const [dbName, pool] of this.pools.entries()) {
79
- console.log(`[DatabasePoolManager] Shutting down pool for ${dbName}`);
80
+ logger.info(`[DatabasePoolManager] Shutting down pool for ${dbName}`);
80
81
  promises.push(pool.end());
81
82
  }
82
83
  await Promise.all(promises);
@@ -1,5 +1,6 @@
1
1
  import { sql } from "drizzle-orm";
2
2
  import { NodePgDatabase } from "drizzle-orm/node-postgres";
3
+ import { logger } from "@rebasepro/server-core";
3
4
 
4
5
  export interface HistoryEntry {
5
6
  id: string;
@@ -99,10 +100,10 @@ export class HistoryService {
99
100
 
100
101
  // Non-blocking prune for this specific entity
101
102
  this.pruneEntity(tableName, entityId).catch(err =>
102
- console.error("History prune failed:", err)
103
+ logger.error("History prune failed", { error: err })
103
104
  );
104
105
  } catch (error) {
105
- console.error("Failed to record entity history:", error);
106
+ logger.error("Failed to record entity history", { error: error });
106
107
  }
107
108
  }
108
109
 
@@ -1,5 +1,6 @@
1
1
  import { sql } from "drizzle-orm";
2
2
  import { NodePgDatabase } from "drizzle-orm/node-postgres";
3
+ import { logger } from "@rebasepro/server-core";
3
4
 
4
5
  /**
5
6
  * Auto-create the entity history table if it doesn't exist.
@@ -7,7 +8,7 @@ import { NodePgDatabase } from "drizzle-orm/node-postgres";
7
8
  * pattern as `ensureAuthTablesExist`.
8
9
  */
9
10
  export async function ensureHistoryTableExists(db: NodePgDatabase): Promise<void> {
10
- console.log("🔍 Checking entity history table...");
11
+ logger.info("🔍 Checking entity history table...");
11
12
 
12
13
  try {
13
14
  // Create the rebase schema (idempotent — may already exist from auth init)
@@ -37,9 +38,9 @@ export async function ensureHistoryTableExists(db: NodePgDatabase): Promise<void
37
38
  ON rebase.entity_history(table_name, entity_id, updated_at DESC)
38
39
  `);
39
40
 
40
- console.log("✅ Entity history table ready");
41
+ logger.info("✅ Entity history table ready");
41
42
  } catch (error) {
42
- console.error("❌ Failed to create entity history table:", error);
43
- console.warn("⚠️ Continuing without creating history table.");
43
+ logger.error("❌ Failed to create entity history table", { error: error });
44
+ logger.warn("⚠️ Continuing without creating history table.");
44
45
  }
45
46
  }
@@ -4,7 +4,7 @@ import { relations } from "drizzle-orm";
4
4
  /**
5
5
  * Factory function to dynamically create the auth tables bound to the specified schema names.
6
6
  */
7
- export function createAuthSchema(usersSchemaName: string = "rebase") {
7
+ export function createAuthSchema(usersSchemaName = "rebase") {
8
8
  const usersSchema = usersSchemaName === "public" ? null : pgSchema(usersSchemaName);
9
9
 
10
10
  const tableCreator = (usersSchema ? usersSchema.table.bind(usersSchema) : pgTable) as typeof pgTable;
@@ -30,7 +30,6 @@ export function createAuthSchema(usersSchemaName: string = "rebase") {
30
30
  });
31
31
 
32
32
 
33
-
34
33
  /**
35
34
  * Refresh tokens for long-lived sessions
36
35
  */
@@ -7,6 +7,7 @@ import path from "path";
7
7
  import chalk from "chalk";
8
8
  import fs from "fs";
9
9
  import { runDoctor } from "./doctor";
10
+ import { logger } from "@rebasepro/server-core";
10
11
 
11
12
  async function main() {
12
13
  const collectionsArg = process.argv.find((a) => a.startsWith("--collections="));
@@ -46,6 +47,6 @@ async function main() {
46
47
  }
47
48
 
48
49
  main().catch((err) => {
49
- console.error(chalk.red(" ✗ Doctor failed:"), err instanceof Error ? err.message : String(err));
50
+ logger.error(chalk.red(" ✗ Doctor failed"), { error: err });
50
51
  process.exit(1);
51
52
  });
@@ -17,6 +17,7 @@ import { generateSchema } from "./generate-drizzle-schema-logic";
17
17
  import { generateTypedefs } from "@rebasepro/sdk-generator";
18
18
  import { getTableName, resolveCollectionRelations, findRelation } from "@rebasepro/common";
19
19
  import { toSnakeCase } from "@rebasepro/utils";
20
+ import { logger } from "@rebasepro/server-core";
20
21
 
21
22
  /**
22
23
  * Resolve the SQL column name for a property.
@@ -149,7 +150,7 @@ export async function loadCollections(collectionsPath: string): Promise<EntityCo
149
150
  }
150
151
  } catch (err: unknown) {
151
152
  const message = err instanceof Error ? err.message : String(err);
152
- console.error(chalk.yellow(` ⚠ Could not load ${file}: ${message}`));
153
+ logger.error(chalk.yellow(` ⚠ Could not load ${file}: ${message}`));
153
154
  }
154
155
  }
155
156
  }
@@ -244,7 +245,8 @@ export async function checkCollectionsVsSdk(
244
245
  message: `Generated SDK typedefs file does not exist at "${sdkFilePath}".`,
245
246
  fix: "Run `rebase generate-sdk`"
246
247
  });
247
- return { passed: false, issues };
248
+ return { passed: false,
249
+ issues };
248
250
  }
249
251
 
250
252
  try {
@@ -277,7 +279,8 @@ export async function checkCollectionsVsSdk(
277
279
  });
278
280
  }
279
281
 
280
- return { passed: issues.length === 0, issues };
282
+ return { passed: issues.length === 0,
283
+ issues };
281
284
  }
282
285
 
283
286
  // ── Phase 2: Collections ↔ Database ──────────────────────────────────────
@@ -326,7 +329,7 @@ export async function checkCollectionsVsDatabase(
326
329
  WHERE table_schema = ANY($1) AND table_type = 'BASE TABLE'`,
327
330
  [schemas]
328
331
  );
329
- const existingTables = new Set(tablesResult.rows.map((r) =>
332
+ const existingTables = new Set(tablesResult.rows.map((r) =>
330
333
  r.table_schema === "public" ? r.table_name : `${r.table_schema}.${r.table_name}`
331
334
  ));
332
335
 
@@ -457,8 +460,8 @@ export async function checkCollectionsVsDatabase(
457
460
  targetSchemaName = targetColl.schema || "public";
458
461
  } catch { /* ignore */ }
459
462
 
460
- const hasFk = tableFks.some((fk) =>
461
- fk.column_name === fkColName &&
463
+ const hasFk = tableFks.some((fk) =>
464
+ fk.column_name === fkColName &&
462
465
  fk.foreign_table_name === targetTableName &&
463
466
  fk.foreign_table_schema === targetSchemaName
464
467
  );
@@ -615,10 +618,10 @@ issues };
615
618
  // ── Report Rendering ─────────────────────────────────────────────────────
616
619
 
617
620
  export function renderReport(report: DoctorReport): void {
618
- console.log("");
619
- console.log(chalk.bold(" 🩺 Rebase Schema Doctor"));
620
- console.log(chalk.gray(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
621
- console.log("");
621
+ logger.info("");
622
+ logger.info(chalk.bold(" 🩺 Rebase Schema Doctor"));
623
+ logger.info(chalk.gray(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
624
+ logger.info("");
622
625
 
623
626
  // Phase 1
624
627
  renderPhase(
@@ -642,7 +645,7 @@ export function renderReport(report: DoctorReport): void {
642
645
  );
643
646
 
644
647
  // Summary
645
- console.log(chalk.gray(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
648
+ logger.info(chalk.gray(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
646
649
  const { passed, warnings, errors } = report.summary;
647
650
 
648
651
  const parts: string[] = [];
@@ -650,50 +653,50 @@ export function renderReport(report: DoctorReport): void {
650
653
  if (warnings > 0) parts.push(chalk.yellow(`${warnings} warnings`));
651
654
  if (errors > 0) parts.push(chalk.red(`${errors} errors`));
652
655
 
653
- console.log(` Summary: ${parts.join(", ")}`);
654
- console.log("");
656
+ logger.info(` Summary: ${parts.join(", ")}`);
657
+ logger.info("");
655
658
 
656
659
  if (errors > 0) {
657
- console.log(chalk.red.bold(" ✗ Schema drift detected. Run the suggested fixes above."));
660
+ logger.info(chalk.red.bold(" ✗ Schema drift detected. Run the suggested fixes above."));
658
661
  } else if (warnings > 0) {
659
- console.log(chalk.yellow.bold(" ⚠ Minor issues detected. Consider running the suggested fixes."));
662
+ logger.info(chalk.yellow.bold(" ⚠ Minor issues detected. Consider running the suggested fixes."));
660
663
  } else {
661
- console.log(chalk.green.bold(" ✓ All schemas are in sync!"));
664
+ logger.info(chalk.green.bold(" ✓ All schemas are in sync!"));
662
665
  }
663
- console.log("");
666
+ logger.info("");
664
667
  }
665
668
 
666
669
  function renderPhase(label: string, passed: boolean, issues: DoctorIssue[]): void {
667
670
  if (passed) {
668
- console.log(` ${chalk.green("✅")} ${label}: ${chalk.green("In sync")}`);
671
+ logger.info(` ${chalk.green("✅")} ${label}: ${chalk.green("In sync")}`);
669
672
  } else {
670
673
  const errorCount = issues.filter((i) => i.severity === "error").length;
671
674
  const warnCount = issues.filter((i) => i.severity === "warning").length;
672
675
  const parts: string[] = [];
673
676
  if (errorCount > 0) parts.push(`${errorCount} error${errorCount > 1 ? "s" : ""}`);
674
677
  if (warnCount > 0) parts.push(`${warnCount} warning${warnCount > 1 ? "s" : ""}`);
675
- console.log(` ${chalk.yellow("⚠️")} ${label}: ${chalk.yellow(parts.join(", "))}`);
678
+ logger.info(` ${chalk.yellow("⚠️")} ${label}: ${chalk.yellow(parts.join(", "))}`);
676
679
  }
677
- console.log("");
680
+ logger.info("");
678
681
 
679
682
  for (const issue of issues) {
680
683
  const severityIcon = issue.severity === "error" ? chalk.red("✗") : chalk.yellow("⚠");
681
684
  const categoryLabel = formatCategory(issue.category);
682
- console.log(` ${chalk.gray("┌─")} ${severityIcon} ${chalk.bold(categoryLabel)} ${chalk.gray("─".repeat(Math.max(0, 42 - categoryLabel.length)))}`);
685
+ logger.info(` ${chalk.gray("┌─")} ${severityIcon} ${chalk.bold(categoryLabel)} ${chalk.gray("─".repeat(Math.max(0, 42 - categoryLabel.length)))}`);
683
686
 
684
687
  if (issue.table) {
685
688
  const colPart = issue.column ? ` │ Column: ${chalk.cyan(issue.column)}` : "";
686
- console.log(` ${chalk.gray("│")} Table: ${chalk.cyan(issue.table)}${colPart}`);
689
+ logger.info(` ${chalk.gray("│")} Table: ${chalk.cyan(issue.table)}${colPart}`);
687
690
  }
688
691
 
689
692
  if (issue.expected && issue.actual) {
690
- console.log(` ${chalk.gray("│")} Expected: ${chalk.green(issue.expected)} │ Actual: ${chalk.red(issue.actual)}`);
693
+ logger.info(` ${chalk.gray("│")} Expected: ${chalk.green(issue.expected)} │ Actual: ${chalk.red(issue.actual)}`);
691
694
  }
692
695
 
693
- console.log(` ${chalk.gray("│")} ${issue.message}`);
694
- console.log(` ${chalk.gray("│")} Fix: ${chalk.blue(issue.fix)}`);
695
- console.log(` ${chalk.gray("└" + "─".repeat(48))}`);
696
- console.log("");
696
+ logger.info(` ${chalk.gray("│")} ${issue.message}`);
697
+ logger.info(` ${chalk.gray("│")} Fix: ${chalk.blue(issue.fix)}`);
698
+ logger.info(` ${chalk.gray("└" + "─".repeat(48))}`);
699
+ logger.info("");
697
700
  }
698
701
  }
699
702
 
@@ -720,33 +723,33 @@ export async function runDoctor(options: {
720
723
  sdkPath: string;
721
724
  databaseUrl?: string;
722
725
  }): Promise<DoctorReport> {
723
- console.log("");
724
- console.log(chalk.bold(" 🩺 Loading collections..."));
726
+ logger.info("");
727
+ logger.info(chalk.bold(" 🩺 Loading collections..."));
725
728
  const collections = await loadCollections(options.collectionsPath);
726
729
  if (collections.length === 0) {
727
- console.error(chalk.red(" ✗ No collections found."));
730
+ logger.error(chalk.red(" ✗ No collections found."));
728
731
  process.exit(1);
729
732
  }
730
- console.log(chalk.gray(` Found ${collections.length} collection(s)`));
731
- console.log("");
733
+ logger.info(chalk.gray(` Found ${collections.length} collection(s)`));
734
+ logger.info("");
732
735
 
733
736
  // Phase 1: Collections ↔ Generated Schema
734
- console.log(chalk.gray(" Checking Collections → Generated Schema..."));
737
+ logger.info(chalk.gray(" Checking Collections → Generated Schema..."));
735
738
  const collectionsToSchema = await checkCollectionsVsSchema(collections, options.schemaPath);
736
739
 
737
740
  // Phase 2: Collections ↔ Database (only if we have a DATABASE_URL)
738
741
  let schemaToDatabase: { passed: boolean; issues: DoctorIssue[] } = { passed: true,
739
742
  issues: [] };
740
743
  if (options.databaseUrl) {
741
- console.log(chalk.gray(" Checking Collections → Database..."));
744
+ logger.info(chalk.gray(" Checking Collections → Database..."));
742
745
  schemaToDatabase = await checkCollectionsVsDatabase(collections, options.databaseUrl);
743
746
  } else {
744
- console.log(chalk.yellow(" ⚠ DATABASE_URL not set — skipping database comparison."));
745
- console.log(chalk.gray(" Set DATABASE_URL in your .env to enable full drift detection."));
747
+ logger.info(chalk.yellow(" ⚠ DATABASE_URL not set — skipping database comparison."));
748
+ logger.info(chalk.gray(" Set DATABASE_URL in your .env to enable full drift detection."));
746
749
  }
747
750
 
748
751
  // Phase 3: Collections ↔ SDK Types
749
- console.log(chalk.gray(" Checking Collections → SDK Types..."));
752
+ logger.info(chalk.gray(" Checking Collections → SDK Types..."));
750
753
  const collectionsToSdk = await checkCollectionsVsSdk(collections, options.sdkPath);
751
754
 
752
755
  const allIssues = [...collectionsToSchema.issues, ...schemaToDatabase.issues, ...collectionsToSdk.issues];
@@ -3,6 +3,7 @@ import { getPrimaryKeys } from "../services/entity-helpers";
3
3
  import { getEnumVarName, getTableName, getTableVarName, resolveCollectionRelations, findRelation } from "@rebasepro/common";
4
4
  import { toSnakeCase } from "@rebasepro/utils";
5
5
  import { createHash } from "crypto";
6
+ import { logger } from "@rebasepro/server-core";
6
7
  // --- Helper Functions ---
7
8
 
8
9
  /**
@@ -41,6 +42,31 @@ type: "string",
41
42
  isUuid: isUuid ?? false };
42
43
  };
43
44
 
45
+ /**
46
+ * Given a raw DB column name (e.g. "client_id"), find the Drizzle property key
47
+ * on the collection that maps to that column. A property matches if:
48
+ * (a) it has an explicit `columnName` equal to the given column, OR
49
+ * (b) its snake_case form equals the given column.
50
+ *
51
+ * Returns the property key (the Drizzle object key) if found, or the original
52
+ * column name as a fallback.
53
+ */
54
+ const resolvePropertyKeyForColumn = (collection: EntityCollection, column: string): string => {
55
+ if (!collection.properties) return column;
56
+ for (const [propKey, prop] of Object.entries(collection.properties)) {
57
+ const p = prop as Property;
58
+ // Explicit columnName match
59
+ if ("columnName" in p && typeof (p as unknown as Record<string, unknown>).columnName === "string") {
60
+ if ((p as unknown as Record<string, unknown>).columnName === column) return propKey;
61
+ }
62
+ // Convention match: snake_case(propKey) === column
63
+ if (toSnakeCase(propKey) === column) return propKey;
64
+ // Exact match (propKey is already the column name)
65
+ if (propKey === column) return propKey;
66
+ }
67
+ return column;
68
+ };
69
+
44
70
  const isNumericId = (collection: EntityCollection): boolean => {
45
71
  return getPrimaryKeyProp(collection).type === "number";
46
72
  };
@@ -143,7 +169,7 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: EntityCo
143
169
  }
144
170
  // autoValue: database-level default for initial value on INSERT
145
171
  if (dateProp.autoValue === "on_create" || dateProp.autoValue === "on_update") {
146
- columnDefinition += `.default(sql\`now()\`)`;
172
+ columnDefinition += ".default(sql`now()`)";
147
173
  }
148
174
  break;
149
175
  }
@@ -206,7 +232,7 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: EntityCo
206
232
 
207
233
  // The localKey property is the source of truth for the FK column name.
208
234
  if (!relation.localKey) {
209
- console.warn(`Could not generate column for owning relation '${relation.relationName}' on '${collection.name}': 'localKey' is not defined.`);
235
+ logger.warn(`Could not generate column for owning relation '${relation.relationName}' on '${collection.name}': 'localKey' is not defined.`);
210
236
  return null;
211
237
  }
212
238
 
@@ -296,7 +322,7 @@ const resolveRawSql = (expression: string): string => {
296
322
  */
297
323
  const wrapWithRoleCheck = (clause: string, roles: string[]): string => {
298
324
  const rolesArrayString = `ARRAY[${roles.map(r => `'${r}'`).join(",")}]`;
299
- const roleCondition = `string_to_array(auth.roles(), ',') @> ${rolesArrayString}`;
325
+ const roleCondition = `string_to_array(auth.roles(), ',') && ${rolesArrayString}`;
300
326
  return `sql\`(${unwrapSql(clause)}) AND (${roleCondition})\``;
301
327
  };
302
328
 
@@ -410,13 +436,13 @@ const generateSinglePolicyCode = (collection: EntityCollection, rule: SecurityRu
410
436
  } else if (needsUsing) {
411
437
  // Roles-only rule (e.g. { operation: "select", roles: ["admin"] })
412
438
  const rolesArrayString = `ARRAY[${roles.map(r => `'${r}'`).join(",")}]`;
413
- usingClause = `sql\`string_to_array(auth.roles(), ',') @> ${rolesArrayString}\``;
439
+ usingClause = `sql\`string_to_array(auth.roles(), ',') && ${rolesArrayString}\``;
414
440
  }
415
441
  if (withCheckClause) {
416
442
  withCheckClause = wrapWithRoleCheck(withCheckClause, roles);
417
443
  } else if (needsWithCheck) {
418
444
  const rolesArrayString = `ARRAY[${roles.map(r => `'${r}'`).join(",")}]`;
419
- withCheckClause = `sql\`string_to_array(auth.roles(), ',') @> ${rolesArrayString}\``;
445
+ withCheckClause = `sql\`string_to_array(auth.roles(), ',') && ${rolesArrayString}\``;
420
446
  }
421
447
  }
422
448
 
@@ -466,15 +492,22 @@ const computeSharedRelationName = (
466
492
 
467
493
  // --- owning one (belongs-to) ---
468
494
  if (rel.direction === "owning" && rel.cardinality === "one" && rel.localKey) {
469
- return `${getTableName(sourceCollection)}_${rel.localKey}`;
495
+ // Normalise the localKey to the actual Drizzle property name so that
496
+ // the owning side produces the same relation name as the inverse side
497
+ // (which resolves foreignKeyOnTarget via the same helper).
498
+ const normalisedKey = resolvePropertyKeyForColumn(sourceCollection, rel.localKey);
499
+ return `${getTableName(sourceCollection)}_${normalisedKey}`;
470
500
  }
471
501
 
472
502
  // --- inverse many (one-to-many has-many) ---
473
503
  if (rel.direction === "inverse" && rel.cardinality === "many" && rel.foreignKeyOnTarget) {
474
- // The owning table is the *target*, the FK column is foreignKeyOnTarget
504
+ // The owning table is the *target*, the FK column is foreignKeyOnTarget.
505
+ // Resolve to the Drizzle property key on the target so it matches the
506
+ // owning side's normalised localKey.
475
507
  try {
476
508
  const targetCollection = rel.target();
477
- return `${getTableName(targetCollection)}_${rel.foreignKeyOnTarget}`;
509
+ const normalisedFK = resolvePropertyKeyForColumn(targetCollection, rel.foreignKeyOnTarget);
510
+ return `${getTableName(targetCollection)}_${normalisedFK}`;
478
511
  } catch {
479
512
  return fallback;
480
513
  }
@@ -483,10 +516,11 @@ const computeSharedRelationName = (
483
516
  // --- inverse one (one-to-one inverse) ---
484
517
  if (rel.direction === "inverse" && rel.cardinality === "one") {
485
518
  if (rel.foreignKeyOnTarget) {
486
- // FK lives on the target table
519
+ // FK lives on the target table — resolve to Drizzle property key
487
520
  try {
488
521
  const targetCollection = rel.target();
489
- return `${getTableName(targetCollection)}_${rel.foreignKeyOnTarget}`;
522
+ const normalisedFK = resolvePropertyKeyForColumn(targetCollection, rel.foreignKeyOnTarget);
523
+ return `${getTableName(targetCollection)}_${normalisedFK}`;
490
524
  } catch {
491
525
  return fallback;
492
526
  }
@@ -521,7 +555,6 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
521
555
  let schemaContent = "// This file is auto-generated by the Rebase Drizzle generator. Do not edit manually.\n\n";
522
556
 
523
557
 
524
-
525
558
  const hasUuid = collections.some(c =>
526
559
  c.properties && Object.values(c.properties).some(
527
560
  (p: Property) => p.type === "string" && ((p as unknown as Record<string, unknown>).autoValue === "uuid" || (p as unknown as Record<string, unknown>).isId === "uuid")
@@ -812,23 +845,23 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
812
845
  const junctionTableVar = getTableVarName(correspondingRelation.through.table);
813
846
  tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: \"${drizzleRelationName}\" })`);
814
847
  } else {
815
- console.warn(`Could not find corresponding owning many-to-many relation for inverse relation '${relationKey}' on '${collection.name}'`);
848
+ logger.warn(`Could not find corresponding owning many-to-many relation for inverse relation '${relationKey}' on '${collection.name}'`);
816
849
  }
817
850
  } catch (e) {
818
- console.warn(`Could not resolve inverse many-to-many relation '${relationKey}':`, e);
851
+ logger.warn(`Could not resolve inverse many-to-many relation '${relationKey}'`, { error: e });
819
852
  }
820
853
  }
821
854
  // joinPath relations don't generate Drizzle relations - they use existing user tables
822
855
  }
823
856
  } catch (e) {
824
- console.warn(`Could not generate relation ${relationKey} for ${collection.name}:`, e);
857
+ logger.warn(`Could not generate relation ${relationKey} for ${collection.name}`, { error: e });
825
858
  }
826
859
  }
827
860
 
828
861
  // Synthesize missing reciprocal relations
829
862
  for (const otherCollection of collections) {
830
863
  if (otherCollection.slug === collection.slug) continue;
831
-
864
+
832
865
  const otherRelations = resolveCollectionRelations(otherCollection);
833
866
  for (const [otherKey, otherRel] of Object.entries(otherRelations)) {
834
867
  if (otherRel.direction === "inverse" && otherRel.foreignKeyOnTarget) {
@@ -837,11 +870,16 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
837
870
  if (otherTarget.slug === collection.slug) {
838
871
  const drizzleRelationName = computeSharedRelationName(otherRel, otherCollection, collections);
839
872
  const deduplicationKey = `${drizzleRelationName}::owning`;
840
-
873
+
841
874
  if (!emittedRelationNames.has(deduplicationKey)) {
842
875
  const otherTableVar = getTableVarName(getTableName(otherCollection));
843
- const synthKey = `_synth_${otherTableVar}_${otherRel.foreignKeyOnTarget}`;
844
- tableRelations.push(` "${synthKey}": one(${otherTableVar}, {\n fields: [${tableVarName}.${otherRel.foreignKeyOnTarget}],\n references: [${otherTableVar}.${getPrimaryKeyName(otherCollection)}],\n relationName: \"${drizzleRelationName}\"\n })`);
876
+ // Resolve foreignKeyOnTarget to the Drizzle property key
877
+ // on THIS collection (the owning table). The raw FK column
878
+ // name (e.g. "client_id") may differ from the property key
879
+ // (e.g. "clientId") when `columnName` is set.
880
+ const drizzleFieldKey = resolvePropertyKeyForColumn(collection, otherRel.foreignKeyOnTarget);
881
+ const synthKey = `_synth_${otherTableVar}_${drizzleFieldKey}`;
882
+ tableRelations.push(` "${synthKey}": one(${otherTableVar}, {\n fields: [${tableVarName}.${drizzleFieldKey}],\n references: [${otherTableVar}.${getPrimaryKeyName(otherCollection)}],\n relationName: \"${drizzleRelationName}\"\n })`);
845
883
  emittedRelationNames.add(deduplicationKey);
846
884
  }
847
885
  }