├── packages ├── .gitkeep └── express-query-adapter │ ├── src │ ├── index.ts │ ├── profile │ │ ├── default-config.ts │ │ ├── defaults │ │ │ ├── index.ts │ │ │ ├── enabled.ts │ │ │ └── disabled.ts │ │ ├── index.ts │ │ ├── enableable-option.ts │ │ ├── find-policy.ts │ │ ├── pagination-option.ts │ │ ├── config-profile.ts │ │ ├── profile-options.ts │ │ └── loader.ts │ ├── express-query.ts │ ├── typeorm │ │ ├── query.ts │ │ ├── filter │ │ │ ├── field │ │ │ │ ├── lookup.ts │ │ │ │ ├── lookups │ │ │ │ │ ├── exact.ts │ │ │ │ │ ├── in.ts │ │ │ │ │ ├── is-null.ts │ │ │ │ │ ├── contains.ts │ │ │ │ │ ├── ends-with.ts │ │ │ │ │ ├── gt.ts │ │ │ │ │ ├── lt.ts │ │ │ │ │ ├── starts-with.ts │ │ │ │ │ ├── icontains.ts │ │ │ │ │ ├── iends-with.ts │ │ │ │ │ ├── gte.ts │ │ │ │ │ ├── istarts-with.ts │ │ │ │ │ ├── lte.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── between.ts │ │ │ │ ├── lookup.enum.ts │ │ │ │ ├── field-filter.ts │ │ │ │ └── lookup-builder-factory.ts │ │ │ ├── options │ │ │ │ ├── filter-option.ts │ │ │ │ ├── container.ts │ │ │ │ ├── select-option.ts │ │ │ │ ├── relations-option.ts │ │ │ │ ├── order-option.ts │ │ │ │ └── pagination-option.ts │ │ │ ├── filter.ts │ │ │ └── filter-factory.ts │ │ └── query-builder.ts │ ├── return-type.ts │ ├── query-builder.ts │ ├── express-query-adapter.ts │ └── factory.ts │ ├── test │ ├── fixtures │ │ └── default-pagination.ts │ ├── unit │ │ ├── express-query-builder.spec.ts │ │ └── factory.spec.ts │ ├── adapters │ │ └── typeorm │ │ │ ├── profile │ │ │ └── loader.spec.ts │ │ │ ├── options │ │ │ ├── select.spec.ts │ │ │ ├── relations.spec.ts │ │ │ ├── order.spec.ts │ │ │ └── pagination.spec.ts │ │ │ └── field │ │ │ ├── filter-factory.spec.ts │ │ │ └── field-filter.spec.ts │ └── integration │ │ └── express │ │ └── express.spec.ts │ ├── package.json │ ├── tsconfig.spec.json │ ├── tsconfig.lib.json │ ├── README.md │ ├── .eslintrc.json │ ├── jest.config.ts │ ├── tsconfig.json │ ├── project.json │ └── CHANGELOG.md ├── tools ├── generators │ └── .gitkeep ├── tsconfig.tools.json └── scripts │ └── publish.mjs ├── .prettierrc ├── logo.png ├── .commitlintrc.json ├── .prettierignore ├── .husky └── commit-msg ├── jest.preset.js ├── express-adapter-pipeline.png ├── jest.config.ts ├── .editorconfig ├── .github └── workflows │ ├── build-and-test.yml │ └── publish.yml ├── .gitignore ├── tsconfig.base.json ├── .eslintrc.json ├── nx.json ├── LICENSE ├── package.json └── README.md /packages/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tools/generators/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tool-Kid/express-query-adapter/HEAD/logo.png -------------------------------------------------------------------------------- /packages/express-query-adapter/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './express-query-adapter'; 2 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/profile/default-config.ts: -------------------------------------------------------------------------------- 1 | export const ITEMS_PER_PAGE = 25; 2 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"], 3 | "rules": {} 4 | } 5 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/express-query.ts: -------------------------------------------------------------------------------- 1 | export type ExpressQuery = Record; 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /dist 4 | /coverage 5 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit $1 5 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nrwl/jest/preset').default; 2 | 3 | module.exports = { ...nxPreset }; 4 | -------------------------------------------------------------------------------- /express-adapter-pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tool-Kid/express-query-adapter/HEAD/express-adapter-pipeline.png -------------------------------------------------------------------------------- /packages/express-query-adapter/src/profile/defaults/index.ts: -------------------------------------------------------------------------------- 1 | export * from './enabled'; 2 | export * from './disabled'; 3 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/profile/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config-profile'; 2 | export * from './loader'; 3 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import { getJestProjects } from '@nrwl/jest'; 2 | 3 | export default { 4 | projects: getJestProjects(), 5 | }; 6 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/profile/enableable-option.ts: -------------------------------------------------------------------------------- 1 | export interface EnableableOption { 2 | status: 'enabled' | 'disabled'; 3 | } 4 | -------------------------------------------------------------------------------- /packages/express-query-adapter/test/fixtures/default-pagination.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_PAGINATION = { 2 | skip: 0, 3 | take: 25, 4 | }; 5 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/typeorm/query.ts: -------------------------------------------------------------------------------- 1 | import { FindManyOptions } from 'typeorm'; 2 | 3 | export type TypeORMQuery = FindManyOptions; 4 | -------------------------------------------------------------------------------- /packages/express-query-adapter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tool-kid/express-query-adapter", 3 | "version": "0.1.1", 4 | "type": "commonjs" 5 | } 6 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/profile/find-policy.ts: -------------------------------------------------------------------------------- 1 | export enum FindPolicy { 2 | Skip = 'skip', 3 | Throw = 'throw', 4 | } 5 | 6 | export type FindPolicyType = `${FindPolicy}`; 7 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/profile/pagination-option.ts: -------------------------------------------------------------------------------- 1 | import { EnableableOption } from './enableable-option'; 2 | 3 | export interface PaginationOption extends EnableableOption { 4 | paginate: boolean; 5 | itemsPerPage: number; 6 | } 7 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/typeorm/filter/field/lookup.ts: -------------------------------------------------------------------------------- 1 | import { FindOptionsUtils } from 'typeorm'; 2 | 3 | export abstract class LookupBuilder { 4 | abstract build(prop: string, value: string): Record; 5 | } 6 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/profile/config-profile.ts: -------------------------------------------------------------------------------- 1 | import { FindPolicyType } from './find-policy'; 2 | import { ProfileOptions } from './profile-options'; 3 | 4 | export interface ConfigProfile { 5 | options: ProfileOptions; 6 | policy: FindPolicyType; 7 | } 8 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/return-type.ts: -------------------------------------------------------------------------------- 1 | import { QueryAdapter } from './express-query-adapter'; 2 | import { TypeORMQueryBuilder } from './typeorm/query-builder'; 3 | 4 | export type QueryBuilderReturnType = 5 | Adapter extends 'typeorm' ? TypeORMQueryBuilder : unknown; 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /packages/express-query-adapter/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/express-query-adapter/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": ["node"] 7 | }, 8 | "include": ["**/*.ts"], 9 | "exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": ["node"], 9 | "importHelpers": false 10 | }, 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/typeorm/filter/field/lookups/exact.ts: -------------------------------------------------------------------------------- 1 | import { FindOptionsUtils } from 'typeorm'; 2 | import { LookupBuilder } from '../lookup'; 3 | 4 | export class ExactLookup implements LookupBuilder { 5 | build(prop: string, value: string): Record { 6 | return { [prop]: value }; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/express-query-adapter/README.md: -------------------------------------------------------------------------------- 1 | # express-query-adapter 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Building 6 | 7 | Run `nx build express-query-adapter` to build the library. 8 | 9 | ## Running unit tests 10 | 11 | Run `nx test express-query-adapter` to execute the unit tests via [Jest](https://jestjs.io). 12 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/typeorm/filter/field/lookups/in.ts: -------------------------------------------------------------------------------- 1 | import { FindOptionsUtils, In } from 'typeorm'; 2 | import { LookupBuilder } from '../lookup'; 3 | 4 | export class InLookup implements LookupBuilder { 5 | build(prop: string, value: string): Record { 6 | return { [prop]: In(value.split(',')) }; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/typeorm/filter/field/lookups/is-null.ts: -------------------------------------------------------------------------------- 1 | import { FindOptionsUtils, IsNull } from 'typeorm'; 2 | import { LookupBuilder } from '../lookup'; 3 | 4 | export class IsNullLookup implements LookupBuilder { 5 | build(prop: string, value: string): Record { 6 | return { [prop]: IsNull() }; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/profile/profile-options.ts: -------------------------------------------------------------------------------- 1 | import { EnableableOption } from './enableable-option'; 2 | import { PaginationOption } from './pagination-option'; 3 | 4 | export interface ProfileOptions { 5 | ordering: EnableableOption; 6 | pagination: PaginationOption; 7 | relations: EnableableOption; 8 | select: EnableableOption; 9 | } 10 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/typeorm/filter/field/lookups/contains.ts: -------------------------------------------------------------------------------- 1 | import { FindOptionsUtils, Like } from 'typeorm'; 2 | import { LookupBuilder } from '../lookup'; 3 | 4 | export class ContainsLookup implements LookupBuilder { 5 | build(prop: string, value: string): Record { 6 | return { [prop]: Like(`%${value}%`) }; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/typeorm/filter/field/lookups/ends-with.ts: -------------------------------------------------------------------------------- 1 | import { FindOptionsUtils, Like } from 'typeorm'; 2 | import { LookupBuilder } from '../lookup'; 3 | 4 | export class EndsWithLookup implements LookupBuilder { 5 | build(prop: string, value: string): Record { 6 | return { [prop]: Like(`%${value}`) }; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/typeorm/filter/field/lookups/gt.ts: -------------------------------------------------------------------------------- 1 | import { FindOptionsUtils, MoreThan } from 'typeorm'; 2 | import { LookupBuilder } from '../lookup'; 3 | 4 | export class GreaterThanLookup implements LookupBuilder { 5 | build(prop: string, value: string): Record { 6 | return { [prop]: MoreThan(value) }; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/typeorm/filter/field/lookups/lt.ts: -------------------------------------------------------------------------------- 1 | import { FindOptionsUtils, LessThan } from 'typeorm'; 2 | import { LookupBuilder } from '../lookup'; 3 | 4 | export class LowerThanLookup implements LookupBuilder { 5 | build(prop: string, value: string): Record { 6 | return { [prop]: LessThan(value) }; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/typeorm/filter/field/lookups/starts-with.ts: -------------------------------------------------------------------------------- 1 | import { FindOptionsUtils, Like } from 'typeorm'; 2 | import { LookupBuilder } from '../lookup'; 3 | 4 | export class StartsWithLookup implements LookupBuilder { 5 | build(prop: string, value: string): Record { 6 | return { [prop]: Like(`${value}%`) }; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/typeorm/filter/field/lookups/icontains.ts: -------------------------------------------------------------------------------- 1 | import { FindOptionsUtils, ILike } from 'typeorm'; 2 | import { LookupBuilder } from '../lookup'; 3 | 4 | export class InsensitiveContainsLookup implements LookupBuilder { 5 | build(prop: string, value: string): Record { 6 | return { [prop]: ILike(`%${value}%`) }; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/typeorm/filter/field/lookups/iends-with.ts: -------------------------------------------------------------------------------- 1 | import { FindOptionsUtils, ILike } from 'typeorm'; 2 | import { LookupBuilder } from '../lookup'; 3 | 4 | export class InsensitiveEndsWithLookup implements LookupBuilder { 5 | build(prop: string, value: string): Record { 6 | return { [prop]: ILike(`%${value}`) }; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/typeorm/filter/field/lookups/gte.ts: -------------------------------------------------------------------------------- 1 | import { FindOptionsUtils, MoreThanOrEqual } from 'typeorm'; 2 | import { LookupBuilder } from '../lookup'; 3 | 4 | export class GreaterThanOrEqualLookup implements LookupBuilder { 5 | build(prop: string, value: string): Record { 6 | return { [prop]: MoreThanOrEqual(value) }; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/typeorm/filter/field/lookups/istarts-with.ts: -------------------------------------------------------------------------------- 1 | import { FindOptionsUtils, ILike } from 'typeorm'; 2 | import { LookupBuilder } from '../lookup'; 3 | 4 | export class InsensitiveStartsWithLookup implements LookupBuilder { 5 | build(prop: string, value: string): Record { 6 | return { [prop]: ILike(`${value}%`) }; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/typeorm/filter/field/lookups/lte.ts: -------------------------------------------------------------------------------- 1 | import { FindOptionsUtils, LessThanOrEqual } from 'typeorm'; 2 | import { LookupBuilder } from '../lookup'; 3 | 4 | export class LowerThanOrEqualLookup implements LookupBuilder { 5 | build(prop: string, value: string): Record { 6 | return { [prop]: LessThanOrEqual(value) }; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/express-query-adapter/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/typeorm/filter/field/lookups/index.ts: -------------------------------------------------------------------------------- 1 | export * from './between'; 2 | export * from './contains'; 3 | export * from './ends-with'; 4 | export * from './exact'; 5 | export * from './gt'; 6 | export * from './gte'; 7 | export * from './icontains'; 8 | export * from './iends-with'; 9 | export * from './in'; 10 | export * from './is-null'; 11 | export * from './istarts-with'; 12 | export * from './lt'; 13 | export * from './lte'; 14 | export * from './starts-with'; 15 | -------------------------------------------------------------------------------- /packages/express-query-adapter/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'express-query-adapter', 4 | preset: '../../jest.preset.js', 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | }, 9 | }, 10 | testEnvironment: 'node', 11 | transform: { 12 | '^.+\\.[tj]s$': 'ts-jest', 13 | }, 14 | moduleFileExtensions: ['ts', 'js', 'html'], 15 | coverageDirectory: '../../coverage/packages/express-query-adapter', 16 | }; 17 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/query-builder.ts: -------------------------------------------------------------------------------- 1 | import { ConfigProfile, ProfileLoader } from './profile'; 2 | import { ExpressQuery } from './express-query'; 3 | 4 | export abstract class QueryBuilder { 5 | protected readonly profile: ConfigProfile; 6 | private readonly profileLoader: ProfileLoader = new ProfileLoader(); 7 | 8 | constructor(profile?: ConfigProfile) { 9 | this.profile = this.profileLoader.load(profile); 10 | } 11 | 12 | abstract build(expressQuery: ExpressQuery): Query; 13 | } 14 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/profile/defaults/enabled.ts: -------------------------------------------------------------------------------- 1 | import { ConfigProfile } from '../config-profile'; 2 | 3 | export const ENABLED_PROFILE: ConfigProfile = { 4 | options: { 5 | pagination: { 6 | status: 'enabled', 7 | paginate: true, 8 | itemsPerPage: 25, 9 | }, 10 | ordering: { 11 | status: 'enabled', 12 | }, 13 | relations: { 14 | status: 'enabled', 15 | }, 16 | select: { 17 | status: 'enabled', 18 | }, 19 | }, 20 | policy: 'skip', 21 | }; 22 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/typeorm/filter/options/filter-option.ts: -------------------------------------------------------------------------------- 1 | import { TypeORMQuery } from '../../query'; 2 | import { ExpressQuery } from '../../../express-query'; 3 | import { ConfigProfile } from '../../../profile/config-profile'; 4 | 5 | export interface FilterOptionQuery { 6 | source: ExpressQuery; 7 | target: TypeORMQuery; 8 | } 9 | 10 | export interface FilterOption { 11 | setOption(query: FilterOptionQuery, profile: ConfigProfile): void; 12 | isAuthorized(profile: ConfigProfile): boolean; 13 | } 14 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/profile/defaults/disabled.ts: -------------------------------------------------------------------------------- 1 | import { ConfigProfile } from '../config-profile'; 2 | 3 | export const DISABLED_PROFILE: ConfigProfile = { 4 | options: { 5 | pagination: { 6 | status: 'disabled', 7 | paginate: true, 8 | itemsPerPage: 25, 9 | }, 10 | ordering: { 11 | status: 'disabled', 12 | }, 13 | relations: { 14 | status: 'disabled', 15 | }, 16 | select: { 17 | status: 'disabled', 18 | }, 19 | }, 20 | policy: 'skip', 21 | }; 22 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test 2 | on: 3 | pull_request: 4 | 5 | jobs: 6 | main: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | with: 11 | fetch-depth: 0 12 | - uses: nrwl/nx-set-shas@v3 13 | - run: npm ci 14 | 15 | - run: npx nx workspace-lint 16 | - run: npx nx affected --target=lint --parallel=3 17 | - run: npx nx affected --target=test --parallel=3 --ci --code-coverage 18 | - run: npx nx affected --target=build --parallel=3 19 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/profile/loader.ts: -------------------------------------------------------------------------------- 1 | import { ConfigProfile } from './config-profile'; 2 | import { DISABLED_PROFILE, ENABLED_PROFILE } from './defaults'; 3 | 4 | export class ProfileLoader { 5 | public load(profile?: 'enabled' | 'disabled' | ConfigProfile): ConfigProfile { 6 | if (!profile) { 7 | return ENABLED_PROFILE; 8 | } 9 | switch (profile) { 10 | case 'enabled': 11 | return ENABLED_PROFILE; 12 | case 'disabled': 13 | return DISABLED_PROFILE; 14 | default: 15 | return profile; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/typeorm/filter/field/lookup.enum.ts: -------------------------------------------------------------------------------- 1 | export enum LookupFilter { 2 | EXACT = 'exact', 3 | CONTAINS = 'contains', 4 | ICONTAINS = 'icontains', 5 | IS_NULL = 'isnull', 6 | GT = 'gt', 7 | GTE = 'gte', 8 | LT = 'lt', 9 | LTE = 'lte', 10 | STARTS_WITH = 'startswith', 11 | ENDS_WITH = 'endswith', 12 | ISTARTS_WITH = 'istartswith', 13 | IENDS_WITH = 'iendswith', 14 | IN = 'in', 15 | BETWEEN = 'between', 16 | NOT = 'not', 17 | } 18 | 19 | export enum LookupDelimiter { 20 | LOOKUP_DELIMITER = '__', 21 | RELATION_DELIMITER = '.', 22 | } 23 | -------------------------------------------------------------------------------- /packages/express-query-adapter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "forceConsistentCasingInFileNames": true, 6 | "strict": true, 7 | "noImplicitOverride": true, 8 | "noPropertyAccessFromIndexSignature": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true 11 | }, 12 | "files": [], 13 | "include": [], 14 | "references": [ 15 | { 16 | "path": "./tsconfig.lib.json" 17 | }, 18 | { 19 | "path": "./tsconfig.spec.json" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/typeorm/filter/options/container.ts: -------------------------------------------------------------------------------- 1 | import { FilterOption } from './filter-option'; 2 | import { OrderOption } from './order-option'; 3 | import { PaginationOption } from './pagination-option'; 4 | import { RelationsOption } from './relations-option'; 5 | import { SelectOption } from './select-option'; 6 | 7 | export class OptionsCollection { 8 | public readonly options: FilterOption[]; 9 | 10 | constructor() { 11 | this.options = [ 12 | new PaginationOption(), 13 | new OrderOption(), 14 | new RelationsOption(), 15 | new SelectOption(), 16 | ]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/express-query-adapter/test/unit/express-query-builder.spec.ts: -------------------------------------------------------------------------------- 1 | import { getQueryAdapter } from '../../src/express-query-adapter'; 2 | import { TypeORMQueryBuilder } from '../../src/typeorm/query-builder'; 3 | 4 | describe('ExpressQueryBuilder', () => { 5 | it('should create an instance', async () => { 6 | const qb = await getQueryAdapter({ adapter: 'typeorm' }); 7 | expect(qb).toBeInstanceOf(TypeORMQueryBuilder); 8 | }); 9 | 10 | it('should build a query', async () => { 11 | const qb = await getQueryAdapter({ adapter: 'typeorm' }); 12 | const query = qb.build({}); 13 | expect(query).toBeDefined(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/typeorm/filter/filter.ts: -------------------------------------------------------------------------------- 1 | import { ExpressQuery } from '../../express-query'; 2 | import { LookupFilter } from './field/lookup.enum'; 3 | 4 | export abstract class AbstractFilter { 5 | public readonly prop: string; 6 | public readonly lookup: LookupFilter; 7 | public readonly value: string; 8 | public query: ExpressQuery; 9 | 10 | constructor( 11 | query: ExpressQuery, 12 | prop: string, 13 | lookup: LookupFilter, 14 | value: string 15 | ) { 16 | this.query = query; 17 | this.prop = prop; 18 | this.lookup = lookup; 19 | this.value = value; 20 | } 21 | 22 | public abstract buildQuery(): void; 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "target": "es2015", 12 | "module": "esnext", 13 | "lib": ["es2017", "dom"], 14 | "skipLibCheck": true, 15 | "skipDefaultLibCheck": true, 16 | "baseUrl": ".", 17 | "paths": { 18 | "@tool-kid/express-query-adapter": [ 19 | "packages/express-query-adapter/src/index.ts" 20 | ] 21 | } 22 | }, 23 | "exclude": ["node_modules", "tmp"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/express-query-adapter.ts: -------------------------------------------------------------------------------- 1 | import { QueryBuilderFactory } from './factory'; 2 | import { ConfigProfile } from './profile'; 3 | import { QueryBuilderReturnType } from './return-type'; 4 | 5 | interface Config { 6 | adapter: Adapter; 7 | profile?: ProfileType; 8 | } 9 | 10 | export type ProfileType = 'enabled' | 'disabled' | ConfigProfile; 11 | export type QueryAdapter = 'typeorm'; 12 | 13 | export async function getQueryAdapter( 14 | config: Config 15 | ): Promise> { 16 | const factory = new QueryBuilderFactory(); 17 | const queryBuilder = await factory.build(config.adapter, config.profile); 18 | return queryBuilder; 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | workflow_dispatch: 4 | 5 | jobs: 6 | publish: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | with: 11 | fetch-depth: 0 12 | - uses: nrwl/nx-set-shas@v3 13 | - run: npm ci 14 | - name: Auth npm 15 | run: | 16 | echo '//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}' > .npmrc 17 | env: 18 | NODE_AUTH_TOKEN: ${{ secrets.DEPLOY_NPMJS }} 19 | 20 | - run: npx nx workspace-lint 21 | - run: npx nx affected --target=lint --parallel=3 22 | - run: npx nx affected --target=test --parallel=3 --ci --code-coverage 23 | - run: npx nx affected --target=build --parallel=3 24 | - run: npx nx deploy express-query-adapter 25 | env: 26 | NODE_AUTH_TOKEN: ${{ secrets.DEPLOY_NPMJS }} 27 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/typeorm/filter/options/select-option.ts: -------------------------------------------------------------------------------- 1 | import { ConfigProfile } from '../../../profile/config-profile'; 2 | import { FilterOption, FilterOptionQuery } from './filter-option'; 3 | 4 | export class SelectOption implements FilterOption { 5 | public setOption(query: FilterOptionQuery, profile: ConfigProfile): void { 6 | if (!this.isAuthorized(profile)) { 7 | delete query.source['select']; 8 | return; 9 | } 10 | if (!query.source['select']) { 11 | return; 12 | } 13 | 14 | const fields = query.source['select'].split(','); 15 | query.target['select'] = fields; 16 | 17 | delete query.source['select']; 18 | } 19 | 20 | public isAuthorized(profile: ConfigProfile): boolean { 21 | if (profile.options.select.status === 'disabled') { 22 | return false; 23 | } 24 | return true; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/typeorm/filter/options/relations-option.ts: -------------------------------------------------------------------------------- 1 | import { ConfigProfile } from '../../../profile/config-profile'; 2 | import { FilterOption, FilterOptionQuery } from './filter-option'; 3 | 4 | export class RelationsOption implements FilterOption { 5 | public setOption(query: FilterOptionQuery, profile: ConfigProfile): void { 6 | if (!this.isAuthorized(profile)) { 7 | delete query.source['with']; 8 | return; 9 | } 10 | if (!query.source['with']) { 11 | return; 12 | } 13 | 14 | const relations = query.source['with'].split(','); 15 | query.target['relations'] = relations; 16 | 17 | delete query.source['with']; 18 | } 19 | 20 | public isAuthorized(profile: ConfigProfile): boolean { 21 | if (profile.options.relations.status === 'disabled') { 22 | return false; 23 | } 24 | return true; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/factory.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-case-declarations */ 2 | import { ProfileType, QueryAdapter } from './express-query-adapter'; 3 | import { ProfileLoader } from './profile'; 4 | import { QueryBuilderReturnType } from './return-type'; 5 | 6 | export class QueryBuilderFactory { 7 | private readonly profileFactory = new ProfileLoader(); 8 | public async build( 9 | adapter: Adapter, 10 | profileType?: ProfileType 11 | ): Promise> { 12 | const profile = this.profileFactory.load(profileType); 13 | switch (adapter) { 14 | case 'typeorm': 15 | const qb = (await import('./typeorm/query-builder')) 16 | .TypeORMQueryBuilder; 17 | return new qb(profile); 18 | default: 19 | throw new Error(`No adapter found for ${adapter}`); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/express-query-adapter/test/unit/factory.spec.ts: -------------------------------------------------------------------------------- 1 | import { QueryBuilderFactory } from '../../src/factory'; 2 | import { TypeORMQueryBuilder } from '../../src/typeorm/query-builder'; 3 | 4 | describe('QueryBuilderFactory', () => { 5 | it('should create an instance', async () => { 6 | const factory = new QueryBuilderFactory(); 7 | expect(factory).toBeInstanceOf(QueryBuilderFactory); 8 | }); 9 | 10 | it('should return a TypeORMQueryBuilder instance when ', async () => { 11 | const factory = new QueryBuilderFactory(); 12 | const qb = await factory.build('typeorm'); 13 | expect(qb).toBeInstanceOf(TypeORMQueryBuilder); 14 | }); 15 | 16 | it('should throw an error when unrecognized strategy provided', async () => { 17 | const factory = new QueryBuilderFactory(); 18 | const qb = () => factory.build('' as any); 19 | expect(() => qb()).rejects.toThrow(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/typeorm/filter/field/lookups/between.ts: -------------------------------------------------------------------------------- 1 | import { Between, FindOptionsUtils } from 'typeorm'; 2 | import { LookupBuilder } from '../lookup'; 3 | 4 | export class BetweenLookup implements LookupBuilder { 5 | build(prop: string, value: string): Record { 6 | const rangeValues = value.split(','); 7 | const isoDateRegex = 8 | /^(?:[+-]?\d{4}(?!\d{2}\b))(?:(-?)(?:(?:0[1-9]|1[0-2])(?:\1(?:[12]\d|0[1-9]|3[01]))?|W(?:[0-4]\d|5[0-2])(?:-?[1-7])?|(?:00[1-9]|0[1-9]\d|[12]\d{2}|3(?:[0-5]\d|6[1-6])))(?:[T\s](?:(?:(?:[01]\d|2[0-3])(?:(:?)[0-5]\d)?|24:?00)(?:[.,]\d+(?!:))?)?(?:\2[0-5]\d(?:[.,]\d+)?)?(?:[zZ]|(?:[+-])(?:[01]\d|2[0-3]):?(?:[0-5]\d)?)?)?)?$/; 9 | const isDate = 10 | isoDateRegex.test(rangeValues[0]) && isoDateRegex.test(rangeValues[0]); 11 | const { from, to } = isDate 12 | ? { from: new Date(rangeValues[0]), to: new Date(rangeValues[1]) } 13 | : { from: +rangeValues[0], to: +rangeValues[1] }; 14 | return { [prop]: Between(from, to) }; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nrwl/nx"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "rules": { 9 | "@nrwl/nx/enforce-module-boundaries": [ 10 | "error", 11 | { 12 | "enforceBuildableLibDependency": true, 13 | "allow": [], 14 | "depConstraints": [ 15 | { 16 | "sourceTag": "*", 17 | "onlyDependOnLibsWithTags": ["*"] 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | }, 24 | { 25 | "files": ["*.ts", "*.tsx"], 26 | "extends": ["plugin:@nrwl/nx/typescript"], 27 | "rules": {} 28 | }, 29 | { 30 | "files": ["*.js", "*.jsx"], 31 | "extends": ["plugin:@nrwl/nx/javascript"], 32 | "rules": {} 33 | }, 34 | { 35 | "files": ["*.spec.ts", "*.spec.tsx", "*.spec.js", "*.spec.jsx"], 36 | "env": { 37 | "jest": true 38 | }, 39 | "rules": {} 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/nx/schemas/nx-schema.json", 3 | "npmScope": "express-query-adapter", 4 | "tasksRunnerOptions": { 5 | "default": { 6 | "runner": "nx/tasks-runners/default", 7 | "options": { 8 | "cacheableOperations": ["build", "lint", "test", "e2e"] 9 | } 10 | } 11 | }, 12 | "targetDefaults": { 13 | "build": { 14 | "dependsOn": ["^build"], 15 | "inputs": ["production", "^production"] 16 | }, 17 | "lint": { 18 | "inputs": ["default", "{workspaceRoot}/.eslintrc.json"] 19 | }, 20 | "test": { 21 | "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"] 22 | } 23 | }, 24 | "namedInputs": { 25 | "default": ["{projectRoot}/**/*", "sharedGlobals"], 26 | "production": [ 27 | "default", 28 | "!{projectRoot}/.eslintrc.json", 29 | "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", 30 | "!{projectRoot}/tsconfig.spec.json", 31 | "!{projectRoot}/jest.config.[jt]s" 32 | ], 33 | "sharedGlobals": [] 34 | }, 35 | "workspaceLayout": { 36 | "appsDir": "packages", 37 | "libsDir": "packages" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Raúl Julián López Caña 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/express-query-adapter/test/adapters/typeorm/profile/loader.spec.ts: -------------------------------------------------------------------------------- 1 | import { ConfigProfile } from '../../../../src/profile/config-profile'; 2 | import { 3 | DISABLED_PROFILE, 4 | ENABLED_PROFILE, 5 | } from '../../../../src/profile/defaults'; 6 | import { ProfileLoader } from '../../../../src/profile/loader'; 7 | 8 | describe('Profile loader', () => { 9 | const loader = new ProfileLoader(); 10 | 11 | it('should return ENABLED_PROFILE when no profile provided', () => { 12 | expect(loader.load(null as any)).toEqual(ENABLED_PROFILE); 13 | }); 14 | it('should return ENABLED_PROFILE when "enabled" strategy provided', () => { 15 | expect(loader.load('enabled')).toEqual(ENABLED_PROFILE); 16 | }); 17 | it('should return DISABLED_PROFILE when "disabled" strategy provided', () => { 18 | expect(loader.load('disabled')).toEqual(DISABLED_PROFILE); 19 | }); 20 | it('should return a custom profile when provided', () => { 21 | const customProfile: ConfigProfile = { 22 | ...ENABLED_PROFILE, 23 | options: { 24 | ...ENABLED_PROFILE.options, 25 | ordering: { status: 'disabled' }, 26 | }, 27 | }; 28 | expect(loader.load(customProfile)).toEqual(customProfile); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/typeorm/query-builder.ts: -------------------------------------------------------------------------------- 1 | import { ExpressQuery } from '../express-query'; 2 | import { TypeORMQuery } from './query'; 3 | import { FilterFactory } from './filter/filter-factory'; 4 | import { OptionsCollection } from './filter/options/container'; 5 | import { ConfigProfile } from '../profile/config-profile'; 6 | import { QueryBuilder } from '../query-builder'; 7 | 8 | export class TypeORMQueryBuilder extends QueryBuilder { 9 | private readonly findOptions: OptionsCollection = new OptionsCollection(); 10 | private readonly filterFactory: FilterFactory = new FilterFactory(); 11 | 12 | constructor(profile?: ConfigProfile) { 13 | super(profile); 14 | } 15 | 16 | build(expressQuery: ExpressQuery): TypeORMQuery { 17 | const typeORMQuery: TypeORMQuery = {}; 18 | for (const option of this.findOptions.options) { 19 | option.setOption( 20 | { 21 | source: expressQuery, 22 | target: typeORMQuery, 23 | }, 24 | this.profile 25 | ); 26 | } 27 | 28 | for (const queryItem in expressQuery) { 29 | const filter = this.filterFactory.get({ 30 | query: typeORMQuery, 31 | key: queryItem, 32 | value: expressQuery[queryItem], 33 | }); 34 | filter.buildQuery(); 35 | } 36 | 37 | return typeORMQuery; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/typeorm/filter/options/order-option.ts: -------------------------------------------------------------------------------- 1 | import { ConfigProfile } from '../../../profile/config-profile'; 2 | import { FilterOption, FilterOptionQuery } from './filter-option'; 3 | 4 | export class OrderOption implements FilterOption { 5 | public setOption(query: FilterOptionQuery, profile: ConfigProfile): void { 6 | if (!this.isAuthorized(profile)) { 7 | delete query.source['order']; 8 | return; 9 | } 10 | if (!query.source['order']) { 11 | return; 12 | } 13 | const orderFields = query.source['order'].split(','); 14 | for (const field of orderFields) { 15 | const orderCriteria = this.getOrderCriteria(field); 16 | query.target['order'] = { 17 | ...query.target['order'], 18 | [field.substr(1, field.length)]: orderCriteria, 19 | }; 20 | } 21 | delete query.source['order']; 22 | } 23 | 24 | private getOrderCriteria(field: string): string { 25 | if (field.startsWith('+')) { 26 | return 'ASC'; 27 | } else if (field.startsWith('-')) { 28 | return 'DESC'; 29 | } else { 30 | throw new Error( 31 | `No order set for <${field}>. Prefix with one of these: [+, -]` 32 | ); 33 | } 34 | } 35 | 36 | public isAuthorized(profile: ConfigProfile): boolean { 37 | if (profile.options.ordering.status === 'disabled') { 38 | return false; 39 | } 40 | return true; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/typeorm/filter/options/pagination-option.ts: -------------------------------------------------------------------------------- 1 | import { ConfigProfile } from '../../../profile/config-profile'; 2 | import { FilterOption, FilterOptionQuery } from './filter-option'; 3 | 4 | export class PaginationOption implements FilterOption { 5 | public setOption(query: FilterOptionQuery, profile: ConfigProfile): void { 6 | const { itemsPerPage } = profile.options.pagination; 7 | 8 | if (!this.isAuthorized(profile)) { 9 | delete query.source['pagination']; 10 | delete query.source['page']; 11 | delete query.source['limit']; 12 | return; 13 | } 14 | 15 | if ( 16 | query.source['pagination'] === undefined || 17 | query.source['pagination'] === true 18 | ) { 19 | query.target['skip'] = 20 | query.source['page'] && query.source['page'] > 1 21 | ? (query.source['page'] - 1) * (query.source['limit'] || itemsPerPage) 22 | : 0; 23 | delete query.source['page']; 24 | query.target['take'] = 25 | query.source['limit'] && query.source['limit'] > 0 26 | ? query.source['limit'] 27 | : itemsPerPage; 28 | delete query.source['limit']; 29 | } 30 | delete query.source['pagination']; 31 | } 32 | 33 | public isAuthorized(profile: ConfigProfile): boolean { 34 | if (profile.options.pagination.status === 'disabled') { 35 | return false; 36 | } 37 | return true; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-query-adapter", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "prepare": "husky install" 7 | }, 8 | "private": true, 9 | "dependencies": { 10 | "@swc/helpers": "~0.4.11", 11 | "tslib": "^2.3.0" 12 | }, 13 | "devDependencies": { 14 | "@commitlint/cli": "^17.0.0", 15 | "@commitlint/config-conventional": "^17.0.0", 16 | "@jscutlery/semver": "^2.29.0", 17 | "@nrwl/cli": "15.1.1", 18 | "@nrwl/esbuild": "15.1.1", 19 | "@nrwl/eslint-plugin-nx": "15.1.1", 20 | "@nrwl/jest": "15.1.1", 21 | "@nrwl/js": "15.1.1", 22 | "@nrwl/linter": "15.1.1", 23 | "@nrwl/workspace": "15.1.1", 24 | "@types/express": "^4.17.13", 25 | "@types/jest": "28.1.1", 26 | "@types/node": "16.11.7", 27 | "@types/supertest": "^2.0.11", 28 | "@typescript-eslint/eslint-plugin": "^5.36.1", 29 | "@typescript-eslint/parser": "^5.36.1", 30 | "codecov": "^3.8.3", 31 | "esbuild": "^0.15.7", 32 | "eslint": "~8.15.0", 33 | "eslint-config-prettier": "8.1.0", 34 | "express": "^4.17.1", 35 | "husky": "^8.0.0", 36 | "jest": "28.1.1", 37 | "jest-environment-jsdom": "28.1.1", 38 | "ngx-deploy-npm": "^4.3.5", 39 | "nx": "15.1.1", 40 | "pg": "^8.8.0", 41 | "prettier": "^2.6.2", 42 | "supertest": "^6.1.6", 43 | "ts-jest": "28.0.5", 44 | "ts-node": "10.9.1", 45 | "typeorm": "^0.2.37", 46 | "typescript": "~4.8.2" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/typeorm/filter/field/field-filter.ts: -------------------------------------------------------------------------------- 1 | import { LookupBuilderFactory } from './lookup-builder-factory'; 2 | import { Not } from 'typeorm'; 3 | import { AbstractFilter } from '../filter'; 4 | import { LookupFilter } from './lookup.enum'; 5 | import { ExpressQuery } from '../../../express-query'; 6 | import { TypeORMQuery } from '../../query'; 7 | 8 | interface FilterConfig { 9 | query: ExpressQuery; 10 | prop: string; 11 | lookup: LookupFilter; 12 | value: string; 13 | notOperator?: boolean; 14 | } 15 | 16 | export class FieldFilter extends AbstractFilter { 17 | private readonly notOperator: boolean; 18 | private readonly lookupBuilderFactory: LookupBuilderFactory = 19 | new LookupBuilderFactory(); 20 | 21 | constructor(config: FilterConfig) { 22 | super(config.query, config.prop, config.lookup, config.value); 23 | this.notOperator = config.notOperator || false; 24 | } 25 | 26 | public buildQuery(): void { 27 | const queryToAdd = this.getQuery(); 28 | this.setQuery(queryToAdd); 29 | } 30 | 31 | private setQuery(queryToAdd: TypeORMQuery) { 32 | this.query['where'] = { 33 | ...this.query['where'], 34 | ...queryToAdd, 35 | }; 36 | } 37 | 38 | private getQuery(): TypeORMQuery { 39 | const builder = this.lookupBuilderFactory.build(this.lookup); 40 | const queryToAdd = builder.build(this.prop, this.value); 41 | if (this.notOperator) { 42 | queryToAdd[this.prop] = Not(queryToAdd[this.prop]); 43 | } 44 | return queryToAdd; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/express-query-adapter/test/adapters/typeorm/options/select.spec.ts: -------------------------------------------------------------------------------- 1 | import { ConfigProfile } from '../../../../src/profile/config-profile'; 2 | import { ENABLED_PROFILE } from '../../../../src/profile/defaults'; 3 | import { TypeORMQueryBuilder } from '../../../../src/typeorm/query-builder'; 4 | import { DEFAULT_PAGINATION } from '../../../fixtures/default-pagination'; 5 | 6 | describe('', () => { 7 | let qb: TypeORMQueryBuilder; 8 | let profile: ConfigProfile; 9 | 10 | beforeEach(() => { 11 | profile = ENABLED_PROFILE; 12 | }); 13 | it('should attach "select" fields when equals to "enabled"', () => { 14 | profile.options.select.status = 'enabled'; 15 | qb = new TypeORMQueryBuilder(profile); 16 | expect(qb.build({ select: 'field1,field2' })).toEqual({ 17 | ...DEFAULT_PAGINATION, 18 | select: ['field1', 'field2'], 19 | }); 20 | }); 21 | it('should not attach "select" fields when equals to "disabled"', () => { 22 | profile.options.select.status = 'disabled'; 23 | qb = new TypeORMQueryBuilder(profile); 24 | expect(qb.build({ select: 'field1,field2' })).toEqual({ 25 | ...DEFAULT_PAGINATION, 26 | }); 27 | }); 28 | it('should not attach fields when equals to "enabled" and "select" key equals to undefined', () => { 29 | profile.options.select.status = 'enabled'; 30 | qb = new TypeORMQueryBuilder(profile); 31 | expect(qb.build({})).toEqual({ 32 | ...DEFAULT_PAGINATION, 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /packages/express-query-adapter/test/adapters/typeorm/options/relations.spec.ts: -------------------------------------------------------------------------------- 1 | import { ConfigProfile } from '../../../../src/profile/config-profile'; 2 | import { ENABLED_PROFILE } from '../../../../src/profile/defaults'; 3 | import { TypeORMQueryBuilder } from '../../../../src/typeorm/query-builder'; 4 | import { DEFAULT_PAGINATION } from '../../../fixtures/default-pagination'; 5 | 6 | describe('', () => { 7 | let qb: TypeORMQueryBuilder; 8 | let profile: ConfigProfile; 9 | 10 | beforeEach(() => { 11 | profile = ENABLED_PROFILE; 12 | }); 13 | it('should attach "relations" when equals to "enabled"', () => { 14 | profile.options.relations.status = 'enabled'; 15 | qb = new TypeORMQueryBuilder(profile); 16 | expect(qb.build({ with: 'rel1,rel2' })).toEqual({ 17 | ...DEFAULT_PAGINATION, 18 | relations: ['rel1', 'rel2'], 19 | }); 20 | }); 21 | it('should not attach "relations" when equals to "disabled"', () => { 22 | profile.options.relations.status = 'disabled'; 23 | qb = new TypeORMQueryBuilder(profile); 24 | expect(qb.build({ with: 'rel1,rel2' })).toEqual({ 25 | ...DEFAULT_PAGINATION, 26 | }); 27 | }); 28 | it('should not attach relations when equals to "enabled" and "with" key equals to undefined', () => { 29 | profile.options.pagination.status = 'enabled'; 30 | qb = new TypeORMQueryBuilder(profile); 31 | expect(qb.build({})).toEqual({ 32 | ...DEFAULT_PAGINATION, 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /packages/express-query-adapter/test/adapters/typeorm/options/order.spec.ts: -------------------------------------------------------------------------------- 1 | import { ConfigProfile } from '../../../../src/profile/config-profile'; 2 | import { ENABLED_PROFILE } from '../../../../src/profile/defaults'; 3 | import { TypeORMQueryBuilder } from '../../../../src/typeorm/query-builder'; 4 | import { DEFAULT_PAGINATION } from '../../../fixtures/default-pagination'; 5 | 6 | describe('', () => { 7 | let qb: TypeORMQueryBuilder; 8 | let profile: ConfigProfile; 9 | 10 | beforeEach(() => { 11 | profile = ENABLED_PROFILE; 12 | }); 13 | it('should attach "order" criteria when equals to "enabled"', () => { 14 | profile.options.ordering.status = 'enabled'; 15 | qb = new TypeORMQueryBuilder(profile); 16 | expect(qb.build({ order: '+field1,-field2' })).toEqual({ 17 | ...DEFAULT_PAGINATION, 18 | order: { 19 | field1: 'ASC', 20 | field2: 'DESC', 21 | }, 22 | }); 23 | }); 24 | it('should not attach "order" criteria when equals to "disabled"', () => { 25 | profile.options.ordering.status = 'disabled'; 26 | qb = new TypeORMQueryBuilder(profile); 27 | expect(qb.build({ order: '+field1,-field2' })).toEqual({ 28 | ...DEFAULT_PAGINATION, 29 | }); 30 | }); 31 | it('should not attach fields when equals to "enabled" and "select" key equals to undefined', () => { 32 | profile.options.ordering.status = 'enabled'; 33 | qb = new TypeORMQueryBuilder(profile); 34 | expect(qb.build({})).toEqual({ 35 | ...DEFAULT_PAGINATION, 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /packages/express-query-adapter/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-query-adapter", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "packages/express-query-adapter/src", 5 | "projectType": "library", 6 | "targets": { 7 | "build": { 8 | "executor": "@nrwl/js:tsc", 9 | "outputs": ["{options.outputPath}"], 10 | "options": { 11 | "outputPath": "dist/packages/express-query-adapter", 12 | "main": "packages/express-query-adapter/src/index.ts", 13 | "tsConfig": "packages/express-query-adapter/tsconfig.lib.json", 14 | "assets": ["README.md"] 15 | } 16 | }, 17 | "lint": { 18 | "executor": "@nrwl/linter:eslint", 19 | "outputs": ["{options.outputFile}"], 20 | "options": { 21 | "lintFilePatterns": ["packages/express-query-adapter/**/*.ts"] 22 | } 23 | }, 24 | "test": { 25 | "executor": "@nrwl/jest:jest", 26 | "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], 27 | "options": { 28 | "jestConfig": "packages/express-query-adapter/jest.config.ts", 29 | "passWithNoTests": true 30 | } 31 | }, 32 | "deploy": { 33 | "executor": "ngx-deploy-npm:deploy", 34 | "options": { 35 | "access": "public" 36 | } 37 | }, 38 | "github": { 39 | "executor": "@jscutlery/semver:github", 40 | "options": { 41 | "tag": "${tag}", 42 | "notes": "${notes}" 43 | } 44 | }, 45 | "version": { 46 | "executor": "@jscutlery/semver:version", 47 | "options": { 48 | "preset": "conventional" 49 | } 50 | } 51 | }, 52 | "tags": [] 53 | } 54 | -------------------------------------------------------------------------------- /packages/express-query-adapter/test/adapters/typeorm/field/filter-factory.spec.ts: -------------------------------------------------------------------------------- 1 | import { FilterFactory } from '../../../../src/typeorm/filter/filter-factory'; 2 | import { FieldFilter } from '../../../../src/typeorm/filter/field/field-filter'; 3 | 4 | describe('Test FilterFactory #get', () => { 5 | const factory = new FilterFactory(); 6 | 7 | it('should return an instance of FieldFilter', () => { 8 | const filter = factory.get({ 9 | query: {}, 10 | key: 'field', 11 | value: 'value', 12 | }); 13 | expect(filter).toBeInstanceOf(FieldFilter); 14 | }); 15 | 16 | it('should return an instance of FieldFilter with notOperator equals to true', () => { 17 | const filter = factory.get({ 18 | query: {}, 19 | key: 'field__not', 20 | value: 'value', 21 | }) as any; 22 | expect(filter).toBeInstanceOf(FieldFilter); 23 | expect(filter.notOperator).toBeTruthy(); 24 | }); 25 | 26 | it('should return an instance of FieldFilter with notOperator equals to false', () => { 27 | const filter = factory.get({ 28 | query: {}, 29 | key: 'field', 30 | value: 'value', 31 | }) as any; 32 | expect(filter).toBeInstanceOf(FieldFilter); 33 | expect(filter.notOperator).toBeFalsy(); 34 | }); 35 | }); 36 | 37 | describe('Test FilterFactory #isFieldFilter', () => { 38 | const factory: any = new FilterFactory(); 39 | 40 | it('should return true', () => { 41 | const isField = factory.isFieldFilter('field'); 42 | expect(isField).toBeTruthy(); 43 | }); 44 | 45 | it('should return false', () => { 46 | const isField = factory.isFieldFilter('fk.field'); 47 | expect(isField).toBeFalsy(); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/typeorm/filter/field/lookup-builder-factory.ts: -------------------------------------------------------------------------------- 1 | import { LookupFilter } from './lookup.enum'; 2 | import { LookupBuilder } from './lookup'; 3 | import { 4 | BetweenLookup, 5 | ContainsLookup, 6 | EndsWithLookup, 7 | ExactLookup, 8 | GreaterThanLookup, 9 | GreaterThanOrEqualLookup, 10 | InLookup, 11 | InsensitiveContainsLookup, 12 | InsensitiveEndsWithLookup, 13 | InsensitiveStartsWithLookup, 14 | IsNullLookup, 15 | LowerThanLookup, 16 | LowerThanOrEqualLookup, 17 | StartsWithLookup, 18 | } from './lookups'; 19 | 20 | const LOOKUP_FILTER_MAP: Map = new Map([ 21 | [LookupFilter.EXACT, new ExactLookup()], 22 | [LookupFilter.CONTAINS, new ContainsLookup()], 23 | [LookupFilter.STARTS_WITH, new StartsWithLookup()], 24 | [LookupFilter.ENDS_WITH, new EndsWithLookup()], 25 | [LookupFilter.ICONTAINS, new InsensitiveContainsLookup()], 26 | [LookupFilter.ISTARTS_WITH, new InsensitiveStartsWithLookup()], 27 | [LookupFilter.IENDS_WITH, new InsensitiveEndsWithLookup()], 28 | [LookupFilter.IS_NULL, new IsNullLookup()], 29 | [LookupFilter.LT, new LowerThanLookup()], 30 | [LookupFilter.LTE, new LowerThanOrEqualLookup()], 31 | [LookupFilter.GT, new GreaterThanLookup()], 32 | [LookupFilter.GTE, new GreaterThanOrEqualLookup()], 33 | [LookupFilter.IN, new InLookup()], 34 | [LookupFilter.BETWEEN, new BetweenLookup()], 35 | ]); 36 | 37 | export class LookupBuilderFactory { 38 | private readonly lookups = LOOKUP_FILTER_MAP; 39 | 40 | build(lookup: LookupFilter): LookupBuilder { 41 | const builder = this.lookups.get(lookup); 42 | if (!builder) { 43 | throw new Error(`Unsupported lookup ${lookup}`); 44 | } 45 | return builder; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/express-query-adapter/src/typeorm/filter/filter-factory.ts: -------------------------------------------------------------------------------- 1 | import { LookupDelimiter, LookupFilter } from './field/lookup.enum'; 2 | import { FieldFilter } from './field/field-filter'; 3 | import { AbstractFilter } from './filter'; 4 | import { TypeORMQuery } from '../query'; 5 | 6 | interface FilterFactoryQuery { 7 | query: TypeORMQuery; 8 | key: string; 9 | value: string; 10 | } 11 | 12 | export class FilterFactory { 13 | public get(query: FilterFactoryQuery): AbstractFilter { 14 | if (!this.isFieldFilter(query.key)) { 15 | throw new Error(`${query.key} is not a field`); 16 | } 17 | const prop = this.getProp(query); 18 | const hasNotOperator = this.hasNotOperator(query); 19 | const lookup = this.getLookupFilter(query, hasNotOperator); 20 | return new FieldFilter({ 21 | query: query.query, 22 | prop, 23 | lookup, 24 | value: query.value, 25 | notOperator: hasNotOperator, 26 | }); 27 | } 28 | 29 | private getLookupFilter( 30 | query: FilterFactoryQuery, 31 | hasNotOperator: boolean 32 | ): LookupFilter { 33 | const includesLookupDelimiter = query.key.includes( 34 | LookupDelimiter.LOOKUP_DELIMITER 35 | ); 36 | if (!includesLookupDelimiter) { 37 | return LookupFilter.EXACT; 38 | } 39 | return query.key.split(LookupDelimiter.LOOKUP_DELIMITER)[ 40 | hasNotOperator ? 2 : 1 41 | ] as LookupFilter; 42 | } 43 | 44 | private getProp(query: FilterFactoryQuery) { 45 | return query.key.split(LookupDelimiter.LOOKUP_DELIMITER)[0]; 46 | } 47 | 48 | private hasNotOperator(query: FilterFactoryQuery) { 49 | return query.key.includes( 50 | `${LookupDelimiter.LOOKUP_DELIMITER}${LookupFilter.NOT}` 51 | ); 52 | } 53 | 54 | private isFieldFilter(key: string): boolean { 55 | if (!key.includes(LookupDelimiter.RELATION_DELIMITER)) { 56 | return true; 57 | } 58 | return false; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tools/scripts/publish.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a minimal script to publish your package to "npm". 3 | * This is meant to be used as-is or customize as you see fit. 4 | * 5 | * This script is executed on "dist/path/to/library" as "cwd" by default. 6 | * 7 | * You might need to authenticate with NPM before running this script. 8 | */ 9 | 10 | import { readCachedProjectGraph } from '@nrwl/devkit'; 11 | import { execSync } from 'child_process'; 12 | import { readFileSync, writeFileSync } from 'fs'; 13 | import chalk from 'chalk'; 14 | 15 | function invariant(condition, message) { 16 | if (!condition) { 17 | console.error(chalk.bold.red(message)); 18 | process.exit(1); 19 | } 20 | } 21 | 22 | // Executing publish script: node path/to/publish.mjs {name} --version {version} --tag {tag} 23 | // Default "tag" to "next" so we won't publish the "latest" tag by accident. 24 | const [, , name, version, tag = 'next'] = process.argv; 25 | 26 | // A simple SemVer validation to validate the version 27 | const validVersion = /^\d+\.\d+\.\d+(-\w+\.\d+)?/; 28 | invariant( 29 | version && validVersion.test(version), 30 | `No version provided or version did not match Semantic Versioning, expected: #.#.#-tag.# or #.#.#, got ${version}.` 31 | ); 32 | 33 | const graph = readCachedProjectGraph(); 34 | const project = graph.nodes[name]; 35 | 36 | invariant( 37 | project, 38 | `Could not find project "${name}" in the workspace. Is the project.json configured correctly?` 39 | ); 40 | 41 | const outputPath = project.data?.targets?.build?.options?.outputPath; 42 | invariant( 43 | outputPath, 44 | `Could not find "build.options.outputPath" of project "${name}". Is project.json configured correctly?` 45 | ); 46 | 47 | process.chdir(outputPath); 48 | 49 | // Updating the version in "package.json" before publishing 50 | try { 51 | const json = JSON.parse(readFileSync(`package.json`).toString()); 52 | json.version = version; 53 | writeFileSync(`package.json`, JSON.stringify(json, null, 2)); 54 | } catch (e) { 55 | console.error( 56 | chalk.bold.red(`Error reading package.json file from library build output.`) 57 | ); 58 | } 59 | 60 | // Execute "npm publish" to publish 61 | execSync(`npm publish --access public --tag ${tag}`); 62 | -------------------------------------------------------------------------------- /packages/express-query-adapter/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). 4 | 5 | ### [0.1.1](https://github.com/Tool-Kid/express-query-adapter/compare/express-query-adapter-0.1.0...express-query-adapter-0.1.1) (2022-11-23) 6 | 7 | ## 0.1.0 (2022-11-23) 8 | 9 | 10 | ### Features 11 | 12 | * CI/CD ([#409](https://github.com/Tool-Kid/express-query-adapter/issues/409)) ([5ca9cc1](https://github.com/Tool-Kid/express-query-adapter/commit/5ca9cc15ce5058d807895218a87f45103f0e5362)), closes [#403](https://github.com/Tool-Kid/express-query-adapter/issues/403) [#408](https://github.com/Tool-Kid/express-query-adapter/issues/408) 13 | * use dynamic imports ([889c148](https://github.com/Tool-Kid/express-query-adapter/commit/889c1482548a1508e86e8ca96fff68724c85a8d9)) 14 | 15 | 16 | ### Bug Fixes 17 | 18 | * build fixes ([6124ca7](https://github.com/Tool-Kid/express-query-adapter/commit/6124ca7becb245086245cf4095c647c50db333b0)) 19 | * Details ([#410](https://github.com/Tool-Kid/express-query-adapter/issues/410)) ([8a0a1c5](https://github.com/Tool-Kid/express-query-adapter/commit/8a0a1c5280e4e08004bec86c8853710fd33a6503)) 20 | * prefix tag ([bac9b32](https://github.com/Tool-Kid/express-query-adapter/commit/bac9b32dba4b42e123098cdff0d7dad8220726c7)) 21 | * remove preid ([32863db](https://github.com/Tool-Kid/express-query-adapter/commit/32863db703d87486b15eb00a4dd1f37318b6518f)) 22 | 23 | ## 0.1.0 (2022-11-23) 24 | 25 | 26 | ### Features 27 | 28 | * CI/CD ([#409](https://github.com/Tool-Kid/express-query-adapter/issues/409)) ([5ca9cc1](https://github.com/Tool-Kid/express-query-adapter/commit/5ca9cc15ce5058d807895218a87f45103f0e5362)), closes [#403](https://github.com/Tool-Kid/express-query-adapter/issues/403) [#408](https://github.com/Tool-Kid/express-query-adapter/issues/408) 29 | * use dynamic imports ([889c148](https://github.com/Tool-Kid/express-query-adapter/commit/889c1482548a1508e86e8ca96fff68724c85a8d9)) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * build fixes ([6124ca7](https://github.com/Tool-Kid/express-query-adapter/commit/6124ca7becb245086245cf4095c647c50db333b0)) 35 | * Details ([#410](https://github.com/Tool-Kid/express-query-adapter/issues/410)) ([8a0a1c5](https://github.com/Tool-Kid/express-query-adapter/commit/8a0a1c5280e4e08004bec86c8853710fd33a6503)) 36 | -------------------------------------------------------------------------------- /packages/express-query-adapter/test/adapters/typeorm/options/pagination.spec.ts: -------------------------------------------------------------------------------- 1 | import { ConfigProfile } from '../../../../src/profile/config-profile'; 2 | import { ENABLED_PROFILE } from '../../../../src/profile/defaults'; 3 | import { TypeORMQueryBuilder } from '../../../../src/typeorm/query-builder'; 4 | 5 | describe('Test Profiles for ', () => { 6 | let qb: TypeORMQueryBuilder; 7 | let profile: ConfigProfile; 8 | 9 | beforeEach(() => { 10 | profile = ENABLED_PROFILE; 11 | }); 12 | it('should paginate with default "itemsPerPage" when equals to "enabled"', () => { 13 | profile.options.pagination.status = 'enabled'; 14 | qb = new TypeORMQueryBuilder(profile); 15 | expect(qb.build({})).toEqual({ 16 | skip: 0, 17 | take: 25, 18 | }); 19 | }); 20 | 21 | it('should paginate with default "itemsPerPage" when equals to "enabled" and "paginate" equals to true', () => { 22 | profile.options.pagination.status = 'enabled'; 23 | qb = new TypeORMQueryBuilder(profile); 24 | expect(qb.build({ pagination: true })).toEqual({ 25 | skip: 0, 26 | take: 25, 27 | }); 28 | }); 29 | it('should paginate with default "itemsPerPage" when equals to "enabled" and "paginate" equals to undefined', () => { 30 | profile.options.pagination.status = 'enabled'; 31 | qb = new TypeORMQueryBuilder(profile); 32 | expect(qb.build({ pagination: undefined })).toEqual({ 33 | skip: 0, 34 | take: 25, 35 | }); 36 | }); 37 | it('should not paginate when equals to "disabled"', () => { 38 | profile.options.pagination.status = 'disabled'; 39 | qb = new TypeORMQueryBuilder(profile); 40 | expect(qb.build({})).toEqual({}); 41 | }); 42 | it('should paginate with custom "itemsPerPage" when equals to "enabled"', () => { 43 | profile.options.pagination.status = 'enabled'; 44 | profile.options.pagination.itemsPerPage = 50; 45 | qb = new TypeORMQueryBuilder(profile); 46 | expect(qb.build({})).toEqual({ 47 | skip: 0, 48 | take: 50, 49 | }); 50 | }); 51 | it('should not paginate when equals to "enabled" and "paginate" equals to false', () => { 52 | profile.options.pagination.status = 'enabled'; 53 | qb = new TypeORMQueryBuilder(profile); 54 | expect(qb.build({ pagination: false })).toEqual({}); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /packages/express-query-adapter/test/integration/express/express.spec.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as request from 'supertest'; 3 | import { TypeORMQueryBuilder } from '../../../src/typeorm/query-builder'; 4 | import { Like } from 'typeorm'; 5 | import { Server } from 'http'; 6 | 7 | describe('Test Express integration', () => { 8 | let server: Server; 9 | 10 | beforeAll((done) => { 11 | const app = express(); 12 | app.use(express.json()); 13 | app.use(express.urlencoded({ extended: true })); 14 | app.get('/get', (req, res) => { 15 | const queryBuilder = new TypeORMQueryBuilder(); 16 | const built = queryBuilder.build(req.query); 17 | res.send(built); 18 | }); 19 | app.post('/post_urlquery', (req, res) => { 20 | const queryBuilder = new TypeORMQueryBuilder(); 21 | const built = queryBuilder.build(req.query); 22 | res.send(built); 23 | }); 24 | app.post('/post_body', (req, res) => { 25 | const queryBuilder = new TypeORMQueryBuilder(); 26 | const built = queryBuilder.build(req.body); 27 | res.send(built); 28 | }); 29 | server = app.listen(3000, () => { 30 | done(); 31 | }); 32 | }); 33 | 34 | afterAll(() => { 35 | server.close(); 36 | }); 37 | 38 | it('should return an appropiate query built for GET /get?...', (done) => { 39 | request(server) 40 | .get('/get?name=rjlopezdev&email__contains=@gmail.com') 41 | .expect(200) 42 | .end((err, res) => { 43 | expect(JSON.parse(res.text)).toEqual({ 44 | where: { 45 | name: 'rjlopezdev', 46 | email: Like('%@gmail.com%'), 47 | }, 48 | skip: 0, 49 | take: 25, 50 | }); 51 | done(); 52 | }); 53 | }); 54 | 55 | it('should return an appropiate query built for POST /post_urlquery?...', (done) => { 56 | request(server) 57 | .post('/post_urlquery?name=rjlopezdev&email__contains=@gmail.com') 58 | .expect(200) 59 | .end((err, res) => { 60 | expect(JSON.parse(res.text)).toEqual({ 61 | where: { 62 | name: 'rjlopezdev', 63 | email: Like('%@gmail.com%'), 64 | }, 65 | skip: 0, 66 | take: 25, 67 | }); 68 | done(); 69 | }); 70 | }); 71 | 72 | it('should return an appropiate query built for POST /post_body, body: {...}', (done) => { 73 | request(server) 74 | .post('/post_body') 75 | .send({ 76 | name: 'rjlopezdev', 77 | email__contains: '@gmail.com', 78 | }) 79 | .expect(200) 80 | .end((err, res) => { 81 | expect(JSON.parse(res.text)).toEqual({ 82 | where: { 83 | name: 'rjlopezdev', 84 | email: Like('%@gmail.com%'), 85 | }, 86 | skip: 0, 87 | take: 25, 88 | }); 89 | done(); 90 | }); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /packages/express-query-adapter/test/adapters/typeorm/field/field-filter.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Like, 3 | ILike, 4 | IsNull, 5 | MoreThan, 6 | MoreThanOrEqual, 7 | LessThanOrEqual, 8 | LessThan, 9 | Between, 10 | In, 11 | Not, 12 | } from 'typeorm'; 13 | import { FieldFilter } from '../../../../src/typeorm/filter/field/field-filter'; 14 | import { LookupFilter } from '../../../../src/typeorm/filter/field/lookup.enum'; 15 | 16 | describe('Test FieldFilter #buildQuery', () => { 17 | const built: any = {}; 18 | 19 | it('should return an filter', () => { 20 | const fieldFilter = new FieldFilter({ 21 | query: built, 22 | prop: 'name', 23 | lookup: LookupFilter.EXACT, 24 | value: 'value', 25 | }); 26 | fieldFilter.buildQuery(); 27 | expect(built['where']['name']).toBe('value'); 28 | }); 29 | 30 | it('should return a filter', () => { 31 | const fieldFilter = new FieldFilter({ 32 | query: built, 33 | prop: 'name', 34 | lookup: LookupFilter.CONTAINS, 35 | value: 'value', 36 | }); 37 | fieldFilter.buildQuery(); 38 | expect(built['where']['name']).toEqual(Like('%value%')); 39 | }); 40 | 41 | it('should return an contains filter', () => { 42 | const fieldFilter = new FieldFilter({ 43 | query: built, 44 | prop: 'name', 45 | lookup: LookupFilter.STARTS_WITH, 46 | value: 'value', 47 | }); 48 | fieldFilter.buildQuery(); 49 | expect(built['where']['name']).toEqual(Like('value%')); 50 | }); 51 | 52 | it('should return an filter', () => { 53 | const fieldFilter = new FieldFilter({ 54 | query: built, 55 | prop: 'name', 56 | lookup: LookupFilter.ENDS_WITH, 57 | value: 'value', 58 | }); 59 | fieldFilter.buildQuery(); 60 | expect(built['where']['name']).toEqual(Like('%value')); 61 | }); 62 | 63 | it('should return a filter', () => { 64 | const fieldFilter = new FieldFilter({ 65 | query: built, 66 | prop: 'name', 67 | lookup: LookupFilter.ICONTAINS, 68 | value: 'value', 69 | }); 70 | fieldFilter.buildQuery(); 71 | expect(built['where']['name']).toEqual(ILike('%value%')); 72 | }); 73 | 74 | it('should return an contains filter', () => { 75 | const fieldFilter = new FieldFilter({ 76 | query: built, 77 | prop: 'name', 78 | lookup: LookupFilter.ISTARTS_WITH, 79 | value: 'value', 80 | }); 81 | fieldFilter.buildQuery(); 82 | expect(built['where']['name']).toEqual(ILike('value%')); 83 | }); 84 | 85 | it('should return an filter', () => { 86 | const fieldFilter = new FieldFilter({ 87 | query: built, 88 | prop: 'name', 89 | lookup: LookupFilter.IENDS_WITH, 90 | value: 'value', 91 | }); 92 | fieldFilter.buildQuery(); 93 | expect(built['where']['name']).toEqual(ILike('%value')); 94 | }); 95 | 96 | it('should return an filter', () => { 97 | const fieldFilter = new FieldFilter({ 98 | query: built, 99 | prop: 'name', 100 | lookup: LookupFilter.IS_NULL, 101 | value: 'value', 102 | }); 103 | fieldFilter.buildQuery(); 104 | expect(built['where']['name']).toEqual(IsNull()); 105 | }); 106 | 107 | it('should return an filter', () => { 108 | const fieldFilter = new FieldFilter({ 109 | query: built, 110 | prop: 'name', 111 | lookup: LookupFilter.GT, 112 | value: '2', 113 | }); 114 | fieldFilter.buildQuery(); 115 | expect(built['where']['name']).toEqual(MoreThan('2')); 116 | }); 117 | 118 | it('should return a filter', () => { 119 | const fieldFilter = new FieldFilter({ 120 | query: built, 121 | prop: 'name', 122 | lookup: LookupFilter.GTE, 123 | value: '2', 124 | }); 125 | fieldFilter.buildQuery(); 126 | expect(built['where']['name']).toEqual(MoreThanOrEqual('2')); 127 | }); 128 | 129 | it('should return a filter', () => { 130 | const fieldFilter = new FieldFilter({ 131 | query: built, 132 | prop: 'name', 133 | lookup: LookupFilter.LT, 134 | value: '2', 135 | }); 136 | fieldFilter.buildQuery(); 137 | expect(built['where']['name']).toEqual(LessThan('2')); 138 | }); 139 | 140 | it('should return a filter', () => { 141 | const fieldFilter = new FieldFilter({ 142 | query: built, 143 | prop: 'name', 144 | lookup: LookupFilter.LTE, 145 | value: '2', 146 | }); 147 | fieldFilter.buildQuery(); 148 | expect(built['where']['name']).toEqual(LessThanOrEqual('2')); 149 | }); 150 | 151 | it('should return a filter', () => { 152 | const fieldFilter = new FieldFilter({ 153 | query: built, 154 | prop: 'name', 155 | lookup: LookupFilter.BETWEEN, 156 | value: '1,10', 157 | }); 158 | fieldFilter.buildQuery(); 159 | expect(built['where']['name']).toEqual(Between(1, 10)); 160 | }); 161 | 162 | it('should return a filter for dates', () => { 163 | const fieldFilter = new FieldFilter({ 164 | query: built, 165 | prop: 'date', 166 | lookup: LookupFilter.BETWEEN, 167 | value: '2022-10-10,2022-11-11', 168 | }); 169 | fieldFilter.buildQuery(); 170 | expect(built['where']['date']).toEqual( 171 | Between(new Date('2022-10-10'), new Date('2022-11-11')) 172 | ); 173 | }); 174 | 175 | it('should return a filter', () => { 176 | const fieldFilter = new FieldFilter({ 177 | query: built, 178 | prop: 'name', 179 | lookup: LookupFilter.IN, 180 | value: '1,2,3,4,foo', 181 | }); 182 | fieldFilter.buildQuery(); 183 | expect(built['where']['name']).toEqual(In(['1', '2', '3', '4', 'foo'])); 184 | }); 185 | 186 | it('should return a filter', () => { 187 | const fieldFilter = new FieldFilter({ 188 | query: built, 189 | prop: 'name', 190 | lookup: LookupFilter.EXACT, 191 | value: 'value', 192 | notOperator: true, 193 | }); 194 | fieldFilter.buildQuery(); 195 | expect(built['where']['name']).toEqual(Not('value')); 196 | }); 197 | }); 198 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Express Query Adapter logo 3 |

4 | 5 |

Express Query Adapter

6 | 7 |

8 | Easily transform an Express req.query into your favourite query tool 9 |
10 |
11 | 12 |
13 |
14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 | Contributing 24 | · 25 | License 26 |

27 | 28 | # Installation 29 | 30 | `npm install @tool-kid/express-query-adapter` 31 | 32 | # How it works? 33 | 34 | ![](https://raw.githubusercontent.com/Tool-Kid/express-query-adapter/main/express-adapter-pipeline.png) 35 | 36 | # Usage 37 | 38 | Use `getQueryBuilder` exported from package and pass your `req.query` as an argument: 39 | 40 | ```typescript 41 | import { getQueryBuilder } from '@tool-kid/express-query-adapter'; 42 | 43 | const builder = await getQueryBuilder({ adapter: 'typeorm' }); 44 | const builtQuery = builder.build(req.query); 45 | // Now your query is built, pass it to your favourite tool 46 | const results = await fooRepository.find(builtQuery); 47 | ``` 48 | 49 | # Adapters 50 | - [TypeORM](#typeorm) 51 | ## TypeORM 52 | 53 | Given the following url query string: 54 | 55 | `foo/?name__contains=foo&role__in=admin,common&age__gte=18&page=3&limit=10` 56 | 57 | It will be transformed into: 58 | 59 | ```typescript 60 | { 61 | where: { 62 | foo: Like('%foo%'), 63 | role: In(['admin', 'common']), 64 | age: MoreThanOrEqual(18) 65 | }, 66 | skip: 20, 67 | take: 10 68 | } 69 | ``` 70 | 71 | ## Different ways of retrieve data 72 | 73 | ### GET, POST method by url query string 74 | 75 | `GET foo/?name__contains=foo&role__in=admin,common&age__gte=18&page=3&limit=10` 76 | 77 | `POST foo/?name__contains=foo&role__in=admin,common&age__gte=18&page=3&limit=10` 78 | 79 | ```javascript 80 | app.get('/foo', (req, res) => { 81 | const qb = await getQueryBuilder({ adapter: 'typeorm' }); 82 | const built = qb.build(req.query); // => Parsed into req.query 83 | }); 84 | ``` 85 | 86 | ### POST method by body 87 | 88 | ```javascript 89 | POST foo/, body: { 90 | "name__contains": "foo", 91 | "role__in": "admin,common", 92 | "age__gte": 18, 93 | "page": 3, 94 | "limit": 10 95 | } 96 | ``` 97 | 98 | ```javascript 99 | app.post('/foo', (req, res) => { 100 | const qb = await getQueryBuilder({ adapter: 'typeorm' }); 101 | const built = qb.build(req.query); // => Parsed into req.body 102 | }); 103 | ``` 104 | 105 | ## Available Lookups 106 | 107 | | Lookup | Behaviour | Example | 108 | | --------------- | ----------------------------------------------------------- | ---------------------- | 109 | | _(none)_ | Return entries that match with value | `foo=raul` | 110 | | **contains** | Return entries that contains value | `foo__contains=lopez` | 111 | | **startswith** | Return entries that starts with value | `foo__startswith=r` | 112 | | **endswith** | Return entries that ends with value | `foo__endswith=dev` | 113 | | **icontains** | Return entries that contains value and ignoring case | `foo__icontains=Lopez` | 114 | | **istartswith** | Return entries that starts with value and ignoring case | `foo__istartswith=R` | 115 | | **iendswith** | Return entries that ends with value and ignoring case | `foo__iendswith=Dev` | 116 | | **isnull** | Return entries with null value | `foo__isnull` | 117 | | **lt** | Return entries with value less than or equal to provided | `foo__lt=18` | 118 | | **lte** | Return entries with value less than provided | `foo__lte=18` | 119 | | **gt** | Returns entries with value greater than provided | `foo__gt=18` | 120 | | **gte** | Return entries with value greater than or equal to provided | `foo__gte=18` | 121 | | **in** | Return entries that match with values in list | `foo__in=admin,common` | 122 | | **between** | Return entries in range (numeric, dates) | `foo__between=1,27` | 123 | 124 | **Notice**: you can use negative logic prefixing lookup with `__not`. 125 | 126 | _Example:_ 127 | `foo__not__contains=value` 128 | 129 | ## Options 130 | 131 | ### Pagination 132 | 133 | | Option | Default | Behaviour | Example | 134 | | ---------- | :------: | ----------------------------------------------------------- | ------------------ | 135 | | pagination | **true** | If _true_, paginate results. If _false_, disable pagination | `pagination=false` | 136 | | page | **1** | Return entries for page `page` | `page=2` | 137 | | limit | **25** | Return entries for page `page` paginated by size `limit` | `limit=15` | 138 | 139 | ### Ordering 140 | 141 | | Option | Default | Behaviour | Example | 142 | | ------ | :-----: | -------------------------------------------------------- | --------------------------- | 143 | | order | - | Order for fields:
`+`: Ascendant
`-`: Descendant | `order=+foo,-name,+surname` | 144 | 145 | ### Selection 146 | 147 | | Option | Default | Behaviour | Example | 148 | | ------ | :-----: | ------------------------------------------------------------------- | -------------------------------- | 149 | | select | - | Fields to select as response. If no provided, it select all fields. | `select=name,surname,foo.nested` | 150 | | with | - | Entity relations to attach to query | `with=posts,comments` | 151 | 152 | # Profile 153 | 154 | If you need to disable some capabilities, you can do using shortcuts to `enable|disable` by default or provide a custom Profile. 155 | 156 | A Profile describe capabilities that can be used by clients & its behaviour. 157 | 158 | ```typescript 159 | const qb = getQueryBuilder({ adapter: 'typeorm', profile: 'enabled' | 'disabled' | ConfigProgile }); 160 | const builtQuery = builder.build(req.query); 161 | ``` 162 | 163 | ## ConfigProfile 164 | 165 | `ConfigProfile` object looks like: 166 | 167 | ```typescript 168 | const customProfile: ConfigProfile = { 169 | options: { 170 | pagination: { 171 | status: 'enabled', 172 | paginate: true, 173 | itemsPerPage: 25, 174 | }, 175 | ordering: { 176 | status: 'enabled', 177 | }, 178 | relations: { 179 | status: 'enabled', 180 | }, 181 | select: { 182 | status: 'enabled', 183 | }, 184 | }, 185 | policy: 'skip', 186 | }; 187 | ``` 188 | 189 | | Field | Default | Behaviour | Type | 190 | | ------- | :-------: | ---------------------------------------------------------- | ---------------- | 191 | | options | 'enabled' | Profile options | `ProfileOptions` | 192 | | policy | 'skip' | Policy to apply in cases client try use `disabled` options | `FindPolicyType` | 193 | --------------------------------------------------------------------------------