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.
Files changed (124) hide show
  1. package/README.md +158 -123
  2. package/dist/css/app.f09c7433.css +1 -0
  3. package/dist/index.tmp +1 -1
  4. package/dist/js/app-legacy.8af87052.js +1 -0
  5. package/dist/js/app.8af87052.js +1 -0
  6. package/dist/js/{chunk-vendors.3e9e3ef1.js → chunk-vendors-legacy.ece5a361.js} +1 -1
  7. package/dist/js/{chunk-vendors-legacy.3e9e3ef1.js → chunk-vendors.ece5a361.js} +1 -1
  8. package/dist/w-web-perm.umd.js +2 -2
  9. package/dist/w-web-perm.umd.js.map +1 -1
  10. package/docs/WWebPerm.html +84 -4
  11. package/docs/global.html +23023 -1125
  12. package/docs/index.html +2 -2
  13. package/docs/server_WWebPerm.mjs.html +232 -32
  14. package/docs/server_cascadeWrite.mjs.html +225 -0
  15. package/docs/server_dataChannel.mjs.html +2 -2
  16. package/docs/server_keyCheck.mjs.html +353 -0
  17. package/docs/server_lockSave.mjs.html +2 -2
  18. package/docs/server_maskLog.mjs.html +2 -2
  19. package/docs/server_procCore.mjs.html +180 -41
  20. package/docs/server_staLogs_resolveShellWorker.mjs.html +2 -2
  21. package/docs/server_syncKeep.mjs.html +311 -0
  22. package/docs/server_tools_authRefsAudit.mjs.html +247 -0
  23. package/docs/server_tools_authRefsClean.mjs.html +335 -0
  24. package/docs/server_tools_fromTools.mjs.html +797 -0
  25. package/docs/server_tools_isMain.mjs.html +103 -0
  26. package/docs/server_tools_nameDupReport.mjs.html +280 -0
  27. package/docs/server_validateRows.mjs.html +2 -2
  28. package/docs/server_wrapInjected.mjs.html +3 -3
  29. package/docs/src_fetchJson.mjs.html +2 -2
  30. package/docs/src_js_cvKpToArr.mjs.html +2 -2
  31. package/docs/src_js_cvKpToObj.mjs.html +44 -33
  32. package/docs/src_js_draftSync.mjs.html +142 -0
  33. package/docs/src_js_eachOwn.mjs.html +3 -3
  34. package/docs/src_js_editorRefRows.mjs.html +356 -0
  35. package/docs/src_js_externalRows.mjs.html +338 -0
  36. package/docs/src_js_externalUi.mjs.html +230 -0
  37. package/docs/src_js_findRefRows.mjs.html +107 -0
  38. package/docs/src_js_findTouchedDup.mjs.html +150 -0
  39. package/docs/src_js_fmtLossyMsg.mjs.html +2 -2
  40. package/docs/src_js_fromAudit.mjs.html +489 -0
  41. package/docs/src_js_getAddImpact.mjs.html +269 -0
  42. package/docs/src_js_getAuthWarn.mjs.html +463 -0
  43. package/docs/src_js_getCascadePreview.mjs.html +258 -0
  44. package/docs/src_js_getDelRefCount.mjs.html +210 -0
  45. package/docs/src_js_getEffMode.mjs.html +92 -0
  46. package/docs/src_js_getKpLossy.mjs.html +47 -52
  47. package/docs/src_js_getNameErrs.mjs.html +266 -0
  48. package/docs/src_js_getOwn.mjs.html +3 -3
  49. package/docs/src_js_getPropKey.mjs.html +109 -0
  50. package/docs/src_js_getRuleEnable.mjs.html +2 -2
  51. package/docs/src_js_isAuthMode.mjs.html +4 -4
  52. package/docs/src_js_joinNames.mjs.html +2 -2
  53. package/docs/src_js_legendFit.mjs.html +2 -2
  54. package/docs/src_js_pairRowsToSnapshot.mjs.html +156 -0
  55. package/docs/src_js_refAudit.mjs.html +359 -0
  56. package/docs/src_js_refCascade.mjs.html +572 -0
  57. package/docs/src_js_relDialogSend.mjs.html +199 -0
  58. package/docs/src_js_rowDigest.mjs.html +265 -0
  59. package/docs/src_plugins_dataReloadGuard.mjs.html +109 -0
  60. package/docs/src_plugins_reuseDeletedIds.mjs.html +2 -2
  61. package/docs/src_plugins_submitGuard.mjs.html +2 -2
  62. package/g_authRefsClean.mjs +79 -0
  63. package/g_fixFromType.mjs +92 -0
  64. package/g_initialData.mjs +3 -3
  65. package/g_purgeFrom.mjs +127 -0
  66. package/package.json +1 -1
  67. package/server/WWebPerm.mjs +230 -30
  68. package/server/cascadeWrite.mjs +153 -0
  69. package/server/keyCheck.mjs +281 -0
  70. package/server/procCore.mjs +178 -39
  71. package/server/procLang.mjs +270 -3
  72. package/server/syncKeep.mjs +239 -0
  73. package/server/tools/authRefsAudit.mjs +175 -0
  74. package/server/tools/authRefsClean.mjs +263 -0
  75. package/server/tools/fromTools.mjs +725 -0
  76. package/server/tools/isMain.mjs +31 -0
  77. package/server/tools/nameDupReport.mjs +208 -0
  78. package/server/wrapInjected.mjs +1 -1
  79. package/src/components/LayoutContentGrups.vue +396 -75
  80. package/src/components/LayoutContentPemis.vue +395 -75
  81. package/src/components/LayoutContentTargets.vue +261 -25
  82. package/src/components/LayoutContentUsers.vue +334 -97
  83. package/src/components/VeCgrups.vue +88 -129
  84. package/src/components/VeCpemis.vue +86 -129
  85. package/src/components/VeCrules.vue +15 -5
  86. package/src/components/VeGrupBlngUsers.vue +202 -89
  87. package/src/components/VePemiBlngGrups.vue +209 -99
  88. package/src/js/cvKpToObj.mjs +42 -31
  89. package/src/js/draftSync.mjs +70 -0
  90. package/src/js/eachOwn.mjs +1 -1
  91. package/src/js/editorRefRows.mjs +284 -0
  92. package/src/js/externalRows.mjs +266 -0
  93. package/src/js/externalUi.mjs +158 -0
  94. package/src/js/findRefRows.mjs +35 -0
  95. package/src/js/findTouchedDup.mjs +78 -0
  96. package/src/js/fromAudit.mjs +417 -0
  97. package/src/js/getAddImpact.mjs +197 -0
  98. package/src/js/getAuthWarn.mjs +391 -0
  99. package/src/js/getCascadePreview.mjs +186 -0
  100. package/src/js/getDelRefCount.mjs +138 -0
  101. package/src/js/getEffMode.mjs +20 -0
  102. package/src/js/getKpLossy.mjs +44 -49
  103. package/src/js/getNameErrs.mjs +194 -0
  104. package/src/js/getOwn.mjs +1 -1
  105. package/src/js/getPropKey.mjs +37 -0
  106. package/src/js/isAuthMode.mjs +2 -2
  107. package/src/js/pairRowsToSnapshot.mjs +84 -0
  108. package/src/js/refAudit.mjs +287 -0
  109. package/src/js/refCascade.mjs +500 -0
  110. package/src/js/relDialogSend.mjs +127 -0
  111. package/src/js/rowDigest.mjs +193 -0
  112. package/src/plugins/dataReloadGuard.mjs +37 -0
  113. package/src/plugins/mShare.mjs +144 -140
  114. package/src/plugins/mUI.mjs +3 -8
  115. package/src/schema/tables/users.mjs +152 -152
  116. package/dist/css/app.6918ec45.css +0 -1
  117. package/dist/js/app-legacy.7cb7e394.js +0 -1
  118. package/dist/js/app.7cb7e394.js +0 -1
  119. package/docs/src_js_getKeyOrderLost.mjs.html +0 -121
  120. package/docs/src_js_getKpWriteLost.mjs.html +0 -100
  121. package/docs/src_js_keepKeyOrder.mjs.html +0 -139
  122. package/src/js/getKeyOrderLost.mjs +0 -49
  123. package/src/js/getKpWriteLost.mjs +0 -28
  124. package/src/js/keepKeyOrder.mjs +0 -67
@@ -0,0 +1,417 @@
1
+ import { isExternalRow } from './externalRows.mjs'
2
+ import { REF_OF, planRefChanges } from './refCascade.mjs'
3
+ import { getDelImpact } from './getDelRefCount.mjs'
4
+
5
+
6
+ //來源欄 from 之稽核、型別正規化與「整來源清除」規劃(run07 升版維運工具之核心; F-23 外部列受保護、規則帳本 R-PRINCIPLE-00 原則 5):
7
+ // 外部列=from 為非空字串者(externalRows.isExternalRow, 單一擁有者, 本檔不另寫判準); from 為 ''、缺欄、非字串(null/數字/布林/物件)者為後台列.
8
+ // 升版後外部列於後台唯讀(不得改、不得刪), 故升版前舊資料之「非空 from」列將成為唯讀; 非字串 from 後台寫入起被拒收.
9
+ //本檔為純函數(不依賴 Vue/DOM/ORM/fs), 供離線維運腳本 g_fixFromType.mjs/g_purgeFrom.mjs(經 server/tools/fromTools.mjs)與單元測試共用;
10
+ //所有函數不改輸入; 輸出之 id 與名稱僅供離線工具列印於 stdout, 不得寫入 srLog(規則帳本: 日誌不記 id/個資).
11
+
12
+
13
+ //TABLE_ORDER: 四表依引用鏈序(被引用者先、引用端後; 同 server/keyCheck.mjs 之 TAB_LOCK_RANK): 標的 → 權限 → 群組 → 使用者
14
+ const TABLE_ORDER = ['targets', 'pemis', 'grups', 'users']
15
+
16
+ //NON_STRING_KINDS: 非字串 from 之型別分類(null/number/boolean/object/array/other)
17
+ const NON_STRING_KINDS = ['null', 'number', 'boolean', 'object', 'array', 'other']
18
+
19
+ //KIND_ORDER: 稽核結果內 from 值分類之固定輸出次序(empty: ''、missing: 缺欄(視同 '')、非字串各型、notObject: 列本身非物件, 最後為非空字串依值分組)
20
+ const KIND_ORDER = ['empty', 'missing', ...NON_STRING_KINDS, 'notObject']
21
+
22
+ //IMPACT_KIND: 清除之表 → getDelImpact 之種類(AND 引用者數之口徑, 同清單頁刪除確認框); targets 之 crules 無 mode、users 無引用端, 不適用
23
+ const IMPACT_KIND = { grups: 'Grup', pemis: 'Pemi' }
24
+
25
+
26
+ let isStr = (v) => typeof v === 'string'
27
+ let hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k)
28
+ let isObj = (v) => v !== null && typeof v === 'object'
29
+ let arr = (v) => (Array.isArray(v) ? v : [])
30
+ let hasId = (r) => isObj(r) && isStr(r.id) && r.id !== '' //可寫回/可刪除之列須有非空字串主鍵(ORM 以字串主鍵定位)
31
+ let strCmp = (a, b) => (a < b ? -1 : (a > b ? 1 : 0)) //依 UTF-16 碼元之確定性比較(不隨 locale)
32
+
33
+
34
+ //fromKind: 列之 from 值分類 { kind, from? }; 非空字串之判準經 isExternalRow(單一擁有者), from 僅 kind 為 'string' 時給
35
+ let fromKind = (row) => {
36
+ if (!isObj(row)) {
37
+ return { kind: 'notObject' }
38
+ }
39
+ if (isExternalRow(row)) {
40
+ return { kind: 'string', from: row.from }
41
+ }
42
+ let v = row.from
43
+ if (v === undefined) {
44
+ return { kind: 'missing' }
45
+ }
46
+ if (v === '') {
47
+ return { kind: 'empty' }
48
+ }
49
+ if (v === null) {
50
+ return { kind: 'null' }
51
+ }
52
+ if (typeof v === 'number') {
53
+ return { kind: 'number' }
54
+ }
55
+ if (typeof v === 'boolean') {
56
+ return { kind: 'boolean' }
57
+ }
58
+ if (Array.isArray(v)) {
59
+ return { kind: 'array' }
60
+ }
61
+ if (typeof v === 'object') {
62
+ return { kind: 'object' }
63
+ }
64
+ return { kind: 'other' }
65
+ }
66
+
67
+
68
+ //newKinds: 非字串各型之計數容器
69
+ let newKinds = () => Object.fromEntries(NON_STRING_KINDS.map((k) => [k, 0]))
70
+
71
+
72
+ /**
73
+ * 稽核四表 from 欄之分佈(純函數, 不讀寫任何資料)
74
+ *
75
+ * from 值分類: ''(empty)、缺欄(missing, 視同 '')、非字串(null/number/boolean/object/array/other, 各記型別)、非空字串(string, 依值分組, 精確 ===, 不 trim、不分大小寫);
76
+ * 列本身非物件者記 notObject. 非空字串者為「升版後將成為外部列(唯讀)」, 列入 external.
77
+ *
78
+ * @param {Object} d 輸入 { users, grups, pemis, targets }(各為列陣列, 缺者視為空)
79
+ * @returns {Object} 回傳 { entries, external, counts }:
80
+ * entries [{ table, kind, n }](kind 為 'string' 者另帶 from 值), 依 TABLE_ORDER 與 KIND_ORDER 輸出, 非空字串依 from 值升冪; 只列 n 大於 0 者
81
+ * external [{ table, from, n }] 升版後將成為外部列(唯讀)之(表, from)與筆數
82
+ * counts { nRows, nEmpty, nMissing, nNonString, nExternal, nNotObject, nSources, byTable: { 表: { nRows, nEmpty, nMissing, nNonString, nExternal, nNotObject, nSources, kinds } } }
83
+ * @example
84
+ *
85
+ * let r = auditFrom({ users: [{ id: 'u1', from: '' }, { id: 'u2', from: 'pd' }, { id: 'u3', from: null }] })
86
+ * console.log(r.external, r.counts.nNonString)
87
+ * // => [ { table: 'users', from: 'pd', n: 1 } ] 1
88
+ *
89
+ */
90
+ function auditFrom(d) {
91
+ let data = isObj(d) ? d : {}
92
+ let entries = []
93
+ let external = []
94
+ let byTable = {}
95
+ let counts = { nRows: 0, nEmpty: 0, nMissing: 0, nNonString: 0, nExternal: 0, nNotObject: 0, nSources: 0, byTable }
96
+
97
+ for (let t of TABLE_ORDER) {
98
+ let rows = arr(data[t])
99
+ let tab = { nRows: rows.length, nEmpty: 0, nMissing: 0, nNonString: 0, nExternal: 0, nNotObject: 0, nSources: 0, kinds: newKinds() }
100
+ let nOf = new Map() //kind → 筆數
101
+ let srcs = new Map() //from 值 → 筆數(Map: 值可為 __proto__ 等任意字串)
102
+ for (let row of rows) {
103
+ let k = fromKind(row)
104
+ if (k.kind === 'string') {
105
+ srcs.set(k.from, (srcs.get(k.from) || 0) + 1)
106
+ }
107
+ else {
108
+ nOf.set(k.kind, (nOf.get(k.kind) || 0) + 1)
109
+ }
110
+ }
111
+ for (let kind of KIND_ORDER) {
112
+ let n = nOf.get(kind) || 0
113
+ if (n === 0) {
114
+ continue
115
+ }
116
+ entries.push({ table: t, kind, n })
117
+ if (kind === 'empty') {
118
+ tab.nEmpty = n
119
+ }
120
+ else if (kind === 'missing') {
121
+ tab.nMissing = n
122
+ }
123
+ else if (kind === 'notObject') {
124
+ tab.nNotObject = n
125
+ }
126
+ else {
127
+ tab.kinds[kind] = n
128
+ tab.nNonString += n
129
+ }
130
+ }
131
+ for (let from of [...srcs.keys()].sort(strCmp)) {
132
+ let n = srcs.get(from)
133
+ entries.push({ table: t, kind: 'string', from, n })
134
+ external.push({ table: t, from, n })
135
+ tab.nExternal += n
136
+ tab.nSources++
137
+ }
138
+ byTable[t] = tab
139
+ for (let key of ['nRows', 'nEmpty', 'nMissing', 'nNonString', 'nExternal', 'nNotObject', 'nSources']) {
140
+ counts[key] += tab[key]
141
+ }
142
+ }
143
+ return { entries, external, counts }
144
+ }
145
+
146
+
147
+ /**
148
+ * 規劃「把非字串 from 正規化為 ''」(g_fixFromType 之核心; 純函數, 不改輸入)
149
+ *
150
+ * 待改列: from 為 null/數字/布林/物件/陣列等非字串者, 改為 ''(後台列; 與缺欄、'' 同為後台列, 故不改變任何列是否為外部列).
151
+ * 缺欄視同 ''(已是後台列), 不改, 只列出其 id; 非空字串為外部列, 不動; 缺非空字串主鍵而無從寫回者列入 noId(不改, 須人工處理).
152
+ *
153
+ * @param {Array} rows 輸入某一表之全部列
154
+ * @returns {Object} 回傳 { changes, missing, noId, counts }:
155
+ * changes [{ id, kind, before, after }] 待改列, after 恆為 ''; before 為原 from 值(原樣, 同輸入之參考)
156
+ * missing 缺 from 欄之列之 id 陣列(只列出, 不改)
157
+ * noId [{ idx, kind }] 非字串 from 而缺非空字串主鍵者(idx 為其於輸入陣列之索引)
158
+ * counts { nRows, nEmpty, nMissing, nNonString, nExternal, nNotObject, nNoId, kinds }
159
+ * @example
160
+ *
161
+ * let r = planFixFromType([{ id: 'a', from: null }, { id: 'b', from: 'pd' }, { id: 'c' }])
162
+ * console.log(r.changes, r.missing)
163
+ * // => [ { id: 'a', kind: 'null', before: null, after: '' } ] [ 'c' ]
164
+ *
165
+ */
166
+ function planFixFromType(rows) {
167
+ let src = arr(rows)
168
+ let res = { changes: [], missing: [], noId: [], counts: { nRows: src.length, nEmpty: 0, nMissing: 0, nNonString: 0, nExternal: 0, nNotObject: 0, nNoId: 0, kinds: newKinds() } }
169
+ let c = res.counts
170
+ src.forEach((row, idx) => {
171
+ let k = fromKind(row)
172
+ if (k.kind === 'empty') {
173
+ c.nEmpty++
174
+ }
175
+ else if (k.kind === 'string') {
176
+ c.nExternal++
177
+ }
178
+ else if (k.kind === 'notObject') {
179
+ c.nNotObject++
180
+ }
181
+ else if (k.kind === 'missing') {
182
+ c.nMissing++
183
+ if (hasId(row)) {
184
+ res.missing.push(row.id)
185
+ }
186
+ }
187
+ else {
188
+ c.nNonString++
189
+ c.kinds[k.kind]++
190
+ if (hasId(row)) {
191
+ res.changes.push({ id: row.id, kind: k.kind, before: row.from, after: '' })
192
+ }
193
+ else {
194
+ c.nNoId++
195
+ res.noId.push({ idx, kind: k.kind })
196
+ }
197
+ }
198
+ })
199
+ return res
200
+ }
201
+
202
+
203
+ //rowName: 列之顯示名稱(僅供離線輸出; targets 之 id 即名稱; users 不輸出名稱——姓名為個資, 同 authRefsAudit 不回顯 email/name 之慣例)
204
+ let rowName = (keyTable, r) => {
205
+ if (keyTable === 'users') {
206
+ return undefined
207
+ }
208
+ if (keyTable === 'targets') {
209
+ return isStr(r.id) ? r.id : ''
210
+ }
211
+ return isStr(r.name) ? r.name : ''
212
+ }
213
+
214
+
215
+ //similarFroms: 與指定 from 不相同但「去前後空白並忽略大小寫後相同」之來源(僅提示, 不納入清除; 防手誤, 如 'PD' 與 'pd'、'pd ' 與 'pd')
216
+ let similarFroms = (rows, from) => {
217
+ let want = from.trim().toLowerCase()
218
+ let m = new Map()
219
+ for (let r of rows) {
220
+ if (isExternalRow(r) && r.from !== from && r.from.trim().toLowerCase() === want) {
221
+ m.set(r.from, (m.get(r.from) || 0) + 1)
222
+ }
223
+ }
224
+ return [...m.keys()].sort(strCmp).map((f) => ({ from: f, n: m.get(f) }))
225
+ }
226
+
227
+
228
+ /**
229
+ * 規劃「整來源清除」: 某表 from 精確等於指定值之全部列, 連同同一次清理引用之計畫(g_purgeFrom 之核心; 純函數, 不改輸入, 不讀寫任何資料)
230
+ *
231
+ * 被清除列 = 該表 `row.from === from`(精確 ===, 不 trim、不忽略大小寫、不模糊); from 須為非空字串, 拒絕 ''、非字串(err 'invalidFrom'); 表名須為 targets/pemis/grups/users(err 'invalidTable').
232
+ * 被清除列缺非空字串主鍵而無從刪除者整體拒絕(err 'purgeRowNoId', 零寫入, 不做部分清除).
233
+ * targets/pemis/grups 之清除另以 refCascade.planRefChanges 規劃「同一次清理引用」(引用端 pemis.crules/grups.cpemis/users.cgrups; rowsNew = 該表扣除被清除列, holdersOld 為引用端全表),
234
+ * 效果同後台刪除該些列(身分以主鍵 id; 歷史同名仍有存續列者不清理); 計畫有 err(如 renameCascadeUnsupported: 引用端含無法安全重新編碼之內容)則整體拒絕、零寫入.
235
+ * users 之清除無引用清理.
236
+ *
237
+ * @param {Object} p 輸入參數物件
238
+ * @param {String} p.keyTable 輸入被清除之表('targets'、'pemis'、'grups' 或 'users')
239
+ * @param {String} p.from 輸入來源名稱(非空字串, 精確比對)
240
+ * @param {Object} p.tables 輸入 { users, grups, pemis, targets }(各為該表全部列; 引用端表須一併給)
241
+ * @returns {Object} 回傳 { err, keyTable, from, holder, field, rows, delIds, nRows, nAdmin, nNoId, changes, delNames, nHolders, nDelHolders, nExternal, impact, similar }:
242
+ * err null 或拒絕原因鍵(invalidTable/invalidFrom/purgeRowNoId/planRefChanges 之 err, 如 renameCascadeUnsupported); 非 null 時 changes 為空(零寫入)
243
+ * rows [{ id, name? }] 被清除列(id 與名稱僅供離線輸出; users 無 name); delIds 為其 id 陣列; nRows 為其筆數; nAdmin 為其中 isAdmin 為 'y' 者(僅 users)
244
+ * changes 引用端改寫 [{ id, field, before, after, nRename, nDel, from }](依 planRefChanges); holder/field 為引用端表與欄位(users 清除為空字串)
245
+ * delNames 須清理之被清除名稱(planRefChanges.delNames); nHolders 為須改寫之引用端列數; nDelHolders 為受刪除影響之列數; nExternal 為其中來源非空者
246
+ * impact { nRefs, nAnd, nWiden }: nRefs 同 nDelHolders; nAnd 為以 AND 引用者(同清單頁刪除確認框 getDelImpact; targets/users 不適用恆 0); nWiden 為歷史同名只刪其一致授權可能變多者
247
+ * similar [{ from, n }] 與指定 from 去空白忽略大小寫後相同而不相同之來源(僅提示, 不清除)
248
+ * @example
249
+ *
250
+ * let r = planPurgeFrom({ keyTable: 'grups', from: 'pd', tables: {
251
+ * users: [{ id: 'u1', cgrups: '{"A":{"mode":"OR","isActive":"y"}}' }],
252
+ * grups: [{ id: 'g1', name: 'A', from: 'pd' }],
253
+ * } })
254
+ * console.log(r.nRows, r.changes[0].after)
255
+ * // => 1 {}
256
+ *
257
+ */
258
+ function planPurgeFrom(p) {
259
+ let keyTable = isObj(p) ? p.keyTable : ''
260
+ let from = isObj(p) ? p.from : undefined
261
+ let plan = {
262
+ err: null,
263
+ keyTable,
264
+ from,
265
+ holder: '',
266
+ field: '',
267
+ rows: [],
268
+ delIds: [],
269
+ nRows: 0,
270
+ nAdmin: 0,
271
+ nNoId: 0,
272
+ changes: [],
273
+ delNames: [],
274
+ nHolders: 0,
275
+ nDelHolders: 0,
276
+ nExternal: 0,
277
+ impact: { nRefs: 0, nAnd: 0, nWiden: 0 },
278
+ similar: [],
279
+ }
280
+ if (!TABLE_ORDER.includes(keyTable)) {
281
+ plan.err = 'invalidTable'
282
+ return plan
283
+ }
284
+ if (!isStr(from) || from === '') {
285
+ plan.err = 'invalidFrom'
286
+ return plan
287
+ }
288
+ let tables = isObj(p.tables) ? p.tables : {}
289
+ let rowsOld = arr(tables[keyTable])
290
+ let isHit = (r) => isObj(r) && r.from === from //精確 ===: from 為非空字串, 故命中者必為外部列
291
+ let hit = rowsOld.filter(isHit)
292
+ plan.similar = similarFroms(rowsOld, from)
293
+ plan.nRows = hit.length
294
+ plan.nNoId = hit.filter((r) => !hasId(r)).length
295
+ plan.rows = hit.filter(hasId).map((r) => {
296
+ let o = { id: r.id }
297
+ let name = rowName(keyTable, r)
298
+ if (name !== undefined) {
299
+ o.name = name
300
+ }
301
+ return o
302
+ })
303
+ plan.delIds = plan.rows.map((r) => r.id)
304
+ if (keyTable === 'users') {
305
+ plan.nAdmin = hit.filter((r) => r.isAdmin === 'y').length
306
+ }
307
+ let ref = hasOwn(REF_OF, keyTable) ? REF_OF[keyTable] : null //users 無引用端
308
+ if (ref !== null) {
309
+ plan.holder = ref.holder
310
+ plan.field = ref.field
311
+ }
312
+ if (plan.nNoId > 0) {
313
+ plan.err = 'purgeRowNoId'
314
+ return plan
315
+ }
316
+ if (hit.length === 0 || ref === null) {
317
+ return plan
318
+ }
319
+
320
+ //引用清理: 與伺服端刪除同一純函數(R-REF-CASCADE-01), rowsNew = 該表扣除被清除列
321
+ let rowsNew = rowsOld.filter((r) => !isHit(r))
322
+ let holders = arr(tables[ref.holder])
323
+ let rp = planRefChanges({ keyTable, rowsOld, rowsNew, holdersOld: holders })
324
+ if (rp.err !== null) {
325
+ plan.err = rp.err
326
+ return plan
327
+ }
328
+ plan.changes = rp.changes
329
+ plan.delNames = rp.delNames
330
+ plan.nHolders = rp.nHolders
331
+ plan.nDelHolders = rp.nDelHolders
332
+ plan.nExternal = rp.nExternal
333
+ plan.impact.nRefs = rp.nDelHolders
334
+ if (hasOwn(IMPACT_KIND, keyTable)) {
335
+ let im = getDelImpact(plan.delIds, rowsOld, holders, IMPACT_KIND[keyTable], rowsNew)
336
+ plan.impact.nAnd = Math.min(im.nAnd, rp.nDelHolders)
337
+ plan.impact.nWiden = im.nWiden
338
+ }
339
+ return plan
340
+ }
341
+
342
+
343
+ /**
344
+ * 套用清除計畫於四表(回傳新之 tables, 不改輸入; 用於 --table all 之逐表模擬與寫後預測): 移除被清除列、引用端欄位換成 after
345
+ *
346
+ * @param {Object} tables 輸入 { users, grups, pemis, targets }
347
+ * @param {Object} plan 輸入 planPurgeFrom 之結果; err 非 null 者原樣回傳(不套用)
348
+ * @returns {Object} 回傳新之 tables(僅被改之表為新陣列, 其餘沿用)
349
+ */
350
+ function applyPurgePlan(tables, plan) {
351
+ let out = { ...tables }
352
+ if (!isObj(plan) || plan.err !== null) {
353
+ return out
354
+ }
355
+ let del = new Set(plan.delIds)
356
+ out[plan.keyTable] = arr(tables[plan.keyTable]).filter((r) => !(isObj(r) && del.has(r.id) && r.from === plan.from))
357
+ if (plan.holder !== '') {
358
+ let m = new Map(plan.changes.map((c) => [c.id, c]))
359
+ out[plan.holder] = arr(tables[plan.holder]).map((r) => {
360
+ let c = isObj(r) ? m.get(r.id) : undefined
361
+ return c === undefined ? r : { ...r, [c.field]: c.after }
362
+ })
363
+ }
364
+ return out
365
+ }
366
+
367
+
368
+ /**
369
+ * 規劃多表整來源清除(--table all): 依引用鏈序 targets → pemis → grups → users 逐表規劃, 每表以「前面各表套用後」之模擬狀態計畫(與逐表真實寫入之狀態一致)
370
+ *
371
+ * 任一表之計畫有 err 則停止並整體拒絕(err 為該鍵, errTable 為該表; plans 含到失敗表為止之計畫). 純函數, 不改輸入.
372
+ *
373
+ * @param {Object} p 輸入參數物件
374
+ * @param {String} p.from 輸入來源名稱(非空字串, 精確比對)
375
+ * @param {Object} p.tables 輸入 { users, grups, pemis, targets }
376
+ * @param {Array} [p.keyTables=TABLE_ORDER] 輸入要清除之表(可為子集; 一律依 TABLE_ORDER 之序處理並去重)
377
+ * @returns {Object} 回傳 { err, errTable, from, plans, tablesAfter, totals }: tablesAfter 為全部套用後之預測狀態(err 非 null 時無意義);
378
+ * totals 為各表加總 { nRows, nHolders, nDelHolders, nAnd, nWiden, nExternal, nAdmin }
379
+ */
380
+ function planPurgeChain(p) {
381
+ let from = isObj(p) ? p.from : undefined
382
+ let tables = isObj(p) && isObj(p.tables) ? p.tables : {}
383
+ let want = isObj(p) && Array.isArray(p.keyTables) ? p.keyTables : TABLE_ORDER
384
+ let res = { err: null, errTable: '', from, plans: [], tablesAfter: { ...tables }, totals: { nRows: 0, nHolders: 0, nDelHolders: 0, nAnd: 0, nWiden: 0, nExternal: 0, nAdmin: 0 } }
385
+ if (!isStr(from) || from === '') {
386
+ res.err = 'invalidFrom'
387
+ return res
388
+ }
389
+ if (want.length === 0 || want.some((t) => !TABLE_ORDER.includes(t))) {
390
+ res.err = 'invalidTable'
391
+ return res
392
+ }
393
+ let cur = tables
394
+ for (let t of TABLE_ORDER.filter((x) => want.includes(x))) {
395
+ let plan = planPurgeFrom({ keyTable: t, from, tables: cur })
396
+ res.plans.push(plan)
397
+ if (plan.err !== null) {
398
+ res.err = plan.err
399
+ res.errTable = t
400
+ return res
401
+ }
402
+ res.totals.nRows += plan.nRows
403
+ res.totals.nHolders += plan.nHolders
404
+ res.totals.nDelHolders += plan.nDelHolders
405
+ res.totals.nAnd += plan.impact.nAnd
406
+ res.totals.nWiden += plan.impact.nWiden
407
+ res.totals.nExternal += plan.nExternal
408
+ res.totals.nAdmin += plan.nAdmin
409
+ cur = applyPurgePlan(cur, plan)
410
+ }
411
+ res.tablesAfter = cur
412
+ return res
413
+ }
414
+
415
+
416
+ export { TABLE_ORDER, NON_STRING_KINDS, KIND_ORDER, fromKind, auditFrom, planFixFromType, planPurgeFrom, applyPurgePlan, planPurgeChain }
417
+ export default auditFrom
@@ -0,0 +1,197 @@
1
+ import { fmtWarn } from './getAuthWarn.mjs'
2
+ import getEffMode from './getEffMode.mjs'
3
+ import { REF_OF, keyOf, entName, decodeKp, listEntries } from './refCascade.mjs'
4
+
5
+
6
+ //清單頁「儲存」前之「新增撞懸空名」確認(F-23/F-26 run07 設計摘要原則 7; 規則帳本 R-PRINCIPLE-00 原則 3「遇衝突或歧義: 收嚴+提示」、明確同意; 純函數, 不依賴 Vue/DOM/ORM):
7
+ //新增(id 不在基準)之群組/權限/標的, 其名稱(群組/權限為 name, 標的為 id)在基準內查無任何同名實體, 而仍有「啟用中之引用」指向該名稱(懸空引用: 使用者 cgrups、群組 cpemis、權限 crules)——
8
+ //新增後該引用即解到此新實體, 引用端(使用者/群組/權限)之授權隨之變多或受其限制(AND). 後台允許新增, 但須於送出前列出影響並由使用者按「是」才送出.
9
+ //引用之解碼、名稱比對鍵、被引用表與引用端之對應皆沿用 refCascade.mjs(REF_OF/decodeKp/listEntries/entName/keyOf, 與伺服端引用連帶同一套口徑);
10
+ //AND 之判定沿用 getEffMode(mode 缺或非 OR 者視為 AND, 與刪除確認框 countAndRefs 一致); 只有群組/權限之引用項有 mode, 標的之 crules 項目無 mode(mergeRules 之規則只取 isActive), 故標的之 nAnd 恆為 0.
11
+
12
+
13
+ //MAX_NAMES: 供顯示之名稱上限(同 getCascadePreview 之 joinList)
14
+ let MAX_NAMES = 5
15
+
16
+
17
+ //TEXT_KEY: 頁面種類 → 語系鍵(無 AND 句, 有 AND 句)
18
+ let TEXT_KEY = {
19
+ Grup: ['confirmAddRefsGrup', 'confirmAddRefsAndGrup'],
20
+ Pemi: ['confirmAddRefsPemi', 'confirmAddRefsAndPemi'],
21
+ Target: ['confirmAddRefsTarget', ''],
22
+ }
23
+
24
+
25
+ let isStr = (v) => typeof v === 'string'
26
+ let isObj = (v) => v !== null && typeof v === 'object'
27
+ let hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k)
28
+
29
+
30
+ //toId: 列之 id 取字串形式(字串原樣; 有限數字取其字串, 同伺服端寫入前之主鍵正規化), 否則 null(視為無 id: 新列)
31
+ let toId = (r) => {
32
+ let id = isObj(r) ? r.id : undefined
33
+ if (isStr(id)) {
34
+ return id
35
+ }
36
+ return (typeof id === 'number' && Number.isFinite(id)) ? String(id) : null
37
+ }
38
+
39
+
40
+ //showName: 名稱之顯示形式, 空字串顯示為 "" (否則文案中看不到, 同 getCascadePreview)
41
+ let showName = (v) => (v === '' ? '""' : String(v))
42
+
43
+
44
+ /**
45
+ * 計算「本次新增且撞懸空名」對引用端之影響
46
+ *
47
+ * 新增列: rowsNew 中 id 不在 rowsOld 者(缺 id 者亦為新增; 有限數字 id 取字串形式比對). 撞懸空名: 新增列之名稱(群組/權限為 name〔缺欄視為 ''〕, 標的為 id)非字串者不計,
48
+ * 其比對鍵(群組/權限為字串本身, 標的為屬性鍵轉型)在 rowsOld 內無任何同名實體(歷史同名、本次一併刪除或改名之舊列仍算有同名實體, 其引用之去留由刪除/改名流程確認);
49
+ * 同一名稱多列新增只計一次. 引用端: holders 中授權字串(cgrups/cpemis/crules)解碼後, 有「isActive 為 'y' 且名稱指向該新名稱」之項目者(停用項目、壞 JSON、空字串、非容器皆不計; 含外部列).
50
+ * nHolders 為受影響之引用端列數(一列引用多個新名稱只計一次); nAnd 為其中至少有一個此類引用項之有效模式為 AND 者(群組/權限; 標的恆為 0); names 為受影響之新名稱(依 rowsNew 次序、去重、至多 5 個), nNames 為其總數.
51
+ *
52
+ * @param {Object} p 輸入參數物件
53
+ * @param {String} p.keyTable 輸入被新增之表, 'grups'(引用端 users.cgrups)、'pemis'(引用端 grups.cpemis)或 'targets'(引用端 pemis.crules); 其他回傳全 0
54
+ * @param {Array} p.rowsOld 輸入基準列(store 之該表列, 已儲存者)
55
+ * @param {Array} p.rowsNew 輸入欲送出之該表整表(清單目前之列; 標的頁為 reuseDeletedIds 正規化後之 rowsSend)
56
+ * @param {Array} p.holders 輸入引用端表全部列(users/grups/pemis, 含外部列)
57
+ * @returns {Object} 回傳 { nHolders, nAnd, names, nNames }
58
+ * @example
59
+ *
60
+ * let r = getAddImpact({
61
+ * keyTable: 'grups',
62
+ * rowsOld: [],
63
+ * rowsNew: [{ id: 'g1', name: 'G1' }],
64
+ * holders: [{ id: 'u1', cgrups: '{"G1":{"mode":"AND","isActive":"y"}}' }],
65
+ * })
66
+ * console.log(r.nHolders, r.nAnd, r.names)
67
+ * // => 1 1 [ 'G1' ]
68
+ *
69
+ */
70
+ function getAddImpact(p) {
71
+ let res = { nHolders: 0, nAnd: 0, names: [], nNames: 0 }
72
+ let p0 = isObj(p) ? p : {}
73
+ let keyTable = p0.keyTable
74
+ if (!isStr(keyTable) || !hasOwn(REF_OF, keyTable)) {
75
+ return res
76
+ }
77
+ let ref = REF_OF[keyTable]
78
+ let rowsOld = Array.isArray(p0.rowsOld) ? p0.rowsOld : []
79
+ let rowsNew = Array.isArray(p0.rowsNew) ? p0.rowsNew : []
80
+ let holders = Array.isArray(p0.holders) ? p0.holders : []
81
+
82
+ //oldIds, keysOld: 基準之字串主鍵集合、基準名稱之比對鍵集合
83
+ let oldIds = new Set()
84
+ let keysOld = new Set()
85
+ rowsOld.forEach((r) => {
86
+ let id = toId(r)
87
+ if (id !== null) {
88
+ oldIds.add(id)
89
+ }
90
+ let k = keyOf(entName(r, keyTable), ref.kind)
91
+ if (k !== null) {
92
+ keysOld.add(k)
93
+ }
94
+ })
95
+
96
+ //kpNew: 比對鍵 → 新名稱(新增列且基準無同名實體; Map 依 rowsNew 次序)
97
+ let kpNew = new Map()
98
+ rowsNew.forEach((r) => {
99
+ if (!isObj(r)) {
100
+ return
101
+ }
102
+ let id = toId(r)
103
+ if (id !== null && oldIds.has(id)) {
104
+ return //既有列(含改名), 非新增
105
+ }
106
+ let name = entName(r, keyTable)
107
+ let k = keyOf(name, ref.kind)
108
+ if (k === null || keysOld.has(k) || kpNew.has(k)) {
109
+ return
110
+ }
111
+ kpNew.set(k, name)
112
+ })
113
+ if (kpNew.size === 0) {
114
+ return res
115
+ }
116
+
117
+ //holders: 解碼各引用端之授權字串, 找啟用中且指向新名稱之項目
118
+ let kpHit = new Set() //受影響之比對鍵
119
+ holders.forEach((h) => {
120
+ if (!isObj(h)) {
121
+ return
122
+ }
123
+ let kp = decodeKp(h[ref.field])
124
+ if (kp === null) {
125
+ return
126
+ }
127
+ let isHit = false
128
+ let isAnd = false
129
+ listEntries(kp, ref.kind).forEach((e) => {
130
+ if (e.key === null || !kpNew.has(e.key) || e.isActive !== 'y') {
131
+ return
132
+ }
133
+ isHit = true
134
+ kpHit.add(e.key)
135
+ if (ref.kind === 'ref') {
136
+ let mode = (e.isObj && hasOwn(e.el, 'mode')) ? e.el.mode : undefined
137
+ if (getEffMode(mode) === 'AND') {
138
+ isAnd = true
139
+ }
140
+ }
141
+ })
142
+ if (isHit) {
143
+ res.nHolders++
144
+ }
145
+ if (isAnd) {
146
+ res.nAnd++
147
+ }
148
+ })
149
+
150
+ //names: 受影響之新名稱(依 rowsNew 次序)
151
+ let names = []
152
+ kpNew.forEach((name, k) => {
153
+ if (kpHit.has(k)) {
154
+ names.push(name)
155
+ }
156
+ })
157
+ res.nNames = names.length
158
+ res.names = names.slice(0, MAX_NAMES)
159
+
160
+ return res
161
+ }
162
+
163
+
164
+ /**
165
+ * 新增撞懸空名之確認框文字: nHolders 為 0 回空字串(不彈框); 有 AND 引用者(群組/權限頁)選含 AND 句之語系鍵
166
+ *
167
+ * 文案依頁面種類選鍵: 'Grup'(群組頁, 引用端為使用者) confirmAddRefsGrup/confirmAddRefsAndGrup; 'Pemi'(權限頁, 引用端為群組) confirmAddRefsPemi/confirmAddRefsAndPemi;
168
+ * 'Target'(標的頁, 引用端為權限) confirmAddRefsTarget(無 AND 句). 佔位符: {list} 為新名稱清單(至多 5 個, 超過附 cascadeListMore 之總數), {n} 為受影響之引用端數, {m} 為其中以 AND 引用者.
169
+ * 單一框、單一問句(「確定新增?」). 種類不明回空字串.
170
+ *
171
+ * @param {Object} impact 輸入 getAddImpact 之結果 { nHolders, nAnd, names, nNames }
172
+ * @param {Function} $t 輸入語系函數(key → 文字)
173
+ * @param {String} kind 輸入頁面種類: 'Grup'、'Pemi' 或 'Target'
174
+ * @returns {String} 回傳確認框文字, 無影響為空字串
175
+ */
176
+ function getAddConfirmText(impact, $t, kind) {
177
+ if (!isStr(kind) || !hasOwn(TEXT_KEY, kind)) {
178
+ return ''
179
+ }
180
+ let n = (isObj(impact) && impact.nHolders > 0) ? impact.nHolders : 0
181
+ if (n === 0) {
182
+ return ''
183
+ }
184
+ let m = (impact.nAnd > 0 && TEXT_KEY[kind][1] !== '') ? Math.min(impact.nAnd, n) : 0
185
+ let key = TEXT_KEY[kind][m > 0 ? 1 : 0]
186
+ let names = Array.isArray(impact.names) ? impact.names : []
187
+ let nNames = impact.nNames > names.length ? impact.nNames : names.length
188
+ let list = names.map(showName).join(', ')
189
+ if (nNames > names.length) {
190
+ list = `${list} ${fmtWarn($t('cascadeListMore'), { n: nNames })}`
191
+ }
192
+ return fmtWarn($t(key), { n, m, list })
193
+ }
194
+
195
+
196
+ export { getAddImpact, getAddConfirmText }
197
+ export default getAddImpact