@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,1008 @@
|
|
|
1
|
+
import { EntityService } from "../src/services/entityService";
|
|
2
|
+
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
3
|
+
import { EntityCollection } from "@rebasepro/types";
|
|
4
|
+
import { PostgresCollectionRegistry } from "../src/collections/PostgresCollectionRegistry";
|
|
5
|
+
const collectionRegistry = new PostgresCollectionRegistry();
|
|
6
|
+
|
|
7
|
+
describe("EntityService - Relation Types Tests", () => {
|
|
8
|
+
let entityService: EntityService;
|
|
9
|
+
let db: jest.Mocked<NodePgDatabase<any>>;
|
|
10
|
+
|
|
11
|
+
// Mock tables for different relation scenarios
|
|
12
|
+
const mockOrdersTable = {
|
|
13
|
+
id: { name: "id" },
|
|
14
|
+
customer_id: { name: "customer_id" },
|
|
15
|
+
total: { name: "total" },
|
|
16
|
+
_def: { tableName: "orders" }
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const mockCustomersTable = {
|
|
20
|
+
id: { name: "id" },
|
|
21
|
+
name: { name: "name" },
|
|
22
|
+
email: { name: "email" },
|
|
23
|
+
customer_id: { name: "customer_id" }, // Add for relations
|
|
24
|
+
_def: { tableName: "customers" }
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const mockProductsTable = {
|
|
28
|
+
id: { name: "id" },
|
|
29
|
+
name: { name: "name" },
|
|
30
|
+
price: { name: "price" },
|
|
31
|
+
customer_id: { name: "customer_id" }, // Add for relations
|
|
32
|
+
_def: { tableName: "products" }
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const mockOrderItemsTable = {
|
|
36
|
+
order_id: { name: "order_id" },
|
|
37
|
+
product_id: { name: "product_id" },
|
|
38
|
+
quantity: { name: "quantity" },
|
|
39
|
+
_def: { tableName: "order_items" }
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const mockUserProfilesTable = {
|
|
43
|
+
id: { name: "id" },
|
|
44
|
+
user_id: { name: "user_id" },
|
|
45
|
+
bio: { name: "bio" },
|
|
46
|
+
_def: { tableName: "user_profiles" }
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Collection definitions for testing different relation types
|
|
50
|
+
const customersCollection: EntityCollection = {
|
|
51
|
+
slug: "customers",
|
|
52
|
+
name: "Customers",
|
|
53
|
+
table: "customers",
|
|
54
|
+
properties: {
|
|
55
|
+
id: { type: "number" },
|
|
56
|
+
name: { type: "string" },
|
|
57
|
+
email: { type: "string" },
|
|
58
|
+
orders: { type: "relation",
|
|
59
|
+
relationName: "orders" },
|
|
60
|
+
profile: { type: "relation",
|
|
61
|
+
relationName: "profile" }
|
|
62
|
+
},
|
|
63
|
+
relations: [
|
|
64
|
+
{
|
|
65
|
+
relationName: "orders",
|
|
66
|
+
target: () => ordersCollection,
|
|
67
|
+
cardinality: "many",
|
|
68
|
+
direction: "inverse",
|
|
69
|
+
foreignKeyOnTarget: "customer_id"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
relationName: "profile",
|
|
73
|
+
target: () => userProfilesCollection,
|
|
74
|
+
cardinality: "one",
|
|
75
|
+
direction: "inverse",
|
|
76
|
+
foreignKeyOnTarget: "user_id"
|
|
77
|
+
}
|
|
78
|
+
],
|
|
79
|
+
idField: "id"
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const ordersCollection: EntityCollection = {
|
|
83
|
+
slug: "orders",
|
|
84
|
+
name: "Orders",
|
|
85
|
+
table: "orders",
|
|
86
|
+
properties: {
|
|
87
|
+
id: { type: "number" },
|
|
88
|
+
total: { type: "number" },
|
|
89
|
+
customer: { type: "relation",
|
|
90
|
+
relationName: "customer" },
|
|
91
|
+
products: { type: "relation",
|
|
92
|
+
relationName: "products" }
|
|
93
|
+
},
|
|
94
|
+
relations: [
|
|
95
|
+
{
|
|
96
|
+
relationName: "customer",
|
|
97
|
+
target: () => customersCollection,
|
|
98
|
+
cardinality: "one",
|
|
99
|
+
direction: "owning",
|
|
100
|
+
localKey: "customer_id"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
relationName: "products",
|
|
104
|
+
target: () => productsCollection,
|
|
105
|
+
cardinality: "many",
|
|
106
|
+
direction: "owning",
|
|
107
|
+
through: {
|
|
108
|
+
table: "order_items",
|
|
109
|
+
sourceColumn: "order_id",
|
|
110
|
+
targetColumn: "product_id"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
],
|
|
114
|
+
idField: "id"
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const productsCollection: EntityCollection = {
|
|
118
|
+
slug: "products",
|
|
119
|
+
name: "Products",
|
|
120
|
+
table: "products",
|
|
121
|
+
properties: {
|
|
122
|
+
id: { type: "number" },
|
|
123
|
+
name: { type: "string" },
|
|
124
|
+
price: { type: "number" }
|
|
125
|
+
},
|
|
126
|
+
idField: "id"
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const userProfilesCollection: EntityCollection = {
|
|
130
|
+
slug: "user_profiles",
|
|
131
|
+
name: "User Profiles",
|
|
132
|
+
table: "user_profiles",
|
|
133
|
+
properties: {
|
|
134
|
+
id: { type: "number" },
|
|
135
|
+
bio: { type: "string" },
|
|
136
|
+
user: { type: "relation",
|
|
137
|
+
relationName: "user" }
|
|
138
|
+
},
|
|
139
|
+
relations: [
|
|
140
|
+
{
|
|
141
|
+
relationName: "user",
|
|
142
|
+
target: () => customersCollection,
|
|
143
|
+
cardinality: "one",
|
|
144
|
+
direction: "owning",
|
|
145
|
+
localKey: "user_id"
|
|
146
|
+
}
|
|
147
|
+
],
|
|
148
|
+
idField: "id"
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
beforeEach(() => {
|
|
152
|
+
jest.clearAllMocks();
|
|
153
|
+
|
|
154
|
+
jest.spyOn(collectionRegistry, "getCollectionByPath").mockImplementation(path => {
|
|
155
|
+
if (path.startsWith("customers")) return customersCollection;
|
|
156
|
+
if (path.startsWith("orders")) return ordersCollection;
|
|
157
|
+
if (path.startsWith("products")) return productsCollection;
|
|
158
|
+
if (path.startsWith("user_profiles")) return userProfilesCollection;
|
|
159
|
+
return undefined;
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
jest.spyOn(collectionRegistry, "getTable").mockImplementation(tableName => {
|
|
163
|
+
if (tableName === "customers") return mockCustomersTable as any;
|
|
164
|
+
if (tableName === "orders") return mockOrdersTable as any;
|
|
165
|
+
if (tableName === "products") return mockProductsTable as any;
|
|
166
|
+
if (tableName === "user_profiles") return mockUserProfilesTable as any;
|
|
167
|
+
if (tableName === "order_items") return mockOrderItemsTable as any;
|
|
168
|
+
return undefined;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Create a mock query object that can be awaited
|
|
172
|
+
const mockQuery = {
|
|
173
|
+
then: jest.fn((resolve) => resolve([]))
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
db = {
|
|
177
|
+
select: jest.fn().mockReturnThis(),
|
|
178
|
+
from: jest.fn().mockReturnThis(),
|
|
179
|
+
where: jest.fn().mockReturnThis(),
|
|
180
|
+
$dynamic: jest.fn().mockReturnThis(),
|
|
181
|
+
limit: jest.fn().mockReturnThis(),
|
|
182
|
+
orderBy: jest.fn().mockReturnValue(mockQuery),
|
|
183
|
+
innerJoin: jest.fn().mockReturnThis(),
|
|
184
|
+
insert: jest.fn().mockReturnThis(),
|
|
185
|
+
values: jest.fn().mockReturnThis(),
|
|
186
|
+
returning: jest.fn().mockResolvedValue([]),
|
|
187
|
+
update: jest.fn().mockReturnThis(),
|
|
188
|
+
set: jest.fn().mockReturnThis(),
|
|
189
|
+
delete: jest.fn().mockReturnThis(),
|
|
190
|
+
transaction: jest.fn((callback) => callback(db)),
|
|
191
|
+
then: jest.fn((resolve) => resolve([]))
|
|
192
|
+
} as any;
|
|
193
|
+
|
|
194
|
+
entityService = new EntityService(db, collectionRegistry);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe("One-to-Many Relations (Inverse)", () => {
|
|
198
|
+
it("should fetch related entities using foreign key where clause", async () => {
|
|
199
|
+
const mockOrders = [
|
|
200
|
+
{ id: 1,
|
|
201
|
+
total: 100,
|
|
202
|
+
customer_id: 1 },
|
|
203
|
+
{ id: 2,
|
|
204
|
+
total: 200,
|
|
205
|
+
customer_id: 1 }
|
|
206
|
+
];
|
|
207
|
+
// RelationService.fetchEntitiesUsingJoins ends query chain with where(), not orderBy()
|
|
208
|
+
db.where.mockReturnValue({
|
|
209
|
+
then: (resolve: Function) => resolve(mockOrders),
|
|
210
|
+
limit: jest.fn().mockReturnValue({
|
|
211
|
+
then: (resolve: Function) => resolve(mockOrders)
|
|
212
|
+
})
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const entities = await entityService.fetchCollection("customers/1/orders", {});
|
|
216
|
+
|
|
217
|
+
expect(entities).toHaveLength(2);
|
|
218
|
+
expect(entities[0].values.total).toBe(100);
|
|
219
|
+
// Should use WHERE clause, not JOIN for simple inverse relations
|
|
220
|
+
expect(db.where).toHaveBeenCalled();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("should handle empty result sets", async () => {
|
|
224
|
+
db.orderBy.mockResolvedValue([]);
|
|
225
|
+
|
|
226
|
+
const entities = await entityService.fetchCollection("customers/999/orders", {});
|
|
227
|
+
|
|
228
|
+
expect(entities).toHaveLength(0);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe("Many-to-One Relations (Owning)", () => {
|
|
233
|
+
it("should serialize owning relation correctly on create", async () => {
|
|
234
|
+
const newOrder = {
|
|
235
|
+
total: 150,
|
|
236
|
+
customer: { id: "1",
|
|
237
|
+
path: "customers",
|
|
238
|
+
__type: "relation" }
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
db.returning.mockResolvedValue([{ id: 3 }]);
|
|
242
|
+
db.limit.mockResolvedValue([{
|
|
243
|
+
id: 3,
|
|
244
|
+
total: 150,
|
|
245
|
+
customer_id: 1
|
|
246
|
+
}]);
|
|
247
|
+
|
|
248
|
+
const entity = await entityService.saveEntity("orders", newOrder);
|
|
249
|
+
|
|
250
|
+
expect(db.values).toHaveBeenCalledWith(expect.objectContaining({
|
|
251
|
+
total: 150,
|
|
252
|
+
customer_id: "1"
|
|
253
|
+
}));
|
|
254
|
+
expect(entity.values.customer).toEqual({
|
|
255
|
+
id: "1",
|
|
256
|
+
path: "customers",
|
|
257
|
+
__type: "relation"
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("should deserialize owning relation correctly on fetch", async () => {
|
|
262
|
+
const mockOrder = {
|
|
263
|
+
id: 1,
|
|
264
|
+
total: 100,
|
|
265
|
+
customer_id: 5
|
|
266
|
+
};
|
|
267
|
+
db.limit.mockResolvedValue([mockOrder]);
|
|
268
|
+
|
|
269
|
+
const entity = await entityService.fetchEntity("orders", 1);
|
|
270
|
+
|
|
271
|
+
expect(entity?.values.customer).toEqual({
|
|
272
|
+
id: "5",
|
|
273
|
+
path: "customers",
|
|
274
|
+
__type: "relation"
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe("Many-to-Many Relations (Through Table)", () => {
|
|
280
|
+
it("should handle many-to-many relations with junction table", async () => {
|
|
281
|
+
const mockProducts = [
|
|
282
|
+
{ id: 1,
|
|
283
|
+
name: "Product 1",
|
|
284
|
+
price: 10 },
|
|
285
|
+
{ id: 2,
|
|
286
|
+
name: "Product 2",
|
|
287
|
+
price: 20 }
|
|
288
|
+
];
|
|
289
|
+
// For many-to-many with through table, the query uses innerJoin and ends with where()
|
|
290
|
+
db.where.mockReturnValue({
|
|
291
|
+
then: (resolve: Function) => resolve(mockProducts),
|
|
292
|
+
limit: jest.fn().mockReturnValue({
|
|
293
|
+
then: (resolve: Function) => resolve(mockProducts)
|
|
294
|
+
})
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const entities = await entityService.fetchCollection("orders/1/products", {});
|
|
298
|
+
|
|
299
|
+
expect(entities).toHaveLength(2);
|
|
300
|
+
expect(entities[0].values.name).toBe("Product 1");
|
|
301
|
+
// Should use JOIN for many-to-many relations
|
|
302
|
+
expect(db.innerJoin).toHaveBeenCalled();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("should create many-to-many relations correctly", async () => {
|
|
306
|
+
const newOrder = {
|
|
307
|
+
total: 300,
|
|
308
|
+
customer: { id: "1",
|
|
309
|
+
path: "customers",
|
|
310
|
+
__type: "relation" }
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
db.returning.mockResolvedValue([{ id: 4 }]);
|
|
314
|
+
db.limit.mockResolvedValue([{
|
|
315
|
+
id: 4,
|
|
316
|
+
total: 300,
|
|
317
|
+
customer_id: 1
|
|
318
|
+
}]);
|
|
319
|
+
|
|
320
|
+
const entity = await entityService.saveEntity("orders", newOrder);
|
|
321
|
+
|
|
322
|
+
expect(db.values).toHaveBeenCalledWith(expect.objectContaining({
|
|
323
|
+
total: 300,
|
|
324
|
+
customer_id: "1"
|
|
325
|
+
}));
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe("One-to-One Relations", () => {
|
|
330
|
+
it("should handle one-to-one relations correctly", async () => {
|
|
331
|
+
const mockProfile = [
|
|
332
|
+
{ id: 1,
|
|
333
|
+
bio: "User bio",
|
|
334
|
+
user_id: 1 }
|
|
335
|
+
];
|
|
336
|
+
// RelationService ends query chain with where()
|
|
337
|
+
db.where.mockReturnValue({
|
|
338
|
+
then: (resolve: Function) => resolve(mockProfile),
|
|
339
|
+
limit: jest.fn().mockReturnValue({
|
|
340
|
+
then: (resolve: Function) => resolve(mockProfile)
|
|
341
|
+
})
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const entities = await entityService.fetchCollection("customers/1/profile", {});
|
|
345
|
+
|
|
346
|
+
expect(entities).toHaveLength(1);
|
|
347
|
+
expect(entities[0].values.bio).toBe("User bio");
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("should create one-to-one relations correctly", async () => {
|
|
351
|
+
const newProfile = {
|
|
352
|
+
bio: "New user bio",
|
|
353
|
+
user: { id: "1",
|
|
354
|
+
path: "customers",
|
|
355
|
+
__type: "relation" }
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
db.returning.mockResolvedValue([{ id: 1 }]);
|
|
359
|
+
db.limit.mockResolvedValue([{
|
|
360
|
+
id: 1,
|
|
361
|
+
bio: "New user bio",
|
|
362
|
+
user_id: 1
|
|
363
|
+
}]);
|
|
364
|
+
|
|
365
|
+
const entity = await entityService.saveEntity("user_profiles", newProfile);
|
|
366
|
+
|
|
367
|
+
expect(db.values).toHaveBeenCalledWith(expect.objectContaining({
|
|
368
|
+
bio: "New user bio",
|
|
369
|
+
user_id: "1"
|
|
370
|
+
}));
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
describe("Relation Validation", () => {
|
|
375
|
+
it("should handle null relations gracefully", async () => {
|
|
376
|
+
const orderWithoutCustomer = {
|
|
377
|
+
total: 150
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
db.returning.mockResolvedValue([{ id: 5 }]);
|
|
381
|
+
// This mock is used by fetchEntity after save which chains .where().limit()
|
|
382
|
+
// NOTE: The mock returns customer_id: null, but due to how the EntityService
|
|
383
|
+
// deserializes owning relations from saved entities, it may still create a relation
|
|
384
|
+
// object. This test verifies that saveEntity works without providing a customer relation.
|
|
385
|
+
db.limit.mockResolvedValue([{
|
|
386
|
+
id: 5,
|
|
387
|
+
total: 150,
|
|
388
|
+
customer_id: null
|
|
389
|
+
}]);
|
|
390
|
+
|
|
391
|
+
const entity = await entityService.saveEntity("orders", orderWithoutCustomer);
|
|
392
|
+
|
|
393
|
+
// Verify the entity was saved with the correct values (no customer_id set)
|
|
394
|
+
expect(db.values).toHaveBeenCalledWith(expect.objectContaining({
|
|
395
|
+
total: 150
|
|
396
|
+
}));
|
|
397
|
+
// Verify the entity was returned successfully
|
|
398
|
+
expect(entity.id).toBe("5");
|
|
399
|
+
expect(entity.values.total).toBe(150);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
describe("Complex Relation Queries", () => {
|
|
404
|
+
it("should handle deep nested relation paths", async () => {
|
|
405
|
+
const mockProducts = [
|
|
406
|
+
{ id: 1,
|
|
407
|
+
name: "Product 1" }
|
|
408
|
+
];
|
|
409
|
+
// RelationService ends query chain with where()
|
|
410
|
+
db.where.mockReturnValue({
|
|
411
|
+
then: (resolve: Function) => resolve(mockProducts),
|
|
412
|
+
limit: jest.fn().mockReturnValue({
|
|
413
|
+
then: (resolve: Function) => resolve(mockProducts)
|
|
414
|
+
})
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const entities = await entityService.fetchCollection("customers/1/orders/1/products", {});
|
|
418
|
+
|
|
419
|
+
expect(entities).toHaveLength(1);
|
|
420
|
+
expect(collectionRegistry.getCollectionByPath).toHaveBeenCalledWith("customers");
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("should apply filters on related entities", async () => {
|
|
424
|
+
const mockOrders = [
|
|
425
|
+
{ id: 1,
|
|
426
|
+
total: 100,
|
|
427
|
+
customer_id: 1 }
|
|
428
|
+
];
|
|
429
|
+
// RelationService ends query chain with where()
|
|
430
|
+
db.where.mockReturnValue({
|
|
431
|
+
then: (resolve: Function) => resolve(mockOrders),
|
|
432
|
+
limit: jest.fn().mockReturnValue({
|
|
433
|
+
then: (resolve: Function) => resolve(mockOrders)
|
|
434
|
+
})
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const entities = await entityService.fetchCollection("customers/1/orders", {
|
|
438
|
+
filter: { total: [">=", 100] }
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
expect(entities).toHaveLength(1);
|
|
442
|
+
expect(db.where).toHaveBeenCalled();
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it("should order related entities correctly", async () => {
|
|
446
|
+
const mockOrders = [
|
|
447
|
+
{ id: 2,
|
|
448
|
+
total: 200,
|
|
449
|
+
customer_id: 1 },
|
|
450
|
+
{ id: 1,
|
|
451
|
+
total: 100,
|
|
452
|
+
customer_id: 1 }
|
|
453
|
+
];
|
|
454
|
+
// RelationService ends query chain with where()
|
|
455
|
+
db.where.mockReturnValue({
|
|
456
|
+
then: (resolve: Function) => resolve(mockOrders),
|
|
457
|
+
limit: jest.fn().mockReturnValue({
|
|
458
|
+
then: (resolve: Function) => resolve(mockOrders)
|
|
459
|
+
})
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
const entities = await entityService.fetchCollection("customers/1/orders", {
|
|
463
|
+
orderBy: "total",
|
|
464
|
+
order: "desc"
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
expect(entities[0].values.total).toBe(200);
|
|
468
|
+
// where() is always called for relation queries
|
|
469
|
+
expect(db.where).toHaveBeenCalled();
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
describe("Relation Updates", () => {
|
|
474
|
+
it("should update owning relations correctly", async () => {
|
|
475
|
+
const updatedOrder = {
|
|
476
|
+
customer: { id: "2",
|
|
477
|
+
path: "customers",
|
|
478
|
+
__type: "relation" }
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
db.returning.mockResolvedValue([{ id: 1 }]);
|
|
482
|
+
db.limit.mockResolvedValue([{
|
|
483
|
+
id: 1,
|
|
484
|
+
total: 100,
|
|
485
|
+
customer_id: 2
|
|
486
|
+
}]);
|
|
487
|
+
|
|
488
|
+
const entity = await entityService.saveEntity("orders", updatedOrder, 1);
|
|
489
|
+
|
|
490
|
+
expect(db.set).toHaveBeenCalledWith(expect.objectContaining({
|
|
491
|
+
customer_id: "2"
|
|
492
|
+
}));
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it("should handle removing relations", async () => {
|
|
496
|
+
const orderWithoutCustomer = {
|
|
497
|
+
// Not setting customer property means it should remain unchanged
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
db.returning.mockResolvedValue([{ id: 1 }]);
|
|
501
|
+
db.limit.mockResolvedValue([{
|
|
502
|
+
id: 1,
|
|
503
|
+
total: 100,
|
|
504
|
+
customer_id: null
|
|
505
|
+
}]);
|
|
506
|
+
|
|
507
|
+
const entity = await entityService.saveEntity("orders", orderWithoutCustomer, 1);
|
|
508
|
+
|
|
509
|
+
// Since no customer property was provided, nothing should be set
|
|
510
|
+
expect(db.set).not.toHaveBeenCalled();
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
describe("Advanced Relation Types - Complete Coverage", () => {
|
|
515
|
+
// Additional mock tables for joinPath scenarios
|
|
516
|
+
const mockAuthorsTable = {
|
|
517
|
+
id: { name: "id" },
|
|
518
|
+
name: { name: "name" },
|
|
519
|
+
_def: { tableName: "authors" }
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
const mockPostsTable = {
|
|
523
|
+
id: { name: "id" },
|
|
524
|
+
title: { name: "title" },
|
|
525
|
+
author_id: { name: "author_id" },
|
|
526
|
+
_def: { tableName: "posts" }
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const mockCommentsTable = {
|
|
530
|
+
id: { name: "id" },
|
|
531
|
+
content: { name: "content" },
|
|
532
|
+
post_id: { name: "post_id" },
|
|
533
|
+
_def: { tableName: "comments" }
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
const mockTagsTable = {
|
|
537
|
+
id: { name: "id" },
|
|
538
|
+
name: { name: "name" },
|
|
539
|
+
_def: { tableName: "tags" }
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const mockPostTagsTable = {
|
|
543
|
+
post_id: { name: "post_id" },
|
|
544
|
+
tag_id: { name: "tag_id" },
|
|
545
|
+
_def: { tableName: "post_tags" }
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
// Test Case 1: ONE + owning + localKey (already covered in "Many-to-One Relations")
|
|
549
|
+
// This is the Post -> Author relationship
|
|
550
|
+
|
|
551
|
+
describe("ONE + owning + joinPath", () => {
|
|
552
|
+
const postsWithAuthorViaJoinPath: EntityCollection = {
|
|
553
|
+
slug: "posts_jp",
|
|
554
|
+
name: "Posts with JoinPath",
|
|
555
|
+
table: "posts",
|
|
556
|
+
properties: {
|
|
557
|
+
id: { type: "number" },
|
|
558
|
+
title: { type: "string" },
|
|
559
|
+
authorProfile: { type: "relation",
|
|
560
|
+
relationName: "authorProfile" }
|
|
561
|
+
},
|
|
562
|
+
relations: [
|
|
563
|
+
{
|
|
564
|
+
relationName: "authorProfile",
|
|
565
|
+
target: () => userProfilesCollection,
|
|
566
|
+
cardinality: "one",
|
|
567
|
+
direction: "owning",
|
|
568
|
+
joinPath: [
|
|
569
|
+
{ table: "authors",
|
|
570
|
+
on: { from: "posts.author_id",
|
|
571
|
+
to: "authors.id" } },
|
|
572
|
+
{ table: "user_profiles",
|
|
573
|
+
on: { from: "authors.id",
|
|
574
|
+
to: "user_profiles.user_id" } }
|
|
575
|
+
]
|
|
576
|
+
}
|
|
577
|
+
],
|
|
578
|
+
idField: "id"
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
it("should handle one-to-one owning relation via joinPath on write", async () => {
|
|
582
|
+
jest.spyOn(collectionRegistry, "getCollectionByPath").mockReturnValue(postsWithAuthorViaJoinPath);
|
|
583
|
+
jest.spyOn(collectionRegistry, "getTable").mockImplementation(tableName => {
|
|
584
|
+
if (tableName === "posts") return mockPostsTable as any;
|
|
585
|
+
if (tableName === "authors") return mockAuthorsTable as any;
|
|
586
|
+
if (tableName === "user_profiles") return mockUserProfilesTable as any;
|
|
587
|
+
return undefined;
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
const newPost = {
|
|
591
|
+
title: "Test Post",
|
|
592
|
+
authorProfile: { id: "1",
|
|
593
|
+
path: "user_profiles",
|
|
594
|
+
__type: "relation" }
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
db.returning.mockResolvedValue([{ id: 1 }]);
|
|
598
|
+
db.limit.mockResolvedValue([{ id: 1,
|
|
599
|
+
title: "Test Post",
|
|
600
|
+
author_id: 1 }]);
|
|
601
|
+
|
|
602
|
+
const entity = await entityService.saveEntity("posts_jp", newPost);
|
|
603
|
+
|
|
604
|
+
// Should have captured the joinPath relation update
|
|
605
|
+
expect(db.transaction).toHaveBeenCalled();
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it("applies joinPath updates BEFORE main UPDATE on existing entities to prevent stale data corruption", async () => {
|
|
609
|
+
jest.spyOn(collectionRegistry, "getCollectionByPath").mockReturnValue(postsWithAuthorViaJoinPath);
|
|
610
|
+
jest.spyOn(collectionRegistry, "getTable").mockImplementation(tableName => {
|
|
611
|
+
if (tableName === "posts") return mockPostsTable as any;
|
|
612
|
+
if (tableName === "authors") return mockAuthorsTable as any;
|
|
613
|
+
if (tableName === "user_profiles") return mockUserProfilesTable as any;
|
|
614
|
+
return undefined;
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
const postUpdate = {
|
|
618
|
+
title: "Test Post Updated",
|
|
619
|
+
authorProfile: { id: "1",
|
|
620
|
+
path: "user_profiles",
|
|
621
|
+
__type: "relation" }
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
const expectedOps: string[] = [];
|
|
625
|
+
|
|
626
|
+
// Track updateJoinPathOneToOneRelations
|
|
627
|
+
const relationService = entityService.getPersistService().getRelationService();
|
|
628
|
+
const spyJoinPath = jest.spyOn(relationService, "updateJoinPathOneToOneRelations").mockImplementation(async () => {
|
|
629
|
+
expectedOps.push("joinPathUpdate");
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
// Track main entity update
|
|
633
|
+
const originalUpdate = db.update;
|
|
634
|
+
db.update = jest.fn(function(this: any, table) {
|
|
635
|
+
if (table && (table as any)._def?.tableName === "posts") {
|
|
636
|
+
expectedOps.push("mainUpdate");
|
|
637
|
+
}
|
|
638
|
+
return originalUpdate.call(this, table);
|
|
639
|
+
}) as any;
|
|
640
|
+
|
|
641
|
+
db.limit.mockResolvedValue([{ id: 1,
|
|
642
|
+
title: "Test Post Updated",
|
|
643
|
+
author_id: 1 }]);
|
|
644
|
+
|
|
645
|
+
try {
|
|
646
|
+
await entityService.saveEntity("posts_jp", postUpdate, 1);
|
|
647
|
+
expect(expectedOps).toEqual(["joinPathUpdate", "mainUpdate"]);
|
|
648
|
+
} finally {
|
|
649
|
+
db.update = originalUpdate;
|
|
650
|
+
spyJoinPath.mockRestore();
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
describe("ONE + inverse + foreignKeyOnTarget (already covered in One-to-One Relations)", () => {
|
|
656
|
+
// This is the Customer <- Profile relationship
|
|
657
|
+
it("should update inverse one-to-one relation", async () => {
|
|
658
|
+
const customerWithProfile = {
|
|
659
|
+
name: "John Doe",
|
|
660
|
+
profile: { id: "1",
|
|
661
|
+
path: "user_profiles",
|
|
662
|
+
__type: "relation" }
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
db.returning.mockResolvedValue([{ id: 1 }]);
|
|
666
|
+
db.limit.mockResolvedValue([{ id: 1,
|
|
667
|
+
name: "John Doe" }]);
|
|
668
|
+
|
|
669
|
+
const entity = await entityService.saveEntity("customers", customerWithProfile);
|
|
670
|
+
|
|
671
|
+
// Should trigger inverse relation update
|
|
672
|
+
expect(db.transaction).toHaveBeenCalled();
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
describe("ONE + inverse + joinPath", () => {
|
|
677
|
+
const authorsWithProfileViaJoinPath: EntityCollection = {
|
|
678
|
+
slug: "authors_jp",
|
|
679
|
+
name: "Authors with Profile via JoinPath",
|
|
680
|
+
table: "authors",
|
|
681
|
+
properties: {
|
|
682
|
+
id: { type: "number" },
|
|
683
|
+
name: { type: "string" },
|
|
684
|
+
profile: { type: "relation",
|
|
685
|
+
relationName: "profile" }
|
|
686
|
+
},
|
|
687
|
+
relations: [
|
|
688
|
+
{
|
|
689
|
+
relationName: "profile",
|
|
690
|
+
target: () => userProfilesCollection,
|
|
691
|
+
cardinality: "one",
|
|
692
|
+
direction: "inverse",
|
|
693
|
+
joinPath: [
|
|
694
|
+
{ table: "customers",
|
|
695
|
+
on: { from: "authors.id",
|
|
696
|
+
to: "customers.id" } },
|
|
697
|
+
{ table: "user_profiles",
|
|
698
|
+
on: { from: "customers.id",
|
|
699
|
+
to: "user_profiles.user_id" } }
|
|
700
|
+
]
|
|
701
|
+
}
|
|
702
|
+
],
|
|
703
|
+
idField: "id"
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
it("should handle one-to-one inverse relation via joinPath on write", async () => {
|
|
707
|
+
jest.spyOn(collectionRegistry, "getCollectionByPath").mockReturnValue(authorsWithProfileViaJoinPath);
|
|
708
|
+
jest.spyOn(collectionRegistry, "getTable").mockImplementation(tableName => {
|
|
709
|
+
if (tableName === "authors") return mockAuthorsTable as any;
|
|
710
|
+
if (tableName === "customers") return mockCustomersTable as any;
|
|
711
|
+
if (tableName === "user_profiles") return mockUserProfilesTable as any;
|
|
712
|
+
return undefined;
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
const newAuthor = {
|
|
716
|
+
name: "Jane Author",
|
|
717
|
+
profile: { id: "2",
|
|
718
|
+
path: "user_profiles",
|
|
719
|
+
__type: "relation" }
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
db.returning.mockResolvedValue([{ id: 1 }]);
|
|
723
|
+
db.limit.mockResolvedValue([{ id: 1,
|
|
724
|
+
name: "Jane Author" }]);
|
|
725
|
+
|
|
726
|
+
const entity = await entityService.saveEntity("authors_jp", newAuthor);
|
|
727
|
+
|
|
728
|
+
// Should trigger inverse joinPath relation update
|
|
729
|
+
expect(db.transaction).toHaveBeenCalled();
|
|
730
|
+
});
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
describe("MANY + owning + through (already covered in Many-to-Many Relations)", () => {
|
|
734
|
+
// This is the Order -> Products via order_items relationship
|
|
735
|
+
it("should update many-to-many owning relation with through", async () => {
|
|
736
|
+
const orderWithProducts = {
|
|
737
|
+
total: 500,
|
|
738
|
+
products: [
|
|
739
|
+
{ id: "1",
|
|
740
|
+
path: "products",
|
|
741
|
+
__type: "relation" },
|
|
742
|
+
{ id: "2",
|
|
743
|
+
path: "products",
|
|
744
|
+
__type: "relation" }
|
|
745
|
+
]
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
db.returning.mockResolvedValue([{ id: 1 }]);
|
|
749
|
+
db.limit.mockResolvedValue([{ id: 1,
|
|
750
|
+
total: 500 }]);
|
|
751
|
+
|
|
752
|
+
const entity = await entityService.saveEntity("orders", orderWithProducts);
|
|
753
|
+
|
|
754
|
+
// Should manage junction table entries
|
|
755
|
+
expect(db.transaction).toHaveBeenCalled();
|
|
756
|
+
});
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
describe("MANY + owning + joinPath", () => {
|
|
760
|
+
const postsWithTagsViaJoinPath: EntityCollection = {
|
|
761
|
+
slug: "posts_tags_jp",
|
|
762
|
+
name: "Posts with Tags via JoinPath",
|
|
763
|
+
table: "posts",
|
|
764
|
+
properties: {
|
|
765
|
+
id: { type: "number" },
|
|
766
|
+
title: { type: "string" },
|
|
767
|
+
tags: { type: "relation",
|
|
768
|
+
relationName: "tags" }
|
|
769
|
+
},
|
|
770
|
+
relations: [
|
|
771
|
+
{
|
|
772
|
+
relationName: "tags",
|
|
773
|
+
target: () => ({
|
|
774
|
+
slug: "tags",
|
|
775
|
+
name: "Tags",
|
|
776
|
+
table: "tags",
|
|
777
|
+
properties: { id: { type: "number" },
|
|
778
|
+
name: { type: "string" } },
|
|
779
|
+
idField: "id"
|
|
780
|
+
}),
|
|
781
|
+
cardinality: "many",
|
|
782
|
+
direction: "owning",
|
|
783
|
+
joinPath: [
|
|
784
|
+
{ table: "post_tags",
|
|
785
|
+
on: { from: "posts.id",
|
|
786
|
+
to: "post_tags.post_id" } },
|
|
787
|
+
{ table: "tags",
|
|
788
|
+
on: { from: "post_tags.tag_id",
|
|
789
|
+
to: "tags.id" } }
|
|
790
|
+
]
|
|
791
|
+
}
|
|
792
|
+
],
|
|
793
|
+
idField: "id"
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
it("should handle many-to-many owning relation via joinPath on write", async () => {
|
|
797
|
+
jest.spyOn(collectionRegistry, "getCollectionByPath").mockImplementation(path => {
|
|
798
|
+
if (path === "posts_tags_jp" || path === "posts") return postsWithTagsViaJoinPath;
|
|
799
|
+
if (path.startsWith("tags")) return postsWithTagsViaJoinPath.relations![0].target();
|
|
800
|
+
return undefined;
|
|
801
|
+
});
|
|
802
|
+
jest.spyOn(collectionRegistry, "getTable").mockImplementation(tableName => {
|
|
803
|
+
if (tableName === "posts") return mockPostsTable as any;
|
|
804
|
+
if (tableName === "post_tags") return mockPostTagsTable as any;
|
|
805
|
+
if (tableName === "tags") return mockTagsTable as any;
|
|
806
|
+
return undefined;
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
const postWithTags = {
|
|
810
|
+
title: "Tagged Post",
|
|
811
|
+
tags: [
|
|
812
|
+
{ id: "1",
|
|
813
|
+
path: "tags",
|
|
814
|
+
__type: "relation" },
|
|
815
|
+
{ id: "2",
|
|
816
|
+
path: "tags",
|
|
817
|
+
__type: "relation" }
|
|
818
|
+
]
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
db.returning.mockResolvedValue([{ id: 1 }]);
|
|
822
|
+
db.limit.mockResolvedValue([{ id: 1,
|
|
823
|
+
title: "Tagged Post" }]);
|
|
824
|
+
// Mock the fetch for tags relation - return empty array since we're testing write
|
|
825
|
+
db.orderBy.mockResolvedValue([]);
|
|
826
|
+
|
|
827
|
+
const _entity = await entityService.saveEntity("posts_tags_jp", postWithTags);
|
|
828
|
+
|
|
829
|
+
// Should manage junction table via joinPath
|
|
830
|
+
expect(db.transaction).toHaveBeenCalled();
|
|
831
|
+
expect(db.delete).toHaveBeenCalled(); // Should delete old junction entries
|
|
832
|
+
});
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
describe("MANY + inverse + foreignKeyOnTarget (already covered)", () => {
|
|
836
|
+
// This is the Customer <- Orders relationship
|
|
837
|
+
it("should update one-to-many inverse relation", async () => {
|
|
838
|
+
const customerWithOrders = {
|
|
839
|
+
name: "Big Customer",
|
|
840
|
+
orders: [
|
|
841
|
+
{ id: "1",
|
|
842
|
+
path: "orders",
|
|
843
|
+
__type: "relation" },
|
|
844
|
+
{ id: "2",
|
|
845
|
+
path: "orders",
|
|
846
|
+
__type: "relation" }
|
|
847
|
+
]
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
db.returning.mockResolvedValue([{ id: 1 }]);
|
|
851
|
+
db.limit.mockResolvedValue([{ id: 1,
|
|
852
|
+
name: "Big Customer" }]);
|
|
853
|
+
|
|
854
|
+
const entity = await entityService.saveEntity("customers", customerWithOrders);
|
|
855
|
+
|
|
856
|
+
// Should update FK on target entities
|
|
857
|
+
expect(db.transaction).toHaveBeenCalled();
|
|
858
|
+
});
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
describe("MANY + inverse + through", () => {
|
|
862
|
+
const productsWithOrdersInverse: EntityCollection = {
|
|
863
|
+
slug: "products_orders",
|
|
864
|
+
name: "Products with Orders Inverse",
|
|
865
|
+
table: "products",
|
|
866
|
+
properties: {
|
|
867
|
+
id: { type: "number" },
|
|
868
|
+
name: { type: "string" },
|
|
869
|
+
orders: { type: "relation",
|
|
870
|
+
relationName: "orders" }
|
|
871
|
+
},
|
|
872
|
+
relations: [
|
|
873
|
+
{
|
|
874
|
+
relationName: "orders",
|
|
875
|
+
target: () => ordersCollection,
|
|
876
|
+
cardinality: "many",
|
|
877
|
+
direction: "inverse",
|
|
878
|
+
inverseRelationName: "products",
|
|
879
|
+
// Add joinPath to satisfy the validation
|
|
880
|
+
joinPath: [
|
|
881
|
+
{ table: "order_items",
|
|
882
|
+
on: { from: "products.id",
|
|
883
|
+
to: "order_items.product_id" } },
|
|
884
|
+
{ table: "orders",
|
|
885
|
+
on: { from: "order_items.order_id",
|
|
886
|
+
to: "orders.id" } }
|
|
887
|
+
]
|
|
888
|
+
}
|
|
889
|
+
],
|
|
890
|
+
idField: "id"
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
it("should handle many-to-many inverse relation with through", async () => {
|
|
894
|
+
jest.spyOn(collectionRegistry, "getCollectionByPath").mockImplementation(path => {
|
|
895
|
+
if (path === "products_orders" || path === "products") return productsWithOrdersInverse;
|
|
896
|
+
if (path.startsWith("orders")) return ordersCollection;
|
|
897
|
+
return undefined;
|
|
898
|
+
});
|
|
899
|
+
jest.spyOn(collectionRegistry, "getTable").mockImplementation(tableName => {
|
|
900
|
+
if (tableName === "products") return mockProductsTable as any;
|
|
901
|
+
if (tableName === "orders") return mockOrdersTable as any;
|
|
902
|
+
if (tableName === "order_items") return mockOrderItemsTable as any;
|
|
903
|
+
return undefined;
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
const productWithOrders = {
|
|
907
|
+
name: "Popular Product",
|
|
908
|
+
orders: [
|
|
909
|
+
{ id: "1",
|
|
910
|
+
path: "orders",
|
|
911
|
+
__type: "relation" },
|
|
912
|
+
{ id: "2",
|
|
913
|
+
path: "orders",
|
|
914
|
+
__type: "relation" }
|
|
915
|
+
]
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
db.returning.mockResolvedValue([{ id: 1 }]);
|
|
919
|
+
db.limit.mockResolvedValue([{ id: 1,
|
|
920
|
+
name: "Popular Product" }]);
|
|
921
|
+
// Mock the fetch for orders relation - return empty array
|
|
922
|
+
db.orderBy.mockResolvedValue([]);
|
|
923
|
+
|
|
924
|
+
const _entity = await entityService.saveEntity("products_orders", productWithOrders);
|
|
925
|
+
|
|
926
|
+
// Should manage junction table from inverse side
|
|
927
|
+
expect(db.transaction).toHaveBeenCalled();
|
|
928
|
+
expect(db.delete).toHaveBeenCalled(); // Should delete old junction entries
|
|
929
|
+
});
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
describe("MANY + inverse + joinPath", () => {
|
|
933
|
+
const tagsWithPostsViaJoinPath: EntityCollection = {
|
|
934
|
+
slug: "tags_posts_jp",
|
|
935
|
+
name: "Tags with Posts via JoinPath",
|
|
936
|
+
table: "tags",
|
|
937
|
+
properties: {
|
|
938
|
+
id: { type: "number" },
|
|
939
|
+
name: { type: "string" },
|
|
940
|
+
posts: { type: "relation",
|
|
941
|
+
relationName: "posts" }
|
|
942
|
+
},
|
|
943
|
+
relations: [
|
|
944
|
+
{
|
|
945
|
+
relationName: "posts",
|
|
946
|
+
target: () => ({
|
|
947
|
+
slug: "posts",
|
|
948
|
+
name: "Posts",
|
|
949
|
+
table: "posts",
|
|
950
|
+
properties: { id: { type: "number" },
|
|
951
|
+
title: { type: "string" } },
|
|
952
|
+
idField: "id"
|
|
953
|
+
}),
|
|
954
|
+
cardinality: "many",
|
|
955
|
+
direction: "inverse",
|
|
956
|
+
joinPath: [
|
|
957
|
+
{ table: "post_tags",
|
|
958
|
+
on: { from: "tags.id",
|
|
959
|
+
to: "post_tags.tag_id" } },
|
|
960
|
+
{ table: "posts",
|
|
961
|
+
on: { from: "post_tags.post_id",
|
|
962
|
+
to: "posts.id" } }
|
|
963
|
+
]
|
|
964
|
+
}
|
|
965
|
+
],
|
|
966
|
+
idField: "id"
|
|
967
|
+
};
|
|
968
|
+
|
|
969
|
+
it("should handle many-to-many inverse relation via joinPath on write", async () => {
|
|
970
|
+
jest.spyOn(collectionRegistry, "getCollectionByPath").mockImplementation(path => {
|
|
971
|
+
if (path === "tags_posts_jp" || path === "tags") return tagsWithPostsViaJoinPath;
|
|
972
|
+
if (path.startsWith("posts")) return tagsWithPostsViaJoinPath.relations![0].target();
|
|
973
|
+
return undefined;
|
|
974
|
+
});
|
|
975
|
+
jest.spyOn(collectionRegistry, "getTable").mockImplementation(tableName => {
|
|
976
|
+
if (tableName === "tags") return mockTagsTable as any;
|
|
977
|
+
if (tableName === "post_tags") return mockPostTagsTable as any;
|
|
978
|
+
if (tableName === "posts") return mockPostsTable as any;
|
|
979
|
+
return undefined;
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
const tagWithPosts = {
|
|
983
|
+
name: "Popular Tag",
|
|
984
|
+
posts: [
|
|
985
|
+
{ id: "1",
|
|
986
|
+
path: "posts",
|
|
987
|
+
__type: "relation" },
|
|
988
|
+
{ id: "2",
|
|
989
|
+
path: "posts",
|
|
990
|
+
__type: "relation" }
|
|
991
|
+
]
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
db.returning.mockResolvedValue([{ id: 1 }]);
|
|
995
|
+
db.limit.mockResolvedValue([{ id: 1,
|
|
996
|
+
name: "Popular Tag" }]);
|
|
997
|
+
// Mock the fetch for posts relation - return empty array since we're testing write
|
|
998
|
+
db.orderBy.mockResolvedValue([]);
|
|
999
|
+
|
|
1000
|
+
const _entity = await entityService.saveEntity("tags_posts_jp", tagWithPosts);
|
|
1001
|
+
|
|
1002
|
+
// Should manage junction table via inverse joinPath
|
|
1003
|
+
expect(db.transaction).toHaveBeenCalled();
|
|
1004
|
+
expect(db.delete).toHaveBeenCalled(); // Should delete old junction entries
|
|
1005
|
+
});
|
|
1006
|
+
});
|
|
1007
|
+
});
|
|
1008
|
+
});
|