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

refactor: migrate errors server controller to typescript #327

Closed
wants to merge 6 commits into from
Closed
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
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@
"@types/ip": "^1.1.0",
"@types/jest": "^26.0.9",
"@types/json-stringify-safe": "^5.0.0",
"@types/mongodb": "^3.5.27",
"@types/mongodb-uri": "^0.9.0",
"@types/mongoose": "^5.7.36",
"@types/node": "^14.0.13",
Expand Down
32 changes: 18 additions & 14 deletions ...p/controllers/errors.server.controller.js → ...p/controllers/errors.server.controller.ts
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,39 +1,41 @@
'use strict'
import _ from 'lodash'
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: prefer explicit destructure of used imports:

// If only one is being used.
import isEmpty from 'lodash/isEmpty'
// If multiple lodash functions are being used in this file
import { isEmpty, somethingElse } from 'lodash'

Copy link
Contributor

Choose a reason for hiding this comment

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

Please update all functions that do not need the this parameter (which is all of the functions in this file) into arrow functions.

See further reading for differences between arrow functions and regular functions

Copy link
Contributor

Choose a reason for hiding this comment

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

Should also move and rename this file to the utility function it is, probably into utils/handleMongoError.ts

Read more about utility vs service functions/classes here, and read more about the differences between service and controllers layers (this is more Java-like but the point still stands) to see why the functions in this file does not belong in the controller layer. Do hit me (or Google) up for a more detailed explanation if you still have any queries


const _ = require('lodash')
import { IMongoError } from 'src/types/error'

/**
* Default error message if no more specific error
* @type {String}
*/
exports.defaultErrorMessage = 'An unexpected error happened. Please try again.'
export const defaultErrorMessage =
Copy link
Contributor

Choose a reason for hiding this comment

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

At first glance, I thought this is only used in one place, but seems to also be used in tests. Add a comment stating this is also exported for tests? Though I don't actually think it's necessary, we can also just hardcode this string in the tests.

If you still want to export it, constants should be named in UPPER_SNAKE_CASE for consistency.

Suggested change
export const defaultErrorMessage =
export const DEFAULT_ERROR_MSG =

'An unexpected error happened. Please try again.'

/**
* Private helper for getMongoErrorMessage to return Mongo error in a String
* @param {Object} err - MongoDB error object
* @param {IMongoError} err - MongoDB error object
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Remove types from JSDocs, it's already typed in the function signature

* @return {String} errorString - Formatted error string
*/
const mongoDuplicateKeyError = function (err) {

const mongoDuplicateKeyError = function (err: IMongoError): string {
Copy link
Contributor

Choose a reason for hiding this comment

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

Not a fan of this function, I think it is too hackish instead of fixing the base issue.

In fact, in the entire schema, there is only one instance of a schema with a unique parameter, i.e UserModel, and that case is already handled by it's own post-validation hook. Meaning we do not even need this function and this function can be deleted

Copy link
Contributor

Choose a reason for hiding this comment

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

We can also keep it in if you want, but I'm not a fan of this weird error message interpolation.

let errorString = ''
try {
let fieldName = err.err.substring(
if (err.err) {
const fieldName = err.err.substring(
err.err.lastIndexOf('.$') + 2,
err.err.lastIndexOf('_1'),
)
errorString =
fieldName.charAt(0).toUpperCase() + fieldName.slice(1) + ' already exists'
} catch (ex) {
} else {
errorString = 'Unique field already exists'
}
return errorString
}

/**
* Gets Mongo error object and returns formatted String for frontend
* @param {Objrct} err MongoDB error object
* @param {IMongoError} err? MongoDB error object
* @return {String} message - Error message returned to frontend
*/
exports.getMongoErrorMessage = function (err) {
export function getMongoErrorMessage(err?: IMongoError | string): string {
Copy link
Contributor

Choose a reason for hiding this comment

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

This whole function seems like it needs a good refactor, maybe we should try to clean up the function?

I don't think we should create the IMongoError interface when we already have the typings of the possible errors:

import { MongoError } from 'mongodb'
import { Error as MongooseError } from 'mongoose'

...
export const getMongoErrorMessage = (
  err?: MongoError | MongooseError | string,
): string => { ... }

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested refactor:

export const getMongoErrorMessage = (
  err?: MongoError | MongooseError | string,
): string => {
  if (!err) {
    return ''
  }

  // Handle base Mongo engine errors.
  if (err instanceof MongoError) {
    switch (err.code) {
      case 10334: // BSONObj size invalid error
        return 'Your form is too large to be supported by the system.'
      default:
        return defaultErrorMessage
    }
  }

  // Handle mongoose errors.
  if (err instanceof MongooseError.ValidationError) {
    // Join all error messages into a single message if available.
    const joinedMessage = Object.values(err.errors)
      .map((err) => err.message)
      .join(', ')

    return joinedMessage ?? err.message ?? defaultErrorMessage
  }

  if (err instanceof MongooseError) {
    return err.message ?? defaultErrorMessage
  }

  return err ?? defaultErrorMessage
}

let message = ''
if (!err) {
return ''
Expand All @@ -50,17 +52,19 @@ exports.getMongoErrorMessage = function (err) {
message = 'Your form is too large to be supported by the system.'
break
default:
message = exports.defaultErrorMessage
message = defaultErrorMessage
}
} else if (!_.isEmpty(err.errors)) {
// Prefer specific error messages to a generic one
let errMsgs = []
for (let subError in err.errors) {
const errMsgs = []
for (const subError in err.errors) {
errMsgs.push(err.errors[subError].message)
}
message = errMsgs.join(', ')
} else {
message = err.message
if (err.message) {
message = err.message
}
}
return message
}
11 changes: 11 additions & 0 deletions src/types/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface IMongoError {
Copy link
Contributor

Choose a reason for hiding this comment

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

Unneeded, as explained above

name?: string
message?: string
code?: number
errmsg?: string
err?: string
errors?: {
[subError: string]: { message: string }
}
[propName: string]: any
}
Original file line number Diff line number Diff line change
@@ -1,65 +1,62 @@
describe('Errors Controller', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Remember to update tests

const controller = spec(
'dist/backend/app/controllers/errors.server.controller',
)
import {
defaultErrorMessage,
getMongoErrorMessage,
} from 'src/app/controllers/errors.server.controller'

describe('Errors Controller', () => {
describe('getMongoErrorMessage', () => {
it('should return blank string if no error', () => {
expect(controller.getMongoErrorMessage()).toEqual('')
expect(getMongoErrorMessage()).toEqual('')
})

it('should return string if error is string', () => {
let err = 'something failed'
expect(controller.getMongoErrorMessage(err)).toEqual(err)
const err = 'something failed'
expect(getMongoErrorMessage(err)).toEqual(err)
})

it('should call mongoDuplicateKeyError helperif error code is 11000 or 11001 (with field name)', () => {
let err = {
const err = {
err:
'E11000 duplicate key error index: test.so.$foo_1 dup key: { : 5.0 }',
code: 11001,
n: 0,
nPrev: 1,
ok: 1,
}
let expected = 'Foo already exists'
expect(controller.getMongoErrorMessage(err)).toEqual(expected)
const expected = 'Foo already exists'
expect(getMongoErrorMessage(err)).toEqual(expected)
})

it('should call mongoDuplicateKeyError helper if error code is 11000 or 11001 (no error msg)', () => {
let err = {
const err = {
code: 11000,
n: 0,
nPrev: 1,
ok: 1,
}
let expected = 'Unique field already exists'
expect(controller.getMongoErrorMessage(err)).toEqual(expected)
const expected = 'Unique field already exists'
expect(getMongoErrorMessage(err)).toEqual(expected)
})

it('should return error message for other error code', () => {
let err = {
const err = {
code: 12,
n: 0,
nPrev: 1,
ok: 1,
}
expect(controller.getMongoErrorMessage(err)).toEqual(
controller.defaultErrorMessage,
)
expect(getMongoErrorMessage(err)).toEqual(defaultErrorMessage)
})

it('should return error message if no error code', () => {
let err = {
const err = {
errors: {
error1: {
message: 'error message 1',
},
},
}
expect(controller.getMongoErrorMessage(err)).toEqual(
err.errors.error1.message,
)
expect(getMongoErrorMessage(err)).toEqual(err.errors.error1.message)
})
})
})