w-orm-reladb 1.1.7 → 1.1.9
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 +3 -1
- package/dist/w-orm-reladb.umd.js +2 -2
- package/dist/w-orm-reladb.umd.js.map +1 -1
- package/docs/WOrmReladb.html +14 -14
- package/docs/WOrmReladb.mjs.html +61 -24
- package/docs/cvFind.mjs.html +277 -32
- package/docs/global.html +41 -7
- package/docs/importModels.mjs.html +1 -1
- package/docs/index.html +1 -1
- package/package.json +1 -1
- package/src/WOrmReladb.mjs +60 -23
- package/src/cvFind.mjs +276 -31
- 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 +23 -0
- package/test/api-suite.mjs +525 -0
- package/test/unit-cvfind.test.mjs +806 -5
- package/toolg/gDistRollup.mjs +2 -0
package/docs/cvFind.mjs.html
CHANGED
|
@@ -92,8 +92,17 @@ let mSymCmp = new Map([
|
|
|
92
92
|
])
|
|
93
93
|
|
|
94
94
|
|
|
95
|
-
//欄位層不接受之Symbol: 邏輯運算子於sequelize
|
|
96
|
-
|
|
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])
|
|
97
106
|
|
|
98
107
|
|
|
99
108
|
//LIKE之跳脫字元, 選用於各方言字串常值內皆無特殊意義者, 令ESCAPE子句不需依方言跳脫
|
|
@@ -134,14 +143,54 @@ let reIsoTz = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(:(\d{2})(\.\d+)?)?(Z|
|
|
|
134
143
|
let reDateOnly = /^(\d{4})-(\d{2})-(\d{2})$/
|
|
135
144
|
|
|
136
145
|
|
|
146
|
+
//無時區之ISO 8601日期時間字串(秒與小數秒可省), 僅呼叫端以ctx.dateNoTz明示'local'時收
|
|
147
|
+
let reIsoNoTz = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?$/
|
|
148
|
+
|
|
149
|
+
|
|
137
150
|
/**
|
|
138
|
-
*
|
|
151
|
+
* 依主機時區解析無時區之ISO 8601日期時間字串為Date, 不合法(格式不符、曆法上不存在、主機時區下不存在)者回傳null
|
|
152
|
+
* 註: 不用Date.parse, 因帶空白分隔等寫法屬實作定義; 以本地時間建構後逐欄回驗, 如2026-02-30、24:00:00、60秒、夏令跳過之時刻皆為null
|
|
153
|
+
* 註: 小數秒取前三位(截斷至毫秒); 夏令重疊之時刻取較早者(Date之行為)
|
|
154
|
+
*
|
|
155
|
+
* @ignore
|
|
156
|
+
*/
|
|
157
|
+
function parseNoTzLocal(s) {
|
|
158
|
+
let m = s.match(reIsoNoTz)
|
|
159
|
+
if (m === null) {
|
|
160
|
+
return null
|
|
161
|
+
}
|
|
162
|
+
let y = Number(m[1])
|
|
163
|
+
let mo = Number(m[2])
|
|
164
|
+
let d = Number(m[3])
|
|
165
|
+
let h = Number(m[4])
|
|
166
|
+
let mi = Number(m[5])
|
|
167
|
+
let sec = m[6] === undefined ? 0 : Number(m[6])
|
|
168
|
+
let ms = m[7] === undefined ? 0 : Number(`${m[7]}00`.slice(0, 3))
|
|
169
|
+
let t = new Date(y, mo - 1, d, h, mi, sec, ms)
|
|
170
|
+
if (t.getFullYear() !== y || t.getMonth() !== mo - 1 || t.getDate() !== d || t.getHours() !== h || t.getMinutes() !== mi || t.getSeconds() !== sec || t.getMilliseconds() !== ms) {
|
|
171
|
+
return null
|
|
172
|
+
}
|
|
173
|
+
if (!inDateRange(t.getTime())) {
|
|
174
|
+
return null //本地時間通過回驗, 但換算UTC後年份超出1–9999(如負偏移時區之9999-12-31晚間)
|
|
175
|
+
}
|
|
176
|
+
return t
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 判定年月日是否為曆法上存在之日期, 如2026-02-30不存在; 年須在1以上(年0000不收)
|
|
182
|
+
* 註: 不用Date.UTC(y,..)建構, 因其把0–99年映射為1900–1999而使年0001–0099之字串被誤拒; 以setUTCFullYear設定完整年份
|
|
139
183
|
*
|
|
140
184
|
* @ignore
|
|
141
185
|
*/
|
|
142
186
|
function isCalendarDate(y, m, d) {
|
|
143
|
-
let
|
|
144
|
-
|
|
187
|
+
let yn = Number(y)
|
|
188
|
+
if (yn < 1) {
|
|
189
|
+
return false
|
|
190
|
+
}
|
|
191
|
+
let t = new Date(0)
|
|
192
|
+
t.setUTCFullYear(yn, Number(m) - 1, Number(d))
|
|
193
|
+
return t.getUTCFullYear() === yn && t.getUTCMonth() === Number(m) - 1 && t.getUTCDate() === Number(d)
|
|
145
194
|
}
|
|
146
195
|
|
|
147
196
|
|
|
@@ -220,6 +269,149 @@ function getColKind(attrs, field) {
|
|
|
220
269
|
}
|
|
221
270
|
|
|
222
271
|
|
|
272
|
+
/**
|
|
273
|
+
* 取model屬性之資料庫欄名: attr.field(字串)優先, 否則為屬性名(sequelize define後恆有field, 手造attrs可能缺)
|
|
274
|
+
*
|
|
275
|
+
* @ignore
|
|
276
|
+
*/
|
|
277
|
+
function colOf(attrs, k) {
|
|
278
|
+
return attrs[k] && typeof attrs[k].field === 'string' ? attrs[k].field : k
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* 建立attrs之索引: 資料庫欄名 → 屬性名陣列(依定義順序), 供欄名解析與sequelize反查之核對
|
|
284
|
+
*
|
|
285
|
+
* @ignore
|
|
286
|
+
*/
|
|
287
|
+
function buildAttrIdx(attrs) {
|
|
288
|
+
let byCol = new Map()
|
|
289
|
+
for (let k of Object.keys(attrs)) {
|
|
290
|
+
let c = colOf(attrs, k)
|
|
291
|
+
let a = byCol.get(c)
|
|
292
|
+
if (a === undefined) {
|
|
293
|
+
a = []
|
|
294
|
+
byCol.set(c, a)
|
|
295
|
+
}
|
|
296
|
+
a.push(k)
|
|
297
|
+
}
|
|
298
|
+
return { byCol }
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* 核對attrs為sequelize之rawAttributes形狀: 一般物件(原型為Object.prototype或null)、無Symbol鍵、自有字串鍵皆可列舉
|
|
304
|
+
* 註: sequelize以rawAttributes[鍵]讀取(含原型鏈、Symbol鍵、不可列舉者), 而cvFind以Object.keys建索引、以自有屬性核對; 三者所見之屬性集合不同時,
|
|
305
|
+
* 反查核對與sequelize實際取用之定義分歧(如與Op.and相撞之Symbol屬性使sequelize把邏輯鍵映射為欄而條件消失); 真正之rawAttributes恆符合
|
|
306
|
+
*
|
|
307
|
+
* @ignore
|
|
308
|
+
*/
|
|
309
|
+
function chkAttrs(attrs) {
|
|
310
|
+
let p = typeof attrs === 'object' ? Object.getPrototypeOf(attrs) : undefined
|
|
311
|
+
if (!(p === Object.prototype || p === null) || Object.getOwnPropertySymbols(attrs).length > 0 || Object.getOwnPropertyNames(attrs).length !== Object.keys(attrs).length) {
|
|
312
|
+
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')
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* 核對欄名可安全交sequelize: 下列名稱會被sequelize於轉成SQL前改寫或重新解讀, 使資料庫實際比對之欄不是此名, 一律拒收
|
|
319
|
+
* 註: 空字串者(sequelize之_joinKeyValue遇空鍵只回值, 條件成常數而delAll刪全表)、含反引號/雙引號/單引號/方括號者(quoteIdentifier依方言刪除之, 如sqlite之`val`ue`成value)、含點者(JSON路徑、表.欄)、
|
|
320
|
+
* 前後皆$者($x$被當成欄引用; sequelize之isColString只比首尾字元, 故含換行者與單一$亦是, 不以正規式判定以免.不匹配換行)、
|
|
321
|
+
* Object.prototype之成員名與__proto__(sequelize以一般物件查表與賦值: 查到函數而不依欄型序列化、賦值__proto__使條件遺失而刪全表)
|
|
322
|
+
*
|
|
323
|
+
* @ignore
|
|
324
|
+
*/
|
|
325
|
+
function chkColName(n, path) {
|
|
326
|
+
if (n === '' || n in Object.prototype || n.indexOf('.') >= 0 || /[`"'[\]]/.test(n) || (n[0] === '$' && n[n.length - 1] === '$')) {
|
|
327
|
+
err(`unsupported column name [${n}]`, path)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* 屬性是否為VIRTUAL(非資料庫欄)
|
|
334
|
+
*
|
|
335
|
+
* @ignore
|
|
336
|
+
*/
|
|
337
|
+
function isVirtualAttr(at) {
|
|
338
|
+
let t = at && at.type
|
|
339
|
+
return !!t && (t instanceof DT.VIRTUAL || t.key === 'VIRTUAL')
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* 將條件之欄名解析為model之屬性名(嚴格鍵): 僅收屬性名完全相符、或資料庫欄名(attr.field)完全相符且唯一者, 其餘一律拒收, 不模擬資料庫之識別字解析
|
|
345
|
+
* 註: 欄名經三方各自解析: cvFind(判型別、主鍵、DATE值)、sequelize(屬性名映射欄名、以映射後之名稱反查屬性型別、引用層改寫字元)、資料庫(依定序折疊);
|
|
346
|
+
* 完全相符之名稱無論資料庫如何折疊皆解析為自己, 故只收完全相符者(另核對sequelize之反查與改寫, 見chkColName與下文), 使三方之解析必然一致, 見CLAUDE_rulebook.md之R4欄名解析
|
|
347
|
+
* 註: 以model之attrs為準, attrs缺少之欄與VIRTUAL屬性皆拒收; attrs未給者回傳null(不解析, 欄位型別為any, 交資料庫處置)
|
|
348
|
+
*
|
|
349
|
+
* @ignore
|
|
350
|
+
*/
|
|
351
|
+
function resolveAttr(attrs, idx, field, path) {
|
|
352
|
+
if (!attrs) {
|
|
353
|
+
return null
|
|
354
|
+
}
|
|
355
|
+
chkColName(field, path)
|
|
356
|
+
|
|
357
|
+
//①屬性名完全相符; ②資料庫欄名完全相符(多於一個者無從判定)
|
|
358
|
+
let name = null
|
|
359
|
+
if (Object.prototype.hasOwnProperty.call(attrs, field)) {
|
|
360
|
+
name = field
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
let hits = idx.byCol.get(field) || []
|
|
364
|
+
if (hits.length > 1) {
|
|
365
|
+
err(`ambiguous column name [${field}]`, path)
|
|
366
|
+
}
|
|
367
|
+
if (hits.length === 1) {
|
|
368
|
+
name = hits[0]
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
//其餘拒收; 提示僅供診斷(不分大小寫相同者), 不影響解析
|
|
373
|
+
if (name === null || isVirtualAttr(attrs[name])) {
|
|
374
|
+
let lf = field.toLowerCase()
|
|
375
|
+
let near = Object.keys(attrs).find((k) => !isVirtualAttr(attrs[k]) && (k.toLowerCase() === lf || colOf(attrs, k).toLowerCase() === lf))
|
|
376
|
+
err(`unknown column name [${field}]${near === undefined ? '' : `, did you mean [${near}]?`}`, path)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
//field已設者須為字串(sequelize保留原值並以String(field)映射, 退回屬性名則二者指向不同欄); 空字串由chkColName拒收
|
|
380
|
+
let at = attrs[name]
|
|
381
|
+
if (at && at.field !== undefined && typeof at.field !== 'string') {
|
|
382
|
+
err(`unsupported column name [${name}], its field must be a string`, path)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
//sequelize把屬性名之鍵映射為其欄名後, 以rawAttributes[欄名](名稱為該欄名之屬性)、其次fieldRawAttributesMap[欄名](欄名為該欄名之屬性, 多個取最後定義者)取得型別序列化值;
|
|
386
|
+
//反查所得須即本屬性, 否則值依他屬性之型別序列化(如DATE之值被INTEGER序列化為Date.toString文字, sqlite以文字比較而命中全部非NULL列), 拒收
|
|
387
|
+
//共用一欄者另須本屬性即最後定義者: 實體欄之型別由最後定義者決定(sync以欄名為鍵而後者覆寫), 先定義者即使反查為自己, sequelize之序列化亦與實體欄不符
|
|
388
|
+
let col = colOf(attrs, name)
|
|
389
|
+
chkColName(col, path)
|
|
390
|
+
let cs = idx.byCol.get(col)
|
|
391
|
+
let last = cs[cs.length - 1]
|
|
392
|
+
let s = Object.prototype.hasOwnProperty.call(attrs, col) ? col : last
|
|
393
|
+
if (s !== name) {
|
|
394
|
+
err(`ambiguous column name [${field}], its column [${col}] resolves to attribute [${s}]`, path)
|
|
395
|
+
}
|
|
396
|
+
if (last !== name) {
|
|
397
|
+
err(`ambiguous column name [${field}], its column [${col}] is shared with attribute [${last}], the last attribute defines the column type`, path)
|
|
398
|
+
}
|
|
399
|
+
return name
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* 判定時間(毫秒)換算UTC後年份是否在1–9999
|
|
405
|
+
* 註: sequelize以YYYY格式化DATE, 年10000起為5位數, sqlite以文字比較致{d:{$gt:年10000}}命中全部日期列; mssql之datetimeoffset範圍亦為0001–9999
|
|
406
|
+
*
|
|
407
|
+
* @ignore
|
|
408
|
+
*/
|
|
409
|
+
function inDateRange(ms) {
|
|
410
|
+
let y = new Date(ms).getUTCFullYear()
|
|
411
|
+
return y >= 1 && y <= 9999
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
|
|
223
415
|
/**
|
|
224
416
|
* 取DECIMAL欄之scale, 未指定者為0(mssql之DECIMAL預設為(18,0)), 非DECIMAL者為null
|
|
225
417
|
*
|
|
@@ -275,18 +467,23 @@ function isKindOk(v, kind, scale = null) {
|
|
|
275
467
|
return true
|
|
276
468
|
}
|
|
277
469
|
if (kind === 'date') {
|
|
470
|
+
//所有形式換算UTC後年份須在1–9999(inDateRange)
|
|
278
471
|
if (v instanceof Date) {
|
|
279
|
-
return !isNaN(v.getTime())
|
|
472
|
+
return !isNaN(v.getTime()) && inDateRange(v.getTime())
|
|
280
473
|
}
|
|
281
474
|
if (typeof v === 'number') {
|
|
282
475
|
//須在Date可表示之範圍(±8.64e15毫秒)內, 超出者被stringify為'Invalid date'而以文字比較, sqlite會命中全部日期列
|
|
283
|
-
return Number.isFinite(v) && !isNaN(new Date(v).getTime())
|
|
476
|
+
return Number.isFinite(v) && !isNaN(new Date(v).getTime()) && inDateRange(v)
|
|
284
477
|
}
|
|
285
478
|
if (typeof v !== 'string') {
|
|
286
479
|
return false
|
|
287
480
|
}
|
|
288
481
|
let m = v.match(reIsoTz)
|
|
289
|
-
|
|
482
|
+
if (m === null || !isCalendarDate(m[1], m[2], m[3])) {
|
|
483
|
+
return false
|
|
484
|
+
}
|
|
485
|
+
let ms = Date.parse(v.replace(' ', 'T'))
|
|
486
|
+
return !isNaN(ms) && inDateRange(ms)
|
|
290
487
|
}
|
|
291
488
|
if (kind === 'dateonly') {
|
|
292
489
|
//Date物件依主機時區取日期而結果隨主機而變, 故只收日期字串
|
|
@@ -392,10 +589,11 @@ function normPk(v, kind, scale = null) {
|
|
|
392
589
|
* 註: 物件、陣列等不得交sequelize之型別stringify, 否則FLOAT成'NaN'、INTEGER成'[object Object]'、DATE成本地今日00:00而誤刪
|
|
393
590
|
* 註: 主鍵欄(pk為true)之字串類、整數類與其他數值類以normPk正規化後回傳(如字串類主鍵欄之123即'123'), 無效或型別不符者拒收,
|
|
394
591
|
* 令select與delAll對主鍵之認定與selectByPk、del等主鍵函數一致; 非主鍵欄不轉型
|
|
592
|
+
* 註: kind為date且呼叫端以ctx.dateNoTz明示'local'(dateNoTz為'local')時, 無時區日期時間字串依主機時區轉為Date回傳(見parseNoTzLocal), 其餘值照舊判定
|
|
395
593
|
*
|
|
396
594
|
* @ignore
|
|
397
595
|
*/
|
|
398
|
-
function chkScalar(v, path, allowNull, kind = 'any', scale = null, pk = false) {
|
|
596
|
+
function chkScalar(v, path, allowNull, kind = 'any', scale = null, pk = false, dateNoTz = undefined) {
|
|
399
597
|
if (v === null) {
|
|
400
598
|
if (allowNull) {
|
|
401
599
|
return v
|
|
@@ -431,9 +629,21 @@ function chkScalar(v, path, allowNull, kind = 'any', scale = null, pk = false) {
|
|
|
431
629
|
}
|
|
432
630
|
err(`value [${String(v)}] does not match column type [${kind}]${scale !== null ? ` with scale [${scale}]` : ''}`, path)
|
|
433
631
|
}
|
|
632
|
+
if (kind === 'date' && dateNoTz === 'local' && typeof v === 'string') {
|
|
633
|
+
//呼叫端明示之無時區日期時間字串: 依主機時區解析並以Date送出, 與直接給Date同義; 不符者續由isKindOk判定(帶時區者照舊)
|
|
634
|
+
let t = parseNoTzLocal(v)
|
|
635
|
+
if (t !== null) {
|
|
636
|
+
return t
|
|
637
|
+
}
|
|
638
|
+
}
|
|
434
639
|
if (!isKindOk(v, kind, scale)) {
|
|
435
640
|
err(`value [${String(v)}] does not match column type [${kind}]${scale !== null ? ` with scale [${scale}]` : ''}`, path)
|
|
436
641
|
}
|
|
642
|
+
if (kind === 'date' && !(v instanceof Date)) {
|
|
643
|
+
//通過檢核之字串與epoch一律轉為Date送出: sequelize只在鍵為屬性名或欄名完全相符時依欄型序列化值, 大小寫變體鍵會原樣送出字串與epoch,
|
|
644
|
+
//sqlite以文字或型別序比較而命中錯列(如{D:{$gt:epoch}}刪光全部日期列); 轉為Date後與鍵之拼寫無關
|
|
645
|
+
return new Date(typeof v === 'string' ? Date.parse(v.replace(' ', 'T')) : v)
|
|
646
|
+
}
|
|
437
647
|
return v
|
|
438
648
|
}
|
|
439
649
|
|
|
@@ -462,14 +672,14 @@ function getItems(arr, path) {
|
|
|
462
672
|
*
|
|
463
673
|
* @ignore
|
|
464
674
|
*/
|
|
465
|
-
function chkRawSym(x, path, kind, scale, pk) {
|
|
675
|
+
function chkRawSym(x, path, kind, scale, pk, dateNoTz) {
|
|
466
676
|
if (x instanceof Sequelize.Utils.SequelizeMethod) {
|
|
467
677
|
return x
|
|
468
678
|
}
|
|
469
679
|
if (Array.isArray(x)) {
|
|
470
|
-
return getItems(x, path).map((y, i) => (y instanceof Sequelize.Utils.SequelizeMethod ? y : chkScalar(y, `${path}.${i}`, true, kind, scale, pk)))
|
|
680
|
+
return getItems(x, path).map((y, i) => (y instanceof Sequelize.Utils.SequelizeMethod ? y : chkScalar(y, `${path}.${i}`, true, kind, scale, pk, dateNoTz)))
|
|
471
681
|
}
|
|
472
|
-
return chkScalar(x, path, true, kind, scale, pk)
|
|
682
|
+
return chkScalar(x, path, true, kind, scale, pk, dateNoTz)
|
|
473
683
|
}
|
|
474
684
|
|
|
475
685
|
|
|
@@ -532,7 +742,7 @@ function normFlags(fs, path) {
|
|
|
532
742
|
*
|
|
533
743
|
* @ignore
|
|
534
744
|
*/
|
|
535
|
-
function cvRegex(field, re, options, path, ctx, kind) {
|
|
745
|
+
function cvRegex(field, col, re, options, path, ctx, kind) {
|
|
536
746
|
if (kind !== 'str' && kind !== 'any') {
|
|
537
747
|
err(`$regex requires a string column, column type is [${kind}]`, path)
|
|
538
748
|
}
|
|
@@ -554,6 +764,10 @@ function cvRegex(field, re, options, path, ctx, kind) {
|
|
|
554
764
|
err('$regex must be a string or RegExp', path)
|
|
555
765
|
}
|
|
556
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
|
+
}
|
|
557
771
|
if (ci && ctx.dialect === 'sqlite') {
|
|
558
772
|
for (let c of lit) {
|
|
559
773
|
if (c.charCodeAt(0) > 127 && c.toLowerCase() !== c.toUpperCase()) {
|
|
@@ -564,8 +778,7 @@ function cvRegex(field, re, options, path, ctx, kind) {
|
|
|
564
778
|
let pat = `%${lit.replace(/[!%_[]/g, (c) => `${cEsc}${c}`)}%`
|
|
565
779
|
let q = ctx.escape(pat)
|
|
566
780
|
if (ci) {
|
|
567
|
-
//LOWER
|
|
568
|
-
let col = ctx.attrs && ctx.attrs[field] && ctx.attrs[field].field ? ctx.attrs[field].field : field
|
|
781
|
+
//LOWER內須為資料庫之實際欄名col, model之屬性名與欄名可不同(欄名已由resolveAttr核對可安全交sequelize)
|
|
569
782
|
return Sequelize.where(Sequelize.fn('LOWER', Sequelize.col(col)), Op.like, Sequelize.literal(`LOWER(${q}) ESCAPE '${cEsc}'`))
|
|
570
783
|
}
|
|
571
784
|
return { [field]: { [Op.like]: Sequelize.literal(`${q} ESCAPE '${cEsc}'`) } }
|
|
@@ -577,7 +790,7 @@ function cvRegex(field, re, options, path, ctx, kind) {
|
|
|
577
790
|
*
|
|
578
791
|
* @ignore
|
|
579
792
|
*/
|
|
580
|
-
function cvIn(field, arr, path, neg, kind, scale, pk) {
|
|
793
|
+
function cvIn(field, arr, path, neg, kind, scale, pk, dateNoTz) {
|
|
581
794
|
if (!Array.isArray(arr)) {
|
|
582
795
|
err(`${neg ? '$nin' : '$in'} must be an array`, path)
|
|
583
796
|
}
|
|
@@ -588,17 +801,17 @@ function cvIn(field, arr, path, neg, kind, scale, pk) {
|
|
|
588
801
|
hasNull = true
|
|
589
802
|
return
|
|
590
803
|
}
|
|
591
|
-
vs.push(chkScalar(v, `${path}.${k}`, false, kind, scale, pk))
|
|
804
|
+
vs.push(chkScalar(v, `${path}.${k}`, false, kind, scale, pk, dateNoTz))
|
|
592
805
|
})
|
|
593
806
|
if (!neg) {
|
|
594
807
|
if (vs.length === 0) {
|
|
595
|
-
return hasNull ? { [field]: null } : sqlFalse()
|
|
808
|
+
return hasNull ? { [field]: null } : { [Op.and]: [{ [field]: null }, sqlFalse()] }
|
|
596
809
|
}
|
|
597
810
|
let c = { [field]: { [Op.in]: vs } }
|
|
598
811
|
return hasNull ? { [Op.or]: [c, { [field]: null }] } : c
|
|
599
812
|
}
|
|
600
813
|
if (vs.length === 0) {
|
|
601
|
-
return hasNull ? { [field]: { [Op.ne]: null } } : sqlTrue()
|
|
814
|
+
return hasNull ? { [field]: { [Op.ne]: null } } : { [Op.or]: [{ [field]: null }, sqlTrue()] }
|
|
602
815
|
}
|
|
603
816
|
let c = { [field]: { [Op.notIn]: vs } }
|
|
604
817
|
return hasNull ? { [Op.and]: [c, { [field]: { [Op.ne]: null } }] } : c
|
|
@@ -611,9 +824,6 @@ function cvIn(field, arr, path, neg, kind, scale, pk) {
|
|
|
611
824
|
* @ignore
|
|
612
825
|
*/
|
|
613
826
|
function cvField(field, v, path, depth, ctx) {
|
|
614
|
-
let kind = getColKind(ctx.attrs, field)
|
|
615
|
-
let scale = getColScale(ctx.attrs, field)
|
|
616
|
-
let pk = ctx.pk !== undefined && field === ctx.pk //主鍵欄之純量依normPk正規化(R8)
|
|
617
827
|
if (depth > nDepthMax) {
|
|
618
828
|
err('find is too deep', path)
|
|
619
829
|
}
|
|
@@ -623,15 +833,23 @@ function cvField(field, v, path, depth, ctx) {
|
|
|
623
833
|
if (v === undefined) {
|
|
624
834
|
err('value is undefined', path)
|
|
625
835
|
}
|
|
836
|
+
|
|
837
|
+
//欄名先解析為model之屬性名再判型別與主鍵(R4欄名解析); attrs已給而解析不到者拒收, attrs未給者為any, 交資料庫處置
|
|
838
|
+
let an = resolveAttr(ctx.attrs, ctx.attrIdx, field, path)
|
|
839
|
+
let col = an === null ? field : colOf(ctx.attrs, an) //資料庫實際欄名
|
|
840
|
+
let kind = an === null ? 'any' : getColKind(ctx.attrs, an)
|
|
841
|
+
let scale = an === null ? null : getColScale(ctx.attrs, an)
|
|
842
|
+
let pk = ctx.pk !== undefined && an === ctx.pk //主鍵欄之純量依normPk正規化(R8)
|
|
843
|
+
let dateNoTz = ctx.dateNoTz
|
|
626
844
|
if (Array.isArray(v)) {
|
|
627
845
|
//陣列值即$in(含null與空陣列之處置), 與MongoDB之陣列比對不同, 已文件化
|
|
628
|
-
return [cvIn(field, v, path, false, kind, scale, pk)]
|
|
846
|
+
return [cvIn(field, v, path, false, kind, scale, pk, dateNoTz)]
|
|
629
847
|
}
|
|
630
848
|
if (v instanceof RegExp) {
|
|
631
|
-
return [cvRegex(field, v, undefined, path, ctx, kind)]
|
|
849
|
+
return [cvRegex(field, col, v, undefined, path, ctx, kind)]
|
|
632
850
|
}
|
|
633
851
|
if (!isPlainObj(v)) {
|
|
634
|
-
return [{ [field]: chkScalar(v, path, true, kind, scale, pk) }]
|
|
852
|
+
return [{ [field]: chkScalar(v, path, true, kind, scale, pk, dateNoTz) }]
|
|
635
853
|
}
|
|
636
854
|
let ks = Reflect.ownKeys(v)
|
|
637
855
|
if (ks.length === 0) {
|
|
@@ -651,8 +869,19 @@ function cvField(field, v, path, depth, ctx) {
|
|
|
651
869
|
}
|
|
652
870
|
name = mSymCmp.get(k)
|
|
653
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
|
+
}
|
|
654
883
|
//其餘sequelize之Symbol運算子為呼叫端自負之原生寫法, 值經檢核後各自成一子式
|
|
655
|
-
cs.push({ [field]: { [k]: chkRawSym(x, kp, kind, scale, pk) } })
|
|
884
|
+
cs.push({ [field]: { [k]: chkRawSym(x, kp, kind, scale, pk, dateNoTz) } })
|
|
656
885
|
continue
|
|
657
886
|
}
|
|
658
887
|
}
|
|
@@ -661,7 +890,7 @@ function cvField(field, v, path, depth, ctx) {
|
|
|
661
890
|
}
|
|
662
891
|
if (mCmp[name] !== undefined) {
|
|
663
892
|
let allowNull = name === '$eq' || name === '$ne'
|
|
664
|
-
let y = chkScalar(x, kp, allowNull, kind, scale, pk)
|
|
893
|
+
let y = chkScalar(x, kp, allowNull, kind, scale, pk, dateNoTz)
|
|
665
894
|
if (Object.prototype.hasOwnProperty.call(simple, mCmp[name])) {
|
|
666
895
|
//同義之鍵(如$gt與Op.gt)各自成一子式, 不得互相覆寫
|
|
667
896
|
cs.push({ [field]: { [mCmp[name]]: y } })
|
|
@@ -672,7 +901,7 @@ function cvField(field, v, path, depth, ctx) {
|
|
|
672
901
|
}
|
|
673
902
|
}
|
|
674
903
|
else if (name === '$in' || name === '$nin') {
|
|
675
|
-
cs.push(cvIn(field, x, kp, name === '$nin', kind, scale, pk))
|
|
904
|
+
cs.push(cvIn(field, x, kp, name === '$nin', kind, scale, pk, dateNoTz))
|
|
676
905
|
}
|
|
677
906
|
else if (name === '$exists') {
|
|
678
907
|
if (x !== true && x !== false && x !== 0 && x !== 1) {
|
|
@@ -682,7 +911,7 @@ function cvField(field, v, path, depth, ctx) {
|
|
|
682
911
|
}
|
|
683
912
|
else if (name === '$regex') {
|
|
684
913
|
hasRegex = true
|
|
685
|
-
cs.push(cvRegex(field, x, v.$options, kp, ctx, kind))
|
|
914
|
+
cs.push(cvRegex(field, col, x, v.$options, kp, ctx, kind))
|
|
686
915
|
}
|
|
687
916
|
else if (name === '$options') {
|
|
688
917
|
//由$regex一併處理
|
|
@@ -701,6 +930,10 @@ function cvField(field, v, path, depth, ctx) {
|
|
|
701
930
|
if (v.$options !== undefined && !hasRegex) {
|
|
702
931
|
err('$options requires $regex', path)
|
|
703
932
|
}
|
|
933
|
+
if (cs.length === 0 && nSimple === 0) {
|
|
934
|
+
//如僅有值為undefined之$options: 不產生任何子式, 該條件即消失而成恆真, delAll會刪全表
|
|
935
|
+
err('operator object has no operator', path)
|
|
936
|
+
}
|
|
704
937
|
if (nSimple > 0) {
|
|
705
938
|
cs.unshift({ [field]: simple })
|
|
706
939
|
}
|
|
@@ -806,13 +1039,19 @@ function cvOr(cs) {
|
|
|
806
1039
|
* @param {*} find 輸入查詢條件
|
|
807
1040
|
* @param {Object} ctx 輸入轉換所需之依賴物件
|
|
808
1041
|
* @param {Function} ctx.escape 輸入依方言將字串轉為SQL字串常值之函數, 通常為sequelize實例之escape
|
|
809
|
-
* @param {Object} [ctx.attrs]
|
|
1042
|
+
* @param {Object} [ctx.attrs] 輸入執行where之model之rawAttributes, 供欄名解析(嚴格鍵: 鍵須與屬性名或資料庫欄名完全相符, 其餘拒收)、依欄位型別檢核純量與取得實際欄名; 未給時不解析鍵、不檢型別而交資料庫(見CLAUDE_rulebook.md之R4)
|
|
810
1043
|
* @param {String} [ctx.dialect] 輸入資料庫種類字串, 供判定方言能力(如sqlite之LOWER僅折疊ASCII)
|
|
811
1044
|
* @param {String} [ctx.pk] 輸入主鍵欄位名, 該欄之純量條件值依normPk正規化(如字串類主鍵欄之123即'123'), 未給時不正規化
|
|
1045
|
+
* @param {String} [ctx.dateNoTz] 輸入無時區日期時間字串之處置, 僅接受undefined(預設, 無時區者拒收)或'local'(依主機時區解析, 與以本地時間分量建構之Date同義), 其餘值拋錯; 僅作用於DATE欄, 供Jet等無時區後端之呼叫端明示啟用
|
|
812
1046
|
* @returns {Object} 回傳sequelize之where物件, 條件不合法時拋出錯誤
|
|
813
1047
|
*/
|
|
814
1048
|
function cvFind(find, ctx) {
|
|
815
1049
|
|
|
1050
|
+
//check, ctx.dateNoTz須為undefined或'local', 其餘值(含拼錯、大小寫不同)拋錯, 不靜默忽略
|
|
1051
|
+
if (ctx && ctx.dateNoTz !== undefined && ctx.dateNoTz !== 'local') {
|
|
1052
|
+
throw new Error(`invalid ctx: dateNoTz must be 'local' or undefined, got [${String(ctx.dateNoTz)}]`)
|
|
1053
|
+
}
|
|
1054
|
+
|
|
816
1055
|
//check, 非物件(undefined、null、字串、數值等純量)與陣列視為未給條件, 此為現況保留, 系列規格另案處理
|
|
817
1056
|
if (find === null || typeof find !== 'object' || Array.isArray(find)) {
|
|
818
1057
|
return {}
|
|
@@ -828,7 +1067,13 @@ function cvFind(find, ctx) {
|
|
|
828
1067
|
return {}
|
|
829
1068
|
}
|
|
830
1069
|
|
|
831
|
-
|
|
1070
|
+
//check, attrs須為rawAttributes形狀(見chkAttrs)
|
|
1071
|
+
if (ctx && ctx.attrs) {
|
|
1072
|
+
chkAttrs(ctx.attrs)
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
//attrs之索引每次轉譯只建一次(內部欄位attrIdx, 不屬ctx之介面)
|
|
1076
|
+
return cvDoc(find, '', 0, ctx && ctx.attrs ? { ...ctx, attrIdx: buildAttrIdx(ctx.attrs) } : ctx)
|
|
832
1077
|
}
|
|
833
1078
|
|
|
834
1079
|
|
|
@@ -848,7 +1093,7 @@ export default cvFind
|
|
|
848
1093
|
<br class="clear">
|
|
849
1094
|
|
|
850
1095
|
<footer>
|
|
851
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 16 2026
|
|
1096
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 16 2026 19:47:28 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
852
1097
|
</footer>
|
|
853
1098
|
|
|
854
1099
|
<script>prettyPrint();</script>
|
package/docs/global.html
CHANGED
|
@@ -149,7 +149,7 @@
|
|
|
149
149
|
|
|
150
150
|
<dt class="tag-source">Source:</dt>
|
|
151
151
|
<dd class="tag-source"><ul class="dummy"><li>
|
|
152
|
-
<a href="cvFind.mjs.html">cvFind.mjs</a>, <a href="cvFind.mjs.html#
|
|
152
|
+
<a href="cvFind.mjs.html">cvFind.mjs</a>, <a href="cvFind.mjs.html#line1001">line 1001</a>
|
|
153
153
|
</li></ul></dd>
|
|
154
154
|
|
|
155
155
|
|
|
@@ -349,7 +349,7 @@
|
|
|
349
349
|
|
|
350
350
|
|
|
351
351
|
|
|
352
|
-
<td class="description last"><p
|
|
352
|
+
<td class="description last"><p>輸入執行where之model之rawAttributes, 供欄名解析(嚴格鍵: 鍵須與屬性名或資料庫欄名完全相符, 其餘拒收)、依欄位型別檢核純量與取得實際欄名; 未給時不解析鍵、不檢型別而交資料庫(見CLAUDE_rulebook.md之R4)</p></td>
|
|
353
353
|
</tr>
|
|
354
354
|
|
|
355
355
|
|
|
@@ -421,6 +421,40 @@
|
|
|
421
421
|
</tr>
|
|
422
422
|
|
|
423
423
|
|
|
424
|
+
|
|
425
|
+
<tr>
|
|
426
|
+
|
|
427
|
+
<td class="name"><code>dateNoTz</code></td>
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
<td class="type">
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
<span class="param-type">String</span>
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
</td>
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
<td class="attributes">
|
|
442
|
+
|
|
443
|
+
<optional><br>
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
</td>
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
<td class="description last"><p>輸入無時區日期時間字串之處置, 僅接受undefined(預設, 無時區者拒收)或'local'(依主機時區解析, 與以本地時間分量建構之Date同義), 其餘值拋錯; 僅作用於DATE欄, 供Jet等無時區後端之呼叫端明示啟用</p></td>
|
|
455
|
+
</tr>
|
|
456
|
+
|
|
457
|
+
|
|
424
458
|
</tbody>
|
|
425
459
|
</table>
|
|
426
460
|
|
|
@@ -493,7 +527,7 @@
|
|
|
493
527
|
|
|
494
528
|
<dt class="tag-source">Source:</dt>
|
|
495
529
|
<dd class="tag-source"><ul class="dummy"><li>
|
|
496
|
-
<a href="cvFind.mjs.html">cvFind.mjs</a>, <a href="cvFind.mjs.html#
|
|
530
|
+
<a href="cvFind.mjs.html">cvFind.mjs</a>, <a href="cvFind.mjs.html#line197">line 197</a>
|
|
497
531
|
</li></ul></dd>
|
|
498
532
|
|
|
499
533
|
|
|
@@ -675,7 +709,7 @@
|
|
|
675
709
|
|
|
676
710
|
<dt class="tag-source">Source:</dt>
|
|
677
711
|
<dd class="tag-source"><ul class="dummy"><li>
|
|
678
|
-
<a href="cvFind.mjs.html">cvFind.mjs</a>, <a href="cvFind.mjs.html#
|
|
712
|
+
<a href="cvFind.mjs.html">cvFind.mjs</a>, <a href="cvFind.mjs.html#line375">line 375</a>
|
|
679
713
|
</li></ul></dd>
|
|
680
714
|
|
|
681
715
|
|
|
@@ -1260,7 +1294,7 @@
|
|
|
1260
1294
|
|
|
1261
1295
|
<dt class="tag-source">Source:</dt>
|
|
1262
1296
|
<dd class="tag-source"><ul class="dummy"><li>
|
|
1263
|
-
<a href="cvFind.mjs.html">cvFind.mjs</a>, <a href="cvFind.mjs.html#
|
|
1297
|
+
<a href="cvFind.mjs.html">cvFind.mjs</a>, <a href="cvFind.mjs.html#line397">line 397</a>
|
|
1264
1298
|
</li></ul></dd>
|
|
1265
1299
|
|
|
1266
1300
|
|
|
@@ -1451,7 +1485,7 @@ DECIMAL主鍵欄之值小數位多於scale者寫入時會被四捨五入而改
|
|
|
1451
1485
|
|
|
1452
1486
|
<dt class="tag-source">Source:</dt>
|
|
1453
1487
|
<dd class="tag-source"><ul class="dummy"><li>
|
|
1454
|
-
<a href="cvFind.mjs.html">cvFind.mjs</a>, <a href="cvFind.mjs.html#
|
|
1488
|
+
<a href="cvFind.mjs.html">cvFind.mjs</a>, <a href="cvFind.mjs.html#line490">line 490</a>
|
|
1455
1489
|
</li></ul></dd>
|
|
1456
1490
|
|
|
1457
1491
|
|
|
@@ -1703,7 +1737,7 @@ DECIMAL主鍵欄之值小數位多於scale者寫入時會被四捨五入而改
|
|
|
1703
1737
|
<br class="clear">
|
|
1704
1738
|
|
|
1705
1739
|
<footer>
|
|
1706
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 16 2026
|
|
1740
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 16 2026 19:47:28 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
1707
1741
|
</footer>
|
|
1708
1742
|
|
|
1709
1743
|
<script>prettyPrint();</script>
|
|
@@ -242,7 +242,7 @@ export default importModels
|
|
|
242
242
|
<br class="clear">
|
|
243
243
|
|
|
244
244
|
<footer>
|
|
245
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 16 2026
|
|
245
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 16 2026 19:47:28 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
246
246
|
</footer>
|
|
247
247
|
|
|
248
248
|
<script>prettyPrint();</script>
|
package/docs/index.html
CHANGED
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
<br class="clear">
|
|
72
72
|
|
|
73
73
|
<footer>
|
|
74
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 16 2026
|
|
74
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 16 2026 19:47:28 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
75
75
|
</footer>
|
|
76
76
|
|
|
77
77
|
<script>prettyPrint();</script>
|