Skip to content

Commit

Permalink
feat: implement immutability via saveMethod=insert
Browse files Browse the repository at this point in the history
Which is more optimal/performant (no redundant read operation)
  • Loading branch information
kirillgroshkov committed Apr 17, 2022
1 parent 209e64d commit 6ab257d
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 59 deletions.
15 changes: 8 additions & 7 deletions src/commondao/common.dao.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,18 +73,19 @@ export interface CommonDaoCfg<
excludeFromIndexes?: (keyof DBM)[]

/**
* @default to false
* Set to true to limit DB writing:
* * Will throw an error if an object with matching ID already exists during save().
* * saveBatch, delete*() and patch() will throw.
* Defaults to false.
* Setting it to true will set saveMethod to `insert` for save/saveBatch, which will
* fail for rows that already exist in the DB (if CommonDB implementation supports it).
*
* Although deletion is possible by passing (opt.overrideImmutability === true)
* `delete*` and `patch` will throw.
*
* You can still override saveMethod, or set opt.allowMutability to allow deletion.
*/
immutable?: boolean

/**
* @default to false
* Set to true to limit DB writing (will throw an error is such case).
* Defaults to false.
* Set to true to limit DB writing (will throw an error in such case).
*/
readOnly?: boolean

Expand Down
81 changes: 29 additions & 52 deletions src/commondao/common.dao.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,15 +245,27 @@ export class CommonDao<
/**
* Throws if readOnly is true
*/
private requireObjectMutability(): void {
if (this.cfg.immutable) {
private requireObjectMutability(opt: CommonDaoOptions): void {
if (this.cfg.immutable && !opt.allowMutability) {
throw new AppError(DBLibError.OBJECT_IS_IMMUTABLE, {
code: DBLibError.OBJECT_IS_IMMUTABLE,
table: this.cfg.table,
})
}
}

private async ensureUniqueId(table: string, dbm: DBM): Promise<void> {
// todo: retry N times
const existing = await this.cfg.db.getByIds<DBM>(table, [dbm.id])
if (existing.length) {
throw new AppError(DBLibError.NON_UNIQUE_ID, {
table,
code: DBLibError.NON_UNIQUE_ID,
ids: existing.map(i => i.id),
})
}
}

async getBy(by: keyof DBM, value: any, limit = 0, opt?: CommonDaoOptions): Promise<Saved<BM>[]> {
return await this.query().filterEq(by, value).limit(limit).runQuery(opt)
}
Expand Down Expand Up @@ -602,7 +614,9 @@ export class CommonDao<
const dbm = await this.bmToDBM(bm as BM, opt)
const table = opt.table || this.cfg.table
if (opt.ensureUniqueId && idWasGenerated) await this.ensureUniqueId(table, dbm)
if (this.cfg.immutable) await this.ensureImmutableDontExist(table, [dbm.id])
if (this.cfg.immutable && !opt.allowMutability) {
opt.saveMethod ||= 'insert'
}
const op = `save(${dbm.id})`
const started = this.logSaveStarted(op, bm, table)
await this.cfg.db.saveBatch(table, [dbm], {
Expand All @@ -614,41 +628,6 @@ export class CommonDao<
return bm as any
}

private async ensureImmutableDontExist(table: string, ids: ID[]): Promise<void> {
await this.throwIfObjectExists(table, ids, [
DBLibError.OBJECT_IS_IMMUTABLE,
{
code: DBLibError.OBJECT_IS_IMMUTABLE,
table,
},
])
}

private async ensureUniqueId(table: string, dbm: DBM): Promise<void> {
// todo: retry N times
await this.throwIfObjectExists(
table,
[dbm.id],
[
DBLibError.OBJECT_IS_IMMUTABLE,
{
code: DBLibError.OBJECT_IS_IMMUTABLE,
table,
},
],
)
}

private async throwIfObjectExists(
table: string,
ids: ID[],
errorMeta: [DBLibError, any],
): Promise<void> {
const existing = await this.cfg.db.getByIds<DBM>(table, ids)
if (existing.length > 0)
throw new AppError(errorMeta[0], { ...errorMeta[1], ids: existing.map(i => i.id) })
}

/**
* Loads the row by id.
* Creates the row (via this.create()) if it doesn't exist
Expand Down Expand Up @@ -692,7 +671,9 @@ export class CommonDao<
this.assignIdCreatedUpdated(dbm, opt) // mutates
dbm = this.anyToDBM(dbm, opt)
if (opt.ensureUniqueId && idWasGenerated) await this.ensureUniqueId(table, dbm)
if (this.cfg.immutable) await this.ensureImmutableDontExist(table, [dbm.id])
}
if (this.cfg.immutable && !opt.allowMutability) {
opt.saveMethod ||= 'insert'
}
const op = `saveAsDBM(${dbm.id})`
const started = this.logSaveStarted(op, dbm, table)
Expand All @@ -710,11 +691,9 @@ export class CommonDao<
bms.forEach(bm => this.assignIdCreatedUpdated(bm, opt))
const dbms = await this.bmsToDBM(bms as BM[], opt)
if (opt.ensureUniqueId) throw new AppError('ensureUniqueId is not supported in saveBatch')
if (this.cfg.immutable)
await this.ensureImmutableDontExist(
table,
dbms.map(dbm => dbm.id),
)
if (this.cfg.immutable && !opt.allowMutability) {
opt.saveMethod ||= 'insert'
}

const op = `saveBatch ${dbms.length} row(s) (${_truncate(
dbms
Expand Down Expand Up @@ -742,11 +721,9 @@ export class CommonDao<
dbms.forEach(dbm => this.assignIdCreatedUpdated(dbm, opt)) // mutates
dbms = this.anyToDBMs(dbms, opt)
if (opt.ensureUniqueId) throw new AppError('ensureUniqueId is not supported in saveBatch')
if (this.cfg.immutable)
await this.ensureImmutableDontExist(
table,
dbms.map(dbm => dbm.id),
)
}
if (this.cfg.immutable && !opt.allowMutability) {
opt.saveMethod ||= 'insert'
}
const op = `saveBatchAsDBM ${dbms.length} row(s) (${_truncate(
dbms
Expand Down Expand Up @@ -775,7 +752,7 @@ export class CommonDao<
async deleteById(id?: ID, opt: CommonDaoOptions = {}): Promise<number> {
if (!id) return 0
this.requireWriteAccess()
if (!opt.allowMutability) this.requireObjectMutability()
this.requireObjectMutability(opt)
const op = `deleteById(${id})`
const table = opt.table || this.cfg.table
const started = this.logStarted(op, table)
Expand All @@ -786,7 +763,7 @@ export class CommonDao<

async deleteByIds(ids: ID[], opt: CommonDaoOptions = {}): Promise<number> {
this.requireWriteAccess()
if (!opt.allowMutability) this.requireObjectMutability()
this.requireObjectMutability(opt)
const op = `deleteByIds(${ids.join(', ')})`
const table = opt.table || this.cfg.table
const started = this.logStarted(op, table)
Expand All @@ -805,7 +782,7 @@ export class CommonDao<
opt: CommonDaoStreamForEachOptions<DBM> & { stream?: boolean } = {},
): Promise<number> {
this.requireWriteAccess()
if (!opt.allowMutability) this.requireObjectMutability()
this.requireObjectMutability(opt)
q.table = opt.table || q.table
const op = `deleteByQuery(${q.pretty()})`
const started = this.logStarted(op, q.table)
Expand Down

0 comments on commit 6ab257d

Please sign in to comment.