WIP more auth work and convert to kysely and zod
This commit is contained in:
parent
04e50a3021
commit
9d9ee1b4ce
@ -1,5 +1,5 @@
|
||||
meta {
|
||||
name: Admissions
|
||||
name: /api/admissions
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
meta {
|
||||
name: /api/invoices/:id
|
||||
type: http
|
||||
seq: 10
|
||||
seq: 8
|
||||
}
|
||||
|
||||
get {
|
||||
@ -1,7 +1,7 @@
|
||||
meta {
|
||||
name: /api/invoices/total-amount
|
||||
type: http
|
||||
seq: 12
|
||||
seq: 9
|
||||
}
|
||||
|
||||
get {
|
||||
@ -1,7 +1,7 @@
|
||||
meta {
|
||||
name: /api/invoices
|
||||
type: http
|
||||
seq: 9
|
||||
seq: 7
|
||||
}
|
||||
|
||||
get {
|
||||
@ -1,7 +1,7 @@
|
||||
meta {
|
||||
name: /api/objects/:id
|
||||
type: http
|
||||
seq: 7
|
||||
seq: 12
|
||||
}
|
||||
|
||||
get {
|
||||
@ -1,7 +1,7 @@
|
||||
meta {
|
||||
name: /api/objects
|
||||
type: http
|
||||
seq: 8
|
||||
seq: 11
|
||||
}
|
||||
|
||||
get {
|
||||
@ -1,7 +1,7 @@
|
||||
meta {
|
||||
name: /api/results
|
||||
type: http
|
||||
seq: 12
|
||||
seq: 13
|
||||
}
|
||||
|
||||
get {
|
||||
@ -1,7 +1,7 @@
|
||||
meta {
|
||||
name: Roles
|
||||
name: /api/roles
|
||||
type: http
|
||||
seq: 17
|
||||
seq: 15
|
||||
}
|
||||
|
||||
get {
|
||||
@ -1,7 +1,7 @@
|
||||
meta {
|
||||
name: /api/suppliers/merge
|
||||
type: http
|
||||
seq: 13
|
||||
seq: 17
|
||||
}
|
||||
|
||||
post {
|
||||
@ -1,7 +1,7 @@
|
||||
meta {
|
||||
name: /api/suppliers
|
||||
type: http
|
||||
seq: 11
|
||||
seq: 16
|
||||
}
|
||||
|
||||
get {
|
||||
@ -1,7 +1,7 @@
|
||||
meta {
|
||||
name: /api/transactions
|
||||
type: http
|
||||
seq: 16
|
||||
seq: 18
|
||||
}
|
||||
|
||||
get {
|
||||
@ -1,7 +1,7 @@
|
||||
meta {
|
||||
name: Users
|
||||
name: /api/users
|
||||
type: http
|
||||
seq: 9
|
||||
seq: 19
|
||||
}
|
||||
|
||||
get {
|
||||
8
.bruno/API/folder.bru
Normal file
8
.bruno/API/folder.bru
Normal file
@ -0,0 +1,8 @@
|
||||
meta {
|
||||
name: API
|
||||
seq: 2
|
||||
}
|
||||
|
||||
auth {
|
||||
mode: inherit
|
||||
}
|
||||
18
.bruno/Auth/Login.bru
Normal file
18
.bruno/Auth/Login.bru
Normal 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
11
.bruno/Auth/Logout.bru
Normal file
@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: Logout
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{base_url}}/auth/logout
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
--
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
) {
|
||||
|
||||
@ -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 })
|
||||
|
||||
|
||||
@ -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': {},
|
||||
},
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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' })
|
||||
|
||||
@ -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()
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@ -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(),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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()
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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()
|
||||
// })
|
||||
// })
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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(),
|
||||
})
|
||||
|
||||
|
||||
27
server/tests/admissions.test.ts
Normal file
27
server/tests/admissions.test.ts
Normal 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
2
shared/global.d.ts
vendored
@ -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>
|
||||
|
||||
@ -133,7 +133,7 @@ export interface Invite {
|
||||
}
|
||||
|
||||
export interface InvitesRoles {
|
||||
invitedId: number
|
||||
inviteId: number
|
||||
roleId: number
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user