w-orm-reladb 1.1.9 → 1.1.11

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.
@@ -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 非物件與陣列之find回空物件(現況保留), 非一般物件拒收', function() {
102
- for (let v of [undefined, null, [], 'a', 1, true, [{ id: 'a' }]]) {
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不得使其他條件失效, 欄名含$視為欄名', function() {
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
- assert.ok(LT.where({ 'p$x': 'q' }).indexOf('`p$x` = \'q\'') >= 0)
122
+ throwsMsg(() => cvFind({ 'p$x': 'q' }, LT.ctx), 'unsupported column name [p$x]')
121
123
  })
122
124
 
123
- it('R2 Symbol須為sequelize之運算子, 欄位層之邏輯Symbol拒收', function() {
124
- throwsMsg(() => cvFind({ i: { [Op.gt]: { x: 1 } } }, LT.ctx), 'value must be')
125
- throwsMsg(() => cvFind({ [Op.like]: 'x' }, LT.ctx), 'unsupported symbol operator')
126
- throwsMsg(() => cvFind({ [Symbol('or')]: [{ s: 'a' }] }, LT.ctx), 'unsupported symbol operator')
127
- throwsMsg(() => cvFind({ i: { [Symbol('gt')]: 1 } }, LT.ctx), 'unsupported symbol operator')
128
- throwsMsg(() => cvFind({ i: { [Op.or]: [{ [Op.eq]: 1 }], [Op.gt]: 10 } }, LT.ctx), 'unsupported symbol operator')
129
- throwsMsg(() => cvFind({ i: { [Op.or]: [] } }, LT.ctx), 'unsupported symbol operator')
130
- throwsMsg(() => cvFind({ i: { [Op.and]: 0 } }, LT.ctx), 'unsupported symbol operator')
131
- throwsMsg(() => cvFind({ s: 'a', [Symbol.toStringTag]: 'Q' }, LT.ctx), 'unsupported symbol operator')
132
- //Symbol與對應之$運算子產生相同之條件
133
- assert.strictEqual(LT.where({ i: { [Op.not]: { [Op.gt]: 2 } } }), LT.where({ i: { $not: { $gt: 2 } } }))
134
- assert.strictEqual(LT.where({ i: { [Op.in]: [1, 2] } }), LT.where({ i: { $in: [1, 2] } }))
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 同義之鍵各自成一子式, 非列舉之鍵亦納入', function() {
138
- let w = LT.where({ i: { $gt: 5, [Op.gt]: 1 } })
139
- assert.ok(w.indexOf('`i` > 5') >= 0 && w.indexOf('`i` > 1') >= 0, w)
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
- assert.ok(LT.where(f).indexOf('`s` = \'a\'') >= 0)
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), 'invalid column name')
149
- throwsMsg(() => cvFind({ '': 1 }, LT.ctx), 'invalid column name')
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 邏輯運算子須為非空之物件陣列, 陣列不得有洞', function() {
163
- for (let op of ['$and', '$or', '$nor']) {
164
- throwsMsg(() => cvFind({ [op]: [] }, LT.ctx), 'nonempty array')
165
- throwsMsg(() => cvFind({ [op]: { s: 1 } }, LT.ctx), 'requires an array')
166
- throwsMsg(() => cvFind({ [op]: ['x'] }, LT.ctx), 'condition must be an object')
167
- throwsMsg(() => cvFind({ [op]: new Array(2) }, LT.ctx), 'holes or undefined')
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({ [Op.and]: [] }, LT.ctx), 'nonempty array')
170
- throwsMsg(() => cvFind({ i: { $in: [1, undefined] } }, LT.ctx), 'holes or undefined')
171
- throwsMsg(() => cvFind({ i: { $exists: 'yes' } }, LT.ctx), '$exists must be a boolean, 0 or 1')
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 must be'],
177
- [{ f: { $gt: [1] } }, 'value must be'],
178
- [{ i: { $in: [{}] } }, 'value must be'],
179
- [{ i: NaN }, 'number must be finite'],
180
- [{ i: Infinity }, 'number must be finite'],
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 must be'],
183
- [{ s: () => 1 }, 'value must be'],
184
- [{ s: Symbol('x') }, 'value must be'],
185
- [{ s: new Map() }, 'value must be'],
186
- [{ s: undefined }, 'value is 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 } }, 'nested object is not supported'],
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] } }, '$not does not accept an array'],
192
- [{ i: { $not: {} } }, 'empty operator object'],
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 must be')
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跳脫LIKE萬用字元並帶ESCAPE, mssql為N前綴', function() {
247
- assert.ok(LT.where({ t: { $regex: '_' } }).indexOf('LIKE \'%!_%\' ESCAPE \'!\'') >= 0)
248
- assert.ok(LT.where({ t: { $regex: '%' } }).indexOf('LIKE \'%!%%\' ESCAPE \'!\'') >= 0)
249
- assert.ok(LT.where({ t: { $regex: '!' } }).indexOf('LIKE \'%!!%\' ESCAPE \'!\'') >= 0)
250
- assert.ok(LT.where({ t: { $regex: '\\[a-c\\]' } }).indexOf('LIKE \'%![a-c]%\' ESCAPE \'!\'') >= 0)
251
- assert.ok(LT.where({ t: { $regex: 'a\\.c' } }).indexOf('LIKE \'%a.c%\'') >= 0)
252
- assert.ok(MS.where({ t: { $regex: '_' } }).indexOf('LIKE N\'%!_%\' ESCAPE \'!\'') >= 0)
253
- assert.ok(MS.where({ t: { $regex: '中' } }).indexOf('LIKE N\'%中%\'') >= 0)
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: { $not: { $eq: 7 } } }, { id: { $not: { $eq: '7' } } }],
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, $regex不受影響
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
- ['$nin:[]', { [f]: { $nin: [] } }],
370
- ['陣列值[]', { [f]: [] }],
371
- ['Op.in:[]', { [f]: { [Op.in]: [] } }],
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
- ['$nor含空$in', { $nor: [{ [f]: { $in: [] } }] }],
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][最低要求②] sequelize原生Op全集 × 各類值(未給attrs): 凡未被拒收者其SQL皆引用其欄位(防日後新增分支再犯)', function() {
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 name of Object.keys(Op)) {
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: { [sym]: v } })
384
+ w = M.where0({ nocol: { [op]: v } })
426
385
  }
427
386
  catch (e) {
428
- continue //拒收(或sequelize拒絕)者不產生SQL
387
+ continue //拒收者不產生SQL
429
388
  }
430
389
  n++
431
- assert.ok(w.indexOf(M.q('nocol')) >= 0, `Op.${name} × ${JSON.stringify(v)}: SQL未引用欄位: ${w}`)
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 > 100, `被檢核之案數過少: ${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、$nin:[]以f IS NULL OR 1 = 1表示, 不用f IS NULL AND f IS NOT NULL之雙引用形式
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
- for (let op of ['$in', '$nin']) {
442
- let w = M.where({ s: { [op]: [] } })
443
- assert.strictEqual(w.split(M.q('s')).length - 1, 1, `${op}:[]: 欄位引用次數不為1: ${w}`)
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: [], $nin: [] } })
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
- throwsMsg(() => cvFind({ [k]: { $lt: 'abc' } }, M.ctx), `unknown column name [${k}]`)
483
- throwsMsg(() => cvFind({ [k]: 1 }, M.ctx), 'unknown column name')
484
- throwsMsg(() => cvFind({ [k]: { $in: [] } }, M.ctx), 'unknown column name')
485
- throwsMsg(() => cvFind({ [k]: { $not: { $gt: 1 } } }, M.ctx), 'unknown column name')
486
- throwsMsg(() => cvFind({ $or: [{ s: 'a' }, { [k]: 1 }] }, M.ctx), 'unknown column name')
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
- throwsMsg(() => cvFind({ [k]: '012' }, cx), 'unknown column name')
600
- throwsMsg(() => cvFind({ [k]: '12' }, cx), 'unknown column name')
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), `unsupported column name [${k}]`)
613
- throwsMsg(() => cvFind({ [k]: '012' }, { ...M.ctx, pk: 'i' }), `unsupported column name [${k}]`)
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: { $regex: 'a', $options: 'i' } }, M2.ctx), `unsupported column name [${fd}]`)
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
- //含$或點但非上述形態者照常(p$x為既有model之欄)
626
- assert.doesNotThrow(() => cvFind({ 'p$x': 'a' }, M.ctx))
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] 欄位運算子物件須至少產生一個子式: 僅有值為undefined之$options者reject(原條件消失而成恆真, delAll刪全表)', function() {
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 object has no operator')
647
- throwsMsg(() => cvFind({ $or: [{ s: { $options: undefined } }, { id: 'a' }] }, M.ctx), 'operator object has no operator')
648
- throwsMsg(() => cvFind({ s: { $not: { $options: undefined } } }, M.ctx), 'operator object has no operator')
649
- throwsMsg(() => cvFind({ s: { $options: 'i' } }, M.ctx), '$options requires $regex')
650
- //$regex存在時$options為undefined視同未給(R6)
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', null], $nin: ['b'], $regex: 'x', $options: 'i', $exists: true } },
669
- { $or: [{ i: 1 }, { i: { $not: { $in: [] } } }], s: [] },
670
- { d: { [Op.between]: [new Date(0), new Date(1)], [Op.gt]: 1 } },
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、陣列值、$not、原生Symbol)皆須經同一轉換, 任一入口漏轉則該入口之字串/epoch與Date之SQL不同; 預期之Date由同一瞬間獨立給出
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: { $not: { $gt: 1893456000000 } } }, { a: { [Op.between]: [0, 1893456000000] } }]) {
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('[R2][R4] 原生Op.startsWith/endsWith/substring不得用於DATE欄(sequelize以字串樣板插值, Date會成為隨主機時區與語系而變之文字)', function() {
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 }), 'unknown column name')
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: { $regex: 'a' } }, { v: { $regex: 'a', $options: 'i' } }, { v: { $exists: true } }, { v: ['a'] }, { v: { $not: { $in: [] } } }, { $or: [{ v: null }, { id: 'a' }] }]) {
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: { $regex: 'a', $options: 'i' } }, { escape: LT.ctx.escape, attrs: { ...attrs, v: { type: Sequelize.STRING, field: fd } }, dialect: 'sqlite' }), 'unsupported column name [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
- it('[R2][最低要求②] 原生Symbol運算子須為sequelize之OperatorMap所有者: Op.values、Op.join不在其中, sequelize退回等值並把整個運算子物件交欄型stringify(DATEONLY成查詢當日、DATE成現在、INTEGER成[object Object]), reject(run05 sol F04)', function() {
1071
- for (let M of [LT, MS]) {
1072
- let syms = Object.values(Op).filter((sym) => M.qg.OperatorMap[sym] === undefined)
1073
- //防sequelize改版後本測試空轉; 目前為values與join
1074
- assert.ok(syms.length >= 2 && syms.indexOf(Op.values) >= 0 && syms.indexOf(Op.join) >= 0, syms.map(String).join())
1075
- for (let sym of syms) {
1076
- for (let f of [{ s: { [sym]: 'a' } }, { do: { [sym]: '2019-01-01' } }, { i: { [sym]: 1 } }, { d: { [sym]: [new Date(0)] } }, { s: { $not: { [sym]: 'a' } } }]) {
1077
- throwsMsg(() => cvFind(f, M.ctx), `unsupported symbol operator [${String(sym)}]`)
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('[R2][最低要求②] 原生Op.between/Op.notBetween之值須為兩元素之陣列(sequelize以value[0]、value[1]取值: 字串被逐字元取值、短陣列補NULL, NOT BETWEEN可命中全部非NULL列; 自查)', function() {
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 [Op.between, Op.notBetween]) {
1086
- for (let v of ['12', '2019-01-01', 12, [], [1], [1, 2, 3], {}, true]) {
1087
- throwsMsg(() => cvFind({ i: { [op]: v } }, M.ctx), `${String(op)} requires an array of two values`)
1088
- throwsMsg(() => cvFind({ do: { [op]: v } }, M.ctx), `${String(op)} requires an array of two values`)
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('[R6] $regex加i時LOWER(col)以Sequelize.col引用: 欄名以*開頭時sequelize回傳*而成LOWER(*), 此情形reject; 不給i或其他運算子照常(opus [3])', function() {
1103
- for (let dl of ['sqlite', 'mssql']) {
1104
- let M = mk(dl, { id: pkStr, v: { type: Sequelize.STRING, field: '*a' } })
1105
- throwsMsg(() => cvFind({ v: { $regex: 'a', $options: 'i' } }, M.ctx), 'unsupported column name [*a]')
1106
- throwsMsg(() => cvFind({ v: /a/i }, M.ctx), 'unsupported column name [*a]')
1107
- throwsMsg(() => cvFind({ v: { $not: { $regex: 'a', $options: 'i' } } }, M.ctx), 'unsupported column name [*a]')
1108
- assert.doesNotThrow(() => cvFind({ v: { $regex: 'a' } }, M.ctx))
1109
- assert.doesNotThrow(() => cvFind({ v: 'a' }, M.ctx))
1110
- //屬性名以*開頭而欄名不同者, 以欄名判定
1111
- let M2 = mk(dl, { 'id': pkStr, '*v': { type: Sequelize.STRING, field: 'vcol' } })
1112
- assert.doesNotThrow(() => cvFind({ '*v': { $regex: 'a', $options: 'i' } }, M2.ctx))
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
  })