refactor(api): extract country procedures

This commit is contained in:
2026-03-31 20:25:11 +02:00
parent e375d634f6
commit a22dee6d25
4 changed files with 618 additions and 215 deletions
+36 -215
View File
@@ -1,13 +1,3 @@
import {
CreateCountrySchema,
CreateMetroCitySchema,
UpdateCountrySchema,
UpdateMetroCitySchema,
} from "@capakraken/shared";
import type { Prisma } from "@capakraken/db";
import { z } from "zod";
import { findUniqueOrThrow } from "../db/helpers.js";
import { createAuditEntry } from "../lib/audit.js";
import {
adminProcedure,
createTRPCRouter,
@@ -15,234 +5,65 @@ import {
resourceOverviewProcedure,
} from "../trpc.js";
import {
assertCountryCodeAvailable,
assertMetroCityDeletable,
buildCountryCreateData,
buildCountryListWhere,
buildCountryUpdateData,
buildMetroCityCreateData,
buildMetroCityUpdateData,
findCountryByIdentifier,
} from "./country-support.js";
type CountryIdentifierReadModel = {
id: string;
code: string;
name: string;
isActive: boolean;
dailyWorkingHours: number;
};
type CountryDetailReadModel = CountryIdentifierReadModel & {
scheduleRules?: Prisma.JsonValue | null;
metroCities: Array<{ id: string; name: string; countryId: string }>;
_count: { resources: number };
};
countryIdInputSchema,
countryIdentifierInputSchema,
countryListInputSchema,
countryUpdateInputSchema,
createCountry,
createMetroCity,
deleteMetroCity,
getCountryById,
getCountryByIdentifier,
getMetroCityById,
listCountries,
metroCityIdInputSchema,
metroCityUpdateInputSchema,
resolveCountryByIdentifier,
updateCountry,
updateMetroCity,
} from "./country-procedure-support.js";
import { CreateCountrySchema, CreateMetroCitySchema } from "@capakraken/shared";
export const countryRouter = createTRPCRouter({
list: protectedProcedure
.input(z.object({ isActive: z.boolean().optional() }).optional())
.query(async ({ ctx, input }) => {
return ctx.db.country.findMany({
where: buildCountryListWhere(input ?? {}),
include: { metroCities: { orderBy: { name: "asc" } } },
orderBy: { name: "asc" },
});
}),
.input(countryListInputSchema)
.query(({ ctx, input }) => listCountries(ctx, input)),
resolveByIdentifier: protectedProcedure
.input(z.object({ identifier: z.string().trim().min(1) }))
.query(async ({ ctx, input }) => {
return findCountryByIdentifier<CountryIdentifierReadModel>(ctx.db, input.identifier, {
select: {
id: true,
code: true,
name: true,
isActive: true,
dailyWorkingHours: true,
},
});
}),
.input(countryIdentifierInputSchema)
.query(({ ctx, input }) => resolveCountryByIdentifier(ctx, input)),
getByIdentifier: resourceOverviewProcedure
.input(z.object({ identifier: z.string().trim().min(1) }))
.query(async ({ ctx, input }) => {
return findCountryByIdentifier<CountryDetailReadModel>(ctx.db, input.identifier, {
include: {
metroCities: { orderBy: { name: "asc" } },
_count: { select: { resources: true } },
},
});
}),
.input(countryIdentifierInputSchema)
.query(({ ctx, input }) => getCountryByIdentifier(ctx, input)),
getById: resourceOverviewProcedure
.input(z.object({ id: z.string() }))
.query(async ({ ctx, input }) => {
const country = await findUniqueOrThrow(
ctx.db.country.findUnique({
where: { id: input.id },
include: {
metroCities: { orderBy: { name: "asc" } },
_count: { select: { resources: true } },
},
}),
"Country",
);
return country;
}),
.input(countryIdInputSchema)
.query(({ ctx, input }) => getCountryById(ctx, input)),
getCityById: protectedProcedure
.input(z.object({ id: z.string() }))
.query(async ({ ctx, input }) => {
const city = await findUniqueOrThrow(
ctx.db.metroCity.findUnique({
where: { id: input.id },
select: { id: true, name: true, countryId: true },
}),
"Metro city",
);
return city;
}),
.input(metroCityIdInputSchema)
.query(({ ctx, input }) => getMetroCityById(ctx, input)),
create: adminProcedure
.input(CreateCountrySchema)
.mutation(async ({ ctx, input }) => {
await assertCountryCodeAvailable(ctx.db, input.code);
const created = await ctx.db.country.create({
data: buildCountryCreateData(input),
include: { metroCities: true },
});
void createAuditEntry({
db: ctx.db,
entityType: "Country",
entityId: created.id,
entityName: created.name,
action: "CREATE",
userId: ctx.dbUser?.id,
after: created as unknown as Record<string, unknown>,
source: "ui",
});
return created;
}),
.mutation(({ ctx, input }) => createCountry(ctx, input)),
update: adminProcedure
.input(z.object({ id: z.string(), data: UpdateCountrySchema }))
.mutation(async ({ ctx, input }) => {
const existing = await findUniqueOrThrow(
ctx.db.country.findUnique({ where: { id: input.id } }),
"Country",
);
if (input.data.code && input.data.code !== existing.code) {
await assertCountryCodeAvailable(ctx.db, input.data.code, existing.id);
}
const before = existing as unknown as Record<string, unknown>;
const updated = await ctx.db.country.update({
where: { id: input.id },
data: buildCountryUpdateData(input.data),
include: { metroCities: true },
});
void createAuditEntry({
db: ctx.db,
entityType: "Country",
entityId: updated.id,
entityName: updated.name,
action: "UPDATE",
userId: ctx.dbUser?.id,
before,
after: updated as unknown as Record<string, unknown>,
source: "ui",
});
return updated;
}),
.input(countryUpdateInputSchema)
.mutation(({ ctx, input }) => updateCountry(ctx, input)),
// ─── Metro City ─────────────────────────────────────────────
createCity: adminProcedure
.input(CreateMetroCitySchema)
.mutation(async ({ ctx, input }) => {
await findUniqueOrThrow(
ctx.db.country.findUnique({ where: { id: input.countryId } }),
"Country",
);
const created = await ctx.db.metroCity.create({
data: buildMetroCityCreateData(input),
});
void createAuditEntry({
db: ctx.db,
entityType: "MetroCity",
entityId: created.id,
entityName: created.name,
action: "CREATE",
userId: ctx.dbUser?.id,
after: created as unknown as Record<string, unknown>,
source: "ui",
});
return created;
}),
.mutation(({ ctx, input }) => createMetroCity(ctx, input)),
updateCity: adminProcedure
.input(z.object({ id: z.string(), data: UpdateMetroCitySchema }))
.mutation(async ({ ctx, input }) => {
const existing = await findUniqueOrThrow(
ctx.db.metroCity.findUnique({ where: { id: input.id } }),
"Metro city",
);
const before = existing as unknown as Record<string, unknown>;
const updated = await ctx.db.metroCity.update({
where: { id: input.id },
data: buildMetroCityUpdateData(input.data),
});
void createAuditEntry({
db: ctx.db,
entityType: "MetroCity",
entityId: updated.id,
entityName: updated.name,
action: "UPDATE",
userId: ctx.dbUser?.id,
before,
after: updated as unknown as Record<string, unknown>,
source: "ui",
});
return updated;
}),
.input(metroCityUpdateInputSchema)
.mutation(({ ctx, input }) => updateMetroCity(ctx, input)),
deleteCity: adminProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ ctx, input }) => {
const city = await findUniqueOrThrow(
ctx.db.metroCity.findUnique({
where: { id: input.id },
include: { _count: { select: { resources: true } } },
}),
"Metro city",
);
assertMetroCityDeletable(city);
await ctx.db.metroCity.delete({ where: { id: input.id } });
void createAuditEntry({
db: ctx.db,
entityType: "MetroCity",
entityId: city.id,
entityName: city.name,
action: "DELETE",
userId: ctx.dbUser?.id,
before: city as unknown as Record<string, unknown>,
source: "ui",
});
return { success: true, id: city.id, name: city.name };
}),
.input(metroCityIdInputSchema)
.mutation(({ ctx, input }) => deleteMetroCity(ctx, input)),
});