WIP more auth work and convert to kysely

This commit is contained in:
Linus Miller 2025-12-16 07:26:15 +01:00
parent 2c18a61315
commit 04e50a3021
34 changed files with 903 additions and 204 deletions

15
.bruno/BRF/Admissions.bru Normal file
View File

@ -0,0 +1,15 @@
meta {
name: Admissions
type: http
seq: 1
}
get {
url: {{base_url}}/api/admissions
body: none
auth: none
}
headers {
Accept: application/json
}

15
.bruno/BRF/Invites.bru Normal file
View File

@ -0,0 +1,15 @@
meta {
name: Invites
type: http
seq: 8
}
get {
url: http://localhost:4040/api/invites
body: none
auth: none
}
headers {
Accept: application/json
}

15
.bruno/BRF/Roles.bru Normal file
View File

@ -0,0 +1,15 @@
meta {
name: Roles
type: http
seq: 17
}
get {
url: {{base_url}}/api/roles
body: none
auth: none
}
headers {
Accept: application/json
}

15
.bruno/BRF/Users.bru Normal file
View File

@ -0,0 +1,15 @@
meta {
name: Users
type: http
seq: 9
}
get {
url: http://localhost:4040/api/users
body: none
auth: none
}
headers {
Accept: application/json
}

View File

@ -1,7 +1,7 @@
meta {
name: /api/balances
type: http
seq: 15
seq: 3
}
get {
@ -12,4 +12,5 @@ get {
settings {
encodeUrl: true
timeout: 0
}

View File

@ -1,7 +1,7 @@
meta {
name: /api/entries/:id
type: http
seq: 4
seq: 5
}
get {

View File

@ -1,7 +1,7 @@
meta {
name: /api/entries
type: http
seq: 3
seq: 4
}
get {

View File

@ -1,7 +1,7 @@
meta {
name: /api/financial-years
type: http
seq: 5
seq: 6
}
get {

View File

@ -1,7 +1,7 @@
meta {
name: /api/invoices/:id
type: http
seq: 8
seq: 10
}
get {

View File

@ -5,11 +5,17 @@ meta {
}
get {
url: {{base_url}}/api/invoices/total-amount
url: {{base_url}}/api/invoices/total-amount?supplier=150
body: none
auth: inherit
}
params:query {
supplier: 150
:
}
settings {
encodeUrl: true
timeout: 0
}

View File

@ -1,7 +1,7 @@
meta {
name: /api/invoices
type: http
seq: 8
seq: 9
}
get {

View File

@ -1,7 +1,7 @@
meta {
name: /api/objects/:id
type: http
seq: 6
seq: 7
}
get {

View File

@ -1,7 +1,7 @@
meta {
name: /api/objects
type: http
seq: 7
seq: 8
}
get {

View File

@ -1,7 +1,7 @@
meta {
name: /api/results/:year
type: http
seq: 12
seq: 14
}
get {

View File

@ -1,7 +1,7 @@
meta {
name: /api/results
type: http
seq: 11
seq: 12
}
get {

View File

@ -6,10 +6,21 @@ meta {
post {
url: {{base_url}}/api/suppliers/merge
body: none
body: json
auth: inherit
}
body:json {
{
"ids": [ 105, 203 ]
}
}
body:multipart-form {
ids: [
}
settings {
encodeUrl: true
timeout: 0
}

View File

@ -1,11 +1,11 @@
meta {
name: /api/suppliers
type: http
seq: 10
seq: 11
}
get {
url: {{base_url}}/
url: {{base_url}}/api/suppliers
body: none
auth: inherit
}

View File

@ -1,15 +1,21 @@
meta {
name: /api/transactions
type: http
seq: 14
seq: 16
}
get {
url: {{base_url}}/api/transactions
url: {{base_url}}/api/transactions?year=2020&accountNumber=4800
body: none
auth: inherit
}
params:query {
year: 2020
accountNumber: 4800
}
settings {
encodeUrl: true
timeout: 0
}

View File

@ -20,7 +20,8 @@
"test": "pnpm run test:client && pnpm run test:server",
"test:client": "node --no-warnings --import=./client/test/jsdom_polyfills.ts --import=./client/test/register_tsx_hook.ts --test ./client/**/*.test.ts{,x}",
"test:server": "node --env-file .env.testing --no-warnings --test ./server/**/*.test.ts",
"types": "tsgo --skipLibCheck"
"types": "tsgo --skipLibCheck",
"types:tsc": "tsc --skipLibCheck"
},
"dependencies": {
"@bmp/console": "^0.1.0",
@ -37,8 +38,10 @@
"fastify": "^5.6.2",
"fastify-plugin": "^5.1.0",
"fastify-session-redis-store": "^7.1.2",
"fastify-type-provider-zod": "^6.1.0",
"ioredis": "^5.8.2",
"knex": "^3.1.0",
"kysely": "^0.28.9",
"lodash": "^4.17.21",
"lowline": "^0.4.2",
"mini-qs": "^0.2.0",
@ -48,7 +51,8 @@
"preact": "^10.28.0",
"preact-iso": "^2.11.0",
"preact-router": "^4.1.2",
"rek": "^0.8.1"
"rek": "^0.8.1",
"zod": "^4.2.1"
},
"devDependencies": {
"@babel/core": "^7.26.10",

66
pnpm-lock.yaml generated
View File

@ -50,12 +50,18 @@ importers:
fastify-session-redis-store:
specifier: ^7.1.2
version: 7.1.2(@fastify/session@11.1.1)
fastify-type-provider-zod:
specifier: ^6.1.0
version: 6.1.0(@fastify/swagger@9.6.1)(fastify@5.6.2)(openapi-types@12.1.3)(zod@4.2.1)
ioredis:
specifier: ^5.8.2
version: 5.8.2
knex:
specifier: ^3.1.0
version: 3.1.0(pg@8.16.3)
kysely:
specifier: ^0.28.9
version: 0.28.9
lodash:
specifier: ^4.17.21
version: 4.17.21
@ -86,6 +92,9 @@ importers:
rek:
specifier: ^0.8.1
version: 0.8.1
zod:
specifier: ^4.2.1
version: 4.2.1
devDependencies:
'@babel/core':
specifier: ^7.26.10
@ -642,6 +651,9 @@ packages:
'@fastify/static@8.3.0':
resolution: {integrity: sha512-yKxviR5PH1OKNnisIzZKmgZSus0r2OZb8qCSbqmw34aolT4g3UlzYfeBRym+HJ1J471CR8e2ldNub4PubD1coA==}
'@fastify/swagger@9.6.1':
resolution: {integrity: sha512-fKlpJqFMWoi4H3EdUkDaMteEYRCfQMEkK0HJJ0eaf4aRlKd8cbq0pVkOfXDXmtvMTXYcnx3E+l023eFDBsA1HA==}
'@fastify/type-provider-typebox@6.1.0':
resolution: {integrity: sha512-k29cOitDRcZhMXVjtRq0+caKxdWoArz7su+dQWGzGWnFG+fSKhevgiZ7nexHWuXOEEQzgJlh6cptIMu69beaTA==}
peerDependencies:
@ -1382,6 +1394,14 @@ packages:
peerDependencies:
'@fastify/session': '>=10'
fastify-type-provider-zod@6.1.0:
resolution: {integrity: sha512-Sl19VZFSX4W/+AFl3hkL5YgWk3eDXZ4XYOdrq94HunK+o7GQBCAqgk7+3gPXoWkF0bNxOiIgfnFGJJ3i9a2BtQ==}
peerDependencies:
'@fastify/swagger': '>=9.5.1'
fastify: ^5.5.0
openapi-types: ^12.1.3
zod: '>=4.1.5'
fastify@5.6.2:
resolution: {integrity: sha512-dPugdGnsvYkBlENLhCgX8yhyGCsCPrpA8lFWbTNU428l+YOnLgYHR69hzV8HWPC79n536EqzqQtvhtdaCE0dKg==}
@ -1655,6 +1675,10 @@ packages:
json-schema-ref-resolver@3.0.0:
resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==}
json-schema-resolver@3.0.0:
resolution: {integrity: sha512-HqMnbz0tz2DaEJ3ntsqtx3ezzZyDE7G56A/pPY/NGmrPu76UzsWquOpHFRAf5beTNXoH2LU5cQePVvRli1nchA==}
engines: {node: '>=20'}
json-schema-traverse@1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
@ -1694,6 +1718,10 @@ packages:
kolorist@1.8.0:
resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
kysely@0.28.9:
resolution: {integrity: sha512-3BeXMoiOhpOwu62CiVpO6lxfq4eS6KMYfQdMsN/2kUCRNuF2YiEr7u0HLHaQU+O4Xu8YXE3bHVkwaQ85i72EuA==}
engines: {node: '>=20.0.0'}
light-my-request@6.6.0:
resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==}
@ -1818,6 +1846,9 @@ packages:
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
engines: {node: '>=18'}
openapi-types@12.1.3:
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
oxlint@1.29.0:
resolution: {integrity: sha512-YqUVUhTYDqazV2qu3QSQn/H4Z1OP+fTnedgZWDk1/lDZxGfR0b1MqRVaEm3rRjBMLHP0zXlriIWUx+DD6UMaPA==}
engines: {node: ^20.19.0 || >=22.12.0}
@ -2384,6 +2415,9 @@ packages:
engines: {node: '>= 14.6'}
hasBin: true
zod@4.2.1:
resolution: {integrity: sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==}
snapshots:
'@acemir/cssom@0.9.23': {}
@ -2785,6 +2819,16 @@ snapshots:
fastq: 1.19.1
glob: 11.1.0
'@fastify/swagger@9.6.1':
dependencies:
fastify-plugin: 5.1.0
json-schema-resolver: 3.0.0
openapi-types: 12.1.3
rfdc: 1.4.1
yaml: 2.8.1
transitivePeerDependencies:
- supports-color
'@fastify/type-provider-typebox@6.1.0(typebox@1.0.55)':
dependencies:
typebox: 1.0.55
@ -3487,6 +3531,14 @@ snapshots:
dependencies:
'@fastify/session': 11.1.1
fastify-type-provider-zod@6.1.0(@fastify/swagger@9.6.1)(fastify@5.6.2)(openapi-types@12.1.3)(zod@4.2.1):
dependencies:
'@fastify/error': 4.2.0
'@fastify/swagger': 9.6.1
fastify: 5.6.2
openapi-types: 12.1.3
zod: 4.2.1
fastify@5.6.2:
dependencies:
'@fastify/ajv-compiler': 4.0.5
@ -3790,6 +3842,14 @@ snapshots:
dependencies:
dequal: 2.0.3
json-schema-resolver@3.0.0:
dependencies:
debug: 4.4.3
fast-uri: 3.1.0
rfdc: 1.4.1
transitivePeerDependencies:
- supports-color
json-schema-traverse@1.0.0: {}
json5@2.2.3: {}
@ -3817,6 +3877,8 @@ snapshots:
kolorist@1.8.0: {}
kysely@0.28.9: {}
light-my-request@6.6.0:
dependencies:
cookie: 1.0.2
@ -3937,6 +3999,8 @@ snapshots:
dependencies:
mimic-function: 5.0.1
openapi-types@12.1.3: {}
oxlint@1.29.0:
optionalDependencies:
'@oxlint/darwin-arm64': 1.29.0
@ -4470,3 +4534,5 @@ snapshots:
yallist@3.1.1: {}
yaml@2.8.1: {}
zod@4.2.1: {}

17
server/lib/kysely.ts Normal file
View File

@ -0,0 +1,17 @@
// @ts-ignore
import { Pool } from 'pg'
import { Kysely, PostgresDialect } from 'kysely'
import env from '../env.ts'
import type { DB } from '../../shared/types.db.ts'
const dialect = new PostgresDialect({
pool: new Pool({
database: env.PGDATABASE,
host: env.PGHOST,
password: env.PGPASSWORD,
user: env.PGUSER,
port: env.PGPORT,
}),
})
export default new Kysely<DB>({ dialect })

View File

@ -0,0 +1,47 @@
// import type { QueryBuilder } from 'knex'
import type { SelectQueryBuilder } from 'kysely'
import _ from 'lodash'
// export function convertToReturning(obj: Record<string, string>) {
// return _.map(obj, (value, key) => _.isString(value) && `${value} as ${key}`).filter(_.identity)
// }
// export function columnAs(name: string, table: string, transformer = _.snakeCase) {
// const transformed = transformer(name)
// if (transformed !== name) {
// // knex automagically wraps everything in ", so they are not needed around ${name}
// return `${table ? table + '.' : ''}${transformed} as ${name}`
// }
// return table ? `${table}.${name}` : name
// }
// export function columnBuilder(builder: QueryBuilder, columns: Record<string, string>, columnNames: string[]) {
// for (const columnName of columnNames) {
// const column = columns[columnName]
// if (column) {
// // @ts-ignore
// builder.column(column)
// }
// }
// return builder
// }
export function where<DB, TB extends keyof DB, O>(builder: SelectQueryBuilder<DB, TB, O>, json: Record<string, ANY>) {
return _.reduce(
json,
(builder, value, key) => {
if (value === null) {
return builder.where(key, 'is', null)
} else if (Array.isArray(value)) {
return builder.where(key, 'in', value)
}
return builder.where(key, '=', value)
},
builder,
)
}

View File

@ -0,0 +1,78 @@
// @ts-nocheck
// import { type Knex } from 'kysely'
// import _ from 'lodash'
// import { where } from './kysely_helpers.ts'
// type QueryName =
// | 'create'
// | 'createMany'
// | 'find'
// | 'findOne'
// | 'findById'
// | 'getAll'
// | 'paginate'
// | 'remove'
// | 'removeById'
// | 'replace'
// | 'update'
// interface Options {
// kysely: Knex
// emitter?: ANY
// pick?: QueryName[]
// omit?: QueryName[]
// columns: string[]
// table: string
// selects?: Record<string, ANY>
// }
// export const count = ({ kysely, table, columns }: Pick<Options, 'kysely' | 'table' | 'columns'>) =>
// function count(query: Record<string, ANY>, client = kysely) {
// let builder = client.table(table)
// if (!_.isEmpty(query)) {
// builder = where(builder, _.pick(query, columns))
// }
// return builder.count().then((count) => parseInt(count[0].count, 10))
// }
export const find = ({
kysely,
table,
columns,
allSelects,
defaults = {},
}: Pick<Options, 'kysely' | 'table' | 'columns'> & { allSelects: string[]; defaults: Record<string, any> }) =>
function find(json, select, client: Knex) {
if (!client) {
// TODO figure out if better, eg instanceof, check is possible
if (select && select.andWhereNotBetween) {
client = select
select = null
} else {
client = kysely
}
}
const { offset = defaults.offset, limit = defaults.limit, sort = defaults.sort, ...query } = json
let builder = client.table(table).select(select ? _.pick(allSelects, select) : allSelects)
if (!_.isEmpty(query)) {
builder = where(builder, _.pick(query, columns))
}
builder = builder.orderBy(sort || 'id', sort?.startsWith('-') ? 'desc' : 'asc')
if (limit) {
builder = builder.limit(limit)
}
if (offset) {
builder = builder.offset(offset)
}
return builder
}

17
server/plugins/db.ts Normal file
View File

@ -0,0 +1,17 @@
import type { FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox'
import type { Kysely } from 'kysely'
import type { DB } from '../../shared/types.db.ts'
import fp from 'fastify-plugin'
const dbPlugin: FastifyPluginCallbackTypebox<{ kysely: Kysely<DB> }> = (fastify, { kysely }, done) => {
fastify.decorate('db', kysely)
fastify.addHook('onClose', () => kysely.destroy())
done()
}
export default fp(dbPlugin, {
fastify: '5.x',
name: 'dbPlugin',
})

View File

@ -10,6 +10,7 @@ import journals from './api/journals.ts'
import objects from './api/objects.ts'
import process from './api/process.ts'
import results from './api/results.ts'
import roles from './api/roles.ts'
import suppliers from './api/suppliers.ts'
import transactions from './api/transactions.ts'
@ -23,6 +24,7 @@ const apiRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
fastify.register(objects, { prefix: '/objects' })
fastify.register(process, { prefix: '/process' })
fastify.register(results, { prefix: '/results' })
fastify.register(roles, { prefix: '/roles' })
fastify.register(suppliers, { prefix: '/suppliers' })
fastify.register(transactions, { prefix: '/transactions' })

View File

@ -1,102 +1,151 @@
import { Type, type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox'
import knex from '../../lib/knex.ts'
import Queries from '../../services/roles/queries.ts'
import _ from 'lodash'
import * as z from 'zod'
import { type FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
// import knex from '../../lib/knex.ts'
// import Queries from '../../services/roles/queries.ts'
export const RoleFullSchema = Type.Object({
id: Type.Number(),
name: Type.String(),
createdAt: Type.String(),
createdById: Type.Number(),
modifiedAt: Type.String(),
modifiedById: Type.Number(),
})
import { RoleSchema } from '../../schemas/db.ts'
export const RoleSchema = Type.Pick(RoleFullSchema, ['id', 'name'])
// export const RoleFullSchema = Type.Object({
// id: Type.Number(),
// name: Type.String(),
// createdAt: Type.String(),
// createdById: Type.Number(),
// modifiedAt: Type.String(),
// modifiedById: Type.Number(),
// })
export const RoleVariableSchema = Type.Pick(RoleFullSchema, ['name', 'createdById'])
// console.log(RoleFullSchema)
const rolesPlugin: FastifyPluginCallbackTypebox = (fastify, _options, done) => {
const queries = Queries({ knex })
// export const RoleSchema = Type.Pick(RoleFullSchema, ['id', 'name'])
fastify.addHook('onRequest', fastify.auth)
// export const RoleVariableSchema = Type.Pick(RoleFullSchema, ['name', 'createdById'])
const rolesPlugin: FastifyPluginCallbackZod = (fastify, _options, done) => {
// const queries = Queries({ knex })
const { db } = fastify
// fastify.addHook('onRequest', fastify.auth)
fastify.route({
url: '/',
method: 'GET',
schema: {
querystring: RoleFullSchema,
querystring: RoleSchema.partial().extend({
limit: z.number().optional(),
sort: z.keyof(RoleSchema).optional(),
offset: z.number().optional(),
}),
response: {
200: Type.Array(RoleFullSchema),
200: z.array(RoleSchema),
},
},
handler(request) {
return queries.find(request.query)
// if (!client) {
// // TODO figure out if better, eg instanceof, check is possible
// if (select && select.andWhereNotBetween) {
// client = select
// select = null
// } else {
// client = kysely
// }
// }
const { offset, limit, sort, ...query } = request.query
let builder = db.selectFrom('role').selectAll()
// if (!_.isEmpty(query)) {
// builder = where(builder, _.pick(query, columns))
// }
for (const [key, value] of Object.entries(query)) {
if (value === null) {
builder = builder.where(key, 'is', null)
} else if (Array.isArray(value)) {
builder = builder.where(key, 'in', value)
} else {
builder = builder.where(key, '=', value)
}
}
builder = builder.orderBy(sort || 'id', sort?.startsWith('-') ? 'desc' : 'asc')
if (limit) {
builder = builder.limit(limit)
}
if (offset) {
builder = builder.offset(offset)
}
return builder.execute()
},
})
fastify.route({
url: '/',
method: 'POST',
schema: {
body: RoleVariableSchema,
response: {
201: RoleFullSchema,
},
},
async handler(request, reply) {
const newRole = request.session.userId
? {
...request.body,
createdById: request.session.userId,
}
: request.body
// fastify.route({
// url: '/',
// method: 'POST',
// schema: {
// body: RoleVariableSchema,
// response: {
// 201: RoleFullSchema,
// },
// },
// async handler(request, reply) {
// const newRole = request.session.userId
// ? {
// ...request.body,
// createdById: request.session.userId,
// }
// : request.body
return queries.create(newRole).then((row) => {
return reply.header('Location', `${request.url}/${row.id}`).status(201).send(row)
})
},
})
// return queries.create(newRole).then((row) => {
// return reply.header('Location', `${request.url}/${row.id}`).status(201).send(row)
// })
// },
// })
fastify.route({
url: '/:id',
method: 'DELETE',
schema: {
params: Type.Object({
id: Type.Number(),
}),
response: {
204: {},
404: {},
},
},
handler(request, reply) {
return queries.removeById(request.params.id).then((count) => reply.status(count > 0 ? 204 : 404).send())
},
})
// fastify.route({
// url: '/:id',
// method: 'DELETE',
// schema: {
// params: Type.Object({
// id: Type.Number(),
// }),
// response: {
// 204: {},
// 404: {},
// },
// },
// handler(request, reply) {
// return queries.removeById(request.params.id).then((count) => reply.status(count > 0 ? 204 : 404).send())
// },
// })
fastify.route({
url: '/:id',
method: 'PATCH',
schema: {
params: Type.Object({
id: Type.Number(),
}),
body: RoleVariableSchema,
response: {
204: {},
},
},
async handler(request) {
const patch = request.session.userId
? {
...request.body,
modifiedById: request.session.userId,
}
: request.body
// fastify.route({
// url: '/:id',
// method: 'PATCH',
// schema: {
// params: Type.Object({
// id: Type.Number(),
// }),
// body: RoleVariableSchema,
// response: {
// 204: {},
// },
// },
// async handler(request) {
// const patch = request.session.userId
// ? {
// ...request.body,
// modifiedById: request.session.userId,
// }
// : request.body
return queries.update(request.params.id, patch)
},
})
// return queries.update(request.params.id, patch)
// },
// })
done()
}

View File

@ -1,13 +1,21 @@
import _ from 'lodash'
import { Type, type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox'
import knex from '../../lib/knex.ts'
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
import z from 'zod'
import { SupplierSchema } from '../../schemas/db.ts'
const supplierRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
const { db } = fastify
const journalRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
fastify.route({
url: '/',
method: 'GET',
async handler() {
return knex('supplier').select('*').orderBy('name')
schema: {
response: {
200: z.array(SupplierSchema),
},
},
handler() {
return db.selectFrom('supplier').selectAll().orderBy('name').execute()
},
})
@ -15,12 +23,13 @@ const journalRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
url: '/:id',
method: 'GET',
schema: {
params: Type.Object({
id: Type.Number(),
}),
params: z.object({ id: z.number() }),
response: {
200: SupplierSchema,
},
},
async handler(req) {
return knex('supplier').first('*').where('id', req.params.id)
handler(req) {
return db.selectFrom('supplier').selectAll().where('id', '=', req.params.id).executeTakeFirst()
},
})
@ -28,20 +37,23 @@ const journalRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
url: '/merge',
method: 'POST',
schema: {
body: Type.Object({
ids: Type.Array(Type.Number()),
body: z.object({
ids: z.array(z.number()),
}),
},
async handler(req) {
const suppliers = await knex('supplier').select('*').whereIn('id', req.body.ids)
const suppliers = await db.selectFrom('supplier').selectAll().where('id', 'in', req.body.ids).execute()
const trx = await knex.transaction()
const trx = await db.startTransaction().execute()
await trx('invoice').update('supplierId', req.body.ids[0]).whereIn('supplierId', req.body.ids.slice(1))
await trx('supplier').delete().whereIn('id', req.body.ids.slice(1))
// 556744-4301
await trx
.updateTable('invoice')
.set('supplierId', req.body.ids[0])
.where('supplierId', 'in', req.body.ids.slice(1))
.execute()
await trx.deleteFrom('supplier').where('id', 'in', req.body.ids.slice(1)).execute()
trx.commit()
await trx.commit().execute()
return suppliers
},
@ -50,4 +62,4 @@ const journalRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
done()
}
export default journalRoutes
export default supplierRoutes

View File

@ -1,25 +1,32 @@
import _ from 'lodash'
import { Type, type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox'
import knex from '../../lib/knex.ts'
import * as z from 'zod'
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
import StatusError from '../../lib/status_error.ts'
const transactionRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
const transactionRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
const { db } = fastify
fastify.route({
url: '/',
method: 'GET',
schema: {
querystring: Type.Object({
year: Type.Optional(Type.Number()),
accountNumber: Type.Optional(Type.Number()),
querystring: z.object({
year: z.optional(z.coerce.number()),
accountNumber: z.optional(z.coerce.number()),
}),
},
async handler(req) {
const query: { financialYearId?: number; accountNumber?: number } = {}
if (req.query.year) {
const year = await knex('financialYear').first('*').where('year', req.query.year)
const year = await db
.selectFrom('financialYear')
.selectAll()
.where('year', '=', req.query.year)
.executeTakeFirst()
if (!year) throw new StatusError(404, `Year ${req.query.year} not found.`)
query.financialYearId = year.id
}
@ -27,18 +34,20 @@ const transactionRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
query.accountNumber = req.query.accountNumber
}
return knex('transaction AS t')
.select(
return db
.selectFrom('transaction as t')
.innerJoin('entry as e', 't.entryId', 'e.id')
.select([
't.accountNumber',
'e.transactionDate',
't.entryId',
't.amount',
't.description',
't.invoiceId',
'e.description AS entryDescription',
)
.innerJoin('entry AS e', 't.entryId', 'e.id')
.where(query)
'e.description as entryDescription',
])
.where((eb) => eb.and(query))
.execute()
},
})
@ -46,12 +55,12 @@ const transactionRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
url: '/:id',
method: 'GET',
schema: {
params: Type.Object({
id: Type.Number(),
params: z.object({
id: z.number(),
}),
},
async handler(req) {
return knex('transaction').first('*').where('id', req.params.id)
handler(req) {
return db.selectFrom('transaction').selectAll().where('id', '=', req.params.id).execute()
},
})

22
server/schemas/db.ts Normal file
View File

@ -0,0 +1,22 @@
import { z } from 'zod'
export const RoleSchema = z.object({
id: z.number().int().optional(),
name: z.string(),
createdAt: z.string().optional(),
createdById: z.number().int().nullable().optional(),
modifiedAt: z.string().nullable().optional(),
modifiedById: z.number().int().nullable().optional(),
})
export const SupplierSchema = z.object({
id: z.number().int().optional(),
name: z.string().nullable().optional(),
supplierTypeId: z.number().int(),
taxId: z.string().nullable().optional(),
})
export const SupplierTypeSchema = z.object({
id: z.number().int().optional(),
name: z.string(),
})

View File

@ -3,12 +3,15 @@ import fcookie from '@fastify/cookie'
import fsession from '@fastify/session'
import fstatic from '@fastify/static'
import RedisStore from 'fastify-session-redis-store'
import { serializerCompiler, validatorCompiler } from 'fastify-type-provider-zod'
import { Redis } from 'ioredis'
import kysely from './lib/kysely.ts'
import StatusError from './lib/status_error.ts'
import env from './env.ts'
import ErrorHandler from './handlers/error.ts'
import authPlugin from './plugins/auth.ts'
import dbPlugin from './plugins/db.ts'
import vitePlugin from './plugins/vite.ts'
import apiRoutes from './routes/api.ts'
import templateAdmin from './templates/admin.ts'
@ -17,6 +20,9 @@ import templatePublic from './templates/public.ts'
export default async (options?: FastifyServerOptions) => {
const server = fastify(options)
server.setValidatorCompiler(validatorCompiler)
server.setSerializerCompiler(serializerCompiler)
server.register(fcookie)
server.register(fsession, {
cookie: {
@ -27,6 +33,7 @@ export default async (options?: FastifyServerOptions) => {
secret: env.SESSION_SECRET,
store: env.NODE_ENV !== 'testing' ? new RedisStore({ client: new Redis(env.REDIS_HOST) }) : undefined,
})
server.register(dbPlugin, { kysely })
server.setNotFoundHandler(() => {
throw new StatusError(404)

View File

@ -0,0 +1,21 @@
import { test, type TestContext } from 'node:test'
import supplierPlugin from '../routes/api/suppliers.ts'
import fastify from 'fastify'
test('/api/suppliers', async (t: TestContext) => {
const server = fastify()
// server.decorate('auth', (_request, _reply, done) => done())
server.register(supplierPlugin, { prefix: '/api/suppliers' })
const res = await server.inject({
method: 'GET',
url: '/api/suppliers',
})
t.assert.equal(res.statusCode, 200)
await server.close()
})

3
shared/global.d.ts vendored
View File

@ -1,5 +1,7 @@
import 'fastify'
import type { onRequestHookHandler } from 'fastify'
import { DB } from './types.db.ts'
import type { Kysely } from 'kysely'
import { ViteDevServer } from 'vite'
@ -16,6 +18,7 @@ declare module 'fastify' {
interface FastifyInstance {
auth: onRequestHookHandler
db: Kysely<DB>
devServer: ViteDevServer
}

261
shared/types.db.ts Normal file
View File

@ -0,0 +1,261 @@
/**
* This file was generated by kysely-codegen.
* Please do not edit it manually.
*/
import type { ColumnType } from 'kysely'
export type Generated<T> =
T extends ColumnType<infer S, infer I, infer U> ? ColumnType<S, I | undefined, U> : ColumnType<T, T | undefined, T>
export type Json = JsonValue
export type JsonArray = JsonValue[]
export type JsonObject = {
[x: string]: JsonValue | undefined
}
export type JsonPrimitive = boolean | number | string | null
export type JsonValue = JsonArray | JsonObject | JsonPrimitive
export type Numeric = ColumnType<string, number | string, number | string>
export type Timestamp = ColumnType<Date, Date | string, Date | string>
export interface Account {
description: string
financialYearId: number
id: Generated<number>
number: number
sru: number | null
}
export interface AccountBalance {
accountNumber: number
financialYearId: number
in: Generated<Numeric>
inQuantity: number | null
out: Generated<Numeric>
outQuantity: number | null
}
export interface Admission {
createdAt: Generated<Timestamp | null>
createdById: number
id: Generated<number>
modifiedAt: Timestamp | null
modifiedById: number | null
regex: string
}
export interface AdmissionsRoles {
admissionId: number
roleId: number
}
export interface AliasesToSupplier {
alias: string
id: Generated<number>
supplierId: number
}
export interface Dimension {
id: Generated<number>
name: string | null
number: number
}
export interface EmailToken {
cancelledAt: Timestamp | null
consumedAt: Timestamp | null
createdAt: Generated<Timestamp>
email: string
id: Generated<number>
token: string
userId: number
}
export interface Entry {
description: string | null
entryDate: Timestamp
financialYearId: number
id: Generated<number>
journalId: number
number: number
signature: string | null
transactionDate: Timestamp
}
export interface Error {
createdAt: Timestamp | null
details: Json | null
headers: Json | null
id: Generated<number>
ip: string | null
message: string | null
method: string | null
path: string | null
reqId: string | null
stack: string | null
statusCode: number | null
type: string | null
}
export interface File {
filename: string
id: Generated<number>
}
export interface FilesToInvoice {
fileId: number
invoiceId: number
}
export interface FinancialYear {
endDate: Timestamp
id: Generated<number>
startDate: Timestamp
year: number
}
export interface Invite {
consumedAt: Timestamp | null
consumedById: number | null
createdAt: Generated<Timestamp>
createdById: number | null
email: string
id: Generated<number>
modifiedAt: Timestamp | null
modifiedById: number | null
token: string
}
export interface InvitesRoles {
invitedId: number
roleId: number
}
export interface Invoice {
amount: Numeric | null
dueDate: Timestamp | null
financialYearId: number | null
fiskenNumber: number | null
id: Generated<number>
invoiceDate: Timestamp | null
invoiceNumber: string | null
ocr: string | null
phmNumber: number | null
supplierId: number
}
export interface Journal {
description: string | null
id: Generated<number>
identifier: string
}
export interface Object {
dimensionId: number
id: Generated<number>
name: string | null
number: number
}
export interface PasswordToken {
cancelledAt: Timestamp | null
consumedAt: Timestamp | null
createdAt: Generated<Timestamp>
id: Generated<number>
token: string
userId: number
}
export interface Role {
createdAt: Generated<Timestamp>
createdById: number | null
id: Generated<number>
modifiedAt: Timestamp | null
modifiedById: number | null
name: string
}
export interface Supplier {
id: Generated<number>
name: string | null
supplierTypeId: number
taxId: string | null
}
export interface SupplierType {
id: Generated<number>
name: string
}
export interface Transaction {
accountNumber: number
amount: Numeric
description: string | null
entryId: number
id: Generated<number>
invoiceId: number | null
objectId: number | null
quantity: Numeric | null
signature: string | null
transactionDate: Timestamp | null
}
export interface TransactionsToObjects {
objectId: number
transactionId: number
}
export interface User {
bannedAt: Timestamp | null
bannedById: number | null
blockedAt: Timestamp | null
blockedById: number | null
createdAt: Generated<Timestamp>
email: string
emailVerifiedAt: Timestamp | null
id: Generated<number>
lastActivityAt: Timestamp | null
lastLoginAt: Timestamp | null
lastLoginAttemptAt: Timestamp | null
loginAttempts: Generated<number | null>
password: string
}
export interface UsersRoles {
roleId: number
userId: number
}
export interface DB {
account: Account
accountBalance: AccountBalance
admission: Admission
admissions_roles: AdmissionsRoles
aliasesToSupplier: AliasesToSupplier
dimension: Dimension
emailToken: EmailToken
entry: Entry
error: Error
file: File
filesToInvoice: FilesToInvoice
financialYear: FinancialYear
invite: Invite
invites_roles: InvitesRoles
invoice: Invoice
journal: Journal
object: Object
passwordToken: PasswordToken
role: Role
supplier: Supplier
supplierType: SupplierType
transaction: Transaction
transactionsToObjects: TransactionsToObjects
user: User
users_roles: UsersRoles
}

View File

@ -1,85 +1,85 @@
export type Account = {
id: number
number: number
description: string
}
// export type Account = {
// id: number
// number: number
// description: string
// }
export type Balance = {
accountNumber: string
description: string
} & Record<number, number>
// export type Balance = {
// accountNumber: string
// description: string
// } & Record<number, number>
export interface Entry {
id: number
journal: string
number: number
amount: number
description: string
transactionDate: string
entryDate: string
transactions: {
accountNumber: number
description: string
amount: number
}[]
}
// export interface Entry {
// id: number
// journal: string
// number: number
// amount: number
// description: string
// transactionDate: string
// entryDate: string
// transactions: {
// accountNumber: number
// description: string
// amount: number
// }[]
// }
export type FinancialYear = {
year: number
startDate: string
endDate: string
}
// export type FinancialYear = {
// year: number
// startDate: string
// endDate: string
// }
export type Invoice = {
id: number
fiskenNumber?: number
phmNumber?: number
invoiceDate: string
dueDate: string
invoiceNumber: number
amount: number
files?: { filename: string }[]
transactions?: {
accountNumber: number
amount: number
description: number
entryId: number
}[]
}
// export type Invoice = {
// id: number
// fiskenNumber?: number
// phmNumber?: number
// invoiceDate: string
// dueDate: string
// invoiceNumber: number
// amount: number
// files?: { filename: string }[]
// transactions?: {
// accountNumber: number
// amount: number
// description: number
// entryId: number
// }[]
// }
export type Journal = {
id: number
identifier: string
}
// export type Journal = {
// id: number
// identifier: string
// }
export type Object = {
id: number
dimensionName: string
name: string
}
// export type Object = {
// id: number
// dimensionName: string
// name: string
// }
export type Result = {
accountNumber: number
description?: string
} & Record<number, number>
// export type Result = {
// accountNumber: number
// description?: string
// } & Record<number, number>
export type Supplier = {
id: number
name: string
}
// export type Supplier = {
// id: number
// name: string
// }
export interface Transaction {
accountNumber: number
description: string
amount: number
entryId: number
}
// export interface Transaction {
// accountNumber: number
// description: string
// amount: number
// entryId: number
// }
export interface TransactionFull extends Transaction {
transactionDate: string
invoiceId: number
entryDescription: string
}
// export interface TransactionFull extends Transaction {
// transactionDate: string
// invoiceId: number
// entryDescription: string
// }
export interface Route {
path: string