@joktec/mysql 0.2.12 → 0.2.14
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 +211 -81
- package/dist/__tests__/mysql.column.decorator.spec.d.ts +2 -0
- package/dist/__tests__/mysql.column.decorator.spec.d.ts.map +1 -0
- package/dist/__tests__/mysql.column.decorator.spec.js +60 -0
- package/dist/__tests__/mysql.column.decorator.spec.js.map +1 -0
- package/dist/__tests__/mysql.dialect.spec.d.ts +2 -0
- package/dist/__tests__/mysql.dialect.spec.d.ts.map +1 -0
- package/dist/__tests__/mysql.dialect.spec.js +28 -0
- package/dist/__tests__/mysql.dialect.spec.js.map +1 -0
- package/dist/__tests__/mysql.exception.spec.d.ts +2 -0
- package/dist/__tests__/mysql.exception.spec.d.ts.map +1 -0
- package/dist/__tests__/mysql.exception.spec.js +42 -0
- package/dist/__tests__/mysql.exception.spec.js.map +1 -0
- package/dist/__tests__/mysql.helper.spec.d.ts +2 -0
- package/dist/__tests__/mysql.helper.spec.d.ts.map +1 -0
- package/dist/__tests__/mysql.helper.spec.js +80 -0
- package/dist/__tests__/mysql.helper.spec.js.map +1 -0
- package/dist/__tests__/mysql.module.integration.spec.d.ts +2 -0
- package/dist/__tests__/mysql.module.integration.spec.d.ts.map +1 -0
- package/dist/__tests__/mysql.module.integration.spec.js +67 -0
- package/dist/__tests__/mysql.module.integration.spec.js.map +1 -0
- package/dist/__tests__/mysql.repo.cursor.spec.d.ts +2 -0
- package/dist/__tests__/mysql.repo.cursor.spec.d.ts.map +1 -0
- package/dist/__tests__/mysql.repo.cursor.spec.js +146 -0
- package/dist/__tests__/mysql.repo.cursor.spec.js.map +1 -0
- package/dist/__tests__/mysql.service.spec.d.ts +2 -0
- package/dist/__tests__/mysql.service.spec.d.ts.map +1 -0
- package/dist/__tests__/mysql.service.spec.js +70 -0
- package/dist/__tests__/mysql.service.spec.js.map +1 -0
- package/dist/__tests__/mysql.utils.spec.d.ts +2 -0
- package/dist/__tests__/mysql.utils.spec.d.ts.map +1 -0
- package/dist/__tests__/mysql.utils.spec.js +21 -0
- package/dist/__tests__/mysql.utils.spec.js.map +1 -0
- package/dist/decorators/column.decorator.d.ts +11 -0
- package/dist/decorators/column.decorator.d.ts.map +1 -0
- package/dist/decorators/column.decorator.js +35 -0
- package/dist/decorators/column.decorator.js.map +1 -0
- package/dist/decorators/columns/array.column.d.ts +2 -0
- package/dist/decorators/columns/array.column.d.ts.map +1 -0
- package/dist/decorators/columns/array.column.js +8 -0
- package/dist/decorators/columns/array.column.js.map +1 -0
- package/dist/decorators/columns/bool.column.d.ts +2 -0
- package/dist/decorators/columns/bool.column.d.ts.map +1 -0
- package/dist/decorators/columns/bool.column.js +8 -0
- package/dist/decorators/columns/bool.column.js.map +1 -0
- package/dist/decorators/columns/column.factory.d.ts +5 -0
- package/dist/decorators/columns/column.factory.d.ts.map +1 -0
- package/dist/decorators/columns/column.factory.js +40 -0
- package/dist/decorators/columns/column.factory.js.map +1 -0
- package/dist/decorators/columns/column.type.d.ts +33 -0
- package/dist/decorators/columns/column.type.d.ts.map +1 -0
- package/dist/decorators/columns/column.type.js +3 -0
- package/dist/decorators/columns/column.type.js.map +1 -0
- package/dist/decorators/columns/column.util.d.ts +15 -0
- package/dist/decorators/columns/column.util.d.ts.map +1 -0
- package/dist/decorators/columns/column.util.js +82 -0
- package/dist/decorators/columns/column.util.js.map +1 -0
- package/dist/decorators/columns/date.column.d.ts +2 -0
- package/dist/decorators/columns/date.column.d.ts.map +1 -0
- package/dist/decorators/columns/date.column.js +8 -0
- package/dist/decorators/columns/date.column.js.map +1 -0
- package/dist/decorators/columns/enum.column.d.ts +3 -0
- package/dist/decorators/columns/enum.column.d.ts.map +1 -0
- package/dist/decorators/columns/enum.column.js +8 -0
- package/dist/decorators/columns/enum.column.js.map +1 -0
- package/dist/decorators/columns/index.d.ts +14 -0
- package/dist/decorators/columns/index.d.ts.map +1 -0
- package/dist/decorators/columns/index.js +30 -0
- package/dist/decorators/columns/index.js.map +1 -0
- package/dist/decorators/columns/nested.column.d.ts +4 -0
- package/dist/decorators/columns/nested.column.d.ts.map +1 -0
- package/dist/decorators/columns/nested.column.js +12 -0
- package/dist/decorators/columns/nested.column.js.map +1 -0
- package/dist/decorators/columns/number.column.d.ts +3 -0
- package/dist/decorators/columns/number.column.d.ts.map +1 -0
- package/dist/decorators/columns/number.column.js +15 -0
- package/dist/decorators/columns/number.column.js.map +1 -0
- package/dist/decorators/columns/primary.column.d.ts +3 -0
- package/dist/decorators/columns/primary.column.d.ts.map +1 -0
- package/dist/decorators/columns/primary.column.js +48 -0
- package/dist/decorators/columns/primary.column.js.map +1 -0
- package/dist/decorators/columns/string.column.d.ts +3 -0
- package/dist/decorators/columns/string.column.d.ts.map +1 -0
- package/dist/decorators/columns/string.column.js +25 -0
- package/dist/decorators/columns/string.column.js.map +1 -0
- package/dist/decorators/columns/swagger.column.d.ts +4 -0
- package/dist/decorators/columns/swagger.column.d.ts.map +1 -0
- package/dist/decorators/columns/swagger.column.js +40 -0
- package/dist/decorators/columns/swagger.column.js.map +1 -0
- package/dist/decorators/columns/transform.column.d.ts +4 -0
- package/dist/decorators/columns/transform.column.d.ts.map +1 -0
- package/dist/decorators/columns/transform.column.js +15 -0
- package/dist/decorators/columns/transform.column.js.map +1 -0
- package/dist/decorators/index.d.ts +1 -0
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js +1 -0
- package/dist/decorators/index.js.map +1 -1
- package/dist/decorators/table.decorator.d.ts.map +1 -1
- package/dist/decorators/table.decorator.js +18 -12
- package/dist/decorators/table.decorator.js.map +1 -1
- package/dist/helpers/mysql.finder.d.ts.map +1 -1
- package/dist/helpers/mysql.finder.js.map +1 -1
- package/dist/helpers/mysql.helper.d.ts +21 -6
- package/dist/helpers/mysql.helper.d.ts.map +1 -1
- package/dist/helpers/mysql.helper.js +152 -82
- package/dist/helpers/mysql.helper.js.map +1 -1
- package/dist/helpers/mysql.utils.d.ts.map +1 -1
- package/dist/helpers/mysql.utils.js +28 -11
- package/dist/helpers/mysql.utils.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/models/mysql.model.d.ts.map +1 -1
- package/dist/models/mysql.model.js.map +1 -1
- package/dist/models/mysql.option.d.ts +11 -2
- package/dist/models/mysql.option.d.ts.map +1 -1
- package/dist/mysql.config.d.ts +1 -17
- package/dist/mysql.config.d.ts.map +1 -1
- package/dist/mysql.config.js +1 -17
- package/dist/mysql.config.js.map +1 -1
- package/dist/mysql.exception.d.ts.map +1 -1
- package/dist/mysql.exception.js +48 -4
- package/dist/mysql.exception.js.map +1 -1
- package/dist/mysql.module.d.ts.map +1 -1
- package/dist/mysql.module.js.map +1 -1
- package/dist/mysql.repo.d.ts +8 -0
- package/dist/mysql.repo.d.ts.map +1 -1
- package/dist/mysql.repo.js +164 -33
- package/dist/mysql.repo.js.map +1 -1
- package/dist/mysql.service.d.ts.map +1 -1
- package/dist/mysql.service.js +8 -2
- package/dist/mysql.service.js.map +1 -1
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +1 -0
- package/dist/services/index.js.map +1 -1
- package/dist/services/mysql.dialect.d.ts +14 -0
- package/dist/services/mysql.dialect.d.ts.map +1 -0
- package/dist/services/mysql.dialect.js +50 -0
- package/dist/services/mysql.dialect.js.map +1 -0
- package/dist/services/mysql.strategy.d.ts +1 -1
- package/dist/services/mysql.strategy.d.ts.map +1 -1
- package/dist/services/mysql.strategy.js +1 -1
- package/dist/services/mysql.strategy.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +14 -10
package/README.md
CHANGED
|
@@ -1,118 +1,248 @@
|
|
|
1
|
-
|
|
2
|
-
<h1>@joktec/mysql</h1>
|
|
3
|
-
<p>A NestJS library that provides a wrapper around the `sequelize` and `sequelize-typescript` libraries for MySQL databases.</p>
|
|
4
|
-
</div>
|
|
1
|
+
# @joktec/mysql
|
|
5
2
|
|
|
6
|
-
|
|
7
|
-
1. [Introduction](#introduction)
|
|
8
|
-
2. [Installation](#installation)
|
|
9
|
-
3. [Getting Started](#getting-started)
|
|
10
|
-
4. [Reference](#reference)
|
|
11
|
-
5. [Contributing](#contributing)
|
|
3
|
+
Relational TypeORM database package for JokTec applications.
|
|
12
4
|
|
|
13
|
-
|
|
14
|
-
This library provides an easy-to-use interface for working with MySQL databases in NestJS applications. It is built on top of the popular `sequelize` and `sequelize-typescript` libraries, and provides a set of convenient abstractions for working with these libraries.
|
|
5
|
+
`@joktec/mysql` wraps TypeORM with JokTec config, lifecycle, entity registration, base repositories, naming strategy support, and shared CRUD pagination contracts from `@joktec/core`. The package name remains `mysql`, but the first-class relational targets are MySQL, MariaDB, and Postgres.
|
|
15
6
|
|
|
16
|
-
##
|
|
17
|
-
To install this library, use either `npm` or `yarn`:
|
|
7
|
+
## Install
|
|
18
8
|
|
|
19
9
|
```bash
|
|
20
|
-
npm install @joktec/mysql
|
|
21
|
-
# or
|
|
22
10
|
yarn add @joktec/mysql
|
|
23
11
|
```
|
|
24
12
|
|
|
25
|
-
##
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
13
|
+
## Public Surface
|
|
14
|
+
|
|
15
|
+
- module and service:
|
|
16
|
+
- `MysqlModule`
|
|
17
|
+
- `MysqlService`
|
|
18
|
+
- `MysqlRepo`
|
|
19
|
+
- config and client:
|
|
20
|
+
- `MysqlConfig`
|
|
21
|
+
- `MysqlClient`
|
|
22
|
+
- `Dialect`
|
|
23
|
+
- `MysqlLogLevel`
|
|
24
|
+
- relational dialect capabilities
|
|
25
|
+
- model contracts:
|
|
26
|
+
- `MysqlModel`
|
|
27
|
+
- `IMysqlRequest`
|
|
28
|
+
- `IMysqlResponse`
|
|
29
|
+
- decorators and helpers:
|
|
30
|
+
- `@Tables`
|
|
31
|
+
- `@Column`
|
|
32
|
+
- `@PrimaryColumn`
|
|
33
|
+
- `MysqlHelper`
|
|
34
|
+
- `MysqlFinder`
|
|
35
|
+
- `MysqlNamingStrategy`
|
|
36
|
+
- selected TypeORM exports.
|
|
37
|
+
|
|
38
|
+
## Module Registration
|
|
39
|
+
|
|
40
|
+
Register application entities through `MysqlModule.forRoot(...)`:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { Module } from '@joktec/core';
|
|
42
44
|
import { MysqlModule } from '@joktec/mysql';
|
|
45
|
+
import { ProfileBadge } from './entities/profile-badge.entity';
|
|
43
46
|
|
|
44
47
|
@Module({
|
|
45
|
-
imports: [
|
|
48
|
+
imports: [
|
|
49
|
+
MysqlModule.forRoot({
|
|
50
|
+
conId: 'default',
|
|
51
|
+
models: [ProfileBadge],
|
|
52
|
+
}),
|
|
53
|
+
],
|
|
46
54
|
})
|
|
47
|
-
export class
|
|
55
|
+
export class RepositoryModule {}
|
|
48
56
|
```
|
|
49
57
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
Use `conId` when the application config contains multiple SQL connections. Repositories resolve the TypeORM repository through the same `conId`, including transaction-scoped managers when passed through repository options.
|
|
59
|
+
|
|
60
|
+
## Repository Usage
|
|
61
|
+
|
|
62
|
+
Extend `MysqlRepo` for each application entity:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
53
65
|
import { Injectable } from '@joktec/core';
|
|
54
|
-
import { MysqlService } from '@joktec/mysql';
|
|
66
|
+
import { MysqlRepo, MysqlService } from '@joktec/mysql';
|
|
67
|
+
import { ProfileBadge } from '../entities/profile-badge.entity';
|
|
55
68
|
|
|
56
69
|
@Injectable()
|
|
57
|
-
export class
|
|
58
|
-
constructor(
|
|
70
|
+
export class ProfileBadgeRepo extends MysqlRepo<ProfileBadge, string> {
|
|
71
|
+
constructor(mysqlService: MysqlService) {
|
|
72
|
+
super(mysqlService, ProfileBadge);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Services can then use the shared `BaseService` contract:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { BaseService, Injectable } from '@joktec/core';
|
|
81
|
+
import { IMysqlRequest } from '@joktec/mysql';
|
|
82
|
+
import { ProfileBadge } from '../entities/profile-badge.entity';
|
|
83
|
+
import { ProfileBadgeRepo } from '../repositories/profile-badge.repo';
|
|
59
84
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
85
|
+
@Injectable()
|
|
86
|
+
export class ProfileBadgeService extends BaseService<ProfileBadge, string, IMysqlRequest<ProfileBadge>> {
|
|
87
|
+
constructor(protected profileBadgeRepo: ProfileBadgeRepo) {
|
|
88
|
+
super(profileBadgeRepo);
|
|
63
89
|
}
|
|
64
90
|
}
|
|
65
91
|
```
|
|
66
92
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
93
|
+
## Config Shape
|
|
94
|
+
|
|
95
|
+
The application config reads the `mysql` section and maps it to `MysqlConfig`.
|
|
96
|
+
|
|
97
|
+
Common fields:
|
|
98
|
+
|
|
99
|
+
```yaml
|
|
100
|
+
mysql:
|
|
101
|
+
conId: default
|
|
102
|
+
dialect: mysql
|
|
103
|
+
host: localhost
|
|
104
|
+
port: 3306
|
|
105
|
+
username: example_user
|
|
106
|
+
password: example_password
|
|
107
|
+
database: example_db
|
|
108
|
+
charset: utf8mb4
|
|
109
|
+
timezone: Z
|
|
110
|
+
connectTimeout: 20000
|
|
111
|
+
sync: false
|
|
112
|
+
benchmark:
|
|
113
|
+
enable: false
|
|
114
|
+
all: false
|
|
115
|
+
level: [error, warn]
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Supported `dialect` values are `mysql`, `mariadb`, and `postgres`. Other TypeORM drivers are not treated as stable package targets until they have package-level contract tests.
|
|
119
|
+
|
|
120
|
+
`slaves` can be configured for TypeORM replication. Slave entries inherit missing host, port, username, password, and database values from the master config.
|
|
72
121
|
|
|
73
|
-
|
|
74
|
-
export class User extends Model<User> {
|
|
75
|
-
@Column
|
|
76
|
-
name: string;
|
|
122
|
+
`sync` is disabled by default. For multi-process deployments, enable `sync` only in one controlled owner process, such as a local example app, migration owner, or development-only bootstrap.
|
|
77
123
|
|
|
78
|
-
|
|
79
|
-
|
|
124
|
+
## Query Contract
|
|
125
|
+
|
|
126
|
+
`IMysqlRequest<T>` extends `IBaseRequest<T>` and adds SQL-specific flags:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
{
|
|
130
|
+
select?: string | Array<keyof T>;
|
|
131
|
+
keyword?: string;
|
|
132
|
+
condition?: ICondition<T>;
|
|
133
|
+
page?: number;
|
|
134
|
+
offset?: number;
|
|
135
|
+
cursor?: string;
|
|
136
|
+
cursorKey?: keyof T | Array<keyof T> | string;
|
|
137
|
+
limit?: number;
|
|
138
|
+
sort?: ISort<T>;
|
|
139
|
+
populate?: IPopulate<T>;
|
|
140
|
+
withDeleted?: boolean;
|
|
80
141
|
}
|
|
81
142
|
```
|
|
82
143
|
|
|
83
|
-
|
|
84
|
-
You can create a repository for your model by extending the MysqlRepository class and providing the model type as a generic argument:
|
|
85
|
-
```typescript
|
|
86
|
-
import { Injectable } from '@nestjs/common';
|
|
87
|
-
import { MysqlRepository } from '@joktec/mysql';
|
|
88
|
-
import { User } from './user.model';
|
|
144
|
+
Supported repository operations include `paginate`, `find`, `count`, `findOne`, `create`, `update`, `delete`, `restore`, `upsert`, and `bulkUpsert`.
|
|
89
145
|
|
|
90
|
-
|
|
91
|
-
|
|
146
|
+
Repository reads use QueryBuilder so condition, projection, sorting, relation population, and cursor pagination share the same field validation path. Request field names are validated against TypeORM metadata before they are interpolated into SQL identifiers.
|
|
147
|
+
|
|
148
|
+
## Pagination
|
|
149
|
+
|
|
150
|
+
`MysqlRepo.paginate` supports page, offset, and cursor pagination through the shared `@joktec/core` response contracts.
|
|
151
|
+
|
|
152
|
+
Runtime priority:
|
|
153
|
+
|
|
154
|
+
1. cursor when `cursor` or `cursorKey` exists
|
|
155
|
+
2. offset when `offset` exists
|
|
156
|
+
3. page as the default fallback
|
|
157
|
+
|
|
158
|
+
Cursor pagination behavior:
|
|
159
|
+
|
|
160
|
+
- default cursor keys: `createdAt` plus entity primary key columns
|
|
161
|
+
- custom `cursorKey`: supported
|
|
162
|
+
- primary keys are appended as tie-breakers
|
|
163
|
+
- cursor keys are validated against TypeORM column metadata
|
|
164
|
+
- cursor conditions are built as lexicographic SQL `OR` clauses
|
|
165
|
+
- fetches `limit + 1` rows to compute `hasNextPage`
|
|
166
|
+
- returns `nextCursor` as an opaque token
|
|
167
|
+
|
|
168
|
+
Example first cursor request:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
const firstPage = await profileBadgeRepo.paginate({
|
|
172
|
+
cursorKey: 'createdAt',
|
|
173
|
+
limit: 20,
|
|
174
|
+
sort: { createdAt: 'desc' },
|
|
175
|
+
});
|
|
92
176
|
```
|
|
93
|
-
#### Using Repository in Service
|
|
94
|
-
You can then use the repository in your service:
|
|
95
|
-
```typescript
|
|
96
|
-
import { Injectable } from '@nestjs/common';
|
|
97
|
-
import { UserRepository } from './user.repository';
|
|
98
177
|
|
|
99
|
-
|
|
100
|
-
export class UserService {
|
|
101
|
-
constructor(private readonly userRepository: UserRepository) {}
|
|
178
|
+
Example next cursor request:
|
|
102
179
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
180
|
+
```ts
|
|
181
|
+
const nextPage = await profileBadgeRepo.paginate({
|
|
182
|
+
cursor: firstPage.nextCursor,
|
|
183
|
+
limit: 20,
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
For stable cursor pagination, prefer cursor keys backed by indexed columns. The default assumes entities have `createdAt` and primary key columns available through the shared model pattern.
|
|
188
|
+
|
|
189
|
+
## Entity Notes
|
|
190
|
+
|
|
191
|
+
Keep entity definitions in the consuming app or package. Keep app-specific query behavior inside app repositories or services, not inside `@joktec/mysql`.
|
|
192
|
+
|
|
193
|
+
`@joktec/mysql` exposes schema-first decorators that wrap TypeORM metadata together with Swagger, `class-validator`, and `class-transformer` metadata. Use them when an entity should be reused as the source class for mapped DTOs.
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import { Column, MysqlModel, PrimaryColumn, Tables } from '@joktec/mysql';
|
|
197
|
+
|
|
198
|
+
@Tables<ProfileBadge>({ name: 'profile_badges', index: ['code', 'active'] })
|
|
199
|
+
export class ProfileBadge extends MysqlModel {
|
|
200
|
+
@PrimaryColumn('uuidv7')
|
|
201
|
+
id?: string;
|
|
202
|
+
|
|
203
|
+
@Column({ length: 64, nullable: false, unique: true })
|
|
204
|
+
code!: string;
|
|
205
|
+
|
|
206
|
+
@Column({ length: 128, nullable: false })
|
|
207
|
+
title!: string;
|
|
208
|
+
|
|
209
|
+
@Column({ nullable: false, default: true })
|
|
210
|
+
active!: boolean;
|
|
107
211
|
}
|
|
108
212
|
```
|
|
109
213
|
|
|
110
|
-
|
|
111
|
-
|
|
214
|
+
`@Column` accepts normal TypeORM column options and adds optional schema metadata such as `hidden`, `groups`, `example`, `deprecated`, `swagger`, `decorators`, `required`, `isEmail`, `isPhone`, `isHexColor`, `isUrl`, `minlength`, `maxlength`, `min`, and `max`.
|
|
215
|
+
|
|
216
|
+
`@PrimaryColumn` supports TypeORM generated strategies (`increment`, `uuid`, `rowid`, `identity`) and JokTec-managed `uuidv7`. `uuidv7` is stored as a 36-character varchar and generated before insert when the entity does not already have an id.
|
|
112
217
|
|
|
113
|
-
|
|
218
|
+
Guidelines:
|
|
114
219
|
|
|
115
|
-
|
|
116
|
-
|
|
220
|
+
- Prefer numeric auto-increment primary keys for write-heavy MySQL tables.
|
|
221
|
+
- Use UUID primary keys only when the application needs globally unique/public identifiers.
|
|
222
|
+
- Prefer `uuidv7` over random UUIDs when the id is also used as a cursor or clustered/indexed ordering signal.
|
|
223
|
+
- For UUID-heavy cursor pagination, prefer a composite cursor such as `createdAt + id`, or a monotonic indexed cursor column.
|
|
224
|
+
- Add indexes that match common filters and cursor sort order. A cursor using `createdAt + id` should have a matching composite index where possible.
|
|
225
|
+
- `@Tables` provides common TypeORM entity/index wiring, but database-specific search/index behavior should still be verified per dialect.
|
|
117
226
|
|
|
118
|
-
|
|
227
|
+
## Error Contract
|
|
228
|
+
|
|
229
|
+
`MysqlCatch` normalizes common TypeORM driver errors into stable framework codes, including duplicate key, foreign key violation, not-null violation, unknown column, deadlock, lock timeout, connection failure, and transaction conflict. Raw driver details remain attached for logging/debugging, but application code should branch on the stable framework message.
|
|
230
|
+
|
|
231
|
+
## Repository Layout
|
|
232
|
+
|
|
233
|
+
- `src/mysql.module.ts`: Nest module and entity registration.
|
|
234
|
+
- `src/mysql.service.ts`: connection lifecycle service.
|
|
235
|
+
- `src/mysql.repo.ts`: base repository and pagination implementation.
|
|
236
|
+
- `src/mysql.config.ts`: config validation and defaults.
|
|
237
|
+
- `src/helpers`: query parsing and TypeORM option helpers.
|
|
238
|
+
- `src/services`: naming strategy and benchmark helpers.
|
|
239
|
+
- `src/models`: model, request, response, and options contracts.
|
|
240
|
+
- `src/index.ts`: public package export boundary.
|
|
241
|
+
|
|
242
|
+
## Development
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
yarn lint --scope @joktec/mysql
|
|
246
|
+
yarn build --scope @joktec/mysql
|
|
247
|
+
yarn test --scope @joktec/mysql
|
|
248
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mysql.column.decorator.spec.d.ts","sourceRoot":"","sources":["../../src/__tests__/mysql.column.decorator.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const globals_1 = require("@jest/globals");
|
|
13
|
+
const utils_1 = require("@joktec/utils");
|
|
14
|
+
const typeorm_1 = require("typeorm");
|
|
15
|
+
const decorators_1 = require("../decorators");
|
|
16
|
+
const UUID_V7_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
17
|
+
(0, globals_1.describe)('MySQL column decorators', () => {
|
|
18
|
+
(0, globals_1.it)('applies TypeORM column metadata and validation decorators', () => {
|
|
19
|
+
class ProductColumnFixture {
|
|
20
|
+
}
|
|
21
|
+
__decorate([
|
|
22
|
+
(0, decorators_1.Column)({ length: 10, nullable: false }),
|
|
23
|
+
__metadata("design:type", String)
|
|
24
|
+
], ProductColumnFixture.prototype, "name", void 0);
|
|
25
|
+
const metadata = (0, typeorm_1.getMetadataArgsStorage)().columns.find(item => item.target === ProductColumnFixture && item.propertyName === 'name');
|
|
26
|
+
(0, globals_1.expect)(metadata).toBeDefined();
|
|
27
|
+
(0, globals_1.expect)(metadata.options.length).toEqual(10);
|
|
28
|
+
const invalid = new ProductColumnFixture();
|
|
29
|
+
(0, globals_1.expect)((0, utils_1.validateSync)(invalid).map(error => error.property)).toContain('name');
|
|
30
|
+
const valid = new ProductColumnFixture();
|
|
31
|
+
valid.name = 'product';
|
|
32
|
+
(0, globals_1.expect)((0, utils_1.validateSync)(valid)).toHaveLength(0);
|
|
33
|
+
});
|
|
34
|
+
(0, globals_1.it)('treats nullable columns as optional validation fields', () => {
|
|
35
|
+
class OptionalColumnFixture {
|
|
36
|
+
}
|
|
37
|
+
__decorate([
|
|
38
|
+
(0, decorators_1.Column)({ length: 10, nullable: true }),
|
|
39
|
+
__metadata("design:type", String)
|
|
40
|
+
], OptionalColumnFixture.prototype, "code", void 0);
|
|
41
|
+
(0, globals_1.expect)((0, utils_1.validateSync)(new OptionalColumnFixture())).toHaveLength(0);
|
|
42
|
+
});
|
|
43
|
+
(0, globals_1.it)('generates uuidv7 values before insert for primary columns', () => {
|
|
44
|
+
class UuidPrimaryFixture {
|
|
45
|
+
}
|
|
46
|
+
__decorate([
|
|
47
|
+
(0, decorators_1.PrimaryColumn)('uuidv7'),
|
|
48
|
+
__metadata("design:type", String)
|
|
49
|
+
], UuidPrimaryFixture.prototype, "id", void 0);
|
|
50
|
+
const fixture = new UuidPrimaryFixture();
|
|
51
|
+
const hookName = Object.getOwnPropertyNames(UuidPrimaryFixture.prototype).find(name => name.includes('__joktecBeforeInsertUuidV7_id'));
|
|
52
|
+
(0, globals_1.expect)(hookName).toBeDefined();
|
|
53
|
+
fixture[hookName]();
|
|
54
|
+
(0, globals_1.expect)(fixture.id).toMatch(UUID_V7_REGEX);
|
|
55
|
+
const metadata = (0, typeorm_1.getMetadataArgsStorage)().columns.find(item => item.target === UuidPrimaryFixture && item.propertyName === 'id');
|
|
56
|
+
(0, globals_1.expect)(metadata).toBeDefined();
|
|
57
|
+
(0, globals_1.expect)(metadata.options.primary).toEqual(true);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
//# sourceMappingURL=mysql.column.decorator.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mysql.column.decorator.spec.js","sourceRoot":"","sources":["../../src/__tests__/mysql.column.decorator.spec.ts"],"names":[],"mappings":";;;;;;;;;;;AAAA,2CAAqD;AACrD,yCAA6C;AAC7C,qCAAiD;AACjD,8CAAsD;AAEtD,MAAM,aAAa,GAAG,uEAAuE,CAAC;AAE9F,IAAA,kBAAQ,EAAC,yBAAyB,EAAE,GAAG,EAAE;IACvC,IAAA,YAAE,EAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,MAAM,oBAAoB;SAGzB;QADC;YADC,IAAA,mBAAM,EAAC,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;;0DAC1B;QAGhB,MAAM,QAAQ,GAAG,IAAA,gCAAsB,GAAE,CAAC,OAAO,CAAC,IAAI,CACpD,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,KAAK,oBAAoB,IAAI,IAAI,CAAC,YAAY,KAAK,MAAM,CAC7E,CAAC;QAEF,IAAA,gBAAM,EAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC;QAC/B,IAAA,gBAAM,EAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAE5C,MAAM,OAAO,GAAG,IAAI,oBAAoB,EAAE,CAAC;QAC3C,IAAA,gBAAM,EAAC,IAAA,oBAAY,EAAC,OAAO,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QAE7E,MAAM,KAAK,GAAG,IAAI,oBAAoB,EAAE,CAAC;QACzC,KAAK,CAAC,IAAI,GAAG,SAAS,CAAC;QACvB,IAAA,gBAAM,EAAC,IAAA,oBAAY,EAAC,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;IAEH,IAAA,YAAE,EAAC,uDAAuD,EAAE,GAAG,EAAE;QAC/D,MAAM,qBAAqB;SAG1B;QADC;YADC,IAAA,mBAAM,EAAC,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;2DACzB;QAGhB,IAAA,gBAAM,EAAC,IAAA,oBAAY,EAAC,IAAI,qBAAqB,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IACpE,CAAC,CAAC,CAAC;IAEH,IAAA,YAAE,EAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,MAAM,kBAAkB;SAGvB;QADC;YADC,IAAA,0BAAa,EAAC,QAAQ,CAAC;;sDACZ;QAGd,MAAM,OAAO,GAAG,IAAI,kBAAkB,EAAE,CAAC;QACzC,MAAM,QAAQ,GAAG,MAAM,CAAC,mBAAmB,CAAC,kBAAkB,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CACpF,IAAI,CAAC,QAAQ,CAAC,+BAA+B,CAAC,CAC/C,CAAC;QAEF,IAAA,gBAAM,EAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC;QAC/B,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QAEpB,IAAA,gBAAM,EAAC,OAAO,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;QAE1C,MAAM,QAAQ,GAAG,IAAA,gCAAsB,GAAE,CAAC,OAAO,CAAC,IAAI,CACpD,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,KAAK,kBAAkB,IAAI,IAAI,CAAC,YAAY,KAAK,IAAI,CACzE,CAAC;QACF,IAAA,gBAAM,EAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC;QAC/B,IAAA,gBAAM,EAAC,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mysql.dialect.spec.d.ts","sourceRoot":"","sources":["../../src/__tests__/mysql.dialect.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const mysql_config_1 = require("../mysql.config");
|
|
4
|
+
const services_1 = require("../services");
|
|
5
|
+
describe('Mysql dialect support', () => {
|
|
6
|
+
it('should default schema sync to explicit opt-in', () => {
|
|
7
|
+
const config = new mysql_config_1.MysqlConfig({ username: 'root', password: 'root', database: 'joktec' });
|
|
8
|
+
expect(config.sync).toBe(false);
|
|
9
|
+
});
|
|
10
|
+
it('should expose first-class capabilities for mysql and postgres', () => {
|
|
11
|
+
expect((0, services_1.getMysqlDialectCapabilities)(mysql_config_1.Dialect.MYSQL)).toMatchObject({
|
|
12
|
+
firstClass: true,
|
|
13
|
+
caseInsensitiveLike: 'LIKE',
|
|
14
|
+
fullTextIndex: true,
|
|
15
|
+
});
|
|
16
|
+
expect((0, services_1.getMysqlDialectCapabilities)(mysql_config_1.Dialect.POSTGRES)).toMatchObject({
|
|
17
|
+
firstClass: true,
|
|
18
|
+
caseInsensitiveLike: 'ILIKE',
|
|
19
|
+
arrayOperators: true,
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
it('should accept only first-class relational dialects', () => {
|
|
23
|
+
expect(() => (0, services_1.assertFirstClassDialect)(mysql_config_1.Dialect.MYSQL)).not.toThrow();
|
|
24
|
+
expect(() => (0, services_1.assertFirstClassDialect)(mysql_config_1.Dialect.MARIADB)).not.toThrow();
|
|
25
|
+
expect(() => (0, services_1.assertFirstClassDialect)(mysql_config_1.Dialect.POSTGRES)).not.toThrow();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
//# sourceMappingURL=mysql.dialect.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mysql.dialect.spec.js","sourceRoot":"","sources":["../../src/__tests__/mysql.dialect.spec.ts"],"names":[],"mappings":";;AAAA,kDAAuD;AACvD,0CAAmF;AAEnF,QAAQ,CAAC,uBAAuB,EAAE,GAAG,EAAE;IACrC,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,MAAM,MAAM,GAAG,IAAI,0BAAW,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAiB,CAAC,CAAC;QAE1G,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAClC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+DAA+D,EAAE,GAAG,EAAE;QACvE,MAAM,CAAC,IAAA,sCAA2B,EAAC,sBAAO,CAAC,KAAK,CAAC,CAAC,CAAC,aAAa,CAAC;YAC/D,UAAU,EAAE,IAAI;YAChB,mBAAmB,EAAE,MAAM;YAC3B,aAAa,EAAE,IAAI;SACpB,CAAC,CAAC;QACH,MAAM,CAAC,IAAA,sCAA2B,EAAC,sBAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,aAAa,CAAC;YAClE,UAAU,EAAE,IAAI;YAChB,mBAAmB,EAAE,OAAO;YAC5B,cAAc,EAAE,IAAI;SACrB,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC5D,MAAM,CAAC,GAAG,EAAE,CAAC,IAAA,kCAAuB,EAAC,sBAAO,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;QACnE,MAAM,CAAC,GAAG,EAAE,CAAC,IAAA,kCAAuB,EAAC,sBAAO,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;QACrE,MAAM,CAAC,GAAG,EAAE,CAAC,IAAA,kCAAuB,EAAC,sBAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;IACxE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mysql.exception.spec.d.ts","sourceRoot":"","sources":["../../src/__tests__/mysql.exception.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const typeorm_1 = require("typeorm");
|
|
13
|
+
const mysql_exception_1 = require("../mysql.exception");
|
|
14
|
+
class QueryFailureHarness {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.PinoLogger = {
|
|
17
|
+
setContext: jest.fn(),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
async run(error) {
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
__decorate([
|
|
25
|
+
mysql_exception_1.MysqlCatch,
|
|
26
|
+
__metadata("design:type", Function),
|
|
27
|
+
__metadata("design:paramtypes", [Error]),
|
|
28
|
+
__metadata("design:returntype", Promise)
|
|
29
|
+
], QueryFailureHarness.prototype, "run", null);
|
|
30
|
+
describe('Mysql exception mapping', () => {
|
|
31
|
+
it('should normalize duplicate key driver errors', async () => {
|
|
32
|
+
const harness = new QueryFailureHarness();
|
|
33
|
+
const error = new typeorm_1.QueryFailedError('insert', [], { code: 'ER_DUP_ENTRY' });
|
|
34
|
+
await expect(harness.run(error)).rejects.toThrow('MYSQL_DUPLICATE_KEY');
|
|
35
|
+
});
|
|
36
|
+
it('should normalize postgres deadlock driver errors', async () => {
|
|
37
|
+
const harness = new QueryFailureHarness();
|
|
38
|
+
const error = new typeorm_1.QueryFailedError('update', [], { code: '40P01' });
|
|
39
|
+
await expect(harness.run(error)).rejects.toThrow('MYSQL_DEADLOCK');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
//# sourceMappingURL=mysql.exception.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mysql.exception.spec.js","sourceRoot":"","sources":["../../src/__tests__/mysql.exception.spec.ts"],"names":[],"mappings":";;;;;;;;;;;AAAA,qCAA2C;AAC3C,wDAAgD;AAEhD,MAAM,mBAAmB;IAAzB;QACE,eAAU,GAAG;YACX,UAAU,EAAE,IAAI,CAAC,EAAE,EAAE;SACtB,CAAC;IAMJ,CAAC;IAHO,AAAN,KAAK,CAAC,GAAG,CAAC,KAAY;QACpB,MAAM,KAAK,CAAC;IACd,CAAC;CACF;AAHO;IADL,4BAAU;;qCACM,KAAK;;8CAErB;AAGH,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;IACvC,EAAE,CAAC,8CAA8C,EAAE,KAAK,IAAI,EAAE;QAC5D,MAAM,OAAO,GAAG,IAAI,mBAAmB,EAAE,CAAC;QAC1C,MAAM,KAAK,GAAG,IAAI,0BAAgB,CAAC,QAAQ,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,cAAc,EAA8B,CAAC,CAAC;QAEvG,MAAM,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAC1E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kDAAkD,EAAE,KAAK,IAAI,EAAE;QAChE,MAAM,OAAO,GAAG,IAAI,mBAAmB,EAAE,CAAC;QAC1C,MAAM,KAAK,GAAG,IAAI,0BAAgB,CAAC,QAAQ,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAA8B,CAAC,CAAC;QAEhG,MAAM,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACrE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mysql.helper.spec.d.ts","sourceRoot":"","sources":["../../src/__tests__/mysql.helper.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const helpers_1 = require("../helpers");
|
|
4
|
+
const mysql_config_1 = require("../mysql.config");
|
|
5
|
+
const metadata = {
|
|
6
|
+
name: 'TestMysqlEntity',
|
|
7
|
+
columns: [
|
|
8
|
+
{ propertyName: 'id', propertyPath: 'id' },
|
|
9
|
+
{ propertyName: 'title', propertyPath: 'title' },
|
|
10
|
+
{ propertyName: 'createdAt', propertyPath: 'createdAt' },
|
|
11
|
+
],
|
|
12
|
+
relations: [{ propertyName: 'author', propertyPath: 'author' }],
|
|
13
|
+
};
|
|
14
|
+
const createQueryBuilder = () => ({
|
|
15
|
+
alias: 'TestMysqlEntity',
|
|
16
|
+
andWhere: jest.fn().mockReturnThis(),
|
|
17
|
+
addOrderBy: jest.fn().mockReturnThis(),
|
|
18
|
+
select: jest.fn().mockReturnThis(),
|
|
19
|
+
leftJoinAndSelect: jest.fn().mockReturnThis(),
|
|
20
|
+
});
|
|
21
|
+
describe('MysqlHelper query safety', () => {
|
|
22
|
+
it('should reject unsafe field paths before interpolating identifiers', () => {
|
|
23
|
+
const qb = createQueryBuilder();
|
|
24
|
+
expect(() => helpers_1.MysqlHelper.applyOrder(qb, { 'title;drop': 'asc' }, { metadata, dialect: mysql_config_1.Dialect.MYSQL })).toThrow('MYSQL_UNSAFE_FIELD_PATH');
|
|
25
|
+
});
|
|
26
|
+
it('should reject unknown columns from request query', () => {
|
|
27
|
+
const qb = createQueryBuilder();
|
|
28
|
+
expect(() => helpers_1.MysqlHelper.applyProjection(qb, ['missingColumn'], { metadata, dialect: mysql_config_1.Dialect.MYSQL })).toThrow('MYSQL_UNKNOWN_COLUMN');
|
|
29
|
+
});
|
|
30
|
+
it('should reject unsupported array operators for mysql dialect', () => {
|
|
31
|
+
const qb = createQueryBuilder();
|
|
32
|
+
expect(() => helpers_1.MysqlHelper.applyCondition(qb, { title: { $size: 2 } }, { metadata, dialect: mysql_config_1.Dialect.MYSQL })).toThrow('MYSQL_OPERATOR_UNSUPPORTED_BY_DIALECT');
|
|
33
|
+
});
|
|
34
|
+
it('should choose postgres case-insensitive like operator and escape wildcard input', () => {
|
|
35
|
+
const qb = createQueryBuilder();
|
|
36
|
+
helpers_1.MysqlHelper.applyCondition(qb, { title: { $like: 'hello_%\\' } }, {
|
|
37
|
+
metadata,
|
|
38
|
+
dialect: mysql_config_1.Dialect.POSTGRES,
|
|
39
|
+
});
|
|
40
|
+
expect(qb.andWhere).toHaveBeenCalledWith("TestMysqlEntity.title ILIKE :title_0 ESCAPE '\\\\'", {
|
|
41
|
+
title_0: '%hello\\_\\%\\\\%',
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
it('should reject empty or non-array values for list operators', () => {
|
|
45
|
+
const qb = createQueryBuilder();
|
|
46
|
+
expect(() => helpers_1.MysqlHelper.applyCondition(qb, { title: { $in: [] } }, { metadata, dialect: mysql_config_1.Dialect.MYSQL })).toThrow('MYSQL_INVALID_OPERATOR_VALUE');
|
|
47
|
+
expect(() => helpers_1.MysqlHelper.applyCondition(qb, { title: { $nin: 'draft' } }, { metadata, dialect: mysql_config_1.Dialect.MYSQL })).toThrow('MYSQL_INVALID_OPERATOR_VALUE');
|
|
48
|
+
});
|
|
49
|
+
it('should respect boolean semantics for nil and exists operators', () => {
|
|
50
|
+
const qb = createQueryBuilder();
|
|
51
|
+
helpers_1.MysqlHelper.applyCondition(qb, {
|
|
52
|
+
title: { $nil: false },
|
|
53
|
+
createdAt: { $exists: false },
|
|
54
|
+
}, { metadata, dialect: mysql_config_1.Dialect.MYSQL });
|
|
55
|
+
expect(qb.andWhere).toHaveBeenCalledWith('TestMysqlEntity.title IS NOT NULL');
|
|
56
|
+
expect(qb.andWhere).toHaveBeenCalledWith('TestMysqlEntity.createdAt IS NULL');
|
|
57
|
+
});
|
|
58
|
+
it('should keep the root alias when building nested logical clauses', () => {
|
|
59
|
+
const qb = createQueryBuilder();
|
|
60
|
+
helpers_1.MysqlHelper.applyCondition(qb, { $or: [{ title: 'first' }, { title: 'second' }] }, {
|
|
61
|
+
metadata,
|
|
62
|
+
dialect: mysql_config_1.Dialect.MYSQL,
|
|
63
|
+
});
|
|
64
|
+
const rootBracket = qb.andWhere.mock.calls[0][0];
|
|
65
|
+
const childBuilder = {
|
|
66
|
+
andWhere: jest.fn().mockReturnThis(),
|
|
67
|
+
orWhere: jest.fn().mockReturnThis(),
|
|
68
|
+
};
|
|
69
|
+
rootBracket.whereFactory(childBuilder);
|
|
70
|
+
const firstNestedBracket = childBuilder.andWhere.mock.calls[0][0];
|
|
71
|
+
const grandChildBuilder = {
|
|
72
|
+
andWhere: jest.fn().mockReturnThis(),
|
|
73
|
+
};
|
|
74
|
+
firstNestedBracket.whereFactory(grandChildBuilder);
|
|
75
|
+
expect(grandChildBuilder.andWhere).toHaveBeenCalledWith('TestMysqlEntity.title = :title_0', {
|
|
76
|
+
title_0: 'first',
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
//# sourceMappingURL=mysql.helper.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mysql.helper.spec.js","sourceRoot":"","sources":["../../src/__tests__/mysql.helper.spec.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mysql.module.integration.spec.d.ts","sourceRoot":"","sources":["../../src/__tests__/mysql.module.integration.spec.ts"],"names":[],"mappings":""}
|