w-orm-reladb 1.1.8 → 1.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -28
- package/dist/w-orm-reladb.umd.js +2 -2
- package/dist/w-orm-reladb.umd.js.map +1 -1
- package/docs/WOrmReladb.html +52 -30
- package/docs/WOrmReladb.mjs.html +84 -30
- package/docs/cvFind.mjs.html +349 -382
- package/docs/global.html +49 -12
- package/docs/importModels.mjs.html +1 -1
- package/docs/index.html +1 -1
- package/g-mssql-transaction-commit.mjs +1 -1
- package/g-mssql-transaction-rollback.mjs +1 -1
- package/g-mssql.mjs +4 -4
- package/g-sqlite-encryption-transaction-commit.mjs +1 -1
- package/g-sqlite-encryption.mjs +4 -4
- package/g-sqlite-transaction-commit.mjs +1 -1
- package/g-sqlite-transaction-rollback.mjs +1 -1
- package/g-sqlite.mjs +4 -4
- package/models/tfind.js +50 -0
- package/package.json +2 -2
- package/src/WOrmReladb.mjs +83 -29
- package/src/cvFind.mjs +348 -381
- package/test/api-find-whitelist.mjs +390 -0
- package/test/api-models.test.mjs +23 -0
- package/test/api-mssql.test.mjs +2 -0
- package/test/api-sqlite.test.mjs +2 -0
- package/test/api-suite.mjs +560 -76
- package/test/unit-cvfind.test.mjs +928 -104
package/src/cvFind.mjs
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import Sequelize from 'sequelize'
|
|
2
|
+
import cvOrmFind from 'wsemi/src/cvOrmFind.mjs'
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
//查詢條件(find)之轉譯, 供select與delAll共用, 令同一find於兩函數由同一SQL謂詞決定命中
|
|
5
6
|
//
|
|
6
|
-
|
|
7
|
-
// 1.
|
|
8
|
-
//
|
|
9
|
-
// 2.
|
|
10
|
-
//
|
|
11
|
-
|
|
7
|
+
//分兩階段(規則見CLAUDE_rulebook.md之R11):
|
|
8
|
+
// 1. 形狀檢核: 第一步經wsemi之cvOrmFind依系列白名單檢核並回傳全新之null原型深複本(檢查之值即使用之值), 之後只走訪複本;
|
|
9
|
+
// 白名單外之運算子、非純量之值、非條件物件之find、異常結構(存取器、Symbol鍵、洞…)一律整批reject, 不靜默忽略、不改變語意、不擴大刪除範圍
|
|
10
|
+
// 2. 翻譯: 本模組負責後端專屬之檢核與轉譯——欄名嚴格鍵、依欄位型別檢核純量、主鍵正規化(R8)、DATE範圍與ctx.dateNoTz、
|
|
11
|
+
// 空集合之欄位引用($in:[]), 見R3–R5、R8
|
|
12
|
+
//歷史: 原select內之cvObj以lodash之each迭代並以Op[k]直譯$鍵, 不支援之運算子成為鍵'undefined'、具數值length之物件被當成陣列樣迭代而丟棄全部條件,
|
|
13
|
+
//不得供破壞性之delAll使用, 1.1.7起改自寫白名單; 第33項批1起形狀層改由cvOrmFind單一擁有, 本模組刪除Symbol/Op全集、$not $nor $regex $options $exists、
|
|
14
|
+
//RegExp值、陣列值即$in、非物件視為{}等形狀層(破壞性變更見CLAUDE_rulebook.md與README)
|
|
12
15
|
//主鍵值之正規化(normPk)亦屬本模組, 供主鍵函數與主鍵欄條件共用, 規則見R8
|
|
13
16
|
|
|
14
17
|
|
|
@@ -16,8 +19,17 @@ import Sequelize from 'sequelize'
|
|
|
16
19
|
let Op = Sequelize.Op
|
|
17
20
|
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
//形狀檢核之上限(傳給cvOrmFind): $and/$or長度999——SQLite平鋪$or/$and恰1000項即Expression tree is too large(DB錯誤), 本套件使用SQLite故下調(預設1000);
|
|
23
|
+
//其餘上限用預設: 邏輯巢狀深度32、$in/$nin長度10000、節點總數100000、欄名128
|
|
24
|
+
let optShape = { maxList: 999 }
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
//$and/$or清單(含同一條件物件內並列欄位之隱含AND)項數超過此值者, 遞迴二分為平衡樹再交sequelize:
|
|
28
|
+
//SQLite之運算式樹深度上限1000(Expression tree is too large), 平鋪之a OR b OR …為左深樹(深度隨項數線性成長), select因欄位帶表別名(`t`.`c`)而較delAll少容1項
|
|
29
|
+
//(實測平鋪$or: select最多998項、delAll最多999項, 999項時select報DB錯誤而delAll照常刪除, 違反select命中即delAll刪除之對象一致);
|
|
30
|
+
//巢狀層數亦會累加(32層 × 每層33項平鋪即超限, 外審sol F1), 另有巢狀遞迴上限(實測右深巢狀OR約830層, SQLITE_ERROR: Recursion limit);
|
|
31
|
+
//二分後每個清單之樹深僅約log2(項數)+3, 邏輯巢狀上限32層下全樹深度恆遠低於兩種上限, select與delAll皆可執行
|
|
32
|
+
let nFlat = 4
|
|
21
33
|
|
|
22
34
|
|
|
23
35
|
//比較運算子, 值須為純量(null僅$eq與$ne)
|
|
@@ -31,32 +43,6 @@ let mCmp = {
|
|
|
31
43
|
}
|
|
32
44
|
|
|
33
45
|
|
|
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
46
|
//欄位型別分類, 以sequelize之型別類別判定(各方言之型別皆繼承之), 另以type.key補判
|
|
61
47
|
//註: DataTypes.DOUBLE之key為'DOUBLE PRECISION', 只比對key會漏判而不檢型別
|
|
62
48
|
let DT = Sequelize.DataTypes
|
|
@@ -87,52 +73,73 @@ let reIsoTz = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(:(\d{2})(\.\d+)?)?(Z|
|
|
|
87
73
|
let reDateOnly = /^(\d{4})-(\d{2})-(\d{2})$/
|
|
88
74
|
|
|
89
75
|
|
|
76
|
+
//無時區之ISO 8601日期時間字串(秒與小數秒可省), 僅呼叫端以ctx.dateNoTz明示'local'時收
|
|
77
|
+
let reIsoNoTz = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?$/
|
|
78
|
+
|
|
79
|
+
|
|
90
80
|
/**
|
|
91
|
-
*
|
|
81
|
+
* 依主機時區解析無時區之ISO 8601日期時間字串為Date, 不合法(格式不符、曆法上不存在、主機時區下不存在)者回傳null
|
|
82
|
+
* 註: 不用Date.parse, 因帶空白分隔等寫法屬實作定義; 以本地時間建構後逐欄回驗, 如2026-02-30、24:00:00、60秒、夏令跳過之時刻皆為null
|
|
83
|
+
* 註: 小數秒取前三位(截斷至毫秒); 夏令重疊之時刻取較早者(Date之行為)
|
|
92
84
|
*
|
|
93
85
|
* @ignore
|
|
94
86
|
*/
|
|
95
|
-
function
|
|
96
|
-
let
|
|
97
|
-
|
|
87
|
+
function parseNoTzLocal(s) {
|
|
88
|
+
let m = s.match(reIsoNoTz)
|
|
89
|
+
if (m === null) {
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
let y = Number(m[1])
|
|
93
|
+
let mo = Number(m[2])
|
|
94
|
+
let d = Number(m[3])
|
|
95
|
+
let h = Number(m[4])
|
|
96
|
+
let mi = Number(m[5])
|
|
97
|
+
let sec = m[6] === undefined ? 0 : Number(m[6])
|
|
98
|
+
let ms = m[7] === undefined ? 0 : Number(`${m[7]}00`.slice(0, 3))
|
|
99
|
+
let t = new Date(y, mo - 1, d, h, mi, sec, ms)
|
|
100
|
+
if (t.getFullYear() !== y || t.getMonth() !== mo - 1 || t.getDate() !== d || t.getHours() !== h || t.getMinutes() !== mi || t.getSeconds() !== sec || t.getMilliseconds() !== ms) {
|
|
101
|
+
return null
|
|
102
|
+
}
|
|
103
|
+
if (!inDateRange(t.getTime())) {
|
|
104
|
+
return null //本地時間通過回驗, 但換算UTC後年份超出1–9999(如負偏移時區之9999-12-31晚間)
|
|
105
|
+
}
|
|
106
|
+
return t
|
|
98
107
|
}
|
|
99
108
|
|
|
100
109
|
|
|
101
110
|
/**
|
|
102
|
-
*
|
|
111
|
+
* 判定年月日是否為曆法上存在之日期, 如2026-02-30不存在; 年須在1以上(年0000不收)
|
|
112
|
+
* 註: 不用Date.UTC(y,..)建構, 因其把0–99年映射為1900–1999而使年0001–0099之字串被誤拒; 以setUTCFullYear設定完整年份
|
|
103
113
|
*
|
|
104
114
|
* @ignore
|
|
105
115
|
*/
|
|
106
|
-
function
|
|
107
|
-
|
|
116
|
+
function isCalendarDate(y, m, d) {
|
|
117
|
+
let yn = Number(y)
|
|
118
|
+
if (yn < 1) {
|
|
119
|
+
return false
|
|
120
|
+
}
|
|
121
|
+
let t = new Date(0)
|
|
122
|
+
t.setUTCFullYear(yn, Number(m) - 1, Number(d))
|
|
123
|
+
return t.getUTCFullYear() === yn && t.getUTCMonth() === Number(m) - 1 && t.getUTCDate() === Number(d)
|
|
108
124
|
}
|
|
109
125
|
|
|
110
126
|
|
|
111
127
|
/**
|
|
112
|
-
*
|
|
113
|
-
* 註: 不以toString判定, 因帶Symbol.toStringTag之一般物件會被誤判為非物件, 而class實例、Object.create(proto)會被誤判為一般物件,
|
|
114
|
-
* 其條件可能放在原型上而取不到自有鍵, 被當成{}而刪全表
|
|
128
|
+
* 拋出條件錯誤
|
|
115
129
|
*
|
|
116
130
|
* @ignore
|
|
117
131
|
*/
|
|
118
|
-
function
|
|
119
|
-
|
|
120
|
-
return false
|
|
121
|
-
}
|
|
122
|
-
let p = Object.getPrototypeOf(v)
|
|
123
|
-
return p === Object.prototype || p === null
|
|
132
|
+
function err(msg, path) {
|
|
133
|
+
throw new Error(`invalid find: ${msg}${path ? ` at [${path}]` : ''}`)
|
|
124
134
|
}
|
|
125
135
|
|
|
126
136
|
|
|
127
137
|
/**
|
|
128
|
-
*
|
|
129
|
-
* 註: sequelize
|
|
138
|
+
* 恆假之謂詞, 以明確之SQL表示, 不以空條件表示
|
|
139
|
+
* 註: sequelize會把空條件於OR、NOT內濾除而改變語意; 本模組僅用於$in:[]之f IS NULL AND 1 = 0(仍引用欄位, 見R5)
|
|
130
140
|
*
|
|
131
141
|
* @ignore
|
|
132
142
|
*/
|
|
133
|
-
function sqlTrue() {
|
|
134
|
-
return Sequelize.literal('1 = 1')
|
|
135
|
-
}
|
|
136
143
|
function sqlFalse() {
|
|
137
144
|
return Sequelize.literal('1 = 0')
|
|
138
145
|
}
|
|
@@ -173,6 +180,149 @@ function getColKind(attrs, field) {
|
|
|
173
180
|
}
|
|
174
181
|
|
|
175
182
|
|
|
183
|
+
/**
|
|
184
|
+
* 取model屬性之資料庫欄名: attr.field(字串)優先, 否則為屬性名(sequelize define後恆有field, 手造attrs可能缺)
|
|
185
|
+
*
|
|
186
|
+
* @ignore
|
|
187
|
+
*/
|
|
188
|
+
function colOf(attrs, k) {
|
|
189
|
+
return attrs[k] && typeof attrs[k].field === 'string' ? attrs[k].field : k
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* 建立attrs之索引: 資料庫欄名 → 屬性名陣列(依定義順序), 供欄名解析與sequelize反查之核對
|
|
195
|
+
*
|
|
196
|
+
* @ignore
|
|
197
|
+
*/
|
|
198
|
+
function buildAttrIdx(attrs) {
|
|
199
|
+
let byCol = new Map()
|
|
200
|
+
for (let k of Object.keys(attrs)) {
|
|
201
|
+
let c = colOf(attrs, k)
|
|
202
|
+
let a = byCol.get(c)
|
|
203
|
+
if (a === undefined) {
|
|
204
|
+
a = []
|
|
205
|
+
byCol.set(c, a)
|
|
206
|
+
}
|
|
207
|
+
a.push(k)
|
|
208
|
+
}
|
|
209
|
+
return { byCol }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 核對attrs為sequelize之rawAttributes形狀: 一般物件(原型為Object.prototype或null)、無Symbol鍵、自有字串鍵皆可列舉
|
|
215
|
+
* 註: sequelize以rawAttributes[鍵]讀取(含原型鏈、Symbol鍵、不可列舉者), 而cvFind以Object.keys建索引、以自有屬性核對; 三者所見之屬性集合不同時,
|
|
216
|
+
* 反查核對與sequelize實際取用之定義分歧(如與Op.and相撞之Symbol屬性使sequelize把邏輯鍵映射為欄而條件消失); 真正之rawAttributes恆符合
|
|
217
|
+
*
|
|
218
|
+
* @ignore
|
|
219
|
+
*/
|
|
220
|
+
function chkAttrs(attrs) {
|
|
221
|
+
let p = typeof attrs === 'object' ? Object.getPrototypeOf(attrs) : undefined
|
|
222
|
+
if (!(p === Object.prototype || p === null) || Object.getOwnPropertySymbols(attrs).length > 0 || Object.getOwnPropertyNames(attrs).length !== Object.keys(attrs).length) {
|
|
223
|
+
throw new Error('invalid ctx: attrs must be a plain object (prototype Object.prototype or null) without symbol keys and with enumerable string keys only')
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 核對欄名可安全交sequelize: 下列名稱會被sequelize於轉成SQL前改寫或重新解讀, 使資料庫實際比對之欄不是此名, 一律拒收
|
|
230
|
+
* 註: 空字串者(sequelize之_joinKeyValue遇空鍵只回值, 條件成常數而delAll刪全表)、含反引號/雙引號/單引號/方括號者(quoteIdentifier依方言刪除之, 如sqlite之`val`ue`成value)、含點者(JSON路徑、表.欄)、
|
|
231
|
+
* 前後皆$者($x$被當成欄引用; sequelize之isColString只比首尾字元, 故含換行者與單一$亦是, 不以正規式判定以免.不匹配換行)、
|
|
232
|
+
* Object.prototype之成員名與__proto__(sequelize以一般物件查表與賦值: 查到函數而不依欄型序列化、賦值__proto__使條件遺失而刪全表)
|
|
233
|
+
*
|
|
234
|
+
* @ignore
|
|
235
|
+
*/
|
|
236
|
+
function chkColName(n, path) {
|
|
237
|
+
if (n === '' || n in Object.prototype || n.indexOf('.') >= 0 || /[`"'[\]]/.test(n) || (n[0] === '$' && n[n.length - 1] === '$')) {
|
|
238
|
+
err(`unsupported column name [${n}]`, path)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* 屬性是否為VIRTUAL(非資料庫欄)
|
|
245
|
+
*
|
|
246
|
+
* @ignore
|
|
247
|
+
*/
|
|
248
|
+
function isVirtualAttr(at) {
|
|
249
|
+
let t = at && at.type
|
|
250
|
+
return !!t && (t instanceof DT.VIRTUAL || t.key === 'VIRTUAL')
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* 將條件之欄名解析為model之屬性名(嚴格鍵): 僅收屬性名完全相符、或資料庫欄名(attr.field)完全相符且唯一者, 其餘一律拒收, 不模擬資料庫之識別字解析
|
|
256
|
+
* 註: 欄名經三方各自解析: cvFind(判型別、主鍵、DATE值)、sequelize(屬性名映射欄名、以映射後之名稱反查屬性型別、引用層改寫字元)、資料庫(依定序折疊);
|
|
257
|
+
* 完全相符之名稱無論資料庫如何折疊皆解析為自己, 故只收完全相符者(另核對sequelize之反查與改寫, 見chkColName與下文), 使三方之解析必然一致, 見CLAUDE_rulebook.md之R4欄名解析
|
|
258
|
+
* 註: 以model之attrs為準, attrs缺少之欄與VIRTUAL屬性皆拒收; attrs未給者回傳null(不解析, 欄位型別為any, 交資料庫處置)
|
|
259
|
+
*
|
|
260
|
+
* @ignore
|
|
261
|
+
*/
|
|
262
|
+
function resolveAttr(attrs, idx, field, path) {
|
|
263
|
+
if (!attrs) {
|
|
264
|
+
return null
|
|
265
|
+
}
|
|
266
|
+
chkColName(field, path)
|
|
267
|
+
|
|
268
|
+
//①屬性名完全相符; ②資料庫欄名完全相符(多於一個者無從判定)
|
|
269
|
+
let name = null
|
|
270
|
+
if (Object.prototype.hasOwnProperty.call(attrs, field)) {
|
|
271
|
+
name = field
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
let hits = idx.byCol.get(field) || []
|
|
275
|
+
if (hits.length > 1) {
|
|
276
|
+
err(`ambiguous column name [${field}]`, path)
|
|
277
|
+
}
|
|
278
|
+
if (hits.length === 1) {
|
|
279
|
+
name = hits[0]
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
//其餘拒收; 提示僅供診斷(不分大小寫相同者), 不影響解析
|
|
284
|
+
if (name === null || isVirtualAttr(attrs[name])) {
|
|
285
|
+
let lf = field.toLowerCase()
|
|
286
|
+
let near = Object.keys(attrs).find((k) => !isVirtualAttr(attrs[k]) && (k.toLowerCase() === lf || colOf(attrs, k).toLowerCase() === lf))
|
|
287
|
+
err(`unknown column name [${field}]${near === undefined ? '' : `, did you mean [${near}]?`}`, path)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
//field已設者須為字串(sequelize保留原值並以String(field)映射, 退回屬性名則二者指向不同欄); 空字串由chkColName拒收
|
|
291
|
+
let at = attrs[name]
|
|
292
|
+
if (at && at.field !== undefined && typeof at.field !== 'string') {
|
|
293
|
+
err(`unsupported column name [${name}], its field must be a string`, path)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
//sequelize把屬性名之鍵映射為其欄名後, 以rawAttributes[欄名](名稱為該欄名之屬性)、其次fieldRawAttributesMap[欄名](欄名為該欄名之屬性, 多個取最後定義者)取得型別序列化值;
|
|
297
|
+
//反查所得須即本屬性, 否則值依他屬性之型別序列化(如DATE之值被INTEGER序列化為Date.toString文字, sqlite以文字比較而命中全部非NULL列), 拒收
|
|
298
|
+
//共用一欄者另須本屬性即最後定義者: 實體欄之型別由最後定義者決定(sync以欄名為鍵而後者覆寫), 先定義者即使反查為自己, sequelize之序列化亦與實體欄不符
|
|
299
|
+
let col = colOf(attrs, name)
|
|
300
|
+
chkColName(col, path)
|
|
301
|
+
let cs = idx.byCol.get(col)
|
|
302
|
+
let last = cs[cs.length - 1]
|
|
303
|
+
let s = Object.prototype.hasOwnProperty.call(attrs, col) ? col : last
|
|
304
|
+
if (s !== name) {
|
|
305
|
+
err(`ambiguous column name [${field}], its column [${col}] resolves to attribute [${s}]`, path)
|
|
306
|
+
}
|
|
307
|
+
if (last !== name) {
|
|
308
|
+
err(`ambiguous column name [${field}], its column [${col}] is shared with attribute [${last}], the last attribute defines the column type`, path)
|
|
309
|
+
}
|
|
310
|
+
return name
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* 判定時間(毫秒)換算UTC後年份是否在1–9999
|
|
316
|
+
* 註: sequelize以YYYY格式化DATE, 年10000起為5位數, sqlite以文字比較致{d:{$gt:年10000}}命中全部日期列; mssql之datetimeoffset範圍亦為0001–9999
|
|
317
|
+
*
|
|
318
|
+
* @ignore
|
|
319
|
+
*/
|
|
320
|
+
function inDateRange(ms) {
|
|
321
|
+
let y = new Date(ms).getUTCFullYear()
|
|
322
|
+
return y >= 1 && y <= 9999
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
|
|
176
326
|
/**
|
|
177
327
|
* 取DECIMAL欄之scale, 未指定者為0(mssql之DECIMAL預設為(18,0)), 非DECIMAL者為null
|
|
178
328
|
*
|
|
@@ -228,18 +378,23 @@ function isKindOk(v, kind, scale = null) {
|
|
|
228
378
|
return true
|
|
229
379
|
}
|
|
230
380
|
if (kind === 'date') {
|
|
381
|
+
//所有形式換算UTC後年份須在1–9999(inDateRange)
|
|
231
382
|
if (v instanceof Date) {
|
|
232
|
-
return !isNaN(v.getTime())
|
|
383
|
+
return !isNaN(v.getTime()) && inDateRange(v.getTime())
|
|
233
384
|
}
|
|
234
385
|
if (typeof v === 'number') {
|
|
235
386
|
//須在Date可表示之範圍(±8.64e15毫秒)內, 超出者被stringify為'Invalid date'而以文字比較, sqlite會命中全部日期列
|
|
236
|
-
return Number.isFinite(v) && !isNaN(new Date(v).getTime())
|
|
387
|
+
return Number.isFinite(v) && !isNaN(new Date(v).getTime()) && inDateRange(v)
|
|
237
388
|
}
|
|
238
389
|
if (typeof v !== 'string') {
|
|
239
390
|
return false
|
|
240
391
|
}
|
|
241
392
|
let m = v.match(reIsoTz)
|
|
242
|
-
|
|
393
|
+
if (m === null || !isCalendarDate(m[1], m[2], m[3])) {
|
|
394
|
+
return false
|
|
395
|
+
}
|
|
396
|
+
let ms = Date.parse(v.replace(' ', 'T'))
|
|
397
|
+
return !isNaN(ms) && inDateRange(ms)
|
|
243
398
|
}
|
|
244
399
|
if (kind === 'dateonly') {
|
|
245
400
|
//Date物件依主機時區取日期而結果隨主機而變, 故只收日期字串
|
|
@@ -339,42 +494,31 @@ function normPk(v, kind, scale = null) {
|
|
|
339
494
|
return { kind: 'invalid' }
|
|
340
495
|
}
|
|
341
496
|
|
|
342
|
-
|
|
343
497
|
/**
|
|
344
|
-
* 檢核純量值: 字串、有限數值、布林、有效Date
|
|
345
|
-
* 註: 物件、陣列等不得交sequelize之型別stringify, 否則FLOAT成'NaN'、INTEGER成'[object Object]'、DATE成本地今日00:00
|
|
498
|
+
* 檢核純量值: 字串、有限數值、布林、有效Date, 以及null(是否允許null已由cvOrmFind依運算子判定); 並須符合欄位型別分類
|
|
499
|
+
* 註: 物件、陣列等不得交sequelize之型別stringify, 否則FLOAT成'NaN'、INTEGER成'[object Object]'、DATE成本地今日00:00而誤刪;
|
|
500
|
+
* 形狀層(cvOrmFind)已保證僅此類值到達, 此處保留型別檢核作為fail-closed之防線(日後上游放寬時不得靜默放行)
|
|
346
501
|
* 註: 主鍵欄(pk為true)之字串類、整數類與其他數值類以normPk正規化後回傳(如字串類主鍵欄之123即'123'), 無效或型別不符者拒收,
|
|
347
502
|
* 令select與delAll對主鍵之認定與selectByPk、del等主鍵函數一致; 非主鍵欄不轉型
|
|
503
|
+
* 註: kind為date且呼叫端以ctx.dateNoTz明示'local'(dateNoTz為'local')時, 無時區日期時間字串依主機時區轉為Date回傳(見parseNoTzLocal), 其餘值照舊判定
|
|
348
504
|
*
|
|
349
505
|
* @ignore
|
|
350
506
|
*/
|
|
351
|
-
function chkScalar(v, path,
|
|
507
|
+
function chkScalar(v, path, kind = 'any', scale = null, pk = false, dateNoTz = undefined) {
|
|
352
508
|
if (v === null) {
|
|
353
|
-
|
|
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
|
|
509
|
+
return v
|
|
361
510
|
}
|
|
362
|
-
|
|
511
|
+
if (typeof v === 'number') {
|
|
363
512
|
if (!Number.isFinite(v)) {
|
|
364
513
|
err(`number must be finite, got [${v}]`, path)
|
|
365
514
|
}
|
|
366
|
-
ok = true
|
|
367
515
|
}
|
|
368
516
|
else if (v instanceof Date) {
|
|
369
517
|
if (isNaN(v.getTime())) {
|
|
370
518
|
err('invalid Date', path)
|
|
371
519
|
}
|
|
372
|
-
ok = true
|
|
373
|
-
}
|
|
374
|
-
else if (Buffer.isBuffer(v)) {
|
|
375
|
-
ok = kind === 'any'
|
|
376
520
|
}
|
|
377
|
-
if (
|
|
521
|
+
else if (typeof v !== 'string' && typeof v !== 'boolean') {
|
|
378
522
|
err(`value must be a string, finite number, boolean, Date or null, got [${Object.prototype.toString.call(v)}]`, path)
|
|
379
523
|
}
|
|
380
524
|
if (pk && (kind === 'str' || kind === 'int' || kind === 'num')) {
|
|
@@ -384,406 +528,229 @@ function chkScalar(v, path, allowNull, kind = 'any', scale = null, pk = false) {
|
|
|
384
528
|
}
|
|
385
529
|
err(`value [${String(v)}] does not match column type [${kind}]${scale !== null ? ` with scale [${scale}]` : ''}`, path)
|
|
386
530
|
}
|
|
531
|
+
if (kind === 'date' && dateNoTz === 'local' && typeof v === 'string') {
|
|
532
|
+
//呼叫端明示之無時區日期時間字串: 依主機時區解析並以Date送出, 與直接給Date同義; 不符者續由isKindOk判定(帶時區者照舊)
|
|
533
|
+
let t = parseNoTzLocal(v)
|
|
534
|
+
if (t !== null) {
|
|
535
|
+
return t
|
|
536
|
+
}
|
|
537
|
+
}
|
|
387
538
|
if (!isKindOk(v, kind, scale)) {
|
|
388
539
|
err(`value [${String(v)}] does not match column type [${kind}]${scale !== null ? ` with scale [${scale}]` : ''}`, path)
|
|
389
540
|
}
|
|
541
|
+
if (kind === 'date' && !(v instanceof Date)) {
|
|
542
|
+
//通過檢核之字串與epoch一律轉為Date送出: sequelize只在鍵為屬性名或欄名完全相符時依欄型序列化值, 大小寫變體鍵會原樣送出字串與epoch,
|
|
543
|
+
//sqlite以文字或型別序比較而命中錯列(如{D:{$gt:epoch}}刪光全部日期列); 轉為Date後與鍵之拼寫無關
|
|
544
|
+
return new Date(typeof v === 'string' ? Date.parse(v.replace(' ', 'T')) : v)
|
|
545
|
+
}
|
|
390
546
|
return v
|
|
391
547
|
}
|
|
392
548
|
|
|
393
549
|
|
|
394
550
|
/**
|
|
395
|
-
*
|
|
396
|
-
* 註:
|
|
551
|
+
* $in與$nin, 元素為純量(不含null, 已由cvOrmFind保證); 空$in以引用欄位之恆假表示
|
|
552
|
+
* 註: $nin:[]已由cvOrmFind拒收(恆真, 易因清單恰為空而刪全表); 此處再擋以免上游放寬時靜默放行
|
|
397
553
|
*
|
|
398
554
|
* @ignore
|
|
399
555
|
*/
|
|
400
|
-
function
|
|
401
|
-
let
|
|
556
|
+
function cvIn(field, arr, path, neg, kind, scale, pk, dateNoTz) {
|
|
557
|
+
let vs = []
|
|
402
558
|
for (let i = 0; i < arr.length; i++) {
|
|
403
|
-
if (
|
|
404
|
-
err('
|
|
559
|
+
if (arr[i] === null) {
|
|
560
|
+
err('null is not allowed here', `${path}[${i}]`)
|
|
405
561
|
}
|
|
406
|
-
|
|
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
|
|
562
|
+
vs.push(chkScalar(arr[i], `${path}[${i}]`, kind, scale, pk, dateNoTz))
|
|
421
563
|
}
|
|
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
564
|
if (!neg) {
|
|
547
565
|
if (vs.length === 0) {
|
|
548
|
-
|
|
566
|
+
//f IS NULL恆為二值(永不為UNKNOWN), 整式二值, 且欄名會出現在SQL中, 不存在之欄由資料庫回報(R5)
|
|
567
|
+
return { [Op.and]: [{ [field]: null }, sqlFalse()] }
|
|
549
568
|
}
|
|
550
|
-
|
|
551
|
-
return hasNull ? { [Op.or]: [c, { [field]: null }] } : c
|
|
569
|
+
return { [field]: { [Op.in]: vs } }
|
|
552
570
|
}
|
|
553
571
|
if (vs.length === 0) {
|
|
554
|
-
|
|
572
|
+
err('$nin with empty array is always true', path)
|
|
555
573
|
}
|
|
556
|
-
|
|
557
|
-
return hasNull ? { [Op.and]: [c, { [field]: { [Op.ne]: null } }] } : c
|
|
574
|
+
return { [field]: { [Op.notIn]: vs } }
|
|
558
575
|
}
|
|
559
576
|
|
|
560
577
|
|
|
561
578
|
/**
|
|
562
|
-
* 欄位層條件, 回傳以AND
|
|
579
|
+
* 欄位層條件, 回傳以AND結合之子式陣列; v為純量、null、Date(等值)或運算子物件(null原型, 鍵為$eq $ne $gt $gte $lt $lte $in $nin)
|
|
563
580
|
*
|
|
564
581
|
* @ignore
|
|
565
582
|
*/
|
|
566
|
-
function cvField(field, v, path,
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
let
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
if (v ===
|
|
577
|
-
|
|
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)
|
|
583
|
+
function cvField(field, v, path, ctx) {
|
|
584
|
+
|
|
585
|
+
//欄名先解析為model之屬性名再判型別與主鍵(R4欄名解析); attrs已給而解析不到者拒收, attrs未給者為any, 交資料庫處置
|
|
586
|
+
let an = resolveAttr(ctx.attrs, ctx.attrIdx, field, path)
|
|
587
|
+
let kind = an === null ? 'any' : getColKind(ctx.attrs, an)
|
|
588
|
+
let scale = an === null ? null : getColScale(ctx.attrs, an)
|
|
589
|
+
let pk = ctx.pk !== undefined && an === ctx.pk //主鍵欄之純量依normPk正規化(R8)
|
|
590
|
+
let dateNoTz = ctx.dateNoTz
|
|
591
|
+
|
|
592
|
+
//欄位直給(等值)
|
|
593
|
+
if (v === null || typeof v !== 'object' || v instanceof Date) {
|
|
594
|
+
return [{ [field]: chkScalar(v, path, kind, scale, pk, dateNoTz) }]
|
|
592
595
|
}
|
|
596
|
+
|
|
597
|
+
//運算子物件
|
|
593
598
|
let simple = {}
|
|
594
599
|
let nSimple = 0
|
|
595
600
|
let cs = []
|
|
596
|
-
let
|
|
597
|
-
|
|
598
|
-
let kp = `${path}.${String(k)}`
|
|
601
|
+
for (let k of Object.keys(v)) {
|
|
602
|
+
let kp = `${path}.${k}`
|
|
599
603
|
let x = v[k]
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
err(`unsupported symbol operator [${String(k)}]`, kp)
|
|
604
|
+
if (k === '$in' || k === '$nin') {
|
|
605
|
+
if (!Array.isArray(x)) {
|
|
606
|
+
err(`${k} must be an array`, kp)
|
|
604
607
|
}
|
|
605
|
-
|
|
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)
|
|
608
|
+
cs.push(cvIn(field, x, kp, k === '$nin', kind, scale, pk, dateNoTz))
|
|
614
609
|
}
|
|
615
|
-
if (mCmp
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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 } })
|
|
610
|
+
else if (Object.prototype.hasOwnProperty.call(mCmp, k)) {
|
|
611
|
+
//同欄之比較運算子併入同一欄位物件, 以AND結合
|
|
612
|
+
simple[mCmp[k]] = chkScalar(x, kp, kind, scale, pk, dateNoTz)
|
|
613
|
+
nSimple++
|
|
649
614
|
}
|
|
650
615
|
else {
|
|
651
|
-
err(`unsupported operator [${
|
|
616
|
+
err(`unsupported operator [${k}]`, kp)
|
|
652
617
|
}
|
|
653
618
|
}
|
|
654
|
-
if (v.$options !== undefined && !hasRegex) {
|
|
655
|
-
err('$options requires $regex', path)
|
|
656
|
-
}
|
|
657
619
|
if (nSimple > 0) {
|
|
658
620
|
cs.unshift({ [field]: simple })
|
|
659
621
|
}
|
|
622
|
+
if (cs.length === 0) {
|
|
623
|
+
//不產生任何子式者條件即消失而成恆真, delAll會刪全表; cvOrmFind已拒收空運算子物件, 此處再擋(含非Date之物件值)
|
|
624
|
+
err('operator object has no operator', path)
|
|
625
|
+
}
|
|
660
626
|
return cs
|
|
661
627
|
}
|
|
662
628
|
|
|
663
629
|
|
|
664
630
|
/**
|
|
665
|
-
* 文件層條件(
|
|
631
|
+
* 文件層條件(含$and、$or), o為cvOrmFind之複本(null原型, 鍵僅字串)
|
|
666
632
|
*
|
|
667
633
|
* @ignore
|
|
668
634
|
*/
|
|
669
|
-
function cvDoc(o, path,
|
|
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
|
-
}
|
|
635
|
+
function cvDoc(o, path, ctx) {
|
|
686
636
|
let cs = []
|
|
687
|
-
for (let k of
|
|
637
|
+
for (let k of Object.keys(o)) {
|
|
688
638
|
let x = o[k]
|
|
689
|
-
let kp =
|
|
690
|
-
if (
|
|
691
|
-
if (
|
|
692
|
-
|
|
693
|
-
}
|
|
694
|
-
else if (k === Op.or) {
|
|
695
|
-
cs.push(cvOr(cvList(x, kp)))
|
|
639
|
+
let kp = `${path}.${k}`
|
|
640
|
+
if (k === '$and' || k === '$or') {
|
|
641
|
+
if (!Array.isArray(x) || x.length === 0) {
|
|
642
|
+
err(`${k} requires a nonempty array`, kp)
|
|
696
643
|
}
|
|
697
|
-
|
|
698
|
-
|
|
644
|
+
let sub = []
|
|
645
|
+
for (let i = 0; i < x.length; i++) {
|
|
646
|
+
sub.push(cvDoc(x[i], `${kp}[${i}]`, ctx))
|
|
699
647
|
}
|
|
700
|
-
|
|
701
|
-
err(`unsupported symbol operator at document level [${String(k)}]`, kp)
|
|
702
|
-
}
|
|
703
|
-
continue
|
|
648
|
+
cs.push(cvGroup(k === '$and' ? cvAnd : cvOr, sub))
|
|
704
649
|
}
|
|
705
|
-
if (k[0] === '$') {
|
|
706
|
-
|
|
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
|
|
650
|
+
else if (k[0] === '$') {
|
|
651
|
+
err(`unsupported operator [${k}]`, kp)
|
|
719
652
|
}
|
|
720
|
-
|
|
653
|
+
else {
|
|
654
|
+
cs.push(...cvField(k, x, kp, ctx))
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
if (cs.length === 0) {
|
|
658
|
+
err('empty condition', path)
|
|
721
659
|
}
|
|
722
|
-
return cvAnd
|
|
660
|
+
return cvGroup(cvAnd, cs)
|
|
723
661
|
}
|
|
724
662
|
|
|
725
663
|
|
|
726
664
|
/**
|
|
727
|
-
* 以AND
|
|
665
|
+
* 以AND結合子式
|
|
728
666
|
*
|
|
729
667
|
* @ignore
|
|
730
668
|
*/
|
|
731
669
|
function cvAnd(cs) {
|
|
732
|
-
if (cs.length === 0) {
|
|
733
|
-
return { [Op.and]: [sqlTrue()] }
|
|
734
|
-
}
|
|
735
670
|
return { [Op.and]: cs }
|
|
736
671
|
}
|
|
737
672
|
|
|
738
673
|
|
|
739
674
|
/**
|
|
740
|
-
* 以OR
|
|
675
|
+
* 以OR結合子式
|
|
741
676
|
*
|
|
742
677
|
* @ignore
|
|
743
678
|
*/
|
|
744
679
|
function cvOr(cs) {
|
|
745
|
-
if (cs.length === 0) {
|
|
746
|
-
return { [Op.and]: [sqlFalse()] }
|
|
747
|
-
}
|
|
748
680
|
return { [Op.or]: cs }
|
|
749
681
|
}
|
|
750
682
|
|
|
751
683
|
|
|
684
|
+
/**
|
|
685
|
+
* 子式過多者遞迴二分為平衡樹, 避免SQLite之運算式樹深度與遞迴上限(見nFlat); 語意不變(AND與OR皆滿足結合律, 含NULL之三值邏輯), 子式順序不變
|
|
686
|
+
*
|
|
687
|
+
* @ignore
|
|
688
|
+
*/
|
|
689
|
+
function cvGroup(fn, cs) {
|
|
690
|
+
if (cs.length <= nFlat) {
|
|
691
|
+
return fn(cs)
|
|
692
|
+
}
|
|
693
|
+
let k = Math.ceil(cs.length / 2)
|
|
694
|
+
return fn([cvGroup(fn, cs.slice(0, k)), cvGroup(fn, cs.slice(k))])
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* 檢核find之形狀(系列白名單, 由wsemi之cvOrmFind單一擁有, 規則見R11)並回傳全新之null原型深複本, 後續一律只使用複本(檢查之值即使用之值)
|
|
700
|
+
* 註: 未給/undefined/{}回傳空物件(全部); 其餘非條件物件、白名單外之運算子與值、異常結構一律throw Error('invalid find: 原因 at [路徑]')
|
|
701
|
+
* 註: maxList傳999(SQLite之運算式樹深度上限); select與delAll於載入model與建立sequelize實例之前先呼叫本函數, 使形狀不合法者不連線、不送出任何SQL
|
|
702
|
+
* 註: 對複本再呼叫為冪等(複本本身符合白名單)
|
|
703
|
+
*
|
|
704
|
+
* @ignore
|
|
705
|
+
* @param {*} find 輸入查詢條件
|
|
706
|
+
* @returns {Object} 回傳null原型之條件複本, 無自有鍵代表全部
|
|
707
|
+
*/
|
|
708
|
+
function chkFindShape(find) {
|
|
709
|
+
return cvOrmFind(find, optShape)
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
|
|
752
713
|
/**
|
|
753
714
|
* 將查詢條件轉為sequelize之where, 供select與delAll共用, 令同一find於兩函數由同一SQL謂詞決定命中
|
|
754
715
|
*
|
|
755
|
-
* 註:
|
|
756
|
-
*
|
|
757
|
-
* 註:
|
|
716
|
+
* 註: 第一步經chkFindShape(wsemi之cvOrmFind)依系列白名單檢核並取複本, 其後只走訪複本; 白名單: 欄位等值{欄:純量|null}、
|
|
717
|
+
* $eq $ne $gt $gte $lt $lte(純量)、$in $nin(純量陣列)、$and $or(條件物件之非空陣列); 其餘(含$not $nor $regex $options $exists、RegExp、Symbol鍵、陣列值)一律拋錯
|
|
718
|
+
* 註: 未給、undefined或{}回傳{}(全部); 其餘非條件物件(null、陣列、字串、數值等)拋錯, 不再視為{}
|
|
719
|
+
* 註: 與欄位型別不符之純量、不存在之欄一律拋錯, 不得交sequelize之型別stringify(見本檔之chkScalar); NULL依SQL三值邏輯
|
|
758
720
|
*
|
|
759
721
|
* @param {*} find 輸入查詢條件
|
|
760
722
|
* @param {Object} ctx 輸入轉換所需之依賴物件
|
|
761
|
-
* @param {Function} ctx.escape
|
|
762
|
-
* @param {Object} [ctx.attrs]
|
|
763
|
-
* @param {String} [ctx.dialect]
|
|
723
|
+
* @param {Function} [ctx.escape] 已不使用(原供$regex之LIKE字面常值; 第33項批1移除$regex), 保留接受以相容既有呼叫端
|
|
724
|
+
* @param {Object} [ctx.attrs] 輸入執行where之model之rawAttributes, 供欄名解析(嚴格鍵: 鍵須與屬性名或資料庫欄名完全相符, 其餘拒收)、依欄位型別檢核純量與取得實際欄名; 未給時不解析鍵、不檢型別而交資料庫(見CLAUDE_rulebook.md之R4)
|
|
725
|
+
* @param {String} [ctx.dialect] 已不使用(原供判定sqlite之LOWER僅折疊ASCII), 保留接受以相容既有呼叫端
|
|
764
726
|
* @param {String} [ctx.pk] 輸入主鍵欄位名, 該欄之純量條件值依normPk正規化(如字串類主鍵欄之123即'123'), 未給時不正規化
|
|
727
|
+
* @param {String} [ctx.dateNoTz] 輸入無時區日期時間字串之處置, 僅接受undefined(預設, 無時區者拒收)或'local'(依主機時區解析, 與以本地時間分量建構之Date同義), 其餘值拋錯; 僅作用於DATE欄, 供Jet等無時區後端之呼叫端明示啟用
|
|
765
728
|
* @returns {Object} 回傳sequelize之where物件, 條件不合法時拋出錯誤
|
|
766
729
|
*/
|
|
767
730
|
function cvFind(find, ctx) {
|
|
768
731
|
|
|
769
|
-
//check,
|
|
770
|
-
if (
|
|
771
|
-
|
|
732
|
+
//check, ctx.dateNoTz須為undefined或'local', 其餘值(含拼錯、大小寫不同)拋錯, 不靜默忽略
|
|
733
|
+
if (ctx && ctx.dateNoTz !== undefined && ctx.dateNoTz !== 'local') {
|
|
734
|
+
throw new Error(`invalid ctx: dateNoTz must be 'local' or undefined, got [${String(ctx.dateNoTz)}]`)
|
|
772
735
|
}
|
|
773
736
|
|
|
774
|
-
//
|
|
775
|
-
|
|
776
|
-
err('find must be a plain object')
|
|
777
|
-
}
|
|
737
|
+
//step1, 形狀檢核(R11), 其後一律只使用複本
|
|
738
|
+
let fd = chkFindShape(find)
|
|
778
739
|
|
|
779
740
|
//check, 空物件為全部
|
|
780
|
-
if (Reflect.ownKeys(
|
|
741
|
+
if (Reflect.ownKeys(fd).length === 0) {
|
|
781
742
|
return {}
|
|
782
743
|
}
|
|
783
744
|
|
|
784
|
-
|
|
745
|
+
//check, attrs須為rawAttributes形狀(見chkAttrs)
|
|
746
|
+
if (ctx && ctx.attrs) {
|
|
747
|
+
chkAttrs(ctx.attrs)
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
//attrs之索引每次轉譯只建一次(內部欄位attrIdx, 不屬ctx之介面)
|
|
751
|
+
return cvDoc(fd, 'find', ctx && ctx.attrs ? { ...ctx, attrIdx: buildAttrIdx(ctx.attrs) } : ctx)
|
|
785
752
|
}
|
|
786
753
|
|
|
787
754
|
|
|
788
|
-
export { getColKind, getColScale, normPk }
|
|
755
|
+
export { getColKind, getColScale, normPk, chkFindShape }
|
|
789
756
|
export default cvFind
|