w-orm-mongodb 1.1.41 → 1.1.43

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.
@@ -2,13 +2,14 @@ import mongodb from 'mongodb'
2
2
  import stream from 'stream'
3
3
  import cloneDeep from 'lodash-es/cloneDeep.js'
4
4
  import every from 'lodash-es/every.js'
5
+ import filter from 'lodash-es/filter.js'
5
6
  import get from 'lodash-es/get.js'
6
7
  import map from 'lodash-es/map.js'
7
8
  import omit from 'lodash-es/omit.js'
8
9
  import size from 'lodash-es/size.js'
9
10
  import evem from 'wsemi/src/evem.mjs'
10
11
  import genPm from 'wsemi/src/genPm.mjs'
11
- import genID from 'wsemi/src/genID.mjs'
12
+ import genIDSeq from 'wsemi/src/genIDSeq.mjs'
12
13
  import isbol from 'wsemi/src/isbol.mjs'
13
14
  import isestr from 'wsemi/src/isestr.mjs'
14
15
  import isarr from 'wsemi/src/isarr.mjs'
@@ -63,6 +64,10 @@ function WOrmMongodb(opt = {}) {
63
64
  let _indexGfsReady = false
64
65
 
65
66
 
67
+ //_canUseTx, 拓樸判定只須一次, 以旗標記錄避免每次操作皆多一次round-trip
68
+ let _canUseTx = null
69
+
70
+
66
71
  //ee, 採wsemi之evem(即eventemitter3), 其於'error'無監聽者時僅回傳false而不拋出,
67
72
  //故本套件之操作行為不因呼叫端有無註冊監聽而改變; Node內建之EventEmitter具該拋出語義, 不可用
68
73
  let ee = evem()
@@ -154,8 +159,8 @@ function WOrmMongodb(opt = {}) {
154
159
  throw new Error(`invalid data[${k}].id, autoGenPk is false`)
155
160
  }
156
161
 
157
- //genID
158
- v.id = genID()
162
+ //genIDSeq
163
+ v.id = genIDSeq()
159
164
 
160
165
  }
161
166
 
@@ -514,6 +519,208 @@ function WOrmMongodb(opt = {}) {
514
519
  }
515
520
 
516
521
 
522
+ /**
523
+ * 判定本次部署是否支援交易
524
+ * 註: MongoDB之交易須replica set或分片叢集,standalone不支援
525
+ * 判定依據為hello回應:replica set帶setName,連至mongos者msg為isdbgrid,standalone兩者皆無
526
+ *
527
+ * @ignore
528
+ * @param {Object} client 輸入MongoClient物件
529
+ * @returns {Promise} 回傳Promise,resolve回傳是否支援交易布林值,reject回傳錯誤訊息
530
+ */
531
+ async function canUseTx(client) {
532
+
533
+ //check
534
+ if (isbol(_canUseTx)) {
535
+ return _canUseTx
536
+ }
537
+
538
+ //hello
539
+ let hello = await client.db(opt.db).admin().command({ hello: 1 })
540
+
541
+ //_canUseTx
542
+ _canUseTx = isestr(get(hello, 'setName')) || get(hello, 'msg') === 'isdbgrid'
543
+
544
+ return _canUseTx
545
+ }
546
+
547
+
548
+ /**
549
+ * 以交易達成insertBulk之全有全無
550
+ * 交易內任一筆失敗即整批回滾,故不會寫入任何一筆,無須補償動作
551
+ *
552
+ * @ignore
553
+ * @param {Object} client 輸入MongoClient物件
554
+ * @param {Object} collection 輸入資料表物件
555
+ * @param {Array} data 輸入數據陣列
556
+ * @returns {Promise} 回傳Promise,resolve回傳undefined,reject回傳錯誤訊息
557
+ */
558
+ async function _insertBulkByTx(client, collection, data) {
559
+
560
+ //session
561
+ let session = client.startSession()
562
+
563
+ try {
564
+
565
+ //withTransaction, ordered:true令首筆衝突即停止, 由交易回滾保證未寫入任何一筆
566
+ await session.withTransaction(async () => {
567
+ await collection.insertMany(data, { session, ordered: true })
568
+ })
569
+
570
+ }
571
+ finally {
572
+ await session.endSession()
573
+ }
574
+
575
+ }
576
+
577
+
578
+ /**
579
+ * 以補償動作達成insertBulk之全有全無,供無交易可用之standalone使用
580
+ * 註: 補償係刪除本次呼叫已寫入者,其_id由驅動於送出前在用戶端產生,
581
+ * 與呼叫前既有數據之_id必不相同,故縱使該筆之id已存在亦不會誤刪既有數據;
582
+ * 刪除未寫入者為無操作,故不須精確得知哪幾筆已寫入
583
+ * 註: 行程若於補償途中中止(斷電、強制終止),已寫入之部份可能殘留,此為無交易可用之固有限制
584
+ *
585
+ * @ignore
586
+ * @param {Object} collection 輸入資料表物件
587
+ * @param {Array} data 輸入數據陣列
588
+ * @returns {Promise} 回傳Promise,resolve回傳undefined,reject回傳錯誤訊息
589
+ */
590
+ async function _insertBulkByCompensate(collection, data) {
591
+
592
+ //errIns
593
+ let errIns = null
594
+
595
+ try {
596
+ await collection.insertMany(data, { ordered: false })
597
+ }
598
+ catch (err) {
599
+ errIns = err
600
+ }
601
+
602
+ //check, 全數插入成功即無須補償
603
+ if (errIns === null) {
604
+ return
605
+ }
606
+
607
+ //idDels, 取本次由驅動所產生之全部_id
608
+ let idDels = map(data, function(v) {
609
+ return v._id
610
+ })
611
+ idDels = filter(idDels, function(v) {
612
+ return v !== undefined && v !== null
613
+ })
614
+
615
+ //補償, 刪除本次已寫入者
616
+ if (size(idDels) > 0) {
617
+ await collection.deleteMany({ _id: { $in: idDels } })
618
+ }
619
+
620
+ throw errIns
621
+ }
622
+
623
+
624
+ /**
625
+ * 批次插入數據,全批視為一個單位:全部插入成功,或一筆都不寫入
626
+ * 任一筆之id已存在(含同批含重複id)即以reject拋出整批性錯誤,且不寫入任何一筆
627
+ * 註: 本函數非insert之加速版,兩者衝突政策不同——insert跳過已存在者而整批ok為1,
628
+ * 本函數則整批reject;需要逐筆處置者改用insert
629
+ * 註: n為輸入筆數,nInserted於成功時恆等於n;不提供逐筆結果,故不出現ok為0與err
630
+ * 註: opt.autoGenPk為true(預設)時未帶有效id者自動產生,為false時未帶有效id即reject且同批皆不寫入
631
+ * 註: 全有全無之達成方式依部署而異——具replica set或分片叢集者以交易包覆,
632
+ * standalone無交易可用而以補償動作達成,其限制詳見_insertBulkByCompensate
633
+ *
634
+ * @memberOf WOrmMongodb
635
+ * @param {Object|Array} data 輸入數據物件或陣列
636
+ * @returns {Promise} 回傳Promise,resolve回傳插入結果,reject回傳錯誤訊息
637
+ */
638
+ async function insertBulk(data) {
639
+ let isErr = false
640
+
641
+ //check
642
+ if (!iseobj(data) && !isearr(data)) {
643
+ return {
644
+ n: 0,
645
+ nInserted: 0,
646
+ ok: 1,
647
+ }
648
+ }
649
+
650
+ //cloneDeep
651
+ data = cloneDeep(data)
652
+
653
+ //client
654
+ let client = new MongoClient(opt.url)
655
+
656
+ //res
657
+ let res = null
658
+ try {
659
+
660
+ //connect, 令連線失敗於寫入前即拋出, 屬影響全部數據之整批性錯誤
661
+ await client.connect()
662
+
663
+ //database, collection
664
+ let database = client.db(opt.db)
665
+ let collection = database.collection(opt.cl)
666
+
667
+ //check
668
+ if (!isarr(data)) {
669
+ data = [data]
670
+ }
671
+
672
+ //check id, 須於任何寫入之前一次完成
673
+ data = map(data, procPk)
674
+
675
+ //ensureIndex
676
+ await ensureIndex(collection)
677
+
678
+ //nAll, n之基準為輸入筆數
679
+ let nAll = size(data)
680
+
681
+ //check, 有交易可用者一律優先採用交易, 其回滾即為全有全無之保證
682
+ let bTx = await canUseTx(client)
683
+ if (bTx) {
684
+ await _insertBulkByTx(client, collection, data)
685
+ }
686
+ else {
687
+ await _insertBulkByCompensate(collection, data)
688
+ }
689
+
690
+ //res, 執行至此表全數插入成功, 故nInserted恆等於n
691
+ res = {
692
+ n: nAll,
693
+ nInserted: nAll,
694
+ ok: 1,
695
+ }
696
+
697
+ }
698
+ catch (err) {
699
+ isErr = true
700
+ res = err
701
+ }
702
+ finally {
703
+ await client.close()
704
+ client = null
705
+ }
706
+
707
+ //emit
708
+ if (!isErr) {
709
+ emitChange('insertBulk', data, res)
710
+ }
711
+
712
+ //check
713
+ if (isErr) {
714
+
715
+ //emit, 整批性錯誤須於reject之前發出
716
+ emitError('insertBulk', data, res)
717
+
718
+ return Promise.reject(res)
719
+ }
720
+ return res
721
+ }
722
+
723
+
517
724
  /**
518
725
  * 儲存單筆數據,由MongoDB於單一updateOne內原子完成[查找id]與[更新或插入]
519
726
  * 註: MongoDB單一文件操作本即為原子,故不須開啟transaction(transaction另須replica set,standalone不支援)
@@ -1380,6 +1587,7 @@ function WOrmMongodb(opt = {}) {
1380
1587
  ee.select = select
1381
1588
  ee.selectByPk = selectByPk
1382
1589
  ee.insert = insert
1590
+ ee.insertBulk = insertBulk
1383
1591
  ee.save = save
1384
1592
  ee.del = del
1385
1593
  ee.delAll = delAll
@@ -599,6 +599,206 @@ describe('insert', function() {
599
599
  })
600
600
 
601
601
 
602
+ //insertBulk之語義與insert不同: insert跳過已存在者而整批ok為1, insertBulk則整批reject且不寫入任何一筆
603
+ //本檔之容器為standalone, 無交易可用, 故走補償動作路徑; 交易路徑另見api-insertbulk-rs.test.mjs
604
+
605
+
606
+ describe('insertBulk', function() {
607
+ let rt = null
608
+ let vans = {}
609
+ let vget = {}
610
+
611
+ before(async function() {
612
+ this.timeout(120000)
613
+
614
+ let cl = 'bulkcnt'
615
+ let wo = WOrm(genOpt(cl))
616
+ await wo.delAll()
617
+
618
+ //全新3筆, nInserted須等於n
619
+ vget[1] = await wo.insertBulk([
620
+ { id: 'b1', name: 'peter' },
621
+ { id: 'b2', name: 'rosemary' },
622
+ { id: 'b3', name: 'kettle' },
623
+ ])
624
+ vget[2] = (await wo.select()).length
625
+
626
+ //撞既有主鍵須整批reject
627
+ rt = null
628
+ let nBefore = (await wo.select()).length
629
+ await wo.insertBulk([
630
+ { id: 'b4', name: 'new1' },
631
+ { id: 'b2', name: 'dup' },
632
+ { id: 'b5', name: 'new2' },
633
+ ])
634
+ .then(function(msg) {
635
+ rt = `不應resolve: ${JSON.stringify(msg)}`
636
+ })
637
+ .catch(function(msg) {
638
+ rt = msg.toString()
639
+ })
640
+ vget[3] = rt.indexOf('E11000') >= 0 || rt.indexOf('duplicate key') >= 0
641
+
642
+ //失敗後資料表不得有任何新增
643
+ vget[4] = (await wo.select()).length - nBefore
644
+ vget[5] = [
645
+ await wo.selectByPk('b4'),
646
+ await wo.selectByPk('b5'),
647
+ ]
648
+
649
+ //撞既有主鍵者本身不得被改動
650
+ vget[6] = (await wo.selectByPk('b2')).name
651
+
652
+ //同批含重複主鍵須整批reject
653
+ rt = null
654
+ nBefore = (await wo.select()).length
655
+ await wo.insertBulk([
656
+ { id: 'b6', name: 'a' },
657
+ { id: 'b6', name: 'b' },
658
+ { id: 'b7', name: 'c' },
659
+ ])
660
+ .then(function(msg) {
661
+ rt = `不應resolve: ${JSON.stringify(msg)}`
662
+ })
663
+ .catch(function(msg) {
664
+ rt = msg.toString()
665
+ })
666
+ vget[7] = rt.indexOf('E11000') >= 0 || rt.indexOf('duplicate key') >= 0
667
+ vget[8] = (await wo.select()).length - nBefore
668
+ vget[9] = [
669
+ await wo.selectByPk('b6'),
670
+ await wo.selectByPk('b7'),
671
+ ]
672
+
673
+ //單一物件輸入亦須可用
674
+ vget[10] = await wo.insertBulk({ id: 'b8', name: 'single' })
675
+
676
+ //輸入無效視為空結果
677
+ vget[11] = await wo.insertBulk(null)
678
+
679
+ //鍵集合須與insert完全相同
680
+ let ri = await wo.insert({ id: 'b9', name: 'x' })
681
+ vget[12] = [genKeys(vget[1]), genKeys(vget[10]), genKeys(vget[11]), genKeys(ri)]
682
+
683
+ //autoGenPk預設為true, 未帶id者自動產生
684
+ vget[13] = await wo.insertBulk([{ name: 'nopk1' }, { name: 'nopk2' }])
685
+
686
+ //autoGenPk為false且未帶id者須reject, 且同批之有效筆數不得被寫入
687
+ let woOff = WOrm(genOptPk('bulkpkoff', false))
688
+ await woOff.delAll()
689
+ rt = null
690
+ await woOff.insertBulk([
691
+ { id: 'p-ok1', name: 'ok1' },
692
+ { name: 'no-id' },
693
+ ])
694
+ .then(function(msg) {
695
+ rt = `不應resolve: ${JSON.stringify(msg)}`
696
+ })
697
+ .catch(function(msg) {
698
+ rt = msg.toString()
699
+ })
700
+ vget[14] = rt
701
+ vget[15] = await woOff.selectByPk('p-ok1')
702
+
703
+ //事件, 成功時發change, 失敗時發error且mode為insertBulk
704
+ let woEv = WOrm(genOpt('bulkevent'))
705
+ await woEv.delAll()
706
+ let evs = []
707
+ woEv.on('change', function(mode) {
708
+ evs.push(`change:${mode}`)
709
+ })
710
+ woEv.on('error', function(mode) {
711
+ evs.push(`error:${mode}`)
712
+ })
713
+ await woEv.insertBulk({ id: 'e1', name: 'a' })
714
+ await woEv.insertBulk({ id: 'e1', name: 'b' }).catch(() => {})
715
+ vget[16] = evs
716
+
717
+ })
718
+
719
+ vans[1] = { n: 3, nInserted: 3, ok: 1 }
720
+ it(`should get ${JSON.stringify(vans[1])} for insertBulk 3 new records`, async function() {
721
+ assert.strict.deepStrictEqual(vget[1], vans[1])
722
+ })
723
+
724
+ vans[2] = 3
725
+ it(`should get ${JSON.stringify(vans[2])} for records after insertBulk`, async function() {
726
+ assert.strict.deepStrictEqual(vget[2], vans[2])
727
+ })
728
+
729
+ vans[3] = true
730
+ it(`should get ${JSON.stringify(vans[3])} for reject of insertBulk with existed id`, async function() {
731
+ assert.strict.deepStrictEqual(vget[3], vans[3])
732
+ })
733
+
734
+ vans[4] = 0
735
+ it(`should get ${JSON.stringify(vans[4])} for no record added after rejected insertBulk`, async function() {
736
+ assert.strict.deepStrictEqual(vget[4], vans[4])
737
+ })
738
+
739
+ vans[5] = [null, null]
740
+ it(`should get ${JSON.stringify(vans[5])} for not writing any record in rejected insertBulk`, async function() {
741
+ assert.strict.deepStrictEqual(vget[5], vans[5])
742
+ })
743
+
744
+ vans[6] = 'rosemary'
745
+ it(`should get ${JSON.stringify(vans[6])} for not modifying existed record by rejected insertBulk`, async function() {
746
+ assert.strict.deepStrictEqual(vget[6], vans[6])
747
+ })
748
+
749
+ vans[7] = true
750
+ it(`should get ${JSON.stringify(vans[7])} for reject of insertBulk with duplicated id in same batch`, async function() {
751
+ assert.strict.deepStrictEqual(vget[7], vans[7])
752
+ })
753
+
754
+ vans[8] = 0
755
+ it(`should get ${JSON.stringify(vans[8])} for no record added after insertBulk with duplicated id`, async function() {
756
+ assert.strict.deepStrictEqual(vget[8], vans[8])
757
+ })
758
+
759
+ vans[9] = [null, null]
760
+ it(`should get ${JSON.stringify(vans[9])} for not writing any record by duplicated id in same batch`, async function() {
761
+ assert.strict.deepStrictEqual(vget[9], vans[9])
762
+ })
763
+
764
+ vans[10] = { n: 1, nInserted: 1, ok: 1 }
765
+ it(`should get ${JSON.stringify(vans[10])} for insertBulk with single object`, async function() {
766
+ assert.strict.deepStrictEqual(vget[10], vans[10])
767
+ })
768
+
769
+ vans[11] = { n: 0, nInserted: 0, ok: 1 }
770
+ it(`should get ${JSON.stringify(vans[11])} for insertBulk with invalid data`, async function() {
771
+ assert.strict.deepStrictEqual(vget[11], vans[11])
772
+ })
773
+
774
+ vans[12] = ['n,nInserted,ok', 'n,nInserted,ok', 'n,nInserted,ok', 'n,nInserted,ok']
775
+ it(`should get ${JSON.stringify(vans[12])} for same key set between insertBulk and insert`, async function() {
776
+ assert.strict.deepStrictEqual(vget[12], vans[12])
777
+ })
778
+
779
+ vans[13] = { n: 2, nInserted: 2, ok: 1 }
780
+ it(`should get ${JSON.stringify(vans[13])} for insertBulk without id by default autoGenPk`, async function() {
781
+ assert.strict.deepStrictEqual(vget[13], vans[13])
782
+ })
783
+
784
+ vans[14] = 'Error: invalid data[1].id, autoGenPk is false'
785
+ it(`should get ${JSON.stringify(vans[14])} for insertBulk without id by autoGenPk=false`, async function() {
786
+ assert.strict.deepStrictEqual(vget[14], vans[14])
787
+ })
788
+
789
+ vans[15] = null
790
+ it(`should get ${JSON.stringify(vans[15])} for not writing valid records in rejected insertBulk by autoGenPk`, async function() {
791
+ assert.strict.deepStrictEqual(vget[15], vans[15])
792
+ })
793
+
794
+ vans[16] = ['change:insertBulk', 'error:insertBulk']
795
+ it(`should get ${JSON.stringify(vans[16])} for events of insertBulk`, async function() {
796
+ assert.strict.deepStrictEqual(vget[16], vans[16])
797
+ })
798
+
799
+ })
800
+
801
+
602
802
  describe('save', function() {
603
803
  let vans = {}
604
804
  let vget = {}