Skip to content

Commit e853646

Browse files
committed
feat(RevertController): support revert in insert/update/delete
1 parent d84a22e commit e853646

File tree

11 files changed

+418
-96
lines changed

11 files changed

+418
-96
lines changed

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"license": "MIT",
6161
"devDependencies": {
6262
"@types/chai": "^4.0.3",
63+
"@types/lodash": "^4.14.73",
6364
"@types/node": "^8.0.23",
6465
"@types/shelljs": "^0.7.4",
6566
"@types/sinon": "^2.3.3",
@@ -71,6 +72,7 @@
7172
"extract-text-webpack-plugin": "^3.0.0",
7273
"happypack": "^4.0.0-beta.2",
7374
"html-webpack-plugin": "^2.30.1",
75+
"lodash": "^4.17.4",
7476
"madge": "^2.0.0",
7577
"moment": "^2.18.1",
7678
"node-watch": "^0.5.5",

src/exception/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './database'
22
export * from './token'
3+
export * from './revert'

src/exception/revert.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { ReactiveDBException } from './Exception'
2+
3+
export const tokenMustBeSymbol = (token: any) =>
4+
new ReactiveDBException(`Symbol type expected, but got ${ typeof token }: ${ token }`)
5+
6+
export const clauseMissingError = () =>
7+
new ReactiveDBException('Clause must be specified when when reverControoler is passed to delete method')
8+
9+
export const revertBeforeOperationSuccessError = () =>
10+
new ReactiveDBException('You can only revert after the operation that you passed RevertController success')

src/storage/Database.ts

+167-90
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import * as Exception from '../exception'
88
import * as typeDefinition from './helper/definition'
99
import Version from '../version'
1010
import { Traversable } from '../shared'
11-
import { Mutation, Selector, QueryToken, PredicateProvider, checkPredicate, predicateOperatorNames } from './modules'
11+
import { Mutation, Selector, QueryToken, PredicateProvider, checkPredicate, predicateOperatorNames, RevertController } from './modules'
1212
import { dispose, contextTableName, fieldIdentifier, hiddenColName } from './symbols'
1313
import { forEach, clone, contains, tryCatch, hasOwn, getType, assert, identity, warn, keys as objKeys } from '../utils'
14-
import { createPredicate, createPkClause, mergeTransactionResult, predicatableQuery, lfFactory } from './helper'
14+
import { createPredicate, createPkClause, predicatableQuery, lfFactory, executor } from './helper'
1515
import { Relationship, RDBType, DataStoreType, LeafType, StatementType, JoinMode } from '../interface/enum'
1616
import { Record, Fields, JoinInfo, Query, Predicate } from '../interface'
1717
import { SchemaDef, ColumnDef, ParsedSchema, Association, ScopedHandler } from '../interface'
@@ -110,57 +110,51 @@ export class Database {
110110
})
111111
}
112112

113-
insert<T>(tableName: string, raw: T[]): Observable<ExecutorResult>
113+
insert<T>(tableName: string, raw: T[], revertController?: RevertController): Observable<ExecutorResult>
114114

115-
insert<T>(tableName: string, raw: T): Observable<ExecutorResult>
115+
insert<T>(tableName: string, raw: T, revertController?: RevertController): Observable<ExecutorResult>
116116

117-
insert<T>(tableName: string, raw: T | T[]): Observable<ExecutorResult>
117+
insert<T>(tableName: string, raw: T | T[], revertController?: RevertController): Observable<ExecutorResult>
118118

119-
insert<T>(tableName: string, raw: T | T[]): Observable<ExecutorResult> {
120-
return this.database$
121-
.concatMap(db => {
122-
const schema = this.findSchema(tableName)
123-
const pk = schema.pk
124-
const columnMapper = schema.mapper
125-
const [ table ] = Database.getTables(db, tableName)
126-
const muts: Mutation[] = []
127-
const entities = clone(raw)
128-
129-
const iterator = Array.isArray(entities) ? entities : [entities]
130-
131-
iterator.forEach((entity: any) => {
132-
const mut = new Mutation(db, table)
133-
const hiddenPayload = Object.create(null)
134-
135-
columnMapper.forEach((mapper, key) => {
136-
// cannot create a hidden column for primary key
137-
if (!hasOwn(entity, key) || key === pk) {
138-
return
119+
insert<T>(tableName: string, raw: T | T[], revertController?: RevertController): Observable<ExecutorResult> {
120+
return this.database$.concatMap(db => {
121+
const { queries, contextIds } = this.buildInsertQuery(db, tableName, raw)
122+
const schema = this.findSchema(tableName)
123+
const pk = schema.pk
124+
const [ table ] = Database.getTables(db, tableName)
125+
return Observable.fromPromise(executor(db, queries))
126+
.do({
127+
next: () => {
128+
if (revertController) {
129+
const equalClause = (Array.isArray(raw) ? raw : [raw])
130+
.map(data => ({
131+
[pk]: data[pk]
132+
}))
133+
const clause = { $or: equalClause }
134+
const tablesStruct: TablesStruct = {
135+
[tableName]: {
136+
table, contextName: tableName
137+
}
138+
}
139+
const provider = new PredicateProvider(tablesStruct, tableName, clause)
140+
const deleteQuery =
141+
predicatableQuery(db, table, provider.getPredicate(), StatementType.Delete)
142+
revertController.inject(
143+
db, [ deleteQuery ]
144+
)
139145
}
140-
141-
const val = entity[key]
142-
hiddenPayload[key] = mapper(val)
143-
hiddenPayload[hiddenColName(key)] = val
144-
})
145-
146-
mut.patch({ ...entity, ...hiddenPayload })
147-
mut.withId(pk, entity[pk])
148-
muts.push(mut)
146+
},
147+
error: () => contextIds.forEach(id => this.storedIds.delete(id))
149148
})
150-
151-
const { contextIds, queries } = Mutation.aggregate(db, muts, [])
152-
contextIds.forEach(id => this.storedIds.add(id))
153-
return this.executor(db, queries)
154-
.do({ error: () => contextIds.forEach(id => this.storedIds.delete(id)) })
155-
})
149+
})
156150
}
157151

158152
get<T>(tableName: string, query: Query<T> = {}, mode: JoinMode = JoinMode.imlicit): QueryToken<T> {
159153
const selector$ = this.buildSelector<T>(tableName, query, mode)
160154
return new QueryToken<T>(selector$)
161155
}
162156

163-
update<T>(tableName: string, clause: Predicate<T>, raw: Partial<T>): Observable<ExecutorResult> {
157+
update<T>(tableName: string, clause: Predicate<T>, raw: Partial<T>, revertController?: RevertController): Observable<ExecutorResult> {
164158
const type = getType(raw)
165159
if (type !== 'Object') {
166160
return Observable.throw(Exception.InvalidType(['Object', type]))
@@ -172,7 +166,7 @@ export class Database {
172166
}
173167

174168
return this.database$
175-
.concatMap<any, any>(db => {
169+
.concatMap(db => {
176170
const entity = clone(raw)
177171
const [ table ] = Database.getTables(db, tableName)
178172
const columnMapper = schema!.mapper
@@ -210,42 +204,80 @@ export class Database {
210204
}
211205
})
212206

213-
return this.executor(db, [query])
207+
if (revertController) {
208+
const columns = Object.keys(raw).map(key => table[key])
209+
predicatableQuery(db, table, predicate!, StatementType.Select, ...columns)
210+
.exec()
211+
.then(([ value ]) => {
212+
const revertQuery = predicatableQuery(db, table, predicate!, StatementType.Update)
213+
forEach(value, (val, key) => {
214+
const column = table[key]
215+
revertQuery.set(column, val)
216+
})
217+
revertController.inject(
218+
db, [ revertQuery ]
219+
)
220+
})
221+
.catch(e => {
222+
revertController.giveup()
223+
warn(e)
224+
})
225+
}
226+
227+
return Observable.fromPromise(executor(db, [ query ]))
228+
.do({
229+
error: () => {
230+
if (revertController) {
231+
revertController.giveup()
232+
}
233+
}
234+
})
214235
})
215236
}
216237

217-
delete<T>(tableName: string, clause: Predicate<T> = {}): Observable<ExecutorResult> {
238+
delete<T>(tableName: string, clause: Predicate<T> = {}, revertController?: RevertController): Observable<ExecutorResult> {
218239
const [pk, err] = tryCatch<string>(this.findPrimaryKey)(tableName)
219240
if (err) {
220241
return Observable.throw(err)
221242
}
222243

223-
return this.database$
224-
.concatMap(db => {
225-
const [ table ] = Database.getTables(db, tableName)
226-
const tables = this.buildTablesStructure(table)
227-
const column = table[pk!]
228-
const provider = new PredicateProvider(tables, tableName, clause)
229-
const prefetch =
230-
predicatableQuery(db, table, provider.getPredicate(), StatementType.Select, column)
231-
232-
return Observable.fromPromise(prefetch.exec())
233-
.concatMap((scopedIds) => {
234-
const predicate = provider.getPredicate()
235-
if (!predicate) {
236-
warn(`The result of parsed Predicate is null, you are deleting all ${ tableName } Table!`)
237-
}
238-
const query = predicatableQuery(db, table, predicate, StatementType.Delete)
239-
240-
scopedIds.forEach((entity: any) =>
241-
this.storedIds.delete(fieldIdentifier(tableName, entity[pk!])))
244+
if (revertController) {
245+
if (!clause) {
246+
throw Exception.clauseMissingError()
247+
}
248+
}
242249

243-
return this.executor(db, [query]).do({ error: () => {
244-
scopedIds.forEach((entity: any) =>
245-
this.storedIds.add(fieldIdentifier(tableName, entity[pk!])))
246-
}})
247-
})
248-
})
250+
return this.database$.concatMap(db => {
251+
const [ table ] = Database.getTables(db, tableName)
252+
const tablesStruct = this.buildTablesStructure(table)
253+
const provider = new PredicateProvider(tablesStruct, tableName, clause)
254+
const predicate = provider.getPredicate()
255+
if (!predicate) {
256+
warn(`The result of parsed Predicate is null, you are deleting all ${ tableName } Tables!`)
257+
}
258+
const query = predicatableQuery(db, table, predicate, StatementType.Delete)
259+
const columns = revertController ? [] as any : [ pk ]
260+
return Observable.fromPromise(
261+
this.deletePrefetch(db, table, provider, columns)
262+
)
263+
.concatMap(scopedIds =>
264+
Observable.fromPromise(executor(db, [query]))
265+
.do({
266+
next: () => {
267+
if (revertController) {
268+
const { queries } = this.buildInsertQuery(db, tableName, scopedIds)
269+
revertController.inject(db, queries)
270+
}
271+
scopedIds.forEach((entity: any) =>
272+
this.storedIds.delete(fieldIdentifier(tableName, entity[pk!])))
273+
},
274+
error: () => {
275+
scopedIds.forEach((entity: any) =>
276+
this.storedIds.add(fieldIdentifier(tableName, entity[pk!])))
277+
}
278+
})
279+
)
280+
})
249281
}
250282

251283
upsert<T>(tableName: string, raw: T): Observable<ExecutorResult>
@@ -264,7 +296,7 @@ export class Database {
264296
const { contextIds, queries } = Mutation.aggregate(db, insert, update)
265297
if (queries.length > 0) {
266298
contextIds.forEach(id => this.storedIds.add(id))
267-
return this.executor(db, queries)
299+
return Observable.fromPromise(executor(db, queries))
268300
.do({ error: () => contextIds.forEach(id => this.storedIds.delete(id)) })
269301
} else {
270302
return Observable.of({ result: false, insert: 0, update: 0, delete: 0, select: 0 })
@@ -289,7 +321,7 @@ export class Database {
289321
}
290322

291323
const queries: lf.query.Builder[] = []
292-
const removedIds: any = []
324+
const removedIds: string[] = []
293325
queries.push(predicatableQuery(db, table, predicate!, StatementType.Delete))
294326

295327
const prefetch = predicatableQuery(db, table, predicate!, StatementType.Select)
@@ -303,10 +335,10 @@ export class Database {
303335
const scope = this.createScopedHandler<T>(db, queries, removedIds)
304336
return disposeHandler(rootEntities, scope)
305337
.do(() => removedIds.forEach((id: string) => this.storedIds.delete(id)))
306-
.concatMap(() => this.executor(db, queries))
338+
.concatMap(() => executor(db, queries))
307339
} else {
308340
removedIds.forEach((id: string) => this.storedIds.delete(id))
309-
return this.executor(db, queries)
341+
return executor(db, queries)
310342
}
311343
})
312344
.do({ error: () =>
@@ -329,7 +361,7 @@ export class Database {
329361
db.getSchema().tables().map(t => db.delete().from(t)))
330362

331363
return this.database$.concatMap(db => {
332-
return cleanup.concatMap(queries => this.executor(db, queries))
364+
return cleanup.concatMap(queries => executor(db, queries))
333365
.do(() => {
334366
db.close()
335367
this.schemas.clear()
@@ -519,6 +551,67 @@ export class Database {
519551
}
520552
}
521553

554+
private buildInsertQuery<T>(db: lf.Database, tableName: string, raw: T | T[]) {
555+
const schema = this.findSchema(tableName)
556+
const pk = schema.pk
557+
const columnMapper = schema.mapper
558+
const [ table ] = Database.getTables(db, tableName)
559+
const muts: Mutation[] = []
560+
const entities = clone(raw)
561+
562+
const iterator = Array.isArray(entities) ? entities : [entities]
563+
564+
iterator.forEach((entity: any) => {
565+
const mut = new Mutation(db, table)
566+
const hiddenPayload = Object.create(null)
567+
568+
columnMapper.forEach((mapper, key) => {
569+
// cannot create a hidden column for primary key
570+
if (!hasOwn(entity, key) || key === pk) {
571+
return
572+
}
573+
574+
const val = entity[key]
575+
hiddenPayload[key] = mapper(val)
576+
hiddenPayload[hiddenColName(key)] = val
577+
})
578+
579+
mut.patch({ ...entity, ...hiddenPayload })
580+
mut.withId(pk, entity[pk])
581+
muts.push(mut)
582+
})
583+
584+
const { contextIds, queries } = Mutation.aggregate(db, muts, [])
585+
contextIds.forEach(id => this.storedIds.add(id))
586+
587+
return { queries, contextIds }
588+
}
589+
590+
private deletePrefetch<T>(
591+
db: lf.Database,
592+
table: lf.schema.Table,
593+
provider: PredicateProvider<T>,
594+
columnNames: string[]
595+
) {
596+
let columns: lf.schema.Column[]
597+
// build revert query
598+
if (!columnNames.length) {
599+
const tableName = table.getName()
600+
columns = Array.from(this.findSchema(tableName).columns.keys())
601+
.map(columnName => {
602+
const column = table[columnName]
603+
const hiddenName = hiddenColName(columnName)
604+
const hiddenCol = table[hiddenName]
605+
return hiddenCol || column
606+
})
607+
} else {
608+
columns = columnNames.map(columnName => table[columnName])
609+
}
610+
const prefetch =
611+
predicatableQuery(db, table, provider.getPredicate(), StatementType.Select, ...columns)
612+
return prefetch.exec()
613+
}
614+
522615
// context 用来标记DFS路径中的所有出现过的表,用于解决self-join时的二义性
523616
// path 用来标记每个查询路径上出现的表,用于解决circular reference
524617
private traverseQueryFields(
@@ -951,20 +1044,4 @@ export class Database {
9511044
return newTableName!
9521045
}
9531046

954-
private executor(db: lf.Database, queries: lf.query.Builder[]) {
955-
const tx = db.createTransaction()
956-
const handler = {
957-
error: () => warn(`Execute failed, transaction is already marked for rollback.`)
958-
}
959-
960-
return Observable.fromPromise(tx.exec(queries))
961-
.do(handler)
962-
.map((ret) => {
963-
return {
964-
result: true,
965-
...mergeTransactionResult(queries, ret)
966-
}
967-
})
968-
}
969-
9701047
}

src/storage/helper/executor.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { mergeTransactionResult } from './merge-transaction-result'
2+
import { ExecutorResult } from '../../interface'
3+
import { warn } from '../../utils'
4+
5+
export function executor(db: lf.Database, queries: lf.query.Builder[]): Promise<ExecutorResult> {
6+
const tx = db.createTransaction()
7+
8+
return tx.exec(queries)
9+
.then(ret => ({
10+
result: true,
11+
...mergeTransactionResult(queries, ret)
12+
}))
13+
.catch((e: Error) => {
14+
warn(`Execute failed, transaction is already marked for rollback.`)
15+
throw e
16+
})
17+
}

0 commit comments

Comments
 (0)