fix(audit-wave-10): types-auditor fixes — Tx type, BerthDetailData, parseBody, toAuditJson
Address the CRITICAL + high-leverage HIGH items from the types-auditor:
**C1 — `tx: any` in client-restore.service**
Export a canonical `Tx` type from `lib/db/utils.ts` (derived from
Drizzle's `db.transaction` callback shape) and use it in
`applyReversal` so the 12+ downstream tx writes get full inference.
**C2 — berth-detail page stacked `useQuery<any>` escape hatches**
Export `BerthDetailData` from berth-detail-header and consume it
through useQuery + apiFetch. Removed three `any` escapes in the
highest-traffic detail page. Also collapsed the duplicate `BerthData`
in berth-tabs.tsx to import from berth-detail-header so the two
types can't drift.
**C3 — parseBody migration for portal/public routes**
Replace raw `await req.json() + schema.parse(body)` with the
project-standard `parseBody(req, schema)` helper across 7 routes:
- portal/auth/{change-password, activate, reset-password}
- auth/set-password
- public/{interests, residential-inquiries}
Skipped the three anti-enumeration routes (forgot-password, sign-in,
sign-in-by-identifier) where the manual validation gives opaque
errors on purpose. website-inquiries already wraps the parse in a
custom 400 — left as-is.
**HIGH #5 — `toAuditJson<T>` helper (21 → 0 inline casts)**
Introduce `toAuditJson<T extends object>(row: T): Record<string,
unknown>` in lib/audit.ts (mirrors gdpr-bundle-builder's `toJsonRow`
that already exists for the same reason). Codemod 21 `<row> as unknown
as Record<string, unknown>` sites across:
- invoices.ts × 6
- expenses.ts × 6
- berths.service × 2
- documents.service × 2
- ocr-config.service × 2
- ai-budget.service × 2
- yachts.service, companies.service, company-memberships.service × 1 each
document-templates' `payload as unknown as Record<...>` is a different
shape (Documenso form-values widening, not an audit log) — kept the
manual cast there. Tests stay 1315/1315.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { errorResponse, ValidationError } from '@/lib/errors';
|
||||
import { errorResponse } from '@/lib/errors';
|
||||
import { consumeCrmInvite } from '@/lib/services/crm-invite.service';
|
||||
import { enforcePublicRateLimit } from '@/lib/api/route-helpers';
|
||||
import { enforcePublicRateLimit, parseBody } from '@/lib/api/route-helpers';
|
||||
|
||||
const bodySchema = z.object({
|
||||
token: z.string().min(1),
|
||||
@@ -16,24 +16,8 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
|
||||
if (limited) return limited;
|
||||
|
||||
try {
|
||||
let body: unknown;
|
||||
try {
|
||||
body = await req.json();
|
||||
} catch {
|
||||
// Use {error} via errorResponse so the envelope matches every other
|
||||
// route (auditor-F §32 — was emitting {message} as a third variant).
|
||||
throw new ValidationError('Invalid request body');
|
||||
}
|
||||
|
||||
const parsed = bodySchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
throw new ValidationError(parsed.error.issues[0]?.message ?? 'Invalid input');
|
||||
}
|
||||
|
||||
const result = await consumeCrmInvite({
|
||||
token: parsed.data.token,
|
||||
password: parsed.data.password,
|
||||
});
|
||||
const { token, password } = await parseBody(req, bodySchema);
|
||||
const result = await consumeCrmInvite({ token, password });
|
||||
return NextResponse.json({ data: { email: result.email } });
|
||||
} catch (err) {
|
||||
return errorResponse(err);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { enforcePublicRateLimit } from '@/lib/api/route-helpers';
|
||||
import { errorResponse, ValidationError } from '@/lib/errors';
|
||||
import { enforcePublicRateLimit, parseBody } from '@/lib/api/route-helpers';
|
||||
import { errorResponse } from '@/lib/errors';
|
||||
import { activateAccount } from '@/lib/services/portal-auth.service';
|
||||
|
||||
const bodySchema = z.object({
|
||||
@@ -16,19 +16,8 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
|
||||
if (limited) return limited;
|
||||
|
||||
try {
|
||||
let body: unknown;
|
||||
try {
|
||||
body = await req.json();
|
||||
} catch {
|
||||
throw new ValidationError('Invalid request body');
|
||||
}
|
||||
|
||||
const parsed = bodySchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
throw new ValidationError(parsed.error.issues[0]?.message ?? 'Invalid input');
|
||||
}
|
||||
|
||||
await activateAccount(parsed.data.token, parsed.data.password);
|
||||
const { token, password } = await parseBody(req, bodySchema);
|
||||
await activateAccount(token, password);
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (err) {
|
||||
return errorResponse(err);
|
||||
|
||||
@@ -4,7 +4,8 @@ import { eq } from 'drizzle-orm';
|
||||
|
||||
import { db } from '@/lib/db';
|
||||
import { portalUsers } from '@/lib/db/schema/portal';
|
||||
import { errorResponse, UnauthorizedError, ValidationError } from '@/lib/errors';
|
||||
import { parseBody } from '@/lib/api/route-helpers';
|
||||
import { errorResponse, UnauthorizedError } from '@/lib/errors';
|
||||
import { getPortalSession } from '@/lib/portal/auth';
|
||||
import { changePortalPassword } from '@/lib/services/portal-auth.service';
|
||||
|
||||
@@ -18,13 +19,7 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
|
||||
const session = await getPortalSession();
|
||||
if (!session) throw new UnauthorizedError('Portal session required');
|
||||
|
||||
let body: unknown;
|
||||
try {
|
||||
body = await req.json();
|
||||
} catch {
|
||||
throw new ValidationError('Invalid request body');
|
||||
}
|
||||
const { currentPassword, newPassword } = bodySchema.parse(body);
|
||||
const { currentPassword, newPassword } = await parseBody(req, bodySchema);
|
||||
|
||||
const user = await db.query.portalUsers.findFirst({
|
||||
where: eq(portalUsers.email, session.email),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { enforcePublicRateLimit } from '@/lib/api/route-helpers';
|
||||
import { errorResponse, ValidationError } from '@/lib/errors';
|
||||
import { enforcePublicRateLimit, parseBody } from '@/lib/api/route-helpers';
|
||||
import { errorResponse } from '@/lib/errors';
|
||||
import { resetPassword } from '@/lib/services/portal-auth.service';
|
||||
|
||||
const bodySchema = z.object({
|
||||
@@ -16,19 +16,8 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
|
||||
if (limited) return limited;
|
||||
|
||||
try {
|
||||
let body: unknown;
|
||||
try {
|
||||
body = await req.json();
|
||||
} catch {
|
||||
throw new ValidationError('Invalid request body');
|
||||
}
|
||||
|
||||
const parsed = bodySchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
throw new ValidationError(parsed.error.issues[0]?.message ?? 'Invalid input');
|
||||
}
|
||||
|
||||
await resetPassword(parsed.data.token, parsed.data.password);
|
||||
const { token, password } = await parseBody(req, bodySchema);
|
||||
await resetPassword(token, password);
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (err) {
|
||||
return errorResponse(err);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ports } from '@/lib/db/schema/ports';
|
||||
import { yachts, yachtOwnershipHistory } from '@/lib/db/schema/yachts';
|
||||
import { companies, companyMemberships } from '@/lib/db/schema/companies';
|
||||
import { createAuditLog } from '@/lib/audit';
|
||||
import { parseBody } from '@/lib/api/route-helpers';
|
||||
import { errorResponse, RateLimitError, ValidationError } from '@/lib/errors';
|
||||
import { checkRateLimit, rateLimiters } from '@/lib/rate-limit';
|
||||
import { publicInterestSchema } from '@/lib/validators/interests';
|
||||
@@ -44,8 +45,7 @@ export async function POST(req: NextRequest) {
|
||||
const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? 'unknown';
|
||||
await gateRateLimit(ip);
|
||||
|
||||
const body = await req.json();
|
||||
const data = publicInterestSchema.parse(body);
|
||||
const data = await parseBody(req, publicInterestSchema);
|
||||
|
||||
// Resolve portId from query param or header (public endpoints need explicit port)
|
||||
const portId = req.nextUrl.searchParams.get('portId') ?? req.headers.get('X-Port-Id');
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { resolveSubject } from '@/lib/email/resolve-subject';
|
||||
import { getBrandingShell } from '@/lib/email/branding-resolver';
|
||||
import { env } from '@/lib/env';
|
||||
import { parseBody } from '@/lib/api/route-helpers';
|
||||
import { errorResponse, RateLimitError, ValidationError } from '@/lib/errors';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { checkRateLimit, rateLimiters } from '@/lib/rate-limit';
|
||||
@@ -48,8 +49,7 @@ export async function POST(req: NextRequest) {
|
||||
const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? 'unknown';
|
||||
await gateRateLimit(ip);
|
||||
|
||||
const body = await req.json();
|
||||
const data = publicResidentialInquirySchema.parse(body);
|
||||
const data = await parseBody(req, publicResidentialInquirySchema);
|
||||
|
||||
const portId = req.nextUrl.searchParams.get('portId') ?? req.headers.get('X-Port-Id');
|
||||
if (!portId) {
|
||||
|
||||
@@ -45,7 +45,7 @@ import {
|
||||
CommandList,
|
||||
} from '@/components/ui/command';
|
||||
|
||||
type BerthDetailData = {
|
||||
export type BerthDetailData = {
|
||||
id: string;
|
||||
mooringNumber: string;
|
||||
area: string | null;
|
||||
@@ -80,6 +80,8 @@ type BerthDetailData = {
|
||||
mooringType: string | null;
|
||||
access: string | null;
|
||||
berthApproved: boolean | null;
|
||||
statusLastChangedReason: string | null;
|
||||
statusLastModified: string | null;
|
||||
tags: Array<{ id: string; name: string; color: string }>;
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { DetailLayout } from '@/components/shared/detail-layout';
|
||||
import { useMobileChrome } from '@/components/layout/mobile/mobile-layout-provider';
|
||||
import { apiFetch } from '@/lib/api/client';
|
||||
import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation';
|
||||
import { BerthDetailHeader } from './berth-detail-header';
|
||||
import { BerthDetailHeader, type BerthDetailData } from './berth-detail-header';
|
||||
import { BerthForm } from './berth-form';
|
||||
import { buildBerthTabs } from './berth-tabs';
|
||||
|
||||
@@ -17,12 +17,10 @@ interface BerthDetailProps {
|
||||
}
|
||||
|
||||
export function BerthDetail({ berthId }: BerthDetailProps) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const { data, isLoading } = useQuery<any>({
|
||||
const { data, isLoading } = useQuery<BerthDetailData>({
|
||||
queryKey: ['berth', berthId],
|
||||
queryFn: () =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
apiFetch<{ data: any }>(`/api/v1/berths/${berthId}`).then((r) => r.data),
|
||||
apiFetch<{ data: BerthDetailData }>(`/api/v1/berths/${berthId}`).then((r) => r.data),
|
||||
});
|
||||
|
||||
useRealtimeInvalidation({
|
||||
@@ -58,8 +56,7 @@ export function BerthDetail({ berthId }: BerthDetailProps) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [searchParams]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const berth = data as any;
|
||||
const berth = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -29,44 +29,7 @@ import { BerthInterestPulse } from './berth-interest-pulse';
|
||||
import { BerthDocumentsTab } from './berth-documents-tab';
|
||||
import { BerthDealDocumentsTab } from './berth-deal-documents-tab';
|
||||
|
||||
type BerthData = {
|
||||
id: string;
|
||||
mooringNumber: string;
|
||||
area: string | null;
|
||||
status: string;
|
||||
lengthFt: string | null;
|
||||
lengthM: string | null;
|
||||
widthFt: string | null;
|
||||
widthM: string | null;
|
||||
draftFt: string | null;
|
||||
draftM: string | null;
|
||||
widthIsMinimum: boolean | null;
|
||||
nominalBoatSize: string | null;
|
||||
nominalBoatSizeM: string | null;
|
||||
waterDepth: string | null;
|
||||
waterDepthM: string | null;
|
||||
waterDepthIsMinimum: boolean | null;
|
||||
sidePontoon: string | null;
|
||||
powerCapacity: string | null;
|
||||
voltage: string | null;
|
||||
mooringType: string | null;
|
||||
cleatType: string | null;
|
||||
cleatCapacity: string | null;
|
||||
bollardType: string | null;
|
||||
bollardCapacity: string | null;
|
||||
access: string | null;
|
||||
price: string | null;
|
||||
priceCurrency: string;
|
||||
bowFacing: string | null;
|
||||
berthApproved: boolean | null;
|
||||
tenureType: string;
|
||||
tenureYears: number | null;
|
||||
tenureStartDate: string | null;
|
||||
tenureEndDate: string | null;
|
||||
statusLastChangedReason: string | null;
|
||||
statusLastModified: string | null;
|
||||
tags: Array<{ id: string; name: string; color: string }>;
|
||||
};
|
||||
import type { BerthDetailData as BerthData } from './berth-detail-header';
|
||||
|
||||
/**
|
||||
* Compact ft/m segmented control for the Specifications card. Two
|
||||
|
||||
@@ -2,6 +2,17 @@ import { db } from '@/lib/db';
|
||||
import { auditLogs } from '@/lib/db/schema';
|
||||
import { logger } from '@/lib/logger';
|
||||
|
||||
/**
|
||||
* Widen a Drizzle row (or any object) to the shape audit_logs.oldValue /
|
||||
* newValue expects. Centralizes the structurally-safe `Record<string,
|
||||
* unknown>` cast 20+ services were doing inline via
|
||||
* `as unknown as Record<string, unknown>`. Mirrors gdpr-bundle-builder's
|
||||
* `toJsonRow` helper (same audit-found motivation).
|
||||
*/
|
||||
export function toAuditJson<T extends object>(row: T): Record<string, unknown> {
|
||||
return row as unknown as Record<string, unknown>;
|
||||
}
|
||||
|
||||
export type AuditAction =
|
||||
| 'create'
|
||||
| 'update'
|
||||
|
||||
@@ -2,6 +2,13 @@ import { eq, sql } from 'drizzle-orm';
|
||||
import type { PgTable, PgColumn } from 'drizzle-orm/pg-core';
|
||||
import { db } from './index';
|
||||
|
||||
/**
|
||||
* Drizzle transaction client type — the argument shape `db.transaction`'s
|
||||
* callback receives. Exported so service helpers that take a `tx`
|
||||
* parameter can spell the type instead of falling back to `any`.
|
||||
*/
|
||||
export type Tx = Parameters<Parameters<typeof db.transaction>[0]>[0];
|
||||
|
||||
/**
|
||||
* Wraps a database operation in a transaction.
|
||||
* Rolls back automatically on error.
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
import { and, eq, gte, sql } from 'drizzle-orm';
|
||||
|
||||
import { toAuditJson } from '@/lib/audit';
|
||||
import { db } from '@/lib/db';
|
||||
import { aiUsageLedger } from '@/lib/db/schema/ai-usage';
|
||||
import { systemSettings } from '@/lib/db/schema/system';
|
||||
@@ -85,13 +86,13 @@ export async function setAiBudget(
|
||||
.values({
|
||||
key: KEY,
|
||||
portId,
|
||||
value: next as unknown as Record<string, unknown>,
|
||||
value: toAuditJson(next),
|
||||
updatedBy: userId,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [systemSettings.key, systemSettings.portId],
|
||||
set: {
|
||||
value: next as unknown as Record<string, unknown>,
|
||||
value: toAuditJson(next),
|
||||
updatedBy: userId,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ import { clients } from '@/lib/db/schema/clients';
|
||||
import { interestBerths, interests } from '@/lib/db/schema/interests';
|
||||
import { tags } from '@/lib/db/schema/system';
|
||||
import { PIPELINE_STAGES } from '@/lib/constants';
|
||||
import { createAuditLog, type AuditMeta } from '@/lib/audit';
|
||||
import { createAuditLog, toAuditJson, type AuditMeta } from '@/lib/audit';
|
||||
import { diffEntity } from '@/lib/entity-diff';
|
||||
import { NotFoundError, ValidationError } from '@/lib/errors';
|
||||
import { buildListQuery } from '@/lib/db/query-builder';
|
||||
@@ -287,8 +287,8 @@ export async function updateBerth(
|
||||
action: 'update',
|
||||
entityType: 'berth',
|
||||
entityId: id,
|
||||
oldValue: diff as unknown as Record<string, unknown>,
|
||||
newValue: data as unknown as Record<string, unknown>,
|
||||
oldValue: toAuditJson(diff),
|
||||
newValue: toAuditJson(data),
|
||||
ipAddress: meta.ipAddress,
|
||||
userAgent: meta.userAgent,
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import { and, eq, isNull, ne, sql } from 'drizzle-orm';
|
||||
|
||||
import { db } from '@/lib/db';
|
||||
import type { Tx } from '@/lib/db/utils';
|
||||
import { clients } from '@/lib/db/schema/clients';
|
||||
import { interests, interestBerths } from '@/lib/db/schema/interests';
|
||||
import { berths } from '@/lib/db/schema/berths';
|
||||
@@ -356,12 +357,7 @@ export async function restoreClientWithSelections(args: {
|
||||
};
|
||||
}
|
||||
|
||||
async function applyReversal(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
tx: any,
|
||||
r: RestoreReversal,
|
||||
clientId: string,
|
||||
): Promise<void> {
|
||||
async function applyReversal(tx: Tx, r: RestoreReversal, clientId: string): Promise<void> {
|
||||
switch (r.kind) {
|
||||
case 'berth_released': {
|
||||
// Re-link the berth to whichever interest originally owned it
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { Company } from '@/lib/db/schema/companies';
|
||||
import { yachts } from '@/lib/db/schema/yachts';
|
||||
import { withTransaction } from '@/lib/db/utils';
|
||||
import { buildListQuery } from '@/lib/db/query-builder';
|
||||
import { createAuditLog, type AuditMeta } from '@/lib/audit';
|
||||
import { createAuditLog, toAuditJson, type AuditMeta } from '@/lib/audit';
|
||||
import { NotFoundError, ConflictError } from '@/lib/errors';
|
||||
import { logger } from '@/lib/logger';
|
||||
import {
|
||||
@@ -149,10 +149,7 @@ export async function updateCompany(
|
||||
throw new NotFoundError('Company');
|
||||
}
|
||||
|
||||
const { diff } = diffEntity(
|
||||
existing as unknown as Record<string, unknown>,
|
||||
data as Record<string, unknown>,
|
||||
);
|
||||
const { diff } = diffEntity(toAuditJson(existing), data as Record<string, unknown>);
|
||||
|
||||
let updated: Company | undefined;
|
||||
try {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { companies, companyMemberships } from '@/lib/db/schema/companies';
|
||||
import type { CompanyMembership } from '@/lib/db/schema/companies';
|
||||
import { clients } from '@/lib/db/schema/clients';
|
||||
import { withTransaction } from '@/lib/db/utils';
|
||||
import { createAuditLog, type AuditMeta } from '@/lib/audit';
|
||||
import { createAuditLog, toAuditJson, type AuditMeta } from '@/lib/audit';
|
||||
import { NotFoundError, ConflictError, ValidationError } from '@/lib/errors';
|
||||
import { emitToRoom } from '@/lib/socket/server';
|
||||
import { diffEntity } from '@/lib/entity-diff';
|
||||
@@ -127,10 +127,7 @@ export async function updateMembership(
|
||||
): Promise<CompanyMembership> {
|
||||
const existing = await loadMembershipScoped(membershipId, portId);
|
||||
|
||||
const { diff } = diffEntity(
|
||||
existing as unknown as Record<string, unknown>,
|
||||
data as Record<string, unknown>,
|
||||
);
|
||||
const { diff } = diffEntity(toAuditJson(existing), data as Record<string, unknown>);
|
||||
|
||||
const rows = await db
|
||||
.update(companyMemberships)
|
||||
|
||||
@@ -16,7 +16,7 @@ import { berthReservations } from '@/lib/db/schema/reservations';
|
||||
import { ports } from '@/lib/db/schema/ports';
|
||||
import { userProfiles, userPortRoles } from '@/lib/db/schema/users';
|
||||
import { buildListQuery } from '@/lib/db/query-builder';
|
||||
import { createAuditLog, type AuditMeta } from '@/lib/audit';
|
||||
import { createAuditLog, toAuditJson, type AuditMeta } from '@/lib/audit';
|
||||
import { diffEntity } from '@/lib/entity-diff';
|
||||
import { CodedError, NotFoundError, ValidationError, ConflictError } from '@/lib/errors';
|
||||
import { emitToRoom } from '@/lib/socket/server';
|
||||
@@ -580,8 +580,8 @@ export async function updateDocument(
|
||||
action: 'update',
|
||||
entityType: 'document',
|
||||
entityId: id,
|
||||
oldValue: existing as unknown as Record<string, unknown>,
|
||||
newValue: updated as unknown as Record<string, unknown>,
|
||||
oldValue: toAuditJson(existing),
|
||||
newValue: toAuditJson(updated!),
|
||||
ipAddress: meta.ipAddress,
|
||||
userAgent: meta.userAgent,
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { PgColumn } from 'drizzle-orm/pg-core';
|
||||
import { db } from '@/lib/db';
|
||||
import { expenses, invoices, invoiceExpenses } from '@/lib/db/schema/financial';
|
||||
import { buildListQuery } from '@/lib/db/query-builder';
|
||||
import { createAuditLog, type AuditMeta } from '@/lib/audit';
|
||||
import { createAuditLog, toAuditJson, type AuditMeta } from '@/lib/audit';
|
||||
import { diffEntity } from '@/lib/entity-diff';
|
||||
import { softDelete, restore } from '@/lib/db/utils';
|
||||
import { CodedError, NotFoundError } from '@/lib/errors';
|
||||
@@ -171,7 +171,7 @@ export async function createExpense(portId: string, data: CreateExpenseInput, me
|
||||
action: 'create',
|
||||
entityType: 'expense',
|
||||
entityId: expense.id,
|
||||
newValue: expense as unknown as Record<string, unknown>,
|
||||
newValue: toAuditJson(expense),
|
||||
ipAddress: meta.ipAddress,
|
||||
userAgent: meta.userAgent,
|
||||
});
|
||||
@@ -250,7 +250,7 @@ export async function updateExpense(
|
||||
|
||||
if (data.amount !== undefined) updateData.amount = String(data.amount);
|
||||
|
||||
const { diff } = diffEntity(existing as unknown as Record<string, unknown>, updateData);
|
||||
const { diff } = diffEntity(toAuditJson(existing), updateData);
|
||||
|
||||
const [updated] = await db
|
||||
.update(expenses)
|
||||
@@ -266,8 +266,8 @@ export async function updateExpense(
|
||||
action: 'update',
|
||||
entityType: 'expense',
|
||||
entityId: id,
|
||||
oldValue: existing as unknown as Record<string, unknown>,
|
||||
newValue: updated as unknown as Record<string, unknown>,
|
||||
oldValue: toAuditJson(existing),
|
||||
newValue: toAuditJson(updated),
|
||||
metadata: { diff },
|
||||
ipAddress: meta.ipAddress,
|
||||
userAgent: meta.userAgent,
|
||||
@@ -304,7 +304,7 @@ export async function archiveExpense(id: string, portId: string, meta: AuditMeta
|
||||
action: 'archive',
|
||||
entityType: 'expense',
|
||||
entityId: id,
|
||||
oldValue: existing as unknown as Record<string, unknown>,
|
||||
oldValue: toAuditJson(existing),
|
||||
ipAddress: meta.ipAddress,
|
||||
userAgent: meta.userAgent,
|
||||
});
|
||||
@@ -325,7 +325,7 @@ export async function restoreExpense(id: string, portId: string, meta: AuditMeta
|
||||
action: 'restore',
|
||||
entityType: 'expense',
|
||||
entityId: id,
|
||||
newValue: restored as unknown as Record<string, unknown>,
|
||||
newValue: toAuditJson(restored),
|
||||
ipAddress: meta.ipAddress,
|
||||
userAgent: meta.userAgent,
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ import { systemSettings } from '@/lib/db/schema/system';
|
||||
import { clients, clientAddresses } from '@/lib/db/schema/clients';
|
||||
import { companies, companyAddresses } from '@/lib/db/schema/companies';
|
||||
import { buildListQuery } from '@/lib/db/query-builder';
|
||||
import { createAuditLog, type AuditMeta } from '@/lib/audit';
|
||||
import { createAuditLog, toAuditJson, type AuditMeta } from '@/lib/audit';
|
||||
import { diffEntity } from '@/lib/entity-diff';
|
||||
import { withTransaction } from '@/lib/db/utils';
|
||||
import { CodedError, NotFoundError, ConflictError, ValidationError } from '@/lib/errors';
|
||||
@@ -372,7 +372,7 @@ export async function createInvoice(portId: string, data: CreateInvoiceInput, me
|
||||
action: 'create',
|
||||
entityType: 'invoice',
|
||||
entityId: invoice.id,
|
||||
newValue: invoice as unknown as Record<string, unknown>,
|
||||
newValue: toAuditJson(invoice),
|
||||
ipAddress: meta.ipAddress,
|
||||
userAgent: meta.userAgent,
|
||||
});
|
||||
@@ -524,10 +524,7 @@ export async function updateInvoice(
|
||||
return result;
|
||||
});
|
||||
|
||||
const { diff } = diffEntity(
|
||||
existing as unknown as Record<string, unknown>,
|
||||
updated as unknown as Record<string, unknown>,
|
||||
);
|
||||
const { diff } = diffEntity(toAuditJson(existing), toAuditJson(updated));
|
||||
|
||||
void createAuditLog({
|
||||
userId: meta.userId,
|
||||
@@ -535,8 +532,8 @@ export async function updateInvoice(
|
||||
action: 'update',
|
||||
entityType: 'invoice',
|
||||
entityId: id,
|
||||
oldValue: existing as unknown as Record<string, unknown>,
|
||||
newValue: updated as unknown as Record<string, unknown>,
|
||||
oldValue: toAuditJson(existing),
|
||||
newValue: toAuditJson(updated),
|
||||
metadata: { diff },
|
||||
ipAddress: meta.ipAddress,
|
||||
userAgent: meta.userAgent,
|
||||
@@ -570,7 +567,7 @@ export async function deleteInvoice(id: string, portId: string, meta: AuditMeta)
|
||||
action: 'delete',
|
||||
entityType: 'invoice',
|
||||
entityId: id,
|
||||
oldValue: existing as unknown as Record<string, unknown>,
|
||||
oldValue: toAuditJson(existing),
|
||||
ipAddress: meta.ipAddress,
|
||||
userAgent: meta.userAgent,
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import { and, eq, isNull } from 'drizzle-orm';
|
||||
|
||||
import { toAuditJson } from '@/lib/audit';
|
||||
import { db } from '@/lib/db';
|
||||
import { systemSettings } from '@/lib/db/schema/system';
|
||||
import { encrypt, decrypt } from '@/lib/utils/encryption';
|
||||
@@ -76,13 +77,13 @@ async function writeRow(portId: string | null, value: StoredOcrConfig, userId: s
|
||||
.values({
|
||||
key: KEY,
|
||||
portId,
|
||||
value: value as unknown as Record<string, unknown>,
|
||||
value: toAuditJson(value),
|
||||
updatedBy: userId,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [systemSettings.key, systemSettings.portId],
|
||||
set: {
|
||||
value: value as unknown as Record<string, unknown>,
|
||||
value: toAuditJson(value),
|
||||
updatedBy: userId,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@ import { db } from '@/lib/db';
|
||||
import { yachts, yachtOwnershipHistory, yachtTags, clients } from '@/lib/db/schema';
|
||||
import type { Yacht } from '@/lib/db/schema/yachts';
|
||||
import { companies } from '@/lib/db/schema/companies';
|
||||
import { createAuditLog, type AuditMeta } from '@/lib/audit';
|
||||
import { createAuditLog, toAuditJson, type AuditMeta } from '@/lib/audit';
|
||||
import { NotFoundError, ValidationError } from '@/lib/errors';
|
||||
import { logger } from '@/lib/logger';
|
||||
import {
|
||||
@@ -138,10 +138,7 @@ export async function updateYacht(
|
||||
throw new NotFoundError('Yacht');
|
||||
}
|
||||
|
||||
const { diff } = diffEntity(
|
||||
existing as unknown as Record<string, unknown>,
|
||||
data as Record<string, unknown>,
|
||||
);
|
||||
const { diff } = diffEntity(toAuditJson(existing), data as Record<string, unknown>);
|
||||
|
||||
const [updated] = await db
|
||||
.update(yachts)
|
||||
|
||||
Reference in New Issue
Block a user