@onivoro/server-typeorm-mysql 24.30.12 → 24.30.13

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 (2) hide show
  1. package/README.md +257 -858
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @onivoro/server-typeorm-mysql
2
2
 
3
- A comprehensive TypeORM MySQL integration library for NestJS applications, providing custom repositories, decorators, utilities, and enhanced MySQL-specific functionality for enterprise-scale database operations.
3
+ A TypeORM MySQL integration library providing a NestJS module configuration, enhanced repository patterns, custom decorators, and utility functions for MySQL database operations.
4
4
 
5
5
  ## Installation
6
6
 
@@ -8,42 +8,51 @@ A comprehensive TypeORM MySQL integration library for NestJS applications, provi
8
8
  npm install @onivoro/server-typeorm-mysql
9
9
  ```
10
10
 
11
- ## Features
11
+ ## Overview
12
12
 
13
- - **TypeORM MySQL Module**: Complete NestJS module for MySQL integration
14
- - **Custom Repository Classes**: Enhanced repository patterns with pagination and utilities
15
- - **Custom Decorators**: MySQL-specific column decorators and table definitions
16
- - **Data Source Factory**: Flexible data source configuration and creation
17
- - **Pagination Support**: Built-in pagination utilities and interfaces
18
- - **Query Utilities**: Helper functions for date queries and data manipulation
19
- - **Type Safety**: Full TypeScript support with comprehensive type definitions
20
- - **MySQL Optimizations**: MySQL-specific optimizations and best practices
13
+ This library provides:
14
+ - **NestJS Module**: Dynamic module configuration for TypeORM with MySQL
15
+ - **Enhanced Repository**: `TypeOrmRepository` with additional convenience methods
16
+ - **Paging Repository**: Abstract base class for implementing pagination
17
+ - **Custom Decorators**: Simplified table and column decorators with OpenAPI integration
18
+ - **Query Streaming**: Support for processing large datasets efficiently
19
+ - **Utility Functions**: Helper functions for pagination, date queries, and data manipulation
21
20
 
22
- ## Quick Start
23
-
24
- ### Import the Module
21
+ ## Module Setup
25
22
 
26
23
  ```typescript
27
24
  import { ServerTypeormMysqlModule } from '@onivoro/server-typeorm-mysql';
25
+ import { User, Product } from './entities';
28
26
 
29
27
  @Module({
30
28
  imports: [
31
- ServerTypeormMysqlModule.forRoot({
32
- host: 'localhost',
33
- port: 3306,
34
- username: 'root',
35
- password: 'password',
36
- database: 'myapp',
37
- entities: [User, Product, Order],
38
- synchronize: false,
39
- logging: true
40
- })
41
- ],
29
+ ServerTypeormMysqlModule.configure(
30
+ [UserRepository, ProductRepository], // Injectables
31
+ [User, Product], // Entities
32
+ {
33
+ host: 'localhost',
34
+ port: 3306,
35
+ username: 'root',
36
+ password: 'password',
37
+ database: 'myapp',
38
+ synchronize: false, // Never true in production
39
+ logging: false
40
+ },
41
+ 'default' // Connection name
42
+ )
43
+ ]
42
44
  })
43
45
  export class AppModule {}
44
46
  ```
45
47
 
46
- ### Define Entities with Custom Decorators
48
+ The module:
49
+ - Provides `DataSource` and `EntityManager` for injection
50
+ - Caches data sources by name to prevent duplicate connections
51
+ - Properly cleans up connections on application shutdown
52
+
53
+ ## Entity Definition with Custom Decorators
54
+
55
+ The library provides simplified decorators that combine TypeORM and OpenAPI functionality:
47
56
 
48
57
  ```typescript
49
58
  import {
@@ -52,124 +61,33 @@ import {
52
61
  TableColumn,
53
62
  NullableTableColumn
54
63
  } from '@onivoro/server-typeorm-mysql';
55
- import { Entity } from 'typeorm';
56
64
 
57
- @Entity()
58
- @Table('users')
65
+ @Table({ name: 'users' })
59
66
  export class User {
60
67
  @PrimaryTableColumn()
61
68
  id: number;
62
69
 
63
- @TableColumn({ type: 'varchar', length: 255 })
70
+ @TableColumn({ type: 'varchar' })
64
71
  email: string;
65
72
 
66
- @TableColumn({ type: 'varchar', length: 100 })
73
+ @TableColumn({ type: 'varchar' })
67
74
  firstName: string;
68
75
 
69
- @TableColumn({ type: 'varchar', length: 100 })
70
- lastName: string;
71
-
72
76
  @NullableTableColumn({ type: 'datetime' })
73
77
  lastLoginAt?: Date;
74
78
 
75
- @TableColumn({ type: 'boolean', default: true })
79
+ @TableColumn({ type: 'boolean' })
76
80
  isActive: boolean;
77
-
78
- @TableColumn({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
79
- createdAt: Date;
80
-
81
- @TableColumn({
82
- type: 'timestamp',
83
- default: () => 'CURRENT_TIMESTAMP',
84
- onUpdate: 'CURRENT_TIMESTAMP'
85
- })
86
- updatedAt: Date;
87
81
  }
88
82
  ```
89
83
 
90
- ### Use Custom Repository
84
+ **Important**: These decorators only accept the `type` property from TypeORM's `ColumnOptions`. For full control over column options, use TypeORM's decorators directly.
91
85
 
92
- ```typescript
93
- import { Injectable } from '@nestjs/common';
94
- import { TypeOrmRepository, TypeOrmPagingRepository } from '@onivoro/server-typeorm-mysql';
95
- import { EntityManager } from 'typeorm';
96
- import { User } from './user.entity';
97
-
98
- @Injectable()
99
- export class UserRepository extends TypeOrmPagingRepository<User> {
100
- constructor(entityManager: EntityManager) {
101
- super(User, entityManager);
102
- }
86
+ ## Repository Classes
103
87
 
104
- async findByEmail(email: string): Promise<User | null> {
105
- return this.getOne({ where: { email } });
106
- }
88
+ ### TypeOrmRepository
107
89
 
108
- async findActiveUsers(): Promise<User[]> {
109
- return this.getMany({ where: { isActive: true } });
110
- }
111
-
112
- async findUsersWithPagination(page: number, limit: number) {
113
- return this.findWithPaging(
114
- { where: { isActive: true } },
115
- { page, limit }
116
- );
117
- }
118
- }
119
- ```
120
-
121
- ## Configuration
122
-
123
- ### Data Source Configuration
124
-
125
- ```typescript
126
- import { dataSourceConfigFactory } from '@onivoro/server-typeorm-mysql';
127
-
128
- const config = dataSourceConfigFactory({
129
- host: process.env.DB_HOST,
130
- port: parseInt(process.env.DB_PORT),
131
- username: process.env.DB_USERNAME,
132
- password: process.env.DB_PASSWORD,
133
- database: process.env.DB_DATABASE,
134
- entities: [User, Product, Order],
135
- migrations: ['src/migrations/*.ts'],
136
- synchronize: false,
137
- logging: process.env.NODE_ENV === 'development',
138
- ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
139
- });
140
- ```
141
-
142
- ### Dynamic Module Configuration
143
-
144
- ```typescript
145
- import { Module } from '@nestjs/common';
146
- import { ServerTypeormMysqlModule } from '@onivoro/server-typeorm-mysql';
147
- import { ConfigService } from '@nestjs/config';
148
-
149
- @Module({
150
- imports: [
151
- ServerTypeormMysqlModule.forRootAsync({
152
- useFactory: (configService: ConfigService) => ({
153
- host: configService.get('DATABASE_HOST'),
154
- port: configService.get('DATABASE_PORT'),
155
- username: configService.get('DATABASE_USERNAME'),
156
- password: configService.get('DATABASE_PASSWORD'),
157
- database: configService.get('DATABASE_NAME'),
158
- entities: [__dirname + '/**/*.entity{.ts,.js}'],
159
- migrations: [__dirname + '/migrations/*{.ts,.js}'],
160
- synchronize: configService.get('NODE_ENV') === 'development',
161
- logging: configService.get('DATABASE_LOGGING') === 'true'
162
- }),
163
- inject: [ConfigService]
164
- })
165
- ],
166
- })
167
- export class DatabaseModule {}
168
- ```
169
-
170
- ## Usage Examples
171
-
172
- ### Basic Repository Operations
90
+ Enhanced repository with convenience methods and streaming support:
173
91
 
174
92
  ```typescript
175
93
  import { Injectable } from '@nestjs/common';
@@ -183,724 +101,260 @@ export class UserRepository extends TypeOrmRepository<User> {
183
101
  super(User, entityManager);
184
102
  }
185
103
 
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 });
104
+ // Available methods:
105
+ async findUsers() {
106
+ // getOne - throws if more than one result
107
+ const user = await this.getOne({ where: { id: 1 } });
108
+
109
+ // getMany - returns array
110
+ const activeUsers = await this.getMany({ where: { isActive: true } });
111
+
112
+ // getManyAndCount - returns [items, count]
113
+ const [users, total] = await this.getManyAndCount({
114
+ where: { isActive: true },
115
+ take: 10,
116
+ skip: 0
117
+ });
118
+
119
+ // postOne - save and return (uses save())
120
+ const newUser = await this.postOne({ email: 'test@example.com', firstName: 'Test' });
121
+
122
+ // postMany - bulk save and return (uses save())
123
+ const newUsers = await this.postMany([
124
+ { email: 'user1@example.com', firstName: 'User1' },
125
+ { email: 'user2@example.com', firstName: 'User2' }
126
+ ]);
127
+
128
+ // patch - update using TypeORM's update()
129
+ await this.patch({ id: 1 }, { isActive: false });
130
+
131
+ // put - update using TypeORM's save()
132
+ await this.put({ id: 1 }, { isActive: false });
133
+
134
+ // delete - hard delete
135
+ await this.delete({ id: 1 });
136
+
137
+ // softDelete - soft delete
138
+ await this.softDelete({ id: 1 });
229
139
  }
230
- }
231
- ```
232
140
 
233
- ### Advanced Repository Usage with Pagination
234
-
235
- ```typescript
236
- import { Injectable } from '@nestjs/common';
237
- import { TypeOrmPagingRepository, PageParams, PagedData } from '@onivoro/server-typeorm-mysql';
238
- import { EntityManager, Like, Between } from 'typeorm';
239
- import { User } from './user.entity';
240
-
241
- @Injectable()
242
- export class AdvancedUserRepository extends TypeOrmPagingRepository<User> {
243
- constructor(entityManager: EntityManager) {
244
- super(User, entityManager);
141
+ // Transaction support
142
+ async updateInTransaction(userId: number, data: Partial<User>, entityManager: EntityManager) {
143
+ const txRepo = this.forTransaction(entityManager);
144
+ await txRepo.patch({ id: userId }, data);
245
145
  }
246
146
 
247
- async searchUsers(
248
- searchTerm: string,
249
- pageParams: PageParams
250
- ): Promise<PagedData<User>> {
251
- const whereConditions = this.buildWhereILike({
252
- firstName: searchTerm,
253
- lastName: searchTerm,
254
- email: searchTerm
147
+ // ILike helper for case-insensitive search
148
+ async searchUsers(term: string) {
149
+ const filters = this.buildWhereILike({
150
+ firstName: term,
151
+ email: term
255
152
  });
256
-
257
- return this.findWithPaging(
258
- {
259
- where: [
260
- { firstName: Like(`%${searchTerm}%`) },
261
- { lastName: Like(`%${searchTerm}%`) },
262
- { email: Like(`%${searchTerm}%`) }
263
- ],
264
- order: { createdAt: 'DESC' }
265
- },
266
- pageParams
267
- );
153
+ return this.getMany({ where: filters });
268
154
  }
269
-
270
- async findUsersByDateRange(
271
- startDate: Date,
272
- endDate: Date,
273
- pageParams: PageParams
274
- ): Promise<PagedData<User>> {
275
- return this.findWithPaging(
276
- {
277
- where: {
278
- createdAt: Between(startDate, endDate)
279
- },
280
- order: { createdAt: 'DESC' }
155
+
156
+ // Query streaming for large datasets
157
+ async exportUsers() {
158
+ const { stream, error } = await this.queryStream({
159
+ query: 'SELECT * FROM users WHERE isActive = 1',
160
+ onData: async (stream, user, count) => {
161
+ console.log(`Processing user ${count}: ${user.email}`);
281
162
  },
282
- pageParams
283
- );
284
- }
285
-
286
- async findRecentlyActiveUsers(days: number = 30): Promise<User[]> {
287
- const cutoffDate = new Date();
288
- cutoffDate.setDate(cutoffDate.getDate() - days);
289
-
290
- return this.getMany({
291
- where: {
292
- lastLoginAt: Between(cutoffDate, new Date())
163
+ onError: async (stream, error) => {
164
+ console.error('Stream error:', error);
293
165
  },
294
- order: { lastLoginAt: 'DESC' }
295
- });
296
- }
297
-
298
- async getUserStatistics(): Promise<{
299
- total: number;
300
- active: number;
301
- inactive: number;
302
- recentlyRegistered: number;
303
- }> {
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
- )
166
+ onEnd: async (stream, count) => {
167
+ console.log(`Processed ${count} users`);
314
168
  }
315
169
  });
316
-
317
- return {
318
- total: totalCount,
319
- active: activeCount,
320
- inactive: totalCount - activeCount,
321
- recentlyRegistered: recentCount
322
- };
323
- }
324
-
325
- async bulkUpdateUsers(
326
- userIds: number[],
327
- updateData: Partial<User>
328
- ): Promise<void> {
329
- for (const id of userIds) {
330
- await this.patch({ id }, updateData);
331
- }
332
- }
333
-
334
- async softDeleteUsers(userIds: number[]): Promise<void> {
335
- for (const id of userIds) {
336
- await this.softDelete({ id });
170
+
171
+ if (error) {
172
+ throw error;
337
173
  }
338
174
  }
339
175
  }
340
176
  ```
341
177
 
342
- ### Complex Entity Relationships
343
-
344
- ```typescript
345
- import {
346
- Table,
347
- PrimaryTableColumn,
348
- TableColumn,
349
- NullableTableColumn,
350
- ManyToOneRelationOptions
351
- } from '@onivoro/server-typeorm-mysql';
352
- import { Entity, ManyToOne, OneToMany, JoinColumn } from 'typeorm';
353
-
354
- @Entity()
355
- @Table('orders')
356
- export class Order {
357
- @PrimaryTableColumn()
358
- id: number;
359
-
360
- @TableColumn({ type: 'varchar', length: 50 })
361
- orderNumber: string;
362
-
363
- @TableColumn({ type: 'decimal', precision: 10, scale: 2 })
364
- totalAmount: number;
365
-
366
- @TableColumn({ type: 'enum', enum: ['pending', 'processing', 'shipped', 'delivered', 'cancelled'] })
367
- status: string;
368
-
369
- @TableColumn({ type: 'int' })
370
- userId: number;
371
-
372
- @TableColumn({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
373
- createdAt: Date;
178
+ The repository provides access to:
179
+ - `repo` - The underlying TypeORM repository
180
+ - `entityManager` - The EntityManager instance
374
181
 
375
- @NullableTableColumn({ type: 'timestamp' })
376
- shippedAt?: Date;
182
+ ### TypeOrmPagingRepository
377
183
 
378
- @NullableTableColumn({ type: 'timestamp' })
379
- deliveredAt?: Date;
380
-
381
- // Relationships
382
- @ManyToOne(() => User, user => user.orders, ManyToOneRelationOptions)
383
- @JoinColumn({ name: 'userId' })
384
- user: User;
385
-
386
- @OneToMany(() => OrderItem, orderItem => orderItem.order)
387
- items: OrderItem[];
388
- }
389
-
390
- @Entity()
391
- @Table('order_items')
392
- export class OrderItem {
393
- @PrimaryTableColumn()
394
- id: number;
395
-
396
- @TableColumn({ type: 'int' })
397
- orderId: number;
398
-
399
- @TableColumn({ type: 'int' })
400
- productId: number;
401
-
402
- @TableColumn({ type: 'int' })
403
- quantity: number;
404
-
405
- @TableColumn({ type: 'decimal', precision: 10, scale: 2 })
406
- unitPrice: number;
407
-
408
- @TableColumn({ type: 'decimal', precision: 10, scale: 2 })
409
- totalPrice: number;
410
-
411
- @ManyToOne(() => Order, order => order.items, ManyToOneRelationOptions)
412
- @JoinColumn({ name: 'orderId' })
413
- order: Order;
414
-
415
- @ManyToOne(() => Product, product => product.orderItems, ManyToOneRelationOptions)
416
- @JoinColumn({ name: 'productId' })
417
- product: Product;
418
- }
419
- ```
420
-
421
- ### Service Layer with Repository
184
+ Abstract base class requiring implementation of `getPage`:
422
185
 
423
186
  ```typescript
424
- import { Injectable, NotFoundException } from '@nestjs/common';
425
- import { AdvancedUserRepository } from './user.repository';
187
+ import { Injectable } from '@nestjs/common';
188
+ import { TypeOrmPagingRepository, IPageParams, IPagedData } from '@onivoro/server-typeorm-mysql';
189
+ import { EntityManager } from 'typeorm';
426
190
  import { User } from './user.entity';
427
- import { PageParams, PagedData } from '@onivoro/server-typeorm-mysql';
428
-
429
- @Injectable()
430
- export class UserService {
431
- constructor(
432
- private userRepository: AdvancedUserRepository
433
- ) {}
434
191
 
435
- async createUser(userData: Partial<User>): Promise<User> {
436
- return this.userRepository.postOne(userData);
437
- }
438
-
439
- async findUserById(id: number): Promise<User> {
440
- const user = await this.userRepository.getOne({ where: { id } });
441
- if (!user) {
442
- throw new NotFoundException(`User with ID ${id} not found`);
443
- }
444
- return user;
445
- }
446
-
447
- async updateUser(id: number, updateData: Partial<User>): Promise<void> {
448
- const user = await this.findUserById(id);
449
- await this.userRepository.patch({ id }, updateData);
450
- }
451
-
452
- async deleteUser(id: number): Promise<void> {
453
- const user = await this.findUserById(id);
454
- await this.userRepository.softDelete({ id });
455
- }
456
-
457
- async searchUsers(
458
- searchTerm: string,
459
- pageParams: PageParams
460
- ): Promise<PagedData<User>> {
461
- return this.userRepository.searchUsers(searchTerm, pageParams);
462
- }
463
-
464
- async getUserStatistics() {
465
- return this.userRepository.getUserStatistics();
466
- }
467
-
468
- async getRecentlyActiveUsers(days: number = 30): Promise<User[]> {
469
- return this.userRepository.findRecentlyActiveUsers(days);
470
- }
471
-
472
- async bulkUpdateUsers(userIds: number[], updateData: Partial<User>): Promise<void> {
473
- await this.userRepository.bulkUpdateUsers(userIds, updateData);
474
- }
192
+ interface UserSearchParams {
193
+ isActive?: boolean;
194
+ departmentId?: number;
195
+ search?: string;
475
196
  }
476
- ```
477
-
478
- ### Query Utilities Usage
479
-
480
- ```typescript
481
- import { Injectable } from '@nestjs/common';
482
- import {
483
- generateDateQuery,
484
- removeFalseyKeys,
485
- getSkip,
486
- getPagingKey,
487
- TypeOrmRepository
488
- } from '@onivoro/server-typeorm-mysql';
489
- import { EntityManager } from 'typeorm';
490
- import { Order } from './order.entity';
491
197
 
492
198
  @Injectable()
493
- export class OrderService extends TypeOrmRepository<Order> {
199
+ export class UserPagingRepository extends TypeOrmPagingRepository<User, UserSearchParams> {
494
200
  constructor(entityManager: EntityManager) {
495
- super(Order, entityManager);
201
+ super(User, entityManager);
496
202
  }
497
203
 
498
- async findOrdersByDateRange(
499
- startDate?: Date,
500
- endDate?: Date,
501
- status?: string,
502
- page: number = 1,
503
- limit: number = 10
504
- ) {
505
- const whereConditions: any = removeFalseyKeys({
506
- status,
507
- ...generateDateQuery('createdAt', startDate, endDate)
508
- });
204
+ // Must implement this abstract method
205
+ async getPage(pageParams: IPageParams, params: UserSearchParams): Promise<IPagedData<User>> {
206
+ const { page, limit } = pageParams;
207
+ const skip = this.getSkip(page, limit);
509
208
 
510
- const skip = getSkip(page, limit);
209
+ const where = this.removeFalseyKeys({
210
+ departmentId: params.departmentId,
211
+ isActive: params.isActive
212
+ });
511
213
 
512
- const [orders, total] = await this.getManyAndCount({
513
- where: whereConditions,
214
+ const [data, total] = await this.getManyAndCount({
215
+ where,
514
216
  skip,
515
217
  take: limit,
516
- order: { createdAt: 'DESC' },
517
- relations: ['user', 'items', 'items.product']
218
+ order: { createdAt: 'DESC' }
518
219
  });
519
220
 
520
221
  return {
521
- data: orders,
522
- pagination: {
523
- page,
524
- limit,
525
- total,
526
- pages: Math.ceil(total / limit),
527
- key: getPagingKey(page, limit)
528
- }
529
- };
530
- }
531
-
532
- async getOrderAnalytics(startDate: Date, endDate: Date) {
533
- const dateQuery = generateDateQuery('createdAt', startDate, endDate);
534
-
535
- const queryBuilder = this.repo.createQueryBuilder('order')
536
- .where(dateQuery);
537
-
538
- const [
539
- totalOrders,
540
- totalRevenue,
541
- averageOrderValue,
542
- statusBreakdown
543
- ] = await Promise.all([
544
- queryBuilder.getCount(),
545
- queryBuilder
546
- .select('SUM(order.totalAmount)', 'total')
547
- .getRawOne()
548
- .then(result => result.total || 0),
549
- queryBuilder
550
- .select('AVG(order.totalAmount)', 'average')
551
- .getRawOne()
552
- .then(result => result.average || 0),
553
- queryBuilder
554
- .select('order.status', 'status')
555
- .addSelect('COUNT(*)', 'count')
556
- .groupBy('order.status')
557
- .getRawMany()
558
- ]);
559
-
560
- return {
561
- totalOrders,
562
- totalRevenue: parseFloat(totalRevenue),
563
- averageOrderValue: parseFloat(averageOrderValue),
564
- statusBreakdown: statusBreakdown.reduce((acc, item) => {
565
- acc[item.status] = parseInt(item.count);
566
- return acc;
567
- }, {})
222
+ data,
223
+ total,
224
+ page,
225
+ limit,
226
+ totalPages: Math.ceil(total / limit),
227
+ hasNext: page < Math.ceil(total / limit),
228
+ hasPrev: page > 1
568
229
  };
569
230
  }
570
231
  }
571
232
  ```
572
233
 
573
- ### Database Transactions
234
+ Inherited helper methods:
235
+ - `getSkip(page, limit)` - Calculate skip value
236
+ - `getPagingKey(page, limit)` - Generate cache key
237
+ - `removeFalseyKeys(obj)` - Remove null/undefined/empty values
574
238
 
575
- ```typescript
576
- import { Injectable } from '@nestjs/common';
577
- import { EntityManager } from 'typeorm';
578
- import { TypeOrmRepository } from '@onivoro/server-typeorm-mysql';
579
- import { User } from './user.entity';
580
- import { Order } from './order.entity';
581
- import { OrderItem } from './order-item.entity';
582
-
583
- @Injectable()
584
- export class OrderTransactionService {
585
- constructor(private entityManager: EntityManager) {}
586
-
587
- async createOrderWithItems(
588
- userId: number,
589
- orderData: Partial<Order>,
590
- items: Array<{productId: number, quantity: number, unitPrice: number}>
591
- ): Promise<Order> {
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
-
597
- // Create the order
598
- const order = await orderRepo.postOne({
599
- ...orderData,
600
- userId,
601
- totalAmount: 0 // Will be calculated
602
- });
603
-
604
- // Create order items
605
- let totalAmount = 0;
606
- const orderItems = [];
607
-
608
- for (const itemData of items) {
609
- const totalPrice = itemData.quantity * itemData.unitPrice;
610
- totalAmount += totalPrice;
611
-
612
- const orderItem = await orderItemRepo.postOne({
613
- orderId: order.id,
614
- productId: itemData.productId,
615
- quantity: itemData.quantity,
616
- unitPrice: itemData.unitPrice,
617
- totalPrice
618
- });
619
-
620
- orderItems.push(orderItem);
621
- }
622
-
623
- // Update order total
624
- await orderRepo.patch({ id: order.id }, { totalAmount });
625
-
626
- // Update user's last order date
627
- await userRepo.patch({ id: userId }, {
628
- updatedAt: new Date()
629
- });
630
-
631
- return order;
632
- });
633
- }
239
+ ## Query Streaming
634
240
 
635
- async transferOrderToNewUser(
636
- orderId: number,
637
- newUserId: number
638
- ): Promise<void> {
639
- await this.entityManager.transaction(async transactionalEntityManager => {
640
- const orderRepo = new TypeOrmRepository<Order>(Order, transactionalEntityManager);
641
-
642
- // Update order
643
- await orderRepo.patch({ id: orderId }, {
644
- userId: newUserId,
645
- updatedAt: new Date()
646
- });
647
-
648
- // Log the transfer using raw query
649
- await transactionalEntityManager.query(
650
- 'INSERT INTO order_transfers (order_id, new_user_id, transferred_at) VALUES (?, ?, ?)',
651
- [orderId, newUserId, new Date()]
652
- );
653
- });
654
- }
655
- }
656
- ```
657
-
658
- ### Query Streaming
241
+ The library supports efficient processing of large datasets using Node.js streams:
659
242
 
660
243
  ```typescript
661
- import { Injectable } from '@nestjs/common';
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';
666
-
667
- @Injectable()
668
- export class UserStreamingService extends TypeOrmRepository<User> {
669
- constructor(entityManager: EntityManager) {
670
- super(User, entityManager);
671
- }
672
-
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
- });
697
-
698
- if (error) {
699
- throw new Error(`Failed to start streaming: ${error.message}`);
700
- }
701
- }
702
-
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
- });
718
-
719
- if (error) {
720
- throw new Error(`Failed to process dataset: ${error.message}`);
721
- }
722
- }
723
-
724
- private async processUserRecord(user: User): Promise<void> {
725
- // Your custom processing logic here
726
- console.log(`Processing user: ${user.email}`);
727
- }
728
-
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
- });
743
-
744
- if (error) {
745
- console.error('Stream failed:', error);
746
- }
244
+ // Instance method on repository
245
+ const { stream, error } = await repository.queryStream({
246
+ query: 'SELECT * FROM large_table',
247
+ onData: async (stream, record, count) => {
248
+ // Process each record
249
+ },
250
+ onError: async (stream, error) => {
251
+ // Handle errors
252
+ },
253
+ onEnd: async (stream, totalCount) => {
254
+ // Cleanup after processing
747
255
  }
748
- }
749
- ```
750
-
751
- ## API Reference
752
-
753
- ### Repository Classes
754
-
755
- #### TypeOrmRepository<T>
756
-
757
- Base repository class with enhanced functionality:
758
-
759
- ```typescript
760
- export class TypeOrmRepository<T> {
761
- constructor(entityType: any, entityManager: EntityManager)
762
-
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>
786
- }
787
- ```
788
-
789
- #### TypeOrmPagingRepository<T>
790
-
791
- Repository with built-in pagination support:
792
-
793
- ```typescript
794
- export class TypeOrmPagingRepository<T> extends TypeOrmRepository<T> {
795
- async findWithPaging(
796
- options: FindManyOptions<T>,
797
- pageParams: PageParams
798
- ): Promise<PagedData<T>>
799
- }
800
- ```
801
-
802
- ### Decorators
803
-
804
- #### @Table(name?: string)
805
-
806
- Enhanced table decorator:
807
-
808
- ```typescript
809
- @Table('table_name')
810
- export class Entity {}
811
- ```
812
-
813
- #### @PrimaryTableColumn(options?)
814
-
815
- Primary key column decorator:
816
-
817
- ```typescript
818
- @PrimaryTableColumn()
819
- id: number;
820
- ```
821
-
822
- #### @TableColumn(options)
823
-
824
- Standard column decorator:
825
-
826
- ```typescript
827
- @TableColumn({ type: 'varchar', length: 255 })
828
- name: string;
829
- ```
830
-
831
- #### @NullableTableColumn(options)
832
-
833
- Nullable column decorator:
834
-
835
- ```typescript
836
- @NullableTableColumn({ type: 'datetime' })
837
- deletedAt?: Date;
838
- ```
839
-
840
- ### Utility Functions
841
-
842
- #### dataSourceConfigFactory(options)
843
-
844
- Create data source configuration:
256
+ });
845
257
 
846
- ```typescript
847
- function dataSourceConfigFactory(options: DataSourceOptions): DataSourceOptions
258
+ // Static method with custom QueryRunner
259
+ const queryRunner = dataSource.createQueryRunner();
260
+ const { stream, error } = await TypeOrmRepository.queryStream(queryRunner, {
261
+ query: 'SELECT * FROM another_table',
262
+ // ... callbacks
263
+ });
848
264
  ```
849
265
 
850
- #### generateDateQuery(field, startDate?, endDate?)
851
-
852
- Generate date range query conditions:
266
+ ## Utility Functions
853
267
 
854
268
  ```typescript
855
- function generateDateQuery(
856
- field: string,
857
- startDate?: Date,
858
- endDate?: Date
859
- ): Record<string, any>
860
- ```
269
+ import {
270
+ getSkip,
271
+ getPagingKey,
272
+ removeFalseyKeys,
273
+ generateDateQuery,
274
+ getApiTypeFromColumn,
275
+ dataSourceFactory,
276
+ dataSourceConfigFactory
277
+ } from '@onivoro/server-typeorm-mysql';
861
278
 
862
- #### removeFalseyKeys(object)
279
+ // Pagination helpers
280
+ const skip = getSkip(2, 20); // page 2, limit 20 = skip 20
281
+ const cacheKey = getPagingKey(2, 20); // "page_2_limit_20"
863
282
 
864
- Remove falsy values from object:
283
+ // Remove null/undefined/empty string values
284
+ const clean = removeFalseyKeys({
285
+ name: 'John',
286
+ age: null, // removed
287
+ email: '', // removed
288
+ active: false // kept
289
+ });
865
290
 
866
- ```typescript
867
- function removeFalseyKeys<T>(obj: T): Partial<T>
291
+ // Date range query builder
292
+ const dateFilter = generateDateQuery('created_at',
293
+ new Date('2024-01-01'),
294
+ new Date('2024-12-31')
295
+ );
296
+ // Returns TypeORM Between operator or MoreThanOrEqual/LessThanOrEqual
297
+
298
+ // Column type to API type mapping
299
+ const apiType = getApiTypeFromColumn('varchar'); // 'string'
300
+ const apiType2 = getApiTypeFromColumn('int'); // 'number'
301
+ const apiType3 = getApiTypeFromColumn('boolean'); // 'boolean'
302
+
303
+ // Create data source
304
+ const ds = dataSourceFactory('main', {
305
+ host: 'localhost',
306
+ port: 3306,
307
+ username: 'user',
308
+ password: 'pass',
309
+ database: 'db'
310
+ }, [User, Product]);
311
+
312
+ // Create data source config
313
+ const config = dataSourceConfigFactory({
314
+ host: 'localhost',
315
+ port: 3306,
316
+ username: 'user',
317
+ password: 'pass',
318
+ database: 'db',
319
+ entities: [User, Product]
320
+ });
868
321
  ```
869
322
 
870
- ### Type Definitions
323
+ ## Type Definitions
871
324
 
872
- #### PageParams
873
-
874
- Pagination parameters:
325
+ ### Core Interfaces
875
326
 
876
327
  ```typescript
877
- interface PageParams {
328
+ // Page parameters
329
+ interface IPageParams {
878
330
  page: number;
879
331
  limit: number;
880
332
  }
881
- ```
882
-
883
- #### PagedData<T>
884
333
 
885
- Paginated response data:
886
-
887
- ```typescript
888
- interface PagedData<T> {
334
+ // Paged data result
335
+ interface IPagedData<T> {
889
336
  data: T[];
890
- pagination: {
891
- page: number;
892
- limit: number;
893
- total: number;
894
- pages: number;
895
- };
337
+ total: number;
338
+ page: number;
339
+ limit: number;
340
+ totalPages: number;
341
+ hasNext: boolean;
342
+ hasPrev: boolean;
896
343
  }
897
- ```
898
344
 
899
- #### TQueryStreamParams
900
-
901
- Query streaming parameters:
345
+ // Data source options
346
+ interface IDataSourceOptions {
347
+ host: string;
348
+ port: number;
349
+ username: string;
350
+ password: string;
351
+ database: string;
352
+ synchronize?: boolean;
353
+ logging?: boolean;
354
+ [key: string]: any;
355
+ }
902
356
 
903
- ```typescript
357
+ // Query stream parameters
904
358
  type TQueryStreamParams<TRecord = any> = {
905
359
  query: string;
906
360
  onData?: (stream: ReadStream, record: TRecord, count: number) => Promise<any | void>;
@@ -909,94 +363,39 @@ type TQueryStreamParams<TRecord = any> = {
909
363
  };
910
364
  ```
911
365
 
912
- ## Best Practices
913
-
914
- 1. **Repository Pattern**: Use custom repositories extending TypeOrmRepository for domain-specific operations
915
- 2. **Transactions**: Use `forTransaction()` method for multi-table operations
916
- 3. **Indexing**: Add proper indexes for frequently queried columns
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
920
- 7. **Type Safety**: Leverage TypeScript for type-safe database operations
921
- 8. **Connection Pooling**: Configure appropriate connection pool settings
922
-
923
- ## Testing
366
+ ## Constants
924
367
 
925
368
  ```typescript
926
- import { Test } from '@nestjs/testing';
927
- import { EntityManager } from 'typeorm';
928
- import { User } from './user.entity';
929
- import { UserService } from './user.service';
930
- import { AdvancedUserRepository } from './user.repository';
931
-
932
- describe('UserService', () => {
933
- let service: UserService;
934
- let repository: AdvancedUserRepository;
935
- let entityManager: EntityManager;
936
-
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
- };
369
+ import { ManyToOneRelationOptions } from '@onivoro/server-typeorm-mysql';
956
370
 
957
- const module = await Test.createTestingModule({
958
- providers: [
959
- UserService,
960
- {
961
- provide: AdvancedUserRepository,
962
- useFactory: () => new AdvancedUserRepository(mockEntityManager as any)
963
- },
964
- {
965
- provide: EntityManager,
966
- useValue: mockEntityManager
967
- }
968
- ],
969
- }).compile();
970
-
971
- service = module.get<UserService>(UserService);
972
- repository = module.get<AdvancedUserRepository>(AdvancedUserRepository);
973
- entityManager = module.get<EntityManager>(EntityManager);
974
- });
975
-
976
- it('should create a user', async () => {
977
- const userData = {
978
- email: 'test@example.com',
979
- firstName: 'John',
980
- lastName: 'Doe'
981
- };
371
+ // Predefined relation options for ManyToOne relationships
372
+ // { eager: false, cascade: false, nullable: false, onDelete: 'RESTRICT' }
373
+ @ManyToOne(() => User, user => user.orders, ManyToOneRelationOptions)
374
+ user: User;
375
+ ```
982
376
 
983
- const createdUser = { id: 1, ...userData };
984
- jest.spyOn(repository, 'postOne').mockResolvedValue(createdUser as User);
377
+ ## Important Implementation Details
985
378
 
986
- const result = await service.createUser(userData);
987
- expect(result).toEqual(createdUser);
988
- });
379
+ 1. **Module Caching**: Data sources are cached by name to prevent duplicate connections
380
+ 2. **Repository Methods**:
381
+ - `postOne` and `postMany` use TypeORM's `save()` method
382
+ - `patch` uses TypeORM's `update()`
383
+ - `put` uses TypeORM's `save()`
384
+ - `getOne` throws error if multiple results found
385
+ 3. **Transaction Support**: The `forTransaction` method returns a shallow copy with new EntityManager
386
+ 4. **Streaming**: Requires manual QueryRunner management for custom use cases
387
+ 5. **Custom Decorators**: Only accept `type` property - use TypeORM decorators for full control
989
388
 
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);
389
+ ## Differences from typeorm-postgres
993
390
 
994
- const result = await service.findUserById(1);
995
- expect(result).toEqual(user);
996
- });
997
- });
998
- ```
391
+ This MySQL library differs from the PostgreSQL version in several ways:
392
+ - No SQL writer utilities (DDL generation)
393
+ - No migration base classes
394
+ - No specialized Redshift repository
395
+ - No metadata-based repository building
396
+ - Simpler repository implementation using TypeORM's save() instead of custom SQL
397
+ - Built-in streaming support for large datasets
999
398
 
1000
399
  ## License
1001
400
 
1002
- This library is licensed under the MIT License. See the LICENSE file in this package for details.
401
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onivoro/server-typeorm-mysql",
3
- "version": "24.30.12",
3
+ "version": "24.30.13",
4
4
  "license": "MIT",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",