Skip to content

Instantly share code, notes, and snippets.

@unicornware
Last active May 13, 2021 14:35
Show Gist options
  • Save unicornware/5414522bba1e348cf5da7638ef2c9008 to your computer and use it in GitHub Desktop.
Save unicornware/5414522bba1e348cf5da7638ef2c9008 to your computer and use it in GitHub Desktop.
NestJS - ParseQueryPipe
import { Module } from '@nestjs/common'
import type { ConfigModuleOptions } from '@nestjs/config'
import { ConfigModule } from '@nestjs/config'
import { APP_FILTER, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'
import configuration from './config/configuration'
import { AllExceptionsFilter } from './lib/filters'
import { PageviewInterceptor } from './lib/interceptors'
import { ParseQueryPipe } from './lib/pipes'
import { UsersModule } from './subdomains'
/**
* @file Root Application Module
* @module app/AppModule
* @see https://docs.nestjs.com/modules
*/
const configModuleOptions: ConfigModuleOptions = {
cache: configuration().PROD,
ignoreEnvFile: true,
isGlobal: true,
load: [configuration]
}
@Module({
imports: [ConfigModule.forRoot(configModuleOptions), UsersModule],
providers: [
{ provide: APP_FILTER, useClass: AllExceptionsFilter },
{ provide: APP_INTERCEPTOR, useClass: PageviewInterceptor },
{ provide: APP_PIPE, useClass: ParseQueryPipe }
]
})
export default class AppModule {}
import { ExceptionStatusCode } from '@flex-development/exceptions/enums'
import Exception from '@flex-development/exceptions/exceptions/base.exception'
import type { Paramtype } from '@nestjs/common'
import merge from 'lodash.merge'
import TestSubject from '../parse-query.pipe'
/**
* @file Unit Tests - ParseQueryPipe
* @module app/lib/pipes/tests/ParseQueryPipe
*/
describe('unit:app/lib/pipes/ParseQueryPipe', () => {
const Subject = new TestSubject()
const metadata = { type: 'query' as Paramtype }
describe('#transform', () => {
it("should return value if metadata.type !== 'query'", () => {
const value = null
expect(Subject.transform(value, { type: 'body' })).toBe(value)
})
it('should throw Exception if value is not plain object', () => {
const value = []
const pattern = `Query parameters must be an object; received ${value}`
const emessage = new RegExp(pattern)
let exception = {} as Exception
try {
Subject.transform(value, metadata)
} catch (error) {
exception = error
}
expect(exception.code).toBe(ExceptionStatusCode.BAD_REQUEST)
expect(exception.errors).toMatchObject({ value })
expect(exception.message).toMatch(emessage)
})
it('should parse key values that are are json strings', () => {
const value = { email: 'true' }
const query = Subject.transform(value, metadata)
expect(typeof query.email === 'boolean').toBeTruthy()
})
it('should remove path parameter', () => {
const value = { path: 'users' }
expect(Subject.transform(value, metadata)).toMatchObject({})
})
it('should handle decorator argument', () => {
const data = 'updated_at'
const value = { [data]: '1620232058179' }
const this_metadata = merge({}, metadata, { data })
const expected = value[this_metadata.data]
expect(Subject.transform(value, this_metadata)).toBe(expected)
})
})
})
import { ExceptionStatusCode } from '@flex-development/exceptions/enums'
import Exception from '@flex-development/exceptions/exceptions/base.exception'
import type { ArgumentMetadata, PipeTransform } from '@nestjs/common'
import { Injectable } from '@nestjs/common'
import type { VercelRequestQuery } from '@vercel/node'
import isPlainObject from 'lodash.isplainobject'
import omit from 'lodash.omit'
import type { PlainObject } from 'simplytyped'
import isJSON from 'validator/lib/isJSON'
/**
* @file Global Pipe - ParseQueryPipe
* @module app/lib/pipes/ParseQueryPipe
* @see https://docs.nestjs.com/pipes#custom-pipes
*/
/**
* Global pipe to parse query parameters.
*
* @class
* @implements {PipeTransform<any, PlainObject | any>}
*/
@Injectable()
export default class ParseQueryPipe
implements PipeTransform<any, PlainObject | any> {
/**
* Uses `JSON.parse` to parse each key of {@param value} if it's a string
* containing JSON data.
*
* If {@param metadata.type} equals `query`, the original value will be
* returned without an attempt to be parsed.
*
* This is useful for methods that use query parameters, but don't take into
* account that queries are `VercelRequestQuery` objects, where all key values
* are actually strings.
*
* Additionally, {@param value.path} will be deleted. With every request to a
* serverless function running on Vercel, the request path is sent as a query
* parameter. When a search is executed, this will cause the search to yield
* zero results.
*
* @param {any} value - Value before it goes to route handler method
* @param {ArgumentMetadata} metadata - Pipe metadata about @param value
* @return {PlainObject} Parsed query, value of `query[metadata.data]`, or
* orignal value if `metadata.type !== 'query'`
* @throws {Exception}
*/
transform(value: any, metadata: ArgumentMetadata): PlainObject | any {
if (metadata.type !== 'query') return value
let query: VercelRequestQuery = {}
if (value && !isPlainObject(value)) {
const message = `Query parameters must be an object; received ${value}`
const data = { errors: { value } }
throw new Exception(ExceptionStatusCode.BAD_REQUEST, message, data)
}
Object.keys((value || {}) as VercelRequestQuery).forEach(key => {
let key_value = value[key]
if (typeof key_value === 'string') {
// @ts-expect-error type definition is wrong
const json = isJSON(key_value, { allow_primitives: true })
key_value = json ? JSON.parse(key_value) : key_value
}
query[key] = key_value
})
query = omit(query, ['path'])
return typeof metadata.data === 'string' ? query[metadata.data] : query
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment