@nest-util/nest-crud 1.1.2 → 1.1.4
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/interfaces/find-mine.interface.d.ts +7 -0
- package/dist/lib/interfaces/find-mine.interface.d.ts.map +1 -1
- package/dist/lib/services/nest-crud.service.d.ts +7 -4
- package/dist/lib/services/nest-crud.service.d.ts.map +1 -1
- package/dist/lib/services/nest-crud.service.js +59 -32
- 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) {
|
|
@@ -28,5 +28,12 @@ export interface FindMineConfig<TEntity extends ObjectLiteral> {
|
|
|
28
28
|
* Takes precedence alongside ownershipBypassPermissions.
|
|
29
29
|
*/
|
|
30
30
|
ownershipBypass?: (user: OwnershipUser) => boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Permission string that grants full access and bypasses ownership checks,
|
|
33
|
+
* mirroring `rbac.superAdminPermission` from @nest-util/nest-auth. When the
|
|
34
|
+
* user's resolved permissions (direct or via roles) include this string,
|
|
35
|
+
* ownership checks are skipped. Example: 'admin.access'.
|
|
36
|
+
*/
|
|
37
|
+
superAdminPermission?: string;
|
|
31
38
|
}
|
|
32
39
|
//# sourceMappingURL=find-mine.interface.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"find-mine.interface.d.ts","sourceRoot":"","sources":["../../../src/lib/interfaces/find-mine.interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAE5D;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,cAAc,CAAC,OAAO,SAAS,aAAa;IAC3D,kBAAkB,CAAC,EAAE,MAAM,OAAO,CAAC;IACnC,aAAa,CAAC,EAAE,CAAC,EAAE,EAAE,kBAAkB,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,KAAK,IAAI,CAAC;IAEnF;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B;;;OAGG;IACH,0BAA0B,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAE/C;;;OAGG;IACH,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"find-mine.interface.d.ts","sourceRoot":"","sources":["../../../src/lib/interfaces/find-mine.interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAE5D;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,cAAc,CAAC,OAAO,SAAS,aAAa;IAC3D,kBAAkB,CAAC,EAAE,MAAM,OAAO,CAAC;IACnC,aAAa,CAAC,EAAE,CAAC,EAAE,EAAE,kBAAkB,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,KAAK,IAAI,CAAC;IAEnF;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B;;;OAGG;IACH,0BAA0B,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAE/C;;;OAGG;IACH,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,OAAO,CAAC;IAEnD;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B"}
|
|
@@ -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;
|
|
@@ -48,13 +48,16 @@ export declare class NestCrudService<Entity extends ObjectLiteral, CreateDto = P
|
|
|
48
48
|
protected readonly enforceOwnership: boolean;
|
|
49
49
|
protected readonly ownershipBypassPermissions: readonly string[];
|
|
50
50
|
protected readonly ownershipBypass?: (user: OwnershipUser) => boolean;
|
|
51
|
+
protected readonly superAdminPermission?: string;
|
|
51
52
|
constructor(options: CrudServiceOptions<Entity, ResponseDto>);
|
|
52
53
|
private resolveRelations;
|
|
53
54
|
private executeInTransaction;
|
|
54
55
|
private executeHook;
|
|
55
56
|
private buildRelationsObject;
|
|
56
57
|
private applyIncludeJoins;
|
|
58
|
+
private applyOrderBy;
|
|
57
59
|
private isOwnershipConfigured;
|
|
60
|
+
private resolveUserPermissions;
|
|
58
61
|
private canBypassOwnership;
|
|
59
62
|
private enforceOwnershipFor;
|
|
60
63
|
private applyOwnershipCondition;
|
|
@@ -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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"}
|
|
@@ -27,6 +27,7 @@ let NestCrudService = class NestCrudService {
|
|
|
27
27
|
this.enforceOwnership = options.enforceOwnership ?? false;
|
|
28
28
|
this.ownershipBypassPermissions = options.ownershipBypassPermissions ?? [];
|
|
29
29
|
this.ownershipBypass = options.ownershipBypass;
|
|
30
|
+
this.superAdminPermission = options.superAdminPermission;
|
|
30
31
|
}
|
|
31
32
|
async resolveRelations(payload) {
|
|
32
33
|
if (!this.relations.length)
|
|
@@ -95,35 +96,80 @@ let NestCrudService = class NestCrudService {
|
|
|
95
96
|
}
|
|
96
97
|
return result;
|
|
97
98
|
}
|
|
98
|
-
applyIncludeJoins(qb) {
|
|
99
|
+
applyIncludeJoins(qb, withSelect = true) {
|
|
99
100
|
if (this.include.length === 0)
|
|
100
101
|
return;
|
|
102
|
+
const join = withSelect
|
|
103
|
+
? (path, alias) => qb.leftJoinAndSelect(path, alias)
|
|
104
|
+
: (path, alias) => qb.leftJoin(path, alias);
|
|
101
105
|
this.include.forEach((relation) => {
|
|
102
106
|
const parts = relation.split('.');
|
|
103
107
|
if (parts.length === 1) {
|
|
104
|
-
|
|
108
|
+
join(`e.${parts[0]}`, parts[0]);
|
|
105
109
|
}
|
|
106
110
|
else {
|
|
107
111
|
const parentAlias = parts.slice(0, -1).join('_');
|
|
108
112
|
const field = parts[parts.length - 1];
|
|
109
113
|
const alias = parts.join('_');
|
|
110
|
-
|
|
114
|
+
join(`${parentAlias}.${field}`, alias);
|
|
111
115
|
}
|
|
112
116
|
});
|
|
113
117
|
}
|
|
118
|
+
applyOrderBy(qb, orderBy, orderDirection) {
|
|
119
|
+
if (!orderBy)
|
|
120
|
+
return;
|
|
121
|
+
const target = (0, filter_helper_1.resolveQueryTarget)(orderBy, this.include.map(String), 'e');
|
|
122
|
+
if (!target)
|
|
123
|
+
return;
|
|
124
|
+
if (this.allowedSortFields.length === 0 ||
|
|
125
|
+
this.allowedSortFields.includes(orderBy)) {
|
|
126
|
+
qb.orderBy(target, orderDirection);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
114
129
|
isOwnershipConfigured() {
|
|
115
130
|
return Boolean(this.userOwnershipField || this.findMineQuery);
|
|
116
131
|
}
|
|
132
|
+
resolveUserPermissions(user) {
|
|
133
|
+
const out = new Set();
|
|
134
|
+
const add = (value) => {
|
|
135
|
+
if (Array.isArray(value)) {
|
|
136
|
+
value
|
|
137
|
+
.filter((item) => typeof item === 'string')
|
|
138
|
+
.forEach((permission) => out.add(permission));
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
add(user.permissions);
|
|
142
|
+
for (const rolesKey of ['userRoles', 'roles']) {
|
|
143
|
+
const rows = user[rolesKey];
|
|
144
|
+
if (!Array.isArray(rows))
|
|
145
|
+
continue;
|
|
146
|
+
for (const row of rows) {
|
|
147
|
+
if (!row || typeof row !== 'object')
|
|
148
|
+
continue;
|
|
149
|
+
const roleLike = row;
|
|
150
|
+
add(roleLike.permissions);
|
|
151
|
+
const nested = roleLike.role;
|
|
152
|
+
if (nested && typeof nested === 'object') {
|
|
153
|
+
add(nested.permissions);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return [...out];
|
|
158
|
+
}
|
|
117
159
|
canBypassOwnership(user) {
|
|
118
160
|
if (!user)
|
|
119
161
|
return false;
|
|
162
|
+
if (this.superAdminPermission) {
|
|
163
|
+
const resolved = this.resolveUserPermissions(user);
|
|
164
|
+
if (resolved.includes(this.superAdminPermission)) {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
120
168
|
if (this.ownershipBypass && this.ownershipBypass(user)) {
|
|
121
169
|
return true;
|
|
122
170
|
}
|
|
123
171
|
if (this.ownershipBypassPermissions.length > 0) {
|
|
124
|
-
const userPermissions =
|
|
125
|
-
? user.permissions
|
|
126
|
-
: [];
|
|
172
|
+
const userPermissions = this.resolveUserPermissions(user);
|
|
127
173
|
return this.ownershipBypassPermissions.some((permission) => userPermissions.includes(permission));
|
|
128
174
|
}
|
|
129
175
|
return false;
|
|
@@ -156,15 +202,9 @@ let NestCrudService = class NestCrudService {
|
|
|
156
202
|
async findAll(query) {
|
|
157
203
|
const qb = this.repo.createQueryBuilder('e');
|
|
158
204
|
this.applyIncludeJoins(qb);
|
|
159
|
-
(0, filter_helper_1.applyFilters)(qb, query.filter, this.allowedFilters);
|
|
205
|
+
(0, filter_helper_1.applyFilters)(qb, query.filter, this.allowedFilters, this.include);
|
|
160
206
|
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
|
-
}
|
|
207
|
+
this.applyOrderBy(qb, query.orderBy, query.orderDirection === 'ASC' ? 'ASC' : 'DESC');
|
|
168
208
|
const [entities, total] = await qb.getManyAndCount();
|
|
169
209
|
const data = this.toResponseDto
|
|
170
210
|
? this.toResponseDto(entities)
|
|
@@ -180,15 +220,9 @@ let NestCrudService = class NestCrudService {
|
|
|
180
220
|
const qb = this.repo.createQueryBuilder('e');
|
|
181
221
|
this.applyOwnershipCondition(qb, userId);
|
|
182
222
|
this.applyIncludeJoins(qb);
|
|
183
|
-
(0, filter_helper_1.applyFilters)(qb, query.filter, this.allowedFilters);
|
|
223
|
+
(0, filter_helper_1.applyFilters)(qb, query.filter, this.allowedFilters, this.include);
|
|
184
224
|
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
|
-
}
|
|
225
|
+
this.applyOrderBy(qb, query.orderBy, query.orderDirection === 'ASC' ? 'ASC' : 'DESC');
|
|
192
226
|
const [entities, total] = await qb.getManyAndCount();
|
|
193
227
|
const data = this.toResponseDto
|
|
194
228
|
? this.toResponseDto(entities)
|
|
@@ -205,7 +239,7 @@ let NestCrudService = class NestCrudService {
|
|
|
205
239
|
// Join relations
|
|
206
240
|
this.applyIncludeJoins(qb);
|
|
207
241
|
// Apply filters
|
|
208
|
-
(0, filter_helper_1.applyFilters)(qb, query.filter, this.allowedFilters);
|
|
242
|
+
(0, filter_helper_1.applyFilters)(qb, query.filter, this.allowedFilters, this.include);
|
|
209
243
|
// Apply cursor filter
|
|
210
244
|
if (query.cursor) {
|
|
211
245
|
const decoded = (0, cursor_pagination_helper_1.decodeCursor)(query.cursor, strategy);
|
|
@@ -225,15 +259,8 @@ let NestCrudService = class NestCrudService {
|
|
|
225
259
|
if (query.includeTotal) {
|
|
226
260
|
// Build a clean count query (reuse same filters but no cursor/order/take)
|
|
227
261
|
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);
|
|
262
|
+
this.applyIncludeJoins(countQb, false);
|
|
263
|
+
(0, filter_helper_1.applyFilters)(countQb, query.filter, this.allowedFilters, this.include);
|
|
237
264
|
total = await countQb.getCount();
|
|
238
265
|
}
|
|
239
266
|
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":"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"}
|