WIP more auth work and convert to kysely and zod
This commit is contained in:
parent
04e50a3021
commit
9d9ee1b4ce
@ -1,5 +1,5 @@
|
|||||||
meta {
|
meta {
|
||||||
name: Admissions
|
name: /api/admissions
|
||||||
type: http
|
type: http
|
||||||
seq: 1
|
seq: 1
|
||||||
}
|
}
|
||||||
@ -1,11 +1,11 @@
|
|||||||
meta {
|
meta {
|
||||||
name: Invites
|
name: /api/invites
|
||||||
type: http
|
type: http
|
||||||
seq: 8
|
seq: 10
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
url: http://localhost:4040/api/invites
|
url: {{base_url}}/api/invites
|
||||||
body: none
|
body: none
|
||||||
auth: none
|
auth: none
|
||||||
}
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: /api/invoices/:id
|
name: /api/invoices/:id
|
||||||
type: http
|
type: http
|
||||||
seq: 10
|
seq: 8
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: /api/invoices/total-amount
|
name: /api/invoices/total-amount
|
||||||
type: http
|
type: http
|
||||||
seq: 12
|
seq: 9
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: /api/invoices
|
name: /api/invoices
|
||||||
type: http
|
type: http
|
||||||
seq: 9
|
seq: 7
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: /api/objects/:id
|
name: /api/objects/:id
|
||||||
type: http
|
type: http
|
||||||
seq: 7
|
seq: 12
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: /api/objects
|
name: /api/objects
|
||||||
type: http
|
type: http
|
||||||
seq: 8
|
seq: 11
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: /api/results
|
name: /api/results
|
||||||
type: http
|
type: http
|
||||||
seq: 12
|
seq: 13
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: Roles
|
name: /api/roles
|
||||||
type: http
|
type: http
|
||||||
seq: 17
|
seq: 15
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: /api/suppliers/merge
|
name: /api/suppliers/merge
|
||||||
type: http
|
type: http
|
||||||
seq: 13
|
seq: 17
|
||||||
}
|
}
|
||||||
|
|
||||||
post {
|
post {
|
||||||
@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: /api/suppliers
|
name: /api/suppliers
|
||||||
type: http
|
type: http
|
||||||
seq: 11
|
seq: 16
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: /api/transactions
|
name: /api/transactions
|
||||||
type: http
|
type: http
|
||||||
seq: 16
|
seq: 18
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: Users
|
name: /api/users
|
||||||
type: http
|
type: http
|
||||||
seq: 9
|
seq: 19
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
8
.bruno/API/folder.bru
Normal file
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()
|
actions.pending()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const form = e.currentTarget
|
||||||
|
|
||||||
const result = await rek[admission ? 'patch' : 'post'](
|
const result = await rek[admission ? 'patch' : 'post'](
|
||||||
`/api/admissions${admission ? '/' + admission.id : ''}`,
|
`/api/admissions${admission ? '/' + admission.id : ''}`,
|
||||||
serializeForm(e.currentTarget),
|
serializeForm(form),
|
||||||
)
|
)
|
||||||
|
|
||||||
actions.success()
|
actions.success()
|
||||||
@ -35,7 +37,7 @@ const AdmissionForm: FunctionComponent<{
|
|||||||
if (admission) {
|
if (admission) {
|
||||||
onUpdate?.(result)
|
onUpdate?.(result)
|
||||||
} else {
|
} else {
|
||||||
e.currentTarget.reset()
|
form.reset()
|
||||||
onCreate?.(result)
|
onCreate?.(result)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -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()
|
actions.pending()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await rek.post('/api/invites', serializeForm(e.currentTarget))
|
const form = e.currentTarget
|
||||||
|
const result = await rek.post('/api/invites', serializeForm(form))
|
||||||
|
|
||||||
e.currentTarget.reset()
|
form.reset()
|
||||||
actions.success()
|
actions.success()
|
||||||
onCreate?.(result)
|
onCreate?.(result)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -23,17 +23,16 @@ const RoleForm: FunctionComponent<{ role?: ANY; onCancel?: ANY; onCreate?: ANY;
|
|||||||
actions.pending()
|
actions.pending()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await rek[role ? 'patch' : 'post'](
|
const form = e.currentTarget
|
||||||
`/api/roles${role ? '/' + role.id : ''}`,
|
|
||||||
serializeForm(e.currentTarget),
|
const result = await rek[role ? 'patch' : 'post'](`/api/roles${role ? '/' + role.id : ''}`, serializeForm(form))
|
||||||
)
|
|
||||||
|
|
||||||
actions.success()
|
actions.success()
|
||||||
|
|
||||||
if (role) {
|
if (role) {
|
||||||
onUpdate?.(result)
|
onUpdate?.(result)
|
||||||
} else {
|
} else {
|
||||||
e.currentTarget.reset()
|
form.reset()
|
||||||
onCreate?.(result)
|
onCreate?.(result)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { h } from 'preact'
|
import { h } from 'preact'
|
||||||
import PageHeader from './page_header.tsx'
|
import PageHeader from './page_header.tsx'
|
||||||
import Row from './row.tsx'
|
import Row from './row.tsx'
|
||||||
import GitLog from './git_log.tsx'
|
|
||||||
import Process from './process.tsx'
|
import Process from './process.tsx'
|
||||||
import Section from './section.tsx'
|
import Section from './section.tsx'
|
||||||
|
|
||||||
@ -16,13 +15,6 @@ const StartPage = () => (
|
|||||||
</Section.Body>
|
</Section.Body>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section>
|
|
||||||
<Section.Heading>Latest Commits</Section.Heading>
|
|
||||||
<Section.Body>
|
|
||||||
<GitLog />
|
|
||||||
</Section.Body>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Row>
|
<Row>
|
||||||
<Section>
|
<Section>
|
||||||
<Section.Heading>Process</Section.Heading>
|
<Section.Heading>Process</Section.Heading>
|
||||||
|
|||||||
@ -159,7 +159,7 @@ ALTER SEQUENCE public.invite_id_seq OWNED BY public.invite.id;
|
|||||||
--
|
--
|
||||||
|
|
||||||
CREATE TABLE public.invites_roles (
|
CREATE TABLE public.invites_roles (
|
||||||
"invitedId" integer NOT NULL,
|
"inviteId" integer NOT NULL,
|
||||||
"roleId" integer NOT NULL
|
"roleId" integer NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -401,7 +401,7 @@ ALTER TABLE ONLY public.invite
|
|||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE ONLY public.invites_roles
|
ALTER TABLE ONLY public.invites_roles
|
||||||
ADD CONSTRAINT invites_roles_pkey PRIMARY KEY ("invitedId", "roleId");
|
ADD CONSTRAINT invites_roles_pkey PRIMARY KEY ("inviteId", "roleId");
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
@ -577,7 +577,7 @@ ALTER TABLE ONLY public.invite
|
|||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE ONLY public.invites_roles
|
ALTER TABLE ONLY public.invites_roles
|
||||||
ADD CONSTRAINT "invites_roles_inviteId_fkey" FOREIGN KEY ("invitedId") REFERENCES public.invite(id) ON UPDATE CASCADE ON DELETE CASCADE;
|
ADD CONSTRAINT "invites_roles_inviteId_fkey" FOREIGN KEY ("inviteId") REFERENCES public.invite(id) ON UPDATE CASCADE ON DELETE CASCADE;
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import env from '../env.ts'
|
import env from '../env.ts'
|
||||||
|
|
||||||
const domain = 'startbit.bitmill.io'
|
const domain = 'brf.lkm.nu'
|
||||||
|
|
||||||
type SiteConfig = {
|
type SiteConfig = {
|
||||||
title: string
|
title: string
|
||||||
@ -17,8 +17,8 @@ type SiteConfig = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const defaults: SiteConfig = {
|
const defaults: SiteConfig = {
|
||||||
title: 'startbit',
|
title: 'BRF',
|
||||||
name: 'startbit,',
|
name: 'brf',
|
||||||
port: null,
|
port: null,
|
||||||
hostname: null,
|
hostname: null,
|
||||||
domain,
|
domain,
|
||||||
|
|||||||
@ -53,7 +53,6 @@ export default read(
|
|||||||
'SESSION_SECRET',
|
'SESSION_SECRET',
|
||||||
] as const,
|
] as const,
|
||||||
{
|
{
|
||||||
MAILGUN_API_KEY: 'this is not a real key',
|
|
||||||
PGPASSWORD: null,
|
PGPASSWORD: null,
|
||||||
PGPORT: null,
|
PGPORT: null,
|
||||||
PGUSER: null,
|
PGUSER: null,
|
||||||
|
|||||||
@ -1,47 +1,80 @@
|
|||||||
// import type { QueryBuilder } from 'knex'
|
import { type SelectQueryBuilder, type ReferenceExpression, type OperandValueExpression, sql } from 'kysely'
|
||||||
import type { SelectQueryBuilder } from 'kysely'
|
|
||||||
import _ from 'lodash'
|
|
||||||
|
|
||||||
// export function convertToReturning(obj: Record<string, string>) {
|
type WhereInput<DB, TB extends keyof DB> = Partial<{
|
||||||
// return _.map(obj, (value, key) => _.isString(value) && `${value} as ${key}`).filter(_.identity)
|
[C in keyof DB[TB]]:
|
||||||
// }
|
| OperandValueExpression<DB, TB, DB[TB][C]>
|
||||||
|
| readonly OperandValueExpression<DB, TB, DB[TB][C]>[]
|
||||||
|
| null
|
||||||
|
}>
|
||||||
|
|
||||||
// export function columnAs(name: string, table: string, transformer = _.snakeCase) {
|
export function applyWhere<DB, TB extends keyof DB, O>(
|
||||||
// const transformed = transformer(name)
|
builder: SelectQueryBuilder<DB, TB, O>,
|
||||||
|
where: WhereInput<DB, TB>,
|
||||||
|
): SelectQueryBuilder<DB, TB, O> {
|
||||||
|
return Object.entries(where).reduce((builder, [key, value]) => {
|
||||||
|
const column = key as ReferenceExpression<DB, TB>
|
||||||
|
|
||||||
// if (transformed !== name) {
|
if (value === null) {
|
||||||
// // knex automagically wraps everything in ", so they are not needed around ${name}
|
return builder.where(column, 'is', null)
|
||||||
// return `${table ? table + '.' : ''}${transformed} as ${name}`
|
} else if (Array.isArray(value)) {
|
||||||
// }
|
return builder.where(column, 'in', value)
|
||||||
|
}
|
||||||
|
|
||||||
// return table ? `${table}.${name}` : name
|
return builder.where(column, '=', value)
|
||||||
// }
|
}, builder)
|
||||||
|
}
|
||||||
// export function columnBuilder(builder: QueryBuilder, columns: Record<string, string>, columnNames: string[]) {
|
|
||||||
// for (const columnName of columnNames) {
|
interface PaginationInput<DB, TB extends keyof DB> {
|
||||||
// const column = columns[columnName]
|
where: WhereInput<DB, TB>
|
||||||
|
limit?: number
|
||||||
// if (column) {
|
offset?: number
|
||||||
// // @ts-ignore
|
orderBy?: {
|
||||||
// builder.column(column)
|
column: ReferenceExpression<DB, TB>
|
||||||
// }
|
direction?: 'asc' | 'desc'
|
||||||
// }
|
}
|
||||||
|
}
|
||||||
// return builder
|
|
||||||
// }
|
export function applyPagination<DB, TB extends keyof DB, O>(
|
||||||
|
builder: SelectQueryBuilder<DB, TB, O>,
|
||||||
export function where<DB, TB extends keyof DB, O>(builder: SelectQueryBuilder<DB, TB, O>, json: Record<string, ANY>) {
|
{ limit, offset, orderBy }: PaginationInput<DB, TB>,
|
||||||
return _.reduce(
|
): SelectQueryBuilder<DB, TB, O> {
|
||||||
json,
|
let qb = builder
|
||||||
(builder, value, key) => {
|
|
||||||
if (value === null) {
|
if (orderBy) {
|
||||||
return builder.where(key, 'is', null)
|
qb = qb.orderBy(orderBy.column, orderBy.direction ?? 'asc')
|
||||||
} else if (Array.isArray(value)) {
|
}
|
||||||
return builder.where(key, 'in', value)
|
|
||||||
}
|
if (limit !== undefined) {
|
||||||
|
qb = qb.limit(limit)
|
||||||
return builder.where(key, '=', value)
|
}
|
||||||
},
|
|
||||||
builder,
|
if (offset !== undefined) {
|
||||||
)
|
qb = qb.offset(offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
return qb
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function paginate<DB, TB extends keyof DB, O>(
|
||||||
|
baseDataQuery: SelectQueryBuilder<DB, TB, O>,
|
||||||
|
baseCountQuery: SelectQueryBuilder<DB, TB, unknown>,
|
||||||
|
input: PaginationInput<DB, TB>,
|
||||||
|
) {
|
||||||
|
const dataQuery = applyPagination(input.where ? applyWhere(baseDataQuery, input.where) : baseDataQuery, input)
|
||||||
|
|
||||||
|
let countQuery = baseCountQuery.select(sql<number>`count(*)::int`.as('totalCount'))
|
||||||
|
|
||||||
|
if (input.where) {
|
||||||
|
countQuery = applyWhere(countQuery, input.where)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [countRow, items] = await Promise.all([countQuery.executeTakeFirstOrThrow(), dataQuery.execute()])
|
||||||
|
|
||||||
|
return {
|
||||||
|
items,
|
||||||
|
totalCount: (countRow as { totalCount: number }).totalCount,
|
||||||
|
limit: input.limit,
|
||||||
|
offset: input.offset,
|
||||||
|
count: items.length,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { RouteHandler } from 'fastify'
|
import type { RouteHandler } from 'fastify'
|
||||||
import { Type, type Static } from '@fastify/type-provider-typebox'
|
import * as z from 'zod'
|
||||||
import config from '../../../config.ts'
|
import config from '../../../config.ts'
|
||||||
import knex from '../../../lib/knex.ts'
|
import knex from '../../../lib/knex.ts'
|
||||||
import StatusError from '../../../lib/status_error.ts'
|
import StatusError from '../../../lib/status_error.ts'
|
||||||
@ -8,10 +8,10 @@ import { hashPassword } from '../helpers.ts'
|
|||||||
import { StatusErrorSchema } from '../../../schemas/status_error.ts'
|
import { StatusErrorSchema } from '../../../schemas/status_error.ts'
|
||||||
const { errors, timeouts } = config.auth
|
const { errors, timeouts } = config.auth
|
||||||
|
|
||||||
const BodySchema = Type.Object({
|
const BodySchema = z.object({
|
||||||
email: Type.String({ format: 'email' }),
|
email: z.email(),
|
||||||
password: Type.String(),
|
password: z.string(),
|
||||||
token: Type.String(),
|
token: z.string(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const ResponseSchema = {
|
const ResponseSchema = {
|
||||||
@ -19,7 +19,7 @@ const ResponseSchema = {
|
|||||||
'4XX': StatusErrorSchema,
|
'4XX': StatusErrorSchema,
|
||||||
}
|
}
|
||||||
|
|
||||||
const changePassword: RouteHandler<{ Body: Static<typeof BodySchema> }> = async function changePassword(
|
const changePassword: RouteHandler<{ Body: z.infer<typeof BodySchema> }> = async function changePassword(
|
||||||
request,
|
request,
|
||||||
reply,
|
reply,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
import * as z from 'zod'
|
||||||
import type { RouteHandler } from 'fastify'
|
import type { RouteHandler } from 'fastify'
|
||||||
import { Type, type Static } from '@fastify/type-provider-typebox'
|
|
||||||
import config from '../../../config.ts'
|
import config from '../../../config.ts'
|
||||||
import UserQueries from '../../../services/users/queries.ts'
|
import UserQueries from '../../../services/users/queries.ts'
|
||||||
import emitter from '../../../lib/emitter.ts'
|
import emitter from '../../../lib/emitter.ts'
|
||||||
@ -12,14 +12,14 @@ import { verifyPassword } from '../helpers.ts'
|
|||||||
const userQueries = UserQueries({ knex, emitter })
|
const userQueries = UserQueries({ knex, emitter })
|
||||||
const { errors, maxLoginAttempts, requireVerification } = config.auth
|
const { errors, maxLoginAttempts, requireVerification } = config.auth
|
||||||
|
|
||||||
const BodySchema = Type.Object({
|
const BodySchema = z.object({
|
||||||
email: Type.String(),
|
email: z.string(),
|
||||||
password: Type.String(),
|
password: z.string(),
|
||||||
remember: Type.Optional(Type.Boolean()),
|
remember: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const ResponseSchema = {
|
const ResponseSchema = {
|
||||||
200: Type.Object({
|
200: z.object({
|
||||||
id: { type: 'integer' },
|
id: { type: 'integer' },
|
||||||
email: { type: 'string' },
|
email: { type: 'string' },
|
||||||
password: { type: 'string' },
|
password: { type: 'string' },
|
||||||
@ -31,7 +31,7 @@ const ResponseSchema = {
|
|||||||
'4XX': StatusErrorSchema,
|
'4XX': StatusErrorSchema,
|
||||||
}
|
}
|
||||||
|
|
||||||
const login: RouteHandler<{ Body: Static<typeof BodySchema> }> = async function (request, reply) {
|
const login: RouteHandler<{ Body: z.infer<typeof BodySchema> }> = async function (request, reply) {
|
||||||
try {
|
try {
|
||||||
const user = await userQueries.findOne({ email: request.body.email })
|
const user = await userQueries.findOne({ email: request.body.email })
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import type { RouteHandler } from 'fastify'
|
import type { FastifySchema, RouteHandler } from 'fastify'
|
||||||
|
|
||||||
/** @type {import('fastify').FastifySchema} */
|
const schema: FastifySchema = {
|
||||||
const schema = {
|
|
||||||
response: {
|
response: {
|
||||||
'3XX': {},
|
'3XX': {},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { Type, type Static } from '@fastify/type-provider-typebox'
|
import * as z from 'zod'
|
||||||
import type { RouteHandler } from 'fastify'
|
import type { RouteHandler } from 'fastify'
|
||||||
import config from '../../../config.ts'
|
import config from '../../../config.ts'
|
||||||
import emitter from '../../../lib/emitter.ts'
|
import emitter from '../../../lib/emitter.ts'
|
||||||
@ -20,11 +20,11 @@ const userQueries = UserQueries({ knex, emitter })
|
|||||||
|
|
||||||
const { errors, timeouts } = config.auth
|
const { errors, timeouts } = config.auth
|
||||||
|
|
||||||
const BodySchema = Type.Object({
|
const BodySchema = z.object({
|
||||||
email: Type.String({ format: 'email' }),
|
email: z.email(),
|
||||||
password: Type.String(),
|
password: z.string(),
|
||||||
inviteEmail: Type.Optional(Type.String({ format: 'email' })),
|
inviteEmail: z.email().optional(),
|
||||||
inviteToken: Type.Optional(Type.String()),
|
inviteToken: z.string().optional(),
|
||||||
// required: config.auth.requireInvite ? ['email', 'password', 'inviteEmail', 'inviteToken'] : ['email', 'password'],
|
// required: config.auth.requireInvite ? ['email', 'password', 'inviteEmail', 'inviteToken'] : ['email', 'password'],
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -44,7 +44,7 @@ const ResponseSchema = {
|
|||||||
'4XX': { $ref: 'status-error' },
|
'4XX': { $ref: 'status-error' },
|
||||||
}
|
}
|
||||||
|
|
||||||
const register: RouteHandler<{ Body: Static<typeof BodySchema> }> = async function (request, reply) {
|
const register: RouteHandler<{ Body: z.infer<typeof BodySchema> }> = async function (request, reply) {
|
||||||
try {
|
try {
|
||||||
// TODO validate and ensure same as confirm
|
// TODO validate and ensure same as confirm
|
||||||
const email = request.body.email.trim().toLowerCase()
|
const email = request.body.email.trim().toLowerCase()
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { RouteHandler } from 'fastify'
|
import type { RouteHandler } from 'fastify'
|
||||||
import { Type, type Static } from '@fastify/type-provider-typebox'
|
import * as z from 'zod'
|
||||||
import config from '../../../config.ts'
|
import config from '../../../config.ts'
|
||||||
import knex from '../../../lib/knex.ts'
|
import knex from '../../../lib/knex.ts'
|
||||||
import sendMail from '../../../lib/send_mail.ts'
|
import sendMail from '../../../lib/send_mail.ts'
|
||||||
@ -11,8 +11,8 @@ import forgotPasswordTemplate from '../../../templates/emails/forgot_password.ts
|
|||||||
|
|
||||||
const { errors } = config.auth
|
const { errors } = config.auth
|
||||||
|
|
||||||
const BodySchema = Type.Object({
|
const BodySchema = z.object({
|
||||||
email: Type.String({ format: 'email' }),
|
email: z.email(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const ResponseSchema = {
|
const ResponseSchema = {
|
||||||
@ -20,7 +20,7 @@ const ResponseSchema = {
|
|||||||
'4XX': StatusErrorSchema,
|
'4XX': StatusErrorSchema,
|
||||||
}
|
}
|
||||||
|
|
||||||
const forgotPassword: RouteHandler<{ Body: Static<typeof BodySchema> }> = async function (request, reply) {
|
const forgotPassword: RouteHandler<{ Body: z.infer<typeof BodySchema> }> = async function (request, reply) {
|
||||||
const { email } = request.body
|
const { email } = request.body
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
import type { RouteHandler } from 'fastify'
|
import type { RouteHandler } from 'fastify'
|
||||||
|
import * as z from 'zod'
|
||||||
import config from '../../../config.ts'
|
import config from '../../../config.ts'
|
||||||
import knex from '../../../lib/knex.ts'
|
import knex from '../../../lib/knex.ts'
|
||||||
import StatusError from '../../../lib/status_error.ts'
|
import StatusError from '../../../lib/status_error.ts'
|
||||||
import { StatusErrorSchema } from '../../../schemas/status_error.ts'
|
import { StatusErrorSchema } from '../../../schemas/status_error.ts'
|
||||||
import { Type, type Static } from '@fastify/type-provider-typebox'
|
|
||||||
|
|
||||||
const { errors, timeouts } = config.auth
|
const { errors, timeouts } = config.auth
|
||||||
|
|
||||||
const QuerystringSchema = Type.Object({
|
const QuerystringSchema = z.object({
|
||||||
email: Type.String({ format: 'email' }),
|
email: z.email(),
|
||||||
token: Type.String(),
|
token: z.string(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const ResponseSchema = {
|
const ResponseSchema = {
|
||||||
@ -17,7 +17,7 @@ const ResponseSchema = {
|
|||||||
'4XX': StatusErrorSchema,
|
'4XX': StatusErrorSchema,
|
||||||
}
|
}
|
||||||
|
|
||||||
const verifyEmail: RouteHandler<{ Querystring: Static<typeof QuerystringSchema> }> = async function (request, reply) {
|
const verifyEmail: RouteHandler<{ Querystring: z.infer<typeof QuerystringSchema> }> = async function (request, reply) {
|
||||||
try {
|
try {
|
||||||
const { token, email } = request.query
|
const { token, email } = request.query
|
||||||
|
|
||||||
|
|||||||
@ -2,9 +2,11 @@ import _ from 'lodash'
|
|||||||
import { type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox'
|
import { type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox'
|
||||||
|
|
||||||
import accounts from './api/accounts.ts'
|
import accounts from './api/accounts.ts'
|
||||||
|
import admissions from './api/admissions.ts'
|
||||||
import balances from './api/balances.ts'
|
import balances from './api/balances.ts'
|
||||||
import entries from './api/entries.ts'
|
import entries from './api/entries.ts'
|
||||||
import financialYears from './api/financial_years.ts'
|
import financialYears from './api/financial_years.ts'
|
||||||
|
import invites from './api/invites.ts'
|
||||||
import invoices from './api/invoices.ts'
|
import invoices from './api/invoices.ts'
|
||||||
import journals from './api/journals.ts'
|
import journals from './api/journals.ts'
|
||||||
import objects from './api/objects.ts'
|
import objects from './api/objects.ts'
|
||||||
@ -16,9 +18,11 @@ import transactions from './api/transactions.ts'
|
|||||||
|
|
||||||
const apiRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
|
const apiRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
|
||||||
fastify.register(accounts, { prefix: '/accounts' })
|
fastify.register(accounts, { prefix: '/accounts' })
|
||||||
|
fastify.register(admissions, { prefix: '/admissions' })
|
||||||
fastify.register(balances, { prefix: '/balances' })
|
fastify.register(balances, { prefix: '/balances' })
|
||||||
fastify.register(entries, { prefix: '/entries' })
|
fastify.register(entries, { prefix: '/entries' })
|
||||||
fastify.register(financialYears, { prefix: '/financial-years' })
|
fastify.register(financialYears, { prefix: '/financial-years' })
|
||||||
|
fastify.register(invites, { prefix: '/invites' })
|
||||||
fastify.register(invoices, { prefix: '/invoices' })
|
fastify.register(invoices, { prefix: '/invoices' })
|
||||||
fastify.register(journals, { prefix: '/journals' })
|
fastify.register(journals, { prefix: '/journals' })
|
||||||
fastify.register(objects, { prefix: '/objects' })
|
fastify.register(objects, { prefix: '/objects' })
|
||||||
|
|||||||
@ -1,13 +1,22 @@
|
|||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox'
|
import * as z from 'zod'
|
||||||
import knex from '../../lib/knex.ts'
|
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
|
||||||
|
|
||||||
|
import { AccountSchema } from '../../schemas/db.ts'
|
||||||
|
|
||||||
|
const accountRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
|
||||||
|
const { db } = fastify
|
||||||
|
|
||||||
const accountRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
|
|
||||||
fastify.route({
|
fastify.route({
|
||||||
url: '/',
|
url: '/',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.array(AccountSchema),
|
||||||
|
},
|
||||||
|
},
|
||||||
handler() {
|
handler() {
|
||||||
return knex('account').select('*').orderBy('number')
|
return db.selectFrom('account').selectAll().orderBy('number').execute()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,30 +1,12 @@
|
|||||||
import { Type, type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox'
|
import * as z from 'zod'
|
||||||
import emitter from '../../lib/emitter.ts'
|
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
|
||||||
import knex from '../../lib/knex.ts'
|
import { sql } from 'kysely'
|
||||||
import Queries from '../../services/admissions/queries.ts'
|
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres'
|
||||||
|
|
||||||
import { RoleSchema } from './roles.ts'
|
import { AdmissionSchema, RoleSchema } from '../../schemas/db.ts'
|
||||||
|
|
||||||
export const AdmissionSchema = Type.Object({
|
const admissionsPlugin: FastifyPluginCallbackZod = (fastify, _options, done) => {
|
||||||
id: Type.Number(),
|
const { db } = fastify
|
||||||
regex: Type.String(),
|
|
||||||
roles: Type.Array(RoleSchema),
|
|
||||||
createdAt: Type.String({ format: 'date-time' }),
|
|
||||||
createdById: { type: 'integer' },
|
|
||||||
createdBy: { type: 'object', properties: { id: { type: 'integer' }, email: { type: 'string' } } },
|
|
||||||
modifiedAt: [Type.String({ format: 'date-time' }), Type.Null()],
|
|
||||||
modifiedById: [Type.Number, Type.Null()],
|
|
||||||
})
|
|
||||||
|
|
||||||
export const AdmissionVariableSchema = Type.Object({
|
|
||||||
regex: Type.String(),
|
|
||||||
roles: Type.Array(Type.Number()),
|
|
||||||
createdById: Type.Optional(Type.Number()),
|
|
||||||
modifiedById: Type.Optional(Type.Number()),
|
|
||||||
})
|
|
||||||
|
|
||||||
const admissionsPlugin: FastifyPluginCallbackTypebox = (fastify, _options, done) => {
|
|
||||||
const queries = Queries({ emitter, knex })
|
|
||||||
|
|
||||||
fastify.addHook('onRequest', fastify.auth)
|
fastify.addHook('onRequest', fastify.auth)
|
||||||
|
|
||||||
@ -33,11 +15,33 @@ const admissionsPlugin: FastifyPluginCallbackTypebox = (fastify, _options, done)
|
|||||||
method: 'GET',
|
method: 'GET',
|
||||||
schema: {
|
schema: {
|
||||||
response: {
|
response: {
|
||||||
200: { type: 'array', items: { $ref: 'admission' } },
|
200: z.array(
|
||||||
|
AdmissionSchema.extend({
|
||||||
|
roles: z.array(RoleSchema.pick({ id: true, name: true })),
|
||||||
|
}),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
handler(request) {
|
handler() {
|
||||||
return queries.find(request.query)
|
return db
|
||||||
|
.selectFrom('admission as a')
|
||||||
|
.selectAll()
|
||||||
|
.select((eb) => [
|
||||||
|
jsonObjectFrom(eb.selectFrom('user as u').select(['u.id', 'u.email']).whereRef('u.id', '=', 'a.createdById'))
|
||||||
|
.$notNull()
|
||||||
|
.as('createdBy'),
|
||||||
|
jsonObjectFrom(
|
||||||
|
eb.selectFrom('user as u').select(['u.id', 'u.email']).whereRef('u.id', '=', 'a.modifiedById'),
|
||||||
|
).as('modifiedBy'),
|
||||||
|
jsonArrayFrom(
|
||||||
|
eb
|
||||||
|
.selectFrom('role as r')
|
||||||
|
.innerJoin('admissions_roles as ra', 'ra.roleId', 'r.id')
|
||||||
|
.select(['r.id', 'r.name'])
|
||||||
|
.whereRef('ra.admissionId', '=', 'a.id'),
|
||||||
|
).as('roles'),
|
||||||
|
])
|
||||||
|
.execute()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -45,24 +49,39 @@ const admissionsPlugin: FastifyPluginCallbackTypebox = (fastify, _options, done)
|
|||||||
url: '/',
|
url: '/',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
schema: {
|
schema: {
|
||||||
body: AdmissionVariableSchema,
|
body: AdmissionSchema.pick({ regex: true }).extend({
|
||||||
|
roles: z.array(z.coerce.number()),
|
||||||
|
}),
|
||||||
response: {
|
response: {
|
||||||
201: AdmissionSchema,
|
201: AdmissionSchema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async handler(request, reply) {
|
async handler(request) {
|
||||||
let body = request.body
|
const { roles, ...admissionProps } = request.body
|
||||||
|
|
||||||
if (request.session?.userId) {
|
const trx = await db.startTransaction().execute()
|
||||||
body = {
|
|
||||||
...request.body,
|
const admission = await trx
|
||||||
createdById: request.session.userId,
|
.insertInto('admission')
|
||||||
}
|
.values({ ...admissionProps, createdById: request.session.userId })
|
||||||
|
.returningAll()
|
||||||
|
.executeTakeFirstOrThrow()
|
||||||
|
|
||||||
|
await trx
|
||||||
|
.insertInto('admissions_roles')
|
||||||
|
.values(roles.map((roleId) => ({ admissionId: admission.id, roleId })))
|
||||||
|
.execute()
|
||||||
|
|
||||||
|
await trx.commit().execute()
|
||||||
|
|
||||||
|
return {
|
||||||
|
...admission,
|
||||||
|
roles: await db
|
||||||
|
.selectFrom('role as r')
|
||||||
|
.innerJoin('admissions_roles as ar', 'ar.roleId', 'r.id')
|
||||||
|
.select(['r.id', 'r.name'])
|
||||||
|
.execute(),
|
||||||
}
|
}
|
||||||
|
|
||||||
return queries.create(body).then((row) => {
|
|
||||||
return reply.header('Location', `${request.url}/${row.id}`).status(201).send(row)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -70,16 +89,18 @@ const admissionsPlugin: FastifyPluginCallbackTypebox = (fastify, _options, done)
|
|||||||
url: '/:id',
|
url: '/:id',
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
schema: {
|
schema: {
|
||||||
params: Type.Object({
|
params: z.object({
|
||||||
id: Type.Number(),
|
id: z.number(),
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
204: {},
|
204: {},
|
||||||
404: {},
|
404: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
handler(request, reply) {
|
handler() {
|
||||||
return queries.removeById(request.params.id).then((count) => reply.status(count > 0 ? 204 : 404).send())
|
return {}
|
||||||
|
|
||||||
|
// return queries.removeById(request.params.id).then((count: number) => reply.status(count > 0 ? 204 : 404).send())
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -87,25 +108,50 @@ const admissionsPlugin: FastifyPluginCallbackTypebox = (fastify, _options, done)
|
|||||||
url: '/:id',
|
url: '/:id',
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
schema: {
|
schema: {
|
||||||
params: Type.Object({
|
params: z.object({
|
||||||
id: Type.Number(),
|
id: z.coerce.number(),
|
||||||
|
}),
|
||||||
|
body: AdmissionSchema.pick({ regex: true }).extend({
|
||||||
|
roles: z.array(z.coerce.number()),
|
||||||
}),
|
}),
|
||||||
body: AdmissionVariableSchema,
|
|
||||||
response: {
|
response: {
|
||||||
204: {},
|
204: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async handler(request) {
|
async handler(request) {
|
||||||
let body = request.body
|
const { roles, ...admissionProps } = request.body
|
||||||
|
const trx = await db.startTransaction().execute()
|
||||||
|
const admissionId = request.params.id
|
||||||
|
|
||||||
if (request.session.userId) {
|
const [updatedAdmission] = await Promise.all([
|
||||||
body = {
|
trx
|
||||||
...request.body,
|
.updateTable('admission')
|
||||||
modifiedById: request.session.userId,
|
.set({ ...admissionProps, modifiedById: request.session.userId })
|
||||||
}
|
.where('id', '=', request.params.id)
|
||||||
|
.returningAll()
|
||||||
|
.execute(),
|
||||||
|
trx
|
||||||
|
.deleteFrom('admissions_roles')
|
||||||
|
.where('admissionId', '=', request.params.id)
|
||||||
|
.where('roleId', 'not in', roles)
|
||||||
|
.execute(),
|
||||||
|
sql`INSERT INTO admissions_roles("admissionId", "roleId")
|
||||||
|
SELECT ${admissionId}, "roleIds" FROM unnest(${roles}::int[]) AS "roleIds" WHERE NOT EXISTS
|
||||||
|
(SELECT 1 FROM admissions_roles WHERE "admissionId" = ${request.params.id} AND "roleId" = "roleIds")`.execute(
|
||||||
|
trx,
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
await trx.commit().execute()
|
||||||
|
|
||||||
|
return {
|
||||||
|
...updatedAdmission,
|
||||||
|
roles: await db
|
||||||
|
.selectFrom('role as r')
|
||||||
|
.innerJoin('admissions_roles as ar', 'ar.roleId', 'r.id')
|
||||||
|
.select(['r.id', 'r.name'])
|
||||||
|
.execute(),
|
||||||
}
|
}
|
||||||
|
|
||||||
return queries.update(request.params.id, body)
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox'
|
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
|
||||||
import knex from '../../lib/knex.ts'
|
import knex from '../../lib/knex.ts'
|
||||||
|
|
||||||
const balanceRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
|
const balanceRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
|
||||||
fastify.route({
|
fastify.route({
|
||||||
url: '/',
|
url: '/',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
|||||||
@ -1,15 +1,16 @@
|
|||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { Type, type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox'
|
import * as z from 'zod'
|
||||||
|
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
|
||||||
import knex from '../../lib/knex.ts'
|
import knex from '../../lib/knex.ts'
|
||||||
|
|
||||||
const entryRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
|
const entryRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
|
||||||
fastify.route({
|
fastify.route({
|
||||||
url: '/',
|
url: '/',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
schema: {
|
schema: {
|
||||||
querystring: Type.Object({
|
querystring: z.object({
|
||||||
journal: Type.String(),
|
journal: z.string(),
|
||||||
year: Type.Number(),
|
year: z.coerce.number(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
async handler(req) {
|
async handler(req) {
|
||||||
@ -35,8 +36,8 @@ const entryRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
|
|||||||
url: '/:id',
|
url: '/:id',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
schema: {
|
schema: {
|
||||||
params: Type.Object({
|
params: z.object({
|
||||||
id: Type.Number(),
|
id: z.number(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
async handler(req) {
|
async handler(req) {
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox'
|
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
|
||||||
import knex from '../../lib/knex.ts'
|
import knex from '../../lib/knex.ts'
|
||||||
|
|
||||||
const financialYearRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
|
const financialYearRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
|
||||||
fastify.route({
|
fastify.route({
|
||||||
url: '/',
|
url: '/',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
|||||||
@ -1,42 +1,44 @@
|
|||||||
import { Type, type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox'
|
import * as z from 'zod'
|
||||||
|
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
|
||||||
|
import { jsonArrayFrom } from 'kysely/helpers/postgres'
|
||||||
import config from '../../config.ts'
|
import config from '../../config.ts'
|
||||||
import knex from '../../lib/knex.ts'
|
|
||||||
import emitter from '../../lib/emitter.ts'
|
|
||||||
import sendMail from '../../lib/send_mail.ts'
|
import sendMail from '../../lib/send_mail.ts'
|
||||||
import Queries from '../../services/invites/queries.ts'
|
import { generateToken } from '../../plugins/auth/helpers.ts'
|
||||||
import inviteEmailTemplate from '../../templates/emails/invite.ts'
|
import inviteEmailTemplate from '../../templates/emails/invite.ts'
|
||||||
|
|
||||||
import { RoleSchema } from './roles.ts'
|
import { InviteSchema, RoleSchema } from '../../schemas/db.ts'
|
||||||
|
|
||||||
export const InviteSchema = Type.Object({
|
const invitesPlugin: FastifyPluginCallbackZod = (fastify, _option, done) => {
|
||||||
id: Type.Number(),
|
const { db } = fastify
|
||||||
email: Type.String({ format: 'email' }),
|
|
||||||
token: Type.String(),
|
|
||||||
roles: Type.Array(RoleSchema),
|
|
||||||
createdAt: Type.String(),
|
|
||||||
createdById: Type.Number(),
|
|
||||||
createdBy: Type.Object({}),
|
|
||||||
consumedAt: [Type.String(), Type.Null()],
|
|
||||||
consumedById: [Type.Number(), Type.Null()],
|
|
||||||
consumedBy: [Type.Object({}), Type.Null()],
|
|
||||||
})
|
|
||||||
|
|
||||||
const invitesPlugin: FastifyPluginCallbackTypebox = (fastify, _option, done) => {
|
// fastify.addHook('onRequest', fastify.auth)
|
||||||
const queries = Queries({ emitter, knex })
|
|
||||||
|
|
||||||
fastify.addHook('onRequest', fastify.auth)
|
|
||||||
|
|
||||||
fastify.route({
|
fastify.route({
|
||||||
url: '/',
|
url: '/',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
schema: {
|
schema: {
|
||||||
querystring: InviteSchema,
|
|
||||||
response: {
|
response: {
|
||||||
200: InviteSchema,
|
200: z.array(
|
||||||
|
InviteSchema.extend({
|
||||||
|
roles: z.array(RoleSchema.pick({ id: true, name: true })),
|
||||||
|
}),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
handler(request) {
|
handler() {
|
||||||
return queries.find(request.query)
|
return db
|
||||||
|
.selectFrom('invite as i')
|
||||||
|
.selectAll()
|
||||||
|
.select((eb) => [
|
||||||
|
jsonArrayFrom(
|
||||||
|
eb
|
||||||
|
.selectFrom('role as r')
|
||||||
|
.innerJoin('invites_roles as ir', 'ir.roleId', 'r.id')
|
||||||
|
.select(['r.id', 'r.name'])
|
||||||
|
.whereRef('r.id', '=', 'ir.roleId'),
|
||||||
|
).as('roles'),
|
||||||
|
])
|
||||||
|
.execute()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -44,31 +46,44 @@ const invitesPlugin: FastifyPluginCallbackTypebox = (fastify, _option, done) =>
|
|||||||
url: '/',
|
url: '/',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
schema: {
|
schema: {
|
||||||
body: {
|
body: z.object({
|
||||||
type: Type.Object({
|
email: z.email(),
|
||||||
email: Type.String({ format: 'email' }),
|
roles: z.array(z.coerce.number()),
|
||||||
roles: Type.Array(Type.Number()),
|
}),
|
||||||
|
response: {
|
||||||
|
201: InviteSchema.extend({
|
||||||
|
roles: z.array(RoleSchema.pick({ id: true, name: true })),
|
||||||
}),
|
}),
|
||||||
properties: {
|
|
||||||
email: { type: 'string' },
|
|
||||||
roles: { type: 'array', items: { type: 'integer' } },
|
|
||||||
},
|
|
||||||
required: ['email', 'roles'],
|
|
||||||
},
|
},
|
||||||
response: { 201: InviteSchema },
|
|
||||||
},
|
},
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
const trx = await knex.transaction()
|
const trx = await db.startTransaction().execute()
|
||||||
|
|
||||||
const invite = await queries.create(
|
const token = generateToken()
|
||||||
{
|
|
||||||
...request.body,
|
const invite = await trx
|
||||||
createdById: request.session.userId,
|
.insertInto('invite')
|
||||||
},
|
.values({ email: request.body.email, token, createdById: request.session.userId })
|
||||||
trx,
|
.returningAll()
|
||||||
)
|
.executeTakeFirstOrThrow()
|
||||||
|
|
||||||
|
await trx
|
||||||
|
.insertInto('invites_roles')
|
||||||
|
.values(
|
||||||
|
request.body.roles.map((roleId) => ({
|
||||||
|
inviteId: invite.id,
|
||||||
|
roleId,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.execute()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const roles = await trx
|
||||||
|
.selectFrom('role as r')
|
||||||
|
.select(['id', 'name'])
|
||||||
|
.innerJoin('invites_roles as ir', 'ir.roleId', 'r.id')
|
||||||
|
.where('ir.inviteId', '=', invite.id)
|
||||||
|
.execute()
|
||||||
const link = `${new URL(config.auth.paths.register, config.site.url)}?email=${invite.email}&token=${invite.token}`
|
const link = `${new URL(config.auth.paths.register, config.site.url)}?email=${invite.email}&token=${invite.token}`
|
||||||
|
|
||||||
await sendMail({
|
await sendMail({
|
||||||
@ -77,11 +92,17 @@ const invitesPlugin: FastifyPluginCallbackTypebox = (fastify, _option, done) =>
|
|||||||
html: await inviteEmailTemplate({ link }).text(),
|
html: await inviteEmailTemplate({ link }).text(),
|
||||||
})
|
})
|
||||||
|
|
||||||
await trx.commit()
|
await trx.commit().execute()
|
||||||
|
|
||||||
return reply.header('Location', `${request.url}/${invite.id}`).status(201).send(invite)
|
return reply
|
||||||
|
.header('Location', `${request.url}/${invite.id}`)
|
||||||
|
.status(201)
|
||||||
|
.send({
|
||||||
|
...invite,
|
||||||
|
roles,
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await trx.rollback()
|
await trx.rollback().execute()
|
||||||
|
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
@ -92,16 +113,18 @@ const invitesPlugin: FastifyPluginCallbackTypebox = (fastify, _option, done) =>
|
|||||||
url: '/:id',
|
url: '/:id',
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
schema: {
|
schema: {
|
||||||
params: Type.Object({
|
params: z.object({
|
||||||
id: Type.Number(),
|
id: z.number(),
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
204: {},
|
204: {},
|
||||||
404: {},
|
404: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
handler(request, reply) {
|
async handler(request, reply) {
|
||||||
return queries.removeById(request.params.id).then((count) => reply.status(count > 0 ? 204 : 404).send())
|
const result = await db.deleteFrom('invite').where('id', '=', request.params.id).executeTakeFirstOrThrow()
|
||||||
|
|
||||||
|
return reply.status(result.numDeletedRows > 0 ? 204 : 404).send()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,16 +1,17 @@
|
|||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { Type, type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox'
|
import * as z from 'zod'
|
||||||
|
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
|
||||||
import knex from '../../lib/knex.ts'
|
import knex from '../../lib/knex.ts'
|
||||||
import StatusError from '../../lib/status_error.ts'
|
import StatusError from '../../lib/status_error.ts'
|
||||||
|
|
||||||
const invoiceRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
|
const invoiceRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
|
||||||
fastify.route({
|
fastify.route({
|
||||||
url: '/',
|
url: '/',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
schema: {
|
schema: {
|
||||||
querystring: Type.Object({
|
querystring: z.object({
|
||||||
year: Type.Optional(Type.Number()),
|
year: z.number().optional(),
|
||||||
supplier: Type.Optional(Type.Number()),
|
supplier: z.number().optional(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
async handler(req) {
|
async handler(req) {
|
||||||
@ -53,9 +54,9 @@ const invoiceRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
|
|||||||
url: '/total-amount',
|
url: '/total-amount',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
schema: {
|
schema: {
|
||||||
querystring: Type.Object({
|
querystring: z.object({
|
||||||
year: Type.Optional(Type.Number()),
|
year: z.number().optional(),
|
||||||
supplier: Type.Optional(Type.Number()),
|
supplier: z.number().optional(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
async handler(req) {
|
async handler(req) {
|
||||||
@ -80,8 +81,8 @@ const invoiceRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
|
|||||||
url: '/:id',
|
url: '/:id',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
schema: {
|
schema: {
|
||||||
params: Type.Object({
|
params: z.object({
|
||||||
id: Type.Number(),
|
id: z.number(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
handler(req) {
|
handler(req) {
|
||||||
@ -110,8 +111,8 @@ const invoiceRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
|
|||||||
url: '/by-supplier/:supplier',
|
url: '/by-supplier/:supplier',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
schema: {
|
schema: {
|
||||||
params: Type.Object({
|
params: z.object({
|
||||||
supplier: Type.Number(),
|
supplier: z.number(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
handler(req) {
|
handler(req) {
|
||||||
@ -136,8 +137,8 @@ const invoiceRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
|
|||||||
url: '/by-year/:year',
|
url: '/by-year/:year',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
schema: {
|
schema: {
|
||||||
params: Type.Object({
|
params: z.object({
|
||||||
year: Type.Number(),
|
year: z.number(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
async handler(req) {
|
async handler(req) {
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox'
|
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
|
||||||
import knex from '../../lib/knex.ts'
|
import knex from '../../lib/knex.ts'
|
||||||
|
|
||||||
const journalRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
|
const journalRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
|
||||||
fastify.route({
|
fastify.route({
|
||||||
url: '/',
|
url: '/',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { Type, type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox'
|
import * as z from 'zod'
|
||||||
|
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
|
||||||
import knex from '../../lib/knex.ts'
|
import knex from '../../lib/knex.ts'
|
||||||
|
|
||||||
const objectRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
|
const objectRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
|
||||||
fastify.route({
|
fastify.route({
|
||||||
url: '/',
|
url: '/',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@ -19,8 +20,8 @@ const objectRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
|
|||||||
url: '/:id',
|
url: '/:id',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
schema: {
|
schema: {
|
||||||
params: Type.Object({
|
params: z.object({
|
||||||
id: Type.Number(),
|
id: z.number(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
async handler(req) {
|
async handler(req) {
|
||||||
|
|||||||
@ -1,55 +1,56 @@
|
|||||||
import process from 'node:process'
|
import process from 'node:process'
|
||||||
import os from 'node:os'
|
import os from 'node:os'
|
||||||
|
import * as z from 'zod'
|
||||||
|
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
|
||||||
import env from '../../env.ts'
|
import env from '../../env.ts'
|
||||||
import { Type, type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox'
|
|
||||||
|
|
||||||
const ProcessSchema = Type.Object({
|
const ProcessSchema = z.object({
|
||||||
env: Type.String(),
|
env: z.string(),
|
||||||
version: Type.String(),
|
version: z.string(),
|
||||||
uptime: Type.Number(),
|
uptime: z.number(),
|
||||||
memoryUsage: Type.Object({
|
memoryUsage: z.object({
|
||||||
rss: Type.Number(),
|
rss: z.number(),
|
||||||
heapTotal: Type.Number(),
|
heapTotal: z.number(),
|
||||||
heapUsed: Type.Number(),
|
heapUsed: z.number(),
|
||||||
external: Type.Number(),
|
external: z.number(),
|
||||||
arrayBuffers: Type.Number(),
|
arrayBuffers: z.number(),
|
||||||
}),
|
}),
|
||||||
resourceUsage: Type.Object({
|
resourceUsage: z.object({
|
||||||
userCPUTime: Type.Number(),
|
userCPUTime: z.number(),
|
||||||
systemCPUTime: Type.Number(),
|
systemCPUTime: z.number(),
|
||||||
maxRSS: Type.Number(),
|
maxRSS: z.number(),
|
||||||
sharedMemorySize: Type.Number(),
|
sharedMemorySize: z.number(),
|
||||||
unsharedDataSize: Type.Number(),
|
unsharedDataSize: z.number(),
|
||||||
unsharedStackSize: Type.Number(),
|
unsharedStackSize: z.number(),
|
||||||
minorPageFault: Type.Number(),
|
minorPageFault: z.number(),
|
||||||
majorPageFault: Type.Number(),
|
majorPageFault: z.number(),
|
||||||
swappedOut: Type.Number(),
|
swappedOut: z.number(),
|
||||||
fsRead: Type.Number(),
|
fsRead: z.number(),
|
||||||
fsWrite: Type.Number(),
|
fsWrite: z.number(),
|
||||||
ipcSent: Type.Number(),
|
ipcSent: z.number(),
|
||||||
ipcReceived: Type.Number(),
|
ipcReceived: z.number(),
|
||||||
signalsCount: Type.Number(),
|
signalsCount: z.number(),
|
||||||
voluntaryContextSwitches: Type.Number(),
|
voluntaryContextSwitches: z.number(),
|
||||||
involuntaryContextSwitches: Type.Number(),
|
involuntaryContextSwitches: z.number(),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const OSSchema = Type.Object({
|
const OSSchema = z.object({
|
||||||
freemem: Type.Number(),
|
freemem: z.number(),
|
||||||
totalmem: Type.Number(),
|
totalmem: z.number(),
|
||||||
arch: Type.String(),
|
arch: z.string(),
|
||||||
homedir: Type.String(),
|
homedir: z.string(),
|
||||||
hostname: Type.String(),
|
hostname: z.string(),
|
||||||
loadavg: Type.Array(Type.Number()),
|
loadavg: z.array(z.number()),
|
||||||
machine: Type.String(),
|
machine: z.string(),
|
||||||
platform: Type.String(),
|
platform: z.string(),
|
||||||
release: Type.String(),
|
release: z.string(),
|
||||||
type: Type.String(),
|
type: z.string(),
|
||||||
uptime: Type.Number(),
|
uptime: z.number(),
|
||||||
version: Type.String(),
|
version: z.string(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const processPlugin: FastifyPluginCallbackTypebox = (fastify, _options, done) => {
|
const processPlugin: FastifyPluginCallbackZod = (fastify, _options, done) => {
|
||||||
fastify.addHook('onRequest', fastify.auth)
|
fastify.addHook('onRequest', fastify.auth)
|
||||||
|
|
||||||
fastify.route({
|
fastify.route({
|
||||||
@ -57,7 +58,7 @@ const processPlugin: FastifyPluginCallbackTypebox = (fastify, _options, done) =>
|
|||||||
url: '/',
|
url: '/',
|
||||||
schema: {
|
schema: {
|
||||||
response: {
|
response: {
|
||||||
200: Type.Object({
|
200: z.object({
|
||||||
process: ProcessSchema,
|
process: ProcessSchema,
|
||||||
os: OSSchema,
|
os: OSSchema,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { Type, type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox'
|
import * as z from 'zod'
|
||||||
|
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
|
||||||
import knex from '../../lib/knex.ts'
|
import knex from '../../lib/knex.ts'
|
||||||
|
|
||||||
const resultRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
|
const resultRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
|
||||||
fastify.route({
|
fastify.route({
|
||||||
url: '/',
|
url: '/',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@ -69,8 +70,8 @@ const resultRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
|
|||||||
url: '/:year',
|
url: '/:year',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
schema: {
|
schema: {
|
||||||
params: Type.Object({
|
params: z.object({
|
||||||
year: Type.Number(),
|
year: z.number(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
async handler(req) {
|
async handler(req) {
|
||||||
|
|||||||
@ -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 _ from 'lodash'
|
||||||
import * as z from 'zod'
|
import * as z from 'zod'
|
||||||
import { type FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
|
import { type FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
|
||||||
// import knex from '../../lib/knex.ts'
|
|
||||||
// import Queries from '../../services/roles/queries.ts'
|
|
||||||
|
|
||||||
import { RoleSchema } from '../../schemas/db.ts'
|
import { RoleSchema } from '../../schemas/db.ts'
|
||||||
|
import { jsonObjectFrom } from 'kysely/helpers/postgres'
|
||||||
// export const RoleFullSchema = Type.Object({
|
|
||||||
// id: Type.Number(),
|
|
||||||
// name: Type.String(),
|
|
||||||
// createdAt: Type.String(),
|
|
||||||
// createdById: Type.Number(),
|
|
||||||
// modifiedAt: Type.String(),
|
|
||||||
// modifiedById: Type.Number(),
|
|
||||||
// })
|
|
||||||
|
|
||||||
// console.log(RoleFullSchema)
|
|
||||||
|
|
||||||
// export const RoleSchema = Type.Pick(RoleFullSchema, ['id', 'name'])
|
|
||||||
|
|
||||||
// export const RoleVariableSchema = Type.Pick(RoleFullSchema, ['name', 'createdById'])
|
|
||||||
|
|
||||||
const rolesPlugin: FastifyPluginCallbackZod = (fastify, _options, done) => {
|
const rolesPlugin: FastifyPluginCallbackZod = (fastify, _options, done) => {
|
||||||
// const queries = Queries({ knex })
|
|
||||||
const { db } = fastify
|
const { db } = fastify
|
||||||
|
|
||||||
// fastify.addHook('onRequest', fastify.auth)
|
fastify.addHook('onRequest', fastify.auth)
|
||||||
|
|
||||||
fastify.route({
|
fastify.route({
|
||||||
url: '/',
|
url: '/',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
schema: {
|
schema: {
|
||||||
querystring: RoleSchema.partial().extend({
|
|
||||||
limit: z.number().optional(),
|
|
||||||
sort: z.keyof(RoleSchema).optional(),
|
|
||||||
offset: z.number().optional(),
|
|
||||||
}),
|
|
||||||
response: {
|
response: {
|
||||||
200: z.array(RoleSchema),
|
200: z.array(
|
||||||
|
RoleSchema.extend({
|
||||||
|
createdBy: z.object({
|
||||||
|
id: z.number(),
|
||||||
|
email: z.string(),
|
||||||
|
}),
|
||||||
|
modifiedBy: z
|
||||||
|
.object({
|
||||||
|
id: z.number(),
|
||||||
|
email: z.string(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
handler(request) {
|
handler() {
|
||||||
// if (!client) {
|
return db
|
||||||
// // TODO figure out if better, eg instanceof, check is possible
|
.selectFrom('role as r')
|
||||||
// if (select && select.andWhereNotBetween) {
|
.selectAll()
|
||||||
// client = select
|
.select((eb) => [
|
||||||
// select = null
|
jsonObjectFrom(eb.selectFrom('user as u').select(['u.id', 'u.email']).whereRef('u.id', '=', 'r.createdById'))
|
||||||
// } else {
|
.$notNull()
|
||||||
// client = kysely
|
.as('createdBy'),
|
||||||
// }
|
jsonObjectFrom(
|
||||||
// }
|
eb.selectFrom('user as u').select(['u.id', 'u.email']).whereRef('u.id', '=', 'r.modifiedById'),
|
||||||
|
).as('modifiedBy'),
|
||||||
const { offset, limit, sort, ...query } = request.query
|
])
|
||||||
|
.execute()
|
||||||
let builder = db.selectFrom('role').selectAll()
|
|
||||||
|
|
||||||
// if (!_.isEmpty(query)) {
|
|
||||||
// builder = where(builder, _.pick(query, columns))
|
|
||||||
// }
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(query)) {
|
|
||||||
if (value === null) {
|
|
||||||
builder = builder.where(key, 'is', null)
|
|
||||||
} else if (Array.isArray(value)) {
|
|
||||||
builder = builder.where(key, 'in', value)
|
|
||||||
} else {
|
|
||||||
builder = builder.where(key, '=', value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
builder = builder.orderBy(sort || 'id', sort?.startsWith('-') ? 'desc' : 'asc')
|
|
||||||
|
|
||||||
if (limit) {
|
|
||||||
builder = builder.limit(limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (offset) {
|
|
||||||
builder = builder.offset(offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.execute()
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// fastify.route({
|
fastify.route({
|
||||||
// url: '/',
|
url: '/',
|
||||||
// method: 'POST',
|
method: 'POST',
|
||||||
// schema: {
|
schema: {
|
||||||
// body: RoleVariableSchema,
|
body: RoleSchema.pick({ name: true }),
|
||||||
// response: {
|
response: {
|
||||||
// 201: RoleFullSchema,
|
201: RoleSchema,
|
||||||
// },
|
},
|
||||||
// },
|
},
|
||||||
// async handler(request, reply) {
|
handler(request) {
|
||||||
// const newRole = request.session.userId
|
return db
|
||||||
// ? {
|
.insertInto('role')
|
||||||
// ...request.body,
|
.values({
|
||||||
// createdById: request.session.userId,
|
...request.body,
|
||||||
// }
|
createdById: request.session.userId,
|
||||||
// : request.body
|
})
|
||||||
|
.returningAll()
|
||||||
|
.executeTakeFirstOrThrow()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
// return queries.create(newRole).then((row) => {
|
fastify.route({
|
||||||
// return reply.header('Location', `${request.url}/${row.id}`).status(201).send(row)
|
url: '/:id',
|
||||||
// })
|
method: 'DELETE',
|
||||||
// },
|
schema: {
|
||||||
// })
|
params: z.object({
|
||||||
|
id: z.number(),
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
204: {},
|
||||||
|
404: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler(request) {
|
||||||
|
return db.deleteFrom('role').where('id', '=', request.params.id).execute()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
// fastify.route({
|
fastify.route({
|
||||||
// url: '/:id',
|
url: '/:id',
|
||||||
// method: 'DELETE',
|
method: 'PATCH',
|
||||||
// schema: {
|
schema: {
|
||||||
// params: Type.Object({
|
params: z.object({
|
||||||
// id: Type.Number(),
|
id: z.number(),
|
||||||
// }),
|
}),
|
||||||
// response: {
|
body: RoleSchema.pick({ name: true }),
|
||||||
// 204: {},
|
response: {
|
||||||
// 404: {},
|
204: {},
|
||||||
// },
|
},
|
||||||
// },
|
},
|
||||||
// handler(request, reply) {
|
async handler(request) {
|
||||||
// return queries.removeById(request.params.id).then((count) => reply.status(count > 0 ? 204 : 404).send())
|
return db
|
||||||
// },
|
.updateTable('role')
|
||||||
// })
|
.set({
|
||||||
|
...request.body,
|
||||||
// fastify.route({
|
modifiedById: request.session.userId,
|
||||||
// url: '/:id',
|
})
|
||||||
// method: 'PATCH',
|
.where('id', '=', request.params.id)
|
||||||
// schema: {
|
.returningAll()
|
||||||
// params: Type.Object({
|
.execute()
|
||||||
// id: Type.Number(),
|
},
|
||||||
// }),
|
})
|
||||||
// body: RoleVariableSchema,
|
|
||||||
// response: {
|
|
||||||
// 204: {},
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// async handler(request) {
|
|
||||||
// const patch = request.session.userId
|
|
||||||
// ? {
|
|
||||||
// ...request.body,
|
|
||||||
// modifiedById: request.session.userId,
|
|
||||||
// }
|
|
||||||
// : request.body
|
|
||||||
|
|
||||||
// return queries.update(request.params.id, patch)
|
|
||||||
// },
|
|
||||||
// })
|
|
||||||
|
|
||||||
done()
|
done()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
|
|
||||||
import z from 'zod'
|
import z from 'zod'
|
||||||
|
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
|
||||||
import { SupplierSchema } from '../../schemas/db.ts'
|
import { SupplierSchema } from '../../schemas/db.ts'
|
||||||
|
|
||||||
const supplierRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
|
const supplierRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import * as z from 'zod'
|
import * as z from 'zod'
|
||||||
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
|
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
|
||||||
import StatusError from '../../lib/status_error.ts'
|
import { paginate } from '../../lib/kysely_helpers.ts'
|
||||||
|
|
||||||
const transactionRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
|
const transactionRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
|
||||||
const { db } = fastify
|
const { db } = fastify
|
||||||
@ -13,31 +13,21 @@ const transactionRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
|
|||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
year: z.optional(z.coerce.number()),
|
year: z.optional(z.coerce.number()),
|
||||||
accountNumber: z.optional(z.coerce.number()),
|
accountNumber: z.optional(z.coerce.number()),
|
||||||
|
limit: z.number().default(2),
|
||||||
|
sort: z.string().default('t.id'),
|
||||||
|
offset: z.number().optional(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
async handler(req) {
|
async handler(request) {
|
||||||
const query: { financialYearId?: number; accountNumber?: number } = {}
|
const { offset, limit, sort: _sort, ...where } = request.query
|
||||||
|
|
||||||
if (req.query.year) {
|
const baseQuery = db
|
||||||
const year = await db
|
|
||||||
.selectFrom('financialYear')
|
|
||||||
.selectAll()
|
|
||||||
.where('year', '=', req.query.year)
|
|
||||||
.executeTakeFirst()
|
|
||||||
|
|
||||||
if (!year) throw new StatusError(404, `Year ${req.query.year} not found.`)
|
|
||||||
|
|
||||||
query.financialYearId = year.id
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.query.accountNumber) {
|
|
||||||
query.accountNumber = req.query.accountNumber
|
|
||||||
}
|
|
||||||
|
|
||||||
return db
|
|
||||||
.selectFrom('transaction as t')
|
.selectFrom('transaction as t')
|
||||||
.innerJoin('entry as e', 't.entryId', 'e.id')
|
.innerJoin('entry as e', 't.entryId', 'e.id')
|
||||||
.select([
|
.innerJoin('financialYear as fy', 'e.financialYearId', 'fy.id')
|
||||||
|
|
||||||
|
const result = await paginate(
|
||||||
|
baseQuery.select([
|
||||||
't.accountNumber',
|
't.accountNumber',
|
||||||
'e.transactionDate',
|
'e.transactionDate',
|
||||||
't.entryId',
|
't.entryId',
|
||||||
@ -45,9 +35,17 @@ const transactionRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
|
|||||||
't.description',
|
't.description',
|
||||||
't.invoiceId',
|
't.invoiceId',
|
||||||
'e.description as entryDescription',
|
'e.description as entryDescription',
|
||||||
])
|
]),
|
||||||
.where((eb) => eb.and(query))
|
baseQuery,
|
||||||
.execute()
|
{
|
||||||
|
// @ts-ignore
|
||||||
|
where,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox'
|
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
|
||||||
import knex from '../../lib/knex.ts'
|
import knex from '../../lib/knex.ts'
|
||||||
import emitter from '../../lib/emitter.ts'
|
import emitter from '../../lib/emitter.ts'
|
||||||
import Queries from '../../services/users/queries.ts'
|
import Queries from '../../services/users/queries.ts'
|
||||||
|
|
||||||
const usersPlugin: FastifyPluginCallbackTypebox<{ addParentSchema: (schema: ANY) => void }> = (
|
const usersPlugin: FastifyPluginCallbackZod<{ addParentSchema: (schema: ANY) => void }> = (
|
||||||
fastify,
|
fastify,
|
||||||
{ addParentSchema },
|
{ addParentSchema },
|
||||||
done,
|
done,
|
||||||
|
|||||||
@ -1,11 +1,62 @@
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
export const AccountSchema = z.object({
|
||||||
|
id: z.number().int().optional(),
|
||||||
|
number: z.int(),
|
||||||
|
financialYearId: z.number().int(),
|
||||||
|
description: z.string(),
|
||||||
|
sru: z.int().nullable().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const AdmissionSchema = z.object({
|
||||||
|
id: z.number().int().optional(),
|
||||||
|
regex: z.string(),
|
||||||
|
createdAt: z.date().nullable().optional(),
|
||||||
|
createdById: z.number().int(),
|
||||||
|
modifiedAt: z.date().nullable().optional(),
|
||||||
|
modifiedById: z.number().int().nullable().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const AccountBalanceSchema = z.object({
|
||||||
|
accountNumber: z.number().int(),
|
||||||
|
financialYearId: z.number().int(),
|
||||||
|
in: z.number().optional(),
|
||||||
|
out: z.number().optional(),
|
||||||
|
inQuantity: z.number().int().nullable().optional(),
|
||||||
|
outQuantity: z.number().int().nullable().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const InviteSchema = z.object({
|
||||||
|
id: z.number().int().optional(),
|
||||||
|
email: z.string(),
|
||||||
|
token: z.string(),
|
||||||
|
createdAt: z.date().optional(),
|
||||||
|
createdById: z.number().int().nullable().optional(),
|
||||||
|
modifiedAt: z.date().nullable().optional(),
|
||||||
|
modifiedById: z.number().int().nullable().optional(),
|
||||||
|
consumedAt: z.date().nullable().optional(),
|
||||||
|
consumedById: z.number().int().nullable().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const InvoiceSchema = z.object({
|
||||||
|
id: z.number().int().optional(),
|
||||||
|
financialYearId: z.number().int().nullable().optional(),
|
||||||
|
supplierId: z.number().int(),
|
||||||
|
fiskenNumber: z.number().int().nullable().optional(),
|
||||||
|
phmNumber: z.number().int().nullable().optional(),
|
||||||
|
invoiceNumber: z.string().nullable().optional(),
|
||||||
|
invoiceDate: z.string().nullable().optional(),
|
||||||
|
dueDate: z.string().nullable().optional(),
|
||||||
|
ocr: z.string().nullable().optional(),
|
||||||
|
amount: z.number().nullable().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
export const RoleSchema = z.object({
|
export const RoleSchema = z.object({
|
||||||
id: z.number().int().optional(),
|
id: z.number().int().optional(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
createdAt: z.string().optional(),
|
createdAt: z.date().optional(),
|
||||||
createdById: z.number().int().nullable().optional(),
|
createdById: z.number().int().nullable().optional(),
|
||||||
modifiedAt: z.string().nullable().optional(),
|
modifiedAt: z.date().nullable().optional(),
|
||||||
modifiedById: z.number().int().nullable().optional(),
|
modifiedById: z.number().int().nullable().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
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 {
|
interface FastifyRequest {
|
||||||
logout: () => void
|
logout: () => Promise<void>
|
||||||
login: (user: ANY) => Promise<void>
|
login: (user: ANY) => Promise<void>
|
||||||
user: Promise<ANY>
|
user: Promise<ANY>
|
||||||
getUser: () => Promise<ANY>
|
getUser: () => Promise<ANY>
|
||||||
|
|||||||
@ -133,7 +133,7 @@ export interface Invite {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface InvitesRoles {
|
export interface InvitesRoles {
|
||||||
invitedId: number
|
inviteId: number
|
||||||
roleId: number
|
roleId: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user