Skip to content

Commit

Permalink
feat: validate CommonDao query to disallow excludeFromIndexes props
Browse files Browse the repository at this point in the history
  • Loading branch information
kirillgroshkov committed Sep 4, 2024
1 parent 4d75b5f commit af00a0f
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 1 deletion.
21 changes: 20 additions & 1 deletion src/commondao/common.dao.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ const daoCfg: CommonDaoCfg<TestItemBM, TestItemDBM> = {
const dao = new CommonDao(daoCfg)

beforeEach(async () => {
jest.resetAllMocks()
await db.resetCache()
mockTime()
})
Expand Down Expand Up @@ -539,3 +538,23 @@ test('runInTransaction', async () => {
const items2 = await dao.query().runQuery()
expect(items2.map(i => i.id).sort()).toEqual(['id1', 'id4'])
})

test('should not be able to query by a non-indexed property', async () => {
const db = new InMemoryDB()
const dao = new CommonDao<TestItemBM>({
table: TEST_TABLE,
db,
excludeFromIndexes: ['k1'],
})

await dao.saveBatch(createTestItemsBM(5))

expect(await dao.query().filterEq('k2', 'v2').runQueryCount()).toBe(1)
expect(await dao.query().filterEq('k2', 'v-non-existing').runQueryCount()).toBe(0)

await expect(
dao.query().filterEq('k1', 'v1').runQueryCount(),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"cannot query on non-indexed property: TEST_TABLE.k1"`,
)
})
30 changes: 30 additions & 0 deletions src/commondao/common.dao.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ export class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity = BM> {
}

async runQueryExtended(q: DBQuery<DBM>, opt: CommonDaoOptions = {}): Promise<RunQueryResult<BM>> {
this.validateQueryIndexes(q) // throws if query uses `excludeFromIndexes` property
q.table = opt.table || q.table
const op = `runQuery(${q.pretty()})`
const started = this.logStarted(op, q.table)
Expand Down Expand Up @@ -323,6 +324,7 @@ export class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity = BM> {
q: DBQuery<DBM>,
opt: CommonDaoOptions = {},
): Promise<RunQueryResult<DBM>> {
this.validateQueryIndexes(q) // throws if query uses `excludeFromIndexes` property
q.table = opt.table || q.table
const op = `runQueryAsDBM(${q.pretty()})`
const started = this.logStarted(op, q.table)
Expand All @@ -340,6 +342,7 @@ export class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity = BM> {
}

async runQueryCount(q: DBQuery<DBM>, opt: CommonDaoOptions = {}): Promise<number> {
this.validateQueryIndexes(q) // throws if query uses `excludeFromIndexes` property
q.table = opt.table || q.table
const op = `runQueryCount(${q.pretty()})`
const started = this.logStarted(op, q.table)
Expand All @@ -355,6 +358,7 @@ export class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity = BM> {
mapper: AsyncMapper<BM, void>,
opt: CommonDaoStreamForEachOptions<BM> = {},
): Promise<void> {
this.validateQueryIndexes(q) // throws if query uses `excludeFromIndexes` property
q.table = opt.table || q.table
opt.skipValidation = opt.skipValidation !== false // default true
opt.errorMode ||= ErrorMode.SUPPRESS
Expand Down Expand Up @@ -404,6 +408,7 @@ export class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity = BM> {
mapper: AsyncMapper<DBM, void>,
opt: CommonDaoStreamForEachOptions<DBM> = {},
): Promise<void> {
this.validateQueryIndexes(q) // throws if query uses `excludeFromIndexes` property
q.table = opt.table || q.table
opt.skipValidation = opt.skipValidation !== false // default true
opt.errorMode ||= ErrorMode.SUPPRESS
Expand Down Expand Up @@ -452,6 +457,7 @@ export class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity = BM> {
* Stream as Readable, to be able to .pipe() it further with support of backpressure.
*/
streamQueryAsDBM(q: DBQuery<DBM>, opt: CommonDaoStreamOptions<DBM> = {}): ReadableTyped<DBM> {
this.validateQueryIndexes(q) // throws if query uses `excludeFromIndexes` property
q.table = opt.table || q.table
opt.skipValidation = opt.skipValidation !== false // default true
opt.errorMode ||= ErrorMode.SUPPRESS
Expand Down Expand Up @@ -490,6 +496,7 @@ export class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity = BM> {
* You can do `.pipe(transformNoOp)` to make it "valid again".
*/
streamQuery(q: DBQuery<DBM>, opt: CommonDaoStreamOptions<BM> = {}): ReadableTyped<BM> {
this.validateQueryIndexes(q) // throws if query uses `excludeFromIndexes` property
q.table = opt.table || q.table
opt.skipValidation = opt.skipValidation !== false // default true
opt.errorMode ||= ErrorMode.SUPPRESS
Expand Down Expand Up @@ -540,12 +547,14 @@ export class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity = BM> {
}

async queryIds(q: DBQuery<DBM>, opt: CommonDaoOptions = {}): Promise<string[]> {
this.validateQueryIndexes(q) // throws if query uses `excludeFromIndexes` property
q.table = opt.table || q.table
const { rows } = await this.cfg.db.runQuery(q.select(['id']), opt)
return rows.map(r => r.id)
}

streamQueryIds(q: DBQuery<DBM>, opt: CommonDaoStreamOptions<string> = {}): ReadableTyped<string> {
this.validateQueryIndexes(q) // throws if query uses `excludeFromIndexes` property
q.table = opt.table || q.table
opt.errorMode ||= ErrorMode.SUPPRESS

Expand All @@ -572,6 +581,7 @@ export class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity = BM> {
mapper: AsyncMapper<string, void>,
opt: CommonDaoStreamForEachOptions<string> = {},
): Promise<void> {
this.validateQueryIndexes(q) // throws if query uses `excludeFromIndexes` property
q.table = opt.table || q.table
opt.errorMode ||= ErrorMode.SUPPRESS

Expand Down Expand Up @@ -1015,6 +1025,7 @@ export class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity = BM> {
q: DBQuery<DBM>,
opt: CommonDaoStreamDeleteOptions<DBM> = {},
): Promise<number> {
this.validateQueryIndexes(q) // throws if query uses `excludeFromIndexes` property
this.requireWriteAccess()
this.requireObjectMutability(opt)
q.table = opt.table || q.table
Expand Down Expand Up @@ -1073,6 +1084,7 @@ export class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity = BM> {
patch: DBPatch<DBM>,
opt: CommonDaoOptions = {},
): Promise<number> {
this.validateQueryIndexes(q) // throws if query uses `excludeFromIndexes` property
this.requireWriteAccess()
this.requireObjectMutability(opt)
q.table = opt.table || q.table
Expand Down Expand Up @@ -1259,6 +1271,24 @@ export class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity = BM> {
return r!
}

/**
* Throws if query uses a property that is in `excludeFromIndexes` list.
*/
private validateQueryIndexes(q: DBQuery<DBM>): void {
const { excludeFromIndexes } = this.cfg
if (!excludeFromIndexes) return

for (const f of q._filters) {
_assert(
!excludeFromIndexes.includes(f.name),
`cannot query on non-indexed property: ${this.cfg.table}.${f.name as string}`,
{
query: q.pretty(),
},
)
}
}

protected logResult(started: number, op: string, res: any, table: string): void {
if (!this.cfg.logLevel) return

Expand Down

0 comments on commit af00a0f

Please sign in to comment.