@rebasepro/server-postgresql 0.0.1-canary.4d4fb3e

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/LICENSE +6 -0
  2. package/README.md +106 -0
  3. package/build-errors.txt +37 -0
  4. package/dist/common/src/collections/CollectionRegistry.d.ts +48 -0
  5. package/dist/common/src/collections/index.d.ts +1 -0
  6. package/dist/common/src/data/buildRebaseData.d.ts +14 -0
  7. package/dist/common/src/index.d.ts +3 -0
  8. package/dist/common/src/util/builders.d.ts +57 -0
  9. package/dist/common/src/util/callbacks.d.ts +6 -0
  10. package/dist/common/src/util/collections.d.ts +11 -0
  11. package/dist/common/src/util/common.d.ts +2 -0
  12. package/dist/common/src/util/conditions.d.ts +26 -0
  13. package/dist/common/src/util/entities.d.ts +36 -0
  14. package/dist/common/src/util/enums.d.ts +3 -0
  15. package/dist/common/src/util/index.d.ts +16 -0
  16. package/dist/common/src/util/navigation_from_path.d.ts +34 -0
  17. package/dist/common/src/util/navigation_utils.d.ts +20 -0
  18. package/dist/common/src/util/parent_references_from_path.d.ts +6 -0
  19. package/dist/common/src/util/paths.d.ts +14 -0
  20. package/dist/common/src/util/permissions.d.ts +5 -0
  21. package/dist/common/src/util/references.d.ts +2 -0
  22. package/dist/common/src/util/relations.d.ts +12 -0
  23. package/dist/common/src/util/resolutions.d.ts +72 -0
  24. package/dist/common/src/util/storage.d.ts +24 -0
  25. package/dist/index.es.js +10635 -0
  26. package/dist/index.es.js.map +1 -0
  27. package/dist/index.umd.js +10643 -0
  28. package/dist/index.umd.js.map +1 -0
  29. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +112 -0
  30. package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +40 -0
  31. package/dist/server-postgresql/src/auth/ensure-tables.d.ts +6 -0
  32. package/dist/server-postgresql/src/auth/services.d.ts +188 -0
  33. package/dist/server-postgresql/src/cli.d.ts +1 -0
  34. package/dist/server-postgresql/src/collections/PostgresCollectionRegistry.d.ts +43 -0
  35. package/dist/server-postgresql/src/connection.d.ts +7 -0
  36. package/dist/server-postgresql/src/data-transformer.d.ts +36 -0
  37. package/dist/server-postgresql/src/databasePoolManager.d.ts +20 -0
  38. package/dist/server-postgresql/src/history/HistoryService.d.ts +71 -0
  39. package/dist/server-postgresql/src/history/ensure-history-table.d.ts +7 -0
  40. package/dist/server-postgresql/src/index.d.ts +13 -0
  41. package/dist/server-postgresql/src/interfaces.d.ts +18 -0
  42. package/dist/server-postgresql/src/schema/auth-schema.d.ts +767 -0
  43. package/dist/server-postgresql/src/schema/generate-drizzle-schema-logic.d.ts +2 -0
  44. package/dist/server-postgresql/src/schema/generate-drizzle-schema.d.ts +1 -0
  45. package/dist/server-postgresql/src/services/BranchService.d.ts +47 -0
  46. package/dist/server-postgresql/src/services/EntityFetchService.d.ts +195 -0
  47. package/dist/server-postgresql/src/services/EntityPersistService.d.ts +41 -0
  48. package/dist/server-postgresql/src/services/RelationService.d.ts +92 -0
  49. package/dist/server-postgresql/src/services/entity-helpers.d.ts +24 -0
  50. package/dist/server-postgresql/src/services/entityService.d.ts +102 -0
  51. package/dist/server-postgresql/src/services/index.d.ts +4 -0
  52. package/dist/server-postgresql/src/services/realtimeService.d.ts +186 -0
  53. package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +116 -0
  54. package/dist/server-postgresql/src/websocket.d.ts +5 -0
  55. package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
  56. package/dist/types/src/controllers/auth.d.ts +117 -0
  57. package/dist/types/src/controllers/client.d.ts +58 -0
  58. package/dist/types/src/controllers/collection_registry.d.ts +44 -0
  59. package/dist/types/src/controllers/customization_controller.d.ts +54 -0
  60. package/dist/types/src/controllers/data.d.ts +141 -0
  61. package/dist/types/src/controllers/data_driver.d.ts +168 -0
  62. package/dist/types/src/controllers/database_admin.d.ts +11 -0
  63. package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
  64. package/dist/types/src/controllers/effective_role.d.ts +4 -0
  65. package/dist/types/src/controllers/index.d.ts +17 -0
  66. package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
  67. package/dist/types/src/controllers/navigation.d.ts +213 -0
  68. package/dist/types/src/controllers/registry.d.ts +51 -0
  69. package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
  70. package/dist/types/src/controllers/side_entity_controller.d.ts +89 -0
  71. package/dist/types/src/controllers/snackbar.d.ts +24 -0
  72. package/dist/types/src/controllers/storage.d.ts +173 -0
  73. package/dist/types/src/index.d.ts +4 -0
  74. package/dist/types/src/rebase_context.d.ts +101 -0
  75. package/dist/types/src/types/backend.d.ts +533 -0
  76. package/dist/types/src/types/builders.d.ts +14 -0
  77. package/dist/types/src/types/chips.d.ts +5 -0
  78. package/dist/types/src/types/collections.d.ts +812 -0
  79. package/dist/types/src/types/data_source.d.ts +64 -0
  80. package/dist/types/src/types/entities.d.ts +145 -0
  81. package/dist/types/src/types/entity_actions.d.ts +98 -0
  82. package/dist/types/src/types/entity_callbacks.d.ts +173 -0
  83. package/dist/types/src/types/entity_link_builder.d.ts +7 -0
  84. package/dist/types/src/types/entity_overrides.d.ts +9 -0
  85. package/dist/types/src/types/entity_views.d.ts +61 -0
  86. package/dist/types/src/types/export_import.d.ts +21 -0
  87. package/dist/types/src/types/index.d.ts +22 -0
  88. package/dist/types/src/types/locales.d.ts +4 -0
  89. package/dist/types/src/types/modify_collections.d.ts +5 -0
  90. package/dist/types/src/types/plugins.d.ts +225 -0
  91. package/dist/types/src/types/properties.d.ts +1091 -0
  92. package/dist/types/src/types/property_config.d.ts +70 -0
  93. package/dist/types/src/types/relations.d.ts +336 -0
  94. package/dist/types/src/types/slots.d.ts +228 -0
  95. package/dist/types/src/types/translations.d.ts +826 -0
  96. package/dist/types/src/types/user_management_delegate.d.ts +120 -0
  97. package/dist/types/src/types/websockets.d.ts +78 -0
  98. package/dist/types/src/users/index.d.ts +2 -0
  99. package/dist/types/src/users/roles.d.ts +22 -0
  100. package/dist/types/src/users/user.d.ts +46 -0
  101. package/jest-all.log +3128 -0
  102. package/jest.log +49 -0
  103. package/package.json +93 -0
  104. package/src/PostgresBackendDriver.ts +1024 -0
  105. package/src/PostgresBootstrapper.ts +232 -0
  106. package/src/auth/ensure-tables.ts +309 -0
  107. package/src/auth/services.ts +740 -0
  108. package/src/cli.ts +347 -0
  109. package/src/collections/PostgresCollectionRegistry.ts +96 -0
  110. package/src/connection.ts +62 -0
  111. package/src/data-transformer.ts +569 -0
  112. package/src/databasePoolManager.ts +84 -0
  113. package/src/history/HistoryService.ts +257 -0
  114. package/src/history/ensure-history-table.ts +45 -0
  115. package/src/index.ts +13 -0
  116. package/src/interfaces.ts +60 -0
  117. package/src/schema/auth-schema.ts +146 -0
  118. package/src/schema/generate-drizzle-schema-logic.ts +618 -0
  119. package/src/schema/generate-drizzle-schema.ts +151 -0
  120. package/src/services/BranchService.ts +237 -0
  121. package/src/services/EntityFetchService.ts +1447 -0
  122. package/src/services/EntityPersistService.ts +351 -0
  123. package/src/services/RelationService.ts +1012 -0
  124. package/src/services/entity-helpers.ts +121 -0
  125. package/src/services/entityService.ts +209 -0
  126. package/src/services/index.ts +13 -0
  127. package/src/services/realtimeService.ts +1005 -0
  128. package/src/utils/drizzle-conditions.ts +999 -0
  129. package/src/websocket.ts +487 -0
  130. package/test/auth-services.test.ts +569 -0
  131. package/test/branchService.test.ts +357 -0
  132. package/test/drizzle-conditions.test.ts +895 -0
  133. package/test/entityService.errors.test.ts +352 -0
  134. package/test/entityService.relations.test.ts +912 -0
  135. package/test/entityService.subcollection-search.test.ts +516 -0
  136. package/test/entityService.test.ts +977 -0
  137. package/test/generate-drizzle-schema.test.ts +795 -0
  138. package/test/historyService.test.ts +126 -0
  139. package/test/postgresDataDriver.test.ts +556 -0
  140. package/test/realtimeService.test.ts +276 -0
  141. package/test/relations.test.ts +662 -0
  142. package/test_drizzle_mock.js +3 -0
  143. package/test_find_changed.mjs +30 -0
  144. package/test_output.txt +3145 -0
  145. package/tsconfig.json +49 -0
  146. package/tsconfig.prod.json +20 -0
  147. package/vite.config.ts +82 -0
@@ -0,0 +1,1012 @@
1
+ import { and, eq, inArray, sql, SQL } from "drizzle-orm";
2
+ import { AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
3
+ import { DrizzleClient } from "../interfaces";
4
+ import { Entity, EntityCollection, FilterValues, Relation } from "@rebasepro/types";
5
+ import { getTableName, resolveCollectionRelations } 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 { parseDataFromServer } from "../data-transformer";
15
+ import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
16
+
17
+ /**
18
+ * Service for handling all relation-related operations.
19
+ * Handles fetching, updating, and managing entity relations.
20
+ */
21
+ export class RelationService {
22
+ constructor(private db: DrizzleClient, private registry: PostgresCollectionRegistry) { }
23
+
24
+ /**
25
+ * Fetch entities related to a parent entity through a specific relation
26
+ */
27
+ async fetchRelatedEntities<M extends Record<string, any>>(
28
+ parentCollectionPath: string,
29
+ parentEntityId: string | number,
30
+ relationKey: string,
31
+ options: {
32
+ filter?: FilterValues<Extract<keyof M, string>>;
33
+ orderBy?: string;
34
+ order?: "desc" | "asc";
35
+ limit?: number;
36
+ startAfter?: Record<string, unknown>;
37
+ searchString?: string;
38
+ databaseId?: string;
39
+ } = {}
40
+ ): Promise<Entity<M>[]> {
41
+ const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
42
+ const resolvedRelations = resolveCollectionRelations(parentCollection as import("@rebasepro/types").PostgresCollection<any, any>);
43
+ const relation = resolvedRelations[relationKey];
44
+
45
+ if (!relation) {
46
+ throw new Error(`Relation '${relationKey}' not found in collection '${parentCollectionPath}'`);
47
+ }
48
+
49
+ return this.fetchEntitiesUsingJoins<M>(parentCollection, parentEntityId, relation, options);
50
+ }
51
+
52
+ /**
53
+ * Fetch entities using join paths for complex relations
54
+ */
55
+ async fetchEntitiesUsingJoins<M extends Record<string, any>>(
56
+ parentCollection: EntityCollection,
57
+ parentEntityId: string | number,
58
+ relation: Relation,
59
+ options: {
60
+ filter?: FilterValues<Extract<keyof M, string>>;
61
+ orderBy?: string;
62
+ order?: "desc" | "asc";
63
+ limit?: number;
64
+ startAfter?: Record<string, unknown>;
65
+ searchString?: string;
66
+ databaseId?: string;
67
+ } = {}
68
+ ): Promise<Entity<M>[]> {
69
+ const targetCollection = relation.target();
70
+ const targetTable = getTableForCollection(targetCollection, this.registry);
71
+ const idInfo = getPrimaryKeys(targetCollection, this.registry);
72
+ const idField = targetTable[idInfo[0].fieldName as keyof typeof targetTable] as AnyPgColumn;
73
+
74
+ const parentPks = getPrimaryKeys(parentCollection, this.registry);
75
+ const parentIdInfo = parentPks[0];
76
+ const parsedParentIdObj = parseIdValues(parentEntityId, parentPks);
77
+ const parsedParentId = parsedParentIdObj[parentIdInfo.fieldName];
78
+ const parentTable = this.registry.getTable(getTableName(parentCollection));
79
+ if (!parentTable) throw new Error("Parent table not found");
80
+ const parentIdCol = parentTable[parentIdInfo.fieldName as keyof typeof parentTable] as AnyPgColumn;
81
+
82
+ // Handle join path relations
83
+ if (relation.joinPath && relation.joinPath.length > 0) {
84
+ let query = this.db.select().from(parentTable).$dynamic();
85
+ let currentTable = parentTable;
86
+
87
+ // Apply each join in the path
88
+ for (const join of relation.joinPath) {
89
+ const joinTable = this.registry.getTable(join.table);
90
+ if (!joinTable) {
91
+ throw new Error(`Join table not found: ${join.table}`);
92
+ }
93
+
94
+ const fromColumn = Array.isArray(join.on.from) ? join.on.from[0] : join.on.from;
95
+ const toColumn = Array.isArray(join.on.to) ? join.on.to[0] : join.on.to;
96
+
97
+ const fromParts = fromColumn.split(".");
98
+ const toParts = toColumn.split(".");
99
+
100
+ const fromColName = fromParts[fromParts.length - 1];
101
+ const toColName = toParts[toParts.length - 1];
102
+
103
+ const fromCol = currentTable[fromColName as keyof typeof currentTable] as AnyPgColumn;
104
+ const toCol = joinTable[toColName as keyof typeof joinTable] as AnyPgColumn;
105
+
106
+ if (!fromCol || !toCol) {
107
+ throw new Error(`Join columns not found: ${fromColumn} -> ${toColumn}`);
108
+ }
109
+
110
+ // @ts-expect-error Drizzle mutates base query generic on innerJoin
111
+ query = query.innerJoin(joinTable, eq(fromCol, toCol));
112
+ currentTable = joinTable;
113
+ }
114
+
115
+ // Add where condition for the parent entity
116
+ const parentIdField = parentTable[getPrimaryKeys(parentCollection, this.registry)[0].fieldName as keyof typeof parentTable] as AnyPgColumn;
117
+ query = query.where(eq(parentIdField, parsedParentId));
118
+
119
+ if (options.limit) {
120
+ query = query.limit(options.limit);
121
+ }
122
+
123
+ const results = await query;
124
+ const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
125
+
126
+ // Process results
127
+ const entities: Entity<M>[] = [];
128
+ for (const row of results as Array<Record<string, unknown>>) {
129
+ const targetEntity = (row[targetTableName] as Record<string, unknown>) || row;
130
+ const entityId = targetEntity[idInfo[0].fieldName as string];
131
+ const parsedValues = await parseDataFromServer(targetEntity, targetCollection, this.db, this.registry);
132
+
133
+ entities.push({
134
+ id: entityId?.toString() || "",
135
+ path: targetCollection.slug,
136
+ values: parsedValues as M
137
+ });
138
+ }
139
+
140
+ return entities;
141
+ }
142
+
143
+ // Handle other relation types
144
+ let query = this.db.select().from(targetTable).$dynamic();
145
+
146
+ // Build additional filter conditions
147
+ const additionalFilters: SQL[] = [];
148
+
149
+ // Handle search conditions if searchString is provided
150
+ if (options.searchString) {
151
+ const searchConditions = DrizzleConditionBuilder.buildSearchConditions(
152
+ options.searchString,
153
+ targetCollection.properties,
154
+ targetTable
155
+ );
156
+
157
+ if (searchConditions.length === 0) {
158
+ // No searchable fields found, return empty results
159
+ return [];
160
+ }
161
+
162
+ const searchCombined = DrizzleConditionBuilder.combineConditionsWithOr(searchConditions);
163
+ if (searchCombined) {
164
+ additionalFilters.push(searchCombined);
165
+ }
166
+ }
167
+
168
+ // Use unified relation query builder
169
+ // @ts-expect-error buildRelationQuery uses dynamic queries
170
+ query = DrizzleConditionBuilder.buildRelationQuery(
171
+ query,
172
+ relation,
173
+ parsedParentId,
174
+ targetTable,
175
+ parentTable,
176
+ parentIdCol,
177
+ idField,
178
+ this.registry,
179
+ additionalFilters
180
+ );
181
+
182
+ if (options.limit) {
183
+ query = query.limit(options.limit);
184
+ }
185
+
186
+ const results = await query;
187
+
188
+ // Process results - ensure results is iterable
189
+ if (!results || !Array.isArray(results)) {
190
+ return [];
191
+ }
192
+
193
+ const entities: Entity<M>[] = [];
194
+ for (const row of results) {
195
+ const targetEntity = row[getTableName(targetCollection)] || row;
196
+ const entityId = targetEntity[idInfo[0].fieldName];
197
+ const parsedValues = await parseDataFromServer(targetEntity, targetCollection, this.db, this.registry);
198
+
199
+ entities.push({
200
+ id: entityId?.toString() || "",
201
+ path: targetCollection.slug,
202
+ values: parsedValues as M
203
+ });
204
+ }
205
+
206
+ return entities;
207
+ }
208
+
209
+ /**
210
+ * Count related entities for a parent entity
211
+ */
212
+ async countRelatedEntities<M extends Record<string, any>>(
213
+ parentCollectionPath: string,
214
+ parentEntityId: string | number,
215
+ relationKey: string,
216
+ options: { filter?: FilterValues<Extract<keyof M, string>>; databaseId?: string } = {}
217
+ ): Promise<number> {
218
+ const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
219
+ const resolvedRelations = resolveCollectionRelations(parentCollection as import("@rebasepro/types").PostgresCollection<any, any>);
220
+ const relation = resolvedRelations[relationKey];
221
+ if (!relation) throw new Error(`Relation '${relationKey}' not found in collection '${parentCollectionPath}'`);
222
+
223
+ const targetCollection = relation.target();
224
+ const targetTable = getTableForCollection(targetCollection, this.registry);
225
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
226
+ const targetIdInfo = targetPks[0];
227
+ const targetIdField = targetTable[targetIdInfo.fieldName as keyof typeof targetTable] as AnyPgColumn;
228
+
229
+ const parentPks = getPrimaryKeys(parentCollection, this.registry);
230
+ const parentIdInfo = parentPks[0];
231
+ const parsedParentIdObj = parseIdValues(parentEntityId, parentPks);
232
+ const parsedParentId = parsedParentIdObj[parentIdInfo.fieldName];
233
+ const parentTable = this.registry.getTable(getTableName(parentCollection));
234
+ if (!parentTable) throw new Error("Parent table not found");
235
+ const parentIdCol = parentTable[parentIdInfo.fieldName as keyof typeof parentTable] as AnyPgColumn;
236
+
237
+ // Start count with distinct to avoid duplicates from junction tables
238
+ let query = this.db.select({ count: sql<number>`count(distinct ${targetIdField})` }).from(targetTable).$dynamic();
239
+
240
+ // Build additional filter conditions
241
+ const additionalFilters: SQL[] = [];
242
+
243
+ // Use unified count query builder from DrizzleConditionBuilder
244
+ query = DrizzleConditionBuilder.buildRelationCountQuery(
245
+ query,
246
+ relation,
247
+ parsedParentId,
248
+ targetTable,
249
+ parentTable,
250
+ parentIdCol,
251
+ targetIdField,
252
+ this.registry,
253
+ additionalFilters
254
+ );
255
+
256
+ const result = await query;
257
+ return Number(result[0]?.count || 0);
258
+ }
259
+
260
+ /**
261
+ * Batch fetch related entities for multiple parent entities to avoid N+1 queries
262
+ */
263
+ async batchFetchRelatedEntities(
264
+ parentCollectionPath: string,
265
+ parentEntityIds: (string | number)[],
266
+ _relationKey: string,
267
+ relation: Relation
268
+ ): Promise<Map<string | number, Entity<Record<string, unknown>>>> {
269
+ if (parentEntityIds.length === 0) return new Map();
270
+
271
+ const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
272
+ const targetCollection = relation.target();
273
+ const targetTable = getTableForCollection(targetCollection, this.registry);
274
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
275
+ const targetIdInfo = targetPks[0];
276
+ const targetIdField = targetTable[targetIdInfo.fieldName as keyof typeof targetTable] as AnyPgColumn;
277
+
278
+ const parentPks = getPrimaryKeys(parentCollection, this.registry);
279
+ const parentIdInfo = parentPks[0];
280
+ const parentTable = this.registry.getTable(getTableName(parentCollection));
281
+ if (!parentTable) throw new Error("Parent table not found");
282
+ const parentIdCol = parentTable[parentIdInfo.fieldName as keyof typeof parentTable] as AnyPgColumn;
283
+
284
+ // Parse all parent IDs once
285
+ const parsedParentIds = parentEntityIds.map(id => parseIdValues(id, parentPks)[parentIdInfo.fieldName]);
286
+
287
+ // Handle join path relations with batching
288
+ if (relation.joinPath && relation.joinPath.length > 0) {
289
+ let query = this.db.select().from(parentTable).$dynamic();
290
+ let currentTable = parentTable;
291
+
292
+ // Apply each join in the path
293
+ for (const join of relation.joinPath) {
294
+ const joinTable = this.registry.getTable(join.table);
295
+ if (!joinTable) {
296
+ throw new Error(`Join table not found: ${join.table}`);
297
+ }
298
+
299
+ const fromColumn = Array.isArray(join.on.from) ? join.on.from[0] : join.on.from;
300
+ const toColumn = Array.isArray(join.on.to) ? join.on.to[0] : join.on.to;
301
+
302
+ const fromParts = fromColumn.split(".");
303
+ const toParts = toColumn.split(".");
304
+
305
+ const fromColName = fromParts[fromParts.length - 1];
306
+ const toColName = toParts[toParts.length - 1];
307
+
308
+ const fromCol = currentTable[fromColName as keyof typeof currentTable] as AnyPgColumn;
309
+ const toCol = joinTable[toColName as keyof typeof joinTable] as AnyPgColumn;
310
+
311
+ if (!fromCol || !toCol) {
312
+ throw new Error(`Join columns not found: ${fromColumn} -> ${toColumn}`);
313
+ }
314
+
315
+ // @ts-expect-error Drizzle mutates base query generic on innerJoin
316
+ query = query.innerJoin(joinTable, eq(fromCol, toCol));
317
+ currentTable = joinTable;
318
+ }
319
+
320
+ // Add where condition for ALL parent entities at once
321
+ const parentIdField = parentTable[getPrimaryKeys(parentCollection, this.registry)[0].fieldName as keyof typeof parentTable] as AnyPgColumn;
322
+ query = query.where(inArray(parentIdField, parsedParentIds));
323
+
324
+ const results = await query;
325
+ const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
326
+ const resultMap = new Map<string | number, Entity<Record<string, unknown>>>();
327
+
328
+ // Group results by parent ID
329
+ results.forEach((row: Record<string, unknown>) => {
330
+ const parentEntity = (row[getTableName(parentCollection)] || row) as Record<string, unknown>;
331
+ const targetEntity = (row[targetTableName] || row) as Record<string, unknown>;
332
+ const parentId = parentEntity[parentIdInfo.fieldName] as string | number;
333
+
334
+ resultMap.set(parentId, {
335
+ id: String(targetEntity[targetIdInfo.fieldName]),
336
+ path: targetCollection.slug,
337
+ values: targetEntity
338
+ });
339
+ });
340
+
341
+ return resultMap;
342
+ }
343
+
344
+ // Handle other relation types with batching
345
+ let query = this.db.select().from(targetTable).$dynamic();
346
+
347
+ // Build the relation query with ALL parent IDs
348
+ // @ts-expect-error buildRelationQuery uses dynamic queries
349
+ query = DrizzleConditionBuilder.buildRelationQuery(
350
+ query,
351
+ relation,
352
+ parsedParentIds, // Pass array instead of single ID
353
+ targetTable,
354
+ parentTable,
355
+ parentIdCol,
356
+ targetIdField,
357
+ this.registry,
358
+ []
359
+ );
360
+
361
+ const results = await query;
362
+ const resultMap = new Map<string | number, Entity<Record<string, unknown>>>();
363
+
364
+ // Map results back to parent entities
365
+ results.forEach((row: Record<string, unknown>) => {
366
+ const targetEntity = (row[getTableName(targetCollection)] || row) as Record<string, unknown>;
367
+
368
+ // Determine the parent ID this result belongs to based on the relation type
369
+ let parentId: string | number | undefined;
370
+
371
+ if (relation.direction === "inverse" && relation.foreignKeyOnTarget) {
372
+ parentId = targetEntity[relation.foreignKeyOnTarget] as string | number | undefined;
373
+ } else if (relation.direction === "inverse" && relation.cardinality === "one" && relation.inverseRelationName) {
374
+ const inferredForeignKeyName = `${relation.inverseRelationName}_id`;
375
+ parentId = targetEntity[inferredForeignKeyName] as string | number | undefined;
376
+ } else if (relation.direction === "owning" && relation.localKey) {
377
+ for (const parsedParentId of parsedParentIds) {
378
+ if (!resultMap.has(parsedParentId)) {
379
+ parentId = parsedParentId;
380
+ break;
381
+ }
382
+ }
383
+ }
384
+
385
+ if (parentId !== undefined && parsedParentIds.includes(parentId)) {
386
+ resultMap.set(parentId, {
387
+ id: String(targetEntity[targetIdInfo.fieldName]),
388
+ path: targetCollection.slug,
389
+ values: targetEntity
390
+ });
391
+ }
392
+ });
393
+
394
+ return resultMap;
395
+ }
396
+
397
+ /**
398
+ * Update many-to-many and junction relations
399
+ */
400
+ async updateRelationsUsingJoins<M extends Record<string, any>>(
401
+ tx: DrizzleClient,
402
+ collection: EntityCollection,
403
+ entityId: string | number,
404
+ relationValues: Partial<M>
405
+ ) {
406
+ const resolvedRelations = resolveCollectionRelations(collection as import("@rebasepro/types").PostgresCollection<any, any>);
407
+
408
+ for (const [key, value] of Object.entries(relationValues)) {
409
+ const relation = resolvedRelations[key];
410
+ if (!relation || relation.cardinality !== "many") continue;
411
+
412
+ const targetEntityIds = (value && Array.isArray(value)) ? value.map((rel: { id: string | number }) => rel.id) : [];
413
+ const targetCollection = relation.target();
414
+
415
+ // Use joinPath if available
416
+ if (relation.joinPath && relation.joinPath.length > 0) {
417
+ const parentTableName = getTableName(collection);
418
+ const targetTableName = getTableName(targetCollection);
419
+
420
+ let junctionTable: PgTable | undefined = undefined;
421
+ let sourceJunctionColumn: AnyPgColumn | null = null;
422
+ let targetJunctionColumn: AnyPgColumn | null = null;
423
+
424
+ const junctionTableName = relation.joinPath.find(step =>
425
+ step.table !== parentTableName && step.table !== targetTableName
426
+ )?.table;
427
+
428
+ if (junctionTableName) {
429
+ junctionTable = this.registry.getTable(junctionTableName);
430
+
431
+ if (junctionTable) {
432
+ for (const joinStep of relation.joinPath) {
433
+ const fromTable = DrizzleConditionBuilder.getTableNamesFromColumns(joinStep.on.from)[0];
434
+ const toTable = DrizzleConditionBuilder.getTableNamesFromColumns(joinStep.on.to)[0];
435
+
436
+ if (fromTable === parentTableName && toTable === junctionTableName) {
437
+ const columnNames = DrizzleConditionBuilder.getColumnNamesFromColumns(joinStep.on.to);
438
+ sourceJunctionColumn = junctionTable[columnNames[0] as keyof typeof junctionTable] as AnyPgColumn;
439
+ } else if (fromTable === junctionTableName && toTable === parentTableName) {
440
+ const columnNames = DrizzleConditionBuilder.getColumnNamesFromColumns(joinStep.on.from);
441
+ sourceJunctionColumn = junctionTable[columnNames[0] as keyof typeof junctionTable] as AnyPgColumn;
442
+ }
443
+
444
+ if (fromTable === junctionTableName && toTable === targetTableName) {
445
+ const columnNames = DrizzleConditionBuilder.getColumnNamesFromColumns(joinStep.on.from);
446
+ targetJunctionColumn = junctionTable[columnNames[0] as keyof typeof junctionTable] as AnyPgColumn;
447
+ } else if (fromTable === targetTableName && toTable === junctionTableName) {
448
+ const columnNames = DrizzleConditionBuilder.getColumnNamesFromColumns(joinStep.on.to);
449
+ targetJunctionColumn = junctionTable[columnNames[0] as keyof typeof junctionTable] as AnyPgColumn;
450
+ }
451
+ }
452
+ }
453
+ }
454
+
455
+ if (!junctionTable || !sourceJunctionColumn || !targetJunctionColumn) {
456
+ console.warn(`Could not determine junction table for relation '${key}' in collection '${collection.slug}'`);
457
+ continue;
458
+ }
459
+
460
+ const parentPks = getPrimaryKeys(collection, this.registry);
461
+ const parentIdInfo = parentPks[0];
462
+ const parsedParentIdObj = parseIdValues(entityId, parentPks);
463
+ const parsedParentId = parsedParentIdObj[parentIdInfo.fieldName];
464
+
465
+ // Delete existing relations for this entity
466
+ await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedParentId));
467
+
468
+ if (targetEntityIds.length > 0) {
469
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
470
+ const targetIdInfo = targetPks[0];
471
+ const parsedTargetIds = targetEntityIds.map(id => parseIdValues(id, targetPks)[targetIdInfo.fieldName]);
472
+
473
+ const newLinks = parsedTargetIds.map(targetId => ({
474
+ [sourceJunctionColumn.name]: parsedParentId,
475
+ [targetJunctionColumn.name]: targetId
476
+ }));
477
+
478
+ if (newLinks.length > 0) {
479
+ await tx.insert(junctionTable).values(newLinks);
480
+ }
481
+ }
482
+ } else if (relation.through && relation.cardinality === "many" && relation.direction === "owning") {
483
+ // Handle many-to-many relations with junction table using 'through' property
484
+ const junctionTable = this.registry.getTable(relation.through.table);
485
+ if (!junctionTable) {
486
+ console.warn(`Junction table '${relation.through.table}' not found for relation '${key}' in collection '${collection.slug}'`);
487
+ continue;
488
+ }
489
+
490
+ const sourceJunctionColumn = junctionTable[relation.through.sourceColumn as keyof typeof junctionTable] as AnyPgColumn;
491
+ const targetJunctionColumn = junctionTable[relation.through.targetColumn as keyof typeof junctionTable] as AnyPgColumn;
492
+
493
+ if (!sourceJunctionColumn || !targetJunctionColumn) {
494
+ console.warn(`Junction columns not found for relation '${key}'`);
495
+ continue;
496
+ }
497
+
498
+ const parentPks = getPrimaryKeys(collection, this.registry);
499
+ const parentIdInfo = parentPks[0];
500
+ const parsedParentIdObj = parseIdValues(entityId, parentPks);
501
+ const parsedParentId = parsedParentIdObj[parentIdInfo.fieldName];
502
+
503
+ // Delete existing relations for this entity
504
+ await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedParentId));
505
+
506
+ if (targetEntityIds.length > 0) {
507
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
508
+ const targetIdInfo = targetPks[0];
509
+ const parsedTargetIds = targetEntityIds.map(id => parseIdValues(id, targetPks)[targetIdInfo.fieldName]);
510
+
511
+ const newLinks = parsedTargetIds.map(targetId => ({
512
+ [sourceJunctionColumn.name]: parsedParentId,
513
+ [targetJunctionColumn.name]: targetId
514
+ }));
515
+
516
+ if (newLinks.length > 0) {
517
+ await tx.insert(junctionTable).values(newLinks);
518
+ }
519
+ }
520
+ } else if (relation.cardinality === "many" && relation.direction === "inverse" && relation.foreignKeyOnTarget) {
521
+ // Handle one-to-many (inverse) by updating target FK to point to parent
522
+ const targetTable = getTableForCollection(targetCollection, this.registry);
523
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
524
+ const targetIdInfo = targetPks[0];
525
+ const targetIdCol = targetTable[targetIdInfo.fieldName as keyof typeof targetTable] as AnyPgColumn;
526
+ const fkCol = targetTable[relation.foreignKeyOnTarget as keyof typeof targetTable] as AnyPgColumn;
527
+
528
+ if (!fkCol || !targetIdCol) {
529
+ console.warn(`Invalid inverse-many config for relation '${key}' in collection '${collection.slug}'`);
530
+ continue;
531
+ }
532
+
533
+ const parentPks = getPrimaryKeys(collection, this.registry);
534
+ const parentIdInfo = parentPks[0];
535
+ const parsedParentIdObj = parseIdValues(entityId, parentPks);
536
+ const parsedParentId = parsedParentIdObj[parentIdInfo.fieldName];
537
+
538
+ // Clear existing links not in the new set
539
+ if (targetEntityIds.length > 0) {
540
+ const parsedTargetIds = targetEntityIds.map(id => parseIdValues(id, targetPks)[targetIdInfo.fieldName]);
541
+ await tx
542
+ .update(targetTable)
543
+ .set({ [relation.foreignKeyOnTarget]: null })
544
+ .where(and(eq(fkCol, parsedParentId), sql`${targetIdCol} NOT IN (${sql.join(parsedTargetIds)})`));
545
+
546
+ // Set FK for the provided targets
547
+ await tx
548
+ .update(targetTable)
549
+ .set({ [relation.foreignKeyOnTarget]: parsedParentId })
550
+ .where(inArray(targetIdCol as AnyPgColumn, parsedTargetIds as unknown[]));
551
+ } else {
552
+ // If empty array provided, clear all existing links for this parent
553
+ await tx
554
+ .update(targetTable)
555
+ .set({ [relation.foreignKeyOnTarget]: null })
556
+ .where(eq(fkCol, parsedParentId));
557
+ }
558
+ } else {
559
+ console.warn(`Many relation '${key}' in collection '${collection.slug}' lacks write configuration and will be skipped during save.`);
560
+ }
561
+ }
562
+ }
563
+
564
+ /**
565
+ * Update inverse relations (where FK is on the target table)
566
+ */
567
+ async updateInverseRelations(
568
+ tx: DrizzleClient,
569
+ sourceCollection: EntityCollection,
570
+ sourceEntityId: string | number,
571
+ inverseRelationUpdates: Array<{
572
+ relationKey: string;
573
+ relation: Relation;
574
+ newValue: unknown;
575
+ currentEntityId?: string | number;
576
+ }>
577
+ ) {
578
+ for (const update of inverseRelationUpdates) {
579
+ const { relation, newValue } = update;
580
+
581
+ try {
582
+ const targetCollection = relation.target();
583
+ const targetTable = getTableForCollection(targetCollection, this.registry);
584
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
585
+ const targetIdInfo = targetPks[0];
586
+ const sourcePks = getPrimaryKeys(sourceCollection, this.registry);
587
+ const sourceIdInfo = sourcePks[0];
588
+
589
+ // Handle inverse relations with joinPath
590
+ if (relation.direction === "inverse" && relation.joinPath && relation.joinPath.length > 0) {
591
+ await this.updateInverseJoinPathRelation(
592
+ tx,
593
+ sourceCollection,
594
+ sourceEntityId,
595
+ targetCollection,
596
+ relation,
597
+ newValue
598
+ );
599
+ continue;
600
+ }
601
+
602
+ // Check if this is a many-to-many inverse relation
603
+ if (relation.cardinality === "many" && relation.direction === "inverse") {
604
+ const targetCollectionRelations = resolveCollectionRelations(targetCollection as import("@rebasepro/types").PostgresCollection<any, any>);
605
+ let junctionInfo: { table: string; sourceColumn: string; targetColumn: string } | null = null;
606
+
607
+ for (const [relationKey, targetRelation] of Object.entries(targetCollectionRelations)) {
608
+ if (targetRelation.cardinality === "many" &&
609
+ targetRelation.direction === "owning" &&
610
+ targetRelation.through &&
611
+ (targetRelation.relationName === relation.inverseRelationName || relationKey === relation.inverseRelationName)) {
612
+ junctionInfo = {
613
+ table: targetRelation.through.table,
614
+ sourceColumn: targetRelation.through.targetColumn,
615
+ targetColumn: targetRelation.through.sourceColumn
616
+ };
617
+ break;
618
+ }
619
+ }
620
+
621
+ if (junctionInfo) {
622
+ await this.updateManyToManyInverseRelation(
623
+ tx,
624
+ sourceCollection,
625
+ sourceEntityId,
626
+ targetCollection,
627
+ relation,
628
+ newValue,
629
+ junctionInfo
630
+ );
631
+ continue;
632
+ }
633
+ }
634
+
635
+ // Handle simple inverse relations
636
+ if (!relation.foreignKeyOnTarget) {
637
+ console.warn(`Inverse relation '${relation.relationName}' is missing foreignKeyOnTarget property. Skipping.`);
638
+ continue;
639
+ }
640
+
641
+ const foreignKeyColumn = targetTable[relation.foreignKeyOnTarget! as keyof typeof targetTable] as AnyPgColumn;
642
+ if (!foreignKeyColumn) {
643
+ console.warn(`Foreign key column '${relation.foreignKeyOnTarget}' not found in target table for relation '${relation.relationName}'`);
644
+ continue;
645
+ }
646
+
647
+ const parsedSourceIdObj = parseIdValues(sourceEntityId, sourcePks);
648
+ const parsedSourceId = parsedSourceIdObj[sourceIdInfo.fieldName];
649
+
650
+ if (newValue === null || newValue === undefined) {
651
+ await tx
652
+ .update(targetTable)
653
+ .set({ [relation.foreignKeyOnTarget!]: null })
654
+ .where(eq(foreignKeyColumn, parsedSourceId));
655
+ } else {
656
+ const parsedNewTargetIdObj = parseIdValues(newValue as string | number, targetPks);
657
+ const parsedNewTargetId = parsedNewTargetIdObj[targetIdInfo.fieldName];
658
+ const targetIdField = targetTable[targetIdInfo.fieldName as keyof typeof targetTable] as AnyPgColumn;
659
+
660
+ // First, clear any existing FK that points to this source entity
661
+ await tx
662
+ .update(targetTable)
663
+ .set({ [relation.foreignKeyOnTarget!]: null })
664
+ .where(eq(foreignKeyColumn, parsedSourceId));
665
+
666
+ // Then, update the new target entity to point to this source entity
667
+ await tx
668
+ .update(targetTable)
669
+ .set({ [relation.foreignKeyOnTarget!]: parsedSourceId })
670
+ .where(eq(targetIdField, parsedNewTargetId));
671
+ }
672
+ } catch (e) {
673
+ console.warn(`Failed to update inverse relation '${relation.relationName}':`, e);
674
+ }
675
+ }
676
+ }
677
+
678
+ /**
679
+ * Handle inverse relations with joinPath
680
+ */
681
+ private async updateInverseJoinPathRelation(
682
+ tx: DrizzleClient,
683
+ sourceCollection: EntityCollection,
684
+ sourceEntityId: string | number,
685
+ targetCollection: EntityCollection,
686
+ relation: Relation,
687
+ newValue: unknown
688
+ ) {
689
+ try {
690
+ if (!relation.joinPath || relation.joinPath.length === 0) {
691
+ console.warn(`Inverse relation '${relation.relationName}' missing joinPath`);
692
+ return;
693
+ }
694
+
695
+ const sourceTableName = getTableName(sourceCollection);
696
+ const targetTableName = getTableName(targetCollection);
697
+
698
+ // Find intermediate tables that are neither source nor target
699
+ const intermediateTables = relation.joinPath
700
+ .map(step => step.table)
701
+ .filter(table => table !== sourceTableName && table !== targetTableName);
702
+
703
+ // If there's exactly one intermediate table, it's likely a junction table for many-to-many
704
+ if (intermediateTables.length === 1 && relation.cardinality === "many") {
705
+ const junctionTableName = intermediateTables[0];
706
+ const junctionTable = this.registry.getTable(junctionTableName);
707
+
708
+ if (!junctionTable) {
709
+ console.warn(`Junction table '${junctionTableName}' not found for inverse joinPath relation '${relation.relationName}'`);
710
+ return;
711
+ }
712
+
713
+ let sourceJunctionColumn: AnyPgColumn | null = null;
714
+ let targetJunctionColumn: AnyPgColumn | null = null;
715
+
716
+ for (const step of relation.joinPath) {
717
+ if (step.table === junctionTableName) {
718
+ const fromTable = DrizzleConditionBuilder.getTableNamesFromColumns(step.on.from)[0];
719
+ const toColumnNames = DrizzleConditionBuilder.getColumnNamesFromColumns(step.on.to);
720
+ const fromColumnNames = DrizzleConditionBuilder.getColumnNamesFromColumns(step.on.from);
721
+
722
+ if (fromTable === sourceTableName) {
723
+ sourceJunctionColumn = junctionTable[toColumnNames[0] as keyof typeof junctionTable] as AnyPgColumn;
724
+ } else if (fromTable === targetTableName) {
725
+ targetJunctionColumn = junctionTable[toColumnNames[0] as keyof typeof junctionTable] as AnyPgColumn;
726
+ } else {
727
+ const toTable = DrizzleConditionBuilder.getTableNamesFromColumns(step.on.to)[0];
728
+ if (toTable === sourceTableName) {
729
+ sourceJunctionColumn = junctionTable[fromColumnNames[0] as keyof typeof junctionTable] as AnyPgColumn;
730
+ } else if (toTable === targetTableName) {
731
+ targetJunctionColumn = junctionTable[fromColumnNames[0] as keyof typeof junctionTable] as AnyPgColumn;
732
+ }
733
+ }
734
+ }
735
+ }
736
+
737
+ if (!sourceJunctionColumn || !targetJunctionColumn) {
738
+ console.warn(`Could not determine junction columns for inverse joinPath relation '${relation.relationName}'`);
739
+ return;
740
+ }
741
+
742
+ // Perform the junction table update
743
+ const sourcePks = getPrimaryKeys(sourceCollection, this.registry);
744
+ const sourceIdInfo = sourcePks[0];
745
+ const parsedSourceIdObj = parseIdValues(sourceEntityId, sourcePks);
746
+ const parsedSourceId = parsedSourceIdObj[sourceIdInfo.fieldName];
747
+
748
+ // Clear existing entries for this source entity
749
+ await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedSourceId));
750
+
751
+ // Add new entries if newValue is provided
752
+ if (newValue && Array.isArray(newValue) && newValue.length > 0) {
753
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
754
+ const targetIdInfo = targetPks[0];
755
+ const targetEntityIds = (newValue as Array<{ id: string | number } | string | number>).map((rel) => typeof rel === 'object' && rel !== null ? rel.id : rel);
756
+ const parsedTargetIds = targetEntityIds.map(id => parseIdValues(id, targetPks)[targetIdInfo.fieldName]);
757
+
758
+ const newLinks = parsedTargetIds.map(targetId => ({
759
+ [sourceJunctionColumn!.name]: parsedSourceId,
760
+ [targetJunctionColumn!.name]: targetId
761
+ }));
762
+
763
+ if (newLinks.length > 0) {
764
+ await tx.insert(junctionTable).values(newLinks);
765
+ }
766
+ } else if (newValue && !Array.isArray(newValue)) {
767
+ // Single value for one-to-one
768
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
769
+ const targetIdInfo = targetPks[0];
770
+ const targetId = typeof newValue === 'object' && newValue !== null ? (newValue as Record<string, unknown>).id as string | number : newValue as string | number;
771
+ const parsedTargetIdObj = parseIdValues(targetId, targetPks);
772
+ const parsedTargetId = parsedTargetIdObj[targetIdInfo.fieldName];
773
+
774
+ const newLink = {
775
+ [sourceJunctionColumn.name]: parsedSourceId,
776
+ [targetJunctionColumn.name]: parsedTargetId
777
+ };
778
+
779
+ await tx.insert(junctionTable).values(newLink);
780
+ }
781
+ }
782
+ } catch (error) {
783
+ console.error(`Failed to update inverse joinPath relation '${relation.relationName}':`, error);
784
+ throw error;
785
+ }
786
+ }
787
+
788
+ /**
789
+ * Handle many-to-many inverse relation updates using junction tables
790
+ */
791
+ private async updateManyToManyInverseRelation(
792
+ tx: DrizzleClient,
793
+ sourceCollection: EntityCollection,
794
+ sourceEntityId: string | number,
795
+ targetCollection: EntityCollection,
796
+ relation: Relation,
797
+ newValue: unknown,
798
+ junctionInfo: { table: string; sourceColumn: string; targetColumn: string }
799
+ ) {
800
+ try {
801
+ const junctionTable = this.registry.getTable(junctionInfo.table);
802
+ if (!junctionTable) {
803
+ console.warn(`Junction table '${junctionInfo.table}' not found for many-to-many inverse relation '${relation.relationName}'`);
804
+ return;
805
+ }
806
+
807
+ const sourceJunctionColumn = junctionTable[junctionInfo.sourceColumn as keyof typeof junctionTable] as AnyPgColumn;
808
+ const targetJunctionColumn = junctionTable[junctionInfo.targetColumn as keyof typeof junctionTable] as AnyPgColumn;
809
+
810
+ if (!sourceJunctionColumn || !targetJunctionColumn) {
811
+ console.warn(`Junction columns not found for relation '${relation.relationName}'`);
812
+ return;
813
+ }
814
+
815
+ const sourcePks = getPrimaryKeys(sourceCollection, this.registry);
816
+ const sourceIdInfo = sourcePks[0];
817
+ const parsedSourceIdObj = parseIdValues(sourceEntityId, sourcePks);
818
+ const parsedSourceId = parsedSourceIdObj[sourceIdInfo.fieldName];
819
+
820
+ // Clear existing entries for this source entity
821
+ await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedSourceId));
822
+
823
+ // Add new entries if newValue is provided
824
+ if (newValue && Array.isArray(newValue) && newValue.length > 0) {
825
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
826
+ const targetIdInfo = targetPks[0];
827
+ const targetEntityIds = (newValue as Array<{ id: string | number }>).map((rel) => rel.id);
828
+ const parsedTargetIds = targetEntityIds.map(id => parseIdValues(id, targetPks)[targetIdInfo.fieldName]);
829
+
830
+ const newLinks = parsedTargetIds.map(targetId => ({
831
+ [sourceJunctionColumn.name]: parsedSourceId,
832
+ [targetJunctionColumn.name]: targetId
833
+ }));
834
+
835
+ if (newLinks.length > 0) {
836
+ await tx.insert(junctionTable).values(newLinks);
837
+ }
838
+ }
839
+ } catch (error) {
840
+ console.error(`Failed to update many-to-many inverse relation '${relation.relationName}':`, error);
841
+ throw error;
842
+ }
843
+ }
844
+
845
+ /**
846
+ * Update one-to-one relations that use joinPath
847
+ */
848
+ async updateJoinPathOneToOneRelations(
849
+ tx: DrizzleClient,
850
+ parentCollection: EntityCollection,
851
+ parentEntityId: string | number,
852
+ updates: Array<{
853
+ relationKey: string;
854
+ relation: Relation;
855
+ newTargetId: string | number | null;
856
+ }>
857
+ ) {
858
+ for (const upd of updates) {
859
+ const { relation, newTargetId } = upd;
860
+ const targetCollection = relation.target();
861
+ const targetTable = getTableForCollection(targetCollection, this.registry);
862
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
863
+ const targetIdInfo = targetPks[0];
864
+ const targetIdCol = targetTable[targetIdInfo.fieldName as keyof typeof targetTable] as AnyPgColumn;
865
+
866
+ // Determine mapping of columns
867
+ const { targetFKColName, parentSourceColName } = this.resolveJoinPathWriteMapping(parentCollection, relation);
868
+ const parentTable = getTableForCollection(parentCollection, this.registry);
869
+ const parentPks = getPrimaryKeys(parentCollection, this.registry);
870
+ const parentIdInfo = parentPks[0];
871
+ const parsedParentIdObj = parseIdValues(parentEntityId, parentPks);
872
+ const parsedParentId = parsedParentIdObj[parentIdInfo.fieldName];
873
+
874
+ const parentIdCol = parentTable[parentIdInfo.fieldName as keyof typeof parentTable] as AnyPgColumn;
875
+ const parentSourceCol = parentTable[parentSourceColName as keyof typeof parentTable] as AnyPgColumn;
876
+ const targetFKCol = targetTable[targetFKColName as keyof typeof targetTable] as AnyPgColumn;
877
+
878
+ if (!parentSourceCol) {
879
+ console.warn(`Parent source column '${parentSourceColName}' not found for joinPath relation '${relation.relationName}'`);
880
+ continue;
881
+ }
882
+ if (!targetFKCol) {
883
+ console.warn(`Target FK column '${targetFKColName}' not found for joinPath relation '${relation.relationName}'`);
884
+ continue;
885
+ }
886
+
887
+ // Fetch the parent row to obtain the value for parentSourceCol
888
+ const parentRows = await tx
889
+ .select({ val: parentSourceCol })
890
+ .from(parentTable)
891
+ .where(eq(parentIdCol, parsedParentId))
892
+ .limit(1);
893
+ if (parentRows.length === 0) continue;
894
+ const parentFKValue = parentRows[0].val as string | number | null;
895
+
896
+ if (newTargetId === null || newTargetId === undefined) {
897
+ // Clear any target rows currently linked to this parent via the FK
898
+ if (parentFKValue !== null && parentFKValue !== undefined) {
899
+ await tx.update(targetTable)
900
+ .set({ [targetFKColName]: null })
901
+ .where(eq(targetFKCol, parentFKValue as unknown as string));
902
+ }
903
+ continue;
904
+ }
905
+
906
+ // Parse the new target id
907
+ const parsedTargetIdObj = parseIdValues(newTargetId, targetPks);
908
+ const parsedTargetId = parsedTargetIdObj[targetIdInfo.fieldName];
909
+
910
+ // Ensure one-to-one by clearing existing link from any target rows with this parent FK
911
+ if (parentFKValue !== null && parentFKValue !== undefined) {
912
+ await tx.update(targetTable)
913
+ .set({ [targetFKColName]: null })
914
+ .where(eq(targetFKCol, parentFKValue as unknown as string));
915
+ } else {
916
+ console.warn(`Cannot set joinPath relation '${relation.relationName}' because parent FK value is null/undefined`);
917
+ continue;
918
+ }
919
+
920
+ // Now set the FK on the target entity
921
+ await tx.update(targetTable)
922
+ .set({ [targetFKColName]: parentFKValue })
923
+ .where(eq(targetIdCol, parsedTargetId));
924
+ }
925
+ }
926
+
927
+ /**
928
+ * Resolve joinPath write mapping for one-to-one relations
929
+ */
930
+ resolveJoinPathWriteMapping(
931
+ parentCollection: EntityCollection,
932
+ relation: Relation
933
+ ): { targetFKColName: string; parentSourceColName: string } {
934
+ if (!relation.joinPath || relation.joinPath.length === 0) {
935
+ throw new Error("resolveJoinPathWriteMapping requires a joinPath relation");
936
+ }
937
+ const parentTableName = getTableName(parentCollection);
938
+ const lastStep = relation.joinPath[relation.joinPath.length - 1];
939
+ const targetFKColName = DrizzleConditionBuilder.getColumnNamesFromColumns(lastStep.on.to)[0];
940
+ let currentFrom = lastStep.on.from;
941
+
942
+ let safety = 0;
943
+ while (safety++ < 10) {
944
+ const currentFromTable = DrizzleConditionBuilder.getTableNamesFromColumns(currentFrom)[0];
945
+ if (currentFromTable === parentTableName) {
946
+ break;
947
+ }
948
+ const prevStep = relation.joinPath.find((s) => {
949
+ const to = Array.isArray(s.on.to) ? s.on.to[0] : s.on.to;
950
+ return to === currentFrom;
951
+ });
952
+ if (!prevStep) {
953
+ throw new Error(`Could not resolve parent source column for joinPath relation '${relation.relationName}'`);
954
+ }
955
+ currentFrom = prevStep.on.from;
956
+ }
957
+ const parentSourceColName = DrizzleConditionBuilder.getColumnNamesFromColumns(currentFrom)[0];
958
+ return { targetFKColName, parentSourceColName };
959
+ }
960
+
961
+ /**
962
+ * Handle junction table creation for many-to-many path-based saves
963
+ */
964
+ async handleJunctionTableCreation(
965
+ tx: DrizzleClient,
966
+ newEntityId: string | number,
967
+ junctionTableInfo: {
968
+ parentCollection: EntityCollection;
969
+ parentId: string | number;
970
+ relation: Relation;
971
+ relationKey: string;
972
+ }
973
+ ) {
974
+ const { parentCollection, parentId, relation, relationKey } = junctionTableInfo;
975
+ const targetCollection = relation.target();
976
+
977
+ try {
978
+ const junctionTable = this.registry.getTable(relation.through!.table);
979
+ if (!junctionTable) {
980
+ console.warn(`Junction table '${relation.through!.table}' not found for relation '${relationKey}'`);
981
+ return;
982
+ }
983
+
984
+ const sourceJunctionColumn = junctionTable[relation.through!.sourceColumn as keyof typeof junctionTable] as AnyPgColumn;
985
+ const targetJunctionColumn = junctionTable[relation.through!.targetColumn as keyof typeof junctionTable] as AnyPgColumn;
986
+
987
+ if (!sourceJunctionColumn || !targetJunctionColumn) {
988
+ console.warn(`Junction columns not found for relation '${relationKey}'`);
989
+ return;
990
+ }
991
+
992
+ // Parse the new entity ID to the correct type
993
+ const targetPks = getPrimaryKeys(targetCollection, this.registry);
994
+ const targetIdInfo = targetPks[0];
995
+ const parsedNewEntityIdObj = parseIdValues(newEntityId, targetPks);
996
+ const parsedNewEntityId = parsedNewEntityIdObj[targetIdInfo.fieldName];
997
+
998
+ // Create the junction table entry linking parent to the new entity
999
+ const junctionData = {
1000
+ [sourceJunctionColumn.name]: parentId,
1001
+ [targetJunctionColumn.name]: parsedNewEntityId
1002
+ };
1003
+
1004
+ await tx.insert(junctionTable).values(junctionData);
1005
+
1006
+ console.log(`Created junction table entry for many-to-many relation '${relationKey}': ${JSON.stringify(junctionData)}`);
1007
+ } catch (error) {
1008
+ console.error(`Failed to create junction table entry for relation '${relationKey}':`, error);
1009
+ throw error;
1010
+ }
1011
+ }
1012
+ }