1. Overview

The reporting system in Nimbly Web API offers a robust solution for generating, managing, and retrieving various types of reports. It provides endpoints for compiled reports, missed reports, and report recaps with different time frames (daily, weekly, monthly). The system features authentication middleware, validation, and integration with various services like PubSub for asynchronous processing.

2. API Endpoints

EndpointMethodDescriptionValidatorController Method
/compiled-reports/countPOSTCount compiled reports matching criteriabulkReportCountreportController.getCompiledReportsCount
/v2/compiled-reports/countPOSTCount compiled reports (v2) matching criteriabulkReportCountreportController.getCompiledReportsCountV2
/compiled-reportsPOSTGenerate compiled reportsbulkReportCreatereportController.getCompiledReports
/v2/compiled-reportsPOSTGenerate compiled reports (v2)bulkReportCreatereportController.getCompiledReportsV2
/daily-report-recapPOSTGenerate daily report recap-reportController.getDailyReportsRecap
/weekly-report-recapPOSTGenerate weekly report recap-reportController.getWeeklyReportsRecap
/monthly-report-recapPOSTGenerate monthly report recap-reportController.getMonthlyReportsRecap
/missed-reportPOSTCount/generate missed reportsbulkReportCountmissedReportController.getMissedReport
/v2/missed-reportPOSTCount/generate missed reports (v2)bulkReportCountmissedReportController.getMissedReportV2
/daily-missed-reportPOSTGenerate daily missed report-missedReportController.getDailyMissedReportV2
/weekly-missed-reportPOSTGenerate weekly missed report-missedReportController.getWeeklyMissedReportV2
/monthly-missed-reportPOSTGenerate monthly missed report-missedReportController.getMonthlyMissedReportV2

3. Libraries and Dependencies

The report functionality relies on several key libraries and dependencies:

3.1. Core Dependencies:

  • Express: Web framework for handling HTTP requests and routing (Router, RequestHandler)
  • jsonwebtoken: For JWT-based authentication
  • @nimbly-technologies/nimbly-backend-utils: Provides utilities like middleware handlers and validators
  • @nimbly-technologies/nimbly-common: Common types and utilities shared across Nimbly services
  • @hapi/joi: Schema validation library (used in report validators)
  • Mongoose: MongoDB ODM for database operations
  • moment/moment-timezone: Date manipulation and formatting with timezone support
  • lodash: Utility library for data manipulation
  • fs-extra: Enhanced file system operations
  • uuid: For generating unique identifiers

3.2. Infrastructure:

  • Google Cloud PubSub: For asynchronous processing of report generation requests
  • Firebase Admin: For authentication and database operations
  • MongoDB: Primary data store

3.3. Repositories and Services:

  • ReportRepository: For CRUD operations on reports
  • FileStorageRepository: For managing file storage (likely using Google Cloud Storage)
  • BulkDownloadRequestV2Repository: For tracking and managing download requests
  • Various domain-specific repositories: For accessing related data like sites, users, departments

4. Architecture and Flow

The reporting system follows a clean architecture pattern with clear separation of concerns:

4.1 Layered Architecture

graph TD
    A[Routes] --> B[Controllers]
    B --> C[Usecases]
    C --> D[Repositories]
    D --> E[Data Sources]
    
    F[Request Validators] --> A
    G[Middleware] --> A
  1. Routes Layer: Defines API endpoints and connects them to controllers
  2. Controllers Layer: Handles requests, delegates to use cases, and formats responses
  3. Use Cases Layer: Contains business logic and orchestrates repository calls
  4. Repositories Layer: Abstracts data access and manipulation
  5. Data Sources Layer: Actual storage mechanisms (MongoDB, Firebase, Cloud Storage)

4.2 Authentication Flow

sequenceDiagram
    participant Client
    participant Router
    participant AuthMiddleware
    participant Controller
    participant Usecase
    
    Client->>Router: HTTP Request with JWT token
    Router->>AuthMiddleware: Pass request to authMiddleware.expressHandler()
    AuthMiddleware->>AuthMiddleware: Validate JWT token using process.env.JWT_SECRET
    AuthMiddleware->>Controller: Request with authenticated user context
    Controller->>Usecase: Execute business logic (e.g., reportController.getCompiledReports)
    Usecase->>Controller: Return result (e.g., reportUsecase.bulkDownloadHandler)
    Controller->>Client: HTTP Response with data or error

4.3 Report Generation Flow

sequenceDiagram
    participant Client
    participant ReportController
    participant ReportUsecase
    participant Repositories
    participant PubSub
    
    Client->>ReportController: Request report generation (POST /compiled-reports)
    ReportController->>ReportController: getCompiledReports({ context, payload })
    ReportController->>ReportUsecase: Validate and process request
    ReportUsecase->>Repositories: Fetch required data (from reportRepository, siteRepository, etc.)
    ReportUsecase->>PubSub: Publish report generation job (reportEmitter.emit)
    PubSub-->>Client: Job accepted notification (return response(jobData, null))
    
    Note over PubSub,Client: Asynchronous processing
    
    PubSub->>ReportUsecase: Trigger report generation (bulkDownloadHandler)
    ReportUsecase->>ReportUsecase: buildFile() processes data and generates reports
    ReportUsecase->>Repositories: Generate and store report (fileStorageRepository.uploadFile)
    ReportUsecase-->>Client: Notification of completed report (email or notification)

4.4 PubSub Integration Flow

sequenceDiagram
    participant PubSub
    participant API
    participant InjectContentMiddleware
    participant Controller
    participant ReportUsecase
    
    PubSub->>API: POST with encoded payload
    API->>InjectContentMiddleware: Process PubSub message (injectContent middleware)
    InjectContentMiddleware->>InjectContentMiddleware: Decode base64 payload (Buffer.from().toString())
    InjectContentMiddleware->>InjectContentMiddleware: Extract authorization and job data (data.authorization, data.payload)
    InjectContentMiddleware->>InjectContentMiddleware: Set req.headers.authorization and req.body.jobData
    InjectContentMiddleware->>Controller: Forward request with injected data (next())
    Controller->>ReportUsecase: Execute report generation (reportUsecase.bulkDownloadHandler)
    ReportUsecase->>PubSub: Report completion status (via notification system)

5. Route Implementation

The core of the reporting functionality is defined in src/routes/report.ts. This file sets up all the API endpoints related to reports.

5.1 Route Setup

// Authentication middleware setup
const authMiddleware = new middlewares.AuthMiddleware(process.env.JWT_SECRET!, jsonwebtoken);
 
// Validator setup
const reportValidator = new Validator(ReportValidator);
 
// Router instance
const router = Router();

5.2 PubSub Content Injection

A special middleware called injectContent is defined to handle PubSub messages:

const injectContent: RequestHandler = (req, res, next) => {
    if (req.query.token !== pubSubToken) {
        res.sendStatus(401);
        return;
    }
    const decoded = Buffer.from(req.body.message.data, 'base64').toString('utf8');
    const data: BulkDownloadReportRequestPubSubPayload = JSON.parse(decoded);
 
    if (!data.authorization) {
        res.sendStatus(401);
        return;
    }
 
    if (!data.payload?.id) {
        res.status(400).send('Invalid job data');
        return;
    }
 
    req.headers.authorization = data.authorization;
    req.body.jobData = data.payload;
 
    next();
};

This middleware:

  1. Validates the PubSub token
  2. Decodes the base64-encoded message
  3. Extracts the authorization token and payload
  4. Injects them into the request for downstream processing

5.3 Route Definitions

The router defines several endpoints for different report operations:

router
    .use(authMiddleware.expressHandler())
    .post(
        '/compiled-reports/count',
        middlewares.validatorHandler(reportValidator, 'bulkReportCount'),
        middlewares.expressMiddlewareHandler(reportController.getCompiledReportsCount),
    )
    .post(
        '/v2/compiled-reports/count',
        middlewares.validatorHandler(reportValidator, 'bulkReportCount'),
        middlewares.expressMiddlewareHandler(reportController.getCompiledReportsCountV2),
    )
    .post(
        '/compiled-reports',
        middlewares.validatorHandler(reportValidator, 'bulkReportCreate'),
        middlewares.expressMiddlewareHandler(reportController.getCompiledReports),
    )
    .post(
        '/v2/compiled-reports',
        middlewares.validatorHandler(reportValidator, 'bulkReportCreate'),
        middlewares.expressMiddlewareHandler(reportController.getCompiledReportsV2),
    )
    .post('/daily-report-recap', middlewares.expressMiddlewareHandler(reportController.getDailyReportsRecap))
    .post('/weekly-report-recap', middlewares.expressMiddlewareHandler(reportController.getWeeklyReportsRecap))
    .post('/monthly-report-recap', middlewares.expressMiddlewareHandler(reportController.getMonthlyReportsRecap))
    .post(
        '/missed-report',
        middlewares.validatorHandler(reportValidator, 'bulkReportCount'),
        middlewares.expressMiddlewareHandler(missedReportController.getMissedReport),
    )
    .post(
        '/v2/missed-report',
        middlewares.validatorHandler(reportValidator, 'bulkReportCount'),
        middlewares.expressMiddlewareHandler(missedReportController.getMissedReportV2),
    )
    .post('/daily-missed-report', middlewares.expressMiddlewareHandler(missedReportController.getDailyMissedReportV2))
    .post('/weekly-missed-report', middlewares.expressMiddlewareHandler(missedReportController.getWeeklyMissedReportV2))
    .post(
        '/monthly-missed-report',
        middlewares.expressMiddlewareHandler(missedReportController.getMonthlyMissedReportV2),
    );

Each route:

  1. Uses the authentication middleware
  2. May employ a validator for request payload validation
  3. Maps to a specific controller method

6. Controller Implementation

The controllers handle request processing and delegate to the appropriate use cases.

6.1 Report Controller

The ReportController class provides methods for handling various report operations:

export default class ReportController {
    public constructor(
        private readonly reportUsecase: IReportUsecase,
        private readonly reportEmitter: IReportSubscriber,
        private readonly downloadUsecase: IBulkDownloadRequestV2Usecase,
    ) {}
 
    // Methods for handling report requests
    public async getCompiledReports({ context, payload }: GetCompiledReportsParam): Promise<FunctionReturn> {...}
    public async getCompiledReportsV2({ context, payload }: GetCompiledReportsParam): Promise<FunctionReturn> {...}
    public async getCompiledReportsCount({ context, payload }: GetCompiledReportsParam): Promise<FunctionReturn> {...}
    public async getCompiledReportsCountV2({ context, payload }: GetCompiledReportsParam): Promise<FunctionReturn> {...}
    // ... other methods
}

6.1.1 Compiled Reports Implementation

public async getCompiledReports({ context, payload }: GetCompiledReportsParam): Promise<FunctionReturn> {
    const ctx = ContextUtil.withTimeout(context, 30 * 1000);
    const jobData = payload.data;
 
    try {
        if (!jobData.reportsCount) throw new ErrorCode(errors.EMPTY, 'No reports found');
        const maxLimit = jobData.isWithAttachments
            ? MAX_REPORT_COUNT_WITH_ATTACHMENTS
            : MAX_REPORT_COUNT_WOUT_ATTACHMENTS;
        if (jobData.reportsCount > maxLimit) {
            throw new ErrorCode(errors.EXCEED, `Number of reports exceeds the ${maxLimit} limit`);
        }
 
        this.reportEmitter.emit(reportEvents.getCompiledReports, ctx, jobData);
        return response(jobData, null);
    } catch (error) {
        return response(error.message, error.code);
    }
}

The V2 version uses a different approach for queuing the report request:

public async getCompiledReportsV2({ context, payload }: GetCompiledReportsParam): Promise<FunctionReturn> {
    const jobData = payload.data;
 
    try {
        if (!jobData.reportsCount) throw new ErrorCode(errors.EMPTY, 'No reports found');
        const maxLimit = jobData.isWithAttachments
            ? MAX_REPORT_COUNT_WITH_ATTACHMENTS
            : MAX_REPORT_COUNT_WOUT_ATTACHMENTS;
        if (jobData.reportsCount > maxLimit) {
            throw new ErrorCode(errors.EXCEED, `Number of reports exceeds the ${maxLimit} limit`);
        }
 
        jobData.type = 'report-completed';
        const result = await this.downloadUsecase.queueDownloadRequest(context, jobData);
        return response(result, null);
    } catch (error) {
        log.error(`[ERROR] - Completed Report Queue Error: ${error.message}`);
        return response(error.message, error.code);
    }
}

6.2 Missed Report Controller

The MissedReportController handles operations related to missed reports:

export default class MissedReportController {
    public constructor(
        private readonly reportEmitter: IReportSubscriber,
        private readonly reportUsecase: IReportUsecase,
        private readonly bulkDownloadRequestUsecase: BulkDownloadRequestV2Usecase,
    ) {}
 
    // Methods for handling missed report requests
    public async getMissedReport({ context, payload }: GetMissedReportsParam): Promise<FunctionReturn> {...}
    public async getMissedReportV2({ context, payload }: GetMissedReportsParam): Promise<FunctionReturn> {...}
    public async getDailyMissedReportV2({ context, payload }: GetNotifyMissedReportsParam): Promise<FunctionReturn> {...}
    public async getWeeklyMissedReportV2({ context, payload }: GetNotifyMissedReportsParam): Promise<FunctionReturn> {...}
    public async getMonthlyMissedReportV2({ context, payload }: GetNotifyMissedReportsParam): Promise<FunctionReturn> {...}
}

6.2.1 Missed Report Implementation

public async getMissedReport({ context, payload }: GetMissedReportsParam): Promise<FunctionReturn> {
    try {
        if (payload.data.type === 'file') {
            if (!payload.data?.reportsCount || payload.data?.reportsCount > MAX_REPORT_COUNT_WOUT_ATTACHMENTS) {
                throw new ErrorCode(
                    errors.EXCEED,
                    `Number of reports exceeds the ${MAX_REPORT_COUNT_WOUT_ATTACHMENTS} limit`,
                );
            }
 
            this.reportEmitter.getCompiledMissedReports({
                context,
                query: {
                    ...payload.data,
                },
            });
            return { message: 'Success' };
        } else {
            const result = await this.reportUsecase.getMissedReport({
                context,
                query: payload.data,
            });
            return { data: result.data, message: 'Success' };
        }
    } catch (error) {
        return response(error.message, error.code);
    }
}

The V2 version uses the bulk download request infrastructure:

public async getMissedReportV2({ context, payload }: GetMissedReportsParam): Promise<FunctionReturn> {
    try {
        const { data } = payload;
        if (data.type === 'file') {
            const downloadRequest: BulkDownloadRequestV2 = new BulkDownloadRequestV2({
                ...data,
                requestedTimestamp: new Date().toISOString(),
                type: 'report-missing',
            });
            log.info(`[INFO] Pushing Missed Report Generation to queue`);
            const result = await this.bulkDownloadRequestUsecase.queueDownloadRequest(context, downloadRequest);
            return {
                data: result,
                message: 'Success',
            };
        } else {
            const result = await this.reportUsecase.getMissedReport({
                context,
                query: { ...payload.data, type: 'countV2' },
            });
            return { data: result.data, message: 'Success' };
        }
    } catch (error) {
        log.error(`[ERROR] - Missed Report: ${error.message}`);
        return response(error.message, error.code);
    }
}

7. Use Case Implementation

The use cases contain the core business logic for report operations.

7.1 Report Use Case

The ReportUsecase class implements the IReportUsecase interface and provides methods for various report operations:

export class ReportUsecase implements IReportUsecase {
    // Constructor with dependencies
    public constructor(
        private readonly reportRepository: IReportRepository,
        private readonly siteRepository: ISiteRepository,
        private readonly departmentRepository: IDepartmentRepository,
        private readonly departmentIndexRepository: IDepartmentIndexRepository,
        private readonly departmentGroupRepository: IDepartmentGroupRepository,
        private readonly userRepository: IUserRepository,
        private readonly organizationRepository: IOrganizationRepository,
        private readonly questionnaireIndexRepository: IQuestionnaireIndexRepository,
        private readonly bulkDownloadRequestV2Repository: IBulkDownloadRequestV2Repository,
        private readonly fileStorageRepository: FileStorageRepository,
        private readonly statisticRepository: StatisticRepository,
        private readonly notificationSettingsRepository: INotificationSettingsRepository,
        private readonly pubsubNotificationRepository: NotificationPubsubRepository,
        private readonly unsubscribeNotificationSettings: IUnsubscribeNotificationSettings,
        private readonly statisticUsecase: StatisticUsecase,
        private readonly nimblyAccess: NimblyAccess,
        private readonly bulkDownloadAttachmentRepository: BulkDownloadAttachmentRepository,
    ) {}
 
    // Methods for report operations
    public async bulkDownloadHandler(context: ContextWithTimeout<UserAuth>, jobData: BulkDownloadRequestV2): Promise<void> {...}
    public async bulkDownloadHandlerCounter(context: ContextWithTimeout<UserAuth>, jobData: BulkDownloadRequestV2): Promise<number> {...}
    public async bulkDownloadHandlerCounterV2(context: ContextWithTimeout<UserAuth>, jobData: BulkDownloadRequestV2): Promise<any> {...}
    public async getReportsRecap(context: Context<UserAuth>, jobData: ReportRecapQuery, recapType: string): Promise<void> {...}
    public async getMissedReport({ context, query }: GetMissedReportParam): Promise<UsecaseReturn> {...}
    // ... other methods
}

7.2 Report Generation Logic

The report generation process involves several steps:

  1. Data Retrieval: Fetch reports, sites, departments, users, and other required data
  2. Data Processing: Filter, group, and transform the data
  3. File Generation: Create Excel and/or PDF files
  4. File Storage: Store the generated files in cloud storage
  5. Notification: Notify the user when the report is ready
// Example of report generation logic
const buildFile = async (): Promise<void> => {
    // ... data retrieval and processing
    
    const param = {
        reports,
        sitesMap,
        isConsolidated: jobData.isConsolidated,
        usersMap,
        departmentsMap,
        reportFormat: jobData.reportFormat,
        mainFolderPathXLS: mainFolderXLSX,
        mainFolderPathPDF: mainFolderPDF,
        attachmentsDownloadURL,
        QVRFeatureFlag,
        language: lang,
        startDate: q.startAt,
        endDate: q.endAt,
        questionnaireIDWithTitle,
        timezone,
        useScheduleActivePeriod,
        imageValidationResult: jobData.imageValidationResults,
    };
    
    await generateCompletedReportBasic(param);
}

7.3 Missed Report Generation

For missed reports, the use case retrieves schedules that should have had reports but don’t:

const getMissedReportFile = async ({ context, dependencies, query }: GetMissedReportParams): Promise<UsecaseReturn> => {
    // ... data retrieval and processing
    
    const missedReports: MissedReportRow[] = missedSchedules.map((sched) => {
        const site = siteMap[sched.siteID];
        const supervisor = site?.supervisors?.find((s) => s.departmentID === sched.departmentID); // supervisor
        
        const row: MissedReportRow = {
            Questionnaire: sched.questionnaireIndexes.length
                ? sched.questionnaireIndexes.map((qID) => questionnairesMap[qID]?.title).join('; ')
                : '',
            'Schedule Date and Time': sched.date
                ? moment(sched.date).add('minutes', sched.startTime)?.format('YYYY-MM-DD HH:mm')
                : '',
            Site: site?.name || 'Unknown Site',
            'Site Timezone': site?.timezone,
            'Assigned Department': departmentMap[sched.departmentID]?.name,
            'Assigned User': usersMap[sched.scheduledTo?.[0]]?.displayName,
            Supervisor: supervisorsMap[supervisor?.userID || '']?.displayName || '', // Supervisor field
            'Report Status': 'Missed Report',
        };
        return row;
    });
    
    // ... file generation and storage
}

8. Validation

The validation for report requests is defined in ReportValidator:

const yyyymmddRegex = /([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))/;
 
const ErrInvalidDate = new ErrorCode(errors.INVALID, 'invalid-date');
 
const ReportValidator: JoiSchema = {
  bulkReportCount: Joi.object({
    body: Joi.object({
      departments: Joi.array().allow(''),
      departmentGroups: Joi.array().allow(''),
      sites: Joi.array().allow(''),
      users: Joi.array().allow(''),
      startDate: Joi.string().regex(yyyymmddRegex).required(),
      endDate: Joi.string().regex(yyyymmddRegex).required(),
    }),
  }),
  bulkReportCreate: Joi.object({
    body: Joi.object({
      dJobID: Joi.string().required(),
      departments: Joi.array().allow(''),
      departmentGroups: Joi.array().allow(''),
      sites: Joi.array().allow(''),
      users: Joi.array().allow(''),
      startDate: Joi.string().regex(yyyymmddRegex).required(),
      endDate: Joi.string().regex(yyyymmddRegex).required(),
    }),
  }),
  compileReport: Joi.object({
    query: Joi.object({
      startAt: Joi.string().required().isoDate().error(ErrInvalidDate),
      endAt: Joi.string().required().isoDate().error(ErrInvalidDate),
      downloadID: Joi.string().required().min(1),
    }),
  }),
};

The validator is used in the router definitions to validate request payloads:

middlewares.validatorHandler(reportValidator, 'bulkReportCount')

9. Models and Interfaces

Several models and interfaces are used in the report system:

9.1 Report Models

interface Report {
  departments: any;
  siteOwnerID: string;
  team: Map<string, string>; // {'uid': 'leader' or 'auditor'}
  scheduleID: string;
  siteID: string;
  auditorID: string;
  questionnaireID: string;
  sections: any;
  signatures: Array<{
    name: string;
    position: string;
    path: string;
    isSigned: boolean;
  }>;
  emailTargets: Array<{
    email: string;
    enabled: boolean;
    setByAdmin: boolean;
    status: string;
  }>;
  checkInAt: Date;
  checkOutAt: Date;
  submittedAt: Date;
  scheduledAt: Date;
  dueAt: Date;
  hasDeadline: boolean;
  startTime: Date;
  endTime: Date;
  questions: Array<any>;
  status: string;
  isMakeUp: boolean;
  makeUpReason: string;
  isAdhoc: boolean;
  isGenerated: boolean;
  isIssueCreated: boolean;
  isInventoryAdjusted: boolean;
  organizationID: string;
  reportID: string;
  downloadURL: string;
}

9.2 Request/Response Models

interface BulkDownloadRequestV2 {
  departments: string[];
  departmentGroups: string[];
  sites: string[];
  users: string[];
  startDate: string;
  endDate: string;
  type: 'file' | 'count';
  dJobID: string;
  createdAt: string;
  totalItem: number;
  reportFormat: enums.ReportType;
  reportsCount?: number;
  issuesCount?: number;
  isWithAttachments?: boolean;
  isConsolidated?: boolean;
}
 
interface MissedReportQueryV2 {
  dJobID: string;
  departments: string[];
  sites: string[];
  auditors: string[];
  reportFormat: string;
  reportsCount: number;
  departmentGroups: string[];
  startDate: string;
  endDate: string;
  type: string;
  isConsolidated: boolean;
}
 
interface BulkDownloadReportRequestPubSubPayload {
  authorization: string;
  payload: IBulkDownloadReportRequest;
}

10. Domain Objects and Repository Setup

The application uses a dependency injection pattern for setting up controllers and use cases:

// Build repositories
const reportRepository = new ReportRepository(nimblyMongoDB, tracer);
const siteRepo = new SiteMongoRepository(nimblyMongoDB, tracer);
// ... other repositories
 
// Build use cases
const reportUsecase = new ReportUsecase(
    reportRepository,
    siteRepo,
    deptRepo,
    deptIndexRepo,
    deptGroupRepo,
    userRepo,
    organizationRepository,
    questionnaireIndexRepository,
    bulkDownloadRequestV2Repo,
    fileStorageRepository,
    statisticRepository,
    notificationSettingsRepository,
    pubsubNotificationRepository,
    unsubscribeNotificationSettings,
    statisticUsecase,
    databaseNimblyAccess,
    bulkDownloadAttachmentRepo,
);
 
// Build controllers
const reportController = new ReportController(reportUsecase, reportSubsciber, bulkDownloadRequestV2Usecase);
const missedReportController = new MissedReportController(reportSubsciber, reportUsecase, bulkDownloadRequestV2Usecase);
 
// Export domain objects
export default {
    reportController,
    missedReportController,
    // ... other domain objects
};

11. Error Handling

The system uses a standardized error handling approach:

// Error code definition
const ErrInvalidDate = new ErrorCode(errors.INVALID, 'invalid-date');
 
// Error handling in controller
try {
    // ... operation
} catch (error) {
    log.error(`[ERROR] - Error message: ${error.message}`);
    return response(error.message, error.code);
}
 
// Controller response helper
function response(data: any, error: any) {
    if (error) {
        return {
            error: {
                code: error.code || 'unknown',
                message: error.message || error,
            },
        };
    }
    return { data };
}

12. PubSub Integration

The system uses Google Cloud PubSub for asynchronous processing of report generation tasks:

// PubSub setup
const pubsubClient = new PubSub({ projectId: process.env.PROJECT_ID });
const pubsubNotificationRepository = new NotificationPubsubRepository(pubsubClient, 'notificationpubsub');
 
// PubSub message handling
const injectContent: RequestHandler = (req, res, next) => {
    if (req.query.token !== pubSubToken) {
        res.sendStatus(401);
        return;
    }
    const decoded = Buffer.from(req.body.message.data, 'base64').toString('utf8');
    const data: BulkDownloadReportRequestPubSubPayload = JSON.parse(decoded);
    
    // ... process message
    
    next();
};

13. Summary

The reporting system in Nimbly Web API provides a comprehensive solution for generating and retrieving various types of reports. Key aspects include:

  1. Clean Architecture: Clear separation of concerns with routes, controllers, use cases, and repositories
  2. Authentication: JWT-based authentication for securing report endpoints
  3. Validation: Request payload validation using Joi schemas
  4. Asynchronous Processing: PubSub integration for handling long-running report generation tasks
  5. Multiple Report Types: Support for compiled reports, missed reports, and report recaps
  6. Multiple Output Formats: Support for Excel and PDF output formats
  7. File Storage: Integration with cloud storage for storing generated reports
  8. Notification: User notification when reports are ready

The system is designed for extensibility, with versioned endpoints (v2) allowing for improvements without breaking backward compatibility.