@heyhru/business-dms-approval 0.4.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/README.md +33 -0
- package/dist/approvals.model.d.ts +14 -0
- package/dist/approvals.model.d.ts.map +1 -0
- package/dist/approvals.service.d.ts +14 -0
- package/dist/approvals.service.d.ts.map +1 -0
- package/dist/approvals.sql.d.ts +6 -0
- package/dist/approvals.sql.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +204 -0
- package/dist/index.mjs +172 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# @heyhru/business-dms-approval
|
|
2
|
+
|
|
3
|
+
DMS approval workflow domain logic: service, model, sql.
|
|
4
|
+
|
|
5
|
+
## Exports
|
|
6
|
+
|
|
7
|
+
### Direct route handlers
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import {
|
|
11
|
+
approvalList,
|
|
12
|
+
approvalGet,
|
|
13
|
+
approvalCreate,
|
|
14
|
+
approvalApprove,
|
|
15
|
+
approvalReject,
|
|
16
|
+
} from "@heyhru/business-dms-approval";
|
|
17
|
+
|
|
18
|
+
app.post("/approvals/list", approvalList);
|
|
19
|
+
app.post("/approvals/get", approvalGet);
|
|
20
|
+
app.post("/approvals/create", approvalCreate);
|
|
21
|
+
app.post("/approvals/approve", approvalApprove);
|
|
22
|
+
app.post("/approvals/reject", approvalReject);
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Curried handler (encryptionKey required)
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { approvalExecute } from "@heyhru/business-dms-approval";
|
|
29
|
+
|
|
30
|
+
app.post("/approvals/execute", approvalExecute(config.encryptionKey));
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`approvalExecute` uses `FastifyBaseLogger` (via `req.log`) internally — no additional logger setup needed.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare function listApprovals(filters?: {
|
|
2
|
+
status?: string;
|
|
3
|
+
submittedBy?: string;
|
|
4
|
+
}): Promise<Record<string, unknown>[]>;
|
|
5
|
+
export declare function getApprovalById(id: string): Promise<Record<string, unknown> | undefined>;
|
|
6
|
+
export declare function insertApproval(dataSourceId: string, sqlText: string, submittedBy: string): Promise<Record<string, unknown> | undefined>;
|
|
7
|
+
export declare function updateReview(id: string, status: string, reviewedBy: string, rejectReason: string | null): Promise<Record<string, unknown> | undefined>;
|
|
8
|
+
export declare function setExecuting(id: string): Promise<{
|
|
9
|
+
changes: number;
|
|
10
|
+
}>;
|
|
11
|
+
export declare function setExecuteResult(id: string, status: string, result: string): Promise<{
|
|
12
|
+
changes: number;
|
|
13
|
+
}>;
|
|
14
|
+
//# sourceMappingURL=approvals.model.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approvals.model.d.ts","sourceRoot":"","sources":["../src/approvals.model.ts"],"names":[],"mappings":"AAGA,wBAAsB,aAAa,CAAC,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,sCAatF;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,gDAEzC;AAED,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,gDAExF;AAED,wBAAgB,YAAY,CAC1B,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,GAAG,IAAI,gDAG5B;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM;;GAEtC;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;;GAE1E"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type FastifyRequest, type FastifyReply } from "fastify";
|
|
2
|
+
interface AuthenticatedRequest extends FastifyRequest {
|
|
3
|
+
user: {
|
|
4
|
+
id: string;
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
export declare function approvalList(req: AuthenticatedRequest, reply: FastifyReply): Promise<never>;
|
|
8
|
+
export declare function approvalGet(req: FastifyRequest, reply: FastifyReply): Promise<never>;
|
|
9
|
+
export declare function approvalCreate(req: AuthenticatedRequest, reply: FastifyReply): Promise<never>;
|
|
10
|
+
export declare function approvalApprove(req: AuthenticatedRequest, reply: FastifyReply): Promise<never>;
|
|
11
|
+
export declare function approvalReject(req: AuthenticatedRequest, reply: FastifyReply): Promise<never>;
|
|
12
|
+
export declare function approvalExecute(encryptionKey: string): (req: AuthenticatedRequest, reply: FastifyReply) => Promise<never>;
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=approvals.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approvals.service.d.ts","sourceRoot":"","sources":["../src/approvals.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,YAAY,EAA0B,MAAM,SAAS,CAAC;AAYzF,UAAU,oBAAqB,SAAQ,cAAc;IACnD,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;CACtB;AAED,wBAAsB,YAAY,CAAC,GAAG,EAAE,oBAAoB,EAAE,KAAK,EAAE,YAAY,kBAQhF;AAED,wBAAsB,WAAW,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,kBAKzE;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,oBAAoB,EAAE,KAAK,EAAE,YAAY,kBAQlF;AAkBD,wBAAsB,eAAe,CAAC,GAAG,EAAE,oBAAoB,EAAE,KAAK,EAAE,YAAY,kBAUnF;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,oBAAoB,EAAE,KAAK,EAAE,YAAY,kBAUlF;AAED,wBAAgB,eAAe,CAAC,aAAa,EAAE,MAAM,IACrC,KAAK,oBAAoB,EAAE,OAAO,YAAY,oBAY7D"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const FIND_BY_ID = "\nSELECT *\nFROM approvals\nWHERE id = ?";
|
|
2
|
+
export declare const CREATE = "\nINSERT INTO approvals (data_source_id, sql_text, submitted_by)\nVALUES (?, ?, ?)\nRETURNING *";
|
|
3
|
+
export declare const UPDATE_REVIEW: () => string;
|
|
4
|
+
export declare const UPDATE_EXECUTING: () => string;
|
|
5
|
+
export declare const UPDATE_RESULT: () => string;
|
|
6
|
+
//# sourceMappingURL=approvals.sql.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approvals.sql.d.ts","sourceRoot":"","sources":["../src/approvals.sql.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,UAAU,6CAGV,CAAC;AAEd,eAAO,MAAM,MAAM,oGAGP,CAAC;AAEb,eAAO,MAAM,aAAa,cAId,CAAC;AAEb,eAAO,MAAM,gBAAgB,cAGhB,CAAC;AAEd,eAAO,MAAM,aAAa,cAGb,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,WAAW,EACX,cAAc,EACd,eAAe,EACf,cAAc,EACd,eAAe,GAChB,MAAM,wBAAwB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
approvalApprove: () => approvalApprove,
|
|
24
|
+
approvalCreate: () => approvalCreate,
|
|
25
|
+
approvalExecute: () => approvalExecute,
|
|
26
|
+
approvalGet: () => approvalGet,
|
|
27
|
+
approvalList: () => approvalList,
|
|
28
|
+
approvalReject: () => approvalReject
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(index_exports);
|
|
31
|
+
|
|
32
|
+
// src/approvals.service.ts
|
|
33
|
+
var import_business_dms_datasource = require("@heyhru/business-dms-datasource");
|
|
34
|
+
var import_business_dms_audit = require("@heyhru/business-dms-audit");
|
|
35
|
+
|
|
36
|
+
// src/approvals.model.ts
|
|
37
|
+
var import_server_plugin_pg = require("@heyhru/server-plugin-pg");
|
|
38
|
+
|
|
39
|
+
// src/approvals.sql.ts
|
|
40
|
+
var FIND_BY_ID = `
|
|
41
|
+
SELECT *
|
|
42
|
+
FROM approvals
|
|
43
|
+
WHERE id = ?`;
|
|
44
|
+
var CREATE = `
|
|
45
|
+
INSERT INTO approvals (data_source_id, sql_text, submitted_by)
|
|
46
|
+
VALUES (?, ?, ?)
|
|
47
|
+
RETURNING *`;
|
|
48
|
+
var UPDATE_REVIEW = () => `
|
|
49
|
+
UPDATE approvals
|
|
50
|
+
SET status = ?, reviewed_by = ?, reject_reason = ?, updated_at = NOW()
|
|
51
|
+
WHERE id = ?
|
|
52
|
+
RETURNING *`;
|
|
53
|
+
var UPDATE_EXECUTING = () => `
|
|
54
|
+
UPDATE approvals
|
|
55
|
+
SET status = 'executing', updated_at = NOW()
|
|
56
|
+
WHERE id = ?`;
|
|
57
|
+
var UPDATE_RESULT = () => `
|
|
58
|
+
UPDATE approvals
|
|
59
|
+
SET status = ?, execute_result = ?, updated_at = NOW()
|
|
60
|
+
WHERE id = ?`;
|
|
61
|
+
|
|
62
|
+
// src/approvals.model.ts
|
|
63
|
+
async function listApprovals(filters) {
|
|
64
|
+
let query = "SELECT * FROM approvals WHERE 1=1";
|
|
65
|
+
const params = [];
|
|
66
|
+
if (filters?.status) {
|
|
67
|
+
query += " AND status = ?";
|
|
68
|
+
params.push(filters.status);
|
|
69
|
+
}
|
|
70
|
+
if (filters?.submittedBy) {
|
|
71
|
+
query += " AND submitted_by = ?";
|
|
72
|
+
params.push(filters.submittedBy);
|
|
73
|
+
}
|
|
74
|
+
query += " ORDER BY created_at DESC";
|
|
75
|
+
return (0, import_server_plugin_pg.getPgDb)().query(query, params);
|
|
76
|
+
}
|
|
77
|
+
function getApprovalById(id) {
|
|
78
|
+
return (0, import_server_plugin_pg.getPgDb)().queryOne(FIND_BY_ID, [id]);
|
|
79
|
+
}
|
|
80
|
+
function insertApproval(dataSourceId, sqlText, submittedBy) {
|
|
81
|
+
return (0, import_server_plugin_pg.getPgDb)().queryOne(CREATE, [dataSourceId, sqlText, submittedBy]);
|
|
82
|
+
}
|
|
83
|
+
function updateReview(id, status, reviewedBy, rejectReason) {
|
|
84
|
+
return (0, import_server_plugin_pg.getPgDb)().queryOne(UPDATE_REVIEW(), [status, reviewedBy, rejectReason, id]);
|
|
85
|
+
}
|
|
86
|
+
function setExecuting(id) {
|
|
87
|
+
return (0, import_server_plugin_pg.getPgDb)().run(UPDATE_EXECUTING(), [id]);
|
|
88
|
+
}
|
|
89
|
+
function setExecuteResult(id, status, result) {
|
|
90
|
+
return (0, import_server_plugin_pg.getPgDb)().run(UPDATE_RESULT(), [status, result, id]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/approvals.service.ts
|
|
94
|
+
async function approvalList(req, reply) {
|
|
95
|
+
const { status, mine } = req.body ?? {};
|
|
96
|
+
return reply.send(
|
|
97
|
+
await listApprovals({
|
|
98
|
+
status,
|
|
99
|
+
submittedBy: mine === "true" ? req.user.id : void 0
|
|
100
|
+
})
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
async function approvalGet(req, reply) {
|
|
104
|
+
const { id } = req.body ?? {};
|
|
105
|
+
const approval = await getApprovalById(id);
|
|
106
|
+
if (!approval) return reply.code(404).send({ error: "Not found" });
|
|
107
|
+
return reply.send(approval);
|
|
108
|
+
}
|
|
109
|
+
async function approvalCreate(req, reply) {
|
|
110
|
+
const { dataSourceId, sql } = req.body ?? {};
|
|
111
|
+
if (!dataSourceId || !sql) {
|
|
112
|
+
return reply.code(400).send({ error: "Data source ID and SQL are required" });
|
|
113
|
+
}
|
|
114
|
+
const approval = await insertApproval(dataSourceId, sql, req.user.id);
|
|
115
|
+
req.log.info("Approval submitted (user=%s)", req.user.id);
|
|
116
|
+
return reply.code(201).send(approval);
|
|
117
|
+
}
|
|
118
|
+
async function reviewApproval(id, reviewerId, decision, rejectReason) {
|
|
119
|
+
const approval = await getApprovalById(id);
|
|
120
|
+
if (!approval || approval["status"] !== "pending") {
|
|
121
|
+
throw new Error("Approval not found or not in pending status");
|
|
122
|
+
}
|
|
123
|
+
if (approval["submitted_by"] === reviewerId) {
|
|
124
|
+
throw new Error("Cannot approve your own submission");
|
|
125
|
+
}
|
|
126
|
+
return updateReview(id, decision, reviewerId, rejectReason ?? null);
|
|
127
|
+
}
|
|
128
|
+
async function approvalApprove(req, reply) {
|
|
129
|
+
const { id } = req.body ?? {};
|
|
130
|
+
try {
|
|
131
|
+
const result = await reviewApproval(id, req.user.id, "approved");
|
|
132
|
+
req.log.info("Approval approved (id=%s, reviewer=%s)", id, req.user.id);
|
|
133
|
+
return reply.send(result);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
req.log.warn(err, "Approve failed (id=%s, reviewer=%s)", id, req.user.id);
|
|
136
|
+
return reply.code(400).send({ error: err instanceof Error ? err.message : String(err) });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async function approvalReject(req, reply) {
|
|
140
|
+
const { id, reason } = req.body ?? {};
|
|
141
|
+
try {
|
|
142
|
+
const result = await reviewApproval(id, req.user.id, "rejected", reason);
|
|
143
|
+
req.log.info("Approval rejected (id=%s, reviewer=%s)", id, req.user.id);
|
|
144
|
+
return reply.send(result);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
req.log.warn(err, "Reject failed (id=%s, reviewer=%s)", id, req.user.id);
|
|
147
|
+
return reply.code(400).send({ error: err instanceof Error ? err.message : String(err) });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function approvalExecute(encryptionKey) {
|
|
151
|
+
return async (req, reply) => {
|
|
152
|
+
const { id } = req.body ?? {};
|
|
153
|
+
const ip = req.headers["x-forwarded-for"] ?? req.headers["x-real-ip"] ?? "unknown";
|
|
154
|
+
try {
|
|
155
|
+
const result = await doExecuteApproval({ id, userId: req.user.id, ip, encryptionKey }, req.log);
|
|
156
|
+
return reply.send(result);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
req.log.error(err, "Execute approval failed (id=%s, user=%s)", id, req.user.id);
|
|
159
|
+
return reply.code(400).send({ error: err instanceof Error ? err.message : String(err) });
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
async function doExecuteApproval({ id, userId, ip, encryptionKey }, log) {
|
|
164
|
+
const approval = await getApprovalById(id);
|
|
165
|
+
if (!approval || !["approved", "execute_failed"].includes(approval["status"])) {
|
|
166
|
+
throw new Error("Approval not found or not approved");
|
|
167
|
+
}
|
|
168
|
+
await setExecuting(id);
|
|
169
|
+
try {
|
|
170
|
+
const ds = await (0, import_business_dms_datasource.getDataSourceWithPassword)(
|
|
171
|
+
approval["data_source_id"],
|
|
172
|
+
encryptionKey
|
|
173
|
+
);
|
|
174
|
+
if (!ds) throw new Error("Data source not found");
|
|
175
|
+
const pool = (0, import_business_dms_datasource.getPool)({ ...ds, database: ds.database ?? "" });
|
|
176
|
+
const rows = await pool.execute(approval["sql_text"]);
|
|
177
|
+
const result = `${rows.length} rows affected`;
|
|
178
|
+
await setExecuteResult(id, "executed", result);
|
|
179
|
+
log.info("Approval executed (id=%s, user=%s)", id, userId);
|
|
180
|
+
await (0, import_business_dms_audit.writeAuditLog)({
|
|
181
|
+
userId,
|
|
182
|
+
dataSourceId: approval["data_source_id"],
|
|
183
|
+
action: "DML_EXECUTE",
|
|
184
|
+
sqlText: approval["sql_text"],
|
|
185
|
+
resultSummary: result,
|
|
186
|
+
ipAddress: ip
|
|
187
|
+
});
|
|
188
|
+
return getApprovalById(id);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
191
|
+
await setExecuteResult(id, "execute_failed", message);
|
|
192
|
+
log.error(err, "Approval execute_failed (id=%s)", id);
|
|
193
|
+
throw err;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
197
|
+
0 && (module.exports = {
|
|
198
|
+
approvalApprove,
|
|
199
|
+
approvalCreate,
|
|
200
|
+
approvalExecute,
|
|
201
|
+
approvalGet,
|
|
202
|
+
approvalList,
|
|
203
|
+
approvalReject
|
|
204
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// src/approvals.service.ts
|
|
2
|
+
import { getDataSourceWithPassword, getPool } from "@heyhru/business-dms-datasource";
|
|
3
|
+
import { writeAuditLog } from "@heyhru/business-dms-audit";
|
|
4
|
+
|
|
5
|
+
// src/approvals.model.ts
|
|
6
|
+
import { getPgDb } from "@heyhru/server-plugin-pg";
|
|
7
|
+
|
|
8
|
+
// src/approvals.sql.ts
|
|
9
|
+
var FIND_BY_ID = `
|
|
10
|
+
SELECT *
|
|
11
|
+
FROM approvals
|
|
12
|
+
WHERE id = ?`;
|
|
13
|
+
var CREATE = `
|
|
14
|
+
INSERT INTO approvals (data_source_id, sql_text, submitted_by)
|
|
15
|
+
VALUES (?, ?, ?)
|
|
16
|
+
RETURNING *`;
|
|
17
|
+
var UPDATE_REVIEW = () => `
|
|
18
|
+
UPDATE approvals
|
|
19
|
+
SET status = ?, reviewed_by = ?, reject_reason = ?, updated_at = NOW()
|
|
20
|
+
WHERE id = ?
|
|
21
|
+
RETURNING *`;
|
|
22
|
+
var UPDATE_EXECUTING = () => `
|
|
23
|
+
UPDATE approvals
|
|
24
|
+
SET status = 'executing', updated_at = NOW()
|
|
25
|
+
WHERE id = ?`;
|
|
26
|
+
var UPDATE_RESULT = () => `
|
|
27
|
+
UPDATE approvals
|
|
28
|
+
SET status = ?, execute_result = ?, updated_at = NOW()
|
|
29
|
+
WHERE id = ?`;
|
|
30
|
+
|
|
31
|
+
// src/approvals.model.ts
|
|
32
|
+
async function listApprovals(filters) {
|
|
33
|
+
let query = "SELECT * FROM approvals WHERE 1=1";
|
|
34
|
+
const params = [];
|
|
35
|
+
if (filters?.status) {
|
|
36
|
+
query += " AND status = ?";
|
|
37
|
+
params.push(filters.status);
|
|
38
|
+
}
|
|
39
|
+
if (filters?.submittedBy) {
|
|
40
|
+
query += " AND submitted_by = ?";
|
|
41
|
+
params.push(filters.submittedBy);
|
|
42
|
+
}
|
|
43
|
+
query += " ORDER BY created_at DESC";
|
|
44
|
+
return getPgDb().query(query, params);
|
|
45
|
+
}
|
|
46
|
+
function getApprovalById(id) {
|
|
47
|
+
return getPgDb().queryOne(FIND_BY_ID, [id]);
|
|
48
|
+
}
|
|
49
|
+
function insertApproval(dataSourceId, sqlText, submittedBy) {
|
|
50
|
+
return getPgDb().queryOne(CREATE, [dataSourceId, sqlText, submittedBy]);
|
|
51
|
+
}
|
|
52
|
+
function updateReview(id, status, reviewedBy, rejectReason) {
|
|
53
|
+
return getPgDb().queryOne(UPDATE_REVIEW(), [status, reviewedBy, rejectReason, id]);
|
|
54
|
+
}
|
|
55
|
+
function setExecuting(id) {
|
|
56
|
+
return getPgDb().run(UPDATE_EXECUTING(), [id]);
|
|
57
|
+
}
|
|
58
|
+
function setExecuteResult(id, status, result) {
|
|
59
|
+
return getPgDb().run(UPDATE_RESULT(), [status, result, id]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/approvals.service.ts
|
|
63
|
+
async function approvalList(req, reply) {
|
|
64
|
+
const { status, mine } = req.body ?? {};
|
|
65
|
+
return reply.send(
|
|
66
|
+
await listApprovals({
|
|
67
|
+
status,
|
|
68
|
+
submittedBy: mine === "true" ? req.user.id : void 0
|
|
69
|
+
})
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
async function approvalGet(req, reply) {
|
|
73
|
+
const { id } = req.body ?? {};
|
|
74
|
+
const approval = await getApprovalById(id);
|
|
75
|
+
if (!approval) return reply.code(404).send({ error: "Not found" });
|
|
76
|
+
return reply.send(approval);
|
|
77
|
+
}
|
|
78
|
+
async function approvalCreate(req, reply) {
|
|
79
|
+
const { dataSourceId, sql } = req.body ?? {};
|
|
80
|
+
if (!dataSourceId || !sql) {
|
|
81
|
+
return reply.code(400).send({ error: "Data source ID and SQL are required" });
|
|
82
|
+
}
|
|
83
|
+
const approval = await insertApproval(dataSourceId, sql, req.user.id);
|
|
84
|
+
req.log.info("Approval submitted (user=%s)", req.user.id);
|
|
85
|
+
return reply.code(201).send(approval);
|
|
86
|
+
}
|
|
87
|
+
async function reviewApproval(id, reviewerId, decision, rejectReason) {
|
|
88
|
+
const approval = await getApprovalById(id);
|
|
89
|
+
if (!approval || approval["status"] !== "pending") {
|
|
90
|
+
throw new Error("Approval not found or not in pending status");
|
|
91
|
+
}
|
|
92
|
+
if (approval["submitted_by"] === reviewerId) {
|
|
93
|
+
throw new Error("Cannot approve your own submission");
|
|
94
|
+
}
|
|
95
|
+
return updateReview(id, decision, reviewerId, rejectReason ?? null);
|
|
96
|
+
}
|
|
97
|
+
async function approvalApprove(req, reply) {
|
|
98
|
+
const { id } = req.body ?? {};
|
|
99
|
+
try {
|
|
100
|
+
const result = await reviewApproval(id, req.user.id, "approved");
|
|
101
|
+
req.log.info("Approval approved (id=%s, reviewer=%s)", id, req.user.id);
|
|
102
|
+
return reply.send(result);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
req.log.warn(err, "Approve failed (id=%s, reviewer=%s)", id, req.user.id);
|
|
105
|
+
return reply.code(400).send({ error: err instanceof Error ? err.message : String(err) });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function approvalReject(req, reply) {
|
|
109
|
+
const { id, reason } = req.body ?? {};
|
|
110
|
+
try {
|
|
111
|
+
const result = await reviewApproval(id, req.user.id, "rejected", reason);
|
|
112
|
+
req.log.info("Approval rejected (id=%s, reviewer=%s)", id, req.user.id);
|
|
113
|
+
return reply.send(result);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
req.log.warn(err, "Reject failed (id=%s, reviewer=%s)", id, req.user.id);
|
|
116
|
+
return reply.code(400).send({ error: err instanceof Error ? err.message : String(err) });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function approvalExecute(encryptionKey) {
|
|
120
|
+
return async (req, reply) => {
|
|
121
|
+
const { id } = req.body ?? {};
|
|
122
|
+
const ip = req.headers["x-forwarded-for"] ?? req.headers["x-real-ip"] ?? "unknown";
|
|
123
|
+
try {
|
|
124
|
+
const result = await doExecuteApproval({ id, userId: req.user.id, ip, encryptionKey }, req.log);
|
|
125
|
+
return reply.send(result);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
req.log.error(err, "Execute approval failed (id=%s, user=%s)", id, req.user.id);
|
|
128
|
+
return reply.code(400).send({ error: err instanceof Error ? err.message : String(err) });
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
async function doExecuteApproval({ id, userId, ip, encryptionKey }, log) {
|
|
133
|
+
const approval = await getApprovalById(id);
|
|
134
|
+
if (!approval || !["approved", "execute_failed"].includes(approval["status"])) {
|
|
135
|
+
throw new Error("Approval not found or not approved");
|
|
136
|
+
}
|
|
137
|
+
await setExecuting(id);
|
|
138
|
+
try {
|
|
139
|
+
const ds = await getDataSourceWithPassword(
|
|
140
|
+
approval["data_source_id"],
|
|
141
|
+
encryptionKey
|
|
142
|
+
);
|
|
143
|
+
if (!ds) throw new Error("Data source not found");
|
|
144
|
+
const pool = getPool({ ...ds, database: ds.database ?? "" });
|
|
145
|
+
const rows = await pool.execute(approval["sql_text"]);
|
|
146
|
+
const result = `${rows.length} rows affected`;
|
|
147
|
+
await setExecuteResult(id, "executed", result);
|
|
148
|
+
log.info("Approval executed (id=%s, user=%s)", id, userId);
|
|
149
|
+
await writeAuditLog({
|
|
150
|
+
userId,
|
|
151
|
+
dataSourceId: approval["data_source_id"],
|
|
152
|
+
action: "DML_EXECUTE",
|
|
153
|
+
sqlText: approval["sql_text"],
|
|
154
|
+
resultSummary: result,
|
|
155
|
+
ipAddress: ip
|
|
156
|
+
});
|
|
157
|
+
return getApprovalById(id);
|
|
158
|
+
} catch (err) {
|
|
159
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
160
|
+
await setExecuteResult(id, "execute_failed", message);
|
|
161
|
+
log.error(err, "Approval execute_failed (id=%s)", id);
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
export {
|
|
166
|
+
approvalApprove,
|
|
167
|
+
approvalCreate,
|
|
168
|
+
approvalExecute,
|
|
169
|
+
approvalGet,
|
|
170
|
+
approvalList,
|
|
171
|
+
approvalReject
|
|
172
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@heyhru/business-dms-approval",
|
|
3
|
+
"publishConfig": {
|
|
4
|
+
"access": "public"
|
|
5
|
+
},
|
|
6
|
+
"version": "0.4.0",
|
|
7
|
+
"description": "DMS approval workflow domain logic: service, model, sql",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"module": "./dist/index.mjs",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.mjs",
|
|
15
|
+
"require": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup && tsc --emitDeclarationOnly",
|
|
23
|
+
"dev": "tsup --watch",
|
|
24
|
+
"lint": "eslint src",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"clean": "rm -rf dist"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@heyhru/business-dms-audit": "0.4.0",
|
|
30
|
+
"@heyhru/business-dms-datasource": "0.4.0",
|
|
31
|
+
"@heyhru/server-plugin-pg": "0.4.0",
|
|
32
|
+
"fastify": "^5.8.4"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"tsup": "^8.5.1",
|
|
36
|
+
"typescript": "^6.0.2",
|
|
37
|
+
"vitest": "^4.1.2"
|
|
38
|
+
},
|
|
39
|
+
"gitHead": "48f00e1f0b1ca5d0789c0df9384a12f2c8e847c4"
|
|
40
|
+
}
|