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.
- package/README.md +84 -1
- package/dist/w-orm-reladb.umd.js +2 -2
- package/dist/w-orm-reladb.umd.js.map +1 -1
- package/docs/WOrmReladb.html +35 -19
- package/docs/WOrmReladb.mjs.html +190 -61
- package/docs/cvFind.mjs.html +173 -7
- package/docs/global.html +560 -11
- package/docs/importModels.mjs.html +2 -2
- package/docs/index.html +2 -2
- package/models/tfloat.js +20 -0
- package/models/tlower.js +38 -0
- package/models/twf.js +20 -0
- package/models/twfk.js +20 -0
- package/package.json +2 -2
- package/src/WOrmReladb.mjs +188 -59
- package/src/cvFind.mjs +171 -5
- package/test/api-dist.test.mjs +28 -0
- package/test/api-float.mjs +262 -0
- package/test/api-lower-cases.mjs +129 -0
- package/test/api-mssql.test.mjs +47 -7
- package/test/api-scope.mjs +208 -0
- package/test/api-sqlite.test.mjs +8 -0
- package/test/api-wellformed.mjs +645 -0
- package/test/tools/floatTools.mjs +117 -0
- package/test/unit-cvfind.test.mjs +23 -2
- package/test/unit-float.test.mjs +492 -0
- package/test/unit-wellformed.test.mjs +322 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import wo from '../src/WOrmReladb.mjs'
|
|
3
|
+
import lowerCases from 'wsemi/src/_lowerFindCases.mjs'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
//本檔為mssql與sqlite共用之wsemi契約案例表_lowerFindCases(方言中立條件IR之契約表, 85案)測試套組(CLAUDE_rulebook.md之R15), 由api-sqlite.test.mjs與api-mssql.test.mjs引入
|
|
7
|
+
//本套件不採IR, 但此表之hits(手寫, 依SQL三值邏輯手推)是SQL族對公開select與delAll之共同期望, 作為日後任何翻譯層變更之守門
|
|
8
|
+
//規則: 依表之columns建model(models/tlower.js), seed()6列; 逐案對公開select與delAll驗——
|
|
9
|
+
// 非schema案兩函數須皆接受, select命中 = delAll實際刪除之id集合 = hits; hits為[]者恆假;
|
|
10
|
+
// hits為null(字串順序比較依collation而異)只驗兩函數一致; schema為true者(型別化後端可因欄位型別拒收)可reject, 但兩函數須一致reject、資料不變, 未reject時須 = hits
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
let { seed, semantics } = lowerCases
|
|
14
|
+
|
|
15
|
+
let ALL = [1, 2, 3, 4, 5, 6]
|
|
16
|
+
|
|
17
|
+
//案例表規模下限(wsemi 1.9.25為85案)與schema案之上限, 防止案例表被掏空後仍全綠
|
|
18
|
+
let nMin = 85
|
|
19
|
+
|
|
20
|
+
let ids = (rs) => rs.map((v) => v.id).sort((a, b) => a - b)
|
|
21
|
+
|
|
22
|
+
let msgOf = (err) => String(err && err.message !== undefined ? err.message : err)
|
|
23
|
+
|
|
24
|
+
//settle, 執行promise並回傳{ok,v}或{err}
|
|
25
|
+
let settle = (pm) => pm.then((v) => ({ ok: true, v })).catch((err) => ({ err }))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 定義共用測試套組
|
|
30
|
+
*
|
|
31
|
+
* @param {String} label 輸入後端名稱字串('sqlite'或'mssql'), 供describe標題使用
|
|
32
|
+
* @param {Function} getOpt 輸入取得建構設定之函數, 於hook內呼叫以取得當下有效之連線資訊
|
|
33
|
+
*/
|
|
34
|
+
export function defineLowerCaseSuites(label, getOpt) {
|
|
35
|
+
|
|
36
|
+
describe(`${label} lowerFind契約表: 案例逐案對select與delAll`, function() {
|
|
37
|
+
|
|
38
|
+
let w = null
|
|
39
|
+
let ins = null
|
|
40
|
+
let cs = null
|
|
41
|
+
|
|
42
|
+
//stats, 各案測試時累計, 供最末之防空轉斷言
|
|
43
|
+
let stats = { accepted: 0, acceptedHit: 0, acceptedNull: 0, rejected: 0 }
|
|
44
|
+
|
|
45
|
+
let reset = async () => {
|
|
46
|
+
await w.delAll({}, cs)
|
|
47
|
+
let r = await w.insert(seed(), cs)
|
|
48
|
+
assert.strictEqual(r.nInserted, 6)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
before(async function() {
|
|
52
|
+
w = wo(getOpt({ cl: 'tlower' }))
|
|
53
|
+
await w.createStorage()
|
|
54
|
+
ins = await w.init()
|
|
55
|
+
cs = { instance: ins }
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
after(async function() {
|
|
59
|
+
await ins.close()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it(`案例表規模: 至少${nMin}案, 代號不重複, 含hits為[]、非空、null三類與schema案`, function() {
|
|
63
|
+
assert.ok(semantics.length >= nMin, `案數 ${semantics.length}`)
|
|
64
|
+
assert.strictEqual(new Set(semantics.map((c) => c.id)).size, semantics.length)
|
|
65
|
+
assert.ok(semantics.some((c) => Array.isArray(c.hits) && c.hits.length === 0))
|
|
66
|
+
assert.ok(semantics.some((c) => Array.isArray(c.hits) && c.hits.length > 0))
|
|
67
|
+
assert.ok(semantics.some((c) => c.hits === null))
|
|
68
|
+
assert.ok(semantics.some((c) => c.schema === true))
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('種子: 6列, id 1~6', async function() {
|
|
72
|
+
let sd = seed()
|
|
73
|
+
assert.deepStrictEqual(sd.map((v) => v.id), ALL)
|
|
74
|
+
await reset()
|
|
75
|
+
assert.deepStrictEqual(ids(await w.select({}, cs)), ALL)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
for (let c of semantics) {
|
|
79
|
+
|
|
80
|
+
it(`${c.id} (${c.node}): select命中 = delAll刪除 = hits${c.hits === null ? '(null: 只驗兩函數一致)' : ''}${c.schema ? '(schema案可reject)' : ''}`, async function() {
|
|
81
|
+
await reset()
|
|
82
|
+
let rs = await settle(w.select(c.find(), cs))
|
|
83
|
+
if (rs.err !== undefined) {
|
|
84
|
+
//schema案: 型別化後端可因欄位型別檢核而reject, 此時兩函數須一致reject且資料不變
|
|
85
|
+
assert.ok(c.schema === true, `非schema案不得reject: ${msgOf(rs.err)}`)
|
|
86
|
+
assert.strictEqual(msgOf(rs.err).indexOf('invalid find: '), 0, msgOf(rs.err))
|
|
87
|
+
let before = ids(await w.select({}, cs))
|
|
88
|
+
let rd = await settle(w.delAll(c.find(), cs))
|
|
89
|
+
assert.ok(rd.err !== undefined, 'select reject而delAll未reject')
|
|
90
|
+
assert.strictEqual(msgOf(rd.err), msgOf(rs.err))
|
|
91
|
+
assert.deepStrictEqual(ids(await w.select({}, cs)), before)
|
|
92
|
+
stats.rejected += 1
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
let sel = ids(rs.v)
|
|
96
|
+
if (c.hits !== null) {
|
|
97
|
+
assert.deepStrictEqual(sel, c.hits, 'select之命中')
|
|
98
|
+
}
|
|
99
|
+
let r = await w.delAll(c.find(), cs)
|
|
100
|
+
let left = ids(await w.select({}, cs))
|
|
101
|
+
let gone = ALL.filter((v) => left.indexOf(v) < 0)
|
|
102
|
+
assert.deepStrictEqual(gone, sel, 'delAll之刪除 = select之命中')
|
|
103
|
+
if (c.hits !== null) {
|
|
104
|
+
assert.deepStrictEqual(gone, c.hits, 'delAll之刪除')
|
|
105
|
+
}
|
|
106
|
+
assert.deepStrictEqual(r, { n: gone.length, nDeleted: gone.length, ok: 1 })
|
|
107
|
+
stats.accepted += 1
|
|
108
|
+
if (c.hits === null) {
|
|
109
|
+
stats.acceptedNull += 1
|
|
110
|
+
}
|
|
111
|
+
else if (c.hits.length > 0) {
|
|
112
|
+
stats.acceptedHit += 1
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
it('防空轉: 全部非schema案皆被接受(任一正常案全部reject即紅), 且命中非空之案數達標', function() {
|
|
119
|
+
let nSchema = semantics.filter((c) => c.schema === true).length
|
|
120
|
+
let nHit = semantics.filter((c) => Array.isArray(c.hits) && c.hits.length > 0).length
|
|
121
|
+
assert.ok(stats.rejected <= nSchema, `reject ${stats.rejected} 案, 超過schema案數 ${nSchema}`)
|
|
122
|
+
assert.ok(stats.accepted >= semantics.length - nSchema, `接受 ${stats.accepted} 案, 少於 ${semantics.length - nSchema}`)
|
|
123
|
+
assert.ok(stats.acceptedHit >= nHit - nSchema, `命中非空之接受案 ${stats.acceptedHit} 少於 ${nHit - nSchema}`)
|
|
124
|
+
assert.ok(stats.acceptedHit >= 50, `命中非空之接受案 ${stats.acceptedHit} 不足50`)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
}
|
package/test/api-mssql.test.mjs
CHANGED
|
@@ -4,6 +4,10 @@ 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'
|
|
8
|
+
import { defineLowerCaseSuites } from './api-lower-cases.mjs'
|
|
9
|
+
import { defineScopeSuites } from './api-scope.mjs'
|
|
10
|
+
import { defineFloatSuites } from './api-float.mjs'
|
|
7
11
|
|
|
8
12
|
|
|
9
13
|
//測試自行創建docker容器供MSSQL服務, 測試結束即銷毀, 故僅需環境已安裝docker
|
|
@@ -56,12 +60,23 @@ let startContainer = async () => {
|
|
|
56
60
|
throw new Error('需先安裝並啟動docker才能執行mssql測試')
|
|
57
61
|
})
|
|
58
62
|
|
|
59
|
-
//清除前次殘留容器
|
|
60
|
-
await docker(['rm', '-f', ctName]).catch(() => {})
|
|
63
|
+
//清除前次殘留容器(-v連其匿名volume一併移除)
|
|
64
|
+
await docker(['rm', '-f', '-v', ctName]).catch(() => {})
|
|
61
65
|
|
|
62
|
-
//run
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
//run, getFreePort取得埠到docker綁定之間可能被同機其他程序佔用(綁定失敗), 故換埠重試
|
|
67
|
+
for (let i = 0; ; i++) {
|
|
68
|
+
ctPort = await getFreePort()
|
|
69
|
+
try {
|
|
70
|
+
await docker(['run', '-d', '--rm', '--name', ctName, '-e', 'ACCEPT_EULA=Y', '-e', `MSSQL_SA_PASSWORD=${saPassword}`, '-p', `${ctPort}:1433`, ctImage])
|
|
71
|
+
break
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
if (i >= 4 || String(err.message).indexOf('bind') < 0) {
|
|
75
|
+
throw err
|
|
76
|
+
}
|
|
77
|
+
await docker(['rm', '-f', '-v', ctName]).catch(() => {})
|
|
78
|
+
}
|
|
79
|
+
}
|
|
65
80
|
|
|
66
81
|
//等待服務就緒, 首次需拉取映像故給予較長時間
|
|
67
82
|
let ready = false
|
|
@@ -86,9 +101,30 @@ let startContainer = async () => {
|
|
|
86
101
|
|
|
87
102
|
}
|
|
88
103
|
|
|
89
|
-
//stopContainer,
|
|
104
|
+
//stopContainer, 銷毀容器: 強制移除須帶-v, 否則映像宣告之VOLUME會成為懸置之匿名volume(逐次累積而佔滿磁碟); 移除後回驗容器與其volume皆已不存在
|
|
90
105
|
let stopContainer = async () => {
|
|
91
|
-
|
|
106
|
+
|
|
107
|
+
//vols, 先記下容器掛載之volume名稱(移除後即無從查詢)
|
|
108
|
+
let vols = await docker(['inspect', '-f', '{{range .Mounts}}{{if eq .Type "volume"}}{{.Name}} {{end}}{{end}}', ctName])
|
|
109
|
+
.then((r) => String(r.stdout).trim().split(/\s+/).filter((v) => v !== ''))
|
|
110
|
+
.catch(() => [])
|
|
111
|
+
|
|
112
|
+
await docker(['rm', '-f', '-v', ctName]).catch(() => {})
|
|
113
|
+
|
|
114
|
+
//回驗: 容器已不存在
|
|
115
|
+
let left = await docker(['ps', '-a', '-q', '--filter', `name=^${ctName}$`]).then((r) => String(r.stdout).trim())
|
|
116
|
+
if (left !== '') {
|
|
117
|
+
throw new Error(`mssql容器[${ctName}]移除後仍存在`)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
//回驗: 其volume已隨容器移除
|
|
121
|
+
for (let v of vols) {
|
|
122
|
+
let still = await docker(['volume', 'inspect', v]).then(() => true, () => false)
|
|
123
|
+
if (still) {
|
|
124
|
+
throw new Error(`mssql容器[${ctName}]之volume[${v}]未隨容器移除`)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
92
128
|
}
|
|
93
129
|
|
|
94
130
|
|
|
@@ -117,3 +153,7 @@ after(async function() {
|
|
|
117
153
|
defineSuites('mssql', getOpt)
|
|
118
154
|
defineWhitelistSuites('mssql', getOpt)
|
|
119
155
|
defineDateWriteSuites('mssql', getOpt)
|
|
156
|
+
defineWellformedSuites('mssql', getOpt)
|
|
157
|
+
defineLowerCaseSuites('mssql', getOpt)
|
|
158
|
+
defineScopeSuites('mssql', getOpt)
|
|
159
|
+
defineFloatSuites('mssql', getOpt)
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import assert from 'assert'
|
|
4
|
+
import wo from '../src/WOrmReladb.mjs'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
//本檔為mssql與sqlite共用之defaultScope測試套組(CLAUDE_rulebook.md之R16), 由api-sqlite.test.mjs與api-mssql.test.mjs引入
|
|
8
|
+
//sequelize預設以Object.assign合併scope.where與傳入where(同名頂層鍵後者覆寫), scope.where為Sequelize.literal或Sequelize.where(非一般物件)時scope直接被丟棄;
|
|
9
|
+
//WOrmReladb於Sequelize實例之define層級設定whereMergeStrategy:'and', 令兩者一律以AND結合
|
|
10
|
+
//5種defaultScope(純鍵、Op.or、Op.and、Sequelize.literal、Sequelize.where) x 5種find, 對公開select與delAll:
|
|
11
|
+
// select之命中 = delAll實際刪除之主鍵集合 = 手推期望(scope範圍內且符合find之列); delAll實際刪除之主鍵以不套scope之讀取(unscoped)取得, 不以select代替
|
|
12
|
+
//Op.and之scope: chkScopeForDelAll維持拒收delAll(R1), select則須套用該scope
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
let ROWS = [
|
|
16
|
+
{ id: '1', g: 'A', name: 'x' },
|
|
17
|
+
{ id: '2', g: 'B', name: 'x' },
|
|
18
|
+
{ id: '3', g: 'A', name: 'y' },
|
|
19
|
+
{ id: '4', g: 'C', name: 'x' },
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
//SCOPES, where為寫入model檔之defaultScope.where原始碼, inScope為scope範圍之手推判定
|
|
23
|
+
let SCOPES = [
|
|
24
|
+
{ key: 'pk', label: '純鍵', where: '{ g: \'A\' }', inScope: (r) => r.g === 'A', delAllRejects: false },
|
|
25
|
+
{ key: 'or', label: 'Op.or', where: '{ [Op.or]: [{ g: \'A\' }, { g: \'C\' }] }', inScope: (r) => r.g === 'A' || r.g === 'C', delAllRejects: false },
|
|
26
|
+
{ key: 'and', label: 'Op.and', where: '{ [Op.and]: [{ g: \'A\' }] }', inScope: (r) => r.g === 'A', delAllRejects: true },
|
|
27
|
+
{ key: 'lit', label: 'Sequelize.literal', where: 'sequelize.literal(\'g = \\\'A\\\'\')', inScope: (r) => r.g === 'A', delAllRejects: false },
|
|
28
|
+
{ key: 'whr', label: 'Sequelize.where', where: 'sequelize.where(sequelize.col(\'g\'), \'A\')', inScope: (r) => r.g === 'A', delAllRejects: false },
|
|
29
|
+
//model自身指定whereMergeStrategy:'overwrite'(蓋過Sequelize實例之define預設): 匯入後仍強制為'and'(外審sol F2)
|
|
30
|
+
{ key: 'ovw', label: 'Sequelize.literal + model自身指定whereMergeStrategy overwrite', where: 'sequelize.literal(\'g = \\\'A\\\'\')', modelOpt: ', whereMergeStrategy: \'overwrite\'', inScope: (r) => r.g === 'A', delAllRejects: false },
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
//FINDS, p為find之手推判定
|
|
34
|
+
let FINDS = [
|
|
35
|
+
{ label: '{g:"B"}(與scope同欄之單葉)', find: () => ({ g: 'B' }), p: (r) => r.g === 'B' },
|
|
36
|
+
{ label: '{name:"x"}(他欄單葉)', find: () => ({ name: 'x' }), p: (r) => r.name === 'x' },
|
|
37
|
+
{ label: '$or根', find: () => ({ $or: [{ g: 'B' }, { name: 'y' }] }), p: (r) => r.g === 'B' || r.name === 'y' },
|
|
38
|
+
{ label: 'and根(多欄)', find: () => ({ name: 'x', g: { $ne: 'Z' } }), p: (r) => r.name === 'x' && r.g !== 'Z' },
|
|
39
|
+
{ label: '{}(全部)', find: () => ({}), p: () => true },
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
let ids = (rs) => rs.map((v) => v.id).sort()
|
|
43
|
+
|
|
44
|
+
let msgOf = (err) => String(err && err.message !== undefined ? err.message : err)
|
|
45
|
+
|
|
46
|
+
let genModel = (cl, where, modelOpt = '') => {
|
|
47
|
+
return [
|
|
48
|
+
'module.exports = function(sequelize, DataTypes) {',
|
|
49
|
+
' let Op = require(\'sequelize\').Op',
|
|
50
|
+
` return sequelize.define('${cl}', {`,
|
|
51
|
+
' id: { type: DataTypes.STRING, allowNull: false, primaryKey: true },',
|
|
52
|
+
' g: { type: DataTypes.STRING, allowNull: true },',
|
|
53
|
+
' name: { type: DataTypes.STRING, allowNull: true },',
|
|
54
|
+
` }, { tableName: '${cl}', defaultScope: { where: ${where} }${modelOpt} })`,
|
|
55
|
+
'}',
|
|
56
|
+
'',
|
|
57
|
+
].join('\n')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 定義共用測試套組
|
|
63
|
+
*
|
|
64
|
+
* @param {String} label 輸入後端名稱字串('sqlite'或'mssql'), 供describe標題與暫存夾使用
|
|
65
|
+
* @param {Function} getOpt 輸入取得建構設定之函數, 於hook內呼叫以取得當下有效之連線資訊
|
|
66
|
+
*/
|
|
67
|
+
export function defineScopeSuites(label, getOpt) {
|
|
68
|
+
|
|
69
|
+
let fd = `./test/_tmp/api-scope-${label}`
|
|
70
|
+
|
|
71
|
+
describe(`${label} defaultScope: 6種scope(含model自身指定overwrite) x 5種find, select與delAll不越出scope`, function() {
|
|
72
|
+
|
|
73
|
+
before(function() {
|
|
74
|
+
fs.mkdirSync(fd, { recursive: true })
|
|
75
|
+
for (let sc of SCOPES) {
|
|
76
|
+
fs.writeFileSync(path.join(fd, `tsc${sc.key}.js`), genModel(`tsc${sc.key}`, sc.where, sc.modelOpt), 'utf8')
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
after(function() {
|
|
81
|
+
fs.rmSync(fd, { recursive: true, force: true })
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
for (let sc of SCOPES) {
|
|
85
|
+
|
|
86
|
+
describe(`scope ${sc.label}`, function() {
|
|
87
|
+
|
|
88
|
+
let cl = `tsc${sc.key}`
|
|
89
|
+
let w = null
|
|
90
|
+
let ins = null
|
|
91
|
+
let cs = null
|
|
92
|
+
|
|
93
|
+
//unscoped, 不套scope之全表讀取(取delAll實際刪除之主鍵), 不經select
|
|
94
|
+
let unscoped = async () => {
|
|
95
|
+
return ids(await ins.mds[cl].unscoped().findAll({ raw: true }))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
before(async function() {
|
|
99
|
+
w = wo(getOpt({ cl, fdModels: fd }))
|
|
100
|
+
await w.createStorage()
|
|
101
|
+
ins = await w.init()
|
|
102
|
+
cs = { instance: ins }
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
after(async function() {
|
|
106
|
+
await ins.close()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
beforeEach(async function() {
|
|
110
|
+
await ins.mds[cl].unscoped().destroy({ where: {} })
|
|
111
|
+
let r = await w.insert(ROWS, cs)
|
|
112
|
+
assert.strictEqual(r.nInserted, 4)
|
|
113
|
+
assert.deepStrictEqual(await unscoped(), ['1', '2', '3', '4'])
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('合併策略: Sequelize實例之define預設與匯入後之model皆為whereMergeStrategy:and(model自身指定overwrite者亦被改為and)', function() {
|
|
117
|
+
let md = ins.mds[cl]
|
|
118
|
+
assert.strictEqual(md.sequelize.options.define.whereMergeStrategy, 'and')
|
|
119
|
+
assert.strictEqual(md.options.whereMergeStrategy, 'and')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('前提: select({})僅回scope範圍內之列(scope確實套用)', async function() {
|
|
123
|
+
let exp = ids(ROWS.filter(sc.inScope))
|
|
124
|
+
assert.ok(exp.length > 0 && exp.length < ROWS.length, '夾具之scope須同時含範圍內外之列')
|
|
125
|
+
assert.deepStrictEqual(ids(await w.select({}, cs)), exp)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
for (let f of FINDS) {
|
|
129
|
+
|
|
130
|
+
it(`${f.label}: select命中 = 手推期望(scope AND find)`, async function() {
|
|
131
|
+
let exp = ids(ROWS.filter((r) => sc.inScope(r) && f.p(r)))
|
|
132
|
+
assert.deepStrictEqual(ids(await w.select(f.find(), cs)), exp)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
if (sc.delAllRejects) {
|
|
136
|
+
it(`${f.label}: delAll拒收(chkScopeForDelAll維持, 含Op.and之scope.where)且不刪除`, async function() {
|
|
137
|
+
let r = await w.delAll(f.find(), cs).then(() => null).catch((err) => msgOf(err))
|
|
138
|
+
assert.ok(r !== null && r.indexOf('defaultScope') >= 0, `未reject: ${r}`)
|
|
139
|
+
assert.deepStrictEqual(await unscoped(), ['1', '2', '3', '4'])
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
it(`${f.label}: delAll實際刪除之主鍵集合 = 手推期望, 不越出scope`, async function() {
|
|
144
|
+
let exp = ids(ROWS.filter((r) => sc.inScope(r) && f.p(r)))
|
|
145
|
+
let r = await w.delAll(f.find(), cs)
|
|
146
|
+
let left = await unscoped()
|
|
147
|
+
let gone = ['1', '2', '3', '4'].filter((v) => left.indexOf(v) < 0)
|
|
148
|
+
assert.deepStrictEqual(gone, exp, `delAll刪除之主鍵(scope範圍: ${ids(ROWS.filter(sc.inScope))})`)
|
|
149
|
+
assert.ok(gone.every((v) => sc.inScope(ROWS.find((x) => x.id === v))), 'delAll刪到scope之外')
|
|
150
|
+
assert.deepStrictEqual(r, { n: exp.length, nDeleted: exp.length, ok: 1 })
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
it('scope內之主鍵: selectByPk命中、del實際刪除該列(其餘列不動)、save更新該列完整欄位(外審sol F5)', async function() {
|
|
157
|
+
let inRows = ROWS.filter(sc.inScope)
|
|
158
|
+
let inId = inRows[0].id
|
|
159
|
+
let other = ['1', '2', '3', '4'].filter((v) => v !== inId)
|
|
160
|
+
assert.deepStrictEqual(await w.selectByPk(inId, cs), ROWS.find((r) => r.id === inId))
|
|
161
|
+
//save: 更新scope內之列
|
|
162
|
+
let rs = await w.save({ id: inId, g: inRows[0].g, name: 'zz' }, cs)
|
|
163
|
+
assert.deepStrictEqual(rs, [{ n: 1, nInserted: 0, nModified: 1, ok: 1 }])
|
|
164
|
+
assert.deepStrictEqual(await w.selectByPk(inId, cs), { id: inId, g: inRows[0].g, name: 'zz' })
|
|
165
|
+
//del: 刪除scope內之列
|
|
166
|
+
let rd = await w.del({ id: inId }, cs)
|
|
167
|
+
assert.deepStrictEqual(rd, [{ n: 1, nDeleted: 1, ok: 1 }])
|
|
168
|
+
assert.deepStrictEqual(await unscoped(), other, 'del只刪該列')
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('scope外已存在之主鍵: save(預設autoInsert:true)本筆失敗(ok:0, 預讀不見該列而改插入, 撞主鍵重試耗盡), 該列不改動(外審minimax F1; 修前literal/where scope下該列會被更新)', async function() {
|
|
172
|
+
let outRow = ROWS.find((r) => !sc.inScope(r))
|
|
173
|
+
let rs = await w.save({ id: outRow.id, g: outRow.g, name: 'zz' }, cs)
|
|
174
|
+
assert.strictEqual(rs.length, 1)
|
|
175
|
+
assert.strictEqual(rs[0].ok, 0, `scope外之既有主鍵save應失敗: ${JSON.stringify(rs)}`)
|
|
176
|
+
assert.strictEqual(rs[0].nModified, 0)
|
|
177
|
+
assert.strictEqual(rs[0].nInserted, 0)
|
|
178
|
+
assert.ok(String(rs[0].err).indexOf('can not save') >= 0, String(rs[0].err))
|
|
179
|
+
let row = (await ins.mds[cl].unscoped().findAll({ where: { id: outRow.id }, raw: true }))[0]
|
|
180
|
+
assert.strictEqual(row.name, outRow.name, 'scope外之列被改動')
|
|
181
|
+
assert.deepStrictEqual(await unscoped(), ['1', '2', '3', '4'])
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('scope外之主鍵: save(autoInsert:false)不改動該列(外審sol F5)', async function() {
|
|
185
|
+
let outRow = ROWS.find((r) => !sc.inScope(r))
|
|
186
|
+
let rs = await w.save({ id: outRow.id, g: outRow.g, name: 'zz' }, { ...cs, autoInsert: false })
|
|
187
|
+
assert.strictEqual(rs[0].nModified, 0, `save更新了scope外之列: ${JSON.stringify(rs)}`)
|
|
188
|
+
let row = (await ins.mds[cl].unscoped().findAll({ where: { id: outRow.id }, raw: true }))[0]
|
|
189
|
+
assert.strictEqual(row.name, outRow.name, 'scope外之列被改動')
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('selectByPk、del亦不越出scope(scope外之主鍵: selectByPk回null、del不刪)', async function() {
|
|
193
|
+
let outId = ROWS.find((r) => !sc.inScope(r)).id
|
|
194
|
+
let inId = ROWS.find((r) => sc.inScope(r)).id
|
|
195
|
+
assert.strictEqual(await w.selectByPk(outId, cs), null)
|
|
196
|
+
assert.ok((await w.selectByPk(inId, cs)) !== null)
|
|
197
|
+
let r = await w.del({ id: outId }, cs)
|
|
198
|
+
assert.deepStrictEqual(r, [{ n: 0, nDeleted: 0, ok: 1 }], 'del刪到scope外之列')
|
|
199
|
+
assert.deepStrictEqual(await unscoped(), ['1', '2', '3', '4'])
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
}
|
package/test/api-sqlite.test.mjs
CHANGED
|
@@ -2,6 +2,10 @@ 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'
|
|
6
|
+
import { defineLowerCaseSuites } from './api-lower-cases.mjs'
|
|
7
|
+
import { defineScopeSuites } from './api-scope.mjs'
|
|
8
|
+
import { defineFloatSuites } from './api-float.mjs'
|
|
5
9
|
|
|
6
10
|
|
|
7
11
|
//sqlite使用獨立暫存storage檔測試, 不使用專案根目錄之worm.sqlite
|
|
@@ -44,3 +48,7 @@ after(function() {
|
|
|
44
48
|
defineSuites('sqlite', getOpt)
|
|
45
49
|
defineWhitelistSuites('sqlite', getOpt)
|
|
46
50
|
defineDateWriteSuites('sqlite', getOpt)
|
|
51
|
+
defineWellformedSuites('sqlite', getOpt)
|
|
52
|
+
defineLowerCaseSuites('sqlite', getOpt)
|
|
53
|
+
defineScopeSuites('sqlite', getOpt)
|
|
54
|
+
defineFloatSuites('sqlite', getOpt)
|