From a438738d19e53359974ec247338eae40aa61086b Mon Sep 17 00:00:00 2001 From: Manish Dixit Date: Thu, 19 Mar 2026 09:47:04 -0700 Subject: [PATCH 1/2] feat(committees): add BFF endpoints for meetings and surveys - Add subResourceHandler factory for standardized committee sub-resource endpoints - Add GET /api/committees/:id/meetings endpoint with query param whitelisting - Add GET /api/committees/:id/surveys endpoint with tag-based filtering - Add getCommitteeMeetings to backend CommitteeService (lazy MeetingService import) - Add getCommitteeSurveys to backend SurveyService with sanitized query params - Add getCommitteeMeetings FE method to CommitteeService - Add getSurveysByCommittee FE method to SurveyService - Switch upcoming-committee-meeting to use CommitteeService.getCommitteeMeetings - Remove unused CommitteeJoinApplication interface - Remove mailing_list/chat_channel PATCH from updateCommittee (not supported upstream) LFXV2-1190 Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Manish Dixit --- .../upcoming-committee-meeting.component.ts | 56 ++++-------- .../app/shared/services/committee.service.ts | 14 ++- .../src/app/shared/services/survey.service.ts | 4 + .../controllers/committee.controller.ts | 85 +++++++++++++++++++ .../src/server/routes/committees.route.ts | 5 ++ .../src/server/services/committee.service.ts | 82 ++++++++++++------ .../src/server/services/survey.service.ts | 34 ++++++++ .../committee-application.interface.ts | 29 ------- packages/shared/src/interfaces/index.ts | 3 - 9 files changed, 213 insertions(+), 99 deletions(-) delete mode 100644 packages/shared/src/interfaces/committee-application.interface.ts diff --git a/apps/lfx-one/src/app/modules/committees/components/upcoming-committee-meeting/upcoming-committee-meeting.component.ts b/apps/lfx-one/src/app/modules/committees/components/upcoming-committee-meeting/upcoming-committee-meeting.component.ts index 8c656787c..e2e671f7c 100644 --- a/apps/lfx-one/src/app/modules/committees/components/upcoming-committee-meeting/upcoming-committee-meeting.component.ts +++ b/apps/lfx-one/src/app/modules/committees/components/upcoming-committee-meeting/upcoming-committee-meeting.component.ts @@ -6,10 +6,10 @@ import { toSignal } from '@angular/core/rxjs-interop'; import { RouterLink } from '@angular/router'; import { Meeting } from '@lfx-one/shared/interfaces'; import { MeetingTimePipe } from '@pipes/meeting-time.pipe'; -import { MeetingService } from '@services/meeting.service'; +import { CommitteeService } from '@services/committee.service'; import { ProjectService } from '@services/project.service'; import { TooltipModule } from 'primeng/tooltip'; -import { filter, map, of } from 'rxjs'; +import { map, of } from 'rxjs'; @Component({ selector: 'lfx-upcoming-committee-meeting', @@ -17,8 +17,8 @@ import { filter, map, of } from 'rxjs'; templateUrl: './upcoming-committee-meeting.component.html', }) export class UpcomingCommitteeMeetingComponent implements OnInit { + private readonly committeeService = inject(CommitteeService); private readonly projectService = inject(ProjectService); - private readonly meetingService = inject(MeetingService); private readonly injector = inject(Injector); public readonly committeeId = input(null); @@ -37,9 +37,21 @@ export class UpcomingCommitteeMeetingComponent implements OnInit { } private initializeUpcomingMeeting(): Signal { - return toSignal(this.project() ? this.getNextUpcomingCommitteeMeeting(this.project()!.uid, this.committeeId()) : of(null), { - initialValue: null, - }); + const committeeId = this.committeeId(); + if (!committeeId) return toSignal(of(null), { initialValue: null }); + + return toSignal( + this.committeeService.getCommitteeMeetings(committeeId).pipe( + map((meetings: Meeting[]) => { + const now = new Date().getTime(); + const upcoming = meetings + .filter((m) => m.start_time && new Date(m.start_time).getTime() > now) + .sort((a, b) => new Date(a.start_time).getTime() - new Date(b.start_time).getTime()); + return upcoming[0] ?? null; + }) + ), + { initialValue: null } + ); } private initializeCommittees() { @@ -50,36 +62,4 @@ export class UpcomingCommitteeMeetingComponent implements OnInit { .join(', ') ?? '' ); } - - private getNextUpcomingCommitteeMeeting(uid: string, committeeId: string | null = null) { - return this.meetingService.getMeetingsByProject(uid).pipe( - filter((meetings: Meeting[]) => { - // Return only meetings that have a start time in the future and has a committee value regardless of the committee id - return ( - meetings.filter((meeting) => new Date(meeting.start_time).getTime() > new Date().getTime() && meeting.committees && meeting.committees?.length > 0) - .length > 0 - ); - }), - map((meetings: Meeting[]) => { - if (meetings.length > 0) { - if (committeeId) { - // Find the earliest upcoming meeting that has the committee id and return it - const committeeMeetings = meetings.filter( - (meeting) => - new Date(meeting.start_time).getTime() > new Date().getTime() && - meeting.committees && - meeting.committees?.length > 0 && - meeting.committees.some((c) => c.uid === committeeId) - ); - - return committeeMeetings.sort((a, b) => new Date(a.start_time).getTime() - new Date(b.start_time).getTime())[0]; - } - - // Return the next upcoming meeting by date in the future - return meetings.filter((meeting) => new Date(meeting.start_time).getTime() > new Date().getTime())[0]; - } - return null; - }) - ); - } } diff --git a/apps/lfx-one/src/app/shared/services/committee.service.ts b/apps/lfx-one/src/app/shared/services/committee.service.ts index 1378a7714..7c9f3a0f9 100644 --- a/apps/lfx-one/src/app/shared/services/committee.service.ts +++ b/apps/lfx-one/src/app/shared/services/committee.service.ts @@ -3,7 +3,15 @@ import { HttpClient, HttpParams } from '@angular/common/http'; import { inject, Injectable, signal, WritableSignal } from '@angular/core'; -import { Committee, CommitteeMember, CreateCommitteeMemberRequest, MyCommittee, QueryServiceCountResponse } from '@lfx-one/shared/interfaces'; +import { + Committee, + CommitteeMember, + CreateCommitteeMemberRequest, + Meeting, + MyCommittee, + PaginatedResponse, + QueryServiceCountResponse, +} from '@lfx-one/shared/interfaces'; import { catchError, map, Observable, of, take, tap, throwError } from 'rxjs'; @Injectable({ @@ -79,6 +87,10 @@ export class CommitteeService { return this.http.delete(`/api/committees/${committeeId}/members/${memberId}`).pipe(take(1)); } + public getCommitteeMeetings(committeeId: string): Observable { + return this.http.get>(`/api/committees/${committeeId}/meetings`).pipe(map((response) => response.data)); + } + // ── Join / Leave Methods ────────────────────────────────────────────────── /** Self-join an open group */ diff --git a/apps/lfx-one/src/app/shared/services/survey.service.ts b/apps/lfx-one/src/app/shared/services/survey.service.ts index ad7ddadb5..3d9a99d16 100644 --- a/apps/lfx-one/src/app/shared/services/survey.service.ts +++ b/apps/lfx-one/src/app/shared/services/survey.service.ts @@ -35,6 +35,10 @@ export class SurveyService { return this.getSurveys(params); } + public getSurveysByCommittee(committeeUid: string): Observable { + return this.http.get(`/api/committees/${committeeUid}/surveys`); + } + public getSurvey(surveyUid: string, projectId?: string): Observable { let params = new HttpParams(); if (projectId) { diff --git a/apps/lfx-one/src/server/controllers/committee.controller.ts b/apps/lfx-one/src/server/controllers/committee.controller.ts index 8ce14968b..23638720d 100644 --- a/apps/lfx-one/src/server/controllers/committee.controller.ts +++ b/apps/lfx-one/src/server/controllers/committee.controller.ts @@ -7,12 +7,24 @@ import { NextFunction, Request, Response } from 'express'; import { ServiceValidationError } from '../errors'; import { logger } from '../services/logger.service'; import { CommitteeService } from '../services/committee.service'; +import { SurveyService } from '../services/survey.service'; /** * Controller for handling committee HTTP requests */ export class CommitteeController { private committeeService: CommitteeService = new CommitteeService(); + // Cross-domain: surveys are accessed via committee context for the surveys tab + private readonly surveyService = new SurveyService(); + + // ── Dashboard Sub-Resource Handlers (via factory) ───────────────────────── + + /** GET /committees/:id/meetings */ + public getCommitteeMeetings = this.subResourceHandler( + 'get_committee_meetings', + (req, id) => this.committeeService.getCommitteeMeetings(req, id, req.query as Record), + 'meeting_count' + ); /** * GET /committees @@ -521,4 +533,77 @@ export class CommitteeController { next(error); } } + + /** + * GET /committees/:id/surveys + * Manual handler (not using subResourceHandler) because this endpoint needs req.query passthrough for survey filtering + */ + public async getCommitteeSurveys(req: Request, res: Response, next: NextFunction): Promise { + const { id } = req.params; + + if (!id) { + next( + ServiceValidationError.forField('id', 'Committee ID is required', { + operation: 'get_committee_surveys', + service: 'committee_controller', + path: req.path, + }) + ); + return; + } + + const startTime = logger.startOperation(req, 'get_committee_surveys', { + committee_id: id, + query_params: logger.sanitize(req.query as Record), + }); + + try { + const surveys = await this.surveyService.getCommitteeSurveys(req, id, req.query as Record); + + logger.success(req, 'get_committee_surveys', startTime, { + committee_id: id, + survey_count: surveys.length, + }); + + res.json(surveys); + } catch (error) { + next(error); + } + } + + // ── Private helpers ────────────────────────────────────────────────────── + + /** + * Factory that produces a standard sub-resource handler. + * Validates the committee ID, starts an operation, calls the service, + * logs success, and delegates errors to Express error middleware. + */ + private subResourceHandler(operation: string, serviceFn: (req: Request, id: string) => Promise, countKey: string) { + return async (req: Request, res: Response, next: NextFunction): Promise => { + const committeeId = req.params['id']; + if (!committeeId) { + next( + ServiceValidationError.forField('id', 'Committee ID is required', { + operation, + service: 'committee_controller', + path: req.path, + }) + ); + return; + } + + const startTime = logger.startOperation(req, operation, { committee_id: committeeId }); + try { + const result = await serviceFn(req, committeeId); + logger.success(req, operation, startTime, { + committee_id: committeeId, + [countKey]: Array.isArray(result) ? result.length : !!result, + }); + res.json(result); + } catch (error) { + logger.error(req, operation, startTime, error, { committee_id: committeeId }); + next(error); + } + }; + } } diff --git a/apps/lfx-one/src/server/routes/committees.route.ts b/apps/lfx-one/src/server/routes/committees.route.ts index 2887494be..70c2ee115 100644 --- a/apps/lfx-one/src/server/routes/committees.route.ts +++ b/apps/lfx-one/src/server/routes/committees.route.ts @@ -25,8 +25,13 @@ router.post('/:id/members', (req, res, next) => committeeController.createCommit router.put('/:id/members/:memberId', (req, res, next) => committeeController.updateCommitteeMember(req, res, next)); router.delete('/:id/members/:memberId', (req, res, next) => committeeController.deleteCommitteeMember(req, res, next)); +// Meeting routes +router.get('/:id/meetings', (req, res, next) => committeeController.getCommitteeMeetings(req, res, next)); + // ── Join / Leave routes ──────────────────────────────────────────────────── router.post('/:id/join', (req, res, next) => committeeController.joinCommittee(req, res, next)); router.delete('/:id/leave', (req, res, next) => committeeController.leaveCommittee(req, res, next)); +// Survey routes +router.get('/:id/surveys', (req, res, next) => committeeController.getCommitteeSurveys(req, res, next)); export default router; diff --git a/apps/lfx-one/src/server/services/committee.service.ts b/apps/lfx-one/src/server/services/committee.service.ts index d975e873f..b3a96526d 100644 --- a/apps/lfx-one/src/server/services/committee.service.ts +++ b/apps/lfx-one/src/server/services/committee.service.ts @@ -8,6 +8,8 @@ import { CommitteeSettingsData, CommitteeUpdateData, CreateCommitteeMemberRequest, + Meeting, + PaginatedResponse, MyCommittee, QueryServiceCountResponse, QueryServiceResponse, @@ -17,11 +19,15 @@ import { Request } from 'express'; import { getUsernameFromAuth } from '../utils/auth-helper'; import { ResourceNotFoundError } from '../errors'; -import { logger } from '../services/logger.service'; +import { logger } from './logger.service'; import { AccessCheckService } from './access-check.service'; import { ETagService } from './etag.service'; import { MicroserviceProxyService } from './microservice-proxy.service'; +// MeetingService is dynamically imported to avoid circular dependency. +// Use import('...') type to reference the class without a static import. +type MeetingServiceType = InstanceType; + /** * Service for handling committee business logic */ @@ -29,6 +35,8 @@ export class CommitteeService { private accessCheckService: AccessCheckService; private etagService: ETagService; private microserviceProxy: MicroserviceProxyService; + // Promise-based lazy initializer to avoid concurrent imports creating duplicate instances + private meetingServicePromise?: Promise; public constructor() { this.accessCheckService = new AccessCheckService(); @@ -138,12 +146,11 @@ export class CommitteeService { * Updates an existing committee using ETag for concurrency control */ public async updateCommittee(req: Request, committeeId: string, data: CommitteeUpdateData): Promise { - // Extract settings and channel fields from core committee data - const { business_email_required, is_audit_enabled, show_meeting_attendees, member_visibility, mailing_list, chat_channel, ...committeeData } = data; + // Extract settings fields from core committee data + const { business_email_required, is_audit_enabled, show_meeting_attendees, member_visibility, ...committeeData } = data; const hasSettingsUpdate = business_email_required !== undefined || is_audit_enabled !== undefined || show_meeting_attendees !== undefined || member_visibility !== undefined; - const hasChannelsUpdate = mailing_list !== undefined || chat_channel !== undefined; const hasCoreUpdate = Object.keys(committeeData).length > 0; let updatedCommittee: Committee; @@ -166,30 +173,7 @@ export class CommitteeService { updatedCommittee = await this.microserviceProxy.proxyRequest(req, 'LFX_V2_SERVICE', `/committees/${committeeId}`, 'GET'); } - // Step 3: Update channels via PATCH (mailing_list/chat_channel are not accepted by PUT) - if (hasChannelsUpdate) { - try { - const channelsPayload: Record = {}; - if (mailing_list !== undefined) channelsPayload['mailing_list'] = mailing_list; - if (chat_channel !== undefined) channelsPayload['chat_channel'] = chat_channel; - - logger.debug(req, 'update_committee_channels', 'Updating committee channels via PATCH', { - committee_uid: committeeId, - fields: Object.keys(channelsPayload), - }); - - const patched = await this.microserviceProxy.proxyRequest(req, 'LFX_V2_SERVICE', `/committees/${committeeId}`, 'PATCH', {}, channelsPayload); - - updatedCommittee = { ...updatedCommittee, ...patched }; - } catch (error) { - logger.warning(req, 'update_committee_channels', 'PATCH failed for channels, returning current committee data', { - committee_uid: committeeId, - error: error instanceof Error ? error.message : 'Unknown error', - }); - } - } - - // Step 5: Update settings if provided + // Step 3: Update settings if provided if (hasSettingsUpdate) { try { await this.updateCommitteeSettings(req, committeeId, { @@ -390,6 +374,48 @@ export class CommitteeService { return userMemberships; } + /** + * Fetches meetings associated with a committee. + */ + public async getCommitteeMeetings(req: Request, committeeId: string, query: Record = {}): Promise> { + try { + // Whitelist allowed query params to prevent unexpected parameters from reaching downstream + const allowedParams = ['page_size', 'page_token', 'order_by', 'committee_uid']; + const sanitizedQuery: Record = {}; + for (const key of allowedParams) { + if (query[key]) sanitizedQuery[key] = String(query[key]); + } + + const params = { + ...sanitizedQuery, + committee_uid: committeeId, + }; + + logger.debug(req, 'get_committee_meetings', 'Fetching meetings for committee', { + committee_uid: committeeId, + }); + + // Lazy import to avoid circular dependency — Promise ensures only one instance is created + if (!this.meetingServicePromise) { + this.meetingServicePromise = import('./meeting.service').then((m) => new m.MeetingService()); + } + const meetingService = await this.meetingServicePromise; + const result = await meetingService.getMeetings(req, params); + + logger.debug(req, 'get_committee_meetings', 'Fetched committee meetings', { + committee_uid: committeeId, + count: result.data.length, + }); + + return result; + } catch { + logger.warning(req, 'get_committee_meetings', 'Failed to fetch committee meetings, returning empty', { + committee_uid: committeeId, + }); + return { data: [], page_token: undefined }; + } + } + // ── My Committees ───────────────────────────────────────────────────────── public async getMyCommittees(req: Request, projectUid?: string): Promise { diff --git a/apps/lfx-one/src/server/services/survey.service.ts b/apps/lfx-one/src/server/services/survey.service.ts index 89195c2d9..c9b459fab 100644 --- a/apps/lfx-one/src/server/services/survey.service.ts +++ b/apps/lfx-one/src/server/services/survey.service.ts @@ -89,6 +89,40 @@ export class SurveyService { return survey; } + /** + * Fetches surveys for a specific committee by committee_uid + */ + public async getCommitteeSurveys(req: Request, committeeId: string, query: Record = {}): Promise { + logger.debug(req, 'get_committee_surveys', 'Fetching surveys for committee', { + committee_uid: committeeId, + }); + + // Whitelist allowed query params to prevent unexpected parameters from reaching downstream + const allowedParams = ['page_size', 'page_token', 'order_by', 'status']; + const sanitizedQuery: Record = {}; + for (const key of allowedParams) { + if (query[key]) sanitizedQuery[key] = String(query[key]); + } + + // Use tags parameter for server-side filtering — committee_uid is not a supported query param + const params = { + ...sanitizedQuery, + type: 'survey', + tags: `committee_uid:${committeeId}`, + }; + + const { resources } = await this.microserviceProxy.proxyRequest>(req, 'LFX_V2_SERVICE', '/query/resources', 'GET', params); + + const surveys: Survey[] = (resources ?? []).map((resource) => resource.data); + + logger.debug(req, 'get_committee_surveys', 'Completed committee survey fetch', { + committee_uid: committeeId, + count: surveys.length, + }); + + return surveys; + } + /** * Deletes a survey using ETag for concurrency control */ diff --git a/packages/shared/src/interfaces/committee-application.interface.ts b/packages/shared/src/interfaces/committee-application.interface.ts deleted file mode 100644 index 3ef9536ee..000000000 --- a/packages/shared/src/interfaces/committee-application.interface.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright The Linux Foundation and each contributor to LFX. -// SPDX-License-Identifier: MIT - -/** - * Status of a committee join application - */ -export type CommitteeJoinApplicationStatus = 'pending' | 'approved' | 'rejected'; - -/** - * Represents a join application for a committee - */ -export interface CommitteeJoinApplication { - uid: string; - committee_uid: string; - applicant_email: string; - applicant_name?: string; - applicant_uid?: string; - status: CommitteeJoinApplicationStatus; - reason?: string; - created_at: string; - updated_at?: string; -} - -/** - * Request payload to create a committee join application - */ -export interface CreateCommitteeJoinApplicationRequest { - reason?: string; -} diff --git a/packages/shared/src/interfaces/index.ts b/packages/shared/src/interfaces/index.ts index 9739d17bc..c755e13ca 100644 --- a/packages/shared/src/interfaces/index.ts +++ b/packages/shared/src/interfaces/index.ts @@ -86,8 +86,5 @@ export * from './filter.interface'; // Lens interfaces export * from './lens.interface'; -// Committee application interfaces -export * from './committee-application.interface'; - // Public committee interfaces export * from './public-committee.interface'; From 0a8befe59408ca1ca865e441cbe3d959d02d4e2d Mon Sep 17 00:00:00 2001 From: Manish Dixit Date: Thu, 19 Mar 2026 10:01:03 -0700 Subject: [PATCH 2/2] fix(committees): address review findings on BFF endpoints PR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix subResourceHandler countKey to handle PaginatedResponse shapes (was logging boolean instead of count for meetings endpoint) - Remove duplicate logger.error from subResourceHandler catch (error middleware handles it) - Fix query param whitelisting to use !== undefined (was dropping falsy values) - Strip mailing_list/chat_channel from updateCommittee PUT payload (upstream rejects these fields) - Reset cached meetingServicePromise on failure (was caching rejected promise forever) - Fix misleading comment on getCommitteeSurveys handler - Fix Record → Record on logger.sanitize call - Add catchError to FE getSurveysByCommittee for consistency LFXV2-1190 Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Manish Dixit --- .../src/app/shared/services/survey.service.ts | 2 +- .../server/controllers/committee.controller.ts | 15 +++++++++++---- .../src/server/services/committee.service.ts | 13 +++++++++---- .../lfx-one/src/server/services/survey.service.ts | 2 +- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/apps/lfx-one/src/app/shared/services/survey.service.ts b/apps/lfx-one/src/app/shared/services/survey.service.ts index 3d9a99d16..f344d6b70 100644 --- a/apps/lfx-one/src/app/shared/services/survey.service.ts +++ b/apps/lfx-one/src/app/shared/services/survey.service.ts @@ -36,7 +36,7 @@ export class SurveyService { } public getSurveysByCommittee(committeeUid: string): Observable { - return this.http.get(`/api/committees/${committeeUid}/surveys`); + return this.http.get(`/api/committees/${committeeUid}/surveys`).pipe(catchError(() => of([]))); } public getSurvey(surveyUid: string, projectId?: string): Observable { diff --git a/apps/lfx-one/src/server/controllers/committee.controller.ts b/apps/lfx-one/src/server/controllers/committee.controller.ts index 23638720d..71831d06f 100644 --- a/apps/lfx-one/src/server/controllers/committee.controller.ts +++ b/apps/lfx-one/src/server/controllers/committee.controller.ts @@ -536,7 +536,7 @@ export class CommitteeController { /** * GET /committees/:id/surveys - * Manual handler (not using subResourceHandler) because this endpoint needs req.query passthrough for survey filtering + * Manual handler (not using subResourceHandler) because surveys need custom query param passthrough for filtering by status */ public async getCommitteeSurveys(req: Request, res: Response, next: NextFunction): Promise { const { id } = req.params; @@ -554,7 +554,7 @@ export class CommitteeController { const startTime = logger.startOperation(req, 'get_committee_surveys', { committee_id: id, - query_params: logger.sanitize(req.query as Record), + query_params: logger.sanitize(req.query as Record), }); try { @@ -595,13 +595,20 @@ export class CommitteeController { const startTime = logger.startOperation(req, operation, { committee_id: committeeId }); try { const result = await serviceFn(req, committeeId); + let count: number | boolean; + if (Array.isArray(result)) { + count = result.length; + } else if (result && typeof result === 'object' && 'data' in result && Array.isArray((result as { data: unknown[] }).data)) { + count = (result as { data: unknown[] }).data.length; + } else { + count = !!result; + } logger.success(req, operation, startTime, { committee_id: committeeId, - [countKey]: Array.isArray(result) ? result.length : !!result, + [countKey]: count, }); res.json(result); } catch (error) { - logger.error(req, operation, startTime, error, { committee_id: committeeId }); next(error); } }; diff --git a/apps/lfx-one/src/server/services/committee.service.ts b/apps/lfx-one/src/server/services/committee.service.ts index b3a96526d..cce808bcc 100644 --- a/apps/lfx-one/src/server/services/committee.service.ts +++ b/apps/lfx-one/src/server/services/committee.service.ts @@ -146,8 +146,10 @@ export class CommitteeService { * Updates an existing committee using ETag for concurrency control */ public async updateCommittee(req: Request, committeeId: string, data: CommitteeUpdateData): Promise { - // Extract settings fields from core committee data - const { business_email_required, is_audit_enabled, show_meeting_attendees, member_visibility, ...committeeData } = data; + // Extract settings and channel fields from core committee data + // mailing_list/chat_channel are stripped because upstream PUT does not accept them + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { business_email_required, is_audit_enabled, show_meeting_attendees, member_visibility, mailing_list, chat_channel, ...committeeData } = data; const hasSettingsUpdate = business_email_required !== undefined || is_audit_enabled !== undefined || show_meeting_attendees !== undefined || member_visibility !== undefined; @@ -383,7 +385,7 @@ export class CommitteeService { const allowedParams = ['page_size', 'page_token', 'order_by', 'committee_uid']; const sanitizedQuery: Record = {}; for (const key of allowedParams) { - if (query[key]) sanitizedQuery[key] = String(query[key]); + if (query[key] !== undefined) sanitizedQuery[key] = String(query[key]); } const params = { @@ -408,9 +410,12 @@ export class CommitteeService { }); return result; - } catch { + } catch (error) { + // Reset cached promise so a transient import failure doesn't stick forever + this.meetingServicePromise = undefined; logger.warning(req, 'get_committee_meetings', 'Failed to fetch committee meetings, returning empty', { committee_uid: committeeId, + error: error instanceof Error ? error.message : 'Unknown error', }); return { data: [], page_token: undefined }; } diff --git a/apps/lfx-one/src/server/services/survey.service.ts b/apps/lfx-one/src/server/services/survey.service.ts index c9b459fab..1c38c41d4 100644 --- a/apps/lfx-one/src/server/services/survey.service.ts +++ b/apps/lfx-one/src/server/services/survey.service.ts @@ -101,7 +101,7 @@ export class SurveyService { const allowedParams = ['page_size', 'page_token', 'order_by', 'status']; const sanitizedQuery: Record = {}; for (const key of allowedParams) { - if (query[key]) sanitizedQuery[key] = String(query[key]); + if (query[key] !== undefined) sanitizedQuery[key] = String(query[key]); } // Use tags parameter for server-side filtering — committee_uid is not a supported query param