@onivoro/server-typeorm-mysql 24.0.0 → 24.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +297 -187
- package/package.json +9 -1
- package/src/lib/decorators/table.decorator.ts +1 -1
package/README.md
CHANGED
|
@@ -92,20 +92,21 @@ export class User {
|
|
|
92
92
|
```typescript
|
|
93
93
|
import { Injectable } from '@nestjs/common';
|
|
94
94
|
import { TypeOrmRepository, TypeOrmPagingRepository } from '@onivoro/server-typeorm-mysql';
|
|
95
|
+
import { EntityManager } from 'typeorm';
|
|
95
96
|
import { User } from './user.entity';
|
|
96
97
|
|
|
97
98
|
@Injectable()
|
|
98
99
|
export class UserRepository extends TypeOrmPagingRepository<User> {
|
|
99
|
-
constructor() {
|
|
100
|
-
super(User);
|
|
100
|
+
constructor(entityManager: EntityManager) {
|
|
101
|
+
super(User, entityManager);
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
async findByEmail(email: string): Promise<User | null> {
|
|
104
|
-
return this.
|
|
105
|
+
return this.getOne({ where: { email } });
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
async findActiveUsers(): Promise<User[]> {
|
|
108
|
-
return this.
|
|
109
|
+
return this.getMany({ where: { isActive: true } });
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
async findUsersWithPagination(page: number, limit: number) {
|
|
@@ -168,33 +169,98 @@ export class DatabaseModule {}
|
|
|
168
169
|
|
|
169
170
|
## Usage Examples
|
|
170
171
|
|
|
171
|
-
###
|
|
172
|
+
### Basic Repository Operations
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
import { Injectable } from '@nestjs/common';
|
|
176
|
+
import { TypeOrmRepository } from '@onivoro/server-typeorm-mysql';
|
|
177
|
+
import { EntityManager } from 'typeorm';
|
|
178
|
+
import { User } from './user.entity';
|
|
179
|
+
|
|
180
|
+
@Injectable()
|
|
181
|
+
export class UserRepository extends TypeOrmRepository<User> {
|
|
182
|
+
constructor(entityManager: EntityManager) {
|
|
183
|
+
super(User, entityManager);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Create a single user
|
|
187
|
+
async createUser(userData: Partial<User>): Promise<User> {
|
|
188
|
+
return this.postOne(userData);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Create multiple users
|
|
192
|
+
async createUsers(usersData: Partial<User>[]): Promise<User[]> {
|
|
193
|
+
return this.postMany(usersData);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Find users with filters
|
|
197
|
+
async findUsers(filters: { isActive?: boolean; email?: string }): Promise<User[]> {
|
|
198
|
+
return this.getMany({ where: filters });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Find users with count
|
|
202
|
+
async findUsersWithCount(filters: { isActive?: boolean }): Promise<[User[], number]> {
|
|
203
|
+
return this.getManyAndCount({ where: filters });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Find a single user
|
|
207
|
+
async findUserById(id: number): Promise<User> {
|
|
208
|
+
return this.getOne({ where: { id } });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Update user
|
|
212
|
+
async updateUser(id: number, updateData: Partial<User>): Promise<void> {
|
|
213
|
+
await this.patch({ id }, updateData);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Replace user data
|
|
217
|
+
async replaceUser(id: number, userData: Partial<User>): Promise<void> {
|
|
218
|
+
await this.put({ id }, userData);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Delete user permanently
|
|
222
|
+
async deleteUser(id: number): Promise<void> {
|
|
223
|
+
await this.delete({ id });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Soft delete user
|
|
227
|
+
async softDeleteUser(id: number): Promise<void> {
|
|
228
|
+
await this.softDelete({ id });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Advanced Repository Usage with Pagination
|
|
172
234
|
|
|
173
235
|
```typescript
|
|
174
236
|
import { Injectable } from '@nestjs/common';
|
|
175
237
|
import { TypeOrmPagingRepository, PageParams, PagedData } from '@onivoro/server-typeorm-mysql';
|
|
238
|
+
import { EntityManager, Like, Between } from 'typeorm';
|
|
176
239
|
import { User } from './user.entity';
|
|
177
|
-
import { FindOptionsWhere, Like, Between } from 'typeorm';
|
|
178
240
|
|
|
179
241
|
@Injectable()
|
|
180
242
|
export class AdvancedUserRepository extends TypeOrmPagingRepository<User> {
|
|
181
|
-
constructor() {
|
|
182
|
-
super(User);
|
|
243
|
+
constructor(entityManager: EntityManager) {
|
|
244
|
+
super(User, entityManager);
|
|
183
245
|
}
|
|
184
246
|
|
|
185
247
|
async searchUsers(
|
|
186
248
|
searchTerm: string,
|
|
187
249
|
pageParams: PageParams
|
|
188
250
|
): Promise<PagedData<User>> {
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
251
|
+
const whereConditions = this.buildWhereILike({
|
|
252
|
+
firstName: searchTerm,
|
|
253
|
+
lastName: searchTerm,
|
|
254
|
+
email: searchTerm
|
|
255
|
+
});
|
|
194
256
|
|
|
195
257
|
return this.findWithPaging(
|
|
196
258
|
{
|
|
197
|
-
where
|
|
259
|
+
where: [
|
|
260
|
+
{ firstName: Like(`%${searchTerm}%`) },
|
|
261
|
+
{ lastName: Like(`%${searchTerm}%`) },
|
|
262
|
+
{ email: Like(`%${searchTerm}%`) }
|
|
263
|
+
],
|
|
198
264
|
order: { createdAt: 'DESC' }
|
|
199
265
|
},
|
|
200
266
|
pageParams
|
|
@@ -221,7 +287,7 @@ export class AdvancedUserRepository extends TypeOrmPagingRepository<User> {
|
|
|
221
287
|
const cutoffDate = new Date();
|
|
222
288
|
cutoffDate.setDate(cutoffDate.getDate() - days);
|
|
223
289
|
|
|
224
|
-
return this.
|
|
290
|
+
return this.getMany({
|
|
225
291
|
where: {
|
|
226
292
|
lastLoginAt: Between(cutoffDate, new Date())
|
|
227
293
|
},
|
|
@@ -235,24 +301,24 @@ export class AdvancedUserRepository extends TypeOrmPagingRepository<User> {
|
|
|
235
301
|
inactive: number;
|
|
236
302
|
recentlyRegistered: number;
|
|
237
303
|
}> {
|
|
238
|
-
const [
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
)
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
|
|
304
|
+
const [allUsers, totalCount] = await this.getManyAndCount({});
|
|
305
|
+
const [activeUsers, activeCount] = await this.getManyAndCount({
|
|
306
|
+
where: { isActive: true }
|
|
307
|
+
});
|
|
308
|
+
const [recentUsers, recentCount] = await this.getManyAndCount({
|
|
309
|
+
where: {
|
|
310
|
+
createdAt: Between(
|
|
311
|
+
new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), // 7 days ago
|
|
312
|
+
new Date()
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
});
|
|
250
316
|
|
|
251
317
|
return {
|
|
252
|
-
total,
|
|
253
|
-
active,
|
|
254
|
-
inactive:
|
|
255
|
-
recentlyRegistered
|
|
318
|
+
total: totalCount,
|
|
319
|
+
active: activeCount,
|
|
320
|
+
inactive: totalCount - activeCount,
|
|
321
|
+
recentlyRegistered: recentCount
|
|
256
322
|
};
|
|
257
323
|
}
|
|
258
324
|
|
|
@@ -260,11 +326,15 @@ export class AdvancedUserRepository extends TypeOrmPagingRepository<User> {
|
|
|
260
326
|
userIds: number[],
|
|
261
327
|
updateData: Partial<User>
|
|
262
328
|
): Promise<void> {
|
|
263
|
-
|
|
329
|
+
for (const id of userIds) {
|
|
330
|
+
await this.patch({ id }, updateData);
|
|
331
|
+
}
|
|
264
332
|
}
|
|
265
333
|
|
|
266
334
|
async softDeleteUsers(userIds: number[]): Promise<void> {
|
|
267
|
-
|
|
335
|
+
for (const id of userIds) {
|
|
336
|
+
await this.softDelete({ id });
|
|
337
|
+
}
|
|
268
338
|
}
|
|
269
339
|
}
|
|
270
340
|
```
|
|
@@ -352,7 +422,6 @@ export class OrderItem {
|
|
|
352
422
|
|
|
353
423
|
```typescript
|
|
354
424
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
355
|
-
import { InjectRepository } from '@nestjs/typeorm';
|
|
356
425
|
import { AdvancedUserRepository } from './user.repository';
|
|
357
426
|
import { User } from './user.entity';
|
|
358
427
|
import { PageParams, PagedData } from '@onivoro/server-typeorm-mysql';
|
|
@@ -360,32 +429,29 @@ import { PageParams, PagedData } from '@onivoro/server-typeorm-mysql';
|
|
|
360
429
|
@Injectable()
|
|
361
430
|
export class UserService {
|
|
362
431
|
constructor(
|
|
363
|
-
@InjectRepository(User)
|
|
364
432
|
private userRepository: AdvancedUserRepository
|
|
365
433
|
) {}
|
|
366
434
|
|
|
367
435
|
async createUser(userData: Partial<User>): Promise<User> {
|
|
368
|
-
|
|
369
|
-
return this.userRepository.save(user);
|
|
436
|
+
return this.userRepository.postOne(userData);
|
|
370
437
|
}
|
|
371
438
|
|
|
372
439
|
async findUserById(id: number): Promise<User> {
|
|
373
|
-
const user = await this.userRepository.
|
|
440
|
+
const user = await this.userRepository.getOne({ where: { id } });
|
|
374
441
|
if (!user) {
|
|
375
442
|
throw new NotFoundException(`User with ID ${id} not found`);
|
|
376
443
|
}
|
|
377
444
|
return user;
|
|
378
445
|
}
|
|
379
446
|
|
|
380
|
-
async updateUser(id: number, updateData: Partial<User>): Promise<
|
|
447
|
+
async updateUser(id: number, updateData: Partial<User>): Promise<void> {
|
|
381
448
|
const user = await this.findUserById(id);
|
|
382
|
-
|
|
383
|
-
return this.userRepository.save(user);
|
|
449
|
+
await this.userRepository.patch({ id }, updateData);
|
|
384
450
|
}
|
|
385
451
|
|
|
386
452
|
async deleteUser(id: number): Promise<void> {
|
|
387
453
|
const user = await this.findUserById(id);
|
|
388
|
-
await this.userRepository.
|
|
454
|
+
await this.userRepository.softDelete({ id });
|
|
389
455
|
}
|
|
390
456
|
|
|
391
457
|
async searchUsers(
|
|
@@ -417,18 +483,17 @@ import {
|
|
|
417
483
|
generateDateQuery,
|
|
418
484
|
removeFalseyKeys,
|
|
419
485
|
getSkip,
|
|
420
|
-
getPagingKey
|
|
486
|
+
getPagingKey,
|
|
487
|
+
TypeOrmRepository
|
|
421
488
|
} from '@onivoro/server-typeorm-mysql';
|
|
422
|
-
import {
|
|
423
|
-
import { InjectRepository } from '@nestjs/typeorm';
|
|
489
|
+
import { EntityManager } from 'typeorm';
|
|
424
490
|
import { Order } from './order.entity';
|
|
425
491
|
|
|
426
492
|
@Injectable()
|
|
427
|
-
export class OrderService {
|
|
428
|
-
constructor(
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
) {}
|
|
493
|
+
export class OrderService extends TypeOrmRepository<Order> {
|
|
494
|
+
constructor(entityManager: EntityManager) {
|
|
495
|
+
super(Order, entityManager);
|
|
496
|
+
}
|
|
432
497
|
|
|
433
498
|
async findOrdersByDateRange(
|
|
434
499
|
startDate?: Date,
|
|
@@ -444,7 +509,7 @@ export class OrderService {
|
|
|
444
509
|
|
|
445
510
|
const skip = getSkip(page, limit);
|
|
446
511
|
|
|
447
|
-
const [orders, total] = await this.
|
|
512
|
+
const [orders, total] = await this.getManyAndCount({
|
|
448
513
|
where: whereConditions,
|
|
449
514
|
skip,
|
|
450
515
|
take: limit,
|
|
@@ -467,7 +532,7 @@ export class OrderService {
|
|
|
467
532
|
async getOrderAnalytics(startDate: Date, endDate: Date) {
|
|
468
533
|
const dateQuery = generateDateQuery('createdAt', startDate, endDate);
|
|
469
534
|
|
|
470
|
-
const queryBuilder = this.
|
|
535
|
+
const queryBuilder = this.repo.createQueryBuilder('order')
|
|
471
536
|
.where(dateQuery);
|
|
472
537
|
|
|
473
538
|
const [
|
|
@@ -509,29 +574,32 @@ export class OrderService {
|
|
|
509
574
|
|
|
510
575
|
```typescript
|
|
511
576
|
import { Injectable } from '@nestjs/common';
|
|
512
|
-
import {
|
|
577
|
+
import { EntityManager } from 'typeorm';
|
|
578
|
+
import { TypeOrmRepository } from '@onivoro/server-typeorm-mysql';
|
|
513
579
|
import { User } from './user.entity';
|
|
514
580
|
import { Order } from './order.entity';
|
|
515
581
|
import { OrderItem } from './order-item.entity';
|
|
516
582
|
|
|
517
583
|
@Injectable()
|
|
518
584
|
export class OrderTransactionService {
|
|
519
|
-
constructor(private
|
|
585
|
+
constructor(private entityManager: EntityManager) {}
|
|
520
586
|
|
|
521
587
|
async createOrderWithItems(
|
|
522
588
|
userId: number,
|
|
523
589
|
orderData: Partial<Order>,
|
|
524
590
|
items: Array<{productId: number, quantity: number, unitPrice: number}>
|
|
525
591
|
): Promise<Order> {
|
|
526
|
-
return this.
|
|
592
|
+
return this.entityManager.transaction(async transactionalEntityManager => {
|
|
593
|
+
const orderRepo = new TypeOrmRepository<Order>(Order, transactionalEntityManager);
|
|
594
|
+
const orderItemRepo = new TypeOrmRepository<OrderItem>(OrderItem, transactionalEntityManager);
|
|
595
|
+
const userRepo = new TypeOrmRepository<User>(User, transactionalEntityManager);
|
|
596
|
+
|
|
527
597
|
// Create the order
|
|
528
|
-
const order =
|
|
598
|
+
const order = await orderRepo.postOne({
|
|
529
599
|
...orderData,
|
|
530
600
|
userId,
|
|
531
601
|
totalAmount: 0 // Will be calculated
|
|
532
602
|
});
|
|
533
|
-
|
|
534
|
-
const savedOrder = await manager.save(order);
|
|
535
603
|
|
|
536
604
|
// Create order items
|
|
537
605
|
let totalAmount = 0;
|
|
@@ -541,27 +609,26 @@ export class OrderTransactionService {
|
|
|
541
609
|
const totalPrice = itemData.quantity * itemData.unitPrice;
|
|
542
610
|
totalAmount += totalPrice;
|
|
543
611
|
|
|
544
|
-
const orderItem =
|
|
545
|
-
orderId:
|
|
612
|
+
const orderItem = await orderItemRepo.postOne({
|
|
613
|
+
orderId: order.id,
|
|
546
614
|
productId: itemData.productId,
|
|
547
615
|
quantity: itemData.quantity,
|
|
548
616
|
unitPrice: itemData.unitPrice,
|
|
549
617
|
totalPrice
|
|
550
618
|
});
|
|
551
619
|
|
|
552
|
-
orderItems.push(
|
|
620
|
+
orderItems.push(orderItem);
|
|
553
621
|
}
|
|
554
622
|
|
|
555
623
|
// Update order total
|
|
556
|
-
|
|
557
|
-
await manager.save(savedOrder);
|
|
624
|
+
await orderRepo.patch({ id: order.id }, { totalAmount });
|
|
558
625
|
|
|
559
626
|
// Update user's last order date
|
|
560
|
-
await
|
|
627
|
+
await userRepo.patch({ id: userId }, {
|
|
561
628
|
updatedAt: new Date()
|
|
562
629
|
});
|
|
563
630
|
|
|
564
|
-
return
|
|
631
|
+
return order;
|
|
565
632
|
});
|
|
566
633
|
}
|
|
567
634
|
|
|
@@ -569,15 +636,17 @@ export class OrderTransactionService {
|
|
|
569
636
|
orderId: number,
|
|
570
637
|
newUserId: number
|
|
571
638
|
): Promise<void> {
|
|
572
|
-
await this.
|
|
639
|
+
await this.entityManager.transaction(async transactionalEntityManager => {
|
|
640
|
+
const orderRepo = new TypeOrmRepository<Order>(Order, transactionalEntityManager);
|
|
641
|
+
|
|
573
642
|
// Update order
|
|
574
|
-
await
|
|
643
|
+
await orderRepo.patch({ id: orderId }, {
|
|
575
644
|
userId: newUserId,
|
|
576
645
|
updatedAt: new Date()
|
|
577
646
|
});
|
|
578
647
|
|
|
579
|
-
// Log the transfer
|
|
580
|
-
await
|
|
648
|
+
// Log the transfer using raw query
|
|
649
|
+
await transactionalEntityManager.query(
|
|
581
650
|
'INSERT INTO order_transfers (order_id, new_user_id, transferred_at) VALUES (?, ?, ?)',
|
|
582
651
|
[orderId, newUserId, new Date()]
|
|
583
652
|
);
|
|
@@ -586,122 +655,95 @@ export class OrderTransactionService {
|
|
|
586
655
|
}
|
|
587
656
|
```
|
|
588
657
|
|
|
589
|
-
###
|
|
658
|
+
### Query Streaming
|
|
590
659
|
|
|
591
660
|
```typescript
|
|
592
661
|
import { Injectable } from '@nestjs/common';
|
|
593
|
-
import {
|
|
594
|
-
import {
|
|
595
|
-
import {
|
|
662
|
+
import { TypeOrmRepository } from '@onivoro/server-typeorm-mysql';
|
|
663
|
+
import { EntityManager, QueryRunner } from 'typeorm';
|
|
664
|
+
import { User } from './user.entity';
|
|
665
|
+
import { createWriteStream } from 'fs';
|
|
596
666
|
|
|
597
667
|
@Injectable()
|
|
598
|
-
export class
|
|
599
|
-
constructor(
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
) {}
|
|
603
|
-
|
|
604
|
-
createBaseQuery(): SelectQueryBuilder<Order> {
|
|
605
|
-
return this.orderRepository
|
|
606
|
-
.createQueryBuilder('order')
|
|
607
|
-
.leftJoinAndSelect('order.user', 'user')
|
|
608
|
-
.leftJoinAndSelect('order.items', 'items')
|
|
609
|
-
.leftJoinAndSelect('items.product', 'product');
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
async findOrdersWithFilters(filters: {
|
|
613
|
-
status?: string[];
|
|
614
|
-
userIds?: number[];
|
|
615
|
-
minAmount?: number;
|
|
616
|
-
maxAmount?: number;
|
|
617
|
-
startDate?: Date;
|
|
618
|
-
endDate?: Date;
|
|
619
|
-
limit?: number;
|
|
620
|
-
offset?: number;
|
|
621
|
-
}) {
|
|
622
|
-
let query = this.createBaseQuery();
|
|
623
|
-
|
|
624
|
-
if (filters.status?.length) {
|
|
625
|
-
query = query.andWhere('order.status IN (:...statuses)', {
|
|
626
|
-
statuses: filters.status
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
if (filters.userIds?.length) {
|
|
631
|
-
query = query.andWhere('order.userId IN (:...userIds)', {
|
|
632
|
-
userIds: filters.userIds
|
|
633
|
-
});
|
|
634
|
-
}
|
|
668
|
+
export class UserStreamingService extends TypeOrmRepository<User> {
|
|
669
|
+
constructor(entityManager: EntityManager) {
|
|
670
|
+
super(User, entityManager);
|
|
671
|
+
}
|
|
635
672
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
673
|
+
async exportUsersToFile(filePath: string): Promise<void> {
|
|
674
|
+
const writeStream = createWriteStream(filePath);
|
|
675
|
+
|
|
676
|
+
writeStream.write('id,email,firstName,lastName,createdAt\n');
|
|
677
|
+
|
|
678
|
+
const { stream, error } = await this.queryStream({
|
|
679
|
+
query: 'SELECT id, email, firstName, lastName, createdAt FROM users WHERE isActive = 1',
|
|
680
|
+
onData: async (stream, record: User, count) => {
|
|
681
|
+
const csvLine = `${record.id},"${record.email}","${record.firstName}","${record.lastName}","${record.createdAt}"\n`;
|
|
682
|
+
writeStream.write(csvLine);
|
|
683
|
+
|
|
684
|
+
if (count % 1000 === 0) {
|
|
685
|
+
console.log(`Processed ${count} records`);
|
|
686
|
+
}
|
|
687
|
+
},
|
|
688
|
+
onError: async (stream, error) => {
|
|
689
|
+
console.error('Stream error:', error);
|
|
690
|
+
writeStream.end();
|
|
691
|
+
},
|
|
692
|
+
onEnd: async (stream, count) => {
|
|
693
|
+
console.log(`Export completed. Total records: ${count}`);
|
|
694
|
+
writeStream.end();
|
|
695
|
+
}
|
|
696
|
+
});
|
|
641
697
|
|
|
642
|
-
if (
|
|
643
|
-
|
|
644
|
-
maxAmount: filters.maxAmount
|
|
645
|
-
});
|
|
698
|
+
if (error) {
|
|
699
|
+
throw new Error(`Failed to start streaming: ${error.message}`);
|
|
646
700
|
}
|
|
701
|
+
}
|
|
647
702
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
703
|
+
async processLargeDataset(): Promise<void> {
|
|
704
|
+
const { stream, error } = await this.queryStream({
|
|
705
|
+
query: 'SELECT * FROM users WHERE createdAt > DATE_SUB(NOW(), INTERVAL 1 YEAR)',
|
|
706
|
+
onData: async (stream, record: User, count) => {
|
|
707
|
+
// Process each record individually
|
|
708
|
+
// This is memory efficient for large datasets
|
|
709
|
+
await this.processUserRecord(record);
|
|
710
|
+
},
|
|
711
|
+
onError: async (stream, error) => {
|
|
712
|
+
console.error('Processing error:', error);
|
|
713
|
+
},
|
|
714
|
+
onEnd: async (stream, count) => {
|
|
715
|
+
console.log(`Processed ${count} user records`);
|
|
716
|
+
}
|
|
717
|
+
});
|
|
653
718
|
|
|
654
|
-
if (
|
|
655
|
-
|
|
656
|
-
endDate: filters.endDate
|
|
657
|
-
});
|
|
719
|
+
if (error) {
|
|
720
|
+
throw new Error(`Failed to process dataset: ${error.message}`);
|
|
658
721
|
}
|
|
722
|
+
}
|
|
659
723
|
|
|
660
|
-
|
|
724
|
+
private async processUserRecord(user: User): Promise<void> {
|
|
725
|
+
// Your custom processing logic here
|
|
726
|
+
console.log(`Processing user: ${user.email}`);
|
|
727
|
+
}
|
|
661
728
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
729
|
+
// Static method usage for custom query runners
|
|
730
|
+
static async streamWithCustomQueryRunner(
|
|
731
|
+
queryRunner: QueryRunner,
|
|
732
|
+
query: string
|
|
733
|
+
): Promise<void> {
|
|
734
|
+
const { stream, error } = await TypeOrmRepository.queryStream(queryRunner, {
|
|
735
|
+
query,
|
|
736
|
+
onData: async (stream, record, count) => {
|
|
737
|
+
console.log(`Record ${count}:`, record);
|
|
738
|
+
},
|
|
739
|
+
onEnd: async (stream, count) => {
|
|
740
|
+
console.log(`Stream completed with ${count} records`);
|
|
741
|
+
}
|
|
742
|
+
});
|
|
665
743
|
|
|
666
|
-
if (
|
|
667
|
-
|
|
744
|
+
if (error) {
|
|
745
|
+
console.error('Stream failed:', error);
|
|
668
746
|
}
|
|
669
|
-
|
|
670
|
-
return query.getManyAndCount();
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
async getOrderSummaryByUser(userId: number) {
|
|
674
|
-
return this.orderRepository
|
|
675
|
-
.createQueryBuilder('order')
|
|
676
|
-
.select([
|
|
677
|
-
'COUNT(order.id) as orderCount',
|
|
678
|
-
'SUM(order.totalAmount) as totalSpent',
|
|
679
|
-
'AVG(order.totalAmount) as averageOrderValue',
|
|
680
|
-
'MAX(order.createdAt) as lastOrderDate',
|
|
681
|
-
'MIN(order.createdAt) as firstOrderDate'
|
|
682
|
-
])
|
|
683
|
-
.where('order.userId = :userId', { userId })
|
|
684
|
-
.andWhere('order.status != :cancelledStatus', { cancelledStatus: 'cancelled' })
|
|
685
|
-
.getRawOne();
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
async getTopCustomers(limit: number = 10) {
|
|
689
|
-
return this.orderRepository
|
|
690
|
-
.createQueryBuilder('order')
|
|
691
|
-
.leftJoin('order.user', 'user')
|
|
692
|
-
.select([
|
|
693
|
-
'user.id as userId',
|
|
694
|
-
'user.firstName as firstName',
|
|
695
|
-
'user.lastName as lastName',
|
|
696
|
-
'user.email as email',
|
|
697
|
-
'COUNT(order.id) as orderCount',
|
|
698
|
-
'SUM(order.totalAmount) as totalSpent'
|
|
699
|
-
])
|
|
700
|
-
.where('order.status != :cancelledStatus', { cancelledStatus: 'cancelled' })
|
|
701
|
-
.groupBy('user.id')
|
|
702
|
-
.orderBy('totalSpent', 'DESC')
|
|
703
|
-
.limit(limit)
|
|
704
|
-
.getRawMany();
|
|
705
747
|
}
|
|
706
748
|
}
|
|
707
749
|
```
|
|
@@ -715,10 +757,32 @@ export class OrderQueryService {
|
|
|
715
757
|
Base repository class with enhanced functionality:
|
|
716
758
|
|
|
717
759
|
```typescript
|
|
718
|
-
export class TypeOrmRepository<T>
|
|
719
|
-
constructor(
|
|
760
|
+
export class TypeOrmRepository<T> {
|
|
761
|
+
constructor(entityType: any, entityManager: EntityManager)
|
|
720
762
|
|
|
721
|
-
//
|
|
763
|
+
// Core CRUD methods
|
|
764
|
+
async getMany(options: FindManyOptions<T>): Promise<T[]>
|
|
765
|
+
async getManyAndCount(options: FindManyOptions<T>): Promise<[T[], number]>
|
|
766
|
+
async getOne(options: FindOneOptions<T>): Promise<T>
|
|
767
|
+
async postOne(body: Partial<T>): Promise<T>
|
|
768
|
+
async postMany(body: Partial<T>[]): Promise<T[]>
|
|
769
|
+
async delete(options: FindOptionsWhere<T>): Promise<void>
|
|
770
|
+
async softDelete(options: FindOptionsWhere<T>): Promise<void>
|
|
771
|
+
async put(options: FindOptionsWhere<T>, body: QueryDeepPartialEntity<T>): Promise<void>
|
|
772
|
+
async patch(options: FindOptionsWhere<T>, body: QueryDeepPartialEntity<T>): Promise<void>
|
|
773
|
+
|
|
774
|
+
// Transaction support
|
|
775
|
+
forTransaction(entityManager: EntityManager): TypeOrmRepository<T>
|
|
776
|
+
|
|
777
|
+
// Streaming support
|
|
778
|
+
async queryStream<TRecord = any>(params: TQueryStreamParams): Promise<{stream: any, error: any}>
|
|
779
|
+
static async queryStream<TRecord = any>(queryRunner: QueryRunner, params: TQueryStreamParams): Promise<{stream: any, error: any}>
|
|
780
|
+
|
|
781
|
+
// Utility methods
|
|
782
|
+
buildWhereILike(filters?: Record<string, any>): FindOptionsWhere<T>
|
|
783
|
+
|
|
784
|
+
// Internal properties
|
|
785
|
+
get repo(): Repository<T>
|
|
722
786
|
}
|
|
723
787
|
```
|
|
724
788
|
|
|
@@ -832,14 +896,27 @@ interface PagedData<T> {
|
|
|
832
896
|
}
|
|
833
897
|
```
|
|
834
898
|
|
|
899
|
+
#### TQueryStreamParams
|
|
900
|
+
|
|
901
|
+
Query streaming parameters:
|
|
902
|
+
|
|
903
|
+
```typescript
|
|
904
|
+
type TQueryStreamParams<TRecord = any> = {
|
|
905
|
+
query: string;
|
|
906
|
+
onData?: (stream: ReadStream, record: TRecord, count: number) => Promise<any | void>;
|
|
907
|
+
onError?: (stream: ReadStream, error: any) => Promise<any | void>;
|
|
908
|
+
onEnd?: (stream: ReadStream, count: number) => Promise<any | void>;
|
|
909
|
+
};
|
|
910
|
+
```
|
|
911
|
+
|
|
835
912
|
## Best Practices
|
|
836
913
|
|
|
837
|
-
1. **Repository Pattern**: Use custom repositories for
|
|
838
|
-
2. **Transactions**: Use
|
|
914
|
+
1. **Repository Pattern**: Use custom repositories extending TypeOrmRepository for domain-specific operations
|
|
915
|
+
2. **Transactions**: Use `forTransaction()` method for multi-table operations
|
|
839
916
|
3. **Indexing**: Add proper indexes for frequently queried columns
|
|
840
|
-
4. **Pagination**: Always implement pagination for list
|
|
841
|
-
5. **
|
|
842
|
-
6. **Error Handling**: Implement proper error handling in repositories
|
|
917
|
+
4. **Pagination**: Always implement pagination using TypeOrmPagingRepository for list operations
|
|
918
|
+
5. **Streaming**: Use `queryStream()` for processing large datasets efficiently
|
|
919
|
+
6. **Error Handling**: Implement proper error handling in repositories and services
|
|
843
920
|
7. **Type Safety**: Leverage TypeScript for type-safe database operations
|
|
844
921
|
8. **Connection Pooling**: Configure appropriate connection pool settings
|
|
845
922
|
|
|
@@ -847,28 +924,53 @@ interface PagedData<T> {
|
|
|
847
924
|
|
|
848
925
|
```typescript
|
|
849
926
|
import { Test } from '@nestjs/testing';
|
|
850
|
-
import {
|
|
851
|
-
import { Repository } from 'typeorm';
|
|
927
|
+
import { EntityManager } from 'typeorm';
|
|
852
928
|
import { User } from './user.entity';
|
|
853
929
|
import { UserService } from './user.service';
|
|
930
|
+
import { AdvancedUserRepository } from './user.repository';
|
|
854
931
|
|
|
855
932
|
describe('UserService', () => {
|
|
856
933
|
let service: UserService;
|
|
857
|
-
let repository:
|
|
934
|
+
let repository: AdvancedUserRepository;
|
|
935
|
+
let entityManager: EntityManager;
|
|
858
936
|
|
|
859
937
|
beforeEach(async () => {
|
|
938
|
+
const mockEntityManager = {
|
|
939
|
+
getRepository: jest.fn().mockReturnValue({
|
|
940
|
+
find: jest.fn(),
|
|
941
|
+
findAndCount: jest.fn(),
|
|
942
|
+
findOne: jest.fn(),
|
|
943
|
+
save: jest.fn(),
|
|
944
|
+
create: jest.fn(),
|
|
945
|
+
update: jest.fn(),
|
|
946
|
+
delete: jest.fn(),
|
|
947
|
+
softDelete: jest.fn(),
|
|
948
|
+
createQueryBuilder: jest.fn().mockReturnValue({
|
|
949
|
+
insert: jest.fn().mockReturnThis(),
|
|
950
|
+
values: jest.fn().mockReturnThis(),
|
|
951
|
+
returning: jest.fn().mockReturnThis(),
|
|
952
|
+
execute: jest.fn()
|
|
953
|
+
})
|
|
954
|
+
})
|
|
955
|
+
};
|
|
956
|
+
|
|
860
957
|
const module = await Test.createTestingModule({
|
|
861
958
|
providers: [
|
|
862
959
|
UserService,
|
|
863
960
|
{
|
|
864
|
-
provide:
|
|
865
|
-
|
|
961
|
+
provide: AdvancedUserRepository,
|
|
962
|
+
useFactory: () => new AdvancedUserRepository(mockEntityManager as any)
|
|
866
963
|
},
|
|
964
|
+
{
|
|
965
|
+
provide: EntityManager,
|
|
966
|
+
useValue: mockEntityManager
|
|
967
|
+
}
|
|
867
968
|
],
|
|
868
969
|
}).compile();
|
|
869
970
|
|
|
870
971
|
service = module.get<UserService>(UserService);
|
|
871
|
-
repository = module.get<
|
|
972
|
+
repository = module.get<AdvancedUserRepository>(AdvancedUserRepository);
|
|
973
|
+
entityManager = module.get<EntityManager>(EntityManager);
|
|
872
974
|
});
|
|
873
975
|
|
|
874
976
|
it('should create a user', async () => {
|
|
@@ -878,11 +980,19 @@ describe('UserService', () => {
|
|
|
878
980
|
lastName: 'Doe'
|
|
879
981
|
};
|
|
880
982
|
|
|
881
|
-
|
|
882
|
-
jest.spyOn(repository, '
|
|
983
|
+
const createdUser = { id: 1, ...userData };
|
|
984
|
+
jest.spyOn(repository, 'postOne').mockResolvedValue(createdUser as User);
|
|
883
985
|
|
|
884
986
|
const result = await service.createUser(userData);
|
|
885
|
-
expect(result).toEqual(
|
|
987
|
+
expect(result).toEqual(createdUser);
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
it('should find user by id', async () => {
|
|
991
|
+
const user = { id: 1, email: 'test@example.com', firstName: 'John', lastName: 'Doe' };
|
|
992
|
+
jest.spyOn(repository, 'getOne').mockResolvedValue(user as User);
|
|
993
|
+
|
|
994
|
+
const result = await service.findUserById(1);
|
|
995
|
+
expect(result).toEqual(user);
|
|
886
996
|
});
|
|
887
997
|
});
|
|
888
998
|
```
|
package/package.json
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onivoro/server-typeorm-mysql",
|
|
3
|
-
"version": "24.0.
|
|
3
|
+
"version": "24.0.2",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"types": "./src/index.d.ts",
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"tslib": "^2.3.0"
|
|
9
|
+
},
|
|
10
|
+
"peerDependencies": {
|
|
11
|
+
"@nestjs/common": "~10.4.6",
|
|
12
|
+
"@nestjs/typeorm": "^11.0.0",
|
|
13
|
+
"mysql": "~2.18.1",
|
|
14
|
+
"reflect-metadata": "~0.2.2",
|
|
15
|
+
"typeorm": "^0.3.22",
|
|
16
|
+
"typeorm-naming-strategies": "~4.1.0"
|
|
9
17
|
}
|
|
10
18
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { applyDecorators } from "@nestjs/common";
|
|
2
2
|
import { Entity } from "typeorm";
|
|
3
|
-
import { snakeCase } from '@onivoro/isomorphic
|
|
3
|
+
import { snakeCase } from '@onivoro/isomorphic-common';
|
|
4
4
|
|
|
5
5
|
export const Table = (EntityClass: { name: string }) => {
|
|
6
6
|
const tableName = snakeCase(EntityClass.name);
|