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
| Endpoint | Method | Description | Validator | Controller Method |
|---|---|---|---|---|
/compiled-reports/count | POST | Count compiled reports matching criteria | bulkReportCount | reportController.getCompiledReportsCount |
/v2/compiled-reports/count | POST | Count compiled reports (v2) matching criteria | bulkReportCount | reportController.getCompiledReportsCountV2 |
/compiled-reports | POST | Generate compiled reports | bulkReportCreate | reportController.getCompiledReports |
/v2/compiled-reports | POST | Generate compiled reports (v2) | bulkReportCreate | reportController.getCompiledReportsV2 |
/daily-report-recap | POST | Generate daily report recap | - | reportController.getDailyReportsRecap |
/weekly-report-recap | POST | Generate weekly report recap | - | reportController.getWeeklyReportsRecap |
/monthly-report-recap | POST | Generate monthly report recap | - | reportController.getMonthlyReportsRecap |
/missed-report | POST | Count/generate missed reports | bulkReportCount | missedReportController.getMissedReport |
/v2/missed-report | POST | Count/generate missed reports (v2) | bulkReportCount | missedReportController.getMissedReportV2 |
/daily-missed-report | POST | Generate daily missed report | - | missedReportController.getDailyMissedReportV2 |
/weekly-missed-report | POST | Generate weekly missed report | - | missedReportController.getWeeklyMissedReportV2 |
/monthly-missed-report | POST | Generate 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
- Routes Layer: Defines API endpoints and connects them to controllers
- Controllers Layer: Handles requests, delegates to use cases, and formats responses
- Use Cases Layer: Contains business logic and orchestrates repository calls
- Repositories Layer: Abstracts data access and manipulation
- 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:
- Validates the PubSub token
- Decodes the base64-encoded message
- Extracts the authorization token and payload
- 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:
- Uses the authentication middleware
- May employ a validator for request payload validation
- 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:
- Data Retrieval: Fetch reports, sites, departments, users, and other required data
- Data Processing: Filter, group, and transform the data
- File Generation: Create Excel and/or PDF files
- File Storage: Store the generated files in cloud storage
- 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:
- Clean Architecture: Clear separation of concerns with routes, controllers, use cases, and repositories
- Authentication: JWT-based authentication for securing report endpoints
- Validation: Request payload validation using Joi schemas
- Asynchronous Processing: PubSub integration for handling long-running report generation tasks
- Multiple Report Types: Support for compiled reports, missed reports, and report recaps
- Multiple Output Formats: Support for Excel and PDF output formats
- File Storage: Integration with cloud storage for storing generated reports
- Notification: User notification when reports are ready
The system is designed for extensibility, with versioned endpoints (v2) allowing for improvements without breaking backward compatibility.