Skip to content

Commit 0509044

Browse files
committed
feat(exception): 令常见报错附带更多上下文信息
以帮助在错误难以重现的情况下,多一点有价值的信息帮助分析、定位问题。 做法: - 在报错现场,除了原有的融入报错语句的信息之外,另添一行(`MoreInfo: ...`),将补充信息对象 `JSON.stringify` 放在这一行; - 当报错现场上下文信息不够(特别是一些公用的基础函数、方法,如 Database 里的 `findSchema()`),避免错误被就地抛出(目前是使用 `tryCatch()` 函数包裹;_但不一定需要,试想在外面 try...catch... 会怎 么样_),然后在调用方往拿到的 Error 对象的 message 里补充信息,再抛出。 feat: 为 PrimaryKey 相关报错消息添加更多上下文信息 refactor(tryCatch): 令其更好利用 ts 类型推导的特性 包括: - 修改返回值形状,利用 ts discriminated union 的特性 - 添加类型参数 `U extends any[]` 来利用 rest parameter 的特性 feat: 为 NonExistentTable 相关报错消息添加额外信息 refactor(tryCatch): 添加 doThrow: boolean 选项及相应类型定义 ...以简化在只需要把 Maybe<T> 的异常值 throw 出来的场景。另外,简化了 tryCatch 模块里的数据结构和类型名。 ...添加相关单元测试。 ...resolves #66
1 parent 95c8b63 commit 0509044

File tree

10 files changed

+261
-59
lines changed

10 files changed

+261
-59
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,13 @@
6262
"license": "MIT",
6363
"devDependencies": {
6464
"@types/chai": "^4.1.2",
65+
"@types/chai-string": "^1.4.1",
6566
"@types/node": "^10.5.5",
6667
"@types/shelljs": "^0.8.0",
6768
"@types/sinon": "^5.0.0",
6869
"@types/sinon-chai": "^3.2.0",
6970
"chai": "^4.1.2",
71+
"chai-string": "^1.4.0",
7072
"coveralls": "^3.0.0",
7173
"css-loader": "^1.0.0",
7274
"extract-text-webpack-plugin": "^4.0.0-beta.0",

src/exception/Exception.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import { attachMoreErrorInfo } from '../utils'
2+
13
export class ReactiveDBException extends Error {
2-
constructor(message: string) {
3-
super(message)
4+
constructor(message: string, moreInfo?: {}) {
5+
const messageWithContext = !moreInfo ? message : attachMoreErrorInfo(message, moreInfo)
6+
super(messageWithContext)
47
this.name = 'ReactiveDBError'
58
Object.setPrototypeOf(this, ReactiveDBException.prototype)
69
}

src/exception/database.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@ export const InvalidType = (expect?: [string, string]) => {
2929
export const UnexpectedTransactionUse = () =>
3030
new ReactiveDBException('Please use Database#transaction to get a transaction scope first.')
3131

32-
export const PrimaryKeyNotProvided = () => new ReactiveDBException(`Primary key was not provided.`)
32+
export const PrimaryKeyNotProvided = (moreInfo?: {}) =>
33+
new ReactiveDBException(`Primary key was not provided.`, moreInfo)
3334

34-
export const PrimaryKeyConflict = () => new ReactiveDBException(`Primary key was already provided.`)
35+
export const PrimaryKeyConflict = (moreInfo?: {}) =>
36+
new ReactiveDBException(`Primary key was already provided.`, moreInfo)
3537

3638
export const DatabaseIsNotEmpty = () =>
3739
new ReactiveDBException('Method: load cannnot be invoked since database is not empty.')

src/storage/Database.ts

Lines changed: 87 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,19 @@ import Version from '../version'
77
import { Traversable } from '../shared'
88
import { Mutation, Selector, QueryToken, PredicateProvider } from './modules'
99
import { dispose, contextTableName, fieldIdentifier, hiddenColName } from './symbols'
10-
import { forEach, clone, contains, tryCatch, hasOwn, getType, assert, assertValue, warn, isNonNullable } from '../utils'
10+
import {
11+
forEach,
12+
clone,
13+
contains,
14+
tryCatch,
15+
isException,
16+
hasOwn,
17+
getType,
18+
assert,
19+
assertValue,
20+
warn,
21+
isNonNullable,
22+
} from '../utils'
1123
import { createPredicate, createPkClause, mergeTransactionResult, predicatableQuery, lfFactory } from './helper'
1224
import { Relationship, RDBType, DataStoreType, LeafType, StatementType, JoinMode } from '../interface/enum'
1325
import { SchemaDef, ColumnDef, ParsedSchema, Association, ScopedHandler } from '../interface'
@@ -28,6 +40,8 @@ const transactionErrorHandler = {
2840
error: () => warn(`Execute failed, transaction is already marked for rollback.`),
2941
}
3042

43+
const tryCatchCreatePredicate = tryCatch(createPredicate)
44+
3145
export class Database {
3246
public static version = Version
3347

@@ -46,14 +60,14 @@ export class Database {
4660
private storedIds = new Set<string>()
4761
private subscription: Subscription | null = null
4862

49-
private findPrimaryKey = (name: string) => {
50-
return this.findSchema(name)!.pk
51-
}
52-
5363
private findSchema = (name: string): ParsedSchema => {
5464
const schema = this.schemas.get(name)
5565
return assertValue(schema, Exception.NonExistentTable, name)
5666
}
67+
private tryCatchFindPrimaryKey = tryCatch((name: string) => {
68+
return this.findSchema(name).pk
69+
})
70+
private tryCatchFindSchema = tryCatch(this.findSchema)
5771

5872
/**
5973
* @method defineSchema
@@ -64,7 +78,7 @@ export class Database {
6478
assert(advanced, Exception.UnmodifiableTable)
6579

6680
const hasPK = Object.keys(schema).some((key: string) => schema[key].primaryKey === true)
67-
assert(hasPK, Exception.PrimaryKeyNotProvided)
81+
assert(hasPK, Exception.PrimaryKeyNotProvided, { tableName })
6882

6983
this.schemaDefs.set(tableName, schema)
7084
return this
@@ -104,14 +118,15 @@ export class Database {
104118
assert(!this.connected, Exception.DatabaseIsNotEmpty)
105119

106120
const load = (db: lf.Database) => {
121+
const findPrimaryKey = this.tryCatchFindPrimaryKey({ call: 'load', doThrow: true })
107122
forEach(data.tables, (entities: any[], name: string) => {
108-
const schema = this.findSchema(name)
109-
entities.forEach((entity: any) => this.storedIds.add(fieldIdentifier(name, entity[schema.pk])))
123+
const { unwrapped: pk } = findPrimaryKey(name)
124+
entities.forEach((entity: any) => this.storedIds.add(fieldIdentifier(name, entity[pk])))
110125
})
111126
return db.import(data).catch(() => {
112127
forEach(data.tables, (entities: any[], name: string) => {
113-
const schema = this.findSchema(name)
114-
entities.forEach((entity: any) => this.storedIds.delete(fieldIdentifier(name, entity[schema.pk])))
128+
const { unwrapped: pk } = findPrimaryKey(name)
129+
entities.forEach((entity: any) => this.storedIds.delete(fieldIdentifier(name, entity[pk])))
115130
})
116131
})
117132
}
@@ -126,7 +141,11 @@ export class Database {
126141

127142
insert<T>(tableName: string, raw: T | T[]): Observable<ExecutorResult> {
128143
const insert = (db: lf.Database) => {
129-
const schema = this.findSchema(tableName)
144+
const maybeSchema = this.tryCatchFindSchema({ op: 'insert' })(tableName)
145+
if (isException(maybeSchema)) {
146+
return throwError(maybeSchema.unwrapped)
147+
}
148+
const schema = maybeSchema.unwrapped
130149
const pk = schema.pk
131150
const columnMapper = schema.mapper
132151
const [table] = Database.getTables(db, tableName)
@@ -180,20 +199,21 @@ export class Database {
180199
return throwError(Exception.InvalidType(['Object', type]))
181200
}
182201

183-
const [schema, err] = tryCatch<ParsedSchema>(this.findSchema)(tableName)
184-
if (err) {
185-
return throwError(err)
202+
const maybeSchema = this.tryCatchFindSchema({ op: 'update' })(tableName)
203+
if (isException(maybeSchema)) {
204+
return throwError(maybeSchema.unwrapped)
186205
}
206+
const schema = maybeSchema.unwrapped
187207

188208
const update = (db: lf.Database) => {
189209
const entity = clone(raw)
190210
const [table] = Database.getTables(db, tableName)
191-
const columnMapper = schema!.mapper
211+
const columnMapper = schema.mapper
192212
const hiddenPayload = Object.create(null)
193213

194214
columnMapper.forEach((mapper, key) => {
195215
// cannot create a hidden column for primary key
196-
if (!hasOwn(entity, key) || key === schema!.pk) {
216+
if (!hasOwn(entity, key) || key === schema.pk) {
197217
return
198218
}
199219

@@ -208,7 +228,7 @@ export class Database {
208228

209229
forEach(mut, (val, key) => {
210230
const column = table[key]
211-
if (key === schema!.pk) {
231+
if (key === schema.pk) {
212232
warn(`Primary key is not modifiable.`)
213233
} else if (!column) {
214234
warn(`Column: ${key} is not existent on table:${tableName}`)
@@ -223,24 +243,25 @@ export class Database {
223243
}
224244

225245
delete<T>(tableName: string, clause: Predicate<T> = {}): Observable<ExecutorResult> {
226-
const [pk, err] = tryCatch<string>(this.findPrimaryKey)(tableName)
227-
if (err) {
228-
return throwError(err)
246+
const maybePK = this.tryCatchFindPrimaryKey({ op: 'delete' })(tableName)
247+
if (isException(maybePK)) {
248+
return throwError(maybePK.unwrapped)
229249
}
250+
const pk = maybePK.unwrapped
230251

231252
const deletion = (db: lf.Database): Observable<ExecutorResult> => {
232253
const [table] = Database.getTables(db, tableName)
233-
const column = table[pk!]
254+
const column = table[pk]
234255
const provider = new PredicateProvider(table, clause)
235256
const prefetch = predicatableQuery(db, table, provider.getPredicate(), StatementType.Select, column)
236257
const deleteByScopedIds = (scopedIds: Object[]) => {
237258
const query = predicatableQuery(db, table, provider.getPredicate(), StatementType.Delete)
238259

239-
scopedIds.forEach((entity) => this.storedIds.delete(fieldIdentifier(tableName, entity[pk!])))
260+
scopedIds.forEach((entity) => this.storedIds.delete(fieldIdentifier(tableName, entity[pk])))
240261

241262
const onError = {
242263
error: () => {
243-
scopedIds.forEach((entity: object) => this.storedIds.add(fieldIdentifier(tableName, entity[pk!])))
264+
scopedIds.forEach((entity: object) => this.storedIds.add(fieldIdentifier(tableName, entity[pk])))
244265
},
245266
}
246267

@@ -290,11 +311,12 @@ export class Database {
290311
}
291312

292313
remove<T>(tableName: string, clause: Clause<T> = {}): Observable<ExecutorResult> {
293-
const [schema, err] = tryCatch<ParsedSchema>(this.findSchema)(tableName)
294-
if (err) {
295-
return throwError(err)
314+
const maybeSchema = this.tryCatchFindSchema({ op: 'remove' })(tableName)
315+
if (isException(maybeSchema)) {
316+
return throwError(maybeSchema.unwrapped)
296317
}
297-
const disposeHandler = schema!.dispose
318+
const schema = maybeSchema.unwrapped
319+
const disposeHandler = schema.dispose
298320

299321
const remove = (db: lf.Database) => {
300322
const [table] = Database.getTables(db, tableName)
@@ -306,7 +328,7 @@ export class Database {
306328

307329
const removeByRootEntities = (rootEntities: Object[]) => {
308330
rootEntities.forEach((entity) => {
309-
removedIds.push(fieldIdentifier(tableName, entity[schema!.pk]))
331+
removedIds.push(fieldIdentifier(tableName, entity[schema.pk]))
310332
})
311333

312334
const onError = {
@@ -470,6 +492,8 @@ export class Database {
470492
// src: schemaDef; dest: uniques, indexes, primaryKey, nullable, associations, mapper
471493
// no short-curcuiting
472494
forEach(schemaDef, (def, key) => {
495+
const currentPK: string | undefined = primaryKey[0]
496+
473497
if (typeof def === 'function') {
474498
return
475499
}
@@ -479,7 +503,7 @@ export class Database {
479503
columns.set(key, def.type as RDBType)
480504

481505
if (def.primaryKey) {
482-
assert(!primaryKey[0], Exception.PrimaryKeyConflict)
506+
assert(!currentPK, Exception.PrimaryKeyConflict, { tableName, currentPK, incomingPK: key })
483507
primaryKey.push(key)
484508
}
485509

@@ -528,7 +552,10 @@ export class Database {
528552
}
529553

530554
private buildSelector<T>(db: lf.Database, tableName: string, clause: Query<T>, mode: JoinMode) {
531-
const schema = this.findSchema(tableName)
555+
const { unwrapped: schema } = this.tryCatchFindSchema({
556+
call: 'buildSelector',
557+
doThrow: true,
558+
})(tableName)
532559
const pk = schema.pk
533560
const containFields = !!clause.fields
534561

@@ -616,7 +643,10 @@ export class Database {
616643
context: Record = {},
617644
mode: JoinMode,
618645
) {
619-
const schema = this.findSchema(tableName)
646+
const { unwrapped: schema } = this.tryCatchFindSchema({
647+
call: 'traverseQueryFields',
648+
doThrow: true,
649+
})(tableName)
620650
const rootDefinition = Object.create(null)
621651
const navigators: string[] = []
622652

@@ -664,10 +694,13 @@ export class Database {
664694
} else {
665695
const { where, type } = defs as Association
666696
rootDefinition[key] = typeDefinition.revise(type!, ret.definition)
667-
const [predicate, err] = tryCatch(createPredicate)(currentTable, where(ret.table))
668-
if (err) {
669-
warn(`Failed to build predicate, since ${err.message}` + `, on table: ${ret.table.getName()}`)
697+
const maybePredicate = tryCatchCreatePredicate({
698+
tableName: ret.table.getName(),
699+
})(currentTable, where(ret.table))
700+
if (isException(maybePredicate)) {
701+
warn(`Failed to build predicate, since ${maybePredicate.unwrapped.message}`)
670702
}
703+
const predicate = maybePredicate.unwrapped
671704
const joinLink = predicate ? [{ table: ret.table, predicate }, ...ret.joinInfo] : ret.joinInfo
672705

673706
joinInfo.push(...joinLink)
@@ -750,10 +783,13 @@ export class Database {
750783
return
751784
}
752785

753-
const schema = this.findSchema(tableName)
786+
const { unwrapped: schema } = this.tryCatchFindSchema({
787+
call: 'traverseCompound',
788+
doThrow: true,
789+
})(tableName)
754790
const pk = schema.pk
755791
const pkVal = compoundEntites[pk]
756-
assert(pkVal !== undefined, Exception.PrimaryKeyNotProvided)
792+
assert(pkVal !== undefined, Exception.PrimaryKeyNotProvided, { tableName, pk, entry: compoundEntites })
757793

758794
const [table] = Database.getTables(db, tableName)
759795
const identifier = fieldIdentifier(tableName, pkVal)
@@ -827,7 +863,10 @@ export class Database {
827863
}
828864

829865
private navigatorLeaf(assocaiation: Association, _: string, val: any) {
830-
const schema = this.findSchema(assocaiation.name)
866+
const { unwrapped: schema } = this.tryCatchFindSchema({
867+
call: 'navigatorLeaf',
868+
doThrow: true,
869+
})(assocaiation.name)
831870
const fields = typeof val === 'string' ? new Set(schema.columns.keys()) : val
832871

833872
return {
@@ -839,7 +878,10 @@ export class Database {
839878

840879
private createScopedHandler<T>(db: lf.Database, queryCollection: any[], keys: any[]) {
841880
return (tableName: string): ScopedHandler => {
842-
const pk = this.findPrimaryKey(tableName)
881+
const { unwrapped: pk } = this.tryCatchFindPrimaryKey({
882+
call: 'createScopedHandler',
883+
doThrow: true,
884+
})(tableName)
843885

844886
const remove = (entities: T[]) => {
845887
const [table] = Database.getTables(db, tableName)
@@ -856,11 +898,14 @@ export class Database {
856898

857899
const get = (where: Predicate<any> | null = null) => {
858900
const [table] = Database.getTables(db, tableName)
859-
const [predicate, err] = tryCatch(createPredicate)(table, where)
860-
if (err) {
861-
return throwError(err)
901+
const maybePredicate = tryCatchCreatePredicate({
902+
call: 'createScopedHandler',
903+
})(table, where)
904+
if (isException(maybePredicate)) {
905+
return throwError(maybePredicate.unwrapped)
862906
}
863-
const query = predicatableQuery(db, table, predicate!, StatementType.Select)
907+
const predicate = maybePredicate.unwrapped
908+
const query = predicatableQuery(db, table, predicate, StatementType.Select)
864909

865910
return from<T[]>(query.exec() as any)
866911
}

0 commit comments

Comments
 (0)