w-orm-reladb 1.1.12 → 1.1.13

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/cvFind.mjs CHANGED
@@ -1,5 +1,8 @@
1
1
  import Sequelize from 'sequelize'
2
+ import get from 'lodash-es/get.js'
2
3
  import cvOrmFind from 'wsemi/src/cvOrmFind.mjs'
4
+ import chkOrmStr from 'wsemi/src/chkOrmStr.mjs'
5
+ import chkOrmData from 'wsemi/src/chkOrmData.mjs'
3
6
 
4
7
 
5
8
  //查詢條件(find)之轉譯, 供select與delAll共用, 令同一find於兩函數由同一SQL謂詞決定命中
@@ -13,6 +16,7 @@ import cvOrmFind from 'wsemi/src/cvOrmFind.mjs'
13
16
  //不得供破壞性之delAll使用, 1.1.7起改自寫白名單; 第33項批1起形狀層改由cvOrmFind單一擁有, 本模組刪除Symbol/Op全集、$not $nor $regex $options $exists、
14
17
  //RegExp值、陣列值即$in、非物件視為{}等形狀層(破壞性變更見CLAUDE_rulebook.md與README)
15
18
  //主鍵值之正規化(normPk)亦屬本模組, 供主鍵函數與主鍵欄條件共用, 規則見R8
19
+ //寫入側之字串良構檢核(chkPkWf、chkWf, 規則見R13)亦屬本模組: 字串之判定單一出處為wsemi之chkOrmStr、chkOrmData, 本模組只負責接線與訊息組裝
16
20
 
17
21
 
18
22
  //Op
@@ -560,6 +564,8 @@ function unwrap(v) {
560
564
  * DECIMAL主鍵欄之值小數位多於scale者寫入時會被四捨五入而改變鍵(mssql之DECIMAL(10,2)存1.254為1.25, insert回報已存在而selectByPk(1.254)回null), 視為型別不符
561
565
  * 註: 其他型別之主鍵欄不檢型別, 收非空字串與有限數值
562
566
  * 註: NaN、±Infinity、空字串、布林、物件、陣列、Date、BigInt、null、undefined為無效
567
+ * 註: 含孤立代理碼元或NUL之字串為無效(第66項, R13; 與條件側同標準): UTF-8存放之資料庫會把孤立代理碼元改寫為U+FFFD而使不同主鍵寫成同一鍵,
568
+ * 回傳形狀不變({kind:'invalid'}), 故沿用各呼叫端對無效主鍵之既有處置; 需區辨[未給]與[不合格字串]者(autoGenPk不得以自動產生之主鍵取代呼叫端所給之不合格字串)用chkPkWf
563
569
  *
564
570
  * @param {*} v 輸入主鍵值
565
571
  * @param {String} kind 輸入主鍵欄之型別分類, 取自getColKind
@@ -576,6 +582,9 @@ function normPk(v, kind, scale = null) {
576
582
  if (v === '') {
577
583
  return { kind: 'invalid' }
578
584
  }
585
+ if (chkOrmStr(v) !== null) {
586
+ return { kind: 'invalid' }
587
+ }
579
588
  if (kind === 'int') {
580
589
  if (!reIntStd.test(v)) {
581
590
  return { kind: 'mismatch' }
@@ -615,6 +624,70 @@ function normPk(v, kind, scale = null) {
615
624
  return { kind: 'invalid' }
616
625
  }
617
626
 
627
+
628
+ /**
629
+ * 檢核主鍵值之字串良構: 字串(含new String之原始值)不得含孤立代理碼元或NUL(第66項, R13)
630
+ *
631
+ * 註: 判定單一出處為wsemi之chkOrmStr(預設連NUL一併檢查, 主鍵與條件側同標準); 非字串回傳null(其有效性由normPk判定)
632
+ * 註: 先依normPk同一規則拆包裝物件(new String), 令其與原始字串同處置
633
+ *
634
+ * @param {*} v 輸入主鍵值
635
+ * @returns {String|null} 回傳null(非字串或合格),或固定原因字串'string contains lone surrogate'、'string contains NUL'
636
+ */
637
+ function chkPkWf(v) {
638
+ v = unwrap(v)
639
+ if (typeof v !== 'string') {
640
+ return null
641
+ }
642
+ return chkOrmStr(v)
643
+ }
644
+
645
+
646
+ /**
647
+ * 逐筆檢核寫入數據之字串良構(第66項, R13), 回傳與輸入等長之訊息陣列
648
+ *
649
+ * 註: 輸入須為呼叫端數據之cloneDeep複本(檢查之值即使用之值): 本函數對各筆先檢主鍵再(withData為true時)檢資料, 同筆內主鍵先於資料,
650
+ * 資料由wsemi之chkOrmData走訪(鍵名先於值, 含巢狀物件、陣列元素、Map/Set等實存內容; 不論欄位是否在model內, 檢核先於欄位過濾)
651
+ * 註: 訊息為固定詞彙且不回顯值: invalid id: <原因> at [data[<索引>].<主鍵欄>]、invalid data: <原因> at [<路徑>]; 合格者為null
652
+ * 註: 主鍵值為包裝物件(new String、new Number)且合格者, 將原始值(字串或數值)寫回複本, 令其後之正規化讀到的即本函數所檢查者(包裝物件之valueOf只求值一次)
653
+ * 註: 資料之NUL不檢查(值之NUL不拒, 僅主鍵拒NUL)
654
+ *
655
+ * @param {Array} data 輸入數據陣列(已cloneDeep)
656
+ * @param {String} pk 輸入主鍵欄位名
657
+ * @param {Boolean} withData 輸入是否一併檢核資料(false只檢主鍵, 供del)
658
+ * @returns {Array} 回傳訊息陣列,合格者為null
659
+ */
660
+ function chkWf(data, pk, withData) {
661
+ let msgs = []
662
+ for (let k = 0; k < data.length; k++) {
663
+ let v = data[k]
664
+ let msg = null
665
+
666
+ //pk, 主鍵先於資料; 包裝物件之valueOf只讀取一次(u為原始值), 合格者寫回複本
667
+ let pv = get(v, pk)
668
+ let u = unwrap(pv)
669
+ let why = chkPkWf(u)
670
+ if (why !== null) {
671
+ msg = `invalid id: ${why} at [data[${k}].${pk}]`
672
+ }
673
+ else if (u !== pv && (typeof u === 'string' || typeof u === 'number') && v !== null && typeof v === 'object') {
674
+ v[pk] = u
675
+ }
676
+
677
+ //data
678
+ if (msg === null && withData) {
679
+ let r = chkOrmData(v, { root: `data[${k}]` })
680
+ if (r !== null) {
681
+ msg = `invalid data: ${r.reason} at [${r.path}]`
682
+ }
683
+ }
684
+
685
+ msgs.push(msg)
686
+ }
687
+ return msgs
688
+ }
689
+
690
+
618
691
  /**
619
692
  * 檢核純量值: 字串、有限數值、布林、有效Date, 以及null(是否允許null已由cvOrmFind依運算子判定); 並須符合欄位型別分類
620
693
  * 註: 物件、陣列等不得交sequelize之型別stringify, 否則FLOAT成'NaN'、INTEGER成'[object Object]'、DATE成本地今日00:00而誤刪;
@@ -873,5 +946,5 @@ function cvFind(find, ctx) {
873
946
  }
874
947
 
875
948
 
876
- export { getColKind, getColScale, normPk, chkFindShape, inDateRange, genDateChecker }
949
+ export { getColKind, getColScale, normPk, chkPkWf, chkWf, chkFindShape, inDateRange, genDateChecker }
877
950
  export default cvFind
@@ -95,6 +95,34 @@ describe('dist (installed layout)', function() {
95
95
  assert.deepStrictEqual(errs, [])
96
96
  })
97
97
 
98
+ it('字串良構(R13): 經dist使用時, 孤立代理之主鍵與資料整批reject、selectByPk回null、建構時opt含孤立代理throw, 良構字串(含emoji)照常', async function() {
99
+ //wsemi之chkOrmStr、chkOrmData隨dist打包(純JS), 須在打包與轉譯後仍判定正確
100
+ let w = wo({ url: 'sqlite://:', db: 'worm', cl: 'users', fdModels, storage: `${fd}/wf.sqlite` })
101
+ let errs = []
102
+ w.on('error', (mode, data, err) => {
103
+ errs.push([mode, err])
104
+ })
105
+ assert.strictEqual(await w.createStorage(), 'created')
106
+
107
+ let msgOf = (err) => String(err && err.message !== undefined ? err.message : err)
108
+ let r1 = await w.insert([{ id: 'ok1', name: 'A' }, { id: 'a\uD800', name: 'B' }]).then((v) => ({ v }), (err) => ({ err }))
109
+ assert.strictEqual(msgOf(r1.err), 'invalid id: string contains lone surrogate at [data[1].id]')
110
+ let r2 = await w.insertBulk([{ id: 'ok2', name: 'x\uDC00' }]).then((v) => ({ v }), (err) => ({ err }))
111
+ assert.strictEqual(msgOf(r2.err), 'invalid data: string contains lone surrogate at [data[0].name]')
112
+ let r3 = await w.insert([{ id: 'ok3', name: 'A', meta: { tags: ['a', 'b\uD800'] } }]).then((v) => ({ v }), (err) => ({ err }))
113
+ assert.strictEqual(msgOf(r3.err), 'invalid data: string contains lone surrogate at [data[0].meta.tags[1]]')
114
+ assert.deepStrictEqual(errs.map((v) => v[0]), ['insert', 'insertBulk', 'insert'])
115
+ assert.deepStrictEqual(await w.select(), [], '整批reject不寫入任何一筆')
116
+
117
+ let rs = await w.save([{ id: 'a\u0000b', name: 'x' }, { id: 'e😀', name: 'emoji' }])
118
+ assert.deepStrictEqual(rs.map((v) => v.ok), [0, 1])
119
+ assert.strictEqual(rs[0].err, 'invalid id: string contains NUL at [data[0].id]')
120
+ assert.strictEqual(await w.selectByPk('e\uD83D'), null)
121
+ assert.deepStrictEqual(await w.selectByPk('e😀'), { id: 'e😀', name: 'emoji', value: null })
122
+
123
+ assert.throws(() => wo({ url: 'sqlite://:', db: 'worm', cl: 'users\uD800', fdModels }), (err) => msgOf(err) === 'invalid opt.cl: string contains lone surrogate')
124
+ })
125
+
98
126
  it('json型model(對照): createStorage、insert、select皆成功且不發error事件', async function() {
99
127
  //rulebook R10: 兩種modelType經main皆須可用; json型不經requireModel, 修正前即可用
100
128
  let w = wo({ url: 'sqlite://:', db: 'worm', cl: 'users', fdModels, modelType: 'json', storage: `${fd}/json.sqlite` })
@@ -4,6 +4,7 @@ import { promisify } from 'util'
4
4
  import { defineSuites } from './api-suite.mjs'
5
5
  import { defineWhitelistSuites } from './api-find-whitelist.mjs'
6
6
  import { defineDateWriteSuites } from './api-date-write.mjs'
7
+ import { defineWellformedSuites } from './api-wellformed.mjs'
7
8
 
8
9
 
9
10
  //測試自行創建docker容器供MSSQL服務, 測試結束即銷毀, 故僅需環境已安裝docker
@@ -56,12 +57,23 @@ let startContainer = async () => {
56
57
  throw new Error('需先安裝並啟動docker才能執行mssql測試')
57
58
  })
58
59
 
59
- //清除前次殘留容器
60
- await docker(['rm', '-f', ctName]).catch(() => {})
60
+ //清除前次殘留容器(-v連其匿名volume一併移除)
61
+ await docker(['rm', '-f', '-v', ctName]).catch(() => {})
61
62
 
62
- //run
63
- ctPort = await getFreePort()
64
- await docker(['run', '-d', '--rm', '--name', ctName, '-e', 'ACCEPT_EULA=Y', '-e', `MSSQL_SA_PASSWORD=${saPassword}`, '-p', `${ctPort}:1433`, ctImage])
63
+ //run, getFreePort取得埠到docker綁定之間可能被同機其他程序佔用(綁定失敗), 故換埠重試
64
+ for (let i = 0; ; i++) {
65
+ ctPort = await getFreePort()
66
+ try {
67
+ await docker(['run', '-d', '--rm', '--name', ctName, '-e', 'ACCEPT_EULA=Y', '-e', `MSSQL_SA_PASSWORD=${saPassword}`, '-p', `${ctPort}:1433`, ctImage])
68
+ break
69
+ }
70
+ catch (err) {
71
+ if (i >= 4 || String(err.message).indexOf('bind') < 0) {
72
+ throw err
73
+ }
74
+ await docker(['rm', '-f', '-v', ctName]).catch(() => {})
75
+ }
76
+ }
65
77
 
66
78
  //等待服務就緒, 首次需拉取映像故給予較長時間
67
79
  let ready = false
@@ -86,9 +98,30 @@ let startContainer = async () => {
86
98
 
87
99
  }
88
100
 
89
- //stopContainer, 銷毀容器
101
+ //stopContainer, 銷毀容器: 強制移除須帶-v, 否則映像宣告之VOLUME會成為懸置之匿名volume(逐次累積而佔滿磁碟); 移除後回驗容器與其volume皆已不存在
90
102
  let stopContainer = async () => {
91
- await docker(['rm', '-f', ctName]).catch(() => {})
103
+
104
+ //vols, 先記下容器掛載之volume名稱(移除後即無從查詢)
105
+ let vols = await docker(['inspect', '-f', '{{range .Mounts}}{{if eq .Type "volume"}}{{.Name}} {{end}}{{end}}', ctName])
106
+ .then((r) => String(r.stdout).trim().split(/\s+/).filter((v) => v !== ''))
107
+ .catch(() => [])
108
+
109
+ await docker(['rm', '-f', '-v', ctName]).catch(() => {})
110
+
111
+ //回驗: 容器已不存在
112
+ let left = await docker(['ps', '-a', '-q', '--filter', `name=^${ctName}$`]).then((r) => String(r.stdout).trim())
113
+ if (left !== '') {
114
+ throw new Error(`mssql容器[${ctName}]移除後仍存在`)
115
+ }
116
+
117
+ //回驗: 其volume已隨容器移除
118
+ for (let v of vols) {
119
+ let still = await docker(['volume', 'inspect', v]).then(() => true, () => false)
120
+ if (still) {
121
+ throw new Error(`mssql容器[${ctName}]之volume[${v}]未隨容器移除`)
122
+ }
123
+ }
124
+
92
125
  }
93
126
 
94
127
 
@@ -117,3 +150,4 @@ after(async function() {
117
150
  defineSuites('mssql', getOpt)
118
151
  defineWhitelistSuites('mssql', getOpt)
119
152
  defineDateWriteSuites('mssql', getOpt)
153
+ defineWellformedSuites('mssql', getOpt)
@@ -2,6 +2,7 @@ import fs from 'fs'
2
2
  import { defineSuites } from './api-suite.mjs'
3
3
  import { defineWhitelistSuites } from './api-find-whitelist.mjs'
4
4
  import { defineDateWriteSuites } from './api-date-write.mjs'
5
+ import { defineWellformedSuites } from './api-wellformed.mjs'
5
6
 
6
7
 
7
8
  //sqlite使用獨立暫存storage檔測試, 不使用專案根目錄之worm.sqlite
@@ -44,3 +45,4 @@ after(function() {
44
45
  defineSuites('sqlite', getOpt)
45
46
  defineWhitelistSuites('sqlite', getOpt)
46
47
  defineDateWriteSuites('sqlite', getOpt)
48
+ defineWellformedSuites('sqlite', getOpt)