WIP knex > kysely

This commit is contained in:
Linus Miller 2025-12-18 07:30:51 +01:00
parent 9d9ee1b4ce
commit 72eeb03425
16 changed files with 266 additions and 294 deletions

View File

@ -11,7 +11,7 @@ get {
}
params:path {
id: 1000
id: 10
}
settings {

View File

@ -12,7 +12,6 @@ get {
params:query {
supplier: 150
:
}
settings {

View File

@ -5,11 +5,17 @@ meta {
}
get {
url: {{base_url}}/api/invoices
url: {{base_url}}/api/invoices?limit=2&supplierId=10
body: none
auth: inherit
}
params:query {
limit: 2
supplierId: 10
~year: 2015
}
settings {
encodeUrl: true
timeout: 0

View File

@ -1,16 +1,15 @@
meta {
name: /api/objects/:id
name: /api/journals
type: http
seq: 12
seq: 20
}
get {
url: {{base_url}}/api/objects/10
url: {{base_url}}/api/journals
body: none
auth: inherit
}
settings {
encodeUrl: true
timeout: 0
}

View File

@ -0,0 +1,20 @@
meta {
name: /api/objects/:id/transactions
type: http
seq: 12
}
get {
url: {{base_url}}/api/objects/:id/transactions
body: none
auth: inherit
}
params:path {
id: 10
}
settings {
encodeUrl: true
timeout: 0
}

View File

@ -5,11 +5,15 @@ meta {
}
get {
url: {{base_url}}/api/results/2018
url: {{base_url}}/api/results/:year
body: none
auth: inherit
}
params:path {
year: 2017
}
settings {
encodeUrl: true
timeout: 0

View File

@ -5,14 +5,14 @@ meta {
}
get {
url: {{base_url}}/api/transactions?year=2020&accountNumber=4800
url: {{base_url}}/api/transactions?year=2020
body: none
auth: inherit
}
params:query {
year: 2020
accountNumber: 4800
~accountNumber: 4800
}
settings {

View File

@ -1,9 +1,25 @@
import { type SelectQueryBuilder, type ReferenceExpression, type OperandValueExpression, sql } from 'kysely'
type ColumnsOf<DB, T extends keyof DB> = T extends any ? keyof DB[T] & string : never
type AppearsIn<DB, TB extends keyof DB, Col extends string> = {
[T in TB]: Col extends ColumnsOf<DB, T> ? T : never
}[TB]
type IsUnion<T, U = T> = T extends any ? ([U] extends [T] ? false : true) : never
type UniqueColumn<DB, TB extends keyof DB> = {
[C in ColumnsOf<DB, TB>]: IsUnion<AppearsIn<DB, TB, C>> extends true ? never : C
}[ColumnsOf<DB, TB>]
type PrefixedColumn<DB, TB extends keyof DB> = TB extends any ? `${TB & string}.${keyof DB[TB] & string}` : never
type UnambiguousColumn<DB, TB extends keyof DB> = PrefixedColumn<DB, TB> | UniqueColumn<DB, TB>
type WhereInput<DB, TB extends keyof DB> = Partial<{
[C in keyof DB[TB]]:
| OperandValueExpression<DB, TB, DB[TB][C]>
| readonly OperandValueExpression<DB, TB, DB[TB][C]>[]
[C in UnambiguousColumn<DB, TB>]:
| OperandValueExpression<DB, TB, any>
| readonly OperandValueExpression<DB, TB, any>[]
| null
}>
@ -16,10 +32,16 @@ export function applyWhere<DB, TB extends keyof DB, O>(
if (value === null) {
return builder.where(column, 'is', null)
} else if (Array.isArray(value)) {
}
if (Array.isArray(value)) {
return builder.where(column, 'in', value)
}
if (value === undefined) {
return builder
}
return builder.where(column, '=', value)
}, builder)
}
@ -28,20 +50,19 @@ interface PaginationInput<DB, TB extends keyof DB> {
where: WhereInput<DB, TB>
limit?: number
offset?: number
orderBy?: {
column: ReferenceExpression<DB, TB>
direction?: 'asc' | 'desc'
}
sort?: UnambiguousColumn<DB, TB> | `-${UnambiguousColumn<DB, TB>}`
}
export function applyPagination<DB, TB extends keyof DB, O>(
builder: SelectQueryBuilder<DB, TB, O>,
{ limit, offset, orderBy }: PaginationInput<DB, TB>,
{ limit, offset, sort }: PaginationInput<DB, TB>,
): SelectQueryBuilder<DB, TB, O> {
let qb = builder
if (orderBy) {
qb = qb.orderBy(orderBy.column, orderBy.direction ?? 'asc')
if (sort) {
const columnName = (sort.startsWith('-') ? sort.slice(1) : sort) as UnambiguousColumn<DB, TB>
qb = qb.orderBy(columnName, columnName === sort ? 'asc' : 'desc')
}
if (limit !== undefined) {
@ -68,13 +89,15 @@ export async function paginate<DB, TB extends keyof DB, O>(
countQuery = applyWhere(countQuery, input.where)
}
const [countRow, items] = await Promise.all([countQuery.executeTakeFirstOrThrow(), dataQuery.execute()])
const [countRow, data] = await Promise.all([countQuery.executeTakeFirstOrThrow(), dataQuery.execute()])
return {
items,
data,
meta: {
totalCount: (countRow as { totalCount: number }).totalCount,
limit: input.limit,
offset: input.offset,
count: items.length,
count: data.length,
},
}
}

View File

@ -1,32 +1,30 @@
import _ from 'lodash'
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
import knex from '../../lib/knex.ts'
import { sql } from 'kysely'
const balanceRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
const { db } = fastify
fastify.route({
url: '/',
method: 'GET',
async handler() {
const financialYears = await knex('financialYear').select('*').orderBy('year', 'asc')
const financialYears = await db.selectFrom('financialYear').select('year').orderBy('year').execute()
return knex('accountBalance AS ab')
return db
.selectFrom('accountBalance as ab')
.innerJoin('financialYear as fy', 'fy.id', 'ab.financialYearId')
.innerJoin('account as a', 'a.number', 'ab.accountNumber')
.select(['ab.accountNumber', 'a.description'])
.select(
'ab.accountNumber',
'a.description',
Object.fromEntries(
financialYears.map((fy) => [
fy.year,
knex.raw(`SUM(CASE WHEN fy.year = ${fy.year} THEN ab."out" ELSE 0 END)`),
]),
financialYears.map((fy) =>
sql`SUM(CASE WHEN fy.year = ${fy.year} THEN ab."out" ELSE 0 END)`.as(fy.year.toString()),
),
)
.innerJoin('financialYear AS fy', 'fy.id', 'ab.financialYearId')
.innerJoin('account AS a', function () {
this.on('ab.accountNumber', '=', 'a.number')
})
.groupBy('ab.accountNumber', 'a.description')
.where('ab.accountNumber', '<', 3000)
.groupBy(['ab.accountNumber', 'a.description'])
.orderBy('ab.accountNumber')
.where('ab.accountNumber', '<', 3000)
.execute()
},
})

View File

@ -1,34 +1,38 @@
import _ from 'lodash'
import * as z from 'zod'
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
import knex from '../../lib/knex.ts'
import { jsonArrayFrom } from 'kysely/helpers/postgres'
import { applyWhere } from '../../lib/kysely_helpers.ts'
const entryRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
const { db } = fastify
fastify.route({
url: '/',
method: 'GET',
schema: {
querystring: z.object({
journal: z.string(),
year: z.coerce.number(),
journal: z.string().optional(),
year: z.coerce.number().optional(),
}),
},
async handler(req) {
const financialYearId = (await knex('financialYear').first('id').where('year', req.query.year))?.id
const journalId = (await knex('journal').first('id').where('identifier', req.query.journal))?.id
if (!financialYearId || !journalId) {
return null
}
return knex('entry AS e')
.select('e.*')
.sum('t.amount AS amount')
.innerJoin('transaction AS t', 'e.id', 't.entryId')
async handler(request) {
return applyWhere(
db
.selectFrom('entry as e')
.innerJoin('transaction as t', 'e.id', 't.entryId')
.innerJoin('journal as j', 'j.id', 'e.journalId')
.innerJoin('financialYear as fy', 'fy.id', 'e.financialYearId')
.selectAll('e')
.select((eb) => eb.fn.sum('t.amount').as('amount'))
.orderBy('e.id')
.where({ financialYearId, journalId })
.andWhere('t.amount', '>', 0)
.groupBy('e.id')
.where('t.amount', '>', 0 as ANY)
.groupBy('e.id'),
{
year: request.query.year,
'j.identifier': request.query.journal,
},
).execute()
},
})
@ -37,27 +41,35 @@ const entryRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
method: 'GET',
schema: {
params: z.object({
id: z.number(),
id: z.coerce.number(),
}),
},
async handler(req) {
return knex('entry AS e')
.first('e.id', 'j.identifier AS journal', 'e.number', 'e.entryDate', 'e.transactionDate', 'e.description', {
transactions: knex
.select(knex.raw('json_agg(transactions)'))
.from(
knex('transaction')
.select('accountNumber', 'objectId')
.where('transaction.entryId', knex.ref('e.id'))
.as('transactions'),
),
})
.sum('t.amount AS amount')
.innerJoin('journal AS j', 'e.journalId', 'j.id')
.innerJoin('transaction AS t', 'e.id', 't.entryId')
.where('e.id', req.params.id)
.andWhere('t.amount', '>', 0)
.groupBy('e.id', 'j.identifier')
return db
.selectFrom('entry as e')
.innerJoin('journal as j', 'e.journalId', 'j.id')
.innerJoin('transaction as t', 'e.id', 't.entryId')
.select([
'e.id',
'j.identifier as journal',
'e.number',
'e.entryDate',
'e.transactionDate',
'e.description',
(eb) => eb.fn.sum('t.amount').as('amount'),
(eb) =>
jsonArrayFrom(
eb
.selectFrom('transaction as t')
.select(['id', 'accountNumber', 'amount', 'description'])
.selectAll()
.whereRef('t.entryId', '=', 'e.id'),
).as('transactions'),
])
.groupBy(['e.id', 'j.identifier'])
.where('e.id', '=', req.params.id)
.where('t.amount', '>', 0 as ANY)
.execute()
},
})

View File

@ -1,13 +1,13 @@
import _ from 'lodash'
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
import knex from '../../lib/knex.ts'
const financialYearRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
const { db } = fastify
fastify.route({
url: '/',
method: 'GET',
handler() {
return knex('financialYear').select('*').orderBy('startDate', 'desc')
return db.selectFrom('financialYear').selectAll().orderBy('startDate', 'desc').execute()
},
})

View File

@ -1,52 +1,52 @@
import _ from 'lodash'
import { jsonArrayFrom } from 'kysely/helpers/postgres'
import * as z from 'zod'
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
import knex from '../../lib/knex.ts'
import StatusError from '../../lib/status_error.ts'
import { applyWhere, paginate } from '../../lib/kysely_helpers.ts'
const invoiceRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
const { db } = fastify
fastify.route({
url: '/',
method: 'GET',
schema: {
querystring: z.object({
year: z.number().optional(),
supplier: z.number().optional(),
year: z.coerce.number().optional(),
supplierId: z.coerce.number().optional(),
limit: z.coerce.number().default(100),
offset: z.coerce.number().optional(),
sort: z.literal(['supplierId', 'i.id', 'invoiceDate', 'dueDate']).default('i.id'),
}),
},
async handler(req) {
let query: { financialYearId?: number; supplierId?: number } = {}
async handler(request) {
const { offset, limit, sort, ...where } = request.query
const baseQuery = db.selectFrom('invoice as i').leftJoin('financialYear as fy', 'fy.id', 'i.financialYearId')
if (req.query.year) {
const year = await knex('financialYear').first('*').where('year', req.query.year)
if (!year) throw new StatusError(404, `Year ${req.query.year} not found.`)
query.financialYearId = year.id
}
if (req.query.supplier) {
query.supplierId = req.query.supplier
}
return knex('invoice AS i')
.select('i.*', 'fy.year', {
files: knex
.select(knex.raw('json_agg(files)'))
.from(
knex
.select('id', 'filename')
.from('file AS f')
.innerJoin('filesToInvoice AS fi', 'f.id', 'fi.fileId')
.where('fi.invoiceId', knex.ref('i.id'))
.as('files'),
return paginate(
baseQuery
.selectAll('i')
.select(['fy.year'])
.select((eb) => [
jsonArrayFrom(
eb
.selectFrom('file as f')
.innerJoin('filesToInvoice as fi', 'f.id', 'fi.fileId')
.select(['id', 'filename'])
.whereRef('fi.invoiceId', '=', 'i.id'),
).as('files'),
jsonArrayFrom(eb.selectFrom('transaction as t').selectAll().whereRef('t.invoiceId', '=', 'i.id')).as(
'transactions',
),
transactions: knex
.select(knex.raw('json_agg(transactions)'))
.from(knex.select('*').from('transaction AS t').where('t.invoiceId', knex.ref('i.id')).as('transactions')),
})
.leftOuterJoin('financialYear AS fy', 'i.financialYearId', 'fy.id')
.orderBy('i.invoiceDate')
.where(query)
]),
baseQuery,
{
where,
limit,
offset,
sort,
},
)
},
})
@ -55,25 +55,19 @@ const invoiceRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
method: 'GET',
schema: {
querystring: z.object({
year: z.number().optional(),
supplier: z.number().optional(),
supplierId: z.number().optional(),
}),
response: {
200: z.object({
totalAmount: z.coerce.number(),
}),
},
async handler(req) {
let query: { financialYearId?: number; supplierId?: number } = {}
if (req.query.year) {
const year = await knex('financialYear').first('*').where('year', req.query.year)
if (!year) throw new StatusError(404, `Year ${req.query.year} not found.`)
query.financialYearId = year.id
}
if (req.query.supplier) {
query.supplierId = req.query.supplier
}
return knex('invoice AS i').first().sum('i.amount AS amount').where(query)
},
handler(request) {
return applyWhere(
db.selectFrom('invoice').select((eb) => eb.fn.sum('amount').as('totalAmount')),
request.query,
).executeTakeFirst()
},
})
@ -82,71 +76,29 @@ const invoiceRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
method: 'GET',
schema: {
params: z.object({
id: z.number(),
id: z.coerce.number(),
}),
},
handler(req) {
return knex('invoice AS i')
.first('i.*', 'fy.year', {
files: knex
.select(knex.raw('json_agg(files)'))
.from(
knex
.select('id', 'filename')
.from('file AS f')
.innerJoin('filesToInvoice AS fi', 'f.id', 'fi.fileId')
.where('fi.invoiceId', knex.ref('i.id'))
.as('files'),
handler(request) {
return db
.selectFrom('invoice as i')
.leftJoin('financialYear as fy', 'fy.id', 'i.financialYearId')
.selectAll('i')
.select(['fy.year'])
.select((eb) => [
jsonArrayFrom(
eb
.selectFrom('file as f')
.innerJoin('filesToInvoice as fi', 'f.id', 'fi.fileId')
.select(['id', 'filename'])
.whereRef('fi.invoiceId', '=', 'i.id'),
).as('files'),
jsonArrayFrom(eb.selectFrom('transaction as t').selectAll().whereRef('t.invoiceId', '=', 'i.id')).as(
'transactions',
),
transactions: knex
.select(knex.raw('json_agg(transactions)'))
.from(knex.select('*').from('transaction AS t').where('t.invoiceId', knex.ref('i.id')).as('transactions')),
})
.leftOuterJoin('financialYear AS fy', 'i.financialYearId', 'fy.id')
.where('i.id', req.params.id)
},
})
fastify.route({
url: '/by-supplier/:supplier',
method: 'GET',
schema: {
params: z.object({
supplier: z.number(),
}),
},
handler(req) {
return knex('invoice AS i')
.select('*', {
files: knex
.select(knex.raw('json_agg(files)'))
.from(
knex
.select('id', 'filename')
.from('file AS f')
.innerJoin('filesToInvoice AS fi', 'f.id', 'fi.fileId')
.where('fi.invoiceId', knex.ref('i.id'))
.as('files'),
),
})
.where('supplierId', req.params.supplier)
},
})
fastify.route({
url: '/by-year/:year',
method: 'GET',
schema: {
params: z.object({
year: z.number(),
}),
},
async handler(req) {
const year = await knex('financialYear').first('*').where('year', req.params.year)
if (!year) throw new StatusError(404, `Year ${req.params.year} not found.`)
return knex('invoice').select('*').where('financialYearId', year.id)
])
.where('i.id', '=', request.params.id)
.execute()
},
})

View File

@ -1,13 +1,14 @@
import _ from 'lodash'
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
import knex from '../../lib/knex.ts'
const journalRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
const { db } = fastify
fastify.route({
url: '/',
method: 'GET',
handler() {
return knex('journal').select('*').orderBy('identifier')
return db.selectFrom('journal').selectAll().orderBy('identifier').execute()
},
})

View File

@ -1,39 +1,38 @@
import _ from 'lodash'
import * as z from 'zod'
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
import knex from '../../lib/knex.ts'
const objectRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
const { db } = fastify
fastify.route({
url: '/',
method: 'GET',
handler() {
return knex('object AS o')
.select('o.id', 'o.number', 'o.name', 'd.number AS dimensionNumber', 'd.name AS dimensionName')
.innerJoin('dimension AS d', function () {
this.on('o.dimensionId', '=', 'd.id')
})
return db
.selectFrom('object as o')
.innerJoin('dimension as d', 'd.id', 'o.dimensionId')
.select(['o.id', 'o.number', 'o.name', 'd.number as dimensionNumber', 'd.name as dimensionName'])
.execute()
},
})
fastify.route({
url: '/:id',
url: '/:id/transactions',
method: 'GET',
schema: {
params: z.object({
id: z.number(),
id: z.coerce.number(),
}),
},
async handler(req) {
return knex('transaction AS t')
.select('t.entryId', 'e.transactionDate', 't.accountNumber', 't.amount')
.innerJoin('transactions_to_objects AS to', function () {
this.on('t.id', 'to.transactionId')
})
.innerJoin('entry AS e', function () {
this.on('e.id', '=', 't.entryId')
})
.where('to.objectId', req.params.id)
async handler(request) {
return db
.selectFrom('transaction as t')
.innerJoin('transactionsToObjects as to', 't.id', 'to.transactionId')
.innerJoin('entry as e', 'e.id', 't.entryId')
.select(['t.entryId', 'e.transactionDate', 't.accountNumber', 't.amount'])
.where('to.objectId', '=', request.params.id)
.execute()
},
})

View File

@ -1,69 +1,33 @@
import _ from 'lodash'
import * as z from 'zod'
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
import knex from '../../lib/knex.ts'
import { sql } from 'kysely'
const resultRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
const { db } = fastify
fastify.route({
url: '/',
method: 'GET',
async handler() {
const financialYears = await knex('financialYear').select('*').orderBy('year', 'asc')
const financialYears = await db.selectFrom('financialYear').selectAll().orderBy('year', 'asc').execute()
return knex('transaction AS t')
return db
.selectFrom('transaction as t')
.innerJoin('entry as e', 't.entryId', 'e.id')
.innerJoin('financialYear as fy', 'fy.id', 'e.financialYearId')
.innerJoin('account as a', 't.accountNumber', 'a.number')
.select(['t.accountNumber', 'a.description'])
.select(
't.accountNumber',
'a.description',
Object.fromEntries(
financialYears.map((fy) => [
fy.year,
knex.raw(`SUM(CASE WHEN fy.year = ${fy.year} THEN t.amount ELSE 0 END)`),
]),
financialYears.map((fy) =>
sql`SUM(CASE WHEN fy.year = ${fy.year} THEN t.amount ELSE 0 END)`.as(fy.year.toString()),
),
)
.sum('t.amount AS amount')
.innerJoin('entry AS e', function () {
this.on('t.entryId', '=', 'e.id')
})
.innerJoin('financialYear AS fy', 'fy.id', 'e.financialYearId')
.innerJoin('account AS a', function () {
this.on('t.accountNumber', '=', 'a.number')
})
.groupBy('t.accountNumber', 'a.description')
.groupBy(['t.accountNumber', 'a.description'])
.where('t.accountNumber', '>=', 3000)
.orderBy('t.accountNumber')
.execute()
},
// async handler() {
// const years = await knex('financialYear').select('*')
// const accounts = await knex('account').select('*')
// return Promise.all(
// years.map((year) =>
// knex('account AS a')
// .select('a.number', 'a.description')
// .sum('t.amount as amount')
// .innerJoin('transaction AS t', function () {
// this.on('t.accountNumber', '=', 'a.number')
// })
// .innerJoin('entry AS e', function () {
// this.on('t.entryId', '=', 'e.id')
// })
// .groupBy('a.number', 'a.description')
// .where('a.number', '>=', 3000)
// .where('e.financialYearId', year.id)
// .orderBy('a.number')
// .then((result) => ({
// startDate: year.startDate,
// endDate: year.endDate,
// result,
// })),
// ),
// ).then((years) => ({
// accounts,
// years,
// }))
// },
})
fastify.route({
@ -71,27 +35,24 @@ const resultRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
method: 'GET',
schema: {
params: z.object({
year: z.number(),
year: z.coerce.number(),
}),
},
async handler(req) {
const year = await knex('financialYear').first('*').where('year', req.params.year)
if (!year) return null
return knex('transaction AS t')
.select('t.accountNumber', 'a.description')
.sum('t.amount as amount')
.innerJoin('account AS a', function () {
this.on('t.accountNumber', '=', 'a.number')
})
.innerJoin('entry AS e', function () {
this.on('t.entryId', '=', 'e.id')
})
.groupBy('t.accountNumber', 'a.description')
async handler(request) {
return (
db
.selectFrom('transaction as t')
.innerJoin('entry as e', 't.entryId', 'e.id')
.innerJoin('financialYear as fy', 'fy.id', 'e.financialYearId')
.innerJoin('account as a', 't.accountNumber', 'a.number')
.select(['t.accountNumber', 'a.description', (eb) => eb.fn.sum('t.amount').as('amount')])
// .sum('t.amount AS amount')
.groupBy(['t.accountNumber', 'a.description'])
.where('t.accountNumber', '>=', 3000)
.where('e.financialYearId', year.id)
.where('year', '=', request.params.year)
.orderBy('t.accountNumber')
.execute()
)
},
})

View File

@ -13,20 +13,20 @@ const transactionRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
querystring: z.object({
year: z.optional(z.coerce.number()),
accountNumber: z.optional(z.coerce.number()),
limit: z.number().default(2),
sort: z.string().default('t.id'),
offset: z.number().optional(),
limit: z.coerce.number().default(100),
offset: z.coerce.number().optional(),
sort: z.literal(['accountNumber', 'e.transactionDate', 't.id']).default('t.id'),
}),
},
async handler(request) {
const { offset, limit, sort: _sort, ...where } = request.query
const { offset, limit, sort, ...where } = request.query
const baseQuery = db
.selectFrom('transaction as t')
.innerJoin('entry as e', 't.entryId', 'e.id')
.innerJoin('financialYear as fy', 'e.financialYearId', 'fy.id')
const result = await paginate(
return paginate(
baseQuery.select([
't.accountNumber',
'e.transactionDate',
@ -38,14 +38,12 @@ const transactionRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
]),
baseQuery,
{
// @ts-ignore
where,
limit,
offset,
sort,
},
)
return result
},
})