@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,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
|
+
});
|