w-orm-reladb 1.1.6 → 1.1.8
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 +25 -3
- package/dist/w-orm-reladb.umd.js +2 -2
- package/dist/w-orm-reladb.umd.js.map +1 -1
- package/docs/WOrmReladb.html +39 -21
- package/docs/WOrmReladb.mjs.html +206 -131
- package/docs/cvFind.mjs.html +861 -0
- package/docs/global.html +1716 -0
- package/docs/importModels.mjs.html +255 -0
- package/docs/index.html +2 -2
- package/g-mssql.mjs +1 -1
- package/g-sqlite-encryption.mjs +1 -1
- package/g-sqlite.mjs +1 -1
- package/models/tcond.js +74 -0
- package/models/tpkdec.js +20 -0
- package/models/tpkint.js +20 -0
- package/package.json +3 -3
- package/src/WOrmReladb.mjs +204 -129
- package/src/cvFind.mjs +789 -0
- package/src/importModels.mjs +180 -114
- package/src/requireModel.js +13 -5
- package/test/api-dist.test.mjs +112 -0
- package/test/api-init.test.mjs +50 -0
- package/test/api-models.test.mjs +216 -0
- package/test/api-suite.mjs +492 -3
- package/test/tools/modelsWorker.mjs +35 -0
- package/test/unit-cvfind.test.mjs +393 -0
- package/toolg/gDistRollup.mjs +2 -0
- package/src/modifyModel.mjs +0 -113
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
|