w-serv-orm 1.1.17 → 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,30 @@
1
+ import size from 'lodash-es/size.js'
2
+ import getItemsFail from 'w-data-collector/src/getItemsFail.mjs'
3
+
4
+
5
+ /**
6
+ * 判斷ORM寫入類函數(insert、save、del、delAll)之回傳結果是否回報失敗
7
+ *
8
+ * 依w-orm系列規格,insert與delAll回傳聚合物件且ok恆為1,save與del回傳逐筆陣列,錯誤皆整批reject;單筆失敗者(save、del)於該筆以ok為0回報。
9
+ * 本函數不論函數與形狀一律判讀:回傳為聚合物件(非null、非陣列之物件)而ok為0者(ORM違約或他種後端)視為失敗;
10
+ * 回傳為逐筆陣列而任一筆ok為0者(同getItemsFail之判讀)視為失敗。
11
+ * 其餘(undefined、null、無ok鍵之物件、空陣列、元素皆ok非0或非物件之陣列、ok為1含略過既有主鍵或未命中之n為0)不視為失敗,維持舊行為。
12
+ * 讀取函數(select)之回傳為數據列,列內欄位可能名為ok,不得以本函數判讀。
13
+ * 供procOrm之寫入類各模式、mix之各步驟、備份還原與測試數據寫入共用。
14
+ *
15
+ * @param {*} r 輸入ORM寫入類函數之回傳結果
16
+ * @returns {Boolean} 回傳是否回報失敗
17
+ */
18
+ function isOrmFail(r) {
19
+
20
+ //check, 逐筆陣列
21
+ if (Array.isArray(r)) {
22
+ return size(getItemsFail(r, [])) > 0
23
+ }
24
+
25
+ //check, 聚合物件
26
+ return r !== null && typeof r === 'object' && r.ok === 0
27
+ }
28
+
29
+
30
+ export default isOrmFail
@@ -92,3 +92,145 @@ describe('procOrm接w-orm-lowdb', function() {
92
92
  })
93
93
 
94
94
  })
95
+
96
+
97
+ //R-PERITEM-01 規則4(orm-run03、run04): 包一層真ORM(w-orm-lowdb), 令insert於injectInsert為true時第0筆不寫入、其餘筆照常寫入, 並回傳聚合物件而ok為0; 為'array'時回傳逐筆陣列而第0筆ok為0(模擬違約之ORM, w-orm-lowdb實際不會如此); 其餘操作原樣
98
+ let fdIns = './test/_tmp/api-procOrm-insertfail'
99
+ let injectInsert = false
100
+ //R-ORM-RESULT-01(orm-run05): injectOther為'save'、'del'或'delAll'時, 該操作照常執行後回傳聚合物件而ok為0(非契約形狀, 模擬違約之ORM)
101
+ let injectOther = null
102
+ function WOrmInsertFail(...args) {
103
+ let wo = WOrm(...args)
104
+ for (let m of ['save', 'del', 'delAll']) {
105
+ let orig = wo[m]
106
+ wo[m] = async (...a) => {
107
+ let r = await orig(...a)
108
+ return injectOther === m ? { n: 1, ok: 0, err: 'E-agg' } : r
109
+ }
110
+ }
111
+ let insertOrig = wo.insert
112
+ wo.insert = async (data) => {
113
+ if (!injectInsert) {
114
+ return insertOrig(data)
115
+ }
116
+ let rows = Array.isArray(data) ? data : [data]
117
+ let rest = rows.slice(1)
118
+ if (rest.length > 0) {
119
+ await insertOrig(rest)
120
+ }
121
+ if (injectInsert === 'array') {
122
+ return rows.map((v, k) => (k === 0 ? { n: 1, nInserted: 0, ok: 0, err: 'E-ins' } : { n: 1, nInserted: 1, ok: 1 }))
123
+ }
124
+ return { n: rows.length, nInserted: rest.length, ok: 0, err: 'E-ins' }
125
+ }
126
+ return wo
127
+ }
128
+
129
+
130
+ describe('procOrm接insert回報ok為0之w-orm-lowdb', function() {
131
+
132
+ let so = null
133
+ let proc = (mode, input) => so.procOrm('u1', 'tests', mode, input)
134
+ let getIds = async () => (await so.woItems.tests.select({})).map((v) => v.id).sort()
135
+
136
+ before(function() {
137
+ fs.rmSync(fdIns, { recursive: true, force: true })
138
+ fs.mkdirSync(fdIns, { recursive: true })
139
+ so = WServOrm(ds, WOrmInsertFail, `${fdIns}/db.json`, 'worm')
140
+ })
141
+
142
+ beforeEach(async function() {
143
+ injectInsert = false
144
+ injectOther = null
145
+ await so.woItems.tests.delAll({})
146
+ await so.woItems.tests.insert([{ id: 'x1' }, { id: 'x2' }, { id: 'x3' }])
147
+ })
148
+
149
+ after(async function() {
150
+ injectInsert = false
151
+ injectOther = null
152
+ await so.closeAll()
153
+ fs.rmSync(fdIns, { recursive: true, force: true })
154
+ })
155
+
156
+ it('F01 insert之ORM回報ok為0時reject而不回成功', async function() {
157
+ //規則4: 聚合結果ok為0視為失敗; 已寫入之筆無從回復(ORM違約, 第1筆已寫入、第0筆未寫入), 但呼叫端得知失敗
158
+ injectInsert = true
159
+ let err = await getReject(proc('insert', [{ id: 'n1' }, { id: 'n2' }]))
160
+ assert.strict.equal(err, 'failed to insert data')
161
+ assert.strict.deepEqual(await getIds(), ['n2', 'x1', 'x2', 'x3'])
162
+ })
163
+
164
+ it('F02 mix之add之ORM回報ok為0時reject且後續del不執行', async function() {
165
+ //規則4: add為首步, 聚合結果ok為0即立即reject, del步驟不執行(x3仍在庫內)
166
+ injectInsert = true
167
+ let err = await getReject(proc('mix', { add: [{ id: 'n1' }, { id: 'n2' }], del: [{ id: 'x3' }] }))
168
+ assert.strict.equal(err, 'failed to insert data')
169
+ assert.strict.deepEqual(await getIds(), ['n2', 'x1', 'x2', 'x3'])
170
+ })
171
+
172
+ it('F04 insert之ORM回報陣列形逐筆ok為0時reject而不回成功', async function() {
173
+ //規則4(orm-run04): 陣列形任一筆ok為0同聚合形處置; 其餘筆已寫入(n2)無從回復
174
+ injectInsert = 'array'
175
+ let err = await getReject(proc('insert', [{ id: 'n1' }, { id: 'n2' }]))
176
+ assert.strict.equal(err, 'failed to insert data')
177
+ assert.strict.deepEqual(await getIds(), ['n2', 'x1', 'x2', 'x3'])
178
+ })
179
+
180
+ it('F05 mix之add之ORM回報陣列形逐筆ok為0時reject且後續del不執行', async function() {
181
+ //規則4(orm-run04): add為首步, 立即reject, del步驟不執行(x3仍在庫內)
182
+ injectInsert = 'array'
183
+ let err = await getReject(proc('mix', { add: [{ id: 'n1' }, { id: 'n2' }], del: [{ id: 'x3' }] }))
184
+ assert.strict.equal(err, 'failed to insert data')
185
+ assert.strict.deepEqual(await getIds(), ['n2', 'x1', 'x2', 'x3'])
186
+ })
187
+
188
+ it('F06 delAll之ORM回報聚合物件ok為0時reject而不回成功', async function() {
189
+ //R-ORM-RESULT-01(orm-run05): ORM已執行刪除而回報ok為0(違約), 呼叫端得知失敗
190
+ injectOther = 'delAll'
191
+ let err = await getReject(proc('delAll', { id: 'x1' }))
192
+ assert.strict.equal(err, 'failed to delete data')
193
+ injectOther = null
194
+ assert.strict.deepEqual(await getIds(), ['x2', 'x3'])
195
+ })
196
+
197
+ it('F07 save與del之ORM回報聚合物件ok為0時reject而不回成功', async function() {
198
+ //R-ORM-RESULT-01(orm-run05): 契約為逐筆陣列, 聚合形(非契約形狀)無從逐筆列出, 固定字串reject
199
+ injectOther = 'save'
200
+ let err = await getReject(proc('save', [{ id: 'x1', isActive: 'n' }]))
201
+ assert.strict.equal(err, 'failed to save data')
202
+ injectOther = 'del'
203
+ err = await getReject(proc('del', [{ id: 'x2' }]))
204
+ assert.strict.equal(err, 'failed to delete data')
205
+ })
206
+
207
+ it('F08 mix之del回報聚合物件ok為0時立即reject且diff不執行', async function() {
208
+ //R-ORM-RESULT-01(orm-run05): del為第2步, 聚合形失敗即整批性, diff步驟不執行(x1之isActive仍為預設而未被改為n)
209
+ injectOther = 'del'
210
+ let err = await getReject(proc('mix', { del: [{ id: 'x3' }], diff: [{ id: 'x1', isActive: 'n' }] }))
211
+ assert.strict.equal(err, 'failed to delete data')
212
+ injectOther = null
213
+ let rs = await so.woItems.tests.select({ id: 'x1' })
214
+ assert.ok(rs.length === 1 && rs[0].isActive !== 'n', JSON.stringify(rs))
215
+ })
216
+
217
+ it('F09 契約內之ORM(delAll恆ok為1)行為不變', async function() {
218
+ //R-ORM-RESULT-01: 未違約時delAll、save、del照常resolve
219
+ let r = await proc('delAll', { id: 'x1' })
220
+ assert.strict.deepEqual(r, { n: 1, nDeleted: 1, ok: 1 })
221
+ let r2 = await proc('save', [{ id: 'x2', isActive: 'n' }])
222
+ assert.strict.deepEqual(r2, [{ id: 'x2' }])
223
+ let r3 = await proc('del', [{ id: 'x3' }])
224
+ assert.strict.deepEqual(r3, [{ n: 1, nDeleted: 1, ok: 1 }])
225
+ })
226
+
227
+ it('F03 契約內之ORM(insert恆ok為1)行為不變', async function() {
228
+ //規則4: 未違約時insert與mix照常resolve並寫入; 略過既有主鍵(n為0)亦照常
229
+ let r = await proc('insert', [{ id: 'n1' }, { id: 'x1' }])
230
+ assert.strict.deepEqual(r, [{ id: 'n1' }, { id: 'x1' }])
231
+ let r2 = await proc('mix', { add: [{ id: 'n2' }], del: [{ id: 'x3' }] })
232
+ assert.strict.deepEqual(r2, [{ id: 'n2' }, { id: 'x3' }])
233
+ assert.strict.deepEqual(await getIds(), ['n1', 'n2', 'x1', 'x2'])
234
+ })
235
+
236
+ })
@@ -31,6 +31,26 @@ function mkWoItems(log, behavior = {}) {
31
31
  }
32
32
  }
33
33
 
34
+ //多表替身: results[表名]為該表insert之回傳函數(未給則回契約內之成功結果)
35
+ function mkWoItemsMulti(log, results = {}) {
36
+ let woItems = {}
37
+ for (let name of ['t1', 't2', 't3']) {
38
+ woItems[name] = {
39
+ select: async () => [],
40
+ insert: async (d) => {
41
+ log.push({ fn: 'insert', table: name, d })
42
+ if (results[name]) {
43
+ return results[name](d)
44
+ }
45
+ return { n: d.length, nInserted: d.length, ok: 1 }
46
+ },
47
+ }
48
+ }
49
+ return woItems
50
+ }
51
+
52
+ let BK3 = [{ name: 't1', data: [{ id: 'a' }] }, { name: 't2', data: [{ id: 'b' }, { id: 'c' }] }, { name: 't3', data: [{ id: 'd' }] }]
53
+
34
54
  async function getReject(pm) {
35
55
  try {
36
56
  await pm
@@ -131,6 +151,64 @@ describe('getDb備份與還原', function() {
131
151
  assert.strict.deepEqual(log, [{ fn: 'insert', table: 'tests', d: [{ id: 'a' }] }])
132
152
  })
133
153
 
154
+ it('B05b recover之insert聚合結果ok為0即reject Error並停止, 先前之表已寫入、之後之表不寫入', async function() {
155
+ //R-DB-01 規則3(orm-run04): ORM回報失敗不得當成功; 錯誤型別為Error(R-ERR-01規則2); 訊息含失敗表名、ORM回傳原文與先前已寫入之表
156
+ let log = []
157
+ let fp = writeBackup('fail-agg.json', JSON.stringify(BK3))
158
+ let woItems = mkWoItemsMulti(log, { t2: (d) => ({ n: d.length, nInserted: 0, ok: 0, err: 'E-ins' }) })
159
+ let err = await quiet(() => getReject(getDb.recover(woItems, fp, { bLog: false })))
160
+ assert.ok(err instanceof Error)
161
+ assert.strict.equal(err.message, 'failed to recover data into table[t2]: {"n":2,"nInserted":0,"ok":0,"err":"E-ins"}; tables already written: t1')
162
+ assert.strict.deepEqual(log.map((v) => v.table), ['t1', 't2'])
163
+ })
164
+
165
+ it('B05c recover之insert陣列形逐筆ok為0即reject Error, 訊息列失敗筆', async function() {
166
+ //R-DB-01 規則3(orm-run04): 陣列形(非契約形狀)任一筆ok為0同聚合形處置; 失敗筆取輸入同位置數據之id(getItemsFail)
167
+ let log = []
168
+ let fp = writeBackup('fail-arr.json', JSON.stringify(BK3))
169
+ let woItems = mkWoItemsMulti(log, { t2: () => [{ n: 1, nInserted: 0, ok: 0, err: 'E-ins' }, { n: 1, nInserted: 1, ok: 1 }] })
170
+ let err = await quiet(() => getReject(getDb.recover(woItems, fp, { bLog: false })))
171
+ assert.ok(err instanceof Error)
172
+ assert.strict.equal(err.message, 'failed to recover data into table[t2]: [{"k":0,"id":"b","err":"E-ins"}]; tables already written: t1')
173
+ assert.strict.deepEqual(log.map((v) => v.table), ['t1', 't2'])
174
+ })
175
+
176
+ it('B05d recover之首表失敗無已寫入之表可列, 失敗筆超過10筆僅列前10筆並附總數', async function() {
177
+ //R-DB-01 規則3(orm-run04): 同getMsgFail之上限(10筆)
178
+ let log = []
179
+ let rows = []
180
+ for (let i = 1; i <= 12; i++) {
181
+ rows.push({ id: `r${i}` })
182
+ }
183
+ let fp = writeBackup('fail-many.json', JSON.stringify([{ name: 't1', data: rows }, { name: 't2', data: [{ id: 'b' }] }]))
184
+ let woItems = mkWoItemsMulti(log, { t1: (d) => d.map(() => ({ n: 1, nInserted: 0, ok: 0, err: 'E-full' })) })
185
+ let err = await quiet(() => getReject(getDb.recover(woItems, fp, { bLog: false })))
186
+ let fails = rows.slice(0, 10).map((v, k) => ({ k, id: v.id, err: 'E-full' }))
187
+ assert.strict.equal(err.message, `failed to recover data into table[t1]: ${JSON.stringify(fails)}, ... (12 in total)`)
188
+ assert.strict.deepEqual(log.map((v) => v.table), ['t1'])
189
+ })
190
+
191
+ it('B05e recover之insert無失敗(略過既有主鍵、回傳undefined、null、空物件、空陣列、逐筆皆ok為1)照舊resolve並寫入全部表', async function() {
192
+ //R-DB-01 規則3(orm-run04): 只認聚合物件ok為0與陣列內任一筆ok為0, 其餘維持舊行為
193
+ for (let rIns of [{ n: 0, nInserted: 0, ok: 1 }, undefined, null, {}, [], [{ n: 1, nInserted: 1, ok: 1 }]]) {
194
+ let log = []
195
+ let fp = writeBackup('ok-shapes.json', JSON.stringify(BK3))
196
+ let woItems = mkWoItemsMulti(log, { t2: () => rIns })
197
+ await quiet(() => getDb.recover(woItems, fp, { bLog: false }))
198
+ assert.strict.deepEqual(log.map((v) => v.table), ['t1', 't2', 't3'], JSON.stringify(rIns))
199
+ }
200
+ })
201
+
202
+ it('B05f 低階recover之insert失敗亦reject Error', async function() {
203
+ //R-DB-01 規則3(orm-run04): WServOrm實例上之recover即此低階函數
204
+ let log = []
205
+ let fp = writeBackup('fail-low.json', JSON.stringify(BK3))
206
+ let woItems = mkWoItemsMulti(log, { t3: (d) => ({ n: d.length, nInserted: 0, ok: 0 }) })
207
+ let err = await quiet(() => getReject(br.recover(fp, woItems)))
208
+ assert.ok(err instanceof Error)
209
+ assert.strict.equal(err.message, 'failed to recover data into table[t3]: {"n":1,"nInserted":0,"ok":0}; tables already written: t1, t2')
210
+ })
211
+
134
212
  it('B06 recover之bRela缺ds或genModelsByTabs即於寫入前reject', async function() {
135
213
  //P4-1: 明確訊息, 不寫入
136
214
  let log = []
@@ -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
+ })
@@ -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
+ })