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/dist/w-serv-orm.umd.js +2 -2
- package/dist/w-serv-orm.umd.js.map +1 -1
- package/docs/WServOrm.html +12 -7
- package/docs/WServOrm.mjs.html +11 -6
- package/docs/index.html +1 -1
- package/package.json +3 -3
- package/src/WServOrm.mjs +10 -5
- package/src/getDbBackupAndRecover.mjs +1 -2
- package/src/getMapOrm.mjs +457 -24
- package/src/getWoItems.mjs +4 -4
- package/test/api-objcopy.test.mjs +232 -0
- package/test/api-pkcast.test.mjs +113 -0
- package/test/api-procOrm.test.mjs +7 -6
- package/test/tools/ormStub.mjs +115 -0
- package/test/unit-WServOrm.test.mjs +2 -2
- package/test/unit-getDb.test.mjs +11 -0
- package/test/unit-mapOrm.test.mjs +7 -7
- package/test/unit-objcopy.test.mjs +907 -0
- package/test/unit-pkcast.test.mjs +325 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import assert from 'assert'
|
|
3
|
+
import WOrm from 'w-orm-lowdb/src/WOrmLowdb.mjs'
|
|
4
|
+
import WServOrm from '../src/WServOrm.mjs'
|
|
5
|
+
import { getReject, quiet, same } from './tools/ormStub.mjs'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
//以真ORM(w-orm-lowdb, mingo查詢)經procOrm驗證R-OBJ-01: 含整數length鍵之find與寫入列, 結果須等於以JS參考實作(預言機)算得者
|
|
9
|
+
//自建夾具: 資料表tabA之id為字串型pk, 各案前重新播種; 「ORM收到」為對woItems.tabA.select/delAll加spy所得之實參
|
|
10
|
+
let fd = './test/_tmp/api-objcopy'
|
|
11
|
+
|
|
12
|
+
let ds = { tabA: { keys: ['id', 'name', 'length'], settings: { id: { pk: true, type: 'STRING' }, name: { type: 'STRING' } } } }
|
|
13
|
+
|
|
14
|
+
let seed = [
|
|
15
|
+
{ id: 'a1', name: 'alpha', length: 3 },
|
|
16
|
+
{ id: 'a2', name: 'beta', length: 1 },
|
|
17
|
+
{ id: 'a3', name: 'gamma' },
|
|
18
|
+
{ id: 'a4', name: 'alpha', length: 'abc' },
|
|
19
|
+
{ id: 'a5', name: 'delta', length: 0 },
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
//refMatch, JS參考實作: 欄位等值(嚴格相等)、$and、$or、$nor、欄位層$in/$nin/$eq/$ne/$not; 缺欄視為undefined
|
|
23
|
+
function refMatch(row, find) {
|
|
24
|
+
let opMatch = (v, c) => {
|
|
25
|
+
if (c !== null && typeof c === 'object' && !Array.isArray(c)) {
|
|
26
|
+
return Object.keys(c).every((op) => {
|
|
27
|
+
if (op === '$eq') return v === c[op]
|
|
28
|
+
if (op === '$ne') return v !== c[op]
|
|
29
|
+
if (op === '$in') return c[op].includes(v)
|
|
30
|
+
if (op === '$nin') return !c[op].includes(v)
|
|
31
|
+
if (op === '$not') return !opMatch(v, c[op])
|
|
32
|
+
throw new Error(`參考實作不支援${op}`)
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
return v === c
|
|
36
|
+
}
|
|
37
|
+
return Object.keys(find).every((k) => {
|
|
38
|
+
let c = find[k]
|
|
39
|
+
if (k === '$and') return c.every((f) => refMatch(row, f))
|
|
40
|
+
if (k === '$or') return c.some((f) => refMatch(row, f))
|
|
41
|
+
if (k === '$nor') return !c.some((f) => refMatch(row, f))
|
|
42
|
+
return opMatch(row[k], c)
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let finds = [
|
|
47
|
+
{ length: 1, name: 'alpha' }, //無列同時符合: 修前條件消失而回全表
|
|
48
|
+
{ length: 1, name: 'beta' },
|
|
49
|
+
{ length: 0 },
|
|
50
|
+
{ length: 0, name: 'alpha' },
|
|
51
|
+
{ id: 'a1', length: 3 },
|
|
52
|
+
{ id: 'a1', length: 4 },
|
|
53
|
+
{ length: 3 },
|
|
54
|
+
{ length: 'abc' },
|
|
55
|
+
{ length: Number.MAX_SAFE_INTEGER },
|
|
56
|
+
{ length: 3.5 },
|
|
57
|
+
{ $or: [{ length: 3 }, { name: 'gamma' }] },
|
|
58
|
+
{ $and: [{ length: 1 }, { name: 'beta' }] },
|
|
59
|
+
{ $nor: [{ length: 3 }] },
|
|
60
|
+
{ id: { $in: ['a1', 'a2'] }, length: 1 },
|
|
61
|
+
{ name: 'alpha' },
|
|
62
|
+
{},
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
describe('R-OBJ-01 接w-orm-lowdb(以JS參考實作為預言機)', function() {
|
|
67
|
+
|
|
68
|
+
let so = null
|
|
69
|
+
let seen = []
|
|
70
|
+
let proc = (mode, input) => so.procOrm('u1', 'tabA', mode, input)
|
|
71
|
+
let rows = async () => (await so.woItems.tabA.select()).sort((a, b) => (a.id < b.id ? -1 : 1))
|
|
72
|
+
let reseed = async () => {
|
|
73
|
+
await so.woItems.tabA.delAll()
|
|
74
|
+
await so.woItems.tabA.insert(structuredClone(seed))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
before(function() {
|
|
78
|
+
fs.rmSync(fd, { recursive: true, force: true })
|
|
79
|
+
fs.mkdirSync(fd, { recursive: true })
|
|
80
|
+
so = WServOrm(ds, WOrm, `${fd}/db.json`, 'worm')
|
|
81
|
+
for (let fn of ['select', 'delAll']) {
|
|
82
|
+
let orig = so.woItems.tabA[fn].bind(so.woItems.tabA)
|
|
83
|
+
so.woItems.tabA[fn] = async (data) => {
|
|
84
|
+
seen.push({ fn, data })
|
|
85
|
+
return orig(data)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
beforeEach(async function() {
|
|
91
|
+
await reseed()
|
|
92
|
+
seen = []
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
after(async function() {
|
|
96
|
+
await so.closeAll()
|
|
97
|
+
fs.rmSync(fd, { recursive: true, force: true })
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('A13 select: ORM收到之條件同送入者, 回傳之列集合等於參考實作', async function() {
|
|
101
|
+
//R-OBJ-01 規則1; dspec 一之2①(同一find於select命中者即delAll刪除者)
|
|
102
|
+
for (let find of finds) {
|
|
103
|
+
seen = []
|
|
104
|
+
let r = await proc('select', find)
|
|
105
|
+
same(seen[0].data, find, `select ${JSON.stringify(find)} ORM收到`)
|
|
106
|
+
let expected = seed.filter((v) => refMatch(v, find)).map((v) => v.id).sort()
|
|
107
|
+
same(r.map((v) => v.id).sort(), expected, `select ${JSON.stringify(find)} 回傳`)
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('A14 delAll: ORM收到之條件同送入者, 刪除之列集合等於參考實作(修前{length:1,name:alpha}刪3/3列)', async function() {
|
|
112
|
+
//R-OBJ-01 規則1; dspec 一之2②③(不得擴大刪除範圍)
|
|
113
|
+
for (let find of finds) {
|
|
114
|
+
await reseed()
|
|
115
|
+
seen = []
|
|
116
|
+
let r = await proc('delAll', find)
|
|
117
|
+
same(seen[0].data, find, `delAll ${JSON.stringify(find)} ORM收到`)
|
|
118
|
+
let left = seed.filter((v) => !refMatch(v, find)).map((v) => v.id).sort()
|
|
119
|
+
same((await rows()).map((v) => v.id), left, `delAll ${JSON.stringify(find)} 剩餘`)
|
|
120
|
+
assert.strictEqual(r.nDeleted, seed.length - left.length, `delAll ${JSON.stringify(find)} 刪除筆數`)
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('A15 寫入列含整數length欄: insert、save、mix 讀回與送入者相同, 回傳主鍵即實際主鍵', async function() {
|
|
125
|
+
//R-OBJ-01 規則1: 修前 insert 含 length:3 之列內容遺失且主鍵被換成產生值, save 單筆之列不寫入而回報 {id:undefined}
|
|
126
|
+
await so.woItems.tabA.delAll()
|
|
127
|
+
let input = [
|
|
128
|
+
{ id: 'l1', name: 'x', length: 3 },
|
|
129
|
+
{ id: 'l2', name: 'y', length: 'abc' },
|
|
130
|
+
{ id: 'l3', name: 'z' },
|
|
131
|
+
{ name: 'noid', length: 0 },
|
|
132
|
+
{ id: 'l5', length: 2 },
|
|
133
|
+
]
|
|
134
|
+
let r = await proc('insert', structuredClone(input))
|
|
135
|
+
//已帶主鍵者須原樣(預期取自輸入, 不取自回傳: 否則被錯改主鍵時回傳與讀回一致而測不出, 外審 sol F5); 未帶者補值, 預期取自回傳
|
|
136
|
+
input.forEach((v, i) => {
|
|
137
|
+
if (v.id !== undefined) {
|
|
138
|
+
assert.strictEqual(r[i].id, v.id, `列${i}之主鍵`)
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
assert.ok(typeof r[i].id === 'string' && r[i].id.length > 30, `列${i}補值`)
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
let expected = input.map((v, i) => ({ ...v, id: v.id !== undefined ? v.id : r[i].id }))
|
|
145
|
+
same(await rows(), expected.sort((a, b) => (a.id < b.id ? -1 : 1)), 'insert讀回')
|
|
146
|
+
|
|
147
|
+
//save: 以回傳主鍵覆寫, length 改值; 單筆之列 {id, length:0} 亦須寫入
|
|
148
|
+
let r2 = await proc('save', expected.map((v) => ({ ...v, length: 7 })))
|
|
149
|
+
same(r2.map((v) => v.id), expected.map((v) => v.id), 'save回傳主鍵')
|
|
150
|
+
same(await rows(), expected.map((v) => ({ ...v, length: 7 })).sort((a, b) => (a.id < b.id ? -1 : 1)), 'save讀回')
|
|
151
|
+
let r3 = await proc('save', { id: 's1', length: 0 })
|
|
152
|
+
same(r3, { id: 's1' }, 'save單筆回傳')
|
|
153
|
+
assert.deepStrictEqual((await so.woItems.tabA.select({ id: 's1' })), [{ id: 's1', length: 0 }], 'save單筆讀回')
|
|
154
|
+
let r4 = await proc('save', [{ id: 's2', length: 1 }])
|
|
155
|
+
same(r4, [{ id: 's2' }], 'save單筆陣列回傳')
|
|
156
|
+
assert.deepStrictEqual((await so.woItems.tabA.select({ id: 's2' })), [{ id: 's2', length: 1 }], 'save單筆陣列讀回')
|
|
157
|
+
|
|
158
|
+
//mix
|
|
159
|
+
await so.woItems.tabA.delAll()
|
|
160
|
+
await so.woItems.tabA.insert([{ id: 'm2', name: 'old', length: 1 }])
|
|
161
|
+
let rm = await proc('mix', { add: [{ id: 'm1', name: 'x', length: 2 }], diff: [{ id: 'm2', name: 'new', length: 5 }] })
|
|
162
|
+
same(rm, [{ id: 'm1' }, { id: 'm2' }], 'mix回傳')
|
|
163
|
+
same(await rows(), [{ id: 'm1', name: 'x', length: 2 }, { id: 'm2', name: 'new', length: 5 }], 'mix讀回')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('A16 自有__proto__鍵: find與寫入列reject, 表內容不變(delAll不刪, insert不寫)', async function() {
|
|
167
|
+
//R-OBJ-01 規則3
|
|
168
|
+
let before = await rows()
|
|
169
|
+
seen = []
|
|
170
|
+
for (let c of ['{"__proto__":{"x":1}}', '{"id":"a1","__proto__":{"id":"zzz"}}', '{"$or":[{"__proto__":1}]}']) {
|
|
171
|
+
assert.strictEqual(await quiet(() => getReject(proc('select', JSON.parse(c)))), 'invalid find of select, unsupported key[__proto__]', c)
|
|
172
|
+
assert.strictEqual(await quiet(() => getReject(proc('delAll', JSON.parse(c)))), 'invalid find of delAll, unsupported key[__proto__]', c)
|
|
173
|
+
}
|
|
174
|
+
assert.strictEqual(await quiet(() => getReject(proc('insert', [JSON.parse('{"id":"p1","name":"x","__proto__":{"y":1}}')]))), 'failed to insert data, unsupported key[__proto__]: data[0]')
|
|
175
|
+
assert.strictEqual(await quiet(() => getReject(proc('mix', { add: [{ id: 'p2' }], diff: [JSON.parse('{"id":"a1","__proto__":{}}')] }))), 'failed to update data, unsupported key[__proto__]: data[0]')
|
|
176
|
+
assert.deepStrictEqual(seen, [], 'ORM之select與delAll不應被呼叫')
|
|
177
|
+
same(await rows(), before, '表內容不變')
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('A17 非純物件與存取器之find: select與delAll reject, 不回全表亦不刪全表(外審 opus #4、sol F1、F2)', async function() {
|
|
181
|
+
//R-INPUT-01 規則3、R-OBJ-01 規則3: 修前/D1後未擋時, 偽造Symbol.toStringTag之Map被複製成{}而刪除全表; getter讀兩次令檢查之值與使用之值不同
|
|
182
|
+
let before = await rows()
|
|
183
|
+
seen = []
|
|
184
|
+
let forged = Object.defineProperty(new Map([['id', 'a1']]), Symbol.toStringTag, { value: 'Object' })
|
|
185
|
+
class G {
|
|
186
|
+
get name() {
|
|
187
|
+
return 'alpha'
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
let reads = 0
|
|
191
|
+
let mkGetter = () => Object.defineProperty({}, 'id', {
|
|
192
|
+
enumerable: true,
|
|
193
|
+
get() {
|
|
194
|
+
return ++reads === 1 ? 'a1' : 'a2'
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
for (let [find, msg] of [
|
|
198
|
+
[forged, 'need an object'],
|
|
199
|
+
[new G(), 'need an object'],
|
|
200
|
+
[Object.create({ name: 'alpha' }), 'need an object'],
|
|
201
|
+
[mkGetter(), 'unsupported accessor property'],
|
|
202
|
+
]) {
|
|
203
|
+
for (let mode of ['select', 'delAll']) {
|
|
204
|
+
assert.strictEqual(await quiet(() => getReject(proc(mode, find))), `invalid find of ${mode}, ${msg}`)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
assert.strictEqual(reads, 0, 'getter不應被呼叫')
|
|
208
|
+
assert.deepStrictEqual(seen, [], 'ORM之select與delAll不應被呼叫')
|
|
209
|
+
same(await rows(), before, '表內容不變')
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('A18 woName為原型鏈名稱或路徑、find巢狀過深: 以固定字串reject, 不回全表亦不刪全表', async function() {
|
|
213
|
+
//R-INPUT-01 規則5、R-OBJ-01 規則5、R-ERR-01 規則1: 修前 woName 以 TypeError 物件 reject, 約1173層$and以 RangeError 物件 reject
|
|
214
|
+
let before = await rows()
|
|
215
|
+
seen = []
|
|
216
|
+
for (let name of ['__proto__', 'constructor', 'toString', 'tabA.select']) {
|
|
217
|
+
for (let [mode, input] of [['select', {}], ['delAll', {}], ['insert', [{ id: 'z' }]], ['del', [{ id: 'a1' }]]]) {
|
|
218
|
+
assert.strictEqual(await quiet(() => getReject(so.procOrm('u1', name, mode, input))), `can not find table[${name}] in ORM`, `${name} ${mode}`)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
let deep = { id: 'a1' }
|
|
222
|
+
for (let i = 0; i < 1e4; i++) {
|
|
223
|
+
deep = { $and: [deep] }
|
|
224
|
+
}
|
|
225
|
+
for (let mode of ['select', 'delAll']) {
|
|
226
|
+
assert.strictEqual(await quiet(() => getReject(proc(mode, deep))), `invalid find of ${mode}, too deep`, mode)
|
|
227
|
+
}
|
|
228
|
+
assert.deepStrictEqual(seen, [], 'ORM之select與delAll不應被呼叫')
|
|
229
|
+
same(await rows(), before, '表內容不變')
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
})
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import assert from 'assert'
|
|
3
|
+
import WOrm from 'w-orm-lowdb/src/WOrmLowdb.mjs'
|
|
4
|
+
import ltdtDiffByKey from 'wsemi/src/ltdtDiffByKey.mjs'
|
|
5
|
+
import ds from '../schema/index.mjs'
|
|
6
|
+
import WServOrm from '../src/WServOrm.mjs'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
//以真ORM(w-orm-lowdb, 主鍵固定為字串id, 數字主鍵原會被其以genIDSeq取代)經procOrm驗證R-ID-02、R-MIX-01; schema之tests表之id為STRING pk
|
|
10
|
+
let fd = './test/_tmp/api-pkcast'
|
|
11
|
+
|
|
12
|
+
async function getReject(pm) {
|
|
13
|
+
try {
|
|
14
|
+
await pm
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
return err
|
|
18
|
+
}
|
|
19
|
+
throw new Error('預期reject但resolve')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
describe('數字主鍵接w-orm-lowdb', function() {
|
|
24
|
+
|
|
25
|
+
let so = null
|
|
26
|
+
let proc = (mode, input) => so.procOrm('u1', 'tests', mode, input)
|
|
27
|
+
let rows = async () => (await so.woItems.tests.select()).map((v) => [v.id, v.isActive])
|
|
28
|
+
let quiet = async (fn) => {
|
|
29
|
+
let logOrig = console.log
|
|
30
|
+
console.log = () => {}
|
|
31
|
+
try {
|
|
32
|
+
return await fn()
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
console.log = logOrig
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
before(function() {
|
|
40
|
+
fs.rmSync(fd, { recursive: true, force: true })
|
|
41
|
+
fs.mkdirSync(fd, { recursive: true })
|
|
42
|
+
so = WServOrm(ds, WOrm, `${fd}/db.json`, 'worm')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
beforeEach(async function() {
|
|
46
|
+
await so.woItems.tests.delAll()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
after(async function() {
|
|
50
|
+
await so.closeAll()
|
|
51
|
+
fs.rmSync(fd, { recursive: true, force: true })
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('A08 數字主鍵存為其字串形式, 重送與修改命中同一筆, 以數字或字串皆可查可刪', async function() {
|
|
55
|
+
//R-ID-02 規則2: 回報 = 實際; 重送不增筆(原每次另增genIDSeq之列); 寫入、查詢、刪除同一規則
|
|
56
|
+
let r = await proc('insert', [{ id: 123, isActive: 'y' }])
|
|
57
|
+
assert.deepStrictEqual(r, [{ id: '123' }])
|
|
58
|
+
await proc('insert', [{ id: 123, isActive: 'y' }])
|
|
59
|
+
await proc('save', [{ id: 123, isActive: 'n' }])
|
|
60
|
+
await proc('mix', { diff: [{ id: 123, isActive: 'm' }] })
|
|
61
|
+
assert.deepStrictEqual(await rows(), [['123', 'm']])
|
|
62
|
+
assert.strictEqual((await proc('select', { id: 123 })).length, 1)
|
|
63
|
+
assert.strictEqual((await proc('select', { id: '123' })).length, 1)
|
|
64
|
+
let rd = await proc('del', [{ id: 123 }])
|
|
65
|
+
assert.deepStrictEqual(rd, [{ n: 1, nDeleted: 1, ok: 1 }])
|
|
66
|
+
assert.deepStrictEqual(await rows(), [])
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('A09 表內已有他方以字串寫入之同值主鍵: 以數字save覆寫該列, insert略過而回報字串主鍵', async function() {
|
|
70
|
+
//R-ID-02 規則2: 123與'123'為同一筆(對外行為變更, 原另增genIDSeq之列)
|
|
71
|
+
await so.woItems.tests.insert([{ id: '777', isActive: 'A' }])
|
|
72
|
+
let rs = await proc('save', [{ id: 777, isActive: 'B' }])
|
|
73
|
+
assert.deepStrictEqual(rs, [{ id: '777' }])
|
|
74
|
+
let ri = await proc('insert', [{ id: 777, isActive: 'C' }])
|
|
75
|
+
assert.deepStrictEqual(ri, [{ id: '777' }])
|
|
76
|
+
assert.deepStrictEqual(await rows(), [['777', 'B']])
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('A10 以ltdtDiffByKey嚴格比對產生之add 888、del 888字串, 整批reject且表內不變', async function() {
|
|
80
|
+
//R-MIX-01 規則1、2: 原為insert略過、del刪除而resolve遺失資料
|
|
81
|
+
await so.woItems.tests.insert([{ id: '888', isActive: 'y' }])
|
|
82
|
+
let old = await so.woItems.tests.select()
|
|
83
|
+
let input = ltdtDiffByKey(old, [{ ...old[0], id: 888 }], 'id', { withInfor: false })
|
|
84
|
+
assert.deepStrictEqual([input.add.length, input.del.length], [1, 1])
|
|
85
|
+
let err = await quiet(() => getReject(proc('mix', input)))
|
|
86
|
+
assert.strictEqual(err, 'invalid input of mix, same id in add and del: add[0], del[0]')
|
|
87
|
+
assert.deepStrictEqual(await rows(), [['888', 'y']])
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('A11 同批之數字與其字串形式: 表內一筆(首筆內容), 回報兩筆同鍵', async function() {
|
|
91
|
+
//R-ID-02 規則2; insert回傳之id含被略過者
|
|
92
|
+
let r = await proc('insert', [{ id: 999, isActive: 'p' }, { id: '999', isActive: 'q' }])
|
|
93
|
+
assert.deepStrictEqual(r, [{ id: '999' }, { id: '999' }])
|
|
94
|
+
assert.deepStrictEqual(await rows(), [['999', 'p']])
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('A12 查詢與delAll之條件以數字給予: 等值、$in、$ne、$not命中字串主鍵, 範圍依字典序', async function() {
|
|
98
|
+
//R-ID-02 規則2(查詢條件)與已知限制(範圍依字典序: '40' < '5')
|
|
99
|
+
await proc('insert', [{ id: 5, isActive: 'y' }, { id: 40, isActive: 'y' }, { id: 'abc', isActive: 'y' }])
|
|
100
|
+
let ids = async (find) => (await proc('select', find)).map((v) => v.id).sort()
|
|
101
|
+
assert.deepStrictEqual(await ids({ id: 5 }), ['5'])
|
|
102
|
+
assert.deepStrictEqual(await ids({ id: { $in: [5, 40] } }), ['40', '5'])
|
|
103
|
+
assert.deepStrictEqual(await ids({ id: { $ne: 5 } }), ['40', 'abc'])
|
|
104
|
+
assert.deepStrictEqual(await ids({ id: { $not: { $eq: 5 } } }), ['40', 'abc'])
|
|
105
|
+
assert.deepStrictEqual(await ids({ id: { $lt: 5 } }), ['40'])
|
|
106
|
+
let r1 = await proc('delAll', { id: { $in: [40] } })
|
|
107
|
+
assert.deepStrictEqual(r1, { n: 1, nDeleted: 1, ok: 1 })
|
|
108
|
+
let r2 = await proc('delAll', { id: 5 })
|
|
109
|
+
assert.deepStrictEqual(r2, { n: 1, nDeleted: 1, ok: 1 })
|
|
110
|
+
assert.deepStrictEqual(await rows(), [['abc', 'y']])
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
})
|
|
@@ -44,17 +44,18 @@ describe('procOrm接w-orm-lowdb', function() {
|
|
|
44
44
|
})
|
|
45
45
|
|
|
46
46
|
it('A02 del含無效主鍵', async function() {
|
|
47
|
-
//R02: 逐筆ok為0即reject
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
//R02: 逐筆ok為0即reject並列出失敗筆(無有效主鍵者列位置); R10: 單筆失敗不中斷整批, 有效筆已刪除
|
|
48
|
+
//無效主鍵取非字串非數字之值: 數字依R-ID-02轉為字串後為有效主鍵(未命中), 不再觸發逐筆失敗
|
|
49
|
+
let err = await getReject(proc('del', [{ id: 'a1' }, { id: true }]))
|
|
50
|
+
assert.strict.equal(err, 'failed to delete data: data[1]')
|
|
50
51
|
let rs = await proc('select', { id: 'a1' })
|
|
51
52
|
assert.strict.equal(rs.length, 0)
|
|
52
53
|
})
|
|
53
54
|
|
|
54
55
|
it('A03 mix之del含無效主鍵', async function() {
|
|
55
|
-
//R03: del之逐筆失敗不中斷diff, diff已寫入, 最後reject列出del之失敗
|
|
56
|
-
let err = await getReject(proc('mix', { del: [{ id:
|
|
57
|
-
assert.strict.equal(err, 'failed to delete data:
|
|
56
|
+
//R03: del之逐筆失敗不中斷diff, diff已寫入, 最後reject列出del之失敗(未帶主鍵之列以位置列出)
|
|
57
|
+
let err = await getReject(proc('mix', { del: [{ id: null }], diff: [{ id: 'a2', isActive: 'y' }] }))
|
|
58
|
+
assert.strict.equal(err, 'failed to delete data: data[0]')
|
|
58
59
|
let rs = await proc('select', { id: 'a2' })
|
|
59
60
|
assert.strict.equal(rs.length, 1)
|
|
60
61
|
})
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from 'util'
|
|
2
|
+
import assert from 'assert'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
//替身ORM與測試共用小工具(供 unit-objcopy、api-objcopy 使用): 每次createStub各自持有calls與plan, 不共用模組層狀態
|
|
6
|
+
//替身ORM記錄收到之數據, 回傳形狀依w-orm系列規格: save、del回與輸入等長之逐筆陣列, insert與delAll回聚合物件; plan可覆寫各函數之回傳
|
|
7
|
+
function createStub() {
|
|
8
|
+
let calls = []
|
|
9
|
+
let plan = {}
|
|
10
|
+
function WOrmStub() {
|
|
11
|
+
let ev = {}
|
|
12
|
+
for (let fn of ['select', 'insert', 'save', 'del', 'delAll']) {
|
|
13
|
+
ev[fn] = async (data) => {
|
|
14
|
+
calls.push({ fn, data })
|
|
15
|
+
if (plan[fn]) {
|
|
16
|
+
return plan[fn](data)
|
|
17
|
+
}
|
|
18
|
+
if (fn === 'select') {
|
|
19
|
+
return []
|
|
20
|
+
}
|
|
21
|
+
if (fn === 'save') {
|
|
22
|
+
return (Array.isArray(data) ? data : [data]).map(() => ({ n: 1, nInserted: 0, nModified: 1, ok: 1 }))
|
|
23
|
+
}
|
|
24
|
+
if (fn === 'del') {
|
|
25
|
+
return (Array.isArray(data) ? data : [data]).map(() => ({ n: 1, nDeleted: 1, ok: 1 }))
|
|
26
|
+
}
|
|
27
|
+
if (fn === 'delAll') {
|
|
28
|
+
return { n: 0, nDeleted: 0, ok: 1 }
|
|
29
|
+
}
|
|
30
|
+
return { n: 1, nInserted: 1, ok: 1 }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return ev
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
WOrmStub,
|
|
37
|
+
calls,
|
|
38
|
+
reset: () => {
|
|
39
|
+
calls.length = 0
|
|
40
|
+
for (let k of Object.keys(plan)) {
|
|
41
|
+
delete plan[k]
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async function getReject(pm) {
|
|
49
|
+
try {
|
|
50
|
+
await pm
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
return err
|
|
54
|
+
}
|
|
55
|
+
throw new Error('預期reject但resolve')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async function quiet(fn) {
|
|
60
|
+
let logOrig = console.log
|
|
61
|
+
console.log = () => {}
|
|
62
|
+
try {
|
|
63
|
+
return await fn()
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
console.log = logOrig
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
//protoDiff: isDeepStrictEqual對原型之比較不完整(實測 Node 24: {__proto__:{name:'x'},id:'a'} 與 {id:'a'} 視為相等, 而這正是複本以r[k]=v建立時原型被換掉之症狀),
|
|
72
|
+
//故於兩者已深度相等後逐層補比原型; 有差異回傳該層路徑, 無則回傳null
|
|
73
|
+
function protoDiff(a, b, path = '') {
|
|
74
|
+
if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') {
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) {
|
|
78
|
+
return path === '' ? '(根)' : path
|
|
79
|
+
}
|
|
80
|
+
for (let k of Object.keys(a)) {
|
|
81
|
+
let r = protoDiff(a[k], b[k], `${path}.${k}`)
|
|
82
|
+
if (r !== null) {
|
|
83
|
+
return r
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
//same: 深度嚴格相等且各層原型相同, 失敗時只印標籤與簡短摘要(輸入含上百萬鍵之物件時, assert.deepStrictEqual之差異輸出會癱瘓測試)
|
|
91
|
+
function same(actual, expected, label = '') {
|
|
92
|
+
let ok = isDeepStrictEqual(actual, expected)
|
|
93
|
+
if (ok) {
|
|
94
|
+
let p = protoDiff(actual, expected)
|
|
95
|
+
if (p !== null) {
|
|
96
|
+
assert.fail(`${label} 原型不同於 ${p}`)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (!ok) {
|
|
100
|
+
let short = (v) => {
|
|
101
|
+
let c
|
|
102
|
+
try {
|
|
103
|
+
c = typeof v === 'function' ? String(v) : JSON.stringify(v, (k, x) => (x === undefined ? '__undefined__' : x))
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
c = String(v)
|
|
107
|
+
}
|
|
108
|
+
return c === undefined ? String(v) : c.slice(0, 200)
|
|
109
|
+
}
|
|
110
|
+
assert.fail(`${label} 不相等: 實得 ${short(actual)} / 預期 ${short(expected)}`)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
export { createStub, getReject, quiet, same }
|
|
@@ -395,14 +395,14 @@ describe('procOrm補主鍵', function() {
|
|
|
395
395
|
})
|
|
396
396
|
|
|
397
397
|
it('P02 依主鍵值決定是否補值', async function() {
|
|
398
|
-
//
|
|
398
|
+
//R-ID-01 規則1: null、空字串、物件補值, 非空字串保留; R-ID-02 規則2: 字串型(或型別未知)主鍵欄之有限數字轉為其字串形式
|
|
399
399
|
let log = []
|
|
400
400
|
let A = WServOrm(dsAll, mkWOrm('A', log), 'url-a', 'db-a')
|
|
401
401
|
let input = [{ id: null }, { id: '' }, { id: { x: 1 } }, { id: 123 }, { id: 'keep' }]
|
|
402
402
|
let r = await A.procOrm('u1', 'tests', 'save', input)
|
|
403
403
|
let got = log[0].data.map((v) => v.id)
|
|
404
404
|
assert.ok(isId(got[0]) && isId(got[1]) && isId(got[2]))
|
|
405
|
-
assert.strict.equal(got[3], 123)
|
|
405
|
+
assert.strict.equal(got[3], '123')
|
|
406
406
|
assert.strict.equal(got[4], 'keep')
|
|
407
407
|
assert.strict.deepEqual(r.map((v) => v.id), got)
|
|
408
408
|
})
|
package/test/unit-getDb.test.mjs
CHANGED
|
@@ -112,6 +112,17 @@ describe('getDb備份與還原', function() {
|
|
|
112
112
|
assert.strict.deepEqual(log, [])
|
|
113
113
|
})
|
|
114
114
|
|
|
115
|
+
it('B04b recover之備份之name為原型鏈名稱(constructor、toString、__proto__)即於寫入前reject, 不留部分還原', async function() {
|
|
116
|
+
//R-DB-01 規則3: 原 wsemi haskey 為 in 運算, 原型鏈名稱通過驗證, 前面之表寫入後才以 TypeError 失敗(外審 opus 二輪 F9)
|
|
117
|
+
for (let name of ['constructor', 'toString', '__proto__']) {
|
|
118
|
+
let log = []
|
|
119
|
+
let fp = writeBackup(`proto-${name}.json`, JSON.stringify([{ name: 'tests', data: [{ id: 'a' }] }, { name, data: [{ id: 'z' }] }]))
|
|
120
|
+
let err = await quiet(() => getReject(getDb.recover(mkWoItems(log), fp, { bLog: false })))
|
|
121
|
+
assert.ok(String(err).includes('invalid backup data') && String(err).includes(name), `${name}: ${err}`)
|
|
122
|
+
assert.strict.deepEqual(log, [], `${name}: 不應有任何寫入`)
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
|
|
115
126
|
it('B05 recover成功', async function() {
|
|
116
127
|
//D5: 成功路徑不變
|
|
117
128
|
let log = []
|
|
@@ -119,8 +119,8 @@ describe('mapOrm逐筆失敗', function() {
|
|
|
119
119
|
})
|
|
120
120
|
|
|
121
121
|
it('U10 del數字主鍵ok為0', async function() {
|
|
122
|
-
//R02: del逐筆ok為0即reject;
|
|
123
|
-
plan.del = () => [okDel, { n:
|
|
122
|
+
//R02: del逐筆ok為0即reject; 數字主鍵依R-ID-02轉為字串後交ORM, 訊息照列
|
|
123
|
+
plan.del = () => [okDel, { n: 1, nDeleted: 0, ok: 0, err: 'E-del' }]
|
|
124
124
|
let err = await getReject(proc('del', [{ id: 'd1' }, { id: 123 }]))
|
|
125
125
|
assert.strict.equal(err, 'failed to delete data: id[123]')
|
|
126
126
|
})
|
|
@@ -277,10 +277,10 @@ describe('mapOrm主鍵與clearXSS', function() {
|
|
|
277
277
|
})
|
|
278
278
|
|
|
279
279
|
it('X05 數字主鍵、未帶主鍵與不被改寫之主鍵照常寫入', async function() {
|
|
280
|
-
//判準為是否被改寫:
|
|
280
|
+
//判準為是否被改寫: 數字依R-ID-02轉為字串(不含標記), 未帶主鍵者由本套件補值, 白名單標籤之規範寫法不被改寫
|
|
281
281
|
plan.save = (data) => data.map(() => okSave)
|
|
282
282
|
let r = await proc('save', [{ id: 5 }, { id: '<b>粗體</b>' }, { name: 'no-id' }])
|
|
283
|
-
assert.strict.equal(r[0].id, 5)
|
|
283
|
+
assert.strict.equal(r[0].id, '5')
|
|
284
284
|
assert.strict.equal(r[1].id, '<b>粗體</b>')
|
|
285
285
|
assert.ok(typeof r[2].id === 'string' && r[2].id.length > 0)
|
|
286
286
|
})
|
|
@@ -295,14 +295,14 @@ describe('mapOrm主鍵與clearXSS', function() {
|
|
|
295
295
|
})
|
|
296
296
|
|
|
297
297
|
it('X07 insert與mix各入口之其餘主鍵狀態', async function() {
|
|
298
|
-
|
|
298
|
+
//數字依R-ID-02轉為字串, 不被改寫之主鍵照寫, mix之修改未帶主鍵者補值, mix之刪除不經clearXSS而原樣交ORM
|
|
299
299
|
plan.insert = (data) => ({ n: data.length, nInserted: data.length, ok: 1 })
|
|
300
300
|
plan.del = (data) => data.map(() => okDel)
|
|
301
301
|
plan.save = (data) => data.map(() => okSave)
|
|
302
302
|
let r1 = await proc('insert', [{ id: 7 }, { id: 'a < b' }])
|
|
303
|
-
assert.strict.deepEqual(r1, [{ id: 7 }, { id: 'a < b' }])
|
|
303
|
+
assert.strict.deepEqual(r1, [{ id: '7' }, { id: 'a < b' }])
|
|
304
304
|
let r2 = await proc('mix', { add: [{ id: 8 }, { id: '1 > 0' }], del: [{ id: 'a<b' }], diff: [{ id: 9 }, { id: 'x <= 5' }, { name: 'no-id' }] })
|
|
305
|
-
assert.strict.deepEqual(r2.slice(0, 5), [{ id: 8 }, { id: '1 > 0' }, { id: 'a<b' }, { id: 9 }, { id: 'x <= 5' }])
|
|
305
|
+
assert.strict.deepEqual(r2.slice(0, 5), [{ id: '8' }, { id: '1 > 0' }, { id: 'a<b' }, { id: '9' }, { id: 'x <= 5' }])
|
|
306
306
|
assert.ok(typeof r2[5].id === 'string' && r2[5].id.length > 0)
|
|
307
307
|
assert.strict.deepEqual(calls.map((v) => v.fn), ['insert', 'insert', 'del', 'save'])
|
|
308
308
|
assert.strict.deepEqual(calls[2].data, [{ id: 'a<b' }])
|