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 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.
- 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", UserCategory, "id", "=", "categoryId").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
- insertItemWithReturning
- insertItems
- insertSelect
- del
- delByPrimaryKey
- updateItem
- updateItemWithReturning
- updateItemByPrimaryKey
- updateItemsByPrimaryKey
- execute
- select
- where
- andWhere
- orWhere
- whereNot
- whereColumn
- whereNull
- orWhereNull
- whereNotNull
- orWhereNotNull
- orderBy
- orderByRaw
- innerJoinColumn
- innerJoin
- innerJoinTableOnFunction
- leftOuterJoin
- 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
- distinctOn
- 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);
Use getColumnAlias
to get the underlying alias of a column, to use in a raw
function.
const query = typedKnex.query(UserCategory);
query.selectRaw("hash", String, `hashFunction(${query.getColumnAlias("name")})`).select("id");
// select (hashFunction("userCategories"."name")) as "hash", "userCategories"."id" as "id" from "userCategories"
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");
});
Use getColumn
when nesting
query.whereExists(User, (subQuery1) => {
subQuery1.whereColumn('status', '=', 'status'); // Compares subQuery1 with its parent (query).
subQuery1.whereExists(User, (subQuery2) => {
subQuery2.whereColumn(subQuery2.getColumn('status'), '=', query.getColumn('status')); // Compares subQuery2 with the first parent (query)
subQuery2.whereExists(User, (subQuery3) => {
subQuery3.whereColumn(subQuery3.getColumn('status'), '=', subQuery1.getColumn('status')); // Compares subQuery3 with the second parent (subQuery1)
});
});
});
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).innerJoin("category", UserCategory, "id", "=", "categoryId");
// select * from "users" inner join "userCategories" as "category" on "category"."id" = "users"."categoryId"
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).leftOuterJoin("category", UserCategory, "id", "=", "categoryId");
// select * from "users" left outer join "userCategories" as "category" on "category"."id" = "users"."categoryId"
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);
query.insertItemWithReturning({ id: "newId" });
// insert into "users" ("id") values ('newId') returning *
query.insertItemWithReturning({ id: "newId" }, ["id"]);
// insert into "users" ("id") values ('newId') returning "users"."id"
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);
query.updateItemWithReturning({ id: "newId" });
// update "users" set "id" = 'newId' returning *
query.updateItemWithReturning({ id: "newId" }, ["id"]);
// update "users" set "id" = 'newId' returning "users"."id"
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(UserCategory).select("id").distinctOn(["name"]);
// select distinct on ("userCategories"."name") "userCategories"."id" as "id" from "userCategories"
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