w-orm-reladb 1.1.10 → 1.1.12

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/src/cvFind.mjs CHANGED
@@ -19,6 +19,10 @@ import cvOrmFind from 'wsemi/src/cvOrmFind.mjs'
19
19
  let Op = Sequelize.Op
20
20
 
21
21
 
22
+ //SQL運算式(Sequelize.fn、literal、col等)之基底類別, 寫入DATE欄之值為此類者由呼叫端明示, 不檢(R12)
23
+ let SM = Sequelize.Utils.SequelizeMethod
24
+
25
+
22
26
  //形狀檢核之上限(傳給cvOrmFind): $and/$or長度999——SQLite平鋪$or/$and恰1000項即Expression tree is too large(DB錯誤), 本套件使用SQLite故下調(預設1000);
23
27
  //其餘上限用預設: 邏輯巢狀深度32、$in/$nin長度10000、節點總數100000、欄名128
24
28
  let optShape = { maxList: 999 }
@@ -311,15 +315,132 @@ function resolveAttr(attrs, idx, field, path) {
311
315
  }
312
316
 
313
317
 
318
+ //DATE可接受之年份範圍, 條件側(isKindOk、parseNoTzLocal)與寫入側(genDateChecker)共用之唯一定義(R4、R12)
319
+ let yearMin = 1
320
+ let yearMax = 9999
321
+
322
+
314
323
  /**
315
- * 判定時間(毫秒)換算UTC後年份是否在1–9999
316
- * 註: sequelize以YYYY格式化DATE, 年10000起為5位數, sqlite以文字比較致{d:{$gt:年10000}}命中全部日期列; mssql之datetimeoffset範圍亦為0001–9999
324
+ * 判定年份是否在1–9999
325
+ * 註: sequelize以YYYY格式化DATE, 年10000起為5位數、年0與負年份亦非4位數, sqlite以文字比較致時間序與字典序不一致(哨兵8.64e15被$lt某日期誤中、$gt漏中);
326
+ * mssql之datetimeoffset與date範圍亦為0001–9999
317
327
  *
318
328
  * @ignore
319
329
  */
330
+ function inYearRange(y) {
331
+ return y >= yearMin && y <= yearMax
332
+ }
333
+
334
+
335
+ /**
336
+ * 判定時間(毫秒)換算UTC後年份是否在1–9999, 供條件側與寫入側共用(R4、R12); 無效時間(NaN、超出Date範圍)為false
337
+ *
338
+ * @param {Number} ms 輸入時間毫秒數
339
+ * @returns {Boolean} 回傳是否在範圍內
340
+ */
320
341
  function inDateRange(ms) {
321
- let y = new Date(ms).getUTCFullYear()
322
- return y >= 1 && y <= 9999
342
+ return inYearRange(new Date(ms).getUTCFullYear())
343
+ }
344
+
345
+
346
+ /**
347
+ * 取DATEONLY欄把值轉為實際存入之YYYY-MM-DD文字之函數(sequelize之DATEONLY._sanitize)
348
+ * 註: 真正之rawAttributes其type為型別實例; 手造attrs可能給型別建構子(如Sequelize.DATEONLY)或只有key之物件, 此時用sequelize內建DATEONLY之_sanitize(兩者同一函數);
349
+ * 取不到(sequelize版本漂移)時於換算時拋錯, 由chkWriteDate視為無效而拒收(fail-closed, 版本漂移另有單元測試之guard案)
350
+ *
351
+ * @ignore
352
+ */
353
+ function getDateOnlySanitizer(type) {
354
+ let t = type && typeof type._sanitize === 'function' ? type : DT.DATEONLY.prototype
355
+ return (v) => t._sanitize(v, {})
356
+ }
357
+
358
+
359
+ /**
360
+ * 檢核寫入DATE/DATEONLY欄之單一值(R12), 回傳{ok, v}: ok為是否為「年份在1–9999之有效時間」, v為建議寫回之標準值(undefined表示維持原值)
361
+ * 註: date——以sequelize之DATE._sanitize同一轉換取得時間(Date原樣, 其餘truthy值new Date(值)), 以inDateRange判定UTC年份;
362
+ * falsy值sequelize不轉換, 僅0(epoch 0)有效, 其餘('', false, NaN)會被存為文字Invalid date而拒收
363
+ * 註: dateonly——sequelize以moment(值)依主機本地時間格式化為YYYY-MM-DD文字存入, 以該型別之_sanitize所得之文字(= 實際存入者)判定:
364
+ * 須為4位數年且在1–9999(範圍外年份之文字形狀為5位以上、帶負號或年0000, 如new Date(8.64e15)得275760-09-13、new Date(-8.64e15)得-271821-04-20), Invalid date文字亦拒收
365
+ * 註: 無效時間(Invalid Date、NaN、無法解析之字串、超出Date範圍之數值、BigInt與Symbol…)一律拒收: sqlite存為文字Invalid date, 其字典序大於任何年份而使$gt命中、delAll誤刪,
366
+ * mssql則由資料庫報錯, 兩方言不一致(外審Argon F5; 與條件側無效Date一律reject同)
367
+ * 註: 檢查之值即使用之值——物件值(Date及其子類別、包裝物件、陣列、含自訂valueOf/toString者)可被sequelize再度求值而得不同結果, 故回傳已檢核之標準值(Date或YYYY-MM-DD文字)供呼叫端換上;
368
+ * 原始值(字串、數值)不可變, 不換
369
+ * 註: Sequelize.fn、literal、col等SQL運算式(SequelizeMethod)由呼叫端明示, sequelize不轉換, 不檢
370
+ *
371
+ * @ignore
372
+ */
373
+ function chkWriteDate(v, kind, sanitize) {
374
+ if (v instanceof SM) {
375
+ return { ok: true, v: undefined }
376
+ }
377
+ if (!v) {
378
+ return { ok: v === 0, v: undefined }
379
+ }
380
+ if (typeof v === 'bigint' || typeof v === 'symbol') {
381
+ return { ok: false, v: undefined } //BigInt與Symbol不是時間(new Date(1n)拋錯, moment(1n)卻視為年2001), 一律拒收
382
+ }
383
+ try {
384
+ if (kind === 'date') {
385
+ let ms = (v instanceof Date ? v : new Date(v)).getTime()
386
+ if (!inDateRange(ms)) {
387
+ return { ok: false, v: undefined }
388
+ }
389
+ return { ok: true, v: typeof v === 'object' ? new Date(ms) : undefined }
390
+ }
391
+ let s = sanitize(v)
392
+ let m = typeof s === 'string' ? s.match(reDateOnly) : null
393
+ if (m === null || !inYearRange(Number(m[1]))) {
394
+ return { ok: false, v: undefined }
395
+ }
396
+ return { ok: true, v: typeof v === 'object' ? s : undefined }
397
+ }
398
+ catch (err) {
399
+ return { ok: false, v: undefined } //換算時拋錯(Symbol、BigInt、無原型物件等)亦非有效時間
400
+ }
401
+ }
402
+
403
+
404
+ /**
405
+ * 產生寫入數據之DATE/DATEONLY欄範圍檢核函數(R12): 回傳之函數對單筆數據(物件)檢核各DATE、DATEONLY屬性之值,
406
+ * 回傳第一個無效或範圍外(年份不在1–9999)之屬性名(依model屬性定義順序), 皆合法回傳null
407
+ * 註: 與條件側共用inDateRange; 只檢核model中型別為DATE(date)與DATEONLY(dateonly)之屬性, 值為null或undefined者不檢;
408
+ * 非物件之記錄回傳null(由呼叫端前段檢核負責)
409
+ * 註: 通過檢核之物件值(Date及其子類別、包裝物件、陣列等)就地換成已檢核之標準值(Date或YYYY-MM-DD文字), 令檢查之值即使用之值(見chkWriteDate);
410
+ * 僅換記錄自有屬性, 原型鏈上之屬性只讀不換(create與bulkCreate不讀原型鏈屬性, update會讀, 故原型鏈上具狀態之getter不在保證範圍)
411
+ *
412
+ * @param {Object} attrs 輸入model之rawAttributes, 非物件時回傳恆為null之檢核函數
413
+ * @returns {Function} 回傳檢核函數(rec)=>屬性名字串或null
414
+ */
415
+ function genDateChecker(attrs) {
416
+ let cols = []
417
+ if (attrs !== null && typeof attrs === 'object') {
418
+ for (let k of Object.keys(attrs)) {
419
+ let kind = getColKind(attrs, k)
420
+ if (kind === 'date' || kind === 'dateonly') {
421
+ cols.push({ k, kind, san: kind === 'dateonly' ? getDateOnlySanitizer(attrs[k].type) : null })
422
+ }
423
+ }
424
+ }
425
+ return (rec) => {
426
+ if (rec === null || typeof rec !== 'object') {
427
+ return null
428
+ }
429
+ for (let c of cols) {
430
+ let v = rec[c.k]
431
+ if (v === null || v === undefined) {
432
+ continue
433
+ }
434
+ let r = chkWriteDate(v, c.kind, c.san)
435
+ if (!r.ok) {
436
+ return c.k
437
+ }
438
+ if (r.v !== undefined && Object.prototype.hasOwnProperty.call(rec, c.k)) {
439
+ rec[c.k] = r.v
440
+ }
441
+ }
442
+ return null
443
+ }
323
444
  }
324
445
 
325
446
 
@@ -752,5 +873,5 @@ function cvFind(find, ctx) {
752
873
  }
753
874
 
754
875
 
755
- export { getColKind, getColScale, normPk, chkFindShape }
876
+ export { getColKind, getColScale, normPk, chkFindShape, inDateRange, genDateChecker }
756
877
  export default cvFind
@@ -0,0 +1,467 @@
1
+ import assert from 'assert'
2
+ import cloneDeep from 'lodash-es/cloneDeep.js'
3
+ import Sequelize from 'sequelize'
4
+ import wo from '../src/WOrmReladb.mjs'
5
+
6
+
7
+ //本檔為mssql與sqlite共用之寫入側DATE範圍檢核測試套組(CLAUDE_rulebook.md之R12), 由api-sqlite.test.mjs與api-mssql.test.mjs引入
8
+ //規格句(R12, dspec〈一之三〉8.): 以文字儲存日期之後端, 寫入拒收範圍外Date——DATE/DATEONLY欄之值須為年份在1–9999之有效時間,
9
+ //範圍外與無效時間(Invalid Date、NaN、垃圾字串…, sqlite會存為字典序大於任何年份之文字Invalid date)皆使insert、insertBulk、save整批reject且資料庫不變;
10
+ //訊息不回顯值; error事件於reject前恰一次、change不發; 兩方言行為一致(mssql於送出前即拒); 檢查之值即使用之值(具狀態之valueOf不得於二次求值時存入範圍外年份)
11
+ //檢核優先序: 已關閉 → 初始化 → procPk(缺主鍵) → normPks(主鍵型別不符) → 日期檢核
12
+ //model為models/tdate.js: d、d2為DATE, o為DATEONLY, n為STRING(非日期欄不檢); models/tdatei.js: 整數主鍵 + DATE欄d(測檢核優先序)
13
+
14
+
15
+ let msgOf = (err) => String(err && err.message !== undefined ? err.message : err)
16
+
17
+ let settle = (pm) => pm.then((v) => ({ ok: true, v })).catch((err) => ({ err }))
18
+
19
+ let byId = (a, b) => (a.id < b.id ? -1 : 1)
20
+
21
+ //期望之訊息: 不回顯值, 只含索引與欄名
22
+ let want = (i, col) => `invalid data: date is not a valid time in years 1-9999 at [data[${i}].${col}]`
23
+
24
+ //normD, 讀回之DATE值統一為ISO字串: sqlite讀回為文字'YYYY-MM-DD HH:mm:ss.SSS +00:00', mssql為Date
25
+ let normD = (x) => {
26
+ if (x === null || x === undefined) {
27
+ return null
28
+ }
29
+ if (x instanceof Date) {
30
+ return x.toISOString()
31
+ }
32
+ let m = String(x).match(/^(\d{4,6})-(\d{2})-(\d{2}) (\d{2}:\d{2}:\d{2}\.\d{3}) \+00:00$/)
33
+ return m ? `${m[1]}-${m[2]}-${m[3]}T${m[4]}Z` : String(x)
34
+ }
35
+
36
+ //normO, 讀回之DATEONLY值統一為'YYYY-MM-DD': sqlite為文字, mssql可能為Date(UTC午夜)
37
+ let normO = (x) => {
38
+ if (x === null || x === undefined) {
39
+ return null
40
+ }
41
+ if (x instanceof Date) {
42
+ return x.toISOString().slice(0, 10)
43
+ }
44
+ return String(x).slice(0, 10)
45
+ }
46
+
47
+
48
+ /**
49
+ * 定義共用測試套組
50
+ *
51
+ * @param {String} label 輸入後端名稱字串('sqlite'或'mssql'), 供describe標題使用
52
+ * @param {Function} getOpt 輸入取得建構設定之函數, 於hook內呼叫以取得當下有效之連線資訊
53
+ */
54
+ export function defineDateWriteSuites(label, getOpt) {
55
+
56
+
57
+ describe(`${label} 寫入側DATE範圍: 入口 × 拒收值 × 欄位`, function() {
58
+
59
+ let w = null
60
+ let ins = null
61
+ let cs = null
62
+
63
+ //種子: 3列合法數據, 供「資料庫不變」與save更新既有之用
64
+ let seed = () => [
65
+ { id: 's1', n: 'a', d: new Date('2000-01-01T00:00:00Z'), d2: null, o: '2000-01-01' },
66
+ { id: 's2', n: 'b', d: new Date('2010-06-15T12:30:00Z'), d2: new Date('2011-01-01T00:00:00Z'), o: null },
67
+ { id: 's3', n: 'c', d: null, d2: null, o: '2020-12-31' },
68
+ ]
69
+
70
+ let dump = async () => {
71
+ let rs = await w.select({}, cs)
72
+ return rs.sort(byId)
73
+ }
74
+
75
+ let reset = async () => {
76
+ await w.delAll({}, cs)
77
+ let r = await w.insert(seed(), cs)
78
+ assert.strictEqual(r.nInserted, 3)
79
+ }
80
+
81
+ before(async function() {
82
+ w = wo(getOpt({ cl: 'tdate' }))
83
+ await w.createStorage()
84
+ ins = await w.init()
85
+ cs = { instance: ins }
86
+ })
87
+
88
+ after(async function() {
89
+ await ins.close()
90
+ })
91
+
92
+ //入口: [名稱, 事件mode, 呼叫函數, 是否針對既有主鍵]
93
+ let entries = [
94
+ ['insert', 'insert', (rs) => w.insert(rs, cs), false],
95
+ ['insertBulk', 'insertBulk', (rs) => w.insertBulk(rs, cs), false],
96
+ ['save(自動插入)', 'save', (rs) => w.save(rs, { ...cs, autoInsert: true }), false],
97
+ ['save(更新既有)', 'save', (rs) => w.save(rs, { ...cs, autoInsert: true }), true],
98
+ ['save(autoInsert:false且主鍵不存在)', 'save', (rs) => w.save(rs, { ...cs, autoInsert: false }), false],
99
+ ]
100
+
101
+ //mk, 產生3筆記錄, patches為[[筆索引, 欄名, 值]…]; 其餘欄與其餘筆皆合法
102
+ let mk = (existing, patches) => {
103
+ let ids = existing ? ['s1', 's2', 's3'] : ['x1', 'x2', 'x3']
104
+ let rs = ids.map((id) => ({ id, n: 'new', d: new Date('2030-01-01T00:00:00Z'), o: '2030-01-01' }))
105
+ for (let [p, col, v] of patches) {
106
+ rs[p][col] = v
107
+ }
108
+ return rs
109
+ }
110
+
111
+ //runReject, 呼叫入口並斷言: reject、訊息(不回顯值)、error事件於reject前恰一次且mode/err一致、change不發、資料庫逐列不變、呼叫端數據不變
112
+ let runReject = async (entry, rs, msg) => {
113
+ let [, mode, fn] = entry
114
+ await reset()
115
+ let before = await dump()
116
+ let snap = cloneDeep(rs)
117
+ let evs = []
118
+ let errs = []
119
+ let fnE = (m, data, e) => {
120
+ evs.push(`error:${m}`)
121
+ errs.push(e)
122
+ }
123
+ let fnC = (m) => evs.push(`change:${m}`)
124
+ w.on('error', fnE)
125
+ w.on('change', fnC)
126
+ let r
127
+ try {
128
+ r = await settle(fn(rs))
129
+ evs.push('reject')
130
+ }
131
+ finally {
132
+ w.off('error', fnE)
133
+ w.off('change', fnC)
134
+ }
135
+ assert.ok(r.err !== undefined, `未reject: ${JSON.stringify(r.v)}`)
136
+ assert.strictEqual(msgOf(r.err), msg)
137
+ assert.deepStrictEqual(evs, [`error:${mode}`, 'reject'], 'error事件於reject前恰一次, change不發')
138
+ assert.deepStrictEqual(errs, [msg], 'error事件之err與reject訊息一致')
139
+ assert.deepStrictEqual(await dump(), before, '資料庫逐列不變')
140
+ assert.deepStrictEqual(rs, snap, '呼叫端數據不變')
141
+ }
142
+
143
+ // ---- DATE欄之拒收值 × 5入口: 中間筆(第2筆)違規, 其餘合法筆亦不得寫入 ----
144
+ let badDate = [
145
+ ['Date 年0末刻', () => new Date('0000-12-31T23:59:59.999Z')],
146
+ ['Date 年10000首刻', () => new Date('+010000-01-01T00:00:00.000Z')],
147
+ ['Date 8.64e15', () => new Date(8.64e15)],
148
+ ['Date -8.64e15', () => new Date(-8.64e15)],
149
+ ['epoch 8.64e15', () => 8.64e15],
150
+ ['epoch -8.64e15', () => -8.64e15],
151
+ ['ISO字串 年0末刻', () => '0000-12-31T23:59:59.999Z'],
152
+ ['ISO字串 年10000首刻', () => '+010000-01-01T00:00:00Z'],
153
+ ['Number包裝物件 8.64e15', () => Object(8.64e15)],
154
+ ]
155
+ for (let entry of entries) {
156
+ for (let [lab, gen] of badDate) {
157
+ it(`${entry[0]} × DATE欄 ${lab}: 整批reject、資料不變`, async function() {
158
+ await runReject(entry, mk(entry[3], [[1, 'd', gen()]]), want(1, 'd'))
159
+ })
160
+ }
161
+ }
162
+
163
+ // ---- DATEONLY欄之拒收值 × 5入口 ----
164
+ let badOnly = [
165
+ ['Date 8.64e15', () => new Date(8.64e15)],
166
+ ['Date -8.64e15', () => new Date(-8.64e15)],
167
+ ['Date 年10000年中', () => new Date('+010000-06-15T12:00:00Z')],
168
+ ['Date 年0年中', () => new Date('0000-06-15T12:00:00Z')],
169
+ ['epoch 8.64e15', () => 8.64e15],
170
+ ['ISO字串 年10000', () => '+010000-01-01'],
171
+ ['ISO字串 年0', () => '0000-12-31'],
172
+ ]
173
+ for (let entry of entries) {
174
+ for (let [lab, gen] of badOnly) {
175
+ it(`${entry[0]} × DATEONLY欄 ${lab}: 整批reject、資料不變`, async function() {
176
+ await runReject(entry, mk(entry[3], [[1, 'o', gen()]]), want(1, 'o'))
177
+ })
178
+ }
179
+ }
180
+
181
+ // ---- 無效時間(sqlite會存為文字Invalid date, mssql由資料庫報錯)亦整批reject × 5入口 ----
182
+ let badInvalid = [
183
+ ['Invalid Date', () => new Date(NaN)],
184
+ ['NaN', () => NaN],
185
+ ['空字串', () => ''],
186
+ ['垃圾字串', () => 'garbage'],
187
+ ['超出Date範圍之epoch 1e16', () => 1e16],
188
+ ['false', () => false],
189
+ ]
190
+ for (let entry of entries) {
191
+ for (let [lab, gen] of badInvalid) {
192
+ it(`${entry[0]} × DATE欄 無效時間 ${lab}: 整批reject、資料不變`, async function() {
193
+ await runReject(entry, mk(entry[3], [[1, 'd', gen()]]), want(1, 'd'))
194
+ })
195
+ }
196
+ it(`${entry[0]} × DATEONLY欄 無效時間 垃圾字串: 整批reject、資料不變`, async function() {
197
+ await runReject(entry, mk(entry[3], [[1, 'o', 'garbage']]), want(1, 'o'))
198
+ })
199
+ }
200
+
201
+ // ---- 位置與多欄 × 5入口 ----
202
+ let bad = () => new Date(8.64e15)
203
+ let positions = [
204
+ ['首筆', (e) => mk(e, [[0, 'd', bad()]]), want(0, 'd')],
205
+ ['末筆', (e) => mk(e, [[2, 'd', bad()]]), want(2, 'd')],
206
+ ['第二個DATE欄(d2)', (e) => mk(e, [[1, 'd2', bad()]]), want(1, 'd2')],
207
+ ['同筆多欄違規: 回報model屬性定義順序之第一個(d先於d2先於o)', (e) => mk(e, [[1, 'o', bad()], [1, 'd2', bad()], [1, 'd', bad()]]), want(1, 'd')],
208
+ ['同筆d2與o違規: 回報d2', (e) => mk(e, [[1, 'o', bad()], [1, 'd2', bad()]]), want(1, 'd2')],
209
+ ['多筆違規: 回報索引最小者', (e) => mk(e, [[2, 'd', bad()], [1, 'd2', bad()]]), want(1, 'd2')],
210
+ ]
211
+ for (let entry of entries) {
212
+ for (let [lab, gen, msg] of positions) {
213
+ it(`${entry[0]} × 位置 ${lab}`, async function() {
214
+ await runReject(entry, gen(entry[3]), msg)
215
+ })
216
+ }
217
+ }
218
+
219
+ // ---- 單一物件(非陣列)輸入 × 5入口 ----
220
+ for (let entry of entries) {
221
+ it(`${entry[0]} × 單一物件(非陣列): 路徑索引為0`, async function() {
222
+ let one = mk(entry[3], [[0, 'd', bad()]])[0]
223
+ await runReject(entry, one, want(0, 'd'))
224
+ })
225
+ }
226
+
227
+ // ---- 接受值 × 5入口: 邊界內合法、讀回等於寫入之值 ----
228
+ let okCases = [
229
+ ['Date 年1首刻', { d: new Date('0001-01-01T00:00:00.000Z') }, { d: '0001-01-01T00:00:00.000Z' }],
230
+ ['Date 年9999末刻', { d: new Date('9999-12-31T23:59:59.999Z') }, { d: '9999-12-31T23:59:59.999Z' }],
231
+ ['epoch 年1首刻與年9999末刻(d與d2)', { d: Date.parse('0001-01-01T00:00:00.000Z'), d2: Date.parse('9999-12-31T23:59:59.999Z') }, { d: '0001-01-01T00:00:00.000Z', d2: '9999-12-31T23:59:59.999Z' }],
232
+ ['ISO字串 年1首刻與年9999末刻', { d: '0001-01-01T00:00:00.000Z', d2: '9999-12-31T23:59:59.999Z' }, { d: '0001-01-01T00:00:00.000Z', d2: '9999-12-31T23:59:59.999Z' }],
233
+ ['DATEONLY 年1首日(字串)', { o: '0001-01-01' }, { o: '0001-01-01' }],
234
+ ['DATEONLY 年9999末日(字串)', { o: '9999-12-31' }, { o: '9999-12-31' }],
235
+ ['DATEONLY Date年中', { o: new Date('2026-06-15T12:00:00Z') }, { o: '2026-06-15' }],
236
+ ['null與非日期欄形似範圍外日期之字串', { d: null, d2: null, o: null, n: '275760-09-13 +275760-09-13T00:00:00.000Z' }, { d: null, d2: null, o: null, n: '275760-09-13 +275760-09-13T00:00:00.000Z' }],
237
+ ]
238
+ for (let entry of entries) {
239
+ if (entry[0] === 'save(autoInsert:false且主鍵不存在)') {
240
+ continue //此入口不會寫入, 接受側以另案斷言resolve
241
+ }
242
+ for (let [lab, patch, exp] of okCases) {
243
+ it(`${entry[0]} × 接受 ${lab}`, async function() {
244
+ await reset()
245
+ let ids = entry[3] ? ['s1', 's2', 's3'] : ['x1', 'x2', 'x3']
246
+ let rs = ids.map((id, k) => ({ id, ...(k === 1 ? patch : { n: 'other' }) }))
247
+ let r = await settle(entry[2](rs))
248
+ assert.ok(r.err === undefined, `不應reject: ${msgOf(r.err)}`)
249
+ let row = (await dump()).find((v) => v.id === ids[1])
250
+ for (let [k, v] of Object.entries(exp)) {
251
+ let got = k === 'o' ? normO(row[k]) : (k === 'n' ? row[k] : normD(row[k]))
252
+ assert.strictEqual(got, v, `${k} 讀回`)
253
+ }
254
+ })
255
+ }
256
+ }
257
+
258
+ it('save(autoInsert:false且主鍵不存在)接受合法邊界值: resolve且不寫入', async function() {
259
+ await reset()
260
+ let before = await dump()
261
+ let r = await settle(w.save([{ id: 'x9', d: new Date('9999-12-31T23:59:59.999Z') }], { ...cs, autoInsert: false }))
262
+ assert.ok(r.err === undefined, msgOf(r.err))
263
+ assert.deepStrictEqual(r.v, [{ n: 0, nInserted: 0, nModified: 0, ok: 1 }])
264
+ assert.deepStrictEqual(await dump(), before)
265
+ })
266
+
267
+ it('無日期值或日期欄為undefined之記錄不受影響', async function() {
268
+ await reset()
269
+ let r = await w.insert([{ id: 'u1', n: 'x' }, { id: 'u2', d: undefined, o: undefined }], cs)
270
+ assert.deepStrictEqual(r, { n: 2, nInserted: 2, ok: 1 })
271
+ })
272
+
273
+ })
274
+
275
+
276
+ describe(`${label} 寫入側DATE範圍: 情境與接縫`, function() {
277
+
278
+ let w = null
279
+ let ins = null
280
+ let cs = null
281
+
282
+ before(async function() {
283
+ w = wo(getOpt({ cl: 'tdate' }))
284
+ await w.createStorage()
285
+ ins = await w.init()
286
+ cs = { instance: ins }
287
+ })
288
+
289
+ after(async function() {
290
+ await ins.close()
291
+ })
292
+
293
+ beforeEach(async function() {
294
+ await w.delAll({}, cs)
295
+ })
296
+
297
+ it('修前紅之原始場景: 哨兵new Date(8.64e15)寫入即reject, 其後delAll({d:{$lt:日期}})只刪過期列, 永不過期之替代寫法(9999年末與null)不被刪', async function() {
298
+ //規格句(R12): 「永不過期」哨兵寫入範圍外Date會於sqlite以文字比較被$lt誤中而遭delAll刪除; 現於寫入即拒收
299
+ let r = await settle(w.insert({ id: 'sentinel', d: new Date(8.64e15) }, cs))
300
+ assert.ok(r.err !== undefined, '哨兵須reject')
301
+ await w.insert([
302
+ { id: 'old', d: new Date('2000-01-01T00:00:00Z') },
303
+ { id: 'never1', d: new Date('9999-12-31T23:59:59.999Z') },
304
+ { id: 'never2', d: null },
305
+ ], cs)
306
+ assert.deepStrictEqual((await w.select({}, cs)).map((v) => v.id).sort(), ['never1', 'never2', 'old'])
307
+ let rd = await w.delAll({ d: { $lt: new Date('3000-01-01T00:00:00Z') } }, cs)
308
+ assert.deepStrictEqual(rd, { n: 1, nDeleted: 1, ok: 1 })
309
+ assert.deepStrictEqual((await w.select({}, cs)).map((v) => v.id).sort(), ['never1', 'never2'])
310
+ let rs = await w.select({ d: { $gt: new Date('3000-01-01T00:00:00Z') } }, cs)
311
+ assert.deepStrictEqual(rs.map((v) => v.id), ['never1'])
312
+ })
313
+
314
+ it('不給instance之呼叫(內部自行初始化): 範圍外值reject、error事件恰一次, 連線已關閉且其後合法呼叫照常', async function() {
315
+ let w2 = wo(getOpt({ cl: 'tdate' }))
316
+ let evs = []
317
+ w2.on('error', (m) => evs.push(m))
318
+ let r = await settle(w2.insertBulk([{ id: 'a', d: new Date(8.64e15) }]))
319
+ assert.strictEqual(msgOf(r.err), want(0, 'd'))
320
+ assert.deepStrictEqual(evs, ['insertBulk'])
321
+ let r2 = await w2.insertBulk([{ id: 'a', d: new Date('9999-12-31T23:59:59.999Z') }])
322
+ assert.deepStrictEqual(r2, { n: 1, nInserted: 1, ok: 1 })
323
+ assert.deepStrictEqual(evs, ['insertBulk'], '合法呼叫不發error')
324
+ })
325
+
326
+ it('呼叫端交易內: 範圍外值reject不影響交易, 同一交易其後合法寫入與commit照常', async function() {
327
+ let transaction = await w.genTransaction()
328
+ let ct = { instance: ins, transaction }
329
+ let done = false
330
+ try {
331
+ let r = await settle(w.insertBulk([{ id: 't1', d: new Date('2001-01-01T00:00:00Z') }, { id: 't2', d: new Date(-8.64e15) }], ct))
332
+ assert.strictEqual(msgOf(r.err), want(1, 'd'))
333
+ let r2 = await w.insertBulk([{ id: 't3', d: new Date('2003-01-01T00:00:00Z') }], ct)
334
+ assert.deepStrictEqual(r2, { n: 1, nInserted: 1, ok: 1 })
335
+ await transaction.commit()
336
+ done = true
337
+ }
338
+ finally {
339
+ if (!done) {
340
+ await transaction.rollback().catch(() => {}) //斷言失敗時不留開啟中之交易(sqlite會鎖庫而連累後續案)
341
+ }
342
+ }
343
+ assert.deepStrictEqual((await w.select({}, cs)).map((v) => v.id), ['t3'])
344
+ })
345
+
346
+ it('save更新既有列: 範圍外值reject後該列維持舊值, 合法值照常更新', async function() {
347
+ await w.insert({ id: 'k', d: new Date('2000-01-01T00:00:00Z') }, cs)
348
+ let r = await settle(w.save({ id: 'k', d: new Date(8.64e15) }, cs))
349
+ assert.strictEqual(msgOf(r.err), want(0, 'd'))
350
+ assert.strictEqual(normD((await w.selectByPk('k', cs)).d), '2000-01-01T00:00:00.000Z')
351
+ let r2 = await w.save({ id: 'k', d: new Date('9999-12-31T23:59:59.999Z') }, cs)
352
+ assert.deepStrictEqual(r2, [{ n: 1, nInserted: 0, nModified: 1, ok: 1 }])
353
+ assert.strictEqual(normD((await w.selectByPk('k', cs)).d), '9999-12-31T23:59:59.999Z')
354
+ })
355
+
356
+ it('insert回傳逐筆結果模式(returnList)亦整批reject', async function() {
357
+ let r = await settle(w.insert([{ id: 'a', d: new Date('2001-01-01T00:00:00Z') }, { id: 'b', d: new Date(8.64e15) }], { ...cs, returnList: true }))
358
+ assert.strictEqual(msgOf(r.err), want(1, 'd'))
359
+ assert.deepStrictEqual(await w.select({}, cs), [])
360
+ })
361
+
362
+ it('訊息不回顯值, 且不隨值而變: 各種範圍外值之訊息逐字相同', async function() {
363
+ let vals = [new Date(8.64e15), new Date(-8.64e15), 8.64e15, '+010000-01-01T00:00:00Z', '0000-12-31T23:59:59.999Z']
364
+ for (let v of vals) {
365
+ let r = await settle(w.insert({ id: 'a', d: v }, cs))
366
+ assert.strictEqual(msgOf(r.err), want(0, 'd'))
367
+ }
368
+ })
369
+
370
+ it('缺主鍵(autoGenPk為false)之錯誤先於日期檢核(既有優先序不變): insert、insertBulk、save', async function() {
371
+ let w2 = wo(getOpt({ cl: 'tdate', autoGenPk: false }))
372
+ for (let [name, fn] of [['insert', (rs) => w2.insert(rs)], ['insertBulk', (rs) => w2.insertBulk(rs)], ['save', (rs) => w2.save(rs)]]) {
373
+ let r = await settle(fn([{ d: new Date(8.64e15) }]))
374
+ assert.ok(r.err !== undefined, name)
375
+ assert.ok(msgOf(r.err).indexOf('autoGenPk is false') >= 0, `${name}: ${msgOf(r.err)}`)
376
+ }
377
+ })
378
+
379
+ it('主鍵型別不符之錯誤先於日期檢核(既有優先序不變): 整數主鍵給\'abc\'且日期範圍外, insert、insertBulk、save', async function() {
380
+ let wi = wo(getOpt({ cl: 'tdatei' }))
381
+ await wi.createStorage()
382
+ for (let [name, fn] of [['insert', (rs) => wi.insert(rs)], ['insertBulk', (rs) => wi.insertBulk(rs)], ['save', (rs) => wi.save(rs)]]) {
383
+ let r = await settle(fn([{ id: 'abc', d: new Date(8.64e15) }]))
384
+ assert.ok(r.err !== undefined, name)
385
+ assert.ok(msgOf(r.err).indexOf('type does not match pk column') >= 0, `${name}: ${msgOf(r.err)}`)
386
+ }
387
+ })
388
+
389
+ it('整數主鍵model之日期檢核照常: 主鍵合法而日期範圍外則整批reject', async function() {
390
+ let wi = wo(getOpt({ cl: 'tdatei' }))
391
+ await wi.createStorage()
392
+ await wi.delAll()
393
+ let r = await settle(wi.insert([{ id: 1, d: new Date('2001-01-01T00:00:00Z') }, { id: 2, d: new Date(-8.64e15) }]))
394
+ assert.strictEqual(msgOf(r.err), want(1, 'd'))
395
+ assert.deepStrictEqual(await wi.select({}), [])
396
+ })
397
+
398
+ // 規格句(R12): 檢查之值即使用之值——具狀態之valueOf第一次回傳合法值、其後回傳範圍外值者, 存入的是檢核時之值(物件值已換成標準值)
399
+ let stateful = (a, b) => {
400
+ let n = 0
401
+ return {
402
+ valueOf() {
403
+ return n++ === 0 ? a : b
404
+ },
405
+ }
406
+ }
407
+
408
+ it('檢查之值即使用之值(DATE, 具狀態valueOf): insert、insertBulk、save(更新既有)皆存入檢核時之值而非範圍外值', async function() {
409
+ await w.insert({ id: 'upd', d: new Date('2000-01-01T00:00:00Z') }, cs)
410
+ let entriesS = [
411
+ ['insert', 'si', (r) => w.insert(r, cs)],
412
+ ['insertBulk', 'sb', (r) => w.insertBulk(r, cs)],
413
+ ['save(更新既有)', 'upd', (r) => w.save(r, cs)],
414
+ ]
415
+ for (let [name, id, fn] of entriesS) {
416
+ let r = await settle(fn([{ id, d: stateful(0, 8.64e15) }]))
417
+ assert.ok(r.err === undefined, `${name}: ${msgOf(r.err)}`)
418
+ let row = await w.selectByPk(id, cs)
419
+ assert.strictEqual(normD(row.d), '1970-01-01T00:00:00.000Z', name)
420
+ }
421
+ })
422
+
423
+ it('檢查之值即使用之值(DATEONLY, 陣列元素具狀態toString): 存入檢核時之日期而非範圍外值', async function() {
424
+ let n = 0
425
+ let el = {
426
+ toString() {
427
+ return n++ === 0 ? '2026' : '10000'
428
+ },
429
+ }
430
+ let r = await settle(w.insert({ id: 'do1', o: [el, 0, 1] }, cs))
431
+ assert.ok(r.err === undefined, msgOf(r.err))
432
+ assert.strictEqual(normO((await w.selectByPk('do1', cs)).o), '2026-01-01')
433
+ })
434
+
435
+ it('Sequelize.literal等SQL運算式不被檢核或換值(照常交資料庫求值)', async function() {
436
+ if (label !== 'sqlite') {
437
+ return //運算式文法依方言
438
+ }
439
+ let r = await settle(w.insert({ id: 'lit', d: Sequelize.literal('\'2020-02-02 00:00:00.000 +00:00\'') }, cs))
440
+ assert.ok(r.err === undefined, msgOf(r.err))
441
+ assert.strictEqual(normD((await w.selectByPk('lit', cs)).d), '2020-02-02T00:00:00.000Z')
442
+ })
443
+
444
+ if (label === 'sqlite') {
445
+ //README之清理範例: 庫內既有之範圍外日期文字(本版前寫入)可由save改寫為範圍內值或null; 以原生SQL模擬舊資料(修前寫入入口已被擋)
446
+ it('既有資料不遷移: 庫內舊的範圍外日期文字不受影響, 可由save改寫為null與9999年末', async function() {
447
+ let sq = new Sequelize({ dialect: 'sqlite', storage: getOpt().storage, logging: false })
448
+ try {
449
+ await sq.query('INSERT INTO tdate (id, d) VALUES (?, ?)', { replacements: ['legacy', '275760-09-13 00:00:00.000 +00:00'] })
450
+ }
451
+ finally {
452
+ await sq.close()
453
+ }
454
+ let row = await w.selectByPk('legacy', cs)
455
+ assert.strictEqual(normD(row.d), '275760-09-13T00:00:00.000Z', '舊資料維持原樣(不遷移)')
456
+ await w.save({ id: 'legacy', d: null }, cs)
457
+ assert.strictEqual((await w.selectByPk('legacy', cs)).d, null)
458
+ await w.save({ id: 'legacy', d: new Date('9999-12-31T23:59:59.999Z') }, cs)
459
+ assert.strictEqual(normD((await w.selectByPk('legacy', cs)).d), '9999-12-31T23:59:59.999Z')
460
+ let r = await settle(w.save({ id: 'legacy', d: new Date(8.64e15) }, cs))
461
+ assert.strictEqual(msgOf(r.err), want(0, 'd'), '以Date再寫範圍外仍被拒')
462
+ })
463
+ }
464
+
465
+ })
466
+
467
+ }
@@ -3,6 +3,7 @@ import { execFile } from 'child_process'
3
3
  import { promisify } from 'util'
4
4
  import { defineSuites } from './api-suite.mjs'
5
5
  import { defineWhitelistSuites } from './api-find-whitelist.mjs'
6
+ import { defineDateWriteSuites } from './api-date-write.mjs'
6
7
 
7
8
 
8
9
  //測試自行創建docker容器供MSSQL服務, 測試結束即銷毀, 故僅需環境已安裝docker
@@ -115,3 +116,4 @@ after(async function() {
115
116
 
116
117
  defineSuites('mssql', getOpt)
117
118
  defineWhitelistSuites('mssql', getOpt)
119
+ defineDateWriteSuites('mssql', getOpt)
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs'
2
2
  import { defineSuites } from './api-suite.mjs'
3
3
  import { defineWhitelistSuites } from './api-find-whitelist.mjs'
4
+ import { defineDateWriteSuites } from './api-date-write.mjs'
4
5
 
5
6
 
6
7
  //sqlite使用獨立暫存storage檔測試, 不使用專案根目錄之worm.sqlite
@@ -42,3 +43,4 @@ after(function() {
42
43
 
43
44
  defineSuites('sqlite', getOpt)
44
45
  defineWhitelistSuites('sqlite', getOpt)
46
+ defineDateWriteSuites('sqlite', getOpt)