Skip to content

Commit 25ad430

Browse files
fix(committees): forward folder_uid on document upload (#660)
1 parent 4f286b1 commit 25ad430

7 files changed

Lines changed: 83 additions & 11 deletions

File tree

apps/lfx-one/src/app/modules/committees/components/committee-documents/committee-documents.component.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,14 +133,14 @@ export class CommitteeDocumentsComponent {
133133
data: {
134134
mode: 'file',
135135
committeeId: this.committee().uid,
136+
folders: this.folderOptions(),
137+
defaultParentUid: this.currentFolderUid(),
136138
},
137139
});
138140

139141
dialogRef?.onClose.pipe(take(1)).subscribe({
140142
next: (result: boolean | undefined) => {
141143
if (result) {
142-
// Upload API doesn't accept folder_uid yet — pop to root so the new file is visible.
143-
this.currentFolderUid.set(null);
144144
this.refreshTrigger.update((v) => v + 1);
145145
}
146146
},

apps/lfx-one/src/app/modules/committees/components/document-form/document-form.component.html

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,23 @@
5454
<p class="mt-1 text-xs text-red-600">Name is required</p>
5555
}
5656
</div>
57+
58+
<!-- Folder (optional) -->
59+
@if (folderOptions?.length > 0) {
60+
<div>
61+
<label class="block text-[11px] font-semibold text-gray-500 uppercase tracking-wide mb-1">Folder (optional)</label>
62+
<lfx-select
63+
size="small"
64+
[form]="form"
65+
control="parent_uid"
66+
[options]="folderOptions"
67+
placeholder="No folder"
68+
[showClear]="true"
69+
styleClass="w-full"
70+
data-testid="document-form-folder"></lfx-select>
71+
<p class="mt-1 text-[11px] text-gray-400">Place this file inside a folder</p>
72+
</div>
73+
}
5774
} @else if (isLink()) {
5875
<!-- Link URL -->
5976
<div>
@@ -88,7 +105,7 @@
88105
</div>
89106

90107
<!-- Folder (optional) -->
91-
@if (folderOptions.length > 0) {
108+
@if (folderOptions?.length > 0) {
92109
<div>
93110
<label class="block text-[11px] font-semibold text-gray-500 uppercase tracking-wide mb-1">Folder (optional)</label>
94111
<lfx-select

apps/lfx-one/src/app/modules/committees/components/document-form/document-form.component.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export class DocumentFormComponent {
8989
.uploadCommitteeDocument(this.committeeId, file, {
9090
name: formValue.name,
9191
description: formValue.description || undefined,
92+
folder_uid: formValue.parent_uid || undefined,
9293
})
9394
.subscribe({
9495
next: () => {
@@ -210,6 +211,7 @@ export class DocumentFormComponent {
210211
return new FormGroup({
211212
name: new FormControl('', [Validators.required]),
212213
description: new FormControl(''),
214+
parent_uid: new FormControl<string | null>(this.defaultParentUid),
213215
});
214216
}
215217

apps/lfx-one/src/app/shared/services/committee.service.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,11 @@ export class CommitteeService {
136136
* with metadata as query params. The BFF forwards as multipart/form-data to the
137137
* upstream committee service.
138138
*/
139-
public uploadCommitteeDocument(committeeId: string, file: File, metadata: { name: string; description?: string }): Observable<CommitteeDocument> {
139+
public uploadCommitteeDocument(
140+
committeeId: string,
141+
file: File,
142+
metadata: { name: string; description?: string; folder_uid?: string }
143+
): Observable<CommitteeDocument> {
140144
let params = new HttpParams()
141145
.set('name', metadata.name)
142146
.set('file_name', file.name)
@@ -146,6 +150,9 @@ export class CommitteeService {
146150
if (metadata.description) {
147151
params = params.set('description', metadata.description);
148152
}
153+
if (metadata.folder_uid) {
154+
params = params.set('folder_uid', metadata.folder_uid);
155+
}
149156

150157
return this.http
151158
.post<CommitteeDocument>(`/api/committees/${committeeId}/documents/upload`, file, {

apps/lfx-one/src/server/controllers/committee.controller.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ function contentDispositionAttachment(fileName: string): string {
3737
return `attachment; filename="${safeAscii}"; filename*=UTF-8''${encoded}`;
3838
}
3939

40+
const FOLDER_UID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
41+
4042
/**
4143
* Controller for handling committee HTTP requests
4244
*/
@@ -634,12 +636,14 @@ export class CommitteeController {
634636
const contentType = getStringQueryParam(req, 'content_type');
635637
const fileSize = getStringQueryParam(req, 'file_size');
636638
const description = getStringQueryParam(req, 'description');
639+
const folderUid = getStringQueryParam(req, 'folder_uid');
637640

638641
const startTime = logger.startOperation(req, 'upload_committee_document', {
639642
committee_id: id,
640643
file_name: fileName,
641644
file_size: fileSize,
642645
content_type: contentType,
646+
folder_uid: folderUid,
643647
});
644648

645649
try {
@@ -743,12 +747,26 @@ export class CommitteeController {
743747
return;
744748
}
745749

750+
// Validate folder_uid shape so upstream can't be sent a malformed identifier.
751+
const trimmedFolderUid = folderUid?.trim();
752+
if (trimmedFolderUid && !FOLDER_UID_PATTERN.test(trimmedFolderUid)) {
753+
next(
754+
ServiceValidationError.forField('folder_uid', 'folder_uid must be a valid UUID', {
755+
operation: 'upload_committee_document',
756+
service: 'committee_controller',
757+
path: req.path,
758+
})
759+
);
760+
return;
761+
}
762+
746763
const uploadData: UploadCommitteeDocumentRequest = {
747764
name: trimmedName!,
748765
file_name: trimmedFileName!,
749766
content_type: contentType!,
750767
file_size: fileSizeNum,
751768
...(description && { description }),
769+
...(trimmedFolderUid && { folder_uid: trimmedFolderUid }),
752770
};
753771

754772
const result = await this.committeeService.uploadCommitteeDocument(req, id, fileBuffer, uploadData);

apps/lfx-one/src/server/services/committee.service.ts

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { Request } from 'express';
2222
import FormData from 'form-data';
2323

2424
import { ResourceNotFoundError } from '../errors';
25+
import { pollEndpoint } from '../helpers/poll-endpoint.helper';
2526
import { fetchAllQueryResources } from '../helpers/query-service.helper';
2627
import { logger } from '../services/logger.service';
2728
import { getUsernameFromAuth } from '../utils/auth-helper';
@@ -93,6 +94,7 @@ interface CommitteeDocumentQueryResult {
9394
content_type?: string;
9495
description?: string;
9596
committee_uid?: string;
97+
folder_uid?: string;
9698
created_at?: string;
9799
updated_at?: string;
98100
uploaded_by_username?: string;
@@ -792,6 +794,7 @@ export class CommitteeService {
792794
created_at: f.created_at,
793795
updated_at: f.updated_at,
794796
uploaded_by: f.uploaded_by_username,
797+
parent_uid: f.folder_uid,
795798
committee_uid: f.committee_uid,
796799
}));
797800

@@ -888,10 +891,8 @@ export class CommitteeService {
888891
file_size: uploadData.file_size,
889892
});
890893

891-
// file_size is intentionally omitted — upstream UploadCommitteeDocumentRequestBody
892-
// only declares name, file_name, content_type, file, description. Goa silently
893-
// drops unknown multipart fields.
894-
// TODO: append folder_uid once upstream accepts it (LFXV2-1632).
894+
// file_size is intentionally omitted — upstream UploadCommitteeDocumentRequestBody declares
895+
// name, file_name, content_type, file, description, folder_uid. Goa drops unknown fields.
895896
const formData = new FormData();
896897
formData.append('file', fileBuffer, {
897898
filename: uploadData.file_name,
@@ -903,14 +904,19 @@ export class CommitteeService {
903904
if (uploadData.description) {
904905
formData.append('description', uploadData.description);
905906
}
907+
if (uploadData.folder_uid) {
908+
formData.append('folder_uid', uploadData.folder_uid);
909+
}
906910

911+
// X-Sync=true blocks until the upstream indexer ACKs the publish, preventing stale list reads.
907912
const result = await this.microserviceProxy.proxyRequest<CommitteeDocumentUpstreamResponse>(
908913
req,
909914
'LFX_V2_SERVICE',
910915
`/committees/${committeeId}/documents`,
911916
'POST',
912917
undefined,
913-
formData
918+
formData,
919+
{ 'X-Sync': 'true' }
914920
);
915921

916922
logger.info(req, 'upload_committee_document', 'Committee document uploaded successfully', {
@@ -920,6 +926,28 @@ export class CommitteeService {
920926
file_size: result.file_size,
921927
});
922928

929+
// Poll until the query service sees the new doc — indexer is async to the upstream write.
930+
await pollEndpoint({
931+
req,
932+
operation: 'upload_committee_document_index_poll',
933+
pollFn: async () => {
934+
const { resources } = await this.microserviceProxy.proxyRequest<QueryServiceResponse<{ uid: string }>>(
935+
req,
936+
'LFX_V2_SERVICE',
937+
'/query/resources',
938+
'GET',
939+
{
940+
type: 'committee_document',
941+
tags: `committee_document_uid:${result.uid}`,
942+
}
943+
);
944+
return (resources?.length ?? 0) > 0;
945+
},
946+
maxRetries: 5,
947+
retryDelayMs: 400,
948+
metadata: { committee_uid: committeeId, document_uid: result.uid },
949+
});
950+
923951
return {
924952
uid: result.uid,
925953
type: 'file',

packages/shared/src/interfaces/committee.interface.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -544,8 +544,8 @@ export interface UploadCommitteeDocumentRequest {
544544
/** Optional description (max 2000 chars) */
545545
description?: string;
546546
/**
547-
* TODO: add once upstream `POST /committees/{uid}/documents` accepts `folder_uid`.
548-
* Until then, all uploaded files land at the committee root.
547+
* Optional folder UID to nest the file inside a committee folder.
548+
* When omitted, the file lands at the committee root.
549549
*/
550550
folder_uid?: string;
551551
}

0 commit comments

Comments
 (0)