w-orm-reladb 1.1.9 → 1.1.10
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 +59 -28
- package/dist/w-orm-reladb.umd.js +2 -2
- package/dist/w-orm-reladb.umd.js.map +1 -1
- package/docs/WOrmReladb.html +52 -30
- package/docs/WOrmReladb.mjs.html +59 -28
- package/docs/cvFind.mjs.html +127 -405
- package/docs/global.html +14 -11
- package/docs/importModels.mjs.html +1 -1
- package/docs/index.html +1 -1
- package/g-mssql-transaction-commit.mjs +1 -1
- package/g-mssql-transaction-rollback.mjs +1 -1
- package/g-mssql.mjs +4 -4
- package/g-sqlite-encryption-transaction-commit.mjs +1 -1
- package/g-sqlite-encryption.mjs +4 -4
- package/g-sqlite-transaction-commit.mjs +1 -1
- package/g-sqlite-transaction-rollback.mjs +1 -1
- package/g-sqlite.mjs +4 -4
- package/models/tfind.js +50 -0
- package/package.json +2 -2
- package/src/WOrmReladb.mjs +58 -27
- package/src/cvFind.mjs +126 -404
- package/test/api-find-whitelist.mjs +390 -0
- package/test/api-mssql.test.mjs +2 -0
- package/test/api-sqlite.test.mjs +2 -0
- package/test/api-suite.mjs +95 -136
- package/test/unit-cvfind.test.mjs +241 -218
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import assert from 'assert'
|
|
2
2
|
import Sequelize from 'sequelize'
|
|
3
|
-
import cvFind, { getColKind, normPk } from '../src/cvFind.mjs'
|
|
3
|
+
import cvFind, { getColKind, normPk, chkFindShape } from '../src/cvFind.mjs'
|
|
4
|
+
import cvOrmFind from 'wsemi/src/cvOrmFind.mjs'
|
|
5
|
+
import cvCases from 'wsemi/src/_cvOrmFindCases.mjs'
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
//cvFind為select與delAll共用之條件轉譯純函數, 本檔不連線資料庫
|
|
7
9
|
//斷言分兩類: 拒收(reject之訊息)與方言專屬之SQL片段(ESCAPE、N前綴、LOWER內之實際欄名);
|
|
8
10
|
//命中集合之語意由test/api-suite.mjs之find段於sqlite與mssql實際執行斷言, 本檔不以SQL之括號結構作斷言
|
|
11
|
+
//第33項批1(R11): 形狀層由wsemi之cvOrmFind單一擁有, 本檔之舊案中屬形狀層者改以其訊息斷言reject, 被移除之功能($not $nor $regex $exists、RegExp、Symbol Op、陣列值等)改為reject案;
|
|
9
12
|
//對應CLAUDE_rulebook.md之R1–R6與R8(normPk);「審計02」組為R3欄位引用不變式(斷言SQL含欄名及引用次數, 非括號結構)、R4欄名解析、R4 ctx.dateNoTz
|
|
10
13
|
|
|
11
14
|
|
|
@@ -98,55 +101,54 @@ let withTz = (tz, fn) => {
|
|
|
98
101
|
|
|
99
102
|
describe('cvFind', function() {
|
|
100
103
|
|
|
101
|
-
it('R1
|
|
102
|
-
for (let v of [undefined,
|
|
104
|
+
it('R1 頂層: 未給、undefined、{}、null原型空物件為全部; 非條件物件(null、陣列、純量、非一般物件等)一律reject', function() {
|
|
105
|
+
for (let v of [undefined, {}, Object.create(null)]) {
|
|
103
106
|
assert.deepStrictEqual(cvFind(v, LT.ctx), {})
|
|
104
107
|
}
|
|
105
|
-
assert.deepStrictEqual(cvFind({}, LT.ctx), {})
|
|
106
108
|
class Q {
|
|
107
109
|
constructor() {
|
|
108
110
|
this.s = 'a'
|
|
109
111
|
}
|
|
110
112
|
}
|
|
111
|
-
for (let v of [Object.create({ s: 'a' }), new Q(), new Date(), new Map([['s', 'a']]), /a/]) {
|
|
112
|
-
throwsMsg(() => cvFind(v, LT.ctx), 'find must be a plain object')
|
|
113
|
+
for (let v of [null, [], [{ id: 'a' }], 'a', 1, true, Object.create({ s: 'a' }), new Q(), new Date(), new Map([['s', 'a']]), /a/]) {
|
|
114
|
+
throwsMsg(() => cvFind(v, LT.ctx), 'invalid find: condition must be a plain object')
|
|
113
115
|
}
|
|
114
116
|
assert.ok(LT.where(Object.assign(Object.create(null), { s: 'a' })).indexOf('`s` = \'a\'') >= 0)
|
|
115
117
|
})
|
|
116
118
|
|
|
117
|
-
it('R2 欄名length不得使其他條件失效,
|
|
119
|
+
it('R2 欄名length不得使其他條件失效, 欄名含$不屬欄名文法而reject', function() {
|
|
118
120
|
let w = LT.where({ s: 'x', length: 0 })
|
|
119
121
|
assert.ok(w.indexOf('`s` = \'x\'') >= 0 && w.indexOf('`length` = 0') >= 0, w)
|
|
120
|
-
|
|
122
|
+
throwsMsg(() => cvFind({ 'p$x': 'q' }, LT.ctx), 'unsupported column name [p$x]')
|
|
121
123
|
})
|
|
122
124
|
|
|
123
|
-
it('R2 Symbol
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
throwsMsg(() => cvFind({
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
125
|
+
it('R2 Symbol鍵一律reject(sequelize之Op全集、非Op之Symbol、toStringTag皆然): 原Op支援已移除', function() {
|
|
126
|
+
for (let f of [{ i: { [Op.gt]: { x: 1 } } }, { [Op.like]: 'x' }, { [Symbol('or')]: [{ s: 'a' }] }, { i: { [Symbol('gt')]: 1 } }, { i: { [Op.or]: [{ [Op.eq]: 1 }], [Op.gt]: 10 } }, { i: { [Op.or]: [] } }, { i: { [Op.and]: 0 } }, { [Op.and]: [{ s: 'a' }] }, { i: { [Op.in]: [1] } }]) {
|
|
127
|
+
throwsMsg(() => cvFind(f, LT.ctx), 'symbol key is not allowed')
|
|
128
|
+
}
|
|
129
|
+
throwsMsg(() => cvFind({ s: 'a', [Symbol.toStringTag]: 'Q' }, LT.ctx), 'condition must be a plain object')
|
|
130
|
+
//Op全集 × 欄位層與文件層
|
|
131
|
+
let n = 0
|
|
132
|
+
for (let name of Object.keys(Op)) {
|
|
133
|
+
for (let f of [{ i: { [Op[name]]: 1 } }, { [Op[name]]: [{ i: 1 }] }, { i: { $gt: 1, [Op[name]]: 1 } }]) {
|
|
134
|
+
throwsMsg(() => cvFind(f, LT.ctx), 'symbol key is not allowed')
|
|
135
|
+
n++
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
assert.ok(n >= 90, `被檢核之案數過少: ${n}`)
|
|
135
139
|
})
|
|
136
140
|
|
|
137
|
-
it('R2
|
|
138
|
-
let w = LT.where({ i: { $gt: 5,
|
|
139
|
-
assert.ok(w.indexOf('`i` > 5') >= 0 && w.indexOf('`i`
|
|
140
|
-
let w2 = LT.where({ $and: [{ s: 'a' }], [Op.and]: [{ s: 'b' }] })
|
|
141
|
-
assert.ok(w2.indexOf('`s` = \'a\'') >= 0 && w2.indexOf('`s` = \'b\'') >= 0, w2)
|
|
141
|
+
it('R2 同欄多運算子併入同一欄位物件; 不可列舉之鍵reject', function() {
|
|
142
|
+
let w = LT.where({ i: { $gt: 5, $lt: 9 } })
|
|
143
|
+
assert.ok(w.indexOf('`i` > 5') >= 0 && w.indexOf('`i` < 9') >= 0, w)
|
|
142
144
|
let f = {}
|
|
143
145
|
Object.defineProperty(f, 's', { value: 'a', enumerable: false })
|
|
144
|
-
|
|
146
|
+
throwsMsg(() => cvFind(f, LT.ctx), 'non-enumerable property is not allowed')
|
|
145
147
|
})
|
|
146
148
|
|
|
147
149
|
it('R2 欄名__proto__與空字串拒收', function() {
|
|
148
|
-
throwsMsg(() => cvFind(JSON.parse('{"__proto__":1}'), LT.ctx), '
|
|
149
|
-
throwsMsg(() => cvFind({ '': 1 }, LT.ctx), '
|
|
150
|
+
throwsMsg(() => cvFind(JSON.parse('{"__proto__":1}'), LT.ctx), 'unsupported column name [__proto__]')
|
|
151
|
+
throwsMsg(() => cvFind({ '': 1 }, LT.ctx), 'unsupported column name []')
|
|
150
152
|
})
|
|
151
153
|
|
|
152
154
|
it('R3 不支援之運算子一律拒收', function() {
|
|
@@ -159,37 +161,39 @@ describe('cvFind', function() {
|
|
|
159
161
|
}
|
|
160
162
|
})
|
|
161
163
|
|
|
162
|
-
it('R3 邏輯運算子須為非空之物件陣列,
|
|
163
|
-
for (let op of ['$and', '$or'
|
|
164
|
-
throwsMsg(() => cvFind({ [op]: [] }, LT.ctx),
|
|
165
|
-
throwsMsg(() => cvFind({ [op]: { s: 1 } }, LT.ctx), '
|
|
166
|
-
throwsMsg(() => cvFind({ [op]: ['x'] }, LT.ctx), 'condition must be
|
|
167
|
-
throwsMsg(() => cvFind({ [op]: new Array(2) }, LT.ctx), '
|
|
164
|
+
it('R3 邏輯運算子須為非空之物件陣列, 陣列不得有洞, 元素不得為空條件; $nor、$exists已移除', function() {
|
|
165
|
+
for (let op of ['$and', '$or']) {
|
|
166
|
+
throwsMsg(() => cvFind({ [op]: [] }, LT.ctx), `${op} with empty array`)
|
|
167
|
+
throwsMsg(() => cvFind({ [op]: { s: 1 } }, LT.ctx), 'value must be an array')
|
|
168
|
+
throwsMsg(() => cvFind({ [op]: ['x'] }, LT.ctx), 'condition must be a plain object')
|
|
169
|
+
throwsMsg(() => cvFind({ [op]: new Array(2) }, LT.ctx), 'sparse array hole')
|
|
170
|
+
throwsMsg(() => cvFind({ [op]: [{}] }, LT.ctx), 'empty condition')
|
|
168
171
|
}
|
|
169
|
-
throwsMsg(() => cvFind({ [
|
|
170
|
-
throwsMsg(() => cvFind({
|
|
171
|
-
throwsMsg(() => cvFind({ i: { $
|
|
172
|
+
throwsMsg(() => cvFind({ $nor: [{ s: 'a' }] }, LT.ctx), 'unsupported operator [$nor]')
|
|
173
|
+
throwsMsg(() => cvFind({ $nor: [] }, LT.ctx), 'unsupported operator [$nor]')
|
|
174
|
+
throwsMsg(() => cvFind({ i: { $in: [1, undefined] } }, LT.ctx), 'undefined is not allowed')
|
|
175
|
+
throwsMsg(() => cvFind({ i: { $exists: true } }, LT.ctx), 'unsupported operator [$exists]')
|
|
172
176
|
})
|
|
173
177
|
|
|
174
178
|
it('R4 非純量值不得交sequelize之型別stringify', function() {
|
|
175
179
|
let bads = [
|
|
176
|
-
[{ d: { $lt: { x: 1 } } }, 'value
|
|
177
|
-
[{ f: { $gt: [1] } }, 'value
|
|
178
|
-
[{ i: { $in: [{}] } }, 'value
|
|
179
|
-
[{ i: NaN }, 'number
|
|
180
|
-
[{ i: Infinity }, 'number
|
|
180
|
+
[{ d: { $lt: { x: 1 } } }, 'unsupported value type'],
|
|
181
|
+
[{ f: { $gt: [1] } }, 'unsupported value type [array]'],
|
|
182
|
+
[{ i: { $in: [{}] } }, 'unsupported value type'],
|
|
183
|
+
[{ i: NaN }, 'number is not finite'],
|
|
184
|
+
[{ i: Infinity }, 'number is not finite'],
|
|
181
185
|
[{ d: new Date('x') }, 'invalid Date'],
|
|
182
|
-
[{ i: 10n }, 'value
|
|
183
|
-
[{ s: () => 1 }, 'value
|
|
184
|
-
[{ s: Symbol('x') }, 'value
|
|
185
|
-
[{ s: new Map() }, 'value
|
|
186
|
-
[{ s: undefined }, '
|
|
186
|
+
[{ i: 10n }, 'unsupported value type'],
|
|
187
|
+
[{ s: () => 1 }, 'unsupported value type'],
|
|
188
|
+
[{ s: Symbol('x') }, 'unsupported value type'],
|
|
189
|
+
[{ s: new Map() }, 'unsupported value type'],
|
|
190
|
+
[{ s: undefined }, 'undefined is not allowed'],
|
|
187
191
|
[{ i: {} }, 'empty operator object'],
|
|
188
|
-
[{ s: { a: 1 } }, '
|
|
192
|
+
[{ s: { a: 1 } }, 'unexpected key'],
|
|
189
193
|
[{ i: { $gt: null } }, 'null is not allowed'],
|
|
190
|
-
[{ i: { $in: 1 } }, 'must be an array'],
|
|
191
|
-
[{ i: { $not: [1] } }, '
|
|
192
|
-
[{ i: { $not: {} } }, '
|
|
194
|
+
[{ i: { $in: 1 } }, 'value must be an array'],
|
|
195
|
+
[{ i: { $not: [1] } }, 'unsupported operator [$not]'],
|
|
196
|
+
[{ i: { $not: {} } }, 'unsupported operator [$not]'],
|
|
193
197
|
]
|
|
194
198
|
for (let [f, msg] of bads) {
|
|
195
199
|
throwsMsg(() => cvFind(f, LT.ctx), msg)
|
|
@@ -229,8 +233,8 @@ describe('cvFind', function() {
|
|
|
229
233
|
for (let [f, msg] of bads) {
|
|
230
234
|
throwsMsg(() => cvFind(f, LT.ctx), `does not match column type ${msg}`)
|
|
231
235
|
}
|
|
232
|
-
//Buffer
|
|
233
|
-
throwsMsg(() => cvFind({ s: Buffer.from('a') }, LT.ctx), 'value
|
|
236
|
+
//Buffer不屬白名單純量(第33項批1: 原僅用於未分類型別之欄)
|
|
237
|
+
throwsMsg(() => cvFind({ s: Buffer.from('a') }, LT.ctx), 'unsupported value type')
|
|
234
238
|
//相符者放行
|
|
235
239
|
for (let f of [{ i: '2' }, { i: '-25' }, { f: '-2.5' }, { dec: '.5' }, { dec: '1.25' }, { dec: 1.254 }, { d: 253402300799999 }, { big: '9007199254740993' }, { p: 2.5 }, { d: { $lt: '2020-01-01T00:00:00Z' } }, { d: { $lt: '2020-01-01 08:00:00+08:00' } }, { d: { $lt: 1577836800000 } }, { do: '2020-01-01' }, { b: 1 }]) {
|
|
236
240
|
assert.doesNotThrow(() => cvFind(f, LT.ctx), JSON.stringify(f))
|
|
@@ -243,43 +247,14 @@ describe('cvFind', function() {
|
|
|
243
247
|
throwsMsg(() => cvFind(o, LT.ctx), 'too deep')
|
|
244
248
|
})
|
|
245
249
|
|
|
246
|
-
it('R6 $regex
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
})
|
|
255
|
-
|
|
256
|
-
it('R6 $regex只收字面子字串, 旗標只收i', function() {
|
|
257
|
-
for (let p of ['^pe', 'a.c', 'a*', '[a]', 'a|b', '(a)', 'a{2}', 'a?', 'a+', 'a$', '\\d', 'a/b', 'a\\']) {
|
|
258
|
-
throwsMsg(() => cvFind({ t: { $regex: p } }, LT.ctx), '$regex supports literal substrings only')
|
|
259
|
-
}
|
|
260
|
-
throwsMsg(() => cvFind({ t: { $regex: 'a' + String.fromCharCode(0) + 'x' } }, LT.ctx), 'NUL')
|
|
261
|
-
throwsMsg(() => cvFind({ t: { $regex: 'a', $options: 'm' } }, LT.ctx), 'supports [i] only')
|
|
262
|
-
throwsMsg(() => cvFind({ t: { $regex: /a/m } }, LT.ctx), 'supports [i] only')
|
|
263
|
-
throwsMsg(() => cvFind({ t: { $regex: /a/, $options: 'i' } }, LT.ctx), '$options can not be used with a RegExp')
|
|
264
|
-
throwsMsg(() => cvFind({ t: { $options: 'i' } }, LT.ctx), '$options requires $regex')
|
|
265
|
-
throwsMsg(() => cvFind({ t: { $regex: 1 } }, LT.ctx), '$regex must be a string or RegExp')
|
|
266
|
-
for (let c of ['i', 'f', 'p', 'dec', 'b', 'd']) {
|
|
267
|
-
throwsMsg(() => cvFind({ [c]: { $regex: '1' } }, LT.ctx), '$regex requires a string column')
|
|
268
|
-
}
|
|
269
|
-
//同義寫法產生相同之條件
|
|
270
|
-
let wi = LT.where({ t: { $regex: 'PeT', $options: 'i' } })
|
|
271
|
-
assert.ok(wi.indexOf('LOWER(`t`) LIKE LOWER(\'%PeT%\') ESCAPE \'!\'') >= 0, wi)
|
|
272
|
-
assert.strictEqual(LT.where({ t: { $regex: 'PeT', $options: '$i' } }), wi)
|
|
273
|
-
assert.strictEqual(LT.where({ t: { $regex: /PeT/i } }), wi)
|
|
274
|
-
assert.strictEqual(LT.where({ t: /PeT/i }), wi)
|
|
275
|
-
assert.strictEqual(LT.where({ t: { $regex: new RegExp('') } }), LT.where({ t: { $regex: '' } }))
|
|
276
|
-
})
|
|
277
|
-
|
|
278
|
-
it('R6 給i時LOWER內為實際欄名; sqlite下含非ASCII大小寫字母者拒收', function() {
|
|
279
|
-
assert.ok(LT.where({ title: { $regex: 'a', $options: 'i' } }).indexOf('LOWER(`physical_title`)') >= 0)
|
|
280
|
-
throwsMsg(() => cvFind({ t: { $regex: 'é', $options: 'i' } }, LT.ctx), 'ASCII letters only')
|
|
281
|
-
assert.doesNotThrow(() => cvFind({ t: { $regex: '中', $options: 'i' } }, LT.ctx))
|
|
282
|
-
assert.ok(MS.where({ t: { $regex: 'é', $options: 'i' } }).indexOf('LOWER([t]) LIKE LOWER(N\'%é%\')') >= 0)
|
|
250
|
+
it('R6 $regex/$options/RegExp已移除(第33項批1): 一律reject, 不再有LIKE轉譯', function() {
|
|
251
|
+
for (let M of [LT, MS]) {
|
|
252
|
+
for (let f of [{ t: { $regex: 'a' } }, { t: { $regex: 'a', $options: 'i' } }, { t: { $regex: /a/ } }, { t: { $options: 'i' } }, { t: { $not: { $regex: 'a' } } }, { i: { $regex: '1' } }]) {
|
|
253
|
+
throwsMsg(() => cvFind(f, M.ctx), 'unsupported operator')
|
|
254
|
+
}
|
|
255
|
+
throwsMsg(() => cvFind({ t: /a/i }, M.ctx), 'unsupported value type [RegExp]')
|
|
256
|
+
throwsMsg(() => cvFind({ t: { $in: [/a/] } }, M.ctx), 'unsupported value type [RegExp]')
|
|
257
|
+
}
|
|
283
258
|
})
|
|
284
259
|
|
|
285
260
|
it('getColKind 依sequelize型別類別分類', function() {
|
|
@@ -303,11 +278,9 @@ describe('cvFind', function() {
|
|
|
303
278
|
let pairs = [
|
|
304
279
|
[{ id: 123 }, { id: '123' }],
|
|
305
280
|
[{ id: { $in: [123, '45'] } }, { id: { $in: ['123', '45'] } }],
|
|
306
|
-
[{ id: [1.5] }, { id: ['1.5'] }],
|
|
281
|
+
[{ id: { $nin: [1.5] } }, { id: { $nin: ['1.5'] } }],
|
|
307
282
|
[{ id: { $gt: 2, $lte: 9 } }, { id: { $gt: '2', $lte: '9' } }],
|
|
308
|
-
[{ id: { $
|
|
309
|
-
[{ id: { [Op.in]: [7] } }, { id: { [Op.in]: ['7'] } }],
|
|
310
|
-
[{ id: { [Op.between]: [1, 2] } }, { id: { [Op.between]: ['1', '2'] } }],
|
|
283
|
+
[{ id: { $ne: 7 } }, { id: { $ne: '7' } }],
|
|
311
284
|
]
|
|
312
285
|
for (let [a, b] of pairs) {
|
|
313
286
|
assert.strictEqual(LT.where(a, { pk: 'id' }), LT.where(b, { pk: 'id' }))
|
|
@@ -315,9 +288,8 @@ describe('cvFind', function() {
|
|
|
315
288
|
}
|
|
316
289
|
assert.ok(LT.where({ id: 123 }, { pk: 'id' }).indexOf('`id` = \'123\'') >= 0)
|
|
317
290
|
assert.ok(MS.where({ id: 123 }, { pk: 'id' }).indexOf('[id] = N\'123\'') >= 0)
|
|
318
|
-
//null依R5
|
|
291
|
+
//null依R5
|
|
319
292
|
assert.ok(LT.where({ id: null }, { pk: 'id' }).indexOf('`id` IS NULL') >= 0)
|
|
320
|
-
assert.ok(LT.where({ id: { $regex: '12' } }, { pk: 'id' }).indexOf('LIKE \'%12%\'') >= 0)
|
|
321
293
|
//整數類主鍵欄: 標準整數寫法字串即數值, 超出安全整數者保留字串
|
|
322
294
|
assert.strictEqual(LT.where({ i: '12' }, { pk: 'i' }), LT.where({ i: 12 }, { pk: 'i' }))
|
|
323
295
|
assert.strictEqual(LT.where({ i: { $in: ['-3', 5] } }, { pk: 'i' }), LT.where({ i: { $in: [-3, 5] } }, { pk: 'i' }))
|
|
@@ -366,20 +338,11 @@ describe('cvFind 審計02', function() {
|
|
|
366
338
|
//空集合之各入口(欄名以f代入)
|
|
367
339
|
let emptyFinds = (f) => [
|
|
368
340
|
['$in:[]', { [f]: { $in: [] } }],
|
|
369
|
-
['$
|
|
370
|
-
['
|
|
371
|
-
['
|
|
372
|
-
['Op.notIn:[]', { [f]: { [Op.notIn]: [] } }],
|
|
373
|
-
['$not:{$in:[]}', { [f]: { $not: { $in: [] } } }],
|
|
374
|
-
['$not:{$nin:[]}', { [f]: { $not: { $nin: [] } } }],
|
|
375
|
-
['Op.not:{Op.notIn:[]}', { [f]: { [Op.not]: { [Op.notIn]: [] } } }],
|
|
376
|
-
['$not:{$not:{$nin:[]}}', { [f]: { $not: { $not: { $nin: [] } } } }],
|
|
377
|
-
['$in:[]+$nin:[]', { [f]: { $in: [], $nin: [] } }],
|
|
378
|
-
['$or含空$nin', { $or: [{ [f]: { $nin: [] } }, { id: 'zz' }] }],
|
|
341
|
+
['$in:[]+$nin非空', { [f]: { $in: [], $nin: ['x'] } }],
|
|
342
|
+
['$in:[]+比較運算子', { [f]: { $in: [], $gt: 'a' } }],
|
|
343
|
+
['$or含空$in', { $or: [{ [f]: { $in: [] } }, { id: 'zz' }] }],
|
|
379
344
|
['$and含空$in', { $and: [{ [f]: { $in: [] } }, { id: 'zz' }] }],
|
|
380
|
-
['$
|
|
381
|
-
['Op.or含空$in', { [Op.or]: [{ [f]: { $in: [] } }] }],
|
|
382
|
-
['文件層Op.not包空$in', { [Op.not]: { [f]: { $in: [] } } }],
|
|
345
|
+
['$or內$and含空$in', { $or: [{ $and: [{ [f]: { $in: [] } }, { id: 'zz' }] }] }],
|
|
383
346
|
]
|
|
384
347
|
|
|
385
348
|
it('[R3][最低要求②] 空集合之各入口產生之SQL皆引用其欄位, 使資料庫得以回報不存在之欄(attrs已給之欄與未給attrs之任意欄)', function() {
|
|
@@ -410,38 +373,36 @@ describe('cvFind 審計02', function() {
|
|
|
410
373
|
}
|
|
411
374
|
})
|
|
412
375
|
|
|
413
|
-
it('[R3][最低要求②]
|
|
414
|
-
let vals = [[], [1, 2], null, 1, 'x', '', true]
|
|
376
|
+
it('[R3][最低要求②] 白名單內之運算子 × 各類值(未給attrs): 凡未被拒收者其SQL皆引用其欄位(防日後新增分支再犯); sequelize之Op Symbol一律於形狀階段reject', function() {
|
|
377
|
+
let vals = [[], [1, 2], ['x'], null, 1, 'x', '', true, false, new Date(0)]
|
|
415
378
|
let n = 0
|
|
416
379
|
for (let M of [LT, MS]) {
|
|
417
|
-
for (let
|
|
418
|
-
let sym = Op[name]
|
|
419
|
-
if (sym === Op.and || sym === Op.or) {
|
|
420
|
-
continue //欄位層已拒收(R2)
|
|
421
|
-
}
|
|
380
|
+
for (let op of ['$eq', '$ne', '$gt', '$gte', '$lt', '$lte', '$in', '$nin']) {
|
|
422
381
|
for (let v of vals) {
|
|
423
382
|
let w = null
|
|
424
383
|
try {
|
|
425
|
-
w = M.where0({ nocol: { [
|
|
384
|
+
w = M.where0({ nocol: { [op]: v } })
|
|
426
385
|
}
|
|
427
386
|
catch (e) {
|
|
428
|
-
continue
|
|
387
|
+
continue //拒收者不產生SQL
|
|
429
388
|
}
|
|
430
389
|
n++
|
|
431
|
-
assert.ok(w.indexOf(M.q('nocol')) >= 0,
|
|
390
|
+
assert.ok(w.indexOf(M.q('nocol')) >= 0, `${op} × ${JSON.stringify(v)}: SQL未引用欄位: ${w}`)
|
|
432
391
|
}
|
|
433
392
|
}
|
|
393
|
+
for (let name of Object.keys(Op)) {
|
|
394
|
+
throwsMsg(() => M.where0({ nocol: { [Op[name]]: 1 } }), 'symbol key is not allowed')
|
|
395
|
+
}
|
|
434
396
|
}
|
|
435
|
-
assert.ok(n >
|
|
397
|
+
assert.ok(n > 20, `被檢核之案數過少: ${n}`)
|
|
436
398
|
})
|
|
437
399
|
|
|
438
400
|
it('[R5] 空集合之欄位只引用一次: mssql驅動遇同一語句≥2個錯誤時err.message為空字串', function() {
|
|
439
|
-
//R5: $in:[]以f IS NULL AND 1 = 0
|
|
401
|
+
//R5: $in:[]以f IS NULL AND 1 = 0表示, 不用f IS NULL AND f IS NOT NULL之雙引用形式($nin:[]已於形狀階段reject)
|
|
440
402
|
for (let M of [LT, MS]) {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}
|
|
403
|
+
let w = M.where({ s: { $in: [] } })
|
|
404
|
+
assert.strictEqual(w.split(M.q('s')).length - 1, 1, `$in:[]: 欄位引用次數不為1: ${w}`)
|
|
405
|
+
throwsMsg(() => M.where({ s: { $nin: [] } }), '$nin with empty array is always true')
|
|
445
406
|
}
|
|
446
407
|
})
|
|
447
408
|
|
|
@@ -458,7 +419,7 @@ describe('cvFind 審計02', function() {
|
|
|
458
419
|
|
|
459
420
|
it('[R5] 同一find含多個空集合時, 各空集合各引用其欄位一次(欄名自然出現多次)', function() {
|
|
460
421
|
for (let M of [LT, MS]) {
|
|
461
|
-
let w = M.where({ s: { $in: [], $
|
|
422
|
+
let w = M.where({ $or: [{ s: { $in: [] } }, { s: { $in: [] } }] })
|
|
462
423
|
assert.strictEqual(w.split(M.q('s')).length - 1, 2, w)
|
|
463
424
|
}
|
|
464
425
|
})
|
|
@@ -478,13 +439,16 @@ describe('cvFind 審計02', function() {
|
|
|
478
439
|
assert.doesNotThrow(() => cvFind({ i: { $lt: 5 } }, M.ctx))
|
|
479
440
|
assert.doesNotThrow(() => cvFind({ physical_title: 'x' }, M.ctx))
|
|
480
441
|
//其餘一律reject: ASCII大小寫變體、全形、尾端空白(含全形空白)、可忽略字元、mssql等價字元、attrs未列之欄、sqlite隱含欄
|
|
442
|
+
//(第33項批1: 含空白、可忽略字元等不屬欄名文法者於形狀階段即reject為unsupported column name; 符合文法者〔字母、數字、底線〕由嚴格鍵reject為unknown column name)
|
|
481
443
|
for (let k of ['I', 'F', 'D', 'PHYSICAL_TITLE', 'TITLE', 'Title', 'i', 'i ', `i${ws}`, `i${fe}`, `${ip}`, ` i`, 'nocol', 'rowid', 'oid', '_rowid_']) {
|
|
482
|
-
|
|
483
|
-
throwsMsg(() => cvFind({ [k]:
|
|
484
|
-
throwsMsg(() => cvFind({ [k]:
|
|
485
|
-
throwsMsg(() => cvFind({ [k]: { $
|
|
486
|
-
throwsMsg(() => cvFind({
|
|
444
|
+
let pre = /^[\p{L}_][\p{L}\p{M}\p{N}_]*$/u.test(k) ? 'unknown column name' : 'unsupported column name'
|
|
445
|
+
throwsMsg(() => cvFind({ [k]: { $lt: 'abc' } }, M.ctx), pre)
|
|
446
|
+
throwsMsg(() => cvFind({ [k]: 1 }, M.ctx), pre)
|
|
447
|
+
throwsMsg(() => cvFind({ [k]: { $in: [] } }, M.ctx), pre)
|
|
448
|
+
throwsMsg(() => cvFind({ [k]: { $nin: [1] } }, M.ctx), pre)
|
|
449
|
+
throwsMsg(() => cvFind({ $or: [{ s: 'a' }, { [k]: 1 }] }, M.ctx), pre)
|
|
487
450
|
}
|
|
451
|
+
throwsMsg(() => cvFind({ I: { $lt: 'abc' } }, M.ctx), 'unknown column name [I]')
|
|
488
452
|
}
|
|
489
453
|
})
|
|
490
454
|
|
|
@@ -596,8 +560,9 @@ describe('cvFind 審計02', function() {
|
|
|
596
560
|
throwsMsg(() => cvFind({ ID_COL: '012' }, cx), '[int]')
|
|
597
561
|
assert.ok(M.where({ ID_COL: '12' }, { pk: 'id' }).indexOf('`ID_COL` = 12') >= 0)
|
|
598
562
|
for (let k of ['ID', 'Id', 'id_col', 'ID_COL ']) {
|
|
599
|
-
|
|
600
|
-
throwsMsg(() => cvFind({ [k]: '
|
|
563
|
+
let pre = k === 'ID_COL ' ? 'unsupported column name' : 'unknown column name' //尾端空白不屬欄名文法, 於形狀階段reject
|
|
564
|
+
throwsMsg(() => cvFind({ [k]: '012' }, cx), pre)
|
|
565
|
+
throwsMsg(() => cvFind({ [k]: '12' }, cx), pre)
|
|
601
566
|
}
|
|
602
567
|
})
|
|
603
568
|
|
|
@@ -608,22 +573,23 @@ describe('cvFind 審計02', function() {
|
|
|
608
573
|
//使用者之鍵(attrs已給: 非完全相符之此類鍵亦為unsupported而非unknown, 以免依賴其是否碰巧解析)
|
|
609
574
|
let M = mk(dl)
|
|
610
575
|
//($開頭之鍵為運算子、__proto__為R2之invalid column name, 不在此)
|
|
576
|
+
//(訊息中之方括號為避免偽造路徑段而以?取代, 故只比對前綴)
|
|
611
577
|
for (let k of ['val`ue', 'i"d', 'i\'d', 'i[d]', 'i]', 'a.b', 'constructor', 'valueOf', 'hasOwnProperty']) {
|
|
612
|
-
throwsMsg(() => cvFind({ [k]: { $lt: 'abc' } }, M.ctx),
|
|
613
|
-
throwsMsg(() => cvFind({ [k]: '012' }, { ...M.ctx, pk: 'i' }),
|
|
578
|
+
throwsMsg(() => cvFind({ [k]: { $lt: 'abc' } }, M.ctx), 'unsupported column name')
|
|
579
|
+
throwsMsg(() => cvFind({ [k]: '012' }, { ...M.ctx, pk: 'i' }), 'unsupported column name')
|
|
614
580
|
}
|
|
615
581
|
//屬性名安全, 但其映射後之資料庫欄名不安全(model之field為此類名稱, 如既有資料表之欄)
|
|
616
582
|
for (let fd of ['meta.a', '$id$', 'a`b', 'x[y]', '__proto__', 'constructor', 'valueOf', 't.name', 'it\'s']) {
|
|
617
583
|
let M2 = mk(dl, { id: pkStr, v: { type: Sequelize.INTEGER, field: fd }, at: { type: Sequelize.DATE, field: fd === 'valueOf' ? fd : 'dt' } })
|
|
618
584
|
throwsMsg(() => cvFind({ v: 1 }, M2.ctx), `unsupported column name [${fd}]`)
|
|
619
|
-
throwsMsg(() => cvFind({ v: { $
|
|
585
|
+
throwsMsg(() => cvFind({ v: { $gt: 1 } }, M2.ctx), `unsupported column name [${fd}]`)
|
|
620
586
|
}
|
|
621
587
|
//DATE欄之欄名為valueOf: 以屬性名查亦reject(sequelize查到Object.prototype.valueOf而不依欄型序列化)
|
|
622
588
|
let M3 = mk(dl, { id: pkStr, ts: { type: Sequelize.DATE, field: 'valueOf' } })
|
|
623
589
|
throwsMsg(() => cvFind({ ts: { $gt: 1893456000000 } }, M3.ctx), 'unsupported column name [valueOf]')
|
|
624
590
|
throwsMsg(() => cvFind({ valueOf: { $gt: 1893456000000 } }, M3.ctx), 'unsupported column name [valueOf]')
|
|
625
|
-
|
|
626
|
-
|
|
591
|
+
//含$者不屬欄名文法(第33項批1: p$x原為既有model之欄而可查詢, 現於形狀階段reject)
|
|
592
|
+
throwsMsg(() => cvFind({ 'p$x': 'a' }, M.ctx), 'unsupported column name [p$x]')
|
|
627
593
|
}
|
|
628
594
|
})
|
|
629
595
|
|
|
@@ -641,14 +607,13 @@ describe('cvFind 審計02', function() {
|
|
|
641
607
|
}
|
|
642
608
|
})
|
|
643
609
|
|
|
644
|
-
it('[R4]
|
|
610
|
+
it('[R4] 欄位運算子物件須至少一個運算子: 僅有值為undefined之$options、空物件皆reject(原條件消失而成恆真, delAll刪全表); $options已移除', function() {
|
|
645
611
|
for (let M of [LT, MS]) {
|
|
646
|
-
throwsMsg(() => cvFind({ s: { $options: undefined } }, M.ctx), 'operator
|
|
647
|
-
throwsMsg(() => cvFind({ $or: [{ s: { $options: undefined } }, { id: 'a' }] }, M.ctx), 'operator
|
|
648
|
-
throwsMsg(() => cvFind({ s: { $
|
|
649
|
-
throwsMsg(() => cvFind({ s: {
|
|
650
|
-
|
|
651
|
-
assert.doesNotThrow(() => cvFind({ s: { $regex: 'a', $options: undefined } }, M.ctx))
|
|
612
|
+
throwsMsg(() => cvFind({ s: { $options: undefined } }, M.ctx), 'unsupported operator [$options]')
|
|
613
|
+
throwsMsg(() => cvFind({ $or: [{ s: { $options: undefined } }, { id: 'a' }] }, M.ctx), 'unsupported operator [$options]')
|
|
614
|
+
throwsMsg(() => cvFind({ s: { $options: 'i' } }, M.ctx), 'unsupported operator [$options]')
|
|
615
|
+
throwsMsg(() => cvFind({ s: {} }, M.ctx), 'empty operator object')
|
|
616
|
+
throwsMsg(() => cvFind({ $or: [{ s: {} }, { id: 'a' }] }, M.ctx), 'empty operator object')
|
|
652
617
|
}
|
|
653
618
|
})
|
|
654
619
|
|
|
@@ -665,9 +630,9 @@ describe('cvFind 審計02', function() {
|
|
|
665
630
|
for (let M of [LT, MS]) {
|
|
666
631
|
for (let find of [
|
|
667
632
|
{ title: { $gt: 'a', $lt: 'z' }, physical_title: { $ne: 'q' } },
|
|
668
|
-
{ title: { $in: ['a'
|
|
669
|
-
{ $or: [{ i: 1 }, { i: { $
|
|
670
|
-
{ d: {
|
|
633
|
+
{ title: { $in: ['a'], $nin: ['b'], $eq: null } },
|
|
634
|
+
{ $or: [{ i: 1 }, { i: { $in: [] } }], s: { $in: [] } },
|
|
635
|
+
{ d: { $gte: new Date(0), $lte: new Date(1), $gt: 1 } },
|
|
671
636
|
]) {
|
|
672
637
|
let acc = []
|
|
673
638
|
walk(cvFind(find, M.ctx), acc)
|
|
@@ -698,7 +663,7 @@ describe('cvFind 審計02', function() {
|
|
|
698
663
|
})
|
|
699
664
|
|
|
700
665
|
it('[R4 DATE] 通過檢核之字串與epoch一律轉為Date送出: 與直接給同一瞬間之Date之SQL相同(令DATE值之SQL與sequelize之欄位查找無關)', function() {
|
|
701
|
-
//各入口(欄位直給、$eq…$lte、$in/$nin
|
|
666
|
+
//各入口(欄位直給、$eq…$lte、$in/$nin、邏輯結合)皆須經同一轉換, 任一入口漏轉則該入口之字串/epoch與Date之SQL不同; 預期之Date由同一瞬間獨立給出
|
|
702
667
|
let entries = [
|
|
703
668
|
['等值', (k, v) => ({ [k]: v })],
|
|
704
669
|
['$eq', (k, v) => ({ [k]: { $eq: v } })],
|
|
@@ -709,16 +674,8 @@ describe('cvFind 審計02', function() {
|
|
|
709
674
|
['$lte', (k, v) => ({ [k]: { $lte: v } })],
|
|
710
675
|
['$in', (k, v) => ({ [k]: { $in: [v, v] } })],
|
|
711
676
|
['$nin', (k, v) => ({ [k]: { $nin: [v] } })],
|
|
712
|
-
['陣列值', (k, v) => ({ [k]: [v] })],
|
|
713
|
-
['$not.$gt', (k, v) => ({ [k]: { $not: { $gt: v } } })],
|
|
714
|
-
['$not.$in', (k, v) => ({ [k]: { $not: { $in: [v] } } })],
|
|
715
|
-
['Op.eq', (k, v) => ({ [k]: { [Op.eq]: v } })],
|
|
716
|
-
['Op.gt', (k, v) => ({ [k]: { [Op.gt]: v } })],
|
|
717
|
-
['Op.in', (k, v) => ({ [k]: { [Op.in]: [v] } })],
|
|
718
|
-
['Op.notIn', (k, v) => ({ [k]: { [Op.notIn]: [v] } })],
|
|
719
|
-
['Op.between', (k, v) => ({ [k]: { [Op.between]: [v, v] } })],
|
|
720
|
-
['Op.not.Op.gt', (k, v) => ({ [k]: { [Op.not]: { [Op.gt]: v } } })],
|
|
721
677
|
['$or內', (k, v) => ({ $or: [{ s: 'a' }, { [k]: v }] })],
|
|
678
|
+
['$and內$or', (k, v) => ({ $and: [{ $or: [{ s: 'a' }, { [k]: { $lt: v } }] }] })],
|
|
722
679
|
]
|
|
723
680
|
let t2019 = new Date('2019-01-01T00:00:00Z')
|
|
724
681
|
let t2030 = new Date(1893456000000)
|
|
@@ -807,14 +764,6 @@ describe('cvFind 審計02', function() {
|
|
|
807
764
|
['$ne', (v) => ({ d: { $ne: v } })],
|
|
808
765
|
['$in', (v) => ({ d: { $in: [v] } })],
|
|
809
766
|
['$nin', (v) => ({ d: { $nin: [v] } })],
|
|
810
|
-
['陣列值', (v) => ({ d: [v] })],
|
|
811
|
-
['$not.$gt', (v) => ({ d: { $not: { $gt: v } } })],
|
|
812
|
-
['$not.$in', (v) => ({ d: { $not: { $in: [v] } } })],
|
|
813
|
-
['Op.between', (v) => ({ d: { [Op.between]: [v, v] } })],
|
|
814
|
-
['Op.eq', (v) => ({ d: { [Op.eq]: v } })],
|
|
815
|
-
['Op.in', (v) => ({ d: { [Op.in]: [v] } })],
|
|
816
|
-
['Op.notIn', (v) => ({ d: { [Op.notIn]: [v] } })],
|
|
817
|
-
['Op.not.Op.gt', (v) => ({ d: { [Op.not]: { [Op.gt]: v } } })],
|
|
818
767
|
['$or內', (v) => ({ $or: [{ s: 'a' }, { d: v }] })],
|
|
819
768
|
]) {
|
|
820
769
|
assert.strictEqual(M.where(mkFind(s), { dateNoTz: 'local' }), M.where(mkFind(dt)), `${tz} ${lb} ${s}`)
|
|
@@ -883,7 +832,7 @@ describe('cvFind 審計02', function() {
|
|
|
883
832
|
|
|
884
833
|
//撞名: 屬性a之欄名b恰為屬性b之名稱(sequelize把鍵a映射為b後以rawAttributes.b之型別序列化: DATE值被INTEGER之NUMBER._stringify轉成Date.toString文字, sqlite以文字比較而命中全部非NULL列)
|
|
885
834
|
let M1 = M({ a: { type: Sequelize.DATE, field: 'b' }, b: { type: Sequelize.INTEGER, field: 'c' } })
|
|
886
|
-
for (let f of [{ a: { $lt: '2026-01-01T00:00:00Z' } }, { a: { $gt: 1893456000000 } }, { a: new Date(0) }, { a: { $in: ['2026-01-01T00:00:00Z'] } }, { a: { $
|
|
835
|
+
for (let f of [{ a: { $lt: '2026-01-01T00:00:00Z' } }, { a: { $gt: 1893456000000 } }, { a: new Date(0) }, { a: { $in: ['2026-01-01T00:00:00Z'] } }, { a: { $nin: [1893456000000] } }, { a: { $gte: 0, $lte: 1893456000000 } }]) {
|
|
887
836
|
throwsMsg(() => cvFind(f, M1.ctx), msg)
|
|
888
837
|
}
|
|
889
838
|
//反向(A為INTEGER、B為DATE): {a:{$lt:5}}會被序列化為日期而語意全變
|
|
@@ -946,20 +895,7 @@ describe('cvFind 審計02', function() {
|
|
|
946
895
|
}
|
|
947
896
|
})
|
|
948
897
|
|
|
949
|
-
it('[
|
|
950
|
-
for (let M of [LT, MS]) {
|
|
951
|
-
for (let op of [Op.startsWith, Op.endsWith, Op.substring]) {
|
|
952
|
-
for (let v of ['2026-01-01T00:00:00Z', 1893456000000, new Date(0)]) {
|
|
953
|
-
throwsMsg(() => cvFind({ d: { [op]: v } }, M.ctx), 'not supported on a date column')
|
|
954
|
-
}
|
|
955
|
-
//其他欄型(字串欄)與未給attrs(欄位型別any)照舊
|
|
956
|
-
assert.doesNotThrow(() => cvFind({ s: { [op]: 'a' } }, M.ctx))
|
|
957
|
-
assert.doesNotThrow(() => M.where0({ nocol: { [op]: 'a' } }))
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
})
|
|
961
|
-
|
|
962
|
-
it('[R4欄名解析] 手造attrs(非define後之rawAttributes): field未設、屬性值為null或字串者不崩潰, 退化為以屬性名為欄名; field已設而非非空字串者reject(見「field須為非空字串」); 欄名含孤立代理字元不崩潰而依unknown column name reject', function() {
|
|
898
|
+
it('[R4欄名解析] 手造attrs(非define後之rawAttributes): field未設、屬性值為null或字串者不崩潰, 退化為以屬性名為欄名; field已設而非非空字串者reject(見「field須為非空字串」); 欄名含孤立代理字元不崩潰而於形狀階段依unsupported column name reject', function() {
|
|
963
899
|
let attrs = { id: { type: Sequelize.STRING }, a: { type: Sequelize.INTEGER }, b: null, c: 'x', d: { type: Sequelize.DATE, field: undefined } }
|
|
964
900
|
let cx = { escape: LT.ctx.escape, attrs, dialect: 'sqlite' }
|
|
965
901
|
assert.doesNotThrow(() => cvFind({ a: 1, b: 2, c: 3, d: new Date(0) }, cx))
|
|
@@ -967,7 +903,7 @@ describe('cvFind 審計02', function() {
|
|
|
967
903
|
throwsMsg(() => cvFind({ A: 'abc' }, cx), 'unknown column name [A]')
|
|
968
904
|
throwsMsg(() => cvFind({ d: 'garbage' }, cx), '[date]')
|
|
969
905
|
for (let dl of ['sqlite', 'mssql']) {
|
|
970
|
-
throwsMsg(() => cvFind({ '\uD800': 1 }, { ...cx, dialect: dl }), '
|
|
906
|
+
throwsMsg(() => cvFind({ '\uD800': 1 }, { ...cx, dialect: dl }), 'unsupported column name')
|
|
971
907
|
}
|
|
972
908
|
})
|
|
973
909
|
|
|
@@ -989,7 +925,7 @@ describe('cvFind 最終審(run05)', function() {
|
|
|
989
925
|
for (let dl of ['sqlite', 'mssql']) {
|
|
990
926
|
for (let fd of ['', '$', '$$', '$\n$', '$a\nb$', '$a\r\nb$', '$a\u2028b$', '$a\u2029b$']) {
|
|
991
927
|
let M = mk(dl, { id: pkStr, v: { type: Sequelize.STRING, field: fd } })
|
|
992
|
-
for (let find of [{ v: 'a' }, { v: { $in: ['a'] } }, { v: { $gt: 'a' } }, { v: { $
|
|
928
|
+
for (let find of [{ v: 'a' }, { v: { $in: ['a'] } }, { v: { $gt: 'a' } }, { v: { $nin: ['a'] } }, { v: { $in: [] } }, { $or: [{ v: null }, { id: 'a' }] }]) {
|
|
993
929
|
throwsMsg(() => cvFind(find, M.ctx), `unsupported column name [${fd}]`)
|
|
994
930
|
}
|
|
995
931
|
}
|
|
@@ -1005,7 +941,7 @@ describe('cvFind 最終審(run05)', function() {
|
|
|
1005
941
|
for (let fd of [null, 0, 123, true, false, {}, []]) {
|
|
1006
942
|
let attrs = { id: { type: Sequelize.STRING }, v: { type: Sequelize.INTEGER, field: fd } }
|
|
1007
943
|
throwsMsg(() => cvFind({ v: 1 }, { escape: LT.ctx.escape, attrs, dialect: 'sqlite' }), 'unsupported column name [v]')
|
|
1008
|
-
throwsMsg(() => cvFind({ v: { $
|
|
944
|
+
throwsMsg(() => cvFind({ v: { $gt: 'a' } }, { escape: LT.ctx.escape, attrs: { ...attrs, v: { type: Sequelize.STRING, field: fd } }, dialect: 'sqlite' }), 'unsupported column name [v]')
|
|
1009
945
|
}
|
|
1010
946
|
//field未設(undefined)者以屬性名為欄名
|
|
1011
947
|
let attrs = { id: { type: Sequelize.STRING }, v: { type: Sequelize.INTEGER, field: undefined }, w: { type: Sequelize.INTEGER } }
|
|
@@ -1067,50 +1003,137 @@ describe('cvFind 最終審(run05)', function() {
|
|
|
1067
1003
|
assert.deepStrictEqual(cvFind({}, cx(a1)), {})
|
|
1068
1004
|
})
|
|
1069
1005
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1006
|
+
})
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
describe('cvFind 第33項批1(R11 系列條件白名單接線)', function() {
|
|
1010
|
+
|
|
1011
|
+
//規格句(CLAUDE_rulebook.md R11): cvFind第一步經wsemi之cvOrmFind(maxList:999)檢核並取複本, 其後只走訪複本; 對已檢核之複本再呼叫為冪等(w-orm-mdb於其normFind後再呼叫cvFind);
|
|
1012
|
+
//$and/$or清單超過32項者分組為平衡樹(SQLite運算式樹深度上限); ctx.escape與ctx.dialect已不使用
|
|
1013
|
+
|
|
1014
|
+
let pkS = { type: Sequelize.STRING, primaryKey: true }
|
|
1015
|
+
//案例表之欄位: id name age g t flag d
|
|
1016
|
+
let defsF = { id: pkS, name: Sequelize.STRING, age: Sequelize.INTEGER, g: Sequelize.STRING, t: Sequelize.STRING, flag: Sequelize.BOOLEAN, d: Sequelize.DATE }
|
|
1017
|
+
let MF = [mk('sqlite', defsF), mk('mssql', defsF)]
|
|
1018
|
+
|
|
1019
|
+
let tryOf = (fn) => {
|
|
1020
|
+
try {
|
|
1021
|
+
return { v: fn() }
|
|
1022
|
+
}
|
|
1023
|
+
catch (err) {
|
|
1024
|
+
return { err: String(err.message) }
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
it('案例表: 每案cvFind(find)與cvFind(chkFindShape複本)結果相同(冪等); REJ案訊息即cvOrmFind之訊息', function() {
|
|
1029
|
+
let n = 0
|
|
1030
|
+
for (let M of MF) {
|
|
1031
|
+
for (let c of cvCases.cases) {
|
|
1032
|
+
let a = tryOf(() => M.where(c.find()))
|
|
1033
|
+
let b = tryOf(() => M.where(chkFindShape(c.find())))
|
|
1034
|
+
assert.deepStrictEqual(a, b, c.id)
|
|
1035
|
+
n++
|
|
1036
|
+
if (c.expect === 'REJ') {
|
|
1037
|
+
let o = tryOf(() => cvOrmFind(c.find(), { maxList: 999 }))
|
|
1038
|
+
assert.ok(a.err !== undefined, `${c.id} 應reject`)
|
|
1039
|
+
assert.strictEqual(a.err, o.err, c.id)
|
|
1040
|
+
assert.ok(a.err.indexOf(c.why) >= 0, `${c.id}: ${a.err}`)
|
|
1041
|
+
}
|
|
1042
|
+
else if (a.err !== undefined) {
|
|
1043
|
+
assert.ok(c.schema === true, `非schema案不得reject: ${c.id} ${a.err}`)
|
|
1078
1044
|
}
|
|
1079
1045
|
}
|
|
1080
1046
|
}
|
|
1047
|
+
assert.ok(n >= 500, `案數 ${n}`)
|
|
1081
1048
|
})
|
|
1082
1049
|
|
|
1083
|
-
it('
|
|
1050
|
+
it('ctx.escape、ctx.dialect已不使用: 給或不給、給任意值結果相同; 空物件回{}', function() {
|
|
1051
|
+
for (let M of MF) {
|
|
1052
|
+
let f = { g: 'A', $or: [{ age: { $in: [20, 30] } }, { d: { $gte: new Date(0) } }] }
|
|
1053
|
+
let a = M.where(f)
|
|
1054
|
+
assert.strictEqual(M.where(f, { escape: undefined, dialect: undefined }), a)
|
|
1055
|
+
assert.strictEqual(M.where(f, { escape: () => 'X', dialect: 'nonsense' }), a)
|
|
1056
|
+
}
|
|
1057
|
+
assert.deepStrictEqual(cvFind({}, {}), {})
|
|
1058
|
+
assert.deepStrictEqual(cvFind(undefined, {}), {})
|
|
1059
|
+
assert.deepStrictEqual(cvFind(Object.create(null), {}), {})
|
|
1060
|
+
})
|
|
1061
|
+
|
|
1062
|
+
it('$and/$or清單與並列欄位之隱含AND分組: 項數超過4者遞迴二分為平衡樹, 項數與順序不變, 邏輯節點層數僅隨項數對數成長(4項以內維持平鋪)', function() {
|
|
1063
|
+
//leaves, 逐層展開僅含單一Op.and/Op.or鍵之節點取葉, mx.v記最深邏輯節點層數
|
|
1064
|
+
let leaves = (o, acc, lv, mx) => {
|
|
1065
|
+
let ks = Reflect.ownKeys(o)
|
|
1066
|
+
if (ks.length === 1 && (ks[0] === Op.or || ks[0] === Op.and) && Array.isArray(o[ks[0]])) {
|
|
1067
|
+
mx.v = Math.max(mx.v, lv + 1)
|
|
1068
|
+
o[ks[0]].forEach((x) => leaves(x, acc, lv + 1, mx))
|
|
1069
|
+
}
|
|
1070
|
+
else {
|
|
1071
|
+
acc.push(o)
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
//[項數, 預期最深邏輯節點層數]: 頂層Op.and → 清單節點 →(每超過4項之二分一層)→ 各子條件之Op.and; 4項以內為3, 5項起每次折半加1層
|
|
1075
|
+
for (let op of ['$or', '$and']) {
|
|
1076
|
+
for (let [n, lvExp] of [[1, 3], [4, 3], [5, 4], [8, 4], [9, 5], [32, 6], [33, 7], [999, 11]]) {
|
|
1077
|
+
let find = { [op]: Array.from({ length: n }, (_, i) => ({ id: `k${i}` })) }
|
|
1078
|
+
let acc = []
|
|
1079
|
+
let mx = { v: 0 }
|
|
1080
|
+
leaves(cvFind(find, LT.ctx), acc, 0, mx)
|
|
1081
|
+
assert.deepStrictEqual(acc.map((x) => x.id), Array.from({ length: n }, (_, i) => `k${i}`), `${op} ${n}: 葉之順序與數量`)
|
|
1082
|
+
assert.strictEqual(mx.v, lvExp, `${op} ${n}: 邏輯節點層數`)
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
//同一條件物件內並列之欄位(隱含AND)亦分組: 40欄 → 葉數與順序不變, 層數僅對數成長
|
|
1086
|
+
let wide = Object.fromEntries(Array.from({ length: 40 }, (_, i) => [`c${i}`, i]))
|
|
1087
|
+
let dfs = { id: { type: Sequelize.STRING, primaryKey: true } }
|
|
1088
|
+
for (let k of Object.keys(wide)) {
|
|
1089
|
+
dfs[k] = Sequelize.INTEGER
|
|
1090
|
+
}
|
|
1091
|
+
let Mw = mk('sqlite', dfs)
|
|
1092
|
+
let accW = []
|
|
1093
|
+
let mxW = { v: 0 }
|
|
1094
|
+
leaves(cvFind(wide, Mw.ctx), accW, 0, mxW)
|
|
1095
|
+
assert.deepStrictEqual(accW.map((x) => Object.keys(x)[0]), Object.keys(wide), '並列欄位之順序與數量')
|
|
1096
|
+
assert.ok(mxW.v <= 8, `並列40欄之邏輯節點層數 ${mxW.v}`)
|
|
1097
|
+
//SQL: 999項之連接詞恆為998個(n項共n-1個), 子條件逐一出現
|
|
1084
1098
|
for (let M of [LT, MS]) {
|
|
1085
|
-
for (let op of [
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
}
|
|
1090
|
-
//元素仍依欄型檢核; null、undefined元素reject(BETWEEN NULL AND x恆為未知)
|
|
1091
|
-
throwsMsg(() => cvFind({ i: { [op]: ['a', 2] } }, M.ctx), '[int]')
|
|
1092
|
-
throwsMsg(() => cvFind({ i: { [op]: [null, 2] } }, M.ctx), 'null is not allowed')
|
|
1093
|
-
throwsMsg(() => cvFind({ i: { [op]: [1, undefined] } }, M.ctx), 'array must not contain holes or undefined')
|
|
1094
|
-
assert.doesNotThrow(() => cvFind({ i: { [op]: [1, 2] } }, M.ctx))
|
|
1095
|
-
assert.doesNotThrow(() => cvFind({ d: { [op]: ['2019-01-01T00:00:00Z', new Date(1)] } }, M.ctx))
|
|
1099
|
+
for (let [op, kw] of [['$or', ' OR '], ['$and', ' AND ']]) {
|
|
1100
|
+
let w = M.where({ [op]: Array.from({ length: 999 }, (_, i) => ({ id: `k${i}` })) })
|
|
1101
|
+
assert.strictEqual((w.match(/'k\d+'/g) || []).length, 999, `${op}: 子條件數`)
|
|
1102
|
+
assert.strictEqual(w.split(kw).length - 1, 998, `${op}: 連接詞數`)
|
|
1096
1103
|
}
|
|
1097
|
-
//sequelize之方法物件元素照舊放行(呼叫端自負)
|
|
1098
|
-
assert.doesNotThrow(() => cvFind({ i: { [Op.between]: [Sequelize.literal('1'), 2] } }, M.ctx))
|
|
1099
1104
|
}
|
|
1100
1105
|
})
|
|
1101
1106
|
|
|
1102
|
-
it('
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1107
|
+
it('檢查之值即使用之值: 走訪不第二次讀取原物件(存取器不被呼叫, 以所讀之複本轉譯)', function() {
|
|
1108
|
+
let n = 0
|
|
1109
|
+
let o = {}
|
|
1110
|
+
Object.defineProperty(o, 'g', {
|
|
1111
|
+
enumerable: true,
|
|
1112
|
+
get() {
|
|
1113
|
+
n++
|
|
1114
|
+
return 'A'
|
|
1115
|
+
}
|
|
1116
|
+
})
|
|
1117
|
+
throwsMsg(() => cvFind(o, LT.ctx), 'accessor property is not allowed')
|
|
1118
|
+
assert.strictEqual(n, 0, '存取器不得被呼叫')
|
|
1119
|
+
//Proxy: 描述子陷阱每個鍵只被讀一次
|
|
1120
|
+
let reads = 0
|
|
1121
|
+
let p = new Proxy({ g: 'A', age: 20 }, {
|
|
1122
|
+
getOwnPropertyDescriptor(t, k) {
|
|
1123
|
+
reads++
|
|
1124
|
+
return Reflect.getOwnPropertyDescriptor(t, k)
|
|
1125
|
+
}
|
|
1126
|
+
})
|
|
1127
|
+
assert.ok(MF[0].where(p).indexOf('`g` = \'A\'') >= 0)
|
|
1128
|
+
assert.strictEqual(reads, 2, '2個鍵各讀1次')
|
|
1129
|
+
})
|
|
1130
|
+
|
|
1131
|
+
it('匯出簽名不變(w-orm-mdb深引用): cvFind(find, ctx)、getColKind、getColScale、normPk; 新增chkFindShape', function() {
|
|
1132
|
+
assert.strictEqual(cvFind.length, 2)
|
|
1133
|
+
assert.strictEqual(getColKind.length, 2)
|
|
1134
|
+
assert.strictEqual(normPk.length, 2)
|
|
1135
|
+
assert.strictEqual(typeof chkFindShape, 'function')
|
|
1136
|
+
assert.strictEqual(chkFindShape.length, 1)
|
|
1114
1137
|
})
|
|
1115
1138
|
|
|
1116
1139
|
})
|