w-orm-reladb 1.0.47 → 1.0.48

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.
@@ -29,7 +29,7 @@
29
29
  <nav >
30
30
 
31
31
 
32
- <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WOrmReladb.html">WOrmReladb</a><ul class='methods'><li data-type='method'><a href="WOrmReladb.html#.closeSequelize">closeSequelize</a></li><li data-type='method'><a href="WOrmReladb.html#.createStorage">createStorage</a></li><li data-type='method'><a href="WOrmReladb.html#.del">del</a></li><li data-type='method'><a href="WOrmReladb.html#.delAll">delAll</a></li><li data-type='method'><a href="WOrmReladb.html#.genModelsByDB">genModelsByDB</a></li><li data-type='method'><a href="WOrmReladb.html#.genModelsByTabs">genModelsByTabs</a></li><li data-type='method'><a href="WOrmReladb.html#.genTransaction">genTransaction</a></li><li data-type='method'><a href="WOrmReladb.html#.initSequelize">initSequelize</a></li><li data-type='method'><a href="WOrmReladb.html#.insert">insert</a></li><li data-type='method'><a href="WOrmReladb.html#.save">save</a></li><li data-type='method'><a href="WOrmReladb.html#.select">select</a></li></ul></li></ul>
32
+ <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WOrmReladb.html">WOrmReladb</a><ul class='methods'><li data-type='method'><a href="WOrmReladb.html#.closeSequelize">closeSequelize</a></li><li data-type='method'><a href="WOrmReladb.html#.createStorage">createStorage</a></li><li data-type='method'><a href="WOrmReladb.html#.del">del</a></li><li data-type='method'><a href="WOrmReladb.html#.delAll">delAll</a></li><li data-type='method'><a href="WOrmReladb.html#.genModelsByDB">genModelsByDB</a></li><li data-type='method'><a href="WOrmReladb.html#.genModelsByTabs">genModelsByTabs</a></li><li data-type='method'><a href="WOrmReladb.html#.genTransaction">genTransaction</a></li><li data-type='method'><a href="WOrmReladb.html#.initSequelize">initSequelize</a></li><li data-type='method'><a href="WOrmReladb.html#.insert">insert</a></li><li data-type='method'><a href="WOrmReladb.html#.insertBulk">insertBulk</a></li><li data-type='method'><a href="WOrmReladb.html#.save">save</a></li><li data-type='method'><a href="WOrmReladb.html#.select">select</a></li><li data-type='method'><a href="WOrmReladb.html#.selectByPk">selectByPk</a></li></ul></li></ul>
33
33
 
34
34
  </nav>
35
35
 
@@ -46,7 +46,6 @@
46
46
  <section>
47
47
  <article>
48
48
  <pre class="prettyprint source linenums"><code>import path from 'path'
49
- import events from 'events'
50
49
  import Sequelize from 'sequelize'
51
50
  import cloneDeep from 'lodash-es/cloneDeep.js'
52
51
  import get from 'lodash-es/get.js'
@@ -54,13 +53,19 @@ import map from 'lodash-es/map.js'
54
53
  import each from 'lodash-es/each.js'
55
54
  import size from 'lodash-es/size.js'
56
55
  import split from 'lodash-es/split.js'
56
+ import keys from 'lodash-es/keys.js'
57
+ import pick from 'lodash-es/pick.js'
58
+ import omit from 'lodash-es/omit.js'
59
+ import isEqual from 'lodash-es/isEqual.js'
60
+ import evem from 'wsemi/src/evem.mjs'
57
61
  import genPm from 'wsemi/src/genPm.mjs'
58
- import genID from 'wsemi/src/genID.mjs'
62
+ import genIDSeq from 'wsemi/src/genIDSeq.mjs'
59
63
  import isarr from 'wsemi/src/isarr.mjs'
60
64
  import isearr from 'wsemi/src/isearr.mjs'
61
65
  import isobj from 'wsemi/src/isobj.mjs'
62
66
  import iseobj from 'wsemi/src/iseobj.mjs'
63
67
  import isbol from 'wsemi/src/isbol.mjs'
68
+ import isnum from 'wsemi/src/isnum.mjs'
64
69
  import isestr from 'wsemi/src/isestr.mjs'
65
70
  import arrHas from 'wsemi/src/arrHas.mjs'
66
71
  import pmSeries from 'wsemi/src/pmSeries.mjs'
@@ -91,7 +96,7 @@ let pmq = pmQueue(1)
91
96
  * @param {String} [opt.modelType='js'] 輸入資料表設定檔類型字串,可有'js'、'json',預設'js'
92
97
  * @param {Boolean} [opt.logging=false] 輸入是否輸出實際執行的sql指令布林值,預設false
93
98
  * @param {String} [opt.pk='id'] 輸入數據主鍵字串,預設'id'
94
- * @param {Boolean} [opt.autoGenPK=true] 輸入若數據pk(id)欄位沒給時則自動給予隨機uuid,型別為布林值,預設true
99
+ * @param {Boolean} [opt.autoGenPk=true] 輸入若數據pk(id)欄位沒給時則自動給予隨機uuid,型別為布林值,預設true
95
100
  * @param {Boolean} [opt.useStable=true] 輸入是否使用穩定模式,使用佇列管理同時只能進行一種操作故會犧牲效能,sqlite需開啟穩定模式才不會有非預期錯誤,型別為布林值,預設true
96
101
  * @returns {Object} 回傳操作資料庫物件,各事件功能詳見說明
97
102
  */
@@ -121,8 +126,8 @@ function WOrmReladb(opt = {}) {
121
126
  if (!isestr(opt.pk)) {
122
127
  opt.pk = 'id'
123
128
  }
124
- if (!isbol(opt.autoGenPK)) {
125
- opt.autoGenPK = true
129
+ if (!isbol(opt.autoGenPk)) {
130
+ opt.autoGenPk = true
126
131
  }
127
132
  if (!isbol(opt.useStable)) {
128
133
  opt.useStable = true
@@ -140,8 +145,18 @@ function WOrmReladb(opt = {}) {
140
145
  let Op = Sequelize.Op
141
146
 
142
147
 
143
- //ee
144
- let ee = new events.EventEmitter()
148
+ //ee, 採wsemi之evem(即eventemitter3), 其於'error'無監聽者時僅回傳false而不拋出,
149
+ //故本套件之操作行為不因呼叫端有無註冊監聽而改變; Node內建之EventEmitter具該拋出語義, 不可用
150
+ let ee = evem()
151
+
152
+
153
+ //opt.url解析失敗一律拋出, 因其屬呼叫端未履行契約, 且解析失敗後各函數皆無從運作,
154
+ //建構為同步而無從以Promise.reject回報, 拋出即為reject之同步對應;
155
+ //若僅console.log而回傳未綁定各函數之ee, 錯誤不經任何管道抵達呼叫端, 呼叫端僅得到
156
+ //[w.select is not a function]之無關訊息, 與本規格[錯誤不得靜默]之通則相違
157
+ let throwUrlErr = (msg) => {
158
+ throw new Error(`${msg}, opt.url[${opt.url}]`)
159
+ }
145
160
 
146
161
 
147
162
  //dialect
@@ -149,12 +164,10 @@ function WOrmReladb(opt = {}) {
149
164
  ss = split(opt.url, '://')
150
165
  dialect = get(ss, 0, null)
151
166
  if (!dialect) {
152
- console.log('no dialect in opt.url')
153
- return ee
167
+ throwUrlErr('no dialect in opt.url')
154
168
  }
155
169
  if (!arrHas(dialect, ['mssql', 'sqlite', 'mysql', 'mariadb', 'postgres'])) {
156
- console.log('invalid dialect in opt.url')
157
- return ee
170
+ throwUrlErr('invalid dialect in opt.url')
158
171
  }
159
172
  u = get(ss, 1, '') //另存給後面使用
160
173
 
@@ -167,19 +180,16 @@ function WOrmReladb(opt = {}) {
167
180
  ss = get(ss, 0, '')
168
181
  ss = split(ss, ':')
169
182
  if (size(ss) !== 2) {
170
- console.log('invalid username or password in opt.url')
171
- return ee
183
+ throwUrlErr('invalid username or password in opt.url')
172
184
  }
173
185
  username = get(ss, 0, '')
174
186
  password = get(ss, 1, '')
175
187
  if (dialect !== 'sqlite' || (dialect === 'sqlite' &amp;&amp; opt.useEncryption)) {
176
188
  if (username === '') {
177
- console.log('invalid username in opt.url')
178
- return ee
189
+ throwUrlErr('invalid username in opt.url')
179
190
  }
180
191
  if (password === '') {
181
- console.log('invalid password in opt.url')
182
- return ee
192
+ throwUrlErr('invalid password in opt.url')
183
193
  }
184
194
  }
185
195
 
@@ -188,24 +198,161 @@ function WOrmReladb(opt = {}) {
188
198
  let host
189
199
  let port
190
200
  ss = split(u, ':')
191
- // if (size(ss) !== 2) {
192
- // console.log('invalid host or port in opt.url')
193
- // return ee
194
- // }
195
201
  host = get(ss, 0, null)
196
202
  port = get(ss, 1, null)
197
203
  if (dialect !== 'sqlite') {
198
204
  if (!host) {
199
- console.log('invalid host in opt.url')
200
- return ee
205
+ throwUrlErr('invalid host in opt.url')
201
206
  }
202
207
  if (!port) {
203
- console.log('invalid port in opt.url')
204
- return ee
208
+ throwUrlErr('invalid port in opt.url')
205
209
  }
206
210
  }
207
211
 
208
212
 
213
+ /**
214
+ * 取錯誤訊息字串,供逐筆結果之err欄位與error事件使用
215
+ *
216
+ * @ignore
217
+ * @param {Error|String} err 輸入錯誤物件或字串
218
+ * @returns {String} 回傳錯誤訊息字串
219
+ */
220
+ function getErrMsg(err) {
221
+
222
+ //message
223
+ let message = get(err, 'message')
224
+ if (isestr(message)) {
225
+ return message
226
+ }
227
+
228
+ return String(err)
229
+ }
230
+
231
+
232
+ /**
233
+ * 發出change事件,訂閱函數拋錯不得影響本次操作之結果,故另包try並自行吞掉
234
+ *
235
+ * @ignore
236
+ * @param {String} mode 輸入操作別字串
237
+ * @param {Object|Array|null} data 輸入本次操作之數據
238
+ * @param {Object|Array} res 輸入本次操作之結果
239
+ * @returns {undefined} 無回傳值
240
+ */
241
+ function emitChange(mode, data, res) {
242
+ try {
243
+ ee.emit('change', mode, data, res)
244
+ }
245
+ catch (err) {
246
+ console.log(err)
247
+ }
248
+ }
249
+
250
+
251
+ /**
252
+ * 發出error事件,操作發生錯誤時發出,錯誤訊息一律轉為字串
253
+ * 註: 事件僅為附加通知,所送出之資訊必另有正規管道——整批性錯誤經Promise.reject,逐筆失敗經該筆之err欄位
254
+ * 註: 訂閱函數拋錯不得影響本次操作之結果,故另包try並自行吞掉
255
+ * 註: 正常結果不得發出本事件,如insert全數已存在、save合併後內容相同、del主鍵未命中、selectByPk查無數據
256
+ *
257
+ * @ignore
258
+ * @param {String} mode 輸入操作別字串
259
+ * @param {Object|Array|null} data 輸入本次操作之數據
260
+ * @param {Error|String} err 輸入錯誤物件或字串
261
+ * @returns {undefined} 無回傳值
262
+ */
263
+ function emitError(mode, data, err) {
264
+ try {
265
+ ee.emit('error', mode, data, getErrMsg(err))
266
+ }
267
+ catch (errEmit) {
268
+ console.log(errEmit)
269
+ }
270
+ }
271
+
272
+
273
+ /**
274
+ * 判定主鍵值是否有效
275
+ * 註: select以外之五函數對主鍵之認定一律以本函數為準,令selectByPk之[命中]與insert、save、del內對既有數據之認定一致
276
+ *
277
+ * @ignore
278
+ * @param {*} v 輸入主鍵值
279
+ * @returns {Boolean} 回傳主鍵值是否有效布林值
280
+ */
281
+ function isEffPk(v) {
282
+
283
+ //非空字串
284
+ if (isestr(v)) {
285
+ return true
286
+ }
287
+
288
+ //數值, 含0, 供整數型主鍵使用
289
+ if (isnum(v)) {
290
+ return true
291
+ }
292
+
293
+ return false
294
+ }
295
+
296
+
297
+ /**
298
+ * 判定是否為主鍵重複之錯誤
299
+ * 註: sequelize於mssql、sqlite、mysql、mariadb、postgres皆將唯一鍵衝突轉為UniqueConstraintError,故此判定與dialect無關
300
+ *
301
+ * @ignore
302
+ * @param {Error} err 輸入錯誤物件
303
+ * @returns {Boolean} 回傳是否為主鍵重複錯誤布林值
304
+ */
305
+ function isDupPkError(err) {
306
+ return err instanceof Sequelize.UniqueConstraintError
307
+ }
308
+
309
+
310
+ /**
311
+ * 檢查並補齊單筆數據之主鍵
312
+ * autoGenPk為true時未帶有效主鍵者自動產生,為false時往外拋
313
+ * 註: 未帶有效主鍵屬呼叫端未履行契約而非某一筆資料本身之問題,故為整批性錯誤而不降級為該筆ok為0,
314
+ * 若降級為逐筆結果,呼叫端易於整批resolve之下漏看,使[忘了給主鍵]靜默變成[少寫了幾筆]
315
+ * 註: 本函數須於任何寫入之前一次對全部數據完成,令拋錯時同批之有效筆數亦不會被寫入
316
+ * 註: 自動產生之主鍵值為字串,故補值前檢查主鍵欄位確為字串類型,不符者以明確訊息拋出,
317
+ * 而非任由資料庫回報型別錯誤; 本套件之model定義於手上得知主鍵型別,故不適用T6之[套件無從得知主鍵型別]例外
318
+ *
319
+ * @ignore
320
+ * @param {Object} md 輸入資料表model物件
321
+ * @param {Object} v 輸入數據物件
322
+ * @param {Number} k 輸入數據於陣列內之索引
323
+ * @returns {Object} 回傳補齊主鍵之數據物件
324
+ */
325
+ function procPk(md, v, k) {
326
+
327
+ //check
328
+ if (isEffPk(get(v, opt.pk))) {
329
+ return v
330
+ }
331
+
332
+ //check, autoGenPk為false時主鍵須由呼叫端自備
333
+ if (!opt.autoGenPk) {
334
+ throw new Error(`invalid data[${k}].${opt.pk}, autoGenPk is false`)
335
+ }
336
+
337
+ //at
338
+ let at = get(md, `rawAttributes.${opt.pk}`)
339
+ if (!iseobj(at)) {
340
+ throw new Error(`can not find pk column [${opt.pk}] in model [${opt.cl}]`)
341
+ }
342
+
343
+ //check, 自動產生之主鍵值為字串, 主鍵欄位須為字串類型方能容納
344
+ let tp = get(at, 'type.key', '')
345
+ if (!arrHas(tp, ['STRING', 'TEXT', 'CHAR', 'UUID', 'CITEXT'])) {
346
+ throw new Error(`autoGenPk is true but pk column [${opt.pk}] in model [${opt.cl}] is [${tp}], auto generated pk is a string, need to set autoGenPk to false and supply pk by caller`)
347
+ }
348
+
349
+ //genIDSeq, 為UUIDv7格式而具單調遞增性, 主鍵接近順序遞增可減少索引頁面分裂
350
+ v[opt.pk] = genIDSeq()
351
+
352
+ return v
353
+ }
354
+
355
+
209
356
  /**
210
357
  * 初始化sequelize
211
358
  *
@@ -398,23 +545,27 @@ function WOrmReladb(opt = {}) {
398
545
  }
399
546
  }
400
547
 
548
+ //isErr, res
549
+ let isErr = false
550
+ let res = null
551
+
401
552
  //si
402
553
  let si = instance
403
554
  if (instance === null) {
404
555
  si = await initSequelize()
405
556
  }
406
- // else {
407
- // console.log('select use instance', instance)
408
- // }
409
557
 
410
- //rs
411
- let rs = null
412
- if (!si.err) {
558
+ try {
559
+
560
+ //check, 連線或匯入models失敗影響本次操作之全部數據, 屬整批性錯誤
561
+ if (si.err) {
562
+ throw si.err
563
+ }
413
564
 
414
565
  //md
415
566
  let md = si.mds[opt.cl]
416
567
 
417
- //setting
568
+ //setting, raw令回傳純數據物件而不含資料庫內部欄位
418
569
  let setting = {
419
570
  where: useFind,
420
571
  raw: true,
@@ -423,32 +574,150 @@ function WOrmReladb(opt = {}) {
423
574
  setting.transaction = transaction
424
575
  }
425
576
 
426
- //findAll
427
- rs = await md.findAll(setting)
577
+ //findAll, 恆回傳陣列, 無符合數據為空陣列
578
+ res = await md.findAll(setting)
579
+ if (!isarr(res)) {
580
+ res = []
581
+ }
428
582
 
429
583
  }
430
- else {
431
- ee.emit('error', si.err)
584
+ catch (err) {
585
+ isErr = true
586
+ res = err
432
587
  }
588
+ finally {
433
589
 
434
- //closeSequelize
435
- if (instance === null) { //內部自動初始化得close
436
- await closeSequelize('select')
590
+ //closeSequelize
591
+ if (instance === null) { //內部自動初始化得close
592
+ await closeSequelize('select')
593
+ }
594
+
595
+ }
596
+
597
+ //check
598
+ if (isErr) {
599
+ emitError('select', null, res)
600
+ return Promise.reject(res)
437
601
  }
438
602
 
439
- return rs
603
+ return res
440
604
  }
441
605
 
442
606
 
443
607
  /**
444
- * 插入數據,插入同樣數據會自動產生不同_id,故insert前需自行判斷有無重複
608
+ * 由主鍵查詢單筆數據,因由資料庫依主鍵索引取值且僅回傳單筆,不需如select提取全部符合數據再處理,故數據量大時效能較佳
609
+ *
610
+ * 註: 主鍵欄位取自opt.pk,預設為id,支援由呼叫端指定
611
+ * 註: 主鍵未命中或主鍵值無效皆回傳null而不reject,[命中]之判定基準與insert、save、del內對既有數據之認定一致
612
+ * 註: 本函數不得有副作用,故不創建資料表
613
+ *
614
+ * @memberOf WOrmReladb
615
+ * @param {String|Number} pk 輸入主鍵值,即數據內opt.pk欄位之值
616
+ * @param {Object} [option={}] 輸入設定物件,預設為{}
617
+ * @param {Object} [option.instance=null] 輸入實例instance物件,預設為null
618
+ * @param {Object} [option.transaction=null] 輸入交易(transaction)物件,預設為null
619
+ * @returns {Promise} 回傳Promise,resolve回傳數據物件,若無此主鍵或主鍵值無效則回傳null,reject回傳錯誤訊息
620
+ */
621
+ async function selectByPk(pk, option = {}) {
622
+
623
+ //check, 未給有效主鍵值視為查無數據, 不送查詢以免無效值被轉為null而誤中其他數據
624
+ if (!isEffPk(pk)) {
625
+ return null
626
+ }
627
+
628
+ //instance
629
+ let instance = get(option, 'instance', null)
630
+
631
+ //transaction
632
+ let transaction = get(option, 'transaction', null)
633
+
634
+ //check
635
+ if (opt.useEncryption &amp;&amp; dialect === 'sqlite') {
636
+ if (transaction !== null) {
637
+ console.log('@journeyapps/sqlcipher can not support transaction.')
638
+ }
639
+ }
640
+
641
+ //isErr, res
642
+ let isErr = false
643
+ let res = null
644
+
645
+ //si
646
+ let si = instance
647
+ if (instance === null) {
648
+ si = await initSequelize()
649
+ }
650
+
651
+ try {
652
+
653
+ //check
654
+ if (si.err) {
655
+ throw si.err
656
+ }
657
+
658
+ //md
659
+ let md = si.mds[opt.cl]
660
+
661
+ //setting
662
+ let setting = {
663
+ where: { [opt.pk]: pk },
664
+ raw: true,
665
+ }
666
+ if (transaction !== null) {
667
+ setting.transaction = transaction
668
+ }
669
+
670
+ //findOne
671
+ let v = await md.findOne(setting)
672
+
673
+ //check, 判定基準與insert、save、del內對既有數據之認定一致
674
+ if (iseobj(v)) {
675
+ res = v
676
+ }
677
+ else {
678
+ //不存在主鍵, 回傳null
679
+ res = null
680
+ }
681
+
682
+ }
683
+ catch (err) {
684
+ isErr = true
685
+ res = err
686
+ }
687
+ finally {
688
+
689
+ //closeSequelize
690
+ if (instance === null) { //內部自動初始化得close
691
+ await closeSequelize('selectByPk')
692
+ }
693
+
694
+ }
695
+
696
+ //check
697
+ if (isErr) {
698
+ emitError('selectByPk', null, res)
699
+ return Promise.reject(res)
700
+ }
701
+
702
+ return res
703
+ }
704
+
705
+
706
+ /**
707
+ * 插入數據,僅於主鍵不存在時寫入,已存在者跳過且不覆寫
708
+ *
709
+ * 由資料庫於主鍵之唯一約束上原子完成[檢查主鍵不存在]與[寫入],併發時同一主鍵僅有一次成功
710
+ *
711
+ * 註: n為輸入筆數即本次嘗試插入之基準,nInserted為實際插入筆數,全數已存在而nInserted為0屬正常結果
712
+ * 註: 同批含重複主鍵時僅首筆計入nInserted,其餘視為已存在
713
+ * 註: opt.autoGenPk為true(預設)時未帶有效主鍵者自動產生,為false時未帶有效主鍵即reject且同批皆不寫入
445
714
  *
446
715
  * @memberOf WOrmReladb
447
716
  * @param {Object|Array} data 輸入數據物件或陣列
448
717
  * @param {Object} [option={}] 輸入設定物件,預設為{}
449
718
  * @param {Object} [option.instance=null] 輸入實例instance物件,預設為null
450
719
  * @param {Object} [option.transaction=null] 輸入交易(transaction)物件,預設為null
451
- * @returns {Promise} 回傳Promise,resolve回傳插入結果,reject回傳錯誤訊息
720
+ * @returns {Promise} 回傳Promise,resolve回傳插入結果{n,nInserted,ok},reject回傳錯誤訊息
452
721
  */
453
722
  async function insert(data, option = {}) {
454
723
 
@@ -477,19 +746,22 @@ function WOrmReladb(opt = {}) {
477
746
  //cloneDeep
478
747
  data = cloneDeep(data)
479
748
 
480
- //pm
481
- let pm = genPm()
749
+ //isErr, res
750
+ let isErr = false
751
+ let res = null
482
752
 
483
753
  //si
484
754
  let si = instance
485
755
  if (instance === null) {
486
756
  si = await initSequelize()
487
757
  }
488
- // else {
489
- // console.log('insert use instance', instance)
490
- // }
491
758
 
492
- if (!si.err) {
759
+ try {
760
+
761
+ //check
762
+ if (si.err) {
763
+ throw si.err
764
+ }
493
765
 
494
766
  //md
495
767
  let md = si.mds[opt.cl]
@@ -499,18 +771,13 @@ function WOrmReladb(opt = {}) {
499
771
  data = [data]
500
772
  }
501
773
 
502
- //n
503
- let n = size(data)
774
+ //procPk, 須於任何寫入之前一次完成, 令autoGenPk為false而拋錯時同批之有效筆數亦不會被寫入
775
+ data = map(data, (v, k) => {
776
+ return procPk(md, v, k)
777
+ })
504
778
 
505
- //check
506
- if (opt.autoGenPK) {
507
- data = map(data, function(v) {
508
- if (!v[opt.pk]) {
509
- v[opt.pk] = genID()
510
- }
511
- return v
512
- })
513
- }
779
+ //nAll, n之基準為輸入筆數
780
+ let nAll = size(data)
514
781
 
515
782
  //setting
516
783
  let setting = { }
@@ -518,53 +785,100 @@ function WOrmReladb(opt = {}) {
518
785
  setting.transaction = transaction
519
786
  }
520
787
 
521
- //bulkCreate
522
- await md.bulkCreate(data, setting)
523
- .then((res) => {
524
- res = { n, nInserted: n, ok: 1 }
525
- pm.resolve(res)
526
- ee.emit('change', 'insert', data, res)
527
- })
528
- .catch(({ original }) => {
529
- ee.emit('error', original)
530
- pm.reject({ n: 0, ok: 0 })
531
- })
788
+ //create, 逐筆插入以取得精確之nInserted
789
+ //註: 不採bulkCreate配合ignoreDuplicates, 因mssql未宣告該能力而選項會被靜默忽略仍撞唯一約束,
790
+ //且bulkCreate無從回報實際插入筆數; 單筆INSERT本即由主鍵之唯一約束原子完成檢查與寫入
791
+ let nInserted = 0
792
+ await pmSeries(data, async(v) => {
793
+
794
+ //bIns, 撞既有主鍵者視為已存在而跳過, 其餘錯誤影響全部數據須往外拋
795
+ let bIns = await md.create(v, setting)
796
+ .then(() => {
797
+ return true
798
+ })
799
+ .catch((err) => {
800
+ if (isDupPkError(err)) {
801
+ return false
802
+ }
803
+ throw err
804
+ })
805
+
806
+ //nInserted
807
+ if (bIns) {
808
+ nInserted += 1
809
+ }
810
+
811
+ return bIns
812
+ })
813
+
814
+ //res, 全數已存在而nInserted為0屬正常結果, 不視為錯誤
815
+ res = {
816
+ n: nAll,
817
+ nInserted,
818
+ ok: 1,
819
+ }
532
820
 
533
821
  }
534
- else {
535
- pm.reject(si.err)
822
+ catch (err) {
823
+ isErr = true
824
+ res = err
536
825
  }
826
+ finally {
827
+
828
+ //closeSequelize
829
+ if (instance === null) { //內部自動初始化得close
830
+ await closeSequelize('insert')
831
+ }
537
832
 
538
- //closeSequelize
539
- if (instance === null) { //內部自動初始化得close
540
- await closeSequelize('insert')
541
833
  }
542
834
 
543
- return pm
835
+ //check
836
+ if (isErr) {
837
+ emitError('insert', data, res)
838
+ return Promise.reject(res)
839
+ }
840
+
841
+ //emit, 須於結果定案後發出
842
+ emitChange('insert', data, res)
843
+
844
+ return res
544
845
  }
545
846
 
546
847
 
547
848
  /**
548
- * 儲存數據
849
+ * 批次插入數據,全批視為一個單位:全部插入成功,或一筆都不寫入
850
+ *
851
+ * 本函數非insert之加速版,兩者衝突政策不同:insert於主鍵已存在時跳過該筆且整批ok為1,
852
+ * 本函數則整批reject且不寫入任何一筆,同批含重複主鍵者亦視為衝突。
853
+ * 確無衝突時兩者之可觀察結果完全相同,差異僅於有衝突時顯現。
854
+ *
855
+ * 註: n為輸入筆數即本次嘗試插入之基準;nInserted於成功時恆等於n,雖無額外資訊仍保留,
856
+ * 令呼叫端得與insert共用同一段結果處理程式碼
857
+ * 註: 不提供逐筆結果,故不出現ok為0與err;需要逐筆處置者改用insert
858
+ * 註: opt.autoGenPk為true(預設)時未帶有效主鍵者自動產生,為false時未帶有效主鍵即reject且同批皆不寫入
859
+ * 註: [全有全無]為獨立於T7之額外要求——T7僅保證每筆之[檢查主鍵不存在]與[寫入]為原子,不保證整批。
860
+ * mssql因綁定參數數量上限會由驅動層自動拆為多語句送出,實測1000筆之批次於末筆撞主鍵時已有946筆落盤,
861
+ * 故一律以交易包覆並於失敗時回滾;呼叫端已給transaction時改以巢狀交易(SAVEPOINT)包覆,
862
+ * 令回滾範圍限於本次呼叫而不影響呼叫端交易內先前之寫入
549
863
  *
550
864
  * @memberOf WOrmReladb
551
865
  * @param {Object|Array} data 輸入數據物件或陣列
552
866
  * @param {Object} [option={}] 輸入設定物件,預設為{}
553
867
  * @param {Object} [option.instance=null] 輸入實例instance物件,預設為null
554
868
  * @param {Object} [option.transaction=null] 輸入交易(transaction)物件,預設為null
555
- * @param {boolean} [option.autoInsert=true] 輸入是否於儲存時發現原本無數據,則自動改以插入處理,預設為true
556
- * @returns {Promise} 回傳Promise,resolve回傳儲存結果,reject回傳錯誤訊息
869
+ * @returns {Promise} 回傳Promise,resolve回傳插入結果{n,nInserted,ok},reject回傳錯誤訊息
557
870
  */
558
- async function save(data, option = {}) {
871
+ async function insertBulk(data, option = {}) {
559
872
 
560
873
  //check
561
874
  if (!iseobj(data) &amp;&amp; !isearr(data)) {
562
- return []
875
+ return {
876
+ n: 0,
877
+ nInserted: 0,
878
+ ok: 1,
879
+ }
563
880
  }
564
881
 
565
- //cloneDeep
566
- data = cloneDeep(data)
567
-
568
882
  //instance
569
883
  let instance = get(option, 'instance', null)
570
884
 
@@ -578,22 +892,25 @@ function WOrmReladb(opt = {}) {
578
892
  }
579
893
  }
580
894
 
581
- //autoInsert
582
- let autoInsert = get(option, 'autoInsert', true)
895
+ //cloneDeep
896
+ data = cloneDeep(data)
583
897
 
584
- //pm
585
- let pm = genPm()
898
+ //isErr, res
899
+ let isErr = false
900
+ let res = null
586
901
 
587
902
  //si
588
903
  let si = instance
589
904
  if (instance === null) {
590
905
  si = await initSequelize()
591
906
  }
592
- // else {
593
- // console.log('save use instance', instance)
594
- // }
595
907
 
596
- if (!si.err) {
908
+ try {
909
+
910
+ //check
911
+ if (si.err) {
912
+ throw si.err
913
+ }
597
914
 
598
915
  //md
599
916
  let md = si.mds[opt.cl]
@@ -603,157 +920,379 @@ function WOrmReladb(opt = {}) {
603
920
  data = [data]
604
921
  }
605
922
 
606
- //check
607
- if (opt.autoGenPK) {
608
- data = map(data, function(v) {
609
- if (!v[opt.pk]) {
610
- v[opt.pk] = genID()
611
- }
612
- return v
613
- })
614
- }
923
+ //procPk, 須於任何寫入之前一次完成, 令autoGenPk為false而拋錯時同批皆不會被寫入
924
+ data = map(data, (v, k) => {
925
+ return procPk(md, v, k)
926
+ })
615
927
 
616
- // //tr
617
- // let t = null
618
- // let tr = {}
619
- // if (atomic) {
620
- // t = await sequelize.transaction() //使用Unmanaged transaction (then-callback)
621
- // tr = {
622
- // transaction: t
623
- // }
624
- // }
928
+ //nAll, n之基準為輸入筆數
929
+ let nAll = size(data)
625
930
 
626
- //pmSeries
627
- await pmSeries(data, async function(v) {
628
- let pmm = genPm()
931
+ //useTx, sqlcipher不支援transaction, 該情形下改倚賴sqlite單一語句之原子性
932
+ let useTx = !(opt.useEncryption &amp;&amp; dialect === 'sqlite')
629
933
 
630
- //err
631
- let err = null
934
+ //t, 呼叫端未給交易時自開; 已給時以巢狀交易(SAVEPOINT)包覆令回滾範圍限於本次呼叫
935
+ let t = null
936
+ if (useTx) {
937
+ t = (transaction !== null)
938
+ ? await md.sequelize.transaction({ transaction })
939
+ : await md.sequelize.transaction()
940
+ }
632
941
 
633
- //r
634
- let r
635
- if (v[opt.pk]) {
636
- //有id
942
+ //setting
943
+ let setting = {}
944
+ if (t !== null) {
945
+ setting.transaction = t
946
+ }
947
+ else if (transaction !== null) {
948
+ setting.transaction = transaction
949
+ }
637
950
 
638
- //setting
639
- let setting = {
640
- where: { [opt.pk]: v[opt.pk] },
641
- raw: true,
642
- }
643
- if (transaction !== null) {
644
- setting.transaction = transaction
645
- }
951
+ try {
646
952
 
647
- //findOne
648
- r = await md.findOne(setting)
649
- .catch((error) => {
650
- ee.emit('error', error)
651
- err = error
652
- })
953
+ //bulkCreate, 未加任何跳過選項, 故任一筆撞主鍵即整句失敗, 同批重複主鍵亦於此被偵測為衝突
954
+ await md.bulkCreate(data, setting)
653
955
 
956
+ if (t !== null) {
957
+ await t.commit()
654
958
  }
655
- else {
656
- //沒有id
657
- err = `${opt.pk} is invalid`
959
+
960
+ }
961
+ catch (err) {
962
+ if (t !== null) {
963
+ await t.rollback().catch(() => {})
658
964
  }
965
+ throw err
966
+ }
659
967
 
660
- if (r) {
661
- //有找到資料
968
+ //res, 全有全無故成功時nInserted恆等於n
969
+ res = {
970
+ n: nAll,
971
+ nInserted: nAll,
972
+ ok: 1,
973
+ }
662
974
 
663
- //setting
664
- let setting = {
665
- where: { [opt.pk]: v[opt.pk] },
666
- }
667
- if (transaction !== null) {
668
- setting.transaction = transaction
669
- }
975
+ }
976
+ catch (err) {
977
+ isErr = true
978
+ res = err
979
+ }
980
+ finally {
670
981
 
671
- let rr = await md.update(v, setting)
672
- .catch((error) => {
673
- ee.emit('error', error)
674
- err = error
675
- })
982
+ //closeSequelize
983
+ if (instance === null) { //內部自動初始化得close
984
+ await closeSequelize('insertBulk')
985
+ }
676
986
 
677
- if (rr) {
678
- //console.log('update 有更新資料', rr)
679
- pmm.resolve({ n: 1, nModified: 1, ok: 1 })
680
- }
681
- else {
682
- //console.log('update 沒有更新資料', err)
683
- pmm.resolve({ n: 1, nModified: 0, ok: 0 })
684
- }
987
+ }
685
988
 
686
- }
687
- else {
688
- //沒有找到資料
989
+ //check
990
+ if (isErr) {
991
+ emitError('insertBulk', data, res)
992
+ return Promise.reject(res)
993
+ }
689
994
 
690
- //autoInsert
691
- if (autoInsert) {
995
+ //emit, 須於結果定案後發出
996
+ emitChange('insertBulk', data, res)
692
997
 
693
- //setting
694
- let setting = { }
695
- if (transaction !== null) {
696
- setting.transaction = transaction
697
- }
998
+ return res
999
+ }
1000
+
1001
+
1002
+ /**
1003
+ * 儲存單筆數據
1004
+ *
1005
+ * 採條件寫入配合衝突偵測與重試: 每次寫入本身為單一條件式原子語句——插入由主鍵之唯一約束判定、
1006
+ * 更新由WHERE主鍵之比對判定,皆非由預讀值決定其成敗;預讀僅用於選擇發出哪一條語句,
1007
+ * 選錯不會產生錯誤結果,只會被偵測並重試(插入撞既有主鍵、更新未命中任何列),故縱使預讀值已過期,
1008
+ * 最終結果仍與該次操作單獨執行時相同。
1009
+ * 註: 不採sequelize之upsert,因sqlite無從於單一語句內回報本次係插入或更新(其created恆為null),
1010
+ * 而insert與save之規格要求nInserted精確,兩者於sqlite互斥,故改採本形式
1011
+ * 註: [內容相同]之判定基準為將待寫入物件合併進現值後與現值相同,相同則不寫入而nModified為0,
1012
+ * 合併取淺層以與後端整欄取代之寫入行為一致,令nModified忠實反映資料庫端是否真的寫入
1013
+ *
1014
+ * @ignore
1015
+ * @param {Object} md 輸入資料表model物件
1016
+ * @param {Object} v 輸入數據物件
1017
+ * @param {Boolean} autoInsert 輸入是否於查無數據時自動改以插入處理布林值
1018
+ * @param {Object|null} transaction 輸入交易(transaction)物件
1019
+ * @param {Array} ks 輸入model之欄位名陣列
1020
+ * @returns {Promise} 回傳Promise,resolve回傳本筆儲存結果,本筆失敗時亦resolve並以ok為0回報
1021
+ */
1022
+ async function saveOne(md, v, autoInsert, transaction, ks) {
1023
+
1024
+ //nTry, 預測錯誤時重試, 給予有限次數令其收斂
1025
+ let nTry = 3
1026
+
1027
+ //rest
1028
+ let rest = null
1029
+
1030
+ //inserted, 供結果定案後發出insert事件用
1031
+ let inserted = false
698
1032
 
699
- //create
700
- let rr = await md.create(v, setting)
701
- .catch((error) => {
702
- ee.emit('error', error)
703
- err = error
704
- })
1033
+ //pkv
1034
+ let pkv = get(v, opt.pk)
705
1035
 
706
- if (rr) {
707
- //console.log('create 有插入資料', rr)
708
- pmm.resolve({ n: 1, nInserted: 1, ok: 1 })
1036
+ //setting
1037
+ let settingRead = {
1038
+ where: { [opt.pk]: pkv },
1039
+ raw: true,
1040
+ }
1041
+ let settingUpd = {
1042
+ where: { [opt.pk]: pkv },
1043
+ }
1044
+ let settingIns = {}
1045
+ if (transaction !== null) {
1046
+ settingRead.transaction = transaction
1047
+ settingUpd.transaction = transaction
1048
+ settingIns.transaction = transaction
1049
+ }
1050
+
1051
+ for (let i = 0; i &lt; nTry; i++) {
1052
+
1053
+ try {
1054
+
1055
+ //預讀, 供判斷內容是否相同之快速路徑, 其結果不用於決定寫入內容
1056
+ let _v = await md.findOne(settingRead)
1057
+
1058
+ if (iseobj(_v)) {
1059
+ //主鍵存在
1060
+
1061
+ //_vt, vt, 以待寫入物件之非主鍵欄位淺層覆蓋現值後與現值比對
1062
+ let _vt = omit(_v, opt.pk)
1063
+ let vt = {
1064
+ ..._vt,
1065
+ ...pick(omit(v, opt.pk), ks),
1066
+ }
1067
+
1068
+ //check, 內容相同者不寫入
1069
+ if (isEqual(vt, _vt)) {
1070
+ rest = {
1071
+ n: 1,
1072
+ nInserted: 0,
1073
+ nModified: 0,
1074
+ ok: 1,
709
1075
  }
710
- else {
711
- //console.log('create 沒有插入資料', err)
712
- pmm.resolve({ n: 1, nInserted: 0, ok: 0 })
1076
+ break
1077
+ }
1078
+
1079
+ //update, 單一UPDATE語句由WHERE主鍵原子完成查找與更新
1080
+ let rr = await md.update(v, settingUpd)
1081
+ let nAff = get(rr, 0, 0)
1082
+
1083
+ if (nAff > 0) {
1084
+ rest = {
1085
+ n: 1,
1086
+ nInserted: 0,
1087
+ nModified: 1,
1088
+ ok: 1,
713
1089
  }
1090
+ break
1091
+ }
1092
+
1093
+ //該列已被他方刪除, 重試改走插入
1094
+ continue
1095
+ }
714
1096
 
1097
+ //主鍵不存在且未開啟autoInsert則不寫入以免無中生有
1098
+ if (!autoInsert) {
1099
+ rest = {
1100
+ n: 0,
1101
+ nInserted: 0,
1102
+ nModified: 0,
1103
+ ok: 1,
715
1104
  }
716
- else {
717
- //console.log('findOne 沒有找到資料也不自動插入', err)
718
- pmm.resolve({ n: 0, nModified: 0, ok: 1 })
1105
+ break
1106
+ }
1107
+
1108
+ //create, 單一INSERT語句由主鍵之唯一約束原子完成檢查與寫入
1109
+ let bIns = await md.create(v, settingIns)
1110
+ .then(() => {
1111
+ return true
1112
+ })
1113
+ .catch((err) => {
1114
+ if (isDupPkError(err)) {
1115
+ return false
1116
+ }
1117
+ throw err
1118
+ })
1119
+
1120
+ if (bIns) {
1121
+ rest = {
1122
+ n: 1,
1123
+ nInserted: 1,
1124
+ nModified: 0,
1125
+ ok: 1,
719
1126
  }
1127
+ inserted = true
1128
+ break
1129
+ }
720
1130
 
1131
+ //他方已插入同一主鍵, 重試改走更新
1132
+ continue
1133
+ }
1134
+ catch (err) {
1135
+
1136
+ //其餘錯誤視為本筆失敗, 不中斷整批, 由呼叫端以ok與err判讀
1137
+ rest = {
1138
+ n: 1,
1139
+ nInserted: 0,
1140
+ nModified: 0,
1141
+ ok: 0,
1142
+ err: getErrMsg(err),
721
1143
  }
1144
+ break
1145
+ }
1146
+
1147
+ }
1148
+
1149
+ //check, 重試耗盡仍未定案
1150
+ if (rest === null) {
1151
+ rest = {
1152
+ n: 1,
1153
+ nInserted: 0,
1154
+ nModified: 0,
1155
+ ok: 0,
1156
+ err: `can not save data by ${opt.pk}[${pkv}] in ${nTry} tries`,
1157
+ }
1158
+ }
1159
+
1160
+ //emit, 須於本筆結果定案後發出, 避免訂閱函數拋錯影響本筆結果
1161
+ if (inserted) {
1162
+ emitChange('insert', [v], rest)
1163
+ }
1164
+ if (rest.ok === 0) {
1165
+ emitError('save', [v], rest.err)
1166
+ }
1167
+
1168
+ return rest
1169
+ }
1170
+
1171
+
1172
+ /**
1173
+ * 儲存數據,以主鍵為準更新既有數據,未給之欄位保留;主鍵不存在且option.autoInsert為true(預設)時改為插入
1174
+ *
1175
+ * 註: 回傳陣列恆與輸入等長,輸入單一物件亦回傳長度1之陣列
1176
+ * 註: n為主鍵命中筆數,值為0或1,命中(不論內容有無變更)或經插入而產生皆為1;nInserted與nModified恆同時出現
1177
+ * 註: [內容相同]之判定基準為將待寫入物件合併進現值後與現值相同,非待寫入物件與現值全等,
1178
+ * 故只給部份欄位且該些欄位值皆與現值相同時,合併結果等於現值,nModified為0
1179
+ * 註: 本筆失敗不中斷整批,該筆以ok為0並附err回報
1180
+ * 註: opt.autoGenPk為true(預設)時未帶有效主鍵者自動產生,為false時未帶有效主鍵即reject且同批皆不寫入
1181
+ *
1182
+ * @memberOf WOrmReladb
1183
+ * @param {Object|Array} data 輸入數據物件或陣列
1184
+ * @param {Object} [option={}] 輸入設定物件,預設為{}
1185
+ * @param {Object} [option.instance=null] 輸入實例instance物件,預設為null
1186
+ * @param {Object} [option.transaction=null] 輸入交易(transaction)物件,預設為null
1187
+ * @param {boolean} [option.autoInsert=true] 輸入是否於儲存時發現原本無數據,則自動改以插入處理,預設為true
1188
+ * @returns {Promise} 回傳Promise,resolve回傳與輸入等長之儲存結果陣列[{n,nInserted,nModified,ok}],reject回傳錯誤訊息
1189
+ */
1190
+ async function save(data, option = {}) {
1191
+
1192
+ //check
1193
+ if (!iseobj(data) &amp;&amp; !isearr(data)) {
1194
+ return []
1195
+ }
1196
+
1197
+ //cloneDeep
1198
+ data = cloneDeep(data)
1199
+
1200
+ //instance
1201
+ let instance = get(option, 'instance', null)
1202
+
1203
+ //transaction
1204
+ let transaction = get(option, 'transaction', null)
1205
+
1206
+ //check
1207
+ if (opt.useEncryption &amp;&amp; dialect === 'sqlite') {
1208
+ if (transaction !== null) {
1209
+ console.log('@journeyapps/sqlcipher can not support transaction.')
1210
+ }
1211
+ }
1212
+
1213
+ //autoInsert
1214
+ let autoInsert = get(option, 'autoInsert', true)
1215
+
1216
+ //isErr, res
1217
+ let isErr = false
1218
+ let res = null
1219
+
1220
+ //si
1221
+ let si = instance
1222
+ if (instance === null) {
1223
+ si = await initSequelize()
1224
+ }
1225
+
1226
+ try {
1227
+
1228
+ //check
1229
+ if (si.err) {
1230
+ throw si.err
1231
+ }
1232
+
1233
+ //md
1234
+ let md = si.mds[opt.cl]
1235
+
1236
+ //check
1237
+ if (!isarr(data)) {
1238
+ data = [data]
1239
+ }
722
1240
 
723
- pmm._err = err //避免eslint錯誤訊息
724
- return pmm
1241
+ //procPk, 須於任何寫入之前一次完成, 令autoGenPk為false而拋錯時同批之有效筆數亦不會被寫入
1242
+ data = map(data, (v, k) => {
1243
+ return procPk(md, v, k)
1244
+ })
1245
+
1246
+ //ks, model之欄位名, 供合併後比對時濾除非欄位之鍵
1247
+ let ks = keys(get(md, 'rawAttributes', {}))
1248
+
1249
+ //pmSeries
1250
+ res = await pmSeries(data, async(v) => {
1251
+ return saveOne(md, v, autoInsert, transaction, ks)
725
1252
  })
726
- .then((res) => {
727
- pm.resolve(res)
728
- ee.emit('change', 'save', data, res)
729
- })
730
- .catch((error) => {
731
- pm.reject(error)
732
- })
733
1253
 
734
1254
  }
735
- else {
736
- pm.reject(si.err)
1255
+ catch (err) {
1256
+ isErr = true
1257
+ res = err
737
1258
  }
1259
+ finally {
1260
+
1261
+ //closeSequelize
1262
+ if (instance === null) { //內部自動初始化得close
1263
+ await closeSequelize('save')
1264
+ }
738
1265
 
739
- //closeSequelize
740
- if (instance === null) { //內部自動初始化得close
741
- await closeSequelize('save')
742
1266
  }
743
1267
 
744
- return pm
1268
+ //check
1269
+ if (isErr) {
1270
+ emitError('save', data, res)
1271
+ return Promise.reject(res)
1272
+ }
1273
+
1274
+ //emit, 須於結果定案後發出
1275
+ emitChange('save', data, res)
1276
+
1277
+ return res
745
1278
  }
746
1279
 
747
1280
 
748
1281
  /**
749
1282
  * 刪除數據
750
1283
  *
1284
+ * 註: 回傳陣列恆與輸入等長,各筆之n與nDeleted皆為主鍵命中筆數,值為0或1,未命中為0且屬正常結果
1285
+ * 註: 判斷某筆是否真的被刪除一律以nDeleted為準
1286
+ * 註: 未帶有效主鍵者為該筆之輸入問題,回ok為0與err且不送查詢條件(以免無效值被轉為null而誤中其他數據),
1287
+ * 不中斷其餘筆數,以此與[主鍵未命中]之ok為1區辨
1288
+ * 註: 本函數不受opt.autoGenPk影響,於任一設定下皆不補值
1289
+ *
751
1290
  * @memberOf WOrmReladb
752
1291
  * @param {Object|Array} data 輸入數據物件或陣列,會查找各數據的opt.pk值,有存在者就刪除
753
1292
  * @param {Object} [option={}] 輸入設定物件,預設為{}
754
1293
  * @param {Object} [option.instance=null] 輸入實例instance物件,預設為null
755
1294
  * @param {Object} [option.transaction=null] 輸入交易(transaction)物件,預設為null
756
- * @returns {Promise} 回傳Promise,resolve回傳刪除結果,reject回傳錯誤訊息
1295
+ * @returns {Promise} 回傳Promise,resolve回傳與輸入等長之刪除結果陣列[{n,nDeleted,ok}],reject回傳錯誤訊息
757
1296
  */
758
1297
  async function del(data, option = {}) {
759
1298
 
@@ -778,19 +1317,22 @@ function WOrmReladb(opt = {}) {
778
1317
  }
779
1318
  }
780
1319
 
781
- //pm
782
- let pm = genPm()
1320
+ //isErr, res
1321
+ let isErr = false
1322
+ let res = null
783
1323
 
784
1324
  //si
785
1325
  let si = instance
786
1326
  if (instance === null) {
787
1327
  si = await initSequelize()
788
1328
  }
789
- // else {
790
- // console.log('del use instance', instance)
791
- // }
792
1329
 
793
- if (!si.err) {
1330
+ try {
1331
+
1332
+ //check
1333
+ if (si.err) {
1334
+ throw si.err
1335
+ }
794
1336
 
795
1337
  //md
796
1338
  let md = si.mds[opt.cl]
@@ -801,106 +1343,111 @@ function WOrmReladb(opt = {}) {
801
1343
  }
802
1344
 
803
1345
  //pmSeries
804
- await pmSeries(data, async function(v) {
805
- let pmm = genPm()
1346
+ res = await pmSeries(data, async(v) => {
806
1347
 
807
- //err
808
- let err = null
1348
+ //rest
1349
+ let rest = null
809
1350
 
810
- //r
811
- let r
812
- if (v[opt.pk]) {
813
- //有id
1351
+ //pkv
1352
+ let pkv = get(v, opt.pk)
814
1353
 
815
- //setting
816
- let setting = {
817
- where: { [opt.pk]: v[opt.pk] },
818
- raw: true,
819
- }
820
- if (transaction !== null) {
821
- setting.transaction = transaction
1354
+ //check, 未帶有效主鍵者不補值亦不送查詢, 直接視為該筆無法處理
1355
+ if (!isEffPk(pkv)) {
1356
+
1357
+ //rest
1358
+ rest = {
1359
+ n: 0,
1360
+ nDeleted: 0,
1361
+ ok: 0,
1362
+ err: `invalid ${opt.pk}[${pkv}]`,
822
1363
  }
823
1364
 
824
- //findOne
825
- r = await md.findOne(setting)
826
- .catch((error) => {
827
- ee.emit('error', error)
828
- err = error
829
- })
1365
+ //emit
1366
+ emitError('del', [v], rest.err)
1367
+
1368
+ return rest
1369
+ }
830
1370
 
1371
+ //setting
1372
+ let setting = {
1373
+ where: { [opt.pk]: pkv },
831
1374
  }
832
- else {
833
- //沒有id
834
- err = `${opt.pk} is invalid`
1375
+ if (transaction !== null) {
1376
+ setting.transaction = transaction //destroy的說明寫不需transaction但實際需要 (2020/10/07)
835
1377
  }
836
1378
 
837
- if (r) {
838
- //有找到資料
1379
+ //destroy, 單一DELETE語句即可取得命中筆數, 不另行findOne以免多一次往返且與刪除間存在競態
1380
+ await md.destroy(setting)
1381
+ .then((r) => {
839
1382
 
840
- //setting
841
- let setting = {
842
- where: { [opt.pk]: v[opt.pk] },
843
- }
844
- if (transaction !== null) {
845
- setting.transaction = transaction //destroy的說明寫不需transaction但實際需要 (2020/10/07)
846
- }
1383
+ //nDel, n與nDeleted皆為主鍵命中筆數, 未命中為0且屬正常結果
1384
+ let nDel = isnum(r) ? r : 0
847
1385
 
848
- //destroy
849
- let rr = await md.destroy(setting)
850
- .catch((error) => {
851
- ee.emit('error', error)
852
- err = error
853
- })
1386
+ //rest
1387
+ rest = {
1388
+ n: nDel,
1389
+ nDeleted: nDel,
1390
+ ok: 1,
1391
+ }
854
1392
 
855
- if (rr) {
856
- //console.log('destroy 有刪除資料', rr)
857
- pmm.resolve({ n: 1, nDeleted: 1, ok: 1 })
858
- }
859
- else {
860
- //console.log('destroy 沒有刪除資料', err)
861
- pmm.resolve({ n: 1, nDeleted: 0, ok: 0 })
862
- }
1393
+ })
1394
+ .catch((err) => {
863
1395
 
864
- }
865
- else {
866
- //console.log('findOne 沒有找到資料', err)
867
- pmm.resolve({ n: 0, nDeleted: 0, ok: 1 })
868
- }
1396
+ //rest, 本筆失敗不中斷整批
1397
+ rest = {
1398
+ n: 1,
1399
+ nDeleted: 0,
1400
+ ok: 0,
1401
+ err: getErrMsg(err),
1402
+ }
869
1403
 
870
- pmm._err = err //避免eslint錯誤訊息
871
- return pmm
1404
+ //emit
1405
+ emitError('del', [v], rest.err)
1406
+
1407
+ })
1408
+
1409
+ return rest
872
1410
  })
873
- .then((res) => {
874
- pm.resolve(res)
875
- ee.emit('change', 'del', data, res)
876
- })
877
- .catch((res) => {
878
- pm.reject(res)
879
- })
880
1411
 
881
1412
  }
882
- else {
883
- pm.reject(si.err)
1413
+ catch (err) {
1414
+ isErr = true
1415
+ res = err
884
1416
  }
1417
+ finally {
1418
+
1419
+ //closeSequelize
1420
+ if (instance === null) { //內部自動初始化得close
1421
+ await closeSequelize('del')
1422
+ }
885
1423
 
886
- //closeSequelize
887
- if (instance === null) { //內部自動初始化得close
888
- await closeSequelize('del')
889
1424
  }
890
1425
 
891
- return pm
1426
+ //check
1427
+ if (isErr) {
1428
+ emitError('del', data, res)
1429
+ return Promise.reject(res)
1430
+ }
1431
+
1432
+ //emit, 須於結果定案後發出
1433
+ emitChange('del', data, res)
1434
+
1435
+ return res
892
1436
  }
893
1437
 
894
1438
 
895
1439
  /**
896
- * 刪除數據,需與del分開,避免未傳數據導致直接刪除全表
1440
+ * 依條件刪除多筆數據,需與del分開,避免未傳數據導致直接刪除全表
1441
+ *
1442
+ * 註: n與nDeleted皆為實際刪除筆數,兩者恆相等,n不得取全表筆數
1443
+ * 註: find未給或為空物件時刪除全部數據;條件無命中時回{n:0,nDeleted:0,ok:1},不視為錯誤
897
1444
  *
898
1445
  * @memberOf WOrmReladb
899
1446
  * @param {Object} [find={}] 輸入刪除條件物件,不給予find則代表刪除全部數據
900
1447
  * @param {Object} [option={}] 輸入設定物件,預設為{}
901
1448
  * @param {Object} [option.instance=null] 輸入實例instance物件,預設為null
902
1449
  * @param {Object} [option.transaction=null] 輸入交易(transaction)物件,預設為null
903
- * @returns {Promise} 回傳Promise,resolve回傳刪除結果,reject回傳錯誤訊息
1450
+ * @returns {Promise} 回傳Promise,resolve回傳刪除結果{n,nDeleted,ok},reject回傳錯誤訊息
904
1451
  */
905
1452
  async function delAll(find = {}, option = {}) {
906
1453
 
@@ -923,19 +1470,22 @@ function WOrmReladb(opt = {}) {
923
1470
  }
924
1471
  }
925
1472
 
926
- //pm
927
- let pm = genPm()
1473
+ //isErr, res
1474
+ let isErr = false
1475
+ let res = null
928
1476
 
929
1477
  //si
930
1478
  let si = instance
931
1479
  if (instance === null) {
932
1480
  si = await initSequelize()
933
1481
  }
934
- // else {
935
- // console.log('delAll use instance', instance)
936
- // }
937
1482
 
938
- if (!si.err) {
1483
+ try {
1484
+
1485
+ //check
1486
+ if (si.err) {
1487
+ throw si.err
1488
+ }
939
1489
 
940
1490
  //md
941
1491
  let md = si.mds[opt.cl]
@@ -948,29 +1498,41 @@ function WOrmReladb(opt = {}) {
948
1498
  setting.transaction = transaction //destroy的說明寫不需transaction但實際需要 (2020/10/07)
949
1499
  }
950
1500
 
951
- //destroy
952
- await md.destroy(setting)
953
- .then((res) => {
954
- res = { n: res, nDeleted: res, ok: 1 }
955
- pm.resolve(res)
956
- ee.emit('change', 'delAll', null, res)
957
- })
958
- .catch((error) => {
959
- ee.emit('error', error)
960
- pm.reject({ n: 0, ok: 0 })
961
- })
1501
+ //destroy, n與nDeleted皆為實際刪除筆數, 條件無命中時為0且屬正常結果
1502
+ let r = await md.destroy(setting)
1503
+ let nDel = isnum(r) ? r : 0
1504
+
1505
+ //res
1506
+ res = {
1507
+ n: nDel,
1508
+ nDeleted: nDel,
1509
+ ok: 1,
1510
+ }
962
1511
 
963
1512
  }
964
- else {
965
- pm.reject(si.err)
1513
+ catch (err) {
1514
+ isErr = true
1515
+ res = err
966
1516
  }
1517
+ finally {
1518
+
1519
+ //closeSequelize
1520
+ if (instance === null) { //內部自動初始化得close
1521
+ await closeSequelize('delAll')
1522
+ }
967
1523
 
968
- //closeSequelize
969
- if (instance === null) { //內部自動初始化得close
970
- await closeSequelize('delAll')
971
1524
  }
972
1525
 
973
- return pm
1526
+ //check
1527
+ if (isErr) {
1528
+ emitError('delAll', null, res)
1529
+ return Promise.reject(res)
1530
+ }
1531
+
1532
+ //emit, 須於結果定案後發出
1533
+ emitChange('delAll', null, res)
1534
+
1535
+ return res
974
1536
  }
975
1537
 
976
1538
 
@@ -1092,9 +1654,15 @@ function WOrmReladb(opt = {}) {
1092
1654
  ee.select = function() {
1093
1655
  return pmq(select, ...arguments)
1094
1656
  }
1657
+ ee.selectByPk = function() {
1658
+ return pmq(selectByPk, ...arguments)
1659
+ }
1095
1660
  ee.insert = function() {
1096
1661
  return pmq(insert, ...arguments)
1097
1662
  }
1663
+ ee.insertBulk = function() {
1664
+ return pmq(insertBulk, ...arguments)
1665
+ }
1098
1666
  ee.save = function() {
1099
1667
  return pmq(save, ...arguments)
1100
1668
  }
@@ -1107,7 +1675,9 @@ function WOrmReladb(opt = {}) {
1107
1675
  }
1108
1676
  else {
1109
1677
  ee.select = select
1678
+ ee.selectByPk = selectByPk
1110
1679
  ee.insert = insert
1680
+ ee.insertBulk = insertBulk
1111
1681
  ee.save = save
1112
1682
  ee.del = del
1113
1683
  ee.delAll = delAll
@@ -1132,7 +1702,7 @@ export default WOrmReladb
1132
1702
  <br class="clear">
1133
1703
 
1134
1704
  <footer>
1135
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Aug 08 2026 15:03:07 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
1705
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Mon Aug 17 2026 12:04:05 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
1136
1706
  </footer>
1137
1707
 
1138
1708
  <script>prettyPrint();</script>