@nest-util/nest-crud 1.0.8 → 1.1.0
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/dist/lib/controllers/nest-crud.controller.d.ts +4 -3
- package/dist/lib/controllers/nest-crud.controller.d.ts.map +1 -1
- package/dist/lib/controllers/nest-crud.controller.js +12 -9
- package/dist/lib/interfaces/crud.interface.d.ts +4 -3
- package/dist/lib/interfaces/crud.interface.d.ts.map +1 -1
- package/dist/lib/interfaces/find-mine.interface.d.ts +26 -0
- package/dist/lib/interfaces/find-mine.interface.d.ts.map +1 -1
- package/dist/lib/services/nest-crud.service.d.ts +13 -4
- package/dist/lib/services/nest-crud.service.d.ts.map +1 -1
- package/dist/lib/services/nest-crud.service.js +100 -65
- package/dist/lib/testing/crud-controller.test-suites.js +3 -3
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ import { CrudEndpoint, CrudInterface } from '../interfaces/crud.interface';
|
|
|
3
3
|
import { PaginationDto } from '../dtos/pagination.dto';
|
|
4
4
|
import { FilterDto } from '../dtos/filter.dto';
|
|
5
5
|
import { ListAuditLogsDto } from '../dtos/list-audit-logs.dto';
|
|
6
|
+
import type { OwnershipUser } from '../interfaces/find-mine.interface';
|
|
6
7
|
export declare const AUTH_PERMISSIONS_METADATA_KEY = "auth:permissions";
|
|
7
8
|
export type CrudEndpointPermissions = string | readonly string[];
|
|
8
9
|
export type CrudPermissionsMap = Partial<Record<CrudEndpoint, CrudEndpointPermissions>>;
|
|
@@ -16,10 +17,10 @@ export interface IBaseController<CD, UD, RD> {
|
|
|
16
17
|
data: RD[];
|
|
17
18
|
meta?: unknown;
|
|
18
19
|
} | RD[]>;
|
|
19
|
-
findOne(id: number): Promise<RD>;
|
|
20
|
+
findOne(id: number, user?: OwnershipUser): Promise<RD>;
|
|
20
21
|
create(dto: CD): Promise<RD>;
|
|
21
|
-
update(id: number, dto: UD): Promise<RD>;
|
|
22
|
-
remove(id: number): Promise<boolean>;
|
|
22
|
+
update(id: number, dto: UD, user?: OwnershipUser): Promise<RD>;
|
|
23
|
+
remove(id: number, user?: OwnershipUser): Promise<boolean>;
|
|
23
24
|
findAuditLogs?(query: ListAuditLogsDto): Promise<unknown>;
|
|
24
25
|
}
|
|
25
26
|
export declare function CreateNestedCrudController<CD, UD, RD>(createDto: Type<CD>, updateDto: Type<UD>, responseDto: Type<RD>, options?: CrudControllerFactoryOptions): Type<IBaseController<CD, UD, RD>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nest-crud.controller.d.ts","sourceRoot":"","sources":["../../../src/lib/controllers/nest-crud.controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAUL,IAAI,EACL,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC3E,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAEvD,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"nest-crud.controller.d.ts","sourceRoot":"","sources":["../../../src/lib/controllers/nest-crud.controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAUL,IAAI,EACL,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC3E,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAEvD,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAE/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAEvE,eAAO,MAAM,6BAA6B,qBAAqB,CAAC;AAEhE,MAAM,MAAM,uBAAuB,GAAG,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;AAEjE,MAAM,MAAM,kBAAkB,GAAG,OAAO,CACtC,MAAM,CAAC,YAAY,EAAE,uBAAuB,CAAC,CAC9C,CAAC;AAEF,MAAM,WAAW,4BAA4B;IAC3C,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,eAAe,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE;IACzC,OAAO,EAAE,aAAa,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;IACnC,OAAO,CACL,KAAK,EAAE,aAAa,GAAG,SAAS,GAC/B,OAAO,CAAC;QAAE,IAAI,EAAE,EAAE,EAAE,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,EAAE,EAAE,CAAC,CAAC;IAClD,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC;IACvD,MAAM,CAAC,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC;IAC/D,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3D,aAAa,CAAC,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC3D;AAyDD,wBAAgB,0BAA0B,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EACnD,SAAS,EAAE,IAAI,CAAC,EAAE,CAAC,EACnB,SAAS,EAAE,IAAI,CAAC,EAAE,CAAC,EACnB,WAAW,EAAE,IAAI,CAAC,EAAE,CAAC,EACrB,OAAO,CAAC,EAAE,4BAA4B,GACrC,IAAI,CAAC,eAAe,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,CAwInC"}
|
|
@@ -72,13 +72,13 @@ function CreateNestedCrudController(createDto, updateDto, responseDto, options)
|
|
|
72
72
|
this.ensureEndpointEnabled('create');
|
|
73
73
|
return this.service.create(dto);
|
|
74
74
|
}
|
|
75
|
-
update(id, dto) {
|
|
75
|
+
update(id, dto, user) {
|
|
76
76
|
this.ensureEndpointEnabled('update');
|
|
77
|
-
return this.service.update(id, dto);
|
|
77
|
+
return this.service.update(id, dto, user);
|
|
78
78
|
}
|
|
79
|
-
remove(id) {
|
|
79
|
+
remove(id, user) {
|
|
80
80
|
this.ensureEndpointEnabled('remove');
|
|
81
|
-
return this.service.remove(id);
|
|
81
|
+
return this.service.remove(id, user);
|
|
82
82
|
}
|
|
83
83
|
findAuditLogs(query) {
|
|
84
84
|
this.ensureEndpointEnabled('findAuditLogs');
|
|
@@ -87,9 +87,9 @@ function CreateNestedCrudController(createDto, updateDto, responseDto, options)
|
|
|
87
87
|
}
|
|
88
88
|
return this.service.findAuditLogs(query);
|
|
89
89
|
}
|
|
90
|
-
findOne(id) {
|
|
90
|
+
findOne(id, user) {
|
|
91
91
|
this.ensureEndpointEnabled('findOne');
|
|
92
|
-
return this.service.findOne(id);
|
|
92
|
+
return this.service.findOne(id, user);
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
tslib_1.__decorate([
|
|
@@ -160,8 +160,9 @@ function CreateNestedCrudController(createDto, updateDto, responseDto, options)
|
|
|
160
160
|
(0, swagger_1.ApiResponse)({ type: responseDto }),
|
|
161
161
|
tslib_1.__param(0, (0, common_1.Param)('id', common_1.ParseIntPipe)),
|
|
162
162
|
tslib_1.__param(1, (0, common_1.Body)()),
|
|
163
|
+
tslib_1.__param(2, (0, nest_auth_1.CurrentUser)()),
|
|
163
164
|
tslib_1.__metadata("design:type", Function),
|
|
164
|
-
tslib_1.__metadata("design:paramtypes", [Number, Object]),
|
|
165
|
+
tslib_1.__metadata("design:paramtypes", [Number, Object, Object]),
|
|
165
166
|
tslib_1.__metadata("design:returntype", void 0)
|
|
166
167
|
], BaseController.prototype, "update", null);
|
|
167
168
|
tslib_1.__decorate([
|
|
@@ -169,8 +170,9 @@ function CreateNestedCrudController(createDto, updateDto, responseDto, options)
|
|
|
169
170
|
(0, response_message_decorator_1.Message)('deleted'),
|
|
170
171
|
(0, audit_log_decorator_1.Audit)({ action: 'DELETE' }),
|
|
171
172
|
tslib_1.__param(0, (0, common_1.Param)('id', common_1.ParseIntPipe)),
|
|
173
|
+
tslib_1.__param(1, (0, nest_auth_1.CurrentUser)()),
|
|
172
174
|
tslib_1.__metadata("design:type", Function),
|
|
173
|
-
tslib_1.__metadata("design:paramtypes", [Number]),
|
|
175
|
+
tslib_1.__metadata("design:paramtypes", [Number, Object]),
|
|
174
176
|
tslib_1.__metadata("design:returntype", void 0)
|
|
175
177
|
], BaseController.prototype, "remove", null);
|
|
176
178
|
tslib_1.__decorate([
|
|
@@ -186,8 +188,9 @@ function CreateNestedCrudController(createDto, updateDto, responseDto, options)
|
|
|
186
188
|
(0, response_message_decorator_1.Message)('fetched'),
|
|
187
189
|
(0, swagger_1.ApiResponse)({ type: responseDto }),
|
|
188
190
|
tslib_1.__param(0, (0, common_1.Param)('id', common_1.ParseIntPipe)),
|
|
191
|
+
tslib_1.__param(1, (0, nest_auth_1.CurrentUser)()),
|
|
189
192
|
tslib_1.__metadata("design:type", Function),
|
|
190
|
-
tslib_1.__metadata("design:paramtypes", [Number]),
|
|
193
|
+
tslib_1.__metadata("design:paramtypes", [Number, Object]),
|
|
191
194
|
tslib_1.__metadata("design:returntype", void 0)
|
|
192
195
|
], BaseController.prototype, "findOne", null);
|
|
193
196
|
applyPermissionMetadata(BaseController, options?.permissions);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FilterDto } from '../dtos/filter.dto';
|
|
2
2
|
import { PaginationDto } from '../dtos/pagination.dto';
|
|
3
3
|
import { CursorPaginationDto } from '../dtos/cursor-pagination.dto';
|
|
4
|
+
import { OwnershipUser } from './find-mine.interface';
|
|
4
5
|
export type CrudEndpoint = 'findAll' | 'findOne' | 'create' | 'update' | 'remove' | 'findAuditLogs' | 'findMine';
|
|
5
6
|
export interface AuditLogQuery {
|
|
6
7
|
user_id?: string;
|
|
@@ -25,10 +26,10 @@ export interface CrudInterface<CreateDto, UpdateDto, ResponseDto> {
|
|
|
25
26
|
meta?: unknown;
|
|
26
27
|
}>;
|
|
27
28
|
findAllWithCursor?(query: CursorPaginationDto & FilterDto): Promise<CursorPaginationResult<ResponseDto>>;
|
|
28
|
-
findOne(id: number): Promise<ResponseDto>;
|
|
29
|
+
findOne(id: number, user?: OwnershipUser): Promise<ResponseDto>;
|
|
29
30
|
create(dto: CreateDto): Promise<ResponseDto>;
|
|
30
|
-
update(id: number, dto: UpdateDto): Promise<ResponseDto>;
|
|
31
|
-
remove(id: number): Promise<boolean>;
|
|
31
|
+
update(id: number, dto: UpdateDto, user?: OwnershipUser): Promise<ResponseDto>;
|
|
32
|
+
remove(id: number, user?: OwnershipUser): Promise<boolean>;
|
|
32
33
|
findAuditLogs?(query: AuditLogQuery): Promise<unknown>;
|
|
33
34
|
findMine?(userId: string | number, query: PaginationDto & FilterDto): Promise<{
|
|
34
35
|
data: ResponseDto[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crud.interface.d.ts","sourceRoot":"","sources":["../../../src/lib/interfaces/crud.interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"crud.interface.d.ts","sourceRoot":"","sources":["../../../src/lib/interfaces/crud.interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAEtD,MAAM,MAAM,YAAY,GACpB,SAAS,GACT,SAAS,GACT,QAAQ,GACR,QAAQ,GACR,QAAQ,GACR,eAAe,GACf,UAAU,CAAC;AAEf,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,sBAAsB,CAAC,CAAC;IACvC,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,IAAI,EAAE;QACJ,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,OAAO,CAAC;QACjB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;QAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;CACH;AAED,MAAM,WAAW,aAAa,CAAC,SAAS,EAAE,SAAS,EAAE,WAAW;IAC9D,iBAAiB,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;IAE5C,OAAO,CAAC,KAAK,EAAE,aAAa,GAAG,SAAS,GAAG,OAAO,CAAC;QACjD,IAAI,EAAE,WAAW,EAAE,CAAC;QACpB,IAAI,CAAC,EAAE,OAAO,CAAC;KAChB,CAAC,CAAC;IAEH,iBAAiB,CAAC,CAChB,KAAK,EAAE,mBAAmB,GAAG,SAAS,GACrC,OAAO,CAAC,sBAAsB,CAAC,WAAW,CAAC,CAAC,CAAC;IAEhD,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAEhE,MAAM,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAE7C,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAE/E,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE3D,aAAa,CAAC,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAEvD,QAAQ,CAAC,CACP,MAAM,EAAE,MAAM,GAAG,MAAM,EACvB,KAAK,EAAE,aAAa,GAAG,SAAS,GAC/B,OAAO,CAAC;QAAE,IAAI,EAAE,WAAW,EAAE,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACrD"}
|
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
import { ObjectLiteral, SelectQueryBuilder } from 'typeorm';
|
|
2
|
+
/**
|
|
3
|
+
* Minimal structural view of the authenticated user passed to ownership
|
|
4
|
+
* enforcement. Compatible with `AuthUser` from `@nest-util/nest-auth`.
|
|
5
|
+
*/
|
|
6
|
+
export interface OwnershipUser {
|
|
7
|
+
id?: string | number;
|
|
8
|
+
permissions?: readonly string[];
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
2
11
|
export interface FindMineConfig<TEntity extends ObjectLiteral> {
|
|
3
12
|
userOwnershipField?: keyof TEntity;
|
|
4
13
|
findMineQuery?: (qb: SelectQueryBuilder<TEntity>, userId: string | number) => void;
|
|
14
|
+
/**
|
|
15
|
+
* When true (and an ownership field/query is configured), the generic
|
|
16
|
+
* `findOne`, `update`, and `remove` operations are scoped to records owned
|
|
17
|
+
* by the passed-in user. Non-owned records resolve to 404; missing user
|
|
18
|
+
* resolves to 403. Defaults to false - existing behavior is preserved.
|
|
19
|
+
*/
|
|
20
|
+
enforceOwnership?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Permission strings that grant full access and bypass ownership checks.
|
|
23
|
+
* Example: ['admin.access'].
|
|
24
|
+
*/
|
|
25
|
+
ownershipBypassPermissions?: readonly string[];
|
|
26
|
+
/**
|
|
27
|
+
* Custom predicate that grants full access and bypasses ownership checks.
|
|
28
|
+
* Takes precedence alongside ownershipBypassPermissions.
|
|
29
|
+
*/
|
|
30
|
+
ownershipBypass?: (user: OwnershipUser) => boolean;
|
|
5
31
|
}
|
|
6
32
|
//# 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,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;
|
|
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"}
|
|
@@ -7,7 +7,7 @@ import { FilterDto } from '../dtos/filter.dto';
|
|
|
7
7
|
import { CrudEndpoint, CrudInterface, CursorPaginationResult } from '../interfaces/crud.interface';
|
|
8
8
|
import { CursorStrategy } from '../interfaces/cursor-strategy.interface';
|
|
9
9
|
import { CrudHooks, TransactionConfig } from '../interfaces/hooks.interface';
|
|
10
|
-
import { FindMineConfig } from '../interfaces/find-mine.interface';
|
|
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
13
|
allowedFilters?: readonly (keyof Entity)[];
|
|
@@ -45,11 +45,20 @@ export declare class NestCrudService<Entity extends ObjectLiteral, CreateDto = P
|
|
|
45
45
|
protected readonly transactionConfig: TransactionConfig;
|
|
46
46
|
protected readonly userOwnershipField?: keyof Entity;
|
|
47
47
|
protected readonly findMineQuery?: (qb: SelectQueryBuilder<Entity>, userId: string | number) => void;
|
|
48
|
+
protected readonly enforceOwnership: boolean;
|
|
49
|
+
protected readonly ownershipBypassPermissions: readonly string[];
|
|
50
|
+
protected readonly ownershipBypass?: (user: OwnershipUser) => boolean;
|
|
48
51
|
constructor(options: CrudServiceOptions<Entity, ResponseDto>);
|
|
49
52
|
private resolveRelations;
|
|
50
53
|
private executeInTransaction;
|
|
51
54
|
private executeHook;
|
|
52
55
|
private buildRelationsObject;
|
|
56
|
+
private applyIncludeJoins;
|
|
57
|
+
private isOwnershipConfigured;
|
|
58
|
+
private canBypassOwnership;
|
|
59
|
+
private enforceOwnershipFor;
|
|
60
|
+
private applyOwnershipCondition;
|
|
61
|
+
private findOwnedEntity;
|
|
53
62
|
findAll(query: PaginationDto & FilterDto): Promise<{
|
|
54
63
|
data: ResponseDto[];
|
|
55
64
|
meta: {
|
|
@@ -66,10 +75,10 @@ export declare class NestCrudService<Entity extends ObjectLiteral, CreateDto = P
|
|
|
66
75
|
meta?: unknown;
|
|
67
76
|
}>;
|
|
68
77
|
findAllWithCursor(query: CursorPaginationDto & FilterDto): Promise<CursorPaginationResult<ResponseDto>>;
|
|
69
|
-
findOne(id: number): Promise<ResponseDto>;
|
|
78
|
+
findOne(id: number, user?: OwnershipUser): Promise<ResponseDto>;
|
|
70
79
|
create(payload: CreateDto): Promise<ResponseDto>;
|
|
71
|
-
update(id: number, payload: UpdateDto): Promise<ResponseDto>;
|
|
72
|
-
remove(id: number): Promise<boolean>;
|
|
80
|
+
update(id: number, payload: UpdateDto, user?: OwnershipUser): Promise<ResponseDto>;
|
|
81
|
+
remove(id: number, user?: OwnershipUser): Promise<boolean>;
|
|
73
82
|
findAuditLogs(query: {
|
|
74
83
|
user_id?: string;
|
|
75
84
|
start_date?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nest-crud.service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/nest-crud.service.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"nest-crud.service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/nest-crud.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -24,6 +24,9 @@ let NestCrudService = class NestCrudService {
|
|
|
24
24
|
this.transactionConfig = options.transactionConfig ?? {};
|
|
25
25
|
this.userOwnershipField = options.userOwnershipField;
|
|
26
26
|
this.findMineQuery = options.findMineQuery;
|
|
27
|
+
this.enforceOwnership = options.enforceOwnership ?? false;
|
|
28
|
+
this.ownershipBypassPermissions = options.ownershipBypassPermissions ?? [];
|
|
29
|
+
this.ownershipBypass = options.ownershipBypass;
|
|
27
30
|
}
|
|
28
31
|
async resolveRelations(payload) {
|
|
29
32
|
if (!this.relations.length)
|
|
@@ -92,22 +95,67 @@ let NestCrudService = class NestCrudService {
|
|
|
92
95
|
}
|
|
93
96
|
return result;
|
|
94
97
|
}
|
|
98
|
+
applyIncludeJoins(qb) {
|
|
99
|
+
if (this.include.length === 0)
|
|
100
|
+
return;
|
|
101
|
+
this.include.forEach((relation) => {
|
|
102
|
+
const parts = relation.split('.');
|
|
103
|
+
if (parts.length === 1) {
|
|
104
|
+
qb.leftJoinAndSelect(`e.${parts[0]}`, parts[0]);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
const parentAlias = parts.slice(0, -1).join('_');
|
|
108
|
+
const field = parts[parts.length - 1];
|
|
109
|
+
const alias = parts.join('_');
|
|
110
|
+
qb.leftJoinAndSelect(`${parentAlias}.${field}`, alias);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
isOwnershipConfigured() {
|
|
115
|
+
return Boolean(this.userOwnershipField || this.findMineQuery);
|
|
116
|
+
}
|
|
117
|
+
canBypassOwnership(user) {
|
|
118
|
+
if (!user)
|
|
119
|
+
return false;
|
|
120
|
+
if (this.ownershipBypass && this.ownershipBypass(user)) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
if (this.ownershipBypassPermissions.length > 0) {
|
|
124
|
+
const userPermissions = Array.isArray(user.permissions)
|
|
125
|
+
? user.permissions
|
|
126
|
+
: [];
|
|
127
|
+
return this.ownershipBypassPermissions.some((permission) => userPermissions.includes(permission));
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
enforceOwnershipFor(user) {
|
|
132
|
+
return Boolean(this.enforceOwnership &&
|
|
133
|
+
this.isOwnershipConfigured() &&
|
|
134
|
+
!this.canBypassOwnership(user));
|
|
135
|
+
}
|
|
136
|
+
applyOwnershipCondition(qb, userId) {
|
|
137
|
+
if (this.findMineQuery) {
|
|
138
|
+
this.findMineQuery(qb, userId);
|
|
139
|
+
}
|
|
140
|
+
else if (this.userOwnershipField) {
|
|
141
|
+
qb.where(`e.${String(this.userOwnershipField)} = :userId`, { userId });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async findOwnedEntity(id, user) {
|
|
145
|
+
const userId = user?.id;
|
|
146
|
+
if (userId === undefined || userId === null) {
|
|
147
|
+
throw new common_1.ForbiddenException('Authentication required to access this resource');
|
|
148
|
+
}
|
|
149
|
+
const qb = this.repo.createQueryBuilder('e');
|
|
150
|
+
this.applyOwnershipCondition(qb, userId);
|
|
151
|
+
qb.andWhere('e.id = :id', { id });
|
|
152
|
+
this.applyIncludeJoins(qb);
|
|
153
|
+
const rows = await qb.getMany();
|
|
154
|
+
return rows[0] ?? null;
|
|
155
|
+
}
|
|
95
156
|
async findAll(query) {
|
|
96
157
|
const qb = this.repo.createQueryBuilder('e');
|
|
97
|
-
|
|
98
|
-
this.include.forEach((relation) => {
|
|
99
|
-
const parts = relation.split('.');
|
|
100
|
-
if (parts.length === 1) {
|
|
101
|
-
qb.leftJoinAndSelect(`e.${parts[0]}`, parts[0]);
|
|
102
|
-
}
|
|
103
|
-
else {
|
|
104
|
-
const parentAlias = parts.slice(0, -1).join('_');
|
|
105
|
-
const field = parts[parts.length - 1];
|
|
106
|
-
const alias = parts.join('_');
|
|
107
|
-
qb.leftJoinAndSelect(`${parentAlias}.${field}`, alias);
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
}
|
|
158
|
+
this.applyIncludeJoins(qb);
|
|
111
159
|
(0, filter_helper_1.applyFilters)(qb, query.filter, this.allowedFilters);
|
|
112
160
|
const paginationMeta = (0, pagination_helper_1.applyPagination)(qb, query);
|
|
113
161
|
if (query.orderBy) {
|
|
@@ -130,26 +178,8 @@ let NestCrudService = class NestCrudService {
|
|
|
130
178
|
throw new common_1.BadRequestException('findMine not configured');
|
|
131
179
|
}
|
|
132
180
|
const qb = this.repo.createQueryBuilder('e');
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
else if (this.userOwnershipField) {
|
|
137
|
-
qb.where(`e.${String(this.userOwnershipField)} = :userId`, { userId });
|
|
138
|
-
}
|
|
139
|
-
if (this.include.length > 0) {
|
|
140
|
-
this.include.forEach((relation) => {
|
|
141
|
-
const parts = relation.split('.');
|
|
142
|
-
if (parts.length === 1) {
|
|
143
|
-
qb.leftJoinAndSelect(`e.${parts[0]}`, parts[0]);
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
146
|
-
const parentAlias = parts.slice(0, -1).join('_');
|
|
147
|
-
const field = parts[parts.length - 1];
|
|
148
|
-
const alias = parts.join('_');
|
|
149
|
-
qb.leftJoinAndSelect(`${parentAlias}.${field}`, alias);
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
}
|
|
181
|
+
this.applyOwnershipCondition(qb, userId);
|
|
182
|
+
this.applyIncludeJoins(qb);
|
|
153
183
|
(0, filter_helper_1.applyFilters)(qb, query.filter, this.allowedFilters);
|
|
154
184
|
const paginationMeta = (0, pagination_helper_1.applyPagination)(qb, query);
|
|
155
185
|
if (query.orderBy) {
|
|
@@ -173,20 +203,7 @@ let NestCrudService = class NestCrudService {
|
|
|
173
203
|
const orderDirection = 'DESC';
|
|
174
204
|
const qb = this.repo.createQueryBuilder('e');
|
|
175
205
|
// Join relations
|
|
176
|
-
|
|
177
|
-
this.include.forEach((relation) => {
|
|
178
|
-
const parts = relation.split('.');
|
|
179
|
-
if (parts.length === 1) {
|
|
180
|
-
qb.leftJoinAndSelect(`e.${parts[0]}`, parts[0]);
|
|
181
|
-
}
|
|
182
|
-
else {
|
|
183
|
-
const parentAlias = parts.slice(0, -1).join('_');
|
|
184
|
-
const field = parts[parts.length - 1];
|
|
185
|
-
const alias = parts.join('_');
|
|
186
|
-
qb.leftJoinAndSelect(`${parentAlias}.${field}`, alias);
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
}
|
|
206
|
+
this.applyIncludeJoins(qb);
|
|
190
207
|
// Apply filters
|
|
191
208
|
(0, filter_helper_1.applyFilters)(qb, query.filter, this.allowedFilters);
|
|
192
209
|
// Apply cursor filter
|
|
@@ -232,15 +249,21 @@ let NestCrudService = class NestCrudService {
|
|
|
232
249
|
},
|
|
233
250
|
};
|
|
234
251
|
}
|
|
235
|
-
async findOne(id) {
|
|
252
|
+
async findOne(id, user) {
|
|
236
253
|
await this.executeHook(this.hooks.beforeFindOne, { id });
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
254
|
+
let entity;
|
|
255
|
+
if (this.enforceOwnershipFor(user)) {
|
|
256
|
+
entity = await this.findOwnedEntity(id, user);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
const relationsObj = this.include.length > 0
|
|
260
|
+
? this.buildRelationsObject(this.include)
|
|
261
|
+
: undefined;
|
|
262
|
+
entity = await this.repo.findOne({
|
|
263
|
+
where: { id },
|
|
264
|
+
relations: relationsObj,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
244
267
|
if (!entity) {
|
|
245
268
|
throw new common_1.NotFoundException('Resource not found');
|
|
246
269
|
}
|
|
@@ -261,10 +284,16 @@ let NestCrudService = class NestCrudService {
|
|
|
261
284
|
await this.executeHook(this.hooks.afterCreate, { entity, payload: payloadSnapshot });
|
|
262
285
|
return result;
|
|
263
286
|
}
|
|
264
|
-
async update(id, payload) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
287
|
+
async update(id, payload, user) {
|
|
288
|
+
let existing;
|
|
289
|
+
if (this.enforceOwnershipFor(user)) {
|
|
290
|
+
existing = await this.findOwnedEntity(id, user);
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
existing = await this.repo.findOneBy({
|
|
294
|
+
id,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
268
297
|
if (!existing) {
|
|
269
298
|
throw new common_1.NotFoundException('Resource not found');
|
|
270
299
|
}
|
|
@@ -273,14 +302,20 @@ let NestCrudService = class NestCrudService {
|
|
|
273
302
|
const resolved = await this.resolveRelations(payload);
|
|
274
303
|
this.repo.merge(existing, resolved);
|
|
275
304
|
await this.repo.save(existing);
|
|
276
|
-
const result = await this.findOne(id);
|
|
305
|
+
const result = await this.findOne(id, user);
|
|
277
306
|
await this.executeHook(this.hooks.afterUpdate, { entity: result, payload: payloadSnapshot, id });
|
|
278
307
|
return result;
|
|
279
308
|
}
|
|
280
|
-
async remove(id) {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
309
|
+
async remove(id, user) {
|
|
310
|
+
let existing;
|
|
311
|
+
if (this.enforceOwnershipFor(user)) {
|
|
312
|
+
existing = await this.findOwnedEntity(id, user);
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
existing = await this.repo.findOneBy({
|
|
316
|
+
id,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
284
319
|
if (!existing) {
|
|
285
320
|
throw new common_1.NotFoundException('Resource not found');
|
|
286
321
|
}
|
|
@@ -71,7 +71,7 @@ function crudControllerTests(config) {
|
|
|
71
71
|
const expectedResult = { id: 1, name: 'Test' };
|
|
72
72
|
service.findOne.mockResolvedValue(expectedResult);
|
|
73
73
|
const result = await controller.findOne(1);
|
|
74
|
-
expect(service.findOne).toHaveBeenCalledWith(1);
|
|
74
|
+
expect(service.findOne).toHaveBeenCalledWith(1, undefined);
|
|
75
75
|
expect(result).toEqual(expectedResult);
|
|
76
76
|
});
|
|
77
77
|
});
|
|
@@ -91,7 +91,7 @@ function crudControllerTests(config) {
|
|
|
91
91
|
const expectedResult = { id: 1 };
|
|
92
92
|
service.update.mockResolvedValue(expectedResult);
|
|
93
93
|
const result = await controller.update(1, dto);
|
|
94
|
-
expect(service.update).toHaveBeenCalledWith(1, dto);
|
|
94
|
+
expect(service.update).toHaveBeenCalledWith(1, dto, undefined);
|
|
95
95
|
expect(result).toEqual(expectedResult);
|
|
96
96
|
});
|
|
97
97
|
});
|
|
@@ -99,7 +99,7 @@ function crudControllerTests(config) {
|
|
|
99
99
|
it('should call service.remove', async () => {
|
|
100
100
|
service.remove.mockResolvedValue(true);
|
|
101
101
|
const result = await controller.remove(1);
|
|
102
|
-
expect(service.remove).toHaveBeenCalledWith(1);
|
|
102
|
+
expect(service.remove).toHaveBeenCalledWith(1, undefined);
|
|
103
103
|
expect(result).toBe(true);
|
|
104
104
|
});
|
|
105
105
|
});
|