@rebasepro/server-postgres 0.0.1-canary.4829d6e
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/LICENSE +22 -0
- package/README.md +86 -0
- package/dist/PostgresAdapter.d.ts +6 -0
- package/dist/PostgresBackendDriver.d.ts +150 -0
- package/dist/PostgresBootstrapper.d.ts +51 -0
- package/dist/auth/ensure-tables.d.ts +10 -0
- package/dist/auth/services.d.ts +250 -0
- package/dist/backup/backup-cli.d.ts +3 -0
- package/dist/backup/backup-cron.d.ts +53 -0
- package/dist/backup/backup-service.d.ts +85 -0
- package/dist/backup/index.d.ts +12 -0
- package/dist/backup/pg-tools.d.ts +110 -0
- package/dist/backup/retention.d.ts +35 -0
- package/dist/chunk-DSJWtz9O.js +40 -0
- package/dist/cli-errors.d.ts +42 -0
- package/dist/cli-helpers.d.ts +7 -0
- package/dist/cli.d.ts +1 -0
- package/dist/collections/PostgresCollectionRegistry.d.ts +47 -0
- package/dist/connection.d.ts +65 -0
- package/dist/data-transformer.d.ts +55 -0
- package/dist/databasePoolManager.d.ts +20 -0
- package/dist/history/HistoryService.d.ts +71 -0
- package/dist/history/ensure-history-table.d.ts +7 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.es.js +23535 -0
- package/dist/index.es.js.map +1 -0
- package/dist/interfaces.d.ts +18 -0
- package/dist/schema/auth-bootstrap-sql.d.ts +24 -0
- package/dist/schema/auth-default-policies.d.ts +10 -0
- package/dist/schema/auth-schema.d.ts +2376 -0
- package/dist/schema/doctor-cli.d.ts +2 -0
- package/dist/schema/doctor.d.ts +58 -0
- package/dist/schema/dynamic-tables.d.ts +31 -0
- package/dist/schema/generate-drizzle-schema-logic.d.ts +2 -0
- package/dist/schema/generate-drizzle-schema.d.ts +1 -0
- package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -0
- package/dist/schema/generate-postgres-ddl.d.ts +1 -0
- package/dist/schema/introspect-db-inference.d.ts +5 -0
- package/dist/schema/introspect-db-logic.d.ts +118 -0
- package/dist/schema/introspect-db.d.ts +1 -0
- package/dist/schema/introspect-runtime.d.ts +57 -0
- package/dist/schema/test-schema.d.ts +24 -0
- package/dist/security/policy-drift.d.ts +57 -0
- package/dist/security/rls-enforcement.d.ts +122 -0
- package/dist/services/BranchService.d.ts +47 -0
- package/dist/services/FetchService.d.ts +214 -0
- package/dist/services/PersistService.d.ts +39 -0
- package/dist/services/RelationService.d.ts +109 -0
- package/dist/services/cdc/CdcListener.d.ts +54 -0
- package/dist/services/cdc/trigger-cdc.d.ts +64 -0
- package/dist/services/collection-helpers.d.ts +38 -0
- package/dist/services/dataService.d.ts +110 -0
- package/dist/services/index.d.ts +4 -0
- package/dist/services/realtimeService.d.ts +298 -0
- package/dist/src-Eh-CZosp.js +595 -0
- package/dist/src-Eh-CZosp.js.map +1 -0
- package/dist/types.d.ts +3 -0
- package/dist/utils/drizzle-conditions.d.ts +138 -0
- package/dist/utils/pg-array-null-patch.d.ts +16 -0
- package/dist/utils/pg-error-utils.d.ts +65 -0
- package/dist/utils/table-classification.d.ts +8 -0
- package/dist/websocket.d.ts +18 -0
- package/package.json +113 -0
- package/src/PostgresAdapter.ts +58 -0
- package/src/PostgresBackendDriver.ts +1387 -0
- package/src/PostgresBootstrapper.ts +581 -0
- package/src/auth/ensure-tables.ts +367 -0
- package/src/auth/services.ts +1321 -0
- package/src/backup/backup-cli.ts +383 -0
- package/src/backup/backup-cron.ts +189 -0
- package/src/backup/backup-service.ts +299 -0
- package/src/backup/index.ts +12 -0
- package/src/backup/pg-tools.ts +231 -0
- package/src/backup/retention.ts +75 -0
- package/src/cli-errors.ts +265 -0
- package/src/cli-helpers.ts +196 -0
- package/src/cli.ts +786 -0
- package/src/collections/PostgresCollectionRegistry.ts +103 -0
- package/src/connection.ts +166 -0
- package/src/data-transformer.ts +733 -0
- package/src/databasePoolManager.ts +87 -0
- package/src/history/HistoryService.ts +272 -0
- package/src/history/ensure-history-table.ts +46 -0
- package/src/index.ts +15 -0
- package/src/interfaces.ts +60 -0
- package/src/schema/auth-bootstrap-sql.ts +41 -0
- package/src/schema/auth-default-policies.ts +125 -0
- package/src/schema/auth-schema.ts +233 -0
- package/src/schema/doctor-cli.ts +113 -0
- package/src/schema/doctor.ts +733 -0
- package/src/schema/dynamic-tables.test.ts +302 -0
- package/src/schema/dynamic-tables.ts +293 -0
- package/src/schema/generate-drizzle-schema-logic.ts +850 -0
- package/src/schema/generate-drizzle-schema.ts +131 -0
- package/src/schema/generate-postgres-ddl-logic.ts +490 -0
- package/src/schema/generate-postgres-ddl.ts +92 -0
- package/src/schema/introspect-db-inference.ts +238 -0
- package/src/schema/introspect-db-logic.ts +910 -0
- package/src/schema/introspect-db.ts +266 -0
- package/src/schema/introspect-runtime.test.ts +212 -0
- package/src/schema/introspect-runtime.ts +293 -0
- package/src/schema/test-schema.ts +11 -0
- package/src/security/policy-drift.test.ts +122 -0
- package/src/security/policy-drift.ts +159 -0
- package/src/security/rls-enforcement.ts +295 -0
- package/src/services/BranchService.ts +251 -0
- package/src/services/FetchService.ts +1661 -0
- package/src/services/PersistService.ts +329 -0
- package/src/services/RelationService.ts +1306 -0
- package/src/services/cdc/CdcListener.ts +167 -0
- package/src/services/cdc/trigger-cdc.ts +169 -0
- package/src/services/collection-helpers.ts +151 -0
- package/src/services/dataService.ts +246 -0
- package/src/services/index.ts +13 -0
- package/src/services/realtimeService.ts +1502 -0
- package/src/types.ts +4 -0
- package/src/utils/drizzle-conditions.ts +1162 -0
- package/src/utils/pg-array-null-patch.ts +42 -0
- package/src/utils/pg-error-utils.ts +227 -0
- package/src/utils/table-classification.ts +16 -0
- package/src/websocket.ts +640 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { eq, and } from "drizzle-orm";
|
|
2
|
+
import { AnyPgColumn } from "drizzle-orm/pg-core";
|
|
3
|
+
// import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
4
|
+
import { CollectionConfig, Properties, Relation } from "@rebasepro/types";
|
|
5
|
+
import { getTableName, resolveCollectionRelations, findRelation } from "@rebasepro/common";
|
|
6
|
+
import { DrizzleConditionBuilder } from "../utils/drizzle-conditions";
|
|
7
|
+
import {
|
|
8
|
+
getCollectionByPath,
|
|
9
|
+
getTableForCollection,
|
|
10
|
+
getPrimaryKeys,
|
|
11
|
+
parseIdValues,
|
|
12
|
+
buildCompositeId
|
|
13
|
+
} from "./collection-helpers";
|
|
14
|
+
import { sanitizeAndConvertDates, serializeDataToServer } from "../data-transformer";
|
|
15
|
+
import { RelationService } from "./RelationService";
|
|
16
|
+
import { FetchService } from "./FetchService";
|
|
17
|
+
import { DrizzleClient } from "../interfaces";
|
|
18
|
+
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
|
|
19
|
+
import { logger } from "@rebasepro/server";
|
|
20
|
+
import { extractPgError, extractCauseMessage, pgErrorToFriendlyMessage } from "../utils/pg-error-utils";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Service for handling all row write operations.
|
|
24
|
+
* Handles saving, deleting, and updating rows.
|
|
25
|
+
*/
|
|
26
|
+
export class PersistService {
|
|
27
|
+
private relationService: RelationService;
|
|
28
|
+
private fetchService: FetchService;
|
|
29
|
+
|
|
30
|
+
constructor(private db: DrizzleClient, private registry: PostgresCollectionRegistry) {
|
|
31
|
+
this.relationService = new RelationService(db, registry);
|
|
32
|
+
this.fetchService = new FetchService(db, registry);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Delete an row by ID
|
|
38
|
+
*/
|
|
39
|
+
async delete(collectionPath: string, id: string | number, _databaseId?: string): Promise<void> {
|
|
40
|
+
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
41
|
+
const table = getTableForCollection(collection, this.registry);
|
|
42
|
+
const idInfoArray = getPrimaryKeys(collection, this.registry);
|
|
43
|
+
|
|
44
|
+
const parsedIdObj = parseIdValues(id, idInfoArray);
|
|
45
|
+
|
|
46
|
+
const conditions = [];
|
|
47
|
+
for (const info of idInfoArray) {
|
|
48
|
+
const field = table[info.fieldName as keyof typeof table] as AnyPgColumn;
|
|
49
|
+
if (!field) {
|
|
50
|
+
throw new Error(`ID field '${info.fieldName}' not found in table for collection '${collectionPath}'`);
|
|
51
|
+
}
|
|
52
|
+
conditions.push(eq(field, parsedIdObj[info.fieldName]));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await this.db
|
|
56
|
+
.delete(table)
|
|
57
|
+
.where(and(...conditions));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Delete all rows from a collection
|
|
62
|
+
*/
|
|
63
|
+
async deleteAll(collectionPath: string, _databaseId?: string): Promise<void> {
|
|
64
|
+
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
65
|
+
const table = getTableForCollection(collection, this.registry);
|
|
66
|
+
await this.db.delete(table);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Save an row (create or update)
|
|
71
|
+
*/
|
|
72
|
+
async save<M extends Record<string, unknown>>(
|
|
73
|
+
collectionPath: string,
|
|
74
|
+
values: Partial<M>,
|
|
75
|
+
id?: string | number,
|
|
76
|
+
databaseId?: string
|
|
77
|
+
): Promise<Record<string, unknown>> {
|
|
78
|
+
// If saving under a nested relation path, resolve the parent and inject FK
|
|
79
|
+
let effectiveCollectionPath = collectionPath;
|
|
80
|
+
const effectiveValues: Partial<M> = { ...values };
|
|
81
|
+
let junctionTableInfo: { parentCollection: CollectionConfig; parentId: string | number; relation: Relation; relationKey: string; } | undefined;
|
|
82
|
+
|
|
83
|
+
if (collectionPath.includes("/")) {
|
|
84
|
+
const segments = collectionPath.split("/").filter(Boolean);
|
|
85
|
+
if (segments.length >= 3 && segments.length % 2 === 1) {
|
|
86
|
+
const rootSegment = segments[0];
|
|
87
|
+
let currentCollection = getCollectionByPath(rootSegment, this.registry);
|
|
88
|
+
let currentId: string | number = segments[1];
|
|
89
|
+
|
|
90
|
+
for (let i = 2; i < segments.length; i += 2) {
|
|
91
|
+
const relationKey = segments[i];
|
|
92
|
+
const resolvedRelations = resolveCollectionRelations(currentCollection);
|
|
93
|
+
const relation = findRelation(resolvedRelations, relationKey);
|
|
94
|
+
|
|
95
|
+
if (!relation) {
|
|
96
|
+
const available = Object.keys(resolvedRelations).join(", ") || "(none)";
|
|
97
|
+
throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'. Available relations: [${available}]`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (i === segments.length - 1) {
|
|
101
|
+
const targetCollection = relation.target();
|
|
102
|
+
effectiveCollectionPath = targetCollection.slug;
|
|
103
|
+
|
|
104
|
+
// Handle many-to-many with junction table
|
|
105
|
+
if (relation.cardinality === "many" && relation.through) {
|
|
106
|
+
const parentIdInfoArray = getPrimaryKeys(currentCollection, this.registry);
|
|
107
|
+
const parentIdInfo = parentIdInfoArray[0];
|
|
108
|
+
const parsedParentIdObj = parseIdValues(currentId, parentIdInfoArray);
|
|
109
|
+
const parsedParentId = parsedParentIdObj[parentIdInfo.fieldName];
|
|
110
|
+
|
|
111
|
+
junctionTableInfo = {
|
|
112
|
+
parentCollection: currentCollection,
|
|
113
|
+
parentId: parsedParentId,
|
|
114
|
+
relation: relation,
|
|
115
|
+
relationKey: relationKey
|
|
116
|
+
};
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Find the FK column that should store the parent ID
|
|
121
|
+
let targetColumnName: string;
|
|
122
|
+
|
|
123
|
+
if (relation.localKey) {
|
|
124
|
+
targetColumnName = relation.localKey;
|
|
125
|
+
} else if (relation.foreignKeyOnTarget) {
|
|
126
|
+
targetColumnName = relation.foreignKeyOnTarget;
|
|
127
|
+
} else if (relation.joinPath && relation.joinPath.length === 1) {
|
|
128
|
+
const targetTableName = getTableName(targetCollection);
|
|
129
|
+
const relevantJoinStep = relation.joinPath.find(joinStep => joinStep.table === targetTableName);
|
|
130
|
+
|
|
131
|
+
if (relevantJoinStep) {
|
|
132
|
+
const targetColumnNames = DrizzleConditionBuilder.getColumnNamesFromColumns(relevantJoinStep.on.to);
|
|
133
|
+
targetColumnName = targetColumnNames[0];
|
|
134
|
+
} else {
|
|
135
|
+
logger.warn(`Could not find specific join step for target table ${targetTableName} in relation '${relationKey}'.`);
|
|
136
|
+
const targetColumnNames = DrizzleConditionBuilder.getColumnNamesFromColumns(relation.joinPath[0].on.to);
|
|
137
|
+
targetColumnName = targetColumnNames[0];
|
|
138
|
+
}
|
|
139
|
+
} else if (relation.joinPath && relation.joinPath.length > 1) {
|
|
140
|
+
// For multi-hop relations (like many-to-many through a junction table),
|
|
141
|
+
// there is no direct foreign key on the target table pointing to the parent.
|
|
142
|
+
// The relationship is managed via the junction table.
|
|
143
|
+
// We shouldn't inject the parent ID directly into the target row payload.
|
|
144
|
+
break;
|
|
145
|
+
} else {
|
|
146
|
+
throw new Error(`Relation '${relationKey}' lacks configuration for path-based saving.`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const parentIdInfoArray = getPrimaryKeys(currentCollection, this.registry);
|
|
150
|
+
const parentIdInfo = parentIdInfoArray[0];
|
|
151
|
+
const parsedParentIdObj = parseIdValues(currentId, parentIdInfoArray);
|
|
152
|
+
const parsedParentId = parsedParentIdObj[parentIdInfo.fieldName];
|
|
153
|
+
|
|
154
|
+
const existingValue = (effectiveValues as Record<string, unknown>)[targetColumnName];
|
|
155
|
+
if (existingValue !== undefined && existingValue !== null && existingValue !== parsedParentId) {
|
|
156
|
+
logger.warn(`Overriding provided value '${existingValue}' for FK '${targetColumnName}' with path parent id '${parsedParentId}'.`);
|
|
157
|
+
}
|
|
158
|
+
(effectiveValues as Record<string, unknown>)[targetColumnName] = parsedParentId;
|
|
159
|
+
break;
|
|
160
|
+
} else {
|
|
161
|
+
const nextEntityId = segments[i + 1];
|
|
162
|
+
currentCollection = relation.target();
|
|
163
|
+
currentId = nextEntityId;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const collection = getCollectionByPath(effectiveCollectionPath, this.registry);
|
|
170
|
+
const table = getTableForCollection(collection, this.registry);
|
|
171
|
+
const idInfoArray = getPrimaryKeys(collection, this.registry);
|
|
172
|
+
const primaryKeyFields = idInfoArray.map(info => info.fieldName);
|
|
173
|
+
|
|
174
|
+
// Build an object mapping required for dynamic returning
|
|
175
|
+
const returningKeys: Record<string, AnyPgColumn> = {};
|
|
176
|
+
idInfoArray.forEach(info => {
|
|
177
|
+
const field = table[info.fieldName as keyof typeof table] as AnyPgColumn;
|
|
178
|
+
if (!field) throw new Error(`Primary key field '${info.fieldName}' not found in table for collection '${effectiveCollectionPath}'`);
|
|
179
|
+
returningKeys[info.fieldName] = field;
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Separate relations that require special handling
|
|
183
|
+
const relationValues: Record<string, unknown> = {};
|
|
184
|
+
const otherValues: Partial<M> = { ...effectiveValues };
|
|
185
|
+
const resolvedRelations = resolveCollectionRelations(collection);
|
|
186
|
+
|
|
187
|
+
for (const key in resolvedRelations) {
|
|
188
|
+
const relation = resolvedRelations[key];
|
|
189
|
+
if (relation && relation.cardinality === "many") {
|
|
190
|
+
if (Object.prototype.hasOwnProperty.call(otherValues, key)) {
|
|
191
|
+
relationValues[key] = otherValues[key as keyof M];
|
|
192
|
+
delete otherValues[key as keyof M];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let savedId: string | number;
|
|
198
|
+
try {
|
|
199
|
+
// Transform relations to IDs, then sanitize
|
|
200
|
+
const serializedResult = serializeDataToServer(otherValues as M, collection.properties as Properties, collection, this.registry);
|
|
201
|
+
|
|
202
|
+
// Extract relation updates from the typed result
|
|
203
|
+
const inverseRelationUpdates = serializedResult.inverseRelationUpdates;
|
|
204
|
+
const joinPathRelationUpdates = serializedResult.joinPathRelationUpdates;
|
|
205
|
+
|
|
206
|
+
const entityData = sanitizeAndConvertDates(serializedResult.scalarData);
|
|
207
|
+
|
|
208
|
+
savedId = await this.db.transaction(async (tx) => {
|
|
209
|
+
let currentId: string | number;
|
|
210
|
+
|
|
211
|
+
if (id) {
|
|
212
|
+
// Update existing row
|
|
213
|
+
currentId = id; // `id` is already the formatted composite or singular string
|
|
214
|
+
const idValues = parseIdValues(id, idInfoArray);
|
|
215
|
+
|
|
216
|
+
// Apply joinPath one-to-one relation updates BEFORE the main UPDATE.
|
|
217
|
+
// This ensures parentSourceCol reads the pre-update FK value, preventing
|
|
218
|
+
// stale joinPath values from corrupting related rows when an
|
|
219
|
+
// intermediate FK (e.g., author_id) changes in the same save.
|
|
220
|
+
// Example: changing author A→B with stale profile P1 (A's):
|
|
221
|
+
// reads old author_id=A → clears P1.author_id → re-sets P1.author_id=A (no-op).
|
|
222
|
+
if (joinPathRelationUpdates.length > 0) {
|
|
223
|
+
await this.relationService.updateJoinPathOneToOneRelations(tx, collection, currentId, joinPathRelationUpdates);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Only issue an UPDATE if there are scalar columns to set.
|
|
227
|
+
// When the payload contains only relation data, entityData is
|
|
228
|
+
// empty after relation stripping and Drizzle throws "No values to set".
|
|
229
|
+
const scalarKeys = Object.keys(entityData as Record<string, unknown>);
|
|
230
|
+
if (scalarKeys.length > 0) {
|
|
231
|
+
const updateQuery = tx.update(table).set(entityData as Record<string, unknown>);
|
|
232
|
+
const conditions = [];
|
|
233
|
+
for (const info of idInfoArray) {
|
|
234
|
+
const field = table[info.fieldName as keyof typeof table] as AnyPgColumn;
|
|
235
|
+
conditions.push(eq(field, idValues[info.fieldName]));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
await updateQuery.where(and(...conditions));
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
const dataForInsert = { ...(entityData as Record<string, unknown>) };
|
|
242
|
+
|
|
243
|
+
// Strip empty primary keys so the database defaults (e.g. uuid_gen(), auto-increment) can trigger
|
|
244
|
+
for (const info of idInfoArray) {
|
|
245
|
+
if (dataForInsert[info.fieldName] === "" || dataForInsert[info.fieldName] === null || dataForInsert[info.fieldName] === undefined) {
|
|
246
|
+
delete dataForInsert[info.fieldName];
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const result = await tx
|
|
251
|
+
.insert(table)
|
|
252
|
+
.values(dataForInsert)
|
|
253
|
+
.returning(returningKeys);
|
|
254
|
+
|
|
255
|
+
const resultRow = result[0];
|
|
256
|
+
currentId = buildCompositeId(resultRow, idInfoArray);
|
|
257
|
+
|
|
258
|
+
// For inserts, apply joinPath after since the parent row didn't exist before
|
|
259
|
+
if (joinPathRelationUpdates.length > 0) {
|
|
260
|
+
await this.relationService.updateJoinPathOneToOneRelations(tx, collection, currentId, joinPathRelationUpdates);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Handle inverse relation updates
|
|
265
|
+
if (inverseRelationUpdates.length > 0) {
|
|
266
|
+
await this.relationService.updateInverseRelations(tx, collection, currentId, inverseRelationUpdates);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Update many-to-many relations
|
|
270
|
+
if (Object.keys(relationValues).length > 0) {
|
|
271
|
+
await this.relationService.updateRelationsUsingJoins(tx, collection, currentId, relationValues);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Handle junction table creation for many-to-many path-based saves
|
|
275
|
+
if (junctionTableInfo && !id) {
|
|
276
|
+
await this.relationService.handleJunctionTableCreation(tx, currentId, junctionTableInfo);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return currentId;
|
|
280
|
+
});
|
|
281
|
+
} catch (error: unknown) {
|
|
282
|
+
throw this.toUserFriendlyError(error, collection.slug);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Fetch the updated/created row to return with proper relation objects
|
|
286
|
+
const finalEntity = await this.fetchService.fetchOne<M>(collection.slug, savedId, databaseId);
|
|
287
|
+
if (!finalEntity) throw new Error("Could not fetch row after save.");
|
|
288
|
+
return finalEntity;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Get the RelationService instance for external use
|
|
293
|
+
*/
|
|
294
|
+
getRelationService(): RelationService {
|
|
295
|
+
return this.relationService;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Get the FetchService instance for external use
|
|
300
|
+
*/
|
|
301
|
+
getFetchService(): FetchService {
|
|
302
|
+
return this.fetchService;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Translate raw PostgreSQL / Drizzle errors into user-friendly messages.
|
|
307
|
+
*/
|
|
308
|
+
private toUserFriendlyError(error: unknown, collectionSlug: string): Error {
|
|
309
|
+
const pgError = extractPgError(error);
|
|
310
|
+
|
|
311
|
+
if (pgError) {
|
|
312
|
+
const { message } = pgErrorToFriendlyMessage(pgError, collectionSlug);
|
|
313
|
+
return new Error(message);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// No PG error found — try to extract a useful message from the
|
|
317
|
+
// Drizzle wrapper instead of leaking the raw SQL query + params.
|
|
318
|
+
const causeMessage = extractCauseMessage(error);
|
|
319
|
+
if (causeMessage) {
|
|
320
|
+
return new Error(`Database error in "${collectionSlug}": ${causeMessage}`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Last resort: generic message, never leak raw SQL
|
|
324
|
+
if (error instanceof Error && error.message.startsWith("Failed query:")) {
|
|
325
|
+
return new Error(`Failed to save row in "${collectionSlug}". Check server logs for details.`);
|
|
326
|
+
}
|
|
327
|
+
return new Error(`Database error in "${collectionSlug}": ${String(error)}`);
|
|
328
|
+
}
|
|
329
|
+
}
|