w-orm-lowdb 1.0.32 → 1.0.34

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.
@@ -1,9 +1,12 @@
1
1
  import assert from 'assert'
2
+ import vm from 'vm'
2
3
  import fs from 'fs'
3
4
  import w from 'wsemi'
4
5
  import cvCases from 'wsemi/src/_cvOrmFindCases.mjs'
5
6
  import cvOrmFind from 'wsemi/src/cvOrmFind.mjs'
6
7
  import WOrm from '../src/WOrmLowdb.mjs'
8
+ import { tick, mkObserver } from './lib/mkObserver.mjs'
9
+ import { leaves, chkSame } from './lib/leaves.mjs'
7
10
 
8
11
 
9
12
  //條件(find)之系列白名單(帳本R-FIND-01; 規則: C:\opensrcOper\dspec\w-orm系列條件語法支援盤點.md〈一之三〉)
@@ -19,9 +22,6 @@ describe('find-whitelist', function() {
19
22
  //ids, 取id集合(升冪)
20
23
  let ids = (rs) => rs.map((v) => v.id).sort()
21
24
 
22
- //tick, 讓出一個macrotask, 令延遲發出之事件有機會發出
23
- let tick = () => new Promise((resolve) => setTimeout(resolve, 0))
24
-
25
25
  //directMsg, 共用檢核對同一條件直接給出之訊息(後端須原樣給出, 不得改寫、包裝或另立訊息)
26
26
  let directMsg = (f) => {
27
27
  try {
@@ -33,46 +33,6 @@ describe('find-whitelist', function() {
33
33
  return null
34
34
  }
35
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
36
  //mk, 於測試資料夾下建立資料庫檔(url為檔案路徑)
77
37
  let mk = (name, opt = {}) => {
78
38
  let url = `${dir}/${name}.json`
@@ -530,4 +490,434 @@ describe('find-whitelist', function() {
530
490
 
531
491
  })
532
492
 
493
+ //範圍外Date條件fail-closed(帳本R-FIND-02): 數據以JSON存檔, Date條件轉ISO字串後以字典序比較, 年份介於0000-9999時為固定24字元, 字典序即時間序;
494
+ //年份<0或>9999時toISOString()帶+yyyyyy-/-yyyyyy-前綴, 字典序與時間序顛倒, 故轉譯層對此類條件值整批reject(訊息格式同cvOrmFind, 不回顯值), select與delAll皆同, 於任何讀檔之前
495
+ describe('date-range', function() {
496
+ let MS = {
497
+ min: -62167219200000, //0000-01-01T00:00:00.000Z
498
+ max: 253402300799999, //9999-12-31T23:59:59.999Z
499
+ belowMin: -62167219200001, //-000001-12-31T23:59:59.999Z
500
+ aboveMax: 253402300800000, //+010000-01-01T00:00:00.000Z
501
+ big: 8.64e15, //+275760-09-13T00:00:00.000Z
502
+ small: -8.64e15, //-271821-04-20T00:00:00.000Z
503
+ }
504
+ let D = (ms) => new Date(ms)
505
+ let ok = () => D(Date.UTC(2026, 5, 1))
506
+ let wo = null
507
+ let ob = null
508
+ let baseRows = null
509
+
510
+ //rowsDr, 皆為範圍內之Date(r3、r4為邊界)
511
+ let rowsDr = () => [
512
+ { id: 'r1', t: D(Date.UTC(2020, 0, 1)) },
513
+ { id: 'r2', t: D(Date.UTC(2099, 0, 1)) },
514
+ { id: 'r3', t: D(MS.min) },
515
+ { id: 'r4', t: D(MS.max) },
516
+ { id: 'r5' },
517
+ ]
518
+
519
+ //resetDr
520
+ let resetDr = async(w0) => {
521
+ await w0.delAll()
522
+ await w0.insertBulk(rowsDr())
523
+ }
524
+
525
+ //expectMsg, 本層之訊息(格式同cvOrmFind: invalid find: 原因 at [路徑])
526
+ let expectMsg = (path) => `invalid find: Date year is out of range 0000-9999 at [${path}]`
527
+
528
+ before(async function() {
529
+ wo = mk('daterange').wo
530
+ ob = mkObserver(wo)
531
+ await resetDr(wo)
532
+ baseRows = sortRows(await wo.select())
533
+ })
534
+
535
+ it('should store Date at the boundaries as fixed 24-char ISO strings', async function() {
536
+ await resetDr(wo)
537
+ assert.strict.equal((await wo.selectByPk('r3')).t, '0000-01-01T00:00:00.000Z')
538
+ assert.strict.equal((await wo.selectByPk('r4')).t, '9999-12-31T23:59:59.999Z')
539
+ })
540
+
541
+ //接受: 邊界(0000-01-01T00:00:00.000Z、9999-12-31T23:59:59.999Z)內之Date, 命中集合正確且delAll刪除之集合等於select命中之集合
542
+ let accepts = [
543
+ { name: 'direct min', find: () => ({ t: D(MS.min) }), ids: ['r3'] },
544
+ { name: 'direct max', find: () => ({ t: D(MS.max) }), ids: ['r4'] },
545
+ { name: '$eq min', find: () => ({ t: { $eq: D(MS.min) } }), ids: ['r3'] },
546
+ { name: '$ne min', find: () => ({ t: { $ne: D(MS.min) } }), ids: ['r1', 'r2', 'r4', 'r5'] },
547
+ { name: '$gte min', find: () => ({ t: { $gte: D(MS.min) } }), ids: ['r1', 'r2', 'r3', 'r4'] },
548
+ { name: '$gt min', find: () => ({ t: { $gt: D(MS.min) } }), ids: ['r1', 'r2', 'r4'] },
549
+ { name: '$lt min', find: () => ({ t: { $lt: D(MS.min) } }), ids: [] },
550
+ { name: '$lte min', find: () => ({ t: { $lte: D(MS.min) } }), ids: ['r3'] },
551
+ { name: '$gt max', find: () => ({ t: { $gt: D(MS.max) } }), ids: [] },
552
+ { name: '$gte max', find: () => ({ t: { $gte: D(MS.max) } }), ids: ['r4'] },
553
+ { name: '$lt max', find: () => ({ t: { $lt: D(MS.max) } }), ids: ['r1', 'r2', 'r3'] },
554
+ { name: '$lte max', find: () => ({ t: { $lte: D(MS.max) } }), ids: ['r1', 'r2', 'r3', 'r4'] },
555
+ { name: '$in min and max', find: () => ({ t: { $in: [D(MS.min), D(MS.max)] } }), ids: ['r3', 'r4'] },
556
+ { name: '$nin min and max', find: () => ({ t: { $nin: [D(MS.min), D(MS.max)] } }), ids: ['r1', 'r2', 'r5'] },
557
+ { name: '$and from min to max', find: () => ({ $and: [{ t: { $gte: D(MS.min) } }, { t: { $lte: D(MS.max) } }] }), ids: ['r1', 'r2', 'r3', 'r4'] },
558
+ { name: '$or min or max', find: () => ({ $or: [{ t: D(MS.min) }, { t: D(MS.max) }] }), ids: ['r3', 'r4'] },
559
+ { name: '$lt now keeps in-range past rows only', find: () => ({ t: { $lt: new Date() } }), ids: ['r1', 'r3'] },
560
+ ]
561
+ for (let f of accepts) {
562
+ it(`should select and delAll the same set by Date in range for ${f.name}`, async function() {
563
+ await resetDr(wo)
564
+ ob.clear()
565
+ let outS = await ob.invoke('select', f.find())
566
+ assert.strict.equal(outS.ok, true, `select should resolve but got: ${outS.err}`)
567
+ assert.strict.deepStrictEqual(ids(outS.val), f.ids)
568
+ assert.strict.equal(outS.evs.filter((v) => v.t === 'error').length, 0)
569
+ let outD = await ob.invoke('delAll', f.find())
570
+ assert.strict.equal(outD.ok, true, `delAll should resolve but got: ${outD.err}`)
571
+ assert.strict.deepStrictEqual(outD.val, { n: f.ids.length, nDeleted: f.ids.length, ok: 1 })
572
+ assert.strict.equal(outD.evs.filter((v) => v.t === 'error').length, 0)
573
+ let rest = ids(await wo.select())
574
+ assert.strict.deepStrictEqual(['r1', 'r2', 'r3', 'r4', 'r5'].filter((id) => rest.indexOf(id) < 0), f.ids)
575
+ })
576
+ }
577
+
578
+ //接受: 範圍內但非端點之Date(端點內側1毫秒、年份交界、一般值), 以真實時間序為期望(範圍內字典序即時間序), 各運算子交叉select與delAll
579
+ let inners = [
580
+ '0000-01-01T00:00:00.001Z',
581
+ '0000-12-31T23:59:59.999Z',
582
+ '0001-01-01T00:00:00.000Z',
583
+ '0999-12-31T23:59:59.999Z',
584
+ '1970-01-01T00:00:00.000Z',
585
+ '9998-12-31T23:59:59.999Z',
586
+ '9999-01-01T00:00:00.000Z',
587
+ '9999-12-31T23:59:59.998Z',
588
+ ]
589
+ let opsInner = [
590
+ { name: '$eq', mk: (v) => ({ t: { $eq: v } }), hit: (a, b) => a === b, miss: false },
591
+ { name: 'direct', mk: (v) => ({ t: v }), hit: (a, b) => a === b, miss: false },
592
+ { name: '$ne', mk: (v) => ({ t: { $ne: v } }), hit: (a, b) => a !== b, miss: true },
593
+ { name: '$gt', mk: (v) => ({ t: { $gt: v } }), hit: (a, b) => a > b, miss: false },
594
+ { name: '$gte', mk: (v) => ({ t: { $gte: v } }), hit: (a, b) => a >= b, miss: false },
595
+ { name: '$lt', mk: (v) => ({ t: { $lt: v } }), hit: (a, b) => a < b, miss: false },
596
+ { name: '$lte', mk: (v) => ({ t: { $lte: v } }), hit: (a, b) => a <= b, miss: false },
597
+ { name: '$in', mk: (v) => ({ t: { $in: [D(MS.min), v] } }), hit: (a, b) => a === b || a === MS.min, miss: false },
598
+ { name: '$nin', mk: (v) => ({ t: { $nin: [v, D(MS.max)] } }), hit: (a, b) => a !== b && a !== MS.max, miss: true },
599
+ ]
600
+ for (let iso of inners) {
601
+ it(`should accept Date ${iso} in every operator and give the time-ordered set in select and delAll`, async function() {
602
+ let v = new Date(iso)
603
+ assert.strict.equal(v.toISOString(), iso)
604
+ for (let op of opsInner) {
605
+ let rows = [...rowsDr(), { id: 'x', t: new Date(iso) }]
606
+ let expect = rows.filter((r) => (r.t ? op.hit(r.t.getTime(), v.getTime()) : op.miss)).map((r) => r.id).sort()
607
+ for (let fn of ['select', 'delAll']) {
608
+ await wo.delAll()
609
+ await wo.insertBulk(rows)
610
+ ob.clear()
611
+ let out = await ob.invoke(fn, op.mk(new Date(iso)))
612
+ assert.strict.equal(out.ok, true, `${fn} ${op.name} should resolve but got: ${out.err}`)
613
+ assert.strict.equal(out.evs.filter((e) => e.t === 'error').length, 0)
614
+ if (fn === 'select') {
615
+ assert.strict.deepStrictEqual(ids(out.val), expect, `select ${op.name} ${iso}`)
616
+ }
617
+ else {
618
+ assert.strict.deepStrictEqual(out.val, { n: expect.length, nDeleted: expect.length, ok: 1 }, `delAll ${op.name} ${iso}`)
619
+ let rest = ids(await wo.select())
620
+ assert.strict.deepStrictEqual(rows.map((r) => r.id).sort().filter((id) => rest.indexOf(id) < 0), expect, `delAll set ${op.name} ${iso}`)
621
+ }
622
+ }
623
+ }
624
+ })
625
+ }
626
+
627
+ //reject: 範圍外Date於各位置, select與delAll皆整批reject, 訊息含路徑且不回顯值, error事件於reject前恰一次, 不發change, 資料不變
628
+ let bads = [
629
+ { name: 'below 0000 (-000001-12-31T23:59:59.999Z)', ms: MS.belowMin, echo: ['000001'] },
630
+ { name: 'above 9999 (+010000-01-01T00:00:00.000Z)', ms: MS.aboveMax, echo: ['010000'] },
631
+ { name: 'new Date(8.64e15)', ms: MS.big, echo: ['275760'] },
632
+ { name: 'new Date(-8.64e15)', ms: MS.small, echo: ['271821'] },
633
+ ]
634
+ let positions = [
635
+ { name: 'direct', find: (d) => ({ t: d }), path: 'find.t' },
636
+ { name: 'direct in other field', find: (d) => ({ id: 'r1', u: d }), path: 'find.u' },
637
+ { name: '$eq', find: (d) => ({ t: { $eq: d } }), path: 'find.t.$eq' },
638
+ { name: '$ne', find: (d) => ({ t: { $ne: d } }), path: 'find.t.$ne' },
639
+ { name: '$gt', find: (d) => ({ t: { $gt: d } }), path: 'find.t.$gt' },
640
+ { name: '$gte', find: (d) => ({ t: { $gte: d } }), path: 'find.t.$gte' },
641
+ { name: '$lt', find: (d) => ({ t: { $lt: d } }), path: 'find.t.$lt' },
642
+ { name: '$lte', find: (d) => ({ t: { $lte: d } }), path: 'find.t.$lte' },
643
+ { name: '$in first', find: (d) => ({ t: { $in: [d, ok()] } }), path: 'find.t.$in[0]' },
644
+ { name: '$in middle', find: (d) => ({ t: { $in: [ok(), d, ok()] } }), path: 'find.t.$in[1]' },
645
+ { name: '$in last', find: (d) => ({ t: { $in: [ok(), ok(), d] } }), path: 'find.t.$in[2]' },
646
+ { name: '$nin', find: (d) => ({ t: { $nin: [ok(), d] } }), path: 'find.t.$nin[1]' },
647
+ { name: '$and', find: (d) => ({ $and: [{ id: 'r1' }, { t: { $lt: d } }] }), path: 'find.$and[1].t.$lt' },
648
+ { name: '$or', find: (d) => ({ $or: [{ t: d }, { id: 'r1' }] }), path: 'find.$or[0].t' },
649
+ { 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' },
650
+ { name: 'range with valid Date sibling', find: (d) => ({ id: 'r1', t: { $gte: ok(), $lte: d } }), path: 'find.t.$lte' },
651
+ { name: 'valid Date before and after', find: (d) => ({ a: ok(), t: d, z: ok() }), path: 'find.t' },
652
+ { name: '$nin first', find: (d) => ({ t: { $nin: [d, ok()] } }), path: 'find.t.$nin[0]' },
653
+ { name: '$nin middle', find: (d) => ({ t: { $nin: [ok(), d, ok()] } }), path: 'find.t.$nin[1]' },
654
+ { name: '$nin last of three', find: (d) => ({ t: { $nin: [ok(), ok(), d] } }), path: 'find.t.$nin[2]' },
655
+ { name: '$in at two-digit index', find: (d) => ({ t: { $in: [...Array.from({ length: 10 }, () => ok()), d, ok()] } }), path: 'find.t.$in[10]' },
656
+ { name: '$and first branch', find: (d) => ({ $and: [{ t: { $lt: d } }, { id: 'r1' }] }), path: 'find.$and[0].t.$lt' },
657
+ { name: '$or last branch', find: (d) => ({ $or: [{ id: 'r1' }, { t: d }] }), path: 'find.$or[1].t' },
658
+ { 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' },
659
+ { name: 'range in reverse order', find: (d) => ({ id: 'r1', t: { $lte: d, $gte: ok() } }), path: 'find.t.$lte' },
660
+ { name: 'two out-of-range Date in different fields', find: (d) => ({ a: d, b: d }), path: 'find.a' },
661
+ { name: 'two out-of-range Date in one $in', find: (d) => ({ t: { $in: [ok(), d, d] } }), path: 'find.t.$in[1]' },
662
+ ]
663
+ for (let b of bads) {
664
+ for (let p of positions) {
665
+ it(`should reject select and delAll without any change for ${b.name} in ${p.name}`, async function() {
666
+ await resetDr(wo)
667
+ ob.clear()
668
+ let d = D(b.ms)
669
+ let f = p.find(d)
670
+ let msg = expectMsg(p.path)
671
+ let before = leaves(f)
672
+
673
+ //cvOrmFind本身接受(證明本檢核屬轉譯層, 非共用檢核)
674
+ assert.strict.equal(directMsg(p.find(D(b.ms))), null, 'cvOrmFind should accept the Date')
675
+
676
+ //兩次呼叫皆傳入同一個f, reject後f逐節點(參照、毫秒、鍵序)須與呼叫前相同
677
+ for (let fn of ['select', 'delAll']) {
678
+ let out = await ob.invoke(fn, f)
679
+ chkRej(out, fn, msg)
680
+ for (let s of b.echo) {
681
+ assert.strict.ok(out.err.message.indexOf(s) < 0, `${fn} message should not echo the value: ${out.err.message}`)
682
+ }
683
+ chkSame(before, leaves(f))
684
+ }
685
+ assert.strict.equal(d.getTime(), b.ms)
686
+ assert.strict.ok(d instanceof Date)
687
+
688
+ //資料不變
689
+ assert.strict.deepStrictEqual(sortRows(await wo.select()), baseRows)
690
+ })
691
+ }
692
+ }
693
+
694
+ //字典序顛倒之場景(修正前: 範圍外Date轉為+yyyyyy-/-yyyyyy-字串後, '2020-…'等較大, 故$gt哨兵命中所有帶日期之數據, delAll因而刪光)
695
+ let inverted = [
696
+ { name: '$gt new Date(8.64e15)', find: () => ({ t: { $gt: D(MS.big) } }) },
697
+ { name: '$gte new Date(8.64e15)', find: () => ({ t: { $gte: D(MS.big) } }) },
698
+ { name: '$lt new Date(8.64e15)', find: () => ({ t: { $lt: D(MS.big) } }) },
699
+ { name: '$lte new Date(8.64e15)', find: () => ({ t: { $lte: D(MS.big) } }) },
700
+ { name: '$gt year 10000', find: () => ({ t: { $gt: D(MS.aboveMax) } }) },
701
+ { name: '$lt year -1', find: () => ({ t: { $lt: D(MS.belowMin) } }) },
702
+ { name: '$ne new Date(8.64e15)', find: () => ({ t: { $ne: D(MS.big) } }) },
703
+ { name: '$nin [new Date(8.64e15)]', find: () => ({ t: { $nin: [D(MS.big)] } }) },
704
+ ]
705
+ for (let f of inverted) {
706
+ it(`should not select or delete any row for ${f.name}`, async function() {
707
+ await resetDr(wo)
708
+ ob.clear()
709
+ for (let fn of ['select', 'delAll']) {
710
+ let out = await ob.invoke(fn, f.find())
711
+ chkRej(out, fn)
712
+ assert.strict.ok(out.err.message.indexOf('out of range') >= 0, out.err.message)
713
+ }
714
+ assert.strict.deepStrictEqual(sortRows(await wo.select()), baseRows)
715
+ })
716
+ }
717
+
718
+ //cvOrmFind自身之reject先於本檢核(同一條件同時含範圍外Date與白名單外運算子, 訊息須為cvOrmFind給出者)
719
+ it('should keep the message of cvOrmFind when the find has other invalid parts', async function() {
720
+ await resetDr(wo)
721
+ let mk0 = () => ({ u: D(MS.big), v: { $regex: 'x' } })
722
+ let expect = directMsg(mk0())
723
+ assert.strict.notEqual(expect, null)
724
+ assert.strict.ok(expect.indexOf('out of range') < 0)
725
+ for (let fn of ['select', 'delAll']) {
726
+ let out = await ob.invoke(fn, mk0())
727
+ chkRej(out, fn, expect)
728
+ }
729
+ assert.strict.deepStrictEqual(sortRows(await wo.select()), baseRows)
730
+ })
731
+
732
+ //無效Date仍由cvOrmFind給出其訊息(不因本檢核改變)
733
+ it('should keep the message of cvOrmFind for invalid Date', async function() {
734
+ await resetDr(wo)
735
+ let mk0 = () => ({ t: { $lt: new Date(NaN) } })
736
+ let expect = directMsg(mk0())
737
+ assert.strict.ok(expect.indexOf('invalid Date') >= 0)
738
+ for (let fn of ['select', 'delAll']) {
739
+ let out = await ob.invoke(fn, mk0())
740
+ chkRej(out, fn, expect)
741
+ }
742
+ })
743
+
744
+ //Date之各種形態: 子類別實例、實例自訂toISOString(回範圍內字串)、跨realm(vm)之Date、實例自訂Symbol.toStringTag, 範圍外者一律reject;
745
+ //共用檢核複製為新Date後才轉字串, 故檢查之值即使用之值, 不受呼叫端之實例方法影響
746
+ class SubDate extends Date {}
747
+ let forms = [
748
+ { name: 'subclass instance', mk: (ms) => new SubDate(ms) },
749
+ { name: 'own toISOString returning an in-range string', mk: (ms) => { let d = D(ms); d.toISOString = () => '2020-01-01T00:00:00.000Z'; return d } },
750
+ { name: 'own getTime returning an in-range value', mk: (ms) => { let d = D(ms); d.getTime = () => 0; return d } },
751
+ { name: 'Date of another realm', mk: (ms) => vm.runInNewContext(`new Date(${ms})`) },
752
+ { name: 'own Symbol.toStringTag', mk: (ms) => { let d = D(ms); d[Symbol.toStringTag] = 'X'; return d } },
753
+ ]
754
+ for (let form of forms) {
755
+ it(`should reject out-of-range Date given as ${form.name}`, async function() {
756
+ await resetDr(wo)
757
+ for (let ms of [MS.big, MS.aboveMax, MS.belowMin, MS.small]) {
758
+ let direct = directMsg({ t: { $lt: form.mk(ms) } })
759
+ let msg = direct === null ? expectMsg('find.t.$lt') : direct
760
+ for (let fn of ['select', 'delAll']) {
761
+ let out = await ob.invoke(fn, { t: { $lt: form.mk(ms) } })
762
+ chkRej(out, fn, msg)
763
+ }
764
+ }
765
+ assert.strict.deepStrictEqual(sortRows(await wo.select()), baseRows)
766
+
767
+ //範圍內者仍照常命中
768
+ assert.strict.deepStrictEqual(ids(await wo.select({ t: { $lte: form.mk(MS.min) } })), ['r3'])
769
+ })
770
+ }
771
+
772
+ //路徑格式: 訊息之路徑須逐字等於cvOrmFind對同一結構給出之路徑(以undefined取代Date令cvOrmFind給出其路徑), 含單鍵>30字元之截短、
773
+ //截斷處切開代理對、全路徑>120字元之截短與恰為120之邊界
774
+ let nest = (n, inner) => {
775
+ let f = inner
776
+ for (let i = 0; i < n; i++) {
777
+ f = { $and: [f] }
778
+ }
779
+ return f
780
+ }
781
+ let pathOf = (msg) => msg.slice(msg.indexOf(' at [') + 5, -1)
782
+ let pathCases = [
783
+ { name: 'key of 30 chars', mk: (v) => ({ [`${'k'.repeat(30)}`]: { $lt: v } }) },
784
+ { name: 'key of 31 chars', mk: (v) => ({ [`${'k'.repeat(31)}`]: { $lt: v } }) },
785
+ { name: 'key of 128 chars', mk: (v) => ({ [`${'b'.repeat(128)}`]: v }) },
786
+ { name: 'key cut inside a surrogate pair', mk: (v) => ({ [`${'a'.repeat(29)}\u{20000}zz`]: { $gt: v } }) },
787
+ { name: 'key with astral letter fully inside 30 chars', mk: (v) => ({ [`${'a'.repeat(27)}\u{20000}`]: { $gt: v } }) },
788
+ { name: 'non-ASCII key', mk: (v) => ({ 欄位: { $in: [v] } }) },
789
+ { name: 'path of 114 chars', mk: (v) => nest(13, { t: { $lt: v } }) },
790
+ { name: 'path of exactly 120 chars', mk: (v) => nest(13, { [`${'k'.repeat(11)}`]: v }) },
791
+ { name: 'path of 121 chars', mk: (v) => nest(13, { [`${'k'.repeat(12)}`]: v }) },
792
+ { name: 'path of 122 chars', mk: (v) => nest(14, { t: { $lt: v } }) },
793
+ { name: 'path of 200 chars with long key', mk: (v) => nest(20, { [`${'k'.repeat(40)}`]: { $lte: v } }) },
794
+ ]
795
+ for (let c of pathCases) {
796
+ it(`should give the same path as cvOrmFind for ${c.name}`, async function() {
797
+ await resetDr(wo)
798
+ let direct = directMsg(c.mk(undefined))
799
+ assert.strict.notEqual(direct, null, 'cvOrmFind should reject the structure with undefined')
800
+ let path = pathOf(direct)
801
+ for (let ms of [MS.big, MS.belowMin]) {
802
+ for (let fn of ['select', 'delAll']) {
803
+ let out = await ob.invoke(fn, c.mk(D(ms)))
804
+ chkRej(out, fn, expectMsg(path))
805
+ }
806
+ }
807
+ })
808
+ }
809
+
810
+ //數據側(R-FIND-02規則5; 寫入側見R-DATE-01與unit-date-write): 資料庫檔內既有之範圍外Date字串(舊版寫入之樣子; 現行寫入已拒收範圍外Date物件,
811
+ //惟字串不是Date物件故仍可寫入)無法以Date條件正確比較(文件已載明)
812
+ it('should document that out-of-range Date strings already in database can not be compared by Date condition', async function() {
813
+ let w1 = mk('daterange-data').wo
814
+ await w1.insertBulk([
815
+ { id: 's1', t: '+275760-09-13T00:00:00.000Z' }, //舊版以new Date(8.64e15)寫入之樣子
816
+ { id: 's2', t: '-271821-04-20T00:00:00.000Z' }, //舊版以new Date(-8.64e15)寫入之樣子
817
+ { id: 'n1', t: D(Date.UTC(2020, 0, 1)) },
818
+ { id: 'nul', t: null },
819
+ { id: 'max9999', t: new Date('9999-12-31T23:59:59.999Z') },
820
+ ])
821
+ assert.strict.equal((await w1.selectByPk('s1')).t, '+275760-09-13T00:00:00.000Z')
822
+ assert.strict.equal((await w1.selectByPk('s2')).t, '-271821-04-20T00:00:00.000Z')
823
+
824
+ //範圍內之條件值被接受, 但正年份哨兵s1(+275760年, 時間序屬未來)因字串字典序小於'2026-…'而被$lt現在命中(此即文件所載之限制, 非本檢核可防);
825
+ //s2(-271821年)雖亦命中, 但屬時間序之過去, 結果碰巧正確; null與範圍內最大日期(9999年底)不被命中(文件建議之永不過期寫法)
826
+ let hit = ids(await w1.select({ t: { $lt: new Date() } }))
827
+ assert.strict.deepStrictEqual(hit, ['n1', 's1', 's2'])
828
+
829
+ //同理$gt最小日期之條件命不到s1(時間序應命中)
830
+ let hit2 = ids(await w1.select({ t: { $gt: D(MS.min) } }))
831
+ assert.strict.deepStrictEqual(hit2, ['max9999', 'n1'])
832
+
833
+ //亦漏查未來之資料: $gt範圍內最大日期按時間序應命中s1(+275760年)卻無命中
834
+ let hit3 = ids(await w1.select({ t: { $gt: D(MS.max) } }))
835
+ assert.strict.deepStrictEqual(hit3, [])
836
+
837
+ //delAll依同一判定刪除(select命中之數據即delAll刪除之數據), 文件建議之null與9999年底寫法保留
838
+ let r = await w1.delAll({ t: { $lt: new Date() } })
839
+ assert.strict.deepStrictEqual(r, { n: 3, nDeleted: 3, ok: 1 })
840
+ assert.strict.deepStrictEqual(ids(await w1.select()), ['max9999', 'nul'])
841
+ })
842
+
843
+ //時序: 壞條件於佇列之外立即reject, 不等候先前已排入佇列之操作, 且不使其後之呼叫失敗
844
+ it('should reject out-of-range Date before queued operations finish and keep queue working', async function() {
845
+ let w1 = mk('daterange-queue').wo
846
+ let o1 = mkObserver(w1)
847
+ await resetDr(w1)
848
+ let rows = []
849
+ for (let i = 0; i < 2000; i++) {
850
+ rows.push({ id: `q${i}`, g: 'Q', v: i })
851
+ }
852
+ for (let fn of ['select', 'delAll']) {
853
+ let done = false
854
+ let pmBig = w1.insertBulk(rows.map((v) => ({ ...v, id: `${fn}-${v.id}` }))).then(function(r) {
855
+ done = true
856
+ return r
857
+ })
858
+ let out = await o1.invoke(fn, { t: { $gt: D(MS.big) } }, () => done)
859
+ chkRej(out, fn)
860
+ assert.strict.equal(out.probe, false, `${fn} should reject before the queued insertBulk finishes`)
861
+ let rBulk = await pmBig
862
+ assert.strict.equal(rBulk.nInserted, 2000)
863
+ }
864
+ assert.strict.deepStrictEqual(ids(await w1.select({ t: { $gte: D(MS.min) } })), ['r1', 'r2', 'r3', 'r4'])
865
+ let r = await w1.delAll({ g: 'Q' })
866
+ assert.strict.deepStrictEqual(r, { n: 4000, nDeleted: 4000, ok: 1 })
867
+ assert.strict.deepStrictEqual(ids(await w1.select()), ['r1', 'r2', 'r3', 'r4', 'r5'])
868
+ })
869
+
870
+ //時序: useCache已填入快取時仍須reject, 快取與資料不受影響; 範圍內之Date條件仍可用
871
+ it('should reject out-of-range Date with filled cache and keep data', async function() {
872
+ let w1 = mk('daterange-cache', { useCache: true }).wo
873
+ let o1 = mkObserver(w1)
874
+ await w1.insertBulk(rowsDr())
875
+ assert.strict.equal((await w1.select()).length, 5)
876
+ for (let fn of ['select', 'delAll']) {
877
+ for (let ms of [MS.belowMin, MS.aboveMax, MS.big, MS.small]) {
878
+ let out = await o1.invoke(fn, { t: { $gt: D(ms) } })
879
+ chkRej(out, fn, expectMsg('find.t.$gt'))
880
+ }
881
+ }
882
+ assert.strict.deepStrictEqual(ids(await w1.select()), ['r1', 'r2', 'r3', 'r4', 'r5'])
883
+ assert.strict.deepStrictEqual(ids(await w1.select({ t: { $gt: D(MS.min) } })), ['r1', 'r2', 'r4'])
884
+
885
+ //熱快取下範圍內之Date條件於delAll亦照常(delAll一律直讀資料庫), 刪除後select不再命中
886
+ assert.strict.deepStrictEqual(await w1.delAll({ t: { $gt: D(MS.min) } }), { n: 3, nDeleted: 3, ok: 1 })
887
+ assert.strict.deepStrictEqual(ids(await w1.select()), ['r3', 'r5'])
888
+ })
889
+
890
+ //時序: 檢核先於讀檔, 資料庫檔案無法解析時, 範圍外Date仍須回invalid find(未讀檔), 範圍內Date則因讀檔而回解析錯誤
891
+ it('should check date range before reading the database file', async function() {
892
+ let { url, wo: w1 } = mk('daterange-unreadable')
893
+ let o1 = mkObserver(w1)
894
+ fs.writeFileSync(url, '{ not a json')
895
+ for (let fn of ['select', 'delAll']) {
896
+ let outOk = await o1.invoke(fn, { t: { $lt: D(MS.max) } })
897
+ assert.strict.equal(outOk.ok, false, `${fn} with in-range Date should reject by unreadable file`)
898
+ assert.strict.ok(outOk.err.message.indexOf('invalid find') < 0, outOk.err.message)
899
+
900
+ let outBad = await o1.invoke(fn, { t: { $lt: D(MS.aboveMax) } })
901
+ chkRej(outBad, fn, expectMsg('find.t.$lt'))
902
+ }
903
+ })
904
+
905
+ //壞條件之delAll不得改寫資料庫檔案(內容與修改時間皆不變)
906
+ it('should not rewrite the database file when delAll is rejected by out-of-range Date', async function() {
907
+ let { url, wo: w1 } = mk('daterange-norewrite')
908
+ let o1 = mkObserver(w1)
909
+ await resetDr(w1)
910
+ let txt0 = fs.readFileSync(url, 'utf8')
911
+ let mt0 = fs.statSync(url).mtimeMs
912
+ await tick()
913
+ for (let ms of [MS.belowMin, MS.aboveMax, MS.big, MS.small]) {
914
+ let out = await o1.invoke('delAll', { t: { $gt: D(ms) } })
915
+ chkRej(out, 'delAll', expectMsg('find.t.$gt'))
916
+ }
917
+ assert.strict.equal(fs.readFileSync(url, 'utf8'), txt0)
918
+ assert.strict.equal(fs.statSync(url).mtimeMs, mt0)
919
+ })
920
+
921
+ })
922
+
533
923
  })