refactor(api): extract country router support
This commit is contained in:
@@ -0,0 +1,102 @@
|
|||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import {
|
||||||
|
assertCountryCodeAvailable,
|
||||||
|
assertMetroCityDeletable,
|
||||||
|
buildCountryCreateData,
|
||||||
|
buildCountryListWhere,
|
||||||
|
buildCountryUpdateData,
|
||||||
|
buildMetroCityCreateData,
|
||||||
|
buildMetroCityUpdateData,
|
||||||
|
findCountryByIdentifier,
|
||||||
|
jsonOrNull,
|
||||||
|
} from "../router/country-support.js";
|
||||||
|
|
||||||
|
describe("country support", () => {
|
||||||
|
it("builds list filters", () => {
|
||||||
|
expect(buildCountryListWhere({ isActive: true })).toEqual({
|
||||||
|
isActive: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves countries by code before fuzzy name fallback", async () => {
|
||||||
|
const db = {
|
||||||
|
country: {
|
||||||
|
findUnique: vi.fn().mockResolvedValue(null),
|
||||||
|
findFirst: vi.fn()
|
||||||
|
.mockResolvedValueOnce({ id: "country_de", code: "DE" }),
|
||||||
|
},
|
||||||
|
} as never;
|
||||||
|
|
||||||
|
const result = await findCountryByIdentifier<{ id: string; code: string }>(
|
||||||
|
db,
|
||||||
|
" de ",
|
||||||
|
{ select: { id: true, code: true } },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({ id: "country_de", code: "DE" });
|
||||||
|
expect(db.country.findFirst).toHaveBeenNthCalledWith(1, {
|
||||||
|
where: { code: { equals: "DE", mode: "insensitive" } },
|
||||||
|
select: { id: true, code: true },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects duplicate country codes outside the ignored id", async () => {
|
||||||
|
const db = {
|
||||||
|
country: {
|
||||||
|
findUnique: vi.fn().mockResolvedValue({ id: "country_existing", code: "DE" }),
|
||||||
|
},
|
||||||
|
} as never;
|
||||||
|
|
||||||
|
await expect(assertCountryCodeAvailable(db, "DE")).rejects.toBeInstanceOf(TRPCError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds create and sparse update payloads", () => {
|
||||||
|
expect(buildCountryCreateData({
|
||||||
|
code: "DE",
|
||||||
|
name: "Germany",
|
||||||
|
dailyWorkingHours: 8,
|
||||||
|
scheduleRules: [{ weekday: 1 }],
|
||||||
|
})).toEqual({
|
||||||
|
code: "DE",
|
||||||
|
name: "Germany",
|
||||||
|
dailyWorkingHours: 8,
|
||||||
|
scheduleRules: [{ weekday: 1 }],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(buildCountryUpdateData({
|
||||||
|
dailyWorkingHours: 7.5,
|
||||||
|
scheduleRules: null,
|
||||||
|
isActive: false,
|
||||||
|
})).toEqual({
|
||||||
|
dailyWorkingHours: 7.5,
|
||||||
|
scheduleRules: expect.anything(),
|
||||||
|
isActive: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(buildMetroCityCreateData({
|
||||||
|
name: "Berlin",
|
||||||
|
countryId: "country_de",
|
||||||
|
})).toEqual({
|
||||||
|
name: "Berlin",
|
||||||
|
countryId: "country_de",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(buildMetroCityUpdateData({
|
||||||
|
name: "Munich",
|
||||||
|
})).toEqual({
|
||||||
|
name: "Munich",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps nullish schedule rules to Prisma.JsonNull", () => {
|
||||||
|
expect(jsonOrNull(null)).toBeTypeOf("object");
|
||||||
|
expect(jsonOrNull(undefined)).toBeTypeOf("object");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects metro-city deletion while resources are assigned", () => {
|
||||||
|
expect(() => assertMetroCityDeletable({
|
||||||
|
_count: { resources: 1 },
|
||||||
|
})).toThrow(TRPCError);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import { Prisma, type PrismaClient } from "@capakraken/db";
|
||||||
|
import {
|
||||||
|
CreateCountrySchema,
|
||||||
|
CreateMetroCitySchema,
|
||||||
|
UpdateCountrySchema,
|
||||||
|
UpdateMetroCitySchema,
|
||||||
|
} from "@capakraken/shared";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
type CountryDb = Pick<PrismaClient, "country">;
|
||||||
|
type MetroCityDb = Pick<PrismaClient, "metroCity">;
|
||||||
|
|
||||||
|
type CountryListInput = {
|
||||||
|
isActive?: boolean | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MetroCityDeleteRecord = {
|
||||||
|
_count: {
|
||||||
|
resources: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type CreateCountryInput = z.infer<typeof CreateCountrySchema>;
|
||||||
|
type UpdateCountryInput = z.infer<typeof UpdateCountrySchema>;
|
||||||
|
type CreateMetroCityInput = z.infer<typeof CreateMetroCitySchema>;
|
||||||
|
type UpdateMetroCityInput = z.infer<typeof UpdateMetroCitySchema>;
|
||||||
|
|
||||||
|
export function jsonOrNull(val: unknown): Prisma.InputJsonValue | typeof Prisma.JsonNull {
|
||||||
|
if (val === null || val === undefined) return Prisma.JsonNull;
|
||||||
|
return val as Prisma.InputJsonValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCountryListWhere(
|
||||||
|
input: CountryListInput,
|
||||||
|
): Prisma.CountryWhereInput {
|
||||||
|
return {
|
||||||
|
...(input.isActive !== undefined ? { isActive: input.isActive } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function findCountryByIdentifier<TCountry>(
|
||||||
|
db: CountryDb,
|
||||||
|
identifier: string,
|
||||||
|
extraArgs: Record<string, unknown>,
|
||||||
|
): Promise<TCountry> {
|
||||||
|
const normalizedIdentifier = identifier.trim();
|
||||||
|
const upperIdentifier = normalizedIdentifier.toUpperCase();
|
||||||
|
|
||||||
|
let country = await db.country.findUnique({
|
||||||
|
where: { id: normalizedIdentifier },
|
||||||
|
...extraArgs,
|
||||||
|
}) as TCountry | null;
|
||||||
|
|
||||||
|
if (!country) {
|
||||||
|
country = await db.country.findFirst({
|
||||||
|
where: { code: { equals: upperIdentifier, mode: "insensitive" } },
|
||||||
|
...extraArgs,
|
||||||
|
}) as TCountry | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!country) {
|
||||||
|
country = await db.country.findFirst({
|
||||||
|
where: { name: { equals: normalizedIdentifier, mode: "insensitive" } },
|
||||||
|
...extraArgs,
|
||||||
|
}) as TCountry | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!country) {
|
||||||
|
country = await db.country.findFirst({
|
||||||
|
where: { name: { contains: normalizedIdentifier, mode: "insensitive" } },
|
||||||
|
...extraArgs,
|
||||||
|
}) as TCountry | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!country) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: `Country not found: ${normalizedIdentifier}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return country;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function assertCountryCodeAvailable(
|
||||||
|
db: CountryDb,
|
||||||
|
code: string,
|
||||||
|
ignoreId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const existing = await db.country.findUnique({ where: { code } });
|
||||||
|
if (existing && existing.id !== ignoreId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "CONFLICT",
|
||||||
|
message: `Country code "${code}" already exists`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCountryCreateData(
|
||||||
|
input: CreateCountryInput,
|
||||||
|
): Prisma.CountryUncheckedCreateInput {
|
||||||
|
return {
|
||||||
|
code: input.code,
|
||||||
|
name: input.name,
|
||||||
|
dailyWorkingHours: input.dailyWorkingHours,
|
||||||
|
...(input.scheduleRules !== undefined
|
||||||
|
? { scheduleRules: jsonOrNull(input.scheduleRules) }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCountryUpdateData(
|
||||||
|
input: UpdateCountryInput,
|
||||||
|
): Prisma.CountryUncheckedUpdateInput {
|
||||||
|
return {
|
||||||
|
...(input.code !== undefined ? { code: input.code } : {}),
|
||||||
|
...(input.name !== undefined ? { name: input.name } : {}),
|
||||||
|
...(input.dailyWorkingHours !== undefined ? { dailyWorkingHours: input.dailyWorkingHours } : {}),
|
||||||
|
...(input.scheduleRules !== undefined ? { scheduleRules: jsonOrNull(input.scheduleRules) } : {}),
|
||||||
|
...(input.isActive !== undefined ? { isActive: input.isActive } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildMetroCityCreateData(
|
||||||
|
input: CreateMetroCityInput,
|
||||||
|
): Prisma.MetroCityUncheckedCreateInput {
|
||||||
|
return {
|
||||||
|
name: input.name,
|
||||||
|
countryId: input.countryId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildMetroCityUpdateData(
|
||||||
|
input: UpdateMetroCityInput,
|
||||||
|
): Prisma.MetroCityUncheckedUpdateInput {
|
||||||
|
return {
|
||||||
|
...(input.name !== undefined ? { name: input.name } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assertMetroCityDeletable(
|
||||||
|
city: MetroCityDeleteRecord,
|
||||||
|
): void {
|
||||||
|
if (city._count.resources > 0) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "PRECONDITION_FAILED",
|
||||||
|
message: `Cannot delete metro city assigned to ${city._count.resources} resource(s)`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,8 +4,7 @@ import {
|
|||||||
UpdateCountrySchema,
|
UpdateCountrySchema,
|
||||||
UpdateMetroCitySchema,
|
UpdateMetroCitySchema,
|
||||||
} from "@capakraken/shared";
|
} from "@capakraken/shared";
|
||||||
import { Prisma } from "@capakraken/db";
|
import type { Prisma } from "@capakraken/db";
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { findUniqueOrThrow } from "../db/helpers.js";
|
import { findUniqueOrThrow } from "../db/helpers.js";
|
||||||
import { createAuditEntry } from "../lib/audit.js";
|
import { createAuditEntry } from "../lib/audit.js";
|
||||||
@@ -15,21 +14,37 @@ import {
|
|||||||
protectedProcedure,
|
protectedProcedure,
|
||||||
resourceOverviewProcedure,
|
resourceOverviewProcedure,
|
||||||
} from "../trpc.js";
|
} from "../trpc.js";
|
||||||
|
import {
|
||||||
|
assertCountryCodeAvailable,
|
||||||
|
assertMetroCityDeletable,
|
||||||
|
buildCountryCreateData,
|
||||||
|
buildCountryListWhere,
|
||||||
|
buildCountryUpdateData,
|
||||||
|
buildMetroCityCreateData,
|
||||||
|
buildMetroCityUpdateData,
|
||||||
|
findCountryByIdentifier,
|
||||||
|
} from "./country-support.js";
|
||||||
|
|
||||||
/** Convert nullable JSON to Prisma-compatible value (null → Prisma.JsonNull). */
|
type CountryIdentifierReadModel = {
|
||||||
function jsonOrNull(val: unknown): Prisma.InputJsonValue | typeof Prisma.JsonNull {
|
id: string;
|
||||||
if (val === null || val === undefined) return Prisma.JsonNull;
|
code: string;
|
||||||
return val as Prisma.InputJsonValue;
|
name: string;
|
||||||
}
|
isActive: boolean;
|
||||||
|
dailyWorkingHours: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CountryDetailReadModel = CountryIdentifierReadModel & {
|
||||||
|
scheduleRules?: Prisma.JsonValue | null;
|
||||||
|
metroCities: Array<{ id: string; name: string; countryId: string }>;
|
||||||
|
_count: { resources: number };
|
||||||
|
};
|
||||||
|
|
||||||
export const countryRouter = createTRPCRouter({
|
export const countryRouter = createTRPCRouter({
|
||||||
list: protectedProcedure
|
list: protectedProcedure
|
||||||
.input(z.object({ isActive: z.boolean().optional() }).optional())
|
.input(z.object({ isActive: z.boolean().optional() }).optional())
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
return ctx.db.country.findMany({
|
return ctx.db.country.findMany({
|
||||||
where: {
|
where: buildCountryListWhere(input ?? {}),
|
||||||
...(input?.isActive !== undefined ? { isActive: input.isActive } : {}),
|
|
||||||
},
|
|
||||||
include: { metroCities: { orderBy: { name: "asc" } } },
|
include: { metroCities: { orderBy: { name: "asc" } } },
|
||||||
orderBy: { name: "asc" },
|
orderBy: { name: "asc" },
|
||||||
});
|
});
|
||||||
@@ -38,95 +53,26 @@ export const countryRouter = createTRPCRouter({
|
|||||||
resolveByIdentifier: protectedProcedure
|
resolveByIdentifier: protectedProcedure
|
||||||
.input(z.object({ identifier: z.string().trim().min(1) }))
|
.input(z.object({ identifier: z.string().trim().min(1) }))
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const identifier = input.identifier.trim();
|
return findCountryByIdentifier<CountryIdentifierReadModel>(ctx.db, input.identifier, {
|
||||||
const select = {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
code: true,
|
code: true,
|
||||||
name: true,
|
name: true,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
dailyWorkingHours: true,
|
dailyWorkingHours: true,
|
||||||
} as const;
|
},
|
||||||
|
|
||||||
let country = await ctx.db.country.findUnique({
|
|
||||||
where: { id: identifier },
|
|
||||||
select,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!country) {
|
|
||||||
country = await ctx.db.country.findFirst({
|
|
||||||
where: { code: { equals: identifier.toUpperCase(), mode: "insensitive" } },
|
|
||||||
select,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!country) {
|
|
||||||
country = await ctx.db.country.findFirst({
|
|
||||||
where: { name: { equals: identifier, mode: "insensitive" } },
|
|
||||||
select,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!country) {
|
|
||||||
country = await ctx.db.country.findFirst({
|
|
||||||
where: { name: { contains: identifier, mode: "insensitive" } },
|
|
||||||
select,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!country) {
|
|
||||||
throw new TRPCError({ code: "NOT_FOUND", message: `Country not found: ${identifier}` });
|
|
||||||
}
|
|
||||||
|
|
||||||
return country;
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getByIdentifier: resourceOverviewProcedure
|
getByIdentifier: resourceOverviewProcedure
|
||||||
.input(z.object({ identifier: z.string().trim().min(1) }))
|
.input(z.object({ identifier: z.string().trim().min(1) }))
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const identifier = input.identifier.trim();
|
return findCountryByIdentifier<CountryDetailReadModel>(ctx.db, input.identifier, {
|
||||||
let country = await ctx.db.country.findUnique({
|
|
||||||
where: { id: identifier },
|
|
||||||
include: {
|
include: {
|
||||||
metroCities: { orderBy: { name: "asc" } },
|
metroCities: { orderBy: { name: "asc" } },
|
||||||
_count: { select: { resources: true } },
|
_count: { select: { resources: true } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!country) {
|
|
||||||
country = await ctx.db.country.findFirst({
|
|
||||||
where: { code: { equals: identifier.toUpperCase(), mode: "insensitive" } },
|
|
||||||
include: {
|
|
||||||
metroCities: { orderBy: { name: "asc" } },
|
|
||||||
_count: { select: { resources: true } },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!country) {
|
|
||||||
country = await ctx.db.country.findFirst({
|
|
||||||
where: { name: { equals: identifier, mode: "insensitive" } },
|
|
||||||
include: {
|
|
||||||
metroCities: { orderBy: { name: "asc" } },
|
|
||||||
_count: { select: { resources: true } },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!country) {
|
|
||||||
country = await ctx.db.country.findFirst({
|
|
||||||
where: { name: { contains: identifier, mode: "insensitive" } },
|
|
||||||
include: {
|
|
||||||
metroCities: { orderBy: { name: "asc" } },
|
|
||||||
_count: { select: { resources: true } },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!country) {
|
|
||||||
throw new TRPCError({ code: "NOT_FOUND", message: `Country not found: ${identifier}` });
|
|
||||||
}
|
|
||||||
|
|
||||||
return country;
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getById: resourceOverviewProcedure
|
getById: resourceOverviewProcedure
|
||||||
@@ -161,17 +107,9 @@ export const countryRouter = createTRPCRouter({
|
|||||||
create: adminProcedure
|
create: adminProcedure
|
||||||
.input(CreateCountrySchema)
|
.input(CreateCountrySchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const existing = await ctx.db.country.findUnique({ where: { code: input.code } });
|
await assertCountryCodeAvailable(ctx.db, input.code);
|
||||||
if (existing) {
|
|
||||||
throw new TRPCError({ code: "CONFLICT", message: `Country code "${input.code}" already exists` });
|
|
||||||
}
|
|
||||||
const created = await ctx.db.country.create({
|
const created = await ctx.db.country.create({
|
||||||
data: {
|
data: buildCountryCreateData(input),
|
||||||
code: input.code,
|
|
||||||
name: input.name,
|
|
||||||
dailyWorkingHours: input.dailyWorkingHours,
|
|
||||||
...(input.scheduleRules !== undefined ? { scheduleRules: jsonOrNull(input.scheduleRules) } : {}),
|
|
||||||
},
|
|
||||||
include: { metroCities: true },
|
include: { metroCities: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -198,23 +136,14 @@ export const countryRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (input.data.code && input.data.code !== existing.code) {
|
if (input.data.code && input.data.code !== existing.code) {
|
||||||
const conflict = await ctx.db.country.findUnique({ where: { code: input.data.code } });
|
await assertCountryCodeAvailable(ctx.db, input.data.code, existing.id);
|
||||||
if (conflict) {
|
|
||||||
throw new TRPCError({ code: "CONFLICT", message: `Country code "${input.data.code}" already exists` });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const before = existing as unknown as Record<string, unknown>;
|
const before = existing as unknown as Record<string, unknown>;
|
||||||
|
|
||||||
const updated = await ctx.db.country.update({
|
const updated = await ctx.db.country.update({
|
||||||
where: { id: input.id },
|
where: { id: input.id },
|
||||||
data: {
|
data: buildCountryUpdateData(input.data),
|
||||||
...(input.data.code !== undefined ? { code: input.data.code } : {}),
|
|
||||||
...(input.data.name !== undefined ? { name: input.data.name } : {}),
|
|
||||||
...(input.data.dailyWorkingHours !== undefined ? { dailyWorkingHours: input.data.dailyWorkingHours } : {}),
|
|
||||||
...(input.data.scheduleRules !== undefined ? { scheduleRules: jsonOrNull(input.data.scheduleRules) } : {}),
|
|
||||||
...(input.data.isActive !== undefined ? { isActive: input.data.isActive } : {}),
|
|
||||||
},
|
|
||||||
include: { metroCities: true },
|
include: { metroCities: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -244,7 +173,7 @@ export const countryRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const created = await ctx.db.metroCity.create({
|
const created = await ctx.db.metroCity.create({
|
||||||
data: { name: input.name, countryId: input.countryId },
|
data: buildMetroCityCreateData(input),
|
||||||
});
|
});
|
||||||
|
|
||||||
void createAuditEntry({
|
void createAuditEntry({
|
||||||
@@ -272,7 +201,7 @@ export const countryRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const updated = await ctx.db.metroCity.update({
|
const updated = await ctx.db.metroCity.update({
|
||||||
where: { id: input.id },
|
where: { id: input.id },
|
||||||
data: { ...(input.data.name !== undefined ? { name: input.data.name } : {}) },
|
data: buildMetroCityUpdateData(input.data),
|
||||||
});
|
});
|
||||||
|
|
||||||
void createAuditEntry({
|
void createAuditEntry({
|
||||||
@@ -300,12 +229,7 @@ export const countryRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
"Metro city",
|
"Metro city",
|
||||||
);
|
);
|
||||||
if (city._count.resources > 0) {
|
assertMetroCityDeletable(city);
|
||||||
throw new TRPCError({
|
|
||||||
code: "PRECONDITION_FAILED",
|
|
||||||
message: `Cannot delete metro city assigned to ${city._count.resources} resource(s)`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await ctx.db.metroCity.delete({ where: { id: input.id } });
|
await ctx.db.metroCity.delete({ where: { id: input.id } });
|
||||||
|
|
||||||
void createAuditEntry({
|
void createAuditEntry({
|
||||||
|
|||||||
Reference in New Issue
Block a user