w-orm-lowdb 1.0.36 → 1.0.37

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
@@ -20,6 +20,8 @@ import iseobj from 'wsemi/src/iseobj.mjs'
20
20
  import isearr from 'wsemi/src/isearr.mjs'
21
21
  import haskey from 'wsemi/src/haskey.mjs'
22
22
  import cvOrmFind from 'wsemi/src/cvOrmFind.mjs'
23
+ import chkOrmStr from 'wsemi/src/chkOrmStr.mjs'
24
+ import chkOrmData from 'wsemi/src/chkOrmData.mjs'
23
25
 
24
26
 
25
27
  //kpGqueue, 以資料庫檔案路徑為鍵之全域序列化佇列, 詳見下方serialize說明
@@ -53,11 +55,21 @@ let kpGqueue = {}
53
55
  * 故僅單一行程內併發成立,跨行程併發不成立,詳見README之併發保證宣告。
54
56
  * 註: 數據以JSON存檔,Date存入後為ISO字串;insert、insertBulk與save寫入之Date年份須介於0000-9999,範圍外(如new Date(8.64e15))整批reject,詳見各函數與README。
55
57
  *
58
+ * 字串良構(w-orm系列規則,wsemi之chkOrmStr與chkOrmData): lowdb以JSON檔存放,孤立代理字元(String.prototype.isWellFormed()為false)可保真,本身不致相撞,
59
+ * 惟系列其他後端(UTF-8、BSON儲存)會改寫為U+FFFD,為使更換後端時資料行為一致,全系列一律於任何讀取與寫入之前拒收:
60
+ * 1. 主鍵(id)不得含孤立代理字元與NUL,不隨autoGenPk改變;
61
+ * 2. 寫入資料(insert、insertBulk、save)之字串值與鍵名(含巢狀物件、陣列元素、Map與Set之項目、Error與RegExp之文字)不得含孤立代理字元,值與鍵名之NUL不拒;
62
+ * 3. opt.url、opt.db、opt.cl含孤立代理字元者於建構時throw,訊息為invalid opt.<名>: string contains lone surrogate。
63
+ * 處置與無效主鍵同形:insert與insertBulk整批reject而不寫入任何一筆(error事件先於reject),save與del該筆ok為0並附err(同批其他筆照常),selectByPk回null;
64
+ * 訊息為固定詞彙而不回顯值:invalid id: 原因 at [data[k].id]、invalid data: 原因 at [data[k].路徑](k為批次索引,單一物件為0;不合格之鍵名以<invalid key>標示),
65
+ * 原因為string contains lone surrogate、string contains NUL(僅主鍵)、not a string或string is empty(del之id未給、非字串或空字串)。
66
+ * U+FFFD本身與成對代理(emoji等)為良構,照常接受。既有資料不遷移,檢出與清除法見README。
67
+ *
56
68
  * @class
57
69
  * @param {Object} [opt={}] 輸入設定物件,預設{}
58
- * @param {String} [opt.url='./db.json'] 輸入資料庫位置字串,預設'./db.json'
59
- * @param {String} [opt.db='worm'] 輸入使用資料庫名稱字串,預設'worm'
60
- * @param {String} [opt.cl='test'] 輸入使用資料表名稱字串,預設'test'
70
+ * @param {String} [opt.url='./db.json'] 輸入資料庫位置字串,預設'./db.json';含孤立代理字元者建構時throw
71
+ * @param {String} [opt.db='worm'] 輸入使用資料庫名稱字串,預設'worm';含孤立代理字元者建構時throw
72
+ * @param {String} [opt.cl='test'] 輸入使用資料表名稱字串,預設'test';含孤立代理字元者建構時throw
61
73
  * @param {Boolean} [opt.useCache=false] 輸入是否使用select快取,適用於單程序操作,預設false
62
74
  * @param {Boolean} [opt.autoGenPk=true] 輸入insert、insertBulk與save於數據未帶有效主鍵(本套件為id欄位)時是否自動產生主鍵值,預設true;給false代表主鍵改由呼叫端自備,套件不產生亦不檢查其唯一性與格式,未帶有效主鍵者將reject
63
75
  * @returns {Object} 回傳操作資料庫物件,各事件功能詳見說明
@@ -67,23 +79,42 @@ function WOrmLowdb(opt = {}) {
67
79
  //_cache
68
80
  let _cache = null
69
81
 
82
+ //chkOptStr, 儲存位址名稱(opt.url、opt.db、opt.cl)之良構檢核, 孤立代理字元者於建構時同步throw, 先於建立任何檔案或讀寫:
83
+ //url為檔案路徑, 經檔案系統UTF-8化後孤立代理被改寫為U+FFFD而落到他檔; db與cl雖為JSON內之鍵可保真, 全系列仍一律拒收(同一應用更換後端時行為一致);
84
+ //名稱不檢NUL(opt.nul為false), 訊息不回顯值
85
+ let chkOptStr = (name, v) => {
86
+ let r = chkOrmStr(v, { nul: false })
87
+ if (r !== null) {
88
+ throw new Error(`invalid opt.${name}: ${r}`)
89
+ }
90
+ }
91
+
70
92
  //url
71
93
  let url = get(opt, 'url')
72
94
  if (!isestr(url)) {
73
95
  url = './db.json'
74
96
  }
97
+ else {
98
+ chkOptStr('url', url)
99
+ }
75
100
 
76
101
  //db
77
102
  let db = get(opt, 'db')
78
103
  if (!isestr(db)) {
79
104
  db = 'worm'
80
105
  }
106
+ else {
107
+ chkOptStr('db', db)
108
+ }
81
109
 
82
110
  //cl
83
111
  let cl = get(opt, 'cl')
84
112
  if (!isestr(cl)) {
85
113
  cl = 'test'
86
114
  }
115
+ else {
116
+ chkOptStr('cl', cl)
117
+ }
87
118
 
88
119
  //useCache
89
120
  let useCache = get(opt, 'useCache')
@@ -358,6 +389,45 @@ function WOrmLowdb(opt = {}) {
358
389
  return data
359
390
  }
360
391
 
392
+ //chkPk, 主鍵之檢核, 回傳null(合格)或固定原因字串(不回顯值), 供寫入前(procWellFormed)、selectByPk與del共用:
393
+ //空字串非有效主鍵(同isestr); 其餘依wsemi之chkOrmStr: 非字串、含NUL(主鍵併入NUL, 與find同標準)、含孤立代理字元
394
+ //(lowdb以JSON存檔可保真, 但全系列一律拒收, 使同一應用更換後端時資料行為一致; 孤立代理字元之主鍵於其他後端會被改寫為U+FFFD而命中他筆)
395
+ let chkPk = (id) => {
396
+ if (id === '') {
397
+ return 'string is empty'
398
+ }
399
+ return chkOrmStr(id)
400
+ }
401
+
402
+ //getWellFormedMsg, 檢核第k筆之主鍵與資料, 回傳null(合格)或錯誤訊息; 訊息為固定詞彙而不回顯值, 路徑含批次索引(單一物件呼叫之索引為data[0]),
403
+ //與wsemi之cvOrmFind及案例表同詞彙:
404
+ // invalid id: <原因> at [data[<k>].id]
405
+ // invalid data: <原因> at [data[<k>].<路徑>]
406
+ //1.作用於procPk之後(已cloneDeep並補主鍵)之複本, 且其後之判定、寫入皆只用該複本, 檢查之值即使用之值; 自有存取器於cloneDeep時已轉為資料屬性
407
+ //2.同一筆主鍵先於資料; 資料(chkOrmData)檢鍵名與字串值(含巢狀物件、陣列、Map、Set、Error與RegExp之文字)之孤立代理字元, 值與鍵名之NUL不拒
408
+ //3.不隨autoGenPk改變: 如實存放性檢核, 非格式偏好
409
+ let getWellFormedMsg = (v, k) => {
410
+ let r = chkPk(v.id)
411
+ if (r !== null) {
412
+ return `invalid id: ${r} at [data[${k}].id]`
413
+ }
414
+ let e = chkOrmData(v, { root: `data[${k}]` })
415
+ if (e !== null) {
416
+ return `invalid data: ${e.reason} at [${e.path}]`
417
+ }
418
+ return null
419
+ }
420
+
421
+ //procWellFormed, 供insert與insertBulk於任何寫入之前檢核各筆: 任一筆不合格即拋出為整批性錯誤(以索引最小者為準)而不寫入任何一筆
422
+ let procWellFormed = (data) => {
423
+ each(data, function(v, k) {
424
+ let msg = getWellFormedMsg(v, k)
425
+ if (msg !== null) {
426
+ throw new Error(msg)
427
+ }
428
+ })
429
+ }
430
+
361
431
  //readData, 讀取資料庫檔案並取得本資料表之陣列
362
432
  //註: 使用lowdb.read()初始化後須馬上檢測, 若無key則須先創建空陣列
363
433
  let readData = async() => {
@@ -382,14 +452,15 @@ function WOrmLowdb(opt = {}) {
382
452
  }
383
453
  }
384
454
 
385
- //getKpPk, 由數據陣列建立[主鍵值 => {k, v}]之對照表, k為所在索引
455
+ //getKpPk, 由數據陣列建立[主鍵值 => {k, v}]之Map, k為所在索引
386
456
  //僅收錄具有效主鍵值者, 令資料表內若有主鍵值無效之殘留數據不致被誤配為命中
457
+ //須用Map而非一般物件加in(haskey)或lodash get: 主鍵為constructor、toString、__proto__等Object.prototype成員名時, 走原型鏈恆有值(帳本R-PKMAP-01)
387
458
  let getKpPk = (ltdt) => {
388
- let kp = {}
459
+ let kp = new Map()
389
460
  each(ltdt, (v, k) => {
390
461
  let id = get(v, 'id')
391
462
  if (isestr(id)) {
392
- kp[id] = { k, v }
463
+ kp.set(id, { k, v })
393
464
  }
394
465
  })
395
466
  return kp
@@ -501,8 +572,8 @@ function WOrmLowdb(opt = {}) {
501
572
  let res = null
502
573
  try {
503
574
 
504
- //check
505
- if (!isestr(pk)) {
575
+ //check, 含NUL或孤立代理字元之主鍵亦屬無效(chkPk): 此類主鍵不可能由本套件寫入(寫入側已拒收), 回null而不reject, 不發出error事件
576
+ if (!isestr(pk) || chkPk(pk) !== null) {
506
577
  //未給有效主鍵值視為查無數據
507
578
  return null
508
579
  }
@@ -512,7 +583,7 @@ function WOrmLowdb(opt = {}) {
512
583
 
513
584
  //查找資料表內pk, 判定基準與insert、insertBulk、save、del內對既有數據之認定一致(皆以id之字串值比對)
514
585
  let kp = getKpPk(ltdt)
515
- let r = get(kp, pk, null)
586
+ let r = kp.get(pk)
516
587
 
517
588
  //check
518
589
  if (iseobj(r)) {
@@ -598,6 +669,10 @@ function WOrmLowdb(opt = {}) {
598
669
  //check Date, 複本內任何深度之Date年份須介於0000-9999, 範圍外整批reject(帳本R-DATE-01)
599
670
  chkDataDate(data)
600
671
 
672
+ //check well-formed, 主鍵(併入NUL)與資料之孤立代理字元, 須於任何讀取與寫入之前完成, 只用上方cloneDeep之複本;
673
+ //置於Date檢核之後: 兩者並存時先報Date(既有行為與測試不變)
674
+ procWellFormed(data)
675
+
601
676
  //read
602
677
  let arrOld = await readData()
603
678
 
@@ -611,14 +686,14 @@ function WOrmLowdb(opt = {}) {
611
686
  each(data, (v) => {
612
687
 
613
688
  //check
614
- if (!haskey(kp, v.id)) {
689
+ if (!kp.has(v.id)) {
615
690
  //未存在v.id
616
691
 
617
692
  //push
618
693
  ltpush.push(v)
619
694
 
620
695
  //update, 須同步更新kp, 否則同批含重複主鍵者會全數寫入而出現重複主鍵
621
- kp[v.id] = { k: -1, v }
696
+ kp.set(v.id, { k: -1, v })
622
697
 
623
698
  ltdone.push(true)
624
699
  }
@@ -726,6 +801,9 @@ function WOrmLowdb(opt = {}) {
726
801
  //check Date, 複本內任何深度之Date年份須介於0000-9999, 範圍外整批reject(帳本R-DATE-01)
727
802
  chkDataDate(data)
728
803
 
804
+ //check well-formed, 同insert(置於Date檢核之後)
805
+ procWellFormed(data)
806
+
729
807
  //read
730
808
  let arrOld = await readData()
731
809
 
@@ -739,11 +817,11 @@ function WOrmLowdb(opt = {}) {
739
817
  //同批含重複主鍵者亦於此被偵測為衝突(kp隨檢查同步更新)
740
818
  let pkConflict = null
741
819
  each(data, (v) => {
742
- if (haskey(kp, v.id)) {
820
+ if (kp.has(v.id)) {
743
821
  pkConflict = v.id
744
822
  return false //中止each
745
823
  }
746
- kp[v.id] = { k: -1, v }
824
+ kp.set(v.id, { k: -1, v })
747
825
  })
748
826
 
749
827
  //check
@@ -832,6 +910,12 @@ function WOrmLowdb(opt = {}) {
832
910
  //check Date, 複本內任何深度之Date年份須介於0000-9999, 範圍外整批reject(帳本R-DATE-01); 合併與覆寫路徑之Date皆來自此複本
833
911
  chkDataDate(data)
834
912
 
913
+ //msgs, 各筆主鍵(併入NUL)與資料之良構檢核結果(合格者為null), 於任何讀取與寫入之前完成, 只用上方cloneDeep之複本;
914
+ //save為逐筆函數, 不合格者僅該筆失敗(ok為0並附err), 同批其他筆照常處理
915
+ let msgs = map(data, function(v, k) {
916
+ return getWellFormedMsg(v, k)
917
+ })
918
+
835
919
  //read
836
920
  let arrOld = await readData()
837
921
 
@@ -841,17 +925,22 @@ function WOrmLowdb(opt = {}) {
841
925
  //kp
842
926
  let kp = getKpPk(arrNew)
843
927
 
844
- //pmSeries
928
+ //pmSeries, 回呼之第二參數為批次索引
845
929
  let b = false
846
- res = await pmSeries(data, async(v) => {
930
+ res = await pmSeries(data, async(v, k) => {
847
931
 
848
932
  //rest
849
933
  let rest = null
850
934
 
851
935
  try {
852
936
 
937
+ //不合格者以該筆失敗回報(下方catch組成ok為0之結果), 先於該筆之任何讀取與寫入
938
+ if (msgs[k] !== null) {
939
+ throw new Error(msgs[k])
940
+ }
941
+
853
942
  //查找資料表內v.id
854
- let r = get(kp, v.id, null)
943
+ let r = kp.get(v.id)
855
944
 
856
945
  //existed
857
946
  let existed = iseobj(r)
@@ -863,8 +952,17 @@ function WOrmLowdb(opt = {}) {
863
952
  //vv, 現值
864
953
  let vv = r.v
865
954
 
955
+ //vIn, 以merge(lodash)讀取v一次所得之純資料物件, 檢核之並以之合併: merge以keysIn取值, 會讀到繼承之可列舉存取器(類別實例之原型上),
956
+ //其每次讀取可回不同值, 上方對複本之檢核與此處合併所讀者未必相同, 故檢查之值即合併之值; 不合格者該筆ok為0(尚未寫入arrNew)
957
+ //只檢核來自輸入者(vIn), 不檢核現值vv之既有內容: 舊資料之孤立代理字元不遷移(lowdb保真而留存), 不得使對該列之無關更新被拒
958
+ let vIn = merge({}, v)
959
+ let eIn = chkOrmData(vIn, { root: `data[${k}]` })
960
+ if (eIn !== null) {
961
+ throw new Error(`invalid data: ${eIn.reason} at [${eIn.path}]`)
962
+ }
963
+
866
964
  //vm, 合併後之結果, 另建新物件以免污染原數據
867
- let vm = merge({}, vv, v)
965
+ let vm = merge({}, vv, vIn)
868
966
 
869
967
  //判定基準為[合併後結果與現值相同], 而非[待寫入物件與現值全等],
870
968
  //令nModified忠實反映是否真的寫入, 僅給部份欄位且值皆相同者不應回報已修改
@@ -882,7 +980,7 @@ function WOrmLowdb(opt = {}) {
882
980
 
883
981
  //update
884
982
  arrNew[r.k] = vm
885
- kp[v.id] = { k: r.k, v: vm }
983
+ kp.set(v.id, { k: r.k, v: vm })
886
984
 
887
985
  b = true
888
986
 
@@ -902,7 +1000,7 @@ function WOrmLowdb(opt = {}) {
902
1000
  //push
903
1001
  let k = size(arrNew)
904
1002
  arrNew.push(v)
905
- kp[v.id] = { k, v }
1003
+ kp.set(v.id, { k, v })
906
1004
 
907
1005
  b = true
908
1006
 
@@ -1028,10 +1126,10 @@ function WOrmLowdb(opt = {}) {
1028
1126
  //kp
1029
1127
  let kp = getKpPk(arrOld)
1030
1128
 
1031
- //pmSeries
1129
+ //pmSeries, 回呼之第二參數為批次索引
1032
1130
  let kpDel = {}
1033
1131
  let b = false
1034
- res = await pmSeries(data, async(v) => {
1132
+ res = await pmSeries(data, async(v, k) => {
1035
1133
 
1036
1134
  //rest
1037
1135
  let rest = null
@@ -1039,24 +1137,27 @@ function WOrmLowdb(opt = {}) {
1039
1137
  try {
1040
1138
 
1041
1139
  //id
1042
- let id = get(v, 'id', '')
1140
+ let id = get(v, 'id')
1141
+
1142
+ //check, 主鍵之檢核(chkPk): 非字串、空字串、含NUL、含孤立代理字元者皆無效
1143
+ let msgPk = chkPk(id)
1043
1144
 
1044
1145
  //check
1045
- if (!isestr(id)) {
1146
+ if (msgPk !== null) {
1046
1147
  //未給有效v.id視為該筆數據有問題而無法處理, 以ok為0並附err回報,
1047
- //與[已給v.id但查無數據]之ok為1有別, 二者須可由ok分辨
1148
+ //與[已給v.id但查無數據]之ok為1有別, 二者須可由ok分辨; 訊息為固定詞彙, 含批次索引而不回顯值
1048
1149
  //註: 亦不可將無效主鍵送進查詢條件, 故於此直接定案
1049
1150
  rest = {
1050
1151
  n: 0,
1051
1152
  nDeleted: 0,
1052
1153
  ok: 0,
1053
- err: `can not delete by invalid id[${id}]`,
1154
+ err: `invalid id: ${msgPk} at [data[${k}].id]`,
1054
1155
  }
1055
1156
  }
1056
1157
  else {
1057
1158
 
1058
1159
  //查找資料表內v.id
1059
- let r = get(kp, id, null)
1160
+ let r = kp.get(id)
1060
1161
 
1061
1162
  //check
1062
1163
  if (iseobj(r)) {
@@ -1066,7 +1167,7 @@ function WOrmLowdb(opt = {}) {
1066
1167
  kpDel[r.k] = true
1067
1168
 
1068
1169
  //update, 須同步移除, 否則同批重複主鍵者會重複計入nDeleted
1069
- delete kp[id]
1170
+ kp.delete(id)
1070
1171
 
1071
1172
  b = true
1072
1173