@heyhru/business-dms-approval 0.11.0 → 0.11.2
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/index.js +7 -260
- package/dist/index.mjs +7 -225
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1,275 +1,22 @@
|
|
|
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
|
-
getApprovalById: () => getApprovalById,
|
|
30
|
-
setExecuteResult: () => setExecuteResult,
|
|
31
|
-
setExecuting: () => setExecuting
|
|
32
|
-
});
|
|
33
|
-
module.exports = __toCommonJS(index_exports);
|
|
34
|
-
|
|
35
|
-
// src/approvals.service.ts
|
|
36
|
-
var import_business_dms_datasource = require("@heyhru/business-dms-datasource");
|
|
37
|
-
var import_business_dms_audit = require("@heyhru/business-dms-audit");
|
|
38
|
-
|
|
39
|
-
// src/approvals.model.ts
|
|
40
|
-
var import_server_plugin_pg = require("@heyhru/server-plugin-pg");
|
|
41
|
-
|
|
42
|
-
// src/approvals.sql.ts
|
|
43
|
-
var FIND_BY_ID = `
|
|
1
|
+
"use strict";var y=Object.defineProperty;var j=Object.getOwnPropertyDescriptor;var M=Object.getOwnPropertyNames;var Q=Object.prototype.hasOwnProperty;var G=(t,e)=>{for(var r in e)y(t,r,{get:e[r],enumerable:!0})},H=(t,e,r,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of M(e))!Q.call(t,a)&&a!==r&&y(t,a,{get:()=>e[a],enumerable:!(s=j(e,a))||s.enumerable});return t};var X=t=>H(y({},"__esModule",{value:!0}),t);var k={};G(k,{approvalApprove:()=>L,approvalCreate:()=>O,approvalExecute:()=>B,approvalGet:()=>F,approvalList:()=>N,approvalReject:()=>U,getApprovalById:()=>c,setExecuteResult:()=>f,setExecuting:()=>v});module.exports=X(k);var l=require("@heyhru/business-dms-datasource"),I=require("@heyhru/business-dms-audit");var d=require("@heyhru/server-plugin-pg");var w=`
|
|
44
2
|
SELECT *
|
|
45
3
|
FROM approvals
|
|
46
|
-
WHERE id =
|
|
47
|
-
var CREATE = `
|
|
4
|
+
WHERE id = ?`,A=`
|
|
48
5
|
INSERT INTO approvals (data_source_id, db_name, sql_text, submitted_by, sql_type)
|
|
49
6
|
VALUES (?, ?, ?, ?, ?)
|
|
50
|
-
RETURNING
|
|
51
|
-
var UPDATE_REVIEW = () => `
|
|
7
|
+
RETURNING *`,R=()=>`
|
|
52
8
|
UPDATE approvals
|
|
53
9
|
SET status = ?, reviewed_by = ?, reject_reason = ?, updated_at = NOW()
|
|
54
10
|
WHERE id = ?
|
|
55
|
-
RETURNING
|
|
56
|
-
var UPDATE_EXECUTING = () => `
|
|
11
|
+
RETURNING *`,x=()=>`
|
|
57
12
|
UPDATE approvals
|
|
58
13
|
SET status = 'executing', updated_at = NOW()
|
|
59
|
-
WHERE id =
|
|
60
|
-
var UPDATE_RESULT = () => `
|
|
14
|
+
WHERE id = ?`,_=()=>`
|
|
61
15
|
UPDATE approvals
|
|
62
16
|
SET status = ?, execute_result = ?, updated_at = NOW()
|
|
63
|
-
WHERE id = ?`;
|
|
64
|
-
|
|
65
|
-
// src/approvals.model.ts
|
|
66
|
-
function buildWhereParams(filters) {
|
|
67
|
-
const clauses = [];
|
|
68
|
-
const params = [];
|
|
69
|
-
if (filters?.status) {
|
|
70
|
-
clauses.push("a.status = ?");
|
|
71
|
-
params.push(filters.status);
|
|
72
|
-
}
|
|
73
|
-
if (filters?.submittedBy) {
|
|
74
|
-
clauses.push("a.submitted_by = ?");
|
|
75
|
-
params.push(filters.submittedBy);
|
|
76
|
-
}
|
|
77
|
-
return { where: clauses.length ? " AND " + clauses.join(" AND ") : "", params };
|
|
78
|
-
}
|
|
79
|
-
async function countApprovals(filters) {
|
|
80
|
-
const { where, params } = buildWhereParams(filters);
|
|
81
|
-
const row = await (0, import_server_plugin_pg.getPgDb)().queryOne(
|
|
82
|
-
`SELECT COUNT(*) AS total FROM approvals a WHERE 1=1` + where,
|
|
83
|
-
params
|
|
84
|
-
);
|
|
85
|
-
return Number(row?.["total"] ?? 0);
|
|
86
|
-
}
|
|
87
|
-
async function listApprovals(filters) {
|
|
88
|
-
let query = `SELECT a.*, u.username AS submitter_name,
|
|
17
|
+
WHERE id = ?`;function b(t){let e=[],r=[];return t?.status&&(e.push("a.status = ?"),r.push(t.status)),t?.submittedBy&&(e.push("a.submitted_by = ?"),r.push(t.submittedBy)),{where:e.length?" AND "+e.join(" AND "):"",params:r}}async function T(t){let{where:e,params:r}=b(t),s=await(0,d.getPgDb)().queryOne("SELECT COUNT(*) AS total FROM approvals a WHERE 1=1"+e,r);return Number(s?.total??0)}async function h(t){let e=`SELECT a.*, u.username AS submitter_name,
|
|
89
18
|
ds.name AS data_source_name, ds.type AS data_source_type
|
|
90
19
|
FROM approvals a
|
|
91
20
|
LEFT JOIN users u ON u.id = a.submitted_by
|
|
92
21
|
LEFT JOIN data_sources ds ON ds.id = a.data_source_id
|
|
93
|
-
WHERE 1=1`;
|
|
94
|
-
const { where, params } = buildWhereParams(filters);
|
|
95
|
-
query += where + " ORDER BY a.created_at DESC LIMIT ? OFFSET ?";
|
|
96
|
-
params.push(filters?.limit ?? 50, filters?.offset ?? 0);
|
|
97
|
-
return (0, import_server_plugin_pg.getPgDb)().query(query, params);
|
|
98
|
-
}
|
|
99
|
-
function getApprovalById(id) {
|
|
100
|
-
return (0, import_server_plugin_pg.getPgDb)().queryOne(FIND_BY_ID, [id]);
|
|
101
|
-
}
|
|
102
|
-
function insertApproval({ dataSourceId, dbName, sqlText, submittedBy, sqlType = "DML" }) {
|
|
103
|
-
return (0, import_server_plugin_pg.getPgDb)().queryOne(CREATE, [dataSourceId, dbName, sqlText, submittedBy, sqlType]);
|
|
104
|
-
}
|
|
105
|
-
function updateReview(id, status, reviewedBy, rejectReason) {
|
|
106
|
-
return (0, import_server_plugin_pg.getPgDb)().queryOne(UPDATE_REVIEW(), [status, reviewedBy, rejectReason, id]);
|
|
107
|
-
}
|
|
108
|
-
function setExecuting(id) {
|
|
109
|
-
return (0, import_server_plugin_pg.getPgDb)().run(UPDATE_EXECUTING(), [id]);
|
|
110
|
-
}
|
|
111
|
-
function setExecuteResult(id, status, result) {
|
|
112
|
-
return (0, import_server_plugin_pg.getPgDb)().run(UPDATE_RESULT(), [status, result, id]);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// src/approvals.service.ts
|
|
116
|
-
async function approvalList(req, reply) {
|
|
117
|
-
const { status, mine, limit, offset } = req.body ?? {};
|
|
118
|
-
const ownOnly = req.user.role === "developer" || req.user.role === "maintainer" || String(mine) === "true";
|
|
119
|
-
const filters = {
|
|
120
|
-
status,
|
|
121
|
-
submittedBy: ownOnly ? req.user.id : void 0,
|
|
122
|
-
limit: limit ? Number(limit) : void 0,
|
|
123
|
-
offset: offset ? Number(offset) : void 0
|
|
124
|
-
};
|
|
125
|
-
const [rows, total] = await Promise.all([listApprovals(filters), countApprovals(filters)]);
|
|
126
|
-
return reply.send({ rows, total });
|
|
127
|
-
}
|
|
128
|
-
async function approvalGet(req, reply) {
|
|
129
|
-
const { id } = req.body ?? {};
|
|
130
|
-
const approval = await getApprovalById(id);
|
|
131
|
-
if (!approval) return reply.code(404).send({ error: "Not found" });
|
|
132
|
-
return reply.send(approval);
|
|
133
|
-
}
|
|
134
|
-
async function approvalCreate(req, reply) {
|
|
135
|
-
const { dataSourceId, database, sql, sqlType } = req.body ?? {};
|
|
136
|
-
if (!dataSourceId || !sql) {
|
|
137
|
-
return reply.code(400).send({ error: "Data source ID and SQL are required" });
|
|
138
|
-
}
|
|
139
|
-
const type = sqlType === "EXPORT" ? "EXPORT" : "DML";
|
|
140
|
-
const approval = await insertApproval({
|
|
141
|
-
dataSourceId,
|
|
142
|
-
dbName: database ?? null,
|
|
143
|
-
sqlText: sql,
|
|
144
|
-
submittedBy: req.user.id,
|
|
145
|
-
sqlType: type
|
|
146
|
-
});
|
|
147
|
-
req.log.info("Approval submitted (user=%s, type=%s)", req.user.id, type);
|
|
148
|
-
return reply.code(201).send(approval);
|
|
149
|
-
}
|
|
150
|
-
async function reviewApproval(id, reviewerId, decision, rejectReason) {
|
|
151
|
-
const approval = await getApprovalById(id);
|
|
152
|
-
if (!approval || approval["status"] !== "pending") {
|
|
153
|
-
throw new Error("Approval not found or not in pending status");
|
|
154
|
-
}
|
|
155
|
-
if (approval["submitted_by"] === reviewerId) {
|
|
156
|
-
throw new Error("Cannot approve your own submission");
|
|
157
|
-
}
|
|
158
|
-
return updateReview(id, decision, reviewerId, rejectReason ?? null);
|
|
159
|
-
}
|
|
160
|
-
async function approvalApprove(req, reply) {
|
|
161
|
-
const { id } = req.body ?? {};
|
|
162
|
-
try {
|
|
163
|
-
const result = await reviewApproval(id, req.user.id, "approved");
|
|
164
|
-
req.log.info("Approval approved (id=%s, reviewer=%s)", id, req.user.id);
|
|
165
|
-
return reply.send(result);
|
|
166
|
-
} catch (err) {
|
|
167
|
-
req.log.warn(err, "Approve failed (id=%s, reviewer=%s)", id, req.user.id);
|
|
168
|
-
return reply.code(400).send({ error: err instanceof Error ? err.message : String(err) });
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
async function approvalReject(req, reply) {
|
|
172
|
-
const { id, reason } = req.body ?? {};
|
|
173
|
-
try {
|
|
174
|
-
const result = await reviewApproval(id, req.user.id, "rejected", reason);
|
|
175
|
-
req.log.info("Approval rejected (id=%s, reviewer=%s)", id, req.user.id);
|
|
176
|
-
return reply.send(result);
|
|
177
|
-
} catch (err) {
|
|
178
|
-
req.log.warn(err, "Reject failed (id=%s, reviewer=%s)", id, req.user.id);
|
|
179
|
-
return reply.code(400).send({ error: err instanceof Error ? err.message : String(err) });
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
function approvalExecute(encryptionKey) {
|
|
183
|
-
return async (req, reply) => {
|
|
184
|
-
const { id } = req.body ?? {};
|
|
185
|
-
const ip = req.headers["x-forwarded-for"] ?? req.headers["x-real-ip"] ?? "unknown";
|
|
186
|
-
try {
|
|
187
|
-
const result = await doExecuteApproval({ id, userId: req.user.id, ip, encryptionKey }, req.log);
|
|
188
|
-
return reply.send(result);
|
|
189
|
-
} catch (err) {
|
|
190
|
-
req.log.error(err, "Execute approval failed (id=%s, user=%s)", id, req.user.id);
|
|
191
|
-
return reply.code(400).send({ error: err instanceof Error ? err.message : String(err) });
|
|
192
|
-
}
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
function splitStatements(sql) {
|
|
196
|
-
const statements = [];
|
|
197
|
-
let current = "";
|
|
198
|
-
let inSingle = false;
|
|
199
|
-
let inDouble = false;
|
|
200
|
-
for (let i = 0; i < sql.length; i++) {
|
|
201
|
-
const ch = sql[i];
|
|
202
|
-
const prev = sql[i - 1];
|
|
203
|
-
if (ch === "'" && !inDouble && prev !== "\\") {
|
|
204
|
-
inSingle = !inSingle;
|
|
205
|
-
} else if (ch === '"' && !inSingle && prev !== "\\") {
|
|
206
|
-
inDouble = !inDouble;
|
|
207
|
-
}
|
|
208
|
-
if (ch === ";" && !inSingle && !inDouble) {
|
|
209
|
-
const trimmed2 = current.trim();
|
|
210
|
-
if (trimmed2) statements.push(trimmed2);
|
|
211
|
-
current = "";
|
|
212
|
-
} else {
|
|
213
|
-
current += ch;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
const trimmed = current.trim();
|
|
217
|
-
if (trimmed) statements.push(trimmed);
|
|
218
|
-
return statements;
|
|
219
|
-
}
|
|
220
|
-
async function doExecuteApproval({ id, userId, ip, encryptionKey }, log) {
|
|
221
|
-
const approval = await getApprovalById(id);
|
|
222
|
-
if (!approval || !["approved", "execute_failed"].includes(approval["status"])) {
|
|
223
|
-
throw new Error("Approval not found or not approved");
|
|
224
|
-
}
|
|
225
|
-
await setExecuting(id);
|
|
226
|
-
try {
|
|
227
|
-
const dataSourceId = approval["data_source_id"];
|
|
228
|
-
const dbName = approval["db_name"];
|
|
229
|
-
const pool = dbName ? await (0, import_business_dms_datasource.getPoolForDatabase)(dataSourceId, dbName, encryptionKey) : await (async () => {
|
|
230
|
-
const ds = await (0, import_business_dms_datasource.getDataSourceWithPassword)(dataSourceId, encryptionKey);
|
|
231
|
-
if (!ds) throw new Error("Data source not found");
|
|
232
|
-
return (0, import_business_dms_datasource.getPool)({ ...ds, database: ds.database ?? "" });
|
|
233
|
-
})();
|
|
234
|
-
if (!pool) throw new Error("Data source not found");
|
|
235
|
-
const sqlText = approval["sql_text"];
|
|
236
|
-
const statements = splitStatements(sqlText);
|
|
237
|
-
let result;
|
|
238
|
-
if (statements.length <= 1) {
|
|
239
|
-
const rows = await pool.execute(sqlText);
|
|
240
|
-
result = `${rows.length} rows affected`;
|
|
241
|
-
} else {
|
|
242
|
-
const results = await pool.executeInTransaction(statements);
|
|
243
|
-
const details = results.map((r) => `${r.rowCount} rows`).join(", ");
|
|
244
|
-
result = `${statements.length} statements executed (${details})`;
|
|
245
|
-
}
|
|
246
|
-
await setExecuteResult(id, "executed", result);
|
|
247
|
-
log.info("Approval executed (id=%s, user=%s, stmts=%d)", id, userId, statements.length);
|
|
248
|
-
await (0, import_business_dms_audit.writeAuditLog)({
|
|
249
|
-
userId,
|
|
250
|
-
dataSourceId,
|
|
251
|
-
action: "DML_EXECUTE",
|
|
252
|
-
sqlText,
|
|
253
|
-
resultSummary: result,
|
|
254
|
-
ipAddress: ip
|
|
255
|
-
});
|
|
256
|
-
return getApprovalById(id);
|
|
257
|
-
} catch (err) {
|
|
258
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
259
|
-
await setExecuteResult(id, "execute_failed", message);
|
|
260
|
-
log.error(err, "Approval execute_failed (id=%s)", id);
|
|
261
|
-
throw err;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
265
|
-
0 && (module.exports = {
|
|
266
|
-
approvalApprove,
|
|
267
|
-
approvalCreate,
|
|
268
|
-
approvalExecute,
|
|
269
|
-
approvalGet,
|
|
270
|
-
approvalList,
|
|
271
|
-
approvalReject,
|
|
272
|
-
getApprovalById,
|
|
273
|
-
setExecuteResult,
|
|
274
|
-
setExecuting
|
|
275
|
-
});
|
|
22
|
+
WHERE 1=1`,{where:r,params:s}=b(t);return e+=r+" ORDER BY a.created_at DESC LIMIT ? OFFSET ?",s.push(t?.limit??50,t?.offset??0),(0,d.getPgDb)().query(e,s)}function c(t){return(0,d.getPgDb)().queryOne(w,[t])}function S({dataSourceId:t,dbName:e,sqlText:r,submittedBy:s,sqlType:a="DML"}){return(0,d.getPgDb)().queryOne(A,[t,e,r,s,a])}function D(t,e,r,s){return(0,d.getPgDb)().queryOne(R(),[e,r,s,t])}function v(t){return(0,d.getPgDb)().run(x(),[t])}function f(t,e,r){return(0,d.getPgDb)().run(_(),[e,r,t])}async function N(t,e){let{status:r,mine:s,limit:a,offset:n}=t.body??{},o=t.user.role==="developer"||t.user.role==="maintainer"||String(s)==="true",i={status:r,submittedBy:o?t.user.id:void 0,limit:a?Number(a):void 0,offset:n?Number(n):void 0},[u,p]=await Promise.all([h(i),T(i)]);return e.send({rows:u,total:p})}async function F(t,e){let{id:r}=t.body??{},s=await c(r);return s?e.send(s):e.code(404).send({error:"Not found"})}async function O(t,e){let{dataSourceId:r,database:s,sql:a,sqlType:n}=t.body??{};if(!r||!a)return e.code(400).send({error:"Data source ID and SQL are required"});let o=n==="EXPORT"?"EXPORT":"DML",i=await S({dataSourceId:r,dbName:s??null,sqlText:a,submittedBy:t.user.id,sqlType:o});return t.log.info("Approval submitted (user=%s, type=%s)",t.user.id,o),e.code(201).send(i)}async function P(t,e,r,s){let a=await c(t);if(!a||a.status!=="pending")throw new Error("Approval not found or not in pending status");if(a.submitted_by===e)throw new Error("Cannot approve your own submission");return D(t,r,e,s??null)}async function L(t,e){let{id:r}=t.body??{};try{let s=await P(r,t.user.id,"approved");return t.log.info("Approval approved (id=%s, reviewer=%s)",r,t.user.id),e.send(s)}catch(s){return t.log.warn(s,"Approve failed (id=%s, reviewer=%s)",r,t.user.id),e.code(400).send({error:s instanceof Error?s.message:String(s)})}}async function U(t,e){let{id:r,reason:s}=t.body??{};try{let a=await P(r,t.user.id,"rejected",s);return t.log.info("Approval rejected (id=%s, reviewer=%s)",r,t.user.id),e.send(a)}catch(a){return t.log.warn(a,"Reject failed (id=%s, reviewer=%s)",r,t.user.id),e.code(400).send({error:a instanceof Error?a.message:String(a)})}}function B(t){return async(e,r)=>{let{id:s}=e.body??{},a=e.headers["x-forwarded-for"]??e.headers["x-real-ip"]??"unknown";try{let n=await Y({id:s,userId:e.user.id,ip:a,encryptionKey:t},e.log);return r.send(n)}catch(n){return e.log.error(n,"Execute approval failed (id=%s, user=%s)",s,e.user.id),r.code(400).send({error:n instanceof Error?n.message:String(n)})}}}function V(t){let e=[],r="",s=!1,a=!1;for(let o=0;o<t.length;o++){let i=t[o],u=t[o-1];if(i==="'"&&!a&&u!=="\\"?s=!s:i==='"'&&!s&&u!=="\\"&&(a=!a),i===";"&&!s&&!a){let p=r.trim();p&&e.push(p),r=""}else r+=i}let n=r.trim();return n&&e.push(n),e}async function Y({id:t,userId:e,ip:r,encryptionKey:s},a){let n=await c(t);if(!n||!["approved","execute_failed"].includes(n.status))throw new Error("Approval not found or not approved");await v(t);try{let o=n.data_source_id,i=n.db_name,u=i?await(0,l.getPoolForDatabase)(o,i,s):await(async()=>{let g=await(0,l.getDataSourceWithPassword)(o,s);if(!g)throw new Error("Data source not found");return(0,l.getPool)({...g,database:g.database??""})})();if(!u)throw new Error("Data source not found");let p=n.sql_text,E=V(p),m;if(E.length<=1)m=`${(await u.execute(p)).length} rows affected`;else{let C=(await u.executeInTransaction(E)).map(W=>`${W.rowCount} rows`).join(", ");m=`${E.length} statements executed (${C})`}return await f(t,"executed",m),a.info("Approval executed (id=%s, user=%s, stmts=%d)",t,e,E.length),await(0,I.writeAuditLog)({userId:e,dataSourceId:o,action:"DML_EXECUTE",sqlText:p,resultSummary:m,ipAddress:r}),c(t)}catch(o){let i=o instanceof Error?o.message:String(o);throw await f(t,"execute_failed",i),a.error(o,"Approval execute_failed (id=%s)",t),o}}0&&(module.exports={approvalApprove,approvalCreate,approvalExecute,approvalGet,approvalList,approvalReject,getApprovalById,setExecuteResult,setExecuting});
|
package/dist/index.mjs
CHANGED
|
@@ -1,240 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
import { getDataSourceWithPassword, getPool, getPoolForDatabase } 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 = `
|
|
1
|
+
import{getDataSourceWithPassword as F,getPool as O,getPoolForDatabase as P}from"@heyhru/business-dms-datasource";import{writeAuditLog as L}from"@heyhru/business-dms-audit";import{getPgDb as d}from"@heyhru/server-plugin-pg";var v=`
|
|
10
2
|
SELECT *
|
|
11
3
|
FROM approvals
|
|
12
|
-
WHERE id =
|
|
13
|
-
var CREATE = `
|
|
4
|
+
WHERE id = ?`,y=`
|
|
14
5
|
INSERT INTO approvals (data_source_id, db_name, sql_text, submitted_by, sql_type)
|
|
15
6
|
VALUES (?, ?, ?, ?, ?)
|
|
16
|
-
RETURNING
|
|
17
|
-
var UPDATE_REVIEW = () => `
|
|
7
|
+
RETURNING *`,w=()=>`
|
|
18
8
|
UPDATE approvals
|
|
19
9
|
SET status = ?, reviewed_by = ?, reject_reason = ?, updated_at = NOW()
|
|
20
10
|
WHERE id = ?
|
|
21
|
-
RETURNING
|
|
22
|
-
var UPDATE_EXECUTING = () => `
|
|
11
|
+
RETURNING *`,A=()=>`
|
|
23
12
|
UPDATE approvals
|
|
24
13
|
SET status = 'executing', updated_at = NOW()
|
|
25
|
-
WHERE id =
|
|
26
|
-
var UPDATE_RESULT = () => `
|
|
14
|
+
WHERE id = ?`,R=()=>`
|
|
27
15
|
UPDATE approvals
|
|
28
16
|
SET status = ?, execute_result = ?, updated_at = NOW()
|
|
29
|
-
WHERE id = ?`;
|
|
30
|
-
|
|
31
|
-
// src/approvals.model.ts
|
|
32
|
-
function buildWhereParams(filters) {
|
|
33
|
-
const clauses = [];
|
|
34
|
-
const params = [];
|
|
35
|
-
if (filters?.status) {
|
|
36
|
-
clauses.push("a.status = ?");
|
|
37
|
-
params.push(filters.status);
|
|
38
|
-
}
|
|
39
|
-
if (filters?.submittedBy) {
|
|
40
|
-
clauses.push("a.submitted_by = ?");
|
|
41
|
-
params.push(filters.submittedBy);
|
|
42
|
-
}
|
|
43
|
-
return { where: clauses.length ? " AND " + clauses.join(" AND ") : "", params };
|
|
44
|
-
}
|
|
45
|
-
async function countApprovals(filters) {
|
|
46
|
-
const { where, params } = buildWhereParams(filters);
|
|
47
|
-
const row = await getPgDb().queryOne(
|
|
48
|
-
`SELECT COUNT(*) AS total FROM approvals a WHERE 1=1` + where,
|
|
49
|
-
params
|
|
50
|
-
);
|
|
51
|
-
return Number(row?.["total"] ?? 0);
|
|
52
|
-
}
|
|
53
|
-
async function listApprovals(filters) {
|
|
54
|
-
let query = `SELECT a.*, u.username AS submitter_name,
|
|
17
|
+
WHERE id = ?`;function x(t){let e=[],r=[];return t?.status&&(e.push("a.status = ?"),r.push(t.status)),t?.submittedBy&&(e.push("a.submitted_by = ?"),r.push(t.submittedBy)),{where:e.length?" AND "+e.join(" AND "):"",params:r}}async function _(t){let{where:e,params:r}=x(t),s=await d().queryOne("SELECT COUNT(*) AS total FROM approvals a WHERE 1=1"+e,r);return Number(s?.total??0)}async function b(t){let e=`SELECT a.*, u.username AS submitter_name,
|
|
55
18
|
ds.name AS data_source_name, ds.type AS data_source_type
|
|
56
19
|
FROM approvals a
|
|
57
20
|
LEFT JOIN users u ON u.id = a.submitted_by
|
|
58
21
|
LEFT JOIN data_sources ds ON ds.id = a.data_source_id
|
|
59
|
-
WHERE 1=1`;
|
|
60
|
-
const { where, params } = buildWhereParams(filters);
|
|
61
|
-
query += where + " ORDER BY a.created_at DESC LIMIT ? OFFSET ?";
|
|
62
|
-
params.push(filters?.limit ?? 50, filters?.offset ?? 0);
|
|
63
|
-
return getPgDb().query(query, params);
|
|
64
|
-
}
|
|
65
|
-
function getApprovalById(id) {
|
|
66
|
-
return getPgDb().queryOne(FIND_BY_ID, [id]);
|
|
67
|
-
}
|
|
68
|
-
function insertApproval({ dataSourceId, dbName, sqlText, submittedBy, sqlType = "DML" }) {
|
|
69
|
-
return getPgDb().queryOne(CREATE, [dataSourceId, dbName, sqlText, submittedBy, sqlType]);
|
|
70
|
-
}
|
|
71
|
-
function updateReview(id, status, reviewedBy, rejectReason) {
|
|
72
|
-
return getPgDb().queryOne(UPDATE_REVIEW(), [status, reviewedBy, rejectReason, id]);
|
|
73
|
-
}
|
|
74
|
-
function setExecuting(id) {
|
|
75
|
-
return getPgDb().run(UPDATE_EXECUTING(), [id]);
|
|
76
|
-
}
|
|
77
|
-
function setExecuteResult(id, status, result) {
|
|
78
|
-
return getPgDb().run(UPDATE_RESULT(), [status, result, id]);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// src/approvals.service.ts
|
|
82
|
-
async function approvalList(req, reply) {
|
|
83
|
-
const { status, mine, limit, offset } = req.body ?? {};
|
|
84
|
-
const ownOnly = req.user.role === "developer" || req.user.role === "maintainer" || String(mine) === "true";
|
|
85
|
-
const filters = {
|
|
86
|
-
status,
|
|
87
|
-
submittedBy: ownOnly ? req.user.id : void 0,
|
|
88
|
-
limit: limit ? Number(limit) : void 0,
|
|
89
|
-
offset: offset ? Number(offset) : void 0
|
|
90
|
-
};
|
|
91
|
-
const [rows, total] = await Promise.all([listApprovals(filters), countApprovals(filters)]);
|
|
92
|
-
return reply.send({ rows, total });
|
|
93
|
-
}
|
|
94
|
-
async function approvalGet(req, reply) {
|
|
95
|
-
const { id } = req.body ?? {};
|
|
96
|
-
const approval = await getApprovalById(id);
|
|
97
|
-
if (!approval) return reply.code(404).send({ error: "Not found" });
|
|
98
|
-
return reply.send(approval);
|
|
99
|
-
}
|
|
100
|
-
async function approvalCreate(req, reply) {
|
|
101
|
-
const { dataSourceId, database, sql, sqlType } = req.body ?? {};
|
|
102
|
-
if (!dataSourceId || !sql) {
|
|
103
|
-
return reply.code(400).send({ error: "Data source ID and SQL are required" });
|
|
104
|
-
}
|
|
105
|
-
const type = sqlType === "EXPORT" ? "EXPORT" : "DML";
|
|
106
|
-
const approval = await insertApproval({
|
|
107
|
-
dataSourceId,
|
|
108
|
-
dbName: database ?? null,
|
|
109
|
-
sqlText: sql,
|
|
110
|
-
submittedBy: req.user.id,
|
|
111
|
-
sqlType: type
|
|
112
|
-
});
|
|
113
|
-
req.log.info("Approval submitted (user=%s, type=%s)", req.user.id, type);
|
|
114
|
-
return reply.code(201).send(approval);
|
|
115
|
-
}
|
|
116
|
-
async function reviewApproval(id, reviewerId, decision, rejectReason) {
|
|
117
|
-
const approval = await getApprovalById(id);
|
|
118
|
-
if (!approval || approval["status"] !== "pending") {
|
|
119
|
-
throw new Error("Approval not found or not in pending status");
|
|
120
|
-
}
|
|
121
|
-
if (approval["submitted_by"] === reviewerId) {
|
|
122
|
-
throw new Error("Cannot approve your own submission");
|
|
123
|
-
}
|
|
124
|
-
return updateReview(id, decision, reviewerId, rejectReason ?? null);
|
|
125
|
-
}
|
|
126
|
-
async function approvalApprove(req, reply) {
|
|
127
|
-
const { id } = req.body ?? {};
|
|
128
|
-
try {
|
|
129
|
-
const result = await reviewApproval(id, req.user.id, "approved");
|
|
130
|
-
req.log.info("Approval approved (id=%s, reviewer=%s)", id, req.user.id);
|
|
131
|
-
return reply.send(result);
|
|
132
|
-
} catch (err) {
|
|
133
|
-
req.log.warn(err, "Approve failed (id=%s, reviewer=%s)", id, req.user.id);
|
|
134
|
-
return reply.code(400).send({ error: err instanceof Error ? err.message : String(err) });
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
async function approvalReject(req, reply) {
|
|
138
|
-
const { id, reason } = req.body ?? {};
|
|
139
|
-
try {
|
|
140
|
-
const result = await reviewApproval(id, req.user.id, "rejected", reason);
|
|
141
|
-
req.log.info("Approval rejected (id=%s, reviewer=%s)", id, req.user.id);
|
|
142
|
-
return reply.send(result);
|
|
143
|
-
} catch (err) {
|
|
144
|
-
req.log.warn(err, "Reject failed (id=%s, reviewer=%s)", id, req.user.id);
|
|
145
|
-
return reply.code(400).send({ error: err instanceof Error ? err.message : String(err) });
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
function approvalExecute(encryptionKey) {
|
|
149
|
-
return async (req, reply) => {
|
|
150
|
-
const { id } = req.body ?? {};
|
|
151
|
-
const ip = req.headers["x-forwarded-for"] ?? req.headers["x-real-ip"] ?? "unknown";
|
|
152
|
-
try {
|
|
153
|
-
const result = await doExecuteApproval({ id, userId: req.user.id, ip, encryptionKey }, req.log);
|
|
154
|
-
return reply.send(result);
|
|
155
|
-
} catch (err) {
|
|
156
|
-
req.log.error(err, "Execute approval failed (id=%s, user=%s)", id, req.user.id);
|
|
157
|
-
return reply.code(400).send({ error: err instanceof Error ? err.message : String(err) });
|
|
158
|
-
}
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
function splitStatements(sql) {
|
|
162
|
-
const statements = [];
|
|
163
|
-
let current = "";
|
|
164
|
-
let inSingle = false;
|
|
165
|
-
let inDouble = false;
|
|
166
|
-
for (let i = 0; i < sql.length; i++) {
|
|
167
|
-
const ch = sql[i];
|
|
168
|
-
const prev = sql[i - 1];
|
|
169
|
-
if (ch === "'" && !inDouble && prev !== "\\") {
|
|
170
|
-
inSingle = !inSingle;
|
|
171
|
-
} else if (ch === '"' && !inSingle && prev !== "\\") {
|
|
172
|
-
inDouble = !inDouble;
|
|
173
|
-
}
|
|
174
|
-
if (ch === ";" && !inSingle && !inDouble) {
|
|
175
|
-
const trimmed2 = current.trim();
|
|
176
|
-
if (trimmed2) statements.push(trimmed2);
|
|
177
|
-
current = "";
|
|
178
|
-
} else {
|
|
179
|
-
current += ch;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
const trimmed = current.trim();
|
|
183
|
-
if (trimmed) statements.push(trimmed);
|
|
184
|
-
return statements;
|
|
185
|
-
}
|
|
186
|
-
async function doExecuteApproval({ id, userId, ip, encryptionKey }, log) {
|
|
187
|
-
const approval = await getApprovalById(id);
|
|
188
|
-
if (!approval || !["approved", "execute_failed"].includes(approval["status"])) {
|
|
189
|
-
throw new Error("Approval not found or not approved");
|
|
190
|
-
}
|
|
191
|
-
await setExecuting(id);
|
|
192
|
-
try {
|
|
193
|
-
const dataSourceId = approval["data_source_id"];
|
|
194
|
-
const dbName = approval["db_name"];
|
|
195
|
-
const pool = dbName ? await getPoolForDatabase(dataSourceId, dbName, encryptionKey) : await (async () => {
|
|
196
|
-
const ds = await getDataSourceWithPassword(dataSourceId, encryptionKey);
|
|
197
|
-
if (!ds) throw new Error("Data source not found");
|
|
198
|
-
return getPool({ ...ds, database: ds.database ?? "" });
|
|
199
|
-
})();
|
|
200
|
-
if (!pool) throw new Error("Data source not found");
|
|
201
|
-
const sqlText = approval["sql_text"];
|
|
202
|
-
const statements = splitStatements(sqlText);
|
|
203
|
-
let result;
|
|
204
|
-
if (statements.length <= 1) {
|
|
205
|
-
const rows = await pool.execute(sqlText);
|
|
206
|
-
result = `${rows.length} rows affected`;
|
|
207
|
-
} else {
|
|
208
|
-
const results = await pool.executeInTransaction(statements);
|
|
209
|
-
const details = results.map((r) => `${r.rowCount} rows`).join(", ");
|
|
210
|
-
result = `${statements.length} statements executed (${details})`;
|
|
211
|
-
}
|
|
212
|
-
await setExecuteResult(id, "executed", result);
|
|
213
|
-
log.info("Approval executed (id=%s, user=%s, stmts=%d)", id, userId, statements.length);
|
|
214
|
-
await writeAuditLog({
|
|
215
|
-
userId,
|
|
216
|
-
dataSourceId,
|
|
217
|
-
action: "DML_EXECUTE",
|
|
218
|
-
sqlText,
|
|
219
|
-
resultSummary: result,
|
|
220
|
-
ipAddress: ip
|
|
221
|
-
});
|
|
222
|
-
return getApprovalById(id);
|
|
223
|
-
} catch (err) {
|
|
224
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
225
|
-
await setExecuteResult(id, "execute_failed", message);
|
|
226
|
-
log.error(err, "Approval execute_failed (id=%s)", id);
|
|
227
|
-
throw err;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
export {
|
|
231
|
-
approvalApprove,
|
|
232
|
-
approvalCreate,
|
|
233
|
-
approvalExecute,
|
|
234
|
-
approvalGet,
|
|
235
|
-
approvalList,
|
|
236
|
-
approvalReject,
|
|
237
|
-
getApprovalById,
|
|
238
|
-
setExecuteResult,
|
|
239
|
-
setExecuting
|
|
240
|
-
};
|
|
22
|
+
WHERE 1=1`,{where:r,params:s}=x(t);return e+=r+" ORDER BY a.created_at DESC LIMIT ? OFFSET ?",s.push(t?.limit??50,t?.offset??0),d().query(e,s)}function c(t){return d().queryOne(v,[t])}function T({dataSourceId:t,dbName:e,sqlText:r,submittedBy:s,sqlType:a="DML"}){return d().queryOne(y,[t,e,r,s,a])}function h(t,e,r,s){return d().queryOne(w(),[e,r,s,t])}function m(t){return d().run(A(),[t])}function E(t,e,r){return d().run(R(),[e,r,t])}async function U(t,e){let{status:r,mine:s,limit:a,offset:n}=t.body??{},o=t.user.role==="developer"||t.user.role==="maintainer"||String(s)==="true",i={status:r,submittedBy:o?t.user.id:void 0,limit:a?Number(a):void 0,offset:n?Number(n):void 0},[u,p]=await Promise.all([b(i),_(i)]);return e.send({rows:u,total:p})}async function B(t,e){let{id:r}=t.body??{},s=await c(r);return s?e.send(s):e.code(404).send({error:"Not found"})}async function C(t,e){let{dataSourceId:r,database:s,sql:a,sqlType:n}=t.body??{};if(!r||!a)return e.code(400).send({error:"Data source ID and SQL are required"});let o=n==="EXPORT"?"EXPORT":"DML",i=await T({dataSourceId:r,dbName:s??null,sqlText:a,submittedBy:t.user.id,sqlType:o});return t.log.info("Approval submitted (user=%s, type=%s)",t.user.id,o),e.code(201).send(i)}async function S(t,e,r,s){let a=await c(t);if(!a||a.status!=="pending")throw new Error("Approval not found or not in pending status");if(a.submitted_by===e)throw new Error("Cannot approve your own submission");return h(t,r,e,s??null)}async function W(t,e){let{id:r}=t.body??{};try{let s=await S(r,t.user.id,"approved");return t.log.info("Approval approved (id=%s, reviewer=%s)",r,t.user.id),e.send(s)}catch(s){return t.log.warn(s,"Approve failed (id=%s, reviewer=%s)",r,t.user.id),e.code(400).send({error:s instanceof Error?s.message:String(s)})}}async function j(t,e){let{id:r,reason:s}=t.body??{};try{let a=await S(r,t.user.id,"rejected",s);return t.log.info("Approval rejected (id=%s, reviewer=%s)",r,t.user.id),e.send(a)}catch(a){return t.log.warn(a,"Reject failed (id=%s, reviewer=%s)",r,t.user.id),e.code(400).send({error:a instanceof Error?a.message:String(a)})}}function M(t){return async(e,r)=>{let{id:s}=e.body??{},a=e.headers["x-forwarded-for"]??e.headers["x-real-ip"]??"unknown";try{let n=await G({id:s,userId:e.user.id,ip:a,encryptionKey:t},e.log);return r.send(n)}catch(n){return e.log.error(n,"Execute approval failed (id=%s, user=%s)",s,e.user.id),r.code(400).send({error:n instanceof Error?n.message:String(n)})}}}function Q(t){let e=[],r="",s=!1,a=!1;for(let o=0;o<t.length;o++){let i=t[o],u=t[o-1];if(i==="'"&&!a&&u!=="\\"?s=!s:i==='"'&&!s&&u!=="\\"&&(a=!a),i===";"&&!s&&!a){let p=r.trim();p&&e.push(p),r=""}else r+=i}let n=r.trim();return n&&e.push(n),e}async function G({id:t,userId:e,ip:r,encryptionKey:s},a){let n=await c(t);if(!n||!["approved","execute_failed"].includes(n.status))throw new Error("Approval not found or not approved");await m(t);try{let o=n.data_source_id,i=n.db_name,u=i?await P(o,i,s):await(async()=>{let l=await F(o,s);if(!l)throw new Error("Data source not found");return O({...l,database:l.database??""})})();if(!u)throw new Error("Data source not found");let p=n.sql_text,g=Q(p),f;if(g.length<=1)f=`${(await u.execute(p)).length} rows affected`;else{let D=(await u.executeInTransaction(g)).map(I=>`${I.rowCount} rows`).join(", ");f=`${g.length} statements executed (${D})`}return await E(t,"executed",f),a.info("Approval executed (id=%s, user=%s, stmts=%d)",t,e,g.length),await L({userId:e,dataSourceId:o,action:"DML_EXECUTE",sqlText:p,resultSummary:f,ipAddress:r}),c(t)}catch(o){let i=o instanceof Error?o.message:String(o);throw await E(t,"execute_failed",i),a.error(o,"Approval execute_failed (id=%s)",t),o}}export{W as approvalApprove,C as approvalCreate,M as approvalExecute,B as approvalGet,U as approvalList,j as approvalReject,c as getApprovalById,E as setExecuteResult,m as setExecuting};
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.11.
|
|
6
|
+
"version": "0.11.2",
|
|
7
7
|
"description": "DMS approval workflow domain logic: service, model, sql",
|
|
8
8
|
"main": "./dist/index.js",
|
|
9
9
|
"module": "./dist/index.mjs",
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"clean": "rm -rf dist"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@heyhru/business-dms-audit": "0.6.
|
|
30
|
-
"@heyhru/business-dms-datasource": "0.8.
|
|
31
|
-
"@heyhru/server-plugin-pg": "0.8.
|
|
29
|
+
"@heyhru/business-dms-audit": "0.6.5",
|
|
30
|
+
"@heyhru/business-dms-datasource": "0.8.5",
|
|
31
|
+
"@heyhru/server-plugin-pg": "0.8.2",
|
|
32
32
|
"fastify": "^5.8.4"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
@@ -36,5 +36,5 @@
|
|
|
36
36
|
"typescript": "^6.0.2",
|
|
37
37
|
"vitest": "^4.1.4"
|
|
38
38
|
},
|
|
39
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "77b53e79244df94faa9a2605f08cb6b41ba22ace"
|
|
40
40
|
}
|