w-orm-lowdb 1.0.36 → 1.0.38

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,628 @@
1
+ import assert from 'assert'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+ import w from 'wsemi'
5
+ import cvStrCases from 'wsemi/src/_cvOrmStrCases.mjs'
6
+ import chkOrmData from 'wsemi/src/chkOrmData.mjs'
7
+ import WOrm from '../src/WOrmLowdb.mjs'
8
+
9
+
10
+ //字串良構(帳本R-ORMSTR-01; 規則: C:\opensrcOper\dspec\w-orm系列條件語法支援盤點.md〈一之三〉9.)
11
+ //共用檢核為wsemi之chkOrmStr、chkOrmData, 契約案例表為wsemi之_cvOrmStrCases(各w-orm-*逐案對公開API執行, 每案全新資料表),
12
+ //本檔驗證各入口(insert、insertBulk、save、del、selectByPk與建構)確實呼叫共用檢核並只使用複製後之複本;
13
+ //lowdb以JSON檔存放, 孤立代理字元可保真(鍵、值、比較三者皆不改寫), 故本批無修缺陷, 價值在[系列一致]: 使同一應用更換後端時資料行為一致(使用者D1: 全系列一律拒收)
14
+ //lowdb能力: doc=可帶任意欄位名與巢狀結構; 無GridFS(group='gfs'與need含'gfs'者略過)
15
+ describe('wellformed', function() {
16
+ let root = './test/_tmp/wellformed'
17
+ let { cases, forms, reasons } = cvStrCases
18
+
19
+ //mk, 案例表之文件工廠(夾具: 字串型主鍵欄id與字串欄name)
20
+ let mk = (id, extra) => ({ id, name: 'n', ...extra })
21
+
22
+ //msgOf, 取reject值之訊息
23
+ let msgOf = (err) => (err instanceof Error ? err.message : String(err))
24
+
25
+ //assertPartial, 取回文件須含期望之各鍵且深度相等(其餘鍵不論)
26
+ let assertPartial = (actual, part) => {
27
+ assert.strict.ok(actual !== null && actual !== undefined, 'row should exist')
28
+ for (let [k, v] of Object.entries(part)) {
29
+ assert.strict.deepStrictEqual(actual[k], v, `row field [${k}]`)
30
+ }
31
+ }
32
+
33
+ let seq = 0
34
+ let nSkipGfs = 0
35
+ let nRan = 0
36
+ let nInvariant = 0
37
+
38
+ //freshBackend, 每案全新資料表(獨立之JSON檔); 回傳wo, 其done()刪除該檔
39
+ let freshBackend = (extraOpt = {}) => {
40
+ seq++
41
+ let fp = path.join(root, `c${seq}.json`)
42
+ let wo = WOrm({ url: fp, db: 'worm', cl: 'test', ...extraOpt })
43
+ wo.done = () => {
44
+ fs.rmSync(fp, { force: true })
45
+ }
46
+ wo.fp = fp
47
+ return wo
48
+ }
49
+
50
+ before(function() {
51
+ w.fsDeleteFolder(root)
52
+ fs.mkdirSync(root, { recursive: true })
53
+ })
54
+
55
+ after(function() {
56
+ w.fsDeleteFolder(root)
57
+ assert.strict.ok(!fs.existsSync(root), 'test folder should be removed')
58
+ })
59
+
60
+ //runCase, 依案例表檔頭[後端套用方式]逐案執行; caseOpt為建構之額外設定
61
+ let runCase = async function(ctx, c, caseOpt = {}) {
62
+
63
+ //caps, lowdb: doc可、gfs無
64
+ if (c.need.includes('gfs')) {
65
+ nSkipGfs++
66
+ return ctx.skip()
67
+ }
68
+ nRan++
69
+ let baseOpt = { url: path.join(root, 'cbase.json'), db: 'worm', cl: 'cbase', ...caseOpt }
70
+ let e = c.expect
71
+ let args = c.args(mk, baseOpt)
72
+
73
+ //建構之案例: 同步throw, 訊息全等
74
+ if (e.kind === 'throw') {
75
+ assert.throws(() => WOrm(...args), (err) => msgOf(err) === e.msg)
76
+ return
77
+ }
78
+
79
+ let wo = freshBackend(caseOpt)
80
+ try {
81
+ await wo.insertBulk(c.pre(mk))
82
+
83
+ let before = {}
84
+ for (let id of (e.unchanged || [])) {
85
+ before[id] = await wo.selectByPk(id)
86
+ }
87
+
88
+ //evs, 記錄呼叫期間之error事件
89
+ let evs = []
90
+ wo.on('error', (mode, data, err) => evs.push({ mode, err }))
91
+
92
+ let r = null
93
+ let rej = null
94
+ try {
95
+ let p = wo[c.api](...args)
96
+ if (c.afterCall) {
97
+ c.afterCall(args)
98
+ }
99
+ r = await p
100
+ }
101
+ catch (err) {
102
+ rej = err
103
+ }
104
+
105
+ if (c.afterCall) {
106
+ //呼叫後立即改動原輸入(group='toctou' mutate-after-call): lowdb於操作開始(序列化佇列輪到時)才cloneDeep輸入(帳本R-WRITE-ERR-01規則1: 前置處理全在Core之try內,
107
+ //外殼不讀輸入值; R-DATE-01之TOCTOU不變式同此), 故呼叫後立即改動之輸入可能被取用, 與案例表預期之[呼叫當時之值]不同(lmdb於呼叫內同步複製故可);
108
+ //本批要求之不變式為[檢查之值即寫入之值]: 無論取到改動前或改動後之值, 寫入者必經檢核, 資料庫內不得出現孤立代理字元, 失敗者以invalid data回報
109
+ nInvariant++
110
+ if (rej) {
111
+ assert.strict.ok(/^invalid data: string contains lone surrogate at \[data\[1\]\.name\]$/.test(msgOf(rej)), `unexpected rejection: ${msgOf(rej)}`)
112
+ }
113
+ else if (Array.isArray(r)) {
114
+ for (let x of r) {
115
+ if (x.ok === 0) {
116
+ assert.strict.ok(/^invalid data: string contains lone surrogate at \[data\[1\]\.name\]$/.test(String(x.err)), `unexpected item error: ${x.err}`)
117
+ }
118
+ }
119
+ }
120
+ for (let row of await wo.select()) {
121
+ assert.strict.equal(chkOrmData(row), null, 'database should not contain lone surrogate')
122
+ }
123
+ for (let id of (e.unchanged || [])) {
124
+ assert.strict.deepStrictEqual(await wo.selectByPk(id), before[id], `unchanged [${id.length}]`)
125
+ }
126
+ return
127
+ }
128
+
129
+ if (e.kind === 'free') {
130
+ //值與鍵名之NUL不屬共用檢核: 可接受, 亦可因自身限制拒收, 但拒收訊息不得為共用檢核之詞彙
131
+ let re = new RegExp(e.notMsg)
132
+ if (rej) {
133
+ assert.strict.ok(!re.test(msgOf(rej)), `free case should not be rejected by shared check: ${msgOf(rej)}`)
134
+ }
135
+ else if (Array.isArray(r)) {
136
+ for (let x of r) {
137
+ if (x.ok === 0) {
138
+ assert.strict.ok(!re.test(String(x.err)), `free case item should not be rejected by shared check: ${x.err}`)
139
+ }
140
+ }
141
+ }
142
+ }
143
+ else if (e.kind === 'rejectAll') {
144
+ assert.strict.ok(rej, 'should reject')
145
+ assert.strict.equal(msgOf(rej), e.msg)
146
+ }
147
+ else {
148
+ assert.strict.ok(!rej, `must not reject: ${rej ? msgOf(rej) : ''}`)
149
+ if (e.kind === 'null') {
150
+ assert.strict.equal(r, null)
151
+ }
152
+ if (e.kind === 'itemErr') {
153
+ assert.strict.ok(Array.isArray(r), 'result should be array')
154
+ r.forEach((x, i) => {
155
+ if (e.bad.includes(i)) {
156
+ assert.strict.deepStrictEqual([x.ok, x.err], [0, e.msgs ? e.msgs[e.bad.indexOf(i)] : e.msg], `item ${i}`)
157
+ }
158
+ else {
159
+ assert.strict.equal(x.ok, 1, `item ${i} should be ok`)
160
+ }
161
+ })
162
+ }
163
+ }
164
+ //error事件(統一規格T10): 整批reject者於reject之前恰一次且訊息全等reject; 逐筆失敗者依序各一次且訊息全等該筆err; 回null與成功者無
165
+ if (e.kind === 'rejectAll') {
166
+ assert.strict.deepStrictEqual(evs, [{ mode: c.api, err: e.msg }], 'error event')
167
+ }
168
+ else if (e.kind === 'itemErr') {
169
+ assert.strict.deepStrictEqual(evs, e.bad.map((v, j) => ({ mode: c.api, err: e.msgs ? e.msgs[j] : e.msg })), 'error events')
170
+ }
171
+ else if (e.kind === 'null' || e.kind === 'ok') {
172
+ assert.strict.deepStrictEqual(evs, [], 'no error event')
173
+ }
174
+ if (e.kind === 'ok' && /^insert/.test(c.api)) {
175
+ assert.strict.equal(r.ok, 1)
176
+ }
177
+ if (e.ids) {
178
+ assert.strict.deepStrictEqual((await wo.select()).map((v) => v.id).sort(), e.ids)
179
+ }
180
+ for (let id of (e.unchanged || [])) {
181
+ assert.strict.deepStrictEqual(await wo.selectByPk(id), before[id], `unchanged [${id.length}]`)
182
+ }
183
+ if (e.kind === 'ok' && e.result) {
184
+ assertPartial(r, e.result)
185
+ }
186
+ for (let [id, part] of Object.entries(e.rows || {})) {
187
+ assertPartial(await wo.selectByPk(id), part)
188
+ }
189
+ }
190
+ finally {
191
+ wo.done()
192
+ }
193
+ }
194
+
195
+ //案例分組: 單元層(group='str'與'data')由wsemi自身測試執行, 本套件只跑API層
196
+ let apiCases = cases.filter((v) => v.group !== 'str' && v.group !== 'data')
197
+ let againCases = apiCases.filter((v) => ['id', 'single', 'val', 'order'].includes(v.group))
198
+
199
+ describe('cases(autoGenPk預設)', function() {
200
+ for (let c of apiCases) {
201
+ it(c.id, async function() {
202
+ await runCase(this, c)
203
+ })
204
+ }
205
+ })
206
+
207
+ describe('cases(autoGenPk:false; 檢核不隨autoGenPk改變, 規則9.6)', function() {
208
+ for (let c of againCases) {
209
+ it(c.id, async function() {
210
+ await runCase(this, c, { autoGenPk: false })
211
+ })
212
+ }
213
+ })
214
+
215
+ describe('案例完整度與略過計數', function() {
216
+ it('API層案例皆被執行, 僅need含gfs者(GridFS, 本後端無)略過', function() {
217
+ let all = [...apiCases, ...againCases]
218
+ let nGfs = all.filter((v) => v.need.includes('gfs')).length
219
+ assert.strict.ok(nGfs > 0 && nGfs < all.length)
220
+ assert.strict.equal(nSkipGfs, nGfs)
221
+ assert.strict.equal(nRan, all.length - nGfs)
222
+ assert.strict.ok(nRan > 1000, `ran ${nRan}`)
223
+
224
+ //mutate-after-call 3案(insert、insertBulk、save)改以不變式斷言(見runCase), 計數固定
225
+ assert.strict.equal(nInvariant, all.filter((v) => v.afterCall).length)
226
+ assert.strict.equal(nInvariant, 3)
227
+ })
228
+ })
229
+
230
+ describe('lowdb特有: JSON檔不產生孤立代理轉義(被拒之寫入不落盤)', function() {
231
+ it('被拒之insert、insertBulk、save(孤立主鍵、值、鍵名)後, 檔案不含孤立代理之轉義且內容不變', async function() {
232
+ let wo = freshBackend()
233
+ try {
234
+ await wo.insert({ id: 'k0', name: 'pre' })
235
+ let text0 = fs.readFileSync(wo.fp, 'utf8')
236
+ let H = 'a\uD800'
237
+ await assert.rejects(() => wo.insert({ id: H, name: 'x' }))
238
+ await assert.rejects(() => wo.insertBulk([{ id: 'g1', name: H }]))
239
+ await assert.rejects(() => wo.insertBulk([{ id: 'g2', meta: { [H]: 1 } }]))
240
+ let rs = await wo.save([{ id: H, name: 'x' }, { id: 'g3', tags: ['ok', H] }])
241
+ assert.strict.deepStrictEqual(rs.map((v) => v.ok), [0, 0])
242
+ let text1 = fs.readFileSync(wo.fp, 'utf8')
243
+ assert.strict.equal(text1, text0)
244
+ assert.strict.ok(!/\\ud[89a-f][0-9a-f]{2}/i.test(text1), 'file should not contain surrogate escapes')
245
+ }
246
+ finally {
247
+ wo.done()
248
+ }
249
+ })
250
+
251
+ it('良構之emoji、CJK擴充B區、U+FFFD主鍵與值照常存檔與讀回', async function() {
252
+ let wo = freshBackend()
253
+ try {
254
+ let okIds = ['a\u{1F600}', '\u{20BB7}野', 'a\uFFFD', 'a\uFFFD\uFFFD\uFFFD']
255
+ await wo.insertBulk(okIds.map((id) => ({ id, name: id, [id]: id })))
256
+ for (let id of okIds) {
257
+ assert.strict.deepStrictEqual(await wo.selectByPk(id), { id, name: id, [id]: id })
258
+ }
259
+ let rs = await wo.del([{ id: 'a\uD800' }, { id: 'a\u{1F600}' }])
260
+ assert.strict.deepStrictEqual(rs.map((v) => v.ok), [0, 1])
261
+ assert.strict.deepStrictEqual((await wo.select()).map((v) => v.id).sort(), ['a\uFFFD', 'a\uFFFD\uFFFD\uFFFD', '\u{20BB7}野'].sort())
262
+ }
263
+ finally {
264
+ wo.done()
265
+ }
266
+ })
267
+ })
268
+
269
+ describe('lowdb特有: 舊資料(孤立主鍵於修正前寫入, 因保真而存在, D8不遷移)', function() {
270
+ //以JSON檔直接造修正前寫入之舊列: JSON.stringify把孤立代理寫為轉義(\ud800), 讀回仍為孤立代理(保真)
271
+ let legacyId = 'a\uD800'
272
+
273
+ //seedLegacy, 直接寫檔; 回傳檔案路徑
274
+ let seedLegacy = () => {
275
+ seq++
276
+ let fp = path.join(root, `legacy${seq}.json`)
277
+ fs.writeFileSync(fp, JSON.stringify({ 'worm:test': [{ id: legacyId, name: 'legacy' }, { id: 'plain', name: 'plain' }, { id: 'a\uFFFD', name: 'fffd' }, { id: 'n\u0000x', name: 'nul' }] }))
278
+ return fp
279
+ }
280
+
281
+ it('select仍讀得到舊列(id為孤立代理): README之檢出法可檢出', async function() {
282
+ let fp = seedLegacy()
283
+ let wo = WOrm({ url: fp, db: 'worm', cl: 'test' })
284
+ try {
285
+ let rs = await wo.select()
286
+ let bad = rs.filter((r) => typeof r.id === 'string' && !r.id.isWellFormed())
287
+ assert.strict.deepStrictEqual(bad.map((v) => v.name), ['legacy'])
288
+ }
289
+ finally {
290
+ fs.rmSync(fp, { force: true })
291
+ }
292
+ })
293
+
294
+ it('以孤立id定址之selectByPk、del被拒(null、逐筆ok:0), 其他列照常; 舊列留存', async function() {
295
+ let fp = seedLegacy()
296
+ let wo = WOrm({ url: fp, db: 'worm', cl: 'test' })
297
+ try {
298
+ assert.strict.equal(await wo.selectByPk(legacyId), null)
299
+ let r = await wo.del([{ id: legacyId }, { id: 'plain' }])
300
+ assert.strict.deepStrictEqual(r.map((v) => v.ok), [0, 1])
301
+ assert.strict.equal(r[0].err, 'invalid id: string contains lone surrogate at [data[0].id]')
302
+ assert.strict.deepStrictEqual((await wo.select()).map((v) => v.name).sort(), ['fffd', 'legacy', 'nul'])
303
+
304
+ //含NUL之舊主鍵同樣被拒(D2): selectByPk回null、del逐筆ok:0, 舊列留存
305
+ assert.strict.equal(await wo.selectByPk('n\u0000x'), null)
306
+ let rn = await wo.del({ id: 'n\u0000x' })
307
+ assert.strict.equal(rn[0].err, 'invalid id: string contains NUL at [data[0].id]')
308
+ assert.strict.deepStrictEqual((await wo.select()).map((v) => v.name).sort(), ['fffd', 'legacy', 'nul'])
309
+ }
310
+ finally {
311
+ fs.rmSync(fp, { force: true })
312
+ }
313
+ })
314
+
315
+ it('舊列之值含孤立代理(保真留存): 對該列save未含該欄之更新照常成功(不連坐檢核既有內容), 舊值原樣保留; save若寫入孤立代理則仍拒收', async function() {
316
+ seq++
317
+ let fp = path.join(root, `legacyval${seq}.json`)
318
+ fs.writeFileSync(fp, JSON.stringify({ 'worm:test': [{ id: 'p2', note: 'x\uD800', n: 1 }] }))
319
+ let wo = WOrm({ url: fp, db: 'worm', cl: 'test' })
320
+ try {
321
+ let r = await wo.save([{ id: 'p2', k: 1 }])
322
+ assert.strict.deepStrictEqual(r.map((v) => v.ok), [1])
323
+ assert.strict.deepStrictEqual(await wo.selectByPk('p2'), { id: 'p2', note: 'x\uD800', n: 1, k: 1 })
324
+ let r2 = await wo.save([{ id: 'p2', note2: 'y\uDC00' }])
325
+ assert.strict.deepStrictEqual(r2.map((v) => v.ok), [0])
326
+ assert.strict.equal(r2[0].err, 'invalid data: string contains lone surrogate at [data[0].note2]')
327
+ }
328
+ finally {
329
+ fs.rmSync(fp, { force: true })
330
+ }
331
+ })
332
+
333
+ it('清除路徑: 依他欄之delAll或delAll()可刪除舊列', async function() {
334
+ for (let way of ['byField', 'all']) {
335
+ let fp = seedLegacy()
336
+ let wo = WOrm({ url: fp, db: 'worm', cl: 'test' })
337
+ try {
338
+ if (way === 'byField') {
339
+ let r = await wo.delAll({ name: 'legacy' })
340
+ assert.strict.deepStrictEqual(r, { n: 1, nDeleted: 1, ok: 1 })
341
+ assert.strict.deepStrictEqual((await wo.select()).map((v) => v.name).sort(), ['fffd', 'nul', 'plain'])
342
+ }
343
+ else {
344
+ let r = await wo.delAll()
345
+ assert.strict.deepStrictEqual(r, { n: 4, nDeleted: 4, ok: 1 })
346
+ assert.strict.deepStrictEqual(await wo.select(), [])
347
+ }
348
+ }
349
+ finally {
350
+ fs.rmSync(fp, { force: true })
351
+ }
352
+ }
353
+ })
354
+ })
355
+
356
+ describe('lowdb特有: save合併路徑之繼承存取器(檢查之值即寫入之值)', function() {
357
+ //類別實例(原型非Object.prototype)上之可列舉存取器: cloneDeep保留原型故仍在複本上, lodash merge(keysIn)於合併時讀取之;
358
+ //存取器每次讀取可回不同值, 故合併後之最終物件(實際寫入者)須再檢核一次
359
+ let mkInh = (id, vals) => {
360
+ let n = 0
361
+ let proto = {
362
+ get flip() {
363
+ let v = vals[Math.min(n, vals.length - 1)]
364
+ n++
365
+ return v
366
+ }
367
+ }
368
+ let d = Object.create(proto)
369
+ d.id = id
370
+ d.name = 'n'
371
+ return d
372
+ }
373
+
374
+ it('繼承存取器首次讀回良構值、其後讀回孤立代理: 該筆ok:0附err, 原記錄不變', async function() {
375
+ let wo = freshBackend()
376
+ try {
377
+ await wo.insert({ id: 'g1', name: 'pre' })
378
+ let r = await wo.save([mkInh('g1', ['ok', 'bad\uD800'])])
379
+ assert.strict.deepStrictEqual(r.map((v) => v.ok), [0])
380
+ assert.strict.equal(r[0].err, 'invalid data: string contains lone surrogate at [data[0].flip]')
381
+ assert.strict.deepStrictEqual(await wo.selectByPk('g1'), { id: 'g1', name: 'pre' })
382
+ }
383
+ finally {
384
+ wo.done()
385
+ }
386
+ })
387
+
388
+ it('繼承存取器自第k次讀取起回孤立代理(k=1..6): 寫入者不得為孤立代理(該筆拒收且原記錄不變, 或寫入檢核所讀之良構值)', async function() {
389
+ let nRej = 0
390
+ let nOk = 0
391
+ for (let kBad = 1; kBad <= 6; kBad++) {
392
+ let wo = freshBackend()
393
+ try {
394
+ await wo.insert({ id: 'g1', name: 'pre' })
395
+ let n = 0
396
+ let proto = {
397
+ get flip() {
398
+ n++
399
+ return n >= kBad ? 'bad\uD800' : 'ok'
400
+ }
401
+ }
402
+ let d = Object.create(proto)
403
+ d.id = 'g1'
404
+ d.name = 'n'
405
+ let r = await wo.save([d])
406
+ let row = await wo.selectByPk('g1')
407
+ if (r[0].ok === 0) {
408
+ nRej++
409
+ assert.strict.equal(r[0].err, 'invalid data: string contains lone surrogate at [data[0].flip]', `k=${kBad}`)
410
+ assert.strict.deepStrictEqual(row, { id: 'g1', name: 'pre' }, `k=${kBad}`)
411
+ }
412
+ else {
413
+ nOk++
414
+ assert.strict.equal(row.flip, 'ok', `k=${kBad}`)
415
+ }
416
+ }
417
+ finally {
418
+ wo.done()
419
+ }
420
+ }
421
+ assert.strict.ok(nRej >= 1 && nOk >= 1, `both outcomes should occur: rejected ${nRej}, written ${nOk}`)
422
+ })
423
+
424
+ it('繼承存取器恆回良構值: 照常合併存入(既有行為不變)', async function() {
425
+ let wo = freshBackend()
426
+ try {
427
+ await wo.insert({ id: 'g1', name: 'pre' })
428
+ let r = await wo.save([mkInh('g1', ['ok'])])
429
+ assert.strict.deepStrictEqual(r.map((v) => v.ok), [1])
430
+ assert.strict.deepStrictEqual(await wo.selectByPk('g1'), { id: 'g1', name: 'n', flip: 'ok' })
431
+ }
432
+ finally {
433
+ wo.done()
434
+ }
435
+ })
436
+
437
+ it('繼承存取器於檢核時即回孤立代理: 該筆ok:0附err(檢核先於讀取與寫入)', async function() {
438
+ let wo = freshBackend()
439
+ try {
440
+ await wo.insert({ id: 'g1', name: 'pre' })
441
+ let r = await wo.save([mkInh('g1', ['bad\uD800'])])
442
+ assert.strict.deepStrictEqual(r.map((v) => v.ok), [0])
443
+ assert.strict.equal(r[0].err, 'invalid data: string contains lone surrogate at [data[0].flip]')
444
+ assert.strict.deepStrictEqual(await wo.selectByPk('g1'), { id: 'g1', name: 'pre' })
445
+ }
446
+ finally {
447
+ wo.done()
448
+ }
449
+ })
450
+ })
451
+
452
+ describe('事件、時序與呼叫形式', function() {
453
+ let H = 'a\uD800'
454
+
455
+ it('insert/insertBulk整批reject: error事件於reject前恰一次, 訊息同reject, 不發change', async function() {
456
+ for (let fn of ['insert', 'insertBulk']) {
457
+ let wo = freshBackend()
458
+ try {
459
+ let evs = []
460
+ wo.on('error', (mode, data, err) => evs.push({ t: 'error', mode, err }))
461
+ wo.on('change', (mode) => evs.push({ t: 'change', mode }))
462
+ let rej = null
463
+ try {
464
+ await wo[fn]([mk('g0'), mk(H)])
465
+ }
466
+ catch (err) {
467
+ rej = err
468
+ }
469
+ assert.strict.ok(rej)
470
+ assert.strict.deepStrictEqual(evs, [{ t: 'error', mode: fn, err: 'invalid id: string contains lone surrogate at [data[1].id]' }], fn)
471
+ assert.strict.equal(msgOf(rej), 'invalid id: string contains lone surrogate at [data[1].id]')
472
+ assert.strict.deepStrictEqual(await wo.select(), [])
473
+ }
474
+ finally {
475
+ wo.done()
476
+ }
477
+ }
478
+ })
479
+
480
+ it('insert之returnList:true遇壞列: 仍整批reject(不回逐筆清單), error事件恰一次', async function() {
481
+ let wo = freshBackend()
482
+ try {
483
+ let evs = []
484
+ wo.on('error', (mode, data, err) => evs.push({ mode, err }))
485
+ await assert.rejects(() => wo.insert([mk('g0'), mk(H)], { returnList: true }), (err) => msgOf(err) === 'invalid id: string contains lone surrogate at [data[1].id]')
486
+ assert.strict.deepStrictEqual(evs, [{ mode: 'insert', err: 'invalid id: string contains lone surrogate at [data[1].id]' }])
487
+ assert.strict.deepStrictEqual(await wo.select(), [])
488
+ }
489
+ finally {
490
+ wo.done()
491
+ }
492
+ })
493
+
494
+ it('save/del逐筆: 各壞筆error事件一次(訊息含該筆索引), 其餘筆照常', async function() {
495
+ let wo = freshBackend()
496
+ try {
497
+ await wo.insertBulk([mk('k1')])
498
+ let evs = []
499
+ wo.on('error', (mode, data, err) => evs.push({ t: 'error', mode, err }))
500
+ wo.on('change', (mode) => evs.push({ t: 'change', mode }))
501
+ let rs = await wo.save([mk(H, { name: 'x' }), mk('k1', { name: 'y' }), mk('b\uDC00', { name: 'z' })])
502
+ assert.strict.deepStrictEqual(rs.map((v) => v.ok), [0, 1, 0])
503
+ assert.strict.deepStrictEqual(evs.filter((v) => v.t === 'error').map((v) => v.err), [
504
+ 'invalid id: string contains lone surrogate at [data[0].id]',
505
+ 'invalid id: string contains lone surrogate at [data[2].id]',
506
+ ])
507
+ evs.length = 0
508
+ let rd = await wo.del([{ id: H }, { id: 'k1' }, { id: 'b\uDC00' }])
509
+ assert.strict.deepStrictEqual(rd.map((v) => v.ok), [0, 1, 0])
510
+ assert.strict.deepStrictEqual(evs.filter((v) => v.t === 'error').map((v) => v.err), [
511
+ 'invalid id: string contains lone surrogate at [data[0].id]',
512
+ 'invalid id: string contains lone surrogate at [data[2].id]',
513
+ ])
514
+ }
515
+ finally {
516
+ wo.done()
517
+ }
518
+ })
519
+
520
+ it('selectByPk孤立或含NUL之主鍵回null且不發error事件', async function() {
521
+ let wo = freshBackend()
522
+ try {
523
+ await wo.insert({ id: 'a\uFFFD', name: 'fffd' })
524
+ let n = 0
525
+ wo.on('error', () => n++)
526
+ assert.strict.equal(await wo.selectByPk(H), null)
527
+ assert.strict.equal(await wo.selectByPk('a\u0000b'), null)
528
+ assert.strict.equal(n, 0)
529
+ }
530
+ finally {
531
+ wo.done()
532
+ }
533
+ })
534
+
535
+ it('建構時孤立代理於同步throw, 且不建立任何檔案', function() {
536
+ let fp = path.join(root, 'ctor-nocreate.json')
537
+ assert.throws(() => WOrm({ url: fp, db: 'd\uD800', cl: 'c' }), (err) => msgOf(err) === 'invalid opt.db: string contains lone surrogate')
538
+ assert.throws(() => WOrm({ url: fp, db: 'd', cl: 'c\uDC00' }), (err) => msgOf(err) === 'invalid opt.cl: string contains lone surrogate')
539
+ assert.throws(() => WOrm({ url: `${fp}\uD800`, db: 'd', cl: 'c' }), (err) => msgOf(err) === 'invalid opt.url: string contains lone surrogate')
540
+ assert.strict.equal(fs.existsSync(fp), false)
541
+ assert.strict.equal(fs.readdirSync(root).filter((n) => n.startsWith('ctor-nocreate')).length, 0)
542
+ })
543
+
544
+ it('autoGenPk為true時未帶有效id之筆先補主鍵再檢核: 照常寫入; 其資料不合格者仍拒收(訊息為資料路徑)', async function() {
545
+ for (let fn of ['insert', 'insertBulk', 'save']) {
546
+ let wo = freshBackend()
547
+ try {
548
+ let r = await wo[fn]([{ name: 'a' }, { id: '', name: 'b' }, { id: 12345, name: 'c' }])
549
+ if (fn === 'save') {
550
+ assert.strict.deepStrictEqual(r.map((v) => v.ok), [1, 1, 1], fn)
551
+ }
552
+ else {
553
+ assert.strict.equal(r.ok, 1, fn)
554
+ }
555
+ let rows = await wo.select()
556
+ assert.strict.equal(rows.length, 3, fn)
557
+ assert.strict.deepStrictEqual(rows.map((v) => v.name).sort(), ['a', 'b', 'c'], fn)
558
+ for (let x of rows) {
559
+ assert.strict.ok(typeof x.id === 'string' && x.id !== '' && x.id.isWellFormed(), fn)
560
+ }
561
+
562
+ //資料不合格: 主鍵已補(良構), 故訊息為資料路徑
563
+ if (fn === 'save') {
564
+ let rs = await wo.save([{ name: 'x\uD800' }])
565
+ assert.strict.equal(rs[0].err, 'invalid data: string contains lone surrogate at [data[0].name]', fn)
566
+ }
567
+ else {
568
+ await assert.rejects(() => wo[fn]([{ name: 'ok' }, { name: 'x\uD800' }]), (err) => msgOf(err) === 'invalid data: string contains lone surrogate at [data[1].name]', fn)
569
+ }
570
+ }
571
+ finally {
572
+ wo.done()
573
+ }
574
+ }
575
+ })
576
+
577
+ it('Date範圍外與孤立代理並存: 先報Date(帳本R-DATE-01之既有行為不變), 兩者單獨存在時各自拒收', async function() {
578
+ for (let fn of ['insert', 'insertBulk']) {
579
+ let wo = freshBackend()
580
+ try {
581
+ await assert.rejects(() => wo[fn]([{ id: 'x', t: new Date(8.64e15), name: 'a\uD800' }]), (err) => /^invalid data: Date year is out of range 0000-9999 at \[data\[0\]\.t\]$/.test(msgOf(err)), fn)
582
+ await assert.rejects(() => wo[fn]([{ id: 'x', t: new Date(0), name: 'a\uD800' }]), (err) => msgOf(err) === 'invalid data: string contains lone surrogate at [data[0].name]', fn)
583
+ await assert.rejects(() => wo[fn]([{ id: 'x', t: new Date(8.64e15), name: 'ok' }]), (err) => /^invalid data: Date year is out of range/.test(msgOf(err)), fn)
584
+ assert.strict.deepStrictEqual(await wo.select(), [], fn)
585
+ }
586
+ finally {
587
+ wo.done()
588
+ }
589
+ }
590
+ })
591
+
592
+ it('單一物件(非陣列)呼叫之批次索引為data[0]', async function() {
593
+ let wo = freshBackend()
594
+ try {
595
+ await assert.rejects(() => wo.insert(mk(H)), (err) => msgOf(err) === 'invalid id: string contains lone surrogate at [data[0].id]')
596
+ let rs = await wo.save(mk(H))
597
+ assert.strict.equal(rs[0].err, 'invalid id: string contains lone surrogate at [data[0].id]')
598
+ let rd = await wo.del(mk(H))
599
+ assert.strict.equal(rd[0].err, 'invalid id: string contains lone surrogate at [data[0].id]')
600
+ }
601
+ finally {
602
+ wo.done()
603
+ }
604
+ })
605
+
606
+ it('del之非字串或空主鍵之訊息不回顯值, 含批次索引', async function() {
607
+ let wo = freshBackend()
608
+ try {
609
+ let rd = await wo.del([{ id: 123456 }, { id: '' }, { name: 'noid' }, { id: { secret: 'S3CR3T' } }])
610
+ assert.strict.deepStrictEqual(rd.map((v) => v.ok), [0, 0, 0, 0])
611
+ for (let x of rd) {
612
+ assert.strict.ok(!/123456|S3CR3T|noid/.test(x.err), x.err)
613
+ assert.strict.ok(/^invalid id: .+ at \[data\[\d\]\.id\]$/.test(x.err), x.err)
614
+ }
615
+ }
616
+ finally {
617
+ wo.done()
618
+ }
619
+ })
620
+ })
621
+
622
+ describe('案例表契約', function() {
623
+ it('forms與reasons詞彙存在(案例表契約)', function() {
624
+ assert.strict.ok(Object.keys(forms).length >= 33)
625
+ assert.strict.deepStrictEqual(Object.keys(reasons).sort(), ['lone', 'notStr', 'nul'])
626
+ })
627
+ })
628
+ })