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/test/api-suite.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import assert from 'assert'
|
|
2
2
|
import Sequelize from 'sequelize'
|
|
3
3
|
import wo from '../src/WOrmReladb.mjs'
|
|
4
|
+
import cvFind from '../src/cvFind.mjs'
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
//本檔為mssql與sqlite共用之測試套組, 令兩種後端以同一組斷言驗證, 避免各自漂移
|
|
@@ -121,10 +122,10 @@ export function defineSuites(label, getOpt) {
|
|
|
121
122
|
assert.deepStrictEqual(sp[1], { id: 'id-rosemary', name: 'rosemary(modify)', value: 123.456 })
|
|
122
123
|
})
|
|
123
124
|
|
|
124
|
-
it('select $regex
|
|
125
|
-
//rulebook R6: $options
|
|
126
|
-
let
|
|
127
|
-
assert.
|
|
125
|
+
it('select $regex(白名單外)整批reject', async function() {
|
|
126
|
+
//rulebook R6/R11: $regex、$options已移除, 於形狀階段reject; 需子字串/不分大小寫者請select後於記憶體過濾
|
|
127
|
+
let r = await w.select({ name: { $regex: 'PeT', $options: 'i' } }).then(() => null).catch((err) => String(err.message || err))
|
|
128
|
+
assert.ok(r !== null && r.indexOf('invalid find: unsupported operator [$regex]') === 0, String(r))
|
|
128
129
|
})
|
|
129
130
|
|
|
130
131
|
it('del 刪除3筆並保留kettle', async function() {
|
|
@@ -420,7 +421,7 @@ export function defineSuites(label, getOpt) {
|
|
|
420
421
|
assert.ok(r.REJECT.indexOf('closed') >= 0, `${t} 之錯誤訊息未指明已關閉: ${r.REJECT}`)
|
|
421
422
|
}
|
|
422
423
|
|
|
423
|
-
await chk('select', () => w.select(
|
|
424
|
+
await chk('select', () => w.select({}, cs))
|
|
424
425
|
await chk('selectByPk', () => w.selectByPk('k1', cs))
|
|
425
426
|
await chk('insert', () => w.insert({ id: 'k9' }, cs))
|
|
426
427
|
await chk('insertBulk', () => w.insertBulk({ id: 'k8' }, cs))
|
|
@@ -593,7 +594,7 @@ export function defineSuites(label, getOpt) {
|
|
|
593
594
|
assert.ok(r !== null, '未reject')
|
|
594
595
|
|
|
595
596
|
//交易內先前之寫入須保留, 本次之tx-2須不存在
|
|
596
|
-
let ssIn = await w.select(
|
|
597
|
+
let ssIn = await w.select({}, connState)
|
|
597
598
|
let ids = ssIn.map((v) => v.id).sort()
|
|
598
599
|
assert.deepStrictEqual(ids, ['tx-1'])
|
|
599
600
|
|
|
@@ -820,7 +821,7 @@ export function defineSuites(label, getOpt) {
|
|
|
820
821
|
assert.deepStrictEqual(md, [{ n: 1, nDeleted: 1, ok: 1 }])
|
|
821
822
|
|
|
822
823
|
//交易內select可查到暫時有效的數據
|
|
823
|
-
let ssIn = sortByName(await w.select(
|
|
824
|
+
let ssIn = sortByName(await w.select({}, connState))
|
|
824
825
|
assert.strictEqual(ssIn.length, 2)
|
|
825
826
|
assert.strictEqual(ssIn[0].name, 'kettle')
|
|
826
827
|
assert.strictEqual(ssIn[1].name, 'peter(modify)')
|
|
@@ -924,6 +925,7 @@ export function defineSuites(label, getOpt) {
|
|
|
924
925
|
})
|
|
925
926
|
|
|
926
927
|
//[說明, find, 預期命中之id]
|
|
928
|
+
//(第33項批1: 白名單外之寫法〔$not $nor $regex $exists、RegExp、Symbol Op、陣列值、$in/$nin含null、空$nin、空子式〕已移至下方removed, 一律reject)
|
|
927
929
|
let cases = [
|
|
928
930
|
//R4 七欄型之等值
|
|
929
931
|
['STRING 等值', { s: 's1' }, ['r1']],
|
|
@@ -946,70 +948,41 @@ export function defineSuites(label, getOpt) {
|
|
|
946
948
|
['DECIMAL $in', { dec: { $in: [2.5] } }, ['r2']],
|
|
947
949
|
['DATE $in', { d: { $in: [new Date('2019-01-01T00:00:00Z')] } }, ['r1']],
|
|
948
950
|
['INTEGER $nin', { i: { $nin: [1, 3] } }, ['r2', 'r4', 'r5']],
|
|
949
|
-
['INTEGER $not $gt', { i: { $not: { $gt: 2 } } }, ['r1', 'r2']],
|
|
950
|
-
['FLOAT $not $lte', { f: { $not: { $lte: 2.5 } } }, ['r3', 'r4', 'r5']],
|
|
951
|
-
['DATE $not $gt', { d: { $not: { $gt: new Date('2020-01-01T00:00:00Z') } } }, ['r1']],
|
|
952
951
|
['同欄多運算子', { i: { $gt: 1, $lt: 5 } }, ['r2', 'r3', 'r4']],
|
|
952
|
+
['同欄三運算子併入同一欄位物件', { i: { $gt: 1, $lt: 5, $ne: 3 } }, ['r2', 'r4']],
|
|
953
953
|
['DATE 以帶時區之ISO字串比較', { d: { $lt: '2020-01-01T00:00:00Z' } }, ['r1']],
|
|
954
954
|
['DATE 以帶偏移之ISO字串比較', { d: { $lt: '2020-01-01T08:00:00+08:00' } }, ['r1']],
|
|
955
955
|
['DOUBLE $gt', { p: { $gt: 3.5 } }, ['r4', 'r5']],
|
|
956
|
-
['DOUBLE $not $lte', { p: { $not: { $lte: 2.5 } } }, ['r3', 'r4', 'r5']],
|
|
957
956
|
['INTEGER 以數值字串比較', { i: '2' }, ['r2']],
|
|
958
957
|
//R3 邏輯
|
|
959
958
|
['$and', { $and: [{ i: { $gte: 2 } }, { b: false }] }, ['r2', 'r4']],
|
|
960
959
|
['$or', { $or: [{ s: 's1' }, { i: 5 }] }, ['r1', 'r5']],
|
|
961
|
-
['$
|
|
960
|
+
['$and 內含$or', { $and: [{ $or: [{ s: 's1' }, { i: 5 }] }, { b: true }] }, ['r1', 'r5']],
|
|
962
961
|
//R5 NULL
|
|
963
962
|
['{t:null}', { t: null }, ['r6']],
|
|
964
963
|
['$eq null', { t: { $eq: null } }, ['r6']],
|
|
965
|
-
['$in [null] 與{t:null}一致', { t: { $in: [null] } }, ['r6']],
|
|
966
|
-
['$in [null,值]', { t: { $in: [null, 'rose_mary'] } }, ['r3', 'r6']],
|
|
967
964
|
['$ne null', { t: { $ne: null } }, NN],
|
|
968
|
-
['$
|
|
969
|
-
['$nin
|
|
970
|
-
['
|
|
971
|
-
['
|
|
965
|
+
['$ne不含NULL(SQL三值)', { t: { $ne: 'rose_mary' } }, ['r1', 'r2', 'r4', 'r5']],
|
|
966
|
+
['$nin不含NULL(SQL三值)', { t: { $nin: ['rose_mary'] } }, ['r1', 'r2', 'r4', 'r5']],
|
|
967
|
+
['可攜寫法: 排除NULL', { $and: [{ t: { $ne: null } }, { t: { $ne: 'rose_mary' } }] }, ['r1', 'r2', 'r4', 'r5']],
|
|
968
|
+
['可攜寫法: 含NULL', { $or: [{ t: null }, { t: { $ne: 'rose_mary' } }] }, ['r1', 'r2', 'r4', 'r5', 'r6']],
|
|
972
969
|
['$in 空陣列為無', { i: { $in: [] } }, []],
|
|
973
|
-
['$nin 空陣列為全部', { i: { $nin: [] } }, ALL],
|
|
974
|
-
['$not $in 空陣列為全部', { i: { $not: { $in: [] } } }, ALL],
|
|
975
|
-
//R6 $regex
|
|
976
|
-
['$regex 字面底線', { t: { $regex: '_' } }, ['r3']],
|
|
977
|
-
['$regex 字面百分比', { t: { $regex: '%' } }, ['r4']],
|
|
978
|
-
['$regex 跳脫之方括號', { t: { $regex: '\\[a-c\\]' } }, ['r5']],
|
|
979
|
-
['$regex $options i', { t: { $regex: 'PET', $options: 'i' } }, ['r1', 'r2']],
|
|
980
|
-
['$regex RegExp i', { t: { $regex: /PET/i } }, ['r1', 'r2']],
|
|
981
|
-
['$regex $options $i(過渡期視同i)', { t: { $regex: 'PeT', $options: '$i' } }, ['r1', 'r2']],
|
|
982
|
-
['RegExp 作為值', { t: /PET/i }, ['r1', 'r2']],
|
|
983
|
-
['$not $regex', { t: { $not: { $regex: '_' } } }, ['r1', 'r2', 'r4', 'r5']],
|
|
984
970
|
//R2 特殊鍵
|
|
985
971
|
['欄名length不得使條件失效', { s: 's1', length: 0 }, ['r1']],
|
|
986
972
|
['欄名length帶運算子', { length: { $gte: 5 } }, ['r3', 'r4', 'r5']],
|
|
987
|
-
['欄名含$', { 'p$x': 'q' }, ['r1', 'r3']],
|
|
988
|
-
['Symbol Op.or', { [Op.or]: [{ s: 's1' }, { s: 's2' }] }, ['r1', 'r2']],
|
|
989
|
-
['Symbol Op.in', { i: { [Op.in]: [1, 2] } }, ['r1', 'r2']],
|
|
990
|
-
['陣列值為IN', { i: [1, 2] }, ['r1', 'r2']],
|
|
991
|
-
['陣列值含null同$in', { t: [null, 'rose_mary'] }, ['r3', 'r6']],
|
|
992
|
-
//空子式為恆真/恆假, 不得被濾除而改變語意
|
|
993
|
-
['$or 含空條件為全部', { $or: [{}, { s: 's1' }] }, ALL],
|
|
994
|
-
['$nor 含空條件為無', { $nor: [{}] }, []],
|
|
995
|
-
['$nor 含空條件與他式為無', { $nor: [{}, { s: 's1' }] }, []],
|
|
996
|
-
['$not $regex i', { t: { $not: { $regex: 'PET', $options: 'i' } } }, ['r3', 'r4', 'r5']],
|
|
997
|
-
['$regex 空字串為非null', { t: { $regex: '' } }, NN],
|
|
998
|
-
['$regex 空RegExp', { t: { $regex: new RegExp('') } }, NN],
|
|
999
|
-
['$exists 1', { i: { $exists: 1 } }, NN],
|
|
1000
|
-
['$exists 0', { i: { $exists: 0 } }, ['r6']],
|
|
1001
|
-
['$options null視同未給(大小寫依定序)', { t: { $regex: 'rose', $options: null } }, ['r3']],
|
|
1002
973
|
['DECIMAL 以不超過scale之字串', { dec: '1.25' }, ['r1']],
|
|
1003
974
|
['DATE 以epoch數值比較', { d: { $lt: Date.parse('2020-01-01T00:00:00Z') } }, ['r1']],
|
|
1004
|
-
|
|
1005
|
-
|
|
975
|
+
|
|
976
|
+
//審計02(規格句 R5): 欄名正確時, 空$in引用其欄位(f IS NULL AND 1 = 0)後, 命中集合須為無且不使其他分支失效; select命中 = delAll刪除(R1)
|
|
977
|
+
['TEXT $in 空陣列為無', { t: { $in: [] } }, []],
|
|
978
|
+
['主鍵欄 $in 空陣列為無', { id: { $in: [] } }, []],
|
|
979
|
+
['DATE 欄 $in 空陣列為無', { d: { $in: [] } }, []],
|
|
980
|
+
['BOOLEAN 欄 $in 空陣列為無', { b: { $in: [] } }, []],
|
|
981
|
+
['$and 含空$in 為無', { $and: [{ t: { $in: [] } }, { s: 's1' }] }, []],
|
|
982
|
+
['$or 含空$in 不使其他分支失效', { $or: [{ t: { $in: [] } }, { s: 's1' }] }, ['r1']],
|
|
983
|
+
['$in空與$nin並存為無', { t: { $in: [], $nin: ['x'] } }, []],
|
|
1006
984
|
//BOOLEAN之比較依1/0
|
|
1007
985
|
['BOOLEAN $gt false', { b: { $gt: false } }, ['r1', 'r3', 'r5']],
|
|
1008
|
-
['BOOLEAN $not $gt true', { b: { $not: { $gt: true } } }, NN],
|
|
1009
|
-
//同義之鍵各自成立, Symbol之$not同$not
|
|
1010
|
-
['同義之鍵不得互相覆寫', { i: { $gt: 3, [Op.gt]: 1 } }, ['r4', 'r5']],
|
|
1011
|
-
['Symbol Op.not', { i: { [Op.not]: { [Op.gt]: 2 } } }, ['r1', 'r2']],
|
|
1012
|
-
['$and與Op.and並存', { $and: [{ i: { $gte: 2 } }], [Op.and]: [{ i: { $lte: 3 } }] }, ['r2', 'r3']],
|
|
1013
986
|
]
|
|
1014
987
|
|
|
1015
988
|
for (let [t, find, exp] of cases) {
|
|
@@ -1036,54 +1009,129 @@ export function defineSuites(label, getOpt) {
|
|
|
1036
1009
|
})
|
|
1037
1010
|
|
|
1038
1011
|
//[說明, find, reject訊息須含之字串]
|
|
1012
|
+
//第33項批1: 形狀不合法者由wsemi之cvOrmFind於連線前reject(訊息格式invalid find: 原因 at [路徑]), 翻譯層(欄名嚴格鍵、欄位型別、主鍵)之reject維持
|
|
1039
1013
|
let bads = [
|
|
1040
1014
|
['未知運算子', { i: { $foo: 1 } }, 'unsupported operator [$foo]'],
|
|
1041
1015
|
['頂層未知運算子', { $foo: 1 }, 'unsupported operator [$foo]'],
|
|
1042
1016
|
['sequelize專屬名稱', { s: { $like: 's%' } }, 'unsupported operator [$like]'],
|
|
1043
1017
|
['頂層$not', { $not: { s: 's1' } }, 'unsupported operator [$not]'],
|
|
1044
1018
|
['MongoDB之$type', { i: { $type: 'number' } }, 'unsupported operator [$type]'],
|
|
1045
|
-
['比較運算子之值為物件', { d: { $gt: { x: 1 } } }, 'value
|
|
1019
|
+
['比較運算子之值為物件', { d: { $gt: { x: 1 } } }, 'unsupported value type'],
|
|
1046
1020
|
['DATE欄收非日期字串', { d: { $lt: 'garbage' } }, 'does not match column type'],
|
|
1047
1021
|
['數值欄收非數值字串', { i: { $lt: 'abc' } }, 'does not match column type'],
|
|
1048
1022
|
['字串欄收數值', { s: 1 }, 'does not match column type'],
|
|
1049
|
-
['
|
|
1050
|
-
['
|
|
1051
|
-
['$options不支援之旗標', { t: { $regex: 'p', $options: 'm' } }, 'supports [i] only'],
|
|
1052
|
-
['undefined值', { s: undefined }, 'value is undefined'],
|
|
1053
|
-
['NaN', { i: NaN }, 'number must be finite'],
|
|
1023
|
+
['undefined值', { s: undefined }, 'undefined is not allowed'],
|
|
1024
|
+
['NaN', { i: NaN }, 'number is not finite'],
|
|
1054
1025
|
['空運算子物件', { i: {} }, 'empty operator object'],
|
|
1055
|
-
['巢狀欄位物件', { s: { a: 1 } }, '
|
|
1056
|
-
['空字串欄名', { '': 1 }, '
|
|
1057
|
-
['非sequelize運算子之Symbol', { [Symbol('or')]: [{ s: 's1' }] }, '
|
|
1058
|
-
['欄位層非運算子之Symbol', { i: { [Symbol('gt')]: 1 } }, '
|
|
1059
|
-
['欄位層Op.or', { i: { [Sequelize.Op.or]: [{ [Sequelize.Op.eq]: 1 }], [Sequelize.Op.gt]: 10 } }, '
|
|
1060
|
-
['稀疏陣列', { $and: new Array(2) }, '
|
|
1061
|
-
['$regex含NUL', { t: { $regex: 'a' + String.fromCharCode(0) + 'x' } }, 'NUL'],
|
|
1026
|
+
['巢狀欄位物件', { s: { a: 1 } }, 'unexpected key'],
|
|
1027
|
+
['空字串欄名', { '': 1 }, 'unsupported column name'],
|
|
1028
|
+
['非sequelize運算子之Symbol', { [Symbol('or')]: [{ s: 's1' }] }, 'symbol key is not allowed'],
|
|
1029
|
+
['欄位層非運算子之Symbol', { i: { [Symbol('gt')]: 1 } }, 'symbol key is not allowed'],
|
|
1030
|
+
['欄位層Op.or', { i: { [Sequelize.Op.or]: [{ [Sequelize.Op.eq]: 1 }], [Sequelize.Op.gt]: 10 } }, 'symbol key is not allowed'],
|
|
1031
|
+
['稀疏陣列', { $and: new Array(2) }, 'sparse array hole'],
|
|
1062
1032
|
['整數欄超出安全整數', { i: Number('9007199254740993') }, 'does not match column type [int]'],
|
|
1063
|
-
['$and空陣列', { $and: [] }, '
|
|
1064
|
-
['$or空陣列', { $or: [] }, '
|
|
1065
|
-
['$nor空陣列', { $nor: [] }, '
|
|
1033
|
+
['$and空陣列', { $and: [] }, '$and with empty array'],
|
|
1034
|
+
['$or空陣列', { $or: [] }, '$or with empty array'],
|
|
1035
|
+
['$nor空陣列', { $nor: [] }, 'unsupported operator [$nor]'],
|
|
1066
1036
|
['DATE欄收無時區之日期字串', { d: { $lt: '2020-01-01' } }, 'does not match column type [date]'],
|
|
1067
1037
|
['DATE欄收不存在之日期', { d: { $lt: '2026-02-30T00:00:00Z' } }, 'does not match column type [date]'],
|
|
1068
1038
|
['DOUBLE欄收非數值字串', { p: { $lt: 'abc' } }, 'does not match column type [num]'],
|
|
1069
|
-
['$regex用於DOUBLE欄', { p: { $regex: '1' } }, '$regex requires a string column'],
|
|
1070
1039
|
['非十進位之數值字串', { i: { $lt: '0x10' } }, 'does not match column type [int]'],
|
|
1071
1040
|
['前後空白之數值字串', { i: ' 5' }, 'does not match column type [int]'],
|
|
1072
1041
|
['整數欄收指數或小數寫法之字串', { i: '1e3' }, 'does not match column type [int]'],
|
|
1073
1042
|
['DECIMAL欄收指數寫法之字串', { dec: '1e3' }, 'does not match column type [num]'],
|
|
1074
1043
|
['DECIMAL欄收超過scale之字串', { dec: '1.254' }, 'with scale [2]'],
|
|
1075
1044
|
['DATE欄收超出Date範圍之epoch', { d: { $lt: 1e16 } }, 'does not match column type [date]'],
|
|
1076
|
-
['原型上之條件', Object.create({ s: 's1' }), '
|
|
1077
|
-
['帶toStringTag之物件', { s: 's1', [Symbol.toStringTag]: 'Q' }, '
|
|
1078
|
-
['欄位層Op.or空陣列', { i: { [Sequelize.Op.or]: [] } }, '
|
|
1079
|
-
|
|
1045
|
+
['原型上之條件', Object.create({ s: 's1' }), 'condition must be a plain object'],
|
|
1046
|
+
['帶toStringTag之物件', { s: 's1', [Symbol.toStringTag]: 'Q' }, 'condition must be a plain object'],
|
|
1047
|
+
['欄位層Op.or空陣列', { i: { [Sequelize.Op.or]: [] } }, 'symbol key is not allowed'],
|
|
1048
|
+
|
|
1049
|
+
//第33項批1(R11): 白名單外之寫法一律reject(原為支援或部分支援, 見帳本〈破壞性變更紀錄(第33項批1)〉)
|
|
1050
|
+
['欄位層$not', { i: { $not: { $gt: 2 } } }, 'unsupported operator [$not]'],
|
|
1051
|
+
['$not包空$in', { i: { $not: { $in: [] } } }, 'unsupported operator [$not]'],
|
|
1052
|
+
['文件層$nor', { $nor: [{ s: 's1' }, { i: 5 }] }, 'unsupported operator [$nor]'],
|
|
1053
|
+
['$nor含空條件', { $nor: [{}] }, 'unsupported operator [$nor]'],
|
|
1054
|
+
['$exists true', { i: { $exists: true } }, 'unsupported operator [$exists]'],
|
|
1055
|
+
['$exists false', { i: { $exists: false } }, 'unsupported operator [$exists]'],
|
|
1056
|
+
['$regex', { t: { $regex: '_' } }, 'unsupported operator [$regex]'],
|
|
1057
|
+
['$regex + $options', { t: { $regex: 'PET', $options: 'i' } }, 'unsupported operator [$regex]'],
|
|
1058
|
+
['$regex RegExp值', { t: { $regex: /PET/i } }, 'unsupported operator [$regex]'],
|
|
1059
|
+
['單獨$options', { t: { $options: 'i' } }, 'unsupported operator [$options]'],
|
|
1060
|
+
['$regex用於非字串欄', { i: { $regex: '1' } }, 'unsupported operator [$regex]'],
|
|
1061
|
+
['RegExp作為值', { t: /PET/i }, 'unsupported value type [RegExp]'],
|
|
1062
|
+
['陣列值即$in(已移除)', { i: [1, 2] }, 'array value is not supported, use $in'],
|
|
1063
|
+
['空陣列值', { t: [] }, 'array value is not supported, use $in'],
|
|
1064
|
+
['陣列值含null', { t: [null, 'rose_mary'] }, 'array value is not supported, use $in'],
|
|
1065
|
+
['Symbol Op.or', { [Op.or]: [{ s: 's1' }, { s: 's2' }] }, 'symbol key is not allowed'],
|
|
1066
|
+
['Symbol Op.in', { i: { [Op.in]: [1, 2] } }, 'symbol key is not allowed'],
|
|
1067
|
+
['Symbol Op.notIn空陣列', { i: { [Op.notIn]: [] } }, 'symbol key is not allowed'],
|
|
1068
|
+
['Symbol Op.not', { i: { [Op.not]: { [Op.gt]: 2 } } }, 'symbol key is not allowed'],
|
|
1069
|
+
['Symbol Op.between', { i: { [Op.between]: [1, 2] } }, 'symbol key is not allowed'],
|
|
1070
|
+
['Symbol Op.like', { t: { [Op.like]: 'p%' } }, 'symbol key is not allowed'],
|
|
1071
|
+
['同義之$lt與Op.lt', { i: { $lt: 3, [Op.lt]: 100 } }, 'symbol key is not allowed'],
|
|
1072
|
+
['$and與Op.and並存', { $and: [{ i: { $gte: 2 } }], [Op.and]: [{ i: { $lte: 3 } }] }, 'symbol key is not allowed'],
|
|
1073
|
+
['$in含null', { t: { $in: [null, 'rose_mary'] } }, 'null is not allowed here'],
|
|
1074
|
+
['$in:[null]', { t: { $in: [null] } }, 'null is not allowed here'],
|
|
1075
|
+
['$nin含null', { t: { $nin: [null, 'rose_mary'] } }, 'null is not allowed here'],
|
|
1076
|
+
['$nin:[null]', { t: { $nin: [null] } }, 'null is not allowed here'],
|
|
1077
|
+
['比較運算子之值為null', { i: { $gt: null } }, 'null is not allowed here'],
|
|
1078
|
+
['空$nin', { i: { $nin: [] } }, '$nin with empty array is always true'],
|
|
1079
|
+
['$or含空$nin', { $or: [{ t: { $nin: [] } }, { s: 'zz' }] }, '$nin with empty array is always true'],
|
|
1080
|
+
['$or含空條件', { $or: [{}, { s: 's1' }] }, 'empty condition'],
|
|
1081
|
+
['$and含空條件', { $and: [{}] }, 'empty condition'],
|
|
1082
|
+
['欄名含$', { 'p$x': 'q' }, 'unsupported column name [p$x]'],
|
|
1083
|
+
['頂層null(原為全部)', null, 'condition must be a plain object'],
|
|
1084
|
+
['頂層陣列(原為全部)', [], 'condition must be a plain object'],
|
|
1085
|
+
['頂層字串(原為全部)', 's1', 'condition must be a plain object'],
|
|
1086
|
+
|
|
1087
|
+
//run04(規格句 R4欄名解析 嚴格鍵): 鍵須與屬性名或資料庫欄名完全相符, 其餘一律於轉譯階段reject, 不倚賴資料庫之識別字解析(兩方言同結果、兩函數訊息相同);
|
|
1088
|
+
//原sqlite下型別不符之變體鍵使delAll刪除全部非NULL列, 並有sqlite隱含欄rowid、引號被sequelize刪除而成既有欄、mssql之全形/尾端空白/可忽略字元等價之繞過
|
|
1089
|
+
//(第33項批1: 尾端空白、全形空白、可忽略字元等含非字母數字底線者於形狀階段即reject為unsupported column name; 全形字母、等價字母等仍屬欄名文法, 由嚴格鍵reject為unknown column name)
|
|
1090
|
+
['欄名大小寫變體 × 非數值字串', { I: { $lt: 'abc' } }, 'unknown column name [I]'],
|
|
1091
|
+
['欄名大小寫變體 × 非日期字串', { D: { $lt: 'garbage' } }, 'unknown column name [D]'],
|
|
1092
|
+
['欄名全形變體(mssql預設定序視為同一識別字)', { 'i': { $lt: 'abc' } }, 'unknown column name'],
|
|
1093
|
+
['欄名尾端空白(mssql忽略識別字尾端空白)', { 'i ': { $lt: 'abc' } }, 'unsupported column name'],
|
|
1094
|
+
['欄名尾端全形空白', { ['i' + String.fromCharCode(0x3000)]: { $lt: 'abc' } }, 'unsupported column name'],
|
|
1095
|
+
['欄名附加可忽略字元(mssql預設定序視為同一識別字)', { ['i' + String.fromCharCode(0xFEFF)]: { $lt: 'abc' } }, 'unsupported column name'],
|
|
1096
|
+
['欄名以mssql預設定序之等價字元(U+026A)取代i', { [String.fromCharCode(0x26A)]: { $lt: 'abc' } }, 'unknown column name'],
|
|
1097
|
+
['sqlite隱含欄rowid(attrs未列)', { rowid: { $lt: 'abc' } }, 'unknown column name [rowid]'],
|
|
1098
|
+
['欄名含反引號(sqlite之quoteIdentifier刪除之而成i)', { 'i`': { $lt: 'abc' } }, 'unsupported column name'],
|
|
1099
|
+
['欄名含方括號(mssql之quoteIdentifier刪除之而成i)', { 'i[]': { $lt: 'abc' } }, 'unsupported column name'],
|
|
1100
|
+
['欄名含單引號', { 'i\'': { $lt: 'abc' } }, 'unsupported column name'],
|
|
1101
|
+
['欄名含雙引號', { 'i"': { $lt: 'abc' } }, 'unsupported column name'],
|
|
1102
|
+
['欄名含點(JSON路徑/表.欄)', { 'i.x': { $lt: 'abc' } }, 'unsupported column name'],
|
|
1103
|
+
['Object.prototype成員名', { constructor: { $lt: 'abc' } }, 'unsupported column name [constructor]'],
|
|
1104
|
+
//run04(sol H1)於第33項批1: $options不再受理, 僅有$options者為白名單外之運算子
|
|
1105
|
+
['僅$options為undefined', { s: { $options: undefined } }, 'unsupported operator [$options]'],
|
|
1106
|
+
//審計02 外審 opus [2](規格句 R4 DATE: 換算UTC後年份須在1–9999): 年10000起 sequelize 以5位數格式化, sqlite 以文字比較致 $gt 命中並刪除全部日期列
|
|
1107
|
+
['DATE欄收UTC年10000之Date', { d: { $gt: new Date(253402300800000) } }, 'does not match column type [date]'],
|
|
1108
|
+
['DATE欄收UTC年10000之epoch', { d: { $gt: 253402300800000 } }, 'does not match column type [date]'],
|
|
1109
|
+
['DATE欄收UTC年10000之ISO字串', { d: { $gt: '9999-12-31T23:30:00-05:00' } }, 'does not match column type [date]'],
|
|
1110
|
+
['DATE欄收UTC年0000之epoch', { d: { $lt: -62135596800001 } }, 'does not match column type [date]'],
|
|
1111
|
+
['讀值之getter不被執行而reject(存取器屬性)', Object.defineProperty({}, 's', {
|
|
1080
1112
|
enumerable: true,
|
|
1081
1113
|
get: () => {
|
|
1082
1114
|
throw new Error('getter boom')
|
|
1083
1115
|
}
|
|
1084
|
-
}), '
|
|
1116
|
+
}), 'accessor property is not allowed'],
|
|
1085
1117
|
]
|
|
1086
1118
|
|
|
1119
|
+
//run04(規格句 R3欄位引用不變式、R4嚴格鍵、系列最低要求②③): attrs已給時不存在之欄 × 空集合各入口於轉譯階段reject(原常數形式使delAll清空全表;
|
|
1120
|
+
//HEAD改為引用欄位後仰賴資料庫回報, 現由嚴格鍵直接reject, 兩函數訊息相同, mssql同一語句多次引用不存在之欄而err.message為空之情形不再可達)
|
|
1121
|
+
for (let [t, col, find] of [
|
|
1122
|
+
['空$in', 'nocol', { nocol: { $in: [] } }],
|
|
1123
|
+
['$or含空$in', 'nocol', { $or: [{ nocol: { $in: [] } }, { id: 'zz' }] }],
|
|
1124
|
+
['$and含空$in', 'nocol', { $and: [{ nocol: { $in: [] } }, { id: 'r1' }] }],
|
|
1125
|
+
['欄名打錯且空$in', 'sttus', { sttus: { $in: [] } }],
|
|
1126
|
+
['欄名打錯且$in有值', 'sttus', { sttus: { $in: ['x'] } }],
|
|
1127
|
+
]) {
|
|
1128
|
+
bads.push([`不存在之欄 × ${t}`, find, `unknown column name [${col}]`])
|
|
1129
|
+
}
|
|
1130
|
+
//合法值 × 各欄型之大小寫變體鍵: 不論型別相符與否一律reject(原與屬性名鍵同結果, 見帳本〈破壞性變更紀錄〉)
|
|
1131
|
+
for (let [col, val] of [['s', 's1'], ['t', 'peter'], ['i', 1], ['f', 1.5], ['p', 1.5], ['dec', 1.25], ['b', true], ['d', new Date('2019-01-01T00:00:00Z')], ['d', '2019-01-01T00:00:00Z'], ['d', { $gt: Date.parse('2030-01-01T00:00:00Z') }]]) {
|
|
1132
|
+
bads.push([`欄名大小寫變體(${col}) × 合法值`, { [col.toUpperCase()]: val }, `unknown column name [${col.toUpperCase()}]`])
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1087
1135
|
for (let [t, find, msg] of bads) {
|
|
1088
1136
|
it(`不合法條件兩函數皆reject且資料不變: ${t}`, async function() {
|
|
1089
1137
|
//rulebook R3/R4/R6/R7: 不得靜默忽略或改變語意; 兩函數訊息相同且各發一次error事件
|
|
@@ -1190,13 +1238,10 @@ export function defineSuites(label, getOpt) {
|
|
|
1190
1238
|
['等值', { id: 123 }, ['123']],
|
|
1191
1239
|
['$eq', { id: { $eq: 45 } }, ['45']],
|
|
1192
1240
|
['$in', { id: { $in: [123, 45] } }, ['123', '45']],
|
|
1193
|
-
['陣列值即$in', { id: [123, 'abc'] }, ['123', 'abc']],
|
|
1194
1241
|
['$nin', { id: { $nin: [123] } }, ['45', 'abc']],
|
|
1195
1242
|
['$ne', { id: { $ne: 45 } }, ['123', 'abc']],
|
|
1196
|
-
['$not', { id: { $not: { $eq: 123 } } }, ['45', 'abc']],
|
|
1197
1243
|
['$gt以字串比較', { id: { $gt: 2 } }, ['45', 'abc']],
|
|
1198
1244
|
['$or', { $or: [{ id: 123 }, { name: 'nabc' }] }, ['123', 'abc']],
|
|
1199
|
-
['Op.in', { id: { [Sequelize.Op.in]: [45] } }, ['45']],
|
|
1200
1245
|
]
|
|
1201
1246
|
for (let [t, find, exp] of cases) {
|
|
1202
1247
|
it(`select與delAll 主鍵欄之數字條件值視為字串且兩者一致: ${t}`, async function() {
|
|
@@ -1315,6 +1360,37 @@ export function defineSuites(label, getOpt) {
|
|
|
1315
1360
|
})
|
|
1316
1361
|
}
|
|
1317
1362
|
|
|
1363
|
+
|
|
1364
|
+
//run04(規格句 R4欄名解析 嚴格鍵、R8): 主鍵欄之非完全相符鍵一律於轉譯階段reject, 不倚賴資料庫之識別字解析(原大小寫變體於兩方言、
|
|
1365
|
+
//mssql之全形/尾端空白/可忽略字元等價皆繞過R8而命中並刪除id=12; 合法值之變體亦reject, 見帳本〈破壞性變更紀錄〉)
|
|
1366
|
+
let ws = String.fromCharCode(0x3000)
|
|
1367
|
+
let fe = String.fromCharCode(0xFEFF)
|
|
1368
|
+
let ip = String.fromCharCode(0x26A)
|
|
1369
|
+
let badKeys = [
|
|
1370
|
+
['欄名大小寫變體+前導零', { ID: '012' }, 'unknown column name [ID]'],
|
|
1371
|
+
['欄名大小寫變體+整數寫法字串', { ID: '12' }, 'unknown column name [ID]'],
|
|
1372
|
+
['欄名大小寫變體+$in字串', { Id: { $in: ['12', '5'] } }, 'unknown column name [Id]'],
|
|
1373
|
+
['欄名大小寫變體+非整數', { Id: 12.4 }, 'unknown column name [Id]'],
|
|
1374
|
+
['欄名全形變體+前導零', { 'ID': '012' }, 'unknown column name'],
|
|
1375
|
+
['欄名尾端空白+前導零', { 'id ': '012' }, 'unsupported column name'],
|
|
1376
|
+
['欄名大寫+尾端空白+前導零', { 'ID ': '012' }, 'unsupported column name'],
|
|
1377
|
+
['欄名尾端全形空白+前導零', { ['id' + ws]: '012' }, 'unsupported column name'],
|
|
1378
|
+
['欄名附加可忽略字元+前導零', { ['id' + fe]: '012' }, 'unsupported column name'],
|
|
1379
|
+
['欄名以U+026A取代i+前導零', { [ip + 'd']: '012' }, 'unknown column name'],
|
|
1380
|
+
['欄名含反引號+前導零', { 'i`d': '012' }, 'unsupported column name'],
|
|
1381
|
+
['欄名含方括號+前導零', { 'i[d]': '012' }, 'unsupported column name'],
|
|
1382
|
+
['欄名含單引號+前導零', { 'i\'d': '012' }, 'unsupported column name'],
|
|
1383
|
+
]
|
|
1384
|
+
for (let [t, find, msg] of badKeys) {
|
|
1385
|
+
it(`select與delAll 主鍵欄之非完全相符鍵皆reject且不刪除: ${t}`, async function() {
|
|
1386
|
+
let rs = await errOf(wi.select(find))
|
|
1387
|
+
let rd = await errOf(wi.delAll(find))
|
|
1388
|
+
assert.ok(rs !== null && rs.indexOf(msg) >= 0, `select: ${rs}`)
|
|
1389
|
+
assert.strictEqual(rd, rs)
|
|
1390
|
+
assert.deepStrictEqual(await ids(), [5, 12])
|
|
1391
|
+
})
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1318
1394
|
})
|
|
1319
1395
|
|
|
1320
1396
|
describe('DECIMAL主鍵欄', function() {
|
|
@@ -1359,5 +1435,413 @@ export function defineSuites(label, getOpt) {
|
|
|
1359
1435
|
|
|
1360
1436
|
})
|
|
1361
1437
|
|
|
1438
|
+
describe(`${label} dateNoTz`, function() {
|
|
1439
|
+
|
|
1440
|
+
//審計02(規格句 R4 ctx.dateNoTz②④⑤): cvFind之ctx.dateNoTz='local'(由呼叫端明示, 供w-orm-mdb等無時區日期後端)——
|
|
1441
|
+
//無時區字串依主機時區解析, select與delAll之命中須等於以本地時間分量建構之Date之命中(命中集合由JS端獨立算出); 不合法值仍reject且資料不變; 未給時維持reject
|
|
1442
|
+
//以cvFind直接轉譯後交sequelize執行(WOrmReladb本身不設定該選項, R4③)
|
|
1443
|
+
|
|
1444
|
+
let sq = null
|
|
1445
|
+
let md = null
|
|
1446
|
+
|
|
1447
|
+
//withTz, 暫時切換主機時區; 還原須以賦值而非delete: Windows上delete process.env.TZ不會還原主機時區(實測仍停在最後設定之時區), 故載入時先記下主機時區名稱
|
|
1448
|
+
let hostTz = Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
1449
|
+
let withTz = async (tz, fn) => {
|
|
1450
|
+
let old = process.env.TZ
|
|
1451
|
+
if (tz !== null) {
|
|
1452
|
+
process.env.TZ = tz
|
|
1453
|
+
}
|
|
1454
|
+
try {
|
|
1455
|
+
return await fn()
|
|
1456
|
+
}
|
|
1457
|
+
finally {
|
|
1458
|
+
if (tz !== null) {
|
|
1459
|
+
if (old !== undefined) {
|
|
1460
|
+
process.env.TZ = old
|
|
1461
|
+
}
|
|
1462
|
+
else if (process.platform === 'win32') {
|
|
1463
|
+
process.env.TZ = hostTz
|
|
1464
|
+
}
|
|
1465
|
+
else {
|
|
1466
|
+
delete process.env.TZ
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
let L = (a) => new Date(a[0], a[1] - 1, a[2], a[3] || 0, a[4] || 0, a[5] || 0, a[6] || 0)
|
|
1473
|
+
let rowsDef = {
|
|
1474
|
+
r1: [2026, 1, 1, 0, 0, 0, 0],
|
|
1475
|
+
r2: [2026, 6, 15, 8, 30, 15, 123],
|
|
1476
|
+
r3: [2026, 6, 15, 8, 30, 15, 0],
|
|
1477
|
+
r4: [2026, 12, 31, 23, 59, 59, 999],
|
|
1478
|
+
}
|
|
1479
|
+
let ALL = ['r1', 'r2', 'r3', 'r4', 'r5']
|
|
1480
|
+
let ctx = (ex = {}) => ({ escape: (v) => sq.escape(v), attrs: md.rawAttributes, dialect: label, pk: 'id', ...ex })
|
|
1481
|
+
let ids = async () => (await md.findAll({ raw: true })).map((v) => v.id).sort()
|
|
1482
|
+
let orc = (fn) => Object.entries(rowsDef).filter(([, a]) => fn(L(a).getTime())).map(([id]) => id).sort()
|
|
1483
|
+
let T = (a) => L(a).getTime()
|
|
1484
|
+
let S0 = [2026, 6, 15, 8, 30, 15, 0]
|
|
1485
|
+
|
|
1486
|
+
before(async function() {
|
|
1487
|
+
let o = getOpt()
|
|
1488
|
+
if (label === 'sqlite') {
|
|
1489
|
+
sq = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
|
|
1490
|
+
}
|
|
1491
|
+
else {
|
|
1492
|
+
let u = new URL(o.url)
|
|
1493
|
+
sq = new Sequelize(o.db, decodeURIComponent(u.username), decodeURIComponent(u.password), { dialect: 'mssql', host: u.hostname, port: Number(u.port), logging: false })
|
|
1494
|
+
}
|
|
1495
|
+
md = sq.define('tdtz', { id: { type: Sequelize.STRING, primaryKey: true }, d: { type: Sequelize.DATE } }, { tableName: 'tdtz', timestamps: false })
|
|
1496
|
+
await md.sync({ force: true })
|
|
1497
|
+
})
|
|
1498
|
+
|
|
1499
|
+
after(async function() {
|
|
1500
|
+
if (md) {
|
|
1501
|
+
await md.drop()
|
|
1502
|
+
}
|
|
1503
|
+
if (sq) {
|
|
1504
|
+
await sq.close()
|
|
1505
|
+
}
|
|
1506
|
+
})
|
|
1507
|
+
|
|
1508
|
+
let seed = async () => {
|
|
1509
|
+
await md.destroy({ where: {} })
|
|
1510
|
+
let rs = Object.entries(rowsDef).map(([id, a]) => ({ id, d: L(a) }))
|
|
1511
|
+
rs.push({ id: 'r5', d: null })
|
|
1512
|
+
await md.bulkCreate(rs)
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
//[說明, find, 預期命中之id的述詞(t: 該列之毫秒時間; 於withTz內以當下時區之本地時間分量求值, 與被測實作無共用程式碼)或id陣列]
|
|
1516
|
+
let cases = [
|
|
1517
|
+
['等值(T分隔,秒)', { d: '2026-01-01T00:00:00' }, (t) => t === T([2026, 1, 1])],
|
|
1518
|
+
['等值(空白分隔)', { d: '2026-06-15 08:30:15' }, (t) => t === T(S0)],
|
|
1519
|
+
['等值(省秒)', { d: '2026-01-01T00:00' }, (t) => t === T([2026, 1, 1])],
|
|
1520
|
+
['等值(毫秒.123)', { d: '2026-06-15T08:30:15.123' }, (t) => t === T([2026, 6, 15, 8, 30, 15, 123])],
|
|
1521
|
+
['等值(.123456截斷至123ms)', { d: '2026-06-15T08:30:15.123456' }, (t) => t === T([2026, 6, 15, 8, 30, 15, 123])],
|
|
1522
|
+
['$gt', { d: { $gt: '2026-06-15T08:30:15' } }, (t) => t > T(S0)],
|
|
1523
|
+
['$gte', { d: { $gte: '2026-06-15T08:30:15' } }, (t) => t >= T(S0)],
|
|
1524
|
+
['$lt', { d: { $lt: '2026-06-15T08:30:15' } }, (t) => t < T(S0)],
|
|
1525
|
+
['$lte', { d: { $lte: '2026-06-15T08:30:15' } }, (t) => t <= T(S0)],
|
|
1526
|
+
['$ne(SQL三值不含NULL列)', { d: { $ne: '2026-06-15T08:30:15' } }, (t) => t !== T(S0)],
|
|
1527
|
+
['$in', { d: { $in: ['2026-01-01T00:00:00', '2026-12-31T23:59:59.999'] } }, (t) => t === T([2026, 1, 1]) || t === T([2026, 12, 31, 23, 59, 59, 999])],
|
|
1528
|
+
['$nin(SQL三值不含NULL列)', { d: { $nin: ['2026-01-01T00:00:00', '2026-12-31T23:59:59.999'] } }, (t) => t !== T([2026, 1, 1]) && t !== T([2026, 12, 31, 23, 59, 59, 999])],
|
|
1529
|
+
['範圍$gte+$lt', { d: { $gte: '2026-06-15T00:00:00', $lt: '2026-06-16T00:00:00' } }, (t) => t >= T([2026, 6, 15]) && t < T([2026, 6, 16])],
|
|
1530
|
+
['$gte+$lte同欄(取代原Op.between)', { d: { $gte: '2026-06-15T08:30:15', $lte: '2026-12-31T23:59:59.999' } }, (t) => t >= T(S0) && t <= T([2026, 12, 31, 23, 59, 59, 999])],
|
|
1531
|
+
['$in混有帶時區字串', { d: { $in: ['2026-01-01T00:00:00', '2026-06-15T00:30:15.123Z'] } }, (t) => t === T([2026, 1, 1]) || t === Date.parse('2026-06-15T00:30:15.123Z')],
|
|
1532
|
+
['null照舊', { d: null }, ['r5']],
|
|
1533
|
+
]
|
|
1534
|
+
|
|
1535
|
+
for (let tz of [null, 'UTC', 'America/New_York']) {
|
|
1536
|
+
for (let [t, find, pe] of cases) {
|
|
1537
|
+
it(`dateNoTz=local 之 select 與 delAll 命中一致且等於本地時間分量之命中 [TZ=${tz === null ? 'host' : tz}]: ${t}`, async function() {
|
|
1538
|
+
await withTz(tz, async () => {
|
|
1539
|
+
//預期值於該時區下以本地時間分量獨立求出(非經被測實作)
|
|
1540
|
+
let exp = typeof pe === 'function' ? orc(pe) : pe
|
|
1541
|
+
await seed()
|
|
1542
|
+
let w = cvFind(find, ctx({ dateNoTz: 'local' }))
|
|
1543
|
+
let hit = (await md.findAll({ where: w, raw: true })).map((v) => v.id).sort()
|
|
1544
|
+
assert.deepStrictEqual(hit, exp, 'select之命中')
|
|
1545
|
+
let r = await md.destroy({ where: w })
|
|
1546
|
+
let left = await ids()
|
|
1547
|
+
let gone = ALL.filter((v) => left.indexOf(v) < 0)
|
|
1548
|
+
assert.deepStrictEqual(gone, hit, 'delAll之刪除')
|
|
1549
|
+
assert.strictEqual(r, hit.length)
|
|
1550
|
+
})
|
|
1551
|
+
})
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
//不合法值與未給時之拒收, 於轉譯階段即拋錯(不交給資料庫), 屬純函數行為, 由test/unit-cvfind.test.mjs之「審計02」組逐案斷言
|
|
1556
|
+
//WOrmReladb未接線dateNoTz, 故不在此以select/delAll重述
|
|
1557
|
+
})
|
|
1558
|
+
|
|
1559
|
+
|
|
1560
|
+
describe(`${label} 欄名別名與撞名`, function() {
|
|
1561
|
+
|
|
1562
|
+
//審計02 外審修正輪2、run04 重構(規格句 R4欄名解析〔嚴格鍵〕、R4 DATE通過檢核者轉Date送出; 系列最低要求①②): 屬性名≠欄名之model於資料庫上實跑——
|
|
1563
|
+
//(1)資料庫欄名 ≡ 屬性名: select命中 = delAll刪除 = 由seed數據獨立推得之id集合(大小寫變體等非完全相符之鍵一律reject, 見bads); (2)型別不符reject且資料不變;
|
|
1564
|
+
//(3)撞名(屬性ra之欄名rb恰為屬性rb之名稱, sequelize把鍵ra映射為rb後以rawAttributes.rb之型別序列化值): 反查所得非本屬性者reject(不論型別類別), 資料不變
|
|
1565
|
+
//以cvFind直接轉譯後交sequelize之findAll/destroy執行(與WOrmReladb之select/delAll同一路徑), 同dateNoTz段
|
|
1566
|
+
|
|
1567
|
+
let sq = null
|
|
1568
|
+
let md = null
|
|
1569
|
+
let ALL = ['r1', 'r2', 'r3', 'r4']
|
|
1570
|
+
let ctx = () => ({ escape: (v) => sq.escape(v), attrs: md.rawAttributes, dialect: label, pk: 'id' })
|
|
1571
|
+
let ids = async () => (await md.findAll({ raw: true })).map((v) => v.id).sort()
|
|
1572
|
+
let hit = async (find) => (await md.findAll({ where: cvFind(find, ctx()), raw: true })).map((v) => v.id).sort()
|
|
1573
|
+
//errOf, 執行fn並回傳其拋出之訊息(未拋錯為null); cvFind於轉譯階段同步拋錯, 故須包在函數內
|
|
1574
|
+
let errOf = async (fn) => {
|
|
1575
|
+
try {
|
|
1576
|
+
await fn()
|
|
1577
|
+
return null
|
|
1578
|
+
}
|
|
1579
|
+
catch (e) {
|
|
1580
|
+
return String(e.message)
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
before(async function() {
|
|
1585
|
+
let o = getOpt()
|
|
1586
|
+
if (label === 'sqlite') {
|
|
1587
|
+
sq = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
|
|
1588
|
+
}
|
|
1589
|
+
else {
|
|
1590
|
+
let u = new URL(o.url)
|
|
1591
|
+
sq = new Sequelize(o.db, decodeURIComponent(u.username), decodeURIComponent(u.password), { dialect: 'mssql', host: u.hostname, port: Number(u.port), logging: false })
|
|
1592
|
+
}
|
|
1593
|
+
md = sq.define('talias', {
|
|
1594
|
+
id: { type: Sequelize.STRING, primaryKey: true },
|
|
1595
|
+
ageNum: { type: Sequelize.INTEGER, field: 'age_num' },
|
|
1596
|
+
title: { type: Sequelize.TEXT, field: 'physical_title' },
|
|
1597
|
+
at: { type: Sequelize.DATE, field: 'at_col' },
|
|
1598
|
+
ra: { type: Sequelize.DATE, field: 'rb' }, //欄名rb, 恰為下一屬性之名稱
|
|
1599
|
+
rb: { type: Sequelize.INTEGER, field: 'rc' },
|
|
1600
|
+
}, { tableName: 'talias', timestamps: false })
|
|
1601
|
+
await md.sync({ force: true })
|
|
1602
|
+
})
|
|
1603
|
+
|
|
1604
|
+
after(async function() {
|
|
1605
|
+
if (md) {
|
|
1606
|
+
await md.drop()
|
|
1607
|
+
}
|
|
1608
|
+
if (sq) {
|
|
1609
|
+
await sq.close()
|
|
1610
|
+
}
|
|
1611
|
+
})
|
|
1612
|
+
|
|
1613
|
+
let seed = async () => {
|
|
1614
|
+
await md.destroy({ where: {} })
|
|
1615
|
+
await md.bulkCreate([
|
|
1616
|
+
{ id: 'r1', ageNum: 5, title: 'rose_mary', at: new Date('2019-01-01T00:00:00Z'), ra: new Date('2019-01-01T00:00:00Z'), rb: 5 },
|
|
1617
|
+
{ id: 'r2', ageNum: 6, title: 'Peter', at: new Date('2026-06-15T00:00:00Z'), ra: new Date('2026-06-15T00:00:00Z'), rb: 6 },
|
|
1618
|
+
{ id: 'r3', ageNum: 7, title: '100%', at: new Date('2026-06-01T00:00:00Z'), ra: new Date('2026-06-01T00:00:00Z'), rb: 7 },
|
|
1619
|
+
{ id: 'r4' },
|
|
1620
|
+
])
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
//[說明, 屬性名, 資料庫欄名, 值, 預期命中之id]; 預期命中由seed()之數據獨立推得(非取自同一實作之屬性名鍵命中, run05 sol F08、opus [8]):
|
|
1624
|
+
//ageNum: r1=5 r2=6 r3=7 r4=NULL; title: r1='rose_mary' r2='Peter' r3='100%' r4=NULL; at: r1=2019-01-01 r2=2026-06-15 r3=2026-06-01 r4=NULL
|
|
1625
|
+
let alias = [
|
|
1626
|
+
['INTEGER 等值', 'ageNum', 'age_num', 5, ['r1']],
|
|
1627
|
+
['INTEGER 等值(另一列)', 'ageNum', 'age_num', 6, ['r2']],
|
|
1628
|
+
['INTEGER $gt', 'ageNum', 'age_num', { $gt: 5 }, ['r2', 'r3']],
|
|
1629
|
+
['INTEGER 數值字串', 'ageNum', 'age_num', '7', ['r3']],
|
|
1630
|
+
['INTEGER 空$in', 'ageNum', 'age_num', { $in: [] }, []],
|
|
1631
|
+
['TEXT 等值', 'title', 'physical_title', 'Peter', ['r2']],
|
|
1632
|
+
['DATE $lt ISO字串', 'at', 'at_col', { $lt: '2026-06-01T00:00:00Z' }, ['r1']],
|
|
1633
|
+
['DATE $gt epoch', 'at', 'at_col', { $gt: Date.parse('2020-01-01T00:00:00Z') }, ['r2', 'r3']],
|
|
1634
|
+
['DATE $in 混合', 'at', 'at_col', { $in: [new Date('2019-01-01T00:00:00Z'), '2026-06-15T00:00:00Z', Date.parse('2026-06-02T00:00:00Z')] }, ['r1', 'r2']],
|
|
1635
|
+
['DATE 等值Date', 'at', 'at_col', new Date('2019-01-01T00:00:00Z'), ['r1']],
|
|
1636
|
+
['DATE null', 'at', 'at_col', null, ['r4']],
|
|
1637
|
+
]
|
|
1638
|
+
for (let [t, prop, col, val, exp] of alias) {
|
|
1639
|
+
it(`資料庫欄名 ≡ 屬性名, select命中 = delAll刪除: ${t}`, async function() {
|
|
1640
|
+
await seed()
|
|
1641
|
+
let a = await hit({ [prop]: val })
|
|
1642
|
+
assert.deepStrictEqual(a, exp, 'select: 屬性名鍵之命中須等於由seed數據獨立推得者')
|
|
1643
|
+
let b = await hit({ [col]: val })
|
|
1644
|
+
assert.deepStrictEqual(b, exp, 'select: 欄名鍵之命中須等於由seed數據獨立推得者')
|
|
1645
|
+
await md.destroy({ where: cvFind({ [col]: val }, ctx()) })
|
|
1646
|
+
let left = await ids()
|
|
1647
|
+
assert.deepStrictEqual(ALL.filter((v) => left.indexOf(v) < 0), exp, 'delAll: 刪除者須等於由seed數據獨立推得者')
|
|
1648
|
+
})
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
//未給attrs(深引用者可不給; 鍵不解析而交資料庫, R4)之空集合各入口經資料庫實跑: 原badsNoCol段於嚴格鍵改制後移除, 此處保留其資料庫實跑之證據(run05 opus [9])
|
|
1652
|
+
//屬性名鍵仍由sequelize映射為資料庫欄名; 不存在之欄須由資料庫回報而不得清空或刪除
|
|
1653
|
+
let ctx0 = () => ({ escape: (v) => sq.escape(v), dialect: label })
|
|
1654
|
+
let hit0 = async (find) => (await md.findAll({ where: cvFind(find, ctx0()), raw: true })).map((v) => v.id).sort()
|
|
1655
|
+
it('未給attrs: 空集合$in於資料庫實跑之命中(A2), 欄名經sequelize映射', async function() {
|
|
1656
|
+
await seed()
|
|
1657
|
+
assert.deepStrictEqual(await hit0({ ageNum: { $in: [] } }), [])
|
|
1658
|
+
assert.deepStrictEqual(await hit0({ age_num: { $in: [] } }), [])
|
|
1659
|
+
assert.deepStrictEqual(await hit0({ ageNum: 5 }), ['r1'])
|
|
1660
|
+
})
|
|
1661
|
+
for (let [t, find] of [
|
|
1662
|
+
['$in:[]', { nocol: { $in: [] } }],
|
|
1663
|
+
['$or含空$in', { $or: [{ nocol: { $in: [] } }, { id: 'zz' }] }],
|
|
1664
|
+
]) {
|
|
1665
|
+
it(`未給attrs: 不存在之欄 × 空集合由資料庫回報而reject, 資料不變: ${t}`, async function() {
|
|
1666
|
+
await seed()
|
|
1667
|
+
let rs = await errOf(() => md.findAll({ where: cvFind(find, ctx0()), raw: true }))
|
|
1668
|
+
let rd = await errOf(() => md.destroy({ where: cvFind(find, ctx0()) }))
|
|
1669
|
+
assert.ok(rs !== null, 'select未reject')
|
|
1670
|
+
assert.ok(rd !== null, 'delAll未reject')
|
|
1671
|
+
assert.deepStrictEqual(await ids(), ALL)
|
|
1672
|
+
})
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
//型別不符須於轉譯階段reject: 原sqlite下 age_num < 'abc' 恆真而delAll刪除全部非NULL列
|
|
1676
|
+
let bads = [
|
|
1677
|
+
['整數欄之欄名 × 非數值字串', { age_num: { $lt: 'abc' } }, '[int]'],
|
|
1678
|
+
['整數欄之欄名大小寫變體(不解析)', { AGE_NUM: 'abc' }, 'unknown column name [AGE_NUM]'],
|
|
1679
|
+
['DATE欄之欄名 × 非日期字串', { at_col: { $lt: 'garbage' } }, '[date]'],
|
|
1680
|
+
['整數欄之欄名 × $regex(白名單外)', { age_num: { $regex: '1' } }, 'unsupported operator [$regex]'],
|
|
1681
|
+
['DATE欄之欄名大小寫變體 × 合法epoch', { AT_COL: { $gt: Date.parse('2030-01-01T00:00:00Z') } }, 'unknown column name [AT_COL]'],
|
|
1682
|
+
['屬性rb(欄rc) × 非數值字串', { rb: { $lt: 'abc' } }, '[int]'],
|
|
1683
|
+
['欄名rc × 非數值字串', { rc: { $lt: 'abc' } }, '[int]'],
|
|
1684
|
+
]
|
|
1685
|
+
for (let [t, find, msg] of bads) {
|
|
1686
|
+
it(`型別不符reject且資料不變: ${t}`, async function() {
|
|
1687
|
+
await seed()
|
|
1688
|
+
let rs = await errOf(() => md.findAll({ where: cvFind(find, ctx()), raw: true }))
|
|
1689
|
+
let rd = await errOf(() => md.destroy({ where: cvFind(find, ctx()) }))
|
|
1690
|
+
assert.ok(rs !== null && rs.indexOf(msg) >= 0, `select: ${rs}`)
|
|
1691
|
+
assert.ok(rd !== null && rd.indexOf(msg) >= 0, `delAll: ${rd}`)
|
|
1692
|
+
assert.deepStrictEqual(await ids(), ALL)
|
|
1693
|
+
})
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
//撞名: 屬性ra(DATE, 欄rb)之值若照常轉Date送出, sequelize把鍵ra映射為rb後取到屬性rb(INTEGER)之型別, 以NUMBER序列化而成Date.toString文字,
|
|
1697
|
+
//sqlite以文字比較而命中並刪除全部非NULL之rb列(HEAD對直接給Date者即如此); 反查所得非本屬性者須reject(不論型別類別)
|
|
1698
|
+
let clash = [
|
|
1699
|
+
['DATE $lt ISO字串', { ra: { $lt: '2026-01-01T00:00:00Z' } }],
|
|
1700
|
+
['DATE 等值Date', { ra: new Date('2019-01-01T00:00:00Z') }],
|
|
1701
|
+
['DATE $gt epoch', { ra: { $gt: Date.parse('2030-01-01T00:00:00Z') } }],
|
|
1702
|
+
['DATE $in', { ra: { $in: ['2019-01-01T00:00:00Z'] } }],
|
|
1703
|
+
['DATE $nin', { ra: { $nin: [new Date('2019-01-01T00:00:00Z')] } }],
|
|
1704
|
+
]
|
|
1705
|
+
for (let [t, find] of clash) {
|
|
1706
|
+
it(`屬性之欄名恰為他屬性之名稱而型別類別不同: 兩函數皆reject且資料不變: ${t}`, async function() {
|
|
1707
|
+
await seed()
|
|
1708
|
+
let rs = await errOf(() => md.findAll({ where: cvFind(find, ctx()), raw: true }))
|
|
1709
|
+
let rd = await errOf(() => md.destroy({ where: cvFind(find, ctx()) }))
|
|
1710
|
+
assert.ok(rs !== null && rs.indexOf('ambiguous column name [ra]') >= 0, `select: ${rs}`)
|
|
1711
|
+
assert.ok(rd !== null && rd.indexOf('ambiguous column name [ra]') >= 0, `delAll: ${rd}`)
|
|
1712
|
+
assert.deepStrictEqual(await ids(), ALL)
|
|
1713
|
+
})
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
it('撞名之他屬性(rb, 欄rc)不受影響: 屬性名鍵、欄名鍵之命中一致', async function() {
|
|
1717
|
+
await seed()
|
|
1718
|
+
let a = await hit({ rb: { $gt: 5 } })
|
|
1719
|
+
assert.deepStrictEqual(a, ['r2', 'r3'])
|
|
1720
|
+
assert.deepStrictEqual(await hit({ rc: { $gt: 5 } }), a)
|
|
1721
|
+
await md.destroy({ where: cvFind({ rc: { $gt: 5 } }, ctx()) })
|
|
1722
|
+
assert.deepStrictEqual(await ids(), ['r1', 'r4'])
|
|
1723
|
+
})
|
|
1724
|
+
|
|
1725
|
+
})
|
|
1726
|
+
|
|
1727
|
+
|
|
1728
|
+
describe(`${label} 欄名邊界`, function() {
|
|
1729
|
+
|
|
1730
|
+
//run05 sol F01/F02、opus [1][2][4]、自查(規格句 R4欄名解析(b)(c), 系列最低要求①②③): 欄名恰為sequelize於轉成SQL前重新解讀之形態, 於資料庫實跑——
|
|
1731
|
+
//(1)映射後之欄名為空字串: sync以field||name建欄(欄名為屬性名)而mapWhereFieldNames把鍵映射為'', _joinKeyValue遇空鍵只回值, 條件成常數(WHERE (5)), delAll刪全表;
|
|
1732
|
+
//(2)首尾皆$且中間有換行: isColString只比首尾字元, 比對的是去掉$之另一欄; $regex加i時LOWER(col)卻引用完整欄名, 同一find因旗標而命中不同列;
|
|
1733
|
+
//(3)共用一欄: 欄名恰為先定義者之名稱時, sequelize依先定義者之型別序列化, 實體欄之型別卻由最後定義者決定
|
|
1734
|
+
let sq = null
|
|
1735
|
+
let mdE = null
|
|
1736
|
+
let mdV = null
|
|
1737
|
+
let mdS = null
|
|
1738
|
+
let ctx = (md) => ({ escape: (v) => sq.escape(v), attrs: md.rawAttributes, dialect: label, pk: 'id' })
|
|
1739
|
+
let ids = async (md) => (await md.findAll({ raw: true, attributes: ['id'] })).map((v) => v.id).sort()
|
|
1740
|
+
let errOf = async (fn) => {
|
|
1741
|
+
try {
|
|
1742
|
+
await fn()
|
|
1743
|
+
return null
|
|
1744
|
+
}
|
|
1745
|
+
catch (e) {
|
|
1746
|
+
return String(e.message)
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
before(async function() {
|
|
1751
|
+
let o = getOpt()
|
|
1752
|
+
if (label === 'sqlite') {
|
|
1753
|
+
sq = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
|
|
1754
|
+
}
|
|
1755
|
+
else {
|
|
1756
|
+
let u = new URL(o.url)
|
|
1757
|
+
sq = new Sequelize(o.db, decodeURIComponent(u.username), decodeURIComponent(u.password), { dialect: 'mssql', host: u.hostname, port: Number(u.port), logging: false })
|
|
1758
|
+
}
|
|
1759
|
+
//(1)
|
|
1760
|
+
mdE = sq.define('tempfd', {
|
|
1761
|
+
id: { type: Sequelize.STRING, primaryKey: true },
|
|
1762
|
+
a: { type: Sequelize.INTEGER, field: '' },
|
|
1763
|
+
}, { tableName: 'tempfd', timestamps: false })
|
|
1764
|
+
//(2)
|
|
1765
|
+
mdV = sq.define('tdollar', {
|
|
1766
|
+
id: { type: Sequelize.STRING, primaryKey: true },
|
|
1767
|
+
t: { type: Sequelize.STRING, field: '\nname' },
|
|
1768
|
+
w: { type: Sequelize.STRING, field: '$\nname$' },
|
|
1769
|
+
}, { tableName: 'tdollar', timestamps: false })
|
|
1770
|
+
//(3)
|
|
1771
|
+
mdS = sq.define('tshare', {
|
|
1772
|
+
id: { type: Sequelize.STRING, primaryKey: true },
|
|
1773
|
+
sa: { type: Sequelize.DATE, field: 'sa' },
|
|
1774
|
+
sb: { type: Sequelize.INTEGER, field: 'sa' },
|
|
1775
|
+
}, { tableName: 'tshare', timestamps: false })
|
|
1776
|
+
await mdE.sync({ force: true })
|
|
1777
|
+
await mdV.sync({ force: true })
|
|
1778
|
+
await mdS.sync({ force: true })
|
|
1779
|
+
})
|
|
1780
|
+
|
|
1781
|
+
after(async function() {
|
|
1782
|
+
for (let md of [mdE, mdV, mdS]) {
|
|
1783
|
+
if (md) {
|
|
1784
|
+
await md.drop()
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
if (sq) {
|
|
1788
|
+
await sq.close()
|
|
1789
|
+
}
|
|
1790
|
+
})
|
|
1791
|
+
|
|
1792
|
+
//rej, select與delAll(以cvFind直接轉譯後交sequelize之findAll/destroy, 同WOrmReladb之路徑)皆須於轉譯階段reject且資料不變
|
|
1793
|
+
let rej = async (md, seed, find, msg) => {
|
|
1794
|
+
await md.destroy({ where: {} })
|
|
1795
|
+
await md.bulkCreate(seed)
|
|
1796
|
+
let before = await ids(md)
|
|
1797
|
+
let rs = await errOf(() => md.findAll({ where: cvFind(find, ctx(md)), raw: true }))
|
|
1798
|
+
let rd = await errOf(() => md.destroy({ where: cvFind(find, ctx(md)) }))
|
|
1799
|
+
assert.ok(rs !== null && rs.indexOf(msg) >= 0, `select: ${rs}`)
|
|
1800
|
+
assert.ok(rd !== null && rd.indexOf(msg) >= 0, `delAll: ${rd}`)
|
|
1801
|
+
assert.deepStrictEqual(await ids(md), before, '資料須不變')
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
for (let [t, find] of [
|
|
1805
|
+
['等值', { a: 5 }],
|
|
1806
|
+
['$gt', { a: { $gt: 100 } }],
|
|
1807
|
+
['$in', { a: { $in: [5] } }],
|
|
1808
|
+
['空$in', { a: { $in: [] } }],
|
|
1809
|
+
]) {
|
|
1810
|
+
it(`映射後之欄名為空字串: 兩函數皆reject且資料不變: ${t}`, async function() {
|
|
1811
|
+
//mssql之sync會把rawAttributes.a.field由''正規化為'a'(2026-10-16實測), 該model於mssql無此形態; sqlite不正規化(實測 WHERE (5) 刪全表), 故僅sqlite實跑, 兩方言之轉譯階段拒收由單元測試涵蓋
|
|
1812
|
+
if (label !== 'sqlite') {
|
|
1813
|
+
this.skip()
|
|
1814
|
+
}
|
|
1815
|
+
await rej(mdE, [{ id: 'r1', a: 0 }, { id: 'r2', a: 5 }, { id: 'r3' }], find, 'unsupported column name []')
|
|
1816
|
+
})
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
for (let [t, find] of [
|
|
1820
|
+
['等值', { w: 'needle' }],
|
|
1821
|
+
['$in', { w: { $in: ['needle'] } }],
|
|
1822
|
+
]) {
|
|
1823
|
+
it(`首尾皆$且中間有換行之欄名: 兩函數皆reject且資料不變: ${t}`, async function() {
|
|
1824
|
+
await rej(mdV, [{ id: 'r1', t: 'needle', w: 'x' }, { id: 'r2', t: 'x', w: 'needle' }, { id: 'r3' }], find, 'unsupported column name [$\nname$]')
|
|
1825
|
+
})
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
for (let [t, find] of [
|
|
1829
|
+
['先定義者 × 日期字串', { sa: { $lt: '2030-01-01T00:00:00Z' } }],
|
|
1830
|
+
['先定義者 × epoch', { sa: { $gt: 1893456000000 } }],
|
|
1831
|
+
['先定義者 × Date', { sa: new Date(0) }],
|
|
1832
|
+
['先定義者 × 空$in', { sa: { $in: [] } }],
|
|
1833
|
+
]) {
|
|
1834
|
+
it(`共用一欄且欄名恰為先定義者之名稱: 兩函數皆reject且資料不變: ${t}`, async function() {
|
|
1835
|
+
await rej(mdS, [{ id: 'r1', sb: 1 }, { id: 'r2', sb: 2 }, { id: 'r3' }], find, 'ambiguous column name [sa]')
|
|
1836
|
+
})
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
//後定義者sb之欄名sa恰為先定義者之名稱: sequelize以rawAttributes.sa(DATE)序列化sb之值, 同樣不可信, 亦reject(單元測試涵蓋共用非屬性名之欄時最後定義者照常)
|
|
1840
|
+
it('共用一欄且欄名恰為先定義者之名稱: 後定義者亦reject(sequelize依先定義者之型別序列化其值), 資料不變', async function() {
|
|
1841
|
+
await rej(mdS, [{ id: 'r1', sb: 1 }, { id: 'r2', sb: 2 }, { id: 'r3' }], { sb: { $lt: 2 } }, 'ambiguous column name [sb]')
|
|
1842
|
+
})
|
|
1843
|
+
|
|
1844
|
+
})
|
|
1845
|
+
|
|
1362
1846
|
|
|
1363
1847
|
}
|