@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
@@ -2,7 +2,7 @@ import {
2
2
  singularize, humanize, toCollectionVarName, getIconForTable,
3
3
  mapPgType, buildEnumMap, buildTablesMap, identifyJoinTables,
4
4
  generateIndexContent, mergeIndexContent, safeHostFromUrl,
5
- EnumValue, TableRow, TableColumn, PrimaryKeyRow, ForeignKeyRow,
5
+ EnumValue, TableRow, TableColumn, PrimaryKeyRow, ForeignKeyRow
6
6
  } from "../src/schema/introspect-db-logic";
7
7
 
8
8
  // ═══════════════════════════════════════════════════════════════════════
@@ -148,7 +148,7 @@ describe("getIconForTable", () => {
148
148
  ["notifications", "Mail"], ["messages", "Mail"], ["emails", "Mail"],
149
149
  ["audit_log", "Activity"], ["events", "Activity"],
150
150
  ["subscriptions", "CreditCard"], ["plans", "CreditCard"], ["billing", "CreditCard"],
151
- ["comments", "MessageCircle"], ["reviews", "MessageCircle"], ["feedback", "MessageCircle"],
151
+ ["comments", "MessageCircle"], ["reviews", "MessageCircle"], ["feedback", "MessageCircle"]
152
152
  ];
153
153
 
154
154
  it.each(cases)("returns %s -> %s", (table, icon) => {
@@ -219,10 +219,18 @@ describe("mapPgType", () => {
219
219
  describe("buildEnumMap", () => {
220
220
  it("groups enum values by name in order", () => {
221
221
  const vals: EnumValue[] = [
222
- { enum_name: "status", enum_value: "active", sort_order: 1 },
223
- { enum_name: "status", enum_value: "inactive", sort_order: 2 },
224
- { enum_name: "role", enum_value: "admin", sort_order: 1 },
225
- { enum_name: "role", enum_value: "user", sort_order: 2 },
222
+ { enum_name: "status",
223
+ enum_value: "active",
224
+ sort_order: 1 },
225
+ { enum_name: "status",
226
+ enum_value: "inactive",
227
+ sort_order: 2 },
228
+ { enum_name: "role",
229
+ enum_value: "admin",
230
+ sort_order: 1 },
231
+ { enum_name: "role",
232
+ enum_value: "user",
233
+ sort_order: 2 }
226
234
  ];
227
235
  const map = buildEnumMap(vals);
228
236
  expect(map.get("status")).toEqual(["active", "inactive"]);
@@ -232,7 +240,9 @@ describe("buildEnumMap", () => {
232
240
  expect(buildEnumMap([]).size).toBe(0);
233
241
  });
234
242
  it("handles single-value enums", () => {
235
- const map = buildEnumMap([{ enum_name: "flag", enum_value: "yes", sort_order: 1 }]);
243
+ const map = buildEnumMap([{ enum_name: "flag",
244
+ enum_value: "yes",
245
+ sort_order: 1 }]);
236
246
  expect(map.get("flag")).toEqual(["yes"]);
237
247
  });
238
248
  });
@@ -244,16 +254,36 @@ describe("buildTablesMap", () => {
244
254
  it("groups columns, pks and fks by table", () => {
245
255
  const tables: TableRow[] = [{ table_name: "users" }, { table_name: "posts" }];
246
256
  const cols: TableColumn[] = [
247
- { table_name: "users", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO", column_default: null },
248
- { table_name: "posts", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO", column_default: null },
249
- { table_name: "posts", column_name: "user_id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO", column_default: null },
257
+ { table_name: "users",
258
+ column_name: "id",
259
+ data_type: "uuid",
260
+ udt_name: "uuid",
261
+ is_nullable: "NO",
262
+ column_default: null },
263
+ { table_name: "posts",
264
+ column_name: "id",
265
+ data_type: "uuid",
266
+ udt_name: "uuid",
267
+ is_nullable: "NO",
268
+ column_default: null },
269
+ { table_name: "posts",
270
+ column_name: "user_id",
271
+ data_type: "uuid",
272
+ udt_name: "uuid",
273
+ is_nullable: "NO",
274
+ column_default: null }
250
275
  ];
251
276
  const pks: PrimaryKeyRow[] = [
252
- { table_name: "users", column_name: "id" },
253
- { table_name: "posts", column_name: "id" },
277
+ { table_name: "users",
278
+ column_name: "id" },
279
+ { table_name: "posts",
280
+ column_name: "id" }
254
281
  ];
255
282
  const fks: ForeignKeyRow[] = [
256
- { table_name: "posts", column_name: "user_id", foreign_table_name: "users", foreign_column_name: "id" },
283
+ { table_name: "posts",
284
+ column_name: "user_id",
285
+ foreign_table_name: "users",
286
+ foreign_column_name: "id" }
257
287
  ];
258
288
  const map = buildTablesMap(tables, cols, pks, fks);
259
289
  expect(map.size).toBe(2);
@@ -268,8 +298,12 @@ describe("buildTablesMap", () => {
268
298
  // ═══════════════════════════════════════════════════════════════════════
269
299
  describe("identifyJoinTables", () => {
270
300
  const mkCol = (table: string, col: string): TableColumn => ({
271
- table_name: table, column_name: col, data_type: "uuid",
272
- udt_name: "uuid", is_nullable: "NO", column_default: null,
301
+ table_name: table,
302
+ column_name: col,
303
+ data_type: "uuid",
304
+ udt_name: "uuid",
305
+ is_nullable: "NO",
306
+ column_default: null
273
307
  });
274
308
 
275
309
  it("detects a pure junction table with exactly 2 FKs", () => {
@@ -279,10 +313,16 @@ describe("identifyJoinTables", () => {
279
313
  columns: [mkCol("posts_to_tags", "post_id"), mkCol("posts_to_tags", "tag_id")],
280
314
  pks: [],
281
315
  fks: [
282
- { table_name: "posts_to_tags", column_name: "post_id", foreign_table_name: "posts", foreign_column_name: "id" },
283
- { table_name: "posts_to_tags", column_name: "tag_id", foreign_table_name: "tags", foreign_column_name: "id" },
284
- ],
285
- }],
316
+ { table_name: "posts_to_tags",
317
+ column_name: "post_id",
318
+ foreign_table_name: "posts",
319
+ foreign_column_name: "id" },
320
+ { table_name: "posts_to_tags",
321
+ column_name: "tag_id",
322
+ foreign_table_name: "tags",
323
+ foreign_column_name: "id" }
324
+ ]
325
+ }]
286
326
  ]);
287
327
  expect(identifyJoinTables(tablesMap)).toEqual(new Set(["posts_to_tags"]));
288
328
  });
@@ -295,14 +335,20 @@ describe("identifyJoinTables", () => {
295
335
  mkCol("posts_tags", "id"),
296
336
  mkCol("posts_tags", "post_id"),
297
337
  mkCol("posts_tags", "tag_id"),
298
- mkCol("posts_tags", "created_at"),
338
+ mkCol("posts_tags", "created_at")
299
339
  ],
300
340
  pks: ["id"],
301
341
  fks: [
302
- { table_name: "posts_tags", column_name: "post_id", foreign_table_name: "posts", foreign_column_name: "id" },
303
- { table_name: "posts_tags", column_name: "tag_id", foreign_table_name: "tags", foreign_column_name: "id" },
304
- ],
305
- }],
342
+ { table_name: "posts_tags",
343
+ column_name: "post_id",
344
+ foreign_table_name: "posts",
345
+ foreign_column_name: "id" },
346
+ { table_name: "posts_tags",
347
+ column_name: "tag_id",
348
+ foreign_table_name: "tags",
349
+ foreign_column_name: "id" }
350
+ ]
351
+ }]
306
352
  ]);
307
353
  expect(identifyJoinTables(tablesMap).has("posts_tags")).toBe(true);
308
354
  });
@@ -314,14 +360,20 @@ describe("identifyJoinTables", () => {
314
360
  columns: [
315
361
  mkCol("enrollments", "student_id"),
316
362
  mkCol("enrollments", "course_id"),
317
- mkCol("enrollments", "grade"), // extra column
363
+ mkCol("enrollments", "grade") // extra column
318
364
  ],
319
365
  pks: [],
320
366
  fks: [
321
- { table_name: "enrollments", column_name: "student_id", foreign_table_name: "students", foreign_column_name: "id" },
322
- { table_name: "enrollments", column_name: "course_id", foreign_table_name: "courses", foreign_column_name: "id" },
323
- ],
324
- }],
367
+ { table_name: "enrollments",
368
+ column_name: "student_id",
369
+ foreign_table_name: "students",
370
+ foreign_column_name: "id" },
371
+ { table_name: "enrollments",
372
+ column_name: "course_id",
373
+ foreign_table_name: "courses",
374
+ foreign_column_name: "id" }
375
+ ]
376
+ }]
325
377
  ]);
326
378
  expect(identifyJoinTables(tablesMap).size).toBe(0);
327
379
  });
@@ -333,9 +385,12 @@ describe("identifyJoinTables", () => {
333
385
  columns: [mkCol("posts", "id"), mkCol("posts", "user_id")],
334
386
  pks: ["id"],
335
387
  fks: [
336
- { table_name: "posts", column_name: "user_id", foreign_table_name: "users", foreign_column_name: "id" },
337
- ],
338
- }],
388
+ { table_name: "posts",
389
+ column_name: "user_id",
390
+ foreign_table_name: "users",
391
+ foreign_column_name: "id" }
392
+ ]
393
+ }]
339
394
  ]);
340
395
  expect(identifyJoinTables(tablesMap).size).toBe(0);
341
396
  });
@@ -356,9 +411,9 @@ describe("generateIndexContent", () => {
356
411
  it("generates import statements and collections array", () => {
357
412
  const result = generateIndexContent(["users"]);
358
413
  expect(result).toContain('import usersCollection from "./users";');
359
- expect(result).toContain('export const collections = [');
360
- expect(result).toContain(' usersCollection,');
361
- expect(result).toContain('];');
414
+ expect(result).toContain("export const collections = [");
415
+ expect(result).toContain(" usersCollection,");
416
+ expect(result).toContain("];");
362
417
  });
363
418
  });
364
419
 
@@ -371,8 +426,8 @@ describe("mergeIndexContent", () => {
371
426
  const result = mergeIndexContent(existing, ["users", "posts"]);
372
427
  expect(result.match(/import usersCollection from ".\/users";/g)!.length).toBe(1);
373
428
  expect(result).toContain('import postsCollection from "./posts";');
374
- expect(result).toContain('usersCollection,');
375
- expect(result).toContain('postsCollection,');
429
+ expect(result).toContain("usersCollection,");
430
+ expect(result).toContain("postsCollection,");
376
431
  });
377
432
 
378
433
  it("returns existing content trimmed + newline when no new files", () => {
@@ -0,0 +1,7 @@
1
+ const mockChalk = new Proxy((x) => x, {
2
+ get: (target, prop) => {
3
+ if (prop === "default") return mockChalk;
4
+ return mockChalk;
5
+ }
6
+ });
7
+ module.exports = mockChalk;
@@ -0,0 +1,221 @@
1
+ import { describe, it, expect } from "@jest/globals";
2
+ import { extractPgError, extractCauseMessage, pgErrorToFriendlyMessage, sanitizeErrorForClient } from "../src/utils/pg-error-utils";
3
+
4
+ // Suppress logger output during tests
5
+ jest.mock("@rebasepro/server-core", () => ({
6
+ logger: {
7
+ error: jest.fn(),
8
+ warn: jest.fn(),
9
+ info: jest.fn(),
10
+ debug: jest.fn()
11
+ }
12
+ }));
13
+
14
+ describe("pg-error-utils", () => {
15
+
16
+ describe("extractPgError", () => {
17
+ it("returns null for non-object values", () => {
18
+ expect(extractPgError(null)).toBeNull();
19
+ expect(extractPgError(undefined)).toBeNull();
20
+ expect(extractPgError("string error")).toBeNull();
21
+ expect(extractPgError(42)).toBeNull();
22
+ });
23
+
24
+ it("returns null for plain Error without PG code", () => {
25
+ expect(extractPgError(new Error("some error"))).toBeNull();
26
+ });
27
+
28
+ it("extracts PG error directly when error has a 5-char alphanumeric code", () => {
29
+ const pgError = Object.assign(new Error("relation \"clients\" does not exist"), {
30
+ code: "42P01",
31
+ table: "clients"
32
+ });
33
+ const result = extractPgError(pgError);
34
+ expect(result).toBe(pgError);
35
+ expect(result?.code).toBe("42P01");
36
+ });
37
+
38
+ it("extracts PG error from Drizzle cause chain", () => {
39
+ const pgError = Object.assign(new Error("relation \"clients\" does not exist"), {
40
+ code: "42P01",
41
+ table: "clients"
42
+ });
43
+ const drizzleError = new Error("Failed query: select ...");
44
+ (drizzleError as any).cause = pgError;
45
+
46
+ const result = extractPgError(drizzleError);
47
+ expect(result).toBe(pgError);
48
+ expect(result?.code).toBe("42P01");
49
+ });
50
+
51
+ it("extracts PG error from deeply nested cause chain", () => {
52
+ const pgError = Object.assign(new Error("column \"foo\" does not exist"), {
53
+ code: "42703",
54
+ column: "foo"
55
+ });
56
+ const mid = new Error("mid-level wrapper");
57
+ (mid as any).cause = pgError;
58
+ const outer = new Error("Failed query: select ...");
59
+ (outer as any).cause = mid;
60
+
61
+ const result = extractPgError(outer);
62
+ expect(result).toBe(pgError);
63
+ });
64
+
65
+ it("ignores non-PG error codes (not 5-char alphanumeric)", () => {
66
+ const error = Object.assign(new Error("some error"), { code: "ERR_SOMETHING" });
67
+ expect(extractPgError(error)).toBeNull();
68
+ });
69
+
70
+ it("handles non-Error objects with cause chain", () => {
71
+ const pgError = Object.assign(new Error("relation does not exist"), {
72
+ code: "42P01"
73
+ });
74
+ const wrapper = { cause: pgError };
75
+ expect(extractPgError(wrapper)).toBe(pgError);
76
+ });
77
+ });
78
+
79
+ describe("extractCauseMessage", () => {
80
+ it("returns null for non-Error values", () => {
81
+ expect(extractCauseMessage(null)).toBeNull();
82
+ expect(extractCauseMessage("string")).toBeNull();
83
+ expect(extractCauseMessage({})).toBeNull();
84
+ });
85
+
86
+ it("returns null for Error without cause", () => {
87
+ expect(extractCauseMessage(new Error("top level"))).toBeNull();
88
+ });
89
+
90
+ it("extracts cause message from single-level cause", () => {
91
+ const inner = new Error("inner message");
92
+ const outer = new Error("outer");
93
+ (outer as any).cause = inner;
94
+ expect(extractCauseMessage(outer)).toBe("inner message");
95
+ });
96
+
97
+ it("extracts deepest cause message", () => {
98
+ const deepest = new Error("deepest");
99
+ const mid = new Error("mid");
100
+ (mid as any).cause = deepest;
101
+ const outer = new Error("outer");
102
+ (outer as any).cause = mid;
103
+ expect(extractCauseMessage(outer)).toBe("deepest");
104
+ });
105
+ });
106
+
107
+ describe("pgErrorToFriendlyMessage", () => {
108
+ it("maps 42P01 (undefined_table) to a friendly message", () => {
109
+ const pgError = Object.assign(new Error("relation \"clients\" does not exist"), {
110
+ code: "42P01",
111
+ table: "clients"
112
+ });
113
+ const result = pgErrorToFriendlyMessage(pgError as any, "clients");
114
+ expect(result.code).toBe("42P01");
115
+ expect(result.message).toContain("Table not found");
116
+ expect(result.message).toContain("run migrations");
117
+ });
118
+
119
+ it("maps 42703 (undefined_column) to a friendly message", () => {
120
+ const pgError = Object.assign(new Error("column \"foo\" of relation \"clients\" does not exist"), {
121
+ code: "42703",
122
+ column: "foo",
123
+ table: "clients"
124
+ });
125
+ const result = pgErrorToFriendlyMessage(pgError as any, "clients");
126
+ expect(result.code).toBe("42703");
127
+ expect(result.message).toContain("Unknown column");
128
+ expect(result.message).toContain("run migrations");
129
+ });
130
+
131
+ it("maps 23505 (unique_violation) with detail", () => {
132
+ const pgError = Object.assign(new Error("duplicate key value"), {
133
+ code: "23505",
134
+ detail: "Key (email)=(test@test.com) already exists.",
135
+ constraint: "clients_email_unique"
136
+ });
137
+ const result = pgErrorToFriendlyMessage(pgError as any, "clients");
138
+ expect(result.code).toBe("23505");
139
+ expect(result.message).toContain("Duplicate value");
140
+ expect(result.message).toContain("already exists");
141
+ });
142
+
143
+ it("maps 42501 (insufficient_privilege) to permission denied", () => {
144
+ const pgError = Object.assign(new Error("permission denied for table clients"), {
145
+ code: "42501",
146
+ table: "clients"
147
+ });
148
+ const result = pgErrorToFriendlyMessage(pgError as any, "clients");
149
+ expect(result.code).toBe("42501");
150
+ expect(result.message).toContain("Permission denied");
151
+ expect(result.message).toContain("RLS policies");
152
+ });
153
+
154
+ it("includes hint when present", () => {
155
+ const pgError = Object.assign(new Error("relation does not exist"), {
156
+ code: "42P01",
157
+ hint: "Perhaps you meant \"client\"?"
158
+ });
159
+ const result = pgErrorToFriendlyMessage(pgError as any, "clients");
160
+ expect(result.message).toContain("Hint: Perhaps you meant");
161
+ });
162
+
163
+ it("handles unknown PG codes with full diagnostic info", () => {
164
+ const pgError = Object.assign(new Error("something unusual"), {
165
+ code: "XX000",
166
+ detail: "internal detail",
167
+ column: "col",
168
+ constraint: "some_constraint"
169
+ });
170
+ const result = pgErrorToFriendlyMessage(pgError as any, "my_collection");
171
+ expect(result.code).toBe("XX000");
172
+ expect(result.message).toContain("[XX000]");
173
+ expect(result.message).toContain("something unusual");
174
+ expect(result.message).toContain("internal detail");
175
+ });
176
+ });
177
+
178
+ describe("sanitizeErrorForClient", () => {
179
+ it("sanitizes a Drizzle-wrapped PG error", () => {
180
+ const pgError = Object.assign(new Error("relation \"clients\" does not exist"), {
181
+ code: "42P01"
182
+ });
183
+ const drizzleError = new Error(
184
+ 'Failed query: select "id", "name" from "clients" where "clients"."id" = $1 limit $2\nparams: some-uuid,1'
185
+ );
186
+ (drizzleError as any).cause = pgError;
187
+
188
+ const result = sanitizeErrorForClient(drizzleError, "clients");
189
+ expect(result.code).toBe("42P01");
190
+ expect(result.message).toContain("Table not found");
191
+ // Must NOT contain the raw SQL
192
+ expect(result.message).not.toContain("select");
193
+ expect(result.message).not.toContain("params:");
194
+ });
195
+
196
+ it("returns generic message when no PG error is found", () => {
197
+ const error = new Error("something went wrong");
198
+ const result = sanitizeErrorForClient(error, "clients");
199
+ expect(result.message).toContain("clients");
200
+ expect(result.code).toBeUndefined();
201
+ });
202
+
203
+ it("uses cause message when available and no PG error", () => {
204
+ const inner = new Error("connection refused");
205
+ const outer = new Error("outer wrapper");
206
+ (outer as any).cause = inner;
207
+ const result = sanitizeErrorForClient(outer, "clients");
208
+ expect(result.message).toContain("connection refused");
209
+ });
210
+
211
+ it("never leaks SQL in the returned message", () => {
212
+ const error = new Error(
213
+ 'Failed query: select "id", "name", "email" from "clients" where "clients"."id" = $1'
214
+ );
215
+ const result = sanitizeErrorForClient(error, "clients");
216
+ expect(result.message).not.toContain("select");
217
+ expect(result.message).not.toContain("Failed query");
218
+ expect(result.message).toContain("Check server logs");
219
+ });
220
+ });
221
+ });
@@ -36,7 +36,8 @@ describe("PostgresBackendDriver", () => {
36
36
  beforeEach(() => {
37
37
  jest.clearAllMocks();
38
38
  const mockRegistry = {
39
- getCollectionByPath: jest.fn().mockReturnValue({ slug: "test_coll", properties: {} }),
39
+ getCollectionByPath: jest.fn().mockReturnValue({ slug: "test_coll",
40
+ properties: {} }),
40
41
  getCollections: jest.fn().mockReturnValue([]),
41
42
  getTable: jest.fn().mockReturnValue({})
42
43
  } as any;
@@ -683,7 +684,9 @@ status: "new" });
683
684
  } as any;
684
685
 
685
686
  jest.spyOn(delegate.entityService, "fetchCollection").mockResolvedValueOnce([
686
- { id: "e1", path: "test_coll", values: {} } as any
687
+ { id: "e1",
688
+ path: "test_coll",
689
+ values: {} } as any
687
690
  ]);
688
691
 
689
692
  await delegate.fetchCollection({
@@ -712,7 +715,9 @@ status: "new" });
712
715
  } as any;
713
716
 
714
717
  jest.spyOn(delegate.entityService, "fetchEntity").mockResolvedValueOnce(
715
- { id: "e1", path: "test_coll", values: {} } as any
718
+ { id: "e1",
719
+ path: "test_coll",
720
+ values: {} } as any
716
721
  );
717
722
 
718
723
  await delegate.fetchEntity({
@@ -744,7 +749,9 @@ status: "new" });
744
749
 
745
750
  jest.spyOn(delegate.entityService, "fetchEntity").mockResolvedValue(undefined);
746
751
  jest.spyOn(delegate.entityService, "saveEntity").mockResolvedValueOnce(
747
- { id: "e1", path: "test_coll", values: { name: "test" } } as any
752
+ { id: "e1",
753
+ path: "test_coll",
754
+ values: { name: "test" } } as any
748
755
  );
749
756
 
750
757
  await delegate.saveEntity({
@@ -780,7 +787,9 @@ status: "new" });
780
787
  jest.spyOn(delegate.entityService, "deleteEntity").mockResolvedValueOnce();
781
788
 
782
789
  await delegate.deleteEntity({
783
- entity: { id: "e1", path: "test_coll", values: {} } as any,
790
+ entity: { id: "e1",
791
+ path: "test_coll",
792
+ values: {} } as any,
784
793
  collection: mockCollectionWithCallback
785
794
  });
786
795