w-orm-reladb 1.1.12 → 1.1.13

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,645 @@
1
+ import assert from 'assert'
2
+ import cloneDeep from 'lodash-es/cloneDeep.js'
3
+ import wo from '../src/WOrmReladb.mjs'
4
+ import cvStrCases from 'wsemi/src/_cvOrmStrCases.mjs'
5
+
6
+
7
+ //本檔為mssql與sqlite共用之字串良構測試套組(CLAUDE_rulebook.md之R13), 由api-sqlite.test.mjs與api-mssql.test.mjs引入
8
+ //規格句(dspec〈一之三〉9.): ORM字串(主鍵值、寫入資料之字串值與鍵名含巢狀與陣列元素、opt.db/opt.cl/opt.url/opt.storage)不得含孤立代理碼元, 主鍵另不得含NUL(值之NUL不拒);
9
+ //處置與既有[無效主鍵]同形: insert/insertBulk寫入前整批reject且不寫任何一筆; save/del該筆ok:0附err而同批其他筆照常; selectByPk回null; 建構時opt不合者throw;
10
+ //訊息為固定詞彙且不回顯值; 檢核作用於cloneDeep後之複本且先於isClosed以外之任何讀取、連線、寫入、刪除; 不隨autoGenPk改變
11
+ //案例表為wsemi之契約案例表(src/_cvOrmStrCases.mjs, 純資料), 逐案對公開API執行, 使共用函數chkOrmStr、chkOrmData被正確接線而非各寫一份;
12
+ //reladb之model固定欄位(users/twf只有id、name), 故需[資料可帶任意欄位名與巢狀結構](need含doc)之接受案略過, 拒收案不需doc(檢核先於欄位過濾)故照跑; 本套件無GridFS, need含gfs者略過
13
+ //model為models/twf.js(id STRING主鍵、name TEXT), 專用資料表, 不與其他套組共用
14
+
15
+
16
+ let msgOf = (err) => String(err && err.message !== undefined ? err.message : err)
17
+
18
+ let settle = (pm) => pm.then((v) => ({ ok: true, v })).catch((err) => ({ err }))
19
+
20
+ let mk = (id, extra) => ({ id, name: 'n', ...extra })
21
+
22
+ let apiCases = cvStrCases.cases.filter((v) => v.group !== 'str' && v.group !== 'data')
23
+
24
+ //assertPartial, actual須含part之每個鍵且深度相等(其餘鍵不論)
25
+ let assertPartial = (actual, part, tag) => {
26
+ assert.ok(actual !== null && actual !== undefined, `${tag}: 取回為${actual}`)
27
+ for (let [k, v] of Object.entries(part)) {
28
+ assert.deepStrictEqual(actual[k], v, `${tag}: 欄位[${k}]`)
29
+ }
30
+ }
31
+
32
+ //不給instance(每次呼叫自行初始化與關閉)時只跑代表性子集, 因每次呼叫各自連線
33
+ let formsSub = ['HI_END', 'LO_MID', 'NUL_MID', 'EMOJI']
34
+
35
+
36
+ /**
37
+ * 定義共用測試套組
38
+ *
39
+ * @param {String} label 輸入後端名稱字串('sqlite'或'mssql'), 供describe標題與方言差異使用
40
+ * @param {Function} getOpt 輸入取得建構設定之函數, 於hook內呼叫以取得當下有效之連線資訊
41
+ */
42
+ export function defineWellformedSuites(label, getOpt) {
43
+
44
+ //runCase, 逐案執行(寫法依案例表檔頭[後端套用方式]); S={ w:驗證用物件, cs:驗證用option, call:呼叫用物件, callCs:呼叫用option, base:建構設定 }
45
+ let runCase = async (c, S) => {
46
+ let e = c.expect
47
+
48
+ //throw, 建構函數同步throw
49
+ if (e.kind === 'throw') {
50
+ let args = c.args(mk, S.base)
51
+ assert.throws(() => wo(...args), (err) => err instanceof Error && msgOf(err) === e.msg)
52
+ return
53
+ }
54
+
55
+ //reset, 每案以全新資料表狀態與預存列開始
56
+ await S.w.delAll({}, S.cs)
57
+ let pre = c.pre(mk)
58
+ if (pre.length > 0) {
59
+ let rp = await S.w.insertBulk(pre, S.cs)
60
+ assert.strictEqual(rp.nInserted, pre.length, '預存列')
61
+ }
62
+
63
+ //args, 第二引數為option(所有被測API皆如此), 合併instance
64
+ let args = c.args(mk, S.base)
65
+ args[1] = { ...(args[1] || {}), ...S.callCs }
66
+
67
+ //before, 失敗之呼叫不得改動既有列(含不得多出鍵)
68
+ let before = {}
69
+ for (let id of (e.unchanged || [])) {
70
+ before[id] = await S.w.selectByPk(id, S.cs)
71
+ }
72
+
73
+ //snap, 呼叫端物件不得被改動(取值一致案之存取器會因取快照而被讀取, 故略過)
74
+ let snap = c.group === 'toctou' ? null : cloneDeep(args[0])
75
+
76
+ //call
77
+ let evs = []
78
+ let fnE = (mode, data, err) => evs.push({ t: 'error', mode, err, nData: Array.isArray(data) ? data.length : -1 })
79
+ let fnC = (mode) => evs.push({ t: 'change', mode })
80
+ S.call.on('error', fnE)
81
+ S.call.on('change', fnC)
82
+ let r
83
+ let rej
84
+ try {
85
+ let p = S.call[c.api](...args)
86
+ if (c.afterCall) {
87
+ c.afterCall(args)
88
+ }
89
+ r = await p
90
+ }
91
+ catch (err) {
92
+ rej = err
93
+ }
94
+ finally {
95
+ evs.push({ t: 'settle' })
96
+ S.call.off('error', fnE)
97
+ S.call.off('change', fnC)
98
+ }
99
+ let errs = evs.filter((v) => v.t === 'error')
100
+ let chgs = evs.filter((v) => v.t === 'change')
101
+
102
+ if (snap !== null) {
103
+ assert.deepStrictEqual(args[0], snap, '呼叫端物件不變')
104
+ }
105
+
106
+ //kind
107
+ if (e.kind === 'free') {
108
+ //資料值與鍵名之NUL不屬共用檢核: 可接受, 亦可因資料庫自身限制拒收, 但訊息不得為共用檢核之詞彙
109
+ let re = new RegExp(e.notMsg)
110
+ if (rej !== undefined) {
111
+ assert.ok(!re.test(msgOf(rej)), `free reject訊息不得為共用檢核詞彙: ${msgOf(rej)}`)
112
+ }
113
+ else if (Array.isArray(r)) {
114
+ for (let x of r) {
115
+ assert.ok(x.err === undefined || !re.test(String(x.err)), `free 逐筆err不得為共用檢核詞彙: ${x.err}`)
116
+ }
117
+ }
118
+ }
119
+ else if (e.kind === 'rejectAll') {
120
+ assert.ok(rej !== undefined, `應整批reject, 實得 ${JSON.stringify(r)}`)
121
+ assert.strictEqual(msgOf(rej), e.msg)
122
+ assert.deepStrictEqual(errs.map((v) => [v.mode, v.err]), [[c.api, e.msg]], 'error事件恰一次且mode/err一致')
123
+ assert.strictEqual(chgs.length, 0, 'change不發')
124
+ assert.ok(evs.findIndex((v) => v.t === 'error') < evs.findIndex((v) => v.t === 'settle'), 'error事件須於reject之前發出')
125
+ }
126
+ else {
127
+ assert.ok(rej === undefined, `不得reject: ${msgOf(rej)}`)
128
+ if (e.kind === 'null') {
129
+ assert.strictEqual(r, null)
130
+ assert.strictEqual(errs.length, 0)
131
+ assert.strictEqual(chgs.length, 0)
132
+ }
133
+ if (e.kind === 'itemErr') {
134
+ assert.ok(Array.isArray(r), '逐筆結果須為陣列')
135
+ let nIn = Array.isArray(args[0]) ? args[0].length : 1
136
+ assert.strictEqual(r.length, nIn, '回傳與輸入等長')
137
+ r.forEach((x, i) => {
138
+ if (e.bad.includes(i)) {
139
+ let msg = e.msgs ? e.msgs[e.bad.indexOf(i)] : e.msg
140
+ assert.deepStrictEqual([x.ok, x.err], [0, msg], `第${i}筆`)
141
+ //不合格筆之結果形狀(save: n為1而不寫入, del: n與nDeleted為0)
142
+ if (c.api === 'save') {
143
+ assert.deepStrictEqual([x.n, x.nInserted, x.nModified], [1, 0, 0], `第${i}筆之形狀`)
144
+ }
145
+ else if (c.api === 'del') {
146
+ assert.deepStrictEqual([x.n, x.nDeleted], [0, 0], `第${i}筆之形狀`)
147
+ }
148
+ }
149
+ else {
150
+ assert.strictEqual(x.ok, 1, `第${i}筆應成功: ${JSON.stringify(x)}`)
151
+ }
152
+ })
153
+ let wants = e.bad.map((i) => (e.msgs ? e.msgs[e.bad.indexOf(i)] : e.msg)).sort()
154
+ assert.deepStrictEqual(errs.map((v) => v.err).sort(), wants, 'error事件為各不合格筆之訊息')
155
+ assert.ok(errs.every((v) => v.mode === c.api), 'error事件之mode')
156
+ assert.ok(errs.every((v) => v.nData === 1), 'error事件之data為該筆(長度1之陣列)')
157
+ assert.strictEqual(chgs.filter((v) => v.mode === c.api).length, 1, '逐筆處置之函數於結果定案後恰發出一次change(沿用既有行為)')
158
+ }
159
+ if (e.kind === 'ok') {
160
+ assert.strictEqual(errs.length, 0)
161
+ if (/^insert/.test(c.api)) {
162
+ assert.strictEqual(r.ok, 1)
163
+ }
164
+ }
165
+ }
166
+
167
+ //ids, 呼叫後資料表內全部id
168
+ if (e.ids) {
169
+ assert.deepStrictEqual((await S.w.select({}, S.cs)).map((v) => v.id).sort(), e.ids, '資料表內id集合')
170
+ }
171
+
172
+ //unchanged
173
+ for (let id of (e.unchanged || [])) {
174
+ assert.deepStrictEqual(await S.w.selectByPk(id, S.cs), before[id], `既有列[${id.length}碼元]不變`)
175
+ }
176
+
177
+ //result, rows
178
+ if (e.kind === 'ok' && e.result) {
179
+ assertPartial(r, e.result, 'result')
180
+ }
181
+ for (let [id, part] of Object.entries(e.rows || {})) {
182
+ assertPartial(await S.w.selectByPk(id, S.cs), part, `rows[${id.length}碼元]`)
183
+ }
184
+ }
185
+
186
+ //defineTable, 案例表逐案(filter為案例篩選, useInst為是否共用instance, optEx為追加之建構設定)
187
+ let defineTable = (title, optEx, filter, useInst) => {
188
+ describe(`${label} 字串良構: ${title}`, function() {
189
+ let S = {}
190
+
191
+ before(async function() {
192
+ S.base = getOpt({ cl: 'twf', ...optEx })
193
+ S.w = wo(S.base)
194
+ await S.w.createStorage()
195
+ let ins = await S.w.init()
196
+ S.ins = ins
197
+ S.cs = { instance: ins }
198
+ if (useInst) {
199
+ S.call = S.w
200
+ S.callCs = S.cs
201
+ }
202
+ else {
203
+ S.call = wo(S.base) //不給instance時另用一個物件, 不與驗證用物件混用兩種風格
204
+ S.callCs = {}
205
+ }
206
+ })
207
+
208
+ after(async function() {
209
+ await S.ins.close()
210
+ })
211
+
212
+ for (let c of apiCases.filter(filter)) {
213
+ it(`${c.id} ${c.label}`, async function() {
214
+ if (c.need.includes('gfs')) {
215
+ return this.skip() //本套件無GridFS
216
+ }
217
+ if (c.need.includes('doc')) {
218
+ return this.skip() //固定欄位之model無法[接受後讀回]巢狀結構與任意鍵名
219
+ }
220
+ await runCase(c, S)
221
+ })
222
+ }
223
+ })
224
+ }
225
+
226
+ defineTable('案例表逐案(給instance, autoGenPk:true)', {}, () => true, true)
227
+ defineTable('案例表逐案(給instance, autoGenPk:false; 檢核不隨autoGenPk改變)', { autoGenPk: false }, (c) => ['id', 'single', 'val', 'order'].includes(c.group), true)
228
+ defineTable('案例表代表性子集(不給instance, 每次呼叫自行初始化與關閉)', {}, (c) => ['order', 'toctou'].includes(c.group) || (['id', 'single', 'val'].includes(c.group) && formsSub.includes(c.form)), false)
229
+
230
+
231
+ describe(`${label} 字串良構: 專屬斷言`, function() {
232
+
233
+ let w = null
234
+ let ins = null
235
+ let cs = null
236
+
237
+ let reset = async () => {
238
+ await w.delAll({}, cs)
239
+ }
240
+
241
+ let ids = async () => (await w.select({}, cs)).map((v) => v.id).sort()
242
+
243
+ before(async function() {
244
+ w = wo(getOpt({ cl: 'twf' }))
245
+ await w.createStorage()
246
+ ins = await w.init()
247
+ cs = { instance: ins }
248
+ })
249
+
250
+ after(async function() {
251
+ await ins.close()
252
+ })
253
+
254
+ beforeEach(async function() {
255
+ await reset()
256
+ })
257
+
258
+
259
+ it('檢核先於連線與model載入: cl指向不存在之model時, 不合格輸入回報invalid、合格輸入回報can not import model(insert/insertBulk)', async function() {
260
+ let wn = wo(getOpt({ cl: 'twf_nomodel' }))
261
+ for (let api of ['insert', 'insertBulk']) {
262
+ let r1 = await settle(wn[api]([mk('a\uD800')]))
263
+ assert.strictEqual(msgOf(r1.err), 'invalid id: string contains lone surrogate at [data[0].id]', api)
264
+ let r2 = await settle(wn[api]([mk('ok', { name: 'x\uD800' })]))
265
+ assert.strictEqual(msgOf(r2.err), 'invalid data: string contains lone surrogate at [data[0].name]', api)
266
+ let r3 = await settle(wn[api]([mk('a\u0000b')]))
267
+ assert.strictEqual(msgOf(r3.err), 'invalid id: string contains NUL at [data[0].id]', api)
268
+ let r4 = await settle(wn[api]([mk('ok')]))
269
+ assert.ok(msgOf(r4.err).indexOf('can not import model') === 0, `${api} 合格輸入應走到model載入: ${msgOf(r4.err)}`)
270
+ }
271
+ })
272
+
273
+ it('save/del: 全部不合格者不連線(回逐筆ok:0), 含合格者須連線(cl指向不存在之model時整批reject can not import model)', async function() {
274
+ let wn = wo(getOpt({ cl: 'twf_nomodel' }))
275
+ let r1 = await wn.save([mk('a\uD800'), mk('b\uDC00')])
276
+ assert.deepStrictEqual(r1, [
277
+ { n: 1, nInserted: 0, nModified: 0, ok: 0, err: 'invalid id: string contains lone surrogate at [data[0].id]' },
278
+ { n: 1, nInserted: 0, nModified: 0, ok: 0, err: 'invalid id: string contains lone surrogate at [data[1].id]' },
279
+ ])
280
+ let r2 = await wn.del([mk('a\uD800'), mk('b\u0000')])
281
+ assert.deepStrictEqual(r2.map((v) => [v.n, v.nDeleted, v.ok, v.err]), [
282
+ [0, 0, 0, 'invalid id: string contains lone surrogate at [data[0].id]'],
283
+ [0, 0, 0, 'invalid id: string contains NUL at [data[1].id]'],
284
+ ])
285
+ let r3 = await settle(wn.save([mk('a\uD800'), mk('ok')]))
286
+ assert.ok(msgOf(r3.err).indexOf('can not import model') === 0, msgOf(r3.err))
287
+ let r4 = await settle(wn.del([mk('a\uD800'), mk('ok')]))
288
+ assert.ok(msgOf(r4.err).indexOf('can not import model') === 0, msgOf(r4.err))
289
+ })
290
+
291
+ it('selectByPk: 不合格主鍵回null且不連線(cl指向不存在之model時合格主鍵才reject)', async function() {
292
+ let wn = wo(getOpt({ cl: 'twf_nomodel' }))
293
+ for (let v of ['a\uD800', '\uDC00', 'x\uD800y', 'a\u0000b', '\u0000', Object('a\uD800'), Object('a\u0000')]) {
294
+ assert.strictEqual(await wn.selectByPk(v), null)
295
+ }
296
+ let r = await settle(wn.selectByPk('ok'))
297
+ assert.ok(msgOf(r.err).indexOf('can not import model') === 0, msgOf(r.err))
298
+ })
299
+
300
+ it('已關閉之instance先於檢核: 七函數中之寫入與讀取皆回報instance is closed, 不回報invalid', async function() {
301
+ let wc = wo(getOpt({ cl: 'twf' }))
302
+ let ic = await wc.init()
303
+ await ic.close()
304
+ let cc = { instance: ic }
305
+ let bad = [mk('a\uD800', { name: 'x\uD800' })]
306
+ for (let [api, args] of [['insert', [bad, cc]], ['insertBulk', [bad, cc]], ['save', [bad, cc]], ['del', [bad, cc]], ['selectByPk', ['a\uD800', cc]]]) {
307
+ let r = await settle(wc[api](...args))
308
+ assert.ok(r.err !== undefined, `${api} 未reject`)
309
+ assert.ok(msgOf(r.err).indexOf('instance is closed') >= 0, `${api}: ${msgOf(r.err)}`)
310
+ }
311
+ })
312
+
313
+ it('檢核失敗不關閉呼叫端先前以init()持有之instance(insert/insertBulk/save/del皆然)', async function() {
314
+ let wk = wo(getOpt({ cl: 'twf' }))
315
+ let ik = await wk.init()
316
+ let ck = { instance: ik }
317
+ try {
318
+ for (let api of ['insert', 'insertBulk']) {
319
+ let r = await settle(wk[api]([mk('a\uD800')])) //不給instance: 預檢失敗於初始化之前
320
+ assert.strictEqual(msgOf(r.err), 'invalid id: string contains lone surrogate at [data[0].id]')
321
+ let r2 = await wk[api]([mk(`ok-${api}`)], ck) //先前持有之instance仍可用
322
+ assert.strictEqual(r2.nInserted, 1)
323
+ }
324
+ let r3 = await wk.save([mk('a\uD800')])
325
+ assert.strictEqual(r3[0].ok, 0)
326
+ let r4 = await wk.save([mk('ok-save')], ck)
327
+ assert.strictEqual(r4[0].ok, 1)
328
+ let r5 = await wk.del([mk('a\uD800')])
329
+ assert.strictEqual(r5[0].ok, 0)
330
+ let r6 = await wk.del([mk('ok-insert')], ck)
331
+ assert.strictEqual(r6[0].nDeleted, 1)
332
+ }
333
+ finally {
334
+ await ik.close()
335
+ }
336
+ })
337
+
338
+ it('autoGenPk:true時不合格之主鍵不被自動產生之主鍵取代(整批reject, 資料表不變)', async function() {
339
+ let wa = wo(getOpt({ cl: 'twf', autoGenPk: true }))
340
+ let ia = await wa.init()
341
+ let ca = { instance: ia }
342
+ try {
343
+ let r = await settle(wa.insert([{ id: 'a\uD800', name: 'x' }, { name: 'no pk' }], ca))
344
+ assert.strictEqual(msgOf(r.err), 'invalid id: string contains lone surrogate at [data[0].id]')
345
+ assert.deepStrictEqual(await ids(), [])
346
+ let r2 = await wa.save([{ id: 'a\u0000b', name: 'x' }], ca)
347
+ assert.deepStrictEqual(r2.map((v) => [v.ok, v.err]), [[0, 'invalid id: string contains NUL at [data[0].id]']])
348
+ assert.deepStrictEqual(await ids(), [])
349
+ }
350
+ finally {
351
+ await ia.close()
352
+ }
353
+ })
354
+
355
+ it('未給主鍵者仍依autoGenPk自動產生(良構檢核不改變既有行為)', async function() {
356
+ let r = await w.insertBulk([{ name: 'no pk 1' }, { id: 'e1', name: 'e' }], cs)
357
+ assert.strictEqual(r.nInserted, 2)
358
+ let rows = await w.select({}, cs)
359
+ assert.strictEqual(rows.length, 2)
360
+ assert.ok(rows.every((v) => v.id.isWellFormed()))
361
+ })
362
+
363
+ it('String包裝物件之主鍵: 良構者取原始值存放、含孤立代理或NUL者拒收', async function() {
364
+ let r = await w.insert([{ id: Object('wrap-ok'), name: 'x' }], cs)
365
+ assert.strictEqual(r.nInserted, 1)
366
+ assert.deepStrictEqual(await ids(), ['wrap-ok'])
367
+ let r1 = await settle(w.insert([{ id: Object('a\uD800'), name: 'x' }], cs))
368
+ assert.strictEqual(msgOf(r1.err), 'invalid id: string contains lone surrogate at [data[0].id]')
369
+ let r2 = await settle(w.insertBulk([{ id: Object('a\u0000'), name: 'x' }], cs))
370
+ assert.strictEqual(msgOf(r2.err), 'invalid id: string contains NUL at [data[0].id]')
371
+ let r3 = await w.del([{ id: Object('a\uD800') }, { id: Object('wrap-ok') }], cs)
372
+ assert.deepStrictEqual(r3.map((v) => v.ok), [0, 1])
373
+ assert.deepStrictEqual(await ids(), [])
374
+ })
375
+
376
+ it('檢查之值即使用之值(主鍵): 偽裝成String之物件其valueOf僅被讀取一次, 存入者即被檢核者', async function() {
377
+ let n = 0
378
+ let evil = {
379
+ [Symbol.toStringTag]: 'String',
380
+ valueOf() {
381
+ n++
382
+ return n === 1 ? 'evil-ok' : 'evil\uD800'
383
+ },
384
+ }
385
+ let r = await settle(w.insert([{ id: evil, name: 'x' }], cs))
386
+ assert.strictEqual(n, 1, `valueOf被讀取${n}次`)
387
+ assert.ok(r.err === undefined, msgOf(r.err))
388
+ assert.deepStrictEqual(await ids(), ['evil-ok'])
389
+ })
390
+
391
+ it('主鍵欄名非id(opt.pk為key): 檢核跟隨opt.pk, 訊息路徑用該欄名(insert、insertBulk、save、del、selectByPk)', async function() {
392
+ let wk = wo(getOpt({ cl: 'twfk', pk: 'key' }))
393
+ await wk.createStorage()
394
+ let ik = await wk.init()
395
+ let ck = { instance: ik }
396
+ let mkk = (key, extra) => ({ key, name: 'n', ...extra })
397
+ try {
398
+ await wk.delAll({}, ck)
399
+ await wk.insert([mkk('k0')], ck)
400
+ for (let api of ['insert', 'insertBulk']) {
401
+ let r = await settle(wk[api]([mkk('g0'), mkk('a\uD800')], ck))
402
+ assert.strictEqual(msgOf(r.err), 'invalid id: string contains lone surrogate at [data[1].key]', api)
403
+ let r2 = await settle(wk[api]([mkk('g1', { name: 'x\uDC00' })], ck))
404
+ assert.strictEqual(msgOf(r2.err), 'invalid data: string contains lone surrogate at [data[0].name]', api)
405
+ }
406
+ let rs = await wk.save([mkk('s1'), mkk('b\u0000')], ck)
407
+ assert.deepStrictEqual(rs.map((v) => [v.ok, v.err]), [[1, undefined], [0, 'invalid id: string contains NUL at [data[1].key]']])
408
+ let rd = await wk.del([mkk('a\uD800'), mkk('s1')], ck)
409
+ assert.deepStrictEqual(rd.map((v) => [v.ok, v.err]), [[0, 'invalid id: string contains lone surrogate at [data[0].key]'], [1, undefined]])
410
+ assert.strictEqual(await wk.selectByPk('a\uD800', ck), null)
411
+ assert.deepStrictEqual((await wk.select({}, ck)).map((v) => v.key), ['k0'])
412
+ }
413
+ finally {
414
+ await ik.close()
415
+ }
416
+ })
417
+
418
+ it('selectByPk: 包裝物件主鍵只求值一次(第一次良構、其後求值回不合格者不影響查詢鍵)', async function() {
419
+ await w.insert([mk('k-1')], cs)
420
+ let n = 0
421
+ let pkObj = {
422
+ [Symbol.toStringTag]: 'String',
423
+ valueOf() {
424
+ n++
425
+ return n === 1 ? 'k-1' : 'k\uD800'
426
+ },
427
+ }
428
+ let r = await w.selectByPk(pkObj, cs)
429
+ assert.strictEqual(n, 1, `valueOf被讀取${n}次`)
430
+ assert.strictEqual(r.id, 'k-1')
431
+ })
432
+
433
+ it('returnList與交易: 不合格輸入仍整批reject(不回傳逐筆陣列), 呼叫端交易不受影響', async function() {
434
+ let r = await settle(w.insert([mk('a\uD800')], { ...cs, returnList: true }))
435
+ assert.strictEqual(msgOf(r.err), 'invalid id: string contains lone surrogate at [data[0].id]')
436
+ let transaction = await w.genTransaction()
437
+ let ct = { instance: ins, transaction }
438
+ let done = false
439
+ try {
440
+ let r1 = await settle(w.insertBulk([mk('t1'), mk('b\uDC00')], ct))
441
+ assert.strictEqual(msgOf(r1.err), 'invalid id: string contains lone surrogate at [data[1].id]')
442
+ let r2 = await w.insertBulk([mk('t3')], ct)
443
+ assert.deepStrictEqual(r2, { n: 1, nInserted: 1, ok: 1 })
444
+ await transaction.commit()
445
+ done = true
446
+ }
447
+ finally {
448
+ if (!done) {
449
+ await transaction.rollback().catch(() => {}) //斷言失敗時不留開啟中之交易(sqlite會鎖庫而連累後續案)
450
+ }
451
+ }
452
+ assert.deepStrictEqual(await ids(), ['t3'])
453
+ })
454
+
455
+ it('數據於呼叫當下即複製(useStable:true經佇列延後執行與useStable:false皆然): 呼叫後立即改動原數據, 不影響檢核與寫入之值', async function() {
456
+ for (let useStable of [true, false]) {
457
+ let wu = wo(getOpt({ cl: 'twf', useStable }))
458
+ let iu = await wu.init()
459
+ let cu = { instance: iu }
460
+ try {
461
+ for (let api of ['insert', 'insertBulk', 'save']) {
462
+ await wu.delAll({}, cu)
463
+ let docs = [mk(`${api}-1`), mk(`${api}-2`)]
464
+ let p = wu[api](docs, cu)
465
+ docs[1].name = 'x\uD800' //呼叫後立即改動(含不合格值)
466
+ docs[0].id = 'changed'
467
+ docs.length = 1
468
+ await p
469
+ let rows = (await wu.select({}, cu)).sort((a, b) => (a.id < b.id ? -1 : 1))
470
+ assert.deepStrictEqual(rows, [{ id: `${api}-1`, name: 'n' }, { id: `${api}-2`, name: 'n' }], `${api} useStable:${useStable}`)
471
+ }
472
+ await wu.delAll({}, cu)
473
+ await wu.insert([mk('del-1'), mk('del-2')], cu)
474
+ let docs = [{ id: 'del-1' }]
475
+ let p = wu.del(docs, cu)
476
+ docs[0].id = 'del-2'
477
+ let rd = await p
478
+ assert.deepStrictEqual(rd.map((v) => v.nDeleted), [1])
479
+ assert.deepStrictEqual((await wu.select({}, cu)).map((v) => v.id), ['del-2'], `del useStable:${useStable}`)
480
+ }
481
+ finally {
482
+ await iu.close()
483
+ }
484
+ }
485
+ })
486
+
487
+ it('複製拋錯(getter拋錯)時回傳rejected promise而非同步拋出(useStable:true與false)', async function() {
488
+ for (let useStable of [true, false]) {
489
+ let wu = wo(getOpt({ cl: 'twf', useStable }))
490
+ for (let api of ['insert', 'insertBulk', 'save', 'del']) {
491
+ let bad = {
492
+ id: 'g',
493
+ get name() {
494
+ throw new Error('boom')
495
+ },
496
+ }
497
+ let p
498
+ assert.doesNotThrow(() => {
499
+ p = wu[api](bad)
500
+ }, `${api} useStable:${useStable}`)
501
+ let r = await settle(p)
502
+ assert.strictEqual(msgOf(r.err), 'boom', `${api} useStable:${useStable}`)
503
+ }
504
+ }
505
+ })
506
+
507
+ it('save: 字串不合格之筆為逐筆失敗, 該筆之日期範圍外等批次性檢核不另行觸發; 合格筆之日期範圍外仍整批reject(既有行為不變)', async function() {
508
+ let wd = wo(getOpt({ cl: 'tdate' }))
509
+ await wd.createStorage()
510
+ let idate = await wd.init()
511
+ let cd = { instance: idate }
512
+ try {
513
+ await wd.delAll({}, cd)
514
+ let r = await wd.save([
515
+ { id: 'd1', n: 'a\uD800', d: new Date(8.64e15) }, //字串不合格且日期範圍外
516
+ { id: 'd2', n: 'ok', d: new Date('2000-01-01T00:00:00Z') },
517
+ ], cd)
518
+ assert.deepStrictEqual(r.map((v) => v.ok), [0, 1])
519
+ assert.strictEqual(r[0].err, 'invalid data: string contains lone surrogate at [data[0].n]')
520
+ let r2 = await settle(wd.save([{ id: 'd3', n: 'a\uD800' }, { id: 'd4', d: new Date(8.64e15) }], cd))
521
+ assert.strictEqual(msgOf(r2.err), 'invalid data: date is not a valid time in years 1-9999 at [data[1].d]')
522
+ assert.deepStrictEqual((await wd.select({}, cd)).map((v) => v.id), ['d2'])
523
+ }
524
+ finally {
525
+ await idate.close()
526
+ }
527
+ })
528
+
529
+ it('save/del不合格筆之結果形狀與error事件: save為{n:1,nInserted:0,nModified:0,ok:0,err}, del為{n:0,nDeleted:0,ok:0,err}, 各發一次error事件(mode為函數名, data為該筆)', async function() {
530
+ let evs = []
531
+ let fnE = (mode, data, err) => evs.push([mode, data.length, err])
532
+ w.on('error', fnE)
533
+ try {
534
+ let rs = await w.save([mk('a\uD800'), mk('s1')], cs)
535
+ assert.deepStrictEqual(rs[0], { n: 1, nInserted: 0, nModified: 0, ok: 0, err: 'invalid id: string contains lone surrogate at [data[0].id]' })
536
+ assert.strictEqual(rs[1].ok, 1)
537
+ let rd = await w.del([mk('b\uDC00'), mk('s1')], cs)
538
+ assert.deepStrictEqual(rd[0], { n: 0, nDeleted: 0, ok: 0, err: 'invalid id: string contains lone surrogate at [data[0].id]' })
539
+ assert.strictEqual(rd[1].nDeleted, 1)
540
+ }
541
+ finally {
542
+ w.off('error', fnE)
543
+ }
544
+ assert.deepStrictEqual(evs, [
545
+ ['save', 1, 'invalid id: string contains lone surrogate at [data[0].id]'],
546
+ ['del', 1, 'invalid id: string contains lone surrogate at [data[0].id]'],
547
+ ])
548
+ })
549
+
550
+ it('select/delAll之條件含孤立代理或NUL: 維持既有invalid find整批reject(條件側不變), 資料不變', async function() {
551
+ await w.insert([mk('c1'), mk('c2')], cs)
552
+ for (let find of [{ id: 'a\uD800' }, { name: 'x\uDC00' }, { id: { $in: ['c1', 'b\uD800'] } }, { id: 'a\u0000b' }]) {
553
+ for (let api of ['select', 'delAll']) {
554
+ let r = await settle(w[api](find, cs))
555
+ assert.ok(msgOf(r.err).indexOf('invalid find: ') === 0, `${api}: ${msgOf(r.err)}`)
556
+ assert.ok(!msgOf(r.err).includes('\uD800'), '訊息不回顯值')
557
+ }
558
+ }
559
+ assert.deepStrictEqual(await ids(), ['c1', 'c2'])
560
+ })
561
+
562
+ it('save重試耗盡(預讀見該列、更新連續3次未命中)之err不回顯主鍵值(以替身model方法構造)', async function() {
563
+ let md = ins.mds.twf
564
+ md.findOne = async () => ({ id: 'tries-secret', name: 'old' })
565
+ md.update = async () => [0]
566
+ try {
567
+ let r = await w.save([{ id: 'tries-secret', name: 'new' }], cs)
568
+ assert.deepStrictEqual(r.map((v) => [v.ok, v.err]), [[0, 'can not save data[0].id in 3 tries']])
569
+ }
570
+ finally {
571
+ delete md.findOne //還原為繼承之靜態方法
572
+ delete md.update
573
+ }
574
+ })
575
+
576
+ it('del只檢核主鍵: 資料欄位含孤立代理但主鍵良構者照常刪除', async function() {
577
+ await w.insert([mk('d1')], cs)
578
+ let r = await w.del([{ id: 'd1', name: 'x\uD800' }], cs)
579
+ assert.deepStrictEqual(r.map((v) => [v.n, v.nDeleted, v.ok]), [[1, 1, 1]])
580
+ assert.deepStrictEqual(await ids(), [])
581
+ })
582
+
583
+ it('良構之U+FFFD主鍵如常存取, 孤立代理字串不被當成同一鍵(selectByPk不命中、del該筆ok:0而U+FFFD列不被刪)', async function() {
584
+ let r = await w.insert([mk('a\uFFFDb'), mk('g1')], cs)
585
+ assert.strictEqual(r.nInserted, 2)
586
+ assert.strictEqual((await w.selectByPk('a\uFFFDb', cs)).id, 'a\uFFFDb')
587
+ assert.strictEqual(await w.selectByPk('a\uD800b', cs), null)
588
+ let rd = await w.del([mk('a\uD800b'), mk('g1')], cs)
589
+ assert.deepStrictEqual(rd.map((v) => v.ok), [0, 1])
590
+ assert.deepStrictEqual(await ids(), ['a\uFFFDb'])
591
+ })
592
+
593
+ it('去回顯: 型別不符與無效主鍵之訊息不含被拒之值(insert、del)', async function() {
594
+ let wi = wo(getOpt({ cl: 'tpkint' }))
595
+ await wi.createStorage()
596
+ let ii = await wi.init()
597
+ let ci = { instance: ii }
598
+ try {
599
+ await wi.delAll({}, ci)
600
+ let r = await settle(wi.insert([{ id: 12.4, name: 'x' }], ci))
601
+ assert.ok(msgOf(r.err).indexOf('type does not match pk column') >= 0, msgOf(r.err))
602
+ assert.strictEqual(msgOf(r.err).indexOf('12.4'), -1, msgOf(r.err))
603
+ let secret = '0123456789012' //整數主鍵欄之非標準整數寫法字串
604
+ let rd = await wi.del([{ id: secret }, { id: NaN }, { id: '' }], ci)
605
+ for (let x of rd) {
606
+ assert.strictEqual(x.ok, 0)
607
+ assert.strictEqual(String(x.err).indexOf(secret), -1, x.err)
608
+ }
609
+ assert.deepStrictEqual(rd.map((v) => v.err), [
610
+ 'invalid data[0].id, type does not match pk column',
611
+ 'invalid data[1].id',
612
+ 'invalid data[2].id',
613
+ ])
614
+ }
615
+ finally {
616
+ await ii.close()
617
+ }
618
+ })
619
+
620
+ it('既有資料(D8): 庫內已存在之孤立代理主鍵不遷移; 可由select檢出並以他欄條件清除, 孤立主鍵之del被拒', async function() {
621
+ //以sequelize直接寫入(不經本套件之檢核, 模擬舊版寫入的列): mssql之NVARCHAR保真, sqlite以UTF-8存放而改寫為U+FFFD(內容已損, 屬良構)
622
+ let md = ins.mds.twf
623
+ await md.create({ id: 'legacy\uD800', name: 'legacy' })
624
+ let rows = await w.select({}, cs)
625
+ let bads = rows.filter((v) => typeof v.id === 'string' && !v.id.isWellFormed())
626
+ if (label === 'mssql') {
627
+ assert.deepStrictEqual(bads.map((v) => v.id), ['legacy\uD800'], 'README之檢出法應檢出孤立代理主鍵')
628
+ let rd = await w.del([{ id: 'legacy\uD800' }], cs)
629
+ assert.deepStrictEqual(rd.map((v) => [v.ok, v.err]), [[0, 'invalid id: string contains lone surrogate at [data[0].id]']])
630
+ assert.strictEqual((await w.select({}, cs)).length, 1, '孤立主鍵之del被拒, 列仍在')
631
+ let ra = await w.delAll({ name: 'legacy' }, cs) //改以他欄條件清除
632
+ assert.strictEqual(ra.nDeleted, 1)
633
+ }
634
+ else {
635
+ assert.deepStrictEqual(bads, [], 'sqlite不可能存有孤立代理(UTF-8存放時已改寫)')
636
+ assert.deepStrictEqual(rows.map((v) => v.id), ['legacy\uFFFD'], '已被改寫為U+FFFD(良構, 內容已損)')
637
+ let ra = await w.delAll({ name: 'legacy' }, cs)
638
+ assert.strictEqual(ra.nDeleted, 1)
639
+ }
640
+ assert.deepStrictEqual(await ids(), [])
641
+ })
642
+
643
+ })
644
+
645
+ }
@@ -1214,4 +1214,16 @@ describe('normPk', function() {
1214
1214
  }
1215
1215
  })
1216
1216
 
1217
+ it('R13 字串良構: 孤立代理碼元與NUL之字串於各型別主鍵欄皆為無效(含包裝物件), 良構之U+FFFD與成對代理照常(第66項, 與條件側同標準)', function() {
1218
+ for (let kind of ['str', 'int', 'num', 'date', 'any']) {
1219
+ for (let v of ['a\uD800', '\uDC00', 'x\uD800y', 'a\uDC00\uD800', 'a\u0000b', '\u0000', Object('a\uD800'), Object('a\u0000')]) {
1220
+ no(v, kind, 'invalid')
1221
+ }
1222
+ }
1223
+ ok('a\uFFFDb', 'str', 'a\uFFFDb')
1224
+ ok('a😀b', 'str', 'a😀b')
1225
+ ok('a😀b', 'date', 'a😀b')
1226
+ ok(Object('a\uFFFDb'), 'str', 'a\uFFFDb')
1227
+ })
1228
+
1217
1229
  })