Skip to content

Commit 92dd844

Browse files
fix(#768): eliminate redundant DB reads in transaction-documents.service (#857)
- Consolidate sequential ensureTransactionExists + document queries into batched ([tx select, doc findFirst]) round-trips for findOne, addVersion, getVersions, and remove — reducing reads per call from 2 to 1 batched operation. - list: batches transaction access-select + document.findMany in a single batch (was 2 sequential queries). - getVersions: was 3 sequential queries (tx check, doc existence, versions findMany with uploadedBy); now 1 batched round-trip with versions and uploader embedded via include. - Use select: { id, buyerId, sellerId } on transaction queries to fetch only the access-control fields needed, reducing data transfer. - Remove @ts-nocheck from service and dto; add proper TypeScript types (TxAccess, DocumentWithVersions, VersionWithUploader, DocumentTypeValue). - Replace as any casts with correct Prisma-typed field assignments.
1 parent 07159ee commit 92dd844

2 files changed

Lines changed: 193 additions & 72 deletions

File tree

‎src/transactions/dto/transaction-document.dto.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
// @ts-nocheck
2-
31
import { IsString, IsIn, IsOptional, IsNumber, Min } from 'class-validator';
42

53
export const DOCUMENT_TYPE_ENUM = [
@@ -12,9 +10,11 @@ export const DOCUMENT_TYPE_ENUM = [
1210
'FLOOR_PLAN',
1311
] as const;
1412

13+
export type DocumentTypeValue = (typeof DOCUMENT_TYPE_ENUM)[number];
14+
1515
export class AttachDocumentDto {
1616
@IsIn(DOCUMENT_TYPE_ENUM)
17-
documentType: (typeof DOCUMENT_TYPE_ENUM)[number];
17+
documentType: DocumentTypeValue;
1818

1919
@IsString()
2020
fileName: string;
Lines changed: 190 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,75 @@
1-
// @ts-nocheck
2-
3-
import { ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
1+
import {
2+
ForbiddenException,
3+
Injectable,
4+
NotFoundException,
5+
} from '@nestjs/common';
6+
import { Document, DocumentVersion, Prisma } from '@prisma/client';
47
import { PrismaService } from '../database/prisma.service';
58
import { AttachDocumentDto, AddVersionDto } from './dto/transaction-document.dto';
69

10+
/** Minimal transaction fields needed for access control. */
11+
type TxAccess = Pick<Prisma.TransactionGetPayload<object>, 'id' | 'buyerId' | 'sellerId'>;
12+
13+
/** Document with its ordered versions. */
14+
type DocumentWithVersions = Document & { versions: DocumentVersion[] };
15+
16+
/** Document version with the uploader's public profile. */
17+
type VersionWithUploader = DocumentVersion & {
18+
uploadedBy: { id: string; firstName: string; lastName: string; email: string } | null;
19+
};
20+
721
@Injectable()
822
export class TransactionDocumentsService {
923
constructor(private readonly prisma: PrismaService) {}
1024

11-
/** Attach a new document to a transaction and record version 1. */
12-
async attach(transactionId: string, dto: AttachDocumentDto, userId: string, userRole?: string) {
13-
const tx = await this.ensureTransactionExists(transactionId);
25+
// ---------------------------------------------------------------------------
26+
// Helpers
27+
// ---------------------------------------------------------------------------
28+
29+
/**
30+
* Throws NotFoundException when the transaction does not exist.
31+
* Used only by `attach` which has no document to co-fetch.
32+
*/
33+
private async fetchTxAccess(transactionId: string): Promise<TxAccess> {
34+
const tx = await this.prisma.transaction.findUnique({
35+
where: { id: transactionId },
36+
select: { id: true, buyerId: true, sellerId: true },
37+
});
38+
if (!tx) throw new NotFoundException(`Transaction ${transactionId} not found`);
39+
return tx;
40+
}
41+
42+
private assertAccess(tx: TxAccess, userId: string, userRole?: string): void {
43+
const isParty = tx.buyerId === userId || tx.sellerId === userId;
44+
const isPrivileged = userRole === 'ADMIN' || userRole === 'AGENT';
45+
if (!isParty && !isPrivileged) {
46+
throw new ForbiddenException('Access denied to this transaction document');
47+
}
48+
}
49+
50+
// ---------------------------------------------------------------------------
51+
// Public methods
52+
// ---------------------------------------------------------------------------
53+
54+
/**
55+
* Attach a new document to a transaction and record version 1.
56+
* 2 queries: (1) transaction access-select, (2) document create + version create
57+
* (version create is a separate write — not avoidable without denormalising).
58+
*/
59+
async attach(
60+
transactionId: string,
61+
dto: AttachDocumentDto,
62+
userId: string,
63+
userRole?: string,
64+
): Promise<Document> {
65+
const tx = await this.fetchTxAccess(transactionId);
1466
this.assertAccess(tx, userId, userRole);
1567

1668
const document = await this.prisma.document.create({
1769
data: {
1870
transactionId,
1971
userId,
20-
documentType: dto.documentType as any,
72+
documentType: dto.documentType,
2173
fileName: dto.fileName,
2274
fileUrl: dto.fileUrl,
2375
fileSize: dto.fileSize,
@@ -26,10 +78,9 @@ export class TransactionDocumentsService {
2678
stage: dto.stage ?? null,
2779
category: dto.documentType.toLowerCase().replace('_', '-'),
2880
auditTrail: [],
29-
} as any,
81+
},
3082
});
3183

32-
// Record initial version
3384
await this.prisma.documentVersion.create({
3485
data: {
3586
documentId: document.id,
@@ -45,45 +96,88 @@ export class TransactionDocumentsService {
4596
return document;
4697
}
4798

48-
/** List all documents attached to a transaction. */
49-
async list(transactionId: string, userId: string, userRole: string) {
50-
const tx = await this.ensureTransactionExists(transactionId);
99+
/**
100+
* List all documents attached to a transaction.
101+
* Reduced from 2 sequential queries to 1 batched round-trip via
102+
* Prisma $transaction: transaction access-select + document list run
103+
* concurrently in the same connection checkout.
104+
*/
105+
async list(
106+
transactionId: string,
107+
userId: string,
108+
userRole: string,
109+
): Promise<DocumentWithVersions[]> {
110+
const [tx, documents] = await this.prisma.$transaction([
111+
this.prisma.transaction.findUnique({
112+
where: { id: transactionId },
113+
select: { id: true, buyerId: true, sellerId: true },
114+
}),
115+
this.prisma.document.findMany({
116+
where: { transactionId },
117+
orderBy: { createdAt: 'desc' },
118+
include: { versions: { orderBy: { versionNumber: 'asc' } } },
119+
}),
120+
]);
121+
122+
if (!tx) throw new NotFoundException(`Transaction ${transactionId} not found`);
51123
this.assertAccess(tx, userId, userRole);
52-
return this.prisma.document.findMany({
53-
where: { transactionId },
54-
orderBy: { createdAt: 'desc' },
55-
include: { versions: { orderBy: { versionNumber: 'asc' } } },
56-
});
124+
return documents;
57125
}
58126

59-
/** Get a single document (must belong to the transaction). */
60-
async findOne(transactionId: string, documentId: string, userId: string, userRole: string) {
61-
const tx = await this.ensureTransactionExists(transactionId);
62-
this.assertAccess(tx, userId, userRole);
127+
/**
128+
* Get a single document (must belong to the transaction).
129+
* Reduced from 2 sequential queries to 1 batched round-trip:
130+
* transaction access-select + document-with-versions fetched together.
131+
*/
132+
async findOne(
133+
transactionId: string,
134+
documentId: string,
135+
userId: string,
136+
userRole: string,
137+
): Promise<DocumentWithVersions> {
138+
const [tx, doc] = await this.prisma.$transaction([
139+
this.prisma.transaction.findUnique({
140+
where: { id: transactionId },
141+
select: { id: true, buyerId: true, sellerId: true },
142+
}),
143+
this.prisma.document.findFirst({
144+
where: { id: documentId, transactionId },
145+
include: { versions: { orderBy: { versionNumber: 'asc' } } },
146+
}),
147+
]);
63148

64-
const doc = await this.prisma.document.findFirst({
65-
where: { id: documentId, transactionId },
66-
include: { versions: { orderBy: { versionNumber: 'asc' } } },
67-
});
149+
if (!tx) throw new NotFoundException(`Transaction ${transactionId} not found`);
150+
this.assertAccess(tx, userId, userRole);
68151
if (!doc) throw new NotFoundException('Document not found for this transaction');
69152
return doc;
70153
}
71154

72-
/** Add a new version to an existing transaction document. */
155+
/**
156+
* Add a new version to an existing transaction document.
157+
* Reduced from 2 sequential read queries to 1 batched round-trip:
158+
* transaction access-select + document-with-versions fetched together,
159+
* followed by the single write batch (version create + document update).
160+
*/
73161
async addVersion(
74162
transactionId: string,
75163
documentId: string,
76164
dto: AddVersionDto,
77165
userId: string,
78166
userRole: string,
79-
) {
80-
const tx = await this.ensureTransactionExists(transactionId);
81-
this.assertAccess(tx, userId, userRole);
167+
): Promise<DocumentVersion> {
168+
const [tx, doc] = await this.prisma.$transaction([
169+
this.prisma.transaction.findUnique({
170+
where: { id: transactionId },
171+
select: { id: true, buyerId: true, sellerId: true },
172+
}),
173+
this.prisma.document.findFirst({
174+
where: { id: documentId, transactionId },
175+
include: { versions: { orderBy: { versionNumber: 'asc' } } },
176+
}),
177+
]);
82178

83-
const doc = await this.prisma.document.findFirst({
84-
where: { id: documentId, transactionId },
85-
include: { versions: { orderBy: { versionNumber: 'asc' } } },
86-
});
179+
if (!tx) throw new NotFoundException(`Transaction ${transactionId} not found`);
180+
this.assertAccess(tx, userId, userRole);
87181
if (!doc) throw new NotFoundException('Document not found for this transaction');
88182

89183
const nextVersion = (doc.versions?.length ?? 0) + 1;
@@ -100,7 +194,6 @@ export class TransactionDocumentsService {
100194
changeNote: dto.changeNote,
101195
},
102196
}),
103-
// Update the document to point to the latest file
104197
this.prisma.document.update({
105198
where: { id: documentId },
106199
data: { fileUrl: dto.fileUrl, fileName: dto.fileName, fileSize: dto.fileSize },
@@ -110,47 +203,75 @@ export class TransactionDocumentsService {
110203
return version;
111204
}
112205

113-
/** List all versions of a document. */
114-
async getVersions(transactionId: string, documentId: string, userId: string, userRole: string) {
115-
const tx = await this.ensureTransactionExists(transactionId);
116-
this.assertAccess(tx, userId, userRole);
117-
118-
const doc = await this.prisma.document.findFirst({
119-
where: { id: documentId, transactionId },
120-
});
121-
if (!doc) throw new NotFoundException('Document not found for this transaction');
122-
return this.prisma.documentVersion.findMany({
123-
where: { documentId },
124-
orderBy: { versionNumber: 'asc' },
125-
include: {
126-
uploadedBy: { select: { id: true, firstName: true, lastName: true, email: true } },
127-
},
128-
});
129-
}
206+
/**
207+
* List all versions of a document.
208+
*
209+
* Previously made 3 sequential round-trips:
210+
* 1. ensureTransactionExists
211+
* 2. document.findFirst (existence check only)
212+
* 3. documentVersion.findMany (with uploadedBy)
213+
*
214+
* Now consolidated to 1 batched round-trip: transaction access fields +
215+
* document-with-versions-and-uploader fetched together.
216+
*/
217+
async getVersions(
218+
transactionId: string,
219+
documentId: string,
220+
userId: string,
221+
userRole: string,
222+
): Promise<VersionWithUploader[]> {
223+
const [tx, doc] = await this.prisma.$transaction([
224+
this.prisma.transaction.findUnique({
225+
where: { id: transactionId },
226+
select: { id: true, buyerId: true, sellerId: true },
227+
}),
228+
this.prisma.document.findFirst({
229+
where: { id: documentId, transactionId },
230+
include: {
231+
versions: {
232+
orderBy: { versionNumber: 'asc' },
233+
include: {
234+
uploadedBy: {
235+
select: { id: true, firstName: true, lastName: true, email: true },
236+
},
237+
},
238+
},
239+
},
240+
}),
241+
]);
130242

131-
/** Remove a document from a transaction. */
132-
async remove(transactionId: string, documentId: string, userId: string, userRole: string) {
133-
const tx = await this.ensureTransactionExists(transactionId);
243+
if (!tx) throw new NotFoundException(`Transaction ${transactionId} not found`);
134244
this.assertAccess(tx, userId, userRole);
135-
136-
const doc = await this.prisma.document.findFirst({
137-
where: { id: documentId, transactionId },
138-
});
139245
if (!doc) throw new NotFoundException('Document not found for this transaction');
140-
return this.prisma.document.delete({ where: { id: documentId } });
246+
247+
return doc.versions as VersionWithUploader[];
141248
}
142249

143-
private async ensureTransactionExists(transactionId: string) {
144-
const tx = await this.prisma.transaction.findUnique({ where: { id: transactionId } });
250+
/**
251+
* Remove a document from a transaction.
252+
* Reduced from 2 sequential read queries to 1 batched round-trip:
253+
* transaction access-select + document existence check fetched together.
254+
*/
255+
async remove(
256+
transactionId: string,
257+
documentId: string,
258+
userId: string,
259+
userRole: string,
260+
): Promise<Document> {
261+
const [tx, doc] = await this.prisma.$transaction([
262+
this.prisma.transaction.findUnique({
263+
where: { id: transactionId },
264+
select: { id: true, buyerId: true, sellerId: true },
265+
}),
266+
this.prisma.document.findFirst({
267+
where: { id: documentId, transactionId },
268+
}),
269+
]);
270+
145271
if (!tx) throw new NotFoundException(`Transaction ${transactionId} not found`);
146-
return tx;
147-
}
272+
this.assertAccess(tx, userId, userRole);
273+
if (!doc) throw new NotFoundException('Document not found for this transaction');
148274

149-
private assertAccess(tx: any, userId: string, userRole?: string) {
150-
const isParty = tx.buyerId === userId || tx.sellerId === userId;
151-
const isPrivileged = userRole === 'ADMIN' || userRole === 'AGENT';
152-
if (!isParty && !isPrivileged) {
153-
throw new ForbiddenException('Access denied to this transaction document');
154-
}
275+
return this.prisma.document.delete({ where: { id: documentId } });
155276
}
156277
}

0 commit comments

Comments
 (0)