w-serv-orm 1.1.11 → 1.1.13

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.
@@ -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,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 }
@@ -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 = []