@rebasepro/server-postgresql 0.0.1-canary.09e5ec5
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 +6 -0
- package/README.md +106 -0
- package/build-errors.txt +37 -0
- package/dist/common/src/collections/CollectionRegistry.d.ts +56 -0
- package/dist/common/src/collections/index.d.ts +1 -0
- package/dist/common/src/data/buildRebaseData.d.ts +14 -0
- package/dist/common/src/index.d.ts +3 -0
- package/dist/common/src/util/builders.d.ts +57 -0
- package/dist/common/src/util/callbacks.d.ts +6 -0
- package/dist/common/src/util/collections.d.ts +11 -0
- package/dist/common/src/util/common.d.ts +2 -0
- package/dist/common/src/util/conditions.d.ts +26 -0
- package/dist/common/src/util/entities.d.ts +58 -0
- package/dist/common/src/util/enums.d.ts +3 -0
- package/dist/common/src/util/index.d.ts +16 -0
- package/dist/common/src/util/navigation_from_path.d.ts +34 -0
- package/dist/common/src/util/navigation_utils.d.ts +20 -0
- package/dist/common/src/util/parent_references_from_path.d.ts +6 -0
- package/dist/common/src/util/paths.d.ts +14 -0
- package/dist/common/src/util/permissions.d.ts +5 -0
- package/dist/common/src/util/references.d.ts +2 -0
- package/dist/common/src/util/relations.d.ts +22 -0
- package/dist/common/src/util/resolutions.d.ts +72 -0
- package/dist/common/src/util/storage.d.ts +24 -0
- package/dist/index.es.js +11298 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +11306 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +100 -0
- package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +40 -0
- package/dist/server-postgresql/src/auth/ensure-tables.d.ts +6 -0
- package/dist/server-postgresql/src/auth/services.d.ts +192 -0
- package/dist/server-postgresql/src/cli.d.ts +1 -0
- package/dist/server-postgresql/src/collections/PostgresCollectionRegistry.d.ts +43 -0
- package/dist/server-postgresql/src/connection.d.ts +40 -0
- package/dist/server-postgresql/src/data-transformer.d.ts +58 -0
- package/dist/server-postgresql/src/databasePoolManager.d.ts +20 -0
- package/dist/server-postgresql/src/history/HistoryService.d.ts +71 -0
- package/dist/server-postgresql/src/history/ensure-history-table.d.ts +7 -0
- package/dist/server-postgresql/src/index.d.ts +13 -0
- package/dist/server-postgresql/src/interfaces.d.ts +18 -0
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +868 -0
- package/dist/server-postgresql/src/schema/doctor-cli.d.ts +2 -0
- package/dist/server-postgresql/src/schema/doctor.d.ts +43 -0
- package/dist/server-postgresql/src/schema/generate-drizzle-schema-logic.d.ts +2 -0
- package/dist/server-postgresql/src/schema/generate-drizzle-schema.d.ts +1 -0
- package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +82 -0
- package/dist/server-postgresql/src/schema/introspect-db.d.ts +1 -0
- package/dist/server-postgresql/src/schema/test-schema.d.ts +24 -0
- package/dist/server-postgresql/src/services/BranchService.d.ts +47 -0
- package/dist/server-postgresql/src/services/EntityFetchService.d.ts +209 -0
- package/dist/server-postgresql/src/services/EntityPersistService.d.ts +41 -0
- package/dist/server-postgresql/src/services/RelationService.d.ts +98 -0
- package/dist/server-postgresql/src/services/entity-helpers.d.ts +38 -0
- package/dist/server-postgresql/src/services/entityService.d.ts +104 -0
- package/dist/server-postgresql/src/services/index.d.ts +4 -0
- package/dist/server-postgresql/src/services/realtimeService.d.ts +188 -0
- package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +116 -0
- package/dist/server-postgresql/src/websocket.d.ts +5 -0
- package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
- package/dist/types/src/controllers/auth.d.ts +119 -0
- package/dist/types/src/controllers/client.d.ts +170 -0
- package/dist/types/src/controllers/collection_registry.d.ts +45 -0
- package/dist/types/src/controllers/customization_controller.d.ts +60 -0
- package/dist/types/src/controllers/data.d.ts +168 -0
- package/dist/types/src/controllers/data_driver.d.ts +160 -0
- package/dist/types/src/controllers/database_admin.d.ts +11 -0
- package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
- package/dist/types/src/controllers/effective_role.d.ts +4 -0
- package/dist/types/src/controllers/email.d.ts +34 -0
- package/dist/types/src/controllers/index.d.ts +18 -0
- package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
- package/dist/types/src/controllers/navigation.d.ts +213 -0
- package/dist/types/src/controllers/registry.d.ts +54 -0
- package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
- package/dist/types/src/controllers/side_entity_controller.d.ts +90 -0
- package/dist/types/src/controllers/snackbar.d.ts +24 -0
- package/dist/types/src/controllers/storage.d.ts +171 -0
- package/dist/types/src/index.d.ts +4 -0
- package/dist/types/src/rebase_context.d.ts +105 -0
- package/dist/types/src/types/backend.d.ts +536 -0
- package/dist/types/src/types/builders.d.ts +15 -0
- package/dist/types/src/types/chips.d.ts +5 -0
- package/dist/types/src/types/collections.d.ts +856 -0
- package/dist/types/src/types/cron.d.ts +102 -0
- package/dist/types/src/types/data_source.d.ts +64 -0
- package/dist/types/src/types/entities.d.ts +145 -0
- package/dist/types/src/types/entity_actions.d.ts +98 -0
- package/dist/types/src/types/entity_callbacks.d.ts +173 -0
- package/dist/types/src/types/entity_link_builder.d.ts +7 -0
- package/dist/types/src/types/entity_overrides.d.ts +10 -0
- package/dist/types/src/types/entity_views.d.ts +61 -0
- package/dist/types/src/types/export_import.d.ts +21 -0
- package/dist/types/src/types/index.d.ts +23 -0
- package/dist/types/src/types/locales.d.ts +4 -0
- package/dist/types/src/types/modify_collections.d.ts +5 -0
- package/dist/types/src/types/plugins.d.ts +279 -0
- package/dist/types/src/types/properties.d.ts +1176 -0
- package/dist/types/src/types/property_config.d.ts +70 -0
- package/dist/types/src/types/relations.d.ts +336 -0
- package/dist/types/src/types/slots.d.ts +252 -0
- package/dist/types/src/types/translations.d.ts +870 -0
- package/dist/types/src/types/user_management_delegate.d.ts +121 -0
- package/dist/types/src/types/websockets.d.ts +78 -0
- package/dist/types/src/users/index.d.ts +2 -0
- package/dist/types/src/users/roles.d.ts +22 -0
- package/dist/types/src/users/user.d.ts +46 -0
- package/drizzle-test/0000_woozy_junta.sql +6 -0
- package/drizzle-test/0001_youthful_arachne.sql +1 -0
- package/drizzle-test/0002_lively_dragon_lord.sql +2 -0
- package/drizzle-test/0003_mean_king_cobra.sql +2 -0
- package/drizzle-test/meta/0000_snapshot.json +47 -0
- package/drizzle-test/meta/0001_snapshot.json +48 -0
- package/drizzle-test/meta/0002_snapshot.json +38 -0
- package/drizzle-test/meta/0003_snapshot.json +48 -0
- package/drizzle-test/meta/_journal.json +34 -0
- package/drizzle-test-out/0000_tan_trauma.sql +6 -0
- package/drizzle-test-out/0001_rapid_drax.sql +1 -0
- package/drizzle-test-out/meta/0000_snapshot.json +44 -0
- package/drizzle-test-out/meta/0001_snapshot.json +54 -0
- package/drizzle-test-out/meta/_journal.json +20 -0
- package/drizzle.test.config.ts +10 -0
- package/jest-all.log +3128 -0
- package/jest.log +49 -0
- package/package.json +92 -0
- package/scratch.ts +41 -0
- package/src/PostgresBackendDriver.ts +1008 -0
- package/src/PostgresBootstrapper.ts +231 -0
- package/src/auth/ensure-tables.ts +381 -0
- package/src/auth/services.ts +799 -0
- package/src/cli.ts +648 -0
- package/src/collections/PostgresCollectionRegistry.ts +96 -0
- package/src/connection.ts +84 -0
- package/src/data-transformer.ts +608 -0
- package/src/databasePoolManager.ts +85 -0
- package/src/history/HistoryService.ts +248 -0
- package/src/history/ensure-history-table.ts +45 -0
- package/src/index.ts +13 -0
- package/src/interfaces.ts +60 -0
- package/src/schema/auth-schema.ts +169 -0
- package/src/schema/doctor-cli.ts +47 -0
- package/src/schema/doctor.ts +595 -0
- package/src/schema/generate-drizzle-schema-logic.ts +765 -0
- package/src/schema/generate-drizzle-schema.ts +151 -0
- package/src/schema/introspect-db-logic.ts +542 -0
- package/src/schema/introspect-db.ts +211 -0
- package/src/schema/test-schema.ts +11 -0
- package/src/services/BranchService.ts +237 -0
- package/src/services/EntityFetchService.ts +1576 -0
- package/src/services/EntityPersistService.ts +349 -0
- package/src/services/RelationService.ts +1274 -0
- package/src/services/entity-helpers.ts +147 -0
- package/src/services/entityService.ts +211 -0
- package/src/services/index.ts +13 -0
- package/src/services/realtimeService.ts +1034 -0
- package/src/utils/drizzle-conditions.ts +1000 -0
- package/src/websocket.ts +518 -0
- package/test/auth-services.test.ts +661 -0
- package/test/batch-many-to-many-regression.test.ts +573 -0
- package/test/branchService.test.ts +367 -0
- package/test/data-transformer-hardening.test.ts +417 -0
- package/test/data-transformer.test.ts +175 -0
- package/test/doctor.test.ts +182 -0
- package/test/drizzle-conditions.test.ts +895 -0
- package/test/entityService.errors.test.ts +367 -0
- package/test/entityService.relations.test.ts +1008 -0
- package/test/entityService.subcollection-search.test.ts +566 -0
- package/test/entityService.test.ts +1035 -0
- package/test/generate-drizzle-schema.test.ts +988 -0
- package/test/historyService.test.ts +141 -0
- package/test/introspect-db-generation.test.ts +436 -0
- package/test/introspect-db-utils.test.ts +389 -0
- package/test/n-plus-one-regression.test.ts +314 -0
- package/test/postgresDataDriver.test.ts +648 -0
- package/test/realtimeService.test.ts +307 -0
- package/test/relation-pipeline-gaps.test.ts +637 -0
- package/test/relations.test.ts +1115 -0
- package/test/unmapped-tables-safety.test.ts +345 -0
- package/test-drizzle-bug.ts +18 -0
- package/test-drizzle-out/0000_cultured_freak.sql +7 -0
- package/test-drizzle-out/0001_tiresome_professor_monster.sql +1 -0
- package/test-drizzle-out/meta/0000_snapshot.json +55 -0
- package/test-drizzle-out/meta/0001_snapshot.json +63 -0
- package/test-drizzle-out/meta/_journal.json +20 -0
- package/test-drizzle-prompt.sh +2 -0
- package/test-policy-prompt.sh +3 -0
- package/test-programmatic.ts +30 -0
- package/test-programmatic2.ts +59 -0
- package/test-schema-no-policies.ts +12 -0
- package/test_drizzle_mock.js +3 -0
- package/test_find_changed.mjs +32 -0
- package/test_hash.js +14 -0
- package/test_output.txt +3145 -0
- package/tsconfig.json +49 -0
- package/tsconfig.prod.json +20 -0
- package/vite.config.ts +82 -0
|
@@ -0,0 +1,349 @@
|
|
|
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 { Entity, EntityCollection, 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 "./entity-helpers";
|
|
14
|
+
import { sanitizeAndConvertDates, serializeDataToServer } from "../data-transformer";
|
|
15
|
+
import { RelationService } from "./RelationService";
|
|
16
|
+
import { EntityFetchService } from "./EntityFetchService";
|
|
17
|
+
import { DrizzleClient } from "../interfaces";
|
|
18
|
+
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Service for handling all entity write operations.
|
|
22
|
+
* Handles saving, deleting, and updating entities.
|
|
23
|
+
*/
|
|
24
|
+
export class EntityPersistService {
|
|
25
|
+
private relationService: RelationService;
|
|
26
|
+
private fetchService: EntityFetchService;
|
|
27
|
+
|
|
28
|
+
constructor(private db: DrizzleClient, private registry: PostgresCollectionRegistry) {
|
|
29
|
+
this.relationService = new RelationService(db, registry);
|
|
30
|
+
this.fetchService = new EntityFetchService(db, registry);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Delete an entity by ID
|
|
36
|
+
*/
|
|
37
|
+
async deleteEntity(collectionPath: string, entityId: string | number, _databaseId?: string): Promise<void> {
|
|
38
|
+
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
39
|
+
const table = getTableForCollection(collection, this.registry);
|
|
40
|
+
const idInfoArray = getPrimaryKeys(collection, this.registry);
|
|
41
|
+
const idInfo = idInfoArray[0];
|
|
42
|
+
const idField = table[idInfo.fieldName as keyof typeof table] as AnyPgColumn;
|
|
43
|
+
|
|
44
|
+
if (!idField) {
|
|
45
|
+
throw new Error(`ID field '${idInfo.fieldName}' not found in table for collection '${collectionPath}'`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const parsedIdObj = parseIdValues(entityId, idInfoArray);
|
|
49
|
+
const parsedId = parsedIdObj[idInfo.fieldName];
|
|
50
|
+
|
|
51
|
+
await this.db
|
|
52
|
+
.delete(table)
|
|
53
|
+
.where(eq(idField, parsedId));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Save an entity (create or update)
|
|
58
|
+
*/
|
|
59
|
+
async saveEntity<M extends Record<string, unknown>>(
|
|
60
|
+
collectionPath: string,
|
|
61
|
+
values: Partial<M>,
|
|
62
|
+
entityId?: string | number,
|
|
63
|
+
databaseId?: string
|
|
64
|
+
): Promise<Entity<M>> {
|
|
65
|
+
// If saving under a nested relation path, resolve the parent and inject FK
|
|
66
|
+
let effectiveCollectionPath = collectionPath;
|
|
67
|
+
const effectiveValues: Partial<M> = { ...values };
|
|
68
|
+
let junctionTableInfo: { parentCollection: EntityCollection; parentId: string | number; relation: Relation; relationKey: string; } | undefined;
|
|
69
|
+
|
|
70
|
+
if (collectionPath.includes("/")) {
|
|
71
|
+
const segments = collectionPath.split("/").filter(Boolean);
|
|
72
|
+
if (segments.length >= 3 && segments.length % 2 === 1) {
|
|
73
|
+
const rootSegment = segments[0];
|
|
74
|
+
let currentCollection = getCollectionByPath(rootSegment, this.registry);
|
|
75
|
+
let currentEntityId: string | number = segments[1];
|
|
76
|
+
|
|
77
|
+
for (let i = 2; i < segments.length; i += 2) {
|
|
78
|
+
const relationKey = segments[i];
|
|
79
|
+
const resolvedRelations = resolveCollectionRelations(currentCollection);
|
|
80
|
+
const relation = findRelation(resolvedRelations, relationKey);
|
|
81
|
+
|
|
82
|
+
if (!relation) {
|
|
83
|
+
const available = Object.keys(resolvedRelations).join(", ") || "(none)";
|
|
84
|
+
throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'. Available relations: [${available}]`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (i === segments.length - 1) {
|
|
88
|
+
const targetCollection = relation.target();
|
|
89
|
+
effectiveCollectionPath = targetCollection.slug;
|
|
90
|
+
|
|
91
|
+
// Handle many-to-many with junction table
|
|
92
|
+
if (relation.cardinality === "many" && relation.through) {
|
|
93
|
+
const parentIdInfoArray = getPrimaryKeys(currentCollection, this.registry);
|
|
94
|
+
const parentIdInfo = parentIdInfoArray[0];
|
|
95
|
+
const parsedParentIdObj = parseIdValues(currentEntityId, parentIdInfoArray);
|
|
96
|
+
const parsedParentId = parsedParentIdObj[parentIdInfo.fieldName];
|
|
97
|
+
|
|
98
|
+
junctionTableInfo = {
|
|
99
|
+
parentCollection: currentCollection,
|
|
100
|
+
parentId: parsedParentId,
|
|
101
|
+
relation: relation,
|
|
102
|
+
relationKey: relationKey
|
|
103
|
+
};
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Find the FK column that should store the parent ID
|
|
108
|
+
let targetColumnName: string;
|
|
109
|
+
|
|
110
|
+
if (relation.localKey) {
|
|
111
|
+
targetColumnName = relation.localKey;
|
|
112
|
+
} else if (relation.foreignKeyOnTarget) {
|
|
113
|
+
targetColumnName = relation.foreignKeyOnTarget;
|
|
114
|
+
} else if (relation.joinPath && relation.joinPath.length > 0) {
|
|
115
|
+
const targetTableName = getTableName(targetCollection);
|
|
116
|
+
const relevantJoinStep = relation.joinPath.find(joinStep => joinStep.table === targetTableName);
|
|
117
|
+
|
|
118
|
+
if (relevantJoinStep) {
|
|
119
|
+
const targetColumnNames = DrizzleConditionBuilder.getColumnNamesFromColumns(relevantJoinStep.on.to);
|
|
120
|
+
targetColumnName = targetColumnNames[0];
|
|
121
|
+
} else {
|
|
122
|
+
console.warn(`Could not find specific join step for target table ${targetTableName} in relation '${relationKey}'.`);
|
|
123
|
+
const targetColumnNames = DrizzleConditionBuilder.getColumnNamesFromColumns(relation.joinPath[0].on.to);
|
|
124
|
+
targetColumnName = targetColumnNames[0];
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
throw new Error(`Relation '${relationKey}' lacks configuration for path-based saving.`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const parentIdInfoArray = getPrimaryKeys(currentCollection, this.registry);
|
|
131
|
+
const parentIdInfo = parentIdInfoArray[0];
|
|
132
|
+
const parsedParentIdObj = parseIdValues(currentEntityId, parentIdInfoArray);
|
|
133
|
+
const parsedParentId = parsedParentIdObj[parentIdInfo.fieldName];
|
|
134
|
+
|
|
135
|
+
const existingValue = (effectiveValues as Record<string, unknown>)[targetColumnName];
|
|
136
|
+
if (existingValue !== undefined && existingValue !== null && existingValue !== parsedParentId) {
|
|
137
|
+
console.warn(`Overriding provided value '${existingValue}' for FK '${targetColumnName}' with path parent id '${parsedParentId}'.`);
|
|
138
|
+
}
|
|
139
|
+
(effectiveValues as Record<string, unknown>)[targetColumnName] = parsedParentId;
|
|
140
|
+
break;
|
|
141
|
+
} else {
|
|
142
|
+
const nextEntityId = segments[i + 1];
|
|
143
|
+
currentCollection = relation.target();
|
|
144
|
+
currentEntityId = nextEntityId;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const collection = getCollectionByPath(effectiveCollectionPath, this.registry);
|
|
151
|
+
const table = getTableForCollection(collection, this.registry);
|
|
152
|
+
const idInfoArray = getPrimaryKeys(collection, this.registry);
|
|
153
|
+
const primaryKeyFields = idInfoArray.map(info => info.fieldName);
|
|
154
|
+
|
|
155
|
+
// Build an object mapping required for dynamic returning
|
|
156
|
+
const returningKeys: Record<string, AnyPgColumn> = {};
|
|
157
|
+
idInfoArray.forEach(info => {
|
|
158
|
+
const field = table[info.fieldName as keyof typeof table] as AnyPgColumn;
|
|
159
|
+
if (!field) throw new Error(`Primary key field '${info.fieldName}' not found in table for collection '${effectiveCollectionPath}'`);
|
|
160
|
+
returningKeys[info.fieldName] = field;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Separate relations that require special handling
|
|
164
|
+
const relationValues: Record<string, unknown> = {};
|
|
165
|
+
const otherValues: Partial<M> = { ...effectiveValues };
|
|
166
|
+
const resolvedRelations = resolveCollectionRelations(collection);
|
|
167
|
+
|
|
168
|
+
for (const key in resolvedRelations) {
|
|
169
|
+
const relation = resolvedRelations[key];
|
|
170
|
+
if (relation && relation.cardinality === "many") {
|
|
171
|
+
if (Object.prototype.hasOwnProperty.call(otherValues, key)) {
|
|
172
|
+
relationValues[key] = otherValues[key as keyof M];
|
|
173
|
+
delete otherValues[key as keyof M];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Transform relations to IDs, then sanitize
|
|
179
|
+
const serializedResult = serializeDataToServer(otherValues as M, collection.properties as Properties, collection, this.registry);
|
|
180
|
+
|
|
181
|
+
// Extract relation updates from the typed result
|
|
182
|
+
const inverseRelationUpdates = serializedResult.inverseRelationUpdates;
|
|
183
|
+
const joinPathRelationUpdates = serializedResult.joinPathRelationUpdates;
|
|
184
|
+
|
|
185
|
+
const entityData = sanitizeAndConvertDates(serializedResult.scalarData);
|
|
186
|
+
|
|
187
|
+
let savedId: string | number;
|
|
188
|
+
try {
|
|
189
|
+
savedId = await this.db.transaction(async (tx) => {
|
|
190
|
+
let currentId: string | number;
|
|
191
|
+
|
|
192
|
+
if (entityId) {
|
|
193
|
+
// Update existing entity
|
|
194
|
+
currentId = entityId; // `entityId` is already the formatted composite or singular string
|
|
195
|
+
const idValues = parseIdValues(entityId, idInfoArray);
|
|
196
|
+
|
|
197
|
+
// Apply joinPath one-to-one relation updates BEFORE the main UPDATE.
|
|
198
|
+
// This ensures parentSourceCol reads the pre-update FK value, preventing
|
|
199
|
+
// stale joinPath values from corrupting related entities when an
|
|
200
|
+
// intermediate FK (e.g., author_id) changes in the same save.
|
|
201
|
+
// Example: changing author A→B with stale profile P1 (A's):
|
|
202
|
+
// reads old author_id=A → clears P1.author_id → re-sets P1.author_id=A (no-op).
|
|
203
|
+
if (joinPathRelationUpdates.length > 0) {
|
|
204
|
+
await this.relationService.updateJoinPathOneToOneRelations(tx, collection, currentId, joinPathRelationUpdates);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Only issue an UPDATE if there are scalar columns to set.
|
|
208
|
+
// When the payload contains only relation data, entityData is
|
|
209
|
+
// empty after relation stripping and Drizzle throws "No values to set".
|
|
210
|
+
const scalarKeys = Object.keys(entityData as Record<string, unknown>);
|
|
211
|
+
if (scalarKeys.length > 0) {
|
|
212
|
+
const updateQuery = tx.update(table).set(entityData as Record<string, unknown>);
|
|
213
|
+
const conditions = [];
|
|
214
|
+
for (const info of idInfoArray) {
|
|
215
|
+
const field = table[info.fieldName as keyof typeof table] as AnyPgColumn;
|
|
216
|
+
conditions.push(eq(field, idValues[info.fieldName]));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
await updateQuery.where(and(...conditions));
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
const dataForInsert = { ...(entityData as Record<string, unknown>) };
|
|
223
|
+
|
|
224
|
+
// Strip empty primary keys so the database defaults (e.g. uuid_gen(), auto-increment) can trigger
|
|
225
|
+
for (const info of idInfoArray) {
|
|
226
|
+
if (dataForInsert[info.fieldName] === "" || dataForInsert[info.fieldName] === null || dataForInsert[info.fieldName] === undefined) {
|
|
227
|
+
delete dataForInsert[info.fieldName];
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const result = await tx
|
|
232
|
+
.insert(table)
|
|
233
|
+
.values(dataForInsert)
|
|
234
|
+
.returning(returningKeys);
|
|
235
|
+
|
|
236
|
+
const resultRow = result[0];
|
|
237
|
+
currentId = buildCompositeId(resultRow, idInfoArray);
|
|
238
|
+
|
|
239
|
+
// For inserts, apply joinPath after since the parent row didn't exist before
|
|
240
|
+
if (joinPathRelationUpdates.length > 0) {
|
|
241
|
+
await this.relationService.updateJoinPathOneToOneRelations(tx, collection, currentId, joinPathRelationUpdates);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Handle inverse relation updates
|
|
246
|
+
if (inverseRelationUpdates.length > 0) {
|
|
247
|
+
await this.relationService.updateInverseRelations(tx, collection, currentId, inverseRelationUpdates);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Update many-to-many relations
|
|
251
|
+
if (Object.keys(relationValues).length > 0) {
|
|
252
|
+
await this.relationService.updateRelationsUsingJoins(tx, collection, currentId, relationValues);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Handle junction table creation for many-to-many path-based saves
|
|
256
|
+
if (junctionTableInfo && !entityId) {
|
|
257
|
+
await this.relationService.handleJunctionTableCreation(tx, currentId, junctionTableInfo);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return currentId;
|
|
261
|
+
});
|
|
262
|
+
} catch (error: unknown) {
|
|
263
|
+
throw this.toUserFriendlyError(error, collection.slug);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Fetch the updated/created entity to return with proper relation objects
|
|
267
|
+
const finalEntity = await this.fetchService.fetchEntity<M>(collection.slug, savedId, databaseId);
|
|
268
|
+
if (!finalEntity) throw new Error("Could not fetch entity after save.");
|
|
269
|
+
return finalEntity;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Get the RelationService instance for external use
|
|
274
|
+
*/
|
|
275
|
+
getRelationService(): RelationService {
|
|
276
|
+
return this.relationService;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Get the FetchService instance for external use
|
|
281
|
+
*/
|
|
282
|
+
getFetchService(): EntityFetchService {
|
|
283
|
+
return this.fetchService;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Translate raw PostgreSQL / Drizzle errors into user-friendly messages.
|
|
288
|
+
*/
|
|
289
|
+
private toUserFriendlyError(error: unknown, collectionSlug: string): Error {
|
|
290
|
+
// Dig into Drizzle's wrapper to find the underlying PG error
|
|
291
|
+
const pgError = this.extractPgError(error);
|
|
292
|
+
|
|
293
|
+
if (pgError) {
|
|
294
|
+
const detail = pgError.detail as string | undefined;
|
|
295
|
+
const constraint = pgError.constraint as string | undefined;
|
|
296
|
+
const column = pgError.column as string | undefined;
|
|
297
|
+
const table = pgError.table as string | undefined;
|
|
298
|
+
|
|
299
|
+
switch (pgError.code) {
|
|
300
|
+
case "23503": // foreign_key_violation
|
|
301
|
+
return new Error(
|
|
302
|
+
detail
|
|
303
|
+
? `Foreign key constraint violated: ${detail}`
|
|
304
|
+
: `Cannot save: a foreign key constraint${constraint ? ` (${constraint})` : ""} was violated in "${collectionSlug}".`
|
|
305
|
+
);
|
|
306
|
+
case "23505": // unique_violation
|
|
307
|
+
return new Error(
|
|
308
|
+
detail
|
|
309
|
+
? `Duplicate value: ${detail}`
|
|
310
|
+
: `Cannot save: a unique constraint${constraint ? ` (${constraint})` : ""} was violated in "${collectionSlug}".`
|
|
311
|
+
);
|
|
312
|
+
case "23502": // not_null_violation
|
|
313
|
+
return new Error(
|
|
314
|
+
`Missing required field: "${column ?? "unknown"}" in "${table ?? collectionSlug}" cannot be empty.`
|
|
315
|
+
);
|
|
316
|
+
case "23514": // check_violation
|
|
317
|
+
return new Error(
|
|
318
|
+
`Validation failed: a check constraint${constraint ? ` (${constraint})` : ""} was violated in "${collectionSlug}".`
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Fall through: re-throw original
|
|
324
|
+
if (error instanceof Error) return error;
|
|
325
|
+
return new Error(String(error));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Extract the underlying PostgreSQL error from a Drizzle wrapper.
|
|
330
|
+
* Drizzle wraps PG errors in a `cause` property.
|
|
331
|
+
*/
|
|
332
|
+
private extractPgError(error: unknown): (Error & { code?: string; detail?: unknown; constraint?: unknown; column?: unknown; table?: unknown }) | null {
|
|
333
|
+
if (!error || typeof error !== "object") return null;
|
|
334
|
+
|
|
335
|
+
const err = error as Error & { code?: string; cause?: unknown; detail?: unknown };
|
|
336
|
+
|
|
337
|
+
// Check if the error itself has a PG error code
|
|
338
|
+
if (err.code && /^[0-9]{5}$/.test(err.code)) {
|
|
339
|
+
return err as Error & { code: string; detail?: unknown; constraint?: unknown; column?: unknown; table?: unknown };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Check the cause chain (Drizzle wraps PG errors)
|
|
343
|
+
if (err.cause && typeof err.cause === "object") {
|
|
344
|
+
return this.extractPgError(err.cause);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
}
|