thomasboyt / typed-knex

A TypeScript wrapper for Knex.js

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

typed-knex

npm version Build Status Dependencies Status

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

Documentation

Quick example

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)
        .select(i => i.id)
        .where(i => i.name, 'Hejlsberg');

    const oneUser = await query.getSingle();

    console.log(oneUser.id); // Ok
    console.log(oneUser.name); // Compilation error
}

Define entities

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;
}

Create instance

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);

Helper

Querybuilder

General

Getting the results (Promises)

Building the query

getTableName

const tableName = getTableName(User);

// tableName = 'users'

getTableName

const columnName = getColumnName(User, 'id');

// columnName = 'id'

query

Use typedKnex.query(Type) to create a query for the table referenced by Type

const query = typedKnex.query(User);

select

https://knexjs.org/#Builder-select

typedKnex.query(User).select(i => i.id);
typedKnex.query(User).select(i => [i.id, i.name]);

where

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%'

andWhere

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%');

orWhere

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%');

whereNot

https://knexjs.org/#Builder-whereNot

typedKnex.query(User).whereNot(i => i.name, 'name');

whereColumn

To use in subqueries

typedKnex.query(User).whereNotExists(UserSetting, (subQuery, parentColumn) => {
    subQuery.whereColumn(i => i.userId, '=', parentColumn.id);
});

whereNull

typedKnex.query(User).whereNull(i => i.name);

orWhereNull

typedKnex
    .query(User)
    .whereNull(i => i.name)
    .orWhereNull(i => i.name);

whereNotNull

typedKnex.query(User).whereNotNull(i => i.name);

orWhereNotNull

typedKnex
    .query(User)
    .whereNotNull(i => i.name)
    .orWhereNotNull(i => i.name);

orderBy

typedKnex.query(User).orderBy(i => i.id);

innerJoinColumn

typedKnex.query(User).innerJoinColumn(i => i.category);

innerJoinTableOnFunction

typedKnex.query(User).innerJoinTableOnFunction('evilTwin', User, join => {
    join.onColumns(
        i => i.id,
        '=',
        j => j.id
    );
});

leftOuterJoinColumn

typedKnex.query(User).leftOuterJoinColumn(i => i.category);

leftOuterJoinTableOnFunction

typedKnex.query(User).leftOuterJoinTableOnFunction('evilTwin', User, join => {
    join.onColumns(
        i => i.id,
        '=',
        j => j.id
    );
});

selectRaw

typedKnex
    .query(User)
    .selectRaw('otherId', Number, 'select other.id from other');

selectQuery

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"

findByPrimaryKey

const user = await typedKnex
    .query(User)
    .findByPrimaryKey('id', i => [i.id, i.name]);

whereIn

typedKnex.query(User).whereIn(i => i.name, ['user1', 'user2']);

whereNotIn

typedKnex.query(User).whereNotIn(i => i.name, ['user1', 'user2']);

orWhereIn

typedKnex
    .query(User)
    .whereIn(i => i.name, ['user1', 'user2'])
    .orWhereIn(i => i.name, ['user3', 'user4']);

orWhereNotIn

typedKnex
    .query(User)
    .whereIn(i => i.name, ['user1', 'user2'])
    .orWhereNotIn(i => i.name, ['user3', 'user4']);

whereBetween

typedKnex.query(UserCategory).whereBetween(i => i.year, [1, 2037]);

whereNotBetween

typedKnex.query(User).whereNotBetween(i => i.year, [1, 2037]);

orWhereBetween

typedKnex
    .query(User)
    .whereBetween(c => c.year, [1, 10])
    .orWhereBetween(c => c.year, [100, 1000]);

orWhereNotBetween

typedKnex
    .query(User)
    .whereBetween(c => c.year, [1, 10])
    .orWhereNotBetween(c => c.year, [100, 1000]);

whereExists

typedKnex.query(User).whereExists(UserSetting, (subQuery, parentColumn) => {
    subQuery.whereColumn(c => c.userId, '=', parentColumn.id);
});

orWhereExists

typedKnex.query(User).orWhereExists(UserSetting, (subQuery, parentColumn) => {
    subQuery.whereColumn(c => c.userId, '=', parentColumn.id);
});

whereNotExists

typedKnex.query(User).whereNotExists(UserSetting, (subQuery, parentColumn) => {
    subQuery.whereColumn(c => c.userId, '=', parentColumn.id);
});

orWhereNotExists

typedKnex
    .query(User)
    .orWhereNotExists(UserSetting, (subQuery, parentColumn) => {
        subQuery.whereColumn(c => c.userId, '=', parentColumn.id);
    });

whereParentheses

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'

groupBy

typedKnex
    .query(User)
    .select(c => c.someValue)
    .selectRaw('total', Number, 'SUM("numericValue")')
    .groupBy(c => c.someValue);

having

typedKnex.query(User).having(c => c.numericValue, '>', 10);

havingNull

typedKnex.query(User).havingNull(c => c.numericValue);

havingNotNull

typedKnex.query(User).havingNotNull(c => c.numericValue);

havingIn

typedKnex.query(User).havingIn(c => c.name, ['user1', 'user2']);

havingNotIn

typedKnex.query(User).havingNotIn(c => c.name, ['user1', 'user2']);

havingExists

typedKnex.query(User).havingExists(UserSetting, (subQuery, parentColumn) => {
    subQuery.whereColumn(c => c.userId, '=', parentColumn.id);
});

havingNotExists

typedKnex.query(User).havingNotExists(UserSetting, (subQuery, parentColumn) => {
    subQuery.whereColumn(c => c.userId, '=', parentColumn.id);
});

havingBetween

typedKnex.query(User).havingBetween(c => c.numericValue, [1, 10]);

havingNotBetween

typedKnex.query(User).havingNotBetween(c => c.numericValue, [1, 10]);

union

typedKnex.query(User).union(User, subQuery => {
    subQuery.select(c => [c.id]).where(c => c.numericValue, 12);
});

unionAll

typedKnex
    .query(User)
    .select(c => [c.id])
    .unionAll(User, subQuery => {
        subQuery.select(c => [c.id]).where(c => c.numericValue, 12);
    });

min

typedKnex.query(User).min(c => c.numericValue, 'minNumericValue');

count

typedKnex.query(User).count(c => c.numericValue, 'countNumericValue');

countDistinct

typedKnex
    .query(User)
    .countDistinct(c => c.numericValue, 'countDistinctNumericValue');

max

typedKnex.query(User).max(c => c.numericValue, 'maxNumericValue');

sum

typedKnex.query(User).sum(c => c.numericValue, 'sumNumericValue');

sumDistinct

typedKnex
    .query(User)
    .sumDistinct(c => c.numericValue, 'sumDistinctNumericValue');

avg

typedKnex.query(User).avg(c => c.numericValue, 'avgNumericValue');

avgDistinct

typedKnex
    .query(User)
    .avgDistinct(c => c.numericValue, 'avgDistinctNumericValue');

clearSelect

typedKnex
    .query(User)
    .select(i => i.id)
    .clearSelect()
    .select((i = i.name));

clearWhere

typedKnex
    .query(User)
    .where(i => i.id, 'name')
    .clearWhere()
    .where((i = i.name), 'name');

clearOrder

typedKnex
    .query(User)
    .orderBy(i => i.id)
    .clearOrder()
    .orderBy((i = i.name));

limit

typedKnex.query(User).limit(10);

offset

typedKnex.query(User).offset(10);

useKnexQueryBuilder

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);
);

keepFlat

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' }

toQuery

const query = typedKnex.query(User);

console.log(query.toQuery()); // select * from "users"

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')
    .getFirstOrNull();

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')
    .getFirst();

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')
    .getSingleOrNull();

getSingle

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();

getMany

const users = await typedKnex
    .query(User)
    .whereNotNull(i => i.name)
    .getMany();

getCount

typedKnex.query(User);

insertItem

typedKnex.query(User);

insertItems

typedKnex.query(User);

del

typedKnex.query(User);

delByPrimaryKey

typedKnex.query(User);

updateItem

typedKnex.query(User);

updateItemByPrimaryKey

typedKnex.query(User);

updateItemsByPrimaryKey

typedKnex.query(User);

execute

typedKnex.query(User);

whereRaw

typedKnex.query(User);

havingRaw

typedKnex.query(User);

transacting

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
}

truncate

typedKnex.query(User);

distinct

typedKnex.query(User);

clone

typedKnex.query(User);

groupByRaw

typedKnex.query(User);

Transactions

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
}

Validate Entities

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);

Test

npm test

Update version

npm version major|minor|patch
npm publish --access public
git push

for beta

npm publish --access public --tag beta

About

A TypeScript wrapper for Knex.js

License:MIT License


Languages

Language:TypeScript 100.0%