w-orm-lowdb 1.0.13 → 1.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/WOrmLowdb.mjs CHANGED
@@ -1,777 +1,1066 @@
1
- import path from 'path'
2
- import events from 'events'
3
- import { Low } from 'lowdb'
4
- import { JSONFile } from 'lowdb/node'
5
- import { Query } from 'mingo'
6
- import get from 'lodash-es/get.js'
7
- import each from 'lodash-es/each.js'
8
- import map from 'lodash-es/map.js'
9
- import merge from 'lodash-es/merge.js'
10
- import isEqual from 'lodash-es/isEqual.js'
11
- import size from 'lodash-es/size.js'
12
- import cloneDeep from 'lodash-es/cloneDeep.js'
13
- import genID from 'wsemi/src/genID.mjs'
14
- import pmSeries from 'wsemi/src/pmSeries.mjs'
15
- import isarr from 'wsemi/src/isarr.mjs'
16
- import isbol from 'wsemi/src/isbol.mjs'
17
- import isestr from 'wsemi/src/isestr.mjs'
18
- import iseobj from 'wsemi/src/iseobj.mjs'
19
- import isearr from 'wsemi/src/isearr.mjs'
20
- import haskey from 'wsemi/src/haskey.mjs'
21
- import arrPullAt from 'wsemi/src/arrPullAt.mjs'
22
- import waitFun from 'wsemi/src/waitFun.mjs'
23
-
24
-
25
- //kpGlock
26
- let kpGlock = {}
27
-
28
-
29
- /**
30
- * 操作資料庫(lowdb)
31
- *
32
- * @class
33
- * @param {Object} [opt={}] 輸入設定物件,預設{}
34
- * @param {String} [opt.url='./db.json'] 輸入資料庫位置字串,預設'./db.json'
35
- * @param {String} [opt.db='worm'] 輸入使用資料庫名稱字串,預設'worm'
36
- * @param {String} [opt.cl='test'] 輸入使用資料表名稱字串,預設'test'
37
- * @param {Boolean} [opt.useCache=false] 輸入是否使用select快取,適用於單程序操作,預設false
38
- * @returns {Object} 回傳操作資料庫物件,各事件功能詳見說明
39
- */
40
- function WOrmLowdb(opt = {}) {
41
-
42
- //_cache
43
- let _cache = null
44
-
45
- //url
46
- let url = get(opt, 'url')
47
- if (!isestr(url)) {
48
- url = './db.json'
49
- }
50
-
51
- //db
52
- let db = get(opt, 'db')
53
- if (!isestr(db)) {
54
- db = 'worm'
55
- }
56
-
57
- //cl
58
- let cl = get(opt, 'cl')
59
- if (!isestr(cl)) {
60
- cl = 'test'
61
- }
62
-
63
- //useCache
64
- let useCache = get(opt, 'useCache')
65
- if (!isbol(useCache)) {
66
- useCache = false
67
- }
68
-
69
- //key
70
- let key = `${db}:${cl}`
71
-
72
- //adapter
73
- let adapter = new JSONFile(url)
74
-
75
- //lowdb
76
- let lowdb = new Low(adapter, {})
77
-
78
- // //default, 此處偵測會失效, 因操作函數內會通過lowdb.read()初始化, 預設創建空陣列會被取代
79
- // if (!haskey(lowdb.data, key)) {
80
- // lowdb.data[key] = []
81
- // }
82
-
83
- //ee
84
- let ee = new events.EventEmitter()
85
-
86
- //gkey, 基於url也就是以檔案路徑為準, 同檔案路徑須使用全域鎖
87
- let gkey = path.resolve(url)
88
-
89
- //全域鎖初始化, 一次僅能執行一種操作函數, 使各類操作url(db.json)時能為獨占模式
90
- //select理論上是不用鎖, 但因為操作上db.jon.tmp更名回db.json時似乎非rename而是串流寫入, 導致有機會撈到不完整json數據進而出錯, 故還是得要上鎖以策安全
91
- if (!haskey(kpGlock, gkey)) {
92
- kpGlock[gkey] = false
93
- }
94
-
95
- //getData
96
- let getData = async() => {
97
-
98
- //check
99
- if (useCache && isarr(_cache)) {
100
- return cloneDeep(_cache) //與外部使用數據脫勾
101
- }
102
-
103
- //read
104
- await lowdb.read()
105
-
106
- //default, 使用lowdb.read()初始化後須馬上檢測, 若無key則須先創建空陣列
107
- if (!haskey(lowdb.data, key)) {
108
- lowdb.data[key] = []
109
- }
110
-
111
- //ltdt
112
- let ltdt = cloneDeep(lowdb.data[key]) //數據為lowdb內部的記憶體狀態本體, 須cloneDeep與外部使用數據脫勾
113
-
114
- if (useCache) {
115
- _cache = ltdt
116
- return cloneDeep(ltdt) //與外部使用數據脫勾
117
- }
118
- return ltdt
119
- }
120
-
121
- /**
122
- * 查詢數據
123
- *
124
- * @memberOf WOrmLowdb
125
- * @param {Object} [find={}] 輸入查詢條件物件
126
- * @returns {Promise} 回傳Promise,resolve回傳數據,reject回傳錯誤訊息
127
- */
128
- async function selectCore(find = {}) {
129
- let isErr = false
130
-
131
- //res
132
- let res = null
133
- try {
134
-
135
- //ltdt
136
- let ltdt = await getData()
137
-
138
- //filter
139
- if (iseobj(find)) {
140
-
141
- //q
142
- let q = new Query(find)
143
- // console.log('q', q)
144
-
145
- //find
146
- res = q.find(ltdt).all()
147
- // console.log('res', res)
148
-
149
- }
150
- else {
151
- res = ltdt
152
- }
153
-
154
- }
155
- catch (err) {
156
- isErr = true
157
- res = err
158
- }
159
-
160
- if (isErr) {
161
- return Promise.reject(res)
162
- }
163
- return res
164
- }
165
- async function select(find = {}) {
166
- if (kpGlock[gkey]) {
167
- await waitFun(() => {
168
- return !kpGlock[gkey]
169
- })
170
- }
171
- kpGlock[gkey] = true
172
- return await selectCore(find)
173
- .finally(() => {
174
- kpGlock[gkey] = false
175
- })
176
- }
177
-
178
- /**
179
- * 插入數據,插入同樣數據會自動產生不同_id,故insert前需自行判斷有無重複
180
- *
181
- * @memberOf WOrmLowdb
182
- * @param {Object|Array} data 輸入數據物件或陣列
183
- * @returns {Promise} 回傳Promise,resolve回傳插入結果,reject回傳錯誤訊息
184
- */
185
- async function insertCore(data) {
186
- let isErr = false
187
-
188
- //check
189
- if (!iseobj(data) && !isearr(data)) {
190
- return {
191
- n: 0,
192
- nInserted: 0,
193
- ok: 1,
194
- }
195
- }
196
-
197
- //cloneDeep, 與外部數據脫勾
198
- data = cloneDeep(data)
199
-
200
- //res
201
- let res = null
202
- try {
203
-
204
- //check
205
- if (!isarr(data)) {
206
- data = [data]
207
- }
208
-
209
- //check id
210
- data = map(data, function(v) {
211
- if (!isestr(v.id)) {
212
- v.id = genID()
213
- }
214
- return v
215
- })
216
-
217
- //read
218
- await lowdb.read()
219
-
220
- //default, 使用lowdb.read()初始化後須馬上檢測, 若無key則須先創建空陣列
221
- if (!haskey(lowdb.data, key)) {
222
- lowdb.data[key] = []
223
- }
224
-
225
- //kp
226
- let kp = {}
227
- each(lowdb.data[key], (v, k) => {
228
- kp[v.id] = { k, v }
229
- })
230
-
231
- //each
232
- let nAll = size(data)
233
- let nPush = 0
234
- let b = false
235
- each(data, (v) => {
236
-
237
- //check
238
- if (!haskey(kp, v.id)) {
239
- //未存在v.id
240
-
241
- //push
242
- lowdb.data[key].push(v)
243
-
244
- nPush++
245
- b = true
246
- }
247
- else {
248
- //已存在v.id則不push
249
- }
250
-
251
- })
252
-
253
- //write
254
- if (b) {
255
- await lowdb.write()
256
- }
257
-
258
- //res
259
- res = {
260
- n: nAll,
261
- nInserted: nPush,
262
- ok: 1,
263
- }
264
-
265
- }
266
- catch (err) {
267
- isErr = true
268
- res = err
269
- }
270
-
271
- //update, 不能保證插入多少, 一律重設快取
272
- _cache = null
273
-
274
- //emit, 於change可能須使用select, 故須放在重設快取之後
275
- if (!isErr) {
276
- try {
277
-
278
- //emit
279
- ee.emit('change', 'insert', data, res)
280
-
281
- }
282
- catch (err) {
283
- console.log(err)
284
- }
285
- }
286
-
287
- if (isErr) {
288
- return Promise.reject(res)
289
- }
290
- return res
291
- }
292
- async function insert(data) {
293
- if (kpGlock[gkey]) {
294
- await waitFun(() => {
295
- return !kpGlock[gkey]
296
- })
297
- }
298
- kpGlock[gkey] = true
299
- return await insertCore(data)
300
- .finally(() => {
301
- kpGlock[gkey] = false
302
- })
303
- }
304
-
305
- /**
306
- * 儲存數據
307
- *
308
- * @memberOf WOrmLowdb
309
- * @param {Object|Array} data 輸入數據物件或陣列
310
- * @param {Object} [option={}] 輸入設定物件,預設為{}
311
- * @param {boolean} [option.autoInsert=true] 輸入是否於儲存時發現原本無數據,則自動改以插入處理,預設為true
312
- * @returns {Promise} 回傳Promise,resolve回傳儲存結果,reject回傳錯誤訊息
313
- */
314
- async function saveCore(data, option = {}) {
315
- let isErr = false
316
-
317
- //check
318
- if (!iseobj(data) && !isearr(data)) {
319
- return []
320
- }
321
-
322
- //cloneDeep, 與外部數據脫勾
323
- data = cloneDeep(data)
324
-
325
- //autoInsert
326
- let autoInsert = get(option, 'autoInsert', true)
327
-
328
- //res
329
- let res = null
330
- try {
331
-
332
- //check
333
- if (!isarr(data)) {
334
- data = [data]
335
- }
336
-
337
- //check id
338
- data = map(data, function(v) {
339
- if (!isestr(v.id)) {
340
- v.id = genID()
341
- }
342
- return v
343
- })
344
-
345
- //read
346
- await lowdb.read()
347
-
348
- //default, 使用lowdb.read()初始化後須馬上檢測, 若無key則須先創建空陣列
349
- if (!haskey(lowdb.data, key)) {
350
- lowdb.data[key] = []
351
- }
352
-
353
- //kp
354
- let kp = {}
355
- each(lowdb.data[key], (v, k) => {
356
- kp[v.id] = { k, v }
357
- })
358
-
359
- //pmSeries
360
- let b = false
361
- res = await pmSeries(data, async(v) => {
362
-
363
- //rest
364
- let rest = null
365
-
366
- //查找資料表內v.id
367
- let r = get(kp, v.id, null)
368
-
369
- //existed
370
- let existed = iseobj(r)
371
-
372
- //check
373
- if (existed) {
374
- //已存在v.id
375
- if (isEqual(v, r.v)) {
376
- //內容相同不更新
377
- }
378
- else {
379
- //內容不同須更新
380
-
381
- //merge
382
- lowdb.data[key][r.k] = merge(lowdb.data[key][r.k], v)
383
-
384
- rest = { update: true }
385
- b = true
386
- }
387
- }
388
- else {
389
- //內容不存在
390
- }
391
-
392
- //rest
393
- if (iseobj(rest)) {
394
- rest = {
395
- n: 1,
396
- nModified: 1,
397
- ok: 1,
398
- }
399
- }
400
- else {
401
- rest = {
402
- n: 0,
403
- nModified: 0,
404
- ok: 1,
405
- }
406
- }
407
- //rest.n === 0:
408
- // 內容相同不更新, 不須update
409
- // 內容不存在, 若autoInsert則須insert
410
-
411
- //autoInsert
412
- if (autoInsert && rest.n === 0 && !existed) {
413
- let k = size(lowdb.data[key])
414
- lowdb.data[key].push(v)
415
- kp[v.id] = {
416
- k,
417
- v: lowdb.data[key][k],
418
- }
419
- b = true
420
- rest = {
421
- n: 1,
422
- nInserted: 1,
423
- ok: 1,
424
- }
425
- }
426
-
427
- return rest
428
- })
429
-
430
- //write
431
- if (b) {
432
- await lowdb.write()
433
- }
434
-
435
- }
436
- catch (err) {
437
- isErr = true
438
- res = err
439
- }
440
-
441
- //update, 不能保證變更多少, 一律重設快取
442
- _cache = null
443
-
444
- //emit, 於change可能須使用select, 故須放在重設快取之後
445
- if (!isErr) {
446
- try {
447
-
448
- //emit
449
- ee.emit('change', 'save', data, res)
450
-
451
- }
452
- catch (err) {
453
- console.log(err)
454
- }
455
- }
456
-
457
- if (isErr) {
458
- return Promise.reject(res)
459
- }
460
- return res
461
- }
462
- async function save(data, option = {}) {
463
- if (kpGlock[gkey]) {
464
- await waitFun(() => {
465
- return !kpGlock[gkey]
466
- })
467
- }
468
- kpGlock[gkey] = true
469
- return await saveCore(data, option)
470
- .finally(() => {
471
- kpGlock[gkey] = false
472
- })
473
- }
474
-
475
- /**
476
- * 刪除數據
477
- *
478
- * @memberOf WOrmLowdb
479
- * @param {Object|Array} data 輸入數據物件或陣列
480
- * @returns {Promise} 回傳Promise,resolve回傳刪除結果,reject回傳錯誤訊息
481
- */
482
- async function delCore(data) {
483
- let isErr = false
484
-
485
- //check
486
- if (!iseobj(data) && !isearr(data)) {
487
- return []
488
- }
489
-
490
- //cloneDeep, 與外部數據脫勾
491
- data = cloneDeep(data)
492
-
493
- //res
494
- let res = null
495
- try {
496
-
497
- //check
498
- if (!isarr(data)) {
499
- data = [data]
500
- }
501
-
502
- //read
503
- await lowdb.read()
504
-
505
- //default, 使用lowdb.read()初始化後須馬上檢測, 若無key則須先創建空陣列
506
- if (!haskey(lowdb.data, key)) {
507
- lowdb.data[key] = []
508
- }
509
-
510
- //kp
511
- let kp = {}
512
- each(lowdb.data[key], (v, k) => {
513
- kp[v.id] = { k, v }
514
- })
515
-
516
- //pmSeries
517
- let ks = []
518
- let b = false
519
- res = await pmSeries(data, async(v) => {
520
-
521
- //rest
522
- let rest = null
523
-
524
- //id
525
- let id = get(v, 'id', '')
526
-
527
- //check
528
- if (isestr(id)) {
529
-
530
- //查找資料表內v.id為_v
531
- let r = get(kp, id, null)
532
-
533
- //check
534
- if (iseobj(r)) {
535
- //已存在v.id則須刪除
536
-
537
- //push
538
- ks.push(r.k)
539
-
540
- b = true
541
-
542
- //rest
543
- rest = {
544
- n: 1,
545
- nDeleted: 1,
546
- ok: 1,
547
- }
548
-
549
- }
550
- else {
551
- //不存在v.id則不刪除
552
-
553
- //rest
554
- rest = {
555
- n: 1,
556
- nDeleted: 0,
557
- ok: 1,
558
- }
559
-
560
- }
561
-
562
- }
563
- else {
564
- //未給v.id則不刪除
565
-
566
- //rest
567
- rest = {
568
- n: 1,
569
- nDeleted: 0,
570
- ok: 0, //未給v.id視為有問題數據, 故ok給0
571
- }
572
-
573
- }
574
-
575
- return rest
576
- })
577
-
578
- //update
579
- if (b) {
580
- lowdb.data[key] = arrPullAt(lowdb.data[key], ks)
581
- }
582
-
583
- //write
584
- if (b) {
585
- await lowdb.write()
586
- }
587
-
588
- }
589
- catch (err) {
590
- isErr = true
591
- res = err
592
- }
593
-
594
- //update, 不能保證刪除多少, 一律重設快取
595
- _cache = null
596
-
597
- //emit, 於change可能須使用select, 故須放在重設快取之後
598
- if (!isErr) {
599
- try {
600
-
601
- //emit
602
- ee.emit('change', 'del', data, res)
603
-
604
- }
605
- catch (err) {
606
- console.log(err)
607
- }
608
- }
609
-
610
- if (isErr) {
611
- return Promise.reject(res)
612
- }
613
- return res
614
- }
615
- async function del(data) {
616
- if (kpGlock[gkey]) {
617
- await waitFun(() => {
618
- return !kpGlock[gkey]
619
- })
620
- }
621
- kpGlock[gkey] = true
622
- return await delCore(data)
623
- .finally(() => {
624
- kpGlock[gkey] = false
625
- })
626
- }
627
-
628
- /**
629
- * 刪除全部數據,需與del分開,避免未傳數據導致直接刪除全表
630
- *
631
- * @memberOf WOrmLowdb
632
- * @param {Object} [find={}] 輸入刪除條件物件
633
- * @returns {Promise} 回傳Promise,resolve回傳刪除結果,reject回傳錯誤訊息
634
- */
635
- async function delAllCore(find = {}) {
636
- let isErr = false
637
-
638
- //res
639
- let res = null
640
- try {
641
-
642
- //read
643
- await lowdb.read()
644
-
645
- //default, 使用lowdb.read()初始化後須馬上檢測, 若無key則須先創建空陣列
646
- if (!haskey(lowdb.data, key)) {
647
- lowdb.data[key] = []
648
- }
649
-
650
- //filter
651
- let nAll = size(lowdb.data[key])
652
- let nDel = 0
653
- let b = false
654
- if (iseobj(find)) {
655
-
656
- //q
657
- let q = new Query(find)
658
- // console.log('q', q)
659
-
660
- //find
661
- let _res = q.find(lowdb.data[key]).all()
662
- // console.log('_res', _res)
663
-
664
- //nDel
665
- nDel = size(_res)
666
- // console.log('nDel', nDel)
667
-
668
- if (nDel === 0) {
669
- //未有find結果等於不刪除
670
- }
671
- else if (nAll === nDel) {
672
- //全在find結果內等於全部刪除
673
-
674
- //empty
675
- lowdb.data[key] = []
676
-
677
- b = true
678
- }
679
- else {
680
- //部份在find結果內
681
-
682
- //_kp
683
- let _kp = {}
684
- each(_res, (v, k) => {
685
- _kp[v.id] = { k, v }
686
- })
687
-
688
- //arr
689
- let arr = []
690
- each(lowdb.data[key], (v, k) => {
691
- if (!haskey(_kp, v.id)) {
692
- //未在find結果內代表須保留
693
- arr.push(v)
694
- }
695
- })
696
-
697
- //update
698
- lowdb.data[key] = arr
699
-
700
- b = true
701
- }
702
- }
703
- else {
704
-
705
- //nDel
706
- nDel = nAll
707
-
708
- //empty
709
- lowdb.data[key] = []
710
-
711
- b = true
712
- }
713
-
714
- //write
715
- if (b) {
716
- await lowdb.write()
717
- }
718
-
719
- //res
720
- res = {
721
- n: nAll,
722
- nDeleted: nDel,
723
- ok: 1,
724
- }
725
-
726
- }
727
- catch (err) {
728
- isErr = true
729
- res = err
730
- }
731
-
732
- //update, 不能保證刪除多少, 一律重設快取
733
- _cache = null
734
-
735
- //emit, 於change可能須使用select, 故須放在重設快取之後
736
- if (!isErr) {
737
- try {
738
-
739
- //emit
740
- ee.emit('change', 'delAll', null, res)
741
-
742
- }
743
- catch (err) {
744
- console.log(err)
745
- }
746
- }
747
-
748
- if (isErr) {
749
- return Promise.reject(res)
750
- }
751
- return res
752
- }
753
- async function delAll(find = {}) {
754
- if (kpGlock[gkey]) {
755
- await waitFun(() => {
756
- return !kpGlock[gkey]
757
- })
758
- }
759
- kpGlock[gkey] = true
760
- return await delAllCore(find)
761
- .finally(() => {
762
- kpGlock[gkey] = false
763
- })
764
- }
765
-
766
- //bind
767
- ee.select = select
768
- ee.insert = insert
769
- ee.save = save
770
- ee.del = del
771
- ee.delAll = delAll
772
-
773
- return ee
774
- }
775
-
776
-
777
- export default WOrmLowdb
1
+ import path from 'path'
2
+ import { Low } from 'lowdb'
3
+ import { JSONFile } from 'lowdb/node'
4
+ import { Query } from 'mingo'
5
+ import get from 'lodash-es/get.js'
6
+ import each from 'lodash-es/each.js'
7
+ import map from 'lodash-es/map.js'
8
+ import filter from 'lodash-es/filter.js'
9
+ import merge from 'lodash-es/merge.js'
10
+ import isEqual from 'lodash-es/isEqual.js'
11
+ import size from 'lodash-es/size.js'
12
+ import cloneDeep from 'lodash-es/cloneDeep.js'
13
+ import evem from 'wsemi/src/evem.mjs'
14
+ import genIDSeq from 'wsemi/src/genIDSeq.mjs'
15
+ import pmSeries from 'wsemi/src/pmSeries.mjs'
16
+ import isarr from 'wsemi/src/isarr.mjs'
17
+ import isbol from 'wsemi/src/isbol.mjs'
18
+ import isestr from 'wsemi/src/isestr.mjs'
19
+ import iseobj from 'wsemi/src/iseobj.mjs'
20
+ import isearr from 'wsemi/src/isearr.mjs'
21
+ import haskey from 'wsemi/src/haskey.mjs'
22
+
23
+
24
+ //kpGqueue, 以資料庫檔案路徑為鍵之全域序列化佇列, 詳見下方serialize說明
25
+ let kpGqueue = {}
26
+
27
+
28
+ /**
29
+ * 操作資料庫(lowdb)
30
+ *
31
+ * 回傳物件為EventEmitter,除各操作函數外另發出change與error兩事件,供呼叫端於單一處集中觀察資料異動與失敗。
32
+ * 事件僅為附加通知,其所送出之資訊皆另有正規管道(操作結果經resolve、整批性錯誤經reject、逐筆失敗經該筆之err欄位),
33
+ * 故不監聽亦能取得完整資訊,且監聽與否不改變任何操作之回傳值。
34
+ *
35
+ * change事件,參數為(mode, data, res),於資料實際異動成功後發出:
36
+ * mode為操作別字串,可為'insert'、'insertBulk'、'save'、'del'、'delAll';save內若逐筆走自動插入則該筆另發出mode為'insert'之事件。
37
+ * data為本次操作之輸入數據,delAll固定為null。
38
+ * res為本次操作之回傳結果。
39
+ * 逐筆函數以整批為單位發出一次而不逐筆發出,select與selectByPk不發出本事件。
40
+ *
41
+ * error事件,參數為(mode, data, err),於操作發生錯誤時發出:
42
+ * mode為操作別字串,可為'select'、'selectByPk'、'insert'、'insertBulk'、'save'、'del'、'delAll'。
43
+ * data為本次操作之輸入數據,無輸入數據者為null。
44
+ * err為錯誤訊息字串,內容與正規管道所送出者一致。
45
+ * 整批性錯誤於reject之前發出;逐筆失敗於該筆結果定案後發出,每筆一次。
46
+ * 註: 逐筆失敗時整批仍resolve,故收到本事件不表示該次呼叫失敗;正常結果(如查無數據、主鍵未命中、全數已存在)不發出本事件。
47
+ *
48
+ * 註: 本套件之主鍵欄位固定為id,尚未支援指定其他欄位為主鍵。
49
+ * 註: 本套件之寫入為[整檔讀出、記憶體修改、整檔寫回],其原子性由行程內之序列化佇列提供,
50
+ * 故僅單一行程內併發成立,跨行程併發不成立,詳見README之併發保證宣告。
51
+ *
52
+ * @class
53
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
54
+ * @param {String} [opt.url='./db.json'] 輸入資料庫位置字串,預設'./db.json'
55
+ * @param {String} [opt.db='worm'] 輸入使用資料庫名稱字串,預設'worm'
56
+ * @param {String} [opt.cl='test'] 輸入使用資料表名稱字串,預設'test'
57
+ * @param {Boolean} [opt.useCache=false] 輸入是否使用select快取,適用於單程序操作,預設false
58
+ * @param {Boolean} [opt.autoGenPk=true] 輸入insert、insertBulk與save於數據未帶有效主鍵(本套件為id欄位)時是否自動產生主鍵值,預設true;給false代表主鍵改由呼叫端自備,套件不產生亦不檢查其唯一性與格式,未帶有效主鍵者將reject
59
+ * @returns {Object} 回傳操作資料庫物件,各事件功能詳見說明
60
+ */
61
+ function WOrmLowdb(opt = {}) {
62
+
63
+ //_cache
64
+ let _cache = null
65
+
66
+ //url
67
+ let url = get(opt, 'url')
68
+ if (!isestr(url)) {
69
+ url = './db.json'
70
+ }
71
+
72
+ //db
73
+ let db = get(opt, 'db')
74
+ if (!isestr(db)) {
75
+ db = 'worm'
76
+ }
77
+
78
+ //cl
79
+ let cl = get(opt, 'cl')
80
+ if (!isestr(cl)) {
81
+ cl = 'test'
82
+ }
83
+
84
+ //useCache
85
+ let useCache = get(opt, 'useCache')
86
+ if (!isbol(useCache)) {
87
+ useCache = false
88
+ }
89
+
90
+ //autoGenPk, 主鍵由誰產生為整個資料表之政策, 故為建構層設定, 不可於各寫入函數之option逐次覆寫,
91
+ //否則同一資料表將混入兩種來源之主鍵而難以追溯
92
+ let autoGenPk = get(opt, 'autoGenPk')
93
+ if (!isbol(autoGenPk)) {
94
+ autoGenPk = true
95
+ }
96
+
97
+ //key
98
+ let key = `${db}:${cl}`
99
+
100
+ //adapter
101
+ let adapter = new JSONFile(url)
102
+
103
+ //lowdb
104
+ let lowdb = new Low(adapter, {})
105
+
106
+ //ee
107
+ let ee = evem() //採eventemitter3, 其於'error'無監聽者時僅回傳false而不拋出, 故操作行為不因監聽者之有無而改變
108
+
109
+ //gkey, 基於url也就是以檔案路徑為準, 同檔案路徑須使用同一佇列
110
+ let gkey = path.resolve(url)
111
+
112
+ //佇列初始化, 同一資料庫檔案之全部操作一律序列化
113
+ if (!haskey(kpGqueue, gkey)) {
114
+ kpGqueue[gkey] = Promise.resolve()
115
+ }
116
+
117
+ /**
118
+ * 序列化同一資料庫檔案之操作
119
+ *
120
+ * 本套件之寫入為[整檔讀出、記憶體修改、整檔寫回],lowdb無條件寫入亦無比較並交換,
121
+ * 故insert之[檢查主鍵不存在與寫入]與save之[查找主鍵與更新或插入],其原子性只能由行程內之序列化達成。
122
+ * 採Promise鏈而非[布林旗標配合輪詢等待],係因後者之[檢查旗標]與[設定旗標]之間隔有await,
123
+ * 並非原子之test-and-set,多個等候者之輪詢若落在同一批timer觸發即會同時進入臨界區;
124
+ * 且輪詢函數逾時後為resolve而非reject,等滿即照樣放行。Promise鏈為嚴格FIFO且無逾時放行,由結構保證互斥。
125
+ * select亦納入序列化,令其不致與寫入之整檔替換交錯。
126
+ *
127
+ * 註: 佇列為module層之變數,僅同一行程內共享,故跨行程併發不受此保護。
128
+ */
129
+ let serialize = (fn) => {
130
+
131
+ //pmPrev, 恆為已攔截錯誤之Promise, 故前一次操作失敗不會中斷佇列
132
+ let pmPrev = kpGqueue[gkey]
133
+
134
+ //pm
135
+ let pm = pmPrev.then(() => {
136
+ return fn()
137
+ })
138
+
139
+ //update, 存入已攔截錯誤者, 避免本次reject成為unhandled
140
+ kpGqueue[gkey] = pm.catch(() => {})
141
+
142
+ return pm
143
+ }
144
+
145
+ //emitChange, 資料實際異動成功後發出, 事件僅為附加通知不承擔回傳義務
146
+ //一律包try/catch, 令訂閱函數自身拋錯不影響本次操作之結果
147
+ let emitChange = (mode, data, res) => {
148
+ try {
149
+ ee.emit('change', mode, data, res)
150
+ }
151
+ catch (err) {
152
+ console.log(err)
153
+ }
154
+ }
155
+
156
+ //emitError, 操作發生錯誤時發出, 錯誤訊息一律轉為字串
157
+ let emitError = (mode, data, err) => {
158
+ try {
159
+ ee.emit('error', mode, data, getErrMsg(err))
160
+ }
161
+ catch (errEmit) {
162
+ console.log(errEmit)
163
+ }
164
+ }
165
+
166
+ //getErrMsg, 取錯誤訊息字串, 供逐筆結果之err欄位與error事件使用
167
+ let getErrMsg = (err) => {
168
+ let m = get(err, 'message')
169
+ if (isestr(m)) {
170
+ return m
171
+ }
172
+ return String(err)
173
+ }
174
+
175
+ //procPk, 依autoGenPk處理各數據之主鍵, 供insert、insertBulk與save共用
176
+ //autoGenPk為true時補值; 為false時不補值, 未帶有效主鍵者屬呼叫端未履行契約, 拋出為整批性錯誤,
177
+ //且檢查於任何寫入之前完成, 故不會有部份筆數已寫入而整批失敗之情形
178
+ let procPk = (data, fnName) => {
179
+ if (autoGenPk) {
180
+ return map(data, function(v) {
181
+ if (!isestr(v.id)) {
182
+ v.id = genIDSeq()
183
+ }
184
+ return v
185
+ })
186
+ }
187
+ each(data, function(v, k) {
188
+ if (!isestr(get(v, 'id'))) {
189
+ throw new Error(`can not ${fnName} by data[${k}] without valid id when autoGenPk is false`)
190
+ }
191
+ })
192
+ return data
193
+ }
194
+
195
+ //readData, 讀取資料庫檔案並取得本資料表之陣列
196
+ //註: 使用lowdb.read()初始化後須馬上檢測, 若無key則須先創建空陣列
197
+ let readData = async() => {
198
+ await lowdb.read()
199
+ if (!haskey(lowdb.data, key)) {
200
+ lowdb.data[key] = []
201
+ }
202
+ return lowdb.data[key]
203
+ }
204
+
205
+ //writeData, 以新陣列取代本資料表後寫檔
206
+ //寫檔失敗時還原記憶體狀態, 令reject之後之記憶體與檔案一致,
207
+ //否則殘留之修改會於下次寫入時被一併帶出, 使本應未生效之數據落盤
208
+ let writeData = async(arrOld, arrNew) => {
209
+ lowdb.data[key] = arrNew
210
+ try {
211
+ await lowdb.write()
212
+ }
213
+ catch (err) {
214
+ lowdb.data[key] = arrOld
215
+ throw err
216
+ }
217
+ }
218
+
219
+ //getKpPk, 由數據陣列建立[主鍵值 => {k, v}]之對照表, k為所在索引
220
+ //僅收錄具有效主鍵值者, 令資料表內若有主鍵值無效之殘留數據不致被誤配為命中
221
+ let getKpPk = (ltdt) => {
222
+ let kp = {}
223
+ each(ltdt, (v, k) => {
224
+ let id = get(v, 'id')
225
+ if (isestr(id)) {
226
+ kp[id] = { k, v }
227
+ }
228
+ })
229
+ return kp
230
+ }
231
+
232
+ //getData
233
+ let getData = async() => {
234
+
235
+ //check
236
+ if (useCache && isarr(_cache)) {
237
+ return cloneDeep(_cache) //與外部使用數據脫勾
238
+ }
239
+
240
+ //read
241
+ let ltdt = await readData()
242
+
243
+ //cloneDeep, 數據為lowdb內部的記憶體狀態本體, 須cloneDeep與外部使用數據脫勾
244
+ ltdt = cloneDeep(ltdt)
245
+
246
+ if (useCache) {
247
+ _cache = ltdt
248
+ return cloneDeep(ltdt) //與外部使用數據脫勾
249
+ }
250
+ return ltdt
251
+ }
252
+
253
+ /**
254
+ * 查詢數據
255
+ *
256
+ * @memberOf WOrmLowdb
257
+ * @param {Object} [find={}] 輸入查詢條件物件
258
+ * @returns {Promise} 回傳Promise,resolve回傳數據陣列,無符合數據回傳空陣列,reject回傳錯誤訊息
259
+ */
260
+ async function selectCore(find = {}) {
261
+ let isErr = false
262
+
263
+ //res
264
+ let res = null
265
+ try {
266
+
267
+ //ltdt
268
+ let ltdt = await getData()
269
+
270
+ //filter
271
+ if (iseobj(find)) {
272
+
273
+ //q
274
+ let q = new Query(find)
275
+ // console.log('q', q)
276
+
277
+ //find
278
+ res = q.find(ltdt).all()
279
+ // console.log('res', res)
280
+
281
+ }
282
+ else {
283
+ res = ltdt
284
+ }
285
+
286
+ //check
287
+ if (!isarr(res)) {
288
+ isErr = true
289
+ res = new Error(`can not select by find[${JSON.stringify(find)}]`)
290
+ }
291
+
292
+ }
293
+ catch (err) {
294
+ isErr = true
295
+ res = err
296
+ }
297
+
298
+ //check
299
+ if (isErr) {
300
+
301
+ //emit, 整批性錯誤須於reject之前發出
302
+ emitError('select', null, res)
303
+
304
+ return Promise.reject(res)
305
+ }
306
+
307
+ return res
308
+ }
309
+ async function select(find = {}) {
310
+ return serialize(() => selectCore(find))
311
+ }
312
+
313
+ /**
314
+ * 由主鍵查詢單筆數據
315
+ * 註: 本套件之主鍵欄位固定為id,尚未支援指定其他欄位為主鍵
316
+ * 註: lowdb無索引,本函數仍須讀取全表,故較select僅省去查詢條件之過濾而無數量級之效能差異,
317
+ * 提供本函數係為與其他w-orm系列套件維持同一組函數
318
+ *
319
+ * @memberOf WOrmLowdb
320
+ * @param {String} pk 輸入主鍵值字串,本套件之主鍵欄位為id
321
+ * @returns {Promise} 回傳Promise,resolve回傳數據物件,若無此主鍵或主鍵值無效則回傳null,reject回傳錯誤訊息
322
+ */
323
+ async function selectByPkCore(pk) {
324
+ let isErr = false
325
+
326
+ //res
327
+ let res = null
328
+ try {
329
+
330
+ //check
331
+ if (!isestr(pk)) {
332
+ //未給有效主鍵值視為查無數據
333
+ return null
334
+ }
335
+
336
+ //ltdt
337
+ let ltdt = await getData()
338
+
339
+ //查找資料表內pk, 判定基準與insert、insertBulk、save、del內對既有數據之認定一致(皆以id之字串值比對)
340
+ let kp = getKpPk(ltdt)
341
+ let r = get(kp, pk, null)
342
+
343
+ //check
344
+ if (iseobj(r)) {
345
+ res = r.v
346
+ }
347
+ else {
348
+ //不存在此主鍵, 回傳null
349
+ res = null
350
+ }
351
+
352
+ }
353
+ catch (err) {
354
+ isErr = true
355
+ res = err
356
+ }
357
+
358
+ //check
359
+ if (isErr) {
360
+
361
+ //emit, 整批性錯誤須於reject之前發出
362
+ emitError('selectByPk', null, res)
363
+
364
+ return Promise.reject(res)
365
+ }
366
+
367
+ return res
368
+ }
369
+ async function selectByPk(pk) {
370
+ return serialize(() => selectByPkCore(pk))
371
+ }
372
+
373
+ /**
374
+ * 插入數據,僅於主鍵不存在時寫入,已存在者跳過且不覆寫
375
+ *
376
+ * @memberOf WOrmLowdb
377
+ * @param {Object|Array} data 輸入數據物件或陣列
378
+ * @param {Object} [option={}] 輸入設定物件,預設為{}
379
+ * @param {Boolean} [option.returnList=false] 輸入是否改回傳逐筆結果布林值,預設false。給true時回傳與輸入等長保序之陣列,各筆為{n,nInserted,ok},nInserted為1即該筆為新增;聚合計數只回答有幾筆是新的,逐筆結果方能回答是哪幾筆,供下游僅對新資料執行昂貴動作。回傳形式由呼叫點靜態決定,共用之結果處理程式碼不可跨不同取值之呼叫點混用
380
+ * @returns {Promise} 回傳Promise,resolve依returnList回傳插入結果:預設回傳聚合物件{n,nInserted,ok},n為輸入筆數、nInserted為實際插入筆數;returnList為true時回傳逐筆陣列,各筆n恆為1(主鍵命中或經插入)、ok恆為1(insert之錯誤皆屬整批性而reject,不進逐筆);reject回傳錯誤訊息
381
+ */
382
+ async function insertCore(data, option = {}) {
383
+ let isErr = false
384
+
385
+ //returnList
386
+ let returnList = get(option, 'returnList')
387
+ if (!isbol(returnList)) {
388
+ returnList = false
389
+ }
390
+
391
+ //check
392
+ if (!iseobj(data) && !isearr(data)) {
393
+ if (returnList) {
394
+ //輸入無效, 對齊save與del之空陣列規定
395
+ return []
396
+ }
397
+ return {
398
+ n: 0,
399
+ nInserted: 0,
400
+ ok: 1,
401
+ }
402
+ }
403
+
404
+ //cloneDeep, 與外部數據脫勾
405
+ data = cloneDeep(data)
406
+
407
+ //res
408
+ let res = null
409
+ try {
410
+
411
+ //check
412
+ if (!isarr(data)) {
413
+ data = [data]
414
+ }
415
+
416
+ //check id
417
+ data = procPk(data, 'insert')
418
+
419
+ //read
420
+ let arrOld = await readData()
421
+
422
+ //kp
423
+ let kp = getKpPk(arrOld)
424
+
425
+ //each
426
+ let nAll = size(data)
427
+ let ltdone = []
428
+ let ltpush = []
429
+ each(data, (v) => {
430
+
431
+ //check
432
+ if (!haskey(kp, v.id)) {
433
+ //未存在v.id
434
+
435
+ //push
436
+ ltpush.push(v)
437
+
438
+ //update, 須同步更新kp, 否則同批含重複主鍵者會全數寫入而出現重複主鍵
439
+ kp[v.id] = { k: -1, v }
440
+
441
+ ltdone.push(true)
442
+ }
443
+ else {
444
+ //已存在v.id(含同批重複之非首筆)則不push
445
+ ltdone.push(false)
446
+ }
447
+
448
+ })
449
+
450
+ //write, 未有須插入者即不寫檔, 全數已存在屬正常結果
451
+ if (size(ltpush) > 0) {
452
+ await writeData(arrOld, [...arrOld, ...ltpush])
453
+ }
454
+
455
+ //res
456
+ if (returnList) {
457
+ //逐筆結果, 與輸入等長保序, 元素沿用逐筆結果之家族形狀(同save與del之元素)
458
+ //n恆為1(主鍵命中既有或經插入而產生), ok恆為1(insert之錯誤皆屬整批性而reject, 不進逐筆),
459
+ //資訊由nInserted承載, filter(v=>v.nInserted===1)之長度必等於聚合模式之nInserted
460
+ res = map(ltdone, function(done) {
461
+ return {
462
+ n: 1,
463
+ nInserted: done ? 1 : 0,
464
+ ok: 1,
465
+ }
466
+ })
467
+ }
468
+ else {
469
+ res = {
470
+ n: nAll,
471
+ nInserted: size(ltpush),
472
+ ok: 1,
473
+ }
474
+ }
475
+
476
+ }
477
+ catch (err) {
478
+ isErr = true
479
+ res = err
480
+ }
481
+
482
+ //update, 不能保證插入多少, 一律重設快取
483
+ _cache = null
484
+
485
+ //emit, 於change可能須使用select, 故須放在重設快取之後
486
+ if (!isErr) {
487
+ emitChange('insert', data, res)
488
+ }
489
+
490
+ //check
491
+ if (isErr) {
492
+
493
+ //emit, 整批性錯誤須於reject之前發出
494
+ emitError('insert', data, res)
495
+
496
+ return Promise.reject(res)
497
+ }
498
+
499
+ return res
500
+ }
501
+ async function insert(data, option = {}) {
502
+ return serialize(() => insertCore(data, option))
503
+ }
504
+
505
+ /**
506
+ * 批次插入數據,全批視為一個單位,全部插入成功或一筆都不寫入
507
+ * 註: 本函數非insert之加速版,兩者衝突政策不同。insert於主鍵已存在時跳過該筆而整批ok為1,
508
+ * 本函數則整批reject且不寫入任何一筆;同批含重複主鍵者亦視為衝突。確無衝突時兩者結果相同
509
+ * 註: 於本套件不會較insert快,因兩者皆為一次讀檔與一次寫檔,
510
+ * 提供本函數係為與其他w-orm系列套件維持同一組函數,令呼叫端得於各套件間替換而不須改寫呼叫
511
+ *
512
+ * @memberOf WOrmLowdb
513
+ * @param {Object|Array} data 輸入數據物件或陣列
514
+ * @returns {Promise} 回傳Promise,resolve回傳插入結果物件{n,nInserted,ok},n為輸入筆數、nInserted成功時恆等於n,任一筆主鍵已存在則reject回傳錯誤訊息
515
+ */
516
+ async function insertBulkCore(data) {
517
+ let isErr = false
518
+
519
+ //check
520
+ if (!iseobj(data) && !isearr(data)) {
521
+ return {
522
+ n: 0,
523
+ nInserted: 0,
524
+ ok: 1,
525
+ }
526
+ }
527
+
528
+ //cloneDeep, 與外部數據脫勾
529
+ data = cloneDeep(data)
530
+
531
+ //res
532
+ let res = null
533
+ try {
534
+
535
+ //check
536
+ if (!isarr(data)) {
537
+ data = [data]
538
+ }
539
+
540
+ //check id
541
+ data = procPk(data, 'insertBulk')
542
+
543
+ //read
544
+ let arrOld = await readData()
545
+
546
+ //kp
547
+ let kp = getKpPk(arrOld)
548
+
549
+ //nAll
550
+ let nAll = size(data)
551
+
552
+ //check, 主鍵檢查於任何寫入之前一次完成, 任一筆衝突即整批中止,
553
+ //同批含重複主鍵者亦於此被偵測為衝突(kp隨檢查同步更新)
554
+ let pkConflict = null
555
+ each(data, (v) => {
556
+ if (haskey(kp, v.id)) {
557
+ pkConflict = v.id
558
+ return false //中止each
559
+ }
560
+ kp[v.id] = { k: -1, v }
561
+ })
562
+
563
+ //check
564
+ if (pkConflict !== null) {
565
+ throw new Error(`can not insertBulk by existed id[${pkConflict}]`)
566
+ }
567
+
568
+ //write, 全部通過檢查方寫入, 且寫檔為單次[寫暫存檔後更名]之原子替換,
569
+ //故全有全無成立: 檢查失敗時尚未修改任何狀態, 寫檔失敗時writeData會還原記憶體
570
+ await writeData(arrOld, [...arrOld, ...data])
571
+
572
+ //res, 未衝突則全數插入, 故nInserted恆等於n
573
+ res = {
574
+ n: nAll,
575
+ nInserted: nAll,
576
+ ok: 1,
577
+ }
578
+
579
+ }
580
+ catch (err) {
581
+ isErr = true
582
+ res = err
583
+ }
584
+
585
+ //update, 不能保證插入多少, 一律重設快取
586
+ _cache = null
587
+
588
+ //emit, 於change可能須使用select, 故須放在重設快取之後
589
+ if (!isErr) {
590
+ emitChange('insertBulk', data, res)
591
+ }
592
+
593
+ //check
594
+ if (isErr) {
595
+
596
+ //emit, 整批性錯誤須於reject之前發出
597
+ emitError('insertBulk', data, res)
598
+
599
+ return Promise.reject(res)
600
+ }
601
+
602
+ return res
603
+ }
604
+ async function insertBulk(data) {
605
+ return serialize(() => insertBulkCore(data))
606
+ }
607
+
608
+ /**
609
+ * 儲存數據,以主鍵為準更新既有數據,未給之欄位保留;主鍵不存在且autoInsert為true時改為插入
610
+ *
611
+ * @memberOf WOrmLowdb
612
+ * @param {Object|Array} data 輸入數據物件或陣列
613
+ * @param {Object} [option={}] 輸入設定物件,預設為{}
614
+ * @param {boolean} [option.autoInsert=true] 輸入是否於儲存時發現原本無數據,則自動改以插入處理,預設為true
615
+ * @returns {Promise} 回傳Promise,resolve回傳與輸入等長之儲存結果陣列,各筆為{n,nInserted,nModified,ok},n為主鍵命中筆數,單筆失敗者ok為0並附err,reject回傳錯誤訊息
616
+ */
617
+ async function saveCore(data, option = {}) {
618
+ let isErr = false
619
+
620
+ //check
621
+ if (!iseobj(data) && !isearr(data)) {
622
+ return []
623
+ }
624
+
625
+ //cloneDeep, 與外部數據脫勾
626
+ data = cloneDeep(data)
627
+
628
+ //autoInsert
629
+ let autoInsert = get(option, 'autoInsert', true)
630
+
631
+ //res
632
+ let res = null
633
+ try {
634
+
635
+ //check
636
+ if (!isarr(data)) {
637
+ data = [data]
638
+ }
639
+
640
+ //check id
641
+ data = procPk(data, 'save')
642
+
643
+ //read
644
+ let arrOld = await readData()
645
+
646
+ //arrNew, 逐筆修改於複本上進行, 全部完成後方取代與寫檔
647
+ let arrNew = [...arrOld]
648
+
649
+ //kp
650
+ let kp = getKpPk(arrNew)
651
+
652
+ //pmSeries
653
+ let b = false
654
+ res = await pmSeries(data, async(v) => {
655
+
656
+ //rest
657
+ let rest = null
658
+
659
+ try {
660
+
661
+ //查找資料表內v.id
662
+ let r = get(kp, v.id, null)
663
+
664
+ //existed
665
+ let existed = iseobj(r)
666
+
667
+ //check
668
+ if (existed) {
669
+ //已存在v.id
670
+
671
+ //vv, 現值
672
+ let vv = r.v
673
+
674
+ //vm, 合併後之結果, 另建新物件以免污染原數據
675
+ let vm = merge({}, vv, v)
676
+
677
+ //判定基準為[合併後結果與現值相同], 而非[待寫入物件與現值全等],
678
+ //令nModified忠實反映是否真的寫入, 僅給部份欄位且值皆相同者不應回報已修改
679
+ if (isEqual(vm, vv)) {
680
+ //合併後與現值相同不須寫入, 惟已命中v.id故n為1
681
+ rest = {
682
+ n: 1,
683
+ nInserted: 0,
684
+ nModified: 0,
685
+ ok: 1,
686
+ }
687
+ }
688
+ else {
689
+ //合併後與現值不同須更新
690
+
691
+ //update
692
+ arrNew[r.k] = vm
693
+ kp[v.id] = { k: r.k, v: vm }
694
+
695
+ b = true
696
+
697
+ rest = {
698
+ n: 1,
699
+ nInserted: 0,
700
+ nModified: 1,
701
+ ok: 1,
702
+ }
703
+ }
704
+
705
+ }
706
+ else {
707
+ //不存在v.id, 若autoInsert則須插入
708
+ if (autoInsert) {
709
+
710
+ //push
711
+ let k = size(arrNew)
712
+ arrNew.push(v)
713
+ kp[v.id] = { k, v }
714
+
715
+ b = true
716
+
717
+ rest = {
718
+ n: 1,
719
+ nInserted: 1,
720
+ nModified: 0,
721
+ ok: 1,
722
+ }
723
+
724
+ }
725
+ else {
726
+ //未命中且未開啟autoInsert, 未命中故n為0
727
+ rest = {
728
+ n: 0,
729
+ nInserted: 0,
730
+ nModified: 0,
731
+ ok: 1,
732
+ }
733
+ }
734
+ }
735
+
736
+ }
737
+ catch (err) {
738
+
739
+ //本筆失敗不中斷整批, 以ok為0並附err回報, 由呼叫端逐筆檢查
740
+ rest = {
741
+ n: 1,
742
+ nInserted: 0,
743
+ nModified: 0,
744
+ ok: 0,
745
+ err: getErrMsg(err),
746
+ }
747
+
748
+ }
749
+
750
+ return rest
751
+ })
752
+
753
+ //write
754
+ if (b) {
755
+ await writeData(arrOld, arrNew)
756
+ }
757
+
758
+ }
759
+ catch (err) {
760
+ isErr = true
761
+ res = err
762
+ }
763
+
764
+ //update, 不能保證變更多少, 一律重設快取
765
+ _cache = null
766
+
767
+ //emit, 於change可能須使用select, 故須放在重設快取之後
768
+ if (!isErr) {
769
+
770
+ //emit, 逐筆失敗於該筆結果定案後發出, 每筆一次
771
+ //此事件之發出不表示整批失敗, 整批仍resolve, 該筆以ok為0回報
772
+ each(res, (rest, k) => {
773
+ if (get(rest, 'ok') === 0) {
774
+ emitError('save', [get(data, k)], get(rest, 'err'))
775
+ }
776
+ })
777
+
778
+ //emit, 逐筆插入另發出mode為insert之事件, 供呼叫端區辨新增與更新
779
+ //逐筆結果與輸入等長保序, 故由nInserted即可定位係哪幾筆走插入, 不須另行記錄
780
+ each(res, (rest, k) => {
781
+ if (get(rest, 'nInserted') === 1) {
782
+ emitChange('insert', [get(data, k)], rest)
783
+ }
784
+ })
785
+
786
+ //emit, 逐筆失敗之error事件已於上方發出, 故必早於本整批change
787
+ emitChange('save', data, res)
788
+
789
+ }
790
+
791
+ //check
792
+ if (isErr) {
793
+
794
+ //emit, 整批性錯誤須於reject之前發出
795
+ emitError('save', data, res)
796
+
797
+ return Promise.reject(res)
798
+ }
799
+
800
+ return res
801
+ }
802
+ async function save(data, option = {}) {
803
+ return serialize(() => saveCore(data, option))
804
+ }
805
+
806
+ /**
807
+ * 刪除數據,依主鍵刪除
808
+ *
809
+ * @memberOf WOrmLowdb
810
+ * @param {Object|Array} data 輸入數據物件或陣列
811
+ * @returns {Promise} 回傳Promise,resolve回傳與輸入等長之刪除結果陣列,各筆為{n,nDeleted,ok},n為主鍵命中筆數,單筆失敗或未給有效id者ok為0並附err,reject回傳錯誤訊息
812
+ */
813
+ async function delCore(data) {
814
+ let isErr = false
815
+
816
+ //check
817
+ if (!iseobj(data) && !isearr(data)) {
818
+ return []
819
+ }
820
+
821
+ //cloneDeep, 與外部數據脫勾
822
+ data = cloneDeep(data)
823
+
824
+ //res
825
+ let res = null
826
+ try {
827
+
828
+ //check
829
+ if (!isarr(data)) {
830
+ data = [data]
831
+ }
832
+
833
+ //read
834
+ let arrOld = await readData()
835
+
836
+ //kp
837
+ let kp = getKpPk(arrOld)
838
+
839
+ //pmSeries
840
+ let kpDel = {}
841
+ let b = false
842
+ res = await pmSeries(data, async(v) => {
843
+
844
+ //rest
845
+ let rest = null
846
+
847
+ try {
848
+
849
+ //id
850
+ let id = get(v, 'id', '')
851
+
852
+ //check
853
+ if (!isestr(id)) {
854
+ //未給有效v.id視為該筆數據有問題而無法處理, 以ok為0並附err回報,
855
+ //與[已給v.id但查無數據]之ok為1有別, 二者須可由ok分辨
856
+ //註: 亦不可將無效主鍵送進查詢條件, 故於此直接定案
857
+ rest = {
858
+ n: 0,
859
+ nDeleted: 0,
860
+ ok: 0,
861
+ err: `can not delete by invalid id[${id}]`,
862
+ }
863
+ }
864
+ else {
865
+
866
+ //查找資料表內v.id
867
+ let r = get(kp, id, null)
868
+
869
+ //check
870
+ if (iseobj(r)) {
871
+ //已存在v.id則須刪除
872
+
873
+ //push
874
+ kpDel[r.k] = true
875
+
876
+ //update, 須同步移除, 否則同批重複主鍵者會重複計入nDeleted
877
+ delete kp[id]
878
+
879
+ b = true
880
+
881
+ rest = {
882
+ n: 1,
883
+ nDeleted: 1,
884
+ ok: 1,
885
+ }
886
+
887
+ }
888
+ else {
889
+ //不存在v.id則不刪除, 未命中故n為0
890
+ rest = {
891
+ n: 0,
892
+ nDeleted: 0,
893
+ ok: 1,
894
+ }
895
+
896
+ }
897
+
898
+ }
899
+
900
+ }
901
+ catch (err) {
902
+
903
+ //本筆失敗不中斷整批, 以ok為0並附err回報, 由呼叫端逐筆檢查
904
+ rest = {
905
+ n: 1,
906
+ nDeleted: 0,
907
+ ok: 0,
908
+ err: getErrMsg(err),
909
+ }
910
+
911
+ }
912
+
913
+ return rest
914
+ })
915
+
916
+ //write
917
+ if (b) {
918
+ let arrNew = filter(arrOld, (v, k) => {
919
+ return !haskey(kpDel, k)
920
+ })
921
+ await writeData(arrOld, arrNew)
922
+ }
923
+
924
+ }
925
+ catch (err) {
926
+ isErr = true
927
+ res = err
928
+ }
929
+
930
+ //update, 不能保證刪除多少, 一律重設快取
931
+ _cache = null
932
+
933
+ //emit, 於change可能須使用select, 故須放在重設快取之後
934
+ if (!isErr) {
935
+
936
+ //emit, 逐筆失敗於該筆結果定案後發出, 每筆一次
937
+ //此事件之發出不表示整批失敗, 整批仍resolve, 該筆以ok為0回報
938
+ each(res, (rest, k) => {
939
+ if (get(rest, 'ok') === 0) {
940
+ emitError('del', [get(data, k)], get(rest, 'err'))
941
+ }
942
+ })
943
+
944
+ //emit, 逐筆失敗之error事件已於上方發出, 故必早於本整批change
945
+ emitChange('del', data, res)
946
+
947
+ }
948
+
949
+ //check
950
+ if (isErr) {
951
+
952
+ //emit, 整批性錯誤須於reject之前發出
953
+ emitError('del', data, res)
954
+
955
+ return Promise.reject(res)
956
+ }
957
+
958
+ return res
959
+ }
960
+ async function del(data) {
961
+ return serialize(() => delCore(data))
962
+ }
963
+
964
+ /**
965
+ * 刪除全部數據,需與del分開,避免未傳數據導致直接刪除全表
966
+ *
967
+ * @memberOf WOrmLowdb
968
+ * @param {Object} [find={}] 輸入刪除條件物件
969
+ * @returns {Promise} 回傳Promise,resolve回傳刪除結果物件{n,nDeleted,ok},n與nDeleted同為實際刪除筆數,reject回傳錯誤訊息
970
+ */
971
+ async function delAllCore(find = {}) {
972
+ let isErr = false
973
+
974
+ //res
975
+ let res = null
976
+ try {
977
+
978
+ //read
979
+ let arrOld = await readData()
980
+
981
+ //filter
982
+ let nAll = size(arrOld)
983
+ let nDel = 0
984
+ let arrNew = null
985
+ if (iseobj(find)) {
986
+
987
+ //q
988
+ let q = new Query(find)
989
+ // console.log('q', q)
990
+
991
+ //filter, 以與select同一條件逐筆判定, 保留未命中者
992
+ arrNew = filter(arrOld, (v) => {
993
+ return !q.test(v)
994
+ })
995
+
996
+ //nDel
997
+ nDel = nAll - size(arrNew)
998
+ // console.log('nDel', nDel)
999
+
1000
+ }
1001
+ else {
1002
+ //find未給或為空物件時刪除全部數據
1003
+
1004
+ //nDel
1005
+ nDel = nAll
1006
+
1007
+ //empty
1008
+ arrNew = []
1009
+
1010
+ }
1011
+
1012
+ //write, 無命中即不寫檔, 屬正常結果
1013
+ if (nDel > 0) {
1014
+ await writeData(arrOld, arrNew)
1015
+ }
1016
+
1017
+ //res, n為實際刪除筆數而非全表筆數, 故與nDeleted同值
1018
+ res = {
1019
+ n: nDel,
1020
+ nDeleted: nDel,
1021
+ ok: 1,
1022
+ }
1023
+
1024
+ }
1025
+ catch (err) {
1026
+ isErr = true
1027
+ res = err
1028
+ }
1029
+
1030
+ //update, 不能保證刪除多少, 一律重設快取
1031
+ _cache = null
1032
+
1033
+ //emit, 於change可能須使用select, 故須放在重設快取之後
1034
+ if (!isErr) {
1035
+ emitChange('delAll', null, res)
1036
+ }
1037
+
1038
+ //check
1039
+ if (isErr) {
1040
+
1041
+ //emit, 整批性錯誤須於reject之前發出
1042
+ emitError('delAll', null, res)
1043
+
1044
+ return Promise.reject(res)
1045
+ }
1046
+
1047
+ return res
1048
+ }
1049
+ async function delAll(find = {}) {
1050
+ return serialize(() => delAllCore(find))
1051
+ }
1052
+
1053
+ //bind
1054
+ ee.select = select
1055
+ ee.selectByPk = selectByPk
1056
+ ee.insert = insert
1057
+ ee.insertBulk = insertBulk
1058
+ ee.save = save
1059
+ ee.del = del
1060
+ ee.delAll = delAll
1061
+
1062
+ return ee
1063
+ }
1064
+
1065
+
1066
+ export default WOrmLowdb