@rebasepro/server-postgresql 0.4.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 (168) hide show
  1. package/README.md +69 -89
  2. package/dist/{server-postgresql/src/PostgresAdapter.d.ts → PostgresAdapter.d.ts} +1 -1
  3. package/dist/{server-postgresql/src/PostgresBackendDriver.d.ts → PostgresBackendDriver.d.ts} +2 -2
  4. package/dist/{server-postgresql/src/PostgresBootstrapper.d.ts → PostgresBootstrapper.d.ts} +11 -1
  5. package/dist/{server-postgresql/src/auth → auth}/services.d.ts +11 -11
  6. package/dist/{server-postgresql/src/collections → collections}/PostgresCollectionRegistry.d.ts +4 -0
  7. package/dist/{server-postgresql/src/data-transformer.d.ts → data-transformer.d.ts} +0 -3
  8. package/dist/{server-postgresql/src/databasePoolManager.d.ts → databasePoolManager.d.ts} +1 -1
  9. package/dist/index.es.js +10174 -11184
  10. package/dist/index.es.js.map +1 -1
  11. package/dist/index.umd.js +10735 -11462
  12. package/dist/index.umd.js.map +1 -1
  13. package/dist/{server-postgresql/src/services → services}/EntityPersistService.d.ts +0 -14
  14. package/dist/types.d.ts +3 -0
  15. package/dist/utils/pg-error-utils.d.ts +55 -0
  16. package/dist/{server-postgresql/src/websocket.d.ts → websocket.d.ts} +8 -3
  17. package/package.json +24 -21
  18. package/src/PostgresAdapter.ts +9 -10
  19. package/src/PostgresBackendDriver.ts +135 -122
  20. package/src/PostgresBootstrapper.ts +90 -16
  21. package/src/auth/ensure-tables.ts +28 -5
  22. package/src/auth/services.ts +56 -45
  23. package/src/cli.ts +140 -110
  24. package/src/collections/PostgresCollectionRegistry.ts +7 -0
  25. package/src/connection.ts +11 -6
  26. package/src/data-transformer.ts +73 -109
  27. package/src/databasePoolManager.ts +5 -3
  28. package/src/history/HistoryService.ts +3 -2
  29. package/src/history/ensure-history-table.ts +5 -4
  30. package/src/schema/auth-schema.ts +1 -2
  31. package/src/schema/doctor-cli.ts +2 -1
  32. package/src/schema/doctor.ts +40 -37
  33. package/src/schema/generate-drizzle-schema-logic.ts +56 -18
  34. package/src/schema/generate-drizzle-schema.ts +11 -11
  35. package/src/schema/introspect-db-inference.ts +25 -25
  36. package/src/schema/introspect-db-logic.ts +38 -38
  37. package/src/schema/introspect-db.ts +28 -27
  38. package/src/services/BranchService.ts +14 -0
  39. package/src/services/EntityFetchService.ts +28 -25
  40. package/src/services/EntityPersistService.ts +11 -124
  41. package/src/services/RelationService.ts +57 -37
  42. package/src/services/entity-helpers.ts +6 -2
  43. package/src/services/realtimeService.ts +45 -32
  44. package/src/types.ts +4 -0
  45. package/src/utils/drizzle-conditions.ts +31 -15
  46. package/src/utils/pg-error-utils.ts +211 -0
  47. package/src/websocket.ts +51 -33
  48. package/test/auth-services.test.ts +36 -19
  49. package/test/batch-many-to-many-regression.test.ts +119 -39
  50. package/test/data-transformer-hardening.test.ts +67 -33
  51. package/test/data-transformer.test.ts +4 -2
  52. package/test/doctor.test.ts +10 -5
  53. package/test/drizzle-conditions.test.ts +59 -6
  54. package/test/generate-drizzle-schema.test.ts +65 -40
  55. package/test/introspect-db-generation.test.ts +179 -81
  56. package/test/introspect-db-utils.test.ts +92 -37
  57. package/test/mocks/chalk.cjs +7 -0
  58. package/test/pg-error-utils.test.ts +221 -0
  59. package/test/postgresDataDriver.test.ts +14 -5
  60. package/test/property-ordering.test.ts +126 -79
  61. package/test/realtimeService.test.ts +6 -2
  62. package/test/relation-pipeline-gaps.test.ts +84 -36
  63. package/test/relations.test.ts +247 -0
  64. package/test/unmapped-tables-safety.test.ts +14 -6
  65. package/test/websocket.test.ts +1 -1
  66. package/tsconfig.json +5 -0
  67. package/tsconfig.prod.json +3 -0
  68. package/vite.config.ts +5 -5
  69. package/dist/common/src/collections/CollectionRegistry.d.ts +0 -56
  70. package/dist/common/src/collections/default-collections.d.ts +0 -9
  71. package/dist/common/src/collections/index.d.ts +0 -2
  72. package/dist/common/src/data/buildRebaseData.d.ts +0 -14
  73. package/dist/common/src/data/query_builder.d.ts +0 -55
  74. package/dist/common/src/index.d.ts +0 -4
  75. package/dist/common/src/util/builders.d.ts +0 -57
  76. package/dist/common/src/util/callbacks.d.ts +0 -6
  77. package/dist/common/src/util/collections.d.ts +0 -11
  78. package/dist/common/src/util/common.d.ts +0 -2
  79. package/dist/common/src/util/conditions.d.ts +0 -26
  80. package/dist/common/src/util/entities.d.ts +0 -58
  81. package/dist/common/src/util/enums.d.ts +0 -3
  82. package/dist/common/src/util/index.d.ts +0 -16
  83. package/dist/common/src/util/navigation_from_path.d.ts +0 -34
  84. package/dist/common/src/util/navigation_utils.d.ts +0 -20
  85. package/dist/common/src/util/parent_references_from_path.d.ts +0 -6
  86. package/dist/common/src/util/paths.d.ts +0 -14
  87. package/dist/common/src/util/permissions.d.ts +0 -6
  88. package/dist/common/src/util/references.d.ts +0 -2
  89. package/dist/common/src/util/relations.d.ts +0 -22
  90. package/dist/common/src/util/resolutions.d.ts +0 -72
  91. package/dist/common/src/util/storage.d.ts +0 -24
  92. package/dist/types/src/controllers/analytics_controller.d.ts +0 -7
  93. package/dist/types/src/controllers/auth.d.ts +0 -104
  94. package/dist/types/src/controllers/client.d.ts +0 -168
  95. package/dist/types/src/controllers/collection_registry.d.ts +0 -46
  96. package/dist/types/src/controllers/customization_controller.d.ts +0 -60
  97. package/dist/types/src/controllers/data.d.ts +0 -207
  98. package/dist/types/src/controllers/data_driver.d.ts +0 -218
  99. package/dist/types/src/controllers/database_admin.d.ts +0 -11
  100. package/dist/types/src/controllers/dialogs_controller.d.ts +0 -36
  101. package/dist/types/src/controllers/effective_role.d.ts +0 -4
  102. package/dist/types/src/controllers/email.d.ts +0 -36
  103. package/dist/types/src/controllers/index.d.ts +0 -18
  104. package/dist/types/src/controllers/local_config_persistence.d.ts +0 -20
  105. package/dist/types/src/controllers/navigation.d.ts +0 -225
  106. package/dist/types/src/controllers/registry.d.ts +0 -63
  107. package/dist/types/src/controllers/side_dialogs_controller.d.ts +0 -67
  108. package/dist/types/src/controllers/side_entity_controller.d.ts +0 -97
  109. package/dist/types/src/controllers/snackbar.d.ts +0 -24
  110. package/dist/types/src/controllers/storage.d.ts +0 -171
  111. package/dist/types/src/index.d.ts +0 -4
  112. package/dist/types/src/rebase_context.d.ts +0 -122
  113. package/dist/types/src/types/auth_adapter.d.ts +0 -301
  114. package/dist/types/src/types/backend.d.ts +0 -536
  115. package/dist/types/src/types/backend_hooks.d.ts +0 -172
  116. package/dist/types/src/types/builders.d.ts +0 -15
  117. package/dist/types/src/types/chips.d.ts +0 -5
  118. package/dist/types/src/types/collections.d.ts +0 -941
  119. package/dist/types/src/types/component_ref.d.ts +0 -47
  120. package/dist/types/src/types/cron.d.ts +0 -102
  121. package/dist/types/src/types/data_source.d.ts +0 -64
  122. package/dist/types/src/types/database_adapter.d.ts +0 -94
  123. package/dist/types/src/types/entities.d.ts +0 -145
  124. package/dist/types/src/types/entity_actions.d.ts +0 -104
  125. package/dist/types/src/types/entity_callbacks.d.ts +0 -173
  126. package/dist/types/src/types/entity_link_builder.d.ts +0 -7
  127. package/dist/types/src/types/entity_overrides.d.ts +0 -10
  128. package/dist/types/src/types/entity_views.d.ts +0 -87
  129. package/dist/types/src/types/export_import.d.ts +0 -21
  130. package/dist/types/src/types/formex.d.ts +0 -40
  131. package/dist/types/src/types/index.d.ts +0 -28
  132. package/dist/types/src/types/locales.d.ts +0 -4
  133. package/dist/types/src/types/modify_collections.d.ts +0 -5
  134. package/dist/types/src/types/plugins.d.ts +0 -282
  135. package/dist/types/src/types/properties.d.ts +0 -1181
  136. package/dist/types/src/types/property_config.d.ts +0 -74
  137. package/dist/types/src/types/relations.d.ts +0 -336
  138. package/dist/types/src/types/slots.d.ts +0 -262
  139. package/dist/types/src/types/translations.d.ts +0 -900
  140. package/dist/types/src/types/user_management_delegate.d.ts +0 -86
  141. package/dist/types/src/types/websockets.d.ts +0 -78
  142. package/dist/types/src/users/index.d.ts +0 -1
  143. package/dist/types/src/users/user.d.ts +0 -50
  144. package/drizzle.test.config.ts +0 -10
  145. /package/dist/{server-postgresql/src/auth → auth}/ensure-tables.d.ts +0 -0
  146. /package/dist/{server-postgresql/src/cli.d.ts → cli.d.ts} +0 -0
  147. /package/dist/{server-postgresql/src/connection.d.ts → connection.d.ts} +0 -0
  148. /package/dist/{server-postgresql/src/history → history}/HistoryService.d.ts +0 -0
  149. /package/dist/{server-postgresql/src/history → history}/ensure-history-table.d.ts +0 -0
  150. /package/dist/{server-postgresql/src/index.d.ts → index.d.ts} +0 -0
  151. /package/dist/{server-postgresql/src/interfaces.d.ts → interfaces.d.ts} +0 -0
  152. /package/dist/{server-postgresql/src/schema → schema}/auth-schema.d.ts +0 -0
  153. /package/dist/{server-postgresql/src/schema → schema}/doctor-cli.d.ts +0 -0
  154. /package/dist/{server-postgresql/src/schema → schema}/doctor.d.ts +0 -0
  155. /package/dist/{server-postgresql/src/schema → schema}/generate-drizzle-schema-logic.d.ts +0 -0
  156. /package/dist/{server-postgresql/src/schema → schema}/generate-drizzle-schema.d.ts +0 -0
  157. /package/dist/{server-postgresql/src/schema → schema}/introspect-db-inference.d.ts +0 -0
  158. /package/dist/{server-postgresql/src/schema → schema}/introspect-db-logic.d.ts +0 -0
  159. /package/dist/{server-postgresql/src/schema → schema}/introspect-db.d.ts +0 -0
  160. /package/dist/{server-postgresql/src/schema → schema}/test-schema.d.ts +0 -0
  161. /package/dist/{server-postgresql/src/services → services}/BranchService.d.ts +0 -0
  162. /package/dist/{server-postgresql/src/services → services}/EntityFetchService.d.ts +0 -0
  163. /package/dist/{server-postgresql/src/services → services}/RelationService.d.ts +0 -0
  164. /package/dist/{server-postgresql/src/services → services}/entity-helpers.d.ts +0 -0
  165. /package/dist/{server-postgresql/src/services → services}/entityService.d.ts +0 -0
  166. /package/dist/{server-postgresql/src/services → services}/index.d.ts +0 -0
  167. /package/dist/{server-postgresql/src/services → services}/realtimeService.d.ts +0 -0
  168. /package/dist/{server-postgresql/src/utils → utils}/drizzle-conditions.d.ts +0 -0
@@ -18,8 +18,9 @@ import {
18
18
  generateCollectionFile,
19
19
  generateIndexContent,
20
20
  mergeIndexContent,
21
- safeHostFromUrl,
21
+ safeHostFromUrl
22
22
  } from "./introspect-db-logic";
23
+ import { logger } from "@rebasepro/server-core";
23
24
 
24
25
  async function main() {
25
26
  const args = arg(
@@ -31,15 +32,15 @@ async function main() {
31
32
  "--data-inference": Boolean,
32
33
  "-o": "--output",
33
34
  "-c": "--collections",
34
- "-f": "--force",
35
+ "-f": "--force"
35
36
  },
36
37
  { permissive: true }
37
38
  );
38
39
 
39
40
  const cwd = process.cwd();
40
41
  const isBackendDir = path.basename(cwd) === "backend";
41
- const defaultOutDir = isBackendDir
42
- ? path.resolve(cwd, "..", "config", "collections")
42
+ const defaultOutDir = isBackendDir
43
+ ? path.resolve(cwd, "..", "config", "collections")
43
44
  : path.resolve(cwd, "config", "collections");
44
45
 
45
46
  const outDir = args["--output"] || args["--collections"] || defaultOutDir;
@@ -67,7 +68,7 @@ async function main() {
67
68
 
68
69
  const databaseUrl = process.env.DATABASE_URL || process.env.ADMIN_CONNECTION_STRING;
69
70
  if (!databaseUrl) {
70
- console.error(chalk.red("✗ DATABASE_URL is not set. Make sure your .env file is configured."));
71
+ logger.error(chalk.red("✗ DATABASE_URL is not set. Make sure your .env file is configured."));
71
72
  process.exit(1);
72
73
  }
73
74
 
@@ -76,15 +77,15 @@ async function main() {
76
77
  try {
77
78
  await client.connect();
78
79
  } catch (err) {
79
- console.error(chalk.red(`✗ Failed to connect to database: ${err instanceof Error ? err.message : String(err)}`));
80
- console.error(chalk.gray(" Check your DATABASE_URL and ensure the database is reachable."));
80
+ logger.error(chalk.red(`✗ Failed to connect to database: ${err instanceof Error ? err.message : String(err)}`));
81
+ logger.error(chalk.gray(" Check your DATABASE_URL and ensure the database is reachable."));
81
82
  process.exit(1);
82
83
  }
83
84
 
84
85
  // Log the host portion safely — handle URLs without "@"
85
86
  const hostPart = safeHostFromUrl(databaseUrl);
86
- console.log(chalk.gray(`Connected to database: ${hostPart}`));
87
- console.log(chalk.gray(`Introspecting schema '${pgSchema}'...`));
87
+ logger.info(chalk.gray(`Connected to database: ${hostPart}`));
88
+ logger.info(chalk.gray(`Introspecting schema '${pgSchema}'...`));
88
89
 
89
90
  try {
90
91
  // 1. Get Tables
@@ -162,7 +163,7 @@ async function main() {
162
163
  const tablesMap = buildTablesMap(tables, columns, pks, fks);
163
164
  const joinTables = identifyJoinTables(tablesMap);
164
165
 
165
- console.log(chalk.blue(`Found ${tablesMap.size} tables (including ${joinTables.size} detected join tables).`));
166
+ logger.info(chalk.blue(`Found ${tablesMap.size} tables (including ${joinTables.size} detected join tables).`));
166
167
 
167
168
  let runDataInference = false;
168
169
  if (args["--data-inference"] !== undefined) {
@@ -173,12 +174,12 @@ async function main() {
173
174
  output: process.stdout
174
175
  });
175
176
  const answer = await new Promise<string>((resolve) => rl.question(chalk.yellow("? Do you want to run comprehensive data inference on sampled rows to auto-detect types, formats, constraints, and UI configurations? (y/N) "), resolve));
176
- runDataInference = answer.trim().toLowerCase() === 'y';
177
+ runDataInference = answer.trim().toLowerCase() === "y";
177
178
  rl.close();
178
179
  }
179
180
 
180
181
  if (runDataInference) {
181
- console.log(chalk.gray(`Sampling database rows for data inference...`));
182
+ logger.info(chalk.gray("Sampling database rows for data inference..."));
182
183
  }
183
184
 
184
185
  // Generate Collections
@@ -186,11 +187,11 @@ async function main() {
186
187
  const skippedFiles: string[] = [];
187
188
 
188
189
  const tablesToProcess = Array.from(tablesMap.entries()).filter(([tableName]) => !joinTables.has(tableName));
189
-
190
+
190
191
  const BATCH_SIZE = 10;
191
192
  for (let i = 0; i < tablesToProcess.length; i += BATCH_SIZE) {
192
193
  const batch = tablesToProcess.slice(i, i + BATCH_SIZE);
193
-
194
+
194
195
  await Promise.all(batch.map(async ([tableName, meta]) => {
195
196
  // ── File overwrite protection ──────────────────────────────
196
197
  const filePath = path.join(outDir, `${tableName}.ts`);
@@ -205,7 +206,7 @@ async function main() {
205
206
  const { rows } = await client.query(`SELECT * FROM "${pgSchema}"."${tableName}" LIMIT 100`);
206
207
  sampleData = rows;
207
208
  } catch (err) {
208
- console.error(chalk.yellow(`⚠ Failed to sample data for table ${tableName}: ${err instanceof Error ? err.message : String(err)}`));
209
+ logger.error(chalk.yellow(`⚠ Failed to sample data for table ${tableName}: ${err instanceof Error ? err.message : String(err)}`));
209
210
  }
210
211
  }
211
212
 
@@ -216,12 +217,12 @@ async function main() {
216
217
  joinTables,
217
218
  tablesMap,
218
219
  enumMap,
219
- sampleData,
220
+ sampleData
220
221
  );
221
222
 
222
223
  fs.writeFileSync(filePath, fileContent, "utf-8");
223
224
  generatedFiles.push(tableName);
224
- console.log(chalk.green(` ✓ ${filePath}`));
225
+ logger.info(chalk.green(` ✓ ${filePath}`));
225
226
  }));
226
227
  }
227
228
 
@@ -238,21 +239,21 @@ async function main() {
238
239
  const indexContent = generateIndexContent(generatedFiles);
239
240
  fs.writeFileSync(indexPath, indexContent, "utf-8");
240
241
  }
241
- console.log(chalk.green(` ✓ ${indexPath}`));
242
+ logger.info(chalk.green(` ✓ ${indexPath}`));
242
243
  }
243
244
 
244
- console.log("");
245
+ logger.info("");
245
246
  if (skippedFiles.length > 0) {
246
- console.log(chalk.yellow(`⚠ Skipped ${skippedFiles.length} existing file(s): ${skippedFiles.join(", ")}`));
247
- console.log(chalk.gray(` Use --force to overwrite existing files.`));
248
- console.log("");
247
+ logger.info(chalk.yellow(`⚠ Skipped ${skippedFiles.length} existing file(s): ${skippedFiles.join(", ")}`));
248
+ logger.info(chalk.gray(" Use --force to overwrite existing files."));
249
+ logger.info("");
249
250
  }
250
- console.log(chalk.bold.green(`✓ Introspected ${tablesMap.size} tables — generated ${generatedFiles.length} collection(s).`));
251
- console.log(chalk.gray(` Review the generated files in ${outDir} and customize properties as needed.`));
252
- console.log("");
251
+ logger.info(chalk.bold.green(`✓ Introspected ${tablesMap.size} tables — generated ${generatedFiles.length} collection(s).`));
252
+ logger.info(chalk.gray(` Review the generated files in ${outDir} and customize properties as needed.`));
253
+ logger.info("");
253
254
 
254
255
  } catch (e) {
255
- console.error(chalk.red(`✗ Error introspecting database: ${e instanceof Error ? e.message : String(e)}`));
256
+ logger.error(chalk.red(`✗ Error introspecting database: ${e instanceof Error ? e.message : String(e)}`));
256
257
  process.exit(1);
257
258
  } finally {
258
259
  await client.end();
@@ -260,6 +261,6 @@ async function main() {
260
261
  }
261
262
 
262
263
  main().catch((err) => {
263
- console.error(err);
264
+ logger.error(String(err));
264
265
  process.exit(1);
265
266
  });
@@ -18,6 +18,16 @@ const BRANCH_DB_PREFIX = "rb_";
18
18
  /** Fully-qualified metadata table in the rebase schema. */
19
19
  const BRANCHES_TABLE = "rebase.branches";
20
20
 
21
+ /**
22
+ * Validate that a user-provided identifier only contains safe characters.
23
+ * Throws if the value contains characters outside [a-zA-Z0-9_-].
24
+ */
25
+ function validateIdentifier(value: string, label: string): void {
26
+ if (!/^[a-zA-Z0-9_-]+$/.test(value)) {
27
+ throw new Error(`Invalid ${label}: only letters, digits, underscores, and hyphens are allowed.`);
28
+ }
29
+ }
30
+
21
31
  /**
22
32
  * Sanitize a user-provided branch name to a safe PostgreSQL identifier.
23
33
  * Only allows alphanumeric characters and underscores.
@@ -70,6 +80,10 @@ export class BranchService {
70
80
  * @param options.source Source database to clone; defaults to the main database.
71
81
  */
72
82
  async createBranch(name: string, options?: { source?: string }): Promise<BranchInfo> {
83
+ if (options?.source) {
84
+ validateIdentifier(options.source, "source database name");
85
+ }
86
+
73
87
  const dbName = toBranchDbName(name);
74
88
  const sanitizedName = sanitizeBranchName(name);
75
89
  const sourceDb = options?.source || this.poolManager.defaultDatabaseName;
@@ -16,6 +16,7 @@ import { RelationService } from "./RelationService";
16
16
  import { RelationalQueryBuilder } from "drizzle-orm/pg-core/query-builders/query";
17
17
  import { DrizzleClient } from "../interfaces";
18
18
  import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
19
+ import { logger } from "@rebasepro/server-core";
19
20
 
20
21
  /** Type-safe accessor for Drizzle's relational query API via dynamic table name */
21
22
  type DbQueryAccessor = Record<string, RelationalQueryBuilder<any, any>> | undefined;
@@ -269,7 +270,7 @@ export class EntityFetchService {
269
270
  );
270
271
  }
271
272
  } catch (e) {
272
- console.warn(`Could not resolve joinPath relation '${key}':`, e);
273
+ logger.warn(`Could not resolve joinPath relation '${key}'`, { error: e });
273
274
  }
274
275
  });
275
276
 
@@ -322,7 +323,7 @@ export class EntityFetchService {
322
323
  }
323
324
  }
324
325
  } catch (e) {
325
- console.warn(`Could not batch resolve joinPath relation '${key}':`, e);
326
+ logger.warn(`Could not batch resolve joinPath relation '${key}'`, { error: e });
326
327
  }
327
328
  }
328
329
  }
@@ -400,7 +401,7 @@ export class EntityFetchService {
400
401
  }
401
402
  }
402
403
  } catch (e) {
403
- console.warn(`Could not batch resolve joinPath relation '${key}' for REST:`, e);
404
+ logger.warn(`Could not batch resolve joinPath relation '${key}' for REST`, { error: e });
404
405
  }
405
406
  }
406
407
  }
@@ -625,10 +626,10 @@ export class EntityFetchService {
625
626
  return entity;
626
627
  } catch (e) {
627
628
  if (e instanceof Error && e.message.includes("not enough information to infer relation")) {
628
- console.error(`[EntityFetchService] Relation inference error for collection '${collectionPath}': ${e.message}`);
629
- console.error(`Hint: This usually means a relation in your drizzle schema is missing a reciprocal 'one()' or 'many()' definition. Run 'rebase schema generate' to fix this.`);
629
+ logger.error(`[EntityFetchService] Relation inference error for collection '${collectionPath}': ${e.message}`);
630
+ logger.error("Hint: This usually means a relation in your drizzle schema is missing a reciprocal 'one()' or 'many()' definition. Run 'rebase schema generate' to fix this.");
630
631
  }
631
- console.warn(`[EntityFetchService] db.query.findFirst failed for ${collectionPath}, falling back to db.select:`, e);
632
+ logger.warn(`[EntityFetchService] db.query.findFirst failed for ${collectionPath}, falling back to db.select`, { error: e });
632
633
  }
633
634
  }
634
635
 
@@ -675,7 +676,7 @@ export class EntityFetchService {
675
676
  (values as Record<string, unknown>)[key] = createRelationRef(e.id, e.path);
676
677
  }
677
678
  } catch (e) {
678
- console.warn(`Could not resolve one-to-one relation property: ${key}`, e);
679
+ logger.warn(`Could not resolve one-to-one relation property: ${key}`, { error: e });
679
680
  }
680
681
  }
681
682
  }
@@ -749,10 +750,10 @@ export class EntityFetchService {
749
750
  return entities;
750
751
  } catch (e) {
751
752
  if (e instanceof Error && e.message.includes("not enough information to infer relation")) {
752
- console.error(`[EntityFetchService] Relation inference error for collection '${collectionPath}': ${e.message}`);
753
- console.error(`Hint: This usually means a relation in your drizzle schema is missing a reciprocal 'one()' or 'many()' definition. Run 'rebase schema generate' to fix this.`);
753
+ logger.error(`[EntityFetchService] Relation inference error for collection '${collectionPath}': ${e.message}`);
754
+ logger.error("Hint: This usually means a relation in your drizzle schema is missing a reciprocal 'one()' or 'many()' definition. Run 'rebase schema generate' to fix this.");
754
755
  }
755
- console.warn(`[EntityFetchService] db.query.findMany failed for ${collectionPath}, falling back to db.select:`, e);
756
+ logger.warn(`[EntityFetchService] db.query.findMany failed for ${collectionPath}, falling back to db.select`, { error: e });
756
757
  }
757
758
  }
758
759
 
@@ -764,7 +765,8 @@ export class EntityFetchService {
764
765
  }
765
766
 
766
767
  let query = vectorMeta
767
- ? this.db.select({ table_row: table, _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
768
+ ? this.db.select({ table_row: table,
769
+ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
768
770
  : this.db.select().from(table).$dynamic();
769
771
  const allConditions: SQL[] = [];
770
772
 
@@ -908,7 +910,7 @@ export class EntityFetchService {
908
910
  }
909
911
  });
910
912
  } catch (e) {
911
- console.warn(`Could not batch load one-to-one relation property: ${key}`, e);
913
+ logger.warn(`Could not batch load one-to-one relation property: ${key}`, { error: e });
912
914
  }
913
915
  }
914
916
 
@@ -935,7 +937,7 @@ export class EntityFetchService {
935
937
  );
936
938
  });
937
939
  } catch (e) {
938
- console.warn(`Could not batch load many relation property: ${key}`, e);
940
+ logger.warn(`Could not batch load many relation property: ${key}`, { error: e });
939
941
  }
940
942
  }
941
943
  }
@@ -1250,10 +1252,10 @@ export class EntityFetchService {
1250
1252
  return restRows;
1251
1253
  } catch (e) {
1252
1254
  if (e instanceof Error && e.message.includes("not enough information to infer relation")) {
1253
- console.error(`[EntityFetchService] Relation inference error for collection '${collectionPath}': ${e.message}`);
1254
- console.error(`Hint: This usually means a relation in your drizzle schema is missing a reciprocal 'one()' or 'many()' definition. Run 'rebase schema generate' to fix this.`);
1255
+ logger.error(`[EntityFetchService] Relation inference error for collection '${collectionPath}': ${e.message}`);
1256
+ logger.error("Hint: This usually means a relation in your drizzle schema is missing a reciprocal 'one()' or 'many()' definition. Run 'rebase schema generate' to fix this.");
1255
1257
  }
1256
- console.warn(`[fetchCollectionForRest] db.query.findMany failed for ${collectionPath}, falling back:`, e);
1258
+ logger.warn(`[fetchCollectionForRest] db.query.findMany failed for ${collectionPath}, falling back`, { error: e });
1257
1259
  }
1258
1260
  }
1259
1261
 
@@ -1290,7 +1292,7 @@ export class EntityFetchService {
1290
1292
  }
1291
1293
  }
1292
1294
  } catch (e) {
1293
- console.warn(`[include] Failed to batch load one-to-one '${key}':`, e);
1295
+ logger.warn(`[include] Failed to batch load one-to-one '${key}'`, { error: e });
1294
1296
  }
1295
1297
  }
1296
1298
 
@@ -1309,7 +1311,7 @@ export class EntityFetchService {
1309
1311
  }));
1310
1312
  }
1311
1313
  } catch (e) {
1312
- console.warn(`[include] Failed to batch load many '${key}':`, e);
1314
+ logger.warn(`[include] Failed to batch load many '${key}'`, { error: e });
1313
1315
  }
1314
1316
  }
1315
1317
 
@@ -1364,10 +1366,10 @@ export class EntityFetchService {
1364
1366
  return restRow;
1365
1367
  } catch (e) {
1366
1368
  if (e instanceof Error && e.message.includes("not enough information to infer relation")) {
1367
- console.error(`[EntityFetchService] Relation inference error for collection '${collectionPath}': ${e.message}`);
1368
- console.error(`Hint: This usually means a relation in your drizzle schema is missing a reciprocal 'one()' or 'many()' definition. Run 'rebase schema generate' to fix this.`);
1369
+ logger.error(`[EntityFetchService] Relation inference error for collection '${collectionPath}': ${e.message}`);
1370
+ logger.error("Hint: This usually means a relation in your drizzle schema is missing a reciprocal 'one()' or 'many()' definition. Run 'rebase schema generate' to fix this.");
1369
1371
  }
1370
- console.warn(`[fetchEntityForRest] db.query.findFirst failed for ${collectionPath}, falling back:`, e);
1372
+ logger.warn(`[fetchEntityForRest] db.query.findFirst failed for ${collectionPath}, falling back`, { error: e });
1371
1373
  }
1372
1374
  }
1373
1375
 
@@ -1415,7 +1417,7 @@ export class EntityFetchService {
1415
1417
  }));
1416
1418
  }
1417
1419
  } catch (e) {
1418
- console.warn(`[include] Failed to load relation '${key}':`, e);
1420
+ logger.warn(`[include] Failed to load relation '${key}'`, { error: e });
1419
1421
  }
1420
1422
  }
1421
1423
 
@@ -1450,7 +1452,8 @@ export class EntityFetchService {
1450
1452
  }
1451
1453
 
1452
1454
  let query = vectorMeta
1453
- ? this.db.select({ table_row: table, _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
1455
+ ? this.db.select({ table_row: table,
1456
+ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
1454
1457
  : this.db.select().from(table).$dynamic();
1455
1458
  const allConditions: SQL[] = [];
1456
1459
 
@@ -1621,7 +1624,7 @@ export class EntityFetchService {
1621
1624
  return flat;
1622
1625
  });
1623
1626
  } catch (e) {
1624
- console.warn(`[include] Drizzle relational query failed for '${collectionPath}', falling back:`, e);
1627
+ logger.warn(`[include] Drizzle relational query failed for '${collectionPath}', falling back`, { error: e });
1625
1628
  return null;
1626
1629
  }
1627
1630
  }
@@ -1647,7 +1650,7 @@ export class EntityFetchService {
1647
1650
  const relation = resolvedRelations[relationKey];
1648
1651
 
1649
1652
  if (!relation) {
1650
- console.warn(`[batchFetchManyRelatedEntities] Relation '${relationKey}' not found, skipping`);
1653
+ logger.warn(`[batchFetchManyRelatedEntities] Relation '${relationKey}' not found, skipping`);
1651
1654
  return new Map();
1652
1655
  }
1653
1656
 
@@ -16,6 +16,8 @@ import { RelationService } from "./RelationService";
16
16
  import { EntityFetchService } from "./EntityFetchService";
17
17
  import { DrizzleClient } from "../interfaces";
18
18
  import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
19
+ import { logger } from "@rebasepro/server-core";
20
+ import { extractPgError, extractCauseMessage, pgErrorToFriendlyMessage } from "../utils/pg-error-utils";
19
21
 
20
22
  /**
21
23
  * Service for handling all entity write operations.
@@ -128,7 +130,7 @@ export class EntityPersistService {
128
130
  const targetColumnNames = DrizzleConditionBuilder.getColumnNamesFromColumns(relevantJoinStep.on.to);
129
131
  targetColumnName = targetColumnNames[0];
130
132
  } else {
131
- console.warn(`Could not find specific join step for target table ${targetTableName} in relation '${relationKey}'.`);
133
+ logger.warn(`Could not find specific join step for target table ${targetTableName} in relation '${relationKey}'.`);
132
134
  const targetColumnNames = DrizzleConditionBuilder.getColumnNamesFromColumns(relation.joinPath[0].on.to);
133
135
  targetColumnName = targetColumnNames[0];
134
136
  }
@@ -149,7 +151,7 @@ export class EntityPersistService {
149
151
 
150
152
  const existingValue = (effectiveValues as Record<string, unknown>)[targetColumnName];
151
153
  if (existingValue !== undefined && existingValue !== null && existingValue !== parsedParentId) {
152
- console.warn(`Overriding provided value '${existingValue}' for FK '${targetColumnName}' with path parent id '${parsedParentId}'.`);
154
+ logger.warn(`Overriding provided value '${existingValue}' for FK '${targetColumnName}' with path parent id '${parsedParentId}'.`);
153
155
  }
154
156
  (effectiveValues as Record<string, unknown>)[targetColumnName] = parsedParentId;
155
157
  break;
@@ -302,139 +304,24 @@ export class EntityPersistService {
302
304
  * Translate raw PostgreSQL / Drizzle errors into user-friendly messages.
303
305
  */
304
306
  private toUserFriendlyError(error: unknown, collectionSlug: string): Error {
305
- // Dig into Drizzle's wrapper to find the underlying PG error
306
- const pgError = this.extractPgError(error);
307
+ const pgError = extractPgError(error);
307
308
 
308
309
  if (pgError) {
309
- const detail = pgError.detail as string | undefined;
310
- const hint = pgError.hint as string | undefined;
311
- const constraint = pgError.constraint as string | undefined;
312
- const column = pgError.column as string | undefined;
313
- const table = pgError.table as string | undefined;
314
- const dataType = pgError.dataType as string | undefined;
315
- const pgMessage = pgError.message || "Unknown database error";
316
-
317
- const suffix = hint ? ` Hint: ${hint}` : "";
318
- const tableRef = table ?? collectionSlug;
319
-
320
- switch (pgError.code) {
321
- case "23503": // foreign_key_violation
322
- return new Error(
323
- detail
324
- ? `Foreign key constraint violated: ${detail}${suffix}`
325
- : `Cannot save: a foreign key constraint${constraint ? ` (${constraint})` : ""} was violated in "${collectionSlug}".${suffix}`
326
- );
327
- case "23505": // unique_violation
328
- return new Error(
329
- detail
330
- ? `Duplicate value: ${detail}${suffix}`
331
- : `Cannot save: a unique constraint${constraint ? ` (${constraint})` : ""} was violated in "${collectionSlug}".${suffix}`
332
- );
333
- case "23502": // not_null_violation
334
- return new Error(
335
- `Missing required field: "${column ?? "unknown"}" in "${tableRef}" cannot be empty.${suffix}`
336
- );
337
- case "23514": // check_violation
338
- return new Error(
339
- `Validation failed: a check constraint${constraint ? ` (${constraint})` : ""} was violated in "${collectionSlug}".${suffix}`
340
- );
341
- case "22P02": // invalid_text_representation (e.g. invalid UUID, wrong enum value)
342
- return new Error(
343
- `Invalid data format in "${collectionSlug}": ${pgMessage}${suffix}`
344
- );
345
- case "22001": // string_data_right_truncation (value too long)
346
- return new Error(
347
- `Value too long for column "${column ?? "unknown"}" in "${tableRef}": ${pgMessage}${suffix}`
348
- );
349
- case "22003": // numeric_value_out_of_range
350
- return new Error(
351
- `Numeric value out of range for column "${column ?? "unknown"}" in "${tableRef}": ${pgMessage}${suffix}`
352
- );
353
- case "42703": // undefined_column
354
- return new Error(
355
- `Unknown column in "${tableRef}": ${pgMessage}. Check if your schema is up to date (run migrations).${suffix}`
356
- );
357
- case "42P01": // undefined_table
358
- return new Error(
359
- `Table not found for "${collectionSlug}": ${pgMessage}. Check if your schema is up to date (run migrations).${suffix}`
360
- );
361
- default: {
362
- // Unhandled PG code — still surface the actual database message
363
- const parts = [`Database error in "${collectionSlug}" [${pgError.code}]: ${pgMessage}`];
364
- if (detail) parts.push(`Detail: ${detail}`);
365
- if (column) parts.push(`Column: ${column}`);
366
- if (dataType) parts.push(`Data type: ${dataType}`);
367
- if (constraint) parts.push(`Constraint: ${constraint}`);
368
- if (hint) parts.push(`Hint: ${hint}`);
369
- return new Error(parts.join(". "));
370
- }
371
- }
310
+ const { message } = pgErrorToFriendlyMessage(pgError, collectionSlug);
311
+ return new Error(message);
372
312
  }
373
313
 
374
314
  // No PG error found — try to extract a useful message from the
375
315
  // Drizzle wrapper instead of leaking the raw SQL query + params.
376
- const causeMessage = this.extractCauseMessage(error);
316
+ const causeMessage = extractCauseMessage(error);
377
317
  if (causeMessage) {
378
318
  return new Error(`Database error in "${collectionSlug}": ${causeMessage}`);
379
319
  }
380
320
 
381
- // Last resort: use the original error message but strip the SQL query
382
- if (error instanceof Error) {
383
- const cleaned = this.stripSqlFromMessage(error.message, collectionSlug);
384
- return new Error(cleaned);
321
+ // Last resort: generic message, never leak raw SQL
322
+ if (error instanceof Error && error.message.startsWith("Failed query:")) {
323
+ return new Error(`Failed to save entity in "${collectionSlug}". Check server logs for details.`);
385
324
  }
386
325
  return new Error(`Database error in "${collectionSlug}": ${String(error)}`);
387
326
  }
388
-
389
- /**
390
- * Walk the error cause chain and return the deepest meaningful message.
391
- */
392
- private extractCauseMessage(error: unknown): string | null {
393
- if (!error || typeof error !== "object") return null;
394
- const err = error as Error & { cause?: unknown };
395
-
396
- if (err.cause && typeof err.cause === "object") {
397
- const deeper = this.extractCauseMessage(err.cause);
398
- if (deeper) return deeper;
399
- // The cause itself has a message
400
- if (err.cause instanceof Error && err.cause.message) {
401
- return err.cause.message;
402
- }
403
- }
404
- return null;
405
- }
406
-
407
- /**
408
- * Strip the raw SQL query from a Drizzle "Failed query: ..." message,
409
- * keeping only the error description.
410
- */
411
- private stripSqlFromMessage(message: string, collectionSlug: string): string {
412
- // Drizzle format: "Failed query: <SQL>\nparams: <params>"
413
- if (message.startsWith("Failed query:")) {
414
- return `Failed to save entity in "${collectionSlug}". Check server logs for details.`;
415
- }
416
- return message;
417
- }
418
-
419
- /**
420
- * Extract the underlying PostgreSQL error from a Drizzle wrapper.
421
- * Drizzle wraps PG errors in a `cause` property.
422
- */
423
- private extractPgError(error: unknown): (Error & { code?: string; detail?: unknown; hint?: unknown; constraint?: unknown; column?: unknown; table?: unknown; dataType?: unknown }) | null {
424
- if (!error || typeof error !== "object") return null;
425
-
426
- const err = error as Error & { code?: string; cause?: unknown; detail?: unknown };
427
-
428
- // Check if the error itself has a PG error code
429
- if (err.code && /^[0-9A-Z]{5}$/.test(err.code)) {
430
- return err as Error & { code: string; detail?: unknown; hint?: unknown; constraint?: unknown; column?: unknown; table?: unknown; dataType?: unknown };
431
- }
432
-
433
- // Check the cause chain (Drizzle wraps PG errors)
434
- if (err.cause && typeof err.cause === "object") {
435
- return this.extractPgError(err.cause);
436
- }
437
-
438
- return null;
439
- }
440
327
  }