@onivoro/server-typeorm-mysql 22.0.4 → 24.0.1
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 +1002 -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/esm/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 -42
- package/dist/cjs/lib/classes/type-orm-repository.class.js +0 -93
- 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.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 -42
- package/dist/esm/lib/classes/type-orm-repository.class.js +0 -93
- 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/index.d.ts +0 -19
- 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 -42
- 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,1002 @@
|
|
|
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 { 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
|
+
}
|
|
103
|
+
|
|
104
|
+
async findByEmail(email: string): Promise<User | null> {
|
|
105
|
+
return this.getOne({ where: { email } });
|
|
106
|
+
}
|
|
107
|
+
|
|
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
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
import { Injectable } from '@nestjs/common';
|
|
176
|
+
import { TypeOrmRepository } from '@onivoro/server-typeorm-mysql';
|
|
177
|
+
import { EntityManager } from 'typeorm';
|
|
178
|
+
import { User } from './user.entity';
|
|
179
|
+
|
|
180
|
+
@Injectable()
|
|
181
|
+
export class UserRepository extends TypeOrmRepository<User> {
|
|
182
|
+
constructor(entityManager: EntityManager) {
|
|
183
|
+
super(User, entityManager);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Create a single user
|
|
187
|
+
async createUser(userData: Partial<User>): Promise<User> {
|
|
188
|
+
return this.postOne(userData);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Create multiple users
|
|
192
|
+
async createUsers(usersData: Partial<User>[]): Promise<User[]> {
|
|
193
|
+
return this.postMany(usersData);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Find users with filters
|
|
197
|
+
async findUsers(filters: { isActive?: boolean; email?: string }): Promise<User[]> {
|
|
198
|
+
return this.getMany({ where: filters });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Find users with count
|
|
202
|
+
async findUsersWithCount(filters: { isActive?: boolean }): Promise<[User[], number]> {
|
|
203
|
+
return this.getManyAndCount({ where: filters });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Find a single user
|
|
207
|
+
async findUserById(id: number): Promise<User> {
|
|
208
|
+
return this.getOne({ where: { id } });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Update user
|
|
212
|
+
async updateUser(id: number, updateData: Partial<User>): Promise<void> {
|
|
213
|
+
await this.patch({ id }, updateData);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Replace user data
|
|
217
|
+
async replaceUser(id: number, userData: Partial<User>): Promise<void> {
|
|
218
|
+
await this.put({ id }, userData);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Delete user permanently
|
|
222
|
+
async deleteUser(id: number): Promise<void> {
|
|
223
|
+
await this.delete({ id });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Soft delete user
|
|
227
|
+
async softDeleteUser(id: number): Promise<void> {
|
|
228
|
+
await this.softDelete({ id });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Advanced Repository Usage with Pagination
|
|
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);
|
|
245
|
+
}
|
|
246
|
+
|
|
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
|
|
255
|
+
});
|
|
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
|
+
);
|
|
268
|
+
}
|
|
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' }
|
|
281
|
+
},
|
|
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())
|
|
293
|
+
},
|
|
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
|
+
)
|
|
314
|
+
}
|
|
315
|
+
});
|
|
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 });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
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;
|
|
374
|
+
|
|
375
|
+
@NullableTableColumn({ type: 'timestamp' })
|
|
376
|
+
shippedAt?: Date;
|
|
377
|
+
|
|
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
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
425
|
+
import { AdvancedUserRepository } from './user.repository';
|
|
426
|
+
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
|
+
|
|
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
|
+
}
|
|
475
|
+
}
|
|
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
|
+
|
|
492
|
+
@Injectable()
|
|
493
|
+
export class OrderService extends TypeOrmRepository<Order> {
|
|
494
|
+
constructor(entityManager: EntityManager) {
|
|
495
|
+
super(Order, entityManager);
|
|
496
|
+
}
|
|
497
|
+
|
|
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
|
+
});
|
|
509
|
+
|
|
510
|
+
const skip = getSkip(page, limit);
|
|
511
|
+
|
|
512
|
+
const [orders, total] = await this.getManyAndCount({
|
|
513
|
+
where: whereConditions,
|
|
514
|
+
skip,
|
|
515
|
+
take: limit,
|
|
516
|
+
order: { createdAt: 'DESC' },
|
|
517
|
+
relations: ['user', 'items', 'items.product']
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
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
|
+
}, {})
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
### Database Transactions
|
|
574
|
+
|
|
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
|
+
}
|
|
634
|
+
|
|
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
|
|
659
|
+
|
|
660
|
+
```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
|
+
}
|
|
747
|
+
}
|
|
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:
|
|
845
|
+
|
|
846
|
+
```typescript
|
|
847
|
+
function dataSourceConfigFactory(options: DataSourceOptions): DataSourceOptions
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
#### generateDateQuery(field, startDate?, endDate?)
|
|
851
|
+
|
|
852
|
+
Generate date range query conditions:
|
|
853
|
+
|
|
854
|
+
```typescript
|
|
855
|
+
function generateDateQuery(
|
|
856
|
+
field: string,
|
|
857
|
+
startDate?: Date,
|
|
858
|
+
endDate?: Date
|
|
859
|
+
): Record<string, any>
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
#### removeFalseyKeys(object)
|
|
863
|
+
|
|
864
|
+
Remove falsy values from object:
|
|
865
|
+
|
|
866
|
+
```typescript
|
|
867
|
+
function removeFalseyKeys<T>(obj: T): Partial<T>
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
### Type Definitions
|
|
871
|
+
|
|
872
|
+
#### PageParams
|
|
873
|
+
|
|
874
|
+
Pagination parameters:
|
|
875
|
+
|
|
876
|
+
```typescript
|
|
877
|
+
interface PageParams {
|
|
878
|
+
page: number;
|
|
879
|
+
limit: number;
|
|
880
|
+
}
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
#### PagedData<T>
|
|
884
|
+
|
|
885
|
+
Paginated response data:
|
|
886
|
+
|
|
887
|
+
```typescript
|
|
888
|
+
interface PagedData<T> {
|
|
889
|
+
data: T[];
|
|
890
|
+
pagination: {
|
|
891
|
+
page: number;
|
|
892
|
+
limit: number;
|
|
893
|
+
total: number;
|
|
894
|
+
pages: number;
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
#### TQueryStreamParams
|
|
900
|
+
|
|
901
|
+
Query streaming parameters:
|
|
902
|
+
|
|
903
|
+
```typescript
|
|
904
|
+
type TQueryStreamParams<TRecord = any> = {
|
|
905
|
+
query: string;
|
|
906
|
+
onData?: (stream: ReadStream, record: TRecord, count: number) => Promise<any | void>;
|
|
907
|
+
onError?: (stream: ReadStream, error: any) => Promise<any | void>;
|
|
908
|
+
onEnd?: (stream: ReadStream, count: number) => Promise<any | void>;
|
|
909
|
+
};
|
|
910
|
+
```
|
|
911
|
+
|
|
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
|
|
924
|
+
|
|
925
|
+
```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
|
+
};
|
|
956
|
+
|
|
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
|
+
};
|
|
982
|
+
|
|
983
|
+
const createdUser = { id: 1, ...userData };
|
|
984
|
+
jest.spyOn(repository, 'postOne').mockResolvedValue(createdUser as User);
|
|
985
|
+
|
|
986
|
+
const result = await service.createUser(userData);
|
|
987
|
+
expect(result).toEqual(createdUser);
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
it('should find user by id', async () => {
|
|
991
|
+
const user = { id: 1, email: 'test@example.com', firstName: 'John', lastName: 'Doe' };
|
|
992
|
+
jest.spyOn(repository, 'getOne').mockResolvedValue(user as User);
|
|
993
|
+
|
|
994
|
+
const result = await service.findUserById(1);
|
|
995
|
+
expect(result).toEqual(user);
|
|
996
|
+
});
|
|
997
|
+
});
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
## License
|
|
1001
|
+
|
|
1002
|
+
This library is part of the Onivoro monorepo ecosystem.
|