refactor(api): extract client procedures
This commit is contained in:
@@ -1,7 +1,4 @@
|
||||
import { CreateClientSchema, UpdateClientSchema } from "@capakraken/shared";
|
||||
import { z } from "zod";
|
||||
import { findUniqueOrThrow } from "../db/helpers.js";
|
||||
import { createAuditEntry } from "../lib/audit.js";
|
||||
import { CreateClientSchema } from "@capakraken/shared";
|
||||
import {
|
||||
adminProcedure,
|
||||
createTRPCRouter,
|
||||
@@ -10,244 +7,62 @@ import {
|
||||
protectedProcedure,
|
||||
} from "../trpc.js";
|
||||
import {
|
||||
assertClientCodeAvailable,
|
||||
assertClientDeletable,
|
||||
buildClientCreateData,
|
||||
buildClientListWhere,
|
||||
buildClientTree,
|
||||
buildClientUpdateData,
|
||||
findClientByIdentifier,
|
||||
} from "./client-support.js";
|
||||
|
||||
type ClientIdentifierReadModel = {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string | null;
|
||||
parentId: string | null;
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
type ClientDetailReadModel = ClientIdentifierReadModel & {
|
||||
sortOrder: number;
|
||||
tags: string[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
_count: {
|
||||
projects: number;
|
||||
children: number;
|
||||
};
|
||||
};
|
||||
batchUpdateClientSortOrder,
|
||||
clientBatchSortOrderInputSchema,
|
||||
clientIdInputSchema,
|
||||
clientIdentifierInputSchema,
|
||||
clientListInputSchema,
|
||||
clientTreeInputSchema,
|
||||
clientUpdateInputSchema,
|
||||
createClient,
|
||||
deactivateClient,
|
||||
deleteClient,
|
||||
getClientById,
|
||||
getClientByIdentifier,
|
||||
getClientTree,
|
||||
listClients,
|
||||
resolveClientByIdentifier,
|
||||
updateClient,
|
||||
} from "./client-procedure-support.js";
|
||||
|
||||
export const clientRouter = createTRPCRouter({
|
||||
list: planningReadProcedure
|
||||
.input(
|
||||
z.object({
|
||||
parentId: z.string().nullable().optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
search: z.string().optional(),
|
||||
}).optional(),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
return ctx.db.client.findMany({
|
||||
where: buildClientListWhere(input ?? {}),
|
||||
include: { _count: { select: { children: true, projects: true } } },
|
||||
orderBy: [{ sortOrder: "asc" }, { name: "asc" }],
|
||||
});
|
||||
}),
|
||||
.input(clientListInputSchema)
|
||||
.query(({ ctx, input }) => listClients(ctx, input)),
|
||||
|
||||
getTree: planningReadProcedure
|
||||
.input(z.object({ isActive: z.boolean().optional() }).optional())
|
||||
.query(async ({ ctx, input }) => {
|
||||
const all = await ctx.db.client.findMany({
|
||||
where: {
|
||||
...(input?.isActive !== undefined ? { isActive: input.isActive } : {}),
|
||||
},
|
||||
orderBy: [{ sortOrder: "asc" }, { name: "asc" }],
|
||||
});
|
||||
return buildClientTree(all);
|
||||
}),
|
||||
.input(clientTreeInputSchema)
|
||||
.query(({ ctx, input }) => getClientTree(ctx, input)),
|
||||
|
||||
getById: planningReadProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const client = await findUniqueOrThrow(
|
||||
ctx.db.client.findUnique({
|
||||
where: { id: input.id },
|
||||
include: {
|
||||
parent: true,
|
||||
children: { orderBy: { sortOrder: "asc" } },
|
||||
_count: { select: { projects: true, children: true } },
|
||||
},
|
||||
}),
|
||||
"Client",
|
||||
);
|
||||
return client;
|
||||
}),
|
||||
.input(clientIdInputSchema)
|
||||
.query(({ ctx, input }) => getClientById(ctx, input)),
|
||||
|
||||
resolveByIdentifier: protectedProcedure
|
||||
.input(z.object({ identifier: z.string().trim().min(1) }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
return findClientByIdentifier<ClientIdentifierReadModel>(ctx.db, input.identifier, {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
code: true,
|
||||
parentId: true,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
}),
|
||||
.input(clientIdentifierInputSchema)
|
||||
.query(({ ctx, input }) => resolveClientByIdentifier(ctx, input)),
|
||||
|
||||
getByIdentifier: planningReadProcedure
|
||||
.input(z.object({ identifier: z.string().trim().min(1) }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
return findClientByIdentifier<ClientDetailReadModel>(ctx.db, input.identifier, {
|
||||
include: { _count: { select: { projects: true, children: true } } },
|
||||
});
|
||||
}),
|
||||
.input(clientIdentifierInputSchema)
|
||||
.query(({ ctx, input }) => getClientByIdentifier(ctx, input)),
|
||||
|
||||
create: managerProcedure
|
||||
.input(CreateClientSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (input.parentId) {
|
||||
await findUniqueOrThrow(
|
||||
ctx.db.client.findUnique({ where: { id: input.parentId } }),
|
||||
"Parent client",
|
||||
);
|
||||
}
|
||||
|
||||
if (input.code) {
|
||||
await assertClientCodeAvailable(ctx.db, input.code);
|
||||
}
|
||||
|
||||
const created = await ctx.db.client.create({
|
||||
data: buildClientCreateData(input),
|
||||
});
|
||||
|
||||
void createAuditEntry({
|
||||
db: ctx.db,
|
||||
entityType: "Client",
|
||||
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 }) => createClient(ctx, input)),
|
||||
|
||||
update: managerProcedure
|
||||
.input(z.object({ id: z.string(), data: UpdateClientSchema }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const existing = await findUniqueOrThrow(
|
||||
ctx.db.client.findUnique({ where: { id: input.id } }),
|
||||
"Client",
|
||||
);
|
||||
|
||||
if (input.data.code && input.data.code !== existing.code) {
|
||||
await assertClientCodeAvailable(ctx.db, input.data.code, existing.id);
|
||||
}
|
||||
|
||||
const before = existing as unknown as Record<string, unknown>;
|
||||
|
||||
const updated = await ctx.db.client.update({
|
||||
where: { id: input.id },
|
||||
data: buildClientUpdateData(input.data),
|
||||
});
|
||||
|
||||
void createAuditEntry({
|
||||
db: ctx.db,
|
||||
entityType: "Client",
|
||||
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(clientUpdateInputSchema)
|
||||
.mutation(({ ctx, input }) => updateClient(ctx, input)),
|
||||
|
||||
deactivate: managerProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const updated = await ctx.db.client.update({
|
||||
where: { id: input.id },
|
||||
data: { isActive: false },
|
||||
});
|
||||
|
||||
void createAuditEntry({
|
||||
db: ctx.db,
|
||||
entityType: "Client",
|
||||
entityId: updated.id,
|
||||
entityName: updated.name,
|
||||
action: "UPDATE",
|
||||
userId: ctx.dbUser?.id,
|
||||
before: { isActive: true },
|
||||
after: { isActive: false },
|
||||
source: "ui",
|
||||
summary: "Deactivated Client",
|
||||
});
|
||||
|
||||
return updated;
|
||||
}),
|
||||
.input(clientIdInputSchema)
|
||||
.mutation(({ ctx, input }) => deactivateClient(ctx, input)),
|
||||
|
||||
delete: adminProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const client = await findUniqueOrThrow(
|
||||
ctx.db.client.findUnique({
|
||||
where: { id: input.id },
|
||||
include: { _count: { select: { projects: true, children: true } } },
|
||||
}),
|
||||
"Client",
|
||||
);
|
||||
assertClientDeletable(client);
|
||||
await ctx.db.client.delete({ where: { id: input.id } });
|
||||
|
||||
void createAuditEntry({
|
||||
db: ctx.db,
|
||||
entityType: "Client",
|
||||
entityId: client.id,
|
||||
entityName: client.name,
|
||||
action: "DELETE",
|
||||
userId: ctx.dbUser?.id,
|
||||
before: client as unknown as Record<string, unknown>,
|
||||
source: "ui",
|
||||
});
|
||||
|
||||
return client;
|
||||
}),
|
||||
.input(clientIdInputSchema)
|
||||
.mutation(({ ctx, input }) => deleteClient(ctx, input)),
|
||||
|
||||
batchUpdateSortOrder: managerProcedure
|
||||
.input(z.array(z.object({ id: z.string(), sortOrder: z.number().int() })))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await ctx.db.$transaction(
|
||||
input.map((item) =>
|
||||
ctx.db.client.update({
|
||||
where: { id: item.id },
|
||||
data: { sortOrder: item.sortOrder },
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
for (const item of input) {
|
||||
void createAuditEntry({
|
||||
db: ctx.db,
|
||||
entityType: "Client",
|
||||
entityId: item.id,
|
||||
action: "UPDATE",
|
||||
userId: ctx.dbUser?.id,
|
||||
after: { sortOrder: item.sortOrder },
|
||||
source: "ui",
|
||||
summary: "Updated sort order",
|
||||
});
|
||||
}
|
||||
|
||||
return { ok: true };
|
||||
}),
|
||||
.input(clientBatchSortOrderInputSchema)
|
||||
.mutation(({ ctx, input }) => batchUpdateClientSortOrder(ctx, input)),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user