Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement type predicates for query builders #846

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ export * from './util/compilable.js'
export * from './util/explainable.js'
export * from './util/streamable.js'
export * from './util/log.js'
export * from './util/query-utils.js'
export {
AnyAliasedColumn,
AnyAliasedColumnWithTable,
Expand Down
4 changes: 4 additions & 0 deletions src/query-builder/delete-query-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ export class DeleteQueryBuilder<DB, TB extends keyof DB, O>
this.#props = freeze(props)
}

get isDeleteQueryBuilder(): true {
return true
}

where<
RE extends ReferenceExpression<DB, TB>,
VE extends OperandValueExpressionOrList<DB, TB, RE>,
Expand Down
4 changes: 4 additions & 0 deletions src/query-builder/insert-query-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ export class InsertQueryBuilder<DB, TB extends keyof DB, O>
this.#props = freeze(props)
}

get isInsertQueryBuilder(): true {
return true
}

/**
* Sets the values to insert for an {@link Kysely.insertInto | insert} query.
*
Expand Down
16 changes: 16 additions & 0 deletions src/query-builder/merge-query-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ export class MergeQueryBuilder<DB, TT extends keyof DB, O>
this.#props = freeze(props)
}

get isMergeQueryBuilder(): true {
return true
}

/**
* Changes a `merge into` query to an `merge top into` query.
*
Expand Down Expand Up @@ -256,6 +260,10 @@ export class WheneableMergeQueryBuilder<
this.#props = freeze(props)
}

get isMergeQueryBuilder(): true {
return true
}

/**
* See {@link MergeQueryBuilder.top}.
*/
Expand Down Expand Up @@ -787,6 +795,10 @@ export class MatchedThenableMergeQueryBuilder<
this.#props = freeze(props)
}

get isMergeQueryBuilder(): true {
return true
}

/**
* Performs the `delete` action.
*
Expand Down Expand Up @@ -987,6 +999,10 @@ export class NotMatchedThenableMergeQueryBuilder<
this.#props = freeze(props)
}

get isMergeQueryBuilder(): true {
return true
}

/**
* Performs the `do nothing` action.
*
Expand Down
4 changes: 4 additions & 0 deletions src/query-builder/update-query-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ export class UpdateQueryBuilder<DB, UT extends keyof DB, TB extends keyof DB, O>
this.#props = freeze(props)
}

get isUpdateQueryBuilder(): true {
return true
}

where<
RE extends ReferenceExpression<DB, TB>,
VE extends OperandValueExpressionOrList<DB, TB, RE>,
Expand Down
129 changes: 129 additions & 0 deletions src/util/query-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { DeleteQueryBuilder } from '../query-builder/delete-query-builder.js'
import { InsertQueryBuilder } from '../query-builder/insert-query-builder.js'
import {
MatchedThenableMergeQueryBuilder,
MergeQueryBuilder,
NotMatchedThenableMergeQueryBuilder,
WheneableMergeQueryBuilder,
} from '../query-builder/merge-query-builder.js'
import { SelectQueryBuilder } from '../query-builder/select-query-builder.js'
import { UpdateQueryBuilder } from '../query-builder/update-query-builder.js'

type AnyMergeQueryBuilder<
DB,
TT extends keyof DB,
ST extends keyof DB,
UT extends TT | ST,
O
> =
| MergeQueryBuilder<DB, TT, O>
| WheneableMergeQueryBuilder<DB, TT, ST, O>
| MatchedThenableMergeQueryBuilder<DB, TT, ST, UT, O>
| NotMatchedThenableMergeQueryBuilder<DB, TT, ST, O>

/**
* A helper type that can accept any query builder object.
*
* You can use helper methods to determine the type of query builder object.
*
* See {@link isSelectQueryBuilder}, {@link isInsertQueryBuilder}, {@link isUpdateQueryBuilder},
* {@link isDeleteQueryBuilder}, {@link isMergeQueryBuilder}.
*
* ### Example
* ```ts
* import { AnyQueryBuilder, isSelectQueryBuilder } from 'kysely'
*
* function alwaysSelectAll<DB, TB extends keyof DB, O>(qb: AnyQueryBuilder<DB, TB, O>) {
* // Here `qb` could be any query builder object.
* // We can use the helper method `isSelectQueryBuilder` to determine the type.
* if (isSelectQueryBuilder(qb)) {
* // We can now use `SelectQueryBuilder` methods and properties.
* return qb.clearSelect().selectAll()
* }
* return qb
* }
* ```
*/
export type AnyQueryBuilder<
DB,
TB extends keyof DB,
O,
UT extends keyof DB = any,
ST extends keyof DB = any
> =
| SelectQueryBuilder<DB, TB, O>
| InsertQueryBuilder<DB, TB, O>
| UpdateQueryBuilder<DB, UT, TB, O>
| DeleteQueryBuilder<DB, TB, O>
| AnyMergeQueryBuilder<DB, TB, ST, TB | ST, O>

/**
* A helper method to determine if a query builder object is of type {@link SelectQueryBuilder}.
*
* Useful when using the {@link AnyQueryBuilder} type.
*/
export function isSelectQueryBuilder<DB, TB extends keyof DB, O>(
qb: AnyQueryBuilder<DB, TB, O>
): qb is SelectQueryBuilder<DB, TB, O> {
return !!(qb as SelectQueryBuilder<DB, TB, O>).isSelectQueryBuilder
}

/**
* A helper method to determine if a query builder object is of type {@link InsertQueryBuilder}.
*
* Useful when using the {@link AnyQueryBuilder} type.
*/
export function isInsertQueryBuilder<DB, TB extends keyof DB, O>(
qb: AnyQueryBuilder<DB, TB, O>
): qb is InsertQueryBuilder<DB, TB, O> {
return !!(qb as InsertQueryBuilder<DB, TB, O>).isInsertQueryBuilder
}

/**
* A helper method to determine if a query builder object is of type {@link UpdateQueryBuilder}.
*
* Useful when using the {@link AnyQueryBuilder} type.
*/
export function isUpdateQueryBuilder<
DB,
UT extends keyof DB,
TB extends keyof DB,
O
>(qb: AnyQueryBuilder<DB, TB, O, UT>): qb is UpdateQueryBuilder<DB, UT, TB, O> {
return !!(qb as UpdateQueryBuilder<DB, UT, TB, O>).isUpdateQueryBuilder
}

/**
* A helper method to determine if a query builder object is of type {@link DeleteQueryBuilder}.
*
* Useful when using the {@link AnyQueryBuilder} type.
*/
export function isDeleteQueryBuilder<DB, TB extends keyof DB, O>(
qb: AnyQueryBuilder<DB, TB, O>
): qb is DeleteQueryBuilder<DB, TB, O> {
return !!(qb as DeleteQueryBuilder<DB, TB, O>).isDeleteQueryBuilder
}

/**
* A helper method to determine if a query builder object is of type {@link MergeQueryBuilder}.
*
* Useful when using the {@link AnyQueryBuilder} type.
*/
export function isMergeQueryBuilder<
DB,
TB extends keyof DB,
O,
ST extends keyof DB = any
>(
qb: AnyQueryBuilder<DB, TB, O, ST>
): qb is typeof qb extends MergeQueryBuilder<DB, TB, O>
? MergeQueryBuilder<DB, TB, O>
: typeof qb extends WheneableMergeQueryBuilder<DB, TB, ST, O>
? WheneableMergeQueryBuilder<DB, TB, ST, O>
: typeof qb extends MatchedThenableMergeQueryBuilder<DB, TB, ST, TB | ST, O>
? MatchedThenableMergeQueryBuilder<DB, TB, ST, TB | ST, O>
: typeof qb extends NotMatchedThenableMergeQueryBuilder<DB, TB, ST, O>
? NotMatchedThenableMergeQueryBuilder<DB, TB, ST, O>
: never {
return !!(qb as any).isMergeQueryBuilder
}
136 changes: 136 additions & 0 deletions test/node/src/query-utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import {
AnyQueryBuilder,
MergeResult,
isDeleteQueryBuilder,
isInsertQueryBuilder,
isMergeQueryBuilder,
isSelectQueryBuilder,
isUpdateQueryBuilder,
} from '../../../'

import {
destroyTest,
initTest,
TestContext,
expect,
DIALECTS,
Database,
} from './test-setup.js'

for (const dialect of DIALECTS) {
describe(`${dialect}: query-utils`, () => {
let ctx: TestContext

before(async function () {
ctx = await initTest(this, dialect)
})

after(async () => {
await destroyTest(ctx)
})

it('should isSelectQueryBuilder be true', async () => {
const query = ctx.db
.selectFrom('person')
.where('first_name', '=', 'Jennifer')

expect(query.isSelectQueryBuilder).to.be.true
expect(isSelectQueryBuilder(query)).to.be.true
expect(isInsertQueryBuilder(query)).to.be.false
expect(isUpdateQueryBuilder(query)).to.be.false
expect(isDeleteQueryBuilder(query)).to.be.false
expect(isMergeQueryBuilder(query)).to.be.false
})

it('should isInsertQueryBuilder be true', async () => {
const query = ctx.db.insertInto('person').values({
first_name: 'David',
last_name: 'Bowie',
gender: 'male',
})

expect(query.isInsertQueryBuilder).to.be.true
expect(isSelectQueryBuilder(query)).to.be.false
expect(isInsertQueryBuilder(query)).to.be.true
expect(isUpdateQueryBuilder(query)).to.be.false
expect(isDeleteQueryBuilder(query)).to.be.false
expect(isMergeQueryBuilder(query)).to.be.false
})

it('should isUpdateQueryBuilder be true', async () => {
const query = ctx.db
.updateTable('person')
.where('first_name', '=', 'John')
.set({ last_name: 'Wick' })

expect(query.isUpdateQueryBuilder).to.be.true
expect(isSelectQueryBuilder(query)).to.be.false
expect(isInsertQueryBuilder(query)).to.be.false
expect(isUpdateQueryBuilder(query)).to.be.true
expect(isDeleteQueryBuilder(query)).to.be.false
expect(isMergeQueryBuilder(query)).to.be.false
})

it('should isDeleteQueryBuilder be true', async () => {
const query = ctx.db.deleteFrom('person').where('first_name', '=', 'John')

expect(query.isDeleteQueryBuilder).to.be.true
expect(isSelectQueryBuilder(query)).to.be.false
expect(isInsertQueryBuilder(query)).to.be.false
expect(isUpdateQueryBuilder(query)).to.be.false
expect(isDeleteQueryBuilder(query)).to.be.true
expect(isMergeQueryBuilder(query)).to.be.false
})

it('should isMergeQueryBuilder be true', async () => {
// MergeQueryBuilder
let query: AnyQueryBuilder<Database, 'person', MergeResult> =
ctx.db.mergeInto('person')

expect(query.isMergeQueryBuilder).to.be.true
expect(isSelectQueryBuilder(query)).to.be.false
expect(isInsertQueryBuilder(query)).to.be.false
expect(isUpdateQueryBuilder(query)).to.be.false
expect(isDeleteQueryBuilder(query)).to.be.false
expect(isMergeQueryBuilder(query)).to.be.true

// WheneableMergeQueryBuilder
query = ctx.db
.mergeInto('person')
.using('pet', 'person.id', 'pet.owner_id')

expect(query.isMergeQueryBuilder).to.be.true
expect(isSelectQueryBuilder(query)).to.be.false
expect(isInsertQueryBuilder(query)).to.be.false
expect(isUpdateQueryBuilder(query)).to.be.false
expect(isDeleteQueryBuilder(query)).to.be.false
expect(isMergeQueryBuilder(query)).to.be.true

// MatchedThenableMergeQueryBuilder
query = ctx.db
.mergeInto('person')
.using('pet', 'person.id', 'pet.owner_id')
.whenMatched()

expect(query.isMergeQueryBuilder).to.be.true
expect(isSelectQueryBuilder(query)).to.be.false
expect(isInsertQueryBuilder(query)).to.be.false
expect(isUpdateQueryBuilder(query)).to.be.false
expect(isDeleteQueryBuilder(query)).to.be.false
expect(isMergeQueryBuilder(query)).to.be.true

// NotMatchedThenableMergeQueryBuilder
query = ctx.db
.mergeInto('person')
.using('pet', 'person.id', 'pet.owner_id')
.whenNotMatched()

expect(query.isMergeQueryBuilder).to.be.true
expect(isSelectQueryBuilder(query)).to.be.false
expect(isInsertQueryBuilder(query)).to.be.false
expect(isUpdateQueryBuilder(query)).to.be.false
expect(isDeleteQueryBuilder(query)).to.be.false
expect(isMergeQueryBuilder(query)).to.be.true
})
})
}
Loading