w-orm-lowdb 1.0.31 → 1.0.33

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,999 @@
1
+ import assert from 'assert'
2
+ import vm from 'vm'
3
+ import fs from 'fs'
4
+ import w from 'wsemi'
5
+ import cvCases from 'wsemi/src/_cvOrmFindCases.mjs'
6
+ import cvOrmFind from 'wsemi/src/cvOrmFind.mjs'
7
+ import WOrm from '../src/WOrmLowdb.mjs'
8
+
9
+
10
+ //條件(find)之系列白名單(帳本R-FIND-01; 規則: C:\opensrcOper\dspec\w-orm系列條件語法支援盤點.md〈一之三〉)
11
+ //共用檢核為wsemi之cvOrmFind, 契約案例表為wsemi之_cvOrmFindCases(各w-orm-*逐案對公開select與delAll執行, 每案以seed()重建),
12
+ //本檔驗證select與delAll確實於入口(serialize佇列之外)呼叫並只使用檢核後之複本, 而未被後端(mingo)略過
13
+ //lowdb屬MongoDB式族, 命中集合取案例之ids.mongo; 數據以JSON存檔, 故條件內之Date由本套件轉為ISO字串後方能命中(E05 C09 I17);
14
+ //本套件之$in上限即共用預設(10000), 故案例之need不使本套件reject
15
+ describe('find-whitelist', function() {
16
+ let { seed, cases } = cvCases
17
+ let dir = './test/_tmp/find-whitelist'
18
+ let dbName = 'worm'
19
+
20
+ //ids, 取id集合(升冪)
21
+ let ids = (rs) => rs.map((v) => v.id).sort()
22
+
23
+ //tick, 讓出一個macrotask, 令延遲發出之事件有機會發出
24
+ let tick = () => new Promise((resolve) => setTimeout(resolve, 0))
25
+
26
+ //directMsg, 共用檢核對同一條件直接給出之訊息(後端須原樣給出, 不得改寫、包裝或另立訊息)
27
+ let directMsg = (f) => {
28
+ try {
29
+ cvOrmFind(f)
30
+ }
31
+ catch (err) {
32
+ return err.message
33
+ }
34
+ return null
35
+ }
36
+
37
+ //mkObserver, 建立呼叫觀測器: 事件記錄於evs, 每筆error事件附帶發出當下呼叫端之await是否已恢復(settledAtEmit, 即呼叫端可觀察之先後)
38
+ let mkObserver = (wo) => {
39
+ let evs = []
40
+ let cur = { settled: false }
41
+ wo.on('error', function(mode, data, err) {
42
+ evs.push({ t: 'error', mode, data, err, settledAtEmit: cur.settled })
43
+ })
44
+ wo.on('change', function(mode) {
45
+ evs.push({ t: 'change', mode })
46
+ })
47
+
48
+ //invoke, 呼叫wo[fn](arg)並回傳{ ok, val, err, nAtSettle, probe, evs }, nAtSettle為結果定案當下之事件數,
49
+ //probe為結果定案當下(同一microtask鏈內, 尚未讓出macrotask)呼叫probeFn()之回傳, evs為再讓出一個macrotask後之事件
50
+ let invoke = async(fn, arg, probeFn = null) => {
51
+ evs.length = 0
52
+ cur = { settled: false }
53
+ let out = { ok: false, val: null, err: null, nAtSettle: 0, probe: null, evs: [] }
54
+ try {
55
+ out.val = await wo[fn](arg)
56
+ out.ok = true
57
+ }
58
+ catch (err) {
59
+ out.err = err
60
+ }
61
+ cur.settled = true
62
+ out.nAtSettle = evs.length
63
+ out.probe = probeFn ? probeFn() : null
64
+ await tick()
65
+ out.evs = evs.slice()
66
+ return out
67
+ }
68
+
69
+ //clear
70
+ let clear = () => {
71
+ evs.length = 0
72
+ }
73
+
74
+ return { invoke, clear }
75
+ }
76
+
77
+ //mk, 於測試資料夾下建立資料庫檔(url為檔案路徑)
78
+ let mk = (name, opt = {}) => {
79
+ let url = `${dir}/${name}.json`
80
+ w.fsDeleteFile(url)
81
+ return { url, wo: WOrm({ url, db: dbName, cl: name, ...opt }) }
82
+ }
83
+
84
+ //reset, 清空後以seed()重建
85
+ let reset = async(wo) => {
86
+ await wo.delAll()
87
+ await wo.insertBulk(seed())
88
+ }
89
+
90
+ //sortRows
91
+ let sortRows = (rows) => rows.sort((a, b) => (a.id < b.id ? -1 : 1))
92
+
93
+ //chkRej, 整批reject之共同斷言(WL-R1、WL-R3): 訊息以invalid find開頭且逐字等於共用檢核給出之訊息(expectMsg), error事件於reject前恰發出一次(訊息同reject), change不發
94
+ let chkRej = (out, fn, expectMsg = null) => {
95
+ assert.strict.equal(out.ok, false, `${fn} should reject`)
96
+ assert.strict.ok(out.err instanceof Error, `${fn} should reject with Error`)
97
+ assert.strict.ok(out.err.message.indexOf('invalid find: ') === 0, `${fn} message should start with 'invalid find: ' but got: ${out.err.message}`)
98
+ if (expectMsg !== null) {
99
+ assert.strict.equal(out.err.message, expectMsg, `${fn} message should be the one of cvOrmFind`)
100
+ }
101
+ let evErr = out.evs.filter((v) => v.t === 'error')
102
+ let evChg = out.evs.filter((v) => v.t === 'change' && v.mode === fn) //只計本函數之change(佇列測試中同時排入之insertBulk會發出自己的change)
103
+ assert.strict.equal(evErr.length, 1, `${fn} should emit error exactly once`)
104
+ assert.strict.equal(out.nAtSettle, 1, `${fn} should emit error before reject`)
105
+ assert.strict.equal(evErr[0].settledAtEmit, false, `${fn} should emit error before reject`)
106
+ assert.strict.equal(evErr[0].mode, fn)
107
+ assert.strict.equal(evErr[0].data, null)
108
+ assert.strict.equal(evErr[0].err, out.err.message, `${fn} error event should carry the reject message`)
109
+ assert.strict.equal(evChg.length, 0, `${fn} should not emit change`)
110
+ }
111
+
112
+ before(function() {
113
+ w.fsDeleteFolder(dir)
114
+ fs.mkdirSync(dir, { recursive: true })
115
+ })
116
+
117
+ after(function() {
118
+ w.fsDeleteFolder(dir)
119
+ })
120
+
121
+ describe('cases', function() {
122
+ let wo = null
123
+ let ob = null
124
+ let seedIds = ids(seed())
125
+ let seedRows = null
126
+
127
+ before(async function() {
128
+ wo = mk('cases').wo
129
+ ob = mkObserver(wo)
130
+ await reset(wo)
131
+ seedRows = sortRows(await wo.select())
132
+ })
133
+
134
+ //案例表本身: 防案例表被替換為空表或缺組別而使下列逐案全綠成為假象
135
+ it('should get nonempty case table covering all groups with seed of 6 rows', function() {
136
+ let groups = [...new Set(cases.map((c) => c.group))].sort().join('')
137
+ assert.strict.equal(groups, 'CEFHILNTVX')
138
+ assert.strict.ok(cases.length >= 241)
139
+ assert.strict.deepStrictEqual(seedIds, ['a1', 'a2', 'a3', 'a4', 'a5', 'a6'])
140
+ })
141
+
142
+ for (let c of cases) {
143
+
144
+ if (c.expect === 'REJ') {
145
+
146
+ //REJ案: select與delAll皆整批reject, 資料表不變(WL-02、WL-07等; 對應案例表expect=REJ)
147
+ it(`should reject select and delAll without any change for ${c.id} ${c.label}`, async function() {
148
+ await reset(wo)
149
+ ob.clear()
150
+
151
+ let expectMsg = directMsg(c.find())
152
+ assert.strict.notEqual(expectMsg, null, 'cvOrmFind should reject the case')
153
+ let outS = await ob.invoke('select', c.find())
154
+ chkRej(outS, 'select', expectMsg)
155
+ let outD = await ob.invoke('delAll', c.find())
156
+ chkRej(outD, 'delAll', expectMsg)
157
+
158
+ //資料不變
159
+ assert.strict.deepStrictEqual(sortRows(await wo.select()), seedRows)
160
+ })
161
+
162
+ }
163
+ else {
164
+
165
+ //ALL與OK案: select命中ids.mongo; delAll刪除之集合等於select命中之集合(系列最低要求①), n與nDeleted為實際刪除筆數
166
+ it(`should select ${JSON.stringify(c.ids.mongo)} and delAll the same set for ${c.id} ${c.label}`, async function() {
167
+ await reset(wo)
168
+ ob.clear()
169
+
170
+ let outS = await ob.invoke('select', c.find())
171
+ assert.strict.equal(outS.ok, true, `select should resolve but got: ${outS.err}`)
172
+ let sel = ids(outS.val)
173
+ assert.strict.deepStrictEqual(sel, c.ids.mongo)
174
+ assert.strict.equal(outS.evs.filter((v) => v.t === 'error').length, 0)
175
+
176
+ await reset(wo)
177
+ ob.clear()
178
+ let outD = await ob.invoke('delAll', c.find())
179
+ assert.strict.equal(outD.ok, true, `delAll should resolve but got: ${outD.err}`)
180
+ assert.strict.deepStrictEqual(outD.val, { n: sel.length, nDeleted: sel.length, ok: 1 })
181
+ assert.strict.equal(outD.evs.filter((v) => v.t === 'error').length, 0)
182
+
183
+ let rest = ids(await wo.select())
184
+ let deleted = seedIds.filter((id) => rest.indexOf(id) < 0)
185
+ assert.strict.deepStrictEqual(deleted, sel)
186
+ })
187
+
188
+ }
189
+
190
+ }
191
+
192
+ })
193
+
194
+ //檢查之值即使用之值(WL-H4): 檢核回傳之複本即後續查詢與刪除所用之條件, 不得再讀呼叫端之原物件
195
+ //以Proxy令欄位g於第1次讀取回'A'(檢核所讀)、其後回'B', 若後端改讀原物件(mingo以[]取值)則命中集合改變
196
+ describe('copy', function() {
197
+ let wo = null
198
+
199
+ before(async function() {
200
+ wo = mk('copy').wo
201
+ await reset(wo)
202
+ })
203
+
204
+ //mkFlip, 欄位g第1次被讀取(不論以屬性描述子或get)回'A', 其後回'B'; 同時回傳讀取次數
205
+ let mkFlip = () => {
206
+ let st = { n: 0 }
207
+ let val = () => {
208
+ st.n++
209
+ return st.n === 1 ? 'A' : 'B'
210
+ }
211
+ let px = new Proxy({ g: 'A' }, {
212
+ getOwnPropertyDescriptor(t, k) {
213
+ let d = Reflect.getOwnPropertyDescriptor(t, k)
214
+ if (k === 'g') {
215
+ d = { ...d, value: val() }
216
+ }
217
+ return d
218
+ },
219
+ get(t, k, r) {
220
+ if (k === 'g') {
221
+ return val()
222
+ }
223
+ return Reflect.get(t, k, r)
224
+ },
225
+ })
226
+ return { px, st }
227
+ }
228
+
229
+ let shapes = {
230
+ top: (px) => px,
231
+ nested: (px) => ({ $or: [px] }),
232
+ }
233
+
234
+ for (let name of Object.keys(shapes)) {
235
+
236
+ it(`should select by the checked value only for ${name} condition`, async function() {
237
+ await reset(wo)
238
+ let { px, st } = mkFlip()
239
+ let r = await wo.select(shapes[name](px))
240
+ assert.strict.deepStrictEqual(ids(r), ['a1', 'a2'])
241
+ assert.strict.equal(st.n, 1, 'value should be read once')
242
+ })
243
+
244
+ it(`should delAll by the checked value only for ${name} condition`, async function() {
245
+ await reset(wo)
246
+ let { px, st } = mkFlip()
247
+ let r = await wo.delAll(shapes[name](px))
248
+ assert.strict.deepStrictEqual(r, { n: 2, nDeleted: 2, ok: 1 })
249
+ assert.strict.deepStrictEqual(ids(await wo.select()), ['a3', 'a4', 'a5', 'a6'])
250
+ assert.strict.equal(st.n, 1, 'value should be read once')
251
+ })
252
+
253
+ }
254
+
255
+ //熱快取(useCache已填入)時亦只用檢核後之複本: 原快取測試只以壞條件與穩定條件驗快取, 守不住「合法條件改讀原物件」
256
+ for (let fn of ['select', 'delAll']) {
257
+ it(`should use the checked copy only with warm cache for ${fn}`, async function() {
258
+ let { wo: wc } = mk(`warm${fn}`, { useCache: true })
259
+ await wc.insertBulk(seed())
260
+ assert.strict.equal((await wc.select()).length, 6) //填入快取
261
+ let { px, st } = mkFlip()
262
+ if (fn === 'select') {
263
+ assert.strict.deepStrictEqual(ids(await wc.select(px)), ['a1', 'a2'])
264
+ }
265
+ else {
266
+ assert.strict.deepStrictEqual(await wc.delAll(px), { n: 2, nDeleted: 2, ok: 1 })
267
+ assert.strict.deepStrictEqual(ids(await wc.select()), ['a3', 'a4', 'a5', 'a6'])
268
+ }
269
+ assert.strict.equal(st.n, 1, 'value should be read once')
270
+ })
271
+ }
272
+
273
+ //「是否有條件」之判斷亦須依檢核後之複本: ownKeys於第1次回['g']、其後回[](若改讀原物件, select會回全部、delAll會刪光)
274
+ for (let fn of ['select', 'delAll']) {
275
+ it(`should decide the empty condition by the checked copy only for ${fn}`, async function() {
276
+ await reset(wo)
277
+ let n = 0
278
+ let px = new Proxy({ g: 'A' }, {
279
+ ownKeys() {
280
+ n++
281
+ return n === 1 ? ['g'] : []
282
+ },
283
+ })
284
+ if (fn === 'select') {
285
+ assert.strict.deepStrictEqual(ids(await wo.select(px)), ['a1', 'a2'])
286
+ }
287
+ else {
288
+ assert.strict.deepStrictEqual(await wo.delAll(px), { n: 2, nDeleted: 2, ok: 1 })
289
+ assert.strict.deepStrictEqual(ids(await wo.select()), ['a3', 'a4', 'a5', 'a6'])
290
+ }
291
+ assert.strict.equal(n, 1, 'ownKeys should be read once')
292
+ })
293
+ }
294
+
295
+ //運算子物件內之值亦只讀一次並只用複本: {age:Proxy({$gt:20})}, $gt第1次被讀取回20、其後回100(若改讀原物件, 命中集合改變)
296
+ for (let fn of ['select', 'delAll']) {
297
+ it(`should read the value in operator object once for ${fn}`, async function() {
298
+ await reset(wo)
299
+ let n = 0
300
+ let val = () => (++n === 1 ? 20 : 100)
301
+ let op = new Proxy({ $gt: 20 }, {
302
+ getOwnPropertyDescriptor(t, k) {
303
+ let d = Reflect.getOwnPropertyDescriptor(t, k)
304
+ return k === '$gt' ? { ...d, value: val() } : d
305
+ },
306
+ get(t, k, r) {
307
+ return k === '$gt' ? val() : Reflect.get(t, k, r)
308
+ },
309
+ })
310
+ if (fn === 'select') {
311
+ assert.strict.deepStrictEqual(ids(await wo.select({ age: op })), ['a2', 'a3', 'a5'])
312
+ }
313
+ else {
314
+ assert.strict.deepStrictEqual(await wo.delAll({ age: op }), { n: 3, nDeleted: 3, ok: 1 })
315
+ assert.strict.deepStrictEqual(ids(await wo.select()), ['a1', 'a4', 'a6'])
316
+ }
317
+ assert.strict.equal(n, 1, 'value should be read once')
318
+ })
319
+ }
320
+
321
+ //呼叫端的條件物件不被改動: 複本內之Date轉為ISO字串不得波及呼叫端之Date
322
+ it('should not modify the find object of caller', async function() {
323
+ await reset(wo)
324
+ let find = { $and: [{ g: 'A' }, { age: { $in: [20, 30] } }], d: new Date(Date.UTC(2026, 0, 1)) }
325
+ let snap = JSON.stringify(find)
326
+ let d0 = find.d.getTime()
327
+ let r = await wo.select(find)
328
+ assert.strict.deepStrictEqual(ids(r), ['a1'])
329
+ await wo.delAll(find)
330
+ assert.strict.equal(JSON.stringify(find), snap)
331
+ assert.strict.ok(find.d instanceof Date)
332
+ assert.strict.equal(find.d.getTime(), d0)
333
+ assert.strict.ok(Object.getPrototypeOf(find) === Object.prototype)
334
+ })
335
+
336
+ //select與delAll對同一條件之判定一致(逐案已驗, 此處以非空巢狀條件再驗一次)
337
+ it('should delAll exactly what select returns for nested condition', async function() {
338
+ await reset(wo)
339
+ let find = { $or: [{ $and: [{ g: 'B' }, { age: { $gte: 40 } }] }, { id: { $in: ['a1'] } }] }
340
+ let sel = ids(await wo.select(find))
341
+ assert.strict.deepStrictEqual(sel, ['a1', 'a3', 'a5'])
342
+ let r = await wo.delAll(find)
343
+ assert.strict.deepStrictEqual(r, { n: 3, nDeleted: 3, ok: 1 })
344
+ assert.strict.deepStrictEqual(ids(await wo.select()), ['a2', 'a4', 'a6'])
345
+ })
346
+
347
+ })
348
+
349
+ //欄名同於Object.prototype成員(WL-F2: constructor、toString、hasOwnProperty、valueOf等為白名單內之合法欄名):
350
+ //數據無此自有欄位者須與一般不存在之欄位同為缺欄(null語意), 不得取到繼承之成員(修正前mingo取到函數, {constructor:null}無命中、{constructor:{$ne:null}}命中全部而delAll刪光)
351
+ describe('protoname', function() {
352
+ let wo = null
353
+
354
+ //resetP, 種子6列(皆無自有constructor、toString等欄)另加p1(自有constructor與toString欄)
355
+ let resetP = async() => {
356
+ await reset(wo)
357
+ await wo.insert({ id: 'p1', constructor: 'own', toString: 'ts' })
358
+ }
359
+ let A6 = ['a1', 'a2', 'a3', 'a4', 'a5', 'a6']
360
+
361
+ before(async function() {
362
+ wo = mk('proto').wo
363
+ })
364
+
365
+ let finds = [
366
+ { name: '{constructor:null}', find: () => ({ constructor: null }), ids: A6 },
367
+ { name: '{constructor:{$eq:null}}', find: () => ({ constructor: { $eq: null } }), ids: A6 },
368
+ { name: '{constructor:{$ne:null}}', find: () => ({ constructor: { $ne: null } }), ids: ['p1'] },
369
+ { name: '{constructor:"own"}', find: () => ({ constructor: 'own' }), ids: ['p1'] },
370
+ { name: '{toString:{$ne:null}}', find: () => ({ toString: { $ne: null } }), ids: ['p1'] },
371
+ { name: '{toString:null}', find: () => ({ toString: null }), ids: A6 },
372
+ { name: '{hasOwnProperty:{$ne:null}}', find: () => ({ hasOwnProperty: { $ne: null } }), ids: [] },
373
+ { name: '{valueOf:null}', find: () => ({ valueOf: null }), ids: [...A6, 'p1'] },
374
+ { name: '{valueOf:{$in:["x"]}}', find: () => ({ valueOf: { $in: ['x'] } }), ids: [] },
375
+ { name: '{valueOf:{$nin:["x"]}}', find: () => ({ valueOf: { $nin: ['x'] } }), ids: [...A6, 'p1'] },
376
+ { name: '$or', find: () => ({ $or: [{ constructor: 'own' }, { id: 'a1' }] }), ids: ['a1', 'p1'] },
377
+ { name: '$and', find: () => ({ $and: [{ toString: null }, { id: 'a2' }] }), ids: ['a2'] },
378
+ { name: 'nested $or in $and', find: () => ({ $and: [{ $or: [{ hasOwnProperty: { $ne: null } }, { id: 'a3' }] }] }), ids: ['a3'] },
379
+ { name: 'with ordinary field', find: () => ({ g: 'A', constructor: null }), ids: ['a1', 'a2'] },
380
+ ]
381
+ for (let f of finds) {
382
+ it(`should select and delAll by ${f.name} as missing-field semantics`, async function() {
383
+ await resetP()
384
+ assert.strict.deepStrictEqual(ids(await wo.select(f.find())), f.ids)
385
+ await resetP()
386
+ let r = await wo.delAll(f.find())
387
+ assert.strict.deepStrictEqual(r, { n: f.ids.length, nDeleted: f.ids.length, ok: 1 })
388
+ let rest = ids(await wo.select())
389
+ assert.strict.deepStrictEqual([...A6, 'p1'].filter((id) => rest.indexOf(id) < 0), f.ids)
390
+ })
391
+ }
392
+
393
+ //對照組: 一般不存在之欄位(全部列皆缺欄)之語意, 原型成員欄名須與之同(數據無自有欄位者皆缺欄)
394
+ it('should get the same result for an ordinary missing field', async function() {
395
+ await resetP()
396
+ assert.strict.deepStrictEqual(ids(await wo.select({ nocolumn: null })), [...A6, 'p1'])
397
+ assert.strict.deepStrictEqual(ids(await wo.select({ nocolumn: { $ne: null } })), [])
398
+ assert.strict.deepStrictEqual(ids(await wo.select({ nocolumn: { $nin: ['x'] } })), [...A6, 'p1'])
399
+ assert.strict.deepStrictEqual(ids(await wo.select({ nocolumn: { $in: ['x'] } })), [])
400
+ })
401
+
402
+ })
403
+
404
+ //Date(WL-S3): 數據以JSON存檔(Date成ISO字串), 條件內之Date由本套件於轉譯層轉為ISO字串, 於各位置皆須命中
405
+ describe('date', function() {
406
+ let wo = null
407
+ let D1 = () => new Date(Date.UTC(2026, 0, 1))
408
+ let D2 = () => new Date(Date.UTC(2026, 1, 1))
409
+
410
+ before(async function() {
411
+ wo = mk('date').wo
412
+ await reset(wo)
413
+ })
414
+
415
+ it('should get ISO string of Date in stored rows', async function() {
416
+ await reset(wo)
417
+ let a1 = (await wo.selectByPk('a1'))
418
+ assert.strict.equal(a1.d, '2026-01-01T00:00:00.000Z')
419
+ })
420
+
421
+ let finds = [
422
+ { name: '$eq', find: () => ({ d: { $eq: D1() } }), ids: ['a1'] },
423
+ { name: '$ne', find: () => ({ d: { $ne: D1() } }), ids: ['a2', 'a3', 'a4', 'a5', 'a6'] },
424
+ { name: '$gt', find: () => ({ d: { $gt: D1() } }), ids: ['a2'] },
425
+ { name: '$lt', find: () => ({ d: { $lt: D2() } }), ids: ['a1'] },
426
+ { name: '$lte', find: () => ({ d: { $lte: D2() } }), ids: ['a1', 'a2'] },
427
+ { name: '$in', find: () => ({ d: { $in: [D2()] } }), ids: ['a2'] },
428
+ { name: '$nin', find: () => ({ d: { $nin: [D1(), D2()] } }), ids: ['a3', 'a4', 'a5', 'a6'] },
429
+ { name: '$or', find: () => ({ $or: [{ d: D2() }, { id: 'a3' }] }), ids: ['a2', 'a3'] },
430
+ { name: '$and', find: () => ({ $and: [{ d: { $gte: D1() } }, { d: { $lte: D1() } }] }), ids: ['a1'] },
431
+ { name: 'range', find: () => ({ d: { $gte: D1(), $lte: D2() } }), ids: ['a1', 'a2'] },
432
+ ]
433
+ for (let f of finds) {
434
+ it(`should select and delAll by Date in ${f.name}`, async function() {
435
+ await reset(wo)
436
+ assert.strict.deepStrictEqual(ids(await wo.select(f.find())), f.ids)
437
+ let r = await wo.delAll(f.find())
438
+ assert.strict.deepStrictEqual(r, { n: f.ids.length, nDeleted: f.ids.length, ok: 1 })
439
+ let rest = ids(await wo.select())
440
+ assert.strict.deepStrictEqual(['a1', 'a2', 'a3', 'a4', 'a5', 'a6'].filter((id) => rest.indexOf(id) < 0), f.ids)
441
+ })
442
+ }
443
+
444
+ })
445
+
446
+ //時序(find檢核置於serialize佇列之外且先於讀取)
447
+ describe('order', function() {
448
+ let bad = () => ({ $where: () => true })
449
+
450
+ //serialize佇列: 壞條件於佇列之外立即reject, 不等候先前已排入佇列之操作, 且不使其後之呼叫失敗
451
+ it('should reject bad find before queued operations finish and keep queue working', async function() {
452
+ let { wo } = mk('queue')
453
+ let ob = mkObserver(wo)
454
+ await reset(wo)
455
+ let rows = []
456
+ for (let i = 0; i < 2000; i++) {
457
+ rows.push({ id: `q${i}`, g: 'Q', v: i })
458
+ }
459
+ for (let fn of ['select', 'delAll']) {
460
+ let done = false
461
+ let pmBig = wo.insertBulk(rows.map((v) => ({ ...v, id: `${fn}-${v.id}` }))).then(function(r) {
462
+ done = true
463
+ return r
464
+ })
465
+ let out = await ob.invoke(fn, bad(), () => done)
466
+ chkRej(out, fn)
467
+ assert.strict.equal(out.probe, false, `${fn} should reject before the queued insertBulk finishes`)
468
+ let rBulk = await pmBig
469
+ assert.strict.equal(rBulk.nInserted, 2000)
470
+ }
471
+ //佇列不受影響
472
+ assert.strict.deepStrictEqual(ids(await wo.select({ id: 'a1' })), ['a1'])
473
+ let r = await wo.delAll({ g: 'Q' })
474
+ assert.strict.deepStrictEqual(r, { n: 4000, nDeleted: 4000, ok: 1 })
475
+ assert.strict.deepStrictEqual(ids(await wo.select()), ['a1', 'a2', 'a3', 'a4', 'a5', 'a6'])
476
+ })
477
+
478
+ //find檢核先於讀取: 資料庫檔案無法解析(任何讀取皆拋錯)時, 壞條件仍須回invalid find(未讀取), 合法條件則因讀取而回解析錯誤
479
+ it('should check find before reading the database file', async function() {
480
+ let { url, wo } = mk('unreadable')
481
+ let ob = mkObserver(wo)
482
+ fs.writeFileSync(url, '{ not a json')
483
+
484
+ //對照: 合法條件須讀取資料庫檔案, 因解析錯誤而reject, 且訊息不是invalid find
485
+ for (let fn of ['select', 'delAll']) {
486
+ let out = await ob.invoke(fn, { g: 'A' })
487
+ assert.strict.equal(out.ok, false, `${fn} with valid find should reject by unreadable file`)
488
+ assert.strict.ok(out.err.message.indexOf('invalid find') < 0, `${fn} valid find should not get invalid find: ${out.err.message}`)
489
+ }
490
+
491
+ //壞條件: 於讀取之前reject
492
+ for (let fn of ['select', 'delAll']) {
493
+ for (let f of [bad(), null, [], { g: undefined }, { $and: [] }]) {
494
+ let out = await ob.invoke(fn, f)
495
+ chkRej(out, fn)
496
+ }
497
+ }
498
+ })
499
+
500
+ //壞條件之delAll不得改寫資料庫檔案(內容與修改時間皆不變)
501
+ it('should not rewrite the database file when delAll is rejected', async function() {
502
+ let { url, wo } = mk('norewrite')
503
+ let ob = mkObserver(wo)
504
+ await reset(wo)
505
+ let txt0 = fs.readFileSync(url, 'utf8')
506
+ let mt0 = fs.statSync(url).mtimeMs
507
+ await tick()
508
+ for (let f of [bad(), null, [], { g: undefined }, { $nin: [] }, { $or: [{}] }]) {
509
+ let out = await ob.invoke('delAll', f)
510
+ chkRej(out, 'delAll')
511
+ }
512
+ assert.strict.equal(fs.readFileSync(url, 'utf8'), txt0)
513
+ assert.strict.equal(fs.statSync(url).mtimeMs, mt0)
514
+ })
515
+
516
+ //useCache: 快取已填入時壞條件仍須reject, 快取與資料不受影響
517
+ it('should reject bad find with filled cache and keep data', async function() {
518
+ let { wo } = mk('cache', { useCache: true })
519
+ let ob = mkObserver(wo)
520
+ await wo.insertBulk(seed())
521
+ assert.strict.equal((await wo.select()).length, 6)
522
+ for (let fn of ['select', 'delAll']) {
523
+ for (let f of [bad(), null, [], { g: undefined }, { g: { $nin: [] } }]) {
524
+ let out = await ob.invoke(fn, f)
525
+ chkRej(out, fn, directMsg(f))
526
+ }
527
+ }
528
+ assert.strict.deepStrictEqual(ids(await wo.select()), ['a1', 'a2', 'a3', 'a4', 'a5', 'a6'])
529
+ assert.strict.deepStrictEqual(ids(await wo.select({ g: 'A' })), ['a1', 'a2'])
530
+ })
531
+
532
+ })
533
+
534
+ //範圍外Date條件fail-closed(帳本R-FIND-02): 數據以JSON存檔, Date條件轉ISO字串後以字典序比較, 年份介於0000-9999時為固定24字元, 字典序即時間序;
535
+ //年份<0或>9999時toISOString()帶+yyyyyy-/-yyyyyy-前綴, 字典序與時間序顛倒, 故轉譯層對此類條件值整批reject(訊息格式同cvOrmFind, 不回顯值), select與delAll皆同, 於任何讀檔之前
536
+ describe('date-range', function() {
537
+ let MS = {
538
+ min: -62167219200000, //0000-01-01T00:00:00.000Z
539
+ max: 253402300799999, //9999-12-31T23:59:59.999Z
540
+ belowMin: -62167219200001, //-000001-12-31T23:59:59.999Z
541
+ aboveMax: 253402300800000, //+010000-01-01T00:00:00.000Z
542
+ big: 8.64e15, //+275760-09-13T00:00:00.000Z
543
+ small: -8.64e15, //-271821-04-20T00:00:00.000Z
544
+ }
545
+ let D = (ms) => new Date(ms)
546
+ let ok = () => D(Date.UTC(2026, 5, 1))
547
+ let wo = null
548
+ let ob = null
549
+ let baseRows = null
550
+
551
+ //rowsDr, 皆為範圍內之Date(r3、r4為邊界)
552
+ let rowsDr = () => [
553
+ { id: 'r1', t: D(Date.UTC(2020, 0, 1)) },
554
+ { id: 'r2', t: D(Date.UTC(2099, 0, 1)) },
555
+ { id: 'r3', t: D(MS.min) },
556
+ { id: 'r4', t: D(MS.max) },
557
+ { id: 'r5' },
558
+ ]
559
+
560
+ //resetDr
561
+ let resetDr = async(w0) => {
562
+ await w0.delAll()
563
+ await w0.insertBulk(rowsDr())
564
+ }
565
+
566
+ //expectMsg, 本層之訊息(格式同cvOrmFind: invalid find: 原因 at [路徑])
567
+ let expectMsg = (path) => `invalid find: Date year is out of range 0000-9999 at [${path}]`
568
+
569
+ before(async function() {
570
+ wo = mk('daterange').wo
571
+ ob = mkObserver(wo)
572
+ await resetDr(wo)
573
+ baseRows = sortRows(await wo.select())
574
+ })
575
+
576
+ it('should store Date at the boundaries as fixed 24-char ISO strings', async function() {
577
+ await resetDr(wo)
578
+ assert.strict.equal((await wo.selectByPk('r3')).t, '0000-01-01T00:00:00.000Z')
579
+ assert.strict.equal((await wo.selectByPk('r4')).t, '9999-12-31T23:59:59.999Z')
580
+ })
581
+
582
+ //接受: 邊界(0000-01-01T00:00:00.000Z、9999-12-31T23:59:59.999Z)內之Date, 命中集合正確且delAll刪除之集合等於select命中之集合
583
+ let accepts = [
584
+ { name: 'direct min', find: () => ({ t: D(MS.min) }), ids: ['r3'] },
585
+ { name: 'direct max', find: () => ({ t: D(MS.max) }), ids: ['r4'] },
586
+ { name: '$eq min', find: () => ({ t: { $eq: D(MS.min) } }), ids: ['r3'] },
587
+ { name: '$ne min', find: () => ({ t: { $ne: D(MS.min) } }), ids: ['r1', 'r2', 'r4', 'r5'] },
588
+ { name: '$gte min', find: () => ({ t: { $gte: D(MS.min) } }), ids: ['r1', 'r2', 'r3', 'r4'] },
589
+ { name: '$gt min', find: () => ({ t: { $gt: D(MS.min) } }), ids: ['r1', 'r2', 'r4'] },
590
+ { name: '$lt min', find: () => ({ t: { $lt: D(MS.min) } }), ids: [] },
591
+ { name: '$lte min', find: () => ({ t: { $lte: D(MS.min) } }), ids: ['r3'] },
592
+ { name: '$gt max', find: () => ({ t: { $gt: D(MS.max) } }), ids: [] },
593
+ { name: '$gte max', find: () => ({ t: { $gte: D(MS.max) } }), ids: ['r4'] },
594
+ { name: '$lt max', find: () => ({ t: { $lt: D(MS.max) } }), ids: ['r1', 'r2', 'r3'] },
595
+ { name: '$lte max', find: () => ({ t: { $lte: D(MS.max) } }), ids: ['r1', 'r2', 'r3', 'r4'] },
596
+ { name: '$in min and max', find: () => ({ t: { $in: [D(MS.min), D(MS.max)] } }), ids: ['r3', 'r4'] },
597
+ { name: '$nin min and max', find: () => ({ t: { $nin: [D(MS.min), D(MS.max)] } }), ids: ['r1', 'r2', 'r5'] },
598
+ { name: '$and from min to max', find: () => ({ $and: [{ t: { $gte: D(MS.min) } }, { t: { $lte: D(MS.max) } }] }), ids: ['r1', 'r2', 'r3', 'r4'] },
599
+ { name: '$or min or max', find: () => ({ $or: [{ t: D(MS.min) }, { t: D(MS.max) }] }), ids: ['r3', 'r4'] },
600
+ { name: '$lt now keeps in-range past rows only', find: () => ({ t: { $lt: new Date() } }), ids: ['r1', 'r3'] },
601
+ ]
602
+ for (let f of accepts) {
603
+ it(`should select and delAll the same set by Date in range for ${f.name}`, async function() {
604
+ await resetDr(wo)
605
+ ob.clear()
606
+ let outS = await ob.invoke('select', f.find())
607
+ assert.strict.equal(outS.ok, true, `select should resolve but got: ${outS.err}`)
608
+ assert.strict.deepStrictEqual(ids(outS.val), f.ids)
609
+ assert.strict.equal(outS.evs.filter((v) => v.t === 'error').length, 0)
610
+ let outD = await ob.invoke('delAll', f.find())
611
+ assert.strict.equal(outD.ok, true, `delAll should resolve but got: ${outD.err}`)
612
+ assert.strict.deepStrictEqual(outD.val, { n: f.ids.length, nDeleted: f.ids.length, ok: 1 })
613
+ assert.strict.equal(outD.evs.filter((v) => v.t === 'error').length, 0)
614
+ let rest = ids(await wo.select())
615
+ assert.strict.deepStrictEqual(['r1', 'r2', 'r3', 'r4', 'r5'].filter((id) => rest.indexOf(id) < 0), f.ids)
616
+ })
617
+ }
618
+
619
+ //接受: 範圍內但非端點之Date(端點內側1毫秒、年份交界、一般值), 以真實時間序為期望(範圍內字典序即時間序), 各運算子交叉select與delAll
620
+ let inners = [
621
+ '0000-01-01T00:00:00.001Z',
622
+ '0000-12-31T23:59:59.999Z',
623
+ '0001-01-01T00:00:00.000Z',
624
+ '0999-12-31T23:59:59.999Z',
625
+ '1970-01-01T00:00:00.000Z',
626
+ '9998-12-31T23:59:59.999Z',
627
+ '9999-01-01T00:00:00.000Z',
628
+ '9999-12-31T23:59:59.998Z',
629
+ ]
630
+ let opsInner = [
631
+ { name: '$eq', mk: (v) => ({ t: { $eq: v } }), hit: (a, b) => a === b, miss: false },
632
+ { name: 'direct', mk: (v) => ({ t: v }), hit: (a, b) => a === b, miss: false },
633
+ { name: '$ne', mk: (v) => ({ t: { $ne: v } }), hit: (a, b) => a !== b, miss: true },
634
+ { name: '$gt', mk: (v) => ({ t: { $gt: v } }), hit: (a, b) => a > b, miss: false },
635
+ { name: '$gte', mk: (v) => ({ t: { $gte: v } }), hit: (a, b) => a >= b, miss: false },
636
+ { name: '$lt', mk: (v) => ({ t: { $lt: v } }), hit: (a, b) => a < b, miss: false },
637
+ { name: '$lte', mk: (v) => ({ t: { $lte: v } }), hit: (a, b) => a <= b, miss: false },
638
+ { name: '$in', mk: (v) => ({ t: { $in: [D(MS.min), v] } }), hit: (a, b) => a === b || a === MS.min, miss: false },
639
+ { name: '$nin', mk: (v) => ({ t: { $nin: [v, D(MS.max)] } }), hit: (a, b) => a !== b && a !== MS.max, miss: true },
640
+ ]
641
+ for (let iso of inners) {
642
+ it(`should accept Date ${iso} in every operator and give the time-ordered set in select and delAll`, async function() {
643
+ let v = new Date(iso)
644
+ assert.strict.equal(v.toISOString(), iso)
645
+ for (let op of opsInner) {
646
+ let rows = [...rowsDr(), { id: 'x', t: new Date(iso) }]
647
+ let expect = rows.filter((r) => (r.t ? op.hit(r.t.getTime(), v.getTime()) : op.miss)).map((r) => r.id).sort()
648
+ for (let fn of ['select', 'delAll']) {
649
+ await wo.delAll()
650
+ await wo.insertBulk(rows)
651
+ ob.clear()
652
+ let out = await ob.invoke(fn, op.mk(new Date(iso)))
653
+ assert.strict.equal(out.ok, true, `${fn} ${op.name} should resolve but got: ${out.err}`)
654
+ assert.strict.equal(out.evs.filter((e) => e.t === 'error').length, 0)
655
+ if (fn === 'select') {
656
+ assert.strict.deepStrictEqual(ids(out.val), expect, `select ${op.name} ${iso}`)
657
+ }
658
+ else {
659
+ assert.strict.deepStrictEqual(out.val, { n: expect.length, nDeleted: expect.length, ok: 1 }, `delAll ${op.name} ${iso}`)
660
+ let rest = ids(await wo.select())
661
+ assert.strict.deepStrictEqual(rows.map((r) => r.id).sort().filter((id) => rest.indexOf(id) < 0), expect, `delAll set ${op.name} ${iso}`)
662
+ }
663
+ }
664
+ }
665
+ })
666
+ }
667
+
668
+ //leaves與chkSame: 取條件物件之全部節點(物件節點記鍵序,Date節點記毫秒,皆記參照), 供reject後確認呼叫端實際傳入之同一物件逐節點未被改動
669
+ let leaves = (v, p = 'find', out = []) => {
670
+ if (v instanceof Date) {
671
+ out.push({ p, ref: v, ms: v.getTime() })
672
+ }
673
+ else if (v !== null && typeof v === 'object') {
674
+ out.push({ p, ref: v, keys: Object.keys(v).join(',') })
675
+ for (let k of Object.keys(v)) {
676
+ leaves(v[k], `${p}.${k}`, out)
677
+ }
678
+ }
679
+ else {
680
+ out.push({ p, ref: v })
681
+ }
682
+ return out
683
+ }
684
+ let chkSame = (a, b) => {
685
+ assert.strict.equal(a.length, b.length, 'find should keep the same number of nodes')
686
+ for (let i = 0; i < a.length; i++) {
687
+ assert.strict.equal(a[i].p, b[i].p)
688
+ assert.strict.equal(a[i].ref, b[i].ref, `${a[i].p} should keep the same reference or value`)
689
+ assert.strict.equal(a[i].ms, b[i].ms, `${a[i].p} should keep the same time value`)
690
+ assert.strict.equal(a[i].keys, b[i].keys, `${a[i].p} should keep the same keys`)
691
+ }
692
+ }
693
+
694
+ //reject: 範圍外Date於各位置, select與delAll皆整批reject, 訊息含路徑且不回顯值, error事件於reject前恰一次, 不發change, 資料不變
695
+ let bads = [
696
+ { name: 'below 0000 (-000001-12-31T23:59:59.999Z)', ms: MS.belowMin, echo: ['000001'] },
697
+ { name: 'above 9999 (+010000-01-01T00:00:00.000Z)', ms: MS.aboveMax, echo: ['010000'] },
698
+ { name: 'new Date(8.64e15)', ms: MS.big, echo: ['275760'] },
699
+ { name: 'new Date(-8.64e15)', ms: MS.small, echo: ['271821'] },
700
+ ]
701
+ let positions = [
702
+ { name: 'direct', find: (d) => ({ t: d }), path: 'find.t' },
703
+ { name: 'direct in other field', find: (d) => ({ id: 'r1', u: d }), path: 'find.u' },
704
+ { name: '$eq', find: (d) => ({ t: { $eq: d } }), path: 'find.t.$eq' },
705
+ { name: '$ne', find: (d) => ({ t: { $ne: d } }), path: 'find.t.$ne' },
706
+ { name: '$gt', find: (d) => ({ t: { $gt: d } }), path: 'find.t.$gt' },
707
+ { name: '$gte', find: (d) => ({ t: { $gte: d } }), path: 'find.t.$gte' },
708
+ { name: '$lt', find: (d) => ({ t: { $lt: d } }), path: 'find.t.$lt' },
709
+ { name: '$lte', find: (d) => ({ t: { $lte: d } }), path: 'find.t.$lte' },
710
+ { name: '$in first', find: (d) => ({ t: { $in: [d, ok()] } }), path: 'find.t.$in[0]' },
711
+ { name: '$in middle', find: (d) => ({ t: { $in: [ok(), d, ok()] } }), path: 'find.t.$in[1]' },
712
+ { name: '$in last', find: (d) => ({ t: { $in: [ok(), ok(), d] } }), path: 'find.t.$in[2]' },
713
+ { name: '$nin', find: (d) => ({ t: { $nin: [ok(), d] } }), path: 'find.t.$nin[1]' },
714
+ { name: '$and', find: (d) => ({ $and: [{ id: 'r1' }, { t: { $lt: d } }] }), path: 'find.$and[1].t.$lt' },
715
+ { name: '$or', find: (d) => ({ $or: [{ t: d }, { id: 'r1' }] }), path: 'find.$or[0].t' },
716
+ { name: '$and and $or nested', find: (d) => ({ $and: [{ $or: [{ id: 'r1' }, { $and: [{ t: { $gte: ok() } }, { t: { $lte: d } }] }] }] }), path: 'find.$and[0].$or[1].$and[1].t.$lte' },
717
+ { name: 'range with valid Date sibling', find: (d) => ({ id: 'r1', t: { $gte: ok(), $lte: d } }), path: 'find.t.$lte' },
718
+ { name: 'valid Date before and after', find: (d) => ({ a: ok(), t: d, z: ok() }), path: 'find.t' },
719
+ { name: '$nin first', find: (d) => ({ t: { $nin: [d, ok()] } }), path: 'find.t.$nin[0]' },
720
+ { name: '$nin middle', find: (d) => ({ t: { $nin: [ok(), d, ok()] } }), path: 'find.t.$nin[1]' },
721
+ { name: '$nin last of three', find: (d) => ({ t: { $nin: [ok(), ok(), d] } }), path: 'find.t.$nin[2]' },
722
+ { name: '$in at two-digit index', find: (d) => ({ t: { $in: [...Array.from({ length: 10 }, () => ok()), d, ok()] } }), path: 'find.t.$in[10]' },
723
+ { name: '$and first branch', find: (d) => ({ $and: [{ t: { $lt: d } }, { id: 'r1' }] }), path: 'find.$and[0].t.$lt' },
724
+ { name: '$or last branch', find: (d) => ({ $or: [{ id: 'r1' }, { t: d }] }), path: 'find.$or[1].t' },
725
+ { name: '$or in $and in $or', find: (d) => ({ $or: [{ id: 'r2' }, { $and: [{ id: 'r1' }, { $or: [{ id: 'r3' }, { t: { $eq: d } }] }] }] }), path: 'find.$or[1].$and[1].$or[1].t.$eq' },
726
+ { name: 'range in reverse order', find: (d) => ({ id: 'r1', t: { $lte: d, $gte: ok() } }), path: 'find.t.$lte' },
727
+ { name: 'two out-of-range Date in different fields', find: (d) => ({ a: d, b: d }), path: 'find.a' },
728
+ { name: 'two out-of-range Date in one $in', find: (d) => ({ t: { $in: [ok(), d, d] } }), path: 'find.t.$in[1]' },
729
+ ]
730
+ for (let b of bads) {
731
+ for (let p of positions) {
732
+ it(`should reject select and delAll without any change for ${b.name} in ${p.name}`, async function() {
733
+ await resetDr(wo)
734
+ ob.clear()
735
+ let d = D(b.ms)
736
+ let f = p.find(d)
737
+ let msg = expectMsg(p.path)
738
+ let before = leaves(f)
739
+
740
+ //cvOrmFind本身接受(證明本檢核屬轉譯層, 非共用檢核)
741
+ assert.strict.equal(directMsg(p.find(D(b.ms))), null, 'cvOrmFind should accept the Date')
742
+
743
+ //兩次呼叫皆傳入同一個f, reject後f逐節點(參照、毫秒、鍵序)須與呼叫前相同
744
+ for (let fn of ['select', 'delAll']) {
745
+ let out = await ob.invoke(fn, f)
746
+ chkRej(out, fn, msg)
747
+ for (let s of b.echo) {
748
+ assert.strict.ok(out.err.message.indexOf(s) < 0, `${fn} message should not echo the value: ${out.err.message}`)
749
+ }
750
+ chkSame(before, leaves(f))
751
+ }
752
+ assert.strict.equal(d.getTime(), b.ms)
753
+ assert.strict.ok(d instanceof Date)
754
+
755
+ //資料不變
756
+ assert.strict.deepStrictEqual(sortRows(await wo.select()), baseRows)
757
+ })
758
+ }
759
+ }
760
+
761
+ //字典序顛倒之場景(修正前: 範圍外Date轉為+yyyyyy-/-yyyyyy-字串後, '2020-…'等較大, 故$gt哨兵命中所有帶日期之數據, delAll因而刪光)
762
+ let inverted = [
763
+ { name: '$gt new Date(8.64e15)', find: () => ({ t: { $gt: D(MS.big) } }) },
764
+ { name: '$gte new Date(8.64e15)', find: () => ({ t: { $gte: D(MS.big) } }) },
765
+ { name: '$lt new Date(8.64e15)', find: () => ({ t: { $lt: D(MS.big) } }) },
766
+ { name: '$lte new Date(8.64e15)', find: () => ({ t: { $lte: D(MS.big) } }) },
767
+ { name: '$gt year 10000', find: () => ({ t: { $gt: D(MS.aboveMax) } }) },
768
+ { name: '$lt year -1', find: () => ({ t: { $lt: D(MS.belowMin) } }) },
769
+ { name: '$ne new Date(8.64e15)', find: () => ({ t: { $ne: D(MS.big) } }) },
770
+ { name: '$nin [new Date(8.64e15)]', find: () => ({ t: { $nin: [D(MS.big)] } }) },
771
+ ]
772
+ for (let f of inverted) {
773
+ it(`should not select or delete any row for ${f.name}`, async function() {
774
+ await resetDr(wo)
775
+ ob.clear()
776
+ for (let fn of ['select', 'delAll']) {
777
+ let out = await ob.invoke(fn, f.find())
778
+ chkRej(out, fn)
779
+ assert.strict.ok(out.err.message.indexOf('out of range') >= 0, out.err.message)
780
+ }
781
+ assert.strict.deepStrictEqual(sortRows(await wo.select()), baseRows)
782
+ })
783
+ }
784
+
785
+ //cvOrmFind自身之reject先於本檢核(同一條件同時含範圍外Date與白名單外運算子, 訊息須為cvOrmFind給出者)
786
+ it('should keep the message of cvOrmFind when the find has other invalid parts', async function() {
787
+ await resetDr(wo)
788
+ let mk0 = () => ({ u: D(MS.big), v: { $regex: 'x' } })
789
+ let expect = directMsg(mk0())
790
+ assert.strict.notEqual(expect, null)
791
+ assert.strict.ok(expect.indexOf('out of range') < 0)
792
+ for (let fn of ['select', 'delAll']) {
793
+ let out = await ob.invoke(fn, mk0())
794
+ chkRej(out, fn, expect)
795
+ }
796
+ assert.strict.deepStrictEqual(sortRows(await wo.select()), baseRows)
797
+ })
798
+
799
+ //無效Date仍由cvOrmFind給出其訊息(不因本檢核改變)
800
+ it('should keep the message of cvOrmFind for invalid Date', async function() {
801
+ await resetDr(wo)
802
+ let mk0 = () => ({ t: { $lt: new Date(NaN) } })
803
+ let expect = directMsg(mk0())
804
+ assert.strict.ok(expect.indexOf('invalid Date') >= 0)
805
+ for (let fn of ['select', 'delAll']) {
806
+ let out = await ob.invoke(fn, mk0())
807
+ chkRej(out, fn, expect)
808
+ }
809
+ })
810
+
811
+ //Date之各種形態: 子類別實例、實例自訂toISOString(回範圍內字串)、跨realm(vm)之Date、實例自訂Symbol.toStringTag, 範圍外者一律reject;
812
+ //共用檢核複製為新Date後才轉字串, 故檢查之值即使用之值, 不受呼叫端之實例方法影響
813
+ class SubDate extends Date {}
814
+ let forms = [
815
+ { name: 'subclass instance', mk: (ms) => new SubDate(ms) },
816
+ { name: 'own toISOString returning an in-range string', mk: (ms) => { let d = D(ms); d.toISOString = () => '2020-01-01T00:00:00.000Z'; return d } },
817
+ { name: 'own getTime returning an in-range value', mk: (ms) => { let d = D(ms); d.getTime = () => 0; return d } },
818
+ { name: 'Date of another realm', mk: (ms) => vm.runInNewContext(`new Date(${ms})`) },
819
+ { name: 'own Symbol.toStringTag', mk: (ms) => { let d = D(ms); d[Symbol.toStringTag] = 'X'; return d } },
820
+ ]
821
+ for (let form of forms) {
822
+ it(`should reject out-of-range Date given as ${form.name}`, async function() {
823
+ await resetDr(wo)
824
+ for (let ms of [MS.big, MS.aboveMax, MS.belowMin, MS.small]) {
825
+ let direct = directMsg({ t: { $lt: form.mk(ms) } })
826
+ let msg = direct === null ? expectMsg('find.t.$lt') : direct
827
+ for (let fn of ['select', 'delAll']) {
828
+ let out = await ob.invoke(fn, { t: { $lt: form.mk(ms) } })
829
+ chkRej(out, fn, msg)
830
+ }
831
+ }
832
+ assert.strict.deepStrictEqual(sortRows(await wo.select()), baseRows)
833
+
834
+ //範圍內者仍照常命中
835
+ assert.strict.deepStrictEqual(ids(await wo.select({ t: { $lte: form.mk(MS.min) } })), ['r3'])
836
+ })
837
+ }
838
+
839
+ //路徑格式: 訊息之路徑須逐字等於cvOrmFind對同一結構給出之路徑(以undefined取代Date令cvOrmFind給出其路徑), 含單鍵>30字元之截短、
840
+ //截斷處切開代理對、全路徑>120字元之截短與恰為120之邊界
841
+ let nest = (n, inner) => {
842
+ let f = inner
843
+ for (let i = 0; i < n; i++) {
844
+ f = { $and: [f] }
845
+ }
846
+ return f
847
+ }
848
+ let pathOf = (msg) => msg.slice(msg.indexOf(' at [') + 5, -1)
849
+ let pathCases = [
850
+ { name: 'key of 30 chars', mk: (v) => ({ [`${'k'.repeat(30)}`]: { $lt: v } }) },
851
+ { name: 'key of 31 chars', mk: (v) => ({ [`${'k'.repeat(31)}`]: { $lt: v } }) },
852
+ { name: 'key of 128 chars', mk: (v) => ({ [`${'b'.repeat(128)}`]: v }) },
853
+ { name: 'key cut inside a surrogate pair', mk: (v) => ({ [`${'a'.repeat(29)}\u{20000}zz`]: { $gt: v } }) },
854
+ { name: 'key with astral letter fully inside 30 chars', mk: (v) => ({ [`${'a'.repeat(27)}\u{20000}`]: { $gt: v } }) },
855
+ { name: 'non-ASCII key', mk: (v) => ({ 欄位: { $in: [v] } }) },
856
+ { name: 'path of 114 chars', mk: (v) => nest(13, { t: { $lt: v } }) },
857
+ { name: 'path of exactly 120 chars', mk: (v) => nest(13, { [`${'k'.repeat(11)}`]: v }) },
858
+ { name: 'path of 121 chars', mk: (v) => nest(13, { [`${'k'.repeat(12)}`]: v }) },
859
+ { name: 'path of 122 chars', mk: (v) => nest(14, { t: { $lt: v } }) },
860
+ { name: 'path of 200 chars with long key', mk: (v) => nest(20, { [`${'k'.repeat(40)}`]: { $lte: v } }) },
861
+ ]
862
+ for (let c of pathCases) {
863
+ it(`should give the same path as cvOrmFind for ${c.name}`, async function() {
864
+ await resetDr(wo)
865
+ let direct = directMsg(c.mk(undefined))
866
+ assert.strict.notEqual(direct, null, 'cvOrmFind should reject the structure with undefined')
867
+ let path = pathOf(direct)
868
+ for (let ms of [MS.big, MS.belowMin]) {
869
+ for (let fn of ['select', 'delAll']) {
870
+ let out = await ob.invoke(fn, c.mk(D(ms)))
871
+ chkRej(out, fn, expectMsg(path))
872
+ }
873
+ }
874
+ })
875
+ }
876
+
877
+ //數據側(R-FIND-02規則5): 範圍外Date之寫入行為不變, 惟其資料無法以Date條件正確比較(文件已載明)
878
+ it('should keep writing out-of-range Date as signed ISO string and document that such data can not be compared by Date condition', async function() {
879
+ let w1 = mk('daterange-data').wo
880
+ //三個寫入入口(insertBulk、insert、save之更新與自動插入)皆不檢核資料側之Date, 範圍外者照存為帶正負號之ISO字串
881
+ await w1.insertBulk([
882
+ { id: 's1', t: D(MS.big) },
883
+ { id: 's2', t: D(MS.small) },
884
+ { id: 'n1', t: D(Date.UTC(2020, 0, 1)) },
885
+ { id: 'nul', t: null },
886
+ { id: 'max9999', t: new Date('9999-12-31T23:59:59.999Z') },
887
+ ])
888
+ assert.strict.equal((await w1.selectByPk('s1')).t, '+275760-09-13T00:00:00.000Z')
889
+ assert.strict.equal((await w1.selectByPk('s2')).t, '-271821-04-20T00:00:00.000Z')
890
+ await w1.insert({ id: 'i1', t: D(MS.aboveMax) })
891
+ assert.strict.equal((await w1.selectByPk('i1')).t, '+010000-01-01T00:00:00.000Z')
892
+ await w1.save({ id: 'sv1', t: D(MS.belowMin) }) //自動插入
893
+ assert.strict.equal((await w1.selectByPk('sv1')).t, '-000001-12-31T23:59:59.999Z')
894
+ await w1.save({ id: 'n1', t: D(MS.big) }) //更新既有
895
+ assert.strict.equal((await w1.selectByPk('n1')).t, '+275760-09-13T00:00:00.000Z')
896
+ await w1.save({ id: 'n1', t: D(Date.UTC(2020, 0, 1)) }) //改回範圍內
897
+ assert.strict.equal((await w1.selectByPk('n1')).t, '2020-01-01T00:00:00.000Z')
898
+ await w1.delAll({ id: { $in: ['i1', 'sv1'] } })
899
+
900
+ //範圍內之條件值被接受, 但正年份哨兵s1(+275760年, 時間序屬未來)因字串字典序小於'2026-…'而被$lt現在命中(此即文件所載之限制, 非本檢核可防);
901
+ //s2(-271821年)雖亦命中, 但屬時間序之過去, 結果碰巧正確; null與範圍內最大日期(9999年底)不被命中(文件建議之永不過期寫法)
902
+ let hit = ids(await w1.select({ t: { $lt: new Date() } }))
903
+ assert.strict.deepStrictEqual(hit, ['n1', 's1', 's2'])
904
+
905
+ //同理$gt最小日期之條件命不到s1(時間序應命中)
906
+ let hit2 = ids(await w1.select({ t: { $gt: D(MS.min) } }))
907
+ assert.strict.deepStrictEqual(hit2, ['max9999', 'n1'])
908
+
909
+ //亦漏查未來之資料: $gt範圍內最大日期按時間序應命中s1(+275760年)卻無命中
910
+ let hit3 = ids(await w1.select({ t: { $gt: D(MS.max) } }))
911
+ assert.strict.deepStrictEqual(hit3, [])
912
+
913
+ //delAll依同一判定刪除(select命中之數據即delAll刪除之數據), 文件建議之null與9999年底寫法保留
914
+ let r = await w1.delAll({ t: { $lt: new Date() } })
915
+ assert.strict.deepStrictEqual(r, { n: 3, nDeleted: 3, ok: 1 })
916
+ assert.strict.deepStrictEqual(ids(await w1.select()), ['max9999', 'nul'])
917
+ })
918
+
919
+ //時序: 壞條件於佇列之外立即reject, 不等候先前已排入佇列之操作, 且不使其後之呼叫失敗
920
+ it('should reject out-of-range Date before queued operations finish and keep queue working', async function() {
921
+ let w1 = mk('daterange-queue').wo
922
+ let o1 = mkObserver(w1)
923
+ await resetDr(w1)
924
+ let rows = []
925
+ for (let i = 0; i < 2000; i++) {
926
+ rows.push({ id: `q${i}`, g: 'Q', v: i })
927
+ }
928
+ for (let fn of ['select', 'delAll']) {
929
+ let done = false
930
+ let pmBig = w1.insertBulk(rows.map((v) => ({ ...v, id: `${fn}-${v.id}` }))).then(function(r) {
931
+ done = true
932
+ return r
933
+ })
934
+ let out = await o1.invoke(fn, { t: { $gt: D(MS.big) } }, () => done)
935
+ chkRej(out, fn)
936
+ assert.strict.equal(out.probe, false, `${fn} should reject before the queued insertBulk finishes`)
937
+ let rBulk = await pmBig
938
+ assert.strict.equal(rBulk.nInserted, 2000)
939
+ }
940
+ assert.strict.deepStrictEqual(ids(await w1.select({ t: { $gte: D(MS.min) } })), ['r1', 'r2', 'r3', 'r4'])
941
+ let r = await w1.delAll({ g: 'Q' })
942
+ assert.strict.deepStrictEqual(r, { n: 4000, nDeleted: 4000, ok: 1 })
943
+ assert.strict.deepStrictEqual(ids(await w1.select()), ['r1', 'r2', 'r3', 'r4', 'r5'])
944
+ })
945
+
946
+ //時序: useCache已填入快取時仍須reject, 快取與資料不受影響; 範圍內之Date條件仍可用
947
+ it('should reject out-of-range Date with filled cache and keep data', async function() {
948
+ let w1 = mk('daterange-cache', { useCache: true }).wo
949
+ let o1 = mkObserver(w1)
950
+ await w1.insertBulk(rowsDr())
951
+ assert.strict.equal((await w1.select()).length, 5)
952
+ for (let fn of ['select', 'delAll']) {
953
+ for (let ms of [MS.belowMin, MS.aboveMax, MS.big, MS.small]) {
954
+ let out = await o1.invoke(fn, { t: { $gt: D(ms) } })
955
+ chkRej(out, fn, expectMsg('find.t.$gt'))
956
+ }
957
+ }
958
+ assert.strict.deepStrictEqual(ids(await w1.select()), ['r1', 'r2', 'r3', 'r4', 'r5'])
959
+ assert.strict.deepStrictEqual(ids(await w1.select({ t: { $gt: D(MS.min) } })), ['r1', 'r2', 'r4'])
960
+
961
+ //熱快取下範圍內之Date條件於delAll亦照常(delAll一律直讀資料庫), 刪除後select不再命中
962
+ assert.strict.deepStrictEqual(await w1.delAll({ t: { $gt: D(MS.min) } }), { n: 3, nDeleted: 3, ok: 1 })
963
+ assert.strict.deepStrictEqual(ids(await w1.select()), ['r3', 'r5'])
964
+ })
965
+
966
+ //時序: 檢核先於讀檔, 資料庫檔案無法解析時, 範圍外Date仍須回invalid find(未讀檔), 範圍內Date則因讀檔而回解析錯誤
967
+ it('should check date range before reading the database file', async function() {
968
+ let { url, wo: w1 } = mk('daterange-unreadable')
969
+ let o1 = mkObserver(w1)
970
+ fs.writeFileSync(url, '{ not a json')
971
+ for (let fn of ['select', 'delAll']) {
972
+ let outOk = await o1.invoke(fn, { t: { $lt: D(MS.max) } })
973
+ assert.strict.equal(outOk.ok, false, `${fn} with in-range Date should reject by unreadable file`)
974
+ assert.strict.ok(outOk.err.message.indexOf('invalid find') < 0, outOk.err.message)
975
+
976
+ let outBad = await o1.invoke(fn, { t: { $lt: D(MS.aboveMax) } })
977
+ chkRej(outBad, fn, expectMsg('find.t.$lt'))
978
+ }
979
+ })
980
+
981
+ //壞條件之delAll不得改寫資料庫檔案(內容與修改時間皆不變)
982
+ it('should not rewrite the database file when delAll is rejected by out-of-range Date', async function() {
983
+ let { url, wo: w1 } = mk('daterange-norewrite')
984
+ let o1 = mkObserver(w1)
985
+ await resetDr(w1)
986
+ let txt0 = fs.readFileSync(url, 'utf8')
987
+ let mt0 = fs.statSync(url).mtimeMs
988
+ await tick()
989
+ for (let ms of [MS.belowMin, MS.aboveMax, MS.big, MS.small]) {
990
+ let out = await o1.invoke('delAll', { t: { $gt: D(ms) } })
991
+ chkRej(out, 'delAll', expectMsg('find.t.$gt'))
992
+ }
993
+ assert.strict.equal(fs.readFileSync(url, 'utf8'), txt0)
994
+ assert.strict.equal(fs.statSync(url).mtimeMs, mt0)
995
+ })
996
+
997
+ })
998
+
999
+ })