WIP more auth work and convert to kysely and zod

This commit is contained in:
Linus Miller 2025-12-16 23:04:38 +01:00
parent 04e50a3021
commit 9d9ee1b4ce
60 changed files with 647 additions and 507 deletions

View File

@ -1,5 +1,5 @@
meta { meta {
name: Admissions name: /api/admissions
type: http type: http
seq: 1 seq: 1
} }

View File

@ -1,11 +1,11 @@
meta { meta {
name: Invites name: /api/invites
type: http type: http
seq: 8 seq: 10
} }
get { get {
url: http://localhost:4040/api/invites url: {{base_url}}/api/invites
body: none body: none
auth: none auth: none
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
meta { meta {
name: Roles name: /api/roles
type: http type: http
seq: 17 seq: 15
} }
get { get {

View File

@ -1,7 +1,7 @@
meta { meta {
name: /api/suppliers/merge name: /api/suppliers/merge
type: http type: http
seq: 13 seq: 17
} }
post { post {

View File

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

View File

@ -1,7 +1,7 @@
meta { meta {
name: /api/transactions name: /api/transactions
type: http type: http
seq: 16 seq: 18
} }
get { get {

View File

@ -1,7 +1,7 @@
meta { meta {
name: Users name: /api/users
type: http type: http
seq: 9 seq: 19
} }
get { get {

8
.bruno/API/folder.bru Normal file
View File

@ -0,0 +1,8 @@
meta {
name: API
seq: 2
}
auth {
mode: inherit
}

18
.bruno/Auth/Login.bru Normal file
View File

@ -0,0 +1,18 @@
meta {
name: Login
type: http
seq: 1
}
post {
url: {{base_url}}/auth/login
body: json
auth: none
}
body:json {
{
"email": "linus.miller@bitmill.io",
"password": "rasmus"
}
}

11
.bruno/Auth/Logout.bru Normal file
View File

@ -0,0 +1,11 @@
meta {
name: Logout
type: http
seq: 2
}
get {
url: {{base_url}}/auth/logout
body: none
auth: none
}

View File

@ -25,9 +25,11 @@ const AdmissionForm: FunctionComponent<{
actions.pending() actions.pending()
try { try {
const form = e.currentTarget
const result = await rek[admission ? 'patch' : 'post']( const result = await rek[admission ? 'patch' : 'post'](
`/api/admissions${admission ? '/' + admission.id : ''}`, `/api/admissions${admission ? '/' + admission.id : ''}`,
serializeForm(e.currentTarget), serializeForm(form),
) )
actions.success() actions.success()
@ -35,7 +37,7 @@ const AdmissionForm: FunctionComponent<{
if (admission) { if (admission) {
onUpdate?.(result) onUpdate?.(result)
} else { } else {
e.currentTarget.reset() form.reset()
onCreate?.(result) onCreate?.(result)
} }
} catch (err) { } catch (err) {

View File

@ -1,37 +0,0 @@
import { h, type FunctionComponent } from 'preact'
import { useEffect, useState } from 'preact/hooks'
import rek from 'rek'
import { Table, Td } from './table.tsx'
const GitLog: FunctionComponent = () => {
const [commits, setCommits] = useState<ANY[] | null>(null)
useEffect(() => {
rek('/api/git-log').then(setCommits)
}, [])
return (
commits && (
<Table>
<tbody>
{commits.map((commit) => (
<tr key={commit.hash}>
<Td minimize>{new Date(commit.date).toLocaleString('sv-SE')}</Td>
<Td minimize>{commit.author}</Td>
<Td>{commit.message}</Td>
<Td>
(
<a target='_blank' href={`https://github.com/tlth/new_fmval/commit/${commit.hash}`} rel='noreferrer'>
{commit.hash.slice(0, 6)}
</a>
)
</Td>
</tr>
))}
</tbody>
</Table>
)
)
}
export default GitLog

View File

@ -18,9 +18,10 @@ const InviteForm: FunctionComponent<{ onCreate?: ANY; roles?: ANY[] }> = ({ onCr
actions.pending() actions.pending()
try { try {
const result = await rek.post('/api/invites', serializeForm(e.currentTarget)) const form = e.currentTarget
const result = await rek.post('/api/invites', serializeForm(form))
e.currentTarget.reset() form.reset()
actions.success() actions.success()
onCreate?.(result) onCreate?.(result)
} catch (err) { } catch (err) {

View File

@ -23,17 +23,16 @@ const RoleForm: FunctionComponent<{ role?: ANY; onCancel?: ANY; onCreate?: ANY;
actions.pending() actions.pending()
try { try {
const result = await rek[role ? 'patch' : 'post']( const form = e.currentTarget
`/api/roles${role ? '/' + role.id : ''}`,
serializeForm(e.currentTarget), const result = await rek[role ? 'patch' : 'post'](`/api/roles${role ? '/' + role.id : ''}`, serializeForm(form))
)
actions.success() actions.success()
if (role) { if (role) {
onUpdate?.(result) onUpdate?.(result)
} else { } else {
e.currentTarget.reset() form.reset()
onCreate?.(result) onCreate?.(result)
} }
} catch (err) { } catch (err) {

View File

@ -1,7 +1,6 @@
import { h } from 'preact' import { h } from 'preact'
import PageHeader from './page_header.tsx' import PageHeader from './page_header.tsx'
import Row from './row.tsx' import Row from './row.tsx'
import GitLog from './git_log.tsx'
import Process from './process.tsx' import Process from './process.tsx'
import Section from './section.tsx' import Section from './section.tsx'
@ -16,13 +15,6 @@ const StartPage = () => (
</Section.Body> </Section.Body>
</Section> </Section>
<Section>
<Section.Heading>Latest Commits</Section.Heading>
<Section.Body>
<GitLog />
</Section.Body>
</Section>
<Row> <Row>
<Section> <Section>
<Section.Heading>Process</Section.Heading> <Section.Heading>Process</Section.Heading>

View File

@ -159,7 +159,7 @@ ALTER SEQUENCE public.invite_id_seq OWNED BY public.invite.id;
-- --
CREATE TABLE public.invites_roles ( CREATE TABLE public.invites_roles (
"invitedId" integer NOT NULL, "inviteId" integer NOT NULL,
"roleId" integer NOT NULL "roleId" integer NOT NULL
); );
@ -401,7 +401,7 @@ ALTER TABLE ONLY public.invite
-- --
ALTER TABLE ONLY public.invites_roles ALTER TABLE ONLY public.invites_roles
ADD CONSTRAINT invites_roles_pkey PRIMARY KEY ("invitedId", "roleId"); ADD CONSTRAINT invites_roles_pkey PRIMARY KEY ("inviteId", "roleId");
-- --
@ -577,7 +577,7 @@ ALTER TABLE ONLY public.invite
-- --
ALTER TABLE ONLY public.invites_roles ALTER TABLE ONLY public.invites_roles
ADD CONSTRAINT "invites_roles_inviteId_fkey" FOREIGN KEY ("invitedId") REFERENCES public.invite(id) ON UPDATE CASCADE ON DELETE CASCADE; ADD CONSTRAINT "invites_roles_inviteId_fkey" FOREIGN KEY ("inviteId") REFERENCES public.invite(id) ON UPDATE CASCADE ON DELETE CASCADE;
-- --

View File

@ -1,7 +1,7 @@
import _ from 'lodash' import _ from 'lodash'
import env from '../env.ts' import env from '../env.ts'
const domain = 'startbit.bitmill.io' const domain = 'brf.lkm.nu'
type SiteConfig = { type SiteConfig = {
title: string title: string
@ -17,8 +17,8 @@ type SiteConfig = {
} }
const defaults: SiteConfig = { const defaults: SiteConfig = {
title: 'startbit', title: 'BRF',
name: 'startbit,', name: 'brf',
port: null, port: null,
hostname: null, hostname: null,
domain, domain,

View File

@ -53,7 +53,6 @@ export default read(
'SESSION_SECRET', 'SESSION_SECRET',
] as const, ] as const,
{ {
MAILGUN_API_KEY: 'this is not a real key',
PGPASSWORD: null, PGPASSWORD: null,
PGPORT: null, PGPORT: null,
PGUSER: null, PGUSER: null,

View File

@ -1,47 +1,80 @@
// import type { QueryBuilder } from 'knex' import { type SelectQueryBuilder, type ReferenceExpression, type OperandValueExpression, sql } from 'kysely'
import type { SelectQueryBuilder } from 'kysely'
import _ from 'lodash'
// export function convertToReturning(obj: Record<string, string>) { type WhereInput<DB, TB extends keyof DB> = Partial<{
// return _.map(obj, (value, key) => _.isString(value) && `${value} as ${key}`).filter(_.identity) [C in keyof DB[TB]]:
// } | OperandValueExpression<DB, TB, DB[TB][C]>
| readonly OperandValueExpression<DB, TB, DB[TB][C]>[]
| null
}>
// export function columnAs(name: string, table: string, transformer = _.snakeCase) { export function applyWhere<DB, TB extends keyof DB, O>(
// const transformed = transformer(name) builder: SelectQueryBuilder<DB, TB, O>,
where: WhereInput<DB, TB>,
): SelectQueryBuilder<DB, TB, O> {
return Object.entries(where).reduce((builder, [key, value]) => {
const column = key as ReferenceExpression<DB, TB>
// if (transformed !== name) { if (value === null) {
// // knex automagically wraps everything in ", so they are not needed around ${name} return builder.where(column, 'is', null)
// return `${table ? table + '.' : ''}${transformed} as ${name}` } else if (Array.isArray(value)) {
// } return builder.where(column, 'in', value)
}
// return table ? `${table}.${name}` : name return builder.where(column, '=', value)
// } }, builder)
}
// export function columnBuilder(builder: QueryBuilder, columns: Record<string, string>, columnNames: string[]) {
// for (const columnName of columnNames) { interface PaginationInput<DB, TB extends keyof DB> {
// const column = columns[columnName] where: WhereInput<DB, TB>
limit?: number
// if (column) { offset?: number
// // @ts-ignore orderBy?: {
// builder.column(column) column: ReferenceExpression<DB, TB>
// } direction?: 'asc' | 'desc'
// } }
}
// return builder
// } export function applyPagination<DB, TB extends keyof DB, O>(
builder: SelectQueryBuilder<DB, TB, O>,
export function where<DB, TB extends keyof DB, O>(builder: SelectQueryBuilder<DB, TB, O>, json: Record<string, ANY>) { { limit, offset, orderBy }: PaginationInput<DB, TB>,
return _.reduce( ): SelectQueryBuilder<DB, TB, O> {
json, let qb = builder
(builder, value, key) => {
if (value === null) { if (orderBy) {
return builder.where(key, 'is', null) qb = qb.orderBy(orderBy.column, orderBy.direction ?? 'asc')
} else if (Array.isArray(value)) { }
return builder.where(key, 'in', value)
} if (limit !== undefined) {
qb = qb.limit(limit)
return builder.where(key, '=', value) }
},
builder, if (offset !== undefined) {
) qb = qb.offset(offset)
}
return qb
}
export async function paginate<DB, TB extends keyof DB, O>(
baseDataQuery: SelectQueryBuilder<DB, TB, O>,
baseCountQuery: SelectQueryBuilder<DB, TB, unknown>,
input: PaginationInput<DB, TB>,
) {
const dataQuery = applyPagination(input.where ? applyWhere(baseDataQuery, input.where) : baseDataQuery, input)
let countQuery = baseCountQuery.select(sql<number>`count(*)::int`.as('totalCount'))
if (input.where) {
countQuery = applyWhere(countQuery, input.where)
}
const [countRow, items] = await Promise.all([countQuery.executeTakeFirstOrThrow(), dataQuery.execute()])
return {
items,
totalCount: (countRow as { totalCount: number }).totalCount,
limit: input.limit,
offset: input.offset,
count: items.length,
}
} }

View File

@ -1,5 +1,5 @@
import type { RouteHandler } from 'fastify' import type { RouteHandler } from 'fastify'
import { Type, type Static } from '@fastify/type-provider-typebox' import * as z from 'zod'
import config from '../../../config.ts' import config from '../../../config.ts'
import knex from '../../../lib/knex.ts' import knex from '../../../lib/knex.ts'
import StatusError from '../../../lib/status_error.ts' import StatusError from '../../../lib/status_error.ts'
@ -8,10 +8,10 @@ import { hashPassword } from '../helpers.ts'
import { StatusErrorSchema } from '../../../schemas/status_error.ts' import { StatusErrorSchema } from '../../../schemas/status_error.ts'
const { errors, timeouts } = config.auth const { errors, timeouts } = config.auth
const BodySchema = Type.Object({ const BodySchema = z.object({
email: Type.String({ format: 'email' }), email: z.email(),
password: Type.String(), password: z.string(),
token: Type.String(), token: z.string(),
}) })
const ResponseSchema = { const ResponseSchema = {
@ -19,7 +19,7 @@ const ResponseSchema = {
'4XX': StatusErrorSchema, '4XX': StatusErrorSchema,
} }
const changePassword: RouteHandler<{ Body: Static<typeof BodySchema> }> = async function changePassword( const changePassword: RouteHandler<{ Body: z.infer<typeof BodySchema> }> = async function changePassword(
request, request,
reply, reply,
) { ) {

View File

@ -1,6 +1,6 @@
import _ from 'lodash' import _ from 'lodash'
import * as z from 'zod'
import type { RouteHandler } from 'fastify' import type { RouteHandler } from 'fastify'
import { Type, type Static } from '@fastify/type-provider-typebox'
import config from '../../../config.ts' import config from '../../../config.ts'
import UserQueries from '../../../services/users/queries.ts' import UserQueries from '../../../services/users/queries.ts'
import emitter from '../../../lib/emitter.ts' import emitter from '../../../lib/emitter.ts'
@ -12,14 +12,14 @@ import { verifyPassword } from '../helpers.ts'
const userQueries = UserQueries({ knex, emitter }) const userQueries = UserQueries({ knex, emitter })
const { errors, maxLoginAttempts, requireVerification } = config.auth const { errors, maxLoginAttempts, requireVerification } = config.auth
const BodySchema = Type.Object({ const BodySchema = z.object({
email: Type.String(), email: z.string(),
password: Type.String(), password: z.string(),
remember: Type.Optional(Type.Boolean()), remember: z.boolean().optional(),
}) })
const ResponseSchema = { const ResponseSchema = {
200: Type.Object({ 200: z.object({
id: { type: 'integer' }, id: { type: 'integer' },
email: { type: 'string' }, email: { type: 'string' },
password: { type: 'string' }, password: { type: 'string' },
@ -31,7 +31,7 @@ const ResponseSchema = {
'4XX': StatusErrorSchema, '4XX': StatusErrorSchema,
} }
const login: RouteHandler<{ Body: Static<typeof BodySchema> }> = async function (request, reply) { const login: RouteHandler<{ Body: z.infer<typeof BodySchema> }> = async function (request, reply) {
try { try {
const user = await userQueries.findOne({ email: request.body.email }) const user = await userQueries.findOne({ email: request.body.email })

View File

@ -1,7 +1,6 @@
import type { RouteHandler } from 'fastify' import type { FastifySchema, RouteHandler } from 'fastify'
/** @type {import('fastify').FastifySchema} */ const schema: FastifySchema = {
const schema = {
response: { response: {
'3XX': {}, '3XX': {},
}, },

View File

@ -1,5 +1,5 @@
import _ from 'lodash' import _ from 'lodash'
import { Type, type Static } from '@fastify/type-provider-typebox' import * as z from 'zod'
import type { RouteHandler } from 'fastify' import type { RouteHandler } from 'fastify'
import config from '../../../config.ts' import config from '../../../config.ts'
import emitter from '../../../lib/emitter.ts' import emitter from '../../../lib/emitter.ts'
@ -20,11 +20,11 @@ const userQueries = UserQueries({ knex, emitter })
const { errors, timeouts } = config.auth const { errors, timeouts } = config.auth
const BodySchema = Type.Object({ const BodySchema = z.object({
email: Type.String({ format: 'email' }), email: z.email(),
password: Type.String(), password: z.string(),
inviteEmail: Type.Optional(Type.String({ format: 'email' })), inviteEmail: z.email().optional(),
inviteToken: Type.Optional(Type.String()), inviteToken: z.string().optional(),
// required: config.auth.requireInvite ? ['email', 'password', 'inviteEmail', 'inviteToken'] : ['email', 'password'], // required: config.auth.requireInvite ? ['email', 'password', 'inviteEmail', 'inviteToken'] : ['email', 'password'],
}) })
@ -44,7 +44,7 @@ const ResponseSchema = {
'4XX': { $ref: 'status-error' }, '4XX': { $ref: 'status-error' },
} }
const register: RouteHandler<{ Body: Static<typeof BodySchema> }> = async function (request, reply) { const register: RouteHandler<{ Body: z.infer<typeof BodySchema> }> = async function (request, reply) {
try { try {
// TODO validate and ensure same as confirm // TODO validate and ensure same as confirm
const email = request.body.email.trim().toLowerCase() const email = request.body.email.trim().toLowerCase()

View File

@ -1,5 +1,5 @@
import type { RouteHandler } from 'fastify' import type { RouteHandler } from 'fastify'
import { Type, type Static } from '@fastify/type-provider-typebox' import * as z from 'zod'
import config from '../../../config.ts' import config from '../../../config.ts'
import knex from '../../../lib/knex.ts' import knex from '../../../lib/knex.ts'
import sendMail from '../../../lib/send_mail.ts' import sendMail from '../../../lib/send_mail.ts'
@ -11,8 +11,8 @@ import forgotPasswordTemplate from '../../../templates/emails/forgot_password.ts
const { errors } = config.auth const { errors } = config.auth
const BodySchema = Type.Object({ const BodySchema = z.object({
email: Type.String({ format: 'email' }), email: z.email(),
}) })
const ResponseSchema = { const ResponseSchema = {
@ -20,7 +20,7 @@ const ResponseSchema = {
'4XX': StatusErrorSchema, '4XX': StatusErrorSchema,
} }
const forgotPassword: RouteHandler<{ Body: Static<typeof BodySchema> }> = async function (request, reply) { const forgotPassword: RouteHandler<{ Body: z.infer<typeof BodySchema> }> = async function (request, reply) {
const { email } = request.body const { email } = request.body
try { try {

View File

@ -1,15 +1,15 @@
import type { RouteHandler } from 'fastify' import type { RouteHandler } from 'fastify'
import * as z from 'zod'
import config from '../../../config.ts' import config from '../../../config.ts'
import knex from '../../../lib/knex.ts' import knex from '../../../lib/knex.ts'
import StatusError from '../../../lib/status_error.ts' import StatusError from '../../../lib/status_error.ts'
import { StatusErrorSchema } from '../../../schemas/status_error.ts' import { StatusErrorSchema } from '../../../schemas/status_error.ts'
import { Type, type Static } from '@fastify/type-provider-typebox'
const { errors, timeouts } = config.auth const { errors, timeouts } = config.auth
const QuerystringSchema = Type.Object({ const QuerystringSchema = z.object({
email: Type.String({ format: 'email' }), email: z.email(),
token: Type.String(), token: z.string(),
}) })
const ResponseSchema = { const ResponseSchema = {
@ -17,7 +17,7 @@ const ResponseSchema = {
'4XX': StatusErrorSchema, '4XX': StatusErrorSchema,
} }
const verifyEmail: RouteHandler<{ Querystring: Static<typeof QuerystringSchema> }> = async function (request, reply) { const verifyEmail: RouteHandler<{ Querystring: z.infer<typeof QuerystringSchema> }> = async function (request, reply) {
try { try {
const { token, email } = request.query const { token, email } = request.query

View File

@ -2,9 +2,11 @@ import _ from 'lodash'
import { type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox' import { type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox'
import accounts from './api/accounts.ts' import accounts from './api/accounts.ts'
import admissions from './api/admissions.ts'
import balances from './api/balances.ts' import balances from './api/balances.ts'
import entries from './api/entries.ts' import entries from './api/entries.ts'
import financialYears from './api/financial_years.ts' import financialYears from './api/financial_years.ts'
import invites from './api/invites.ts'
import invoices from './api/invoices.ts' import invoices from './api/invoices.ts'
import journals from './api/journals.ts' import journals from './api/journals.ts'
import objects from './api/objects.ts' import objects from './api/objects.ts'
@ -16,9 +18,11 @@ import transactions from './api/transactions.ts'
const apiRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => { const apiRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
fastify.register(accounts, { prefix: '/accounts' }) fastify.register(accounts, { prefix: '/accounts' })
fastify.register(admissions, { prefix: '/admissions' })
fastify.register(balances, { prefix: '/balances' }) fastify.register(balances, { prefix: '/balances' })
fastify.register(entries, { prefix: '/entries' }) fastify.register(entries, { prefix: '/entries' })
fastify.register(financialYears, { prefix: '/financial-years' }) fastify.register(financialYears, { prefix: '/financial-years' })
fastify.register(invites, { prefix: '/invites' })
fastify.register(invoices, { prefix: '/invoices' }) fastify.register(invoices, { prefix: '/invoices' })
fastify.register(journals, { prefix: '/journals' }) fastify.register(journals, { prefix: '/journals' })
fastify.register(objects, { prefix: '/objects' }) fastify.register(objects, { prefix: '/objects' })

View File

@ -1,13 +1,22 @@
import _ from 'lodash' import _ from 'lodash'
import { type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox' import * as z from 'zod'
import knex from '../../lib/knex.ts' import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
import { AccountSchema } from '../../schemas/db.ts'
const accountRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
const { db } = fastify
const accountRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
fastify.route({ fastify.route({
url: '/', url: '/',
method: 'GET', method: 'GET',
schema: {
response: {
200: z.array(AccountSchema),
},
},
handler() { handler() {
return knex('account').select('*').orderBy('number') return db.selectFrom('account').selectAll().orderBy('number').execute()
}, },
}) })

View File

@ -1,30 +1,12 @@
import { Type, type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox' import * as z from 'zod'
import emitter from '../../lib/emitter.ts' import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
import knex from '../../lib/knex.ts' import { sql } from 'kysely'
import Queries from '../../services/admissions/queries.ts' import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres'
import { RoleSchema } from './roles.ts' import { AdmissionSchema, RoleSchema } from '../../schemas/db.ts'
export const AdmissionSchema = Type.Object({ const admissionsPlugin: FastifyPluginCallbackZod = (fastify, _options, done) => {
id: Type.Number(), const { db } = fastify
regex: Type.String(),
roles: Type.Array(RoleSchema),
createdAt: Type.String({ format: 'date-time' }),
createdById: { type: 'integer' },
createdBy: { type: 'object', properties: { id: { type: 'integer' }, email: { type: 'string' } } },
modifiedAt: [Type.String({ format: 'date-time' }), Type.Null()],
modifiedById: [Type.Number, Type.Null()],
})
export const AdmissionVariableSchema = Type.Object({
regex: Type.String(),
roles: Type.Array(Type.Number()),
createdById: Type.Optional(Type.Number()),
modifiedById: Type.Optional(Type.Number()),
})
const admissionsPlugin: FastifyPluginCallbackTypebox = (fastify, _options, done) => {
const queries = Queries({ emitter, knex })
fastify.addHook('onRequest', fastify.auth) fastify.addHook('onRequest', fastify.auth)
@ -33,11 +15,33 @@ const admissionsPlugin: FastifyPluginCallbackTypebox = (fastify, _options, done)
method: 'GET', method: 'GET',
schema: { schema: {
response: { response: {
200: { type: 'array', items: { $ref: 'admission' } }, 200: z.array(
AdmissionSchema.extend({
roles: z.array(RoleSchema.pick({ id: true, name: true })),
}),
),
}, },
}, },
handler(request) { handler() {
return queries.find(request.query) return db
.selectFrom('admission as a')
.selectAll()
.select((eb) => [
jsonObjectFrom(eb.selectFrom('user as u').select(['u.id', 'u.email']).whereRef('u.id', '=', 'a.createdById'))
.$notNull()
.as('createdBy'),
jsonObjectFrom(
eb.selectFrom('user as u').select(['u.id', 'u.email']).whereRef('u.id', '=', 'a.modifiedById'),
).as('modifiedBy'),
jsonArrayFrom(
eb
.selectFrom('role as r')
.innerJoin('admissions_roles as ra', 'ra.roleId', 'r.id')
.select(['r.id', 'r.name'])
.whereRef('ra.admissionId', '=', 'a.id'),
).as('roles'),
])
.execute()
}, },
}) })
@ -45,24 +49,39 @@ const admissionsPlugin: FastifyPluginCallbackTypebox = (fastify, _options, done)
url: '/', url: '/',
method: 'POST', method: 'POST',
schema: { schema: {
body: AdmissionVariableSchema, body: AdmissionSchema.pick({ regex: true }).extend({
roles: z.array(z.coerce.number()),
}),
response: { response: {
201: AdmissionSchema, 201: AdmissionSchema,
}, },
}, },
async handler(request, reply) { async handler(request) {
let body = request.body const { roles, ...admissionProps } = request.body
if (request.session?.userId) { const trx = await db.startTransaction().execute()
body = {
...request.body, const admission = await trx
createdById: request.session.userId, .insertInto('admission')
} .values({ ...admissionProps, createdById: request.session.userId })
.returningAll()
.executeTakeFirstOrThrow()
await trx
.insertInto('admissions_roles')
.values(roles.map((roleId) => ({ admissionId: admission.id, roleId })))
.execute()
await trx.commit().execute()
return {
...admission,
roles: await db
.selectFrom('role as r')
.innerJoin('admissions_roles as ar', 'ar.roleId', 'r.id')
.select(['r.id', 'r.name'])
.execute(),
} }
return queries.create(body).then((row) => {
return reply.header('Location', `${request.url}/${row.id}`).status(201).send(row)
})
}, },
}) })
@ -70,16 +89,18 @@ const admissionsPlugin: FastifyPluginCallbackTypebox = (fastify, _options, done)
url: '/:id', url: '/:id',
method: 'DELETE', method: 'DELETE',
schema: { schema: {
params: Type.Object({ params: z.object({
id: Type.Number(), id: z.number(),
}), }),
response: { response: {
204: {}, 204: {},
404: {}, 404: {},
}, },
}, },
handler(request, reply) { handler() {
return queries.removeById(request.params.id).then((count) => reply.status(count > 0 ? 204 : 404).send()) return {}
// return queries.removeById(request.params.id).then((count: number) => reply.status(count > 0 ? 204 : 404).send())
}, },
}) })
@ -87,25 +108,50 @@ const admissionsPlugin: FastifyPluginCallbackTypebox = (fastify, _options, done)
url: '/:id', url: '/:id',
method: 'PATCH', method: 'PATCH',
schema: { schema: {
params: Type.Object({ params: z.object({
id: Type.Number(), id: z.coerce.number(),
}),
body: AdmissionSchema.pick({ regex: true }).extend({
roles: z.array(z.coerce.number()),
}), }),
body: AdmissionVariableSchema,
response: { response: {
204: {}, 204: {},
}, },
}, },
async handler(request) { async handler(request) {
let body = request.body const { roles, ...admissionProps } = request.body
const trx = await db.startTransaction().execute()
const admissionId = request.params.id
if (request.session.userId) { const [updatedAdmission] = await Promise.all([
body = { trx
...request.body, .updateTable('admission')
modifiedById: request.session.userId, .set({ ...admissionProps, modifiedById: request.session.userId })
} .where('id', '=', request.params.id)
.returningAll()
.execute(),
trx
.deleteFrom('admissions_roles')
.where('admissionId', '=', request.params.id)
.where('roleId', 'not in', roles)
.execute(),
sql`INSERT INTO admissions_roles("admissionId", "roleId")
SELECT ${admissionId}, "roleIds" FROM unnest(${roles}::int[]) AS "roleIds" WHERE NOT EXISTS
(SELECT 1 FROM admissions_roles WHERE "admissionId" = ${request.params.id} AND "roleId" = "roleIds")`.execute(
trx,
),
])
await trx.commit().execute()
return {
...updatedAdmission,
roles: await db
.selectFrom('role as r')
.innerJoin('admissions_roles as ar', 'ar.roleId', 'r.id')
.select(['r.id', 'r.name'])
.execute(),
} }
return queries.update(request.params.id, body)
}, },
}) })

View File

@ -1,8 +1,8 @@
import _ from 'lodash' import _ from 'lodash'
import { type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox' import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
import knex from '../../lib/knex.ts' import knex from '../../lib/knex.ts'
const balanceRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => { const balanceRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
fastify.route({ fastify.route({
url: '/', url: '/',
method: 'GET', method: 'GET',

View File

@ -1,15 +1,16 @@
import _ from 'lodash' import _ from 'lodash'
import { Type, type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox' import * as z from 'zod'
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
import knex from '../../lib/knex.ts' import knex from '../../lib/knex.ts'
const entryRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => { const entryRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
fastify.route({ fastify.route({
url: '/', url: '/',
method: 'GET', method: 'GET',
schema: { schema: {
querystring: Type.Object({ querystring: z.object({
journal: Type.String(), journal: z.string(),
year: Type.Number(), year: z.coerce.number(),
}), }),
}, },
async handler(req) { async handler(req) {
@ -35,8 +36,8 @@ const entryRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
url: '/:id', url: '/:id',
method: 'GET', method: 'GET',
schema: { schema: {
params: Type.Object({ params: z.object({
id: Type.Number(), id: z.number(),
}), }),
}, },
async handler(req) { async handler(req) {

View File

@ -1,8 +1,8 @@
import _ from 'lodash' import _ from 'lodash'
import { type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox' import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
import knex from '../../lib/knex.ts' import knex from '../../lib/knex.ts'
const financialYearRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => { const financialYearRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
fastify.route({ fastify.route({
url: '/', url: '/',
method: 'GET', method: 'GET',

View File

@ -1,42 +1,44 @@
import { Type, type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox' import * as z from 'zod'
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
import { jsonArrayFrom } from 'kysely/helpers/postgres'
import config from '../../config.ts' import config from '../../config.ts'
import knex from '../../lib/knex.ts'
import emitter from '../../lib/emitter.ts'
import sendMail from '../../lib/send_mail.ts' import sendMail from '../../lib/send_mail.ts'
import Queries from '../../services/invites/queries.ts' import { generateToken } from '../../plugins/auth/helpers.ts'
import inviteEmailTemplate from '../../templates/emails/invite.ts' import inviteEmailTemplate from '../../templates/emails/invite.ts'
import { RoleSchema } from './roles.ts' import { InviteSchema, RoleSchema } from '../../schemas/db.ts'
export const InviteSchema = Type.Object({ const invitesPlugin: FastifyPluginCallbackZod = (fastify, _option, done) => {
id: Type.Number(), const { db } = fastify
email: Type.String({ format: 'email' }),
token: Type.String(),
roles: Type.Array(RoleSchema),
createdAt: Type.String(),
createdById: Type.Number(),
createdBy: Type.Object({}),
consumedAt: [Type.String(), Type.Null()],
consumedById: [Type.Number(), Type.Null()],
consumedBy: [Type.Object({}), Type.Null()],
})
const invitesPlugin: FastifyPluginCallbackTypebox = (fastify, _option, done) => { // fastify.addHook('onRequest', fastify.auth)
const queries = Queries({ emitter, knex })
fastify.addHook('onRequest', fastify.auth)
fastify.route({ fastify.route({
url: '/', url: '/',
method: 'GET', method: 'GET',
schema: { schema: {
querystring: InviteSchema,
response: { response: {
200: InviteSchema, 200: z.array(
InviteSchema.extend({
roles: z.array(RoleSchema.pick({ id: true, name: true })),
}),
),
}, },
}, },
handler(request) { handler() {
return queries.find(request.query) return db
.selectFrom('invite as i')
.selectAll()
.select((eb) => [
jsonArrayFrom(
eb
.selectFrom('role as r')
.innerJoin('invites_roles as ir', 'ir.roleId', 'r.id')
.select(['r.id', 'r.name'])
.whereRef('r.id', '=', 'ir.roleId'),
).as('roles'),
])
.execute()
}, },
}) })
@ -44,31 +46,44 @@ const invitesPlugin: FastifyPluginCallbackTypebox = (fastify, _option, done) =>
url: '/', url: '/',
method: 'POST', method: 'POST',
schema: { schema: {
body: { body: z.object({
type: Type.Object({ email: z.email(),
email: Type.String({ format: 'email' }), roles: z.array(z.coerce.number()),
roles: Type.Array(Type.Number()), }),
response: {
201: InviteSchema.extend({
roles: z.array(RoleSchema.pick({ id: true, name: true })),
}), }),
properties: {
email: { type: 'string' },
roles: { type: 'array', items: { type: 'integer' } },
},
required: ['email', 'roles'],
}, },
response: { 201: InviteSchema },
}, },
async handler(request, reply) { async handler(request, reply) {
const trx = await knex.transaction() const trx = await db.startTransaction().execute()
const invite = await queries.create( const token = generateToken()
{
...request.body, const invite = await trx
createdById: request.session.userId, .insertInto('invite')
}, .values({ email: request.body.email, token, createdById: request.session.userId })
trx, .returningAll()
) .executeTakeFirstOrThrow()
await trx
.insertInto('invites_roles')
.values(
request.body.roles.map((roleId) => ({
inviteId: invite.id,
roleId,
})),
)
.execute()
try { try {
const roles = await trx
.selectFrom('role as r')
.select(['id', 'name'])
.innerJoin('invites_roles as ir', 'ir.roleId', 'r.id')
.where('ir.inviteId', '=', invite.id)
.execute()
const link = `${new URL(config.auth.paths.register, config.site.url)}?email=${invite.email}&token=${invite.token}` const link = `${new URL(config.auth.paths.register, config.site.url)}?email=${invite.email}&token=${invite.token}`
await sendMail({ await sendMail({
@ -77,11 +92,17 @@ const invitesPlugin: FastifyPluginCallbackTypebox = (fastify, _option, done) =>
html: await inviteEmailTemplate({ link }).text(), html: await inviteEmailTemplate({ link }).text(),
}) })
await trx.commit() await trx.commit().execute()
return reply.header('Location', `${request.url}/${invite.id}`).status(201).send(invite) return reply
.header('Location', `${request.url}/${invite.id}`)
.status(201)
.send({
...invite,
roles,
})
} catch (err) { } catch (err) {
await trx.rollback() await trx.rollback().execute()
throw err throw err
} }
@ -92,16 +113,18 @@ const invitesPlugin: FastifyPluginCallbackTypebox = (fastify, _option, done) =>
url: '/:id', url: '/:id',
method: 'DELETE', method: 'DELETE',
schema: { schema: {
params: Type.Object({ params: z.object({
id: Type.Number(), id: z.number(),
}), }),
response: { response: {
204: {}, 204: {},
404: {}, 404: {},
}, },
}, },
handler(request, reply) { async handler(request, reply) {
return queries.removeById(request.params.id).then((count) => reply.status(count > 0 ? 204 : 404).send()) const result = await db.deleteFrom('invite').where('id', '=', request.params.id).executeTakeFirstOrThrow()
return reply.status(result.numDeletedRows > 0 ? 204 : 404).send()
}, },
}) })

View File

@ -1,16 +1,17 @@
import _ from 'lodash' import _ from 'lodash'
import { Type, type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox' import * as z from 'zod'
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
import knex from '../../lib/knex.ts' import knex from '../../lib/knex.ts'
import StatusError from '../../lib/status_error.ts' import StatusError from '../../lib/status_error.ts'
const invoiceRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => { const invoiceRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
fastify.route({ fastify.route({
url: '/', url: '/',
method: 'GET', method: 'GET',
schema: { schema: {
querystring: Type.Object({ querystring: z.object({
year: Type.Optional(Type.Number()), year: z.number().optional(),
supplier: Type.Optional(Type.Number()), supplier: z.number().optional(),
}), }),
}, },
async handler(req) { async handler(req) {
@ -53,9 +54,9 @@ const invoiceRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
url: '/total-amount', url: '/total-amount',
method: 'GET', method: 'GET',
schema: { schema: {
querystring: Type.Object({ querystring: z.object({
year: Type.Optional(Type.Number()), year: z.number().optional(),
supplier: Type.Optional(Type.Number()), supplier: z.number().optional(),
}), }),
}, },
async handler(req) { async handler(req) {
@ -80,8 +81,8 @@ const invoiceRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
url: '/:id', url: '/:id',
method: 'GET', method: 'GET',
schema: { schema: {
params: Type.Object({ params: z.object({
id: Type.Number(), id: z.number(),
}), }),
}, },
handler(req) { handler(req) {
@ -110,8 +111,8 @@ const invoiceRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
url: '/by-supplier/:supplier', url: '/by-supplier/:supplier',
method: 'GET', method: 'GET',
schema: { schema: {
params: Type.Object({ params: z.object({
supplier: Type.Number(), supplier: z.number(),
}), }),
}, },
handler(req) { handler(req) {
@ -136,8 +137,8 @@ const invoiceRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
url: '/by-year/:year', url: '/by-year/:year',
method: 'GET', method: 'GET',
schema: { schema: {
params: Type.Object({ params: z.object({
year: Type.Number(), year: z.number(),
}), }),
}, },
async handler(req) { async handler(req) {

View File

@ -1,8 +1,8 @@
import _ from 'lodash' import _ from 'lodash'
import { type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox' import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
import knex from '../../lib/knex.ts' import knex from '../../lib/knex.ts'
const journalRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => { const journalRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
fastify.route({ fastify.route({
url: '/', url: '/',
method: 'GET', method: 'GET',

View File

@ -1,8 +1,9 @@
import _ from 'lodash' import _ from 'lodash'
import { Type, type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox' import * as z from 'zod'
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
import knex from '../../lib/knex.ts' import knex from '../../lib/knex.ts'
const objectRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => { const objectRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
fastify.route({ fastify.route({
url: '/', url: '/',
method: 'GET', method: 'GET',
@ -19,8 +20,8 @@ const objectRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
url: '/:id', url: '/:id',
method: 'GET', method: 'GET',
schema: { schema: {
params: Type.Object({ params: z.object({
id: Type.Number(), id: z.number(),
}), }),
}, },
async handler(req) { async handler(req) {

View File

@ -1,55 +1,56 @@
import process from 'node:process' import process from 'node:process'
import os from 'node:os' import os from 'node:os'
import * as z from 'zod'
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
import env from '../../env.ts' import env from '../../env.ts'
import { Type, type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox'
const ProcessSchema = Type.Object({ const ProcessSchema = z.object({
env: Type.String(), env: z.string(),
version: Type.String(), version: z.string(),
uptime: Type.Number(), uptime: z.number(),
memoryUsage: Type.Object({ memoryUsage: z.object({
rss: Type.Number(), rss: z.number(),
heapTotal: Type.Number(), heapTotal: z.number(),
heapUsed: Type.Number(), heapUsed: z.number(),
external: Type.Number(), external: z.number(),
arrayBuffers: Type.Number(), arrayBuffers: z.number(),
}), }),
resourceUsage: Type.Object({ resourceUsage: z.object({
userCPUTime: Type.Number(), userCPUTime: z.number(),
systemCPUTime: Type.Number(), systemCPUTime: z.number(),
maxRSS: Type.Number(), maxRSS: z.number(),
sharedMemorySize: Type.Number(), sharedMemorySize: z.number(),
unsharedDataSize: Type.Number(), unsharedDataSize: z.number(),
unsharedStackSize: Type.Number(), unsharedStackSize: z.number(),
minorPageFault: Type.Number(), minorPageFault: z.number(),
majorPageFault: Type.Number(), majorPageFault: z.number(),
swappedOut: Type.Number(), swappedOut: z.number(),
fsRead: Type.Number(), fsRead: z.number(),
fsWrite: Type.Number(), fsWrite: z.number(),
ipcSent: Type.Number(), ipcSent: z.number(),
ipcReceived: Type.Number(), ipcReceived: z.number(),
signalsCount: Type.Number(), signalsCount: z.number(),
voluntaryContextSwitches: Type.Number(), voluntaryContextSwitches: z.number(),
involuntaryContextSwitches: Type.Number(), involuntaryContextSwitches: z.number(),
}), }),
}) })
const OSSchema = Type.Object({ const OSSchema = z.object({
freemem: Type.Number(), freemem: z.number(),
totalmem: Type.Number(), totalmem: z.number(),
arch: Type.String(), arch: z.string(),
homedir: Type.String(), homedir: z.string(),
hostname: Type.String(), hostname: z.string(),
loadavg: Type.Array(Type.Number()), loadavg: z.array(z.number()),
machine: Type.String(), machine: z.string(),
platform: Type.String(), platform: z.string(),
release: Type.String(), release: z.string(),
type: Type.String(), type: z.string(),
uptime: Type.Number(), uptime: z.number(),
version: Type.String(), version: z.string(),
}) })
const processPlugin: FastifyPluginCallbackTypebox = (fastify, _options, done) => { const processPlugin: FastifyPluginCallbackZod = (fastify, _options, done) => {
fastify.addHook('onRequest', fastify.auth) fastify.addHook('onRequest', fastify.auth)
fastify.route({ fastify.route({
@ -57,7 +58,7 @@ const processPlugin: FastifyPluginCallbackTypebox = (fastify, _options, done) =>
url: '/', url: '/',
schema: { schema: {
response: { response: {
200: Type.Object({ 200: z.object({
process: ProcessSchema, process: ProcessSchema,
os: OSSchema, os: OSSchema,
}), }),

View File

@ -1,8 +1,9 @@
import _ from 'lodash' import _ from 'lodash'
import { Type, type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox' import * as z from 'zod'
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
import knex from '../../lib/knex.ts' import knex from '../../lib/knex.ts'
const resultRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => { const resultRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
fastify.route({ fastify.route({
url: '/', url: '/',
method: 'GET', method: 'GET',
@ -69,8 +70,8 @@ const resultRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
url: '/:year', url: '/:year',
method: 'GET', method: 'GET',
schema: { schema: {
params: Type.Object({ params: z.object({
year: Type.Number(), year: z.number(),
}), }),
}, },
async handler(req) { async handler(req) {

View File

@ -1,11 +0,0 @@
// import { test, type TestContext } from 'node:test'
// import Server from '../../server.ts'
// import rolesPlugin from './roles.ts'
// import type { FastifyInstance } from 'fastify'
// let server: FastifyInstance
// test('roles', (t: TestContext) => {
// t.beforeEach(() => {
// server = Server()
// })
// })

View File

@ -1,151 +1,114 @@
import _ from 'lodash' import _ from 'lodash'
import * as z from 'zod' import * as z from 'zod'
import { type FastifyPluginCallbackZod } from 'fastify-type-provider-zod' import { type FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
// import knex from '../../lib/knex.ts'
// import Queries from '../../services/roles/queries.ts'
import { RoleSchema } from '../../schemas/db.ts' import { RoleSchema } from '../../schemas/db.ts'
import { jsonObjectFrom } from 'kysely/helpers/postgres'
// export const RoleFullSchema = Type.Object({
// id: Type.Number(),
// name: Type.String(),
// createdAt: Type.String(),
// createdById: Type.Number(),
// modifiedAt: Type.String(),
// modifiedById: Type.Number(),
// })
// console.log(RoleFullSchema)
// export const RoleSchema = Type.Pick(RoleFullSchema, ['id', 'name'])
// export const RoleVariableSchema = Type.Pick(RoleFullSchema, ['name', 'createdById'])
const rolesPlugin: FastifyPluginCallbackZod = (fastify, _options, done) => { const rolesPlugin: FastifyPluginCallbackZod = (fastify, _options, done) => {
// const queries = Queries({ knex })
const { db } = fastify const { db } = fastify
// fastify.addHook('onRequest', fastify.auth) fastify.addHook('onRequest', fastify.auth)
fastify.route({ fastify.route({
url: '/', url: '/',
method: 'GET', method: 'GET',
schema: { schema: {
querystring: RoleSchema.partial().extend({
limit: z.number().optional(),
sort: z.keyof(RoleSchema).optional(),
offset: z.number().optional(),
}),
response: { response: {
200: z.array(RoleSchema), 200: z.array(
RoleSchema.extend({
createdBy: z.object({
id: z.number(),
email: z.string(),
}),
modifiedBy: z
.object({
id: z.number(),
email: z.string(),
})
.nullable(),
}),
),
}, },
}, },
handler(request) { handler() {
// if (!client) { return db
// // TODO figure out if better, eg instanceof, check is possible .selectFrom('role as r')
// if (select && select.andWhereNotBetween) { .selectAll()
// client = select .select((eb) => [
// select = null jsonObjectFrom(eb.selectFrom('user as u').select(['u.id', 'u.email']).whereRef('u.id', '=', 'r.createdById'))
// } else { .$notNull()
// client = kysely .as('createdBy'),
// } jsonObjectFrom(
// } eb.selectFrom('user as u').select(['u.id', 'u.email']).whereRef('u.id', '=', 'r.modifiedById'),
).as('modifiedBy'),
const { offset, limit, sort, ...query } = request.query ])
.execute()
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({ fastify.route({
// url: '/', url: '/',
// method: 'POST', method: 'POST',
// schema: { schema: {
// body: RoleVariableSchema, body: RoleSchema.pick({ name: true }),
// response: { response: {
// 201: RoleFullSchema, 201: RoleSchema,
// }, },
// }, },
// async handler(request, reply) { handler(request) {
// const newRole = request.session.userId return db
// ? { .insertInto('role')
// ...request.body, .values({
// createdById: request.session.userId, ...request.body,
// } createdById: request.session.userId,
// : request.body })
.returningAll()
.executeTakeFirstOrThrow()
},
})
// return queries.create(newRole).then((row) => { fastify.route({
// return reply.header('Location', `${request.url}/${row.id}`).status(201).send(row) url: '/:id',
// }) method: 'DELETE',
// }, schema: {
// }) params: z.object({
id: z.number(),
}),
response: {
204: {},
404: {},
},
},
handler(request) {
return db.deleteFrom('role').where('id', '=', request.params.id).execute()
},
})
// fastify.route({ fastify.route({
// url: '/:id', url: '/:id',
// method: 'DELETE', method: 'PATCH',
// schema: { schema: {
// params: Type.Object({ params: z.object({
// id: Type.Number(), id: z.number(),
// }), }),
// response: { body: RoleSchema.pick({ name: true }),
// 204: {}, response: {
// 404: {}, 204: {},
// }, },
// }, },
// handler(request, reply) { async handler(request) {
// return queries.removeById(request.params.id).then((count) => reply.status(count > 0 ? 204 : 404).send()) return db
// }, .updateTable('role')
// }) .set({
...request.body,
// fastify.route({ modifiedById: request.session.userId,
// url: '/:id', })
// method: 'PATCH', .where('id', '=', request.params.id)
// schema: { .returningAll()
// params: Type.Object({ .execute()
// 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)
// },
// })
done() done()
} }

View File

@ -1,6 +1,6 @@
import _ from 'lodash' import _ from 'lodash'
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
import z from 'zod' import z from 'zod'
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
import { SupplierSchema } from '../../schemas/db.ts' import { SupplierSchema } from '../../schemas/db.ts'
const supplierRoutes: FastifyPluginCallbackZod = (fastify, _, done) => { const supplierRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {

View File

@ -1,7 +1,7 @@
import _ from 'lodash' import _ from 'lodash'
import * as z from 'zod' import * as z from 'zod'
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod' import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
import StatusError from '../../lib/status_error.ts' import { paginate } from '../../lib/kysely_helpers.ts'
const transactionRoutes: FastifyPluginCallbackZod = (fastify, _, done) => { const transactionRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
const { db } = fastify const { db } = fastify
@ -13,31 +13,21 @@ const transactionRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
querystring: z.object({ querystring: z.object({
year: z.optional(z.coerce.number()), year: z.optional(z.coerce.number()),
accountNumber: 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(),
}), }),
}, },
async handler(req) { async handler(request) {
const query: { financialYearId?: number; accountNumber?: number } = {} const { offset, limit, sort: _sort, ...where } = request.query
if (req.query.year) { const baseQuery = db
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
}
if (req.query.accountNumber) {
query.accountNumber = req.query.accountNumber
}
return db
.selectFrom('transaction as t') .selectFrom('transaction as t')
.innerJoin('entry as e', 't.entryId', 'e.id') .innerJoin('entry as e', 't.entryId', 'e.id')
.select([ .innerJoin('financialYear as fy', 'e.financialYearId', 'fy.id')
const result = await paginate(
baseQuery.select([
't.accountNumber', 't.accountNumber',
'e.transactionDate', 'e.transactionDate',
't.entryId', 't.entryId',
@ -45,9 +35,17 @@ const transactionRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
't.description', 't.description',
't.invoiceId', 't.invoiceId',
'e.description as entryDescription', 'e.description as entryDescription',
]) ]),
.where((eb) => eb.and(query)) baseQuery,
.execute() {
// @ts-ignore
where,
limit,
offset,
},
)
return result
}, },
}) })

View File

@ -1,9 +1,9 @@
import { type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox' import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
import knex from '../../lib/knex.ts' import knex from '../../lib/knex.ts'
import emitter from '../../lib/emitter.ts' import emitter from '../../lib/emitter.ts'
import Queries from '../../services/users/queries.ts' import Queries from '../../services/users/queries.ts'
const usersPlugin: FastifyPluginCallbackTypebox<{ addParentSchema: (schema: ANY) => void }> = ( const usersPlugin: FastifyPluginCallbackZod<{ addParentSchema: (schema: ANY) => void }> = (
fastify, fastify,
{ addParentSchema }, { addParentSchema },
done, done,

View File

@ -1,11 +1,62 @@
import { z } from 'zod' import { z } from 'zod'
export const AccountSchema = z.object({
id: z.number().int().optional(),
number: z.int(),
financialYearId: z.number().int(),
description: z.string(),
sru: z.int().nullable().optional(),
})
export const AdmissionSchema = z.object({
id: z.number().int().optional(),
regex: z.string(),
createdAt: z.date().nullable().optional(),
createdById: z.number().int(),
modifiedAt: z.date().nullable().optional(),
modifiedById: z.number().int().nullable().optional(),
})
export const AccountBalanceSchema = z.object({
accountNumber: z.number().int(),
financialYearId: z.number().int(),
in: z.number().optional(),
out: z.number().optional(),
inQuantity: z.number().int().nullable().optional(),
outQuantity: z.number().int().nullable().optional(),
})
export const InviteSchema = z.object({
id: z.number().int().optional(),
email: z.string(),
token: z.string(),
createdAt: z.date().optional(),
createdById: z.number().int().nullable().optional(),
modifiedAt: z.date().nullable().optional(),
modifiedById: z.number().int().nullable().optional(),
consumedAt: z.date().nullable().optional(),
consumedById: z.number().int().nullable().optional(),
})
export const InvoiceSchema = z.object({
id: z.number().int().optional(),
financialYearId: z.number().int().nullable().optional(),
supplierId: z.number().int(),
fiskenNumber: z.number().int().nullable().optional(),
phmNumber: z.number().int().nullable().optional(),
invoiceNumber: z.string().nullable().optional(),
invoiceDate: z.string().nullable().optional(),
dueDate: z.string().nullable().optional(),
ocr: z.string().nullable().optional(),
amount: z.number().nullable().optional(),
})
export const RoleSchema = z.object({ export const RoleSchema = z.object({
id: z.number().int().optional(), id: z.number().int().optional(),
name: z.string(), name: z.string(),
createdAt: z.string().optional(), createdAt: z.date().optional(),
createdById: z.number().int().nullable().optional(), createdById: z.number().int().nullable().optional(),
modifiedAt: z.string().nullable().optional(), modifiedAt: z.date().nullable().optional(),
modifiedById: z.number().int().nullable().optional(), modifiedById: z.number().int().nullable().optional(),
}) })

View File

@ -0,0 +1,27 @@
import { test, type TestContext } from 'node:test'
import { serializerCompiler, validatorCompiler } from 'fastify-type-provider-zod'
import admissionPlugin from '../routes/api/admissions.ts'
import fastify from 'fastify'
test('/api/admissions', async (t: TestContext) => {
const server = fastify()
server.setValidatorCompiler(validatorCompiler)
server.setSerializerCompiler(serializerCompiler)
server.decorate('auth', (_request, _reply, done) => done())
server.register(admissionPlugin, { prefix: '/api/admissions' })
const res = await server.inject({
method: 'GET',
url: '/api/admissions',
})
t.assert.equal(res.statusCode, 200)
await server.close()
// TODO verify that roles are inserted and deleted on PATCH
})

2
shared/global.d.ts vendored
View File

@ -23,7 +23,7 @@ declare module 'fastify' {
} }
interface FastifyRequest { interface FastifyRequest {
logout: () => void logout: () => Promise<void>
login: (user: ANY) => Promise<void> login: (user: ANY) => Promise<void>
user: Promise<ANY> user: Promise<ANY>
getUser: () => Promise<ANY> getUser: () => Promise<ANY>

View File

@ -133,7 +133,7 @@ export interface Invite {
} }
export interface InvitesRoles { export interface InvitesRoles {
invitedId: number inviteId: number
roleId: number roleId: number
} }