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.
- package/README.md +100 -28
- package/dist/w-orm-reladb.umd.js +2 -2
- package/dist/w-orm-reladb.umd.js.map +1 -1
- package/docs/WOrmReladb.html +62 -32
- package/docs/WOrmReladb.mjs.html +101 -29
- package/docs/cvFind.mjs.html +253 -410
- package/docs/global.html +336 -12
- package/docs/importModels.mjs.html +2 -2
- package/docs/index.html +2 -2
- 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/tdate.js +38 -0
- package/models/tdatei.js +20 -0
- package/models/tfind.js +50 -0
- package/package.json +3 -3
- package/src/WOrmReladb.mjs +99 -27
- package/src/cvFind.mjs +251 -408
- package/test/api-date-write.mjs +467 -0
- package/test/api-find-whitelist.mjs +390 -0
- package/test/api-mssql.test.mjs +4 -0
- package/test/api-sqlite.test.mjs +4 -0
- package/test/api-suite.mjs +95 -136
- package/test/tools/withTz.mjs +43 -0
- package/test/unit-cvfind.test.mjs +241 -218
- package/test/unit-date-write.test.mjs +457 -0
package/docs/cvFind.mjs.html
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
<nav >
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WOrmReladb.html">WOrmReladb</a><ul class='methods'><li data-type='method'><a href="WOrmReladb.html#.closeSequelize">closeSequelize</a></li><li data-type='method'><a href="WOrmReladb.html#.createStorage">createStorage</a></li><li data-type='method'><a href="WOrmReladb.html#.del">del</a></li><li data-type='method'><a href="WOrmReladb.html#.delAll">delAll</a></li><li data-type='method'><a href="WOrmReladb.html#.genModelsByDB">genModelsByDB</a></li><li data-type='method'><a href="WOrmReladb.html#.genModelsByTabs">genModelsByTabs</a></li><li data-type='method'><a href="WOrmReladb.html#.genTransaction">genTransaction</a></li><li data-type='method'><a href="WOrmReladb.html#.initSequelize">initSequelize</a></li><li data-type='method'><a href="WOrmReladb.html#.insert">insert</a></li><li data-type='method'><a href="WOrmReladb.html#.insertBulk">insertBulk</a></li><li data-type='method'><a href="WOrmReladb.html#.save">save</a></li><li data-type='method'><a href="WOrmReladb.html#.select">select</a></li><li data-type='method'><a href="WOrmReladb.html#.selectByPk">selectByPk</a></li></ul></li></ul><h3>Global</h3><ul><li><a href="global.html#cvFind">cvFind</a></li><li><a href="global.html#getColKind">getColKind</a></li><li><a href="global.html#getColScale">getColScale</a></li><li><a href="global.html#importModels">importModels</a></li><li><a href="global.html#isKindOk">isKindOk</a></li><li><a href="global.html#normPk">normPk</a></li></ul>
|
|
32
|
+
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WOrmReladb.html">WOrmReladb</a><ul class='methods'><li data-type='method'><a href="WOrmReladb.html#.closeSequelize">closeSequelize</a></li><li data-type='method'><a href="WOrmReladb.html#.createStorage">createStorage</a></li><li data-type='method'><a href="WOrmReladb.html#.del">del</a></li><li data-type='method'><a href="WOrmReladb.html#.delAll">delAll</a></li><li data-type='method'><a href="WOrmReladb.html#.genModelsByDB">genModelsByDB</a></li><li data-type='method'><a href="WOrmReladb.html#.genModelsByTabs">genModelsByTabs</a></li><li data-type='method'><a href="WOrmReladb.html#.genTransaction">genTransaction</a></li><li data-type='method'><a href="WOrmReladb.html#.initSequelize">initSequelize</a></li><li data-type='method'><a href="WOrmReladb.html#.insert">insert</a></li><li data-type='method'><a href="WOrmReladb.html#.insertBulk">insertBulk</a></li><li data-type='method'><a href="WOrmReladb.html#.save">save</a></li><li data-type='method'><a href="WOrmReladb.html#.select">select</a></li><li data-type='method'><a href="WOrmReladb.html#.selectByPk">selectByPk</a></li></ul></li></ul><h3>Global</h3><ul><li><a href="global.html#cvFind">cvFind</a></li><li><a href="global.html#genDateChecker">genDateChecker</a></li><li><a href="global.html#getColKind">getColKind</a></li><li><a href="global.html#getColScale">getColScale</a></li><li><a href="global.html#importModels">importModels</a></li><li><a href="global.html#inDateRange">inDateRange</a></li><li><a href="global.html#isKindOk">isKindOk</a></li><li><a href="global.html#normPk">normPk</a></li></ul>
|
|
33
33
|
|
|
34
34
|
</nav>
|
|
35
35
|
|
|
@@ -46,16 +46,19 @@
|
|
|
46
46
|
<section>
|
|
47
47
|
<article>
|
|
48
48
|
<pre class="prettyprint source linenums"><code>import Sequelize from 'sequelize'
|
|
49
|
+
import cvOrmFind from 'wsemi/src/cvOrmFind.mjs'
|
|
49
50
|
|
|
50
51
|
|
|
51
52
|
//查詢條件(find)之轉譯, 供select與delAll共用, 令同一find於兩函數由同一SQL謂詞決定命中
|
|
52
53
|
//
|
|
53
|
-
|
|
54
|
-
// 1.
|
|
55
|
-
//
|
|
56
|
-
// 2.
|
|
57
|
-
//
|
|
58
|
-
|
|
54
|
+
//分兩階段(規則見CLAUDE_rulebook.md之R11):
|
|
55
|
+
// 1. 形狀檢核: 第一步經wsemi之cvOrmFind依系列白名單檢核並回傳全新之null原型深複本(檢查之值即使用之值), 之後只走訪複本;
|
|
56
|
+
// 白名單外之運算子、非純量之值、非條件物件之find、異常結構(存取器、Symbol鍵、洞…)一律整批reject, 不靜默忽略、不改變語意、不擴大刪除範圍
|
|
57
|
+
// 2. 翻譯: 本模組負責後端專屬之檢核與轉譯——欄名嚴格鍵、依欄位型別檢核純量、主鍵正規化(R8)、DATE範圍與ctx.dateNoTz、
|
|
58
|
+
// 空集合之欄位引用($in:[]), 見R3–R5、R8
|
|
59
|
+
//歷史: 原select內之cvObj以lodash之each迭代並以Op[k]直譯$鍵, 不支援之運算子成為鍵'undefined'、具數值length之物件被當成陣列樣迭代而丟棄全部條件,
|
|
60
|
+
//不得供破壞性之delAll使用, 1.1.7起改自寫白名單; 第33項批1起形狀層改由cvOrmFind單一擁有, 本模組刪除Symbol/Op全集、$not $nor $regex $options $exists、
|
|
61
|
+
//RegExp值、陣列值即$in、非物件視為{}等形狀層(破壞性變更見CLAUDE_rulebook.md與README)
|
|
59
62
|
//主鍵值之正規化(normPk)亦屬本模組, 供主鍵函數與主鍵欄條件共用, 規則見R8
|
|
60
63
|
|
|
61
64
|
|
|
@@ -63,8 +66,21 @@
|
|
|
63
66
|
let Op = Sequelize.Op
|
|
64
67
|
|
|
65
68
|
|
|
66
|
-
//
|
|
67
|
-
let
|
|
69
|
+
//SQL運算式(Sequelize.fn、literal、col等)之基底類別, 寫入DATE欄之值為此類者由呼叫端明示, 不檢(R12)
|
|
70
|
+
let SM = Sequelize.Utils.SequelizeMethod
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
//形狀檢核之上限(傳給cvOrmFind): $and/$or長度999——SQLite平鋪$or/$and恰1000項即Expression tree is too large(DB錯誤), 本套件使用SQLite故下調(預設1000);
|
|
74
|
+
//其餘上限用預設: 邏輯巢狀深度32、$in/$nin長度10000、節點總數100000、欄名128
|
|
75
|
+
let optShape = { maxList: 999 }
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
//$and/$or清單(含同一條件物件內並列欄位之隱含AND)項數超過此值者, 遞迴二分為平衡樹再交sequelize:
|
|
79
|
+
//SQLite之運算式樹深度上限1000(Expression tree is too large), 平鋪之a OR b OR …為左深樹(深度隨項數線性成長), select因欄位帶表別名(`t`.`c`)而較delAll少容1項
|
|
80
|
+
//(實測平鋪$or: select最多998項、delAll最多999項, 999項時select報DB錯誤而delAll照常刪除, 違反select命中即delAll刪除之對象一致);
|
|
81
|
+
//巢狀層數亦會累加(32層 × 每層33項平鋪即超限, 外審sol F1), 另有巢狀遞迴上限(實測右深巢狀OR約830層, SQLITE_ERROR: Recursion limit);
|
|
82
|
+
//二分後每個清單之樹深僅約log2(項數)+3, 邏輯巢狀上限32層下全樹深度恆遠低於兩種上限, select與delAll皆可執行
|
|
83
|
+
let nFlat = 4
|
|
68
84
|
|
|
69
85
|
|
|
70
86
|
//比較運算子, 值須為純量(null僅$eq與$ne)
|
|
@@ -78,41 +94,6 @@ let mCmp = {
|
|
|
78
94
|
}
|
|
79
95
|
|
|
80
96
|
|
|
81
|
-
//sequelize之Symbol運算子對應之$名稱, 令呼叫端以Symbol給條件時亦經同一檢核與轉譯
|
|
82
|
-
let mSymCmp = new Map([
|
|
83
|
-
[Op.eq, '$eq'],
|
|
84
|
-
[Op.ne, '$ne'],
|
|
85
|
-
[Op.gt, '$gt'],
|
|
86
|
-
[Op.gte, '$gte'],
|
|
87
|
-
[Op.lt, '$lt'],
|
|
88
|
-
[Op.lte, '$lte'],
|
|
89
|
-
[Op.in, '$in'],
|
|
90
|
-
[Op.notIn, '$nin'],
|
|
91
|
-
[Op.not, '$not'],
|
|
92
|
-
])
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
//欄位層不接受之Symbol: 邏輯運算子於sequelize欄位層會提前返回而丟棄同層其他條件;
|
|
96
|
-
//Op.values與Op.join不在sequelize之OperatorMap(各方言皆然), 欄位層遇之退回等值並把整個運算子物件交欄型stringify(DATEONLY成查詢當日、DATE成現在、INTEGER成'[object Object]')
|
|
97
|
-
let setSymFieldNo = new Set([Op.and, Op.or, Op.values, Op.join])
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
//sequelize以value[0]、value[1]取值之Symbol運算子, 值須為兩元素陣列(字串被逐字元取值、短陣列補NULL, NOT BETWEEN可命中全部非NULL列)
|
|
101
|
-
let setSymBetween = new Set([Op.between, Op.notBetween])
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
//sequelize以字串樣板(`${value}%`)插值之Symbol運算子: DATE欄之值(轉為Date後)會成為隨主機時區與語系而變之文字, 故不得用於DATE欄
|
|
105
|
-
let setSymInterp = new Set([Op.startsWith, Op.endsWith, Op.substring])
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
//LIKE之跳脫字元, 選用於各方言字串常值內皆無特殊意義者, 令ESCAPE子句不需依方言跳脫
|
|
109
|
-
let cEsc = '!'
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
//巢狀深度上限, 防循環參照與過深之條件
|
|
113
|
-
let nDepthMax = 32
|
|
114
|
-
|
|
115
|
-
|
|
116
97
|
//欄位型別分類, 以sequelize之型別類別判定(各方言之型別皆繼承之), 另以type.key補判
|
|
117
98
|
//註: DataTypes.DOUBLE之key為'DOUBLE PRECISION', 只比對key會漏判而不檢型別
|
|
118
99
|
let DT = Sequelize.DataTypes
|
|
@@ -205,30 +186,11 @@ function err(msg, path) {
|
|
|
205
186
|
|
|
206
187
|
|
|
207
188
|
/**
|
|
208
|
-
*
|
|
209
|
-
* 註:
|
|
210
|
-
* 其條件可能放在原型上而取不到自有鍵, 被當成{}而刪全表
|
|
189
|
+
* 恆假之謂詞, 以明確之SQL表示, 不以空條件表示
|
|
190
|
+
* 註: sequelize會把空條件於OR、NOT內濾除而改變語意; 本模組僅用於$in:[]之f IS NULL AND 1 = 0(仍引用欄位, 見R5)
|
|
211
191
|
*
|
|
212
192
|
* @ignore
|
|
213
193
|
*/
|
|
214
|
-
function isPlainObj(v) {
|
|
215
|
-
if (v === null || typeof v !== 'object') {
|
|
216
|
-
return false
|
|
217
|
-
}
|
|
218
|
-
let p = Object.getPrototypeOf(v)
|
|
219
|
-
return p === Object.prototype || p === null
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* 恆真與恆假之謂詞, 以明確之SQL表示, 不以空條件表示
|
|
225
|
-
* 註: sequelize會把空條件(如{}、空$and)於OR、NOT內濾除, 致恆真被當成不存在而改變語意(如$nor:[{}]成全部)
|
|
226
|
-
*
|
|
227
|
-
* @ignore
|
|
228
|
-
*/
|
|
229
|
-
function sqlTrue() {
|
|
230
|
-
return Sequelize.literal('1 = 1')
|
|
231
|
-
}
|
|
232
194
|
function sqlFalse() {
|
|
233
195
|
return Sequelize.literal('1 = 0')
|
|
234
196
|
}
|
|
@@ -400,15 +362,132 @@ function resolveAttr(attrs, idx, field, path) {
|
|
|
400
362
|
}
|
|
401
363
|
|
|
402
364
|
|
|
365
|
+
//DATE可接受之年份範圍, 條件側(isKindOk、parseNoTzLocal)與寫入側(genDateChecker)共用之唯一定義(R4、R12)
|
|
366
|
+
let yearMin = 1
|
|
367
|
+
let yearMax = 9999
|
|
368
|
+
|
|
369
|
+
|
|
403
370
|
/**
|
|
404
|
-
*
|
|
405
|
-
* 註: sequelize以YYYY格式化DATE, 年10000起為5位數, sqlite
|
|
371
|
+
* 判定年份是否在1–9999
|
|
372
|
+
* 註: sequelize以YYYY格式化DATE, 年10000起為5位數、年0與負年份亦非4位數, sqlite以文字比較致時間序與字典序不一致(哨兵8.64e15被$lt某日期誤中、$gt漏中);
|
|
373
|
+
* mssql之datetimeoffset與date範圍亦為0001–9999
|
|
406
374
|
*
|
|
407
375
|
* @ignore
|
|
408
376
|
*/
|
|
377
|
+
function inYearRange(y) {
|
|
378
|
+
return y >= yearMin && y <= yearMax
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* 判定時間(毫秒)換算UTC後年份是否在1–9999, 供條件側與寫入側共用(R4、R12); 無效時間(NaN、超出Date範圍)為false
|
|
384
|
+
*
|
|
385
|
+
* @param {Number} ms 輸入時間毫秒數
|
|
386
|
+
* @returns {Boolean} 回傳是否在範圍內
|
|
387
|
+
*/
|
|
409
388
|
function inDateRange(ms) {
|
|
410
|
-
|
|
411
|
-
|
|
389
|
+
return inYearRange(new Date(ms).getUTCFullYear())
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* 取DATEONLY欄把值轉為實際存入之YYYY-MM-DD文字之函數(sequelize之DATEONLY._sanitize)
|
|
395
|
+
* 註: 真正之rawAttributes其type為型別實例; 手造attrs可能給型別建構子(如Sequelize.DATEONLY)或只有key之物件, 此時用sequelize內建DATEONLY之_sanitize(兩者同一函數);
|
|
396
|
+
* 取不到(sequelize版本漂移)時於換算時拋錯, 由chkWriteDate視為無效而拒收(fail-closed, 版本漂移另有單元測試之guard案)
|
|
397
|
+
*
|
|
398
|
+
* @ignore
|
|
399
|
+
*/
|
|
400
|
+
function getDateOnlySanitizer(type) {
|
|
401
|
+
let t = type && typeof type._sanitize === 'function' ? type : DT.DATEONLY.prototype
|
|
402
|
+
return (v) => t._sanitize(v, {})
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* 檢核寫入DATE/DATEONLY欄之單一值(R12), 回傳{ok, v}: ok為是否為「年份在1–9999之有效時間」, v為建議寫回之標準值(undefined表示維持原值)
|
|
408
|
+
* 註: date——以sequelize之DATE._sanitize同一轉換取得時間(Date原樣, 其餘truthy值new Date(值)), 以inDateRange判定UTC年份;
|
|
409
|
+
* falsy值sequelize不轉換, 僅0(epoch 0)有效, 其餘('', false, NaN)會被存為文字Invalid date而拒收
|
|
410
|
+
* 註: dateonly——sequelize以moment(值)依主機本地時間格式化為YYYY-MM-DD文字存入, 以該型別之_sanitize所得之文字(= 實際存入者)判定:
|
|
411
|
+
* 須為4位數年且在1–9999(範圍外年份之文字形狀為5位以上、帶負號或年0000, 如new Date(8.64e15)得275760-09-13、new Date(-8.64e15)得-271821-04-20), Invalid date文字亦拒收
|
|
412
|
+
* 註: 無效時間(Invalid Date、NaN、無法解析之字串、超出Date範圍之數值、BigInt與Symbol…)一律拒收: sqlite存為文字Invalid date, 其字典序大於任何年份而使$gt命中、delAll誤刪,
|
|
413
|
+
* mssql則由資料庫報錯, 兩方言不一致(外審Argon F5; 與條件側無效Date一律reject同)
|
|
414
|
+
* 註: 檢查之值即使用之值——物件值(Date及其子類別、包裝物件、陣列、含自訂valueOf/toString者)可被sequelize再度求值而得不同結果, 故回傳已檢核之標準值(Date或YYYY-MM-DD文字)供呼叫端換上;
|
|
415
|
+
* 原始值(字串、數值)不可變, 不換
|
|
416
|
+
* 註: Sequelize.fn、literal、col等SQL運算式(SequelizeMethod)由呼叫端明示, sequelize不轉換, 不檢
|
|
417
|
+
*
|
|
418
|
+
* @ignore
|
|
419
|
+
*/
|
|
420
|
+
function chkWriteDate(v, kind, sanitize) {
|
|
421
|
+
if (v instanceof SM) {
|
|
422
|
+
return { ok: true, v: undefined }
|
|
423
|
+
}
|
|
424
|
+
if (!v) {
|
|
425
|
+
return { ok: v === 0, v: undefined }
|
|
426
|
+
}
|
|
427
|
+
if (typeof v === 'bigint' || typeof v === 'symbol') {
|
|
428
|
+
return { ok: false, v: undefined } //BigInt與Symbol不是時間(new Date(1n)拋錯, moment(1n)卻視為年2001), 一律拒收
|
|
429
|
+
}
|
|
430
|
+
try {
|
|
431
|
+
if (kind === 'date') {
|
|
432
|
+
let ms = (v instanceof Date ? v : new Date(v)).getTime()
|
|
433
|
+
if (!inDateRange(ms)) {
|
|
434
|
+
return { ok: false, v: undefined }
|
|
435
|
+
}
|
|
436
|
+
return { ok: true, v: typeof v === 'object' ? new Date(ms) : undefined }
|
|
437
|
+
}
|
|
438
|
+
let s = sanitize(v)
|
|
439
|
+
let m = typeof s === 'string' ? s.match(reDateOnly) : null
|
|
440
|
+
if (m === null || !inYearRange(Number(m[1]))) {
|
|
441
|
+
return { ok: false, v: undefined }
|
|
442
|
+
}
|
|
443
|
+
return { ok: true, v: typeof v === 'object' ? s : undefined }
|
|
444
|
+
}
|
|
445
|
+
catch (err) {
|
|
446
|
+
return { ok: false, v: undefined } //換算時拋錯(Symbol、BigInt、無原型物件等)亦非有效時間
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* 產生寫入數據之DATE/DATEONLY欄範圍檢核函數(R12): 回傳之函數對單筆數據(物件)檢核各DATE、DATEONLY屬性之值,
|
|
453
|
+
* 回傳第一個無效或範圍外(年份不在1–9999)之屬性名(依model屬性定義順序), 皆合法回傳null
|
|
454
|
+
* 註: 與條件側共用inDateRange; 只檢核model中型別為DATE(date)與DATEONLY(dateonly)之屬性, 值為null或undefined者不檢;
|
|
455
|
+
* 非物件之記錄回傳null(由呼叫端前段檢核負責)
|
|
456
|
+
* 註: 通過檢核之物件值(Date及其子類別、包裝物件、陣列等)就地換成已檢核之標準值(Date或YYYY-MM-DD文字), 令檢查之值即使用之值(見chkWriteDate);
|
|
457
|
+
* 僅換記錄自有屬性, 原型鏈上之屬性只讀不換(create與bulkCreate不讀原型鏈屬性, update會讀, 故原型鏈上具狀態之getter不在保證範圍)
|
|
458
|
+
*
|
|
459
|
+
* @param {Object} attrs 輸入model之rawAttributes, 非物件時回傳恆為null之檢核函數
|
|
460
|
+
* @returns {Function} 回傳檢核函數(rec)=>屬性名字串或null
|
|
461
|
+
*/
|
|
462
|
+
function genDateChecker(attrs) {
|
|
463
|
+
let cols = []
|
|
464
|
+
if (attrs !== null && typeof attrs === 'object') {
|
|
465
|
+
for (let k of Object.keys(attrs)) {
|
|
466
|
+
let kind = getColKind(attrs, k)
|
|
467
|
+
if (kind === 'date' || kind === 'dateonly') {
|
|
468
|
+
cols.push({ k, kind, san: kind === 'dateonly' ? getDateOnlySanitizer(attrs[k].type) : null })
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return (rec) => {
|
|
473
|
+
if (rec === null || typeof rec !== 'object') {
|
|
474
|
+
return null
|
|
475
|
+
}
|
|
476
|
+
for (let c of cols) {
|
|
477
|
+
let v = rec[c.k]
|
|
478
|
+
if (v === null || v === undefined) {
|
|
479
|
+
continue
|
|
480
|
+
}
|
|
481
|
+
let r = chkWriteDate(v, c.kind, c.san)
|
|
482
|
+
if (!r.ok) {
|
|
483
|
+
return c.k
|
|
484
|
+
}
|
|
485
|
+
if (r.v !== undefined && Object.prototype.hasOwnProperty.call(rec, c.k)) {
|
|
486
|
+
rec[c.k] = r.v
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return null
|
|
490
|
+
}
|
|
412
491
|
}
|
|
413
492
|
|
|
414
493
|
|
|
@@ -583,43 +662,31 @@ function normPk(v, kind, scale = null) {
|
|
|
583
662
|
return { kind: 'invalid' }
|
|
584
663
|
}
|
|
585
664
|
|
|
586
|
-
|
|
587
665
|
/**
|
|
588
|
-
* 檢核純量值: 字串、有限數值、布林、有效Date
|
|
589
|
-
* 註: 物件、陣列等不得交sequelize之型別stringify, 否則FLOAT成'NaN'、INTEGER成'[object Object]'、DATE成本地今日00:00
|
|
666
|
+
* 檢核純量值: 字串、有限數值、布林、有效Date, 以及null(是否允許null已由cvOrmFind依運算子判定); 並須符合欄位型別分類
|
|
667
|
+
* 註: 物件、陣列等不得交sequelize之型別stringify, 否則FLOAT成'NaN'、INTEGER成'[object Object]'、DATE成本地今日00:00而誤刪;
|
|
668
|
+
* 形狀層(cvOrmFind)已保證僅此類值到達, 此處保留型別檢核作為fail-closed之防線(日後上游放寬時不得靜默放行)
|
|
590
669
|
* 註: 主鍵欄(pk為true)之字串類、整數類與其他數值類以normPk正規化後回傳(如字串類主鍵欄之123即'123'), 無效或型別不符者拒收,
|
|
591
670
|
* 令select與delAll對主鍵之認定與selectByPk、del等主鍵函數一致; 非主鍵欄不轉型
|
|
592
671
|
* 註: kind為date且呼叫端以ctx.dateNoTz明示'local'(dateNoTz為'local')時, 無時區日期時間字串依主機時區轉為Date回傳(見parseNoTzLocal), 其餘值照舊判定
|
|
593
672
|
*
|
|
594
673
|
* @ignore
|
|
595
674
|
*/
|
|
596
|
-
function chkScalar(v, path,
|
|
675
|
+
function chkScalar(v, path, kind = 'any', scale = null, pk = false, dateNoTz = undefined) {
|
|
597
676
|
if (v === null) {
|
|
598
|
-
|
|
599
|
-
return v
|
|
600
|
-
}
|
|
601
|
-
err('null is not allowed here', path)
|
|
677
|
+
return v
|
|
602
678
|
}
|
|
603
|
-
|
|
604
|
-
if (typeof v === 'string' || typeof v === 'boolean') {
|
|
605
|
-
ok = true
|
|
606
|
-
}
|
|
607
|
-
else if (typeof v === 'number') {
|
|
679
|
+
if (typeof v === 'number') {
|
|
608
680
|
if (!Number.isFinite(v)) {
|
|
609
681
|
err(`number must be finite, got [${v}]`, path)
|
|
610
682
|
}
|
|
611
|
-
ok = true
|
|
612
683
|
}
|
|
613
684
|
else if (v instanceof Date) {
|
|
614
685
|
if (isNaN(v.getTime())) {
|
|
615
686
|
err('invalid Date', path)
|
|
616
687
|
}
|
|
617
|
-
ok = true
|
|
618
|
-
}
|
|
619
|
-
else if (Buffer.isBuffer(v)) {
|
|
620
|
-
ok = kind === 'any'
|
|
621
688
|
}
|
|
622
|
-
if (
|
|
689
|
+
else if (typeof v !== 'string' && typeof v !== 'boolean') {
|
|
623
690
|
err(`value must be a string, finite number, boolean, Date or null, got [${Object.prototype.toString.call(v)}]`, path)
|
|
624
691
|
}
|
|
625
692
|
if (pk && (kind === 'str' || kind === 'int' || kind === 'num')) {
|
|
@@ -649,398 +716,181 @@ function chkScalar(v, path, allowNull, kind = 'any', scale = null, pk = false, d
|
|
|
649
716
|
|
|
650
717
|
|
|
651
718
|
/**
|
|
652
|
-
*
|
|
653
|
-
* 註:
|
|
654
|
-
*
|
|
655
|
-
* @ignore
|
|
656
|
-
*/
|
|
657
|
-
function getItems(arr, path) {
|
|
658
|
-
let rs = []
|
|
659
|
-
for (let i = 0; i < arr.length; i++) {
|
|
660
|
-
if (!(i in arr) || arr[i] === undefined) {
|
|
661
|
-
err('array must not contain holes or undefined', `${path}.${i}`)
|
|
662
|
-
}
|
|
663
|
-
rs.push(arr[i])
|
|
664
|
-
}
|
|
665
|
-
return rs
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
/**
|
|
670
|
-
* 檢核sequelize原生Symbol運算子之值: 純量、純量陣列或sequelize之literal/fn/col
|
|
671
|
-
* 註: 原生寫法由呼叫端自負語意, 但仍不得把物件交sequelize之型別stringify
|
|
672
|
-
*
|
|
673
|
-
* @ignore
|
|
674
|
-
*/
|
|
675
|
-
function chkRawSym(x, path, kind, scale, pk, dateNoTz) {
|
|
676
|
-
if (x instanceof Sequelize.Utils.SequelizeMethod) {
|
|
677
|
-
return x
|
|
678
|
-
}
|
|
679
|
-
if (Array.isArray(x)) {
|
|
680
|
-
return getItems(x, path).map((y, i) => (y instanceof Sequelize.Utils.SequelizeMethod ? y : chkScalar(y, `${path}.${i}`, true, kind, scale, pk, dateNoTz)))
|
|
681
|
-
}
|
|
682
|
-
return chkScalar(x, path, true, kind, scale, pk, dateNoTz)
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
/**
|
|
687
|
-
* 將正規式來源轉為字面字串, 僅接受無未跳脫元字元者
|
|
688
|
-
*
|
|
689
|
-
* @ignore
|
|
690
|
-
*/
|
|
691
|
-
function regexToLiteral(src, path) {
|
|
692
|
-
let r = ''
|
|
693
|
-
let meta = '\\^$.|?*+()[]{}/'
|
|
694
|
-
for (let i = 0; i < src.length; i++) {
|
|
695
|
-
let c = src[i]
|
|
696
|
-
if (c === '\\') {
|
|
697
|
-
let n = src[i + 1]
|
|
698
|
-
if (n !== undefined && meta.indexOf(n) >= 0) {
|
|
699
|
-
r += n
|
|
700
|
-
i++
|
|
701
|
-
continue
|
|
702
|
-
}
|
|
703
|
-
err(`$regex supports literal substrings only, escape [\\${n === undefined ? '' : n}] is not supported`, path)
|
|
704
|
-
}
|
|
705
|
-
if (meta.indexOf(c) >= 0) {
|
|
706
|
-
err(`$regex supports literal substrings only, unescaped metacharacter [${c}] is not supported`, path)
|
|
707
|
-
}
|
|
708
|
-
r += c
|
|
709
|
-
}
|
|
710
|
-
if (r.indexOf('\u0000') >= 0) {
|
|
711
|
-
err('$regex must not contain NUL', path)
|
|
712
|
-
}
|
|
713
|
-
return r
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
/**
|
|
718
|
-
* 正規化正規式旗標, 僅接受i; '$i'為既有寫法, 過渡期視同i
|
|
719
|
-
*
|
|
720
|
-
* @ignore
|
|
721
|
-
*/
|
|
722
|
-
function normFlags(fs, path) {
|
|
723
|
-
if (fs === undefined || fs === null || fs === '') {
|
|
724
|
-
return false
|
|
725
|
-
}
|
|
726
|
-
if (typeof fs !== 'string') {
|
|
727
|
-
err('$options must be a string', path)
|
|
728
|
-
}
|
|
729
|
-
let s = fs === '$i' ? 'i' : fs
|
|
730
|
-
for (let c of s) {
|
|
731
|
-
if (c !== 'i') {
|
|
732
|
-
err(`$options/flags supports [i] only, got [${fs}]`, path)
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
return s.indexOf('i') >= 0
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
/**
|
|
740
|
-
* 產生LIKE子字串條件, 跳脫萬用字元並帶ESCAPE子句; 不分大小寫時兩側皆以LOWER處理
|
|
741
|
-
* 註: sqlite之LOWER只折疊ASCII, 故sqlite下給i而字面含非ASCII之大小寫字母者拒收, 不宣稱做不到之語意
|
|
742
|
-
*
|
|
743
|
-
* @ignore
|
|
744
|
-
*/
|
|
745
|
-
function cvRegex(field, col, re, options, path, ctx, kind) {
|
|
746
|
-
if (kind !== 'str' && kind !== 'any') {
|
|
747
|
-
err(`$regex requires a string column, column type is [${kind}]`, path)
|
|
748
|
-
}
|
|
749
|
-
let src
|
|
750
|
-
let ci
|
|
751
|
-
if (re instanceof RegExp) {
|
|
752
|
-
if (options !== undefined) {
|
|
753
|
-
err('$options can not be used with a RegExp $regex', path)
|
|
754
|
-
}
|
|
755
|
-
let flags = re.flags.replace(/[gu]/g, '') //g、u不影響是否命中
|
|
756
|
-
src = re.source === '(?:)' ? '' : re.source //new RegExp('')之source為(?:), 即空字串
|
|
757
|
-
ci = normFlags(flags, path)
|
|
758
|
-
}
|
|
759
|
-
else if (typeof re === 'string') {
|
|
760
|
-
src = re
|
|
761
|
-
ci = normFlags(options, path)
|
|
762
|
-
}
|
|
763
|
-
else {
|
|
764
|
-
err('$regex must be a string or RegExp', path)
|
|
765
|
-
}
|
|
766
|
-
let lit = regexToLiteral(src, path)
|
|
767
|
-
if (ci && col[0] === '*') {
|
|
768
|
-
//sequelize之Col遇以*開頭之名稱一律回傳*, LOWER(col)成LOWER(*)
|
|
769
|
-
err(`unsupported column name [${col}], it can not be used with $regex and i`, path)
|
|
770
|
-
}
|
|
771
|
-
if (ci && ctx.dialect === 'sqlite') {
|
|
772
|
-
for (let c of lit) {
|
|
773
|
-
if (c.charCodeAt(0) > 127 && c.toLowerCase() !== c.toUpperCase()) {
|
|
774
|
-
err(`$regex with i supports ASCII letters only on sqlite, got [${c}]`, path)
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
let pat = `%${lit.replace(/[!%_[]/g, (c) => `${cEsc}${c}`)}%`
|
|
779
|
-
let q = ctx.escape(pat)
|
|
780
|
-
if (ci) {
|
|
781
|
-
//LOWER內須為資料庫之實際欄名col, model之屬性名與欄名可不同(欄名已由resolveAttr核對可安全交sequelize)
|
|
782
|
-
return Sequelize.where(Sequelize.fn('LOWER', Sequelize.col(col)), Op.like, Sequelize.literal(`LOWER(${q}) ESCAPE '${cEsc}'`))
|
|
783
|
-
}
|
|
784
|
-
return { [field]: { [Op.like]: Sequelize.literal(`${q} ESCAPE '${cEsc}'`) } }
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
/**
|
|
789
|
-
* $in與$nin, 含null者另以IS [NOT] NULL補上, 與{欄位:null}一致; 空集合以恆假/恆真表示
|
|
719
|
+
* $in與$nin, 元素為純量(不含null, 已由cvOrmFind保證); 空$in以引用欄位之恆假表示
|
|
720
|
+
* 註: $nin:[]已由cvOrmFind拒收(恆真, 易因清單恰為空而刪全表); 此處再擋以免上游放寬時靜默放行
|
|
790
721
|
*
|
|
791
722
|
* @ignore
|
|
792
723
|
*/
|
|
793
724
|
function cvIn(field, arr, path, neg, kind, scale, pk, dateNoTz) {
|
|
794
|
-
if (!Array.isArray(arr)) {
|
|
795
|
-
err(`${neg ? '$nin' : '$in'} must be an array`, path)
|
|
796
|
-
}
|
|
797
|
-
let hasNull = false
|
|
798
725
|
let vs = []
|
|
799
|
-
|
|
800
|
-
if (
|
|
801
|
-
|
|
802
|
-
return
|
|
726
|
+
for (let i = 0; i < arr.length; i++) {
|
|
727
|
+
if (arr[i] === null) {
|
|
728
|
+
err('null is not allowed here', `${path}[${i}]`)
|
|
803
729
|
}
|
|
804
|
-
vs.push(chkScalar(
|
|
805
|
-
}
|
|
730
|
+
vs.push(chkScalar(arr[i], `${path}[${i}]`, kind, scale, pk, dateNoTz))
|
|
731
|
+
}
|
|
806
732
|
if (!neg) {
|
|
807
733
|
if (vs.length === 0) {
|
|
808
|
-
|
|
734
|
+
//f IS NULL恆為二值(永不為UNKNOWN), 整式二值, 且欄名會出現在SQL中, 不存在之欄由資料庫回報(R5)
|
|
735
|
+
return { [Op.and]: [{ [field]: null }, sqlFalse()] }
|
|
809
736
|
}
|
|
810
|
-
|
|
811
|
-
return hasNull ? { [Op.or]: [c, { [field]: null }] } : c
|
|
737
|
+
return { [field]: { [Op.in]: vs } }
|
|
812
738
|
}
|
|
813
739
|
if (vs.length === 0) {
|
|
814
|
-
|
|
740
|
+
err('$nin with empty array is always true', path)
|
|
815
741
|
}
|
|
816
|
-
|
|
817
|
-
return hasNull ? { [Op.and]: [c, { [field]: { [Op.ne]: null } }] } : c
|
|
742
|
+
return { [field]: { [Op.notIn]: vs } }
|
|
818
743
|
}
|
|
819
744
|
|
|
820
745
|
|
|
821
746
|
/**
|
|
822
|
-
* 欄位層條件, 回傳以AND
|
|
747
|
+
* 欄位層條件, 回傳以AND結合之子式陣列; v為純量、null、Date(等值)或運算子物件(null原型, 鍵為$eq $ne $gt $gte $lt $lte $in $nin)
|
|
823
748
|
*
|
|
824
749
|
* @ignore
|
|
825
750
|
*/
|
|
826
|
-
function cvField(field, v, path,
|
|
827
|
-
if (depth > nDepthMax) {
|
|
828
|
-
err('find is too deep', path)
|
|
829
|
-
}
|
|
830
|
-
if (field === '' || field === '__proto__') {
|
|
831
|
-
err(`invalid column name [${field}]`, path)
|
|
832
|
-
}
|
|
833
|
-
if (v === undefined) {
|
|
834
|
-
err('value is undefined', path)
|
|
835
|
-
}
|
|
751
|
+
function cvField(field, v, path, ctx) {
|
|
836
752
|
|
|
837
753
|
//欄名先解析為model之屬性名再判型別與主鍵(R4欄名解析); attrs已給而解析不到者拒收, attrs未給者為any, 交資料庫處置
|
|
838
754
|
let an = resolveAttr(ctx.attrs, ctx.attrIdx, field, path)
|
|
839
|
-
let col = an === null ? field : colOf(ctx.attrs, an) //資料庫實際欄名
|
|
840
755
|
let kind = an === null ? 'any' : getColKind(ctx.attrs, an)
|
|
841
756
|
let scale = an === null ? null : getColScale(ctx.attrs, an)
|
|
842
757
|
let pk = ctx.pk !== undefined && an === ctx.pk //主鍵欄之純量依normPk正規化(R8)
|
|
843
758
|
let dateNoTz = ctx.dateNoTz
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
if (v instanceof RegExp) {
|
|
849
|
-
return [cvRegex(field, col, v, undefined, path, ctx, kind)]
|
|
850
|
-
}
|
|
851
|
-
if (!isPlainObj(v)) {
|
|
852
|
-
return [{ [field]: chkScalar(v, path, true, kind, scale, pk, dateNoTz) }]
|
|
853
|
-
}
|
|
854
|
-
let ks = Reflect.ownKeys(v)
|
|
855
|
-
if (ks.length === 0) {
|
|
856
|
-
err('empty operator object', path)
|
|
759
|
+
|
|
760
|
+
//欄位直給(等值)
|
|
761
|
+
if (v === null || typeof v !== 'object' || v instanceof Date) {
|
|
762
|
+
return [{ [field]: chkScalar(v, path, kind, scale, pk, dateNoTz) }]
|
|
857
763
|
}
|
|
764
|
+
|
|
765
|
+
//運算子物件
|
|
858
766
|
let simple = {}
|
|
859
767
|
let nSimple = 0
|
|
860
768
|
let cs = []
|
|
861
|
-
let
|
|
862
|
-
|
|
863
|
-
let kp = `${path}.${String(k)}`
|
|
769
|
+
for (let k of Object.keys(v)) {
|
|
770
|
+
let kp = `${path}.${k}`
|
|
864
771
|
let x = v[k]
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
err(`unsupported symbol operator [${String(k)}]`, kp)
|
|
869
|
-
}
|
|
870
|
-
name = mSymCmp.get(k)
|
|
871
|
-
if (name === undefined) {
|
|
872
|
-
if (kind === 'date' && setSymInterp.has(k)) {
|
|
873
|
-
err(`operator [${String(k)}] is not supported on a date column`, kp)
|
|
874
|
-
}
|
|
875
|
-
if (setSymBetween.has(k)) {
|
|
876
|
-
//值須為兩元素陣列, 元素為純量(不含null)或sequelize之方法物件
|
|
877
|
-
if (!Array.isArray(x) || x.length !== 2) {
|
|
878
|
-
err(`${String(k)} requires an array of two values`, kp)
|
|
879
|
-
}
|
|
880
|
-
cs.push({ [field]: { [k]: getItems(x, kp).map((y, i) => (y instanceof Sequelize.Utils.SequelizeMethod ? y : chkScalar(y, `${kp}.${i}`, false, kind, scale, pk, dateNoTz))) } })
|
|
881
|
-
continue
|
|
882
|
-
}
|
|
883
|
-
//其餘sequelize之Symbol運算子為呼叫端自負之原生寫法, 值經檢核後各自成一子式
|
|
884
|
-
cs.push({ [field]: { [k]: chkRawSym(x, kp, kind, scale, pk, dateNoTz) } })
|
|
885
|
-
continue
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
if (typeof name !== 'string' || name[0] !== '$') {
|
|
889
|
-
err('nested object is not supported, operators must start with $', kp)
|
|
890
|
-
}
|
|
891
|
-
if (mCmp[name] !== undefined) {
|
|
892
|
-
let allowNull = name === '$eq' || name === '$ne'
|
|
893
|
-
let y = chkScalar(x, kp, allowNull, kind, scale, pk, dateNoTz)
|
|
894
|
-
if (Object.prototype.hasOwnProperty.call(simple, mCmp[name])) {
|
|
895
|
-
//同義之鍵(如$gt與Op.gt)各自成一子式, 不得互相覆寫
|
|
896
|
-
cs.push({ [field]: { [mCmp[name]]: y } })
|
|
772
|
+
if (k === '$in' || k === '$nin') {
|
|
773
|
+
if (!Array.isArray(x)) {
|
|
774
|
+
err(`${k} must be an array`, kp)
|
|
897
775
|
}
|
|
898
|
-
|
|
899
|
-
simple[mCmp[name]] = y
|
|
900
|
-
nSimple++
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
else if (name === '$in' || name === '$nin') {
|
|
904
|
-
cs.push(cvIn(field, x, kp, name === '$nin', kind, scale, pk, dateNoTz))
|
|
905
|
-
}
|
|
906
|
-
else if (name === '$exists') {
|
|
907
|
-
if (x !== true && x !== false && x !== 0 && x !== 1) {
|
|
908
|
-
err('$exists must be a boolean, 0 or 1', kp)
|
|
909
|
-
}
|
|
910
|
-
cs.push(x ? { [field]: { [Op.ne]: null } } : { [field]: null })
|
|
776
|
+
cs.push(cvIn(field, x, kp, k === '$nin', kind, scale, pk, dateNoTz))
|
|
911
777
|
}
|
|
912
|
-
else if (
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
else if (name === '$options') {
|
|
917
|
-
//由$regex一併處理
|
|
918
|
-
}
|
|
919
|
-
else if (name === '$not') {
|
|
920
|
-
if (Array.isArray(x)) {
|
|
921
|
-
err('$not does not accept an array', kp)
|
|
922
|
-
}
|
|
923
|
-
let ns = cvField(field, x, kp, depth + 1, ctx)
|
|
924
|
-
cs.push({ [Op.not]: { [Op.and]: ns } })
|
|
778
|
+
else if (Object.prototype.hasOwnProperty.call(mCmp, k)) {
|
|
779
|
+
//同欄之比較運算子併入同一欄位物件, 以AND結合
|
|
780
|
+
simple[mCmp[k]] = chkScalar(x, kp, kind, scale, pk, dateNoTz)
|
|
781
|
+
nSimple++
|
|
925
782
|
}
|
|
926
783
|
else {
|
|
927
|
-
err(`unsupported operator [${
|
|
784
|
+
err(`unsupported operator [${k}]`, kp)
|
|
928
785
|
}
|
|
929
786
|
}
|
|
930
|
-
if (v.$options !== undefined && !hasRegex) {
|
|
931
|
-
err('$options requires $regex', path)
|
|
932
|
-
}
|
|
933
|
-
if (cs.length === 0 && nSimple === 0) {
|
|
934
|
-
//如僅有值為undefined之$options: 不產生任何子式, 該條件即消失而成恆真, delAll會刪全表
|
|
935
|
-
err('operator object has no operator', path)
|
|
936
|
-
}
|
|
937
787
|
if (nSimple > 0) {
|
|
938
788
|
cs.unshift({ [field]: simple })
|
|
939
789
|
}
|
|
790
|
+
if (cs.length === 0) {
|
|
791
|
+
//不產生任何子式者條件即消失而成恆真, delAll會刪全表; cvOrmFind已拒收空運算子物件, 此處再擋(含非Date之物件值)
|
|
792
|
+
err('operator object has no operator', path)
|
|
793
|
+
}
|
|
940
794
|
return cs
|
|
941
795
|
}
|
|
942
796
|
|
|
943
797
|
|
|
944
798
|
/**
|
|
945
|
-
* 文件層條件(
|
|
799
|
+
* 文件層條件(含$and、$or), o為cvOrmFind之複本(null原型, 鍵僅字串)
|
|
946
800
|
*
|
|
947
801
|
* @ignore
|
|
948
802
|
*/
|
|
949
|
-
function cvDoc(o, path,
|
|
950
|
-
if (depth > nDepthMax) {
|
|
951
|
-
err('find is too deep', path)
|
|
952
|
-
}
|
|
953
|
-
if (!isPlainObj(o)) {
|
|
954
|
-
err('condition must be an object', path)
|
|
955
|
-
}
|
|
956
|
-
let cvList = (x, kp) => {
|
|
957
|
-
if (!Array.isArray(x)) {
|
|
958
|
-
err('logical operator requires an array', kp)
|
|
959
|
-
}
|
|
960
|
-
//空陣列拒收(同MongoDB), 以免呼叫端之條件清單恰為空時成為恆真而刪全表
|
|
961
|
-
if (x.length === 0) {
|
|
962
|
-
err('logical operator requires a nonempty array', kp)
|
|
963
|
-
}
|
|
964
|
-
return getItems(x, kp).map((y, i) => cvDoc(y, `${kp}.${i}`, depth + 1, ctx))
|
|
965
|
-
}
|
|
803
|
+
function cvDoc(o, path, ctx) {
|
|
966
804
|
let cs = []
|
|
967
|
-
for (let k of
|
|
805
|
+
for (let k of Object.keys(o)) {
|
|
968
806
|
let x = o[k]
|
|
969
|
-
let kp =
|
|
970
|
-
if (
|
|
971
|
-
if (
|
|
972
|
-
|
|
973
|
-
}
|
|
974
|
-
else if (k === Op.or) {
|
|
975
|
-
cs.push(cvOr(cvList(x, kp)))
|
|
976
|
-
}
|
|
977
|
-
else if (k === Op.not) {
|
|
978
|
-
cs.push({ [Op.not]: cvDoc(x, kp, depth + 1, ctx) })
|
|
807
|
+
let kp = `${path}.${k}`
|
|
808
|
+
if (k === '$and' || k === '$or') {
|
|
809
|
+
if (!Array.isArray(x) || x.length === 0) {
|
|
810
|
+
err(`${k} requires a nonempty array`, kp)
|
|
979
811
|
}
|
|
980
|
-
|
|
981
|
-
|
|
812
|
+
let sub = []
|
|
813
|
+
for (let i = 0; i < x.length; i++) {
|
|
814
|
+
sub.push(cvDoc(x[i], `${kp}[${i}]`, ctx))
|
|
982
815
|
}
|
|
983
|
-
|
|
816
|
+
cs.push(cvGroup(k === '$and' ? cvAnd : cvOr, sub))
|
|
984
817
|
}
|
|
985
|
-
if (k[0] === '$') {
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
cs.push(cvOr(cvList(x, kp)))
|
|
991
|
-
}
|
|
992
|
-
else if (k === '$nor') {
|
|
993
|
-
cs.push({ [Op.not]: cvOr(cvList(x, kp)) })
|
|
994
|
-
}
|
|
995
|
-
else {
|
|
996
|
-
err(`unsupported operator [${k}]`, kp)
|
|
997
|
-
}
|
|
998
|
-
continue
|
|
818
|
+
else if (k[0] === '$') {
|
|
819
|
+
err(`unsupported operator [${k}]`, kp)
|
|
820
|
+
}
|
|
821
|
+
else {
|
|
822
|
+
cs.push(...cvField(k, x, kp, ctx))
|
|
999
823
|
}
|
|
1000
|
-
cs.push(...cvField(k, x, kp, depth + 1, ctx))
|
|
1001
824
|
}
|
|
1002
|
-
|
|
825
|
+
if (cs.length === 0) {
|
|
826
|
+
err('empty condition', path)
|
|
827
|
+
}
|
|
828
|
+
return cvGroup(cvAnd, cs)
|
|
1003
829
|
}
|
|
1004
830
|
|
|
1005
831
|
|
|
1006
832
|
/**
|
|
1007
|
-
* 以AND
|
|
833
|
+
* 以AND結合子式
|
|
1008
834
|
*
|
|
1009
835
|
* @ignore
|
|
1010
836
|
*/
|
|
1011
837
|
function cvAnd(cs) {
|
|
1012
|
-
if (cs.length === 0) {
|
|
1013
|
-
return { [Op.and]: [sqlTrue()] }
|
|
1014
|
-
}
|
|
1015
838
|
return { [Op.and]: cs }
|
|
1016
839
|
}
|
|
1017
840
|
|
|
1018
841
|
|
|
1019
842
|
/**
|
|
1020
|
-
* 以OR
|
|
843
|
+
* 以OR結合子式
|
|
1021
844
|
*
|
|
1022
845
|
* @ignore
|
|
1023
846
|
*/
|
|
1024
847
|
function cvOr(cs) {
|
|
1025
|
-
if (cs.length === 0) {
|
|
1026
|
-
return { [Op.and]: [sqlFalse()] }
|
|
1027
|
-
}
|
|
1028
848
|
return { [Op.or]: cs }
|
|
1029
849
|
}
|
|
1030
850
|
|
|
1031
851
|
|
|
852
|
+
/**
|
|
853
|
+
* 子式過多者遞迴二分為平衡樹, 避免SQLite之運算式樹深度與遞迴上限(見nFlat); 語意不變(AND與OR皆滿足結合律, 含NULL之三值邏輯), 子式順序不變
|
|
854
|
+
*
|
|
855
|
+
* @ignore
|
|
856
|
+
*/
|
|
857
|
+
function cvGroup(fn, cs) {
|
|
858
|
+
if (cs.length <= nFlat) {
|
|
859
|
+
return fn(cs)
|
|
860
|
+
}
|
|
861
|
+
let k = Math.ceil(cs.length / 2)
|
|
862
|
+
return fn([cvGroup(fn, cs.slice(0, k)), cvGroup(fn, cs.slice(k))])
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* 檢核find之形狀(系列白名單, 由wsemi之cvOrmFind單一擁有, 規則見R11)並回傳全新之null原型深複本, 後續一律只使用複本(檢查之值即使用之值)
|
|
868
|
+
* 註: 未給/undefined/{}回傳空物件(全部); 其餘非條件物件、白名單外之運算子與值、異常結構一律throw Error('invalid find: 原因 at [路徑]')
|
|
869
|
+
* 註: maxList傳999(SQLite之運算式樹深度上限); select與delAll於載入model與建立sequelize實例之前先呼叫本函數, 使形狀不合法者不連線、不送出任何SQL
|
|
870
|
+
* 註: 對複本再呼叫為冪等(複本本身符合白名單)
|
|
871
|
+
*
|
|
872
|
+
* @ignore
|
|
873
|
+
* @param {*} find 輸入查詢條件
|
|
874
|
+
* @returns {Object} 回傳null原型之條件複本, 無自有鍵代表全部
|
|
875
|
+
*/
|
|
876
|
+
function chkFindShape(find) {
|
|
877
|
+
return cvOrmFind(find, optShape)
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
|
|
1032
881
|
/**
|
|
1033
882
|
* 將查詢條件轉為sequelize之where, 供select與delAll共用, 令同一find於兩函數由同一SQL謂詞決定命中
|
|
1034
883
|
*
|
|
1035
|
-
* 註:
|
|
1036
|
-
*
|
|
1037
|
-
* 註:
|
|
884
|
+
* 註: 第一步經chkFindShape(wsemi之cvOrmFind)依系列白名單檢核並取複本, 其後只走訪複本; 白名單: 欄位等值{欄:純量|null}、
|
|
885
|
+
* $eq $ne $gt $gte $lt $lte(純量)、$in $nin(純量陣列)、$and $or(條件物件之非空陣列); 其餘(含$not $nor $regex $options $exists、RegExp、Symbol鍵、陣列值)一律拋錯
|
|
886
|
+
* 註: 未給、undefined或{}回傳{}(全部); 其餘非條件物件(null、陣列、字串、數值等)拋錯, 不再視為{}
|
|
887
|
+
* 註: 與欄位型別不符之純量、不存在之欄一律拋錯, 不得交sequelize之型別stringify(見本檔之chkScalar); NULL依SQL三值邏輯
|
|
1038
888
|
*
|
|
1039
889
|
* @param {*} find 輸入查詢條件
|
|
1040
890
|
* @param {Object} ctx 輸入轉換所需之依賴物件
|
|
1041
|
-
* @param {Function} ctx.escape
|
|
891
|
+
* @param {Function} [ctx.escape] 已不使用(原供$regex之LIKE字面常值; 第33項批1移除$regex), 保留接受以相容既有呼叫端
|
|
1042
892
|
* @param {Object} [ctx.attrs] 輸入執行where之model之rawAttributes, 供欄名解析(嚴格鍵: 鍵須與屬性名或資料庫欄名完全相符, 其餘拒收)、依欄位型別檢核純量與取得實際欄名; 未給時不解析鍵、不檢型別而交資料庫(見CLAUDE_rulebook.md之R4)
|
|
1043
|
-
* @param {String} [ctx.dialect]
|
|
893
|
+
* @param {String} [ctx.dialect] 已不使用(原供判定sqlite之LOWER僅折疊ASCII), 保留接受以相容既有呼叫端
|
|
1044
894
|
* @param {String} [ctx.pk] 輸入主鍵欄位名, 該欄之純量條件值依normPk正規化(如字串類主鍵欄之123即'123'), 未給時不正規化
|
|
1045
895
|
* @param {String} [ctx.dateNoTz] 輸入無時區日期時間字串之處置, 僅接受undefined(預設, 無時區者拒收)或'local'(依主機時區解析, 與以本地時間分量建構之Date同義), 其餘值拋錯; 僅作用於DATE欄, 供Jet等無時區後端之呼叫端明示啟用
|
|
1046
896
|
* @returns {Object} 回傳sequelize之where物件, 條件不合法時拋出錯誤
|
|
@@ -1052,18 +902,11 @@ function cvFind(find, ctx) {
|
|
|
1052
902
|
throw new Error(`invalid ctx: dateNoTz must be 'local' or undefined, got [${String(ctx.dateNoTz)}]`)
|
|
1053
903
|
}
|
|
1054
904
|
|
|
1055
|
-
//
|
|
1056
|
-
|
|
1057
|
-
return {}
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
//check, 非一般物件(class實例、Object.create(proto)、Date、Map等)之條件可能不在自有鍵上, 拒收而不當成{}
|
|
1061
|
-
if (!isPlainObj(find)) {
|
|
1062
|
-
err('find must be a plain object')
|
|
1063
|
-
}
|
|
905
|
+
//step1, 形狀檢核(R11), 其後一律只使用複本
|
|
906
|
+
let fd = chkFindShape(find)
|
|
1064
907
|
|
|
1065
908
|
//check, 空物件為全部
|
|
1066
|
-
if (Reflect.ownKeys(
|
|
909
|
+
if (Reflect.ownKeys(fd).length === 0) {
|
|
1067
910
|
return {}
|
|
1068
911
|
}
|
|
1069
912
|
|
|
@@ -1073,11 +916,11 @@ function cvFind(find, ctx) {
|
|
|
1073
916
|
}
|
|
1074
917
|
|
|
1075
918
|
//attrs之索引每次轉譯只建一次(內部欄位attrIdx, 不屬ctx之介面)
|
|
1076
|
-
return cvDoc(
|
|
919
|
+
return cvDoc(fd, 'find', ctx && ctx.attrs ? { ...ctx, attrIdx: buildAttrIdx(ctx.attrs) } : ctx)
|
|
1077
920
|
}
|
|
1078
921
|
|
|
1079
922
|
|
|
1080
|
-
export { getColKind, getColScale, normPk }
|
|
923
|
+
export { getColKind, getColScale, normPk, chkFindShape, inDateRange, genDateChecker }
|
|
1081
924
|
export default cvFind
|
|
1082
925
|
</code></pre>
|
|
1083
926
|
</article>
|
|
@@ -1093,7 +936,7 @@ export default cvFind
|
|
|
1093
936
|
<br class="clear">
|
|
1094
937
|
|
|
1095
938
|
<footer>
|
|
1096
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on
|
|
939
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Mon Oct 19 2026 23:23:31 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
1097
940
|
</footer>
|
|
1098
941
|
|
|
1099
942
|
<script>prettyPrint();</script>
|