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,572 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+
5
+ <meta charset="utf-8">
6
+ <title>src/js/refCascade.mjs - Documentation</title>
7
+
8
+
9
+ <script src="scripts/prettify/prettify.js"></script>
10
+ <script src="scripts/prettify/lang-css.js"></script>
11
+ <!--[if lt IE 9]>
12
+ <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
13
+ <![endif]-->
14
+ <link type="text/css" rel="stylesheet" href="styles/prettify.css">
15
+ <link type="text/css" rel="stylesheet" href="styles/jsdoc.css">
16
+ <script src="scripts/nav.js" defer></script>
17
+
18
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
19
+ </head>
20
+ <body>
21
+
22
+ <input type="checkbox" id="nav-trigger" class="nav-trigger" />
23
+ <label for="nav-trigger" class="navicon-button x">
24
+ <div class="navicon"></div>
25
+ </label>
26
+
27
+ <label for="nav-trigger" class="overlay"></label>
28
+
29
+ <nav >
30
+
31
+
32
+ <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WWebPerm.html">WWebPerm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#applyPurgePlan">applyPurgePlan</a></li><li><a href="global.html#auditFrom">auditFrom</a></li><li><a href="global.html#auditRefs">auditRefs</a></li><li><a href="global.html#buildReport">buildReport</a></li><li><a href="global.html#buildRowsSend">buildRowsSend</a></li><li><a href="global.html#buildSyncRowsNew">buildSyncRowsNew</a></li><li><a href="global.html#checkBaseline">checkBaseline</a></li><li><a href="global.html#checkCascadePlan">checkCascadePlan</a></li><li><a href="global.html#checkDbNotInUse">checkDbNotInUse</a></li><li><a href="global.html#checkExternalWrite">checkExternalWrite</a></li><li><a href="global.html#checkMapKeyBatch">checkMapKeyBatch</a></li><li><a href="global.html#checkUsersWrite">checkUsersWrite</a></li><li><a href="global.html#cleanOutOf">cleanOutOf</a></li><li><a href="global.html#compareBaseline">compareBaseline</a></li><li><a href="global.html#countAndRefs">countAndRefs</a></li><li><a href="global.html#countExternalRows">countExternalRows</a></li><li><a href="global.html#countIdRenumber">countIdRenumber</a></li><li><a href="global.html#createAuthorizeExec">createAuthorizeExec</a></li><li><a href="global.html#createLegendMeasurer">createLegendMeasurer</a></li><li><a href="global.html#createLockSave">createLockSave</a></li><li><a href="global.html#cvKpToArr">cvKpToArr</a></li><li><a href="global.html#cvKpToObj">cvKpToObj</a></li><li><a href="global.html#decideLegendType">decideLegendType</a></li><li><a href="global.html#describeUpstream">describeUpstream</a></li><li><a href="global.html#diffRefTargets">diffRefTargets</a></li><li><a href="global.html#dropServerOnlyDiff">dropServerOnlyDiff</a></li><li><a href="global.html#eachOwn">eachOwn</a></li><li><a href="global.html#fetchJson">fetchJson</a></li><li><a href="global.html#fillIdlessIds">fillIdlessIds</a></li><li><a href="global.html#findDupIndex">findDupIndex</a></li><li><a href="global.html#findKeyOtherFrom">findKeyOtherFrom</a></li><li><a href="global.html#findRefRows">findRefRows</a></li><li><a href="global.html#findTouchedDup">findTouchedDup</a></li><li><a href="global.html#fmtLossyMsg">fmtLossyMsg</a></li><li><a href="global.html#fmtVars">fmtVars</a></li><li><a href="global.html#fmtWarn">fmtWarn</a></li><li><a href="global.html#formatFixReport">formatFixReport</a></li><li><a href="global.html#formatFromAudit">formatFromAudit</a></li><li><a href="global.html#formatPurgeReport">formatPurgeReport</a></li><li><a href="global.html#genLegend">genLegend</a></li><li><a href="global.html#getAddConfirmText">getAddConfirmText</a></li><li><a href="global.html#getAddImpact">getAddImpact</a></li><li><a href="global.html#getBannerLines">getBannerLines</a></li><li><a href="global.html#getBaselineDigest">getBaselineDigest</a></li><li><a href="global.html#getCascadePreview">getCascadePreview</a></li><li><a href="global.html#getChangedIdx">getChangedIdx</a></li><li><a href="global.html#getDelConfirmText">getDelConfirmText</a></li><li><a href="global.html#getDelImpact">getDelImpact</a></li><li><a href="global.html#getDelRefCount">getDelRefCount</a></li><li><a href="global.html#getDigestFields">getDigestFields</a></li><li><a href="global.html#getDupRowsInconsistent">getDupRowsInconsistent</a></li><li><a href="global.html#getEffMode">getEffMode</a></li><li><a href="global.html#getExternalEdits">getExternalEdits</a></li><li><a href="global.html#getItemWarnText">getItemWarnText</a></li><li><a href="global.html#getKpLossy">getKpLossy</a></li><li><a href="global.html#getLockFields">getLockFields</a></li><li><a href="global.html#getNameErrs">getNameErrs</a></li><li><a href="global.html#getOwn">getOwn</a></li><li><a href="global.html#getPropKey">getPropKey</a></li><li><a href="global.html#getRawModeMap">getRawModeMap</a></li><li><a href="global.html#getRefRowsExtra">getRefRowsExtra</a></li><li><a href="global.html#getRefState">getRefState</a></li><li><a href="global.html#getRefWarnText">getRefWarnText</a></li><li><a href="global.html#getRefWarns">getRefWarns</a></li><li><a href="global.html#getRowIdxs">getRowIdxs</a></li><li><a href="global.html#getRowState">getRowState</a></li><li><a href="global.html#getRuleEnable">getRuleEnable</a></li><li><a href="global.html#getRuleWarnText">getRuleWarnText</a></li><li><a href="global.html#getRuleWarns">getRuleWarns</a></li><li><a href="global.html#getSavePreview">getSavePreview</a></li><li><a href="global.html#getSyncWarnLines">getSyncWarnLines</a></li><li><a href="global.html#hasDupKey">hasDupKey</a></li><li><a href="global.html#hasExternalDiff">hasExternalDiff</a></li><li><a href="global.html#isAuthMode">isAuthMode</a></li><li><a href="global.html#isBaselineStale">isBaselineStale</a></li><li><a href="global.html#isBaselineValid">isBaselineValid</a></li><li><a href="global.html#isDraftStale">isDraftStale</a></li><li><a href="global.html#isExternalRow">isExternalRow</a></li><li><a href="global.html#isJsonLossy">isJsonLossy</a></li><li><a href="global.html#isMain">isMain</a></li><li><a href="global.html#joinList">joinList</a></li><li><a href="global.html#joinNames">joinNames</a></li><li><a href="global.html#keepAssignByEntity">keepAssignByEntity</a></li><li><a href="global.html#keepAssignOnMissing">keepAssignOnMissing</a></li><li><a href="global.html#makeBaseline">makeBaseline</a></li><li><a href="global.html#makeRowStates">makeRowStates</a></li><li><a href="global.html#mapKeyOf">mapKeyOf</a></li><li><a href="global.html#mapNameKp">mapNameKp</a></li><li><a href="global.html#markDataReload">markDataReload</a></li><li><a href="global.html#markHasKey">markHasKey</a></li><li><a href="global.html#maskQuery">maskQuery</a></li><li><a href="global.html#maskTok">maskTok</a></li><li><a href="global.html#maskUrl">maskUrl</a></li><li><a href="global.html#mergePickKeysOnly">mergePickKeysOnly</a></li><li><a href="global.html#pairRowsToSnapshot">pairRowsToSnapshot</a></li><li><a href="global.html#pickUniqueUser">pickUniqueUser</a></li><li><a href="global.html#planDraftSync">planDraftSync</a></li><li><a href="global.html#planFixFromType">planFixFromType</a></li><li><a href="global.html#planPurgeChain">planPurgeChain</a></li><li><a href="global.html#planPurgeFrom">planPurgeFrom</a></li><li><a href="global.html#planRefChanges">planRefChanges</a></li><li><a href="global.html#putRefEntry">putRefEntry</a></li><li><a href="global.html#putRowsToKp">putRowsToKp</a></li><li><a href="global.html#refsChanged">refsChanged</a></li><li><a href="global.html#resolveShellWorker">resolveShellWorker</a></li><li><a href="global.html#restoreExternalOrder">restoreExternalOrder</a></li><li><a href="global.html#restoreServerFields">restoreServerFields</a></li><li><a href="global.html#reuseDeletedIds">reuseDeletedIds</a></li><li><a href="global.html#rewriteRefs">rewriteRefs</a></li><li><a href="global.html#rowDigest">rowDigest</a></li><li><a href="global.html#runAuditFrom">runAuditFrom</a></li><li><a href="global.html#runClean">runClean</a></li><li><a href="global.html#runFixFromType">runFixFromType</a></li><li><a href="global.html#runPurgeFrom">runPurgeFrom</a></li><li><a href="global.html#splitCheckedRows">splitCheckedRows</a></li><li><a href="global.html#submitGuard">submitGuard</a></li><li><a href="global.html#tabWriteKey">tabWriteKey</a></li><li><a href="global.html#validateRowsBeforeWrite">validateRowsBeforeWrite</a></li><li><a href="global.html#withTabLocks">withTabLocks</a></li><li><a href="global.html#wrapInjected">wrapInjected</a></li><li><a href="global.html#wrapOperOrm">wrapOperOrm</a></li><li><a href="global.html#writeWithCascade">writeWithCascade</a></li></ul>
33
+
34
+ </nav>
35
+
36
+ <div id="main">
37
+
38
+ <h1 class="page-title">src/js/refCascade.mjs</h1>
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+ <section>
47
+ <article>
48
+ <pre class="prettyprint source linenums"><code>import JSON5 from 'json5'
49
+ import iseobj from 'wsemi/src/iseobj.mjs'
50
+ import getPropKey from './getPropKey.mjs'
51
+
52
+
53
+ //引用連帶(規則帳本 R-PRINCIPLE-00 原則 6、適用表 m/n; R-REF-CASCADE-01; ADR-036): 群組/權限/標的「改名」或「被刪除」時, 引用它之授權字串須於同一次寫入內連帶更新:
54
+ // users.cgrups(引用 grups.name)、grups.cpemis(引用 pemis.name)、pemis.crules(引用 targets.id)
55
+ //本檔為純函數(不依賴 Vue/DOM/ORM), 伺服端寫入(server/procCore.mjs、server/WWebPerm.mjs 之外部同步)與前端預覽/刪除確認框(src/components 三個清單頁)共用同一份規劃(R-REF-CASCADE-01: 預覽與伺服端計畫同一純函數)
56
+
57
+
58
+ //REF_OF: 被引用表 → 引用端表與欄位; kind 為引用名稱之比對方式('ref': cgrups/cpemis 以 === 比對字串名稱; 'rule': crules 以屬性鍵轉型後比對, 與伺服端 mergeRules 同)
59
+ let REF_OF = {
60
+ grups: { holder: 'users', field: 'cgrups', kind: 'ref' },
61
+ pemis: { holder: 'grups', field: 'cpemis', kind: 'ref' },
62
+ targets: { holder: 'pemis', field: 'crules', kind: 'rule' },
63
+ }
64
+
65
+
66
+ let isStr = (v) => typeof v === 'string'
67
+ let hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k)
68
+
69
+
70
+ //keyOf: 引用名稱 → 比對鍵; 'ref' 只認字串(非字串名稱以 === 比對不到任何實體, 不動), 'rule' 與伺服端 mergeRules 同取屬性鍵轉型(R-KEY-PROP-01)
71
+ let keyOf = (name, kind) => {
72
+ if (kind === 'rule') {
73
+ return getPropKey(name)
74
+ }
75
+ return isStr(name) ? name : null
76
+ }
77
+
78
+
79
+ //entName: 被引用表列之名稱(grups/pemis 為 name, 缺欄視為 ''(同 findRefRows); targets 為 id), 非字串回 null
80
+ let entName = (r, keyTable) => {
81
+ if (r === null || typeof r !== 'object') {
82
+ return null
83
+ }
84
+ let n = (keyTable === 'targets') ? r.id : (r.name === undefined ? '' : r.name)
85
+ return isStr(n) ? n : null
86
+ }
87
+
88
+
89
+ //decodeKp: 授權字串 → JSON5 解析結果(物件或陣列), 空字串/壞 JSON/純量回 null(伺服端 cvStrToArr 同為視為空容器, 不動)
90
+ let decodeKp = (str) => {
91
+ if (!isStr(str) || str.trim() === '') {
92
+ return null
93
+ }
94
+ let kp = null
95
+ try {
96
+ kp = JSON5.parse(str)
97
+ }
98
+ catch (err) {
99
+ return null
100
+ }
101
+ return (kp !== null &amp;&amp; typeof kp === 'object') ? kp : null
102
+ }
103
+
104
+
105
+ //listEntries: 容器內所有項目(含停用者)之 { key: 比對鍵, el: 原值, i: 物件形式之外層鍵或陣列索引, isObj, isActive }; 物件形式以外層鍵為名, 陣列形式以元素之 name 為名(非物件或無 name 之元素以索引為名, 同 cvKpToArr)
106
+ let listEntries = (kp, kind) => {
107
+ let rs = []
108
+ if (Array.isArray(kp)) {
109
+ kp.forEach((el, i) => {
110
+ let name = (iseobj(el) &amp;&amp; hasOwn(el, 'name')) ? el.name : i
111
+ rs.push({ key: keyOf(name, kind), el, i, isObj: iseobj(el), isActive: iseobj(el) ? el.isActive : el })
112
+ })
113
+ return rs
114
+ }
115
+ Object.keys(kp).forEach((k) => {
116
+ let v = kp[k]
117
+ rs.push({ key: keyOf(k, kind), el: v, i: k, isObj: iseobj(v), isActive: iseobj(v) ? v.isActive : v })
118
+ })
119
+ return rs
120
+ }
121
+
122
+
123
+ //hasNonFinite: 解析結果內是否有非有限數值(Infinity、-Infinity、NaN; JSON5 可寫, JSON.stringify 會編成 null)
124
+ let hasNonFinite = (v) => {
125
+ if (typeof v === 'number') {
126
+ return !Number.isFinite(v)
127
+ }
128
+ if (v !== null &amp;&amp; typeof v === 'object') {
129
+ return Object.keys(v).some((k) => hasNonFinite(v[k]))
130
+ }
131
+ return false
132
+ }
133
+
134
+
135
+ /**
136
+ * 改寫一個授權字串(cgrups/cpemis/crules)內對被改名/被刪除者之引用
137
+ *
138
+ * 同時套用所有改名與刪除(互換 A↔B、鏈式 A→B 且 B→C 皆正確, 不逐一套用); 先判刪除、再判改名. 物件形式(以名稱為鍵)仍輸出物件形式、陣列形式仍輸出陣列形式(不互轉, 免有損),
139
+ * 其餘項目(含值內之 mode/isActive/額外欄位)原樣; 未命中任何改名或刪除者「同一字串原樣回傳」(不重新編碼, 保留註解與排版); 命中者以 JSON.stringify 重新編碼(註解與單引號不保留).
140
+ * 壞 JSON/空字串/非容器視為空容器不動(與伺服端 cvStrToArr 容錯一致).
141
+ * unsafe: 容器內含無法原樣重新編碼之值(JSON5 Infinity/NaN 數值)而命中須改寫時, 不改寫(str 原樣、changed 為 false)並回報 true.
142
+ * conflict: 改名後新名與容器內另一個(未被一併改走或刪除之)同名項目相撞——自動改寫會與既有引用混成一項, 不改寫並回報(上層整次拒存).
143
+ *
144
+ * @param {String} str 輸入授權字串(JSON5)
145
+ * @param {Object} [opt={}] 輸入設定物件
146
+ * @param {String} [opt.kind='ref'] 輸入引用種類, 'ref'(cgrups/cpemis)或 'rule'(crules)
147
+ * @param {Map} [opt.rename] 輸入改名對映 Map(舊名 → 新名), 預設空
148
+ * @param {Set} [opt.del] 輸入被刪除之名稱 Set, 預設空
149
+ * @returns {Object} 回傳 { str, changed, nRename, nDel, conflict }: str 為改寫後之字串(未改為同一參考), nRename/nDel 為被改名/被刪除之項目數
150
+ * @example
151
+ *
152
+ * let r = rewriteRefs('{"A":{"mode":"OR","isActive":"y"}}', { rename: new Map([['A', 'B']]) })
153
+ * console.log(r.str)
154
+ * // => {"B":{"mode":"OR","isActive":"y"}}
155
+ *
156
+ */
157
+ function rewriteRefs(str, opt = {}) {
158
+ let kind = opt.kind === 'rule' ? 'rule' : 'ref'
159
+ let rename = opt.rename instanceof Map ? opt.rename : new Map()
160
+ let del = opt.del instanceof Set ? opt.del : new Set()
161
+ let res = { str, changed: false, nRename: 0, nDel: 0, conflict: false, unsafe: false }
162
+ if (rename.size === 0 &amp;&amp; del.size === 0) {
163
+ return res
164
+ }
165
+ let kp = decodeKp(str)
166
+ if (kp === null) {
167
+ return res
168
+ }
169
+
170
+ //entries, present: 容器內既有之比對鍵(含停用項目)
171
+ let entries = listEntries(kp, kind)
172
+ let present = new Set()
173
+ entries.forEach((e) => {
174
+ if (e.key !== null) {
175
+ present.add(e.key)
176
+ }
177
+ })
178
+
179
+ //conflict: 命中改名之項目, 其新名與容器內另一個「未被改走且未被刪除」之既有項目相撞
180
+ let isDel = (key) => key !== null &amp;&amp; del.has(key)
181
+ let isRen = (key) => key !== null &amp;&amp; !isDel(key) &amp;&amp; rename.has(key)
182
+ for (let e of entries) {
183
+ if (isRen(e.key)) {
184
+ let to = rename.get(e.key)
185
+ if (present.has(to) &amp;&amp; !isRen(to) &amp;&amp; !isDel(to)) {
186
+ res.conflict = true
187
+ return res
188
+ }
189
+ }
190
+ }
191
+
192
+ //改寫
193
+ let nRename = 0
194
+ let nDel = 0
195
+ let out = null
196
+ if (Array.isArray(kp)) {
197
+ out = []
198
+
199
+ //kFirstDel: 第一個被刪除項目之索引; 'rule'(crules)之索引名元素(非物件或無 name 者, 名稱即其索引)排在其後者, 刪除後索引前移會改指別的規則(伺服端 cvKpToArr 以索引為名),
200
+ //故具名化為其原索引(解碼結果不變: 非物件元素 { name: 索引, isActive: 值 }), 免靜默改變授權; 'ref' 之索引名為非字串, 不對應任何實體, 前移無影響
201
+ let kFirstDel = -1
202
+ if (kind === 'rule') {
203
+ entries.forEach((e) => {
204
+ if (kFirstDel &lt; 0 &amp;&amp; isDel(e.key)) {
205
+ kFirstDel = e.i
206
+ }
207
+ })
208
+ }
209
+ entries.forEach((e) => {
210
+ let hasName = e.isObj &amp;&amp; hasOwn(e.el, 'name')
211
+ if (isDel(e.key)) {
212
+ nDel++
213
+ }
214
+ else if (isRen(e.key) &amp;&amp; e.isObj) {
215
+ nRename++
216
+ out.push({ ...e.el, name: rename.get(e.key) })
217
+ }
218
+ else if (kFirstDel >= 0 &amp;&amp; e.i > kFirstDel &amp;&amp; !hasName) {
219
+ out.push(e.isObj ? { ...e.el, name: e.i } : { name: e.i, isActive: e.el })
220
+ }
221
+ else {
222
+ out.push(e.el)
223
+ }
224
+ })
225
+ }
226
+ else {
227
+ out = Object.create(null) //無原型: 鍵為使用者資料, 含 __proto__ 者須為自有鍵(規則帳本 R-KEY-01)
228
+ entries.forEach((e) => {
229
+ if (isDel(e.key)) {
230
+ nDel++
231
+ }
232
+ else if (isRen(e.key)) {
233
+ nRename++
234
+ out[rename.get(e.key)] = e.el
235
+ }
236
+ else {
237
+ out[e.i] = e.el
238
+ }
239
+ })
240
+ }
241
+ if (nRename === 0 &amp;&amp; nDel === 0) {
242
+ return res
243
+ }
244
+
245
+ //unsafe: 容器內含 JSON.stringify 無法原樣編碼之值(JSON5 之 Infinity/NaN 數值會被編成 null, 作為規則名時改指別的規則『null』, 作為值時亦改變語意), 命中而須重新編碼時不改寫並回報(上層整次拒存; 稽核工具列入 unsafe)
246
+ if (hasNonFinite(kp)) {
247
+ res.unsafe = true
248
+ return res
249
+ }
250
+
251
+ return { str: JSON.stringify(out), changed: true, nRename, nDel, conflict: false, unsafe: false }
252
+ }
253
+
254
+
255
+ /**
256
+ * 只依被引用表自身之新舊列, 判定「改名」與「須清理引用之被刪名稱」(與引用端無關; 後台整表寫入據以決定是否須再取引用端表之鎖, 僅被引用表有改名或刪除時才須)
257
+ *
258
+ * 身分以主鍵 id 判定: 同 id 且名稱皆為字串而不同=改名(僅 grups/pemis, 且 opt.cascadeRename 非 false; 互換與鏈式同時套用); 舊列 id 不在新表=刪除(targets 之 id 即名稱, 改 id 即刪舊增新, 標的 id 改名之級聯為下一階段);
259
+ * 刪除之名稱若仍有「同 id 同名之存續列」(歷史同名只刪其一)則不清理(引用仍解得出, 其授權變寬另由刪除前確認框提示); 刪除後以同名新增(新 id)者不繼承引用(原則 6: 不被同名新資料接管).
260
+ *
261
+ * @param {Object} p 輸入參數物件, 同 planRefChanges(只用 keyTable、rowsOld、rowsNew、opt.cascadeRename)
262
+ * @returns {Object} 回傳 { renames, rename, delNames, del, oldById, newById }: renames 為 [{ id, from, to }], rename 為 Map(舊名 → 新名), delNames/del 為須清理之名稱陣列/Set, oldById/newById 為以字串主鍵建立之列 Map
263
+ */
264
+ function diffRefTargets(p) {
265
+ let keyTable = p ? p.keyTable : ''
266
+ let rs = { renames: [], rename: new Map(), delNames: [], del: new Set(), oldById: new Map(), newById: new Map() }
267
+ if (!hasOwn(REF_OF, keyTable)) {
268
+ return rs
269
+ }
270
+ let cascadeRename = !(p.opt &amp;&amp; p.opt.cascadeRename === false)
271
+ let rowsOld = Array.isArray(p.rowsOld) ? p.rowsOld : []
272
+ let rowsNew = Array.isArray(p.rowsNew) ? p.rowsNew : []
273
+
274
+ //oldById, newById: 只認字串主鍵(缺 id 之新列為新增, 不對應既有列)
275
+ let oldById = rs.oldById
276
+ rowsOld.forEach((r) => {
277
+ if (r !== null &amp;&amp; typeof r === 'object' &amp;&amp; isStr(r.id)) {
278
+ oldById.set(r.id, r)
279
+ }
280
+ })
281
+ let newById = rs.newById
282
+ rowsNew.forEach((r) => {
283
+ if (r !== null &amp;&amp; typeof r === 'object' &amp;&amp; isStr(r.id)) {
284
+ newById.set(r.id, r)
285
+ }
286
+ })
287
+
288
+ //renames: 同 id 且名稱皆為字串而不同; keptNames: 同 id 同名之存續列之名稱
289
+ let keptNames = new Set()
290
+ newById.forEach((rn, id) => {
291
+ let ro = oldById.get(id)
292
+ if (ro === undefined) {
293
+ return
294
+ }
295
+ let from = entName(ro, keyTable)
296
+ let to = entName(rn, keyTable)
297
+ if (from !== null &amp;&amp; from === to) {
298
+ keptNames.add(from)
299
+ }
300
+ if (cascadeRename &amp;&amp; keyTable !== 'targets' &amp;&amp; from !== null &amp;&amp; to !== null &amp;&amp; from !== to) {
301
+ rs.renames.push({ id, from, to })
302
+ rs.rename.set(from, to)
303
+ }
304
+ })
305
+
306
+ //delNames: 舊列 id 不在新表, 其名稱不再有同 id 同名之存續列者
307
+ oldById.forEach((ro, id) => {
308
+ if (newById.has(id)) {
309
+ return
310
+ }
311
+ let name = entName(ro, keyTable)
312
+ if (name !== null &amp;&amp; !keptNames.has(name)) {
313
+ rs.del.add(name)
314
+ }
315
+ })
316
+ rs.delNames = [...rs.del]
317
+
318
+ return rs
319
+ }
320
+
321
+
322
+ /**
323
+ * 計算「被引用表之寫入」對引用端授權字串之連帶更新計畫(改名級聯+刪除連帶清理)
324
+ *
325
+ * 改名與刪除之判定(身分以主鍵 id、同時套用、歷史同名只刪其一不清理、刪後同名新增不繼承)見 diffRefTargets.
326
+ * 拒絕(整次不寫入, 收嚴+提示, 原則 3、適用表 m):
327
+ * renameCascadeDupGroup 被改名者屬歷史同名組(庫內另有同名列)且該名稱仍被引用(級聯會把原綁各筆之交集的引用轉給被改名者, 外審 A.1)——須先清理再改名; 該名稱無人引用則不擋(歷史同名之清理路徑)
328
+ * renameCascadeNameInUse 新名為「仍被啟用中引用但無任何實體」之懸空名(級聯後舊引用與新實體混成一項, 授權被新名稱接管, Q4)
329
+ * renameCascadeConflict 某容器內新名與另一既有項目相撞, 無法自動改寫
330
+ * renameCascadeUnsupported 某容器內含無法安全重新編碼之內容(JSON5 之 Infinity/NaN 數值), 命中而須改寫時無法自動改寫(改刪除時亦同)
331
+ * opt.cascadeRename 為 false 時不做改名級聯(亦不做上述改名相關拒絕; 舊行為, 引用成為懸空); 刪除連帶清理不受此設定影響.
332
+ *
333
+ * @param {Object} p 輸入參數物件
334
+ * @param {String} p.keyTable 輸入被寫入之表, 'grups'、'pemis' 或 'targets'; 其他回傳空計畫
335
+ * @param {Array} p.rowsOld 輸入庫內該表全部列(寫入前)
336
+ * @param {Array} p.rowsNew 輸入寫入後該表全部列(後台為送入之整表; 外部同步為他 from 之既有列加本批列)
337
+ * @param {Array} p.holdersOld 輸入引用端表全部列(users/grups/pemis, 寫入前)
338
+ * @param {Object} [p.opt={}] 輸入設定物件
339
+ * @param {Boolean} [p.opt.cascadeRename=true] 輸入是否做改名級聯, 預設true
340
+ * @returns {Object} 回傳 { err, keyTable, holder, field, renames, delNames, changes, nHolders, nRenameHolders, nDelHolders, nExternal }:
341
+ * err 為 null 或拒絕之語系鍵; renames 為 [{ id, from, to }]; delNames 為須清理之名稱; changes 為須改寫之引用端列 [{ id, field, before, after, nRename, nDel, from }](before/after 為該欄位字串);
342
+ * nHolders 為須改寫之引用端列數, nRenameHolders/nDelHolders 為受改名/刪除影響之列數(一列可兩者皆計), nExternal 為其中來源 from 非空(外部應用)之列數
343
+ * @example
344
+ *
345
+ * let p = planRefChanges({
346
+ * keyTable: 'grups',
347
+ * rowsOld: [{ id: 'g1', name: 'A' }],
348
+ * rowsNew: [{ id: 'g1', name: 'B' }],
349
+ * holdersOld: [{ id: 'u1', cgrups: '{"A":{"mode":"OR","isActive":"y"}}' }],
350
+ * })
351
+ * console.log(p.changes[0].after)
352
+ * // => {"B":{"mode":"OR","isActive":"y"}}
353
+ *
354
+ */
355
+ function planRefChanges(p) {
356
+ let keyTable = p ? p.keyTable : ''
357
+ let ref = hasOwn(REF_OF, keyTable) ? REF_OF[keyTable] : null
358
+ let plan = {
359
+ err: null,
360
+ keyTable,
361
+ holder: ref ? ref.holder : '',
362
+ field: ref ? ref.field : '',
363
+ renames: [],
364
+ delNames: [],
365
+ changes: [],
366
+ nHolders: 0,
367
+ nRenameHolders: 0,
368
+ nDelHolders: 0,
369
+ nExternal: 0,
370
+ }
371
+ if (ref === null) {
372
+ return plan
373
+ }
374
+ let holders = Array.isArray(p.holdersOld) ? p.holdersOld : []
375
+
376
+ //renames, delNames: 只依被引用表自身之新舊列判定(與引用端無關)
377
+ let d0 = diffRefTargets(p)
378
+ let { oldById, newById, rename, del } = d0
379
+ plan.renames = d0.renames
380
+ plan.delNames = d0.delNames
381
+ if (rename.size === 0 &amp;&amp; del.size === 0) {
382
+ return plan
383
+ }
384
+
385
+ //防禦: 同批兩列改名為同一新名(或兩個舊名映射到同一新名)——引用改寫後兩項併為一項而改變授權; 上游名稱唯一檢查(findTouchedDup/findDupIndex/前端 getNameErrs)本會擋下, 此處不依賴它(外審 minimax R6)
386
+ let namesTo = new Set()
387
+ for (let rn of plan.renames) {
388
+ if (namesTo.has(rn.to)) {
389
+ plan.err = 'renameCascadeConflict'
390
+ return plan
391
+ }
392
+ namesTo.add(rn.to)
393
+ }
394
+
395
+ //解碼各引用端列一次(歷史同名組檢查、接管檢查與改寫共用)
396
+ let decoded = holders.map((h) => {
397
+ let str = (h !== null &amp;&amp; typeof h === 'object') ? h[ref.field] : undefined
398
+ return { h, str, kp: decodeKp(str) }
399
+ }).filter((d) => d.h !== null &amp;&amp; typeof d.h === 'object' &amp;&amp; isStr(d.h.id)) //缺字串 id 之引用端列無從寫回, 不納入
400
+
401
+ //歷史同名組之改名: 庫內另有同名列且該名稱仍被引用(含停用項)——引用原綁「各筆之交集」, 把引用轉給被改名者會改變授權(外審 A.1); 無人引用則無引用可轉, 不擋(清理歷史同名之改名路徑)
402
+ let dupRenames = plan.renames.filter((rn) => {
403
+ let nSame = 0
404
+ oldById.forEach((ro) => {
405
+ if (entName(ro, keyTable) === rn.from) {
406
+ nSame++
407
+ }
408
+ })
409
+ return nSame >= 2
410
+ })
411
+ if (dupRenames.length > 0) {
412
+ let namesDup = new Set(dupRenames.map((rn) => rn.from))
413
+ for (let d of decoded) {
414
+ if (d.kp !== null &amp;&amp; listEntries(d.kp, ref.kind).some((e) => e.key !== null &amp;&amp; namesDup.has(e.key))) {
415
+ plan.err = 'renameCascadeDupGroup'
416
+ return plan
417
+ }
418
+ }
419
+ }
420
+
421
+ //新名接管檢查: 新名無任何舊實體(或其舊實體不是在本次一併改走/刪除), 而仍被啟用中之引用指向(懸空名)
422
+ if (plan.renames.length > 0) {
423
+ let namesOld = new Set()
424
+ oldById.forEach((ro) => {
425
+ let n = entName(ro, keyTable)
426
+ if (n !== null) {
427
+ namesOld.add(n)
428
+ }
429
+ })
430
+ let ghosts = new Set()
431
+ plan.renames.forEach((rn) => {
432
+ if (!namesOld.has(rn.to)) {
433
+ ghosts.add(rn.to)
434
+ }
435
+ })
436
+ if (ghosts.size > 0) {
437
+ for (let d of decoded) {
438
+ if (d.kp === null) {
439
+ continue
440
+ }
441
+ for (let e of listEntries(d.kp, ref.kind)) {
442
+ if (e.key !== null &amp;&amp; ghosts.has(e.key) &amp;&amp; e.isActive === 'y') {
443
+ plan.err = 'renameCascadeNameInUse'
444
+ return plan
445
+ }
446
+ }
447
+ }
448
+ }
449
+ }
450
+
451
+ //改寫
452
+ for (let d of decoded) {
453
+ let r = rewriteRefs(d.str, { kind: ref.kind, rename, del })
454
+ if (r.conflict || r.unsafe) {
455
+ plan.err = r.conflict ? 'renameCascadeConflict' : 'renameCascadeUnsupported'
456
+ plan.changes = []
457
+ plan.nHolders = 0
458
+ plan.nRenameHolders = 0
459
+ plan.nDelHolders = 0
460
+ plan.nExternal = 0
461
+ return plan
462
+ }
463
+ if (!r.changed) {
464
+ continue
465
+ }
466
+ let from = (isStr(d.h.from) &amp;&amp; d.h.from !== '') ? d.h.from : ''
467
+ plan.changes.push({ id: d.h.id, field: ref.field, before: d.str, after: r.str, nRename: r.nRename, nDel: r.nDel, from })
468
+ if (r.nRename > 0) {
469
+ plan.nRenameHolders++
470
+ }
471
+ if (r.nDel > 0) {
472
+ plan.nDelHolders++
473
+ }
474
+ if (from !== '') {
475
+ plan.nExternal++
476
+ }
477
+ }
478
+ plan.nHolders = plan.changes.length
479
+
480
+ return plan
481
+ }
482
+
483
+
484
+ /**
485
+ * 為「寫入後整表」中未帶主鍵 id 之列補規劃用之 id(僅供 diffRefTargets/planRefChanges, 不改寫入之列)
486
+ *
487
+ * 後台整表寫入(API 直打)與外部同步之列可不帶 id, 寫入時由 ORM 補隨機主鍵, 即每次皆為「舊列被刪、新列被新增」而身分不穩定; 若以 id 判定會把每次寫入都當成全部刪除再新增而清空引用.
488
+ * 故未帶 id 之列(grups/pemis)改以「名稱」對應 rowsPool 中 id 未被本批帶用之既有列(同名即同一實體, 不算刪除再新增; 同名多列依序對應); 名稱不同者視為新實體(給不與任何既有主鍵相同之規劃 id).
489
+ * 有限數字之 id 取其字串形式(同寫入前之主鍵正規化); targets 無此問題(id 即名稱, 必帶).
490
+ *
491
+ * @param {String} keyTable 輸入被寫入之表名
492
+ * @param {Array} rowsPool 輸入可供對應之既有列(後台為該表全部既有列; 外部同步為同一 from 之既有列)
493
+ * @param {Array} rows 輸入本批列
494
+ * @param {Array} [rowsAvoid=[]] 輸入規劃 id 須避開之其他既有列(外部同步之他 from 列), 預設空
495
+ * @returns {Array} 回傳補好規劃 id 之新列陣列(複本)
496
+ */
497
+ function fillIdlessIds(keyTable, rowsPool, rows, rowsAvoid = []) {
498
+ let toId = (v) => {
499
+ let id = (v !== null &amp;&amp; typeof v === 'object') ? v.id : undefined
500
+ if (isStr(id) &amp;&amp; id !== '') {
501
+ return id
502
+ }
503
+ return (typeof id === 'number' &amp;&amp; Number.isFinite(id)) ? String(id) : null
504
+ }
505
+ let idsGiven = new Set()
506
+ rows.forEach((v) => {
507
+ let id = toId(v)
508
+ if (id !== null) {
509
+ idsGiven.add(id)
510
+ }
511
+ })
512
+ let pool = new Map()
513
+ rowsPool.forEach((r) => {
514
+ if (r !== null &amp;&amp; typeof r === 'object' &amp;&amp; isStr(r.id) &amp;&amp; !idsGiven.has(r.id) &amp;&amp; isStr(r.name)) {
515
+ if (!pool.has(r.name)) {
516
+ pool.set(r.name, [])
517
+ }
518
+ pool.get(r.name).push(r.id)
519
+ }
520
+ })
521
+ //used: 已被占用之 id(pool 與 rowsAvoid 之既有列之全部主鍵與本批已帶之 id), 新實體之規劃 id 須避開; 外部同步之 pool 只含同 from 之列, 他 from 之列經 rowsAvoid 傳入(其主鍵仍屬寫入後整表, 撞號會使規劃把兩個實體併成一個)
522
+ let used = new Set(idsGiven)
523
+ ;[...rowsPool, ...rowsAvoid].forEach((r) => {
524
+ if (r !== null &amp;&amp; typeof r === 'object' &amp;&amp; isStr(r.id)) {
525
+ used.add(r.id)
526
+ }
527
+ })
528
+ return rows.map((v, k) => {
529
+ let id = toId(v)
530
+ if (id === null &amp;&amp; keyTable !== 'targets' &amp;&amp; v !== null &amp;&amp; typeof v === 'object' &amp;&amp; isStr(v.name) &amp;&amp; pool.has(v.name) &amp;&amp; pool.get(v.name).length > 0) {
531
+ id = pool.get(v.name).shift()
532
+ }
533
+ if (id === null) {
534
+ let n = k
535
+ while (used.has(`__new__${n}`)) { //新實體: 不與任何既有主鍵或本批已帶 id 相同
536
+ n++
537
+ }
538
+ id = `__new__${n}`
539
+ used.add(id)
540
+ }
541
+ return { ...v, id }
542
+ })
543
+ }
544
+
545
+
546
+ export { REF_OF, rewriteRefs, diffRefTargets, planRefChanges, fillIdlessIds, keyOf, entName, decodeKp, listEntries } //keyOf/entName/decodeKp/listEntries: run07 新增匯出(行為不變), 供 getAddImpact 沿用同一套引用解碼與名稱比對口徑
547
+ export default planRefChanges
548
+ </code></pre>
549
+ </article>
550
+ </section>
551
+
552
+
553
+
554
+
555
+
556
+
557
+ </div>
558
+
559
+ <br class="clear">
560
+
561
+ <footer>
562
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 10:56:39 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
563
+ </footer>
564
+
565
+ <script>prettyPrint();</script>
566
+ <script src="scripts/polyfill.js"></script>
567
+ <script src="scripts/linenumber.js"></script>
568
+
569
+
570
+
571
+ </body>
572
+ </html>