w-serv-orm 1.1.15 → 1.1.17

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,952 @@
1
+ import assert from 'assert'
2
+ import WServOrm from '../src/WServOrm.mjs'
3
+ import { buildBaseline } from '../src/baseline.mjs'
4
+ import { createMemOrm } from './tools/memOrm.mjs'
5
+ import { getReject, quiet } from './tools/ormStub.mjs'
6
+
7
+
8
+ //規格: 多人同時編輯之寫入契約 §6、§7、V1、V6、V7、V12、V13、V14; 規則帳本 R-LOCK-01、R-GUARD-01、R-ONLYMOD-01
9
+ //以有狀態之記憶體ORM(test/tools/memOrm.mjs)驗證表鎖、guardWrite與procOrm之opt(mustExist); 真後端(lowdb)見 api-guard.test.mjs
10
+ //
11
+ //規格句對照(spec = C:\opensrcOper\dspec\多人同時編輯之寫入契約.md):
12
+ // L01-L07 §7「每表一鍵; 每個 w-serv-orm 實例一個鎖管理器(withTabLocks). 多表依表名碼元排序取鎖」「不可重入」
13
+ // W01-W03 §6「原子: 讀取、比對、diff、寫入在同一表鎖內(guardWrite)」、V1「B 以舊基準存 → 整次拒收、零寫入」 W04 V12「清單頁多列提交其中一列過期 → 零寫入」
14
+ // W05 §1「不帶基準之寫入照常寫入」(仍取鎖) W06 §5「不識之格式或版本一律拒收 baselineFormat, 不得降級為不帶基準」、V13 W07 §5「逐列存檔之基準」、V6、§6「rowNotFound」
15
+ // W11-W12 V14「比對與寫入間插入他人寫入(含同步、系統直寫) → 不可能發生(同鎖)」、V2
16
+ // M01-M06 §6「修改:只改不增(autoInsert:false); 列已不存在 → rowNotFound」、V6 M07-M08 §6「刪除:檢查存在…未命中 → rowNotFound(不帶基準者亦同)」、V7
17
+ // M09-M14 實作者補強(事後偵測、select 失敗、opt 驗證、擴充函數形狀), 見規則帳本 R-ONLYMOD-01 M15 任務「不得改變: procOrm 既有四參數呼叫之行為」
18
+
19
+ let wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
20
+
21
+ //withTimeout: 死結時以逾時而非整份測試卡死回報
22
+ function withTimeout(pm, ms, label) {
23
+ let t = null
24
+ let tm = new Promise((resolve, reject) => {
25
+ t = setTimeout(() => reject(new Error(`逾時(疑似死結): ${label}`)), ms)
26
+ })
27
+ return Promise.race([pm, tm]).finally(() => clearTimeout(t))
28
+ }
29
+
30
+ let KEYS = ['id', 'name', 'order', 'timeUpdate', 'userIdUpdate']
31
+
32
+ //mkSo: 建立實例; names為資料表名, 各表之keys同
33
+ function mkSo(names = ['a', 'b', 'c']) {
34
+ let ds = {}
35
+ for (let n of names) {
36
+ ds[n] = { keys: KEYS }
37
+ }
38
+ let m = createMemOrm()
39
+ let so = WServOrm(ds, m.WOrmMem, './unused', 'db')
40
+ return { so, m }
41
+ }
42
+
43
+ let CG = { table: 'a', pk: 'id', fields: ['name'], orderKey: 'order' }
44
+
45
+
46
+ describe('withTabLocks 每實例共用表鎖(規格 §7)', function() {
47
+
48
+ it('L01 同表之呼叫循序執行(先進先出), 不同表並行', async function() {
49
+ let { so } = mkSo()
50
+ let log = []
51
+ let job = (tabs, name, ms) => so.withTabLocks(tabs, async () => {
52
+ log.push(`${name}:in`)
53
+ await wait(ms)
54
+ log.push(`${name}:out`)
55
+ return name
56
+ })
57
+ let rs = await Promise.all([job(['a'], 'x', 30), job(['a'], 'y', 5), job(['a'], 'z', 1)])
58
+ assert.deepStrictEqual(rs, ['x', 'y', 'z'])
59
+ assert.deepStrictEqual(log, ['x:in', 'x:out', 'y:in', 'y:out', 'z:in', 'z:out'])
60
+
61
+ log.length = 0
62
+ await Promise.all([job(['a'], 'p', 30), job(['b'], 'q', 30)])
63
+ assert.deepStrictEqual(log.slice(0, 2), ['p:in', 'q:in'], '不同表之鎖互不阻擋')
64
+ })
65
+
66
+ it('L02 回傳fn之結果; fn reject原樣轉出且鎖已釋放(後續呼叫照常執行)', async function() {
67
+ let { so } = mkSo()
68
+ assert.strict.equal(await so.withTabLocks(['a'], () => 42), 42, '同步fn亦可')
69
+ let err = await getReject(so.withTabLocks(['a'], async () => {
70
+ await wait(5)
71
+ return Promise.reject('boom')
72
+ }))
73
+ assert.strict.equal(err, 'boom')
74
+ let e2 = await getReject(so.withTabLocks(['a'], async () => {
75
+ throw new Error('thrown')
76
+ }))
77
+ assert.strict.equal(e2.message, 'thrown')
78
+ let e3 = await getReject(so.withTabLocks(['a', 'b'], () => {
79
+ throw new Error('sync-throw')
80
+ }))
81
+ assert.strict.equal(e3.message, 'sync-throw', '同步拋錯亦為reject, 多表之鎖皆釋放')
82
+ assert.strict.equal(await withTimeout(so.withTabLocks(['b', 'a'], async () => 'both-free'), 1000, '多表鎖未釋放'), 'both-free')
83
+ assert.strict.equal(await withTimeout(so.withTabLocks(['a'], async () => 'after'), 1000, '失敗後之鎖未釋放'), 'after')
84
+ })
85
+
86
+ it('L03 多表取鎖依表名碼元序且不死結: 交錯之兩組多表呼叫與隨機100組呼叫皆完成, 且同表不同時有兩人在鎖內', async function() {
87
+ let { so } = mkSo(['a', 'b', 'c'])
88
+ let inside = { a: 0, b: 0, c: 0 }
89
+ let maxInside = { a: 0, b: 0, c: 0 }
90
+ let done = 0
91
+ let job = (tabs, ms) => so.withTabLocks(tabs, async () => {
92
+ for (let t of tabs) {
93
+ inside[t]++
94
+ maxInside[t] = Math.max(maxInside[t], inside[t])
95
+ }
96
+ await wait(ms)
97
+ for (let t of tabs) {
98
+ inside[t]--
99
+ }
100
+ done++
101
+ })
102
+
103
+ //明確交錯: 取鎖順序若依呼叫端給之次序會互等
104
+ await withTimeout(Promise.all([job(['a', 'b'], 20), job(['b', 'a'], 20), job(['c', 'b', 'a'], 5), job(['a', 'c'], 5)]), 3000, '交錯多表')
105
+ assert.strict.equal(done, 4)
106
+
107
+ //隨機(固定種子, 可重現)
108
+ let seed = 12345
109
+ let rnd = (n) => {
110
+ seed = (seed * 1103515245 + 12345) & 0x7fffffff
111
+ return seed % n
112
+ }
113
+ let all = ['a', 'b', 'c']
114
+ let ps = []
115
+ for (let i = 0; i < 100; i++) {
116
+ let tabs = all.filter(() => rnd(2) === 1)
117
+ if (tabs.length === 0) {
118
+ tabs = [all[rnd(3)]]
119
+ }
120
+ tabs.sort(() => rnd(3) - 1)
121
+ ps.push(job(tabs, rnd(3)))
122
+ }
123
+ await withTimeout(Promise.all(ps), 10000, '隨機多表')
124
+ assert.strict.equal(done, 104)
125
+ assert.deepStrictEqual(maxInside, { a: 1, b: 1, c: 1 }, '同表同時只有一人在鎖內')
126
+ })
127
+
128
+ it('L03b 取鎖次序為表名UTF-16碼元遞增(可觀察): 先持有後取得之表, 才能證明前面之表已被占住; 大寫在小寫之前(非localeCompare)', async function() {
129
+ //表名 'B'(66) 與 'a'(97): 碼元序 B < a, localeCompare 則 a < B
130
+ let { so } = mkSo(['a', 'B', 'c'])
131
+ let log = []
132
+ let holder = so.withTabLocks(['a'], async () => {
133
+ log.push('holder:in')
134
+ await wait(80)
135
+ log.push('holder:out')
136
+ })
137
+ await wait(10)
138
+ //x 宣告 ['a', 'B']: 碼元序先取 B 再取 a(a 被 holder 占住而等待), 故等待期間 x 持有 B
139
+ let x = so.withTabLocks(['a', 'B'], async () => {
140
+ log.push('x:in')
141
+ await wait(10)
142
+ log.push('x:out')
143
+ })
144
+ await wait(10)
145
+ //y 只要 B: 若 x 依碼元序先取了 B, y 會被擋到 x 完成; 若 x 先取 a(如 localeCompare 或呼叫端次序), B 空著, y 會立即執行
146
+ let y = so.withTabLocks(['B'], async () => {
147
+ log.push('y:in')
148
+ })
149
+ await Promise.all([holder, x, y])
150
+ assert.deepStrictEqual(log, ['holder:in', 'holder:out', 'x:in', 'x:out', 'y:in'])
151
+
152
+ //反向對照: 取鎖次序若依呼叫端給之次序(['B','a'])與碼元序同, 此例以 ['B', 'a'] 呼叫結果相同(證明排序與給定次序無關)
153
+ log.length = 0
154
+ let holder2 = so.withTabLocks(['a'], async () => {
155
+ log.push('holder:in')
156
+ await wait(80)
157
+ log.push('holder:out')
158
+ })
159
+ await wait(10)
160
+ let x2 = so.withTabLocks(['B', 'a'], async () => {
161
+ log.push('x:in')
162
+ })
163
+ await wait(10)
164
+ let y2 = so.withTabLocks(['B'], async () => {
165
+ log.push('y:in')
166
+ })
167
+ await Promise.all([holder2, x2, y2])
168
+ assert.deepStrictEqual(log, ['holder:in', 'holder:out', 'x:in', 'y:in'])
169
+ })
170
+
171
+ it('L04 表名去重: 同一表重複給予不自我死結', async function() {
172
+ let { so } = mkSo()
173
+ assert.strict.equal(await withTimeout(so.withTabLocks(['a', 'a', 'b', 'a'], async () => 'ok'), 1000, '重複表名'), 'ok')
174
+ })
175
+
176
+ it('L05 輸入檢查: 非陣列、空陣列、非字串、不屬於本實例之表名、fn非函數皆reject Error, 不取鎖', async function() {
177
+ let { so } = mkSo(['a', 'b'])
178
+ let bads = [
179
+ ['a', undefined], [[], () => 1], ['a', () => 1], [null, () => 1], [[1], () => 1], [['x'], () => 1], [['a', 'x'], () => 1], [['toString'], () => 1], [['__proto__'], () => 1], [['a'], null], [['a'], 'f'],
180
+ ]
181
+ for (let [tabs, fn] of bads) {
182
+ let e = await getReject(so.withTabLocks(tabs, fn))
183
+ assert.ok(e instanceof Error && /invalid (tableNames|fn) of withTabLocks/.test(e.message), `${JSON.stringify(tabs)} → ${e.message}`)
184
+ }
185
+ assert.strict.equal(await withTimeout(so.withTabLocks(['a'], () => 'free'), 1000, '被拒之呼叫不得占鎖'), 'free')
186
+ })
187
+
188
+ it('L06 每實例各一個鎖管理器: 兩個實例之同名表互不阻擋', async function() {
189
+ let o1 = mkSo()
190
+ let o2 = mkSo()
191
+ let log = []
192
+ let job = (so, name) => so.withTabLocks(['a'], async () => {
193
+ log.push(`${name}:in`)
194
+ await wait(30)
195
+ log.push(`${name}:out`)
196
+ })
197
+ await Promise.all([job(o1.so, 'p'), job(o2.so, 'q')])
198
+ assert.deepStrictEqual(log.slice(0, 2), ['p:in', 'q:in'])
199
+ })
200
+
201
+ it('L07 不可重入(JSDoc載明之限制): fn內對同表再取鎖永不返回, 而非重入', async function() {
202
+ let { so } = mkSo()
203
+ let settled = false
204
+ let p = so.withTabLocks(['a'], async () => {
205
+ await so.withTabLocks(['a'], async () => 1)
206
+ return 2
207
+ })
208
+ p.then(() => {
209
+ settled = true
210
+ }, () => {
211
+ settled = true
212
+ })
213
+ await wait(150)
214
+ assert.strict.equal(settled, false)
215
+ //不同表且依全域遞增序(先a後b)巢狀: 單獨一組可完成(不保證與其他呼叫交錯時不死結, 見L08)
216
+ let { so: so2 } = mkSo()
217
+ let r = await withTimeout(so2.withTabLocks(['a'], () => so2.withTabLocks(['b'], async () => 'nested-other')), 1000, '巢狀不同表')
218
+ assert.strict.equal(r, 'nested-other')
219
+ })
220
+
221
+ it('L08 跨層巢狀取不同表之死結(JSDoc與README載明之限制, 外審sol#7): 兩呼叫一個先持b再等a、一個先持a再等b, 各自排序無法修正跨層次序; 巢狀只能依全域遞增序, 或一次宣告全部表', async function() {
222
+ let { so } = mkSo(['a', 'b'])
223
+ let settled = 0
224
+ let hold = () => wait(40)
225
+ //呼叫1: 外層持 b, 內層再取 a(降序); 呼叫2: 外層持 a, 內層再取 b(升序)
226
+ so.withTabLocks(['b'], async () => {
227
+ await hold()
228
+ await so.withTabLocks(['a'], async () => 1)
229
+ }).then(() => {
230
+ settled++
231
+ }, () => {
232
+ settled++
233
+ })
234
+ so.withTabLocks(['a'], async () => {
235
+ await hold()
236
+ await so.withTabLocks(['b'], async () => 2)
237
+ }).then(() => {
238
+ settled++
239
+ }, () => {
240
+ settled++
241
+ })
242
+ await wait(250)
243
+ assert.strict.equal(settled, 0, '兩呼叫互等, 皆未返回(降序之巢狀取鎖為使用端之錯)')
244
+
245
+ //正確作法: 一次宣告全部表(內部依碼元序取鎖), 交錯呼叫皆完成
246
+ let { so: so2 } = mkSo(['a', 'b'])
247
+ let rs = await withTimeout(Promise.all([
248
+ so2.withTabLocks(['b', 'a'], async () => {
249
+ await hold()
250
+ return 1
251
+ }),
252
+ so2.withTabLocks(['a', 'b'], async () => {
253
+ await hold()
254
+ return 2
255
+ }),
256
+ ]), 2000, '一次宣告全部表')
257
+ assert.deepStrictEqual(rs, [1, 2])
258
+ })
259
+
260
+ })
261
+
262
+
263
+ describe('guardWrite 原子防護(規格 §6、V1、V12、V13、V14)', function() {
264
+
265
+ //seed: 寫入初始列並回傳基準
266
+ async function seed(so, rows) {
267
+ await so.procOrm('u0', 'a', 'save', rows)
268
+ let now = await so.woItems.a.select({})
269
+ return { rows: now, baseline: buildBaseline(now, CG) }
270
+ }
271
+ let read = (so) => () => so.woItems.a.select({})
272
+
273
+ it('W01 相符時依序read、write(rows)並回傳write之結果; write收到read之列', async function() {
274
+ let { so } = mkSo()
275
+ let { rows, baseline } = await seed(so, [{ id: 'r1', name: 'A', order: 1 }, { id: 'r2', name: 'B', order: 2 }])
276
+ let got = null
277
+ let r = await so.guardWrite({
278
+ tables: ['a'],
279
+ read: read(so),
280
+ baseline,
281
+ contract: CG,
282
+ write: async (rs) => {
283
+ got = rs
284
+ return 'written'
285
+ },
286
+ })
287
+ assert.strict.equal(r, 'written')
288
+ assert.deepStrictEqual(got.map((v) => v.id).sort(), ['r1', 'r2'])
289
+ assert.strict.equal(got.length, rows.length)
290
+ })
291
+
292
+ it('W02 不符時reject Error(message與key為固定拒收鍵, 附added/removed/changed/reordered), write不被呼叫、資料不變(V1)', async function() {
293
+ let { so, m } = mkSo()
294
+ let { baseline } = await seed(so, [{ id: 'r1', name: 'A', order: 1 }, { id: 'r2', name: 'B', order: 2 }])
295
+ //他人先存: 改r1、刪r2、增r3
296
+ await so.procOrm('u1', 'a', 'save', [{ id: 'r1', name: 'A-other' }, { id: 'r3', name: 'C', order: 3 }])
297
+ await so.procOrm('u1', 'a', 'del', [{ id: 'r2' }])
298
+ let before = JSON.stringify(m.rows('a'))
299
+ let called = 0
300
+ let e = await getReject(so.guardWrite({
301
+ tables: ['a'],
302
+ read: read(so),
303
+ baseline,
304
+ contract: CG,
305
+ write: async () => {
306
+ called++
307
+ },
308
+ }))
309
+ assert.ok(e instanceof Error)
310
+ assert.strict.equal(e.message, 'baselineMismatch')
311
+ assert.strict.equal(e.key, 'baselineMismatch')
312
+ assert.deepStrictEqual(e.added, ['r3'])
313
+ assert.deepStrictEqual(e.removed, ['r2'])
314
+ assert.deepStrictEqual(e.changed, ['r1'])
315
+ assert.strict.equal(e.reordered, false)
316
+ assert.strict.equal(called, 0)
317
+ assert.strict.equal(JSON.stringify(m.rows('a')), before)
318
+ })
319
+
320
+ it('W03 同一基準之兩次寫入, 先動先贏: 第二次整次拒收且零寫入(V1)', async function() {
321
+ let { so, m } = mkSo()
322
+ let { baseline } = await seed(so, [{ id: 'r1', name: 'A', order: 1 }])
323
+ let save = (name) => so.guardWrite({
324
+ tables: ['a'],
325
+ read: read(so),
326
+ baseline,
327
+ contract: CG,
328
+ write: () => so.procOrm('u', 'a', 'save', [{ id: 'r1', name }], { mustExist: true }),
329
+ })
330
+ await save('first')
331
+ let nSave = m.callsOf('save').length
332
+ let e = await getReject(save('second'))
333
+ assert.strict.equal(e.key, 'baselineMismatch')
334
+ assert.strict.equal(m.callsOf('save').length, nSave, '第二次零寫入')
335
+ assert.strict.equal(m.rows('a')[0].name, 'first')
336
+ })
337
+
338
+ it('W04 清單頁多列提交其中一列過期: 整份零寫入(V12)', async function() {
339
+ let { so, m } = mkSo()
340
+ let { baseline } = await seed(so, [{ id: 'r1', name: 'A', order: 1 }, { id: 'r2', name: 'B', order: 2 }, { id: 'r3', name: 'C', order: 3 }])
341
+ await so.procOrm('u1', 'a', 'save', [{ id: 'r3', name: 'C-other' }]) //他人只改r3
342
+ let nSave = m.callsOf('save').length
343
+ let e = await getReject(so.guardWrite({
344
+ tables: ['a'],
345
+ read: read(so),
346
+ baseline,
347
+ contract: CG,
348
+ write: () => so.procOrm('u', 'a', 'save', [{ id: 'r1', name: 'A2' }, { id: 'r2', name: 'B2' }], { mustExist: true }),
349
+ }))
350
+ assert.strict.equal(e.key, 'baselineMismatch')
351
+ assert.deepStrictEqual(e.changed, ['r3'])
352
+ assert.strict.equal(m.callsOf('save').length, nSave)
353
+ assert.deepStrictEqual(m.rows('a').map((v) => v.name), ['A', 'B', 'C-other'])
354
+ })
355
+
356
+ it('W05 baseline為undefined(不帶基準): 跳過比對照常寫入, 仍取鎖與讀取', async function() {
357
+ let { so } = mkSo()
358
+ await seed(so, [{ id: 'r1', name: 'A', order: 1 }])
359
+ await so.procOrm('u1', 'a', 'save', [{ id: 'r1', name: 'changed-by-other' }])
360
+ let log = []
361
+ let calls = []
362
+ let g1 = so.guardWrite({
363
+ tables: ['a'],
364
+ read: async () => {
365
+ log.push('g1:read')
366
+ calls.push('read')
367
+ await wait(30)
368
+ return so.woItems.a.select({})
369
+ },
370
+ write: async (rows) => {
371
+ log.push('g1:write')
372
+ assert.strict.equal(rows.length, 1)
373
+ },
374
+ })
375
+ let g2 = so.withTabLocks(['a'], async () => {
376
+ log.push('g2:in')
377
+ })
378
+ await Promise.all([g1, g2])
379
+ assert.deepStrictEqual(log, ['g1:read', 'g1:write', 'g2:in'], '不帶基準之寫入仍與其他寫入者互斥')
380
+ assert.deepStrictEqual(calls, ['read'])
381
+ })
382
+
383
+ it('W06 baseline為null或格式不合或版本不識: reject baselineFormat, 不降級為不帶基準, write不被呼叫(V13)', async function() {
384
+ let { so, m } = mkSo()
385
+ await seed(so, [{ id: 'r1', name: 'A', order: 1 }])
386
+ let nSave = m.callsOf('save').length
387
+ let called = 0
388
+ for (let b of [null, 'x', 5, {}, [['r1']], [['r1', 'h2:0123456789abcd']], [['r1', 'h1:0123456789abcd'], ['r1', 'h1:0123456789abcd']], [['r1', 'm5zq9x1k']]]) {
389
+ let e = await getReject(so.guardWrite({
390
+ tables: ['a'],
391
+ read: read(so),
392
+ baseline: b,
393
+ contract: CG,
394
+ write: async () => {
395
+ called++
396
+ },
397
+ }))
398
+ assert.strict.equal(e.key, 'baselineFormat', JSON.stringify(b))
399
+ assert.strict.equal(e.message, 'baselineFormat')
400
+ }
401
+ assert.strict.equal(called, 0)
402
+ assert.strict.equal(m.callsOf('save').length, nSave)
403
+ })
404
+
405
+ it('W07 逐列存檔之基準: 該列已被刪 → rowNotFound; 該列被改 → baselineMismatch(V6、V7之前置)', async function() {
406
+ let { so } = mkSo()
407
+ let { rows } = await seed(so, [{ id: 'r1', name: 'A', order: 1 }, { id: 'r2', name: 'B', order: 2 }])
408
+ let blOne = buildBaseline(rows.filter((v) => v.id === 'r1'), CG)
409
+ let readOne = () => so.woItems.a.select({ id: { $in: ['r1'] } })
410
+ let w = () => 'x'
411
+ await so.procOrm('u1', 'a', 'save', [{ id: 'r1', name: 'A-other' }])
412
+ let e1 = await getReject(so.guardWrite({ tables: ['a'], read: readOne, baseline: blOne, contract: CG, write: w }))
413
+ assert.strict.equal(e1.key, 'baselineMismatch')
414
+ await so.procOrm('u1', 'a', 'del', [{ id: 'r1' }])
415
+ let e2 = await getReject(so.guardWrite({ tables: ['a'], read: readOne, baseline: blOne, contract: CG, write: w }))
416
+ assert.strict.equal(e2.key, 'rowNotFound')
417
+ assert.deepStrictEqual(e2.removed, ['r1'])
418
+ })
419
+
420
+ it('W08 write自身之reject原樣轉出(字串不包裝)且鎖已釋放; read回非陣列為Error且鎖已釋放', async function() {
421
+ let { so } = mkSo()
422
+ let { baseline } = await seed(so, [{ id: 'r1', name: 'A', order: 1 }])
423
+ let e1 = await getReject(so.guardWrite({ tables: ['a'], read: read(so), baseline, contract: CG, write: () => Promise.reject('failed to save data') }))
424
+ assert.strict.equal(e1, 'failed to save data')
425
+ let e2 = await getReject(so.guardWrite({ tables: ['a'], read: async () => 'not-array', baseline, contract: CG, write: () => 1 }))
426
+ assert.ok(e2 instanceof Error && /read needs to resolve an array/.test(e2.message))
427
+ let e3 = await getReject(so.guardWrite({
428
+ tables: ['a'],
429
+ read: async () => {
430
+ throw new Error('read failed')
431
+ },
432
+ baseline,
433
+ contract: CG,
434
+ write: () => 1
435
+ }))
436
+ assert.strict.equal(e3.message, 'read failed')
437
+ let r = await withTimeout(so.guardWrite({ tables: ['a'], read: read(so), baseline, contract: CG, write: () => 'ok-after' }), 1000, '失敗後之鎖未釋放')
438
+ assert.strict.equal(r, 'ok-after')
439
+ })
440
+
441
+ it('W09 輸入檢查: 非物件、read或write非函數、帶基準而無契約、契約無效、contract.table不在tables內、tables無效皆reject Error且不取鎖', async function() {
442
+ let { so } = mkSo()
443
+ let { baseline } = await seed(so, [{ id: 'r1', name: 'A', order: 1 }])
444
+ let ok = { tables: ['a'], read: read(so), baseline, contract: CG, write: () => 1 }
445
+ let cases = [
446
+ [undefined, /need an object/], [null, /need an object/], ['x', /need an object/],
447
+ [{ ...ok, read: null }, /invalid read/], [{ ...ok, write: 'f' }, /invalid write/],
448
+ [{ ...ok, contract: undefined }, /invalid contract/], [{ ...ok, contract: { pk: 'id' } }, /invalid contract/],
449
+ [{ ...ok, contract: { ...CG, fields: ['name', 'order'] } }, /fields must not contain orderKey/],
450
+ [{ ...ok, tables: ['b'] }, /need to contain contract.table/], [{ ...ok, tables: 'a' }, /need to contain contract.table/],
451
+ [{ ...ok, tables: [], contract: { ...CG, table: undefined } }, /invalid tableNames/], [{ ...ok, tables: ['zzz'], contract: { ...CG, table: undefined } }, /invalid tableNames/],
452
+ ]
453
+ for (let [p, re] of cases) {
454
+ let e = await getReject(so.guardWrite(p))
455
+ assert.ok(e instanceof Error && re.test(e.message), `${e.message} 不符 ${re}`)
456
+ }
457
+ //未帶基準而給了無效契約者亦檢查(及早發現使用端之錯誤)
458
+ let e = await getReject(so.guardWrite({ tables: ['a'], read: read(so), contract: { pk: 'id' }, write: () => 1 }))
459
+ assert.ok(/invalid contract/.test(e.message))
460
+ assert.strict.equal(await withTimeout(so.guardWrite({ tables: ['a'], read: read(so), write: () => 'free' }), 1000, '被拒之呼叫不得占鎖'), 'free')
461
+ })
462
+
463
+ it('W10 多表guardWrite: 交錯之兩組多表呼叫不死結', async function() {
464
+ let { so } = mkSo(['a', 'b'])
465
+ let { baseline } = await seed(so, [{ id: 'r1', name: 'A', order: 1 }])
466
+ let mk = (tables, ms) => so.guardWrite({
467
+ tables,
468
+ read: async () => {
469
+ await wait(ms)
470
+ return so.woItems.a.select({})
471
+ },
472
+ baseline,
473
+ contract: CG,
474
+ write: async () => 'ok',
475
+ })
476
+ let rs = await withTimeout(Promise.all([mk(['a', 'b'], 20), mk(['b', 'a'], 20), mk(['b', 'a'], 1)]), 3000, '多表guardWrite')
477
+ assert.deepStrictEqual(rs, ['ok', 'ok', 'ok'])
478
+ })
479
+
480
+ it('W11 原子(V14): 比對與寫入之間插不進他人之寫入(含另一withTabLocks呼叫與另一guardWrite), 他人於其後才執行', async function() {
481
+ let { so } = mkSo()
482
+ let { baseline } = await seed(so, [{ id: 'r1', name: 'orig', order: 1 }])
483
+ let log = []
484
+
485
+ let g = so.guardWrite({
486
+ tables: ['a'],
487
+ read: async () => {
488
+ log.push('g:read-start')
489
+ await wait(40)
490
+ log.push('g:read-end')
491
+ return so.woItems.a.select({})
492
+ },
493
+ baseline,
494
+ contract: CG,
495
+ write: async () => {
496
+ log.push('g:write-start')
497
+ await wait(40)
498
+ await so.procOrm('u', 'a', 'save', [{ id: 'r1', name: 'from-g' }], { mustExist: true })
499
+ log.push('g:write-end')
500
+ },
501
+ })
502
+
503
+ await wait(10) //g已在讀取之人為延遲內
504
+ let x = so.withTabLocks(['a'], async () => {
505
+ log.push('x:in')
506
+ await so.procOrm('u', 'a', 'save', [{ id: 'r1', name: 'from-x' }])
507
+ log.push('x:out')
508
+ })
509
+ let y = so.guardWrite({
510
+ tables: ['a'],
511
+ read: read(so),
512
+ baseline,
513
+ contract: CG,
514
+ write: async () => {
515
+ log.push('y:write')
516
+ },
517
+ }).catch((e) => log.push(`y:${e.key}`))
518
+
519
+ await Promise.all([g, x, y])
520
+ assert.deepStrictEqual(log, ['g:read-start', 'g:read-end', 'g:write-start', 'g:write-end', 'x:in', 'x:out', 'y:baselineMismatch'])
521
+ assert.strict.equal(so.woItems.a && (await so.woItems.a.select({}))[0].name, 'from-x')
522
+ })
523
+
524
+ it('W12 不帶基準之他人寫入(外部同步型: 整段delAll加insert)取鎖後, guardWrite於其後讀到完整新資料而非中間態', async function() {
525
+ let { so } = mkSo()
526
+ let { baseline } = await seed(so, [{ id: 'r1', name: 'A', order: 1 }, { id: 'r2', name: 'B', order: 2 }])
527
+ let sync = so.withTabLocks(['a'], async () => {
528
+ await so.woItems.a.delAll({})
529
+ await wait(40) //中間態: 表為空
530
+ await so.woItems.a.insert([{ id: 'r1', name: 'A', order: 1 }, { id: 'r2', name: 'B', order: 2 }])
531
+ })
532
+ await wait(10)
533
+ let seen = null
534
+ let g = so.guardWrite({
535
+ tables: ['a'],
536
+ read: read(so),
537
+ baseline,
538
+ contract: CG,
539
+ write: async (rows) => {
540
+ seen = rows.length
541
+ },
542
+ })
543
+ await Promise.all([sync, g])
544
+ assert.strict.equal(seen, 2, '內容未變之同步不使基準失效(V2), 且guardWrite不會讀到空表')
545
+ })
546
+
547
+ })
548
+
549
+
550
+ describe('procOrm opt: mustExist 只改不增與刪除未命中(規格 §6、V6、V7)', function() {
551
+
552
+ let proc = (so) => (mode, input, opt) => so.procOrm('u1', 'a', mode, input, opt)
553
+
554
+ it('M01 修改不存在之列: reject rowNotFound, 列未被建立且未呼叫ORM之save(V6)', async function() {
555
+ let { so, m } = mkSo()
556
+ let p = proc(so)
557
+ let err = await quiet(() => getReject(p('save', [{ id: 'ghost', name: 'x' }], { mustExist: true })))
558
+ assert.strict.equal(err, 'rowNotFound')
559
+ assert.strict.equal(m.callsOf('save').length, 0)
560
+ assert.strict.equal(m.tables.a.size, 0)
561
+ })
562
+
563
+ it('M02 多列中一列不存在: 零寫入(存在之列亦不改)', async function() {
564
+ let { so, m } = mkSo()
565
+ let p = proc(so)
566
+ await p('save', [{ id: 'r1', name: 'A' }, { id: 'r2', name: 'B' }])
567
+ m.reset()
568
+ let err = await quiet(() => getReject(p('save', [{ id: 'r1', name: 'A2' }, { id: 'ghost', name: 'x' }, { id: 'r2', name: 'B2' }], { mustExist: true })))
569
+ assert.strict.equal(err, 'rowNotFound')
570
+ assert.strict.equal(m.callsOf('save').length, 0)
571
+ assert.deepStrictEqual(m.rows('a').map((v) => v.name), ['A', 'B'])
572
+ })
573
+
574
+ it('M03 全部存在: 正常修改, 回傳id陣列, 轉傳autoInsert:false給ORM之save, 存在檢查為單次select({id:{$in}})且去重', async function() {
575
+ let { so, m } = mkSo()
576
+ let p = proc(so)
577
+ await p('save', [{ id: 'r1', name: 'A' }, { id: 'r2', name: 'B' }])
578
+ m.reset()
579
+ let r = await p('save', [{ id: 'r1', name: 'A2' }, { id: 'r2', name: 'B2' }, { id: 'r1', name: 'A3' }], { mustExist: true })
580
+ assert.deepStrictEqual(r, [{ id: 'r1' }, { id: 'r2' }, { id: 'r1' }])
581
+ assert.deepStrictEqual(m.callsOf('select').map((c) => c.args), [[{ id: { $in: ['r1', 'r2'] } }]])
582
+ let saves = m.callsOf('save')
583
+ assert.strict.equal(saves.length, 1)
584
+ assert.deepStrictEqual(saves[0].args[1], { autoInsert: false })
585
+ assert.deepStrictEqual(m.rows('a').map((v) => v.name), ['A3', 'B2'])
586
+ //單一物件輸入亦可
587
+ let r1 = await p('save', { id: 'r2', name: 'B3' }, { mustExist: true })
588
+ assert.deepStrictEqual(r1, { id: 'r2' })
589
+ })
590
+
591
+ it('M03b 大批: 存在檢查分批(每批至多1000個主鍵, 避開各w-orm-*共用之$in上限10000); 12000列全存在可修改, 缺於最後一批者零寫入', async function() {
592
+ let { so, m } = mkSo()
593
+ let p = proc(so)
594
+ let rows = []
595
+ for (let i = 0; i < 12000; i++) {
596
+ rows.push({ id: `id-${i}`, name: 'A' })
597
+ }
598
+ await p('save', rows)
599
+ m.reset()
600
+ let r = await p('save', rows.map((v) => ({ ...v, name: 'B' })), { mustExist: true })
601
+ assert.strict.equal(r.length, 12000)
602
+ let sels = m.callsOf('select')
603
+ assert.strict.equal(sels.length, 12)
604
+ assert.strict.equal(sels.every((c) => c.args[0].id.$in.length <= 1000), true)
605
+ assert.strict.equal(m.rows('a').every((v) => v.name === 'B'), true)
606
+
607
+ m.reset()
608
+ let withGhost = rows.map((v) => ({ ...v, name: 'C' }))
609
+ withGhost.push({ id: 'ghost-in-last-chunk', name: 'C' })
610
+ let err = await quiet(() => getReject(p('save', withGhost, { mustExist: true })))
611
+ assert.strict.equal(err, 'rowNotFound')
612
+ assert.strict.equal(m.callsOf('save').length, 0, '缺於最後一批, 仍零寫入')
613
+ assert.strict.equal(m.rows('a')[0].name, 'B')
614
+
615
+ let e2 = await quiet(() => getReject(p('del', [...rows, { id: 'ghost' }], { mustExist: true })))
616
+ assert.strict.equal(e2, 'rowNotFound')
617
+ assert.strict.equal(m.rows('a').length, 12000)
618
+ let r3 = await p('del', rows, { mustExist: true })
619
+ assert.strict.equal(r3.length, 12000)
620
+ assert.strict.equal(m.rows('a').length, 0)
621
+ })
622
+
623
+ it('M04 未帶opt或mustExist為false: 既有行為不變(不存在之列被新增, ORM之save為單參數呼叫, 無額外select)', async function() {
624
+ let { so, m } = mkSo()
625
+ let p = proc(so)
626
+ for (let opt of [undefined, null, {}, { mustExist: false }]) {
627
+ m.reset()
628
+ m.tables.a.clear()
629
+ let r = await p('save', [{ id: 'new1', name: 'N' }], opt)
630
+ assert.deepStrictEqual(r, [{ id: 'new1' }])
631
+ assert.strict.equal(m.callsOf('select').length, 0)
632
+ assert.strict.equal(m.callsOf('save')[0].args.length, 1)
633
+ assert.strict.equal(m.rows('a').length, 1)
634
+ }
635
+ })
636
+
637
+ it('M05 修改時未帶主鍵之列: 視為不存在, reject rowNotFound且不補號新增(反); 帶主鍵者照常(正)', async function() {
638
+ let { so, m } = mkSo()
639
+ let p = proc(so)
640
+ await p('save', [{ id: 'r1', name: 'A' }])
641
+ m.reset()
642
+ for (let input of [[{ name: 'no-id' }], { name: 'no-id' }, [{ id: 'r1', name: 'ok' }, { name: 'no-id' }], [{ id: '', name: 'empty-id' }], {}]) {
643
+ let err = await quiet(() => getReject(p('save', input, { mustExist: true })))
644
+ assert.strict.equal(err, 'rowNotFound', JSON.stringify(input))
645
+ }
646
+ assert.strict.equal(m.callsOf('save').length, 0)
647
+ assert.strict.equal(m.tables.a.size, 1)
648
+ assert.deepStrictEqual(await p('save', [{ id: 'r1', name: 'ok' }], { mustExist: true }), [{ id: 'r1' }])
649
+ })
650
+
651
+ it('M06 空陣列之修改視為無事可做, 不查詢不寫入', async function() {
652
+ let { so, m } = mkSo()
653
+ let r = await proc(so)('save', [], { mustExist: true })
654
+ assert.deepStrictEqual(r, [])
655
+ assert.strict.equal(m.callsOf('select').length, 0, '無列可查, 不發存在檢查')
656
+ assert.strict.equal(m.callsOf('save').every((c) => c.args[0].length === 0), true, '若呼叫ORM之save, 內容亦為空陣列(既有行為)')
657
+ assert.strict.equal(m.tables.a.size, 0)
658
+ })
659
+
660
+ it('M07 刪除: 任一列不存在即零刪除並reject rowNotFound(含單列)(V7); 全部存在則刪除; 未帶opt之刪除未命中仍回ORM逐筆結果', async function() {
661
+ let { so, m } = mkSo()
662
+ let p = proc(so)
663
+ await p('save', [{ id: 'r1', name: 'A' }, { id: 'r2', name: 'B' }])
664
+ m.reset()
665
+ let e1 = await quiet(() => getReject(p('del', [{ id: 'ghost' }], { mustExist: true })))
666
+ assert.strict.equal(e1, 'rowNotFound')
667
+ let e2 = await quiet(() => getReject(p('del', [{ id: 'r1' }, { id: 'ghost' }, { id: 'r2' }], { mustExist: true })))
668
+ assert.strict.equal(e2, 'rowNotFound')
669
+ assert.strict.equal(m.callsOf('del').length, 0)
670
+ assert.strict.equal(m.rows('a').length, 2)
671
+ let r = await p('del', [{ id: 'r1' }, { id: 'r2' }], { mustExist: true })
672
+ assert.deepStrictEqual(r, [{ n: 1, nDeleted: 1, ok: 1 }, { n: 1, nDeleted: 1, ok: 1 }])
673
+ assert.strict.equal(m.rows('a').length, 0)
674
+ //重複刪除: 已不存在
675
+ let e3 = await quiet(() => getReject(p('del', [{ id: 'r1' }], { mustExist: true })))
676
+ assert.strict.equal(e3, 'rowNotFound')
677
+ //預設行為不變
678
+ let r0 = await p('del', [{ id: 'r1' }])
679
+ assert.deepStrictEqual(r0, [{ n: 0, nDeleted: 0, ok: 1 }])
680
+ })
681
+
682
+ it('M08 刪除之列無有效主鍵(null、空字串、布林、非物件): 視為不存在, reject rowNotFound且零刪除', async function() {
683
+ let { so, m } = mkSo()
684
+ let p = proc(so)
685
+ await p('save', [{ id: 'r1', name: 'A' }])
686
+ m.reset()
687
+ for (let input of [[{ id: 'r1' }, { id: null }], [{ id: 'r1' }, {}], [{ id: 'r1' }, { id: true }], [{ id: 'r1' }, 'r1'], [{ id: 'r1' }, { id: '' }]]) {
688
+ let err = await quiet(() => getReject(p('del', input, { mustExist: true })))
689
+ assert.strict.equal(err, 'rowNotFound', JSON.stringify(input))
690
+ }
691
+ assert.strict.equal(m.callsOf('del').length, 0)
692
+ assert.strict.equal(m.rows('a').length, 1)
693
+ })
694
+
695
+ it('M07b 刪除之同批重複主鍵(mustExist): 整批拒絕且零刪除(否則第1筆刪除後第2筆必未命中, 事後偵測雖reject卻已刪, 外審sol#1); 訊息列位置不回顯主鍵; 存檔之重複主鍵不受影響', async function() {
696
+ let { so, m } = mkSo()
697
+ let p = proc(so)
698
+ await p('save', [{ id: 'secret-r1', name: 'A' }, { id: 'r2', name: 'B' }])
699
+ m.reset()
700
+ let e1 = await quiet(() => getReject(p('del', [{ id: 'secret-r1' }, { id: 'secret-r1' }], { mustExist: true })))
701
+ assert.strict.equal(e1, 'invalid input of del, duplicate id: data[1]')
702
+ assert.ok(e1.indexOf('secret-r1') < 0)
703
+ let e2 = await quiet(() => getReject(p('del', [{ id: 'r2' }, { id: 'secret-r1' }, { id: 'r2' }, { id: 'secret-r1' }], { mustExist: true })))
704
+ assert.strict.equal(e2, 'invalid input of del, duplicate id: data[2], data[3]')
705
+ assert.strict.equal(m.callsOf('del').length, 0)
706
+ assert.strict.equal(m.rows('a').length, 2, '零刪除')
707
+ //數字與其字串為同一主鍵(R-ID-02)
708
+ let e3 = await quiet(() => getReject(p('del', [{ id: 7 }, { id: '7' }], { mustExist: true })))
709
+ assert.strict.equal(e3, 'invalid input of del, duplicate id: data[1]')
710
+ //無效主鍵不計入重複(由存在檢查以rowNotFound處置)
711
+ let e4 = await quiet(() => getReject(p('del', [{ id: null }, { id: null }], { mustExist: true })))
712
+ assert.strict.equal(e4, 'rowNotFound')
713
+ //未帶mustExist之del對重複主鍵之既有行為不變(第1筆命中、第2筆未命中, 不視為錯誤)
714
+ let r0 = await p('del', [{ id: 'r2' }, { id: 'r2' }])
715
+ assert.deepStrictEqual(r0, [{ n: 1, nDeleted: 1, ok: 1 }, { n: 0, nDeleted: 0, ok: 1 }])
716
+ //save之同批重複主鍵(逐筆依序更新, 後者蓋前者)不受影響
717
+ let r1 = await p('save', [{ id: 'secret-r1', name: 'first' }, { id: 'secret-r1', name: 'second' }], { mustExist: true })
718
+ assert.strict.equal(r1.length, 2)
719
+ assert.strict.equal(m.rows('a').find((v) => v.id === 'secret-r1').name, 'second')
720
+ })
721
+
722
+ it('M09 事後偵測(表鎖紀律被破壞): 檢查通過後ORM仍回逐筆未命中 → rowNotFound; save看n, del之n與nDeleted任一為0皆算(分別守住)', async function() {
723
+ let { so, m } = mkSo()
724
+ let p = proc(so)
725
+ await p('save', [{ id: 'r1', name: 'A' }])
726
+ let each = (f) => ([data]) => (Array.isArray(data) ? data : [data]).map(f)
727
+ m.plan.save = each(() => ({ n: 0, nInserted: 0, nModified: 0, ok: 1 }))
728
+ let e1 = await quiet(() => getReject(p('save', [{ id: 'r1', name: 'x' }], { mustExist: true })))
729
+ assert.strict.equal(e1, 'rowNotFound')
730
+ //save: n為1(命中但值未變, nModified為0)不算未命中
731
+ m.plan.save = each(() => ({ n: 1, nInserted: 0, nModified: 0, ok: 1 }))
732
+ assert.deepStrictEqual(await p('save', [{ id: 'r1', name: 'x' }], { mustExist: true }), [{ id: 'r1' }])
733
+ //save: ok為0之逐筆失敗仍走既有之失敗字串(不被誤判為rowNotFound)
734
+ m.plan.save = each(() => ({ n: 0, nInserted: 0, nModified: 0, ok: 0, err: 'boom' }))
735
+ assert.strict.equal(await quiet(() => getReject(p('save', [{ id: 'r1', name: 'x' }], { mustExist: true }))), 'failed to save data: id[r1]')
736
+
737
+ //del: n與nDeleted各自為0皆算未命中; 兩者皆為正才算命中; 省略其中一鍵者以另一鍵判定
738
+ for (let [res, expectKey] of [
739
+ [{ n: 0, nDeleted: 0, ok: 1 }, 'rowNotFound'],
740
+ [{ n: 1, nDeleted: 0, ok: 1 }, 'rowNotFound'],
741
+ [{ n: 0, nDeleted: 1, ok: 1 }, 'rowNotFound'],
742
+ [{ nDeleted: 0, ok: 1 }, 'rowNotFound'],
743
+ [{ n: 0, ok: 1 }, 'rowNotFound'],
744
+ [{ n: 1, nDeleted: 1, ok: 1 }, null],
745
+ [{ n: 1, ok: 1 }, null],
746
+ [{ nDeleted: 1, ok: 1 }, null],
747
+ ]) {
748
+ m.plan.del = each(() => res)
749
+ let got = await quiet(() => p('del', [{ id: 'r1' }], { mustExist: true }).then(() => null, (e) => e))
750
+ assert.strict.equal(got, expectKey, JSON.stringify(res))
751
+ }
752
+ //未開啟mustExist時, 同樣之未命中結果不視為錯誤(既有行為)
753
+ m.plan.del = each(() => ({ n: 0, nDeleted: 0, ok: 1 }))
754
+ let r = await p('del', [{ id: 'r1' }])
755
+ assert.deepStrictEqual(r, [{ n: 0, nDeleted: 0, ok: 1 }])
756
+ })
757
+
758
+ it('M10 存在檢查之select失敗: reject固定字串(failed to save data、failed to delete data)且零寫入', async function() {
759
+ let { so, m } = mkSo()
760
+ let p = proc(so)
761
+ await p('save', [{ id: 'r1', name: 'A' }])
762
+ m.reset()
763
+ m.plan.select = () => Promise.reject('db down: secret detail')
764
+ let e1 = await quiet(() => getReject(p('save', [{ id: 'r1', name: 'x' }], { mustExist: true })))
765
+ assert.strict.equal(e1, 'failed to save data')
766
+ let e2 = await quiet(() => getReject(p('del', [{ id: 'r1' }], { mustExist: true })))
767
+ assert.strict.equal(e2, 'failed to delete data')
768
+ assert.strict.equal(m.callsOf('save').length, 0)
769
+ assert.strict.equal(m.callsOf('del').length, 0)
770
+ })
771
+
772
+ it('M11 整數主鍵之有限數字與其字串視為同一(R-ID-02), 存在檢查以字串比對', async function() {
773
+ let ds = { a: { keys: KEYS } }
774
+ let m = createMemOrm()
775
+ let so = WServOrm(ds, m.WOrmMem, './unused', 'db')
776
+ await so.procOrm('u', 'a', 'save', [{ id: '123', name: 'A' }])
777
+ m.reset()
778
+ let r = await so.procOrm('u', 'a', 'save', [{ id: 123, name: 'A2' }], { mustExist: true })
779
+ assert.deepStrictEqual(r, [{ id: '123' }])
780
+ assert.strict.equal(m.rows('a')[0].name, 'A2')
781
+ let r2 = await so.procOrm('u', 'a', 'del', [{ id: 123 }], { mustExist: true })
782
+ assert.strict.equal(r2[0].nDeleted, 1)
783
+ })
784
+
785
+ it('M11b 整數主鍵表(不轉型): 數字主鍵之存在檢查以字串形式比對, 存在者可修改與刪除, 不存在者rowNotFound', async function() {
786
+ let ds = { a: { keys: KEYS, settings: { id: { pk: true, type: 'INTEGER' } } } }
787
+ let m = createMemOrm()
788
+ let so = WServOrm(ds, m.WOrmMem, './unused', 'db')
789
+ await so.procOrm('u', 'a', 'save', [{ id: 1, name: 'A' }, { id: 2, name: 'B' }])
790
+ assert.deepStrictEqual(m.rows('a').map((v) => v.id), [1, 2], '整數主鍵不轉型')
791
+ m.reset()
792
+ let r = await so.procOrm('u', 'a', 'save', [{ id: 1, name: 'A2' }, { id: 2, name: 'B2' }], { mustExist: true })
793
+ assert.deepStrictEqual(r, [{ id: 1 }, { id: 2 }])
794
+ let e1 = await quiet(() => getReject(so.procOrm('u', 'a', 'save', [{ id: 1, name: 'x' }, { id: 3, name: 'x' }], { mustExist: true })))
795
+ assert.strict.equal(e1, 'rowNotFound')
796
+ let e2 = await quiet(() => getReject(so.procOrm('u', 'a', 'save', [{ name: 'no-id' }], { mustExist: true })))
797
+ assert.strict.equal(e2, 'rowNotFound', '整數主鍵表不補號, 無主鍵視為不存在')
798
+ let e3 = await quiet(() => getReject(so.procOrm('u', 'a', 'del', [{ id: 2 }, { id: 9 }], { mustExist: true })))
799
+ assert.strict.equal(e3, 'rowNotFound')
800
+ assert.deepStrictEqual(m.rows('a').map((v) => v.name), ['A2', 'B2'])
801
+ let r2 = await so.procOrm('u', 'a', 'del', [{ id: 2 }], { mustExist: true })
802
+ assert.strict.equal(r2[0].nDeleted, 1)
803
+ })
804
+
805
+ it('M12 opt驗證: 非純物件、未知鍵(含拼錯)、非布林、存取器、symbol鍵 → 固定字串reject, 任何讀寫之前', async function() {
806
+ let { so, m } = mkSo()
807
+ let p = proc(so)
808
+ await p('save', [{ id: 'r1', name: 'A' }])
809
+ m.reset()
810
+ let acc = {}
811
+ Object.defineProperty(acc, 'mustExist', { get: () => true, enumerable: true })
812
+ let sym = { mustExist: true, [Symbol('x')]: 1 }
813
+ let cases = [
814
+ ['x', 'need an object'], [5, 'need an object'], [[], 'need an object'], [true, 'need an object'], [() => 1, 'need an object'], [new Map(), 'need an object'],
815
+ [{ mustexist: true }, 'unsupported key'], [{ mustExist: true, extra: 1 }, 'unsupported key'], [sym, 'unsupported key'], [{ autoInsert: false }, 'unsupported key'],
816
+ [{ mustExist: 'true' }, 'mustExist needs a boolean'], [{ mustExist: 1 }, 'mustExist needs a boolean'], [{ mustExist: null }, 'mustExist needs a boolean'], [{ mustExist: undefined }, 'mustExist needs a boolean'], [acc, 'mustExist needs a boolean'],
817
+ ]
818
+ for (let [opt, tail] of cases) {
819
+ for (let mode of ['save', 'del']) {
820
+ let input = mode === 'save' ? [{ id: 'new-x', name: 'x' }] : [{ id: 'r1' }]
821
+ let err = await quiet(() => getReject(p(mode, input, opt)))
822
+ assert.strict.equal(err, `invalid opt of procOrm, ${tail}`, `${mode} ${String(opt)}`)
823
+ }
824
+ }
825
+ assert.strict.equal(m.calls.length, 0, '任何ORM呼叫之前即拒收')
826
+ assert.strict.equal(m.rows('a').length, 1)
827
+ })
828
+
829
+ it('M12b 無效opt先於擴充函數驗證(外審sol#3): 註冊有副作用之check與前處理時, 無效opt仍固定字串拒收, 擴充函數0次呼叫、ORM 0次呼叫; check拋錯亦不遮蔽固定拒收字串', async function() {
830
+ let { so, m } = mkSo()
831
+ let called = []
832
+ so.addFunCheck(() => {
833
+ called.push('check')
834
+ throw new Error('check would reject first')
835
+ })
836
+ so.addFunPreProcessing(async (x) => {
837
+ called.push('pre')
838
+ await so.woItems.a.save({ id: 'side-effect' })
839
+ return { input: x.input }
840
+ })
841
+ await wait(5)
842
+ m.reset()
843
+ for (let opt of [{ mustExist: 'true' }, { mustexist: true }, 'x', { mustExist: true, extra: 1 }]) {
844
+ let err = await quiet(() => getReject(so.procOrm('u', 'a', 'save', [{ id: 'r1', name: 'A' }], opt)))
845
+ assert.ok(/^invalid opt of procOrm, /.test(err), String(err))
846
+ }
847
+ let errMode = await quiet(() => getReject(so.procOrm('u', 'a', 'select', {}, { mustExist: true })))
848
+ assert.strict.equal(errMode, 'invalid opt of procOrm, mustExist supports only save and del')
849
+ assert.deepStrictEqual(called, [])
850
+ assert.strict.equal(m.calls.length, 0)
851
+ //有效opt時, check照常先執行(確認上面之0次不是擴充函數壞了)
852
+ let err2 = await quiet(() => getReject(so.procOrm('u', 'a', 'save', [{ id: 'r1', name: 'A' }], { mustExist: true })))
853
+ assert.strict.equal(err2.message, 'check would reject first')
854
+ assert.deepStrictEqual(called, ['check'])
855
+ })
856
+
857
+ it('M13 mustExist:true只適用save與del: insert、select、delAll、mix與無效mode皆reject; mustExist:false於各mode為無效果', async function() {
858
+ let { so, m } = mkSo()
859
+ let p = proc(so)
860
+ await p('save', [{ id: 'r1', name: 'A' }])
861
+ m.reset()
862
+ let inputs = { insert: [{ id: 'i1' }], select: {}, delAll: { id: 'r1' }, mix: { add: [{ id: 'i2' }] }, bogus: [] }
863
+ for (let mode of Object.keys(inputs)) {
864
+ let err = await quiet(() => getReject(p(mode, inputs[mode], { mustExist: true })))
865
+ assert.strict.equal(err, 'invalid opt of procOrm, mustExist supports only save and del', mode)
866
+ }
867
+ assert.strict.equal(m.calls.length, 0)
868
+ assert.deepStrictEqual(await p('select', {}, { mustExist: false }).then((r) => r.map((v) => v.id)), ['r1'])
869
+ assert.deepStrictEqual(await p('insert', [{ id: 'i1' }], { mustExist: false }).then((r) => r), [{ id: 'i1' }])
870
+ assert.deepStrictEqual(await p('mix', { add: [{ id: 'i2' }] }, {}).then((r) => r), [{ id: 'i2' }])
871
+ })
872
+
873
+ it('M14 擴充函數(check、前處理、後處理)之參數物件不含opt(形狀不變): 帶mustExist之成功save與del, 三種擴充函數皆被呼叫且鍵集合精確同舊介面', async function() {
874
+ let { so } = mkSo()
875
+ await so.procOrm('u', 'a', 'save', [{ id: 'r1', name: 'A' }, { id: 'r2', name: 'B' }])
876
+ let seen = []
877
+ so.addFunCheck((x) => {
878
+ seen.push(`check:${Object.keys(x).sort().join(',')}`)
879
+ })
880
+ so.addFunPreProcessing((x) => {
881
+ seen.push(`pre:${Object.keys(x).sort().join(',')}`)
882
+ return { input: x.input }
883
+ })
884
+ so.addFunPostProcessing((x) => {
885
+ seen.push(`post:${Object.keys(x).sort().join(',')}`)
886
+ return x.output
887
+ })
888
+ await wait(5) //擴充函數註冊後約1毫秒生效
889
+ let r = await so.procOrm('u', 'a', 'save', [{ id: 'r1', name: 'A2' }], { mustExist: true })
890
+ assert.deepStrictEqual(r, [{ id: 'r1' }])
891
+ let r2 = await so.procOrm('u', 'a', 'del', [{ id: 'r2' }], { mustExist: true })
892
+ assert.strict.equal(r2[0].nDeleted, 1)
893
+ let base = 'input,mode,userId,woName'
894
+ let basePost = 'input,mode,output,userId,woName'
895
+ assert.deepStrictEqual(seen, [`check:${base}`, `pre:${base}`, `post:${basePost}`, `check:${base}`, `pre:${base}`, `post:${basePost}`])
896
+ })
897
+
898
+ it('M14b 前處理改寫後之input仍被存在檢查(改成不存在之id → rowNotFound, 後處理不執行, 零寫入)', async function() {
899
+ let { so, m } = mkSo()
900
+ await so.procOrm('u', 'a', 'save', [{ id: 'r1', name: 'A' }])
901
+ let calledPost = 0
902
+ so.addFunPreProcessing((x) => ({ input: x.input.map((v) => ({ ...v, id: 'ghost-by-hook' })) }))
903
+ so.addFunPostProcessing((x) => {
904
+ calledPost++
905
+ return x.output
906
+ })
907
+ await wait(5)
908
+ m.reset()
909
+ let err = await quiet(() => getReject(so.procOrm('u', 'a', 'save', [{ id: 'r1', name: 'A' }], { mustExist: true })))
910
+ assert.strict.equal(err, 'rowNotFound', '前處理改寫後之id不存在')
911
+ assert.strict.equal(calledPost, 0)
912
+ assert.strict.equal(m.callsOf('save').length, 0)
913
+ })
914
+
915
+ it('M15 既有四參數呼叫之行為鎖定(以固定預期, 非與自身對照): 各mode之回傳形狀、補主鍵、稽核欄位、XSS清除、拒收字串、ORM收到之實參', async function() {
916
+ let { so, m } = mkSo()
917
+ let p = (mode, input) => so.procOrm('u1', 'a', mode, input)
918
+ //save: 補主鍵並回傳, 稽核欄位補值, 字串依clearXSS清除, ORM之save為單參數呼叫
919
+ let r = await p('save', [{ name: 'x<script>alert(1)</script>y' }])
920
+ assert.strict.equal(r.length, 1)
921
+ assert.ok(typeof r[0].id === 'string' && r[0].id.length > 0, '補主鍵')
922
+ let row = m.rows('a')[0]
923
+ assert.strict.equal(row.id, r[0].id)
924
+ assert.strict.equal(row.userIdUpdate, 'u1')
925
+ assert.ok(typeof row.timeUpdate === 'string' && row.timeUpdate.length > 0)
926
+ assert.ok(row.name.indexOf('<script>') < 0, 'XSS已清除')
927
+ assert.strict.equal(m.callsOf('save').length, 1)
928
+ assert.strict.equal(m.callsOf('save')[0].args.length, 1)
929
+ //insert、select、del、mix、delAll 之回傳形狀
930
+ assert.deepStrictEqual(await p('insert', [{ id: 'i1', name: 'I' }]), [{ id: 'i1' }])
931
+ assert.deepStrictEqual((await p('select', { id: { $in: ['i1'] } })).map((v) => v.id), ['i1'])
932
+ assert.deepStrictEqual(await p('mix', { add: [{ id: 'm1' }], diff: [{ id: 'i1', name: 'I2' }], del: [] }), [{ id: 'm1' }, { id: 'i1' }])
933
+ assert.deepStrictEqual(await p('del', [{ id: 'm1' }]), [{ n: 1, nDeleted: 1, ok: 1 }])
934
+ assert.deepStrictEqual(await p('del', [{ id: 'm1' }]), [{ n: 0, nDeleted: 0, ok: 1 }], '未帶opt之del未命中不視為錯誤(既有行為)')
935
+ let nBefore = m.rows('a').length
936
+ let rd = await p('delAll', {})
937
+ assert.deepStrictEqual(rd, { n: nBefore, nDeleted: nBefore, ok: 1 })
938
+ assert.strict.equal(m.rows('a').length, 0)
939
+ //拒收字串(既有)
940
+ assert.strict.equal(await quiet(() => getReject(p('save', 'not-an-array'))), 'failed to save data, not an object: data[0]')
941
+ assert.strict.equal(await quiet(() => getReject(p('nope', []))), 'invalid mode: nope')
942
+ assert.strict.equal(await quiet(() => getReject(so.procOrm('u1', 'zzz', 'select', {}))), 'can not find table[zzz] in ORM')
943
+ //四參數、第5參數為undefined或null: ORM之save皆為單參數呼叫, 無額外select
944
+ for (let args of [[], [undefined], [null]]) {
945
+ m.reset()
946
+ await so.procOrm('u1', 'a', 'save', [{ id: 'k9', name: 'K' }], ...args)
947
+ assert.strict.equal(m.callsOf('save')[0].args.length, 1)
948
+ assert.strict.equal(m.callsOf('select').length, 0)
949
+ }
950
+ })
951
+
952
+ })