From 8f6591b6792595dfae56c7d1ddaad9b4e9373d30 Mon Sep 17 00:00:00 2001 From: Linus Miller Date: Thu, 18 Dec 2025 10:20:02 +0100 Subject: [PATCH] WIP knex > kysely --- .bruno/API/api-journals.bru | 3 +- .bruno/API/api-suppliers--id.bru | 20 ++ .bruno/API/api-suppliers-merge.bru | 2 +- .bruno/API/api-transactions.bru | 2 +- .bruno/API/api-users.bru | 2 +- bin/add_fisken_invoices.ts | 28 +- bin/add_phm_invoices.ts | 58 ++-- bin/parse_file.ts | 4 +- .../components/invoices_by_supplier_page.tsx | 10 +- .../public/components/transactions_page.tsx | 4 +- server/env.ts | 1 + server/lib/parse_stream.ts | 288 +++++++++++------- server/plugins/auth/routes/change_password.ts | 44 +-- server/plugins/auth/routes/login.ts | 25 +- server/plugins/auth/routes/reset_password.ts | 7 +- server/plugins/auth/routes/verify_email.ts | 28 +- server/routes/api/entries.ts | 2 +- server/routes/api/invoices.ts | 2 +- server/routes/api/suppliers.ts | 2 +- shared/types.db_composite.ts | 9 + 20 files changed, 349 insertions(+), 192 deletions(-) create mode 100644 .bruno/API/api-suppliers--id.bru create mode 100644 shared/types.db_composite.ts diff --git a/.bruno/API/api-journals.bru b/.bruno/API/api-journals.bru index 64ab9ea..3a098b8 100644 --- a/.bruno/API/api-journals.bru +++ b/.bruno/API/api-journals.bru @@ -1,7 +1,7 @@ meta { name: /api/journals type: http - seq: 20 + seq: 21 } get { @@ -12,4 +12,5 @@ get { settings { encodeUrl: true + timeout: 0 } diff --git a/.bruno/API/api-suppliers--id.bru b/.bruno/API/api-suppliers--id.bru new file mode 100644 index 0000000..d3b26bc --- /dev/null +++ b/.bruno/API/api-suppliers--id.bru @@ -0,0 +1,20 @@ +meta { + name: /api/suppliers/:id + type: http + seq: 17 +} + +get { + url: {{base_url}}/api/suppliers/:id + body: none + auth: inherit +} + +params:path { + id: 105 +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/.bruno/API/api-suppliers-merge.bru b/.bruno/API/api-suppliers-merge.bru index 5233499..d22853f 100644 --- a/.bruno/API/api-suppliers-merge.bru +++ b/.bruno/API/api-suppliers-merge.bru @@ -1,7 +1,7 @@ meta { name: /api/suppliers/merge type: http - seq: 17 + seq: 18 } post { diff --git a/.bruno/API/api-transactions.bru b/.bruno/API/api-transactions.bru index daebbf4..bca4962 100644 --- a/.bruno/API/api-transactions.bru +++ b/.bruno/API/api-transactions.bru @@ -1,7 +1,7 @@ meta { name: /api/transactions type: http - seq: 18 + seq: 19 } get { diff --git a/.bruno/API/api-users.bru b/.bruno/API/api-users.bru index 216f3e7..28284ea 100644 --- a/.bruno/API/api-users.bru +++ b/.bruno/API/api-users.bru @@ -1,7 +1,7 @@ meta { name: /api/users type: http - seq: 19 + seq: 20 } get { diff --git a/bin/add_fisken_invoices.ts b/bin/add_fisken_invoices.ts index 328456f..3f167b7 100644 --- a/bin/add_fisken_invoices.ts +++ b/bin/add_fisken_invoices.ts @@ -1,7 +1,7 @@ import fs from 'node:fs/promises' import { existsSync } from 'node:fs' import path from 'node:path' -import knex from '../server/lib/knex.ts' +import db from '../server/lib/kysely.ts' const dirs = process.argv.slice(2) @@ -12,7 +12,7 @@ for await (const dir of dirs) { await readdir(dir) } -knex.destroy() +db.destroy() async function readdir(dir: string) { const files = (await fs.readdir(dir)).toSorted((a: string, b: string) => { @@ -26,7 +26,7 @@ async function readdir(dir: string) { } }) - const trx = await knex.transaction() + const trx = await db.startTransaction().execute() for await (const originalFilename of files) { const result = originalFilename.match(rFileName) @@ -37,15 +37,21 @@ async function readdir(dir: string) { const [, fiskenNumber, invoiceDate, supplierName] = result - let supplier = await trx('supplier').first('*').where('name', supplierName) + let supplier = await trx.selectFrom('supplier').selectAll().where('name', '=', supplierName).executeTakeFirst() if (!supplier) { - supplier = (await trx('supplier').insert({ name: supplierName, supplierTypeId: 1 }).returning('*'))[0] + supplier = await trx + .insertInto('supplier') + .values({ name: supplierName, supplierTypeId: 1 }) + .returningAll() + .executeTakeFirstOrThrow() } - const invoice = ( - await trx('invoice').insert({ fiskenNumber, invoiceDate, supplierId: supplier.id }).returning('*') - )[0] + const invoice = await trx + .insertInto('invoice') + .values({ fiskenNumber: parseInt(fiskenNumber), invoiceDate, supplierId: supplier.id }) + .returningAll() + .executeTakeFirstOrThrow() const ext = path.extname(originalFilename) const filename = `${invoiceDate}_fisken_${fiskenNumber}_${supplierName.split(/[\s/]/).join('_').split(/[']/).join('')}${ext}` @@ -58,9 +64,9 @@ async function readdir(dir: string) { console.info('ALREADY EXISTS: ' + filename) } - const file = (await trx('file').insert({ filename }).returning('id'))[0] - await trx('filesToInvoice').insert({ fileId: file.id, invoiceId: invoice.id }) + const file = await trx.insertInto('file').values({ filename }).returning('id').executeTakeFirstOrThrow() + await trx.insertInto('filesToInvoice').values({ fileId: file.id, invoiceId: invoice.id }).execute() } - await trx.commit() + await trx.commit().execute() } diff --git a/bin/add_phm_invoices.ts b/bin/add_phm_invoices.ts index 80990b6..9d7c894 100644 --- a/bin/add_phm_invoices.ts +++ b/bin/add_phm_invoices.ts @@ -1,52 +1,56 @@ import fs from 'node:fs/promises' import { existsSync } from 'node:fs' import path from 'node:path' -import knex from '../server/lib/knex.ts' +import db from '../server/lib/kysely.ts' import { csvParseRows } from 'd3-dsv' const csvFilename = process.argv[2] const csvString = await fs.readFile(csvFilename, { encoding: 'utf8' }) const rows = csvParseRows(csvString) -const trx = await knex.transaction() +const trx = await db.startTransaction().execute() for (const row of rows.toReversed()) { const [ phmNumber, - // type, - // supplierId, + _type, + _supplierId, supplierName, invoiceDate, dueDate, invoiceNumber, ocr, amount, - // vat, - // balance, - // currency, - // status, + _vat, + _balance, + _currency, + _status, filesString, ] = row - let supplier = await trx('supplier').first('*').where('name', supplierName) + let supplier = await trx.selectFrom('supplier').selectAll().where('name', '=', supplierName).executeTakeFirst() if (!supplier) { - supplier = (await trx('supplier').insert({ name: supplierName, supplierTypeId: 1 }).returning('*'))[0] + supplier = await trx + .insertInto('supplier') + .values({ name: supplierName, supplierTypeId: 1 }) + .returningAll() + .executeTakeFirstOrThrow() } - const invoice = ( - await trx('invoice') - .insert({ - invoiceDate, - supplierId: supplier.id, - dueDate, - ocr, - invoiceNumber, - phmNumber, - amount, - }) - .returning('id') - )[0] + const invoice = await trx + .insertInto('invoice') + .values({ + invoiceDate, + supplierId: supplier.id, + dueDate, + ocr, + invoiceNumber, + phmNumber: parseInt(phmNumber), + amount, + }) + .returning('id') + .executeTakeFirstOrThrow() const filenames = filesString.split(',').map((filename) => filename.trim()) @@ -63,11 +67,11 @@ for (const row of rows.toReversed()) { console.info('ALREADY EXISTS: ' + filename) } - const file = (await trx('file').insert({ filename }).returning('id'))[0] - await trx('filesToInvoice').insert({ fileId: file.id, invoiceId: invoice.id }) + const file = await trx.insertInto('file').values({ filename }).returning('id').executeTakeFirstOrThrow() + await trx.insertInto('filesToInvoice').values({ fileId: file.id, invoiceId: invoice.id }).execute() } } -trx.commit() +await trx.commit().execute() -knex.destroy() +db.destroy() diff --git a/bin/parse_file.ts b/bin/parse_file.ts index a5b48a1..1b2efa9 100644 --- a/bin/parse_file.ts +++ b/bin/parse_file.ts @@ -1,6 +1,6 @@ import fs from 'fs/promises' import parseStream from '../server/lib/parse_stream.ts' -import knex from '../server/lib/knex.ts' +import db from '../server/lib/kysely.ts' for await (const file of process.argv.slice(2)) { const fh = await fs.open(file) @@ -12,4 +12,4 @@ for await (const file of process.argv.slice(2)) { await fh.close() } -knex.destroy() +db.destroy() diff --git a/client/public/components/invoices_by_supplier_page.tsx b/client/public/components/invoices_by_supplier_page.tsx index 398941c..9738043 100644 --- a/client/public/components/invoices_by_supplier_page.tsx +++ b/client/public/components/invoices_by_supplier_page.tsx @@ -13,12 +13,14 @@ const format = Format.bind(null, null, 'YYYY.MM.DD') const InvoicesPage: FunctionComponent = () => { const route = useRoute() - const [supplier, invoices, totalAmount] = usePromise<[Supplier, (Invoice & { files?: File[] })[], number]>(() => + const [supplier, { data: invoices }, totalAmount] = usePromise< + [Supplier, { data: (Invoice & { files?: File[] })[] }, number] + >(() => Promise.all([ rek(`/api/suppliers/${route.params.supplier}`), - rek(`/api/invoices?supplier=${route.params.supplier}`), - rek(`/api/invoices/total-amount?supplier=${route.params.supplier}`).then( - (totalAmount: { amount: number }) => totalAmount.amount, + rek(`/api/invoices?supplierId=${route.params.supplier}`), + rek(`/api/invoices/total-amount?supplierId=${route.params.supplier}`).then( + (totalAmount: { totalAmount: number }) => totalAmount.totalAmount, ), ]), ) diff --git a/client/public/components/transactions_page.tsx b/client/public/components/transactions_page.tsx index 0375f68..3f2de43 100644 --- a/client/public/components/transactions_page.tsx +++ b/client/public/components/transactions_page.tsx @@ -12,7 +12,9 @@ import type { Transaction, FinancialYear } from '../../../shared/types.db.ts' const TransactionsPage: FunctionComponent = () => { const [financialYears, setFinancialYears] = useState([]) - const [transactions, setTransactions] = useState<(Transaction & { entryDescription: string })[]>([]) + const [{ data: transactions }, setTransactions] = useState<{ data: (Transaction & { entryDescription: string })[] }>({ + data: [], + }) const location = useLocation() diff --git a/server/env.ts b/server/env.ts index 6c7e375..81dfbbb 100644 --- a/server/env.ts +++ b/server/env.ts @@ -53,6 +53,7 @@ export default read( 'SESSION_SECRET', ] as const, { + MAILGUN_API_KEY: 'invalid key', PGPASSWORD: null, PGPORT: null, PGUSER: null, diff --git a/server/lib/parse_stream.ts b/server/lib/parse_stream.ts index 1b7f45a..67d39bf 100644 --- a/server/lib/parse_stream.ts +++ b/server/lib/parse_stream.ts @@ -1,5 +1,6 @@ import type { ReadableStream } from 'stream/web' -import knex from './knex.ts' +import type { Selectable } from 'kysely' +import db from './kysely.ts' import split, { type Decoder } from './split.ts' import { @@ -37,25 +38,27 @@ const defaultDecoder = { }, } +import type { Entry, FinancialYear } from '../../shared/types.db.ts' + export default async function parseStream(stream: ReadableStream, decoder: Decoder = defaultDecoder) { const journals = new Map() - let currentEntry: { id: number; description: string } - let currentInvoiceId: number | null - let currentYear = null + let currentEntry: Pick, 'id' | 'description'> + let currentInvoiceId: number | null | undefined + let currentYear: Selectable const details: Record = {} - const trx = await knex.transaction() + const trx = await db.startTransaction().execute() async function getJournalId(identifier: string) { if (journals.has(identifier)) { return journals.get(identifier) } - let journal = await trx('journal').first('*').where('identifier', identifier) + let journal = await trx.selectFrom('journal').selectAll().where('identifier', '=', identifier).executeTakeFirst() if (!journal) { - journal = (await trx('journal').insert({ identifier }).returning('*'))[0] + journal = await trx.insertInto('journal').values({ identifier }).returningAll().executeTakeFirstOrThrow() } journals.set(identifier, journal.id) @@ -77,12 +80,16 @@ export default async function parseStream(stream: ReadableStream, decoder: Decod case '#DIM': { const { number, name } = parseDim(line) - const existingDimension = await trx('dimension').first('*').where('number', number) + const existingDimension = await db + .selectFrom('dimension') + .selectAll() + .where('number', '=', number) + .executeTakeFirst() if (!existingDimension) { - await trx('dimension').insert({ number, name }) + await trx.insertInto('dimension').values({ number, name }).execute() } else if (existingDimension.name !== name) { - await trx('dimension').update({ name }).where('number', number) + await trx.updateTable('dimension').set({ name }).where('number', '=', number).execute() } break @@ -92,27 +99,36 @@ export default async function parseStream(stream: ReadableStream, decoder: Decod if (yearNumber !== 0) continue - const existingAccountBalance = await trx('accountBalance') - .first('*') - .where({ financialYearId: currentYear.id, accountNumber }) + const existingAccountBalance = await trx + .selectFrom('accountBalance') + .selectAll() + .where((eb) => eb.and({ financialYearId: currentYear!.id, accountNumber })) + .executeTakeFirst() if (!existingAccountBalance) { - await trx('accountBalance').insert({ - financialYearId: currentYear.id, - accountNumber, - in: balance, - inQuantity: quantity, - }) - } else { - await trx('accountBalance') - .update({ + await trx + .insertInto('accountBalance') + .values({ + financialYearId: currentYear!.id, + accountNumber, in: balance, inQuantity: quantity, }) - .where({ - financialYearId: currentYear.id, - accountNumber, + .execute() + } else { + await trx + .updateTable('accountBalance') + .set({ + in: balance, + inQuantity: quantity, }) + .where((eb) => + eb.and({ + financialYearId: currentYear!.id, + accountNumber, + }), + ) + .execute() } break @@ -120,19 +136,24 @@ export default async function parseStream(stream: ReadableStream, decoder: Decod case '#KONTO': { const { number, description } = parseKonto(line) - const existingAccount = await trx('account') - .first('*') - .where('number', number) + const existingAccount = await trx + .selectFrom('account') + .selectAll() + .where('number', '=', number) .orderBy('financialYearId', 'desc') + .executeTakeFirst() if (!existingAccount) { - await trx('account').insert({ - financialYearId: currentYear!.id, - number, - description, - }) + await trx + .insertInto('account') + .values({ + financialYearId: currentYear!.id, + number, + description, + }) + .execute() } else if (existingAccount.description !== description) { - await trx('account').update({ description }).where('id', existingAccount.id) + await trx.updateTable('account').set({ description }).where('id', '=', existingAccount.id).execute() } break @@ -140,16 +161,28 @@ export default async function parseStream(stream: ReadableStream, decoder: Decod case '#OBJEKT': { const { dimensionNumber, number, name } = parseObjekt(line) - const dimension = await trx('dimension').first('*').where('number', dimensionNumber) + const dimension = await trx + .selectFrom('dimension') + .selectAll() + .where('number', '=', dimensionNumber) + .executeTakeFirst() if (!dimension) throw new Error(`Dimension "${dimensionNumber}" does not exist`) - const existingObject = await trx('object').first('*').where({ dimensionId: dimension.id, number }) + const existingObject = await trx + .selectFrom('object') + .selectAll() + .where((eb) => eb.and({ dimensionId: dimension.id, number })) + .executeTakeFirst() if (!existingObject) { - await trx('object').insert({ dimensionId: dimension.id, number, name }) + await trx.insertInto('object').values({ dimensionId: dimension.id, number, name }).execute() } else if (existingObject.name !== name) { - await trx('object').update({ name }).where({ dimensionId: dimension.id, number }) + await trx + .updateTable('object') + .set({ name }) + .where((eb) => eb.and({ dimensionId: dimension.id, number })) + .execute() } break @@ -160,9 +193,11 @@ export default async function parseStream(stream: ReadableStream, decoder: Decod if (yearNumber !== 0) continue currentYear = ( - await trx('financialYear') - .insert({ year: startDate.slice(0, 4), startDate, endDate }) - .returning('*') + await trx + .insertInto('financialYear') + .values({ year: parseInt(startDate.slice(0, 4)), startDate, endDate }) + .returningAll() + .execute() )[0] break @@ -170,22 +205,27 @@ export default async function parseStream(stream: ReadableStream, decoder: Decod case '#SRU': { const { number, sru } = parseSRU(line) - const existingAccount = await trx('account') - .first('*') - .where('number', number) + const existingAccount = await trx + .selectFrom('account') + .selectAll() + .where('number', '=', number) .orderBy('financialYearId', 'desc') + .executeTakeFirst() if (existingAccount) { if (existingAccount.sru !== sru) { - await trx('account').update({ sru: sru }).where('id', existingAccount.id) + await trx.updateTable('account').set({ sru: sru }).where('id', '=', existingAccount.id).execute() } } else { - await trx('account').insert({ - financialYearId: currentYear!.id, - number, - description: existingAccount.description, - sru, - }) + await trx + .insertInto('account') + .values({ + financialYearId: currentYear!.id, + number, + description: existingAccount!.description, + sru, + }) + .execute() } break @@ -198,67 +238,95 @@ export default async function parseStream(stream: ReadableStream, decoder: Decod const result = transaction.description?.match(rFisken) - let invoiceId: number + let invoiceId: number | null | undefined if (result) { const [, fiskenNumber, supplierName] = result // invoiceId = (await trx('invoice').first('id').where({ fiskenNumber})).id - invoiceId = (await trx('invoice').first('id').where({ fiskenNumber }))?.id + invoiceId = ( + await trx + .selectFrom('invoice') + .select('id') + .where('fiskenNumber', '=', parseInt(fiskenNumber)) + .executeTakeFirst() + )?.id if (!invoiceId) { - let supplierId = (await trx('supplier').first('id').where('name', supplierName))?.id + let supplierId = ( + await trx.selectFrom('supplier').select('id').where('name', '=', supplierName).executeTakeFirst() + )?.id if (!supplierId) { - supplierId = (await trx('supplier').insert({ name: supplierName, supplierTypeId: 1 }).returning('id'))[0] - ?.id + supplierId = ( + await trx + .insertInto('supplier') + .values({ name: supplierName, supplierTypeId: 1 }) + .returning('id') + .executeTakeFirstOrThrow() + ).id } invoiceId = ( - await trx('invoice') - .insert({ financialYearId: currentYear!.id, fiskenNumber, supplierId }) + await trx + .insertInto('invoice') + .values({ financialYearId: currentYear!.id, fiskenNumber: parseInt(fiskenNumber), supplierId }) .returning('id') - )[0]?.id + .executeTakeFirstOrThrow() + ).id } if (transaction.accountNumber === 2441 && currentEntry!.description?.includes('Fakturajournal')) { - await trx('invoice').update('amount', Math.abs(transaction.amount)).where('id', invoiceId) + await trx + .updateTable('invoice') + .set('amount', Math.abs(transaction.amount)) + .where('id', '=', invoiceId!) + .execute() } } - if (invoiceId! && currentInvoiceId!) { + if (invoiceId && currentInvoiceId) { throw new Error('invoiceId and currentInvoiceId') } const transactionId = ( - await trx('transaction') - .insert({ + await trx + .insertInto('transaction') + .values({ entryId: currentEntry!.id, ...transaction, invoiceId: invoiceId! || currentInvoiceId!, }) .returning('id') - )[0].id + .executeTakeFirstOrThrow() + ).id if (objectList) { for (const [dimensionNumber, objectNumber] of objectList) { const objectId = ( - await trx('object') - .first('object.id') + await trx + .selectFrom('object') .innerJoin('dimension', 'object.dimensionId', 'dimension.id') - .where({ - 'object.number': objectNumber, - 'dimension.number': dimensionNumber, - }) + .select('object.id') + .where((eb) => + eb.and({ + 'object.number': objectNumber, + 'dimension.number': dimensionNumber, + }), + ) + .executeTakeFirst() )?.id if (!objectId) { throw new Error(`Object {${dimensionNumber} ${objectNumber}} does not exist!`) } - await trx('transactionsToObjects').insert({ - transactionId, - objectId, - }) + await trx + .insertInto('transactionsToObjects') + .values({ + transactionId, + objectId, + }) + .execute() } } @@ -269,27 +337,36 @@ export default async function parseStream(stream: ReadableStream, decoder: Decod if (yearNumber !== 0) continue - const existingAccountBalance = await trx('accountBalance') - .first('*') - .where({ financialYearId: currentYear!.id, accountNumber }) + const existingAccountBalance = await trx + .selectFrom('accountBalance') + .selectAll() + .where((eb) => eb.and({ financialYearId: currentYear!.id, accountNumber })) + .executeTakeFirst() if (!existingAccountBalance) { - await trx('accountBalance').insert({ - financialYearId: currentYear!.id, - accountNumber, - out: balance, - outQuantity: quantity, - }) - } else { - await trx('accountBalance') - .update({ + await trx + .insertInto('accountBalance') + .values({ + financialYearId: currentYear!.id, + accountNumber, out: balance, outQuantity: quantity, }) - .where({ - financialYearId: currentYear!.id, - accountNumber, + .execute() + } else { + await trx + .updateTable('accountBalance') + .set({ + out: balance, + outQuantity: quantity, }) + .where((eb) => + eb.and({ + financialYearId: currentYear!.id, + accountNumber, + }), + ) + .execute() } break @@ -310,24 +387,27 @@ export default async function parseStream(stream: ReadableStream, decoder: Decod if (result) { currentInvoiceId = ( - await trx('invoice') - .select('invoice.*', 'supplier.name') + await trx + .selectFrom('invoice') .innerJoin('supplier', 'invoice.supplierId', 'supplier.id') - .where('phmNumber', result[1]) - )[0]?.id + .selectAll('invoice') + .select('supplier.name') + .where('phmNumber', '=', parseInt(result[1])) + .executeTakeFirst() + )?.id } else { currentInvoiceId = null } - currentEntry = ( - await trx('entry') - .insert({ - journalId, - financialYearId: currentYear!.id, - ...rest, - }) - .returning(['id', 'description']) - )[0] + currentEntry = await trx + .insertInto('entry') + .values({ + journalId, + financialYearId: currentYear!.id, + ...rest, + }) + .returning(['id', 'description']) + .executeTakeFirstOrThrow() break } @@ -336,10 +416,10 @@ export default async function parseStream(stream: ReadableStream, decoder: Decod } } - await trx.commit() + await trx.commit().execute() // oxlint-disable-next-line no-console console.dir(details) - console.info(`DONE!: ${currentYear.startDate} - ${currentYear.endDate}`) + console.info(`DONE!: ${currentYear!.startDate} - ${currentYear!.endDate}`) } diff --git a/server/plugins/auth/routes/change_password.ts b/server/plugins/auth/routes/change_password.ts index 1cc96e1..59228a2 100644 --- a/server/plugins/auth/routes/change_password.ts +++ b/server/plugins/auth/routes/change_password.ts @@ -1,7 +1,7 @@ import type { RouteHandler } from 'fastify' import * as z from 'zod' +import { sql } from 'kysely' import config from '../../../config.ts' -import knex from '../../../lib/knex.ts' import StatusError from '../../../lib/status_error.ts' import { hashPassword } from '../helpers.ts' @@ -23,32 +23,38 @@ const changePassword: RouteHandler<{ Body: z.infer }> = async request, reply, ) { + const { db } = request.server + try { if (!request.body.email || !request.body.password || !request.body.token) { throw new StatusError(...errors.missingParameters) } const [token, latestToken] = await Promise.all([ - knex('passwordToken as pt') - .first('pt.id', 'u.id as userId', 'u.email', 'pt.token', 'pt.createdAt', 'pt.cancelledAt', 'pt.consumedAt') + db + .selectFrom('passwordToken as pt') .innerJoin('user as u', 'pt.userId', 'u.id') - .where({ - 'u.email': request.body.email, - 'pt.token': request.body.token, - }), - knex('passwordToken as pt') - .first('pt.id') + .select(['pt.id', 'u.id as userId', 'u.email', 'pt.token', 'pt.createdAt', 'pt.cancelledAt', 'pt.consumedAt']) + .where((eb) => + eb.and({ + 'u.email': request.body.email, + 'pt.token': request.body.token, + }), + ) + .executeTakeFirst(), + db + .selectFrom('passwordToken as pt') .innerJoin('user as u', 'pt.userId', 'u.id') - .where({ - 'u.email': request.body.email, - }) + .select('pt.id') + .where('u.email', '=', request.body.email) .orderBy('pt.createdAt', 'desc') - .limit(1), + .limit(1) + .executeTakeFirst(), ]) if (!token) { throw new StatusError(...errors.tokenNotFound) - } else if (token.id !== latestToken.id) { + } else if (token.id !== latestToken!.id) { throw new StatusError(...errors.tokenNotLatest) } else if (token.consumedAt) { throw new StatusError(...errors.tokenConsumed) @@ -58,10 +64,14 @@ const changePassword: RouteHandler<{ Body: z.infer }> = async const hash = await hashPassword(request.body.password) - await knex.transaction((trx) => + await db.transaction().execute((trx) => Promise.all([ - trx('user').update({ password: hash }).where('id', token.userId), - trx('password_token').update({ consumedAt: knex.fn.now() }).where('id', token.id), + trx.updateTable('user').set({ password: hash }).where('id', '=', token.userId).execute(), + trx + .updateTable('passwordToken') + .set({ consumedAt: sql`now()` }) + .where('id', '=', token.id) + .execute(), ]), ) diff --git a/server/plugins/auth/routes/login.ts b/server/plugins/auth/routes/login.ts index 02b69a7..2975d4d 100644 --- a/server/plugins/auth/routes/login.ts +++ b/server/plugins/auth/routes/login.ts @@ -1,15 +1,12 @@ import _ from 'lodash' -import * as z from 'zod' import type { RouteHandler } from 'fastify' +import * as z from 'zod' +import { sql } from 'kysely' import config from '../../../config.ts' -import UserQueries from '../../../services/users/queries.ts' -import emitter from '../../../lib/emitter.ts' -import knex from '../../../lib/knex.ts' import StatusError from '../../../lib/status_error.ts' import { StatusErrorSchema } from '../../../schemas/status_error.ts' import { verifyPassword } from '../helpers.ts' -const userQueries = UserQueries({ knex, emitter }) const { errors, maxLoginAttempts, requireVerification } = config.auth const BodySchema = z.object({ @@ -32,21 +29,27 @@ const ResponseSchema = { } const login: RouteHandler<{ Body: z.infer }> = async function (request, reply) { + const { db } = request.server + try { - const user = await userQueries.findOne({ email: request.body.email }) + const user = await db.selectFrom('user').selectAll().where('email', '=', request.body.email).executeTakeFirst() if (!user) { throw new StatusError(...errors.noUserFound) } else if (!user.password) { throw new StatusError(...errors.notLocal) - } else if (user.loginAttempts >= maxLoginAttempts) { + } else if (user.loginAttempts && user.loginAttempts >= maxLoginAttempts) { throw new StatusError(...errors.tooManyLoginAttempts) } const result = await verifyPassword(request.body.password, user.password) if (!result) { - await userQueries.onLoginAttempt(user.id) + await db + .updateTable('user') + .set({ lastLoginAttemptAt: sql`now()`, loginAttempts: sql`"loginAttempts" + 1` }) + .where('id', '=', user.id) + .execute() throw new StatusError(...errors.wrongPassword) } @@ -65,7 +68,11 @@ const login: RouteHandler<{ Body: z.infer }> = async function await request.login(user) - await userQueries.onLogin(user.id) + await db + .updateTable('user') + .set({ lastLoginAt: sql`now()`, loginAttempts: 0, lastLoginAttemptAt: null }) + .where('id', '=', user.id) + .execute() return reply.status(200).send(_.omit(user, 'password')) } catch (err) { diff --git a/server/plugins/auth/routes/reset_password.ts b/server/plugins/auth/routes/reset_password.ts index 7b151a0..f7c65ef 100644 --- a/server/plugins/auth/routes/reset_password.ts +++ b/server/plugins/auth/routes/reset_password.ts @@ -1,7 +1,6 @@ import type { RouteHandler } from 'fastify' import * as z from 'zod' import config from '../../../config.ts' -import knex from '../../../lib/knex.ts' import sendMail from '../../../lib/send_mail.ts' import StatusError from '../../../lib/status_error.ts' import { StatusErrorSchema } from '../../../schemas/status_error.ts' @@ -21,16 +20,18 @@ const ResponseSchema = { } const forgotPassword: RouteHandler<{ Body: z.infer }> = async function (request, reply) { + const { db } = request.server + const { email } = request.body try { - const user = await knex('user').first('id').where({ email }) + const user = await db.selectFrom('user').select('id').where('email', '=', email).executeTakeFirst() if (!user) throw new StatusError(...errors.noUserFound) const token = generateToken() - await knex('password_token').insert({ userId: user.id, token }) + await db.insertInto('passwordToken').values({ userId: user.id, token }).execute() const link = `${new URL('/admin/change-password', config.site.url)}?email=${encodeURI(email)}&token=${token}` diff --git a/server/plugins/auth/routes/verify_email.ts b/server/plugins/auth/routes/verify_email.ts index 5400be4..5693eb7 100644 --- a/server/plugins/auth/routes/verify_email.ts +++ b/server/plugins/auth/routes/verify_email.ts @@ -1,9 +1,9 @@ import type { RouteHandler } from 'fastify' import * as z from 'zod' import config from '../../../config.ts' -import knex from '../../../lib/knex.ts' import StatusError from '../../../lib/status_error.ts' import { StatusErrorSchema } from '../../../schemas/status_error.ts' +import { sql } from 'kysely' const { errors, timeouts } = config.auth @@ -18,23 +18,37 @@ const ResponseSchema = { } const verifyEmail: RouteHandler<{ Querystring: z.infer }> = async function (request, reply) { + const { db } = request.server + try { const { token, email } = request.query - const emailToken = await knex('email_token').first('*').where({ email, token }) + const emailToken = await db + .selectFrom('emailToken') + .selectAll() + .where((eb) => eb.and({ email, token })) + .executeTakeFirst() if (!emailToken) { throw new StatusError(...errors.tokenNotFound) - } else if (emailToken.consumed_at) { + } else if (emailToken.consumedAt) { throw new StatusError(...errors.tokenConsumed) - } else if (Date.now() > emailToken.created_at.getTime() + timeouts.verifyEmail) { + } else if (Date.now() > emailToken.createdAt.getTime() + timeouts.verifyEmail) { throw new StatusError(...errors.tokenExpired) } - await knex.transaction((trx) => + await db.transaction().execute((trx) => Promise.all([ - trx('user').update({ email_verified_at: knex.fn.now() }).where('id', emailToken.user_id), - trx('email_token').update({ consumed_at: knex.fn.now() }).where('id', emailToken.id), + trx + .updateTable('user') + .set({ emailVerifiedAt: sql`now()` }) + .where('id', '=', emailToken.userId) + .execute(), + trx + .updateTable('emailToken') + .set({ consumedAt: sql`now()` }) + .where('id', '=', emailToken.id) + .execute(), ]), ) diff --git a/server/routes/api/entries.ts b/server/routes/api/entries.ts index 2558687..78f0192 100644 --- a/server/routes/api/entries.ts +++ b/server/routes/api/entries.ts @@ -69,7 +69,7 @@ const entryRoutes: FastifyPluginCallbackZod = (fastify, _, done) => { .groupBy(['e.id', 'j.identifier']) .where('e.id', '=', req.params.id) .where('t.amount', '>', 0 as ANY) - .execute() + .executeTakeFirst() }, }) diff --git a/server/routes/api/invoices.ts b/server/routes/api/invoices.ts index d212889..be5c353 100644 --- a/server/routes/api/invoices.ts +++ b/server/routes/api/invoices.ts @@ -55,7 +55,7 @@ const invoiceRoutes: FastifyPluginCallbackZod = (fastify, _, done) => { method: 'GET', schema: { querystring: z.object({ - supplierId: z.number().optional(), + supplierId: z.coerce.number().optional(), }), response: { 200: z.object({ diff --git a/server/routes/api/suppliers.ts b/server/routes/api/suppliers.ts index 02bf08b..886853e 100644 --- a/server/routes/api/suppliers.ts +++ b/server/routes/api/suppliers.ts @@ -23,7 +23,7 @@ const supplierRoutes: FastifyPluginCallbackZod = (fastify, _, done) => { url: '/:id', method: 'GET', schema: { - params: z.object({ id: z.number() }), + params: z.object({ id: z.coerce.number() }), response: { 200: SupplierSchema, }, diff --git a/shared/types.db_composite.ts b/shared/types.db_composite.ts new file mode 100644 index 0000000..42d6b9a --- /dev/null +++ b/shared/types.db_composite.ts @@ -0,0 +1,9 @@ +export type Balance = { + accountNumber: string + description: string +} & Record + +export type Result = { + accountNumber: number + description?: string +} & Record