w-orm-reladb 1.1.12 → 1.1.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.
@@ -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#.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><h3>Global</h3><ul><li><a href="global.html#cvFind">cvFind</a></li><li><a href="global.html#genDateChecker">genDateChecker</a></li><li><a href="global.html#getColKind">getColKind</a></li><li><a href="global.html#getColScale">getColScale</a></li><li><a href="global.html#importModels">importModels</a></li><li><a href="global.html#inDateRange">inDateRange</a></li><li><a href="global.html#isKindOk">isKindOk</a></li><li><a href="global.html#normPk">normPk</a></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><h3>Global</h3><ul><li><a href="global.html#chkPkWf">chkPkWf</a></li><li><a href="global.html#chkWf">chkWf</a></li><li><a href="global.html#cvFind">cvFind</a></li><li><a href="global.html#genDateChecker">genDateChecker</a></li><li><a href="global.html#getColKind">getColKind</a></li><li><a href="global.html#getColScale">getColScale</a></li><li><a href="global.html#importModels">importModels</a></li><li><a href="global.html#inDateRange">inDateRange</a></li><li><a href="global.html#isKindOk">isKindOk</a></li><li><a href="global.html#normPk">normPk</a></li><li><a href="global.html#toFloating">toFloating</a></li></ul>
33
33
 
34
34
  </nav>
35
35
 
@@ -69,10 +69,11 @@ import arrHas from 'wsemi/src/arrHas.mjs'
69
69
  import pmSeries from 'wsemi/src/pmSeries.mjs'
70
70
  import pm2resolve from 'wsemi/src/pm2resolve.mjs'
71
71
  import pmQueue from 'wsemi/src/pmQueue.mjs'
72
+ import chkOrmStr from 'wsemi/src/chkOrmStr.mjs'
72
73
  import WAutoSequelize from 'w-auto-sequelize/src/WAutoSequelize.mjs'
73
74
  import importModels from './importModels.mjs'
74
75
  import _genModelsByTabs from './genModelsByTabs.mjs'
75
- import cvFind, { getColKind, getColScale, normPk, chkFindShape, genDateChecker } from './cvFind.mjs'
76
+ import cvFind, { getColKind, getColScale, normPk, chkWf, chkFindShape, genDateChecker } from './cvFind.mjs'
76
77
 
77
78
 
78
79
  //放置於全域宣告, 若使用useStable=true才可於當創建多實例時仍能全域控管
@@ -84,6 +85,14 @@ let pmq = pmQueue(1)
84
85
  *
85
86
  * 注意: 各model內id欄位不是主鍵(primary key)時需要強制更改成為主鍵,否則sequelize無法匯入
86
87
  *
88
+ * 注意: opt.url、opt.db、opt.cl、opt.storage(儲存位址名稱與路徑)含孤立代理碼元(UTF-16代理對被拆開後只剩一半,String.prototype.isWellFormed()為false)者,
89
+ * 建構時即拋出invalid opt.&lt;名>: string contains lone surrogate(不回顯值):UTF-8轉換會把孤立代理碼元改寫為U+FFFD,不同名稱將指向同一儲存
90
+ *
91
+ * 注意: 寫入與主鍵查詢之字串須良構(第66項):主鍵不得含孤立代理碼元或NUL,insert、insertBulk、save之數據字串值與鍵名(含巢狀、陣列元素)不得含孤立代理碼元(值之NUL不拒);
92
+ * 不合格者insert與insertBulk整批reject、save與del該筆ok為0附err、selectByPk回傳null,詳見各函數說明與README。
93
+ * 註: mssql預設排序規則(如SQL_Latin1_General_CP1_CI_AS)下字串相等由排序規則決定,良構字串亦可能相等('a'、'a'+emoji、'a'+U+FFFD),屬資料庫語意不在檢核範圍,
94
+ * 需精確比對時請以二進位排序規則(如Latin1_General_100_BIN2)建表
95
+ *
87
96
  * @class
88
97
  * @param {Object} [opt={}] 輸入設定物件,預設{}
89
98
  * @param {String} [opt.url='mssql://username:password@localhost:1433'] 輸入連接資料庫字串,資料庫可選'mssql'、'sqlite'、'mysql'、'mariadb'、'postgres',預設'mssql://username:password@localhost:1433'
@@ -134,6 +143,18 @@ function WOrmReladb(opt = {}) {
134
143
  if (!isestr(opt.storage)) {
135
144
  opt.storage = './worm.db' //storage, only use for sqlite
136
145
  }
146
+
147
+
148
+ //儲存位址名稱(opt.db、opt.cl、opt.url、opt.storage)含孤立代理碼元者建構時即拋出(第66項, R13):
149
+ //路徑與名稱經UTF-8轉換後孤立代理碼元被改寫為U+FFFD, 不同名稱會指向同一儲存(如兩個不同之storage路徑共用同一個sqlite檔);
150
+ //須先於opt.url之解析(其錯誤訊息會回顯opt.url); 訊息為固定詞彙且不回顯值, 此處不檢NUL(不屬本規則)
151
+ for (let k of ['db', 'cl', 'url', 'storage']) {
152
+ let why = chkOrmStr(opt[k], { nul: false })
153
+ if (why !== null) {
154
+ throw new Error(`invalid opt.${k}: ${why}`)
155
+ }
156
+ }
157
+
137
158
  opt.storage = path.resolve(opt.storage)
138
159
  if (!isbol(opt.useEncryption)) {
139
160
  opt.useEncryption = false
@@ -269,6 +290,8 @@ function WOrmReladb(opt = {}) {
269
290
  * 判定主鍵值是否有效(與主鍵欄型別無關之判定)
270
291
  * 註: select以外之五函數對主鍵之認定一律以本函數與normPkOf為準,令selectByPk之[命中]與insert、save、del內對既有數據之認定一致
271
292
  * 註: 有效即非空字串或有限數值(含0), new String與new Number取原始值; NaN、±Infinity、布林、物件等為無效(見cvFind.mjs之normPk)
293
+ * 註: 含孤立代理碼元或NUL之字串亦為無效(第66項, R13),故selectByPk回null;寫入側(insert、insertBulk、save、del)於procPk之前即由chkWf逐筆檢出,
294
+ * 令autoGenPk為true時呼叫端所給之不合格字串不被當成[未給主鍵]而以自動產生之主鍵取代
272
295
  *
273
296
  * @ignore
274
297
  * @param {*} v 輸入主鍵值
@@ -374,8 +397,8 @@ function WOrmReladb(opt = {}) {
374
397
  * 兩者命中之數據不同而會刪錯列, 故delAll遇此類model即拒絕
375
398
  * 註: defaultScope帶group或having時, findAll套用而destroy不套用(having過濾後只回部分列, 卻刪除全部符合where者);
376
399
  * 帶非空之include時, findAll以INNER JOIN篩列(required)而destroy不JOIN; 兩者亦拒絕
377
- * 註: defaultScope.where含Op.and時, sequelize以Object.assign合併scope與cvFind輸出之where, 後者頂層恆為Op.and而覆寫scope之條件(select與delAll皆然),
378
- * delAll會刪到scope範圍外之列, 故拒絕; 其他鍵(含Op.or)與cvFind輸出之Op.and並存而以AND結合, 不受影響
400
+ * 註: defaultScope.where含Op.and者, delAll一律拒絕(維持既有處置): 原因為sequelize預設以Object.assign合併scope與cvFind輸出之where而被頂層Op.and覆寫,
401
+ * delAll會刪到scope範圍外之列; 自R16起建構時設定whereMergeStrategy:'and', 兩者一律以AND結合(select亦正確套用該scope), 此拒絕為保守維持, 不再是越界之防線
379
402
  *
380
403
  * @ignore
381
404
  * @param {Object} md 輸入資料表model物件
@@ -412,17 +435,22 @@ function WOrmReladb(opt = {}) {
412
435
  * 正規化各筆數據之主鍵值並就地寫回, 型別不符者整批拋出
413
436
  * 註: 須於procPk之後、任何寫入之前一次完成; 已給主鍵而型別不符屬呼叫端未履行契約, 不以自動產生之主鍵取代
414
437
  * 註: 正規化值寫回數據, 令寫入值與其後各函數之比對值一致(如字串類主鍵欄之123存為'123'), 事件之數據亦為實際寫入者
438
+ * 註: 訊息只含筆數索引與主鍵欄名, 不回顯主鍵值(第66項, R13)
415
439
  *
416
440
  * @ignore
417
441
  * @param {Object} md 輸入資料表model物件
418
442
  * @param {Array} data 輸入已補齊主鍵之數據陣列
443
+ * @param {Array} [bads=[]] 輸入chkWf之逐筆訊息陣列, 非null者(save之不合格筆, 已另行處置為逐筆失敗)略過
419
444
  * @returns {undefined} 無回傳值
420
445
  */
421
- function normPks(md, data) {
446
+ function normPks(md, data, bads = []) {
422
447
  data.forEach((v, k) => {
448
+ if (get(bads, k, null) !== null) {
449
+ return
450
+ }
423
451
  let r = normPkOf(md, get(v, opt.pk))
424
452
  if (r.kind !== 'ok') {
425
- throw new Error(`invalid data[${k}].${opt.pk}[${get(v, opt.pk)}], type does not match pk column [${opt.pk}]`)
453
+ throw new Error(`invalid data[${k}].${opt.pk}, type does not match pk column [${opt.pk}]`)
426
454
  }
427
455
  v[opt.pk] = r.v
428
456
  })
@@ -440,11 +468,15 @@ function WOrmReladb(opt = {}) {
440
468
  * @ignore
441
469
  * @param {Object} md 輸入資料表model物件
442
470
  * @param {Array} data 輸入已補齊並正規化主鍵之數據陣列
471
+ * @param {Array} [bads=[]] 輸入chkWf之逐筆訊息陣列, 非null者(save之不合格筆, 已另行處置為逐筆失敗)略過
443
472
  * @returns {undefined} 無回傳值
444
473
  */
445
- function chkDates(md, data) {
474
+ function chkDates(md, data, bads = []) {
446
475
  let chk = genDateChecker(get(md, 'rawAttributes', {}))
447
476
  data.forEach((v, k) => {
477
+ if (get(bads, k, null) !== null) {
478
+ return
479
+ }
448
480
  let col = chk(v)
449
481
  if (col !== null) {
450
482
  throw new Error(`invalid data: date is not a valid time in years 1-9999 at [data[${k}].${col}]`)
@@ -541,7 +573,10 @@ function WOrmReladb(opt = {}) {
541
573
  //dialectOptions: {
542
574
  // collate: 'utf8_general_ci'
543
575
  //},
544
- timestamps: false
576
+ timestamps: false,
577
+ //model之defaultScope.where與select、delAll等傳入之where一律以AND結合(R16): sequelize預設為overwrite, 同名頂層鍵後者覆寫,
578
+ //scope.where為Sequelize.literal或Sequelize.where(非一般物件)時scope直接被丟棄, 致select與delAll越出scope
579
+ whereMergeStrategy: 'and'
545
580
  },
546
581
  // pool: {
547
582
  // max: 20,
@@ -574,6 +609,13 @@ function WOrmReladb(opt = {}) {
574
609
  //mds
575
610
  let mds = r.msg
576
611
 
612
+ //model自身指定whereMergeStrategy時以model選項為準而蓋過上列define預設(scope又被覆寫或丟棄), 故匯入後一律設為'and'(R16)
613
+ if (err === null) {
614
+ for (let md of Object.values(mds)) {
615
+ md.options.whereMergeStrategy = 'and'
616
+ }
617
+ }
618
+
577
619
  return {
578
620
  mds,
579
621
  err,
@@ -742,6 +784,7 @@ function WOrmReladb(opt = {}) {
742
784
  *
743
785
  * 註: 主鍵欄位取自opt.pk,預設為id,支援由呼叫端指定
744
786
  * 註: 主鍵未命中或主鍵值無效皆回傳null而不reject,[命中]之判定基準與insert、save、del內對既有數據之認定一致
787
+ * 註: 含孤立代理碼元或NUL之字串主鍵(含new String包裝者)為無效而回傳null,不連線、不送查詢(第66項, R13): UTF-8存放之資料庫會把孤立代理碼元改寫為U+FFFD,若照送會命中他筆
745
788
  * 註: 主鍵值依主鍵欄型別正規化後查詢: 字串類主鍵欄之有限數字以String(v)處置(selectByPk(123)即查'123'), 整數類主鍵欄之標準整數寫法字串轉為數值;
746
789
  * 型別不符(整數類主鍵欄給'012'、12.4、'abc'等)亦屬主鍵值無效而回傳null
747
790
  * 註: 本函數不得有副作用,故不創建資料表
@@ -770,10 +813,13 @@ function WOrmReladb(opt = {}) {
770
813
  return Promise.reject(err)
771
814
  }
772
815
 
773
- //check, 未給有效主鍵值視為查無數據, 不送查詢以免無效值被轉為null而誤中其他數據
774
- if (!isEffPk(pk)) {
816
+ //check, 未給有效主鍵值視為查無數據, 不送查詢以免無效值被轉為null而誤中其他數據;
817
+ //含孤立代理碼元或NUL之字串亦為無效(R13); 包裝物件(new String、new Number)只求值一次, 其後以原始值查詢(檢查之值即使用之值)
818
+ let r0 = normPk(pk, 'any')
819
+ if (r0.kind !== 'ok') {
775
820
  return null
776
821
  }
822
+ pk = r0.v
777
823
 
778
824
  //check
779
825
  if (opt.useEncryption &amp;&amp; dialect === 'sqlite') {
@@ -874,6 +920,10 @@ function WOrmReladb(opt = {}) {
874
920
  * 錯誤訊息為invalid data: date is not a valid time in years 1-9999 at [data[索引].欄名],不回顯值;sqlite以文字存日期,範圍外年份之文字與時間序不一致,會使以日期為條件之delAll刪錯列,
875
921
  * 故兩方言一律於送出前拒收;無效時間(Invalid Date、NaN、無法解析之字串、空字串等,sqlite會存為字典序大於任何年份之文字Invalid date,mssql則由資料庫報錯)亦一併reject;
876
922
  * 庫內既有之範圍外日期不遷移,須自行清理或改寫(見README)
923
+ * 註: 主鍵與數據之字串須良構(第66項, R13): 主鍵不得含孤立代理碼元(UTF-16代理對被拆開後只剩一半, UTF-8存放時被改寫為U+FFFD而使不同主鍵寫成同一鍵)或NUL,
924
+ * 數據之字串值與鍵名(含巢狀物件、陣列元素、Map/Set等,不論欄位是否在model內)不得含孤立代理碼元(值之NUL不拒);U+FFFD本身與成對代理(emoji等)為良構,照常接受。
925
+ * 任一筆不合格即reject且同批皆不寫入,訊息為invalid id: 原因 at [data[索引].主鍵欄]或invalid data: 原因 at [data[索引].路徑],不回顯值;
926
+ * 不隨opt.autoGenPk改變(autoGenPk為true時亦不以自動產生之主鍵取代呼叫端所給之不合格主鍵),檢核先於連線與model載入,作用於複本
877
927
  * 註: option.returnList開啟時改回與輸入等長且保序之逐筆結果陣列[{n,nInserted,ok}],
878
928
  * 聚合計數只答得出[有幾筆是新的]而答不出[是哪幾筆],而後者正是去重之產出物;
879
929
  * 逐筆元素之n與ok恆為1,資訊由nInserted承載,不變式為
@@ -917,9 +967,6 @@ function WOrmReladb(opt = {}) {
917
967
  }
918
968
  }
919
969
 
920
- //cloneDeep
921
- data = cloneDeep(data)
922
-
923
970
  //isErr, res
924
971
  let isErr = false
925
972
  let res = null
@@ -927,13 +974,25 @@ function WOrmReladb(opt = {}) {
927
974
  //si
928
975
  let si = instance
929
976
 
977
+ //inited, 本次呼叫是否已進入initSequelize(僅此時finally才關閉sequelize; 預檢失敗於其前者不得關閉呼叫端先前持有之連線)
978
+ let inited = false
979
+
930
980
  try {
931
981
 
932
982
  //check, 實例已關閉屬整批性錯誤, 於入口快速失敗
933
983
  procClosed(instance)
934
984
 
985
+ //chkWf, 主鍵與資料之字串良構(孤立代理碼元、主鍵之NUL)逐筆檢核(R13): 先於連線、model載入與任何寫入, 檢查之物件即cloneDeep後之複本且其後只使用該複本;
986
+ //任一筆不合格即整批拋出(以索引最小者為準)而不寫入任何一筆, 不隨autoGenPk改變, 不以自動產生之主鍵取代不合格之主鍵
987
+ let bads = chkWf(isarr(data) ? data : [data], opt.pk, true)
988
+ let iBad = bads.findIndex((v) => v !== null)
989
+ if (iBad >= 0) {
990
+ throw new Error(bads[iBad])
991
+ }
992
+
935
993
  //initSequelize, 內部自動初始化須在try內: 其例外(如驅動缺失)與si.err同屬整批性錯誤, 方能於reject前發出error事件, 已建構之sequelize亦由finally關閉
936
994
  if (instance === null) {
995
+ inited = true
937
996
  si = await initSequelize()
938
997
  }
939
998
 
@@ -1023,7 +1082,7 @@ function WOrmReladb(opt = {}) {
1023
1082
  finally {
1024
1083
 
1025
1084
  //closeSequelize
1026
- if (instance === null) { //內部自動初始化得close
1085
+ if (instance === null &amp;&amp; inited) { //內部自動初始化得close
1027
1086
  await closeSequelize('insert')
1028
1087
  }
1029
1088
 
@@ -1056,6 +1115,7 @@ function WOrmReladb(opt = {}) {
1056
1115
  * 註: 主鍵值依主鍵欄型別正規化後寫入(字串類主鍵欄之123存為'123');型別不符(整數類主鍵欄給'012'等)即reject且同批皆不寫入
1057
1116
  * 註: DATE、DATEONLY欄之值換算後年份須在1–9999,範圍外或無效者(如new Date(8.64e15)、new Date(NaN))即reject且同批皆不寫入,訊息為invalid data: date is not a valid time in years 1-9999 at [data[索引].欄名],
1058
1117
  * 不回顯值,兩方言一致於送出前拒收,規則同insert
1118
+ * 註: 主鍵與數據之字串須良構(孤立代理碼元、主鍵之NUL),不合格者整批reject且同批皆不寫入,規則同insert(第66項, R13)
1059
1119
  * 註: [全有全無]為獨立於T7之額外要求——T7僅保證每筆之[檢查主鍵不存在]與[寫入]為原子,不保證整批。
1060
1120
  * mssql因綁定參數數量上限會由驅動層自動拆為多語句送出,實測1000筆之批次於末筆撞主鍵時已有946筆落盤,
1061
1121
  * 故一律以交易包覆並於失敗時回滾;呼叫端已給transaction時改以巢狀交易(SAVEPOINT)包覆,
@@ -1092,9 +1152,6 @@ function WOrmReladb(opt = {}) {
1092
1152
  }
1093
1153
  }
1094
1154
 
1095
- //cloneDeep
1096
- data = cloneDeep(data)
1097
-
1098
1155
  //isErr, res
1099
1156
  let isErr = false
1100
1157
  let res = null
@@ -1102,13 +1159,25 @@ function WOrmReladb(opt = {}) {
1102
1159
  //si
1103
1160
  let si = instance
1104
1161
 
1162
+ //inited, 本次呼叫是否已進入initSequelize(僅此時finally才關閉sequelize; 預檢失敗於其前者不得關閉呼叫端先前持有之連線)
1163
+ let inited = false
1164
+
1105
1165
  try {
1106
1166
 
1107
1167
  //check, 實例已關閉屬整批性錯誤, 於入口快速失敗
1108
1168
  procClosed(instance)
1109
1169
 
1170
+ //chkWf, 主鍵與資料之字串良構(孤立代理碼元、主鍵之NUL)逐筆檢核(R13): 先於連線、model載入與任何寫入, 檢查之物件即cloneDeep後之複本且其後只使用該複本;
1171
+ //任一筆不合格即整批拋出(以索引最小者為準)而不寫入任何一筆, 不隨autoGenPk改變, 不以自動產生之主鍵取代不合格之主鍵
1172
+ let bads = chkWf(isarr(data) ? data : [data], opt.pk, true)
1173
+ let iBad = bads.findIndex((v) => v !== null)
1174
+ if (iBad >= 0) {
1175
+ throw new Error(bads[iBad])
1176
+ }
1177
+
1110
1178
  //initSequelize, 內部自動初始化須在try內: 其例外(如驅動缺失)與si.err同屬整批性錯誤, 方能於reject前發出error事件, 已建構之sequelize亦由finally關閉
1111
1179
  if (instance === null) {
1180
+ inited = true
1112
1181
  si = await initSequelize()
1113
1182
  }
1114
1183
 
@@ -1191,7 +1260,7 @@ function WOrmReladb(opt = {}) {
1191
1260
  finally {
1192
1261
 
1193
1262
  //closeSequelize
1194
- if (instance === null) { //內部自動初始化得close
1263
+ if (instance === null &amp;&amp; inited) { //內部自動初始化得close
1195
1264
  await closeSequelize('insertBulk')
1196
1265
  }
1197
1266
 
@@ -1228,9 +1297,10 @@ function WOrmReladb(opt = {}) {
1228
1297
  * @param {Boolean} autoInsert 輸入是否於查無數據時自動改以插入處理布林值
1229
1298
  * @param {Object|null} transaction 輸入交易(transaction)物件
1230
1299
  * @param {Array} ks 輸入model之欄位名陣列
1300
+ * @param {Number} k 輸入數據於陣列內之索引,供訊息使用(訊息不回顯主鍵值)
1231
1301
  * @returns {Promise} 回傳Promise,resolve回傳本筆儲存結果,本筆失敗時亦resolve並以ok為0回報
1232
1302
  */
1233
- async function saveOne(md, v, autoInsert, transaction, ks) {
1303
+ async function saveOne(md, v, autoInsert, transaction, ks, k) {
1234
1304
 
1235
1305
  //nTry, 預測錯誤時重試, 給予有限次數令其收斂
1236
1306
  let nTry = 3
@@ -1369,7 +1439,7 @@ function WOrmReladb(opt = {}) {
1369
1439
  nInserted: 0,
1370
1440
  nModified: 0,
1371
1441
  ok: 0,
1372
- err: `can not save data by ${opt.pk}[${pkv}] in ${nTry} tries`,
1442
+ err: `can not save data[${k}].${opt.pk} in ${nTry} tries`,
1373
1443
  }
1374
1444
  }
1375
1445
 
@@ -1397,6 +1467,8 @@ function WOrmReladb(opt = {}) {
1397
1467
  * 註: 主鍵值依主鍵欄型別正規化(字串類主鍵欄之123即'123');型別不符(整數類主鍵欄給'012'等)屬呼叫端未履行契約,整批reject且同批皆不寫入
1398
1468
  * 註: DATE、DATEONLY欄之值換算後年份須在1–9999,範圍外或無效者(如new Date(8.64e15)、new Date(NaN))屬呼叫端未履行契約,整批reject且同批皆不寫入(含autoInsert為false而不會寫入者;不降為該筆ok為0),
1399
1469
  * 訊息為invalid data: date is not a valid time in years 1-9999 at [data[索引].欄名],不回顯值,兩方言一致於送出前拒收,規則同insert
1470
+ * 註: 主鍵與數據之字串須良構(孤立代理碼元、主鍵之NUL;規則同insert, 第66項, R13),但不合格者為該筆失敗而非整批reject:該筆ok為0並附err(invalid id: 原因 at [data[索引].主鍵欄]或invalid data: 原因 at [data[索引].路徑],不回顯值),
1471
+ * 不讀取資料庫、不寫入(含autoInsert為false),同批其他筆照常處理;不隨opt.autoGenPk改變。全部筆數皆不合格時不連線
1400
1472
  *
1401
1473
  * @memberOf WOrmReladb
1402
1474
  * @param {Object|Array} data 輸入數據物件或陣列
@@ -1413,9 +1485,6 @@ function WOrmReladb(opt = {}) {
1413
1485
  return []
1414
1486
  }
1415
1487
 
1416
- //cloneDeep
1417
- data = cloneDeep(data)
1418
-
1419
1488
  //instance
1420
1489
  let instance = get(option, 'instance', null)
1421
1490
 
@@ -1439,23 +1508,32 @@ function WOrmReladb(opt = {}) {
1439
1508
  //si
1440
1509
  let si = instance
1441
1510
 
1511
+ //inited, 本次呼叫是否已進入initSequelize(僅此時finally才關閉sequelize; 預檢失敗於其前者不得關閉呼叫端先前持有之連線)
1512
+ let inited = false
1513
+
1442
1514
  try {
1443
1515
 
1444
1516
  //check, 實例已關閉屬整批性錯誤, 於入口快速失敗
1445
1517
  procClosed(instance)
1446
1518
 
1447
- //initSequelize, 內部自動初始化須在try內: 其例外(如驅動缺失)與si.err同屬整批性錯誤, 方能於reject前發出error事件, 已建構之sequelize亦由finally關閉
1448
- if (instance === null) {
1519
+ //chkWf, 主鍵與資料之字串良構(孤立代理碼元、主鍵之NUL)逐筆檢核(R13): 先於連線、model載入與任何讀取、寫入、刪除, 檢查之物件即cloneDeep後之複本且其後只使用該複本;
1520
+ //不合格者為該筆失敗(ok為0附err, 同批其他筆照常), bads為逐筆訊息(合格者為null); 不隨autoGenPk改變
1521
+ let bads = chkWf(isarr(data) ? data : [data], opt.pk, true)
1522
+ let nOk = bads.filter((v) => v === null).length
1523
+
1524
+ //initSequelize, 內部自動初始化須在try內: 其例外(如驅動缺失)與si.err同屬整批性錯誤, 方能於reject前發出error事件, 已建構之sequelize亦由finally關閉; 全部不合格者無須連線
1525
+ if (instance === null &amp;&amp; nOk > 0) {
1526
+ inited = true
1449
1527
  si = await initSequelize()
1450
1528
  }
1451
1529
 
1452
1530
  //check
1453
- if (si.err) {
1531
+ if (si !== null &amp;&amp; si.err) {
1454
1532
  throw si.err
1455
1533
  }
1456
1534
 
1457
1535
  //md
1458
- let md = si.mds[opt.cl]
1536
+ let md = si === null ? null : si.mds[opt.cl]
1459
1537
 
1460
1538
  //check
1461
1539
  if (!isarr(data)) {
@@ -1464,21 +1542,34 @@ function WOrmReladb(opt = {}) {
1464
1542
 
1465
1543
  //procPk, 須於任何寫入之前一次完成, 令autoGenPk為false而拋錯時同批之有效筆數亦不會被寫入
1466
1544
  data = map(data, (v, k) => {
1467
- return procPk(md, v, k)
1545
+ return bads[k] !== null ? v : procPk(md, v, k)
1468
1546
  })
1469
1547
 
1470
1548
  //normPks, 主鍵值依主鍵欄型別正規化後寫回, 型別不符者整批reject且不寫入任何一筆
1471
- normPks(md, data)
1549
+ normPks(md, data, bads)
1472
1550
 
1473
1551
  //chkDates, DATE、DATEONLY欄之值須為年份在1–9999之有效時間, 範圍外或無效者整批reject且不寫入任何一筆(含autoInsert為false而不會寫入者; 與條件側共用範圍判定, 見R12)
1474
- chkDates(md, data)
1552
+ chkDates(md, data, bads)
1475
1553
 
1476
1554
  //ks, model之欄位名, 供合併後比對時濾除非欄位之鍵
1477
1555
  let ks = keys(get(md, 'rawAttributes', {}))
1478
1556
 
1479
1557
  //pmSeries
1480
- res = await pmSeries(data, async(v) => {
1481
- return saveOne(md, v, autoInsert, transaction, ks)
1558
+ res = await pmSeries(data, async(v, k) => {
1559
+
1560
+ //check, 字串不合格之筆為該筆失敗: 不讀取資料庫、不寫入, 同批其他筆照常
1561
+ if (bads[k] !== null) {
1562
+ emitError('save', [v], bads[k])
1563
+ return {
1564
+ n: 1,
1565
+ nInserted: 0,
1566
+ nModified: 0,
1567
+ ok: 0,
1568
+ err: bads[k],
1569
+ }
1570
+ }
1571
+
1572
+ return saveOne(md, v, autoInsert, transaction, ks, k)
1482
1573
  })
1483
1574
 
1484
1575
  }
@@ -1489,7 +1580,7 @@ function WOrmReladb(opt = {}) {
1489
1580
  finally {
1490
1581
 
1491
1582
  //closeSequelize
1492
- if (instance === null) { //內部自動初始化得close
1583
+ if (instance === null &amp;&amp; inited) { //內部自動初始化得close
1493
1584
  await closeSequelize('save')
1494
1585
  }
1495
1586
 
@@ -1517,6 +1608,8 @@ function WOrmReladb(opt = {}) {
1517
1608
  * 不中斷其餘筆數,以此與[主鍵未命中]之ok為1區辨;主鍵值與主鍵欄型別不符者(整數類主鍵欄給'012'等)亦同
1518
1609
  * 註: 主鍵值依主鍵欄型別正規化後刪除(字串類主鍵欄之123即'123'),正規化值寫回該筆數據
1519
1610
  * 註: 本函數不受opt.autoGenPk影響,於任一設定下皆不補值
1611
+ * 註: 主鍵含孤立代理碼元或NUL者(含new String包裝者;UTF-8存放之資料庫會把孤立代理碼元改寫為U+FFFD而刪到他筆)為該筆失敗,ok為0並附err(invalid id: 原因 at [data[索引].主鍵欄],不回顯值),
1612
+ * 不送查詢,同批其他筆照常;只檢主鍵,其他欄位不檢(第66項, R13);全部筆數皆不合格時不連線。其他無效主鍵之err亦不回顯主鍵值(invalid data[索引].主鍵欄…)
1520
1613
  *
1521
1614
  * @memberOf WOrmReladb
1522
1615
  * @param {Object|Array} data 輸入數據物件或陣列,會查找各數據的opt.pk值,有存在者就刪除
@@ -1532,9 +1625,6 @@ function WOrmReladb(opt = {}) {
1532
1625
  return []
1533
1626
  }
1534
1627
 
1535
- //cloneDeep
1536
- data = cloneDeep(data)
1537
-
1538
1628
  //instance
1539
1629
  let instance = get(option, 'instance', null)
1540
1630
 
@@ -1555,23 +1645,32 @@ function WOrmReladb(opt = {}) {
1555
1645
  //si
1556
1646
  let si = instance
1557
1647
 
1648
+ //inited, 本次呼叫是否已進入initSequelize(僅此時finally才關閉sequelize; 預檢失敗於其前者不得關閉呼叫端先前持有之連線)
1649
+ let inited = false
1650
+
1558
1651
  try {
1559
1652
 
1560
1653
  //check, 實例已關閉屬整批性錯誤, 於入口快速失敗
1561
1654
  procClosed(instance)
1562
1655
 
1563
- //initSequelize, 內部自動初始化須在try內: 其例外(如驅動缺失)與si.err同屬整批性錯誤, 方能於reject前發出error事件, 已建構之sequelize亦由finally關閉
1564
- if (instance === null) {
1656
+ //chkWf, 主鍵之字串良構(孤立代理碼元、主鍵之NUL)逐筆檢核(R13): 先於連線、model載入與任何讀取、寫入、刪除, 檢查之物件即cloneDeep後之複本且其後只使用該複本;
1657
+ //不合格者為該筆失敗(ok為0附err, 同批其他筆照常), bads為逐筆訊息(合格者為null); 不隨autoGenPk改變
1658
+ let bads = chkWf(isarr(data) ? data : [data], opt.pk, false)
1659
+ let nOk = bads.filter((v) => v === null).length
1660
+
1661
+ //initSequelize, 內部自動初始化須在try內: 其例外(如驅動缺失)與si.err同屬整批性錯誤, 方能於reject前發出error事件, 已建構之sequelize亦由finally關閉; 全部不合格者無須連線
1662
+ if (instance === null &amp;&amp; nOk > 0) {
1663
+ inited = true
1565
1664
  si = await initSequelize()
1566
1665
  }
1567
1666
 
1568
1667
  //check
1569
- if (si.err) {
1668
+ if (si !== null &amp;&amp; si.err) {
1570
1669
  throw si.err
1571
1670
  }
1572
1671
 
1573
1672
  //md
1574
- let md = si.mds[opt.cl]
1673
+ let md = si === null ? null : si.mds[opt.cl]
1575
1674
 
1576
1675
  //check
1577
1676
  if (!isarr(data)) {
@@ -1579,11 +1678,23 @@ function WOrmReladb(opt = {}) {
1579
1678
  }
1580
1679
 
1581
1680
  //pmSeries
1582
- res = await pmSeries(data, async(v) => {
1681
+ res = await pmSeries(data, async(v, k) => {
1583
1682
 
1584
1683
  //rest
1585
1684
  let rest = null
1586
1685
 
1686
+ //check, 主鍵字串不合格之筆為該筆失敗: 不送查詢, 同批其他筆照常
1687
+ if (bads[k] !== null) {
1688
+ rest = {
1689
+ n: 0,
1690
+ nDeleted: 0,
1691
+ ok: 0,
1692
+ err: bads[k],
1693
+ }
1694
+ emitError('del', [v], rest.err)
1695
+ return rest
1696
+ }
1697
+
1587
1698
  //pkv
1588
1699
  let pkv = get(v, opt.pk)
1589
1700
 
@@ -1595,7 +1706,7 @@ function WOrmReladb(opt = {}) {
1595
1706
  n: 0,
1596
1707
  nDeleted: 0,
1597
1708
  ok: 0,
1598
- err: `invalid ${opt.pk}[${pkv}]`,
1709
+ err: `invalid data[${k}].${opt.pk}`,
1599
1710
  }
1600
1711
 
1601
1712
  //emit
@@ -1613,7 +1724,7 @@ function WOrmReladb(opt = {}) {
1613
1724
  n: 0,
1614
1725
  nDeleted: 0,
1615
1726
  ok: 0,
1616
- err: `invalid ${opt.pk}[${pkv}], type does not match pk column`,
1727
+ err: `invalid data[${k}].${opt.pk}, type does not match pk column`,
1617
1728
  }
1618
1729
 
1619
1730
  //emit
@@ -1679,7 +1790,7 @@ function WOrmReladb(opt = {}) {
1679
1790
  finally {
1680
1791
 
1681
1792
  //closeSequelize
1682
- if (instance === null) { //內部自動初始化得close
1793
+ if (instance === null &amp;&amp; inited) { //內部自動初始化得close
1683
1794
  await closeSequelize('del')
1684
1795
  }
1685
1796
 
@@ -1936,6 +2047,24 @@ function WOrmReladb(opt = {}) {
1936
2047
  // return closeSequelize('external')
1937
2048
  // }
1938
2049
  ee.genTransaction = genTransaction
2050
+
2051
+
2052
+ //snap, 數據於呼叫當下即同步複製(R13): useStable時各函數經佇列延後執行(pmQueue於執行前delay), 若於函數內才複製,
2053
+ //呼叫端於呼叫後立即改動原數據(如重用同一陣列)會影響實際檢核與寫入之值; 函數內只使用此複本, 檢核與寫入皆作用於同一份複本
2054
+ //複製拋錯(如getter拋錯)與函數內之其他錯誤同為rejected promise
2055
+ let snap = (fn) => {
2056
+ return function(data, ...rest) {
2057
+ let d
2058
+ try {
2059
+ d = cloneDeep(data)
2060
+ }
2061
+ catch (err) {
2062
+ return Promise.reject(err)
2063
+ }
2064
+ return fn(d, ...rest)
2065
+ }
2066
+ }
2067
+
1939
2068
  if (opt.useStable) {
1940
2069
  //用佇列(同時最大執行數1且先進先執行)處理高併發之情形
1941
2070
  //若沒管控:
@@ -1947,18 +2076,18 @@ function WOrmReladb(opt = {}) {
1947
2076
  ee.selectByPk = function() {
1948
2077
  return pmq(selectByPk, ...arguments)
1949
2078
  }
1950
- ee.insert = function() {
1951
- return pmq(insert, ...arguments)
1952
- }
1953
- ee.insertBulk = function() {
1954
- return pmq(insertBulk, ...arguments)
1955
- }
1956
- ee.save = function() {
1957
- return pmq(save, ...arguments)
1958
- }
1959
- ee.del = function() {
1960
- return pmq(del, ...arguments)
1961
- }
2079
+ ee.insert = snap((...args) => {
2080
+ return pmq(insert, ...args)
2081
+ })
2082
+ ee.insertBulk = snap((...args) => {
2083
+ return pmq(insertBulk, ...args)
2084
+ })
2085
+ ee.save = snap((...args) => {
2086
+ return pmq(save, ...args)
2087
+ })
2088
+ ee.del = snap((...args) => {
2089
+ return pmq(del, ...args)
2090
+ })
1962
2091
  ee.delAll = function() {
1963
2092
  return pmq(delAll, ...arguments)
1964
2093
  }
@@ -1966,10 +2095,10 @@ function WOrmReladb(opt = {}) {
1966
2095
  else {
1967
2096
  ee.select = select
1968
2097
  ee.selectByPk = selectByPk
1969
- ee.insert = insert
1970
- ee.insertBulk = insertBulk
1971
- ee.save = save
1972
- ee.del = del
2098
+ ee.insert = snap(insert)
2099
+ ee.insertBulk = snap(insertBulk)
2100
+ ee.save = snap(save)
2101
+ ee.del = snap(del)
1973
2102
  ee.delAll = delAll
1974
2103
  }
1975
2104
 
@@ -1992,7 +2121,7 @@ export default WOrmReladb
1992
2121
  <br class="clear">
1993
2122
 
1994
2123
  <footer>
1995
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Tue Oct 20 2026 04:13:54 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
2124
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Wed Oct 21 2026 11:57:34 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
1996
2125
  </footer>
1997
2126
 
1998
2127
  <script>prettyPrint();</script>