Because TypeScript 4.1 supports template literal string types, the function syntax is no longer necessary. You can now use strings while maintaining type-safety. The function syntax is removed since version 4.
To help with the upgrade, you can run npx typed-knex -u string-parameters
to automatically switch over to the string syntax.
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.95.0, TypeScript v4.2.3 and Node.js v14.11.0
- Because TypeScript 4.1 supports template literal string types, the function syntax is no longer necessary. You can now use strings while maintaining type-safety. The function syntax is removed.
Run
npx typed-knex -u string-parameters
to automatically upgrade. .onColumn()
is deprecated. Use.on()
. Remember that the columns switched eg.onColumns(i=>i.prop1, '=' j=>j.prop2) should become .on("prop2", '=', "prop1")
. Runnpx typed-knex -u join-on-columns-to-on
to automatically upgrade.- The use of optional columns (
@Column() public nickName?: string;
) is deprecated. This was used to signal a nullable column. The correct way to do this is@Column() public nickName: string | null;
.
To reference a column, use the name. Like this .select("name")
or this .where("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("category")
.where("name", "Hejlsberg")
.select("id", "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 Table
decorator to reference 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 Table
to reference another table.
import { Column, Table } from "@wwwouter/typed-knex";
@Table("userCategories")
export class UserCategory {
@Column({ primary: true })
public id: string;
@Column()
public name: string;
@Column()
public year: number;
}
@Table("users")
export class User {
@Column({ primary: true })
public id: string;
@Column()
public name: string;
@Column()
public categoryId: string;
@Column({ name: "categoryId" })
public category: UserCategory;
@Column()
public someNullableValue: string | null;
}
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
- insertSelect
- del
- delByPrimaryKey
- updateItem
- updateItemByPrimaryKey
- updateItemsByPrimaryKey
- execute
- select
- where
- andWhere
- orWhere
- whereNot
- whereColumn
- whereNull
- orWhereNull
- whereNotNull
- orWhereNotNull
- orderBy
- orderByRaw
- 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'
Hook that is run before doing an insert. Execute this function as soon as possible. For example at the top of index.ts
or server.ts
.
registerBeforeInsertTransform((item: any, typedQueryBuilder: ITypedQueryBuilder<{}, {}, {}>) => {
if (typedQueryBuilder.columns.find(column => column.name === 'created_at') && !item.hasOwnProperty('created_at')) {
item.created_at = new Date();
}
if (typedQueryBuilder.columns.find(column => column.name === 'updated_at') && !item.hasOwnProperty('updated_at')) {
item.updated_at = new Date();
}
if (typedQueryBuilder.columns.find(column => column.name === 'id') && !item.hasOwnProperty('id')) {
item.id = guid();
}
return item;
});
Hook that is run before doing an update. Execute this function as soon as possible. For example at the top of index.ts
or server.ts
.
registerBeforeUpdateTransform((item: any, typedQueryBuilder: ITypedQueryBuilder<{}, {}, {}>) => {
if (typedQueryBuilder.columns.find("name" === 'updated_at') && !item.hasOwnProperty('updated_at')) {
item.updated_at = new Date();
}
return item;
});
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("id");
typedKnex.query(User).select("id","name");
https://knexjs.org/#Builder-where
typedKnex.query(User).where("name", "name");
Or with operator
typedKnex.query(User).where("name", "like", "%user%");
// select * from "users" where "users"."name" like '%user%'
typedKnex
.query(User)
.where("name", "name")
.andWhere("name", "name");
typedKnex
.query(User)
.where("name", "name")
.andWhere("name", "like", "%na%");
typedKnex
.query(User)
.where("name", "name")
.orWhere("name", "name");
typedKnex
.query(User)
.where("name", "name")
.orWhere("name", "like", "%na%");
https://knexjs.org/#Builder-whereNot
typedKnex.query(User).whereNot("name", "name");
To use in subqueries. First parameter is for sub query columns and the third parameter is for columns from the parent query.
typedKnex.query(User).whereNotExists(UserSetting, (subQuery) => {
subQuery.whereColumn("userId", "=", "id");
});
typedKnex.query(User).whereNull("name");
typedKnex
.query(User)
.whereNull("name")
.orWhereNull("name");
typedKnex.query(User).whereNotNull("name");
typedKnex
.query(User)
.whereNotNull("name")
.orWhereNotNull("name");
typedKnex.query(User).orderBy("id");
await typedKnex.query(User).orderByRaw("SUM(??) DESC", "users.year");
// select * from "users" order by SUM("users"."year") DESC
typedKnex.query(User).innerJoinColumn("category");
typedKnex.query(User).innerJoinTableOnFunction("evilTwin", User, (join) => {
join.on(
"id",
"=",
"id"
)
.andOn(
"name",
"=",
"id"
)
.orOn(
"someValue",
"=",
"id"
)
.onVal("name", "=", "1")
.andOnVal("name", "=", "2")
.orOnVal("name", "=", "3")
.onNull("name");
});
typedKnex.query(User).leftOuterJoinColumn("category");
typedKnex.query(User).leftOuterJoinTableOnFunction("evilTwin", User, (join) => {
join.on(
"id",
"=",
"id"
)
.andOn(
"name",
"=",
"id"
)
.orOn(
"someValue",
"=",
"id"
)
.onVal("name", "=", "1")
.andOnVal("name", "=", "2")
.orOnVal("name", "=", "3")
.onNull("name");
});
typedKnex.query(User).selectRaw("otherId", Number, "select other.id from other");
typedKnex
.query(UserCategory)
.select("id")
.selectQuery("total", Number, User, (subQuery) => {
subQuery.count("id", "total").whereColumn("categoryId", "=", "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", "d","name");
typedKnex.query(User).whereIn("name", ["user1", "user2"]);
typedKnex.query(User).whereNotIn("name", ["user1", "user2"]);
typedKnex
.query(User)
.whereIn("name", ["user1", "user2"])
.orWhereIn("name", ["user3", "user4"]);
typedKnex
.query(User)
.whereIn("name", ["user1", "user2"])
.orWhereNotIn("name", ["user3", "user4"]);
typedKnex.query(UserCategory).whereBetween("year", [1, 2037]);
typedKnex.query(User).whereNotBetween("year", [1, 2037]);
typedKnex
.query(User)
.whereBetween("year", [1, 10])
.orWhereBetween("year", [100, 1000]);
typedKnex
.query(User)
.whereBetween("year", [1, 10])
.orWhereNotBetween("year", [100, 1000]);
typedKnex.query(User).whereExists(UserSetting, (subQuery) => {
subQuery.whereColumn("userId", "=", "id");
});
typedKnex.query(User).orWhereExists(UserSetting, (subQuery) => {
subQuery.whereColumn("userId", "=", "id");
});
typedKnex.query(User).whereNotExists(UserSetting, (subQuery) => {
subQuery.whereColumn("userId", "=", "id");
});
typedKnex.query(User).orWhereNotExists(UserSetting, (subQuery) => {
subQuery.whereColumn("userId", "=", "id");
});
typedKnex
.query(User)
.whereParentheses((sub) => sub.where("id", "1").orWhere("id", "2"))
.orWhere("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("someValue")
.selectRaw("total", Number, 'SUM("numericValue")')
.groupBy("someValue");
typedKnex.query(User).having("numericValue", ">", 10);
typedKnex.query(User).havingNull("numericValue");
typedKnex.query(User).havingNotNull("numericValue");
typedKnex.query(User).havingIn("name", ["user1", "user2"]);
typedKnex.query(User).havingNotIn("name", ["user1", "user2"]);
typedKnex.query(User).havingExists(UserSetting, (subQuery) => {
subQuery.whereColumn("userId", "=", "id");
});
typedKnex.query(User).havingNotExists(UserSetting, (subQuery) => {
subQuery.whereColumn("userId", "=", "id");
});
typedKnex.query(User).havingBetween("numericValue", [1, 10]);
typedKnex.query(User).havingNotBetween("numericValue", [1, 10]);
typedKnex.query(User).union(User, (subQuery) => {
subQuery.select("id").where("numericValue", 12);
});
typedKnex
.query(User)
.select("id")
.unionAll(User, (subQuery) => {
subQuery.select("id").where("numericValue", 12);
});
typedKnex.query(User).min("numericValue", "minNumericValue");
typedKnex.query(User).count("numericValue", "countNumericValue");
typedKnex.query(User).countDistinct("numericValue", "countDistinctNumericValue");
typedKnex.query(User).max("numericValue", "maxNumericValue");
typedKnex.query(User).sum("numericValue", "sumNumericValue");
typedKnex.query(User).sumDistinct("numericValue", "sumDistinctNumericValue");
typedKnex.query(User).avg("numericValue", "avgNumericValue");
typedKnex.query(User).avgDistinct("numericValue", "avgDistinctNumericValue");
typedKnex
.query(User)
.select("id")
.clearSelect()
.select("name");
typedKnex
.query(User)
.where("id", "name")
.clearWhere()
.where(("name", "name");
typedKnex
.query(User)
.orderBy("id")
.clearOrder()
.orderBy(("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("name");
);
Use keepFlat
to prevent unflattening of the result.
const item = await typedKnex
.query(User)
.where("name", 'name')
.innerJoinColumn("category");
.select("name", "category.name")
.getFirst();
// returns { name: 'user name', category: { name: 'category name' }}
const item = await typedKnex
.query(User)
.where("name", 'name')
.innerJoinColumn("category");
.select("name", "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("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("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("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("name", "name")
.getSingle();
const users = await typedKnex
.query(User)
.whereNotNull("name")
.getMany();
typedKnex.query(User);
typedKnex.query(User);
typedKnex.query(User);
await typedKnex.query(User);
.selectRaw('f', String, '\'fixedValue\'')
.select("name")
.distinct()
.whereNotNull("name")
.insertSelect(UserSetting, "id", "initialValue");
// insert into "userSettings" ("userSettings"."id","userSettings"."initialValue") select distinct ('fixedValue') as "f", "users"."name" as "name" from "users" where "users"."name" is not null
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 validateTables
function to make sure that the Table
's and Column
's in TypeScript exist in the database.
import * as Knex from "knex";
import { validateTables } from "@wwwouter/typed-knex";
const knex = Knex({
client: "pg",
connection: "postgres://user:pass@localhost:5432/dbname",
});
await validateTables(knex);
npm test
npm version major|minor|patch
update CHANGELOG.md
git commit --amend
npm publish --access=public --otp=CODE
git push
for beta
update version to x.x.x-beta.x
npm publish --access public --tag beta --otp=CODE