w-web-perm 1.1.11 → 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 (122) hide show
  1. package/README.md +158 -117
  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 +24747 -832
  12. package/docs/index.html +2 -2
  13. package/docs/server_WWebPerm.mjs.html +245 -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 +15 -4
  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 +125 -0
  31. package/docs/src_js_cvKpToObj.mjs.html +210 -0
  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 +95 -0
  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 +155 -0
  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 +92 -0
  52. package/docs/src_js_joinNames.mjs.html +96 -0
  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 +243 -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 +271 -0
  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/validateRows.mjs +13 -2
  79. package/server/wrapInjected.mjs +1 -1
  80. package/src/components/LayoutContentGrups.vue +399 -77
  81. package/src/components/LayoutContentPemis.vue +400 -78
  82. package/src/components/LayoutContentTargets.vue +261 -25
  83. package/src/components/LayoutContentUsers.vue +336 -98
  84. package/src/components/VeCgrups.vue +119 -115
  85. package/src/components/VeCpemis.vue +117 -115
  86. package/src/components/VeCrules.vue +18 -2
  87. package/src/components/VeGrupBlngUsers.vue +240 -73
  88. package/src/components/VePemiBlngGrups.vue +247 -83
  89. package/src/js/cvKpToArr.mjs +53 -0
  90. package/src/js/cvKpToObj.mjs +138 -0
  91. package/src/js/draftSync.mjs +70 -0
  92. package/src/js/eachOwn.mjs +1 -1
  93. package/src/js/editorRefRows.mjs +284 -0
  94. package/src/js/externalRows.mjs +266 -0
  95. package/src/js/externalUi.mjs +158 -0
  96. package/src/js/findRefRows.mjs +35 -0
  97. package/src/js/findTouchedDup.mjs +78 -0
  98. package/src/js/fmtLossyMsg.mjs +23 -0
  99. package/src/js/fromAudit.mjs +417 -0
  100. package/src/js/getAddImpact.mjs +197 -0
  101. package/src/js/getAuthWarn.mjs +391 -0
  102. package/src/js/getCascadePreview.mjs +186 -0
  103. package/src/js/getDelRefCount.mjs +138 -0
  104. package/src/js/getEffMode.mjs +20 -0
  105. package/src/js/getKpLossy.mjs +83 -0
  106. package/src/js/getNameErrs.mjs +194 -0
  107. package/src/js/getOwn.mjs +1 -1
  108. package/src/js/getPropKey.mjs +37 -0
  109. package/src/js/isAuthMode.mjs +20 -0
  110. package/src/js/joinNames.mjs +24 -0
  111. package/src/js/pairRowsToSnapshot.mjs +84 -0
  112. package/src/js/refAudit.mjs +287 -0
  113. package/src/js/refCascade.mjs +500 -0
  114. package/src/js/relDialogSend.mjs +127 -0
  115. package/src/js/rowDigest.mjs +193 -0
  116. package/src/plugins/dataReloadGuard.mjs +37 -0
  117. package/src/plugins/mShare.mjs +147 -174
  118. package/src/plugins/mUI.mjs +3 -8
  119. package/src/schema/tables/users.mjs +152 -152
  120. package/dist/css/app.6918ec45.css +0 -1
  121. package/dist/js/app-legacy.963e800c.js +0 -1
  122. package/dist/js/app.963e800c.js +0 -1
@@ -146,6 +146,221 @@ let kpLang = {
146
146
  eng: `Duplicate field value in saved data`,
147
147
  cht: `儲存資料的欄位值重複`,
148
148
  },
149
+ nameDuplicate: { //後台整表寫入: 群組/權限之名稱重複(本次造成或觸及者; 歷史既有重複原樣送回不拒), 整次拒存、資料未變更(規則帳本 R-NAME-UNIQ-02, ADR-034)
150
+ eng: `Not saved: the name is duplicated. Group names and permission names must be unique. Nothing was changed.`,
151
+ cht: `未儲存:名稱重複。群組名稱與權限名稱不得重複。資料未變更。`,
152
+ },
153
+ authLossyBlocked: { //編輯器對「物件形式無法無損表示」之授權資料拒存(規則帳本 R-AUTH-LOSSY-01, 主控複審 R4; run04 起「同名同時 OR 與 AND」, 順序部分隨 ADR-033 移除; run05 起另含「啟用而無字串名稱之項目」, R-PRINCIPLE-00 f; run06 起「啟用的項目其合併模式皆缺漏或不合法」一格放寬為可存〔方案 A 原樣寫回, ADR-035 Update 2026-10-22〕, 文字去掉該項); {names} 為受影響之名稱
154
+ eng: `Not saved: the authorization settings for {names} contain entries the editor cannot write without changing access (the same name set to both OR and AND, or an entry without a usable name). Nothing was changed. Please correct the source data first.`,
155
+ cht: `未儲存:{names} 的授權設定含有編輯器無法在不改變授權下寫出的項目(同一名稱同時以 OR 與 AND 設定,或項目沒有可用的名稱)。資料未變更,請先修正資料來源。`,
156
+ },
157
+ //授權資料衝突之頁面提示(規則帳本 R-PRINCIPLE-00 原則 3「遇衝突:收嚴+提示」、R-AUTH-WARN-01): 警告圖示(三角形驚嘆號)之 tooltip; 偵測與代入見 src/js/getAuthWarn.mjs; {names} 為受影響之名稱、{kind} 為被引用者種類(warnKindGrup/warnKindPemi)、{n} 為筆數
158
+ warnKindGrup: {
159
+ eng: `permission group`,
160
+ cht: `權限群組`,
161
+ },
162
+ warnKindPemi: {
163
+ eng: `permission`,
164
+ cht: `權限`,
165
+ },
166
+ warnRefDangling: { //c: 引用之名稱查無實體
167
+ eng: `Not found in the {kind} list: {names}. Each is still counted as an entry with no authorization (an OR entry adds nothing; an AND entry leaves nothing granted). Restore it or remove it.`,
168
+ cht: `在{kind}清單中找不到:{names}。仍視為一個沒有任何授權的項目(OR 項目不增加授權;AND 項目使結果為空)。請還原或移除。`,
169
+ },
170
+ warnRefAmbiguous: { //d: 引用之名稱對到多筆(歷史同名)
171
+ eng: `Matches several {kind}s with the same name: {names}. Only the authorization they all share is used (intersection). Rename or delete the duplicates.`,
172
+ cht: `對到多筆同名的{kind}:{names}。僅採用它們共同擁有的授權(交集)。請將同名項目改名或刪除。`,
173
+ },
174
+ warnRefModeInvalid: { //e: mode 缺或非 OR/AND
175
+ eng: `The merge mode is missing or invalid: {names}. It is treated as AND, but only as a restriction: it never grants access on its own.`,
176
+ cht: `合併模式缺漏或不合法:{names}。系統視為 AND,且僅作為限制,不會單獨授予任何授權。`,
177
+ },
178
+ warnRefBothModes: { //b: 同名同時有 OR 與 AND
179
+ eng: `Set as both OR and AND: {names}. The editor cannot save this without changing access, so saving is refused until the source data is fixed.`,
180
+ cht: `同時以 OR 與 AND 設定:{names}。編輯器無法在不改變授權下儲存,於資料來源修正前將拒絕儲存。`,
181
+ },
182
+ warnRefEmptyName: { //f: 名稱為空字串
183
+ eng: `An entry has an empty name. It is kept as it is and matches only a {kind} whose name is empty.`,
184
+ cht: `有項目的名稱為空字串。照原樣保留,且僅與名稱為空字串的{kind}相符。`,
185
+ },
186
+ warnRefNoName: { //f: 名稱不是字串
187
+ eng: `An entry has no usable name ({names}). The editor cannot keep it, so saving is refused until the source data is fixed.`,
188
+ cht: `有項目沒有可用的名稱({names})。編輯器無法保留,於資料來源修正前將拒絕儲存。`,
189
+ },
190
+ warnDataUnparsable: { //格式無法判讀(JSON5 解析失敗或非物件/陣列): 伺服端視為沒有任何授權(收嚴)
191
+ eng: `The authorization data cannot be parsed (invalid format). It is treated as granting nothing. Correct the source data or set it again and save.`,
192
+ cht: `授權資料的格式無法解析,系統視為沒有任何授權。請修正資料來源,或重新設定後儲存。`,
193
+ },
194
+ warnRuleConflict: { //a: crules 同一管理對象重複列且有勾與沒勾並存
195
+ eng: `Listed more than once, with both checked and unchecked: {names}. It is treated as unchecked (n).`,
196
+ cht: `重複列出且有勾與沒勾並存:{names}。系統視為沒勾選(n)。`,
197
+ },
198
+ warnNameHistDup: { //g: 歷史同名列(非本次編輯造成)
199
+ eng: `This name is shared by {n} rows (existing duplicates, not caused by this edit). Anything that refers to it by name uses only what the rows share. Rename or delete the extra rows.`,
200
+ cht: `此名稱由 {n} 筆資料共用(既有的重複,並非本次編輯造成)。以名稱引用它的授權僅採用各筆共同的部分。請將多餘的資料改名或刪除。`,
201
+ },
202
+ warnDupRowsInconsistent: { //編輯器: 同名之列設定不一致而拒存
203
+ eng: `Not saved: rows with the same name have different settings ({names}). Nothing was changed. Please make them identical, or fix the duplicate names first.`,
204
+ cht: `未儲存:同名的列設定不一致({names})。資料未變更,請先使其一致,或先修正同名資料。`,
205
+ },
206
+ //@@U1-KEYS
207
+ //使用者清單頁: 歷史既有(庫內原樣、非本次編輯造成)之同名/同 email/空值/格式不符不擋存, 僅於該儲存格顯示警告圖示(規則帳本 R-PRINCIPLE-00 g、h); 本次造成者仍擋存並顯示 userNameEmpty/userNameDuplicate/userEmail* 既有文字. {n} 為共用該值之使用者數
208
+ warnUserNameHistDup: {
209
+ eng: `This name is shared by {n} users (existing duplicates, not caused by this edit). This does not block saving. If name is the sign-in key, signing in with it is refused until the duplicates are fixed.`,
210
+ cht: `此名稱由 {n} 位使用者共用(既有的重複,並非本次編輯造成),不影響儲存。若以名稱作為登入對應鍵,在重複修正前該名稱將無法登入。`,
211
+ },
212
+ warnUserEmailHistDup: {
213
+ eng: `This email is shared by {n} users (existing duplicates, not caused by this edit). This does not block saving. If email is the sign-in key, signing in with it is refused until the duplicates are fixed.`,
214
+ cht: `此 Email 由 {n} 位使用者共用(既有的重複,並非本次編輯造成),不影響儲存。若以 Email 作為登入對應鍵,在重複修正前該 Email 將無法登入。`,
215
+ },
216
+ warnUserNameHistEmpty: {
217
+ eng: `This user has no valid name (existing data, not caused by this edit). This does not block saving.`,
218
+ cht: `此使用者尚未給予有效名稱(既有資料,並非本次編輯造成),不影響儲存。`,
219
+ },
220
+ warnUserEmailHistEmpty: {
221
+ eng: `This user has no email (existing data, not caused by this edit). This does not block saving. If email is the sign-in key, this user cannot sign in.`,
222
+ cht: `此使用者尚未給予 Email(既有資料,並非本次編輯造成),不影響儲存。若以 Email 作為登入對應鍵,此使用者將無法登入。`,
223
+ },
224
+ warnUserEmailHistInvalid: {
225
+ eng: `The email format of this user is invalid (existing data, not caused by this edit). This does not block saving.`,
226
+ cht: `此使用者的 Email 格式錯誤(既有資料,並非本次編輯造成),不影響儲存。`,
227
+ },
228
+ //刪除群組/權限前之確認(規則帳本 R-PRINCIPLE-00 c、d、原則 6、適用表 n; ADR-036; src/js/getDelRefCount.mjs:getDelConfirmText): 刪除時引用它之引用端(群組頁為使用者 cgrups、權限頁為群組 cpemis)於同一次寫入內被連帶清理(移除該引用),
229
+ //故不再是「授權被收回」: 其中以 AND 引用者, 引用被移除後其 AND 限制解除, 授權可能變多. 單一框、單一問句, 依下列三個數量組合選鍵:
230
+ // {n} 為將被移除引用之引用端數(任何 mode、含停用項目); {m} 為其中以 AND 引用者(限制解除); {w} 為引用的名稱有多筆同名(歷史同名)而只刪其一(各候選之交集變寬)之引用端數(名稱仍有實體, 不清理, 不計入 {n})
231
+ // confirmDelRefs(n)、confirmDelRefsAnd(n、m)、confirmDelRefsDup(n、w)、confirmDelRefsAndDup(n、m、w); confirmDelDupRefs 僅有 w, 其 {n} 即 w 之數量
232
+ confirmDelRefsGrup: {
233
+ eng: `The reference to the selected group(s) will be removed from {n} user(s). Delete anyway?`,
234
+ cht: `將自 {n} 位使用者移除對所選群組的引用。確定刪除?`,
235
+ },
236
+ confirmDelRefsPemi: {
237
+ eng: `The reference to the selected permission(s) will be removed from {n} group(s). Delete anyway?`,
238
+ cht: `將自 {n} 個群組移除對所選權限的引用。確定刪除?`,
239
+ },
240
+ confirmDelRefsAndGrup: {
241
+ eng: `The reference to the selected group(s) will be removed from {n} user(s). For the {m} user(s) that refer to it with AND, the restriction will be lifted and their authorization may increase. Delete anyway?`,
242
+ cht: `將自 {n} 位使用者移除對所選群組的引用;其中 {m} 位以 AND 引用,移除後其限制解除,授權可能變多。確定刪除?`,
243
+ },
244
+ confirmDelRefsAndPemi: {
245
+ eng: `The reference to the selected permission(s) will be removed from {n} group(s). For the {m} group(s) that refer to it with AND, the restriction will be lifted and their authorization may increase. Delete anyway?`,
246
+ cht: `將自 {n} 個群組移除對所選權限的引用;其中 {m} 個以 AND 引用,移除後其限制解除,授權可能變多。確定刪除?`,
247
+ },
248
+ confirmDelDupRefsGrup: {
249
+ eng: `{n} user(s) refer to a group name that is shared by several groups. If you delete one of them, those users' authorization may increase. Delete anyway?`,
250
+ cht: `有 {n} 位使用者引用的群組名稱有多筆同名資料,刪除其中一筆後,其授權可能變多。確定刪除?`,
251
+ },
252
+ confirmDelDupRefsPemi: {
253
+ eng: `{n} group(s) refer to a permission name that is shared by several permissions. If you delete one of them, the authorization of those groups may increase. Delete anyway?`,
254
+ cht: `有 {n} 個群組引用的權限名稱有多筆同名資料,刪除其中一筆後,其授權可能變多。確定刪除?`,
255
+ },
256
+ confirmDelRefsDupGrup: {
257
+ eng: `The reference to the selected group(s) will be removed from {n} user(s). Also, {w} user(s) refer to a group name that is shared by several groups; if you delete one of them, those users' authorization may increase. Delete anyway?`,
258
+ cht: `將自 {n} 位使用者移除對所選群組的引用;另有 {w} 位使用者引用的群組名稱有多筆同名資料,刪除其中一筆後,其授權可能變多。確定刪除?`,
259
+ },
260
+ confirmDelRefsDupPemi: {
261
+ eng: `The reference to the selected permission(s) will be removed from {n} group(s). Also, {w} group(s) refer to a permission name that is shared by several permissions; if you delete one of them, the authorization of those groups may increase. Delete anyway?`,
262
+ cht: `將自 {n} 個群組移除對所選權限的引用;另有 {w} 個群組引用的權限名稱有多筆同名資料,刪除其中一筆後,其授權可能變多。確定刪除?`,
263
+ },
264
+ confirmDelRefsAndDupGrup: {
265
+ eng: `The reference to the selected group(s) will be removed from {n} user(s). For the {m} user(s) that refer to it with AND, the restriction will be lifted and their authorization may increase. Also, {w} user(s) refer to a group name that is shared by several groups; if you delete one of them, those users' authorization may increase. Delete anyway?`,
266
+ cht: `將自 {n} 位使用者移除對所選群組的引用,其中 {m} 位以 AND 引用,移除後其限制解除,授權可能變多;另有 {w} 位使用者引用的群組名稱有多筆同名資料,刪除其中一筆後,其授權可能變多。確定刪除?`,
267
+ },
268
+ confirmDelRefsAndDupPemi: {
269
+ eng: `The reference to the selected permission(s) will be removed from {n} group(s). For the {m} group(s) that refer to it with AND, the restriction will be lifted and their authorization may increase. Also, {w} group(s) refer to a permission name that is shared by several permissions; if you delete one of them, the authorization of those groups may increase. Delete anyway?`,
270
+ cht: `將自 {n} 個群組移除對所選權限的引用,其中 {m} 個以 AND 引用,移除後其限制解除,授權可能變多;另有 {w} 個群組引用的權限名稱有多筆同名資料,刪除其中一筆後,其授權可能變多。確定刪除?`,
271
+ },
272
+ //儲存前之引用連帶預覽(ADR-036; src/js/getCascadePreview.mjs): 群組/權限改名時, 引用它之引用端(使用者 cgrups/群組 cpemis)於同一次寫入內連帶改名; 標的頁刪除或改 id 時, 引用它之權限 crules 項目被連帶清理(標的 id 即名稱, 改 id 不級聯).
273
+ //{n} 為受影響之引用端數、{list} 為名單(改名為「舊→新」, 至多 5 個, 其餘以 cascadeListMore 帶總數)、{e} 為其中來源為外部應用(from 非空)者
274
+ confirmSaveRenameRefsGrup: {
275
+ eng: `After saving, the group references of {n} user(s) will be updated to the new names of these renamed group(s): {list}. Continue?`,
276
+ cht: `儲存後將一併更新 {n} 位使用者對下列已改名群組的引用:{list}。是否繼續?`,
277
+ },
278
+ confirmSaveRenameRefsExtGrup: {
279
+ eng: `After saving, the group references of {n} user(s) will be updated to the new names of these renamed group(s): {list}. The source of {e} of them is an external application; their references may be overwritten by external data at the next external sync. Continue?`,
280
+ cht: `儲存後將一併更新 {n} 位使用者對下列已改名群組的引用:{list}。其中 {e} 位來源為外部應用,下次外部同步時可能被外部資料覆寫。是否繼續?`,
281
+ },
282
+ confirmSaveRenameRefsPemi: {
283
+ eng: `After saving, the permission references of {n} group(s) will be updated to the new names of these renamed permission(s): {list}. Continue?`,
284
+ cht: `儲存後將一併更新 {n} 個群組對下列已改名權限的引用:{list}。是否繼續?`,
285
+ },
286
+ confirmSaveRenameRefsExtPemi: {
287
+ eng: `After saving, the permission references of {n} group(s) will be updated to the new names of these renamed permission(s): {list}. The source of {e} of them is an external application; their references may be overwritten by external data at the next external sync. Continue?`,
288
+ cht: `儲存後將一併更新 {n} 個群組對下列已改名權限的引用:{list}。其中 {e} 個來源為外部應用,下次外部同步時可能被外部資料覆寫。是否繼續?`,
289
+ },
290
+ confirmSaveDelRefsTarget: {
291
+ eng: `The settings for the following target(s) (deleted or renamed) will be removed from the rules of {n} permission(s): {list}. Save anyway?`,
292
+ cht: `將自 {n} 個權限的規則移除對下列管控對象的設定(已刪除或已改名):{list}。確定儲存?`,
293
+ },
294
+ cascadeListMore: { //名單超過 5 個時附於其後, {n} 為總數
295
+ eng: `… ({n} in total)`,
296
+ cht: `…等 {n} 個`,
297
+ },
298
+ //儲存成功且有引用端因改名被連帶更新時之成功訊息(只描述改名; 群組頁/權限頁): {n} 為前端 store 之預覽筆數(nRenameHolders, 伺服端以實際為準);
299
+ //無改名連帶時、刪除所致之連帶清理(筆數已於刪除確認框告知)與標的頁, 一律維持原成功訊息(grupSaveGrupsSuccess/pemiSavePemisSuccess/targetSaveTargetsSuccess)
300
+ grupSaveGrupsSuccessCascade: {
301
+ eng: `Save permission groups successfully. The group references of {n} user(s) were updated to the new name(s).`,
302
+ cht: `儲存權限群組數據成功,已將 {n} 位使用者對已改名群組的引用一併更新為新名稱。`,
303
+ },
304
+ pemiSavePemisSuccessCascade: {
305
+ eng: `Save permissions successfully. The permission references of {n} group(s) were updated to the new name(s).`,
306
+ cht: `儲存權限數據成功,已將 {n} 個群組對已改名權限的引用一併更新為新名稱。`,
307
+ },
308
+ //@@U2-KEYS
309
+ //@@S-KEYS
310
+ //@@M-KEYS
311
+ relationRowsStale: { //反向關聯對話框存檔時, 對話框內有列所對應的使用者/群組已被刪除或改名而整次拒存(規則帳本 R-REL-PAIR-01, ADR-032); {names} 為受影響之名稱
312
+ eng: `Not saved: the list changed while this dialog was open (removed or renamed: {names}). Nothing was changed. Please close this dialog, reload the page and try again.`,
313
+ cht: `未儲存:此對話框開啟期間清單已有異動(已刪除或改名:{names})。資料未變更,請關閉對話框並重新整理頁面後再操作。`,
314
+ },
315
+ //@@UI1-KEYS
316
+ //F-23/F-26 清單頁(標的/權限/群組/使用者)之外部列與外部同步提示(R-PRINCIPLE-00 原則 3「收嚴+提示」、原則 5; R-EXT-OWN-01; ADR-037; src/js/externalUi.mjs、src/js/getAddImpact.mjs):
317
+ // externalRowLocked 外部列首欄鎖頭圖示之 tooltip({from} 為來源); externalRowsNotice/externalRowsNoticeTarget 清單上方提示({n} 為外部列數; 標的無指派欄故另一鍵);
318
+ // syncWarnIdRenumber 外部同步疑似 id 重編之警示({from} 來源、{n} 筆數); externalRowDelBlocked 勾選含外部列而被略過之提示({n}); nameUsedByExternal 後台列與外部列撞名之儲存前提示({name}、{from});
319
+ // confirmAddRefs* 新增撞「懸空名」(仍被啟用中之引用指向、查無實體)之確認: {list} 新名稱(至多 5 個)、{n} 受影響之引用端數、{m} 其中以 AND 引用者(標的之 crules 項目無 mode, 無 AND 句)
320
+ externalRowLocked: {
321
+ eng: `Synchronized from external source "{from}". Read-only here.`,
322
+ cht: `由外部來源「{from}」同步,於此為唯讀。`,
323
+ },
324
+ externalRowsNotice: {
325
+ eng: `{n} row(s) are synchronized from an external source. Their identity fields are read-only and they cannot be deleted here; only their assignment settings can be changed.`,
326
+ cht: `有 {n} 筆資料由外部來源同步。其身分欄位於此為唯讀且不可刪除,僅可修改其授權指派設定。`,
327
+ },
328
+ externalRowsNoticeTarget: {
329
+ eng: `{n} row(s) are synchronized from an external source. They are read-only and cannot be deleted here.`,
330
+ cht: `有 {n} 筆資料由外部來源同步,於此為唯讀且不可刪除。`,
331
+ },
332
+ syncWarnIdRenumber: {
333
+ eng: `The external source "{from}" appears to have renumbered ids ({n} item(s) now carry a name that previously belonged to an item with a different id). Each synchronization removes the old items and cleans up the references to them, which may erase the assignments made here. Please ask the source to keep ids stable or to omit ids.`,
334
+ cht: `外部來源「{from}」疑似重編 id(有 {n} 筆項目的名稱,先前屬於不同 id 之項目)。每次同步會移除舊項目並連帶清理其引用,可能抹掉在此做的指派。請來源端保持 id 穩定或不帶 id。`,
335
+ },
336
+ externalRowDelBlocked: {
337
+ eng: `{n} selected row(s) are synchronized from an external source and cannot be deleted here. They were skipped.`,
338
+ cht: `所選資料中有 {n} 筆由外部來源同步,無法在此刪除,已略過。`,
339
+ },
340
+ nameUsedByExternal: {
341
+ eng: `External source "{from}" already uses the name "{name}".`,
342
+ cht: `外部來源「{from}」已使用此名稱「{name}」。`,
343
+ },
344
+ confirmAddRefsGrup: {
345
+ eng: `Adding the group(s) "{list}": {n} user(s) already refer to the name, so after saving they will get (or be limited by) the permissions of the new group(s). Add anyway?`,
346
+ cht: `新增群組「{list}」後,將有 {n} 位使用者取得或受此群組限制(其授權設定原已引用此名稱)。確定新增?`,
347
+ },
348
+ confirmAddRefsAndGrup: {
349
+ eng: `Adding the group(s) "{list}": {n} user(s) already refer to the name, so after saving they will get (or be limited by) the permissions of the new group(s); {m} of them refer to it with AND. Add anyway?`,
350
+ cht: `新增群組「{list}」後,將有 {n} 位使用者(其中 {m} 位以 AND 引用)取得或受此群組限制(其授權設定原已引用此名稱)。確定新增?`,
351
+ },
352
+ confirmAddRefsPemi: {
353
+ eng: `Adding the permission(s) "{list}": {n} group(s) already refer to the name, so after saving they will get (or be limited by) the new permission(s). Add anyway?`,
354
+ cht: `新增權限「{list}」後,將有 {n} 個群組取得或受此權限限制(其授權設定原已引用此名稱)。確定新增?`,
355
+ },
356
+ confirmAddRefsAndPemi: {
357
+ eng: `Adding the permission(s) "{list}": {n} group(s) already refer to the name, so after saving they will get (or be limited by) the new permission(s); {m} of them refer to it with AND. Add anyway?`,
358
+ cht: `新增權限「{list}」後,將有 {n} 個群組(其中 {m} 個以 AND 引用)取得或受此權限限制(其授權設定原已引用此名稱)。確定新增?`,
359
+ },
360
+ confirmAddRefsTarget: {
361
+ eng: `Adding the target(s) "{list}": the rules of {n} permission(s) already contain settings for the name, so after saving those settings will take effect for the new target(s). Add anyway?`,
362
+ cht: `新增管控對象「{list}」後,將有 {n} 個權限的規則取得此管控對象的設定(其規則原已寫有此名稱)。確定新增?`,
363
+ },
149
364
  saveInProgress: { //同一操作者之同表儲存處理中再送出 (ADR-025)
150
365
  eng: `The previous save is still in progress. Please do not submit again.`,
151
366
  cht: `上一次儲存仍在處理中,請勿重複送出`,
@@ -168,6 +383,58 @@ let kpLang = {
168
383
  eng: `Not all changes were written, and some may have taken effect. Please reload the page and check before making changes.`,
169
384
  cht: `資料寫入未全部完成,部分變更可能已生效,請重新整理頁面確認後再操作`,
170
385
  },
386
+ //引用連帶(改名級聯/刪除連帶清理; R-PRINCIPLE-00 原則 6、適用表 m/n; R-REF-CASCADE-01): 伺服端預檢被拒(零變更)之固定訊息, 動態細節只入 srLog
387
+ renameCascadeDupGroup: {
388
+ eng: `Not saved: a renamed item shares its old name with other existing items, so the references to it cannot be updated safely. Nothing was changed. Please delete or rename the duplicates first (save them separately), then rename it.`,
389
+ cht: `未儲存:被改名的項目與其他既有項目同名,無法安全地連帶更新引用。資料未變更。請先清理同名項目(分次儲存)後再改名。`,
390
+ },
391
+ renameCascadeNameInUse: {
392
+ eng: `Not saved: a new name is still referenced by settings of items that no longer exist, so the renamed item would take over those references. Nothing was changed. Please clean up the dangling references first, or choose another name.`,
393
+ cht: `未儲存:新名稱仍被已不存在之項目的授權設定引用,改名後會被這些引用接管。資料未變更。請先清理懸空引用,或改用其他名稱。`,
394
+ },
395
+ renameCascadeConflict: {
396
+ eng: `Not saved: a setting contains both the old and the new name, so its references cannot be updated automatically. Nothing was changed.`,
397
+ cht: `未儲存:有授權設定同時含有舊名稱與新名稱,無法自動更新引用。資料未變更。`,
398
+ },
399
+ renameCascadeUnsupported: {
400
+ eng: `Not saved: a setting that refers to the renamed or deleted item contains a value that cannot be safely rewritten (such as a non-finite number), so its references cannot be updated automatically. Nothing was changed. Please fix that setting first.`,
401
+ cht: `未儲存:引用此項目之授權設定含有無法安全改寫的內容(如非有限數值),無法自動更新引用。資料未變更,請先修正該授權設定。`,
402
+ },
403
+ saveCascadePartial: {
404
+ eng: `The related references may be out of sync with this change (the automatic restore failed). Please do not make further changes and contact the system administrator to run the reference audit.`,
405
+ cht: `相關的授權引用可能與本次變更不一致(自動還原失敗)。請勿再變更資料,並通知系統管理者執行引用稽核。`,
406
+ },
407
+ //F-23/F-26(外部列保護與過期快照; R-PRINCIPLE-00 原則 3、5; R-EXT-OWN-01、R-BASELINE-01, ADR-037): 後台整表寫入被拒(零變更)之固定訊息, 動態細節(來源、筆數、原因)只入 srLog
408
+ saveFromNotAllowed: {
409
+ eng: `Not saved: rows saved here must have an empty "From" value (rows with a source are managed by the external source). Nothing was changed.`,
410
+ cht: `未儲存:於此儲存之列其「來源」須為空白(有來源之列由外部來源管理)。資料未變更。`,
411
+ },
412
+ saveExternalRowChanged: {
413
+ eng: `Not saved: rows synchronized from an external source cannot be deleted here, and only their assignment settings can be changed. Nothing was changed. Please change them at the external source.`,
414
+ cht: `未儲存:由外部來源同步之列不可在此刪除,且僅能修改其授權指派設定。資料未變更,請至外部來源端處理。`,
415
+ },
416
+ saveStale: {
417
+ eng: `Not saved: the data changed (by someone else or by an external synchronization) after this page was loaded. Nothing was changed. Please reload the page and try again.`,
418
+ cht: `未儲存:載入此頁面後資料已被他人或外部同步更新。資料未變更,請重新整理頁面後再操作。`,
419
+ },
420
+ //R-PRINCIPLE-00 原則 7(多人同時編輯, 2026-10-22; ADR-037 Update run11): 有未存編輯時他人或外部同步改了本表 → 清單頁保留草稿並顯示 staleDraftNotice(附「重新載入」鈕 staleDraftReload);
421
+ //反向關聯對話框(開框後資料已被更新)顯示 staleDraftNoticeDialog. 存檔時由伺服端以表雜湊比對拒收, 訊息仍為 saveStale(不改其文字)
422
+ staleDraftNotice: {
423
+ eng: `The data was updated by someone else or by an external synchronization after you started editing. Your unsaved edits are kept here, but saving now will be rejected. Click "Reload" to get the latest data (your unsaved edits will be discarded).`,
424
+ cht: `資料已被他人或外部同步更新。您尚未儲存的編輯仍保留於此,但現在儲存將被拒絕。請按「重新載入」取得最新資料(未儲存的編輯將被捨棄)。`,
425
+ },
426
+ staleDraftReload: {
427
+ eng: `Reload`,
428
+ cht: `重新載入`,
429
+ },
430
+ staleDraftNoticeDialog: {
431
+ eng: `The data was updated by someone else or by an external synchronization after this window was opened. Saving from this window will be rejected. Please close this window and open it again to get the latest data.`,
432
+ cht: `資料已被他人或外部同步更新,本視窗內的資料已過期,儲存將被拒絕。請關閉本視窗後重新開啟,以取得最新資料。`,
433
+ },
434
+ saveBaselineRequired: {
435
+ eng: `Not saved: changing the assignment settings of rows synchronized from an external source requires the latest loaded data, which this request did not carry. Nothing was changed. Please reload the page and try again.`,
436
+ cht: `未儲存:修改外部來源同步列之授權指派設定須附載入時之資料版本,此次請求未附。資料未變更,請重新整理頁面後再操作。`,
437
+ },
171
438
  cannotGetUserId: {
172
439
  eng: `Cannot get the user id`,
173
440
  cht: `無法取得使用者 Id`,
@@ -196,6 +463,10 @@ let kpLang = {
196
463
  eng: `Cannot find the user in permission system`,
197
464
  cht: `無法於權限系統取得使用者`,
198
465
  },
466
+ ambiguousUserFromPerm: { //登入查找: 以對應鍵(mappingBy)查 perm users(isActive='y')命中多於 1 筆, 拒絕而不取首筆(規則帳本 R-MAPKEY-02, ADR-034); 訊息不含使用者之對應值
467
+ eng: `Cannot identify the user in permission system: more than one active user matches`,
468
+ cht: `無法於權限系統確認使用者:有多位有效使用者符合`,
469
+ },
199
470
  userNoPermission: {
200
471
  eng: `User does not have permission`,
201
472
  cht: `使用者權限不足`,
@@ -0,0 +1,239 @@
1
+ //外部同步(WWebPerm.mjs:syncAndReplaceTabs)之三個純函數(F-23; R-PRINCIPLE-00 原則 3、5; 規則帳本 R-EXT-OWN-01; ADR-037), 不依賴 ORM/Vue, 供單元測試:
2
+ // keepAssignOnMissing: users 列「payload 缺 cgrups 鍵」時保留庫內指派(指派欄屬後台, Lv1; 含該鍵則以外部為準)——修正前 ltdtmapping 缺鍵補 ''、delAll+insert 即清掉後台已指派之群組(run06 已知限制④);
3
+ // keepAssignByEntity: grups 列缺 cpemis 鍵、pemis 列缺 crules 鍵時同樣保留(run08; 同為後台可寫之指派欄)——群組/權限無登入對應鍵, 實體身分依 id(無 id 者依名稱), 見該函數說明;
4
+ // countIdRenumber: 同一 from 同一批內出現「同名異 id」(疑似外部 id 重編)之筆數, 供 warn log 與頁面提示(外部 id 不穩定時, id 消失=刪除會每次清理引用而抹掉後台指派, 不加開關, 以提示處理)
5
+
6
+
7
+ let isStr = (v) => typeof v === 'string'
8
+ let isObj = (v) => v !== null && typeof v === 'object'
9
+ let hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k)
10
+
11
+
12
+ //toId: 列之 id 取字串形式(非空字串; 有限數字取其字串), 否則 null(同 refCascade.fillIdlessIds)
13
+ let toId = (v) => {
14
+ let id = isObj(v) ? v.id : undefined
15
+ if (isStr(id) && id !== '') {
16
+ return id
17
+ }
18
+ return (typeof id === 'number' && Number.isFinite(id)) ? String(id) : null
19
+ }
20
+
21
+
22
+ /**
23
+ * 記錄 payload 各列是否自帶指定鍵(須於 ltdtmapping 補鍵之前呼叫; ltdtmapping 為 1:1 同序, 故可依索引對應)
24
+ *
25
+ * @param {Array} rowsRaw 輸入 payload 原始列(皆為物件)
26
+ * @param {String} key 輸入鍵名(如 'cgrups')
27
+ * @returns {Array} 回傳布林陣列, 第 k 項為第 k 列是否自帶該鍵(自有屬性)
28
+ */
29
+ function markHasKey(rowsRaw, key) {
30
+ return rowsRaw.map((v) => isObj(v) && hasOwn(v, key))
31
+ }
32
+
33
+
34
+ /**
35
+ * users 列缺 cgrups 鍵者, 以庫內同一 from 之對應列之值取代(對應: 先依 id; 無 id 者在 mappingBy 為 email/name 時依該欄位, 須恰命中 1 筆; 對不到則不保留, 即新使用者/無從對應者維持 '')
36
+ *
37
+ * @param {Object} p 輸入參數物件
38
+ * @param {Array} p.rows 輸入已 ltdtmapping 並帶 from 之本批列
39
+ * @param {Array} p.hasKey 輸入 markHasKey 之結果(與 rows 同序同長)
40
+ * @param {Array} p.rowsOldSameFrom 輸入庫內同一 from 之既有 users 列
41
+ * @param {String} [p.mappingBy='id'] 輸入使用者對應鍵('id'|'email'|'name')
42
+ * @param {String} [p.key='cgrups'] 輸入指派欄名稱
43
+ * @param {Function} [p.isKeepable] 輸入判斷庫內值是否可保留之函數(收庫內值, 回布林), 預設恆可; 不可保留者(如無法原樣通過寫入前驗證)不保留並計入 nSkip
44
+ * @returns {Object} 回傳 { rows, nKeep, nSkip, nConflict }: rows 為新陣列(被保留值之列為複本, 其餘同一參考), nKeep 為保留筆數, nSkip 為因 isKeepable 為假而未保留之筆數,
45
+ * nConflict 為因身分矛盾而未保留之筆數;
46
+ * mappingBy 為 email/name 時, 以 id 對到之舊列其對應鍵值須與本批列相同、且庫內同一來源不得另有別列持有同值(舊登入擁有者矛盾), 否則不保留(同來源 id 重用/重編之矛盾)
47
+ */
48
+ function keepAssignOnMissing({ rows, hasKey, rowsOldSameFrom, mappingBy = 'id', key = 'cgrups', isKeepable = () => true }) {
49
+ let oldById = new Map()
50
+ let oldByMap = new Map() //mappingBy 欄位值 → 同值之既有列陣列
51
+ rowsOldSameFrom.forEach((o) => {
52
+ if (!isObj(o)) {
53
+ return
54
+ }
55
+ if (isStr(o.id)) {
56
+ oldById.set(o.id, o)
57
+ }
58
+ if ((mappingBy === 'email' || mappingBy === 'name') && isStr(o[mappingBy]) && o[mappingBy] !== '') {
59
+ if (!oldByMap.has(o[mappingBy])) {
60
+ oldByMap.set(o[mappingBy], [])
61
+ }
62
+ oldByMap.get(o[mappingBy]).push(o)
63
+ }
64
+ })
65
+ let byMap = mappingBy === 'email' || mappingBy === 'name'
66
+ let nKeep = 0
67
+ let nSkip = 0
68
+ let nConflict = 0
69
+ let out = rows.map((v, k) => {
70
+ if (hasKey[k] === true || !isObj(v)) {
71
+ return v
72
+ }
73
+ let id = toId(v)
74
+ let o = null
75
+ if (id !== null) {
76
+ o = oldById.has(id) ? oldById.get(id) : null
77
+
78
+ //對應鍵矛盾(外審 sol R3): mappingBy 為 email/name 時, 登入以對應鍵找使用者; 本批列以 id 對到之舊列, 其對應鍵值須與本批列相同, 否則為同來源 id 重用/重編(id 指向 A、對應鍵卻是 B),
79
+ //自動把 A 之後台指派移轉給 B 屬靜默擇一(R-PRINCIPLE-00 原則 3), 一律不保留(cgrups 維持空字串, 收嚴); 不保留者計入 nConflict 供呼叫端提示(原則 3: 收嚴須附提示, 增量外審 N2)
80
+ if (o !== null && byMap && (v[mappingBy] === undefined ? '' : v[mappingBy]) !== (o[mappingBy] === undefined ? '' : o[mappingBy])) {
81
+ o = null
82
+ nConflict++
83
+ }
84
+
85
+ //舊登入擁有者矛盾(增量外審 N1): 對應鍵值非空時, 庫內同一來源須只有該列自己持有此值; 若另有別列(如舊 active 擁有者為另一個 id, 本列為停用列被本批改為啟用)持有同值,
86
+ //登入查找(以對應鍵+isActive)原指向別列, 保留本列之指派會使該登入值改取得本列之授權(移轉), 同樣不保留
87
+ if (o !== null && byMap && isStr(v[mappingBy]) && v[mappingBy] !== '') {
88
+ let cands = oldByMap.get(v[mappingBy])
89
+ if (cands !== undefined && cands.some((c) => c !== o)) {
90
+ o = null
91
+ nConflict++
92
+ }
93
+ }
94
+ }
95
+ else if (byMap && isStr(v[mappingBy]) && v[mappingBy] !== '') {
96
+ let cands = oldByMap.get(v[mappingBy])
97
+ o = (cands !== undefined && cands.length === 1) ? cands[0] : null
98
+
99
+ //對應鍵命中多筆(如同值之啟用與停用列)無從判定歸屬, 不取首筆而不保留; 須附提示, 計入 nConflict(收嚴+提示, run08 外審 sol #3; 與 keepAssignByEntity 之無 id 同名多筆一致)
100
+ if (cands !== undefined && cands.length > 1) {
101
+ nConflict++
102
+ }
103
+ }
104
+ if (o === null || !hasOwn(o, key)) {
105
+ return v
106
+ }
107
+
108
+ //保留值須能通過與外部寫入同一套寫入前驗證(外審 sol R4): 保留之歷史值若含會被 clearXSS 改寫之標記或非純量, 入庫時被改寫而使引用改指他名(增權), 一律不保留(視為空, 收嚴)
109
+ if (!isKeepable(o[key])) {
110
+ nSkip++
111
+ return v
112
+ }
113
+ nKeep++
114
+ return { ...v, [key]: o[key] }
115
+ })
116
+ return { rows: out, nKeep, nSkip, nConflict }
117
+ }
118
+
119
+
120
+ /**
121
+ * grups 列缺 cpemis 鍵、pemis 列缺 crules 鍵者, 以庫內同一 from 之同一實體之值取代(run08; 與 keepAssignOnMissing 同一精神: 指派欄屬後台, 同步不得靜默清掉; 含該鍵則以外部為準)
122
+ *
123
+ * 實體身分(群組/權限無登入對應鍵, 不適用 mappingBy 之矛盾檢查): 有 id 者依 id 對到庫內同一 from 之列; 無 id 者(寫入時 ORM 補隨機主鍵, 身分不穩定)依名稱, 與引用連帶(refCascade.fillIdlessIds)同一口徑:
124
+ * 對庫內同一 from 中 id 未被本批帶用、名稱相同之列, 須恰 1 筆(同名多筆為歷史同名, 不取首筆); 對不到者視為新實體, 維持缺鍵之預設('').
125
+ * 收嚴(原則 3, 不保留並計入 nConflict, 供呼叫端記 warn): ①無 id 而同名命中多筆; ②同 id 但名稱不同(改名)且 cascadeRename 為假——引用不跟著改時名稱即授權身分,
126
+ * 把舊名之指派移轉給新名, 會使原引用新名之使用者(懸空)取得該授權; ③同 id 改名而新舊名稱有一方非字串(歷史資料)——級聯無從處理(缺欄視 '', 同 refCascade.entName), 保留同樣會增權(run08 外審 sol #1).
127
+ *
128
+ * @param {Object} p 輸入參數物件
129
+ * @param {Array} p.rows 輸入已 ltdtmapping 並帶 from 之本批列
130
+ * @param {Array} p.hasKey 輸入 markHasKey 之結果(與 rows 同序同長)
131
+ * @param {Array} p.rowsOldSameFrom 輸入庫內同一 from 之既有列
132
+ * @param {String} p.key 輸入指派欄名稱('cpemis'|'crules')
133
+ * @param {Boolean} [p.cascadeRename=true] 輸入改名是否連帶更新引用(opt.cascadeRename), 預設 true
134
+ * @param {Function} [p.isKeepable] 輸入判斷庫內值是否可保留之函數(收庫內值, 回布林), 預設恆可; 不可保留者不保留並計入 nSkip
135
+ * @returns {Object} 回傳 { rows, nKeep, nSkip, nConflict }: rows 為新陣列(被保留值之列為複本, 其餘同一參考), nKeep 為保留筆數, nSkip 為因 isKeepable 為假而未保留之筆數, nConflict 為因身分不確定而未保留之筆數
136
+ */
137
+ function keepAssignByEntity({ rows, hasKey, rowsOldSameFrom, key, cascadeRename = true, isKeepable = () => true }) {
138
+ let nameOf = (r) => (r.name === undefined ? '' : r.name) //同 refCascade.entName: 缺欄視 ''
139
+ let idsBatch = new Set()
140
+ rows.forEach((v) => {
141
+ let id = toId(v)
142
+ if (id !== null) {
143
+ idsBatch.add(id)
144
+ }
145
+ })
146
+ let oldById = new Map()
147
+ let oldByName = new Map() //名稱 → 本批未帶用其 id 之既有列陣列(無 id 者之對應池)
148
+ rowsOldSameFrom.forEach((o) => {
149
+ if (!isObj(o) || !isStr(o.id)) {
150
+ return
151
+ }
152
+ oldById.set(o.id, o)
153
+ if (isStr(o.name) && !idsBatch.has(o.id)) {
154
+ if (!oldByName.has(o.name)) {
155
+ oldByName.set(o.name, [])
156
+ }
157
+ oldByName.get(o.name).push(o)
158
+ }
159
+ })
160
+ let nKeep = 0
161
+ let nSkip = 0
162
+ let nConflict = 0
163
+ let out = rows.map((v, k) => {
164
+ if (hasKey[k] === true || !isObj(v)) {
165
+ return v
166
+ }
167
+ let id = toId(v)
168
+ let o = null
169
+ if (id !== null) {
170
+ o = oldById.has(id) ? oldById.get(id) : null
171
+
172
+ //改名(同 id 異名)須「引用跟著改」才算同一實體: cascadeRename 為假時引用不跟著改; 為真時級聯只處理新舊名稱皆為字串之改名(refCascade.diffRefTargets: 缺欄視 '', 非字串不成實體),
173
+ //舊名稱為歷史非字串者無從級聯, 新名稱撞懸空名之檢查(renameCascadeNameInUse)亦不會觸發, 保留會把舊列之指派帶給原指向新名之懸空引用者, 同樣不保留(run08 外審 sol #1)
174
+ if (o !== null && nameOf(o) !== nameOf(v) && (cascadeRename === false || !isStr(nameOf(o)) || !isStr(nameOf(v)))) {
175
+ o = null
176
+ nConflict++
177
+ }
178
+ }
179
+ else if (isStr(v.name) && oldByName.has(v.name)) {
180
+ let cands = oldByName.get(v.name)
181
+ if (cands.length === 1) {
182
+ o = cands[0]
183
+ }
184
+ else {
185
+ nConflict++
186
+ }
187
+ }
188
+ if (o === null || !hasOwn(o, key)) {
189
+ return v
190
+ }
191
+
192
+ //保留值須能通過與外部寫入同一套寫入前驗證(同 keepAssignOnMissing): 不通過者不保留(視為空, 收嚴)
193
+ if (!isKeepable(o[key])) {
194
+ nSkip++
195
+ return v
196
+ }
197
+ nKeep++
198
+ return { ...v, [key]: o[key] }
199
+ })
200
+ return { rows: out, nKeep, nSkip, nConflict }
201
+ }
202
+
203
+
204
+ /**
205
+ * 同一 from 同一批內「同名異 id」之筆數(疑似外部 id 重編): 本批列帶 id 與其名稱, 而庫內同一 from 有另一列同名、不同 id、且該舊 id 不在本批
206
+ *
207
+ * @param {Array} rowsOldSameFrom 輸入庫內同一 from 之既有列(grups/pemis; users 之 name 可重複故不適用)
208
+ * @param {Array} rows 輸入本批列(帶 id 者才計)
209
+ * @returns {Number} 回傳筆數
210
+ */
211
+ function countIdRenumber(rowsOldSameFrom, rows) {
212
+ let idsBatch = new Set()
213
+ rows.forEach((v) => {
214
+ let id = toId(v)
215
+ if (id !== null) {
216
+ idsBatch.add(id)
217
+ }
218
+ })
219
+ let oldByName = new Map() //名稱 → 本批未沿用之舊 id 集合
220
+ rowsOldSameFrom.forEach((o) => {
221
+ if (isObj(o) && isStr(o.id) && isStr(o.name) && !idsBatch.has(o.id)) {
222
+ if (!oldByName.has(o.name)) {
223
+ oldByName.set(o.name, new Set())
224
+ }
225
+ oldByName.get(o.name).add(o.id)
226
+ }
227
+ })
228
+ let n = 0
229
+ rows.forEach((v) => {
230
+ let id = toId(v)
231
+ if (id !== null && isObj(v) && isStr(v.name) && oldByName.has(v.name)) {
232
+ n++
233
+ }
234
+ })
235
+ return n
236
+ }
237
+
238
+
239
+ export { markHasKey, keepAssignOnMissing, keepAssignByEntity, countIdRenumber }