@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
@@ -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,18 +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
-
20
- /** Shape of PostgreSQL errors with diagnostic metadata. */
21
- interface PostgresError extends Error {
22
- code?: string;
23
- detail?: string;
24
- hint?: string;
25
- constraint?: string;
26
- column?: string;
27
- table?: string;
28
- dataType?: string;
29
- cause?: unknown;
30
- }
19
+ import { logger } from "@rebasepro/server-core";
20
+ import { extractPgError, extractCauseMessage, pgErrorToFriendlyMessage } from "../utils/pg-error-utils";
31
21
 
32
22
  /**
33
23
  * Service for handling all entity write operations.
@@ -140,7 +130,7 @@ export class EntityPersistService {
140
130
  const targetColumnNames = DrizzleConditionBuilder.getColumnNamesFromColumns(relevantJoinStep.on.to);
141
131
  targetColumnName = targetColumnNames[0];
142
132
  } else {
143
- 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}'.`);
144
134
  const targetColumnNames = DrizzleConditionBuilder.getColumnNamesFromColumns(relation.joinPath[0].on.to);
145
135
  targetColumnName = targetColumnNames[0];
146
136
  }
@@ -161,7 +151,7 @@ export class EntityPersistService {
161
151
 
162
152
  const existingValue = (effectiveValues as Record<string, unknown>)[targetColumnName];
163
153
  if (existingValue !== undefined && existingValue !== null && existingValue !== parsedParentId) {
164
- 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}'.`);
165
155
  }
166
156
  (effectiveValues as Record<string, unknown>)[targetColumnName] = parsedParentId;
167
157
  break;
@@ -314,144 +304,24 @@ export class EntityPersistService {
314
304
  * Translate raw PostgreSQL / Drizzle errors into user-friendly messages.
315
305
  */
316
306
  private toUserFriendlyError(error: unknown, collectionSlug: string): Error {
317
- // Dig into Drizzle's wrapper to find the underlying PG error
318
- const pgError = this.extractPgError(error);
307
+ const pgError = extractPgError(error);
319
308
 
320
309
  if (pgError) {
321
- const detail = pgError.detail as string | undefined;
322
- const hint = pgError.hint as string | undefined;
323
- const constraint = pgError.constraint as string | undefined;
324
- const column = pgError.column as string | undefined;
325
- const table = pgError.table as string | undefined;
326
- const dataType = pgError.dataType as string | undefined;
327
- const pgMessage = pgError.message || "Unknown database error";
328
-
329
- const suffix = hint ? ` Hint: ${hint}` : "";
330
- const tableRef = table ?? collectionSlug;
331
-
332
- switch (pgError.code) {
333
- case "23503": // foreign_key_violation
334
- return new Error(
335
- detail
336
- ? `Foreign key constraint violated: ${detail}${suffix}`
337
- : `Cannot save: a foreign key constraint${constraint ? ` (${constraint})` : ""} was violated in "${collectionSlug}".${suffix}`
338
- );
339
- case "23505": // unique_violation
340
- return new Error(
341
- detail
342
- ? `Duplicate value: ${detail}${suffix}`
343
- : `Cannot save: a unique constraint${constraint ? ` (${constraint})` : ""} was violated in "${collectionSlug}".${suffix}`
344
- );
345
- case "23502": // not_null_violation
346
- return new Error(
347
- `Missing required field: "${column ?? "unknown"}" in "${tableRef}" cannot be empty.${suffix}`
348
- );
349
- case "23514": // check_violation
350
- return new Error(
351
- `Validation failed: a check constraint${constraint ? ` (${constraint})` : ""} was violated in "${collectionSlug}".${suffix}`
352
- );
353
- case "22P02": // invalid_text_representation (e.g. invalid UUID, wrong enum value)
354
- return new Error(
355
- `Invalid data format in "${collectionSlug}": ${pgMessage}${suffix}`
356
- );
357
- case "22001": // string_data_right_truncation (value too long)
358
- return new Error(
359
- `Value too long for column "${column ?? "unknown"}" in "${tableRef}": ${pgMessage}${suffix}`
360
- );
361
- case "22003": // numeric_value_out_of_range
362
- return new Error(
363
- `Numeric value out of range for column "${column ?? "unknown"}" in "${tableRef}": ${pgMessage}${suffix}`
364
- );
365
- case "42703": // undefined_column
366
- return new Error(
367
- `Unknown column in "${tableRef}": ${pgMessage}. Check if your schema is up to date (run migrations).${suffix}`
368
- );
369
- case "42P01": // undefined_table
370
- return new Error(
371
- `Table not found for "${collectionSlug}": ${pgMessage}. Check if your schema is up to date (run migrations).${suffix}`
372
- );
373
- default: {
374
- // Unhandled PG code — still surface the actual database message
375
- const parts = [`Database error in "${collectionSlug}" [${pgError.code}]: ${pgMessage}`];
376
- if (detail) parts.push(`Detail: ${detail}`);
377
- if (column) parts.push(`Column: ${column}`);
378
- if (dataType) parts.push(`Data type: ${dataType}`);
379
- if (constraint) parts.push(`Constraint: ${constraint}`);
380
- if (hint) parts.push(`Hint: ${hint}`);
381
- return new Error(parts.join(". "));
382
- }
383
- }
310
+ const { message } = pgErrorToFriendlyMessage(pgError, collectionSlug);
311
+ return new Error(message);
384
312
  }
385
313
 
386
314
  // No PG error found — try to extract a useful message from the
387
315
  // Drizzle wrapper instead of leaking the raw SQL query + params.
388
- const causeMessage = this.extractCauseMessage(error);
316
+ const causeMessage = extractCauseMessage(error);
389
317
  if (causeMessage) {
390
318
  return new Error(`Database error in "${collectionSlug}": ${causeMessage}`);
391
319
  }
392
320
 
393
- // Last resort: use the original error message but strip the SQL query
394
- if (error instanceof Error) {
395
- const cleaned = this.stripSqlFromMessage(error.message, collectionSlug);
396
- 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.`);
397
324
  }
398
325
  return new Error(`Database error in "${collectionSlug}": ${String(error)}`);
399
326
  }
400
-
401
- /**
402
- * Walk the error cause chain and return the deepest meaningful message.
403
- */
404
- private extractCauseMessage(error: unknown): string | null {
405
- if (!error || typeof error !== "object") return null;
406
- if (!(error instanceof Error)) return null;
407
-
408
- if (error.cause && typeof error.cause === "object") {
409
- const deeper = this.extractCauseMessage(error.cause);
410
- if (deeper) return deeper;
411
- // The cause itself has a message
412
- if (error.cause instanceof Error && error.cause.message) {
413
- return error.cause.message;
414
- }
415
- }
416
- return null;
417
- }
418
-
419
- /**
420
- * Strip the raw SQL query from a Drizzle "Failed query: ..." message,
421
- * keeping only the error description.
422
- */
423
- private stripSqlFromMessage(message: string, collectionSlug: string): string {
424
- // Drizzle format: "Failed query: <SQL>\nparams: <params>"
425
- if (message.startsWith("Failed query:")) {
426
- return `Failed to save entity in "${collectionSlug}". Check server logs for details.`;
427
- }
428
- return message;
429
- }
430
-
431
- /**
432
- * Extract the underlying PostgreSQL error from a Drizzle wrapper.
433
- * Drizzle wraps PG errors in a `cause` property.
434
- */
435
- private extractPgError(error: unknown): PostgresError | null {
436
- if (!error || typeof error !== "object") return null;
437
- if (!(error instanceof Error)) {
438
- // Check non-Error objects for a cause chain (Drizzle sometimes wraps oddly)
439
- if ("cause" in error && (error as Record<string, unknown>).cause && typeof (error as Record<string, unknown>).cause === "object") {
440
- return this.extractPgError((error as Record<string, unknown>).cause);
441
- }
442
- return null;
443
- }
444
-
445
- // Check if the error itself has a PG error code
446
- if ("code" in error && typeof (error as PostgresError).code === "string" && /^[0-9A-Z]{5}$/.test((error as PostgresError).code!)) {
447
- return error as PostgresError;
448
- }
449
-
450
- // Check the cause chain (Drizzle wraps PG errors)
451
- if (error.cause && typeof error.cause === "object") {
452
- return this.extractPgError(error.cause);
453
- }
454
-
455
- return null;
456
- }
457
327
  }