@nest-util/nest-crud 2.0.2 → 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 +34 -0
- package/dist/lib/interfaces/approval-pipeline.interface.d.ts.map +1 -1
- package/dist/lib/services/nest-crud.service.d.ts +7 -0
- package/dist/lib/services/nest-crud.service.d.ts.map +1 -1
- package/dist/lib/services/nest-crud.service.js +76 -11
- 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
|
+
}
|
|
@@ -31,6 +31,12 @@ export interface ModificationItem {
|
|
|
31
31
|
* '[Circular]').
|
|
32
32
|
*/
|
|
33
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;
|
|
34
40
|
/** Value the reviewer wants instead. */
|
|
35
41
|
wantedValue: unknown;
|
|
36
42
|
/** Optional human-readable explanation. */
|
|
@@ -106,6 +112,34 @@ export interface ApprovalPipelineConfig {
|
|
|
106
112
|
* `after*`) the previous status.
|
|
107
113
|
*/
|
|
108
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>;
|
|
109
143
|
}
|
|
110
144
|
export interface RequestModificationPayload {
|
|
111
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,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD;;;;;;;;;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;IAE5C;;;;OAIG;IACH,KAAK,CAAC,EAAE,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"approval-pipeline.interface.d.ts","sourceRoot":"","sources":["../../../src/lib/interfaces/approval-pipeline.interface.ts"],"names":[],"mappings":"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"}
|
|
@@ -138,6 +138,13 @@ export declare class NestCrudService<Entity extends ObjectLiteral, CreateDto = P
|
|
|
138
138
|
private assertTransitionAllowed;
|
|
139
139
|
private userHasPermission;
|
|
140
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;
|
|
141
148
|
private approvalIdCastExpression;
|
|
142
149
|
private toApprovalStatusView;
|
|
143
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");
|
|
@@ -559,12 +561,15 @@ let NestCrudService = class NestCrudService {
|
|
|
559
561
|
* Loads the target entity with its configured `include` relations so that
|
|
560
562
|
* relation fields can be read as objects when capturing modification values.
|
|
561
563
|
*/
|
|
562
|
-
async getEntityWithRelations(id, user) {
|
|
564
|
+
async getEntityWithRelations(id, user, extraRelations) {
|
|
563
565
|
if (this.enforceOwnershipFor(user)) {
|
|
564
566
|
return this.findOwnedEntity(id, user);
|
|
565
567
|
}
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
+
const relationPaths = [
|
|
569
|
+
...new Set([...this.include, ...(extraRelations ?? [])]),
|
|
570
|
+
];
|
|
571
|
+
const relations = relationPaths.length
|
|
572
|
+
? this.buildRelationsObject(relationPaths)
|
|
568
573
|
: undefined;
|
|
569
574
|
return this.repo.findOne({
|
|
570
575
|
where: { id },
|
|
@@ -678,14 +683,23 @@ let NestCrudService = class NestCrudService {
|
|
|
678
683
|
case 'requestModification': {
|
|
679
684
|
this.assertTransitionAllowed(action, current, reviewable);
|
|
680
685
|
approval.status = approval_pipeline_interface_1.APPROVAL_STATUS.modificationRequested;
|
|
681
|
-
const
|
|
682
|
-
const
|
|
683
|
-
|
|
684
|
-
|
|
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
|
|
685
694
|
? m.currentValue
|
|
686
|
-
: this.readCurrentValue(entity, m.field))
|
|
687
|
-
|
|
688
|
-
|
|
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
|
+
});
|
|
689
703
|
approval.currentModifications = capturedModifications;
|
|
690
704
|
await this.getHistoryRepository().save(this.getHistoryRepository().create({
|
|
691
705
|
approvalStatusId: approval.id,
|
|
@@ -695,15 +709,17 @@ let NestCrudService = class NestCrudService {
|
|
|
695
709
|
}));
|
|
696
710
|
break;
|
|
697
711
|
}
|
|
698
|
-
case 'resubmit':
|
|
712
|
+
case 'resubmit': {
|
|
699
713
|
if (current !== approval_pipeline_interface_1.APPROVAL_STATUS.modificationRequested) {
|
|
700
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) });
|
|
701
715
|
}
|
|
716
|
+
await this.assertResubmitSatisfied(approval, id, user);
|
|
702
717
|
approval.status = approval_pipeline_interface_1.APPROVAL_STATUS.resubmitted;
|
|
703
718
|
approval.currentModifications = null;
|
|
704
719
|
approval.resubmittedBy = userId;
|
|
705
720
|
approval.resubmittedAt = now;
|
|
706
721
|
break;
|
|
722
|
+
}
|
|
707
723
|
}
|
|
708
724
|
const saved = await this.getApprovalRepository().save(approval);
|
|
709
725
|
const afterContext = {
|
|
@@ -776,6 +792,55 @@ let NestCrudService = class NestCrudService {
|
|
|
776
792
|
approvalVisibleStatuses: [...visible],
|
|
777
793
|
});
|
|
778
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
|
+
}
|
|
779
844
|
approvalIdCastExpression() {
|
|
780
845
|
const type = this.repo.manager.connection.options.type;
|
|
781
846
|
if (type === 'mysql' || type === 'mariadb' || type === 'aurora-mysql') {
|