From 6b732e2aaa08d14eea408bfc5bbfc1d740597c1e Mon Sep 17 00:00:00 2001 From: Anand Chowdhary Date: Thu, 22 Oct 2020 19:05:18 +0530 Subject: [PATCH] :sparkles: Add support for cursor --- src/helpers/parse-object-literal.ts | 76 +++++++++++++++++++++++++++++ src/modules/user/user.controller.ts | 2 + src/pipes/cursor.pipe.ts | 14 +++--- 3 files changed, 86 insertions(+), 6 deletions(-) create mode 100644 src/helpers/parse-object-literal.ts diff --git a/src/helpers/parse-object-literal.ts b/src/helpers/parse-object-literal.ts new file mode 100644 index 000000000..687b8f46c --- /dev/null +++ b/src/helpers/parse-object-literal.ts @@ -0,0 +1,76 @@ +/** + * Parse a string like "a: 1, b: 2" to { a: 1, b: 2 } + * @param objectLiteralString - String to parse + * @source https://github.com/mbest/js-object-literal-parse + */ +export const parseObjectLiteral = ( + objectLiteralString: string, +): [string, string | undefined][] => { + const stringDouble = '"(?:[^"\\\\]|\\\\.)*"'; + const stringSingle = "'(?:[^'\\\\]|\\\\.)*'"; + const stringRegexp = '/(?:[^/\\\\]|\\\\.)*/w*'; + const specials = ',"\'{}()/:[\\]'; + const everyThingElse = '[^\\s:,/][^' + specials + ']*[^\\s' + specials + ']'; + const oneNotSpace = '[^\\s]'; + const token = RegExp( + stringDouble + + '|' + + stringSingle + + '|' + + stringRegexp + + '|' + + everyThingElse + + '|' + + oneNotSpace, + 'g', + ); + const divisionLookBehind = /[\])"'A-Za-z0-9_$]+$/; + const keywordRegexLookBehind = { in: 1, return: 1, typeof: 1 }; + let str = objectLiteralString.trim(); + if (str.charCodeAt(0) === 123) str = str.slice(1, -1); + const result: [string, string | undefined][] = []; + let toks = str.match(token); + let key: string | undefined = undefined; + let values = []; + let depth = 0; + if (toks) { + toks.push(','); + for (let i = 0, tok: string; (tok = toks[i]); ++i) { + const c = tok.charCodeAt(0); + if (c === 44) { + if (depth <= 0) { + if (!key && values.length === 1) { + key = values.pop(); + } + result.push([key, values.length ? values.join('') : undefined]); + key = undefined; + values = []; + depth = 0; + continue; + } + } else if (c === 58) { + if (!depth && !key && values.length === 1) { + key = values.pop(); + continue; + } + } else if (c === 47 && i && tok.length > 1) { + const match = toks[i - 1].match(divisionLookBehind); + if (match && !keywordRegexLookBehind[match[0]]) { + str = str.substr(str.indexOf(tok) + 1); + toks = str.match(token); + toks.push(','); + i = -1; + tok = '/'; + } + } else if (c === 40 || c === 123 || c === 91) { + ++depth; + } else if (c === 41 || c === 125 || c === 93) { + --depth; + } else if (!key && !values.length && (c === 34 || c === 39)) { + tok = tok.slice(1, -1); + } + values.push(tok); + } + } + return result; +}; diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index 40ddf4016..f9fab78a9 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -1,6 +1,7 @@ import { Controller, Get, Param, ParseIntPipe, Query } from '@nestjs/common'; import { users } from '@prisma/client'; import { OmitSecrets } from 'src/modules/prisma/prisma.interface'; +import { CursorPipe } from 'src/pipes/cursor.pipe'; import { OptionalIntPipe } from 'src/pipes/optional-int.pipe'; import { OrderByPipe } from 'src/pipes/order-by.pipe'; import { UsersService } from './user.service'; @@ -13,6 +14,7 @@ export class UserController { async getAll( @Query('skip', OptionalIntPipe) skip?: number, @Query('take', OptionalIntPipe) take?: number, + @Query('cursor', CursorPipe) cursor?: Record, @Query('orderBy', OrderByPipe) orderBy?: Record, ): Promise[]> { return this.usersService.users({ skip, take, orderBy }); diff --git a/src/pipes/cursor.pipe.ts b/src/pipes/cursor.pipe.ts index 75414b93b..b6264e5d7 100644 --- a/src/pipes/cursor.pipe.ts +++ b/src/pipes/cursor.pipe.ts @@ -5,8 +5,9 @@ import { HttpStatus, ArgumentMetadata, } from '@nestjs/common'; +import { parseObjectLiteral } from 'src/helpers/parse-object-literal'; -/** Convert a string like "id 12, name Anand" to { id: 12, name: "Anand" } */ +/** Convert a string like "id: 12, b: 'Anand'" to { id: 12, name: "Anand" } */ @Injectable() export class CursorPipe implements PipeTransform { transform( @@ -15,13 +16,14 @@ export class CursorPipe implements PipeTransform { ): Record | undefined { if (value == null) return undefined; try { - const rules = value.split(' ').map(val => val.trim()); - const cursor: Record = {}; + const rules = parseObjectLiteral(value); + const items: Record = {}; rules.forEach(rule => { - const [key, val] = rule.split(' ', 1); - rules[key] = val; + const num = Number(rule[1]); + if (!isNaN(num)) items[rule[0]] = num; + else items[rule[0]] = rule[1]; }); - return cursor; + return items; } catch (_) { throw new HttpException( `"${metadata.data}" should be like "id 12, name Anand", provided "${value}"`,