w-web-perm 1.1.13 → 1.1.15
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 +98 -7
- package/dist/css/{app.f09c7433.css → app.f24c5e50.css} +1 -1
- package/dist/index.tmp +1 -1
- package/dist/js/app-legacy.307bef4f.js +1 -0
- package/dist/js/app.307bef4f.js +1 -0
- package/dist/js/{chunk-vendors.ece5a361.js → chunk-vendors-legacy.6cc4d11e.js} +1 -1
- package/dist/js/{chunk-vendors-legacy.ece5a361.js → chunk-vendors.6cc4d11e.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 +15 -7
- package/docs/global.html +11733 -5078
- package/docs/index.html +2 -2
- package/docs/server_WWebPerm.mjs.html +154 -345
- package/docs/server_adminCheck.mjs.html +92 -0
- package/docs/server_cascadeWrite.mjs.html +5 -26
- package/docs/server_dataChannel.mjs.html +4 -4
- package/docs/server_keyCheck.mjs.html +150 -23
- package/docs/server_lockSave.mjs.html +2 -2
- package/docs/server_maskLog.mjs.html +2 -2
- package/docs/server_procCore.mjs.html +261 -65
- package/docs/server_staLogs_resolveShellWorker.mjs.html +2 -2
- package/docs/server_syncCore.mjs.html +533 -0
- package/docs/server_syncKeep.mjs.html +27 -20
- package/docs/server_tools_authRefsAudit.mjs.html +30 -10
- package/docs/server_tools_authRefsClean.mjs.html +2 -2
- package/docs/server_tools_fromTools.mjs.html +2 -2
- package/docs/server_tools_isMain.mjs.html +2 -2
- package/docs/server_tools_nameDupReport.mjs.html +40 -7
- package/docs/server_validateRows.mjs.html +2 -2
- package/docs/server_wrapInjected.mjs.html +2 -2
- package/docs/src_fetchJson.mjs.html +2 -2
- package/docs/src_getPermUserInfor.mjs.html +175 -0
- package/docs/src_js_applySaved.mjs.html +128 -0
- package/docs/src_js_cellEditing.mjs.html +163 -0
- package/docs/src_js_cvKpToArr.mjs.html +2 -2
- package/docs/src_js_cvKpToObj.mjs.html +2 -2
- package/docs/src_js_draftSync.mjs.html +83 -7
- package/docs/src_js_eachOwn.mjs.html +2 -2
- package/docs/src_js_editorRefRows.mjs.html +2 -2
- package/docs/src_js_externalRows.mjs.html +34 -10
- package/docs/src_js_externalUi.mjs.html +4 -8
- package/docs/src_js_findRefRows.mjs.html +2 -2
- package/docs/src_js_findTouchedDup.mjs.html +2 -2
- package/docs/src_js_fmtLossyMsg.mjs.html +2 -2
- package/docs/src_js_fromAudit.mjs.html +2 -2
- package/docs/src_js_getAddImpact.mjs.html +2 -2
- package/docs/src_js_getAuthWarn.mjs.html +2 -2
- package/docs/src_js_getCascadePreview.mjs.html +2 -2
- package/docs/src_js_getDelRefCount.mjs.html +2 -2
- package/docs/src_js_getEffMode.mjs.html +2 -2
- package/docs/src_js_getKpLossy.mjs.html +2 -2
- package/docs/src_js_getNameErrs.mjs.html +2 -2
- package/docs/src_js_getOwn.mjs.html +2 -2
- package/docs/src_js_getPropKey.mjs.html +2 -2
- package/docs/src_js_getRuleEnable.mjs.html +2 -2
- package/docs/src_js_isAuthMode.mjs.html +2 -2
- 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 +2 -2
- package/docs/src_js_refAudit.mjs.html +2 -2
- package/docs/src_js_refCascade.mjs.html +2 -2
- package/docs/src_js_refIdentity.mjs.html +500 -0
- package/docs/src_js_relDialogSend.mjs.html +2 -2
- package/docs/src_js_rowDigest.mjs.html +75 -145
- package/docs/src_plugins_dataReloadGuard.mjs.html +2 -2
- package/docs/src_plugins_reuseDeletedIds.mjs.html +2 -2
- package/docs/src_plugins_submitGuard.mjs.html +2 -2
- package/g_provideGrups.mjs +1 -1
- package/g_providePemis.mjs +1 -1
- package/g_provideTargets.mjs +1 -1
- package/g_provideUsers.mjs +1 -1
- package/package.json +2 -2
- package/server/WWebPerm.mjs +152 -343
- package/server/adminCheck.mjs +20 -0
- package/server/cascadeWrite.mjs +3 -24
- package/server/dataChannel.mjs +2 -2
- package/server/keyCheck.mjs +148 -21
- package/server/procCore.mjs +259 -63
- package/server/procLang.mjs +78 -4
- package/server/provideTabs.mjs +6 -4
- package/server/syncCore.mjs +461 -0
- package/server/syncKeep.mjs +25 -18
- package/server/tools/authRefsAudit.mjs +28 -8
- package/server/tools/nameDupReport.mjs +38 -5
- package/src/App.vue +6 -1
- package/src/components/LayoutContentGrups.vue +57 -8
- package/src/components/LayoutContentPemis.vue +57 -8
- package/src/components/LayoutContentTargets.vue +38 -7
- package/src/components/LayoutContentUsers.vue +57 -8
- package/src/components/LayoutState.vue +12 -0
- package/src/components/VeGrupBlngUsers.vue +128 -20
- package/src/components/VePemiBlngGrups.vue +128 -20
- package/src/getPermUserInfor.mjs +10 -0
- package/src/js/applySaved.mjs +56 -0
- package/src/js/cellEditing.mjs +91 -0
- package/src/js/draftSync.mjs +81 -5
- package/src/js/externalRows.mjs +32 -8
- package/src/js/externalUi.mjs +2 -6
- package/src/js/refIdentity.mjs +428 -0
- package/src/js/rowDigest.mjs +73 -143
- package/src/plugins/mUI.mjs +5 -0
- package/src/schema/contracts/grups.mjs +8 -0
- package/src/schema/contracts/index.mjs +17 -0
- package/src/schema/contracts/pemis.mjs +8 -0
- package/src/schema/contracts/targets.mjs +8 -0
- package/src/schema/contracts/users.mjs +13 -0
- package/src/schema/tables/grups.mjs +4 -0
- package/src/schema/tables/pemis.mjs +4 -0
- package/src/schema/tables/targets.mjs +4 -0
- package/src/schema/tables/users.mjs +4 -0
- package/src/store/mutations.mjs +1 -1
- package/srv.mjs +43 -0
- package/dist/js/app-legacy.8af87052.js +0 -1
- package/dist/js/app.8af87052.js +0 -1
package/src/js/externalUi.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isExternalRow, getLockFields } from './externalRows.mjs'
|
|
1
|
+
import { isExternalRow, getLockFields, normC } from './externalRows.mjs'
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
//清單頁(標的/權限/群組/使用者)之外部列(from 為非空字串者)前端輔助純函數(F-23; R-PRINCIPLE-00 原則 3、5; 規則帳本 R-EXT-OWN-01; ADR-037): 不依賴 Vue/DOM/語系檔,
|
|
@@ -9,10 +9,6 @@ let isStr = (v) => typeof v === 'string'
|
|
|
9
9
|
let isObj = (v) => v !== null && typeof v === 'object'
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
//normV: 比對用正規化, 同 externalRows 之 checkExternalWrite(缺欄與 '' 視為同一; 其餘以 JSON 字串比對)
|
|
13
|
-
let normV = (v) => JSON.stringify(v === undefined ? '' : v)
|
|
14
|
-
|
|
15
|
-
|
|
16
12
|
/**
|
|
17
13
|
* 把範本之 {key} 代入(單趟掃描: 代入之值不再解析, 名稱可含 {n}、{from} 等字樣原樣顯示; 未給之佔位符原樣保留)
|
|
18
14
|
*
|
|
@@ -143,7 +139,7 @@ function getExternalEdits({ keyTable, keys, rowsNew, rowsOld }) {
|
|
|
143
139
|
return
|
|
144
140
|
}
|
|
145
141
|
let r = newById.get(o.id)
|
|
146
|
-
if (r === undefined || fields.some((f) =>
|
|
142
|
+
if (r === undefined || fields.some((f) => normC(r[f], f) !== normC(o[f], f))) {
|
|
147
143
|
n++
|
|
148
144
|
if (froms.indexOf(o.from) < 0 && froms.length < 5) {
|
|
149
145
|
froms.push(o.from)
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import { REF_OF, keyOf, entName, decodeKp, listEntries } from './refCascade.mjs'
|
|
2
|
+
import { checkSubject } from './draftSync.mjs'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
//正反向關聯之「被引用實體身分」檢查(perm-run02 範圍一; 使用者 2026-10-23 裁示「正反關聯與主體身份檢查, 應該是前後端都要做」; R-PRINCIPLE-00 原則 4、6; 規則帳本 R-SUBJECT-ID-01、R-REF-IDENTITY-01; ADR-037 Update(perm-run02)):
|
|
6
|
+
//授權以**名稱**引用他表實體(users.cgrups→grups.name、grups.cpemis→pemis.name、pemis.crules→targets.id), 被引用實體於使用者編輯期間被刪除後以同名重建(id 不同), 存檔時引用會靜默指向新實體(被同名新資料接管).
|
|
7
|
+
//本檔為純函數(不依賴 Vue/DOM/ORM), 前端(送出前自檢、組宣告)與伺服端(updateTabItems 於引用表鎖內以新鮮讀取驗證)共用同一份判準, 單一擁有者:
|
|
8
|
+
// ①反向對話框之主體: opt.subject = { id, name, saved }(開框時之主體摘要), 判準即 draftSync.checkSubject(伺服端以引用表之新鮮讀取呼叫);
|
|
9
|
+
// ②清單頁指派欄之正向引用: opt.refs = [{ name, ids }](本次新增之啟用引用項, 其名稱在使用者**選取當時**對應之實體 id 陣列; 空陣列=選取當時查無該名稱), 伺服端於引用表鎖內重算名稱對應之 id 陣列, 須逐名稱相等.
|
|
10
|
+
//「新增之啟用引用項」=列之授權字串內 isActive 為 'y' 的項目, 其名稱不在該列舊值之啟用項內(停用項目不授權, 不入; 未變更之舊引用不入: 它們是已持久之關係, 刪除時引用連帶已清理).
|
|
11
|
+
//「移除之啟用引用項」=舊值啟用項中不再啟用者(被移除或被停用; perm-run03 範圍一); 整列被刪除不算(刪的是引用端之列, 未對任何被引用實體做操作).
|
|
12
|
+
//perm-run03 範圍一(使用者 2026-10-23 裁示「這本來就該強制必帶吧? 要不然怎麼確認與驗證調用者身份?」): 宣告強制必帶——凡存檔改變了引用(新增或移除啟用之引用項, 新增列所帶之引用亦算), 每個涉及之名稱都須有身分宣告,
|
|
13
|
+
//缺宣告或不完整整次拒收(saveIdentityRequired/saveRefChanged); 不改變引用之存檔不需宣告. 原「未帶者維持名稱引用原行為」之相容已取消(不支援舊呼叫方式, 升版前開著之舊頁面須重新載入); 帶了但格式不合或不符一律拒收(不降級為不帶, 原則 1、3).
|
|
14
|
+
//targets 之 id 即名稱(主鍵=標的名稱, 外部同步每次以同 id 重寫列), 故其身分定義即 id: 檢查涵蓋「已被刪除」, 刪除後同 id 重建依定義是同一實體(見 R-REF-IDENTITY-01 規則 5).
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
let hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k)
|
|
18
|
+
let isStr = (v) => typeof v === 'string'
|
|
19
|
+
let isPlain = (v) => v !== null && typeof v === 'object' && !Array.isArray(v)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
//MAX_DECL/MAX_IDS: 宣告筆數與每名稱之 id 數上限(防濫用; 正常使用遠小於此)
|
|
23
|
+
let MAX_DECL = 5000
|
|
24
|
+
let MAX_IDS = 1000
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
//REF_TARGET_OF: 引用端表 → { table: 被引用表, field: 授權欄, kind }(REF_OF 之反向; users→grups、grups→pemis、pemis→targets; targets 無)
|
|
28
|
+
let REF_TARGET_OF = Object.create(null)
|
|
29
|
+
Object.keys(REF_OF).forEach((t) => {
|
|
30
|
+
REF_TARGET_OF[REF_OF[t].holder] = { table: t, field: REF_OF[t].field, kind: REF_OF[t].kind }
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 取引用端表之被引用表資訊
|
|
36
|
+
*
|
|
37
|
+
* @param {String} keyTable 輸入引用端表名(users/grups/pemis)
|
|
38
|
+
* @returns {Object|null} 回傳 { table, field, kind }; 該表無授權引用欄(如 targets)或表名不識回 null
|
|
39
|
+
*/
|
|
40
|
+
function getRefTarget(keyTable) {
|
|
41
|
+
return (isStr(keyTable) && hasOwn(REF_TARGET_OF, keyTable)) ? REF_TARGET_OF[keyTable] : null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
//toId: 列之 id 取字串形式(字串原樣; 有限數字取其字串, 同伺服端寫入前之主鍵正規化), 否則 null
|
|
46
|
+
let toId = (r) => {
|
|
47
|
+
let id = isPlain(r) ? r.id : undefined
|
|
48
|
+
if (isStr(id)) {
|
|
49
|
+
return id
|
|
50
|
+
}
|
|
51
|
+
return (typeof id === 'number' && Number.isFinite(id)) ? String(id) : null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 授權字串內「啟用中」之引用名稱集合(isActive 為 'y' 者; 壞 JSON、空字串、非容器皆不計; cgrups/cpemis('ref')之非字串名稱不計, crules('rule')之名稱依屬性鍵轉型〔getPropKey, 如數字 5 → '5'〕計入, 與伺服端 mergeRules 之比對鍵口徑一致)
|
|
57
|
+
*
|
|
58
|
+
* @param {String} str 輸入授權字串(JSON5)
|
|
59
|
+
* @param {String} kind 輸入引用種類, 'ref'(cgrups/cpemis)或 'rule'(crules)
|
|
60
|
+
* @returns {Set} 回傳名稱(比對鍵)集合
|
|
61
|
+
*/
|
|
62
|
+
function activeNames(str, kind) {
|
|
63
|
+
let kp = decodeKp(str)
|
|
64
|
+
let out = new Set()
|
|
65
|
+
if (kp === null) {
|
|
66
|
+
return out
|
|
67
|
+
}
|
|
68
|
+
listEntries(kp, kind).forEach((e) => {
|
|
69
|
+
if (e.key !== null && e.isActive === 'y') {
|
|
70
|
+
out.add(e.key)
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
return out
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 一列相對舊值「新增之啟用引用名稱」(新值啟用項 − 舊值啟用項); 舊值為 null(新增列)時為新值之全部啟用項
|
|
79
|
+
*
|
|
80
|
+
* @param {String} keyTable 輸入引用端表名
|
|
81
|
+
* @param {Object|null} rowOld 輸入舊列(null 為新增)
|
|
82
|
+
* @param {Object} rowNew 輸入新列
|
|
83
|
+
* @returns {Array} 回傳名稱陣列(依新值內之次序、去重)
|
|
84
|
+
*/
|
|
85
|
+
function newActiveNames(keyTable, rowOld, rowNew) {
|
|
86
|
+
let t = getRefTarget(keyTable)
|
|
87
|
+
if (t === null || !isPlain(rowNew)) {
|
|
88
|
+
return []
|
|
89
|
+
}
|
|
90
|
+
let was = isPlain(rowOld) ? activeNames(rowOld[t.field], t.kind) : new Set()
|
|
91
|
+
let out = []
|
|
92
|
+
activeNames(rowNew[t.field], t.kind).forEach((n) => {
|
|
93
|
+
if (!was.has(n)) {
|
|
94
|
+
out.push(n)
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
return out
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 一列相對舊值「移除之啟用引用名稱」(舊值啟用項 − 新值啟用項: 項目被移除、被停用〔isActive 不再為 'y'〕皆算); 舊值為 null(新增列)時為空
|
|
103
|
+
*
|
|
104
|
+
* perm-run03 範圍一: 移除引用亦須身分宣告(把「移除」套到被同名新實體替換之位置, 同樣是把操作套到錯的實體上, 原則 6; 使用者 2026-10-23 裁示)。
|
|
105
|
+
* 整列被刪除不在此列(刪除的是引用端之列本身, 其身分由列 id 與表級基準把關, 未對任何被引用實體做操作)。
|
|
106
|
+
*
|
|
107
|
+
* @param {String} keyTable 輸入引用端表名
|
|
108
|
+
* @param {Object|null} rowOld 輸入舊列(null 為新增)
|
|
109
|
+
* @param {Object} rowNew 輸入新列
|
|
110
|
+
* @returns {Array} 回傳名稱陣列(依舊值內之次序、去重)
|
|
111
|
+
*/
|
|
112
|
+
function removedActiveNames(keyTable, rowOld, rowNew) {
|
|
113
|
+
let t = getRefTarget(keyTable)
|
|
114
|
+
if (t === null || !isPlain(rowOld) || !isPlain(rowNew)) {
|
|
115
|
+
return []
|
|
116
|
+
}
|
|
117
|
+
let now = activeNames(rowNew[t.field], t.kind)
|
|
118
|
+
let out = []
|
|
119
|
+
activeNames(rowOld[t.field], t.kind).forEach((n) => {
|
|
120
|
+
if (!now.has(n)) {
|
|
121
|
+
out.push(n)
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
return out
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 多列相對舊表(以 id 對應)新增之啟用引用名稱集合
|
|
130
|
+
*
|
|
131
|
+
* @param {Object} p 輸入參數物件
|
|
132
|
+
* @param {String} p.keyTable 輸入引用端表名
|
|
133
|
+
* @param {Array} p.rowsNew 輸入新列陣列(整表或僅改動列皆可)
|
|
134
|
+
* @param {Array} p.rowsOld 輸入舊表列陣列(同一版本: 前端為草稿載入時之列, 伺服端為鎖內新鮮讀取)
|
|
135
|
+
* @returns {Set} 回傳名稱集合
|
|
136
|
+
*/
|
|
137
|
+
function collectNewNames({ keyTable, rowsNew, rowsOld }) {
|
|
138
|
+
let byId = new Map()
|
|
139
|
+
;(Array.isArray(rowsOld) ? rowsOld : []).forEach((r) => {
|
|
140
|
+
let id = toId(r)
|
|
141
|
+
if (id !== null) {
|
|
142
|
+
byId.set(id, r)
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
let out = new Set()
|
|
146
|
+
;(Array.isArray(rowsNew) ? rowsNew : []).forEach((r) => {
|
|
147
|
+
let id = toId(r)
|
|
148
|
+
let old = (id !== null && byId.has(id)) ? byId.get(id) : null
|
|
149
|
+
newActiveNames(keyTable, old, r).forEach((n) => out.add(n))
|
|
150
|
+
})
|
|
151
|
+
return out
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 多列相對舊表(以 id 對應)「改變了引用」之啟用引用名稱集合: 新增之啟用引用名稱(含新增列所帶者) ∪ 移除之啟用引用名稱(perm-run03 範圍一: 新增與移除皆須宣告)
|
|
157
|
+
*
|
|
158
|
+
* @param {Object} p 輸入參數物件
|
|
159
|
+
* @param {String} p.keyTable 輸入引用端表名
|
|
160
|
+
* @param {Array} p.rowsNew 輸入新列陣列(整表或僅改動列皆可; 不在其中之舊列〔被刪除或未改動〕不計)
|
|
161
|
+
* @param {Array} p.rowsOld 輸入舊表列陣列(同一版本: 前端為草稿載入時之列, 伺服端為鎖內新鮮讀取)
|
|
162
|
+
* @returns {Set} 回傳名稱集合
|
|
163
|
+
*/
|
|
164
|
+
function collectChangedNames({ keyTable, rowsNew, rowsOld }) {
|
|
165
|
+
let byId = new Map()
|
|
166
|
+
;(Array.isArray(rowsOld) ? rowsOld : []).forEach((r) => {
|
|
167
|
+
let id = toId(r)
|
|
168
|
+
if (id !== null) {
|
|
169
|
+
byId.set(id, r)
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
let out = new Set()
|
|
173
|
+
;(Array.isArray(rowsNew) ? rowsNew : []).forEach((r) => {
|
|
174
|
+
let id = toId(r)
|
|
175
|
+
let old = (id !== null && byId.has(id)) ? byId.get(id) : null
|
|
176
|
+
newActiveNames(keyTable, old, r).forEach((n) => out.add(n))
|
|
177
|
+
removedActiveNames(keyTable, old, r).forEach((n) => out.add(n))
|
|
178
|
+
})
|
|
179
|
+
return out
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 被引用表內與名稱對應之實體 id 陣列(排序、去重; 空陣列=查無; 歷史同名多列則多個)
|
|
185
|
+
*
|
|
186
|
+
* @param {Array} refRows 輸入被引用表列
|
|
187
|
+
* @param {String} refTable 輸入被引用表名(grups/pemis/targets)
|
|
188
|
+
* @param {String} name 輸入引用名稱(比對鍵)
|
|
189
|
+
* @returns {Array} 回傳 id 字串陣列
|
|
190
|
+
*/
|
|
191
|
+
function idsOfName(refRows, refTable, name) {
|
|
192
|
+
let s = new Set()
|
|
193
|
+
;(Array.isArray(refRows) ? refRows : []).forEach((r) => {
|
|
194
|
+
let n = entName(r, refTable)
|
|
195
|
+
let id = toId(r)
|
|
196
|
+
if (n !== null && id !== null && keyOf(n, refTable === 'targets' ? 'rule' : 'ref') === name) {
|
|
197
|
+
s.add(id)
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
return [...s].sort()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* 以被引用表目前之列建「名稱 → id 陣列」快照(使用者開啟編輯器當下呼叫; 之後由 recordPicks 取用)
|
|
206
|
+
*
|
|
207
|
+
* @param {String} keyTable 輸入引用端表名
|
|
208
|
+
* @param {Array} refRows 輸入被引用表列(前端 store 的 grups/pemis/targets)
|
|
209
|
+
* @returns {Map} 回傳 Map(名稱 → id 陣列)
|
|
210
|
+
*/
|
|
211
|
+
function snapRefIds(keyTable, refRows) {
|
|
212
|
+
let t = getRefTarget(keyTable)
|
|
213
|
+
let m = new Map()
|
|
214
|
+
if (t === null) {
|
|
215
|
+
return m
|
|
216
|
+
}
|
|
217
|
+
;(Array.isArray(refRows) ? refRows : []).forEach((r) => {
|
|
218
|
+
let n = entName(r, t.table)
|
|
219
|
+
if (n === null) {
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
let k = keyOf(n, t.kind)
|
|
223
|
+
if (k !== null && !m.has(k)) {
|
|
224
|
+
m.set(k, idsOfName(refRows, t.table, k))
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
return m
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* 編輯器回填授權字串時, 記下本次「新增之啟用引用名稱」在編輯器開啟當下對應之 id(就地更新 picks)
|
|
233
|
+
*
|
|
234
|
+
* 查無者記空陣列(編輯器開啟當下被引用表無此名稱: 懸空引用, 伺服端須仍查無)。
|
|
235
|
+
*
|
|
236
|
+
* 選取記錄以名稱為鍵(跨列共用)。同一名稱先後被選取(不同列, 或同列取消後重選)而當時對應之 id 陣列不同, 代表被引用實體在兩次選取之間被替換過——
|
|
237
|
+
* 兩次選取不可能同時與「現在」一致(至少一次已過期), 故該名稱記為衝突(值 null), 組宣告時令宣告無效而前端自檢不放行(收嚴; 外審 sol#1: 不同列之同名選取互相覆蓋快照, 使先前選取之授權被同名新實體接管)。
|
|
238
|
+
* 衝突之名稱在草稿重建(放棄未存編輯)前不會自行解除; 對應 id 陣列相同之重複選取不是衝突。
|
|
239
|
+
*
|
|
240
|
+
* @param {Object} p 輸入參數物件
|
|
241
|
+
* @param {String} p.keyTable 輸入引用端表名
|
|
242
|
+
* @param {String} p.strOld 輸入該列回填前之授權字串
|
|
243
|
+
* @param {String} p.strNew 輸入編輯器回傳之授權字串
|
|
244
|
+
* @param {Map} p.snap 輸入 snapRefIds 於編輯器開啟當下之結果
|
|
245
|
+
* @param {Map} p.picks 輸入累積之選取記錄(就地更新; 值為 id 陣列, 衝突為 null)
|
|
246
|
+
* @returns {Map} 回傳 picks
|
|
247
|
+
*/
|
|
248
|
+
function recordPicks({ keyTable, strOld, strNew, snap, picks }) {
|
|
249
|
+
let t = getRefTarget(keyTable)
|
|
250
|
+
if (t === null || !(picks instanceof Map)) {
|
|
251
|
+
return picks
|
|
252
|
+
}
|
|
253
|
+
let was = activeNames(strOld, t.kind)
|
|
254
|
+
let now = activeNames(strNew, t.kind)
|
|
255
|
+
let rec = (n) => {
|
|
256
|
+
let ids = [...((snap instanceof Map && snap.has(n)) ? snap.get(n) : [])].sort()
|
|
257
|
+
if (picks.has(n) && (picks.get(n) === null || !sameIds([...picks.get(n)].sort(), ids))) {
|
|
258
|
+
picks.set(n, null) //衝突: 同名兩次選取當時對應不同實體
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
picks.set(n, ids)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
now.forEach((n) => {
|
|
265
|
+
if (!was.has(n)) {
|
|
266
|
+
rec(n) //新增之啟用引用項
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
was.forEach((n) => {
|
|
270
|
+
if (!now.has(n)) {
|
|
271
|
+
rec(n) //移除(或停用)之啟用引用項(perm-run03 範圍一: 移除亦須宣告, 身分以編輯器開啟當下被移除之名稱所對應之實體為準; 懸空引用〔當時查無該名稱〕記空陣列)
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
return picks
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* 組存檔時送伺服端之引用身分宣告 opt.refs: 本次新增之啟用引用名稱, 各附選取當時對應之 id 陣列
|
|
280
|
+
*
|
|
281
|
+
* 選取記錄(picks)沒有的名稱(複製列、貼上等未經編輯器回填的路徑)退回以送出當下之被引用表為準(只涵蓋送出到寫入之窗口)。
|
|
282
|
+
* 選取記錄為衝突(null, 見 recordPicks)之名稱, 其 ids 為 null: 此宣告無法通過 parseRefDecl/countRefMismatch, 前端自檢拒收(伺服端亦以格式不合拒收)。
|
|
283
|
+
*
|
|
284
|
+
* @param {Object} p 輸入參數物件
|
|
285
|
+
* @param {String} p.keyTable 輸入引用端表名
|
|
286
|
+
* @param {Array} p.rowsNew 輸入欲送出之整表
|
|
287
|
+
* @param {Array} p.rowsOld 輸入草稿載入時之列(前端 baseRows)
|
|
288
|
+
* @param {Map} p.picks 輸入選取記錄
|
|
289
|
+
* @param {Array} p.refRows 輸入送出當下之被引用表列
|
|
290
|
+
* @returns {Array} 回傳 [{ name, ids }](依名稱排序; 無新增引用為空陣列; 衝突名稱之 ids 為 null)
|
|
291
|
+
*/
|
|
292
|
+
function buildRefDecl({ keyTable, rowsNew, rowsOld, picks, refRows }) {
|
|
293
|
+
let t = getRefTarget(keyTable)
|
|
294
|
+
if (t === null) {
|
|
295
|
+
return []
|
|
296
|
+
}
|
|
297
|
+
let names = [...collectChangedNames({ keyTable, rowsNew, rowsOld })].sort() //新增與移除之啟用引用名稱皆須宣告(perm-run03 範圍一)
|
|
298
|
+
return names.map((name) => {
|
|
299
|
+
if (picks instanceof Map && picks.has(name)) {
|
|
300
|
+
let v = picks.get(name)
|
|
301
|
+
return { name, ids: v === null ? null : [...v] }
|
|
302
|
+
}
|
|
303
|
+
return { name, ids: idsOfName(refRows, t.table, name) }
|
|
304
|
+
})
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* 驗證並正規化主體摘要 opt.subject = { id, name, saved }
|
|
310
|
+
*
|
|
311
|
+
* @param {*} raw 輸入任何值(伺服端來自用戶端 JSON)
|
|
312
|
+
* @returns {Object|null} 回傳 { id, name, saved }; 格式不合回 null
|
|
313
|
+
*/
|
|
314
|
+
function parseSubject(raw) {
|
|
315
|
+
if (!isPlain(raw) || !isStr(raw.id) || raw.id === '' || !isStr(raw.name) || raw.name === '' || typeof raw.saved !== 'boolean') {
|
|
316
|
+
return null
|
|
317
|
+
}
|
|
318
|
+
return { id: raw.id, name: raw.name, saved: raw.saved }
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* 驗證並正規化引用身分宣告 opt.refs = [{ name, ids }]
|
|
324
|
+
*
|
|
325
|
+
* @param {*} raw 輸入任何值(伺服端來自用戶端 JSON)
|
|
326
|
+
* @returns {Array|null} 回傳 [{ name, ids }](ids 排序去重); 格式不合(非陣列、超量、名稱非字串、重複名稱、ids 非字串陣列)回 null
|
|
327
|
+
*/
|
|
328
|
+
function parseRefDecl(raw) {
|
|
329
|
+
if (!Array.isArray(raw) || raw.length > MAX_DECL) {
|
|
330
|
+
return null
|
|
331
|
+
}
|
|
332
|
+
let seen = new Set()
|
|
333
|
+
let out = []
|
|
334
|
+
for (let d of raw) {
|
|
335
|
+
if (!isPlain(d) || !isStr(d.name) || seen.has(d.name) || !Array.isArray(d.ids) || d.ids.length > MAX_IDS || !d.ids.every(isStr)) {
|
|
336
|
+
return null
|
|
337
|
+
}
|
|
338
|
+
seen.add(d.name)
|
|
339
|
+
out.push({ name: d.name, ids: [...new Set(d.ids)].sort() })
|
|
340
|
+
}
|
|
341
|
+
return out
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
let sameIds = (a, b) => a.length === b.length && a.every((v, k) => v === b[k])
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* 宣告之各名稱在被引用表目前對應之 id 陣列是否仍與宣告相同; 回傳不符之名稱數(供前端送出前自檢與伺服端共用)
|
|
350
|
+
*
|
|
351
|
+
* @param {Object} p 輸入參數物件
|
|
352
|
+
* @param {String} p.keyTable 輸入引用端表名
|
|
353
|
+
* @param {Array} p.decl 輸入已正規化之宣告
|
|
354
|
+
* @param {Array} p.refRows 輸入被引用表目前之列(伺服端為鎖內新鮮讀取, 前端為 store)
|
|
355
|
+
* @returns {Number} 回傳不符名稱數(0 為全部相符)
|
|
356
|
+
*/
|
|
357
|
+
function countRefMismatch({ keyTable, decl, refRows }) {
|
|
358
|
+
let t = getRefTarget(keyTable)
|
|
359
|
+
if (!Array.isArray(decl) || t === null) {
|
|
360
|
+
return Array.isArray(decl) ? decl.length : 1 //宣告無效(非陣列)或該表無授權引用欄: 視為不符(收嚴)
|
|
361
|
+
}
|
|
362
|
+
return decl.filter((d) => !sameIds(idsOfName(refRows, t.table, d.name), d.ids)).length
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* 伺服端: 寫入前驗證引用之身分宣告(引用表鎖內, refRows 為新鮮讀取; 改變引用者宣告為強制必帶, perm-run03 範圍一)
|
|
368
|
+
*
|
|
369
|
+
* ⓪本次寫入「改變了引用」(任一列相對庫內舊列新增或移除啟用之引用項, 新增列所帶之引用亦算; collectChangedNames)而請求未附任何身分宣告(opt.refs 與 opt.subject 皆未帶)
|
|
370
|
+
* → saveIdentityRequired(不再「未帶者維持名稱引用原行為」; 使用者 2026-10-23 裁示: 不宣告身分, 無從確認與驗證調用者身分);
|
|
371
|
+
* 不改變引用之存檔(只改名、只改其他欄)不需宣告, 照常通過。
|
|
372
|
+
* ①宣告之每個名稱, 目前對應之 id 陣列須與宣告相同(被刪除、被刪除後同名重建〔id 不同〕、新增了同名實體皆不符);
|
|
373
|
+
* ②宣告須完整: 本次寫入之列改變之啟用引用名稱, 每個都須在宣告內(opt.refs 之各名稱, 或反向對話框 opt.subject 之主體名稱; 不完整即拒, 免用戶端漏報)。
|
|
374
|
+
* 主體(subject)本身之身分由呼叫端以 checkSubjectIdentity 於引用表鎖內先驗(先於基準比對), 此處只認「主體名稱已被宣告」。
|
|
375
|
+
*
|
|
376
|
+
* @param {Object} p 輸入參數物件
|
|
377
|
+
* @param {String} p.keyTable 輸入引用端表名
|
|
378
|
+
* @param {Array|null} p.decl 輸入已正規化之宣告(parseRefDecl); 請求未帶 refs 為 null
|
|
379
|
+
* @param {Object|null} [p.subject=null] 輸入已正規化之主體摘要(parseSubject); 請求未帶 subject 為 null
|
|
380
|
+
* @param {Array} p.rowsNew 輸入本次將寫入之列(新增列+改動列)
|
|
381
|
+
* @param {Array} p.rowsOld 輸入鎖內新鮮讀取之引用端表舊列
|
|
382
|
+
* @param {Array|null} p.refRows 輸入鎖內新鮮讀取之被引用表列(decl 與 subject 皆為 null 時未讀取, 可為 null)
|
|
383
|
+
* @returns {Object|null} 回傳 null 為通過; 否則 { key, reason, n }(key 為語系鍵 saveIdentityRequired/saveRefChanged/saveIdentityFormat; reason 為 identityRequired/refChanged/refIncomplete/refNoTarget)
|
|
384
|
+
*/
|
|
385
|
+
function checkRefIdentity({ keyTable, decl, subject = null, rowsNew, rowsOld, refRows }) {
|
|
386
|
+
let t = getRefTarget(keyTable)
|
|
387
|
+
if (t === null) {
|
|
388
|
+
return { key: 'saveIdentityFormat', reason: 'refNoTarget', n: 0 }
|
|
389
|
+
}
|
|
390
|
+
let changed = collectChangedNames({ keyTable, rowsNew, rowsOld })
|
|
391
|
+
if (decl === null && subject === null) {
|
|
392
|
+
return changed.size > 0 ? { key: 'saveIdentityRequired', reason: 'identityRequired', n: changed.size } : null
|
|
393
|
+
}
|
|
394
|
+
let declList = decl === null ? [] : decl
|
|
395
|
+
let n = countRefMismatch({ keyTable, decl: declList, refRows })
|
|
396
|
+
if (n > 0) {
|
|
397
|
+
return { key: 'saveRefChanged', reason: 'refChanged', n }
|
|
398
|
+
}
|
|
399
|
+
let declared = new Set(declList.map((d) => d.name))
|
|
400
|
+
if (subject !== null) {
|
|
401
|
+
declared.add(subject.name)
|
|
402
|
+
}
|
|
403
|
+
let miss = [...changed].filter((name) => !declared.has(name))
|
|
404
|
+
if (miss.length > 0) {
|
|
405
|
+
return { key: 'saveRefChanged', reason: 'refIncomplete', n: miss.length }
|
|
406
|
+
}
|
|
407
|
+
return null
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* 伺服端: 驗證反向對話框之主體身分(引用表鎖內, refRows 為新鮮讀取; 判準即 draftSync.checkSubject)
|
|
413
|
+
*
|
|
414
|
+
* @param {Object} p 輸入參數物件
|
|
415
|
+
* @param {Object} p.subject 輸入已正規化之主體摘要(parseSubject)
|
|
416
|
+
* @param {Array} p.refRows 輸入主體表(grups/pemis)目前之列
|
|
417
|
+
* @returns {Object|null} 回傳 null 為通過; 否則 { key: 'saveSubjectChanged', reason }(reason 為 gone/renamed/taken/noSubject)
|
|
418
|
+
*/
|
|
419
|
+
function checkSubjectIdentity({ subject, refRows }) {
|
|
420
|
+
let sc = checkSubject({ subject, rows: refRows })
|
|
421
|
+
return sc === 'ok' ? null : { key: 'saveSubjectChanged', reason: sc }
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
export {
|
|
426
|
+
getRefTarget, activeNames, newActiveNames, removedActiveNames, collectNewNames, collectChangedNames, idsOfName, snapRefIds, recordPicks, buildRefDecl,
|
|
427
|
+
parseSubject, parseRefDecl, countRefMismatch, checkRefIdentity, checkSubjectIdentity,
|
|
428
|
+
}
|