w-orm-mongodb 1.1.37 → 1.1.39

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,827 +1,1263 @@
1
- import events from 'events'
2
- import mongodb from 'mongodb'
3
- import stream from 'stream'
4
- import cloneDeep from 'lodash-es/cloneDeep.js'
5
- import get from 'lodash-es/get.js'
6
- import map from 'lodash-es/map.js'
7
- import omit from 'lodash-es/omit.js'
8
- import size from 'lodash-es/size.js'
9
- import genPm from 'wsemi/src/genPm.mjs'
10
- import genID from 'wsemi/src/genID.mjs'
11
- import isestr from 'wsemi/src/isestr.mjs'
12
- import isarr from 'wsemi/src/isarr.mjs'
13
- import isearr from 'wsemi/src/isearr.mjs'
14
- import iseobj from 'wsemi/src/iseobj.mjs'
15
- import pmSeries from 'wsemi/src/pmSeries.mjs'
16
-
17
-
18
- // //optMGConn
19
- // let optMGConn = {
20
- // useNewUrlParser: true,
21
- // useUnifiedTopology: true
22
- // }
23
-
24
-
25
- /**
26
- * 操作資料庫(MongoDB)
27
- *
28
- * @class
29
- * @param {Object} [opt={}] 輸入設定物件,預設{}
30
- * @param {String} [opt.url='mongodb://127.0.0.1:27017'] 輸入連接資料庫字串,預設'mongodb://127.0.0.1:27017'
31
- * @param {String} [opt.db='worm'] 輸入使用資料庫名稱字串,預設'worm'
32
- * @param {String} [opt.cl='test'] 輸入使用資料表名稱字串,預設'test'
33
- * @returns {Object} 回傳操作資料庫物件,各事件功能詳見說明
34
- */
35
- function WOrmMongodb(opt = {}) {
36
-
37
-
38
- //default
39
- if (!opt.url) {
40
- opt.url = 'mongodb://127.0.0.1:27017'
41
- }
42
- if (!opt.db) {
43
- opt.db = 'worm'
44
- }
45
- if (!opt.cl) {
46
- opt.cl = 'test'
47
- }
48
-
49
-
50
- //ee
51
- let ee = new events.EventEmitter()
52
-
53
-
54
- //MongoClient
55
- let MongoClient = mongodb.MongoClient
56
-
57
-
58
- /**
59
- * 查詢數據
60
- *
61
- * @memberOf WOrmMongodb
62
- * @param {Object} [find={}] 輸入查詢條件物件
63
- * @returns {Promise} 回傳Promise,resolve回傳數據,reject回傳錯誤訊息
64
- */
65
- async function select(find = {}) {
66
- let isErr = false
67
-
68
- //client
69
- // let client = await MongoClient.connect(opt.url, optMGConn)
70
- // let client = new MongoClient(opt.url, optMGConn)
71
- let client = new MongoClient(opt.url)
72
-
73
- //res
74
- let res = null
75
- try {
76
-
77
- //database, collection
78
- let database = client.db(opt.db)
79
- let collection = database.collection(opt.cl)
80
-
81
- //find
82
- let cursor = collection.find(find)
83
- //.sort({ $natural: -1 })
84
- //.limit(N)
85
-
86
- //toArray
87
- res = await cursor.toArray()
88
-
89
- //omit
90
- res = map(res, function(v) {
91
- v = omit(v, '_id')
92
- return v
93
- })
94
-
95
- }
96
- catch (err) {
97
- isErr = true
98
- res = err
99
- }
100
- finally {
101
- await client.close()
102
- client = null
103
- }
104
-
105
- if (isErr) {
106
- return Promise.reject(res)
107
- }
108
- return res
109
- }
110
-
111
-
112
- /**
113
- * 插入數據,插入同樣數據會自動產生不同_id,故insert前需自行判斷有無重複
114
- *
115
- * @memberOf WOrmMongodb
116
- * @param {Object|Array} data 輸入數據物件或陣列
117
- * @returns {Promise} 回傳Promise,resolve回傳插入結果,reject回傳錯誤訊息
118
- */
119
- async function insert(data) {
120
- let isErr = false
121
-
122
- //check
123
- if (!iseobj(data) && !isearr(data)) {
124
- return {
125
- n: 0,
126
- nInserted: 0,
127
- ok: 1,
128
- }
129
- }
130
-
131
- //cloneDeep
132
- data = cloneDeep(data)
133
-
134
- //client
135
- // let client = await MongoClient.connect(opt.url, optMGConn)
136
- // let client = new MongoClient(opt.url, optMGConn)
137
- let client = new MongoClient(opt.url)
138
-
139
- //res
140
- let res = null
141
- try {
142
-
143
- //database, collection
144
- let database = client.db(opt.db)
145
- let collection = database.collection(opt.cl)
146
-
147
- //check
148
- if (!isarr(data)) {
149
- data = [data]
150
- }
151
-
152
- //check id
153
- data = map(data, function(v) {
154
- if (!isestr(v.id)) {
155
- v.id = genID()
156
- }
157
- return v
158
- })
159
-
160
- //insertMany
161
- res = await collection.insertMany(data)
162
-
163
- //check
164
- if (res.insertedCount > 0) {
165
-
166
- //res
167
- res = {
168
- n: size(data),
169
- nInserted: res.insertedCount,
170
- ok: res.acknowledged ? 1 : 0,
171
- }
172
-
173
- //emit
174
- ee.emit('change', 'insert', data, res)
175
-
176
- }
177
- else {
178
- isErr = true
179
-
180
- //res
181
- res = `no insert data`
182
-
183
- }
184
-
185
- }
186
- catch (err) {
187
- isErr = true
188
- res = err
189
- }
190
- finally {
191
- await client.close()
192
- client = null
193
- }
194
-
195
- if (isErr) {
196
- return Promise.reject(res)
197
- }
198
- return res
199
- }
200
-
201
-
202
- /**
203
- * 儲存數據
204
- *
205
- * @memberOf WOrmMongodb
206
- * @param {Object|Array} data 輸入數據物件或陣列
207
- * @param {Object} [option={}] 輸入設定物件,預設為{}
208
- * @param {boolean} [option.autoInsert=true] 輸入是否於儲存時發現原本無數據,則自動改以插入處理,預設為true
209
- * @returns {Promise} 回傳Promise,resolve回傳儲存結果,reject回傳錯誤訊息
210
- */
211
- async function save(data, option = {}) {
212
- let isErr = false
213
-
214
- //check
215
- if (!iseobj(data) && !isearr(data)) {
216
- return []
217
- }
218
-
219
- //cloneDeep
220
- data = cloneDeep(data)
221
-
222
- //autoInsert
223
- let autoInsert = get(option, 'autoInsert', true)
224
-
225
- //client
226
- // let client = await MongoClient.connect(opt.url, optMGConn)
227
- // let client = new MongoClient(opt.url, optMGConn)
228
- let client = new MongoClient(opt.url)
229
-
230
- //res
231
- let res = null
232
- try {
233
-
234
- //database, collection
235
- let database = client.db(opt.db)
236
- let collection = database.collection(opt.cl)
237
-
238
- //check
239
- if (!isarr(data)) {
240
- data = [data]
241
- }
242
-
243
- //check id
244
- data = map(data, function(v) {
245
- if (!isestr(v.id)) {
246
- v.id = genID()
247
- }
248
- return v
249
- })
250
-
251
- //pmSeries
252
- res = await pmSeries(data, async(v) => {
253
-
254
- //rest
255
- let rest = null
256
-
257
- //oper
258
- rest = await collection.findOneAndUpdate({ id: v.id }, { $set: v })
259
- // console.log('rest', rest)
260
-
261
- if (iseobj(rest)) {
262
- rest = {
263
- n: 1,
264
- nModified: 1,
265
- ok: 1,
266
- }
267
- }
268
- else {
269
- rest = {
270
- n: 0,
271
- nModified: 0,
272
- ok: 1,
273
- }
274
- }
275
-
276
- //autoInsert
277
- if (autoInsert && rest.n === 0) {
278
- rest = await insert(v)
279
- }
280
-
281
- return rest
282
- })
283
-
284
- //emit
285
- ee.emit('change', 'save', data, res)
286
-
287
- }
288
- catch (err) {
289
- isErr = true
290
- res = err
291
- }
292
- finally {
293
- await client.close()
294
- client = null
295
- }
296
-
297
- if (isErr) {
298
- return Promise.reject(res)
299
- }
300
- return res
301
- }
302
-
303
-
304
- /**
305
- * 刪除數據
306
- *
307
- * @memberOf WOrmMongodb
308
- * @param {Object|Array} data 輸入數據物件或陣列
309
- * @returns {Promise} 回傳Promise,resolve回傳刪除結果,reject回傳錯誤訊息
310
- */
311
- async function del(data) {
312
- let isErr = false
313
-
314
- //check
315
- if (!iseobj(data) && !isearr(data)) {
316
- return []
317
- }
318
-
319
- //cloneDeep
320
- data = cloneDeep(data)
321
-
322
- //client
323
- // let client = await MongoClient.connect(opt.url, optMGConn)
324
- // let client = new MongoClient(opt.url, optMGConn)
325
- let client = new MongoClient(opt.url)
326
-
327
- //res
328
- let res = null
329
- try {
330
-
331
- //database, collection
332
- let database = client.db(opt.db)
333
- let collection = database.collection(opt.cl)
334
-
335
- //check
336
- if (!isarr(data)) {
337
- data = [data]
338
- }
339
-
340
- //pmSeries
341
- res = await pmSeries(data, async(v) => {
342
-
343
- //rest
344
- let rest = null
345
-
346
- //deleteOne
347
- rest = await collection.deleteOne({ id: v.id })
348
-
349
- //rest
350
- rest = {
351
- n: rest.deletedCount,
352
- nDeleted: rest.deletedCount,
353
- ok: rest.acknowledged ? 1 : 0,
354
- }
355
-
356
- return rest
357
- })
358
-
359
- //emit
360
- ee.emit('change', 'del', data, res)
361
-
362
- }
363
- catch (err) {
364
- isErr = true
365
- res = err
366
- }
367
- finally {
368
- await client.close()
369
- client = null
370
- }
371
-
372
- if (isErr) {
373
- return Promise.reject(res)
374
- }
375
- return res
376
- }
377
-
378
-
379
- /**
380
- * 刪除全部數據,需與del分開,避免未傳數據導致直接刪除全表
381
- *
382
- * @memberOf WOrmMongodb
383
- * @param {Object} [find={}] 輸入刪除條件物件
384
- * @returns {Promise} 回傳Promise,resolve回傳刪除結果,reject回傳錯誤訊息
385
- */
386
- async function delAll(find = {}) {
387
- let isErr = false
388
-
389
- //client
390
- // let client = await MongoClient.connect(opt.url, optMGConn)
391
- // let client = new MongoClient(opt.url, optMGConn)
392
- let client = new MongoClient(opt.url)
393
-
394
- //res
395
- let res = null
396
- try {
397
-
398
- //database, collection
399
- let database = client.db(opt.db)
400
- let collection = database.collection(opt.cl)
401
-
402
- //deleteMany
403
- res = await collection.deleteMany(find)
404
-
405
- //res
406
- res = {
407
- n: res.deletedCount,
408
- nDeleted: res.deletedCount,
409
- ok: res.acknowledged ? 1 : 0,
410
- }
411
-
412
- //emit
413
- ee.emit('change', 'delAll', null, res)
414
-
415
- }
416
- catch (err) {
417
- isErr = true
418
- res = err
419
- }
420
- finally {
421
- await client.close()
422
- client = null
423
- }
424
-
425
-
426
- if (isErr) {
427
- return Promise.reject(res)
428
- }
429
- return res
430
- }
431
-
432
-
433
- /**
434
- * 使用GridFS,插入數據,需為Uint8Array格式
435
- *
436
- * @memberOf WOrmMongodb
437
- * @param {Uint8Array} u8a
438
- * @returns {Promise} 回傳Promise,resolve回傳插入結果,reject回傳錯誤訊息
439
- */
440
- async function insertGfs(u8a) {
441
- let isErr = false
442
-
443
- //id
444
- let id = genID()
445
-
446
- //buf
447
- let buf = Buffer.from(u8a)
448
-
449
- //client
450
- // let client = await MongoClient.connect(opt.url, optMGConn)
451
- // let client = new MongoClient(opt.url, optMGConn)
452
- let client = new MongoClient(opt.url)
453
-
454
- //core
455
- let core = async (id, buf) => {
456
-
457
- //pm
458
- let pm = genPm()
459
-
460
- //database
461
- let database = client.db(opt.db)
462
-
463
- //bucket
464
- let bucket = new mongodb.GridFSBucket(database, {
465
- chunkSizeBytes: 10 * 1024 * 1024, //10mb
466
- bucketName: opt.cl
467
- })
468
-
469
- //stream
470
- let sm = new stream.Readable()
471
- sm._read = () => {}
472
- sm.push(buf)
473
- sm.push(null)
474
- sm.pipe(bucket.openUploadStream(id)) //pipe是接bucket的Writable, 所以會監聽finish
475
- .on('error', function(err) {
476
-
477
- //reject
478
- pm.reject(err)
479
-
480
- })
481
- .on('finish', function() {
482
-
483
- //res
484
- let res = { n: 1, ok: 1, id }
485
-
486
- //resolve
487
- pm.resolve(res)
488
-
489
- })
490
-
491
- return pm
492
- }
493
-
494
- //res
495
- let res = null
496
- try {
497
-
498
- //core
499
- res = await core(id, buf)
500
-
501
- //emit
502
- ee.emit('change', 'insertGfs', null, res)
503
-
504
- }
505
- catch (err) {
506
- isErr = true
507
- res = err
508
- }
509
- finally {
510
- await client.close()
511
- client = null
512
- }
513
-
514
- if (isErr) {
515
- return Promise.reject(res)
516
- }
517
- return res
518
- }
519
-
520
-
521
- /**
522
- * 使用GridFS,查詢數據
523
- *
524
- * @memberOf WOrmMongodb
525
- * @param {String} id 輸入查詢id字串
526
- * @returns {Promise} 回傳Promise,resolve回傳數據,reject回傳錯誤訊息
527
- */
528
- async function selectGfs(id) {
529
- let isErr = false
530
-
531
- //client
532
- // let client = await MongoClient.connect(opt.url, optMGConn)
533
- // let client = new MongoClient(opt.url, optMGConn)
534
- let client = new MongoClient(opt.url)
535
-
536
- //core
537
- let core = async (id) => {
538
-
539
- //pm
540
- let pm = genPm()
541
-
542
- //database
543
- let database = client.db(opt.db)
544
-
545
- //bucket
546
- let bucket = new mongodb.GridFSBucket(database, {
547
- chunkSizeBytes: 10 * 1024 * 1024, //10mb
548
- bucketName: opt.cl
549
- })
550
-
551
- //buf
552
- let buf = Buffer.from('')
553
-
554
- //stream
555
- let sm = bucket.openDownloadStreamByName(id)
556
- sm.on('data', function (chunk) {
557
- buf = Buffer.concat([buf, chunk])
558
- })
559
- sm.on('error', function (err) {
560
-
561
- //reject
562
- pm.reject(err)
563
-
564
- })
565
- sm.on('end', function () {
566
-
567
- //u8a
568
- let u8a = new Uint8Array(buf)
569
-
570
- //clean memory
571
- buf = null
572
-
573
- //resolve
574
- pm.resolve(u8a)
575
-
576
- })
577
-
578
- return pm
579
- }
580
-
581
- //res
582
- let res = null
583
- try {
584
-
585
- //core
586
- res = await core(id)
587
-
588
- }
589
- catch (err) {
590
- isErr = true
591
- res = err
592
- }
593
- finally {
594
- await client.close()
595
- client = null
596
- }
597
-
598
- if (isErr) {
599
- return Promise.reject(res)
600
- }
601
- return res
602
- }
603
-
604
-
605
- async function _findGfs(find = {}, bucket) {
606
- let isErr = false
607
-
608
- //res
609
- let res = null
610
- try {
611
-
612
- //find
613
- let cursor = bucket.find(find)
614
-
615
- //toArray
616
- res = await cursor.toArray()
617
-
618
- }
619
- catch (err) {
620
- isErr = true
621
- res = err
622
- }
623
- finally {
624
- // await client.close()
625
- }
626
-
627
- if (isErr) {
628
- return Promise.reject(res)
629
- }
630
- return res
631
- }
632
-
633
-
634
- /**
635
- * 使用GridFS,刪除數據
636
- *
637
- * @memberOf WOrmMongodb
638
- * @param {String} id 輸入刪除id字串
639
- * @returns {Promise} 回傳Promise,resolve回傳刪除結果,reject回傳錯誤訊息
640
- */
641
- async function delGfs(id) {
642
- let isErr = false
643
-
644
- //client
645
- // let client = await MongoClient.connect(opt.url, optMGConn)
646
- // let client = new MongoClient(opt.url, optMGConn)
647
- let client = new MongoClient(opt.url)
648
-
649
- //res
650
- let res = null
651
- try {
652
-
653
- //database, collection
654
- let database = client.db(opt.db)
655
-
656
- //bucket
657
- let bucket = new mongodb.GridFSBucket(database, {
658
- chunkSizeBytes: 10 * 1024 * 1024, //10mb
659
- bucketName: opt.cl
660
- })
661
-
662
- //_findGfs
663
- res = await _findGfs({ filename: id }, bucket)
664
-
665
- //check
666
- if (res.length === 0) {
667
- res = `can not find id[${id}]`
668
- }
669
- else if (res.length > 1) {
670
- res = `duplicate id[${id}]`
671
- }
672
- else {
673
-
674
- //bid, get _id from res[0]
675
- let bid = res[0]._id
676
-
677
- //delete
678
- res = await bucket.delete(bid)
679
-
680
- //res
681
- res = {
682
- n: 1,
683
- nDeleted: 1,
684
- ok: 1,
685
- }
686
-
687
- //emit
688
- ee.emit('change', 'delGfs', null, res)
689
-
690
- }
691
-
692
- }
693
- catch (err) {
694
- isErr = true
695
- res = err
696
- }
697
- finally {
698
- await client.close()
699
- client = null
700
- }
701
-
702
- if (isErr) {
703
- return Promise.reject(res)
704
- }
705
- return res
706
- }
707
-
708
-
709
- async function _delGfs(bid, bucket) {
710
- let isErr = false
711
-
712
- //res
713
- let res = null
714
- try {
715
-
716
- //delete
717
- res = await bucket.delete(bid)
718
-
719
- //res
720
- res = {
721
- n: 1,
722
- nDeleted: 1,
723
- ok: 1,
724
- }
725
-
726
- }
727
- catch (err) {
728
- isErr = true
729
- res = err
730
- }
731
- finally {
732
- // await client.close()
733
- }
734
-
735
- if (isErr) {
736
- return Promise.reject(res)
737
- }
738
- return res
739
- }
740
-
741
-
742
- /**
743
- * 使用GridFS,刪除全部數據,需與del分開,避免未傳數據導致直接刪除全表
744
- *
745
- * @memberOf WOrmMongodb
746
- * @param {Object} [find={}] 輸入刪除條件物件
747
- * @returns {Promise} 回傳Promise,resolve回傳刪除結果,reject回傳錯誤訊息
748
- */
749
- async function delAllGfs(find = {}) {
750
- let isErr = false
751
-
752
- //client
753
- // let client = await MongoClient.connect(opt.url, optMGConn)
754
- // let client = new MongoClient(opt.url, optMGConn)
755
- let client = new MongoClient(opt.url)
756
-
757
- //res
758
- let res = null
759
- try {
760
-
761
- //database, collection
762
- let database = client.db(opt.db)
763
-
764
- //bucket
765
- let bucket = new mongodb.GridFSBucket(database, {
766
- chunkSizeBytes: 10 * 1024 * 1024, //10mb
767
- bucketName: opt.cl
768
- })
769
-
770
- //_findGfs
771
- res = await _findGfs(find, bucket)
772
-
773
- //n
774
- let n = size(res)
775
-
776
- //ps
777
- let ps = map(res, function(v) {
778
- let bid = v._id
779
- return _delGfs(bid, bucket)
780
- })
781
-
782
- //all
783
- res = await Promise.all(ps)
784
-
785
- //res
786
- res = {
787
- n,
788
- ok: 1
789
- }
790
-
791
- //emit
792
- ee.emit('change', 'delAllGfs', null, res)
793
-
794
- }
795
- catch (err) {
796
- isErr = true
797
- res = err
798
- }
799
- finally {
800
- await client.close()
801
- client = null
802
- }
803
-
804
- if (isErr) {
805
- return Promise.reject(res)
806
- }
807
- return res
808
- }
809
-
810
-
811
- //bind
812
- ee.select = select
813
- ee.insert = insert
814
- ee.save = save
815
- ee.del = del
816
- ee.delAll = delAll
817
- ee.selectGfs = selectGfs
818
- ee.insertGfs = insertGfs
819
- ee.delGfs = delGfs
820
- ee.delAllGfs = delAllGfs
821
-
822
-
823
- return ee
824
- }
825
-
826
-
827
- export default WOrmMongodb
1
+ import events from 'events'
2
+ import mongodb from 'mongodb'
3
+ import stream from 'stream'
4
+ import cloneDeep from 'lodash-es/cloneDeep.js'
5
+ import every from 'lodash-es/every.js'
6
+ import get from 'lodash-es/get.js'
7
+ import map from 'lodash-es/map.js'
8
+ import omit from 'lodash-es/omit.js'
9
+ import size from 'lodash-es/size.js'
10
+ import genPm from 'wsemi/src/genPm.mjs'
11
+ import genID from 'wsemi/src/genID.mjs'
12
+ import isestr from 'wsemi/src/isestr.mjs'
13
+ import isarr from 'wsemi/src/isarr.mjs'
14
+ import isearr from 'wsemi/src/isearr.mjs'
15
+ import iseobj from 'wsemi/src/iseobj.mjs'
16
+ import isu8arr from 'wsemi/src/isu8arr.mjs'
17
+ import pmSeries from 'wsemi/src/pmSeries.mjs'
18
+
19
+
20
+ /**
21
+ * 操作資料庫(MongoDB)
22
+ *
23
+ * 本套件之主鍵欄位固定為id,尚未支援由呼叫端指定。
24
+ * id為無業務語義之識別碼,故insert與save於輸入未帶有效id時自動補值;del不補值。
25
+ *
26
+ * @class
27
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
28
+ * @param {String} [opt.url='mongodb://127.0.0.1:27017'] 輸入連接資料庫字串,預設'mongodb://127.0.0.1:27017'
29
+ * @param {String} [opt.db='worm'] 輸入使用資料庫名稱字串,預設'worm'
30
+ * @param {String} [opt.cl='test'] 輸入使用資料表名稱字串,預設'test'
31
+ * @returns {Object} 回傳操作資料庫物件,各事件功能詳見說明
32
+ */
33
+ function WOrmMongodb(opt = {}) {
34
+
35
+
36
+ //default
37
+ if (!opt.url) {
38
+ opt.url = 'mongodb://127.0.0.1:27017'
39
+ }
40
+ if (!opt.db) {
41
+ opt.db = 'worm'
42
+ }
43
+ if (!opt.cl) {
44
+ opt.cl = 'test'
45
+ }
46
+
47
+
48
+ //_indexReady, 唯一索引只須建立一次, 以旗標記錄避免每次操作皆多一次round-trip
49
+ let _indexReady = false
50
+
51
+
52
+ //_indexGfsReady, GridFS之唯一索引亦只須建立一次
53
+ let _indexGfsReady = false
54
+
55
+
56
+ //ee
57
+ let ee = new events.EventEmitter()
58
+
59
+
60
+ //MongoClient
61
+ let MongoClient = mongodb.MongoClient
62
+
63
+
64
+ /**
65
+ * 取錯誤訊息字串,供逐筆結果之err欄位使用
66
+ *
67
+ * @ignore
68
+ * @param {Error|String} err 輸入錯誤物件或字串
69
+ * @returns {String} 回傳錯誤訊息字串
70
+ */
71
+ function genErrMsg(err) {
72
+
73
+ //message
74
+ let message = get(err, 'message')
75
+ if (isestr(message)) {
76
+ return message
77
+ }
78
+
79
+ return String(err)
80
+ }
81
+
82
+
83
+ /**
84
+ * 發出change事件,訂閱函數拋錯不得影響本次操作之結果,故另包try並自行吞掉
85
+ *
86
+ * @ignore
87
+ * @param {String} mode 輸入操作模式字串
88
+ * @param {Array|null} data 輸入本次操作之數據
89
+ * @param {Object|Array} res 輸入本次操作之結果
90
+ * @returns {undefined} 無回傳值
91
+ */
92
+ function emitChange(mode, data, res) {
93
+ try {
94
+ ee.emit('change', mode, data, res)
95
+ }
96
+ catch (err) {
97
+ console.log(err)
98
+ }
99
+ }
100
+
101
+
102
+ /**
103
+ * 判定是否為唯一索引重複鍵錯誤(11000),批次插入時須全部寫入錯誤皆為重複鍵才算
104
+ *
105
+ * @ignore
106
+ * @param {Error} err 輸入錯誤物件
107
+ * @returns {Boolean} 回傳是否為重複鍵錯誤布林值
108
+ */
109
+ function isDupKeyError(err) {
110
+
111
+ //writeErrors, 批次寫入時各筆錯誤置於writeErrors內
112
+ let writeErrors = get(err, 'writeErrors')
113
+ if (isearr(writeErrors)) {
114
+ return every(writeErrors, function(v) {
115
+ return get(v, 'code') === 11000 || get(v, 'err.code') === 11000
116
+ })
117
+ }
118
+
119
+ return get(err, 'code') === 11000
120
+ }
121
+
122
+
123
+ /**
124
+ * 於id欄位建立唯一索引,令[檢查id是否存在]與[寫入]得由MongoDB於單一文件操作內原子完成
125
+ * 註: 若既有資料表內已存在重複id,建立索引會失敗並將錯誤往外拋,須先自行清除重複數據,
126
+ * 此錯誤影響本次操作之全部數據,屬整批性錯誤,故不轉為逐筆之ok為0
127
+ *
128
+ * @ignore
129
+ * @param {Object} collection 輸入資料表物件
130
+ * @returns {Promise} 回傳Promise,resolve回傳undefined,reject回傳錯誤訊息
131
+ */
132
+ async function ensureIndex(collection) {
133
+
134
+ //check
135
+ if (_indexReady) {
136
+ return
137
+ }
138
+
139
+ //createIndex, 已存在同樣索引時MongoDB不會重建亦不報錯
140
+ await collection.createIndex({ id: 1 }, { unique: true })
141
+
142
+ //_indexReady
143
+ _indexReady = true
144
+
145
+ }
146
+
147
+
148
+ /**
149
+ * 使用GridFS,於filename欄位建立唯一索引,令[檢查id是否存在]與[寫入]由MongoDB判定而非由本套件先讀後寫
150
+ * 註: GridFS以files資料表之filename存放id,其本身不具唯一性,須另建唯一索引方能達成[已存在則跳過];
151
+ * 若既有資料表內已存在重複id,建立索引會失敗並將錯誤往外拋,須先自行清除重複數據
152
+ *
153
+ * @ignore
154
+ * @param {Object} database 輸入資料庫物件
155
+ * @returns {Promise} 回傳Promise,resolve回傳undefined,reject回傳錯誤訊息
156
+ */
157
+ async function ensureIndexGfs(database) {
158
+
159
+ //check
160
+ if (_indexGfsReady) {
161
+ return
162
+ }
163
+
164
+ //createIndex, 已存在同樣索引時MongoDB不會重建亦不報錯
165
+ await database.collection(`${opt.cl}.files`).createIndex({ filename: 1 }, { unique: true })
166
+
167
+ //_indexGfsReady
168
+ _indexGfsReady = true
169
+
170
+ }
171
+
172
+
173
+ /**
174
+ * 使用GridFS,上傳單筆數據
175
+ * 註: 已存在同一id時MongoDB以重複鍵錯誤拒絕,惟chunks於files文件之前即已寫入,
176
+ * 故須以files_id清除該次所遺留之chunks,避免累積孤兒數據
177
+ *
178
+ * @ignore
179
+ * @param {Object} bucket 輸入GridFSBucket物件
180
+ * @param {Object} clChunks 輸入GridFS之chunks資料表物件
181
+ * @param {String} id 輸入id字串
182
+ * @param {Uint8Array} u8a 輸入數據Uint8Array
183
+ * @returns {Promise} 回傳Promise,resolve回傳本次是否已插入布林值,reject回傳錯誤訊息
184
+ */
185
+ function _uploadGfs(bucket, clChunks, id, u8a) {
186
+
187
+ //pm
188
+ let pm = genPm()
189
+
190
+ //ws, files_id於finish前即可取得, 供清除孤兒chunks用
191
+ let ws = bucket.openUploadStream(id)
192
+ let fid = ws.id
193
+
194
+ //stream
195
+ let sm = new stream.Readable()
196
+ sm._read = () => {}
197
+ sm.push(Buffer.from(u8a))
198
+ sm.push(null)
199
+ sm.pipe(ws) //pipe是接bucket的Writable, 所以會監聽finish
200
+ .on('error', function(err) {
201
+
202
+ //check, 重複鍵錯誤表示已存在同一id而跳過, 屬正常結果而非錯誤
203
+ if (!isDupKeyError(err)) {
204
+ pm.reject(err)
205
+ return
206
+ }
207
+
208
+ //清除本次所遺留之孤兒chunks
209
+ clChunks.deleteMany({ files_id: fid })
210
+ .then(function() {
211
+ pm.resolve(false)
212
+ })
213
+ .catch(function(err) {
214
+ pm.reject(err)
215
+ })
216
+
217
+ })
218
+ .on('finish', function() {
219
+
220
+ //resolve
221
+ pm.resolve(true)
222
+
223
+ })
224
+
225
+ return pm
226
+ }
227
+
228
+
229
+ /**
230
+ * 查詢數據
231
+ *
232
+ * @memberOf WOrmMongodb
233
+ * @param {Object} [find={}] 輸入查詢條件物件
234
+ * @returns {Promise} 回傳Promise,resolve回傳數據陣列,無符合數據回傳空陣列,reject回傳錯誤訊息
235
+ */
236
+ async function select(find = {}) {
237
+ let isErr = false
238
+
239
+ //client
240
+ let client = new MongoClient(opt.url)
241
+
242
+ //res
243
+ let res = null
244
+ try {
245
+
246
+ //database, collection
247
+ let database = client.db(opt.db)
248
+ let collection = database.collection(opt.cl)
249
+
250
+ //find
251
+ let cursor = collection.find(find)
252
+
253
+ //toArray
254
+ res = await cursor.toArray()
255
+
256
+ //omit, 去除資料庫內部欄位_id, 令回傳欄位與寫入時所給者一致
257
+ res = map(res, function(v) {
258
+ v = omit(v, '_id')
259
+ return v
260
+ })
261
+
262
+ }
263
+ catch (err) {
264
+ isErr = true
265
+ res = err
266
+ }
267
+ finally {
268
+ await client.close()
269
+ client = null
270
+ }
271
+
272
+ if (isErr) {
273
+ return Promise.reject(res)
274
+ }
275
+ return res
276
+ }
277
+
278
+
279
+ /**
280
+ * 由主鍵查詢單筆數據,因由MongoDB查找且僅回傳單筆,不需如select提取全部符合數據再處理,故數據量大時效能較佳
281
+ * 註: 本套件之主鍵欄位固定為id,尚未支援由呼叫端指定
282
+ * 註: 本函數不得有副作用,故不建立唯一索引
283
+ *
284
+ * @memberOf WOrmMongodb
285
+ * @param {String} pk 輸入主鍵值字串,即數據之id
286
+ * @returns {Promise} 回傳Promise,resolve回傳數據物件,若無此主鍵或主鍵值無效則回傳null,reject回傳錯誤訊息
287
+ */
288
+ async function selectByPk(pk) {
289
+ let isErr = false
290
+
291
+ //check
292
+ if (!isestr(pk)) {
293
+ //未給有效主鍵值視為查無數據, 判定基準與insert、save、del內對id之認定一致
294
+ return null
295
+ }
296
+
297
+ //client
298
+ let client = new MongoClient(opt.url)
299
+
300
+ //res
301
+ let res = null
302
+ try {
303
+
304
+ //database, collection
305
+ let database = client.db(opt.db)
306
+ let collection = database.collection(opt.cl)
307
+
308
+ //findOne, 以投影去除_id, 令回傳形狀與select一致
309
+ let v = await collection.findOne({ id: pk }, { projection: { _id: 0 } })
310
+
311
+ //check, 判定基準與insert、save、del內對既有數據之認定一致
312
+ if (iseobj(v)) {
313
+ res = v
314
+ }
315
+ else {
316
+ //不存在id, 回傳null
317
+ res = null
318
+ }
319
+
320
+ }
321
+ catch (err) {
322
+ isErr = true
323
+ res = err
324
+ }
325
+ finally {
326
+ await client.close()
327
+ client = null
328
+ }
329
+
330
+ if (isErr) {
331
+ return Promise.reject(res)
332
+ }
333
+ return res
334
+ }
335
+
336
+
337
+ /**
338
+ * 插入數據,僅於id不存在時寫入,已存在者跳過且不覆寫
339
+ * 由MongoDB於唯一索引上原子完成[檢查id未存在]與[寫入],併發時同一id僅有一次成功
340
+ * 註: n為輸入筆數,nInserted為實際插入筆數,全數已存在而nInserted為0屬正常結果
341
+ *
342
+ * @memberOf WOrmMongodb
343
+ * @param {Object|Array} data 輸入數據物件或陣列
344
+ * @returns {Promise} 回傳Promise,resolve回傳插入結果,reject回傳錯誤訊息
345
+ */
346
+ async function insert(data) {
347
+ let isErr = false
348
+
349
+ //check
350
+ if (!iseobj(data) && !isearr(data)) {
351
+ return {
352
+ n: 0,
353
+ nInserted: 0,
354
+ ok: 1,
355
+ }
356
+ }
357
+
358
+ //cloneDeep
359
+ data = cloneDeep(data)
360
+
361
+ //client
362
+ let client = new MongoClient(opt.url)
363
+
364
+ //res
365
+ let res = null
366
+ try {
367
+
368
+ //connect, 令連線失敗於寫入前即拋出, 屬影響全部數據之整批性錯誤
369
+ await client.connect()
370
+
371
+ //database, collection
372
+ let database = client.db(opt.db)
373
+ let collection = database.collection(opt.cl)
374
+
375
+ //check
376
+ if (!isarr(data)) {
377
+ data = [data]
378
+ }
379
+
380
+ //check id
381
+ data = map(data, function(v) {
382
+ if (!isestr(v.id)) {
383
+ v.id = genID()
384
+ }
385
+ return v
386
+ })
387
+
388
+ //ensureIndex
389
+ await ensureIndex(collection)
390
+
391
+ //nAll, n之基準為輸入筆數
392
+ let nAll = size(data)
393
+
394
+ //insertMany, ordered:false令已存在id者跳過而不中斷整批插入,
395
+ //同批含重複id時亦僅首筆成功, 故不須逐筆插入即可取得實際插入筆數
396
+ let nInserted = 0
397
+ try {
398
+ let r = await collection.insertMany(data, { ordered: false })
399
+ nInserted = r.insertedCount
400
+ }
401
+ catch (err) {
402
+
403
+ //僅重複鍵錯誤可視為[已存在id而跳過], 其餘錯誤影響全部數據, 須往外拋
404
+ if (!isDupKeyError(err)) {
405
+ throw err
406
+ }
407
+
408
+ //重複鍵錯誤時仍可由result取得實際插入筆數
409
+ nInserted = get(err, 'result.insertedCount', 0)
410
+
411
+ }
412
+
413
+ //res, 全數已存在而nInserted為0屬正常結果, 不視為錯誤
414
+ res = {
415
+ n: nAll,
416
+ nInserted,
417
+ ok: 1,
418
+ }
419
+
420
+ }
421
+ catch (err) {
422
+ isErr = true
423
+ res = err
424
+ }
425
+ finally {
426
+ await client.close()
427
+ client = null
428
+ }
429
+
430
+ //emit
431
+ if (!isErr) {
432
+ emitChange('insert', data, res)
433
+ }
434
+
435
+ if (isErr) {
436
+ return Promise.reject(res)
437
+ }
438
+ return res
439
+ }
440
+
441
+
442
+ /**
443
+ * 儲存單筆數據,由MongoDB於單一updateOne內原子完成[查找id]與[更新或插入]
444
+ * 註: MongoDB單一文件操作本即為原子,故不須開啟transaction(transaction另須replica set,standalone不支援)
445
+ *
446
+ * @ignore
447
+ * @param {Object} collection 輸入資料表物件
448
+ * @param {Object} v 輸入數據物件
449
+ * @param {Boolean} autoInsert 輸入是否於查無數據時自動改以插入處理布林值
450
+ * @returns {Promise} 回傳Promise,resolve回傳本筆儲存結果,本筆失敗時亦resolve並以ok為0回報
451
+ */
452
+ async function saveOne(collection, v, autoInsert) {
453
+
454
+ //nTry, 併發upsert可能因他方同時插入同一id而拋重複鍵錯誤,
455
+ //此時該id已存在, 重試即會走更新路徑, 故給予有限次數重試令其收斂
456
+ let nTry = 3
457
+
458
+ //rest
459
+ let rest = null
460
+
461
+ //inserted, 供結果定案後發出insert事件用
462
+ let inserted = false
463
+
464
+ for (let i = 0; i < nTry; i++) {
465
+
466
+ try {
467
+
468
+ //updateOne, upsert僅於autoInsert時開啟, 未開啟時查無數據即不寫入以免無中生有,
469
+ //由MongoDB於單一操作內原子完成[查找id]與[更新或插入],
470
+ //併發時不會有兩方各自讀到查無數據再各自插入而產生重複id, 亦不會有讀改寫之遺失更新
471
+ let r = await collection.updateOne({ id: v.id }, { $set: v }, {
472
+ upsert: autoInsert,
473
+ })
474
+
475
+ //matchedCount, modifiedCount, upsertedCount
476
+ let matchedCount = get(r, 'matchedCount', 0)
477
+ let modifiedCount = get(r, 'modifiedCount', 0)
478
+ let upsertedCount = get(r, 'upsertedCount', 0)
479
+
480
+ if (upsertedCount > 0) {
481
+ //原不存在而插入
482
+
483
+ //rest
484
+ rest = {
485
+ n: 1,
486
+ nInserted: 1,
487
+ nModified: 0,
488
+ ok: 1,
489
+ }
490
+
491
+ //inserted
492
+ inserted = true
493
+
494
+ }
495
+ else if (matchedCount > 0) {
496
+ //原已存在, 合併後內容有變更方為更新
497
+ //由MongoDB於伺服器端將待寫入物件合併進現值後與現值比對, 結果相同者不寫入而回modifiedCount為0,
498
+ //故僅給部份欄位且該些欄位值皆與現值相同時, 合併結果等於現值, nModified亦為0,
499
+ //係於同一原子操作內完成比對與寫入, 故不須另行預讀, 亦無預讀值過期之疑慮
500
+
501
+ //rest
502
+ rest = {
503
+ n: 1,
504
+ nInserted: 0,
505
+ nModified: modifiedCount > 0 ? 1 : 0,
506
+ ok: 1,
507
+ }
508
+
509
+ }
510
+ else {
511
+ //查無數據且未開啟autoInsert
512
+
513
+ //rest
514
+ rest = {
515
+ n: 0,
516
+ nInserted: 0,
517
+ nModified: 0,
518
+ ok: 1,
519
+ }
520
+
521
+ }
522
+
523
+ break
524
+ }
525
+ catch (err) {
526
+
527
+ //重複鍵錯誤且未達重試上限則重試
528
+ if (isDupKeyError(err) && i < nTry - 1) {
529
+ continue
530
+ }
531
+
532
+ //其餘視為本筆失敗, 不中斷整批, 由呼叫端以ok與err判讀
533
+
534
+ //rest
535
+ rest = {
536
+ n: 1,
537
+ nInserted: 0,
538
+ nModified: 0,
539
+ ok: 0,
540
+ err: genErrMsg(err),
541
+ }
542
+
543
+ break
544
+ }
545
+
546
+ }
547
+
548
+ //emit, 須於結果定案後發出, 避免訂閱函數拋錯影響本筆結果
549
+ if (inserted) {
550
+ emitChange('insert', [v], rest)
551
+ }
552
+
553
+ return rest
554
+ }
555
+
556
+
557
+ /**
558
+ * 儲存數據,以id為準更新既有數據,未給之欄位會保留;id不存在且option.autoInsert為true時改為插入
559
+ * 註: n為id命中筆數,命中或經插入而產生皆為1;[內容相同]之判定基準為將待儲存物件合併進現值後結果與現值相同,
560
+ * 相同者不寫入而nModified為0;本筆失敗不中斷整批,該筆以ok為0並附err回報
561
+ *
562
+ * @memberOf WOrmMongodb
563
+ * @param {Object|Array} data 輸入數據物件或陣列
564
+ * @param {Object} [option={}] 輸入設定物件,預設為{}
565
+ * @param {boolean} [option.autoInsert=true] 輸入是否於儲存時發現原本無數據,則自動改以插入處理,預設為true
566
+ * @returns {Promise} 回傳Promise,resolve回傳與輸入等長之儲存結果陣列,reject回傳錯誤訊息
567
+ */
568
+ async function save(data, option = {}) {
569
+ let isErr = false
570
+
571
+ //check
572
+ if (!iseobj(data) && !isearr(data)) {
573
+ return []
574
+ }
575
+
576
+ //cloneDeep
577
+ data = cloneDeep(data)
578
+
579
+ //autoInsert
580
+ let autoInsert = get(option, 'autoInsert', true)
581
+
582
+ //client
583
+ let client = new MongoClient(opt.url)
584
+
585
+ //res
586
+ let res = null
587
+ try {
588
+
589
+ //connect, 令連線失敗於寫入前即拋出, 屬影響全部數據之整批性錯誤
590
+ await client.connect()
591
+
592
+ //database, collection
593
+ let database = client.db(opt.db)
594
+ let collection = database.collection(opt.cl)
595
+
596
+ //check
597
+ if (!isarr(data)) {
598
+ data = [data]
599
+ }
600
+
601
+ //check id
602
+ data = map(data, function(v) {
603
+ if (!isestr(v.id)) {
604
+ v.id = genID()
605
+ }
606
+ return v
607
+ })
608
+
609
+ //ensureIndex
610
+ await ensureIndex(collection)
611
+
612
+ //pmSeries
613
+ res = await pmSeries(data, async(v) => {
614
+ return saveOne(collection, v, autoInsert)
615
+ })
616
+
617
+ }
618
+ catch (err) {
619
+ isErr = true
620
+ res = err
621
+ }
622
+ finally {
623
+ await client.close()
624
+ client = null
625
+ }
626
+
627
+ //emit
628
+ if (!isErr) {
629
+ emitChange('save', data, res)
630
+ }
631
+
632
+ if (isErr) {
633
+ return Promise.reject(res)
634
+ }
635
+ return res
636
+ }
637
+
638
+
639
+ /**
640
+ * 刪除數據
641
+ * 註: n為id命中筆數;未帶有效id者視為本筆無法處理,不送查詢條件且以ok為0並附err回報;
642
+ * 判斷本筆是否真的被刪除一律以nDeleted為準
643
+ *
644
+ * @memberOf WOrmMongodb
645
+ * @param {Object|Array} data 輸入數據物件或陣列
646
+ * @returns {Promise} 回傳Promise,resolve回傳與輸入等長之刪除結果陣列,reject回傳錯誤訊息
647
+ */
648
+ async function del(data) {
649
+ let isErr = false
650
+
651
+ //check
652
+ if (!iseobj(data) && !isearr(data)) {
653
+ return []
654
+ }
655
+
656
+ //cloneDeep
657
+ data = cloneDeep(data)
658
+
659
+ //client
660
+ let client = new MongoClient(opt.url)
661
+
662
+ //res
663
+ let res = null
664
+ try {
665
+
666
+ //connect, 令連線失敗於刪除前即拋出, 屬影響全部數據之整批性錯誤
667
+ await client.connect()
668
+
669
+ //database, collection
670
+ let database = client.db(opt.db)
671
+ let collection = database.collection(opt.cl)
672
+
673
+ //check
674
+ if (!isarr(data)) {
675
+ data = [data]
676
+ }
677
+
678
+ //pmSeries
679
+ res = await pmSeries(data, async(v) => {
680
+
681
+ //id, del不補值, 未帶有效id者視為本筆無法處理
682
+ let id = get(v, 'id')
683
+
684
+ //check, 不得將無效id送進查詢條件, 因undefined經序列化為null會誤中id為null之數據
685
+ if (!isestr(id)) {
686
+ return {
687
+ n: 0,
688
+ nDeleted: 0,
689
+ ok: 0,
690
+ err: `invalid id[${id}]`,
691
+ }
692
+ }
693
+
694
+ //rest
695
+ let rest = null
696
+
697
+ try {
698
+
699
+ //deleteOne
700
+ let r = await collection.deleteOne({ id })
701
+
702
+ //nDeleted
703
+ let nDeleted = get(r, 'deletedCount', 0)
704
+
705
+ //rest, 未命中時nDeleted為0, 屬正常結果
706
+ rest = {
707
+ n: nDeleted,
708
+ nDeleted,
709
+ ok: 1,
710
+ }
711
+
712
+ }
713
+ catch (err) {
714
+
715
+ //本筆失敗不中斷整批
716
+
717
+ //rest
718
+ rest = {
719
+ n: 1,
720
+ nDeleted: 0,
721
+ ok: 0,
722
+ err: genErrMsg(err),
723
+ }
724
+
725
+ }
726
+
727
+ return rest
728
+ })
729
+
730
+ }
731
+ catch (err) {
732
+ isErr = true
733
+ res = err
734
+ }
735
+ finally {
736
+ await client.close()
737
+ client = null
738
+ }
739
+
740
+ //emit
741
+ if (!isErr) {
742
+ emitChange('del', data, res)
743
+ }
744
+
745
+ if (isErr) {
746
+ return Promise.reject(res)
747
+ }
748
+ return res
749
+ }
750
+
751
+
752
+ /**
753
+ * 刪除全部數據,需與del分開,避免未傳數據導致直接刪除全表
754
+ * 註: n為實際刪除筆數,恆等於nDeleted;條件無命中時回傳0亦屬正常結果
755
+ *
756
+ * @memberOf WOrmMongodb
757
+ * @param {Object} [find={}] 輸入刪除條件物件
758
+ * @returns {Promise} 回傳Promise,resolve回傳刪除結果,reject回傳錯誤訊息
759
+ */
760
+ async function delAll(find = {}) {
761
+ let isErr = false
762
+
763
+ //client
764
+ let client = new MongoClient(opt.url)
765
+
766
+ //res
767
+ let res = null
768
+ try {
769
+
770
+ //database, collection
771
+ let database = client.db(opt.db)
772
+ let collection = database.collection(opt.cl)
773
+
774
+ //deleteMany
775
+ let r = await collection.deleteMany(find)
776
+
777
+ //nDeleted
778
+ let nDeleted = get(r, 'deletedCount', 0)
779
+
780
+ //res
781
+ res = {
782
+ n: nDeleted,
783
+ nDeleted,
784
+ ok: 1,
785
+ }
786
+
787
+ }
788
+ catch (err) {
789
+ isErr = true
790
+ res = err
791
+ }
792
+ finally {
793
+ await client.close()
794
+ client = null
795
+ }
796
+
797
+ //emit
798
+ if (!isErr) {
799
+ emitChange('delAll', null, res)
800
+ }
801
+
802
+ if (isErr) {
803
+ return Promise.reject(res)
804
+ }
805
+ return res
806
+ }
807
+
808
+
809
+ /**
810
+ * 使用GridFS,插入數據,僅於id不存在時寫入,已存在者跳過且不覆寫
811
+ * 數據物件形狀為{ id, u8a },id未給時自動產生,u8a須為Uint8Array
812
+ * 註: n為輸入筆數,nInserted為實際插入筆數,全數已存在而nInserted為0屬正常結果
813
+ *
814
+ * @memberOf WOrmMongodb
815
+ * @param {Object|Array} data 輸入數據物件或陣列,各數據物件形狀為{ id, u8a }
816
+ * @returns {Promise} 回傳Promise,resolve回傳插入結果,reject回傳錯誤訊息
817
+ */
818
+ async function insertGfs(data) {
819
+ let isErr = false
820
+
821
+ //check
822
+ if (!iseobj(data) && !isearr(data)) {
823
+ return {
824
+ n: 0,
825
+ nInserted: 0,
826
+ ok: 1,
827
+ }
828
+ }
829
+
830
+ //client
831
+ let client = new MongoClient(opt.url)
832
+
833
+ //res
834
+ let res = null
835
+ try {
836
+
837
+ //connect, 令連線失敗於寫入前即拋出, 屬影響全部數據之整批性錯誤
838
+ await client.connect()
839
+
840
+ //database
841
+ let database = client.db(opt.db)
842
+
843
+ //bucket
844
+ let bucket = new mongodb.GridFSBucket(database, {
845
+ chunkSizeBytes: 10 * 1024 * 1024, //10mb
846
+ bucketName: opt.cl
847
+ })
848
+
849
+ //clChunks
850
+ let clChunks = database.collection(`${opt.cl}.chunks`)
851
+
852
+ //check
853
+ if (!isarr(data)) {
854
+ data = [data]
855
+ }
856
+
857
+ //check id與u8a, 僅淺拷貝數據物件而不深拷貝u8a,
858
+ //因u8a僅供讀取, 深拷貝大量二進位數據成本過高;
859
+ //u8a無效屬呼叫端給值錯誤且整批函數無從逐筆回報, 故往外拋
860
+ data = map(data, function(v, k) {
861
+ v = { ...v }
862
+ if (!isestr(v.id)) {
863
+ v.id = genID()
864
+ }
865
+ if (!isu8arr(v.u8a)) {
866
+ throw new Error(`invalid data[${k}].u8a`)
867
+ }
868
+ return v
869
+ })
870
+
871
+ //ensureIndexGfs
872
+ await ensureIndexGfs(database)
873
+
874
+ //nAll, n之基準為輸入筆數
875
+ let nAll = size(data)
876
+
877
+ //上傳, 逐筆序列處理以免大量二進位數據同時佔用記憶體
878
+ let nInserted = 0
879
+ await pmSeries(data, async(v) => {
880
+ let inserted = await _uploadGfs(bucket, clChunks, v.id, v.u8a)
881
+ if (inserted) {
882
+ nInserted++
883
+ }
884
+ })
885
+
886
+ //res, 全數已存在而nInserted為0屬正常結果, 不視為錯誤
887
+ res = {
888
+ n: nAll,
889
+ nInserted,
890
+ ok: 1,
891
+ }
892
+
893
+ }
894
+ catch (err) {
895
+ isErr = true
896
+ res = err
897
+ }
898
+ finally {
899
+ await client.close()
900
+ client = null
901
+ }
902
+
903
+ //emit
904
+ if (!isErr) {
905
+ emitChange('insertGfs', data, res)
906
+ }
907
+
908
+ if (isErr) {
909
+ return Promise.reject(res)
910
+ }
911
+ return res
912
+ }
913
+
914
+
915
+ /**
916
+ * 使用GridFS,由主鍵查詢單筆數據
917
+ * 註: 查無數據或主鍵值無效時回傳null,判定基準與selectByPk一致
918
+ * 註: 本套件之主鍵欄位固定為id,尚未支援由呼叫端指定
919
+ * 本函數不得有副作用,故不建立唯一索引
920
+ *
921
+ * @memberOf WOrmMongodb
922
+ * @param {String} pk 輸入主鍵值字串,即數據之id
923
+ * @returns {Promise} 回傳Promise,resolve回傳數據物件{ id, u8a },若無此主鍵或主鍵值無效則回傳null,reject回傳錯誤訊息
924
+ */
925
+ async function selectByPkGfs(pk) {
926
+ let isErr = false
927
+
928
+ //check
929
+ if (!isestr(pk)) {
930
+ //未給有效主鍵值視為查無數據, 判定基準與selectByPk一致
931
+ return null
932
+ }
933
+
934
+ //client
935
+ let client = new MongoClient(opt.url)
936
+
937
+ //core
938
+ let core = async (id) => {
939
+
940
+ //pm
941
+ let pm = genPm()
942
+
943
+ //database
944
+ let database = client.db(opt.db)
945
+
946
+ //bucket
947
+ let bucket = new mongodb.GridFSBucket(database, {
948
+ chunkSizeBytes: 10 * 1024 * 1024, //10mb
949
+ bucketName: opt.cl
950
+ })
951
+
952
+ //buf
953
+ let buf = Buffer.from('')
954
+
955
+ //stream
956
+ let sm = bucket.openDownloadStreamByName(id)
957
+ sm.on('data', function (chunk) {
958
+ buf = Buffer.concat([buf, chunk])
959
+ })
960
+ sm.on('error', function (err) {
961
+
962
+ //reject
963
+ pm.reject(err)
964
+
965
+ })
966
+ sm.on('end', function () {
967
+
968
+ //u8a
969
+ let u8a = new Uint8Array(buf)
970
+
971
+ //clean memory
972
+ buf = null
973
+
974
+ //resolve
975
+ pm.resolve(u8a)
976
+
977
+ })
978
+
979
+ return pm
980
+ }
981
+
982
+ //res
983
+ let res = null
984
+ try {
985
+
986
+ //core
987
+ let u8a = await core(pk)
988
+
989
+ //res, 形狀與insertGfs所收之數據物件一致
990
+ res = {
991
+ id: pk,
992
+ u8a,
993
+ }
994
+
995
+ }
996
+ catch (err) {
997
+
998
+ //check, GridFS查無檔案時驅動以code為ENOENT之錯誤回報, 屬正常結果故回傳null
999
+ if (get(err, 'code') === 'ENOENT') {
1000
+ res = null
1001
+ }
1002
+ else {
1003
+ isErr = true
1004
+ res = err
1005
+ }
1006
+
1007
+ }
1008
+ finally {
1009
+ await client.close()
1010
+ client = null
1011
+ }
1012
+
1013
+ if (isErr) {
1014
+ return Promise.reject(res)
1015
+ }
1016
+ return res
1017
+ }
1018
+
1019
+
1020
+ async function _findGfs(find = {}, bucket) {
1021
+ let isErr = false
1022
+
1023
+ //res
1024
+ let res = null
1025
+ try {
1026
+
1027
+ //find
1028
+ let cursor = bucket.find(find)
1029
+
1030
+ //toArray
1031
+ res = await cursor.toArray()
1032
+
1033
+ }
1034
+ catch (err) {
1035
+ isErr = true
1036
+ res = err
1037
+ }
1038
+
1039
+ if (isErr) {
1040
+ return Promise.reject(res)
1041
+ }
1042
+ return res
1043
+ }
1044
+
1045
+
1046
+ /**
1047
+ * 使用GridFS,刪除數據
1048
+ * 註: n為id命中筆數;未帶有效id者視為本筆無法處理,以ok為0並附err回報;
1049
+ * 判斷本筆是否真的被刪除一律以nDeleted為準
1050
+ *
1051
+ * @memberOf WOrmMongodb
1052
+ * @param {Object|Array} data 輸入數據物件或陣列,各數據物件須帶id
1053
+ * @returns {Promise} 回傳Promise,resolve回傳與輸入等長之刪除結果陣列,reject回傳錯誤訊息
1054
+ */
1055
+ async function delGfs(data) {
1056
+ let isErr = false
1057
+
1058
+ //check
1059
+ if (!iseobj(data) && !isearr(data)) {
1060
+ return []
1061
+ }
1062
+
1063
+ //client
1064
+ let client = new MongoClient(opt.url)
1065
+
1066
+ //res
1067
+ let res = null
1068
+ try {
1069
+
1070
+ //connect, 令連線失敗於刪除前即拋出, 屬影響全部數據之整批性錯誤
1071
+ await client.connect()
1072
+
1073
+ //database
1074
+ let database = client.db(opt.db)
1075
+
1076
+ //bucket
1077
+ let bucket = new mongodb.GridFSBucket(database, {
1078
+ chunkSizeBytes: 10 * 1024 * 1024, //10mb
1079
+ bucketName: opt.cl
1080
+ })
1081
+
1082
+ //check
1083
+ if (!isarr(data)) {
1084
+ data = [data]
1085
+ }
1086
+
1087
+ //pmSeries
1088
+ res = await pmSeries(data, async(v) => {
1089
+
1090
+ //id, delGfs不補值, 未帶有效id者視為本筆無法處理
1091
+ let id = get(v, 'id')
1092
+
1093
+ //check, 判定基準與del一致
1094
+ if (!isestr(id)) {
1095
+ return {
1096
+ n: 0,
1097
+ nDeleted: 0,
1098
+ ok: 0,
1099
+ err: `invalid id[${id}]`,
1100
+ }
1101
+ }
1102
+
1103
+ //rest
1104
+ let rest = null
1105
+
1106
+ try {
1107
+
1108
+ //_findGfs
1109
+ let ltdt = await _findGfs({ filename: id }, bucket)
1110
+
1111
+ //delete, 建立唯一索引後同一id至多一筆,
1112
+ //既有數據若尚存重複id則一併刪除並如實回報nDeleted
1113
+ let nDeleted = 0
1114
+ for (let vv of ltdt) {
1115
+ await bucket.delete(vv._id)
1116
+ nDeleted++
1117
+ }
1118
+
1119
+ //rest, n為命中與否, 未命中時兩者皆為0且屬正常結果
1120
+ rest = {
1121
+ n: nDeleted > 0 ? 1 : 0,
1122
+ nDeleted,
1123
+ ok: 1,
1124
+ }
1125
+
1126
+ }
1127
+ catch (err) {
1128
+
1129
+ //本筆失敗不中斷整批
1130
+
1131
+ //rest
1132
+ rest = {
1133
+ n: 1,
1134
+ nDeleted: 0,
1135
+ ok: 0,
1136
+ err: genErrMsg(err),
1137
+ }
1138
+
1139
+ }
1140
+
1141
+ return rest
1142
+ })
1143
+
1144
+ }
1145
+ catch (err) {
1146
+ isErr = true
1147
+ res = err
1148
+ }
1149
+ finally {
1150
+ await client.close()
1151
+ client = null
1152
+ }
1153
+
1154
+ //emit
1155
+ if (!isErr) {
1156
+ emitChange('delGfs', data, res)
1157
+ }
1158
+
1159
+ if (isErr) {
1160
+ return Promise.reject(res)
1161
+ }
1162
+ return res
1163
+ }
1164
+
1165
+
1166
+ async function _delGfs(bid, bucket) {
1167
+
1168
+ //delete, 連同chunks一併刪除
1169
+ await bucket.delete(bid)
1170
+
1171
+ }
1172
+
1173
+
1174
+ /**
1175
+ * 使用GridFS,刪除全部數據,需與delGfs分開,避免未傳數據導致直接刪除全部
1176
+ * 註: n為實際刪除筆數,恆等於nDeleted;條件無命中時回傳0亦屬正常結果
1177
+ * 本函數不建立唯一索引,以免既有數據尚存重複id時無法清除
1178
+ *
1179
+ * @memberOf WOrmMongodb
1180
+ * @param {Object} [find={}] 輸入刪除條件物件
1181
+ * @returns {Promise} 回傳Promise,resolve回傳刪除結果,reject回傳錯誤訊息
1182
+ */
1183
+ async function delAllGfs(find = {}) {
1184
+ let isErr = false
1185
+
1186
+ //client
1187
+ let client = new MongoClient(opt.url)
1188
+
1189
+ //res
1190
+ let res = null
1191
+ try {
1192
+
1193
+ //database
1194
+ let database = client.db(opt.db)
1195
+
1196
+ //bucket
1197
+ let bucket = new mongodb.GridFSBucket(database, {
1198
+ chunkSizeBytes: 10 * 1024 * 1024, //10mb
1199
+ bucketName: opt.cl
1200
+ })
1201
+
1202
+ //_findGfs
1203
+ let ltdt = await _findGfs(find, bucket)
1204
+
1205
+ //ps
1206
+ let ps = map(ltdt, function(v) {
1207
+ let bid = v._id
1208
+ return _delGfs(bid, bucket)
1209
+ })
1210
+
1211
+ //all, 任一刪除失敗即往外拋, 屬整批性錯誤
1212
+ await Promise.all(ps)
1213
+
1214
+ //nDeleted, 全數刪除成功方會執行至此, 故實際刪除筆數即為符合條件筆數
1215
+ let nDeleted = size(ltdt)
1216
+
1217
+ //res
1218
+ res = {
1219
+ n: nDeleted,
1220
+ nDeleted,
1221
+ ok: 1,
1222
+ }
1223
+
1224
+ }
1225
+ catch (err) {
1226
+ isErr = true
1227
+ res = err
1228
+ }
1229
+ finally {
1230
+ await client.close()
1231
+ client = null
1232
+ }
1233
+
1234
+ //emit
1235
+ if (!isErr) {
1236
+ emitChange('delAllGfs', null, res)
1237
+ }
1238
+
1239
+ if (isErr) {
1240
+ return Promise.reject(res)
1241
+ }
1242
+ return res
1243
+ }
1244
+
1245
+
1246
+ //bind
1247
+ ee.select = select
1248
+ ee.selectByPk = selectByPk
1249
+ ee.insert = insert
1250
+ ee.save = save
1251
+ ee.del = del
1252
+ ee.delAll = delAll
1253
+ ee.selectByPkGfs = selectByPkGfs
1254
+ ee.insertGfs = insertGfs
1255
+ ee.delGfs = delGfs
1256
+ ee.delAllGfs = delAllGfs
1257
+
1258
+
1259
+ return ee
1260
+ }
1261
+
1262
+
1263
+ export default WOrmMongodb