@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 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
- export declare function applyFilters<Entity extends ObjectLiteral>(qb: SelectQueryBuilder<Entity>, filters: Record<string, unknown> | undefined, allowedFilters: readonly (keyof Entity)[]): void;
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;AAsL5D,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,GACxC,IAAI,CAoBN"}
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
- const buildCondition = (field, operator, value, paramIndex) => {
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: `e.${field} = :${paramKey}`, params: { [paramKey]: value } };
66
+ return { sql: `${target} = :${paramKey}`, params: { [paramKey]: value } };
44
67
  case 'ne':
45
- return { sql: `e.${field} != :${paramKey}`, params: { [paramKey]: value } };
68
+ return { sql: `${target} != :${paramKey}`, params: { [paramKey]: value } };
46
69
  case 'cont':
47
70
  return {
48
- sql: `e.${field} ILIKE :${paramKey}`,
71
+ sql: `${target} ILIKE :${paramKey}`,
49
72
  params: { [paramKey]: `%${value}%` },
50
73
  };
51
74
  case 'notcont':
52
75
  return {
53
- sql: `e.${field} NOT ILIKE :${paramKey}`,
76
+ sql: `${target} NOT ILIKE :${paramKey}`,
54
77
  params: { [paramKey]: `%${value}%` },
55
78
  };
56
79
  case 'starts':
57
80
  return {
58
- sql: `e.${field} ILIKE :${paramKey}`,
81
+ sql: `${target} ILIKE :${paramKey}`,
59
82
  params: { [paramKey]: `${value}%` },
60
83
  };
61
84
  case 'ends':
62
85
  return {
63
- sql: `e.${field} ILIKE :${paramKey}`,
86
+ sql: `${target} ILIKE :${paramKey}`,
64
87
  params: { [paramKey]: `%${value}` },
65
88
  };
66
89
  case 'gte':
67
- return { sql: `e.${field} >= :${paramKey}`, params: { [paramKey]: value } };
90
+ return { sql: `${target} >= :${paramKey}`, params: { [paramKey]: value } };
68
91
  case 'lte':
69
- return { sql: `e.${field} <= :${paramKey}`, params: { [paramKey]: value } };
92
+ return { sql: `${target} <= :${paramKey}`, params: { [paramKey]: value } };
70
93
  case 'gt':
71
- return { sql: `e.${field} > :${paramKey}`, params: { [paramKey]: value } };
94
+ return { sql: `${target} > :${paramKey}`, params: { [paramKey]: value } };
72
95
  case 'lt':
73
- return { sql: `e.${field} < :${paramKey}`, params: { [paramKey]: value } };
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
- ? `e.${field} IN (:...${paramKey})`
82
- : `e.${field} NOT IN (:...${paramKey})`,
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 ? `e.${field} IS NULL` : `e.${field} IS NOT NULL` };
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 condition = buildCondition(field, operator, value, paramIndex);
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;CACpD"}
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":"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"}
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
- qb.leftJoinAndSelect(`e.${parts[0]}`, parts[0]);
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
- qb.leftJoinAndSelect(`${parentAlias}.${field}`, alias);
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 = Array.isArray(user.permissions)
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
- if (query.orderBy) {
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
- if (query.orderBy) {
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
- if (this.include.length > 0) {
229
- this.include.forEach((relation) => {
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,CA6VxF"}
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;IAC5C,iBAAiB,CAAC,EAAE,SAAS,CAAC,MAAM,OAAO,CAAC,EAAE,CAAC;IAC/C,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"}
1
+ {"version":3,"file":"testing.interface.d.ts","sourceRoot":"","sources":["../../../src/lib/testing/testing.interface.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nest-util/nest-crud",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",