1. Overview

This documentation provides a comprehensive technical analysis of the Question Category and Questionnaire Index API modules within the api-questionnaires service. These modules form critical components of a questionnaire management system, handling categorization of questions and indexing of questionnaires for efficient retrieval and management.

Key Features

  • Question Category Module: Manages categories for organizing questions within questionnaires
  • Questionnaire Index Module: Provides indexing and retrieval mechanisms for questionnaires with support for pagination, filtering, and [access control](../Settings/Access control/AccessControlOverview.md)

Technology Stack

  • Runtime: Node.js with TypeScript
  • Framework: Express.js
  • Database: MongoDB with Mongoose ODM
  • Secondary Database: Firebase Realtime Database & Firestore
  • Authentication: JWT-based authentication
  • Architecture Pattern: Clean Architecture with Repository Pattern

2. Libraries and Dependencies

Core Dependencies

LibraryVersionPurpose
express^4.xWeb framework for Node.js
mongoose^5.xMongoDB object modeling
mongoose-paginate-v2^1.xPagination support for Mongoose
firebase-admin^9.xFirebase SDK for server-side operations
jsonwebtoken^8.xJWT token generation and validation
ramda^0.27.xFunctional programming utilities
normalize-url^6.xURL normalization

Internal Dependencies

PackagePurpose
@nimbly-technologies/nimbly-backend-utilsShared backend utilities and helpers
@nimbly-technologies/nimbly-commonCommon types and interfaces
@nimbly-technologies/entity-nodeEntity definitions and repositories
@nimbly-technologies/nimbly-access[Access control](../Settings/Access control/AccessControlOverview.md) and [permissions](../Settings/Access control/AccessControlOverview.md)

Development Dependencies

LibraryPurpose
typescriptTypeScript compiler
@types/*TypeScript type definitions
eslintCode linting
jestTesting framework
nodemonDevelopment server with hot reload

3. API Endpoints

3.1. Question Category Endpoints

MethodEndpointDescriptionAuthentication
GET/api/question-categories/Search and retrieve question categoriesRequired

3.2. Questionnaire Index Endpoints

MethodEndpointDescriptionAuthentication
GET/api/questionnaire-index/Get all questionnaire indexesRequired
GET/api/questionnaire-index/minifiedGet minified questionnaire indexesRequired
GET/api/questionnaire-index/activeGet active questionnaire indexesRequired
GET/api/questionnaire-index/active/minifiedGet active minified indexesRequired
GET/api/questionnaire-index/active/no-formula/minifiedGet active indexes without formulasRequired
GET/api/questionnaire-index/paginateGet paginated questionnaire indexesRequired
GET/api/questionnaire-index/profile-onlyGet profile-only indexesRequired
GET/api/questionnaire-index/profile-only/minifiedGet minified profile-only indexesRequired
GET/api/questionnaire-index/:questionnaireIndexIDGet specific questionnaire indexRequired
GET/api/questionnaire-index/:questionnaireIndexID/latestGet latest version of questionnaireRequired

3.3. Questionnaire Template Endpoints

MethodEndpointDescriptionAuthentication
GET/api/v2/questionnairetemplates/v2Get all accessible templates with filteringRequired
POST/api/v2/questionnairetemplates/v2/create-templateCreate a new templateRequired
GET/api/v2/questionnairetemplates/v2/:templateIDGet specific template detailsRequired
PATCH/api/v2/questionnairetemplates/v2/update-template/:templateIDUpdate existing templateRequired
DELETE/api/v2/questionnairetemplates/v2/delete-template/:templateIDSoft delete templateRequired
PATCH/api/v2/questionnairetemplates/v2/restore-template/:templateIDRestore archived templateRequired

4. Module Analysis

4.1. Question Category Module

Architecture

graph LR
    subgraph "Question Category Module"
        QCR[questionCategory.router.ts]
        QCC[QuestionCategoryController]
        QCU[QuestionCategoryUsecase]
        QCRepo[QuestionCategoryRepository]
        QCS[QuestionCategorySchema]
    end
    
    QCR --> QCC
    QCC --> QCU
    QCU --> QCRepo
    QCRepo --> QCS

Component Details

Router (questionCategory.router.ts)

The router configures the HTTP endpoints for question category operations:

// Key implementation details
const router = Router();
const authMiddleware = new middlewares.AuthMiddleware(process.env.JWT_SECRET, jsonwebtoken);
const find = middlewares.expressMiddlewareHandler(questionCategoryController.find);
router.use(authMiddleware.expressHandler().bind(authMiddleware)).get('/', find);

Responsibilities:

  • Route definition and HTTP method mapping
  • Authentication middleware application
  • Request handler wrapping for error handling
Controller (QuestionCategoryController)

The controller manages request/response transformation:

public async find({ context, payload }: FindParam) {
    const result = await this.questionCategoryUsecase.find(context, payload.query.search);
    return response(result.data, null);
}

Key Features:

  • Parameter extraction from request
  • Use case invocation
  • Response formatting with standard structure
Use Case (QuestionCategoryUsecase)

Implements the business logic for category retrieval:

public async find(
    ctx: Context<UserAuth>,
    search: string,
): Promise<UsecaseReturn<Array<{ _id: string; label: string; value: string; status: string }>>> {
    const query: MongoQuery<QuestionCategory> = {
        organizationID: ctx.user.organizationID,
    };
    if (search) {
        query.label = new RegExp(search, 'gi');
    }
    const questionCategories = await this.questionCategoryRepository.find(query);
    // Deduplication logic
    const uniqueCategories = Array.from(
        questionCategories.reduce((map, val) => {
            const sanitizedLabel = val.label.replace(/[\r\n]+/g, '');
            const sanitizedTag = val.tag ? val.tag.replace(/[\r\n]+/g, ''): '';
            const key = `${sanitizedLabel}-${sanitizedTag}`;
            if (!map.has(key)) {
                map.set(key, val);
            }
            return map;
        }, new Map<string, typeof questionCategories[0]>())
        .values(),
    );
    return {
        data: uniqueCategories.map((val) => ({
            label: val.label,
            value: val.label,
            _id: val._id,
            status: val.status,
        })),
    };
}

Business Logic:

  1. Filters by organization ID for data isolation
  2. Implements case-insensitive search using regex
  3. Removes duplicate categories based on label and tag combination
  4. Sanitizes labels by removing line breaks
  5. Returns formatted response with specific fields
Repository (QuestionCategoryRepository)

Handles database operations:

export default class QuestionCategoryRepository implements IQuestionCategoryRepository {
    private model: Model<QuestionCategory & Document>;
 
    public constructor(private conn: Connection) {
        this.model = conn.model('QuestionCategory', QuestionCategorySchema) as any;
    }
 
    public async find(query: MongoQuery<QuestionCategory>) {
        const builder = this.model.find(query).sort({ label: 1 });
        return builder;
    }
 
    public async findPaging(query: MongoQuery<QuestionCategory>, options: PaginationOptions) {
        const offset = (options.page > 0 ? options.page - 1 : 0) * options.limit;
        return this.model.find(query).sort({ label: 1 }).limit(options.limit).skip(offset);
    }
 
    public async upsert(
        questionCategory: QuestionCategory & { updatedAt: Date },
    ): Promise<QuestionCategory & Document> {
        const newCategory = await this.model.findOneAndUpdate(
            { label: questionCategory.label, organizationID: questionCategory.organizationID },
            { $set: questionCategory },
            { upsert: true, returnOriginal: false },
        );
        return newCategory;
    }
 
    public async createMany(categories: Array<QuestionCategory>): Promise<void> {
        await this.model.insertMany(categories, { rawResult: true });
    }
}

Features:

  • Mongoose model initialization
  • Sorted queries (alphabetical by label)
  • Pagination support with offset calculation
  • Upsert functionality for create/update operations
  • Bulk insert capability
Schema (QuestionCategorySchema)

Defines the data structure:

const questionCategorySchema: MongoSchema<QuestionCategory> = {
    organizationID: String,
    label: String,
    tag: String,
    status: String,
};
 
const QuestionCategorySchema = new Schema(questionCategorySchema, { timestamps: true });

Schema Fields:

  • organizationID: Organization identifier for multi-tenancy
  • label: Display name of the category
  • tag: Additional categorization tag
  • status: Category status (active/inactive)
  • Automatic createdAt and updatedAt timestamps

4.2. Questionnaire Index Module

Architecture

graph TB
    subgraph "Questionnaire Index Module"
        QIR[questionnaireIndex.router.ts]
        QIC[QuestionnaireIndexController]
        QIU[QuestionnaireIndexUsecase]
        
        subgraph "Repositories"
            QIRepo[QuestionnaireIndexRepository]
            QRepo[QuestionnaireRepository]
            DRepo[DepartmentRepository]
            FRepo[FormulaRepository]
            RRepo[RoleRepository]
        end
        
        subgraph "External Services"
            FB[Firebase]
            RD[Redis]
        end
    end
    
    QIR --> QIC
    QIC --> QIU
    QIU --> QIRepo
    QIU --> QRepo
    QIU --> DRepo
    QIU --> FRepo
    QIU --> RRepo
    QIRepo --> FB
    DRepo --> RD

Component Details

Router (questionnaireIndex.router.ts)

Complex routing configuration with multiple endpoints:

const router = Router();
const authMiddleware = new middlewares.AuthMiddleware(process.env.JWT_SECRET, jsonwebtoken);
 
// Multiple endpoint handlers
const findAllActiveByOrganizationID = middlewares.expressMiddlewareHandler(
    questionnaireIndexController.findAllActiveByOrganizationID,
);
// ... other handlers
 
router
    .use(authMiddleware.expressHandler().bind(authMiddleware))
    .get('/active', findAllActiveByOrganizationID)
    .get('/active/minified', findAllActiveByOrganizationIDMinified)
    .get('/active/no-formula/minified', findActiveOrganizationQuestionnaireWoutFormula)
    .get('/paginate', findAllByorganizationIDPaginate)
    .get('/profile-only', findAllProfileOnly)
    .get('/profile-only/minified', findAllProfileOnlyMinified)
    .get('/minified', findAllByorganizationIDMinified)
    .get('/:questionnaireIndexID/latest', findLatestByID)
    .get('/:questionnaireIndexID', findByID)
    .get('/', findAllByorganizationID);

Route Organization:

  • Base routes for general queries
  • Specialized routes for filtered data
  • Parameter-based routes for specific resources
  • Consistent authentication across all endpoints
Controller (QuestionnaireIndexController)

Manages multiple endpoint handlers with varied functionality:

export default class QuestionnaireIndexController {
    public constructor(private questionnaireIndexUsecase: QuestionnaireIndexUsecase) {
        // Bind all methods to maintain context
        this.findByID = this.findByID.bind(this);
        this.findLatestByID = this.findLatestByID.bind(this);
        // ... other bindings
    }
 
    public async findByID({ context, payload }: FindByIDParam): Promise<FunctionReturn> {
        try {
            const questionnaireIndex = await this.questionnaireIndexUsecase.findByID({
                context,
                questionnaireIndexID: payload.params.questionnaireIndexID,
            });
            return response(questionnaireIndex, null);
        } catch (error) {
            return response(null, error);
        }
    }
 
    public async findAllByorganizationIDPaginate({
        context,
        payload,
    }: FindAllByOrganizationIDPaginateParam): Promise<FunctionReturn> {
        try {
            const questionnaireIndexes = await this.questionnaireIndexUsecase.findAllByOrganizationIDPaginate({
                context,
                paginationOptions: { ...payload.query },
                queryOptions: { ...payload.query },
                withDisabled: payload.query.with_disabled === 'true' ? true : false,
            });
            return response(questionnaireIndexes, null);
        } catch (error) {
            return response(null, error);
        }
    }
}

Key Patterns:

  • Consistent error handling with try-catch blocks
  • Parameter transformation and validation
  • Response standardization
  • Method binding for proper context
Use Case (QuestionnaireIndexUsecase)

Complex business logic implementation with multiple data sources:

export default class QuestionnaireIndexUsecase {
    public constructor(
        private questionnaireIndexRepository: IQuestionnaireIndexRepository,
        private questionnaireRepo: QuestionnaireRepository,
        private deptRepo: IDepartmentRepository,
        private formulaRepo: IFormulaRepository,
        private roleRepo: DatabaseAccess,
    ) {}
 
    public async findAllActiveByOrganizationID(context: Context<UserAuth>, props: string[]) {
        let departmentIndexes = await this.deptRepo.findDepartmentIndex(context);
 
        if (departmentIndexes?.length === 0) {
            return null;
        }
 
        const allowedQuestionnaireIndexID = R.flatten(
            departmentIndexes.map((x) => (x.questionnaires || []).map((y) => y.id)),
        );
 
        const questionnaireIndexQuery: MongoQuery<QuestionnaireIndex> = {
            organizationID: context.user.organizationID,
            questionnaireIndexID: { $in: allowedQuestionnaireIndexID },
            disabled: false,
        };
 
        const questionnaireIndices = await this.questionnaireIndexRepository.find(questionnaireIndexQuery, props);
 
        return questionnaireIndices;
    }
 
    public async findAllByOrganizationID(context: Context<UserAuth>, withDisabled = false, props: string[]) {
        const query: MongoQuery<QuestionnaireIndex> = {
            organizationID: context.user.organizationID,
        };
 
        if (!withDisabled) {
            query.disabled = false;
        }
 
        const { userID, organizationID } = context.user;
 
        const role = await this.roleRepo.getRoleForUser(userID, organizationID);
        context.user.role = role?.origin || context.user.role;
 
        const resourceMap = role.resources?.length
            ? role.resources.reduce((acc, curr) => {
                  acc[curr['resource']] = curr;
                  return acc;
              }, {})
            : {};
 
        const dataVisibility = getDataVisibility(resourceMap, context.user.role);
 
        switch (dataVisibility) {
            case DataVisibility.SPECIFIC_DEPT:
            case DataVisibility.SPECIFIC_SITE:
            case DataVisibility.SPECIFIC_DEPT_SITE: {
                const departmentIndexes = await this.deptRepo.findDepartmentIndex(context);
 
                const allowedQuestionnaireIndexID = R.flatten(
                    departmentIndexes.map((x) => (x.questionnaires || []).map((y) => y.id)),
                );
                query.questionnaireIndexID = { $in: allowedQuestionnaireIndexID };
                break;
            }
            default:
                break;
        }
        const questionnaireIndex = await this.questionnaireIndexRepository.find(query, props);
        return questionnaireIndex;
    }
}

Business Logic Features:

  1. Access Control Implementation:

    • Department-based access filtering
    • Role-based data visibility
    • Dynamic permission checking
  2. Data Aggregation:

    • Combines data from multiple repositories
    • Flattens nested structures using Ramda
    • Applies complex filtering logic
  3. Performance Optimization:

    • Property selection for minimal data transfer
    • Early returns for empty results
    • Efficient query building
Pagination Implementation

The pagination logic in findAllByOrganizationIDPaginate.ts:

export const findAllByOrganizationIDPaginate = async (
    context: Context<UserAuth>,
    repositories: {
        questionnaireIndexRepository: IQuestionnaireIndexRepository;
        departmentRepository: IDepartmentRepository;
        questionnairesRepo: QuestionnaireRepository;
    },
    q: FindAllByOrganizationIDPaginateQueryParam,
    withDepartmentAccess = false,
) => {
    const query: MongoQuery<QuestionnaireIndex> = {
        organizationID: context.user.organizationID,
    };
 
    if (
        withDepartmentAccess &&
        context.user.role !== userRoles.ACCOUNT_HOLDER &&
        context.user.role !== userRoles.SUPERADMIN
    ) {
        const departmentIndexes = await repositories.departmentRepository.findDepartmentIndex(context);
 
        const deptQuestionnaireIndexID = flatten(
            departmentIndexes.map((x) => (x.questionnaires || []).map((y) => y.id)),
        );
        query.questionnaireIndexID = { $in: deptQuestionnaireIndexID };
    }
 
    query.disabled = false;
 
    ifElse(
        equals(true),
        () => {
            query.disabled = true;
        },
        T,
    )(q.withDisabled);
 
    ifElse(
        equals(true),
        () => {
            query.title = new RegExp(q.queryOptions.search, 'gi');
        },
        T,
    )(!!q.queryOptions.search);
 
    return repositories.questionnaireIndexRepository.findPaginate(
        query,
        parsePaginationOptions(q.paginationOptions),
        parseSort(q.queryOptions, 'questionnaireIndexID'),
    );
};

Pagination Features:

  • Dynamic query building based on user permissions
  • Search functionality with regex
  • Functional programming approach using Ramda
  • Configurable sorting and filtering
Additional Response Fields Utility

The additionalResponseFields function in utils.ts:

export const additionalResponseFields = (questionnaire) => {
    if (!questionnaire.hasOwnProperty('passingCriteria')) {
        questionnaire.passingCriteria = null;
    }
    if (!questionnaire.hasOwnProperty('passingType')) {
        questionnaire.passingType = null;
    }
    if (!questionnaire.hasOwnProperty('passingValue')) {
        questionnaire.passingValue = null;
    }
    if (!questionnaire.hasOwnProperty('deductionToggle')) {
        questionnaire.deductionToggle = false;
    }
    if (!questionnaire.hasOwnProperty('maxScore')) {
        questionnaire.maxScore = 100;
    }
 
    if (questionnaire.questions) {
        for (const question of questionnaire.questions) {
            if (!question.hasOwnProperty('toggle')) {
                question.toggle = [];
            }
            if (!question.hasOwnProperty('categoryWeights')) {
                question.categoryWeights = null;
            }
            if (!question.hasOwnProperty('negativeScoreToggle')) {
                question.negativeScoreToggle = false;
            }
        }
    }
 
    return questionnaire;
};

Purpose: Ensures backward compatibility by adding default values for newer fields

4.3. Questionnaire Template Module

Architecture

graph TB
    subgraph "Questionnaire Template Module"
        QTR[questionnaireTemplate.router.ts]
        QTC[QuestionnaireTemplateController]
        QTU[QuestionnaireTemplateUsecase]
        QTRepo[QuestionnaireTemplateRepository]
        QTS[QuestionnaireTemplateSchema]
        
        subgraph "External Dependencies"
            CloudStorage[Cloud Storage]
            QRepo[QuestionnaireRepository]
        end
    end
    
    QTR --> QTC
    QTC --> QTU
    QTU --> QTRepo
    QTU --> CloudStorage
    QTU --> QRepo
    QTRepo --> QTS

Component Details

Schema (QuestionnaireTemplateSchema)
const questionnaireTemplateSchema = {
    templateID: { type: String, unique: true, required: true },
    templateIndexID: String,  // Future: versioning support
    organizationID: String,   // null for default templates
    type: {
        type: String,
        enum: ['default', 'custom'],
        required: true
    },
    title: { type: String, required: true },
    description: String,
    questions: [{
        // Same structure as questionnaire questions
        content: String,
        type: String,
        category: String,
        answerRequired: Boolean,
        // ... all question fields
    }],
    visibility: {
        type: String,
        enum: ['organization', 'public'],
        default: 'organization'
    },
    status: {
        type: String,
        enum: ['draft', 'published', 'archived'],
        default: 'draft'
    },
    createdBy: String,
    modifiedBy: String,
    modifiedByName: String,  // Display name for UI
    version: String
};
Use Case (QuestionnaireTemplateUsecase)

Key business logic implementations:

public async getAllTemplates(ctx: Context<UserAuth>, queryParams?: TemplateQueryParams) {
    const { organizationID } = ctx.user;
    
    // Build query for organization templates + default templates
    const query = {
        $or: [
            { organizationID, type: 'custom' },
            { type: 'default', status: 'published' }
        ]
    };
    
    // Apply filters if provided
    if (queryParams?.search) {
        query.$and = [{
            $or: [
                { title: new RegExp(queryParams.search, 'i') },
                { description: new RegExp(queryParams.search, 'i') }
            ]
        }];
    }
    
    if (queryParams?.type) {
        const types = queryParams.type.split(',');
        query.type = { $in: types };
    }
    
    if (queryParams?.status) {
        const statuses = queryParams.status.split(',');
        query.status = { $in: statuses };
    }
    
    // Handle pagination
    if (queryParams?.page || queryParams?.limit) {
        return this.repository.findPaginated(query, {
            page: queryParams.page || 1,
            limit: queryParams.limit || 10,
            sort: { [queryParams.sortBy || 'createdAt']: queryParams.sortOrder || -1 }
        });
    }
    
    // Return array for backward compatibility
    return this.repository.find(query);
}
 
public async createTemplate(ctx: Context<UserAuth>, data: CreateTemplateDto) {
    const template = {
        templateID: uuidv4(),
        organizationID: ctx.user.organizationID,
        type: data.type || 'custom',
        title: data.title,
        description: data.description,
        questions: data.questions,
        status: data.publish ? 'published' : 'draft',
        visibility: data.type === 'default' ? 'public' : 'organization',
        createdBy: ctx.user.userID,
        modifiedBy: ctx.user.userID
    };
    
    // Validate template
    if (!template.questions || template.questions.length === 0) {
        throw new Error('Template must have at least one question');
    }
    
    return this.repository.create(template);
}
 
public async updateTemplate(ctx: Context<UserAuth>, templateID: string, updates: UpdateTemplateDto) {
    const template = await this.repository.findById(templateID);
    
    // Authorization check
    if (template.type === 'custom' && template.organizationID !== ctx.user.organizationID) {
        throw new Error('Unauthorized access to template');
    }
    
    // Prevent updates to archived templates
    if (template.status === 'archived') {
        throw new Error('Cannot update archived template');
    }
    
    // Handle publish flag
    if (updates.publish && template.status === 'draft') {
        updates.status = 'published';
    }
    
    updates.modifiedBy = ctx.user.userID;
    updates.modifiedByName = ctx.user.displayName;
    
    return this.repository.update(templateID, updates);
}
Controller (QuestionnaireTemplateController)
public async getAllTemplates({ context, payload }: GetAllTemplatesParam) {
    try {
        const queryParams = payload.query;
        const result = await this.usecase.getAllTemplates(context, queryParams);
        
        // Add updatedBy display name for UI
        if (Array.isArray(result)) {
            result.forEach(template => {
                template.updatedBy = template.modifiedByName || 'Unknown';
            });
        } else if (result.data) {
            result.data.forEach(template => {
                template.updatedBy = template.modifiedByName || 'Unknown';
            });
        }
        
        return response(result, null);
    } catch (error) {
        return response(null, error);
    }
}

API Request/Response Examples

Get Templates with Filtering
GET /api/v2/questionnairetemplates/v2?search=safety&type=custom&status=published&page=1&limit=20
 
Response:
{
  "data": {
    "data": [
      {
        "templateID": "550e8400-e29b-41d4-a716-446655440000",
        "title": "Safety Inspection Template",
        "type": "custom",
        "status": "published",
        "description": "Template for daily safety inspections",
        "updatedBy": "John Smith",
        "createdAt": "2024-01-15T10:30:00Z"
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 20,
      "total": 45,
      "totalPages": 3
    }
  },
  "error": null,
  "type": "json"
}
Create Template
POST /api/v2/questionnairetemplates/v2/create-template
{
  "title": "Equipment Inspection",
  "description": "Standard equipment check",
  "publish": true,
  "questions": [
    {
      "content": "Is equipment operational?",
      "type": "binary",
      "category": "Equipment",
      "answerRequired": true
    }
  ]
}

Business Rules

  1. Template Types:

    • default: Created by Nimbly, available to all orgs
    • custom: Organization-specific templates
  2. Access Control:

    • Users see their org’s templates + published default templates
    • Only custom templates can be edited/deleted by org users
    • Default templates are read-only
  3. Status Workflow:

    draft → published → archived
      ↑                    ↓
      ←─── restore ────────
    
  4. Validation:

    • Title is required
    • At least one question required
    • Cannot update archived templates
    • Questions follow same validation as regular questionnaires

Note: For complete template API documentation including all endpoints, request/response examples, and implementation details, refer to @api-questionnaires/QUESTIONNAIRE_TEMPLATE_API.md in the api-questionnaires repository.


5. Data Flow Architecture

5.1. Question Category Search Flow

sequenceDiagram
    participant Client
    participant AuthMiddleware
    participant Controller
    participant UseCase
    participant Repository
    participant MongoDB
    
    Client->>AuthMiddleware: GET /api/question-categories/?search=safety
    AuthMiddleware->>AuthMiddleware: Verify JWT token
    AuthMiddleware->>Controller: Authenticated request with user context
    Controller->>UseCase: find(context, "safety")
    UseCase->>UseCase: Build query with organizationID
    UseCase->>UseCase: Add search regex if provided
    UseCase->>Repository: find(query)
    Repository->>MongoDB: db.questioncategories.find({...}).sort({label: 1})
    MongoDB-->>Repository: Category documents
    Repository-->>UseCase: Raw categories array
    UseCase->>UseCase: Deduplicate by label+tag
    UseCase->>UseCase: Format response data
    UseCase-->>Controller: UsecaseReturn with formatted data
    Controller->>Controller: Wrap in standard response
    Controller-->>Client: HTTP 200 with categories

5.2. Questionnaire Index Retrieval Flow (with Access Control)

sequenceDiagram
    participant Client
    participant AuthMiddleware
    participant Controller
    participant UseCase
    participant RoleRepo
    participant DeptRepo
    participant QIRepo
    participant Redis
    
    Client->>AuthMiddleware: GET /api/questionnaire-index/
    AuthMiddleware->>Controller: Authenticated request
    Controller->>UseCase: findAllByOrganizationID(context, withDisabled, props)
    UseCase->>RoleRepo: getRoleForUser(userID, organizationID)
    RoleRepo-->>UseCase: User role and permissions
    UseCase->>UseCase: Calculate data visibility
    
    alt Restricted Access
        UseCase->>DeptRepo: findDepartmentIndex(context)
        DeptRepo->>Redis: Check cache
        Redis-->>DeptRepo: Cached data or null
        DeptRepo-->>UseCase: Department indexes
        UseCase->>UseCase: Extract allowed questionnaire IDs
    end
    
    UseCase->>QIRepo: find(query, props)
    QIRepo-->>UseCase: Questionnaire indexes
    UseCase-->>Controller: Filtered results
    Controller-->>Client: HTTP 200 with data

5.3. Paginated Query Flow

flowchart TB
    Start([Client Request]) --> Parse[Parse Query Parameters]
    Parse --> BuildQuery[Build Base Query]
    BuildQuery --> CheckRole{Check User Role}
    
    CheckRole -->|Admin| NoFilter[No Department Filter]
    CheckRole -->|Regular User| DeptFilter[Apply Department Filter]
    
    DeptFilter --> GetDepts[Get Department Indexes]
    GetDepts --> FilterQI[Filter Questionnaire IDs]
    
    NoFilter --> ApplyOptions[Apply Query Options]
    FilterQI --> ApplyOptions
    
    ApplyOptions --> Search{Has Search Term?}
    Search -->|Yes| AddRegex[Add Title Regex]
    Search -->|No| Skip1[Continue]
    
    AddRegex --> Disabled{Include Disabled?}
    Skip1 --> Disabled
    
    Disabled -->|Yes| SetDisabled[Set disabled=true]
    Disabled -->|No| SetEnabled[Set disabled=false]
    
    SetDisabled --> Paginate[Execute Paginated Query]
    SetEnabled --> Paginate
    
    Paginate --> Format[Format Response]
    Format --> Return([Return to Client])

6. Implementation Details

6.1. Authentication and Authorization

The authentication system uses JWT tokens with a middleware layer:

const authMiddleware = new middlewares.AuthMiddleware(process.env.JWT_SECRET, jsonwebtoken);

Features:

  1. JWT token validation
  2. User context extraction
  3. Organization-based data isolation
  4. Role-based access control

6.2. Error Handling Strategy

Controllers implement consistent error handling:

try {
    // Business logic
    return response(data, null);
} catch (error) {
    return response(null, error);
}

Response Helper Function:

// helpers/controllerResponse.ts
export default function response(data: any, error: any) {
    return {
        data: data,
        error: error
    };
}

6.3. Database Connection Management

The application uses MongoDB with Mongoose for primary data storage:

// Repository initialization
public constructor(private conn: Connection) {
    this.model = conn.model('QuestionCategory', QuestionCategorySchema) as any;
}

Connection Features:

  • Shared connection instance
  • Model caching
  • Schema versioning with timestamps

6.4. Caching Strategy

Redis is used for caching department indexes:

const deptIndexCache = initRedis<DepartmentIndex[]>({
    host: redisHost,
    port: Number(redisPort),
    password: redisPassword,
    prefix: 'department_index_user:',
});

Cache Benefits:

  • Reduced database load
  • Faster department access checks
  • User-specific cache keys

6.5. Multi-tenancy Implementation

Organization-based data isolation is enforced at multiple levels:

  1. Query Level: All queries include organizationID
  2. Schema Level: Organization ID is a required field
  3. Access Control: Department-based filtering
  4. Cache Keys: Organization-specific prefixes

6.6. Performance Optimizations

  1. Projection Support:

    const questionnaireIndices = await this.questionnaireIndexRepository.find(query, props);

    Allows selecting only required fields

  2. Pagination: Limits result set size for large collections

  3. Indexing: Sorted queries utilize database indexes

  4. Deduplication: In-memory deduplication reduces response size

6.7. Security Considerations

  1. Input Validation:

    • Regex patterns are sanitized
    • Query parameters are typed and validated
  2. Data Isolation:

    • Organization-based filtering
    • Department-based access control
  3. Authentication:

    • JWT token validation
    • Token expiration handling
  4. Authorization:

    • Role-based permissions
    • Resource-level access control

7. Database Schemas

7.1. Question Category Schema

interface QuestionCategory {
    organizationID: string;  // Multi-tenancy identifier
    label: string;          // Display name
    tag?: string;           // Optional categorization
    status: string;         // Active/Inactive status
    createdAt?: Date;       // Auto-generated
    updatedAt?: Date;       // Auto-generated
}

Indexes:

  • Compound index on (organizationID, label)
  • Text index on label for search

7.2. Questionnaire Index Schema

interface QuestionnaireIndex {
    questionnaireIndexID: string;
    organizationID: string;
    title: string;
    disabled: boolean;
    latest: string;  // Reference to latest questionnaire version
    populated?: {
        latest: Questionnaire;
    };
}

Relationships:

  • One-to-many with Questionnaires
  • Many-to-many with Departments
  • One-to-many with Formulas

8. Error Handling

8.1. Error Types

  1. Authentication Errors:

    • Invalid or expired JWT tokens
    • Missing authentication headers
  2. Authorization Errors:

    • Insufficient permissions
    • Department access violations
  3. Validation Errors:

    • Invalid query parameters
    • Schema validation failures
  4. Database Errors:

    • Connection failures
    • Query timeouts
    • Duplicate key violations

8.2. Error Response Format

{
    "data": null,
    "error": {
        "message": "Error description",
        "code": "ERROR_CODE",
        "details": {}
    }
}

8.3. Error Handling Best Practices

  1. Graceful Degradation: Return empty results instead of failing for missing departments

  2. Error Logging: Console logging for debugging in development

  3. User-Friendly Messages: Transform technical errors into actionable messages

  4. Consistent Format: All errors follow the same response structure


9. Conclusion

The Question Category and Questionnaire Index APIs demonstrate a well-architected system following clean architecture principles. The implementation provides:

  1. Robust Architecture: Clear separation of concerns with layered architecture
  2. Scalability: Designed for horizontal scaling with caching strategies
  3. Security: Multi-level security with authentication and authorization
  4. Performance: Optimized queries with pagination and projection support
  5. Maintainability: Clean code structure with TypeScript for type safety

The system effectively handles multi-tenant data isolation while providing flexible query capabilities and maintaining high performance through strategic caching and database optimization.

This documentation serves as a comprehensive guide for developers working with these APIs, providing both high-level architecture understanding and detailed implementation insights.