w-serv-orm 1.1.10 → 1.1.12

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/src/getMapOrm.mjs CHANGED
@@ -8,6 +8,8 @@ import truncate from 'lodash-es/truncate.js'
8
8
  import nowms2str from 'wsemi/src/nowms2str.mjs'
9
9
  import genIDSeq from 'wsemi/src/genIDSeq.mjs'
10
10
  import isarr from 'wsemi/src/isarr.mjs'
11
+ import isearr from 'wsemi/src/isearr.mjs'
12
+ import isobj from 'wsemi/src/isobj.mjs'
11
13
  import iseobj from 'wsemi/src/iseobj.mjs'
12
14
  import isestr from 'wsemi/src/isestr.mjs'
13
15
  import isnum from 'wsemi/src/isnum.mjs'
@@ -18,6 +20,7 @@ import getItemsFail from 'w-data-collector/src/getItemsFail.mjs'
18
20
  function getMsgFail(msg, fails) {
19
21
  //前端訊息僅列出失敗筆, 錯誤原文另由console.log輸出, 與既有整批錯誤之處理一致
20
22
  //有效主鍵(非空字串或數字)列為id[...], 否則列其於輸入內之位置, 失敗筆數過多時僅列前nShow筆並附總筆數
23
+ //呼叫端給予位置標籤ref者(如mix同鍵之add[0])原樣列出
21
24
  //主鍵超過nIdMax字元者截斷顯示(LMDB逐筆失敗常因主鍵超長, 故兩者常同時出現), log仍保留全文
22
25
 
23
26
  //nShow, nIdMax
@@ -26,6 +29,9 @@ function getMsgFail(msg, fails) {
26
29
 
27
30
  //ss
28
31
  let ss = map(take(fails, nShow), (v) => {
32
+ if (isestr(v.ref)) {
33
+ return v.ref
34
+ }
29
35
  if (isestr(v.id) || isnum(v.id)) {
30
36
  return `id[${truncate(String(v.id), { length: nIdMax, omission: '…' })}]`
31
37
  }
@@ -55,8 +61,9 @@ function isPkIdString(dsTab, keys) {
55
61
  }
56
62
 
57
63
  //check, 型別已知者須為字串型, 與w-orm-reladb可自動補值之主鍵欄型別一致
64
+ //比對前取「(」之前之字並轉大寫, 如STRING(36)視為STRING, 同w-orm-reladb以type.key判定而不含長度參數
58
65
  let type = get(settings, 'id.type', '') || get(dsTab, 'kpType.id', '')
59
- if (isestr(type) && ['STRING', 'TEXT', 'CHAR', 'UUID', 'CITEXT'].indexOf(type.toUpperCase()) < 0) {
66
+ if (isestr(type) && ['STRING', 'TEXT', 'CHAR', 'UUID', 'CITEXT'].indexOf(type.split('(')[0].trim().toUpperCase()) < 0) {
60
67
  return false
61
68
  }
62
69
 
@@ -64,6 +71,314 @@ function isPkIdString(dsTab, keys) {
64
71
  }
65
72
 
66
73
 
74
+ function getOwnItem(o, k) {
75
+ //取o之自有屬性k, 非自有、k非字串、o非物件或值為undefined者回傳null(R-INPUT-01 規則5)
76
+ //不用lodash get: 其會取到原型鏈上之值(__proto__、constructor、toString等)並解析路徑字串(如'tests.select'取到ORM之函數); 鍵本身含「.」者為自有屬性, 照常
77
+ if (o === null || typeof o !== 'object' || typeof k !== 'string' || !Object.prototype.hasOwnProperty.call(o, k)) {
78
+ return null
79
+ }
80
+ return o[k] === undefined ? null : o[k]
81
+ }
82
+
83
+
84
+ function isIdCast(ds, woName) {
85
+ //資料表之主鍵欄為字串型或型別未知者, 主鍵之有限數字以其字串形式處置(R-ID-02); ds無該表者不轉型, 令select與del照舊原樣交ORM
86
+ let dsTab = getOwnItem(ds, woName)
87
+ let keys = get(dsTab, 'keys', null)
88
+ if (!isarr(keys)) {
89
+ return false
90
+ }
91
+ return isPkIdString(dsTab, keys)
92
+ }
93
+
94
+
95
+ function isIdNumFinite(v) {
96
+ //主鍵值是否為有限數字, 不用wsemi之isnum: 其對數字字串與Infinity亦為true
97
+ return typeof v === 'number' && Number.isFinite(v)
98
+ }
99
+
100
+
101
+ function castIdValue(v) {
102
+ //主鍵值之正規化: 有限數字轉為其字串形式, 其餘原樣(NaN、±Infinity亦原樣, 寫入時由adjustData視為未提供而補值)
103
+ //各後端之主鍵判準互異, lmdb、lowdb、level、mongodb只認非空字串而以genIDSeq取代數字, reladb、mdb之字串欄由資料庫把數字存為字串,
104
+ //故由本套件先轉, 令寫入、刪除與查詢條件同一規則, 回傳之主鍵與實際寫入者一致
105
+ return isIdNumFinite(v) ? String(v) : v
106
+ }
107
+
108
+
109
+ function mapOwn(o, f) {
110
+ //使用端物件之複製與逐鍵轉換(R-OBJ-01 規則1、2): 只走自有可列舉鍵, 回傳新物件, f收(值, 鍵)
111
+ //不用lodash each: 其對具非負整數length鍵之物件(如{length:1,name:'x'})採陣列樣迭代, 只走索引0..length-1而丟棄全部自有鍵
112
+ //以fromEntries建立自有屬性, 不用r[k]=v: k為__proto__時會改設r之原型而不建立自有屬性
113
+ return Object.fromEntries(Object.keys(o).map((k) => [k, f(o[k], k)]))
114
+ }
115
+
116
+
117
+ //不支援之鍵或屬性的原因與訊息(R-OBJ-01 規則3、5), 訊息只列固定字句, 不回顯客戶端之值
118
+ const MSGS_BAD_KEY = Object.freeze({
119
+ proto: 'unsupported key[__proto__]',
120
+ accessor: 'unsupported accessor property',
121
+ unreadable: 'unreadable value',
122
+ deep: 'too deep',
123
+ })
124
+
125
+ //find之巢狀深度上限, 以容器(一般物件與陣列)層數計, 根為第1層(R-OBJ-01 規則5)
126
+ //castIdFind、castIdCond以遞迴處理$and、$or、$nor與$not, 遞迴深度即呼叫堆疊深度, 實測字串型主鍵表約1173層$and(約2346個容器)起爆堆疊;
127
+ //64個容器層約為32層邏輯巢狀, 遠大於合理查詢(庫內最深約7個容器)而遠小於爆堆疊之閾值
128
+ const MAX_DEPTH_FIND = 64
129
+
130
+
131
+ function isPlainObj(v) {
132
+ //純物件(R-INPUT-01 規則3): isobj為真且原型為null, 或為某realm之Object.prototype(同lodash isPlainObject: 原型之原型為null、有constructor函數、其函數原始碼同Object, 故跨realm之物件亦通過)
133
+ //類別實例、Object.create(proto)、原型為null原型之一般物件、偽造Symbol.toStringTag之Map或包裝物件, 其自有可列舉鍵可能為空而被複製成{}, 令條件消失, 故不算
134
+ if (!isobj(v)) {
135
+ return false
136
+ }
137
+ let p = Object.getPrototypeOf(v)
138
+ if (p === null) {
139
+ return true
140
+ }
141
+ //p之原型為null, 故p.constructor只可能是p之自有屬性
142
+ return Object.getPrototypeOf(p) === null &&
143
+ typeof p.constructor === 'function' &&
144
+ Function.prototype.toString.call(p.constructor) === Function.prototype.toString.call(Object)
145
+ }
146
+
147
+
148
+ function logSafe(label, ...args) {
149
+ //診斷用之console.log: 輸出使用端原物件時會呼叫其util.inspect.custom, 拋錯不得使procOrm之reject變成例外物件(R-ERR-01 規則5)
150
+ try {
151
+ console.log(label, ...args)
152
+ }
153
+ catch (e) {
154
+ try {
155
+ console.log(label, '(unprintable)')
156
+ }
157
+ catch (e2) {
158
+ //放棄輸出
159
+ }
160
+ }
161
+ }
162
+
163
+
164
+ function getReasonBadKey(v, maxDepth = Infinity) {
165
+ //v之任何深度(一般物件之值、陣列之元素)含不支援之鍵或屬性者回傳原因(R-OBJ-01 規則3、5), 通過回傳null:
166
+ // proto: 含自有__proto__鍵, 不論可否列舉; 自有鍵可由JSON.parse、Object.fromEntries、展開、structuredClone等產生, 物件字面量之__proto__為設原型而不算
167
+ // accessor: 含存取器屬性(getter或setter), 其值每讀一次可能不同, 令檢查之值與使用之值無法保證相同, 且讀取可能有副作用
168
+ // unreadable: 檢查時拋錯, 如Proxy之陷阱或已撤銷之Proxy
169
+ // deep: 容器(一般物件與陣列)層數超過maxDepth(根為第1層), 走訪到第maxDepth+1層即停止; 只有find給予maxDepth, 寫入列不設上限(見R-OBJ-01 規則5)
170
+ //只走一般物件與陣列(JSON可表達之資料)之可列舉資料屬性, 以屬性描述子讀值而不呼叫getter; 其餘型別(Date、Map、Set、函數等)不進入
171
+ //以堆疊迭代避免本檢查爆呼叫堆疊(其後castIdFind、castIdCond之遞迴由maxDepth保護), seen避免循環結構無窮迭代(循環不會被判為過深, 見R-OBJ-01 已知限制)
172
+ try {
173
+ let seen = new Set()
174
+ let stack = [v, 1] //成對: 節點, 其層數
175
+ while (stack.length > 0) {
176
+ let depth = stack.pop()
177
+ let o = stack.pop()
178
+ let arr = Array.isArray(o) //不用isarr: 其以Object.prototype.toString判定, 可被Symbol.toStringTag偽造而走訪其length getter; 偽造者isobj亦為假, 故不進入
179
+ if (seen.has(o) || (!arr && !isobj(o))) {
180
+ continue
181
+ }
182
+ if (depth > maxDepth) {
183
+ return 'deep'
184
+ }
185
+ seen.add(o)
186
+ if (Object.prototype.hasOwnProperty.call(o, '__proto__')) {
187
+ return 'proto'
188
+ }
189
+ let ks = arr ? null : Object.keys(o)
190
+ let n = arr ? o.length : ks.length
191
+ for (let i = 0; i < n; i++) {
192
+ let d = Object.getOwnPropertyDescriptor(o, arr ? i : ks[i])
193
+ if (d === undefined) {
194
+ continue //稀疏陣列之空洞
195
+ }
196
+ if (d.get !== undefined || d.set !== undefined) {
197
+ return 'accessor'
198
+ }
199
+ if (typeof d.value === 'object' && d.value !== null) {
200
+ stack.push(d.value, depth + 1)
201
+ }
202
+ }
203
+ }
204
+ return null
205
+ }
206
+ catch (e) {
207
+ return 'unreadable'
208
+ }
209
+ }
210
+
211
+
212
+ function getErrFind(mode, find) {
213
+ //select、delAll之find檢查(R-INPUT-01 規則3、R-OBJ-01 規則3), 通過回傳null, 否則回傳reject之字串
214
+ //find給予(非undefined)須為純物件: 未給、undefined與空物件{}為無條件(select回全部、delAll刪全部), 各後端對非物件之find多視為無條件而回全表或刪全表, 故由本套件先擋
215
+ //任何深度不得含自有__proto__鍵(複製時該鍵會被丟棄, 令ORM所見之條件與呼叫端含授權函數所見者不同)與存取器屬性(見getReasonBadKey), 且巢狀深度不得超過MAX_DEPTH_FIND
216
+ try {
217
+ if (find !== undefined && !isPlainObj(find)) {
218
+ return `invalid find of ${mode}, need an object`
219
+ }
220
+ let reason = getReasonBadKey(find, MAX_DEPTH_FIND)
221
+ if (reason !== null) {
222
+ return `invalid find of ${mode}, ${MSGS_BAD_KEY[reason]}`
223
+ }
224
+ return null
225
+ }
226
+ catch (e) {
227
+ //isobj、取原型時拋錯, 如已撤銷之Proxy
228
+ return `invalid find of ${mode}, ${MSGS_BAD_KEY.unreadable}`
229
+ }
230
+ }
231
+
232
+
233
+ function getMsgsBadKey(msg, data) {
234
+ //寫入之各筆任何深度含不支援之鍵或屬性者(R-OBJ-01 規則3), 依原因分組回傳訊息陣列, 各組列其於輸入內之位置data[k]; 單一物件輸入視為第0筆
235
+ //輸入為陣列時(批次容器)逐索引以屬性描述子取值: 索引為存取器即reject, 使其後各次讀取皆為資料屬性之讀取而與檢查之值一致
236
+ let groups = new Map()
237
+ let addFail = (reason, k) => {
238
+ if (!groups.has(reason)) {
239
+ groups.set(reason, [])
240
+ }
241
+ groups.get(reason).push({ k })
242
+ }
243
+ if (Array.isArray(data)) {
244
+ for (let k = 0; k < data.length; k++) {
245
+ try {
246
+ let d = Object.getOwnPropertyDescriptor(data, k)
247
+ if (d === undefined) {
248
+ continue //空洞, 已於形狀檢查以非物件拒絕
249
+ }
250
+ if (d.get !== undefined || d.set !== undefined) {
251
+ addFail('accessor', k)
252
+ continue
253
+ }
254
+ let reason = getReasonBadKey(d.value)
255
+ if (reason !== null) {
256
+ addFail(reason, k)
257
+ }
258
+ }
259
+ catch (e) { //Proxy陣列之陷阱拋錯
260
+ addFail('unreadable', k)
261
+ }
262
+ }
263
+ }
264
+ else {
265
+ let reason = getReasonBadKey(data)
266
+ if (reason !== null) {
267
+ addFail(reason, 0)
268
+ }
269
+ }
270
+ return map([...groups], ([reason, fails]) => getMsgFail(`${msg}, ${MSGS_BAD_KEY[reason]}`, fails))
271
+ }
272
+
273
+
274
+ function castIdRows(rows) {
275
+ //寫入與刪除之數據: 各列之主鍵正規化, 寫於複本, 不改呼叫端之物件; 非物件之列原樣
276
+ let one = !isarr(rows)
277
+ let rs = map(one ? [rows] : rows, (v) => {
278
+ if (isobj(v) && isIdNumFinite(v.id)) {
279
+ return { ...v, id: castIdValue(v.id) }
280
+ }
281
+ return v
282
+ })
283
+ return one ? rs[0] : rs
284
+ }
285
+
286
+
287
+ function castIdCond(c) {
288
+ //主鍵之條件: 值或運算子物件; 比較運算子之值(陣列逐一)轉型, $not遞迴, 其餘運算子($regex、$exists等)原樣
289
+ if (!isobj(c)) {
290
+ return castIdValue(c)
291
+ }
292
+ return mapOwn(c, (v, k) => {
293
+ if (k === '$not') {
294
+ return castIdCond(v)
295
+ }
296
+ if (['$eq', '$ne', '$gt', '$gte', '$lt', '$lte', '$in', '$nin', '$all'].indexOf(k) >= 0) {
297
+ return isarr(v) ? map(v, castIdValue) : castIdValue(v)
298
+ }
299
+ return v
300
+ })
301
+ }
302
+
303
+
304
+ function castIdFind(find) {
305
+ //查詢條件(select、delAll之find): id之條件轉型, 遞迴$and、$or、$nor; 他欄原樣; 寫於複本, 非物件原樣
306
+ if (!isobj(find)) {
307
+ return find
308
+ }
309
+ return mapOwn(find, (v, k) => {
310
+ if (k === 'id') {
311
+ return castIdCond(v)
312
+ }
313
+ if (['$and', '$or', '$nor'].indexOf(k) >= 0 && isarr(v)) {
314
+ return map(v, castIdFind)
315
+ }
316
+ return v
317
+ })
318
+ }
319
+
320
+
321
+ function getItemsNotObj(data) {
322
+ //寫入之各筆須為一般物件(R-INPUT-01), 回傳[{k}], k為其於輸入內之位置; 單一非物件輸入視為第0筆
323
+ //非物件之元素經adjustData之clearXSS逐鍵複製時, 字串、陣列被展開成{0:...,1:...}、數字等變成{}, 寫入垃圾紀錄
324
+ let rs = []
325
+ each(isarr(data) ? data : [data], (v, k) => {
326
+ if (!isobj(v)) {
327
+ rs.push({ k })
328
+ }
329
+ })
330
+ return rs
331
+ }
332
+
333
+
334
+ function getMixIdKey(v) {
335
+ //mix跨段比對用之主鍵鍵值(R-MIX-01): 以castIdValue正規化(有限數字取其字串形式, 與寫入、刪除之轉型同一函數), 正規化後為非空字串者取之, 其餘(未提供)不參與比對
336
+ //不論資料表是否適用轉型(R-ID-02)皆正規化, 故整數主鍵欄之5與'5'亦視為同鍵
337
+ let id = castIdValue(get(v, 'id'))
338
+ return isestr(id) ? id : null
339
+ }
340
+
341
+
342
+ function getMsgsMixSameId(input) {
343
+ //mix之add與del、del與diff含同一主鍵者回傳各組之訊息(R-MIX-01), 位置以段名[k]列出而不回顯主鍵
344
+ //mix依add、del、diff之序執行, add與del同鍵將使新增因已存在而略過後即被刪除, del與diff同鍵將使刪除後由save以部份欄位重新插入
345
+ let msgs = []
346
+ for (let [ka, kb] of [['add', 'del'], ['del', 'diff']]) {
347
+
348
+ //ma, 前段各主鍵之位置
349
+ let ma = new Map()
350
+ each(get(input, ka, []), (v, k) => {
351
+ let key = getMixIdKey(v)
352
+ if (key !== null) {
353
+ ma.set(key, [...(ma.get(key) || []), k])
354
+ }
355
+ })
356
+
357
+ //ia, ib, 同鍵者之位置
358
+ let ia = []
359
+ let ib = []
360
+ each(get(input, kb, []), (v, k) => {
361
+ let key = getMixIdKey(v)
362
+ if (key !== null && ma.has(key)) {
363
+ ia = [...ia, ...ma.get(key)]
364
+ ib.push(k)
365
+ }
366
+ })
367
+
368
+ //msgs
369
+ if (size(ib) > 0) {
370
+ let refs = [
371
+ ...map([...new Set(ia)].sort((a, b) => a - b), (k) => ({ ref: `${ka}[${k}]` })),
372
+ ...map(ib, (k) => ({ ref: `${kb}[${k}]` })),
373
+ ]
374
+ msgs.push(getMsgFail(`invalid input of mix, same id in ${ka} and ${kb}`, refs))
375
+ }
376
+
377
+ }
378
+ return msgs
379
+ }
380
+
381
+
67
382
  function getItemsIdXSS(data) {
68
383
  //輸入數據之主鍵(非空字串)會被clearXSS改寫者, 回傳[{k,id}], k為其於輸入內之位置
69
384
  //寫入路徑之欄位皆經clearXSS而select與del不經過, 主鍵被改寫後以原主鍵查詢與刪除皆找不到, 故呼叫端須於寫入前整批拒絕
@@ -162,25 +477,27 @@ function adjustData(ds, woName, userId, data, mode = 'save') { //mode: 'insert',
162
477
 
163
478
  //clearXSS
164
479
  if (true) {
165
- data = map(data, (v, k) => {
166
- let dt = {}
167
- each(v, (vv, kk) => {
168
- vv = clearXSS(vv)
169
- dt[kk] = vv
480
+ data = map(data, (v) => {
481
+ return mapOwn(v, (vv) => {
482
+ return clearXSS(vv)
170
483
  })
171
- return dt
172
484
  })
173
485
  }
174
486
 
175
- //id, 未提供有效主鍵者由本套件補值, 令回傳之主鍵與實際寫入者一致, 補值器同各ORM之genIDSeq
176
- //有效主鍵之判定同w-orm系列: 非空字串或數字(reladb、mdb視數字為有效)
177
- //補值寫在上方之複本, 不改呼叫端原物件; 無欄位之列不補, 保留ORM視空物件為無效輸入之行為
487
+ //id, 未提供有效主鍵者由本套件補值, 令回傳之主鍵與實際寫入者一致, 補值器同各ORM之genIDSeq(R-ID-01)
488
+ //有效主鍵為非空字串: 字串型主鍵欄之有限數字已於mapOrm轉為字串(R-ID-02), 剩餘之數字(NaN、±Infinity)與其他型別視為未提供
489
+ //補值寫在上方之複本, 不改呼叫端原物件
490
+ //輸入依wsemi之iseobj、isearr判為無效者(單一空物件、只含一個空物件之陣列)不補, 保留ORM視之為無效輸入(規格T5)而不寫入之行為;
491
+ //多筆批次中之空物件列則補, 因後端照常補值寫入, 不補則回傳undefined而與實際不符
178
492
  if (isPkIdString(ds[woName], keys)) {
179
- each(data, (v) => {
180
- if (size(v) > 0 && !isestr(v.id) && !isnum(v.id)) {
181
- v.id = genIDSeq()
182
- }
183
- })
493
+ let effective = one ? iseobj(data[0]) : isearr(data)
494
+ if (effective) {
495
+ each(data, (v) => {
496
+ if (!isestr(v.id)) {
497
+ v.id = genIDSeq()
498
+ }
499
+ })
500
+ }
184
501
  }
185
502
 
186
503
  //復原data型態, 若原本data為物件則取第0個回傳
@@ -191,6 +508,22 @@ function adjustData(ds, woName, userId, data, mode = 'save') { //mode: 'insert',
191
508
 
192
509
 
193
510
  async function mapOrm(ds, woItems, userId, woName, mode, input) {
511
+ //邊界保證(R-ERR-01 規則5): execMapOrm之檢查、轉型與ORM呼叫階段, 非字串之例外(RangeError、使用端物件之Proxy陷阱/toString拋錯、woItems之非ORM值等)
512
+ //一律console.log原文後以固定字串reject, 不轉出例外物件與其拋出之值; 字串(各處之固定訊息)原樣轉出
513
+ try {
514
+ return await execMapOrm(ds, woItems, userId, woName, mode, input)
515
+ }
516
+ catch (e) {
517
+ if (typeof e === 'string') {
518
+ return Promise.reject(e)
519
+ }
520
+ logSafe(`${typeof woName === 'string' ? woName : ''}.${typeof mode === 'string' ? mode : ''} mapOrm catch`, e)
521
+ return Promise.reject('failed to process input')
522
+ }
523
+ }
524
+
525
+
526
+ async function execMapOrm(ds, woItems, userId, woName, mode, input) {
194
527
  //由woName自動綁定至woItems內的ORM物件
195
528
  let err = null
196
529
  let r
@@ -200,18 +533,28 @@ async function mapOrm(ds, woItems, userId, woName, mode, input) {
200
533
  return Promise.reject(`invalid woItems`)
201
534
  }
202
535
 
203
- //wo
204
- let wo = get(woItems, woName, null)
536
+ //wo, woName須為woItems之自有字串屬性(R-INPUT-01 規則5)
537
+ let wo = getOwnItem(woItems, woName)
205
538
 
206
- //check
539
+ //check, 訊息回顯字串之woName為既有慣例(R-MSG-01 其他已知限制), 非字串者不回顯
207
540
  if (wo === null) {
208
- return Promise.reject(`can not find table[${woName}] in ORM`)
541
+ return Promise.reject(`can not find table[${typeof woName === 'string' ? woName : ''}] in ORM`)
209
542
  }
210
543
 
544
+ //idCast, 主鍵之有限數字以其字串形式處置(R-ID-02), 寫入、刪除與查詢條件共用同一判定
545
+ let idCast = isIdCast(ds, woName)
546
+
211
547
  if (mode === 'select') {
212
548
 
213
- //select
214
- r = await wo.select(input)
549
+ //check, 條件若給予須為一般物件且不含自有__proto__鍵(R-INPUT-01 規則3、R-OBJ-01 規則3), 於轉型與查詢之前
550
+ let errFind = getErrFind('select', input)
551
+ if (errFind !== null) {
552
+ logSafe(`${woName}.select invalid find`, input)
553
+ return Promise.reject(errFind)
554
+ }
555
+
556
+ //select, 條件中主鍵之比較值轉型(R-ID-02)
557
+ r = await wo.select(idCast ? castIdFind(input) : input)
215
558
  .catch((msg) => {
216
559
  console.log(`${woName}.select catch`, msg)
217
560
  err = 'failed to select data'
@@ -228,11 +571,33 @@ async function mapOrm(ds, woItems, userId, woName, mode, input) {
228
571
  //inputOrig, 呼叫端原輸入, 逐筆失敗之訊息以呼叫端視角列出(本套件補出之主鍵只寫於複本, reject時呼叫端未曾取得)
229
572
  let inputOrig = input
230
573
 
574
+ //msgMode
575
+ let msgMode = mode === 'insert' ? 'failed to insert data' : 'failed to save data'
576
+
577
+ //check, 各筆須為一般物件, 否則整批拒絕且不寫入任何一筆(R-INPUT-01)
578
+ let failsNotObj = getItemsNotObj(input)
579
+ if (size(failsNotObj) > 0) {
580
+ console.log(`${woName}.${mode} data is not an object`, failsNotObj)
581
+ return Promise.reject(getMsgFail(`${msgMode}, not an object`, failsNotObj))
582
+ }
583
+
584
+ //check, 各筆任何深度不得含自有__proto__鍵與存取器屬性, 否則整批拒絕且不寫入任何一筆(R-OBJ-01 規則3)
585
+ let msgsBadKey = getMsgsBadKey(msgMode, input)
586
+ if (size(msgsBadKey) > 0) {
587
+ console.log(`${woName}.${mode} data has unsupported key`, msgsBadKey)
588
+ return Promise.reject(join(msgsBadKey, '; '))
589
+ }
590
+
591
+ //castIdRows, 主鍵之有限數字轉為字串, 寫於複本(R-ID-02)
592
+ if (idCast) {
593
+ input = castIdRows(input)
594
+ }
595
+
231
596
  //check, 主鍵會被clearXSS改寫者整批拒絕, 不寫入任何一筆
232
597
  let failsIdXSS = getItemsIdXSS(input)
233
598
  if (size(failsIdXSS) > 0) {
234
599
  console.log(`${woName}.${mode} id changed by clearXSS`, failsIdXSS)
235
- return Promise.reject(getMsgIdXSS(mode === 'insert' ? 'failed to insert data' : 'failed to save data', failsIdXSS))
600
+ return Promise.reject(getMsgIdXSS(msgMode, failsIdXSS))
236
601
  }
237
602
 
238
603
  //adjustData
@@ -290,6 +655,11 @@ async function mapOrm(ds, woItems, userId, woName, mode, input) {
290
655
  }
291
656
  else if (mode === 'del') {
292
657
 
658
+ //castIdRows, 主鍵之有限數字轉為字串, 寫於複本(R-ID-02); 刪除不經clearXSS(R-XSS-01)
659
+ if (idCast) {
660
+ input = castIdRows(input)
661
+ }
662
+
293
663
  //del
294
664
  r = await wo.del(input)
295
665
  .catch((msg) => {
@@ -309,8 +679,15 @@ async function mapOrm(ds, woItems, userId, woName, mode, input) {
309
679
  }
310
680
  else if (mode === 'delAll') {
311
681
 
312
- //delAll
313
- r = await wo.delAll(input)
682
+ //check, 條件若給予須為一般物件(R-INPUT-01 規則3): 各後端對非物件之條件一律視為無條件而刪除全表; 未給(undefined)與空物件依規格為刪除全部; 另不得含自有__proto__鍵(R-OBJ-01 規則3)
683
+ let errFind = getErrFind('delAll', input)
684
+ if (errFind !== null) {
685
+ logSafe(`${woName}.delAll invalid find`, input)
686
+ return Promise.reject(errFind)
687
+ }
688
+
689
+ //delAll, 條件中主鍵之比較值轉型(R-ID-02)
690
+ r = await wo.delAll(idCast ? castIdFind(input) : input)
314
691
  .catch((msg) => {
315
692
  console.log(`${woName}.delAll catch`, msg)
316
693
  err = 'failed to delete data'
@@ -324,6 +701,55 @@ async function mapOrm(ds, woItems, userId, woName, mode, input) {
324
701
  return Promise.reject('invalid input of mix, need the result object of ltdtDiffByKey')
325
702
  }
326
703
 
704
+ //check, add、del、diff為存取器者整批拒絕(R-OBJ-01 規則3 批次容器), 並取快照(各讀一次, 其後只用快照), 使各檢查與寫入所見一致
705
+ for (let key of ['add', 'del', 'diff']) {
706
+ let d = Object.getOwnPropertyDescriptor(input, key)
707
+ if (d !== undefined && (d.get !== undefined || d.set !== undefined)) {
708
+ return Promise.reject(`invalid input of mix, ${MSGS_BAD_KEY.accessor}`)
709
+ }
710
+ }
711
+ input = { add: input.add, del: input.del, diff: input.diff }
712
+
713
+ //check, add、del、diff若給予須為陣列(R-INPUT-01), 否則各段之回傳將逐欄位產生
714
+ for (let key of ['add', 'del', 'diff']) {
715
+ if (input[key] !== undefined && !isarr(input[key])) {
716
+ return Promise.reject('invalid input of mix, need the result object of ltdtDiffByKey')
717
+ }
718
+ }
719
+
720
+ //check, 新增與修改之各筆須為一般物件, 於任何步驟寫入之前整批拒絕(R-INPUT-01)
721
+ let msgsNotObj = []
722
+ for (let [key, msg] of [['add', 'failed to insert data'], ['diff', 'failed to update data']]) {
723
+ let fails = getItemsNotObj(get(input, key, []))
724
+ if (size(fails) > 0) {
725
+ console.log(`${woName}.mix ${key} data is not an object`, fails)
726
+ msgsNotObj.push(getMsgFail(`${msg}, not an object`, fails))
727
+ }
728
+ }
729
+ if (size(msgsNotObj) > 0) {
730
+ return Promise.reject(join(msgsNotObj, '; '))
731
+ }
732
+
733
+ //check, 新增與修改之各筆任何深度不得含自有__proto__鍵與存取器屬性, 於任何步驟寫入之前整批拒絕(R-OBJ-01 規則3); del之各筆不檢查(R-OBJ-01 已知限制)
734
+ let msgsBadKey = []
735
+ for (let [key, msg] of [['add', 'failed to insert data'], ['diff', 'failed to update data']]) {
736
+ msgsBadKey.push(...getMsgsBadKey(msg, get(input, key, [])))
737
+ }
738
+ if (size(msgsBadKey) > 0) {
739
+ console.log(`${woName}.mix data has unsupported key`, msgsBadKey)
740
+ return Promise.reject(join(msgsBadKey, '; '))
741
+ }
742
+
743
+ //castIdRows, 各段主鍵之有限數字轉為字串, 寫於複本(R-ID-02)
744
+ if (idCast) {
745
+ input = { ...input }
746
+ for (let key of ['add', 'del', 'diff']) {
747
+ if (isarr(input[key])) {
748
+ input[key] = castIdRows(input[key])
749
+ }
750
+ }
751
+ }
752
+
327
753
  //check, 新增與修改之主鍵會被clearXSS改寫者, 於任何步驟寫入之前整批拒絕
328
754
  let msgsIdXSS = []
329
755
  for (let [key, msg] of [['add', 'failed to insert data'], ['diff', 'failed to update data']]) {
@@ -337,6 +763,13 @@ async function mapOrm(ds, woItems, userId, woName, mode, input) {
337
763
  return Promise.reject(join(msgsIdXSS, '; '))
338
764
  }
339
765
 
766
+ //check, add與del、del與diff同鍵者, 於任何步驟寫入之前整批拒絕(R-MIX-01)
767
+ let msgsSameId = getMsgsMixSameId(input)
768
+ if (size(msgsSameId) > 0) {
769
+ console.log(`${woName}.mix same id across steps`, msgsSameId)
770
+ return Promise.reject(join(msgsSameId, '; '))
771
+ }
772
+
340
773
  //rt,r
341
774
  let rt
342
775
  r = [] //default to array
@@ -1,5 +1,5 @@
1
- import each from 'lodash-es/each.js'
2
- import keys from 'lodash-es/keys.js'
1
+ import each from 'lodash-es/each.js'
2
+ import keys from 'lodash-es/keys.js'
3
3
  import iseobj from 'wsemi/src/iseobj.mjs'
4
4
  import isfun from 'wsemi/src/isfun.mjs'
5
5
  import isestr from 'wsemi/src/isestr.mjs'
@@ -41,8 +41,8 @@ function getWoItems(ds, WOrm, url, db) {
41
41
  //wo
42
42
  let wo = new WOrm(useOpt)
43
43
 
44
- //save
45
- woItems[v] = wo
44
+ //save, 以defineProperty建立自有屬性: 不用woItems[v]=wo, ds之鍵為__proto__時會改設woItems之原型而該表不可用(R-INPUT-01 規則5)
45
+ Object.defineProperty(woItems, v, { value: wo, enumerable: true, writable: true, configurable: true })
46
46
 
47
47
  // //change, 當數據有改變時清除快取
48
48
  // wo.on('change', (mode) => {