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,335 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+
5
+ <meta charset="utf-8">
6
+ <title>server/tools/authRefsClean.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">server/tools/authRefsClean.mjs</h1>
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+ <section>
47
+ <article>
48
+ <pre class="prettyprint source linenums"><code>//懸空引用之一次性清理(升版工具; 規則帳本 R-REF-CASCADE-01 之配套): 稽核 users.cgrups/grups.cpemis/pemis.crules 中查無實體之引用, 預設只列不改, apply 時才寫入.
49
+ //由專案根之 g_authRefsClean.mjs 呼叫(連專案資料庫); 邏輯抽成可注入 woItems 之函數, 供單元測試以臨時資料庫驗證(test/unit-refAudit.test.mjs).
50
+ //
51
+ //apply 之順序(任一步失敗即中止, 其後之步驟不執行; 備份失敗時零寫入):
52
+ // 1. 讀取四表並稽核(src/js/refAudit.mjs); 無須清理者不備份、不寫入
53
+ // 2. (a) 備份整個資料庫(getDb.backup(woItems), 落點為模組載入時之工作目錄之 backup-時間.json, 同 g_backupDb.mjs; 可由 backupDb 注入)
54
+ // 3. (b) 另匯出將被改寫之三張引用端表(users/grups/pemis)全部列(並附 targets 供稽核重現)為 json(backupPath; 不覆寫既有檔), 寫後讀回驗證
55
+ // 4. 重新讀取引用端表, 逐筆確認 before 仍等於庫內現值(稽核後被他人改動者中止, 免以過期資料覆蓋)
56
+ // 5. 寫入: woItems[表].save([{ id, [欄位]: 新字串 }], { autoInsert: false })——只改該欄位, 列已不存在時不得補出只有一欄之殘列; 逐筆結果 ok 非 1、n 為 0 或有 nInserted 者視為失敗並列出
57
+ // 6. 重新讀取並稽核: 懸空須為 0、變更之列只有該欄位改變、其餘列逐位元不變, 否則視為失敗
58
+ // 例外(已知殘留, 非失敗): 含無法安全重新編碼之內容(JSON5 Infinity/NaN, 重新編碼會變成 null 而改指別的規則名; 見 refCascade.rewriteRefs 之 unsafe)之授權欄位不清理,
59
+ // 其懸空項目寫前寫後皆在, 逐筆須與寫前相同(多或少即失敗), 並於報告列為「須人工處理」(res.unsafe/res.residue)
60
+ //備份檔路徑: 以 flag wx 建立(同名既有檔、同一檔之別名——大小寫不同、相對/絕對、硬連結——一律拒絕、不覆寫, 失敗於 backupRows 階段、零寫入)
61
+ //備份檔含 email 等個資, 請妥善保管、勿外傳、勿納入版控.
62
+
63
+ import fs from 'fs'
64
+ import path from 'path'
65
+ import { isDeepStrictEqual } from 'util'
66
+ import getDb from 'w-serv-orm/src/getDb.mjs'
67
+ import now2strp from 'wsemi/src/now2strp.mjs'
68
+ import { buildReport, formatReport } from './authRefsAudit.mjs'
69
+ import { isJsonLossy } from './fromTools.mjs'
70
+
71
+
72
+ const TABLES = ['users', 'grups', 'pemis', 'targets']
73
+ const HOLDERS = ['users', 'grups', 'pemis']
74
+
75
+
76
+ let arr = (v) => (Array.isArray(v) ? v : [])
77
+ let msgOf = (err) => (err &amp;&amp; err.message ? err.message : String(err))
78
+ let byId = (rows) => new Map(arr(rows).map((r) => [r.id, r]))
79
+
80
+
81
+ //readTables: 讀取四表全部列
82
+ async function readTables(woItems) {
83
+ let rows = {}
84
+ for (let t of TABLES) {
85
+ let rs = await woItems[t].select()
86
+ if (!Array.isArray(rs)) {
87
+ throw new Error(`select ${t} did not return an array`)
88
+ }
89
+ rows[t] = rs
90
+ }
91
+ return rows
92
+ }
93
+
94
+
95
+ /**
96
+ * 稽核並(apply 時)清理懸空引用
97
+ *
98
+ * @param {Object} p 輸入參數物件
99
+ * @param {Object} p.woItems 輸入 ORM 資料表物件(users、grups、pemis、targets 各有 select/save)
100
+ * @param {Boolean} [p.apply=false] 輸入是否寫入, 預設 false(只列不改)
101
+ * @param {String} [p.backupPath] 輸入引用端表之備份檔路徑, 預設 ./authRefsClean-backup-&lt;時間戳>.json(相對工作目錄)
102
+ * @param {Function} [p.backupDb] 輸入資料庫備份函數(回傳 Promise, reject 表失敗), 預設 getDb.backup(woItems)
103
+ * @param {Function} [p.log] 輸入輸出文字之函數, 預設 console.log
104
+ * @returns {Promise&lt;Object>} 回傳 { ok, apply, stage, err, audit, auditAfter, dbBackup, backupPath, written, failed, stale }; ok 為 false 時 stage 為失敗之階段(check/select/backupFidelity/backupDb/backupRows/stale/write/verify), err 為訊息; 不 reject
105
+ */
106
+ async function runClean(p = {}) {
107
+ let woItems = p.woItems
108
+ let apply = p.apply === true
109
+ let log = typeof p.log === 'function' ? p.log : console.log
110
+ let backupDb = typeof p.backupDb === 'function' ? p.backupDb : () => getDb.backup(woItems)
111
+ let res = { ok: false, apply, stage: 'check', err: '', audit: null, auditAfter: null, dbBackup: false, backupPath: '', written: [], failed: [], stale: [], unsafe: [], residue: [] }
112
+ let fail = (stage, err) => {
113
+ res.ok = false
114
+ res.stage = stage
115
+ res.err = msgOf(err)
116
+ log(`失敗(階段 ${stage}): ${res.err}`)
117
+ if (res.written.length > 0 || res.failed.length > 0) {
118
+ log(`已寫入 ${res.written.length} 列, 失敗 ${res.failed.length} 列${res.failed.length > 0 ? ': ' + res.failed.map((f) => `${f.table} id=${f.id}(${f.err})`).join('; ') : ''}; 請依備份檔還原或重跑本工具`)
119
+ }
120
+ return res
121
+ }
122
+
123
+ //check
124
+ if (woItems === null || typeof woItems !== 'object') {
125
+ return fail('check', 'invalid woItems')
126
+ }
127
+ for (let t of TABLES) {
128
+ if (!woItems[t] || typeof woItems[t].select !== 'function' || (apply &amp;&amp; typeof woItems[t].save !== 'function')) {
129
+ return fail('check', `woItems.${t} lacks select${apply ? '/save' : ''}`)
130
+ }
131
+ }
132
+
133
+ //select, audit
134
+ let rows0 = null
135
+ try {
136
+ rows0 = await readTables(woItems)
137
+ }
138
+ catch (err) {
139
+ return fail('select', err)
140
+ }
141
+ let rp = buildReport(rows0)
142
+ res.audit = rp
143
+ res.unsafe = rp.unsafe
144
+ log(formatReport(rp))
145
+ let unsafeNote = rp.unsafe.length > 0 ? `另有 ${rp.unsafe.length} 個授權欄位含無法安全重新編碼之內容(JSON5 Infinity/NaN), 本工具不清理(其懸空項目仍在), 須人工處理: ${rp.unsafe.map((u) => `${u.table} id=${u.id}`).join('、')}` : ''
146
+ if (!apply) {
147
+ log('')
148
+ log('(預設只列不改; 確認清單後加 --apply 才寫入, 寫入前會自動備份)')
149
+ res.ok = true
150
+ res.stage = 'audit'
151
+ return res
152
+ }
153
+ if (rp.changes.length === 0) {
154
+ log('')
155
+ log('無須清理(沒有可改寫之懸空引用), 不備份、不寫入.')
156
+ if (unsafeNote !== '') {
157
+ log(unsafeNote)
158
+ }
159
+ res.ok = true
160
+ res.stage = 'audit'
161
+ return res
162
+ }
163
+
164
+ //備份保真(run08 外審 sol #2, 與 fromTools.backupBefore 同一判準 isJsonLossy): 引用端表備份為 JSON, NaN/±Infinity/bigint/Date/undefined/Buffer 等非 JSON 值會被存成 null 或別型,
165
+ //依備份還原即遺失原值而筆數回驗不會發現; 列內含此類值時於任何備份與寫入之前中止(零寫入), 須人工處理該列
166
+ res.stage = 'backupFidelity'
167
+ for (let t of TABLES) {
168
+ if (isJsonLossy(rows0[t])) {
169
+ return fail('backupFidelity', `backup would be lossy: table ${t} has a row that does not survive a JSON round trip (NaN, Infinity, bigint, Date, undefined, Buffer...); nothing was written. Please fix that row first`)
170
+ }
171
+ }
172
+
173
+ //(a) 備份整個資料庫
174
+ res.stage = 'backupDb'
175
+ try {
176
+ await backupDb()
177
+ res.dbBackup = true
178
+ }
179
+ catch (err) {
180
+ return fail('backupDb', err)
181
+ }
182
+
183
+ //(b) 匯出引用端表全部列
184
+ res.stage = 'backupRows'
185
+ try {
186
+ let fp = path.resolve(p.backupPath || `./authRefsClean-backup-${now2strp()}.json`)
187
+ res.backupPath = fp
188
+ let body = { meta: { tool: 'authRefsClean', time: new Date().toISOString(), note: 'users/grups/pemis 為將被改寫之引用端表之全部列(寫入前); targets 未被改寫, 附上供稽核重現. 含個資, 請妥善保管.' } }
189
+ for (let t of TABLES) {
190
+ body[t] = rows0[t]
191
+ }
192
+ let txt = JSON.stringify(body)
193
+ fs.writeFileSync(fp, txt, { encoding: 'utf8', flag: 'wx' }) //wx: 既有檔不覆寫
194
+ let back = JSON.parse(fs.readFileSync(fp, 'utf8'))
195
+ for (let t of TABLES) {
196
+ if (!Array.isArray(back[t]) || back[t].length !== rows0[t].length) {
197
+ throw new Error(`backup file verification failed: ${t}`)
198
+ }
199
+ }
200
+ log(`已備份: 資料庫(backup-時間.json, 於工作目錄)、引用端表全部列 ${fp}`)
201
+ }
202
+ catch (err) {
203
+ return fail('backupRows', err)
204
+ }
205
+
206
+ //稽核後被他人改動者中止
207
+ res.stage = 'stale'
208
+ let rows1 = null
209
+ try {
210
+ rows1 = await readTables(woItems)
211
+ }
212
+ catch (err) {
213
+ return fail('stale', err)
214
+ }
215
+ for (let c of rp.changes) {
216
+ let r = byId(rows1[c.table]).get(c.id)
217
+ if (r === undefined || r[c.field] !== c.before) {
218
+ res.stale.push({ table: c.table, id: c.id })
219
+ }
220
+ }
221
+ if (res.stale.length > 0) {
222
+ return fail('stale', `${res.stale.length} rows changed or removed since the audit: ${res.stale.slice(0, 10).map((s) => `${s.table} id=${s.id}`).join(', ')}`)
223
+ }
224
+
225
+ //write
226
+ res.stage = 'write'
227
+ for (let t of HOLDERS) {
228
+ let cs = rp.changes.filter((c) => c.table === t)
229
+ if (cs.length === 0) {
230
+ continue
231
+ }
232
+ let rs = null
233
+ try {
234
+ rs = await woItems[t].save(cs.map((c) => ({ id: c.id, [c.field]: c.after })), { autoInsert: false })
235
+ }
236
+ catch (err) {
237
+ cs.forEach((c) => res.failed.push({ table: t, id: c.id, err: msgOf(err) }))
238
+ continue
239
+ }
240
+ if (!Array.isArray(rs) || rs.length !== cs.length) {
241
+ cs.forEach((c) => res.failed.push({ table: t, id: c.id, err: 'unexpected save result' }))
242
+ continue
243
+ }
244
+ cs.forEach((c, k) => {
245
+ let r = rs[k]
246
+ if (r &amp;&amp; r.ok === 1 &amp;&amp; r.n !== 0 &amp;&amp; !(r.nInserted > 0)) {
247
+ res.written.push({ table: t, id: c.id })
248
+ }
249
+ else {
250
+ res.failed.push({ table: t, id: c.id, err: (r &amp;&amp; r.err) ? String(r.err) : `save result ${JSON.stringify(r)}` })
251
+ }
252
+ })
253
+ }
254
+ if (res.failed.length > 0) {
255
+ return fail('write', `${res.failed.length} rows failed to save`)
256
+ }
257
+
258
+ //verify: 重新稽核須為 0 懸空, 變更之列只有該欄位改變, 其餘列逐位元不變
259
+ res.stage = 'verify'
260
+ try {
261
+ let rows2 = await readTables(woItems)
262
+ res.auditAfter = buildReport(rows2)
263
+ let newOf = new Map(rp.changes.map((c) => [`${c.table}\u0000${c.id}`, c]))
264
+ let bad = []
265
+ for (let t of HOLDERS) {
266
+ let m0 = byId(rows0[t])
267
+ let m2 = byId(rows2[t])
268
+ if (m2.size !== m0.size || rows2[t].length !== rows0[t].length) {
269
+ bad.push(`${t} row count ${rows0[t].length} -> ${rows2[t].length}`)
270
+ continue
271
+ }
272
+ for (let [id, r0] of m0) {
273
+ let c = newOf.get(`${t}\u0000${id}`)
274
+ let expect = c ? { ...r0, [c.field]: c.after } : r0
275
+ if (!isDeepStrictEqual(m2.get(id), expect)) {
276
+ bad.push(`${t} id=${id}`)
277
+ }
278
+ }
279
+ }
280
+ if (bad.length > 0) {
281
+ return fail('verify', `rows differ from expected after write: ${bad.slice(0, 10).join(', ')}`)
282
+ }
283
+ //已知殘留: 含無法安全重新編碼之內容(unsafe)之列不清理, 其懸空項目於寫前寫後皆在(須人工處理); 除此之外不得有懸空
284
+ let unsafeKeys = new Set(rp.unsafe.map((u) => `${u.table}\u0000${u.id}`))
285
+ res.residue = rp.items.filter((x) => unsafeKeys.has(`${x.table}\u0000${x.holderId}`))
286
+ let rest = res.auditAfter.items.filter((x) => !unsafeKeys.has(`${x.table}\u0000${x.holderId}`))
287
+ if (rest.length !== 0) {
288
+ return fail('verify', `${rest.length} dangling refs remain after clean`)
289
+ }
290
+ if (!isDeepStrictEqual(res.auditAfter.items, res.residue)) {
291
+ return fail('verify', 'residue of unsafe rows changed unexpectedly')
292
+ }
293
+ }
294
+ catch (err) {
295
+ return fail('verify', err)
296
+ }
297
+
298
+ res.ok = true
299
+ res.stage = 'done'
300
+ let n = (t) => res.written.filter((w) => w.table === t).length
301
+ log('')
302
+ log(`已清理: 寫入 ${res.written.length} 列(users ${n('users')}、grups ${n('grups')}、pemis ${n('pemis')}); 重新稽核懸空 ${res.auditAfter.items.length} 項${res.residue.length > 0 ? `(皆為下述不清理之列的既知殘留, 非錯誤)` : ''}. 請重啟服務並抽查授權.`)
303
+ if (unsafeNote !== '') {
304
+ log(unsafeNote)
305
+ }
306
+ return res
307
+ }
308
+
309
+
310
+ export { runClean, TABLES }
311
+ </code></pre>
312
+ </article>
313
+ </section>
314
+
315
+
316
+
317
+
318
+
319
+
320
+ </div>
321
+
322
+ <br class="clear">
323
+
324
+ <footer>
325
+ 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.
326
+ </footer>
327
+
328
+ <script>prettyPrint();</script>
329
+ <script src="scripts/polyfill.js"></script>
330
+ <script src="scripts/linenumber.js"></script>
331
+
332
+
333
+
334
+ </body>
335
+ </html>