refactor(api): extract report template procedures
This commit is contained in:
@@ -0,0 +1,258 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import {
|
||||||
|
DeleteReportTemplateInputSchema,
|
||||||
|
deleteReportTemplate,
|
||||||
|
listReportTemplates,
|
||||||
|
SaveReportTemplateInputSchema,
|
||||||
|
saveReportTemplate,
|
||||||
|
} from "../router/report-template-procedure-support.js";
|
||||||
|
|
||||||
|
function createContext(reportTemplate: Record<string, unknown>) {
|
||||||
|
return {
|
||||||
|
db: {
|
||||||
|
reportTemplate,
|
||||||
|
},
|
||||||
|
dbUser: {
|
||||||
|
id: "user_controller",
|
||||||
|
systemRole: "CONTROLLER",
|
||||||
|
permissionOverrides: null,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("report template procedure support", () => {
|
||||||
|
it("lists shared and owned templates with parsed config and ownership flags", async () => {
|
||||||
|
const updatedAt = new Date("2026-03-31T10:00:00.000Z");
|
||||||
|
const ctx = createContext({
|
||||||
|
findMany: vi.fn().mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: "tpl_1",
|
||||||
|
name: "Owned resource month",
|
||||||
|
description: "Monthly default",
|
||||||
|
entity: "RESOURCE_MONTH",
|
||||||
|
config: {
|
||||||
|
entity: "resource_month",
|
||||||
|
columns: ["displayName", "monthlySahHours"],
|
||||||
|
filters: [],
|
||||||
|
periodMonth: "2026-03",
|
||||||
|
},
|
||||||
|
isShared: false,
|
||||||
|
ownerId: "user_controller",
|
||||||
|
updatedAt,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tpl_2",
|
||||||
|
name: "Shared project",
|
||||||
|
description: null,
|
||||||
|
entity: "PROJECT",
|
||||||
|
config: {
|
||||||
|
entity: "project",
|
||||||
|
columns: ["name"],
|
||||||
|
filters: [],
|
||||||
|
},
|
||||||
|
isShared: true,
|
||||||
|
ownerId: "user_other",
|
||||||
|
updatedAt,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await listReportTemplates(ctx);
|
||||||
|
|
||||||
|
expect(ctx.db.reportTemplate.findMany).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ ownerId: "user_controller" },
|
||||||
|
{ isShared: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
orderBy: [{ name: "asc" }],
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
description: true,
|
||||||
|
entity: true,
|
||||||
|
config: true,
|
||||||
|
isShared: true,
|
||||||
|
ownerId: true,
|
||||||
|
updatedAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
id: "tpl_1",
|
||||||
|
name: "Owned resource month",
|
||||||
|
description: "Monthly default",
|
||||||
|
entity: "resource_month",
|
||||||
|
config: {
|
||||||
|
entity: "resource_month",
|
||||||
|
columns: ["displayName", "monthlySahHours"],
|
||||||
|
filters: [],
|
||||||
|
periodMonth: "2026-03",
|
||||||
|
sortDir: "asc",
|
||||||
|
},
|
||||||
|
isShared: false,
|
||||||
|
isOwner: true,
|
||||||
|
updatedAt,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tpl_2",
|
||||||
|
name: "Shared project",
|
||||||
|
description: null,
|
||||||
|
entity: "project",
|
||||||
|
config: {
|
||||||
|
entity: "project",
|
||||||
|
columns: ["name"],
|
||||||
|
filters: [],
|
||||||
|
sortDir: "asc",
|
||||||
|
},
|
||||||
|
isShared: true,
|
||||||
|
isOwner: false,
|
||||||
|
updatedAt,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("upserts new templates under the current owner with converted entity values", async () => {
|
||||||
|
const upsert = vi.fn().mockResolvedValue({
|
||||||
|
id: "tpl_new",
|
||||||
|
updatedAt: new Date("2026-03-31T11:00:00.000Z"),
|
||||||
|
});
|
||||||
|
const ctx = createContext({
|
||||||
|
upsert,
|
||||||
|
});
|
||||||
|
const input = SaveReportTemplateInputSchema.parse({
|
||||||
|
name: "Monthly default",
|
||||||
|
description: "Team-wide default",
|
||||||
|
isShared: true,
|
||||||
|
config: {
|
||||||
|
entity: "resource_month",
|
||||||
|
columns: ["displayName", "monthlyTargetHours"],
|
||||||
|
filters: [],
|
||||||
|
periodMonth: "2026-03",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await saveReportTemplate(ctx, input);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
id: "tpl_new",
|
||||||
|
updatedAt: new Date("2026-03-31T11:00:00.000Z"),
|
||||||
|
});
|
||||||
|
expect(upsert).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
ownerId_name: {
|
||||||
|
ownerId: "user_controller",
|
||||||
|
name: "Monthly default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
description: "Team-wide default",
|
||||||
|
entity: "RESOURCE_MONTH",
|
||||||
|
config: {
|
||||||
|
entity: "resource_month",
|
||||||
|
columns: ["displayName", "monthlyTargetHours"],
|
||||||
|
filters: [],
|
||||||
|
periodMonth: "2026-03",
|
||||||
|
sortDir: "asc",
|
||||||
|
},
|
||||||
|
isShared: true,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
ownerId: "user_controller",
|
||||||
|
name: "Monthly default",
|
||||||
|
description: "Team-wide default",
|
||||||
|
entity: "RESOURCE_MONTH",
|
||||||
|
config: {
|
||||||
|
entity: "resource_month",
|
||||||
|
columns: ["displayName", "monthlyTargetHours"],
|
||||||
|
filters: [],
|
||||||
|
periodMonth: "2026-03",
|
||||||
|
sortDir: "asc",
|
||||||
|
},
|
||||||
|
isShared: true,
|
||||||
|
},
|
||||||
|
select: { id: true, updatedAt: true },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates owned templates and rejects updates for foreign templates", async () => {
|
||||||
|
const findUnique = vi.fn()
|
||||||
|
.mockResolvedValueOnce({ ownerId: "user_controller" })
|
||||||
|
.mockResolvedValueOnce({ ownerId: "user_other" });
|
||||||
|
const update = vi.fn().mockResolvedValue({
|
||||||
|
id: "tpl_1",
|
||||||
|
updatedAt: new Date("2026-03-31T12:00:00.000Z"),
|
||||||
|
});
|
||||||
|
const ctx = createContext({
|
||||||
|
findUnique,
|
||||||
|
update,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ownedResult = await saveReportTemplate(ctx, SaveReportTemplateInputSchema.parse({
|
||||||
|
id: "tpl_1",
|
||||||
|
name: "Owned template",
|
||||||
|
description: "Updated",
|
||||||
|
config: {
|
||||||
|
entity: "project",
|
||||||
|
columns: ["name"],
|
||||||
|
filters: [],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(ownedResult).toEqual({
|
||||||
|
id: "tpl_1",
|
||||||
|
updatedAt: new Date("2026-03-31T12:00:00.000Z"),
|
||||||
|
});
|
||||||
|
expect(update).toHaveBeenCalledWith({
|
||||||
|
where: { id: "tpl_1" },
|
||||||
|
data: {
|
||||||
|
name: "Owned template",
|
||||||
|
description: "Updated",
|
||||||
|
entity: "PROJECT",
|
||||||
|
config: {
|
||||||
|
entity: "project",
|
||||||
|
columns: ["name"],
|
||||||
|
filters: [],
|
||||||
|
sortDir: "asc",
|
||||||
|
},
|
||||||
|
isShared: false,
|
||||||
|
},
|
||||||
|
select: { id: true, updatedAt: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(saveReportTemplate(ctx, SaveReportTemplateInputSchema.parse({
|
||||||
|
id: "tpl_2",
|
||||||
|
name: "Foreign template",
|
||||||
|
config: {
|
||||||
|
entity: "resource",
|
||||||
|
columns: ["displayName"],
|
||||||
|
filters: [],
|
||||||
|
},
|
||||||
|
}))).rejects.toMatchObject({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message: "Template cannot be updated",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deletes only owned templates", async () => {
|
||||||
|
const findUnique = vi.fn()
|
||||||
|
.mockResolvedValueOnce({ ownerId: "user_controller" })
|
||||||
|
.mockResolvedValueOnce({ ownerId: "user_other" });
|
||||||
|
const del = vi.fn().mockResolvedValue(undefined);
|
||||||
|
const ctx = createContext({
|
||||||
|
findUnique,
|
||||||
|
delete: del,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await deleteReportTemplate(ctx, DeleteReportTemplateInputSchema.parse({ id: "tpl_1" }));
|
||||||
|
|
||||||
|
expect(result).toEqual({ ok: true });
|
||||||
|
expect(del).toHaveBeenCalledWith({ where: { id: "tpl_1" } });
|
||||||
|
|
||||||
|
await expect(deleteReportTemplate(ctx, DeleteReportTemplateInputSchema.parse({ id: "tpl_2" }))).rejects.toMatchObject({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message: "Template cannot be deleted",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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 { controllerProcedure, createTRPCRouter } from "../trpc.js";
|
||||||
import {
|
|
||||||
type EntityKey,
|
|
||||||
ReportTemplateConfigSchema,
|
|
||||||
validateReportInput,
|
|
||||||
} from "./report-query-config.js";
|
|
||||||
import { reportQueryProcedures } from "./report-query-engine.js";
|
import { reportQueryProcedures } from "./report-query-engine.js";
|
||||||
|
import {
|
||||||
const ReportTemplateEntity = {
|
DeleteReportTemplateInputSchema,
|
||||||
RESOURCE: "RESOURCE",
|
deleteReportTemplate,
|
||||||
PROJECT: "PROJECT",
|
listReportTemplates,
|
||||||
ASSIGNMENT: "ASSIGNMENT",
|
SaveReportTemplateInputSchema,
|
||||||
RESOURCE_MONTH: "RESOURCE_MONTH",
|
saveReportTemplate,
|
||||||
} as const;
|
} from "./report-template-procedure-support.js";
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const reportRouter = createTRPCRouter({
|
export const reportRouter = createTRPCRouter({
|
||||||
...reportQueryProcedures,
|
...reportQueryProcedures,
|
||||||
|
|
||||||
listTemplates: controllerProcedure.query(async ({ ctx }) => {
|
listTemplates: controllerProcedure.query(({ ctx }) => listReportTemplates(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,
|
|
||||||
}));
|
|
||||||
}),
|
|
||||||
|
|
||||||
saveTemplate: controllerProcedure
|
saveTemplate: controllerProcedure
|
||||||
.input(z.object({
|
.input(SaveReportTemplateInputSchema)
|
||||||
id: z.string().optional(),
|
.mutation(({ ctx, input }) => saveReportTemplate(ctx, input)),
|
||||||
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 },
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
|
|
||||||
deleteTemplate: controllerProcedure
|
deleteTemplate: controllerProcedure
|
||||||
.input(z.object({ id: z.string() }))
|
.input(DeleteReportTemplateInputSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(({ ctx, input }) => deleteReportTemplate(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 };
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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