refactor(services): centralize AuditMeta + transactional setEntityTags helper
The same `interface AuditMeta { userId; portId; ipAddress; userAgent }`
was duplicated in 26 service files. Move the canonical definition into
`@/lib/audit` next to the related types and update every service to
import it. `ServiceAuditMeta` (the alias used in invoices.ts and
expenses.ts) collapses into the same name.
Tag CRUD across clients/companies/yachts/interests/berths followed an
identical wipe-then-rewrite recipe with two latent issues: the delete
and insert weren't wrapped in a transaction (a partial failure left
the entity with zero tags) and the audit-log payload shape diverged
(`newValue: { tagIds }` for clients/yachts/companies but
`metadata: { type: 'tags_updated', tagIds }` for interests/berths).
Extract `setEntityTags` in `entity-tags.helper.ts` that performs the
delete+insert inside a single transaction, normalizes the audit payload
to `newValue: { tagIds }`, and dispatches the per-entity socket event
through a switch so `ServerToClientEvents` typing stays intact.
The five `setXTags(...)` service functions now do parent-row tenant
verification and delegate the join-table work + side effects.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import { db } from '@/lib/db';
|
||||
import { files, documents } from '@/lib/db/schema/documents';
|
||||
import { expenses } from '@/lib/db/schema/financial';
|
||||
import { berthMaintenanceLog } from '@/lib/db/schema/berths';
|
||||
import { createAuditLog } from '@/lib/audit';
|
||||
import { createAuditLog, type AuditMeta } from '@/lib/audit';
|
||||
import { ConflictError, NotFoundError, ValidationError } from '@/lib/errors';
|
||||
import { emitToRoom } from '@/lib/socket/server';
|
||||
import { minioClient, getPresignedUrl } from '@/lib/minio';
|
||||
@@ -20,13 +20,6 @@ import type { UploadFileInput, UpdateFileInput, ListFilesInput } from '@/lib/val
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
interface AuditMeta {
|
||||
userId: string;
|
||||
portId: string;
|
||||
ipAddress: string;
|
||||
userAgent: string;
|
||||
}
|
||||
|
||||
interface UploadFileParams {
|
||||
buffer: Buffer;
|
||||
originalName: string;
|
||||
@@ -57,13 +50,9 @@ export async function uploadFile(
|
||||
const sanitizedOriginal = sanitizeFilename(file.originalName);
|
||||
const sanitizedFilename = sanitizeFilename(data.filename);
|
||||
|
||||
await minioClient.putObject(
|
||||
env.MINIO_BUCKET,
|
||||
storagePath,
|
||||
file.buffer,
|
||||
file.size,
|
||||
{ 'Content-Type': file.mimeType },
|
||||
);
|
||||
await minioClient.putObject(env.MINIO_BUCKET, storagePath, file.buffer, file.size, {
|
||||
'Content-Type': file.mimeType,
|
||||
});
|
||||
|
||||
const [record] = await db
|
||||
.insert(files)
|
||||
@@ -176,12 +165,7 @@ export async function deleteFile(id: string, portId: string, meta: AuditMeta) {
|
||||
db
|
||||
.select({ id: expenses.id })
|
||||
.from(expenses)
|
||||
.where(
|
||||
and(
|
||||
eq(expenses.portId, portId),
|
||||
arrayContains(expenses.receiptFileIds, [id]),
|
||||
),
|
||||
)
|
||||
.where(and(eq(expenses.portId, portId), arrayContains(expenses.receiptFileIds, [id])))
|
||||
.limit(1),
|
||||
db
|
||||
.select({ id: berthMaintenanceLog.id })
|
||||
@@ -196,9 +180,7 @@ export async function deleteFile(id: string, portId: string, meta: AuditMeta) {
|
||||
]);
|
||||
|
||||
if (docRefs.length > 0 || expenseRefs.length > 0 || maintenanceRefs.length > 0) {
|
||||
throw new ConflictError(
|
||||
'File cannot be deleted because it is referenced by other records',
|
||||
);
|
||||
throw new ConflictError('File cannot be deleted because it is referenced by other records');
|
||||
}
|
||||
|
||||
// Delete from MinIO first, then DB
|
||||
@@ -235,9 +217,7 @@ export async function listFiles(portId: string, query: ListFilesInput) {
|
||||
}
|
||||
|
||||
const sortColumn =
|
||||
sort === 'filename' ? files.filename :
|
||||
sort === 'sizeBytes' ? files.sizeBytes :
|
||||
files.createdAt;
|
||||
sort === 'filename' ? files.filename : sort === 'sizeBytes' ? files.sizeBytes : files.createdAt;
|
||||
|
||||
return buildListQuery({
|
||||
table: files,
|
||||
|
||||
Reference in New Issue
Block a user