Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ref: migrate get feedback flow to TypeScript #735

Merged
merged 7 commits into from
Nov 30, 2020
Merged
48 changes: 0 additions & 48 deletions src/app/controllers/admin-forms.server.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
* Module dependencies.
*/
const mongoose = require('mongoose')
const moment = require('moment-timezone')
const _ = require('lodash')
const { StatusCodes } = require('http-status-codes')

Expand All @@ -22,8 +21,6 @@ const {
getEmailFormModel,
} = require('../models/form.server.model')
const getFormModel = require('../models/form.server.model').default
const getFormFeedbackModel = require('../models/form_feedback.server.model')
.default
const getSubmissionModel = require('../models/submission.server.model').default
const { ResponseMode } = require('../../types')

Expand Down Expand Up @@ -417,51 +414,6 @@ function makeModule(connection) {
}
})
},
/**
* Return form feedback matching query
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
getFeedback: function (req, res) {
let FormFeedback = getFormFeedbackModel(connection)
let query = FormFeedback.find({ formId: req.form._id }).sort({
created: 1,
})
query.exec(function (err, feedback) {
if (err) {
return respondOnMongoError(req, res, err)
} else if (!feedback) {
return res
.status(StatusCodes.NOT_FOUND)
.json({ message: 'No feedback found' })
} else {
let sum = 0
let count = 0
feedback = feedback.map(function (element) {
sum += element.rating
count += 1
return {
index: count,
timestamp: moment(element.created).valueOf(),
rating: element.rating,
comment: element.comment,
date: moment(element.created)
.tz('Asia/Singapore')
.format('D MMM YYYY'),
dateShort: moment(element.created)
.tz('Asia/Singapore')
.format('D MMM'),
}
})
let average = count > 0 ? (sum / count).toFixed(2) : undefined
return res.json({
average: average,
count: count,
feedback: feedback,
})
}
})
},
/**
* Submit feedback when previewing forms
* Preview feedback is not stored
Expand Down
123 changes: 123 additions & 0 deletions src/app/modules/feedback/__tests__/feedback.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ObjectId } from 'bson-ext'
import { times } from 'lodash'
import moment from 'moment-timezone'
import mongoose from 'mongoose'

import getFormFeedbackModel from 'src/app/models/form_feedback.server.model'
Expand All @@ -8,6 +9,7 @@ import dbHandler from 'tests/unit/backend/helpers/jest-db'

import { DatabaseError } from '../../core/core.errors'
import * as FeedbackService from '../feedback.service'
import { FeedbackResponse } from '../feedback.types'

const FormFeedback = getFormFeedbackModel(mongoose)

Expand Down Expand Up @@ -103,4 +105,125 @@ describe('feedback.service', () => {
expect(streamSpy).toHaveBeenCalledWith(mockFormId)
})
})

describe('getFormFeedbacks', () => {
it('should return correct feedback responses', async () => {
// Arrange
const expectedCount = 3
const mockFormId = new ObjectId().toHexString()
const expectedFbPromises = times(expectedCount, (count) =>
FormFeedback.create({
formId: mockFormId,
comment: `cool form ${count}`,
rating: 5 - count,
}),
)
// Add another feedback with a different form id.
await FormFeedback.create({
formId: new ObjectId(),
comment: 'boo this form sux',
rating: 1,
})
const expectedCreatedFbs = await Promise.all(expectedFbPromises)
const expectedFeedbackList = expectedCreatedFbs.map((fb, idx) => ({
index: idx + 1,
timestamp: moment(fb.created).valueOf(),
rating: fb.rating,
comment: fb.comment,
date: moment(fb.created).tz('Asia/Singapore').format('D MMM YYYY'),
dateShort: moment(fb.created).tz('Asia/Singapore').format('D MMM'),
}))

// Act
const actualResult = await FeedbackService.getFormFeedbacks(mockFormId)

// Assert
// Should only average from the feedbacks for given formId.
const expectedAverage = (
expectedCreatedFbs.reduce((acc, curr) => acc + curr.rating, 0) /
expectedCount
).toFixed(2)
expect(actualResult.isOk()).toEqual(true)
expect(actualResult._unsafeUnwrap()).toEqual({
average: expectedAverage,
count: expectedCount,
feedback: expectedFeedbackList,
})
})

it('should return feedback response with zero count and empty array when no feedback is available', async () => {
// Arrange
const mockFormId = new ObjectId().toHexString()

// Act
const actualResult = await FeedbackService.getFormFeedbacks(mockFormId)

// Assert
expect(actualResult.isOk()).toEqual(true)
expect(actualResult._unsafeUnwrap()).toEqual({
count: 0,
feedback: [],
})
})

it('should return feedback response with empty string comment if feedback comment is undefined', async () => {
// Arrange
const mockFormId = new ObjectId().toHexString()
const createdFb = await FormFeedback.create({
formId: mockFormId,
// Missing comment key value.
rating: 3,
})

// Act
const actualResult = await FeedbackService.getFormFeedbacks(mockFormId)

// Assert
const expectedResult: FeedbackResponse = {
count: 1,
average: '3.00',
feedback: [
{
index: 1,
timestamp: moment(createdFb.created).valueOf(),
rating: createdFb.rating,
// Empty comment string
comment: '',
date: moment(createdFb.created)
.tz('Asia/Singapore')
.format('D MMM YYYY'),
dateShort: moment(createdFb.created)
.tz('Asia/Singapore')
.format('D MMM'),
},
],
}
expect(actualResult.isOk()).toEqual(true)
expect(actualResult._unsafeUnwrap()).toEqual(expectedResult)
})

it('should return DatabaseError when error occurs whilst querying database', async () => {
// Arrange
const mockFormId = new ObjectId().toHexString()
const sortSpy = jest.fn().mockReturnThis()
const findSpy = jest.spyOn(FormFeedback, 'find').mockImplementationOnce(
() =>
(({
sort: sortSpy,
exec: () => Promise.reject(new Error('boom')),
} as unknown) as mongoose.Query<any>),
)

// Act
const actualResult = await FeedbackService.getFormFeedbacks(mockFormId)

// Assert
expect(findSpy).toHaveBeenCalledWith({
formId: mockFormId,
})
expect(sortSpy).toHaveBeenCalledWith({ created: 1 })
expect(actualResult.isErr()).toEqual(true)
expect(actualResult._unsafeUnwrapErr()).toBeInstanceOf(DatabaseError)
})
})
})
62 changes: 62 additions & 0 deletions src/app/modules/feedback/feedback.service.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isEmpty } from 'lodash'
import moment from 'moment-timezone'
import mongoose from 'mongoose'
import { ResultAsync } from 'neverthrow'

Expand All @@ -7,6 +9,8 @@ import getFormFeedbackModel from '../../models/form_feedback.server.model'
import { getMongoErrorMessage } from '../../utils/handle-mongo-error'
import { DatabaseError } from '../core/core.errors'

import { FeedbackResponse, ProcessedFeedback } from './feedback.types'

const FormFeedbackModel = getFormFeedbackModel(mongoose)
const logger = createLoggerWithLabel(module)

Expand Down Expand Up @@ -47,3 +51,61 @@ export const getFormFeedbackStream = (
): mongoose.QueryCursor<IFormFeedbackSchema> => {
return FormFeedbackModel.getFeedbackCursorByFormId(formId)
}

/**
* Returned processed object containing count of feedback, average rating, and
* list of feedback.
* @param formId the form to retrieve feedback for
* @returns ok(feedback response object) on success
* @returns err(DatabaseError) if database error occurs during query
*/
export const getFormFeedbacks = (
formId: string,
): ResultAsync<FeedbackResponse, DatabaseError> => {
return ResultAsync.fromPromise(
FormFeedbackModel.find({ formId }).sort({ created: 1 }).exec(),
(error) => {
logger.error({
message: 'Error retrieving feedback documents from database',
meta: {
action: 'getFormFeedbacks',
formId,
},
error,
})

return new DatabaseError(getMongoErrorMessage(error))
},
).map((feedbacks) => {
if (isEmpty(feedbacks)) {
return {
count: 0,
feedback: [],
}
}

// Process retrieved feedback.
const totalFeedbackCount = feedbacks.length
let totalRating = 0
const processedFeedback = feedbacks.map((fb, idx) => {
totalRating += fb.rating
const response: ProcessedFeedback = {
// 1-based indexing.
index: idx + 1,
timestamp: moment(fb.created).valueOf(),
rating: fb.rating,
comment: fb.comment ?? '',
date: moment(fb.created).tz('Asia/Singapore').format('D MMM YYYY'),
dateShort: moment(fb.created).tz('Asia/Singapore').format('D MMM'),
}

return response
})
const averageRating = (totalRating / totalFeedbackCount).toFixed(2)
return {
average: averageRating,
count: totalFeedbackCount,
feedback: processedFeedback,
}
})
}
14 changes: 14 additions & 0 deletions src/app/modules/feedback/feedback.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export type ProcessedFeedback = {
index: number
timestamp: number
rating: number
comment: string
date: string
dateShort: string
}

export type FeedbackResponse = {
average?: string
count: number
feedback: ProcessedFeedback[]
}
Loading