w-orm-lowdb 1.0.32 → 1.0.34

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.
@@ -0,0 +1,711 @@
1
+ import assert from 'assert'
2
+ import vm from 'vm'
3
+ import fs from 'fs'
4
+ import w from 'wsemi'
5
+ import WOrm from '../src/WOrmLowdb.mjs'
6
+ import { mkObserver } from './lib/mkObserver.mjs'
7
+ import { leaves, chkSame } from './lib/leaves.mjs'
8
+
9
+
10
+ //數據側(寫入)之Date範圍檢核(帳本R-DATE-01; 使用者2026-10-19裁示採A: 寫入時reject範圍外Date, fail-closed, 與條件側R-FIND-02一致)
11
+ //數據以JSON存值, Date經JSON序列化為toISOString()字串; 年份0000-9999恆為24字元(字典序即時間序), 範圍外(年份<0或>9999)帶+/-前綴而字典序顛倒,
12
+ //故「永不過期」哨兵new Date(8.64e15)會被delAll({t:{$lt:現在}})當成過去而刪除, 改為寫入時整批reject
13
+ //寫入入口: insert、insertBulk、save(自動插入、更新既有、autoInsert為false而主鍵不存在); 紀錄內任何深度之Date皆檢核
14
+ describe('date-write', function() {
15
+ let url = './test/_tmp/date-write'
16
+ let dbName = 'worm'
17
+ let MS = {
18
+ min: -62167219200000, //0000-01-01T00:00:00.000Z
19
+ max: 253402300799999, //9999-12-31T23:59:59.999Z
20
+ belowMin: -62167219200001, //-000001-12-31T23:59:59.999Z
21
+ aboveMax: 253402300800000, //+010000-01-01T00:00:00.000Z
22
+ big: 8.64e15, //+275760-09-13T00:00:00.000Z
23
+ small: -8.64e15, //-271821-04-20T00:00:00.000Z
24
+ }
25
+ let D = (ms) => new Date(ms)
26
+ let ok = () => D(Date.UTC(2026, 5, 1)) //2026-06-01T00:00:00.000Z
27
+ let okIso = '2026-06-01T00:00:00.000Z'
28
+
29
+ //ids, 取id集合(升冪)
30
+ let ids = (rs) => rs.map((v) => v.id).sort()
31
+
32
+ //sortRows
33
+ let sortRows = (rows) => rows.sort((a, b) => (a.id < b.id ? -1 : 1))
34
+
35
+ //image, 寫入後應讀回之樣子(JSON序列化之影像)
36
+ let image = (v) => JSON.parse(JSON.stringify(v))
37
+
38
+ //expectMsg, 本檢核之訊息(格式同條件側: invalid <對象>: 原因 at [路徑], 不回顯值)
39
+ let expectMsg = (path) => `invalid data: Date year is out of range 0000-9999 at [${path}]`
40
+
41
+ //mk: 於測試資料夾下建立資料庫檔(url為檔案路徑)
42
+ let mk = (name, opt = {}) => {
43
+ let file = `${url}/${name}.json`
44
+ w.fsDeleteFile(file)
45
+ return WOrm({ url: file, db: dbName, cl: name, ...opt })
46
+ }
47
+
48
+ //mkCorrupt: 建立「讀檔即失敗」之資料庫(資料庫檔內容為壞JSON), 供驗證檢核先於任何讀取
49
+ let mkCorrupt = async(name) => {
50
+ let file = `${url}/${name}.json`
51
+ w.fsDeleteFile(file)
52
+ fs.writeFileSync(file, '{ not a json')
53
+ return WOrm({ url: file, db: dbName, cl: name })
54
+ }
55
+
56
+ before(function() {
57
+ w.fsDeleteFolder(url)
58
+ fs.mkdirSync(url, { recursive: true })
59
+ })
60
+
61
+ after(function() {
62
+ w.fsDeleteFolder(url)
63
+ })
64
+
65
+ //entries: 寫入入口(5格). seedP為true者, 批次內主鍵p0-p4於寫入前已存在(save走更新). writes為false者不會真的寫入(autoInsert為false且主鍵不存在), 但同樣先經檢核
66
+ //key為資料庫資料夾用之唯一名稱
67
+ let entries = [
68
+ { key: 'insert', name: 'insert', fn: 'insert', writes: true, seedP: false, call: (wo, d) => wo.insert(d) },
69
+ { key: 'insertBulk', name: 'insertBulk', fn: 'insertBulk', writes: true, seedP: false, call: (wo, d) => wo.insertBulk(d) },
70
+ { key: 'saveAuto', name: 'save (auto insert)', fn: 'save', writes: true, seedP: false, call: (wo, d) => wo.save(d) },
71
+ { key: 'saveUpdate', name: 'save (update existing)', fn: 'save', writes: true, seedP: true, call: (wo, d) => wo.save(d) },
72
+ { key: 'saveNoAuto', name: 'save (autoInsert false, id missing)', fn: 'save', writes: false, seedP: false, call: (wo, d) => wo.save(d, { autoInsert: false }) },
73
+ ]
74
+
75
+ //writeEntries: 會真的寫入之入口(4格); 凡依賴寫入結果(讀回值、儲存端錯誤)之案例只跑這些, 檢核本身之案例跑全部5格(檢核不依是否真會寫入而異)
76
+ let writeEntries = entries.filter((v) => v.writes)
77
+
78
+ //seedRows: 寫入前資料庫內之數據(皆為範圍內), seedP者另含p0-p4
79
+ let seedRows = (entry) => {
80
+ let rows = [{ id: 'base1', t: ok() }, { id: 'base2', n: 2 }]
81
+ if (entry.seedP) {
82
+ for (let i = 0; i < 5; i++) {
83
+ rows.push({ id: `p${i}`, t: ok(), old: true })
84
+ }
85
+ }
86
+ return rows
87
+ }
88
+
89
+ //expectedStored: 寫入成功後該筆應讀回之樣子(save更新為lodash merge之遞迴合併; 此處舊值僅t與old, 與新值之巢狀欄位無重疊, 故等同展開覆蓋; 巢狀合併另見專案案例)
90
+ let expectedStored = (entry, rec) => {
91
+ if (entry.seedP) {
92
+ return { id: rec.id, t: okIso, old: true, ...image(rec) }
93
+ }
94
+ return image(rec)
95
+ }
96
+
97
+ let wo = null
98
+ let ob = null
99
+
100
+ //reset, 清空後依entry重建寫入前之數據
101
+ let reset = async(entry) => {
102
+ await wo.delAll()
103
+ await wo.insertBulk(seedRows(entry))
104
+ }
105
+
106
+ //snap, 資料庫現況(升冪)
107
+ let snap = async() => sortRows(await wo.select())
108
+
109
+ before(function() {
110
+ wo = mk('main')
111
+ ob = mkObserver(wo)
112
+ })
113
+
114
+ //chkRej, 整批reject之共同斷言: 訊息逐字等於expect, error事件於reject前恰一次(訊息同reject、mode為該入口), 不發任何change(含save內逐筆之insert)
115
+ let chkRej = (out, entry, expect) => {
116
+ assert.strict.equal(out.ok, false, `${entry.name} should reject`)
117
+ assert.strict.ok(out.err instanceof Error, `${entry.name} should reject with Error`)
118
+ assert.strict.equal(out.err.message, expect, `${entry.name} message`)
119
+ let evErr = out.evs.filter((v) => v.t === 'error')
120
+ let evChg = out.evs.filter((v) => v.t === 'change')
121
+ assert.strict.equal(evErr.length, 1, `${entry.name} should emit error exactly once`)
122
+ assert.strict.equal(out.nAtSettle, 1, `${entry.name} should emit error before reject`)
123
+ assert.strict.equal(evErr[0].settledAtEmit, false, `${entry.name} should emit error before reject`)
124
+ assert.strict.equal(evErr[0].mode, entry.fn)
125
+ assert.strict.equal(evErr[0].err, out.err.message, `${entry.name} error event should carry the reject message`)
126
+ assert.strict.equal(evChg.length, 0, `${entry.name} should not emit change`)
127
+ }
128
+
129
+ //positions: 範圍外Date出現於紀錄內之位置(rec(d, x): d為待測值, x為同一紀錄內其餘Date), path為訊息內應給出之路徑
130
+ let positions = [
131
+ { name: 'top-level field', rec: (d) => [{ id: 'p0', t: d }], path: 'data[0].t' },
132
+ { name: 'nested object', rec: (d) => [{ id: 'p0', o: { a: { b: d } } }], path: 'data[0].o.a.b' },
133
+ { name: 'array first', rec: (d) => [{ id: 'p0', arr: [d, ok()] }], path: 'data[0].arr[0]' },
134
+ { name: 'array middle', rec: (d) => [{ id: 'p0', arr: [ok(), d, ok()] }], path: 'data[0].arr[1]' },
135
+ { name: 'array index 10', rec: (d) => [{ id: 'p0', arr: [...Array.from({ length: 10 }, () => ok()), d] }], path: 'data[0].arr[10]' },
136
+ { name: 'object in array', rec: (d) => [{ id: 'p0', list: [{ x: ok() }, { x: d }] }], path: 'data[0].list[1].x' },
137
+ { name: 'array in array', rec: (d) => [{ id: 'p0', m: [[ok()], [ok(), d]] }], path: 'data[0].m[1][1]' },
138
+ { name: 'deep mixed', rec: (d) => [{ id: 'p0', a: { b: [{ c: { d: [ok(), { e: d }] } }] } }], path: 'data[0].a.b[0].c.d[1].e' },
139
+ { name: 'second record', rec: (d) => [{ id: 'p0', t: ok() }, { id: 'p1', t: d }], path: 'data[1].t' },
140
+ { name: 'fifth record', rec: (d) => [0, 1, 2, 3].map((i) => ({ id: `p${i}`, t: ok() })).concat([{ id: 'p4', t: d }]), path: 'data[4].t' },
141
+ { name: 'beside valid Dates', rec: (d) => [{ id: 'p0', a: ok(), t: d, b: ok() }], path: 'data[0].t' },
142
+ { name: 'several in one record (first by key order)', rec: (d, x) => [{ id: 'p0', t1: d, t2: x, t3: x }], path: 'data[0].t1' },
143
+ { name: 'single object not array', rec: (d) => ({ id: 'p0', t: d }), path: 'data[0].t' },
144
+ { name: 'bare Date as array element', rec: (d) => [d], path: 'data[0]', rejectOnly: true },
145
+ ]
146
+ let recsOf = (data) => (Array.isArray(data) ? data : [data])
147
+
148
+ //bads: 範圍外之值(echo為不得出現在訊息內之年份片段)
149
+ let bads = [
150
+ { name: 'below 0000 (-000001-12-31T23:59:59.999Z)', ms: MS.belowMin, echo: ['000001'] },
151
+ { name: 'above 9999 (+010000-01-01T00:00:00.000Z)', ms: MS.aboveMax, echo: ['010000'] },
152
+ { name: 'new Date(8.64e15)', ms: MS.big, echo: ['275760'] },
153
+ { name: 'new Date(-8.64e15)', ms: MS.small, echo: ['271821'] },
154
+ ]
155
+
156
+ //reject: 入口 × 範圍外之值 × 位置; 資料庫逐筆不變、呼叫端物件逐節點不變
157
+ for (let entry of entries) {
158
+ for (let bad of bads) {
159
+ for (let pos of positions) {
160
+ it(`should reject ${entry.name} by ${bad.name} at ${pos.name}`, async function() {
161
+ await reset(entry)
162
+ let before = await snap()
163
+ let data = pos.rec(D(bad.ms), D(MS.big))
164
+ let nodes = leaves(data)
165
+ let out = await ob.run(() => entry.call(wo, data))
166
+ chkRej(out, entry, expectMsg(pos.path))
167
+ for (let e of bad.echo) {
168
+ assert.strict.ok(out.err.message.indexOf(e) < 0, `message should not echo the value (${e}): ${out.err.message}`)
169
+ }
170
+ assert.strict.deepStrictEqual(await snap(), before, 'database should be unchanged')
171
+ chkSame(nodes, leaves(data)) //呼叫端實際傳入之同一物件逐節點不變
172
+ })
173
+ }
174
+ }
175
+ }
176
+
177
+ //accept: 端點(0000-01-01T00:00:00.000Z、9999-12-31T23:59:59.999Z)與內側值, 入口 × 位置; 讀回值等於JSON影像
178
+ let accepts = [
179
+ '0000-01-01T00:00:00.000Z',
180
+ '9999-12-31T23:59:59.999Z',
181
+ '0000-01-01T00:00:00.001Z', //端點內側1毫秒
182
+ '0000-12-31T23:59:59.999Z', //年份交界
183
+ '1970-01-01T00:00:00.000Z',
184
+ '9999-01-01T00:00:00.000Z',
185
+ '9999-12-31T23:59:59.998Z', //端點內側1毫秒
186
+ ]
187
+ for (let entry of entries) {
188
+ for (let iso of accepts) {
189
+ it(`should accept ${entry.name} by ${iso} at every position`, async function() {
190
+ assert.strict.equal(new Date(iso).toISOString(), iso)
191
+ for (let pos of positions.filter((v) => !v.rejectOnly)) {
192
+ await reset(entry)
193
+ let before = await snap()
194
+ let data = pos.rec(new Date(iso), new Date(iso))
195
+ let out = await ob.run(() => entry.call(wo, data))
196
+ assert.strict.equal(out.ok, true, `${pos.name} should resolve but got: ${out.err}`)
197
+ assert.strict.equal(out.evs.filter((v) => v.t === 'error').length, 0, `${pos.name} should not emit error`)
198
+ let after = await snap()
199
+ if (!entry.writes) {
200
+ assert.strict.deepStrictEqual(after, before, `${pos.name}: nothing is written`)
201
+ continue
202
+ }
203
+ let recs = recsOf(data)
204
+ let expect = sortRows(before.filter((r) => !recs.some((x) => x.id === r.id)).concat(recs.map((r) => expectedStored(entry, r))))
205
+ assert.strict.deepStrictEqual(after, expect, `${pos.name}: stored value should be the JSON image`)
206
+ assert.strict.ok(out.evs.some((v) => v.t === 'change' && v.mode === entry.fn), `${pos.name}: change should be emitted`)
207
+ if (pos.name === 'top-level field') {
208
+ assert.strict.equal((await wo.selectByPk('p0')).t, iso, 'stored as the fixed 24-char ISO string')
209
+ assert.strict.equal(iso.length, 24)
210
+ }
211
+ }
212
+ })
213
+ }
214
+ }
215
+
216
+ //無效Date(Invalid Date): 既有處置不變, 序列化為null存入, 不reject(R-DATE-01規則2)
217
+ for (let entry of entries) {
218
+ it(`should keep storing invalid Date as null for ${entry.name}`, async function() {
219
+ await reset(entry)
220
+ let before = await snap()
221
+ let data = [{ id: 'p0', t: new Date(NaN), arr: [new Date(NaN)], o: { u: new Date('x') } }]
222
+ let out = await ob.run(() => entry.call(wo, data))
223
+ assert.strict.equal(out.ok, true, `should resolve but got: ${out.err}`)
224
+ assert.strict.equal(out.evs.filter((v) => v.t === 'error').length, 0)
225
+ let after = await snap()
226
+ if (!entry.writes) {
227
+ assert.strict.deepStrictEqual(after, before)
228
+ return
229
+ }
230
+ let rec = (await wo.selectByPk('p0'))
231
+ assert.strict.deepStrictEqual(rec.t, null)
232
+ assert.strict.deepStrictEqual(rec.arr, [null])
233
+ assert.strict.deepStrictEqual(rec.o, { u: null })
234
+ })
235
+ }
236
+
237
+ //Date形態: 以JSON序列化之結果判定(檢查之值即使用之值), 子類別、跨realm、被改Symbol.toStringTag/getTime者仍辨得出
238
+ class SubDate extends Date {}
239
+ let rejectForms = [
240
+ { name: 'Date subclass', mk: (ms) => new SubDate(ms) },
241
+ { name: 'cross-realm Date', mk: (ms) => vm.runInNewContext(`new Date(${ms})`) },
242
+ { name: 'Date with own Symbol.toStringTag', mk: (ms) => { let d = new Date(ms); d[Symbol.toStringTag] = 'NotDate'; return d } },
243
+ { name: 'Date with own getTime returning an in-range value', mk: (ms) => { let d = new Date(ms); d.getTime = () => 0; return d } },
244
+ ]
245
+ for (let entry of entries) {
246
+ for (let form of rejectForms) {
247
+ for (let bad of [MS.big, MS.small, MS.aboveMax, MS.belowMin]) {
248
+ it(`should reject ${entry.name} by ${form.name} (${bad})`, async function() {
249
+ await reset(entry)
250
+ let before = await snap()
251
+ let out = await ob.run(() => entry.call(wo, { id: 'p0', t: form.mk(bad) }))
252
+ chkRej(out, entry, expectMsg('data[0].t'))
253
+ assert.strict.deepStrictEqual(await snap(), before)
254
+ })
255
+ }
256
+ }
257
+
258
+ //自有toISOString/toJSON覆寫: 序列化結果為範圍外字串者reject; 結果為範圍內字串或非字串者即存入該值
259
+ it(`should reject ${entry.name} by Date whose own toISOString gives an out-of-range string`, async function() {
260
+ await reset(entry)
261
+ let d = ok()
262
+ d.toISOString = () => '+275760-09-13T00:00:00.000Z'
263
+ let out = await ob.run(() => entry.call(wo, { id: 'p0', t: d }))
264
+ chkRej(out, entry, expectMsg('data[0].t'))
265
+ })
266
+ it(`should reject ${entry.name} by Date whose own toJSON gives an out-of-range string`, async function() {
267
+ await reset(entry)
268
+ let d = ok()
269
+ d.toJSON = () => '-271821-04-20T00:00:00.000Z'
270
+ let out = await ob.run(() => entry.call(wo, { id: 'p0', t: d }))
271
+ chkRej(out, entry, expectMsg('data[0].t'))
272
+ })
273
+ it(`should inspect own properties of a Date whose toJSON is not callable for ${entry.name}`, async function() {
274
+ await reset(entry)
275
+ let d = ok()
276
+ d.toJSON = null //不可呼叫之toJSON: JSON序列化視為一般物件, 自有屬性照常輸出
277
+ d.extra = D(MS.big)
278
+ let out = await ob.run(() => entry.call(wo, [{ id: 'p0', t: d }]))
279
+ chkRej(out, entry, expectMsg('data[0].t.extra'))
280
+ })
281
+ it(`should store what own toISOString/toJSON give when it is an in-range string or not a string for ${entry.name}`, async function() {
282
+ await reset(entry)
283
+ let d1 = D(MS.big)
284
+ d1.toISOString = () => '2020-01-01T00:00:00.000Z' //序列化結果為範圍內字串(實際存入之值即檢查之值)
285
+ let d2 = D(MS.small)
286
+ d2.toJSON = () => '2021-01-01T00:00:00.000Z'
287
+ let d3 = D(MS.big)
288
+ d3.toJSON = () => 5 //非字串, 不是日期字串
289
+ let out = await ob.run(() => entry.call(wo, [{ id: 'p0', a: d1, b: d2, c: d3 }]))
290
+ assert.strict.equal(out.ok, true, `should resolve but got: ${out.err}`)
291
+ if (!entry.writes) {
292
+ return
293
+ }
294
+ let rec = await wo.selectByPk('p0')
295
+ assert.strict.equal(rec.a, '2020-01-01T00:00:00.000Z')
296
+ assert.strict.equal(rec.b, '2021-01-01T00:00:00.000Z')
297
+ assert.strict.equal(rec.c, 5)
298
+ })
299
+ }
300
+
301
+ //不檢核者(R-DATE-01規則6): 已是字串、Map/Set內之Date、Buffer(含其自有屬性)、非Date之toJSON物件(其輸出不檢核, 此為已載明之範圍邊界); 只驗證其存入行為不變
302
+ for (let entry of writeEntries) {
303
+ it(`should not inspect strings, Map, Set, Buffer and non-Date toJSON objects for ${entry.name}`, async function() {
304
+ await reset(entry)
305
+ let big = '+275760-09-13T00:00:00.000Z'
306
+ let buf = Buffer.from('ab')
307
+ buf.bad = D(MS.big) //Buffer序列化取其toJSON輸出({type,data}), 自有屬性不輸出
308
+ let rec = {
309
+ id: 'p0',
310
+ s1: big, //已序列化之字串不是Date物件
311
+ s2: '-271821-04-20T00:00:00.000Z',
312
+ m: new Map([['k', D(MS.big)]]), //Map序列化為{}
313
+ st: new Set([D(MS.big)]), //Set序列化為{}
314
+ b: buf,
315
+ ta: new Uint8Array([1, 2]), //無自有屬性之TypedArray照常序列化為索引鍵物件
316
+ j: { toJSON() { return 'custom' } },
317
+ j2: { toJSON() { return { d: D(MS.big) } } }, //非Date之toJSON輸出內含範圍外Date: 不檢核(範圍邊界)
318
+ }
319
+ let out = await ob.run(() => entry.call(wo, [rec]))
320
+ assert.strict.equal(out.ok, true, `should resolve but got: ${out.err}`)
321
+ let got = await wo.selectByPk('p0')
322
+ assert.strict.equal(got.s1, big)
323
+ assert.strict.equal(got.s2, '-271821-04-20T00:00:00.000Z')
324
+ assert.strict.deepStrictEqual(got.m, {})
325
+ assert.strict.deepStrictEqual(got.st, {})
326
+ assert.strict.deepStrictEqual(got.b, { type: 'Buffer', data: [97, 98] })
327
+ assert.strict.deepStrictEqual(got.ta, { 0: 1, 1: 2 })
328
+ assert.strict.equal(got.j, 'custom')
329
+ assert.strict.deepStrictEqual(got.j2, { d: big })
330
+ })
331
+ }
332
+
333
+ //ArrayBuffer視圖: Buffer取toJSON輸出而不含自有屬性(不走訪); 其餘視圖(TypedArray、DataView)無toJSON, 自有屬性照常序列化, 其內之範圍外Date須拒收(外審F-01)
334
+ for (let entry of entries) {
335
+ for (let [name, mkv] of [
336
+ ['Uint8Array', () => new Uint8Array([1, 2])],
337
+ ['Float64Array', () => new Float64Array(2)],
338
+ ['DataView', () => new DataView(new ArrayBuffer(2))],
339
+ ]) {
340
+ it(`should reject ${entry.name} by out-of-range Date set on a ${name}`, async function() {
341
+ await reset(entry)
342
+ let before = await snap()
343
+ let view = mkv()
344
+ view.bad = D(MS.big)
345
+ let out = await ob.run(() => entry.call(wo, [{ id: 'p0', v: view }]))
346
+ chkRej(out, entry, expectMsg('data[0].v.bad'))
347
+ assert.strict.deepStrictEqual(await snap(), before)
348
+ })
349
+ }
350
+ it(`should accept ${entry.name} by out-of-range Date set on a Buffer because JSON takes its toJSON output only`, async function() {
351
+ await reset(entry)
352
+ let buf = Buffer.from([1, 2])
353
+ buf.bad = D(MS.big)
354
+ let out = await ob.run(() => entry.call(wo, [{ id: 'p0', b: buf }]))
355
+ assert.strict.equal(out.ok, true, `should resolve but got: ${out.err}`)
356
+ if (entry.writes) {
357
+ assert.strict.deepStrictEqual((await wo.selectByPk('p0')).b, { type: 'Buffer', data: [1, 2] })
358
+ }
359
+ })
360
+ }
361
+
362
+ //循環參照與BigInt: 既有結果不變(真的要寫入者於寫入時JSON序列化拋錯; 不會寫入者〔autoInsert為false而主鍵不存在〕不序列化故照常resolve); 檢核只略過回邊, 不吞掉兄弟節點之檢查
363
+ for (let entry of writeEntries) {
364
+ it(`should keep rejecting circular structure by the storage error for ${entry.name}`, async function() {
365
+ await reset(entry)
366
+ let o = { id: 'p0', t: ok() }
367
+ o.self = o
368
+ let out = await ob.run(() => entry.call(wo, o))
369
+ assert.strict.equal(out.ok, false)
370
+ assert.strict.ok(/circular/i.test(out.err.message), out.err.message)
371
+ assert.strict.equal(out.evs.filter((v) => v.t === 'error').length, 1)
372
+ })
373
+ it(`should keep rejecting BigInt by the storage error for ${entry.name}`, async function() {
374
+ await reset(entry)
375
+ let out = await ob.run(() => entry.call(wo, { id: 'p0', n: 1n }))
376
+ assert.strict.equal(out.ok, false)
377
+ assert.strict.ok(/BigInt/.test(out.err.message), out.err.message)
378
+ })
379
+ }
380
+ for (let entry of entries) {
381
+ it(`should report the Date even when the record is circular (${entry.name})`, async function() {
382
+ await reset(entry)
383
+ let o = { id: 'p0' }
384
+ o.self = o //回邊在前, 其後之兄弟節點仍須檢查
385
+ o.t = D(MS.big)
386
+ o.arr = [o, D(MS.small)]
387
+ let out = await ob.run(() => entry.call(wo, o))
388
+ chkRej(out, entry, expectMsg('data[0].t'))
389
+ })
390
+ }
391
+ it('should keep resolving save(autoInsert:false) of a circular record that is not written', async function() {
392
+ await reset(entries[4])
393
+ let o = { id: 'nothere' }
394
+ o.self = o
395
+ let out = await ob.run(() => wo.save(o, { autoInsert: false }))
396
+ assert.strict.equal(out.ok, true, `should resolve but got: ${out.err}`)
397
+ assert.strict.deepStrictEqual(out.val, [{ n: 0, nInserted: 0, nModified: 0, ok: 1 }])
398
+ })
399
+
400
+ //共用參照(菱形圖,如 cur = { x: prev, y: prev })只走訪一次(外審F-06): 深度28之圖僅30個物件, 逐路徑走訪需2^28次; save(autoInsert:false)不會序列化, 耗時全在檢核
401
+ it('should walk a diamond-shaped shared-reference graph in linear time', async function() {
402
+ await reset(entries[4])
403
+ let cur = { v: 1 }
404
+ for (let i = 0; i < 28; i++) {
405
+ cur = { x: cur, y: cur }
406
+ }
407
+ let t0 = Date.now()
408
+ let out = await ob.run(() => wo.save({ id: 'nothere', dag: cur }, { autoInsert: false }))
409
+ assert.strict.equal(out.ok, true, `should resolve but got: ${out.err}`)
410
+ assert.strict.ok(Date.now() - t0 < 3000, `took ${Date.now() - t0} ms`)
411
+ })
412
+ for (let entry of entries) {
413
+ it(`should report the first path to an out-of-range Date inside a shared-reference graph for ${entry.name}`, async function() {
414
+ await reset(entry)
415
+ let leaf = { t: D(MS.big) }
416
+ let cur = leaf
417
+ for (let i = 0; i < 3; i++) {
418
+ cur = { x: cur, y: cur }
419
+ }
420
+ let shared = D(MS.small) //同一個Date物件被兩處引用: 回報先走到的路徑
421
+ let out = await ob.run(() => entry.call(wo, [{ id: 'p0', a: shared, b: shared }]))
422
+ chkRej(out, entry, expectMsg('data[0].a'))
423
+ out = await ob.run(() => entry.call(wo, [{ id: 'p0', dag: cur }]))
424
+ chkRej(out, entry, expectMsg('data[0].dag.x.x.x.t'))
425
+ })
426
+ }
427
+
428
+ //呼叫端無id之紀錄: 補id只發生在複本, reject後呼叫端物件仍無id
429
+ for (let entry of entries) {
430
+ it(`should leave the caller record without id untouched when ${entry.name} rejects`, async function() {
431
+ await reset(entry)
432
+ let before = await snap()
433
+ let rec = { t: D(MS.big) }
434
+ let nodes = leaves(rec)
435
+ let out = await ob.run(() => entry.call(wo, rec))
436
+ chkRej(out, entry, expectMsg('data[0].t'))
437
+ assert.strict.deepStrictEqual(Object.keys(rec), ['t'])
438
+ chkSame(nodes, leaves(rec))
439
+ assert.strict.deepStrictEqual(await snap(), before)
440
+ })
441
+ }
442
+
443
+ //路徑格式: 資料之鍵不受白名單文法約束, 非一般識別字者以JSON字串括號法表示, 過長者截短(單鍵>30字元取前30加..., 全路徑>120取...加末100字元)
444
+ let keyCases = [
445
+ { name: 'identifier', key: 'abc', seg: '.abc' },
446
+ { name: 'underscore and digits', key: '_a1', seg: '._a1' },
447
+ { name: 'dollar prefix', key: '$meta', seg: '.$meta' },
448
+ { name: 'non-ASCII letters', key: '日期', seg: '.日期' },
449
+ { name: 'starts with digit', key: '1a', seg: '["1a"]' },
450
+ { name: 'with dot', key: 'a.b', seg: '["a.b"]' },
451
+ { name: 'with space', key: 'a b', seg: '["a b"]' },
452
+ { name: 'with hyphen', key: 'a-b', seg: '["a-b"]' },
453
+ { name: 'with bracket', key: 'a]b', seg: '["a]b"]' },
454
+ { name: 'with double quote', key: 'a"b', seg: '["a\\"b"]' },
455
+ { name: 'with newline', key: 'a\nb', seg: '["a\\nb"]' },
456
+ { name: 'empty', key: '', seg: '[""]' },
457
+ { name: 'lone surrogate', key: 'a\ud800b', seg: '["a\\ud800b"]' },
458
+ { name: 'identifier of 30 chars', key: 'k'.repeat(30), seg: `.${'k'.repeat(30)}` },
459
+ { name: 'identifier of 31 chars', key: 'k'.repeat(31), seg: `.${'k'.repeat(30)}...` },
460
+ { name: 'identifier of 128 chars', key: 'k'.repeat(128), seg: `.${'k'.repeat(30)}...` },
461
+ { name: 'non-identifier of 30 chars', key: `a b${'k'.repeat(27)}`, seg: `["a b${'k'.repeat(27)}"]` },
462
+ { name: 'non-identifier of 31 chars', key: `a b${'k'.repeat(28)}`, seg: `["a b${'k'.repeat(27)}..."]` },
463
+ { name: 'surrogate pair cut at 30 chars', key: `${'a'.repeat(29)}\u{10400}b`, seg: `.${'a'.repeat(29)}?...` },
464
+ { name: 'astral letter ending exactly at 30 chars', key: `${'a'.repeat(28)}\u{10400}${'b'.repeat(5)}`, seg: `.${'a'.repeat(28)}\u{10400}...` },
465
+ { name: 'astral non-letter ending exactly at 30 chars', key: `${'x'.repeat(28)}\u{1F389}${'y'.repeat(5)}`, seg: `["${'x'.repeat(28)}\u{1F389}..."]` },
466
+ ]
467
+ for (let entry of entries) {
468
+ for (let kc of keyCases) {
469
+ it(`should render the key path for ${entry.name} by ${kc.name}`, async function() {
470
+ await reset(entry)
471
+ let rec = { id: 'p0', o: {} }
472
+ rec.o[kc.key] = D(MS.big)
473
+ let out = await ob.run(() => entry.call(wo, [rec]))
474
+ chkRej(out, entry, expectMsg(`data[0].o${kc.seg}`))
475
+ })
476
+ }
477
+
478
+ //全路徑長度: 120(不截短)、121(截短)、更長
479
+ for (let n of [120, 121, 122, 200]) {
480
+ it(`should truncate the whole path longer than 120 chars for ${entry.name} (${n} chars)`, async function() {
481
+ await reset(entry)
482
+ let head = 'data[0]'
483
+ let keys = []
484
+ let len = head.length
485
+ while (len + 11 <= n - 15) { //每段'.'加10字元
486
+ keys.push('k'.repeat(10))
487
+ len += 11
488
+ }
489
+ let last = 'z'.repeat(n - len - 1) //最末段補足至n字元(<=30字元)
490
+ assert.strict.ok(last.length >= 1 && last.length <= 30, `last key length ${last.length}`)
491
+ keys.push(last)
492
+ let full = head + keys.map((k) => `.${k}`).join('')
493
+ assert.strict.equal(full.length, n)
494
+ let o = D(MS.big)
495
+ for (let i = keys.length - 1; i >= 0; i--) {
496
+ o = { [keys[i]]: o }
497
+ }
498
+ let rec = { id: 'p0' }
499
+ Object.assign(rec, o) //keys[0]直接掛在紀錄上
500
+ let out = await ob.run(() => entry.call(wo, [rec]))
501
+ let expect = full.length > 120 ? `...${full.slice(-100)}` : full
502
+ chkRej(out, entry, expectMsg(expect))
503
+ })
504
+ }
505
+ }
506
+
507
+ //呼叫之後、佇列執行之前呼叫端改動Date: 不變式「資料庫內不得出現範圍外Date字串」(檢核須針對實際寫入之值)
508
+ for (let entry of entries) {
509
+ it(`should never store an out-of-range time set on the same Date object after the call for ${entry.name}`, async function() {
510
+ await reset(entry)
511
+ let d = ok()
512
+ let data = { id: 'p0', t: d }
513
+ let out = await ob.run(() => {
514
+ let pm = entry.call(wo, data)
515
+ d.setTime(MS.big) //就地改動同一個Date物件(淺複製不會抓到, 須複製Date本身)
516
+ return pm
517
+ })
518
+ let all = JSON.stringify(await snap())
519
+ assert.strict.ok(all.indexOf('+275760') < 0, 'no out-of-range string in database')
520
+ if (!out.ok) {
521
+ assert.strict.equal(out.err.message, expectMsg('data[0].t'))
522
+ }
523
+ })
524
+ it(`should never store an out-of-range Date set after the call for ${entry.name}`, async function() {
525
+ await reset(entry)
526
+ let data = { id: 'p0', t: ok(), arr: [ok()] }
527
+ let out = await ob.run(() => {
528
+ let pm = entry.call(wo, data)
529
+ data.t = D(MS.big) //呼叫之後同步改動, 佇列執行時才複製與檢核
530
+ data.arr[0] = D(MS.small)
531
+ return pm
532
+ })
533
+ let all = JSON.stringify(await snap())
534
+ assert.strict.ok(all.indexOf('+275760') < 0 && all.indexOf('-271821') < 0, 'no out-of-range string in database')
535
+ if (!out.ok) {
536
+ assert.strict.equal(out.err.message, expectMsg('data[0].t'))
537
+ }
538
+ })
539
+ it(`should never store an out-of-range Date that is replaced after the call for ${entry.name}`, async function() {
540
+ await reset(entry)
541
+ let data = { id: 'p0', t: D(MS.big) }
542
+ let out = await ob.run(() => {
543
+ let pm = entry.call(wo, data)
544
+ data.t = ok()
545
+ return pm
546
+ })
547
+ let all = JSON.stringify(await snap())
548
+ assert.strict.ok(all.indexOf('+275760') < 0, 'no out-of-range string in database')
549
+ if (!out.ok) {
550
+ assert.strict.equal(out.err.message, expectMsg('data[0].t'))
551
+ }
552
+ })
553
+ }
554
+
555
+ //佇列順序: 檢核於佇列內, 先排入之寫入先完成並先發change, 其後之壞寫入才reject, 再其後之寫入照常
556
+ it('should keep queue order: earlier writes finish before a later bad write rejects, and later writes go on', async function() {
557
+ await reset(entries[0])
558
+ let out = await ob.run(async() => {
559
+ let p1 = wo.insertBulk([{ id: 'q1', t: ok() }])
560
+ let p2 = wo.insert({ id: 'q2', t: D(MS.big) }).catch((err) => err)
561
+ let p3 = wo.save({ id: 'q3', t: ok() })
562
+ return Promise.all([p1, p2, p3])
563
+ })
564
+ assert.strict.equal(out.ok, true)
565
+ assert.strict.ok(out.val[1] instanceof Error)
566
+ assert.strict.equal(out.val[1].message, expectMsg('data[0].t'))
567
+ assert.strict.deepStrictEqual(out.evs.map((v) => `${v.t}:${v.mode}`), ['change:insertBulk', 'error:insert', 'change:insert', 'change:save'])
568
+ assert.strict.deepStrictEqual(ids(await snap()), ['base1', 'base2', 'q1', 'q3'])
569
+ })
570
+
571
+ //useCache: reject後快取不殘留舊值, 其後之合法寫入可見
572
+ it('should keep select consistent after a rejected write with useCache', async function() {
573
+ let w1 = mk('cache', { useCache: true })
574
+ await w1.insertBulk([{ id: 'a', t: ok() }])
575
+ assert.strict.deepStrictEqual(ids(await w1.select()), ['a']) //暖快取
576
+ let o1 = mkObserver(w1)
577
+ let out = await o1.invoke('insert', { id: 'b', t: D(MS.big) })
578
+ assert.strict.equal(out.ok, false)
579
+ assert.strict.deepStrictEqual(ids(await w1.select()), ['a'])
580
+ await w1.insert({ id: 'c', t: ok() })
581
+ assert.strict.deepStrictEqual(ids(await w1.select()), ['a', 'c'])
582
+ })
583
+
584
+ //autoGenPk為false: 缺主鍵之既有錯誤先於本檢核(既有優先序不變), 帶主鍵者才進本檢核
585
+ for (let entry of entries) {
586
+ it(`should keep the missing id error before date check when autoGenPk is false for ${entry.name}`, async function() {
587
+ let w1 = mk(`nogen-${entry.key}`, { autoGenPk: false })
588
+ let o1 = mkObserver(w1)
589
+ let out = await o1.run(() => entry.call(w1, { t: D(MS.big) }))
590
+ assert.strict.equal(out.ok, false)
591
+ assert.strict.equal(out.err.message, `can not ${entry.fn} by data[0] without valid id when autoGenPk is false`)
592
+ out = await o1.run(() => entry.call(w1, { id: 'a', t: D(MS.big) }))
593
+ assert.strict.equal(out.ok, false)
594
+ assert.strict.equal(out.err.message, expectMsg('data[0].t'))
595
+ assert.strict.deepStrictEqual(await w1.select(), [])
596
+ })
597
+ }
598
+
599
+ //檢核先於任何讀取(R-DATE-01規則1): 資料庫內放一筆「讀到主鍵p0即失敗」之壞資料(對照: 合法數據會讀到它而失敗), 帶範圍外Date者須先得到本檢核之錯誤
600
+ for (let entry of entries) {
601
+ it(`should check Date before reading the database for ${entry.name}`, async function() {
602
+ let w1 = await mkCorrupt(`noread-${entry.key}`)
603
+ let o1 = mkObserver(w1)
604
+ let ctrl = await o1.run(() => entry.call(w1, [{ id: 'p0', t: ok() }]))
605
+ assert.strict.equal(ctrl.ok, false, 'control: valid data should hit the corrupt record and fail')
606
+ assert.strict.ok(ctrl.err.message.indexOf('invalid data') < 0, ctrl.err.message)
607
+ let out = await o1.run(() => entry.call(w1, [{ id: 'p0', t: D(MS.big) }]))
608
+ chkRej(out, entry, expectMsg('data[0].t'))
609
+ })
610
+ }
611
+
612
+ //主鍵已存在或同批重複者亦先經檢核(insert遇已存在主鍵原本靜默跳過該筆、insertBulk原本以主鍵衝突reject): 範圍外Date優先, 整批reject
613
+ for (let entry of [entries[0], entries[1]]) {
614
+ it(`should reject ${entry.name} by out-of-range Date even when the id already exists`, async function() {
615
+ await reset(entry)
616
+ let before = await snap()
617
+ let out = await ob.run(() => entry.call(wo, [{ id: 'base1', t: D(MS.big) }]))
618
+ chkRej(out, entry, expectMsg('data[0].t'))
619
+ assert.strict.deepStrictEqual(await snap(), before)
620
+ })
621
+ it(`should reject ${entry.name} by out-of-range Date in a record whose id repeats within the batch`, async function() {
622
+ await reset(entry)
623
+ let before = await snap()
624
+ let out = await ob.run(() => entry.call(wo, [{ id: 'dup', t: ok() }, { id: 'dup', t: D(MS.big) }]))
625
+ chkRej(out, entry, expectMsg('data[1].t'))
626
+ assert.strict.deepStrictEqual(await snap(), before)
627
+ })
628
+ }
629
+
630
+ //save之合併(lodash merge為遞迴合併): 巢狀物件合併時新值之Date照常檢核; 同批同主鍵之後筆合併入前筆, 任一筆範圍外即整批reject
631
+ it('should deep-merge nested objects in save update and check the incoming Date', async function() {
632
+ await wo.delAll()
633
+ await wo.insertBulk([{ id: 'p0', o: { keep: 1, a: { x: 1 } }, t: okIso }])
634
+ let out = await ob.run(() => wo.save({ id: 'p0', o: { a: { y: D(Date.UTC(2020, 0, 1)) } } }))
635
+ assert.strict.equal(out.ok, true, `should resolve but got: ${out.err}`)
636
+ assert.strict.deepStrictEqual(await wo.selectByPk('p0'), { id: 'p0', o: { keep: 1, a: { x: 1, y: '2020-01-01T00:00:00.000Z' } }, t: okIso })
637
+ let before = await snap()
638
+ out = await ob.run(() => wo.save({ id: 'p0', o: { a: { y: D(MS.big) } } }))
639
+ chkRej(out, entries[3], expectMsg('data[0].o.a.y'))
640
+ assert.strict.deepStrictEqual(await snap(), before)
641
+ })
642
+ it('should merge a later record of the same id into the earlier one in one save batch and reject the whole batch when either is out of range', async function() {
643
+ await wo.delAll()
644
+ let out = await ob.run(() => wo.save([{ id: 'p0', a: { x: ok() } }, { id: 'p0', a: { y: D(Date.UTC(2020, 0, 1)) } }]))
645
+ assert.strict.equal(out.ok, true, `should resolve but got: ${out.err}`)
646
+ assert.strict.deepStrictEqual(await wo.selectByPk('p0'), { id: 'p0', a: { x: okIso, y: '2020-01-01T00:00:00.000Z' } })
647
+ await wo.delAll()
648
+ out = await ob.run(() => wo.save([{ id: 'p0', a: { x: ok() } }, { id: 'p0', a: { y: D(MS.big) } }]))
649
+ chkRej(out, entries[2], expectMsg('data[1].a.y'))
650
+ out = await ob.run(() => wo.save([{ id: 'p0', a: { x: D(MS.small) } }, { id: 'p0', a: { y: ok() } }]))
651
+ chkRej(out, entries[2], expectMsg('data[0].a.x'))
652
+ assert.strict.deepStrictEqual(await snap(), [])
653
+ })
654
+
655
+ //情境: 原始外審場景「永不過期哨兵new Date(8.64e15)存入後被delAll({t:{$lt:現在}})刪除」改為寫入即reject
656
+ describe('sentinel', function() {
657
+ it('should reject the sentinel on write so that delAll by now can not delete it', async function() {
658
+ let w1 = mk('sentinel')
659
+ await w1.insertBulk([{ id: 'past', t: D(Date.UTC(2020, 0, 1)) }, { id: 'future', t: D(Date.UTC(2099, 0, 1)) }])
660
+ await assert.rejects(() => w1.insert({ id: 'forever', t: D(8.64e15) }), { message: expectMsg('data[0].t') })
661
+ await assert.rejects(() => w1.insertBulk([{ id: 'forever', t: D(8.64e15) }]), { message: expectMsg('data[0].t') })
662
+ await assert.rejects(() => w1.save({ id: 'forever', t: D(8.64e15) }), { message: expectMsg('data[0].t') })
663
+ assert.strict.deepStrictEqual(ids(await w1.select()), ['future', 'past'])
664
+ let r = await w1.delAll({ t: { $lt: new Date() } })
665
+ assert.strict.deepStrictEqual(r, { n: 1, nDeleted: 1, ok: 1 })
666
+ assert.strict.deepStrictEqual(ids(await w1.select()), ['future'])
667
+ })
668
+
669
+ it('should keep null and 9999-12-31T23:59:59.999Z as never-expire alternatives that now-conditions do not hit', async function() {
670
+ let w1 = mk('alt')
671
+ await w1.insertBulk([
672
+ { id: 'past', t: D(Date.UTC(2020, 0, 1)) },
673
+ { id: 'nul', t: null },
674
+ { id: 'end9999', t: new Date('9999-12-31T23:59:59.999Z') },
675
+ ])
676
+ assert.strict.deepStrictEqual(ids(await w1.select({ t: { $lt: new Date() } })), ['past'])
677
+ let r = await w1.delAll({ t: { $lt: new Date() } })
678
+ assert.strict.deepStrictEqual(r, { n: 1, nDeleted: 1, ok: 1 })
679
+ assert.strict.deepStrictEqual(ids(await w1.select()), ['end9999', 'nul'])
680
+ assert.strict.deepStrictEqual(ids(await w1.select({ t: { $gt: D(Date.UTC(2099, 0, 1)) } })), ['end9999'])
681
+ })
682
+
683
+ //既有資料不遷移、不改讀取(R-DATE-01規則7): 已存在之範圍外Date字串仍有字典序風險, 可讀回後以save改寫清理
684
+ it('should leave legacy out-of-range strings readable and uncomparable, and let save rewrite them', async function() {
685
+ let w1 = mk('legacy')
686
+ await w1.insertBulk([
687
+ { id: 'legacyBig', t: '+275760-09-13T00:00:00.000Z' }, //舊版寫入之哨兵(字串不是Date物件, 寫入照常)
688
+ { id: 'legacySmall', t: '-271821-04-20T00:00:00.000Z' },
689
+ { id: 'future', t: D(Date.UTC(2099, 0, 1)) },
690
+ ])
691
+ assert.strict.equal((await w1.selectByPk('legacyBig')).t, '+275760-09-13T00:00:00.000Z')
692
+
693
+ //字典序: '+275760-…'與'-271821-…'皆小於'2026-…', 故$lt現在命中二者(legacyBig按時間序屬未來, 為文件所載之既有資料風險)
694
+ assert.strict.deepStrictEqual(ids(await w1.select({ t: { $lt: new Date() } })), ['legacyBig', 'legacySmall'])
695
+
696
+ //讀回再存回(字串)可行, 其餘欄位更新不受影響
697
+ let row = await w1.selectByPk('legacyBig')
698
+ await w1.save({ ...row, n: 2 })
699
+ assert.strict.deepStrictEqual(await w1.selectByPk('legacyBig'), { id: 'legacyBig', t: '+275760-09-13T00:00:00.000Z', n: 2 })
700
+
701
+ //清理: 改寫為null或範圍內之值後不再被命中
702
+ await w1.save({ id: 'legacyBig', t: null })
703
+ await w1.save({ id: 'legacySmall', t: new Date('0001-01-01T00:00:00.000Z') })
704
+ assert.strict.deepStrictEqual(ids(await w1.select({ t: { $lt: new Date() } })), ['legacySmall'])
705
+
706
+ //以Date改寫為範圍外值仍被拒收, 資料不變
707
+ await assert.rejects(() => w1.save({ id: 'legacyBig', t: D(MS.big) }), { message: expectMsg('data[0].t') })
708
+ assert.strict.equal((await w1.selectByPk('legacyBig')).t, null)
709
+ })
710
+ })
711
+ })