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.
Files changed (107) hide show
  1. package/README.md +70 -6
  2. package/dist/css/{app.f09c7433.css → app.f24c5e50.css} +1 -1
  3. package/dist/index.tmp +1 -1
  4. package/dist/js/app-legacy.033deb22.js +1 -0
  5. package/dist/js/app.033deb22.js +1 -0
  6. package/dist/js/{chunk-vendors.ece5a361.js → chunk-vendors-legacy.b0bbb5ac.js} +4 -4
  7. package/dist/js/{chunk-vendors-legacy.ece5a361.js → chunk-vendors.b0bbb5ac.js} +4 -4
  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 +11 -6
  11. package/docs/global.html +11316 -5113
  12. package/docs/index.html +2 -2
  13. package/docs/server_WWebPerm.mjs.html +114 -334
  14. package/docs/server_cascadeWrite.mjs.html +5 -26
  15. package/docs/server_dataChannel.mjs.html +3 -3
  16. package/docs/server_keyCheck.mjs.html +150 -23
  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 +246 -62
  20. package/docs/server_staLogs_resolveShellWorker.mjs.html +2 -2
  21. package/docs/server_syncCore.mjs.html +533 -0
  22. package/docs/server_syncKeep.mjs.html +27 -20
  23. package/docs/server_tools_authRefsAudit.mjs.html +2 -2
  24. package/docs/server_tools_authRefsClean.mjs.html +2 -2
  25. package/docs/server_tools_fromTools.mjs.html +2 -2
  26. package/docs/server_tools_isMain.mjs.html +2 -2
  27. package/docs/server_tools_nameDupReport.mjs.html +40 -7
  28. package/docs/server_validateRows.mjs.html +2 -2
  29. package/docs/server_wrapInjected.mjs.html +2 -2
  30. package/docs/src_fetchJson.mjs.html +2 -2
  31. package/docs/src_getPermUserInfor.mjs.html +175 -0
  32. package/docs/src_js_applySaved.mjs.html +128 -0
  33. package/docs/src_js_cellEditing.mjs.html +163 -0
  34. package/docs/src_js_cvKpToArr.mjs.html +2 -2
  35. package/docs/src_js_cvKpToObj.mjs.html +2 -2
  36. package/docs/src_js_draftSync.mjs.html +83 -7
  37. package/docs/src_js_eachOwn.mjs.html +2 -2
  38. package/docs/src_js_editorRefRows.mjs.html +2 -2
  39. package/docs/src_js_externalRows.mjs.html +34 -10
  40. package/docs/src_js_externalUi.mjs.html +4 -8
  41. package/docs/src_js_findRefRows.mjs.html +2 -2
  42. package/docs/src_js_findTouchedDup.mjs.html +2 -2
  43. package/docs/src_js_fmtLossyMsg.mjs.html +2 -2
  44. package/docs/src_js_fromAudit.mjs.html +2 -2
  45. package/docs/src_js_getAddImpact.mjs.html +2 -2
  46. package/docs/src_js_getAuthWarn.mjs.html +2 -2
  47. package/docs/src_js_getCascadePreview.mjs.html +2 -2
  48. package/docs/src_js_getDelRefCount.mjs.html +2 -2
  49. package/docs/src_js_getEffMode.mjs.html +2 -2
  50. package/docs/src_js_getKpLossy.mjs.html +2 -2
  51. package/docs/src_js_getNameErrs.mjs.html +2 -2
  52. package/docs/src_js_getOwn.mjs.html +2 -2
  53. package/docs/src_js_getPropKey.mjs.html +2 -2
  54. package/docs/src_js_getRuleEnable.mjs.html +2 -2
  55. package/docs/src_js_isAuthMode.mjs.html +2 -2
  56. package/docs/src_js_joinNames.mjs.html +2 -2
  57. package/docs/src_js_legendFit.mjs.html +2 -2
  58. package/docs/src_js_pairRowsToSnapshot.mjs.html +2 -2
  59. package/docs/src_js_refAudit.mjs.html +2 -2
  60. package/docs/src_js_refCascade.mjs.html +2 -2
  61. package/docs/src_js_refIdentity.mjs.html +500 -0
  62. package/docs/src_js_relDialogSend.mjs.html +2 -2
  63. package/docs/src_js_rowDigest.mjs.html +75 -145
  64. package/docs/src_plugins_dataReloadGuard.mjs.html +2 -2
  65. package/docs/src_plugins_reuseDeletedIds.mjs.html +2 -2
  66. package/docs/src_plugins_submitGuard.mjs.html +2 -2
  67. package/g_provideGrups.mjs +1 -1
  68. package/g_providePemis.mjs +1 -1
  69. package/g_provideTargets.mjs +1 -1
  70. package/g_provideUsers.mjs +1 -1
  71. package/package.json +2 -2
  72. package/server/WWebPerm.mjs +112 -332
  73. package/server/cascadeWrite.mjs +3 -24
  74. package/server/dataChannel.mjs +1 -1
  75. package/server/keyCheck.mjs +148 -21
  76. package/server/procCore.mjs +244 -60
  77. package/server/procLang.mjs +72 -4
  78. package/server/provideTabs.mjs +6 -4
  79. package/server/syncCore.mjs +461 -0
  80. package/server/syncKeep.mjs +25 -18
  81. package/server/tools/nameDupReport.mjs +38 -5
  82. package/src/components/LayoutContentGrups.vue +57 -8
  83. package/src/components/LayoutContentPemis.vue +57 -8
  84. package/src/components/LayoutContentTargets.vue +38 -7
  85. package/src/components/LayoutContentUsers.vue +57 -8
  86. package/src/components/VeGrupBlngUsers.vue +128 -20
  87. package/src/components/VePemiBlngGrups.vue +128 -20
  88. package/src/getPermUserInfor.mjs +10 -0
  89. package/src/js/applySaved.mjs +56 -0
  90. package/src/js/cellEditing.mjs +91 -0
  91. package/src/js/draftSync.mjs +81 -5
  92. package/src/js/externalRows.mjs +32 -8
  93. package/src/js/externalUi.mjs +2 -6
  94. package/src/js/refIdentity.mjs +428 -0
  95. package/src/js/rowDigest.mjs +73 -143
  96. package/src/schema/contracts/grups.mjs +8 -0
  97. package/src/schema/contracts/index.mjs +17 -0
  98. package/src/schema/contracts/pemis.mjs +8 -0
  99. package/src/schema/contracts/targets.mjs +8 -0
  100. package/src/schema/contracts/users.mjs +13 -0
  101. package/src/schema/tables/grups.mjs +4 -0
  102. package/src/schema/tables/pemis.mjs +4 -0
  103. package/src/schema/tables/targets.mjs +4 -0
  104. package/src/schema/tables/users.mjs +4 -0
  105. package/srv.mjs +19 -0
  106. package/dist/js/app-legacy.8af87052.js +0 -1
  107. package/dist/js/app.8af87052.js +0 -1
@@ -1,193 +1,123 @@
1
- //列內容指紋與載入基準(F-26 過期快照保護; R-PRINCIPLE-00 原則 3「遇衝突或歧義:收嚴+提示」; 規則帳本 R-BASELINE-01; ADR-037): 純函數(不依賴 Vue/DOM/ORM), 前端與伺服端同一份.
2
- //
3
- //基準(baseline)=「編輯草稿所依據之那一版資料」之逐列內容指紋 [[id, digest], ...]; 於**開啟編輯(建立草稿)時**由草稿所依據之那份整表計算並綁定,
4
- //之後即使資料推播使 store 變新, 舊草稿仍帶舊基準(外審 B.1: 存檔當下才取基準, 證明的是「store 新」而不是「待存之編輯內容新」).
5
- //伺服端於整表寫入之互斥鎖內以庫內整表現況比對(表級, R-PRINCIPLE-00 原則 7, 使用者 2026-10-22 裁示): 表內任何一列之內容與載入時不同, 或列被增刪, 即整次拒收 saveStale、零寫入, 提示重新載入;
6
- //不部分變更、不上鎖, 先動先贏(run07 版只比對「本次將寫入之列」, run11 改為整表).
7
- //指紋只含內容欄位(排除 order 與伺服器管理欄 userId/timeCreate/userIdUpdate/timeUpdate): 外部同步整來源重寫使每列之建立/更新時間皆變, 內容未變者不應使草稿作廢; order 由儲存時依列序重給
8
- //(外部列之 order 由外部擁有), 僅 order 不同不是內容變更.
1
+ //載入基準(多人同時編輯之過期保護, 先動先贏; R-PRINCIPLE-00 原則 3、使用者 2026-10-22 裁示原則 10; 規則帳本 R-BASELINE-01、R-CONTRACT-01; ADR-037 Update(perm-run01)):
2
+ //本檔為套內轉接層, 摘要、基準與比對一律由 w-serv-orm 共用核心(w-serv-orm/src/baseline.mjs, 零 import 之同構純函數, 版本 h1)計算, 套內不再自帶指紋演算法:
3
+ // - 摘要 digestRow: 依寫入契約(src/schema/contracts)之 fields 白名單; undefined/null/'' 同一空值; 數字與字串不互轉; 物件鍵序無關.
4
+ // - 基準 buildBaseline: 有序 [[id, 摘要]], 序即名次(order 數值, 主鍵碼元); 他人重排被偵測, 系統重編號(名次不變)不誤擋.
5
+ // - 比對 compareBaseline: 增、刪、改、名次任一不同即不符.
6
+ //基準=「編輯草稿所依據之那一版資料」之基準, 於開啟編輯(建立草稿)時由草稿所依據之整表計算並綁定, 之後即使 store 變新, 舊草稿仍帶舊基準.
7
+ //伺服端於表鎖內以新鮮讀取之庫內整表比對(先動先贏, 零寫入, 不部分變更、不上鎖); 前端用同一組函數判定草稿是否已過期(提示「資料已被更新」).
8
+ //舊格式(套內版, 無 'h1:' 前綴)或不識版本之基準一律拒收 baselineFormat, 提示重新載入, 不降級為「未帶基準」.
9
9
  //
10
10
  //未帶基準之舊客戶端: 後台列維持舊行為(最後寫入者勝, ADR-013); 但**修改外部列之指派欄**無從證明快照新舊(Lv1 放行之指派欄不得因舊快照還原外部已撤銷之授權, 外審 B.2), 依原則 3 收嚴拒收(saveBaselineRequired).
11
11
 
12
- import { SERVER_FIELDS } from './externalRows.mjs'
13
-
12
+ import { buildBaseline, compareBaseline as compareCore, isBaselineValid, digestRow } from 'w-serv-orm/src/baseline.mjs'
13
+ import contracts from '../schema/contracts/index.mjs'
14
14
 
15
- let isStr = (v) => typeof v === 'string'
16
- let isObj = (v) => v !== null && typeof v === 'object'
17
15
 
18
-
19
- //cyrb53: 53 位元非密碼學雜湊(純 JS, 前後端皆可用; 用途為一致性比對, 非安全邊界)
20
- function cyrb53(str, seed = 0) {
21
- let h1 = 0xdeadbeef ^ seed
22
- let h2 = 0x41c6ce57 ^ seed
23
- for (let i = 0; i < str.length; i++) {
24
- let ch = str.charCodeAt(i)
25
- h1 = Math.imul(h1 ^ ch, 2654435761)
26
- h2 = Math.imul(h2 ^ ch, 1597334677)
27
- }
28
- h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909)
29
- h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909)
30
- return 4294967296 * (2097151 & h2) + (h1 >>> 0)
31
- }
16
+ let hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k)
32
17
 
33
18
 
34
19
  /**
35
- * 取得列內容指紋所含欄位: schema 全部欄位扣除 order 與伺服器管理欄
20
+ * 取得表之寫入契約(表名須為 targets/pemis/grups/users)
36
21
  *
37
- * @param {Array} keys 輸入該表 schema 欄位名陣列(ds[keyTable].keys)
38
- * @returns {Array} 回傳欄位名陣列
22
+ * @param {String} keyTable 輸入表名
23
+ * @returns {Object} 回傳寫入契約
39
24
  */
40
- function getDigestFields(keys) {
41
- return (Array.isArray(keys) ? keys : []).filter((k) => k !== 'order' && SERVER_FIELDS.indexOf(k) < 0)
25
+ function getContract(keyTable) {
26
+ if (typeof keyTable !== 'string' || !hasOwn(contracts, keyTable)) {
27
+ throw new Error(`invalid keyTable of contract: ${String(keyTable)}`)
28
+ }
29
+ return contracts[keyTable]
42
30
  }
43
31
 
44
32
 
45
33
  /**
46
- * 計算單列內容指紋(欄位依 schema 次序; 缺欄視為 null; 與列內其他鍵、鍵序無關)
34
+ * 由草稿所依據之整表列計算載入基準(核心 buildBaseline; 前端以畫面所見之同一快照自算, 不向後端取雜湊)
47
35
  *
48
- * @param {Object} row 輸入列物件
49
- * @param {Array} keys 輸入該表 schema 欄位名陣列
50
- * @returns {String} 回傳指紋字串
51
- */
52
- function rowDigest(row, keys) {
53
- let fields = getDigestFields(keys)
54
- let vals = fields.map((k) => {
55
- let v = isObj(row) ? row[k] : undefined
56
- return v === undefined ? null : v
57
- })
58
- return cyrb53(JSON.stringify(vals)).toString(36)
59
- }
60
-
61
-
62
- /**
63
- * 由草稿所依據之列計算載入基準(只含主鍵為字串之列)
36
+ * 主鍵缺漏或重複(資料異常, 正常不會發生)時不拋錯, 回傳空基準: 與非空之庫內整表比對必不符, 存檔被拒並提示重新載入(fail-closed).
64
37
  *
65
38
  * @param {Array} rows 輸入草稿所依據之那份列(開啟編輯時之 store 快照, 非存檔當下)
66
- * @param {Array} keys 輸入該表 schema 欄位名陣列
67
- * @returns {Array} 回傳 [[id, digest], ...]
68
- */
69
- function makeBaseline(rows, keys) {
70
- let rs = []
71
- ;(Array.isArray(rows) ? rows : []).forEach((r) => {
72
- if (isObj(r) && isStr(r.id)) {
73
- rs.push([r.id, rowDigest(r, keys)])
74
- }
75
- })
76
- return rs
77
- }
78
-
79
-
80
- /**
81
- * 判斷客戶端送入之基準格式是否合法(陣列, 每項為 [字串, 字串])
82
- *
83
- * @param {*} baseline 輸入客戶端送入之基準
84
- * @returns {Boolean} 回傳是否合法
39
+ * @param {String} keyTable 輸入表名
40
+ * @returns {Array} 回傳 [[id, 'h1:…'], ...](依名次)
85
41
  */
86
- function isBaselineValid(baseline) {
87
- if (!Array.isArray(baseline) || !baseline.every((e) => Array.isArray(e) && e.length === 2 && isStr(e[0]) && isStr(e[1]))) {
88
- return false
42
+ function makeBaseline(rows, keyTable) {
43
+ try {
44
+ return buildBaseline(Array.isArray(rows) ? rows : [], getContract(keyTable))
89
45
  }
90
-
91
- //同 id 不得出現兩次(外審 sol R8): 同 id 帶不同版本為矛盾之基準, 由列序決定採哪版屬靜默擇一(R-PRINCIPLE-00 原則 3), 格式驗證即拒收
92
- let ids = new Set()
93
- for (let e of baseline) {
94
- if (ids.has(e[0])) {
95
- return false
96
- }
97
- ids.add(e[0])
46
+ catch (err) {
47
+ return []
98
48
  }
99
- return true
100
- }
101
-
102
-
103
- /**
104
- * 取得基準中某列之指紋(供前端確認草稿所依據之某列版本於期間是否變動)
105
- *
106
- * @param {Array} baseline 輸入載入基準 [[id, digest], ...]
107
- * @param {String} id 輸入列 id
108
- * @returns {String|undefined} 回傳該列指紋; 基準內無該列為 undefined
109
- */
110
- function getBaselineDigest(baseline, id) {
111
- let hit = (Array.isArray(baseline) ? baseline : []).find((e) => Array.isArray(e) && e[0] === id)
112
- return hit === undefined ? undefined : hit[1]
113
49
  }
114
50
 
115
51
 
116
52
  /**
117
- * 表級比對: 目前整表與載入基準(草稿所依據之整表)逐列內容指紋與列集合之差異數
53
+ * 表級比對: 目前整表與載入基準(草稿所依據之整表)之差異
118
54
  *
119
- * 比對對象是整表而非「本次將寫入之列」(R-PRINCIPLE-00 原則 7, 使用者 2026-10-22 裁示「以表雜湊比對, 先動先贏」): 表內任何一列之內容(指紋口徑見 getDigestFields: 排除 order 與伺服器管理欄)與基準不同,
120
- * 或列被增刪(id 集合不同), 即視為資料已被他人/外部同步更新. 只含主鍵為字串之列(同 makeBaseline; 主鍵非字串之列 ORM 不允許入庫, 兩側一致忽略).
121
- * 前端(判定草稿是否過期、顯示提示)與伺服端(checkBaseline)共用本函數, 口徑單一.
55
+ * 前端(判定草稿是否過期、顯示提示)與伺服端(checkBaseline)共用, 口徑單一(核心 compareBaseline).
122
56
  *
123
57
  * @param {Object} p 輸入參數物件
124
- * @param {Array} p.keys 輸入該表 schema 欄位名陣列
125
- * @param {Array} p.rows 輸入目前整表之列(伺服端為庫內全部列, 前端為 store 之該表列)
126
- * @param {Array} p.baseline 輸入載入基準 [[id, digest], ...](須已通過 isBaselineValid; 非陣列視為空基準)
127
- * @returns {Object} 回傳 { nChanged, nRemoved, nAdded }: 內容指紋不同之列數/基準有而現已不在之列數/基準無而現在才有之列數; 三者皆 0 為一致
58
+ * @param {String} p.keyTable 輸入表名
59
+ * @param {Array} p.rows 輸入目前整表之列(伺服端為表鎖內新鮮讀取之全部列, 前端為 store 之該表列)
60
+ * @param {Array} p.baseline 輸入載入基準
61
+ * @returns {Object} 回傳 { nChanged, nRemoved, nAdded, reordered, reason }: 內容摘要不同之列數/基準有而現已不在之列數/基準無而現在才有之列數/名次是否不同/核心拒收鍵(相符為空字串);
62
+ * 基準格式不合時 reason 為 baselineFormat; 目前列之主鍵缺漏或重複(rowsInvalid)時拋錯(key 為 rowsInvalid)
128
63
  */
129
- function compareBaseline({ keys, rows, baseline }) {
130
- let kp = new Map(Array.isArray(baseline) ? baseline : [])
131
- let nChanged = 0
132
- let nAdded = 0
133
- let seen = new Set()
134
- ;(Array.isArray(rows) ? rows : []).forEach((o) => {
135
- if (!isObj(o) || !isStr(o.id) || seen.has(o.id)) {
136
- return
137
- }
138
- seen.add(o.id)
139
- if (!kp.has(o.id)) {
140
- nAdded++
141
- }
142
- else if (kp.get(o.id) !== rowDigest(o, keys)) {
143
- nChanged++
144
- }
145
- })
146
- let nRemoved = 0
147
- kp.forEach((v, id) => {
148
- if (!seen.has(id)) {
149
- nRemoved++
150
- }
151
- })
152
- return { nChanged, nRemoved, nAdded }
64
+ function compareBaseline({ keyTable, rows, baseline }) {
65
+ let c = compareCore(Array.isArray(rows) ? rows : [], baseline, getContract(keyTable))
66
+ if (c.ok) {
67
+ return { nChanged: 0, nRemoved: 0, nAdded: 0, reordered: false, reason: '' }
68
+ }
69
+ return { nChanged: c.changed.length, nRemoved: c.removed.length, nAdded: c.added.length, reordered: c.reordered === true, reason: c.reason }
153
70
  }
154
71
 
155
72
 
156
73
  /**
157
- * 判斷載入基準相對於目前整表是否已過期(compareBaseline 之布林版, 供前端提示用)
74
+ * 判斷載入基準相對於目前整表是否已過期(供前端提示用)
75
+ *
76
+ * 前端容錯: 基準格式不合或目前列主鍵異常皆視為過期(fail-closed, 提示重新載入).
158
77
  *
159
78
  * @param {Object} p 輸入參數物件(同 compareBaseline)
160
- * @returns {Boolean} 回傳 true 為過期(有任何一列內容不同或列被增刪)
79
+ * @returns {Boolean} 回傳 true 為過期(有任何一列內容不同、列被增刪或名次不同)
161
80
  */
162
81
  function isBaselineStale(p) {
163
- let c = compareBaseline(p)
164
- return c.nChanged + c.nRemoved + c.nAdded > 0
82
+ try {
83
+ return compareBaseline(p).reason !== ''
84
+ }
85
+ catch (err) {
86
+ return true
87
+ }
165
88
  }
166
89
 
167
90
 
168
91
  /**
169
- * 以載入基準做表級過期檢查(在表鎖內, 以庫內整表現況比對; 不符整次拒收、零寫入; R-PRINCIPLE-00 原則 7)
92
+ * 以載入基準做表級過期檢查(伺服端, 須在表鎖內以新鮮讀取之整表呼叫; 不符整次拒收、零寫入)
170
93
  *
171
- * 規則: 載入時之整表與目前整表有任何差異(compareBaseline: 任一列內容指紋不同, 或列被增刪)即過期, 不論差異是否落在「本次將寫入之列」——
172
- * 不部分變更(不逐項判斷哪些可蓋)、不上鎖, 先動先贏; 指紋排除 order 與伺服器管理欄, 故外部同步整批重寫但內容未變(含時間/建立者改變、order 重編)、純粹重排 order 皆不使草稿作廢.
173
- * 修改前之 run07 版只檢查「本次將寫入之列」(他人改別列不擋), 使用者 2026-10-22 裁示改為表級, 見 ADR-037 Update(run11).
94
+ * 規則: 載入時之整表與目前整表有任何差異(列內容、列增刪、名次)即過期, 不論差異是否落在「本次將寫入之列」——不部分變更, 先動先贏;
95
+ * 外部同步整批重寫但內容未變(含時間/建立者改變、order 重編而名次不變)不使草稿作廢.
96
+ * 基準格式或版本不識(含套內舊版指紋)→ saveBaselineFormat(提示重新載入, 不降級為未帶基準).
174
97
  *
175
98
  * @param {Object} p 輸入參數物件
176
- * @param {Array} p.keys 輸入該表 schema 欄位名陣列
177
- * @param {Array} p.ltdtOld 輸入該表庫內全部列
178
- * @param {Array} p.baseline 輸入客戶端載入基準(須已通過 isBaselineValid)
179
- * @returns {Object|null} 回傳 null 為通過; 否則 { key: 'saveStale', reason, n }: reason 為 rowChanged(有列內容不同)/rowRemoved(基準之列已不在)/rowAdded(多出基準沒有的列), 依序取第一個成立者; n 為差異總列數
99
+ * @param {String} p.keyTable 輸入表名
100
+ * @param {Array} p.ltdtOld 輸入該表庫內全部列(表鎖內新鮮讀取)
101
+ * @param {Array} p.baseline 輸入客戶端載入基準(任何值; 格式不合由核心判定)
102
+ * @returns {Object|null} 回傳 null 為通過; 否則 { key, reason, n }: key 為語系鍵(saveStale/saveBaselineFormat), reason 為核心拒收鍵(baselineMismatch/rowNotFound/baselineFormat)加細節
103
+ * (baselineMismatch 時為 rowChanged/rowRemoved/rowAdded/rowReordered, 依序取第一個成立者; rowNotFound/baselineFormat 原樣), n 為差異列數(格式不合為 0, 僅名次不同為 1)
180
104
  */
181
- function checkBaseline({ keys, ltdtOld, baseline }) {
182
- let c = compareBaseline({ keys, rows: ltdtOld, baseline })
183
- let n = c.nChanged + c.nRemoved + c.nAdded
184
- if (n === 0) {
105
+ function checkBaseline({ keyTable, ltdtOld, baseline }) {
106
+ let c = compareBaseline({ keyTable, rows: ltdtOld, baseline })
107
+ if (c.reason === '') {
185
108
  return null
186
109
  }
187
- let reason = c.nChanged > 0 ? 'rowChanged' : (c.nRemoved > 0 ? 'rowRemoved' : 'rowAdded')
188
- return { key: 'saveStale', reason, n }
110
+ if (c.reason === 'baselineFormat') {
111
+ return { key: 'saveBaselineFormat', reason: 'baselineFormat', n: 0 }
112
+ }
113
+ let n = c.nChanged + c.nRemoved + c.nAdded
114
+ if (c.reason === 'rowNotFound') {
115
+ return { key: 'saveStale', reason: 'rowNotFound', n }
116
+ }
117
+ let reason = c.nChanged > 0 ? 'rowChanged' : (c.nRemoved > 0 ? 'rowRemoved' : (c.nAdded > 0 ? 'rowAdded' : 'rowReordered'))
118
+ return { key: 'saveStale', reason, n: n > 0 ? n : 1 }
189
119
  }
190
120
 
191
121
 
192
- export { cyrb53, getDigestFields, rowDigest, makeBaseline, isBaselineValid, getBaselineDigest, compareBaseline, isBaselineStale, checkBaseline }
122
+ export { getContract, makeBaseline, isBaselineValid, digestRow, compareBaseline, isBaselineStale, checkBaseline }
193
123
  export default makeBaseline
@@ -0,0 +1,8 @@
1
+ //grups 之寫入契約(規格與欄位政策見 users.mjs 之說明; 規則帳本 R-CONTRACT-01)
2
+ export default {
3
+ table: 'grups',
4
+ pk: 'id',
5
+ fields: ['name', 'description', 'from', 'srcKey', 'cpemis'], //srcKey: 外部匯入之來源鍵(身分欄, 見 users.mjs)
6
+ serverOnly: ['userId', 'timeCreate', 'userIdUpdate', 'timeUpdate'],
7
+ orderKey: 'order',
8
+ }
@@ -0,0 +1,17 @@
1
+ //四張表之寫入契約集合(鍵為表名): 伺服端(server/procCore.mjs)與前端(清單頁、關聯對話框)共用同一份, 以 w-serv-orm/src/baseline.mjs 之 buildBaseline/compareBaseline 計算與比對(規則帳本 R-CONTRACT-01)
2
+ import users from './users.mjs'
3
+ import grups from './grups.mjs'
4
+ import pemis from './pemis.mjs'
5
+ import targets from './targets.mjs'
6
+
7
+
8
+ let contracts = {
9
+ users,
10
+ grups,
11
+ pemis,
12
+ targets,
13
+ }
14
+
15
+
16
+ export { contracts }
17
+ export default contracts
@@ -0,0 +1,8 @@
1
+ //pemis 之寫入契約(規格與欄位政策見 users.mjs 之說明; 規則帳本 R-CONTRACT-01)
2
+ export default {
3
+ table: 'pemis',
4
+ pk: 'id',
5
+ fields: ['name', 'description', 'from', 'srcKey', 'crules'], //srcKey: 外部匯入之來源鍵(身分欄, 見 users.mjs)
6
+ serverOnly: ['userId', 'timeCreate', 'userIdUpdate', 'timeUpdate'],
7
+ orderKey: 'order',
8
+ }
@@ -0,0 +1,8 @@
1
+ //targets 之寫入契約(規格與欄位政策見 users.mjs 之說明; 規則帳本 R-CONTRACT-01): 主鍵 id 即標的名稱(使用者給定)
2
+ export default {
3
+ table: 'targets',
4
+ pk: 'id',
5
+ fields: ['description', 'from', 'srcKey'], //srcKey: 外部匯入之來源鍵(身分欄, 見 users.mjs; 外部匯入時 id=srcKey)
6
+ serverOnly: ['userId', 'timeCreate', 'userIdUpdate', 'timeUpdate'],
7
+ orderKey: 'order',
8
+ }
@@ -0,0 +1,13 @@
1
+ //users 之寫入契約(多人同時編輯, 規格 dspec/多人同時編輯之寫入契約.md 第 3~5 節; 規則帳本 R-CONTRACT-01): 前後端 import 同一檔, 純資料(零 import, 可進瀏覽器打包).
2
+ //fields 明列(白名單): 存檔採用客戶端值且入基準之欄位; 不以「排除系統欄位」推得.
3
+ // from 為外部列身分(外部同步擁有, 後台不得改)與 isActive/cgrups 等皆為客戶端欄位或外部欄位, 入基準: 外部同步改變其中任一欄, 持舊畫面者存檔即被拒(以外部為主, 原則 5).
4
+ //serverOnly: 伺服器專有欄位(建立者/建立時間/更新者/更新時間, 只由伺服端寫入): 不入基準, 存檔不採用客戶端值(取庫內值).
5
+ //orderKey: 存檔依列序重編 order(updateTabItems), 故 order 不入列摘要, 改以「名次」入基準——他人重排被偵測、系統重編號(名次不變)不誤擋.
6
+ // 外部列之 order 屬外部(R-EXT-OWN-01: 後台存檔不重編外部列之 order, 還原為庫內值), 排序口徑同一(order 數值, 主鍵碼元).
7
+ export default {
8
+ table: 'users',
9
+ pk: 'id',
10
+ fields: ['name', 'email', 'description', 'from', 'srcKey', 'cgrups', 'isAdmin', 'isActive'], //srcKey: 外部匯入之來源鍵(身分欄, perm-run03; 與 from 同, 後台不可改〔externalRows.checkExternalWrite〕, 外部匯入補上或改變者持舊畫面者存檔即被拒)
11
+ serverOnly: ['userId', 'timeCreate', 'userIdUpdate', 'timeUpdate'],
12
+ orderKey: 'order',
13
+ }
@@ -33,6 +33,10 @@ let settings = {
33
33
  name: '來源', //供外部批次變更使用
34
34
  type: 'STRING',
35
35
  },
36
+ srcKey: {
37
+ name: '來源鍵', //外部匯入(syncAndReplaceTabs)依其差異同步之識別: 外部自訂、同一來源(from)內唯一且穩定; 後台列(from 為空)恆為空, 後台不可改(身分欄, 與 from 同; perm-run03, R-IMPORT-01)
38
+ type: 'STRING',
39
+ },
36
40
  cpemis: {
37
41
  name: '權限清單', //為json字串
38
42
  type: 'TEXT',
@@ -33,6 +33,10 @@ let settings = {
33
33
  name: '來源', //供外部批次變更使用
34
34
  type: 'STRING',
35
35
  },
36
+ srcKey: {
37
+ name: '來源鍵', //外部匯入(syncAndReplaceTabs)依其差異同步之識別: 外部自訂、同一來源(from)內唯一且穩定; 後台列(from 為空)恆為空, 後台不可改(身分欄, 與 from 同; perm-run03, R-IMPORT-01)
38
+ type: 'STRING',
39
+ },
36
40
  crules: {
37
41
  name: '管控對象清單', //為json字串
38
42
  type: 'TEXT',
@@ -29,6 +29,10 @@ let settings = {
29
29
  name: '來源', //供外部批次變更使用
30
30
  type: 'STRING',
31
31
  },
32
+ srcKey: {
33
+ name: '來源鍵', //外部匯入(syncAndReplaceTabs)依其差異同步之識別: 外部自訂、同一來源(from)內唯一且穩定; 後台列(from 為空)恆為空, 後台不可改(身分欄, 與 from 同; perm-run03, R-IMPORT-01). targets 之 id 即標的名稱, 外部匯入時 id=srcKey
34
+ type: 'STRING',
35
+ },
32
36
  userId: {
33
37
  name: '創建使用者主鍵',
34
38
  type: 'STRING',
@@ -37,6 +37,10 @@ let settings = {
37
37
  name: '來源', //標注使用者來源, 亦供外部批次變更使用
38
38
  type: 'STRING',
39
39
  },
40
+ srcKey: {
41
+ name: '來源鍵', //外部匯入(syncAndReplaceTabs)依其差異同步之識別: 外部自訂、同一來源(from)內唯一且穩定; 後台列(from 為空)恆為空, 後台不可改(身分欄, 與 from 同; perm-run03, R-IMPORT-01)
42
+ type: 'STRING',
43
+ },
40
44
  cgrups: {
41
45
  name: '權限群組清單', //為json字串
42
46
  type: 'TEXT',
package/srv.mjs CHANGED
@@ -55,6 +55,22 @@ let getUserByToken = async (token) => {
55
55
  if (token === '{token-for-reject}') { //模擬部署方以舊版 w-web-sso helper(≤1.1.4)注入: 失敗時 reject 之字串含已代入系統介接權杖(合成秘密)與使用者權杖之完整網址
56
56
  return Promise.reject(`can not get user data by url[http://127.0.0.1:11007/api/getSsoUserInfor?token=SYNTH-SYS-SECRET-FOR-TEST&key=token&value=${token}]`)
57
57
  }
58
+ if (token === '{token-for-admin2}') { //測試用(perm-run03 範圍三): 第二位系統管理員, 供「標頭 token 與本體 __sysToken__ 為不同使用者」「本體 token 之使用者被停用」等 getUserIdByToken 完整驗證案例(其 perm users 列由測試自行建立/停用)
59
+ return {
60
+ id: 'id-for-admin2',
61
+ name: 'admin2',
62
+ email: 'admin2@example.com',
63
+ isAdmin: 'y',
64
+ }
65
+ }
66
+ if (token === '{token-for-member}') { //測試用(perm-run03 範圍四): 在 perm users 表內(測試自行建立)、有效、isAdmin='n' 但**通過** verifyClientUser(見下方)之瀏覽使用者, 供「通過連線閘之非管理員呼叫寫入函數被 requireAdmin 拒收」案例
67
+ return {
68
+ id: 'id-for-member',
69
+ name: 'member',
70
+ email: 'member@example.com',
71
+ isAdmin: 'n',
72
+ }
73
+ }
58
74
  if (token === '{token-for-verify-throw}') { //回有效使用者, 但 verifyClientUser / verifyAppUser 對其拋錯(Error.message 為合成秘密), 模擬部署方驗證函數拋錯
59
75
  return {
60
76
  id: 'id-for-verify-throw',
@@ -75,6 +91,9 @@ let verifyClientUser = (user, from) => {
75
91
  if (process.env.NODE_ENV !== 'production' && user.id === 'id-for-verify-throw') { //測試用: 見 getUserByToken 之 '{token-for-verify-throw}'
76
92
  throw new Error('SYNTH-VERIFY-SECRET-FOR-TEST')
77
93
  }
94
+ if (process.env.NODE_ENV !== 'production' && user.id === 'id-for-member') { //測試用: 見 getUserByToken 之 '{token-for-member}'(部署方放行非管理員, 套件仍須於函數層級另檢查角色)
95
+ return true
96
+ }
78
97
  return user.isAdmin === 'y' //測試僅系統管理者使用
79
98
  }
80
99