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 {
name: Admissions
name: /api/admissions
type: http
seq: 1
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
meta {
name: Users
name: /api/users
type: http
seq: 9
seq: 19
}
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()
try {
const form = e.currentTarget
const result = await rek[admission ? 'patch' : 'post'](
`/api/admissions${admission ? '/' + admission.id : ''}`,
serializeForm(e.currentTarget),
serializeForm(form),
)
actions.success()
@ -35,7 +37,7 @@ const AdmissionForm: FunctionComponent<{
if (admission) {
onUpdate?.(result)
} else {
e.currentTarget.reset()
form.reset()
onCreate?.(result)
}
} 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()
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()
onCreate?.(result)
} catch (err) {

View File

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

View File

@ -1,7 +1,6 @@
import { h } from 'preact'
import PageHeader from './page_header.tsx'
import Row from './row.tsx'
import GitLog from './git_log.tsx'
import Process from './process.tsx'
import Section from './section.tsx'
@ -16,13 +15,6 @@ const StartPage = () => (
</Section.Body>
</Section>
<Section>
<Section.Heading>Latest Commits</Section.Heading>
<Section.Body>
<GitLog />
</Section.Body>
</Section>
<Row>
<Section>
<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 (
"invitedId" integer NOT NULL,
"inviteId" integer NOT NULL,
"roleId" integer NOT NULL
);
@ -401,7 +401,7 @@ ALTER TABLE ONLY public.invite
--
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
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 env from '../env.ts'
const domain = 'startbit.bitmill.io'
const domain = 'brf.lkm.nu'
type SiteConfig = {
title: string
@ -17,8 +17,8 @@ type SiteConfig = {
}
const defaults: SiteConfig = {
title: 'startbit',
name: 'startbit,',
title: 'BRF',
name: 'brf',
port: null,
hostname: null,
domain,

View File

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

View File

@ -1,47 +1,80 @@
// import type { QueryBuilder } from 'knex'
import type { SelectQueryBuilder } from 'kysely'
import _ from 'lodash'
import { type SelectQueryBuilder, type ReferenceExpression, type OperandValueExpression, sql } from 'kysely'
// export function convertToReturning(obj: Record<string, string>) {
// return _.map(obj, (value, key) => _.isString(value) && `${value} as ${key}`).filter(_.identity)
// }
type WhereInput<DB, TB extends keyof DB> = Partial<{
[C in keyof DB[TB]]:
| OperandValueExpression<DB, TB, DB[TB][C]>
| readonly OperandValueExpression<DB, TB, DB[TB][C]>[]
| null
}>
// export function columnAs(name: string, table: string, transformer = _.snakeCase) {
// const transformed = transformer(name)
export function applyWhere<DB, TB extends keyof DB, O>(
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) {
// // 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)
return builder.where(column, 'is', null)
} else if (Array.isArray(value)) {
return builder.where(key, 'in', value)
return builder.where(column, 'in', value)
}
return builder.where(key, '=', value)
},
builder,
)
return builder.where(column, '=', value)
}, builder)
}
interface PaginationInput<DB, TB extends keyof DB> {
where: WhereInput<DB, TB>
limit?: number
offset?: number
orderBy?: {
column: ReferenceExpression<DB, TB>
direction?: 'asc' | 'desc'
}
}
export function applyPagination<DB, TB extends keyof DB, O>(
builder: SelectQueryBuilder<DB, TB, O>,
{ limit, offset, orderBy }: PaginationInput<DB, TB>,
): SelectQueryBuilder<DB, TB, O> {
let qb = builder
if (orderBy) {
qb = qb.orderBy(orderBy.column, orderBy.direction ?? 'asc')
}
if (limit !== undefined) {
qb = qb.limit(limit)
}
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, type Static } from '@fastify/type-provider-typebox'
import * as z from 'zod'
import config from '../../../config.ts'
import knex from '../../../lib/knex.ts'
import StatusError from '../../../lib/status_error.ts'
@ -8,10 +8,10 @@ import { hashPassword } from '../helpers.ts'
import { StatusErrorSchema } from '../../../schemas/status_error.ts'
const { errors, timeouts } = config.auth
const BodySchema = Type.Object({
email: Type.String({ format: 'email' }),
password: Type.String(),
token: Type.String(),
const BodySchema = z.object({
email: z.email(),
password: z.string(),
token: z.string(),
})
const ResponseSchema = {
@ -19,7 +19,7 @@ const ResponseSchema = {
'4XX': StatusErrorSchema,
}
const changePassword: RouteHandler<{ Body: Static<typeof BodySchema> }> = async function changePassword(
const changePassword: RouteHandler<{ Body: z.infer<typeof BodySchema> }> = async function changePassword(
request,
reply,
) {

View File

@ -1,6 +1,6 @@
import _ from 'lodash'
import * as z from 'zod'
import type { RouteHandler } from 'fastify'
import { Type, type Static } from '@fastify/type-provider-typebox'
import config from '../../../config.ts'
import UserQueries from '../../../services/users/queries.ts'
import emitter from '../../../lib/emitter.ts'
@ -12,14 +12,14 @@ import { verifyPassword } from '../helpers.ts'
const userQueries = UserQueries({ knex, emitter })
const { errors, maxLoginAttempts, requireVerification } = config.auth
const BodySchema = Type.Object({
email: Type.String(),
password: Type.String(),
remember: Type.Optional(Type.Boolean()),
const BodySchema = z.object({
email: z.string(),
password: z.string(),
remember: z.boolean().optional(),
})
const ResponseSchema = {
200: Type.Object({
200: z.object({
id: { type: 'integer' },
email: { type: 'string' },
password: { type: 'string' },
@ -31,7 +31,7 @@ const ResponseSchema = {
'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 {
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 = {
const schema: FastifySchema = {
response: {
'3XX': {},
},

View File

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

View File

@ -1,5 +1,5 @@
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 knex from '../../../lib/knex.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 BodySchema = Type.Object({
email: Type.String({ format: 'email' }),
const BodySchema = z.object({
email: z.email(),
})
const ResponseSchema = {
@ -20,7 +20,7 @@ const ResponseSchema = {
'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
try {

View File

@ -1,15 +1,15 @@
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 { Type, type Static } from '@fastify/type-provider-typebox'
const { errors, timeouts } = config.auth
const QuerystringSchema = Type.Object({
email: Type.String({ format: 'email' }),
token: Type.String(),
const QuerystringSchema = z.object({
email: z.email(),
token: z.string(),
})
const ResponseSchema = {
@ -17,7 +17,7 @@ const ResponseSchema = {
'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 {
const { token, email } = request.query

View File

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

View File

@ -1,13 +1,22 @@
import _ from 'lodash'
import { 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 { AccountSchema } from '../../schemas/db.ts'
const accountRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
const { db } = fastify
const accountRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
fastify.route({
url: '/',
method: 'GET',
schema: {
response: {
200: z.array(AccountSchema),
},
},
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 emitter from '../../lib/emitter.ts'
import knex from '../../lib/knex.ts'
import Queries from '../../services/admissions/queries.ts'
import * as z from 'zod'
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
import { sql } from 'kysely'
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres'
import { RoleSchema } from './roles.ts'
import { AdmissionSchema, RoleSchema } from '../../schemas/db.ts'
export const AdmissionSchema = Type.Object({
id: Type.Number(),
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 })
const admissionsPlugin: FastifyPluginCallbackZod = (fastify, _options, done) => {
const { db } = fastify
fastify.addHook('onRequest', fastify.auth)
@ -33,11 +15,33 @@ const admissionsPlugin: FastifyPluginCallbackTypebox = (fastify, _options, done)
method: 'GET',
schema: {
response: {
200: { type: 'array', items: { $ref: 'admission' } },
200: z.array(
AdmissionSchema.extend({
roles: z.array(RoleSchema.pick({ id: true, name: true })),
}),
),
},
},
handler(request) {
return queries.find(request.query)
handler() {
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: '/',
method: 'POST',
schema: {
body: AdmissionVariableSchema,
body: AdmissionSchema.pick({ regex: true }).extend({
roles: z.array(z.coerce.number()),
}),
response: {
201: AdmissionSchema,
},
},
async handler(request, reply) {
let body = request.body
async handler(request) {
const { roles, ...admissionProps } = request.body
if (request.session?.userId) {
body = {
...request.body,
createdById: request.session.userId,
}
}
const trx = await db.startTransaction().execute()
return queries.create(body).then((row) => {
return reply.header('Location', `${request.url}/${row.id}`).status(201).send(row)
})
const admission = await trx
.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(),
}
},
})
@ -70,16 +89,18 @@ const admissionsPlugin: FastifyPluginCallbackTypebox = (fastify, _options, done)
url: '/:id',
method: 'DELETE',
schema: {
params: Type.Object({
id: Type.Number(),
params: z.object({
id: z.number(),
}),
response: {
204: {},
404: {},
},
},
handler(request, reply) {
return queries.removeById(request.params.id).then((count) => reply.status(count > 0 ? 204 : 404).send())
handler() {
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',
method: 'PATCH',
schema: {
params: Type.Object({
id: Type.Number(),
params: z.object({
id: z.coerce.number(),
}),
body: AdmissionSchema.pick({ regex: true }).extend({
roles: z.array(z.coerce.number()),
}),
body: AdmissionVariableSchema,
response: {
204: {},
},
},
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) {
body = {
...request.body,
modifiedById: request.session.userId,
}
}
const [updatedAdmission] = await Promise.all([
trx
.updateTable('admission')
.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,
),
])
return queries.update(request.params.id, body)
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(),
}
},
})

View File

@ -1,8 +1,8 @@
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'
const balanceRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
const balanceRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
fastify.route({
url: '/',
method: 'GET',

View File

@ -1,15 +1,16 @@
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'
const entryRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
const entryRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
fastify.route({
url: '/',
method: 'GET',
schema: {
querystring: Type.Object({
journal: Type.String(),
year: Type.Number(),
querystring: z.object({
journal: z.string(),
year: z.coerce.number(),
}),
},
async handler(req) {
@ -35,8 +36,8 @@ const entryRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
url: '/:id',
method: 'GET',
schema: {
params: Type.Object({
id: Type.Number(),
params: z.object({
id: z.number(),
}),
},
async handler(req) {

View File

@ -1,8 +1,8 @@
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'
const financialYearRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
const financialYearRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
fastify.route({
url: '/',
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 knex from '../../lib/knex.ts'
import emitter from '../../lib/emitter.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 { RoleSchema } from './roles.ts'
import { InviteSchema, RoleSchema } from '../../schemas/db.ts'
export const InviteSchema = Type.Object({
id: Type.Number(),
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: FastifyPluginCallbackZod = (fastify, _option, done) => {
const { db } = fastify
const invitesPlugin: FastifyPluginCallbackTypebox = (fastify, _option, done) => {
const queries = Queries({ emitter, knex })
fastify.addHook('onRequest', fastify.auth)
// fastify.addHook('onRequest', fastify.auth)
fastify.route({
url: '/',
method: 'GET',
schema: {
querystring: InviteSchema,
response: {
200: InviteSchema,
200: z.array(
InviteSchema.extend({
roles: z.array(RoleSchema.pick({ id: true, name: true })),
}),
),
},
},
handler(request) {
return queries.find(request.query)
handler() {
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: '/',
method: 'POST',
schema: {
body: {
type: Type.Object({
email: Type.String({ format: 'email' }),
roles: Type.Array(Type.Number()),
body: z.object({
email: z.email(),
roles: z.array(z.coerce.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) {
const trx = await knex.transaction()
const trx = await db.startTransaction().execute()
const invite = await queries.create(
{
...request.body,
createdById: request.session.userId,
},
trx,
const token = generateToken()
const invite = await trx
.insertInto('invite')
.values({ email: request.body.email, token, createdById: request.session.userId })
.returningAll()
.executeTakeFirstOrThrow()
await trx
.insertInto('invites_roles')
.values(
request.body.roles.map((roleId) => ({
inviteId: invite.id,
roleId,
})),
)
.execute()
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}`
await sendMail({
@ -77,11 +92,17 @@ const invitesPlugin: FastifyPluginCallbackTypebox = (fastify, _option, done) =>
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) {
await trx.rollback()
await trx.rollback().execute()
throw err
}
@ -92,16 +113,18 @@ const invitesPlugin: FastifyPluginCallbackTypebox = (fastify, _option, done) =>
url: '/:id',
method: 'DELETE',
schema: {
params: Type.Object({
id: Type.Number(),
params: z.object({
id: z.number(),
}),
response: {
204: {},
404: {},
},
},
handler(request, reply) {
return queries.removeById(request.params.id).then((count) => reply.status(count > 0 ? 204 : 404).send())
async handler(request, reply) {
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 { 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 StatusError from '../../lib/status_error.ts'
const invoiceRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
const invoiceRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
fastify.route({
url: '/',
method: 'GET',
schema: {
querystring: Type.Object({
year: Type.Optional(Type.Number()),
supplier: Type.Optional(Type.Number()),
querystring: z.object({
year: z.number().optional(),
supplier: z.number().optional(),
}),
},
async handler(req) {
@ -53,9 +54,9 @@ const invoiceRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
url: '/total-amount',
method: 'GET',
schema: {
querystring: Type.Object({
year: Type.Optional(Type.Number()),
supplier: Type.Optional(Type.Number()),
querystring: z.object({
year: z.number().optional(),
supplier: z.number().optional(),
}),
},
async handler(req) {
@ -80,8 +81,8 @@ const invoiceRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
url: '/:id',
method: 'GET',
schema: {
params: Type.Object({
id: Type.Number(),
params: z.object({
id: z.number(),
}),
},
handler(req) {
@ -110,8 +111,8 @@ const invoiceRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
url: '/by-supplier/:supplier',
method: 'GET',
schema: {
params: Type.Object({
supplier: Type.Number(),
params: z.object({
supplier: z.number(),
}),
},
handler(req) {
@ -136,8 +137,8 @@ const invoiceRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
url: '/by-year/:year',
method: 'GET',
schema: {
params: Type.Object({
year: Type.Number(),
params: z.object({
year: z.number(),
}),
},
async handler(req) {

View File

@ -1,8 +1,8 @@
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'
const journalRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
const journalRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
fastify.route({
url: '/',
method: 'GET',

View File

@ -1,8 +1,9 @@
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'
const objectRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
const objectRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
fastify.route({
url: '/',
method: 'GET',
@ -19,8 +20,8 @@ const objectRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
url: '/:id',
method: 'GET',
schema: {
params: Type.Object({
id: Type.Number(),
params: z.object({
id: z.number(),
}),
},
async handler(req) {

View File

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

View File

@ -1,8 +1,9 @@
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'
const resultRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
const resultRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
fastify.route({
url: '/',
method: 'GET',
@ -69,8 +70,8 @@ const resultRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
url: '/:year',
method: 'GET',
schema: {
params: Type.Object({
year: Type.Number(),
params: z.object({
year: z.number(),
}),
},
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 * 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'
import { RoleSchema } from '../../schemas/db.ts'
// 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'])
import { jsonObjectFrom } from 'kysely/helpers/postgres'
const rolesPlugin: FastifyPluginCallbackZod = (fastify, _options, done) => {
// const queries = Queries({ knex })
const { db } = fastify
// fastify.addHook('onRequest', fastify.auth)
fastify.addHook('onRequest', fastify.auth)
fastify.route({
url: '/',
method: 'GET',
schema: {
querystring: RoleSchema.partial().extend({
limit: z.number().optional(),
sort: z.keyof(RoleSchema).optional(),
offset: z.number().optional(),
}),
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) {
// 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()
handler() {
return db
.selectFrom('role as r')
.selectAll()
.select((eb) => [
jsonObjectFrom(eb.selectFrom('user as u').select(['u.id', 'u.email']).whereRef('u.id', '=', 'r.createdById'))
.$notNull()
.as('createdBy'),
jsonObjectFrom(
eb.selectFrom('user as u').select(['u.id', 'u.email']).whereRef('u.id', '=', 'r.modifiedById'),
).as('modifiedBy'),
])
.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: RoleSchema.pick({ name: true }),
response: {
201: RoleSchema,
},
},
handler(request) {
return db
.insertInto('role')
.values({
...request.body,
createdById: request.session.userId,
})
.returningAll()
.executeTakeFirstOrThrow()
},
})
// 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: z.object({
id: z.number(),
}),
response: {
204: {},
404: {},
},
},
handler(request) {
return db.deleteFrom('role').where('id', '=', request.params.id).execute()
},
})
// 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
// return queries.update(request.params.id, patch)
// },
// })
fastify.route({
url: '/:id',
method: 'PATCH',
schema: {
params: z.object({
id: z.number(),
}),
body: RoleSchema.pick({ name: true }),
response: {
204: {},
},
},
async handler(request) {
return db
.updateTable('role')
.set({
...request.body,
modifiedById: request.session.userId,
})
.where('id', '=', request.params.id)
.returningAll()
.execute()
},
})
done()
}

View File

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

View File

@ -1,7 +1,7 @@
import _ from 'lodash'
import * as z from '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 { db } = fastify
@ -13,31 +13,21 @@ const transactionRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
querystring: z.object({
year: z.optional(z.coerce.number()),
accountNumber: z.optional(z.coerce.number()),
limit: z.number().default(2),
sort: z.string().default('t.id'),
offset: z.number().optional(),
}),
},
async handler(req) {
const query: { financialYearId?: number; accountNumber?: number } = {}
async handler(request) {
const { offset, limit, sort: _sort, ...where } = request.query
if (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
}
if (req.query.accountNumber) {
query.accountNumber = req.query.accountNumber
}
return db
const baseQuery = db
.selectFrom('transaction as t')
.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',
'e.transactionDate',
't.entryId',
@ -45,9 +35,17 @@ const transactionRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
't.description',
't.invoiceId',
'e.description as entryDescription',
])
.where((eb) => eb.and(query))
.execute()
]),
baseQuery,
{
// @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 emitter from '../../lib/emitter.ts'
import Queries from '../../services/users/queries.ts'
const usersPlugin: FastifyPluginCallbackTypebox<{ addParentSchema: (schema: ANY) => void }> = (
const usersPlugin: FastifyPluginCallbackZod<{ addParentSchema: (schema: ANY) => void }> = (
fastify,
{ addParentSchema },
done,

View File

@ -1,11 +1,62 @@
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({
id: z.number().int().optional(),
name: z.string(),
createdAt: z.string().optional(),
createdAt: z.date().optional(),
createdById: z.number().int().nullable().optional(),
modifiedAt: z.string().nullable().optional(),
modifiedAt: z.date().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 {
logout: () => void
logout: () => Promise<void>
login: (user: ANY) => Promise<void>
user: Promise<ANY>
getUser: () => Promise<ANY>

View File

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