w-serv-orm 1.1.16 → 1.1.18

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,35 @@
1
+ import assert from 'assert'
2
+ import isOrmFail from '../src/isOrmFail.mjs'
3
+
4
+
5
+ //R-ORM-RESULT-01(原R-PERITEM-01 規則4, orm-run03、run04): ORM寫入類函數之回傳結果是否回報失敗之判準; procOrm各寫入模式與mix各步驟、備份還原、測試數據寫入共用
6
+ describe('isOrmFail', function() {
7
+
8
+ it('V01 聚合物件ok為0為失敗', function() {
9
+ //規則: 聚合物件ok恆等於0(違約之ORM或他種後端)
10
+ for (let r of [{ n: 2, nInserted: 0, ok: 0 }, { n: 2, nInserted: 1, ok: 0, err: 'E' }, { n: 0, nDeleted: 0, ok: 0 }, { ok: 0 }]) {
11
+ assert.strict.equal(isOrmFail(r), true, JSON.stringify(r))
12
+ }
13
+ })
14
+
15
+ it('V02 逐筆陣列任一筆ok為0為失敗', function() {
16
+ //規則(run04): 同getItemsFail之判讀(僅ok恆等於0者)
17
+ for (let r of [[{ ok: 0 }], [{ ok: 1 }, { ok: 0 }], [{ ok: 1 }, null, { ok: 0, err: 'E' }], [1, 'x', { ok: 0 }]]) {
18
+ assert.strict.equal(isOrmFail(r), true, JSON.stringify(r))
19
+ }
20
+ })
21
+
22
+ it('V03 其餘回傳形狀不是失敗(維持舊行為)', function() {
23
+ //規則: 略過既有主鍵或未命中(n為0、ok為1)為正常結果; ok非恆等於0(含false、'0'、null、undefined)與非物件、非陣列者不判
24
+ let cases = [
25
+ { n: 2, nInserted: 2, ok: 1 }, { n: 0, nInserted: 0, ok: 1 }, { n: 0, nDeleted: 0, ok: 1 }, {}, { n: 1 },
26
+ { ok: false }, { ok: '0' }, { ok: null }, { ok: undefined },
27
+ [], [{ ok: 1 }], [{ n: 1 }], [null, 1, 'x', {}], [{ ok: false }, { ok: '0' }],
28
+ undefined, null, 0, 1, '', 'x', true, false,
29
+ ]
30
+ for (let r of cases) {
31
+ assert.strict.equal(isOrmFail(r), false, JSON.stringify(r) + ' ' + typeof r)
32
+ }
33
+ })
34
+
35
+ })
@@ -0,0 +1,307 @@
1
+ import assert from 'assert'
2
+ import WServOrm from '../src/WServOrm.mjs'
3
+ import getMapOrm from '../src/getMapOrm.mjs'
4
+ import { createMemOrm } from './tools/memOrm.mjs'
5
+ import { getReject, quiet } from './tools/ormStub.mjs'
6
+
7
+
8
+ //規格: w-web-sso 需求 T2①(任務 orm-run02): sso之tokens.userId是權杖擁有者而非建立者, 依擁有者分組以擁有者當procOrm之第1參數寫入會使userIdUpdate亦被記成擁有者;
9
+ //須能於insert保留列上之userId(擁有者), 同時userIdUpdate記為操作者(procOrm之第1參數); 規則帳本 R-KEEPUSER-01(rulebook_concurrency.md)
10
+ //以有狀態之記憶體ORM(test/tools/memOrm.mjs)驗證; 真後端(lowdb)見 api-keepuser.test.mjs
11
+ //
12
+ //規格句對照(spec = 派工提示詞「本輪範圍」與〈完成條件〉1):
13
+ // K01-K03 「keepUserId:true 時保留擁有者、userIdUpdate 為操作者」「userIdUpdate、timeCreate、timeUpdate 照現行」
14
+ // K02、K05 「列上userId為非空字串者保留, 其餘照現行以操作者補」
15
+ // K04 「未給或 false 時行為與現行完全相同」
16
+ // K06-K07 實作者補強(表無userId欄、clearXSS照常)
17
+ // K08-K11 「與mustExist相同之驗證時點與方式: 未知鍵、非布林、用於不適用之mode → 於任何讀寫與擴充函數之前以固定字串reject」「無效opt於寫入前拒收」
18
+ // K12-K13 實作者補強(擴充函數參數形狀不變、前處理改寫後之input為保留判定之依據)
19
+ // K14 「若save時現行會覆寫userId, 一併評估」→ save現行不覆寫userId(只補userIdUpdate、timeUpdate), 故不適用而拒收
20
+
21
+ let wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
22
+
23
+ let KEYS = ['id', 'name', 'userId', 'userIdUpdate', 'timeCreate', 'timeUpdate']
24
+
25
+ function mkSo(keys = KEYS) {
26
+ let m = createMemOrm()
27
+ let so = WServOrm({ a: { keys } }, m.WOrmMem, './unused', 'db')
28
+ return { so, m }
29
+ }
30
+
31
+ let RE_TIME = /^\d{4}-\d{2}-\d{2}/
32
+
33
+ let MSG_BOOL = 'invalid opt of procOrm, keepUserId needs a boolean'
34
+ let MSG_MODE = 'invalid opt of procOrm, keepUserId supports only insert'
35
+
36
+
37
+ describe('procOrm opt: keepUserId 保留擁有者userId(T2①)', function() {
38
+
39
+ let ins = (so) => (input, opt) => so.procOrm('op', 'a', 'insert', input, opt)
40
+
41
+ it('K01 keepUserId:true: 列上userId(擁有者)保留, userIdUpdate為操作者, timeCreate與timeUpdate照現行補值; 單物件與陣列皆然', async function() {
42
+ let { so, m } = mkSo()
43
+ let p = ins(so)
44
+ let r = await p([{ id: 'r1', name: 'x', userId: 'owner' }], { keepUserId: true })
45
+ assert.deepStrictEqual(r, [{ id: 'r1' }])
46
+ let row = m.rows('a')[0]
47
+ assert.strict.equal(row.userId, 'owner')
48
+ assert.strict.equal(row.userIdUpdate, 'op')
49
+ assert.strict.equal(row.name, 'x')
50
+ assert.ok(RE_TIME.test(row.timeCreate), row.timeCreate)
51
+ assert.strict.equal(row.timeUpdate, row.timeCreate)
52
+
53
+ let r2 = await p({ id: 'r2', name: 'y', userId: 'owner2' }, { keepUserId: true })
54
+ assert.deepStrictEqual(r2, { id: 'r2' })
55
+ let row2 = m.rows('a')[1]
56
+ assert.strict.equal(row2.userId, 'owner2')
57
+ assert.strict.equal(row2.userIdUpdate, 'op')
58
+ assert.ok(RE_TIME.test(row2.timeCreate) && RE_TIME.test(row2.timeUpdate))
59
+
60
+ //ORM之insert實際收到者即上述(非事後改寫)
61
+ let sent = m.callsOf('insert')[0].args[0][0]
62
+ assert.strict.equal(sent.userId, 'owner')
63
+ assert.strict.equal(sent.userIdUpdate, 'op')
64
+ })
65
+
66
+ it('K02 逐列判定: 非空字串保留, 空字串、缺欄、null、undefined、數字、物件、布林皆以操作者補; 同批各列擁有者不同者各自保留', async function() {
67
+ let { so, m } = mkSo()
68
+ let p = ins(so)
69
+ let rows = [
70
+ { id: 'a1', userId: 'o1' },
71
+ { id: 'a2' },
72
+ { id: 'a3', userId: '' },
73
+ { id: 'a4', userId: null },
74
+ { id: 'a5', userId: undefined },
75
+ { id: 'a6', userId: 5 },
76
+ { id: 'a7', userId: { x: 1 } },
77
+ { id: 'a8', userId: 'o8' },
78
+ { id: 'a9', userId: true },
79
+ ]
80
+ await p(rows, { keepUserId: true })
81
+ let got = Object.fromEntries(m.rows('a').map((v) => [v.id, v.userId]))
82
+ assert.deepStrictEqual(got, { a1: 'o1', a2: 'op', a3: 'op', a4: 'op', a5: 'op', a6: 'op', a7: 'op', a8: 'o8', a9: 'op' })
83
+ assert.strict.equal(m.rows('a').every((v) => v.userIdUpdate === 'op'), true)
84
+ assert.strict.equal(m.rows('a').every((v) => RE_TIME.test(v.timeCreate) && RE_TIME.test(v.timeUpdate)), true)
85
+ })
86
+
87
+ it('K03 呼叫端給之userIdUpdate、timeCreate、timeUpdate仍被覆寫(照現行, keepUserId只影響userId)', async function() {
88
+ let { so, m } = mkSo()
89
+ await ins(so)([{ id: 'r1', userId: 'owner', userIdUpdate: 'forged', timeCreate: '1999-01-01', timeUpdate: '1999-01-02' }], { keepUserId: true })
90
+ let row = m.rows('a')[0]
91
+ assert.strict.equal(row.userId, 'owner')
92
+ assert.strict.equal(row.userIdUpdate, 'op')
93
+ assert.notStrictEqual(row.timeCreate, '1999-01-01')
94
+ assert.notStrictEqual(row.timeUpdate, '1999-01-02')
95
+ assert.ok(RE_TIME.test(row.timeCreate) && row.timeCreate.slice(0, 4) >= '2026')
96
+ })
97
+
98
+ //K04、K04b共用: 預期為固定值(非與自身對照)
99
+ async function checkSameAsCurrent(args) {
100
+ let { so, m } = mkSo()
101
+ await so.procOrm('op', 'a', 'insert', [{ id: 'r1', name: 'x', userId: 'owner' }, { id: 'r2' }], ...args)
102
+ let rows = m.rows('a')
103
+ assert.deepStrictEqual(rows.map((v) => [v.id, v.userId, v.userIdUpdate]), [['r1', 'op', 'op'], ['r2', 'op', 'op']], JSON.stringify(args))
104
+ let sent = m.callsOf('insert')[0].args[0]
105
+ assert.deepStrictEqual(sent.map((v) => v.userId), ['op', 'op'])
106
+ assert.strict.equal(m.callsOf('insert').length, 1)
107
+ assert.strict.equal(m.callsOf('select').length, 0)
108
+ }
109
+
110
+ it('K04 未給opt、opt為undefined或null、{}、{mustExist:false}: 行為與現行完全相同(列上userId被操作者覆寫), ORM收到之列同(修前(HEAD)即綠, 為既有行為不變之獨立證據)', async function() {
111
+ for (let args of [[], [undefined], [null], [{}], [{ mustExist: false }]]) {
112
+ await checkSameAsCurrent(args)
113
+ }
114
+ })
115
+
116
+ it('K04b {keepUserId:false}(單獨或與mustExist:false併用): 行為與現行完全相同(列上userId被操作者覆寫)', async function() {
117
+ for (let args of [[{ keepUserId: false }], [{ keepUserId: false, mustExist: false }]]) {
118
+ await checkSameAsCurrent(args)
119
+ }
120
+ })
121
+
122
+ it('K05 繼承或不可列舉之userId視為未提供(與主鍵只認自有可列舉資料屬性同), 以操作者補; 自有可列舉者保留', async function() {
123
+ let { so, m } = mkSo()
124
+ let inh = Object.create({ userId: 'proto-owner' })
125
+ inh.id = 'i1'
126
+ let hid = { id: 'i2' }
127
+ Object.defineProperty(hid, 'userId', { value: 'hidden-owner', enumerable: false, configurable: true, writable: true })
128
+ let own = { id: 'i3', userId: 'own-owner' }
129
+ await ins(so)([inh, hid, own], { keepUserId: true })
130
+ let got = Object.fromEntries(m.rows('a').map((v) => [v.id, v.userId]))
131
+ assert.deepStrictEqual(got, { i1: 'op', i2: 'op', i3: 'own-owner' })
132
+ })
133
+
134
+ it('K06 表無userId欄: keepUserId:true無效果(不補userId亦不刪除列上之userId, 同未帶opt); userIdUpdate仍為操作者', async function() {
135
+ let { so, m } = mkSo(['id', 'name', 'userIdUpdate', 'timeUpdate'])
136
+ await ins(so)([{ id: 'r1', userId: 'x' }], { keepUserId: true })
137
+ await ins(so)([{ id: 'r2', userId: 'x' }])
138
+ let rows = m.rows('a')
139
+ assert.deepStrictEqual(rows.map((v) => [v.id, v.userId, v.userIdUpdate]), [['r1', 'x', 'op'], ['r2', 'x', 'op']])
140
+ })
141
+
142
+ it('K07 保留之userId仍經clearXSS(與其他字串欄位同); 單純空白字串屬非空字串而保留', async function() {
143
+ let { so, m } = mkSo()
144
+ await ins(so)([{ id: 'r1', userId: 'o<script>alert(1)</script>wner' }, { id: 'r2', userId: ' ' }], { keepUserId: true })
145
+ let rows = m.rows('a')
146
+ assert.ok(rows[0].userId.indexOf('<script>') < 0, rows[0].userId)
147
+ assert.ok(rows[0].userId !== 'op' && rows[0].userId.indexOf('wner') >= 0, rows[0].userId)
148
+ assert.strict.equal(rows[1].userId, ' ')
149
+ })
150
+
151
+ it('K08 keepUserId非布林(字串、數字、null、undefined值、陣列、物件、函數、存取器) → 固定字串reject, ORM 0次呼叫', async function() {
152
+ let { so, m } = mkSo()
153
+ let acc = {}
154
+ Object.defineProperty(acc, 'keepUserId', { get: () => true, enumerable: true })
155
+ let accNoVal = {}
156
+ Object.defineProperty(accNoVal, 'keepUserId', { get: undefined, set: undefined, enumerable: true })
157
+ let cases = [{ keepUserId: 'true' }, { keepUserId: 1 }, { keepUserId: 0 }, { keepUserId: null }, { keepUserId: undefined }, { keepUserId: [] }, { keepUserId: {} }, { keepUserId: () => true }, acc, accNoVal]
158
+ for (let opt of cases) {
159
+ let err = await quiet(() => getReject(ins(so)([{ id: 'n1', userId: 'o' }], opt)))
160
+ assert.strict.equal(err, MSG_BOOL, String(JSON.stringify(opt)))
161
+ }
162
+ assert.strict.equal(m.calls.length, 0)
163
+ assert.strict.equal(m.tables.a === undefined || m.tables.a.size === 0, true)
164
+ })
165
+
166
+ it('K09 keepUserId:true只適用insert: save、del、select、delAll、mix與無效mode皆固定字串reject且ORM 0次呼叫、庫內不變; keepUserId:false於各mode無效果', async function() {
167
+ let { so, m } = mkSo()
168
+ await so.procOrm('op', 'a', 'save', [{ id: 'r1', name: 'A', userId: 'o' }])
169
+ let before = JSON.stringify(m.rows('a'))
170
+ m.reset()
171
+ let inputs = { save: [{ id: 'r1', name: 'B', userId: 'o' }], del: [{ id: 'r1' }], select: {}, delAll: {}, mix: { add: [{ id: 'm1' }] }, bogus: [] }
172
+ for (let mode of Object.keys(inputs)) {
173
+ let err = await quiet(() => getReject(so.procOrm('op', 'a', mode, inputs[mode], { keepUserId: true })))
174
+ assert.strict.equal(err, MSG_MODE, mode)
175
+ }
176
+ assert.strict.equal(m.calls.length, 0)
177
+ assert.strict.equal(JSON.stringify(m.rows('a')), before)
178
+
179
+ //keepUserId:false於各mode無效果(沿用既有各mode之行為與回傳)
180
+ let f = { keepUserId: false }
181
+ assert.deepStrictEqual((await so.procOrm('op', 'a', 'select', {}, f)).map((v) => v.id), ['r1'])
182
+ assert.deepStrictEqual(await so.procOrm('op', 'a', 'save', [{ id: 'r1', name: 'B', userId: 'o' }], f), [{ id: 'r1' }])
183
+ assert.deepStrictEqual(await so.procOrm('op', 'a', 'mix', { add: [{ id: 'm1' }] }, f), [{ id: 'm1' }])
184
+ assert.deepStrictEqual(await so.procOrm('op', 'a', 'del', [{ id: 'm1' }], f), [{ n: 1, nDeleted: 1, ok: 1 }])
185
+ assert.strict.equal(await quiet(() => getReject(so.procOrm('op', 'a', 'bogus', [], f))), 'invalid mode: bogus')
186
+ assert.deepStrictEqual(await so.procOrm('op', 'a', 'delAll', {}, f), { n: 1, nDeleted: 1, ok: 1 })
187
+ })
188
+
189
+ it('K10 與mustExist及其他鍵之組合: 型別先於mode檢查; 未知鍵(含拼錯、symbol鍵)reject; keepUserId:true加mustExist:false於insert可用', async function() {
190
+ let { so, m } = mkSo()
191
+ await so.procOrm('op', 'a', 'save', [{ id: 'r1', name: 'A' }])
192
+ m.reset()
193
+ let sym = { keepUserId: true, [Symbol('x')]: 1 }
194
+ let cases = [
195
+ ['insert', { mustExist: true, keepUserId: true }, 'invalid opt of procOrm, mustExist supports only save and del'],
196
+ ['save', { mustExist: true, keepUserId: true }, MSG_MODE],
197
+ ['save', { mustExist: true, keepUserId: 'x' }, MSG_BOOL],
198
+ ['insert', { mustExist: 'x', keepUserId: true }, 'invalid opt of procOrm, mustExist needs a boolean'],
199
+ ['insert', { keepUserId: true, extra: 1 }, 'invalid opt of procOrm, unsupported key'],
200
+ ['insert', { keepuserid: true }, 'invalid opt of procOrm, unsupported key'],
201
+ ['insert', { keepUserID: true }, 'invalid opt of procOrm, unsupported key'],
202
+ ['insert', sym, 'invalid opt of procOrm, unsupported key'],
203
+ ['insert', 'keepUserId', 'invalid opt of procOrm, need an object'],
204
+ ['insert', [], 'invalid opt of procOrm, need an object'],
205
+ ]
206
+ for (let [mode, opt, msg] of cases) {
207
+ let input = mode === 'insert' ? [{ id: 'n1', userId: 'o' }] : [{ id: 'r1', name: 'Z' }]
208
+ let err = await quiet(() => getReject(so.procOrm('op', 'a', mode, input, opt)))
209
+ assert.strict.equal(err, msg, `${mode} ${Object.keys(opt).map(String).join(',')}`)
210
+ }
211
+ assert.strict.equal(m.calls.length, 0)
212
+ let r = await so.procOrm('op', 'a', 'insert', [{ id: 'n2', userId: 'o2' }], { keepUserId: true, mustExist: false })
213
+ assert.deepStrictEqual(r, [{ id: 'n2' }])
214
+ assert.strict.equal(m.rows('a').find((v) => v.id === 'n2').userId, 'o2')
215
+ //mustExist既有行為不受影響: save + mustExist:true仍只改不增
216
+ let err = await quiet(() => getReject(so.procOrm('op', 'a', 'save', [{ id: 'ghost' }], { mustExist: true })))
217
+ assert.strict.equal(err, 'rowNotFound')
218
+ })
219
+
220
+ it('K11 直接使用getMapOrm(不經getProc)亦同: 保留、預設覆寫、無效opt於ORM呼叫之前reject', async function() {
221
+ let m = createMemOrm()
222
+ let ds = { a: { keys: KEYS } }
223
+ let mapOrm = getMapOrm(ds, { a: m.WOrmMem({ cl: 'a' }) })
224
+ await mapOrm('op', 'a', 'insert', [{ id: 'r1', userId: 'o1' }, { id: 'r2' }], { keepUserId: true })
225
+ await mapOrm('op', 'a', 'insert', [{ id: 'r3', userId: 'o3' }])
226
+ assert.deepStrictEqual(m.rows('a').map((v) => [v.id, v.userId, v.userIdUpdate]), [['r1', 'o1', 'op'], ['r2', 'op', 'op'], ['r3', 'op', 'op']])
227
+ m.reset()
228
+ assert.strict.equal(await quiet(() => getReject(mapOrm('op', 'a', 'insert', [{ id: 'x' }], { keepUserId: 1 }))), MSG_BOOL)
229
+ assert.strict.equal(await quiet(() => getReject(mapOrm('op', 'a', 'save', [{ id: 'r1' }], { keepUserId: true }))), MSG_MODE)
230
+ assert.strict.equal(m.calls.length, 0)
231
+ })
232
+
233
+ it('K12 無效opt先於擴充函數驗證: 註冊有副作用之check與前處理時, keepUserId非布林與用於非insert皆固定字串拒收, 擴充函數0次呼叫、ORM 0次呼叫; 有效opt時check照常先執行', async function() {
234
+ let { so, m } = mkSo()
235
+ let called = []
236
+ so.addFunCheck(() => {
237
+ called.push('check')
238
+ throw new Error('check would reject first')
239
+ })
240
+ so.addFunPreProcessing(async (x) => {
241
+ called.push('pre')
242
+ await so.woItems.a.save({ id: 'side-effect' })
243
+ return { input: x.input }
244
+ })
245
+ await wait(5)
246
+ m.reset()
247
+ for (let opt of [{ keepUserId: 'true' }, { keepUserId: 1 }, { keepuserid: true }]) {
248
+ let err = await quiet(() => getReject(so.procOrm('op', 'a', 'insert', [{ id: 'r1' }], opt)))
249
+ assert.ok(/^invalid opt of procOrm, /.test(err), String(err))
250
+ }
251
+ for (let mode of ['save', 'del', 'select', 'delAll', 'mix']) {
252
+ let err = await quiet(() => getReject(so.procOrm('op', 'a', mode, mode === 'mix' ? { add: [] } : [], { keepUserId: true })))
253
+ assert.strict.equal(err, MSG_MODE, mode)
254
+ }
255
+ assert.deepStrictEqual(called, [])
256
+ assert.strict.equal(m.calls.length, 0)
257
+ let err2 = await quiet(() => getReject(so.procOrm('op', 'a', 'insert', [{ id: 'r1' }], { keepUserId: true })))
258
+ assert.strict.equal(err2.message, 'check would reject first')
259
+ assert.deepStrictEqual(called, ['check'])
260
+ })
261
+
262
+ it('K13 擴充函數(check、前處理、後處理)之參數物件不含opt(鍵集合同舊介面); 前處理改寫後之input為保留判定之依據', async function() {
263
+ let { so, m } = mkSo()
264
+ let seen = []
265
+ so.addFunCheck((x) => {
266
+ seen.push(`check:${Object.keys(x).sort().join(',')}`)
267
+ })
268
+ so.addFunPreProcessing((x) => {
269
+ seen.push(`pre:${Object.keys(x).sort().join(',')}`)
270
+ return { input: x.input.map((v) => ({ ...v, userId: 'hook-owner' })) }
271
+ })
272
+ so.addFunPostProcessing((x) => {
273
+ seen.push(`post:${Object.keys(x).sort().join(',')}`)
274
+ return x.output
275
+ })
276
+ await wait(5)
277
+ let r = await so.procOrm('op', 'a', 'insert', [{ id: 'r1' }], { keepUserId: true })
278
+ assert.deepStrictEqual(r, [{ id: 'r1' }])
279
+ let base = 'input,mode,userId,woName'
280
+ assert.deepStrictEqual(seen, [`check:${base}`, `pre:${base}`, `post:input,mode,output,userId,woName`])
281
+ let row = m.rows('a')[0]
282
+ assert.strict.equal(row.userId, 'hook-owner', '前處理補之userId為進入ORM前之列上值, 被保留')
283
+ assert.strict.equal(row.userIdUpdate, 'op')
284
+ //未帶opt時前處理補之userId同樣被操作者覆寫(現行)
285
+ await so.procOrm('op', 'a', 'insert', [{ id: 'r2' }])
286
+ assert.strict.equal(m.rows('a')[1].userId, 'op')
287
+ })
288
+
289
+ it('K14 save現行不覆寫userId(只補userIdUpdate、timeUpdate): 列上userId原樣寫入, 故keepUserId不適用save(K09拒收); 本案鎖定save之現行行為', async function() {
290
+ let { so, m } = mkSo()
291
+ await so.procOrm('creator', 'a', 'insert', [{ id: 'r1', name: 'A', userId: 'owner' }], { keepUserId: true })
292
+ assert.strict.equal(m.rows('a')[0].userId, 'owner')
293
+ await so.procOrm('editor', 'a', 'save', [{ id: 'r1', name: 'B', userId: 'owner' }])
294
+ let row = m.rows('a')[0]
295
+ assert.strict.equal(row.userId, 'owner')
296
+ assert.strict.equal(row.userIdUpdate, 'editor')
297
+ assert.strict.equal(row.name, 'B')
298
+ //save之列未帶userId: 不補操作者(現行), 庫內以ORM之save語意合併(memOrm為合併)而保留擁有者
299
+ await so.procOrm('editor2', 'a', 'save', [{ id: 'r1', name: 'C' }])
300
+ let row2 = m.rows('a')[0]
301
+ assert.strict.equal(row2.userId, 'owner')
302
+ assert.strict.equal(row2.userIdUpdate, 'editor2')
303
+ let sent = m.callsOf('save').map((c) => c.args[0][0])
304
+ assert.strict.equal('userId' in sent[1], false, 'save未對未帶userId之列補userId')
305
+ })
306
+
307
+ })
@@ -146,6 +146,80 @@ describe('mapOrm逐筆失敗', function() {
146
146
  assert.strict.deepEqual(r, { n: 1, nDeleted: 1, ok: 1 })
147
147
  })
148
148
 
149
+ it('U03b save之ORM回傳聚合物件ok為0(非契約形狀)', async function() {
150
+ //R-ORM-RESULT-01(orm-run05): 契約為逐筆陣列; 回傳聚合物件而ok為0者無從逐筆列出, 仍視為失敗, 固定字串reject(陣列與單一物件輸入皆是)
151
+ plan.save = () => ({ n: 2, nInserted: 0, nModified: 0, ok: 0, err: 'E-agg' })
152
+ let err = await getReject(proc('save', [{ id: 's1' }, { id: 's2' }]))
153
+ assert.strict.equal(err, 'failed to save data')
154
+ err = await getReject(proc('save', { id: 's3' }))
155
+ assert.strict.equal(err, 'failed to save data')
156
+ })
157
+
158
+ it('U03c save之ORM回傳非陣列且無失敗者照舊', async function() {
159
+ //R-ORM-RESULT-01: undefined、null、無ok鍵之物件、ok為1之聚合物件不判失敗(維持舊行為, 同U07)
160
+ for (let rSave of [undefined, null, {}, { n: 1 }, { n: 1, ok: 1 }]) {
161
+ plan.save = () => rSave
162
+ let r = await proc('save', [{ id: 's1' }])
163
+ assert.strict.deepEqual(r, [{ id: 's1' }], JSON.stringify(rSave))
164
+ }
165
+ })
166
+
167
+ it('U10c del之ORM回傳聚合物件ok為0(非契約形狀)', async function() {
168
+ //R-ORM-RESULT-01(orm-run05): 同U03b; 陣列與單一物件輸入皆是
169
+ plan.del = () => ({ n: 1, nDeleted: 0, ok: 0, err: 'E-agg' })
170
+ let err = await getReject(proc('del', [{ id: 'd1' }, { id: 'd2' }]))
171
+ assert.strict.equal(err, 'failed to delete data')
172
+ err = await getReject(proc('del', { id: 'd3' }))
173
+ assert.strict.equal(err, 'failed to delete data')
174
+ })
175
+
176
+ it('U12b del之ORM回傳非陣列且無失敗者照舊回傳ORM原值', async function() {
177
+ //R-ORM-RESULT-01: undefined、null、無ok鍵之物件、ok為1之聚合物件不判失敗(同U12)
178
+ for (let rDel of [undefined, null, {}, { n: 1 }, { n: 1, nDeleted: 1, ok: 1 }]) {
179
+ plan.del = () => rDel
180
+ let r = await proc('del', [{ id: 'd1' }])
181
+ assert.strict.deepEqual(r, rDel, JSON.stringify(rDel))
182
+ }
183
+ })
184
+
185
+ it('U24 delAll之ORM回報失敗(聚合物件ok為0, 或陣列形任一筆ok為0)', async function() {
186
+ //R-ORM-RESULT-01(orm-run05): 契約為聚合物件ok恆為1(錯誤皆整批reject); ok為0者視為失敗, 固定字串reject, 不回成功; 無條件與有條件皆是
187
+ for (let rAll of [{ n: 0, nDeleted: 0, ok: 0, err: 'E-agg' }, [{ n: 0, nDeleted: 0, ok: 1 }, { ok: 0 }]]) {
188
+ plan.delAll = () => rAll
189
+ let err = await getReject(proc('delAll', {}))
190
+ assert.strict.equal(err, 'failed to delete data', JSON.stringify(rAll))
191
+ err = await getReject(proc('delAll', { id: 'd1' }))
192
+ assert.strict.equal(err, 'failed to delete data', JSON.stringify(rAll))
193
+ }
194
+ })
195
+
196
+ it('U24b delAll之ORM回傳無失敗者照舊回傳ORM原值', async function() {
197
+ //R-ORM-RESULT-01: 條件無命中(n為0、ok為1)為正常結果; undefined、null、無ok鍵之物件、空陣列不判失敗
198
+ for (let rAll of [{ n: 0, nDeleted: 0, ok: 1 }, { n: 3, nDeleted: 3, ok: 1 }, undefined, null, {}, [], [{ ok: 1 }]]) {
199
+ plan.delAll = () => rAll
200
+ let r = await proc('delAll', { id: 'd1' })
201
+ assert.strict.deepEqual(r, rAll, JSON.stringify(rAll))
202
+ }
203
+ })
204
+
205
+ it('U24c delAll之ORM整批reject', async function() {
206
+ //R-PERITEM-01 規則1之既有處置不變: ORM reject轉固定字串
207
+ plan.delAll = () => Promise.reject(new Error('E-batch'))
208
+ let err = await getReject(proc('delAll', {}))
209
+ assert.strict.equal(err, 'failed to delete data')
210
+ })
211
+
212
+ it('U24d mustExist之save與del遇聚合物件ok為0時以一般失敗字串reject, 不誤報rowNotFound', async function() {
213
+ //R-ORM-RESULT-01: ok為0優先於mustExist之事後偵測(hasNoHit只認ok非0者)
214
+ plan.select = () => [{ id: 's1' }]
215
+ plan.save = () => ({ n: 0, nInserted: 0, nModified: 0, ok: 0, err: 'E-agg' })
216
+ plan.del = () => ({ n: 0, nDeleted: 0, ok: 0, err: 'E-agg' })
217
+ let err = await getReject(so.procOrm('u1', 'tests', 'save', [{ id: 's1' }], { mustExist: true }))
218
+ assert.strict.equal(err, 'failed to save data')
219
+ err = await getReject(so.procOrm('u1', 'tests', 'del', [{ id: 's1' }], { mustExist: true }))
220
+ assert.strict.equal(err, 'failed to delete data')
221
+ })
222
+
149
223
  it('U13 mix全部成功', async function() {
150
224
  //R04: mix依add、del、diff順序執行, 回傳累積之id
151
225
  plan.insert = (data) => ({ n: data.length, nInserted: data.length, ok: 1 })
@@ -166,6 +240,37 @@ describe('mapOrm逐筆失敗', function() {
166
240
  assert.strict.deepEqual(calls.map((v) => v.fn), ['insert', 'del', 'save'])
167
241
  })
168
242
 
243
+ it('U14b mix之del回傳聚合物件ok為0(非契約形狀)', async function() {
244
+ //R-ORM-RESULT-01(orm-run05): 無從逐筆列出, 視為該步驟整批性失敗, 固定字串立即reject, diff步驟不執行(同add; 先前之逐筆失敗僅由log輸出, 同U18)
245
+ plan.insert = (data) => ({ n: data.length, nInserted: data.length, ok: 1 })
246
+ plan.del = () => ({ n: 1, nDeleted: 0, ok: 0, err: 'E-agg' })
247
+ plan.save = (data) => data.map(() => okSave)
248
+ let err = await getReject(proc('mix', { add: [{ id: 'a1' }], del: [{ id: 'd1' }], diff: [{ id: 'f1' }] }))
249
+ assert.strict.equal(err, 'failed to delete data')
250
+ assert.strict.deepEqual(calls.map((v) => v.fn), ['insert', 'del'])
251
+ })
252
+
253
+ it('U14c mix之diff回傳聚合物件ok為0(非契約形狀)', async function() {
254
+ //R-ORM-RESULT-01(orm-run05): 同U14b; 先前del之逐筆失敗不影響以diff之固定字串reject
255
+ plan.del = () => [{ n: 0, nDeleted: 0, ok: 0, err: 'E-del' }]
256
+ plan.save = () => ({ n: 1, nInserted: 0, nModified: 0, ok: 0, err: 'E-agg' })
257
+ let err = await getReject(proc('mix', { del: [{ id: 'd1' }], diff: [{ id: 'f1' }] }))
258
+ assert.strict.equal(err, 'failed to update data')
259
+ assert.strict.deepEqual(calls.map((v) => v.fn), ['del', 'save'])
260
+ })
261
+
262
+ it('U14d mix之del與diff回傳非陣列且無失敗者照舊', async function() {
263
+ //R-ORM-RESULT-01: undefined、null、無ok鍵之物件、ok為1之聚合物件不判失敗
264
+ for (let rStep of [undefined, null, {}, { n: 1 }, { n: 1, ok: 1 }]) {
265
+ calls = []
266
+ plan.del = () => rStep
267
+ plan.save = () => rStep
268
+ let r = await proc('mix', { del: [{ id: 'd1' }], diff: [{ id: 'f1' }] })
269
+ assert.strict.deepEqual(r, [{ id: 'd1' }, { id: 'f1' }], JSON.stringify(rStep))
270
+ assert.strict.deepEqual(calls.map((v) => v.fn), ['del', 'save'], JSON.stringify(rStep))
271
+ }
272
+ })
273
+
169
274
  it('U15 mix僅del有ok為0', async function() {
170
275
  //R03: del之逐筆失敗不中斷diff, diff仍寫入, 最後reject僅列del之失敗
171
276
  plan.del = () => [{ n: 0, nDeleted: 0, ok: 0, err: 'E-del' }]
@@ -201,6 +306,41 @@ describe('mapOrm逐筆失敗', function() {
201
306
  assert.strict.equal(err, 'failed to update data')
202
307
  })
203
308
 
309
+ it('U17b mix之add聚合結果ok為0', async function() {
310
+ //R-PERITEM-01 規則4(orm-run03): insert契約上ok恆為1, 聚合結果ok為0(ORM違約)視為整批性失敗, 立即reject, 後續步驟不執行
311
+ plan.insert = (data) => ({ n: data.length, nInserted: 0, ok: 0, err: 'E-ins' })
312
+ plan.del = (data) => data.map(() => okDel)
313
+ plan.save = (data) => data.map(() => okSave)
314
+ let err = await getReject(proc('mix', { add: [{ id: 'a1' }], del: [{ id: 'd1' }], diff: [{ id: 'f1' }] }))
315
+ assert.strict.equal(err, 'failed to insert data')
316
+ assert.strict.deepEqual(calls.map((v) => v.fn), ['insert'])
317
+ })
318
+
319
+ it('U17c mix之add略過既有主鍵或ORM回傳無失敗者照舊', async function() {
320
+ //R-PERITEM-01 規則4: 略過(n為0、ok為1)屬正常結果; 回傳undefined、null、無ok鍵之物件、空陣列、元素皆ok非0或非物件之陣列不判失敗
321
+ for (let rIns of [{ n: 0, nInserted: 0, ok: 1 }, undefined, null, [], [{ n: 1, nInserted: 1, ok: 1 }, { n: 0, nInserted: 0, ok: 1 }], [null, 1, 'x', {}], {}, { n: 1 }]) {
322
+ calls = []
323
+ plan.insert = () => rIns
324
+ plan.del = (data) => data.map(() => okDel)
325
+ let r = await proc('mix', { add: [{ id: 'a1' }], del: [{ id: 'd1' }] })
326
+ assert.strict.deepEqual(r, [{ id: 'a1' }, { id: 'd1' }], JSON.stringify(rIns))
327
+ assert.strict.deepEqual(calls.map((v) => v.fn), ['insert', 'del'], JSON.stringify(rIns))
328
+ }
329
+ })
330
+
331
+ it('U17d mix之add陣列形逐筆ok為0', async function() {
332
+ //R-PERITEM-01 規則4(orm-run04): insert回傳陣列形者(非契約形狀)亦逐筆判讀, 任一筆ok為0即同聚合形處置: 固定字串立即reject, 後續步驟不執行
333
+ for (let rIns of [[{ n: 1, nInserted: 0, ok: 0, err: 'E-ins' }], [{ n: 1, nInserted: 1, ok: 1 }, { n: 1, nInserted: 0, ok: 0 }], [{ ok: 1 }, null, { ok: 0 }]]) {
334
+ calls = []
335
+ plan.insert = () => rIns
336
+ plan.del = (data) => data.map(() => okDel)
337
+ plan.save = (data) => data.map(() => okSave)
338
+ let err = await getReject(proc('mix', { add: [{ id: 'a1' }, { id: 'a2' }], del: [{ id: 'd1' }], diff: [{ id: 'f1' }] }))
339
+ assert.strict.equal(err, 'failed to insert data', JSON.stringify(rIns))
340
+ assert.strict.deepEqual(calls.map((v) => v.fn), ['insert'], JSON.stringify(rIns))
341
+ }
342
+ })
343
+
204
344
  it('U19 insert成功', async function() {
205
345
  //R04: insert回傳聚合物件而非逐筆陣列, 照舊回傳id
206
346
  plan.insert = (data) => ({ n: data.length, nInserted: data.length, ok: 1 })
@@ -208,6 +348,55 @@ describe('mapOrm逐筆失敗', function() {
208
348
  assert.strict.deepEqual(r, [{ id: 'i1' }])
209
349
  })
210
350
 
351
+ it('U19b insert聚合結果ok為0', async function() {
352
+ //R-PERITEM-01 規則4(orm-run03): 陣列與單一物件輸入皆須檢出, 固定字串reject而不回成功
353
+ plan.insert = (data) => ({ n: data.length, nInserted: 0, ok: 0, err: 'E-ins' })
354
+ let err = await getReject(proc('insert', [{ id: 'i1' }, { id: 'i2' }]))
355
+ assert.strict.equal(err, 'failed to insert data')
356
+ err = await getReject(proc('insert', { id: 'i3' }))
357
+ assert.strict.equal(err, 'failed to insert data')
358
+ assert.strict.deepEqual(calls.map((v) => v.fn), ['insert', 'insert'])
359
+ })
360
+
361
+ it('U19c insert略過既有主鍵或ORM回傳無失敗者照舊', async function() {
362
+ //R-PERITEM-01 規則4: 略過(n為0、ok為1)屬正常結果, 回傳id含被略過者; 回傳undefined、null、無ok鍵之物件、空陣列、元素皆ok非0或非物件之陣列不判失敗
363
+ for (let rIns of [{ n: 0, nInserted: 0, ok: 1 }, undefined, null, [], [{ n: 1, nInserted: 1, ok: 1 }, { n: 0, nInserted: 0, ok: 1 }], [null, 1, 'x', {}], {}, { n: 1 }]) {
364
+ plan.insert = () => rIns
365
+ let r = await proc('insert', [{ id: 'i1' }])
366
+ assert.strict.deepEqual(r, [{ id: 'i1' }], JSON.stringify(rIns))
367
+ }
368
+ })
369
+
370
+ it('U19e insert陣列形逐筆ok為0', async function() {
371
+ //R-PERITEM-01 規則4(orm-run04): 陣列形(非契約形狀)任一筆ok為0即同聚合形處置, 固定字串reject; 陣列與單一物件輸入皆是; 其餘筆是否寫入由ORM決定, 已寫入者無從回復
372
+ for (let rIns of [[{ n: 1, nInserted: 0, ok: 0, err: 'E-ins' }], [{ n: 1, nInserted: 1, ok: 1 }, { n: 1, nInserted: 0, ok: 0 }], [null, { ok: 0 }]]) {
373
+ plan.insert = () => rIns
374
+ let err = await getReject(proc('insert', [{ id: 'i1' }, { id: 'i2' }]))
375
+ assert.strict.equal(err, 'failed to insert data', JSON.stringify(rIns))
376
+ err = await getReject(proc('insert', { id: 'i3' }))
377
+ assert.strict.equal(err, 'failed to insert data', JSON.stringify(rIns))
378
+ }
379
+ })
380
+
381
+ it('U19d insert聚合結果ok為0時錯誤原文只進log而不進reject訊息', async function() {
382
+ //R06: 前端訊息為固定字串, 錯誤原文由console.log輸出
383
+ plan.insert = (data) => ({ n: data.length, nInserted: 0, ok: 0, err: 'E-secret-detail' })
384
+ let logs = []
385
+ let logOrig = console.log
386
+ console.log = (...args) => {
387
+ logs.push(args)
388
+ }
389
+ let err
390
+ try {
391
+ err = await getReject(proc('insert', [{ id: 'i1' }]))
392
+ }
393
+ finally {
394
+ console.log = logOrig
395
+ }
396
+ assert.strict.equal(err, 'failed to insert data')
397
+ assert.ok(JSON.stringify(logs).includes('E-secret-detail'))
398
+ })
399
+
211
400
  it('U22 錯誤原文寫入log而不進reject訊息', async function() {
212
401
  //R06: 前端訊息只帶主鍵, 錯誤原文由console.log輸出
213
402
  plan.save = () => [{ n: 1, nInserted: 0, nModified: 0, ok: 0, err: 'E-secret-detail' }]
@@ -348,6 +537,30 @@ describe('genTestData逐筆失敗', function() {
348
537
  assert.ok(String(err).includes('g2'))
349
538
  })
350
539
 
540
+ it('U21b funTest之save回傳聚合物件ok為0(非契約形狀)', async function() {
541
+ //R-ORM-RESULT-01(orm-run05): 無從逐筆列出, 仍視為失敗, 同U21之Error與訊息前綴, 內文列ORM回傳原文
542
+ let woItems = {
543
+ tests: {
544
+ save: async () => ({ n: 2, nInserted: 0, nModified: 0, ok: 0, err: 'E-gen' }),
545
+ },
546
+ }
547
+ let err = await getReject(genTestData(dsGen, woItems, { mode: 'funTest', bLog: false }))
548
+ assert.ok(err instanceof Error)
549
+ assert.strict.equal(err.message, 'failed to write test data into table[tests]: {"n":2,"nInserted":0,"nModified":0,"ok":0,"err":"E-gen"}')
550
+ })
551
+
552
+ it('U21c funTest之save回傳非陣列且無失敗者照舊', async function() {
553
+ //R-ORM-RESULT-01: undefined、null、無ok鍵之物件、ok為1之聚合物件不判失敗(同U20)
554
+ for (let rSave of [undefined, null, {}, { n: 2, ok: 1 }]) {
555
+ let woItems = {
556
+ tests: {
557
+ save: async () => rSave,
558
+ },
559
+ }
560
+ await genTestData(dsGen, woItems, { mode: 'funTest', bLog: false })
561
+ }
562
+ })
563
+
351
564
  it('U23 funTestAndSave與funTest之逐筆失敗訊息相同', async function() {
352
565
  //R09: funTestAndSave由w-data-collector之build產生(schema/index.mjs)且其錯誤原樣轉出; w-data-collector 1.0.46起兩種模式之訊息同為英文同格式
353
566
  let woItems = {
@@ -0,0 +1,61 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import assert from 'assert'
4
+
5
+
6
+ //R-ORM-RESULT-01(orm-run05): src內呼叫ORM實例方法之站點全集須已歸類; 新增站點(或改變某檔對某方法之呼叫數)時本案失敗, 須回帳本逐站判定「已檢查」或「不適用」後再更新此表
7
+ //盤點方式: 逐行去除註解後, 以正則取出「.方法(」之呼叫, 依(檔, 方法)計次; 計次而非行號, 避免無關修改使行號位移而誤報
8
+ let METHODS = ['select', 'selectByPk', 'selectAll', 'insert', 'insertBulk', 'save', 'del', 'delAll', 'selectGfs', 'selectByPkGfs', 'insertGfs', 'delGfs', 'delAllGfs', 'createStorage', 'close', 'backup', 'recover', 'funTest', 'funTestAndSave']
9
+
10
+ //已歸類之站點(檔 -> 方法 -> 呼叫次數), 判定見 sgud/rule/rulebook_error-input.md R-ORM-RESULT-01 之站點表
11
+ let EXPECT = {
12
+ 'genTestData.mjs': { funTestAndSave: 1, funTest: 1, save: 1 },
13
+ 'getDb.mjs': { backup: 1, recover: 1 },
14
+ 'getDbBackupAndRecover.mjs': { select: 1, createStorage: 1, insert: 1 },
15
+ 'getMapOrm.mjs': { select: 2, insert: 2, save: 3, del: 2, delAll: 1 },
16
+ 'WServOrm.mjs': { close: 1 },
17
+ }
18
+
19
+ function stripComment(line) {
20
+ let t = line.trim()
21
+ if (t.startsWith('//') || t.startsWith('*') || t.startsWith('/*')) {
22
+ return ''
23
+ }
24
+ return line.replace(/\s\/\/.*$/, '')
25
+ }
26
+
27
+ function scan(re) {
28
+ let fd = path.resolve('./src')
29
+ let out = {}
30
+ for (let f of fs.readdirSync(fd).filter((v) => v.endsWith('.mjs')).sort()) {
31
+ let txt = fs.readFileSync(path.join(fd, f), 'utf8').split(/\r?\n/).map(stripComment).join('\n')
32
+ for (let m of txt.matchAll(re)) {
33
+ out[f] = out[f] || {}
34
+ out[f][m[1]] = (out[f][m[1]] || 0) + 1
35
+ }
36
+ }
37
+ return out
38
+ }
39
+
40
+
41
+ describe('ORM呼叫站點盤點', function() {
42
+
43
+ it('O01 src內呼叫ORM實例方法之站點全集同已歸類之表(未歸類為0)', function() {
44
+ //R-ORM-RESULT-01: 新增或移除呼叫須同步更新帳本與此表
45
+ let got = scan(new RegExp(`\\.(${METHODS.join('|')})\\(`, 'g'))
46
+ assert.strict.deepEqual(got, EXPECT)
47
+ })
48
+
49
+ it('O02 不得以括號記法或動態方法名呼叫ORM(盤點正則無從涵蓋)', function() {
50
+ //R-ORM-RESULT-01: 以[方法名字串]呼叫者改寫為直接呼叫, 否則O01之盤點不完整
51
+ let got = scan(new RegExp(`\\[['"\`](${METHODS.join('|')})['"\`]\\]`, 'g'))
52
+ assert.strict.deepEqual(got, {})
53
+ })
54
+
55
+ it('O03 ORM建構僅getWoItems一處', function() {
56
+ //R-ORM-RESULT-01: 建構失敗以拋錯表達, 無回傳可檢查(不適用)
57
+ let got = scan(/(new) WOrm\(/g)
58
+ assert.strict.deepEqual(got, { 'getWoItems.mjs': { new: 1 } })
59
+ })
60
+
61
+ })