w-web-perm 1.1.4 → 1.1.6
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 +1 -1
- package/dist/css/app.6918ec45.css +1 -0
- package/dist/css/{chunk-vendors.2bcfce08.css → chunk-vendors.3c580e72.css} +1 -1
- package/dist/index.tmp +2 -2
- package/dist/js/app-legacy.5f15a58f.js +1 -0
- package/dist/js/app.5f15a58f.js +1 -0
- package/dist/js/{chunk-vendors-legacy.fc37f076.js → chunk-vendors-legacy.38f515f9.js} +16 -16
- package/dist/js/{chunk-vendors.fc37f076.js → chunk-vendors.38f515f9.js} +16 -16
- package/dist/w-web-perm.umd.js +2 -2
- package/dist/w-web-perm.umd.js.map +1 -1
- package/docs/WWebPerm.html +7 -7
- package/docs/global.html +2068 -111
- package/docs/index.html +2 -2
- package/docs/server_WWebPerm.mjs.html +63 -31
- package/docs/server_lockSave.mjs.html +115 -0
- package/docs/server_maskLog.mjs.html +176 -0
- package/docs/server_procCore.mjs.html +31 -5
- package/docs/server_wrapInjected.mjs.html +163 -0
- package/docs/src_fetchJson.mjs.html +31 -5
- package/docs/src_js_legendFit.mjs.html +225 -0
- package/docs/src_plugins_reuseDeletedIds.mjs.html +136 -0
- package/docs/src_plugins_submitGuard.mjs.html +163 -0
- package/package.json +13 -13
- package/public/index.html +2 -0
- package/server/WWebPerm.mjs +61 -29
- package/server/lockSave.mjs +43 -0
- package/server/maskLog.mjs +104 -0
- package/server/procCore.mjs +29 -3
- package/server/procLang.mjs +8 -0
- package/server/procStaInfor.mjs +32 -14
- package/server/wrapInjected.mjs +91 -0
- package/src/components/Common/CheckYes.vue +6 -0
- package/src/components/Common/CheckYesNo.vue +6 -0
- package/src/components/LayoutContentGrups.vue +30 -21
- package/src/components/LayoutContentPemis.vue +30 -21
- package/src/components/LayoutContentStaInfor.vue +74 -5
- package/src/components/LayoutContentTargets.vue +36 -22
- package/src/components/LayoutContentUsers.vue +31 -21
- package/src/components/RelationChips.vue +11 -31
- package/src/components/VeCgrups.vue +8 -1
- package/src/components/VeCpemis.vue +8 -1
- package/src/components/VeCrules.vue +8 -1
- package/src/components/VeGrupBlngUsers.vue +37 -31
- package/src/components/VePemiBlngGrups.vue +37 -31
- package/src/fetchJson.mjs +28 -2
- package/src/getPerm.mjs +9 -6
- package/src/getPermUserInfor.mjs +12 -9
- package/src/js/legendFit.mjs +153 -0
- package/src/plugins/mUI.mjs +20 -0
- package/src/plugins/reuseDeletedIds.mjs +64 -0
- package/src/plugins/submitGuard.mjs +91 -0
- package/srv.mjs +20 -1
- package/dist/css/app.506ce4d8.css +0 -1
- package/dist/js/app-legacy.8e615442.js +0 -1
- package/dist/js/app.8e615442.js +0 -1
- package/tableTags-web-perm.json +0 -1
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>src/plugins/reuseDeletedIds.mjs - Documentation</title>
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
<script src="scripts/prettify/prettify.js"></script>
|
|
10
|
+
<script src="scripts/prettify/lang-css.js"></script>
|
|
11
|
+
<!--[if lt IE 9]>
|
|
12
|
+
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
|
|
13
|
+
<![endif]-->
|
|
14
|
+
<link type="text/css" rel="stylesheet" href="styles/prettify.css">
|
|
15
|
+
<link type="text/css" rel="stylesheet" href="styles/jsdoc.css">
|
|
16
|
+
<script src="scripts/nav.js" defer></script>
|
|
17
|
+
|
|
18
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
19
|
+
</head>
|
|
20
|
+
<body>
|
|
21
|
+
|
|
22
|
+
<input type="checkbox" id="nav-trigger" class="nav-trigger" />
|
|
23
|
+
<label for="nav-trigger" class="navicon-button x">
|
|
24
|
+
<div class="navicon"></div>
|
|
25
|
+
</label>
|
|
26
|
+
|
|
27
|
+
<label for="nav-trigger" class="overlay"></label>
|
|
28
|
+
|
|
29
|
+
<nav >
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WWebPerm.html">WWebPerm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#createLegendMeasurer">createLegendMeasurer</a></li><li><a href="global.html#createLockSave">createLockSave</a></li><li><a href="global.html#decideLegendType">decideLegendType</a></li><li><a href="global.html#describeUpstream">describeUpstream</a></li><li><a href="global.html#fetchJson">fetchJson</a></li><li><a href="global.html#genLegend">genLegend</a></li><li><a href="global.html#maskQuery">maskQuery</a></li><li><a href="global.html#maskTok">maskTok</a></li><li><a href="global.html#maskUrl">maskUrl</a></li><li><a href="global.html#mergePickKeysOnly">mergePickKeysOnly</a></li><li><a href="global.html#reuseDeletedIds">reuseDeletedIds</a></li><li><a href="global.html#submitGuard">submitGuard</a></li><li><a href="global.html#wrapInjected">wrapInjected</a></li></ul>
|
|
33
|
+
|
|
34
|
+
</nav>
|
|
35
|
+
|
|
36
|
+
<div id="main">
|
|
37
|
+
|
|
38
|
+
<h1 class="page-title">src/plugins/reuseDeletedIds.mjs</h1>
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
<section>
|
|
47
|
+
<article>
|
|
48
|
+
<pre class="prettyprint source linenums"><code>import get from 'lodash-es/get.js'
|
|
49
|
+
import map from 'lodash-es/map.js'
|
|
50
|
+
import each from 'lodash-es/each.js'
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 新增列沿用資料庫既有 id 時之正規化 (標的清單專用, 其 id 為可就地改寫之標的路徑; ADR-025)
|
|
55
|
+
*
|
|
56
|
+
* 同一次儲存中刪除(或把 id 改掉)某既有列, 又新增一列並把 id 改成該既有列之 id, 淨效果為「修改該列」:
|
|
57
|
+
* 該新增列改為一般列送出(_isNew 設 false), 建立者 / 建立時間沿用資料庫之值(修改者 / 修改時間由後端寫入).
|
|
58
|
+
* 否則後端之「新增列已存在即拒絕」(saveNewRowExists, 擋同一包重送) 會把它當重送擋下, 且重新整理後重做仍被擋;
|
|
59
|
+
* 亦避免以前端佔位字({待自動給予})覆寫建立者 / 建立時間.
|
|
60
|
+
* 只處理 id 於 rows 內唯一者; 重複 id 不處理, 由後端 ckKey 回 saveRowFieldDuplicate.
|
|
61
|
+
* 使用者 / 群組 / 權限清單之 id 為系統產生且不可編輯, 新增列不會與既有列同 id, 不需此處理.
|
|
62
|
+
*
|
|
63
|
+
* @param {Array} rows 輸入欲送出之列陣列(清單之 opt.rows), 不修改
|
|
64
|
+
* @param {Array} rowsDb 輸入資料庫之列陣列(前端 store 之同表資料)
|
|
65
|
+
* @returns {Array} 回傳正規化後之新陣列
|
|
66
|
+
*/
|
|
67
|
+
function reuseDeletedIds(rows, rowsDb) {
|
|
68
|
+
|
|
69
|
+
//kpDb, kpCount: 以 Map 存放, 不以 lodash get 取值(標的 id 為路徑, 可能含「.」而被當成巢狀路徑)
|
|
70
|
+
let kpDb = new Map()
|
|
71
|
+
each(rowsDb, (r) => {
|
|
72
|
+
kpDb.set(get(r, 'id', ''), r)
|
|
73
|
+
})
|
|
74
|
+
let kpCount = new Map()
|
|
75
|
+
each(rows, (r) => {
|
|
76
|
+
let id = get(r, 'id', '')
|
|
77
|
+
kpCount.set(id, (kpCount.get(id) || 0) + 1)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
let rs = map(rows, (r) => {
|
|
81
|
+
let id = get(r, 'id', '')
|
|
82
|
+
|
|
83
|
+
//非新增列
|
|
84
|
+
if (get(r, '_isNew') !== true) {
|
|
85
|
+
return r
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
//資料庫無此 id: 真正之新增
|
|
89
|
+
if (!kpDb.has(id)) {
|
|
90
|
+
return r
|
|
91
|
+
}
|
|
92
|
+
let rDb = kpDb.get(id)
|
|
93
|
+
|
|
94
|
+
//rows 內重複 id: 不處理
|
|
95
|
+
if (kpCount.get(id) !== 1) {
|
|
96
|
+
return r
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
...r,
|
|
101
|
+
_isNew: false,
|
|
102
|
+
userId: get(rDb, 'userId', ''),
|
|
103
|
+
timeCreate: get(rDb, 'timeCreate', ''),
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
return rs
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
export default reuseDeletedIds
|
|
112
|
+
</code></pre>
|
|
113
|
+
</article>
|
|
114
|
+
</section>
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<br class="clear">
|
|
124
|
+
|
|
125
|
+
<footer>
|
|
126
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 02 2026 20:53:56 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
127
|
+
</footer>
|
|
128
|
+
|
|
129
|
+
<script>prettyPrint();</script>
|
|
130
|
+
<script src="scripts/polyfill.js"></script>
|
|
131
|
+
<script src="scripts/linenumber.js"></script>
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
</body>
|
|
136
|
+
</html>
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>src/plugins/submitGuard.mjs - Documentation</title>
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
<script src="scripts/prettify/prettify.js"></script>
|
|
10
|
+
<script src="scripts/prettify/lang-css.js"></script>
|
|
11
|
+
<!--[if lt IE 9]>
|
|
12
|
+
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
|
|
13
|
+
<![endif]-->
|
|
14
|
+
<link type="text/css" rel="stylesheet" href="styles/prettify.css">
|
|
15
|
+
<link type="text/css" rel="stylesheet" href="styles/jsdoc.css">
|
|
16
|
+
<script src="scripts/nav.js" defer></script>
|
|
17
|
+
|
|
18
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
19
|
+
</head>
|
|
20
|
+
<body>
|
|
21
|
+
|
|
22
|
+
<input type="checkbox" id="nav-trigger" class="nav-trigger" />
|
|
23
|
+
<label for="nav-trigger" class="navicon-button x">
|
|
24
|
+
<div class="navicon"></div>
|
|
25
|
+
</label>
|
|
26
|
+
|
|
27
|
+
<label for="nav-trigger" class="overlay"></label>
|
|
28
|
+
|
|
29
|
+
<nav >
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WWebPerm.html">WWebPerm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#createLegendMeasurer">createLegendMeasurer</a></li><li><a href="global.html#createLockSave">createLockSave</a></li><li><a href="global.html#decideLegendType">decideLegendType</a></li><li><a href="global.html#describeUpstream">describeUpstream</a></li><li><a href="global.html#fetchJson">fetchJson</a></li><li><a href="global.html#genLegend">genLegend</a></li><li><a href="global.html#maskQuery">maskQuery</a></li><li><a href="global.html#maskTok">maskTok</a></li><li><a href="global.html#maskUrl">maskUrl</a></li><li><a href="global.html#mergePickKeysOnly">mergePickKeysOnly</a></li><li><a href="global.html#reuseDeletedIds">reuseDeletedIds</a></li><li><a href="global.html#submitGuard">submitGuard</a></li><li><a href="global.html#wrapInjected">wrapInjected</a></li></ul>
|
|
33
|
+
|
|
34
|
+
</nav>
|
|
35
|
+
|
|
36
|
+
<div id="main">
|
|
37
|
+
|
|
38
|
+
<h1 class="page-title">src/plugins/submitGuard.mjs</h1>
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
<section>
|
|
47
|
+
<article>
|
|
48
|
+
<pre class="prettyprint source linenums"><code>import get from 'lodash-es/get.js'
|
|
49
|
+
import each from 'lodash-es/each.js'
|
|
50
|
+
import isfun from 'wsemi/src/isfun.mjs'
|
|
51
|
+
import haskey from 'wsemi/src/haskey.mjs'
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 送出類操作之前端雙擊防護核心 (與 Vue / store 無關, 供單元測試; 由 mUI 之 runSubmit 與 updateLoading 使用, ADR-025)
|
|
56
|
+
*
|
|
57
|
+
* - run(key, fn, opt): 同 key 之送出流程進行中(自觸發起至流程結束, 含結果訊息框開啟期間)再觸發即略過.
|
|
58
|
+
* 按鈕之滑鼠與鍵盤 Enter、輸入框之 Enter 等入口共用同一狀態.
|
|
59
|
+
* opt.pm 為 WButtonChip / WButtonCircle 之 promiseUnlock 鎖(click 事件之 msg.pm), 於 releaseBtnLocks()(全頁 loading 關閉, 即請求結束)
|
|
60
|
+
* 或流程結束時釋放; 被略過之觸發亦立即釋放其 pm, 否則該按鈕永久鎖住.
|
|
61
|
+
* fn 收到 unlock 函數: 流程需先開確認框者於開框前呼叫, 按鈕鎖只涵蓋請求期間(確認框背後之按鈕不顯示載入圖示), 重入仍由流程狀態擋.
|
|
62
|
+
* opt.hold 為函數, 輸入流程結果, 回傳 true 時保持占位與按鈕鎖(如登入成功轉址, 頁面即將離開).
|
|
63
|
+
* - releaseBtnLocks(): 釋放進行中流程所登記之按鈕鎖, 流程狀態不變(結果訊息框開啟期間之重入仍擋).
|
|
64
|
+
* - isRunning(key): 查詢 key 是否進行中.
|
|
65
|
+
*
|
|
66
|
+
* 為何需要: 全頁 loading(WDialog)不搶焦點, 只擋滑鼠, 鍵盤事件仍送達背後之按鈕與輸入框;
|
|
67
|
+
* 按鈕鎖若於 handler 第一行即解除, 鍵盤連按照樣送出(2026-09-29 實測).
|
|
68
|
+
*
|
|
69
|
+
* @returns {Object} 回傳 { run, releaseBtnLocks, isRunning }
|
|
70
|
+
*/
|
|
71
|
+
function submitGuard() {
|
|
72
|
+
|
|
73
|
+
//kpRunning, 進行中流程: key → { unlock }
|
|
74
|
+
let kpRunning = {}
|
|
75
|
+
|
|
76
|
+
let run = (key, fn, opt = {}) => {
|
|
77
|
+
|
|
78
|
+
//unlock, 釋放本次觸發之按鈕鎖; pm.resolve 重複呼叫無作用
|
|
79
|
+
let pm = get(opt, 'pm', null)
|
|
80
|
+
let unlock = () => {
|
|
81
|
+
if (pm && isfun(pm.resolve)) {
|
|
82
|
+
pm.resolve()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
//同 key 進行中: 略過, 並立即釋放本次觸發之按鈕鎖
|
|
87
|
+
if (haskey(kpRunning, key)) {
|
|
88
|
+
unlock()
|
|
89
|
+
return Promise.resolve(null)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
//占位
|
|
93
|
+
kpRunning[key] = { unlock }
|
|
94
|
+
|
|
95
|
+
//hold
|
|
96
|
+
let hold = get(opt, 'hold', null)
|
|
97
|
+
|
|
98
|
+
//end
|
|
99
|
+
let end = () => {
|
|
100
|
+
unlock()
|
|
101
|
+
delete kpRunning[key]
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return Promise.resolve()
|
|
105
|
+
.then(() => {
|
|
106
|
+
return fn(unlock)
|
|
107
|
+
})
|
|
108
|
+
.then((r) => {
|
|
109
|
+
if (isfun(hold) && hold(r) === true) {
|
|
110
|
+
return r //保持占位與按鈕鎖
|
|
111
|
+
}
|
|
112
|
+
end()
|
|
113
|
+
return r
|
|
114
|
+
}, (err) => {
|
|
115
|
+
end()
|
|
116
|
+
return Promise.reject(err)
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let releaseBtnLocks = () => {
|
|
121
|
+
each(kpRunning, (v) => {
|
|
122
|
+
v.unlock()
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let isRunning = (key) => {
|
|
127
|
+
return haskey(kpRunning, key)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
run,
|
|
132
|
+
releaseBtnLocks,
|
|
133
|
+
isRunning,
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
export default submitGuard
|
|
139
|
+
</code></pre>
|
|
140
|
+
</article>
|
|
141
|
+
</section>
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<br class="clear">
|
|
151
|
+
|
|
152
|
+
<footer>
|
|
153
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 02 2026 20:53:56 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
154
|
+
</footer>
|
|
155
|
+
|
|
156
|
+
<script>prettyPrint();</script>
|
|
157
|
+
<script src="scripts/polyfill.js"></script>
|
|
158
|
+
<script src="scripts/linenumber.js"></script>
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
</body>
|
|
163
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "w-web-perm",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
4
4
|
"main": "dist/w-web-perm.umd.js",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"json5": "^2.2.3",
|
|
7
|
-
"w-aggrid-vue": "^2.1.
|
|
8
|
-
"w-component-vue": "^2.5.
|
|
9
|
-
"w-data-collector": "^1.0.
|
|
10
|
-
"w-echarts-vue": "^1.0.
|
|
11
|
-
"w-serv-hapi": "^1.1.
|
|
12
|
-
"w-serv-orm": "^1.1.
|
|
13
|
-
"w-syslog": "^1.0.
|
|
14
|
-
"w-ui-loginout": "^1.0.
|
|
15
|
-
"wsemi": "^1.9.
|
|
7
|
+
"w-aggrid-vue": "^2.1.5",
|
|
8
|
+
"w-component-vue": "^2.5.26",
|
|
9
|
+
"w-data-collector": "^1.0.41",
|
|
10
|
+
"w-echarts-vue": "^1.0.16",
|
|
11
|
+
"w-serv-hapi": "^1.1.3",
|
|
12
|
+
"w-serv-orm": "^1.1.4",
|
|
13
|
+
"w-syslog": "^1.0.30",
|
|
14
|
+
"w-ui-loginout": "^1.0.29",
|
|
15
|
+
"wsemi": "^1.9.9"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"@vue/cli-plugin-babel": "^5.0.8",
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
"eslint-plugin-vue": "^9.10.0",
|
|
28
28
|
"vue": "^2.7.16",
|
|
29
29
|
"vuex": "^3.6.2",
|
|
30
|
-
"w-orm-lmdb": "^1.0.
|
|
30
|
+
"w-orm-lmdb": "^1.0.35",
|
|
31
31
|
"w-package-tools": "^1.1.16",
|
|
32
|
-
"w-package-tools-e2e": "^1.0.
|
|
33
|
-
"w-screenctl": "^1.0.
|
|
32
|
+
"w-package-tools-e2e": "^1.0.6",
|
|
33
|
+
"w-screenctl": "^1.0.23"
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
36
|
"serve": "vue-cli-service serve",
|
package/public/index.html
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
|
|
6
6
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
7
|
+
<!-- referrer: 本站以 ?token= 開啟(w-ui-loginout 讀完不清網址), 瀏覽器預設會把含權杖之完整網址放進同源請求之 Referer, 進而寫入後端 log 與反向代理 access log; strict-origin 使請求只帶 origin(見 spec/設計要點與取捨.md ADR-023) -->
|
|
8
|
+
<meta name="referrer" content="strict-origin">
|
|
7
9
|
<title></title>
|
|
8
10
|
|
|
9
11
|
<!-- mdi font -->
|
package/server/WWebPerm.mjs
CHANGED
|
@@ -23,14 +23,18 @@ import replace from 'wsemi/src/replace.mjs'
|
|
|
23
23
|
import strdelleft from 'wsemi/src/strdelleft.mjs'
|
|
24
24
|
import ltdtmapping from 'wsemi/src/ltdtmapping.mjs'
|
|
25
25
|
import pmKeyMutex from 'wsemi/src/pmKeyMutex.mjs'
|
|
26
|
+
import cacheSt from 'wsemi/src/cacheSt.mjs'
|
|
26
27
|
import WServHapiServer from 'w-serv-hapi/src/WServHapiServer.mjs'
|
|
27
28
|
import WServOrm from 'w-serv-orm/src/WServOrm.mjs'
|
|
28
29
|
import ds from '../src/schema/index.mjs'
|
|
29
30
|
import { getUserRules } from '../src/plugins/mShare.mjs'
|
|
30
31
|
import procCore from './procCore.mjs'
|
|
32
|
+
import createLockSave from './lockSave.mjs'
|
|
31
33
|
import procLang from './procLang.mjs'
|
|
32
34
|
import srLogInit from './srLog.mjs'
|
|
33
35
|
import procStaInfor from './procStaInfor.mjs'
|
|
36
|
+
import { maskTok, maskQuery, maskUrl } from './maskLog.mjs'
|
|
37
|
+
import wrapInjected from './wrapInjected.mjs'
|
|
34
38
|
|
|
35
39
|
|
|
36
40
|
/**
|
|
@@ -40,9 +44,9 @@ import procStaInfor from './procStaInfor.mjs'
|
|
|
40
44
|
* @param {Function} WOrm 輸入資料庫ORM函數
|
|
41
45
|
* @param {String} url 輸入資料庫連線字串,例如w-orm-lmdb為'./db',或w-orm-mongodb為'mongodb://username:password@$127.0.0.1:27017'
|
|
42
46
|
* @param {String} db 輸入資料庫名稱字串
|
|
43
|
-
* @param {Function} getUserByToken 輸入處理函數,函數會傳入使用者token,通過此函數處理後並回傳使用者資訊物件,並至少須提供'id'、'email'、'name'、'isAdmin'欄位,且'isAdmin'限輸入'y'或'n'。注意: token 回傳之'isAdmin'僅供識別,權限系統一律以自身users表該使用者之'isAdmin'欄位值為權威,不會被 token
|
|
44
|
-
* @param {Function} verifyClientUser 輸入驗證瀏覽使用者身份之處理函數,函數會傳入使用者資訊物件(perm users 表之列)與來源字串from,通過此函數識別後回傳布林值(可為Promise),允許使用者回傳true,反之回傳false。from 值域: 'getUserByToken'(登入)、'getPerm'(api/getPerm)、以及資料通道之 apiType 'main'(RPC 與 ORM 執行)、'upload-controller'、'upload-slice'、'download-get-filename'、'download-get-file'、'download';資料通道與登入採同一授權強度(反查 perm users 且 isActive='y' 後再經本函數)
|
|
45
|
-
* @param {Function} verifyAppUser 輸入驗證應用程序使用者身份之處理函數,函數會傳入使用者資訊物件,通過此函數識別後回傳布林值,允許使用者回傳true,反之回傳false
|
|
47
|
+
* @param {Function} getUserByToken 輸入處理函數,函數會傳入使用者token,通過此函數處理後並回傳使用者資訊物件,並至少須提供'id'、'email'、'name'、'isAdmin'欄位,且'isAdmin'限輸入'y'或'n'。注意: token 回傳之'isAdmin'僅供識別,權限系統一律以自身users表該使用者之'isAdmin'欄位值為權威,不會被 token 宣稱之值複寫。函數拋錯或reject時視同查無使用者(回自家錯誤key,錯誤原文不回前端、不入log,只於srLog記warn)
|
|
48
|
+
* @param {Function} verifyClientUser 輸入驗證瀏覽使用者身份之處理函數,函數會傳入使用者資訊物件(perm users 表之列)與來源字串from,通過此函數識別後回傳布林值(可為Promise),允許使用者回傳true,反之回傳false。from 值域: 'getUserByToken'(登入)、'getPerm'(api/getPerm)、以及資料通道之 apiType 'main'(RPC 與 ORM 執行)、'upload-controller'、'upload-slice'、'download-get-filename'、'download-get-file'、'download';資料通道與登入採同一授權強度(反查 perm users 且 isActive='y' 後再經本函數);函數拋錯或reject時視同回傳false
|
|
49
|
+
* @param {Function} verifyAppUser 輸入驗證應用程序使用者身份之處理函數,函數會傳入使用者資訊物件,通過此函數識別後回傳布林值,允許使用者回傳true,反之回傳false;函數拋錯或reject時視同回傳false
|
|
46
50
|
* @param {Object} [opt={}] 輸入設定物件,預設{}
|
|
47
51
|
* @param {Integer} [opt.serverPort=11006] 輸入伺服器通訊port,預設11006
|
|
48
52
|
* @param {Boolean} [opt.useCheckUser=false] 輸入是否檢查使用者資訊布林值,預設false
|
|
@@ -110,7 +114,7 @@ import procStaInfor from './procStaInfor.mjs'
|
|
|
110
114
|
* isAdmin: 'y',
|
|
111
115
|
* }
|
|
112
116
|
* }
|
|
113
|
-
* console.log('invalid token'
|
|
117
|
+
* console.log('invalid token') //勿印權杖值
|
|
114
118
|
* console.log('於生產環境時得加入SSO等驗證token機制')
|
|
115
119
|
* return {}
|
|
116
120
|
* }
|
|
@@ -274,6 +278,27 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
274
278
|
let srLog = srLogInit(opt)
|
|
275
279
|
|
|
276
280
|
|
|
281
|
+
//注入函數一律經包裝(W 契約, spec/設計要點與取捨.md ADR-023), 以包裝版取代三個參數
|
|
282
|
+
//why: 注入函數拋錯或 reject 時原會被 await 原樣上拋, 其原文經 pm2resolve 回前端、經 verifyConn 之 msg 入 srLog、經 error 事件印出;
|
|
283
|
+
//部署方常以 w-web-sso 對外 helper(≤1.1.4)包裝 getUserByToken, 其 reject 字串含已代入系統介接權杖與使用者權杖之完整網址.
|
|
284
|
+
//包裝後失敗視同否定結果(getUserByToken → null 等同查無, verify* → false 等同無權限), 走各呼叫點既有分支回自家 key;
|
|
285
|
+
//上游原文不回前端、不入 log, 只於此記 warn(白名單 key, 或型別與 Error.name). 同一次失敗 log 2~3 筆: 此處 warn(因) + 呼叫點 error(果).
|
|
286
|
+
//以下取代後, 本函數內除此三行外不得再直接使用原始參數(盤點見 CLAUDE_rulebook.md「權杖不外洩」, 測試 unit-wrapInjected WRAP-SITE-*)
|
|
287
|
+
let logInjectFail = (name) => {
|
|
288
|
+
return (d) => {
|
|
289
|
+
srLog.warn({ event: `inject-${name}-fail`, ...d })
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
getUserByToken = wrapInjected(getUserByToken, { failValue: null, onFail: logInjectFail('getUserByToken') })
|
|
293
|
+
verifyClientUser = wrapInjected(verifyClientUser, { failValue: false, onFail: logInjectFail('verifyClientUser') })
|
|
294
|
+
verifyAppUser = wrapInjected(verifyAppUser, { failValue: false, onFail: logInjectFail('verifyAppUser') })
|
|
295
|
+
//getUserById: 第四個注入函數(選填; useCheckUser=true 時由 WServOrm 於 execute 內呼叫, 原 reject 原文經 pm2resolve 回前端), 有給才包裝;
|
|
296
|
+
//失敗 → null 等同查無, 由 WServOrm 既有之查無分支拒絕(fail-closed). 須於 WServOrm 建構前取代(建構時即取走此函數)
|
|
297
|
+
if (isfun(getUserById)) {
|
|
298
|
+
getUserById = wrapInjected(getUserById, { failValue: null, onFail: logInjectFail('getUserById') })
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
|
|
277
302
|
//WServOrm
|
|
278
303
|
let optWServOrm = {
|
|
279
304
|
useCheckUser,
|
|
@@ -297,8 +322,14 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
297
322
|
let kmx = pmKeyMutex()
|
|
298
323
|
|
|
299
324
|
|
|
325
|
+
//lockSave: 後台整表寫入(updateTabItems)之雙擊防護(後端, 見 lockSave.mjs, ADR-025): 以「updateTabItems:{表名}:{操作者 id}」於 cacheSt 原子占位,
|
|
326
|
+
//同一操作者之同表寫入處理中再送出即 reject 'saveInProgress'(不排隊); 包在 kmx 之外層, 不同操作者之並行仍由 kmx 序列化(ADR-012)
|
|
327
|
+
let cst = cacheSt()
|
|
328
|
+
let lockSave = createLockSave(cst)
|
|
329
|
+
|
|
330
|
+
|
|
300
331
|
//procCore: 整表批次 diff 寫入邏輯 (updateTabItems + updateTargets/Pemis/Grups/Users)
|
|
301
|
-
let pc = procCore(woItems, procOrm, { srLog, kmx })
|
|
332
|
+
let pc = procCore(woItems, procOrm, { srLog, kmx, lockSave })
|
|
302
333
|
|
|
303
334
|
|
|
304
335
|
//procStaInfor: 統計資訊, fdLog 對齊 srLog 使用同一目錄(讀 settings.logFd, fallback 邏輯同 srLog.mjs); mock 由 settings.staEventMock 注入(e2e 統計圖穩定用)
|
|
@@ -396,7 +427,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
396
427
|
|
|
397
428
|
//check
|
|
398
429
|
if (!iseobj(userSelf)) {
|
|
399
|
-
console.log(`token`, token)
|
|
430
|
+
console.log(`token`, maskTok(token))
|
|
400
431
|
srLog.error({ event: 'getTokenUser-error', msg: 'cannotFindUserFromToken' })
|
|
401
432
|
return Promise.reject(`cannotFindUserFromToken`)
|
|
402
433
|
}
|
|
@@ -478,7 +509,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
478
509
|
|
|
479
510
|
//check
|
|
480
511
|
if (!iseobj(user)) {
|
|
481
|
-
console.log(`token`, token)
|
|
512
|
+
console.log(`token`, maskTok(token))
|
|
482
513
|
srLog.error({ event: 'getAndVerifyAppUser-error', msg: 'cannotFindUserFromToken' })
|
|
483
514
|
return Promise.reject(`cannotFindUserFromToken`)
|
|
484
515
|
}
|
|
@@ -750,8 +781,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
750
781
|
|
|
751
782
|
//check
|
|
752
783
|
if (!isestr(token)) {
|
|
753
|
-
console.log('req.query', get(req, 'query'))
|
|
754
|
-
console.log('token', token)
|
|
784
|
+
console.log('req.query', maskQuery(get(req, 'query')))
|
|
785
|
+
console.log('token', maskTok(token))
|
|
755
786
|
console.log('[API]getUserByToken/check token: invalid token')
|
|
756
787
|
srLog.error({ event: 'api/getUserByToken-error', msg: 'tokenNoPermission' })
|
|
757
788
|
return Promise.reject(`tokenNoPermission`)
|
|
@@ -763,7 +794,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
763
794
|
|
|
764
795
|
//check
|
|
765
796
|
if (!iseobj(user)) {
|
|
766
|
-
console.log('token', token)
|
|
797
|
+
console.log('token', maskTok(token))
|
|
767
798
|
console.log('[API]getUserByToken/check user: invalid user')
|
|
768
799
|
srLog.error({ event: 'api/getUserByToken-error', msg: 'tokenNoPermission' })
|
|
769
800
|
return Promise.reject(`tokenNoPermission`)
|
|
@@ -800,8 +831,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
800
831
|
|
|
801
832
|
//check
|
|
802
833
|
if (!isestr(token)) {
|
|
803
|
-
console.log('req.query', get(req, 'query'))
|
|
804
|
-
console.log('token', token)
|
|
834
|
+
console.log('req.query', maskQuery(get(req, 'query')))
|
|
835
|
+
console.log('token', maskTok(token))
|
|
805
836
|
console.log('[API]getPerm/check token: invalid token')
|
|
806
837
|
srLog.error({ event: 'api/getPerm-error', msg: 'tokenNoPermission' })
|
|
807
838
|
return Promise.reject(`tokenNoPermission`)
|
|
@@ -813,7 +844,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
813
844
|
|
|
814
845
|
//check
|
|
815
846
|
if (!iseobj(userSelf)) {
|
|
816
|
-
console.log('token', token)
|
|
847
|
+
console.log('token', maskTok(token))
|
|
817
848
|
console.log('[API]getPerm/check userSelf: invalid userSelf')
|
|
818
849
|
srLog.error({ event: 'api/getPerm-error', msg: 'tokenNoPermission' })
|
|
819
850
|
return Promise.reject(`tokenNoPermission`)
|
|
@@ -825,7 +856,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
825
856
|
|
|
826
857
|
//check
|
|
827
858
|
if (!isestr(userIdSelf)) {
|
|
828
|
-
console.log('token', token)
|
|
859
|
+
console.log('token', maskTok(token))
|
|
829
860
|
console.log('userSelf', userSelf)
|
|
830
861
|
console.log('[API]getPerm/check userIdSelf: invalid userIdSelf')
|
|
831
862
|
srLog.error({ event: 'api/getPerm-error', msg: 'tokenNoPermission' })
|
|
@@ -838,7 +869,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
838
869
|
|
|
839
870
|
//check
|
|
840
871
|
if (!iseobj(userWithRulesSelf)) {
|
|
841
|
-
console.log('token', token)
|
|
872
|
+
console.log('token', maskTok(token))
|
|
842
873
|
console.log('userSelf', userSelf)
|
|
843
874
|
console.log('userIdSelf', userIdSelf)
|
|
844
875
|
console.log('[API]getPerm/check userWithRulesSelf: invalid userWithRulesSelf')
|
|
@@ -877,7 +908,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
877
908
|
|
|
878
909
|
//check
|
|
879
910
|
if (!isestr(tokenSelf)) {
|
|
880
|
-
console.log('req.query', get(req, 'query'))
|
|
911
|
+
console.log('req.query', maskQuery(get(req, 'query')))
|
|
881
912
|
console.log('[API]getPermUserInfor/check tokenSelf: invalid tokenSelf')
|
|
882
913
|
srLog.error({ event: 'api/getPermUserInfor-error', msg: 'tokenNoPermission' })
|
|
883
914
|
return Promise.reject(`tokenNoPermission`)
|
|
@@ -889,8 +920,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
889
920
|
|
|
890
921
|
//check
|
|
891
922
|
if (!iseobj(userSelf)) {
|
|
892
|
-
console.log('req.query', get(req, 'query'))
|
|
893
|
-
console.log('tokenSelf', tokenSelf)
|
|
923
|
+
console.log('req.query', maskQuery(get(req, 'query')))
|
|
924
|
+
console.log('tokenSelf', maskTok(tokenSelf))
|
|
894
925
|
console.log('[API]getPermUserInfor/check userSelf: invalid userSelf')
|
|
895
926
|
srLog.error({ event: 'api/getPermUserInfor-error', msg: 'tokenNoPermission' })
|
|
896
927
|
return Promise.reject(`tokenNoPermission`)
|
|
@@ -902,8 +933,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
902
933
|
|
|
903
934
|
//check
|
|
904
935
|
if (!isestr(userIdFind)) {
|
|
905
|
-
console.log('req.query', get(req, 'query'))
|
|
906
|
-
console.log('tokenSelf', tokenSelf)
|
|
936
|
+
console.log('req.query', maskQuery(get(req, 'query')))
|
|
937
|
+
console.log('tokenSelf', maskTok(tokenSelf))
|
|
907
938
|
console.log('[API]getPermUserInfor/check userIdFind: invalid userIdFind')
|
|
908
939
|
srLog.error({ event: 'api/getPermUserInfor-error', msg: 'tokenNoPermission' })
|
|
909
940
|
return Promise.reject(`tokenNoPermission`)
|
|
@@ -952,8 +983,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
952
983
|
|
|
953
984
|
//check
|
|
954
985
|
if (!isestr(token)) {
|
|
955
|
-
console.log('req.query', get(req, 'query'))
|
|
956
|
-
console.log('token', token)
|
|
986
|
+
console.log('req.query', maskQuery(get(req, 'query')))
|
|
987
|
+
console.log('token', maskTok(token))
|
|
957
988
|
console.log('[API]syncAndReplaceTabs/check token: invalid token')
|
|
958
989
|
srLog.error({ event: 'api/syncAndReplaceTabs-error', msg: 'tokenNoPermission' })
|
|
959
990
|
return Promise.reject(`tokenNoPermission`)
|
|
@@ -964,7 +995,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
964
995
|
|
|
965
996
|
//check
|
|
966
997
|
if (!isestr(keyTable)) {
|
|
967
|
-
console.log('req.query', get(req, 'query'))
|
|
998
|
+
console.log('req.query', maskQuery(get(req, 'query')))
|
|
968
999
|
console.log('keyTable', keyTable)
|
|
969
1000
|
console.log('[API]syncAndReplaceTabs/check keyTable: invalid keyTable')
|
|
970
1001
|
srLog.error({ event: 'api/syncAndReplaceTabs-error', msg: 'tokenNoPermission' })
|
|
@@ -984,7 +1015,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
984
1015
|
|
|
985
1016
|
//check user
|
|
986
1017
|
if (!iseobj(user)) {
|
|
987
|
-
console.log('token', token)
|
|
1018
|
+
console.log('token', maskTok(token))
|
|
988
1019
|
console.log('[API]syncAndReplaceTabs/check user: invalid user')
|
|
989
1020
|
srLog.error({ event: 'api/syncAndReplaceTabs-error', msg: 'tokenNoPermission' })
|
|
990
1021
|
return Promise.reject(`tokenNoPermission`)
|
|
@@ -995,7 +1026,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
995
1026
|
|
|
996
1027
|
//check userId
|
|
997
1028
|
if (!isestr(userId)) {
|
|
998
|
-
console.log('token', token)
|
|
1029
|
+
console.log('token', maskTok(token))
|
|
999
1030
|
console.log('user', user)
|
|
1000
1031
|
console.log('[API]syncAndReplaceTabs/check userId: invalid userId')
|
|
1001
1032
|
srLog.error({ event: 'api/syncAndReplaceTabs-error', msg: 'tokenNoPermission' })
|
|
@@ -1052,8 +1083,9 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
1052
1083
|
//check
|
|
1053
1084
|
if (!isestr(token)) {
|
|
1054
1085
|
console.log('apiType', apiType)
|
|
1055
|
-
console.log('authorization', authorization)
|
|
1086
|
+
console.log('authorization', maskTok(authorization))
|
|
1056
1087
|
console.log('invalid token')
|
|
1088
|
+
srLog.error({ event: 'verifyConn-error', ok: false, apiType, msg: 'tokenNoPermission' }) //認證失敗須入 log 供稽核/統計(CLAUDE.md「log 記錄要點」; 同 w-web-api、w-web-task); 只記 key, 不記 authorization 原值
|
|
1057
1089
|
return false
|
|
1058
1090
|
}
|
|
1059
1091
|
|
|
@@ -1078,8 +1110,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
1078
1110
|
//origin
|
|
1079
1111
|
let origin = get(headers, 'origin', '')
|
|
1080
1112
|
|
|
1081
|
-
//referer
|
|
1082
|
-
let referer = get(headers, 'referer', '')
|
|
1113
|
+
//referer, 只記 origin + pathname(ADR-023): 前端以 ?token= 開站(w-ui-loginout 讀完不清網址), 同源請求之 Referer 預設帶完整網址, 原樣記錄即把使用者權杖寫入 srLog
|
|
1114
|
+
let referer = maskUrl(get(headers, 'referer', ''))
|
|
1083
1115
|
|
|
1084
1116
|
//info, 失敗時 msg 為 err key(cannotFindUserFromToken / cannotGetUserFromPerm / userNoPermission 等), 供資安稽核與統計
|
|
1085
1117
|
if (b) {
|
|
@@ -1096,7 +1128,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
1096
1128
|
let user = await getUserByToken(token)
|
|
1097
1129
|
let userId = get(user, 'id', '')
|
|
1098
1130
|
if (!isestr(userId)) {
|
|
1099
|
-
console.log('token', token)
|
|
1131
|
+
console.log('token', maskTok(token))
|
|
1100
1132
|
console.log('userId', userId)
|
|
1101
1133
|
srLog.error({ event: 'getUserIdByToken-error', msg: 'cannotFindUserId' })
|
|
1102
1134
|
return Promise.reject(`cannotFindUserId`)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import get from 'lodash-es/get.js'
|
|
2
|
+
import isestr from 'wsemi/src/isestr.mjs'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 有副作用之寫入路徑(如清單儲存 updateUsersList / updateTokensList / updateIpsList)之雙擊防護(後端, ADR-025)
|
|
7
|
+
*
|
|
8
|
+
* 以「操作:操作者 id」於 wsemi cacheSt 原子占位執行 fn, 同一操作者之同一操作處理中再送出即 reject 錯誤 key
|
|
9
|
+
* (預設 'saveInProgress', 可以 opt.errKey 指定; 皆為 i18n key, 見 procLang); 不同操作者、不同操作互不影響;
|
|
10
|
+
* fn 之 reject 原樣 bubble; fn 結束(含失敗)即釋放.
|
|
11
|
+
* 第 2 次不排隊執行: 其內容與第 1 次相同, 排隊後再寫一次會把第 1 次剛建立之列當修改覆寫
|
|
12
|
+
* (依序重送另由各專案之資料層檢查擋, 如 SSO procCore.updateTabItems 之新增列檢查回 'saveNewRowExists').
|
|
13
|
+
* operatorId 可為操作者 id 或「操作者 id + 列識別」(只擋同一列之連發, 同批其他列不受影響).
|
|
14
|
+
*
|
|
15
|
+
* @param {Object} cst 輸入 wsemi cacheSt 實例
|
|
16
|
+
* @returns {Function} 回傳 async (op, operatorId, fn, opt = {}) => fn 之結果, opt.errKey 為占位衝突時 reject 之 key
|
|
17
|
+
*/
|
|
18
|
+
function createLockSave(cst) {
|
|
19
|
+
|
|
20
|
+
let lockSave = async (op, operatorId, fn, opt = {}) => {
|
|
21
|
+
|
|
22
|
+
//errKey
|
|
23
|
+
let errKey = get(opt, 'errKey', '')
|
|
24
|
+
if (!isestr(errKey)) {
|
|
25
|
+
errKey = 'saveInProgress'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let key = `${op}:${operatorId}`
|
|
29
|
+
return await cst.setWithFree([key], fn)
|
|
30
|
+
.catch((err) => {
|
|
31
|
+
//remap setWithFree 之占位衝突 → i18n key; fn 內之 reject 原樣 bubble
|
|
32
|
+
if (err === `setWithFree: key in use: ${key}`) {
|
|
33
|
+
return Promise.reject(errKey)
|
|
34
|
+
}
|
|
35
|
+
return Promise.reject(err)
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return lockSave
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
export default createLockSave
|