w-serv-orm 1.1.15 → 1.1.16

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
@@ -82,6 +82,24 @@ function getOwnItem(o, k) {
82
82
  }
83
83
 
84
84
 
85
+ function isArr(v) {
86
+ //是否為真陣列(Array.isArray, 不用wsemi isarr: 其以Object.prototype.toString判定, 可被Symbol.toStringTag宣稱而走訪其length getter, R-OBJ-01 規則7 ④)
87
+ //已撤銷之Proxy使Array.isArray拋TypeError, 視為非陣列(由列檢查以irregular回報, 不觸發陷阱)
88
+ try {
89
+ return Array.isArray(v)
90
+ }
91
+ catch (e) {
92
+ return false
93
+ }
94
+ }
95
+
96
+
97
+ function isAcc(d) {
98
+ //描述子是否為存取器: 以有無value欄位判斷, 不以get/set是否非undefined(get與set皆為undefined之存取器 Object.defineProperty(o, 'id', { get: undefined, set: undefined, enumerable: true }) 亦為存取器)
99
+ return !Object.prototype.hasOwnProperty.call(d, 'value')
100
+ }
101
+
102
+
85
103
  function isIdCast(ds, woName) {
86
104
  //資料表之主鍵欄為字串型或型別未知者, 主鍵之有限數字以其字串形式處置(R-ID-02); ds無該表者不轉型, 令select與del照舊原樣交ORM
87
105
  let dsTab = getOwnItem(ds, woName)
@@ -115,12 +133,14 @@ function mapOwn(o, f) {
115
133
  }
116
134
 
117
135
 
118
- //不支援之鍵或屬性的原因與訊息(R-OBJ-01 規則3、5、6), 訊息只列固定字句, 不回顯客戶端之值
136
+ //不支援之鍵或屬性的原因與訊息(R-OBJ-01 規則3、5、5續、6、7), 訊息只列固定字句, 不回顯客戶端之值
119
137
  const MSGS_BAD_KEY = Object.freeze({
120
138
  proto: 'unsupported key[__proto__]',
121
139
  accessor: 'unsupported accessor property',
122
140
  unreadable: 'unreadable value',
123
141
  deep: 'too deep',
142
+ large: 'too large',
143
+ sparse: 'unsupported sparse array',
124
144
  hidden: 'unsupported symbol key or non-enumerable property',
125
145
  irregular: 'unsupported irregular object or array',
126
146
  })
@@ -130,6 +150,15 @@ const MSGS_BAD_KEY = Object.freeze({
130
150
  //64個容器層約為32層邏輯巢狀, 遠大於合理查詢(庫內最深約7個容器)而遠小於爆堆疊之閾值
131
151
  const MAX_DEPTH_FIND = 64
132
152
 
153
+ //原型鏈長度上限(isCleanChain; R-OBJ-01 規則6、7)
154
+ const MAX_CHAIN = 64
155
+
156
+ //find展開後之物件總數上限(不含陣列; 共用子結構依出現次數重複計, R-OBJ-01 規則5續)
157
+ //getReasonBadKey以seen去重而castIdFind依路徑展開, 指數型共用子結構(x_{i+1}={$and:[x_i,x_i]})k層展開2^(k+1)-1個物件, 實測k=22約5.7秒、+762MB, 256MB堆積下k=22即OOM崩潰(無法catch);
158
+ //與wsemi cvOrmFind之maxNodes(100000)同值且口徑為其子集: 後端計節點(條件物件、運算子物件、純量各計1, $and/$or之陣列不計), 本層只計物件, 故凡本層因逾此值拒收者後端必也拒收, 本層不拒收後端會接受者
159
+ //(castIdFind只經物件遞迴$and、$or之元素, 陣列元素不展開, 故不計陣列仍足以限制其展開量)
160
+ const MAX_NODES_FIND = 100000
161
+
133
162
  //find之字串碼元總長(字串值與物件鍵, 含重複出現者)逾此值不做整棵結構化複製, 改走精確品牌探針(R-OBJ-01 規則6 ⑥)
134
163
  //structuredClone對重複出現之字串每次皆寫出並讀回, 記憶體與(字串碼元總長 x 出現次數)成正比, 同一64KB字串於$in內重複1萬次即邏輯長度655M碼元; 與wsemi cvOrmFind之CLONE_MAX_CHARS同值
135
164
  const MAX_CHARS_CLONE = 4000000
@@ -150,8 +179,10 @@ const probeToken = {}
150
179
  function isCleanChain(o) {
151
180
  //o及其原型鏈上無Proxy、且無任何自有Symbol.toStringTag(R-OBJ-01 規則6): 全程只用util.types.isProxy(不觸發陷阱)、Object.getPrototypeOf與自有屬性查詢(不讀值),
152
181
  //故不執行使用端程式(Proxy之陷阱、標籤getter); 不用in運算子: 其遇Proxy原型會執行has陷阱, 令檢查之值與使用之值可不同(外審第二輪 A-02)
182
+ //原型鏈長度逾MAX_CHAIN者視為不乾淨(合法物件之原型鏈至多數層), 令每次分類之成本有界: 共用陣列帶P層原型而被引用E次時, measureFind與castIdFind之分類為E x P(外審 sol F08)
183
+ let n = 0
153
184
  for (let x = o; x !== null; x = Object.getPrototypeOf(x)) {
154
- if (types.isProxy(x) || Object.prototype.hasOwnProperty.call(x, Symbol.toStringTag)) {
185
+ if (n++ > MAX_CHAIN || types.isProxy(x) || Object.prototype.hasOwnProperty.call(x, Symbol.toStringTag)) {
155
186
  return false
156
187
  }
157
188
  }
@@ -335,7 +366,7 @@ function snapSafe(v, st = { n: 0 }, depth = 0) {
335
366
  if (d === undefined) {
336
367
  return '[empty]'
337
368
  }
338
- if (d.get !== undefined || d.set !== undefined) {
369
+ if (isAcc(d)) {
339
370
  return '[Getter/Setter]'
340
371
  }
341
372
  return snapSafe(d.value, st, depth + 1)
@@ -378,12 +409,114 @@ function logFind(label, input) {
378
409
  }
379
410
 
380
411
 
412
+ function getRowId(row) {
413
+ //寫入列(及mix之del列)之主鍵值: 只認自有、可列舉之資料屬性, 其餘(不可列舉、繼承、存取器、無此屬性)視為未提供, 不執行任何getter
414
+ //與adjustData之mapOwn(只複製自有可列舉鍵)一致, 令本套件所見之id即實際寫入之id
415
+ if (typeof row !== 'object' || row === null) {
416
+ return undefined
417
+ }
418
+ let d = Object.getOwnPropertyDescriptor(row, 'id')
419
+ if (d === undefined || !d.enumerable || isAcc(d)) {
420
+ return undefined
421
+ }
422
+ return d.value
423
+ }
424
+
425
+
426
+ function firstBadIndex(a) {
427
+ //陣列首個空洞或存取器索引, 回傳[索引, 'sparse'|'accessor'], 無則null; 成本為該索引之位置, 稀疏巨陣列於索引0即返回
428
+ //索引為存取器者, 其後各步驟(each、map以array[index]讀取)會執行使用端程式並可每次回傳不同列(外審 sol F01)
429
+ for (let i = 0; i < a.length; i++) {
430
+ let d = Object.getOwnPropertyDescriptor(a, i)
431
+ if (d === undefined) {
432
+ return [i, 'sparse']
433
+ }
434
+ if (isAcc(d)) {
435
+ return [i, 'accessor']
436
+ }
437
+ }
438
+ return null
439
+ }
440
+
441
+
442
+ function getKidsFind(o) {
443
+ //容器o之容器子節點(依出現次數, 含重複); 呼叫時find已通過strict檢查(只含乾淨之純物件與密陣列, 無存取器), 故可直接以描述子讀值
444
+ let rs = []
445
+ let ks = Array.isArray(o) ? null : Object.keys(o)
446
+ let n = ks === null ? o.length : ks.length
447
+ for (let i = 0; i < n; i++) {
448
+ let v = Object.getOwnPropertyDescriptor(o, ks === null ? i : ks[i]).value
449
+ if (getKindFind(v) !== null) {
450
+ rs.push(v)
451
+ }
452
+ }
453
+ return rs
454
+ }
455
+
456
+
457
+ function measureFind(root, maxDepth, maxNodes) {
458
+ //find之最長路徑深度(容器=物件與陣列)與展開後物件數(不含陣列; 共用子結構依出現次數重複計), 通過回傳null, 逾上限回傳'deep'(含循環)或'large'(R-OBJ-01 規則5續)
459
+ //getReasonBadKey以seen去重而只計首次到達之深度, 共用子結構(DAG)可繞過maxDepth, 且castIdFind依路徑展開, 指數型DAG展開2^k個物件
460
+ //作法: 顯式堆疊之記憶化後序走訪, 每個容器只展開一次(O(節點+邊)); 堆疊深度不超過maxDepth+1(逾即返回); 展開數於逾maxNodes即返回, 不產生大數
461
+ let st = new Map() //容器 -> -1(走訪中, 遇到即為循環) | 展開後容器數(已完成)
462
+ let dep = new Map() //容器 -> 以其為根之最長路徑容器數
463
+ let stack = [{ o: root, kids: getKidsFind(root), i: 0, d: 0, n: Array.isArray(root) ? 0 : 1 }]
464
+ st.set(root, -1)
465
+ while (stack.length > 0) {
466
+ let f = stack[stack.length - 1]
467
+ if (stack.length > maxDepth) {
468
+ return 'deep'
469
+ }
470
+ if (f.i < f.kids.length) {
471
+ let c = f.kids[f.i++]
472
+ let s = st.get(c)
473
+ if (s === undefined) {
474
+ st.set(c, -1)
475
+ stack.push({ o: c, kids: getKidsFind(c), i: 0, d: 0, n: Array.isArray(c) ? 0 : 1 })
476
+ }
477
+ else if (s === -1) {
478
+ return 'deep'
479
+ }
480
+ else {
481
+ f.n += s
482
+ f.d = Math.max(f.d, dep.get(c))
483
+ if (f.n > maxNodes) {
484
+ return 'large'
485
+ }
486
+ if (stack.length + dep.get(c) > maxDepth) {
487
+ return 'deep'
488
+ }
489
+ }
490
+ }
491
+ else {
492
+ stack.pop()
493
+ st.set(f.o, f.n)
494
+ dep.set(f.o, f.d + 1)
495
+ if (stack.length > 0) {
496
+ let p = stack[stack.length - 1]
497
+ p.n += f.n
498
+ p.d = Math.max(p.d, f.d + 1)
499
+ if (p.n > maxNodes) {
500
+ return 'large'
501
+ }
502
+ if (stack.length + dep.get(f.o) > maxDepth) {
503
+ return 'deep'
504
+ }
505
+ }
506
+ }
507
+ }
508
+ return null
509
+ }
510
+
511
+
381
512
  function getReasonBadKey(v, maxDepth = Infinity, strict = false) {
382
513
  //v之任何深度(一般物件之值、陣列之元素)含不支援之鍵或屬性者回傳原因(R-OBJ-01 規則3、5、6), 通過回傳null:
383
514
  // proto: 含自有__proto__鍵, 不論可否列舉; 自有鍵可由JSON.parse、Object.fromEntries、展開、structuredClone等產生, 物件字面量之__proto__為設原型而不算
384
515
  // accessor: 含存取器屬性(getter或setter), 其值每讀一次可能不同, 令檢查之值與使用之值無法保證相同, 且讀取可能有副作用
385
516
  // unreadable: 檢查時拋錯, 如Proxy之陷阱或已撤銷之Proxy
517
+ // sparse: 陣列有空洞(寫入列; 逐索引迴圈對稀疏巨陣列近乎不終止, 其後clearXSS又把空洞展成密陣列, 規則7 ⑤; find之strict路徑同樣拒收但回irregular)
386
518
  // deep: 容器(一般物件與陣列)層數超過maxDepth(根為第1層), 走訪到第maxDepth+1層即停止; 只有find給予maxDepth, 寫入列不設上限(見R-OBJ-01 規則5)
519
+ // large、deep(共用子結構與循環): strict走訪全數通過後由measureFind判定最長路徑與展開後容器數(規則5續), 因seen去重只計首次到達之深度
387
520
  //strict(只有find開啟, 規則6): castIdFind、castIdCond以mapOwn(只走自有可列舉字串鍵)與lodash map(只走索引)複製find, 複本會洗掉下列結構,
388
521
  //令後端白名單版本會reject之條件變成較寬甚至無條件(delAll刪全表), 故走訪到之每個節點須為複本與原物件等價者, 容器之分類與複製共用getKindFind:
389
522
  // irregular: 有Symbol.toStringTag(原型鏈上任一層之自有屬性, 含內建品牌物件與宣稱標籤者, 以hasOwnProperty判斷而不讀取)、物件節點非純物件(類別實例、偽原型等, 同頂層之isPlainObj)、
@@ -392,7 +525,7 @@ function getReasonBadKey(v, maxDepth = Infinity, strict = false) {
392
525
  // hidden: 物件節點有Symbol鍵或不可列舉鍵
393
526
  //寫入列不開strict: 後端自己亦略過Symbol鍵與不可列舉鍵, 複本不改變其語意
394
527
  //只走一般物件與陣列(JSON可表達之資料)之可列舉資料屬性, 以屬性描述子讀值而不呼叫getter; 其餘型別(Date、函數等)不進入
395
- //以堆疊迭代避免本檢查爆呼叫堆疊(其後castIdFind、castIdCond之遞迴由maxDepth保護), seen避免循環結構無窮迭代(循環不會被判為過深, 見R-OBJ-01 已知限制)
528
+ //以堆疊迭代避免本檢查爆呼叫堆疊(其後castIdFind、castIdCond之遞迴由maxDepth保護), seen避免循環結構無窮迭代(循環與共用子結構之深度與展開由其後之measureFind判定)
396
529
  try {
397
530
  let seen = new Set()
398
531
  let stack = [v, 1] //成對: 節點, 其層數
@@ -449,12 +582,10 @@ function getReasonBadKey(v, maxDepth = Infinity, strict = false) {
449
582
  for (let i = 0; i < n; i++) {
450
583
  let d = Object.getOwnPropertyDescriptor(o, arr ? i : ks[i])
451
584
  if (d === undefined) {
452
- if (strict) {
453
- return 'irregular' //空洞: map以array[index]讀到原型上之繼承值而成為密陣列(寫入列則不檢查)
454
- }
455
- continue //稀疏陣列之空洞
585
+ //空洞: find的map以array[index]讀到原型上之繼承值而成為密陣列; 寫入列逐索引迴圈對稀疏巨陣列(如new Array(2**32-1))近乎不終止, 其後的複製又把空洞展成密陣列
586
+ return strict ? 'irregular' : 'sparse'
456
587
  }
457
- if (d.get !== undefined || d.set !== undefined) {
588
+ if (isAcc(d)) {
458
589
  return 'accessor'
459
590
  }
460
591
  if (strict && arr && !d.enumerable) {
@@ -477,6 +608,11 @@ function getReasonBadKey(v, maxDepth = Infinity, strict = false) {
477
608
  //品牌物件與不可複製者之通用網, 於走訪全數通過(已無存取器)之後: 字串碼元總長在預算內且有structuredClone時以一次結構化複製證明,
478
609
  //否則(逾預算或無structuredClone)對走訪到之物件節點逐一以精確探針判斷util.types無對應判定之WeakRef與FinalizationRegistry(規則6 ⑥)
479
610
  if (strict && typeof v === 'object' && v !== null) {
611
+ //最長路徑深度與展開後容器數(共用子結構、循環), 於結構化複製之前: 走訪全數通過故已無存取器與Proxy
612
+ let rm = measureFind(v, maxDepth, MAX_NODES_FIND)
613
+ if (rm !== null) {
614
+ return rm
615
+ }
480
616
  if (chars <= MAX_CHARS_CLONE && typeof structuredClone === 'function') {
481
617
  if (hasIrregularClone(v)) {
482
618
  return 'irregular'
@@ -523,6 +659,72 @@ function getErrFind(mode, find) {
523
659
  }
524
660
 
525
661
 
662
+ function isIrregularRow(v) {
663
+ //寫入列是否為不乾淨之物件: Proxy、原型鏈上有Symbol.toStringTag、內建品牌物件(原型改為null之Map等無標籤可辨, 會被mapOwn洗成{}而寫入垃圾列)
664
+ //全程不執行使用端程式(同find之分類); 類別實例與Object.create(proto)之列仍照常(mapOwn只複製自有可列舉鍵, 與後端一致), 此為與find之strict判準的差異
665
+ //函數(含callable Proxy)亦判之: 宣稱Symbol.toStringTag為Object之函數可通過isobj而被當成列(外審 sol F02); 無標籤之普通函數仍由isobj判為非物件(not an object)
666
+ if (typeof v === 'function') {
667
+ return !isCleanChain(v)
668
+ }
669
+ return typeof v === 'object' && v !== null && (!isCleanChain(v) || isBrandObj(v))
670
+ }
671
+
672
+
673
+ function getItemsIrregularId(data) {
674
+ //mix之del列: 後端讀id之方式不一(lodash cloneDeep保留原型而可讀到繼承之id並執行getter, 不可列舉之id有的後端讀得到), 本套件之跨段同鍵比對(R-MIX-01)只認自有可列舉之資料屬性,
675
+ //id為存取器、繼承而來、不可列舉、或列為Proxy/帶標籤者, 後端與本套件所見之id可不同, 令同鍵防線失效(新增後即被刪除), 故整批拒絕; 回傳[{k}]
676
+ let rs = []
677
+ each(data, (v, k) => {
678
+ if ((typeof v !== 'object' || v === null) && typeof v !== 'function') {
679
+ return
680
+ }
681
+ if (!isCleanChain(v)) {
682
+ rs.push({ k }) //含Proxy(isCleanChain已含自身之判斷)與callable Proxy、宣稱標籤之函數(外審 sol F02)
683
+ return
684
+ }
685
+ if (typeof v === 'function') {
686
+ return //普通函數: 後端無id可讀, 同單獨del交後端逐筆處置
687
+ }
688
+ for (let x = v; x !== null; x = Object.getPrototypeOf(x)) {
689
+ let d = Object.getOwnPropertyDescriptor(x, 'id')
690
+ if (d !== undefined) {
691
+ if (x !== v || isAcc(d) || !d.enumerable) {
692
+ rs.push({ k })
693
+ }
694
+ return
695
+ }
696
+ }
697
+ })
698
+ return rs
699
+ }
700
+
701
+
702
+ function getErrBatch(msg, data) {
703
+ //批次容器(insert、save、del之輸入陣列, mix之add、del、diff)須為乾淨之真密陣列, 通過回傳null, 否則回傳reject之字串
704
+ //Proxy與原型鏈上有標籤者: 讀length與索引會執行使用端程式; 空洞: 迴圈與lodash each、map依length逐索引, 稀疏巨陣列(如new Array(2**32-1))近乎不終止並配置同數量之物件,
705
+ //且空洞索引在原型上有資料時, 後續各步驟(each、map以array[index])讀到未經檢查之繼承列
706
+ //單一物件輸入(非陣列)不在此列, 由列檢查處理
707
+ let arr
708
+ try {
709
+ arr = Array.isArray(data)
710
+ }
711
+ catch (e) {
712
+ return `${msg}, ${MSGS_BAD_KEY.irregular}` //已撤銷之Proxy: 無法分類為陣列或單一列, 於任何讀取與ORM呼叫之前固定拒收(外審 sol F06)
713
+ }
714
+ if (!arr) {
715
+ return null
716
+ }
717
+ if (!isCleanChain(data)) {
718
+ return `${msg}, ${MSGS_BAD_KEY.irregular}`
719
+ }
720
+ let bad = firstBadIndex(data)
721
+ if (bad !== null) {
722
+ return getMsgFail(`${msg}, ${MSGS_BAD_KEY[bad[1]]}`, [{ k: bad[0] }])
723
+ }
724
+ return null
725
+ }
726
+
727
+
526
728
  function getMsgsBadKey(msg, data) {
527
729
  //寫入之各筆任何深度含不支援之鍵或屬性者(R-OBJ-01 規則3), 依原因分組回傳訊息陣列, 各組列其於輸入內之位置data[k]; 單一物件輸入視為第0筆
528
730
  //輸入為陣列時(批次容器)逐索引以屬性描述子取值: 索引為存取器即reject, 使其後各次讀取皆為資料屬性之讀取而與檢查之值一致
@@ -533,18 +735,18 @@ function getMsgsBadKey(msg, data) {
533
735
  }
534
736
  groups.get(reason).push({ k })
535
737
  }
536
- if (Array.isArray(data)) {
738
+ if (isArr(data)) {
537
739
  for (let k = 0; k < data.length; k++) {
538
740
  try {
539
741
  let d = Object.getOwnPropertyDescriptor(data, k)
540
742
  if (d === undefined) {
541
- continue //空洞, 已於形狀檢查以非物件拒絕
743
+ continue //空洞, 已由getErrBatch拒收(此處為縱深防禦, 不可達)
542
744
  }
543
- if (d.get !== undefined || d.set !== undefined) {
544
- addFail('accessor', k)
745
+ if (isAcc(d)) {
746
+ addFail('accessor', k) //索引存取器亦已由getErrBatch拒收(縱深防禦)
545
747
  continue
546
748
  }
547
- let reason = getReasonBadKey(d.value)
749
+ let reason = isIrregularRow(d.value) ? 'irregular' : getReasonBadKey(d.value)
548
750
  if (reason !== null) {
549
751
  addFail(reason, k)
550
752
  }
@@ -555,7 +757,7 @@ function getMsgsBadKey(msg, data) {
555
757
  }
556
758
  }
557
759
  else {
558
- let reason = getReasonBadKey(data)
760
+ let reason = isIrregularRow(data) ? 'irregular' : getReasonBadKey(data)
559
761
  if (reason !== null) {
560
762
  addFail(reason, 0)
561
763
  }
@@ -564,12 +766,27 @@ function getMsgsBadKey(msg, data) {
564
766
  }
565
767
 
566
768
 
769
+ function copyRowWithId(v, id) {
770
+ //列之複本: 自有可列舉屬性(含Symbol鍵)以其描述子複製而不讀值(存取器原樣保留、不執行), 僅id換為id; 原物件展開 { ...v, id } 會執行可列舉Symbol鍵與其他可列舉getter(外審 sol F03)
771
+ let r = {}
772
+ for (let k of Reflect.ownKeys(v)) {
773
+ let d = Object.getOwnPropertyDescriptor(v, k)
774
+ if (d.enumerable) {
775
+ Object.defineProperty(r, k, k === 'id' ? { value: id, writable: true, enumerable: true, configurable: true } : d)
776
+ }
777
+ }
778
+ return r
779
+ }
780
+
781
+
567
782
  function castIdRows(rows) {
568
783
  //寫入與刪除之數據: 各列之主鍵正規化, 寫於複本, 不改呼叫端之物件; 非物件之列原樣
569
- let one = !isarr(rows)
784
+ //主鍵只認自有可列舉之資料屬性(getRowId, R-OBJ-01 規則7 ①): 不可列舉、繼承、經存取器者視為未提供, 不轉型亦不執行getter
785
+ let one = !isArr(rows)
570
786
  let rs = map(one ? [rows] : rows, (v) => {
571
- if (isobj(v) && isIdNumFinite(v.id)) {
572
- return { ...v, id: castIdValue(v.id) }
787
+ let id = getRowId(v) //只讀自有可列舉之資料屬性, 不執行getter, 與adjustData之mapOwn所見一致
788
+ if (isIdNumFinite(id) && isobj(v)) {
789
+ return copyRowWithId(v, castIdValue(id))
573
790
  }
574
791
  return v
575
792
  })
@@ -615,7 +832,10 @@ function getItemsNotObj(data) {
615
832
  //寫入之各筆須為一般物件(R-INPUT-01), 回傳[{k}], k為其於輸入內之位置; 單一非物件輸入視為第0筆
616
833
  //非物件之元素經adjustData之clearXSS逐鍵複製時, 字串、陣列被展開成{0:...,1:...}、數字等變成{}, 寫入垃圾紀錄
617
834
  let rs = []
618
- each(isarr(data) ? data : [data], (v, k) => {
835
+ each(isArr(data) ? data : [data], (v, k) => {
836
+ if (isIrregularRow(v)) {
837
+ return //由getMsgsBadKey以irregular回報, 且不對其執行isobj(會讀Symbol.toStringTag而執行使用端程式)
838
+ }
619
839
  if (!isobj(v)) {
620
840
  rs.push({ k })
621
841
  }
@@ -626,8 +846,9 @@ function getItemsNotObj(data) {
626
846
 
627
847
  function getMixIdKey(v) {
628
848
  //mix跨段比對用之主鍵鍵值(R-MIX-01): 以castIdValue正規化(有限數字取其字串形式, 與寫入、刪除之轉型同一函數), 正規化後為非空字串者取之, 其餘(未提供)不參與比對
849
+ //主鍵經getRowId只認自有可列舉之資料屬性, 即adjustData之mapOwn實際寫入者(原get(v, 'id')會讀到不可列舉、繼承、經存取器之id而誤拒, R-MIX-01 規則6)
629
850
  //不論資料表是否適用轉型(R-ID-02)皆正規化, 故整數主鍵欄之5與'5'亦視為同鍵
630
- let id = castIdValue(get(v, 'id'))
851
+ let id = castIdValue(getRowId(v))
631
852
  return isestr(id) ? id : null
632
853
  }
633
854
 
@@ -673,11 +894,11 @@ function getMsgsMixSameId(input) {
673
894
 
674
895
 
675
896
  function getItemsIdXSS(data) {
676
- //輸入數據之主鍵(非空字串)會被clearXSS改寫者, 回傳[{k,id}], k為其於輸入內之位置
897
+ //輸入數據之主鍵(非空字串)會被clearXSS改寫者, 回傳[{k,id}], k為其於輸入內之位置; 主鍵經getRowId只認自有可列舉之資料屬性(不執行getter)
677
898
  //寫入路徑之欄位皆經clearXSS而select與del不經過, 主鍵被改寫後以原主鍵查詢與刪除皆找不到, 故呼叫端須於寫入前整批拒絕
678
899
  let rs = []
679
- each(isarr(data) ? data : [data], (v, k) => {
680
- let id = get(v, 'id')
900
+ each(isArr(data) ? data : [data], (v, k) => {
901
+ let id = getRowId(v)
681
902
  if (isestr(id) && clearXSS(id) !== id) {
682
903
  rs.push({ k, id })
683
904
  }
@@ -694,6 +915,13 @@ function getMsgIdXSS(msg, fails) {
694
915
  }
695
916
 
696
917
 
918
+ function setOwnData(o, k, v) {
919
+ //以defineProperty寫入自有可列舉之資料屬性, 不用賦值: 賦值會執行原型上之setter(類別DTO以setter攔截稽核欄位時, 其副作用可在跨段同鍵檢查之後改寫id, 外審 sol F04);
920
+ //結果與無setter時之賦值相同(呼叫端物件多一個自有欄位, 既有行為見R-OBJ-01 已知限制), 凍結物件仍拋TypeError而由adjustData之呼叫端轉為failed to adjust data
921
+ Object.defineProperty(o, k, { value: v, writable: true, enumerable: true, configurable: true })
922
+ }
923
+
924
+
697
925
  function adjustData(ds, woName, userId, data, mode = 'save') { //mode: 'insert', 'save', 'insert+save'
698
926
 
699
927
  //check
@@ -721,7 +949,7 @@ function adjustData(ds, woName, userId, data, mode = 'save') { //mode: 'insert',
721
949
  //userId
722
950
  if (keys.indexOf('userId') >= 0) {
723
951
  each(data, (v, k) => {
724
- data[k].userId = userId
952
+ setOwnData(data[k], 'userId', userId)
725
953
  })
726
954
  }
727
955
 
@@ -729,21 +957,21 @@ function adjustData(ds, woName, userId, data, mode = 'save') { //mode: 'insert',
729
957
  if (keys.indexOf('timeCreate') >= 0) {
730
958
 
731
959
  each(data, (v, k) => {
732
- data[k].timeCreate = t
960
+ setOwnData(data[k], 'timeCreate', t)
733
961
  })
734
962
  }
735
963
 
736
964
  //userIdUpdate
737
965
  if (keys.indexOf('userIdUpdate') >= 0) {
738
966
  each(data, (v, k) => {
739
- data[k].userIdUpdate = userId
967
+ setOwnData(data[k], 'userIdUpdate', userId)
740
968
  })
741
969
  }
742
970
 
743
971
  //timeUpdate
744
972
  if (keys.indexOf('timeUpdate') >= 0) {
745
973
  each(data, (v, k) => {
746
- data[k].timeUpdate = t
974
+ setOwnData(data[k], 'timeUpdate', t)
747
975
  })
748
976
  }
749
977
 
@@ -755,14 +983,14 @@ function adjustData(ds, woName, userId, data, mode = 'save') { //mode: 'insert',
755
983
  //userIdUpdate
756
984
  if (keys.indexOf('userIdUpdate') >= 0) {
757
985
  each(data, (v, k) => {
758
- data[k].userIdUpdate = userId
986
+ setOwnData(data[k], 'userIdUpdate', userId)
759
987
  })
760
988
  }
761
989
 
762
990
  //timeUpdate
763
991
  if (keys.indexOf('timeUpdate') >= 0) {
764
992
  each(data, (v, k) => {
765
- data[k].timeUpdate = t
993
+ setOwnData(data[k], 'timeUpdate', t)
766
994
  })
767
995
  }
768
996
 
@@ -867,6 +1095,13 @@ async function execMapOrm(ds, woItems, userId, woName, mode, input) {
867
1095
  //msgMode
868
1096
  let msgMode = mode === 'insert' ? 'failed to insert data' : 'failed to save data'
869
1097
 
1098
+ //check, 批次容器須為乾淨之真密陣列(Proxy、標籤、空洞), 於讀取任何元素之前
1099
+ let errBatch = getErrBatch(msgMode, input)
1100
+ if (errBatch !== null) {
1101
+ console.log(`${woName}.${mode} invalid batch container`)
1102
+ return Promise.reject(errBatch)
1103
+ }
1104
+
870
1105
  //check, 各筆須為一般物件, 否則整批拒絕且不寫入任何一筆(R-INPUT-01)
871
1106
  let failsNotObj = getItemsNotObj(input)
872
1107
  if (size(failsNotObj) > 0) {
@@ -948,6 +1183,13 @@ async function execMapOrm(ds, woItems, userId, woName, mode, input) {
948
1183
  }
949
1184
  else if (mode === 'del') {
950
1185
 
1186
+ //check, 批次容器須為乾淨之真密陣列; 刪除之各筆仍不檢查(交後端逐筆處置)
1187
+ let errBatch = getErrBatch('failed to delete data', input)
1188
+ if (errBatch !== null) {
1189
+ console.log(`${woName}.del invalid batch container`)
1190
+ return Promise.reject(errBatch)
1191
+ }
1192
+
951
1193
  //castIdRows, 主鍵之有限數字轉為字串, 寫於複本(R-ID-02); 刪除不經clearXSS(R-XSS-01)
952
1194
  if (idCast) {
953
1195
  input = castIdRows(input)
@@ -989,27 +1231,42 @@ async function execMapOrm(ds, woItems, userId, woName, mode, input) {
989
1231
  }
990
1232
  else if (mode === 'mix') {
991
1233
 
992
- //check
993
- if (!iseobj(input)) {
1234
+ //check, input須為乾淨之一般物件: Proxy(含已撤銷者)、原型鏈有標籤者於讀取任何屬性之前拒收(iseobj讀標籤、input.add讀屬性皆會執行使用端程式, 外審 minimax F3)
1235
+ if (typeof input !== 'object' || input === null || !isCleanChain(input) || !iseobj(input)) {
994
1236
  return Promise.reject('invalid input of mix, need the result object of ltdtDiffByKey')
995
1237
  }
996
1238
 
997
- //check, add、del、diff為存取器者整批拒絕(R-OBJ-01 規則3 批次容器), 並取快照(各讀一次, 其後只用快照), 使各檢查與寫入所見一致
1239
+ //check, add、del、diff為存取器者整批拒絕(R-OBJ-01 規則3 批次容器), 並取自有資料屬性之快照(各讀一次, 其後只用快照; 繼承而來者不取), 使各檢查與寫入所見一致
1240
+ let snap = {}
998
1241
  for (let key of ['add', 'del', 'diff']) {
999
1242
  let d = Object.getOwnPropertyDescriptor(input, key)
1000
- if (d !== undefined && (d.get !== undefined || d.set !== undefined)) {
1243
+ if (d !== undefined && isAcc(d)) {
1001
1244
  return Promise.reject(`invalid input of mix, ${MSGS_BAD_KEY.accessor}`)
1002
1245
  }
1246
+ snap[key] = d === undefined ? undefined : d.value
1003
1247
  }
1004
- input = { add: input.add, del: input.del, diff: input.diff }
1248
+ input = snap
1005
1249
 
1006
1250
  //check, add、del、diff若給予須為陣列(R-INPUT-01), 否則各段之回傳將逐欄位產生
1007
1251
  for (let key of ['add', 'del', 'diff']) {
1008
- if (input[key] !== undefined && !isarr(input[key])) {
1252
+ if (input[key] !== undefined && !isArr(input[key])) {
1009
1253
  return Promise.reject('invalid input of mix, need the result object of ltdtDiffByKey')
1010
1254
  }
1011
1255
  }
1012
1256
 
1257
+ //check, add、del、diff須為乾淨之真密陣列(Proxy、標籤、空洞), 於讀取任何元素之前
1258
+ let msgsBatch = []
1259
+ for (let [key, msg] of [['add', 'failed to insert data'], ['del', 'failed to delete data'], ['diff', 'failed to update data']]) {
1260
+ let errBatch = getErrBatch(msg, input[key])
1261
+ if (errBatch !== null) {
1262
+ msgsBatch.push(errBatch)
1263
+ }
1264
+ }
1265
+ if (size(msgsBatch) > 0) {
1266
+ console.log(`${woName}.mix invalid batch container`)
1267
+ return Promise.reject(join(msgsBatch, '; '))
1268
+ }
1269
+
1013
1270
  //check, 新增與修改之各筆須為一般物件, 於任何步驟寫入之前整批拒絕(R-INPUT-01)
1014
1271
  let msgsNotObj = []
1015
1272
  for (let [key, msg] of [['add', 'failed to insert data'], ['diff', 'failed to update data']]) {
@@ -1033,6 +1290,13 @@ async function execMapOrm(ds, woItems, userId, woName, mode, input) {
1033
1290
  return Promise.reject(join(msgsBadKey, '; '))
1034
1291
  }
1035
1292
 
1293
+ //check, del列之id不得為存取器或繼承而來(同鍵防線之前提), 於任何步驟寫入之前整批拒絕
1294
+ let failsIrregularId = getItemsIrregularId(get(input, 'del', []))
1295
+ if (size(failsIrregularId) > 0) {
1296
+ console.log(`${woName}.mix del id is not an own data property`, failsIrregularId)
1297
+ return Promise.reject(getMsgFail(`failed to delete data, ${MSGS_BAD_KEY.irregular}`, failsIrregularId))
1298
+ }
1299
+
1036
1300
  //castIdRows, 各段主鍵之有限數字轉為字串, 寫於複本(R-ID-02)
1037
1301
  if (idCast) {
1038
1302
  input = { ...input }
@@ -1070,6 +1334,20 @@ async function execMapOrm(ds, woItems, userId, woName, mode, input) {
1070
1334
  //msgsFail, 逐筆失敗僅影響該筆, 不中斷後續步驟, 待全部步驟完成後一併reject; 整批性錯誤則仍立即reject
1071
1335
  let msgsFail = []
1072
1336
 
1337
+ //diff之adjustData提前於任何ORM呼叫之前(取得清除後之深複本): 後端於del步驟會讀取del列(可能執行其上之使用端程式), 其副作用不得改動尚未寫入之diff(外審 sol F03)
1338
+ //add之adjustData原即於第一個ORM呼叫之前; 整批性之adjust失敗亦因而不致發生於add、del已寫入之後
1339
+ let diffAdj = null
1340
+ if (size(get(input, 'diff', [])) > 0) {
1341
+ try {
1342
+ diffAdj = adjustData(ds, woName, userId, get(input, 'diff', []))
1343
+ }
1344
+ catch (e) {
1345
+ console.log(`${woName}.adjustData catch`, e.toString())
1346
+ err = 'failed to adjust data'
1347
+ return Promise.reject(err)
1348
+ }
1349
+ }
1350
+
1073
1351
  let add = get(input, 'add', [])
1074
1352
  if (size(add) > 0) {
1075
1353
 
@@ -1137,15 +1415,8 @@ async function execMapOrm(ds, woItems, userId, woName, mode, input) {
1137
1415
  //diffOrig, 呼叫端原輸入, 逐筆失敗之訊息以呼叫端視角列出
1138
1416
  let diffOrig = diff
1139
1417
 
1140
- //adjustData
1141
- try {
1142
- diff = adjustData(ds, woName, userId, diff)
1143
- }
1144
- catch (e) {
1145
- console.log(`${woName}.adjustData catch`, e.toString())
1146
- err = 'failed to adjust data'
1147
- return Promise.reject(err)
1148
- }
1418
+ //adjustData已於上方提前完成
1419
+ diff = diffAdj
1149
1420
 
1150
1421
  //save
1151
1422
  let rs = await wo.save(diff)