@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.
- package/README.md +257 -858
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @onivoro/server-typeorm-mysql
|
|
2
2
|
|
|
3
|
-
A
|
|
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
|
-
##
|
|
11
|
+
## Overview
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
18
|
-
- **Query
|
|
19
|
-
- **
|
|
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
|
-
##
|
|
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.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
@
|
|
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'
|
|
70
|
+
@TableColumn({ type: 'varchar' })
|
|
64
71
|
email: string;
|
|
65
72
|
|
|
66
|
-
@TableColumn({ type: 'varchar'
|
|
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'
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
105
|
-
return this.getOne({ where: { email } });
|
|
106
|
-
}
|
|
88
|
+
### TypeOrmRepository
|
|
107
89
|
|
|
108
|
-
|
|
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
|
-
//
|
|
187
|
-
async
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
318
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
376
|
-
shippedAt?: Date;
|
|
182
|
+
### TypeOrmPagingRepository
|
|
377
183
|
|
|
378
|
-
|
|
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
|
|
425
|
-
import {
|
|
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
|
-
|
|
436
|
-
|
|
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
|
|
199
|
+
export class UserPagingRepository extends TypeOrmPagingRepository<User, UserSearchParams> {
|
|
494
200
|
constructor(entityManager: EntityManager) {
|
|
495
|
-
super(
|
|
201
|
+
super(User, entityManager);
|
|
496
202
|
}
|
|
497
203
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
|
209
|
+
const where = this.removeFalseyKeys({
|
|
210
|
+
departmentId: params.departmentId,
|
|
211
|
+
isActive: params.isActive
|
|
212
|
+
});
|
|
511
213
|
|
|
512
|
-
const [
|
|
513
|
-
where
|
|
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
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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
|
-
|
|
847
|
-
|
|
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
|
-
|
|
851
|
-
|
|
852
|
-
Generate date range query conditions:
|
|
266
|
+
## Utility Functions
|
|
853
267
|
|
|
854
268
|
```typescript
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
867
|
-
|
|
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
|
-
|
|
323
|
+
## Type Definitions
|
|
871
324
|
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
Pagination parameters:
|
|
325
|
+
### Core Interfaces
|
|
875
326
|
|
|
876
327
|
```typescript
|
|
877
|
-
|
|
328
|
+
// Page parameters
|
|
329
|
+
interface IPageParams {
|
|
878
330
|
page: number;
|
|
879
331
|
limit: number;
|
|
880
332
|
}
|
|
881
|
-
```
|
|
882
|
-
|
|
883
|
-
#### PagedData<T>
|
|
884
333
|
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
```typescript
|
|
888
|
-
interface PagedData<T> {
|
|
334
|
+
// Paged data result
|
|
335
|
+
interface IPagedData<T> {
|
|
889
336
|
data: T[];
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
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
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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 {
|
|
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
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
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
|
-
|
|
984
|
-
jest.spyOn(repository, 'postOne').mockResolvedValue(createdUser as User);
|
|
377
|
+
## Important Implementation Details
|
|
985
378
|
|
|
986
|
-
|
|
987
|
-
|
|
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
|
-
|
|
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
|
-
|
|
995
|
-
|
|
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
|
-
|
|
401
|
+
MIT
|