Compare commits
4 commits
34bed279db
...
0e72eb2a62
Author | SHA1 | Date | |
---|---|---|---|
0e72eb2a62 | |||
a3f7d4a8fd | |||
161d793ebf | |||
150a0eddd2 |
43 changed files with 853 additions and 170 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -8,3 +8,5 @@ node_modules
|
||||||
!.env.example
|
!.env.example
|
||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
vite.config.ts.timestamp-*
|
vite.config.ts.timestamp-*
|
||||||
|
sveltekit-zero-api.d.ts
|
||||||
|
/.vscode
|
||||||
|
|
|
@ -46,6 +46,7 @@
|
||||||
"prisma": "^5.7.0",
|
"prisma": "^5.7.0",
|
||||||
"svelte": "^4.2.8",
|
"svelte": "^4.2.8",
|
||||||
"svelte-check": "^3.6.2",
|
"svelte-check": "^3.6.2",
|
||||||
|
"sveltekit-zero-api": "^0.15.7",
|
||||||
"tailwindcss": "^3.3.6",
|
"tailwindcss": "^3.3.6",
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.6.2",
|
||||||
"tsx": "^4.7.0",
|
"tsx": "^4.7.0",
|
||||||
|
|
|
@ -88,6 +88,9 @@ devDependencies:
|
||||||
svelte-check:
|
svelte-check:
|
||||||
specifier: ^3.6.2
|
specifier: ^3.6.2
|
||||||
version: 3.6.2(postcss@8.4.32)(svelte@4.2.8)
|
version: 3.6.2(postcss@8.4.32)(svelte@4.2.8)
|
||||||
|
sveltekit-zero-api:
|
||||||
|
specifier: ^0.15.7
|
||||||
|
version: 0.15.7(@sveltejs/kit@2.0.4)(svelte@4.2.8)(typescript@5.3.3)
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^3.3.6
|
specifier: ^3.3.6
|
||||||
version: 3.3.6
|
version: 3.3.6
|
||||||
|
@ -2771,6 +2774,18 @@ packages:
|
||||||
magic-string: 0.30.5
|
magic-string: 0.30.5
|
||||||
periscopic: 3.1.0
|
periscopic: 3.1.0
|
||||||
|
|
||||||
|
/sveltekit-zero-api@0.15.7(@sveltejs/kit@2.0.4)(svelte@4.2.8)(typescript@5.3.3):
|
||||||
|
resolution: {integrity: sha512-yklGBeRfHw4dmMjvzskkuXbeakyKWyRqmiVbuD/a4xXPsU3TerBfNKAy6+7Bmpo2KIE2Q6LTdrs+JUQyYYtltA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@sveltejs/kit': ^1.5.5
|
||||||
|
svelte: ^3.55.1
|
||||||
|
typescript: '>=5.0.0'
|
||||||
|
dependencies:
|
||||||
|
'@sveltejs/kit': 2.0.4(@sveltejs/vite-plugin-svelte@3.0.1)(svelte@4.2.8)(vite@5.0.10)
|
||||||
|
svelte: 4.2.8
|
||||||
|
typescript: 5.3.3
|
||||||
|
dev: true
|
||||||
|
|
||||||
/tailwindcss@3.3.6:
|
/tailwindcss@3.3.6:
|
||||||
resolution: {integrity: sha512-AKjF7qbbLvLaPieoKeTjG1+FyNZT6KaJMJPFeQyLfIp7l82ggH1fbHJSsYIvnbTFQOlkh+gBYpyby5GT1LIdLw==}
|
resolution: {integrity: sha512-AKjF7qbbLvLaPieoKeTjG1+FyNZT6KaJMJPFeQyLfIp7l82ggH1fbHJSsYIvnbTFQOlkh+gBYpyby5GT1LIdLw==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
|
@ -114,7 +114,7 @@ ALTER TABLE "rooms" ADD CONSTRAINT "rooms_station_id_fkey" FOREIGN KEY ("station
|
||||||
ALTER TABLE "patients" ADD CONSTRAINT "patients_room_id_fkey" FOREIGN KEY ("room_id") REFERENCES "rooms"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
ALTER TABLE "patients" ADD CONSTRAINT "patients_room_id_fkey" FOREIGN KEY ("room_id") REFERENCES "rooms"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "entries" ADD CONSTRAINT "entries_patient_id_fkey" FOREIGN KEY ("patient_id") REFERENCES "patients"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
ALTER TABLE "entries" ADD CONSTRAINT "entries_patient_id_fkey" FOREIGN KEY ("patient_id") REFERENCES "patients"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "entry_versions" ADD CONSTRAINT "entry_versions_entry_id_fkey" FOREIGN KEY ("entry_id") REFERENCES "entries"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
ALTER TABLE "entry_versions" ADD CONSTRAINT "entry_versions_entry_id_fkey" FOREIGN KEY ("entry_id") REFERENCES "entries"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
CREATE EXTENSION pg_trgm;
|
||||||
|
|
||||||
ALTER TABLE entries
|
ALTER TABLE entries
|
||||||
ADD COLUMN tsvec tsvector;
|
ADD COLUMN tsvec tsvector;
|
||||||
|
|
||||||
|
@ -27,3 +29,8 @@ AFTER INSERT
|
||||||
OR
|
OR
|
||||||
UPDATE ON entry_executions FOR EACH ROW
|
UPDATE ON entry_executions FOR EACH ROW
|
||||||
EXECUTE PROCEDURE update_entry_tsvec ();
|
EXECUTE PROCEDURE update_entry_tsvec ();
|
||||||
|
|
||||||
|
ALTER TABLE patients
|
||||||
|
ADD COLUMN full_name TEXT GENERATED ALWAYS AS (first_name || ' ' || last_name) STORED;
|
||||||
|
|
||||||
|
CREATE INDEX patients_full_name ON patients USING gin (full_name gin_trgm_ops);
|
||||||
|
|
|
@ -93,7 +93,7 @@ model Category {
|
||||||
|
|
||||||
model Entry {
|
model Entry {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
patient Patient @relation(fields: [patient_id], references: [id], onDelete: Cascade)
|
patient Patient @relation(fields: [patient_id], references: [id], onDelete: Restrict)
|
||||||
patient_id Int
|
patient_id Int
|
||||||
|
|
||||||
EntryVersion EntryVersion[]
|
EntryVersion EntryVersion[]
|
||||||
|
|
|
@ -9,4 +9,9 @@ echo 'Waiting for database to be ready...'
|
||||||
|
|
||||||
# Create temporary test database
|
# Create temporary test database
|
||||||
docker-compose exec -u 999:999 db sh -e -c 'dropdb -f --if-exists test; createdb test'
|
docker-compose exec -u 999:999 db sh -e -c 'dropdb -f --if-exists test; createdb test'
|
||||||
cd "$DIR/../" && DATABASE_URL="postgresql://postgres:1234@localhost:5432/test?schema=public" npx prisma migrate reset --force
|
cd "$DIR/../"
|
||||||
|
DATABASE_URL="postgresql://postgres:1234@localhost:5432/test?schema=public" npx prisma migrate reset --force
|
||||||
|
|
||||||
|
# Reset main database
|
||||||
|
npx prisma migrate reset --force
|
||||||
|
npx tsx run/gen-mockdata.ts
|
||||||
|
|
9
src/api.ts
Normal file
9
src/api.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
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;
|
69
src/lib/server/handleError.ts
Normal file
69
src/lib/server/handleError.ts
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
import { ErrorConflict, ErrorNotFound } from "$lib/shared/util/error";
|
||||||
|
import { ZodError } from "zod";
|
||||||
|
import {
|
||||||
|
Ok,
|
||||||
|
BadRequest,
|
||||||
|
NotFound,
|
||||||
|
Conflict,
|
||||||
|
InternalServerError,
|
||||||
|
} from "sveltekit-zero-api/http";
|
||||||
|
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
||||||
|
import { userId } from "./util";
|
||||||
|
import type { RequestEvent } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
function handleError(error: unknown) {
|
||||||
|
if (error instanceof ZodError) {
|
||||||
|
// return { status: 400, msg: "Invalid input", data: error.flatten() };
|
||||||
|
return BadRequest({
|
||||||
|
body: { status: 400, msg: "Invalid input", data: error.flatten() },
|
||||||
|
});
|
||||||
|
} else if (error instanceof PrismaClientKnownRequestError) {
|
||||||
|
if (error.code === "P2025") {
|
||||||
|
return NotFound({
|
||||||
|
body: { status: 404, msg: error.message },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return InternalServerError({
|
||||||
|
body: { status: 500, msg: error.message, prismaCode: error.code },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (error instanceof ErrorNotFound) {
|
||||||
|
return NotFound({
|
||||||
|
body: { status: 404, msg: error.message },
|
||||||
|
});
|
||||||
|
} else if (error instanceof ErrorConflict) {
|
||||||
|
return Conflict({
|
||||||
|
body: { status: 409, msg: error.message },
|
||||||
|
});
|
||||||
|
} else if (error instanceof Error) {
|
||||||
|
return InternalServerError({
|
||||||
|
body: { status: 500, msg: error.message },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return InternalServerError({
|
||||||
|
body: { status: 500, msg: "unknown error" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function apiWrap<T>(f: () => Promise<T>) {
|
||||||
|
try {
|
||||||
|
const body = await f();
|
||||||
|
return Ok({ body });
|
||||||
|
} catch (error) {
|
||||||
|
return handleError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function apiWrapUser<T>(
|
||||||
|
event: RequestEvent,
|
||||||
|
f: (uId: number) => Promise<T>
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const uId = await userId(event);
|
||||||
|
const body = await f(uId);
|
||||||
|
return Ok({ body });
|
||||||
|
} catch (error) {
|
||||||
|
return handleError(error);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +1,22 @@
|
||||||
import type { Category, CategoryNew } from "$lib/shared/model";
|
import type { Category, CategoryNew } from "$lib/shared/model";
|
||||||
import { ZCategoryNew } from "$lib/shared/model/validation";
|
|
||||||
import { prisma } from "$lib/server/prisma";
|
import { prisma } from "$lib/server/prisma";
|
||||||
|
|
||||||
export async function newCategory(category: CategoryNew): Promise<number> {
|
export async function newCategory(category: CategoryNew): Promise<number> {
|
||||||
const data = ZCategoryNew.parse(category);
|
const created = await prisma.category.create({
|
||||||
const created = await prisma.category.create({ data, select: { id: true } });
|
data: category,
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
return created.id;
|
return created.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateCategory(id: number, category: Partial<CategoryNew>) {
|
||||||
|
await prisma.category.update({ where: { id }, data: category });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteCategory(id: number) {
|
||||||
|
await prisma.category.delete({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
export async function getCategory(id: number): Promise<Category> {
|
export async function getCategory(id: number): Promise<Category> {
|
||||||
return prisma.category.findUniqueOrThrow({ where: { id } });
|
return prisma.category.findUniqueOrThrow({ where: { id } });
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,16 @@
|
||||||
import { prisma } from "$lib/server/prisma";
|
import { prisma } from "$lib/server/prisma";
|
||||||
import type {
|
import type {
|
||||||
EntriesRequest,
|
EntriesFilter,
|
||||||
Entry,
|
Entry,
|
||||||
EntryExecutionNew,
|
EntryExecutionNew,
|
||||||
EntryNew,
|
EntryNew,
|
||||||
|
EntryVersion,
|
||||||
EntryVersionNew,
|
EntryVersionNew,
|
||||||
Pagination,
|
Pagination,
|
||||||
|
PaginationRequest,
|
||||||
} from "$lib/shared/model";
|
} from "$lib/shared/model";
|
||||||
import {
|
|
||||||
ZEntryExecutionNew,
|
|
||||||
ZEntryNew,
|
|
||||||
ZEntryVersionNew,
|
|
||||||
} from "$lib/shared/model/validation";
|
|
||||||
import { ErrorConflict } from "$lib/shared/util/error";
|
import { ErrorConflict } from "$lib/shared/util/error";
|
||||||
import { mapEntry } from "./mapping";
|
import { mapEntry, mapVersion } from "./mapping";
|
||||||
import { QueryBuilder, parseSearchQuery } from "./util";
|
import { QueryBuilder, parseSearchQuery } from "./util";
|
||||||
|
|
||||||
const USER_SELECT = { select: { id: true, name: true } };
|
const USER_SELECT = { select: { id: true, name: true } };
|
||||||
|
@ -39,15 +36,24 @@ export async function getEntry(id: number): Promise<Entry> {
|
||||||
return mapEntry(entry);
|
return mapEntry(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getEntryHistory(id: number): Promise<EntryVersion[]> {
|
||||||
|
const entries = await prisma.entryVersion.findMany({
|
||||||
|
where: { entry_id: id },
|
||||||
|
include: { author: USER_SELECT, category: true },
|
||||||
|
orderBy: { created_at: "desc" },
|
||||||
|
});
|
||||||
|
|
||||||
|
return entries.map(mapVersion);
|
||||||
|
}
|
||||||
|
|
||||||
export async function newEntry(author_id: number, entry: EntryNew): Promise<number> {
|
export async function newEntry(author_id: number, entry: EntryNew): Promise<number> {
|
||||||
const data = ZEntryNew.parse(entry);
|
|
||||||
const created = await prisma.entry.create({
|
const created = await prisma.entry.create({
|
||||||
data: {
|
data: {
|
||||||
patient_id: data.patient_id,
|
patient_id: entry.patient_id,
|
||||||
EntryVersion: {
|
EntryVersion: {
|
||||||
create: {
|
create: {
|
||||||
author_id,
|
author_id,
|
||||||
...data.version,
|
...entry.version,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -61,8 +67,6 @@ export async function newEntryVersion(
|
||||||
entry_id: number,
|
entry_id: number,
|
||||||
version: EntryVersionNew
|
version: EntryVersionNew
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
const data = ZEntryVersionNew.parse(version);
|
|
||||||
|
|
||||||
return prisma.$transaction(async (tx) => {
|
return prisma.$transaction(async (tx) => {
|
||||||
const entry = await tx.entry.findUniqueOrThrow({
|
const entry = await tx.entry.findUniqueOrThrow({
|
||||||
where: { id: entry_id },
|
where: { id: entry_id },
|
||||||
|
@ -76,7 +80,7 @@ export async function newEntryVersion(
|
||||||
const cver = entry.EntryVersion[0];
|
const cver = entry.EntryVersion[0];
|
||||||
|
|
||||||
// Check if the entry has been updated by someone else
|
// Check if the entry has been updated by someone else
|
||||||
if (data.old_version && (!cver || cver.id !== data.old_version)) {
|
if (version.old_version && (!cver || cver.id !== version.old_version)) {
|
||||||
throw new ErrorConflict("old version id does not match");
|
throw new ErrorConflict("old version id does not match");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,10 +89,10 @@ export async function newEntryVersion(
|
||||||
// Old version
|
// Old version
|
||||||
entry_id,
|
entry_id,
|
||||||
author_id,
|
author_id,
|
||||||
text: data.text || cver.text,
|
text: version.text || cver.text,
|
||||||
date: data.date || cver.date,
|
date: version.date || cver.date,
|
||||||
category_id: data.category_id || cver.category_id,
|
category_id: version.category_id || cver.category_id,
|
||||||
priority: data.priority || cver.priority,
|
priority: version.priority || cver.priority,
|
||||||
},
|
},
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
});
|
});
|
||||||
|
@ -101,8 +105,6 @@ export async function newEntryExecution(
|
||||||
entry_id: number,
|
entry_id: number,
|
||||||
execution: EntryExecutionNew
|
execution: EntryExecutionNew
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
const data = ZEntryExecutionNew.parse(execution);
|
|
||||||
|
|
||||||
return prisma.$transaction(async (tx) => {
|
return prisma.$transaction(async (tx) => {
|
||||||
const entry = await tx.entry.findUniqueOrThrow({
|
const entry = await tx.entry.findUniqueOrThrow({
|
||||||
where: { id: entry_id },
|
where: { id: entry_id },
|
||||||
|
@ -117,8 +119,8 @@ export async function newEntryExecution(
|
||||||
|
|
||||||
// Check if the execution has been updated by someone else
|
// Check if the execution has been updated by someone else
|
||||||
if (
|
if (
|
||||||
(data.old_execution && (!cex || cex.id !== data.old_execution)) ||
|
(execution.old_execution && (!cex || cex.id !== execution.old_execution)) ||
|
||||||
(data.old_execution === null && cex)
|
(execution.old_execution === null && cex)
|
||||||
) {
|
) {
|
||||||
throw new ErrorConflict("old execution id does not match");
|
throw new ErrorConflict("old execution id does not match");
|
||||||
}
|
}
|
||||||
|
@ -127,7 +129,7 @@ export async function newEntryExecution(
|
||||||
data: {
|
data: {
|
||||||
entry_id,
|
entry_id,
|
||||||
author_id,
|
author_id,
|
||||||
text: data.text,
|
text: execution.text,
|
||||||
},
|
},
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
});
|
});
|
||||||
|
@ -135,7 +137,10 @@ export async function newEntryExecution(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getEntries(req: EntriesRequest): Promise<Pagination<Entry>> {
|
export async function getEntries(
|
||||||
|
filter: EntriesFilter,
|
||||||
|
pagination: PaginationRequest
|
||||||
|
): Promise<Pagination<Entry>> {
|
||||||
const qb = new QueryBuilder(
|
const qb = new QueryBuilder(
|
||||||
`select
|
`select
|
||||||
e.id,
|
e.id,
|
||||||
|
@ -197,29 +202,29 @@ join rooms r on r.id = p.room_id
|
||||||
join stations s on s.id = r.station_id`
|
join stations s on s.id = r.station_id`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (req.filter?.search && req.filter.search.length > 0) {
|
if (filter?.search && filter.search.length > 0) {
|
||||||
const query = parseSearchQuery(req.filter.search);
|
const query = parseSearchQuery(filter.search);
|
||||||
qb.addFilterClause(
|
qb.addFilterClause(
|
||||||
`to_tsquery('german', ${qb.pvar()}) @@ e.tsvec`,
|
`to_tsquery('german', ${qb.pvar()}) @@ e.tsvec`,
|
||||||
query.toTsquery()
|
query.toTsquery()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.filter?.done === true) {
|
if (filter?.done === true) {
|
||||||
qb.addFilterClause("ex.id is not null");
|
qb.addFilterClause("ex.id is not null");
|
||||||
} else if (req.filter?.done === false) {
|
} else if (filter?.done === false) {
|
||||||
qb.addFilterClause("ex.id is null");
|
qb.addFilterClause("ex.id is null");
|
||||||
}
|
}
|
||||||
|
|
||||||
qb.addFilterList("xau.id", req.filter?.executor);
|
qb.addFilterList("xau.id", filter?.executor);
|
||||||
qb.addFilterList("c.id", req.filter?.category);
|
qb.addFilterList("c.id", filter?.category);
|
||||||
qb.addFilterList("p.id", req.filter?.patient);
|
qb.addFilterList("p.id", filter?.patient);
|
||||||
qb.addFilterList("s.id", req.filter?.station);
|
qb.addFilterList("s.id", filter?.station);
|
||||||
qb.addFilterList("r.id", req.filter?.room);
|
qb.addFilterList("r.id", filter?.room);
|
||||||
qb.addFilter("ev.priority", req.filter?.priority);
|
qb.addFilter("ev.priority", filter?.priority);
|
||||||
|
|
||||||
if (req.filter?.author) {
|
if (filter?.author) {
|
||||||
let author = req.filter?.author;
|
let author = filter?.author;
|
||||||
if (!Array.isArray(author)) {
|
if (!Array.isArray(author)) {
|
||||||
author = [author];
|
author = [author];
|
||||||
}
|
}
|
||||||
|
@ -229,10 +234,10 @@ join stations s on s.id = r.station_id`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
qb.setOrderClause(`order by e.created_at desc`);
|
qb.addOrderClause("e.created_at desc");
|
||||||
if (req.pagination) qb.setPagination(req.pagination);
|
qb.setPagination(pagination);
|
||||||
|
|
||||||
type RequestItem = {
|
type RowItem = {
|
||||||
id: number;
|
id: number;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
text: string;
|
text: string;
|
||||||
|
@ -264,10 +269,9 @@ join stations s on s.id = r.station_id`
|
||||||
const [res, countRes] = (await Promise.all([
|
const [res, countRes] = (await Promise.all([
|
||||||
prisma.$queryRawUnsafe(qb.getQuery(), ...qb.getParams()),
|
prisma.$queryRawUnsafe(qb.getQuery(), ...qb.getParams()),
|
||||||
prisma.$queryRawUnsafe(qb.getCountQuery(), ...qb.getCountParams()),
|
prisma.$queryRawUnsafe(qb.getCountQuery(), ...qb.getCountParams()),
|
||||||
])) as [RequestItem[], { count: bigint }[]];
|
])) as [RowItem[], { count: bigint }[]];
|
||||||
|
|
||||||
const total = Number(countRes[0].count);
|
const total = Number(countRes[0].count);
|
||||||
|
|
||||||
const items: Entry[] = res.map((item) => {
|
const items: Entry[] = res.map((item) => {
|
||||||
return {
|
return {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
import type { Entry, Patient, User, UserTag, Room } from "$lib/shared/model";
|
import type {
|
||||||
|
Entry,
|
||||||
|
Patient,
|
||||||
|
User,
|
||||||
|
UserTag,
|
||||||
|
Room,
|
||||||
|
EntryVersion,
|
||||||
|
EntryExecution,
|
||||||
|
} from "$lib/shared/model";
|
||||||
import { ErrorNotFound } from "$lib/shared/util/error";
|
import { ErrorNotFound } from "$lib/shared/util/error";
|
||||||
import type {
|
import type {
|
||||||
Patient as DbPatient,
|
Patient as DbPatient,
|
||||||
|
@ -18,6 +26,11 @@ type DbEntryVersionLn = DbEntryVersion & {
|
||||||
author: UserTag;
|
author: UserTag;
|
||||||
};
|
};
|
||||||
type DbEntryExecutionLn = DbEntryExecution & { author: UserTag };
|
type DbEntryExecutionLn = DbEntryExecution & { author: UserTag };
|
||||||
|
type DbEntryFull = DbEntry & {
|
||||||
|
EntryVersion: DbEntryVersionLn[];
|
||||||
|
EntryExecution: DbEntryExecutionLn[];
|
||||||
|
patient: DbPatientLn;
|
||||||
|
};
|
||||||
|
|
||||||
export function mapPatient(patient: DbPatientLn): Patient {
|
export function mapPatient(patient: DbPatientLn): Patient {
|
||||||
return {
|
return {
|
||||||
|
@ -48,13 +61,28 @@ export function mapRoom(room: DbRoomLn): Room {
|
||||||
return { id: room.id, name: room.name, station: room.station };
|
return { id: room.id, name: room.name, station: room.station };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapEntry(
|
export function mapVersion(version: DbEntryVersionLn): EntryVersion {
|
||||||
entry: DbEntry & {
|
return {
|
||||||
EntryVersion: DbEntryVersionLn[];
|
id: version.id,
|
||||||
EntryExecution: DbEntryExecutionLn[];
|
text: version.text,
|
||||||
patient: DbPatientLn;
|
date: version.date,
|
||||||
}
|
category: version.category,
|
||||||
): Entry {
|
priority: version.priority,
|
||||||
|
author: version.author,
|
||||||
|
created_at: version.created_at,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapExecution(execution: DbEntryExecutionLn): EntryExecution {
|
||||||
|
return {
|
||||||
|
id: execution.id,
|
||||||
|
author: execution.author,
|
||||||
|
created_at: execution.created_at,
|
||||||
|
text: execution.text,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapEntry(entry: DbEntryFull): Entry {
|
||||||
const v = entry.EntryVersion[0];
|
const v = entry.EntryVersion[0];
|
||||||
if (!v) throw new ErrorNotFound("no version associated with that entry");
|
if (!v) throw new ErrorNotFound("no version associated with that entry");
|
||||||
const x = entry.EntryExecution[0];
|
const x = entry.EntryExecution[0];
|
||||||
|
@ -63,22 +91,7 @@ export function mapEntry(
|
||||||
id: entry.id,
|
id: entry.id,
|
||||||
patient: mapPatient(entry.patient),
|
patient: mapPatient(entry.patient),
|
||||||
created_at: entry.created_at,
|
created_at: entry.created_at,
|
||||||
current_version: {
|
current_version: mapVersion(v),
|
||||||
id: v.id,
|
execution: x ? mapExecution(x) : null,
|
||||||
text: v.text,
|
|
||||||
date: v.date,
|
|
||||||
category: v.category,
|
|
||||||
priority: v.priority,
|
|
||||||
author: v.author,
|
|
||||||
created_at: v.created_at,
|
|
||||||
},
|
|
||||||
execution: x
|
|
||||||
? {
|
|
||||||
id: x.id,
|
|
||||||
author: x.author,
|
|
||||||
created_at: x.created_at,
|
|
||||||
text: x.text,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,43 @@
|
||||||
import type {
|
import type {
|
||||||
Patient,
|
Patient,
|
||||||
PatientNew,
|
PatientNew,
|
||||||
PatientsRequest,
|
|
||||||
Pagination,
|
Pagination,
|
||||||
|
PaginationRequest,
|
||||||
|
PatientsFilter,
|
||||||
} from "$lib/shared/model";
|
} from "$lib/shared/model";
|
||||||
import { ZPatientNew } from "$lib/shared/model/validation";
|
|
||||||
import { prisma } from "$lib/server/prisma";
|
import { prisma } from "$lib/server/prisma";
|
||||||
|
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
||||||
import { mapPatient } from "./mapping";
|
import { mapPatient } from "./mapping";
|
||||||
import { PAGINATION_LIMIT } from "$lib/shared/constants";
|
import { QueryBuilder } from "./util";
|
||||||
import { convertFilterList } from "./util";
|
import { ErrorConflict } from "$lib/shared/util/error";
|
||||||
|
|
||||||
export async function newPatient(patient: PatientNew): Promise<number> {
|
export async function newPatient(patient: PatientNew): Promise<number> {
|
||||||
const data = ZPatientNew.parse(patient);
|
const created = await prisma.patient.create({ data: patient, select: { id: true } });
|
||||||
const created = await prisma.patient.create({ data, select: { id: true } });
|
|
||||||
return created.id;
|
return created.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Update a patient */
|
||||||
|
export async function updatePatient(id: number, patient: Partial<PatientNew>) {
|
||||||
|
await prisma.patient.update({ where: { id }, data: patient });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Delete a patient (Note: this only works if the patient is not associated with any entries) */
|
||||||
|
export async function deletePatient(id: number) {
|
||||||
|
try {
|
||||||
|
await prisma.patient.delete({ where: { id } });
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof PrismaClientKnownRequestError) {
|
||||||
|
// Foreign key constraint failed
|
||||||
|
if (error.code === "P2003") {
|
||||||
|
throw new ErrorConflict("cannot delete patient with entries");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Hide a patient
|
||||||
|
|
||||||
export async function getPatient(id: number): Promise<Patient> {
|
export async function getPatient(id: number): Promise<Patient> {
|
||||||
const patient = await prisma.patient.findUniqueOrThrow({
|
const patient = await prisma.patient.findUniqueOrThrow({
|
||||||
where: { id },
|
where: { id },
|
||||||
|
@ -26,30 +48,68 @@ export async function getPatient(id: number): Promise<Patient> {
|
||||||
return mapPatient(patient);
|
return mapPatient(patient);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPatients(req: PatientsRequest): Promise<Pagination<Patient>> {
|
export async function getPatients(
|
||||||
const offset = req.pagination?.offset || 0;
|
filter: PatientsFilter,
|
||||||
const where = {
|
pagination: PaginationRequest
|
||||||
room_id: convertFilterList(req.filter?.room),
|
): Promise<Pagination<Patient>> {
|
||||||
|
const qb = new QueryBuilder(
|
||||||
|
`select p.id, p.first_name, p.last_name, p.created_at, p.age,
|
||||||
|
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
|
||||||
|
join stations s on s.id = r.station_id`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (filter.search && filter.search.length > 0) {
|
||||||
|
const qvar = qb.pvar();
|
||||||
|
qb.addFilterClause(`p.full_name % ${qvar}`, filter.search);
|
||||||
|
qb.addOrderClause(`similarity(p.full_name, ${qvar}) desc`);
|
||||||
|
}
|
||||||
|
|
||||||
|
qb.addFilterList("r.id", filter.room);
|
||||||
|
qb.addFilterList("s.id", filter.station);
|
||||||
|
qb.addOrderClause("p.created_at desc");
|
||||||
|
qb.setPagination(pagination);
|
||||||
|
|
||||||
|
type RowItem = {
|
||||||
|
id: number;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
created_at: Date;
|
||||||
|
age: number;
|
||||||
|
room_id: number;
|
||||||
|
room_name: string;
|
||||||
|
station_id: number;
|
||||||
|
station_name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [res, countRes] = (await Promise.all([
|
||||||
|
prisma.$queryRawUnsafe(qb.getQuery(), ...qb.getParams()),
|
||||||
|
prisma.$queryRawUnsafe(qb.getCountQuery(), ...qb.getCountParams()),
|
||||||
|
])) as [RowItem[], { count: bigint }[]];
|
||||||
|
|
||||||
|
const total = Number(countRes[0].count);
|
||||||
|
const items: Patient[] = res.map((patient) => {
|
||||||
|
return {
|
||||||
|
id: patient.id,
|
||||||
|
first_name: patient.first_name,
|
||||||
|
last_name: patient.last_name,
|
||||||
|
age: patient.age,
|
||||||
|
created_at: patient.created_at,
|
||||||
room: {
|
room: {
|
||||||
station_id: convertFilterList(req.filter?.station),
|
id: patient.room_id,
|
||||||
|
name: patient.room_name,
|
||||||
|
station: {
|
||||||
|
id: patient.station_id,
|
||||||
|
name: patient.station_name,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const [patients, total] = await Promise.all([
|
});
|
||||||
prisma.patient.findMany({
|
|
||||||
where,
|
|
||||||
include: {
|
|
||||||
room: { include: { station: true } },
|
|
||||||
},
|
|
||||||
orderBy: { created_at: "desc" },
|
|
||||||
skip: offset,
|
|
||||||
take: req.pagination?.limit || PAGINATION_LIMIT,
|
|
||||||
}),
|
|
||||||
prisma.patient.count({ where }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: patients.map(mapPatient),
|
items,
|
||||||
offset,
|
offset: qb.getOffset(),
|
||||||
total,
|
total,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
import type { RoomNew, Room, Station, StationNew } from "$lib/shared/model";
|
import type { RoomNew, Room, Station, StationNew } from "$lib/shared/model";
|
||||||
import { ZRoomNew, ZStationNew } from "$lib/shared/model/validation";
|
|
||||||
import { prisma } from "$lib/server/prisma";
|
import { prisma } from "$lib/server/prisma";
|
||||||
import { mapRoom } from "./mapping";
|
import { mapRoom } from "./mapping";
|
||||||
|
|
||||||
export async function newStation(station: StationNew): Promise<number> {
|
export async function newStation(station: StationNew): Promise<number> {
|
||||||
const data = ZStationNew.parse(station);
|
const created = await prisma.station.create({ data: station, select: { id: true } });
|
||||||
const created = await prisma.station.create({ data, select: { id: true } });
|
|
||||||
return created.id;
|
return created.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateStation(id: number, station: Partial<StationNew>) {
|
||||||
|
await prisma.station.update({ where: { id }, data: station });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteStation(id: number) {
|
||||||
|
await prisma.station.delete({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
export async function getStation(id: number): Promise<Station> {
|
export async function getStation(id: number): Promise<Station> {
|
||||||
return await prisma.station.findUniqueOrThrow({ where: { id } });
|
return await prisma.station.findUniqueOrThrow({ where: { id } });
|
||||||
}
|
}
|
||||||
|
@ -18,11 +24,18 @@ export async function getStations(): Promise<Station[]> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function newRoom(room: RoomNew): Promise<number> {
|
export async function newRoom(room: RoomNew): Promise<number> {
|
||||||
const data = ZRoomNew.parse(room);
|
const created = await prisma.room.create({ data: room, select: { id: true } });
|
||||||
const created = await prisma.room.create({ data, select: { id: true } });
|
|
||||||
return created.id;
|
return created.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateRoom(id: number, room: Partial<RoomNew>) {
|
||||||
|
await prisma.room.update({ where: { id }, data: room });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteRoom(id: number) {
|
||||||
|
await prisma.room.delete({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
export async function getRoom(id: number): Promise<Room> {
|
export async function getRoom(id: number): Promise<Room> {
|
||||||
const room = await prisma.room.findUniqueOrThrow({
|
const room = await prisma.room.findUniqueOrThrow({
|
||||||
where: { id },
|
where: { id },
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Pagination, User, UserTag, UsersRequest } from "$lib/shared/model";
|
import type { Pagination, PaginationRequest, User, UserTag } from "$lib/shared/model";
|
||||||
import { prisma } from "$lib/server/prisma";
|
import { prisma } from "$lib/server/prisma";
|
||||||
import { mapUser, mapUserTag } from "./mapping";
|
import { mapUser, mapUserTag } from "./mapping";
|
||||||
import { PAGINATION_LIMIT } from "$lib/shared/constants";
|
import { PAGINATION_LIMIT } from "$lib/shared/constants";
|
||||||
|
@ -8,13 +8,15 @@ export async function getUser(id: number): Promise<User> {
|
||||||
return mapUser(user);
|
return mapUser(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUsers(req: UsersRequest): Promise<Pagination<UserTag>> {
|
export async function getUsers(
|
||||||
const offset = req.pagination?.offset || 0;
|
pagination: PaginationRequest
|
||||||
|
): Promise<Pagination<UserTag>> {
|
||||||
|
const offset = pagination.offset || 0;
|
||||||
const [users, total] = await Promise.all([
|
const [users, total] = await Promise.all([
|
||||||
prisma.user.findMany({
|
prisma.user.findMany({
|
||||||
orderBy: { id: "asc" },
|
orderBy: { id: "asc" },
|
||||||
skip: offset,
|
skip: offset,
|
||||||
take: req.pagination?.limit || PAGINATION_LIMIT,
|
take: pagination.limit || PAGINATION_LIMIT,
|
||||||
}),
|
}),
|
||||||
prisma.user.count(),
|
prisma.user.count(),
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -108,7 +108,7 @@ export class QueryBuilder {
|
||||||
private selectClause;
|
private selectClause;
|
||||||
private fromClause;
|
private fromClause;
|
||||||
private filterClauses: string[] = [];
|
private filterClauses: string[] = [];
|
||||||
private orderClause = "";
|
private orderClauses: string[] = [];
|
||||||
private params: unknown[] = [];
|
private params: unknown[] = [];
|
||||||
private nP = 0;
|
private nP = 0;
|
||||||
private limit = PAGINATION_LIMIT;
|
private limit = PAGINATION_LIMIT;
|
||||||
|
@ -120,12 +120,12 @@ export class QueryBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
setPagination(pag: PaginationRequest) {
|
setPagination(pag: PaginationRequest) {
|
||||||
this.limit = pag.limit;
|
if (pag.limit) this.limit = pag.limit;
|
||||||
this.offset = pag.offset;
|
if (pag.offset) this.offset = pag.offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
setOrderClause(orderClause: string) {
|
addOrderClause(orderClause: string) {
|
||||||
this.orderClause = orderClause;
|
this.orderClauses.push(orderClause);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get the next parameter variable (e.g. $1) and increment the counter */
|
/** Get the next parameter variable (e.g. $1) and increment the counter */
|
||||||
|
@ -163,7 +163,10 @@ export class QueryBuilder {
|
||||||
queryParts.push("where " + this.filterClauses.join(" and "));
|
queryParts.push("where " + this.filterClauses.join(" and "));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.orderClause.length > 0) queryParts.push(this.orderClause);
|
if (this.orderClauses.length > 0) {
|
||||||
|
queryParts.push(" order by ");
|
||||||
|
queryParts.push(this.orderClauses.join(", "));
|
||||||
|
}
|
||||||
queryParts.push(`limit $${this.nP + 1} offset $${this.nP + 2}`);
|
queryParts.push(`limit $${this.nP + 1} offset $${this.nP + 2}`);
|
||||||
|
|
||||||
return queryParts.join(" ");
|
return queryParts.join(" ");
|
||||||
|
|
12
src/lib/server/util.ts
Normal file
12
src/lib/server/util.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import type { RequestEvent } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
export async function userId(event: RequestEvent): Promise<number> {
|
||||||
|
const sess = await event.locals.getSession();
|
||||||
|
const id = Number(sess?.user?.id);
|
||||||
|
if (id) {
|
||||||
|
return id;
|
||||||
|
} else {
|
||||||
|
// This should never happen, since unauthorized requests are caught by hooks.server.ts
|
||||||
|
throw new Error("no user id");
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,19 @@ export type Pagination<T> = {
|
||||||
offset: number;
|
offset: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ApiBody<T> = {
|
||||||
|
body: T;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiError = {
|
||||||
|
status: number;
|
||||||
|
msg: string;
|
||||||
|
} & Partial<{ data: unknown }>;
|
||||||
|
|
||||||
|
export type ApiCreated = {
|
||||||
|
id: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
id: number;
|
id: number;
|
||||||
name: Option<string>;
|
name: Option<string>;
|
||||||
|
@ -22,9 +35,7 @@ export type Station = {
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StationNew = {
|
export type StationNew = Omit<Station, "id">;
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Room = {
|
export type Room = {
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -44,11 +55,7 @@ export type Category = {
|
||||||
description: Option<string>;
|
description: Option<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CategoryNew = {
|
export type CategoryNew = Omit<Category, "id">;
|
||||||
name: string;
|
|
||||||
color: Option<string>;
|
|
||||||
description: Option<string>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Patient = {
|
export type Patient = {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
export type PaginationRequest = {
|
export type PaginationRequest = Partial<{
|
||||||
limit: number;
|
limit: number;
|
||||||
offset: number;
|
offset: number;
|
||||||
};
|
}>;
|
||||||
|
|
||||||
export type FilterList<T> = T | T[];
|
export type FilterList<T> = T | T[];
|
||||||
|
|
||||||
|
@ -17,19 +17,8 @@ export type EntriesFilter = Partial<{
|
||||||
priority: boolean;
|
priority: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type EntriesRequest = Partial<{
|
|
||||||
filter: EntriesFilter;
|
|
||||||
pagination: PaginationRequest;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export type UsersRequest = Partial<{ pagination: PaginationRequest }>;
|
|
||||||
|
|
||||||
export type PatientsFilter = Partial<{
|
export type PatientsFilter = Partial<{
|
||||||
|
search: string;
|
||||||
station: FilterList<number>;
|
station: FilterList<number>;
|
||||||
room: FilterList<number>;
|
room: FilterList<number>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type PatientsRequest = Partial<{
|
|
||||||
filter: PatientsFilter;
|
|
||||||
pagination: PaginationRequest;
|
|
||||||
}>;
|
|
||||||
|
|
25
src/lib/shared/model/validation.test.ts
Normal file
25
src/lib/shared/model/validation.test.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
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);
|
||||||
|
});
|
|
@ -6,6 +6,7 @@ import type {
|
||||||
EntryNew,
|
EntryNew,
|
||||||
EntryVersionNdata,
|
EntryVersionNdata,
|
||||||
EntryVersionNew,
|
EntryVersionNew,
|
||||||
|
PaginationRequest,
|
||||||
PatientNew,
|
PatientNew,
|
||||||
RoomNew,
|
RoomNew,
|
||||||
StationNew,
|
StationNew,
|
||||||
|
@ -61,3 +62,38 @@ export const ZEntryExecutionNew = implement<EntryExecutionNew>().with({
|
||||||
old_execution: ZEntityId.optional().nullable(),
|
old_execution: ZEntityId.optional().nullable(),
|
||||||
text: ZTextString,
|
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 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,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ZPatientsFilterUrl = z.object({
|
||||||
|
room: ZFilterListUrl,
|
||||||
|
station: ZFilterListUrl,
|
||||||
|
});
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import api from "$api";
|
||||||
|
|
||||||
|
api.category
|
||||||
|
.id$(1)
|
||||||
|
.GET()
|
||||||
|
.Ok((resp) => console.log(resp.body));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1 class="text-4xl">Planung</h1>
|
<h1 class="text-4xl">Planung</h1>
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
import { json } from "@sveltejs/kit";
|
|
||||||
|
|
||||||
export const GET = () => {
|
|
||||||
return json({ hello: "world" });
|
|
||||||
};
|
|
21
src/routes/api/category/+server.ts
Normal file
21
src/routes/api/category/+server.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
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 };
|
||||||
|
});
|
||||||
|
};
|
35
src/routes/api/category/[id]/+server.ts
Normal file
35
src/routes/api/category/[id]/+server.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
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 };
|
||||||
|
});
|
||||||
|
};
|
15
src/routes/api/entry/+server.ts
Normal file
15
src/routes/api/entry/+server.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
};
|
13
src/routes/api/entry/[id]/+server.ts
Normal file
13
src/routes/api/entry/[id]/+server.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
};
|
18
src/routes/api/entry/[id]/execution/+server.ts
Normal file
18
src/routes/api/entry/[id]/execution/+server.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
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 };
|
||||||
|
});
|
||||||
|
};
|
26
src/routes/api/entry/[id]/version/+server.ts
Normal file
26
src/routes/api/entry/[id]/version/+server.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
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 };
|
||||||
|
});
|
||||||
|
};
|
15
src/routes/api/patient/+server.ts
Normal file
15
src/routes/api/patient/+server.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
};
|
35
src/routes/api/patient/[id]/+server.ts
Normal file
35
src/routes/api/patient/[id]/+server.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
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 };
|
||||||
|
});
|
||||||
|
};
|
20
src/routes/api/room/+server.ts
Normal file
20
src/routes/api/room/+server.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
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 };
|
||||||
|
});
|
||||||
|
};
|
35
src/routes/api/room/[id]/+server.ts
Normal file
35
src/routes/api/room/[id]/+server.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
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 };
|
||||||
|
});
|
||||||
|
};
|
20
src/routes/api/station/+server.ts
Normal file
20
src/routes/api/station/+server.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
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 };
|
||||||
|
});
|
||||||
|
};
|
35
src/routes/api/station/[id]/+server.ts
Normal file
35
src/routes/api/station/[id]/+server.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
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 };
|
||||||
|
});
|
||||||
|
};
|
14
src/routes/api/user/+server.ts
Normal file
14
src/routes/api/user/+server.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
};
|
|
@ -12,6 +12,11 @@ const config = {
|
||||||
adapter: adapter({
|
adapter: adapter({
|
||||||
precompress: true,
|
precompress: true,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
alias: {
|
||||||
|
$api: "./src/api",
|
||||||
|
$tests: "./tests",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,10 @@ const N_USERS = 10;
|
||||||
const N_ROOMS = 20;
|
const N_ROOMS = 20;
|
||||||
const N_PATIENTS = 50;
|
const N_PATIENTS = 50;
|
||||||
|
|
||||||
|
function randomId(len: number): number {
|
||||||
|
return faker.number.int({ min: 1, max: len - 1 });
|
||||||
|
}
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
// Reset database
|
// Reset database
|
||||||
await prisma.$transaction([
|
await prisma.$transaction([
|
||||||
|
@ -52,10 +56,6 @@ export default async () => {
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.map((l) => JSON.parse(l));
|
.map((l) => JSON.parse(l));
|
||||||
|
|
||||||
function randomId(len: number): number {
|
|
||||||
return faker.number.int({ min: 1, max: len - 1 });
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 1; i <= N_USERS; i++) {
|
for (let i = 1; i <= N_USERS; i++) {
|
||||||
const firstName = faker.person.firstName();
|
const firstName = faker.person.firstName();
|
||||||
const lastName = faker.person.lastName();
|
const lastName = faker.person.lastName();
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { getCategories, getCategory, newCategory } from "$lib/server/query";
|
import { getCategories, getCategory, newCategory } from "$lib/server/query";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
// eslint-disable-next-line no-relative-import-paths/no-relative-import-paths
|
import { CATEGORIES } from "$tests/helpers/testdata";
|
||||||
import { CATEGORIES } from "../../helpers/testdata";
|
|
||||||
|
|
||||||
test("create category", async () => {
|
test("create category", async () => {
|
||||||
const data = {
|
const data = {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {
|
import {
|
||||||
getEntries,
|
getEntries,
|
||||||
getEntry,
|
getEntry,
|
||||||
|
getEntryHistory,
|
||||||
newEntry,
|
newEntry,
|
||||||
newEntryExecution,
|
newEntryExecution,
|
||||||
newEntryVersion,
|
newEntryVersion,
|
||||||
|
@ -49,13 +50,20 @@ test("create entry version", async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const entry = await getEntry(eId);
|
const entry = await getEntry(eId);
|
||||||
expect(entry.current_version).toMatchObject({
|
const expectedVersion = {
|
||||||
author: { id: 1 },
|
author: { id: 1 },
|
||||||
date,
|
date,
|
||||||
text,
|
text,
|
||||||
category: { id: 2 },
|
category: { id: 2 },
|
||||||
priority: true,
|
priority: true,
|
||||||
});
|
};
|
||||||
|
expect(entry.current_version).toMatchObject(expectedVersion);
|
||||||
|
|
||||||
|
const history = await getEntryHistory(eId);
|
||||||
|
expect(history).length(2);
|
||||||
|
expect(history[0]).toMatchObject(expectedVersion);
|
||||||
|
expect(history[1].text).toBe(TEST_VERSION.text);
|
||||||
|
expect(history[0].created_at).greaterThan(history[1].created_at);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("create entry version (partial)", async () => {
|
test("create entry version (partial)", async () => {
|
||||||
|
@ -86,6 +94,14 @@ test("create entry version (wrong old vid)", async () => {
|
||||||
patient_id: 1,
|
patient_id: 1,
|
||||||
version: TEST_VERSION,
|
version: TEST_VERSION,
|
||||||
});
|
});
|
||||||
|
const entry = await getEntry(eId);
|
||||||
|
|
||||||
|
expect(async () => {
|
||||||
|
await newEntryVersion(1, eId, {
|
||||||
|
old_version: entry.current_version.id + 1,
|
||||||
|
text: "Hello World",
|
||||||
|
});
|
||||||
|
}).rejects.toThrowError(new ErrorConflict("old version id does not match"));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("create entry execution", async () => {
|
test("create entry execution", async () => {
|
||||||
|
@ -125,8 +141,8 @@ test("create entry execution (wrong old xid)", async () => {
|
||||||
});
|
});
|
||||||
const x1 = await newEntryExecution(1, eId, { old_execution: null, text: "x1" });
|
const x1 = await newEntryExecution(1, eId, { old_execution: null, text: "x1" });
|
||||||
|
|
||||||
expect(async () =>
|
expect(
|
||||||
newEntryExecution(1, eId, { old_execution: x1 + 1, text: "x2" })
|
async () => await newEntryExecution(1, eId, { old_execution: x1 + 1, text: "x2" })
|
||||||
).rejects.toThrowError(new ErrorConflict("old execution id does not match"));
|
).rejects.toThrowError(new ErrorConflict("old execution id does not match"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -172,65 +188,65 @@ async function insertTestEntries() {
|
||||||
|
|
||||||
test("get entries", async () => {
|
test("get entries", async () => {
|
||||||
const { eId1, eId2, eId3 } = await insertTestEntries();
|
const { eId1, eId2, eId3 } = await insertTestEntries();
|
||||||
const entries = await getEntries({});
|
const entries = await getEntries({}, {});
|
||||||
expect(entries.items).length(3);
|
expect(entries.items).length(3);
|
||||||
expect(entries.total).toBe(3);
|
expect(entries.total).toBe(3);
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
|
|
||||||
const entriesLim2 = await getEntries({ pagination: { limit: 2, offset: 0 } });
|
const entriesLim2 = await getEntries({}, { limit: 2, offset: 0 });
|
||||||
expect(entriesLim2.items).length(2);
|
expect(entriesLim2.items).length(2);
|
||||||
expect(entriesLim2.total).toBe(3);
|
expect(entriesLim2.total).toBe(3);
|
||||||
|
|
||||||
const entriesLim2Offset = await getEntries({ pagination: { limit: 2, offset: 2 } });
|
const entriesLim2Offset = await getEntries({}, { limit: 2, offset: 2 });
|
||||||
expect(entriesLim2Offset.items).length(1);
|
expect(entriesLim2Offset.items).length(1);
|
||||||
expect(entriesLim2Offset.offset).toBe(2);
|
expect(entriesLim2Offset.offset).toBe(2);
|
||||||
expect(entriesLim2Offset.total).toBe(3);
|
expect(entriesLim2Offset.total).toBe(3);
|
||||||
|
|
||||||
// Filter by category
|
// Filter by category
|
||||||
const entriesCategory = await getEntries({ filter: { category: 3 } });
|
const entriesCategory = await getEntries({ category: 3 }, {});
|
||||||
expect(entriesCategory.items).length(1);
|
expect(entriesCategory.items).length(1);
|
||||||
expect(entriesCategory.total).toBe(1);
|
expect(entriesCategory.total).toBe(1);
|
||||||
expect(entriesCategory.items[0].id).toBe(eId1);
|
expect(entriesCategory.items[0].id).toBe(eId1);
|
||||||
|
|
||||||
// Filter by author
|
// Filter by author
|
||||||
const entriesAuthor = await getEntries({ filter: { author: 2 } });
|
const entriesAuthor = await getEntries({ author: 2 }, {});
|
||||||
expect(entriesAuthor.items).length(1);
|
expect(entriesAuthor.items).length(1);
|
||||||
expect(entriesAuthor.total).toBe(1);
|
expect(entriesAuthor.total).toBe(1);
|
||||||
expect(entriesAuthor.items[0].id).toBe(eId1);
|
expect(entriesAuthor.items[0].id).toBe(eId1);
|
||||||
|
|
||||||
// Filter by executor
|
// Filter by executor
|
||||||
const entriesExecutor = await getEntries({ filter: { executor: 1 } });
|
const entriesExecutor = await getEntries({ executor: 1 }, {});
|
||||||
expect(entriesExecutor.items).length(1);
|
expect(entriesExecutor.items).length(1);
|
||||||
expect(entriesExecutor.total).toBe(1);
|
expect(entriesExecutor.total).toBe(1);
|
||||||
expect(entriesExecutor.items[0].id).toBe(eId2);
|
expect(entriesExecutor.items[0].id).toBe(eId2);
|
||||||
|
|
||||||
// Filter by patient
|
// Filter by patient
|
||||||
const entriesPatient = await getEntries({ filter: { patient: 1 } });
|
const entriesPatient = await getEntries({ patient: 1 }, {});
|
||||||
expect(entriesPatient.items).length(2);
|
expect(entriesPatient.items).length(2);
|
||||||
expect(entriesPatient.total).toBe(2);
|
expect(entriesPatient.total).toBe(2);
|
||||||
expect(entriesPatient.items[0].id).toBe(eId3);
|
expect(entriesPatient.items[0].id).toBe(eId3);
|
||||||
expect(entriesPatient.items[1].id).toBe(eId1);
|
expect(entriesPatient.items[1].id).toBe(eId1);
|
||||||
|
|
||||||
// Filter by room
|
// Filter by room
|
||||||
const entriesRoom = await getEntries({ filter: { room: 1 } });
|
const entriesRoom = await getEntries({ room: 1 }, {});
|
||||||
expect(entriesRoom).toStrictEqual(entriesPatient);
|
expect(entriesRoom).toStrictEqual(entriesPatient);
|
||||||
|
|
||||||
// Filter done
|
// Filter done
|
||||||
const entriesDone = await getEntries({ filter: { done: true } });
|
const entriesDone = await getEntries({ done: true }, {});
|
||||||
expect(entriesDone.items).length(2);
|
expect(entriesDone.items).length(2);
|
||||||
expect(entriesDone.total).toBe(2);
|
expect(entriesDone.total).toBe(2);
|
||||||
expect(entriesDone.items[0].id).toBe(eId3);
|
expect(entriesDone.items[0].id).toBe(eId3);
|
||||||
expect(entriesDone.items[1].id).toBe(eId2);
|
expect(entriesDone.items[1].id).toBe(eId2);
|
||||||
|
|
||||||
// Filter by priority
|
// Filter by priority
|
||||||
const entriesPrio = await getEntries({ filter: { priority: true } });
|
const entriesPrio = await getEntries({ priority: true }, {});
|
||||||
expect(entriesPrio.items).length(1);
|
expect(entriesPrio.items).length(1);
|
||||||
expect(entriesPrio.total).toBe(1);
|
expect(entriesPrio.total).toBe(1);
|
||||||
expect(entriesPrio.items[0].id).toBe(eId1);
|
expect(entriesPrio.items[0].id).toBe(eId1);
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
const entriesSearch = await getEntries({ filter: { search: "Blu" } });
|
const entriesSearch = await getEntries({ search: "Blu" }, {});
|
||||||
expect(entriesSearch.items).length(1);
|
expect(entriesSearch.items).length(1);
|
||||||
expect(entriesSearch.total).toBe(1);
|
expect(entriesSearch.total).toBe(1);
|
||||||
expect(entriesSearch.items[0].id).toBe(eId1);
|
expect(entriesSearch.items[0].id).toBe(eId1);
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
import { newPatient, getPatient, getPatients } from "$lib/server/query";
|
import {
|
||||||
|
newPatient,
|
||||||
|
getPatient,
|
||||||
|
getPatients,
|
||||||
|
updatePatient,
|
||||||
|
deletePatient,
|
||||||
|
newEntry,
|
||||||
|
} from "$lib/server/query";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
// eslint-disable-next-line no-relative-import-paths/no-relative-import-paths
|
import { S1, S2 } from "$tests/helpers/testdata";
|
||||||
import { S1, S2 } from "../../helpers/testdata";
|
|
||||||
|
|
||||||
test("create patient", async () => {
|
test("create patient", async () => {
|
||||||
const pId = await newPatient({
|
const pId = await newPatient({
|
||||||
|
@ -20,8 +26,47 @@ test("create patient", async () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("update patient", async () => {
|
||||||
|
const data = {
|
||||||
|
first_name: "Max",
|
||||||
|
last_name: "Mustermann",
|
||||||
|
age: 66,
|
||||||
|
room_id: 2,
|
||||||
|
};
|
||||||
|
await updatePatient(1, data);
|
||||||
|
const patient = await getPatient(1);
|
||||||
|
expect(patient).toMatchObject({
|
||||||
|
first_name: "Max",
|
||||||
|
last_name: "Mustermann",
|
||||||
|
age: 66,
|
||||||
|
room: { id: 2 },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("delete patient", async () => {
|
||||||
|
await deletePatient(1);
|
||||||
|
expect(async () => await getPatient(1)).rejects.toThrowError("No Patient found");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("delete patient (restricted)", async () => {
|
||||||
|
// Patients should not be deleted if they have at least 1 entry
|
||||||
|
const pId = await newEntry(1, {
|
||||||
|
patient_id: 1,
|
||||||
|
version: {
|
||||||
|
category_id: null,
|
||||||
|
date: new Date(2024, 1, 1),
|
||||||
|
priority: false,
|
||||||
|
text: "Hello World",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(async () => await deletePatient(pId)).rejects.toThrowError(
|
||||||
|
"cannot delete patient with entries"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("get patients", async () => {
|
test("get patients", async () => {
|
||||||
const patients = await getPatients({});
|
const patients = await getPatients({}, {});
|
||||||
expect(patients).toMatchObject({
|
expect(patients).toMatchObject({
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
|
@ -52,7 +97,7 @@ test("get patients", async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("get patients (pagination)", async () => {
|
test("get patients (pagination)", async () => {
|
||||||
const patients = await getPatients({ pagination: { offset: 1, limit: 100 } });
|
const patients = await getPatients({}, { offset: 1, limit: 100 });
|
||||||
expect(patients.items).length(2);
|
expect(patients.items).length(2);
|
||||||
expect(patients.items[0].id).toBe(2);
|
expect(patients.items[0].id).toBe(2);
|
||||||
expect(patients.items[1].id).toBe(3);
|
expect(patients.items[1].id).toBe(3);
|
||||||
|
@ -61,14 +106,20 @@ test("get patients (pagination)", async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("get patients (by room)", async () => {
|
test("get patients (by room)", async () => {
|
||||||
const patients = await getPatients({ filter: { room: 1 } });
|
const patients = await getPatients({ room: 1 }, {});
|
||||||
expect(patients.items).length(1);
|
expect(patients.items).length(1);
|
||||||
expect(patients.items[0].id).toBe(1);
|
expect(patients.items[0].id).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("get patients (by station)", async () => {
|
test("get patients (by station)", async () => {
|
||||||
const patients = await getPatients({ filter: { station: 1 } });
|
const patients = await getPatients({ station: 1 }, {});
|
||||||
expect(patients.items).length(2);
|
expect(patients.items).length(2);
|
||||||
expect(patients.items[0].id).toBe(1);
|
expect(patients.items[0].id).toBe(1);
|
||||||
expect(patients.items[1].id).toBe(2);
|
expect(patients.items[1].id).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("search patients", async () => {
|
||||||
|
const patients = await getPatients({ search: "Schustr" }, {});
|
||||||
|
expect(patients.items).length(1);
|
||||||
|
expect(patients.items[0].id).toBe(3);
|
||||||
|
});
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
import {
|
import {
|
||||||
|
deleteStation,
|
||||||
getRoom,
|
getRoom,
|
||||||
getRooms,
|
getRooms,
|
||||||
getStation,
|
getStation,
|
||||||
getStations,
|
getStations,
|
||||||
newRoom,
|
newRoom,
|
||||||
newStation,
|
newStation,
|
||||||
|
updateStation,
|
||||||
} from "$lib/server/query";
|
} from "$lib/server/query";
|
||||||
import type { Room, Station } from "$lib/shared/model";
|
import type { Room, Station } from "$lib/shared/model";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
// eslint-disable-next-line no-relative-import-paths/no-relative-import-paths
|
import { S1, S2 } from "$tests/helpers/testdata";
|
||||||
import { S1, S2 } from "../../helpers/testdata";
|
|
||||||
|
|
||||||
test("create station", async () => {
|
test("create station", async () => {
|
||||||
const sId = await newStation({ name: "S3" });
|
const sId = await newStation({ name: "S3" });
|
||||||
|
@ -17,6 +18,19 @@ test("create station", async () => {
|
||||||
expect(station).toStrictEqual({ id: sId, name: "S3" } satisfies Station);
|
expect(station).toStrictEqual({ id: sId, name: "S3" } satisfies Station);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("update station", async () => {
|
||||||
|
const name = "HelloStation";
|
||||||
|
await updateStation(S1.id, { name });
|
||||||
|
const station = await getStation(S1.id);
|
||||||
|
expect(station.id).toBe(S1.id);
|
||||||
|
expect(station.name).toBe(name);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("delete station", async () => {
|
||||||
|
await deleteStation(S1.id);
|
||||||
|
expect(async () => await getStation(S1.id)).rejects.toThrowError("No Station found");
|
||||||
|
});
|
||||||
|
|
||||||
test("get stations", async () => {
|
test("get stations", async () => {
|
||||||
const stations = await getStations();
|
const stations = await getStations();
|
||||||
expect(stations).toStrictEqual([S1, S2]);
|
expect(stations).toStrictEqual([S1, S2]);
|
||||||
|
@ -32,6 +46,19 @@ test("create room", async () => {
|
||||||
} satisfies Room);
|
} satisfies Room);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("update room", async () => {
|
||||||
|
const name = "HelloStation";
|
||||||
|
await updateStation(S1.id, { name });
|
||||||
|
const station = await getStation(S1.id);
|
||||||
|
expect(station.id).toBe(S1.id);
|
||||||
|
expect(station.name).toBe(name);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("delete room", async () => {
|
||||||
|
await deleteStation(S1.id);
|
||||||
|
expect(async () => await getStation(S1.id)).rejects.toThrowError("No Station found");
|
||||||
|
});
|
||||||
|
|
||||||
test("get rooms", async () => {
|
test("get rooms", async () => {
|
||||||
const rooms = await getRooms();
|
const rooms = await getRooms();
|
||||||
expect(rooms).toStrictEqual([
|
expect(rooms).toStrictEqual([
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { sveltekit } from "@sveltejs/kit/vite";
|
import { sveltekit } from "@sveltejs/kit/vite";
|
||||||
import { defineConfig } from "vitest/config";
|
import { defineConfig } from "vitest/config";
|
||||||
|
import { zeroAPI } from "sveltekit-zero-api";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [sveltekit()],
|
plugins: [sveltekit(), zeroAPI()],
|
||||||
test: {
|
test: {
|
||||||
include: ["src/**/*.{test,spec}.{js,ts}"],
|
include: ["src/**/*.{test,spec}.{js,ts}"],
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue