w-web-perm 1.1.13 → 1.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -6
- package/dist/css/{app.f09c7433.css → app.f24c5e50.css} +1 -1
- package/dist/index.tmp +1 -1
- package/dist/js/app-legacy.033deb22.js +1 -0
- package/dist/js/app.033deb22.js +1 -0
- package/dist/js/{chunk-vendors.ece5a361.js → chunk-vendors-legacy.b0bbb5ac.js} +4 -4
- package/dist/js/{chunk-vendors-legacy.ece5a361.js → chunk-vendors.b0bbb5ac.js} +4 -4
- package/dist/w-web-perm.umd.js +2 -2
- package/dist/w-web-perm.umd.js.map +1 -1
- package/docs/WWebPerm.html +11 -6
- package/docs/global.html +11316 -5113
- package/docs/index.html +2 -2
- package/docs/server_WWebPerm.mjs.html +114 -334
- package/docs/server_cascadeWrite.mjs.html +5 -26
- package/docs/server_dataChannel.mjs.html +3 -3
- package/docs/server_keyCheck.mjs.html +150 -23
- package/docs/server_lockSave.mjs.html +2 -2
- package/docs/server_maskLog.mjs.html +2 -2
- package/docs/server_procCore.mjs.html +246 -62
- package/docs/server_staLogs_resolveShellWorker.mjs.html +2 -2
- package/docs/server_syncCore.mjs.html +533 -0
- package/docs/server_syncKeep.mjs.html +27 -20
- package/docs/server_tools_authRefsAudit.mjs.html +2 -2
- package/docs/server_tools_authRefsClean.mjs.html +2 -2
- package/docs/server_tools_fromTools.mjs.html +2 -2
- package/docs/server_tools_isMain.mjs.html +2 -2
- package/docs/server_tools_nameDupReport.mjs.html +40 -7
- package/docs/server_validateRows.mjs.html +2 -2
- package/docs/server_wrapInjected.mjs.html +2 -2
- package/docs/src_fetchJson.mjs.html +2 -2
- package/docs/src_getPermUserInfor.mjs.html +175 -0
- package/docs/src_js_applySaved.mjs.html +128 -0
- package/docs/src_js_cellEditing.mjs.html +163 -0
- package/docs/src_js_cvKpToArr.mjs.html +2 -2
- package/docs/src_js_cvKpToObj.mjs.html +2 -2
- package/docs/src_js_draftSync.mjs.html +83 -7
- package/docs/src_js_eachOwn.mjs.html +2 -2
- package/docs/src_js_editorRefRows.mjs.html +2 -2
- package/docs/src_js_externalRows.mjs.html +34 -10
- package/docs/src_js_externalUi.mjs.html +4 -8
- package/docs/src_js_findRefRows.mjs.html +2 -2
- package/docs/src_js_findTouchedDup.mjs.html +2 -2
- package/docs/src_js_fmtLossyMsg.mjs.html +2 -2
- package/docs/src_js_fromAudit.mjs.html +2 -2
- package/docs/src_js_getAddImpact.mjs.html +2 -2
- package/docs/src_js_getAuthWarn.mjs.html +2 -2
- package/docs/src_js_getCascadePreview.mjs.html +2 -2
- package/docs/src_js_getDelRefCount.mjs.html +2 -2
- package/docs/src_js_getEffMode.mjs.html +2 -2
- package/docs/src_js_getKpLossy.mjs.html +2 -2
- package/docs/src_js_getNameErrs.mjs.html +2 -2
- package/docs/src_js_getOwn.mjs.html +2 -2
- package/docs/src_js_getPropKey.mjs.html +2 -2
- package/docs/src_js_getRuleEnable.mjs.html +2 -2
- package/docs/src_js_isAuthMode.mjs.html +2 -2
- package/docs/src_js_joinNames.mjs.html +2 -2
- package/docs/src_js_legendFit.mjs.html +2 -2
- package/docs/src_js_pairRowsToSnapshot.mjs.html +2 -2
- package/docs/src_js_refAudit.mjs.html +2 -2
- package/docs/src_js_refCascade.mjs.html +2 -2
- package/docs/src_js_refIdentity.mjs.html +500 -0
- package/docs/src_js_relDialogSend.mjs.html +2 -2
- package/docs/src_js_rowDigest.mjs.html +75 -145
- package/docs/src_plugins_dataReloadGuard.mjs.html +2 -2
- package/docs/src_plugins_reuseDeletedIds.mjs.html +2 -2
- package/docs/src_plugins_submitGuard.mjs.html +2 -2
- package/g_provideGrups.mjs +1 -1
- package/g_providePemis.mjs +1 -1
- package/g_provideTargets.mjs +1 -1
- package/g_provideUsers.mjs +1 -1
- package/package.json +2 -2
- package/server/WWebPerm.mjs +112 -332
- package/server/cascadeWrite.mjs +3 -24
- package/server/dataChannel.mjs +1 -1
- package/server/keyCheck.mjs +148 -21
- package/server/procCore.mjs +244 -60
- package/server/procLang.mjs +72 -4
- package/server/provideTabs.mjs +6 -4
- package/server/syncCore.mjs +461 -0
- package/server/syncKeep.mjs +25 -18
- package/server/tools/nameDupReport.mjs +38 -5
- package/src/components/LayoutContentGrups.vue +57 -8
- package/src/components/LayoutContentPemis.vue +57 -8
- package/src/components/LayoutContentTargets.vue +38 -7
- package/src/components/LayoutContentUsers.vue +57 -8
- package/src/components/VeGrupBlngUsers.vue +128 -20
- package/src/components/VePemiBlngGrups.vue +128 -20
- package/src/getPermUserInfor.mjs +10 -0
- package/src/js/applySaved.mjs +56 -0
- package/src/js/cellEditing.mjs +91 -0
- package/src/js/draftSync.mjs +81 -5
- package/src/js/externalRows.mjs +32 -8
- package/src/js/externalUi.mjs +2 -6
- package/src/js/refIdentity.mjs +428 -0
- package/src/js/rowDigest.mjs +73 -143
- package/src/schema/contracts/grups.mjs +8 -0
- package/src/schema/contracts/index.mjs +17 -0
- package/src/schema/contracts/pemis.mjs +8 -0
- package/src/schema/contracts/targets.mjs +8 -0
- package/src/schema/contracts/users.mjs +13 -0
- package/src/schema/tables/grups.mjs +4 -0
- package/src/schema/tables/pemis.mjs +4 -0
- package/src/schema/tables/targets.mjs +4 -0
- package/src/schema/tables/users.mjs +4 -0
- package/srv.mjs +19 -0
- package/dist/js/app-legacy.8af87052.js +0 -1
- package/dist/js/app.8af87052.js +0 -1
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
<nav >
|
|
30
30
|
|
|
31
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#
|
|
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#activeNames">activeNames</a></li><li><a href="global.html#applyPurgePlan">applyPurgePlan</a></li><li><a href="global.html#applySavedRows">applySavedRows</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#bindCellEditingStop">bindCellEditingStop</a></li><li><a href="global.html#bindCellEditingStopRetry">bindCellEditingStopRetry</a></li><li><a href="global.html#buildRefDecl">buildRefDecl</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#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#checkRefIdentity">checkRefIdentity</a></li><li><a href="global.html#checkSubject">checkSubject</a></li><li><a href="global.html#checkSubjectIdentity">checkSubjectIdentity</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#collectChangedNames">collectChangedNames</a></li><li><a href="global.html#collectNewNames">collectNewNames</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#countRefMismatch">countRefMismatch</a></li><li><a href="global.html#countSrcKeyRenumber">countSrcKeyRenumber</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#findLoginIdClash">findLoginIdClash</a></li><li><a href="global.html#findLoginIdClashDetail">findLoginIdClashDetail</a></li><li><a href="global.html#findLoginUsers">findLoginUsers</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#getCascadePreview">getCascadePreview</a></li><li><a href="global.html#getChangedIdx">getChangedIdx</a></li><li><a href="global.html#getContract">getContract</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#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#getPermUserInfor">getPermUserInfor</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#getRefTarget">getRefTarget</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#getSavedTables">getSavedTables</a></li><li><a href="global.html#getSyncWarnLines">getSyncWarnLines</a></li><li><a href="global.html#hasDraftNow">hasDraftNow</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#idsOfName">idsOfName</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#isCellEditing">isCellEditing</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#loginIdOf">loginIdOf</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#newActiveNames">newActiveNames</a></li><li><a href="global.html#pairRowsToSnapshot">pairRowsToSnapshot</a></li><li><a href="global.html#parseRefDecl">parseRefDecl</a></li><li><a href="global.html#parseSubject">parseSubject</a></li><li><a href="global.html#pickClientOpt">pickClientOpt</a></li><li><a href="global.html#pickUniqueUser">pickUniqueUser</a></li><li><a href="global.html#planDialogSync">planDialogSync</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#recordPicks">recordPicks</a></li><li><a href="global.html#refsChanged">refsChanged</a></li><li><a href="global.html#removedActiveNames">removedActiveNames</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#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#snapRefIds">snapRefIds</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#syncCore">syncCore</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
33
|
|
|
34
34
|
</nav>
|
|
35
35
|
|
|
@@ -62,8 +62,10 @@ import procLang from './procLang.mjs'
|
|
|
62
62
|
import validateRowsBeforeWrite from './validateRows.mjs'
|
|
63
63
|
import planRefChanges, { REF_OF, diffRefTargets, fillIdlessIds } from '../src/js/refCascade.mjs'
|
|
64
64
|
import { restoreExternalOrder, dropServerOnlyDiff, restoreServerFields, checkExternalWrite, hasExternalDiff } from '../src/js/externalRows.mjs'
|
|
65
|
-
import { isBaselineValid,
|
|
66
|
-
import {
|
|
65
|
+
import { isBaselineValid, getContract } from '../src/js/rowDigest.mjs'
|
|
66
|
+
import { getRefTarget, parseSubject, parseRefDecl, checkSubjectIdentity, checkRefIdentity } from '../src/js/refIdentity.mjs'
|
|
67
|
+
import { getRejectKey } from 'w-serv-orm/src/baseline.mjs'
|
|
68
|
+
import { findTouchedDup, findDupIndex, withTabLocks, checkUsersWrite, mapKeyOf, loginIdOf, findLoginIdClashDetail } from './keyCheck.mjs'
|
|
67
69
|
import { checkCascadePlan, writeWithCascade } from './cascadeWrite.mjs'
|
|
68
70
|
|
|
69
71
|
|
|
@@ -110,16 +112,71 @@ function mergePickKeysOnly(ltdtDb, rowsIn, keyDetect, pickKeys) {
|
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
|
|
115
|
+
//拒收鍵(w-serv-orm baseline.mjs 之固定英文鍵) → 本套件語系鍵; 一律提示重新載入(規格第 6 節; R-BASELINE-01):
|
|
116
|
+
// baselineMismatch: 持舊畫面存檔(先動先贏); rowNotFound: 修改或刪除時該列已不存在(已被他人刪除, 不復活); baselineFormat: 基準格式或版本不識(含套內舊版指紋; 不得降級為不帶基準)
|
|
117
|
+
let kpRejectKey = {
|
|
118
|
+
baselineMismatch: 'saveStale',
|
|
119
|
+
rowNotFound: 'saveStale',
|
|
120
|
+
baselineFormat: 'saveBaselineFormat',
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 用戶端(kpFunExt 第 3 參)的 opt 只採納 baseline、returnRows(是否於回傳附寫入後之列, 布林)、subject(反向對話框之主體身分摘要)與 refs(清單頁新增引用之被引用實體身分宣告),
|
|
126
|
+
* 不接受 pickKeysOnly 等伺服端設定, 更不轉給 procOrm 之 opt(規格第 6 節: procOrm 之 opt 只能由伺服端程式自組);
|
|
127
|
+
* 未給(undefined/null)之 baseline 視為舊客戶端/外部應用(不帶, 本 major 允許; 不比原行為多放行任何寫入); 未給之 subject/refs 自 perm-run03 起僅限「本次存檔不改變引用」者,
|
|
128
|
+
* 改變引用(新增或移除啟用之引用項、新增列帶引用)而未附宣告 → 拒收 saveIdentityRequired(R-REF-IDENTITY-01 規則 7); 給了則一律驗證, 格式不合即拒收(baselineFormat/identityFormat; 不降級為不帶, R-SUBJECT-ID-01、R-REF-IDENTITY-01).
|
|
129
|
+
* subject/refs 在此原樣轉出(不信任其內容), 由 updateTabItems 解析驗證; 驗證所需之表與鎖皆由伺服端依 keyTable 自組(getRefTarget), 用戶端不得指定
|
|
130
|
+
*
|
|
131
|
+
* @param {*} opt 輸入用戶端給之 opt(任何值)
|
|
132
|
+
* @returns {Object} 回傳 { baseline?, returnRows?, subject?, refs? }
|
|
133
|
+
*/
|
|
134
|
+
function pickClientOpt(opt) {
|
|
135
|
+
let o = (opt !== null && typeof opt === 'object') ? opt : {}
|
|
136
|
+
let out = {}
|
|
137
|
+
if (o.baseline !== undefined && o.baseline !== null) {
|
|
138
|
+
out.baseline = o.baseline
|
|
139
|
+
}
|
|
140
|
+
if (o.returnRows === true) {
|
|
141
|
+
out.returnRows = true
|
|
142
|
+
}
|
|
143
|
+
if (o.subject !== undefined && o.subject !== null) {
|
|
144
|
+
out.subject = o.subject
|
|
145
|
+
}
|
|
146
|
+
if (o.refs !== undefined && o.refs !== null) {
|
|
147
|
+
out.refs = o.refs
|
|
148
|
+
}
|
|
149
|
+
return out
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
113
153
|
//mappingBy: 使用者對應鍵('id'|'email'|'name', WWebPerm 已正規化; 未給或其他值視同 'id'), 供 users 整表寫入的對應鍵檢查(R-PRINCIPLE-00 適用表 h, R-MAPKEY-03)
|
|
114
154
|
//cascadeRename: 群組/權限改名時是否連帶更新引用(預設 true; 只管改名, 刪除連帶清理不受影響, R-REF-CASCADE-01)
|
|
115
155
|
//lockExternalRows: 後台是否不得改外部列(from 為非空字串者)之身分/結構欄(預設 true, R-EXT-OWN-01; 'n' 時回到舊行為, 僅保留 from 型別檢查)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
156
|
+
//withTabLocks(別名 lockTabs), guardWrite: w-serv-orm 實例(WServOrm 回傳物件)之每表一鍵表鎖與原子防護(R-LOCK-CORE-01), 與外部同步(WWebPerm.syncAndReplaceTabs)共用同一個鎖管理器; 未注入即 TypeError(fail-closed, 不寫入)
|
|
157
|
+
function proc(woItems, procOrm, { srLog, withTabLocks: lockTabs, guardWrite, lockSave, mappingBy = 'id', cascadeRename = true, lockExternalRows = true }) {
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
//mapGuardErr: guardWrite/procOrm(mustExist)之拒收轉本套件語系鍵並記 srLog(只記錯誤鍵、原因與差異筆數, 不記列 id/個資值); rowsInvalid(庫內主鍵缺漏或重複)屬伺服端資料錯誤, 回 saveWriteFail 並記 log;
|
|
161
|
+
//其餘(語系鍵字串、ORM 失敗、非預期例外)原樣轉出(由 errLog/toOutKey 處置)
|
|
162
|
+
let mapGuardErr = (keyTable, userId) => (err) => {
|
|
163
|
+
let k = getRejectKey(err)
|
|
164
|
+
if (typeof k === 'string' && Object.prototype.hasOwnProperty.call(kpRejectKey, k)) {
|
|
165
|
+
let n = 0
|
|
166
|
+
let reason = k
|
|
167
|
+
if (err !== null && typeof err === 'object' && Array.isArray(err.changed)) {
|
|
168
|
+
n = err.changed.length + err.removed.length + err.added.length
|
|
169
|
+
reason = err.changed.length > 0 ? 'rowChanged' : (err.removed.length > 0 ? 'rowRemoved' : (err.added.length > 0 ? 'rowAdded' : (err.reordered === true ? 'rowReordered' : k)))
|
|
170
|
+
n = n > 0 ? n : (err.reordered === true ? 1 : 0)
|
|
171
|
+
}
|
|
172
|
+
srLog.error({ event: 'updateTabItems-stale-error', keyTable, userId, msg: kpRejectKey[k], reason, n })
|
|
173
|
+
return Promise.reject(kpRejectKey[k])
|
|
174
|
+
}
|
|
175
|
+
if (k === 'rowsInvalid') {
|
|
176
|
+
srLog.error({ event: 'updateTabItems-rows-invalid', keyTable, userId, msg: 'saveWriteFail', reason: 'rowsInvalid' })
|
|
177
|
+
return Promise.reject('saveWriteFail')
|
|
178
|
+
}
|
|
179
|
+
return Promise.reject(err)
|
|
123
180
|
}
|
|
124
181
|
|
|
125
182
|
|
|
@@ -136,13 +193,39 @@ function proc(woItems, procOrm, { srLog, kmx, lockSave, mappingBy = 'id', cascad
|
|
|
136
193
|
return Promise.reject('saveRowFieldInvalid')
|
|
137
194
|
}
|
|
138
195
|
|
|
139
|
-
//baseline: 草稿所依據之載入基準 [[id, digest], ...](F-26, R-BASELINE-01; 前端於開啟編輯時綁定); 未給(
|
|
196
|
+
//baseline: 草稿所依據之載入基準 [[id, digest], ...](h1 版, w-serv-orm baseline.mjs; F-26, R-BASELINE-01; 前端於開啟編輯時綁定); 未給(舊客戶端、外部應用)為 null;
|
|
197
|
+
//給了但格式或版本不識(含套內舊版指紋、非陣列)→ 拒收 saveBaselineFormat(提示重新載入, 不降級為不帶基準), 於任何讀寫之前
|
|
140
198
|
let baseline = get(opt, 'baseline', null)
|
|
141
199
|
if (baseline !== null && !isBaselineValid(baseline)) {
|
|
142
|
-
srLog.error({ event: 'updateTabItems-
|
|
143
|
-
return Promise.reject('
|
|
200
|
+
srLog.error({ event: 'updateTabItems-stale-error', keyTable, userId, msg: 'saveBaselineFormat', reason: 'baselineFormat', n: 0 })
|
|
201
|
+
return Promise.reject('saveBaselineFormat')
|
|
144
202
|
}
|
|
145
203
|
|
|
204
|
+
//被引用實體身分(perm-run02 範圍一; R-SUBJECT-ID-01、R-REF-IDENTITY-01; 使用者 2026-10-23 裁示「正反關聯與主體身份檢查, 前後端都要做」; 原則 4: 檢查與寫入同一把鎖、原則 6: 引用不得被同名新資料接管):
|
|
205
|
+
// subject: 反向關聯對話框(users 之主體為 grups、grups 之主體為 pemis)開框時之主體摘要 { id, name, saved }; refs: 清單頁新增或移除之啟用引用項其名稱在選取當時對應之實體 id 宣告 [{ name, ids }].
|
|
206
|
+
// 被引用表由伺服端依 keyTable 自組(getRefTarget, 用戶端不得指定), 於本表鎖之外層取其表鎖(階層: 被引用者先、引用端後, R-WRITE-LOCK-02)並新鮮讀取後驗證;
|
|
207
|
+
// perm-run03 範圍一(使用者 2026-10-23 裁示「這本來就該強制必帶吧? 要不然怎麼確認與驗證調用者身份?」): 宣告強制必帶——存檔改變了引用(新增或移除啟用之引用項、新增列帶引用)而未附宣告 → 拒收 saveIdentityRequired
|
|
208
|
+
// (於 write 內本表鎖下判定、零寫入; 無宣告者不取被引用表鎖: 無從驗證即拒收, 不需讀被引用表); 不改變引用之存檔(只改名、只改其他欄)不需宣告, 照常. 給了但格式不合或該表無授權引用欄 → 拒收 saveIdentityFormat(不降級), 於任何讀寫之前
|
|
209
|
+
let subjectRaw = get(opt, 'subject', null)
|
|
210
|
+
let refsRaw = get(opt, 'refs', null)
|
|
211
|
+
let refInfo = getRefTarget(keyTable) //本表有授權引用欄者(users/grups/pemis)為 { table, field, kind }; targets 為 null(無需宣告)
|
|
212
|
+
let refTarget = null
|
|
213
|
+
let subject = null
|
|
214
|
+
let refDecl = null
|
|
215
|
+
if (subjectRaw !== null || refsRaw !== null) {
|
|
216
|
+
refTarget = refInfo
|
|
217
|
+
subject = subjectRaw === null ? null : parseSubject(subjectRaw)
|
|
218
|
+
refDecl = refsRaw === null ? null : parseRefDecl(refsRaw)
|
|
219
|
+
//主體摘要只用於反向對話框(主體表 grups/pemis, 有 name 欄); pemis 之被引用表為 targets(無 name、無反向對話框), 帶 subject 屬格式錯誤
|
|
220
|
+
if (refTarget === null || (subjectRaw !== null && (subject === null || refTarget.table === 'targets')) || (refsRaw !== null && refDecl === null)) {
|
|
221
|
+
srLog.error({ event: 'updateTabItems-identity-error', keyTable, userId, msg: 'saveIdentityFormat', reason: 'format', n: 0 })
|
|
222
|
+
return Promise.reject('saveIdentityFormat')
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
//returnRows: 存檔成功後於同一表鎖內新鮮讀回該表全部列隨回傳附上(規格第 8 節): 前端立即以之更新畫面與基準, 不必等推播/輪詢; 不以前端送出之值當新基準(寫入時 clearXSS 等會改寫字串)
|
|
227
|
+
let returnRows = get(opt, 'returnRows', false) === true
|
|
228
|
+
|
|
146
229
|
//pickKeysOnly, 僅採納既有列之指定欄位變更(見 mergePickKeysOnly), 未給則整表 diff 寫入
|
|
147
230
|
let pickKeysOnly = get(opt, 'pickKeysOnly', null)
|
|
148
231
|
|
|
@@ -157,23 +240,9 @@ function proc(woItems, procOrm, { srLog, kmx, lockSave, mappingBy = 'id', cascad
|
|
|
157
240
|
})
|
|
158
241
|
}
|
|
159
242
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
//持鎖: 先取本表之鎖; 本表有改名或刪除而須改寫引用端時, 才於內層依階層再取引用端表之鎖(見下方「引用連帶」段). 皆經單一取鎖函數(R-WRITE-LOCK-02); 與外部同步(WWebPerm.syncAndReplaceTabs)共用同表互斥鍵(外審 sol R10)
|
|
164
|
-
return await lockSave(`updateTabItems:${keyTable}`, userId, () => withTabLocks(kmx, [keyTable], async () => {
|
|
165
|
-
|
|
166
|
-
//pickKeysOnly, 須於 mutex 內讀取資料庫為基底, 避免與並行寫入 race
|
|
167
|
-
if (isearr(pickKeysOnly)) {
|
|
168
|
-
//有限數字 id 先轉為字串形式再與庫內對應(外審 sol R7): mergePickKeysOnly 只認非空字串 id, 數字 id(如 1)對不上庫內之 '1' 而該列指派被靜默略過卻回成功(R-ORM-04 之同一正規化, 此處原晚於合併)
|
|
169
|
-
rows = map(rows, (r) => {
|
|
170
|
-
let id = get(r, 'id')
|
|
171
|
-
return (typeof id === 'number' && Number.isFinite(id)) ? { ...r, id: String(id) } : r
|
|
172
|
-
})
|
|
173
|
-
let ltdtDb = await woItems[keyTable].select()
|
|
174
|
-
rows = mergePickKeysOnly(ltdtDb, rows, keyDetect, pickKeysOnly)
|
|
175
|
-
srLog.info({ event: 'updateTabItems-pickKeysOnly', keyTable, userId, pickKeysOnly })
|
|
176
|
-
}
|
|
243
|
+
//prepRows: 寫入前之純函式前處理(不讀庫、不取鎖): ltdtmapping、重給 order、空值/重複檢查、主鍵正規化與重複主鍵檢查; 錯誤以 Promise.reject(語系鍵) 回報(次序與修正前同: 先於基準比對).
|
|
244
|
+
//非 pickKeysOnly 者在取鎖之前執行; pickKeysOnly(users 之 for:grups 模式)須以表鎖內新鮮讀取之庫內列為基底合併後才能前處理(見下方 write)
|
|
245
|
+
let prepRows = async (rows) => {
|
|
177
246
|
|
|
178
247
|
//ltdtmapping
|
|
179
248
|
rows = ltdtmapping(rows, ds[keyTable].keys)
|
|
@@ -254,19 +323,69 @@ function proc(woItems, procOrm, { srLog, kmx, lockSave, mappingBy = 'id', cascad
|
|
|
254
323
|
}
|
|
255
324
|
}
|
|
256
325
|
|
|
257
|
-
|
|
258
|
-
let ltdtOld = await woItems[keyTable].select()
|
|
326
|
+
return rows
|
|
259
327
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if (
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
//非 pickKeysOnly: 前處理在取鎖之前(純函式, 不讀庫)
|
|
331
|
+
let rowsPre = null
|
|
332
|
+
if (!isearr(pickKeysOnly)) {
|
|
333
|
+
rowsPre = await prepRows(rows)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
//雙擊防護(後端): 同一操作者之同表寫入處理中再送出即拒絕 'saveInProgress'(不排隊, 見 lockSave.mjs, ADR-025);
|
|
337
|
+
//占位包在表鎖之外層, 不同操作者之並行寫入仍由表鎖序列化、各自成功(ADR-012)
|
|
338
|
+
//表鎖=w-serv-orm 實例之 guardWrite(withTabLocks): 取鎖 → 新鮮讀取 → 載入基準比對 → write 於**同一把表鎖內**(規格第 6、7 節; R-LOCK-CORE-01), 防 lost update 與唯一性檢查被交錯略過;
|
|
339
|
+
//與外部同步(WWebPerm.syncAndReplaceTabs)共用同一個鎖管理器(外審 sol R10). 本表有改名或刪除而須改寫引用端時, 才於 write 內依階層再取引用端表之鎖(見下方「引用連帶」段, R-WRITE-LOCK-02)
|
|
340
|
+
//refRows: 被引用表(grups/pemis/targets)於引用表鎖內之新鮮讀取(僅帶 subject/refs 時; 其餘為 null), 供 subject 事前驗證與 write 內之 refs 驗證共用——兩者與本表寫入在同一段持鎖期間, 其間被引用表不會被任何入口改動(R-LOCK-CORE-01)
|
|
341
|
+
let refRows = null
|
|
342
|
+
|
|
343
|
+
//withRefLock: 未帶 subject/refs 者同修正前(僅本表鎖); 帶了: 先取被引用表鎖(外層, 階層較低)、新鮮讀取、驗主體身分(先於基準比對, 使「主體已被取代」優先於泛稱之 saveStale), 再進本表之 guardWrite
|
|
344
|
+
let withRefLock = (fn) => {
|
|
345
|
+
if (refTarget === null) {
|
|
346
|
+
return fn()
|
|
269
347
|
}
|
|
348
|
+
return withTabLocks(lockTabs, [refTarget.table], async () => {
|
|
349
|
+
refRows = await woItems[refTarget.table].select()
|
|
350
|
+
if (subject !== null) {
|
|
351
|
+
let si = checkSubjectIdentity({ subject, refRows })
|
|
352
|
+
if (si !== null) {
|
|
353
|
+
srLog.error({ event: 'updateTabItems-identity-error', keyTable, userId, msg: si.key, reason: si.reason, n: 1 })
|
|
354
|
+
return Promise.reject(si.key)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return await fn()
|
|
358
|
+
})
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return await lockSave(`updateTabItems:${keyTable}`, userId, () => withRefLock(() => guardWrite({
|
|
362
|
+
tables: [keyTable],
|
|
363
|
+
|
|
364
|
+
//新鮮讀取: 直讀 woItems(WServOrm 未開 useCache), 不經任何快取(規格第 12 節)
|
|
365
|
+
read: () => woItems[keyTable].select(),
|
|
366
|
+
|
|
367
|
+
//表級載入基準比對(R-PRINCIPLE-00 原則 7、使用者 2026-10-22 裁示原則 10「多人同時編輯以表雜湊比對, 先動先贏」; 規則帳本 R-BASELINE-01, ADR-037 Update(perm-run01)):
|
|
368
|
+
//有基準者, 草稿載入時之整表須與目前庫內整表相同(任一列內容摘要不同、列被增刪、名次不同即過期), 否則整次拒收 saveStale、零寫入, 不部分變更、不上鎖; 摘要依寫入契約(排除 order 與伺服器專有欄, 名次入基準).
|
|
369
|
+
//比對在 guardWrite 內、write 之前: 基準過期時下列各檢查之結果(撞名、新增列已存在、外部列守門…)皆以過期之畫面為前提, 應先回「請重新載入」而非誤導之其他訊息.
|
|
370
|
+
//無基準者(舊客戶端、外部應用)不比對(undefined 才跳過; null 不傳到此): 後台列維持最後寫入者勝(ADR-013), 外部列之指派欄改動另於下方拒收(saveBaselineRequired)
|
|
371
|
+
baseline: baseline === null ? undefined : baseline,
|
|
372
|
+
contract: getContract(keyTable),
|
|
373
|
+
|
|
374
|
+
write: async (ltdtOld) => {
|
|
375
|
+
|
|
376
|
+
//rows: 非 pickKeysOnly 為取鎖前已前處理者; pickKeysOnly 於鎖內以新鮮讀取之庫內列為基底合併(避免與並行寫入 race)再前處理
|
|
377
|
+
if (isearr(pickKeysOnly)) {
|
|
378
|
+
//有限數字 id 先轉為字串形式再與庫內對應(外審 sol R7): mergePickKeysOnly 只認非空字串 id, 數字 id(如 1)對不上庫內之 '1' 而該列指派被靜默略過卻回成功(R-ORM-04 之同一正規化, 此處原晚於合併)
|
|
379
|
+
rows = map(rows, (r) => {
|
|
380
|
+
let id = get(r, 'id')
|
|
381
|
+
return (typeof id === 'number' && Number.isFinite(id)) ? { ...r, id: String(id) } : r
|
|
382
|
+
})
|
|
383
|
+
rows = mergePickKeysOnly(ltdtOld, rows, keyDetect, pickKeysOnly)
|
|
384
|
+
srLog.info({ event: 'updateTabItems-pickKeysOnly', keyTable, userId, pickKeysOnly })
|
|
385
|
+
rows = await prepRows(rows)
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
rows = rowsPre
|
|
270
389
|
}
|
|
271
390
|
|
|
272
391
|
//外部列之 order 由外部擁有(R-EXT-OWN-01): 上方 order=k+1 重編不得改外部列之順序, 還原為庫內值(使其不因後台存檔而成為 diff)
|
|
@@ -287,7 +406,7 @@ function proc(woItems, procOrm, { srLog, kmx, lockSave, mappingBy = 'id', cascad
|
|
|
287
406
|
|
|
288
407
|
//users 之 email/對應鍵(R-PRINCIPLE-00 適用表 h, 規則帳本 R-MAPKEY-03): 遭遇衝突就收嚴, 但只擋「本次造成或觸及」者, 歷史既有之空值/重複原樣送回不拒(否則已有歷史資料之庫無法在後台修復, 連群組指派 pickKeysOnly 也會失敗);
|
|
289
408
|
//①全表 email 須為非空字串且不重複; ②mappingBy 為 email/name 時另對 isActive==='y' 之列檢查該欄位(含停用改啟用); 判準單一擁有者 keyCheck.checkUsersWrite(與外部同步 F-14 同口徑).
|
|
290
|
-
//放在 select 之後、saveNewRowExists 之前(次序同 pemis/grups 之 findTouchedDup), 於任何破壞性寫入之前(本函式在
|
|
409
|
+
//放在 select 之後、saveNewRowExists 之前(次序同 pemis/grups 之 findTouchedDup), 於任何破壞性寫入之前(本函式在 guardWrite 之表鎖內, R-WRITE-LOCK-01、R-LOCK-CORE-01); 被拒時資料表零變更.
|
|
291
410
|
//srLog 只記錯誤鍵、原因、欄位與列索引, 不記 email/name 之值(個資; 修正前重複時記 value)
|
|
292
411
|
if (keyTable === 'users') {
|
|
293
412
|
let ck = checkUsersWrite(rows, ltdtOld, mappingBy)
|
|
@@ -333,10 +452,20 @@ function proc(woItems, procOrm, { srLog, kmx, lockSave, mappingBy = 'id', cascad
|
|
|
333
452
|
//②from 型別與外部列守門(checkExternalWrite): 新增列 from 須為 ''、外部列不可刪/不可改 from 與身分結構欄, 指派欄可寫(草稿新鮮而仍違規者在此被拒);
|
|
334
453
|
//③無基準而修改外部列之指派欄: 無從證明快照新舊, Lv1 放行之指派欄不得因舊快照還原外部已撤銷之授權(外審 B.2), 依原則 3 拒收(saveBaselineRequired); 後台列無基準維持舊行為(最後寫入者勝, ADR-013).
|
|
335
454
|
//srLog 只記錯誤鍵、原因、筆數與來源名稱, 不記列 id/個資值
|
|
455
|
+
//(perm-run01, 寫入契約 serverOnly: 伺服器專有欄位〔建立者/建立時間/更新者/更新時間〕存檔不採用客戶端值〔規格第 4、6 節; R-CONTRACT-01〕:
|
|
456
|
+
//dropServerOnlyDiff/restoreServerFields 改為不分外部列或後台列、亦不論 lockExternalRows 一律套用〔all: true〕——僅伺服器專有欄不同之列視為無變更, 確有改動之列改帶庫內之專有欄位寫入, 客戶端持舊列〔或偽造〕不得回寫.)
|
|
336
457
|
let keysTab = ds[keyTable].keys
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
458
|
+
r = dropServerOnlyDiff({ keys: keysTab, r, ltdtOld, all: true })
|
|
459
|
+
r = restoreServerFields({ r, ltdtOld, all: true }) //改動之列帶庫內之伺服器管理欄: 持舊草稿(保留未存編輯期間外部同步重寫了該列)不得把較新之建立者/建立時間回退(外審 sol 第 3 點)
|
|
460
|
+
|
|
461
|
+
//引用身分(perm-run02 範圍一; R-REF-IDENTITY-01; perm-run03 範圍一起宣告強制必帶): 本次寫入之列(新增+改動)相對庫內舊列新增或移除之啟用引用項, 其名稱對應之被引用實體須與選取當時同一身分(同 id), 且宣告須完整;
|
|
462
|
+
//改變了引用卻未附宣告 → saveIdentityRequired; 被刪除、刪除後同名重建(id 不同)、同名新增實體皆整次拒收 saveRefChanged(零寫入): 不得把授權寫到(或移除到)同名新實體(原則 6). refRows 為被引用表鎖內之新鮮讀取(無宣告者未讀取, 為 null)
|
|
463
|
+
if (refInfo !== null) {
|
|
464
|
+
let ri = checkRefIdentity({ keyTable, decl: refDecl, subject, rowsNew: [...r.add, ...r.diff], rowsOld: ltdtOld, refRows })
|
|
465
|
+
if (ri !== null) {
|
|
466
|
+
srLog.error({ event: 'updateTabItems-identity-error', keyTable, userId, msg: ri.key, reason: ri.reason, n: ri.n })
|
|
467
|
+
return Promise.reject(ri.key)
|
|
468
|
+
}
|
|
340
469
|
}
|
|
341
470
|
let ex =checkExternalWrite({ keyTable, keys: keysTab, r, ltdtOld, lockExternalRows })
|
|
342
471
|
if (ex !== null) {
|
|
@@ -348,6 +477,23 @@ function proc(woItems, procOrm, { srLog, kmx, lockSave, mappingBy = 'id', cascad
|
|
|
348
477
|
return Promise.reject('saveBaselineRequired')
|
|
349
478
|
}
|
|
350
479
|
|
|
480
|
+
//users 登入識別撞值(mappingBy 為 'id' 時; perm-run04, 規則帳本 R-MAPKEY-04, 規格 dspec/外部匯入之定義與機制.md 第 9 節末 O1): 外部列以 srcKey、後台列以 id 對應外部 token 之使用者 id;
|
|
481
|
+
//perm-run05(主控 2026-10-24 收件裁示): 登入識別值須跨全表唯一——不論 isActive、含他來源外部列(登入查找不論有效與否計筆, 放行停用列之撞值, 其後轉有效即造成登入永久拒絕).
|
|
482
|
+
//被本次觸及=新增之列、或登入識別(類別〔後台/外部+來源 from〕+值〔loginIdOf: 後台列為 id、外部列為 srcKey〕)與庫內不同之列(外審 sol#2: lockExternalRows 為假時後台可改 from, 外部列改為後台列即換了登入識別而撞到他列;
|
|
483
|
+
//外部列守門啟用時 from 與 srcKey 不可改, 由上方 checkExternalWrite 先拒——故本檢查置於外部列守門之後, perm-run03 既有之錯誤鍵〔saveExternalRowChanged 等〕不變, 被觸及者實務上只有後台列).
|
|
484
|
+
//停用改啟用只是狀態改變, 登入識別不變, 不算被觸及(歷史撞值不因啟用而新擋; 登入時照拒). 只擋「本次造成或觸及」者(歷史既有撞值原樣送回不擋, 同 R-MAPKEY-03; 後台仍可修復).
|
|
485
|
+
//缺 id 之列無從比對(ORM 補隨機主鍵). 於任何寫入之前(procOrm 在下方). srLog 不記 id/srcKey 之值
|
|
486
|
+
if (keyTable === 'users' && mapKeyOf(mappingBy) === null) {
|
|
487
|
+
//sigOf: 登入識別之「類別+來源+值」(後台列 a/外部列 x:來源; 外審 sol 第二輪#1: 外部列改為後台列而 srcKey 恰等於 id 時值不變、類別已變, 仍須視為被觸及; perm-run05: 來源改變亦同)
|
|
488
|
+
let sigOf = (v) => `${isestr(v.from) ? `x:${v.from}` : 'a'}|${loginIdOf(v)}`
|
|
489
|
+
let loginOld = new Map(ltdtOld.filter((v) => v !== null && typeof v === 'object' && isestr(v.id)).map((v) => [v.id, sigOf(v)]))
|
|
490
|
+
let dClash = findLoginIdClashDetail(rows, (r) => !(isestr(r.id) && loginOld.has(r.id) && loginOld.get(r.id) === sigOf(r)))
|
|
491
|
+
if (dClash !== null) {
|
|
492
|
+
srLog.error({ event: 'updateTabItems-ckKey-error', keyTable, key: 'id', msg: 'loginIdCollision', reason: (dClash.kind === 'otherFrom') ? 'srcKeyOtherFrom' : ((dClash.kind === 'sameFrom') ? 'srcKeyClaimTwice' : 'adminIdIsSrcKey'), row: dClash.k })
|
|
493
|
+
return Promise.reject('loginIdCollision')
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
351
497
|
//寫入前驗證(規則帳本 R-ORM-01, ADR-026): 於第一個破壞步驟(del)之前擋下可預判之寫入失敗, 被拒時資料表零變更.
|
|
352
498
|
//why: 寫入順序為 del → insert → save, 而 w-serv-orm 1.1.8 起對「會被 clearXSS 改寫之主鍵」整批拒絕、LMDB 對 > 1978 B 之主鍵整批拒絕,
|
|
353
499
|
//修正前遇之即「舊列已刪、新列被拒」(改標的 id 時該列遺失, 2026-10-14 V3/V7 實測); 非主鍵欄位之標記則被靜默改寫(存值≠送入值).
|
|
@@ -366,11 +512,13 @@ function proc(woItems, procOrm, { srLog, kmx, lockSave, mappingBy = 'id', cascad
|
|
|
366
512
|
}
|
|
367
513
|
|
|
368
514
|
//writeMain
|
|
515
|
+
//修改只改不增、刪除未命中回報(規格第 6 節; R-ONLYMOD-01): del/save 帶 opt { mustExist: true }(伺服端自組, 不轉傳客戶端之任何參數), 列已不存在(同鎖內新鮮讀取之後仍不在, 理論上不會發生)→ 'rowNotFound' 零寫入、不復活;
|
|
516
|
+
//insert 為新增(主鍵已存在即略過之冪等新增, 不比對、不去重, 資料規則要求唯一者照其規則擋)
|
|
369
517
|
let writeMain = async () => {
|
|
370
518
|
|
|
371
519
|
//del
|
|
372
520
|
if (size(r.del) > 0) {
|
|
373
|
-
await procOrm(userId, keyTable, 'del', r.del) //須使用procOrm才有辦法自動給予相關欄位
|
|
521
|
+
await procOrm(userId, keyTable, 'del', r.del, { mustExist: true }) //須使用procOrm才有辦法自動給予相關欄位
|
|
374
522
|
// .catch((err) => {
|
|
375
523
|
// console.log('woItems[keyTable].del err', err)
|
|
376
524
|
// })
|
|
@@ -386,7 +534,7 @@ function proc(woItems, procOrm, { srLog, kmx, lockSave, mappingBy = 'id', cascad
|
|
|
386
534
|
|
|
387
535
|
//diff
|
|
388
536
|
if (size(r.diff) > 0) {
|
|
389
|
-
await procOrm(userId, keyTable, 'save', r.diff) //須使用procOrm才有辦法自動給予相關欄位
|
|
537
|
+
await procOrm(userId, keyTable, 'save', r.diff, { mustExist: true }) //須使用procOrm才有辦法自動給予相關欄位
|
|
390
538
|
// .catch((err) => {
|
|
391
539
|
// console.log('woItems[keyTable].save err', err)
|
|
392
540
|
// })
|
|
@@ -394,6 +542,27 @@ function proc(woItems, procOrm, { srLog, kmx, lockSave, mappingBy = 'id', cascad
|
|
|
394
542
|
|
|
395
543
|
}
|
|
396
544
|
|
|
545
|
+
//readAfter: 存檔成功後於同一表鎖內(含引用端鎖)新鮮讀回該表全部列(opt.returnRows 時; 規格第 8 節), 前端立即以之更新畫面與基準; 寫入已生效, 讀回失敗不使存檔失敗(回 null, 前端退回等推播), 失敗入 srLog
|
|
546
|
+
//holder 給了(引用連帶有改寫引用端表)時一併讀回引用端表(others[引用端表名])——該表亦被本次寫入改動, 前端同樣立即更新其基準, 免自己存檔後馬上去存引用端頁而被自己上一次之寫入誤擋
|
|
547
|
+
let readAfter = async (holder = null) => {
|
|
548
|
+
if (!returnRows) {
|
|
549
|
+
return null
|
|
550
|
+
}
|
|
551
|
+
try {
|
|
552
|
+
let rs = await woItems[keyTable].select()
|
|
553
|
+
let out = { rows: sortBy(rs, 'order'), others: {} }
|
|
554
|
+
if (holder !== null) {
|
|
555
|
+
let rh = await woItems[holder].select()
|
|
556
|
+
out.others[holder] = sortBy(rh, 'order')
|
|
557
|
+
}
|
|
558
|
+
return out
|
|
559
|
+
}
|
|
560
|
+
catch (err) {
|
|
561
|
+
srLog.error({ event: 'updateTabItems-readBack-error', keyTable, userId, msg: 'getDataError', raw: (typeof err === 'string') ? err : ((err && err.message) || String(err)) })
|
|
562
|
+
return null
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
397
566
|
//引用連帶(R-PRINCIPLE-00 原則 6、適用表 m/n, 規則帳本 R-REF-CASCADE-01, ADR-036): grups/pemis 改名(同 id 異名)與 grups/pemis/targets 刪除(舊 id 不在送入整表),
|
|
398
567
|
//須於同一次寫入內連帶更新/清理引用它之授權字串(users.cgrups、grups.cpemis、pemis.crules). 是否須改寫引用端只依本表新舊列判定(diffRefTargets), 故於持有本表鎖之下即可決定是否再取引用端表之鎖;
|
|
399
568
|
//取齊鎖之後才讀取引用端、規劃與預檢(新名撞懸空名、歷史同名組改名、容器內相撞、改寫後欄位驗證), 預檢與寫入涵蓋同一版本(外審 A.3), 被拒時資料表零變更.
|
|
@@ -403,7 +572,7 @@ function proc(woItems, procOrm, { srLog, kmx, lockSave, mappingBy = 'id', cascad
|
|
|
403
572
|
let kind = arrHas(keyTable, ['targets', 'pemis', 'grups']) ? diffRefTargets({ keyTable, rowsOld: ltdtOld, rowsNew: rowsPlan, opt: { cascadeRename } }) : null
|
|
404
573
|
if (kind !== null && (kind.renames.length > 0 || kind.delNames.length > 0)) {
|
|
405
574
|
let holder = REF_OF[keyTable].holder
|
|
406
|
-
return await withTabLocks(
|
|
575
|
+
return await withTabLocks(lockTabs, [holder], async () => {
|
|
407
576
|
let holdersOld = await woItems[holder].select()
|
|
408
577
|
let plan = planRefChanges({ keyTable, rowsOld: ltdtOld, rowsNew: rowsPlan, holdersOld, opt: { cascadeRename } })
|
|
409
578
|
let errPlan = plan.err !== null ? plan.err : checkCascadePlan(plan)
|
|
@@ -412,15 +581,16 @@ function proc(woItems, procOrm, { srLog, kmx, lockSave, mappingBy = 'id', cascad
|
|
|
412
581
|
return Promise.reject(errPlan)
|
|
413
582
|
}
|
|
414
583
|
await writeWithCascade({ plan, holdersOld, woItems, procOrm, userId, srLog, writeMain })
|
|
415
|
-
return
|
|
584
|
+
return await readAfter(plan.changes.length > 0 ? holder : null)
|
|
416
585
|
}, [keyTable])
|
|
417
586
|
}
|
|
418
587
|
|
|
419
588
|
await writeMain()
|
|
420
589
|
|
|
421
|
-
return
|
|
590
|
+
return await readAfter()
|
|
422
591
|
|
|
423
|
-
|
|
592
|
+
},
|
|
593
|
+
}).catch(mapGuardErr(keyTable, userId))))
|
|
424
594
|
}
|
|
425
595
|
|
|
426
596
|
|
|
@@ -436,48 +606,62 @@ function proc(woItems, procOrm, { srLog, kmx, lockSave, mappingBy = 'id', cascad
|
|
|
436
606
|
}
|
|
437
607
|
|
|
438
608
|
|
|
609
|
+
//outOf: 成功回傳. 預設為成功 msg key 字串(既有呼叫端不變); 客戶端帶 returnRows:true 且讀回成功時改回 { msg, rows }(rows=寫入後該表全部列, 鎖內新鮮讀取; 規格第 8 節)
|
|
610
|
+
// others: 引用連帶有改寫引用端表時, 引用端表之寫入後全部列(鍵為表名); 無則為空物件
|
|
611
|
+
let outOf = (after, opt) => {
|
|
612
|
+
return (get(opt, 'returnRows', false) === true && after !== null && typeof after === 'object' && Array.isArray(after.rows)) ? { msg: 'saveTabItemsSuccess', rows: after.rows, others: after.others } : 'saveTabItemsSuccess'
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
|
|
439
616
|
//updateTargets
|
|
440
617
|
let updateTargets = async (userId, rows, opt = {}) => {
|
|
618
|
+
let rowsAfter = null
|
|
619
|
+
let optc = pickClientOpt(opt)
|
|
441
620
|
try {
|
|
442
|
-
await updateTabItems('targets', userId, rows, 'id',
|
|
621
|
+
rowsAfter = await updateTabItems('targets', userId, rows, 'id', optc)
|
|
443
622
|
}
|
|
444
623
|
catch (err) { throw errLog('updateTargets-error', userId, err) }
|
|
445
624
|
srLog.info({ event: 'updateTargets-success', userId })
|
|
446
|
-
return
|
|
625
|
+
return outOf(rowsAfter, optc) //成功 msg key
|
|
447
626
|
}
|
|
448
627
|
|
|
449
628
|
|
|
450
629
|
//updatePemis
|
|
451
630
|
let updatePemis = async (userId, rows, opt = {}) => {
|
|
631
|
+
let rowsAfter = null
|
|
632
|
+
let optc = pickClientOpt(opt)
|
|
452
633
|
try {
|
|
453
|
-
await updateTabItems('pemis', userId, rows, 'name',
|
|
634
|
+
rowsAfter = await updateTabItems('pemis', userId, rows, 'name', optc)
|
|
454
635
|
}
|
|
455
636
|
catch (err) { throw errLog('updatePemis-error', userId, err) }
|
|
456
637
|
srLog.info({ event: 'updatePemis-success', userId })
|
|
457
|
-
return
|
|
638
|
+
return outOf(rowsAfter, optc) //成功 msg key
|
|
458
639
|
}
|
|
459
640
|
|
|
460
641
|
|
|
461
642
|
//updateGrups
|
|
462
643
|
let updateGrups = async (userId, rows, opt = {}) => {
|
|
644
|
+
let rowsAfter = null
|
|
645
|
+
let optc = pickClientOpt(opt)
|
|
463
646
|
try {
|
|
464
|
-
await updateTabItems('grups', userId, rows, 'name',
|
|
647
|
+
rowsAfter = await updateTabItems('grups', userId, rows, 'name', optc)
|
|
465
648
|
}
|
|
466
649
|
catch (err) { throw errLog('updateGrups-error', userId, err) }
|
|
467
650
|
srLog.info({ event: 'updateGrups-success', userId })
|
|
468
|
-
return
|
|
651
|
+
return outOf(rowsAfter, optc) //成功 msg key
|
|
469
652
|
}
|
|
470
653
|
|
|
471
654
|
|
|
472
655
|
//updateUsers, opt.pickKeysOnly 給 ['cgrups'] 時僅採納既有使用者之群組指派變更(modeEditUsers='for:grups'; 伺服端設定, WWebPerm 組入, 不接受用戶端給);
|
|
473
|
-
//opt.baseline
|
|
656
|
+
//opt.baseline/opt.returnRows 為用戶端給(其餘 opt 鍵不採納, 見 pickClientOpt 與 WWebPerm 之 kpFunExt)
|
|
474
657
|
let updateUsers = async (userId, rows, opt = {}) => {
|
|
658
|
+
let rowsAfter = null
|
|
475
659
|
try {
|
|
476
|
-
await updateTabItems('users', userId, rows, 'id', opt)
|
|
660
|
+
rowsAfter = await updateTabItems('users', userId, rows, 'id', opt)
|
|
477
661
|
}
|
|
478
662
|
catch (err) { throw errLog('updateUsers-error', userId, err) }
|
|
479
663
|
srLog.info({ event: 'updateUsers-success', userId })
|
|
480
|
-
return
|
|
664
|
+
return outOf(rowsAfter, opt) //成功 msg key
|
|
481
665
|
}
|
|
482
666
|
|
|
483
667
|
|
|
@@ -503,7 +687,7 @@ function toOutKey(err, keyFail = 'saveWriteFail') {
|
|
|
503
687
|
}
|
|
504
688
|
|
|
505
689
|
|
|
506
|
-
export { mergePickKeysOnly, toOutKey, isRowObj }
|
|
690
|
+
export { mergePickKeysOnly, toOutKey, isRowObj, pickClientOpt }
|
|
507
691
|
|
|
508
692
|
|
|
509
693
|
export default proc
|
|
@@ -521,7 +705,7 @@ export default proc
|
|
|
521
705
|
<br class="clear">
|
|
522
706
|
|
|
523
707
|
<footer>
|
|
524
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on
|
|
708
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Oct 24 2026 09:42:07 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
525
709
|
</footer>
|
|
526
710
|
|
|
527
711
|
<script>prettyPrint();</script>
|