@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.
Files changed (196) 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 +56 -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 +58 -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 +22 -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 +11298 -0
  26. package/dist/index.es.js.map +1 -0
  27. package/dist/index.umd.js +11306 -0
  28. package/dist/index.umd.js.map +1 -0
  29. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +100 -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 +192 -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 +40 -0
  36. package/dist/server-postgresql/src/data-transformer.d.ts +58 -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 +868 -0
  43. package/dist/server-postgresql/src/schema/doctor-cli.d.ts +2 -0
  44. package/dist/server-postgresql/src/schema/doctor.d.ts +43 -0
  45. package/dist/server-postgresql/src/schema/generate-drizzle-schema-logic.d.ts +2 -0
  46. package/dist/server-postgresql/src/schema/generate-drizzle-schema.d.ts +1 -0
  47. package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +82 -0
  48. package/dist/server-postgresql/src/schema/introspect-db.d.ts +1 -0
  49. package/dist/server-postgresql/src/schema/test-schema.d.ts +24 -0
  50. package/dist/server-postgresql/src/services/BranchService.d.ts +47 -0
  51. package/dist/server-postgresql/src/services/EntityFetchService.d.ts +209 -0
  52. package/dist/server-postgresql/src/services/EntityPersistService.d.ts +41 -0
  53. package/dist/server-postgresql/src/services/RelationService.d.ts +98 -0
  54. package/dist/server-postgresql/src/services/entity-helpers.d.ts +38 -0
  55. package/dist/server-postgresql/src/services/entityService.d.ts +104 -0
  56. package/dist/server-postgresql/src/services/index.d.ts +4 -0
  57. package/dist/server-postgresql/src/services/realtimeService.d.ts +188 -0
  58. package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +116 -0
  59. package/dist/server-postgresql/src/websocket.d.ts +5 -0
  60. package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
  61. package/dist/types/src/controllers/auth.d.ts +119 -0
  62. package/dist/types/src/controllers/client.d.ts +170 -0
  63. package/dist/types/src/controllers/collection_registry.d.ts +45 -0
  64. package/dist/types/src/controllers/customization_controller.d.ts +60 -0
  65. package/dist/types/src/controllers/data.d.ts +168 -0
  66. package/dist/types/src/controllers/data_driver.d.ts +160 -0
  67. package/dist/types/src/controllers/database_admin.d.ts +11 -0
  68. package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
  69. package/dist/types/src/controllers/effective_role.d.ts +4 -0
  70. package/dist/types/src/controllers/email.d.ts +34 -0
  71. package/dist/types/src/controllers/index.d.ts +18 -0
  72. package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
  73. package/dist/types/src/controllers/navigation.d.ts +213 -0
  74. package/dist/types/src/controllers/registry.d.ts +54 -0
  75. package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
  76. package/dist/types/src/controllers/side_entity_controller.d.ts +90 -0
  77. package/dist/types/src/controllers/snackbar.d.ts +24 -0
  78. package/dist/types/src/controllers/storage.d.ts +171 -0
  79. package/dist/types/src/index.d.ts +4 -0
  80. package/dist/types/src/rebase_context.d.ts +105 -0
  81. package/dist/types/src/types/backend.d.ts +536 -0
  82. package/dist/types/src/types/builders.d.ts +15 -0
  83. package/dist/types/src/types/chips.d.ts +5 -0
  84. package/dist/types/src/types/collections.d.ts +856 -0
  85. package/dist/types/src/types/cron.d.ts +102 -0
  86. package/dist/types/src/types/data_source.d.ts +64 -0
  87. package/dist/types/src/types/entities.d.ts +145 -0
  88. package/dist/types/src/types/entity_actions.d.ts +98 -0
  89. package/dist/types/src/types/entity_callbacks.d.ts +173 -0
  90. package/dist/types/src/types/entity_link_builder.d.ts +7 -0
  91. package/dist/types/src/types/entity_overrides.d.ts +10 -0
  92. package/dist/types/src/types/entity_views.d.ts +61 -0
  93. package/dist/types/src/types/export_import.d.ts +21 -0
  94. package/dist/types/src/types/index.d.ts +23 -0
  95. package/dist/types/src/types/locales.d.ts +4 -0
  96. package/dist/types/src/types/modify_collections.d.ts +5 -0
  97. package/dist/types/src/types/plugins.d.ts +279 -0
  98. package/dist/types/src/types/properties.d.ts +1176 -0
  99. package/dist/types/src/types/property_config.d.ts +70 -0
  100. package/dist/types/src/types/relations.d.ts +336 -0
  101. package/dist/types/src/types/slots.d.ts +252 -0
  102. package/dist/types/src/types/translations.d.ts +870 -0
  103. package/dist/types/src/types/user_management_delegate.d.ts +121 -0
  104. package/dist/types/src/types/websockets.d.ts +78 -0
  105. package/dist/types/src/users/index.d.ts +2 -0
  106. package/dist/types/src/users/roles.d.ts +22 -0
  107. package/dist/types/src/users/user.d.ts +46 -0
  108. package/drizzle-test/0000_woozy_junta.sql +6 -0
  109. package/drizzle-test/0001_youthful_arachne.sql +1 -0
  110. package/drizzle-test/0002_lively_dragon_lord.sql +2 -0
  111. package/drizzle-test/0003_mean_king_cobra.sql +2 -0
  112. package/drizzle-test/meta/0000_snapshot.json +47 -0
  113. package/drizzle-test/meta/0001_snapshot.json +48 -0
  114. package/drizzle-test/meta/0002_snapshot.json +38 -0
  115. package/drizzle-test/meta/0003_snapshot.json +48 -0
  116. package/drizzle-test/meta/_journal.json +34 -0
  117. package/drizzle-test-out/0000_tan_trauma.sql +6 -0
  118. package/drizzle-test-out/0001_rapid_drax.sql +1 -0
  119. package/drizzle-test-out/meta/0000_snapshot.json +44 -0
  120. package/drizzle-test-out/meta/0001_snapshot.json +54 -0
  121. package/drizzle-test-out/meta/_journal.json +20 -0
  122. package/drizzle.test.config.ts +10 -0
  123. package/jest-all.log +3128 -0
  124. package/jest.log +49 -0
  125. package/package.json +92 -0
  126. package/scratch.ts +41 -0
  127. package/src/PostgresBackendDriver.ts +1008 -0
  128. package/src/PostgresBootstrapper.ts +231 -0
  129. package/src/auth/ensure-tables.ts +381 -0
  130. package/src/auth/services.ts +799 -0
  131. package/src/cli.ts +648 -0
  132. package/src/collections/PostgresCollectionRegistry.ts +96 -0
  133. package/src/connection.ts +84 -0
  134. package/src/data-transformer.ts +608 -0
  135. package/src/databasePoolManager.ts +85 -0
  136. package/src/history/HistoryService.ts +248 -0
  137. package/src/history/ensure-history-table.ts +45 -0
  138. package/src/index.ts +13 -0
  139. package/src/interfaces.ts +60 -0
  140. package/src/schema/auth-schema.ts +169 -0
  141. package/src/schema/doctor-cli.ts +47 -0
  142. package/src/schema/doctor.ts +595 -0
  143. package/src/schema/generate-drizzle-schema-logic.ts +765 -0
  144. package/src/schema/generate-drizzle-schema.ts +151 -0
  145. package/src/schema/introspect-db-logic.ts +542 -0
  146. package/src/schema/introspect-db.ts +211 -0
  147. package/src/schema/test-schema.ts +11 -0
  148. package/src/services/BranchService.ts +237 -0
  149. package/src/services/EntityFetchService.ts +1576 -0
  150. package/src/services/EntityPersistService.ts +349 -0
  151. package/src/services/RelationService.ts +1274 -0
  152. package/src/services/entity-helpers.ts +147 -0
  153. package/src/services/entityService.ts +211 -0
  154. package/src/services/index.ts +13 -0
  155. package/src/services/realtimeService.ts +1034 -0
  156. package/src/utils/drizzle-conditions.ts +1000 -0
  157. package/src/websocket.ts +518 -0
  158. package/test/auth-services.test.ts +661 -0
  159. package/test/batch-many-to-many-regression.test.ts +573 -0
  160. package/test/branchService.test.ts +367 -0
  161. package/test/data-transformer-hardening.test.ts +417 -0
  162. package/test/data-transformer.test.ts +175 -0
  163. package/test/doctor.test.ts +182 -0
  164. package/test/drizzle-conditions.test.ts +895 -0
  165. package/test/entityService.errors.test.ts +367 -0
  166. package/test/entityService.relations.test.ts +1008 -0
  167. package/test/entityService.subcollection-search.test.ts +566 -0
  168. package/test/entityService.test.ts +1035 -0
  169. package/test/generate-drizzle-schema.test.ts +988 -0
  170. package/test/historyService.test.ts +141 -0
  171. package/test/introspect-db-generation.test.ts +436 -0
  172. package/test/introspect-db-utils.test.ts +389 -0
  173. package/test/n-plus-one-regression.test.ts +314 -0
  174. package/test/postgresDataDriver.test.ts +648 -0
  175. package/test/realtimeService.test.ts +307 -0
  176. package/test/relation-pipeline-gaps.test.ts +637 -0
  177. package/test/relations.test.ts +1115 -0
  178. package/test/unmapped-tables-safety.test.ts +345 -0
  179. package/test-drizzle-bug.ts +18 -0
  180. package/test-drizzle-out/0000_cultured_freak.sql +7 -0
  181. package/test-drizzle-out/0001_tiresome_professor_monster.sql +1 -0
  182. package/test-drizzle-out/meta/0000_snapshot.json +55 -0
  183. package/test-drizzle-out/meta/0001_snapshot.json +63 -0
  184. package/test-drizzle-out/meta/_journal.json +20 -0
  185. package/test-drizzle-prompt.sh +2 -0
  186. package/test-policy-prompt.sh +3 -0
  187. package/test-programmatic.ts +30 -0
  188. package/test-programmatic2.ts +59 -0
  189. package/test-schema-no-policies.ts +12 -0
  190. package/test_drizzle_mock.js +3 -0
  191. package/test_find_changed.mjs +32 -0
  192. package/test_hash.js +14 -0
  193. package/test_output.txt +3145 -0
  194. package/tsconfig.json +49 -0
  195. package/tsconfig.prod.json +20 -0
  196. package/vite.config.ts +82 -0
@@ -0,0 +1,895 @@
1
+ import { beforeEach, describe, expect, it, jest } from "@jest/globals";
2
+ import { eq } from "drizzle-orm";
3
+ import { integer, pgTable, primaryKey, serial, varchar } from "drizzle-orm/pg-core";
4
+ import { EntityCollection, Relation } from "@rebasepro/types";
5
+ import { PostgresCollectionRegistry } from "../src/collections/PostgresCollectionRegistry";
6
+ import { DrizzleConditionBuilder } from "../src/utils/drizzle-conditions";
7
+
8
+ // Mock tables for testing
9
+ const mockAuthorsTable = pgTable("authors", {
10
+ id: serial("id").primaryKey(),
11
+ name: varchar("name").notNull(),
12
+ email: varchar("email").notNull()
13
+ });
14
+
15
+ const mockPostsTable = pgTable("posts", {
16
+ id: serial("id").primaryKey(),
17
+ title: varchar("title").notNull(),
18
+ content: varchar("content"),
19
+ author_id: integer("author_id")
20
+ });
21
+
22
+ const mockTagsTable = pgTable("tags", {
23
+ id: serial("id").primaryKey(),
24
+ name: varchar("name").notNull()
25
+ });
26
+
27
+ const mockPostsTagsTable = pgTable("posts_tags", {
28
+ post_id: integer("post_id").notNull(),
29
+ tag_id: integer("tag_id").notNull()
30
+ }, (table) => ({
31
+ pk: primaryKey({ columns: [table.post_id, table.tag_id] })
32
+ }));
33
+
34
+ // Mock registry
35
+ const createMockRegistry = () => {
36
+ const registry = {
37
+ getTable: jest.fn()
38
+ } as unknown as PostgresCollectionRegistry;
39
+
40
+ (registry.getTable as jest.Mock).mockImplementation((tableName: string) => {
41
+ switch (tableName) {
42
+ case "authors": return mockAuthorsTable;
43
+ case "posts": return mockPostsTable;
44
+ case "tags": return mockTagsTable;
45
+ case "posts_tags": return mockPostsTagsTable;
46
+ default: return undefined;
47
+ }
48
+ });
49
+
50
+ return registry;
51
+ };
52
+
53
+ describe("DrizzleConditionBuilder - Many-to-Many Relations", () => {
54
+ let mockRegistry: PostgresCollectionRegistry;
55
+
56
+ beforeEach(() => {
57
+ mockRegistry = createMockRegistry();
58
+ jest.clearAllMocks();
59
+ });
60
+
61
+ describe("buildRelationConditions - Owning Many-to-Many", () => {
62
+ it("should build correct conditions for owning many-to-many relation", () => {
63
+ const relation: Relation = {
64
+ relationName: "tags",
65
+ target: () => ({ slug: "tags" } as unknown as EntityCollection),
66
+ cardinality: "many",
67
+ direction: "owning",
68
+ through: {
69
+ table: "posts_tags",
70
+ sourceColumn: "post_id",
71
+ targetColumn: "tag_id"
72
+ }
73
+ };
74
+
75
+ const result = DrizzleConditionBuilder.buildRelationConditions(
76
+ relation,
77
+ 1, // parentEntityId (post ID)
78
+ mockTagsTable, // targetTable
79
+ mockPostsTable, // parentTable
80
+ mockPostsTable.id, // parentIdColumn
81
+ mockTagsTable.id, // targetIdColumn
82
+ mockRegistry
83
+ );
84
+
85
+ expect(result.joinConditions).toHaveLength(1);
86
+ expect(result.whereConditions).toHaveLength(1);
87
+ expect(mockRegistry.getTable).toHaveBeenCalledWith("posts_tags");
88
+ });
89
+
90
+ it("should handle array of parent entity IDs for owning relation", () => {
91
+ const relation: Relation = {
92
+ relationName: "tags",
93
+ target: () => ({ slug: "tags" } as unknown as EntityCollection),
94
+ cardinality: "many",
95
+ direction: "owning",
96
+ through: {
97
+ table: "posts_tags",
98
+ sourceColumn: "post_id",
99
+ targetColumn: "tag_id"
100
+ }
101
+ };
102
+
103
+ const result = DrizzleConditionBuilder.buildRelationConditions(
104
+ relation,
105
+ [1, 2, 3], // multiple post IDs
106
+ mockTagsTable,
107
+ mockPostsTable,
108
+ mockPostsTable.id,
109
+ mockTagsTable.id,
110
+ mockRegistry
111
+ );
112
+
113
+ expect(result.joinConditions).toHaveLength(1);
114
+ expect(result.whereConditions).toHaveLength(1);
115
+ });
116
+ });
117
+
118
+ describe("buildRelationConditions - Inverse Many-to-Many", () => {
119
+ it("should build correct conditions for inverse many-to-many relation", () => {
120
+ const relation: Relation = {
121
+ relationName: "posts",
122
+ target: () => ({ slug: "posts" } as unknown as EntityCollection),
123
+ cardinality: "many",
124
+ direction: "inverse",
125
+ through: {
126
+ table: "posts_tags",
127
+ sourceColumn: "tag_id",
128
+ targetColumn: "post_id"
129
+ }
130
+ };
131
+
132
+ const result = DrizzleConditionBuilder.buildRelationConditions(
133
+ relation,
134
+ 20, // parentEntityId (tag ID)
135
+ mockPostsTable, // targetTable
136
+ mockTagsTable, // parentTable
137
+ mockTagsTable.id, // parentIdColumn
138
+ mockPostsTable.id, // targetIdColumn
139
+ mockRegistry
140
+ );
141
+
142
+ expect(result.joinConditions).toHaveLength(1);
143
+ expect(result.whereConditions).toHaveLength(1);
144
+ expect(mockRegistry.getTable).toHaveBeenCalledWith("posts_tags");
145
+ });
146
+
147
+ it("should handle array of parent entity IDs for inverse relation", () => {
148
+ const relation: Relation = {
149
+ relationName: "posts",
150
+ target: () => ({ slug: "posts" } as unknown as EntityCollection),
151
+ cardinality: "many",
152
+ direction: "inverse",
153
+ through: {
154
+ table: "posts_tags",
155
+ sourceColumn: "tag_id",
156
+ targetColumn: "post_id"
157
+ }
158
+ };
159
+
160
+ const result = DrizzleConditionBuilder.buildRelationConditions(
161
+ relation,
162
+ [20, 21, 22], // multiple tag IDs
163
+ mockPostsTable,
164
+ mockTagsTable,
165
+ mockTagsTable.id,
166
+ mockPostsTable.id,
167
+ mockRegistry
168
+ );
169
+
170
+ expect(result.joinConditions).toHaveLength(1);
171
+ expect(result.whereConditions).toHaveLength(1);
172
+ });
173
+ });
174
+
175
+ describe("Join Path Relations with Junction Tables", () => {
176
+ it("should handle join paths that include many-to-many relationships", () => {
177
+ // Create a special mock registry that simulates missing direct foreign keys
178
+ const mockRegistryForJunction = {
179
+ getTable: jest.fn()
180
+ } as unknown as PostgresCollectionRegistry;
181
+
182
+ // Create tables without the direct foreign key relationship
183
+ const mockPostsTableNoDirect = pgTable("posts", {
184
+ id: serial("id").primaryKey(),
185
+ title: varchar("title").notNull(),
186
+ content: varchar("content")
187
+ // Note: NO tag_id foreign key column
188
+ });
189
+
190
+ const mockTagsTableNoDirect = pgTable("tags", {
191
+ id: serial("id").primaryKey(),
192
+ name: varchar("name").notNull()
193
+ // Note: NO post_id foreign key column
194
+ });
195
+
196
+ (mockRegistryForJunction.getTable as jest.Mock).mockImplementation((tableName: string) => {
197
+ switch (tableName) {
198
+ case "posts": return mockPostsTableNoDirect;
199
+ case "tags": return mockTagsTableNoDirect;
200
+ case "posts_tags": return mockPostsTagsTable;
201
+ default: return undefined;
202
+ }
203
+ });
204
+
205
+ // Simulate a join path like: Post -> Tags (where posts would need tag_id but doesn't have it)
206
+ const joinPathWithJunction = [
207
+ {
208
+ table: "tags",
209
+ on: {
210
+ from: "posts.tag_id", // This column doesn't exist - should trigger junction table discovery
211
+ to: "tags.id"
212
+ }
213
+ }
214
+ ];
215
+
216
+ const relation: Relation = {
217
+ relationName: "tags_via_join",
218
+ target: () => ({ slug: "tags" } as unknown as EntityCollection),
219
+ cardinality: "many",
220
+ direction: "inverse",
221
+ joinPath: joinPathWithJunction
222
+ };
223
+
224
+ // Should automatically detect and use the posts_tags junction table
225
+ const result = DrizzleConditionBuilder.buildRelationConditions(
226
+ relation,
227
+ 1, // post ID
228
+ mockTagsTableNoDirect, // target table (tags)
229
+ mockPostsTableNoDirect, // parent table (posts)
230
+ mockPostsTableNoDirect.id, // parent ID column
231
+ mockTagsTableNoDirect.id, // target ID column
232
+ mockRegistryForJunction
233
+ );
234
+
235
+ expect(result.joinConditions.length).toBeGreaterThan(0);
236
+ expect(result.whereConditions).toHaveLength(1);
237
+ expect(mockRegistryForJunction.getTable).toHaveBeenCalledWith("posts_tags");
238
+ });
239
+
240
+ it("should fallback to error when no junction table is found for missing foreign keys", () => {
241
+ const joinPathWithMissingRelation = [
242
+ {
243
+ table: "nonexistent_table",
244
+ on: {
245
+ from: "posts.nonexistent_column",
246
+ to: "nonexistent_table.id"
247
+ }
248
+ }
249
+ ];
250
+
251
+ const relation: Relation = {
252
+ relationName: "missing_relation",
253
+ target: () => ({ slug: "nonexistent" } as unknown as EntityCollection),
254
+ cardinality: "one",
255
+ direction: "inverse",
256
+ joinPath: joinPathWithMissingRelation
257
+ };
258
+
259
+ expect(() => {
260
+ DrizzleConditionBuilder.buildRelationConditions(
261
+ relation,
262
+ 1,
263
+ mockTagsTable,
264
+ mockPostsTable,
265
+ mockPostsTable.id,
266
+ mockTagsTable.id,
267
+ mockRegistry
268
+ );
269
+ }).toThrow("Join tables not found");
270
+ });
271
+
272
+ it("should handle complex multi-hop join paths with junction tables", () => {
273
+ // Simulate: Author -> Posts -> Tags (where Posts-Tags uses junction table)
274
+ const complexJoinPath = [
275
+ {
276
+ table: "posts",
277
+ on: {
278
+ from: "authors.id",
279
+ to: "posts.author_id"
280
+ }
281
+ },
282
+ {
283
+ table: "tags",
284
+ on: {
285
+ from: "posts.id", // This will require posts_tags junction
286
+ to: "tags.id"
287
+ }
288
+ }
289
+ ];
290
+
291
+ const relation: Relation = {
292
+ relationName: "author_tags",
293
+ target: () => ({ slug: "tags" } as unknown as EntityCollection),
294
+ cardinality: "many",
295
+ direction: "inverse",
296
+ joinPath: complexJoinPath
297
+ };
298
+
299
+ const result = DrizzleConditionBuilder.buildRelationConditions(
300
+ relation,
301
+ 1, // author ID
302
+ mockTagsTable, // target (tags)
303
+ mockAuthorsTable, // parent (authors)
304
+ mockAuthorsTable.id,
305
+ mockTagsTable.id,
306
+ mockRegistry
307
+ );
308
+
309
+ expect(result.joinConditions.length).toBeGreaterThan(1); // Should have multiple joins
310
+ expect(result.whereConditions).toHaveLength(1);
311
+ });
312
+ });
313
+
314
+ describe("Junction Table Discovery", () => {
315
+ it("should try multiple naming patterns for junction tables", () => {
316
+ const relation: Relation = {
317
+ relationName: "test_junction",
318
+ target: () => ({ slug: "tags" } as unknown as EntityCollection),
319
+ cardinality: "many",
320
+ direction: "inverse",
321
+ joinPath: [
322
+ {
323
+ table: "tags",
324
+ on: {
325
+ from: "posts.id",
326
+ to: "tags.id"
327
+ }
328
+ }
329
+ ]
330
+ };
331
+
332
+ // Mock the registry to return undefined for first attempts, then return junction table
333
+ const mockRegistryWithPatterns = {
334
+ getTable: jest.fn()
335
+ } as unknown as PostgresCollectionRegistry;
336
+
337
+ (mockRegistryWithPatterns.getTable as jest.Mock)
338
+ .mockReturnValueOnce(mockPostsTable) // posts table
339
+ .mockReturnValueOnce(mockTagsTable) // tags table
340
+ .mockReturnValueOnce(undefined) // posts_tags (first attempt)
341
+ .mockReturnValueOnce(undefined) // tags_posts (second attempt)
342
+ .mockReturnValueOnce(mockPostsTagsTable); // Found it!
343
+
344
+ expect(() => {
345
+ DrizzleConditionBuilder.buildRelationConditions(
346
+ relation,
347
+ 1,
348
+ mockTagsTable,
349
+ mockPostsTable,
350
+ mockPostsTable.id,
351
+ mockTagsTable.id,
352
+ mockRegistryWithPatterns
353
+ );
354
+ }).not.toThrow();
355
+ });
356
+ });
357
+
358
+ describe("Error handling", () => {
359
+ it("should throw error when junction table is not found", () => {
360
+ const relation: Relation = {
361
+ relationName: "posts",
362
+ target: () => ({ slug: "posts" } as unknown as EntityCollection),
363
+ cardinality: "many",
364
+ direction: "inverse",
365
+ through: {
366
+ table: "nonexistent_table",
367
+ sourceColumn: "tag_id",
368
+ targetColumn: "post_id"
369
+ }
370
+ };
371
+
372
+ expect(() => {
373
+ DrizzleConditionBuilder.buildRelationConditions(
374
+ relation,
375
+ 20,
376
+ mockPostsTable,
377
+ mockTagsTable,
378
+ mockTagsTable.id,
379
+ mockPostsTable.id,
380
+ mockRegistry
381
+ );
382
+ }).toThrow("Junction table not found: nonexistent_table");
383
+ });
384
+
385
+ it("should throw error when source column is not found in junction table", () => {
386
+ const relation: Relation = {
387
+ relationName: "posts",
388
+ target: () => ({ slug: "posts" } as unknown as EntityCollection),
389
+ cardinality: "many",
390
+ direction: "inverse",
391
+ through: {
392
+ table: "posts_tags",
393
+ sourceColumn: "nonexistent_column",
394
+ targetColumn: "post_id"
395
+ }
396
+ };
397
+
398
+ expect(() => {
399
+ DrizzleConditionBuilder.buildRelationConditions(
400
+ relation,
401
+ 20,
402
+ mockPostsTable,
403
+ mockTagsTable,
404
+ mockTagsTable.id,
405
+ mockPostsTable.id,
406
+ mockRegistry
407
+ );
408
+ }).toThrow("Source column 'nonexistent_column' not found in junction table 'posts_tags'");
409
+ });
410
+
411
+ it("should throw error when target column is not found in junction table", () => {
412
+ const relation: Relation = {
413
+ relationName: "posts",
414
+ target: () => ({ slug: "posts" } as unknown as EntityCollection),
415
+ cardinality: "many",
416
+ direction: "inverse",
417
+ through: {
418
+ table: "posts_tags",
419
+ sourceColumn: "tag_id",
420
+ targetColumn: "nonexistent_column"
421
+ }
422
+ };
423
+
424
+ expect(() => {
425
+ DrizzleConditionBuilder.buildRelationConditions(
426
+ relation,
427
+ 20,
428
+ mockPostsTable,
429
+ mockTagsTable,
430
+ mockTagsTable.id,
431
+ mockPostsTable.id,
432
+ mockRegistry
433
+ );
434
+ }).toThrow("Target column 'nonexistent_column' not found in junction table 'posts_tags'");
435
+ });
436
+ });
437
+
438
+ describe("buildRelationCountQuery - Many-to-Many", () => {
439
+ it("should build correct count query for owning many-to-many relation", () => {
440
+ const relation: Relation = {
441
+ relationName: "tags",
442
+ target: () => ({ slug: "tags" } as unknown as EntityCollection),
443
+ cardinality: "many",
444
+ direction: "owning",
445
+ through: {
446
+ table: "posts_tags",
447
+ sourceColumn: "post_id",
448
+ targetColumn: "tag_id"
449
+ }
450
+ };
451
+
452
+ const mockBaseQuery = {
453
+ innerJoin: jest.fn().mockReturnThis(),
454
+ where: jest.fn().mockReturnThis(),
455
+ $dynamic: jest.fn().mockReturnThis()
456
+ };
457
+
458
+ const result = DrizzleConditionBuilder.buildRelationCountQuery(
459
+ mockBaseQuery,
460
+ relation,
461
+ 1, // parentEntityId
462
+ mockTagsTable,
463
+ mockPostsTable,
464
+ mockPostsTable.id,
465
+ mockTagsTable.id,
466
+ mockRegistry
467
+ );
468
+
469
+ expect(mockBaseQuery.innerJoin).toHaveBeenCalled();
470
+ expect(mockBaseQuery.where).toHaveBeenCalled();
471
+ });
472
+
473
+ it("should build correct count query for inverse many-to-many relation", () => {
474
+ const relation: Relation = {
475
+ relationName: "posts",
476
+ target: () => ({ slug: "posts" } as unknown as EntityCollection),
477
+ cardinality: "many",
478
+ direction: "inverse",
479
+ through: {
480
+ table: "posts_tags",
481
+ sourceColumn: "tag_id",
482
+ targetColumn: "post_id"
483
+ }
484
+ };
485
+
486
+ const mockBaseQuery = {
487
+ innerJoin: jest.fn().mockReturnThis(),
488
+ where: jest.fn().mockReturnThis(),
489
+ $dynamic: jest.fn().mockReturnThis()
490
+ };
491
+
492
+ const result = DrizzleConditionBuilder.buildRelationCountQuery(
493
+ mockBaseQuery,
494
+ relation,
495
+ 20, // parentEntityId (tag ID)
496
+ mockPostsTable,
497
+ mockTagsTable,
498
+ mockTagsTable.id,
499
+ mockPostsTable.id,
500
+ mockRegistry
501
+ );
502
+
503
+ expect(mockBaseQuery.innerJoin).toHaveBeenCalled();
504
+ expect(mockBaseQuery.where).toHaveBeenCalled();
505
+ });
506
+ });
507
+
508
+ describe("buildRelationQuery - Many-to-Many", () => {
509
+ it("should build correct query for owning many-to-many relation with additional filters", () => {
510
+ const relation: Relation = {
511
+ relationName: "tags",
512
+ target: () => ({ slug: "tags" } as unknown as EntityCollection),
513
+ cardinality: "many",
514
+ direction: "owning",
515
+ through: {
516
+ table: "posts_tags",
517
+ sourceColumn: "post_id",
518
+ targetColumn: "tag_id"
519
+ }
520
+ };
521
+
522
+ const mockBaseQuery = {
523
+ innerJoin: jest.fn().mockReturnThis(),
524
+ where: jest.fn().mockReturnThis(),
525
+ $dynamic: jest.fn().mockReturnThis()
526
+ };
527
+
528
+ const additionalFilters = [eq(mockTagsTable.name, "javascript")];
529
+
530
+ const result = DrizzleConditionBuilder.buildRelationQuery(
531
+ mockBaseQuery,
532
+ relation,
533
+ 1, // parentEntityId
534
+ mockTagsTable,
535
+ mockPostsTable,
536
+ mockPostsTable.id,
537
+ mockTagsTable.id,
538
+ mockRegistry,
539
+ additionalFilters
540
+ );
541
+
542
+ expect(mockBaseQuery.innerJoin).toHaveBeenCalled();
543
+ expect(mockBaseQuery.where).toHaveBeenCalled();
544
+ });
545
+
546
+ it("should build correct query for inverse many-to-many relation with additional filters", () => {
547
+ const relation: Relation = {
548
+ relationName: "posts",
549
+ target: () => ({ slug: "posts" } as unknown as EntityCollection),
550
+ cardinality: "many",
551
+ direction: "inverse",
552
+ through: {
553
+ table: "posts_tags",
554
+ sourceColumn: "tag_id",
555
+ targetColumn: "post_id"
556
+ }
557
+ };
558
+
559
+ const mockBaseQuery = {
560
+ innerJoin: jest.fn().mockReturnThis(),
561
+ where: jest.fn().mockReturnThis(),
562
+ $dynamic: jest.fn().mockReturnThis()
563
+ };
564
+
565
+ const additionalFilters = [eq(mockPostsTable.title, "Test Post")];
566
+
567
+ const result = DrizzleConditionBuilder.buildRelationQuery(
568
+ mockBaseQuery,
569
+ relation,
570
+ 20, // parentEntityId (tag ID)
571
+ mockPostsTable,
572
+ mockTagsTable,
573
+ mockTagsTable.id,
574
+ mockPostsTable.id,
575
+ mockRegistry,
576
+ additionalFilters
577
+ );
578
+
579
+ expect(mockBaseQuery.innerJoin).toHaveBeenCalled();
580
+ expect(mockBaseQuery.where).toHaveBeenCalled();
581
+ });
582
+ });
583
+
584
+ describe("Real-world scenario: tags/20/posts", () => {
585
+ it("should correctly handle the tags/20/posts scenario that was failing", () => {
586
+ // This is the exact scenario from the user's error
587
+ const tagsToPostsRelation: Relation = {
588
+ relationName: "posts",
589
+ target: () => ({ slug: "posts" } as unknown as EntityCollection),
590
+ cardinality: "many",
591
+ direction: "inverse",
592
+ through: {
593
+ table: "posts_tags",
594
+ sourceColumn: "tag_id", // FK to this collection's PK in junction table
595
+ targetColumn: "post_id" // FK to the target collection's PK in junction table
596
+ }
597
+ };
598
+
599
+ const result = DrizzleConditionBuilder.buildRelationConditions(
600
+ tagsToPostsRelation,
601
+ 20, // tag ID from URL: tags/20/posts
602
+ mockPostsTable, // we want to get posts
603
+ mockTagsTable, // from the tags collection
604
+ mockTagsTable.id, // tag ID column
605
+ mockPostsTable.id, // post ID column
606
+ mockRegistry
607
+ );
608
+
609
+ // Should not throw an error and should return proper conditions
610
+ expect(result.joinConditions).toHaveLength(1);
611
+ expect(result.whereConditions).toHaveLength(1);
612
+
613
+ // Verify the registry was called correctly
614
+ expect(mockRegistry.getTable).toHaveBeenCalledWith("posts_tags");
615
+
616
+ // This should no longer throw "Foreign key column 'tags_id' not found in target table"
617
+ expect(() => result).not.toThrow();
618
+ });
619
+
620
+ it("should handle inverse many-to-many without explicit through property (real user scenario)", () => {
621
+ // Create a more realistic mock that simulates the actual scenario
622
+ const mockPostsCollection = {
623
+ slug: "posts",
624
+ table: "posts",
625
+ relations: [
626
+ {
627
+ relationName: "tags",
628
+ cardinality: "many",
629
+ direction: "owning",
630
+ through: {
631
+ table: "posts_tags",
632
+ sourceColumn: "post_id",
633
+ targetColumn: "tag_id"
634
+ },
635
+ target: () => ({ slug: "tags" })
636
+ }
637
+ ]
638
+ };
639
+
640
+ // This is the ACTUAL scenario: inverse relation without through property
641
+ // but with foreignKeyOnTarget incorrectly added by sanitizeRelation
642
+ const tagsToPostsRelation: Relation = {
643
+ relationName: "posts",
644
+ target: () => mockPostsCollection as unknown as EntityCollection,
645
+ cardinality: "many",
646
+ direction: "inverse",
647
+ inverseRelationName: "tags",
648
+ foreignKeyOnTarget: "tag_id" // This gets added by sanitizeRelation at runtime
649
+ // NO through property - this is the key difference
650
+ };
651
+
652
+ // The fix should handle this case correctly by ignoring the foreignKeyOnTarget
653
+ // and finding the junction table from the corresponding owning relation
654
+ const result = DrizzleConditionBuilder.buildRelationConditions(
655
+ tagsToPostsRelation,
656
+ 23, // tag ID from URL: tags/23/posts (matching the user's log)
657
+ mockPostsTable, // we want to get posts
658
+ mockTagsTable, // from the tags collection
659
+ mockTagsTable.id, // tag ID column
660
+ mockPostsTable.id, // post ID column
661
+ mockRegistry
662
+ );
663
+
664
+ // Should successfully find junction table and build conditions
665
+ expect(result.joinConditions).toHaveLength(1);
666
+ expect(result.whereConditions).toHaveLength(1);
667
+
668
+ // Verify it used the junction table approach, not the simple relation approach
669
+ expect(mockRegistry.getTable).toHaveBeenCalledWith("posts_tags");
670
+
671
+ // Should not throw the "Foreign key column 'tag_id' not found in target table" error
672
+ expect(() => result).not.toThrow();
673
+ });
674
+ });
675
+
676
+ // Test the specific fix for findCorrespondingJunctionTable method
677
+ describe("findCorrespondingJunctionTable - Junction Table Lookup Fix", () => {
678
+ beforeEach(() => {
679
+ jest.clearAllMocks();
680
+ });
681
+
682
+ it("should find corresponding junction table for inverse many-to-many relation", () => {
683
+ // Create real test collections with proper relation configurations
684
+ const mockTagsCollection = {
685
+ slug: "tags",
686
+ table: "tags"
687
+ };
688
+
689
+ const mockPostsCollection = {
690
+ slug: "posts",
691
+ table: "posts",
692
+ relations: [
693
+ {
694
+ relationName: "tags",
695
+ cardinality: "many" as const,
696
+ direction: "owning" as const,
697
+ through: {
698
+ table: "posts_tags",
699
+ sourceColumn: "post_id",
700
+ targetColumn: "tag_id"
701
+ },
702
+ target: () => mockTagsCollection
703
+ }
704
+ ]
705
+ };
706
+
707
+ // Create the inverse relation (tags -> posts)
708
+ const inverseRelation: Relation = {
709
+ relationName: "posts",
710
+ target: () => mockPostsCollection as unknown as EntityCollection,
711
+ cardinality: "many",
712
+ direction: "inverse",
713
+ inverseRelationName: "tags" // This should match the key in the target collection relations
714
+ };
715
+
716
+ // Test the buildRelationConditions with the inverse relation (without explicit through)
717
+ const result = DrizzleConditionBuilder.buildRelationConditions(
718
+ inverseRelation,
719
+ 5, // tag ID
720
+ mockPostsTable, // targetTable (posts)
721
+ mockTagsTable, // parentTable (tags)
722
+ mockTagsTable.id, // parentIdColumn (tag.id)
723
+ mockPostsTable.id, // targetIdColumn (post.id)
724
+ mockRegistry
725
+ );
726
+
727
+ // Should successfully build conditions using the found junction table
728
+ expect(result.joinConditions).toHaveLength(1);
729
+ expect(result.whereConditions).toHaveLength(1);
730
+
731
+ // Should have looked up the junction table
732
+ expect(mockRegistry.getTable).toHaveBeenCalledWith("posts_tags");
733
+ });
734
+
735
+ it("should handle the exact user scenario that was failing", () => {
736
+ // This is the exact scenario from the user's collection configuration
737
+ const mockTagsCollection = {
738
+ slug: "tags",
739
+ table: "tags"
740
+ };
741
+
742
+ const mockPostsCollection = {
743
+ slug: "posts",
744
+ table: "posts",
745
+ relations: [
746
+ {
747
+ relationName: "tags",
748
+ cardinality: "many" as const,
749
+ direction: "owning" as const,
750
+ through: {
751
+ table: "posts_tags",
752
+ sourceColumn: "post_id",
753
+ targetColumn: "tag_id"
754
+ },
755
+ target: () => mockTagsCollection
756
+ }
757
+ ]
758
+ };
759
+
760
+ // The inverse relation from tags collection (this was failing before the fix)
761
+ const tagsToPostsRelation: Relation = {
762
+ relationName: "posts",
763
+ target: () => mockPostsCollection as unknown as EntityCollection,
764
+ cardinality: "many",
765
+ direction: "inverse",
766
+ inverseRelationName: "tags" // This is the key that should match the owning relation
767
+ };
768
+
769
+ // This should NOT throw "Foreign key column 'tag_id' not found in target table"
770
+ const result = DrizzleConditionBuilder.buildRelationConditions(
771
+ tagsToPostsRelation,
772
+ 42, // tag ID
773
+ mockPostsTable,
774
+ mockTagsTable,
775
+ mockTagsTable.id,
776
+ mockPostsTable.id,
777
+ mockRegistry
778
+ );
779
+
780
+ // Should successfully find the junction table and build conditions
781
+ expect(result.joinConditions).toHaveLength(1);
782
+ expect(result.whereConditions).toHaveLength(1);
783
+
784
+ // Verify it found the junction table correctly
785
+ expect(mockRegistry.getTable).toHaveBeenCalledWith("posts_tags");
786
+ });
787
+
788
+ it("should return appropriate error when no corresponding junction table is found", () => {
789
+ const mockPostsCollection = {
790
+ slug: "posts",
791
+ table: "posts",
792
+ relations: [] // No relations - should fail to find junction table
793
+ };
794
+
795
+ const inverseRelation: Relation = {
796
+ relationName: "posts",
797
+ target: () => mockPostsCollection as unknown as EntityCollection,
798
+ cardinality: "many",
799
+ direction: "inverse",
800
+ inverseRelationName: "nonexistent"
801
+ };
802
+
803
+ // Should fall back to checking foreignKeyOnTarget or throw appropriate error
804
+ expect(() => {
805
+ DrizzleConditionBuilder.buildRelationConditions(
806
+ inverseRelation,
807
+ 5,
808
+ mockPostsTable,
809
+ mockTagsTable,
810
+ mockTagsTable.id,
811
+ mockPostsTable.id,
812
+ mockRegistry
813
+ );
814
+ }).toThrow(/Cannot resolve inverse many relation/);
815
+ });
816
+
817
+ it("should swap source and target columns correctly for inverse relations", () => {
818
+ const mockTagsCollection = {
819
+ slug: "tags",
820
+ table: "tags"
821
+ };
822
+
823
+ const mockPostsCollection = {
824
+ slug: "posts",
825
+ table: "posts",
826
+ relations: [
827
+ {
828
+ relationName: "tags",
829
+ cardinality: "many" as const,
830
+ direction: "owning" as const,
831
+ through: {
832
+ table: "posts_tags",
833
+ sourceColumn: "post_id", // From posts perspective
834
+ targetColumn: "tag_id" // To tags perspective
835
+ },
836
+ target: () => mockTagsCollection
837
+ }
838
+ ]
839
+ };
840
+
841
+ const inverseRelation: Relation = {
842
+ relationName: "posts",
843
+ target: () => mockPostsCollection as unknown as EntityCollection,
844
+ cardinality: "many",
845
+ direction: "inverse",
846
+ inverseRelationName: "tags"
847
+ };
848
+
849
+ const result = DrizzleConditionBuilder.buildRelationConditions(
850
+ inverseRelation,
851
+ 7, // tag ID
852
+ mockPostsTable,
853
+ mockTagsTable,
854
+ mockTagsTable.id,
855
+ mockPostsTable.id,
856
+ mockRegistry
857
+ );
858
+
859
+ // The junction table lookup should swap the columns for inverse direction
860
+ // From tags perspective: sourceColumn becomes "tag_id", targetColumn becomes "post_id"
861
+ expect(result.joinConditions).toHaveLength(1);
862
+ expect(result.whereConditions).toHaveLength(1);
863
+ expect(mockRegistry.getTable).toHaveBeenCalledWith("posts_tags");
864
+ });
865
+
866
+ it("should handle missing inverseRelationName gracefully", () => {
867
+ const mockPostsCollection = {
868
+ slug: "posts",
869
+ table: "posts",
870
+ relations: []
871
+ };
872
+
873
+ const inverseRelationWithoutInverseName: Relation = {
874
+ relationName: "posts",
875
+ target: () => mockPostsCollection as unknown as EntityCollection,
876
+ cardinality: "many",
877
+ direction: "inverse"
878
+ // No inverseRelationName specified
879
+ };
880
+
881
+ // Should throw an appropriate error since it can't find the junction table
882
+ expect(() => {
883
+ DrizzleConditionBuilder.buildRelationConditions(
884
+ inverseRelationWithoutInverseName,
885
+ 5,
886
+ mockPostsTable,
887
+ mockTagsTable,
888
+ mockTagsTable.id,
889
+ mockPostsTable.id,
890
+ mockRegistry
891
+ );
892
+ }).toThrow(/Cannot resolve inverse many relation/);
893
+ });
894
+ });
895
+ });