@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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
629
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
753
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1254
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1368
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
318
|
-
const pgError = this.extractPgError(error);
|
|
307
|
+
const pgError = extractPgError(error);
|
|
319
308
|
|
|
320
309
|
if (pgError) {
|
|
321
|
-
const
|
|
322
|
-
|
|
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 =
|
|
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:
|
|
394
|
-
if (error instanceof Error) {
|
|
395
|
-
|
|
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
|
}
|