Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/seeden/kysely-orm

TypeSafe ORM for Kysely library
https://github.com/seeden/kysely-orm

knex kysely mysql nodejs objectionjs orm postgres postgresql sql sqllite typescript

Last synced: 3 months ago
JSON representation

TypeSafe ORM for Kysely library

Awesome Lists containing this project

README

        

# kysely-orm
TypeSafe ORM for [kysely library](https://github.com/koskimas/kysely)

# Define TypeSafe DB structure

```ts ./@types/Database
import { type Generated } from 'kysely';

interface Users {
id: Generated;
name: string;
email: string;
}

interface Books {
id: Generated;
title: string;
userId: number;
}

interface DB {
users: Users;
books: Books;
};
```

I will recommend using [library kysely-codegen](https://github.com/RobinBlomberg/kysely-codegen) for autogenerating this structure.

# Define Database connection

```ts: ./config/db.ts
import { PostgresDialect } from 'kysely';
import { Pool } from 'pg';
import { Database } from 'kysely-orm';
import type DB from './@types/Database';

export default new Database({
dialect: new PostgresDialect({
pool: new Pool({
connectionString,
}),
}),
});
```

## SQLite Example

```ts: ./config/db.ts
import { SqliteDialect } from 'kysely';
import SQLLiteDatabase from 'better-sqlite3';
import { Database } from 'kysely-orm';
import type DB from './@types/Database';

export default new Database({
dialect: new SqliteDialect({
database: new SQLLiteDatabase('test.db'),
}),
});
```

# Define Model

```ts ./models/User.ts
import db from './db';

export default class User extends db.model('users', 'id') {
static findByEmail(email: string) {
return this.findOne('email', email);
}
}
```

## Export all models

```ts ./models/index.ts
export { default as User } from './User';
export { default as Book } from './Book';
```

# Transactions

```ts
import { User, Book } from '../models';

async function createUser(data) {
const newUser = User.transaction(async () => {
const user = await User.findByEmail(email);
if (user) {
throw new Error('User already exists');
}

return User.insert(data);
});

...
}
```

How is it working?
Transactions are using node AsyncLocalStorage whitch is stable node feature.
Therefore you do not need to pass any transaction object to your current models.
Everything working out of the box.

### Use db.transaction instead of Model.transaction
Model.transaction is alias for db.transaction

```ts
import db from '../config/db';
import { User, Book } from '../models';

async function createUser(data) {
const newUser = db.transaction(async () => {
const user = await User.findByEmail(email);
if (user) {
throw new Error('User already exists');
}

return User.insert(data);
});

...
}
```

### AsyncLocalStorage pitfalls

If you are using everywhere async/await, you should be fine.
If you need to use a callback, you have two options:
1. use utils.promisify and stay with async/await (preferred option)
2. use AsyncResource.bind(yourCallback)

Without it, the thread chain will lose transaction details which are hard to track.

Performance of AsyncLocalStorage is fine with Node >=16.2.0.
More details you can find here https://github.com/nodejs/node/issues/34493#issuecomment-845094849
This is the reason why I use it as a minimal nodejs version.

### How to use multiple transactions

Working with multiple models and different transactions is not an easy task. For this purpose you can use

```ts
import { User } from '../models';

async function createUsers(userData1, userData2) {
const [user1, user2] = await Promise.all([
User.transaction(() => User.insert(userData1)),
User.transaction(() => User.insert(userData2)),
]);
...
}
```

### The afterCommit hook

A transaction object allows tracking if and when it is committed.

An afterCommit hook can be added to both managed and unmanaged transaction objects:

```ts
import { User } from '../models';

async function createUser(data) {
const newUser = User.transaction(async ({ afterCommit }) => {
const user = await User.findByEmail(email);
if (user) {
throw new Error('User already exists');
}

const user = await User.insert(data);

afterCommit(async () => {
await notifyUser(user);
});

...

return user;
});

...
}
```

The callback passed to afterCommit can be async. In this case transaction call will wait for it before settling.

The afterCommit hook is not raised if the transaction is rolled back.
The afterCommit hook does not modify the return value of the transaction.

# Requests and models/transactions isolation

For each http request, you should create a new isolated model instance (best security practice). Here is an example of how to do that.

```ts
import { isolate } from 'kysely-orm';
import { User } from '../models';

const { User: IsolatedUser } = isolate({ User });
```

For example why to use it:
1. when your model is using dataloaders
2. when your model is storing data and using it later

# Mixins

Sometimes you want to use same logic across your models. For example automatically set updatedAt when you update row data.
Here is example how to define model which has support for mixins.

```ts
import { updatedAt } from 'kysely-orm';
import db from './db';

const BaseModel = db.model('users', 'id');

class User extends updatedAt(BaseModel, 'updatedAt') {
findByEmail(email: string) {
return this.findOne('email', email);
}
}
```

## Helper applyMixins

If you are using many mixins it can be complicated and messy. Therefore you can use applyMixin which will help you to write "nicer" code.

```ts
import { applyMixins, updatedAt, slug } from 'kysely-orm';
import db from './db';

class User extends applyMixins(db, 'users', 'id')(
(model) => updatedAt(model, 'updatedAt'),
(model) => slug(model, {
field: 'username',
sources: ['name', 'firstName', 'lastName'],
slugOptions: {
truncate: 15,
},
}),
) {
findByEmail(email: string) {
return this.findOne('email', email);
}
}
```

## Mixin updatedAt

It will set your db field to NOW() during any update

```ts
import { applyMixins, updatedAt } from 'kysely-orm';
import db from './db';

export default class User extends applyMixins(db, 'users', 'id')(
(model) => updatedAt(model, 'updatedAt'),
) {
findByEmail(email: string) {
return this.findOne('email', email);
}
}
```

## Mixin slug

It will automatically compute url slug from your data and use it during db insert

```ts
import { applyPlugins, slug } from 'kysely-orm';
import type DB from './@types/DB';

export default class User extends applyMixins(db, 'users', 'id')(
(model) => slug(model, {
field: 'username',
sources: ['name', 'firstName', 'lastName'],
slugOptions: {
truncate: 15,
},
}),
) {
findByEmail(email: string) {
return this.findOne('email', email);
}
}
```

# Use model as correct selectable type

Until TypeScript fix https://github.com/microsoft/TypeScript/issues/40451 there is a simple option how to use model as correct type. Interface named same as type in file will merge all attributes automatically.

```ts
import { type Selectable, Database } from 'kysely-orm';

interface Users {
id: Generated;
name: string;
}

interface DB {
users: Users;
};

const db = new Database({
connectionString: '...',
});

export default interface User extends Selectable {};
export default class User extends db.model('users', 'id') {

}
```

```ts
import User from './User';

const user = new User({
id: 1,
name: 'Adam',
});

console.log(user.name); // name has correct type string
```

Without interface, user.name will throw error about unknown property.

# Best practices and coments and answer
- Do not import models into your mixins. It will breaks isolation if you use it and throw errors
- Models are not available from Database object because you can not ask for it from database object. It will breaks isolation, types and whole encapsulation.
- Why not to use functions instead of classes? We need to bind db, table and id for all functions. If we use functions we will reinvent wheel.
- When typescript fixed max limit issue we can use instance for each data row

# Standard Model methods and properties

```ts
class Model {
static readonly db: Database = db;
static readonly table: TableName = table;
static readonly id: IdColumnName = id;
static readonly noResultError: typeof NoResultError = noResultError;
static isolated: boolean = false;

// constructor(data: Data);
constructor(...args: any[]) {
Object.assign(this, args[0]);
}

static relation<
FromColumnName extends keyof DB[TableName] & string,
FromReferenceExpression extends ReferenceExpression,
ToTableName extends keyof DB & string,
ToColumnName extends keyof DB[ToTableName] & string,
>(
type: RelationType.BelongsToOneRelation | RelationType.HasOneRelation | RelationType.HasOneThroughRelation,
from: FromReferenceExpression,
to: ReferenceExpression
): OneRelation<
DB,
TableName,
FromColumnName,
ToTableName,
ToColumnName
>;
static relation<
FromColumnName extends keyof DB[TableName] & string,
FromReferenceExpression extends ReferenceExpression,
ToTableName extends keyof DB & string,
ToColumnName extends keyof DB[ToTableName] & string,
>(
type: RelationType.BelongsToManyRelation | RelationType.HasManyRelation | RelationType.HasManyThroughRelation,
from: FromReferenceExpression,
to: ReferenceExpression
): ManyRelation<
DB,
TableName,
FromColumnName,
ToTableName,
ToColumnName
>;
static relation<
FromColumnName extends keyof DB[TableName] & string,
FromReferenceExpression extends ReferenceExpression,
ToTableName extends keyof DB & string,
ToColumnName extends keyof DB[ToTableName] & string,
>(type: RelationType, from: FromReferenceExpression, to: ReferenceExpression): any {
return {
type,
from,
to,
};
}

static transaction(callback: TransactionCallback) {
return this.db.transaction(callback);
}

static get dynamic() {
return this.db.dynamic;
}

static ref(reference: string) {
return this.db.dynamic.ref(reference);
}

static get fn() {
return this.db.fn;
}

static selectFrom() {
if (this.db.isolated && !this.isolated) {
throw new Error('Cannot use selectFrom() in not isolated model. Call isolate({ Model }) first.');
}
return this.db.selectFrom(this.table);
}

static updateTable() {
if (this.db.isolated && !this.isolated) {
throw new Error('Cannot use updateTable() in not isolated model. Call isolate({ Model }) first.');
}
return this.db.updateTable(this.table);
}

static insertInto() {
if (this.db.isolated && !this.isolated) {
throw new Error('Cannot use insertInto() in not isolated model. Call isolate({ Model }) first.');
}
return this.db.insertInto(this.table);
}

static deleteFrom() {
if (this.db.isolated && !this.isolated) {
throw new Error('Cannot use deleteFrom() in not isolated model. Call isolate({ Model }) first.');
}
return this.db.deleteFrom(this.table);
}

static with>(name: Name, expression: Expression) {
return this.db.with(name, expression);
}

static async afterSingleInsert(singleResult: Selectable) {
return singleResult;
}

static async afterSingleUpdate(singleResult: Selectable) {
return singleResult;
}

static async afterSingleUpsert(singleResult: Selectable) {
return singleResult;
}

static processDataBeforeUpdate(data: UpdateExpression): UpdateExpression;
static processDataBeforeUpdate(data: UpdateExpression, OnConflictTables, OnConflictTables>): UpdateExpression, OnConflictTables, OnConflictTables>;
static processDataBeforeUpdate(data: UpdateExpression | UpdateExpression, OnConflictTables, OnConflictTables>) {
return data;
}

static processDataBeforeInsert(data: InsertObjectOrList) {
return data;
}

static async find(
column: ColumnName,
values: Readonly[]> | Readonly>,
func?: (qb: SelectQueryBuilder) => SelectQueryBuilder,
) {
const isArray = Array.isArray(values);

return this
.selectFrom()
.selectAll()
.where(this.ref(`${this.table}.${column}`), isArray ? 'in' : '=', values)
.$if(!!func, (qb) => func?.(qb as unknown as SelectQueryBuilder) as unknown as typeof qb)
.execute();
}

static async findOne(
column: ColumnName,
value: Readonly>,
func?: (qb: SelectQueryBuilder) => SelectQueryBuilder,
) {
return this
.selectFrom()
.selectAll()
.where(this.ref(`${this.table}.${column}`), '=', value)
.$if(!!func, (qb) => func?.(qb as unknown as SelectQueryBuilder) as unknown as typeof qb)
.limit(1)
.executeTakeFirst();
}

static async findByFields(
fields: Readonly | SelectType[];
}>>,
func?: (qb: SelectQueryBuilder) => SelectQueryBuilder,
) {
return this
.selectFrom()
.selectAll()
.where((qb) => {
let currentQuery = qb;
for (const [column, value] of Object.entries(fields)) {
const isArray = Array.isArray(value);
currentQuery = currentQuery.where(this.ref(`${this.table}.${column}`), isArray ? 'in' : '=', value);
}
return currentQuery;
})
.$if(!!func, (qb) => func?.(qb as unknown as SelectQueryBuilder) as unknown as typeof qb)
.execute();
}

static async findOneByFields(
fields: Readonly;
}>>,
func?: (qb: SelectQueryBuilder) => SelectQueryBuilder,
) {
return this
.selectFrom()
.selectAll()
.where((qb) => {
let currentQuery = qb;
for (const [column, value] of Object.entries(fields)) {
const isArray = Array.isArray(value);
currentQuery = currentQuery.where(this.ref(`${this.table}.${column}`), isArray ? 'in' : '=', value);
}
return currentQuery;
})
.$if(!!func, (qb) => func?.(qb as unknown as SelectQueryBuilder) as unknown as typeof qb)
.executeTakeFirst();
}

static async getOneByFields(
fields: Readonly;
}>>,
func?: (qb: SelectQueryBuilder) => SelectQueryBuilder,
error: typeof NoResultError = this.noResultError,
) {
return this
.selectFrom()
.where((qb) => {
let currentQuery = qb;
for (const [column, value] of Object.entries(fields)) {
const isArray = Array.isArray(value);
currentQuery = currentQuery.where(this.ref(`${this.table}.${column}`), isArray ? 'in' : '=', value);
}
return currentQuery;
})
.selectAll()
.$if(!!func, (qb) => func?.(qb as unknown as SelectQueryBuilder) as unknown as typeof qb)
.executeTakeFirstOrThrow(error);
}

static findById(
id: Id,
func?: (qb: SelectQueryBuilder) => SelectQueryBuilder,
) {
return this.findOne(this.id, id, func);
}

static findByIds(
ids: Readonly[]>,
func?: (qb: SelectQueryBuilder) => SelectQueryBuilder,
) {
return this.find(this.id, ids, func);
}

static async getOne(
column: ColumnName,
value: Readonly>,
func?: (qb: SelectQueryBuilder) => SelectQueryBuilder,
error: typeof NoResultError = this.noResultError,
) {
const item = await this
.selectFrom()
.selectAll()
.where(this.ref(`${this.table}.${column}`), '=', value)
.$if(!!func, (qb) => func?.(qb as unknown as SelectQueryBuilder) as unknown as typeof qb)
.limit(1)
.executeTakeFirstOrThrow(error);

return item;
}

static getById(
id: Id,
func?: (qb: SelectQueryBuilder) => SelectQueryBuilder,
error: typeof NoResultError = this.noResultError,
) {
return this.getOne(this.id, id, func, error);
}

static async findOneAndUpdate(
column: ColumnName,
value: Readonly>,
data: UpdateExpression,
func?: (qb: UpdateQueryBuilder) => UpdateQueryBuilder,
) {
const updatedData = this.processDataBeforeUpdate(data);
const record = await this
.updateTable()
// @ts-ignore
.set(updatedData)
.where(this.ref(`${this.table}.${column}`), '=', value)
.$if(!!func, (qb) => func?.(qb as unknown as UpdateQueryBuilder) as unknown as typeof qb)
.returningAll()
.executeTakeFirst();

return record ? this.afterSingleUpdate(record as Selectable) : record;
}

static async findByFieldsAndUpdate(
fields: Readonly | SelectType[];
}>>,
data: UpdateExpression,
func?: (qb: UpdateQueryBuilder) => UpdateQueryBuilder,
) {
const updatedData = this.processDataBeforeUpdate(data);

return await this
.updateTable()
// @ts-ignore
.set(updatedData)
.where((qb) => {
let currentQuery = qb;
for (const [column, value] of Object.entries(fields)) {
if (Array.isArray(value)) {
currentQuery = currentQuery.where(this.ref(`${this.table}.${column}`), 'in', value);
} else {
currentQuery = currentQuery.where(this.ref(`${this.table}.${column}`), '=', value);
}
}
return currentQuery;
})
.$if(!!func, (qb) => func?.(qb as unknown as UpdateQueryBuilder) as unknown as typeof qb)
.returningAll()
.execute();
}

static async findOneByFieldsAndUpdate(
fields: Readonly | SelectType[];
}>>,
data: UpdateExpression,
func?: (qb: UpdateQueryBuilder) => UpdateQueryBuilder,
) {
// TODO use with and select with limit 1
const updatedData = this.processDataBeforeUpdate(data);
const record = await this
.updateTable()
// @ts-ignore
.set(updatedData)
.where((qb) => {
let currentQuery = qb;
for (const [column, value] of Object.entries(fields)) {
if (Array.isArray(value)) {
currentQuery = currentQuery.where(this.ref(`${this.table}.${column}`), 'in', value);
} else {
currentQuery = currentQuery.where(this.ref(`${this.table}.${column}`), '=', value);
}
}
return currentQuery;
})
.$if(!!func, (qb) => func?.(qb as unknown as UpdateQueryBuilder) as unknown as typeof qb)
.returningAll()
.executeTakeFirst();

return record ? this.afterSingleUpdate(record as Selectable) : record;
}

static async getOneByFieldsAndUpdate(
fields: Readonly | SelectType[];
}>>,
data: UpdateExpression,
func?: (qb: UpdateQueryBuilder) => UpdateQueryBuilder,
error: typeof NoResultError = this.noResultError,
) {
// TODO use with and select with limit 1
const updatedData = this.processDataBeforeUpdate(data);
const record = await this
.updateTable()
// @ts-ignore
.set(updatedData)
.where((qb) => {
let currentQuery = qb;
for (const [column, value] of Object.entries(fields)) {
if (Array.isArray(value)) {
currentQuery = currentQuery.where(this.ref(`${this.table}.${column}`), 'in', value);
} else {
currentQuery = currentQuery.where(this.ref(`${this.table}.${column}`), '=', value);
}
}
return currentQuery;
})
.$if(!!func, (qb) => func?.(qb as unknown as UpdateQueryBuilder) as unknown as typeof qb)
.returningAll()
.executeTakeFirstOrThrow(error);

return this.afterSingleUpdate(record as Selectable);
}

static findByIdAndUpdate(
id: SelectType,
data: UpdateExpression,
func?: (qb: UpdateQueryBuilder) => UpdateQueryBuilder,
) {
return this.findOneAndUpdate(this.id, id, data, func);
}

static async getOneAndUpdate(
column: ColumnName,
value: Readonly>,
data: UpdateExpression,
func?: (qb: UpdateQueryBuilder) => UpdateQueryBuilder,
error: typeof NoResultError = this.noResultError,
) {
const updatedData = this.processDataBeforeUpdate(data);
const record = await this
.updateTable()
// @ts-ignore
.set(updatedData)
.where(this.ref(`${this.table}.${column}`), '=', value)
.$if(!!func, (qb) => func?.(qb as unknown as UpdateQueryBuilder) as unknown as typeof qb)
.returningAll()
.executeTakeFirstOrThrow(error);

return this.afterSingleUpdate(record as Selectable);
}

static getByIdAndUpdate(
id: SelectType,
data: UpdateExpression,
func?: (qb: UpdateQueryBuilder) => UpdateQueryBuilder,
error: typeof NoResultError = this.noResultError,
) {
return this.getOneAndUpdate(this.id, id, data, func, error);
}

static lock(
column: ColumnName,
value: Readonly>,
) {
return this
.selectFrom()
.where(this.ref(`${this.table}.${column}`), '=', value)
.selectAll()
.forUpdate()
.executeTakeFirst();
}

static lockById(id: SelectType) {
return this.lock(this.id, id);
}

static async insertOne(
values: InsertObject,
error: typeof NoResultError = this.noResultError,
) {
const record = await this
.insertInto()
.values(this.processDataBeforeInsert(values))
.returningAll()
.executeTakeFirstOrThrow(error);

return this.afterSingleInsert(record);
}

static async insert(
values: InsertObjectOrList,
) {
return this
.insertInto()
.values(this.processDataBeforeInsert(values))
.returningAll()
.execute();
}

static async upsertOne(
values: InsertObject,
upsertValues: UpdateExpression, OnConflictTables, OnConflictTables>,
conflictColumns: Readonly<(keyof Table & string)[]> | Readonly,
func?: (qb: OnConflictUpdateBuilder) => OnConflictUpdateBuilder,
error: typeof NoResultError = this.noResultError,
) {
const record = await this
.insertInto()
.values(this.processDataBeforeInsert(values))
.onConflict((ocb) => {
const updatedOCB = ocb
.columns(Array.isArray(conflictColumns) ? conflictColumns : [conflictColumns])
.doUpdateSet(this.processDataBeforeUpdate(upsertValues)) as OnConflictUpdateBuilder;

return func ? func(updatedOCB) : updatedOCB;
})
.returningAll()
.executeTakeFirstOrThrow(error);

return this.afterSingleUpsert(record);
}

static async upsert(
values: InsertObjectOrList,
upsertValues: UpdateExpression, OnConflictTables, OnConflictTables>,
conflictColumns: Readonly<(keyof Table & string)[]> | Readonly,
func?: (qb: OnConflictUpdateBuilder) => OnConflictUpdateBuilder,
) {
return await this
.insertInto()
.values(this.processDataBeforeInsert(values))
.onConflict((ocb) => {
const updatedOCB = ocb
.columns(Array.isArray(conflictColumns) ? conflictColumns : [conflictColumns])
.doUpdateSet(this.processDataBeforeUpdate(upsertValues)) as OnConflictUpdateBuilder;

return func ? func(updatedOCB) : updatedOCB;
})
.returningAll()
.execute();
}

static async insertOneIfNotExists>(
values: Values,
sameColumn: keyof DB[TableName] & string,
conflictColumns: Readonly<(keyof Table & string)[]> | Readonly,
func?: (qb: OnConflictUpdateBuilder) => OnConflictUpdateBuilder,
error: typeof NoResultError = this.noResultError,
) {
const record = await this
.insertInto()
.values(values)
.onConflict((ocb) => {
const updatedOCB = ocb
.columns(Array.isArray(conflictColumns) ? conflictColumns : [conflictColumns])
.doUpdateSet({
// use current value instead of excluded because excluded value is not required
[sameColumn]: (eb: any) => eb.ref(`${this.table}.${sameColumn}`)
} as UpdateExpression, OnConflictTables, OnConflictTables>) as OnConflictUpdateBuilder;

return func ? func(updatedOCB) : updatedOCB;
})
.returningAll()
.executeTakeFirstOrThrow(error);

return this.afterSingleInsert(record);
}

// todo add limit via with
static async deleteOne(
column: ColumnName,
value: Readonly>,
func?: (qb: DeleteQueryBuilder) => DeleteQueryBuilder,
error: typeof NoResultError = this.noResultError,
) {
const { numDeletedRows } = await this
.deleteFrom()
.where(this.ref(`${this.table}.${column}`), '=', value)
.$if(!!func, (qb) => func?.(qb as unknown as DeleteQueryBuilder) as unknown as typeof qb)
.executeTakeFirstOrThrow(error);

return numDeletedRows;
}

// todo add limit via with
static async deleteOneByFields(
fields: Readonly | SelectType[];
}>>,
func?: (qb: DeleteQueryBuilder) => DeleteQueryBuilder,
error: typeof NoResultError = this.noResultError,
) {
const { numDeletedRows } = await this
.deleteFrom()
.where((qb) => {
let currentQuery = qb;
for (const [column, value] of Object.entries(fields)) {
if (Array.isArray(value)) {
currentQuery = currentQuery.where(this.ref(`${this.table}.${column}`), 'in', value);
} else {
currentQuery = currentQuery.where(this.ref(`${this.table}.${column}`), '=', value);
}
}
return currentQuery;
})
.$if(!!func, (qb) => func?.(qb as unknown as DeleteQueryBuilder) as unknown as typeof qb)
.executeTakeFirstOrThrow(error);

return numDeletedRows;
}

static async deleteMany(
column: ColumnName,
values: Readonly[]>,
func?: (qb: DeleteQueryBuilder) => DeleteQueryBuilder,
error: typeof NoResultError = this.noResultError,
) {
const { numDeletedRows } = await this
.deleteFrom()
.where(this.ref(`${this.table}.${column}`), 'in', values)
.$if(!!func, (qb) => func?.(qb as unknown as DeleteQueryBuilder) as unknown as typeof qb)
.executeTakeFirstOrThrow(error);

return numDeletedRows;
}

static deleteById(id: SelectType) {
return this.deleteOne(this.id, id);
}

static findByIdAndIncrementQuery(
id: Id,
columns: Partial>,
func?: (qb: UpdateQueryBuilder) => UpdateQueryBuilder,
) {
const setData: Updateable = {};

Object.keys(columns).forEach((column) => {
const value = columns[column as keyof Table] as number;
const correctColumn = column as keyof Updateable;

setData[correctColumn] = sql`${sql.ref(`${this.table}.${column}`)} + ${value}` as any;
});

return this
.updateTable()
// @ts-ignore
.set(setData)
.where(this.ref(this.id), '=', id)
.$if(!!func, (qb) => func?.(qb as unknown as UpdateQueryBuilder) as unknown as typeof qb)
.returningAll();
}

static async findByIdAndIncrement(
id: Id,
columns: Partial>,
func?: (qb: UpdateQueryBuilder) => UpdateQueryBuilder,
) {
const record = await this.findByIdAndIncrementQuery(id, columns, func).executeTakeFirst();

return record ? this.afterSingleUpdate(record as Selectable) : record;
}

static async getByIdAndIncrement(
id: Id,
columns: Partial>,
func?: (qb: UpdateQueryBuilder) => UpdateQueryBuilder,
error: typeof NoResultError = this.noResultError,
) {
const record = await this
.findByIdAndIncrementQuery(id, columns, func)
.executeTakeFirstOrThrow(error);

return this.afterSingleUpdate(record as Selectable);
}

static relatedQuery<
FromTableName extends TableName,
FromColumnName extends keyof DB[TableName] & string,
ToTableName extends keyof DB & string,
ToColumnName extends keyof DB[ToTableName] & string,
>(relation: AnyRelation, ids: Id | Id[] = []) {
const { from, to } = relation;
const [fromTable] = from.split('.') as [FromTableName, FromColumnName];
const [toTable] = to.split('.') as [ToTableName, ToColumnName];

return this.db
.selectFrom(fromTable)
.innerJoin(toTable, (jb) => jb.onRef(this.ref(from), '=', this.ref(to)))
.where(this.ref(`${fromTable}.${this.id}`), Array.isArray(ids) ? 'in' : '=', ids)
.selectAll(toTable);
}

static async findRelatedById<
FromTableName extends TableName,
FromColumnName extends keyof DB[TableName] & string,
ToTableName extends keyof DB & string,
ToColumnName extends keyof DB[ToTableName] & string,
>(
relation: OneRelation,
id: Id,
error?: typeof NoResultError,
): Promise | undefined>;

static async findRelatedById<
FromTableName extends TableName,
FromColumnName extends keyof DB[TableName] & string,
ToTableName extends keyof DB & string,
ToColumnName extends keyof DB[ToTableName] & string,
>(
relation: ManyRelation,
id: Id,
error?: typeof NoResultError,
): Promise[]>;

static async findRelatedById<
FromTableName extends TableName,
FromColumnName extends keyof DB[TableName] & string,
ToTableName extends keyof DB & string,
ToColumnName extends keyof DB[ToTableName] & string,
>(
relation: AnyRelation,
id: Id,
): Promise | undefined | Selectable[]> {
const { type } = relation;
const oneResult = type === RelationType.HasOneRelation || type === RelationType.BelongsToOneRelation;
if (oneResult) {
return await this.relatedQuery(relation, id).executeTakeFirst() as Selectable | undefined;
}

return await this.relatedQuery(relation, id).execute() as Selectable[];
}

static async getRelatedById<
FromTableName extends TableName,
FromColumnName extends keyof DB[TableName] & string,
ToTableName extends keyof DB & string,
ToColumnName extends keyof DB[ToTableName] & string,
>(
relation: OneRelation,
id: Id,
error?: typeof NoResultError,
): Promise>;

static async getRelatedById<
FromTableName extends TableName,
FromColumnName extends keyof DB[TableName] & string,
ToTableName extends keyof DB & string,
ToColumnName extends keyof DB[ToTableName] & string,
>(
relation: ManyRelation,
id: Id,
error?: typeof NoResultError,
): Promise[]>;

static async getRelatedById<
FromTableName extends TableName,
FromColumnName extends keyof DB[TableName] & string,
ToTableName extends keyof DB & string,
ToColumnName extends keyof DB[ToTableName] & string,
>(
relation: AnyRelation,
id: Id,
error: typeof NoResultError = this.noResultError,
): Promise | Selectable[]> {
const { type } = relation;
const oneResult = type === RelationType.HasOneRelation || type === RelationType.BelongsToOneRelation;
if (oneResult) {
return await this.relatedQuery(relation, id).executeTakeFirstOrThrow(error) as Selectable;
}

return await this.relatedQuery(relation, id).execute() as Selectable[];
}

static async findRelated<
FromTableName extends TableName,
FromColumnName extends keyof DB[TableName] & string,
ToTableName extends keyof DB & string,
ToColumnName extends keyof DB[ToTableName] & string,
>(relation: AnyRelation, models: Data[]) {
const { from, to } = relation;
const [fromTable, fromColumn] = from.split('.') as [FromTableName, FromColumnName];
const [toTable] = to.split('.') as [ToTableName, ToColumnName];

// @ts-ignore
const ids = models.map((model) => model[fromColumn]);

return this
.db
.selectFrom(fromTable)
.innerJoin(toTable, (jb) => jb.onRef(this.ref(from), '=', this.ref(to)))
.where(this.ref(from), 'in', ids)
.selectAll(toTable)
.execute();
}

static async findRelatedAndCombine<
FromTableName extends TableName,
FromColumnName extends keyof DB[TableName] & string,
ToTableName extends keyof DB & string,
ToColumnName extends keyof DB[ToTableName] & string,
Field extends string,
>(relation: OneRelation, models: Data[], field: Field): Promise<(Data & {
[key in Field]: Selectable | undefined;
})[]>;
static async findRelatedAndCombine<
FromTableName extends TableName,
FromColumnName extends keyof DB[TableName] & string,
ToTableName extends keyof DB & string,
ToColumnName extends keyof DB[ToTableName] & string,
Field extends string,
>(relation: ManyRelation, models: Data[], field: Field): Promise<(Data & {
[key in Field]: Selectable[];
})[]>;

static async findRelatedAndCombine<
FromTableName extends TableName,
FromColumnName extends keyof DB[TableName] & string,
ToTableName extends keyof DB & string,
ToColumnName extends keyof DB[ToTableName] & string,
Field extends string,
>(relation: AnyRelation, models: Data[], field: Field): Promise {
const rows = await this.findRelated(relation, models);

const { type, from, to } = relation;
const [_fromTable, fromColumn] = from.split('.') as [FromTableName, FromColumnName];
const [_toTable, toColumn] = to.split('.') as [ToTableName, ToColumnName];

const oneResult = type === RelationType.HasOneRelation || type === RelationType.BelongsToOneRelation;

// combine models and rows
return models.map((model) => {
// @ts-ignore
const id = model[fromColumn];

const row = oneResult
// @ts-ignore
? rows.find((row) => row[toColumn] === id)
// @ts-ignore
: rows.filter((row) => row[toColumn] === id);

return { ...model, [field]: row };
});
}

static jsonbIncrement(column: keyof Table & string, data: Record) {
const entries = Object.entries(data);
if (!entries.length) {
throw new Error('Data is empty');
}

const [[key, value], ...rest] = entries;

let update: RawBuilder = sql`jsonb_set(
COALESCE(${sql.ref(`${this.table}.${column}`)}, '{}'),
${sql.lit(`{${key}}`)},
(COALESCE(${sql.ref(`${this.table}.${column}`)}->>${sql.lit(key)}, '0')::int + ${value})::text::jsonb
)`;

rest.forEach(([key, value]) => {
update = sql`jsonb_set(
${update},
${sql.lit(`{${key}}`)},
(COALESCE(${sql.ref(`${this.table}.${column}`)}->>${sql.lit(key)}, '0')::int + ${value})::text::jsonb
)`;
});

return update;
}
}
```