@onivoro/server-typeorm-mysql 22.0.3 → 24.0.0
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 +892 -0
- package/{dist/cjs/index.d.ts → index.ts} +6 -1
- package/jest.config.ts +11 -0
- package/package.json +8 -47
- package/project.json +23 -0
- package/{dist/types/index.d.ts → src/index.ts} +6 -1
- package/src/lib/classes/__snapshots__/sql-writer.class.spec.ts.snap +8 -0
- package/src/lib/classes/type-orm-paging-repository.class.ts +22 -0
- package/src/lib/classes/type-orm-repository.class.ts +152 -0
- package/src/lib/constants/many-to-one-relation-options.constant.ts +3 -0
- package/src/lib/decorators/nullable-table-column.decorator.ts +9 -0
- package/src/lib/decorators/primary-table-column.decorator.ts +9 -0
- package/src/lib/decorators/table-column.decorator.ts +9 -0
- package/src/lib/decorators/table.decorator.ts +8 -0
- package/src/lib/functions/data-source-config-factory.function.ts +33 -0
- package/src/lib/functions/data-source-factory.function.ts +10 -0
- package/src/lib/functions/generate-date-query.function.ts +20 -0
- package/src/lib/functions/get-api-type-from-column.function.ts +14 -0
- package/src/lib/functions/get-paging-key.function.ts +3 -0
- package/src/lib/functions/get-skip.function.ts +3 -0
- package/src/lib/functions/remove-falsey-keys.function.ts +8 -0
- package/src/lib/server-typeorm-mysql.module.ts +59 -0
- package/src/lib/types/data-source-options.interface.ts +10 -0
- package/{dist/cjs/lib/types/entity-provider.interface.d.ts → src/lib/types/entity-provider.interface.ts} +1 -1
- package/{dist/types/lib/types/page-params.interface.d.ts → src/lib/types/page-params.interface.ts} +2 -0
- package/tsconfig.json +16 -0
- package/tsconfig.lib.json +8 -0
- package/tsconfig.spec.json +21 -0
- package/dist/cjs/index.js +0 -35
- package/dist/cjs/lib/classes/type-orm-paging-repository.class.d.ts +0 -14
- package/dist/cjs/lib/classes/type-orm-paging-repository.class.js +0 -16
- package/dist/cjs/lib/classes/type-orm-repository.class.d.ts +0 -21
- package/dist/cjs/lib/classes/type-orm-repository.class.js +0 -62
- package/dist/cjs/lib/constants/many-to-one-relation-options.constant.d.ts +0 -2
- package/dist/cjs/lib/constants/many-to-one-relation-options.constant.js +0 -4
- package/dist/cjs/lib/decorators/nullable-table-column.decorator.d.ts +0 -2
- package/dist/cjs/lib/decorators/nullable-table-column.decorator.js +0 -12
- package/dist/cjs/lib/decorators/primary-table-column.decorator.d.ts +0 -2
- package/dist/cjs/lib/decorators/primary-table-column.decorator.js +0 -12
- package/dist/cjs/lib/decorators/table-column.decorator.d.ts +0 -2
- package/dist/cjs/lib/decorators/table-column.decorator.js +0 -12
- package/dist/cjs/lib/decorators/table.decorator.d.ts +0 -3
- package/dist/cjs/lib/decorators/table.decorator.js +0 -11
- package/dist/cjs/lib/functions/data-source-config-factory.function.d.ts +0 -3
- package/dist/cjs/lib/functions/data-source-config-factory.function.js +0 -24
- package/dist/cjs/lib/functions/data-source-factory.function.d.ts +0 -3
- package/dist/cjs/lib/functions/data-source-factory.function.js +0 -7
- package/dist/cjs/lib/functions/generate-date-query.function.d.ts +0 -2
- package/dist/cjs/lib/functions/generate-date-query.function.js +0 -16
- package/dist/cjs/lib/functions/get-api-type-from-column.function.d.ts +0 -2
- package/dist/cjs/lib/functions/get-api-type-from-column.function.js +0 -14
- package/dist/cjs/lib/functions/get-paging-key.function.d.ts +0 -1
- package/dist/cjs/lib/functions/get-paging-key.function.js +0 -6
- package/dist/cjs/lib/functions/get-skip.function.d.ts +0 -1
- package/dist/cjs/lib/functions/get-skip.function.js +0 -6
- package/dist/cjs/lib/functions/remove-falsey-keys.function.d.ts +0 -1
- package/dist/cjs/lib/functions/remove-falsey-keys.function.js +0 -11
- package/dist/cjs/lib/server-typeorm-mysql.module.d.ts +0 -6
- package/dist/cjs/lib/server-typeorm-mysql.module.js +0 -63
- package/dist/cjs/lib/types/data-source-options.interface.d.ts +0 -10
- package/dist/cjs/lib/types/data-source-options.interface.js +0 -2
- package/dist/cjs/lib/types/entity-provider.interface.js +0 -2
- package/dist/cjs/lib/types/page-params.interface.d.ts +0 -4
- package/dist/cjs/lib/types/page-params.interface.js +0 -2
- package/dist/cjs/lib/types/paged-data.interface.js +0 -2
- package/dist/esm/index.d.ts +0 -19
- package/dist/esm/index.js +0 -35
- package/dist/esm/lib/classes/type-orm-paging-repository.class.d.ts +0 -14
- package/dist/esm/lib/classes/type-orm-paging-repository.class.js +0 -16
- package/dist/esm/lib/classes/type-orm-repository.class.d.ts +0 -21
- package/dist/esm/lib/classes/type-orm-repository.class.js +0 -62
- package/dist/esm/lib/constants/many-to-one-relation-options.constant.d.ts +0 -2
- package/dist/esm/lib/constants/many-to-one-relation-options.constant.js +0 -4
- package/dist/esm/lib/decorators/nullable-table-column.decorator.d.ts +0 -2
- package/dist/esm/lib/decorators/nullable-table-column.decorator.js +0 -12
- package/dist/esm/lib/decorators/primary-table-column.decorator.d.ts +0 -2
- package/dist/esm/lib/decorators/primary-table-column.decorator.js +0 -12
- package/dist/esm/lib/decorators/table-column.decorator.d.ts +0 -2
- package/dist/esm/lib/decorators/table-column.decorator.js +0 -12
- package/dist/esm/lib/decorators/table.decorator.d.ts +0 -3
- package/dist/esm/lib/decorators/table.decorator.js +0 -11
- package/dist/esm/lib/functions/data-source-config-factory.function.d.ts +0 -3
- package/dist/esm/lib/functions/data-source-config-factory.function.js +0 -24
- package/dist/esm/lib/functions/data-source-factory.function.d.ts +0 -3
- package/dist/esm/lib/functions/data-source-factory.function.js +0 -7
- package/dist/esm/lib/functions/generate-date-query.function.d.ts +0 -2
- package/dist/esm/lib/functions/generate-date-query.function.js +0 -16
- package/dist/esm/lib/functions/get-api-type-from-column.function.d.ts +0 -2
- package/dist/esm/lib/functions/get-api-type-from-column.function.js +0 -14
- package/dist/esm/lib/functions/get-paging-key.function.d.ts +0 -1
- package/dist/esm/lib/functions/get-paging-key.function.js +0 -6
- package/dist/esm/lib/functions/get-skip.function.d.ts +0 -1
- package/dist/esm/lib/functions/get-skip.function.js +0 -6
- package/dist/esm/lib/functions/remove-falsey-keys.function.d.ts +0 -1
- package/dist/esm/lib/functions/remove-falsey-keys.function.js +0 -11
- package/dist/esm/lib/server-typeorm-mysql.module.d.ts +0 -6
- package/dist/esm/lib/server-typeorm-mysql.module.js +0 -63
- package/dist/esm/lib/types/data-source-options.interface.d.ts +0 -10
- package/dist/esm/lib/types/data-source-options.interface.js +0 -2
- package/dist/esm/lib/types/entity-provider.interface.d.ts +0 -9
- package/dist/esm/lib/types/entity-provider.interface.js +0 -2
- package/dist/esm/lib/types/page-params.interface.d.ts +0 -4
- package/dist/esm/lib/types/page-params.interface.js +0 -2
- package/dist/esm/lib/types/paged-data.interface.d.ts +0 -6
- package/dist/esm/lib/types/paged-data.interface.js +0 -2
- package/dist/types/lib/classes/type-orm-paging-repository.class.d.ts +0 -14
- package/dist/types/lib/classes/type-orm-repository.class.d.ts +0 -21
- package/dist/types/lib/constants/many-to-one-relation-options.constant.d.ts +0 -2
- package/dist/types/lib/decorators/nullable-table-column.decorator.d.ts +0 -2
- package/dist/types/lib/decorators/primary-table-column.decorator.d.ts +0 -2
- package/dist/types/lib/decorators/table-column.decorator.d.ts +0 -2
- package/dist/types/lib/decorators/table.decorator.d.ts +0 -3
- package/dist/types/lib/functions/data-source-config-factory.function.d.ts +0 -3
- package/dist/types/lib/functions/data-source-factory.function.d.ts +0 -3
- package/dist/types/lib/functions/generate-date-query.function.d.ts +0 -2
- package/dist/types/lib/functions/get-api-type-from-column.function.d.ts +0 -2
- package/dist/types/lib/functions/get-paging-key.function.d.ts +0 -1
- package/dist/types/lib/functions/get-skip.function.d.ts +0 -1
- package/dist/types/lib/functions/remove-falsey-keys.function.d.ts +0 -1
- package/dist/types/lib/server-typeorm-mysql.module.d.ts +0 -6
- package/dist/types/lib/types/data-source-options.interface.d.ts +0 -10
- package/dist/types/lib/types/entity-provider.interface.d.ts +0 -9
- package/dist/types/lib/types/paged-data.interface.d.ts +0 -6
- /package/{dist/cjs/lib/types/paged-data.interface.d.ts → src/lib/types/paged-data.interface.ts} +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,892 @@
|
|
|
1
|
+
# @onivoro/server-typeorm-mysql
|
|
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.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @onivoro/server-typeorm-mysql
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
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
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
### Import the Module
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { ServerTypeormMysqlModule } from '@onivoro/server-typeorm-mysql';
|
|
28
|
+
|
|
29
|
+
@Module({
|
|
30
|
+
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
|
+
],
|
|
42
|
+
})
|
|
43
|
+
export class AppModule {}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Define Entities with Custom Decorators
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import {
|
|
50
|
+
Table,
|
|
51
|
+
PrimaryTableColumn,
|
|
52
|
+
TableColumn,
|
|
53
|
+
NullableTableColumn
|
|
54
|
+
} from '@onivoro/server-typeorm-mysql';
|
|
55
|
+
import { Entity } from 'typeorm';
|
|
56
|
+
|
|
57
|
+
@Entity()
|
|
58
|
+
@Table('users')
|
|
59
|
+
export class User {
|
|
60
|
+
@PrimaryTableColumn()
|
|
61
|
+
id: number;
|
|
62
|
+
|
|
63
|
+
@TableColumn({ type: 'varchar', length: 255 })
|
|
64
|
+
email: string;
|
|
65
|
+
|
|
66
|
+
@TableColumn({ type: 'varchar', length: 100 })
|
|
67
|
+
firstName: string;
|
|
68
|
+
|
|
69
|
+
@TableColumn({ type: 'varchar', length: 100 })
|
|
70
|
+
lastName: string;
|
|
71
|
+
|
|
72
|
+
@NullableTableColumn({ type: 'datetime' })
|
|
73
|
+
lastLoginAt?: Date;
|
|
74
|
+
|
|
75
|
+
@TableColumn({ type: 'boolean', default: true })
|
|
76
|
+
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
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Use Custom Repository
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
import { Injectable } from '@nestjs/common';
|
|
94
|
+
import { TypeOrmRepository, TypeOrmPagingRepository } from '@onivoro/server-typeorm-mysql';
|
|
95
|
+
import { User } from './user.entity';
|
|
96
|
+
|
|
97
|
+
@Injectable()
|
|
98
|
+
export class UserRepository extends TypeOrmPagingRepository<User> {
|
|
99
|
+
constructor() {
|
|
100
|
+
super(User);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async findByEmail(email: string): Promise<User | null> {
|
|
104
|
+
return this.findOne({ where: { email } });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async findActiveUsers(): Promise<User[]> {
|
|
108
|
+
return this.find({ where: { isActive: true } });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async findUsersWithPagination(page: number, limit: number) {
|
|
112
|
+
return this.findWithPaging(
|
|
113
|
+
{ where: { isActive: true } },
|
|
114
|
+
{ page, limit }
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Configuration
|
|
121
|
+
|
|
122
|
+
### Data Source Configuration
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
import { dataSourceConfigFactory } from '@onivoro/server-typeorm-mysql';
|
|
126
|
+
|
|
127
|
+
const config = dataSourceConfigFactory({
|
|
128
|
+
host: process.env.DB_HOST,
|
|
129
|
+
port: parseInt(process.env.DB_PORT),
|
|
130
|
+
username: process.env.DB_USERNAME,
|
|
131
|
+
password: process.env.DB_PASSWORD,
|
|
132
|
+
database: process.env.DB_DATABASE,
|
|
133
|
+
entities: [User, Product, Order],
|
|
134
|
+
migrations: ['src/migrations/*.ts'],
|
|
135
|
+
synchronize: false,
|
|
136
|
+
logging: process.env.NODE_ENV === 'development',
|
|
137
|
+
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Dynamic Module Configuration
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { Module } from '@nestjs/common';
|
|
145
|
+
import { ServerTypeormMysqlModule } from '@onivoro/server-typeorm-mysql';
|
|
146
|
+
import { ConfigService } from '@nestjs/config';
|
|
147
|
+
|
|
148
|
+
@Module({
|
|
149
|
+
imports: [
|
|
150
|
+
ServerTypeormMysqlModule.forRootAsync({
|
|
151
|
+
useFactory: (configService: ConfigService) => ({
|
|
152
|
+
host: configService.get('DATABASE_HOST'),
|
|
153
|
+
port: configService.get('DATABASE_PORT'),
|
|
154
|
+
username: configService.get('DATABASE_USERNAME'),
|
|
155
|
+
password: configService.get('DATABASE_PASSWORD'),
|
|
156
|
+
database: configService.get('DATABASE_NAME'),
|
|
157
|
+
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
|
158
|
+
migrations: [__dirname + '/migrations/*{.ts,.js}'],
|
|
159
|
+
synchronize: configService.get('NODE_ENV') === 'development',
|
|
160
|
+
logging: configService.get('DATABASE_LOGGING') === 'true'
|
|
161
|
+
}),
|
|
162
|
+
inject: [ConfigService]
|
|
163
|
+
})
|
|
164
|
+
],
|
|
165
|
+
})
|
|
166
|
+
export class DatabaseModule {}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Usage Examples
|
|
170
|
+
|
|
171
|
+
### Advanced Repository Usage
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
import { Injectable } from '@nestjs/common';
|
|
175
|
+
import { TypeOrmPagingRepository, PageParams, PagedData } from '@onivoro/server-typeorm-mysql';
|
|
176
|
+
import { User } from './user.entity';
|
|
177
|
+
import { FindOptionsWhere, Like, Between } from 'typeorm';
|
|
178
|
+
|
|
179
|
+
@Injectable()
|
|
180
|
+
export class AdvancedUserRepository extends TypeOrmPagingRepository<User> {
|
|
181
|
+
constructor() {
|
|
182
|
+
super(User);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async searchUsers(
|
|
186
|
+
searchTerm: string,
|
|
187
|
+
pageParams: PageParams
|
|
188
|
+
): Promise<PagedData<User>> {
|
|
189
|
+
const where: FindOptionsWhere<User> = [
|
|
190
|
+
{ firstName: Like(`%${searchTerm}%`) },
|
|
191
|
+
{ lastName: Like(`%${searchTerm}%`) },
|
|
192
|
+
{ email: Like(`%${searchTerm}%`) }
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
return this.findWithPaging(
|
|
196
|
+
{
|
|
197
|
+
where,
|
|
198
|
+
order: { createdAt: 'DESC' }
|
|
199
|
+
},
|
|
200
|
+
pageParams
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async findUsersByDateRange(
|
|
205
|
+
startDate: Date,
|
|
206
|
+
endDate: Date,
|
|
207
|
+
pageParams: PageParams
|
|
208
|
+
): Promise<PagedData<User>> {
|
|
209
|
+
return this.findWithPaging(
|
|
210
|
+
{
|
|
211
|
+
where: {
|
|
212
|
+
createdAt: Between(startDate, endDate)
|
|
213
|
+
},
|
|
214
|
+
order: { createdAt: 'DESC' }
|
|
215
|
+
},
|
|
216
|
+
pageParams
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async findRecentlyActiveUsers(days: number = 30): Promise<User[]> {
|
|
221
|
+
const cutoffDate = new Date();
|
|
222
|
+
cutoffDate.setDate(cutoffDate.getDate() - days);
|
|
223
|
+
|
|
224
|
+
return this.find({
|
|
225
|
+
where: {
|
|
226
|
+
lastLoginAt: Between(cutoffDate, new Date())
|
|
227
|
+
},
|
|
228
|
+
order: { lastLoginAt: 'DESC' }
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async getUserStatistics(): Promise<{
|
|
233
|
+
total: number;
|
|
234
|
+
active: number;
|
|
235
|
+
inactive: number;
|
|
236
|
+
recentlyRegistered: number;
|
|
237
|
+
}> {
|
|
238
|
+
const [total, active, recentlyRegistered] = await Promise.all([
|
|
239
|
+
this.count(),
|
|
240
|
+
this.count({ where: { isActive: true } }),
|
|
241
|
+
this.count({
|
|
242
|
+
where: {
|
|
243
|
+
createdAt: Between(
|
|
244
|
+
new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), // 7 days ago
|
|
245
|
+
new Date()
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
]);
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
total,
|
|
253
|
+
active,
|
|
254
|
+
inactive: total - active,
|
|
255
|
+
recentlyRegistered
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async bulkUpdateUsers(
|
|
260
|
+
userIds: number[],
|
|
261
|
+
updateData: Partial<User>
|
|
262
|
+
): Promise<void> {
|
|
263
|
+
await this.update(userIds, updateData);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async softDeleteUsers(userIds: number[]): Promise<void> {
|
|
267
|
+
await this.update(userIds, { isActive: false });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Complex Entity Relationships
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
import {
|
|
276
|
+
Table,
|
|
277
|
+
PrimaryTableColumn,
|
|
278
|
+
TableColumn,
|
|
279
|
+
NullableTableColumn,
|
|
280
|
+
ManyToOneRelationOptions
|
|
281
|
+
} from '@onivoro/server-typeorm-mysql';
|
|
282
|
+
import { Entity, ManyToOne, OneToMany, JoinColumn } from 'typeorm';
|
|
283
|
+
|
|
284
|
+
@Entity()
|
|
285
|
+
@Table('orders')
|
|
286
|
+
export class Order {
|
|
287
|
+
@PrimaryTableColumn()
|
|
288
|
+
id: number;
|
|
289
|
+
|
|
290
|
+
@TableColumn({ type: 'varchar', length: 50 })
|
|
291
|
+
orderNumber: string;
|
|
292
|
+
|
|
293
|
+
@TableColumn({ type: 'decimal', precision: 10, scale: 2 })
|
|
294
|
+
totalAmount: number;
|
|
295
|
+
|
|
296
|
+
@TableColumn({ type: 'enum', enum: ['pending', 'processing', 'shipped', 'delivered', 'cancelled'] })
|
|
297
|
+
status: string;
|
|
298
|
+
|
|
299
|
+
@TableColumn({ type: 'int' })
|
|
300
|
+
userId: number;
|
|
301
|
+
|
|
302
|
+
@TableColumn({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
|
303
|
+
createdAt: Date;
|
|
304
|
+
|
|
305
|
+
@NullableTableColumn({ type: 'timestamp' })
|
|
306
|
+
shippedAt?: Date;
|
|
307
|
+
|
|
308
|
+
@NullableTableColumn({ type: 'timestamp' })
|
|
309
|
+
deliveredAt?: Date;
|
|
310
|
+
|
|
311
|
+
// Relationships
|
|
312
|
+
@ManyToOne(() => User, user => user.orders, ManyToOneRelationOptions)
|
|
313
|
+
@JoinColumn({ name: 'userId' })
|
|
314
|
+
user: User;
|
|
315
|
+
|
|
316
|
+
@OneToMany(() => OrderItem, orderItem => orderItem.order)
|
|
317
|
+
items: OrderItem[];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
@Entity()
|
|
321
|
+
@Table('order_items')
|
|
322
|
+
export class OrderItem {
|
|
323
|
+
@PrimaryTableColumn()
|
|
324
|
+
id: number;
|
|
325
|
+
|
|
326
|
+
@TableColumn({ type: 'int' })
|
|
327
|
+
orderId: number;
|
|
328
|
+
|
|
329
|
+
@TableColumn({ type: 'int' })
|
|
330
|
+
productId: number;
|
|
331
|
+
|
|
332
|
+
@TableColumn({ type: 'int' })
|
|
333
|
+
quantity: number;
|
|
334
|
+
|
|
335
|
+
@TableColumn({ type: 'decimal', precision: 10, scale: 2 })
|
|
336
|
+
unitPrice: number;
|
|
337
|
+
|
|
338
|
+
@TableColumn({ type: 'decimal', precision: 10, scale: 2 })
|
|
339
|
+
totalPrice: number;
|
|
340
|
+
|
|
341
|
+
@ManyToOne(() => Order, order => order.items, ManyToOneRelationOptions)
|
|
342
|
+
@JoinColumn({ name: 'orderId' })
|
|
343
|
+
order: Order;
|
|
344
|
+
|
|
345
|
+
@ManyToOne(() => Product, product => product.orderItems, ManyToOneRelationOptions)
|
|
346
|
+
@JoinColumn({ name: 'productId' })
|
|
347
|
+
product: Product;
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Service Layer with Repository
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
355
|
+
import { InjectRepository } from '@nestjs/typeorm';
|
|
356
|
+
import { AdvancedUserRepository } from './user.repository';
|
|
357
|
+
import { User } from './user.entity';
|
|
358
|
+
import { PageParams, PagedData } from '@onivoro/server-typeorm-mysql';
|
|
359
|
+
|
|
360
|
+
@Injectable()
|
|
361
|
+
export class UserService {
|
|
362
|
+
constructor(
|
|
363
|
+
@InjectRepository(User)
|
|
364
|
+
private userRepository: AdvancedUserRepository
|
|
365
|
+
) {}
|
|
366
|
+
|
|
367
|
+
async createUser(userData: Partial<User>): Promise<User> {
|
|
368
|
+
const user = this.userRepository.create(userData);
|
|
369
|
+
return this.userRepository.save(user);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async findUserById(id: number): Promise<User> {
|
|
373
|
+
const user = await this.userRepository.findOne({ where: { id } });
|
|
374
|
+
if (!user) {
|
|
375
|
+
throw new NotFoundException(`User with ID ${id} not found`);
|
|
376
|
+
}
|
|
377
|
+
return user;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async updateUser(id: number, updateData: Partial<User>): Promise<User> {
|
|
381
|
+
const user = await this.findUserById(id);
|
|
382
|
+
Object.assign(user, updateData);
|
|
383
|
+
return this.userRepository.save(user);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async deleteUser(id: number): Promise<void> {
|
|
387
|
+
const user = await this.findUserById(id);
|
|
388
|
+
await this.userRepository.softDeleteUsers([id]);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async searchUsers(
|
|
392
|
+
searchTerm: string,
|
|
393
|
+
pageParams: PageParams
|
|
394
|
+
): Promise<PagedData<User>> {
|
|
395
|
+
return this.userRepository.searchUsers(searchTerm, pageParams);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async getUserStatistics() {
|
|
399
|
+
return this.userRepository.getUserStatistics();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async getRecentlyActiveUsers(days: number = 30): Promise<User[]> {
|
|
403
|
+
return this.userRepository.findRecentlyActiveUsers(days);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async bulkUpdateUsers(userIds: number[], updateData: Partial<User>): Promise<void> {
|
|
407
|
+
await this.userRepository.bulkUpdateUsers(userIds, updateData);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### Query Utilities Usage
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
import { Injectable } from '@nestjs/common';
|
|
416
|
+
import {
|
|
417
|
+
generateDateQuery,
|
|
418
|
+
removeFalseyKeys,
|
|
419
|
+
getSkip,
|
|
420
|
+
getPagingKey
|
|
421
|
+
} from '@onivoro/server-typeorm-mysql';
|
|
422
|
+
import { Repository } from 'typeorm';
|
|
423
|
+
import { InjectRepository } from '@nestjs/typeorm';
|
|
424
|
+
import { Order } from './order.entity';
|
|
425
|
+
|
|
426
|
+
@Injectable()
|
|
427
|
+
export class OrderService {
|
|
428
|
+
constructor(
|
|
429
|
+
@InjectRepository(Order)
|
|
430
|
+
private orderRepository: Repository<Order>
|
|
431
|
+
) {}
|
|
432
|
+
|
|
433
|
+
async findOrdersByDateRange(
|
|
434
|
+
startDate?: Date,
|
|
435
|
+
endDate?: Date,
|
|
436
|
+
status?: string,
|
|
437
|
+
page: number = 1,
|
|
438
|
+
limit: number = 10
|
|
439
|
+
) {
|
|
440
|
+
const whereConditions: any = removeFalseyKeys({
|
|
441
|
+
status,
|
|
442
|
+
...generateDateQuery('createdAt', startDate, endDate)
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
const skip = getSkip(page, limit);
|
|
446
|
+
|
|
447
|
+
const [orders, total] = await this.orderRepository.findAndCount({
|
|
448
|
+
where: whereConditions,
|
|
449
|
+
skip,
|
|
450
|
+
take: limit,
|
|
451
|
+
order: { createdAt: 'DESC' },
|
|
452
|
+
relations: ['user', 'items', 'items.product']
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
data: orders,
|
|
457
|
+
pagination: {
|
|
458
|
+
page,
|
|
459
|
+
limit,
|
|
460
|
+
total,
|
|
461
|
+
pages: Math.ceil(total / limit),
|
|
462
|
+
key: getPagingKey(page, limit)
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async getOrderAnalytics(startDate: Date, endDate: Date) {
|
|
468
|
+
const dateQuery = generateDateQuery('createdAt', startDate, endDate);
|
|
469
|
+
|
|
470
|
+
const queryBuilder = this.orderRepository.createQueryBuilder('order')
|
|
471
|
+
.where(dateQuery);
|
|
472
|
+
|
|
473
|
+
const [
|
|
474
|
+
totalOrders,
|
|
475
|
+
totalRevenue,
|
|
476
|
+
averageOrderValue,
|
|
477
|
+
statusBreakdown
|
|
478
|
+
] = await Promise.all([
|
|
479
|
+
queryBuilder.getCount(),
|
|
480
|
+
queryBuilder
|
|
481
|
+
.select('SUM(order.totalAmount)', 'total')
|
|
482
|
+
.getRawOne()
|
|
483
|
+
.then(result => result.total || 0),
|
|
484
|
+
queryBuilder
|
|
485
|
+
.select('AVG(order.totalAmount)', 'average')
|
|
486
|
+
.getRawOne()
|
|
487
|
+
.then(result => result.average || 0),
|
|
488
|
+
queryBuilder
|
|
489
|
+
.select('order.status', 'status')
|
|
490
|
+
.addSelect('COUNT(*)', 'count')
|
|
491
|
+
.groupBy('order.status')
|
|
492
|
+
.getRawMany()
|
|
493
|
+
]);
|
|
494
|
+
|
|
495
|
+
return {
|
|
496
|
+
totalOrders,
|
|
497
|
+
totalRevenue: parseFloat(totalRevenue),
|
|
498
|
+
averageOrderValue: parseFloat(averageOrderValue),
|
|
499
|
+
statusBreakdown: statusBreakdown.reduce((acc, item) => {
|
|
500
|
+
acc[item.status] = parseInt(item.count);
|
|
501
|
+
return acc;
|
|
502
|
+
}, {})
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### Database Transactions
|
|
509
|
+
|
|
510
|
+
```typescript
|
|
511
|
+
import { Injectable } from '@nestjs/common';
|
|
512
|
+
import { DataSource } from 'typeorm';
|
|
513
|
+
import { User } from './user.entity';
|
|
514
|
+
import { Order } from './order.entity';
|
|
515
|
+
import { OrderItem } from './order-item.entity';
|
|
516
|
+
|
|
517
|
+
@Injectable()
|
|
518
|
+
export class OrderTransactionService {
|
|
519
|
+
constructor(private dataSource: DataSource) {}
|
|
520
|
+
|
|
521
|
+
async createOrderWithItems(
|
|
522
|
+
userId: number,
|
|
523
|
+
orderData: Partial<Order>,
|
|
524
|
+
items: Array<{productId: number, quantity: number, unitPrice: number}>
|
|
525
|
+
): Promise<Order> {
|
|
526
|
+
return this.dataSource.transaction(async manager => {
|
|
527
|
+
// Create the order
|
|
528
|
+
const order = manager.create(Order, {
|
|
529
|
+
...orderData,
|
|
530
|
+
userId,
|
|
531
|
+
totalAmount: 0 // Will be calculated
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
const savedOrder = await manager.save(order);
|
|
535
|
+
|
|
536
|
+
// Create order items
|
|
537
|
+
let totalAmount = 0;
|
|
538
|
+
const orderItems = [];
|
|
539
|
+
|
|
540
|
+
for (const itemData of items) {
|
|
541
|
+
const totalPrice = itemData.quantity * itemData.unitPrice;
|
|
542
|
+
totalAmount += totalPrice;
|
|
543
|
+
|
|
544
|
+
const orderItem = manager.create(OrderItem, {
|
|
545
|
+
orderId: savedOrder.id,
|
|
546
|
+
productId: itemData.productId,
|
|
547
|
+
quantity: itemData.quantity,
|
|
548
|
+
unitPrice: itemData.unitPrice,
|
|
549
|
+
totalPrice
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
orderItems.push(await manager.save(orderItem));
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Update order total
|
|
556
|
+
savedOrder.totalAmount = totalAmount;
|
|
557
|
+
await manager.save(savedOrder);
|
|
558
|
+
|
|
559
|
+
// Update user's last order date
|
|
560
|
+
await manager.update(User, userId, {
|
|
561
|
+
updatedAt: new Date()
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
return savedOrder;
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async transferOrderToNewUser(
|
|
569
|
+
orderId: number,
|
|
570
|
+
newUserId: number
|
|
571
|
+
): Promise<void> {
|
|
572
|
+
await this.dataSource.transaction(async manager => {
|
|
573
|
+
// Update order
|
|
574
|
+
await manager.update(Order, orderId, {
|
|
575
|
+
userId: newUserId,
|
|
576
|
+
updatedAt: new Date()
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// Log the transfer
|
|
580
|
+
await manager.query(
|
|
581
|
+
'INSERT INTO order_transfers (order_id, new_user_id, transferred_at) VALUES (?, ?, ?)',
|
|
582
|
+
[orderId, newUserId, new Date()]
|
|
583
|
+
);
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
### Custom Query Builder
|
|
590
|
+
|
|
591
|
+
```typescript
|
|
592
|
+
import { Injectable } from '@nestjs/common';
|
|
593
|
+
import { Repository, SelectQueryBuilder } from 'typeorm';
|
|
594
|
+
import { InjectRepository } from '@nestjs/typeorm';
|
|
595
|
+
import { Order } from './order.entity';
|
|
596
|
+
|
|
597
|
+
@Injectable()
|
|
598
|
+
export class OrderQueryService {
|
|
599
|
+
constructor(
|
|
600
|
+
@InjectRepository(Order)
|
|
601
|
+
private orderRepository: Repository<Order>
|
|
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
|
+
}
|
|
635
|
+
|
|
636
|
+
if (filters.minAmount !== undefined) {
|
|
637
|
+
query = query.andWhere('order.totalAmount >= :minAmount', {
|
|
638
|
+
minAmount: filters.minAmount
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (filters.maxAmount !== undefined) {
|
|
643
|
+
query = query.andWhere('order.totalAmount <= :maxAmount', {
|
|
644
|
+
maxAmount: filters.maxAmount
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (filters.startDate) {
|
|
649
|
+
query = query.andWhere('order.createdAt >= :startDate', {
|
|
650
|
+
startDate: filters.startDate
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (filters.endDate) {
|
|
655
|
+
query = query.andWhere('order.createdAt <= :endDate', {
|
|
656
|
+
endDate: filters.endDate
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
query = query.orderBy('order.createdAt', 'DESC');
|
|
661
|
+
|
|
662
|
+
if (filters.limit) {
|
|
663
|
+
query = query.take(filters.limit);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (filters.offset) {
|
|
667
|
+
query = query.skip(filters.offset);
|
|
668
|
+
}
|
|
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
|
+
}
|
|
706
|
+
}
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
## API Reference
|
|
710
|
+
|
|
711
|
+
### Repository Classes
|
|
712
|
+
|
|
713
|
+
#### TypeOrmRepository<T>
|
|
714
|
+
|
|
715
|
+
Base repository class with enhanced functionality:
|
|
716
|
+
|
|
717
|
+
```typescript
|
|
718
|
+
export class TypeOrmRepository<T> extends Repository<T> {
|
|
719
|
+
constructor(entity: EntityTarget<T>)
|
|
720
|
+
|
|
721
|
+
// Enhanced methods with better error handling and utilities
|
|
722
|
+
}
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
#### TypeOrmPagingRepository<T>
|
|
726
|
+
|
|
727
|
+
Repository with built-in pagination support:
|
|
728
|
+
|
|
729
|
+
```typescript
|
|
730
|
+
export class TypeOrmPagingRepository<T> extends TypeOrmRepository<T> {
|
|
731
|
+
async findWithPaging(
|
|
732
|
+
options: FindManyOptions<T>,
|
|
733
|
+
pageParams: PageParams
|
|
734
|
+
): Promise<PagedData<T>>
|
|
735
|
+
}
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
### Decorators
|
|
739
|
+
|
|
740
|
+
#### @Table(name?: string)
|
|
741
|
+
|
|
742
|
+
Enhanced table decorator:
|
|
743
|
+
|
|
744
|
+
```typescript
|
|
745
|
+
@Table('table_name')
|
|
746
|
+
export class Entity {}
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
#### @PrimaryTableColumn(options?)
|
|
750
|
+
|
|
751
|
+
Primary key column decorator:
|
|
752
|
+
|
|
753
|
+
```typescript
|
|
754
|
+
@PrimaryTableColumn()
|
|
755
|
+
id: number;
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
#### @TableColumn(options)
|
|
759
|
+
|
|
760
|
+
Standard column decorator:
|
|
761
|
+
|
|
762
|
+
```typescript
|
|
763
|
+
@TableColumn({ type: 'varchar', length: 255 })
|
|
764
|
+
name: string;
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
#### @NullableTableColumn(options)
|
|
768
|
+
|
|
769
|
+
Nullable column decorator:
|
|
770
|
+
|
|
771
|
+
```typescript
|
|
772
|
+
@NullableTableColumn({ type: 'datetime' })
|
|
773
|
+
deletedAt?: Date;
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
### Utility Functions
|
|
777
|
+
|
|
778
|
+
#### dataSourceConfigFactory(options)
|
|
779
|
+
|
|
780
|
+
Create data source configuration:
|
|
781
|
+
|
|
782
|
+
```typescript
|
|
783
|
+
function dataSourceConfigFactory(options: DataSourceOptions): DataSourceOptions
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
#### generateDateQuery(field, startDate?, endDate?)
|
|
787
|
+
|
|
788
|
+
Generate date range query conditions:
|
|
789
|
+
|
|
790
|
+
```typescript
|
|
791
|
+
function generateDateQuery(
|
|
792
|
+
field: string,
|
|
793
|
+
startDate?: Date,
|
|
794
|
+
endDate?: Date
|
|
795
|
+
): Record<string, any>
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
#### removeFalseyKeys(object)
|
|
799
|
+
|
|
800
|
+
Remove falsy values from object:
|
|
801
|
+
|
|
802
|
+
```typescript
|
|
803
|
+
function removeFalseyKeys<T>(obj: T): Partial<T>
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
### Type Definitions
|
|
807
|
+
|
|
808
|
+
#### PageParams
|
|
809
|
+
|
|
810
|
+
Pagination parameters:
|
|
811
|
+
|
|
812
|
+
```typescript
|
|
813
|
+
interface PageParams {
|
|
814
|
+
page: number;
|
|
815
|
+
limit: number;
|
|
816
|
+
}
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
#### PagedData<T>
|
|
820
|
+
|
|
821
|
+
Paginated response data:
|
|
822
|
+
|
|
823
|
+
```typescript
|
|
824
|
+
interface PagedData<T> {
|
|
825
|
+
data: T[];
|
|
826
|
+
pagination: {
|
|
827
|
+
page: number;
|
|
828
|
+
limit: number;
|
|
829
|
+
total: number;
|
|
830
|
+
pages: number;
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
## Best Practices
|
|
836
|
+
|
|
837
|
+
1. **Repository Pattern**: Use custom repositories for complex queries
|
|
838
|
+
2. **Transactions**: Use transactions for multi-table operations
|
|
839
|
+
3. **Indexing**: Add proper indexes for frequently queried columns
|
|
840
|
+
4. **Pagination**: Always implement pagination for list endpoints
|
|
841
|
+
5. **Query Optimization**: Use query builders for complex queries
|
|
842
|
+
6. **Error Handling**: Implement proper error handling in repositories
|
|
843
|
+
7. **Type Safety**: Leverage TypeScript for type-safe database operations
|
|
844
|
+
8. **Connection Pooling**: Configure appropriate connection pool settings
|
|
845
|
+
|
|
846
|
+
## Testing
|
|
847
|
+
|
|
848
|
+
```typescript
|
|
849
|
+
import { Test } from '@nestjs/testing';
|
|
850
|
+
import { getRepositoryToken } from '@nestjs/typeorm';
|
|
851
|
+
import { Repository } from 'typeorm';
|
|
852
|
+
import { User } from './user.entity';
|
|
853
|
+
import { UserService } from './user.service';
|
|
854
|
+
|
|
855
|
+
describe('UserService', () => {
|
|
856
|
+
let service: UserService;
|
|
857
|
+
let repository: Repository<User>;
|
|
858
|
+
|
|
859
|
+
beforeEach(async () => {
|
|
860
|
+
const module = await Test.createTestingModule({
|
|
861
|
+
providers: [
|
|
862
|
+
UserService,
|
|
863
|
+
{
|
|
864
|
+
provide: getRepositoryToken(User),
|
|
865
|
+
useClass: Repository,
|
|
866
|
+
},
|
|
867
|
+
],
|
|
868
|
+
}).compile();
|
|
869
|
+
|
|
870
|
+
service = module.get<UserService>(UserService);
|
|
871
|
+
repository = module.get<Repository<User>>(getRepositoryToken(User));
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
it('should create a user', async () => {
|
|
875
|
+
const userData = {
|
|
876
|
+
email: 'test@example.com',
|
|
877
|
+
firstName: 'John',
|
|
878
|
+
lastName: 'Doe'
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
jest.spyOn(repository, 'create').mockReturnValue(userData as User);
|
|
882
|
+
jest.spyOn(repository, 'save').mockResolvedValue(userData as User);
|
|
883
|
+
|
|
884
|
+
const result = await service.createUser(userData);
|
|
885
|
+
expect(result).toEqual(userData);
|
|
886
|
+
});
|
|
887
|
+
});
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
## License
|
|
891
|
+
|
|
892
|
+
This library is part of the Onivoro monorepo ecosystem.
|