Skip to content

Commit

Permalink
feat(mixins): add MangoParser
Browse files Browse the repository at this point in the history
  • Loading branch information
unicornware committed May 21, 2021
1 parent 87d89c0 commit fae840b
Show file tree
Hide file tree
Showing 9 changed files with 238 additions and 3 deletions.
2 changes: 0 additions & 2 deletions src/enums/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,3 @@ export { BSONTypeAlias } from './bson-type-alias.enum'
export { BSONTypeCode } from './bson-type-code.enum'
export { ProjectRule } from './project-rule.enum'
export { SortOrder } from './sort-order.enum'

/* eslint-disable prettier/prettier */
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@

export * from './enums'
export * from './interfaces'
export * from './mixins'
export * from './types'
1 change: 1 addition & 0 deletions src/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type { CustomQSMongoParser } from './custom-qs-mongo-parser.interface'
export type { MangoCache } from './mango-cache.interface'
export type { MangoOptions } from './mango-options.interface'
export type { MangoParserOptions } from './mango-parser-options.interface'
export type { IMangoParser } from './mango-parser.interface'
export type { ProjectionOperators } from './projection-operators.interface'
export type { QueryCriteriaOptions } from './query-criteria-options.interface'
export type { QueryOperators } from './query-operators.interface'
2 changes: 1 addition & 1 deletion src/interfaces/mango-cache.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { Document } from '@/types'
*/

/**
* `Mango` query client data cache.
* `Mango` client data cache.
*
* @template D - Document (collection object)
*/
Expand Down
23 changes: 23 additions & 0 deletions src/interfaces/mango-parser.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { Document, MangoParsedUrlQuery, MangoSearchParams } from '@/types'
import qsm from 'qs-to-mongo'
import type { ParsedOptions } from 'qs-to-mongo/lib/query/options-to-mongo'
import type { MangoParserOptions } from './mango-parser-options.interface'
import type { QueryCriteriaOptions } from './query-criteria-options.interface'

/**
* @file Interface - IMangoParser
* @module interfaces/IMangoParser
*/

/**
* `MangoParser` mixin interface.
*
* @template D - Document (collection object)
*/
export interface IMangoParser<D extends Document = Document> {
readonly parser: typeof qsm
readonly options: MangoParserOptions

params(query?: MangoParsedUrlQuery<D> | string): MangoSearchParams<D>
queryCriteriaOptions(base?: Partial<ParsedOptions>): QueryCriteriaOptions<D>
}
Empty file removed src/mixins/.gitkeep
Empty file.
88 changes: 88 additions & 0 deletions src/mixins/__tests__/mango-parser.mixin.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { ProjectRule } from '@/enums'
import type { MangoParserOptions } from '@/interfaces'
import { ExceptionStatusCode } from '@flex-development/exceptions/enums'
import Exception from '@flex-development/exceptions/exceptions/base.exception'
import TestSubject from '../mango-parser.mixin'

/**
* @file Unit Tests - MangoParser
* @module mixins/tests/MangoParser
*/

describe('unit:mixins/MangoParser', () => {
describe('constructor', () => {
it('should remove options.objectIdFields', () => {
const options = ({ objectIdFields: [] } as unknown) as MangoParserOptions

const mparser = new TestSubject(options)

// @ts-expect-error testing
expect(mparser.options.objectIdFields).not.toBeDefined()
})

it('should remove options.parameters', () => {
const options = ({ parameters: '' } as unknown) as MangoParserOptions

const mparser = new TestSubject(options)

// @ts-expect-error testing
expect(mparser.options.parameters).not.toBeDefined()
})
})

describe('#params', () => {
const Subject = new TestSubject({ fullTextFields: ['created_at', 'id'] })

const spy_parser = jest.spyOn(Subject, 'parser')

it('should parse url query string', () => {
const querystring = 'fields=created_at&sort=created_at,-id&limit=10'

Subject.params(querystring)

expect(spy_parser).toBeCalledTimes(1)
expect(spy_parser.mock.calls[0][0]).toBe(querystring)
})

it('should parse url query object', () => {
const query = {
fields: 'created_at,-updated_at',
limit: '10',
q: 'foo',
sort: 'created_at,-id'
}

Subject.params(query)

expect(spy_parser).toBeCalledTimes(1)
expect(spy_parser.mock.calls[0][0]).toBe(query)
})

it('should throw Exception if #parser throws', () => {
const query = { q: 'will cause error' }

let exception = {} as Exception

try {
new TestSubject().params(query)
} catch (error) {
exception = error
}

expect(exception.code).toBe(ExceptionStatusCode.BAD_REQUEST)
expect(exception.data).toMatchObject({ parser_options: {}, query })
})
})

describe('#queryCriteriaOptions', () => {
const Subject = new TestSubject()

it('should convert base options into QueryCriteriaOptions object', () => {
const projection = { created_at: ProjectRule.PICK }

const options = Subject.queryCriteriaOptions({ projection })

expect(options.$project).toMatchObject(projection)
})
})
})
6 changes: 6 additions & 0 deletions src/mixins/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* @file Entry Point - Mixins
* @module mixins
*/

export { default as MangoParser } from './mango-parser.mixin'
118 changes: 118 additions & 0 deletions src/mixins/mango-parser.mixin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import type {
CustomQSMongoParser,
IMangoParser,
MangoParserOptions,
QueryCriteriaOptions
} from '@/interfaces'
import type { Document, MangoParsedUrlQuery, MangoSearchParams } from '@/types'
import { ExceptionStatusCode } from '@flex-development/exceptions/enums'
import Exception from '@flex-development/exceptions/exceptions/base.exception'
import type { OneOrMany, PlainObject } from '@flex-development/tutils'
import qsm from 'qs-to-mongo'
import type { ParsedOptions } from 'qs-to-mongo/lib/query/options-to-mongo'

/**
* @file Mixin - MangoParser
* @module mixins/MangoParser
*/

/**
* Converts Mongo URL queries into repository search parameters objects.
*
* @template D - Document (collection object)
*
* @class
* @implements {IMangoParser<D>}
*/
export default class MangoParser<D extends Document = Document>
implements IMangoParser<D> {
/**
* @readonly
* @instance
* @property {typeof qsm} parser - `qs-to-mongo` module
* @see https://github.com/fox1t/qs-to-mongo
*/
readonly parser: typeof qsm = qsm

/**
* @readonly
* @instance
* @property {MangoParserOptions} options - `qs-to-mongo` module options
*/
readonly options: MangoParserOptions

/**
* Creates a new `MangoParser` client.
*
* Converts MongoDB query objects and strings into search parameters objects,
* and formats query criteria objects.
*
* - https://github.com/fox1t/qs-to-mongo
* - https://github.com/kofrasa/mingo
*
* @param {MangoParserOptions} [options] - Parser options
* @param {OneOrMany<string>} [options.dateFields] - Fields that will be
* converted to `Date`; if no fields are passed, any valid date string will be
* converted to an ISO-8601 string
* @param {OneOrMany<string>} [options.fullTextFields] - Fields that will be
* used as criteria when passing `text` query parameter
* @param {OneOrMany<string>} [options.ignoredFields] - Array of query
* parameters that are ignored, in addition to the defaults (`fields`,
* `limit`, `offset`, `omit`, `sort`, `text`)
* @param {number} [options.maxLimit] - Max that can be passed to limit option
* @param {CustomQSMongoParser} [options.parser] - Custom query parser
* @param {any} [options.parserOptions] - Custom query parser options
*/
constructor(options: MangoParserOptions = {}) {
const parser_options = Object.assign({}, options)

Reflect.deleteProperty(parser_options, 'objectIdFields')
Reflect.deleteProperty(parser_options, 'parameters')

this.options = parser_options
}

/**
* Convert URL query objects and strings into a search parameters objects.
*
* @param {MangoParsedUrlQuery<D> | string} [query] - Query object or string
* @return {MangoSearchParams<D>} Mango search parameters object
* @throws {Exception}
*/
params(query: MangoParsedUrlQuery<D> | string = ''): MangoSearchParams<D> {
let build: PlainObject = {}

try {
build = this.parser(query, this.options)
} catch ({ message, stack }) {
const code = ExceptionStatusCode.BAD_REQUEST
const data = { parser_options: this.options, query }

throw new Exception(code, message, data, stack)
}

return {
...build.criteria,
options: this.queryCriteriaOptions(build.options)
} as MangoSearchParams<D>
}

/**
* Converts parsed URL query criteria options into `QueryCriteriaOptions`
* objects.
*
* @param {Partial<ParsedOptions>} [base] - Parsed query criteria options
* @return {QueryCriteriaOptions<D>} Query criteria options
*/
queryCriteriaOptions(
base: Partial<ParsedOptions> = {}
): QueryCriteriaOptions<D> {
const { projection, sort, ...rest } = base

return {
...rest,
$project: projection as QueryCriteriaOptions<D>['$project'],
sort: sort as QueryCriteriaOptions<D>['sort']
}
}
}

0 comments on commit fae840b

Please sign in to comment.