w-orm-lowdb 1.0.32 → 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.
- package/README.md +7 -3
- package/dist/w-orm-lowdb.umd.js +2 -2
- package/dist/w-orm-lowdb.umd.js.map +1 -1
- package/docs/WOrmLowdb.html +15 -11
- package/docs/WOrmLowdb.mjs.html +39 -6
- package/docs/index.html +1 -1
- package/package.json +3 -3
- package/src/WOrmLowdb.mjs +38 -5
- package/test/unit-find-whitelist.test.mjs +466 -0
|
@@ -1,4 +1,5 @@
|
|
|
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'
|
|
@@ -530,4 +531,469 @@ describe('find-whitelist', function() {
|
|
|
530
531
|
|
|
531
532
|
})
|
|
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
|
+
|
|
533
999
|
})
|