w-orm-lowdb 1.0.30 → 1.0.32

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,533 @@
1
+ import assert from 'assert'
2
+ import fs from 'fs'
3
+ import w from 'wsemi'
4
+ import cvCases from 'wsemi/src/_cvOrmFindCases.mjs'
5
+ import cvOrmFind from 'wsemi/src/cvOrmFind.mjs'
6
+ import WOrm from '../src/WOrmLowdb.mjs'
7
+
8
+
9
+ //條件(find)之系列白名單(帳本R-FIND-01; 規則: C:\opensrcOper\dspec\w-orm系列條件語法支援盤點.md〈一之三〉)
10
+ //共用檢核為wsemi之cvOrmFind, 契約案例表為wsemi之_cvOrmFindCases(各w-orm-*逐案對公開select與delAll執行, 每案以seed()重建),
11
+ //本檔驗證select與delAll確實於入口(serialize佇列之外)呼叫並只使用檢核後之複本, 而未被後端(mingo)略過
12
+ //lowdb屬MongoDB式族, 命中集合取案例之ids.mongo; 數據以JSON存檔, 故條件內之Date由本套件轉為ISO字串後方能命中(E05 C09 I17);
13
+ //本套件之$in上限即共用預設(10000), 故案例之need不使本套件reject
14
+ describe('find-whitelist', function() {
15
+ let { seed, cases } = cvCases
16
+ let dir = './test/_tmp/find-whitelist'
17
+ let dbName = 'worm'
18
+
19
+ //ids, 取id集合(升冪)
20
+ let ids = (rs) => rs.map((v) => v.id).sort()
21
+
22
+ //tick, 讓出一個macrotask, 令延遲發出之事件有機會發出
23
+ let tick = () => new Promise((resolve) => setTimeout(resolve, 0))
24
+
25
+ //directMsg, 共用檢核對同一條件直接給出之訊息(後端須原樣給出, 不得改寫、包裝或另立訊息)
26
+ let directMsg = (f) => {
27
+ try {
28
+ cvOrmFind(f)
29
+ }
30
+ catch (err) {
31
+ return err.message
32
+ }
33
+ return null
34
+ }
35
+
36
+ //mkObserver, 建立呼叫觀測器: 事件記錄於evs, 每筆error事件附帶發出當下呼叫端之await是否已恢復(settledAtEmit, 即呼叫端可觀察之先後)
37
+ let mkObserver = (wo) => {
38
+ let evs = []
39
+ let cur = { settled: false }
40
+ wo.on('error', function(mode, data, err) {
41
+ evs.push({ t: 'error', mode, data, err, settledAtEmit: cur.settled })
42
+ })
43
+ wo.on('change', function(mode) {
44
+ evs.push({ t: 'change', mode })
45
+ })
46
+
47
+ //invoke, 呼叫wo[fn](arg)並回傳{ ok, val, err, nAtSettle, probe, evs }, nAtSettle為結果定案當下之事件數,
48
+ //probe為結果定案當下(同一microtask鏈內, 尚未讓出macrotask)呼叫probeFn()之回傳, evs為再讓出一個macrotask後之事件
49
+ let invoke = async(fn, arg, probeFn = null) => {
50
+ evs.length = 0
51
+ cur = { settled: false }
52
+ let out = { ok: false, val: null, err: null, nAtSettle: 0, probe: null, evs: [] }
53
+ try {
54
+ out.val = await wo[fn](arg)
55
+ out.ok = true
56
+ }
57
+ catch (err) {
58
+ out.err = err
59
+ }
60
+ cur.settled = true
61
+ out.nAtSettle = evs.length
62
+ out.probe = probeFn ? probeFn() : null
63
+ await tick()
64
+ out.evs = evs.slice()
65
+ return out
66
+ }
67
+
68
+ //clear
69
+ let clear = () => {
70
+ evs.length = 0
71
+ }
72
+
73
+ return { invoke, clear }
74
+ }
75
+
76
+ //mk, 於測試資料夾下建立資料庫檔(url為檔案路徑)
77
+ let mk = (name, opt = {}) => {
78
+ let url = `${dir}/${name}.json`
79
+ w.fsDeleteFile(url)
80
+ return { url, wo: WOrm({ url, db: dbName, cl: name, ...opt }) }
81
+ }
82
+
83
+ //reset, 清空後以seed()重建
84
+ let reset = async(wo) => {
85
+ await wo.delAll()
86
+ await wo.insertBulk(seed())
87
+ }
88
+
89
+ //sortRows
90
+ let sortRows = (rows) => rows.sort((a, b) => (a.id < b.id ? -1 : 1))
91
+
92
+ //chkRej, 整批reject之共同斷言(WL-R1、WL-R3): 訊息以invalid find開頭且逐字等於共用檢核給出之訊息(expectMsg), error事件於reject前恰發出一次(訊息同reject), change不發
93
+ let chkRej = (out, fn, expectMsg = null) => {
94
+ assert.strict.equal(out.ok, false, `${fn} should reject`)
95
+ assert.strict.ok(out.err instanceof Error, `${fn} should reject with Error`)
96
+ assert.strict.ok(out.err.message.indexOf('invalid find: ') === 0, `${fn} message should start with 'invalid find: ' but got: ${out.err.message}`)
97
+ if (expectMsg !== null) {
98
+ assert.strict.equal(out.err.message, expectMsg, `${fn} message should be the one of cvOrmFind`)
99
+ }
100
+ let evErr = out.evs.filter((v) => v.t === 'error')
101
+ let evChg = out.evs.filter((v) => v.t === 'change' && v.mode === fn) //只計本函數之change(佇列測試中同時排入之insertBulk會發出自己的change)
102
+ assert.strict.equal(evErr.length, 1, `${fn} should emit error exactly once`)
103
+ assert.strict.equal(out.nAtSettle, 1, `${fn} should emit error before reject`)
104
+ assert.strict.equal(evErr[0].settledAtEmit, false, `${fn} should emit error before reject`)
105
+ assert.strict.equal(evErr[0].mode, fn)
106
+ assert.strict.equal(evErr[0].data, null)
107
+ assert.strict.equal(evErr[0].err, out.err.message, `${fn} error event should carry the reject message`)
108
+ assert.strict.equal(evChg.length, 0, `${fn} should not emit change`)
109
+ }
110
+
111
+ before(function() {
112
+ w.fsDeleteFolder(dir)
113
+ fs.mkdirSync(dir, { recursive: true })
114
+ })
115
+
116
+ after(function() {
117
+ w.fsDeleteFolder(dir)
118
+ })
119
+
120
+ describe('cases', function() {
121
+ let wo = null
122
+ let ob = null
123
+ let seedIds = ids(seed())
124
+ let seedRows = null
125
+
126
+ before(async function() {
127
+ wo = mk('cases').wo
128
+ ob = mkObserver(wo)
129
+ await reset(wo)
130
+ seedRows = sortRows(await wo.select())
131
+ })
132
+
133
+ //案例表本身: 防案例表被替換為空表或缺組別而使下列逐案全綠成為假象
134
+ it('should get nonempty case table covering all groups with seed of 6 rows', function() {
135
+ let groups = [...new Set(cases.map((c) => c.group))].sort().join('')
136
+ assert.strict.equal(groups, 'CEFHILNTVX')
137
+ assert.strict.ok(cases.length >= 241)
138
+ assert.strict.deepStrictEqual(seedIds, ['a1', 'a2', 'a3', 'a4', 'a5', 'a6'])
139
+ })
140
+
141
+ for (let c of cases) {
142
+
143
+ if (c.expect === 'REJ') {
144
+
145
+ //REJ案: select與delAll皆整批reject, 資料表不變(WL-02、WL-07等; 對應案例表expect=REJ)
146
+ it(`should reject select and delAll without any change for ${c.id} ${c.label}`, async function() {
147
+ await reset(wo)
148
+ ob.clear()
149
+
150
+ let expectMsg = directMsg(c.find())
151
+ assert.strict.notEqual(expectMsg, null, 'cvOrmFind should reject the case')
152
+ let outS = await ob.invoke('select', c.find())
153
+ chkRej(outS, 'select', expectMsg)
154
+ let outD = await ob.invoke('delAll', c.find())
155
+ chkRej(outD, 'delAll', expectMsg)
156
+
157
+ //資料不變
158
+ assert.strict.deepStrictEqual(sortRows(await wo.select()), seedRows)
159
+ })
160
+
161
+ }
162
+ else {
163
+
164
+ //ALL與OK案: select命中ids.mongo; delAll刪除之集合等於select命中之集合(系列最低要求①), n與nDeleted為實際刪除筆數
165
+ it(`should select ${JSON.stringify(c.ids.mongo)} and delAll the same set for ${c.id} ${c.label}`, async function() {
166
+ await reset(wo)
167
+ ob.clear()
168
+
169
+ let outS = await ob.invoke('select', c.find())
170
+ assert.strict.equal(outS.ok, true, `select should resolve but got: ${outS.err}`)
171
+ let sel = ids(outS.val)
172
+ assert.strict.deepStrictEqual(sel, c.ids.mongo)
173
+ assert.strict.equal(outS.evs.filter((v) => v.t === 'error').length, 0)
174
+
175
+ await reset(wo)
176
+ ob.clear()
177
+ let outD = await ob.invoke('delAll', c.find())
178
+ assert.strict.equal(outD.ok, true, `delAll should resolve but got: ${outD.err}`)
179
+ assert.strict.deepStrictEqual(outD.val, { n: sel.length, nDeleted: sel.length, ok: 1 })
180
+ assert.strict.equal(outD.evs.filter((v) => v.t === 'error').length, 0)
181
+
182
+ let rest = ids(await wo.select())
183
+ let deleted = seedIds.filter((id) => rest.indexOf(id) < 0)
184
+ assert.strict.deepStrictEqual(deleted, sel)
185
+ })
186
+
187
+ }
188
+
189
+ }
190
+
191
+ })
192
+
193
+ //檢查之值即使用之值(WL-H4): 檢核回傳之複本即後續查詢與刪除所用之條件, 不得再讀呼叫端之原物件
194
+ //以Proxy令欄位g於第1次讀取回'A'(檢核所讀)、其後回'B', 若後端改讀原物件(mingo以[]取值)則命中集合改變
195
+ describe('copy', function() {
196
+ let wo = null
197
+
198
+ before(async function() {
199
+ wo = mk('copy').wo
200
+ await reset(wo)
201
+ })
202
+
203
+ //mkFlip, 欄位g第1次被讀取(不論以屬性描述子或get)回'A', 其後回'B'; 同時回傳讀取次數
204
+ let mkFlip = () => {
205
+ let st = { n: 0 }
206
+ let val = () => {
207
+ st.n++
208
+ return st.n === 1 ? 'A' : 'B'
209
+ }
210
+ let px = new Proxy({ g: 'A' }, {
211
+ getOwnPropertyDescriptor(t, k) {
212
+ let d = Reflect.getOwnPropertyDescriptor(t, k)
213
+ if (k === 'g') {
214
+ d = { ...d, value: val() }
215
+ }
216
+ return d
217
+ },
218
+ get(t, k, r) {
219
+ if (k === 'g') {
220
+ return val()
221
+ }
222
+ return Reflect.get(t, k, r)
223
+ },
224
+ })
225
+ return { px, st }
226
+ }
227
+
228
+ let shapes = {
229
+ top: (px) => px,
230
+ nested: (px) => ({ $or: [px] }),
231
+ }
232
+
233
+ for (let name of Object.keys(shapes)) {
234
+
235
+ it(`should select by the checked value only for ${name} condition`, async function() {
236
+ await reset(wo)
237
+ let { px, st } = mkFlip()
238
+ let r = await wo.select(shapes[name](px))
239
+ assert.strict.deepStrictEqual(ids(r), ['a1', 'a2'])
240
+ assert.strict.equal(st.n, 1, 'value should be read once')
241
+ })
242
+
243
+ it(`should delAll by the checked value only for ${name} condition`, async function() {
244
+ await reset(wo)
245
+ let { px, st } = mkFlip()
246
+ let r = await wo.delAll(shapes[name](px))
247
+ assert.strict.deepStrictEqual(r, { n: 2, nDeleted: 2, ok: 1 })
248
+ assert.strict.deepStrictEqual(ids(await wo.select()), ['a3', 'a4', 'a5', 'a6'])
249
+ assert.strict.equal(st.n, 1, 'value should be read once')
250
+ })
251
+
252
+ }
253
+
254
+ //熱快取(useCache已填入)時亦只用檢核後之複本: 原快取測試只以壞條件與穩定條件驗快取, 守不住「合法條件改讀原物件」
255
+ for (let fn of ['select', 'delAll']) {
256
+ it(`should use the checked copy only with warm cache for ${fn}`, async function() {
257
+ let { wo: wc } = mk(`warm${fn}`, { useCache: true })
258
+ await wc.insertBulk(seed())
259
+ assert.strict.equal((await wc.select()).length, 6) //填入快取
260
+ let { px, st } = mkFlip()
261
+ if (fn === 'select') {
262
+ assert.strict.deepStrictEqual(ids(await wc.select(px)), ['a1', 'a2'])
263
+ }
264
+ else {
265
+ assert.strict.deepStrictEqual(await wc.delAll(px), { n: 2, nDeleted: 2, ok: 1 })
266
+ assert.strict.deepStrictEqual(ids(await wc.select()), ['a3', 'a4', 'a5', 'a6'])
267
+ }
268
+ assert.strict.equal(st.n, 1, 'value should be read once')
269
+ })
270
+ }
271
+
272
+ //「是否有條件」之判斷亦須依檢核後之複本: ownKeys於第1次回['g']、其後回[](若改讀原物件, select會回全部、delAll會刪光)
273
+ for (let fn of ['select', 'delAll']) {
274
+ it(`should decide the empty condition by the checked copy only for ${fn}`, async function() {
275
+ await reset(wo)
276
+ let n = 0
277
+ let px = new Proxy({ g: 'A' }, {
278
+ ownKeys() {
279
+ n++
280
+ return n === 1 ? ['g'] : []
281
+ },
282
+ })
283
+ if (fn === 'select') {
284
+ assert.strict.deepStrictEqual(ids(await wo.select(px)), ['a1', 'a2'])
285
+ }
286
+ else {
287
+ assert.strict.deepStrictEqual(await wo.delAll(px), { n: 2, nDeleted: 2, ok: 1 })
288
+ assert.strict.deepStrictEqual(ids(await wo.select()), ['a3', 'a4', 'a5', 'a6'])
289
+ }
290
+ assert.strict.equal(n, 1, 'ownKeys should be read once')
291
+ })
292
+ }
293
+
294
+ //運算子物件內之值亦只讀一次並只用複本: {age:Proxy({$gt:20})}, $gt第1次被讀取回20、其後回100(若改讀原物件, 命中集合改變)
295
+ for (let fn of ['select', 'delAll']) {
296
+ it(`should read the value in operator object once for ${fn}`, async function() {
297
+ await reset(wo)
298
+ let n = 0
299
+ let val = () => (++n === 1 ? 20 : 100)
300
+ let op = new Proxy({ $gt: 20 }, {
301
+ getOwnPropertyDescriptor(t, k) {
302
+ let d = Reflect.getOwnPropertyDescriptor(t, k)
303
+ return k === '$gt' ? { ...d, value: val() } : d
304
+ },
305
+ get(t, k, r) {
306
+ return k === '$gt' ? val() : Reflect.get(t, k, r)
307
+ },
308
+ })
309
+ if (fn === 'select') {
310
+ assert.strict.deepStrictEqual(ids(await wo.select({ age: op })), ['a2', 'a3', 'a5'])
311
+ }
312
+ else {
313
+ assert.strict.deepStrictEqual(await wo.delAll({ age: op }), { n: 3, nDeleted: 3, ok: 1 })
314
+ assert.strict.deepStrictEqual(ids(await wo.select()), ['a1', 'a4', 'a6'])
315
+ }
316
+ assert.strict.equal(n, 1, 'value should be read once')
317
+ })
318
+ }
319
+
320
+ //呼叫端的條件物件不被改動: 複本內之Date轉為ISO字串不得波及呼叫端之Date
321
+ it('should not modify the find object of caller', async function() {
322
+ await reset(wo)
323
+ let find = { $and: [{ g: 'A' }, { age: { $in: [20, 30] } }], d: new Date(Date.UTC(2026, 0, 1)) }
324
+ let snap = JSON.stringify(find)
325
+ let d0 = find.d.getTime()
326
+ let r = await wo.select(find)
327
+ assert.strict.deepStrictEqual(ids(r), ['a1'])
328
+ await wo.delAll(find)
329
+ assert.strict.equal(JSON.stringify(find), snap)
330
+ assert.strict.ok(find.d instanceof Date)
331
+ assert.strict.equal(find.d.getTime(), d0)
332
+ assert.strict.ok(Object.getPrototypeOf(find) === Object.prototype)
333
+ })
334
+
335
+ //select與delAll對同一條件之判定一致(逐案已驗, 此處以非空巢狀條件再驗一次)
336
+ it('should delAll exactly what select returns for nested condition', async function() {
337
+ await reset(wo)
338
+ let find = { $or: [{ $and: [{ g: 'B' }, { age: { $gte: 40 } }] }, { id: { $in: ['a1'] } }] }
339
+ let sel = ids(await wo.select(find))
340
+ assert.strict.deepStrictEqual(sel, ['a1', 'a3', 'a5'])
341
+ let r = await wo.delAll(find)
342
+ assert.strict.deepStrictEqual(r, { n: 3, nDeleted: 3, ok: 1 })
343
+ assert.strict.deepStrictEqual(ids(await wo.select()), ['a2', 'a4', 'a6'])
344
+ })
345
+
346
+ })
347
+
348
+ //欄名同於Object.prototype成員(WL-F2: constructor、toString、hasOwnProperty、valueOf等為白名單內之合法欄名):
349
+ //數據無此自有欄位者須與一般不存在之欄位同為缺欄(null語意), 不得取到繼承之成員(修正前mingo取到函數, {constructor:null}無命中、{constructor:{$ne:null}}命中全部而delAll刪光)
350
+ describe('protoname', function() {
351
+ let wo = null
352
+
353
+ //resetP, 種子6列(皆無自有constructor、toString等欄)另加p1(自有constructor與toString欄)
354
+ let resetP = async() => {
355
+ await reset(wo)
356
+ await wo.insert({ id: 'p1', constructor: 'own', toString: 'ts' })
357
+ }
358
+ let A6 = ['a1', 'a2', 'a3', 'a4', 'a5', 'a6']
359
+
360
+ before(async function() {
361
+ wo = mk('proto').wo
362
+ })
363
+
364
+ let finds = [
365
+ { name: '{constructor:null}', find: () => ({ constructor: null }), ids: A6 },
366
+ { name: '{constructor:{$eq:null}}', find: () => ({ constructor: { $eq: null } }), ids: A6 },
367
+ { name: '{constructor:{$ne:null}}', find: () => ({ constructor: { $ne: null } }), ids: ['p1'] },
368
+ { name: '{constructor:"own"}', find: () => ({ constructor: 'own' }), ids: ['p1'] },
369
+ { name: '{toString:{$ne:null}}', find: () => ({ toString: { $ne: null } }), ids: ['p1'] },
370
+ { name: '{toString:null}', find: () => ({ toString: null }), ids: A6 },
371
+ { name: '{hasOwnProperty:{$ne:null}}', find: () => ({ hasOwnProperty: { $ne: null } }), ids: [] },
372
+ { name: '{valueOf:null}', find: () => ({ valueOf: null }), ids: [...A6, 'p1'] },
373
+ { name: '{valueOf:{$in:["x"]}}', find: () => ({ valueOf: { $in: ['x'] } }), ids: [] },
374
+ { name: '{valueOf:{$nin:["x"]}}', find: () => ({ valueOf: { $nin: ['x'] } }), ids: [...A6, 'p1'] },
375
+ { name: '$or', find: () => ({ $or: [{ constructor: 'own' }, { id: 'a1' }] }), ids: ['a1', 'p1'] },
376
+ { name: '$and', find: () => ({ $and: [{ toString: null }, { id: 'a2' }] }), ids: ['a2'] },
377
+ { name: 'nested $or in $and', find: () => ({ $and: [{ $or: [{ hasOwnProperty: { $ne: null } }, { id: 'a3' }] }] }), ids: ['a3'] },
378
+ { name: 'with ordinary field', find: () => ({ g: 'A', constructor: null }), ids: ['a1', 'a2'] },
379
+ ]
380
+ for (let f of finds) {
381
+ it(`should select and delAll by ${f.name} as missing-field semantics`, async function() {
382
+ await resetP()
383
+ assert.strict.deepStrictEqual(ids(await wo.select(f.find())), f.ids)
384
+ await resetP()
385
+ let r = await wo.delAll(f.find())
386
+ assert.strict.deepStrictEqual(r, { n: f.ids.length, nDeleted: f.ids.length, ok: 1 })
387
+ let rest = ids(await wo.select())
388
+ assert.strict.deepStrictEqual([...A6, 'p1'].filter((id) => rest.indexOf(id) < 0), f.ids)
389
+ })
390
+ }
391
+
392
+ //對照組: 一般不存在之欄位(全部列皆缺欄)之語意, 原型成員欄名須與之同(數據無自有欄位者皆缺欄)
393
+ it('should get the same result for an ordinary missing field', async function() {
394
+ await resetP()
395
+ assert.strict.deepStrictEqual(ids(await wo.select({ nocolumn: null })), [...A6, 'p1'])
396
+ assert.strict.deepStrictEqual(ids(await wo.select({ nocolumn: { $ne: null } })), [])
397
+ assert.strict.deepStrictEqual(ids(await wo.select({ nocolumn: { $nin: ['x'] } })), [...A6, 'p1'])
398
+ assert.strict.deepStrictEqual(ids(await wo.select({ nocolumn: { $in: ['x'] } })), [])
399
+ })
400
+
401
+ })
402
+
403
+ //Date(WL-S3): 數據以JSON存檔(Date成ISO字串), 條件內之Date由本套件於轉譯層轉為ISO字串, 於各位置皆須命中
404
+ describe('date', function() {
405
+ let wo = null
406
+ let D1 = () => new Date(Date.UTC(2026, 0, 1))
407
+ let D2 = () => new Date(Date.UTC(2026, 1, 1))
408
+
409
+ before(async function() {
410
+ wo = mk('date').wo
411
+ await reset(wo)
412
+ })
413
+
414
+ it('should get ISO string of Date in stored rows', async function() {
415
+ await reset(wo)
416
+ let a1 = (await wo.selectByPk('a1'))
417
+ assert.strict.equal(a1.d, '2026-01-01T00:00:00.000Z')
418
+ })
419
+
420
+ let finds = [
421
+ { name: '$eq', find: () => ({ d: { $eq: D1() } }), ids: ['a1'] },
422
+ { name: '$ne', find: () => ({ d: { $ne: D1() } }), ids: ['a2', 'a3', 'a4', 'a5', 'a6'] },
423
+ { name: '$gt', find: () => ({ d: { $gt: D1() } }), ids: ['a2'] },
424
+ { name: '$lt', find: () => ({ d: { $lt: D2() } }), ids: ['a1'] },
425
+ { name: '$lte', find: () => ({ d: { $lte: D2() } }), ids: ['a1', 'a2'] },
426
+ { name: '$in', find: () => ({ d: { $in: [D2()] } }), ids: ['a2'] },
427
+ { name: '$nin', find: () => ({ d: { $nin: [D1(), D2()] } }), ids: ['a3', 'a4', 'a5', 'a6'] },
428
+ { name: '$or', find: () => ({ $or: [{ d: D2() }, { id: 'a3' }] }), ids: ['a2', 'a3'] },
429
+ { name: '$and', find: () => ({ $and: [{ d: { $gte: D1() } }, { d: { $lte: D1() } }] }), ids: ['a1'] },
430
+ { name: 'range', find: () => ({ d: { $gte: D1(), $lte: D2() } }), ids: ['a1', 'a2'] },
431
+ ]
432
+ for (let f of finds) {
433
+ it(`should select and delAll by Date in ${f.name}`, async function() {
434
+ await reset(wo)
435
+ assert.strict.deepStrictEqual(ids(await wo.select(f.find())), f.ids)
436
+ let r = await wo.delAll(f.find())
437
+ assert.strict.deepStrictEqual(r, { n: f.ids.length, nDeleted: f.ids.length, ok: 1 })
438
+ let rest = ids(await wo.select())
439
+ assert.strict.deepStrictEqual(['a1', 'a2', 'a3', 'a4', 'a5', 'a6'].filter((id) => rest.indexOf(id) < 0), f.ids)
440
+ })
441
+ }
442
+
443
+ })
444
+
445
+ //時序(find檢核置於serialize佇列之外且先於讀取)
446
+ describe('order', function() {
447
+ let bad = () => ({ $where: () => true })
448
+
449
+ //serialize佇列: 壞條件於佇列之外立即reject, 不等候先前已排入佇列之操作, 且不使其後之呼叫失敗
450
+ it('should reject bad find before queued operations finish and keep queue working', async function() {
451
+ let { wo } = mk('queue')
452
+ let ob = mkObserver(wo)
453
+ await reset(wo)
454
+ let rows = []
455
+ for (let i = 0; i < 2000; i++) {
456
+ rows.push({ id: `q${i}`, g: 'Q', v: i })
457
+ }
458
+ for (let fn of ['select', 'delAll']) {
459
+ let done = false
460
+ let pmBig = wo.insertBulk(rows.map((v) => ({ ...v, id: `${fn}-${v.id}` }))).then(function(r) {
461
+ done = true
462
+ return r
463
+ })
464
+ let out = await ob.invoke(fn, bad(), () => done)
465
+ chkRej(out, fn)
466
+ assert.strict.equal(out.probe, false, `${fn} should reject before the queued insertBulk finishes`)
467
+ let rBulk = await pmBig
468
+ assert.strict.equal(rBulk.nInserted, 2000)
469
+ }
470
+ //佇列不受影響
471
+ assert.strict.deepStrictEqual(ids(await wo.select({ id: 'a1' })), ['a1'])
472
+ let r = await wo.delAll({ g: 'Q' })
473
+ assert.strict.deepStrictEqual(r, { n: 4000, nDeleted: 4000, ok: 1 })
474
+ assert.strict.deepStrictEqual(ids(await wo.select()), ['a1', 'a2', 'a3', 'a4', 'a5', 'a6'])
475
+ })
476
+
477
+ //find檢核先於讀取: 資料庫檔案無法解析(任何讀取皆拋錯)時, 壞條件仍須回invalid find(未讀取), 合法條件則因讀取而回解析錯誤
478
+ it('should check find before reading the database file', async function() {
479
+ let { url, wo } = mk('unreadable')
480
+ let ob = mkObserver(wo)
481
+ fs.writeFileSync(url, '{ not a json')
482
+
483
+ //對照: 合法條件須讀取資料庫檔案, 因解析錯誤而reject, 且訊息不是invalid find
484
+ for (let fn of ['select', 'delAll']) {
485
+ let out = await ob.invoke(fn, { g: 'A' })
486
+ assert.strict.equal(out.ok, false, `${fn} with valid find should reject by unreadable file`)
487
+ assert.strict.ok(out.err.message.indexOf('invalid find') < 0, `${fn} valid find should not get invalid find: ${out.err.message}`)
488
+ }
489
+
490
+ //壞條件: 於讀取之前reject
491
+ for (let fn of ['select', 'delAll']) {
492
+ for (let f of [bad(), null, [], { g: undefined }, { $and: [] }]) {
493
+ let out = await ob.invoke(fn, f)
494
+ chkRej(out, fn)
495
+ }
496
+ }
497
+ })
498
+
499
+ //壞條件之delAll不得改寫資料庫檔案(內容與修改時間皆不變)
500
+ it('should not rewrite the database file when delAll is rejected', async function() {
501
+ let { url, wo } = mk('norewrite')
502
+ let ob = mkObserver(wo)
503
+ await reset(wo)
504
+ let txt0 = fs.readFileSync(url, 'utf8')
505
+ let mt0 = fs.statSync(url).mtimeMs
506
+ await tick()
507
+ for (let f of [bad(), null, [], { g: undefined }, { $nin: [] }, { $or: [{}] }]) {
508
+ let out = await ob.invoke('delAll', f)
509
+ chkRej(out, 'delAll')
510
+ }
511
+ assert.strict.equal(fs.readFileSync(url, 'utf8'), txt0)
512
+ assert.strict.equal(fs.statSync(url).mtimeMs, mt0)
513
+ })
514
+
515
+ //useCache: 快取已填入時壞條件仍須reject, 快取與資料不受影響
516
+ it('should reject bad find with filled cache and keep data', async function() {
517
+ let { wo } = mk('cache', { useCache: true })
518
+ let ob = mkObserver(wo)
519
+ await wo.insertBulk(seed())
520
+ assert.strict.equal((await wo.select()).length, 6)
521
+ for (let fn of ['select', 'delAll']) {
522
+ for (let f of [bad(), null, [], { g: undefined }, { g: { $nin: [] } }]) {
523
+ let out = await ob.invoke(fn, f)
524
+ chkRej(out, fn, directMsg(f))
525
+ }
526
+ }
527
+ assert.strict.deepStrictEqual(ids(await wo.select()), ['a1', 'a2', 'a3', 'a4', 'a5', 'a6'])
528
+ assert.strict.deepStrictEqual(ids(await wo.select({ g: 'A' })), ['a1', 'a2'])
529
+ })
530
+
531
+ })
532
+
533
+ })