@nest-util/nest-crud 2.0.1 → 2.0.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/dist/lib/helpers/relation-path.helper.d.ts +19 -0
- package/dist/lib/helpers/relation-path.helper.d.ts.map +1 -0
- package/dist/lib/helpers/relation-path.helper.js +48 -0
- package/dist/lib/helpers/value-hash.helper.d.ts +15 -0
- package/dist/lib/helpers/value-hash.helper.d.ts.map +1 -0
- package/dist/lib/helpers/value-hash.helper.js +50 -0
- package/dist/lib/interfaces/approval-pipeline.interface.d.ts +41 -0
- package/dist/lib/interfaces/approval-pipeline.interface.d.ts.map +1 -1
- package/dist/lib/interfaces/hooks.interface.d.ts +29 -0
- package/dist/lib/interfaces/hooks.interface.d.ts.map +1 -1
- package/dist/lib/services/nest-crud.service.d.ts +11 -1
- package/dist/lib/services/nest-crud.service.d.ts.map +1 -1
- package/dist/lib/services/nest-crud.service.js +137 -18
- package/package.json +1 -1
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { EntityMetadata } from 'typeorm';
|
|
2
|
+
/**
|
|
3
|
+
* Expands modification-field dot paths into every relation prefix required to
|
|
4
|
+
* load the referenced values, so relation fields (e.g. `addresses` or
|
|
5
|
+
* `company.addresses.city`) can be read without the consumer configuring
|
|
6
|
+
* `include`.
|
|
7
|
+
*
|
|
8
|
+
* Non-relation columns yield nothing. Unknown paths are ignored.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveRelationPaths(metadata: EntityMetadata, fields: readonly string[]): string[];
|
|
11
|
+
/** Checks whether a (root) property name exists on the entity as a column,
|
|
12
|
+
* embedded column, or relation. */
|
|
13
|
+
export declare function isKnownProperty(metadata: EntityMetadata, propertyName: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Normalizes a captured modification value so it is always jsonb-serializable
|
|
16
|
+
* and distinguishable from "not loaded": `undefined` becomes explicit `null`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function normalizeCapturedValue(value: unknown): unknown;
|
|
19
|
+
//# sourceMappingURL=relation-path.helper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relation-path.helper.d.ts","sourceRoot":"","sources":["../../../src/lib/helpers/relation-path.helper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,cAAc,EACxB,MAAM,EAAE,SAAS,MAAM,EAAE,GACxB,MAAM,EAAE,CAyBV;AAED;mCACmC;AACnC,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,cAAc,EACxB,YAAY,EAAE,MAAM,GACnB,OAAO,CAST;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE9D"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveRelationPaths = resolveRelationPaths;
|
|
4
|
+
exports.isKnownProperty = isKnownProperty;
|
|
5
|
+
exports.normalizeCapturedValue = normalizeCapturedValue;
|
|
6
|
+
/**
|
|
7
|
+
* Expands modification-field dot paths into every relation prefix required to
|
|
8
|
+
* load the referenced values, so relation fields (e.g. `addresses` or
|
|
9
|
+
* `company.addresses.city`) can be read without the consumer configuring
|
|
10
|
+
* `include`.
|
|
11
|
+
*
|
|
12
|
+
* Non-relation columns yield nothing. Unknown paths are ignored.
|
|
13
|
+
*/
|
|
14
|
+
function resolveRelationPaths(metadata, fields) {
|
|
15
|
+
const resolved = new Set();
|
|
16
|
+
for (const field of fields) {
|
|
17
|
+
if (!field) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const segments = field.split('.');
|
|
21
|
+
let relations = metadata.relations ?? [];
|
|
22
|
+
let prefix = [];
|
|
23
|
+
for (const segment of segments) {
|
|
24
|
+
const match = relations.find((relation) => relation.propertyName === segment);
|
|
25
|
+
if (!match) {
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
prefix = [...prefix, match.propertyName];
|
|
29
|
+
resolved.add(prefix.join('.'));
|
|
30
|
+
relations = match.inverseEntityMetadata.relations;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return [...resolved];
|
|
34
|
+
}
|
|
35
|
+
/** Checks whether a (root) property name exists on the entity as a column,
|
|
36
|
+
* embedded column, or relation. */
|
|
37
|
+
function isKnownProperty(metadata, propertyName) {
|
|
38
|
+
return ((metadata.columns ?? []).some((column) => column.propertyPath === propertyName ||
|
|
39
|
+
column.propertyPath.startsWith(`${propertyName}.`)) ||
|
|
40
|
+
(metadata.relations ?? []).some((relation) => relation.propertyName === propertyName));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Normalizes a captured modification value so it is always jsonb-serializable
|
|
44
|
+
* and distinguishable from "not loaded": `undefined` becomes explicit `null`.
|
|
45
|
+
*/
|
|
46
|
+
function normalizeCapturedValue(value) {
|
|
47
|
+
return value === undefined ? null : value;
|
|
48
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Produces a deterministic, stable serialization of an arbitrary value so it
|
|
3
|
+
* can be hashed consistently across processes and after jsonb round-trips:
|
|
4
|
+
* - object keys are sorted recursively
|
|
5
|
+
* - Date instances become ISO strings
|
|
6
|
+
* - undefined / null / missing all normalize to null
|
|
7
|
+
* - arrays keep their order
|
|
8
|
+
* - circular references become the string '[Circular]'
|
|
9
|
+
*/
|
|
10
|
+
export declare function canonicalJson(value: unknown): string;
|
|
11
|
+
/** SHA-256 hex hash of a value's canonical JSON form. */
|
|
12
|
+
export declare function hashValue(value: unknown): string;
|
|
13
|
+
/** Structural equality used as legacy fallback when no hash is stored. */
|
|
14
|
+
export declare function deepEqual(a: unknown, b: unknown): boolean;
|
|
15
|
+
//# sourceMappingURL=value-hash.helper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"value-hash.helper.d.ts","sourceRoot":"","sources":["../../../src/lib/helpers/value-hash.helper.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CA0BpD;AAED,yDAAyD;AACzD,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEhD;AAED,0EAA0E;AAC1E,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CAEzD"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.canonicalJson = canonicalJson;
|
|
4
|
+
exports.hashValue = hashValue;
|
|
5
|
+
exports.deepEqual = deepEqual;
|
|
6
|
+
const crypto_1 = require("crypto");
|
|
7
|
+
/**
|
|
8
|
+
* Produces a deterministic, stable serialization of an arbitrary value so it
|
|
9
|
+
* can be hashed consistently across processes and after jsonb round-trips:
|
|
10
|
+
* - object keys are sorted recursively
|
|
11
|
+
* - Date instances become ISO strings
|
|
12
|
+
* - undefined / null / missing all normalize to null
|
|
13
|
+
* - arrays keep their order
|
|
14
|
+
* - circular references become the string '[Circular]'
|
|
15
|
+
*/
|
|
16
|
+
function canonicalJson(value) {
|
|
17
|
+
const seen = new WeakSet();
|
|
18
|
+
const walk = (node) => {
|
|
19
|
+
if (node === undefined || node === null) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
if (node instanceof Date) {
|
|
23
|
+
return Number.isNaN(node.getTime()) ? null : node.toISOString();
|
|
24
|
+
}
|
|
25
|
+
if (typeof node === 'object') {
|
|
26
|
+
if (seen.has(node)) {
|
|
27
|
+
return '[Circular]';
|
|
28
|
+
}
|
|
29
|
+
seen.add(node);
|
|
30
|
+
if (Array.isArray(node)) {
|
|
31
|
+
return node.map((item) => walk(item));
|
|
32
|
+
}
|
|
33
|
+
const out = {};
|
|
34
|
+
for (const key of Object.keys(node).sort()) {
|
|
35
|
+
out[key] = walk(node[key]);
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
return node;
|
|
40
|
+
};
|
|
41
|
+
return JSON.stringify(walk(value));
|
|
42
|
+
}
|
|
43
|
+
/** SHA-256 hex hash of a value's canonical JSON form. */
|
|
44
|
+
function hashValue(value) {
|
|
45
|
+
return (0, crypto_1.createHash)('sha256').update(canonicalJson(value)).digest('hex');
|
|
46
|
+
}
|
|
47
|
+
/** Structural equality used as legacy fallback when no hash is stored. */
|
|
48
|
+
function deepEqual(a, b) {
|
|
49
|
+
return canonicalJson(a) === canonicalJson(b);
|
|
50
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ApprovalHooks } from './hooks.interface';
|
|
1
2
|
/**
|
|
2
3
|
* Lifecycle of an approval status row.
|
|
3
4
|
*
|
|
@@ -30,6 +31,12 @@ export interface ModificationItem {
|
|
|
30
31
|
* '[Circular]').
|
|
31
32
|
*/
|
|
32
33
|
currentValue?: unknown;
|
|
34
|
+
/**
|
|
35
|
+
* SHA-256 hash of the captured value's canonical JSON form. Stored so the
|
|
36
|
+
* resubmit check can cheaply detect whether the field changed since the
|
|
37
|
+
* request without deep-comparing (possibly large) relation snapshots.
|
|
38
|
+
*/
|
|
39
|
+
currentValueHash?: string;
|
|
33
40
|
/** Value the reviewer wants instead. */
|
|
34
41
|
wantedValue: unknown;
|
|
35
42
|
/** Optional human-readable explanation. */
|
|
@@ -99,6 +106,40 @@ export interface ApprovalPipelineConfig {
|
|
|
99
106
|
* of approval state.
|
|
100
107
|
*/
|
|
101
108
|
visibleStatuses?: readonly ApprovalStatus[];
|
|
109
|
+
/**
|
|
110
|
+
* Before/after hooks for each approval transition, mirroring the generic
|
|
111
|
+
* `CrudHooks` API. Each hook receives the approval row view and (for
|
|
112
|
+
* `after*`) the previous status.
|
|
113
|
+
*/
|
|
114
|
+
hooks?: ApprovalHooks;
|
|
115
|
+
/**
|
|
116
|
+
* Verifies that the requester actually applied the requested changes before
|
|
117
|
+
* allowing a resubmit. For each modification item the live field value is
|
|
118
|
+
* hashed and compared with the hash captured at request time — an identical
|
|
119
|
+
* hash means the field was left untouched.
|
|
120
|
+
*/
|
|
121
|
+
resubmitCheck?: ApprovalResubmitCheckConfig;
|
|
122
|
+
}
|
|
123
|
+
export interface ApprovalResubmitCheckConfig {
|
|
124
|
+
/**
|
|
125
|
+
* 'all' (default): every non-ignored modification field must have changed.
|
|
126
|
+
* 'any': at least one must have changed.
|
|
127
|
+
*/
|
|
128
|
+
mode?: 'all' | 'any';
|
|
129
|
+
/** Modification fields exempt from the "must have changed" gate. */
|
|
130
|
+
ignoreFields?: readonly string[];
|
|
131
|
+
/**
|
|
132
|
+
* Full override of the built-in comparison. Return false to block the
|
|
133
|
+
* resubmit with CRUD_APPROVAL_RESUBMIT_NOT_SATISFIED.
|
|
134
|
+
*/
|
|
135
|
+
customChecker?: (context: {
|
|
136
|
+
modifications: ModificationItem[];
|
|
137
|
+
satisfied: {
|
|
138
|
+
field: string;
|
|
139
|
+
satisfied: boolean;
|
|
140
|
+
}[];
|
|
141
|
+
entity: unknown;
|
|
142
|
+
}) => boolean | Promise<boolean>;
|
|
102
143
|
}
|
|
103
144
|
export interface RequestModificationPayload {
|
|
104
145
|
modifications: ModificationItem[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"approval-pipeline.interface.d.ts","sourceRoot":"","sources":["../../../src/lib/interfaces/approval-pipeline.interface.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe;;;;;;;CAOlB,CAAC;AAEX,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,OAAO,eAAe,CAAC,CAAC;AAEpF;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,wCAAwC;IACxC,WAAW,EAAE,OAAO,CAAC;IACrB,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,cAAc,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,IAAI,CAAC;IAClB,oBAAoB,CAAC,EAAE,gBAAgB,EAAE,GAAG,IAAI,CAAC;IACjD,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,aAAa,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,gBAAgB,EAAE,MAAM,CAAC;IACzB,aAAa,EAAE,gBAAgB,EAAE,CAAC;IAClC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,IAAI,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,MAAM,WAAW,2BAA2B;IAC1C,0EAA0E;IAC1E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oDAAoD;IACpD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,OAAO,GAAG,WAAW,CAAC;IAEtC;;;OAGG;IACH,WAAW,CAAC,EAAE,2BAA2B,CAAC;IAE1C;;;;;OAKG;IACH,eAAe,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;CAC7C;AAED,MAAM,WAAW,0BAA0B;IACzC,aAAa,EAAE,gBAAgB,EAAE,CAAC;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf"}
|
|
1
|
+
{"version":3,"file":"approval-pipeline.interface.d.ts","sourceRoot":"","sources":["../../../src/lib/interfaces/approval-pipeline.interface.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ApprovalStatus, ApprovalStatusView, ModificationItem } from './approval-pipeline.interface';
|
|
2
|
+
import { OwnershipUser } from './find-mine.interface';
|
|
1
3
|
export type CrudHook<TContext = any> = (context: TContext) => Promise<any> | any;
|
|
2
4
|
export interface CrudHookConfig<TContext = any> {
|
|
3
5
|
handler: CrudHook<TContext>;
|
|
@@ -49,4 +51,31 @@ export interface TransactionConfig {
|
|
|
49
51
|
isolationLevel?: 'READ UNCOMMITTED' | 'READ COMMITTED' | 'REPEATABLE READ' | 'SERIALIZABLE';
|
|
50
52
|
timeout?: number;
|
|
51
53
|
}
|
|
54
|
+
export interface ApprovalHookContext {
|
|
55
|
+
id: number;
|
|
56
|
+
user?: OwnershipUser;
|
|
57
|
+
approval: ApprovalStatusView;
|
|
58
|
+
}
|
|
59
|
+
export interface AfterApprovalHookContext extends ApprovalHookContext {
|
|
60
|
+
previousStatus: ApprovalStatus;
|
|
61
|
+
}
|
|
62
|
+
export interface RequestModificationApprovalHookContext extends ApprovalHookContext {
|
|
63
|
+
modifications: ModificationItem[];
|
|
64
|
+
note?: string;
|
|
65
|
+
}
|
|
66
|
+
export interface AfterRequestModificationApprovalHookContext extends RequestModificationApprovalHookContext {
|
|
67
|
+
previousStatus: ApprovalStatus;
|
|
68
|
+
}
|
|
69
|
+
export interface ApprovalHooks {
|
|
70
|
+
beforeSubmit?: CrudHookConfig<ApprovalHookContext>;
|
|
71
|
+
afterSubmit?: CrudHookConfig<AfterApprovalHookContext>;
|
|
72
|
+
beforeApprove?: CrudHookConfig<ApprovalHookContext>;
|
|
73
|
+
afterApprove?: CrudHookConfig<AfterApprovalHookContext>;
|
|
74
|
+
beforeReject?: CrudHookConfig<ApprovalHookContext>;
|
|
75
|
+
afterReject?: CrudHookConfig<AfterApprovalHookContext>;
|
|
76
|
+
beforeRequestModification?: CrudHookConfig<RequestModificationApprovalHookContext>;
|
|
77
|
+
afterRequestModification?: CrudHookConfig<AfterRequestModificationApprovalHookContext>;
|
|
78
|
+
beforeResubmit?: CrudHookConfig<ApprovalHookContext>;
|
|
79
|
+
afterResubmit?: CrudHookConfig<AfterApprovalHookContext>;
|
|
80
|
+
}
|
|
52
81
|
//# sourceMappingURL=hooks.interface.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks.interface.d.ts","sourceRoot":"","sources":["../../../src/lib/interfaces/hooks.interface.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,CAAC,QAAQ,GAAG,GAAG,IAAI,CAAC,OAAO,EAAE,QAAQ,KAAK,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;AAEjF,MAAM,WAAW,cAAc,CAAC,QAAQ,GAAG,GAAG;IAC5C,OAAO,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC5B,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB,CAAC,OAAO,EAAE,UAAU;IACtD,OAAO,EAAE,UAAU,CAAC;CACrB;AAED,MAAM,WAAW,kBAAkB,CAAC,OAAO,EAAE,UAAU;IACrD,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,UAAU,CAAC;CACrB;AAED,MAAM,WAAW,mBAAmB,CAAC,OAAO,EAAE,UAAU;IACtD,OAAO,EAAE,UAAU,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,kBAAkB,CAAC,OAAO,EAAE,UAAU;IACrD,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,UAAU,CAAC;IACpB,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,mBAAmB,CAAC,OAAO;IAC1C,MAAM,EAAE,OAAO,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,mBAAmB,CAAC,OAAO;IAC1C,MAAM,EAAE,OAAO,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,SAAS,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU;IACxD,YAAY,CAAC,EAAE,cAAc,CAAC,mBAAmB,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC;IACxE,WAAW,CAAC,EAAE,cAAc,CAAC,kBAAkB,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC;IACtE,YAAY,CAAC,EAAE,cAAc,CAAC,mBAAmB,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC;IACxE,WAAW,CAAC,EAAE,cAAc,CAAC,kBAAkB,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC;IACtE,YAAY,CAAC,EAAE,cAAc,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC,CAAC;IAC5D,WAAW,CAAC,EAAE,cAAc,CAAC,kBAAkB,CAAC,CAAC;IACjD,aAAa,CAAC,EAAE,cAAc,CAAC,oBAAoB,CAAC,CAAC;IACrD,YAAY,CAAC,EAAE,cAAc,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC,CAAC;CAC7D;AAED,MAAM,WAAW,iBAAiB;IAChC,cAAc,CAAC,EACX,kBAAkB,GAClB,gBAAgB,GAChB,iBAAiB,GACjB,cAAc,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB"}
|
|
1
|
+
{"version":3,"file":"hooks.interface.d.ts","sourceRoot":"","sources":["../../../src/lib/interfaces/hooks.interface.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,7 +6,7 @@ import { CursorPaginationDto } from '../dtos/cursor-pagination.dto';
|
|
|
6
6
|
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
|
-
import { CrudHooks, TransactionConfig } from '../interfaces/hooks.interface';
|
|
9
|
+
import { ApprovalHooks, CrudHooks, TransactionConfig } from '../interfaces/hooks.interface';
|
|
10
10
|
import { FindMineConfig, OwnershipUser } from '../interfaces/find-mine.interface';
|
|
11
11
|
import { StatusPipelineConfig, StatusTransitionAction, StatusValue } from '../interfaces/status-pipeline.interface';
|
|
12
12
|
import { ApprovalPipelineConfig, ApprovalStatusView, ApprovalHistoryView, RequestModificationPayload } from '../interfaces/approval-pipeline.interface';
|
|
@@ -63,6 +63,7 @@ export declare class NestCrudService<Entity extends ObjectLiteral, CreateDto = P
|
|
|
63
63
|
action?: StatusTransitionAction<Entity>;
|
|
64
64
|
}>;
|
|
65
65
|
protected readonly approvalPipeline?: ApprovalPipelineConfig;
|
|
66
|
+
protected readonly approvalHooks: ApprovalHooks;
|
|
66
67
|
constructor(options: CrudServiceOptions<Entity, ResponseDto>);
|
|
67
68
|
private normalizeStatusTransitions;
|
|
68
69
|
private resolveRelations;
|
|
@@ -132,9 +133,18 @@ export declare class NestCrudService<Entity extends ObjectLiteral, CreateDto = P
|
|
|
132
133
|
private sanitizeForJsonb;
|
|
133
134
|
private loadApprovalStatus;
|
|
134
135
|
private transitionApproval;
|
|
136
|
+
private fireApprovalBeforeHook;
|
|
137
|
+
private fireApprovalAfterHook;
|
|
135
138
|
private assertTransitionAllowed;
|
|
136
139
|
private userHasPermission;
|
|
137
140
|
private applyApprovalVisibility;
|
|
141
|
+
/**
|
|
142
|
+
* Ensures the requester actually changed the fields covered by the stored
|
|
143
|
+
* modification request before a resubmit is allowed. Each item's live value
|
|
144
|
+
* hash is compared against the hash captured at request time; an identical
|
|
145
|
+
* hash means the field was left untouched.
|
|
146
|
+
*/
|
|
147
|
+
private assertResubmitSatisfied;
|
|
138
148
|
private approvalIdCastExpression;
|
|
139
149
|
private toApprovalStatusView;
|
|
140
150
|
private toApprovalHistoryView;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nest-crud.service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/nest-crud.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAE9D,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;
|
|
1
|
+
{"version":3,"file":"nest-crud.service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/nest-crud.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -7,6 +7,8 @@ const nest_error_1 = require("@nest-util/nest-error");
|
|
|
7
7
|
const audit_log_entity_1 = require("../entities/audit-log.entity");
|
|
8
8
|
const filter_helper_1 = require("../helpers/filter.helper");
|
|
9
9
|
const pagination_helper_1 = require("../helpers/pagination.helper");
|
|
10
|
+
const relation_path_helper_1 = require("../helpers/relation-path.helper");
|
|
11
|
+
const value_hash_helper_1 = require("../helpers/value-hash.helper");
|
|
10
12
|
const cursor_pagination_helper_1 = require("../helpers/cursor-pagination.helper");
|
|
11
13
|
const approval_status_entity_1 = require("../entities/approval-status.entity");
|
|
12
14
|
const modification_request_history_entity_1 = require("../entities/modification-request-history.entity");
|
|
@@ -46,6 +48,7 @@ let NestCrudService = class NestCrudService {
|
|
|
46
48
|
this.statusTransitions = this.normalizeStatusTransitions(options.statusPipeline.transitions);
|
|
47
49
|
}
|
|
48
50
|
this.approvalPipeline = options.approvalPipeline;
|
|
51
|
+
this.approvalHooks = options.approvalPipeline?.hooks ?? {};
|
|
49
52
|
}
|
|
50
53
|
normalizeStatusTransitions(transitions) {
|
|
51
54
|
const result = new Map();
|
|
@@ -558,12 +561,15 @@ let NestCrudService = class NestCrudService {
|
|
|
558
561
|
* Loads the target entity with its configured `include` relations so that
|
|
559
562
|
* relation fields can be read as objects when capturing modification values.
|
|
560
563
|
*/
|
|
561
|
-
async getEntityWithRelations(id, user) {
|
|
564
|
+
async getEntityWithRelations(id, user, extraRelations) {
|
|
562
565
|
if (this.enforceOwnershipFor(user)) {
|
|
563
566
|
return this.findOwnedEntity(id, user);
|
|
564
567
|
}
|
|
565
|
-
const
|
|
566
|
-
|
|
568
|
+
const relationPaths = [
|
|
569
|
+
...new Set([...this.include, ...(extraRelations ?? [])]),
|
|
570
|
+
];
|
|
571
|
+
const relations = relationPaths.length
|
|
572
|
+
? this.buildRelationsObject(relationPaths)
|
|
567
573
|
: undefined;
|
|
568
574
|
return this.repo.findOne({
|
|
569
575
|
where: { id },
|
|
@@ -635,20 +641,28 @@ let NestCrudService = class NestCrudService {
|
|
|
635
641
|
throw (0, nest_error_1.keyed)(common_1.HttpStatus.NOT_FOUND, nest_error_1.ErrorKey.CRUD_RESOURCE_NOT_FOUND);
|
|
636
642
|
}
|
|
637
643
|
const current = approval.status;
|
|
644
|
+
const previousStatus = current;
|
|
638
645
|
const now = new Date();
|
|
639
646
|
const userId = user?.id != null ? String(user.id) : null;
|
|
640
647
|
const reviewable = [
|
|
641
648
|
approval_pipeline_interface_1.APPROVAL_STATUS.submitted,
|
|
642
649
|
approval_pipeline_interface_1.APPROVAL_STATUS.resubmitted,
|
|
643
650
|
];
|
|
651
|
+
// Already in a reviewable/terminal state (e.g. the pipeline was
|
|
652
|
+
// configured with initialStatus: 'submitted'): no submit transition is
|
|
653
|
+
// needed, so this is a harmless no-op returning the current status. Hooks
|
|
654
|
+
// are skipped because no transition actually occurs.
|
|
655
|
+
if (action === 'submit' && current !== approval_pipeline_interface_1.APPROVAL_STATUS.draft) {
|
|
656
|
+
return this.toApprovalStatusView(approval);
|
|
657
|
+
}
|
|
658
|
+
const beforeContext = {
|
|
659
|
+
id,
|
|
660
|
+
user,
|
|
661
|
+
approval: this.toApprovalStatusView(approval),
|
|
662
|
+
};
|
|
663
|
+
await this.fireApprovalBeforeHook(action, beforeContext, modifications ?? [], note);
|
|
644
664
|
switch (action) {
|
|
645
665
|
case 'submit':
|
|
646
|
-
// Already in a reviewable/terminal state (e.g. the pipeline was
|
|
647
|
-
// configured with initialStatus: 'submitted'): no submit transition is
|
|
648
|
-
// needed, so this is a harmless no-op returning the current status.
|
|
649
|
-
if (current !== approval_pipeline_interface_1.APPROVAL_STATUS.draft) {
|
|
650
|
-
return this.toApprovalStatusView(approval);
|
|
651
|
-
}
|
|
652
666
|
approval.status = approval_pipeline_interface_1.APPROVAL_STATUS.submitted;
|
|
653
667
|
approval.requestedBy = userId;
|
|
654
668
|
break;
|
|
@@ -669,14 +683,23 @@ let NestCrudService = class NestCrudService {
|
|
|
669
683
|
case 'requestModification': {
|
|
670
684
|
this.assertTransitionAllowed(action, current, reviewable);
|
|
671
685
|
approval.status = approval_pipeline_interface_1.APPROVAL_STATUS.modificationRequested;
|
|
672
|
-
const
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
686
|
+
const fields = (modifications ?? []).map((m) => m.field);
|
|
687
|
+
const extraRelations = (0, relation_path_helper_1.resolveRelationPaths)(this.repo.metadata, fields);
|
|
688
|
+
const entity = await this.getEntityWithRelations(id, user, extraRelations);
|
|
689
|
+
const capturedModifications = (modifications ?? []).map((m) => {
|
|
690
|
+
if (!(0, relation_path_helper_1.isKnownProperty)(this.repo.metadata, m.field.split('.')[0])) {
|
|
691
|
+
console.warn(`[NestCrudService] Approval modification field "${m.field}" does not exist on ${this.repo.metadata.tableName}; capturing null`);
|
|
692
|
+
}
|
|
693
|
+
const currentValue = this.sanitizeForJsonb((0, relation_path_helper_1.normalizeCapturedValue)(m.currentValue !== undefined
|
|
676
694
|
? m.currentValue
|
|
677
|
-
: this.readCurrentValue(entity, m.field))
|
|
678
|
-
|
|
679
|
-
|
|
695
|
+
: this.readCurrentValue(entity, m.field)));
|
|
696
|
+
return {
|
|
697
|
+
...m,
|
|
698
|
+
currentValue,
|
|
699
|
+
currentValueHash: (0, value_hash_helper_1.hashValue)(currentValue),
|
|
700
|
+
wantedValue: this.sanitizeForJsonb(m.wantedValue),
|
|
701
|
+
};
|
|
702
|
+
});
|
|
680
703
|
approval.currentModifications = capturedModifications;
|
|
681
704
|
await this.getHistoryRepository().save(this.getHistoryRepository().create({
|
|
682
705
|
approvalStatusId: approval.id,
|
|
@@ -686,18 +709,65 @@ let NestCrudService = class NestCrudService {
|
|
|
686
709
|
}));
|
|
687
710
|
break;
|
|
688
711
|
}
|
|
689
|
-
case 'resubmit':
|
|
712
|
+
case 'resubmit': {
|
|
690
713
|
if (current !== approval_pipeline_interface_1.APPROVAL_STATUS.modificationRequested) {
|
|
691
714
|
throw (0, nest_error_1.keyed)(common_1.HttpStatus.BAD_REQUEST, nest_error_1.ErrorKey.CRUD_APPROVAL_INVALID_TRANSITION, { action: 'resubmit', current: String(current) });
|
|
692
715
|
}
|
|
716
|
+
await this.assertResubmitSatisfied(approval, id, user);
|
|
693
717
|
approval.status = approval_pipeline_interface_1.APPROVAL_STATUS.resubmitted;
|
|
694
718
|
approval.currentModifications = null;
|
|
695
719
|
approval.resubmittedBy = userId;
|
|
696
720
|
approval.resubmittedAt = now;
|
|
697
721
|
break;
|
|
722
|
+
}
|
|
698
723
|
}
|
|
699
724
|
const saved = await this.getApprovalRepository().save(approval);
|
|
700
|
-
|
|
725
|
+
const afterContext = {
|
|
726
|
+
id,
|
|
727
|
+
user,
|
|
728
|
+
approval: this.toApprovalStatusView(saved),
|
|
729
|
+
previousStatus,
|
|
730
|
+
};
|
|
731
|
+
await this.fireApprovalAfterHook(action, afterContext, modifications ?? [], note);
|
|
732
|
+
return afterContext.approval;
|
|
733
|
+
}
|
|
734
|
+
async fireApprovalBeforeHook(action, context, modifications, note) {
|
|
735
|
+
switch (action) {
|
|
736
|
+
case 'submit':
|
|
737
|
+
await this.executeHook(this.approvalHooks.beforeSubmit, context);
|
|
738
|
+
break;
|
|
739
|
+
case 'approve':
|
|
740
|
+
await this.executeHook(this.approvalHooks.beforeApprove, context);
|
|
741
|
+
break;
|
|
742
|
+
case 'reject':
|
|
743
|
+
await this.executeHook(this.approvalHooks.beforeReject, context);
|
|
744
|
+
break;
|
|
745
|
+
case 'resubmit':
|
|
746
|
+
await this.executeHook(this.approvalHooks.beforeResubmit, context);
|
|
747
|
+
break;
|
|
748
|
+
case 'requestModification':
|
|
749
|
+
await this.executeHook(this.approvalHooks.beforeRequestModification, { ...context, modifications, note });
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
async fireApprovalAfterHook(action, context, modifications, note) {
|
|
754
|
+
switch (action) {
|
|
755
|
+
case 'submit':
|
|
756
|
+
await this.executeHook(this.approvalHooks.afterSubmit, context);
|
|
757
|
+
break;
|
|
758
|
+
case 'approve':
|
|
759
|
+
await this.executeHook(this.approvalHooks.afterApprove, context);
|
|
760
|
+
break;
|
|
761
|
+
case 'reject':
|
|
762
|
+
await this.executeHook(this.approvalHooks.afterReject, context);
|
|
763
|
+
break;
|
|
764
|
+
case 'resubmit':
|
|
765
|
+
await this.executeHook(this.approvalHooks.afterResubmit, context);
|
|
766
|
+
break;
|
|
767
|
+
case 'requestModification':
|
|
768
|
+
await this.executeHook(this.approvalHooks.afterRequestModification, { ...context, modifications, note });
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
701
771
|
}
|
|
702
772
|
assertTransitionAllowed(action, current, allowedFrom) {
|
|
703
773
|
if (!allowedFrom.includes(current)) {
|
|
@@ -722,6 +792,55 @@ let NestCrudService = class NestCrudService {
|
|
|
722
792
|
approvalVisibleStatuses: [...visible],
|
|
723
793
|
});
|
|
724
794
|
}
|
|
795
|
+
/**
|
|
796
|
+
* Ensures the requester actually changed the fields covered by the stored
|
|
797
|
+
* modification request before a resubmit is allowed. Each item's live value
|
|
798
|
+
* hash is compared against the hash captured at request time; an identical
|
|
799
|
+
* hash means the field was left untouched.
|
|
800
|
+
*/
|
|
801
|
+
async assertResubmitSatisfied(approval, id, user) {
|
|
802
|
+
const checkConfig = this.approvalPipeline?.resubmitCheck;
|
|
803
|
+
const modifications = approval.currentModifications ?? [];
|
|
804
|
+
if (modifications.length === 0) {
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
const fields = modifications.map((m) => m.field);
|
|
808
|
+
const extraRelations = (0, relation_path_helper_1.resolveRelationPaths)(this.repo.metadata, fields);
|
|
809
|
+
const entity = await this.getEntityWithRelations(id, user, extraRelations);
|
|
810
|
+
const ignored = new Set(checkConfig?.ignoreFields ?? []);
|
|
811
|
+
const satisfied = modifications.map((m) => {
|
|
812
|
+
if (ignored.has(m.field)) {
|
|
813
|
+
return { field: m.field, satisfied: true };
|
|
814
|
+
}
|
|
815
|
+
const live = this.readCurrentValue(entity, m.field);
|
|
816
|
+
const satisfiedField = m.currentValueHash !== undefined
|
|
817
|
+
? (0, value_hash_helper_1.hashValue)(live) !== m.currentValueHash
|
|
818
|
+
: m.currentValue !== undefined
|
|
819
|
+
? !(0, value_hash_helper_1.deepEqual)(live, m.currentValue)
|
|
820
|
+
: true;
|
|
821
|
+
return { field: m.field, satisfied: satisfiedField };
|
|
822
|
+
});
|
|
823
|
+
let allowed;
|
|
824
|
+
if (checkConfig?.customChecker) {
|
|
825
|
+
allowed = await checkConfig.customChecker({
|
|
826
|
+
modifications,
|
|
827
|
+
satisfied,
|
|
828
|
+
entity,
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
else {
|
|
832
|
+
const required = satisfied.filter((s) => s.satisfied);
|
|
833
|
+
allowed =
|
|
834
|
+
checkConfig?.mode === 'any' ? required.length > 0 : required.length === satisfied.length;
|
|
835
|
+
}
|
|
836
|
+
if (!allowed) {
|
|
837
|
+
throw (0, nest_error_1.keyed)(common_1.HttpStatus.BAD_REQUEST, nest_error_1.ErrorKey.CRUD_APPROVAL_RESUBMIT_NOT_SATISFIED, {
|
|
838
|
+
unchangedFields: satisfied
|
|
839
|
+
.filter((s) => !s.satisfied)
|
|
840
|
+
.map((s) => s.field),
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
}
|
|
725
844
|
approvalIdCastExpression() {
|
|
726
845
|
const type = this.repo.manager.connection.options.type;
|
|
727
846
|
if (type === 'mysql' || type === 'mariadb' || type === 'aurora-mysql') {
|