w-orm-mongodb 1.1.36 → 1.1.38

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