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/README.md +10 -0
- package/dist/w-serv-orm.umd.js +2 -2
- package/dist/w-serv-orm.umd.js.map +1 -1
- package/docs/WServOrm.html +5 -4
- package/docs/WServOrm.mjs.html +3 -2
- package/docs/index.html +1 -1
- package/package.json +1 -1
- package/src/WServOrm.mjs +2 -1
- package/src/getMapOrm.mjs +315 -44
- package/test/api-objcopy.test.mjs +277 -0
- package/test/unit-objcopy.test.mjs +1522 -26
|
@@ -611,4 +611,281 @@ describe('R-OBJ-01 接w-orm-lowdb(以JS參考實作為預言機)', function() {
|
|
|
611
611
|
}
|
|
612
612
|
})
|
|
613
613
|
|
|
614
|
+
//以下為第82項(Argon 外審第三輪 B-02、A-07 與核實時另發現之 F5、F6)之真ORM案: 預言機為「直呼真ORM」之表內容(不取自被測函式回傳)
|
|
615
|
+
let hideProp = (o, k, v) => Object.defineProperty(o, k, { value: v, enumerable: false, configurable: true, writable: true })
|
|
616
|
+
let GEN = '<GEN>'
|
|
617
|
+
let snap = async () => (await rows()).map((v) => (typeof v.id === 'string' && v.id.length > 30 ? { ...v, id: GEN } : v)).sort((a, b) => (JSON.stringify(a) < JSON.stringify(b) ? -1 : 1))
|
|
618
|
+
let evilRow = () => JSON.parse('{"id":"zz","name":"e","meta":{"__proto__":{"polluted":1}}}')
|
|
619
|
+
let hasProtoKey = (v) => {
|
|
620
|
+
let found = false
|
|
621
|
+
let walk = (o) => {
|
|
622
|
+
if (o === null || typeof o !== 'object') {
|
|
623
|
+
return
|
|
624
|
+
}
|
|
625
|
+
if (Object.prototype.hasOwnProperty.call(o, '__proto__')) {
|
|
626
|
+
found = true
|
|
627
|
+
}
|
|
628
|
+
for (let k of Object.keys(o)) {
|
|
629
|
+
walk(o[k])
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
walk(v)
|
|
633
|
+
return found
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
it('A27 寫入列之id只認自有可列舉資料屬性: 不可列舉/繼承之id經procOrm之結果同直呼真ORM; getter不被執行, 其加入之含__proto__欄位不被寫入; null原型品牌列被拒收(B-02 ①②③④)', async function() {
|
|
637
|
+
//R-OBJ-01 規則7 ①②; 修前: 不可列舉/繼承之數字id被「復活」為自有字串id寫入(直呼則忽略並產生新主鍵); getter於檢查後被執行並可使meta含自有__proto__
|
|
638
|
+
let cases = [
|
|
639
|
+
['自有不可列舉數字id 123', () => [hideProp({ name: 'n3' }, 'id', 123)]],
|
|
640
|
+
['自有不可列舉字串id', () => [hideProp({ name: 'n3' }, 'id', 'a9')]],
|
|
641
|
+
['繼承數字id', () => [Object.assign(Object.create({ id: 123 }), { name: 'n3' })]],
|
|
642
|
+
['單一物件(非陣列)之不可列舉數字id', () => hideProp({ name: 'n3' }, 'id', 456)],
|
|
643
|
+
['自有不可列舉getter(直呼不執行)', () => {
|
|
644
|
+
let r = { name: 'n3' }
|
|
645
|
+
Object.defineProperty(r, 'id', {
|
|
646
|
+
enumerable: false,
|
|
647
|
+
get() {
|
|
648
|
+
return 'a9'
|
|
649
|
+
}
|
|
650
|
+
})
|
|
651
|
+
return [r]
|
|
652
|
+
}],
|
|
653
|
+
]
|
|
654
|
+
for (let mode of ['insert', 'save']) {
|
|
655
|
+
for (let [lab, mk] of cases) {
|
|
656
|
+
await reseed()
|
|
657
|
+
await so.woItems.tabA[mode](mk())
|
|
658
|
+
let direct = await snap()
|
|
659
|
+
await reseed()
|
|
660
|
+
await quiet(() => proc(mode, mk()))
|
|
661
|
+
same(await snap(), direct, `${mode} ${lab}: 表內容同直呼`)
|
|
662
|
+
assert.ok((await rows()).every((v) => v.id !== '123' && v.id !== '456'), `${mode} ${lab}: 數字id不被復活為字串id`)
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
//繼承之字串id: 真ORM(lodash cloneDeep保留原型)於邏輯上讀到原型上之id, 但序列化只含自有鍵, 結果與本層不同(直呼寫入無id之列或視為已存在), 故不以直呼為預言機;
|
|
666
|
+
//本層規則7 ①: id只認自有可列舉資料屬性, 繼承者視為未提供 → 寫入列補主鍵, 寫入內容為自有可列舉鍵
|
|
667
|
+
for (let mode of ['insert', 'save']) {
|
|
668
|
+
await reseed()
|
|
669
|
+
await quiet(() => proc(mode, [Object.assign(Object.create({ id: 'a9' }), { name: 'n3' })]))
|
|
670
|
+
let got = (await rows()).filter((v) => v.name === 'n3')
|
|
671
|
+
assert.strictEqual(got.length, 1, `${mode} 繼承字串id: 寫入一筆`)
|
|
672
|
+
assert.ok(typeof got[0].id === 'string' && got[0].id.length > 30, `${mode} 繼承字串id: 補主鍵, 實得 ${got[0].id}`)
|
|
673
|
+
}
|
|
674
|
+
//getter: 檢查通過後才被執行並向列加入含自有__proto__之meta(修前寫入); 現getter不被執行, 無自有__proto__鍵被寫入
|
|
675
|
+
for (let [lab, mkWithCounter] of [
|
|
676
|
+
['自有不可列舉getter', (c) => {
|
|
677
|
+
let r = { name: 'n3' }
|
|
678
|
+
Object.defineProperty(r, 'id', {
|
|
679
|
+
enumerable: false,
|
|
680
|
+
get() {
|
|
681
|
+
c.n++; r.meta = JSON.parse('{"__proto__":{"polluted":1}}'); return 'a3'
|
|
682
|
+
}
|
|
683
|
+
})
|
|
684
|
+
return r
|
|
685
|
+
}],
|
|
686
|
+
['繼承getter(類別)', (c) => {
|
|
687
|
+
class R {
|
|
688
|
+
constructor() {
|
|
689
|
+
this.name = 'n3'
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
get id() {
|
|
693
|
+
c.n++; this.meta = JSON.parse('{"__proto__":{"polluted":1}}'); return 'a3'
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return new R()
|
|
697
|
+
}],
|
|
698
|
+
]) {
|
|
699
|
+
for (let [mode, mk] of [
|
|
700
|
+
['insert', (r) => [r]],
|
|
701
|
+
['save', (r) => [r]],
|
|
702
|
+
['mix', (r) => ({ add: [r] })],
|
|
703
|
+
['mix', (r) => ({ diff: [r] })],
|
|
704
|
+
]) {
|
|
705
|
+
await reseed()
|
|
706
|
+
let c = { n: 0 }
|
|
707
|
+
await quiet(() => proc(mode, mk(mkWithCounter(c))))
|
|
708
|
+
assert.strictEqual(c.n, 0, `${mode} ${lab}: getter不被執行`)
|
|
709
|
+
assert.ok(!hasProtoKey(await rows()), `${mode} ${lab}: 表內無自有__proto__鍵`)
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
//null原型之品牌物件(Map)於多筆批次: 整批reject, 表不變(直呼真ORM則寫入垃圾列, 本層以fail-closed拒收, 非模仿直呼)
|
|
713
|
+
for (let mode of ['insert', 'save']) {
|
|
714
|
+
await reseed()
|
|
715
|
+
let before = await rows()
|
|
716
|
+
let err = await getReject(proc(mode, [{ id: 'ok1', name: 'ok' }, Object.setPrototypeOf(new Map([['id', 'a1']]), null)]))
|
|
717
|
+
assert.strictEqual(err, `failed to ${mode} data, ${msgIrreg}: data[1]`, mode)
|
|
718
|
+
same(await rows(), before, `${mode} 表不變`)
|
|
719
|
+
}
|
|
720
|
+
})
|
|
721
|
+
|
|
722
|
+
it('A27b mix同鍵: 不可列舉/繼承之id不誤拒, 結果同直呼真ORM依序insert、del; 自有同鍵仍reject; mix.del列之id為getter整批reject, 表不變(B-02 ④、F6)', async function() {
|
|
723
|
+
//R-MIX-01 規則1、6; 修前: 不可列舉之 add id 'a1' 與 del 'a1' 被誤拒; del 列 id 為 getter 時 add 之 a7 隨即被後端刪除而resolve
|
|
724
|
+
//自有不可列舉: 直呼真ORM之lodash cloneDeep不複製不可列舉鍵, 新增列得到產生之主鍵, 同本層(以直呼為預言機)
|
|
725
|
+
await reseed()
|
|
726
|
+
await so.woItems.tabA.insert([hideProp({ name: 'new' }, 'id', 'a1')])
|
|
727
|
+
await so.woItems.tabA.del([{ id: 'a1' }])
|
|
728
|
+
let direct = await snap()
|
|
729
|
+
await reseed()
|
|
730
|
+
await quiet(() => proc('mix', { add: [hideProp({ name: 'new' }, 'id', 'a1')], del: [{ id: 'a1' }] }))
|
|
731
|
+
same(await snap(), direct, '不可列舉: 表內容同直呼(新增列補主鍵、a1被刪)')
|
|
732
|
+
assert.ok((await rows()).every((v) => v.id !== 'a1'), '不可列舉: a1已刪')
|
|
733
|
+
//繼承: 真ORM於邏輯上讀到原型上之id(直呼時視為已存在而略過),與本層「只認自有可列舉」不同, 不以直呼為預言機; 本層視為未提供, 新增列補主鍵, a1被刪
|
|
734
|
+
await reseed()
|
|
735
|
+
await quiet(() => proc('mix', { add: [Object.assign(Object.create({ id: 'a1' }), { name: 'new' })], del: [{ id: 'a1' }] }))
|
|
736
|
+
let after = await rows()
|
|
737
|
+
assert.ok(after.every((v) => v.id !== 'a1'), '繼承: a1已刪')
|
|
738
|
+
assert.strictEqual(after.filter((v) => v.name === 'new').length, 1, '繼承: 新增列已寫入')
|
|
739
|
+
assert.ok(after.find((v) => v.name === 'new').id.length > 30, '繼承: 新增列補主鍵')
|
|
740
|
+
await reseed()
|
|
741
|
+
let before = await rows()
|
|
742
|
+
assert.strictEqual(await getReject(proc('mix', { add: [{ id: 'a1', name: 'new' }], del: [{ id: 'a1' }] })), 'invalid input of mix, same id in add and del: add[0], del[0]')
|
|
743
|
+
same(await rows(), before, '自有可列舉同鍵: 表不變')
|
|
744
|
+
//mix.del 列 id 為 getter: 前3次讀回 'x', 其後讀回 'a7'(add 同批新增 a7)——修前 add 之 a7 被刪
|
|
745
|
+
let n = 0
|
|
746
|
+
let delRow = {
|
|
747
|
+
get id() {
|
|
748
|
+
n++; return n <= 3 ? 'x' : 'a7'
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
let err = await getReject(proc('mix', { add: [{ id: 'a7', name: 'added' }], del: [delRow] }))
|
|
752
|
+
assert.strictEqual(err, 'failed to delete data, unsupported irregular object or array: data[0]')
|
|
753
|
+
assert.strictEqual(n, 0, 'getter不被執行')
|
|
754
|
+
same(await rows(), before, 'mix.del getter: 表不變(修前 a7 新增後被刪, resolve)')
|
|
755
|
+
})
|
|
756
|
+
|
|
757
|
+
it('A28 批次容器(偽造標籤之類陣列、空洞+原型帶索引)與共用子結構、稀疏巨陣列: 固定字串reject, 表不變, delAll不刪(F5、A-07)', async function() {
|
|
758
|
+
//R-OBJ-01 規則5續、規則7 ③⑤; 建議檔驗證 #4(delAll 不得因此刪全表)
|
|
759
|
+
await reseed()
|
|
760
|
+
let before = await rows()
|
|
761
|
+
//F5: 繞過規則3之容器形狀(修前含巢狀自有__proto__之列被寫入)
|
|
762
|
+
let fakeTag = () => ({ [Symbol.toStringTag]: 'Array', length: 1, 0: evilRow() })
|
|
763
|
+
let holeInherited = () => Object.setPrototypeOf(new Array(1), { 0: evilRow() })
|
|
764
|
+
for (let [lab, mk, want] of [
|
|
765
|
+
['偽造標籤之類陣列', fakeTag, (m) => `failed to ${m} data, ${msgIrreg}: data[0]`],
|
|
766
|
+
['空洞且原型帶索引', holeInherited, (m) => `failed to ${m} data, unsupported sparse array: data[0]`],
|
|
767
|
+
]) {
|
|
768
|
+
for (let mode of ['insert', 'save']) {
|
|
769
|
+
assert.strictEqual(await getReject(proc(mode, mk())), want(mode), `${lab} ${mode}`)
|
|
770
|
+
same(await rows(), before, `${lab} ${mode} 表不變`)
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
assert.strictEqual(await getReject(proc('mix', { add: holeInherited() })), 'failed to insert data, unsupported sparse array: data[0]')
|
|
774
|
+
assert.strictEqual(await getReject(proc('mix', { add: fakeTag() })), 'invalid input of mix, need the result object of ltdtDiffByKey')
|
|
775
|
+
same(await rows(), before, 'mix 表不變')
|
|
776
|
+
|
|
777
|
+
//稀疏巨陣列: 列內巢狀、批次容器、find之$in(先有限大小)
|
|
778
|
+
for (let len of [1e5, 2 ** 32 - 1]) {
|
|
779
|
+
for (let [lab, mode, input, want] of [
|
|
780
|
+
['列內巢狀', 'insert', [{ id: 'a9', arr: new Array(len) }], 'failed to insert data, unsupported sparse array: data[0]'],
|
|
781
|
+
['容器 save', 'save', new Array(len), 'failed to save data, unsupported sparse array: data[0]'],
|
|
782
|
+
['容器 del', 'del', new Array(len), 'failed to delete data, unsupported sparse array: data[0]'],
|
|
783
|
+
['find $in select', 'select', { id: { $in: new Array(len) } }, `invalid find of select, ${msgIrreg}`],
|
|
784
|
+
['find $in delAll', 'delAll', { id: { $in: new Array(len) } }, `invalid find of delAll, ${msgIrreg}`],
|
|
785
|
+
]) {
|
|
786
|
+
let t = Date.now()
|
|
787
|
+
assert.strictEqual(await getReject(proc(mode, input)), want, `${lab} len=${len}`)
|
|
788
|
+
assert.ok(Date.now() - t < 1000, `${lab} len=${len} 耗時 ${Date.now() - t} ms`)
|
|
789
|
+
same(await rows(), before, `${lab} len=${len} 表不變`)
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
//共用子結構(字串型主鍵表, 經castIdFind): 最長路徑逾64、展開逾100,000 → 固定字串reject, 不展開、不呼叫真ORM; delAll後表不變
|
|
794
|
+
let leaf = () => ({ id: 'a1' })
|
|
795
|
+
let chainDag = (n) => {
|
|
796
|
+
let ns = new Array(n + 1)
|
|
797
|
+
ns[n] = leaf()
|
|
798
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
799
|
+
ns[i] = { $and: [ns[i + 1]] }
|
|
800
|
+
}
|
|
801
|
+
return { $or: ns.slice(0, n) }
|
|
802
|
+
}
|
|
803
|
+
let expDag = (k) => {
|
|
804
|
+
let x = leaf()
|
|
805
|
+
for (let i = 0; i < k; i++) {
|
|
806
|
+
x = { $and: [x, x] }
|
|
807
|
+
}
|
|
808
|
+
return x
|
|
809
|
+
}
|
|
810
|
+
for (let [lab, mk, want] of [
|
|
811
|
+
['鏈狀DAG 最長路徑129個容器', () => chainDag(63), 'too deep'],
|
|
812
|
+
['鏈狀DAG 最長路徑403個容器', () => chainDag(200), 'too deep'],
|
|
813
|
+
['指數DAG k=16 展開196,606個容器', () => expDag(16), 'too large'],
|
|
814
|
+
['指數DAG k=20 展開4,194,302個容器', () => expDag(20), 'too large'],
|
|
815
|
+
['循環', () => {
|
|
816
|
+
let c = { $and: [] }
|
|
817
|
+
c.$and.push(c)
|
|
818
|
+
return c
|
|
819
|
+
}, 'too deep'],
|
|
820
|
+
]) {
|
|
821
|
+
for (let mode of ['select', 'delAll']) {
|
|
822
|
+
let t = Date.now()
|
|
823
|
+
assert.strictEqual(await getReject(proc(mode, mk())), `invalid find of ${mode}, ${want}`, `${lab} ${mode}`)
|
|
824
|
+
assert.ok(Date.now() - t < 1000, `${lab} ${mode} 耗時 ${Date.now() - t} ms`)
|
|
825
|
+
same(await rows(), before, `${lab} ${mode} 表不變(delAll不刪)`)
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
//合理之重用(同一子條件於多處)照常: 經procOrm之delAll刪除之列同直呼真ORM
|
|
830
|
+
let shared = { id: 'a1' }
|
|
831
|
+
await reseed()
|
|
832
|
+
await so.woItems.tabA.delAll({ $or: [{ id: 'a1' }, { id: 'a1' }] })
|
|
833
|
+
let direct = await rows()
|
|
834
|
+
await reseed()
|
|
835
|
+
await proc('delAll', { $or: [shared, shared] })
|
|
836
|
+
same(await rows(), direct, '重用同一子條件之delAll同直呼')
|
|
837
|
+
assert.strictEqual(direct.length, 4, '只刪a1')
|
|
838
|
+
})
|
|
839
|
+
|
|
840
|
+
it('A29 通過檢查之後不執行使用端程式(外審 sol 第一輪 F01、F03、F04): 批次容器索引為getter(每次回傳不同列)、del列之其他getter改動add/diff、DTO原型上之稽核欄位setter改寫id——真lowdb之結果須為預期, 表不變或僅依預期變動', async function() {
|
|
841
|
+
//R-OBJ-01 規則7、R-MIX-01 規則6; 修前: ①mix.del 容器索引getter第1次讀回{id:other}通過mix.del檢查、其後讀回繼承id=a7之列而被後端刪除剛新增之a7(表內無a7);
|
|
842
|
+
//②稽核欄位setter於同鍵檢查之後把新增列之id由new改為keep, insert因已存在而略過、隨後del刪除既有keep(表內兩者皆無)
|
|
843
|
+
await reseed()
|
|
844
|
+
let before = await rows()
|
|
845
|
+
//①
|
|
846
|
+
let n = 0
|
|
847
|
+
let dels = Object.defineProperty([], 0, {
|
|
848
|
+
enumerable: true,
|
|
849
|
+
get() {
|
|
850
|
+
n++
|
|
851
|
+
return n === 1 ? { id: 'other' } : Object.create({ id: 'a7' })
|
|
852
|
+
},
|
|
853
|
+
})
|
|
854
|
+
let err = await getReject(proc('mix', { add: [{ id: 'a7', name: 'new' }], del: dels }))
|
|
855
|
+
assert.strictEqual(err, 'failed to delete data, unsupported accessor property: data[0]')
|
|
856
|
+
assert.strictEqual(n, 0, 'index getter不被執行')
|
|
857
|
+
same(await rows(), before, '①表不變(修前 a7 新增後被刪)')
|
|
858
|
+
//②: 另建含稽核欄位之表(獨立資料檔)
|
|
859
|
+
let fd2 = './test/_tmp/api-objcopy-audit'
|
|
860
|
+
fs.rmSync(fd2, { recursive: true, force: true })
|
|
861
|
+
fs.mkdirSync(fd2, { recursive: true })
|
|
862
|
+
let ds2 = { tabA: { keys: ['id', 'name', 'meta', 'userId', 'timeCreate', 'userIdUpdate', 'timeUpdate'], settings: { id: { pk: true, type: 'STRING' }, name: { type: 'STRING' } } } }
|
|
863
|
+
let so2 = WServOrm(ds2, WOrm, `${fd2}/db.json`, 'worm')
|
|
864
|
+
try {
|
|
865
|
+
let reads = 0
|
|
866
|
+
class Dto {
|
|
867
|
+
constructor() {
|
|
868
|
+
this.id = 'new'
|
|
869
|
+
this.name = 'replacement'
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
set userIdUpdate(v) { // eslint-disable-line accessor-pairs
|
|
873
|
+
reads++
|
|
874
|
+
this.id = 'keep'
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
await so2.woItems.tabA.insert([{ id: 'keep', name: 'valuable' }])
|
|
878
|
+
await quiet(() => so2.procOrm('u1', 'tabA', 'mix', { add: [new Dto()], del: [{ id: 'keep' }] }))
|
|
879
|
+
assert.strictEqual(reads, 0, '原型setter不被執行')
|
|
880
|
+
let rs = await so2.woItems.tabA.select()
|
|
881
|
+
same(rs.map((v) => v.id), ['new'], '②新增列以id=new寫入、依請求刪除keep(修前兩者皆無)')
|
|
882
|
+
assert.strictEqual(rs[0].name, 'replacement')
|
|
883
|
+
assert.strictEqual(rs[0].userIdUpdate, 'u1', '稽核欄位已寫入')
|
|
884
|
+
}
|
|
885
|
+
finally {
|
|
886
|
+
await so2.closeAll()
|
|
887
|
+
fs.rmSync(fd2, { recursive: true, force: true })
|
|
888
|
+
}
|
|
889
|
+
})
|
|
890
|
+
|
|
614
891
|
})
|