refactor(api): extract report template procedures
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
import { Prisma } from "@capakraken/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import type { TRPCContext } from "../trpc.js";
|
||||
import {
|
||||
type EntityKey,
|
||||
ReportTemplateConfigSchema,
|
||||
validateReportInput,
|
||||
} from "./report-query-config.js";
|
||||
|
||||
const ReportTemplateEntity = {
|
||||
RESOURCE: "RESOURCE",
|
||||
PROJECT: "PROJECT",
|
||||
ASSIGNMENT: "ASSIGNMENT",
|
||||
RESOURCE_MONTH: "RESOURCE_MONTH",
|
||||
} as const;
|
||||
|
||||
type ReportTemplateEntity = (typeof ReportTemplateEntity)[keyof typeof ReportTemplateEntity];
|
||||
|
||||
type ReportTemplateRecord = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
entity: ReportTemplateEntity;
|
||||
config: unknown;
|
||||
isShared: boolean;
|
||||
ownerId: string;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
type ReportTemplateContext = Pick<TRPCContext, "db" | "dbUser">;
|
||||
|
||||
export const SaveReportTemplateInputSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
name: z.string().trim().min(1).max(120),
|
||||
description: z.string().trim().max(500).optional(),
|
||||
isShared: z.boolean().default(false),
|
||||
config: ReportTemplateConfigSchema,
|
||||
});
|
||||
|
||||
export const DeleteReportTemplateInputSchema = z.object({
|
||||
id: z.string(),
|
||||
});
|
||||
|
||||
export async function listReportTemplates(ctx: ReportTemplateContext) {
|
||||
const ownerId = ctx.dbUser!.id;
|
||||
const templates = await ctx.db.reportTemplate.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ ownerId },
|
||||
{ isShared: true },
|
||||
],
|
||||
},
|
||||
orderBy: [{ name: "asc" }],
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
description: true,
|
||||
entity: true,
|
||||
config: true,
|
||||
isShared: true,
|
||||
ownerId: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
return templates.map((template) => mapTemplateRecord(template, ownerId));
|
||||
}
|
||||
|
||||
export async function saveReportTemplate(
|
||||
ctx: ReportTemplateContext,
|
||||
input: z.infer<typeof SaveReportTemplateInputSchema>,
|
||||
) {
|
||||
const ownerId = ctx.dbUser!.id;
|
||||
validateReportInput(input.config);
|
||||
const payload = input.config as unknown as Prisma.InputJsonValue;
|
||||
const entity = toTemplateEntity(input.config.entity);
|
||||
const writeData = buildTemplateWriteData(input, entity, payload);
|
||||
|
||||
if (input.id) {
|
||||
const existing = await ctx.db.reportTemplate.findUnique({
|
||||
where: { id: input.id },
|
||||
select: { ownerId: true },
|
||||
});
|
||||
|
||||
if (!existing || existing.ownerId !== ownerId) {
|
||||
throw new TRPCError({ code: "FORBIDDEN", message: "Template cannot be updated" });
|
||||
}
|
||||
|
||||
return ctx.db.reportTemplate.update({
|
||||
where: { id: input.id },
|
||||
data: writeData,
|
||||
select: { id: true, updatedAt: true },
|
||||
});
|
||||
}
|
||||
|
||||
return ctx.db.reportTemplate.upsert({
|
||||
where: {
|
||||
ownerId_name: {
|
||||
ownerId,
|
||||
name: input.name,
|
||||
},
|
||||
},
|
||||
update: omitNameFromTemplateWriteData(writeData),
|
||||
create: {
|
||||
ownerId,
|
||||
...writeData,
|
||||
},
|
||||
select: { id: true, updatedAt: true },
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteReportTemplate(
|
||||
ctx: ReportTemplateContext,
|
||||
input: z.infer<typeof DeleteReportTemplateInputSchema>,
|
||||
) {
|
||||
const ownerId = ctx.dbUser!.id;
|
||||
const existing = await ctx.db.reportTemplate.findUnique({
|
||||
where: { id: input.id },
|
||||
select: { ownerId: true },
|
||||
});
|
||||
|
||||
if (!existing || existing.ownerId !== ownerId) {
|
||||
throw new TRPCError({ code: "FORBIDDEN", message: "Template cannot be deleted" });
|
||||
}
|
||||
|
||||
await ctx.db.reportTemplate.delete({ where: { id: input.id } });
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
function buildTemplateWriteData(
|
||||
input: z.infer<typeof SaveReportTemplateInputSchema>,
|
||||
entity: ReportTemplateEntity,
|
||||
config: Prisma.InputJsonValue,
|
||||
) {
|
||||
return {
|
||||
name: input.name,
|
||||
entity,
|
||||
config,
|
||||
isShared: input.isShared,
|
||||
...(input.description !== undefined ? { description: input.description } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function omitNameFromTemplateWriteData(
|
||||
data: ReturnType<typeof buildTemplateWriteData>,
|
||||
) {
|
||||
const { name: _name, ...updateData } = data;
|
||||
return updateData;
|
||||
}
|
||||
|
||||
function mapTemplateRecord(template: ReportTemplateRecord, ownerId: string) {
|
||||
return {
|
||||
id: template.id,
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
entity: fromTemplateEntity(template.entity),
|
||||
config: ReportTemplateConfigSchema.parse(template.config),
|
||||
isShared: template.isShared,
|
||||
isOwner: template.ownerId === ownerId,
|
||||
updatedAt: template.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
function toTemplateEntity(entity: EntityKey): ReportTemplateEntity {
|
||||
switch (entity) {
|
||||
case "resource":
|
||||
return ReportTemplateEntity.RESOURCE;
|
||||
case "project":
|
||||
return ReportTemplateEntity.PROJECT;
|
||||
case "assignment":
|
||||
return ReportTemplateEntity.ASSIGNMENT;
|
||||
case "resource_month":
|
||||
return ReportTemplateEntity.RESOURCE_MONTH;
|
||||
default:
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: `Unknown entity: ${entity}` });
|
||||
}
|
||||
}
|
||||
|
||||
function fromTemplateEntity(entity: ReportTemplateEntity): EntityKey {
|
||||
switch (entity) {
|
||||
case ReportTemplateEntity.RESOURCE:
|
||||
return "resource";
|
||||
case ReportTemplateEntity.PROJECT:
|
||||
return "project";
|
||||
case ReportTemplateEntity.ASSIGNMENT:
|
||||
return "assignment";
|
||||
case ReportTemplateEntity.RESOURCE_MONTH:
|
||||
return "resource_month";
|
||||
default:
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: `Unknown entity: ${entity}` });
|
||||
}
|
||||
}
|
||||
@@ -1,189 +1,23 @@
|
||||
import { Prisma } from "@capakraken/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { controllerProcedure, createTRPCRouter } from "../trpc.js";
|
||||
import {
|
||||
type EntityKey,
|
||||
ReportTemplateConfigSchema,
|
||||
validateReportInput,
|
||||
} from "./report-query-config.js";
|
||||
import { reportQueryProcedures } from "./report-query-engine.js";
|
||||
|
||||
const ReportTemplateEntity = {
|
||||
RESOURCE: "RESOURCE",
|
||||
PROJECT: "PROJECT",
|
||||
ASSIGNMENT: "ASSIGNMENT",
|
||||
RESOURCE_MONTH: "RESOURCE_MONTH",
|
||||
} as const;
|
||||
|
||||
type ReportTemplateEntity = (typeof ReportTemplateEntity)[keyof typeof ReportTemplateEntity];
|
||||
|
||||
type ReportTemplateRecord = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
entity: ReportTemplateEntity;
|
||||
config: unknown;
|
||||
isShared: boolean;
|
||||
ownerId: string;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
function getReportTemplateDelegate(db: unknown) {
|
||||
return (db as {
|
||||
reportTemplate: {
|
||||
findMany: (args: unknown) => Promise<ReportTemplateRecord[]>;
|
||||
findUnique: (args: unknown) => Promise<{ ownerId: string } | null>;
|
||||
update: (args: unknown) => Promise<{ id: string; updatedAt: Date }>;
|
||||
upsert: (args: unknown) => Promise<{ id: string; updatedAt: Date }>;
|
||||
delete: (args: unknown) => Promise<unknown>;
|
||||
};
|
||||
}).reportTemplate;
|
||||
}
|
||||
import {
|
||||
DeleteReportTemplateInputSchema,
|
||||
deleteReportTemplate,
|
||||
listReportTemplates,
|
||||
SaveReportTemplateInputSchema,
|
||||
saveReportTemplate,
|
||||
} from "./report-template-procedure-support.js";
|
||||
|
||||
export const reportRouter = createTRPCRouter({
|
||||
...reportQueryProcedures,
|
||||
|
||||
listTemplates: controllerProcedure.query(async ({ ctx }) => {
|
||||
const reportTemplate = getReportTemplateDelegate(ctx.db);
|
||||
const templates = await reportTemplate.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ ownerId: ctx.dbUser!.id },
|
||||
{ isShared: true },
|
||||
],
|
||||
},
|
||||
orderBy: [{ name: "asc" }],
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
description: true,
|
||||
entity: true,
|
||||
config: true,
|
||||
isShared: true,
|
||||
ownerId: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
return templates.map((template: ReportTemplateRecord) => ({
|
||||
id: template.id,
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
entity: fromTemplateEntity(template.entity),
|
||||
config: ReportTemplateConfigSchema.parse(template.config),
|
||||
isShared: template.isShared,
|
||||
isOwner: template.ownerId === ctx.dbUser!.id,
|
||||
updatedAt: template.updatedAt,
|
||||
}));
|
||||
}),
|
||||
listTemplates: controllerProcedure.query(({ ctx }) => listReportTemplates(ctx)),
|
||||
|
||||
saveTemplate: controllerProcedure
|
||||
.input(z.object({
|
||||
id: z.string().optional(),
|
||||
name: z.string().trim().min(1).max(120),
|
||||
description: z.string().trim().max(500).optional(),
|
||||
isShared: z.boolean().default(false),
|
||||
config: ReportTemplateConfigSchema,
|
||||
}))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
validateReportInput(input.config);
|
||||
const reportTemplate = getReportTemplateDelegate(ctx.db);
|
||||
const payload = input.config as unknown as Prisma.InputJsonValue;
|
||||
const entity = toTemplateEntity(input.config.entity);
|
||||
|
||||
if (input.id) {
|
||||
const existing = await reportTemplate.findUnique({
|
||||
where: { id: input.id },
|
||||
select: { ownerId: true },
|
||||
});
|
||||
|
||||
if (!existing || existing.ownerId !== ctx.dbUser!.id) {
|
||||
throw new TRPCError({ code: "FORBIDDEN", message: "Template cannot be updated" });
|
||||
}
|
||||
|
||||
return reportTemplate.update({
|
||||
where: { id: input.id },
|
||||
data: {
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
entity,
|
||||
config: payload,
|
||||
isShared: input.isShared,
|
||||
},
|
||||
select: { id: true, updatedAt: true },
|
||||
});
|
||||
}
|
||||
|
||||
return reportTemplate.upsert({
|
||||
where: {
|
||||
ownerId_name: {
|
||||
ownerId: ctx.dbUser!.id,
|
||||
name: input.name,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
description: input.description,
|
||||
entity,
|
||||
config: payload,
|
||||
isShared: input.isShared,
|
||||
},
|
||||
create: {
|
||||
ownerId: ctx.dbUser!.id,
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
entity,
|
||||
config: payload,
|
||||
isShared: input.isShared,
|
||||
},
|
||||
select: { id: true, updatedAt: true },
|
||||
});
|
||||
}),
|
||||
.input(SaveReportTemplateInputSchema)
|
||||
.mutation(({ ctx, input }) => saveReportTemplate(ctx, input)),
|
||||
|
||||
deleteTemplate: controllerProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const reportTemplate = getReportTemplateDelegate(ctx.db);
|
||||
const existing = await reportTemplate.findUnique({
|
||||
where: { id: input.id },
|
||||
select: { ownerId: true },
|
||||
});
|
||||
|
||||
if (!existing || existing.ownerId !== ctx.dbUser!.id) {
|
||||
throw new TRPCError({ code: "FORBIDDEN", message: "Template cannot be deleted" });
|
||||
}
|
||||
|
||||
await reportTemplate.delete({ where: { id: input.id } });
|
||||
return { ok: true };
|
||||
}),
|
||||
.input(DeleteReportTemplateInputSchema)
|
||||
.mutation(({ ctx, input }) => deleteReportTemplate(ctx, input)),
|
||||
});
|
||||
|
||||
function toTemplateEntity(entity: EntityKey): ReportTemplateEntity {
|
||||
switch (entity) {
|
||||
case "resource":
|
||||
return ReportTemplateEntity.RESOURCE;
|
||||
case "project":
|
||||
return ReportTemplateEntity.PROJECT;
|
||||
case "assignment":
|
||||
return ReportTemplateEntity.ASSIGNMENT;
|
||||
case "resource_month":
|
||||
return ReportTemplateEntity.RESOURCE_MONTH;
|
||||
default:
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: `Unknown entity: ${entity}` });
|
||||
}
|
||||
}
|
||||
|
||||
function fromTemplateEntity(entity: ReportTemplateEntity): EntityKey {
|
||||
switch (entity) {
|
||||
case ReportTemplateEntity.RESOURCE:
|
||||
return "resource";
|
||||
case ReportTemplateEntity.PROJECT:
|
||||
return "project";
|
||||
case ReportTemplateEntity.ASSIGNMENT:
|
||||
return "assignment";
|
||||
case ReportTemplateEntity.RESOURCE_MONTH:
|
||||
return "resource_month";
|
||||
default:
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: `Unknown entity: ${entity}` });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user