w-orm-reladb 1.1.6 → 1.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cvFind.mjs ADDED
@@ -0,0 +1,789 @@
1
+ import Sequelize from 'sequelize'
2
+
3
+
4
+ //查詢條件(find)之轉譯, 供select與delAll共用, 令同一find於兩函數由同一SQL謂詞決定命中
5
+ //
6
+ //原select內之cvObj以lodash之each迭代並以Op[k]直譯$鍵, 有下列缺陷而不得供破壞性之delAll使用:
7
+ // 1. 不支援之運算子成為鍵'undefined', 運算子物件被交給sequelize之欄位型別stringify:
8
+ // FLOAT成'NaN'、INTEGER成'[object Object]'、DATE成本地今日00:00, 致靜默無命中或刪錯列
9
+ // 2. 具數值length之物件被當成陣列樣迭代而丟棄全部條件(回全表)、Symbol鍵被丟棄
10
+ // 3. $regex轉LIKE而未跳脫萬用字元, 欄位層$not遇運算子物件被轉為!=
11
+ //本模組改以白名單轉譯, 凡不能保證語意者一律拋錯, 規則見CLAUDE_rulebook.md之R1–R6;
12
+ //主鍵值之正規化(normPk)亦屬本模組, 供主鍵函數與主鍵欄條件共用, 規則見R8
13
+
14
+
15
+ //Op
16
+ let Op = Sequelize.Op
17
+
18
+
19
+ //sequelize之運算子Symbol全集, 非其成員之Symbol會被sequelize靜默丟棄, 故一律拒收
20
+ let setOp = new Set(Object.values(Op))
21
+
22
+
23
+ //比較運算子, 值須為純量(null僅$eq與$ne)
24
+ let mCmp = {
25
+ $eq: Op.eq,
26
+ $ne: Op.ne,
27
+ $gt: Op.gt,
28
+ $gte: Op.gte,
29
+ $lt: Op.lt,
30
+ $lte: Op.lte,
31
+ }
32
+
33
+
34
+ //sequelize之Symbol運算子對應之$名稱, 令呼叫端以Symbol給條件時亦經同一檢核與轉譯
35
+ let mSymCmp = new Map([
36
+ [Op.eq, '$eq'],
37
+ [Op.ne, '$ne'],
38
+ [Op.gt, '$gt'],
39
+ [Op.gte, '$gte'],
40
+ [Op.lt, '$lt'],
41
+ [Op.lte, '$lte'],
42
+ [Op.in, '$in'],
43
+ [Op.notIn, '$nin'],
44
+ [Op.not, '$not'],
45
+ ])
46
+
47
+
48
+ //欄位層不接受之Symbol: 邏輯運算子於sequelize欄位層會提前返回而丟棄同層其他條件
49
+ let setSymFieldNo = new Set([Op.and, Op.or])
50
+
51
+
52
+ //LIKE之跳脫字元, 選用於各方言字串常值內皆無特殊意義者, 令ESCAPE子句不需依方言跳脫
53
+ let cEsc = '!'
54
+
55
+
56
+ //巢狀深度上限, 防循環參照與過深之條件
57
+ let nDepthMax = 32
58
+
59
+
60
+ //欄位型別分類, 以sequelize之型別類別判定(各方言之型別皆繼承之), 另以type.key補判
61
+ //註: DataTypes.DOUBLE之key為'DOUBLE PRECISION', 只比對key會漏判而不檢型別
62
+ let DT = Sequelize.DataTypes
63
+ let tpsStr = ['STRING', 'TEXT', 'CHAR', 'UUID', 'CITEXT']
64
+ let tpsInt = ['INTEGER', 'BIGINT', 'SMALLINT', 'MEDIUMINT', 'TINYINT']
65
+ let tpsNum = ['FLOAT', 'REAL', 'DOUBLE', 'DOUBLE PRECISION', 'DECIMAL', 'NUMBER']
66
+ let tpsDate = ['DATE']
67
+ let tpsDateOnly = ['DATEONLY']
68
+ let tpsBool = ['BOOLEAN']
69
+
70
+
71
+ //嚴格十進位之數值字串, 不收前後空白與0x、0b、0o等寫法(sqlite不認而當成文字比較)
72
+ //整數欄只收整數寫法: mssql轉'2.5'、'1e3'、'5.'為int時失敗, sqlite卻以'1e3'命中1000、'5.'命中5, 兩方言分歧
73
+ //其他數值欄不收指數與尾端小數點: mssql轉'1e3'為DECIMAL時失敗
74
+ let reInt = /^[+-]?\d+$/
75
+ let reNum = /^[+-]?(\d+(\.\d+)?|\.\d+)$/
76
+
77
+
78
+ //標準整數寫法, 即String(Number(s))===s之整數字串(無正號、前導零與-0), 供主鍵正規化
79
+ let reIntStd = /^(0|-?[1-9]\d*)$/
80
+
81
+
82
+ //帶時區之ISO 8601日期時間字串; 無時區者依主機時區解析而結果隨主機而變, 故不收
83
+ let reIsoTz = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(:(\d{2})(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})$/
84
+
85
+
86
+ //日期字串, 供DATEONLY欄
87
+ let reDateOnly = /^(\d{4})-(\d{2})-(\d{2})$/
88
+
89
+
90
+ /**
91
+ * 判定年月日是否為曆法上存在之日期, 如2026-02-30不存在
92
+ *
93
+ * @ignore
94
+ */
95
+ function isCalendarDate(y, m, d) {
96
+ let t = new Date(Date.UTC(Number(y), Number(m) - 1, Number(d)))
97
+ return t.getUTCFullYear() === Number(y) && t.getUTCMonth() === Number(m) - 1 && t.getUTCDate() === Number(d)
98
+ }
99
+
100
+
101
+ /**
102
+ * 拋出條件錯誤
103
+ *
104
+ * @ignore
105
+ */
106
+ function err(msg, path) {
107
+ throw new Error(`invalid find: ${msg}${path ? ` at [${path}]` : ''}`)
108
+ }
109
+
110
+
111
+ /**
112
+ * 是否為一般物件: 原型為Object.prototype或null者
113
+ * 註: 不以toString判定, 因帶Symbol.toStringTag之一般物件會被誤判為非物件, 而class實例、Object.create(proto)會被誤判為一般物件,
114
+ * 其條件可能放在原型上而取不到自有鍵, 被當成{}而刪全表
115
+ *
116
+ * @ignore
117
+ */
118
+ function isPlainObj(v) {
119
+ if (v === null || typeof v !== 'object') {
120
+ return false
121
+ }
122
+ let p = Object.getPrototypeOf(v)
123
+ return p === Object.prototype || p === null
124
+ }
125
+
126
+
127
+ /**
128
+ * 恆真與恆假之謂詞, 以明確之SQL表示, 不以空條件表示
129
+ * 註: sequelize會把空條件(如{}、空$and)於OR、NOT內濾除, 致恆真被當成不存在而改變語意(如$nor:[{}]成全部)
130
+ *
131
+ * @ignore
132
+ */
133
+ function sqlTrue() {
134
+ return Sequelize.literal('1 = 1')
135
+ }
136
+ function sqlFalse() {
137
+ return Sequelize.literal('1 = 0')
138
+ }
139
+
140
+
141
+ /**
142
+ * 取欄位之型別分類: str、int、num、date、bool, 未知或非上列者為any
143
+ *
144
+ * @param {Object} attrs 輸入model之rawAttributes
145
+ * @param {String} field 輸入欄位名
146
+ * @returns {String} 回傳型別分類字串
147
+ */
148
+ function getColKind(attrs, field) {
149
+ if (!attrs || !Object.prototype.hasOwnProperty.call(attrs, field)) {
150
+ return 'any'
151
+ }
152
+ let t = attrs[field] && attrs[field].type
153
+ let k = t && t.key
154
+ if (t instanceof DT.INTEGER || tpsInt.indexOf(k) >= 0) {
155
+ return 'int'
156
+ }
157
+ if (t instanceof DT.NUMBER || tpsNum.indexOf(k) >= 0) {
158
+ return 'num'
159
+ }
160
+ if (t instanceof DT.DATEONLY || tpsDateOnly.indexOf(k) >= 0) {
161
+ return 'dateonly'
162
+ }
163
+ if (t instanceof DT.DATE || tpsDate.indexOf(k) >= 0) {
164
+ return 'date'
165
+ }
166
+ if (t instanceof DT.BOOLEAN || tpsBool.indexOf(k) >= 0) {
167
+ return 'bool'
168
+ }
169
+ if (t instanceof DT.STRING || t instanceof DT.TEXT || t instanceof DT.UUID || t instanceof DT.CITEXT || tpsStr.indexOf(k) >= 0) {
170
+ return 'str'
171
+ }
172
+ return 'any'
173
+ }
174
+
175
+
176
+ /**
177
+ * 取DECIMAL欄之scale, 未指定者為0(mssql之DECIMAL預設為(18,0)), 非DECIMAL者為null
178
+ *
179
+ * @param {Object} attrs 輸入model之rawAttributes
180
+ * @param {String} field 輸入欄位名
181
+ * @returns {Number|null} 回傳scale
182
+ */
183
+ function getColScale(attrs, field) {
184
+ if (!attrs || !Object.prototype.hasOwnProperty.call(attrs, field)) {
185
+ return null
186
+ }
187
+ let t = attrs[field] && attrs[field].type
188
+ if (!(t instanceof DT.DECIMAL)) {
189
+ return null
190
+ }
191
+ return typeof t._scale === 'number' ? t._scale : 0
192
+ }
193
+
194
+
195
+ /**
196
+ * 判定純量是否符合欄位型別分類
197
+ * 註: sqlite比較不同型別時不報錯而依型別排序, 如INTEGER < 'abc'、DATE < 'garbage'恆為真,
198
+ * 若放行則delAll會刪除全部非NULL之列; mssql則轉型失敗而reject, 兩方言分歧, 故一律於送出前判定
199
+ * 註: 整數欄之數值須在安全整數範圍內, 超出者已失精度而會比對到相鄰之值; 精確之大整數以數值字串給予
200
+ *
201
+ * @param {*} v 輸入純量
202
+ * @param {String} kind 輸入欄位型別分類
203
+ * @returns {Boolean} 回傳是否相符
204
+ */
205
+ function isKindOk(v, kind, scale = null) {
206
+ if (kind === 'str') {
207
+ return typeof v === 'string'
208
+ }
209
+ if (kind === 'int' || kind === 'num') {
210
+ if (typeof v === 'number') {
211
+ if (!Number.isFinite(v)) {
212
+ return false
213
+ }
214
+ if (kind === 'int' && Math.abs(v) > Number.MAX_SAFE_INTEGER) {
215
+ return false
216
+ }
217
+ return true
218
+ }
219
+ if (typeof v !== 'string' || !(kind === 'int' ? reInt : reNum).test(v)) {
220
+ return false
221
+ }
222
+ //DECIMAL欄之字串小數位不得多於欄位之scale(未指定為0): mssql把字串轉為欄位型別時四捨五入, '1.254'會命中1.25
223
+ if (scale !== null) {
224
+ let k = v.indexOf('.')
225
+ let nd = k >= 0 ? v.length - k - 1 : 0
226
+ return nd <= scale
227
+ }
228
+ return true
229
+ }
230
+ if (kind === 'date') {
231
+ if (v instanceof Date) {
232
+ return !isNaN(v.getTime())
233
+ }
234
+ if (typeof v === 'number') {
235
+ //須在Date可表示之範圍(±8.64e15毫秒)內, 超出者被stringify為'Invalid date'而以文字比較, sqlite會命中全部日期列
236
+ return Number.isFinite(v) && !isNaN(new Date(v).getTime())
237
+ }
238
+ if (typeof v !== 'string') {
239
+ return false
240
+ }
241
+ let m = v.match(reIsoTz)
242
+ return m !== null && isCalendarDate(m[1], m[2], m[3]) && !isNaN(Date.parse(v.replace(' ', 'T')))
243
+ }
244
+ if (kind === 'dateonly') {
245
+ //Date物件依主機時區取日期而結果隨主機而變, 故只收日期字串
246
+ if (typeof v !== 'string') {
247
+ return false
248
+ }
249
+ let m = v.match(reDateOnly)
250
+ return m !== null && isCalendarDate(m[1], m[2], m[3])
251
+ }
252
+ if (kind === 'bool') {
253
+ return typeof v === 'boolean' || v === 0 || v === 1
254
+ }
255
+ return true
256
+ }
257
+
258
+
259
+ /**
260
+ * 拆包裝物件: new String(x)與new Number(x)取原始值, 令其與原始值同處置(同w-orm-mdb)
261
+ *
262
+ * @ignore
263
+ */
264
+ function unwrap(v) {
265
+ if (v !== null && typeof v === 'object') {
266
+ let c = Object.prototype.toString.call(v)
267
+ if (c === '[object String]' || c === '[object Number]') {
268
+ return v.valueOf()
269
+ }
270
+ }
271
+ return v
272
+ }
273
+
274
+
275
+ /**
276
+ * 依主鍵欄型別分類正規化主鍵值, 與w-orm-mdb之normPk同一規則(見CLAUDE_rulebook.md之R8)
277
+ *
278
+ * 註: kind為'ok'(v為正規化後之值)、'invalid'(未給或非有效值, 依規格T6處置)、'mismatch'(與主鍵欄型別不符)
279
+ * 註: 字串類主鍵欄之有限數字以String(v)處置, 與w-serv-orm、w-orm-mdb一致
280
+ * 註: 整數類主鍵欄收安全整數, 以及標準整數寫法之字串(String(Number(s))===s)而轉為數值;
281
+ * 超出安全整數之標準整數寫法字串保留為字串(精確值, 供BIGINT), 超出安全整數之數值已失精度而會命中相鄰主鍵, 視為型別不符;
282
+ * 非整數之數值與其他字串(如'012'、'1e1'、'0x1')寫入時會被資料庫轉型而改變鍵, 視為型別不符
283
+ * 註: 其他數值類主鍵欄收有限數值與標準數字字串(轉為數值), 其他字串視為型別不符;
284
+ * DECIMAL主鍵欄之值小數位多於scale者寫入時會被四捨五入而改變鍵(mssql之DECIMAL(10,2)存1.254為1.25, insert回報已存在而selectByPk(1.254)回null), 視為型別不符
285
+ * 註: 其他型別之主鍵欄不檢型別, 收非空字串與有限數值
286
+ * 註: NaN、±Infinity、空字串、布林、物件、陣列、Date、BigInt、null、undefined為無效
287
+ *
288
+ * @param {*} v 輸入主鍵值
289
+ * @param {String} kind 輸入主鍵欄之型別分類, 取自getColKind
290
+ * @param {Number|null} [scale=null] 輸入DECIMAL主鍵欄之scale, 取自getColScale, 非DECIMAL者為null
291
+ * @returns {Object} 回傳{kind,v}
292
+ */
293
+ function normPk(v, kind, scale = null) {
294
+
295
+ //unwrap
296
+ v = unwrap(v)
297
+
298
+ //string
299
+ if (typeof v === 'string') {
300
+ if (v === '') {
301
+ return { kind: 'invalid' }
302
+ }
303
+ if (kind === 'int') {
304
+ if (!reIntStd.test(v)) {
305
+ return { kind: 'mismatch' }
306
+ }
307
+ let n = Number(v)
308
+ return { kind: 'ok', v: Number.isSafeInteger(n) ? n : v }
309
+ }
310
+ if (kind === 'num') {
311
+ let n = Number(v)
312
+ if (!Number.isFinite(n) || String(n) !== v) {
313
+ return { kind: 'mismatch' }
314
+ }
315
+ v = n //續由數值判定scale
316
+ }
317
+ else {
318
+ return { kind: 'ok', v }
319
+ }
320
+ }
321
+
322
+ //number
323
+ if (typeof v === 'number') {
324
+ if (!Number.isFinite(v)) {
325
+ return { kind: 'invalid' }
326
+ }
327
+ if (kind === 'str') {
328
+ return { kind: 'ok', v: String(v) }
329
+ }
330
+ if (kind === 'int' && !Number.isSafeInteger(v)) {
331
+ return { kind: 'mismatch' }
332
+ }
333
+ if (kind === 'num' && scale !== null && Number(v.toFixed(scale)) !== v) {
334
+ return { kind: 'mismatch' }
335
+ }
336
+ return { kind: 'ok', v }
337
+ }
338
+
339
+ return { kind: 'invalid' }
340
+ }
341
+
342
+
343
+ /**
344
+ * 檢核純量值: 字串、有限數值、布林、有效Date、Buffer, 以及允許時之null; 並須符合欄位型別分類
345
+ * 註: 物件、陣列等不得交sequelize之型別stringify, 否則FLOAT成'NaN'、INTEGER成'[object Object]'、DATE成本地今日00:00而誤刪
346
+ * 註: 主鍵欄(pk為true)之字串類、整數類與其他數值類以normPk正規化後回傳(如字串類主鍵欄之123即'123'), 無效或型別不符者拒收,
347
+ * 令select與delAll對主鍵之認定與selectByPk、del等主鍵函數一致; 非主鍵欄不轉型
348
+ *
349
+ * @ignore
350
+ */
351
+ function chkScalar(v, path, allowNull, kind = 'any', scale = null, pk = false) {
352
+ if (v === null) {
353
+ if (allowNull) {
354
+ return v
355
+ }
356
+ err('null is not allowed here', path)
357
+ }
358
+ let ok = false
359
+ if (typeof v === 'string' || typeof v === 'boolean') {
360
+ ok = true
361
+ }
362
+ else if (typeof v === 'number') {
363
+ if (!Number.isFinite(v)) {
364
+ err(`number must be finite, got [${v}]`, path)
365
+ }
366
+ ok = true
367
+ }
368
+ else if (v instanceof Date) {
369
+ if (isNaN(v.getTime())) {
370
+ err('invalid Date', path)
371
+ }
372
+ ok = true
373
+ }
374
+ else if (Buffer.isBuffer(v)) {
375
+ ok = kind === 'any'
376
+ }
377
+ if (!ok) {
378
+ err(`value must be a string, finite number, boolean, Date or null, got [${Object.prototype.toString.call(v)}]`, path)
379
+ }
380
+ if (pk && (kind === 'str' || kind === 'int' || kind === 'num')) {
381
+ let r = normPk(v, kind, scale)
382
+ if (r.kind === 'ok') {
383
+ return r.v
384
+ }
385
+ err(`value [${String(v)}] does not match column type [${kind}]${scale !== null ? ` with scale [${scale}]` : ''}`, path)
386
+ }
387
+ if (!isKindOk(v, kind, scale)) {
388
+ err(`value [${String(v)}] does not match column type [${kind}]${scale !== null ? ` with scale [${scale}]` : ''}`, path)
389
+ }
390
+ return v
391
+ }
392
+
393
+
394
+ /**
395
+ * 取陣列之各元素, 稀疏陣列之洞與undefined一律拒收
396
+ * 註: forEach與map會略過洞, 洞若被略過則$and之子式消失而成恆真
397
+ *
398
+ * @ignore
399
+ */
400
+ function getItems(arr, path) {
401
+ let rs = []
402
+ for (let i = 0; i < arr.length; i++) {
403
+ if (!(i in arr) || arr[i] === undefined) {
404
+ err('array must not contain holes or undefined', `${path}.${i}`)
405
+ }
406
+ rs.push(arr[i])
407
+ }
408
+ return rs
409
+ }
410
+
411
+
412
+ /**
413
+ * 檢核sequelize原生Symbol運算子之值: 純量、純量陣列或sequelize之literal/fn/col
414
+ * 註: 原生寫法由呼叫端自負語意, 但仍不得把物件交sequelize之型別stringify
415
+ *
416
+ * @ignore
417
+ */
418
+ function chkRawSym(x, path, kind, scale, pk) {
419
+ if (x instanceof Sequelize.Utils.SequelizeMethod) {
420
+ return x
421
+ }
422
+ if (Array.isArray(x)) {
423
+ return getItems(x, path).map((y, i) => (y instanceof Sequelize.Utils.SequelizeMethod ? y : chkScalar(y, `${path}.${i}`, true, kind, scale, pk)))
424
+ }
425
+ return chkScalar(x, path, true, kind, scale, pk)
426
+ }
427
+
428
+
429
+ /**
430
+ * 將正規式來源轉為字面字串, 僅接受無未跳脫元字元者
431
+ *
432
+ * @ignore
433
+ */
434
+ function regexToLiteral(src, path) {
435
+ let r = ''
436
+ let meta = '\\^$.|?*+()[]{}/'
437
+ for (let i = 0; i < src.length; i++) {
438
+ let c = src[i]
439
+ if (c === '\\') {
440
+ let n = src[i + 1]
441
+ if (n !== undefined && meta.indexOf(n) >= 0) {
442
+ r += n
443
+ i++
444
+ continue
445
+ }
446
+ err(`$regex supports literal substrings only, escape [\\${n === undefined ? '' : n}] is not supported`, path)
447
+ }
448
+ if (meta.indexOf(c) >= 0) {
449
+ err(`$regex supports literal substrings only, unescaped metacharacter [${c}] is not supported`, path)
450
+ }
451
+ r += c
452
+ }
453
+ if (r.indexOf('\u0000') >= 0) {
454
+ err('$regex must not contain NUL', path)
455
+ }
456
+ return r
457
+ }
458
+
459
+
460
+ /**
461
+ * 正規化正規式旗標, 僅接受i; '$i'為既有寫法, 過渡期視同i
462
+ *
463
+ * @ignore
464
+ */
465
+ function normFlags(fs, path) {
466
+ if (fs === undefined || fs === null || fs === '') {
467
+ return false
468
+ }
469
+ if (typeof fs !== 'string') {
470
+ err('$options must be a string', path)
471
+ }
472
+ let s = fs === '$i' ? 'i' : fs
473
+ for (let c of s) {
474
+ if (c !== 'i') {
475
+ err(`$options/flags supports [i] only, got [${fs}]`, path)
476
+ }
477
+ }
478
+ return s.indexOf('i') >= 0
479
+ }
480
+
481
+
482
+ /**
483
+ * 產生LIKE子字串條件, 跳脫萬用字元並帶ESCAPE子句; 不分大小寫時兩側皆以LOWER處理
484
+ * 註: sqlite之LOWER只折疊ASCII, 故sqlite下給i而字面含非ASCII之大小寫字母者拒收, 不宣稱做不到之語意
485
+ *
486
+ * @ignore
487
+ */
488
+ function cvRegex(field, re, options, path, ctx, kind) {
489
+ if (kind !== 'str' && kind !== 'any') {
490
+ err(`$regex requires a string column, column type is [${kind}]`, path)
491
+ }
492
+ let src
493
+ let ci
494
+ if (re instanceof RegExp) {
495
+ if (options !== undefined) {
496
+ err('$options can not be used with a RegExp $regex', path)
497
+ }
498
+ let flags = re.flags.replace(/[gu]/g, '') //g、u不影響是否命中
499
+ src = re.source === '(?:)' ? '' : re.source //new RegExp('')之source為(?:), 即空字串
500
+ ci = normFlags(flags, path)
501
+ }
502
+ else if (typeof re === 'string') {
503
+ src = re
504
+ ci = normFlags(options, path)
505
+ }
506
+ else {
507
+ err('$regex must be a string or RegExp', path)
508
+ }
509
+ let lit = regexToLiteral(src, path)
510
+ if (ci && ctx.dialect === 'sqlite') {
511
+ for (let c of lit) {
512
+ if (c.charCodeAt(0) > 127 && c.toLowerCase() !== c.toUpperCase()) {
513
+ err(`$regex with i supports ASCII letters only on sqlite, got [${c}]`, path)
514
+ }
515
+ }
516
+ }
517
+ let pat = `%${lit.replace(/[!%_[]/g, (c) => `${cEsc}${c}`)}%`
518
+ let q = ctx.escape(pat)
519
+ if (ci) {
520
+ //LOWER內須為資料庫之實際欄名, model之屬性名與欄名可不同
521
+ let col = ctx.attrs && ctx.attrs[field] && ctx.attrs[field].field ? ctx.attrs[field].field : field
522
+ return Sequelize.where(Sequelize.fn('LOWER', Sequelize.col(col)), Op.like, Sequelize.literal(`LOWER(${q}) ESCAPE '${cEsc}'`))
523
+ }
524
+ return { [field]: { [Op.like]: Sequelize.literal(`${q} ESCAPE '${cEsc}'`) } }
525
+ }
526
+
527
+
528
+ /**
529
+ * $in與$nin, 含null者另以IS [NOT] NULL補上, 與{欄位:null}一致; 空集合以恆假/恆真表示
530
+ *
531
+ * @ignore
532
+ */
533
+ function cvIn(field, arr, path, neg, kind, scale, pk) {
534
+ if (!Array.isArray(arr)) {
535
+ err(`${neg ? '$nin' : '$in'} must be an array`, path)
536
+ }
537
+ let hasNull = false
538
+ let vs = []
539
+ getItems(arr, path).forEach((v, k) => {
540
+ if (v === null) {
541
+ hasNull = true
542
+ return
543
+ }
544
+ vs.push(chkScalar(v, `${path}.${k}`, false, kind, scale, pk))
545
+ })
546
+ if (!neg) {
547
+ if (vs.length === 0) {
548
+ return hasNull ? { [field]: null } : sqlFalse()
549
+ }
550
+ let c = { [field]: { [Op.in]: vs } }
551
+ return hasNull ? { [Op.or]: [c, { [field]: null }] } : c
552
+ }
553
+ if (vs.length === 0) {
554
+ return hasNull ? { [field]: { [Op.ne]: null } } : sqlTrue()
555
+ }
556
+ let c = { [field]: { [Op.notIn]: vs } }
557
+ return hasNull ? { [Op.and]: [c, { [field]: { [Op.ne]: null } }] } : c
558
+ }
559
+
560
+
561
+ /**
562
+ * 欄位層條件, 回傳以AND結合之子式陣列
563
+ *
564
+ * @ignore
565
+ */
566
+ function cvField(field, v, path, depth, ctx) {
567
+ let kind = getColKind(ctx.attrs, field)
568
+ let scale = getColScale(ctx.attrs, field)
569
+ let pk = ctx.pk !== undefined && field === ctx.pk //主鍵欄之純量依normPk正規化(R8)
570
+ if (depth > nDepthMax) {
571
+ err('find is too deep', path)
572
+ }
573
+ if (field === '' || field === '__proto__') {
574
+ err(`invalid column name [${field}]`, path)
575
+ }
576
+ if (v === undefined) {
577
+ err('value is undefined', path)
578
+ }
579
+ if (Array.isArray(v)) {
580
+ //陣列值即$in(含null與空陣列之處置), 與MongoDB之陣列比對不同, 已文件化
581
+ return [cvIn(field, v, path, false, kind, scale, pk)]
582
+ }
583
+ if (v instanceof RegExp) {
584
+ return [cvRegex(field, v, undefined, path, ctx, kind)]
585
+ }
586
+ if (!isPlainObj(v)) {
587
+ return [{ [field]: chkScalar(v, path, true, kind, scale, pk) }]
588
+ }
589
+ let ks = Reflect.ownKeys(v)
590
+ if (ks.length === 0) {
591
+ err('empty operator object', path)
592
+ }
593
+ let simple = {}
594
+ let nSimple = 0
595
+ let cs = []
596
+ let hasRegex = false
597
+ for (let k of ks) {
598
+ let kp = `${path}.${String(k)}`
599
+ let x = v[k]
600
+ let name = k
601
+ if (typeof k === 'symbol') {
602
+ if (!setOp.has(k) || setSymFieldNo.has(k)) {
603
+ err(`unsupported symbol operator [${String(k)}]`, kp)
604
+ }
605
+ name = mSymCmp.get(k)
606
+ if (name === undefined) {
607
+ //其餘sequelize之Symbol運算子為呼叫端自負之原生寫法, 值經檢核後各自成一子式
608
+ cs.push({ [field]: { [k]: chkRawSym(x, kp, kind, scale, pk) } })
609
+ continue
610
+ }
611
+ }
612
+ if (typeof name !== 'string' || name[0] !== '$') {
613
+ err('nested object is not supported, operators must start with $', kp)
614
+ }
615
+ if (mCmp[name] !== undefined) {
616
+ let allowNull = name === '$eq' || name === '$ne'
617
+ let y = chkScalar(x, kp, allowNull, kind, scale, pk)
618
+ if (Object.prototype.hasOwnProperty.call(simple, mCmp[name])) {
619
+ //同義之鍵(如$gt與Op.gt)各自成一子式, 不得互相覆寫
620
+ cs.push({ [field]: { [mCmp[name]]: y } })
621
+ }
622
+ else {
623
+ simple[mCmp[name]] = y
624
+ nSimple++
625
+ }
626
+ }
627
+ else if (name === '$in' || name === '$nin') {
628
+ cs.push(cvIn(field, x, kp, name === '$nin', kind, scale, pk))
629
+ }
630
+ else if (name === '$exists') {
631
+ if (x !== true && x !== false && x !== 0 && x !== 1) {
632
+ err('$exists must be a boolean, 0 or 1', kp)
633
+ }
634
+ cs.push(x ? { [field]: { [Op.ne]: null } } : { [field]: null })
635
+ }
636
+ else if (name === '$regex') {
637
+ hasRegex = true
638
+ cs.push(cvRegex(field, x, v.$options, kp, ctx, kind))
639
+ }
640
+ else if (name === '$options') {
641
+ //由$regex一併處理
642
+ }
643
+ else if (name === '$not') {
644
+ if (Array.isArray(x)) {
645
+ err('$not does not accept an array', kp)
646
+ }
647
+ let ns = cvField(field, x, kp, depth + 1, ctx)
648
+ cs.push({ [Op.not]: { [Op.and]: ns } })
649
+ }
650
+ else {
651
+ err(`unsupported operator [${name}]`, kp)
652
+ }
653
+ }
654
+ if (v.$options !== undefined && !hasRegex) {
655
+ err('$options requires $regex', path)
656
+ }
657
+ if (nSimple > 0) {
658
+ cs.unshift({ [field]: simple })
659
+ }
660
+ return cs
661
+ }
662
+
663
+
664
+ /**
665
+ * 文件層條件(含頂層邏輯運算子)
666
+ *
667
+ * @ignore
668
+ */
669
+ function cvDoc(o, path, depth, ctx) {
670
+ if (depth > nDepthMax) {
671
+ err('find is too deep', path)
672
+ }
673
+ if (!isPlainObj(o)) {
674
+ err('condition must be an object', path)
675
+ }
676
+ let cvList = (x, kp) => {
677
+ if (!Array.isArray(x)) {
678
+ err('logical operator requires an array', kp)
679
+ }
680
+ //空陣列拒收(同MongoDB), 以免呼叫端之條件清單恰為空時成為恆真而刪全表
681
+ if (x.length === 0) {
682
+ err('logical operator requires a nonempty array', kp)
683
+ }
684
+ return getItems(x, kp).map((y, i) => cvDoc(y, `${kp}.${i}`, depth + 1, ctx))
685
+ }
686
+ let cs = []
687
+ for (let k of Reflect.ownKeys(o)) {
688
+ let x = o[k]
689
+ let kp = path ? `${path}.${String(k)}` : String(k)
690
+ if (typeof k === 'symbol') {
691
+ if (k === Op.and) {
692
+ cs.push(cvAnd(cvList(x, kp)))
693
+ }
694
+ else if (k === Op.or) {
695
+ cs.push(cvOr(cvList(x, kp)))
696
+ }
697
+ else if (k === Op.not) {
698
+ cs.push({ [Op.not]: cvDoc(x, kp, depth + 1, ctx) })
699
+ }
700
+ else {
701
+ err(`unsupported symbol operator at document level [${String(k)}]`, kp)
702
+ }
703
+ continue
704
+ }
705
+ if (k[0] === '$') {
706
+ if (k === '$and') {
707
+ cs.push(cvAnd(cvList(x, kp)))
708
+ }
709
+ else if (k === '$or') {
710
+ cs.push(cvOr(cvList(x, kp)))
711
+ }
712
+ else if (k === '$nor') {
713
+ cs.push({ [Op.not]: cvOr(cvList(x, kp)) })
714
+ }
715
+ else {
716
+ err(`unsupported operator [${k}]`, kp)
717
+ }
718
+ continue
719
+ }
720
+ cs.push(...cvField(k, x, kp, depth + 1, ctx))
721
+ }
722
+ return cvAnd(cs)
723
+ }
724
+
725
+
726
+ /**
727
+ * 以AND結合子式, 無子式者為恆真
728
+ *
729
+ * @ignore
730
+ */
731
+ function cvAnd(cs) {
732
+ if (cs.length === 0) {
733
+ return { [Op.and]: [sqlTrue()] }
734
+ }
735
+ return { [Op.and]: cs }
736
+ }
737
+
738
+
739
+ /**
740
+ * 以OR結合子式, 無子式者為恆假
741
+ *
742
+ * @ignore
743
+ */
744
+ function cvOr(cs) {
745
+ if (cs.length === 0) {
746
+ return { [Op.and]: [sqlFalse()] }
747
+ }
748
+ return { [Op.or]: cs }
749
+ }
750
+
751
+
752
+ /**
753
+ * 將查詢條件轉為sequelize之where, 供select與delAll共用, 令同一find於兩函數由同一SQL謂詞決定命中
754
+ *
755
+ * 註: 支援欄位等值與$eq、$ne、$gt、$gte、$lt、$lte、$in、$nin、$not、$regex($options僅i)、$exists, 以及$and、$or、$nor
756
+ * 註: 不支援之運算子、非純量之值、與欄位型別不符之純量一律拋錯, 不得交sequelize之型別stringify(見本檔之chkScalar)
757
+ * 註: NULL依SQL三值邏輯; $regex僅支援字面子字串; 非物件之find回{}(即全部), 此為現況保留
758
+ *
759
+ * @param {*} find 輸入查詢條件
760
+ * @param {Object} ctx 輸入轉換所需之依賴物件
761
+ * @param {Function} ctx.escape 輸入依方言將字串轉為SQL字串常值之函數, 通常為sequelize實例之escape
762
+ * @param {Object} [ctx.attrs] 輸入model之rawAttributes, 供依欄位型別檢核純量與取得實際欄名, 未給時不檢型別
763
+ * @param {String} [ctx.dialect] 輸入資料庫種類字串, 供判定方言能力(如sqlite之LOWER僅折疊ASCII)
764
+ * @param {String} [ctx.pk] 輸入主鍵欄位名, 該欄之純量條件值依normPk正規化(如字串類主鍵欄之123即'123'), 未給時不正規化
765
+ * @returns {Object} 回傳sequelize之where物件, 條件不合法時拋出錯誤
766
+ */
767
+ function cvFind(find, ctx) {
768
+
769
+ //check, 非物件(undefined、null、字串、數值等純量)與陣列視為未給條件, 此為現況保留, 系列規格另案處理
770
+ if (find === null || typeof find !== 'object' || Array.isArray(find)) {
771
+ return {}
772
+ }
773
+
774
+ //check, 非一般物件(class實例、Object.create(proto)、Date、Map等)之條件可能不在自有鍵上, 拒收而不當成{}
775
+ if (!isPlainObj(find)) {
776
+ err('find must be a plain object')
777
+ }
778
+
779
+ //check, 空物件為全部
780
+ if (Reflect.ownKeys(find).length === 0) {
781
+ return {}
782
+ }
783
+
784
+ return cvDoc(find, '', 0, ctx)
785
+ }
786
+
787
+
788
+ export { getColKind, getColScale, normPk }
789
+ export default cvFind