@node-c/data-typeorm 1.0.0-alpha64 → 1.0.0-beta1

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.
@@ -0,0 +1,1180 @@
1
+ import { ApplicationError, DataFindResults, DataOrderBy, DataSelectOperator, GenericObject } from '@node-c/core';
2
+ import {
3
+ BulkCreateOptions,
4
+ CountOptions,
5
+ CreateOptions,
6
+ FindOneOptions,
7
+ FindOptions,
8
+ IncludeItems,
9
+ ParsedFilter,
10
+ PostgresErrorCode,
11
+ SQLQueryBuilderService
12
+ } from '@node-c/data-rdb';
13
+ import { EntityManager, EntitySchema, Repository } from 'typeorm';
14
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
15
+
16
+ import { TypeORMDBEntityService } from './index';
17
+
18
+ class PostgresError extends Error {
19
+ code: string;
20
+ detail: string;
21
+ table: string;
22
+ constructor(message: string, data: { code: string; detail: string; table: string }) {
23
+ super(message);
24
+ for (const key in data) {
25
+ this[key as keyof PostgresError] = data[key as keyof typeof data];
26
+ }
27
+ }
28
+ }
29
+ // Define a minimal test entity interface.
30
+ interface TestEntity {
31
+ id: number;
32
+ name: string;
33
+ }
34
+ interface TransactionManagerGetter {
35
+ __getTransactionManager: () => EntityManager;
36
+ }
37
+ // Define a minimal dummy query builder interface.
38
+ interface QueryBuilderMock {
39
+ execute?: () => Promise<{ affected?: number; raw?: TestEntity[] }>;
40
+ delete?: () => QueryBuilderMock;
41
+ getCount?: () => Promise<number>;
42
+ getMany?: () => Promise<TestEntity[]>;
43
+ getOne?: () => Promise<TestEntity | null>;
44
+ returning?: (_columns: string) => QueryBuilderMock;
45
+ set?: (_data: unknown) => QueryBuilderMock;
46
+ skip?: (_value: number) => QueryBuilderMock;
47
+ softDelete?: () => QueryBuilderMock;
48
+ take?: (_value: number) => QueryBuilderMock;
49
+ update?: () => QueryBuilderMock;
50
+ }
51
+ // Create a dummy SQLQueryBuilderService with only the needed methods.
52
+ const createQBMock = (): SQLQueryBuilderService =>
53
+ ({
54
+ columnQuotesSymbol: '"',
55
+ buildQuery: vi.fn(),
56
+ parseFilters: vi.fn().mockImplementation(
57
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
58
+ (_tableName: string, _filters: unknown, _options: { operator: unknown; isTopLevel: boolean }) => {
59
+ // Return dummy "where" and "include" objects.
60
+ return {
61
+ where: { dummy: 'filter' } as unknown as Record<string, ParsedFilter>,
62
+ include: { dummyInclude: true }
63
+ };
64
+ }
65
+ ),
66
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
67
+ parseOrderBy: vi.fn().mockImplementation((_tableName: string, _orderBy: unknown) => {
68
+ // Return a dummy orderBy array.
69
+ return { orderBy: [{ column: 'id', order: 'ASC' }] as unknown as DataOrderBy[] };
70
+ }),
71
+ parseRelations: vi
72
+ .fn()
73
+ .mockImplementation(
74
+ (_tableName: string, _optRelations: unknown[], includeFromFilters: Record<string, unknown>) => {
75
+ // Merge the include from filters with extra dummy data.
76
+ return { ...includeFromFilters, extraRelation: true } as unknown as IncludeItems;
77
+ }
78
+ )
79
+ }) as unknown as SQLQueryBuilderService;
80
+ // Create a dummy repository and transaction manager.
81
+ const createRepositoryMock = (qbm: QueryBuilderMock): Repository<TestEntity> => {
82
+ const tmgm = createTransactionManagerMock(qbm);
83
+ return {
84
+ manager: {
85
+ transaction: vi.fn().mockImplementation(async (cb: (_tm: EntityManager) => Promise<TestEntity | null>) => {
86
+ return await cb(tmgm);
87
+ })
88
+ },
89
+ metadata: { name: 'TestEntity', tableName: 'TestEntity' },
90
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
91
+ target: 'TestEntity' as unknown as Function,
92
+ __getTransactionManager: () => tmgm,
93
+ createQueryBuilder: vi.fn().mockReturnValue(qbm)
94
+ } as unknown as Repository<TestEntity>;
95
+ };
96
+ const createTransactionManagerMock = (qbm: QueryBuilderMock): EntityManager => {
97
+ return {
98
+ getRepository: vi.fn().mockImplementation(() => createRepositoryMock(qbm)),
99
+ query: vi.fn().mockResolvedValue(undefined)
100
+ } as unknown as EntityManager;
101
+ };
102
+ // We declare repositoryMock later since it needs the queryBuilderMock.
103
+ // A dummy query builder that simply returns a dummy entity.
104
+ const dummyEntity: TestEntity = { id: 1, name: 'Test' };
105
+ let queryBuilderMock: QueryBuilderMock;
106
+ let repositoryMock: Repository<TestEntity>;
107
+ let transactionManagerMock: EntityManager;
108
+
109
+ describe('TypeORMDBEntityService', () => {
110
+ describe('constructor', () => {
111
+ let qbMock: SQLQueryBuilderService;
112
+ beforeEach(() => {
113
+ qbMock = createQBMock();
114
+ vi.clearAllMocks();
115
+ });
116
+ it('should correctly set primaryKeys when a single primary key is defined', () => {
117
+ const dummySchema = new EntitySchema<TestEntity>({
118
+ name: 'TestEntity',
119
+ columns: {
120
+ id: { type: Number, primary: true },
121
+ name: { type: String }
122
+ }
123
+ });
124
+ const service = new TypeORMDBEntityService<TestEntity>(qbMock, repositoryMock, dummySchema);
125
+ expect(service['primaryKeys']).toEqual(['id']);
126
+ });
127
+ it('should correctly set primaryKeys when multiple primary keys are defined', () => {
128
+ interface CompositeEntity {
129
+ id: number;
130
+ code: string;
131
+ value: number;
132
+ }
133
+ const dummySchemaMulti = new EntitySchema<CompositeEntity>({
134
+ name: 'CompositeEntity',
135
+ columns: {
136
+ id: { type: Number, primary: true },
137
+ code: { type: String, primary: true },
138
+ value: { type: Number }
139
+ }
140
+ });
141
+ const repositoryMulti = {
142
+ metadata: { name: 'CompositeEntity', tableName: 'CompositeEntity' },
143
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
144
+ target: 'CompositeEntity' as unknown as Function,
145
+ createQueryBuilder: vi.fn(),
146
+ manager: { transaction: vi.fn() }
147
+ } as unknown as Repository<CompositeEntity>;
148
+ const serviceMulti = new TypeORMDBEntityService<CompositeEntity>(qbMock, repositoryMulti, dummySchemaMulti);
149
+ expect(serviceMulti['primaryKeys']).toEqual(['id', 'code']);
150
+ });
151
+ it('should set primaryKeys to an empty array when no primary key is defined', () => {
152
+ const dummySchemaNoPK = new EntitySchema<TestEntity>({
153
+ name: 'NoPKEntity',
154
+ columns: {
155
+ id: { type: Number },
156
+ name: { type: String }
157
+ }
158
+ });
159
+ const repositoryNoPK = {
160
+ metadata: { name: 'NoPKEntity', tableName: 'NoPKEntity' },
161
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
162
+ target: 'NoPKEntity' as unknown as Function,
163
+ createQueryBuilder: vi.fn(),
164
+ manager: { transaction: vi.fn() }
165
+ } as unknown as Repository<TestEntity>;
166
+ const serviceNoPK = new TypeORMDBEntityService<TestEntity>(qbMock, repositoryNoPK, dummySchemaNoPK);
167
+ expect(serviceNoPK['primaryKeys']).toEqual([]);
168
+ });
169
+ });
170
+
171
+ describe('buildPrimaryKeyWhereClause', () => {
172
+ let qbMock: SQLQueryBuilderService;
173
+ let service: TypeORMDBEntityService<TestEntity>;
174
+ beforeEach(() => {
175
+ // Reset mocks before each test.
176
+ qbMock = createQBMock();
177
+ queryBuilderMock = {
178
+ getCount: vi.fn().mockResolvedValue(5),
179
+ getOne: vi.fn().mockResolvedValue(dummyEntity)
180
+ };
181
+ // Create a repository mock that uses our dummy query builder.
182
+ repositoryMock = createRepositoryMock(queryBuilderMock);
183
+ transactionManagerMock = (repositoryMock as unknown as TransactionManagerGetter).__getTransactionManager();
184
+ // Create a dummy schema with one primary key ("id") and one extra column.
185
+ const dummySchema = new EntitySchema<TestEntity>({
186
+ name: 'TestEntity',
187
+ columns: {
188
+ id: { type: Number, primary: true },
189
+ name: { type: String }
190
+ }
191
+ });
192
+ service = new TypeORMDBEntityService<TestEntity>(qbMock, repositoryMock, dummySchema);
193
+ // Clear mock history.
194
+ vi.clearAllMocks();
195
+ });
196
+ it('should build correct clause for a single primary key', () => {
197
+ const testData: TestEntity[] = [
198
+ { id: 1, name: 'Test1' },
199
+ { id: 2, name: 'Test2' }
200
+ ];
201
+ // Using the service from the earlier beforeEach (with single primary key "id")
202
+ const result = service['buildPrimaryKeyWhereClause'](testData);
203
+ expect(result.field).toBe('id');
204
+ expect(result.value.params).toEqual({ id: [1, 2] });
205
+ expect(result.value.query).toBe('"TestEntity"."id" in :id');
206
+ });
207
+ it('should build correct clause for composite primary keys', () => {
208
+ interface CompositeEntity {
209
+ id: number;
210
+ code: string;
211
+ value: number;
212
+ }
213
+ const compositeSchema = new EntitySchema<CompositeEntity>({
214
+ name: 'CompositeEntity',
215
+ columns: {
216
+ id: { type: Number, primary: true },
217
+ code: { type: String, primary: true },
218
+ value: { type: Number }
219
+ }
220
+ });
221
+ const compositeRepository = {
222
+ metadata: { name: 'CompositeEntity', tableName: 'CompositeEntity' },
223
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
224
+ target: 'CompositeEntity' as unknown as Function,
225
+ createQueryBuilder: vi.fn(),
226
+ manager: { transaction: vi.fn() }
227
+ } as unknown as Repository<CompositeEntity>;
228
+ const compositeService = new TypeORMDBEntityService<CompositeEntity>(
229
+ qbMock,
230
+ compositeRepository,
231
+ compositeSchema
232
+ );
233
+ const testData: CompositeEntity[] = [
234
+ { id: 1, code: 'A', value: 100 },
235
+ { id: 2, code: 'B', value: 200 }
236
+ ];
237
+ const result = compositeService['buildPrimaryKeyWhereClause'](testData);
238
+ expect(result.field).toBe(DataSelectOperator.Or);
239
+ expect(result.value.params).toEqual({
240
+ id0: 1,
241
+ code0: 'A',
242
+ id1: 2,
243
+ code1: 'B'
244
+ });
245
+ const expectedQuery =
246
+ '(("CompositeEntity"."id" = :id0 and "CompositeEntity"."code" = :code0) or ("CompositeEntity"."id" = :id1 and "CompositeEntity"."code" = :code1))';
247
+ expect(result.value.query).toBe(expectedQuery);
248
+ });
249
+ });
250
+
251
+ describe('bulkCreate', () => {
252
+ let dummyEntities: TestEntity[];
253
+ let qbMock: SQLQueryBuilderService;
254
+ let service: TypeORMDBEntityService<TestEntity>;
255
+ beforeEach(() => {
256
+ qbMock = createQBMock();
257
+ queryBuilderMock = {
258
+ getCount: vi.fn().mockResolvedValue(5),
259
+ getOne: vi.fn().mockResolvedValue(dummyEntity)
260
+ };
261
+ repositoryMock = createRepositoryMock(queryBuilderMock);
262
+ transactionManagerMock = (repositoryMock as unknown as TransactionManagerGetter).__getTransactionManager();
263
+ const dummySchema = new EntitySchema<TestEntity>({
264
+ name: 'TestEntity',
265
+ columns: {
266
+ id: { type: Number, primary: true },
267
+ name: { type: String }
268
+ }
269
+ });
270
+ service = new TypeORMDBEntityService<TestEntity>(qbMock, repositoryMock, dummySchema);
271
+ dummyEntities = [
272
+ { id: 1, name: 'Test1' },
273
+ { id: 2, name: 'Test2' }
274
+ ];
275
+ });
276
+ it('should call save directly when transactionManager is provided', async () => {
277
+ // Spy on the protected "save" method.
278
+ const saveSpy = vi
279
+ .spyOn(
280
+ service as unknown as {
281
+ save(_data: TestEntity[], _transactionManager?: EntityManager): Promise<TestEntity[]>;
282
+ },
283
+ 'save'
284
+ )
285
+ .mockResolvedValue(dummyEntities);
286
+ const options: BulkCreateOptions = { forceTransaction: false, transactionManager: transactionManagerMock };
287
+ const result = await service.bulkCreate(dummyEntities, options);
288
+ expect(result).toEqual(dummyEntities);
289
+ expect(saveSpy).toHaveBeenCalledWith(dummyEntities, transactionManagerMock);
290
+ });
291
+ it('should use transaction when forceTransaction is true and transactionManager is not provided', async () => {
292
+ // Spy on the protected "save" method.
293
+ const saveSpy = vi
294
+ .spyOn(
295
+ service as unknown as {
296
+ save(_data: TestEntity[], _transactionManager?: EntityManager): Promise<TestEntity[]>;
297
+ },
298
+ 'save'
299
+ )
300
+ .mockResolvedValue(dummyEntities);
301
+ // Prepare options without transactionManager and force a transaction.
302
+ const options: BulkCreateOptions = { forceTransaction: true };
303
+ const transactionSpy = repositoryMock.manager.transaction as ReturnType<typeof vi.fn>;
304
+ const result = await service.bulkCreate(dummyEntities, options);
305
+ expect(result).toEqual(dummyEntities);
306
+ expect(transactionSpy).toHaveBeenCalledTimes(1);
307
+ // The inner call of bulkCreate (triggered by the transaction) should call save with the provided transactionManager.
308
+ expect(saveSpy).toHaveBeenCalledWith(dummyEntities, transactionManagerMock);
309
+ });
310
+ });
311
+
312
+ describe('create', () => {
313
+ let dummyEntity: TestEntity;
314
+ let qbMock: SQLQueryBuilderService;
315
+ let service: TypeORMDBEntityService<TestEntity>;
316
+ beforeEach(() => {
317
+ qbMock = createQBMock();
318
+ queryBuilderMock = {
319
+ getCount: vi.fn().mockResolvedValue(5),
320
+ getOne: vi.fn().mockResolvedValue(dummyEntity)
321
+ };
322
+ repositoryMock = createRepositoryMock(queryBuilderMock);
323
+ transactionManagerMock = (repositoryMock as unknown as TransactionManagerGetter).__getTransactionManager();
324
+ const dummySchema = new EntitySchema<TestEntity>({
325
+ name: 'TestEntity',
326
+ columns: { id: { type: Number, primary: true }, name: { type: String } }
327
+ });
328
+ service = new TypeORMDBEntityService<TestEntity>(qbMock, repositoryMock, dummySchema);
329
+ dummyEntity = { id: 1, name: 'Test' };
330
+ });
331
+ it('should call save and return the entity when transactionManager is provided', async () => {
332
+ const saveSpy = vi
333
+ .spyOn(
334
+ service as unknown as { save(_data: TestEntity, _transactionManager?: EntityManager): Promise<TestEntity> },
335
+ 'save'
336
+ )
337
+ .mockResolvedValue(dummyEntity);
338
+ const options: CreateOptions = { forceTransaction: false, transactionManager: transactionManagerMock };
339
+ const result = await service.create(dummyEntity, options);
340
+ expect(result).toEqual(dummyEntity);
341
+ expect(saveSpy).toHaveBeenCalledWith(dummyEntity, transactionManagerMock);
342
+ });
343
+ it('should use transaction when forceTransaction is true and no transactionManager is provided', async () => {
344
+ const saveSpy = vi
345
+ .spyOn(
346
+ service as unknown as { save(_data: TestEntity, _transactionManager?: EntityManager): Promise<TestEntity> },
347
+ 'save'
348
+ )
349
+ .mockResolvedValue(dummyEntity);
350
+ const options: CreateOptions = { forceTransaction: true };
351
+ const transactionSpy = repositoryMock.manager.transaction as ReturnType<typeof vi.fn>;
352
+ const result = await service.create(dummyEntity, options);
353
+ expect(result).toEqual(dummyEntity);
354
+ expect(transactionSpy).toHaveBeenCalledTimes(1);
355
+ expect(saveSpy).toHaveBeenCalledWith(dummyEntity, transactionManagerMock);
356
+ });
357
+ it('should throw ApplicationError with specific message when unique violation occurs with matching regex', async () => {
358
+ const saveSpy = vi
359
+ .spyOn(
360
+ service as unknown as { save(_data: TestEntity, _transactionManager?: EntityManager): Promise<TestEntity> },
361
+ 'save'
362
+ )
363
+ .mockImplementation(() => {
364
+ throw new PostgresError('err', {
365
+ code: PostgresErrorCode.UniqueViolation,
366
+ detail: 'Key (name)=Test',
367
+ table: 'TestEntity'
368
+ });
369
+ });
370
+ const options: CreateOptions = { forceTransaction: false, transactionManager: transactionManagerMock };
371
+ await expect(service.create(dummyEntity, options)).rejects.toEqual(
372
+ new ApplicationError('TestEntity: name needs to be unique')
373
+ );
374
+ expect(saveSpy).toHaveBeenCalledWith(dummyEntity, transactionManagerMock);
375
+ });
376
+ it('should throw ApplicationError with default message when unique violation occurs with non matching detail', async () => {
377
+ const errorObj = {
378
+ code: PostgresErrorCode.UniqueViolation,
379
+ detail: 'Non matching detail',
380
+ table: 'TestEntity'
381
+ };
382
+ const saveSpy = vi
383
+ .spyOn(
384
+ service as unknown as { save(_data: TestEntity, _transactionManager?: EntityManager): Promise<TestEntity> },
385
+ 'save'
386
+ )
387
+ .mockRejectedValue(errorObj);
388
+ const options: CreateOptions = { forceTransaction: false, transactionManager: transactionManagerMock };
389
+ await expect(service.create(dummyEntity, options)).rejects.toEqual(
390
+ new ApplicationError('TestEntity: a column value you have provided needs to be unique')
391
+ );
392
+ expect(saveSpy).toHaveBeenCalledWith(dummyEntity, transactionManagerMock);
393
+ });
394
+ it('should rethrow error if error code is not UniqueViolation', async () => {
395
+ const errorObj = {
396
+ code: 'SomeOtherError',
397
+ message: 'Error occurred'
398
+ };
399
+ const saveSpy = vi
400
+ .spyOn(
401
+ service as unknown as { save(_data: TestEntity, _transactionManager?: EntityManager): Promise<TestEntity> },
402
+ 'save'
403
+ )
404
+ .mockRejectedValue(errorObj);
405
+ const options: CreateOptions = { forceTransaction: false, transactionManager: transactionManagerMock };
406
+ await expect(service.create(dummyEntity, options)).rejects.toEqual(errorObj);
407
+ expect(saveSpy).toHaveBeenCalledWith(dummyEntity, transactionManagerMock);
408
+ });
409
+ });
410
+
411
+ describe('count', () => {
412
+ let qbMock: SQLQueryBuilderService;
413
+ let queryBuilderCountMock: QueryBuilderMock;
414
+ let service: TypeORMDBEntityService<TestEntity>;
415
+ beforeEach(() => {
416
+ // Create a qbMock with our dummy implementations.
417
+ qbMock = createQBMock();
418
+ // Create a dummy query builder for count that implements getCount.
419
+ queryBuilderCountMock = {
420
+ getCount: vi.fn().mockResolvedValue(5),
421
+ getOne: vi.fn().mockResolvedValue(dummyEntity)
422
+ };
423
+ // Create a repository mock that returns our count query builder.
424
+ repositoryMock = createRepositoryMock(queryBuilderCountMock);
425
+ transactionManagerMock = (repositoryMock as unknown as TransactionManagerGetter).__getTransactionManager();
426
+ // Build a dummy schema with a single primary key.
427
+ const dummySchema = new EntitySchema<TestEntity>({
428
+ name: 'TestEntity',
429
+ columns: {
430
+ id: { type: Number, primary: true },
431
+ name: { type: String }
432
+ }
433
+ });
434
+ service = new TypeORMDBEntityService<TestEntity>(qbMock, repositoryMock, dummySchema);
435
+ });
436
+ it('should return count when transactionManager is provided', async () => {
437
+ const options: CountOptions = {
438
+ filters: { id: 1 },
439
+ forceTransaction: false,
440
+ transactionManager: transactionManagerMock,
441
+ withDeleted: true
442
+ };
443
+ const count = await service.count(options);
444
+ expect(count).toEqual(5);
445
+ // Verify that parseFilters was called with correct arguments.
446
+ expect(qbMock.parseFilters).toHaveBeenCalledWith('TestEntity', options.filters);
447
+ // Verify that parseRelations is called.
448
+ expect(qbMock.parseRelations).toHaveBeenCalledWith('TestEntity', [], { dummyInclude: true });
449
+ // Verify that buildQuery is called with the proper parameters.
450
+ expect(qbMock.buildQuery).toHaveBeenCalledTimes(1);
451
+ });
452
+ it('should use a transaction when forceTransaction is true and transactionManager is not provided', async () => {
453
+ const transactionSpy = vi.spyOn(repositoryMock.manager, 'transaction');
454
+ const options: CountOptions = {
455
+ filters: { id: 1 },
456
+ forceTransaction: true,
457
+ withDeleted: false
458
+ };
459
+ const count = await service.count(options);
460
+ expect(count).toEqual(5);
461
+ expect(transactionSpy).toHaveBeenCalledTimes(1);
462
+ });
463
+ });
464
+
465
+ describe('delete', () => {
466
+ const dummyFilters = { id: 1 };
467
+ let qbMock: SQLQueryBuilderService;
468
+ let deleteQueryBuilderMock: QueryBuilderMock;
469
+ let dummySchema: EntitySchema<TestEntity>;
470
+ let service: TypeORMDBEntityService<TestEntity>;
471
+ beforeEach(() => {
472
+ qbMock = createQBMock();
473
+ dummySchema = new EntitySchema<TestEntity>({
474
+ name: 'TestEntity',
475
+ columns: {
476
+ id: { type: Number, primary: true },
477
+ name: { type: String }
478
+ }
479
+ });
480
+ // Create a dummy query builder with only an execute method.
481
+ deleteQueryBuilderMock = {
482
+ delete: vi.fn().mockReturnThis(),
483
+ execute: vi.fn(),
484
+ getMany: vi.fn().mockResolvedValue([]),
485
+ returning: vi.fn().mockReturnThis(),
486
+ set: vi.fn().mockReturnThis(),
487
+ skip: vi.fn().mockReturnThis(),
488
+ softDelete: vi.fn().mockReturnThis(),
489
+ take: vi.fn().mockReturnThis(),
490
+ update: vi.fn().mockReturnThis()
491
+ };
492
+ repositoryMock = createRepositoryMock(deleteQueryBuilderMock);
493
+ transactionManagerMock = (repositoryMock as unknown as TransactionManagerGetter).__getTransactionManager();
494
+ service = new TypeORMDBEntityService<TestEntity>(qbMock, repositoryMock, dummySchema);
495
+ });
496
+ it('should use a transaction when forceTransaction is true and no transactionManager is provided', async () => {
497
+ const options = { filters: dummyFilters, forceTransaction: true, softDelete: true };
498
+ // Stub parseFilters to return a dummy where clause with an empty include.
499
+ vi.spyOn(qbMock, 'parseFilters').mockReturnValue({
500
+ where: { dummy: { params: { a: 1 }, query: 'dummyQuery' } },
501
+ include: {}
502
+ });
503
+ // Stub buildQuery as no-op.
504
+ vi.spyOn(qbMock, 'buildQuery').mockImplementation(() => {});
505
+ (deleteQueryBuilderMock.execute as ReturnType<typeof vi.fn>).mockResolvedValue({ affected: 2 });
506
+ const result = await service.delete(options);
507
+ expect(repositoryMock.manager.transaction).toHaveBeenCalled();
508
+ expect(result).toEqual({ count: 2 });
509
+ });
510
+ it('should delete using softDelete when include is empty', async () => {
511
+ const options = {
512
+ filters: dummyFilters,
513
+ forceTransaction: false,
514
+ softDelete: true,
515
+ transactionManager: transactionManagerMock
516
+ };
517
+ // Stub parseFilters to return an empty include.
518
+ vi.spyOn(qbMock, 'parseFilters').mockReturnValue({
519
+ where: { dummy: { params: { a: 1 }, query: 'dummyQuery' } },
520
+ include: {}
521
+ });
522
+ vi.spyOn(qbMock, 'buildQuery').mockImplementation(() => {});
523
+ (deleteQueryBuilderMock.execute as ReturnType<typeof vi.fn>).mockResolvedValue({ affected: 3 });
524
+ const result = await service.delete(options);
525
+ expect(qbMock.parseFilters).toHaveBeenCalledWith('TestEntity', dummyFilters);
526
+ expect(qbMock.buildQuery).toHaveBeenCalledWith(deleteQueryBuilderMock, {
527
+ where: { dummy: { params: { a: 1 }, query: 'dummyQuery' } }
528
+ });
529
+ expect(result).toEqual({ count: 3 });
530
+ });
531
+ it('should delete using hard delete when softDelete is false and include is empty', async () => {
532
+ const options = {
533
+ filters: dummyFilters,
534
+ forceTransaction: false,
535
+ softDelete: false,
536
+ transactionManager: transactionManagerMock
537
+ };
538
+ vi.spyOn(qbMock, 'parseFilters').mockReturnValue({
539
+ where: { dummy: { params: { a: 2 }, query: 'dummyQuery2' } },
540
+ include: {}
541
+ });
542
+ vi.spyOn(qbMock, 'buildQuery').mockImplementation(() => {});
543
+ (deleteQueryBuilderMock.execute as ReturnType<typeof vi.fn>).mockResolvedValue({ affected: 4 });
544
+ const result = await service.delete(options);
545
+ expect(qbMock.buildQuery).toHaveBeenCalledWith(deleteQueryBuilderMock, {
546
+ where: { dummy: { params: { a: 2 }, query: 'dummyQuery2' } }
547
+ });
548
+ expect(result).toEqual({ count: 4 });
549
+ });
550
+ it('should delete using include branch when include is not empty', async () => {
551
+ const options = {
552
+ filters: dummyFilters,
553
+ forceTransaction: false,
554
+ softDelete: true,
555
+ transactionManager: transactionManagerMock
556
+ };
557
+ // Stub parseFilters to return a non-empty include.
558
+ vi.spyOn(qbMock, 'parseFilters').mockReturnValue({
559
+ where: { dummy: { params: { a: 3 }, query: 'dummyQuery3' } },
560
+ include: { relation: 'relationName' }
561
+ });
562
+ // Stub the find method to return a dummy find result.
563
+ const findResult = { items: [{ id: 1, name: 'Test' }] };
564
+ const findSpy = vi.spyOn(service, 'find').mockResolvedValue(findResult as DataFindResults<TestEntity>);
565
+ vi.spyOn(qbMock, 'buildQuery').mockImplementation(() => {});
566
+ (deleteQueryBuilderMock.execute as ReturnType<typeof vi.fn>).mockResolvedValue({ affected: 5 });
567
+ // The expected where clause is produced by buildPrimaryKeyWhereClause.
568
+ // For a single primary key "id", it should be:
569
+ // { field: "id", value: { params: { id: [1] }, query: `"TestEntity"."id" in :id` } }
570
+ const expectedPKClause = { field: 'id', value: { params: { id: [1] }, query: '"TestEntity"."id" in :id' } };
571
+ const result = await service.delete(options);
572
+ expect(findSpy).toHaveBeenCalledWith({ filters: dummyFilters, transactionManager: transactionManagerMock });
573
+ expect(qbMock.buildQuery).toHaveBeenCalledWith(deleteQueryBuilderMock, {
574
+ where: { [expectedPKClause.field]: expectedPKClause.value }
575
+ });
576
+ expect(result).toEqual({ count: 5 });
577
+ });
578
+ });
579
+
580
+ describe('find', () => {
581
+ let findQueryBuilderMock: QueryBuilderMock;
582
+ let service: TypeORMDBEntityService<TestEntity>;
583
+ let qbMock: SQLQueryBuilderService;
584
+ beforeEach(() => {
585
+ qbMock = createQBMock();
586
+ // Create a dummy query builder that simulates chainable skip and take, and a getMany method.
587
+ findQueryBuilderMock = {
588
+ skip: vi.fn().mockReturnThis(),
589
+ take: vi.fn().mockReturnThis(),
590
+ getMany: vi.fn().mockResolvedValue([])
591
+ };
592
+ repositoryMock = createRepositoryMock(findQueryBuilderMock);
593
+ transactionManagerMock = (repositoryMock as unknown as TransactionManagerGetter).__getTransactionManager();
594
+ const dummySchema = new EntitySchema<TestEntity>({
595
+ name: 'TestEntity',
596
+ columns: {
597
+ id: { type: Number, primary: true },
598
+ name: { type: String }
599
+ }
600
+ });
601
+ service = new TypeORMDBEntityService<TestEntity>(qbMock, repositoryMock, dummySchema);
602
+ });
603
+ it('should return paginated results with "more" flag true when items length equals perPage+1', async () => {
604
+ // If perPage is 2, then perPage + 1 is 3.
605
+ const dummyItems: TestEntity[] = [
606
+ { id: 1, name: 'A' },
607
+ { id: 2, name: 'B' },
608
+ { id: 3, name: 'C' }
609
+ ];
610
+ findQueryBuilderMock.getMany = vi.fn().mockResolvedValue(dummyItems);
611
+ const options = {
612
+ filters: { name: 'test' },
613
+ forceTransaction: false,
614
+ page: 1,
615
+ perPage: 2,
616
+ findAll: false,
617
+ transactionManager: transactionManagerMock,
618
+ withDeleted: false
619
+ };
620
+ const results = await service.find(options);
621
+ // With page=1 and perPage=2, skip should be called with 0 and take with 3.
622
+ expect(findQueryBuilderMock.skip).toHaveBeenCalledWith(0);
623
+ expect(findQueryBuilderMock.take).toHaveBeenCalledWith(2 + 1);
624
+ // The service should pop the extra item and set "more" to true.
625
+ expect(results.page).toBe(1);
626
+ expect(results.perPage).toBe(2);
627
+ expect(results.more).toBe(true);
628
+ expect(results.items).toEqual(dummyItems.slice(0, 2));
629
+ });
630
+ it('should return paginated results with "more" flag false when items length is less than perPage+1', async () => {
631
+ const dummyItems: TestEntity[] = [
632
+ { id: 1, name: 'A' },
633
+ { id: 2, name: 'B' }
634
+ ];
635
+ findQueryBuilderMock.getMany = vi.fn().mockResolvedValue(dummyItems);
636
+ const options = {
637
+ filters: { name: 'test' },
638
+ forceTransaction: false,
639
+ page: 1,
640
+ perPage: 2,
641
+ findAll: false,
642
+ transactionManager: transactionManagerMock,
643
+ withDeleted: false
644
+ };
645
+ const results = await service.find(options);
646
+ expect(findQueryBuilderMock.skip).toHaveBeenCalledWith(0);
647
+ expect(findQueryBuilderMock.take).toHaveBeenCalledWith(3);
648
+ expect(results.page).toBe(1);
649
+ expect(results.perPage).toBe(2);
650
+ expect(results.more).toBe(false);
651
+ expect(results.items).toEqual(dummyItems);
652
+ });
653
+ it('should process orderBy option if provided', async () => {
654
+ // Prepare dummy items to be returned by getMany.
655
+ const dummyItems: TestEntity[] = [
656
+ { id: 1, name: 'A' },
657
+ { id: 2, name: 'B' }
658
+ ];
659
+ // Simulate getMany returning our dummy items.
660
+ findQueryBuilderMock.getMany = vi.fn().mockResolvedValue(dummyItems);
661
+ // Create a fake orderBy data to be returned by parseOrderBy.
662
+ const orderByData = {
663
+ orderBy: [{ column: 'id', order: 'DESC' }] as unknown as DataOrderBy[],
664
+ include: { extraOrder: true } as unknown as IncludeItems
665
+ };
666
+ const parseOrderBySpy = vi.spyOn(qbMock, 'parseOrderBy').mockReturnValue(orderByData);
667
+ const options = {
668
+ filters: { name: 'test' },
669
+ forceTransaction: false,
670
+ page: 1,
671
+ perPage: 10,
672
+ findAll: false,
673
+ transactionManager: transactionManagerMock,
674
+ withDeleted: false,
675
+ orderBy: 'id_DESC'
676
+ };
677
+ const results = await service.find(options as unknown as FindOptions);
678
+ // Verify that parseOrderBy was called with the proper table name and orderBy value.
679
+ expect(parseOrderBySpy).toHaveBeenCalledWith('TestEntity', 'id_DESC');
680
+ // Verify that the overall include contains the extra include from orderBy.
681
+ // Also, check that qbMock.buildQuery was called with the orderBy array from orderByData.
682
+ expect(qbMock.buildQuery).toHaveBeenCalledWith(
683
+ findQueryBuilderMock,
684
+ expect.objectContaining({
685
+ orderBy: orderByData.orderBy
686
+ })
687
+ );
688
+ // Confirm the results remain consistent with pagination.
689
+ expect(results.page).toBe(1);
690
+ expect(results.perPage).toBe(10);
691
+ expect(results.items).toEqual(dummyItems);
692
+ });
693
+ it('should return all results when findAll is true and not call skip/take', async () => {
694
+ const dummyItems: TestEntity[] = [
695
+ { id: 1, name: 'A' },
696
+ { id: 2, name: 'B' },
697
+ { id: 3, name: 'C' }
698
+ ];
699
+ findQueryBuilderMock.getMany = vi.fn().mockResolvedValue(dummyItems);
700
+ const options = {
701
+ filters: { name: 'test' },
702
+ forceTransaction: false,
703
+ page: 1,
704
+ perPage: 2,
705
+ findAll: true,
706
+ transactionManager: transactionManagerMock,
707
+ withDeleted: false
708
+ };
709
+ const results = await service.find(options);
710
+ // In the "findAll" branch, skip() and take() should not be called.
711
+ expect(findQueryBuilderMock.skip).not.toHaveBeenCalled();
712
+ expect(findQueryBuilderMock.take).not.toHaveBeenCalled();
713
+ // findResults.perPage is set to the length of the returned items.
714
+ expect(results.page).toBe(1);
715
+ expect(results.perPage).toBe(dummyItems.length);
716
+ expect(results.more).toBe(false);
717
+ expect(results.items).toEqual(dummyItems);
718
+ });
719
+ it('should parse page and perPage as numbers when provided as strings', async () => {
720
+ const dummyItems: TestEntity[] = [
721
+ { id: 1, name: 'A' },
722
+ { id: 2, name: 'B' },
723
+ { id: 3, name: 'C' }
724
+ ];
725
+ findQueryBuilderMock.getMany = vi.fn().mockResolvedValue(dummyItems);
726
+ const options = {
727
+ filters: { name: 'test' },
728
+ forceTransaction: false,
729
+ page: '2', // provided as string
730
+ perPage: '2', // provided as string
731
+ findAll: false,
732
+ transactionManager: transactionManagerMock,
733
+ withDeleted: false
734
+ };
735
+ const results = await service.find(options as unknown as FindOptions);
736
+ // For page "2" and perPage "2": skip = (2 - 1) * 2 = 2, take = 3.
737
+ expect(findQueryBuilderMock.skip).toHaveBeenCalledWith(2);
738
+ expect(findQueryBuilderMock.take).toHaveBeenCalledWith(2 + 1);
739
+ expect(results.page).toBe(2);
740
+ expect(results.perPage).toBe(2);
741
+ });
742
+ it('should use a transaction when forceTransaction is true and no transactionManager is provided', async () => {
743
+ const transactionSpy = vi.spyOn(repositoryMock.manager, 'transaction');
744
+ // For this branch, simulate getMany returning an empty array.
745
+ findQueryBuilderMock.getMany = vi.fn().mockResolvedValue([]);
746
+ const options = {
747
+ filters: { name: 'test' },
748
+ forceTransaction: true,
749
+ page: 1,
750
+ perPage: 2,
751
+ findAll: false,
752
+ withDeleted: false
753
+ };
754
+ const results = await service.find(options);
755
+ expect(transactionSpy).toHaveBeenCalledTimes(1);
756
+ expect(results.items).toEqual([]);
757
+ expect(results.page).toEqual(1);
758
+ expect(results.perPage).toEqual(2);
759
+ });
760
+ });
761
+
762
+ describe('findOne', () => {
763
+ let qbMock: SQLQueryBuilderService;
764
+ let service: TypeORMDBEntityService<TestEntity>;
765
+ beforeEach(() => {
766
+ // Reset mocks before each test.
767
+ qbMock = createQBMock();
768
+ // Create a repository mock that uses our dummy query builder.
769
+ repositoryMock = createRepositoryMock(queryBuilderMock);
770
+ transactionManagerMock = (repositoryMock as unknown as TransactionManagerGetter).__getTransactionManager();
771
+ // Create a dummy schema with one primary key ("id") and one extra column.
772
+ const dummySchema = new EntitySchema<TestEntity>({
773
+ name: 'TestEntity',
774
+ columns: {
775
+ id: { type: Number, primary: true },
776
+ name: { type: String }
777
+ }
778
+ });
779
+ service = new TypeORMDBEntityService<TestEntity>(qbMock, repositoryMock, dummySchema);
780
+ // Clear mock history.
781
+ vi.clearAllMocks();
782
+ });
783
+ it('should return the entity when found without forceTransaction and without orderBy', async () => {
784
+ const options: FindOneOptions = {
785
+ filters: { id: 1 },
786
+ forceTransaction: false,
787
+ transactionManager: transactionManagerMock,
788
+ withDeleted: false
789
+ };
790
+ const result = await service.findOne(options);
791
+ expect(result).toEqual(dummyEntity);
792
+ // Verify that parseFilters was called with the correct arguments.
793
+ expect(qbMock.parseFilters).toHaveBeenCalledWith('TestEntity', options.filters, {
794
+ operator: undefined,
795
+ isTopLevel: true
796
+ });
797
+ // When no relations are passed, the optRelations defaults to an empty array.
798
+ expect(qbMock.parseRelations).toHaveBeenCalledWith('TestEntity', [], { dummyInclude: true });
799
+ // Since no orderBy option is provided, the orderBy array should be empty.
800
+ expect(qbMock.buildQuery).toHaveBeenCalledWith(queryBuilderMock, {
801
+ where: { dummy: 'filter' },
802
+ include: { dummyInclude: true, extraRelation: true },
803
+ orderBy: [],
804
+ withDeleted: false
805
+ });
806
+ });
807
+ it('should return the entity when found with orderBy provided', async () => {
808
+ const options: FindOneOptions = {
809
+ filters: { id: 1 },
810
+ forceTransaction: false,
811
+ transactionManager: transactionManagerMock,
812
+ orderBy: 'id_ASC' as unknown as GenericObject<string>,
813
+ withDeleted: true
814
+ };
815
+ const result = await service.findOne(options);
816
+ expect(result).toEqual(dummyEntity);
817
+ expect(qbMock.parseFilters).toHaveBeenCalledWith('TestEntity', options.filters, {
818
+ operator: undefined,
819
+ isTopLevel: true
820
+ });
821
+ expect(qbMock.parseRelations).toHaveBeenCalledWith('TestEntity', [], { dummyInclude: true });
822
+ expect(qbMock.parseOrderBy).toHaveBeenCalledWith('TestEntity', options.orderBy);
823
+ expect(qbMock.buildQuery).toHaveBeenCalledWith(queryBuilderMock, {
824
+ where: { dummy: 'filter' },
825
+ include: { dummyInclude: true, extraRelation: true },
826
+ orderBy: [{ column: 'id', order: 'ASC' }],
827
+ withDeleted: true
828
+ });
829
+ });
830
+ it('should pass selectOperator to parseFilters if provided', async () => {
831
+ // Here we cast the string to unknown and then to the expected type.
832
+ const options: FindOneOptions = {
833
+ filters: { id: 1 },
834
+ forceTransaction: false,
835
+ transactionManager: transactionManagerMock,
836
+ selectOperator: 'customOperator' as unknown as FindOneOptions['selectOperator'],
837
+ withDeleted: false
838
+ };
839
+ const result = await service.findOne(options);
840
+ expect(result).toEqual(dummyEntity);
841
+ expect(qbMock.parseFilters).toHaveBeenCalledWith('TestEntity', options.filters, {
842
+ operator: 'customOperator',
843
+ isTopLevel: true
844
+ });
845
+ });
846
+ it('should use a transaction when forceTransaction is true and transactionManager is not provided', async () => {
847
+ const options: FindOneOptions = {
848
+ filters: { id: 1 },
849
+ forceTransaction: true,
850
+ withDeleted: false
851
+ };
852
+ const result = await service.findOne(options);
853
+ expect(result).toEqual(dummyEntity);
854
+ expect(repositoryMock.manager.transaction).toHaveBeenCalledTimes(1);
855
+ // qb.buildQuery should be called in the recursive call.
856
+ expect(qbMock.buildQuery).toHaveBeenCalled();
857
+ });
858
+ });
859
+
860
+ describe('getEntityTarget', () => {
861
+ it('should return the repository target', () => {
862
+ const qbMock = createQBMock();
863
+ // Create a dummy entity target (can be a function or string)
864
+ const dummyTarget = 'TestEntityTarget';
865
+ // Create a dummy schema for our test entity.
866
+ const dummySchema = new EntitySchema<TestEntity>({
867
+ name: 'TestEntity',
868
+ columns: {
869
+ id: { type: Number, primary: true },
870
+ name: { type: String }
871
+ }
872
+ });
873
+ // Create a dummy repository that includes the target.
874
+ const repositoryForTarget = {
875
+ metadata: { name: 'TestEntity', tableName: 'TestEntity' },
876
+ target: dummyTarget,
877
+ createQueryBuilder: vi.fn(),
878
+ manager: { transaction: vi.fn() }
879
+ } as unknown as Repository<TestEntity>;
880
+ // Instantiate the service with the dummy repository.
881
+ const service = new TypeORMDBEntityService<TestEntity>(qbMock, repositoryForTarget, dummySchema);
882
+ // Verify that getEntityTarget returns the repository's target.
883
+ expect(service.getEntityTarget()).toEqual(dummyTarget);
884
+ });
885
+ });
886
+
887
+ describe('getRepository', () => {
888
+ let qbMock: SQLQueryBuilderService;
889
+ let repositoryForTest: Repository<TestEntity>;
890
+ let service: TypeORMDBEntityService<TestEntity>;
891
+ beforeEach(() => {
892
+ qbMock = createQBMock();
893
+ // Create a dummy repository with a target.
894
+ repositoryForTest = {
895
+ metadata: { name: 'TestEntity', tableName: 'TestEntity' },
896
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
897
+ target: 'TestEntityTarget' as unknown as Function,
898
+ createQueryBuilder: vi.fn(),
899
+ manager: { transaction: vi.fn() }
900
+ } as unknown as Repository<TestEntity>;
901
+ const dummySchema = new EntitySchema<TestEntity>({
902
+ name: 'TestEntity',
903
+ columns: {
904
+ id: { type: Number, primary: true },
905
+ name: { type: String }
906
+ }
907
+ });
908
+ service = new TypeORMDBEntityService<TestEntity>(qbMock, repositoryForTest, dummySchema);
909
+ });
910
+ it('should return the repository when no transactionManager is provided', () => {
911
+ const result = service['getRepository']();
912
+ expect(result).toEqual(repositoryForTest);
913
+ });
914
+ it('should return the repository from the transactionManager when provided', () => {
915
+ const dummyRepoFromTM = { ...repositoryForTest, extraProp: 'dummy' };
916
+ const transactionManager: EntityManager = {
917
+ getRepository: vi.fn().mockReturnValue(dummyRepoFromTM)
918
+ } as unknown as EntityManager;
919
+ const result = service['getRepository'](transactionManager);
920
+ expect(transactionManager.getRepository).toHaveBeenCalledWith(repositoryForTest.target);
921
+ expect(result).toEqual(dummyRepoFromTM);
922
+ });
923
+ });
924
+
925
+ describe('processManyToMany', () => {
926
+ let qbMock: SQLQueryBuilderService;
927
+ let service: TypeORMDBEntityService<TestEntity>;
928
+ // Use a simple dummy schema; the entity type details are not important for this test.
929
+ const dummySchema = new EntitySchema<TestEntity>({
930
+ name: 'TestEntity',
931
+ columns: {
932
+ id: { type: Number, primary: true },
933
+ name: { type: String }
934
+ }
935
+ });
936
+ beforeEach(() => {
937
+ // Create a qbMock with a defined columnQuotesSymbol.
938
+ qbMock = createQBMock();
939
+ qbMock.columnQuotesSymbol = '"';
940
+ // Create a dummy repository (its methods won't be used except for transaction handling).
941
+ // repositoryMock = createRepositoryMock({} as FindQueryBuilderMock, 'TestEntity');
942
+ repositoryMock = createRepositoryMock(queryBuilderMock);
943
+ transactionManagerMock = (repositoryMock as unknown as TransactionManagerGetter).__getTransactionManager();
944
+ // Instantiate the service.
945
+ service = new TypeORMDBEntityService<TestEntity>(qbMock, repositoryMock, dummySchema);
946
+ });
947
+ it('should use a transaction when no transactionManager is provided', async () => {
948
+ // Create a fake transaction manager with a spy on the query method.
949
+ const data = {
950
+ counterpartColumn: 'counterpart',
951
+ currentEntityColumn: 'current',
952
+ id: 42,
953
+ tableName: 'TestTable',
954
+ items: [] // no items
955
+ };
956
+ await service['processManyToMany'](data);
957
+ expect(repositoryMock.manager.transaction).toHaveBeenCalledTimes(1);
958
+ // With no items, no query should be executed.
959
+ expect(transactionManagerMock.query).not.toHaveBeenCalled();
960
+ });
961
+ it('should execute both delete and insert queries for mixed items', async () => {
962
+ const data = {
963
+ counterpartColumn: 'counterpart',
964
+ currentEntityColumn: 'current',
965
+ id: 42,
966
+ tableName: 'TestTable',
967
+ items: [
968
+ { deleted: true, value: 10 },
969
+ { deleted: false, value: 20 }
970
+ ]
971
+ };
972
+ await service['processManyToMany'](data, { transactionManager: transactionManagerMock });
973
+ // Expected delete query:
974
+ // Initially: deleteQuery = `delete from "TestTable" where `
975
+ // After first (deleted) item: becomes `delete from "TestTable" where ("current" = 42 and "counterpart" = 10)`
976
+ const expectedDeleteQuery = 'delete from "TestTable" where ("current" = 42 and "counterpart" = 10)';
977
+ // Expected insert query:
978
+ // insertQuery = `insert into "TestTable" ("current", "counterpart") values (42, 20) on conflict do nothing`
979
+ const expectedInsertQuery =
980
+ 'insert into "TestTable" ("current", "counterpart") values (42, 20) on conflict do nothing';
981
+ // Expect query to be called twice in order: first delete then insert.
982
+ expect(transactionManagerMock.query).toHaveBeenNthCalledWith(1, expectedDeleteQuery);
983
+ expect(transactionManagerMock.query).toHaveBeenNthCalledWith(2, expectedInsertQuery);
984
+ });
985
+ it('should execute only a delete query when all items are deleted', async () => {
986
+ const data = {
987
+ counterpartColumn: 'counterpart',
988
+ currentEntityColumn: 'current',
989
+ id: 42,
990
+ tableName: 'TestTable',
991
+ items: [
992
+ { deleted: true, value: 10 },
993
+ { deleted: true, value: 15 }
994
+ ]
995
+ };
996
+ await service['processManyToMany'](data, { transactionManager: transactionManagerMock });
997
+ // Expected delete query:
998
+ // After first item: `delete from "TestTable" where ("current" = 42 and "counterpart" = 10)`
999
+ // After second item: appended with ` or ("current" = 42 and "counterpart" = 15)`
1000
+ const expectedDeleteQuery =
1001
+ 'delete from "TestTable" where ("current" = 42 and "counterpart" = 10) or ("current" = 42 and "counterpart" = 15)';
1002
+ expect(transactionManagerMock.query).toHaveBeenCalledTimes(1);
1003
+ expect(transactionManagerMock.query).toHaveBeenCalledWith(expectedDeleteQuery);
1004
+ });
1005
+ it('should execute only an insert query when all items are not deleted', async () => {
1006
+ const data = {
1007
+ counterpartColumn: 'counterpart',
1008
+ currentEntityColumn: 'current',
1009
+ id: 42,
1010
+ tableName: 'TestTable',
1011
+ items: [
1012
+ { deleted: false, value: 20 },
1013
+ { deleted: false, value: 30 }
1014
+ ]
1015
+ };
1016
+ await service['processManyToMany'](data, { transactionManager: transactionManagerMock });
1017
+ // Expected insert query:
1018
+ // For first item: `insert into "TestTable" ("current", "counterpart") values (42, 20)`
1019
+ // For second item: appended with `, (42, 30)`
1020
+ // Final query with on conflict clause:
1021
+ const expectedInsertQuery =
1022
+ 'insert into "TestTable" ("current", "counterpart") values (42, 20), (42, 30) on conflict do nothing';
1023
+ expect(transactionManagerMock.query).toHaveBeenCalledTimes(1);
1024
+ expect(transactionManagerMock.query).toHaveBeenCalledWith(expectedInsertQuery);
1025
+ });
1026
+ it('should not execute any queries when items array is empty', async () => {
1027
+ const data = {
1028
+ counterpartColumn: 'counterpart',
1029
+ currentEntityColumn: 'current',
1030
+ id: 42,
1031
+ tableName: 'TestTable',
1032
+ items: [] as { deleted: boolean; value: number }[]
1033
+ };
1034
+ await service['processManyToMany'](data, { transactionManager: transactionManagerMock });
1035
+ expect(transactionManagerMock.query).not.toHaveBeenCalled();
1036
+ });
1037
+ });
1038
+
1039
+ describe('save', () => {
1040
+ let dummyData: TestEntity;
1041
+ let dummySchema: EntitySchema<TestEntity>;
1042
+ let qbMock: SQLQueryBuilderService;
1043
+ let repositoryForSave: Repository<TestEntity>;
1044
+ let service: TypeORMDBEntityService<TestEntity>;
1045
+ beforeEach(() => {
1046
+ qbMock = createQBMock();
1047
+ dummyData = { id: 1, name: 'Test' };
1048
+ // Create a dummy repository that includes a save method.
1049
+ repositoryForSave = {
1050
+ metadata: { name: 'TestEntity', tableName: 'TestEntity' },
1051
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
1052
+ target: 'TestEntityTarget' as unknown as Function,
1053
+ save: vi.fn().mockResolvedValue(dummyData),
1054
+ createQueryBuilder: vi.fn(),
1055
+ manager: { transaction: vi.fn() }
1056
+ } as unknown as Repository<TestEntity>;
1057
+ dummySchema = new EntitySchema<TestEntity>({
1058
+ name: 'TestEntity',
1059
+ columns: {
1060
+ id: { type: Number, primary: true },
1061
+ name: { type: String }
1062
+ }
1063
+ });
1064
+ service = new TypeORMDBEntityService<TestEntity>(qbMock, repositoryForSave, dummySchema);
1065
+ });
1066
+ it('should call transactionManager.save when transactionManager is provided', async () => {
1067
+ // Create a dummy transaction manager with a save method.
1068
+ const tmSaveSpy = vi.fn().mockResolvedValue(dummyData);
1069
+ const dummyTransactionManager: EntityManager = { save: tmSaveSpy } as unknown as EntityManager;
1070
+ const result = await service['save'](dummyData, dummyTransactionManager);
1071
+ expect(tmSaveSpy).toHaveBeenCalledWith(repositoryForSave.target, dummyData);
1072
+ expect(result).toEqual(dummyData);
1073
+ });
1074
+ it('should call repository.save when no transactionManager is provided', async () => {
1075
+ const repoSaveSpy = repositoryForSave.save as ReturnType<typeof vi.fn>;
1076
+ const result = await service['save'](dummyData);
1077
+ expect(repoSaveSpy).toHaveBeenCalledWith(dummyData);
1078
+ expect(result).toEqual(dummyData);
1079
+ });
1080
+ });
1081
+
1082
+ describe('update', () => {
1083
+ let dummySchema: EntitySchema<TestEntity>;
1084
+ let dummyEntity: TestEntity;
1085
+ let qbMock: SQLQueryBuilderService;
1086
+ let service: TypeORMDBEntityService<TestEntity>;
1087
+ let updateQueryBuilderMock: QueryBuilderMock;
1088
+ beforeEach(() => {
1089
+ qbMock = createQBMock();
1090
+ updateQueryBuilderMock = {
1091
+ execute: vi.fn(),
1092
+ getMany: vi.fn().mockResolvedValue([]),
1093
+ returning: vi.fn().mockReturnThis(),
1094
+ set: vi.fn().mockReturnThis(),
1095
+ skip: vi.fn().mockReturnThis(),
1096
+ take: vi.fn().mockReturnThis(),
1097
+ update: vi.fn().mockReturnThis()
1098
+ };
1099
+ repositoryMock = createRepositoryMock(updateQueryBuilderMock);
1100
+ transactionManagerMock = (repositoryMock as unknown as TransactionManagerGetter).__getTransactionManager();
1101
+ dummyEntity = { id: 1, name: 'Test' };
1102
+ dummySchema = new EntitySchema<TestEntity>({
1103
+ columns: {
1104
+ id: { type: Number, primary: true },
1105
+ name: { type: String }
1106
+ },
1107
+ name: 'TestEntity'
1108
+ });
1109
+ service = new TypeORMDBEntityService<TestEntity>(qbMock, repositoryMock, dummySchema);
1110
+ vi.clearAllMocks();
1111
+ });
1112
+ it('should use a transaction when forceTransaction is true and no transactionManager is provided', async () => {
1113
+ (updateQueryBuilderMock.execute as ReturnType<typeof vi.fn>).mockResolvedValue({ affected: 1 });
1114
+ const options = { filters: { name: 'Test' }, forceTransaction: true, returnData: false };
1115
+ const result = await service.update(dummyEntity, options);
1116
+ expect(repositoryMock.manager.transaction).toHaveBeenCalled();
1117
+ expect(result).toEqual({ count: 1 });
1118
+ });
1119
+ it('should update and return count when returnData is false and no include is returned', async () => {
1120
+ const parsedWhere = { dummy: { params: { a: 1 }, query: 'dummy' } };
1121
+ vi.spyOn(qbMock, 'buildQuery').mockImplementation(() => {});
1122
+ vi.spyOn(qbMock, 'parseFilters').mockReturnValue({ where: parsedWhere, include: {} });
1123
+ (updateQueryBuilderMock.execute as ReturnType<typeof vi.fn>).mockResolvedValue({ affected: 2 });
1124
+ const options = {
1125
+ filters: { name: 'Test' },
1126
+ forceTransaction: false,
1127
+ returnData: false,
1128
+ transactionManager: transactionManagerMock
1129
+ };
1130
+ const result = await service.update(dummyEntity, options);
1131
+ expect(qbMock.parseFilters).toHaveBeenCalledWith('TestEntity', { name: 'Test' });
1132
+ expect(qbMock.buildQuery).toHaveBeenCalledWith(updateQueryBuilderMock, { where: parsedWhere });
1133
+ expect(result).toEqual({ count: 2 });
1134
+ });
1135
+ it('should update and return data when returnData is true', async () => {
1136
+ const parsedWhere = { dummy: { params: { a: 1 }, query: 'dummy' } };
1137
+ vi.spyOn(qbMock, 'buildQuery').mockImplementation(() => {});
1138
+ vi.spyOn(qbMock, 'parseFilters').mockReturnValue({ where: parsedWhere, include: {} });
1139
+ // Simulate the returning branch.
1140
+ (updateQueryBuilderMock.returning as ReturnType<typeof vi.fn>).mockReturnThis();
1141
+ const fakeRaw: TestEntity[] = [dummyEntity];
1142
+ (updateQueryBuilderMock.execute as ReturnType<typeof vi.fn>).mockResolvedValue({ raw: fakeRaw });
1143
+ const options = {
1144
+ filters: { name: 'Test' },
1145
+ forceTransaction: false,
1146
+ returnData: true,
1147
+ transactionManager: transactionManagerMock
1148
+ };
1149
+ const result = await service.update(dummyEntity, options);
1150
+ expect(qbMock.buildQuery).toHaveBeenCalledWith(updateQueryBuilderMock, { where: parsedWhere });
1151
+ expect(result).toEqual({ items: fakeRaw });
1152
+ });
1153
+ it('should update using include branch by calling find and buildPrimaryKeyWhereClause', async () => {
1154
+ // Simulate a non-empty include.
1155
+ const includeObj = { related: true };
1156
+ const parsedWhere = { dummy: { params: { a: 1 }, query: 'dummy' } };
1157
+ vi.spyOn(qbMock, 'parseFilters').mockReturnValue({
1158
+ where: parsedWhere,
1159
+ include: includeObj as unknown as IncludeItems
1160
+ });
1161
+ // Stub the find method to return a dummy result.
1162
+ const findResult = { items: [dummyEntity] };
1163
+ vi.spyOn(service, 'find').mockResolvedValue(findResult as unknown as DataFindResults<TestEntity>);
1164
+ vi.spyOn(qbMock, 'buildQuery').mockImplementation(() => {});
1165
+ (updateQueryBuilderMock.execute as ReturnType<typeof vi.fn>).mockResolvedValue({ affected: 3 });
1166
+ const options = {
1167
+ filters: { name: 'Test' },
1168
+ forceTransaction: false,
1169
+ returnData: false,
1170
+ transactionManager: transactionManagerMock
1171
+ };
1172
+ const result = await service.update(dummyEntity, options);
1173
+ // Use the actual buildPrimaryKeyWhereClause to compute expected where clause.
1174
+ const expectedPKClause = service['buildPrimaryKeyWhereClause'](findResult.items);
1175
+ const expectedWhere = { [expectedPKClause.field]: expectedPKClause.value };
1176
+ expect(qbMock.buildQuery).toHaveBeenCalledWith(updateQueryBuilderMock, { where: expectedWhere });
1177
+ expect(result).toEqual({ count: 3 });
1178
+ });
1179
+ });
1180
+ });