tRPC Hinzugefügt

This commit is contained in:
Moritz Utcke
2024-01-07 22:58:12 +07:00
parent dfd7cce6c8
commit ff16c3b547
39 changed files with 1302 additions and 1652 deletions

View File

@@ -1,137 +0,0 @@
import type { APIRoute } from "astro";
import { error, success } from "src/lib/APIResponse";
import { z } from "zod";
const AusweisUploadChecker = z.object({
typ: z.enum(["VA", "BA", "VANW"]),
objekt: z.object({
typ: z.string(),
plz: z.string().min(4).max(5),
ort: z.string(),
strasse: z.string(),
baujahr: z.number(),
saniert: z.boolean(),
gebaeudeteil: z.enum(["Gesamtgebäude", "Wohnen"]),
einheiten: z.number(),
}),
heizquellen: z
.array(
z.object({
verbrauch: z.array(z.number()).max(3).min(3),
einheit: z.string(),
brennstoff: z.string(),
anteil_warmwasser: z.number(),
})
)
.max(2)
.min(1),
energieverbrauch_zeitraum: z.date(),
wohnflaeche: z.number(),
keller_beheizt: z.boolean(),
dachgeschoss: z.number(),
zusaetzliche_heizquelle: z.boolean(),
warmwasser_enthalten: z.boolean(),
lueftungskonzept: z.enum([
"Fensterlüftung",
"Schachtlüftung",
"Lüftungsanlage ohne Wärmerückgewinnung",
"Lüftungsanlage mit Wärmerückgewinnung",
]),
wird_gekuehlt: z.boolean(),
leerstand: z.number(),
images: z.array(z.string()),
versorgungssysteme: z.number(),
fenster_dach: z.number(),
energiequelle_2_nutzung: z.number(),
daemmung: z.number(),
/**
* Bedarfsausweis spezifische Eigenschaften
*/
anzahl_vollgeschosse: z.number(),
geschosshoehe: z.number(),
anzahl_gauben: z.number(),
breite_gauben: z.number(),
masse_a: z.number(),
masse_b: z.number(),
masse_c: z.number(),
masse_d: z.number(),
masse_e: z.number(),
masse_f: z.number(),
fensterflaeche_so_sw: z.number(),
fensterflaeche_nw_no: z.number(),
aussenwandflaeche_unbeheizt: z.number(),
dachflaeche: z.number(),
dach_u_wert: z.number(),
deckenflaeche: z.number(),
decke_u_wert: z.number(),
aussenwand_flaeche: z.number(),
aussenwand_u_wert: z.number(),
fussboden_flaeche: z.number(),
fussboden_u_wert: z.number(),
volumen: z.number(),
dicht: z.boolean(),
fenster_flaeche_1: z.number(),
fenster_art_1: z.number(),
fenster_flaeche_2: z.number(),
fenster_art_2: z.number(),
dachfenster_flaeche: z.number(),
dachfenster_art: z.number(),
haustuer_flaeche: z.number(),
haustuer_art: z.number(),
dach_bauart: z.string(),
dach_daemmung: z.number(),
decke_bauart: z.string(),
decke_daemmung: z.number(),
aussenwand_bauart: z.string(),
aussenwand_daemmung: z.number(),
boden_bauart: z.string(),
boden_daemmung: z.number(),
warmwasser_verteilung: z.string(),
warmwasser_speicherung: z.string(),
warmwasser_erzeugung: z.string(),
heizung_zentral: z.boolean(),
heizung_verteilung: z.string(),
heizung_speicherung: z.string(),
waerme_erzeugung_heizung: z.string(),
anteil_zusatzheizung: z.number(),
kollektor_flaeche: z.number(),
// VANW
vanw_stromverbrauch_enthalten: z.number(),
vanw_stromverbrauch_sonstige: z.string(),
vanw_strom_1: z.number(),
vanw_strom_2: z.number(),
vanw_strom_3: z.number(),
erledigt: z.boolean(),
anrede: z.string(),
name: z.string(),
vorname: z.string(),
email: z.string(),
telefonnummer: z.string(),
});
/**
* Erstellt einen Verbrauchsausweis anhand der gegebenen Daten und trägt ihn in die Datenbank ein.
* @param param0
* @returns
*/
export const post: APIRoute = async ({ request }) => {
const body = await request.json();
const result = AusweisUploadChecker.safeParse(body);
if (!result.success) {
return error(result.error.issues);
}
return success({});
};

View File

@@ -1,149 +0,0 @@
import { prisma } from "@ibcornelsen/database";
import type { APIRoute } from "astro";
import { ActionFailedError, MissingEntityError, error, success } from "src/lib/APIResponse";
import { Ausweis } from "src/lib/Ausweis/Ausweis";
import { Gebaeude } from "src/lib/Gebaeude";
import { z } from "zod";
const AusweisUploadChecker = z.object({
ausweis: z.object({
ausweisart: z.enum(["VA", "BA", "VANW"]),
ausstellgrund: z.enum([
"Vermietung",
"Neubau",
"Verkauf",
"Modernisierung",
"Sonstiges",
]),
}),
gebaeude: z.object({
typ: z.string(),
plz: z.string(),
ort: z.string(),
strasse: z.string(),
gebaeudeteil: z.string(),
saniert: z.boolean(),
baujahr: z.number(),
einheiten: z.number(),
wohnflaeche: z.number(),
keller_beheizt: z.boolean(),
dachgeschoss_beheizt: z.number(),
lueftungskonzept: z.enum([
"Fensterlüftung",
"Schachtlüftung",
"Lüftungsanlage ohne Wärmerückgewinnung",
"Lüftungsanlage mit Wärmerückgewinnung",
]),
wird_gekuehlt: z.boolean(),
leerstand: z.number(),
versorgungssysteme: z.number(),
fenster_dach: z.number(),
energiequelle_2_nutzung: z.number(),
daemmung: z.number(),
}),
kennwerte: z.object({
zeitraum: z.string(),
verbrauch_1: z.number(),
verbrauch_2: z.number(),
verbrauch_3: z.number(),
verbrauch_4: z.number(),
verbrauch_5: z.number(),
verbrauch_6: z.number(),
einheit_1: z.string(),
einheit_2: z.string(),
energietraeger_1: z.string(),
energietraeger_2: z.string(),
anteil_warmwasser_1: z.number(),
anteil_warmwasser_2: z.number(),
}),
gebaeude_uid: z.string().optional(),
kennwerte_uid: z.string().optional(),
ausweis_uid: z.string().optional(),
});
const AusweisDownloadChecker = z.object({
uid: z.string()
})
/**
* Erstellt einen Verbrauchsausweis anhand der gegebenen Daten und trägt ihn in die Datenbank ein.
* @param param0
* @returns
*/
export const post: APIRoute = async ({ request }) => {
const body: z.infer<typeof AusweisUploadChecker> = await request.json();
const validation = AusweisUploadChecker.safeParse(body);
if (!validation.success) {
return error(validation.error.issues);
}
let gebaeude, ausweis;
if (body.gebaeude_uid) {
gebaeude = await prisma.gebaeudeStammdaten.update({
where: {
uid: body.gebaeude_uid,
},
data: body.gebaeude,
})
} else {
gebaeude = await prisma.gebaeudeStammdaten.create({
data: body.gebaeude,
})
}
if (!gebaeude) {
return ActionFailedError();
}
if (body.ausweis_uid) {
ausweis = await prisma.verbrauchsausweisWohnen.update({
where: {
uid: body.ausweis_uid,
},
data: body.ausweis,
})
} else {
ausweis = await prisma.verbrauchsausweisWohnen.create({
data: body.ausweis,
})
}
if (!ausweis) {
return ActionFailedError();
}
return success({
ausweis: ausweis,
gebaeude: gebaeude,
});
};
export const get: APIRoute = async ({ request }) => {
const body: z.infer<typeof AusweisDownloadChecker> = await request.json();
const validation = AusweisDownloadChecker.safeParse(body);
if (!validation.success) {
return error(validation.error.issues);
}
const ausweis = await prisma.verbrauchsausweisWohnen.findUnique({
where: {
uid: body.uid,
},
include: {
gebaeude_stammdaten: true,
}
})
if (!ausweis) {
return MissingEntityError("gebäude");
}
return success(ausweis);
};

View File

@@ -1,39 +0,0 @@
import type { APIRoute } from "astro";
import {
MissingEntityError,
error,
success,
} from "src/lib/APIResponse";
import { prisma } from "@ibcornelsen/database";
export const get: APIRoute = async ({ url }) => {
const body = url.searchParams
const uid = body.get("uid")
if (!body.has("uid") || !uid) {
return error(["Missing 'uid' in request body."])
}
const gebaeude = await prisma.gebaeudeStammdaten.findUnique({
where: {
uid: uid
}
})
if (!gebaeude) {
return MissingEntityError("gebaeude")
}
const images = await prisma.gebaeudeBilder.findMany({
where: {
gebaeude_stammdaten_id: gebaeude.id
},
select: {
uid: true,
kategorie: true
}
})
return success(images);
};

View File

@@ -1,63 +0,0 @@
import type { APIRoute } from "astro";
import moment from "moment";
import { ActionFailedError, MissingPropertyError, error, success } from "src/lib/APIResponse";
import { getClimateFactor } from "src/lib/Klimafaktoren/getClimateFactor";
export const get: APIRoute = async function({ url }) {
const body = url.searchParams;
let zip = body.get("zip");
if (!body.get("date")) {
return MissingPropertyError(["date"]);
}
let accuracy = body.get("accuracy") || "months";
if (accuracy !== "months" && accuracy !== "years") {
return error(["Accuracy must be either 'months' or 'years'."])
}
if (!zip) {
return error(["Invalid ZIP Code, must be 4 or 5 characters long."])
}
let start = moment(body.get("date"));
let end = moment(body.get("date")).add("2", "years");
if (!start.isValid()) {
return error(["Invalid start date given."]);
}
if (!end.isValid()) {
return error(["Invalid end date given."]);
}
if (start.isSameOrAfter(end)) {
return error(["Start date not before end date."])
}
const intervals = [];
let currentDate = start.clone();
while (currentDate.isSameOrBefore(end)) {
let copy = currentDate.clone();
intervals.push(copy);
currentDate.add(1, accuracy);
}
const klimafaktoren = await getClimateFactor(intervals, zip);
if (!klimafaktoren) {
return ActionFailedError();
}
if (klimafaktoren.length !== intervals.length) {
return error(["Not all dates could be found."]);
}
return success(klimafaktoren.map(klimafaktor => ({
month: klimafaktor.month,
year: klimafaktor.year,
klimafaktor: klimafaktor.klimafaktor,
})));
}

View File

@@ -0,0 +1,31 @@
// NOTE: Öffentliche API benötigt OpenApiMeta. Das Package bräuchte momentan noch einen extra Server, deshalb nehmen wir es momentan noch nicht mit rein.
//import { OpenApiMeta } from "trpc-openapi";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { APIRoute } from "astro";
import { t } from "./context";
import { v1Router } from "./procedures/v1";
export const AppRouter = t.router({
v1: v1Router,
})
export const all: APIRoute = ({ request }) => {
console.log(request);
return fetchRequestHandler({
req: request,
endpoint: "/api/trpc",
router: AppRouter,
createContext: async ({ req }) => {
return { uid: req.headers.get("X-Session") ?? undefined };
},
});
};
export function tRPCCaller(request: Request) {
const { uid } = { uid: request.headers.get("Authorization") || "" };
const createCaller = t.createCallerFactory(AppRouter);
return createCaller({ uid });
}
export type AppRouter = typeof AppRouter;

View File

@@ -0,0 +1,34 @@
import { TRPCError, initTRPC } from "@trpc/server";
import { ZodError } from "zod";
import { OpenApiMeta } from "trpc-openapi";
type Context = { uid?: string };
export const t = initTRPC.context<Context>().meta<OpenApiMeta>().create({
errorFormatter(opts) {
const { shape, error } = opts;
return {
success: false,
...shape,
data: {
zodError:
error.code === 'BAD_REQUEST' && error.cause instanceof ZodError
? error.cause.flatten()
: null,
},
};
}
});
export const publicProcedure = t.procedure
export const privateProcedure = t.procedure.use((opts) => {
if (!opts.ctx.uid) {
throw new TRPCError({
code: 'FORBIDDEN',
message: "Diese Ressource benötigt eine UID, welche im 'Authorization' Header gegeben sein muss.",
});
}
return opts.next();
});

View File

@@ -0,0 +1,42 @@
import { z } from "zod";
import { t } from "../../context";
import { tRPCKlimafaktorenProcedure } from "./klimafaktoren";
import { VerbrauchsausweisWohnen2016Erstellen } from "./verbrauchsausweis-wohnen/2016/erstellen";
const router = t.router;
export const v1Router = router({
verbrauchsausweisWohnen: router({
2016: router({
erstellen: VerbrauchsausweisWohnen2016Erstellen
}),
2023: router({
})
}),
verbrauchsausweisGewerbe: router({
2016: router({
}),
2023: router({
})
}),
bedarfsausweisWohen: router({
2016: router({
}),
2023: router({
})
}),
klimafaktoren: tRPCKlimafaktorenProcedure,
test: t.procedure.meta({
openapi: {
method: "GET",
path: "/v1/test",
}
}).input(z.void({})).output(z.string()).query(async (opts) => {
return "Hello World!";
})
})

View File

@@ -0,0 +1,78 @@
import { z } from "zod";
import { t } from "../../context";
import moment from "moment";
import { TRPCError } from "@trpc/server";
import { prisma } from "@ibcornelsen/database";
export const tRPCKlimafaktorenProcedure = t.procedure
.input(
z.object({
plz: z.string().min(4).max(5),
startdatum: z.coerce.date(),
enddatum: z.coerce.date(),
genauigkeit: z.enum(["months", "years"]),
})
)
.output(
z.array(
z.object({
month: z.number(),
year: z.number(),
klimafaktor: z.number(),
})
)
)
.query(async (opts) => {
const start = moment(opts.input.startdatum);
const end = moment(opts.input.enddatum);
if (start.isSameOrAfter(end)) {
throw new TRPCError({
code: "PRECONDITION_FAILED",
message: "Das Startdatum kann nicht vor dem Enddatum liegen.",
});
}
const intervals = [];
let currentDate = start.clone();
while (currentDate.isSameOrBefore(end)) {
let copy = currentDate.clone();
intervals.push(copy);
currentDate.add(1, opts.input.genauigkeit);
}
let klimafaktoren = await prisma.klimafaktoren.findMany({
where: {
plz: opts.input.plz,
month: intervals[0].month(),
OR: intervals.map((date) => {
return {
year: date.year(),
};
}),
},
});
if (!klimafaktoren) {
throw new TRPCError({
code: "NOT_FOUND",
message:
"Die Klimafaktoren konnten nicht geladen werden. Das kann daran liegen, dass sie für diesen Zeitraum oder Ort nicht verfügbar sind.",
});
}
if (klimafaktoren.length !== intervals.length) {
throw new TRPCError({
code: "NOT_FOUND",
message:
"Für diesen Zeitraum konnten nicht alle Klimafaktoren gefunden werden.",
});
}
return klimafaktoren.map((klimafaktor) => ({
month: klimafaktor.month,
year: klimafaktor.year,
klimafaktor: klimafaktor.klimafaktor,
}));
});

View File

@@ -0,0 +1,127 @@
import { ZodSchema, z } from "zod";
import { publicProcedure } from "../../../../context";
import { GebaeudeStammdaten, VerbrauchsausweisWohnen, prisma } from "@ibcornelsen/database";
export const VerbrauchsausweisWohnen2016Erstellen = publicProcedure
.meta({
openapi: {
method: "POST",
path: "/v1/verbrauchsausweis-wohnen/2016/erstellen",
contentTypes: ["application/json"],
description: "Erstellt einen neuen Verbrauchsausweis für Wohngebäude nach dem Schema der EnEV von 2016.",
tags: ["Verbrauchsausweis Wohnen"],
}
})
.input(z.object({
baujahr_heizung: z.array(z.number()).optional(),
zusaetzliche_heizquelle: z.boolean().optional(),
brennstoff_1: z.string().max(50).optional(),
einheit_1: z.string().max(10).optional(),
brennstoff_2: z.string().max(50).optional(),
einheit_2: z.string().max(10).optional(),
startdatum: z.date().optional(),
enddatum: z.date().optional(),
verbrauch_1: z.number().optional(),
verbrauch_2: z.number().optional(),
verbrauch_3: z.number().optional(),
verbrauch_4: z.number().optional(),
verbrauch_5: z.number().optional(),
verbrauch_6: z.number().optional(),
warmwasser_enthalten: z.boolean().optional(),
warmwasser_anteil_bekannt: z.boolean().optional(),
wird_gekuehlt: z.boolean().optional(),
keller_beheizt: z.boolean().optional(),
alternative_heizung: z.boolean().optional(),
alternative_warmwasser: z.boolean().optional(),
alternative_lueftung: z.boolean().optional(),
alternative_kuehlung: z.boolean().optional(),
anteil_warmwasser_1: z.number().optional(),
anteil_warmwasser_2: z.number().optional(),
gebaeude_stammdaten: z.string().uuid().or(z.object({
gebaeudetyp: z.string().max(255).optional(), // Adjust max length as needed
gebaeudeteil: z.string().max(255).optional(), // Adjust max length as needed
baujahr_gebaeude: z.array(z.number()).optional(),
baujahr_heizung: z.array(z.number()).optional(),
baujahr_klima: z.array(z.number()).optional(),
einheiten: z.number().optional(),
flaeche: z.number().optional(),
saniert: z.boolean().optional(),
keller: z.number().optional(),
dachgeschoss: z.number().optional(),
lueftung: z.string().max(50).optional(), // Adjust max length as needed
kuehlung: z.string().max(50).optional(), // Adjust max length as needed
leerstand: z.number().optional(),
plz: z.string().max(5).optional(), // Adjust max length as needed
ort: z.string().max(50).optional(), // Adjust max length as needed
adresse: z.string().max(100).optional(), // Adjust max length as needed
zentralheizung: z.boolean().optional(),
solarsystem_warmwasser: z.boolean().optional(),
warmwasser_rohre_gedaemmt: z.boolean().optional(),
niedertemperatur_kessel: z.boolean().optional(),
brennwert_kessel: z.boolean().optional(),
heizungsrohre_gedaemmt: z.boolean().optional(),
standard_kessel: z.boolean().optional(),
waermepumpe: z.boolean().optional(),
raum_temperatur_regler: z.boolean().optional(),
photovoltaik: z.boolean().optional(),
durchlauf_erhitzer: z.boolean().optional(),
einzelofen: z.boolean().optional(),
zirkulation: z.boolean().optional(),
einfach_verglasung: z.boolean().optional(),
dreifach_verglasung: z.boolean().optional(),
fenster_teilweise_undicht: z.boolean().optional(),
doppel_verglasung: z.boolean().optional(),
fenster_dicht: z.boolean().optional(),
rolllaeden_kaesten_gedaemmt: z.boolean().optional(),
isolier_verglasung: z.boolean().optional(),
tueren_undicht: z.boolean().optional(),
tueren_dicht: z.boolean().optional(),
dachgeschoss_gedaemmt: z.boolean().optional(),
keller_decke_gedaemmt: z.boolean().optional(),
keller_wand_gedaemmt: z.boolean().optional(),
aussenwand_gedaemmt: z.boolean().optional(),
oberste_geschossdecke_gedaemmt: z.boolean().optional(),
aussenwand_min_12cm_gedaemmt: z.boolean().optional(),
dachgeschoss_min_12cm_gedaemmt: z.boolean().optional(),
oberste_geschossdecke_min_12cm_gedaemmt: z.boolean().optional(),
} satisfies ZodSchema<GebaeudeStammdaten>))
} satisfies ZodSchema<VerbrauchsausweisWohnen>))
.output(
z.object({
uid: z.string().uuid(),
})
)
.mutation(async (opts) => {
// Es kann sein, dass ein Gebäude bereits existiert. In diesem Fall wird es nicht neu erstellt, sondern nur der Verbrauchsausweis.
// Das können wir ganz einfach überprüfen, indem wir schauen, ob eine UUID für das Gebäude übergeben wurde.
if (typeof opts.input.gebaeude_stammdaten === "string") {
// Gebäude existiert bereits
const gebaeude = await prisma.gebaeudeStammdaten.findUnique({
where: {
uid: opts.input.gebaeude_stammdaten
}
});
if (!gebaeude) {
throw new Error("Das Gebäude mit der übergebenen UUID existiert nicht.");
}
const verbrauchsausweis = await prisma.verbrauchsausweisWohnen.create({
data: {
...opts.input,
gebaeude_stammdaten: {
connect: {
uid: opts.input.gebaeude_stammdaten
}
}
}
});
return { uid: verbrauchsausweis.uid };
}
return { uid: "" };
});

View File

@@ -0,0 +1,41 @@
import type { APIRoute } from "astro";
import { MissingEntityError, error } from "src/lib/APIResponse";
import { prisma } from "@ibcornelsen/database";
import { xmlVerbrauchsausweisWohnen_2016 } from "#lib/XML/VerbrauchsausweisWohnen/xmlVerbrauchsausweisWohnen_2016";
import uuid from "uuid";
export const get: APIRoute = async ({ url }) => {
const body = url.searchParams;
const uid = body.get("uid");
if (!body.has("uid") || !uid) {
return error(["Missing 'uid' in request body."]);
}
if (!uuid.validate(uid)) {
return error(["'uid' in request body must follow the UUID v4 format."]);
}
const ausweis = await prisma.verbrauchsausweisWohnen.findUnique({
where: {
uid,
},
include: {
gebaeude_stammdaten: true,
},
});
if (!ausweis) {
return MissingEntityError(uid);
}
const xml = await xmlVerbrauchsausweisWohnen_2016(ausweis);
const response = new Response(xml, {
headers: {
"Content-Type": "application/xml",
},
});
return response;
};

View File

@@ -1,7 +1,7 @@
import type { APIRoute } from "astro";
import { success, MissingPropertyError, MissingEntityError, ActionFailedError, InvalidDataError, error } from "../../lib/APIResponse";
import { User } from "../../lib/User";
import { UserRegisterValidator, UserType, UserTypeValidator } from "../../lib/User/type";
import { UserRegisterValidator, UserType } from "../../lib/User/type";
import { z } from "zod";
/**
@@ -36,7 +36,7 @@ export const put: APIRoute = async ({ request }) => {
const user = await User.fromEmail(body.email);
if (user) {
return error(["Email address is already being used."]);
return error(["Diese Email Adresse wird bereits verwendet."]);
}
const result = await User.create(body as UserType);

View File

@@ -1,7 +1,7 @@
import type { APIRoute } from "astro";
import { success, MissingPropertyError, MissingEntityError, InvalidDataError, error } from "../../lib/APIResponse";
import { ZIPInformation } from "src/lib/ZIPInformation";
import { success, MissingPropertyError, MissingEntityError, error } from "../../lib/APIResponse";
import { validateAuthorizationHeader } from "src/lib/server/Authorization";
import { prisma } from "@ibcornelsen/database";
/**
* Ruft einen Nutzer anhand seiner uid aus der Datenbank ab.
@@ -18,11 +18,23 @@ export const get: APIRoute = async ({ request }) => {
let result;
if (body.zip) {
result = await ZIPInformation.fromZipCode(body.zip)
result = await prisma.postleitzahlen.findUnique({
where: {
plz: body.zip,
},
})
} else if (body.city) {
result = await ZIPInformation.fromCity(body.city)
result = await prisma.postleitzahlen.findMany({
where: {
stadt: body.city,
},
})
} else if (body.state) {
result = await ZIPInformation.fromState(body.state)
result = await prisma.postleitzahlen.findMany({
where: {
bundesland: body.state,
},
})
} else {
return MissingPropertyError(["Either 'state', 'city' or 'zip' have to exist in request body."])
}