w-web-perm 1.1.11 → 1.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +158 -117
  2. package/dist/css/app.f09c7433.css +1 -0
  3. package/dist/index.tmp +1 -1
  4. package/dist/js/app-legacy.8af87052.js +1 -0
  5. package/dist/js/app.8af87052.js +1 -0
  6. package/dist/js/{chunk-vendors.3e9e3ef1.js → chunk-vendors-legacy.ece5a361.js} +1 -1
  7. package/dist/js/{chunk-vendors-legacy.3e9e3ef1.js → chunk-vendors.ece5a361.js} +1 -1
  8. package/dist/w-web-perm.umd.js +2 -2
  9. package/dist/w-web-perm.umd.js.map +1 -1
  10. package/docs/WWebPerm.html +84 -4
  11. package/docs/global.html +24747 -832
  12. package/docs/index.html +2 -2
  13. package/docs/server_WWebPerm.mjs.html +245 -32
  14. package/docs/server_cascadeWrite.mjs.html +225 -0
  15. package/docs/server_dataChannel.mjs.html +2 -2
  16. package/docs/server_keyCheck.mjs.html +353 -0
  17. package/docs/server_lockSave.mjs.html +2 -2
  18. package/docs/server_maskLog.mjs.html +2 -2
  19. package/docs/server_procCore.mjs.html +180 -41
  20. package/docs/server_staLogs_resolveShellWorker.mjs.html +2 -2
  21. package/docs/server_syncKeep.mjs.html +311 -0
  22. package/docs/server_tools_authRefsAudit.mjs.html +247 -0
  23. package/docs/server_tools_authRefsClean.mjs.html +335 -0
  24. package/docs/server_tools_fromTools.mjs.html +797 -0
  25. package/docs/server_tools_isMain.mjs.html +103 -0
  26. package/docs/server_tools_nameDupReport.mjs.html +280 -0
  27. package/docs/server_validateRows.mjs.html +15 -4
  28. package/docs/server_wrapInjected.mjs.html +3 -3
  29. package/docs/src_fetchJson.mjs.html +2 -2
  30. package/docs/src_js_cvKpToArr.mjs.html +125 -0
  31. package/docs/src_js_cvKpToObj.mjs.html +210 -0
  32. package/docs/src_js_draftSync.mjs.html +142 -0
  33. package/docs/src_js_eachOwn.mjs.html +3 -3
  34. package/docs/src_js_editorRefRows.mjs.html +356 -0
  35. package/docs/src_js_externalRows.mjs.html +338 -0
  36. package/docs/src_js_externalUi.mjs.html +230 -0
  37. package/docs/src_js_findRefRows.mjs.html +107 -0
  38. package/docs/src_js_findTouchedDup.mjs.html +150 -0
  39. package/docs/src_js_fmtLossyMsg.mjs.html +95 -0
  40. package/docs/src_js_fromAudit.mjs.html +489 -0
  41. package/docs/src_js_getAddImpact.mjs.html +269 -0
  42. package/docs/src_js_getAuthWarn.mjs.html +463 -0
  43. package/docs/src_js_getCascadePreview.mjs.html +258 -0
  44. package/docs/src_js_getDelRefCount.mjs.html +210 -0
  45. package/docs/src_js_getEffMode.mjs.html +92 -0
  46. package/docs/src_js_getKpLossy.mjs.html +155 -0
  47. package/docs/src_js_getNameErrs.mjs.html +266 -0
  48. package/docs/src_js_getOwn.mjs.html +3 -3
  49. package/docs/src_js_getPropKey.mjs.html +109 -0
  50. package/docs/src_js_getRuleEnable.mjs.html +2 -2
  51. package/docs/src_js_isAuthMode.mjs.html +92 -0
  52. package/docs/src_js_joinNames.mjs.html +96 -0
  53. package/docs/src_js_legendFit.mjs.html +2 -2
  54. package/docs/src_js_pairRowsToSnapshot.mjs.html +156 -0
  55. package/docs/src_js_refAudit.mjs.html +359 -0
  56. package/docs/src_js_refCascade.mjs.html +572 -0
  57. package/docs/src_js_relDialogSend.mjs.html +199 -0
  58. package/docs/src_js_rowDigest.mjs.html +265 -0
  59. package/docs/src_plugins_dataReloadGuard.mjs.html +109 -0
  60. package/docs/src_plugins_reuseDeletedIds.mjs.html +2 -2
  61. package/docs/src_plugins_submitGuard.mjs.html +2 -2
  62. package/g_authRefsClean.mjs +79 -0
  63. package/g_fixFromType.mjs +92 -0
  64. package/g_initialData.mjs +3 -3
  65. package/g_purgeFrom.mjs +127 -0
  66. package/package.json +1 -1
  67. package/server/WWebPerm.mjs +243 -30
  68. package/server/cascadeWrite.mjs +153 -0
  69. package/server/keyCheck.mjs +281 -0
  70. package/server/procCore.mjs +178 -39
  71. package/server/procLang.mjs +271 -0
  72. package/server/syncKeep.mjs +239 -0
  73. package/server/tools/authRefsAudit.mjs +175 -0
  74. package/server/tools/authRefsClean.mjs +263 -0
  75. package/server/tools/fromTools.mjs +725 -0
  76. package/server/tools/isMain.mjs +31 -0
  77. package/server/tools/nameDupReport.mjs +208 -0
  78. package/server/validateRows.mjs +13 -2
  79. package/server/wrapInjected.mjs +1 -1
  80. package/src/components/LayoutContentGrups.vue +399 -77
  81. package/src/components/LayoutContentPemis.vue +400 -78
  82. package/src/components/LayoutContentTargets.vue +261 -25
  83. package/src/components/LayoutContentUsers.vue +336 -98
  84. package/src/components/VeCgrups.vue +119 -115
  85. package/src/components/VeCpemis.vue +117 -115
  86. package/src/components/VeCrules.vue +18 -2
  87. package/src/components/VeGrupBlngUsers.vue +240 -73
  88. package/src/components/VePemiBlngGrups.vue +247 -83
  89. package/src/js/cvKpToArr.mjs +53 -0
  90. package/src/js/cvKpToObj.mjs +138 -0
  91. package/src/js/draftSync.mjs +70 -0
  92. package/src/js/eachOwn.mjs +1 -1
  93. package/src/js/editorRefRows.mjs +284 -0
  94. package/src/js/externalRows.mjs +266 -0
  95. package/src/js/externalUi.mjs +158 -0
  96. package/src/js/findRefRows.mjs +35 -0
  97. package/src/js/findTouchedDup.mjs +78 -0
  98. package/src/js/fmtLossyMsg.mjs +23 -0
  99. package/src/js/fromAudit.mjs +417 -0
  100. package/src/js/getAddImpact.mjs +197 -0
  101. package/src/js/getAuthWarn.mjs +391 -0
  102. package/src/js/getCascadePreview.mjs +186 -0
  103. package/src/js/getDelRefCount.mjs +138 -0
  104. package/src/js/getEffMode.mjs +20 -0
  105. package/src/js/getKpLossy.mjs +83 -0
  106. package/src/js/getNameErrs.mjs +194 -0
  107. package/src/js/getOwn.mjs +1 -1
  108. package/src/js/getPropKey.mjs +37 -0
  109. package/src/js/isAuthMode.mjs +20 -0
  110. package/src/js/joinNames.mjs +24 -0
  111. package/src/js/pairRowsToSnapshot.mjs +84 -0
  112. package/src/js/refAudit.mjs +287 -0
  113. package/src/js/refCascade.mjs +500 -0
  114. package/src/js/relDialogSend.mjs +127 -0
  115. package/src/js/rowDigest.mjs +193 -0
  116. package/src/plugins/dataReloadGuard.mjs +37 -0
  117. package/src/plugins/mShare.mjs +147 -174
  118. package/src/plugins/mUI.mjs +3 -8
  119. package/src/schema/tables/users.mjs +152 -152
  120. package/dist/css/app.6918ec45.css +0 -1
  121. package/dist/js/app-legacy.963e800c.js +0 -1
  122. package/dist/js/app.963e800c.js +0 -1
@@ -0,0 +1,266 @@
1
+ //外部列受保護(F-23; R-PRINCIPLE-00 原則 5「開了外部同步就以外部為主」、原則 3; 規則帳本 R-EXT-OWN-01; ADR-037): 後台整表寫入對「外部列」之守門, 純函數(不依賴 Vue/DOM/ORM),
2
+ //伺服端(server/procCore.mjs)為強制者, 前端(清單頁唯讀、複製、刪除過濾)共用同一份判定, 不各自重寫.
3
+ //
4
+ //判別(定案 D1): from 為非空字串者為「外部列」(外部同步 syncAndReplaceTabs 只收非空字串 from, 故非空 from 必曾由外部同步寫入或後台誤植);
5
+ //from 為 ''、缺欄、或非字串(null/數字/布林)者為「後台列」——非字串 from 必為後台寫入(同步入口 isestr 把關), 遷移工具正規化為 ''(g_fixFromType.mjs), 後台寫入起拒收非字串 from.
6
+ //Lv1 鎖定層級: 外部列之身分/結構欄(指派欄、順序、伺服器管理欄以外之全部欄位)後台不得改, 不得刪除, 不得改 from; 指派欄(users.cgrups/grups.cpemis/pemis.crules)後台可寫
7
+ // (改指派須附載入基準, 見 rowDigest.mjs; 無基準無從證明快照新舊, 依原則 3 收嚴拒收); 外部列之 order 由外部擁有(後台儲存不重編, 還原為庫內值).
8
+
9
+
10
+ //SERVER_FIELDS: 伺服器管理欄(procOrm 寫入時覆寫), 不納入內容比對
11
+ let SERVER_FIELDS = ['userId', 'timeCreate', 'userIdUpdate', 'timeUpdate']
12
+
13
+
14
+ //ASSIGN_FIELD: 各表之授權指派欄(後台可寫); targets 無指派欄
15
+ let ASSIGN_FIELD = { users: 'cgrups', grups: 'cpemis', pemis: 'crules', targets: null }
16
+
17
+
18
+ let isStr = (v) => typeof v === 'string'
19
+ let hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k)
20
+ let isObj = (v) => v !== null && typeof v === 'object'
21
+
22
+
23
+ //normV: 比對用正規化(缺欄與 '' 視為同一; 其餘以 JSON 字串比對, 值為資料庫與前端同源之純量/字串)
24
+ let normV = (v) => JSON.stringify(v === undefined ? '' : v)
25
+
26
+
27
+ /**
28
+ * 判斷是否為外部列(from 為非空字串)
29
+ *
30
+ * @param {Object} row 輸入列物件
31
+ * @returns {Boolean} 回傳是否為外部列
32
+ * @example
33
+ *
34
+ * isExternalRow({ id: '1', from: 'pd' })
35
+ * // => true
36
+ * isExternalRow({ id: '1', from: '' })
37
+ * // => false
38
+ * isExternalRow({ id: '1', from: null })
39
+ * // => false
40
+ *
41
+ */
42
+ function isExternalRow(row) {
43
+ return isObj(row) && isStr(row.from) && row.from !== ''
44
+ }
45
+
46
+
47
+ /**
48
+ * 取得外部列之鎖定欄位(身分/結構欄): schema 全部欄位扣除 id、from、order、指派欄、伺服器管理欄
49
+ *
50
+ * @param {String} keyTable 輸入表名(targets/pemis/grups/users)
51
+ * @param {Array} keys 輸入該表 schema 之欄位名陣列(ds[keyTable].keys)
52
+ * @returns {Array} 回傳鎖定欄位名陣列
53
+ */
54
+ function getLockFields(keyTable, keys) {
55
+ let ka = hasOwn(ASSIGN_FIELD, keyTable) ? ASSIGN_FIELD[keyTable] : null
56
+ return (Array.isArray(keys) ? keys : []).filter((k) => k !== 'id' && k !== 'from' && k !== 'order' && k !== ka && SERVER_FIELDS.indexOf(k) < 0)
57
+ }
58
+
59
+
60
+ /**
61
+ * 外部列之 order 由外部擁有: 把送入列中「庫內為外部列者」之 order 還原為庫內值(後台儲存重編 order=k+1 不得改外部列之順序), 回傳新陣列(不改輸入)
62
+ *
63
+ * @param {Array} rows 輸入送入之整表列(已重給 order)
64
+ * @param {Array} ltdtOld 輸入該表庫內全部列(select 結果)
65
+ * @returns {Array} 回傳 order 已還原之列陣列
66
+ */
67
+ function restoreExternalOrder(rows, ltdtOld) {
68
+ let oldById = new Map()
69
+ ;(Array.isArray(ltdtOld) ? ltdtOld : []).forEach((o) => {
70
+ if (isObj(o) && isStr(o.id)) {
71
+ oldById.set(o.id, o)
72
+ }
73
+ })
74
+ return rows.map((row) => {
75
+ let o = isObj(row) && isStr(row.id) ? oldById.get(row.id) : undefined
76
+ if (o !== undefined && isExternalRow(o) && hasOwn(o, 'order')) {
77
+ return { ...row, order: o.order }
78
+ }
79
+ return row
80
+ })
81
+ }
82
+
83
+
84
+ /**
85
+ * 將「外部列且僅伺服器管理欄(建立者/時間、更新者/時間)不同」之 diff 列移入 same(視為無變更, 不寫入)
86
+ *
87
+ * 外部同步每次整來源重寫, 使外部列之 timeUpdate/timeCreate/userId 隨之更新; 持舊快照之後台存檔若原樣送回該列, 其內容與庫內相同而僅伺服器管理欄較舊,
88
+ * 寫入會把建立者與建立時間回退為舊值(D2 設計稿 S2). 內容相同即無須寫入. 只處理外部列, 後台列之 diff 判定不變.
89
+ *
90
+ * @param {Object} p 輸入參數物件
91
+ * @param {Array} p.keys 輸入該表 schema 欄位名陣列
92
+ * @param {Object} p.r 輸入 ltdtDiffByKey 結果 { add, del, diff, same }
93
+ * @param {Array} p.ltdtOld 輸入該表庫內全部列
94
+ * @returns {Object} 回傳新之 { add, del, diff, same }
95
+ */
96
+ function dropServerOnlyDiff({ keys, r, ltdtOld }) {
97
+ let oldById = new Map()
98
+ ltdtOld.forEach((o) => {
99
+ if (isObj(o) && isStr(o.id)) {
100
+ oldById.set(o.id, o)
101
+ }
102
+ })
103
+ let fields = (Array.isArray(keys) ? keys : []).filter((k) => SERVER_FIELDS.indexOf(k) < 0)
104
+ let diff = []
105
+ let same = [...r.same]
106
+ r.diff.forEach((row) => {
107
+ let o = isObj(row) && isStr(row.id) ? oldById.get(row.id) : undefined
108
+ if (o !== undefined && isExternalRow(o) && fields.every((k) => normV(row[k]) === normV(o[k]))) {
109
+ same.push(row)
110
+ }
111
+ else {
112
+ diff.push(row)
113
+ }
114
+ })
115
+ return { add: r.add, del: r.del, diff, same }
116
+ }
117
+
118
+
119
+ /**
120
+ * 外部列之 diff 列(內容確有改動, 例如指派欄)改帶庫內之伺服器管理欄(userId/timeCreate/userIdUpdate/timeUpdate)
121
+ *
122
+ * 伺服器管理欄由伺服器擁有(procOrm save 只覆寫 userIdUpdate/timeUpdate, 建立者與建立時間取自送入之列); 外部同步每次整來源重寫使外部列之建立者/建立時間更新,
123
+ * 持舊草稿(前端保留未存編輯, 見 draftSync)送回該列時, 基準因排除伺服器管理欄而通過(內容未變), 若原樣寫入就把較新之建立者與建立時間回退為舊值(R-EXT-OWN-01 S2 之同一缺陷, 外審 sol 第 3 點).
124
+ * 故改動之外部列一律以庫內之伺服器管理欄寫入. 只處理庫內為外部列者(後台列之寫入不變); 庫內無該欄者不補.
125
+ *
126
+ * @param {Object} p 輸入參數物件
127
+ * @param {Object} p.r 輸入 ltdtDiffByKey(及 dropServerOnlyDiff)結果 { add, del, diff, same }
128
+ * @param {Array} p.ltdtOld 輸入該表庫內全部列
129
+ * @returns {Object} 回傳新之 { add, del, diff, same }(不改輸入; 未改動之列為同一物件)
130
+ */
131
+ function restoreServerFields({ r, ltdtOld }) {
132
+ let oldById = new Map()
133
+ ltdtOld.forEach((o) => {
134
+ if (isObj(o) && isStr(o.id)) {
135
+ oldById.set(o.id, o)
136
+ }
137
+ })
138
+ let diff = r.diff.map((row) => {
139
+ let o = isObj(row) && isStr(row.id) ? oldById.get(row.id) : undefined
140
+ if (o === undefined || !isExternalRow(o)) {
141
+ return row
142
+ }
143
+ let out = { ...row }
144
+ SERVER_FIELDS.forEach((k) => {
145
+ if (hasOwn(o, k)) {
146
+ out[k] = o[k]
147
+ }
148
+ })
149
+ return out
150
+ })
151
+ return { add: r.add, del: r.del, diff, same: r.same }
152
+ }
153
+
154
+
155
+ /**
156
+ * 後台整表寫入對外部列之守門(於 ltdtDiffByKey 與 dropServerOnlyDiff 之後、寫入之前; 違規整次拒收、零寫入, 不靜默忽略)
157
+ *
158
+ * from 型別(不論 lockExternalRows): 新增列之 from 須為字串(缺欄視為 ''), 修改列之 from 若改為非字串須與庫內值相同(歷史 null 原樣送回不拒, 否則已有歷史資料之庫無法儲存);
159
+ * lockExternalRows 為真時另有: ①新增列之 from 須為 ''(含復活他來源之列、複製繼承、手打); ②不得刪除外部列; ③修改列: 外部列不得改 from 與鎖定欄位(getLockFields), 後台列不得改成非空 from.
160
+ * 指派欄與 order/伺服器管理欄不在比對內.
161
+ *
162
+ * @param {Object} p 輸入參數物件
163
+ * @param {String} p.keyTable 輸入表名
164
+ * @param {Array} p.keys 輸入該表 schema 欄位名陣列
165
+ * @param {Object} p.r 輸入 { add, del, diff, same }
166
+ * @param {Array} p.ltdtOld 輸入該表庫內全部列
167
+ * @param {Boolean} [p.lockExternalRows=true] 輸入是否啟用外部列保護, 預設 true
168
+ * @returns {Object|null} 回傳 null 為通過; 否則 { key, reason, k, n, froms }: key 為語系鍵(saveFromNotAllowed/saveExternalRowChanged), reason 為原因(fromType/addFrom/del/fromChanged/fieldChanged), k 為 r 內該陣列之第一個違規索引, n 為違規列數, froms 為涉及之來源(至多 5 個)
169
+ */
170
+ function checkExternalWrite({ keyTable, keys, r, ltdtOld, lockExternalRows = true }) {
171
+ let oldById = new Map()
172
+ ltdtOld.forEach((o) => {
173
+ if (isObj(o) && isStr(o.id)) {
174
+ oldById.set(o.id, o)
175
+ }
176
+ })
177
+ let normFrom = (v) => (v === undefined ? '' : v)
178
+ let lockFields = getLockFields(keyTable, keys)
179
+
180
+ let bad = [] //{ key, reason, k, from }
181
+ let push = (key, reason, k, from) => {
182
+ bad.push({ key, reason, k, from: isStr(from) ? from : '' })
183
+ }
184
+
185
+ //add
186
+ r.add.forEach((row, k) => {
187
+ let f = isObj(row) ? normFrom(row.from) : ''
188
+ if (!isStr(f)) {
189
+ push('saveFromNotAllowed', 'fromType', k, '')
190
+ }
191
+ else if (lockExternalRows && f !== '') {
192
+ push('saveFromNotAllowed', 'addFrom', k, f)
193
+ }
194
+ })
195
+
196
+ //del
197
+ if (lockExternalRows) {
198
+ r.del.forEach((row, k) => {
199
+ if (isExternalRow(row)) {
200
+ push('saveExternalRowChanged', 'del', k, row.from)
201
+ }
202
+ })
203
+ }
204
+
205
+ //diff
206
+ r.diff.forEach((row, k) => {
207
+ let o = isObj(row) && isStr(row.id) ? oldById.get(row.id) : undefined
208
+ let nf = isObj(row) ? normFrom(row.from) : ''
209
+ let of = o === undefined ? '' : normFrom(o.from)
210
+ if (!isStr(nf) && nf !== of) {
211
+ push('saveFromNotAllowed', 'fromType', k, '')
212
+ return
213
+ }
214
+ if (!lockExternalRows || o === undefined) {
215
+ return
216
+ }
217
+ if (isExternalRow(o)) {
218
+ if (nf !== o.from) {
219
+ push('saveExternalRowChanged', 'fromChanged', k, o.from)
220
+ }
221
+ else if (lockFields.some((f) => normV(row[f]) !== normV(o[f]))) {
222
+ push('saveExternalRowChanged', 'fieldChanged', k, o.from)
223
+ }
224
+ }
225
+ else if (isStr(nf) && nf !== '') {
226
+ push('saveFromNotAllowed', 'fromChanged', k, nf)
227
+ }
228
+ })
229
+
230
+ if (bad.length === 0) {
231
+ return null
232
+ }
233
+ let froms = []
234
+ bad.forEach((b) => {
235
+ if (b.from !== '' && froms.indexOf(b.from) < 0 && froms.length < 5) {
236
+ froms.push(b.from)
237
+ }
238
+ })
239
+ return { key: bad[0].key, reason: bad[0].reason, k: bad[0].k, n: bad.length, froms }
240
+ }
241
+
242
+
243
+ /**
244
+ * 本次寫入是否修改了外部列之指派欄(須附載入基準, 見 rowDigest.mjs checkBaseline)
245
+ *
246
+ * @param {Object} p 輸入參數物件
247
+ * @param {Object} p.r 輸入 { add, del, diff, same }(經 dropServerOnlyDiff 後)
248
+ * @param {Array} p.ltdtOld 輸入該表庫內全部列
249
+ * @returns {Boolean} 回傳是否有外部列之 diff
250
+ */
251
+ function hasExternalDiff({ r, ltdtOld }) {
252
+ let oldById = new Map()
253
+ ltdtOld.forEach((o) => {
254
+ if (isObj(o) && isStr(o.id)) {
255
+ oldById.set(o.id, o)
256
+ }
257
+ })
258
+ return r.diff.some((row) => {
259
+ let o = isObj(row) && isStr(row.id) ? oldById.get(row.id) : undefined
260
+ return o !== undefined && isExternalRow(o)
261
+ })
262
+ }
263
+
264
+
265
+ export { SERVER_FIELDS, ASSIGN_FIELD, isExternalRow, getLockFields, restoreExternalOrder, dropServerOnlyDiff, restoreServerFields, checkExternalWrite, hasExternalDiff }
266
+ export default isExternalRow
@@ -0,0 +1,158 @@
1
+ import { isExternalRow, getLockFields } from './externalRows.mjs'
2
+
3
+
4
+ //清單頁(標的/權限/群組/使用者)之外部列(from 為非空字串者)前端輔助純函數(F-23; R-PRINCIPLE-00 原則 3、5; 規則帳本 R-EXT-OWN-01; ADR-037): 不依賴 Vue/DOM/語系檔,
5
+ //判別外部列一律經 externalRows.isExternalRow(單一擁有者, 本檔不另寫判準); 伺服端為強制者, 本檔只供畫面提示與送出前擋下(不取代伺服端守門).
6
+
7
+
8
+ let isStr = (v) => typeof v === 'string'
9
+ let isObj = (v) => v !== null && typeof v === 'object'
10
+
11
+
12
+ //normV: 比對用正規化, 同 externalRows 之 checkExternalWrite(缺欄與 '' 視為同一; 其餘以 JSON 字串比對)
13
+ let normV = (v) => JSON.stringify(v === undefined ? '' : v)
14
+
15
+
16
+ /**
17
+ * 把範本之 {key} 代入(單趟掃描: 代入之值不再解析, 名稱可含 {n}、{from} 等字樣原樣顯示; 未給之佔位符原樣保留)
18
+ *
19
+ * 與 getAuthWarn.fmtWarn 同設計, 但佔位符不限定名單(如 {from}、{name}); 值一律轉字串, 不特殊處理 $ 字樣.
20
+ *
21
+ * @param {String} tpl 輸入範本
22
+ * @param {Object} [vars={}] 輸入 { 佔位符名: 值 }
23
+ * @returns {String} 回傳代入後之文字
24
+ * @example
25
+ *
26
+ * console.log(fmtVars('{from}: {n}', { from: 'teamA', n: 3 }))
27
+ * // => 'teamA: 3'
28
+ *
29
+ */
30
+ function fmtVars(tpl, vars = {}) {
31
+ let kp = Object.create(null)
32
+ Object.keys(isObj(vars) ? vars : {}).forEach((k) => {
33
+ kp[k] = String(vars[k])
34
+ })
35
+ return String(tpl).replace(/\{(\w+)\}/g, (hit, k) => (k in kp ? kp[k] : hit))
36
+ }
37
+
38
+
39
+ /**
40
+ * 計算外部列(from 為非空字串者)之列數
41
+ *
42
+ * @param {Array} rows 輸入列陣列(非陣列視為空)
43
+ * @returns {Number} 回傳外部列數
44
+ */
45
+ function countExternalRows(rows) {
46
+ if (!Array.isArray(rows)) {
47
+ return 0
48
+ }
49
+ return rows.filter((r) => isExternalRow(r)).length
50
+ }
51
+
52
+
53
+ /**
54
+ * 取出屬於某表之外部同步警示(getWebInfor().syncWarns: [{ keyTable, from, n, time }]; 外部同步疑似 id 重編)
55
+ *
56
+ * 只取 keyTable 與本頁表名相符、from 為非空字串、n 為正數者; 輸出為 { from, n }(次序同輸入); 非陣列或元素格式不符者略過.
57
+ *
58
+ * @param {Array} syncWarns 輸入 webInfor.syncWarns
59
+ * @param {String} keyTable 輸入本頁表名(targets/pemis/grups/users)
60
+ * @returns {Array} 回傳 [{ from, n }]
61
+ */
62
+ function getSyncWarnLines(syncWarns, keyTable) {
63
+ let rs = []
64
+ if (!Array.isArray(syncWarns)) {
65
+ return rs
66
+ }
67
+ syncWarns.forEach((w) => {
68
+ if (isObj(w) && w.keyTable === keyTable && isStr(w.from) && w.from !== '' && typeof w.n === 'number' && w.n > 0) {
69
+ rs.push({ from: w.from, n: w.n })
70
+ }
71
+ })
72
+ return rs
73
+ }
74
+
75
+
76
+ /**
77
+ * 產生清單上方橫幅之各行: 外部列提示(啟用外部列保護且外部列數 > 0 才有)+本表之外部同步警示(每個來源一行); 皆無則回空陣列(頁面不渲染任何節點)
78
+ *
79
+ * 外部列提示用語系鍵 externalRowsNotice(群組/權限/使用者: 身分欄唯讀、不可刪除、指派可改)或 externalRowsNoticeTarget(標的無指派欄); 警示用 syncWarnIdRenumber({from}、{n}).
80
+ *
81
+ * @param {Object} p 輸入參數物件
82
+ * @param {String} p.keyTable 輸入本頁表名
83
+ * @param {String} p.kind 輸入頁面種類: 'Target'(標的頁)或其他
84
+ * @param {Array} p.rows 輸入本頁之基準列(store 之該表列, 已儲存者; 外部列只可能由外部同步寫入)
85
+ * @param {Boolean} p.lock 輸入是否啟用外部列保護(webInfor.lockExternalRows === 'y')
86
+ * @param {Array} p.syncWarns 輸入 webInfor.syncWarns
87
+ * @param {Function} p.$t 輸入語系函數(key → 文字)
88
+ * @returns {Array} 回傳 [{ kind: 'ext'|'warn', text }]
89
+ */
90
+ function getBannerLines({ keyTable, kind, rows, lock, syncWarns, $t }) {
91
+ let rs = []
92
+ let n = lock === true ? countExternalRows(rows) : 0
93
+ if (n > 0) {
94
+ rs.push({ kind: 'ext', text: fmtVars($t(kind === 'Target' ? 'externalRowsNoticeTarget' : 'externalRowsNotice'), { n }) })
95
+ }
96
+ getSyncWarnLines(syncWarns, keyTable).forEach((w) => {
97
+ rs.push({ kind: 'warn', text: fmtVars($t('syncWarnIdRenumber'), { from: w.from, n: w.n }) })
98
+ })
99
+ return rs
100
+ }
101
+
102
+
103
+ /**
104
+ * 把勾選列依外部列保護切成「可處理」與「被擋下」: 啟用外部列保護時外部列不得刪除(伺服端 saveExternalRowChanged), 先於前端過濾並計數供提示
105
+ *
106
+ * @param {Array} items 輸入勾選之項目(清單頁 vo.itemsCheck, 每項之 data 為該列資料)
107
+ * @param {Boolean} lock 輸入是否啟用外部列保護
108
+ * @returns {Object} 回傳 { rows, nBlocked }: rows 為可處理之項目(未啟用保護時為全部), nBlocked 為被擋下之外部列數
109
+ */
110
+ function splitCheckedRows(items, lock) {
111
+ let all = Array.isArray(items) ? items : []
112
+ if (lock !== true) {
113
+ return { rows: all, nBlocked: 0 }
114
+ }
115
+ let rows = all.filter((v) => !isExternalRow(isObj(v) ? v.data : undefined))
116
+ return { rows, nBlocked: all.length - rows.length }
117
+ }
118
+
119
+
120
+ /**
121
+ * 送出前檢查: 外部列(以基準列判定)是否被改動身分/結構欄、from 或被刪除(含改 id 致原 id 消失)——清單頁以逐列唯讀與刪除過濾擋下, 但貼上(Ctrl+V)等路徑可繞過逐列唯讀,
122
+ * 故送出前再比對一次(判準同 externalRows.checkExternalWrite: 鎖定欄位=getLockFields, 另含 from; 指派欄與 order/伺服器管理欄不比對), 有違規即不送出並提示(伺服端同樣整次拒收)
123
+ *
124
+ * @param {Object} p 輸入參數物件
125
+ * @param {String} p.keyTable 輸入表名(targets/pemis/grups/users)
126
+ * @param {Array} p.keys 輸入該表 schema 欄位名陣列(ds[keyTable].keys)
127
+ * @param {Array} p.rowsNew 輸入欲送出之整表(清單目前之列)
128
+ * @param {Array} p.rowsOld 輸入基準列(store 之該表列)
129
+ * @returns {Object|null} 回傳 null 為無違規; 否則 { n, froms }: n 為違規之外部列數, froms 為涉及之來源(至多 5 個)
130
+ */
131
+ function getExternalEdits({ keyTable, keys, rowsNew, rowsOld }) {
132
+ let fields = [...getLockFields(keyTable, keys), 'from']
133
+ let newById = new Map()
134
+ ;(Array.isArray(rowsNew) ? rowsNew : []).forEach((r) => {
135
+ if (isObj(r) && (isStr(r.id) || (typeof r.id === 'number' && Number.isFinite(r.id)))) {
136
+ newById.set(String(r.id), r)
137
+ }
138
+ })
139
+ let n = 0
140
+ let froms = []
141
+ ;(Array.isArray(rowsOld) ? rowsOld : []).forEach((o) => {
142
+ if (!isExternalRow(o) || !isStr(o.id)) {
143
+ return
144
+ }
145
+ let r = newById.get(o.id)
146
+ if (r === undefined || fields.some((f) => normV(r[f]) !== normV(o[f]))) {
147
+ n++
148
+ if (froms.indexOf(o.from) < 0 && froms.length < 5) {
149
+ froms.push(o.from)
150
+ }
151
+ }
152
+ })
153
+ return n > 0 ? { n, froms } : null
154
+ }
155
+
156
+
157
+ export { fmtVars, countExternalRows, getSyncWarnLines, getBannerLines, splitCheckedRows, getExternalEdits }
158
+ export default fmtVars
@@ -0,0 +1,35 @@
1
+ /**
2
+ * 取得「名稱為 name」之表列(群組表/權限表), 授權引用(使用者 cgrups、群組 cpemis)以名稱字串查找實體之唯一判準,
3
+ * 伺服端權限評估(src/plugins/mShare.mjs 之 getUserRules)與頁面提示(src/js/getAuthWarn.mjs)共用(規則帳本 R-AUTH-REF-01)
4
+ *
5
+ * 比對為精確 ===(不做大小寫/空白正規化); 列無 name 欄(undefined)視為空字串 '', 與引用名稱 '' 相符(沿用修正前 lodash get(row, 'name', '') 之行為).
6
+ * 回傳全部符合之列(依表中順序): 0 筆=引用查無實體(懸空), 1 筆=正常, ≥ 2 筆=歷史同名(僅外部同步可造成; 授權取各筆之交集, R-PRINCIPLE-00 d).
7
+ *
8
+ * @param {Array} rows 輸入表列陣列(非陣列視為空)
9
+ * @param {*} name 輸入引用之名稱(以 === 比對, 不做型別轉換: 名稱與列名同為數字時相符,與修正前 arrFinder 相同; 實務上表列名稱為字串, 非字串名稱之引用視為懸空並由 getAuthWarn 提示 noName)
10
+ * @returns {Array} 回傳符合之列陣列
11
+ * @example
12
+ *
13
+ * console.log(findRefRows([{ name: 'A' }, { name: 'B' }, { name: 'A' }], 'A').length)
14
+ * // => 2
15
+ * console.log(findRefRows([{ name: 'A' }], 'X').length)
16
+ * // => 0
17
+ *
18
+ */
19
+ function findRefRows(rows, name) {
20
+ let rs = []
21
+ if (!Array.isArray(rows)) {
22
+ return rs
23
+ }
24
+ for (let k = 0; k < rows.length; k++) {
25
+ let r = rows[k]
26
+ let n = (r === null || r === undefined || r.name === undefined) ? '' : r.name
27
+ if (n === name) {
28
+ rs.push(r)
29
+ }
30
+ }
31
+ return rs
32
+ }
33
+
34
+
35
+ export default findRefRows
@@ -0,0 +1,78 @@
1
+ //後台整表寫入之「本次造成或觸及之同名」判準, 單一擁有者(規則帳本 R-NAME-UNIQ-02、R-DUP-UI-01, ADR-034/035):
2
+ //伺服端(server/keyCheck.mjs 轉出, procCore.mjs:updateTabItems 呼叫)與前端清單頁(LayoutContentGrups/Pemis/Users 之 errItemsByName/errItemsByEmail)共用同一份, 兩端判準一致.
3
+ //純函數, 不依賴 node 專屬模組. 比對一律精確 ===; 以 Map 計, 免原型鍵名(constructor、__proto__)誤判(規則帳本 R-KEY-01).
4
+
5
+
6
+ let isStr = (v) => typeof v === 'string'
7
+
8
+
9
+ /**
10
+ * 後台整表寫入(procCore.updateTabItems)用: 回傳 rowsNew 中第一個「本次寫入造成或觸及」之同名列索引, 無則 -1
11
+ *
12
+ * 寫入後之名稱集合即送入之整表(後台整表寫入, 含他 from 之外部同步列). 名稱於 rowsNew 出現 ≥ 2 次者為重複; 其中
13
+ * 「庫內該名稱已有 ≥ 2 列, 且 rowsNew 中該名稱之每一列皆為庫內同 id 同名之既有列, 且筆數不多於庫內」為歷史既有重複原樣送回(與本次無關), 不算;
14
+ * 其餘(新增第三個同名列、他列改名併入、重複中一列改成已被他列使用之名、以新 id 取代)皆為本次造成或觸及.
15
+ * why: 歷史重複(僅外部同步可造成)不得使後台所有存檔失敗, 否則已有重複之庫無法在後台修復; 但本次不得再造出新的重複.
16
+ *
17
+ * @param {Array} rowsNew 送入之整表(已 ltdtmapping、id 數字轉字串)
18
+ * @param {Array} rowsOld 庫內現有全部列(select 結果)
19
+ * @param {String} key 欄位名(name)
20
+ * @returns {Number} 造成或觸及重複之列索引(該名稱各列中第一個不是「庫內同 id 同名之既有列」者; 歷史原樣之組跳過; 多組取索引最小者), 無為 -1
21
+ */
22
+ function findTouchedDup(rowsNew, rowsOld, key) {
23
+ if (!Array.isArray(rowsNew)) {
24
+ return -1
25
+ }
26
+ //庫內: id → 名稱, 名稱 → 筆數
27
+ let nameOfId = new Map()
28
+ let cntOld = new Map()
29
+ for (let r of (Array.isArray(rowsOld) ? rowsOld : [])) {
30
+ if (r === null || typeof r !== 'object') {
31
+ continue
32
+ }
33
+ if (isStr(r.id) && isStr(r[key])) {
34
+ nameOfId.set(r.id, r[key])
35
+ }
36
+ if (isStr(r[key])) {
37
+ cntOld.set(r[key], (cntOld.get(r[key]) || 0) + 1)
38
+ }
39
+ }
40
+ //送入: 名稱 → 該名稱各列之索引
41
+ let kpIdx = new Map()
42
+ for (let k = 0; k < rowsNew.length; k++) {
43
+ let r = rowsNew[k]
44
+ let v = (r === null || typeof r !== 'object') ? undefined : r[key]
45
+ if (!isStr(v)) {
46
+ continue
47
+ }
48
+ if (!kpIdx.has(v)) {
49
+ kpIdx.set(v, [])
50
+ }
51
+ kpIdx.get(v).push(k)
52
+ }
53
+ let kBad = -1
54
+ for (let [name, idxs] of kpIdx) {
55
+ if (idxs.length < 2) {
56
+ continue
57
+ }
58
+ let legacy = (cntOld.get(name) || 0) >= 2 &&
59
+ idxs.length <= cntOld.get(name) &&
60
+ idxs.every((k) => {
61
+ let id = rowsNew[k].id
62
+ return isStr(id) && nameOfId.get(id) === name
63
+ }) &&
64
+ new Set(idxs.map((k) => rowsNew[k].id)).size === idxs.length //同一 id 重送不算兩列
65
+ if (!legacy) {
66
+ //回「造成或觸及」之列: 該名稱各列中第一個不是「庫內同 id 同名之既有列」者(新增、或由他名改入); 無(僅同 id 重送等)則回第二次出現者
67
+ let kTouch = idxs.find((k) => !(isStr(rowsNew[k].id) && nameOfId.get(rowsNew[k].id) === name))
68
+ let k = (kTouch === undefined) ? idxs[1] : kTouch
69
+ if (kBad < 0 || k < kBad) {
70
+ kBad = k
71
+ }
72
+ }
73
+ }
74
+ return kBad
75
+ }
76
+
77
+
78
+ export default findTouchedDup
@@ -0,0 +1,23 @@
1
+ import joinNames from './joinNames.mjs'
2
+
3
+
4
+ /**
5
+ * 把拒存訊息範本(authLossyBlocked)之 {names} 代入受影響之名稱
6
+ *
7
+ * 名稱為使用者/外部應用寫入之資料, 可含 $&、$$、$' 等字樣; 以 split/join 代入(不用 String.replace, 其替換字串中的 $ 樣式會被展開), 名稱原樣顯示.
8
+ *
9
+ * @param {String} tpl 輸入訊息範本, 含 {names}
10
+ * @param {Array} names 輸入受影響之名稱陣列(至多列出 5 個, 超過加省略號, 見 joinNames)
11
+ * @returns {String} 回傳代入後之訊息
12
+ * @example
13
+ *
14
+ * console.log(fmtLossyMsg('Not saved: {names}', ['a', '$&']))
15
+ * // => 'Not saved: a, $&'
16
+ *
17
+ */
18
+ function fmtLossyMsg(tpl, names) {
19
+ return String(tpl).split('{names}').join(joinNames(names))
20
+ }
21
+
22
+
23
+ export default fmtLossyMsg