w-serv-orm 1.1.15 → 1.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -0
- package/dist/w-serv-orm.umd.js +2 -2
- package/dist/w-serv-orm.umd.js.map +1 -1
- package/docs/WServOrm.html +12 -6
- package/docs/WServOrm.mjs.html +96 -4
- package/docs/baseline.mjs.html +624 -0
- package/docs/global.html +1622 -0
- package/docs/index.html +2 -2
- package/package.json +1 -1
- package/src/WServOrm.mjs +94 -2
- package/src/baseline.mjs +552 -0
- package/src/getMapOrm.mjs +503 -54
- package/src/getProc.mjs +13 -4
- package/test/api-guard.test.mjs +401 -0
- package/test/api-keepuser.test.mjs +131 -0
- package/test/api-objcopy.test.mjs +277 -0
- package/test/tools/memOrm.mjs +122 -0
- package/test/unit-baseline.test.mjs +766 -0
- package/test/unit-guard.test.mjs +952 -0
- package/test/unit-keepuser.test.mjs +307 -0
- package/test/unit-objcopy.test.mjs +1522 -26
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
|
|
369
|
+
if (isAcc(d)) {
|
|
339
370
|
return '[Getter/Setter]'
|
|
340
371
|
}
|
|
341
372
|
return snapSafe(d.value, st, depth + 1)
|
|
@@ -378,12 +409,227 @@ 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 hasOwnerUserId(row) {
|
|
427
|
+
//寫入列上之userId是否為擁有者: 只認自有、可列舉之資料屬性且為非空字串(與getRowId同一認定, 不執行getter), 其餘(缺欄、空字串、非字串、繼承、不可列舉)視為未提供
|
|
428
|
+
if (typeof row !== 'object' || row === null) {
|
|
429
|
+
return false
|
|
430
|
+
}
|
|
431
|
+
let d = Object.getOwnPropertyDescriptor(row, 'userId')
|
|
432
|
+
return d !== undefined && d.enumerable && !isAcc(d) && typeof d.value === 'string' && d.value !== ''
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
//procOrm第5參數opt允許之鍵(R-ONLYMOD-01、R-KEEPUSER-01), 皆為布林
|
|
437
|
+
const OPT_KEYS_PROC = ['mustExist', 'keepUserId']
|
|
438
|
+
|
|
439
|
+
//mustExist之存在檢查每次select之主鍵數上限(R-ONLYMOD-01 規則 11)
|
|
440
|
+
const EXIST_CHUNK = 1000
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
function getOptProc(mode, opt) {
|
|
444
|
+
//procOrm之第5參數opt: 未給(undefined、null)為空設定; 否則須為純物件且只含mustExist、keepUserId(皆布林), 其餘鍵、存取器、非布林者於任何讀寫之前reject
|
|
445
|
+
//拼錯之鍵(如mustexist)若被靜默忽略, 會令只改不增失效而重新長出已刪之列(keepUserId拼錯則擁有者被覆寫成操作者), 故fail-closed; 訊息不回顯呼叫端之值
|
|
446
|
+
//mustExist只適用save與del; keepUserId只適用insert(save現行即不覆寫userId, mix之新增為內部步驟不開放); 先逐一驗型別再驗mode, 結果與鍵之次序無關
|
|
447
|
+
let bad = (err) => ({ err, mustExist: false, keepUserId: false })
|
|
448
|
+
if (opt === undefined || opt === null) {
|
|
449
|
+
return { err: null, mustExist: false, keepUserId: false }
|
|
450
|
+
}
|
|
451
|
+
if (!isPlainObj(opt)) {
|
|
452
|
+
return bad('invalid opt of procOrm, need an object')
|
|
453
|
+
}
|
|
454
|
+
for (let k of Reflect.ownKeys(opt)) {
|
|
455
|
+
if (OPT_KEYS_PROC.indexOf(k) < 0) {
|
|
456
|
+
return bad('invalid opt of procOrm, unsupported key')
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
let flags = { mustExist: false, keepUserId: false }
|
|
460
|
+
for (let key of OPT_KEYS_PROC) {
|
|
461
|
+
let d = Object.getOwnPropertyDescriptor(opt, key)
|
|
462
|
+
if (d !== undefined) {
|
|
463
|
+
if (isAcc(d) || typeof d.value !== 'boolean') {
|
|
464
|
+
return bad(`invalid opt of procOrm, ${key} needs a boolean`)
|
|
465
|
+
}
|
|
466
|
+
flags[key] = d.value
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (flags.mustExist && mode !== 'save' && mode !== 'del') {
|
|
470
|
+
return bad('invalid opt of procOrm, mustExist supports only save and del')
|
|
471
|
+
}
|
|
472
|
+
if (flags.keepUserId && mode !== 'insert') {
|
|
473
|
+
return bad('invalid opt of procOrm, keepUserId supports only insert')
|
|
474
|
+
}
|
|
475
|
+
return { err: null, mustExist: flags.mustExist, keepUserId: flags.keepUserId }
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
async function hasMissingRow(wo, rows) {
|
|
480
|
+
//rows(寫入或刪除之列)是否有任一列不存在於資料表(含無有效主鍵者: 無從識別即視為不存在), 以select(id $in)分批查詢; true為有缺
|
|
481
|
+
//只認自有可列舉之資料屬性之id(getRowId), 與寫入與刪除實際使用之id同一; 比對以字串形式(整數主鍵與其字串視為同一)
|
|
482
|
+
//此讀取為ORM之select, ORM開啟useCache時可能非新鮮, 屬部署前提(單一行程、同表同一ORM實例、不經快取), 見規則帳本R-ONLYMOD-01
|
|
483
|
+
let ids = []
|
|
484
|
+
let seen = new Set()
|
|
485
|
+
for (let v of (isArr(rows) ? rows : [rows])) {
|
|
486
|
+
let id = getRowId(v)
|
|
487
|
+
if (!((typeof id === 'string' && id !== '') || isIdNumFinite(id))) {
|
|
488
|
+
return true
|
|
489
|
+
}
|
|
490
|
+
if (!seen.has(String(id))) {
|
|
491
|
+
seen.add(String(id))
|
|
492
|
+
ids.push(id)
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
//分批查詢: 各w-orm-*共用之條件檢核(wsemi cvOrmFind)限$in之元素數(預設10000, 只可下調), 超過者整批reject; 每批EXIST_CHUNK個, 任一批有缺即止
|
|
496
|
+
for (let k = 0; k < ids.length; k += EXIST_CHUNK) {
|
|
497
|
+
let part = ids.slice(k, k + EXIST_CHUNK)
|
|
498
|
+
let rs = await wo.select({ id: { $in: part } })
|
|
499
|
+
let found = new Set(map(isArr(rs) ? rs : [], (r) => String(get(r, 'id'))))
|
|
500
|
+
if (part.some((id) => !found.has(String(id)))) {
|
|
501
|
+
return true
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return false
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
function getItemsDupId(rows) {
|
|
509
|
+
//mustExist之del: 同批重複之有效主鍵(第2次以後出現者)之位置[{k}]; 無效主鍵者不計(由存在檢查處置)
|
|
510
|
+
//重複者若放行, 第1筆刪除後第2筆必未命中, 事後偵測雖會reject卻已刪除, 違反[任一列不存在即零刪除], 故於刪除之前整批拒絕
|
|
511
|
+
let seen = new Set()
|
|
512
|
+
let rs = []
|
|
513
|
+
let arr = isArr(rows) ? rows : [rows]
|
|
514
|
+
for (let k = 0; k < arr.length; k++) {
|
|
515
|
+
let id = getRowId(arr[k])
|
|
516
|
+
if (!((typeof id === 'string' && id !== '') || isIdNumFinite(id))) {
|
|
517
|
+
continue
|
|
518
|
+
}
|
|
519
|
+
let s = String(id)
|
|
520
|
+
if (seen.has(s)) {
|
|
521
|
+
rs.push({ k })
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
seen.add(s)
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return rs
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
function hasNoHit(r, keyDel) {
|
|
532
|
+
//ORM之逐筆結果是否有已正常處理(ok非0)而未命中者(n為0; keyDel非null者另認該鍵為0, 如del之nDeleted): 表鎖紀律被破壞(檢查後、寫入前列被他方刪除)時之事後偵測
|
|
533
|
+
return (isArr(r) ? r : [r]).some((x) => {
|
|
534
|
+
return x !== null && typeof x === 'object' && x.ok !== 0 && (x.n === 0 || (keyDel !== null && x[keyDel] === 0))
|
|
535
|
+
})
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
function firstBadIndex(a) {
|
|
540
|
+
//陣列首個空洞或存取器索引, 回傳[索引, 'sparse'|'accessor'], 無則null; 成本為該索引之位置, 稀疏巨陣列於索引0即返回
|
|
541
|
+
//索引為存取器者, 其後各步驟(each、map以array[index]讀取)會執行使用端程式並可每次回傳不同列(外審 sol F01)
|
|
542
|
+
for (let i = 0; i < a.length; i++) {
|
|
543
|
+
let d = Object.getOwnPropertyDescriptor(a, i)
|
|
544
|
+
if (d === undefined) {
|
|
545
|
+
return [i, 'sparse']
|
|
546
|
+
}
|
|
547
|
+
if (isAcc(d)) {
|
|
548
|
+
return [i, 'accessor']
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return null
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
function getKidsFind(o) {
|
|
556
|
+
//容器o之容器子節點(依出現次數, 含重複); 呼叫時find已通過strict檢查(只含乾淨之純物件與密陣列, 無存取器), 故可直接以描述子讀值
|
|
557
|
+
let rs = []
|
|
558
|
+
let ks = Array.isArray(o) ? null : Object.keys(o)
|
|
559
|
+
let n = ks === null ? o.length : ks.length
|
|
560
|
+
for (let i = 0; i < n; i++) {
|
|
561
|
+
let v = Object.getOwnPropertyDescriptor(o, ks === null ? i : ks[i]).value
|
|
562
|
+
if (getKindFind(v) !== null) {
|
|
563
|
+
rs.push(v)
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return rs
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
function measureFind(root, maxDepth, maxNodes) {
|
|
571
|
+
//find之最長路徑深度(容器=物件與陣列)與展開後物件數(不含陣列; 共用子結構依出現次數重複計), 通過回傳null, 逾上限回傳'deep'(含循環)或'large'(R-OBJ-01 規則5續)
|
|
572
|
+
//getReasonBadKey以seen去重而只計首次到達之深度, 共用子結構(DAG)可繞過maxDepth, 且castIdFind依路徑展開, 指數型DAG展開2^k個物件
|
|
573
|
+
//作法: 顯式堆疊之記憶化後序走訪, 每個容器只展開一次(O(節點+邊)); 堆疊深度不超過maxDepth+1(逾即返回); 展開數於逾maxNodes即返回, 不產生大數
|
|
574
|
+
let st = new Map() //容器 -> -1(走訪中, 遇到即為循環) | 展開後容器數(已完成)
|
|
575
|
+
let dep = new Map() //容器 -> 以其為根之最長路徑容器數
|
|
576
|
+
let stack = [{ o: root, kids: getKidsFind(root), i: 0, d: 0, n: Array.isArray(root) ? 0 : 1 }]
|
|
577
|
+
st.set(root, -1)
|
|
578
|
+
while (stack.length > 0) {
|
|
579
|
+
let f = stack[stack.length - 1]
|
|
580
|
+
if (stack.length > maxDepth) {
|
|
581
|
+
return 'deep'
|
|
582
|
+
}
|
|
583
|
+
if (f.i < f.kids.length) {
|
|
584
|
+
let c = f.kids[f.i++]
|
|
585
|
+
let s = st.get(c)
|
|
586
|
+
if (s === undefined) {
|
|
587
|
+
st.set(c, -1)
|
|
588
|
+
stack.push({ o: c, kids: getKidsFind(c), i: 0, d: 0, n: Array.isArray(c) ? 0 : 1 })
|
|
589
|
+
}
|
|
590
|
+
else if (s === -1) {
|
|
591
|
+
return 'deep'
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
f.n += s
|
|
595
|
+
f.d = Math.max(f.d, dep.get(c))
|
|
596
|
+
if (f.n > maxNodes) {
|
|
597
|
+
return 'large'
|
|
598
|
+
}
|
|
599
|
+
if (stack.length + dep.get(c) > maxDepth) {
|
|
600
|
+
return 'deep'
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
stack.pop()
|
|
606
|
+
st.set(f.o, f.n)
|
|
607
|
+
dep.set(f.o, f.d + 1)
|
|
608
|
+
if (stack.length > 0) {
|
|
609
|
+
let p = stack[stack.length - 1]
|
|
610
|
+
p.n += f.n
|
|
611
|
+
p.d = Math.max(p.d, f.d + 1)
|
|
612
|
+
if (p.n > maxNodes) {
|
|
613
|
+
return 'large'
|
|
614
|
+
}
|
|
615
|
+
if (stack.length + dep.get(f.o) > maxDepth) {
|
|
616
|
+
return 'deep'
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return null
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
|
|
381
625
|
function getReasonBadKey(v, maxDepth = Infinity, strict = false) {
|
|
382
626
|
//v之任何深度(一般物件之值、陣列之元素)含不支援之鍵或屬性者回傳原因(R-OBJ-01 規則3、5、6), 通過回傳null:
|
|
383
627
|
// proto: 含自有__proto__鍵, 不論可否列舉; 自有鍵可由JSON.parse、Object.fromEntries、展開、structuredClone等產生, 物件字面量之__proto__為設原型而不算
|
|
384
628
|
// accessor: 含存取器屬性(getter或setter), 其值每讀一次可能不同, 令檢查之值與使用之值無法保證相同, 且讀取可能有副作用
|
|
385
629
|
// unreadable: 檢查時拋錯, 如Proxy之陷阱或已撤銷之Proxy
|
|
630
|
+
// sparse: 陣列有空洞(寫入列; 逐索引迴圈對稀疏巨陣列近乎不終止, 其後clearXSS又把空洞展成密陣列, 規則7 ⑤; find之strict路徑同樣拒收但回irregular)
|
|
386
631
|
// deep: 容器(一般物件與陣列)層數超過maxDepth(根為第1層), 走訪到第maxDepth+1層即停止; 只有find給予maxDepth, 寫入列不設上限(見R-OBJ-01 規則5)
|
|
632
|
+
// large、deep(共用子結構與循環): strict走訪全數通過後由measureFind判定最長路徑與展開後容器數(規則5續), 因seen去重只計首次到達之深度
|
|
387
633
|
//strict(只有find開啟, 規則6): castIdFind、castIdCond以mapOwn(只走自有可列舉字串鍵)與lodash map(只走索引)複製find, 複本會洗掉下列結構,
|
|
388
634
|
//令後端白名單版本會reject之條件變成較寬甚至無條件(delAll刪全表), 故走訪到之每個節點須為複本與原物件等價者, 容器之分類與複製共用getKindFind:
|
|
389
635
|
// irregular: 有Symbol.toStringTag(原型鏈上任一層之自有屬性, 含內建品牌物件與宣稱標籤者, 以hasOwnProperty判斷而不讀取)、物件節點非純物件(類別實例、偽原型等, 同頂層之isPlainObj)、
|
|
@@ -392,7 +638,7 @@ function getReasonBadKey(v, maxDepth = Infinity, strict = false) {
|
|
|
392
638
|
// hidden: 物件節點有Symbol鍵或不可列舉鍵
|
|
393
639
|
//寫入列不開strict: 後端自己亦略過Symbol鍵與不可列舉鍵, 複本不改變其語意
|
|
394
640
|
//只走一般物件與陣列(JSON可表達之資料)之可列舉資料屬性, 以屬性描述子讀值而不呼叫getter; 其餘型別(Date、函數等)不進入
|
|
395
|
-
//以堆疊迭代避免本檢查爆呼叫堆疊(其後castIdFind、castIdCond之遞迴由maxDepth保護), seen避免循環結構無窮迭代(
|
|
641
|
+
//以堆疊迭代避免本檢查爆呼叫堆疊(其後castIdFind、castIdCond之遞迴由maxDepth保護), seen避免循環結構無窮迭代(循環與共用子結構之深度與展開由其後之measureFind判定)
|
|
396
642
|
try {
|
|
397
643
|
let seen = new Set()
|
|
398
644
|
let stack = [v, 1] //成對: 節點, 其層數
|
|
@@ -449,12 +695,10 @@ function getReasonBadKey(v, maxDepth = Infinity, strict = false) {
|
|
|
449
695
|
for (let i = 0; i < n; i++) {
|
|
450
696
|
let d = Object.getOwnPropertyDescriptor(o, arr ? i : ks[i])
|
|
451
697
|
if (d === undefined) {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
455
|
-
continue //稀疏陣列之空洞
|
|
698
|
+
//空洞: find的map以array[index]讀到原型上之繼承值而成為密陣列; 寫入列逐索引迴圈對稀疏巨陣列(如new Array(2**32-1))近乎不終止, 其後的複製又把空洞展成密陣列
|
|
699
|
+
return strict ? 'irregular' : 'sparse'
|
|
456
700
|
}
|
|
457
|
-
if (d
|
|
701
|
+
if (isAcc(d)) {
|
|
458
702
|
return 'accessor'
|
|
459
703
|
}
|
|
460
704
|
if (strict && arr && !d.enumerable) {
|
|
@@ -477,6 +721,11 @@ function getReasonBadKey(v, maxDepth = Infinity, strict = false) {
|
|
|
477
721
|
//品牌物件與不可複製者之通用網, 於走訪全數通過(已無存取器)之後: 字串碼元總長在預算內且有structuredClone時以一次結構化複製證明,
|
|
478
722
|
//否則(逾預算或無structuredClone)對走訪到之物件節點逐一以精確探針判斷util.types無對應判定之WeakRef與FinalizationRegistry(規則6 ⑥)
|
|
479
723
|
if (strict && typeof v === 'object' && v !== null) {
|
|
724
|
+
//最長路徑深度與展開後容器數(共用子結構、循環), 於結構化複製之前: 走訪全數通過故已無存取器與Proxy
|
|
725
|
+
let rm = measureFind(v, maxDepth, MAX_NODES_FIND)
|
|
726
|
+
if (rm !== null) {
|
|
727
|
+
return rm
|
|
728
|
+
}
|
|
480
729
|
if (chars <= MAX_CHARS_CLONE && typeof structuredClone === 'function') {
|
|
481
730
|
if (hasIrregularClone(v)) {
|
|
482
731
|
return 'irregular'
|
|
@@ -523,6 +772,72 @@ function getErrFind(mode, find) {
|
|
|
523
772
|
}
|
|
524
773
|
|
|
525
774
|
|
|
775
|
+
function isIrregularRow(v) {
|
|
776
|
+
//寫入列是否為不乾淨之物件: Proxy、原型鏈上有Symbol.toStringTag、內建品牌物件(原型改為null之Map等無標籤可辨, 會被mapOwn洗成{}而寫入垃圾列)
|
|
777
|
+
//全程不執行使用端程式(同find之分類); 類別實例與Object.create(proto)之列仍照常(mapOwn只複製自有可列舉鍵, 與後端一致), 此為與find之strict判準的差異
|
|
778
|
+
//函數(含callable Proxy)亦判之: 宣稱Symbol.toStringTag為Object之函數可通過isobj而被當成列(外審 sol F02); 無標籤之普通函數仍由isobj判為非物件(not an object)
|
|
779
|
+
if (typeof v === 'function') {
|
|
780
|
+
return !isCleanChain(v)
|
|
781
|
+
}
|
|
782
|
+
return typeof v === 'object' && v !== null && (!isCleanChain(v) || isBrandObj(v))
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
function getItemsIrregularId(data) {
|
|
787
|
+
//mix之del列: 後端讀id之方式不一(lodash cloneDeep保留原型而可讀到繼承之id並執行getter, 不可列舉之id有的後端讀得到), 本套件之跨段同鍵比對(R-MIX-01)只認自有可列舉之資料屬性,
|
|
788
|
+
//id為存取器、繼承而來、不可列舉、或列為Proxy/帶標籤者, 後端與本套件所見之id可不同, 令同鍵防線失效(新增後即被刪除), 故整批拒絕; 回傳[{k}]
|
|
789
|
+
let rs = []
|
|
790
|
+
each(data, (v, k) => {
|
|
791
|
+
if ((typeof v !== 'object' || v === null) && typeof v !== 'function') {
|
|
792
|
+
return
|
|
793
|
+
}
|
|
794
|
+
if (!isCleanChain(v)) {
|
|
795
|
+
rs.push({ k }) //含Proxy(isCleanChain已含自身之判斷)與callable Proxy、宣稱標籤之函數(外審 sol F02)
|
|
796
|
+
return
|
|
797
|
+
}
|
|
798
|
+
if (typeof v === 'function') {
|
|
799
|
+
return //普通函數: 後端無id可讀, 同單獨del交後端逐筆處置
|
|
800
|
+
}
|
|
801
|
+
for (let x = v; x !== null; x = Object.getPrototypeOf(x)) {
|
|
802
|
+
let d = Object.getOwnPropertyDescriptor(x, 'id')
|
|
803
|
+
if (d !== undefined) {
|
|
804
|
+
if (x !== v || isAcc(d) || !d.enumerable) {
|
|
805
|
+
rs.push({ k })
|
|
806
|
+
}
|
|
807
|
+
return
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
})
|
|
811
|
+
return rs
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
function getErrBatch(msg, data) {
|
|
816
|
+
//批次容器(insert、save、del之輸入陣列, mix之add、del、diff)須為乾淨之真密陣列, 通過回傳null, 否則回傳reject之字串
|
|
817
|
+
//Proxy與原型鏈上有標籤者: 讀length與索引會執行使用端程式; 空洞: 迴圈與lodash each、map依length逐索引, 稀疏巨陣列(如new Array(2**32-1))近乎不終止並配置同數量之物件,
|
|
818
|
+
//且空洞索引在原型上有資料時, 後續各步驟(each、map以array[index])讀到未經檢查之繼承列
|
|
819
|
+
//單一物件輸入(非陣列)不在此列, 由列檢查處理
|
|
820
|
+
let arr
|
|
821
|
+
try {
|
|
822
|
+
arr = Array.isArray(data)
|
|
823
|
+
}
|
|
824
|
+
catch (e) {
|
|
825
|
+
return `${msg}, ${MSGS_BAD_KEY.irregular}` //已撤銷之Proxy: 無法分類為陣列或單一列, 於任何讀取與ORM呼叫之前固定拒收(外審 sol F06)
|
|
826
|
+
}
|
|
827
|
+
if (!arr) {
|
|
828
|
+
return null
|
|
829
|
+
}
|
|
830
|
+
if (!isCleanChain(data)) {
|
|
831
|
+
return `${msg}, ${MSGS_BAD_KEY.irregular}`
|
|
832
|
+
}
|
|
833
|
+
let bad = firstBadIndex(data)
|
|
834
|
+
if (bad !== null) {
|
|
835
|
+
return getMsgFail(`${msg}, ${MSGS_BAD_KEY[bad[1]]}`, [{ k: bad[0] }])
|
|
836
|
+
}
|
|
837
|
+
return null
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
|
|
526
841
|
function getMsgsBadKey(msg, data) {
|
|
527
842
|
//寫入之各筆任何深度含不支援之鍵或屬性者(R-OBJ-01 規則3), 依原因分組回傳訊息陣列, 各組列其於輸入內之位置data[k]; 單一物件輸入視為第0筆
|
|
528
843
|
//輸入為陣列時(批次容器)逐索引以屬性描述子取值: 索引為存取器即reject, 使其後各次讀取皆為資料屬性之讀取而與檢查之值一致
|
|
@@ -533,18 +848,18 @@ function getMsgsBadKey(msg, data) {
|
|
|
533
848
|
}
|
|
534
849
|
groups.get(reason).push({ k })
|
|
535
850
|
}
|
|
536
|
-
if (
|
|
851
|
+
if (isArr(data)) {
|
|
537
852
|
for (let k = 0; k < data.length; k++) {
|
|
538
853
|
try {
|
|
539
854
|
let d = Object.getOwnPropertyDescriptor(data, k)
|
|
540
855
|
if (d === undefined) {
|
|
541
|
-
continue //空洞,
|
|
856
|
+
continue //空洞, 已由getErrBatch拒收(此處為縱深防禦, 不可達)
|
|
542
857
|
}
|
|
543
|
-
if (d
|
|
544
|
-
addFail('accessor', k)
|
|
858
|
+
if (isAcc(d)) {
|
|
859
|
+
addFail('accessor', k) //索引存取器亦已由getErrBatch拒收(縱深防禦)
|
|
545
860
|
continue
|
|
546
861
|
}
|
|
547
|
-
let reason = getReasonBadKey(d.value)
|
|
862
|
+
let reason = isIrregularRow(d.value) ? 'irregular' : getReasonBadKey(d.value)
|
|
548
863
|
if (reason !== null) {
|
|
549
864
|
addFail(reason, k)
|
|
550
865
|
}
|
|
@@ -555,7 +870,7 @@ function getMsgsBadKey(msg, data) {
|
|
|
555
870
|
}
|
|
556
871
|
}
|
|
557
872
|
else {
|
|
558
|
-
let reason = getReasonBadKey(data)
|
|
873
|
+
let reason = isIrregularRow(data) ? 'irregular' : getReasonBadKey(data)
|
|
559
874
|
if (reason !== null) {
|
|
560
875
|
addFail(reason, 0)
|
|
561
876
|
}
|
|
@@ -564,12 +879,27 @@ function getMsgsBadKey(msg, data) {
|
|
|
564
879
|
}
|
|
565
880
|
|
|
566
881
|
|
|
882
|
+
function copyRowWithId(v, id) {
|
|
883
|
+
//列之複本: 自有可列舉屬性(含Symbol鍵)以其描述子複製而不讀值(存取器原樣保留、不執行), 僅id換為id; 原物件展開 { ...v, id } 會執行可列舉Symbol鍵與其他可列舉getter(外審 sol F03)
|
|
884
|
+
let r = {}
|
|
885
|
+
for (let k of Reflect.ownKeys(v)) {
|
|
886
|
+
let d = Object.getOwnPropertyDescriptor(v, k)
|
|
887
|
+
if (d.enumerable) {
|
|
888
|
+
Object.defineProperty(r, k, k === 'id' ? { value: id, writable: true, enumerable: true, configurable: true } : d)
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
return r
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
|
|
567
895
|
function castIdRows(rows) {
|
|
568
896
|
//寫入與刪除之數據: 各列之主鍵正規化, 寫於複本, 不改呼叫端之物件; 非物件之列原樣
|
|
569
|
-
|
|
897
|
+
//主鍵只認自有可列舉之資料屬性(getRowId, R-OBJ-01 規則7 ①): 不可列舉、繼承、經存取器者視為未提供, 不轉型亦不執行getter
|
|
898
|
+
let one = !isArr(rows)
|
|
570
899
|
let rs = map(one ? [rows] : rows, (v) => {
|
|
571
|
-
|
|
572
|
-
|
|
900
|
+
let id = getRowId(v) //只讀自有可列舉之資料屬性, 不執行getter, 與adjustData之mapOwn所見一致
|
|
901
|
+
if (isIdNumFinite(id) && isobj(v)) {
|
|
902
|
+
return copyRowWithId(v, castIdValue(id))
|
|
573
903
|
}
|
|
574
904
|
return v
|
|
575
905
|
})
|
|
@@ -615,7 +945,10 @@ function getItemsNotObj(data) {
|
|
|
615
945
|
//寫入之各筆須為一般物件(R-INPUT-01), 回傳[{k}], k為其於輸入內之位置; 單一非物件輸入視為第0筆
|
|
616
946
|
//非物件之元素經adjustData之clearXSS逐鍵複製時, 字串、陣列被展開成{0:...,1:...}、數字等變成{}, 寫入垃圾紀錄
|
|
617
947
|
let rs = []
|
|
618
|
-
each(
|
|
948
|
+
each(isArr(data) ? data : [data], (v, k) => {
|
|
949
|
+
if (isIrregularRow(v)) {
|
|
950
|
+
return //由getMsgsBadKey以irregular回報, 且不對其執行isobj(會讀Symbol.toStringTag而執行使用端程式)
|
|
951
|
+
}
|
|
619
952
|
if (!isobj(v)) {
|
|
620
953
|
rs.push({ k })
|
|
621
954
|
}
|
|
@@ -626,8 +959,9 @@ function getItemsNotObj(data) {
|
|
|
626
959
|
|
|
627
960
|
function getMixIdKey(v) {
|
|
628
961
|
//mix跨段比對用之主鍵鍵值(R-MIX-01): 以castIdValue正規化(有限數字取其字串形式, 與寫入、刪除之轉型同一函數), 正規化後為非空字串者取之, 其餘(未提供)不參與比對
|
|
962
|
+
//主鍵經getRowId只認自有可列舉之資料屬性, 即adjustData之mapOwn實際寫入者(原get(v, 'id')會讀到不可列舉、繼承、經存取器之id而誤拒, R-MIX-01 規則6)
|
|
629
963
|
//不論資料表是否適用轉型(R-ID-02)皆正規化, 故整數主鍵欄之5與'5'亦視為同鍵
|
|
630
|
-
let id = castIdValue(
|
|
964
|
+
let id = castIdValue(getRowId(v))
|
|
631
965
|
return isestr(id) ? id : null
|
|
632
966
|
}
|
|
633
967
|
|
|
@@ -673,11 +1007,11 @@ function getMsgsMixSameId(input) {
|
|
|
673
1007
|
|
|
674
1008
|
|
|
675
1009
|
function getItemsIdXSS(data) {
|
|
676
|
-
//輸入數據之主鍵(非空字串)會被clearXSS改寫者, 回傳[{k,id}], k
|
|
1010
|
+
//輸入數據之主鍵(非空字串)會被clearXSS改寫者, 回傳[{k,id}], k為其於輸入內之位置; 主鍵經getRowId只認自有可列舉之資料屬性(不執行getter)
|
|
677
1011
|
//寫入路徑之欄位皆經clearXSS而select與del不經過, 主鍵被改寫後以原主鍵查詢與刪除皆找不到, 故呼叫端須於寫入前整批拒絕
|
|
678
1012
|
let rs = []
|
|
679
|
-
each(
|
|
680
|
-
let id =
|
|
1013
|
+
each(isArr(data) ? data : [data], (v, k) => {
|
|
1014
|
+
let id = getRowId(v)
|
|
681
1015
|
if (isestr(id) && clearXSS(id) !== id) {
|
|
682
1016
|
rs.push({ k, id })
|
|
683
1017
|
}
|
|
@@ -694,7 +1028,14 @@ function getMsgIdXSS(msg, fails) {
|
|
|
694
1028
|
}
|
|
695
1029
|
|
|
696
1030
|
|
|
697
|
-
function
|
|
1031
|
+
function setOwnData(o, k, v) {
|
|
1032
|
+
//以defineProperty寫入自有可列舉之資料屬性, 不用賦值: 賦值會執行原型上之setter(類別DTO以setter攔截稽核欄位時, 其副作用可在跨段同鍵檢查之後改寫id, 外審 sol F04);
|
|
1033
|
+
//結果與無setter時之賦值相同(呼叫端物件多一個自有欄位, 既有行為見R-OBJ-01 已知限制), 凍結物件仍拋TypeError而由adjustData之呼叫端轉為failed to adjust data
|
|
1034
|
+
Object.defineProperty(o, k, { value: v, writable: true, enumerable: true, configurable: true })
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
function adjustData(ds, woName, userId, data, mode = 'save', keepUserId = false) { //mode: 'insert', 'save', 'insert+save'; keepUserId只由insert傳true(R-KEEPUSER-01)
|
|
698
1039
|
|
|
699
1040
|
//check
|
|
700
1041
|
if (!iseobj(ds)) {
|
|
@@ -718,10 +1059,13 @@ function adjustData(ds, woName, userId, data, mode = 'save') { //mode: 'insert',
|
|
|
718
1059
|
//userId, timeCreate
|
|
719
1060
|
if (mode.indexOf('insert') >= 0) {
|
|
720
1061
|
|
|
721
|
-
//userId
|
|
1062
|
+
//userId, keepUserId者(opt.keepUserId, 列上userId為擁有者而非建立者)逐列保留自有非空字串之userId, 其餘以操作者補; userIdUpdate不受影響, 恆為操作者
|
|
722
1063
|
if (keys.indexOf('userId') >= 0) {
|
|
723
1064
|
each(data, (v, k) => {
|
|
724
|
-
data[k]
|
|
1065
|
+
if (keepUserId && hasOwnerUserId(data[k])) {
|
|
1066
|
+
return
|
|
1067
|
+
}
|
|
1068
|
+
setOwnData(data[k], 'userId', userId)
|
|
725
1069
|
})
|
|
726
1070
|
}
|
|
727
1071
|
|
|
@@ -729,21 +1073,21 @@ function adjustData(ds, woName, userId, data, mode = 'save') { //mode: 'insert',
|
|
|
729
1073
|
if (keys.indexOf('timeCreate') >= 0) {
|
|
730
1074
|
|
|
731
1075
|
each(data, (v, k) => {
|
|
732
|
-
data[k]
|
|
1076
|
+
setOwnData(data[k], 'timeCreate', t)
|
|
733
1077
|
})
|
|
734
1078
|
}
|
|
735
1079
|
|
|
736
1080
|
//userIdUpdate
|
|
737
1081
|
if (keys.indexOf('userIdUpdate') >= 0) {
|
|
738
1082
|
each(data, (v, k) => {
|
|
739
|
-
data[k]
|
|
1083
|
+
setOwnData(data[k], 'userIdUpdate', userId)
|
|
740
1084
|
})
|
|
741
1085
|
}
|
|
742
1086
|
|
|
743
1087
|
//timeUpdate
|
|
744
1088
|
if (keys.indexOf('timeUpdate') >= 0) {
|
|
745
1089
|
each(data, (v, k) => {
|
|
746
|
-
data[k]
|
|
1090
|
+
setOwnData(data[k], 'timeUpdate', t)
|
|
747
1091
|
})
|
|
748
1092
|
}
|
|
749
1093
|
|
|
@@ -755,14 +1099,14 @@ function adjustData(ds, woName, userId, data, mode = 'save') { //mode: 'insert',
|
|
|
755
1099
|
//userIdUpdate
|
|
756
1100
|
if (keys.indexOf('userIdUpdate') >= 0) {
|
|
757
1101
|
each(data, (v, k) => {
|
|
758
|
-
data[k]
|
|
1102
|
+
setOwnData(data[k], 'userIdUpdate', userId)
|
|
759
1103
|
})
|
|
760
1104
|
}
|
|
761
1105
|
|
|
762
1106
|
//timeUpdate
|
|
763
1107
|
if (keys.indexOf('timeUpdate') >= 0) {
|
|
764
1108
|
each(data, (v, k) => {
|
|
765
|
-
data[k]
|
|
1109
|
+
setOwnData(data[k], 'timeUpdate', t)
|
|
766
1110
|
})
|
|
767
1111
|
}
|
|
768
1112
|
|
|
@@ -800,11 +1144,11 @@ function adjustData(ds, woName, userId, data, mode = 'save') { //mode: 'insert',
|
|
|
800
1144
|
}
|
|
801
1145
|
|
|
802
1146
|
|
|
803
|
-
async function mapOrm(ds, woItems, userId, woName, mode, input) {
|
|
1147
|
+
async function mapOrm(ds, woItems, userId, woName, mode, input, opt) {
|
|
804
1148
|
//邊界保證(R-ERR-01 規則5): execMapOrm之檢查、轉型與ORM呼叫階段, 非字串之例外(RangeError、使用端物件之Proxy陷阱/toString拋錯、woItems之非ORM值等)
|
|
805
1149
|
//一律console.log原文後以固定字串reject, 不轉出例外物件與其拋出之值; 字串(各處之固定訊息)原樣轉出
|
|
806
1150
|
try {
|
|
807
|
-
return await execMapOrm(ds, woItems, userId, woName, mode, input)
|
|
1151
|
+
return await execMapOrm(ds, woItems, userId, woName, mode, input, opt)
|
|
808
1152
|
}
|
|
809
1153
|
catch (e) {
|
|
810
1154
|
if (typeof e === 'string') {
|
|
@@ -816,7 +1160,7 @@ async function mapOrm(ds, woItems, userId, woName, mode, input) {
|
|
|
816
1160
|
}
|
|
817
1161
|
|
|
818
1162
|
|
|
819
|
-
async function execMapOrm(ds, woItems, userId, woName, mode, input) {
|
|
1163
|
+
async function execMapOrm(ds, woItems, userId, woName, mode, input, opt) {
|
|
820
1164
|
//由woName自動綁定至woItems內的ORM物件
|
|
821
1165
|
let err = null
|
|
822
1166
|
let r
|
|
@@ -837,6 +1181,12 @@ async function execMapOrm(ds, woItems, userId, woName, mode, input) {
|
|
|
837
1181
|
//idCast, 主鍵之有限數字以其字串形式處置(R-ID-02), 寫入、刪除與查詢條件共用同一判定
|
|
838
1182
|
let idCast = isIdCast(ds, woName)
|
|
839
1183
|
|
|
1184
|
+
//optProc, 第5參數opt(只改不增、刪除回報未命中, R-ONLYMOD-01), 於任何讀寫之前檢查
|
|
1185
|
+
let optProc = getOptProc(mode, opt)
|
|
1186
|
+
if (optProc.err !== null) {
|
|
1187
|
+
return Promise.reject(optProc.err)
|
|
1188
|
+
}
|
|
1189
|
+
|
|
840
1190
|
if (mode === 'select') {
|
|
841
1191
|
|
|
842
1192
|
//check, 條件若給予須為一般物件且不含自有__proto__鍵(R-INPUT-01 規則3、R-OBJ-01 規則3), 於轉型與查詢之前
|
|
@@ -867,6 +1217,13 @@ async function execMapOrm(ds, woItems, userId, woName, mode, input) {
|
|
|
867
1217
|
//msgMode
|
|
868
1218
|
let msgMode = mode === 'insert' ? 'failed to insert data' : 'failed to save data'
|
|
869
1219
|
|
|
1220
|
+
//check, 批次容器須為乾淨之真密陣列(Proxy、標籤、空洞), 於讀取任何元素之前
|
|
1221
|
+
let errBatch = getErrBatch(msgMode, input)
|
|
1222
|
+
if (errBatch !== null) {
|
|
1223
|
+
console.log(`${woName}.${mode} invalid batch container`)
|
|
1224
|
+
return Promise.reject(errBatch)
|
|
1225
|
+
}
|
|
1226
|
+
|
|
870
1227
|
//check, 各筆須為一般物件, 否則整批拒絕且不寫入任何一筆(R-INPUT-01)
|
|
871
1228
|
let failsNotObj = getItemsNotObj(input)
|
|
872
1229
|
if (size(failsNotObj) > 0) {
|
|
@@ -895,7 +1252,7 @@ async function execMapOrm(ds, woItems, userId, woName, mode, input) {
|
|
|
895
1252
|
|
|
896
1253
|
//adjustData
|
|
897
1254
|
try {
|
|
898
|
-
input = adjustData(ds, woName, userId, input, mode)
|
|
1255
|
+
input = adjustData(ds, woName, userId, input, mode, optProc.keepUserId)
|
|
899
1256
|
}
|
|
900
1257
|
catch (e) {
|
|
901
1258
|
console.log(`${woName}.adjustData catch`, e.toString())
|
|
@@ -903,6 +1260,23 @@ async function execMapOrm(ds, woItems, userId, woName, mode, input) {
|
|
|
903
1260
|
return Promise.reject(err)
|
|
904
1261
|
}
|
|
905
1262
|
|
|
1263
|
+
//exist, 只改不增(opt.mustExist, R-ONLYMOD-01): 寫入之前檢查全部列(含補主鍵後)皆已存在, 任一列不存在(含無主鍵之列)即整批reject 'rowNotFound'而不寫入任何一筆
|
|
1264
|
+
//檢查與寫入非原子, 呼叫端須已持有該表之表鎖(withTabLocks、guardWrite)
|
|
1265
|
+
if (mode === 'save' && optProc.mustExist) {
|
|
1266
|
+
let missing = await hasMissingRow(wo, input)
|
|
1267
|
+
.catch((msg) => {
|
|
1268
|
+
console.log(`${woName}.save exist check catch`, msg)
|
|
1269
|
+
err = 'failed to save data'
|
|
1270
|
+
})
|
|
1271
|
+
if (isestr(err)) {
|
|
1272
|
+
return Promise.reject(err)
|
|
1273
|
+
}
|
|
1274
|
+
if (missing) {
|
|
1275
|
+
console.log(`${woName}.save row not found`)
|
|
1276
|
+
return Promise.reject('rowNotFound')
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
906
1280
|
//save
|
|
907
1281
|
if (mode === 'insert') {
|
|
908
1282
|
r = await wo.insert(input)
|
|
@@ -912,7 +1286,8 @@ async function execMapOrm(ds, woItems, userId, woName, mode, input) {
|
|
|
912
1286
|
})
|
|
913
1287
|
}
|
|
914
1288
|
else if (mode === 'save') {
|
|
915
|
-
|
|
1289
|
+
//只改不增者轉傳autoInsert:false(各ORM於未命中時不寫入); 其餘維持單參數呼叫
|
|
1290
|
+
r = await (optProc.mustExist ? wo.save(input, { autoInsert: false }) : wo.save(input))
|
|
916
1291
|
.catch((msg) => {
|
|
917
1292
|
console.log(`${woName}.save catch`, msg)
|
|
918
1293
|
err = 'failed to save data'
|
|
@@ -932,6 +1307,12 @@ async function execMapOrm(ds, woItems, userId, woName, mode, input) {
|
|
|
932
1307
|
console.log(`${woName}.save fail`, fails)
|
|
933
1308
|
return Promise.reject(getMsgFail('failed to save data', getItemsFail(r, inputOrig)))
|
|
934
1309
|
}
|
|
1310
|
+
|
|
1311
|
+
//check, 只改不增者之事後偵測: 逐筆n為0表檢查後、寫入前該列被他方刪除(表鎖紀律被破壞), 已寫入之筆無從回復, 仍回報rowNotFound
|
|
1312
|
+
if (optProc.mustExist && hasNoHit(r, null)) {
|
|
1313
|
+
console.log(`${woName}.save row not found after check`)
|
|
1314
|
+
return Promise.reject('rowNotFound')
|
|
1315
|
+
}
|
|
935
1316
|
}
|
|
936
1317
|
|
|
937
1318
|
//僅回傳id避免大量數據時造成負擔
|
|
@@ -948,11 +1329,43 @@ async function execMapOrm(ds, woItems, userId, woName, mode, input) {
|
|
|
948
1329
|
}
|
|
949
1330
|
else if (mode === 'del') {
|
|
950
1331
|
|
|
1332
|
+
//check, 批次容器須為乾淨之真密陣列; 刪除之各筆仍不檢查(交後端逐筆處置)
|
|
1333
|
+
let errBatch = getErrBatch('failed to delete data', input)
|
|
1334
|
+
if (errBatch !== null) {
|
|
1335
|
+
console.log(`${woName}.del invalid batch container`)
|
|
1336
|
+
return Promise.reject(errBatch)
|
|
1337
|
+
}
|
|
1338
|
+
|
|
951
1339
|
//castIdRows, 主鍵之有限數字轉為字串, 寫於複本(R-ID-02); 刪除不經clearXSS(R-XSS-01)
|
|
952
1340
|
if (idCast) {
|
|
953
1341
|
input = castIdRows(input)
|
|
954
1342
|
}
|
|
955
1343
|
|
|
1344
|
+
//exist, 刪除回報未命中(opt.mustExist, R-ONLYMOD-01): 刪除之前檢查全部列皆存在, 任一列不存在(含無主鍵之列)即整批reject 'rowNotFound'而不刪除任何一筆
|
|
1345
|
+
//檢查與刪除非原子, 呼叫端須已持有該表之表鎖(withTabLocks、guardWrite)
|
|
1346
|
+
if (optProc.mustExist) {
|
|
1347
|
+
|
|
1348
|
+
//check, 同批重複主鍵整批拒絕, 訊息列其位置而不回顯主鍵(同R-MIX-01)
|
|
1349
|
+
let failsDup = getItemsDupId(input)
|
|
1350
|
+
if (size(failsDup) > 0) {
|
|
1351
|
+
console.log(`${woName}.del duplicate id`, failsDup)
|
|
1352
|
+
return Promise.reject(getMsgFail('invalid input of del, duplicate id', failsDup))
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
let missing = await hasMissingRow(wo, input)
|
|
1356
|
+
.catch((msg) => {
|
|
1357
|
+
console.log(`${woName}.del exist check catch`, msg)
|
|
1358
|
+
err = 'failed to delete data'
|
|
1359
|
+
})
|
|
1360
|
+
if (isestr(err)) {
|
|
1361
|
+
return Promise.reject(err)
|
|
1362
|
+
}
|
|
1363
|
+
if (missing) {
|
|
1364
|
+
console.log(`${woName}.del row not found`)
|
|
1365
|
+
return Promise.reject('rowNotFound')
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
956
1369
|
//del
|
|
957
1370
|
r = await wo.del(input)
|
|
958
1371
|
.catch((msg) => {
|
|
@@ -969,6 +1382,12 @@ async function execMapOrm(ds, woItems, userId, woName, mode, input) {
|
|
|
969
1382
|
}
|
|
970
1383
|
}
|
|
971
1384
|
|
|
1385
|
+
//check, 刪除回報未命中者之事後偵測: 逐筆nDeleted或n為0表檢查後、刪除前該列被他方刪除(表鎖紀律被破壞), 已刪除之筆無從回復, 仍回報rowNotFound
|
|
1386
|
+
if (!isestr(err) && optProc.mustExist && hasNoHit(r, 'nDeleted')) {
|
|
1387
|
+
console.log(`${woName}.del row not found after check`)
|
|
1388
|
+
err = 'rowNotFound'
|
|
1389
|
+
}
|
|
1390
|
+
|
|
972
1391
|
}
|
|
973
1392
|
else if (mode === 'delAll') {
|
|
974
1393
|
|
|
@@ -989,27 +1408,42 @@ async function execMapOrm(ds, woItems, userId, woName, mode, input) {
|
|
|
989
1408
|
}
|
|
990
1409
|
else if (mode === 'mix') {
|
|
991
1410
|
|
|
992
|
-
//check
|
|
993
|
-
if (!iseobj(input)) {
|
|
1411
|
+
//check, input須為乾淨之一般物件: Proxy(含已撤銷者)、原型鏈有標籤者於讀取任何屬性之前拒收(iseobj讀標籤、input.add讀屬性皆會執行使用端程式, 外審 minimax F3)
|
|
1412
|
+
if (typeof input !== 'object' || input === null || !isCleanChain(input) || !iseobj(input)) {
|
|
994
1413
|
return Promise.reject('invalid input of mix, need the result object of ltdtDiffByKey')
|
|
995
1414
|
}
|
|
996
1415
|
|
|
997
|
-
//check, add、del、diff為存取器者整批拒絕(R-OBJ-01 規則3 批次容器),
|
|
1416
|
+
//check, add、del、diff為存取器者整批拒絕(R-OBJ-01 規則3 批次容器), 並取自有資料屬性之快照(各讀一次, 其後只用快照; 繼承而來者不取), 使各檢查與寫入所見一致
|
|
1417
|
+
let snap = {}
|
|
998
1418
|
for (let key of ['add', 'del', 'diff']) {
|
|
999
1419
|
let d = Object.getOwnPropertyDescriptor(input, key)
|
|
1000
|
-
if (d !== undefined && (d
|
|
1420
|
+
if (d !== undefined && isAcc(d)) {
|
|
1001
1421
|
return Promise.reject(`invalid input of mix, ${MSGS_BAD_KEY.accessor}`)
|
|
1002
1422
|
}
|
|
1423
|
+
snap[key] = d === undefined ? undefined : d.value
|
|
1003
1424
|
}
|
|
1004
|
-
input =
|
|
1425
|
+
input = snap
|
|
1005
1426
|
|
|
1006
1427
|
//check, add、del、diff若給予須為陣列(R-INPUT-01), 否則各段之回傳將逐欄位產生
|
|
1007
1428
|
for (let key of ['add', 'del', 'diff']) {
|
|
1008
|
-
if (input[key] !== undefined && !
|
|
1429
|
+
if (input[key] !== undefined && !isArr(input[key])) {
|
|
1009
1430
|
return Promise.reject('invalid input of mix, need the result object of ltdtDiffByKey')
|
|
1010
1431
|
}
|
|
1011
1432
|
}
|
|
1012
1433
|
|
|
1434
|
+
//check, add、del、diff須為乾淨之真密陣列(Proxy、標籤、空洞), 於讀取任何元素之前
|
|
1435
|
+
let msgsBatch = []
|
|
1436
|
+
for (let [key, msg] of [['add', 'failed to insert data'], ['del', 'failed to delete data'], ['diff', 'failed to update data']]) {
|
|
1437
|
+
let errBatch = getErrBatch(msg, input[key])
|
|
1438
|
+
if (errBatch !== null) {
|
|
1439
|
+
msgsBatch.push(errBatch)
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
if (size(msgsBatch) > 0) {
|
|
1443
|
+
console.log(`${woName}.mix invalid batch container`)
|
|
1444
|
+
return Promise.reject(join(msgsBatch, '; '))
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1013
1447
|
//check, 新增與修改之各筆須為一般物件, 於任何步驟寫入之前整批拒絕(R-INPUT-01)
|
|
1014
1448
|
let msgsNotObj = []
|
|
1015
1449
|
for (let [key, msg] of [['add', 'failed to insert data'], ['diff', 'failed to update data']]) {
|
|
@@ -1033,6 +1467,13 @@ async function execMapOrm(ds, woItems, userId, woName, mode, input) {
|
|
|
1033
1467
|
return Promise.reject(join(msgsBadKey, '; '))
|
|
1034
1468
|
}
|
|
1035
1469
|
|
|
1470
|
+
//check, del列之id不得為存取器或繼承而來(同鍵防線之前提), 於任何步驟寫入之前整批拒絕
|
|
1471
|
+
let failsIrregularId = getItemsIrregularId(get(input, 'del', []))
|
|
1472
|
+
if (size(failsIrregularId) > 0) {
|
|
1473
|
+
console.log(`${woName}.mix del id is not an own data property`, failsIrregularId)
|
|
1474
|
+
return Promise.reject(getMsgFail(`failed to delete data, ${MSGS_BAD_KEY.irregular}`, failsIrregularId))
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1036
1477
|
//castIdRows, 各段主鍵之有限數字轉為字串, 寫於複本(R-ID-02)
|
|
1037
1478
|
if (idCast) {
|
|
1038
1479
|
input = { ...input }
|
|
@@ -1070,6 +1511,20 @@ async function execMapOrm(ds, woItems, userId, woName, mode, input) {
|
|
|
1070
1511
|
//msgsFail, 逐筆失敗僅影響該筆, 不中斷後續步驟, 待全部步驟完成後一併reject; 整批性錯誤則仍立即reject
|
|
1071
1512
|
let msgsFail = []
|
|
1072
1513
|
|
|
1514
|
+
//diff之adjustData提前於任何ORM呼叫之前(取得清除後之深複本): 後端於del步驟會讀取del列(可能執行其上之使用端程式), 其副作用不得改動尚未寫入之diff(外審 sol F03)
|
|
1515
|
+
//add之adjustData原即於第一個ORM呼叫之前; 整批性之adjust失敗亦因而不致發生於add、del已寫入之後
|
|
1516
|
+
let diffAdj = null
|
|
1517
|
+
if (size(get(input, 'diff', [])) > 0) {
|
|
1518
|
+
try {
|
|
1519
|
+
diffAdj = adjustData(ds, woName, userId, get(input, 'diff', []))
|
|
1520
|
+
}
|
|
1521
|
+
catch (e) {
|
|
1522
|
+
console.log(`${woName}.adjustData catch`, e.toString())
|
|
1523
|
+
err = 'failed to adjust data'
|
|
1524
|
+
return Promise.reject(err)
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1073
1528
|
let add = get(input, 'add', [])
|
|
1074
1529
|
if (size(add) > 0) {
|
|
1075
1530
|
|
|
@@ -1137,15 +1592,8 @@ async function execMapOrm(ds, woItems, userId, woName, mode, input) {
|
|
|
1137
1592
|
//diffOrig, 呼叫端原輸入, 逐筆失敗之訊息以呼叫端視角列出
|
|
1138
1593
|
let diffOrig = diff
|
|
1139
1594
|
|
|
1140
|
-
//adjustData
|
|
1141
|
-
|
|
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
|
-
}
|
|
1595
|
+
//adjustData已於上方提前完成
|
|
1596
|
+
diff = diffAdj
|
|
1149
1597
|
|
|
1150
1598
|
//save
|
|
1151
1599
|
let rs = await wo.save(diff)
|
|
@@ -1194,11 +1642,12 @@ async function execMapOrm(ds, woItems, userId, woName, mode, input) {
|
|
|
1194
1642
|
|
|
1195
1643
|
|
|
1196
1644
|
function getMapOrm(ds, woItems) {
|
|
1197
|
-
//每次呼叫各自綁定ds與woItems並回傳新函數, 令同一行程內之多個實例互不干擾;
|
|
1198
|
-
return async (userId, woName, mode, input) => {
|
|
1199
|
-
return mapOrm(ds, woItems, userId, woName, mode, input)
|
|
1645
|
+
//每次呼叫各自綁定ds與woItems並回傳新函數, 令同一行程內之多個實例互不干擾; 回傳函數之參數形狀為(userId, woName, mode, input, opt), 前四個維持, opt為選用(R-ONLYMOD-01)
|
|
1646
|
+
return async (userId, woName, mode, input, opt) => {
|
|
1647
|
+
return mapOrm(ds, woItems, userId, woName, mode, input, opt)
|
|
1200
1648
|
}
|
|
1201
1649
|
}
|
|
1202
1650
|
|
|
1203
1651
|
|
|
1652
|
+
export { getOptProc }
|
|
1204
1653
|
export default getMapOrm
|