w-serv-orm 1.1.16 → 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.
- package/README.md +109 -0
- package/dist/w-serv-orm.umd.js +2 -2
- package/dist/w-serv-orm.umd.js.map +1 -1
- package/docs/WServOrm.html +9 -4
- package/docs/WServOrm.mjs.html +94 -3
- package/docs/baseline.mjs.html +624 -0
- package/docs/global.html +1622 -0
- package/docs/index.html +2 -2
- package/package.json +1 -1
- package/src/WServOrm.mjs +92 -1
- package/src/baseline.mjs +552 -0
- package/src/getMapOrm.mjs +188 -10
- package/src/getProc.mjs +13 -4
- package/test/api-guard.test.mjs +401 -0
- package/test/api-keepuser.test.mjs +131 -0
- package/test/tools/memOrm.mjs +122 -0
- package/test/unit-baseline.test.mjs +766 -0
- package/test/unit-guard.test.mjs +952 -0
- package/test/unit-keepuser.test.mjs +307 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import assert from 'assert'
|
|
3
|
+
import WOrm from 'w-orm-lowdb/src/WOrmLowdb.mjs'
|
|
4
|
+
import build from 'w-data-collector/src/build.mjs'
|
|
5
|
+
import WServOrm from '../src/WServOrm.mjs'
|
|
6
|
+
import { buildBaseline, compareBaseline, pickWriteRow, getRejectKey } from '../src/baseline.mjs'
|
|
7
|
+
import { getReject, quiet } from './tools/ormStub.mjs'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
//規格: 多人同時編輯之寫入契約 V2、V4、V5、V6、V7、V9、V10、V14; 規則帳本 R-GUARD-01、R-ONLYMOD-01
|
|
11
|
+
//以真ORM(w-orm-lowdb)經procOrm、withTabLocks、guardWrite驗證只改不增、刪除未命中、鎖內原子與基準之傳輸等價; 替身ORM版(含逐筆事後偵測、opt驗證)見 unit-guard.test.mjs
|
|
12
|
+
//
|
|
13
|
+
//規格句對照(spec = C:\opensrcOper\dspec\多人同時編輯之寫入契約.md §13 驗收):
|
|
14
|
+
// R01-R04、R06 V6「修改時該列已被刪 → rowNotFound, 列不復活」 R05、R08 V7「刪除已不存在之列(帶或不帶基準) → rowNotFound」 R07 §5「逐列存檔之基準」、V6
|
|
15
|
+
// R09 V10「前端算之基準(經實際傳輸路徑)=伺服端新鮮讀取所算」、V5「系統寫伺服器專有欄位後, 後台存檔可存, 且該欄維持系統所寫之值」
|
|
16
|
+
// R10 V2/V3/V4「外部同步相同內容基準不變 / 改變內容被拒 / 系統直寫雙方欄位後舊基準被拒」 R11 V9「他人重排被拒 / 系統重編號可存」
|
|
17
|
+
// R12 V14「並行: 比對與寫入間插入他人寫入 → 不可能發生」 R13 §7「多表依表名碼元排序取鎖」 R14 實作者補強(逾 $in 上限之大批), 見規則帳本 R-ONLYMOD-01 規則 11
|
|
18
|
+
let fd = './test/_tmp/api-guard'
|
|
19
|
+
|
|
20
|
+
let wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
21
|
+
|
|
22
|
+
function mkTab(keyTable) {
|
|
23
|
+
return build({
|
|
24
|
+
keyTable,
|
|
25
|
+
settings: {
|
|
26
|
+
id: { pk: true, name: '主鍵', type: 'STRING' },
|
|
27
|
+
name: { name: '名稱', type: 'STRING' },
|
|
28
|
+
blocked: { name: '封鎖', type: 'STRING' },
|
|
29
|
+
pw: { name: '密碼', type: 'STRING' },
|
|
30
|
+
tags: { name: '標籤', type: 'STRING' },
|
|
31
|
+
order: { name: '排序', type: 'INTEGER' },
|
|
32
|
+
userIdUpdate: { name: '更新者', type: 'STRING' },
|
|
33
|
+
timeCreate: { name: '創建時間', type: 'STRING' },
|
|
34
|
+
timeUpdate: { name: '更新時間', type: 'STRING' },
|
|
35
|
+
},
|
|
36
|
+
}, { useCreateStorage: false })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
//契約: blocked為雙方欄位(後台可改、系統亦寫), pw為伺服器專有, order為重編型之名次
|
|
40
|
+
let CT = { table: 'items', pk: 'id', fields: ['name', 'blocked', 'tags'], serverOnly: ['pw'], orderKey: 'order' }
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
describe('procOrm opt與guardWrite接w-orm-lowdb', function() {
|
|
44
|
+
|
|
45
|
+
let so = null
|
|
46
|
+
let proc = (mode, input, opt, tab = 'items') => so.procOrm('u1', tab, mode, input, opt)
|
|
47
|
+
let sel = (tab = 'items') => so.woItems[tab].select({})
|
|
48
|
+
let byId = async (id, tab = 'items') => (await so.woItems[tab].select({ id: { $in: [id] } }))[0]
|
|
49
|
+
|
|
50
|
+
before(function() {
|
|
51
|
+
fs.rmSync(fd, { recursive: true, force: true })
|
|
52
|
+
fs.mkdirSync(fd, { recursive: true })
|
|
53
|
+
so = WServOrm({ items: mkTab('items'), items2: mkTab('items2') }, WOrm, `${fd}/db.json`, 'worm')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
beforeEach(async function() {
|
|
57
|
+
await so.woItems.items.delAll({})
|
|
58
|
+
await so.woItems.items2.delAll({})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
after(async function() {
|
|
62
|
+
await so.closeAll()
|
|
63
|
+
fs.rmSync(fd, { recursive: true, force: true })
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('R01 修改不存在之列: rowNotFound, 列未被建立(V6); 未帶opt之既有行為仍為新增', async function() {
|
|
67
|
+
let err = await quiet(() => getReject(proc('save', [{ id: 'ghost', name: 'x' }], { mustExist: true })))
|
|
68
|
+
assert.strict.equal(err, 'rowNotFound')
|
|
69
|
+
assert.strict.equal((await sel()).length, 0)
|
|
70
|
+
|
|
71
|
+
//既有行為(不變): save對不存在之列自動新增
|
|
72
|
+
let r = await proc('save', [{ id: 'ghost', name: 'x' }])
|
|
73
|
+
assert.deepStrictEqual(r, [{ id: 'ghost' }])
|
|
74
|
+
assert.strict.equal((await sel()).length, 1)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('R02 修改已被他人刪除之列: rowNotFound且列不復活(V6)', async function() {
|
|
78
|
+
await proc('save', [{ id: 'r1', name: 'A' }])
|
|
79
|
+
await proc('del', [{ id: 'r1' }]) //他人先刪
|
|
80
|
+
let err = await quiet(() => getReject(proc('save', [{ id: 'r1', name: 'A2' }], { mustExist: true })))
|
|
81
|
+
assert.strict.equal(err, 'rowNotFound')
|
|
82
|
+
assert.strict.equal(await byId('r1'), undefined)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('R03 多列中一列不存在: 零寫入, 存在之列之值與更新時間皆不變', async function() {
|
|
86
|
+
await proc('save', [{ id: 'r1', name: 'A' }, { id: 'r2', name: 'B' }])
|
|
87
|
+
let before = JSON.stringify(await sel())
|
|
88
|
+
await wait(5)
|
|
89
|
+
let err = await quiet(() => getReject(proc('save', [{ id: 'r1', name: 'A2' }, { id: 'ghost', name: 'x' }, { id: 'r2', name: 'B2' }], { mustExist: true })))
|
|
90
|
+
assert.strict.equal(err, 'rowNotFound')
|
|
91
|
+
assert.strict.equal(JSON.stringify(await sel()), before)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('R04 全部存在: 修改成功, 未給之欄位保留, 系統欄位依既有規則補值(userIdUpdate、timeUpdate), timeCreate不變', async function() {
|
|
95
|
+
await proc('save', [{ id: 'r1', name: 'A', pw: 'secret' }])
|
|
96
|
+
let v0 = await byId('r1')
|
|
97
|
+
await wait(5)
|
|
98
|
+
let r = await so.procOrm('u9', 'items', 'save', [{ id: 'r1', name: 'A2' }], { mustExist: true })
|
|
99
|
+
assert.deepStrictEqual(r, [{ id: 'r1' }])
|
|
100
|
+
let v1 = await byId('r1')
|
|
101
|
+
assert.strict.equal(v1.name, 'A2')
|
|
102
|
+
assert.strict.equal(v1.pw, 'secret')
|
|
103
|
+
assert.strict.equal(v1.userIdUpdate, 'u9')
|
|
104
|
+
assert.notStrictEqual(v1.timeUpdate, v0.timeUpdate)
|
|
105
|
+
assert.strict.equal(v1.timeCreate, v0.timeCreate)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('R05 刪除不存在之列: rowNotFound(V7), 多列中一列不存在則零刪除; 全部存在則刪除; 未帶opt時仍回逐筆{n:0,nDeleted:0,ok:1}', async function() {
|
|
109
|
+
await proc('save', [{ id: 'r1', name: 'A' }, { id: 'r2', name: 'B' }])
|
|
110
|
+
let e1 = await quiet(() => getReject(proc('del', [{ id: 'ghost' }], { mustExist: true })))
|
|
111
|
+
assert.strict.equal(e1, 'rowNotFound')
|
|
112
|
+
let e2 = await quiet(() => getReject(proc('del', [{ id: 'r1' }, { id: 'ghost' }], { mustExist: true })))
|
|
113
|
+
assert.strict.equal(e2, 'rowNotFound')
|
|
114
|
+
assert.strict.equal((await sel()).length, 2, '零刪除')
|
|
115
|
+
let r = await proc('del', [{ id: 'r1' }, { id: 'r2' }], { mustExist: true })
|
|
116
|
+
assert.deepStrictEqual(r.map((v) => v.nDeleted), [1, 1])
|
|
117
|
+
assert.strict.equal((await sel()).length, 0)
|
|
118
|
+
let r0 = await proc('del', [{ id: 'r1' }])
|
|
119
|
+
assert.deepStrictEqual(r0, [{ n: 0, nDeleted: 0, ok: 1 }])
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('R06 修改時未帶主鍵之列不補號新增: rowNotFound(表列數不變)', async function() {
|
|
123
|
+
await proc('save', [{ id: 'r1', name: 'A' }])
|
|
124
|
+
let err = await quiet(() => getReject(proc('save', [{ name: 'no-id' }], { mustExist: true })))
|
|
125
|
+
assert.strict.equal(err, 'rowNotFound')
|
|
126
|
+
assert.strict.equal((await sel()).length, 1)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('R07 guardWrite逐列存檔(帶基準): 該列已被刪 → rowNotFound且零寫入(V6); 該列被改 → baselineMismatch; 未改 → 寫入成功', async function() {
|
|
130
|
+
await proc('save', [{ id: 'r1', name: 'A', order: 1 }, { id: 'r2', name: 'B', order: 2 }])
|
|
131
|
+
let rowsLoaded = await sel()
|
|
132
|
+
let blOne = buildBaseline(rowsLoaded.filter((v) => v.id === 'r1'), CT)
|
|
133
|
+
let readOne = () => so.woItems.items.select({ id: { $in: ['r1'] } })
|
|
134
|
+
let save = (name, baseline) => so.guardWrite({
|
|
135
|
+
tables: ['items'],
|
|
136
|
+
read: readOne,
|
|
137
|
+
baseline,
|
|
138
|
+
contract: CT,
|
|
139
|
+
write: () => proc('save', [{ id: 'r1', name }], { mustExist: true }),
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
assert.deepStrictEqual(await save('A2', blOne), [{ id: 'r1' }])
|
|
143
|
+
assert.strict.equal((await byId('r1')).name, 'A2')
|
|
144
|
+
let e1 = await quiet(() => getReject(save('A3', blOne)))
|
|
145
|
+
assert.strict.equal(e1.key, 'baselineMismatch', '前一次已改, 舊基準被拒')
|
|
146
|
+
await proc('del', [{ id: 'r1' }])
|
|
147
|
+
let e2 = await quiet(() => getReject(save('A4', blOne)))
|
|
148
|
+
assert.strict.equal(e2.key, 'rowNotFound')
|
|
149
|
+
assert.strict.equal(await byId('r1'), undefined, '已刪之列不復活')
|
|
150
|
+
//不帶基準之寫入(外部應用型): 取鎖、只改不增, 該列已不存在 → rowNotFound
|
|
151
|
+
let e3 = await quiet(() => getReject(save('A5', undefined)))
|
|
152
|
+
assert.strict.equal(e3, 'rowNotFound')
|
|
153
|
+
assert.strict.equal(await byId('r1'), undefined)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('R08 刪除(帶或不帶基準)已不存在之列皆rowNotFound(V7)', async function() {
|
|
157
|
+
await proc('save', [{ id: 'r1', name: 'A', order: 1 }])
|
|
158
|
+
let bl = buildBaseline(await sel(), CT)
|
|
159
|
+
let del = (baseline) => so.guardWrite({
|
|
160
|
+
tables: ['items'],
|
|
161
|
+
read: () => so.woItems.items.select({ id: { $in: ['r1'] } }),
|
|
162
|
+
baseline,
|
|
163
|
+
contract: CT,
|
|
164
|
+
write: () => proc('del', [{ id: 'r1' }], { mustExist: true }),
|
|
165
|
+
})
|
|
166
|
+
let r = await del(bl)
|
|
167
|
+
assert.strict.equal(r[0].nDeleted, 1)
|
|
168
|
+
let e1 = await quiet(() => getReject(del(bl)))
|
|
169
|
+
assert.strict.equal(e1.key, 'rowNotFound', '帶基準: 該列已不在')
|
|
170
|
+
let e2 = await quiet(() => getReject(del(undefined)))
|
|
171
|
+
assert.strict.equal(e2, 'rowNotFound', '不帶基準: 以存在檢查回報')
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('R09 傳輸等價(V10): 後端讀回之列算出之基準, 與經JSON傳輸之列算出者相同; 系統欄位(userIdUpdate、timeUpdate)與serverOnly變動不影響(V5)', async function() {
|
|
175
|
+
await proc('save', [{ id: 'r2', name: 'B', order: 2, tags: '[x]', blocked: 'n' }, { id: 'r1', name: 'A', order: 1, pw: 'p1' }])
|
|
176
|
+
let server = await sel()
|
|
177
|
+
let wire = JSON.parse(JSON.stringify(server))
|
|
178
|
+
let bl = buildBaseline(server, CT)
|
|
179
|
+
assert.deepStrictEqual(buildBaseline(wire, CT), bl)
|
|
180
|
+
assert.deepStrictEqual(bl.map((v) => v[0]), ['r1', 'r2'])
|
|
181
|
+
//系統寫入伺服器專有欄位(例如密碼)與系統欄位: 基準不變, 後台以舊基準仍可存
|
|
182
|
+
await wait(5)
|
|
183
|
+
await so.procOrm('system', 'items', 'save', [{ id: 'r1', pw: 'changed-by-system' }])
|
|
184
|
+
assert.deepStrictEqual(compareBaseline(await sel(), bl, CT), { ok: true })
|
|
185
|
+
let r = await so.guardWrite({
|
|
186
|
+
tables: ['items'],
|
|
187
|
+
read: () => sel(),
|
|
188
|
+
baseline: bl,
|
|
189
|
+
contract: CT,
|
|
190
|
+
write: () => proc('save', [{ id: 'r1', name: 'A-admin' }], { mustExist: true }),
|
|
191
|
+
})
|
|
192
|
+
assert.deepStrictEqual(r, [{ id: 'r1' }])
|
|
193
|
+
let v = await byId('r1')
|
|
194
|
+
assert.strict.equal(v.pw, 'changed-by-system', '後台存檔未採用伺服器專有欄位, 維持系統所寫之值')
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('R09b V5(外審sol#4): 客戶端持舊列(含舊之伺服器專有欄位pw)整列送出時, 寫入前經pickWriteRow白名單則pw維持系統所寫之值; 不經白名單直接寫入則被回寫成舊值(說明白名單之必要)', async function() {
|
|
198
|
+
await proc('save', [{ id: 'r1', name: 'A', pw: 'old', order: 1, blocked: 'n' }])
|
|
199
|
+
let loaded = (await sel())[0] //客戶端載入之整列, 含舊pw
|
|
200
|
+
let bl = buildBaseline([loaded], CT)
|
|
201
|
+
await so.procOrm('system', 'items', 'save', [{ id: 'r1', pw: 'new-by-system' }], { mustExist: true })
|
|
202
|
+
let draft = { ...loaded, name: 'A-edited' } //客戶端整列送出(含舊pw)
|
|
203
|
+
|
|
204
|
+
let guardSave = (row) => so.guardWrite({
|
|
205
|
+
tables: ['items'],
|
|
206
|
+
read: () => sel(),
|
|
207
|
+
baseline: bl,
|
|
208
|
+
contract: CT,
|
|
209
|
+
write: () => proc('save', [row], { mustExist: true }),
|
|
210
|
+
})
|
|
211
|
+
//經白名單: 基準比對通過(pw不入基準), pw維持系統值
|
|
212
|
+
await guardSave(pickWriteRow(draft, CT))
|
|
213
|
+
let v = await byId('r1')
|
|
214
|
+
assert.strict.equal(v.name, 'A-edited')
|
|
215
|
+
assert.strict.equal(v.pw, 'new-by-system')
|
|
216
|
+
//不經白名單(對照): 同一基準仍可存, 但pw被回寫成舊值
|
|
217
|
+
await so.procOrm('system', 'items', 'save', [{ id: 'r1', pw: 'new2-by-system' }], { mustExist: true })
|
|
218
|
+
let bl2 = buildBaseline(await sel(), CT)
|
|
219
|
+
await so.guardWrite({ tables: ['items'], read: () => sel(), baseline: bl2, contract: CT, write: () => proc('save', [{ ...draft, name: 'A-naive' }], { mustExist: true }) })
|
|
220
|
+
assert.strict.equal((await byId('r1')).pw, 'old', '不經白名單則舊值被回寫(此即pickWriteRow存在之理由)')
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('R10 雙方欄位被系統改動後, 持舊基準之後台存檔被拒(V4); 外部同步以相同內容整批重寫(delAll加insert)則基準不變(V2); 外部同步改內容則被拒(V3)', async function() {
|
|
224
|
+
await proc('save', [{ id: 'r1', name: 'A', blocked: 'n', order: 1 }, { id: 'r2', name: 'B', blocked: 'n', order: 2 }])
|
|
225
|
+
let bl = buildBaseline(await sel(), CT)
|
|
226
|
+
let tryAdmin = () => so.guardWrite({ tables: ['items'], read: () => sel(), baseline: bl, contract: CT, write: async () => 'saved' })
|
|
227
|
+
|
|
228
|
+
//外部同步: 內容相同(系統欄位之時間必不同)
|
|
229
|
+
let rowsSame = [{ id: 'r1', name: 'A', blocked: 'n', order: 1 }, { id: 'r2', name: 'B', blocked: 'n', order: 2 }]
|
|
230
|
+
await wait(5)
|
|
231
|
+
await so.withTabLocks(['items'], async () => {
|
|
232
|
+
await so.procOrm('sync', 'items', 'delAll', {})
|
|
233
|
+
await so.procOrm('sync', 'items', 'insert', rowsSame)
|
|
234
|
+
})
|
|
235
|
+
assert.strict.equal(await tryAdmin(), 'saved', '內容不變, 後台以同步前之基準可存(V2)')
|
|
236
|
+
|
|
237
|
+
//外部同步: 內容改變
|
|
238
|
+
await so.withTabLocks(['items'], async () => {
|
|
239
|
+
await so.procOrm('sync', 'items', 'delAll', {})
|
|
240
|
+
await so.procOrm('sync', 'items', 'insert', [{ id: 'r1', name: 'A-ext', blocked: 'n', order: 1 }, rowsSame[1]])
|
|
241
|
+
})
|
|
242
|
+
let e1 = await quiet(() => getReject(tryAdmin()))
|
|
243
|
+
assert.strict.equal(e1.key, 'baselineMismatch')
|
|
244
|
+
assert.deepStrictEqual(e1.changed, ['r1'])
|
|
245
|
+
|
|
246
|
+
//系統直寫雙方欄位(封鎖)
|
|
247
|
+
let bl2 = buildBaseline(await sel(), CT)
|
|
248
|
+
await so.withTabLocks(['items'], () => so.procOrm('system', 'items', 'save', [{ id: 'r2', blocked: 'y' }], { mustExist: true }))
|
|
249
|
+
let e2 = await quiet(() => getReject(so.guardWrite({ tables: ['items'], read: () => sel(), baseline: bl2, contract: CT, write: async () => 'saved' })))
|
|
250
|
+
assert.strict.equal(e2.key, 'baselineMismatch')
|
|
251
|
+
assert.deepStrictEqual(e2.changed, ['r2'])
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('R11 他人重排被偵測、系統重編號不被偵測(V9)', async function() {
|
|
255
|
+
await proc('save', [{ id: 'r1', name: 'A', order: 1 }, { id: 'r2', name: 'B', order: 2 }, { id: 'r3', name: 'C', order: 3 }])
|
|
256
|
+
let bl = buildBaseline(await sel(), CT)
|
|
257
|
+
let g = () => so.guardWrite({ tables: ['items'], read: () => sel(), baseline: bl, contract: CT, write: async () => 'saved' })
|
|
258
|
+
|
|
259
|
+
await proc('save', [{ id: 'r1', order: 10 }, { id: 'r2', order: 20 }, { id: 'r3', order: 30 }])
|
|
260
|
+
assert.strict.equal(await g(), 'saved', '重編號(名次不變)可存')
|
|
261
|
+
|
|
262
|
+
await proc('save', [{ id: 'r1', order: 20 }, { id: 'r2', order: 10 }])
|
|
263
|
+
let e = await quiet(() => getReject(g()))
|
|
264
|
+
assert.strict.equal(e.key, 'baselineMismatch')
|
|
265
|
+
assert.strict.equal(e.reordered, true)
|
|
266
|
+
assert.deepStrictEqual(e.changed, [])
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('R12 原子(V14, 真ORM): 比對與寫入之間插不進他人之寫入(withTabLocks與另一guardWrite), 他人於其後才執行; 最終值為最後取得鎖者之值', async function() {
|
|
270
|
+
await proc('save', [{ id: 'r1', name: 'orig', order: 1 }])
|
|
271
|
+
let bl = buildBaseline(await sel(), CT)
|
|
272
|
+
let log = []
|
|
273
|
+
let g = so.guardWrite({
|
|
274
|
+
tables: ['items'],
|
|
275
|
+
read: async () => {
|
|
276
|
+
log.push('g:read-start')
|
|
277
|
+
await wait(60)
|
|
278
|
+
log.push('g:read-end')
|
|
279
|
+
return sel()
|
|
280
|
+
},
|
|
281
|
+
baseline: bl,
|
|
282
|
+
contract: CT,
|
|
283
|
+
write: async () => {
|
|
284
|
+
log.push('g:write-start')
|
|
285
|
+
await wait(60)
|
|
286
|
+
await proc('save', [{ id: 'r1', name: 'from-g' }], { mustExist: true })
|
|
287
|
+
log.push('g:write-end')
|
|
288
|
+
},
|
|
289
|
+
})
|
|
290
|
+
await wait(15)
|
|
291
|
+
let x = so.withTabLocks(['items'], async () => {
|
|
292
|
+
log.push('x:in')
|
|
293
|
+
await proc('save', [{ id: 'r1', name: 'from-x' }])
|
|
294
|
+
log.push('x:out')
|
|
295
|
+
})
|
|
296
|
+
let y = so.guardWrite({
|
|
297
|
+
tables: ['items'],
|
|
298
|
+
read: () => sel(),
|
|
299
|
+
baseline: bl,
|
|
300
|
+
contract: CT,
|
|
301
|
+
write: async () => {
|
|
302
|
+
log.push('y:write')
|
|
303
|
+
},
|
|
304
|
+
}).catch((e) => log.push(`y:${e.key}`))
|
|
305
|
+
await Promise.all([g, x, y])
|
|
306
|
+
assert.deepStrictEqual(log, ['g:read-start', 'g:read-end', 'g:write-start', 'g:write-end', 'x:in', 'x:out', 'y:baselineMismatch'])
|
|
307
|
+
assert.strict.equal((await byId('r1')).name, 'from-x')
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
it('R05b 刪除之同批重複主鍵(真ORM, mustExist): 整批拒絕且零刪除; getRejectKey可統一取得兩種reject形狀之拒收鍵', async function() {
|
|
311
|
+
await proc('save', [{ id: 'r1', name: 'A' }, { id: 'r2', name: 'B' }])
|
|
312
|
+
let err = await quiet(() => getReject(proc('del', [{ id: 'r1' }, { id: 'r1' }], { mustExist: true })))
|
|
313
|
+
assert.strict.equal(err, 'invalid input of del, duplicate id: data[1]')
|
|
314
|
+
assert.strict.equal((await sel()).length, 2, '零刪除')
|
|
315
|
+
//字串reject(procOrm)與Error reject(guardWrite)之拒收鍵
|
|
316
|
+
let e1 = await quiet(() => getReject(proc('del', [{ id: 'ghost' }], { mustExist: true })))
|
|
317
|
+
assert.strict.equal(getRejectKey(e1), 'rowNotFound')
|
|
318
|
+
let e2 = await quiet(() => getReject(so.guardWrite({ tables: ['items'], read: () => sel(), baseline: [['r1', 'h2:0123456789abcd']], contract: CT, write: async () => 1 })))
|
|
319
|
+
assert.strict.equal(getRejectKey(e2), 'baselineFormat')
|
|
320
|
+
let e3 = await quiet(() => getReject(so.guardWrite({
|
|
321
|
+
tables: ['items'],
|
|
322
|
+
read: async () => {
|
|
323
|
+
throw new Error('plain failure')
|
|
324
|
+
},
|
|
325
|
+
write: async () => 1
|
|
326
|
+
})))
|
|
327
|
+
assert.strict.equal(getRejectKey(e3), null)
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it('R15 V8之參考流程(整表存檔, 真ORM): 刪除集合=基準內且在scope內之id − 送出之id, 他人載入後新增之範圍外列不被刪; 若誤用「庫內全部 − 送出」則會刪到(說明正確作法)', async function() {
|
|
331
|
+
let CS = { table: 'items', pk: 'id', fields: ['name'], scope: (r) => r.isApp === 'y' }
|
|
332
|
+
await proc('save', [
|
|
333
|
+
{ id: 't1', name: 'app1', isApp: 'y' }, { id: 't2', name: 'app2', isApp: 'y' }, { id: 's1', name: 'sess1', isApp: 'n' },
|
|
334
|
+
])
|
|
335
|
+
let loaded = await sel()
|
|
336
|
+
let bl = buildBaseline(loaded, CS)
|
|
337
|
+
assert.deepStrictEqual(bl.map((v) => v[0]), ['t1', 't2'])
|
|
338
|
+
await proc('save', [{ id: 's2', name: 'sess-after-load', isApp: 'n' }]) //載入後他人登入新增(範圍外)
|
|
339
|
+
|
|
340
|
+
let sent = [{ id: 't1', name: 'app1-edited', isApp: 'y' }] //管理員存檔: 改 t1, 刪 t2
|
|
341
|
+
let write = (delIds) => async () => procOrm2('items', { diff: sent, del: delIds.map((id) => ({ id })) })
|
|
342
|
+
let procOrm2 = (tab, input) => so.procOrm('admin', tab, 'mix', input)
|
|
343
|
+
|
|
344
|
+
//錯誤作法(對照): 庫內全部 − 送出 → 刪到 s1、s2
|
|
345
|
+
let wrongDel = (await sel()).map((v) => v.id).filter((id) => !sent.some((s) => s.id === id))
|
|
346
|
+
assert.deepStrictEqual(wrongDel.sort(), ['s1', 's2', 't2'], '錯誤作法會把範圍外之列算入刪除集合')
|
|
347
|
+
|
|
348
|
+
//正確作法: 基準內 − 送出
|
|
349
|
+
let rightDel = bl.map((v) => v[0]).filter((id) => !sent.some((s) => s.id === id))
|
|
350
|
+
assert.deepStrictEqual(rightDel, ['t2'])
|
|
351
|
+
await so.guardWrite({ tables: ['items'], read: () => sel(), baseline: bl, contract: CS, write: write(rightDel) })
|
|
352
|
+
let left = (await sel()).map((v) => v.id).sort()
|
|
353
|
+
assert.deepStrictEqual(left, ['s1', 's2', 't1'])
|
|
354
|
+
assert.strict.equal((await byId('t1')).name, 'app1-edited')
|
|
355
|
+
|
|
356
|
+
//載入後他人新增範圍內之列 → 整次拒收
|
|
357
|
+
let bl2 = buildBaseline(await sel(), CS)
|
|
358
|
+
await proc('save', [{ id: 't9', name: 'app9', isApp: 'y' }])
|
|
359
|
+
let e = await quiet(() => getReject(so.guardWrite({ tables: ['items'], read: () => sel(), baseline: bl2, contract: CS, write: write([]) })))
|
|
360
|
+
assert.strict.equal(e.key, 'baselineMismatch')
|
|
361
|
+
assert.deepStrictEqual(e.added, ['t9'])
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
it('R14 大批(真ORM): 超過共用條件檢核之$in上限(10000)之列數, 存在檢查分批仍可修改; 缺一列者零寫入', async function() {
|
|
365
|
+
this.timeout(120000)
|
|
366
|
+
let rows = []
|
|
367
|
+
for (let i = 0; i < 10500; i++) {
|
|
368
|
+
rows.push({ id: `id-${i}`, name: 'A', order: i })
|
|
369
|
+
}
|
|
370
|
+
await proc('save', rows)
|
|
371
|
+
let r = await proc('save', rows.map((v) => ({ id: v.id, name: 'B' })), { mustExist: true })
|
|
372
|
+
assert.strict.equal(r.length, 10500)
|
|
373
|
+
assert.strict.equal((await byId('id-10499')).name, 'B')
|
|
374
|
+
let err = await quiet(() => getReject(proc('save', [...rows.map((v) => ({ id: v.id, name: 'C' })), { id: 'ghost', name: 'C' }], { mustExist: true })))
|
|
375
|
+
assert.strict.equal(err, 'rowNotFound')
|
|
376
|
+
assert.strict.equal((await byId('id-0')).name, 'B', '零寫入')
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
it('R13 多表取鎖(真ORM): 交錯之兩組多表guardWrite不死結, 且各自之寫入完整', async function() {
|
|
380
|
+
await so.procOrm('u', 'items', 'save', [{ id: 'r1', name: 'A', order: 1 }])
|
|
381
|
+
await so.procOrm('u', 'items2', 'save', [{ id: 'q1', name: 'Q', order: 1 }])
|
|
382
|
+
let bl = buildBaseline(await sel(), CT)
|
|
383
|
+
let job = (tables, name) => so.guardWrite({
|
|
384
|
+
tables,
|
|
385
|
+
read: () => sel(),
|
|
386
|
+
baseline: bl,
|
|
387
|
+
contract: CT,
|
|
388
|
+
write: async () => {
|
|
389
|
+
await so.procOrm('u', 'items2', 'save', [{ id: 'q1', name }], { mustExist: true })
|
|
390
|
+
await wait(20)
|
|
391
|
+
return name
|
|
392
|
+
},
|
|
393
|
+
})
|
|
394
|
+
let t = setTimeout(() => assert.fail('疑似死結'), 5000)
|
|
395
|
+
let rs = await Promise.all([job(['items', 'items2'], 'n1'), job(['items2', 'items'], 'n2')])
|
|
396
|
+
clearTimeout(t)
|
|
397
|
+
assert.deepStrictEqual(rs, ['n1', 'n2'])
|
|
398
|
+
assert.strict.equal((await byId('q1', 'items2')).name, 'n2')
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
})
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import assert from 'assert'
|
|
3
|
+
import WOrm from 'w-orm-lowdb/src/WOrmLowdb.mjs'
|
|
4
|
+
import build from 'w-data-collector/src/build.mjs'
|
|
5
|
+
import WServOrm from '../src/WServOrm.mjs'
|
|
6
|
+
import { getReject, quiet } from './tools/ormStub.mjs'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
//規格: w-web-sso 需求 T2①(任務 orm-run02): tokens.userId是權杖擁有者, procOrm insert以opt.keepUserId保留擁有者並將userIdUpdate記為操作者; 規則帳本 R-KEEPUSER-01
|
|
10
|
+
//以真ORM(w-orm-lowdb)經procOrm與guardWrite驗證; 替身ORM版(含各mode、擴充函數、直接getMapOrm)見 unit-keepuser.test.mjs
|
|
11
|
+
//
|
|
12
|
+
//規格句對照(spec = 派工提示詞「本輪範圍」與〈完成條件〉1):
|
|
13
|
+
// A01-A02 「keepUserId:true 時保留擁有者、userIdUpdate 為操作者」(sso依擁有者分組之寫入) A03 「列無userId時以操作者補」
|
|
14
|
+
// A04 「未給或false時行為與現行完全相同」 A05 「無效opt於寫入前拒收」 A06 「save現行不覆寫userId」之評估 A07 於表鎖與guardWrite內之實際用法
|
|
15
|
+
let fd = './test/_tmp/api-keepuser'
|
|
16
|
+
|
|
17
|
+
function mkTab(keyTable) {
|
|
18
|
+
return build({
|
|
19
|
+
keyTable,
|
|
20
|
+
settings: {
|
|
21
|
+
id: { pk: true, name: '主鍵', type: 'STRING' },
|
|
22
|
+
name: { name: '名稱', type: 'STRING' },
|
|
23
|
+
userId: { name: '擁有者', type: 'STRING' },
|
|
24
|
+
userIdUpdate: { name: '更新者', type: 'STRING' },
|
|
25
|
+
timeCreate: { name: '創建時間', type: 'STRING' },
|
|
26
|
+
timeUpdate: { name: '更新時間', type: 'STRING' },
|
|
27
|
+
},
|
|
28
|
+
}, { useCreateStorage: false })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
describe('procOrm opt.keepUserId接w-orm-lowdb(T2①)', function() {
|
|
33
|
+
|
|
34
|
+
let so = null
|
|
35
|
+
let proc = (user, mode, input, opt) => so.procOrm(user, 'tokens', mode, input, opt)
|
|
36
|
+
let sel = () => so.woItems.tokens.select({})
|
|
37
|
+
let byId = async (id) => (await so.woItems.tokens.select({ id: { $in: [id] } }))[0]
|
|
38
|
+
|
|
39
|
+
before(function() {
|
|
40
|
+
fs.rmSync(fd, { recursive: true, force: true })
|
|
41
|
+
fs.mkdirSync(fd, { recursive: true })
|
|
42
|
+
so = WServOrm({ tokens: mkTab('tokens') }, WOrm, `${fd}/db.json`, 'worm')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
beforeEach(async function() {
|
|
46
|
+
await so.woItems.tokens.delAll({})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
after(async function() {
|
|
50
|
+
await so.closeAll()
|
|
51
|
+
fs.rmSync(fd, { recursive: true, force: true })
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('A01 sso情境: 操作者admin-1代擁有者owner-1建立權杖, 庫內userId為擁有者、userIdUpdate為操作者, 時間欄補值', async function() {
|
|
55
|
+
let r = await proc('admin-1', 'insert', [{ id: 't1', name: 'tk', userId: 'owner-1' }], { keepUserId: true })
|
|
56
|
+
assert.deepStrictEqual(r, [{ id: 't1' }])
|
|
57
|
+
let row = await byId('t1')
|
|
58
|
+
assert.strict.equal(row.userId, 'owner-1')
|
|
59
|
+
assert.strict.equal(row.userIdUpdate, 'admin-1')
|
|
60
|
+
assert.strict.equal(row.name, 'tk')
|
|
61
|
+
assert.ok(/^\d{4}-\d{2}-\d{2}/.test(row.timeCreate), row.timeCreate)
|
|
62
|
+
assert.strict.equal(row.timeUpdate, row.timeCreate)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('A02 同一批多位擁有者: 各列各自保留其擁有者, userIdUpdate皆為操作者', async function() {
|
|
66
|
+
await proc('admin-1', 'insert', [{ id: 't1', userId: 'owner-1' }, { id: 't2', userId: 'owner-2' }, { id: 't3', userId: 'owner-1' }], { keepUserId: true })
|
|
67
|
+
let rows = (await sel()).sort((a, b) => a.id.localeCompare(b.id))
|
|
68
|
+
assert.deepStrictEqual(rows.map((v) => [v.id, v.userId, v.userIdUpdate]), [['t1', 'owner-1', 'admin-1'], ['t2', 'owner-2', 'admin-1'], ['t3', 'owner-1', 'admin-1']])
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('A03 列無userId(缺欄、空字串、null、數字): 以操作者補', async function() {
|
|
72
|
+
await proc('admin-1', 'insert', [{ id: 't1' }, { id: 't2', userId: '' }, { id: 't3', userId: null }, { id: 't4', userId: 7 }, { id: 't5', userId: 'owner-5' }], { keepUserId: true })
|
|
73
|
+
let got = Object.fromEntries((await sel()).map((v) => [v.id, v.userId]))
|
|
74
|
+
assert.deepStrictEqual(got, { t1: 'admin-1', t2: 'admin-1', t3: 'admin-1', t4: 'admin-1', t5: 'owner-5' })
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('A04 未給opt或{keepUserId:false}: 與現行相同(列上userId被操作者覆寫)', async function() {
|
|
78
|
+
await proc('admin-1', 'insert', [{ id: 't1', userId: 'owner-1' }])
|
|
79
|
+
await proc('admin-2', 'insert', [{ id: 't2', userId: 'owner-2' }], { keepUserId: false })
|
|
80
|
+
await proc('admin-3', 'insert', [{ id: 't3', userId: 'owner-3' }], {})
|
|
81
|
+
let got = Object.fromEntries((await sel()).map((v) => [v.id, [v.userId, v.userIdUpdate]]))
|
|
82
|
+
assert.deepStrictEqual(got, { t1: ['admin-1', 'admin-1'], t2: ['admin-2', 'admin-2'], t3: ['admin-3', 'admin-3'] })
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('A05 無效opt於寫入前拒收且零寫入: keepUserId非布林、未知鍵、用於save/del/select/delAll/mix; 既有列不變', async function() {
|
|
86
|
+
await proc('admin-1', 'insert', [{ id: 'r1', name: 'A', userId: 'owner-1' }], { keepUserId: true })
|
|
87
|
+
let before = JSON.stringify(await sel())
|
|
88
|
+
let cases = [
|
|
89
|
+
['insert', [{ id: 'n1', userId: 'o' }], { keepUserId: 'true' }, 'invalid opt of procOrm, keepUserId needs a boolean'],
|
|
90
|
+
['insert', [{ id: 'n1', userId: 'o' }], { keepuserid: true }, 'invalid opt of procOrm, unsupported key'],
|
|
91
|
+
['save', [{ id: 'r1', name: 'B', userId: 'owner-1' }], { keepUserId: true }, 'invalid opt of procOrm, keepUserId supports only insert'],
|
|
92
|
+
['del', [{ id: 'r1' }], { keepUserId: true }, 'invalid opt of procOrm, keepUserId supports only insert'],
|
|
93
|
+
['delAll', {}, { keepUserId: true }, 'invalid opt of procOrm, keepUserId supports only insert'],
|
|
94
|
+
['select', {}, { keepUserId: true }, 'invalid opt of procOrm, keepUserId supports only insert'],
|
|
95
|
+
['mix', { add: [{ id: 'n2', userId: 'o' }] }, { keepUserId: true }, 'invalid opt of procOrm, keepUserId supports only insert'],
|
|
96
|
+
]
|
|
97
|
+
for (let [mode, input, opt, msg] of cases) {
|
|
98
|
+
let err = await quiet(() => getReject(proc('admin-2', mode, input, opt)))
|
|
99
|
+
assert.strict.equal(err, msg, `${mode} ${JSON.stringify(opt)}`)
|
|
100
|
+
}
|
|
101
|
+
assert.strict.equal(JSON.stringify(await sel()), before)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('A06 save現行不覆寫userId: 保留擁有者之列經save修改, userId維持(列上帶原值)、userIdUpdate為修改者; keepUserId不適用save', async function() {
|
|
105
|
+
await proc('admin-1', 'insert', [{ id: 't1', name: 'A', userId: 'owner-1' }], { keepUserId: true })
|
|
106
|
+
let row = await byId('t1')
|
|
107
|
+
await proc('admin-2', 'save', [{ ...row, name: 'B' }])
|
|
108
|
+
let row2 = await byId('t1')
|
|
109
|
+
assert.strict.equal(row2.userId, 'owner-1')
|
|
110
|
+
assert.strict.equal(row2.userIdUpdate, 'admin-2')
|
|
111
|
+
assert.strict.equal(row2.name, 'B')
|
|
112
|
+
assert.strict.equal(row2.timeCreate, row.timeCreate)
|
|
113
|
+
let err = await quiet(() => getReject(proc('admin-2', 'save', [{ ...row, name: 'C' }], { keepUserId: true })))
|
|
114
|
+
assert.strict.equal(err, 'invalid opt of procOrm, keepUserId supports only insert')
|
|
115
|
+
assert.strict.equal((await byId('t1')).name, 'B')
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('A07 於withTabLocks與guardWrite之write內使用: 與mustExist(另一次save)並用, 鎖內寫入成功', async function() {
|
|
119
|
+
await so.guardWrite({
|
|
120
|
+
tables: ['tokens'],
|
|
121
|
+
read: () => so.woItems.tokens.select({}),
|
|
122
|
+
write: () => proc('admin-1', 'insert', [{ id: 't1', name: 'A', userId: 'owner-1' }], { keepUserId: true }),
|
|
123
|
+
})
|
|
124
|
+
await so.withTabLocks(['tokens'], async () => {
|
|
125
|
+
await proc('admin-2', 'save', [{ id: 't1', name: 'B', userId: 'owner-1' }], { mustExist: true })
|
|
126
|
+
})
|
|
127
|
+
let row = await byId('t1')
|
|
128
|
+
assert.deepStrictEqual([row.userId, row.userIdUpdate, row.name], ['owner-1', 'admin-2', 'B'])
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
})
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
//記憶體ORM(供 unit-guard 使用): 有狀態之替身, 回傳形狀依w-orm系列規格(save、del回與輸入等長之逐筆陣列, insert與delAll回聚合物件)
|
|
2
|
+
//與 ormStub.mjs 之 createStub 不同: 本工具真的存取資料(Map), 並支援select之{ id: { $in } }、save之autoInsert選項、各操作之人為延遲與逐項覆寫(plan),
|
|
3
|
+
//用以驗證只改不增、刪除未命中與表鎖原子性; 每次createMemOrm各自持有tables、calls與plan, 不共用模組層狀態
|
|
4
|
+
function createMemOrm() {
|
|
5
|
+
let tables = {} //cl -> Map(id -> row)
|
|
6
|
+
let calls = [] //{ cl, fn, args }
|
|
7
|
+
let plan = {
|
|
8
|
+
delay: 0, //每個操作開始前之延遲毫秒
|
|
9
|
+
select: null, //function(args, state)覆寫回傳或拋錯
|
|
10
|
+
save: null,
|
|
11
|
+
del: null,
|
|
12
|
+
}
|
|
13
|
+
let clone = (v) => JSON.parse(JSON.stringify(v))
|
|
14
|
+
let wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
15
|
+
|
|
16
|
+
function WOrmMem({ cl }) {
|
|
17
|
+
if (!tables[cl]) {
|
|
18
|
+
tables[cl] = new Map()
|
|
19
|
+
}
|
|
20
|
+
let tb = tables[cl]
|
|
21
|
+
let ev = {}
|
|
22
|
+
|
|
23
|
+
ev.select = async (find) => {
|
|
24
|
+
calls.push({ cl, fn: 'select', args: [find] })
|
|
25
|
+
if (plan.delay > 0) {
|
|
26
|
+
await wait(plan.delay)
|
|
27
|
+
}
|
|
28
|
+
if (plan.select) {
|
|
29
|
+
return plan.select([find], tb)
|
|
30
|
+
}
|
|
31
|
+
let rows = [...tb.values()].map(clone)
|
|
32
|
+
if (find && find.id && Array.isArray(find.id.$in)) {
|
|
33
|
+
//同各w-orm-*共用之條件檢核(wsemi cvOrmFind): $in之元素數上限10000, 超過整批reject
|
|
34
|
+
if (find.id.$in.length > 10000) {
|
|
35
|
+
return Promise.reject(new Error('invalid find: array is too large (> 10000) at [find.id.$in]'))
|
|
36
|
+
}
|
|
37
|
+
let ids = new Set(find.id.$in)
|
|
38
|
+
rows = rows.filter((r) => ids.has(r.id))
|
|
39
|
+
}
|
|
40
|
+
return rows
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
ev.insert = async (data) => {
|
|
44
|
+
calls.push({ cl, fn: 'insert', args: [data] })
|
|
45
|
+
if (plan.delay > 0) {
|
|
46
|
+
await wait(plan.delay)
|
|
47
|
+
}
|
|
48
|
+
let n = 0
|
|
49
|
+
for (let v of (Array.isArray(data) ? data : [data])) {
|
|
50
|
+
if (!tb.has(v.id)) {
|
|
51
|
+
tb.set(v.id, clone(v))
|
|
52
|
+
n++
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { n, nInserted: n, ok: 1 }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
ev.save = async (data, option) => {
|
|
59
|
+
calls.push({ cl, fn: 'save', args: option === undefined ? [data] : [data, option] })
|
|
60
|
+
if (plan.delay > 0) {
|
|
61
|
+
await wait(plan.delay)
|
|
62
|
+
}
|
|
63
|
+
if (plan.save) {
|
|
64
|
+
return plan.save([data, option], tb)
|
|
65
|
+
}
|
|
66
|
+
let autoInsert = !(option && option.autoInsert === false)
|
|
67
|
+
return (Array.isArray(data) ? data : [data]).map((v) => {
|
|
68
|
+
if (tb.has(v.id)) {
|
|
69
|
+
tb.set(v.id, { ...tb.get(v.id), ...clone(v) })
|
|
70
|
+
return { n: 1, nInserted: 0, nModified: 1, ok: 1 }
|
|
71
|
+
}
|
|
72
|
+
if (autoInsert) {
|
|
73
|
+
tb.set(v.id, clone(v))
|
|
74
|
+
return { n: 1, nInserted: 1, nModified: 0, ok: 1 }
|
|
75
|
+
}
|
|
76
|
+
return { n: 0, nInserted: 0, nModified: 0, ok: 1 }
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
ev.del = async (data) => {
|
|
81
|
+
calls.push({ cl, fn: 'del', args: [data] })
|
|
82
|
+
if (plan.delay > 0) {
|
|
83
|
+
await wait(plan.delay)
|
|
84
|
+
}
|
|
85
|
+
if (plan.del) {
|
|
86
|
+
return plan.del([data], tb)
|
|
87
|
+
}
|
|
88
|
+
return (Array.isArray(data) ? data : [data]).map((v) => {
|
|
89
|
+
let hit = tb.delete(v.id)
|
|
90
|
+
return { n: hit ? 1 : 0, nDeleted: hit ? 1 : 0, ok: 1 }
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
ev.delAll = async (find) => {
|
|
95
|
+
calls.push({ cl, fn: 'delAll', args: [find] })
|
|
96
|
+
let n = tb.size
|
|
97
|
+
tb.clear()
|
|
98
|
+
return { n, nDeleted: n, ok: 1 }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return ev
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
WOrmMem,
|
|
106
|
+
tables,
|
|
107
|
+
calls,
|
|
108
|
+
plan,
|
|
109
|
+
rows: (cl) => [...tables[cl].values()].map(clone),
|
|
110
|
+
callsOf: (fn) => calls.filter((c) => c.fn === fn),
|
|
111
|
+
reset: () => {
|
|
112
|
+
calls.length = 0
|
|
113
|
+
plan.delay = 0
|
|
114
|
+
plan.select = null
|
|
115
|
+
plan.save = null
|
|
116
|
+
plan.del = null
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
export { createMemOrm }
|