fix(types): replace structural DB types with Pick<PrismaClient> and remove Prisma boundary as any casts
Replace ~440 lines of hand-written structural DB client types across 7 lib files with `Pick<PrismaClient, ...>` from @capakraken/db. This eliminates all `as any` casts at Prisma boundaries (cron routes, allocation effects, vacation procedures) and surfaces two pre-existing bugs: - weekly-digest.ts: `db.allocation.count()` called non-existent model (fixed → demandRequirement) - estimate-reminders.ts: `submittedAt` field doesn't exist on EstimateVersion (fixed → updatedAt) Also adds root eslint.config.mjs so lint-staged can lint package files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,21 @@
|
||||
import type { PrismaClient } from "@capakraken/db";
|
||||
import { createDemandRequirement, fillDemandRequirement } from "@capakraken/application";
|
||||
import { buildTaskAction, CreateDemandRequirementSchema, FillDemandRequirementSchema } from "@capakraken/shared";
|
||||
import { z } from "zod";
|
||||
import type {
|
||||
CreateDemandRequirementSchema,
|
||||
FillDemandRequirementSchema,
|
||||
} from "@capakraken/shared";
|
||||
import { buildTaskAction } from "@capakraken/shared";
|
||||
import type { z } from "zod";
|
||||
import { checkBudgetThresholds } from "../../lib/budget-alerts.js";
|
||||
import { generateAutoSuggestions } from "../../lib/auto-staffing.js";
|
||||
import { invalidateDashboardCache } from "../../lib/cache.js";
|
||||
import { logger } from "../../lib/logger.js";
|
||||
import { dispatchWebhooks } from "../../lib/webhook-dispatcher.js";
|
||||
import { emitAllocationCreated, emitAllocationUpdated, emitNotificationCreated } from "../../sse/event-bus.js";
|
||||
import {
|
||||
emitAllocationCreated,
|
||||
emitAllocationUpdated,
|
||||
emitNotificationCreated,
|
||||
} from "../../sse/event-bus.js";
|
||||
|
||||
export function runAllocationBackgroundEffect(
|
||||
effectName: string,
|
||||
@@ -27,44 +36,37 @@ export function invalidateDashboardCacheInBackground(): void {
|
||||
runAllocationBackgroundEffect("invalidateDashboardCache", () => invalidateDashboardCache());
|
||||
}
|
||||
|
||||
export function checkBudgetThresholdsInBackground(
|
||||
db: import("@capakraken/db").PrismaClient,
|
||||
projectId: string,
|
||||
): void {
|
||||
export function checkBudgetThresholdsInBackground(db: PrismaClient, projectId: string): void {
|
||||
runAllocationBackgroundEffect(
|
||||
"checkBudgetThresholds",
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
() => checkBudgetThresholds(db as any, projectId),
|
||||
() => checkBudgetThresholds(db, projectId),
|
||||
{ projectId },
|
||||
);
|
||||
}
|
||||
|
||||
export function dispatchAllocationWebhookInBackground(
|
||||
db: import("@capakraken/db").PrismaClient,
|
||||
db: PrismaClient,
|
||||
event: string,
|
||||
payload: Record<string, unknown>,
|
||||
): void {
|
||||
runAllocationBackgroundEffect(
|
||||
"dispatchWebhooks",
|
||||
() => dispatchWebhooks(db, event, payload),
|
||||
{ event },
|
||||
);
|
||||
runAllocationBackgroundEffect("dispatchWebhooks", () => dispatchWebhooks(db, event, payload), {
|
||||
event,
|
||||
});
|
||||
}
|
||||
|
||||
export function generateAutoSuggestionsInBackground(
|
||||
db: import("@capakraken/db").PrismaClient,
|
||||
db: PrismaClient,
|
||||
demandRequirementId: string,
|
||||
): void {
|
||||
runAllocationBackgroundEffect(
|
||||
"generateAutoSuggestions",
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
() => generateAutoSuggestions(db as any, demandRequirementId),
|
||||
() => generateAutoSuggestions(db, demandRequirementId),
|
||||
{ demandRequirementId },
|
||||
);
|
||||
}
|
||||
|
||||
export async function createDemandRequirementWithEffects(
|
||||
db: import("@capakraken/db").PrismaClient,
|
||||
db: PrismaClient,
|
||||
input: z.infer<typeof CreateDemandRequirementSchema>,
|
||||
) {
|
||||
const demandRequirement = await db.$transaction(async (tx) => {
|
||||
@@ -132,7 +134,7 @@ export async function createDemandRequirementWithEffects(
|
||||
}
|
||||
|
||||
export async function fillDemandRequirementWithEffects(
|
||||
db: import("@capakraken/db").PrismaClient,
|
||||
db: PrismaClient,
|
||||
input: z.infer<typeof FillDemandRequirementSchema>,
|
||||
) {
|
||||
const result = await fillDemandRequirement(db, input);
|
||||
@@ -151,8 +153,10 @@ export async function fillDemandRequirementWithEffects(
|
||||
invalidateDashboardCacheInBackground();
|
||||
checkBudgetThresholdsInBackground(db, result.assignment.projectId);
|
||||
|
||||
if (result.updatedDemandRequirement.headcount > 0
|
||||
&& result.updatedDemandRequirement.status !== "COMPLETED") {
|
||||
if (
|
||||
result.updatedDemandRequirement.headcount > 0 &&
|
||||
result.updatedDemandRequirement.status !== "COMPLETED"
|
||||
) {
|
||||
generateAutoSuggestionsInBackground(db, result.updatedDemandRequirement.id);
|
||||
}
|
||||
|
||||
|
||||
@@ -43,56 +43,55 @@ const BatchCreatePublicHolidaysSchema = z.object({
|
||||
});
|
||||
|
||||
export const vacationManagementProcedures = {
|
||||
approve: managerProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userRecord = ctx.dbUser;
|
||||
const audit = makeAuditLogger(ctx.db, userRecord?.id);
|
||||
approve: managerProcedure.input(z.object({ id: z.string() })).mutation(async ({ ctx, input }) => {
|
||||
const userRecord = ctx.dbUser;
|
||||
const audit = makeAuditLogger(ctx.db, userRecord?.id);
|
||||
|
||||
const result = await approveVacation(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
ctx.db as any,
|
||||
{ id: input.id, actorUserId: userRecord?.id },
|
||||
{
|
||||
assertVacationApprovable,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
assertVacationStillChargeable: assertVacationStillChargeable as any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
buildVacationApprovalWriteData: buildVacationApprovalWriteData as any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
checkVacationConflicts: checkVacationConflicts as any,
|
||||
buildApprovedVacationUpdateData,
|
||||
},
|
||||
const result = await approveVacation(
|
||||
ctx.db,
|
||||
{ id: input.id, actorUserId: userRecord?.id },
|
||||
{
|
||||
assertVacationApprovable,
|
||||
assertVacationStillChargeable,
|
||||
buildVacationApprovalWriteData,
|
||||
checkVacationConflicts,
|
||||
buildApprovedVacationUpdateData,
|
||||
},
|
||||
);
|
||||
|
||||
const { vacation: updated, existingStatus, warnings } = result;
|
||||
|
||||
emitVacationUpdated({ id: updated.id, resourceId: updated.resourceId, status: updated.status });
|
||||
|
||||
audit({
|
||||
entityType: "Vacation",
|
||||
entityId: updated.id,
|
||||
entityName: `Vacation ${updated.id}`,
|
||||
action: "UPDATE",
|
||||
after: updated as unknown as Record<string, unknown>,
|
||||
summary: `Approved vacation (was ${existingStatus})`,
|
||||
});
|
||||
|
||||
dispatchVacationWebhookInBackground(ctx.db, "vacation.approved", {
|
||||
id: updated.id,
|
||||
resourceId: updated.resourceId,
|
||||
startDate: updated.startDate.toISOString(),
|
||||
endDate: updated.endDate.toISOString(),
|
||||
});
|
||||
|
||||
await completeVacationApprovalTasks(ctx.db, input.id, userRecord?.id);
|
||||
|
||||
if (existingStatus === VacationStatus.PENDING) {
|
||||
notifyVacationStatusInBackground(
|
||||
ctx.db,
|
||||
updated.id,
|
||||
updated.resourceId,
|
||||
VacationStatus.APPROVED,
|
||||
);
|
||||
}
|
||||
|
||||
const { vacation: updated, existingStatus, warnings } = result;
|
||||
|
||||
emitVacationUpdated({ id: updated.id, resourceId: updated.resourceId, status: updated.status });
|
||||
|
||||
audit({
|
||||
entityType: "Vacation",
|
||||
entityId: updated.id,
|
||||
entityName: `Vacation ${updated.id}`,
|
||||
action: "UPDATE",
|
||||
after: updated as unknown as Record<string, unknown>,
|
||||
summary: `Approved vacation (was ${existingStatus})`,
|
||||
});
|
||||
|
||||
dispatchVacationWebhookInBackground(ctx.db, "vacation.approved", {
|
||||
id: updated.id,
|
||||
resourceId: updated.resourceId,
|
||||
startDate: updated.startDate.toISOString(),
|
||||
endDate: updated.endDate.toISOString(),
|
||||
});
|
||||
|
||||
await completeVacationApprovalTasks(ctx.db, input.id, userRecord?.id);
|
||||
|
||||
if (existingStatus === VacationStatus.PENDING) {
|
||||
notifyVacationStatusInBackground(ctx.db, updated.id, updated.resourceId, VacationStatus.APPROVED);
|
||||
}
|
||||
|
||||
return { ...updated, warnings };
|
||||
}),
|
||||
return { ...updated, warnings };
|
||||
}),
|
||||
|
||||
reject: managerProcedure
|
||||
.input(z.object({ id: z.string(), rejectionReason: z.string().max(500).optional() }))
|
||||
@@ -105,7 +104,11 @@ export const vacationManagementProcedures = {
|
||||
|
||||
const { vacation: updated } = result;
|
||||
|
||||
emitVacationUpdated({ id: updated.id, resourceId: updated.resourceId, status: updated.status });
|
||||
emitVacationUpdated({
|
||||
id: updated.id,
|
||||
resourceId: updated.resourceId,
|
||||
status: updated.status,
|
||||
});
|
||||
|
||||
const userRecord = ctx.dbUser;
|
||||
const audit = makeAuditLogger(ctx.db, userRecord?.id);
|
||||
@@ -138,23 +141,28 @@ export const vacationManagementProcedures = {
|
||||
const audit = makeAuditLogger(ctx.db, userRecord?.id);
|
||||
|
||||
const result = await batchApproveVacations(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
ctx.db as any,
|
||||
ctx.db,
|
||||
{ ids: input.ids, actorUserId: userRecord?.id },
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
assertVacationStillChargeable: assertVacationStillChargeable as any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
buildVacationApprovalWriteData: buildVacationApprovalWriteData as any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
checkBatchVacationConflicts: checkBatchVacationConflicts as any,
|
||||
assertVacationStillChargeable,
|
||||
buildVacationApprovalWriteData,
|
||||
checkBatchVacationConflicts,
|
||||
buildApprovedVacationUpdateData,
|
||||
},
|
||||
);
|
||||
|
||||
for (const updated of result.updatedVacations) {
|
||||
emitVacationUpdated({ id: updated.id, resourceId: updated.resourceId, status: updated.status });
|
||||
notifyVacationStatusInBackground(ctx.db, updated.id, updated.resourceId, VacationStatus.APPROVED);
|
||||
emitVacationUpdated({
|
||||
id: updated.id,
|
||||
resourceId: updated.resourceId,
|
||||
status: updated.status,
|
||||
});
|
||||
notifyVacationStatusInBackground(
|
||||
ctx.db,
|
||||
updated.id,
|
||||
updated.resourceId,
|
||||
VacationStatus.APPROVED,
|
||||
);
|
||||
await completeVacationApprovalTasks(ctx.db, updated.id, userRecord?.id);
|
||||
audit({
|
||||
entityType: "Vacation",
|
||||
@@ -187,7 +195,11 @@ export const vacationManagementProcedures = {
|
||||
);
|
||||
|
||||
for (const vacation of result.vacations) {
|
||||
emitVacationUpdated({ id: vacation.id, resourceId: vacation.resourceId, status: VacationStatus.REJECTED });
|
||||
emitVacationUpdated({
|
||||
id: vacation.id,
|
||||
resourceId: vacation.resourceId,
|
||||
status: VacationStatus.REJECTED,
|
||||
});
|
||||
notifyVacationStatusInBackground(
|
||||
ctx.db,
|
||||
vacation.id,
|
||||
@@ -201,7 +213,10 @@ export const vacationManagementProcedures = {
|
||||
entityId: vacation.id,
|
||||
entityName: `Vacation ${vacation.id}`,
|
||||
action: "UPDATE",
|
||||
after: { status: VacationStatus.REJECTED, rejectionReason: input.rejectionReason } as unknown as Record<string, unknown>,
|
||||
after: {
|
||||
status: VacationStatus.REJECTED,
|
||||
rejectionReason: input.rejectionReason,
|
||||
} as unknown as Record<string, unknown>,
|
||||
summary: `Batch rejected vacation${input.rejectionReason ? `: ${input.rejectionReason}` : ""}`,
|
||||
});
|
||||
|
||||
@@ -236,7 +251,11 @@ export const vacationManagementProcedures = {
|
||||
|
||||
const { vacation: updated, existingStatus } = result;
|
||||
|
||||
emitVacationUpdated({ id: updated.id, resourceId: updated.resourceId, status: updated.status });
|
||||
emitVacationUpdated({
|
||||
id: updated.id,
|
||||
resourceId: updated.resourceId,
|
||||
status: updated.status,
|
||||
});
|
||||
|
||||
audit({
|
||||
entityType: "Vacation",
|
||||
@@ -281,7 +300,13 @@ export const vacationManagementProcedures = {
|
||||
entityId: `public-holidays-${input.year}`,
|
||||
entityName: `Public Holidays ${input.year}${input.federalState ? ` (${input.federalState})` : ""}`,
|
||||
action: "CREATE",
|
||||
after: { created, holidays, resources, year: input.year, federalState: input.federalState } as unknown as Record<string, unknown>,
|
||||
after: {
|
||||
created,
|
||||
holidays,
|
||||
resources,
|
||||
year: input.year,
|
||||
federalState: input.federalState,
|
||||
} as unknown as Record<string, unknown>,
|
||||
summary: `Batch created ${created} public holidays for ${resources} resources (${input.year})`,
|
||||
});
|
||||
|
||||
@@ -303,7 +328,10 @@ export const vacationManagementProcedures = {
|
||||
}
|
||||
|
||||
if (input.status !== "CANCELLED" && !isVacationManagerRole(userRecord.systemRole)) {
|
||||
throw new TRPCError({ code: "FORBIDDEN", message: "Manager role required to approve/reject" });
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "Manager role required to approve/reject",
|
||||
});
|
||||
}
|
||||
|
||||
const updated = await ctx.db.vacation.update({
|
||||
@@ -316,7 +344,11 @@ export const vacationManagementProcedures = {
|
||||
}),
|
||||
});
|
||||
|
||||
emitVacationUpdated({ id: updated.id, resourceId: updated.resourceId, status: updated.status });
|
||||
emitVacationUpdated({
|
||||
id: updated.id,
|
||||
resourceId: updated.resourceId,
|
||||
status: updated.status,
|
||||
});
|
||||
|
||||
audit({
|
||||
entityType: "Vacation",
|
||||
|
||||
Reference in New Issue
Block a user