@nest-util/nest-crud 1.1.2 → 1.1.3
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 +55 -0
- package/dist/lib/helpers/filter.helper.d.ts +12 -1
- package/dist/lib/helpers/filter.helper.d.ts.map +1 -1
- package/dist/lib/helpers/filter.helper.js +46 -22
- package/dist/lib/services/nest-crud.service.d.ts +5 -4
- package/dist/lib/services/nest-crud.service.d.ts.map +1 -1
- package/dist/lib/services/nest-crud.service.js +24 -29
- package/dist/lib/testing/crud-service.test-suites.d.ts.map +1 -1
- package/dist/lib/testing/crud-service.test-suites.js +58 -0
- package/dist/lib/testing/testing.interface.d.ts +2 -2
- package/dist/lib/testing/testing.interface.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,6 +21,61 @@ pnpm add @nest-util/nest-crud@^1.0.7 @nest-util/nest-auth@^1.1.0 typeorm@^1.1.0
|
|
|
21
21
|
- Automatic Swagger documentation
|
|
22
22
|
- TypeORM exception filter for duplicate key errors
|
|
23
23
|
|
|
24
|
+
## Filtering and Sorting
|
|
25
|
+
|
|
26
|
+
### Filtering
|
|
27
|
+
|
|
28
|
+
Query format: `?filter[field_operator]=value` (requires Express `query parser` set to `extended`).
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
app.getHttpAdapter().getInstance().set('query parser', 'extended');
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Supported operators: `eq`, `ne`, `cont`, `notcont`, `starts`, `ends`, `gte`, `lte`, `gt`, `lt`, `in`, `nin`, `isnull`. Groups via `filter[and][0][field_eq]=...` and `filter[or][0][field_eq]=...`.
|
|
35
|
+
|
|
36
|
+
Filterable fields must be whitelisted with `allowedFilters` in the service options.
|
|
37
|
+
|
|
38
|
+
### Filtering by nested (related) fields
|
|
39
|
+
|
|
40
|
+
Nested relation fields use dot notation. The relation must be listed in `include` (so the join exists) and the full path must be whitelisted in `allowedFilters`:
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
super({
|
|
44
|
+
repository,
|
|
45
|
+
include: ['author', 'author.profile'],
|
|
46
|
+
allowedFilters: ['title', 'author.name', 'author.profile.bio'],
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```http
|
|
51
|
+
GET /post?filter[author.name_cont]=John
|
|
52
|
+
GET /post?filter[author.profile.bio_starts]=Software
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Nested filter keys with dots resolve against the joined alias (`author.name` → `author.name`, `author.profile.bio` → `author_profile.bio`). A nested filter whose join prefix is missing from `include` is silently skipped.
|
|
56
|
+
|
|
57
|
+
### Sorting
|
|
58
|
+
|
|
59
|
+
```http
|
|
60
|
+
GET /post?orderBy=title&orderDirection=ASC
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Sort fields are whitelisted with `allowedSortFields`. Nested sorting works the same way:
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
super({
|
|
67
|
+
repository,
|
|
68
|
+
include: ['author'],
|
|
69
|
+
allowedSortFields: ['id', 'title', 'author.name'],
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```http
|
|
74
|
+
GET /post?orderBy=author.name&orderDirection=DESC
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Nested sort paths also require their join prefix to be in `include`. Cursor pagination keeps its fixed `id` ordering.
|
|
78
|
+
|
|
24
79
|
## Building
|
|
25
80
|
|
|
26
81
|
Run `nx build nest-crud` to build the library.
|
|
@@ -1,3 +1,14 @@
|
|
|
1
1
|
import { ObjectLiteral, SelectQueryBuilder } from 'typeorm';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Resolves a filter/sort field to a query-builder target.
|
|
4
|
+
*
|
|
5
|
+
* - Plain fields resolve against the root alias: `name` -> `e.name`.
|
|
6
|
+
* - Nested fields (dot notation) resolve against a joined alias whose path
|
|
7
|
+
* prefix must be present in `include`: `author.name` -> `author.name`,
|
|
8
|
+
* `userRoles.role.name` -> `userRoles_role.name`.
|
|
9
|
+
* - Returns `null` when the field is unsafe or the join prefix is not
|
|
10
|
+
* configured, so callers can skip the condition entirely.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveQueryTarget(field: string, include?: readonly string[], rootAlias?: string): string | null;
|
|
13
|
+
export declare function applyFilters<Entity extends ObjectLiteral>(qb: SelectQueryBuilder<Entity>, filters: Record<string, unknown> | undefined, allowedFilters: readonly (keyof Entity)[], include?: readonly string[]): void;
|
|
3
14
|
//# sourceMappingURL=filter.helper.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"filter.helper.d.ts","sourceRoot":"","sources":["../../../src/lib/helpers/filter.helper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"filter.helper.d.ts","sourceRoot":"","sources":["../../../src/lib/helpers/filter.helper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AA+C5D;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,SAAS,MAAM,EAAO,EAC/B,SAAS,SAAM,GACd,MAAM,GAAG,IAAI,CAWf;AA6ID,wBAAgB,YAAY,CAAC,MAAM,SAAS,aAAa,EACvD,EAAE,EAAE,kBAAkB,CAAC,MAAM,CAAC,EAC9B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC5C,cAAc,EAAE,SAAS,CAAC,MAAM,MAAM,CAAC,EAAE,EACzC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,GAC1B,IAAI,CAqBN"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveQueryTarget = resolveQueryTarget;
|
|
3
4
|
exports.applyFilters = applyFilters;
|
|
4
|
-
const SAFE_FIELD_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
5
|
+
const SAFE_FIELD_PATTERN = /^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)*$/;
|
|
5
6
|
const toArrayValue = (value) => {
|
|
6
7
|
if (Array.isArray(value)) {
|
|
7
8
|
return value;
|
|
@@ -36,41 +37,63 @@ const parseFilterKey = (rawKey) => {
|
|
|
36
37
|
const operator = rawKey.slice(separatorIndex + 1).toLowerCase();
|
|
37
38
|
return { field, operator };
|
|
38
39
|
};
|
|
39
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Resolves a filter/sort field to a query-builder target.
|
|
42
|
+
*
|
|
43
|
+
* - Plain fields resolve against the root alias: `name` -> `e.name`.
|
|
44
|
+
* - Nested fields (dot notation) resolve against a joined alias whose path
|
|
45
|
+
* prefix must be present in `include`: `author.name` -> `author.name`,
|
|
46
|
+
* `userRoles.role.name` -> `userRoles_role.name`.
|
|
47
|
+
* - Returns `null` when the field is unsafe or the join prefix is not
|
|
48
|
+
* configured, so callers can skip the condition entirely.
|
|
49
|
+
*/
|
|
50
|
+
function resolveQueryTarget(field, include = [], rootAlias = 'e') {
|
|
51
|
+
if (!SAFE_FIELD_PATTERN.test(field))
|
|
52
|
+
return null;
|
|
53
|
+
const parts = field.split('.');
|
|
54
|
+
if (parts.length === 1)
|
|
55
|
+
return `${rootAlias}.${field}`;
|
|
56
|
+
const joinPath = parts.slice(0, -1).join('.');
|
|
57
|
+
if (!include.includes(joinPath))
|
|
58
|
+
return null;
|
|
59
|
+
const alias = joinPath.replace(/\./g, '_');
|
|
60
|
+
return `${alias}.${parts[parts.length - 1]}`;
|
|
61
|
+
}
|
|
62
|
+
const buildCondition = (target, operator, value, paramIndex) => {
|
|
40
63
|
const paramKey = `filter_${paramIndex.current++}`;
|
|
41
64
|
switch (operator) {
|
|
42
65
|
case 'eq':
|
|
43
|
-
return { sql:
|
|
66
|
+
return { sql: `${target} = :${paramKey}`, params: { [paramKey]: value } };
|
|
44
67
|
case 'ne':
|
|
45
|
-
return { sql:
|
|
68
|
+
return { sql: `${target} != :${paramKey}`, params: { [paramKey]: value } };
|
|
46
69
|
case 'cont':
|
|
47
70
|
return {
|
|
48
|
-
sql:
|
|
71
|
+
sql: `${target} ILIKE :${paramKey}`,
|
|
49
72
|
params: { [paramKey]: `%${value}%` },
|
|
50
73
|
};
|
|
51
74
|
case 'notcont':
|
|
52
75
|
return {
|
|
53
|
-
sql:
|
|
76
|
+
sql: `${target} NOT ILIKE :${paramKey}`,
|
|
54
77
|
params: { [paramKey]: `%${value}%` },
|
|
55
78
|
};
|
|
56
79
|
case 'starts':
|
|
57
80
|
return {
|
|
58
|
-
sql:
|
|
81
|
+
sql: `${target} ILIKE :${paramKey}`,
|
|
59
82
|
params: { [paramKey]: `${value}%` },
|
|
60
83
|
};
|
|
61
84
|
case 'ends':
|
|
62
85
|
return {
|
|
63
|
-
sql:
|
|
86
|
+
sql: `${target} ILIKE :${paramKey}`,
|
|
64
87
|
params: { [paramKey]: `%${value}` },
|
|
65
88
|
};
|
|
66
89
|
case 'gte':
|
|
67
|
-
return { sql:
|
|
90
|
+
return { sql: `${target} >= :${paramKey}`, params: { [paramKey]: value } };
|
|
68
91
|
case 'lte':
|
|
69
|
-
return { sql:
|
|
92
|
+
return { sql: `${target} <= :${paramKey}`, params: { [paramKey]: value } };
|
|
70
93
|
case 'gt':
|
|
71
|
-
return { sql:
|
|
94
|
+
return { sql: `${target} > :${paramKey}`, params: { [paramKey]: value } };
|
|
72
95
|
case 'lt':
|
|
73
|
-
return { sql:
|
|
96
|
+
return { sql: `${target} < :${paramKey}`, params: { [paramKey]: value } };
|
|
74
97
|
case 'in':
|
|
75
98
|
case 'nin': {
|
|
76
99
|
const values = toArrayValue(value);
|
|
@@ -78,8 +101,8 @@ const buildCondition = (field, operator, value, paramIndex) => {
|
|
|
78
101
|
return null;
|
|
79
102
|
return {
|
|
80
103
|
sql: operator === 'in'
|
|
81
|
-
?
|
|
82
|
-
:
|
|
104
|
+
? `${target} IN (:...${paramKey})`
|
|
105
|
+
: `${target} NOT IN (:...${paramKey})`,
|
|
83
106
|
params: { [paramKey]: values },
|
|
84
107
|
};
|
|
85
108
|
}
|
|
@@ -87,13 +110,13 @@ const buildCondition = (field, operator, value, paramIndex) => {
|
|
|
87
110
|
const isNull = toBooleanValue(value);
|
|
88
111
|
if (isNull === null)
|
|
89
112
|
return null;
|
|
90
|
-
return { sql: isNull ?
|
|
113
|
+
return { sql: isNull ? `${target} IS NULL` : `${target} IS NOT NULL` };
|
|
91
114
|
}
|
|
92
115
|
default:
|
|
93
116
|
return null;
|
|
94
117
|
}
|
|
95
118
|
};
|
|
96
|
-
const buildExpression = (node, combinator, allowedFilters, paramIndex, params) => {
|
|
119
|
+
const buildExpression = (node, combinator, allowedFilters, include, paramIndex, params) => {
|
|
97
120
|
if (!node || typeof node !== 'object')
|
|
98
121
|
return null;
|
|
99
122
|
const conditions = [];
|
|
@@ -105,7 +128,7 @@ const buildExpression = (node, combinator, allowedFilters, paramIndex, params) =
|
|
|
105
128
|
const nestedCombinator = rawKey;
|
|
106
129
|
const nestedNodes = Array.isArray(value) ? value : [value];
|
|
107
130
|
const nestedExpressions = nestedNodes
|
|
108
|
-
.map((nestedNode) => buildExpression(nestedNode, 'and', allowedFilters, paramIndex, params))
|
|
131
|
+
.map((nestedNode) => buildExpression(nestedNode, 'and', allowedFilters, include, paramIndex, params))
|
|
109
132
|
.filter((sql) => Boolean(sql));
|
|
110
133
|
if (nestedExpressions.length > 0) {
|
|
111
134
|
conditions.push(`(${nestedExpressions.join(` ${nestedCombinator.toUpperCase()} `)})`);
|
|
@@ -116,11 +139,12 @@ const buildExpression = (node, combinator, allowedFilters, paramIndex, params) =
|
|
|
116
139
|
if (!parsed)
|
|
117
140
|
continue;
|
|
118
141
|
const { field, operator } = parsed;
|
|
119
|
-
if (!SAFE_FIELD_PATTERN.test(field))
|
|
120
|
-
continue;
|
|
121
142
|
if (!allowedFilters.includes(field))
|
|
122
143
|
continue;
|
|
123
|
-
const
|
|
144
|
+
const target = resolveQueryTarget(field, include);
|
|
145
|
+
if (!target)
|
|
146
|
+
continue;
|
|
147
|
+
const condition = buildCondition(target, operator, value, paramIndex);
|
|
124
148
|
if (!condition)
|
|
125
149
|
continue;
|
|
126
150
|
if (condition.params)
|
|
@@ -133,11 +157,11 @@ const buildExpression = (node, combinator, allowedFilters, paramIndex, params) =
|
|
|
133
157
|
return conditions[0];
|
|
134
158
|
return `(${conditions.join(` ${combinator.toUpperCase()} `)})`;
|
|
135
159
|
};
|
|
136
|
-
function applyFilters(qb, filters, allowedFilters) {
|
|
160
|
+
function applyFilters(qb, filters, allowedFilters, include) {
|
|
137
161
|
if (!filters)
|
|
138
162
|
return;
|
|
139
163
|
const params = {};
|
|
140
|
-
const expression = buildExpression(filters, 'and', allowedFilters.map(String), { current: 0 }, params);
|
|
164
|
+
const expression = buildExpression(filters, 'and', allowedFilters.map(String), include ?? [], { current: 0 }, params);
|
|
141
165
|
if (!expression)
|
|
142
166
|
return;
|
|
143
167
|
if (Object.keys(params).length > 0) {
|
|
@@ -10,8 +10,8 @@ import { CrudHooks, TransactionConfig } from '../interfaces/hooks.interface';
|
|
|
10
10
|
import { FindMineConfig, OwnershipUser } from '../interfaces/find-mine.interface';
|
|
11
11
|
export interface CrudServiceOptions<Entity extends ObjectLiteral, ResponseDto> extends FindMineConfig<Entity> {
|
|
12
12
|
repository: Repository<Entity>;
|
|
13
|
-
allowedFilters?: readonly (keyof Entity)[];
|
|
14
|
-
allowedSortFields?: readonly (keyof Entity)[];
|
|
13
|
+
allowedFilters?: readonly (keyof Entity | (string & {}))[];
|
|
14
|
+
allowedSortFields?: readonly (keyof Entity | (string & {}))[];
|
|
15
15
|
include?: readonly string[];
|
|
16
16
|
relations?: {
|
|
17
17
|
property: keyof Entity;
|
|
@@ -28,8 +28,8 @@ export interface CrudServiceOptions<Entity extends ObjectLiteral, ResponseDto> e
|
|
|
28
28
|
}
|
|
29
29
|
export declare class NestCrudService<Entity extends ObjectLiteral, CreateDto = Partial<Entity>, UpdateDto = Partial<Entity>, ResponseDto = Entity> implements CrudInterface<CreateDto, UpdateDto, ResponseDto> {
|
|
30
30
|
protected readonly repo: Repository<Entity>;
|
|
31
|
-
protected readonly allowedFilters: readonly (keyof Entity)[];
|
|
32
|
-
protected readonly allowedSortFields: readonly (keyof Entity)[];
|
|
31
|
+
protected readonly allowedFilters: readonly (keyof Entity | (string & {}))[];
|
|
32
|
+
protected readonly allowedSortFields: readonly (keyof Entity | (string & {}))[];
|
|
33
33
|
protected readonly include: readonly string[];
|
|
34
34
|
protected readonly relations: {
|
|
35
35
|
property: keyof Entity;
|
|
@@ -54,6 +54,7 @@ export declare class NestCrudService<Entity extends ObjectLiteral, CreateDto = P
|
|
|
54
54
|
private executeHook;
|
|
55
55
|
private buildRelationsObject;
|
|
56
56
|
private applyIncludeJoins;
|
|
57
|
+
private applyOrderBy;
|
|
57
58
|
private isOwnershipConfigured;
|
|
58
59
|
private canBypassOwnership;
|
|
59
60
|
private enforceOwnershipFor;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nest-crud.service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/nest-crud.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0E,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAC9G,OAAO,EAAe,aAAa,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AACrF,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAE9D,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE/C,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AACnG,OAAO,EAAE,cAAc,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,EAAkB,SAAS,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAC7F,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAQlF,MAAM,WAAW,kBAAkB,CAAC,MAAM,SAAS,aAAa,EAAE,WAAW,CAC3E,SAAQ,cAAc,CAAC,MAAM,CAAC;IAC9B,UAAU,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;IAC/B,cAAc,CAAC,EAAE,SAAS,CAAC,MAAM,MAAM,CAAC,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"nest-crud.service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/nest-crud.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -95,22 +95,36 @@ let NestCrudService = class NestCrudService {
|
|
|
95
95
|
}
|
|
96
96
|
return result;
|
|
97
97
|
}
|
|
98
|
-
applyIncludeJoins(qb) {
|
|
98
|
+
applyIncludeJoins(qb, withSelect = true) {
|
|
99
99
|
if (this.include.length === 0)
|
|
100
100
|
return;
|
|
101
|
+
const join = withSelect
|
|
102
|
+
? (path, alias) => qb.leftJoinAndSelect(path, alias)
|
|
103
|
+
: (path, alias) => qb.leftJoin(path, alias);
|
|
101
104
|
this.include.forEach((relation) => {
|
|
102
105
|
const parts = relation.split('.');
|
|
103
106
|
if (parts.length === 1) {
|
|
104
|
-
|
|
107
|
+
join(`e.${parts[0]}`, parts[0]);
|
|
105
108
|
}
|
|
106
109
|
else {
|
|
107
110
|
const parentAlias = parts.slice(0, -1).join('_');
|
|
108
111
|
const field = parts[parts.length - 1];
|
|
109
112
|
const alias = parts.join('_');
|
|
110
|
-
|
|
113
|
+
join(`${parentAlias}.${field}`, alias);
|
|
111
114
|
}
|
|
112
115
|
});
|
|
113
116
|
}
|
|
117
|
+
applyOrderBy(qb, orderBy, orderDirection) {
|
|
118
|
+
if (!orderBy)
|
|
119
|
+
return;
|
|
120
|
+
const target = (0, filter_helper_1.resolveQueryTarget)(orderBy, this.include.map(String), 'e');
|
|
121
|
+
if (!target)
|
|
122
|
+
return;
|
|
123
|
+
if (this.allowedSortFields.length === 0 ||
|
|
124
|
+
this.allowedSortFields.includes(orderBy)) {
|
|
125
|
+
qb.orderBy(target, orderDirection);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
114
128
|
isOwnershipConfigured() {
|
|
115
129
|
return Boolean(this.userOwnershipField || this.findMineQuery);
|
|
116
130
|
}
|
|
@@ -156,15 +170,9 @@ let NestCrudService = class NestCrudService {
|
|
|
156
170
|
async findAll(query) {
|
|
157
171
|
const qb = this.repo.createQueryBuilder('e');
|
|
158
172
|
this.applyIncludeJoins(qb);
|
|
159
|
-
(0, filter_helper_1.applyFilters)(qb, query.filter, this.allowedFilters);
|
|
173
|
+
(0, filter_helper_1.applyFilters)(qb, query.filter, this.allowedFilters, this.include);
|
|
160
174
|
const paginationMeta = (0, pagination_helper_1.applyPagination)(qb, query);
|
|
161
|
-
|
|
162
|
-
const orderDirection = query.orderDirection === 'ASC' ? 'ASC' : 'DESC';
|
|
163
|
-
if (this.allowedSortFields.length === 0 ||
|
|
164
|
-
this.allowedSortFields.includes(query.orderBy)) {
|
|
165
|
-
qb.orderBy(`e.${query.orderBy}`, orderDirection);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
175
|
+
this.applyOrderBy(qb, query.orderBy, query.orderDirection === 'ASC' ? 'ASC' : 'DESC');
|
|
168
176
|
const [entities, total] = await qb.getManyAndCount();
|
|
169
177
|
const data = this.toResponseDto
|
|
170
178
|
? this.toResponseDto(entities)
|
|
@@ -180,15 +188,9 @@ let NestCrudService = class NestCrudService {
|
|
|
180
188
|
const qb = this.repo.createQueryBuilder('e');
|
|
181
189
|
this.applyOwnershipCondition(qb, userId);
|
|
182
190
|
this.applyIncludeJoins(qb);
|
|
183
|
-
(0, filter_helper_1.applyFilters)(qb, query.filter, this.allowedFilters);
|
|
191
|
+
(0, filter_helper_1.applyFilters)(qb, query.filter, this.allowedFilters, this.include);
|
|
184
192
|
const paginationMeta = (0, pagination_helper_1.applyPagination)(qb, query);
|
|
185
|
-
|
|
186
|
-
const orderDirection = query.orderDirection === 'ASC' ? 'ASC' : 'DESC';
|
|
187
|
-
if (this.allowedSortFields.length === 0 ||
|
|
188
|
-
this.allowedSortFields.includes(query.orderBy)) {
|
|
189
|
-
qb.orderBy(`e.${query.orderBy}`, orderDirection);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
193
|
+
this.applyOrderBy(qb, query.orderBy, query.orderDirection === 'ASC' ? 'ASC' : 'DESC');
|
|
192
194
|
const [entities, total] = await qb.getManyAndCount();
|
|
193
195
|
const data = this.toResponseDto
|
|
194
196
|
? this.toResponseDto(entities)
|
|
@@ -205,7 +207,7 @@ let NestCrudService = class NestCrudService {
|
|
|
205
207
|
// Join relations
|
|
206
208
|
this.applyIncludeJoins(qb);
|
|
207
209
|
// Apply filters
|
|
208
|
-
(0, filter_helper_1.applyFilters)(qb, query.filter, this.allowedFilters);
|
|
210
|
+
(0, filter_helper_1.applyFilters)(qb, query.filter, this.allowedFilters, this.include);
|
|
209
211
|
// Apply cursor filter
|
|
210
212
|
if (query.cursor) {
|
|
211
213
|
const decoded = (0, cursor_pagination_helper_1.decodeCursor)(query.cursor, strategy);
|
|
@@ -225,15 +227,8 @@ let NestCrudService = class NestCrudService {
|
|
|
225
227
|
if (query.includeTotal) {
|
|
226
228
|
// Build a clean count query (reuse same filters but no cursor/order/take)
|
|
227
229
|
const countQb = this.repo.createQueryBuilder('e');
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const parts = relation.split('.');
|
|
231
|
-
if (parts.length === 1) {
|
|
232
|
-
countQb.leftJoin(`e.${parts[0]}`, parts[0]);
|
|
233
|
-
}
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
(0, filter_helper_1.applyFilters)(countQb, query.filter, this.allowedFilters);
|
|
230
|
+
this.applyIncludeJoins(countQb, false);
|
|
231
|
+
(0, filter_helper_1.applyFilters)(countQb, query.filter, this.allowedFilters, this.include);
|
|
237
232
|
total = await countQb.getCount();
|
|
238
233
|
}
|
|
239
234
|
const response = this.toResponseDto
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crud-service.test-suites.d.ts","sourceRoot":"","sources":["../../../src/lib/testing/crud-service.test-suites.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAExC,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAGtE,wBAAgB,gBAAgB,CAC9B,OAAO,SAAS,aAAa,EAC7B,UAAU,EACV,UAAU,EACV,YAAY,EACZ,MAAM,EAAE,cAAc,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,CAAC,GAAG,eAAe,
|
|
1
|
+
{"version":3,"file":"crud-service.test-suites.d.ts","sourceRoot":"","sources":["../../../src/lib/testing/crud-service.test-suites.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAExC,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAGtE,wBAAgB,gBAAgB,CAC9B,OAAO,SAAS,aAAa,EAC7B,UAAU,EACV,UAAU,EACV,YAAY,EACZ,MAAM,EAAE,cAAc,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,CAAC,GAAG,eAAe,CA6axF"}
|
|
@@ -59,6 +59,44 @@ function crudServiceTests(config) {
|
|
|
59
59
|
});
|
|
60
60
|
expect(lastQb.andWhere).toHaveBeenCalled();
|
|
61
61
|
});
|
|
62
|
+
it('should apply nested filters via joined aliases', async () => {
|
|
63
|
+
const nestedFilters = (config.allowedFilters ?? []).filter((field) => String(field).includes('.'));
|
|
64
|
+
if (nestedFilters.length === 0)
|
|
65
|
+
return;
|
|
66
|
+
lastQb = (0, mock_repository_1.createMockQb)();
|
|
67
|
+
lastQb.getManyAndCount.mockResolvedValue([[], 0]);
|
|
68
|
+
repository.createQueryBuilder.mockReturnValue(lastQb);
|
|
69
|
+
const nestedField = String(nestedFilters[0]);
|
|
70
|
+
await service.findAll({
|
|
71
|
+
page: 1,
|
|
72
|
+
limit: 10,
|
|
73
|
+
filter: { [`${nestedField}_cont`]: 'test' },
|
|
74
|
+
});
|
|
75
|
+
const [sql] = lastQb.andWhere.mock.calls[0];
|
|
76
|
+
const parts = nestedField.split('.');
|
|
77
|
+
const alias = parts.slice(0, -1).join('_');
|
|
78
|
+
const column = parts[parts.length - 1];
|
|
79
|
+
expect(sql).toContain(`${alias}.${column} ILIKE`);
|
|
80
|
+
});
|
|
81
|
+
it('should sort by nested fields via joined aliases', async () => {
|
|
82
|
+
const nestedSorts = (config.allowedSortFields ?? []).filter((field) => String(field).includes('.'));
|
|
83
|
+
if (nestedSorts.length === 0)
|
|
84
|
+
return;
|
|
85
|
+
lastQb = (0, mock_repository_1.createMockQb)();
|
|
86
|
+
lastQb.getManyAndCount.mockResolvedValue([[], 0]);
|
|
87
|
+
repository.createQueryBuilder.mockReturnValue(lastQb);
|
|
88
|
+
const nestedField = String(nestedSorts[0]);
|
|
89
|
+
await service.findAll({
|
|
90
|
+
page: 1,
|
|
91
|
+
limit: 10,
|
|
92
|
+
orderBy: nestedField,
|
|
93
|
+
orderDirection: 'ASC',
|
|
94
|
+
});
|
|
95
|
+
const parts = nestedField.split('.');
|
|
96
|
+
const alias = parts.slice(0, -1).join('_');
|
|
97
|
+
const column = parts[parts.length - 1];
|
|
98
|
+
expect(lastQb.orderBy).toHaveBeenCalledWith(`${alias}.${column}`, 'ASC');
|
|
99
|
+
});
|
|
62
100
|
it('should join include relations via leftJoinAndSelect', async () => {
|
|
63
101
|
if (!config.include || config.include.length === 0)
|
|
64
102
|
return;
|
|
@@ -246,6 +284,26 @@ function crudServiceTests(config) {
|
|
|
246
284
|
});
|
|
247
285
|
expect(result.meta).toHaveProperty('total');
|
|
248
286
|
});
|
|
287
|
+
it('should join nested relations on the count query when includeTotal is true', async () => {
|
|
288
|
+
const nestedIncludes = (config.include ?? []).filter((relation) => relation.includes('.'));
|
|
289
|
+
if (nestedIncludes.length === 0)
|
|
290
|
+
return;
|
|
291
|
+
lastQb = (0, mock_repository_1.createMockQb)();
|
|
292
|
+
lastQb.getMany.mockResolvedValue([]);
|
|
293
|
+
repository.createQueryBuilder.mockReturnValue(lastQb);
|
|
294
|
+
const countQb = (0, mock_repository_1.createMockQb)();
|
|
295
|
+
countQb.getCount.mockResolvedValue(0);
|
|
296
|
+
repository.createQueryBuilder.mockReturnValueOnce(lastQb);
|
|
297
|
+
repository.createQueryBuilder.mockReturnValueOnce(countQb);
|
|
298
|
+
await service.findAllWithCursor({ limit: 10, includeTotal: true });
|
|
299
|
+
for (const relation of nestedIncludes) {
|
|
300
|
+
const parts = relation.split('.');
|
|
301
|
+
const parentAlias = parts.slice(0, -1).join('_');
|
|
302
|
+
const field = parts[parts.length - 1];
|
|
303
|
+
const alias = parts.join('_');
|
|
304
|
+
expect(countQb.leftJoin).toHaveBeenCalledWith(`${parentAlias}.${field}`, alias);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
249
307
|
});
|
|
250
308
|
describe('findAuditLogs', () => {
|
|
251
309
|
it('should query audit logs with pagination', async () => {
|
|
@@ -8,8 +8,8 @@ export interface CrudTestConfig<TEntity extends ObjectLiteral, TCreateDto, TUpda
|
|
|
8
8
|
createDto?: Type<TCreateDto>;
|
|
9
9
|
updateDto?: Type<TUpdateDto>;
|
|
10
10
|
responseDto?: Type<TResponseDto>;
|
|
11
|
-
allowedFilters?: readonly (keyof TEntity)[];
|
|
12
|
-
allowedSortFields?: readonly (keyof TEntity)[];
|
|
11
|
+
allowedFilters?: readonly (keyof TEntity | (string & {}))[];
|
|
12
|
+
allowedSortFields?: readonly (keyof TEntity | (string & {}))[];
|
|
13
13
|
include?: readonly string[];
|
|
14
14
|
userOwnershipField?: keyof TEntity;
|
|
15
15
|
findMineQuery?: (qb: any, userId: string | number) => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.interface.d.ts","sourceRoot":"","sources":["../../../src/lib/testing/testing.interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAE5D,MAAM,WAAW,cAAc,CAC7B,OAAO,SAAS,aAAa,EAC7B,UAAU,EACV,UAAU,EACV,YAAY;IAEZ,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IACtB,YAAY,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;IACxB,SAAS,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IAC7B,SAAS,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IAC7B,WAAW,CAAC,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;IACjC,cAAc,CAAC,EAAE,SAAS,CAAC,MAAM,OAAO,CAAC,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"testing.interface.d.ts","sourceRoot":"","sources":["../../../src/lib/testing/testing.interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAE5D,MAAM,WAAW,cAAc,CAC7B,OAAO,SAAS,aAAa,EAC7B,UAAU,EACV,UAAU,EACV,YAAY;IAEZ,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IACtB,YAAY,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;IACxB,SAAS,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IAC7B,SAAS,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IAC7B,WAAW,CAAC,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;IACjC,cAAc,CAAC,EAAE,SAAS,CAAC,MAAM,OAAO,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC;IAC5D,iBAAiB,CAAC,EAAE,SAAS,CAAC,MAAM,OAAO,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC;IAC/D,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,kBAAkB,CAAC,EAAE,MAAM,OAAO,CAAC;IACnC,aAAa,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,KAAK,IAAI,CAAC;IAC3D,iBAAiB,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;IAC5C,KAAK,CAAC,EAAE,kBAAkB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,OAAO,CAAC,CAAC;IAC3D,aAAa,CAAC,EAAE,CACd,MAAM,EAAE,OAAO,GAAG,OAAO,EAAE,KACxB,YAAY,GAAG,YAAY,EAAE,CAAC;IACnC,SAAS,CAAC,EAAE,kBAAkB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,WAAW,CAAC,CAAC;IACnE,IAAI,EAAE;QACJ,aAAa,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;QACnC,aAAa,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;QACnC,UAAU,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;QAC9B,YAAY,CAAC,EAAE,OAAO,EAAE,CAAC;QACzB,iBAAiB,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;KAC/D,CAAC;CACH;AAED,MAAM,WAAW,wBAAwB,CACvC,OAAO,SAAS,aAAa,EAC7B,UAAU,EACV,UAAU,EACV,YAAY,CACZ,SAAQ,IAAI,CACV,cAAc,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,CAAC,EAC7D,cAAc,CACf;IACD,iBAAiB,EAAE,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC;IACnC,YAAY,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,GAAG,CAAC;IACZ,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC7B,YAAY,EAAE,MAAM,gBAAgB,CAAC;CACtC;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC;IACjB,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC;IACpB,iBAAiB,EAAE,IAAI,CAAC,IAAI,CAAC;IAC7B,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC;IACpB,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC;IACnB,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC;IAChB,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC;IAChB,eAAe,EAAE,IAAI,CAAC,IAAI,CAAC;IAC3B,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC;IACnB,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC;IAClB,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC;IACpB,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC;CACtB"}
|