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
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import vm from 'vm'
|
|
3
|
+
import Sequelize from 'sequelize'
|
|
4
|
+
import cvFind from '../src/cvFind.mjs'
|
|
5
|
+
import * as cv from '../src/cvFind.mjs'
|
|
6
|
+
import { withTz } from './tools/withTz.mjs'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
//寫入側DATE範圍檢核之純函數測試(CLAUDE_rulebook.md之R12), 不連線資料庫
|
|
10
|
+
//規格句(R12): 凡寫入DATE/DATEONLY欄之值, 須為年份在1–9999之有效時間(DATE依UTC年, DATEONLY依實際存入之日期文字), 範圍外與無效時間(Invalid Date、NaN、垃圾字串…)皆拒收;
|
|
11
|
+
//與條件側(R4)共用同一範圍判定inDateRange; 非DATE欄、非model欄位之鍵不檢; Sequelize.fn/literal等SQL運算式不檢;
|
|
12
|
+
//檢查之值即使用之值: 通過檢核之物件值就地換成已檢核之標準值(Date或YYYY-MM-DD文字), 不再受自訂valueOf/toString/getTime之二次求值影響
|
|
13
|
+
//實際寫入入口(insert、insertBulk、save)之整批reject與資料庫不變由test/api-date-write.mjs於sqlite與mssql斷言
|
|
14
|
+
//本檔以import * as cv取用被測匯出, 令被測匯出不存在時為逐案斷言失敗(而非整檔載入失敗)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
let sq = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
|
|
18
|
+
let md = sq.define('tdw', {
|
|
19
|
+
id: { type: Sequelize.STRING, primaryKey: true },
|
|
20
|
+
n: Sequelize.STRING,
|
|
21
|
+
i: Sequelize.INTEGER,
|
|
22
|
+
d: Sequelize.DATE,
|
|
23
|
+
d2: Sequelize.DATE(3),
|
|
24
|
+
o: Sequelize.DATEONLY,
|
|
25
|
+
}, { tableName: 'tdw', timestamps: false })
|
|
26
|
+
let attrs = md.rawAttributes
|
|
27
|
+
|
|
28
|
+
let chk = (rec) => cv.genDateChecker(attrs)(rec)
|
|
29
|
+
|
|
30
|
+
//邊界(毫秒): 年1與年9999之首尾
|
|
31
|
+
let MIN_OK = Date.parse('0001-01-01T00:00:00.000Z') //-62135596800000
|
|
32
|
+
let MAX_OK = Date.parse('9999-12-31T23:59:59.999Z') //253402300799999
|
|
33
|
+
let MIN_BAD = MIN_OK - 1
|
|
34
|
+
let MAX_BAD = MAX_OK + 1
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
describe('R12 寫入側DATE範圍: inDateRange(條件側與寫入側共用之單一範圍判定)', function() {
|
|
38
|
+
|
|
39
|
+
it('匯出存在', function() {
|
|
40
|
+
assert.strictEqual(typeof cv.inDateRange, 'function')
|
|
41
|
+
assert.strictEqual(typeof cv.genDateChecker, 'function')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// 規格句: 換算UTC後年份須在1–9999
|
|
45
|
+
let cases = [
|
|
46
|
+
['年1首刻', MIN_OK, true],
|
|
47
|
+
['年0末刻', MIN_BAD, false],
|
|
48
|
+
['年9999末刻', MAX_OK, true],
|
|
49
|
+
['年10000首刻', MAX_BAD, false],
|
|
50
|
+
['1970', 0, true],
|
|
51
|
+
['Date上界8.64e15', 8.64e15, false],
|
|
52
|
+
['Date下界-8.64e15', -8.64e15, false],
|
|
53
|
+
['NaN', NaN, false],
|
|
54
|
+
['Infinity', Infinity, false],
|
|
55
|
+
['-Infinity', -Infinity, false],
|
|
56
|
+
]
|
|
57
|
+
for (let [label, ms, want] of cases) {
|
|
58
|
+
it(`${label}: ${want}`, function() {
|
|
59
|
+
assert.strictEqual(cv.inDateRange(ms), want)
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
describe('R12 寫入側DATE範圍: genDateChecker(attrs)(rec) 回傳第一個範圍外之欄位名, 皆合法回傳null', function() {
|
|
67
|
+
|
|
68
|
+
// ---- DATE欄: 拒收值 ----
|
|
69
|
+
let badDate = [
|
|
70
|
+
['Date 年0末刻', () => new Date('0000-12-31T23:59:59.999Z')],
|
|
71
|
+
['Date 年10000首刻', () => new Date('+010000-01-01T00:00:00.000Z')],
|
|
72
|
+
['Date 8.64e15', () => new Date(8.64e15)],
|
|
73
|
+
['Date -8.64e15', () => new Date(-8.64e15)],
|
|
74
|
+
['epoch 年0末刻', () => MIN_BAD],
|
|
75
|
+
['epoch 年10000首刻', () => MAX_BAD],
|
|
76
|
+
['epoch 8.64e15', () => 8.64e15],
|
|
77
|
+
['epoch -8.64e15', () => -8.64e15],
|
|
78
|
+
['ISO字串 年0末刻', () => '0000-12-31T23:59:59.999Z'],
|
|
79
|
+
['ISO字串 年10000首刻(帶號6位數年)', () => '+010000-01-01T00:00:00Z'],
|
|
80
|
+
['ISO字串 8.64e15', () => '+275760-09-13T00:00:00.000Z'],
|
|
81
|
+
['ISO字串 -8.64e15', () => '-271821-04-20T00:00:00.000Z'],
|
|
82
|
+
['帶偏移ISO字串 換算UTC後年10000', () => '9999-12-31T23:30:00-05:00'],
|
|
83
|
+
['帶偏移ISO字串 換算UTC後年0', () => '0001-01-01T00:30:00+01:00'],
|
|
84
|
+
['Number包裝物件 8.64e15', () => Object(8.64e15)],
|
|
85
|
+
['String包裝物件 年10000', () => Object('+010000-01-01T00:00:00Z')],
|
|
86
|
+
['Date子類別 8.64e15', () => new (class MyDate extends Date {})(8.64e15)],
|
|
87
|
+
['跨realm之Date 8.64e15', () => vm.runInNewContext('new Date(8.64e15)')],
|
|
88
|
+
['跨realm之Date -8.64e15', () => vm.runInNewContext('new Date(-8.64e15)')],
|
|
89
|
+
]
|
|
90
|
+
for (let [label, gen] of badDate) {
|
|
91
|
+
it(`DATE欄拒收: ${label}`, function() {
|
|
92
|
+
assert.strictEqual(chk({ id: 'a', d: gen() }), 'd')
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---- DATE欄: 接受值(邊界與範圍內) ----
|
|
97
|
+
let okDate = [
|
|
98
|
+
['Date 年1首刻', () => new Date('0001-01-01T00:00:00.000Z')],
|
|
99
|
+
['Date 年9999末刻', () => new Date('9999-12-31T23:59:59.999Z')],
|
|
100
|
+
['Date 年1首刻+1ms', () => new Date(MIN_OK + 1)],
|
|
101
|
+
['Date 年9999末刻-1ms', () => new Date(MAX_OK - 1)],
|
|
102
|
+
['Date 1970', () => new Date(0)],
|
|
103
|
+
['epoch 年1首刻', () => MIN_OK],
|
|
104
|
+
['epoch 年9999末刻', () => MAX_OK],
|
|
105
|
+
['epoch 0(falsy數值)', () => 0],
|
|
106
|
+
['ISO字串 年1首刻', () => '0001-01-01T00:00:00.000Z'],
|
|
107
|
+
['ISO字串 年9999末刻', () => '9999-12-31T23:59:59.999Z'],
|
|
108
|
+
['帶偏移ISO字串 換算UTC後年9999內', () => '9999-12-31T23:30:00+00:30'],
|
|
109
|
+
['純日期字串(sequelize現行接受)', () => '2026-01-01'],
|
|
110
|
+
['空白分隔之ISO字串', () => '2026-01-01 12:00:00Z'],
|
|
111
|
+
['Number包裝物件 範圍內', () => Object(Date.parse('2026-01-01T00:00:00Z'))],
|
|
112
|
+
['陣列(new Date([y,m,d])得範圍內時間)', () => [2026, 1, 1]],
|
|
113
|
+
['null', () => null],
|
|
114
|
+
['undefined', () => undefined],
|
|
115
|
+
]
|
|
116
|
+
for (let [label, gen] of okDate) {
|
|
117
|
+
it(`DATE欄接受: ${label}`, function() {
|
|
118
|
+
assert.strictEqual(chk({ id: 'a', d: gen() }), null)
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---- 無效時間一律拒收(sequelize在sqlite存為文字Invalid date, 字典序大於任何年份而使$gt命中、delAll誤刪, mssql由資料庫報錯, 兩方言不一致) ----
|
|
123
|
+
let invalid = [
|
|
124
|
+
['Invalid Date', () => new Date(NaN)],
|
|
125
|
+
['NaN', () => NaN],
|
|
126
|
+
['Infinity', () => Infinity],
|
|
127
|
+
['-Infinity', () => -Infinity],
|
|
128
|
+
['超出Date範圍之epoch 1e16', () => 1e16],
|
|
129
|
+
['空字串(falsy, sequelize不轉換而存為Invalid date)', () => ''],
|
|
130
|
+
['垃圾字串', () => 'garbage'],
|
|
131
|
+
['無+號之5位數年字串(JS不認)', () => '10000-01-01T00:00:00Z'],
|
|
132
|
+
['false(falsy)', () => false],
|
|
133
|
+
['BigInt 0n(falsy)', () => 0n],
|
|
134
|
+
['BigInt 1n(DATEONLY之moment卻視為年2001)', () => 1n],
|
|
135
|
+
['Symbol', () => Symbol('x')],
|
|
136
|
+
]
|
|
137
|
+
for (let [label, gen] of invalid) {
|
|
138
|
+
it(`DATE欄拒收無效時間: ${label}`, function() {
|
|
139
|
+
assert.strictEqual(chk({ id: 'a', d: gen() }), 'd')
|
|
140
|
+
})
|
|
141
|
+
it(`DATEONLY欄拒收無效時間: ${label}`, function() {
|
|
142
|
+
assert.strictEqual(chk({ id: 'a', o: gen() }), 'o')
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let invalidDate = [
|
|
147
|
+
['一般物件{a:1}(new Date得Invalid Date)', () => ({ a: 1 })],
|
|
148
|
+
['無原型物件Object.create(null)(new Date拋TypeError)', () => Object.create(null)],
|
|
149
|
+
['陣列元素為垃圾字串', () => ['garbage']],
|
|
150
|
+
]
|
|
151
|
+
for (let [label, gen] of invalidDate) {
|
|
152
|
+
it(`DATE欄拒收無法換算為時間之值(換算拋錯亦拒收而非拋出): ${label}`, function() {
|
|
153
|
+
assert.strictEqual(chk({ id: 'a', d: gen() }), 'd')
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---- SQL運算式由呼叫端明示, sequelize不轉換, 不檢 ----
|
|
158
|
+
let sqlExprs = [
|
|
159
|
+
['Sequelize.fn(SQL函數值)', () => Sequelize.fn('datetime', 'now')],
|
|
160
|
+
['Sequelize.literal(SQL常值)', () => Sequelize.literal('\'2026-01-01 00:00:00.000 +00:00\'')],
|
|
161
|
+
['Sequelize.col', () => Sequelize.col('d2')],
|
|
162
|
+
]
|
|
163
|
+
for (let [label, gen] of sqlExprs) {
|
|
164
|
+
it(`DATE與DATEONLY欄不檢SQL運算式: ${label}`, function() {
|
|
165
|
+
let v = gen()
|
|
166
|
+
let rec = { id: 'a', d: v, o: v }
|
|
167
|
+
assert.strictEqual(chk(rec), null)
|
|
168
|
+
assert.strictEqual(rec.d, v, '不換值')
|
|
169
|
+
assert.strictEqual(rec.o, v, '不換值')
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
it('falsy數值0與-0為epoch 0(有效, sequelize不轉換而依epoch存入)', function() {
|
|
174
|
+
assert.strictEqual(chk({ d: 0 }), null)
|
|
175
|
+
assert.strictEqual(chk({ d: -0 }), null)
|
|
176
|
+
assert.strictEqual(chk({ o: 0 }), null)
|
|
177
|
+
assert.strictEqual(chk({ o: -0 }), null)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
// ---- DATEONLY欄 ----
|
|
181
|
+
// 規格句: sequelize以moment(值).format('YYYY-MM-DD')存入文字, 以實際存入者判定: 年須為4位數且在1–9999
|
|
182
|
+
// 註: Date物件依主機本地曆年格式化, 故拒收值取年中(不受時區影響), 邊界年另於withTz案驗證
|
|
183
|
+
let badOnly = [
|
|
184
|
+
['Date 8.64e15', () => new Date(8.64e15)],
|
|
185
|
+
['Date -8.64e15', () => new Date(-8.64e15)],
|
|
186
|
+
['Date 年10000年中', () => new Date('+010000-06-15T12:00:00Z')],
|
|
187
|
+
['Date 年0年中', () => new Date('0000-06-15T12:00:00Z')],
|
|
188
|
+
['epoch 8.64e15', () => 8.64e15],
|
|
189
|
+
['epoch -8.64e15', () => -8.64e15],
|
|
190
|
+
['ISO字串 年10000(帶號6位數年)', () => '+010000-01-01'],
|
|
191
|
+
['ISO字串 年0', () => '0000-12-31'],
|
|
192
|
+
['帶時間之ISO字串 年10000年中', () => '+010000-06-15T12:00:00Z'],
|
|
193
|
+
['Number包裝物件 8.64e15', () => Object(8.64e15)],
|
|
194
|
+
]
|
|
195
|
+
for (let [label, gen] of badOnly) {
|
|
196
|
+
it(`DATEONLY欄拒收: ${label}`, function() {
|
|
197
|
+
assert.strictEqual(chk({ id: 'a', o: gen() }), 'o')
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let okOnly = [
|
|
202
|
+
['字串 年1首日', () => '0001-01-01'],
|
|
203
|
+
['字串 年9999末日', () => '9999-12-31'],
|
|
204
|
+
['字串 一般', () => '2026-06-15'],
|
|
205
|
+
['Date 年中', () => new Date('2026-06-15T12:00:00Z')],
|
|
206
|
+
['epoch 年中', () => Date.parse('2026-06-15T12:00:00Z')],
|
|
207
|
+
['null', () => null],
|
|
208
|
+
['undefined', () => undefined],
|
|
209
|
+
]
|
|
210
|
+
for (let [label, gen] of okOnly) {
|
|
211
|
+
it(`DATEONLY欄接受: ${label}`, function() {
|
|
212
|
+
assert.strictEqual(chk({ id: 'a', o: gen() }), null)
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 曆年依主機本地時間(moment格式化): 同一瞬間在不同時區可能跨年
|
|
217
|
+
it('DATEONLY欄之邊界年依主機時區: UTC+8下UTC年9999末刻之Date其本地曆年為10000而拒收', function() {
|
|
218
|
+
withTz('Asia/Taipei', () => {
|
|
219
|
+
assert.strictEqual(chk({ o: new Date('9999-12-31T23:30:00Z') }), 'o')
|
|
220
|
+
assert.strictEqual(chk({ o: new Date('9999-12-31T15:59:59Z') }), null)
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
it('DATEONLY欄之邊界年依主機時區: UTC-5下UTC年1首刻之Date其本地曆年為0而拒收', function() {
|
|
224
|
+
withTz('America/New_York', () => {
|
|
225
|
+
assert.strictEqual(chk({ o: new Date('0001-01-01T00:30:00Z') }), 'o')
|
|
226
|
+
assert.strictEqual(chk({ o: new Date('0001-01-02T12:00:00Z') }), null)
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
it('DATE欄與主機時區無關(UTC年判定): 各時區下結果相同', function() {
|
|
230
|
+
for (let tz of ['Asia/Taipei', 'America/New_York', 'UTC', 'Pacific/Kiritimati']) {
|
|
231
|
+
withTz(tz, () => {
|
|
232
|
+
assert.strictEqual(chk({ d: new Date(MAX_OK) }), null, tz)
|
|
233
|
+
assert.strictEqual(chk({ d: new Date(MIN_OK) }), null, tz)
|
|
234
|
+
assert.strictEqual(chk({ d: new Date(MAX_BAD) }), 'd', tz)
|
|
235
|
+
assert.strictEqual(chk({ d: new Date(MIN_BAD) }), 'd', tz)
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
// ---- 多欄與欄位選取 ----
|
|
241
|
+
it('回報第一個範圍外之欄: 依model屬性定義順序而非數據鍵順序', function() {
|
|
242
|
+
let bad = new Date(8.64e15)
|
|
243
|
+
assert.strictEqual(chk({ o: bad, d2: bad, d: bad }), 'd')
|
|
244
|
+
assert.strictEqual(chk({ o: bad, d2: bad }), 'd2')
|
|
245
|
+
assert.strictEqual(chk({ d2: new Date(0), o: bad }), 'o')
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('同筆其他欄合法時只回報範圍外者', function() {
|
|
249
|
+
assert.strictEqual(chk({ id: 'a', n: 'x', i: 1, d: new Date(0), d2: new Date(8.64e15), o: '2026-01-01' }), 'd2')
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('非DATE/DATEONLY欄不檢(字串欄、整數欄收到形似範圍外日期之值)', function() {
|
|
253
|
+
assert.strictEqual(chk({ id: '275760-09-13', n: '+275760-09-13T00:00:00.000Z', i: 8.64e15 }), null)
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('非model欄位之鍵不檢', function() {
|
|
257
|
+
assert.strictEqual(chk({ 'zzz': new Date(8.64e15), 'd.x': new Date(8.64e15) }), null)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('數據欄位自原型鏈繼承者亦讀取檢核(保守: create與bulkCreate不讀原型鏈屬性而update會讀)', function() {
|
|
261
|
+
let rec = Object.create({ d: new Date(8.64e15) })
|
|
262
|
+
assert.strictEqual(chk(rec), 'd')
|
|
263
|
+
let ok = Object.create({ d: new Date(0) })
|
|
264
|
+
assert.strictEqual(chk(ok), null)
|
|
265
|
+
assert.ok(!Object.prototype.hasOwnProperty.call(ok, 'd'), '原型鏈屬性只讀不換')
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('無日期欄之model或未給attrs: 一律回傳null', function() {
|
|
269
|
+
assert.strictEqual(cv.genDateChecker({ id: { type: Sequelize.STRING } })({ d: new Date(8.64e15) }), null)
|
|
270
|
+
assert.strictEqual(cv.genDateChecker(undefined)({ d: new Date(8.64e15) }), null)
|
|
271
|
+
assert.strictEqual(cv.genDateChecker(null)({ d: new Date(8.64e15) }), null)
|
|
272
|
+
assert.strictEqual(cv.genDateChecker({})({ d: new Date(8.64e15) }), null)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('記錄非物件時不拋錯(回傳null, 由前段檢核負責)', function() {
|
|
276
|
+
for (let v of [null, undefined, 5, 'x', true]) {
|
|
277
|
+
assert.strictEqual(chk(v), null)
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it('檢核失敗時不改動數據(Date、包裝物件逐一不變)', function() {
|
|
282
|
+
let w = Object(8.64e15)
|
|
283
|
+
let dt = new Date(8.64e15)
|
|
284
|
+
let rec = { d: dt, d2: w }
|
|
285
|
+
assert.strictEqual(chk(rec), 'd')
|
|
286
|
+
assert.strictEqual(rec.d, dt)
|
|
287
|
+
assert.strictEqual(rec.d2, w)
|
|
288
|
+
assert.strictEqual(dt.getTime(), 8.64e15)
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it('原始值(字串、數值)不換: 不可變, 檢查之值即使用之值', function() {
|
|
292
|
+
let rec = { d: '2026-01-01T00:00:00Z', d2: 1700000000000, o: '2026-06-15' }
|
|
293
|
+
assert.strictEqual(chk(rec), null)
|
|
294
|
+
assert.deepStrictEqual(rec, { d: '2026-01-01T00:00:00Z', d2: 1700000000000, o: '2026-06-15' })
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it('guard: DATEONLY型別之_sanitize存在於已安裝之sequelize(寫入側以其判定實際存入之文字, 版本漂移時須讓此案失敗而非靜默不檢)', function() {
|
|
298
|
+
assert.strictEqual(typeof Sequelize.DataTypes.DATEONLY.prototype._sanitize, 'function')
|
|
299
|
+
assert.strictEqual(typeof new Sequelize.DataTypes.DATEONLY()._sanitize, 'function')
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
describe('R12 寫入側DATE範圍: 檢查之值即使用之值(物件值只求值一次並換成標準值)', function() {
|
|
306
|
+
|
|
307
|
+
// 規格句(R12): 通過檢核之物件值被sequelize再度求值(DATE._sanitize之new Date(值)、moment(值)、Model.update之讀取)時, 結果須與檢核時相同;
|
|
308
|
+
// 具狀態之valueOf/toString/getTime第一次回傳合法值、其後回傳範圍外值者, 不得因二次求值而存入範圍外年份
|
|
309
|
+
let stateful = (a, b) => {
|
|
310
|
+
let n = 0
|
|
311
|
+
return {
|
|
312
|
+
valueOf() {
|
|
313
|
+
return n++ === 0 ? a : b
|
|
314
|
+
},
|
|
315
|
+
nCall: () => n,
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
it('DATE欄: 具狀態valueOf之物件只求值一次, 換成Date(0)而非保留物件', function() {
|
|
320
|
+
let o = stateful(0, 8.64e15)
|
|
321
|
+
let rec = { d: o }
|
|
322
|
+
assert.strictEqual(chk(rec), null)
|
|
323
|
+
assert.ok(rec.d instanceof Date)
|
|
324
|
+
assert.strictEqual(rec.d.getTime(), 0)
|
|
325
|
+
assert.strictEqual(o.nCall(), 1, '只求值一次')
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
it('DATE欄: 具狀態valueOf之物件第一次即範圍外則拒收', function() {
|
|
329
|
+
assert.strictEqual(chk({ d: stateful(8.64e15, 0) }), 'd')
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it('DATE欄: 覆寫getTime之Date子類別換成純Date(取檢核時之時間值)', function() {
|
|
333
|
+
let n = 0
|
|
334
|
+
class StatefulDate extends Date {
|
|
335
|
+
getTime() {
|
|
336
|
+
return n++ === 0 ? 0 : 8.64e15
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
let rec = { d: new StatefulDate(0) }
|
|
340
|
+
assert.strictEqual(chk(rec), null)
|
|
341
|
+
assert.strictEqual(Object.getPrototypeOf(rec.d), Date.prototype)
|
|
342
|
+
assert.strictEqual(rec.d.getTime(), 0)
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it('DATE欄: Number包裝物件、陣列換成Date', function() {
|
|
346
|
+
let rec = { d: Object(1700000000000), d2: [2026, 1, 1] }
|
|
347
|
+
assert.strictEqual(chk(rec), null)
|
|
348
|
+
assert.ok(rec.d instanceof Date && rec.d.getTime() === 1700000000000)
|
|
349
|
+
assert.ok(rec.d2 instanceof Date && rec.d2.getTime() === new Date([2026, 1, 1]).getTime())
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
it('DATE欄: 純Date物件換成同時間值之新Date(檢核後不再與呼叫端之Date物件共用)', function() {
|
|
353
|
+
let dt = new Date('2026-01-01T00:00:00Z')
|
|
354
|
+
let rec = { d: dt }
|
|
355
|
+
assert.strictEqual(chk(rec), null)
|
|
356
|
+
assert.notStrictEqual(rec.d, dt)
|
|
357
|
+
assert.strictEqual(rec.d.getTime(), dt.getTime())
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
it('DATEONLY欄: 陣列元素具狀態toString者(moment([…])會二次求值)換成YYYY-MM-DD文字', function() {
|
|
361
|
+
let n = 0
|
|
362
|
+
let el = {
|
|
363
|
+
toString() {
|
|
364
|
+
return n++ === 0 ? '2026' : '10000'
|
|
365
|
+
},
|
|
366
|
+
}
|
|
367
|
+
let rec = { o: [el, 0, 1] }
|
|
368
|
+
assert.strictEqual(chk(rec), null)
|
|
369
|
+
assert.strictEqual(rec.o, '2026-01-01')
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
it('DATEONLY欄: Date換成YYYY-MM-DD文字(與sequelize之_sanitize所得相同)', function() {
|
|
373
|
+
let rec = { o: new Date('2026-06-15T12:00:00Z') }
|
|
374
|
+
assert.strictEqual(chk(rec), null)
|
|
375
|
+
assert.strictEqual(rec.o, '2026-06-15')
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
it('回報第一個失敗欄時, 其之前已通過者仍已換值(換值不影響回報)', function() {
|
|
379
|
+
let rec = { d: Object(1700000000000), d2: new Date(8.64e15) }
|
|
380
|
+
assert.strictEqual(chk(rec), 'd2')
|
|
381
|
+
assert.ok(rec.d instanceof Date)
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
describe('R12 寫入側DATE範圍: genDateChecker 對手造attrs與型別漂移 fail-closed', function() {
|
|
388
|
+
|
|
389
|
+
// 規格句(R12): DATEONLY以_sanitize所得之實際存入文字判定; 手造attrs之type可為型別建構子(未經sequelize.define實例化)——此時不得靜默放行(外審Argon F2)
|
|
390
|
+
let bad = () => new Date(8.64e15)
|
|
391
|
+
|
|
392
|
+
it('手造attrs之type為型別建構子(Sequelize.DATEONLY / Sequelize.DATE)亦檢核', function() {
|
|
393
|
+
let c = cv.genDateChecker({ d: { type: Sequelize.DATE }, o: { type: Sequelize.DATEONLY } })
|
|
394
|
+
assert.strictEqual(c({ d: bad() }), 'd')
|
|
395
|
+
assert.strictEqual(c({ o: bad() }), 'o')
|
|
396
|
+
assert.strictEqual(c({ o: '+010000-01-01' }), 'o')
|
|
397
|
+
assert.strictEqual(c({ d: new Date(0), o: '2026-01-01' }), null)
|
|
398
|
+
assert.strictEqual(c({ o: new Date('2026-06-15T12:00:00Z') }), null, '合法Date須經_sanitize換算為文字後放行')
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
it('手造attrs之type為型別實例亦檢核', function() {
|
|
402
|
+
let c = cv.genDateChecker({ o: { type: new Sequelize.DATEONLY() } })
|
|
403
|
+
assert.strictEqual(c({ o: bad() }), 'o')
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
it('type僅有key而無_sanitize(自訂型別物件): 以sequelize內建DATEONLY之_sanitize判定, 不放行', function() {
|
|
407
|
+
let c = cv.genDateChecker({ o: { type: { key: 'DATEONLY' } } })
|
|
408
|
+
assert.strictEqual(c({ o: bad() }), 'o')
|
|
409
|
+
assert.strictEqual(c({ o: '2026-01-01' }), null)
|
|
410
|
+
assert.strictEqual(c({ o: new Date('2026-06-15T12:00:00Z') }), null, '合法Date須經_sanitize換算為文字後放行')
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
it('type之_sanitize回傳非字串(漂移)時視為無法確認為有效日期而拒收, 不放行', function() {
|
|
414
|
+
let c = cv.genDateChecker({ o: { type: { key: 'DATEONLY', _sanitize: () => 42 } } })
|
|
415
|
+
assert.strictEqual(c({ o: '2026-01-01' }), 'o')
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
it('type之_sanitize拋錯時視為無效而拒收, 不往外拋', function() {
|
|
419
|
+
let boom = () => {
|
|
420
|
+
throw new Error('x')
|
|
421
|
+
}
|
|
422
|
+
let c = cv.genDateChecker({ o: { type: { key: 'DATEONLY', _sanitize: boom } } })
|
|
423
|
+
assert.strictEqual(c({ o: '2026-01-01' }), 'o')
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
describe('R12 寫入側DATE範圍: 與條件側(cvFind)同界', function() {
|
|
430
|
+
|
|
431
|
+
// 規格句(R12單一擁有者): 條件側與寫入側共用inDateRange, 同一時間點兩側判定一致
|
|
432
|
+
let edges = [MIN_BAD, MIN_OK, MIN_OK + 1, -1, 0, 1, 1e12, MAX_OK - 1, MAX_OK, MAX_BAD, 8.64e15, -8.64e15]
|
|
433
|
+
|
|
434
|
+
let condOk = (v) => {
|
|
435
|
+
try {
|
|
436
|
+
cvFind({ d: { $gt: v } }, { attrs })
|
|
437
|
+
return true
|
|
438
|
+
}
|
|
439
|
+
catch (err) {
|
|
440
|
+
return false
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
for (let ms of edges) {
|
|
445
|
+
it(`Date(${ms}): 條件側接受 = 寫入側接受`, function() {
|
|
446
|
+
assert.strictEqual(condOk(new Date(ms)), chk({ d: new Date(ms) }) === null)
|
|
447
|
+
})
|
|
448
|
+
it(`epoch ${ms}: 條件側接受 = 寫入側接受`, function() {
|
|
449
|
+
assert.strictEqual(condOk(ms), chk({ d: ms }) === null)
|
|
450
|
+
})
|
|
451
|
+
it(`ISO字串(Date(${ms})之toISOString): 條件側接受 = 寫入側接受`, function() {
|
|
452
|
+
let s = new Date(ms).toISOString()
|
|
453
|
+
assert.strictEqual(condOk(s), chk({ d: s }) === null, s)
|
|
454
|
+
})
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
})
|