@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.
- package/dist/{server-postgresql/src/PostgresAdapter.d.ts → PostgresAdapter.d.ts} +1 -1
- package/dist/{server-postgresql/src/PostgresBackendDriver.d.ts → PostgresBackendDriver.d.ts} +2 -2
- package/dist/{server-postgresql/src/PostgresBootstrapper.d.ts → PostgresBootstrapper.d.ts} +11 -1
- package/dist/{server-postgresql/src/collections → collections}/PostgresCollectionRegistry.d.ts +4 -0
- package/dist/index.es.js +10168 -11145
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +10735 -11429
- package/dist/index.umd.js.map +1 -1
- package/dist/{server-postgresql/src/services → services}/EntityPersistService.d.ts +0 -14
- package/dist/utils/pg-error-utils.d.ts +55 -0
- package/package.json +24 -21
- package/src/PostgresAdapter.ts +9 -10
- package/src/PostgresBackendDriver.ts +134 -121
- package/src/PostgresBootstrapper.ts +86 -13
- package/src/auth/ensure-tables.ts +28 -5
- package/src/auth/services.ts +28 -18
- package/src/cli.ts +99 -96
- package/src/collections/PostgresCollectionRegistry.ts +7 -0
- package/src/connection.ts +11 -6
- package/src/data-transformer.ts +16 -14
- package/src/databasePoolManager.ts +3 -2
- package/src/history/HistoryService.ts +3 -2
- package/src/history/ensure-history-table.ts +5 -4
- package/src/schema/auth-schema.ts +1 -2
- package/src/schema/doctor-cli.ts +2 -1
- package/src/schema/doctor.ts +40 -37
- package/src/schema/generate-drizzle-schema-logic.ts +56 -18
- package/src/schema/generate-drizzle-schema.ts +11 -11
- package/src/schema/introspect-db-inference.ts +25 -25
- package/src/schema/introspect-db-logic.ts +38 -38
- package/src/schema/introspect-db.ts +28 -27
- package/src/services/BranchService.ts +14 -0
- package/src/services/EntityFetchService.ts +28 -25
- package/src/services/EntityPersistService.ts +11 -141
- package/src/services/RelationService.ts +57 -37
- package/src/services/entity-helpers.ts +6 -2
- package/src/services/realtimeService.ts +45 -32
- package/src/utils/drizzle-conditions.ts +31 -15
- package/src/utils/pg-error-utils.ts +211 -0
- package/src/websocket.ts +15 -12
- package/test/auth-services.test.ts +36 -19
- package/test/batch-many-to-many-regression.test.ts +119 -39
- package/test/data-transformer-hardening.test.ts +67 -33
- package/test/data-transformer.test.ts +4 -2
- package/test/doctor.test.ts +10 -5
- package/test/drizzle-conditions.test.ts +59 -6
- package/test/generate-drizzle-schema.test.ts +65 -40
- package/test/introspect-db-generation.test.ts +179 -81
- package/test/introspect-db-utils.test.ts +92 -37
- package/test/mocks/chalk.cjs +7 -0
- package/test/pg-error-utils.test.ts +221 -0
- package/test/postgresDataDriver.test.ts +14 -5
- package/test/property-ordering.test.ts +126 -79
- package/test/realtimeService.test.ts +6 -2
- package/test/relation-pipeline-gaps.test.ts +84 -36
- package/test/relations.test.ts +247 -0
- package/test/unmapped-tables-safety.test.ts +14 -6
- package/test/websocket.test.ts +1 -1
- package/tsconfig.json +5 -0
- package/tsconfig.prod.json +3 -0
- package/vite.config.ts +5 -5
- package/dist/common/src/collections/CollectionRegistry.d.ts +0 -56
- package/dist/common/src/collections/default-collections.d.ts +0 -9
- package/dist/common/src/collections/index.d.ts +0 -2
- package/dist/common/src/data/buildRebaseData.d.ts +0 -14
- package/dist/common/src/data/query_builder.d.ts +0 -55
- package/dist/common/src/index.d.ts +0 -4
- package/dist/common/src/util/builders.d.ts +0 -57
- package/dist/common/src/util/callbacks.d.ts +0 -6
- package/dist/common/src/util/collections.d.ts +0 -11
- package/dist/common/src/util/common.d.ts +0 -2
- package/dist/common/src/util/conditions.d.ts +0 -26
- package/dist/common/src/util/entities.d.ts +0 -58
- package/dist/common/src/util/enums.d.ts +0 -3
- package/dist/common/src/util/index.d.ts +0 -16
- package/dist/common/src/util/navigation_from_path.d.ts +0 -34
- package/dist/common/src/util/navigation_utils.d.ts +0 -20
- package/dist/common/src/util/parent_references_from_path.d.ts +0 -6
- package/dist/common/src/util/paths.d.ts +0 -14
- package/dist/common/src/util/permissions.d.ts +0 -14
- package/dist/common/src/util/references.d.ts +0 -2
- package/dist/common/src/util/relations.d.ts +0 -22
- package/dist/common/src/util/resolutions.d.ts +0 -72
- package/dist/common/src/util/storage.d.ts +0 -24
- package/dist/types/src/controllers/analytics_controller.d.ts +0 -7
- package/dist/types/src/controllers/auth.d.ts +0 -104
- package/dist/types/src/controllers/client.d.ts +0 -168
- package/dist/types/src/controllers/collection_registry.d.ts +0 -46
- package/dist/types/src/controllers/customization_controller.d.ts +0 -60
- package/dist/types/src/controllers/data.d.ts +0 -207
- package/dist/types/src/controllers/data_driver.d.ts +0 -218
- package/dist/types/src/controllers/database_admin.d.ts +0 -11
- package/dist/types/src/controllers/dialogs_controller.d.ts +0 -36
- package/dist/types/src/controllers/effective_role.d.ts +0 -4
- package/dist/types/src/controllers/email.d.ts +0 -36
- package/dist/types/src/controllers/index.d.ts +0 -18
- package/dist/types/src/controllers/local_config_persistence.d.ts +0 -20
- package/dist/types/src/controllers/navigation.d.ts +0 -225
- package/dist/types/src/controllers/registry.d.ts +0 -63
- package/dist/types/src/controllers/side_dialogs_controller.d.ts +0 -67
- package/dist/types/src/controllers/side_entity_controller.d.ts +0 -97
- package/dist/types/src/controllers/snackbar.d.ts +0 -24
- package/dist/types/src/controllers/storage.d.ts +0 -171
- package/dist/types/src/index.d.ts +0 -4
- package/dist/types/src/rebase_context.d.ts +0 -122
- package/dist/types/src/types/auth_adapter.d.ts +0 -301
- package/dist/types/src/types/backend.d.ts +0 -571
- package/dist/types/src/types/backend_hooks.d.ts +0 -172
- package/dist/types/src/types/builders.d.ts +0 -15
- package/dist/types/src/types/chips.d.ts +0 -5
- package/dist/types/src/types/collections.d.ts +0 -961
- package/dist/types/src/types/component_ref.d.ts +0 -47
- package/dist/types/src/types/cron.d.ts +0 -102
- package/dist/types/src/types/data_source.d.ts +0 -64
- package/dist/types/src/types/database_adapter.d.ts +0 -94
- package/dist/types/src/types/entities.d.ts +0 -145
- package/dist/types/src/types/entity_actions.d.ts +0 -104
- package/dist/types/src/types/entity_callbacks.d.ts +0 -173
- package/dist/types/src/types/entity_link_builder.d.ts +0 -7
- package/dist/types/src/types/entity_overrides.d.ts +0 -10
- package/dist/types/src/types/entity_views.d.ts +0 -87
- package/dist/types/src/types/export_import.d.ts +0 -21
- package/dist/types/src/types/formex.d.ts +0 -40
- package/dist/types/src/types/index.d.ts +0 -28
- package/dist/types/src/types/locales.d.ts +0 -4
- package/dist/types/src/types/modify_collections.d.ts +0 -5
- package/dist/types/src/types/plugins.d.ts +0 -282
- package/dist/types/src/types/properties.d.ts +0 -1173
- package/dist/types/src/types/property_config.d.ts +0 -74
- package/dist/types/src/types/relations.d.ts +0 -336
- package/dist/types/src/types/slots.d.ts +0 -262
- package/dist/types/src/types/translations.d.ts +0 -900
- package/dist/types/src/types/user_management_delegate.d.ts +0 -86
- package/dist/types/src/types/websockets.d.ts +0 -78
- package/dist/types/src/users/index.d.ts +0 -1
- package/dist/types/src/users/user.d.ts +0 -50
- /package/dist/{server-postgresql/src/auth → auth}/ensure-tables.d.ts +0 -0
- /package/dist/{server-postgresql/src/auth → auth}/services.d.ts +0 -0
- /package/dist/{server-postgresql/src/cli.d.ts → cli.d.ts} +0 -0
- /package/dist/{server-postgresql/src/connection.d.ts → connection.d.ts} +0 -0
- /package/dist/{server-postgresql/src/data-transformer.d.ts → data-transformer.d.ts} +0 -0
- /package/dist/{server-postgresql/src/databasePoolManager.d.ts → databasePoolManager.d.ts} +0 -0
- /package/dist/{server-postgresql/src/history → history}/HistoryService.d.ts +0 -0
- /package/dist/{server-postgresql/src/history → history}/ensure-history-table.d.ts +0 -0
- /package/dist/{server-postgresql/src/index.d.ts → index.d.ts} +0 -0
- /package/dist/{server-postgresql/src/interfaces.d.ts → interfaces.d.ts} +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/auth-schema.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/doctor-cli.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/doctor.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/generate-drizzle-schema-logic.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/generate-drizzle-schema.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/introspect-db-inference.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/introspect-db-logic.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/introspect-db.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/test-schema.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/BranchService.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/EntityFetchService.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/RelationService.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/entity-helpers.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/entityService.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/index.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/realtimeService.d.ts +0 -0
- /package/dist/{server-postgresql/src/types.d.ts → types.d.ts} +0 -0
- /package/dist/{server-postgresql/src/utils → utils}/drizzle-conditions.d.ts +0 -0
- /package/dist/{server-postgresql/src/websocket.d.ts → websocket.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",
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
{ enum_name: "
|
|
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",
|
|
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",
|
|
248
|
-
|
|
249
|
-
|
|
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",
|
|
253
|
-
|
|
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",
|
|
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,
|
|
272
|
-
|
|
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",
|
|
283
|
-
|
|
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",
|
|
303
|
-
|
|
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")
|
|
363
|
+
mkCol("enrollments", "grade") // extra column
|
|
318
364
|
],
|
|
319
365
|
pks: [],
|
|
320
366
|
fks: [
|
|
321
|
-
{ table_name: "enrollments",
|
|
322
|
-
|
|
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",
|
|
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(
|
|
360
|
-
expect(result).toContain(
|
|
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(
|
|
375
|
-
expect(result).toContain(
|
|
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,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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
790
|
+
entity: { id: "e1",
|
|
791
|
+
path: "test_coll",
|
|
792
|
+
values: {} } as any,
|
|
784
793
|
collection: mockCollectionWithCallback
|
|
785
794
|
});
|
|
786
795
|
|