w-web-perm 1.1.12 → 1.1.13
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 +158 -123
- package/dist/css/app.f09c7433.css +1 -0
- package/dist/index.tmp +1 -1
- package/dist/js/app-legacy.8af87052.js +1 -0
- package/dist/js/app.8af87052.js +1 -0
- package/dist/js/{chunk-vendors.3e9e3ef1.js → chunk-vendors-legacy.ece5a361.js} +1 -1
- package/dist/js/{chunk-vendors-legacy.3e9e3ef1.js → chunk-vendors.ece5a361.js} +1 -1
- package/dist/w-web-perm.umd.js +2 -2
- package/dist/w-web-perm.umd.js.map +1 -1
- package/docs/WWebPerm.html +84 -4
- package/docs/global.html +23023 -1125
- package/docs/index.html +2 -2
- package/docs/server_WWebPerm.mjs.html +232 -32
- package/docs/server_cascadeWrite.mjs.html +225 -0
- package/docs/server_dataChannel.mjs.html +2 -2
- package/docs/server_keyCheck.mjs.html +353 -0
- package/docs/server_lockSave.mjs.html +2 -2
- package/docs/server_maskLog.mjs.html +2 -2
- package/docs/server_procCore.mjs.html +180 -41
- package/docs/server_staLogs_resolveShellWorker.mjs.html +2 -2
- package/docs/server_syncKeep.mjs.html +311 -0
- package/docs/server_tools_authRefsAudit.mjs.html +247 -0
- package/docs/server_tools_authRefsClean.mjs.html +335 -0
- package/docs/server_tools_fromTools.mjs.html +797 -0
- package/docs/server_tools_isMain.mjs.html +103 -0
- package/docs/server_tools_nameDupReport.mjs.html +280 -0
- package/docs/server_validateRows.mjs.html +2 -2
- package/docs/server_wrapInjected.mjs.html +3 -3
- package/docs/src_fetchJson.mjs.html +2 -2
- package/docs/src_js_cvKpToArr.mjs.html +2 -2
- package/docs/src_js_cvKpToObj.mjs.html +44 -33
- package/docs/src_js_draftSync.mjs.html +142 -0
- package/docs/src_js_eachOwn.mjs.html +3 -3
- package/docs/src_js_editorRefRows.mjs.html +356 -0
- package/docs/src_js_externalRows.mjs.html +338 -0
- package/docs/src_js_externalUi.mjs.html +230 -0
- package/docs/src_js_findRefRows.mjs.html +107 -0
- package/docs/src_js_findTouchedDup.mjs.html +150 -0
- package/docs/src_js_fmtLossyMsg.mjs.html +2 -2
- package/docs/src_js_fromAudit.mjs.html +489 -0
- package/docs/src_js_getAddImpact.mjs.html +269 -0
- package/docs/src_js_getAuthWarn.mjs.html +463 -0
- package/docs/src_js_getCascadePreview.mjs.html +258 -0
- package/docs/src_js_getDelRefCount.mjs.html +210 -0
- package/docs/src_js_getEffMode.mjs.html +92 -0
- package/docs/src_js_getKpLossy.mjs.html +47 -52
- package/docs/src_js_getNameErrs.mjs.html +266 -0
- package/docs/src_js_getOwn.mjs.html +3 -3
- package/docs/src_js_getPropKey.mjs.html +109 -0
- package/docs/src_js_getRuleEnable.mjs.html +2 -2
- package/docs/src_js_isAuthMode.mjs.html +4 -4
- package/docs/src_js_joinNames.mjs.html +2 -2
- package/docs/src_js_legendFit.mjs.html +2 -2
- package/docs/src_js_pairRowsToSnapshot.mjs.html +156 -0
- package/docs/src_js_refAudit.mjs.html +359 -0
- package/docs/src_js_refCascade.mjs.html +572 -0
- package/docs/src_js_relDialogSend.mjs.html +199 -0
- package/docs/src_js_rowDigest.mjs.html +265 -0
- package/docs/src_plugins_dataReloadGuard.mjs.html +109 -0
- package/docs/src_plugins_reuseDeletedIds.mjs.html +2 -2
- package/docs/src_plugins_submitGuard.mjs.html +2 -2
- package/g_authRefsClean.mjs +79 -0
- package/g_fixFromType.mjs +92 -0
- package/g_initialData.mjs +3 -3
- package/g_purgeFrom.mjs +127 -0
- package/package.json +1 -1
- package/server/WWebPerm.mjs +230 -30
- package/server/cascadeWrite.mjs +153 -0
- package/server/keyCheck.mjs +281 -0
- package/server/procCore.mjs +178 -39
- package/server/procLang.mjs +270 -3
- package/server/syncKeep.mjs +239 -0
- package/server/tools/authRefsAudit.mjs +175 -0
- package/server/tools/authRefsClean.mjs +263 -0
- package/server/tools/fromTools.mjs +725 -0
- package/server/tools/isMain.mjs +31 -0
- package/server/tools/nameDupReport.mjs +208 -0
- package/server/wrapInjected.mjs +1 -1
- package/src/components/LayoutContentGrups.vue +396 -75
- package/src/components/LayoutContentPemis.vue +395 -75
- package/src/components/LayoutContentTargets.vue +261 -25
- package/src/components/LayoutContentUsers.vue +334 -97
- package/src/components/VeCgrups.vue +88 -129
- package/src/components/VeCpemis.vue +86 -129
- package/src/components/VeCrules.vue +15 -5
- package/src/components/VeGrupBlngUsers.vue +202 -89
- package/src/components/VePemiBlngGrups.vue +209 -99
- package/src/js/cvKpToObj.mjs +42 -31
- package/src/js/draftSync.mjs +70 -0
- package/src/js/eachOwn.mjs +1 -1
- package/src/js/editorRefRows.mjs +284 -0
- package/src/js/externalRows.mjs +266 -0
- package/src/js/externalUi.mjs +158 -0
- package/src/js/findRefRows.mjs +35 -0
- package/src/js/findTouchedDup.mjs +78 -0
- package/src/js/fromAudit.mjs +417 -0
- package/src/js/getAddImpact.mjs +197 -0
- package/src/js/getAuthWarn.mjs +391 -0
- package/src/js/getCascadePreview.mjs +186 -0
- package/src/js/getDelRefCount.mjs +138 -0
- package/src/js/getEffMode.mjs +20 -0
- package/src/js/getKpLossy.mjs +44 -49
- package/src/js/getNameErrs.mjs +194 -0
- package/src/js/getOwn.mjs +1 -1
- package/src/js/getPropKey.mjs +37 -0
- package/src/js/isAuthMode.mjs +2 -2
- package/src/js/pairRowsToSnapshot.mjs +84 -0
- package/src/js/refAudit.mjs +287 -0
- package/src/js/refCascade.mjs +500 -0
- package/src/js/relDialogSend.mjs +127 -0
- package/src/js/rowDigest.mjs +193 -0
- package/src/plugins/dataReloadGuard.mjs +37 -0
- package/src/plugins/mShare.mjs +144 -140
- package/src/plugins/mUI.mjs +3 -8
- package/src/schema/tables/users.mjs +152 -152
- package/dist/css/app.6918ec45.css +0 -1
- package/dist/js/app-legacy.7cb7e394.js +0 -1
- package/dist/js/app.7cb7e394.js +0 -1
- package/docs/src_js_getKeyOrderLost.mjs.html +0 -121
- package/docs/src_js_getKpWriteLost.mjs.html +0 -100
- package/docs/src_js_keepKeyOrder.mjs.html +0 -139
- package/src/js/getKeyOrderLost.mjs +0 -49
- package/src/js/getKpWriteLost.mjs +0 -28
- package/src/js/keepKeyOrder.mjs +0 -67
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import iseobj from 'wsemi/src/iseobj.mjs'
|
|
2
|
+
import findTouchedDup from '../src/js/findTouchedDup.mjs' //判準單一擁有者在 src/js(前端清單頁共用), 本檔轉出供 procCore.mjs 與測試沿用既有匯入路徑
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
//名稱唯一(F-02)與使用者對應鍵(F-14)之判準, 單一擁有者(規則帳本 R-NAME-UNIQ-01/02、R-MAPKEY-01/02/03, ADR-034):
|
|
6
|
+
// 外部同步(WWebPerm.mjs:syncAndReplaceTabs)、後台整表寫入(procCore.mjs:updateTabItems)、登入查找(WWebPerm.mjs:getTokenUser/getGenUserByKV)只呼叫本檔, 不複製判準.
|
|
7
|
+
//比對一律精確 ===(與伺服端 arrFinder 與 getTokenUser 之 select 一致; 不做大小寫/空白正規化: 正規化後比對會放行伺服端視為不同之名稱, 或拒絕伺服端視為不同之鍵).
|
|
8
|
+
//鍵之值非字串者不參與重複比對(空/非字串另由呼叫端之 isestr 檢查負責). 以 Map/Set 計, 免原型鍵名(constructor、__proto__)誤判(規則帳本 R-KEY-01).
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
let isStr = (v) => typeof v === 'string'
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 回傳 rows 內第一個「key 欄位值(須為字串)與先前某參與列相同」之列索引, 無則 -1
|
|
16
|
+
*
|
|
17
|
+
* @param {Array} rows 列陣列
|
|
18
|
+
* @param {String} key 欄位名
|
|
19
|
+
* @param {Function} [isPart] 判斷該列是否參與比對(如只計 active), 預設全部參與
|
|
20
|
+
* @returns {Number} 第二次(含)出現者之索引, 無重複為 -1
|
|
21
|
+
*/
|
|
22
|
+
function findDupIndex(rows, key, isPart = () => true) {
|
|
23
|
+
if (!Array.isArray(rows)) {
|
|
24
|
+
return -1
|
|
25
|
+
}
|
|
26
|
+
let seen = new Set()
|
|
27
|
+
for (let k = 0; k < rows.length; k++) {
|
|
28
|
+
let r = rows[k]
|
|
29
|
+
if (!isPart(r)) {
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
let v = (r === null || typeof r !== 'object') ? undefined : r[key]
|
|
33
|
+
if (!isStr(v)) {
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
if (seen.has(v)) {
|
|
37
|
+
return k
|
|
38
|
+
}
|
|
39
|
+
seen.add(v)
|
|
40
|
+
}
|
|
41
|
+
return -1
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* rows 內是否有兩列 key 值相同(字串, 精確 ===)
|
|
47
|
+
*/
|
|
48
|
+
function hasDupKey(rows, key) {
|
|
49
|
+
return findDupIndex(rows, key) >= 0
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 回傳 rows 中第一個「key 值已被 from 不等於本 from 之既有列使用」之索引, 無則 -1; 同 from 之既有列不算衝突(隨後由 delAll 重寫)
|
|
55
|
+
*
|
|
56
|
+
* @param {Array} rows 本批列
|
|
57
|
+
* @param {Array} rowsExist 該表既有之全部列(select 結果)
|
|
58
|
+
* @param {String} from 本批之 from
|
|
59
|
+
* @param {String} key 欄位名
|
|
60
|
+
* @param {Object} [opt={}] 設定
|
|
61
|
+
* @param {Boolean} [opt.onlyActive=false] 為 true 時兩側皆只計 isActive==='y' 之列(使用者對應鍵: getTokenUser 只匹配 active)
|
|
62
|
+
* @returns {Number} 衝突列之索引, 無為 -1
|
|
63
|
+
*/
|
|
64
|
+
function findKeyOtherFrom(rows, rowsExist, from, key, opt = {}) {
|
|
65
|
+
if (!Array.isArray(rows) || !Array.isArray(rowsExist)) {
|
|
66
|
+
return -1
|
|
67
|
+
}
|
|
68
|
+
let onlyActive = opt.onlyActive === true
|
|
69
|
+
let used = new Set()
|
|
70
|
+
for (let r of rowsExist) {
|
|
71
|
+
if (r === null || typeof r !== 'object') {
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
if (r.from === from) {
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
if (onlyActive && r.isActive !== 'y') {
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
if (isStr(r[key])) {
|
|
81
|
+
used.add(r[key])
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (let k = 0; k < rows.length; k++) {
|
|
85
|
+
let r = rows[k]
|
|
86
|
+
if (r === null || typeof r !== 'object') {
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
if (onlyActive && r.isActive !== 'y') {
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
if (isStr(r[key]) && used.has(r[key])) {
|
|
93
|
+
return k
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return -1
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 同表寫入互斥鍵(外審 sol R10, D2 設計稿 (a) 之最小版): 外部同步(syncAndReplaceTabs)與後台整表寫入(updateTabItems)對同一張表須共用同一個 kmx 鍵,
|
|
102
|
+
* 否則兩者各自「先檢查名稱/對應鍵、後寫入」可交錯——檢查後、寫入前對方已完成寫入, F-02/F-14 之全域唯一性被略過(終態同名兩列、授權取決於列序).
|
|
103
|
+
* 兩條路徑內都不得再取同一個鍵(kmx 非可重入, 巢狀同鍵即死結): syncAndReplaceTabs 內只呼叫 procOrm/woItems, 不呼叫 updateTabItems.
|
|
104
|
+
*
|
|
105
|
+
* @param {String} keyTable 表名
|
|
106
|
+
* @returns {String} 回傳 kmx 鍵
|
|
107
|
+
*/
|
|
108
|
+
function tabWriteKey(keyTable) {
|
|
109
|
+
return 'tabWrite:' + keyTable
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
//取鎖階層(規則帳本 R-WRITE-LOCK-02, ADR-036): 引用鏈序「被引用者先、引用端後」targets → pemis → grups → users(引用端之階層恆高於被引用者); 所有需持多把表鎖之路徑(後台整表寫入、外部同步、日後之外部列保護/清源)只能經 withTabLocks 取鎖,
|
|
114
|
+
//同一把鎖不巢狀(pmKeyMutex 非可重入, 巢狀同鍵與相反次序皆死結, D1 設計稿 probe-mutex 實測); 持鎖後才讀取、預檢與寫入, 使預檢與寫入涵蓋同一版本(外審 A.3)
|
|
115
|
+
let TAB_LOCK_RANK = { targets: 0, pemis: 1, grups: 2, users: 3 }
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 依取鎖階層取齊指定表鎖後執行 fn(單一取鎖函數): 表名去重、依 targets → pemis → grups → users 排序(階層外之表名排最後、彼此依字典序), 由外而內巢狀取 kmx(tabWriteKey(表));
|
|
120
|
+
* 各路徑取鎖次序一致故不成環.
|
|
121
|
+
* 分段取鎖: 先取被引用表之鎖、依該表之新舊資料判定須改寫引用端後, 才在內層以 held 指明已持之表再取引用端表之鎖(寫入引用端表不涉改名/刪除者免取, 免無謂之互等);
|
|
122
|
+
* held 非空時, keyTables 之階層須全數高於 held(否則拋錯, 即違反階層, 不嘗試取鎖).
|
|
123
|
+
*
|
|
124
|
+
* @param {Function} kmx 輸入 pmKeyMutex 實例
|
|
125
|
+
* @param {Array} keyTables 輸入須持鎖之表名陣列
|
|
126
|
+
* @param {Function} fn 輸入持鎖後執行之 async 函數
|
|
127
|
+
* @param {Array} [held=[]] 輸入呼叫端已持有鎖之表名陣列(內層分段取鎖用)
|
|
128
|
+
* @returns {Promise} 回傳 fn 之結果
|
|
129
|
+
*/
|
|
130
|
+
function withTabLocks(kmx, keyTables, fn, held = []) {
|
|
131
|
+
let rank = (t) => (Object.prototype.hasOwnProperty.call(TAB_LOCK_RANK, t) ? TAB_LOCK_RANK[t] : Infinity)
|
|
132
|
+
let ts = [...new Set(keyTables)].sort((a, b) => (rank(a) - rank(b)) || (a < b ? -1 : (a > b ? 1 : 0)))
|
|
133
|
+
if (held.length > 0) {
|
|
134
|
+
let maxHeld = Math.max(...held.map(rank))
|
|
135
|
+
if (ts.some((t) => rank(t) <= maxHeld)) {
|
|
136
|
+
throw new Error('tab lock order violation')
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
let run = fn
|
|
140
|
+
for (let k = ts.length - 1; k >= 0; k--) {
|
|
141
|
+
let inner = run
|
|
142
|
+
let key = tabWriteKey(ts[k])
|
|
143
|
+
run = () => kmx(key, inner)
|
|
144
|
+
}
|
|
145
|
+
return run()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 使用者對應鍵(mappingBy)所指之 users 欄位: 'email'/'name' 回該欄位名, 其餘(含預設 'id')回 null(不另檢查: id 為主鍵, 既有 idOtherFrom 已涵蓋跨 from)
|
|
151
|
+
*/
|
|
152
|
+
function mapKeyOf(mappingBy) {
|
|
153
|
+
return (mappingBy === 'email' || mappingBy === 'name') ? mappingBy : null
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 外部同步 users 之對應鍵批內檢查(F-14): mappingBy 為 'email'/'name' 時, 每個 isActive==='y' 之列, 該欄位須為非空字串(trim 後長度 > 0)且批內唯一(精確 ===);
|
|
159
|
+
* isActive 非 'y' 者不參與(getTokenUser 只匹配 active). 先掃全批之空值再查重複(次序決定性).
|
|
160
|
+
*
|
|
161
|
+
* @param {Array} rows 本批 users 列(已 ltdtmapping)
|
|
162
|
+
* @param {String} mappingBy 'id'|'email'|'name'
|
|
163
|
+
* @returns {Object|null} null 為通過; { reason: 'mapKeyInvalid'|'mapKeyDuplicate', k } 為第一個違規列
|
|
164
|
+
*/
|
|
165
|
+
function checkMapKeyBatch(rows, mappingBy) {
|
|
166
|
+
let key = mapKeyOf(mappingBy)
|
|
167
|
+
if (key === null || !Array.isArray(rows)) {
|
|
168
|
+
return null
|
|
169
|
+
}
|
|
170
|
+
let isActive = (r) => r !== null && typeof r === 'object' && r.isActive === 'y'
|
|
171
|
+
let kInvalid = rows.findIndex((r) => isActive(r) && !(isStr(r[key]) && r[key].trim().length > 0))
|
|
172
|
+
if (kInvalid >= 0) {
|
|
173
|
+
return { reason: 'mapKeyInvalid', k: kInvalid }
|
|
174
|
+
}
|
|
175
|
+
let kDup = findDupIndex(rows, key, isActive)
|
|
176
|
+
if (kDup >= 0) {
|
|
177
|
+
return { reason: 'mapKeyDuplicate', k: kDup }
|
|
178
|
+
}
|
|
179
|
+
return null
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 後台整表寫入 users 之 email/對應鍵檢查(R-PRINCIPLE-00 適用表 h, 規則帳本 R-MAPKEY-03; 遭遇衝突就收嚴, 但歷史資料不得使所有存檔失敗, 只擋「本次造成或觸及」者):
|
|
185
|
+
*
|
|
186
|
+
* ① 一般 email(任何 mappingBy, 全表含停用列): 須為非空字串(僅檢查「新增列」與「email 值與庫內同 id 列不同的列」, 歷史既有之空/非字串原樣送回不拒),
|
|
187
|
+
* 且不得與他列重複(判準 findTouchedDup, 歷史既有重複原樣送回不拒).
|
|
188
|
+
* ② 對應鍵(mappingBy 為 'email'/'name' 時, 與外部同步 F-14 同口徑): 兩側先過濾成 isActive==='y' 之列再比;
|
|
189
|
+
* 該欄位須為非空字串(trim 後長度 > 0; 僅檢查「新增列」「該欄位值與庫內同 id 之 active 列不同的列」與「由非 active 改為 active 的列」),
|
|
190
|
+
* 且 active 列間不得重複(findTouchedDup; 一列由 inactive 改 active 而撞值亦屬本次造成). inactive 列不參與.
|
|
191
|
+
* mappingBy 為 'id'(預設)不做②, 故 name 不檢查; mappingBy 為 'email' 時 ① 已涵蓋 active 列之 email, ② 另補「停用列改為啟用」之情形.
|
|
192
|
+
*
|
|
193
|
+
* 錯誤鍵沿用 saveRowFieldInvalid(空/非字串)與 saveRowFieldDuplicate(重複); reason 供 srLog 區分來源(不含欄位值: email/name 為個資).
|
|
194
|
+
* 次序: 先 ① 後 ②; 同一階段內空值與重複取列索引較小者(與修正前 ckKey 逐列掃描之次序一致).
|
|
195
|
+
*
|
|
196
|
+
* @param {Array} rowsNew 送入之整表(已 ltdtmapping、id 數字轉字串)
|
|
197
|
+
* @param {Array} rowsOld 庫內現有全部列(select 結果)
|
|
198
|
+
* @param {String} mappingBy 'id'|'email'|'name'(其他值視同 'id')
|
|
199
|
+
* @returns {Object|null} null 為通過; 否則 { key: 'saveRowFieldInvalid'|'saveRowFieldDuplicate', reason, field, k }(k 為 rowsNew 之列索引)
|
|
200
|
+
*/
|
|
201
|
+
function checkUsersWrite(rowsNew, rowsOld, mappingBy) {
|
|
202
|
+
if (!Array.isArray(rowsNew)) {
|
|
203
|
+
return null
|
|
204
|
+
}
|
|
205
|
+
let isObj = (r) => r !== null && typeof r === 'object'
|
|
206
|
+
let isEstr = (v) => isStr(v) && v.length > 0
|
|
207
|
+
let isFilled = (v) => isStr(v) && v.trim().length > 0
|
|
208
|
+
let isActive = (r) => isObj(r) && r.isActive === 'y'
|
|
209
|
+
let listOld = Array.isArray(rowsOld) ? rowsOld.filter(isObj) : []
|
|
210
|
+
|
|
211
|
+
//findBad: listNew 中第一個「本次造成或觸及」且 key 值不合 isOk 之索引, 無則 -1; 新增列(庫內無同 id)與 key 值與庫內同 id 列不同者為造成或觸及
|
|
212
|
+
let findBad = (listNew, list, key, isOk) => {
|
|
213
|
+
let byId = new Map()
|
|
214
|
+
for (let r of list) {
|
|
215
|
+
if (isStr(r.id)) {
|
|
216
|
+
byId.set(r.id, r)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return listNew.findIndex((r) => {
|
|
220
|
+
let v = isObj(r) ? r[key] : undefined
|
|
221
|
+
let o = (isObj(r) && isStr(r.id)) ? byId.get(r.id) : undefined
|
|
222
|
+
let touched = o === undefined || v !== o[key]
|
|
223
|
+
return touched && !isOk(v)
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
//judge: 一個階段(列集合, 欄位, 空值判準, reason 前綴)之結果; idxMap 為過濾後索引對應回 rowsNew 之原索引
|
|
228
|
+
let judge = (listNew, idxMap, list, key, isOk, tag) => {
|
|
229
|
+
let kBad = findBad(listNew, list, key, isOk)
|
|
230
|
+
let kDup = findTouchedDup(listNew, list, key)
|
|
231
|
+
if (kBad < 0 && kDup < 0) {
|
|
232
|
+
return null
|
|
233
|
+
}
|
|
234
|
+
let isBad = kBad >= 0 && (kDup < 0 || kBad <= kDup) //同一列既空又被當重複(如新增第二個空字串 email)時以「空值」為主(與修正前 ckKey 先判空再判重複一致)
|
|
235
|
+
return {
|
|
236
|
+
key: isBad ? 'saveRowFieldInvalid' : 'saveRowFieldDuplicate',
|
|
237
|
+
reason: isBad ? `${tag}Invalid` : `${tag}Duplicate`,
|
|
238
|
+
field: key,
|
|
239
|
+
k: idxMap[isBad ? kBad : kDup],
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
//① 一般 email
|
|
244
|
+
let r1 = judge(rowsNew, rowsNew.map((v, k) => k), listOld, 'email', isEstr, 'email')
|
|
245
|
+
if (r1 !== null) {
|
|
246
|
+
return r1
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
//② 對應鍵(僅 active 列)
|
|
250
|
+
let keyMap = mapKeyOf(mappingBy)
|
|
251
|
+
if (keyMap === null) {
|
|
252
|
+
return null
|
|
253
|
+
}
|
|
254
|
+
let idxAct = []
|
|
255
|
+
let rowsNewAct = []
|
|
256
|
+
rowsNew.forEach((r, k) => {
|
|
257
|
+
if (isActive(r)) {
|
|
258
|
+
idxAct.push(k)
|
|
259
|
+
rowsNewAct.push(r)
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
return judge(rowsNewAct, idxAct, listOld.filter(isActive), keyMap, isFilled, 'mapKey')
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* 登入查找之結果判定(F-14): 以對應鍵查 perm users(isActive='y')命中 0 筆/1 筆/多筆; 多筆不取首筆(user 為 null, 由呼叫端依 count 拒絕)
|
|
268
|
+
*
|
|
269
|
+
* @param {Array} rs select 結果
|
|
270
|
+
* @returns {Object} { user, count }: count 為物件列之筆數; 僅 count===1 時 user 為該列
|
|
271
|
+
*/
|
|
272
|
+
function pickUniqueUser(rs) {
|
|
273
|
+
let rows = Array.isArray(rs) ? rs.filter((r) => iseobj(r)) : []
|
|
274
|
+
return {
|
|
275
|
+
user: rows.length === 1 ? rows[0] : null,
|
|
276
|
+
count: rows.length,
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
export { findDupIndex, hasDupKey, findKeyOtherFrom, findTouchedDup, tabWriteKey, withTabLocks, mapKeyOf, checkMapKeyBatch, checkUsersWrite, pickUniqueUser }
|
package/server/procCore.mjs
CHANGED
|
@@ -13,6 +13,11 @@ import getOwn from '../src/js/getOwn.mjs'
|
|
|
13
13
|
import ds from '../src/schema/index.mjs'
|
|
14
14
|
import procLang from './procLang.mjs'
|
|
15
15
|
import validateRowsBeforeWrite from './validateRows.mjs'
|
|
16
|
+
import planRefChanges, { REF_OF, diffRefTargets, fillIdlessIds } from '../src/js/refCascade.mjs'
|
|
17
|
+
import { restoreExternalOrder, dropServerOnlyDiff, restoreServerFields, checkExternalWrite, hasExternalDiff } from '../src/js/externalRows.mjs'
|
|
18
|
+
import { isBaselineValid, checkBaseline } from '../src/js/rowDigest.mjs'
|
|
19
|
+
import { findTouchedDup, findDupIndex, withTabLocks, checkUsersWrite } from './keyCheck.mjs'
|
|
20
|
+
import { checkCascadePlan, writeWithCascade } from './cascadeWrite.mjs'
|
|
16
21
|
|
|
17
22
|
|
|
18
23
|
//isRowObj: 待寫入之列須為至少有一個自有可列舉鍵之物件(非 null、非陣列; 不用依 toString 判型之 iseobj, 免被 Symbol.toStringTag 偽稱)
|
|
@@ -58,7 +63,17 @@ function mergePickKeysOnly(ltdtDb, rowsIn, keyDetect, pickKeys) {
|
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
|
|
61
|
-
|
|
66
|
+
//mappingBy: 使用者對應鍵('id'|'email'|'name', WWebPerm 已正規化; 未給或其他值視同 'id'), 供 users 整表寫入的對應鍵檢查(R-PRINCIPLE-00 適用表 h, R-MAPKEY-03)
|
|
67
|
+
//cascadeRename: 群組/權限改名時是否連帶更新引用(預設 true; 只管改名, 刪除連帶清理不受影響, R-REF-CASCADE-01)
|
|
68
|
+
//lockExternalRows: 後台是否不得改外部列(from 為非空字串者)之身分/結構欄(預設 true, R-EXT-OWN-01; 'n' 時回到舊行為, 僅保留 from 型別檢查)
|
|
69
|
+
function proc(woItems, procOrm, { srLog, kmx, lockSave, mappingBy = 'id', cascadeRename = true, lockExternalRows = true }) {
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
//pickClientOpt: 用戶端(kpFunExt 第 3 參)只採納 baseline, 不接受 pickKeysOnly 等伺服端設定; 未給(undefined/null)視為舊客戶端
|
|
73
|
+
let pickClientOpt = (opt) => {
|
|
74
|
+
let b = (opt !== null && typeof opt === 'object') ? opt.baseline : undefined
|
|
75
|
+
return (b === undefined || b === null) ? {} : { baseline: b }
|
|
76
|
+
}
|
|
62
77
|
|
|
63
78
|
|
|
64
79
|
//updateTabItems
|
|
@@ -74,6 +89,13 @@ function proc(woItems, procOrm, { srLog, kmx, lockSave }) {
|
|
|
74
89
|
return Promise.reject('saveRowFieldInvalid')
|
|
75
90
|
}
|
|
76
91
|
|
|
92
|
+
//baseline: 草稿所依據之載入基準 [[id, digest], ...](F-26, R-BASELINE-01; 前端於開啟編輯時綁定); 未給(舊客戶端)為 null; 給了但格式不合視為無效請求, 於任何讀寫之前拒絕
|
|
93
|
+
let baseline = get(opt, 'baseline', null)
|
|
94
|
+
if (baseline !== null && !isBaselineValid(baseline)) {
|
|
95
|
+
srLog.error({ event: 'updateTabItems-validate-error', keyTable, msg: 'saveRowFieldInvalid', reason: 'baselineInvalid', row: -1, field: '' })
|
|
96
|
+
return Promise.reject('saveRowFieldInvalid')
|
|
97
|
+
}
|
|
98
|
+
|
|
77
99
|
//pickKeysOnly, 僅採納既有列之指定欄位變更(見 mergePickKeysOnly), 未給則整表 diff 寫入
|
|
78
100
|
let pickKeysOnly = get(opt, 'pickKeysOnly', null)
|
|
79
101
|
|
|
@@ -91,10 +113,16 @@ function proc(woItems, procOrm, { srLog, kmx, lockSave }) {
|
|
|
91
113
|
//雙擊防護(後端): 同一操作者之同表寫入處理中再送出即拒絕 'saveInProgress'(不排隊, 見 lockSave.mjs, ADR-025);
|
|
92
114
|
//占位包在 kmx 之外層, 不同操作者之並行寫入仍由 kmx 序列化、各自成功(ADR-012)
|
|
93
115
|
//序列化同表之並行整表批次寫入: 防 lost update (同 keyTable 序列化, 不同表並行)
|
|
94
|
-
|
|
116
|
+
//持鎖: 先取本表之鎖; 本表有改名或刪除而須改寫引用端時, 才於內層依階層再取引用端表之鎖(見下方「引用連帶」段). 皆經單一取鎖函數(R-WRITE-LOCK-02); 與外部同步(WWebPerm.syncAndReplaceTabs)共用同表互斥鍵(外審 sol R10)
|
|
117
|
+
return await lockSave(`updateTabItems:${keyTable}`, userId, () => withTabLocks(kmx, [keyTable], async () => {
|
|
95
118
|
|
|
96
119
|
//pickKeysOnly, 須於 mutex 內讀取資料庫為基底, 避免與並行寫入 race
|
|
97
120
|
if (isearr(pickKeysOnly)) {
|
|
121
|
+
//有限數字 id 先轉為字串形式再與庫內對應(外審 sol R7): mergePickKeysOnly 只認非空字串 id, 數字 id(如 1)對不上庫內之 '1' 而該列指派被靜默略過卻回成功(R-ORM-04 之同一正規化, 此處原晚於合併)
|
|
122
|
+
rows = map(rows, (r) => {
|
|
123
|
+
let id = get(r, 'id')
|
|
124
|
+
return (typeof id === 'number' && Number.isFinite(id)) ? { ...r, id: String(id) } : r
|
|
125
|
+
})
|
|
98
126
|
let ltdtDb = await woItems[keyTable].select()
|
|
99
127
|
rows = mergePickKeysOnly(ltdtDb, rows, keyDetect, pickKeysOnly)
|
|
100
128
|
srLog.info({ event: 'updateTabItems-pickKeysOnly', keyTable, userId, pickKeysOnly })
|
|
@@ -111,7 +139,8 @@ function proc(woItems, procOrm, { srLog, kmx, lockSave }) {
|
|
|
111
139
|
})
|
|
112
140
|
|
|
113
141
|
//ckKey, 回傳 err key (saveRowFieldInvalid / saveRowFieldDuplicate) 或 null; 動態細節 (row index/field/value) 改記 srLog, 不進使用者訊息
|
|
114
|
-
|
|
142
|
+
//opt.skipDup 為 true 時只檢查空值/非字串, 不檢查重複: pemis/grups 之名稱唯一改由下方 findTouchedDup 判定(只拒本次造成或觸及者, F-02, ADR-034)
|
|
143
|
+
let ckKey = (rows, key, opt = {}) => {
|
|
115
144
|
let err = null
|
|
116
145
|
|
|
117
146
|
//check
|
|
@@ -129,7 +158,7 @@ function proc(woItems, procOrm, { srLog, kmx, lockSave }) {
|
|
|
129
158
|
}
|
|
130
159
|
|
|
131
160
|
//check
|
|
132
|
-
if (haskey(kp, value)) {
|
|
161
|
+
if (opt.skipDup !== true && haskey(kp, value)) {
|
|
133
162
|
srLog.error({ event: 'updateTabItems-ckKey-error', keyTable, key, msg: 'saveRowFieldDuplicate', row: k, value })
|
|
134
163
|
err = `saveRowFieldDuplicate`
|
|
135
164
|
return false //跳出
|
|
@@ -152,18 +181,13 @@ function proc(woItems, procOrm, { srLog, kmx, lockSave }) {
|
|
|
152
181
|
return Promise.reject(err)
|
|
153
182
|
}
|
|
154
183
|
}
|
|
155
|
-
if (arrHas(keyTable, ['pemis', 'grups'])) { //users可重複name
|
|
156
|
-
err = ckKey(rows, 'name')
|
|
157
|
-
if (err !== null) {
|
|
158
|
-
return Promise.reject(err)
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
if (arrHas(keyTable, ['users'])) {
|
|
162
|
-
err = ckKey(rows, 'email')
|
|
184
|
+
if (arrHas(keyTable, ['pemis', 'grups'])) { //users可重複name故不列入; 此處只驗空值, 名稱重複見下方 findTouchedDup
|
|
185
|
+
err = ckKey(rows, 'name', { skipDup: true })
|
|
163
186
|
if (err !== null) {
|
|
164
187
|
return Promise.reject(err)
|
|
165
188
|
}
|
|
166
189
|
}
|
|
190
|
+
//users 之 email/對應鍵檢查改至下方 select 之後(需庫內現況判定「本次造成或觸及」, R-PRINCIPLE-00 適用表 h), 不再於此以整表 ckKey('email') 擋(歷史空值/重複會使所有存檔失敗)
|
|
167
191
|
}
|
|
168
192
|
|
|
169
193
|
//主鍵之有限數字轉為其字串形式(規則帳本 R-ORM-04; w-serv-orm 1.1.11 起庫內即如此存放): ltdtDiffByKey 以嚴格比對, 未轉時 123 對不上庫內之 '123',
|
|
@@ -173,9 +197,59 @@ function proc(woItems, procOrm, { srLog, kmx, lockSave }) {
|
|
|
173
197
|
return (typeof id === 'number' && Number.isFinite(id)) ? { ...r, id: String(id) } : r
|
|
174
198
|
})
|
|
175
199
|
|
|
200
|
+
//pemis/grups 以 id 對應既有列(見下方 isNameKeyTable 分支), 送入之整表內主鍵 id(字串)不得重複, 否則 Map(id → 列) 靜默併成一列而遺失其一(unit-procCore-writeValidate WV-008);
|
|
201
|
+
//缺 id 之列(API 直打, 視為新增)不參與; 錯誤鍵同 validateRowsBeforeWrite 之 idDuplicate: saveRowFieldDuplicate
|
|
202
|
+
if (arrHas(keyTable, ['pemis', 'grups'])) {
|
|
203
|
+
let kIdDup = findDupIndex(rows, 'id', (r) => isestr(get(r, 'id'))) //空字串 id 視為未給(待自動產號, validateRows 同), 不參與重複判定(外審 sol R6)
|
|
204
|
+
if (kIdDup >= 0) {
|
|
205
|
+
srLog.error({ event: 'updateTabItems-ckKey-error', keyTable, key: 'id', msg: 'saveRowFieldDuplicate', row: kIdDup, value: get(rows, [kIdDup, 'id']) })
|
|
206
|
+
return Promise.reject('saveRowFieldDuplicate')
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
176
210
|
//ltdtDiffByKey
|
|
177
211
|
let ltdtOld = await woItems[keyTable].select()
|
|
178
212
|
|
|
213
|
+
//表級載入基準比對(R-PRINCIPLE-00 原則 7, 規則帳本 R-BASELINE-01, ADR-037 Update(run11); 使用者 2026-10-22 裁示「多人同時編輯以表雜湊比對, 先動先贏」):
|
|
214
|
+
//有基準者, 草稿載入時之整表須與目前庫內整表相同(任一列內容指紋不同或列被增刪即過期), 否則整次拒收 saveStale、零寫入, 不部分變更、不上鎖; 指紋排除 order 與伺服器管理欄(見 rowDigest.getDigestFields).
|
|
215
|
+
//放在 select 之後、所有檢查與還原之前: 基準過期時下列各檢查之結果(撞名、新增列已存在、外部列守門…)皆以過期之畫面為前提, 應先回「請重新載入」而非誤導之其他訊息.
|
|
216
|
+
//無基準者(舊客戶端)不在此檢查: 後台列維持最後寫入者勝(ADR-013), 外部列之指派欄改動另於下方拒收(saveBaselineRequired). srLog 只記錯誤鍵、原因與差異筆數, 不記列 id/個資值
|
|
217
|
+
if (baseline !== null) {
|
|
218
|
+
let ck = checkBaseline({ keys: ds[keyTable].keys, ltdtOld, baseline })
|
|
219
|
+
if (ck !== null) {
|
|
220
|
+
srLog.error({ event: 'updateTabItems-stale-error', keyTable, userId, msg: ck.key, reason: ck.reason, n: ck.n })
|
|
221
|
+
return Promise.reject(ck.key)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
//外部列之 order 由外部擁有(R-EXT-OWN-01): 上方 order=k+1 重編不得改外部列之順序, 還原為庫內值(使其不因後台存檔而成為 diff)
|
|
226
|
+
if (lockExternalRows) {
|
|
227
|
+
rows = restoreExternalOrder(rows, ltdtOld)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
//pemis/grups 名稱唯一(F-02, 規則帳本 R-NAME-UNIQ-02, ADR-034): 寫入後之名稱集合即送入之整表(含他 from 之外部同步列), 其中出現兩列(不同 id)同名且為「本次造成或觸及」者整次拒收於任何寫入之前;
|
|
231
|
+
//庫內已有之歷史重複(僅外部同步可造成)原樣送回不算, 否則已有重複之庫將無法在後台修復. 比對為精確 ===(同 arrFinder). 放在 select 之後、saveNewRowExists 之前, 使撞名先於「新增列已存在」被回報(與修正前 ckKey 之次序相同)
|
|
232
|
+
let isNameKeyTable = arrHas(keyTable, ['pemis', 'grups'])
|
|
233
|
+
if (isNameKeyTable) {
|
|
234
|
+
let kDup = findTouchedDup(rows, ltdtOld, 'name')
|
|
235
|
+
if (kDup >= 0) {
|
|
236
|
+
srLog.error({ event: 'updateTabItems-ckKey-error', keyTable, key: 'name', msg: 'nameDuplicate', row: kDup, value: get(rows, [kDup, 'name']) })
|
|
237
|
+
return Promise.reject('nameDuplicate')
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
//users 之 email/對應鍵(R-PRINCIPLE-00 適用表 h, 規則帳本 R-MAPKEY-03): 遭遇衝突就收嚴, 但只擋「本次造成或觸及」者, 歷史既有之空值/重複原樣送回不拒(否則已有歷史資料之庫無法在後台修復, 連群組指派 pickKeysOnly 也會失敗);
|
|
242
|
+
//①全表 email 須為非空字串且不重複; ②mappingBy 為 email/name 時另對 isActive==='y' 之列檢查該欄位(含停用改啟用); 判準單一擁有者 keyCheck.checkUsersWrite(與外部同步 F-14 同口徑).
|
|
243
|
+
//放在 select 之後、saveNewRowExists 之前(次序同 pemis/grups 之 findTouchedDup), 於任何破壞性寫入之前(本函式在 kmx(tabWriteKey) 內, R-WRITE-LOCK-01); 被拒時資料表零變更.
|
|
244
|
+
//srLog 只記錯誤鍵、原因、欄位與列索引, 不記 email/name 之值(個資; 修正前重複時記 value)
|
|
245
|
+
if (keyTable === 'users') {
|
|
246
|
+
let ck = checkUsersWrite(rows, ltdtOld, mappingBy)
|
|
247
|
+
if (ck !== null) {
|
|
248
|
+
srLog.error({ event: 'updateTabItems-ckKey-error', keyTable, key: ck.field, msg: ck.key, reason: ck.reason, row: ck.k })
|
|
249
|
+
return Promise.reject(ck.key)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
179
253
|
//新增列已存在即拒絕 (ADR-025): 同一包重送且第 2 次於第 1 次完成後才處理時(占位已釋放), 第 1 次已建立之列會被當「修改」(diff),
|
|
180
254
|
//以前端之佔位字覆寫伺服器於新增時填入之建立者 / 建立時間(procOrm save 只覆寫 userIdUpdate / timeUpdate); 故整包拒絕、不寫入
|
|
181
255
|
if (size(idsNew) > 0) {
|
|
@@ -189,9 +263,44 @@ function proc(woItems, procOrm, { srLog, kmx, lockSave }) {
|
|
|
189
263
|
}
|
|
190
264
|
|
|
191
265
|
let ltdtNew = rows
|
|
192
|
-
let r =
|
|
266
|
+
let r = null
|
|
267
|
+
if (isNameKeyTable || keyTable === 'users') { //users 亦拆出缺 id 之列(外審 sol R5): 同批兩列缺 id 時 ltdtmapping 皆補 id:'', ltdtDiffByKey 以 Map(id → 列) 使前列被後列覆蓋而只建立一列卻回成功
|
|
268
|
+
//pemis/grups 一律以主鍵 id 對應(F-02 完整化, run04 主責補強, ADR-034 Update): 寫入後之名稱集合須等於送入之整表, findTouchedDup 對「送入整表」驗唯一才成立,
|
|
269
|
+
//而以 name 為 diff 鍵時 ltdtDiffByKey 以 Map(name → 列) 對應, 下列單次存檔雖通過檢查卻使庫內與送入不同、留下同名兩列且仍回成功(D1 設計稿 §1.5 以真 procCore+ORM 替身實測,
|
|
270
|
+
//unit-procCore-nameUniq NU-021~024 守護): ①改名 A→A2 並以舊名 A 新增另一列(新 id)——改名靜默遺失; ②刪除 A 並以同名新增(新 id, F-02② 原案)——舊列未刪;
|
|
271
|
+
//③鏈式改名 A→B 且 B→C——id-2 應為 C 卻停在 B; ④庫內已有同名列(歷史重複)時改名其中先出現之一列——新名稱之列走 add 而其 id 已存在, insert 對既有主鍵略過(NU-011 修前實測).
|
|
272
|
+
//改以 id 對應後: 改名成為 diff 而非 del + add(建立者與建立時間沿用庫內, 同名之歷史重複各自獨立); 缺 id 之送入列(API 直打)無從對應既有列, 視為新增.
|
|
273
|
+
let withId = ltdtNew.filter((v) => isestr(get(v, 'id')))
|
|
274
|
+
let noId = ltdtNew.filter((v) => !isestr(get(v, 'id')))
|
|
275
|
+
r = ltdtDiffByKey(ltdtOld, withId, 'id')
|
|
276
|
+
r.add = [...r.add, ...noId]
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
r = ltdtDiffByKey(ltdtOld, ltdtNew, keyDetect)
|
|
280
|
+
}
|
|
193
281
|
// console.log('ltdtDiffByKey r', r)
|
|
194
282
|
|
|
283
|
+
//外部列保護(F-23; R-PRINCIPLE-00 原則 3、5; 規則帳本 R-EXT-OWN-01, ADR-037): 於任何寫入之前(含引用端), 違規整次拒收、資料表零變更, 不靜默忽略;
|
|
284
|
+
//(載入基準之表級比對 checkBaseline 已在上方 select 之後先行, run11: 基準過期時應先回「請重新載入」, 而非誤導之「外部列不可修改」, run07 U1 觀察 1)
|
|
285
|
+
//①外部列僅伺服器管理欄不同者(外部同步重寫使其時間/建立者較新, 持舊快照送回)視為無變更, 免把建立者與建立時間回退為舊值(S2); 內容確有改動(指派欄)之外部列改帶庫內之伺服器管理欄寫入(restoreServerFields);
|
|
286
|
+
//②from 型別與外部列守門(checkExternalWrite): 新增列 from 須為 ''、外部列不可刪/不可改 from 與身分結構欄, 指派欄可寫(草稿新鮮而仍違規者在此被拒);
|
|
287
|
+
//③無基準而修改外部列之指派欄: 無從證明快照新舊, Lv1 放行之指派欄不得因舊快照還原外部已撤銷之授權(外審 B.2), 依原則 3 拒收(saveBaselineRequired); 後台列無基準維持舊行為(最後寫入者勝, ADR-013).
|
|
288
|
+
//srLog 只記錯誤鍵、原因、筆數與來源名稱, 不記列 id/個資值
|
|
289
|
+
let keysTab = ds[keyTable].keys
|
|
290
|
+
if (lockExternalRows) {
|
|
291
|
+
r = dropServerOnlyDiff({ keys: keysTab, r, ltdtOld })
|
|
292
|
+
r = restoreServerFields({ r, ltdtOld }) //改動之外部列帶庫內之伺服器管理欄: 持舊草稿(保留未存編輯期間外部同步重寫了該列)不得把較新之建立者/建立時間回退(外審 sol 第 3 點)
|
|
293
|
+
}
|
|
294
|
+
let ex =checkExternalWrite({ keyTable, keys: keysTab, r, ltdtOld, lockExternalRows })
|
|
295
|
+
if (ex !== null) {
|
|
296
|
+
srLog.error({ event: 'updateTabItems-external-error', keyTable, userId, msg: ex.key, reason: ex.reason, n: ex.n, froms: ex.froms })
|
|
297
|
+
return Promise.reject(ex.key)
|
|
298
|
+
}
|
|
299
|
+
if (baseline === null && lockExternalRows && hasExternalDiff({ r, ltdtOld })) {
|
|
300
|
+
srLog.error({ event: 'updateTabItems-stale-error', keyTable, userId, msg: 'saveBaselineRequired', reason: 'externalAssignNoBaseline' })
|
|
301
|
+
return Promise.reject('saveBaselineRequired')
|
|
302
|
+
}
|
|
303
|
+
|
|
195
304
|
//寫入前驗證(規則帳本 R-ORM-01, ADR-026): 於第一個破壞步驟(del)之前擋下可預判之寫入失敗, 被拒時資料表零變更.
|
|
196
305
|
//why: 寫入順序為 del → insert → save, 而 w-serv-orm 1.1.8 起對「會被 clearXSS 改寫之主鍵」整批拒絕、LMDB 對 > 1978 B 之主鍵整批拒絕,
|
|
197
306
|
//修正前遇之即「舊列已刪、新列被拒」(改標的 id 時該列遺失, 2026-10-14 V3/V7 實測); 非主鍵欄位之標記則被靜默改寫(存值≠送入值).
|
|
@@ -209,30 +318,59 @@ function proc(woItems, procOrm, { srLog, kmx, lockSave }) {
|
|
|
209
318
|
return Promise.reject(key)
|
|
210
319
|
}
|
|
211
320
|
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
321
|
+
//writeMain
|
|
322
|
+
let writeMain = async () => {
|
|
323
|
+
|
|
324
|
+
//del
|
|
325
|
+
if (size(r.del) > 0) {
|
|
326
|
+
await procOrm(userId, keyTable, 'del', r.del) //須使用procOrm才有辦法自動給予相關欄位
|
|
327
|
+
// .catch((err) => {
|
|
328
|
+
// console.log('woItems[keyTable].del err', err)
|
|
329
|
+
// })
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
//add
|
|
333
|
+
if (size(r.add) > 0) {
|
|
334
|
+
await procOrm(userId, keyTable, 'insert', r.add) //須使用procOrm才有辦法自動給予相關欄位
|
|
335
|
+
// .catch((err) => {
|
|
336
|
+
// console.log('woItems[keyTable].insert err', err)
|
|
337
|
+
// })
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
//diff
|
|
341
|
+
if (size(r.diff) > 0) {
|
|
342
|
+
await procOrm(userId, keyTable, 'save', r.diff) //須使用procOrm才有辦法自動給予相關欄位
|
|
343
|
+
// .catch((err) => {
|
|
344
|
+
// console.log('woItems[keyTable].save err', err)
|
|
345
|
+
// })
|
|
346
|
+
}
|
|
219
347
|
|
|
220
|
-
//add
|
|
221
|
-
if (size(r.add) > 0) {
|
|
222
|
-
await procOrm(userId, keyTable, 'insert', r.add) //須使用procOrm才有辦法自動給予相關欄位
|
|
223
|
-
// .catch((err) => {
|
|
224
|
-
// console.log('woItems[keyTable].insert err', err)
|
|
225
|
-
// })
|
|
226
348
|
}
|
|
227
349
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
350
|
+
//引用連帶(R-PRINCIPLE-00 原則 6、適用表 m/n, 規則帳本 R-REF-CASCADE-01, ADR-036): grups/pemis 改名(同 id 異名)與 grups/pemis/targets 刪除(舊 id 不在送入整表),
|
|
351
|
+
//須於同一次寫入內連帶更新/清理引用它之授權字串(users.cgrups、grups.cpemis、pemis.crules). 是否須改寫引用端只依本表新舊列判定(diffRefTargets), 故於持有本表鎖之下即可決定是否再取引用端表之鎖;
|
|
352
|
+
//取齊鎖之後才讀取引用端、規劃與預檢(新名撞懸空名、歷史同名組改名、容器內相撞、改寫後欄位驗證), 預檢與寫入涵蓋同一版本(外審 A.3), 被拒時資料表零變更.
|
|
353
|
+
//寫入為引用端先、主體後, 失敗時補償還原引用端(cascadeWrite.mjs)
|
|
354
|
+
//未帶 id 之列(API 直打; 寫入時 ORM 補隨機主鍵, 即舊列刪、新列增)改依名稱對應既有列, 同名即同一實體而不清空引用(fillIdlessIds)
|
|
355
|
+
let rowsPlan = arrHas(keyTable, ['targets', 'pemis', 'grups']) ? fillIdlessIds(keyTable, ltdtOld, ltdtNew) : ltdtNew
|
|
356
|
+
let kind = arrHas(keyTable, ['targets', 'pemis', 'grups']) ? diffRefTargets({ keyTable, rowsOld: ltdtOld, rowsNew: rowsPlan, opt: { cascadeRename } }) : null
|
|
357
|
+
if (kind !== null && (kind.renames.length > 0 || kind.delNames.length > 0)) {
|
|
358
|
+
let holder = REF_OF[keyTable].holder
|
|
359
|
+
return await withTabLocks(kmx, [holder], async () => {
|
|
360
|
+
let holdersOld = await woItems[holder].select()
|
|
361
|
+
let plan = planRefChanges({ keyTable, rowsOld: ltdtOld, rowsNew: rowsPlan, holdersOld, opt: { cascadeRename } })
|
|
362
|
+
let errPlan = plan.err !== null ? plan.err : checkCascadePlan(plan)
|
|
363
|
+
if (errPlan !== null) {
|
|
364
|
+
srLog.error({ event: 'updateTabItems-refCascade-error', keyTable, msg: errPlan })
|
|
365
|
+
return Promise.reject(errPlan)
|
|
366
|
+
}
|
|
367
|
+
await writeWithCascade({ plan, holdersOld, woItems, procOrm, userId, srLog, writeMain })
|
|
368
|
+
return ltdtNew
|
|
369
|
+
}, [keyTable])
|
|
234
370
|
}
|
|
235
371
|
|
|
372
|
+
await writeMain()
|
|
373
|
+
|
|
236
374
|
return ltdtNew
|
|
237
375
|
|
|
238
376
|
}))
|
|
@@ -252,9 +390,9 @@ function proc(woItems, procOrm, { srLog, kmx, lockSave }) {
|
|
|
252
390
|
|
|
253
391
|
|
|
254
392
|
//updateTargets
|
|
255
|
-
let updateTargets = async (userId, rows) => {
|
|
393
|
+
let updateTargets = async (userId, rows, opt = {}) => {
|
|
256
394
|
try {
|
|
257
|
-
await updateTabItems('targets', userId, rows, 'id')
|
|
395
|
+
await updateTabItems('targets', userId, rows, 'id', pickClientOpt(opt))
|
|
258
396
|
}
|
|
259
397
|
catch (err) { throw errLog('updateTargets-error', userId, err) }
|
|
260
398
|
srLog.info({ event: 'updateTargets-success', userId })
|
|
@@ -263,9 +401,9 @@ function proc(woItems, procOrm, { srLog, kmx, lockSave }) {
|
|
|
263
401
|
|
|
264
402
|
|
|
265
403
|
//updatePemis
|
|
266
|
-
let updatePemis = async (userId, rows) => {
|
|
404
|
+
let updatePemis = async (userId, rows, opt = {}) => {
|
|
267
405
|
try {
|
|
268
|
-
await updateTabItems('pemis', userId, rows, 'name')
|
|
406
|
+
await updateTabItems('pemis', userId, rows, 'name', pickClientOpt(opt))
|
|
269
407
|
}
|
|
270
408
|
catch (err) { throw errLog('updatePemis-error', userId, err) }
|
|
271
409
|
srLog.info({ event: 'updatePemis-success', userId })
|
|
@@ -274,9 +412,9 @@ function proc(woItems, procOrm, { srLog, kmx, lockSave }) {
|
|
|
274
412
|
|
|
275
413
|
|
|
276
414
|
//updateGrups
|
|
277
|
-
let updateGrups = async (userId, rows) => {
|
|
415
|
+
let updateGrups = async (userId, rows, opt = {}) => {
|
|
278
416
|
try {
|
|
279
|
-
await updateTabItems('grups', userId, rows, 'name')
|
|
417
|
+
await updateTabItems('grups', userId, rows, 'name', pickClientOpt(opt))
|
|
280
418
|
}
|
|
281
419
|
catch (err) { throw errLog('updateGrups-error', userId, err) }
|
|
282
420
|
srLog.info({ event: 'updateGrups-success', userId })
|
|
@@ -284,7 +422,8 @@ function proc(woItems, procOrm, { srLog, kmx, lockSave }) {
|
|
|
284
422
|
}
|
|
285
423
|
|
|
286
424
|
|
|
287
|
-
//updateUsers, opt.pickKeysOnly 給 ['cgrups'] 時僅採納既有使用者之群組指派變更(modeEditUsers='for:grups')
|
|
425
|
+
//updateUsers, opt.pickKeysOnly 給 ['cgrups'] 時僅採納既有使用者之群組指派變更(modeEditUsers='for:grups'; 伺服端設定, WWebPerm 組入, 不接受用戶端給);
|
|
426
|
+
//opt.baseline 為草稿載入基準(用戶端給, 其餘 opt 鍵不採納, 見 pickClientOpt 與 WWebPerm 之 kpFunExt)
|
|
288
427
|
let updateUsers = async (userId, rows, opt = {}) => {
|
|
289
428
|
try {
|
|
290
429
|
await updateTabItems('users', userId, rows, 'id', opt)
|