Compare commits

...

2 commits

Author SHA1 Message Date
37e4cdda0b
feat: move to trpc API 2024-01-26 20:53:17 +01:00
eb2187738b
feat: add hidden patients 2024-01-24 23:22:05 +01:00
39 changed files with 1049 additions and 980 deletions

View file

@ -17,42 +17,44 @@
"test:e2e": "playwright test"
},
"dependencies": {
"@auth/core": "^0.18.4",
"@auth/sveltekit": "^0.5.0",
"@mdi/js": "^7.3.67",
"@prisma/client": "^5.7.0",
"@auth/core": "^0.18.6",
"@auth/sveltekit": "^0.5.3",
"@mdi/js": "^7.4.47",
"@prisma/client": "^5.8.1",
"zod": "^3.22.4"
},
"devDependencies": {
"@faker-js/faker": "^8.3.1",
"@playwright/test": "^1.40.1",
"@sveltejs/adapter-node": "^2.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@faker-js/faker": "^8.4.0",
"@playwright/test": "^1.41.1",
"@sveltejs/adapter-node": "^2.1.2",
"@sveltejs/kit": "^2.5.0",
"@sveltejs/vite-plugin-svelte": "^3.0.1",
"@tailwindcss/typography": "^0.5.10",
"@types/node": "^20.10.5",
"@typescript-eslint/eslint-plugin": "^6.13.2",
"@typescript-eslint/parser": "^6.13.2",
"autoprefixer": "^10.4.16",
"daisyui": "^4.4.19",
"eslint": "^8.55.0",
"@trpc/client": "^10.45.0",
"@trpc/server": "^10.45.0",
"@types/node": "^20.11.7",
"@typescript-eslint/eslint-plugin": "^6.19.1",
"@typescript-eslint/parser": "^6.19.1",
"autoprefixer": "^10.4.17",
"daisyui": "^4.6.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-no-relative-import-paths": "^1.5.3",
"eslint-plugin-svelte": "^2.35.1",
"postcss-import": "^15.1.0",
"postcss-nesting": "^12.0.1",
"prettier": "^3.1.0",
"postcss-nesting": "^12.0.2",
"prettier": "^3.2.4",
"prettier-plugin-svelte": "^3.1.2",
"prisma": "^5.7.0",
"svelte": "^4.2.8",
"svelte-check": "^3.6.2",
"sveltekit-zero-api": "^0.15.7",
"tailwindcss": "^3.3.6",
"prisma": "^5.8.1",
"svelte": "^4.2.9",
"svelte-check": "^3.6.3",
"tailwindcss": "^3.4.1",
"trpc-sveltekit": "^3.5.22",
"tslib": "^2.6.2",
"tsx": "^4.7.0",
"typescript": "^5.3.3",
"vite": "^5.0.0",
"vitest": "^1.0.0"
"vite": "^5.0.12",
"vitest": "^1.2.2"
},
"type": "module"
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "patients"
ADD COLUMN "hidden" BOOLEAN NOT NULL DEFAULT FALSE;

View file

@ -14,7 +14,7 @@ datasource db {
model Account {
id Int @id @default(autoincrement())
user_id Int @map("userId")
user_id Int @map("userId")
type String
provider String
providerAccountId String
@ -74,9 +74,13 @@ model Patient {
room Room? @relation(fields: [room_id], references: [id], onDelete: SetNull)
room_id Int?
Entry Entry[]
hidden Boolean @default(false)
created_at DateTime @default(now())
full_name String? @default(dbgenerated("((first_name || ' '::text) || last_name)")) @ignore
@@index([full_name(ops: raw("gin_trgm_ops"))], map: "patients_full_name", type: Gin)
@@map("patients")
}
@ -101,6 +105,8 @@ model Entry {
created_at DateTime @default(now())
tsvec Unsupported("tsvector")?
@@map("entries")
}

View file

@ -1,9 +0,0 @@
import { createZeroApi } from "sveltekit-zero-api/api";
import type { GeneratedAPI } from "./sveltekit-zero-api";
const routes = createZeroApi({
// eslint-disable-next-line no-console
onError: async (err) => console.error("[API]", err),
}) as GeneratedAPI;
export default routes.api;

View file

@ -1,5 +1,3 @@
import { prisma } from "$lib/server/prisma";
import { PrismaAdapter } from "$lib/server/authAdapter";
import { SvelteKitAuth } from "@auth/sveltekit";
import Keycloak from "@auth/core/providers/keycloak";
import {
@ -9,6 +7,12 @@ import {
} from "$env/static/private";
import { redirect, type Handle } from "@sveltejs/kit";
import { sequence } from "@sveltejs/kit/hooks";
import { createTRPCHandle } from "trpc-sveltekit";
import { prisma } from "$lib/server/prisma";
import { PrismaAdapter } from "$lib/server/authAdapter";
import { createContext } from "$lib/server/trpc/context";
import { router } from "$lib/server/trpc/router";
/**
* Protect the application against unauthorized access.
@ -41,5 +45,6 @@ export const handle = sequence(
strategy: "jwt",
},
}),
authorization
authorization,
createTRPCHandle({ router, createContext })
);

View file

@ -25,7 +25,7 @@ function mapAccount(account: Account): AdapterAccount {
refresh_token: account.refresh_token ?? undefined,
access_token: account.access_token ?? undefined,
expires_at: account.expires_at ?? undefined,
token_type: account.token_type ?? undefined,
token_type: (account.token_type as Lowercase<string>) ?? undefined,
scope: account.scope ?? undefined,
id_token: account.id_token ?? undefined,
};

View file

@ -1,3 +1,4 @@
/*
import { ErrorConflict, ErrorNotFound } from "$lib/shared/util/error";
import { ZodError } from "zod";
import {
@ -67,3 +68,4 @@ export async function apiWrapUser<T>(
return handleError(error);
}
}
*/

View file

@ -2,6 +2,7 @@ import { prisma } from "$lib/server/prisma";
import type {
EntriesFilter,
Entry,
EntryExecution,
EntryExecutionNew,
EntryNew,
EntryVersion,
@ -10,7 +11,7 @@ import type {
PaginationRequest,
} from "$lib/shared/model";
import { ErrorConflict } from "$lib/shared/util/error";
import { mapEntry, mapVersion } from "./mapping";
import { mapEntry, mapVersion, mapExecution } from "./mapping";
import { QueryBuilder, parseSearchQuery } from "./util";
const USER_SELECT = { select: { id: true, name: true } };
@ -36,14 +37,22 @@ export async function getEntry(id: number): Promise<Entry> {
return mapEntry(entry);
}
export async function getEntryHistory(id: number): Promise<EntryVersion[]> {
const entries = await prisma.entryVersion.findMany({
export async function getEntryVersions(id: number): Promise<EntryVersion[]> {
const versions = await prisma.entryVersion.findMany({
where: { entry_id: id },
include: { author: USER_SELECT, category: true },
orderBy: { created_at: "desc" },
});
return versions.map(mapVersion);
}
return entries.map(mapVersion);
export async function getEntryExecutions(id: number): Promise<EntryExecution[]> {
const executions = await prisma.entryExecution.findMany({
where: { entry_id: id },
include: { author: USER_SELECT },
orderBy: { created_at: "desc" },
});
return executions.map(mapExecution);
}
export async function newEntry(author_id: number, entry: EntryNew): Promise<number> {
@ -65,7 +74,8 @@ export async function newEntry(author_id: number, entry: EntryNew): Promise<numb
export async function newEntryVersion(
author_id: number,
entry_id: number,
version: EntryVersionNew
version: EntryVersionNew,
old_version_id: number | undefined = undefined
): Promise<number> {
return prisma.$transaction(async (tx) => {
const entry = await tx.entry.findUniqueOrThrow({
@ -80,7 +90,7 @@ export async function newEntryVersion(
const cver = entry.EntryVersion[0];
// Check if the entry has been updated by someone else
if (version.old_version && (!cver || cver.id !== version.old_version)) {
if (old_version_id && (!cver || cver.id !== old_version_id)) {
throw new ErrorConflict("old version id does not match");
}
@ -103,7 +113,8 @@ export async function newEntryVersion(
export async function newEntryExecution(
author_id: number,
entry_id: number,
execution: EntryExecutionNew
execution: EntryExecutionNew,
old_execution_id: number | null | undefined = undefined
): Promise<number> {
return prisma.$transaction(async (tx) => {
const entry = await tx.entry.findUniqueOrThrow({
@ -119,8 +130,8 @@ export async function newEntryExecution(
// Check if the execution has been updated by someone else
if (
(execution.old_execution && (!cex || cex.id !== execution.old_execution)) ||
(execution.old_execution === null && cex)
(old_execution_id && (!cex || cex.id !== old_execution_id)) ||
(old_execution_id === null && cex)
) {
throw new ErrorConflict("old execution id does not match");
}
@ -164,6 +175,7 @@ export async function getEntries(
p.first_name as patient_first_name,
p.last_name as patient_last_name,
p.age as patient_age,
p.hidden as patient_hidden,
p.created_at as patient_created_at,
r.id as room_id,
r.name as room_name,
@ -259,6 +271,7 @@ join stations s on s.id = r.station_id`
patient_first_name: string;
patient_last_name: string;
patient_age: number;
patient_hidden: boolean;
patient_created_at: Date;
room_id: number;
room_name: string;
@ -281,6 +294,7 @@ join stations s on s.id = r.station_id`
last_name: item.patient_last_name,
created_at: item.patient_created_at,
age: item.patient_age,
hidden: item.patient_hidden,
room: {
id: item.room_id,
name: item.room_name,

View file

@ -38,6 +38,7 @@ export function mapPatient(patient: DbPatientLn): Patient {
first_name: patient.first_name,
last_name: patient.last_name,
created_at: patient.created_at,
hidden: patient.hidden,
age: patient.age,
room: patient.room
? {
@ -73,7 +74,7 @@ export function mapVersion(version: DbEntryVersionLn): EntryVersion {
};
}
function mapExecution(execution: DbEntryExecutionLn): EntryExecution {
export function mapExecution(execution: DbEntryExecutionLn): EntryExecution {
return {
id: execution.id,
author: execution.author,

View file

@ -36,7 +36,10 @@ export async function deletePatient(id: number) {
}
}
// TODO: Hide a patient
/** Hide/show a patient */
export async function hidePatient(id: number, hidden: boolean) {
await prisma.patient.update({ where: { id }, data: { hidden } });
}
export async function getPatient(id: number): Promise<Patient> {
const patient = await prisma.patient.findUniqueOrThrow({
@ -53,7 +56,7 @@ export async function getPatients(
pagination: PaginationRequest
): Promise<Pagination<Patient>> {
const qb = new QueryBuilder(
`select p.id, p.first_name, p.last_name, p.created_at, p.age,
`select p.id, p.first_name, p.last_name, p.created_at, p.age, p.hidden,
r.id as room_id, r.name as room_name, s.id as station_id, s.name as station_name`,
`from patients p
join rooms r on r.id = p.room_id
@ -66,6 +69,10 @@ export async function getPatients(
qb.addOrderClause(`similarity(p.full_name, ${qvar}) desc`);
}
if (filter.hidden !== undefined) {
qb.addFilter("p.hidden", filter.hidden);
}
qb.addFilterList("r.id", filter.room);
qb.addFilterList("s.id", filter.station);
qb.addOrderClause("p.created_at desc");
@ -77,6 +84,7 @@ export async function getPatients(
last_name: string;
created_at: Date;
age: number;
hidden: boolean;
room_id: number;
room_name: string;
station_id: number;
@ -96,6 +104,7 @@ export async function getPatients(
last_name: patient.last_name,
age: patient.age,
created_at: patient.created_at,
hidden: patient.hidden,
room: {
id: patient.room_id,
name: patient.room_name,

View file

@ -0,0 +1,21 @@
import { ZUser } from "$lib/shared/model/validation";
import type { RequestEvent } from "@sveltejs/kit";
import { type inferAsyncReturnType, TRPCError } from "@trpc/server";
// we're not using the event parameter is this example,
// hence the eslint-disable rule
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export async function createContext(event: RequestEvent) {
const session = await event.locals.getSession();
if (!session?.user) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "no session" });
}
const user = ZUser.parse(session?.user);
return {
user,
};
}
export type Context = inferAsyncReturnType<typeof createContext>;

View file

@ -0,0 +1,4 @@
import { initTRPC } from "@trpc/server";
import type { Context } from "./context";
export const t = initTRPC.context<Context>().create();

View file

@ -0,0 +1,14 @@
import { t } from ".";
import { categoryRouter } from "./routes/category";
import { entryRouter } from "./routes/entry";
export const router = t.router({
greeting: t.procedure.query(async () => {
return `Hello tRPC v10 @ ${new Date().toLocaleTimeString()}`;
}),
category: categoryRouter,
entry: entryRouter,
});
export type Router = typeof router;

View file

@ -0,0 +1,26 @@
import { t } from "..";
import { z } from "zod";
import {
deleteCategory,
getCategories,
newCategory,
updateCategory,
} from "$lib/server/query";
import { ZCategoryNew, ZEntityId } from "$lib/shared/model/validation";
export const categoryRouter = t.router({
list: t.procedure.query(getCategories),
create: t.procedure.input(ZCategoryNew).mutation(async (opts) => {
const id = await newCategory(opts.input);
return { id };
}),
update: t.procedure
.input(z.object({ id: ZEntityId, category: ZCategoryNew.partial() }))
.mutation(async (opts) => {
await updateCategory(opts.input.id, opts.input.category);
}),
delete: t.procedure.input(ZEntityId).mutation(async (opts) => {
await deleteCategory(opts.input);
}),
});

View file

@ -0,0 +1,74 @@
import { t } from "..";
import { z } from "zod";
import {
ZEntityId,
ZEntriesFilter,
ZEntryExecutionNew,
ZEntryVersionNew,
ZPagination,
} from "$lib/shared/model/validation";
import {
getEntries,
getEntry,
getEntryExecutions,
getEntryVersions,
newEntryExecution,
newEntryVersion,
} from "$lib/server/query";
export const entryRouter = t.router({
get: t.procedure.input(ZEntityId).query(async (opts) => {
return getEntry(opts.input);
}),
list: t.procedure
.input(
z
.object({
filter: ZEntriesFilter,
pagination: ZPagination,
})
.partial()
)
.query(async (opts) => {
return getEntries(opts.input.filter || {}, opts.input.pagination || {});
}),
versions: t.procedure.input(ZEntityId).query(async (opts) => {
return getEntryVersions(opts.input);
}),
executions: t.procedure.input(ZEntityId).query(async (opts) => {
return getEntryExecutions(opts.input);
}),
newVersion: t.procedure
.input(
z.object({
id: ZEntityId,
version: ZEntryVersionNew,
old_version_id: ZEntityId.optional(),
})
)
.query(async (opts) => {
return newEntryVersion(
opts.ctx.user.id,
opts.input.id,
opts.input.version,
opts.input.old_version_id
);
}),
newExecution: t.procedure
.input(
z.object({
id: ZEntityId,
execution: ZEntryExecutionNew,
old_execution_id: ZEntityId.nullable().optional(),
})
)
.query(async (opts) => {
return newEntryExecution(
opts.ctx.user.id,
opts.input.id,
opts.input.execution,
opts.input.old_execution_id
);
}),
});

View file

@ -63,6 +63,7 @@ export type Patient = {
last_name: string;
age: Option<number>;
room: Option<Room>;
hidden: boolean;
created_at: Date;
};
@ -103,10 +104,7 @@ export type EntryVersionNdata = {
priority: boolean;
};
export type EntryVersionNew = Partial<{
old_version: number;
}> &
Partial<EntryVersionNdata>;
export type EntryVersionNew = Partial<EntryVersionNdata>;
export type EntryExecution = {
id: number;
@ -115,8 +113,6 @@ export type EntryExecution = {
created_at: Date;
};
export type EntryExecutionNew = Partial<{
old_execution: number | null;
}> & {
export type EntryExecutionNew = {
text: string;
};

View file

@ -21,4 +21,5 @@ export type PatientsFilter = Partial<{
search: string;
station: FilterList<number>;
room: FilterList<number>;
hidden: boolean;
}>;

View file

@ -1,25 +0,0 @@
import { expect, test } from "vitest";
import { ZEntriesFilterUrl, ZPatientsFilterUrl } from "./validation";
import type { EntriesFilter, PatientsFilter } from ".";
test("valid EntriesFilterUrl", () => {
const data = ZEntriesFilterUrl.parse({
done: "0",
});
expect(data).toStrictEqual({
done: false,
} satisfies EntriesFilter);
});
test("valid PatientsFilterUrl", () => {
const data = ZPatientsFilterUrl.parse({
room: "1;2;3;4",
station: "5",
});
expect(data).toStrictEqual({
room: [1, 2, 3, 4],
station: [5],
} satisfies PatientsFilter);
});

View file

@ -10,12 +10,19 @@ import type {
PatientNew,
RoomNew,
StationNew,
User,
} from ".";
const ZEntityId = z.number().int().nonnegative();
export const ZEntityId = z.number().int().nonnegative();
const ZNameString = z.string().min(1).max(200).trim();
const ZTextString = z.string().trim();
export const ZUser = implement<User>().with({
id: ZEntityId,
name: z.string().nullable(),
email: z.string().nullable(),
});
export const ZStationNew = implement<StationNew>().with({ name: ZNameString });
export const ZRoomNew = implement<RoomNew>().with({
@ -41,7 +48,6 @@ export const ZPatientNew = implement<PatientNew>().with({
});
export const ZEntryVersionNew = implement<EntryVersionNew>().with({
old_version: ZEntityId.optional(),
text: ZTextString.optional(),
date: z.date().optional(),
category_id: ZEntityId.optional().nullable(),
@ -59,41 +65,40 @@ export const ZEntryNew = implement<EntryNew>().with({
});
export const ZEntryExecutionNew = implement<EntryExecutionNew>().with({
old_execution: ZEntityId.optional().nullable(),
text: ZTextString,
});
// From URL
const ZFilterListUrl = z
.string()
.regex(/^\d+(;\d+)*$/)
.transform((s) => s.split(";").map(Number))
.optional();
export const ZEntityIdUrl = z.coerce.number().int().nonnegative();
export const BooleanUrl = z
.string()
.transform((s) => s !== "0" && s.toLowerCase() !== "false");
export const ZPaginationUrl = implement<PaginationRequest>().with({
limit: ZEntityIdUrl.optional(),
offset: ZEntityIdUrl.optional(),
export const ZPagination = implement<PaginationRequest>().with({
limit: ZEntityId.optional(),
offset: ZEntityId.optional(),
});
export const ZEntriesFilterUrl = z.object({
author: ZFilterListUrl,
category: ZFilterListUrl,
done: BooleanUrl.optional(),
executor: ZFilterListUrl,
patient: ZFilterListUrl,
priority: BooleanUrl.optional(),
room: ZFilterListUrl,
search: z.string().optional(),
station: ZFilterListUrl,
});
// const ZFilterList = z
// .string()
// .regex(/^\d+(;\d+)*$/)
// .transform((s) => s.split(";").map(Number))
// .optional();
const ZFilterList = z.array(ZEntityId).or(ZEntityId);
export const ZPatientsFilterUrl = z.object({
room: ZFilterListUrl,
station: ZFilterListUrl,
});
export const ZEntriesFilter = z
.object({
author: ZFilterList,
category: ZFilterList,
done: z.boolean(),
executor: ZFilterList,
patient: ZFilterList,
priority: z.boolean(),
room: ZFilterList,
search: z.string(),
station: ZFilterList,
})
.partial();
export const ZPatientsFilterUrl = z
.object({
search: z.string(),
room: ZFilterList,
station: ZFilterList,
hidden: z.boolean(),
})
.partial();

16
src/lib/shared/trpc.ts Normal file
View file

@ -0,0 +1,16 @@
import type { Router } from "$lib/server/trpc/router";
import { createTRPCClient, type TRPCClientInit } from "trpc-sveltekit";
let browserClient: ReturnType<typeof createTRPCClient<Router>>;
/** Get a new tRPC client
*
* This function switches between calling the tRPC server directly and using the TRPC
* HTTP client depending on the side it is called on. */
export function trpc(init?: TRPCClientInit) {
const isBrowser = typeof window !== "undefined";
if (isBrowser && browserClient) return browserClient;
const client = createTRPCClient<Router>({ init });
if (isBrowser) browserClient = client;
return client;
}

View file

@ -1,10 +1,10 @@
<script lang="ts">
import api from "$api";
import type { PageData } from "./$types";
api.category
.id$(1)
.GET()
.Ok((resp) => console.log(resp.body));
export let data: PageData;
</script>
<h1 class="text-4xl">Planung</h1>
<p>{data.greeting}</p>
<p>{JSON.stringify(data.categories)}</p>

View file

@ -0,0 +1,11 @@
import { trpc } from "$lib/shared/trpc";
import type { PageLoad } from "./$types";
export const load: PageLoad = async (event) => {
const [greeting, categories] = await Promise.all([
trpc(event).greeting.query(),
trpc(event).category.list.query(),
]);
return { greeting, categories };
};

View file

@ -1,21 +0,0 @@
import type { KitEvent } from "sveltekit-zero-api";
import type { RequestEvent } from "./$types";
import { getCategories, newCategory } from "$lib/server/query";
import type { ApiBody, CategoryNew } from "$lib/shared/model";
import { ZCategoryNew } from "$lib/shared/model/validation";
import { apiWrap } from "$lib/server/handleError";
/** Get all categories */
export const GET = async (event: KitEvent<object, RequestEvent>) => {
return apiWrap(getCategories);
};
/** Create a new category */
export const POST = async (event: KitEvent<ApiBody<CategoryNew>, RequestEvent>) => {
return apiWrap(async () => {
const category = ZCategoryNew.parse(await event.request.json());
const id = await newCategory(category);
return { id };
});
};

View file

@ -1,35 +0,0 @@
import { apiWrap } from "$lib/server/handleError";
import { deleteCategory, getCategory, updateCategory } from "$lib/server/query";
import { ZCategoryNew, ZEntityIdUrl } from "$lib/shared/model/validation";
import type { KitEvent } from "sveltekit-zero-api";
import type { RequestEvent } from "./$types";
import type { CategoryNew, ApiBody } from "$lib/shared/model";
/** Get a category */
export const GET = async (event: KitEvent<object, RequestEvent>) => {
return apiWrap(async () => {
const id = ZEntityIdUrl.parse(event.params.id);
return await getCategory(id);
});
};
/** Update a category */
export const PATCH = async (
event: KitEvent<ApiBody<Partial<CategoryNew>>, RequestEvent>
) => {
return apiWrap(async () => {
const id = ZEntityIdUrl.parse(event.params.id);
const category = ZCategoryNew.partial().parse(await event.request.json());
await updateCategory(id, category);
return { id };
});
};
/** Delete a category */
export const DELETE = async (event: KitEvent<object, RequestEvent>) => {
return apiWrap(async () => {
const id = ZEntityIdUrl.parse(event.params.id);
await deleteCategory(id);
return { id };
});
};

View file

@ -1,15 +0,0 @@
import { getEntries } from "$lib/server/query";
import { apiWrap } from "$lib/server/handleError";
import { ZEntriesFilterUrl, ZPaginationUrl } from "$lib/shared/model/validation";
import type { KitEvent } from "sveltekit-zero-api";
import type { RequestEvent } from "./$types";
/** Get entries */
export const GET = async (event: KitEvent<object, RequestEvent>) => {
return apiWrap(async () => {
const params = Object.fromEntries(event.url.searchParams);
const filter = ZEntriesFilterUrl.parse(params);
const pagination = ZPaginationUrl.parse(params);
return await getEntries(filter, pagination);
});
};

View file

@ -1,13 +0,0 @@
import { apiWrap } from "$lib/server/handleError";
import { getEntry } from "$lib/server/query";
import { ZEntityIdUrl } from "$lib/shared/model/validation";
import type { KitEvent } from "sveltekit-zero-api";
import type { RequestEvent } from "./$types";
/** Get a entry */
export const GET = async (event: KitEvent<object, RequestEvent>) => {
return apiWrap(async () => {
const id = ZEntityIdUrl.parse(event.params.id);
return getEntry(id);
});
};

View file

@ -1,18 +0,0 @@
import type { KitEvent } from "sveltekit-zero-api";
import type { RequestEvent } from "./$types";
import type { ApiBody, EntryExecutionNew } from "$lib/shared/model";
import { apiWrapUser } from "$lib/server/handleError";
import { ZEntityIdUrl, ZEntryExecutionNew } from "$lib/shared/model/validation";
import { newEntryExecution } from "$lib/server/query";
/** Create an entry execution */
export const POST = async (
event: KitEvent<ApiBody<EntryExecutionNew>, RequestEvent>
) => {
return apiWrapUser(event, async (uId) => {
const id = ZEntityIdUrl.parse(event.params.id);
const execution = ZEntryExecutionNew.parse(await event.request.json());
const newExecutionId = await newEntryExecution(uId, id, execution);
return { id, newExecutionId };
});
};

View file

@ -1,26 +0,0 @@
import { apiWrap, apiWrapUser } from "$lib/server/handleError";
import { getEntryHistory, newEntryVersion } from "$lib/server/query";
import { ZEntityIdUrl, ZEntryVersionNew } from "$lib/shared/model/validation";
import type { KitEvent } from "sveltekit-zero-api";
import type { RequestEvent } from "./$types";
import type { ApiBody, EntryVersionNew } from "$lib/shared/model";
/** Get all versions of an entry */
export const GET = async (event: KitEvent<object, RequestEvent>) => {
return apiWrap(async () => {
const id = ZEntityIdUrl.parse(event.params.id);
return getEntryHistory(id);
});
};
/** Create an entry version */
export const PATCH = async (
event: KitEvent<ApiBody<Partial<EntryVersionNew>>, RequestEvent>
) => {
return apiWrapUser(event, async (uId) => {
const id = ZEntityIdUrl.parse(event.params.id);
const version = ZEntryVersionNew.parse(await event.request.json());
const newVersionId = await newEntryVersion(uId, id, version);
return { id, newVersionId };
});
};

View file

@ -1,15 +0,0 @@
import { apiWrap } from "$lib/server/handleError";
import { getPatients } from "$lib/server/query";
import { ZPaginationUrl, ZPatientsFilterUrl } from "$lib/shared/model/validation";
import type { KitEvent } from "sveltekit-zero-api";
import type { RequestEvent } from "./$types";
/** Get patients */
export const GET = async (event: KitEvent<object, RequestEvent>) => {
return apiWrap(async () => {
const params = Object.fromEntries(event.url.searchParams);
const filter = ZPatientsFilterUrl.parse(params);
const pagination = ZPaginationUrl.parse(params);
return await getPatients(filter, pagination);
});
};

View file

@ -1,35 +0,0 @@
import { apiWrap } from "$lib/server/handleError";
import { deletePatient, getPatient, updatePatient } from "$lib/server/query";
import { ZEntityIdUrl, ZPatientNew } from "$lib/shared/model/validation";
import type { KitEvent } from "sveltekit-zero-api";
import type { RequestEvent } from "./$types";
import type { ApiBody, PatientNew } from "$lib/shared/model";
/** Get a patient */
export const GET = async (event: KitEvent<object, RequestEvent>) => {
return apiWrap(async () => {
const id = ZEntityIdUrl.parse(event.params.id);
return await getPatient(id);
});
};
/** Update a patient */
export const PATCH = async (
event: KitEvent<ApiBody<Partial<PatientNew>>, RequestEvent>
) => {
return apiWrap(async () => {
const id = ZEntityIdUrl.parse(event.params.id);
const patient = ZPatientNew.partial().parse(await event.request.json());
await updatePatient(id, patient);
return { id };
});
};
/** Delete a patient */
export const DELETE = async (event: KitEvent<object, RequestEvent>) => {
return apiWrap(async () => {
const id = ZEntityIdUrl.parse(event.params.id);
await deletePatient(id);
return { id };
});
};

View file

@ -1,20 +0,0 @@
import { getRooms, newRoom } from "$lib/server/query";
import { apiWrap } from "$lib/server/handleError";
import type { KitEvent } from "sveltekit-zero-api";
import type { RequestEvent } from "./$types";
import type { ApiBody, RoomNew } from "$lib/shared/model";
import { ZRoomNew } from "$lib/shared/model/validation";
/** Get all rooms */
export const GET = async (event: KitEvent<object, RequestEvent>) => {
return apiWrap(getRooms);
};
/** Create a new room */
export const POST = async (event: KitEvent<ApiBody<RoomNew>, RequestEvent>) => {
return apiWrap(async () => {
const room = ZRoomNew.parse(await event.request.json());
const id = await newRoom(room);
return { id };
});
};

View file

@ -1,35 +0,0 @@
import { apiWrap } from "$lib/server/handleError";
import { getRoom, updateRoom, deleteRoom } from "$lib/server/query";
import { ZRoomNew, ZEntityIdUrl } from "$lib/shared/model/validation";
import type { KitEvent } from "sveltekit-zero-api";
import type { RequestEvent } from "./$types";
import type { ApiBody, RoomNew } from "$lib/shared/model";
/** Get a room */
export const GET = async (event: KitEvent<object, RequestEvent>) => {
return apiWrap(async () => {
const id = ZEntityIdUrl.parse(event.params.id);
return await getRoom(id);
});
};
/** Update a room */
export const PATCH = async (
event: KitEvent<ApiBody<Partial<RoomNew>>, RequestEvent>
) => {
return apiWrap(async () => {
const id = ZEntityIdUrl.parse(event.params.id);
const room = ZRoomNew.partial().parse(await event.request.json());
await updateRoom(id, room);
return { id };
});
};
/** Delete a room */
export const DELETE = async (event: KitEvent<object, RequestEvent>) => {
return apiWrap(async () => {
const id = ZEntityIdUrl.parse(event.params.id);
await deleteRoom(id);
return { id };
});
};

View file

@ -1,20 +0,0 @@
import { getRooms, newRoom } from "$lib/server/query";
import { apiWrap } from "$lib/server/handleError";
import type { KitEvent } from "sveltekit-zero-api";
import type { RequestEvent } from "./$types";
import type { ApiBody, RoomNew } from "$lib/shared/model";
import { ZRoomNew } from "$lib/shared/model/validation";
/** Get all rooms */
export const GET = async (event: KitEvent<object, RequestEvent>) => {
return apiWrap(getRooms);
};
/** Create a new room */
export const POST = async (event: KitEvent<ApiBody<RoomNew>, RequestEvent>) => {
return apiWrap(async () => {
const room = ZRoomNew.parse(await event.request.json());
const id = await newRoom(room);
return { id };
});
};

View file

@ -1,35 +0,0 @@
import { apiWrap } from "$lib/server/handleError";
import { deleteStation, getStation, updateStation } from "$lib/server/query";
import { ZEntityIdUrl, ZStationNew } from "$lib/shared/model/validation";
import type { KitEvent } from "sveltekit-zero-api";
import type { RequestEvent } from "./$types";
import type { ApiBody, StationNew } from "$lib/shared/model";
/** Get a station */
export const GET = async (event: KitEvent<object, RequestEvent>) => {
return apiWrap(async () => {
const id = ZEntityIdUrl.parse(event.params.id);
return await getStation(id);
});
};
/** Update a station */
export const PATCH = async (
event: KitEvent<ApiBody<Partial<StationNew>>, RequestEvent>
) => {
return apiWrap(async () => {
const id = ZEntityIdUrl.parse(event.params.id);
const station = ZStationNew.partial().parse(await event.request.json());
await updateStation(id, station);
return { id };
});
};
/** Delete a station */
export const DELETE = async (event: KitEvent<object, RequestEvent>) => {
return apiWrap(async () => {
const id = ZEntityIdUrl.parse(event.params.id);
await deleteStation(id);
return { id };
});
};

View file

@ -1,14 +0,0 @@
import { apiWrap } from "$lib/server/handleError";
import { getUsers } from "$lib/server/query";
import { ZPaginationUrl } from "$lib/shared/model/validation";
import type { KitEvent } from "sveltekit-zero-api";
import type { RequestEvent } from "./$types";
/** Get users */
export const GET = async (event: KitEvent<object, RequestEvent>) => {
return apiWrap(async () => {
const params = Object.fromEntries(event.url.searchParams);
const pagination = ZPaginationUrl.parse(params);
return await getUsers(pagination);
});
};

View file

@ -1,7 +1,7 @@
import {
getEntries,
getEntry,
getEntryHistory,
getEntryVersions,
newEntry,
newEntryExecution,
newEntryVersion,
@ -59,7 +59,7 @@ test("create entry version", async () => {
};
expect(entry.current_version).toMatchObject(expectedVersion);
const history = await getEntryHistory(eId);
const history = await getEntryVersions(eId);
expect(history).length(2);
expect(history[0]).toMatchObject(expectedVersion);
expect(history[1].text).toBe(TEST_VERSION.text);
@ -74,10 +74,7 @@ test("create entry version (partial)", async () => {
const oldEntry = await getEntry(eId);
const text = "10ml Blut abnehmen\n\nPS: Nadel nicht vergessen";
await newEntryVersion(2, eId, {
old_version: oldEntry.current_version.id,
text,
});
await newEntryVersion(2, eId, { text }, oldEntry.current_version.id);
const entry = await getEntry(eId);
expect(entry.current_version).toMatchObject({
@ -97,10 +94,12 @@ test("create entry version (wrong old vid)", async () => {
const entry = await getEntry(eId);
expect(async () => {
await newEntryVersion(1, eId, {
old_version: entry.current_version.id + 1,
text: "Hello World",
});
await newEntryVersion(
1,
eId,
{ text: "Hello World" },
entry.current_version.id + 1
);
}).rejects.toThrowError(new ErrorConflict("old version id does not match"));
});
@ -111,7 +110,7 @@ test("create entry execution", async () => {
});
const text = "Blutabnahme erledigt.";
const xId = await newEntryExecution(1, eId, { old_execution: null, text });
const xId = await newEntryExecution(1, eId, { text }, null);
const entry = await getEntry(eId);
expect(entry.execution?.id).toBe(xId);
@ -125,8 +124,8 @@ test("create entry execution (update)", async () => {
version: TEST_VERSION,
});
const x1 = await newEntryExecution(1, eId, { old_execution: null, text: "x1" });
const x2 = await newEntryExecution(2, eId, { old_execution: x1, text: "x2" });
const x1 = await newEntryExecution(1, eId, { text: "x1" }, null);
const x2 = await newEntryExecution(2, eId, { text: "x2" }, x1);
const entry = await getEntry(eId);
expect(entry.execution?.id).toBe(x2);
@ -139,10 +138,10 @@ test("create entry execution (wrong old xid)", async () => {
patient_id: 1,
version: TEST_VERSION,
});
const x1 = await newEntryExecution(1, eId, { old_execution: null, text: "x1" });
const x1 = await newEntryExecution(1, eId, { text: "x1" }, null);
expect(
async () => await newEntryExecution(1, eId, { old_execution: x1 + 1, text: "x2" })
async () => await newEntryExecution(1, eId, { text: "x2" }, x1 + 1)
).rejects.toThrowError(new ErrorConflict("old execution id does not match"));
});
@ -239,6 +238,12 @@ test("get entries", async () => {
expect(entriesDone.items[0].id).toBe(eId3);
expect(entriesDone.items[1].id).toBe(eId2);
// Filter not done
const entriesNotDone = await getEntries({ done: false }, {});
expect(entriesNotDone.items).length(1);
expect(entriesNotDone.total).toBe(1);
expect(entriesNotDone.items[0].id).toBe(eId1);
// Filter by priority
const entriesPrio = await getEntries({ priority: true }, {});
expect(entriesPrio.items).length(1);

View file

@ -5,6 +5,7 @@ import {
updatePatient,
deletePatient,
newEntry,
hidePatient,
} from "$lib/server/query";
import { expect, test } from "vitest";
import { S1, S2 } from "$tests/helpers/testdata";
@ -65,6 +66,16 @@ test("delete patient (restricted)", async () => {
);
});
test("hide patient", async () => {
await hidePatient(1, true);
let patient = await getPatient(1);
expect(patient.hidden).toBe(true);
await hidePatient(1, false);
patient = await getPatient(1);
expect(patient.hidden).toBe(false);
});
test("get patients", async () => {
const patients = await getPatients({}, {});
expect(patients).toMatchObject({
@ -118,6 +129,18 @@ test("get patients (by station)", async () => {
expect(patients.items[1].id).toBe(2);
});
test("get patients (hidden)", async () => {
await hidePatient(2, true);
let patients = await getPatients({ hidden: false }, {});
expect(patients.items).length(2);
expect(patients.items[0].id).toBe(1);
patients = await getPatients({ hidden: true }, {});
expect(patients.items).length(1);
expect(patients.items[0].id).toBe(2);
});
test("search patients", async () => {
const patients = await getPatients({ search: "Schustr" }, {});
expect(patients.items).length(1);

View file

@ -1,9 +1,8 @@
import { sveltekit } from "@sveltejs/kit/vite";
import { defineConfig } from "vitest/config";
import { zeroAPI } from "sveltekit-zero-api";
export default defineConfig({
plugins: [sveltekit(), zeroAPI()],
plugins: [sveltekit()],
test: {
include: ["src/**/*.{test,spec}.{js,ts}"],
},