@rebasepro/server-postgresql 0.0.1-canary.09e5ec5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/LICENSE +6 -0
  2. package/README.md +106 -0
  3. package/build-errors.txt +37 -0
  4. package/dist/common/src/collections/CollectionRegistry.d.ts +56 -0
  5. package/dist/common/src/collections/index.d.ts +1 -0
  6. package/dist/common/src/data/buildRebaseData.d.ts +14 -0
  7. package/dist/common/src/index.d.ts +3 -0
  8. package/dist/common/src/util/builders.d.ts +57 -0
  9. package/dist/common/src/util/callbacks.d.ts +6 -0
  10. package/dist/common/src/util/collections.d.ts +11 -0
  11. package/dist/common/src/util/common.d.ts +2 -0
  12. package/dist/common/src/util/conditions.d.ts +26 -0
  13. package/dist/common/src/util/entities.d.ts +58 -0
  14. package/dist/common/src/util/enums.d.ts +3 -0
  15. package/dist/common/src/util/index.d.ts +16 -0
  16. package/dist/common/src/util/navigation_from_path.d.ts +34 -0
  17. package/dist/common/src/util/navigation_utils.d.ts +20 -0
  18. package/dist/common/src/util/parent_references_from_path.d.ts +6 -0
  19. package/dist/common/src/util/paths.d.ts +14 -0
  20. package/dist/common/src/util/permissions.d.ts +5 -0
  21. package/dist/common/src/util/references.d.ts +2 -0
  22. package/dist/common/src/util/relations.d.ts +22 -0
  23. package/dist/common/src/util/resolutions.d.ts +72 -0
  24. package/dist/common/src/util/storage.d.ts +24 -0
  25. package/dist/index.es.js +11298 -0
  26. package/dist/index.es.js.map +1 -0
  27. package/dist/index.umd.js +11306 -0
  28. package/dist/index.umd.js.map +1 -0
  29. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +100 -0
  30. package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +40 -0
  31. package/dist/server-postgresql/src/auth/ensure-tables.d.ts +6 -0
  32. package/dist/server-postgresql/src/auth/services.d.ts +192 -0
  33. package/dist/server-postgresql/src/cli.d.ts +1 -0
  34. package/dist/server-postgresql/src/collections/PostgresCollectionRegistry.d.ts +43 -0
  35. package/dist/server-postgresql/src/connection.d.ts +40 -0
  36. package/dist/server-postgresql/src/data-transformer.d.ts +58 -0
  37. package/dist/server-postgresql/src/databasePoolManager.d.ts +20 -0
  38. package/dist/server-postgresql/src/history/HistoryService.d.ts +71 -0
  39. package/dist/server-postgresql/src/history/ensure-history-table.d.ts +7 -0
  40. package/dist/server-postgresql/src/index.d.ts +13 -0
  41. package/dist/server-postgresql/src/interfaces.d.ts +18 -0
  42. package/dist/server-postgresql/src/schema/auth-schema.d.ts +868 -0
  43. package/dist/server-postgresql/src/schema/doctor-cli.d.ts +2 -0
  44. package/dist/server-postgresql/src/schema/doctor.d.ts +43 -0
  45. package/dist/server-postgresql/src/schema/generate-drizzle-schema-logic.d.ts +2 -0
  46. package/dist/server-postgresql/src/schema/generate-drizzle-schema.d.ts +1 -0
  47. package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +82 -0
  48. package/dist/server-postgresql/src/schema/introspect-db.d.ts +1 -0
  49. package/dist/server-postgresql/src/schema/test-schema.d.ts +24 -0
  50. package/dist/server-postgresql/src/services/BranchService.d.ts +47 -0
  51. package/dist/server-postgresql/src/services/EntityFetchService.d.ts +209 -0
  52. package/dist/server-postgresql/src/services/EntityPersistService.d.ts +41 -0
  53. package/dist/server-postgresql/src/services/RelationService.d.ts +98 -0
  54. package/dist/server-postgresql/src/services/entity-helpers.d.ts +38 -0
  55. package/dist/server-postgresql/src/services/entityService.d.ts +104 -0
  56. package/dist/server-postgresql/src/services/index.d.ts +4 -0
  57. package/dist/server-postgresql/src/services/realtimeService.d.ts +188 -0
  58. package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +116 -0
  59. package/dist/server-postgresql/src/websocket.d.ts +5 -0
  60. package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
  61. package/dist/types/src/controllers/auth.d.ts +119 -0
  62. package/dist/types/src/controllers/client.d.ts +170 -0
  63. package/dist/types/src/controllers/collection_registry.d.ts +45 -0
  64. package/dist/types/src/controllers/customization_controller.d.ts +60 -0
  65. package/dist/types/src/controllers/data.d.ts +168 -0
  66. package/dist/types/src/controllers/data_driver.d.ts +160 -0
  67. package/dist/types/src/controllers/database_admin.d.ts +11 -0
  68. package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
  69. package/dist/types/src/controllers/effective_role.d.ts +4 -0
  70. package/dist/types/src/controllers/email.d.ts +34 -0
  71. package/dist/types/src/controllers/index.d.ts +18 -0
  72. package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
  73. package/dist/types/src/controllers/navigation.d.ts +213 -0
  74. package/dist/types/src/controllers/registry.d.ts +54 -0
  75. package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
  76. package/dist/types/src/controllers/side_entity_controller.d.ts +90 -0
  77. package/dist/types/src/controllers/snackbar.d.ts +24 -0
  78. package/dist/types/src/controllers/storage.d.ts +171 -0
  79. package/dist/types/src/index.d.ts +4 -0
  80. package/dist/types/src/rebase_context.d.ts +105 -0
  81. package/dist/types/src/types/backend.d.ts +536 -0
  82. package/dist/types/src/types/builders.d.ts +15 -0
  83. package/dist/types/src/types/chips.d.ts +5 -0
  84. package/dist/types/src/types/collections.d.ts +856 -0
  85. package/dist/types/src/types/cron.d.ts +102 -0
  86. package/dist/types/src/types/data_source.d.ts +64 -0
  87. package/dist/types/src/types/entities.d.ts +145 -0
  88. package/dist/types/src/types/entity_actions.d.ts +98 -0
  89. package/dist/types/src/types/entity_callbacks.d.ts +173 -0
  90. package/dist/types/src/types/entity_link_builder.d.ts +7 -0
  91. package/dist/types/src/types/entity_overrides.d.ts +10 -0
  92. package/dist/types/src/types/entity_views.d.ts +61 -0
  93. package/dist/types/src/types/export_import.d.ts +21 -0
  94. package/dist/types/src/types/index.d.ts +23 -0
  95. package/dist/types/src/types/locales.d.ts +4 -0
  96. package/dist/types/src/types/modify_collections.d.ts +5 -0
  97. package/dist/types/src/types/plugins.d.ts +279 -0
  98. package/dist/types/src/types/properties.d.ts +1176 -0
  99. package/dist/types/src/types/property_config.d.ts +70 -0
  100. package/dist/types/src/types/relations.d.ts +336 -0
  101. package/dist/types/src/types/slots.d.ts +252 -0
  102. package/dist/types/src/types/translations.d.ts +870 -0
  103. package/dist/types/src/types/user_management_delegate.d.ts +121 -0
  104. package/dist/types/src/types/websockets.d.ts +78 -0
  105. package/dist/types/src/users/index.d.ts +2 -0
  106. package/dist/types/src/users/roles.d.ts +22 -0
  107. package/dist/types/src/users/user.d.ts +46 -0
  108. package/drizzle-test/0000_woozy_junta.sql +6 -0
  109. package/drizzle-test/0001_youthful_arachne.sql +1 -0
  110. package/drizzle-test/0002_lively_dragon_lord.sql +2 -0
  111. package/drizzle-test/0003_mean_king_cobra.sql +2 -0
  112. package/drizzle-test/meta/0000_snapshot.json +47 -0
  113. package/drizzle-test/meta/0001_snapshot.json +48 -0
  114. package/drizzle-test/meta/0002_snapshot.json +38 -0
  115. package/drizzle-test/meta/0003_snapshot.json +48 -0
  116. package/drizzle-test/meta/_journal.json +34 -0
  117. package/drizzle-test-out/0000_tan_trauma.sql +6 -0
  118. package/drizzle-test-out/0001_rapid_drax.sql +1 -0
  119. package/drizzle-test-out/meta/0000_snapshot.json +44 -0
  120. package/drizzle-test-out/meta/0001_snapshot.json +54 -0
  121. package/drizzle-test-out/meta/_journal.json +20 -0
  122. package/drizzle.test.config.ts +10 -0
  123. package/jest-all.log +3128 -0
  124. package/jest.log +49 -0
  125. package/package.json +92 -0
  126. package/scratch.ts +41 -0
  127. package/src/PostgresBackendDriver.ts +1008 -0
  128. package/src/PostgresBootstrapper.ts +231 -0
  129. package/src/auth/ensure-tables.ts +381 -0
  130. package/src/auth/services.ts +799 -0
  131. package/src/cli.ts +648 -0
  132. package/src/collections/PostgresCollectionRegistry.ts +96 -0
  133. package/src/connection.ts +84 -0
  134. package/src/data-transformer.ts +608 -0
  135. package/src/databasePoolManager.ts +85 -0
  136. package/src/history/HistoryService.ts +248 -0
  137. package/src/history/ensure-history-table.ts +45 -0
  138. package/src/index.ts +13 -0
  139. package/src/interfaces.ts +60 -0
  140. package/src/schema/auth-schema.ts +169 -0
  141. package/src/schema/doctor-cli.ts +47 -0
  142. package/src/schema/doctor.ts +595 -0
  143. package/src/schema/generate-drizzle-schema-logic.ts +765 -0
  144. package/src/schema/generate-drizzle-schema.ts +151 -0
  145. package/src/schema/introspect-db-logic.ts +542 -0
  146. package/src/schema/introspect-db.ts +211 -0
  147. package/src/schema/test-schema.ts +11 -0
  148. package/src/services/BranchService.ts +237 -0
  149. package/src/services/EntityFetchService.ts +1576 -0
  150. package/src/services/EntityPersistService.ts +349 -0
  151. package/src/services/RelationService.ts +1274 -0
  152. package/src/services/entity-helpers.ts +147 -0
  153. package/src/services/entityService.ts +211 -0
  154. package/src/services/index.ts +13 -0
  155. package/src/services/realtimeService.ts +1034 -0
  156. package/src/utils/drizzle-conditions.ts +1000 -0
  157. package/src/websocket.ts +518 -0
  158. package/test/auth-services.test.ts +661 -0
  159. package/test/batch-many-to-many-regression.test.ts +573 -0
  160. package/test/branchService.test.ts +367 -0
  161. package/test/data-transformer-hardening.test.ts +417 -0
  162. package/test/data-transformer.test.ts +175 -0
  163. package/test/doctor.test.ts +182 -0
  164. package/test/drizzle-conditions.test.ts +895 -0
  165. package/test/entityService.errors.test.ts +367 -0
  166. package/test/entityService.relations.test.ts +1008 -0
  167. package/test/entityService.subcollection-search.test.ts +566 -0
  168. package/test/entityService.test.ts +1035 -0
  169. package/test/generate-drizzle-schema.test.ts +988 -0
  170. package/test/historyService.test.ts +141 -0
  171. package/test/introspect-db-generation.test.ts +436 -0
  172. package/test/introspect-db-utils.test.ts +389 -0
  173. package/test/n-plus-one-regression.test.ts +314 -0
  174. package/test/postgresDataDriver.test.ts +648 -0
  175. package/test/realtimeService.test.ts +307 -0
  176. package/test/relation-pipeline-gaps.test.ts +637 -0
  177. package/test/relations.test.ts +1115 -0
  178. package/test/unmapped-tables-safety.test.ts +345 -0
  179. package/test-drizzle-bug.ts +18 -0
  180. package/test-drizzle-out/0000_cultured_freak.sql +7 -0
  181. package/test-drizzle-out/0001_tiresome_professor_monster.sql +1 -0
  182. package/test-drizzle-out/meta/0000_snapshot.json +55 -0
  183. package/test-drizzle-out/meta/0001_snapshot.json +63 -0
  184. package/test-drizzle-out/meta/_journal.json +20 -0
  185. package/test-drizzle-prompt.sh +2 -0
  186. package/test-policy-prompt.sh +3 -0
  187. package/test-programmatic.ts +30 -0
  188. package/test-programmatic2.ts +59 -0
  189. package/test-schema-no-policies.ts +12 -0
  190. package/test_drizzle_mock.js +3 -0
  191. package/test_find_changed.mjs +32 -0
  192. package/test_hash.js +14 -0
  193. package/test_output.txt +3145 -0
  194. package/tsconfig.json +49 -0
  195. package/tsconfig.prod.json +20 -0
  196. package/vite.config.ts +82 -0
@@ -0,0 +1,1035 @@
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
+ // --- Mock Drizzle ORM table definitions ---
8
+ const mockAuthorsTable = {
9
+ id: { name: "id" },
10
+ name: { name: "name" },
11
+ _def: { tableName: "authors" }
12
+ };
13
+ const mockPostsTable = {
14
+ id: { name: "id",
15
+ dataType: "number" },
16
+ title: { name: "title" },
17
+ author_id: { name: "author_id",
18
+ dataType: "number" },
19
+ _def: { tableName: "posts" }
20
+ };
21
+ const mockTagsTable = {
22
+ id: { name: "id",
23
+ dataType: "number" },
24
+ name: { name: "name" },
25
+ _def: { tableName: "tags" }
26
+ };
27
+ const mockPostsTagsTable = {
28
+ post_id: { name: "post_id",
29
+ dataType: "number" },
30
+ tag_id: { name: "tag_id",
31
+ dataType: "number" },
32
+ _def: { tableName: "posts_tags" }
33
+ };
34
+ const mockProjectUsersTable = {
35
+ project_id: { name: "project_id" },
36
+ id: { name: "id" },
37
+ email: { name: "email" },
38
+ _def: { tableName: "project_users" }
39
+ };
40
+
41
+ // --- Correctly typed Mock Entity Collections ---
42
+ let authorsCollection: EntityCollection;
43
+ const tagsCollection: EntityCollection = {
44
+ slug: "tags",
45
+ name: "Tags",
46
+ table: "tags",
47
+ properties: {
48
+ id: { type: "number" },
49
+ name: { type: "string" }
50
+ },
51
+ idField: "id"
52
+ };
53
+
54
+ const projectUsersCollection: EntityCollection = {
55
+ slug: "project_users",
56
+ name: "Project Users",
57
+ table: "project_users",
58
+ properties: {
59
+ project_id: { type: "string" },
60
+ id: { type: "string" },
61
+ email: { type: "string" }
62
+ },
63
+ primaryKeys: ["project_id", "id"]
64
+ };
65
+
66
+ const postsCollection: EntityCollection = {
67
+ slug: "posts",
68
+ name: "Posts",
69
+ table: "posts",
70
+ properties: {
71
+ id: { type: "number" },
72
+ title: { type: "string" },
73
+ author: {
74
+ type: "relation",
75
+ relationName: "author"
76
+ },
77
+ tags: {
78
+ type: "relation",
79
+ relationName: "tags"
80
+ }
81
+ },
82
+ relations: [
83
+ {
84
+ relationName: "author",
85
+ target: () => authorsCollection,
86
+ cardinality: "one",
87
+ direction: "owning",
88
+ localKey: "author_id"
89
+ },
90
+ {
91
+ relationName: "tags",
92
+ target: () => tagsCollection,
93
+ cardinality: "many",
94
+ direction: "owning",
95
+ through: {
96
+ table: "posts_tags",
97
+ sourceColumn: "post_id",
98
+ targetColumn: "tag_id"
99
+ }
100
+ }
101
+ ],
102
+ idField: "id"
103
+ };
104
+
105
+ authorsCollection = {
106
+ slug: "authors",
107
+ name: "Authors",
108
+ table: "authors",
109
+ properties: {
110
+ id: { type: "number" },
111
+ name: { type: "string" },
112
+ posts: {
113
+ type: "relation",
114
+ relationName: "posts"
115
+ }
116
+ },
117
+ relations: [
118
+ {
119
+ relationName: "posts",
120
+ target: () => postsCollection,
121
+ cardinality: "many",
122
+ direction: "inverse",
123
+ foreignKeyOnTarget: "author_id"
124
+ }
125
+ ],
126
+ idField: "id"
127
+ };
128
+
129
+ describe("EntityService", () => {
130
+ let entityService: EntityService;
131
+ let db: jest.Mocked<NodePgDatabase>;
132
+
133
+ beforeEach(() => {
134
+ jest.spyOn(collectionRegistry, "getCollectionByPath").mockImplementation(path => {
135
+ if (path.startsWith("authors")) return authorsCollection;
136
+ if (path.startsWith("posts")) return postsCollection;
137
+ if (path.startsWith("tags")) return tagsCollection;
138
+ if (path.startsWith("project_users")) return projectUsersCollection;
139
+ return undefined;
140
+ });
141
+ jest.spyOn(collectionRegistry, "getTable").mockImplementation(tableName => {
142
+ if (tableName === "authors") return mockAuthorsTable as unknown as ReturnType<typeof collectionRegistry.getTable>;
143
+ if (tableName === "posts") return mockPostsTable as unknown as ReturnType<typeof collectionRegistry.getTable>;
144
+ if (tableName === "tags") return mockTagsTable as unknown as ReturnType<typeof collectionRegistry.getTable>;
145
+ if (tableName === "posts_tags") return mockPostsTagsTable as unknown as ReturnType<typeof collectionRegistry.getTable>;
146
+ if (tableName === "project_users") return mockProjectUsersTable as unknown as ReturnType<typeof collectionRegistry.getTable>;
147
+ return undefined;
148
+ });
149
+ jest.spyOn(collectionRegistry, "getCollections").mockReturnValue([authorsCollection, postsCollection, tagsCollection, projectUsersCollection]);
150
+
151
+ db = {
152
+ select: jest.fn().mockReturnThis(),
153
+ from: jest.fn().mockReturnThis(),
154
+ where: jest.fn().mockReturnThis(),
155
+ $dynamic: jest.fn().mockReturnThis(),
156
+ limit: jest.fn().mockResolvedValue([]),
157
+ orderBy: jest.fn().mockResolvedValue([]), // This is now a terminal operation by default
158
+ innerJoin: jest.fn().mockReturnThis(),
159
+ insert: jest.fn().mockReturnThis(),
160
+ values: jest.fn().mockReturnThis(),
161
+ returning: jest.fn().mockResolvedValue([]),
162
+ update: jest.fn().mockReturnThis(),
163
+ set: jest.fn().mockReturnThis(),
164
+ delete: jest.fn().mockReturnThis(),
165
+ transaction: jest.fn((callback) => callback(db))
166
+ } as unknown as jest.Mocked<NodePgDatabase>;
167
+
168
+ entityService = new EntityService(db, collectionRegistry);
169
+ });
170
+
171
+ afterEach(() => {
172
+ jest.restoreAllMocks();
173
+ });
174
+
175
+ describe("fetchEntity", () => {
176
+ it("should parse a 'one' relation ID into a relation object with a string ID", async () => {
177
+ const mockPost = {
178
+ id: 2,
179
+ title: "My First Post",
180
+ author: 1
181
+ };
182
+ db.limit.mockResolvedValue([mockPost]);
183
+ const entity = await entityService.fetchEntity("posts", 2);
184
+ // The service correctly converts relation IDs to strings.
185
+ expect(entity?.values.author).toEqual({
186
+ id: "1",
187
+ path: "authors",
188
+ __type: "relation"
189
+ });
190
+ });
191
+
192
+ it("should parse a composite ID and use both parts in the WHERE clause", async () => {
193
+ const mockUser = {
194
+ project_id: "proj1",
195
+ id: "user1",
196
+ email: "test@test.com"
197
+ };
198
+ db.limit.mockResolvedValue([mockUser] as unknown as never);
199
+
200
+ const entity = await entityService.fetchEntity("project_users", "proj1:::user1");
201
+
202
+ // Check that we fetched the actual mocked user
203
+ expect(entity?.id).toBe("proj1:::user1");
204
+ expect(entity?.values.email).toBe("test@test.com");
205
+
206
+ expect(db.select).toHaveBeenCalled();
207
+ expect(db.from).toHaveBeenCalled();
208
+ expect(db.where).toHaveBeenCalled();
209
+ expect(db.limit).toHaveBeenCalledWith(1);
210
+ });
211
+ });
212
+
213
+ describe("saveEntity (create)", () => {
214
+ it("should correctly serialize and deserialize a 'one' relation", async () => {
215
+ const newPost = {
216
+ title: "Post by Jane",
217
+ author: {
218
+ id: "3",
219
+ path: "authors",
220
+ __type: "relation"
221
+ }
222
+ };
223
+ db.returning.mockResolvedValue([{ id: 4 }]);
224
+ // Mock the fetch-back call after the save
225
+ db.limit.mockResolvedValue([{
226
+ id: 4,
227
+ title: "Post by Jane",
228
+ author_id: "3" // Database stores the foreign key
229
+ }]);
230
+
231
+ const entity = await entityService.saveEntity("posts", newPost);
232
+
233
+ // 1. Check that the relation was serialized to a foreign key for the database insert
234
+ expect(db.values).toHaveBeenCalledWith(expect.objectContaining({
235
+ title: "Post by Jane",
236
+ author_id: "3" // Should be serialized to FK for database storage
237
+ }));
238
+
239
+ // 2. Check that the returned entity has the relation deserialized correctly
240
+ expect(entity.id).toBe("4");
241
+ expect(entity.values.author).toEqual({
242
+ id: "3",
243
+ path: "authors",
244
+ __type: "relation"
245
+ });
246
+ });
247
+
248
+ it("should save an entity with composite primary keys in UPSERT/onConflictDoUpdate mode", async () => {
249
+ const valuesToSave = {
250
+ project_id: "proj1",
251
+ id: "user1",
252
+ email: "new@test.com"
253
+ };
254
+
255
+ const returnedSaved = {
256
+ project_id: "proj1",
257
+ id: "user1",
258
+ email: "new@test.com"
259
+ };
260
+
261
+ const mockWhere = jest.fn().mockResolvedValue([returnedSaved]);
262
+ const mockSet = jest.fn().mockReturnValue({
263
+ where: mockWhere
264
+ });
265
+
266
+ // Intercept update chain
267
+ db.update.mockReturnValue({
268
+ set: mockSet
269
+ } as unknown as ReturnType<typeof db.update>);
270
+
271
+ // Mock fetch back (the final step of saveEntity)
272
+ db.limit.mockResolvedValue([returnedSaved] as unknown as never);
273
+
274
+ const savedEntity = await entityService.saveEntity("project_users", valuesToSave, "proj1:::user1");
275
+
276
+ expect(db.update).toHaveBeenCalled();
277
+ expect(mockSet).toHaveBeenCalledWith(expect.objectContaining({ email: "new@test.com" }));
278
+ expect(mockWhere).toHaveBeenCalled();
279
+
280
+ expect(savedEntity.id).toBe("proj1:::user1");
281
+ expect(savedEntity.values.email).toBe("new@test.com");
282
+ });
283
+ });
284
+
285
+ describe("saveEntity (update)", () => {
286
+ it("should update junction table for a 'many' relation", async () => {
287
+ const updatedPost = { tags: [{ id: 11 }, { id: 12 }] };
288
+ db.limit.mockResolvedValue([{
289
+ id: 5,
290
+ title: "Post with Tags"
291
+ }]);
292
+
293
+ await entityService.saveEntity("posts", updatedPost, 5);
294
+
295
+ expect(db.delete).toHaveBeenCalledWith(mockPostsTagsTable);
296
+ expect(db.where).toHaveBeenCalledWith(expect.any(Object));
297
+ expect(db.insert).toHaveBeenCalledWith(mockPostsTagsTable);
298
+ expect(db.values).toHaveBeenLastCalledWith([{
299
+ post_id: 5,
300
+ tag_id: 11
301
+ }, {
302
+ post_id: 5,
303
+ tag_id: 12
304
+ }]);
305
+ });
306
+ });
307
+
308
+ describe("fetchCollectionFromPath", () => {
309
+ it("should fetch related entities from a nested path", async () => {
310
+ const mockRelatedPosts = [
311
+ { id: 1,
312
+ title: "Post by John",
313
+ author_id: 1 },
314
+ { id: 2,
315
+ title: "Another Post by John",
316
+ author_id: 1 }
317
+ ];
318
+ // RelationService.fetchEntitiesUsingJoins ends query chain with where(), not orderBy()
319
+ // Make where() awaitable by returning a promise-like object
320
+ db.where.mockReturnValue({
321
+ then: (resolve: Function) => resolve(mockRelatedPosts),
322
+ limit: jest.fn().mockReturnValue({
323
+ then: (resolve: Function) => resolve(mockRelatedPosts)
324
+ })
325
+ });
326
+
327
+ const entities = await entityService.fetchCollection("authors/1/posts", {});
328
+
329
+ // The service should have been called to get the 'authors' collection definition.
330
+ expect(collectionRegistry.getCollectionByPath).toHaveBeenCalledWith("authors");
331
+ // For inverse relations like authors->posts, no join is needed as it uses a WHERE clause on the foreign key
332
+ expect(entities).toHaveLength(2);
333
+ expect(entities[0].values.title).toBe("Post by John");
334
+ });
335
+ });
336
+ });
337
+
338
+
339
+ describe("EntityService - Comprehensive Tests", () => {
340
+ let entityService: EntityService;
341
+ let db: jest.Mocked<NodePgDatabase<any>>;
342
+
343
+ // Extended mock tables for more complex scenarios
344
+ const mockUsersTable = {
345
+ id: { name: "id",
346
+ dataType: "number" },
347
+ email: { name: "email" },
348
+ name: { name: "name" },
349
+ created_at: { name: "created_at" },
350
+ project_id: { name: "project_id" }, // Add for relations
351
+ assignee_id: { name: "assignee_id" }, // Add for relations
352
+ _def: { tableName: "users" }
353
+ };
354
+
355
+ const mockCompaniesTable = {
356
+ id: { name: "id" },
357
+ name: { name: "name" },
358
+ user_id: { name: "user_id" },
359
+ company_id: { name: "company_id" }, // Add for relations
360
+ _def: { tableName: "companies" }
361
+ };
362
+
363
+ const mockProjectsTable = {
364
+ id: { name: "id" },
365
+ title: { name: "title" },
366
+ description: { name: "description" },
367
+ company_id: { name: "company_id" },
368
+ status: { name: "status" },
369
+ priority: { name: "priority" },
370
+ project_id: { name: "project_id" }, // Add for relations
371
+ assignee_id: { name: "assignee_id" }, // Add for relations
372
+ _def: { tableName: "projects" }
373
+ };
374
+
375
+ const mockTasksTable = {
376
+ id: { name: "id" },
377
+ title: { name: "title" },
378
+ project_id: { name: "project_id" },
379
+ assignee_id: { name: "assignee_id" },
380
+ _def: { tableName: "tasks" }
381
+ };
382
+
383
+ const mockCategoriesTable = {
384
+ id: { name: "id" },
385
+ name: { name: "name" },
386
+ parent_id: { name: "parent_id" },
387
+ _def: { tableName: "categories" }
388
+ };
389
+
390
+ const mockProjectTagsTable = {
391
+ project_id: { name: "project_id" },
392
+ tag_id: { name: "tag_id" },
393
+ _def: { tableName: "project_tags" }
394
+ };
395
+
396
+ const mockTagsTable = {
397
+ id: { name: "id",
398
+ dataType: "number" },
399
+ name: { name: "name" },
400
+ _def: { tableName: "tags" }
401
+ };
402
+
403
+ // Complex collection definitions
404
+ const usersCollection: EntityCollection = {
405
+ slug: "users",
406
+ name: "Users",
407
+ table: "users",
408
+ properties: {
409
+ id: { type: "number" },
410
+ email: { type: "string" },
411
+ name: { type: "string" },
412
+ created_at: { type: "date" },
413
+ companies: { type: "relation",
414
+ relationName: "companies" }
415
+ },
416
+ relations: [{
417
+ relationName: "companies",
418
+ target: () => companiesCollection,
419
+ cardinality: "many",
420
+ direction: "inverse",
421
+ foreignKeyOnTarget: "user_id"
422
+ }],
423
+ idField: "id"
424
+ };
425
+
426
+ const companiesCollection: EntityCollection = {
427
+ slug: "companies",
428
+ name: "Companies",
429
+ table: "companies",
430
+ properties: {
431
+ id: { type: "number" },
432
+ name: { type: "string" },
433
+ owner: { type: "relation",
434
+ relationName: "owner" },
435
+ projects: { type: "relation",
436
+ relationName: "projects" }
437
+ },
438
+ relations: [
439
+ {
440
+ relationName: "owner",
441
+ target: () => usersCollection,
442
+ cardinality: "one",
443
+ direction: "owning",
444
+ localKey: "user_id"
445
+ },
446
+ {
447
+ relationName: "projects",
448
+ target: () => projectsCollection,
449
+ cardinality: "many",
450
+ direction: "inverse",
451
+ foreignKeyOnTarget: "company_id"
452
+ }
453
+ ],
454
+ idField: "id"
455
+ };
456
+
457
+ const projectsCollection: EntityCollection = {
458
+ slug: "projects",
459
+ name: "Projects",
460
+ table: "projects",
461
+ properties: {
462
+ id: { type: "number" },
463
+ title: { type: "string" },
464
+ description: { type: "string" },
465
+ status: { type: "string" },
466
+ priority: { type: "number" },
467
+ company: { type: "relation",
468
+ relationName: "company" },
469
+ tasks: { type: "relation",
470
+ relationName: "tasks" },
471
+ tags: { type: "relation",
472
+ relationName: "tags" }
473
+ },
474
+ relations: [
475
+ {
476
+ relationName: "company",
477
+ target: () => companiesCollection,
478
+ cardinality: "one",
479
+ direction: "owning",
480
+ localKey: "company_id"
481
+ },
482
+ {
483
+ relationName: "tasks",
484
+ target: () => tasksCollection,
485
+ cardinality: "many",
486
+ direction: "inverse",
487
+ foreignKeyOnTarget: "project_id"
488
+ },
489
+ {
490
+ relationName: "tags",
491
+ target: () => tagsCollection,
492
+ cardinality: "many",
493
+ direction: "owning",
494
+ through: {
495
+ table: "project_tags",
496
+ sourceColumn: "project_id",
497
+ targetColumn: "tag_id"
498
+ }
499
+ }
500
+ ],
501
+ idField: "id"
502
+ };
503
+
504
+ const tasksCollection: EntityCollection = {
505
+ slug: "tasks",
506
+ name: "Tasks",
507
+ table: "tasks",
508
+ properties: {
509
+ id: { type: "number" },
510
+ title: { type: "string" },
511
+ project: { type: "relation",
512
+ relationName: "project" },
513
+ assignee: { type: "relation",
514
+ relationName: "assignee" }
515
+ },
516
+ relations: [
517
+ {
518
+ relationName: "project",
519
+ target: () => projectsCollection,
520
+ cardinality: "one",
521
+ direction: "owning",
522
+ localKey: "project_id"
523
+ },
524
+ {
525
+ relationName: "assignee",
526
+ target: () => usersCollection,
527
+ cardinality: "one",
528
+ direction: "owning",
529
+ localKey: "assignee_id"
530
+ }
531
+ ],
532
+ idField: "id"
533
+ };
534
+
535
+ const tagsCollection: EntityCollection = {
536
+ slug: "tags",
537
+ name: "Tags",
538
+ table: "tags",
539
+ properties: {
540
+ id: { type: "number" },
541
+ name: { type: "string" }
542
+ },
543
+ idField: "id"
544
+ };
545
+
546
+ const categoriesCollection: EntityCollection = {
547
+ slug: "categories",
548
+ name: "Categories",
549
+ table: "categories",
550
+ properties: {
551
+ id: { type: "number" },
552
+ name: { type: "string" },
553
+ parent: { type: "relation",
554
+ relationName: "parent" },
555
+ children: { type: "relation",
556
+ relationName: "children" }
557
+ },
558
+ relations: [
559
+ {
560
+ relationName: "parent",
561
+ target: () => categoriesCollection,
562
+ cardinality: "one",
563
+ direction: "owning",
564
+ localKey: "parent_id"
565
+ },
566
+ {
567
+ relationName: "children",
568
+ target: () => categoriesCollection,
569
+ cardinality: "many",
570
+ direction: "inverse",
571
+ foreignKeyOnTarget: "parent_id"
572
+ }
573
+ ],
574
+ idField: "id"
575
+ };
576
+
577
+ beforeEach(() => {
578
+ jest.clearAllMocks();
579
+
580
+ jest.spyOn(collectionRegistry, "getCollectionByPath").mockImplementation(path => {
581
+ if (path.startsWith("users")) return usersCollection;
582
+ if (path.startsWith("companies")) return companiesCollection;
583
+ if (path.startsWith("projects")) return projectsCollection;
584
+ if (path.startsWith("tasks")) return tasksCollection;
585
+ if (path.startsWith("tags")) return tagsCollection;
586
+ if (path.startsWith("categories")) return categoriesCollection;
587
+ return undefined;
588
+ });
589
+
590
+ jest.spyOn(collectionRegistry, "getTable").mockImplementation(tableName => {
591
+ if (tableName === "users") return mockUsersTable as unknown as ReturnType<typeof collectionRegistry.getTable>;
592
+ if (tableName === "companies") return mockCompaniesTable as unknown as ReturnType<typeof collectionRegistry.getTable>;
593
+ if (tableName === "projects") return mockProjectsTable as unknown as ReturnType<typeof collectionRegistry.getTable>;
594
+ if (tableName === "tasks") return mockTasksTable as unknown as ReturnType<typeof collectionRegistry.getTable>;
595
+ if (tableName === "tags") return mockTagsTable as unknown as ReturnType<typeof collectionRegistry.getTable>;
596
+ if (tableName === "categories") return mockCategoriesTable as unknown as ReturnType<typeof collectionRegistry.getTable>;
597
+ if (tableName === "project_tags") return mockProjectTagsTable as unknown as ReturnType<typeof collectionRegistry.getTable>;
598
+ return undefined;
599
+ });
600
+
601
+ db = {
602
+ select: jest.fn().mockReturnThis(),
603
+ from: jest.fn().mockReturnThis(),
604
+ where: jest.fn().mockReturnThis(),
605
+ $dynamic: jest.fn().mockReturnThis(),
606
+ limit: jest.fn().mockReturnThis(),
607
+ orderBy: jest.fn().mockReturnThis(),
608
+ innerJoin: jest.fn().mockReturnThis(),
609
+ insert: jest.fn().mockReturnThis(),
610
+ values: jest.fn().mockReturnThis(),
611
+ returning: jest.fn().mockResolvedValue([]),
612
+ update: jest.fn().mockReturnThis(),
613
+ set: jest.fn().mockReturnThis(),
614
+ delete: jest.fn().mockReturnThis(),
615
+ transaction: jest.fn((callback) => callback(db))
616
+ } as unknown as jest.Mocked<NodePgDatabase>;
617
+
618
+ // Add a then method to make the db object awaitable when the query chain ends
619
+ (db as unknown as Record<string, jest.Mock>).then = jest.fn((resolve) => resolve([]));
620
+
621
+ entityService = new EntityService(db, collectionRegistry);
622
+ });
623
+
624
+ describe("fetchEntity - Edge Cases", () => {
625
+ it("should handle numeric IDs correctly", async () => {
626
+ const mockUser = { id: 123,
627
+ email: "test@example.com",
628
+ name: "Test User" };
629
+ db.limit.mockResolvedValue([mockUser]);
630
+
631
+ const entity = await entityService.fetchEntity("users", 123);
632
+
633
+ expect(entity?.id).toBe("123");
634
+ expect(entity?.values.email).toBe("test@example.com");
635
+ });
636
+
637
+ it("should handle string IDs correctly for string ID type collections", async () => {
638
+ // Create a collection with string ID type
639
+ const stringIdCollection = {
640
+ ...usersCollection,
641
+ properties: {
642
+ ...usersCollection.properties,
643
+ id: { type: "string" }
644
+ }
645
+ };
646
+ jest.spyOn(collectionRegistry, "getCollectionByPath").mockReturnValue(stringIdCollection);
647
+ jest.spyOn(collectionRegistry, "getTable").mockImplementation(tableName => {
648
+ if (tableName === "users") return {
649
+ id: { name: "id",
650
+ dataType: "string" },
651
+ email: { name: "email" },
652
+ name: { name: "name" },
653
+ _def: { tableName: "users" }
654
+ } as unknown as ReturnType<typeof collectionRegistry.getTable>;
655
+ if (tableName === "companies") return mockCompaniesTable as unknown as ReturnType<typeof collectionRegistry.getTable>;
656
+ if (tableName === "projects") return mockProjectsTable as unknown as ReturnType<typeof collectionRegistry.getTable>;
657
+ if (tableName === "tasks") return mockTasksTable as unknown as ReturnType<typeof collectionRegistry.getTable>;
658
+ if (tableName === "tags") return mockTagsTable as unknown as ReturnType<typeof collectionRegistry.getTable>;
659
+ if (tableName === "categories") return mockCategoriesTable as unknown as ReturnType<typeof collectionRegistry.getTable>;
660
+ if (tableName === "project_tags") return mockProjectTagsTable as unknown as ReturnType<typeof collectionRegistry.getTable>;
661
+ return undefined;
662
+ });
663
+
664
+ const mockUser = { id: "uuid-123",
665
+ email: "test@example.com",
666
+ name: "Test User" };
667
+ db.limit.mockResolvedValue([mockUser]);
668
+
669
+ const entity = await entityService.fetchEntity("users", "uuid-123");
670
+
671
+ expect(entity?.id).toBe("uuid-123");
672
+ expect(entity?.values.email).toBe("test@example.com");
673
+ });
674
+
675
+ it("should return undefined for non-existent entity", async () => {
676
+ db.limit.mockResolvedValue([]);
677
+
678
+ const entity = await entityService.fetchEntity("users", 999);
679
+
680
+ expect(entity).toBeUndefined();
681
+ });
682
+
683
+ it("should handle entities with null relation fields", async () => {
684
+ const mockTask = { id: 1,
685
+ title: "Task 1",
686
+ project_id: null,
687
+ assignee_id: null };
688
+ db.limit.mockResolvedValue([mockTask]);
689
+
690
+ const entity = await entityService.fetchEntity("tasks", 1);
691
+
692
+ // When foreign keys are null, the EntityService may still create relation objects
693
+ // if it finds related entities through other means. The actual behavior depends
694
+ // on the relation resolution logic, so we should check if they are either
695
+ // undefined or have the expected structure
696
+ if (entity?.values.project) {
697
+ expect(entity.values.project).toHaveProperty("__type", "relation");
698
+ }
699
+ if (entity?.values.assignee) {
700
+ expect(entity.values.assignee).toHaveProperty("__type", "relation");
701
+ }
702
+
703
+ // The main test is that the entity was successfully fetched despite null relations
704
+ expect(entity?.id).toBe("1");
705
+ expect(entity?.values.title).toBe("Task 1");
706
+ });
707
+ });
708
+
709
+ describe("fetchCollection - Filtering and Pagination", () => {
710
+ it("should apply filters correctly", async () => {
711
+ const mockProjects = [
712
+ { id: 1,
713
+ title: "Project 1",
714
+ status: "active",
715
+ priority: 1 },
716
+ { id: 2,
717
+ title: "Project 2",
718
+ status: "active",
719
+ priority: 2 }
720
+ ];
721
+ db.orderBy.mockResolvedValue(mockProjects);
722
+
723
+ await entityService.fetchCollection("projects", {
724
+ filter: { status: ["==", "active"],
725
+ priority: [">=", 1] }
726
+ });
727
+
728
+ expect(db.where).toHaveBeenCalled();
729
+ });
730
+
731
+ it("should apply ordering correctly", async () => {
732
+ const mockProjects = [
733
+ { id: 2,
734
+ title: "Project 2",
735
+ priority: 2 },
736
+ { id: 1,
737
+ title: "Project 1",
738
+ priority: 1 }
739
+ ];
740
+ db.orderBy.mockResolvedValue(mockProjects);
741
+
742
+ await entityService.fetchCollection("projects", {
743
+ orderBy: "priority",
744
+ order: "desc"
745
+ });
746
+
747
+ expect(db.orderBy).toHaveBeenCalled();
748
+ });
749
+
750
+ it("should apply limit correctly", async () => {
751
+ const mockProjects = [
752
+ { id: 1,
753
+ title: "Project 1" },
754
+ { id: 2,
755
+ title: "Project 2" }
756
+ ];
757
+ // Override the then method to return our mock data for this specific test
758
+ (db as unknown as Record<string, jest.Mock>).then = jest.fn((resolve) => resolve(mockProjects));
759
+
760
+ await entityService.fetchCollection("projects", {
761
+ limit: 10
762
+ });
763
+
764
+ expect(db.limit).toHaveBeenCalledWith(10);
765
+ });
766
+ });
767
+
768
+ describe("Nested Path Relations", () => {
769
+ it("should handle deep nested paths", async () => {
770
+ const mockTasks = [
771
+ { id: 1,
772
+ title: "Task 1",
773
+ project_id: 1 },
774
+ { id: 2,
775
+ title: "Task 2",
776
+ project_id: 1 }
777
+ ];
778
+ // RelationService.fetchEntitiesUsingJoins ends query chain with where(), not orderBy()
779
+ // Make where() awaitable by returning a promise-like object
780
+ db.where.mockReturnValue({
781
+ then: (resolve: Function) => resolve(mockTasks),
782
+ limit: jest.fn().mockReturnValue({
783
+ then: (resolve: Function) => resolve(mockTasks)
784
+ })
785
+ });
786
+
787
+ const entities = await entityService.fetchCollection("users/1/companies/1/projects/1/tasks", {});
788
+
789
+ expect(collectionRegistry.getCollectionByPath).toHaveBeenCalledWith("users");
790
+ expect(entities).toHaveLength(2);
791
+ });
792
+
793
+ it("should handle self-referencing relations", async () => {
794
+ const mockCategories = [
795
+ { id: 2,
796
+ name: "Subcategory 1",
797
+ parent_id: 1 },
798
+ { id: 3,
799
+ name: "Subcategory 2",
800
+ parent_id: 1 }
801
+ ];
802
+ // RelationService.fetchEntitiesUsingJoins ends query chain with where(), not orderBy()
803
+ db.where.mockReturnValue({
804
+ then: (resolve: Function) => resolve(mockCategories),
805
+ limit: jest.fn().mockReturnValue({
806
+ then: (resolve: Function) => resolve(mockCategories)
807
+ })
808
+ });
809
+
810
+ const entities = await entityService.fetchCollection("categories/1/children", {});
811
+
812
+ expect(entities).toHaveLength(2);
813
+ });
814
+
815
+ it("should throw error for invalid path format", async () => {
816
+ await expect(
817
+ entityService.fetchCollection("invalid/path", {})
818
+ ).rejects.toThrow("Invalid relation path");
819
+ });
820
+
821
+ it("should throw error for non-existent relation", async () => {
822
+ await expect(
823
+ entityService.fetchCollection("users/1/nonexistent", {})
824
+ ).rejects.toThrow("Relation 'nonexistent' not found");
825
+ });
826
+ });
827
+
828
+ describe("saveEntity - Complex Scenarios", () => {
829
+ it("should handle creating entity with multiple relations", async () => {
830
+ const newProject = {
831
+ title: "New Project",
832
+ description: "A new project",
833
+ company: { id: "1",
834
+ path: "companies",
835
+ __type: "relation" }
836
+ };
837
+
838
+ db.returning.mockResolvedValue([{ id: 5 }]);
839
+ db.limit.mockResolvedValue([{
840
+ id: 5,
841
+ title: "New Project",
842
+ description: "A new project",
843
+ company_id: 1
844
+ }]);
845
+
846
+ const entity = await entityService.saveEntity("projects", newProject);
847
+
848
+ expect(db.values).toHaveBeenCalledWith(expect.objectContaining({
849
+ title: "New Project",
850
+ company_id: "1"
851
+ }));
852
+ expect(entity.id).toBe("5");
853
+ });
854
+
855
+ it("should handle updating entity with relation changes", async () => {
856
+ const updatedTask = {
857
+ title: "Updated Task",
858
+ assignee: { id: "2",
859
+ path: "users",
860
+ __type: "relation" }
861
+ };
862
+
863
+ db.returning.mockResolvedValue([{ id: 1 }]);
864
+ db.limit.mockResolvedValue([{
865
+ id: 1,
866
+ title: "Updated Task",
867
+ assignee_id: 2
868
+ }]);
869
+
870
+ const entity = await entityService.saveEntity("tasks", updatedTask, 1);
871
+
872
+ expect(db.set).toHaveBeenCalledWith(expect.objectContaining({
873
+ title: "Updated Task",
874
+ assignee_id: "2"
875
+ }));
876
+ });
877
+
878
+ it("should handle setting relation to null", async () => {
879
+ const updatedTask = {
880
+ title: "Task Without Assignee"
881
+ };
882
+
883
+ db.returning.mockResolvedValue([{ id: 1 }]);
884
+ db.limit.mockResolvedValue([{
885
+ id: 1,
886
+ title: "Task Without Assignee",
887
+ assignee_id: null
888
+ }]);
889
+
890
+ const entity = await entityService.saveEntity("tasks", updatedTask, 1);
891
+
892
+ expect(db.set).toHaveBeenCalledWith(expect.objectContaining({
893
+ title: "Task Without Assignee"
894
+ }));
895
+ });
896
+ });
897
+
898
+ describe("deleteEntity", () => {
899
+ it("should delete entity successfully", async () => {
900
+ db.returning.mockResolvedValue([{ id: 1 }]);
901
+
902
+ await entityService.deleteEntity("users", 1);
903
+
904
+ expect(db.delete).toHaveBeenCalled();
905
+ expect(db.where).toHaveBeenCalled();
906
+ });
907
+
908
+ it("should handle deletion of non-existent entity gracefully", async () => {
909
+ db.returning.mockResolvedValue([]);
910
+
911
+ // The service doesn't throw for non-existent entities
912
+ await entityService.deleteEntity("users", 999);
913
+
914
+ expect(db.delete).toHaveBeenCalled();
915
+ });
916
+ });
917
+
918
+ describe("searchEntities", () => {
919
+ it("should perform search across specified fields", async () => {
920
+ const mockResults = [
921
+ { id: 1,
922
+ title: "Searchable Project",
923
+ description: "Test description" }
924
+ ];
925
+ // Override the then method to return our mock data for this specific test
926
+ (db as unknown as Record<string, jest.Mock>).then = jest.fn((resolve) => resolve(mockResults));
927
+
928
+ const entities = await entityService.searchEntities("projects", "Searchable", {});
929
+
930
+ expect(entities).toHaveLength(1);
931
+ expect(entities[0].values.title).toBe("Searchable Project");
932
+ });
933
+
934
+ it("should combine search with filters", async () => {
935
+ const mockResults = [
936
+ { id: 1,
937
+ title: "Active Project",
938
+ status: "active" }
939
+ ];
940
+ // Override the then method to return our mock data for this specific test
941
+ (db as unknown as Record<string, jest.Mock>).then = jest.fn((resolve) => resolve(mockResults));
942
+
943
+ const entities = await entityService.searchEntities("projects", "Active", {
944
+ filter: { status: ["==", "active"] }
945
+ });
946
+
947
+ expect(entities).toHaveLength(1);
948
+ });
949
+ });
950
+
951
+ describe("Error Handling", () => {
952
+ it("should handle database connection errors", async () => {
953
+ db.limit.mockRejectedValue(new Error("Database connection failed"));
954
+
955
+ await expect(
956
+ entityService.fetchEntity("users", 1)
957
+ ).rejects.toThrow("Database connection failed");
958
+ });
959
+
960
+ it("should handle invalid collection paths", async () => {
961
+ jest.spyOn(collectionRegistry, "getCollectionByPath").mockReturnValue(undefined);
962
+
963
+ await expect(
964
+ entityService.fetchEntity("nonexistent", 1)
965
+ ).rejects.toThrow("Collection not found: nonexistent");
966
+ });
967
+
968
+ it("should handle missing table definitions", async () => {
969
+ jest.spyOn(collectionRegistry, "getTable").mockReturnValue(undefined);
970
+
971
+ await expect(
972
+ entityService.fetchEntity("users", 1)
973
+ ).rejects.toThrow("Table not found for collection");
974
+ });
975
+
976
+ it("should handle invalid ID types", async () => {
977
+ await expect(
978
+ entityService.fetchEntity("users", "invalid-number")
979
+ ).rejects.toThrow("Invalid numeric ID: invalid-number");
980
+ });
981
+ });
982
+
983
+ describe("Transaction Handling", () => {
984
+ it("should handle transactions correctly", async () => {
985
+ const newUser = {
986
+ email: "test@example.com",
987
+ name: "Test User"
988
+ };
989
+
990
+ db.returning.mockResolvedValue([{ id: 1 }]);
991
+ db.limit.mockResolvedValue([{
992
+ id: 1,
993
+ email: "test@example.com",
994
+ name: "Test User"
995
+ }]);
996
+
997
+ await entityService.saveEntity("users", newUser);
998
+
999
+ expect(db.transaction).toHaveBeenCalled();
1000
+ });
1001
+ });
1002
+
1003
+ describe("Data Type Handling", () => {
1004
+ it("should handle date fields correctly", async () => {
1005
+ const mockUser = {
1006
+ id: 1,
1007
+ email: "test@example.com",
1008
+ name: "Test User",
1009
+ created_at: "2023-01-01T00:00:00.000Z"
1010
+ };
1011
+ db.limit.mockResolvedValue([mockUser]);
1012
+
1013
+ const entity = await entityService.fetchEntity("users", 1);
1014
+
1015
+ // The actual implementation converts dates to objects with __type and value
1016
+ expect(entity?.values.created_at).toEqual({
1017
+ __type: "date",
1018
+ value: "2023-01-01T00:00:00.000Z"
1019
+ });
1020
+ });
1021
+
1022
+ it("should handle numeric fields correctly", async () => {
1023
+ const mockProject = {
1024
+ id: 1,
1025
+ title: "Test Project",
1026
+ priority: 5
1027
+ };
1028
+ db.limit.mockResolvedValue([mockProject]);
1029
+
1030
+ const entity = await entityService.fetchEntity("projects", 1);
1031
+
1032
+ expect(typeof entity?.values.priority).toBe("number");
1033
+ });
1034
+ });
1035
+ });