Standing on the shoulders of Knex.js, but now everything is typed!
Goals:
- Be useful for 80% of the use cases, for the other 20% easily switch to lower-level Knex.js.
- Be as concise a possible.
- Mirror Knex.js as much a possible, with these exceptions:
- Don't use
this
.- Be selective on what returns a
Promise
and what not.- Less overloading, which makes typings easier and code completion better.
- Get the most of the benefits TypeScript provides: type-checking of parameters, typed results, rename refactorings.
Install:
npm install @wwwouter/typed-knex
Make sure experimentalDecorators and emitDecoratorMetadata are turned on in your tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
...
},
...
}
Tested with Knex.js v0.20.7, TypeScript v3.7.4 and Node.js v12.13.0
To reference a column, use an arrow function. Like this .select(i=>i.name)
or this .where(i=>i.name, "Hejlsberg")
import * as Knex from 'knex';
import { TypedKnex } from '@wwwouter/typed-knex';
const knex = Knex({
client: 'pg',
connection: 'postgres://user:pass@localhost:5432/dbname'
});
async function example() {
const typedKnex = new TypedKnex(knex);
const query = typedKnex
.query(User)
.innerJoin(i => i.category)
.where(i => i.name, 'Hejlsberg')
.select(i => [i.id, i.category.name]);
const oneUser = await query.getSingle();
console.log(oneUser.id); // Ok
console.log(oneUser.category.name); // Ok
console.log(oneUser.name); // Compilation error
}
Use the Entity
decorator to refence a table and use the Column
decorator to reference a column.
Use @Column({ primary: true })
for primary key columns.
Use @Column({ name: '[column name]' })
on property with the type of another Entity
to reference another table.
import { Column, Entity } from '@wwwouter/typed-knex';
@Entity('userCategories')
export class UserCategory {
@Column({ primary: true })
public id: string;
@Column()
public name: string;
@Column()
public year: number;
}
@Entity('users')
export class User {
@Column({ primary: true })
public id: string;
@Column()
public name: string;
@Column({ name: 'categoryId' })
public category: UserCategory;
}
import * as Knex from 'knex';
import { TypedKnex } from '@wwwouter/typed-knex';
const knex = Knex({
client: 'pg',
connection: 'postgres://user:pass@localhost:5432/dbname'
});
const typedKnex = new TypedKnex(knex);
- findByPrimaryKey
- getFirstOrNull
- getFirst
- getSingleOrNull
- getSingle
- getMany
- getCount
- insertItem
- insertItems
- del
- delByPrimaryKey
- updateItem
- updateItemByPrimaryKey
- updateItemsByPrimaryKey
- execute
- select
- where
- andWhere
- orWhere
- whereNot
- whereColumn
- whereNull
- orWhereNull
- whereNotNull
- orWhereNotNull
- orderBy
- innerJoinColumn
- innerJoinTableOnFunction
- leftOuterJoinColumn
- leftOuterJoinTableOnFunction
- selectRaw
- selectQuery
- whereIn
- whereNotIn
- orWhereIn
- orWhereNotIn
- whereBetween
- whereNotBetween
- orWhereBetween
- orWhereNotBetween
- whereExists
- orWhereExists
- whereNotExists
- orWhereNotExists
- whereParentheses
- groupBy
- having
- havingNull
- havingNotNull
- havingIn
- havingNotIn
- havingExists
- havingNotExists
- havingBetween
- havingNotBetween
- union
- unionAll
- min
- count
- countDistinct
- max
- sum
- sumDistinct
- avg
- avgDistinct
- clearSelect
- clearWhere
- clearOrder
- limit
- offset
- whereRaw
- havingRaw
- truncate
- distinct
- clone
- groupByRaw
const tableName = getTableName(User);
// tableName = 'users'
const columnName = getColumnName(User, 'id');
// columnName = 'id'
Use typedKnex.query(Type)
to create a query for the table referenced by Type
const query = typedKnex.query(User);
https://knexjs.org/#Builder-select
typedKnex.query(User).select(i => i.id);
typedKnex.query(User).select(i => [i.id, i.name]);
https://knexjs.org/#Builder-where
typedKnex.query(User).where(i => i.name, 'name');
Or with operator
typedKnex.query(User).where(c => c.name, 'like', '%user%');
// select * from "users" where "users"."name" like '%user%'
typedKnex
.query(User)
.where(i => i.name, 'name')
.andWhere(i => i.name, 'name');
typedKnex
.query(User)
.where(i => i.name, 'name')
.andWhere(i => i.name, 'like', '%na%');
typedKnex
.query(User)
.where(i => i.name, 'name')
.orWhere(i => i.name, 'name');
typedKnex
.query(User)
.where(i => i.name, 'name')
.orWhere(i => i.name, 'like', '%na%');
https://knexjs.org/#Builder-whereNot
typedKnex.query(User).whereNot(i => i.name, 'name');
To use in subqueries
typedKnex.query(User).whereNotExists(UserSetting, (subQuery, parentColumn) => {
subQuery.whereColumn(i => i.userId, '=', parentColumn.id);
});
typedKnex.query(User).whereNull(i => i.name);
typedKnex
.query(User)
.whereNull(i => i.name)
.orWhereNull(i => i.name);
typedKnex.query(User).whereNotNull(i => i.name);
typedKnex
.query(User)
.whereNotNull(i => i.name)
.orWhereNotNull(i => i.name);
typedKnex.query(User).orderBy(i => i.id);
typedKnex.query(User).innerJoinColumn(i => i.category);
typedKnex.query(User).innerJoinTableOnFunction('evilTwin', User, join => {
join.onColumns(
i => i.id,
'=',
j => j.id
);
});
typedKnex.query(User).leftOuterJoinColumn(i => i.category);
typedKnex.query(User).leftOuterJoinTableOnFunction('evilTwin', User, join => {
join.onColumns(
i => i.id,
'=',
j => j.id
);
});
typedKnex
.query(User)
.selectRaw('otherId', Number, 'select other.id from other');
typedKnex
.query(UserCategory)
.select(i => i.id)
.selectQuery('total', Number, User, (subQuery, parentColumn) => {
subQuery
.count(i => i.id, 'total')
.whereColumn(c => c.categoryId, '=', parentColumn.id);
});
select "userCategories"."id" as "id", (select count("users"."id") as "total" from "users" where "users"."categoryId" = "userCategories"."id") as "total" from "userCategories"
const user = await typedKnex
.query(User)
.findByPrimaryKey('id', i => [i.id, i.name]);
typedKnex.query(User).whereIn(i => i.name, ['user1', 'user2']);
typedKnex.query(User).whereNotIn(i => i.name, ['user1', 'user2']);
typedKnex
.query(User)
.whereIn(i => i.name, ['user1', 'user2'])
.orWhereIn(i => i.name, ['user3', 'user4']);
typedKnex
.query(User)
.whereIn(i => i.name, ['user1', 'user2'])
.orWhereNotIn(i => i.name, ['user3', 'user4']);
typedKnex.query(UserCategory).whereBetween(i => i.year, [1, 2037]);
typedKnex.query(User).whereNotBetween(i => i.year, [1, 2037]);
typedKnex
.query(User)
.whereBetween(c => c.year, [1, 10])
.orWhereBetween(c => c.year, [100, 1000]);
typedKnex
.query(User)
.whereBetween(c => c.year, [1, 10])
.orWhereNotBetween(c => c.year, [100, 1000]);
typedKnex.query(User).whereExists(UserSetting, (subQuery, parentColumn) => {
subQuery.whereColumn(c => c.userId, '=', parentColumn.id);
});
typedKnex.query(User).orWhereExists(UserSetting, (subQuery, parentColumn) => {
subQuery.whereColumn(c => c.userId, '=', parentColumn.id);
});
typedKnex.query(User).whereNotExists(UserSetting, (subQuery, parentColumn) => {
subQuery.whereColumn(c => c.userId, '=', parentColumn.id);
});
typedKnex
.query(User)
.orWhereNotExists(UserSetting, (subQuery, parentColumn) => {
subQuery.whereColumn(c => c.userId, '=', parentColumn.id);
});
typedKnex
.query(User)
.whereParentheses(sub => sub.where(c => c.id, '1').orWhere(c => c.id, '2'))
.orWhere(c => c.name, 'Tester');
const queryString = query.toQuery();
console.log(queryString);
Outputs:
select * from "users" where ("users"."id" = '1' or "users"."id" = '2') or "users"."name" = 'Tester'
typedKnex
.query(User)
.select(c => c.someValue)
.selectRaw('total', Number, 'SUM("numericValue")')
.groupBy(c => c.someValue);
typedKnex.query(User).having(c => c.numericValue, '>', 10);
typedKnex.query(User).havingNull(c => c.numericValue);
typedKnex.query(User).havingNotNull(c => c.numericValue);
typedKnex.query(User).havingIn(c => c.name, ['user1', 'user2']);
typedKnex.query(User).havingNotIn(c => c.name, ['user1', 'user2']);
typedKnex.query(User).havingExists(UserSetting, (subQuery, parentColumn) => {
subQuery.whereColumn(c => c.userId, '=', parentColumn.id);
});
typedKnex.query(User).havingNotExists(UserSetting, (subQuery, parentColumn) => {
subQuery.whereColumn(c => c.userId, '=', parentColumn.id);
});
typedKnex.query(User).havingBetween(c => c.numericValue, [1, 10]);
typedKnex.query(User).havingNotBetween(c => c.numericValue, [1, 10]);
typedKnex.query(User).union(User, subQuery => {
subQuery.select(c => [c.id]).where(c => c.numericValue, 12);
});
typedKnex
.query(User)
.select(c => [c.id])
.unionAll(User, subQuery => {
subQuery.select(c => [c.id]).where(c => c.numericValue, 12);
});
typedKnex.query(User).min(c => c.numericValue, 'minNumericValue');
typedKnex.query(User).count(c => c.numericValue, 'countNumericValue');
typedKnex
.query(User)
.countDistinct(c => c.numericValue, 'countDistinctNumericValue');
typedKnex.query(User).max(c => c.numericValue, 'maxNumericValue');
typedKnex.query(User).sum(c => c.numericValue, 'sumNumericValue');
typedKnex
.query(User)
.sumDistinct(c => c.numericValue, 'sumDistinctNumericValue');
typedKnex.query(User).avg(c => c.numericValue, 'avgNumericValue');
typedKnex
.query(User)
.avgDistinct(c => c.numericValue, 'avgDistinctNumericValue');
typedKnex
.query(User)
.select(i => i.id)
.clearSelect()
.select((i = i.name));
typedKnex
.query(User)
.where(i => i.id, 'name')
.clearWhere()
.where((i = i.name), 'name');
typedKnex
.query(User)
.orderBy(i => i.id)
.clearOrder()
.orderBy((i = i.name));
typedKnex.query(User).limit(10);
typedKnex.query(User).offset(10);
Use useKnexQueryBuilder
to get to the underlying Knex.js query builder.
const query = typedKnex.query(User)
.useKnexQueryBuilder(queryBuilder => queryBuilder.where('somethingelse', 'value')
.select(i=>i.name);
);
Use keepFlat
to prevent unflattening of the result.
const item = await typedKnex
.query(User)
.where(i => i.name, 'name')
.innerJoinColumn(i => i.category);
.select(i=>[i.name, i.category.name)
.getFirst();
// returns { name: 'user name', category: { name: 'category name' }}
const item = await typedKnex
.query(User)
.where(i => i.name, 'name')
.innerJoinColumn(i => i.category);
.select(i=>[i.name, i.category.name)
.keepFlat()
.getFirst();
// returns { name: 'user name', category.name: 'category name' }
const query = typedKnex.query(User);
console.log(query.toQuery()); // select * from "users"
Result | No item | One item | Many items |
---|---|---|---|
getFirst |
Error |
Item | First item |
getSingle |
Error |
Item | Error |
getFirstOrNull |
null |
Item | First item |
getSingleOrNull |
null |
Item | Error |
const user = await typedKnex
.query(User)
.where(i => i.name, 'name')
.getFirstOrNull();
Result | No item | One item | Many items |
---|---|---|---|
getFirst |
Error |
Item | First item |
getSingle |
Error |
Item | Error |
getFirstOrNull |
null |
Item | First item |
getSingleOrNull |
null |
Item | Error |
const user = await typedKnex
.query(User)
.where(i => i.name, 'name')
.getFirst();
Result | No item | One item | Many items |
---|---|---|---|
getFirst |
Error |
Item | First item |
getSingle |
Error |
Item | Error |
getFirstOrNull |
null |
Item | First item |
getSingleOrNull |
null |
Item | Error |
const user = await typedKnex
.query(User)
.where(i => i.name, 'name')
.getSingleOrNull();
Result | No item | One item | Many items |
---|---|---|---|
getFirst |
Error |
Item | First item |
getSingle |
Error |
Item | Error |
getFirstOrNull |
null |
Item | First item |
getSingleOrNull |
null |
Item | Error |
const user = await typedKnex
.query(User)
.where(i => i.name, 'name')
.getSingle();
const users = await typedKnex
.query(User)
.whereNotNull(i => i.name)
.getMany();
typedKnex.query(User);
typedKnex.query(User);
typedKnex.query(User);
typedKnex.query(User);
typedKnex.query(User);
typedKnex.query(User);
typedKnex.query(User);
typedKnex.query(User);
typedKnex.query(User);
typedKnex.query(User);
typedKnex.query(User);
const typedKnex = new TypedKnex(database);
const transaction = await typedKnex.beginTransaction();
try {
await typedKnex
.query(User)
.transacting(transaction)
.insertItem(user1);
await typedKnex
.query(User)
.transacting(transaction)
.insertItem(user2);
await transaction.commit();
} catch (error) {
await transaction.rollback();
// handle error
}
typedKnex.query(User);
typedKnex.query(User);
typedKnex.query(User);
typedKnex.query(User);
const typedKnex = new TypedKnex(database);
const transaction = await typedKnex.beginTransaction();
try {
await typedKnex
.query(User)
.transacting(transaction)
.insertItem(user1);
await typedKnex
.query(User)
.transacting(transaction)
.insertItem(user2);
await transaction.commit();
} catch (error) {
await transaction.rollback();
// handle error
}
Use the validateEntities
function to make sure that the Entitiy
's and Column
's in TypeScript exist in the database.
import * as Knex from 'knex';
import { validateEntities } from '@wwwouter/typed-knex';
const knex = Knex({
client: 'pg',
connection: 'postgres://user:pass@localhost:5432/dbname'
});
await validateEntities(knex);
npm test
npm version major|minor|patch
npm publish --access public
git push
for beta
npm publish --access public --tag beta