Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/deploy-dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Deploy Survey Dev

on:
push:
branches:
- dev-release

jobs:
deploy:
name: Deploy to server
runs-on: ubuntu-latest

steps:
- name: SSH and deploy
uses: appleboy/[email protected]
with:
host: ${{ secrets.DEV_SSH_HOST }}
username: ${{ secrets.DEV_SSH_USER }}
key: ${{ secrets.DEV_SSH_PRIVATE_KEY }}
script: |
cd ~/Microservices/survey-dev-configs
./deploy.sh dev-release
5 changes: 5 additions & 0 deletions src/common/dto/pagination.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ export class FiltersDto {
@IsOptional()
@IsString()
status?: string;

@ApiPropertyOptional({ description: 'Filter by context type (e.g. learner, none)' })
@IsOptional()
@IsString()
contextType?: string;
}

export class PaginationDto {
Expand Down
2 changes: 1 addition & 1 deletion src/common/responses/api-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class APIResponse {
ts: new Date().toISOString(),
params,
responseCode: statusCode,
...result,
result,
};
return response.status(statusCode).json(resObj);
} catch (e) {
Expand Down
1 change: 1 addition & 0 deletions src/modules/response/dto/create-response.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';

export class CreateResponseDto {
@ApiProperty({ description: 'Survey ID to respond to' })
@IsOptional()
@IsUUID()
surveyId: string;
Comment on lines +11 to 13
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The surveyId field should be mandatory when creating a response. Marking it as @IsOptional() allows requests with a missing surveyId to pass validation, which will subsequently cause a NotFoundException in the service layer when it fails to fetch the survey. It is better to enforce this requirement at the DTO level to provide a clearer error response (400 Bad Request).

Suggested change
@IsOptional()
@IsUUID()
surveyId: string;
@IsUUID()
surveyId: string;


Expand Down
36 changes: 27 additions & 9 deletions src/modules/response/services/response.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export class ResponseService {
return APIResponse.success(
response,
apiId,
{ data: saved },
saved,
HttpStatus.CREATED,
RESPONSE_MESSAGES.RESPONSE_CREATE_SUCCESS,
);
Expand Down Expand Up @@ -186,7 +186,7 @@ export class ResponseService {
return APIResponse.success(
response,
apiId,
{ data: result },
result,
HttpStatus.OK,
RESPONSE_MESSAGES.RESPONSE_LIST_SUCCESS,
);
Expand Down Expand Up @@ -225,7 +225,7 @@ export class ResponseService {
return APIResponse.success(
response,
apiId,
{ data: surveyResponse },
surveyResponse,
HttpStatus.OK,
RESPONSE_MESSAGES.RESPONSE_READ_SUCCESS,
);
Expand Down Expand Up @@ -322,7 +322,7 @@ export class ResponseService {
return APIResponse.success(
response,
apiId,
{ data: saved },
saved,
HttpStatus.OK,
RESPONSE_MESSAGES.RESPONSE_UPDATE_SUCCESS,
);
Expand Down Expand Up @@ -434,7 +434,7 @@ export class ResponseService {
return APIResponse.success(
response,
apiId,
{ data: saved },
saved,
HttpStatus.OK,
RESPONSE_MESSAGES.RESPONSE_SUBMIT_SUCCESS,
);
Expand Down Expand Up @@ -494,7 +494,7 @@ export class ResponseService {
return APIResponse.success(
response,
apiId,
{ data: result },
result,
HttpStatus.OK,
RESPONSE_MESSAGES.RESPONSE_STATS_SUCCESS,
);
Expand Down Expand Up @@ -532,14 +532,32 @@ export class ResponseService {
return surveyResponse;
}

private isConditionMet(field: any, allFields: any[], responseData: Record<string, any>): boolean {
const logic = field.conditionalLogic;
if (!logic || !logic.depends_on || !logic.show_if) return true;

const parentField = allFields.find(f => f.fieldName === logic.depends_on);
if (!parentField) return true;

const parentValue = responseData[parentField.fieldId];
const showIf: string[] = logic.show_if;

if (Array.isArray(parentValue)) {
return parentValue.some(v => showIf.includes(v));
}
return showIf.includes(parentValue);
}
Comment on lines +535 to +549
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation of isConditionMet performs an O(N) search using allFields.find(). Since this method is called inside a loop in validateRequiredFields, the overall complexity becomes O(N^2). Changing the parameter to a Map will allow for O(1) lookups, which is significantly more efficient for surveys with a large number of fields.

Suggested change
private isConditionMet(field: any, allFields: any[], responseData: Record<string, any>): boolean {
const logic = field.conditionalLogic;
if (!logic || !logic.depends_on || !logic.show_if) return true;
const parentField = allFields.find(f => f.fieldName === logic.depends_on);
if (!parentField) return true;
const parentValue = responseData[parentField.fieldId];
const showIf: string[] = logic.show_if;
if (Array.isArray(parentValue)) {
return parentValue.some(v => showIf.includes(v));
}
return showIf.includes(parentValue);
}
private isConditionMet(field: any, fieldsMap: Map<string, any>, responseData: Record<string, any>): boolean {
const logic = field.conditionalLogic;
if (!logic || !logic.depends_on || !logic.show_if) return true;
const parentField = fieldsMap.get(logic.depends_on);
if (!parentField) return true;
const parentValue = responseData[parentField.fieldId];
const showIf: string[] = logic.show_if;
if (Array.isArray(parentValue)) {
return parentValue.some(v => showIf.includes(v));
}
return showIf.includes(parentValue);
}


private validateRequiredFields(survey: any, surveyResponse: SurveyResponse): void {
const errors: string[] = [];

const allFields = (survey.sections || []).flatMap((s: any) => s.fields || []);

for (const section of survey.sections || []) {
for (const field of section.fields || []) {
if (field.isRequired) {
const value = surveyResponse.responseData[field.fieldId];
const fileIds = surveyResponse.fileUploadIds[field.fieldId];
if (field.isRequired && this.isConditionMet(field, allFields, surveyResponse.responseData ?? {})) {
const value = (surveyResponse.responseData ?? {})[field.fieldId];
const fileIds = (surveyResponse.fileUploadIds ?? {})[field.fieldId];
Comment on lines +554 to +560
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To optimize the validation process and improve code readability, create a Map for field lookups once and extract responseData and fileUploadIds into local variables. This avoids repeated O(N) searches and redundant nullish coalescing operations inside the nested loops.

    const allFields = (survey.sections || []).flatMap((s: any) => s.fields || []);
    const fieldsMap = new Map(allFields.map(f => [f.fieldName, f]));
    const responseData = surveyResponse.responseData ?? {};
    const fileUploadIds = surveyResponse.fileUploadIds ?? {};

    for (const section of survey.sections || []) {
      for (const field of section.fields || []) {
        if (field.isRequired && this.isConditionMet(field, fieldsMap, responseData)) {
          const value = responseData[field.fieldId];
          const fileIds = fileUploadIds[field.fieldId];


const isUploadField = ['image_upload', 'video_upload', 'file_upload'].includes(
field.fieldType,
Expand Down
21 changes: 14 additions & 7 deletions src/modules/survey/services/survey.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export class SurveyService {
return APIResponse.success(
response,
apiId,
{ data: result },
result,
HttpStatus.CREATED,
RESPONSE_MESSAGES.SURVEY_CREATE_SUCCESS,
);
Expand Down Expand Up @@ -186,6 +186,13 @@ export class SurveyService {
});
}

// Filter by contextType if provided
if (filters?.contextType) {
queryBuilder = queryBuilder.andWhere('survey.contextType = :contextType', {
contextType: filters.contextType,
});
}

const [surveys, total] = await queryBuilder
.orderBy(`survey.${sortBy}`, sortOrder)
.skip(pagination.skip)
Expand All @@ -207,7 +214,7 @@ export class SurveyService {
return APIResponse.success(
response,
apiId,
{ data: result },
result,
HttpStatus.OK,
RESPONSE_MESSAGES.SURVEY_LIST_SUCCESS,
);
Expand Down Expand Up @@ -246,7 +253,7 @@ export class SurveyService {
return APIResponse.success(
response,
apiId,
{ data: survey },
survey,
HttpStatus.OK,
RESPONSE_MESSAGES.SURVEY_READ_SUCCESS,
);
Expand Down Expand Up @@ -316,7 +323,7 @@ export class SurveyService {
return APIResponse.success(
response,
apiId,
{ data: result },
result,
HttpStatus.OK,
RESPONSE_MESSAGES.SURVEY_UPDATE_SUCCESS,
);
Expand Down Expand Up @@ -395,7 +402,7 @@ export class SurveyService {
return APIResponse.success(
response,
apiId,
{ data: survey },
survey,
HttpStatus.OK,
RESPONSE_MESSAGES.SURVEY_PUBLISH_SUCCESS,
);
Expand Down Expand Up @@ -453,7 +460,7 @@ export class SurveyService {
return APIResponse.success(
response,
apiId,
{ data: survey },
survey,
HttpStatus.OK,
RESPONSE_MESSAGES.SURVEY_CLOSE_SUCCESS,
);
Expand Down Expand Up @@ -509,7 +516,7 @@ export class SurveyService {
return APIResponse.success(
response,
apiId,
{ data: { surveyId } },
{ surveyId },
HttpStatus.OK,
RESPONSE_MESSAGES.SURVEY_DELETE_SUCCESS,
);
Expand Down