w-orm-lmdb 1.0.16 → 1.0.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.
- package/README.md +15 -0
- package/dist/req-mingo.umd.js +1 -1
- package/dist/w-orm-lmdb.umd.js +2 -2
- package/dist/w-orm-lmdb.umd.js.map +1 -1
- package/docs/WOrmLmdb.html +63 -9
- package/docs/WOrmLmdb.mjs.html +139 -78
- package/docs/index.html +1 -1
- package/g-basic.mjs +15 -0
- package/package.json +1 -1
- package/src/WOrmLmdb.mjs +138 -77
- package/test/unit-autogenpk.test.mjs +188 -0
- package/test/unit-event.test.mjs +242 -0
- package//350/263/207/346/226/231/345/272/253/345/207/275/346/225/270/345/233/236/345/202/263/345/256/232/347/276/251/350/250/210/347/256/227.md +165 -22
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import _ from 'lodash-es'
|
|
3
|
+
import w from 'wsemi'
|
|
4
|
+
import WOrm from '../src/WOrmLmdb.mjs'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
describe('event', function() {
|
|
8
|
+
let rt = null
|
|
9
|
+
let vans = {}
|
|
10
|
+
let vget = {}
|
|
11
|
+
|
|
12
|
+
before(async function() {
|
|
13
|
+
|
|
14
|
+
w.fsDeleteFolder('./_db_event')
|
|
15
|
+
|
|
16
|
+
let url = './_db_event'
|
|
17
|
+
|
|
18
|
+
//evs, 蒐集事件, 事件名與各參數皆記錄以供比對
|
|
19
|
+
let mkEvs = function(wo) {
|
|
20
|
+
let evs = []
|
|
21
|
+
wo.on('change', function(mode, data, res) {
|
|
22
|
+
evs.push({ ev: 'change', mode, res })
|
|
23
|
+
})
|
|
24
|
+
wo.on('error', function(mode, data, err) {
|
|
25
|
+
evs.push({ ev: 'error', mode, data, err })
|
|
26
|
+
})
|
|
27
|
+
return evs
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
//change事件之形狀: 逐筆函數以整批為單位發出一次
|
|
31
|
+
let woC = WOrm({ url, db: 'worm', cl: 'ch' })
|
|
32
|
+
let evsC = mkEvs(woC)
|
|
33
|
+
await woC.insert([{ id: 'c1' }, { id: 'c2' }])
|
|
34
|
+
await woC.save({ id: 'c1', a: 1 })
|
|
35
|
+
await woC.del({ id: 'c2' })
|
|
36
|
+
await woC.delAll()
|
|
37
|
+
vget[1] = _.map(evsC, function(v) {
|
|
38
|
+
return `${v.ev}:${v.mode}`
|
|
39
|
+
})
|
|
40
|
+
await woC.close()
|
|
41
|
+
|
|
42
|
+
//save之逐筆插入另發mode為insert之事件, 且早於整批save事件
|
|
43
|
+
let woI = WOrm({ url, db: 'worm', cl: 'ins' })
|
|
44
|
+
let evsI = mkEvs(woI)
|
|
45
|
+
await woI.save({ id: 'i1', a: 1 })
|
|
46
|
+
vget[2] = _.map(evsI, function(v) {
|
|
47
|
+
return `${v.ev}:${v.mode}`
|
|
48
|
+
})
|
|
49
|
+
await woI.close()
|
|
50
|
+
|
|
51
|
+
//error事件: 整批性錯誤, 須於reject之前發出且err為字串
|
|
52
|
+
let woE = WOrm({ url, db: 'worm', cl: 'errb', autoGenPk: false })
|
|
53
|
+
let evsE = mkEvs(woE)
|
|
54
|
+
rt = null
|
|
55
|
+
await woE.insert({ name: 'no-id' })
|
|
56
|
+
.then(function() {
|
|
57
|
+
rt = 'resolve'
|
|
58
|
+
})
|
|
59
|
+
.catch(function() {
|
|
60
|
+
rt = 'reject'
|
|
61
|
+
})
|
|
62
|
+
vget[3] = rt
|
|
63
|
+
vget[4] = _.map(evsE, function(v) {
|
|
64
|
+
return `${v.ev}:${v.mode}`
|
|
65
|
+
})
|
|
66
|
+
vget[5] = w.isestr(_.get(evsE, '0.err'))
|
|
67
|
+
vget[6] = _.get(evsE, '0.data') !== null
|
|
68
|
+
await woE.close()
|
|
69
|
+
|
|
70
|
+
//error事件: 逐筆失敗, 整批仍resolve且每筆一次
|
|
71
|
+
let woD = WOrm({ url, db: 'worm', cl: 'errp' })
|
|
72
|
+
let evsD = mkEvs(woD)
|
|
73
|
+
await woD.insert({ id: 'd1' })
|
|
74
|
+
evsD.length = 0
|
|
75
|
+
rt = null
|
|
76
|
+
await woD.del([{ id: 'd1' }, { name: 'no-id-a' }, { name: 'no-id-b' }])
|
|
77
|
+
.then(function(msg) {
|
|
78
|
+
rt = _.map(msg, 'ok')
|
|
79
|
+
})
|
|
80
|
+
.catch(function() {
|
|
81
|
+
rt = 'reject'
|
|
82
|
+
})
|
|
83
|
+
vget[7] = rt
|
|
84
|
+
vget[8] = _.map(evsD, function(v) {
|
|
85
|
+
return `${v.ev}:${v.mode}`
|
|
86
|
+
})
|
|
87
|
+
vget[9] = w.isestr(_.get(evsD, '0.err'))
|
|
88
|
+
await woD.close()
|
|
89
|
+
|
|
90
|
+
//正常結果不得發出error事件
|
|
91
|
+
let woN = WOrm({ url, db: 'worm', cl: 'norm' })
|
|
92
|
+
let evsN = mkEvs(woN)
|
|
93
|
+
await woN.insert({ id: 'n1', a: 1 })
|
|
94
|
+
evsN.length = 0
|
|
95
|
+
await woN.insert({ id: 'n1', a: 2 }) //已存在, nInserted為0
|
|
96
|
+
await woN.save({ id: 'n1', a: 1 }) //合併後內容相同, 不寫入
|
|
97
|
+
await woN.save({ id: 'n2' }, { autoInsert: false }) //不存在且不autoInsert
|
|
98
|
+
await woN.del({ id: 'n-none' }) //主鍵未命中
|
|
99
|
+
await woN.delAll({ zz: 'none' }) //條件無命中
|
|
100
|
+
await woN.selectByPk('n-none') //查無數據
|
|
101
|
+
vget[10] = _.size(_.filter(evsN, function(v) {
|
|
102
|
+
return v.ev === 'error'
|
|
103
|
+
}))
|
|
104
|
+
await woN.close()
|
|
105
|
+
|
|
106
|
+
//核心不變式: 同一操作於有註冊與未註冊error監聽兩種情況下, 回傳值須完全相同
|
|
107
|
+
let run = async function(withListener) {
|
|
108
|
+
let wo = WOrm({ url, db: 'worm', cl: `inv${withListener ? 1 : 0}`, autoGenPk: false })
|
|
109
|
+
if (withListener) {
|
|
110
|
+
wo.on('error', function() {})
|
|
111
|
+
}
|
|
112
|
+
let r = {}
|
|
113
|
+
|
|
114
|
+
//整批性錯誤
|
|
115
|
+
r.batch = await wo.insert({ name: 'no-id' })
|
|
116
|
+
.then(function(msg) {
|
|
117
|
+
return { type: 'resolve', msg }
|
|
118
|
+
})
|
|
119
|
+
.catch(function() {
|
|
120
|
+
return { type: 'reject' }
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
//逐筆失敗
|
|
124
|
+
await wo.insert({ id: 'x1' })
|
|
125
|
+
r.each = await wo.del([{ id: 'x1' }, { name: 'no-id' }])
|
|
126
|
+
.then(function(msg) {
|
|
127
|
+
return {
|
|
128
|
+
type: 'resolve',
|
|
129
|
+
msg: _.map(msg, function(v) {
|
|
130
|
+
return _.omit(v, 'err')
|
|
131
|
+
}),
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
.catch(function() {
|
|
135
|
+
return { type: 'reject' }
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
await wo.close()
|
|
139
|
+
return r
|
|
140
|
+
}
|
|
141
|
+
let rWith = await run(true)
|
|
142
|
+
let rWithout = await run(false)
|
|
143
|
+
vget[11] = _.isEqual(rWith, rWithout)
|
|
144
|
+
vget[12] = rWithout.batch.type
|
|
145
|
+
vget[13] = rWithout.each.type
|
|
146
|
+
|
|
147
|
+
//訂閱函數拋錯不得影響本次操作
|
|
148
|
+
let woT = WOrm({ url, db: 'worm', cl: 'thr' })
|
|
149
|
+
woT.on('change', function() {
|
|
150
|
+
throw new Error('listener boom')
|
|
151
|
+
})
|
|
152
|
+
woT.on('error', function() {
|
|
153
|
+
throw new Error('listener boom')
|
|
154
|
+
})
|
|
155
|
+
rt = null
|
|
156
|
+
await woT.insert({ id: 't1', a: 1 })
|
|
157
|
+
.then(function(msg) {
|
|
158
|
+
rt = msg
|
|
159
|
+
})
|
|
160
|
+
.catch(function(msg) {
|
|
161
|
+
rt = 'reject: ' + msg.toString()
|
|
162
|
+
})
|
|
163
|
+
vget[14] = rt
|
|
164
|
+
await woT.close()
|
|
165
|
+
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
//逐筆函數以整批為單位發出一次change, 不逐筆發出
|
|
169
|
+
vans[1] = ['change:insert', 'change:save', 'change:del', 'change:delAll']
|
|
170
|
+
it(`should get ${JSON.stringify(vans[1])} for change events`, async function() {
|
|
171
|
+
assert.strict.deepStrictEqual(vget[1], vans[1])
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
vans[2] = ['change:insert', 'change:save']
|
|
175
|
+
it(`should get ${JSON.stringify(vans[2])} for change events of save with autoInsert`, async function() {
|
|
176
|
+
assert.strict.deepStrictEqual(vget[2], vans[2])
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
vans[3] = 'reject'
|
|
180
|
+
it(`should get ${JSON.stringify(vans[3])} for insert without id by autoGenPk=false`, async function() {
|
|
181
|
+
assert.strict.deepStrictEqual(vget[3], vans[3])
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
vans[4] = ['error:insert']
|
|
185
|
+
it(`should get ${JSON.stringify(vans[4])} for error event of batch error`, async function() {
|
|
186
|
+
assert.strict.deepStrictEqual(vget[4], vans[4])
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
vans[5] = true
|
|
190
|
+
it(`should get ${JSON.stringify(vans[5])} for err of error event being string`, async function() {
|
|
191
|
+
assert.strict.deepStrictEqual(vget[5], vans[5])
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
vans[6] = true
|
|
195
|
+
it(`should get ${JSON.stringify(vans[6])} for data of error event being input data`, async function() {
|
|
196
|
+
assert.strict.deepStrictEqual(vget[6], vans[6])
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
//逐筆失敗時整批仍resolve, 且每筆失敗各發出一次error
|
|
200
|
+
vans[7] = [1, 0, 0]
|
|
201
|
+
it(`should get ${JSON.stringify(vans[7])} for ok of each item when items failed`, async function() {
|
|
202
|
+
assert.strict.deepStrictEqual(vget[7], vans[7])
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
vans[8] = ['error:del', 'error:del', 'change:del']
|
|
206
|
+
it(`should get ${JSON.stringify(vans[8])} for error events emitted before change`, async function() {
|
|
207
|
+
assert.strict.deepStrictEqual(vget[8], vans[8])
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
vans[9] = true
|
|
211
|
+
it(`should get ${JSON.stringify(vans[9])} for err of each error event being string`, async function() {
|
|
212
|
+
assert.strict.deepStrictEqual(vget[9], vans[9])
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
//正常結果不得發出error
|
|
216
|
+
vans[10] = 0
|
|
217
|
+
it(`should get ${JSON.stringify(vans[10])} for error events by normal results`, async function() {
|
|
218
|
+
assert.strict.deepStrictEqual(vget[10], vans[10])
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
//T10.1核心不變式
|
|
222
|
+
vans[11] = true
|
|
223
|
+
it(`should get ${JSON.stringify(vans[11])} for same results with and without error listener`, async function() {
|
|
224
|
+
assert.strict.deepStrictEqual(vget[11], vans[11])
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
vans[12] = 'reject'
|
|
228
|
+
it(`should get ${JSON.stringify(vans[12])} for batch error still rejecting without error listener`, async function() {
|
|
229
|
+
assert.strict.deepStrictEqual(vget[12], vans[12])
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
vans[13] = 'resolve'
|
|
233
|
+
it(`should get ${JSON.stringify(vans[13])} for each-item failure still resolving without error listener`, async function() {
|
|
234
|
+
assert.strict.deepStrictEqual(vget[13], vans[13])
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
vans[14] = { n: 1, nInserted: 1, ok: 1 }
|
|
238
|
+
it(`should get ${JSON.stringify(vans[14])} for result not affected by throwing listener`, async function() {
|
|
239
|
+
assert.strict.deepStrictEqual(vget[14], vans[14])
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
})
|
|
@@ -67,15 +67,36 @@
|
|
|
67
67
|
|
|
68
68
|
主鍵值無效(未給、型別不符)之處置見各函數說明。
|
|
69
69
|
|
|
70
|
-
### T6
|
|
70
|
+
### T6 主鍵補值與 `autoGenPk`
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
主鍵由誰產生,由建構設定 **`opt.autoGenPk`** 決定,**預設為 `true`**。各套件皆須提供此設定,惟符合下述例外者得不提供。
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
| `autoGenPk` | 行為 |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `true`(預設) | `insert` 與 `save` 於輸入未帶有效主鍵時,由套件自動產生主鍵值後再寫入 |
|
|
77
|
+
| `false` | 套件一律不產生主鍵值,主鍵須由呼叫端於寫入前自備 |
|
|
78
|
+
|
|
79
|
+
`autoGenPk: false` 之定位為**依賴注入**:主鍵的產生規則改由呼叫端掌握(如採用外部發號器、以業務欄位組合、沿用上游系統既有識別碼),套件不介入。採用此設定後,**主鍵之唯一性、格式與是否與既有資料衝突,皆由呼叫端自負**;套件不做補救,亦不因主鍵不合預期而額外檢查或修正。
|
|
80
|
+
|
|
81
|
+
`autoGenPk` 為建構層設定,**不得於 `insert`/`save` 之 `option` 逐次覆寫**。主鍵由誰產生是資料所有權的歸屬,屬整個資料表的政策;若逐次可改,同一資料表將混入兩種來源之主鍵而難以追溯。
|
|
82
|
+
|
|
83
|
+
`autoGenPk` 為 `true` 時,自動產生之主鍵值須具足夠唯一性(如 UUID 或單調序列),不得採用可預期碰撞之來源。
|
|
84
|
+
|
|
85
|
+
**`autoGenPk` 為 `false` 而輸入未帶有效主鍵時,以 `Promise.reject` 拋出**,屬 T4 之整批性錯誤,不進入逐筆結果。理由為此屬呼叫端未履行契約,而非某一筆資料本身的問題;若降級為該筆 `ok: 0`,呼叫端容易在整批 resolve 之下漏看,使「忘了給主鍵」靜默變成「少寫了幾筆」。
|
|
86
|
+
|
|
87
|
+
**例外:套件無從產生合格主鍵值者,得完全不提供 `autoGenPk`。** 符合下列任一情形者適用:
|
|
88
|
+
|
|
89
|
+
1. **主鍵承載業務語義。** 主鍵非無語義之識別碼,而是承載業務意義之欄位(如時序資料以觀測時間為主鍵)。補值等同替呼叫端決定一筆資料的業務內容,會讓「呼叫端漏給主鍵」靜默變成「以當下之值寫入一筆」,其錯誤比直接失敗更難察覺。
|
|
90
|
+
|
|
91
|
+
2. **套件無從得知主鍵之型別或值域。** 主鍵欄位得由呼叫端指定,而其型別由資料表 schema 決定、套件並不知情時,即無從產生型別相容且符合前述「須具足夠唯一性」之值。例如主鍵為時間戳型別者,可產生之值僅有當下時間,同一瞬間併發即碰撞,違反「不得採用可預期碰撞之來源」;為字串型別者方適用隨機字串;為整數型別者須倚賴資料庫端之序列。三者所需之產生策略互斥,而套件於寫入前無從得知係何者。
|
|
92
|
+
|
|
93
|
+
採用本例外者為**完全不提供該設定**,而非提供一個恆為 `false` 之設定——後者會讓呼叫端誤以為可切換,且徒增一個讀了文件仍不能用的選項。
|
|
75
94
|
|
|
76
|
-
|
|
95
|
+
此類套件之 `insert` 與 `save` 於輸入未帶有效主鍵時,一律以 `Promise.reject` 拋出整批性錯誤,行為等同 `autoGenPk` 為 `false`;主鍵之唯一性與格式同樣由呼叫端自負。
|
|
77
96
|
|
|
78
|
-
|
|
97
|
+
採用本例外之套件,須於下方「各套件符合狀態」載明其主鍵欄位、所依據之情形與理由;未載明者一律適用 `autoGenPk` 預設為 `true` 之規定。
|
|
98
|
+
|
|
99
|
+
`del` **不受 `autoGenPk` 影響**,於任一設定下皆不補值——未帶有效主鍵者視為該筆無法處理,回 `ok: 0` 並附 `err`,且不得將無效主鍵送進查詢條件(部分後端會把 `undefined` 轉為 `null` 而誤中其他資料)。`del` 之未帶有效主鍵屬該筆問題而非整批性錯誤,因刪除係逐筆比對既有資料,一筆缺主鍵不影響其餘筆之處理。
|
|
79
100
|
|
|
80
101
|
### T7 原子性要求
|
|
81
102
|
|
|
@@ -107,6 +128,91 @@
|
|
|
107
128
|
|
|
108
129
|
`select` 與 `selectByPk` 不得寫入資料、不得建立索引或資料表、不得改變任何可被觀察的狀態。需要初始化的動作應於建構時或寫入函數內完成。
|
|
109
130
|
|
|
131
|
+
### T10 事件
|
|
132
|
+
|
|
133
|
+
各套件之操作物件為 `EventEmitter`,須發出 `change` 與 `error` 兩種事件,供呼叫端於單一處集中觀察資料異動與失敗,而不必在每個呼叫點各自包裝。
|
|
134
|
+
|
|
135
|
+
事件僅為**附加通知**,不承擔任何規格所定之回傳義務。
|
|
136
|
+
|
|
137
|
+
#### T10.1 共同要求
|
|
138
|
+
|
|
139
|
+
以下五項對所有事件一律適用:
|
|
140
|
+
|
|
141
|
+
1. **不得為唯一管道。** 凡經由事件送出之資訊,必須同時經由該函數之正規管道送達——整批性錯誤經 `Promise.reject`,逐筆失敗經該筆之 `err` 欄位,操作結果經 resolve 值。判準為:**把全部事件移除之後,呼叫端仍能取得完整資訊**。不得出現「錯誤訊息只在事件裡」之情形。
|
|
142
|
+
|
|
143
|
+
2. **操作行為不得因監聽者之有無而改變。** 同一份程式碼、同一組輸入,於呼叫端有註冊監聽與未註冊監聽兩種情況下,回傳值與 resolve/reject 之選擇必須完全相同。
|
|
144
|
+
|
|
145
|
+
此為 T10 之核心不變式。為此,**所採用之 `EventEmitter` 實作不得具有「`'error'` 事件於無監聽者時將錯誤拋出」之語義**。
|
|
146
|
+
|
|
147
|
+
Node 內建之 `events.EventEmitter` **具有**此語義:`emit('error')` 而無任何監聽者時,該錯誤會被直接拋出。`eventemitter3` 則**無**此語義,無監聽者時僅回傳 `false`。故本規格要求採用後者或其他無此語義之實作;`wsemi` 之 `evem()` 即為 `eventemitter3`,各套件既有之相依已涵蓋,改用不增加相依。
|
|
148
|
+
|
|
149
|
+
**不採「Node 內建 + 每處 `emit` 包 try/catch」之理由**:兩者皆可達成本不變式,但前者由實作結構保證,後者倚賴每一處 `emit` 都記得包覆。漏包一處的症狀是「同一份程式碼只在未註冊監聽之呼叫端身上改變行為」——測試若剛好註冊了監聽即永遠測不到,屬條件性且極難察覺之缺陷。規則應設計成違反不了,而非須時時記得遵守。
|
|
150
|
+
|
|
151
|
+
**Node 該語義之設計理由在本規格下不成立**:其用意為防止錯誤被靜默吞掉,適用於「事件即錯誤唯一出口」之物件(如 socket、stream)。本規格第 1 條已要求事件不得為唯一管道,錯誤恆可經 `Promise.reject` 或逐筆 `err` 取得,故無監聽者不代表錯誤消失,僅代表呼叫端未選擇以集中管道觀察。
|
|
152
|
+
|
|
153
|
+
3. **訂閱函數拋錯不得影響本次操作。** **每一處 `emit` 皆須以 try/catch 包覆**,攔得之例外自行記錄即可,不得往外傳遞。
|
|
154
|
+
|
|
155
|
+
本項獨立於第 2 條,不因採用無「無監聽者即拋出」語義之實作而免除:訂閱函數自身之例外,於任何 `EventEmitter` 實作下皆會沿 `emit()` 往外傳遞。兩者之分工為——第 2 條由**實作選擇**保證,本項由**每處 `emit` 之 try/catch** 保證。
|
|
156
|
+
|
|
157
|
+
4. **須於結果定案之後發出。** 事件發出時,該次結果須已確定——逐筆事件於該筆結果定案後發出,整批事件於整批結果定案後發出。令事件之發出與否、訂閱函數之行為,皆無從改變回傳結果。
|
|
158
|
+
|
|
159
|
+
5. **不得影響控制流。** 事件不得改變 `ok`、各計數欄位、或 resolve/reject 之選擇。
|
|
160
|
+
|
|
161
|
+
#### T10.2 `change` 事件
|
|
162
|
+
|
|
163
|
+
資料**實際異動成功**後發出。
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
ee.emit('change', mode, data, res)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
| 參數 | 內容 |
|
|
170
|
+
|---|---|
|
|
171
|
+
| `mode` | 操作別字串,取該函數之名稱 |
|
|
172
|
+
| `data` | 本次操作之輸入數據;無輸入數據者(如 `delAll`)為 `null` |
|
|
173
|
+
| `res` | 本次操作之回傳結果,形狀同該函數之規格 |
|
|
174
|
+
|
|
175
|
+
- 僅於未發生整批性錯誤時發出;整批 `reject` 者不發出。
|
|
176
|
+
- 逐筆函數以**整批為單位發出一次**,不逐筆發出。
|
|
177
|
+
- `save` 之逐筆插入得另行發出 `mode` 為 `'insert'` 之事件,供呼叫端區辨新增與更新;採用者須於「各套件符合狀態」載明。
|
|
178
|
+
- 讀取函數(`select`、`selectByPk`)不發出本事件。
|
|
179
|
+
|
|
180
|
+
#### T10.3 `error` 事件
|
|
181
|
+
|
|
182
|
+
操作發生錯誤時發出。**六函數皆須發出**,含讀取函數。
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
ee.emit('error', mode, data, err)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
| 參數 | 內容 |
|
|
189
|
+
|---|---|
|
|
190
|
+
| `mode` | 操作別字串,取該函數之名稱 |
|
|
191
|
+
| `data` | 本次操作之輸入數據;無輸入數據者(如 `delAll`、`select`、`selectByPk`)為 `null` |
|
|
192
|
+
| `err` | 錯誤訊息**字串**,內容與正規管道所送出者一致;正規管道送出 `Error` 物件者,取其 `message` |
|
|
193
|
+
|
|
194
|
+
發出時機:
|
|
195
|
+
|
|
196
|
+
| 錯誤類別 | 時機 | `err` 內容 |
|
|
197
|
+
|---|---|---|
|
|
198
|
+
| 整批性錯誤 | 於 `reject` **之前**發出 | 與 reject 之訊息一致 |
|
|
199
|
+
| 逐筆失敗 | 於該筆結果定案後發出,每筆一次 | 與該筆 `err` 欄位一致 |
|
|
200
|
+
|
|
201
|
+
- **正常結果不得發出本事件。** `insert` 全數已存在、`save` 合併後內容相同而未寫入、`del` 主鍵未命中、`delAll` 條件無命中、`selectByPk` 查無數據,皆為正常結果而非錯誤。誤發會使監聽者把常態當異常。
|
|
202
|
+
- **收到本事件不表示該次呼叫失敗。** 逐筆失敗時整批仍 resolve;欲判斷整批成敗仍依 T4 與「呼叫端判讀準則」。
|
|
203
|
+
- 同一次呼叫若既有逐筆失敗又整批 resolve,則逐筆 `error` 先於整批 `change` 發出——依 T10.1 第 4 項,各自於其結果定案時發出,順序由此自然決定。
|
|
204
|
+
|
|
205
|
+
#### T10.4 事件速查
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
change (mode, data, res) 資料實際異動成功後,整批一次
|
|
209
|
+
error (mode, data, err) 整批性錯誤於 reject 前;逐筆失敗於該筆定案後,每筆一次
|
|
210
|
+
|
|
211
|
+
mode 六函數之名稱;專屬函數取其自身名稱
|
|
212
|
+
data 輸入數據,無者為 null
|
|
213
|
+
err 字串
|
|
214
|
+
```
|
|
215
|
+
|
|
110
216
|
---
|
|
111
217
|
|
|
112
218
|
## 各函數規格
|
|
@@ -248,8 +354,10 @@ delAll(find) → { n, nDeleted, ok }
|
|
|
248
354
|
7. `save` 之「內容相同」採**合併後比對**,基準寫進註解。
|
|
249
355
|
8. `del` 對未帶有效主鍵者不送查詢,直接回 `ok: 0` + `err`。
|
|
250
356
|
9. 輸入無效之回傳依 T5;讀取函數無副作用(T9)。
|
|
251
|
-
10.
|
|
252
|
-
11.
|
|
357
|
+
10. 提供 `opt.autoGenPk` 且預設為 `true`;為 `false` 時不產生主鍵值,未帶有效主鍵者 `reject`;不得於 `option` 逐次覆寫。符合 T6 例外而完全不提供該設定者(主鍵具業務語義,或套件無從得知主鍵型別),已於「各套件符合狀態」載明所依據之情形與理由。
|
|
358
|
+
11. README 依 T8 宣告併發保證範圍,無法保證者載明後果、實測依據與迴避方式。
|
|
359
|
+
12. 依 T10 發出 `change` 與 `error` 事件,參數形狀為 `(mode, data, res)` 與 `(mode, data, err)`;所採用之 `EventEmitter` 無「`'error'` 於無監聽者時拋出」之語義;每一處 `emit` 皆以 try/catch 包覆;正常結果不發出 `error`;事件所送出之資訊皆另有正規管道,移除全部事件後呼叫端仍能取得完整資訊。
|
|
360
|
+
13. 測試須涵蓋:同批重複主鍵、主鍵不存在、合併後內容相同、只給部份欄位且值相同、`autoInsert` 兩種取值、`autoGenPk` 兩種取值(含 `false` 且未帶主鍵須 `reject`)、單筆失敗、未帶有效主鍵、`delAll` 帶條件且僅部份命中,以及**同一操作於有註冊與未註冊 `error` 監聽兩種情況下回傳值完全相同**。
|
|
253
361
|
|
|
254
362
|
---
|
|
255
363
|
|
|
@@ -257,9 +365,13 @@ delAll(find) → { n, nDeleted, ok }
|
|
|
257
365
|
|
|
258
366
|
### w-orm-lmdb
|
|
259
367
|
|
|
260
|
-
主鍵欄位為 `id
|
|
368
|
+
主鍵欄位為 `id`,為無業務語義之識別碼。主鍵欄位目前**固定為 `id`,尚未支援由呼叫端指定**。
|
|
369
|
+
|
|
370
|
+
已符合 T1–T10 與六函數全部規格,無待處理項目。
|
|
371
|
+
|
|
372
|
+
T10 之實作:`EventEmitter` 採 `wsemi` 之 `evem()`(即 `eventemitter3`),本即無「`'error'` 於無監聽者時拋出」之語義,符合 T10.1 第 2 條且無須改動。全部事件收斂於共用之 `emitChange(mode, data, res)` 與 `emitError(mode, data, err)` 兩函數,try/catch 由該二處統一保證,`err` 一律經 `getErrMsg` 轉為字串。`change` 之逐筆函數以整批為單位發出一次,`save` 之逐筆插入另發 `mode` 為 `insert` 之事件;`error` 於六函數皆發出,整批性錯誤於 `reject` 之前、逐筆失敗於該筆定案後各一次。
|
|
261
373
|
|
|
262
|
-
|
|
374
|
+
`opt.autoGenPk` 預設為 `true`,以 `genIDSeq()` 產生主鍵值;為 `false` 時 `insert` 與 `save` 皆不補值,未帶有效主鍵者以 `Promise.reject` 拋出整批性錯誤。主鍵檢查於任何寫入之前一次完成,故整批 `reject` 時同批之有效筆數亦不會被寫入。`del` 不受此設定影響。
|
|
263
375
|
|
|
264
376
|
`save` 之「內容相同」判定採合併後比對:將待寫入物件深層合併進現值後與現值比對,相同則不寫入。快速路徑之預讀僅用於判斷是否略過寫入,寫入內容一律由寫交易內讀到之現值決定。
|
|
265
377
|
|
|
@@ -269,17 +381,26 @@ T8 已完成:README 已宣告單一行程內保證成立、跨行程因 `lmdb-
|
|
|
269
381
|
|
|
270
382
|
### w-orm-mongodb
|
|
271
383
|
|
|
272
|
-
主鍵欄位為 `id
|
|
384
|
+
主鍵欄位為 `id`,為無業務語義之識別碼。主鍵欄位目前**固定為 `id`,尚未支援由呼叫端指定**,已於類別註解、`selectByPk` 註解與 README 載明。
|
|
273
385
|
|
|
274
|
-
已符合 T1–T9
|
|
386
|
+
已符合 T1–T9 與六函數全部規格。T10 之 `change` 已符合,且已集中於共用之 `emitChange(mode, data, res)`(內含 try/catch),`error` 待新增。
|
|
387
|
+
|
|
388
|
+
T10 待處理:
|
|
389
|
+
|
|
390
|
+
| 項目 | 現況 | 規格 |
|
|
391
|
+
|---|---|---|
|
|
392
|
+
| `error` 事件 | 未發出 | 依 T10.3 新增;既有之 `emitChange` 可直接擴充為共用之事件發出函數 |
|
|
393
|
+
| `EventEmitter` 實作 | `events.EventEmitter`(Node 內建,具「`error` 於無監聽者時拋出」之語義) | 依 T10.1 第 2 條改用 `wsemi` 之 `evem()`(`eventemitter3`),既有相依已涵蓋 |
|
|
394
|
+
|
|
395
|
+
`opt.autoGenPk` 預設為 `true`,以 `genID()` 產生主鍵值——為 62 進位 32 碼之隨機字串(約 2^190 種),碰撞機率遠低於 UUIDv4 之 122 bits;為 `false` 時 `insert`、`save` 與 `insertGfs` 皆不補值,未帶有效主鍵者以 `Promise.reject` 拋出整批性錯誤。主鍵檢查以共用之 `procPk` 於任何寫入之前一次完成,故整批 `reject` 時同批之有效筆數亦不會被寫入。`autoGenPk` 僅為建構層設定,`insert` 與 `save` 之 `option` 未提供覆寫。`del` 不受此設定影響。
|
|
275
396
|
|
|
276
397
|
`save` 之「內容相同」判定即本規格之基準來源:由 MongoDB 於伺服器端將 `$set` 之待寫入物件合併進現值後與現值比對,未寫入即回 `modifiedCount` 為 `0`,比對與寫入於同一原子操作內完成,故不須預讀。
|
|
277
398
|
|
|
278
399
|
T7 之原子性以主鍵之唯一索引達成,索引一律建立且無關閉選項;README 已載明既有資料表尚存重複主鍵時之清除步驟。
|
|
279
400
|
|
|
280
|
-
T8 無須宣告:單一行程內併發與跨行程併發皆成立,故依
|
|
401
|
+
T8 無須宣告:單一行程內併發與跨行程併發皆成立,故依 T8 未於 README 宣告。原子性全由 MongoDB 伺服器端提供,本套件每次操作各自建立連線且不保有行程內狀態,故兩種範圍於後端為同一情形。已測試依據含 2 個獨立行程對相同 20 個主鍵併發 `insert`、`nInserted` 總和為 20 且資料表僅 20 筆,以及 2 個行程對同一主鍵各寫入 20 個不同欄位、40 欄位全數保留。
|
|
281
402
|
|
|
282
|
-
本套件另有 GridFS 專屬函數 `selectByPkGfs`、`insertGfs`、`delGfs`、`delAllGfs`,依 §7
|
|
403
|
+
本套件另有 GridFS 專屬函數 `selectByPkGfs`、`insertGfs`、`delGfs`、`delAllGfs`,依 §7 不在本規格範圍內,惟參數形狀、回傳結構與 `autoGenPk` 行為皆已比照對應之六函數:數據物件為 `{ id, u8a }`,`insertGfs(data)` 收物件或陣列並回 `{ n, nInserted, ok }`、具「已存在則跳過」語義(以 `<cl>.files` 之 `filename` 唯一索引達成),`selectByPkGfs(pk)` 查無回 `null`,`delGfs(data)` 收物件或陣列並回等長陣列,`delAllGfs(find)` 回 `{ n, nDeleted, ok }`。`insertGfs` 一併套用 `autoGenPk`,否則同一實例將出現「一般 `insert` 拒絕、GFS 照補」之分裂,且補出之主鍵呼叫端無從取得。
|
|
283
404
|
|
|
284
405
|
GridFS 之寫入係先寫 chunks 再寫 files 文件,故違反唯一索引時 chunks 已先行寫入,`insertGfs` 於攔得重複鍵錯誤後以該次之 `files_id` 清除所遺留之孤兒 chunks,並以測試斷言重複插入與併發插入後 files 與 chunks 之增量皆為預期值。
|
|
285
406
|
|
|
@@ -287,21 +408,43 @@ GridFS 之寫入係先寫 chunks 再寫 files 文件,故違反唯一索引時
|
|
|
287
408
|
|
|
288
409
|
### w-orm-postgresql
|
|
289
410
|
|
|
290
|
-
|
|
411
|
+
主鍵欄位由建構時之 `opt.pk` 指定,預設為 `time`,**已支援由呼叫端指定**,`select` 以外之五函數皆以該欄位認定主鍵,已於類別註解、`selectByPk` 註解與 README 載明。
|
|
291
412
|
|
|
292
|
-
|
|
293
|
-
主鍵欄位為 `time`,承載觀測時間之業務意義,故採 T6 例外:`insert` 與 `save` 於輸入未帶有效 `time` 時**不補值**,以 `Promise.reject` 拋出整批性錯誤。理由為此類時序資料若自動補入當下時間,將使「呼叫端漏給 `time`」靜默變成「多寫入一筆現在時刻之資料」,且該筆無從與正常資料區辨。`del` 不適用本例外,未帶有效 `time` 者仍依 T6 回該筆 `ok: 0` + `err`。
|
|
413
|
+
已符合 T1–T9 與六函數全部規格。T10 之 `change` 已符合(4 處 `emit` 皆以 try/catch 包覆),`error` 待新增。
|
|
294
414
|
|
|
295
|
-
待處理:
|
|
415
|
+
T10 待處理:
|
|
296
416
|
|
|
297
417
|
| 項目 | 現況 | 規格 |
|
|
298
418
|
|---|---|---|
|
|
299
|
-
|
|
|
419
|
+
| `error` 事件 | 未發出 | 依 T10.3 新增 |
|
|
420
|
+
| `EventEmitter` 實作 | `events.EventEmitter`(Node 內建,具「`error` 於無監聽者時拋出」之語義) | 依 T10.1 第 2 條改用 `wsemi` 之 `evem()`(`eventemitter3`),既有相依已涵蓋 |
|
|
421
|
+
|
|
422
|
+
**不提供`autoGenPk`**,依 T6 之例外辦理,且兩種情形皆成立:
|
|
423
|
+
|
|
424
|
+
1. 預設主鍵 `time` 承載觀測時間之業務意義。此類欄位若自動補值,將使「呼叫端漏給主鍵」靜默變成「以當下之值多寫入一筆」,且該筆無從與正常資料區辨。
|
|
425
|
+
2. **主鍵欄位可由呼叫端指定,其型別則由資料表 schema 決定,套件並不知情**,故無從產生型別相容且符合 T6「須具足夠唯一性」之值:主鍵為 `TIMESTAMPTZ` 時可產生者僅有當下時間,同一毫秒內併發即碰撞,違反 T6「不得採用可預期碰撞之來源」;為 `TEXT` 時方適用隨機字串;為 `INTEGER` 時須倚賴資料庫端之 sequence。三者所需之產生策略互斥,而套件於寫入前無從得知係何者。
|
|
426
|
+
|
|
427
|
+
故縱使呼叫端將主鍵指定為無業務語義之欄位,本套件仍不提供補值,主鍵一律由呼叫端自備。`insert` 與 `save` 於輸入未帶有效主鍵值時,以 `Promise.reject` 拋出整批性錯誤;主鍵檢查於任何寫入之前一次完成,故整批 `reject` 時同批之有效筆數亦不會被寫入。`del` 不受此設定影響,未帶有效主鍵值者仍依 T6 回該筆 `ok: 0` + `err`。
|
|
300
428
|
|
|
301
|
-
|
|
429
|
+
主鍵值之有效性僅要求有給值而不限定型別,理由同上——主鍵欄位得由呼叫端指定而其型別隨欄位而異。型別與欄位不符者由 PostgreSQL 回報 `22P02` 或 `22007`,各函數依其規格分別處置:`selectByPk` 視為主鍵值無效而回傳 `null`,`insert` 與 `save` 為整批性錯誤而 `reject`,`del` 為該筆失敗而回 `ok: 0` + `err`。
|
|
302
430
|
|
|
303
|
-
`save` 之「內容相同」判定採合併後比對:以待寫入物件之非主鍵欄位淺層覆蓋現值後與現值比對。合併取淺層而非深層,係為與後端以 `EXCLUDED` 整欄取代之寫入行為一致——判定基準與實際寫入行為若不一致,`nModified`
|
|
431
|
+
`save` 之「內容相同」判定採合併後比對:以待寫入物件之非主鍵欄位淺層覆蓋現值後與現值比對。合併取淺層而非深層,係為與後端以 `EXCLUDED` 整欄取代之寫入行為一致——判定基準與實際寫入行為若不一致,`nModified` 即無法忠實反映是否真的寫入。快速路徑之預讀僅用於判斷是否略過寫入,寫入內容一律由原子語句自身決定。
|
|
304
432
|
|
|
305
|
-
T7 之原子性以 `ON CONFLICT` 達成:`insert` 為單一 `INSERT ... ON CONFLICT (
|
|
433
|
+
T7 之原子性以 `ON CONFLICT` 達成:`insert` 為單一 `INSERT ... ON CONFLICT (主鍵) DO NOTHING`,取 `rowCount` 為 `nInserted`;`save` 於 `autoInsert` 開啟時為單一 `INSERT ... ON CONFLICT (主鍵) DO UPDATE ... RETURNING (xmax = 0)`,以 `xmax` 區辨本語句插入與衝突後更新,關閉時為單一 `UPDATE ... WHERE 主鍵` 以免無中生有。其前提為主鍵具唯一約束,`createTable` 一律以 `PRIMARY KEY` 建立且無關閉選項;README 之 Upgrading 已載明資料表於他處建立而缺該約束時之補建步驟,以及補建前須先清除重複主鍵值。
|
|
306
434
|
|
|
307
|
-
T8
|
|
435
|
+
T8 無須宣告:單一行程內併發與跨行程併發皆成立,故依 T8 未於 README 宣告。原子性全由 PostgreSQL 伺服器端提供,本套件不保有行程內狀態且每次呼叫各自開啟連線,故兩種範圍於後端為同一情形。已測試依據含 2 個獨立行程對相同 20 個主鍵併發 `insert`、`nInserted` 總和為 20 且資料表僅 20 筆,以及 2 個行程對同一主鍵各寫入 5 個不同欄位、10 欄位全數保留。
|
|
436
|
+
|
|
437
|
+
本套件另有專屬函數 `createTable`,依 §7 不在本規格範圍內,其 `pk` 參數未給時採 `opt.pk`;因 T7 之原子性倚賴主鍵之唯一約束,該參數若給予與 `opt.pk` 不同之欄位,將建出其餘函數無法正確操作之資料表,已於函數註解與 README 載明。
|
|
438
|
+
|
|
439
|
+
### w-orm-reladb
|
|
440
|
+
|
|
441
|
+
待確認撰寫
|
|
442
|
+
<!--
|
|
443
|
+
主鍵欄位由建構時之 `opt.pk` 指定,預設為 `id`,**已支援由呼叫端指定**。已提供 `opt.autoGenPk` 且預設為 `true`,為本規格 T6 之 `autoGenPk` 條款的來源實作。
|
|
444
|
+
|
|
445
|
+
**尚未完成完整合規盤查**,下列為已查證項目,其餘條款待逐項比對後補入:
|
|
446
|
+
|
|
447
|
+
| 項目 | 現況 | 規格 |
|
|
448
|
+
|---|---|---|
|
|
449
|
+
| 單筆直讀函數 | 未提供,僅有 `select`、`insert`、`save`、`del`、`delAll` 五函數 | 依 T1 新增 `selectByPk` |
|
|
450
|
+
| `opt.autoGenPk` | 已提供,預設 `true`;為 `false` 時 `insert` 與 `save` 皆不補值 | 符合 T6,惟未帶有效主鍵時之處置待查證是否為 `reject` | -->
|