Repository: nestjsx/crud
Branch: master
Commit: d6d3c4ebd844
Files: 201
Total size: 379.5 KB
Directory structure:
gitextract_rxpb3oso/
├── .eslintignore
├── .eslintrc.js
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── Bug_report.md
│ │ └── Feature_request.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ └── workflows/
│ ├── release.yml
│ └── tests.yml
├── .gitignore
├── .husky/
│ └── pre-commit
├── .prettierrc.json
├── .yarnrc
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── docker-compose.yml
├── integration/
│ ├── crud-typeorm/
│ │ ├── app.module.ts
│ │ ├── auth.guard.ts
│ │ ├── base-entity.ts
│ │ ├── companies/
│ │ │ ├── companies.controller.ts
│ │ │ ├── companies.module.ts
│ │ │ ├── companies.service.ts
│ │ │ ├── company.entity.ts
│ │ │ ├── index.ts
│ │ │ ├── requests/
│ │ │ │ ├── create-company.dto.ts
│ │ │ │ └── index.ts
│ │ │ └── responses/
│ │ │ ├── get-company-response.dto.ts
│ │ │ └── index.ts
│ │ ├── constants.ts
│ │ ├── devices/
│ │ │ ├── device.entity.ts
│ │ │ ├── devices.controller.ts
│ │ │ ├── devices.module.ts
│ │ │ ├── devices.service.ts
│ │ │ ├── index.ts
│ │ │ └── response/
│ │ │ ├── delete-device-response.dto.ts
│ │ │ └── index.ts
│ │ ├── main.ts
│ │ ├── notes/
│ │ │ ├── index.ts
│ │ │ ├── note.entity.ts
│ │ │ ├── notes.controller.ts
│ │ │ ├── notes.module.ts
│ │ │ ├── notes.service.ts
│ │ │ ├── requests/
│ │ │ │ ├── create-note.dto.ts
│ │ │ │ └── index.ts
│ │ │ └── responses/
│ │ │ ├── get-note-response.dto.ts
│ │ │ └── index.ts
│ │ ├── orm.config.ts
│ │ ├── orm.mysql.ts
│ │ ├── orm.postgres.ts
│ │ ├── orm.yaml
│ │ ├── projects/
│ │ │ ├── index.ts
│ │ │ ├── my-projects.controller.ts
│ │ │ ├── project.entity.ts
│ │ │ ├── projects.controller.ts
│ │ │ ├── projects.module.ts
│ │ │ ├── projects.service.ts
│ │ │ ├── user-project.entity.ts
│ │ │ └── user-projects.service.ts
│ │ ├── seeds.ts
│ │ ├── users/
│ │ │ ├── index.ts
│ │ │ ├── me.controller.ts
│ │ │ ├── user.entity.ts
│ │ │ ├── users.controller.ts
│ │ │ ├── users.module.ts
│ │ │ └── users.service.ts
│ │ ├── users-licenses/
│ │ │ ├── index.ts
│ │ │ ├── license.entity.ts
│ │ │ └── user-license.entity.ts
│ │ └── users-profiles/
│ │ ├── index.ts
│ │ └── user-profile.entity.ts
│ └── shared/
│ └── https-exception.filter.ts
├── jest.config.js
├── lerna.json
├── mrepo.json
├── package.json
├── packages/
│ ├── crud/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── constants.ts
│ │ │ ├── crud/
│ │ │ │ ├── crud-routes.factory.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── reflection.helper.ts
│ │ │ │ ├── serialize.helper.ts
│ │ │ │ ├── swagger.helper.ts
│ │ │ │ └── validation.helper.ts
│ │ │ ├── decorators/
│ │ │ │ ├── crud-auth.decorator.ts
│ │ │ │ ├── crud.decorator.ts
│ │ │ │ ├── feature-action.decorator.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── override.decorator.ts
│ │ │ │ ├── parsed-body.decorator.ts
│ │ │ │ └── parsed-request.decorator.ts
│ │ │ ├── enums/
│ │ │ │ ├── crud-actions.enum.ts
│ │ │ │ ├── crud-validation-groups.enum.ts
│ │ │ │ └── index.ts
│ │ │ ├── index.ts
│ │ │ ├── interceptors/
│ │ │ │ ├── crud-base.interceptor.ts
│ │ │ │ ├── crud-request.interceptor.ts
│ │ │ │ ├── crud-response.interceptor.ts
│ │ │ │ └── index.ts
│ │ │ ├── interfaces/
│ │ │ │ ├── auth-options.interface.ts
│ │ │ │ ├── base-route.interface.ts
│ │ │ │ ├── create-many-dto.interface.ts
│ │ │ │ ├── crud-controller.interface.ts
│ │ │ │ ├── crud-global-config.interface.ts
│ │ │ │ ├── crud-options.interface.ts
│ │ │ │ ├── crud-request.interface.ts
│ │ │ │ ├── dto-options.interface.ts
│ │ │ │ ├── get-many-default-response.interface.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── model-options.interface.ts
│ │ │ │ ├── params-options.interface.ts
│ │ │ │ ├── query-options.interface.ts
│ │ │ │ ├── routes-options.interface.ts
│ │ │ │ └── serialize-options.interface.ts
│ │ │ ├── module/
│ │ │ │ ├── crud-config.service.ts
│ │ │ │ └── index.ts
│ │ │ ├── services/
│ │ │ │ ├── crud-service.abstract.ts
│ │ │ │ └── index.ts
│ │ │ ├── types/
│ │ │ │ ├── base-route-name.type.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── query-filter-option.type.ts
│ │ │ └── util.ts
│ │ ├── test/
│ │ │ ├── __fixture__/
│ │ │ │ ├── dto/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── test-create.dto.ts
│ │ │ │ │ └── test-update.dto.ts
│ │ │ │ ├── exception.filter.ts
│ │ │ │ ├── models/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── test-serialize-2.model.ts
│ │ │ │ │ ├── test-serialize.model.ts
│ │ │ │ │ └── test.model.ts
│ │ │ │ ├── response/
│ │ │ │ │ ├── delete-model-response.dto.ts
│ │ │ │ │ ├── get-many-model-response.dto.ts
│ │ │ │ │ ├── get-model-response.dto.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── recover-model-response.dto.ts
│ │ │ │ └── services/
│ │ │ │ ├── index.ts
│ │ │ │ ├── test-serialize.service.ts
│ │ │ │ └── test.service.ts
│ │ │ ├── crud-config.service.global.spec.ts
│ │ │ ├── crud-config.service.spec.ts
│ │ │ ├── crud-request.interceptor.spec.ts
│ │ │ ├── crud-service.abstract.spec.ts
│ │ │ ├── crud.decorator.base.spec.ts
│ │ │ ├── crud.decorator.exclude.spec.ts
│ │ │ ├── crud.decorator.options.spec.ts
│ │ │ ├── crud.decorator.override.spec.ts
│ │ │ ├── crud.decorator.soft.spec.ts
│ │ │ ├── crud.dto.options.spec.ts
│ │ │ ├── crud.serialize.options.spec.ts
│ │ │ └── feature-action.decorator.spec.ts
│ │ └── tsconfig.json
│ ├── crud-request/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── exceptions/
│ │ │ │ ├── index.ts
│ │ │ │ └── request-query.exception.ts
│ │ │ ├── index.ts
│ │ │ ├── interfaces/
│ │ │ │ ├── create-query-params.interface.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── params-options.interface.ts
│ │ │ │ ├── parsed-request.interface.ts
│ │ │ │ └── request-query-builder-options.interface.ts
│ │ │ ├── request-query.builder.ts
│ │ │ ├── request-query.parser.ts
│ │ │ ├── request-query.validator.ts
│ │ │ └── types/
│ │ │ ├── index.ts
│ │ │ ├── request-param.types.ts
│ │ │ └── request-query.types.ts
│ │ ├── test/
│ │ │ ├── request-query.builder.spec.ts
│ │ │ ├── request-query.parser.spec.ts
│ │ │ └── request.query.validator.spec.ts
│ │ └── tsconfig.json
│ ├── crud-typeorm/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── index.ts
│ │ │ └── typeorm-crud.service.ts
│ │ ├── test/
│ │ │ ├── __fixture__/
│ │ │ │ ├── companies.service.ts
│ │ │ │ ├── devices.service.ts
│ │ │ │ ├── notes.service.ts
│ │ │ │ ├── projects.service.ts
│ │ │ │ └── users.service.ts
│ │ │ ├── a.params-options.spec.ts
│ │ │ ├── b.query-params.spec.ts
│ │ │ ├── c.basic-crud.spec.ts
│ │ │ └── d.crud-auth.spec.ts
│ │ └── tsconfig.json
│ └── util/
│ ├── README.md
│ ├── package.json
│ ├── src/
│ │ ├── checks.util.ts
│ │ ├── index.ts
│ │ ├── obj.util.ts
│ │ └── types/
│ │ ├── class.type.ts
│ │ ├── index.ts
│ │ └── object-literal.type.ts
│ ├── test/
│ │ ├── checks.util.spec.ts
│ │ └── obj.util.spec.ts
│ └── tsconfig.json
├── tsconfig.eslint.json
├── tsconfig.jest.json
├── tsconfig.json
└── tslint.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintignore
================================================
================================================
FILE: .eslintrc.js
================================================
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.eslint.json',
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: ['plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-var-requires': 'off',
'lines-between-class-members': ['error', 'always'],
'padded-blocks': ['error', 'never'],
semi: ['error', 'always'],
quotes: ['error', 'single', { avoidEscape: true }],
'max-len': ['error', { code: 150, comments: 200 }],
'comma-dangle': ['error', 'always-multiline'],
'@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/member-ordering': [
'error',
{
default: [
'signature',
'public-static-field',
'protected-static-field',
'private-static-field',
'public-instance-field',
'protected-instance-field',
'private-instance-field',
'public-abstract-field',
'protected-abstract-field',
'private-abstract-field',
'public-constructor',
'protected-constructor',
'private-constructor',
'public-static-method',
'protected-static-method',
'private-static-method',
'public-instance-method',
'protected-instance-method',
'private-instance-method',
'public-abstract-method',
'protected-abstract-method',
'private-abstract-method',
],
},
],
},
};
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: nestjsx
ko_fi: # Replace with a single Ko-fi username
tidelift: npm/@nestjsx/crud
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with a single custom sponsorship URL
================================================
FILE: .github/ISSUE_TEMPLATE/Bug_report.md
================================================
---
name: "\U0001F41B Bug Report"
about: "If something isn't working as expected \U0001F914."
title: ''
labels: 'type: potential issue'
assignees: ''
---
## Bug Report
## Current behavior
## Input Code
```ts
const your = (code) => here;
```
## Expected behavior
## Possible Solution
## Environment
Package version: X.Y.Z
For Tooling issues:
- Node version: XX
- Platform:
- Database
Others:
## Repository with minimal reproduction
================================================
FILE: .github/ISSUE_TEMPLATE/Feature_request.md
================================================
---
name: "\U0001F680 Feature Request"
about: "I have a suggestion \U0001F63B!"
title: ''
labels: 'type: enhancement'
assignees: ''
---
## Feature Request
## Is your feature request related to a problem? Please describe.
## Describe the solution you'd like
## Teachability, Documentation, Adoption, Migration Strategy
## What is the motivation / use case for changing the behavior?
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
## PR Checklist
Please check if your PR fulfills the following requirements:
- [ ] The commit message was generated by `yarn commit`
- [ ] Tests for the changes have been added (for bug fixes / features)
## PR Type
What kind of change does this PR introduce?
```
[ ] Bugfix
[ ] Feature
[ ] Code style update (formatting, local variables)
[ ] Refactoring (no functional changes, no api changes)
[ ] Build related changes
[ ] Tests
[ ] Release
[ ] CI related changes
[ ] Other... Please describe:
```
## What is the current behavior?
Issue Number: N/A
## What is the new behavior?
## Does this PR introduce a breaking change?
```
[ ] Yes
[ ] No
```
## Other information
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
pull_request:
types: [closed]
jobs:
release:
if: github.event.pull_request.merged && startsWith(github.head_ref, 'release')
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: '0'
- name: Set NPM Token
uses: actions/setup-node@v2
with:
registry-url: https://registry.npmjs.org
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Install
run: yarn boot
- name: Build
run: yarn build
- name: Run Tests
run: yarn test
- name: Get Latest Tag
id: get_latest_tag
uses: actions-ecosystem/action-get-latest-tag@v1
- name: Parse Latest Git Tag
id: parse_latest_tag
run: |
TAG=$(echo ${{ steps.get_latest_tag.outputs.tag }} | cut -d 'v' -f 2)
echo "::set-output name=tag::${TAG}"
- name: Get Next Tag Semver
id: get_next_tag_semver
uses: WyriHaximus/github-action-next-semvers@v1
with:
version: ${{ steps.parse_latest_tag.outputs.tag }}
- name: Define Publishing Strategy
id: lerna_strategy
run: |
MREPO_LERNA_STRATEGY=$(cat lerna.json | npx jase version)
echo "MREPO_LERNA_STRATEGY=${MREPO_LERNA_STRATEGY}" >> $GITHUB_ENV
- name: Define Current Version
run: |
echo ${{ env.MREPO_LERNA_STRATEGY }}
if [ "${{ env.MREPO_LERNA_STRATEGY }}" = "independent" ]; then
CURRENT_VERSION=${{ steps.get_next_tag_semver.outputs.v_minor }}
DIST_TAG=$(cat lerna.json | npx jase distTag)
else
CURRENT_VERSION=$(cat lerna.json | npx jase version)
[[ $CURRENT_VERSION =~ "-" ]] && DIST_TAG=$(echo $CURRENT_VERSION | cut -d '-' -f 2 | cut -d '.' -f 1) || DIST_TAG="latest"
CURRENT_VERSION=v$CURRENT_VERSION
fi
echo $CURRENT_VERSION
echo $DIST_TAG
echo "CURRENT_VERSION=${CURRENT_VERSION}" >> $GITHUB_ENV
echo "DIST_TAG=${DIST_TAG}" >> $GITHUB_ENV
- name: Ensure Latest Git Tag
uses: mukunku/tag-exists-action@v1.0.0
id: checkTag
with:
tag: ${{ env.CURRENT_VERSION }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create Git Tag
if: steps.checkTag.outputs.exists == 'false'
uses: negz/create-tag@v1
with:
version: ${{ env.CURRENT_VERSION }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Create Release
if: steps.checkTag.outputs.exists == 'false'
uses: actions/create-release@v1
with:
tag_name: ${{ env.CURRENT_VERSION }}
release_name: ${{ env.CURRENT_VERSION }}
body: |
${{ github.event.pull_request.body }}
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Publish Packages to NPM Registry
if: steps.checkTag.outputs.exists == 'false'
run: |
lerna publish from-package --no-git-reset --no-verify-access --yes --registry https://registry.npmjs.org --dist-tag ${{ env.DIST_TAG }}
git update-index --assume-unchanged ./packages/**/package.json
env:
GH_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Create a Comment
uses: actions/github-script@0.8.0
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Packages with version [${{ env.CURRENT_VERSION }}](https://github.com/${{ github.repository }}/releases/tag/${{ env.CURRENT_VERSION }}) have been released 🎉'
})
================================================
FILE: .github/workflows/tests.yml
================================================
name: Tests
on:
push:
branches: [master]
pull_request:
branches: [master]
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
env:
COMPOSE_FILE: ./docker-compose.yml
steps:
- uses: actions/checkout@v2
- name: build docker db
run: docker-compose up -d
- name: install
run: yarn boot
- name: build
run: yarn build
- name: check docker
run: docker-compose up -d
- name: tests
run: yarn test:coverage
- name: Coveralls Parallel
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.github_token }}
flag-name: run-${{ matrix.test_number }}
parallel: true
coverage:
needs: test
runs-on: ubuntu-latest
steps:
- name: Coveralls coverage
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.github_token }}
parallel-finished: true
================================================
FILE: .gitignore
================================================
# dependencies
/**/node_modules
yarn-error.log
npm-debug.log
# IDE
/.idea
/.awcache
/.vscode
# misc
.DS_Store
lerna-debug.log
.npmrc
.nvmrc
# tests
/coverage
/.nyc_output
# dist
packages/**/lib
packages/**/tsconfig.tsbuildinfo
.mrepo
================================================
FILE: .husky/pre-commit
================================================
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn lint && yarn format
================================================
FILE: .prettierrc.json
================================================
{
"printWidth": 120,
"singleQuote": true,
"trailingComma": "all",
"arrowParens": "always"
}
================================================
FILE: .yarnrc
================================================
save-prefix ""
================================================
FILE: CHANGELOG.md
================================================
## [4.6.2] - 2020-05-14
### Bug Fixes
- **typeorm** - fixed selected fields on joins [#510](https://github.com/nestjsx/crud/issues/510)). Kudos to @jbrousseau for finding this bug
## [4.6.1] - 2020-05-08
### Bug Fixes
- **typeorm** - fixed query generation when a column display name differs from its name in db [#401](https://github.com/nestjsx/crud/issues/401)). Kudos to @farhad2161 for finding this bug
## [4.6.0] - 2020-05-07
### Features
- **crud**/**typeorm** - added `select` (boolean) to `join` options which allows to join relation but not select it ([#218](https://github.com/nestjsx/crud/issues/218))
### Bug Fixes
- **typeorm** - fixed column identifier for MySQL ([#401](https://github.com/nestjsx/crud/issues/401))
- **typeorm** - fixed nested relations aliases, filtering, sorting ([#419](https://github.com/nestjsx/crud/issues/419), [#450](https://github.com/nestjsx/crud/issues/450), [#267](https://github.com/nestjsx/crud/issues/267), [#385](https://github.com/nestjsx/crud/issues/385))
## [4.5.0] - 2020-05-01
### Improvements
- **crud** - added enum support for params Swagger. Kudos to @tbrannam
### Bug Fixes
- **crud** - fixed auth property definition. Kudos to @lafeuil
- **typeorm** - fixed request generation with aliases ([#321](https://github.com/nestjsx/crud/issues/321), [#401](https://github.com/nestjsx/crud/issues/401)). Kudos to @joennlae
## [4.4.5] - 2020-04-18
## Deps
- **crud** fixted imports
## [4.4.4] - 2020-04-18
### Deps
- **dev** fixted lerna
## [4.4.3] - 2020-04-18
### Bug Fixes
- **crud** fixed returning `pageCount` in some cases ([#465](https://github.com/nestjsx/crud/pull/465))
- **typeorm** fixed critical bug with possible SQL injections when using query `?sort=` (big kudos to João Maurício)
- **typeorm** fixed filter conditions for LIKE/iLIKE operators ([#395](https://github.com/nestjsx/crud/pull/395))
## [4.4.2] - 2020-03-17
### Bug Fixes
- **crud** fixed custom routes params caused by NestJs v7 breaking changes ([#443](https://github.com/nestjsx/crud/issues/443))
## [4.4.1] - 2019-12-28
### Bug Fixes
- **crud** fixed `CrudRequestInterceptor` validation status code from 500 to 400 ([#374](https://github.com/nestjsx/crud/issues/374), [#247](https://github.com/nestjsx/crud/issues/247))
## [4.4.0] - 2019-12-27
### Features
- **crud** added `serialize` to the global options
## [4.3.0] - 2019-12-21
### Features
- **crud** added `dto` to the `CrudOptions` ([#132](https://github.com/nestjsx/crud/issues/132))
- **crud** added `serialize` to the `CrudOptions`
- **crud** added `search` query param and a new search condition api
- **crud** added new condition operators: `$eqL`, `$neL`, `$startsL`, `$endsL`, `$contL`, `$exclL`, `$inL`, `$notinL` for case insensitive queries ([#77](https://github.com/nestjsx/crud/issues/77))
- **crud** added `@crudAuth()` class decorator for authorized requests
### Improvements
- **crud** `CrudRequestInterceptor` can be used for both crud and non-crud controllers or for custom routes within crud controller
- **crud** support `@nestjs/swagger` major versions: v3 and v4 ([#340](https://github.com/nestjsx/crud/issues/340))
- **crud** added `returnShallow` option to the `CrudOptions.routes` `createOneBase`, `updateOneBase`, `replaceOneBase` methods ([#158](https://github.com/nestjsx/crud/issues/158))
- **crud** added `alias` to the `CrudOptions.join` ([#350](https://github.com/nestjsx/crud/issues/55))
- **crud** added `alwaysPaginate` to the `CrudOptions.query`, can be used globally as well ([#213](https://github.com/nestjsx/crud/issues/213))
- **crud** `CrudOptions.query.filter` can be a function that returns transformed `search` object
- **crud** added `disabled` for an objects withing `CrudOptions.params`
- **request** query builder: now uses [qs](https://www.npmjs.com/package/qs) package
- **request** query builder: `filter` and `or` methods can accept array of filter objects
- **typeorm** changed visibility of all methods ([#226](https://github.com/nestjsx/crud/issues/226))
- **typeorm** use `ILIKE` for PostgreSQL ([#212](https://github.com/nestjsx/crud/issues/212))
### Bug Fixes
- **crud** swagger: fixed response models ([#350](https://github.com/nestjsx/crud/issues/350))
- **crud** swagger: fixed query params ([#196](https://github.com/nestjsx/crud/issues/196))
- **crud** swagger: fixed renamed params ([#283](https://github.com/nestjsx/crud/issues/283))
- **crud** swagger: fixed swagger method decoration on overridden methods
- **crud** query parser: fixed parsing integers when it's a big int
- **typeorm** fixed load embedded entities ([#138](https://github.com/nestjsx/crud/issues/138))
- **typeorm** fixed left join issues ([#31](https://github.com/nestjsx/crud/issues/31), [#98](https://github.com/nestjsx/crud/issues/98))
- **typeorm** fixed composite key joins ([#238](https://github.com/nestjsx/crud/issues/238))
- **typeorm** fixed entity events ([#51](https://github.com/nestjsx/crud/issues/51))
- **typeorm** all methods return entity instances ([#259](https://github.com/nestjsx/crud/issues/259))
## [4.2.0] - 2019-07-26
### Features
- **crud** added support for older versions of `UUID` ([#186])
### Bug Fixes
- **crud** fixed `BulkDto` swagger description ([#159])
- **crud** fixed `CrudRequestInterceptor` request parsing
- **requests** added `@nestjsx/util` as a dependency ([#184])
- **requests** fixed condition operators mapping ([#148])
- **requests** fixed ISO date string validation ([#161])
- **typeorm** fixed filtering and sorting by nested fields ([#105])
- **typeorm** fixed `too many nested levels` exception ([#87])
- **typeorm** fixed pagination `pageCount` ([#179])
### Deps
- **dev** updated deps
## [4.1.0] - 2019-06-27
### Features
- **crud** added `PUT` request handling ([#107])
- **requests** added creating request builder with params ([#131])
- **requests** improved query params naming parsing ([#101])
### Bug Fixes
- **crud** set decorators after Swagger so metadata can be overwritten
- **requests** added support for ISO-8610 date strings
## [4.0.1] - 2019-06-21
### Bug Fixes
- **requests** fixed query parser to properly accept numbers and booleans ([#97])
## [4.0.0] - 2019-06-12
### BREAKING CHANGES
- **crud:** changed `CrudOptions` ([docs](https://github.com/nestjsx/crud/wiki/Controllers#options))
- **crud:** remove decorators: `@ParsedOptions`, `@ParsedParams`, `@ParsedQuery`. Add decorator `@ParsedRequest` instead.
- **crud:** change interfaces
- **services:** remove `RestfulOptions` from services
- **services:** changed base abstract class
### Features
- **repo:** refactor to monorepository
- **docs:** new [documentation](https://github.com/nestjsx/crud/wiki)
- **packages:** totally refactor `@nestjsx/crud` to be service (ORM) agnostic
- **packages:** add `@nestjsx/crud-typeorm` ([docs](https://github.com/nestjsx/crud/wiki/ServiceTypeorm))
- **packages:** add `@nestjsx/crud-request` ([docs](https://github.com/nestjsx/crud/wiki/Requests#description), [#53])
- **crud:** add global options ([docs](https://github.com/nestjsx/crud/wiki/Controllers#global-options), [#64])
- **crud:** add eager relations option ([#54], [#67])
### Bug Fixes
- several fixes
[4.6.2]: https://github.com/nestjsx/crud/compare/v4.6.1...v4.6.2
[4.6.1]: https://github.com/nestjsx/crud/compare/v4.6.0...v4.6.1
[4.6.0]: https://github.com/nestjsx/crud/compare/v4.5.0...v4.6.0
[4.5.0]: https://github.com/nestjsx/crud/compare/v4.4.5...v4.5.0
[4.4.5]: https://github.com/nestjsx/crud/compare/v4.4.4...v4.4.5
[4.4.4]: https://github.com/nestjsx/crud/compare/v4.4.3...v4.4.4
[4.4.3]: https://github.com/nestjsx/crud/compare/v4.4.2...v4.4.3
[4.4.2]: https://github.com/nestjsx/crud/compare/v4.4.1...v4.4.2
[4.4.1]: https://github.com/nestjsx/crud/compare/v4.4.0...v4.4.1
[4.4.0]: https://github.com/nestjsx/crud/compare/v4.3.0...v4.4.0
[4.3.0]: https://github.com/nestjsx/crud/compare/v4.2.0...v4.3.0
[4.2.0]: https://github.com/nestjsx/crud/compare/v4.1.0...v4.2.0
[4.1.0]: https://github.com/nestjsx/crud/compare/v4.0.1...v4.1.0
[4.0.1]: https://github.com/nestjsx/crud/compare/v4.0.0...v4.0.1
[4.0.0]: https://github.com/nestjsx/crud/compare/v.3.2.0...v4.0.0
[#97]: https://github.com/nestjsx/crud/issues/97
[#53]: https://github.com/nestjsx/crud/issues/53
[#64]: https://github.com/nestjsx/crud/issues/64
[#54]: https://github.com/nestjsx/crud/issues/54
[#67]: https://github.com/nestjsx/crud/issues/67
[#107]: https://github.com/nestjsx/crud/issues/107
[#131]: https://github.com/nestjsx/crud/issues/131
[#101]: https://github.com/nestjsx/crud/issues/101
[#186]: https://github.com/nestjsx/crud/pull/186
[#184]: https://github.com/nestjsx/crud/issues/184
[#148]: https://github.com/nestjsx/crud/issues/148
[#105]: https://github.com/nestjsx/crud/issues/105
[#87]: https://github.com/nestjsx/crud/issues/87
[#159]: https://github.com/nestjsx/crud/issues/159
[#161]: https://github.com/nestjsx/crud/issues/161
[#179]: https://github.com/nestjsx/crud/issues/179
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at mihon4ik@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
================================================
FILE: LICENSE
================================================
(The MIT License)
Copyright (c) 2018-Present Michael Yali
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: README.md
================================================
[](https://vshymanskyy.github.io/StandWithUkraine/)
CRUD
for RESTful APIs built with NestJs
We believe that everyone who's working with NestJs and building some RESTful services and especially some CRUD functionality will find `@nestjsx/crud` microframework very useful.
## Features
- :electric_plug: Super easy to install and start using the full-featured controllers and services :point_right:
- :octopus: DB and service agnostic extendable CRUD controllers
- :mag_right: Reach query parsing with filtering, pagination, sorting, relations, nested relations, cache, etc.
- :telescope: Framework agnostic package with query builder for a frontend usage
- :space_invader: Query, path params and DTOs validation included
- :clapper: Overriding controller methods with ease
- :wrench: Tiny config (including globally)
- :gift: Additional helper decorators
- :pencil2: Swagger documentation
## Packages
- [**@nestjsx/crud**](https://www.npmjs.com/package/@nestjsx/crud) - core package which provides `@Crud()` decorator for endpoints generation, global configuration, validation, helper decorators ([docs](https://github.com/nestjsx/crud/wiki/Controllers#description))
- [**@nestjsx/crud-request**](https://www.npmjs.com/package/@nestjsx/crud-request) - request builder/parser package which provides `RequestQueryBuilder` class for a frontend usage and `RequestQueryParser` that is being used internally for handling and validating query/path params on a backend side ([docs](https://github.com/nestjsx/crud/wiki/Requests#frontend-usage))
- [**@nestjsx/crud-typeorm**](https://www.npmjs.com/package/@nestjsx/crud-typeorm) - TypeORM package which provides base `TypeOrmCrudService` with methods for CRUD database operations ([docs](https://github.com/nestjsx/crud/wiki/ServiceTypeorm))
## Documentation
- :dart: [General Information](https://github.com/nestjsx/crud/wiki#why)
- :video_game: [CRUD Controllers](https://github.com/nestjsx/crud/wiki/Controllers#description)
- :horse_racing: [CRUD ORM Services](https://github.com/nestjsx/crud/wiki/Services#description)
- :trumpet: [Handling Requests](https://github.com/nestjsx/crud/wiki/Requests#description)
## Support
Any support is welcome. At least you can give us a star :star:
## Contributors
### Code Contributors
This project exists thanks to all the people who contribute. [[Contribute](CODE_OF_CONDUCT.md)].
### Financial Contributors
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/nestjsx#backer)]
#### Individuals
#### Organizations
Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/nestjsx#sponsor)]
## License
[MIT](LICENSE)
================================================
FILE: docker-compose.yml
================================================
version: '3'
networks:
nestjsx_crud:
services:
postgres:
# TypeORM fails with Postgres v.12
image: postgres:11.5
ports:
- 5455:5432
environment:
POSTGRES_USER: root
POSTGRES_PASSWORD: root
POSTGRES_DB: nestjsx_crud
networks:
- nestjsx_crud
mysql:
platform: linux/x86_64
image: mysql:5.7
ports:
- 3316:3306
environment:
MYSQL_DATABASE: nestjsx_crud
MYSQL_USER: nestjsx_crud
MYSQL_PASSWORD: nestjsx_crud
MYSQL_ROOT_PASSWORD: nestjsx_crud
redis:
image: redis:alpine
ports:
- 6399:6379
command: redis-server
networks:
- nestjsx_crud
================================================
FILE: integration/crud-typeorm/app.module.ts
================================================
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthGuard } from './auth.guard';
import { withCache } from './orm.config';
import { CompaniesModule } from './companies/companies.module';
import { ProjectsModule } from './projects/projects.module';
import { UsersModule } from './users/users.module';
import { DevicesModule } from './devices/devices.module';
import { NotesModule } from './notes/notes.module';
@Module({
imports: [
TypeOrmModule.forRoot(withCache),
CompaniesModule,
ProjectsModule,
UsersModule,
DevicesModule,
NotesModule,
],
providers: [
{
provide: APP_GUARD,
useClass: AuthGuard,
},
],
})
export class AppModule {}
================================================
FILE: integration/crud-typeorm/auth.guard.ts
================================================
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { UsersService } from './users';
import { USER_REQUEST_KEY } from './constants';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private usersService: UsersService) {}
async canActivate(ctx: ExecutionContext): Promise {
const req = ctx.switchToHttp().getRequest();
req[USER_REQUEST_KEY] = await this.usersService.findOne(1);
return true;
}
}
================================================
FILE: integration/crud-typeorm/base-entity.ts
================================================
import { PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
export class BaseEntity {
@PrimaryGeneratedColumn()
id?: number;
@CreateDateColumn({ nullable: true })
createdAt?: Date;
@UpdateDateColumn({ nullable: true })
updatedAt?: Date;
}
================================================
FILE: integration/crud-typeorm/companies/companies.controller.ts
================================================
import { Controller } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Crud } from '@nestjsx/crud';
import { Company } from './company.entity';
import { CompaniesService } from './companies.service';
import { serialize } from './responses';
@Crud({
model: {
type: Company
},
serialize,
routes: {
deleteOneBase: {
returnDeleted: false,
},
},
query: {
alwaysPaginate: false,
softDelete: true,
allow: ['name'],
join: {
users: {
alias: 'companyUsers',
exclude: ['email'],
eager: true,
},
'users.projects': {
eager: true,
alias: 'usersProjects',
allow: ['name'],
},
'users.projects.company': {
eager: true,
alias: 'usersProjectsCompany',
},
projects: {
eager: true,
select: false,
},
},
},
})
@ApiTags('companies')
@Controller('companies')
export class CompaniesController {
constructor(public service: CompaniesService) {}
}
================================================
FILE: integration/crud-typeorm/companies/companies.module.ts
================================================
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Company } from './company.entity';
import { CompaniesService } from './companies.service';
import { CompaniesController } from './companies.controller';
@Module({
imports: [TypeOrmModule.forFeature([Company])],
providers: [CompaniesService],
exports: [CompaniesService],
controllers: [CompaniesController],
})
export class CompaniesModule {}
================================================
FILE: integration/crud-typeorm/companies/companies.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { TypeOrmCrudService } from '@nestjsx/crud-typeorm';
import { Company } from './company.entity';
@Injectable()
export class CompaniesService extends TypeOrmCrudService {
constructor(@InjectRepository(Company) repo) {
super(repo);
}
}
================================================
FILE: integration/crud-typeorm/companies/company.entity.ts
================================================
import { CrudValidationGroups } from '@nestjsx/crud';
import { Entity, Column, OneToMany, PrimaryGeneratedColumn, DeleteDateColumn } from 'typeorm';
import {
IsOptional,
IsString,
MaxLength,
IsNotEmpty,
IsNumber,
IsEmpty,
} from 'class-validator';
import { Type } from 'class-transformer';
import { BaseEntity } from '../base-entity';
import { User } from '../users/user.entity';
import { Project } from '../projects/project.entity';
const { CREATE, UPDATE } = CrudValidationGroups;
@Entity('companies')
export class Company extends BaseEntity {
@IsOptional({ groups: [UPDATE] })
@IsEmpty({ groups: [CREATE] })
@IsNumber({}, { groups: [UPDATE] })
@PrimaryGeneratedColumn()
id?: number;
@IsOptional({ groups: [UPDATE] })
@IsNotEmpty({ groups: [CREATE] })
@IsString({ always: true })
@MaxLength(100, { always: true })
@Column({ type: 'varchar', length: 100, nullable: false })
name: string;
@IsOptional({ groups: [UPDATE] })
@IsNotEmpty({ groups: [CREATE] })
@IsString({ groups: [CREATE, UPDATE] })
@MaxLength(100, { groups: [CREATE, UPDATE] })
@Column({ type: 'varchar', length: 100, nullable: false, unique: true })
domain: string;
@IsOptional({ always: true })
@IsString({ always: true })
@Column({ type: 'text', nullable: true, default: null })
description: string;
@DeleteDateColumn({ nullable: true })
deletedAt?: Date;
/**
* Relations
*/
@OneToMany((type) => User, (u) => u.company)
@Type((t) => User)
users: User[];
@OneToMany((type) => Project, (p) => p.company)
projects: Project[];
}
================================================
FILE: integration/crud-typeorm/companies/index.ts
================================================
export * from './company.entity';
export * from './companies.service';
================================================
FILE: integration/crud-typeorm/companies/requests/create-company.dto.ts
================================================
import { ApiProperty } from '@nestjs/swagger';
import { IsString, MaxLength } from 'class-validator';
export class CreateCompanyDto {
@ApiProperty({ type: 'string' })
@IsString()
@MaxLength(100)
name: string;
@ApiProperty({ type: 'string' })
@IsString()
@MaxLength(100)
domain: string;
@ApiProperty({ type: 'string' })
@IsString()
@MaxLength(100)
description: string;
}
================================================
FILE: integration/crud-typeorm/companies/requests/index.ts
================================================
import { CreateCompanyDto } from './create-company.dto';
export const dto = {
create: CreateCompanyDto,
};
================================================
FILE: integration/crud-typeorm/companies/responses/get-company-response.dto.ts
================================================
import { ApiProperty } from '@nestjs/swagger';
import { Exclude } from 'class-transformer';
export class GetCompanyResponseDto {
@ApiProperty({ type: 'number' })
id: string;
@ApiProperty({ type: 'string' })
name: string;
@ApiProperty({ type: 'string' })
domain: string;
@ApiProperty({ type: 'string' })
description: string;
@Exclude()
createdAt: any;
@Exclude()
updatedAt: any;
}
================================================
FILE: integration/crud-typeorm/companies/responses/index.ts
================================================
import { SerializeOptions } from '@nestjsx/crud';
import { GetCompanyResponseDto } from './get-company-response.dto';
export const serialize: SerializeOptions = {
get: GetCompanyResponseDto,
};
================================================
FILE: integration/crud-typeorm/constants.ts
================================================
export const USER_REQUEST_KEY = 'user';
================================================
FILE: integration/crud-typeorm/devices/device.entity.ts
================================================
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
import { IsOptional, IsString, IsUUID } from 'class-validator';
import { CrudValidationGroups } from '@nestjsx/crud';
const { CREATE, UPDATE } = CrudValidationGroups;
@Entity('devices')
export class Device {
@IsOptional({ always: true })
@IsUUID('4', { always: true })
@PrimaryGeneratedColumn('uuid')
deviceKey: string;
@IsOptional({ always: true })
@IsString({ always: true })
@Column({ type: 'text', nullable: true })
description?: string;
}
================================================
FILE: integration/crud-typeorm/devices/devices.controller.ts
================================================
import { Controller } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Crud } from '@nestjsx/crud';
import { Device } from './device.entity';
import { DevicesService } from './devices.service';
import { serialize } from './response';
@Crud({
model: { type: Device },
serialize,
params: {
deviceKey: {
field: 'deviceKey',
type: 'uuid',
primary: true,
},
},
routes: {
deleteOneBase: {
returnDeleted: true,
},
},
})
@ApiTags('devices')
@Controller('/devices')
export class DevicesController {
constructor(public service: DevicesService) {}
}
================================================
FILE: integration/crud-typeorm/devices/devices.module.ts
================================================
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Device } from './device.entity';
import { DevicesService } from './devices.service';
import { DevicesController } from './devices.controller';
@Module({
imports: [TypeOrmModule.forFeature([Device])],
providers: [DevicesService],
exports: [DevicesService],
controllers: [DevicesController],
})
export class DevicesModule {}
================================================
FILE: integration/crud-typeorm/devices/devices.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { TypeOrmCrudService } from '@nestjsx/crud-typeorm';
import { Device } from './device.entity';
@Injectable()
export class DevicesService extends TypeOrmCrudService {
constructor(@InjectRepository(Device) repo) {
super(repo);
}
}
================================================
FILE: integration/crud-typeorm/devices/index.ts
================================================
export * from './device.entity';
export * from './devices.service';
================================================
FILE: integration/crud-typeorm/devices/response/delete-device-response.dto.ts
================================================
import { ApiProperty } from '@nestjs/swagger';
import { Exclude } from 'class-transformer';
export class DeleteDeviceResponseDto {
@ApiProperty({ type: 'string' })
deviceKey: string;
@Exclude()
description?: string;
}
================================================
FILE: integration/crud-typeorm/devices/response/index.ts
================================================
import { SerializeOptions } from '@nestjsx/crud';
import { DeleteDeviceResponseDto } from './delete-device-response.dto';
export const serialize: SerializeOptions = {
delete: DeleteDeviceResponseDto,
};
================================================
FILE: integration/crud-typeorm/main.ts
================================================
import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { CrudConfigService } from '@nestjsx/crud';
import { USER_REQUEST_KEY } from './constants';
// Important: load config before (!!!) you import AppModule
// https://github.com/nestjsx/crud/wiki/Controllers#global-options
CrudConfigService.load({
auth: {
property: USER_REQUEST_KEY,
},
routes: {
// exclude: ['createManyBase'],
},
});
import { HttpExceptionFilter } from '../shared/https-exception.filter';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());
const options = new DocumentBuilder()
.setTitle('@nestjsx/crud-typeorm')
.setDescription('@nestjsx/crud-typeorm')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('docs', app, document);
await app.listen(process.env.PORT || 3000);
}
bootstrap();
================================================
FILE: integration/crud-typeorm/notes/index.ts
================================================
export * from './note.entity';
export * from './notes.service';
================================================
FILE: integration/crud-typeorm/notes/note.entity.ts
================================================
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity('notes')
export class Note {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'revision_id', nullable: false })
revisionId: number;
}
================================================
FILE: integration/crud-typeorm/notes/notes.controller.ts
================================================
import { Controller } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Crud } from '@nestjsx/crud';
import { Note } from './note.entity';
import { NotesService } from './notes.service';
import { dto } from './requests';
import { serialize } from './responses';
@Crud({
model: { type: Note },
dto,
serialize,
query: {
alwaysPaginate: true,
},
})
@ApiTags('notes')
@Controller('/notes')
export class NotesController {
constructor(public service: NotesService) {}
}
================================================
FILE: integration/crud-typeorm/notes/notes.module.ts
================================================
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Note } from './note.entity';
import { NotesService } from './notes.service';
import { NotesController } from './notes.controller';
@Module({
imports: [TypeOrmModule.forFeature([Note])],
providers: [NotesService],
exports: [NotesService],
controllers: [NotesController],
})
export class NotesModule {}
================================================
FILE: integration/crud-typeorm/notes/notes.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { TypeOrmCrudService } from '../../../packages/crud-typeorm/src';
import { Note } from './note.entity';
@Injectable()
export class NotesService extends TypeOrmCrudService {
constructor(@InjectRepository(Note) repo) {
super(repo);
}
}
================================================
FILE: integration/crud-typeorm/notes/requests/create-note.dto.ts
================================================
import { ApiProperty } from '@nestjs/swagger';
import { IsNumber } from 'class-validator';
export class CreateNoteDto {
@ApiProperty({ type: 'number' })
@IsNumber()
revisionId: string;
}
================================================
FILE: integration/crud-typeorm/notes/requests/index.ts
================================================
import { CreateNoteDto } from './create-note.dto';
export const dto = {
create: CreateNoteDto,
};
================================================
FILE: integration/crud-typeorm/notes/responses/get-note-response.dto.ts
================================================
import { ApiProperty } from '@nestjs/swagger';
import { IsNumber } from 'class-validator';
export class GetNoteResponseDto {
@ApiProperty({ type: 'number' })
@IsNumber()
id: string;
@ApiProperty({ type: 'number' })
@IsNumber()
revisionId: string;
}
================================================
FILE: integration/crud-typeorm/notes/responses/index.ts
================================================
import { GetNoteResponseDto } from './get-note-response.dto';
export const serialize = {
get: GetNoteResponseDto,
};
================================================
FILE: integration/crud-typeorm/orm.config.ts
================================================
import { join } from 'path';
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { isNil } from '@nestjsx/util';
const type = (process.env.TYPEORM_CONNECTION as any) || 'postgres';
export const withCache: TypeOrmModuleOptions = {
type,
host: '127.0.0.1',
port: type === 'postgres' ? 5455 : 3316,
username: type === 'mysql' ? 'nestjsx_crud' : 'root',
password: type === 'mysql' ? 'nestjsx_crud' : 'root',
database: 'nestjsx_crud',
synchronize: false,
logging: !isNil(process.env.TYPEORM_LOGGING) ? !!parseInt(process.env.TYPEORM_LOGGING, 10) : true,
entities: [join(__dirname, './**/*.entity{.ts,.js}')],
};
================================================
FILE: integration/crud-typeorm/orm.mysql.ts
================================================
import { DataSource } from 'typeorm';
exports.default = new DataSource({
type: 'mysql',
host: '127.0.0.1',
port: 3316,
username: 'nestjsx_crud',
password: 'nestjsx_crud',
database: 'nestjsx_crud',
entities: ['./**/*.entity.ts'],
migrationsTableName: 'orm_migrations',
migrations: ['./seeds.ts'],
});
================================================
FILE: integration/crud-typeorm/orm.postgres.ts
================================================
import { DataSource } from 'typeorm';
exports.default = new DataSource({
type: 'postgres',
host: '127.0.0.1',
port: 5455,
username: 'root',
password: 'root',
database: 'nestjsx_crud',
entities: ['./**/*.entity.ts'],
migrationsTableName: 'orm_migrations',
migrations: ['./seeds.ts'],
});
================================================
FILE: integration/crud-typeorm/orm.yaml
================================================
default:
type: postgres
host: 127.0.0.1
port: 5455
username: root
password: root
database: nestjsx_crud
entities:
- ./**/*.entity.ts
migrationsTableName: orm_migrations
migrations:
- ./seeds.ts
mysql:
type: mysql
host: 127.0.0.1
port: 3316
username: nestjsx_crud
password: nestjsx_crud
database: nestjsx_crud
entities:
- ./**/*.entity.ts
migrationsTableName: orm_migrations
migrations:
- ./seeds.ts
================================================
FILE: integration/crud-typeorm/projects/index.ts
================================================
export * from './project.entity';
export * from './user-project.entity';
export * from './projects.service';
================================================
FILE: integration/crud-typeorm/projects/my-projects.controller.ts
================================================
import { Controller } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Crud, CrudAuth } from '@nestjsx/crud';
import { User } from '../users/user.entity';
import { UserProject } from './user-project.entity';
import { UserProjectsService } from './user-projects.service';
@Crud({
model: {
type: UserProject,
},
params: {
projectId: {
field: 'projectId',
type: 'number',
primary: true,
},
},
query: {
join: {
project: {
eager: true,
},
},
},
})
@CrudAuth({
filter: (user: User) => ({
userId: user.id,
}),
persist: (user: User) => ({
userId: user.id,
}),
})
@ApiTags('my-projects')
@Controller('my-projects')
export class MyProjectsController {
constructor(public service: UserProjectsService) {}
}
================================================
FILE: integration/crud-typeorm/projects/project.entity.ts
================================================
import { Entity, Column, ManyToOne, ManyToMany, JoinTable, OneToMany } from 'typeorm';
import {
IsOptional,
IsString,
IsNumber,
MaxLength,
IsDefined,
IsBoolean,
} from 'class-validator';
import { CrudValidationGroups } from '@nestjsx/crud';
import { BaseEntity } from '../base-entity';
import { Company } from '../companies/company.entity';
import { User } from '../users/user.entity';
import { UserProject } from './user-project.entity';
const { CREATE, UPDATE } = CrudValidationGroups;
@Entity('projects')
export class Project extends BaseEntity {
@IsOptional({ groups: [UPDATE] })
@IsDefined({ groups: [CREATE] })
@IsString({ always: true })
@MaxLength(100, { always: true })
@Column({ type: 'varchar', length: 100, nullable: false, unique: true })
name?: string;
@IsOptional({ always: true })
@Column({ type: 'text', nullable: true })
description?: string;
@IsOptional({ always: true })
@IsBoolean({ always: true })
@Column({ type: 'boolean', default: true })
isActive?: boolean;
@IsOptional({ always: true })
@IsNumber({}, { always: true })
@Column({ nullable: false })
companyId?: number;
/**
* Relations
*/
@ManyToOne((type) => Company, (c) => c.projects)
company?: Company;
@ManyToMany((type) => User, (u) => u.projects, { cascade: true })
@JoinTable({
name: 'user_projects',
joinColumn: {
name: 'projectId',
referencedColumnName: 'id',
},
inverseJoinColumn: {
name: 'userId',
referencedColumnName: 'id',
},
})
users?: User[];
@OneToMany((type) => UserProject, (el) => el.project, {
persistence: false,
onDelete: 'CASCADE',
})
userProjects!: UserProject[];
}
================================================
FILE: integration/crud-typeorm/projects/projects.controller.ts
================================================
import { Controller } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Crud } from '@nestjsx/crud';
import { Project } from './project.entity';
import { ProjectsService } from './projects.service';
@Crud({
model: {
type: Project,
},
params: {
companyId: {
field: 'companyId',
type: 'number',
},
id: {
field: 'id',
type: 'number',
primary: true,
},
},
query: {
join: {
users: {},
},
},
})
@ApiTags('projects')
@Controller('/companies/:companyId/projects')
export class ProjectsController {
constructor(public service: ProjectsService) {}
}
================================================
FILE: integration/crud-typeorm/projects/projects.module.ts
================================================
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Project } from './project.entity';
import { UserProject } from './user-project.entity';
import { ProjectsService } from './projects.service';
import { UserProjectsService } from './user-projects.service';
import { ProjectsController } from './projects.controller';
import { MyProjectsController } from './my-projects.controller';
@Module({
imports: [TypeOrmModule.forFeature([Project, UserProject])],
providers: [ProjectsService, UserProjectsService],
exports: [ProjectsService, UserProjectsService],
controllers: [ProjectsController, MyProjectsController],
})
export class ProjectsModule {}
================================================
FILE: integration/crud-typeorm/projects/projects.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { TypeOrmCrudService } from '@nestjsx/crud-typeorm';
import { Project } from './project.entity';
@Injectable()
export class ProjectsService extends TypeOrmCrudService {
constructor(@InjectRepository(Project) repo) {
super(repo);
}
}
================================================
FILE: integration/crud-typeorm/projects/user-project.entity.ts
================================================
import { Entity, Column, ManyToOne, PrimaryColumn } from 'typeorm';
import { User } from '../users/user.entity';
import { Project } from './project.entity';
@Entity('user_projects')
export class UserProject {
@PrimaryColumn()
public projectId!: number;
@PrimaryColumn()
public userId!: number;
@Column({ nullable: true })
public review!: string;
@ManyToOne(() => Project, (el) => el.userProjects, {
persistence: false,
onDelete: 'CASCADE',
})
public project: Project;
@ManyToOne(() => User, (el) => el.userProjects, {
persistence: false,
})
public user: User;
}
================================================
FILE: integration/crud-typeorm/projects/user-projects.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { TypeOrmCrudService } from '@nestjsx/crud-typeorm';
import { UserProject } from './user-project.entity';
@Injectable()
export class UserProjectsService extends TypeOrmCrudService {
constructor(@InjectRepository(UserProject) repo) {
super(repo);
}
}
================================================
FILE: integration/crud-typeorm/seeds.ts
================================================
import { ClassType } from '@nestjsx/util';
import { plainToClass } from 'class-transformer';
import { MigrationInterface, Repository, QueryRunner } from 'typeorm';
import { Company } from './companies';
import { Project, UserProject } from './projects';
import { Name, User } from './users';
import { License, UserLicense } from './users-licenses';
import { UserProfile } from './users-profiles';
import { Note } from './notes';
export class Seeds1544303473346 implements MigrationInterface {
private save(repo: Repository, data: Partial[]): Promise {
return repo.save(
data.map((partial: Partial) =>
plainToClass(repo.target as ClassType, partial, {
ignoreDecorators: true,
}),
),
);
}
public async up(queryRunner: QueryRunner): Promise {
const { connection } = queryRunner;
const companiesRepo = connection.getRepository(Company);
const projectsRepo = connection.getRepository(Project);
const usersProfilesRepo = connection.getRepository(UserProfile);
const usersRepo = connection.getRepository(User);
const licensesRepo = connection.getRepository(License);
const usersLincesesRepo = connection.getRepository(UserLicense);
const usersProjectsRepo = connection.getRepository(UserProject);
const notesRepo = connection.getRepository(Note);
// companies
await this.save(companiesRepo, [
{ name: 'Name1', domain: 'Domain1' },
{ name: 'Name2', domain: 'Domain2' },
{ name: 'Name3', domain: 'Domain3' },
{ name: 'Name4', domain: 'Domain4' },
{ name: 'Name5', domain: 'Domain5' },
{ name: 'Name6', domain: 'Domain6' },
{ name: 'Name7', domain: 'Domain7' },
{ name: 'Name8', domain: 'Domain8' },
{ name: 'Name9', domain: 'Domain9', deletedAt: new Date() },
{ name: 'Name10', domain: 'Domain10' },
]);
// projects
await this.save(projectsRepo, [
{ name: 'Project1', description: 'description1', isActive: true, companyId: 1 },
{ name: 'Project2', description: 'description2', isActive: true, companyId: 1 },
{ name: 'Project3', description: 'description3', isActive: true, companyId: 2 },
{ name: 'Project4', description: 'description4', isActive: true, companyId: 2 },
{ name: 'Project5', description: 'description5', isActive: true, companyId: 3 },
{ name: 'Project6', description: 'description6', isActive: true, companyId: 3 },
{ name: 'Project7', description: 'description7', isActive: true, companyId: 4 },
{ name: 'Project8', description: 'description8', isActive: true, companyId: 4 },
{ name: 'Project9', description: 'description9', isActive: true, companyId: 5 },
{ name: 'Project10', description: 'description10', isActive: true, companyId: 5 },
{ name: 'Project11', description: 'description11', isActive: false, companyId: 6 },
{ name: 'Project12', description: 'description12', isActive: false, companyId: 6 },
{ name: 'Project13', description: 'description13', isActive: false, companyId: 7 },
{ name: 'Project14', description: 'description14', isActive: false, companyId: 7 },
{ name: 'Project15', description: 'description15', isActive: false, companyId: 8 },
{ name: 'Project16', description: 'description16', isActive: false, companyId: 8 },
{ name: 'Project17', description: 'description17', isActive: false, companyId: 9 },
{ name: 'Project18', description: 'description18', isActive: false, companyId: 9 },
{ name: 'Project19', description: 'description19', isActive: false, companyId: 10 },
{ name: 'Project20', description: 'description20', isActive: false, companyId: 10 },
]);
// user-profiles
await this.save(usersProfilesRepo, [
{ name: 'User1' },
{ name: 'User2' },
{ name: 'User3' },
{ name: 'User4' },
{ name: 'User5' },
{ name: 'User6' },
{ name: 'User7' },
{ name: 'User8' },
{ name: 'User9' },
{ name: 'User1' },
{ name: 'User1' },
{ name: 'User1' },
{ name: 'User1' },
{ name: 'User1' },
{ name: 'User1' },
{ name: 'User1' },
{ name: 'User1' },
{ name: 'User1' },
{ name: 'User1' },
{ name: 'User2' },
]);
// users
const name: Name = { first: null, last: null };
const name1: Name = { first: 'firstname1', last: 'lastname1' };
await this.save(usersRepo, [
{ email: '1@email.com', isActive: true, companyId: 1, profileId: 1, name: name1 },
{ email: '2@email.com', isActive: true, companyId: 1, profileId: 2, name },
{ email: '3@email.com', isActive: true, companyId: 1, profileId: 3, name },
{ email: '4@email.com', isActive: true, companyId: 1, profileId: 4, name },
{ email: '5@email.com', isActive: true, companyId: 1, profileId: 5, name },
{ email: '6@email.com', isActive: true, companyId: 1, profileId: 6, name },
{ email: '7@email.com', isActive: false, companyId: 1, profileId: 7, name },
{ email: '8@email.com', isActive: false, companyId: 1, profileId: 8, name },
{ email: '9@email.com', isActive: false, companyId: 1, profileId: 9, name },
{ email: '10@email.com', isActive: true, companyId: 1, profileId: 10, name },
{ email: '11@email.com', isActive: true, companyId: 2, profileId: 11, name },
{ email: '12@email.com', isActive: true, companyId: 2, profileId: 12, name },
{ email: '13@email.com', isActive: true, companyId: 2, profileId: 13, name },
{ email: '14@email.com', isActive: true, companyId: 2, profileId: 14, name },
{ email: '15@email.com', isActive: true, companyId: 2, profileId: 15, name },
{ email: '16@email.com', isActive: true, companyId: 2, profileId: 16, name },
{ email: '17@email.com', isActive: false, companyId: 2, profileId: 17, name },
{ email: '18@email.com', isActive: false, companyId: 2, profileId: 18, name },
{ email: '19@email.com', isActive: false, companyId: 2, profileId: 19, name },
{ email: '20@email.com', isActive: false, companyId: 2, profileId: 20, name },
{ email: '21@email.com', isActive: false, companyId: 2, profileId: null, name },
]);
// licenses
await this.save(licensesRepo, [
{ name: 'License1' },
{ name: 'License2' },
{ name: 'License3' },
{ name: 'License4' },
{ name: 'License5' },
]);
// user-licenses
await this.save(usersLincesesRepo, [
{ userId: 1, licenseId: 1, yearsActive: 3 },
{ userId: 1, licenseId: 2, yearsActive: 5 },
{ userId: 1, licenseId: 4, yearsActive: 7 },
{ userId: 2, licenseId: 5, yearsActive: 1 },
]);
// user-projects
await this.save(usersProjectsRepo, [
{ projectId: 1, userId: 1, review: 'User project 1 1' },
{ projectId: 1, userId: 2, review: 'User project 1 2' },
{ projectId: 2, userId: 2, review: 'User project 2 2' },
{ projectId: 3, userId: 3, review: 'User project 3 3' },
]);
// notes
await this.save(notesRepo, [
{ revisionId: 1 },
{ revisionId: 1 },
{ revisionId: 2 },
{ revisionId: 2 },
{ revisionId: 3 },
{ revisionId: 3 },
]);
}
public async down(queryRunner: QueryRunner): Promise {}
}
================================================
FILE: integration/crud-typeorm/users/index.ts
================================================
export * from './user.entity';
export * from './users.service';
================================================
FILE: integration/crud-typeorm/users/me.controller.ts
================================================
import { Controller } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Crud, CrudAuth } from '@nestjsx/crud';
import { User } from './user.entity';
import { UsersService } from './users.service';
@Crud({
model: {
type: User,
},
routes: {
only: ['getOneBase', 'updateOneBase'],
},
params: {
id: {
primary: true,
disabled: true,
},
},
query: {
join: {
company: {
eager: true,
},
profile: {
eager: true,
},
},
},
})
@CrudAuth({
filter: (user: User) => ({
id: user.id,
}),
})
@ApiTags('me')
@Controller('me')
export class MeController {
constructor(public service: UsersService) {}
}
================================================
FILE: integration/crud-typeorm/users/user.entity.ts
================================================
import {
Entity,
Column,
JoinColumn,
OneToOne,
OneToMany,
ManyToOne,
ManyToMany,
DeleteDateColumn,
} from 'typeorm';
import {
IsOptional,
IsString,
MaxLength,
IsNotEmpty,
IsEmail,
IsBoolean,
ValidateNested,
} from 'class-validator';
import { Type } from 'class-transformer';
import { CrudValidationGroups } from '@nestjsx/crud';
import { BaseEntity } from '../base-entity';
import { UserProfile } from '../users-profiles/user-profile.entity';
import { UserLicense } from '../users-licenses/user-license.entity';
import { Company } from '../companies/company.entity';
import { Project } from '../projects/project.entity';
import { UserProject } from '../projects/user-project.entity';
const { CREATE, UPDATE } = CrudValidationGroups;
export class Name {
@IsString({ always: true })
@Column({ nullable: true })
first: string;
@IsString({ always: true })
@Column({ nullable: true })
last: string;
}
// tslint:disable-next-line:max-classes-per-file
@Entity('users')
export class User extends BaseEntity {
@IsOptional({ groups: [UPDATE] })
@IsNotEmpty({ groups: [CREATE] })
@IsString({ always: true })
@MaxLength(255, { always: true })
@IsEmail({ require_tld: false }, { always: true })
@Column({ type: 'varchar', length: 255, nullable: false, unique: true })
email: string;
@IsOptional({ groups: [UPDATE] })
@IsNotEmpty({ groups: [CREATE] })
@IsBoolean({ always: true })
@Column({ type: 'boolean', default: true })
isActive: boolean;
@Type((t) => Name)
@Column((type) => Name)
name: Name;
@Column({ nullable: true })
profileId?: number;
@Column({ nullable: false })
companyId?: number;
@DeleteDateColumn({ nullable: true })
deletedAt?: Date;
/**
* Relations
*/
@IsOptional({ groups: [UPDATE] })
@IsNotEmpty({ groups: [CREATE] })
@ValidateNested({ always: true })
@Type((t) => UserProfile)
@OneToOne((type) => UserProfile, (p) => p.user, { cascade: true })
@JoinColumn()
profile?: UserProfile;
@ManyToOne((type) => Company, (c) => c.users)
company?: Company;
@ManyToMany((type) => Project, (c) => c.users)
projects?: Project[];
@OneToMany((type) => UserProject, (el) => el.user, {
persistence: false,
onDelete: 'CASCADE',
})
userProjects?: UserProject[];
@OneToMany((type) => UserLicense, (ul) => ul.user)
@Type((t) => UserLicense)
@JoinColumn()
userLicenses?: UserLicense[];
}
================================================
FILE: integration/crud-typeorm/users/users.controller.ts
================================================
import { Controller } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import {
Crud,
CrudController,
CrudRequest,
ParsedRequest,
Override,
} from '@nestjsx/crud';
import { User } from './user.entity';
import { UsersService } from './users.service';
@Crud({
model: {
type: User,
},
params: {
companyId: {
field: 'companyId',
type: 'number',
},
id: {
field: 'id',
type: 'number',
primary: true,
},
},
query: {
softDelete: true,
join: {
company: {
exclude: ['description'],
},
'company.projects': {
alias: 'pr',
exclude: ['description'],
},
profile: {
eager: true,
exclude: ['updatedAt'],
},
},
},
})
@ApiTags('users')
@Controller('/companies/:companyId/users')
export class UsersController implements CrudController {
constructor(public service: UsersService) {}
get base(): CrudController {
return this;
}
@Override('getManyBase')
getAll(@ParsedRequest() req: CrudRequest) {
return this.base.getManyBase(req);
}
}
================================================
FILE: integration/crud-typeorm/users/users.module.ts
================================================
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
import { UserProfile } from '../users-profiles/user-profile.entity';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { MeController } from './me.controller';
@Module({
imports: [TypeOrmModule.forFeature([User, UserProfile])],
providers: [UsersService],
exports: [UsersService],
controllers: [UsersController, MeController],
})
export class UsersModule {}
================================================
FILE: integration/crud-typeorm/users/users.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { TypeOrmCrudService } from '@nestjsx/crud-typeorm';
import { User } from './user.entity';
@Injectable()
export class UsersService extends TypeOrmCrudService {
constructor(@InjectRepository(User) repo) {
super(repo);
}
}
================================================
FILE: integration/crud-typeorm/users-licenses/index.ts
================================================
export * from './license.entity';
export * from './user-license.entity';
================================================
FILE: integration/crud-typeorm/users-licenses/license.entity.ts
================================================
import { Column, Entity } from 'typeorm';
import { BaseEntity } from '../base-entity';
import { IsOptional, IsString, MaxLength } from 'class-validator';
@Entity('licenses')
export class License extends BaseEntity {
@IsOptional({ always: true })
@IsString({ always: true })
@MaxLength(32, { always: true })
@Column({ type: 'varchar', length: 32, nullable: true, default: null })
name: string;
}
================================================
FILE: integration/crud-typeorm/users-licenses/user-license.entity.ts
================================================
import { Column, Entity, ManyToOne, PrimaryColumn } from 'typeorm';
import { User } from '../users/user.entity';
import { Type } from 'class-transformer';
import { License } from './license.entity';
@Entity('user_licenses')
export class UserLicense {
@PrimaryColumn()
userId: number;
@PrimaryColumn()
licenseId: number;
@ManyToOne((type) => User)
@Type((t) => User)
user: User;
@ManyToOne((type) => License)
@Type((t) => License)
license: License;
@Column()
yearsActive: number;
}
================================================
FILE: integration/crud-typeorm/users-profiles/index.ts
================================================
export * from './user-profile.entity';
================================================
FILE: integration/crud-typeorm/users-profiles/user-profile.entity.ts
================================================
import { Entity, Column, OneToOne, DeleteDateColumn } from 'typeorm';
import { IsOptional, IsString, MaxLength } from 'class-validator';
import { BaseEntity } from '../base-entity';
import { User } from '../users/user.entity';
@Entity('user_profiles')
export class UserProfile extends BaseEntity {
@IsOptional({ always: true })
@IsString({ always: true })
@MaxLength(32, { always: true })
@Column({ type: 'varchar', length: 32, nullable: true, default: null })
name: string;
@DeleteDateColumn({ nullable: true })
deletedAt?: Date;
/**
* Relations
*/
@OneToOne((type) => User, (u) => u.profile)
user?: User;
}
================================================
FILE: integration/shared/https-exception.filter.ts
================================================
import { ExceptionFilter, Catch, ArgumentsHost } from '@nestjs/common';
import { HttpException, InternalServerErrorException } from '@nestjs/common';
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const { status, json } = this.prepareException(exception);
response.status(status).send(json);
}
prepareException(exc: any): { status: number; json: object } {
if (process.env.NODE_ENV !== 'test') {
console.log(exc);
}
const error =
exc instanceof HttpException ? exc : new InternalServerErrorException(exc.message);
const status = error.getStatus();
const response = error.getResponse();
const json = typeof response === 'string' ? { error: response } : response;
return { status, json };
}
}
================================================
FILE: jest.config.js
================================================
/* eslint-disable @typescript-eslint/no-var-requires */
const tsconfig = require('tsconfig-extends');
const { pathsToModuleNameMapper } = require('ts-jest/utils');
const compilerOptions = tsconfig.load_file_sync('./tsconfig.jest.json', __dirname);
module.exports = {
testEnvironment: 'node',
setupFilesAfterEnv: ['jest-extended/all'],
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, {
prefix: '/packages/',
}),
moduleFileExtensions: ['ts', 'js'],
testRegex: '\\.spec.ts$',
rootDir: '.',
transform: {
'^.+\\.ts$': 'ts-jest',
},
globals: {
'ts-jest': {
tsconfig: 'tsconfig.jest.json',
},
},
coverageReporters: ['json', 'lcov', 'text-summary'],
coverageDirectory: 'coverage',
collectCoverageFrom: [
'packages/**/*.ts',
'!packages/**/*.d.ts',
'!packages/**/index.ts',
'!packages/**/*.interface.ts',
'!**/node_modules/**',
'!**/__stubs__/**',
'!**/__fixture__/**',
'!integration/*',
],
};
================================================
FILE: lerna.json
================================================
{
"packages": [
"packages/*"
],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "5.0.0-alpha.3",
"command": {
"publish": {
"message": "chore: release"
}
}
}
================================================
FILE: mrepo.json
================================================
{
"workspace": {
"name": "packages",
"scope": "nestjsx",
"registry": "npm",
"packages": [
{
"name": "util"
},
{
"name": "crud-request"
},
{
"name": "crud-typeorm"
},
{
"name": "crud"
}
]
},
"packageGenerator": {
"subGenerators": [],
"defaultOptions": {
"access": "public",
"license": "MIT",
"authorName": "Michael Yali",
"authorEmail": "mihon4ik@gmail.com",
"updateTsconfig": true,
"subGenerators": [],
"dependencies": []
}
}
}
================================================
FILE: package.json
================================================
{
"name": "@nestjsx/crud",
"version": "4.0.0",
"description": "Nest CRUD for RESTful APIs",
"license": "MIT",
"workspaces": [
"packages/*"
],
"private": true,
"scripts": {
"boot": "npx lerna bootstrap",
"rebuild": "yarn clean && yarn build",
"build": "npx mrepo build",
"clean": "npx mrepo clean && npx rimraf ./.mrepo",
"test": "npx mrepo test",
"test:coverage": "yarn test:all --coverage",
"test:coveralls": "yarn test:coverage --coverageReporters=text-lcov | coveralls",
"test:all": "yarn test:mysql && yarn test:postgres",
"test:postgres": "yarn db:prepare:typeorm:postgres && yarn test",
"test:mysql": "yarn db:prepare:typeorm:mysql && TYPEORM_CONNECTION=mysql yarn test",
"start:typeorm": "npx nodemon -w ./integration/crud-typeorm -e ts node_modules/ts-node/dist/bin.js integration/crud-typeorm/main.ts",
"db:cli:typeorm": "cd ./integration/crud-typeorm && npx ts-node -r tsconfig-paths/register ../../node_modules/typeorm/cli.js",
"db:sync:typeorm": "yarn db:cli:typeorm schema:sync",
"db:drop:typeorm": "yarn db:cli:typeorm schema:drop",
"db:seeds:typeorm": "yarn db:cli:typeorm migration:run",
"db:prepare:typeorm:postgres": "yarn db:drop:typeorm -d=orm.postgres.ts && yarn db:sync:typeorm -d=orm.postgres.ts && yarn db:seeds:typeorm -d=orm.postgres.ts",
"db:prepare:typeorm:mysql": "yarn db:drop:typeorm -d=orm.mysql.ts && yarn db:sync:typeorm -d=orm.mysql.ts && yarn db:seeds:typeorm -d=orm.mysql.ts",
"format": "npx pretty-quick --pattern \"packages/**/!(*.d).ts\"",
"lint": "npx eslint \"packages/**/!(*.d).ts\" --fix",
"commit": "npx git-cz",
"postinstall": "npx opencollective",
"prepare": "npx husky install",
"release": "mrepo release",
"release:alpha": "yarn release prerelease --preid alpha --dist-tag alpha",
"release:beta": "yarn release prerelease --preid beta --dist-tag beta"
},
"config": {
"commitizen": {
"path": "node_modules/cz-conventional-changelog"
}
},
"collective": {
"type": "opencollective",
"url": "https://opencollective.com/nestjsx",
"donation": {
"text": "Become a partner:"
}
},
"dependencies": {
"@nestjs/common": "^9.0.0",
"@nestjs/core": "^9.0.0",
"@nestjs/platform-express": "^9.0.0",
"@nestjs/swagger": "5.2.1",
"@nestjs/testing": "^9.0.0",
"@nestjs/typeorm": "^9.0.0",
"@zmotivat0r/mrepo": "0.8.1",
"class-transformer": "0.3.2",
"class-validator": "0.13.2",
"mysql": "2.18.1",
"pg": "8.7.3",
"pluralize": "8.0.0",
"qs": "6.10.3",
"redis": "4.0.4",
"reflect-metadata": "0.1.13",
"rxjs": "7.5.5",
"swagger-ui-express": "4.3.0",
"typeorm": "^0.3.0"
},
"devDependencies": {
"@nuxtjs/opencollective": "0.2.2",
"@types/jest": "27.4.1",
"@types/node": "17.0.23",
"@types/qs": "6.9.7",
"@types/supertest": "2.0.12",
"@typescript-eslint/eslint-plugin": "4.19.0",
"@typescript-eslint/parser": "4.19.0",
"commitizen": "4.2.3",
"conventional-changelog-cli": "2.1.1",
"coveralls": "3.1.1",
"eslint": "7.22.0",
"eslint-config-airbnb-base": "14.2.1",
"eslint-config-prettier": "8.1.0",
"eslint-plugin-import": "2.22.1",
"husky": "7.0.4",
"jest": "27.5.1",
"jest-extended": "2.0.0",
"lerna": "4.0.0",
"nodemon": "2.0.15",
"prettier": "2.6.1",
"pretty-quick": "3.1.3",
"rimraf": "3.0.2",
"supertest": "6.2.2",
"ts-jest": "27.1.4",
"ts-node": "10.7.0",
"tsconfig-extends": "1.0.1",
"tsconfig-paths": "3.14.1",
"typescript": "4.6.3"
},
"author": {
"name": "Michael Yali",
"email": "mihon4ik@gmail.com"
}
}
================================================
FILE: packages/crud/README.md
================================================
CRUD (@nestjsx/crud)
for RESTful APIs built with NestJs
We believe that everyone who's working with NestJs and building some RESTful services and especially some CRUD functionality will find `@nestjsx/crud` microframework very useful.
## Features
- Super easy to install and start using the full-featured controllers and services :point_right:
- DB and service agnostic extendable CRUD controllers
- Reach query parsing with filtering, pagination, sorting, relations, nested relations, cache, etc.
- Framework agnostic package with query builder for a frontend usage
- Query, path params and DTOs validation included
- Overriding controller methods with ease
- Tiny config (including globally)
- Additional helper decorators
- Swagger documentation
## Install
```shell
npm i @nestjsx/crud class-transformer class-validator
```
## Packages
- [**@nestjsx/crud**](https://www.npmjs.com/package/@nestjsx/crud) - core package which provides `@Crud()` decorator for endpoints generation, global configuration, validation, helper decorators ([docs](https://github.com/nestjsx/crud/wiki/Controllers#description))
- [**@nestjsx/crud-request**](https://www.npmjs.com/package/@nestjsx/crud-request) - request builder/parser package which provides `RequestQueryBuilder` class for a frontend usage and `RequestQueryParser` that is being used internally for handling and validating query/path params on a backend side ([docs](https://github.com/nestjsx/crud/wiki/Requests#frontend-usage))
- [**@nestjsx/crud-typeorm**](https://www.npmjs.com/package/@nestjsx/crud-typeorm) - TypeORM package which provides base `TypeOrmCrudService` with methods for CRUD database operations ([docs](https://github.com/nestjsx/crud/wiki/ServiceTypeorm))
## Documentation
- [General Information](https://github.com/nestjsx/crud/wiki#why)
- [CRUD Controllers](https://github.com/nestjsx/crud/wiki/Controllers#description)
- [CRUD ORM Services](https://github.com/nestjsx/crud/wiki/Services#description)
- [Handling Requests](https://github.com/nestjsx/crud/wiki/Requests#description)
## Support
Any support is welcome. At least you can give us a star.
## Contributors
### Code Contributors
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
### Financial Contributors
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/nestjsx#backer)]
#### Individuals
#### Organizations
Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/nestjsx#sponsor)]
## License
[MIT](LICENSE)
================================================
FILE: packages/crud/package.json
================================================
{
"name": "@nestjsx/crud",
"description": "NestJs CRUD for RESTful APIs",
"version": "5.0.0-alpha.3",
"license": "MIT",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
"publishConfig": {
"access": "public"
},
"files": [
"lib"
],
"repository": {
"type": "git",
"url": "https://github.com/nestjsx/crud.git"
},
"bugs": {
"url": "https://github.com/nestjsx/crud/issues"
},
"keywords": [
"typescript",
"typeorm",
"nest",
"nestjs",
"rest",
"restful",
"api",
"crud",
"crud-generator",
"backend",
"frameworks"
],
"author": {
"name": "Michael Yali",
"email": "mihon4ik@gmail.com"
},
"scripts": {
"build": "npx tsc -b"
},
"dependencies": {
"@nestjsx/crud-request": "^5.0.0-alpha.3",
"@nestjsx/util": "^5.0.0-alpha.3",
"deepmerge": "^3.2.0",
"pluralize": "^8.0.0"
},
"peerDependencies": {
"class-transformer": "*",
"class-validator": "*"
}
}
================================================
FILE: packages/crud/src/constants.ts
================================================
export const FEAUTURE_NAME_METADATA = 'NESTJSX_FEAUTURE_NAME_METADATA';
export const ACTION_NAME_METADATA = 'NESTJSX_ACTION_NAME_METADATA';
export const OVERRIDE_METHOD_METADATA = 'NESTJSX_OVERRIDE_METHOD_METADATA';
export const PARSED_BODY_METADATA = 'NESTJSX_PARSED_BODY_METADATA';
export const PARSED_CRUD_REQUEST_KEY = 'NESTJSX_PARSED_CRUD_REQUEST_KEY';
export const CRUD_OPTIONS_METADATA = 'NESTJSX_CRUD_OPTIONS_METADATA';
export const CRUD_AUTH_OPTIONS_METADATA = 'NESTJSX_CRUD_AUTH_OPTIONS_METADATA';
================================================
FILE: packages/crud/src/crud/crud-routes.factory.ts
================================================
import { RequestMethod } from '@nestjs/common';
import { RouteParamtypes } from '@nestjs/common/enums/route-paramtypes.enum';
import {
isFalse,
isArrayFull,
isObjectFull,
isFunction,
objKeys,
isIn,
isEqual,
getOwnPropNames,
isNil,
isUndefined,
} from '@nestjsx/util';
import * as deepmerge from 'deepmerge';
import { R } from './reflection.helper';
import { SerializeHelper } from './serialize.helper';
import { Swagger } from './swagger.helper';
import { Validation } from './validation.helper';
import { CrudRequestInterceptor, CrudResponseInterceptor } from '../interceptors';
import { BaseRoute, CrudOptions, CrudRequest, MergedCrudOptions } from '../interfaces';
import { BaseRouteName } from '../types';
import { CrudActions, CrudValidationGroups } from '../enums';
import { CrudConfigService } from '../module';
export class CrudRoutesFactory {
protected options: MergedCrudOptions;
protected swaggerModels: any = {};
constructor(protected target: any, options: CrudOptions) {
this.options = options;
this.create();
}
/* istanbul ignore next */
static create(target: any, options: CrudOptions): CrudRoutesFactory {
return new CrudRoutesFactory(target, options);
}
protected get targetProto(): any {
return this.target.prototype;
}
protected get modelName(): string {
return this.options.model.type.name;
}
protected get modelType(): any {
return this.options.model.type;
}
protected get actionsMap(): { [key in BaseRouteName]: CrudActions } {
return {
getManyBase: CrudActions.ReadAll,
getOneBase: CrudActions.ReadOne,
createManyBase: CrudActions.CreateMany,
createOneBase: CrudActions.CreateOne,
updateOneBase: CrudActions.UpdateOne,
deleteOneBase: CrudActions.DeleteOne,
replaceOneBase: CrudActions.ReplaceOne,
recoverOneBase: CrudActions.RecoverOne,
};
}
protected create() {
const routesSchema = this.getRoutesSchema();
this.mergeOptions();
this.setResponseModels();
this.createRoutes(routesSchema);
this.overrideRoutes(routesSchema);
this.enableRoutes(routesSchema);
}
protected mergeOptions() {
// merge auth config
const authOptions = R.getCrudAuthOptions(this.target);
this.options.auth = isObjectFull(authOptions) ? authOptions : {};
if (isUndefined(this.options.auth.property)) {
this.options.auth.property = CrudConfigService.config.auth.property;
}
if (isUndefined(this.options.auth.groups)) {
this.options.auth.groups = CrudConfigService.config.auth.groups;
}
if (isUndefined(this.options.auth.classTransformOptions)) {
this.options.auth.classTransformOptions = CrudConfigService.config.auth.classTransformOptions;
}
// merge query config
const query = isObjectFull(this.options.query) ? this.options.query : {};
this.options.query = { ...CrudConfigService.config.query, ...query };
// merge routes config
const routes = isObjectFull(this.options.routes) ? this.options.routes : {};
this.options.routes = deepmerge(CrudConfigService.config.routes, routes, {
arrayMerge: (a, b, c) => b,
});
// set params
this.options.params = isObjectFull(this.options.params)
? this.options.params
: isObjectFull(CrudConfigService.config.params)
? CrudConfigService.config.params
: {};
const hasPrimary = this.getPrimaryParams().length > 0;
if (!hasPrimary) {
this.options.params['id'] = {
field: 'id',
type: 'number',
primary: true,
};
}
// set dto
if (!isObjectFull(this.options.dto)) {
this.options.dto = {};
}
// set serialize
const serialize = isObjectFull(this.options.serialize) ? this.options.serialize : {};
this.options.serialize = { ...CrudConfigService.config.serialize, ...serialize };
this.options.serialize.get = isFalse(this.options.serialize.get)
? false
: this.options.serialize.get || this.modelType;
this.options.serialize.getMany = isFalse(this.options.serialize.getMany)
? false
: this.options.serialize.getMany
? this.options.serialize.getMany
: isFalse(this.options.serialize.get)
? /* istanbul ignore next */ false
: SerializeHelper.createGetManyDto(this.options.serialize.get, this.modelName);
this.options.serialize.create = isFalse(this.options.serialize.create)
? false
: this.options.serialize.create || this.modelType;
this.options.serialize.update = isFalse(this.options.serialize.update)
? false
: this.options.serialize.update || this.modelType;
this.options.serialize.replace = isFalse(this.options.serialize.replace)
? false
: this.options.serialize.replace || this.modelType;
this.options.serialize.delete =
isFalse(this.options.serialize.delete) || !this.options.routes.deleteOneBase.returnDeleted
? false
: this.options.serialize.delete || this.modelType;
R.setCrudOptions(this.options, this.target);
}
protected getRoutesSchema(): BaseRoute[] {
return [
{
name: 'getOneBase',
path: '/',
method: RequestMethod.GET,
enable: false,
override: false,
withParams: true,
},
{
name: 'getManyBase',
path: '/',
method: RequestMethod.GET,
enable: false,
override: false,
withParams: false,
},
{
name: 'createOneBase',
path: '/',
method: RequestMethod.POST,
enable: false,
override: false,
withParams: false,
},
{
name: 'createManyBase',
path: '/bulk',
method: RequestMethod.POST,
enable: false,
override: false,
withParams: false,
},
{
name: 'updateOneBase',
path: '/',
method: RequestMethod.PATCH,
enable: false,
override: false,
withParams: true,
},
{
name: 'replaceOneBase',
path: '/',
method: RequestMethod.PUT,
enable: false,
override: false,
withParams: true,
},
{
name: 'deleteOneBase',
path: '/',
method: RequestMethod.DELETE,
enable: false,
override: false,
withParams: true,
},
{
name: 'recoverOneBase',
path: '/recover',
method: RequestMethod.PATCH,
enable: false,
override: false,
withParams: true,
},
];
}
protected getManyBase(name: BaseRouteName) {
this.targetProto[name] = function getManyBase(req: CrudRequest) {
return this.service.getMany(req);
};
}
protected getOneBase(name: BaseRouteName) {
this.targetProto[name] = function getOneBase(req: CrudRequest) {
return this.service.getOne(req);
};
}
protected createOneBase(name: BaseRouteName) {
this.targetProto[name] = function createOneBase(req: CrudRequest, dto: any) {
return this.service.createOne(req, dto);
};
}
protected createManyBase(name: BaseRouteName) {
this.targetProto[name] = function createManyBase(req: CrudRequest, dto: any) {
return this.service.createMany(req, dto);
};
}
protected updateOneBase(name: BaseRouteName) {
this.targetProto[name] = function updateOneBase(req: CrudRequest, dto: any) {
return this.service.updateOne(req, dto);
};
}
protected replaceOneBase(name: BaseRouteName) {
this.targetProto[name] = function replaceOneBase(req: CrudRequest, dto: any) {
return this.service.replaceOne(req, dto);
};
}
protected deleteOneBase(name: BaseRouteName) {
this.targetProto[name] = function deleteOneBase(req: CrudRequest) {
return this.service.deleteOne(req);
};
}
protected recoverOneBase(name: BaseRouteName) {
this.targetProto[name] = function recoverOneBase(req: CrudRequest) {
return this.service.recoverOne(req);
};
}
protected canCreateRoute(name: BaseRouteName) {
const only = this.options.routes.only;
const exclude = this.options.routes.exclude;
// include recover route only for models with soft delete option
if (name === 'recoverOneBase' && this.options.query.softDelete !== true) {
return false;
}
if (isArrayFull(only)) {
return only.some((route) => route === name);
}
if (isArrayFull(exclude)) {
return !exclude.some((route) => route === name);
}
return true;
}
protected setResponseModels() {
const modelType = isFunction(this.modelType)
? this.modelType
: SerializeHelper.createGetOneResponseDto(this.modelName);
this.swaggerModels.get = isFunction(this.options.serialize.get) ? this.options.serialize.get : modelType;
this.swaggerModels.getMany =
this.options.serialize.getMany || SerializeHelper.createGetManyDto(this.swaggerModels.get, this.modelName);
this.swaggerModels.create = isFunction(this.options.serialize.create) ? this.options.serialize.create : modelType;
this.swaggerModels.update = isFunction(this.options.serialize.update) ? this.options.serialize.update : modelType;
this.swaggerModels.replace = isFunction(this.options.serialize.replace)
? this.options.serialize.replace
: modelType;
this.swaggerModels.delete = isFunction(this.options.serialize.delete) ? this.options.serialize.delete : modelType;
this.swaggerModels.recover = isFunction(this.options.serialize.recover)
? this.options.serialize.recover
: modelType;
Swagger.setExtraModels(this.swaggerModels);
}
protected createRoutes(routesSchema: BaseRoute[]) {
const primaryParams = this.getPrimaryParams().filter((param) => !this.options.params[param].disabled);
routesSchema.forEach((route) => {
if (this.canCreateRoute(route.name)) {
// create base method
this[route.name](route.name);
route.enable = true;
// set metadata
this.setBaseRouteMeta(route.name);
}
if (route.withParams && primaryParams.length > 0) {
route.path =
route.path !== '/'
? `${primaryParams.map((param) => `/:${param}`).join('')}${route.path}`
: primaryParams.map((param) => `/:${param}`).join('');
}
});
}
protected overrideRoutes(routesSchema: BaseRoute[]) {
getOwnPropNames(this.targetProto).forEach((name) => {
const override = R.getOverrideRoute(this.targetProto[name]);
const route = routesSchema.find((r) => isEqual(r.name, override));
if (override && route && route.enable) {
// get metadata
const interceptors = R.getInterceptors(this.targetProto[name]);
const baseInterceptors = R.getInterceptors(this.targetProto[override]);
const baseAction = R.getAction(this.targetProto[override]);
const operation = Swagger.getOperation(this.targetProto[name]);
const baseOperation = Swagger.getOperation(this.targetProto[override]);
const swaggerParams = Swagger.getParams(this.targetProto[name]);
const baseSwaggerParams = Swagger.getParams(this.targetProto[override]);
const responseOk = Swagger.getResponseOk(this.targetProto[name]);
const baseResponseOk = Swagger.getResponseOk(this.targetProto[override]);
// set metadata
R.setInterceptors([...baseInterceptors, ...interceptors], this.targetProto[name]);
R.setAction(baseAction, this.targetProto[name]);
Swagger.setOperation({ ...baseOperation, ...operation }, this.targetProto[name]);
Swagger.setParams([...baseSwaggerParams, ...swaggerParams], this.targetProto[name]);
Swagger.setResponseOk({ ...baseResponseOk, ...responseOk }, this.targetProto[name]);
this.overrideParsedBodyDecorator(override, name);
// enable route
R.setRoute(route, this.targetProto[name]);
route.override = true;
}
});
}
protected enableRoutes(routesSchema: BaseRoute[]) {
routesSchema.forEach((route) => {
if (!route.override && route.enable) {
R.setRoute(route, this.targetProto[route.name]);
}
});
}
protected overrideParsedBodyDecorator(override: BaseRouteName, name: string) {
const allowed = ['createManyBase', 'createOneBase', 'updateOneBase', 'replaceOneBase'] as BaseRouteName[];
const withBody = isIn(override, allowed);
const parsedBody = R.getParsedBody(this.targetProto[name]);
if (withBody && parsedBody) {
const baseKey = `${RouteParamtypes.BODY}:1`;
const key = `${RouteParamtypes.BODY}:${parsedBody.index}`;
const baseRouteArgs = R.getRouteArgs(this.target, override);
const routeArgs = R.getRouteArgs(this.target, name);
const baseBodyArg = baseRouteArgs[baseKey];
R.setRouteArgs(
{
...routeArgs,
[key]: {
...baseBodyArg,
index: parsedBody.index,
},
},
this.target,
name,
);
/* istanbul ignore else */
if (isEqual(override, 'createManyBase')) {
const paramTypes = R.getRouteArgsTypes(this.targetProto, name);
const metatype = paramTypes[parsedBody.index];
const types = [String, Boolean, Number, Array, Object];
const toCopy = isIn(metatype, types) || /* istanbul ignore next */ isNil(metatype);
/* istanbul ignore else */
if (toCopy) {
const baseParamTypes = R.getRouteArgsTypes(this.targetProto, override);
const baseMetatype = baseParamTypes[1];
paramTypes.splice(parsedBody.index, 1, baseMetatype);
R.setRouteArgsTypes(paramTypes, this.targetProto, name);
}
}
}
}
protected getPrimaryParams(): string[] {
return objKeys(this.options.params).filter(
(param) => this.options.params[param] && this.options.params[param].primary,
);
}
protected setBaseRouteMeta(name: BaseRouteName) {
this.setRouteArgs(name);
this.setRouteArgsTypes(name);
this.setInterceptors(name);
this.setAction(name);
this.setSwaggerOperation(name);
this.setSwaggerPathParams(name);
this.setSwaggerQueryParams(name);
this.setSwaggerResponseOk(name);
// set decorators after Swagger so metadata can be overwritten
this.setDecorators(name);
}
protected setRouteArgs(name: BaseRouteName) {
let rest = {};
const routes: BaseRouteName[] = ['createManyBase', 'createOneBase', 'updateOneBase', 'replaceOneBase'];
if (isIn(name, routes)) {
const action = this.routeNameAction(name);
const hasDto = !isNil(this.options.dto[action]);
const { UPDATE, CREATE } = CrudValidationGroups;
const groupEnum = isIn(name, ['updateOneBase', 'replaceOneBase']) ? UPDATE : CREATE;
const group = !hasDto ? groupEnum : undefined;
rest = R.setBodyArg(1, [Validation.getValidationPipe(this.options, group)]);
}
R.setRouteArgs({ ...R.setParsedRequestArg(0), ...rest }, this.target, name);
}
protected setRouteArgsTypes(name: BaseRouteName) {
if (isEqual(name, 'createManyBase')) {
const bulkDto = Validation.createBulkDto(this.options);
R.setRouteArgsTypes([Object, bulkDto], this.targetProto, name);
} else if (isIn(name, ['createOneBase', 'updateOneBase', 'replaceOneBase'])) {
const action = this.routeNameAction(name);
const dto = this.options.dto[action] || this.modelType;
R.setRouteArgsTypes([Object, dto], this.targetProto, name);
} else {
R.setRouteArgsTypes([Object], this.targetProto, name);
}
}
protected setInterceptors(name: BaseRouteName) {
const interceptors = this.options.routes[name].interceptors;
R.setInterceptors(
[
CrudRequestInterceptor,
CrudResponseInterceptor,
...(isArrayFull(interceptors) ? /* istanbul ignore next */ interceptors : []),
],
this.targetProto[name],
);
}
protected setDecorators(name: BaseRouteName) {
const decorators = this.options.routes[name].decorators;
R.setDecorators(isArrayFull(decorators) ? /* istanbul ignore next */ decorators : [], this.targetProto, name);
}
protected setAction(name: BaseRouteName) {
R.setAction(this.actionsMap[name], this.targetProto[name]);
}
protected setSwaggerOperation(name: BaseRouteName) {
const summary = Swagger.operationsMap(this.modelName)[name];
const operationId = name + this.targetProto.constructor.name + this.modelName;
Swagger.setOperation({ summary, operationId }, this.targetProto[name]);
}
protected setSwaggerPathParams(name: BaseRouteName) {
const metadata = Swagger.getParams(this.targetProto[name]);
const withoutPrimary: BaseRouteName[] = ['createManyBase', 'createOneBase', 'getManyBase'];
const removePrimary = isIn(name, withoutPrimary);
const params = objKeys(this.options.params)
.filter((key) => !this.options.params[key].disabled)
.filter((key) => !(removePrimary && this.options.params[key].primary))
.reduce((a, c) => ({ ...a, [c]: this.options.params[c] }), {});
const pathParamsMeta = Swagger.createPathParamsMeta(params);
Swagger.setParams([...metadata, ...pathParamsMeta], this.targetProto[name]);
}
protected setSwaggerQueryParams(name: BaseRouteName) {
const metadata = Swagger.getParams(this.targetProto[name]);
const queryParamsMeta = Swagger.createQueryParamsMeta(name, this.options);
Swagger.setParams([...metadata, ...queryParamsMeta], this.targetProto[name]);
}
protected setSwaggerResponseOk(name: BaseRouteName) {
const metadata = Swagger.getResponseOk(this.targetProto[name]);
const metadataToAdd =
Swagger.createResponseMeta(name, this.options, this.swaggerModels) || /* istanbul ignore next */ {};
Swagger.setResponseOk({ ...metadata, ...metadataToAdd }, this.targetProto[name]);
}
protected routeNameAction(name: BaseRouteName): string {
return name.split('OneBase')[0] || /* istanbul ignore next */ name.split('ManyBase')[0];
}
}
================================================
FILE: packages/crud/src/crud/index.ts
================================================
export * from './crud-routes.factory';
export * from './reflection.helper';
export * from './swagger.helper';
export * from './validation.helper';
================================================
FILE: packages/crud/src/crud/reflection.helper.ts
================================================
import { RouteParamtypes } from '@nestjs/common/enums/route-paramtypes.enum';
import {
CUSTOM_ROUTE_AGRS_METADATA,
INTERCEPTORS_METADATA,
METHOD_METADATA,
PARAMTYPES_METADATA,
PATH_METADATA,
ROUTE_ARGS_METADATA,
} from '@nestjs/common/constants';
import { ArgumentsHost } from '@nestjs/common';
import { isFunction } from '@nestjsx/util';
import { BaseRoute, MergedCrudOptions, AuthOptions } from '../interfaces';
import { BaseRouteName } from '../types';
import {
CRUD_OPTIONS_METADATA,
ACTION_NAME_METADATA,
PARSED_CRUD_REQUEST_KEY,
PARSED_BODY_METADATA,
OVERRIDE_METHOD_METADATA,
CRUD_AUTH_OPTIONS_METADATA,
} from '../constants';
import { CrudActions } from '../enums';
export class R {
static set(metadataKey: any, metadataValue: any, target: unknown, propertyKey: string | symbol = undefined) {
if (propertyKey) {
Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);
} else {
Reflect.defineMetadata(metadataKey, metadataValue, target);
}
}
static get(metadataKey: any, target: unknown, propertyKey: string | symbol = undefined): T {
return propertyKey
? Reflect.getMetadata(metadataKey, target, propertyKey)
: Reflect.getMetadata(metadataKey, target);
}
static createCustomRouteArg(
paramtype: string,
index: number,
/* istanbul ignore next */
pipes: any[] = [],
data = undefined,
): any {
return {
[`${paramtype}${CUSTOM_ROUTE_AGRS_METADATA}:${index}`]: {
index,
factory: (_, ctx) => R.getContextRequest(ctx)[paramtype],
data,
pipes,
},
};
}
static createRouteArg(
paramtype: RouteParamtypes,
index: number,
/* istanbul ignore next */
pipes: any[] = [],
data = undefined,
): any {
return {
[`${paramtype}:${index}`]: {
index,
pipes,
data,
},
};
}
static setDecorators(decorators: (PropertyDecorator | MethodDecorator)[], target: any, name: string) {
// this makes metadata decorator works
const decoratedDescriptor = Reflect.decorate(
decorators,
target,
name,
Reflect.getOwnPropertyDescriptor(target, name),
);
// this makes proxy decorator works
Reflect.defineProperty(target, name, decoratedDescriptor);
}
static setParsedRequestArg(index: number) {
return R.createCustomRouteArg(PARSED_CRUD_REQUEST_KEY, index);
}
static setBodyArg(index: number, /* istanbul ignore next */ pipes: any[] = []) {
return R.createRouteArg(RouteParamtypes.BODY, index, pipes);
}
static setCrudOptions(options: MergedCrudOptions, target: any) {
R.set(CRUD_OPTIONS_METADATA, options, target);
}
static setRoute(route: BaseRoute, func: unknown) {
R.set(PATH_METADATA, route.path, func);
R.set(METHOD_METADATA, route.method, func);
}
static setInterceptors(interceptors: any[], func: unknown) {
R.set(INTERCEPTORS_METADATA, interceptors, func);
}
static setRouteArgs(metadata: any, target: any, name: string) {
R.set(ROUTE_ARGS_METADATA, metadata, target, name);
}
static setRouteArgsTypes(metadata: any, target: any, name: string) {
R.set(PARAMTYPES_METADATA, metadata, target, name);
}
static setAction(action: CrudActions, func: unknown) {
R.set(ACTION_NAME_METADATA, action, func);
}
static setCrudAuthOptions(metadata: any, target: any) {
R.set(CRUD_AUTH_OPTIONS_METADATA, metadata, target);
}
static getCrudAuthOptions(target: any): AuthOptions {
return R.get(CRUD_AUTH_OPTIONS_METADATA, target);
}
static getCrudOptions(target: any): MergedCrudOptions {
return R.get(CRUD_OPTIONS_METADATA, target);
}
static getAction(func: unknown): CrudActions {
return R.get(ACTION_NAME_METADATA, func);
}
static getOverrideRoute(func: unknown): BaseRouteName {
return R.get(OVERRIDE_METHOD_METADATA, func);
}
static getInterceptors(func: unknown): any[] {
return R.get(INTERCEPTORS_METADATA, func) || [];
}
static getRouteArgs(target: any, name: string): any {
return R.get(ROUTE_ARGS_METADATA, target, name);
}
static getRouteArgsTypes(target: any, name: string): any[] {
return R.get(PARAMTYPES_METADATA, target, name) || /* istanbul ignore next */ [];
}
static getParsedBody(func: unknown): any {
return R.get(PARSED_BODY_METADATA, func);
}
static getContextRequest(ctx: ArgumentsHost): any {
return isFunction(ctx.switchToHttp) ? ctx.switchToHttp().getRequest() : /* istanbul ignore next */ ctx;
}
}
================================================
FILE: packages/crud/src/crud/serialize.helper.ts
================================================
import { Type } from 'class-transformer';
import { GetManyDefaultResponse } from '../interfaces';
import { ApiProperty } from './swagger.helper';
export class SerializeHelper {
static createGetManyDto(dto: any, resourceName: string): any {
class GetManyResponseDto implements GetManyDefaultResponse {
@ApiProperty({ type: dto, isArray: true })
@Type(() => dto)
data: any[];
@ApiProperty({ type: 'number' })
count: number;
@ApiProperty({ type: 'number' })
total: number;
@ApiProperty({ type: 'number' })
page: number;
@ApiProperty({ type: 'number' })
pageCount: number;
}
Object.defineProperty(GetManyResponseDto, 'name', {
writable: false,
value: `GetMany${resourceName}ResponseDto`,
});
return GetManyResponseDto;
}
static createGetOneResponseDto(resourceName: string): any {
class GetOneResponseDto {}
Object.defineProperty(GetOneResponseDto, 'name', {
writable: false,
value: `${resourceName}ResponseDto`,
});
return GetOneResponseDto;
}
}
================================================
FILE: packages/crud/src/crud/swagger.helper.ts
================================================
import { HttpStatus } from '@nestjs/common';
import { RequestQueryBuilder } from '@nestjsx/crud-request';
import { isString, objKeys } from '@nestjsx/util';
import { MergedCrudOptions, ParamsOptions } from '../interfaces';
import { BaseRouteName } from '../types';
import { safeRequire } from '../util';
import { R } from './reflection.helper';
const pluralize = require('pluralize');
export const swagger = safeRequire('@nestjs/swagger', () => require('@nestjs/swagger'));
export const swaggerConst = safeRequire('@nestjs/swagger/dist/constants', () =>
require('@nestjs/swagger/dist/constants'),
);
export const swaggerPkgJson = safeRequire('@nestjs/swagger/package.json', () =>
require('@nestjs/swagger/package.json'),
);
export class Swagger {
static operationsMap(modelName: string): { [key in BaseRouteName]: string } {
return {
getManyBase: `Retrieve multiple ${pluralize(modelName)}`,
getOneBase: `Retrieve a single ${modelName}`,
createManyBase: `Create multiple ${pluralize(modelName)}`,
createOneBase: `Create a single ${modelName}`,
updateOneBase: `Update a single ${modelName}`,
replaceOneBase: `Replace a single ${modelName}`,
deleteOneBase: `Delete a single ${modelName}`,
recoverOneBase: `Recover one ${modelName}`,
};
}
static setOperation(metadata: unknown, func: any): void {
/* istanbul ignore else */
if (swaggerConst) {
R.set(swaggerConst.DECORATORS.API_OPERATION, metadata, func);
}
}
static setParams(metadata: unknown, func: any): void {
/* istanbul ignore else */
if (swaggerConst) {
R.set(swaggerConst.DECORATORS.API_PARAMETERS, metadata, func);
}
}
static setExtraModels(swaggerModels: any): void {
/* istanbul ignore else */
if (swaggerConst) {
const meta = Swagger.getExtraModels(swaggerModels.get);
const models: any[] = [
...meta,
...objKeys(swaggerModels)
.map((name) => swaggerModels[name])
.filter((one) => one && one.name !== swaggerModels.get.name),
];
R.set(swaggerConst.DECORATORS.API_EXTRA_MODELS, models, swaggerModels.get);
}
}
static setResponseOk(metadata: unknown, func: any): void {
/* istanbul ignore else */
if (swaggerConst) {
R.set(swaggerConst.DECORATORS.API_RESPONSE, metadata, func);
}
}
static getOperation(func: any): any {
/* istanbul ignore next */
return swaggerConst ? R.get(swaggerConst.DECORATORS.API_OPERATION, func) || {} : {};
}
static getParams(func: any): any[] {
/* istanbul ignore next */
return swaggerConst ? R.get(swaggerConst.DECORATORS.API_PARAMETERS, func) || [] : [];
}
static getExtraModels(target: unknown): any[] {
/* istanbul ignore next */
return swaggerConst ? R.get(swaggerConst.API_EXTRA_MODELS, target) || [] : [];
}
static getResponseOk(func: any): any {
/* istanbul ignore next */
return swaggerConst ? R.get(swaggerConst.DECORATORS.API_RESPONSE, func) || {} : {};
}
static createResponseMeta(name: BaseRouteName, options: MergedCrudOptions, swaggerModels: any): any {
/* istanbul ignore else */
if (swagger) {
const { routes, query } = options;
const oldVersion = Swagger.getSwaggerVersion() < 4;
switch (name) {
case 'getOneBase':
return {
[HttpStatus.OK]: {
description: 'Get one base response',
type: swaggerModels.get,
},
};
case 'getManyBase':
/* istanbul ignore if */
if (oldVersion) {
return {
[HttpStatus.OK]: {
type: swaggerModels.getMany,
},
};
}
return {
[HttpStatus.OK]: query.alwaysPaginate
? {
description: 'Get paginated response',
type: swaggerModels.getMany,
}
: {
description: 'Get many base response',
schema: {
oneOf: [
{ $ref: swagger.getSchemaPath(swaggerModels.getMany.name) },
{
type: 'array',
items: { $ref: swagger.getSchemaPath(swaggerModels.get.name) },
},
],
},
},
};
case 'createOneBase':
/* istanbul ignore if */
if (oldVersion) {
return {
[HttpStatus.OK]: {
type: swaggerModels.create,
},
};
}
return {
[HttpStatus.CREATED]: {
description: 'Get create one base response',
schema: { $ref: swagger.getSchemaPath(swaggerModels.create.name) },
},
};
case 'createManyBase':
/* istanbul ignore if */
if (oldVersion) {
return {
[HttpStatus.OK]: {
type: swaggerModels.create,
isArray: true,
},
};
}
return {
[HttpStatus.CREATED]: swaggerModels.createMany
? /* istanbul ignore next */ {
description: 'Get create many base response',
schema: { $ref: swagger.getSchemaPath(swaggerModels.createMany.name) },
}
: {
description: 'Get create many base response',
schema: {
type: 'array',
items: { $ref: swagger.getSchemaPath(swaggerModels.create.name) },
},
},
};
case 'deleteOneBase':
/* istanbul ignore if */
if (oldVersion) {
return {
[HttpStatus.OK]: routes.deleteOneBase.returnDeleted
? {
type: swaggerModels.delete,
}
: {},
};
}
return {
[HttpStatus.OK]: routes.deleteOneBase.returnDeleted
? {
description: 'Delete one base response',
schema: { $ref: swagger.getSchemaPath(swaggerModels.delete.name) },
}
: {
description: 'Delete one base response',
},
};
case 'recoverOneBase':
/* istanbul ignore if */
if (oldVersion) {
return {
[HttpStatus.OK]: routes.recoverOneBase.returnRecovered
? {
type: swaggerModels.delete,
}
: {},
};
}
return {
[HttpStatus.OK]: routes.recoverOneBase.returnRecovered
? {
description: 'Recover one base response',
schema: { $ref: swagger.getSchemaPath(swaggerModels.recover.name) },
}
: {
description: 'Recover one base response',
},
};
default:
const dto = swaggerModels[name.split('OneBase')[0]];
/* istanbul ignore if */
if (oldVersion) {
return {
[HttpStatus.OK]: {
type: dto,
},
};
}
return {
[HttpStatus.OK]: {
description: 'Response',
schema: { $ref: swagger.getSchemaPath(dto.name) },
},
};
}
} else {
return {};
}
}
static createPathParamsMeta(options: ParamsOptions): any[] {
return swaggerConst
? objKeys(options).map((param) => ({
name: param,
required: true,
in: 'path',
type: options[param].type === 'number' ? Number : String,
enum: options[param].enum ? Object.values(options[param].enum) : undefined,
}))
: /* istanbul ignore next */ [];
}
static createQueryParamsMeta(name: BaseRouteName, options: MergedCrudOptions) {
/* istanbul ignore if */
if (!swaggerConst) {
return [];
}
const {
delim: d,
delimStr: coma,
fields,
search,
filter,
or,
join,
sort,
limit,
offset,
page,
cache,
includeDeleted,
} = Swagger.getQueryParamsNames();
const oldVersion = Swagger.getSwaggerVersion() < 4;
const docsLink = (a: string) =>
`Docs`;
const fieldsMetaBase = {
name: fields,
description: `Selects resource fields. ${docsLink('select')}`,
required: false,
in: 'query',
};
const fieldsMeta = oldVersion
? /* istanbul ignore next */ {
...fieldsMetaBase,
type: 'array',
items: {
type: 'string',
},
collectionFormat: 'csv',
}
: {
...fieldsMetaBase,
schema: {
type: 'array',
items: {
type: 'string',
},
},
style: 'form',
explode: false,
};
const searchMetaBase = {
name: search,
description: `Adds search condition. ${docsLink('search')}`,
required: false,
in: 'query',
};
const searchMeta = oldVersion
? /* istanbul ignore next */ { ...searchMetaBase, type: 'string' }
: { ...searchMetaBase, schema: { type: 'string' } };
const filterMetaBase = {
name: filter,
description: `Adds filter condition. ${docsLink('filter')}`,
required: false,
in: 'query',
};
const filterMeta = oldVersion
? /* istanbul ignore next */ {
...filterMetaBase,
items: {
type: 'string',
},
type: 'array',
collectionFormat: 'multi',
}
: {
...filterMetaBase,
schema: {
type: 'array',
items: {
type: 'string',
},
},
style: 'form',
explode: true,
};
const orMetaBase = {
name: or,
description: `Adds OR condition. ${docsLink('or')}`,
required: false,
in: 'query',
};
const orMeta = oldVersion
? /* istanbul ignore next */ {
...orMetaBase,
items: {
type: 'string',
},
type: 'array',
collectionFormat: 'multi',
}
: {
...orMetaBase,
schema: {
type: 'array',
items: {
type: 'string',
},
},
style: 'form',
explode: true,
};
const sortMetaBase = {
name: sort,
description: `Adds sort by field. ${docsLink('sort')}`,
required: false,
in: 'query',
};
const sortMeta = oldVersion
? /* istanbul ignore next */ {
...sortMetaBase,
items: {
type: 'string',
},
type: 'array',
collectionFormat: 'multi',
}
: {
...sortMetaBase,
schema: {
type: 'array',
items: {
type: 'string',
},
},
style: 'form',
explode: true,
};
const joinMetaBase = {
name: join,
description: `Adds relational resources. ${docsLink('join')}`,
required: false,
in: 'query',
};
const joinMeta = oldVersion
? /* istanbul ignore next */ {
...joinMetaBase,
items: {
type: 'string',
},
type: 'array',
collectionFormat: 'multi',
}
: {
...joinMetaBase,
schema: {
type: 'array',
items: {
type: 'string',
},
},
style: 'form',
explode: true,
};
const limitMetaBase = {
name: limit,
description: `Limit amount of resources. ${docsLink('limit')}`,
required: false,
in: 'query',
};
const limitMeta = oldVersion
? /* istanbul ignore next */ { ...limitMetaBase, type: 'integer' }
: { ...limitMetaBase, schema: { type: 'integer' } };
const offsetMetaBase = {
name: offset,
description: `Offset amount of resources. ${docsLink('offset')}`,
required: false,
in: 'query',
};
const offsetMeta = oldVersion
? /* istanbul ignore next */ { ...offsetMetaBase, type: 'integer' }
: { ...offsetMetaBase, schema: { type: 'integer' } };
const pageMetaBase = {
name: page,
description: `Page portion of resources. ${docsLink('page')}`,
required: false,
in: 'query',
};
const pageMeta = oldVersion
? /* istanbul ignore next */ { ...pageMetaBase, type: 'integer' }
: { ...pageMetaBase, schema: { type: 'integer' } };
const cacheMetaBase = {
name: cache,
description: `Reset cache (if was enabled). ${docsLink('cache')}`,
required: false,
in: 'query',
};
const cacheMeta = oldVersion
? /* istanbul ignore next */ {
...cacheMetaBase,
type: 'integer',
minimum: 0,
maximum: 1,
}
: { ...cacheMetaBase, schema: { type: 'integer', minimum: 0, maximum: 1 } };
const includeDeletedMetaBase = {
name: includeDeleted,
description: `Include deleted. ${docsLink('includeDeleted')}`,
required: false,
in: 'query',
};
const includeDeletedMeta = oldVersion
? /* istanbul ignore next */ {
...includeDeletedMetaBase,
type: 'integer',
minimum: 0,
maximum: 1,
}
: {
...includeDeletedMetaBase,
schema: { type: 'integer', minimum: 0, maximum: 1 },
};
switch (name) {
case 'getManyBase':
return options.query.softDelete
? [
fieldsMeta,
searchMeta,
filterMeta,
orMeta,
sortMeta,
joinMeta,
limitMeta,
offsetMeta,
pageMeta,
cacheMeta,
includeDeletedMeta,
]
: [
fieldsMeta,
searchMeta,
filterMeta,
orMeta,
sortMeta,
joinMeta,
limitMeta,
offsetMeta,
pageMeta,
cacheMeta,
];
case 'getOneBase':
return options.query.softDelete
? [fieldsMeta, joinMeta, cacheMeta, includeDeletedMeta]
: [fieldsMeta, joinMeta, cacheMeta];
default:
return [];
}
}
static getQueryParamsNames(): any {
const qbOptions = RequestQueryBuilder.getOptions();
const name = (n) => {
const selected = qbOptions.paramNamesMap[n];
return isString(selected) ? selected : selected[0];
};
return {
delim: qbOptions.delim,
delimStr: qbOptions.delimStr,
fields: name('fields'),
search: name('search'),
filter: name('filter'),
or: name('or'),
join: name('join'),
sort: name('sort'),
limit: name('limit'),
offset: name('offset'),
page: name('page'),
cache: name('cache'),
includeDeleted: name('includeDeleted'),
};
}
private static getSwaggerVersion(): number {
return swaggerPkgJson ? parseInt(swaggerPkgJson.version[0], 10) : /* istanbul ignore next */ 3;
}
}
// tslint:disable-next-line:ban-types
export function ApiProperty(options?: any): PropertyDecorator {
return (target: unknown, propertyKey: string | symbol) => {
/* istanbul ignore else */
if (swagger) {
// tslint:disable-next-line
const ApiPropertyDecorator = swagger.ApiProperty || /* istanbul ignore next */ swagger.ApiModelProperty;
// tslint:disable-next-line
ApiPropertyDecorator(options)(target, propertyKey);
}
};
}
================================================
FILE: packages/crud/src/crud/validation.helper.ts
================================================
import { ValidationPipe } from '@nestjs/common';
import { isFalse, isNil } from '@nestjsx/util';
import { CrudValidationGroups } from '../enums';
import { CreateManyDto, CrudOptions, MergedCrudOptions } from '../interfaces';
import { safeRequire } from '../util';
import { ApiProperty } from './swagger.helper';
const validator = safeRequire('class-validator', () => require('class-validator'));
const transformer = safeRequire('class-transformer', () => require('class-transformer'));
class BulkDto implements CreateManyDto {
bulk: T[];
}
export class Validation {
static getValidationPipe(options: CrudOptions, group?: CrudValidationGroups): ValidationPipe {
return validator && !isFalse(options.validation)
? new ValidationPipe({
...(options.validation || {}),
groups: group ? [group] : undefined,
})
: /* istanbul ignore next */ undefined;
}
static createBulkDto(options: MergedCrudOptions): any {
/* istanbul ignore else */
if (validator && transformer && !isFalse(options.validation)) {
const { IsArray, ArrayNotEmpty, ValidateNested } = validator;
const { Type } = transformer;
const hasDto = !isNil(options.dto.create);
const groups = !hasDto ? [CrudValidationGroups.CREATE] : undefined;
const always = hasDto ? true : undefined;
const Model = hasDto ? options.dto.create : options.model.type;
// tslint:disable-next-line:max-classes-per-file
class BulkDtoImpl implements CreateManyDto {
@ApiProperty({ type: Model, isArray: true })
@IsArray({ groups, always })
@ArrayNotEmpty({ groups, always })
@ValidateNested({ each: true, groups, always })
@Type(() => Model)
bulk: T[];
}
Object.defineProperty(BulkDtoImpl, 'name', {
writable: false,
value: `CreateMany${options.model.type.name}Dto`,
});
return BulkDtoImpl;
} else {
return BulkDto;
}
}
}
================================================
FILE: packages/crud/src/decorators/crud-auth.decorator.ts
================================================
import { R } from '../crud/reflection.helper';
import { AuthOptions } from '../interfaces';
export const CrudAuth =
(options: AuthOptions) =>
(target: unknown): void => {
R.setCrudAuthOptions(options, target);
};
================================================
FILE: packages/crud/src/decorators/crud.decorator.ts
================================================
import { CrudRoutesFactory } from '../crud';
import { CrudOptions } from '../interfaces';
export const Crud =
(options: CrudOptions) =>
(target: unknown): void => {
const factoryMethod = options.routesFactory || CrudRoutesFactory;
const factory = new factoryMethod(target, options);
};
================================================
FILE: packages/crud/src/decorators/feature-action.decorator.ts
================================================
import { SetMetadata, Type } from '@nestjs/common';
import { ACTION_NAME_METADATA, FEAUTURE_NAME_METADATA } from '../constants';
export const Feature = (name: string) => SetMetadata(FEAUTURE_NAME_METADATA, name);
export const Action = (name: string) => SetMetadata(ACTION_NAME_METADATA, name);
export const getFeature = (target: Type) => Reflect.getMetadata(FEAUTURE_NAME_METADATA, target);
export const getAction = (target: unknown) => Reflect.getMetadata(ACTION_NAME_METADATA, target);
================================================
FILE: packages/crud/src/decorators/index.ts
================================================
export * from './crud.decorator';
export * from './crud-auth.decorator';
export * from './override.decorator';
export * from './parsed-request.decorator';
export * from './parsed-body.decorator';
export * from './feature-action.decorator';
================================================
FILE: packages/crud/src/decorators/override.decorator.ts
================================================
import { BaseRouteName } from '../types/base-route-name.type';
import { OVERRIDE_METHOD_METADATA } from '../constants';
export const Override = (name?: BaseRouteName) => (
target,
key,
descriptor: PropertyDescriptor,
) => {
Reflect.defineMetadata(OVERRIDE_METHOD_METADATA, name || `${key}Base`, target[key]);
return descriptor;
};
================================================
FILE: packages/crud/src/decorators/parsed-body.decorator.ts
================================================
import { PARSED_BODY_METADATA } from '../constants';
export const ParsedBody = () => (target, key, index) => {
Reflect.defineMetadata(PARSED_BODY_METADATA, { index }, target[key]);
};
================================================
FILE: packages/crud/src/decorators/parsed-request.decorator.ts
================================================
import { createParamDecorator } from '@nestjs/common';
import { PARSED_CRUD_REQUEST_KEY } from '../constants';
import { R } from '../crud/reflection.helper';
export const ParsedRequest = createParamDecorator(
(_, ctx): ParameterDecorator => {
return R.getContextRequest(ctx)[PARSED_CRUD_REQUEST_KEY];
},
);
================================================
FILE: packages/crud/src/enums/crud-actions.enum.ts
================================================
export enum CrudActions {
ReadAll = 'Read-All',
ReadOne = 'Read-One',
CreateOne = 'Create-One',
CreateMany = 'Create-Many',
UpdateOne = 'Update-One',
ReplaceOne = 'Replace-One',
DeleteOne = 'Delete-One',
DeleteAll = 'Delete-All',
RecoverOne = 'Recover-One',
}
================================================
FILE: packages/crud/src/enums/crud-validation-groups.enum.ts
================================================
export enum CrudValidationGroups {
CREATE = 'CRUD-CREATE',
UPDATE = 'CRUD-UPDATE',
}
================================================
FILE: packages/crud/src/enums/index.ts
================================================
export * from './crud-actions.enum';
export * from './crud-validation-groups.enum';
================================================
FILE: packages/crud/src/index.ts
================================================
export * from './crud/crud-routes.factory';
export * from './decorators';
export * from './enums';
export * from './interfaces';
export * from './types';
export * from './module';
export * from './interceptors';
export * from './services';
================================================
FILE: packages/crud/src/interceptors/crud-base.interceptor.ts
================================================
import { ExecutionContext } from '@nestjs/common';
import { R } from '../crud/reflection.helper';
import { CrudActions } from '../enums';
import { MergedCrudOptions } from '../interfaces';
export class CrudBaseInterceptor {
protected getCrudInfo(
context: ExecutionContext,
): {
ctrlOptions: MergedCrudOptions;
crudOptions: Partial;
action: CrudActions;
} {
const ctrl = context.getClass();
const handler = context.getHandler();
const ctrlOptions = R.getCrudOptions(ctrl);
const crudOptions = ctrlOptions
? ctrlOptions
: {
query: {},
routes: {},
params: {},
};
const action = R.getAction(handler);
return { ctrlOptions, crudOptions, action };
}
}
================================================
FILE: packages/crud/src/interceptors/crud-request.interceptor.ts
================================================
import { BadRequestException, CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { RequestQueryException, RequestQueryParser, SCondition, QueryFilter } from '@nestjsx/crud-request';
import { isNil, isFunction, isArrayFull, hasLength } from '@nestjsx/util';
import { ClassTransformOptions } from 'class-transformer';
import { PARSED_CRUD_REQUEST_KEY } from '../constants';
import { CrudActions } from '../enums';
import { MergedCrudOptions, CrudRequest } from '../interfaces';
import { QueryFilterFunction } from '../types';
import { CrudBaseInterceptor } from './crud-base.interceptor';
@Injectable()
export class CrudRequestInterceptor extends CrudBaseInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler) {
const req = context.switchToHttp().getRequest();
try {
/* istanbul ignore else */
if (!req[PARSED_CRUD_REQUEST_KEY]) {
const { ctrlOptions, crudOptions, action } = this.getCrudInfo(context);
const parser = RequestQueryParser.create();
parser.parseQuery(req.query);
if (!isNil(ctrlOptions)) {
const search = this.getSearch(parser, crudOptions, action, req.params);
const auth = this.getAuth(parser, crudOptions, req);
parser.search = auth.or ? { $or: [auth.or, { $and: search }] } : { $and: [auth.filter, ...search] };
} else {
parser.search = { $and: this.getSearch(parser, crudOptions, action) };
}
req[PARSED_CRUD_REQUEST_KEY] = this.getCrudRequest(parser, crudOptions);
}
return next.handle();
} catch (error) {
/* istanbul ignore next */
throw error instanceof RequestQueryException ? new BadRequestException(error.message) : error;
}
}
getCrudRequest(parser: RequestQueryParser, crudOptions: Partial): CrudRequest {
const parsed = parser.getParsed();
const { query, routes, params } = crudOptions;
return {
parsed,
options: {
query,
routes,
params,
},
};
}
getSearch(
parser: RequestQueryParser,
crudOptions: Partial,
action: CrudActions,
params?: any,
): SCondition[] {
// params condition
const paramsSearch = this.getParamsSearch(parser, crudOptions, params);
// if `CrudOptions.query.filter` is a function then return transformed query search conditions
if (isFunction(crudOptions.query.filter)) {
const filterCond =
(crudOptions.query.filter as QueryFilterFunction)(parser.search, action === CrudActions.ReadAll) ||
/* istanbul ignore next */ {};
return [...paramsSearch, filterCond];
}
// if `CrudOptions.query.filter` is array or search condition type
const optionsFilter = isArrayFull(crudOptions.query.filter)
? (crudOptions.query.filter as QueryFilter[]).map(parser.convertFilterToSearch)
: [(crudOptions.query.filter as SCondition) || {}];
let search: SCondition[] = [];
if (parser.search) {
search = [parser.search];
} else if (hasLength(parser.filter) && hasLength(parser.or)) {
search =
parser.filter.length === 1 && parser.or.length === 1
? [
{
$or: [parser.convertFilterToSearch(parser.filter[0]), parser.convertFilterToSearch(parser.or[0])],
},
]
: [
{
$or: [
{ $and: parser.filter.map(parser.convertFilterToSearch) },
{ $and: parser.or.map(parser.convertFilterToSearch) },
],
},
];
} else if (hasLength(parser.filter)) {
search = parser.filter.map(parser.convertFilterToSearch);
} else {
if (hasLength(parser.or)) {
search =
parser.or.length === 1
? [parser.convertFilterToSearch(parser.or[0])]
: /* istanbul ignore next */ [
{
$or: parser.or.map(parser.convertFilterToSearch),
},
];
}
}
return [...paramsSearch, ...optionsFilter, ...search];
}
getParamsSearch(parser: RequestQueryParser, crudOptions: Partial, params?: any): SCondition[] {
if (params) {
parser.parseParams(params, crudOptions.params);
return isArrayFull(parser.paramsFilter) ? parser.paramsFilter.map(parser.convertFilterToSearch) : [];
}
return [];
}
getAuth(parser: RequestQueryParser, crudOptions: Partial, req: any): { filter?: any; or?: any } {
const auth: any = {};
/* istanbul ignore else */
if (crudOptions.auth) {
const userOrRequest = crudOptions.auth.property ? req[crudOptions.auth.property] : req;
if (isFunction(crudOptions.auth.or)) {
auth.or = crudOptions.auth.or(userOrRequest);
}
if (isFunction(crudOptions.auth.filter) && !auth.or) {
auth.filter = crudOptions.auth.filter(userOrRequest) || /* istanbul ignore next */ {};
}
if (isFunction(crudOptions.auth.persist)) {
parser.setAuthPersist(crudOptions.auth.persist(userOrRequest));
}
const options: ClassTransformOptions = {};
if (isFunction(crudOptions.auth.classTransformOptions)) {
Object.assign(options, crudOptions.auth.classTransformOptions(userOrRequest));
}
if (isFunction(crudOptions.auth.groups)) {
options.groups = crudOptions.auth.groups(userOrRequest);
}
parser.setClassTransformOptions(options);
}
return auth;
}
}
================================================
FILE: packages/crud/src/interceptors/crud-response.interceptor.ts
================================================
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { isFalse, isObject, isFunction } from '@nestjsx/util';
import { classToPlain, classToPlainFromExist, ClassTransformOptions } from 'class-transformer';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { CrudActions } from '../enums';
import { SerializeOptions } from '../interfaces';
import { CrudBaseInterceptor } from './crud-base.interceptor';
const actionToDtoNameMap: {
[key in CrudActions]: keyof SerializeOptions;
} = {
[CrudActions.ReadAll]: 'getMany',
[CrudActions.ReadOne]: 'get',
[CrudActions.CreateMany]: 'createMany',
[CrudActions.CreateOne]: 'create',
[CrudActions.UpdateOne]: 'update',
[CrudActions.ReplaceOne]: 'replace',
[CrudActions.DeleteAll]: 'delete',
[CrudActions.DeleteOne]: 'delete',
[CrudActions.RecoverOne]: 'recover',
};
@Injectable()
export class CrudResponseInterceptor extends CrudBaseInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable {
return next.handle().pipe(map((data) => this.serialize(context, data)));
}
protected transform(dto: any, data: any, options: ClassTransformOptions) {
if (!isObject(data) || isFalse(dto)) {
return data;
}
if (!isFunction(dto)) {
return data.constructor !== Object ? classToPlain(data, options) : data;
}
return data instanceof dto
? classToPlain(data, options)
: classToPlain(classToPlainFromExist(data, new dto()), options);
}
protected serialize(context: ExecutionContext, data: any): any {
const req = context.switchToHttp().getRequest();
const { crudOptions, action } = this.getCrudInfo(context);
const { serialize } = crudOptions;
const dto = serialize[actionToDtoNameMap[action]];
const isArray = Array.isArray(data);
const options: ClassTransformOptions = {};
/* istanbul ignore else */
if (isFunction(crudOptions.auth?.classTransformOptions)) {
const userOrRequest = crudOptions.auth.property ? req[crudOptions.auth.property] : req;
Object.assign(options, crudOptions.auth.classTransformOptions(userOrRequest));
}
/* istanbul ignore else */
if (isFunction(crudOptions.auth?.groups)) {
const userOrRequest = crudOptions.auth.property ? req[crudOptions.auth.property] : req;
options.groups = crudOptions.auth.groups(userOrRequest);
}
switch (action) {
case CrudActions.ReadAll:
return isArray
? (data as any[]).map((item) => this.transform(serialize.get, item, options))
: this.transform(dto, data, options);
case CrudActions.CreateMany:
return isArray
? (data as any[]).map((item) => this.transform(dto, item, options))
: this.transform(dto, data, options);
default:
return this.transform(dto, data, options);
}
}
}
================================================
FILE: packages/crud/src/interceptors/index.ts
================================================
export * from './crud-request.interceptor';
export * from './crud-response.interceptor';
================================================
FILE: packages/crud/src/interfaces/auth-options.interface.ts
================================================
import { SCondition } from '@nestjsx/crud-request/lib/types/request-query.types';
import { ObjectLiteral } from '@nestjsx/util';
import { ClassTransformOptions } from 'class-transformer';
export interface AuthGlobalOptions {
property?: string;
/** Get options for the `classToPlain` function (response) */
classTransformOptions?: (req: any) => ClassTransformOptions;
/** Get `groups` value for the `classToPlain` function options (response) */
groups?: (req: any) => string[];
}
export interface AuthOptions {
property?: string;
/** Get options for the `classToPlain` function (response) */
classTransformOptions?: (req: any) => ClassTransformOptions;
/** Get `groups` value for the `classToPlain` function options (response) */
groups?: (req: any) => string[];
filter?: (req: any) => SCondition | void;
or?: (req: any) => SCondition | void;
persist?: (req: any) => ObjectLiteral;
}
================================================
FILE: packages/crud/src/interfaces/base-route.interface.ts
================================================
import { RequestMethod } from '@nestjs/common';
import { BaseRouteName } from '../types';
export interface BaseRoute {
name: BaseRouteName;
path: string;
method: RequestMethod;
enable: boolean;
override: boolean;
withParams: boolean;
}
================================================
FILE: packages/crud/src/interfaces/create-many-dto.interface.ts
================================================
export interface CreateManyDto {
bulk: T[];
}
================================================
FILE: packages/crud/src/interfaces/crud-controller.interface.ts
================================================
import { CrudService } from '../services';
import { CrudRequest, GetManyDefaultResponse, CreateManyDto } from '../interfaces';
export interface CrudController {
service: CrudService;
getManyBase?(req: CrudRequest): Promise | T[]>;
getOneBase?(req: CrudRequest): Promise;
createOneBase?(req: CrudRequest, dto: T): Promise;
createManyBase?(req: CrudRequest, dto: CreateManyDto): Promise;
updateOneBase?(req: CrudRequest, dto: T): Promise;
replaceOneBase?(req: CrudRequest, dto: T): Promise;
deleteOneBase?(req: CrudRequest): Promise;
recoverOneBase?(req: CrudRequest): Promise;
}
================================================
FILE: packages/crud/src/interfaces/crud-global-config.interface.ts
================================================
import { RequestQueryBuilderOptions } from '@nestjsx/crud-request';
import { RoutesOptions } from './routes-options.interface';
import { ParamsOptions } from './params-options.interface';
import { AuthGlobalOptions } from './auth-options.interface';
export interface CrudGlobalConfig {
queryParser?: RequestQueryBuilderOptions;
auth?: AuthGlobalOptions;
routes?: RoutesOptions;
params?: ParamsOptions;
query?: {
limit?: number;
maxLimit?: number;
cache?: number | false;
alwaysPaginate?: boolean;
softDelete?: boolean;
};
serialize?: {
getMany?: false;
get?: false;
create?: false;
createMany?: false;
update?: false;
replace?: false;
delete?: false;
recover?: false;
};
}
================================================
FILE: packages/crud/src/interfaces/crud-options.interface.ts
================================================
import { ValidationPipeOptions } from '@nestjs/common';
import { CrudRoutesFactory } from '../crud';
import { ModelOptions } from './model-options.interface';
import { ParamsOptions } from './params-options.interface';
import { QueryOptions } from './query-options.interface';
import { RoutesOptions } from './routes-options.interface';
import { AuthOptions } from './auth-options.interface';
import { DtoOptions } from './dto-options.interface';
import { SerializeOptions } from './serialize-options.interface';
export interface CrudRequestOptions {
query?: QueryOptions;
routes?: RoutesOptions;
params?: ParamsOptions;
}
export interface CrudOptions {
model: ModelOptions;
dto?: DtoOptions;
serialize?: SerializeOptions;
query?: QueryOptions;
routes?: RoutesOptions;
routesFactory?: typeof CrudRoutesFactory;
params?: ParamsOptions;
validation?: ValidationPipeOptions | false;
}
export interface MergedCrudOptions extends CrudOptions {
auth?: AuthOptions;
}
================================================
FILE: packages/crud/src/interfaces/crud-request.interface.ts
================================================
import { ParsedRequestParams } from '@nestjsx/crud-request';
import { CrudRequestOptions } from '../interfaces';
export interface CrudRequest {
parsed: ParsedRequestParams;
options: CrudRequestOptions;
}
================================================
FILE: packages/crud/src/interfaces/dto-options.interface.ts
================================================
export interface DtoOptions {
create?: any;
update?: any;
replace?: any;
}
================================================
FILE: packages/crud/src/interfaces/get-many-default-response.interface.ts
================================================
export interface GetManyDefaultResponse {
data: T[];
count: number;
total: number;
page: number;
pageCount: number;
}
================================================
FILE: packages/crud/src/interfaces/index.ts
================================================
export * from './crud-controller.interface';
export * from './crud-options.interface';
export * from './auth-options.interface';
export * from './params-options.interface';
export * from './query-options.interface';
export * from './routes-options.interface';
export * from './base-route.interface';
export * from './crud-request.interface';
export * from './model-options.interface';
export * from './create-many-dto.interface';
export * from './get-many-default-response.interface';
export * from './crud-global-config.interface';
export * from './dto-options.interface';
export * from './serialize-options.interface';
================================================
FILE: packages/crud/src/interfaces/model-options.interface.ts
================================================
export interface ModelOptions {
type: any;
}
================================================
FILE: packages/crud/src/interfaces/params-options.interface.ts
================================================
import { SwaggerEnumType } from '@nestjs/swagger/dist/types/swagger-enum.type';
import { ParamOptionType } from '@nestjsx/crud-request';
export interface ParamsOptions {
[key: string]: ParamOption;
}
export interface ParamOption {
field?: string;
type?: ParamOptionType;
enum?: SwaggerEnumType;
primary?: boolean;
disabled?: boolean;
}
================================================
FILE: packages/crud/src/interfaces/query-options.interface.ts
================================================
import {
QueryFields,
QuerySort,
} from '@nestjsx/crud-request/lib/types/request-query.types';
import { QueryFilterOption } from '../types';
export interface QueryOptions {
allow?: QueryFields;
exclude?: QueryFields;
persist?: QueryFields;
filter?: QueryFilterOption;
join?: JoinOptions;
sort?: QuerySort[];
limit?: number;
maxLimit?: number;
cache?: number | false;
alwaysPaginate?: boolean;
softDelete?: boolean;
}
export interface JoinOptions {
[key: string]: JoinOption;
}
export interface JoinOption {
alias?: string;
allow?: QueryFields;
eager?: boolean;
exclude?: QueryFields;
persist?: QueryFields;
select?: false;
required?: boolean;
}
================================================
FILE: packages/crud/src/interfaces/routes-options.interface.ts
================================================
import { BaseRouteName } from '../types';
export interface RoutesOptions {
exclude?: BaseRouteName[];
only?: BaseRouteName[];
getManyBase?: GetManyRouteOptions;
getOneBase?: GetOneRouteOptions;
createOneBase?: CreateOneRouteOptions;
createManyBase?: CreateManyRouteOptions;
updateOneBase?: UpdateOneRouteOptions;
replaceOneBase?: ReplaceOneRouteOptions;
deleteOneBase?: DeleteOneRouteOptions;
recoverOneBase?: RecoverOneRouteOptions;
}
export interface BaseRouteOptions {
interceptors?: any[];
decorators?: (PropertyDecorator | MethodDecorator)[];
}
export type GetManyRouteOptions = BaseRouteOptions;
export type GetOneRouteOptions = BaseRouteOptions;
export interface CreateOneRouteOptions extends BaseRouteOptions {
returnShallow?: boolean;
}
export type CreateManyRouteOptions = BaseRouteOptions;
export interface ReplaceOneRouteOptions extends BaseRouteOptions {
allowParamsOverride?: boolean;
returnShallow?: boolean;
}
export interface UpdateOneRouteOptions extends BaseRouteOptions {
allowParamsOverride?: boolean;
returnShallow?: boolean;
}
export interface DeleteOneRouteOptions extends BaseRouteOptions {
returnDeleted?: boolean;
}
export interface RecoverOneRouteOptions extends BaseRouteOptions {
returnRecovered?: boolean;
}
================================================
FILE: packages/crud/src/interfaces/serialize-options.interface.ts
================================================
import { Type } from '@nestjs/common';
export interface SerializeOptions {
getMany?: Type | false;
get?: Type | false;
create?: Type | false;
createMany?: Type | false;
update?: Type | false;
replace?: Type | false;
delete?: Type | false;
recover?: Type | false;
}
================================================
FILE: packages/crud/src/module/crud-config.service.ts
================================================
import { RequestQueryBuilder } from '@nestjsx/crud-request';
import { isObjectFull } from '@nestjsx/util';
import * as deepmerge from 'deepmerge';
import { CrudGlobalConfig } from '../interfaces';
export class CrudConfigService {
static config: CrudGlobalConfig = {
auth: {},
query: {
alwaysPaginate: false,
},
routes: {
getManyBase: { interceptors: [], decorators: [] },
getOneBase: { interceptors: [], decorators: [] },
createOneBase: { interceptors: [], decorators: [], returnShallow: false },
createManyBase: { interceptors: [], decorators: [] },
updateOneBase: {
interceptors: [],
decorators: [],
allowParamsOverride: false,
returnShallow: false,
},
replaceOneBase: {
interceptors: [],
decorators: [],
allowParamsOverride: false,
returnShallow: false,
},
deleteOneBase: { interceptors: [], decorators: [], returnDeleted: false },
recoverOneBase: { interceptors: [], decorators: [], returnRecovered: false },
},
params: {},
};
static load(config: CrudGlobalConfig = {}) {
if (isObjectFull(config.queryParser)) {
RequestQueryBuilder.setOptions(config.queryParser);
}
const auth = isObjectFull(config.auth) ? config.auth : {};
const query = isObjectFull(config.query) ? config.query : {};
const routes = isObjectFull(config.routes) ? config.routes : {};
const params = isObjectFull(config.params) ? config.params : {};
const serialize = isObjectFull(config.serialize) ? config.serialize : {};
CrudConfigService.config = deepmerge(
CrudConfigService.config,
{ auth, query, routes, params, serialize },
{ arrayMerge: (a, b, c) => b },
);
}
}
================================================
FILE: packages/crud/src/module/index.ts
================================================
export * from './crud-config.service';
================================================
FILE: packages/crud/src/services/crud-service.abstract.ts
================================================
import { BadRequestException, NotFoundException } from '@nestjs/common';
import { ParsedRequestParams } from '@nestjsx/crud-request';
import { objKeys } from '@nestjsx/util';
import { CreateManyDto, CrudRequest, CrudRequestOptions, GetManyDefaultResponse, QueryOptions } from '../interfaces';
export abstract class CrudService {
throwBadRequestException(msg?: unknown): BadRequestException {
throw new BadRequestException(msg);
}
throwNotFoundException(name: string): NotFoundException {
throw new NotFoundException(`${name} not found`);
}
/**
* Wrap page into page-info
* override this method to create custom page-info response
* or set custom `serialize.getMany` dto in the controller's CrudOption
* @param data
* @param total
* @param limit
* @param offset
*/
createPageInfo(data: T[], total: number, limit: number, offset: number): GetManyDefaultResponse {
return {
data,
count: data.length,
total,
page: limit ? Math.floor(offset / limit) + 1 : 1,
pageCount: limit && total ? Math.ceil(total / limit) : 1,
};
}
/**
* Determine if need paging
* @param parsed
* @param options
*/
decidePagination(parsed: ParsedRequestParams, options: CrudRequestOptions): boolean {
return (
options.query.alwaysPaginate ||
((Number.isFinite(parsed.page) || Number.isFinite(parsed.offset)) &&
/* istanbul ignore next */ !!this.getTake(parsed, options.query))
);
}
/**
* Get number of resources to be fetched
* @param query
* @param options
*/
getTake(query: ParsedRequestParams, options: QueryOptions): number | null {
if (query.limit) {
return options.maxLimit ? (query.limit <= options.maxLimit ? query.limit : options.maxLimit) : query.limit;
}
/* istanbul ignore if */
if (options.limit) {
return options.maxLimit ? (options.limit <= options.maxLimit ? options.limit : options.maxLimit) : options.limit;
}
return options.maxLimit ? options.maxLimit : null;
}
/**
* Get number of resources to be skipped
* @param query
* @param take
*/
getSkip(query: ParsedRequestParams, take: number): number | null {
return query.page && take ? take * (query.page - 1) : query.offset ? query.offset : null;
}
/**
* Get primary param name from CrudOptions
* @param options
*/
getPrimaryParams(options: CrudRequestOptions): string[] {
const params = objKeys(options.params).filter((n) => options.params[n] && options.params[n].primary);
return params.map((p) => options.params[p].field);
}
abstract getMany(req: CrudRequest): Promise | T[]>;
abstract getOne(req: CrudRequest): Promise;
abstract createOne(req: CrudRequest, dto: T | Partial): Promise;
abstract createMany(req: CrudRequest, dto: CreateManyDto): Promise;
abstract updateOne(req: CrudRequest, dto: T | Partial): Promise;
abstract replaceOne(req: CrudRequest, dto: T | Partial): Promise;
abstract deleteOne(req: CrudRequest): Promise;
abstract recoverOne(req: CrudRequest): Promise;
}
================================================
FILE: packages/crud/src/services/index.ts
================================================
export * from './crud-service.abstract';
================================================
FILE: packages/crud/src/types/base-route-name.type.ts
================================================
export type BaseRouteName =
| 'getManyBase'
| 'getOneBase'
| 'createOneBase'
| 'createManyBase'
| 'updateOneBase'
| 'replaceOneBase'
| 'deleteOneBase'
| 'recoverOneBase';
================================================
FILE: packages/crud/src/types/index.ts
================================================
export * from './base-route-name.type';
export * from './query-filter-option.type';
================================================
FILE: packages/crud/src/types/query-filter-option.type.ts
================================================
import {
QueryFilter,
SCondition,
} from '@nestjsx/crud-request/lib/types/request-query.types';
export type QueryFilterFunction = (
search?: SCondition,
getMany?: boolean,
) => SCondition | void;
export type QueryFilterOption = QueryFilter[] | SCondition | QueryFilterFunction;
================================================
FILE: packages/crud/src/util.ts
================================================
export function safeRequire(path: string, loader?: () => T): T | null {
try {
/* istanbul ignore next */
const pack = loader ? loader() : require(path);
return pack;
} catch (_) {
/* istanbul ignore next */
return null;
}
}
================================================
FILE: packages/crud/test/__fixture__/dto/index.ts
================================================
export * from './test-create.dto';
export * from './test-update.dto';
================================================
FILE: packages/crud/test/__fixture__/dto/test-create.dto.ts
================================================
import {
IsString,
IsEmail,
IsNumber,
IsOptional,
IsNotEmpty,
IsEmpty,
} from 'class-validator';
export class TestCreateDto {
@IsString()
firstName: string;
@IsString()
lastName: string;
@IsEmail({ require_tld: false })
email: string;
@IsNumber()
age: number;
}
================================================
FILE: packages/crud/test/__fixture__/dto/test-update.dto.ts
================================================
import {
IsString,
IsEmail,
IsNumber,
IsOptional,
IsNotEmpty,
IsEmpty,
} from 'class-validator';
export class TestUpdateDto {
@IsOptional()
@IsString()
firstName?: string;
@IsOptional()
@IsString()
lastName?: string;
@IsOptional()
@IsEmail({ require_tld: false })
email?: string;
@IsOptional()
@IsNumber()
age?: number;
}
================================================
FILE: packages/crud/test/__fixture__/exception.filter.ts
================================================
import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus } from '@nestjs/common';
import { RequestQueryException } from '@nestjsx/crud-request';
import { Response } from 'express';
@Catch(RequestQueryException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: RequestQueryException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
response.status(HttpStatus.BAD_REQUEST).json({
statusCode: HttpStatus.BAD_REQUEST,
message: exception.message,
});
}
}
================================================
FILE: packages/crud/test/__fixture__/models/index.ts
================================================
export * from './test.model';
export * from './test-serialize.model';
export * from './test-serialize-2.model';
================================================
FILE: packages/crud/test/__fixture__/models/test-serialize-2.model.ts
================================================
import { Exclude } from 'class-transformer';
import { TestSerializeModel } from './test-serialize.model';
export class TestSerialize2Model extends TestSerializeModel {
id: number;
name: string;
email: string;
@Exclude()
isActive: boolean;
constructor(partial: Partial) {
super(partial);
Object.assign(this, partial);
}
}
================================================
FILE: packages/crud/test/__fixture__/models/test-serialize.model.ts
================================================
export class TestSerializeModel {
id: number;
name: string;
email: string;
isActive: boolean;
constructor(partial: Partial) {
Object.assign(this, partial);
}
}
================================================
FILE: packages/crud/test/__fixture__/models/test.model.ts
================================================
import {
IsString,
IsEmail,
IsNumber,
IsOptional,
IsNotEmpty,
IsEmpty,
} from 'class-validator';
import { CrudValidationGroups } from '../../../src';
const { CREATE, UPDATE } = CrudValidationGroups;
export class TestModel {
@IsEmpty({ groups: [CREATE] })
@IsNumber({}, { groups: [UPDATE] })
id?: number;
@IsOptional({ groups: [UPDATE] })
@IsNotEmpty({ groups: [CREATE] })
@IsString({ always: true })
firstName?: string;
@IsOptional({ groups: [UPDATE] })
@IsNotEmpty({ groups: [CREATE] })
@IsString({ always: true })
lastName?: string;
@IsOptional({ groups: [UPDATE] })
@IsNotEmpty({ groups: [CREATE] })
@IsEmail({ require_tld: false }, { always: true })
email?: string;
@IsOptional({ groups: [UPDATE] })
@IsNotEmpty({ groups: [CREATE] })
@IsNumber({}, { always: true })
age?: number;
}
================================================
FILE: packages/crud/test/__fixture__/response/delete-model-response.dto.ts
================================================
import { Exclude, Expose } from 'class-transformer';
@Exclude()
export class DeleteModelResponseDto {
@Expose()
id: number;
}
================================================
FILE: packages/crud/test/__fixture__/response/get-many-model-response.dto.ts
================================================
import { Type } from 'class-transformer';
import { GetModelResponseDto } from './get-model-response.dto';
export class GetManyModelResponseDto {
@Type(() => GetModelResponseDto)
items: GetModelResponseDto[];
}
================================================
FILE: packages/crud/test/__fixture__/response/get-model-response.dto.ts
================================================
import { Exclude } from 'class-transformer';
export class GetModelResponseDto {
id: number;
name: string;
@Exclude()
email: string;
isActive: boolean;
}
================================================
FILE: packages/crud/test/__fixture__/response/index.ts
================================================
export * from './get-many-model-response.dto';
export * from './get-model-response.dto';
export * from './delete-model-response.dto';
export * from './recover-model-response.dto';
================================================
FILE: packages/crud/test/__fixture__/response/recover-model-response.dto.ts
================================================
import { Exclude } from 'class-transformer';
export class RecoverModelResponseDto {
id: number;
name: string;
email: string;
isActive: boolean;
}
================================================
FILE: packages/crud/test/__fixture__/services/index.ts
================================================
export * from './test.service';
export * from './test-serialize.service';
================================================
FILE: packages/crud/test/__fixture__/services/test-serialize.service.ts
================================================
import { Injectable, Type } from '@nestjs/common';
import { CreateManyDto, CrudRequest, GetManyDefaultResponse } from '../../../src/interfaces';
import { CrudService } from '../../../src/services';
import { TestSerializeModel } from '../models';
@Injectable()
export class TestSerializeService extends CrudService {
private store: T[] = [];
constructor(private Model: Type) {
super();
this.store = [
new this.Model({ id: 1, name: 'name', email: 'email1', isActive: true }),
new this.Model({ id: 2, name: 'name2', email: 'email2', isActive: false }),
new this.Model({ id: 3, name: 'name3', email: 'email3', isActive: true }),
new this.Model({ id: 4, name: 'name4', email: 'email4', isActive: false }),
new this.Model({ id: 5, name: 'name5', email: 'email5', isActive: true }),
];
}
async getMany(req: CrudRequest): Promise | T[]> {
const total = this.store.length;
const limit = this.getTake(req.parsed, req.options.query);
const offset = this.getSkip(req.parsed, limit);
return this.decidePagination(req.parsed, req.options)
? this.createPageInfo(this.store, total, limit || total, offset || 0)
: this.store;
}
async getOne(req: CrudRequest): Promise {
return this.store[0];
}
async createOne(req: CrudRequest, dto: T): Promise {}
async createMany(req: CrudRequest, dto: CreateManyDto): Promise {}
async updateOne(req: CrudRequest, dto: T): Promise {}
async replaceOne(req: CrudRequest, dto: T): Promise {}
async deleteOne(req: CrudRequest): Promise {
return req.options.routes.deleteOneBase.returnDeleted ? this.store[0] : undefined;
}
async recoverOne(req: CrudRequest): Promise {
return req.options.routes.recoverOneBase.returnRecovered ? this.store[0] : undefined;
}
}
================================================
FILE: packages/crud/test/__fixture__/services/test.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { ParsedRequestParams } from '@nestjsx/crud-request';
import { CrudRequestOptions } from '../../../src/interfaces';
import { CreateManyDto, CrudRequest } from '../../../src/interfaces';
import { CrudService } from '../../../src/services';
@Injectable()
export class TestService extends CrudService {
async getMany(req: CrudRequest): Promise {
return { req };
}
async getOne(req: CrudRequest): Promise {
return { req };
}
async createOne(req: CrudRequest, dto: T): Promise {
return { req, dto };
}
async createMany(req: CrudRequest, dto: CreateManyDto): Promise {
return { req, dto };
}
async updateOne(req: CrudRequest, dto: T): Promise {
return { req, dto };
}
async replaceOne(req: CrudRequest, dto: T): Promise {
return { req, dto };
}
async deleteOne(req: CrudRequest): Promise {
return { req };
}
async recoverOne(req: CrudRequest): Promise {
return { req };
}
decidePagination(parsed: ParsedRequestParams, options: CrudRequestOptions): boolean {
return true;
}
}
================================================
FILE: packages/crud/test/crud-config.service.global.spec.ts
================================================
import * as request from 'supertest';
import { Test } from '@nestjs/testing';
import { Controller, INestApplication } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { CrudGlobalConfig } from '../src/interfaces';
import { CrudConfigService } from '../src/module/crud-config.service';
// IMPORTANT:
// CrudConfigService.load() should be called before importing @Crud() controllers
const conf: CrudGlobalConfig = {
query: {
limit: 10,
},
params: {
id: {
field: 'id',
type: 'uuid',
primary: true,
},
},
routes: {
exclude: ['createManyBase'],
updateOneBase: {
allowParamsOverride: true,
},
replaceOneBase: {
allowParamsOverride: true,
},
},
serialize: {
get: false,
},
};
// Important: load config before (!!!) you import AppModule
// https://github.com/nestjsx/crud/wiki/Controllers#global-options
CrudConfigService.load(conf);
import { Crud } from '../src/decorators/crud.decorator';
import { HttpExceptionFilter } from './__fixture__/exception.filter';
import { TestModel } from './__fixture__/models';
import { TestService } from './__fixture__/services';
describe('#crud', () => {
describe('#CrudConfigService', () => {
let app: INestApplication;
let server: any;
@Crud({
model: { type: TestModel },
})
@Controller('test')
class GlobalTestController {
constructor(public service: TestService) {}
}
@Crud({
model: { type: TestModel },
query: {
limit: 12,
},
params: {
id: {
field: 'id',
type: 'number',
primary: true,
},
},
routes: {
updateOneBase: {
allowParamsOverride: false,
},
replaceOneBase: {
allowParamsOverride: false,
},
deleteOneBase: {
returnDeleted: true,
},
},
})
@Controller('test2')
class GlobalTestController2 {
constructor(public service: TestService) {}
}
beforeAll(async () => {
const fixture = await Test.createTestingModule({
controllers: [GlobalTestController, GlobalTestController2],
providers: [{ provide: APP_FILTER, useClass: HttpExceptionFilter }, TestService],
}).compile();
app = fixture.createNestApplication();
await app.init();
server = app.getHttpServer();
});
afterAll(async () => {
app.close();
});
it('should use global config', (done) => {
request(server)
.get('/test')
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.req.options.query).toMatchObject(conf.query);
expect(res.body.req.options.params).toMatchObject(conf.params);
expect(res.body.req.options.routes.updateOneBase.allowParamsOverride).toBe(true);
expect(res.body.req.options.routes.replaceOneBase.allowParamsOverride).toBe(true);
done();
});
});
it('should use merged config', (done) => {
request(server)
.get('/test2')
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.req.options.query).toMatchObject({
limit: 12,
});
expect(res.body.req.options.params).toMatchObject({
id: {
field: 'id',
type: 'number',
primary: true,
},
});
expect(res.body.req.options.routes.updateOneBase.allowParamsOverride).toBe(false);
expect(res.body.req.options.routes.replaceOneBase.allowParamsOverride).toBe(false);
expect(res.body.req.options.routes.deleteOneBase.returnDeleted).toBe(true);
done();
});
});
it('should exclude route, 1', (done) => {
request(server)
.post('/test/bulk')
.send({})
.end((_, res) => {
expect(res.status).toBe(404);
done();
});
});
it('should exclude route, 1', (done) => {
request(server)
.post('/test2/bulk')
.send({})
.end((_, res) => {
expect(res.status).toBe(404);
done();
});
});
});
});
================================================
FILE: packages/crud/test/crud-config.service.spec.ts
================================================
import { RequestQueryBuilder } from '@nestjsx/crud-request';
import { CrudGlobalConfig } from '../src/interfaces';
import { CrudConfigService } from '../src/module/crud-config.service';
describe('#crud', () => {
describe('#CrudConfigService', () => {
const defaultConfig = { ...CrudConfigService.config };
beforeEach(() => {
CrudConfigService.config = { ...defaultConfig };
});
it('should set default config, 1', () => {
const conf: CrudGlobalConfig = {};
const expected = { ...CrudConfigService.config };
CrudConfigService.load(conf);
expect(CrudConfigService.config).toEqual(expect.objectContaining(expected));
});
it('should set default config, 2', () => {
const expected = { ...CrudConfigService.config };
CrudConfigService.load();
expect(CrudConfigService.config).toEqual(expect.objectContaining(expected));
});
it('should set queryParser', () => {
const requestOptions = { ...RequestQueryBuilder.getOptions() };
const conf: CrudGlobalConfig = {
queryParser: {
delim: '__',
},
};
const expected = { ...CrudConfigService.config };
CrudConfigService.load(conf);
expect(CrudConfigService.config).toEqual(expect.objectContaining(expected));
expect(RequestQueryBuilder.getOptions()).toEqual(
expect.objectContaining({ ...requestOptions, delim: '__' }),
);
});
it('should set query, routes, params', () => {
const conf: CrudGlobalConfig = {
auth: {
property: 'user',
},
query: {
limit: 10,
},
params: {
id: {
field: 'id',
type: 'uuid',
primary: true,
},
},
routes: {
updateOneBase: {
allowParamsOverride: true,
returnShallow: true,
},
replaceOneBase: {
allowParamsOverride: true,
},
getManyBase: {
interceptors: [() => {}],
},
},
};
const expected = {
auth: {
property: 'user',
},
query: {
limit: 10,
},
params: {
id: {
field: 'id',
type: 'uuid',
primary: true,
},
},
routes: {
getManyBase: {
interceptors: [() => {}],
decorators: [],
},
getOneBase: { interceptors: [], decorators: [] },
createOneBase: { interceptors: [], decorators: [], returnShallow: false },
createManyBase: { interceptors: [], decorators: [] },
updateOneBase: {
interceptors: [],
decorators: [],
allowParamsOverride: true,
returnShallow: true,
},
replaceOneBase: {
interceptors: [],
decorators: [],
allowParamsOverride: true,
returnShallow: false,
},
deleteOneBase: { interceptors: [], decorators: [], returnDeleted: false },
recoverOneBase: { interceptors: [], decorators: [], returnRecovered: false },
},
};
CrudConfigService.load(conf);
expect(CrudConfigService.config.params).toEqual(
expect.objectContaining(expected.params),
);
expect(CrudConfigService.config.query).toEqual(
expect.objectContaining(expected.query),
);
expect(JSON.stringify(CrudConfigService.config.routes)).toEqual(
JSON.stringify(expected.routes),
);
});
});
});
================================================
FILE: packages/crud/test/crud-request.interceptor.spec.ts
================================================
import { Controller, Get, Param, ParseIntPipe, Query, UseInterceptors } from '@nestjs/common';
import { NestApplication } from '@nestjs/core';
import { Test } from '@nestjs/testing';
import { RequestQueryBuilder } from '@nestjsx/crud-request';
import * as supertest from 'supertest';
import { Crud, ParsedRequest, CrudAuth, Override } from '../src/decorators';
import { CrudRequestInterceptor } from '../src/interceptors';
import { CrudRequest } from '../src/interfaces';
import { TestModel } from './__fixture__/models';
import { TestService } from './__fixture__/services';
// tslint:disable:max-classes-per-file
describe('#crud', () => {
@UseInterceptors(CrudRequestInterceptor)
@Controller('test')
class TestController {
@Get('/query')
async query(@ParsedRequest() req: CrudRequest) {
return req;
}
@Get('/other')
async other(@Query('page', ParseIntPipe) page: number) {
return { page };
}
@Get('/other2/:someParam')
async routeWithParam(@Param('someParam', ParseIntPipe) p: number) {
return { p };
}
}
@Crud({
model: { type: TestModel },
params: {
someParam: { field: 'someParam', type: 'number' },
},
})
@Controller('test2')
class Test2Controller {
constructor(public service: TestService) {}
@UseInterceptors(CrudRequestInterceptor)
@Get('normal/:id')
async normal(@ParsedRequest() req: CrudRequest) {
return { filter: req.parsed.paramsFilter };
}
@UseInterceptors(CrudRequestInterceptor)
@Get('/other2/:someParam')
async routeWithParam(@Param('someParam', ParseIntPipe) p: number) {
return { p };
}
@UseInterceptors(CrudRequestInterceptor)
@Get('other2/:id/twoParams/:someParam')
async twoParams(@ParsedRequest() req: CrudRequest, @Param('someParam', ParseIntPipe) p: number) {
return { filter: req.parsed.paramsFilter };
}
}
@Crud({
model: { type: TestModel },
query: {
filter: () => ({ name: 'persist' }),
},
})
@CrudAuth({
property: 'user',
filter: (user) => ({ user: 'test', buz: 1 }),
persist: () => ({ bar: false }),
})
@Controller('test3')
class Test3Controller {
constructor(public service: TestService) {}
@Override('getManyBase')
get(@ParsedRequest() req: CrudRequest) {
return req;
}
@Override('createOneBase')
post(@ParsedRequest() req: CrudRequest) {
return req;
}
}
@Crud({
model: { type: TestModel },
})
@CrudAuth({
or: () => ({ id: 1 }),
})
@Controller('test4')
class Test4Controller {
constructor(public service: TestService) {}
@Override('getManyBase')
get(@ParsedRequest() req: CrudRequest) {
return req;
}
}
@Crud({
model: { type: TestModel },
params: {
someParam: { field: 'someParam', type: 'number', primary: true },
someParam2: { field: 'someParam2', type: 'number', primary: true },
},
})
@Controller('test5')
class Test5Controller {
constructor(public service: TestService) {}
}
@Crud({
model: { type: TestModel },
})
@CrudAuth({
groups: () => ['TEST_2'],
classTransformOptions: () => ({ groups: ['TEST_1'] }),
})
@Controller('test6')
class Test6Controller {
constructor(public service: TestService) {}
@Override('getManyBase')
get(@ParsedRequest() req: CrudRequest) {
return req;
}
}
let $: supertest.SuperTest;
let app: NestApplication;
beforeAll(async () => {
const module = await Test.createTestingModule({
providers: [TestService],
controllers: [
TestController,
Test2Controller,
Test3Controller,
Test4Controller,
Test5Controller,
Test6Controller,
],
}).compile();
app = module.createNestApplication();
await app.init();
$ = supertest(app.getHttpServer());
});
afterAll(async () => {
await app.close();
});
describe('#interceptor', () => {
let qb: RequestQueryBuilder;
beforeEach(() => {
qb = RequestQueryBuilder.create();
});
it('should working on non-crud controller', async () => {
const page = 2;
const limit = 10;
const fields = ['a', 'b', 'c'];
const sorts: any[][] = [
['a', 'ASC'],
['b', 'DESC'],
];
const filters: any[][] = [
['a', 'eq', 1],
['c', 'in', [1, 2, 3]],
['d', 'notnull'],
];
qb.setPage(page).setLimit(limit);
qb.select(fields);
for (const s of sorts) {
qb.sortBy({ field: s[0], order: s[1] });
}
for (const f of filters) {
qb.setFilter({ field: f[0], operator: f[1], value: f[2] });
}
const res = await $.get('/test/query').query(qb.query()).expect(200);
expect(res.body.parsed).toHaveProperty('page', page);
expect(res.body.parsed).toHaveProperty('limit', limit);
expect(res.body.parsed).toHaveProperty('fields', fields);
expect(res.body.parsed).toHaveProperty('sort');
for (let i = 0; i < sorts.length; i++) {
expect(res.body.parsed.sort[i]).toHaveProperty('field', sorts[i][0]);
expect(res.body.parsed.sort[i]).toHaveProperty('order', sorts[i][1]);
}
expect(res.body.parsed).toHaveProperty('filter');
for (let i = 0; i < filters.length; i++) {
expect(res.body.parsed.filter[i]).toHaveProperty('field', filters[i][0]);
expect(res.body.parsed.filter[i]).toHaveProperty('operator', filters[i][1]);
expect(res.body.parsed.filter[i]).toHaveProperty('value', filters[i][2] || '');
}
});
it('should others working', async () => {
const res = await $.get('/test/other').query({ page: 2, per_page: 11 }).expect(200);
expect(res.body.page).toBe(2);
});
it('should parse param', async () => {
const res = await $.get('/test/other2/123').expect(200);
expect(res.body.p).toBe(123);
});
it('should parse custom param in crud', async () => {
const res = await $.get('/test2/other2/123').expect(200);
expect(res.body.p).toBe(123);
});
it('should parse crud param and custom param', async () => {
const res = await $.get('/test2/other2/1/twoParams/123').expect(200);
expect(res.body.filter).toHaveLength(2);
expect(res.body.filter[0].field).toBe('id');
expect(res.body.filter[0].value).toBe(1);
});
it('should parse multiple primary key', async () => {
const res = await $.get('/test5/123/456').expect(200);
});
it('should work like before', async () => {
const res = await $.get('/test2/normal/0').expect(200);
expect(res.body.filter).toHaveLength(1);
expect(res.body.filter[0].field).toBe('id');
expect(res.body.filter[0].value).toBe(0);
});
it('should handle authorized request, 1', async () => {
const res = await $.post('/test3').send({}).expect(201);
const authPersist = { bar: false };
const { parsed } = res.body;
expect(parsed.authPersist).toMatchObject(authPersist);
});
it('should handle authorized request, 2', async () => {
const res = await $.get('/test3').expect(200);
const search = { $and: [{ user: 'test', buz: 1 }, {}] };
expect(res.body.parsed.search).toMatchObject(search);
});
it('should handle authorized request, 3', async () => {
const query = qb.search({ name: 'test' }).query();
const res = await $.get('/test4').query(query).expect(200);
const search = { $or: [{ id: 1 }, { $and: [{}, { name: 'test' }] }] };
expect(res.body.parsed.search).toMatchObject(search);
});
it('should handle authorized request, 4', async () => {
const query = qb.search({ name: 'test' }).query();
const res = await $.get('/test3').query(query).expect(200);
const search = { $and: [{ user: 'test', buz: 1 }, { name: 'persist' }] };
expect(res.body.parsed.search).toMatchObject(search);
});
it('should handle classTransformOptions, 1', async () => {
const res = await $.get('/test6').expect(200);
const groups = ['TEST_2'];
expect(res.body.parsed.classTransformOptions.groups).toMatchObject(groups);
});
});
});
================================================
FILE: packages/crud/test/crud-service.abstract.spec.ts
================================================
import { BadRequestException, NotFoundException } from '@nestjs/common';
import { TestService } from './__fixture__/services';
describe('#crud', () => {
describe('#CrudService', () => {
let service: TestService;
beforeAll(() => {
service = new TestService();
});
describe('#throwBadRequestException', () => {
it('should throw BadRequestException', () => {
expect(service.throwBadRequestException.bind(service, '')).toThrowError(
BadRequestException,
);
});
});
describe('#throwNotFoundException', () => {
it('should throw NotFoundException', () => {
expect(service.throwNotFoundException.bind(service, '')).toThrowError(
NotFoundException,
);
});
});
describe('#createPageInfo', () => {
it('should return an object', () => {
const expected = {
count: 0,
data: [],
page: 2,
pageCount: 10,
total: 100,
};
expect(service.createPageInfo([], 100, 10, 10)).toMatchObject(expected);
});
it('should return an object when limit and offset undefined', () => {
const expected = {
count: 0,
data: [],
page: 1,
pageCount: 1,
total: 100,
};
expect(service.createPageInfo([], 100, undefined, undefined)).toMatchObject(
expected,
);
});
});
});
});
================================================
FILE: packages/crud/test/crud.decorator.base.spec.ts
================================================
import * as request from 'supertest';
import { Test } from '@nestjs/testing';
import { Controller, INestApplication } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { RequestQueryBuilder } from '@nestjsx/crud-request';
import { Crud } from '../src/decorators/crud.decorator';
import { CreateManyDto } from '../src/interfaces';
import { HttpExceptionFilter } from './__fixture__/exception.filter';
import { TestModel } from './__fixture__/models';
import { TestService } from './__fixture__/services';
describe('#crud', () => {
describe('#base methods', () => {
let app: INestApplication;
let server: any;
let qb: RequestQueryBuilder;
@Crud({
model: { type: TestModel },
})
@Controller('test')
class TestController {
constructor(public service: TestService) {}
}
beforeAll(async () => {
const fixture = await Test.createTestingModule({
controllers: [TestController],
providers: [{ provide: APP_FILTER, useClass: HttpExceptionFilter }, TestService],
}).compile();
app = fixture.createNestApplication();
await app.init();
server = app.getHttpServer();
});
beforeEach(() => {
qb = RequestQueryBuilder.create();
});
afterAll(async () => {
app.close();
});
describe('#getManyBase', () => {
it('should return status 200', (done) => {
request(server)
.get('/test')
.end((_, res) => {
expect(res.status).toEqual(200);
done();
});
});
it('should return status 400', (done) => {
const query = qb.setFilter({ field: 'foo', operator: 'gt' }).query();
request(server)
.get('/test')
.query(query)
.end((_, res) => {
const expected = { statusCode: 400, message: 'Invalid filter value' };
expect(res.status).toEqual(400);
expect(res.body).toMatchObject(expected);
done();
});
});
});
describe('#getOneBase', () => {
it('should return status 200', (done) => {
request(server)
.get('/test/1')
.end((_, res) => {
expect(res.status).toEqual(200);
done();
});
});
it('should return status 400', (done) => {
request(server)
.get('/test/invalid')
.end((_, res) => {
const expected = {
statusCode: 400,
message: 'Invalid param id. Number expected',
};
expect(res.status).toEqual(400);
expect(res.body).toMatchObject(expected);
done();
});
});
});
describe('#createOneBase', () => {
it('should return status 201', (done) => {
const send: TestModel = {
firstName: 'firstName',
lastName: 'lastName',
email: 'test@test.com',
age: 15,
};
request(server)
.post('/test')
.send(send)
.end((_, res) => {
expect(res.status).toEqual(201);
done();
});
});
it('should return status 400', (done) => {
const send: TestModel = {
firstName: 'firstName',
lastName: 'lastName',
email: 'test@test.com',
};
request(server)
.post('/test')
.send(send)
.end((_, res) => {
expect(res.status).toEqual(400);
done();
});
});
});
describe('#createMadyBase', () => {
it('should return status 201', (done) => {
const send: CreateManyDto = {
bulk: [
{
firstName: 'firstName',
lastName: 'lastName',
email: 'test@test.com',
age: 15,
},
{
firstName: 'firstName',
lastName: 'lastName',
email: 'test@test.com',
age: 15,
},
],
};
request(server)
.post('/test/bulk')
.send(send)
.end((_, res) => {
expect(res.status).toEqual(201);
done();
});
});
it('should return status 400', (done) => {
const send: CreateManyDto = {
bulk: [],
};
request(server)
.post('/test/bulk')
.send(send)
.end((_, res) => {
expect(res.status).toEqual(400);
done();
});
});
});
describe('#replaceOneBase', () => {
it('should return status 200', (done) => {
const send: TestModel = {
id: 1,
firstName: 'firstName',
lastName: 'lastName',
email: 'test@test.com',
age: 15,
};
request(server)
.put('/test/1')
.send(send)
.end((_, res) => {
expect(res.status).toEqual(200);
done();
});
});
it('should return status 400', (done) => {
const send: TestModel = {
firstName: 'firstName',
lastName: 'lastName',
email: 'test@test.com',
};
request(server)
.put('/test/1')
.send(send)
.end((_, res) => {
expect(res.status).toEqual(400);
done();
});
});
});
describe('#updateOneBase', () => {
it('should return status 200', (done) => {
const send: TestModel = {
id: 1,
firstName: 'firstName',
lastName: 'lastName',
email: 'test@test.com',
age: 15,
};
request(server)
.patch('/test/1')
.send(send)
.end((_, res) => {
expect(res.status).toEqual(200);
done();
});
});
it('should return status 400', (done) => {
const send: TestModel = {
firstName: 'firstName',
lastName: 'lastName',
email: 'test@test.com',
};
request(server)
.patch('/test/1')
.send(send)
.end((_, res) => {
expect(res.status).toEqual(400);
done();
});
});
});
describe('#deleteOneBase', () => {
it('should return status 200', (done) => {
request(server)
.delete('/test/1')
.end((_, res) => {
expect(res.status).toEqual(200);
done();
});
});
});
});
});
================================================
FILE: packages/crud/test/crud.decorator.exclude.spec.ts
================================================
import * as request from 'supertest';
import { Test } from '@nestjs/testing';
import { Controller, INestApplication } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { Crud } from '../src/decorators';
import { HttpExceptionFilter } from './__fixture__/exception.filter';
import { TestModel } from './__fixture__/models';
import { TestService } from './__fixture__/services';
describe('#crud', () => {
describe('#exclude routes', () => {
let app: INestApplication;
let server: any;
@Crud({
model: { type: TestModel },
routes: {
exclude: ['getManyBase'],
},
})
@Controller('test')
class TestController {
constructor(public service: TestService) {}
}
beforeAll(async () => {
const fixture = await Test.createTestingModule({
controllers: [TestController],
providers: [{ provide: APP_FILTER, useClass: HttpExceptionFilter }, TestService],
}).compile();
app = fixture.createNestApplication();
await app.init();
server = app.getHttpServer();
});
afterAll(async () => {
app.close();
});
describe('#getManyBase excluded', () => {
it('should return status 404', (done) => {
request(server)
.get('/test')
.end((_, res) => {
expect(res.status).toEqual(404);
done();
});
});
});
});
describe('#only routes', () => {
let app: INestApplication;
let server: any;
@Crud({
model: { type: TestModel },
routes: {
only: ['getManyBase'],
},
})
@Controller('test')
class TestController {
constructor(public service: TestService) {}
}
beforeAll(async () => {
const fixture = await Test.createTestingModule({
controllers: [TestController],
providers: [{ provide: APP_FILTER, useClass: HttpExceptionFilter }, TestService],
}).compile();
app = fixture.createNestApplication();
await app.init();
server = app.getHttpServer();
});
afterAll(async () => {
app.close();
});
describe('#getManyBase only', () => {
it('should return status 200', (done) => {
request(server)
.get('/test')
.end((_, res) => {
expect(res.status).toEqual(200);
done();
});
});
});
describe('#getOneBase excluded', () => {
it('should return status 404', (done) => {
request(server)
.get('/test/1')
.end((_, res) => {
expect(res.status).toEqual(404);
done();
});
});
});
});
});
================================================
FILE: packages/crud/test/crud.decorator.options.spec.ts
================================================
import * as request from 'supertest';
import { Test } from '@nestjs/testing';
import { Controller, INestApplication } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { CrudRoutesFactory } from '../src/crud/crud-routes.factory';
import { Swagger } from '../src/crud/swagger.helper';
import { Crud } from '../src/decorators';
import { CrudOptions } from '../src/interfaces';
import { BaseRouteName } from '../src/types';
import { HttpExceptionFilter } from './__fixture__/exception.filter';
import { TestModel } from './__fixture__/models';
import { TestService } from './__fixture__/services';
describe('#crud', () => {
describe('#options', () => {
let app: INestApplication;
let server: any;
class CustomSwaggerRoutesFactory extends CrudRoutesFactory {
protected setSwaggerOperation(name: BaseRouteName) {
const summary = Swagger.operationsMap(this.modelName)[name];
const operationId = '_' + name + this.modelName;
Swagger.setOperation({ summary, operationId }, this.targetProto[name]);
}
}
const options: CrudOptions = {
model: { type: TestModel },
params: {
id: {
field: 'id',
type: 'uuid',
primary: true,
},
},
query: {
limit: 10,
},
routes: {
getManyBase: {
interceptors: [],
decorators: [],
},
getOneBase: {
interceptors: [],
decorators: [],
},
createOneBase: {
interceptors: [],
decorators: [],
},
createManyBase: {
interceptors: [],
decorators: [],
},
updateOneBase: {
interceptors: [],
decorators: [],
allowParamsOverride: true,
},
replaceOneBase: {
interceptors: [],
decorators: [],
allowParamsOverride: true,
},
deleteOneBase: {
interceptors: [],
decorators: [],
returnDeleted: true,
},
},
routesFactory: CustomSwaggerRoutesFactory,
};
@Crud(options)
@Controller('test')
class TestController {
constructor(public service: TestService) {}
}
beforeAll(async () => {
const fixture = await Test.createTestingModule({
controllers: [TestController],
providers: [{ provide: APP_FILTER, useClass: HttpExceptionFilter }, TestService],
}).compile();
app = fixture.createNestApplication();
await app.init();
server = app.getHttpServer();
});
afterAll(async () => {
app.close();
});
it('should return options in ParsedRequest', (done) => {
request(server)
.get('/test')
.expect(200)
.end((_, res) => {
const opt = res.body.req.options;
expect(opt.query).toMatchObject(options.query);
expect(opt.routes).toMatchObject(options.routes);
expect(opt.params).toMatchObject(options.params);
done();
});
});
it('should use crudRoutesFactory override', () => {
const testController = app.get(TestController);
const { operationId } = Swagger.getOperation((testController as any).replaceOneBase);
expect(operationId).toEqual('_replaceOneBaseTestModel');
});
});
});
================================================
FILE: packages/crud/test/crud.decorator.override.spec.ts
================================================
import * as request from 'supertest';
import { Test } from '@nestjs/testing';
import { Controller, INestApplication } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { RequestQueryBuilder } from '@nestjsx/crud-request';
import { Crud, Override, ParsedRequest, ParsedBody } from '../src/decorators';
import { CrudController, CrudRequest, CreateManyDto } from '../src/interfaces';
import { R, Swagger } from '../src/crud';
import { CrudActions } from '../src/enums';
import { HttpExceptionFilter } from './__fixture__/exception.filter';
import { TestModel } from './__fixture__/models';
import { TestService } from './__fixture__/services';
describe('#crud', () => {
describe('#override methods', () => {
let app: INestApplication;
let server: any;
let qb: RequestQueryBuilder;
enum Field {
ONE = 'one',
}
@Crud({
model: { type: TestModel },
params: {
enumField: {
field: 'enum_field',
type: 'string',
enum: Field,
},
disabledField: {
field: 'disabled_field',
type: 'number',
disabled: true,
},
},
})
@Controller('test')
class TestController implements CrudController {
constructor(public service: TestService) {}
get base(): CrudController {
return this;
}
@Override()
getMany(@ParsedRequest() req: CrudRequest) {
return { foo: 'bar' };
}
@Override('createManyBase')
createBulk(@ParsedBody() dto: CreateManyDto, @ParsedRequest() req: CrudRequest) {
return this.base.createManyBase(req, dto);
}
}
beforeAll(async () => {
const fixture = await Test.createTestingModule({
controllers: [TestController],
providers: [{ provide: APP_FILTER, useClass: HttpExceptionFilter }, TestService],
}).compile();
app = fixture.createNestApplication();
await app.init();
server = app.getHttpServer();
});
beforeEach(() => {
qb = RequestQueryBuilder.create();
});
afterAll(async () => {
app.close();
});
describe('#override getMany', () => {
it('should return status 200', (done) => {
request(server)
.get('/test')
.expect(200)
.end((_, res) => {
const expected = { foo: 'bar' };
expect(res.body).toMatchObject(expected);
done();
});
});
it('should return status 400', (done) => {
const query = qb.setFilter({ field: 'foo', operator: 'gt' }).query();
request(server)
.get('/test')
.query(query)
.end((_, res) => {
const expected = { statusCode: 400, message: 'Invalid filter value' };
expect(res.status).toEqual(400);
expect(res.body).toMatchObject(expected);
done();
});
});
it('should have action metadata', () => {
const action = R.getAction(TestController.prototype.getMany);
expect(action).toBe(CrudActions.ReadAll);
});
it('should return swagger operation', () => {
const operation = Swagger.getOperation(TestController.prototype.getMany);
const expected = { summary: 'Retrieve multiple TestModels' };
expect(operation).toMatchObject(expected);
});
it('should return swagger params', () => {
const params = Swagger.getParams(TestController.prototype.getMany);
expect(Array.isArray(params)).toBe(true);
expect(params.length > 0).toBe(true);
const enumParam = params.find((param) => param.name === 'enumField');
expect(enumParam).toBeDefined();
expect(enumParam.enum).toEqual(['one']);
});
it('should not return disabled fields in swagger', () => {
const params = Swagger.getParams(TestController.prototype.getMany);
expect(Array.isArray(params)).toBe(true);
expect(params.length > 0).toBe(true);
const disabledParam = params.find((param) => param.name === 'disabledField');
expect(disabledParam).toBeUndefined();
});
it('should return swagger response ok', () => {
const response = Swagger.getResponseOk(TestController.prototype.getMany);
const expected = {
'200': {
schema: {
oneOf: [
{ $ref: '#/components/schemas/GetManyTestModelResponseDto' },
{ items: { $ref: '#/components/schemas/TestModel' }, type: 'array' },
],
},
},
};
expect(response).toMatchObject(expected);
});
});
describe('#override createMany', () => {
it('should still validate dto', (done) => {
const send: CreateManyDto = {
bulk: [],
};
request(server)
.post('/test/bulk')
.send(send)
.end((_, res) => {
expect(res.status).toEqual(400);
done();
});
});
it('should return status 201', (done) => {
const send: CreateManyDto = {
bulk: [
{
firstName: 'firstName',
lastName: 'lastName',
email: 'test@test.com',
age: 15,
},
{
firstName: 'firstName',
lastName: 'lastName',
email: 'test@test.com',
age: 15,
},
],
};
request(server)
.post('/test/bulk')
.send(send)
.expect(201)
.end((_, res) => {
expect(res.body).toHaveProperty('req');
expect(res.body).toHaveProperty('dto');
expect(res.body.dto).toMatchObject(send);
done();
});
});
});
});
});
================================================
FILE: packages/crud/test/crud.decorator.soft.spec.ts
================================================
import * as request from 'supertest';
import { Test } from '@nestjs/testing';
import { Controller, INestApplication } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { Crud } from '../src/decorators';
import { HttpExceptionFilter } from './__fixture__/exception.filter';
import { TestModel } from './__fixture__/models';
import { TestService } from './__fixture__/services';
describe('#crud', () => {
describe('#soft delete disabled', () => {
let app: INestApplication;
let server: any;
@Crud({
model: { type: TestModel },
})
@Controller('test')
class TestController {
constructor(public service: TestService) {}
}
beforeAll(async () => {
const fixture = await Test.createTestingModule({
controllers: [TestController],
providers: [{ provide: APP_FILTER, useClass: HttpExceptionFilter }, TestService],
}).compile();
app = fixture.createNestApplication();
await app.init();
server = app.getHttpServer();
});
afterAll(async () => {
app.close();
});
describe('#recoverOneBase', () => {
it('should return status 404 if controller does not have soft delete', (done) => {
request(server)
.patch('/test/1/recover')
.end((_, res) => {
expect(res.status).toEqual(404);
done();
});
});
});
});
describe('#soft delete enabled', () => {
let app: INestApplication;
let server: any;
@Crud({
model: { type: TestModel },
query: {
softDelete: true,
},
})
@Controller('test')
class TestController {
constructor(public service: TestService) {}
}
beforeAll(async () => {
const fixture = await Test.createTestingModule({
controllers: [TestController],
providers: [{ provide: APP_FILTER, useClass: HttpExceptionFilter }, TestService],
}).compile();
app = fixture.createNestApplication();
await app.init();
server = app.getHttpServer();
});
afterAll(async () => {
app.close();
});
describe('#recoverOneBase', () => {
it('should return status 200 if controller has soft delete', (done) => {
request(server)
.patch('/test/1/recover')
.end((_, res) => {
expect(res.status).toEqual(200);
done();
});
});
});
});
});
================================================
FILE: packages/crud/test/crud.dto.options.spec.ts
================================================
import * as request from 'supertest';
import { Test } from '@nestjs/testing';
import { Controller, INestApplication } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { Crud } from '../src/decorators/crud.decorator';
import { HttpExceptionFilter } from './__fixture__/exception.filter';
import { TestModel } from './__fixture__/models';
import { TestCreateDto, TestUpdateDto } from './__fixture__/dto';
import { TestService } from './__fixture__/services';
describe('#crud', () => {
describe('#dto options', () => {
let app: INestApplication;
let server: any;
@Crud({
model: {
type: TestModel,
},
dto: {
create: TestCreateDto,
update: TestUpdateDto,
},
})
@Controller('test')
class TestController {
constructor(public service: TestService) {}
}
beforeAll(async () => {
const fixture = await Test.createTestingModule({
controllers: [TestController],
providers: [{ provide: APP_FILTER, useClass: HttpExceptionFilter }, TestService],
}).compile();
app = fixture.createNestApplication();
await app.init();
server = app.getHttpServer();
});
afterAll(async () => {
await app.close();
});
describe('#createOneBase', () => {
it('should return status 201', (done) => {
const send: TestCreateDto = {
firstName: 'firstName',
lastName: 'lastName',
email: 'test@test.com',
age: 15,
};
request(server)
.post('/test')
.send(send)
.end((_, res) => {
expect(res.status).toEqual(201);
done();
});
});
it('should return status 400', (done) => {
const send: TestModel = {
firstName: 'firstName',
lastName: 'lastName',
email: 'test@test.com',
};
request(server)
.post('/test')
.send(send)
.end((_, res) => {
expect(res.status).toEqual(400);
done();
});
});
});
describe('#updateOneBase', () => {
it('should return status 200', (done) => {
const send: TestModel = {
id: 1,
firstName: 'firstName',
lastName: 'lastName',
email: 'test@test.com',
age: 15,
};
request(server)
.patch('/test/1')
.send(send)
.end((_, res) => {
expect(res.status).toEqual(200);
done();
});
});
it('should return status 400', (done) => {
const send: TestModel = {
firstName: 'firstName',
lastName: 'lastName',
email: 'foo',
};
request(server)
.patch('/test/1')
.send(send)
.end((_, res) => {
expect(res.status).toEqual(400);
done();
});
});
});
});
});
================================================
FILE: packages/crud/test/crud.serialize.options.spec.ts
================================================
import * as request from 'supertest';
import { Test } from '@nestjs/testing';
import { Controller, INestApplication, Inject } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { Crud, Override, ParsedRequest } from '../src/decorators';
import { CrudController, CrudRequest } from '../src/interfaces';
import { HttpExceptionFilter } from './__fixture__/exception.filter';
import { TestSerializeModel, TestSerialize2Model } from './__fixture__/models';
import {
GetModelResponseDto,
GetManyModelResponseDto,
DeleteModelResponseDto,
RecoverModelResponseDto,
} from './__fixture__/response';
import { TestSerializeService } from './__fixture__/services';
describe('#crud', () => {
describe('#serialize options', () => {
let app: INestApplication;
let server: any;
const SERVICE_TOKEN = 'TestSerializeServiceToken';
const SERVICE2_TOKEN = 'TestSerializeServiceToken2';
@Crud({
model: {
type: TestSerializeModel,
},
serialize: {
get: GetModelResponseDto,
delete: DeleteModelResponseDto,
},
routes: {
deleteOneBase: {
returnDeleted: true,
},
},
})
@Controller('test')
class TestController {
constructor(@Inject(SERVICE_TOKEN) public service: TestSerializeService) {}
}
@Crud({
model: {
type: TestSerialize2Model,
},
serialize: {
get: GetModelResponseDto,
},
routes: {
deleteOneBase: {
returnDeleted: true,
},
},
query: {
alwaysPaginate: true,
},
})
@Controller('test2')
class Test2Controller {
constructor(@Inject(SERVICE2_TOKEN) public service: TestSerializeService) {}
}
@Crud({
model: {
type: TestSerialize2Model,
},
serialize: {
get: GetModelResponseDto,
getMany: GetManyModelResponseDto,
},
})
@Controller('test3')
class Test3Controller implements CrudController {
constructor(@Inject(SERVICE2_TOKEN) public service: TestSerializeService) {}
get base(): CrudController {
return this;
}
@Override()
async getMany(@ParsedRequest() req: CrudRequest) {
const items = (await this.base.getManyBase(req)) as TestSerialize2Model[];
const response = new GetManyModelResponseDto();
response.items = items;
return response;
}
}
@Crud({
model: {
type: TestSerialize2Model,
},
})
@Controller('test4')
class Test4Controller {
constructor(@Inject(SERVICE2_TOKEN) public service: TestSerializeService) {}
}
@Crud({
model: {
type: { name: 'SomeModel' },
},
serialize: {
get: false,
getMany: false,
create: false,
createMany: false,
update: false,
replace: false,
recover: false,
},
})
@Controller('test5')
class Test5Controller {
constructor(@Inject(SERVICE2_TOKEN) public service: TestSerializeService) {}
}
@Crud({
model: {
type: TestSerializeModel,
},
serialize: {
get: GetModelResponseDto,
recover: RecoverModelResponseDto,
},
query: {
softDelete: true,
},
routes: {
recoverOneBase: {
returnRecovered: true,
},
},
})
@Controller('test6')
class Test6Controller {
constructor(@Inject(SERVICE_TOKEN) public service: TestSerializeService) {}
}
beforeAll(async () => {
const fixture = await Test.createTestingModule({
controllers: [
TestController,
Test2Controller,
Test3Controller,
Test4Controller,
Test5Controller,
Test6Controller,
],
providers: [
{ provide: APP_FILTER, useClass: HttpExceptionFilter },
{
provide: SERVICE_TOKEN,
useFactory: () => new TestSerializeService(TestSerializeModel),
},
{
provide: SERVICE2_TOKEN,
useFactory: () => new TestSerializeService(TestSerialize2Model),
},
],
}).compile();
app = fixture.createNestApplication();
await app.init();
server = app.getHttpServer();
});
afterAll(async () => {
await app.close();
});
describe('#getManyBase', () => {
it('should return an array', (done) => {
request(server)
.get('/test')
.expect(200)
.end((_, res) => {
expect(res.body.length).toBe(5);
expect(res.body[0].email).toBeUndefined();
expect(res.body[1].email).toBeUndefined();
done();
});
});
it('should return an object', (done) => {
request(server)
.get('/test2')
.expect(200)
.end((_, res) => {
expect(res.body.data.length).toBe(5);
expect(res.body.data[0].email).toBeUndefined();
expect(res.body.data[1].email).toBeUndefined();
done();
});
});
it('should return custom response', (done) => {
request(server)
.get('/test3')
.expect(200)
.end((_, res) => {
expect(res.body.items.length).toBe(5);
expect(res.body.items[0].email).toBeUndefined();
expect(res.body.items[1].email).toBeUndefined();
done();
});
});
});
describe('#getOneBase', () => {
it('should return model', (done) => {
request(server)
.get('/test4/1')
.expect(200)
.end((_, res) => {
expect(res.body.email).toBeDefined();
expect(res.body.isActive).toBeUndefined();
done();
});
});
it('should return serialized model', (done) => {
request(server)
.get('/test/1')
.expect(200)
.end((_, res) => {
expect(res.body.isActive).toBeDefined();
expect(res.body.email).toBeUndefined();
done();
});
});
it('should return model without serializing', (done) => {
request(server)
.get('/test5/1')
.expect(200)
.end((_, res) => {
expect(res.body.id).toBeDefined();
expect(res.body.name).toBeDefined();
expect(res.body.email).toBeDefined();
expect(res.body.isActive).toBeDefined();
done();
});
});
});
describe('#deleteManyBase', () => {
it('should return serialized model', (done) => {
request(server)
.delete('/test/1')
.expect(200)
.end((_, res) => {
expect(res.body.id).toBeDefined();
expect(res.body.name).toBeUndefined();
expect(res.body.email).toBeUndefined();
expect(res.body.isActive).toBeUndefined();
done();
});
});
it('should return model', (done) => {
request(server)
.delete('/test2/1')
.expect(200)
.end((_, res) => {
expect(res.body.id).toBeDefined();
expect(res.body.name).toBeDefined();
expect(res.body.email).toBeDefined();
expect(res.body.isActive).toBeUndefined();
done();
});
});
it('should return en empty response', (done) => {
request(server)
.delete('/test3/1')
.expect(200)
.end((_, res) => {
expect(res.body).toMatchObject({});
done();
});
});
});
describe('#recoverOneBase', () => {
it('should return model', (done) => {
request(server)
.patch('/test6/1/recover')
.expect(200)
.end((_, res) => {
expect(res.body.id).toBeDefined();
expect(res.body.name).toBeDefined();
expect(res.body.email).toBeDefined();
expect(res.body.isActive).toBeDefined();
done();
});
});
it('should return en empty response', (done) => {
request(server)
.patch('/test3/1/recover')
.expect(200)
.end((_, res) => {
expect(res.body).toMatchObject({});
done();
});
});
});
});
});
================================================
FILE: packages/crud/test/feature-action.decorator.spec.ts
================================================
import { Feature, Action, getFeature, getAction } from '../src/decorators';
describe('#crud', () => {
const feature = 'feature';
const action = 'action';
@Feature(feature)
class TestClass {
@Action(action)
root() {}
}
describe('#feature decorator', () => {
it('should save metadata', () => {
const metadata = getFeature(TestClass);
expect(metadata).toBe(feature);
});
});
describe('#action decorator', () => {
it('should save metadata', () => {
const metadata = getAction(TestClass.prototype.root);
expect(metadata).toBe(action);
});
});
});
================================================
FILE: packages/crud/tsconfig.json
================================================
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib"
},
"include": ["src"],
"exclude": ["lib"],
"references": [{ "path": "../util" }, { "path": "../crud-request" }]
}
================================================
FILE: packages/crud-request/README.md
================================================
CRUD (@nestjsx/crud-request)
for RESTful APIs built with NestJs
We believe that everyone who's working with NestJs and building some RESTful services and especially some CRUD functionality will find `@nestjsx/crud` microframework very useful.
## Features
- Super easy to install and start using the full-featured controllers and services :point_right:
- DB and service agnostic extendable CRUD controllers
- Reach query parsing with filtering, pagination, sorting, relations, nested relations, cache, etc.
- Framework agnostic package with query builder for a frontend usage
- Query, path params and DTOs validation included
- Overriding controller methods with ease
- Tiny config (including globally)
- Additional helper decorators
- Swagger documentation
## Install
```shell
npm i @nestjsx/crud-request
```
## Packages
- [**@nestjsx/crud**](https://www.npmjs.com/package/@nestjsx/crud) - core package which provides `@Crud()` decorator for endpoints generation, global configuration, validation, helper decorators ([docs](https://github.com/nestjsx/crud/wiki/Controllers#description))
- [**@nestjsx/crud-request**](https://www.npmjs.com/package/@nestjsx/crud-request) - request builder/parser package which provides `RequestQueryBuilder` class for a frontend usage and `RequestQueryParser` that is being used internally for handling and validating query/path params on a backend side ([docs](https://github.com/nestjsx/crud/wiki/Requests#frontend-usage))
- [**@nestjsx/crud-typeorm**](https://www.npmjs.com/package/@nestjsx/crud-typeorm) - TypeORM package which provides base `TypeOrmCrudService` with methods for CRUD database operations ([docs](https://github.com/nestjsx/crud/wiki/ServiceTypeorm))
## Documentation
- [General Information](https://github.com/nestjsx/crud/wiki#why)
- [CRUD Controllers](https://github.com/nestjsx/crud/wiki/Controllers#description)
- [CRUD ORM Services](https://github.com/nestjsx/crud/wiki/Services#description)
- [Handling Requests](https://github.com/nestjsx/crud/wiki/Requests#description)
## Support
Any support is welcome. At least you can give us a star.
## Contributors
### Code Contributors
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
### Financial Contributors
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/nestjsx#backer)]
#### Individuals
#### Organizations
Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/nestjsx#sponsor)]
## License
[MIT](LICENSE)
================================================
FILE: packages/crud-request/package.json
================================================
{
"name": "@nestjsx/crud-request",
"description": "NestJs CRUD for RESTful APIs - request query builder",
"version": "5.0.0-alpha.3",
"license": "MIT",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
"publishConfig": {
"access": "public"
},
"files": [
"lib"
],
"repository": {
"type": "git",
"url": "https://github.com/nestjsx/crud.git"
},
"bugs": {
"url": "https://github.com/nestjsx/crud/issues"
},
"keywords": [
"typescript",
"typeorm",
"nest",
"nestjs",
"rest",
"restful",
"api",
"crud",
"crud-generator",
"http",
"request",
"request-query",
"requestquery",
"get",
"query",
"query-string",
"querystring",
"query-builder",
"querybuilder"
],
"author": {
"name": "Michael Yali",
"email": "mihon4ik@gmail.com"
},
"scripts": {
"build": "npx tsc -b"
},
"dependencies": {
"@nestjsx/util": "^5.0.0-alpha.3",
"qs": "^6.10.3"
}
}
================================================
FILE: packages/crud-request/src/exceptions/index.ts
================================================
export * from './request-query.exception';
================================================
FILE: packages/crud-request/src/exceptions/request-query.exception.ts
================================================
export class RequestQueryException extends Error {
constructor(msg: string) {
super(msg);
}
}
================================================
FILE: packages/crud-request/src/index.ts
================================================
export * from './exceptions';
export * from './request-query.builder';
export * from './request-query.parser';
export * from './interfaces';
export * from './types';
================================================
FILE: packages/crud-request/src/interfaces/create-query-params.interface.ts
================================================
import {
QueryFields,
QueryFilter,
QueryFilterArr,
QueryJoin,
QueryJoinArr,
QuerySort,
QuerySortArr,
SCondition,
} from '../types';
export interface CreateQueryParams {
fields?: QueryFields;
search?: SCondition;
filter?: QueryFilter | QueryFilterArr | Array;
or?: QueryFilter | QueryFilterArr | Array;
join?: QueryJoin | QueryJoinArr | Array;
sort?: QuerySort | QuerySortArr | Array;
limit?: number;
offset?: number;
page?: number;
resetCache?: boolean;
includeDeleted?: number;
}
================================================
FILE: packages/crud-request/src/interfaces/index.ts
================================================
export * from './request-query-builder-options.interface';
export * from './params-options.interface';
export * from './parsed-request.interface';
export * from './create-query-params.interface';
================================================
FILE: packages/crud-request/src/interfaces/params-options.interface.ts
================================================
import { ParamOptionType } from '../types';
export interface ParamsOptions {
[key: string]: ParamOption;
}
export interface ParamOption {
field?: string;
type?: ParamOptionType;
primary?: boolean;
disabled?: boolean;
}
================================================
FILE: packages/crud-request/src/interfaces/parsed-request.interface.ts
================================================
import { ObjectLiteral } from '@nestjsx/util';
import { ClassTransformOptions } from 'class-transformer';
import { QueryFields, QueryFilter, QueryJoin, QuerySort, SCondition } from '../types';
export interface ParsedRequestParams {
fields: QueryFields;
paramsFilter: QueryFilter[];
authPersist: ObjectLiteral;
classTransformOptions: ClassTransformOptions;
search: SCondition;
filter: QueryFilter[];
or: QueryFilter[];
join: QueryJoin[];
sort: QuerySort[];
limit: number;
offset: number;
page: number;
cache: number;
includeDeleted: number;
}
================================================
FILE: packages/crud-request/src/interfaces/request-query-builder-options.interface.ts
================================================
export interface RequestQueryBuilderOptions {
delim?: string;
delimStr?: string;
paramNamesMap?: {
fields?: string | string[];
search?: string | string[];
filter?: string | string[];
or?: string | string[];
join?: string | string[];
sort?: string | string[];
limit?: string | string[];
offset?: string | string[];
page?: string | string[];
cache?: string | string[];
includeDeleted?: string | string[];
};
}
================================================
FILE: packages/crud-request/src/request-query.builder.ts
================================================
import { hasValue, isObject, isString, isArrayFull, isNil, isUndefined } from '@nestjsx/util';
import { stringify } from 'qs';
import { RequestQueryBuilderOptions, CreateQueryParams } from './interfaces';
import {
validateCondition,
validateFields,
validateJoin,
validateNumeric,
validateSort,
} from './request-query.validator';
import {
QueryFields,
QueryFilter,
QueryFilterArr,
QueryJoin,
QueryJoinArr,
QuerySort,
QuerySortArr,
SCondition,
} from './types';
// tslint:disable:variable-name ban-types
export class RequestQueryBuilder {
private static _options: RequestQueryBuilderOptions = {
delim: '||',
delimStr: ',',
paramNamesMap: {
fields: ['fields', 'select'],
search: 's',
filter: 'filter',
or: 'or',
join: 'join',
sort: 'sort',
limit: ['limit', 'per_page'],
offset: 'offset',
page: 'page',
cache: 'cache',
includeDeleted: 'include_deleted',
},
};
public queryObject: { [key: string]: any } = {};
public queryString: string;
private paramNames: {
[key in keyof RequestQueryBuilderOptions['paramNamesMap']]: string;
} = {};
constructor() {
this.setParamNames();
}
static setOptions(options: RequestQueryBuilderOptions) {
RequestQueryBuilder._options = {
...RequestQueryBuilder._options,
...options,
paramNamesMap: {
...RequestQueryBuilder._options.paramNamesMap,
...(options.paramNamesMap ? options.paramNamesMap : {}),
},
};
}
static getOptions(): RequestQueryBuilderOptions {
return RequestQueryBuilder._options;
}
static create(params?: CreateQueryParams): RequestQueryBuilder {
const qb = new RequestQueryBuilder();
return isObject(params) ? qb.createFromParams(params) : qb;
}
get options(): RequestQueryBuilderOptions {
return RequestQueryBuilder._options;
}
setParamNames() {
Object.keys(RequestQueryBuilder._options.paramNamesMap).forEach((key) => {
const name = RequestQueryBuilder._options.paramNamesMap[key];
this.paramNames[key] = isString(name) ? (name as string) : (name[0] as string);
});
}
query(encode = true): string {
if (this.queryObject[this.paramNames.search]) {
this.queryObject[this.paramNames.filter] = undefined;
this.queryObject[this.paramNames.or] = undefined;
}
this.queryString = stringify(this.queryObject, { encode });
return this.queryString;
}
select(fields: QueryFields): this {
if (isArrayFull(fields)) {
validateFields(fields);
this.queryObject[this.paramNames.fields] = fields.join(this.options.delimStr);
}
return this;
}
search(s: SCondition) {
if (!isNil(s) && isObject(s)) {
this.queryObject[this.paramNames.search] = JSON.stringify(s);
}
return this;
}
setFilter(f: QueryFilter | QueryFilterArr | Array): this {
this.setCondition(f, 'filter');
return this;
}
setOr(f: QueryFilter | QueryFilterArr | Array): this {
this.setCondition(f, 'or');
return this;
}
setJoin(j: QueryJoin | QueryJoinArr | Array): this {
if (!isNil(j)) {
const param = this.checkQueryObjectParam('join', []);
this.queryObject[param] = [
...this.queryObject[param],
...(Array.isArray(j) && !isString(j[0])
? (j as Array).map((o) => this.addJoin(o))
: [this.addJoin(j as QueryJoin | QueryJoinArr)]),
];
}
return this;
}
sortBy(s: QuerySort | QuerySortArr | Array): this {
if (!isNil(s)) {
const param = this.checkQueryObjectParam('sort', []);
this.queryObject[param] = [
...this.queryObject[param],
...(Array.isArray(s) && !isString(s[0])
? (s as Array).map((o) => this.addSortBy(o))
: [this.addSortBy(s as QuerySort | QuerySortArr)]),
];
}
return this;
}
setLimit(n: number): this {
this.setNumeric(n, 'limit');
return this;
}
setOffset(n: number): this {
this.setNumeric(n, 'offset');
return this;
}
setPage(n: number): this {
this.setNumeric(n, 'page');
return this;
}
resetCache(): this {
this.setNumeric(0, 'cache');
return this;
}
setIncludeDeleted(n: number): this {
this.setNumeric(n, 'includeDeleted');
return this;
}
cond(f: QueryFilter | QueryFilterArr, cond: 'filter' | 'or' | 'search' = 'search'): string {
const filter = Array.isArray(f) ? { field: f[0], operator: f[1], value: f[2] } : f;
validateCondition(filter, cond);
const d = this.options.delim;
return filter.field + d + filter.operator + (hasValue(filter.value) ? d + filter.value : '');
}
private addJoin(j: QueryJoin | QueryJoinArr): string {
const join = Array.isArray(j) ? { field: j[0], select: j[1] } : j;
validateJoin(join);
const d = this.options.delim;
const ds = this.options.delimStr;
return join.field + (isArrayFull(join.select) ? d + join.select.join(ds) : '');
}
private addSortBy(s: QuerySort | QuerySortArr): string {
const sort = Array.isArray(s) ? { field: s[0], order: s[1] } : s;
validateSort(sort);
const ds = this.options.delimStr;
return sort.field + ds + sort.order;
}
private createFromParams(params: CreateQueryParams): this {
this.select(params.fields);
this.search(params.search);
this.setFilter(params.filter);
this.setOr(params.or);
this.setJoin(params.join);
this.setLimit(params.limit);
this.setOffset(params.offset);
this.setPage(params.page);
this.sortBy(params.sort);
if (params.resetCache) {
this.resetCache();
}
this.setIncludeDeleted(params.includeDeleted);
return this;
}
private checkQueryObjectParam(cond: keyof RequestQueryBuilderOptions['paramNamesMap'], defaults: any): string {
const param = this.paramNames[cond];
if (isNil(this.queryObject[param]) && !isUndefined(defaults)) {
this.queryObject[param] = defaults;
}
return param;
}
private setCondition(
f: QueryFilter | QueryFilterArr | Array,
cond: 'filter' | 'or',
): void {
if (!isNil(f)) {
const param = this.checkQueryObjectParam(cond, []);
this.queryObject[param] = [
...this.queryObject[param],
...(Array.isArray(f) && !isString(f[0])
? (f as Array).map((o) => this.cond(o, cond))
: [this.cond(f as QueryFilter | QueryFilterArr, cond)]),
];
}
}
private setNumeric(n: number, cond: 'limit' | 'offset' | 'page' | 'cache' | 'includeDeleted'): void {
if (!isNil(n)) {
validateNumeric(n, cond);
this.queryObject[this.paramNames[cond]] = n;
}
}
}
================================================
FILE: packages/crud-request/src/request-query.parser.ts
================================================
import {
hasLength,
hasValue,
isString,
isArrayFull,
isDate,
isDateString,
isObject,
isStringFull,
objKeys,
isNil,
ObjectLiteral,
} from '@nestjsx/util';
import { ClassTransformOptions } from 'class-transformer';
import { RequestQueryException } from './exceptions';
import { ParamsOptions, ParsedRequestParams, RequestQueryBuilderOptions } from './interfaces';
import { RequestQueryBuilder } from './request-query.builder';
import {
validateCondition,
validateJoin,
validateNumeric,
validateParamOption,
validateSort,
validateUUID,
} from './request-query.validator';
import {
ComparisonOperator,
QueryFields,
QueryFilter,
QueryJoin,
QuerySort,
SCondition,
SConditionAND,
SFields,
} from './types';
// tslint:disable:variable-name ban-types
export class RequestQueryParser implements ParsedRequestParams {
public fields: QueryFields = [];
public paramsFilter: QueryFilter[] = [];
public authPersist: ObjectLiteral = undefined;
public classTransformOptions: ClassTransformOptions = undefined;
public search: SCondition;
public filter: QueryFilter[] = [];
public or: QueryFilter[] = [];
public join: QueryJoin[] = [];
public sort: QuerySort[] = [];
public limit: number;
public offset: number;
public page: number;
public cache: number;
public includeDeleted: number;
private _params: any;
private _query: any;
private _paramNames: string[];
private _paramsOptions: ParamsOptions;
private get _options(): RequestQueryBuilderOptions {
return RequestQueryBuilder.getOptions();
}
static create(): RequestQueryParser {
return new RequestQueryParser();
}
getParsed(): ParsedRequestParams {
return {
fields: this.fields,
paramsFilter: this.paramsFilter,
authPersist: this.authPersist,
classTransformOptions: this.classTransformOptions,
search: this.search,
filter: this.filter,
or: this.or,
join: this.join,
sort: this.sort,
limit: this.limit,
offset: this.offset,
page: this.page,
cache: this.cache,
includeDeleted: this.includeDeleted,
};
}
parseQuery(query: any): this {
if (isObject(query)) {
const paramNames = objKeys(query);
if (hasLength(paramNames)) {
this._query = query;
this._paramNames = paramNames;
const searchData = this._query[this.getParamNames('search')[0]];
this.search = this.parseSearchQueryParam(searchData) as any;
if (isNil(this.search)) {
this.filter = this.parseQueryParam('filter', this.conditionParser.bind(this, 'filter'));
this.or = this.parseQueryParam('or', this.conditionParser.bind(this, 'or'));
}
this.fields = this.parseQueryParam('fields', this.fieldsParser.bind(this))[0] || [];
this.join = this.parseQueryParam('join', this.joinParser.bind(this));
this.sort = this.parseQueryParam('sort', this.sortParser.bind(this));
this.limit = this.parseQueryParam('limit', this.numericParser.bind(this, 'limit'))[0];
this.offset = this.parseQueryParam('offset', this.numericParser.bind(this, 'offset'))[0];
this.page = this.parseQueryParam('page', this.numericParser.bind(this, 'page'))[0];
this.cache = this.parseQueryParam('cache', this.numericParser.bind(this, 'cache'))[0];
this.includeDeleted = this.parseQueryParam(
'includeDeleted',
this.numericParser.bind(this, 'includeDeleted'),
)[0];
}
}
return this;
}
parseParams(params: any, options: ParamsOptions): this {
if (isObject(params)) {
const paramNames = objKeys(params);
if (hasLength(paramNames)) {
this._params = params;
this._paramsOptions = options;
this.paramsFilter = paramNames.map((name) => this.paramParser(name)).filter((filter) => filter);
}
}
return this;
}
setAuthPersist(persist: ObjectLiteral = {}) {
this.authPersist = persist || /* istanbul ignore next */ {};
}
setClassTransformOptions(options: ClassTransformOptions = {}) {
this.classTransformOptions = options || /* istanbul ignore next */ {};
}
convertFilterToSearch(filter: QueryFilter): SFields | SConditionAND {
const isEmptyValue = {
isnull: true,
notnull: true,
};
return filter
? {
[filter.field]: {
[filter.operator]: isEmptyValue[filter.operator] ? isEmptyValue[filter.operator] : filter.value,
},
}
: /* istanbul ignore next */ {};
}
private getParamNames(type: keyof RequestQueryBuilderOptions['paramNamesMap']): string[] {
return this._paramNames.filter((p) => {
const name = this._options.paramNamesMap[type];
return isString(name) ? name === p : (name as string[]).some((m) => m === p);
});
}
private getParamValues(value: string | string[], parser: any): string[] {
if (isStringFull(value)) {
return [parser.call(this, value)];
}
if (isArrayFull(value)) {
return (value as string[]).map((val) => parser(val));
}
return [];
}
private parseQueryParam(type: keyof RequestQueryBuilderOptions['paramNamesMap'], parser: any) {
const param = this.getParamNames(type);
if (isArrayFull(param)) {
return param.reduce((a, name) => [...a, ...this.getParamValues(this._query[name], parser)], []);
}
return [];
}
private parseValue(val: any) {
try {
const parsed = JSON.parse(val);
if (!isDate(parsed) && isObject(parsed)) {
// throw new Error('Don\'t support object now');
return val;
} else if (typeof parsed === 'number' && parsed.toLocaleString('fullwide', { useGrouping: false }) !== val) {
// JS cannot handle big numbers. Leave it as a string to prevent data loss
return val;
}
return parsed;
} catch (ignored) {
if (isDateString(val)) {
return new Date(val);
}
return val;
}
}
private parseValues(vals: any) {
if (isArrayFull(vals)) {
return vals.map((v: any) => this.parseValue(v));
} else {
return this.parseValue(vals);
}
}
private fieldsParser(data: string): QueryFields {
return data.split(this._options.delimStr);
}
private parseSearchQueryParam(d: any): SCondition {
try {
if (isNil(d)) {
return undefined;
}
const data = JSON.parse(d);
if (!isObject(data)) {
throw new Error();
}
return data;
} catch (_) {
throw new RequestQueryException('Invalid search param. JSON expected');
}
}
private conditionParser(cond: 'filter' | 'or' | 'search', data: string): QueryFilter {
const isArrayValue = ['in', 'notin', 'between', '$in', '$notin', '$between', '$inL', '$notinL'];
const isEmptyValue = ['isnull', 'notnull', '$isnull', '$notnull'];
const param = data.split(this._options.delim);
const field = param[0];
const operator = param[1] as ComparisonOperator;
let value = param[2] || '';
if (isArrayValue.some((name) => name === operator)) {
value = value.split(this._options.delimStr) as any;
}
value = this.parseValues(value);
if (!isEmptyValue.some((name) => name === operator) && !hasValue(value)) {
throw new RequestQueryException(`Invalid ${cond} value`);
}
const condition: QueryFilter = { field, operator, value };
validateCondition(condition, cond);
return condition;
}
private joinParser(data: string): QueryJoin {
const param = data.split(this._options.delim);
const join: QueryJoin = {
field: param[0],
select: isStringFull(param[1]) ? param[1].split(this._options.delimStr) : undefined,
};
validateJoin(join);
return join;
}
private sortParser(data: string): QuerySort {
const param = data.split(this._options.delimStr);
const sort: QuerySort = {
field: param[0],
order: param[1] as any,
};
validateSort(sort);
return sort;
}
private numericParser(num: 'limit' | 'offset' | 'page' | 'cache' | 'includeDeleted', data: string): number {
const val = this.parseValue(data);
validateNumeric(val, num);
return val;
}
private paramParser(name: string): QueryFilter {
validateParamOption(this._paramsOptions, name);
const option = this._paramsOptions[name];
if (option.disabled) {
return undefined;
}
let value = this._params[name];
switch (option.type) {
case 'number':
value = this.parseValue(value);
validateNumeric(value, `param ${name}`);
break;
case 'uuid':
validateUUID(value, name);
break;
default:
break;
}
return { field: option.field, operator: '$eq', value };
}
}
================================================
FILE: packages/crud-request/src/request-query.validator.ts
================================================
import { isUndefined, isArrayStrings, isStringFull, isObject, isEqual, isNumber, isNil, objKeys } from '@nestjsx/util';
import { RequestQueryException } from './exceptions';
import { ParamsOptions, ParamOption } from './interfaces';
import { QueryFields, QueryFilter, ComparisonOperator, QueryJoin, QuerySort, CondOperator } from './types';
export const deprecatedComparisonOperatorsList = [
'eq',
'ne',
'gt',
'lt',
'gte',
'lte',
'starts',
'ends',
'cont',
'excl',
'in',
'notin',
'isnull',
'notnull',
'between',
];
export const comparisonOperatorsList = [
...deprecatedComparisonOperatorsList,
...objKeys(CondOperator).map((n) => CondOperator[n]),
];
export const sortOrdersList = ['ASC', 'DESC'];
const comparisonOperatorsListStr = comparisonOperatorsList.join();
const sortOrdersListStr = sortOrdersList.join();
export function validateFields(fields: QueryFields): void {
if (!isArrayStrings(fields)) {
throw new RequestQueryException('Invalid fields. Array of strings expected');
}
}
export function validateCondition(val: QueryFilter, cond: 'filter' | 'or' | 'search'): void {
if (!isObject(val) || !isStringFull(val.field)) {
throw new RequestQueryException(`Invalid field type in ${cond} condition. String expected`);
}
validateComparisonOperator(val.operator);
}
export function validateComparisonOperator(operator: ComparisonOperator): void {
if (!comparisonOperatorsList.includes(operator)) {
throw new RequestQueryException(`Invalid comparison operator. ${comparisonOperatorsListStr} expected`);
}
}
export function validateJoin(join: QueryJoin): void {
if (!isObject(join) || !isStringFull(join.field)) {
throw new RequestQueryException('Invalid join field. String expected');
}
if (!isUndefined(join.select) && !isArrayStrings(join.select)) {
throw new RequestQueryException('Invalid join select. Array of strings expected');
}
}
export function validateSort(sort: QuerySort): void {
if (!isObject(sort) || !isStringFull(sort.field)) {
throw new RequestQueryException('Invalid sort field. String expected');
}
if (!isEqual(sort.order, sortOrdersList[0]) && !isEqual(sort.order, sortOrdersList[1])) {
throw new RequestQueryException(`Invalid sort order. ${sortOrdersListStr} expected`);
}
}
export function validateNumeric(
val: number,
num: 'limit' | 'offset' | 'page' | 'cache' | 'include_deleted' | string,
): void {
if (!isNumber(val)) {
throw new RequestQueryException(`Invalid ${num}. Number expected`);
}
}
export function validateParamOption(options: ParamsOptions, name: string) {
if (!isObject(options)) {
throw new RequestQueryException(`Invalid param ${name}. Invalid crud options`);
}
const option = options[name];
if (option && option.disabled) {
return;
}
if (!isObject(option) || isNil(option.field) || isNil(option.type)) {
throw new RequestQueryException('Invalid param option in Crud');
}
}
export function validateUUID(str: string, name: string) {
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const uuidV4 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!uuidV4.test(str) && !uuid.test(str)) {
throw new RequestQueryException(`Invalid param ${name}. UUID string expected`);
}
}
================================================
FILE: packages/crud-request/src/types/index.ts
================================================
export * from './request-query.types';
export * from './request-param.types';
================================================
FILE: packages/crud-request/src/types/request-param.types.ts
================================================
export type ParamOptionType = 'number' | 'string' | 'uuid';
================================================
FILE: packages/crud-request/src/types/request-query.types.ts
================================================
export type QueryFields = string[];
export type QueryFilter = {
field: string;
operator: ComparisonOperator;
value?: any;
};
export type QueryFilterArr = [string, ComparisonOperator, any?];
export type QueryJoin = {
field: string;
select?: QueryFields;
};
export type QueryJoinArr = [string, QueryFields?];
export type QuerySort = {
field: string;
order: QuerySortOperator;
};
export type QuerySortArr = [string, QuerySortOperator];
export type QuerySortOperator = 'ASC' | 'DESC';
type DeprecatedCondOperator =
| 'eq'
| 'ne'
| 'gt'
| 'lt'
| 'gte'
| 'lte'
| 'starts'
| 'ends'
| 'cont'
| 'excl'
| 'in'
| 'notin'
| 'isnull'
| 'notnull'
| 'between';
export enum CondOperator {
EQUALS = '$eq',
NOT_EQUALS = '$ne',
GREATER_THAN = '$gt',
LOWER_THAN = '$lt',
GREATER_THAN_EQUALS = '$gte',
LOWER_THAN_EQUALS = '$lte',
STARTS = '$starts',
ENDS = '$ends',
CONTAINS = '$cont',
EXCLUDES = '$excl',
IN = '$in',
NOT_IN = '$notin',
IS_NULL = '$isnull',
NOT_NULL = '$notnull',
BETWEEN = '$between',
EQUALS_LOW = '$eqL',
NOT_EQUALS_LOW = '$neL',
STARTS_LOW = '$startsL',
ENDS_LOW = '$endsL',
CONTAINS_LOW = '$contL',
EXCLUDES_LOW = '$exclL',
IN_LOW = '$inL',
NOT_IN_LOW = '$notinL',
}
export type ComparisonOperator = DeprecatedCondOperator | keyof SFieldOperator;
// new search
export type SPrimitivesVal = string | number | boolean;
export type SFiledValues = SPrimitivesVal | Array;
export type SFieldOperator = {
$eq?: SFiledValues;
$ne?: SFiledValues;
$gt?: SFiledValues;
$lt?: SFiledValues;
$gte?: SFiledValues;
$lte?: SFiledValues;
$starts?: SFiledValues;
$ends?: SFiledValues;
$cont?: SFiledValues;
$excl?: SFiledValues;
$in?: SFiledValues;
$notin?: SFiledValues;
$between?: SFiledValues;
$isnull?: SFiledValues;
$notnull?: SFiledValues;
$eqL?: SFiledValues;
$neL?: SFiledValues;
$startsL?: SFiledValues;
$endsL?: SFiledValues;
$contL?: SFiledValues;
$exclL?: SFiledValues;
$inL?: SFiledValues;
$notinL?: SFiledValues;
$or?: SFieldOperator;
$and?: never;
};
export type SField = SPrimitivesVal | SFieldOperator;
export type SFields = {
[key: string]: SField | Array | undefined;
$or?: Array;
$and?: never;
};
export type SConditionAND = {
$and?: Array;
$or?: never;
};
export type SConditionKey = '$and' | '$or';
export type SCondition = SFields | SConditionAND;
================================================
FILE: packages/crud-request/test/request-query.builder.spec.ts
================================================
import 'jest-extended';
import { RequestQueryBuilder } from '../src/request-query.builder';
import { RequestQueryException } from '../src/exceptions/request-query.exception';
const defaultOptions = { ...(RequestQueryBuilder as any)._options };
describe('#request-query', () => {
describe('#RequestQueryBuilder', () => {
let qb: RequestQueryBuilder;
beforeEach(() => {
qb = RequestQueryBuilder.create();
});
afterEach(() => {
(RequestQueryBuilder as any)._options = defaultOptions;
});
it('should be a function', () => {
expect(typeof RequestQueryBuilder).toBe('function');
});
describe('#static setOptions', () => {
it('should merge options, 1', () => {
const override = 'override';
RequestQueryBuilder.setOptions({
paramNamesMap: { fields: [override] },
});
const paramNamesMap = (RequestQueryBuilder as any)._options.paramNamesMap;
expect(paramNamesMap.fields[0]).toBe(override);
expect(paramNamesMap.page).toBe('page');
});
it('should merge options, 2', () => {
const override = 'override';
RequestQueryBuilder.setOptions({ delim: override });
const _options = (RequestQueryBuilder as any)._options;
expect(_options.delim).toBe(override);
});
});
describe('#select', () => {
it('should not throw', () => {
(qb as any).select();
expect(qb.queryObject.fields).toBeUndefined();
});
it('should throw an error', () => {
expect((qb.select as any).bind(qb, [false])).toThrowError(RequestQueryException);
});
it('should set fields', () => {
qb.select(['foo', 'bar']);
const expected = 'foo,bar';
expect(qb.queryObject.fields).toBe(expected);
});
});
describe('#setFilter', () => {
it('should not throw', () => {
(qb as any).setFilter();
expect(qb.queryObject.filter).toBeUndefined();
});
it('should throw an error, 1', () => {
expect((qb.setFilter as any).bind(qb, { field: 1 })).toThrowError(RequestQueryException);
});
it('should throw an error, 2', () => {
expect((qb.setFilter as any).bind(qb, { field: 'foo', operator: 'bar' })).toThrowError(RequestQueryException);
});
it('should throw an error, 3', () => {
expect((qb.setFilter as any).bind(qb, [{}])).toThrowError(RequestQueryException);
});
it('should set filter, 1', () => {
qb.setFilter({ field: 'foo', operator: 'eq', value: 'bar' });
const expected = ['foo||eq||bar'];
expect(qb.queryObject.filter).toIncludeSameMembers(expected);
});
it('should set filter, 2', () => {
qb.setFilter([
{ field: 'foo', operator: 'eq', value: 'bar' },
{ field: 'baz', operator: 'ne', value: 'zoo' },
]);
const expected = ['foo||eq||bar', 'baz||ne||zoo'];
expect(qb.queryObject.filter).toIncludeSameMembers(expected);
});
it('should set filter, 3', () => {
qb.setFilter([['foo', 'eq', 'bar'], { field: 'baz', operator: 'ne', value: 'zoo' }]);
const expected = ['foo||eq||bar', 'baz||ne||zoo'];
expect(qb.queryObject.filter).toIncludeSameMembers(expected);
});
it('should set filter, 4', () => {
qb.setFilter([
['foo', 'eq', 'bar'],
['baz', 'ne', 'zoo'],
]);
const expected = ['foo||eq||bar', 'baz||ne||zoo'];
expect(qb.queryObject.filter).toIncludeSameMembers(expected);
});
it('should set filter, 5', () => {
qb.setFilter(['foo', 'eq', 'bar']);
const expected = ['foo||eq||bar'];
expect(qb.queryObject.filter).toIncludeSameMembers(expected);
});
});
describe('#setOr', () => {
it('should not throw', () => {
(qb as any).setOr();
expect(qb.queryObject.or).toBeUndefined();
});
it('should throw an error, 1', () => {
expect((qb.setOr as any).bind(qb, { field: 1 })).toThrowError(RequestQueryException);
});
it('should throw an error, 2', () => {
expect((qb.setOr as any).bind(qb, { field: 'foo', operator: 'bar' })).toThrowError(RequestQueryException);
});
it('should throw an error, 3', () => {
expect((qb.setOr as any).bind(qb, [{}])).toThrowError(RequestQueryException);
});
it('should set or, 1', () => {
qb.setOr({ field: 'foo', operator: 'eq', value: 'bar' });
const expected = ['foo||eq||bar'];
expect(qb.queryObject.or).toIncludeSameMembers(expected);
});
it('should set or, 2', () => {
qb.setOr([
{ field: 'foo', operator: 'eq', value: 'bar' },
{ field: 'baz', operator: 'ne', value: 'zoo' },
]);
const expected = ['foo||eq||bar', 'baz||ne||zoo'];
expect(qb.queryObject.or).toIncludeSameMembers(expected);
});
});
describe('#setJoin', () => {
it('should not throw', () => {
(qb as any).setJoin();
expect(qb.queryObject.join).toBeUndefined();
});
it('should throw an error, 1', () => {
expect((qb.setJoin as any).bind(qb, { field: 1 })).toThrowError(RequestQueryException);
});
it('should throw an error, 2', () => {
expect((qb.setJoin as any).bind(qb, { field: 'foo', select: 1 })).toThrowError(RequestQueryException);
});
it('should throw an error, 3', () => {
expect((qb.setJoin as any).bind(qb, [{}])).toThrowError(RequestQueryException);
});
it('should set join, 1', () => {
qb.setJoin({ field: 'foo' });
const expected = ['foo'];
expect(qb.queryObject.join).toIncludeSameMembers(expected);
});
it('should set join, 2', () => {
qb.setJoin([{ field: 'foo' }, { field: 'bar', select: ['a', 'b', 'c'] }]);
const expected = ['foo', 'bar||a,b,c'];
expect(qb.queryObject.join).toIncludeSameMembers(expected);
});
it('should set join, 3', () => {
qb.setJoin(['foo']);
const expected = ['foo'];
expect(qb.queryObject.join).toIncludeSameMembers(expected);
});
it('should set join, 4', () => {
qb.setJoin(['foo', ['a', 'b', 'c']]);
const expected = ['foo||a,b,c'];
expect(qb.queryObject.join).toIncludeSameMembers(expected);
});
it('should set join, 5', () => {
qb.setJoin([{ field: 'baz' }, ['foo', ['a', 'b', 'c']]]);
const expected = ['baz', 'foo||a,b,c'];
expect(qb.queryObject.join).toIncludeSameMembers(expected);
});
});
describe('#sortBy', () => {
it('should not throw', () => {
(qb as any).sortBy();
expect(qb.queryObject.sort).toBeUndefined();
});
it('should throw an error, 1', () => {
expect((qb.sortBy as any).bind(qb, { field: 1 })).toThrowError(RequestQueryException);
});
it('should throw an error, 2', () => {
expect((qb.sortBy as any).bind(qb, { field: 'foo', order: 'bar' })).toThrowError(RequestQueryException);
});
it('should throw an error, 3', () => {
expect((qb.sortBy as any).bind(qb, [{}])).toThrowError(RequestQueryException);
});
it('should set sort, 1', () => {
qb.sortBy({ field: 'foo', order: 'ASC' });
const expected = ['foo,ASC'];
expect(qb.queryObject.sort).toIncludeSameMembers(expected);
});
it('should set sort, 2', () => {
qb.sortBy([
{ field: 'foo', order: 'ASC' },
{ field: 'bar', order: 'DESC' },
]);
const expected = ['foo,ASC', 'bar,DESC'];
expect(qb.queryObject.sort).toIncludeSameMembers(expected);
});
it('should set sort, 3', () => {
qb.sortBy(['foo', 'ASC']);
const expected = ['foo,ASC'];
expect(qb.queryObject.sort).toIncludeSameMembers(expected);
});
it('should set sort, 4', () => {
qb.sortBy([['foo', 'ASC']]);
const expected = ['foo,ASC'];
expect(qb.queryObject.sort).toIncludeSameMembers(expected);
});
it('should set sort, 5', () => {
qb.sortBy([{ field: 'bar', order: 'DESC' }, ['foo', 'ASC']]);
const expected = ['bar,DESC', 'foo,ASC'];
expect(qb.queryObject.sort).toIncludeSameMembers(expected);
});
});
describe('#setLimit', () => {
it('should not throw', () => {
(qb as any).setLimit();
expect(qb.queryObject.limit).toBeUndefined();
});
it('should throw an error', () => {
expect((qb.setLimit as any).bind(qb, {})).toThrowError(RequestQueryException);
});
it('should set limit', () => {
const expected = 10;
qb.setLimit(expected);
expect(qb.queryObject.limit).toBe(expected);
});
});
describe('#setOffset', () => {
it('should not throw', () => {
(qb as any).setOffset();
expect(qb.queryObject.offset).toBeUndefined();
});
it('should throw an error', () => {
expect((qb.setOffset as any).bind(qb, {})).toThrowError(RequestQueryException);
});
it('should set offset', () => {
const expected = 10;
qb.setOffset(expected);
expect(qb.queryObject.offset).toBe(expected);
});
});
describe('#setPage', () => {
it('should not throw', () => {
(qb as any).setPage();
expect(qb.queryObject.page).toBeUndefined();
});
it('should throw an error', () => {
expect((qb.setPage as any).bind(qb, {})).toThrowError(RequestQueryException);
});
it('should set page', () => {
const expected = 10;
qb.setPage(expected);
expect(qb.queryObject.page).toBe(expected);
});
});
describe('#resetCache', () => {
it('should set cache', () => {
expect(qb.queryObject.cache).toBeUndefined();
qb.resetCache();
expect(qb.queryObject.cache).toBe(0);
});
});
describe('#cond', () => {
it('should throw an error, 1', () => {
expect(qb.cond as any).toThrowError(RequestQueryException);
});
it('should throw an error, 2', () => {
expect((qb.cond as any).bind(qb, {})).toThrowError(RequestQueryException);
});
it('should return a filter string from an object', () => {
const test = qb.cond({ field: 'foo', operator: 'eq', value: 'bar' });
const expected = 'foo||eq||bar';
expect(test).toBe(expected);
});
it('should return a filter string from an array', () => {
const test = qb.cond(['foo', 'eq', 'bar']);
const expected = 'foo||eq||bar';
expect(test).toBe(expected);
});
});
describe('#query', () => {
it('should return an empty string', () => {
expect(qb.query()).toBe('');
});
it('should return query with overrided fields name', () => {
RequestQueryBuilder.setOptions({ paramNamesMap: { fields: ['override'] } });
qb.setParamNames();
const test = qb.select(['foo', 'bar']).query();
const test2 = qb.select(['foo', 'bar']).query(false);
const expected = 'override=foo%2Cbar';
const expected2 = 'override=foo,bar';
expect(test).toBe(expected);
expect(test2).toBe(expected2);
});
it('should return valid query string with filters', () => {
const test = qb
.select(['foo', 'bar'])
.setFilter([
{ field: 'is', operator: 'notnull' },
{ field: 'foo', operator: 'lt', value: 10 },
])
.query(false);
const expected = 'fields=foo,bar&filter[0]=is||notnull&filter[1]=foo||lt||10';
expect(test).toBe(expected);
});
it('should return a valid query string', () => {
const test = qb
.select(['foo', 'bar'])
.setFilter(['is', 'notnull'])
.setOr({ field: 'ok', operator: 'ne', value: false })
.setJoin({ field: 'voo', select: ['h', 'data'] })
.setLimit(1)
.setOffset(2)
.setPage(3)
.sortBy({ field: 'foo', order: 'DESC' })
.resetCache()
.setIncludeDeleted(1)
.query(false);
const expected =
'fields=foo,bar&filter[0]=is||notnull&or[0]=ok||ne||false&join[0]=voo||h,data&limit=1&offset=2&page=3&sort[0]=foo,DESC&cache=0&include_deleted=1'; // eslint-disable-line
expect(test).toBe(expected);
});
});
describe('#search', () => {
it('should not throw, 1', () => {
(qb as any).search();
expect(qb.queryObject.search).toBeUndefined();
});
it('should not throw, 2', () => {
(qb as any).search(false);
expect(qb.queryObject.search).toBeUndefined();
});
it('should set search string, 1', () => {
const test = qb.search({ $or: [{ id: 1 }, { name: 'foo' }] }).query(false);
const expected = 's={"$or":[{"id":1},{"name":"foo"}]}';
expect(test).toBe(expected);
});
it('should set search string, 2', () => {
const test = qb.search({ $or: [{ id: 1 }, { name: 'foo' }] }).query();
const expected = 's=%7B%22%24or%22%3A%5B%7B%22id%22%3A1%7D%2C%7B%22name%22%3A%22foo%22%7D%5D%7D';
expect(test).toBe(expected);
});
});
describe('#createFromParams', () => {
it('should return an empty query string', () => {
const test = RequestQueryBuilder.create().query();
expect(test).toBe('');
});
it('should return a valid query string, 1', () => {
const test = RequestQueryBuilder.create({
fields: ['foo', 'bar'],
filter: ['is', 'notnull'],
or: { field: 'ok', operator: 'ne', value: false },
join: { field: 'voo', select: ['h', 'data'] },
limit: 1,
offset: 2,
page: 3,
sort: [['foo', 'DESC']],
resetCache: true,
}).query(false);
const expected =
'fields=foo,bar&filter[0]=is||notnull&or[0]=ok||ne||false&join[0]=voo||h,data&limit=1&offset=2&page=3&sort[0]=foo,DESC&cache=0';
expect(test).toBe(expected);
});
it('should return a valid query string, 2', () => {
const test = RequestQueryBuilder.create({
fields: ['foo', 'bar'],
}).query(false);
const expected = 'fields=foo,bar';
expect(test).toBe(expected);
});
});
});
});
================================================
FILE: packages/crud-request/test/request-query.parser.spec.ts
================================================
import 'jest-extended';
import { RequestQueryException } from '../src/exceptions/request-query.exception';
import { ParamsOptions, ParsedRequestParams } from '../src/interfaces';
import { RequestQueryParser } from '../src/request-query.parser';
import { QueryFilter, QueryJoin, QuerySort } from '../src/types';
describe('#request-query', () => {
describe('RequestQueryParser', () => {
let qp: RequestQueryParser;
beforeEach(() => {
qp = RequestQueryParser.create();
});
describe('#parseQury', () => {
it('should return instance of RequestQueryParse', () => {
expect((qp as any).parseQuery()).toBeInstanceOf(RequestQueryParser);
expect((qp as any).parseQuery({})).toBeInstanceOf(RequestQueryParser);
});
describe('#parse fields', () => {
it('should set empty array, 1', () => {
const query = { select: '' };
const expected = [];
const test = qp.parseQuery(query);
expect(test.fields).toMatchObject(expected);
});
it('should set empty array, 2', () => {
const query = { foo: '' };
const expected = [];
const test = qp.parseQuery(query);
expect(test.fields).toMatchObject(expected);
});
it('should set array, 1', () => {
const query = { select: 'foo' };
const expected = ['foo'];
const test = qp.parseQuery(query);
expect(test.fields).toMatchObject(expected);
});
it('should set array, 2', () => {
const query = { select: 'foo,bar' };
const expected = ['foo', 'bar'];
const test = qp.parseQuery(query);
expect(test.fields).toMatchObject(expected);
});
});
describe('#parse filter', () => {
it('should set empty array, 1', () => {
const query = { filter: '' };
const expected = [];
const test = qp.parseQuery(query);
expect(test.filter).toMatchObject(expected);
});
it('should set empty array, 2', () => {
const query = { foo: '' };
const expected = [];
const test = qp.parseQuery(query);
expect(test.filter).toMatchObject(expected);
});
it('should throw an error, 1', () => {
const query = { filter: 'foo||invalid||bar' };
expect(qp.parseQuery.bind(qp, query)).toThrowError(RequestQueryException);
});
it('should throw an error, 2', () => {
const query = { filter: 'foo||eq' };
expect(qp.parseQuery.bind(qp, query)).toThrowError(RequestQueryException);
});
it('should set array, 1', () => {
const query = { filter: 'foo||eq||bar' };
const expected: QueryFilter[] = [{ field: 'foo', operator: 'eq', value: 'bar' }];
const test = qp.parseQuery(query);
expect(test.filter[0]).toMatchObject(expected[0]);
});
it('should set array, 2', () => {
const query = { filter: ['foo||eq||bar', 'baz||ne||boo'] };
const expected: QueryFilter[] = [
{ field: 'foo', operator: 'eq', value: 'bar' },
{ field: 'baz', operator: 'ne', value: 'boo' },
];
const test = qp.parseQuery(query);
expect(test.filter[0]).toMatchObject(expected[0]);
expect(test.filter[1]).toMatchObject(expected[1]);
});
it('should set array, 3', () => {
const query = { filter: ['foo||in||1,2'] };
const expected: QueryFilter[] = [{ field: 'foo', operator: 'in', value: [1, 2] }];
const test = qp.parseQuery(query);
expect(test.filter[0]).toMatchObject(expected[0]);
});
it('should set array, 4', () => {
const query = { filter: ['foo||isnull'] };
const expected: QueryFilter[] = [{ field: 'foo', operator: 'isnull', value: '' }];
const test = qp.parseQuery(query);
expect(test.filter[0]).toMatchObject(expected[0]);
});
it('should set array, 5', () => {
const query = { filter: ['foo||eq||{"foo":true}'] };
const expected: QueryFilter[] = [{ field: 'foo', operator: 'eq', value: '{"foo":true}' }];
const test = qp.parseQuery(query);
expect(test.filter[0]).toMatchObject(expected[0]);
});
it('should set array, 6', () => {
const query = { filter: ['foo||eq||1'] };
const expected: QueryFilter[] = [{ field: 'foo', operator: 'eq', value: 1 }];
const test = qp.parseQuery(query);
expect(test.filter[0]).toMatchObject(expected[0]);
});
it('should set date, 7', () => {
const now = new Date();
const query = { filter: [`foo||eq||${now.toJSON()}`] };
const expected: QueryFilter[] = [{ field: 'foo', operator: 'eq', value: now }];
const test = qp.parseQuery(query);
expect(test.filter[0]).toMatchObject(expected[0]);
});
it('should set false, 8', () => {
const query = { filter: ['foo||eq||false'] };
const expected: QueryFilter[] = [{ field: 'foo', operator: 'eq', value: false }];
const test = qp.parseQuery(query);
expect(test.filter[0]).toMatchObject(expected[0]);
});
it('should set true, 9', () => {
const query = { filter: ['foo||eq||true'] };
const expected: QueryFilter[] = [{ field: 'foo', operator: 'eq', value: true }];
const test = qp.parseQuery(query);
expect(test.filter[0]).toMatchObject(expected[0]);
});
it('should set number, 10', () => {
const query = { filter: ['foo||eq||12345'] };
const expected: QueryFilter[] = [{ field: 'foo', operator: 'eq', value: 12345 }];
const test = qp.parseQuery(query);
expect(test.filter[0]).toMatchObject(expected[0]);
});
it('should set string, 11', () => {
const query = { filter: ['foo||eq||4202140192612927005304000000236630'] };
const expected: QueryFilter[] = [
{ field: 'foo', operator: 'eq', value: '4202140192612927005304000000236630' },
];
const test = qp.parseQuery(query);
expect(test.filter[0]).toMatchObject(expected[0]);
});
});
describe('#parse or', () => {
it('should set empty array, 1', () => {
const query = { or: '' };
const expected = [];
const test = qp.parseQuery(query);
expect(test.or).toMatchObject(expected);
});
it('should set empty array, 2', () => {
const query = { foo: '' };
const expected = [];
const test = qp.parseQuery(query);
expect(test.or).toMatchObject(expected);
});
it('should throw an error, 1', () => {
const query = { or: 'foo||invalid||bar' };
expect(qp.parseQuery.bind(qp, query)).toThrowError(RequestQueryException);
});
it('should throw an error, 2', () => {
const query = { or: 'foo||eq' };
expect(qp.parseQuery.bind(qp, query)).toThrowError(RequestQueryException);
});
it('should set array, 1', () => {
const query = { or: 'foo||eq||bar' };
const expected: QueryFilter[] = [{ field: 'foo', operator: 'eq', value: 'bar' }];
const test = qp.parseQuery(query);
expect(test.or[0]).toMatchObject(expected[0]);
});
it('should set array, 2', () => {
const query = { or: ['foo||eq||bar', 'baz||ne||boo'] };
const expected: QueryFilter[] = [
{ field: 'foo', operator: 'eq', value: 'bar' },
{ field: 'baz', operator: 'ne', value: 'boo' },
];
const test = qp.parseQuery(query);
expect(test.or[0]).toMatchObject(expected[0]);
expect(test.or[1]).toMatchObject(expected[1]);
});
it('should set array, 3', () => {
const query = { or: ['foo||in||1,2'] };
const expected: QueryFilter[] = [{ field: 'foo', operator: 'in', value: [1, 2] }];
const test = qp.parseQuery(query);
expect(test.or[0]).toMatchObject(expected[0]);
});
it('should set array, 4', () => {
const query = { or: ['foo||isnull'] };
const expected: QueryFilter[] = [{ field: 'foo', operator: 'isnull', value: '' }];
const test = qp.parseQuery(query);
expect(test.or[0]).toMatchObject(expected[0]);
});
});
describe('#parse join', () => {
it('should set empty array, 1', () => {
const query = { join: '' };
const expected = [];
const test = qp.parseQuery(query);
expect(test.join).toMatchObject(expected);
});
it('should set empty array, 2', () => {
const query = { foo: '' };
const expected = [];
const test = qp.parseQuery(query);
expect(test.join).toMatchObject(expected);
});
it('should set array, 1', () => {
const query = { join: 'foo' };
const expected: QueryJoin[] = [{ field: 'foo' }];
const test = qp.parseQuery(query);
expect(test.join[0]).toMatchObject(expected[0]);
});
it('should set array, 2', () => {
const query = { join: ['foo', 'bar||baz,boo'] };
const expected: QueryJoin[] = [{ field: 'foo' }, { field: 'bar', select: ['baz', 'boo'] }];
const test = qp.parseQuery(query);
expect(test.join[0]).toMatchObject(expected[0]);
expect(test.join[1]).toMatchObject(expected[1]);
});
});
describe('#parse sort', () => {
it('should set empty array, 1', () => {
const query = { sort: '' };
const expected = [];
const test = qp.parseQuery(query);
expect(test.sort).toMatchObject(expected);
});
it('should set empty array, 2', () => {
const query = { foo: '' };
const expected = [];
const test = qp.parseQuery(query);
expect(test.sort).toMatchObject(expected);
});
it('should throw an error, 1', () => {
const query = { sort: 'foo' };
expect(qp.parseQuery.bind(qp, query)).toThrowError(RequestQueryException);
});
it('should throw an error, 2', () => {
const query = { sort: 'foo,boo' };
expect(qp.parseQuery.bind(qp, query)).toThrowError(RequestQueryException);
});
it('should set array', () => {
const query = { sort: ['foo,ASC', 'bar,DESC'] };
const expected: QuerySort[] = [
{ field: 'foo', order: 'ASC' },
{ field: 'bar', order: 'DESC' },
];
const test = qp.parseQuery(query);
expect(test.sort[0]).toMatchObject(expected[0]);
expect(test.sort[1]).toMatchObject(expected[1]);
});
});
describe('#parse limit', () => {
it('should set undefined, 1', () => {
const query = { limit: '' };
const test = qp.parseQuery(query);
expect(test.limit).toBeUndefined();
});
it('should set undefined, 2', () => {
const query = { foo: '' };
const test = qp.parseQuery(query);
expect(test.limit).toBeUndefined();
});
it('should throw an error', () => {
const query = { limit: 'a' };
expect(qp.parseQuery.bind(qp, query)).toThrowError(RequestQueryException);
});
it('should set value', () => {
const query = { limit: '10' };
const expected = 10;
const test = qp.parseQuery(query);
expect(test.limit).toBe(expected);
});
});
describe('#parse offset', () => {
it('should set undefined, 1', () => {
const query = { offset: '' };
const test = qp.parseQuery(query);
expect(test.offset).toBeUndefined();
});
it('should set undefined, 2', () => {
const query = { foo: '' };
const test = qp.parseQuery(query);
expect(test.offset).toBeUndefined();
});
it('should throw an error', () => {
const query = { offset: 'a' };
expect(qp.parseQuery.bind(qp, query)).toThrowError(RequestQueryException);
});
it('should set value', () => {
const query = { offset: '10' };
const expected = 10;
const test = qp.parseQuery(query);
expect(test.offset).toBe(expected);
});
});
describe('#parse page', () => {
it('should set undefined, 1', () => {
const query = { page: '' };
const test = qp.parseQuery(query);
expect(test.page).toBeUndefined();
});
it('should set undefined, 2', () => {
const query = { foo: '' };
const test = qp.parseQuery(query);
expect(test.page).toBeUndefined();
});
it('should throw an error', () => {
const query = { page: ['a'] };
expect(qp.parseQuery.bind(qp, query)).toThrowError(RequestQueryException);
});
it('should set value', () => {
const query = { page: ['10'] };
const expected = 10;
const test = qp.parseQuery(query);
expect(test.page).toBe(expected);
});
});
describe('#parse cache', () => {
it('should set undefined, 1', () => {
const query = { cache: '' };
const test = qp.parseQuery(query);
expect(test.cache).toBeUndefined();
});
it('should set undefined, 2', () => {
const query = { foo: '' };
const test = qp.parseQuery(query);
expect(test.cache).toBeUndefined();
});
it('should throw an error', () => {
const query = { cache: ['a'] };
expect(qp.parseQuery.bind(qp, query)).toThrowError(RequestQueryException);
});
it('should set value', () => {
const query = { cache: ['10'] };
const expected = 10;
const test = qp.parseQuery(query);
expect(test.cache).toBe(expected);
});
});
});
describe('#parse search', () => {
it('should set undefined', () => {
const query = { foo: '' };
const test = qp.parseQuery(query);
expect(test.search).toBeUndefined();
});
it('should throw an error, 1', () => {
const query = { s: 'invalid' };
expect(qp.parseQuery.bind(qp, query)).toThrowError(RequestQueryException);
});
it('should throw an error, 2', () => {
const query = { s: 'true' };
expect(qp.parseQuery.bind(qp, query)).toThrowError(RequestQueryException);
});
it('should parse search', () => {
const query = { s: '{"$or":[{"id":1},{"name":"foo"}]}' };
const expected = { $or: [{ id: 1 }, { name: 'foo' }] };
const test = qp.parseQuery(query);
expect(test.search).toMatchObject(expected);
});
});
describe('#parseParams', () => {
it('should return instance of RequestQueryParse', () => {
expect((qp as any).parseParams()).toBeInstanceOf(RequestQueryParser);
expect((qp as any).parseParams({})).toBeInstanceOf(RequestQueryParser);
});
it('should throw an error, 1', () => {
const params = { foo: 'bar' };
const options = undefined;
expect(qp.parseParams.bind(qp, params, options)).toThrowError(RequestQueryException);
});
it('should throw an error, 2', () => {
const params = { foo: 'bar' };
const options = {};
expect(qp.parseParams.bind(qp, params, options)).toThrowError(RequestQueryException);
});
it('should throw an error, 3', () => {
const params = { foo: 'bar' };
const options = { foo: {} };
expect(qp.parseParams.bind(qp, params, options)).toThrowError(RequestQueryException);
});
it('should throw an error, 4', () => {
const params = { foo: 'bar' };
const options = { foo: { field: 'number' } };
expect(qp.parseParams.bind(qp, params, options)).toThrowError(RequestQueryException);
});
it('should throw an error, 5', () => {
const params = { foo: 'bar' };
const options = { foo: { field: 'foo', type: 'number' } };
expect(qp.parseParams.bind(qp, params, options)).toThrowError(RequestQueryException);
});
it('should throw an error, 6', () => {
const params = { foo: 'bar' };
const options = { foo: { field: 'foo', type: 'uuid' } };
expect(qp.parseParams.bind(qp, params, options)).toThrowError(RequestQueryException);
});
it('should set paramsFilter', () => {
const params = {
foo: 'cb1751fd-7fcf-4eb5-b38e-86428b1fd88d',
bar: '1',
buz: 'string',
bigInt: '9007199254740999', // Bigger than Number.MAX_SAFE_INTEGER
};
const options: ParamsOptions = {
foo: { field: 'foo', type: 'uuid' },
bar: { field: 'bb', type: 'number' },
buz: { field: 'buz', type: 'string' },
bigInt: { field: 'bigInt', type: 'string' },
};
const test = qp.parseParams(params, options);
const expected = [
{
field: 'foo',
operator: '$eq',
value: 'cb1751fd-7fcf-4eb5-b38e-86428b1fd88d',
},
{ field: 'bb', operator: '$eq', value: 1 },
{ field: 'buz', operator: '$eq', value: 'string' },
{ field: 'bigInt', operator: '$eq', value: '9007199254740999' },
];
expect(test.paramsFilter).toMatchObject(expected);
});
it('should set paramsFilter with disabled validation', () => {
const params = {
foo: 'cb1751fd',
bar: '123',
};
const options: ParamsOptions = {
foo: { disabled: true },
bar: { field: 'bar', type: 'number' },
};
const test = qp.parseParams(params, options);
const expected = [{ field: 'bar', operator: '$eq', value: 123 }];
expect(test.paramsFilter).toMatchObject(expected);
});
});
describe('#setAuthPersist', () => {
it('it should set authPersist, 1', () => {
qp.setAuthPersist();
expect(qp.authPersist).toMatchObject({});
});
it('it should set authPersist, 2', () => {
const test = { foo: 'bar' };
qp.setAuthPersist(test);
const parsed = qp.getParsed();
expect(parsed.authPersist).toMatchObject(test);
});
});
describe('#setClassTransformOptions', () => {
it('it should set classTransformOptions, 1', () => {
qp.setClassTransformOptions();
expect(qp.classTransformOptions).toMatchObject({});
});
it('it should set classTransformOptions, 2', () => {
const testOptions = { groups: ['TEST'] };
qp.setClassTransformOptions(testOptions);
const parsed = qp.getParsed();
expect(parsed.classTransformOptions).toMatchObject(testOptions);
});
});
describe('#getParsed', () => {
it('should return parsed params', () => {
const expected: ParsedRequestParams = {
fields: [],
paramsFilter: [],
search: undefined,
authPersist: undefined,
classTransformOptions: undefined,
filter: [],
or: [],
join: [],
sort: [],
limit: undefined,
offset: undefined,
page: undefined,
cache: undefined,
includeDeleted: undefined,
};
const test = qp.getParsed();
expect(test).toMatchObject(expected);
});
});
});
});
================================================
FILE: packages/crud-request/test/request.query.validator.spec.ts
================================================
import { validateUUID } from '../src/request-query.validator';
describe('#request-query', () => {
describe('#validator', () => {
describe('#validateUUID', () => {
const uuid = 'cf0917fc-af7d-11e9-a2a3-2a2ae2dbcce4';
const uuidV4 = '6650aad9-29bd-4601-b9b1-543a7a2d2d54';
const invalid = 'invalid-uuid';
it('should throw an error', () => {
expect(validateUUID.bind(validateUUID, invalid)).toThrow();
});
it('should pass, 1', () => {
expect(validateUUID(uuid, '')).toBeUndefined();
});
it('should pass, 2', () => {
expect(validateUUID(uuidV4, '')).toBeUndefined();
});
});
});
});
================================================
FILE: packages/crud-request/tsconfig.json
================================================
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib"
},
"include": ["src"],
"exclude": ["lib"],
"references": [{ "path": "../util" }]
}
================================================
FILE: packages/crud-typeorm/README.md
================================================
CRUD (@nestjsx/crud-typeorm)
for RESTful APIs built with NestJs
We believe that everyone who's working with NestJs and building some RESTful services and especially some CRUD functionality will find `@nestjsx/crud` microframework very useful.
## Features
- Super easy to install and start using the full-featured controllers and services :point_right:
- DB and service agnostic extendable CRUD controllers
- Reach query parsing with filtering, pagination, sorting, relations, nested relations, cache, etc.
- Framework agnostic package with query builder for a frontend usage
- Query, path params and DTOs validation included
- Overriding controller methods with ease
- Tiny config (including globally)
- Additional helper decorators
- Swagger documentation
## Install
```shell
npm i @nestjsx/crud-typeorm @nestjs/typeorm typeorm
```
## Packages
- [**@nestjsx/crud**](https://www.npmjs.com/package/@nestjsx/crud) - core package which provides `@Crud()` decorator for endpoints generation, global configuration, validation, helper decorators ([docs](https://github.com/nestjsx/crud/wiki/Controllers#description))
- [**@nestjsx/crud-request**](https://www.npmjs.com/package/@nestjsx/crud-request) - request builder/parser package which provides `RequestQueryBuilder` class for a frontend usage and `RequestQueryParser` that is being used internally for handling and validating query/path params on a backend side ([docs](https://github.com/nestjsx/crud/wiki/Requests#frontend-usage))
- [**@nestjsx/crud-typeorm**](https://www.npmjs.com/package/@nestjsx/crud-typeorm) - TypeORM package which provides base `TypeOrmCrudService` with methods for CRUD database operations ([docs](https://github.com/nestjsx/crud/wiki/ServiceTypeorm))
## Documentation
- [General Information](https://github.com/nestjsx/crud/wiki#why)
- [CRUD Controllers](https://github.com/nestjsx/crud/wiki/Controllers#description)
- [CRUD ORM Services](https://github.com/nestjsx/crud/wiki/Services#description)
- [Handling Requests](https://github.com/nestjsx/crud/wiki/Requests#description)
## Support
Any support is welcome. At least you can give us a star.
## Contributors
### Code Contributors
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
### Financial Contributors
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/nestjsx#backer)]
#### Individuals
#### Organizations
Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/nestjsx#sponsor)]
## License
[MIT](LICENSE)
================================================
FILE: packages/crud-typeorm/package.json
================================================
{
"name": "@nestjsx/crud-typeorm",
"description": "NestJs CRUD for RESTful APIs - TypeORM",
"version": "5.0.0-alpha.3",
"license": "MIT",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
"publishConfig": {
"access": "public"
},
"files": [
"lib"
],
"repository": {
"type": "git",
"url": "https://github.com/nestjsx/crud.git"
},
"bugs": {
"url": "https://github.com/nestjsx/crud/issues"
},
"keywords": [
"typescript",
"typeorm",
"nest",
"nestjs",
"rest",
"restful",
"api",
"crud",
"crud-generator",
"backend",
"frameworks"
],
"author": {
"name": "Michael Yali",
"email": "mihon4ik@gmail.com"
},
"scripts": {
"build": "npx tsc -b"
},
"dependencies": {
"@zmotivat0r/o0": "^1.0.2"
}
}
================================================
FILE: packages/crud-typeorm/src/index.ts
================================================
export * from './typeorm-crud.service';
================================================
FILE: packages/crud-typeorm/src/typeorm-crud.service.ts
================================================
import {
CreateManyDto,
CrudRequest,
CrudRequestOptions,
CrudService,
GetManyDefaultResponse,
JoinOption,
JoinOptions,
QueryOptions,
} from '@nestjsx/crud';
import {
ParsedRequestParams,
QueryFilter,
QueryJoin,
QuerySort,
SCondition,
SConditionKey,
ComparisonOperator,
} from '@nestjsx/crud-request';
import { ClassType, hasLength, isArrayFull, isObject, isUndefined, objKeys, isNil, isNull } from '@nestjsx/util';
import { oO } from '@zmotivat0r/o0';
import { plainToClass } from 'class-transformer';
import {
Brackets,
DeepPartial,
ObjectLiteral,
Repository,
SelectQueryBuilder,
DataSourceOptions,
EntityMetadata,
} from 'typeorm';
interface IAllowedRelation {
alias?: string;
nested: boolean;
name: string;
path: string;
columns: string[];
primaryColumns: string[];
allowedColumns: string[];
}
export class TypeOrmCrudService extends CrudService {
protected dbName: DataSourceOptions['type'];
protected entityColumns: string[];
protected entityPrimaryColumns: string[];
protected entityHasDeleteColumn = false;
protected entityColumnsHash: ObjectLiteral = {};
protected entityRelationsHash: Map = new Map();
protected sqlInjectionRegEx: RegExp[] = [
/(%27)|(\')|(--)|(%23)|(#)/gi,
/((%3D)|(=))[^\n]*((%27)|(\')|(--)|(%3B)|(;))/gi,
/w*((%27)|(\'))((%6F)|o|(%4F))((%72)|r|(%52))/gi,
/((%27)|(\'))union/gi,
];
constructor(protected repo: Repository) {
super();
this.dbName = this.repo.metadata.connection.options.type;
this.onInitMapEntityColumns();
}
public get findOne(): Repository['findOne'] {
return this.repo.findOne.bind(this.repo);
}
public get find(): Repository['find'] {
return this.repo.find.bind(this.repo);
}
public get count(): Repository['count'] {
return this.repo.count.bind(this.repo);
}
protected get entityType(): ClassType {
return this.repo.target as ClassType;
}
protected get alias(): string {
return this.repo.metadata.targetName;
}
/**
* Get many
* @param req
*/
public async getMany(req: CrudRequest): Promise | T[]> {
const { parsed, options } = req;
const builder = await this.createBuilder(parsed, options);
return this.doGetMany(builder, parsed, options);
}
/**
* Get one
* @param req
*/
public async getOne(req: CrudRequest): Promise {
return this.getOneOrFail(req);
}
/**
* Create one
* @param req
* @param dto
*/
public async createOne(req: CrudRequest, dto: T | Partial): Promise {
const { returnShallow } = req.options.routes.createOneBase;
const entity = this.prepareEntityBeforeSave(dto, req.parsed);
/* istanbul ignore if */
if (!entity) {
this.throwBadRequestException('Empty data. Nothing to save.');
}
const saved = await this.repo.save(entity);
if (returnShallow) {
return saved;
} else {
const primaryParams = this.getPrimaryParams(req.options);
/* istanbul ignore next */
if (!primaryParams.length && primaryParams.some((p) => isNil(saved[p]))) {
return saved;
} else {
req.parsed.search = primaryParams.reduce((acc, p) => ({ ...acc, [p]: saved[p] }), {});
return this.getOneOrFail(req);
}
}
}
/**
* Create many
* @param req
* @param dto
*/
public async createMany(req: CrudRequest, dto: CreateManyDto>): Promise {
/* istanbul ignore if */
if (!isObject(dto) || !isArrayFull(dto.bulk)) {
this.throwBadRequestException('Empty data. Nothing to save.');
}
const bulk = dto.bulk.map((one) => this.prepareEntityBeforeSave(one, req.parsed)).filter((d) => !isUndefined(d));
/* istanbul ignore if */
if (!hasLength(bulk)) {
this.throwBadRequestException('Empty data. Nothing to save.');
}
return this.repo.save(bulk, { chunk: 50 });
}
/**
* Update one
* @param req
* @param dto
*/
public async updateOne(req: CrudRequest, dto: T | Partial): Promise {
const { allowParamsOverride, returnShallow } = req.options.routes.updateOneBase;
const paramsFilters = this.getParamFilters(req.parsed);
const found = await this.getOneOrFail(req, returnShallow);
const toSave = !allowParamsOverride
? { ...found, ...dto, ...paramsFilters, ...req.parsed.authPersist }
: { ...found, ...dto, ...req.parsed.authPersist };
const updated = await this.repo.save(
plainToClass(this.entityType, toSave, req.parsed.classTransformOptions) as unknown as DeepPartial,
);
if (returnShallow) {
return updated;
} else {
req.parsed.paramsFilter.forEach((filter) => {
filter.value = updated[filter.field];
});
return this.getOneOrFail(req);
}
}
/**
* Recover one
* @param req
* @param dto
*/
public async recoverOne(req: CrudRequest): Promise {
const found = await this.getOneOrFail(req, false, true);
return this.repo.recover(found as unknown as DeepPartial);
}
/**
* Replace one
* @param req
* @param dto
*/
public async replaceOne(req: CrudRequest, dto: T | Partial): Promise {
const { allowParamsOverride, returnShallow } = req.options.routes.replaceOneBase;
const paramsFilters = this.getParamFilters(req.parsed);
const [_, found] = await oO(this.getOneOrFail(req, returnShallow));
const toSave = !allowParamsOverride
? { ...(found || {}), ...dto, ...paramsFilters, ...req.parsed.authPersist }
: {
...(found || /* istanbul ignore next */ {}),
...paramsFilters,
...dto,
...req.parsed.authPersist,
};
const replaced = await this.repo.save(
plainToClass(this.entityType, toSave, req.parsed.classTransformOptions) as unknown as DeepPartial,
);
if (returnShallow) {
return replaced;
} else {
const primaryParams = this.getPrimaryParams(req.options);
/* istanbul ignore if */
if (!primaryParams.length) {
return replaced;
}
req.parsed.search = primaryParams.reduce((acc, p) => ({ ...acc, [p]: replaced[p] }), {});
return this.getOneOrFail(req);
}
}
/**
* Delete one
* @param req
*/
public async deleteOne(req: CrudRequest): Promise {
const { returnDeleted } = req.options.routes.deleteOneBase;
const found = await this.getOneOrFail(req, returnDeleted);
const toReturn = returnDeleted
? plainToClass(this.entityType, { ...found }, req.parsed.classTransformOptions)
: undefined;
const deleted =
req.options.query.softDelete === true
? await this.repo.softRemove(found as unknown as DeepPartial)
: await this.repo.remove(found);
return toReturn;
}
public getParamFilters(parsed: CrudRequest['parsed']): ObjectLiteral {
const filters = {};
/* istanbul ignore else */
if (hasLength(parsed.paramsFilter)) {
for (const filter of parsed.paramsFilter) {
filters[filter.field] = filter.value;
}
}
return filters;
}
/**
* Create TypeOrm QueryBuilder
* @param parsed
* @param options
* @param many
*/
public async createBuilder(
parsed: ParsedRequestParams,
options: CrudRequestOptions,
many = true,
withDeleted = false,
): Promise> {
// create query builder
const builder = this.repo.createQueryBuilder(this.alias);
// get select fields
const select = this.getSelect(parsed, options.query);
// select fields
builder.select(select);
// search
this.setSearchCondition(builder, parsed.search);
// set joins
const joinOptions = options.query.join || {};
const allowedJoins = objKeys(joinOptions);
if (hasLength(allowedJoins)) {
const eagerJoins: any = {};
for (let i = 0; i < allowedJoins.length; i++) {
/* istanbul ignore else */
if (joinOptions[allowedJoins[i]].eager) {
const cond = parsed.join.find((j) => j && j.field === allowedJoins[i]) || {
field: allowedJoins[i],
};
this.setJoin(cond, joinOptions, builder);
eagerJoins[allowedJoins[i]] = true;
}
}
if (isArrayFull(parsed.join)) {
for (let i = 0; i < parsed.join.length; i++) {
/* istanbul ignore else */
if (!eagerJoins[parsed.join[i].field]) {
this.setJoin(parsed.join[i], joinOptions, builder);
}
}
}
}
// if soft deleted is enabled add where statement to filter deleted records
if (this.entityHasDeleteColumn && options.query.softDelete) {
if (parsed.includeDeleted === 1 || withDeleted) {
builder.withDeleted();
}
}
/* istanbul ignore else */
if (many) {
// set sort (order by)
const sort = this.getSort(parsed, options.query);
builder.orderBy(sort);
// set take
const take = this.getTake(parsed, options.query);
/* istanbul ignore else */
if (isFinite(take)) {
builder.take(take);
}
// set skip
const skip = this.getSkip(parsed, take);
/* istanbul ignore else */
if (isFinite(skip)) {
builder.skip(skip);
}
}
// set cache
/* istanbul ignore else */
if (options.query.cache && parsed.cache !== 0) {
builder.cache(builder.getQueryAndParameters(), options.query.cache);
}
return builder;
}
/**
* depends on paging call `SelectQueryBuilder#getMany` or `SelectQueryBuilder#getManyAndCount`
* helpful for overriding `TypeOrmCrudService#getMany`
* @see getMany
* @see SelectQueryBuilder#getMany
* @see SelectQueryBuilder#getManyAndCount
* @param builder
* @param query
* @param options
*/
protected async doGetMany(
builder: SelectQueryBuilder,
query: ParsedRequestParams,
options: CrudRequestOptions,
): Promise | T[]> {
if (this.decidePagination(query, options)) {
const [data, total] = await builder.getManyAndCount();
const limit = builder.expressionMap.take;
const offset = builder.expressionMap.skip;
return this.createPageInfo(data, total, limit || total, offset || 0);
}
return builder.getMany();
}
protected onInitMapEntityColumns() {
this.entityColumns = this.repo.metadata.columns.map((prop) => {
// In case column is an embedded, use the propertyPath to get complete path
if (prop.embeddedMetadata) {
this.entityColumnsHash[prop.propertyPath] = prop.databasePath;
return prop.propertyPath;
}
this.entityColumnsHash[prop.propertyName] = prop.databasePath;
return prop.propertyName;
});
this.entityPrimaryColumns = this.repo.metadata.columns
.filter((prop) => prop.isPrimary)
.map((prop) => prop.propertyName);
this.entityHasDeleteColumn = this.repo.metadata.columns.filter((prop) => prop.isDeleteDate).length > 0;
}
protected async getOneOrFail(req: CrudRequest, shallow = false, withDeleted = false): Promise {
const { parsed, options } = req;
const builder = shallow
? this.repo.createQueryBuilder(this.alias)
: await this.createBuilder(parsed, options, true, withDeleted);
if (shallow) {
this.setSearchCondition(builder, parsed.search);
}
const found = withDeleted ? await builder.withDeleted().getOne() : await builder.getOne();
if (!found) {
this.throwNotFoundException(this.alias);
}
return found;
}
protected prepareEntityBeforeSave(dto: T | Partial, parsed: CrudRequest['parsed']): T {
/* istanbul ignore if */
if (!isObject(dto)) {
return undefined;
}
if (hasLength(parsed.paramsFilter)) {
for (const filter of parsed.paramsFilter) {
dto[filter.field] = filter.value;
}
}
/* istanbul ignore if */
if (!hasLength(objKeys(dto))) {
return undefined;
}
return dto instanceof this.entityType
? Object.assign(dto, parsed.authPersist)
: plainToClass(this.entityType, { ...dto, ...parsed.authPersist }, parsed.classTransformOptions);
}
protected getAllowedColumns(columns: string[], options: QueryOptions): string[] {
return (!options.exclude || !options.exclude.length) &&
(!options.allow || /* istanbul ignore next */ !options.allow.length)
? columns
: columns.filter(
(column) =>
(options.exclude && options.exclude.length
? !options.exclude.some((col) => col === column)
: /* istanbul ignore next */ true) &&
(options.allow && options.allow.length
? options.allow.some((col) => col === column)
: /* istanbul ignore next */ true),
);
}
protected getEntityColumns(entityMetadata: EntityMetadata): { columns: string[]; primaryColumns: string[] } {
const columns = entityMetadata.columns.map((prop) => prop.propertyPath) || /* istanbul ignore next */ [];
const primaryColumns =
entityMetadata.primaryColumns.map((prop) => prop.propertyPath) || /* istanbul ignore next */ [];
return { columns, primaryColumns };
}
protected getRelationMetadata(field: string, options: JoinOption): IAllowedRelation {
try {
let allowedRelation;
let nested = false;
if (this.entityRelationsHash.has(field)) {
allowedRelation = this.entityRelationsHash.get(field);
} else {
const fields = field.split('.');
let relationMetadata: EntityMetadata;
let name: string;
let path: string;
let parentPath: string;
if (fields.length === 1) {
const found = this.repo.metadata.relations.find((one) => one.propertyName === fields[0]);
if (found) {
name = fields[0];
path = `${this.alias}.${fields[0]}`;
relationMetadata = found.inverseEntityMetadata;
}
} else {
nested = true;
parentPath = '';
const reduced = fields.reduce(
(res, propertyName: string, i) => {
const found = res.relations.length
? res.relations.find((one) => one.propertyName === propertyName)
: null;
const relationMetadata = found ? found.inverseEntityMetadata : null;
const relations = relationMetadata ? relationMetadata.relations : [];
name = propertyName;
if (i !== fields.length - 1) {
parentPath = !parentPath ? propertyName : /* istanbul ignore next */ `${parentPath}.${propertyName}`;
}
return {
relations,
relationMetadata,
};
},
{
relations: this.repo.metadata.relations,
relationMetadata: null,
},
);
relationMetadata = reduced.relationMetadata;
}
if (relationMetadata) {
const { columns, primaryColumns } = this.getEntityColumns(relationMetadata);
if (!path && parentPath) {
const parentAllowedRelation = this.entityRelationsHash.get(parentPath);
/* istanbul ignore next */
if (parentAllowedRelation) {
path = parentAllowedRelation.alias ? `${parentAllowedRelation.alias}.${name}` : field;
}
}
allowedRelation = {
alias: options.alias,
name,
path,
columns,
nested,
primaryColumns,
};
}
}
if (allowedRelation) {
const allowedColumns = this.getAllowedColumns(allowedRelation.columns, options);
const toSave: IAllowedRelation = { ...allowedRelation, allowedColumns };
this.entityRelationsHash.set(field, toSave);
if (options.alias) {
this.entityRelationsHash.set(options.alias, toSave);
}
return toSave;
}
} catch (_) {
/* istanbul ignore next */
return null;
}
}
protected setJoin(cond: QueryJoin, joinOptions: JoinOptions, builder: SelectQueryBuilder) {
const options = joinOptions[cond.field];
if (!options) {
return true;
}
const allowedRelation = this.getRelationMetadata(cond.field, options);
if (!allowedRelation) {
return true;
}
const relationType = options.required ? 'innerJoin' : 'leftJoin';
const alias = options.alias ? options.alias : allowedRelation.name;
builder[relationType](allowedRelation.path, alias);
if (options.select !== false) {
const columns = isArrayFull(cond.select)
? cond.select.filter((column) => allowedRelation.allowedColumns.some((allowed) => allowed === column))
: allowedRelation.allowedColumns;
const select = new Set(
[...allowedRelation.primaryColumns, ...(isArrayFull(options.persist) ? options.persist : []), ...columns].map(
(col) => `${alias}.${col}`,
),
);
builder.addSelect(Array.from(select));
}
}
protected setAndWhere(cond: QueryFilter, i: any, builder: SelectQueryBuilder) {
const { str, params } = this.mapOperatorsToQuery(cond, `andWhere${i}`);
builder.andWhere(str, params);
}
protected setOrWhere(cond: QueryFilter, i: any, builder: SelectQueryBuilder) {
const { str, params } = this.mapOperatorsToQuery(cond, `orWhere${i}`);
builder.orWhere(str, params);
}
protected setSearchCondition(builder: SelectQueryBuilder, search: SCondition, condition: SConditionKey = '$and') {
/* istanbul ignore else */
if (isObject(search)) {
const keys = objKeys(search);
/* istanbul ignore else */
if (keys.length) {
// search: {$and: [...], ...}
if (isArrayFull(search.$and)) {
// search: {$and: [{}]}
if (search.$and.length === 1) {
this.setSearchCondition(builder, search.$and[0], condition);
}
// search: {$and: [{}, {}, ...]}
else {
this.builderAddBrackets(
builder,
condition,
new Brackets((qb: any) => {
search.$and.forEach((item: any) => {
this.setSearchCondition(qb, item, '$and');
});
}),
);
}
}
// search: {$or: [...], ...}
else if (isArrayFull(search.$or)) {
// search: {$or: [...]}
if (keys.length === 1) {
// search: {$or: [{}]}
if (search.$or.length === 1) {
this.setSearchCondition(builder, search.$or[0], condition);
}
// search: {$or: [{}, {}, ...]}
else {
this.builderAddBrackets(
builder,
condition,
new Brackets((qb: any) => {
search.$or.forEach((item: any) => {
this.setSearchCondition(qb, item, '$or');
});
}),
);
}
}
// search: {$or: [...], foo, ...}
else {
this.builderAddBrackets(
builder,
condition,
new Brackets((qb: any) => {
keys.forEach((field: string) => {
if (field !== '$or') {
const value = search[field];
if (!isObject(value)) {
this.builderSetWhere(qb, '$and', field, value);
} else {
this.setSearchFieldObjectCondition(qb, '$and', field, value);
}
} else {
if (search.$or.length === 1) {
this.setSearchCondition(builder, search.$or[0], '$and');
} else {
this.builderAddBrackets(
qb,
'$and',
new Brackets((qb2: any) => {
search.$or.forEach((item: any) => {
this.setSearchCondition(qb2, item, '$or');
});
}),
);
}
}
});
}),
);
}
}
// search: {...}
else {
// search: {foo}
if (keys.length === 1) {
const field = keys[0];
const value = search[field];
if (!isObject(value)) {
this.builderSetWhere(builder, condition, field, value);
} else {
this.setSearchFieldObjectCondition(builder, condition, field, value);
}
}
// search: {foo, ...}
else {
this.builderAddBrackets(
builder,
condition,
new Brackets((qb: any) => {
keys.forEach((field: string) => {
const value = search[field];
if (!isObject(value)) {
this.builderSetWhere(qb, '$and', field, value);
} else {
this.setSearchFieldObjectCondition(qb, '$and', field, value);
}
});
}),
);
}
}
}
}
}
protected builderAddBrackets(builder: SelectQueryBuilder, condition: SConditionKey, brackets: Brackets) {
if (condition === '$and') {
builder.andWhere(brackets);
} else {
builder.orWhere(brackets);
}
}
protected builderSetWhere(
builder: SelectQueryBuilder,
condition: SConditionKey,
field: string,
value: any,
operator: ComparisonOperator = '$eq',
) {
const time = process.hrtime();
const index = `${field}${time[0]}${time[1]}`;
const args = [{ field, operator: isNull(value) ? '$isnull' : operator, value }, index, builder];
const fn = condition === '$and' ? this.setAndWhere : this.setOrWhere;
fn.apply(this, args);
}
protected setSearchFieldObjectCondition(
builder: SelectQueryBuilder,
condition: SConditionKey,
field: string,
object: any,
) {
/* istanbul ignore else */
if (isObject(object)) {
const operators = objKeys(object);
if (operators.length === 1) {
const operator = operators[0] as ComparisonOperator;
const value = object[operator];
if (isObject(object.$or)) {
const orKeys = objKeys(object.$or);
this.setSearchFieldObjectCondition(builder, orKeys.length === 1 ? condition : '$or', field, object.$or);
} else {
this.builderSetWhere(builder, condition, field, value, operator);
}
} else {
/* istanbul ignore else */
if (operators.length > 1) {
this.builderAddBrackets(
builder,
condition,
new Brackets((qb: any) => {
operators.forEach((operator: ComparisonOperator) => {
const value = object[operator];
if (operator !== '$or') {
this.builderSetWhere(qb, condition, field, value, operator);
} else {
const orKeys = objKeys(object.$or);
if (orKeys.length === 1) {
this.setSearchFieldObjectCondition(qb, condition, field, object.$or);
} else {
this.builderAddBrackets(
qb,
condition,
new Brackets((qb2: any) => {
this.setSearchFieldObjectCondition(qb2, '$or', field, object.$or);
}),
);
}
}
});
}),
);
}
}
}
}
protected getSelect(query: ParsedRequestParams, options: QueryOptions): string[] {
const allowed = this.getAllowedColumns(this.entityColumns, options);
const columns =
query.fields && query.fields.length
? query.fields.filter((field) => allowed.some((col) => field === col))
: allowed;
const select = new Set(
[
...(options.persist && options.persist.length ? options.persist : []),
...columns,
...this.entityPrimaryColumns,
].map((col) => `${this.alias}.${col}`),
);
return Array.from(select);
}
protected getSort(query: ParsedRequestParams, options: QueryOptions) {
return query.sort && query.sort.length
? this.mapSort(query.sort)
: options.sort && options.sort.length
? this.mapSort(options.sort)
: {};
}
protected getFieldWithAlias(field: string, sort = false) {
/* istanbul ignore next */
const i = ['mysql','mariadb'].includes(this.dbName) ? '`' : '"';
const cols = field.split('.');
switch (cols.length) {
case 1:
if (sort) {
return `${this.alias}.${field}`;
}
const dbColName = this.entityColumnsHash[field] !== field ? this.entityColumnsHash[field] : field;
return `${i}${this.alias}${i}.${i}${dbColName}${i}`;
case 2:
return field;
default:
return cols.slice(cols.length - 2, cols.length).join('.');
}
}
protected mapSort(sort: QuerySort[]) {
const params: ObjectLiteral = {};
for (let i = 0; i < sort.length; i++) {
const field = this.getFieldWithAlias(sort[i].field, true);
const checkedFiled = this.checkSqlInjection(field);
params[checkedFiled] = sort[i].order;
}
return params;
}
protected mapOperatorsToQuery(cond: QueryFilter, param: any): { str: string; params: ObjectLiteral } {
const field = this.getFieldWithAlias(cond.field);
const likeOperator = this.dbName === 'postgres' ? 'ILIKE' : /* istanbul ignore next */ 'LIKE';
let str: string;
let params: ObjectLiteral;
if (cond.operator[0] !== '$') {
cond.operator = ('$' + cond.operator) as ComparisonOperator;
}
switch (cond.operator) {
case '$eq':
str = `${field} = :${param}`;
break;
case '$ne':
str = `${field} != :${param}`;
break;
case '$gt':
str = `${field} > :${param}`;
break;
case '$lt':
str = `${field} < :${param}`;
break;
case '$gte':
str = `${field} >= :${param}`;
break;
case '$lte':
str = `${field} <= :${param}`;
break;
case '$starts':
str = `${field} LIKE :${param}`;
params = { [param]: `${cond.value}%` };
break;
case '$ends':
str = `${field} LIKE :${param}`;
params = { [param]: `%${cond.value}` };
break;
case '$cont':
str = `${field} LIKE :${param}`;
params = { [param]: `%${cond.value}%` };
break;
case '$excl':
str = `${field} NOT LIKE :${param}`;
params = { [param]: `%${cond.value}%` };
break;
case '$in':
this.checkFilterIsArray(cond);
str = `${field} IN (:...${param})`;
break;
case '$notin':
this.checkFilterIsArray(cond);
str = `${field} NOT IN (:...${param})`;
break;
case '$isnull':
str = `${field} IS NULL`;
params = {};
break;
case '$notnull':
str = `${field} IS NOT NULL`;
params = {};
break;
case '$between':
this.checkFilterIsArray(cond, cond.value.length !== 2);
str = `${field} BETWEEN :${param}0 AND :${param}1`;
params = {
[`${param}0`]: cond.value[0],
[`${param}1`]: cond.value[1],
};
break;
// case insensitive
case '$eqL':
str = `LOWER(${field}) = :${param}`;
break;
case '$neL':
str = `LOWER(${field}) != :${param}`;
break;
case '$startsL':
str = `LOWER(${field}) ${likeOperator} :${param}`;
params = { [param]: `${cond.value}%` };
break;
case '$endsL':
str = `LOWER(${field}) ${likeOperator} :${param}`;
params = { [param]: `%${cond.value}` };
break;
case '$contL':
str = `LOWER(${field}) ${likeOperator} :${param}`;
params = { [param]: `%${cond.value}%` };
break;
case '$exclL':
str = `LOWER(${field}) NOT ${likeOperator} :${param}`;
params = { [param]: `%${cond.value}%` };
break;
case '$inL':
this.checkFilterIsArray(cond);
str = `LOWER(${field}) IN (:...${param})`;
break;
case '$notinL':
this.checkFilterIsArray(cond);
str = `LOWER(${field}) NOT IN (:...${param})`;
break;
/* istanbul ignore next */
default:
str = `${field} = :${param}`;
break;
}
if (typeof params === 'undefined') {
params = { [param]: cond.value };
}
return { str, params };
}
private checkFilterIsArray(cond: QueryFilter, withLength?: boolean) {
/* istanbul ignore if */
if (!Array.isArray(cond.value) || !cond.value.length || (!isNil(withLength) ? withLength : false)) {
this.throwBadRequestException(`Invalid column '${cond.field}' value`);
}
}
private checkSqlInjection(field: string): string {
/* istanbul ignore else */
if (this.sqlInjectionRegEx.length) {
for (let i = 0; i < this.sqlInjectionRegEx.length; i++) {
/* istanbul ignore else */
if (this.sqlInjectionRegEx[0].test(field)) {
this.throwBadRequestException(`SQL injection detected: "${field}"`);
}
}
}
return field;
}
}
================================================
FILE: packages/crud-typeorm/test/__fixture__/companies.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { TypeOrmCrudService } from '../../../crud-typeorm/src/typeorm-crud.service';
import { Company } from '../../../../integration/crud-typeorm/companies';
@Injectable()
export class CompaniesService extends TypeOrmCrudService {
constructor(@InjectRepository(Company) repo) {
super(repo);
}
}
================================================
FILE: packages/crud-typeorm/test/__fixture__/devices.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { TypeOrmCrudService } from '../../../crud-typeorm/src/typeorm-crud.service';
import { Device } from '../../../../integration/crud-typeorm/devices';
@Injectable()
export class DevicesService extends TypeOrmCrudService {
constructor(@InjectRepository(Device) repo) {
super(repo);
}
}
================================================
FILE: packages/crud-typeorm/test/__fixture__/notes.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { TypeOrmCrudService } from '../../../crud-typeorm/src/typeorm-crud.service';
import { Note } from '../../../../integration/crud-typeorm/notes';
@Injectable()
export class NotesService extends TypeOrmCrudService {
constructor(@InjectRepository(Note) repo) {
super(repo);
}
}
================================================
FILE: packages/crud-typeorm/test/__fixture__/projects.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { TypeOrmCrudService } from '../../../crud-typeorm/src/typeorm-crud.service';
import { Project } from '../../../../integration/crud-typeorm/projects';
@Injectable()
export class ProjectsService extends TypeOrmCrudService {
constructor(@InjectRepository(Project) repo) {
super(repo);
}
}
================================================
FILE: packages/crud-typeorm/test/__fixture__/users.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { TypeOrmCrudService } from '../../../crud-typeorm/src/typeorm-crud.service';
import { User } from '../../../../integration/crud-typeorm/users';
@Injectable()
export class UsersService extends TypeOrmCrudService {
constructor(@InjectRepository(User) repo) {
super(repo);
}
}
@Injectable()
export class UsersService2 extends TypeOrmCrudService {
constructor(@InjectRepository(User) repo) {
super(repo);
}
}
================================================
FILE: packages/crud-typeorm/test/a.params-options.spec.ts
================================================
import 'jest-extended';
import { Controller, INestApplication } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { Test } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm';
import * as request from 'supertest';
import { Company } from '../../../integration/crud-typeorm/companies';
import { withCache } from '../../../integration/crud-typeorm/orm.config';
import { Project } from '../../../integration/crud-typeorm/projects';
import { User } from '../../../integration/crud-typeorm/users';
import { UserProfile } from '../../../integration/crud-typeorm/users-profiles';
import { HttpExceptionFilter } from '../../../integration/shared/https-exception.filter';
import { Crud } from '../../crud/src/decorators/crud.decorator';
import { UsersService } from './__fixture__/users.service';
// tslint:disable:max-classes-per-file
describe('#crud-typeorm', () => {
describe('#params options', () => {
let app: INestApplication;
let server: any;
@Crud({
model: { type: User },
params: {
companyId: {
field: 'companyId',
type: 'number',
},
id: {
field: 'id',
type: 'number',
primary: true,
},
},
routes: {
updateOneBase: {
allowParamsOverride: true,
returnShallow: true,
},
replaceOneBase: {
allowParamsOverride: true,
returnShallow: true,
},
},
})
@Controller('/companiesA/:companyId/users')
class UsersController1 {
constructor(public service: UsersService) {}
}
@Crud({
model: { type: User },
params: {
companyId: {
field: 'companyId',
type: 'number',
},
id: {
field: 'id',
type: 'number',
primary: true,
},
},
query: {
join: {
company: {
eager: true,
},
},
},
})
@Controller('/companiesB/:companyId/users')
class UsersController2 {
constructor(public service: UsersService) {}
}
beforeAll(async () => {
const fixture = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot({ ...withCache, logging: false }),
TypeOrmModule.forFeature([Company, Project, User, UserProfile]),
],
controllers: [UsersController1, UsersController2],
providers: [{ provide: APP_FILTER, useClass: HttpExceptionFilter }, UsersService],
}).compile();
app = fixture.createNestApplication();
await app.init();
server = app.getHttpServer();
});
afterAll(async () => {
await app.close();
});
describe('#updateOneBase', () => {
it('should override params', async () => {
const dto = { isActive: false, companyId: 2 };
const res = await request(server)
.patch('/companiesA/1/users/2')
.send(dto)
.expect(200);
expect(res.body.companyId).toBe(2);
});
it('should not override params', async () => {
const dto = { isActive: false, companyId: 2 };
const res = await request(server)
.patch('/companiesB/1/users/3')
.send(dto)
.expect(200);
expect(res.body.companyId).toBe(1);
});
it('should return full entity', async () => {
const dto = { isActive: false };
const res = await request(server)
.patch('/companiesB/2/users/2')
.send(dto)
.expect(200);
expect(res.body.company.id).toBe(2);
});
it('should return shallow entity', async () => {
const dto = { isActive: false };
const res = await request(server)
.patch('/companiesA/2/users/2')
.send(dto)
.expect(200);
expect(res.body.company).toBeUndefined();
});
});
describe('#replaceOneBase', () => {
it('should override params', async () => {
const dto = { isActive: false, companyId: 2, email: '4@email.com' };
const res = await request(server)
.put('/companiesA/1/users/4')
.send(dto)
.expect(200);
expect(res.body.companyId).toBe(2);
});
it('should not override params', async () => {
const dto = { isActive: false, companyId: 1 };
const res = await request(server)
.put('/companiesB/2/users/4')
.send(dto)
.expect(200);
expect(res.body.companyId).toBe(2);
});
it('should return full entity', async () => {
const dto = { isActive: false };
const res = await request(server)
.put('/companiesB/2/users/4')
.send(dto)
.expect(200);
expect(res.body.company.id).toBe(2);
});
it('should return shallow entity', async () => {
const dto = { isActive: false };
const res = await request(server)
.put('/companiesA/2/users/4')
.send(dto)
.expect(200);
expect(res.body.company).toBeUndefined();
});
});
});
});
================================================
FILE: packages/crud-typeorm/test/b.query-params.spec.ts
================================================
import { Controller, INestApplication } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { Test } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm';
import { RequestQueryBuilder } from '@nestjsx/crud-request';
import 'jest-extended';
import * as request from 'supertest';
import { Company } from '../../../integration/crud-typeorm/companies';
import { withCache } from '../../../integration/crud-typeorm/orm.config';
import { Project } from '../../../integration/crud-typeorm/projects';
import { User } from '../../../integration/crud-typeorm/users';
import { UserProfile } from '../../../integration/crud-typeorm/users-profiles';
import { Note } from '../../../integration/crud-typeorm/notes';
import { HttpExceptionFilter } from '../../../integration/shared/https-exception.filter';
import { Crud } from '../../crud/src/decorators';
import { CompaniesService } from './__fixture__/companies.service';
import { ProjectsService } from './__fixture__/projects.service';
import { UsersService, UsersService2 } from './__fixture__/users.service';
import { NotesService } from './__fixture__/notes.service';
// tslint:disable:max-classes-per-file
describe('#crud-typeorm', () => {
describe('#query params', () => {
let app: INestApplication;
let server: any;
let qb: RequestQueryBuilder;
@Crud({
model: { type: Company },
query: {
exclude: ['updatedAt'],
allow: ['id', 'name', 'domain', 'description'],
filter: [{ field: 'id', operator: 'ne', value: 1 }],
join: {
users: {
allow: ['id'],
},
},
maxLimit: 5,
},
})
@Controller('companies')
class CompaniesController {
constructor(public service: CompaniesService) {}
}
@Crud({
model: { type: Project },
routes: {
updateOneBase: {
returnShallow: true,
},
},
query: {
join: {
company: {
eager: true,
persist: ['id'],
exclude: ['updatedAt', 'createdAt'],
},
users: {},
userProjects: {},
},
sort: [{ field: 'id', order: 'ASC' }],
limit: 100,
},
})
@Controller('projects')
class ProjectsController {
constructor(public service: ProjectsService) {}
}
@Crud({
model: { type: Project },
})
@Controller('projects2')
class ProjectsController2 {
constructor(public service: ProjectsService) {}
}
@Crud({
model: { type: Project },
query: {
filter: [{ field: 'isActive', operator: 'eq', value: false }],
},
})
@Controller('projects3')
class ProjectsController3 {
constructor(public service: ProjectsService) {}
}
@Crud({
model: { type: Project },
query: {
filter: { isActive: true },
},
})
@Controller('projects4')
class ProjectsController4 {
constructor(public service: ProjectsService) {}
}
@Crud({
model: { type: User },
query: {
join: {
company: {},
'company.projects': {},
userLicenses: {},
invalid: {
eager: true,
},
'foo.bar': {
eager: true,
},
},
},
})
@Controller('users')
class UsersController {
constructor(public service: UsersService) {}
}
@Crud({
model: { type: User },
query: {
join: {
company: {},
'company.projects': {
alias: 'pr',
},
},
},
})
@Controller('users2')
class UsersController2 {
constructor(public service: UsersService) {}
}
@Crud({
model: { type: User },
query: {
join: {
company: {
alias: 'userCompany',
eager: true,
select: false,
},
},
},
})
@Controller('myusers')
class UsersController3 {
constructor(public service: UsersService2) {}
}
@Crud({
model: { type: Note },
})
@Controller('notes')
class NotesController {
constructor(public service: NotesService) {}
}
beforeAll(async () => {
const fixture = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot({ ...withCache, logging: false }),
TypeOrmModule.forFeature([Company, Project, User, UserProfile, Note]),
],
controllers: [
CompaniesController,
ProjectsController,
ProjectsController2,
ProjectsController3,
ProjectsController4,
UsersController,
UsersController2,
UsersController3,
NotesController,
],
providers: [
{ provide: APP_FILTER, useClass: HttpExceptionFilter },
CompaniesService,
UsersService,
UsersService2,
ProjectsService,
NotesService,
],
}).compile();
app = fixture.createNestApplication();
await app.init();
server = app.getHttpServer();
});
beforeEach(() => {
qb = RequestQueryBuilder.create();
});
afterAll(async () => {
await app.close();
});
describe('#select', () => {
it('should throw status 400', (done) => {
const query = qb.setFilter({ field: 'invalid', operator: 'isnull' }).query();
request(server)
.get('/companies')
.query(query)
.end((_, res) => {
expect(res.status).toBe(500);
done();
});
});
});
describe('#query filter', () => {
it('should return data with limit', (done) => {
const query = qb.setLimit(4).query();
request(server)
.get('/companies')
.query(query)
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.length).toBe(4);
res.body.forEach((e: Company) => {
expect(e.id).not.toBe(1);
});
done();
});
});
it('should return with maxLimit', (done) => {
const query = qb.setLimit(7).query();
request(server)
.get('/companies')
.query(query)
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.length).toBe(5);
done();
});
});
it('should return with filter and or, 1', (done) => {
const query = qb
.setFilter({ field: 'name', operator: 'notin', value: ['Name2', 'Name3'] })
.setOr({ field: 'domain', operator: 'cont', value: 5 })
.query();
request(server)
.get('/companies')
.query(query)
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.length).toBe(5);
done();
});
});
it('should return with filter and or, 2', (done) => {
const query = qb
.setFilter({ field: 'name', operator: 'ends', value: 'foo' })
.setOr({ field: 'name', operator: 'starts', value: 'P' })
.setOr({ field: 'isActive', operator: 'eq', value: true })
.query();
request(server)
.get('/projects')
.query(query)
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.length).toBe(10);
done();
});
});
it('should return with filter and or, 3', (done) => {
const query = qb
.setOr({ field: 'companyId', operator: 'gt', value: 22 })
.setFilter({ field: 'companyId', operator: 'gte', value: 6 })
.setFilter({ field: 'companyId', operator: 'lt', value: 10 })
.query();
request(server)
.get('/projects')
.query(query)
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.length).toBe(8);
done();
});
});
it('should return with filter and or, 4', (done) => {
const query = qb
.setOr({ field: 'companyId', operator: 'in', value: [6, 10] })
.setOr({ field: 'companyId', operator: 'lte', value: 10 })
.setFilter({ field: 'isActive', operator: 'eq', value: false })
.setFilter({ field: 'description', operator: 'notnull' })
.query();
request(server)
.get('/projects')
.query(query)
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.length).toBe(10);
done();
});
});
it('should return with filter and or, 6', (done) => {
const query = qb.setOr({ field: 'companyId', operator: 'isnull' }).query();
request(server)
.get('/projects')
.query(query)
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.length).toBe(0);
done();
});
});
it('should return with filter and or, 6', (done) => {
const query = qb.setOr({ field: 'companyId', operator: 'between', value: [1, 5] }).query();
request(server)
.get('/projects')
.query(query)
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.length).toBe(10);
done();
});
});
it('should return with filter, 1', (done) => {
const query = qb.setOr({ field: 'companyId', operator: 'eq', value: 1 }).query();
request(server)
.get('/projects')
.query(query)
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.length).toBe(2);
done();
});
});
});
describe('#query join', () => {
it('should return joined entity, 1', (done) => {
const query = qb.setJoin({ field: 'company', select: ['name'] }).query();
request(server)
.get('/projects/2')
.query(query)
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.company).toBeDefined();
done();
});
});
it('should return joined entity, 2', (done) => {
const query = qb.setJoin({ field: 'users', select: ['name'] }).query();
request(server)
.get('/companies/2')
.query(query)
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.users).toBeDefined();
expect(res.body.users.length).not.toBe(0);
done();
});
});
it('should eager join without selection', (done) => {
const query = qb.search({ 'userCompany.id': { $eq: 1 } }).query();
request(server)
.get('/myusers')
.query(query)
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.length).toBe(10);
expect(res.body[0].company).toBeUndefined();
done();
});
});
});
describe('#query nested join', () => {
it('should return status 400, 1', (done) => {
const query = qb
.setJoin({ field: 'company' })
.setJoin({ field: 'company.projects' })
.setFilter({
field: 'company.projects.foo',
operator: 'excl',
value: 'invalid',
})
.query();
request(server)
.get('/users/1')
.query(query)
.end((_, res) => {
expect(res.status).toBe(500);
done();
});
});
it('should return status 400, 2', (done) => {
const query = qb
.setJoin({ field: 'company' })
.setJoin({ field: 'company.projects' })
.setFilter({
field: 'invalid.projects',
operator: 'excl',
value: 'invalid',
})
.query();
request(server)
.get('/users/1')
.query(query)
.end((_, res) => {
expect(res.status).toBe(500);
done();
});
});
it('should return status 400, 3', (done) => {
const query = qb
.setJoin({ field: 'company' })
.setJoin({ field: 'company.projects' })
.setFilter({
field: 'company.foo',
operator: 'excl',
value: 'invalid',
})
.query();
request(server)
.get('/users/1')
.query(query)
.end((_, res) => {
expect(res.status).toBe(500);
done();
});
});
it('should return status 200', (done) => {
const query = qb.setJoin({ field: 'company' }).setJoin({ field: 'company.projectsinvalid' }).query();
request(server)
.get('/users/1')
.query(query)
.end((_, res) => {
expect(res.status).toBe(200);
done();
});
});
it('should return joined entity, 1', (done) => {
const query = qb
.setFilter({ field: 'company.name', operator: 'excl', value: 'invalid' })
.setJoin({ field: 'company' })
.setJoin({ field: 'company.projects' })
.query();
request(server)
.get('/users/1')
.query(query)
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.company).toBeDefined();
expect(res.body.company.projects).toBeDefined();
done();
});
});
it('should return joined entity, 2', (done) => {
const query = qb
.setFilter({ field: 'company.projects.id', operator: 'notnull' })
.setJoin({ field: 'company' })
.setJoin({ field: 'company.projects' })
.query();
request(server)
.get('/users/1')
.query(query)
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.company).toBeDefined();
expect(res.body.company.projects).toBeDefined();
done();
});
});
it('should return joined entity with alias', (done) => {
const query = qb
.setFilter({ field: 'pr.id', operator: 'notnull' })
.setJoin({ field: 'company' })
.setJoin({ field: 'company.projects' })
.query();
request(server)
.get('/users2/1')
.query(query)
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.company).toBeDefined();
expect(res.body.company.projects).toBeDefined();
done();
});
});
it('should return joined entity with ManyToMany pivot table', (done) => {
const query = qb.setJoin({ field: 'users' }).setJoin({ field: 'userProjects' }).query();
request(server)
.get('/projects/1')
.query(query)
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.users).toBeDefined();
expect(res.body.users.length).toBe(2);
expect(res.body.users[0].id).toBe(1);
expect(res.body.userProjects).toBeDefined();
expect(res.body.userProjects.length).toBe(2);
expect(res.body.userProjects[0].review).toBe('User project 1 1');
done();
});
});
});
describe('#query composite key join', () => {
it('should return joined relation', (done) => {
const query = qb.setJoin({ field: 'userLicenses' }).query();
request(server)
.get('/users/1')
.query(query)
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.userLicenses).toBeDefined();
done();
});
});
});
describe('#sort', () => {
it('should sort by field', async () => {
const query = qb.sortBy({ field: 'id', order: 'DESC' }).query();
const res = await request(server).get('/users').query(query).expect(200);
expect(res.body[1].id).toBeLessThan(res.body[0].id);
});
it('should sort by nested field, 1', async () => {
const query = qb
.setFilter({ field: 'company.id', operator: 'notnull' })
.setJoin({ field: 'company' })
.sortBy({ field: 'company.id', order: 'DESC' })
.query();
const res = await request(server).get('/users').query(query).expect(200);
expect(res.body[res.body.length - 1].company.id).toBeLessThan(res.body[0].company.id);
});
it('should sort by nested field, 2', async () => {
const query = qb
.setFilter({ field: 'id', operator: 'eq', value: 1 })
.setFilter({ field: 'company.id', operator: 'notnull' })
.setFilter({ field: 'projects.id', operator: 'notnull' })
.setJoin({ field: 'company' })
.setJoin({ field: 'company.projects' })
.sortBy({ field: 'projects.id', order: 'DESC' })
.query();
const res = await request(server).get('/users').query(query).expect(200);
expect(res.body[0].company.projects[1].id).toBeLessThan(res.body[0].company.projects[0].id);
});
it('should sort by nested field, 3', async () => {
const query = qb
.setFilter({ field: 'id', operator: 'eq', value: 1 })
.setFilter({ field: 'company.id', operator: 'notnull' })
.setFilter({ field: 'projects.id', operator: 'notnull' })
.setJoin({ field: 'company' })
.setJoin({ field: 'company.projects' })
.sortBy({ field: 'company.projects.id', order: 'DESC' })
.query();
const res = await request(server).get('/users').query(query).expect(200);
expect(res.body[0].company.projects[1].id).toBeLessThan(res.body[0].company.projects[0].id);
});
it('should throw 400 if SQL injection has been detected', (done) => {
const query = qb
.sortBy({
field: ' ASC; SELECT CAST( version() AS INTEGER); --',
order: 'DESC',
})
.query();
request(server)
.get('/companies')
.query(query)
.end((_, res) => {
expect(res.status).toBe(400);
done();
});
});
});
describe('#search', () => {
const projects2 = () => request(server).get('/projects2');
const projects3 = () => request(server).get('/projects3');
const projects4 = () => request(server).get('/projects4');
it('should return with search, 1', async () => {
const query = qb.search({ id: 1 }).query();
const res = await projects2().query(query).expect(200);
expect(res.body).toBeArrayOfSize(1);
expect(res.body[0].id).toBe(1);
});
it('should return with search, 2', async () => {
const query = qb.search({ id: 1, name: 'Project1' }).query();
const res = await projects2().query(query).expect(200);
expect(res.body).toBeArrayOfSize(1);
expect(res.body[0].id).toBe(1);
});
it('should return with search, 3', async () => {
const query = qb.search({ id: 1, name: { $eq: 'Project1' } }).query();
const res = await projects2().query(query).expect(200);
expect(res.body).toBeArrayOfSize(1);
expect(res.body[0].id).toBe(1);
});
it('should return with search, 4', async () => {
const query = qb.search({ name: { $eq: 'Project1' } }).query();
const res = await projects2().query(query).expect(200);
expect(res.body).toBeArrayOfSize(1);
expect(res.body[0].id).toBe(1);
});
it('should return with search, 5', async () => {
const query = qb.search({ id: { $notnull: true, $eq: 1 } }).query();
const res = await projects2().query(query).expect(200);
expect(res.body).toBeArrayOfSize(1);
expect(res.body[0].id).toBe(1);
});
it('should return with search, 6', async () => {
const query = qb.search({ id: { $or: { $isnull: true, $eq: 1 } } }).query();
const res = await projects2().query(query).expect(200);
expect(res.body).toBeArrayOfSize(1);
expect(res.body[0].id).toBe(1);
});
it('should return with search, 7', async () => {
const query = qb.search({ id: { $or: { $eq: 1 } } }).query();
const res = await projects2().query(query).expect(200);
expect(res.body).toBeArrayOfSize(1);
expect(res.body[0].id).toBe(1);
});
it('should return with search, 8', async () => {
const query = qb.search({ id: { $notnull: true, $or: { $eq: 1, $in: [30, 31] } } }).query();
const res = await projects2().query(query).expect(200);
expect(res.body).toBeArrayOfSize(1);
expect(res.body[0].id).toBe(1);
});
it('should return with search, 9', async () => {
const query = qb.search({ id: { $notnull: true, $or: { $eq: 1 } } }).query();
const res = await projects2().query(query).expect(200);
expect(res.body).toBeArrayOfSize(1);
expect(res.body[0].id).toBe(1);
});
it('should return with search, 10', async () => {
const query = qb.search({ id: null }).query();
const res = await projects2().query(query).expect(200);
expect(res.body).toBeArrayOfSize(0);
});
it('should return with search, 11', async () => {
const query = qb.search({ $and: [{ id: { $notin: [5, 6, 7, 8, 9, 10] } }, { isActive: true }] }).query();
const res = await projects2().query(query).expect(200);
expect(res.body).toBeArrayOfSize(4);
});
it('should return with search, 12', async () => {
const query = qb.search({ $and: [{ id: { $notin: [5, 6, 7, 8, 9, 10] } }] }).query();
const res = await projects2().query(query).expect(200);
expect(res.body).toBeArrayOfSize(14);
});
it('should return with search, 13', async () => {
const query = qb.search({ $or: [{ id: 54 }] }).query();
const res = await projects2().query(query).expect(200);
expect(res.body).toBeArrayOfSize(0);
});
it('should return with search, 14', async () => {
const query = qb.search({ $or: [{ id: 54 }, { id: 33 }, { id: { $in: [1, 2] } }] }).query();
const res = await projects2().query(query).expect(200);
expect(res.body).toBeArrayOfSize(2);
expect(res.body[0].id).toBe(1);
expect(res.body[1].id).toBe(2);
});
it('should return with search, 15', async () => {
const query = qb.search({ $or: [{ id: 54 }], name: 'Project1' }).query();
const res = await projects2().query(query).expect(200);
expect(res.body).toBeArrayOfSize(0);
});
it('should return with search, 16', async () => {
const query = qb.search({ $or: [{ isActive: false }, { id: 3 }], name: 'Project3' }).query();
const res = await projects2().query(query).expect(200);
expect(res.body).toBeArrayOfSize(1);
expect(res.body[0].id).toBe(3);
});
it('should return with search, 17', async () => {
const query = qb.search({ $or: [{ isActive: false }, { id: { $eq: 3 } }], name: 'Project3' }).query();
const res = await projects2().query(query).expect(200);
expect(res.body).toBeArrayOfSize(1);
expect(res.body[0].id).toBe(3);
});
it('should return with search, 17', async () => {
const query = qb
.search({
$or: [{ isActive: false }, { id: { $eq: 3 } }],
name: { $eq: 'Project3' },
})
.query();
const res = await projects2().query(query).expect(200);
expect(res.body).toBeArrayOfSize(1);
expect(res.body[0].id).toBe(3);
});
it('should return with default filter, 1', async () => {
const query = qb.search({ name: 'Project11' }).query();
const res = await projects3().query(query).expect(200);
expect(res.body).toBeArrayOfSize(1);
expect(res.body[0].id).toBe(11);
});
it('should return with default filter, 2', async () => {
const query = qb.search({ name: 'Project1' }).query();
const res = await projects3().query(query).expect(200);
expect(res.body).toBeArrayOfSize(0);
});
it('should return with default filter, 3', async () => {
const query = qb.search({ name: 'Project2' }).query();
const res = await projects4().query(query).expect(200);
expect(res.body).toBeArrayOfSize(1);
expect(res.body[0].id).toBe(2);
});
it('should return with default filter, 4', async () => {
const query = qb.search({ name: 'Project11' }).query();
const res = await projects4().query(query).expect(200);
expect(res.body).toBeArrayOfSize(0);
});
it('should return with $eqL search operator', async () => {
const query = qb.search({ name: { $eqL: 'project1' } }).query();
const res = await projects4().query(query).expect(200);
expect(res.body).toBeArrayOfSize(1);
});
it('should return with $neL search operator', async () => {
const query = qb.search({ name: { $neL: 'project1' } }).query();
const res = await projects4().query(query).expect(200);
expect(res.body).toBeArrayOfSize(9);
});
it('should return with $startsL search operator', async () => {
const query = qb.search({ email: { $startsL: '2' } }).query();
const res = await request(server).get('/users').query(query).expect(200);
expect(res.body).toBeArrayOfSize(3);
});
it('should return with $endsL search operator', async () => {
const query = qb.search({ domain: { $endsL: 'AiN10' } }).query();
const res = await request(server).get('/companies').query(query).expect(200);
expect(res.body).toBeArrayOfSize(1);
expect(res.body[0].domain).toBe('Domain10');
});
it('should return with $contL search operator', async () => {
const query = qb.search({ email: { $contL: '1@' } }).query();
const res = await request(server).get('/users').query(query).expect(200);
expect(res.body).toBeArrayOfSize(3);
});
it('should return with $exclL search operator', async () => {
const query = qb.search({ email: { $exclL: '1@' } }).query();
const res = await request(server).get('/users').query(query).expect(200);
expect(res.body).toBeArrayOfSize(18);
});
it('should return with $inL search operator', async () => {
const query = qb.search({ name: { $inL: ['name2', 'name3'] } }).query();
const res = await request(server).get('/companies').query(query).expect(200);
expect(res.body).toBeArrayOfSize(2);
});
it('should return with $notinL search operator', async () => {
const query = qb.search({ name: { $notinL: ['project7', 'project8', 'project9'] } }).query();
const res = await projects4().query(query).expect(200);
expect(res.body).toBeArrayOfSize(7);
});
it('should search by display column name, but use dbName in sql query', async () => {
const query = qb.search({ revisionId: 2 }).query();
const res = await request(server).get('/notes').query(query).expect(200);
expect(res.body).toBeArrayOfSize(2);
expect(res.body[0].revisionId).toBe(2);
expect(res.body[1].revisionId).toBe(2);
});
});
describe('#update', () => {
it('should update company id of project', async () => {
await request(server).patch('/projects/18').send({ companyId: 10 }).expect(200);
const modified = await request(server).get('/projects/18').expect(200);
expect(modified.body.companyId).toBe(10);
});
});
});
});
================================================
FILE: packages/crud-typeorm/test/c.basic-crud.spec.ts
================================================
import { Controller, INestApplication } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { Test } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Crud } from '@nestjsx/crud';
import { RequestQueryBuilder } from '@nestjsx/crud-request';
import * as request from 'supertest';
import { Company } from '../../../integration/crud-typeorm/companies';
import { Device } from '../../../integration/crud-typeorm/devices';
import { withCache } from '../../../integration/crud-typeorm/orm.config';
import { Project } from '../../../integration/crud-typeorm/projects';
import { User } from '../../../integration/crud-typeorm/users';
import { UserProfile } from '../../../integration/crud-typeorm/users-profiles';
import { HttpExceptionFilter } from '../../../integration/shared/https-exception.filter';
import { CompaniesService } from './__fixture__/companies.service';
import { UsersService } from './__fixture__/users.service';
import { DevicesService } from './__fixture__/devices.service';
const isMysql = process.env.TYPEORM_CONNECTION === 'mysql';
// tslint:disable:max-classes-per-file no-shadowed-variable
describe('#crud-typeorm', () => {
describe('#basic crud using alwaysPaginate default respects global limit', () => {
let app: INestApplication;
let server: any;
let qb: RequestQueryBuilder;
let service: CompaniesService;
@Crud({
model: { type: Company },
query: {
alwaysPaginate: true,
limit: 3,
},
})
@Controller('companies0')
class CompaniesController0 {
constructor(public service: CompaniesService) {}
}
beforeAll(async () => {
const fixture = await Test.createTestingModule({
imports: [TypeOrmModule.forRoot(withCache), TypeOrmModule.forFeature([Company])],
controllers: [CompaniesController0],
providers: [{ provide: APP_FILTER, useClass: HttpExceptionFilter }, CompaniesService],
}).compile();
app = fixture.createNestApplication();
service = app.get(CompaniesService);
await app.init();
server = app.getHttpServer();
});
beforeEach(() => {
qb = RequestQueryBuilder.create();
});
afterAll(async () => {
await app.close();
});
describe('#getAllBase', () => {
it('should return an array of all entities', (done) => {
request(server)
.get('/companies0')
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.data.length).toBe(3);
expect(res.body.page).toBe(1);
done();
});
});
});
});
describe('#basic crud using alwaysPaginate default', () => {
let app: INestApplication;
let server: any;
let qb: RequestQueryBuilder;
let service: CompaniesService;
@Crud({
model: { type: Company },
query: { alwaysPaginate: true },
})
@Controller('companies')
class CompaniesController {
constructor(public service: CompaniesService) {}
}
beforeAll(async () => {
const fixture = await Test.createTestingModule({
imports: [TypeOrmModule.forRoot(withCache), TypeOrmModule.forFeature([Company])],
controllers: [CompaniesController],
providers: [{ provide: APP_FILTER, useClass: HttpExceptionFilter }, CompaniesService],
}).compile();
app = fixture.createNestApplication();
service = app.get(CompaniesService);
await app.init();
server = app.getHttpServer();
});
beforeEach(() => {
qb = RequestQueryBuilder.create();
});
afterAll(async () => {
await app.close();
});
describe('#getAllBase', () => {
it('should return an array of all entities', (done) => {
request(server)
.get('/companies')
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.data.length).toBe(9);
expect(res.body.page).toBe(1);
done();
});
});
it('should return an entities with limit', (done) => {
const query = qb.setLimit(5).query();
request(server)
.get('/companies')
.query(query)
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.data.length).toBe(5);
expect(res.body.page).toBe(1);
done();
});
});
it('should return an entities with limit and page', (done) => {
const query = qb.setLimit(3).setPage(1).sortBy({ field: 'id', order: 'DESC' }).query();
request(server)
.get('/companies')
.query(query)
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.data.length).toBe(3);
expect(res.body.count).toBe(3);
expect(res.body.page).toBe(1);
done();
});
});
});
});
describe('#basic crud', () => {
let app: INestApplication;
let server: any;
let qb: RequestQueryBuilder;
let service: CompaniesService;
@Crud({
model: { type: Company },
query: {
softDelete: true,
},
})
@Controller('companies')
class CompaniesController {
constructor(public service: CompaniesService) {}
}
@Crud({
model: { type: User },
params: {
companyId: {
field: 'companyId',
type: 'number',
},
id: {
field: 'id',
type: 'number',
primary: true,
},
},
routes: {
deleteOneBase: {
returnDeleted: true,
},
},
query: {
persist: ['isActive'],
cache: 10,
},
validation: {
transform: true,
},
})
@Controller('companies/:companyId/users')
class UsersController {
constructor(public service: UsersService) {}
}
@Crud({
model: { type: User },
query: {
join: {
profile: {
eager: true,
required: true,
},
},
},
})
@Controller('/users2')
class UsersController2 {
constructor(public service: UsersService) {}
}
@Crud({
model: { type: User },
query: {
join: {
profile: {
eager: true,
},
},
},
})
@Controller('/users3')
class UsersController3 {
constructor(public service: UsersService) {}
}
@Crud({
model: { type: User },
params: {
companyId: { field: 'companyId', type: 'number', primary: true },
profileId: { field: 'profileId', type: 'number', primary: true },
},
})
@Controller('users4')
class UsersController4 {
constructor(public service: UsersService) {}
}
@Crud({
model: { type: Device },
params: {
deviceKey: {
field: 'deviceKey',
type: 'uuid',
primary: true,
},
},
routes: {
createOneBase: {
returnShallow: true,
},
},
})
@Controller('devices')
class DevicesController {
constructor(public service: DevicesService) {}
}
beforeAll(async () => {
const fixture = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot({ ...withCache, logging: false }),
TypeOrmModule.forFeature([Company, Project, User, UserProfile, Device]),
],
controllers: [
CompaniesController,
UsersController,
UsersController2,
UsersController3,
UsersController4,
DevicesController,
],
providers: [
{ provide: APP_FILTER, useClass: HttpExceptionFilter },
CompaniesService,
UsersService,
DevicesService,
],
}).compile();
app = fixture.createNestApplication();
service = app.get(CompaniesService);
await app.init();
server = app.getHttpServer();
});
beforeEach(() => {
qb = RequestQueryBuilder.create();
});
afterAll(async () => {
await app.close();
});
describe('#find', () => {
it('should return entities', async () => {
const data = await service.find();
expect(data.length).toBe(9);
});
});
describe('#findOne', () => {
it('should return one entity', async () => {
const data = await service.findOne({ where: { id: 1 } });
expect(data.id).toBe(1);
});
});
describe('#count', () => {
it('should return number', async () => {
const data = await service.count();
expect(typeof data).toBe('number');
});
});
describe('#getAllBase', () => {
it('should return an array of all entities', (done) => {
request(server)
.get('/companies?include_deleted=1')
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.length).toBe(10);
done();
});
});
it('should return an entities with limit', (done) => {
const query = qb.setLimit(5).query();
request(server)
.get('/companies')
.query(query)
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.length).toBe(5);
done();
});
});
it('should return an entities with limit and page', (done) => {
const query = qb.setLimit(3).setPage(1).sortBy({ field: 'id', order: 'DESC' }).query();
request(server)
.get('/companies')
.query(query)
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.data.length).toBe(3);
expect(res.body.count).toBe(3);
expect(res.body.total).toBe(9);
expect(res.body.page).toBe(1);
expect(res.body.pageCount).toBe(3);
done();
});
});
it('should return an entities with offset', (done) => {
const queryObj = qb.setOffset(3);
if (isMysql) {
queryObj.setLimit(10);
}
const query = queryObj.query();
request(server)
.get('/companies')
.query(query)
.end((_, res) => {
expect(res.status).toBe(200);
if (isMysql) {
expect(res.body.count).toBe(6);
expect(res.body.data.length).toBe(6);
} else {
expect(res.body.length).toBe(6);
}
done();
});
});
});
describe('#getOneBase', () => {
it('should return status 404', (done) => {
request(server)
.get('/companies/333')
.end((_, res) => {
expect(res.status).toBe(404);
done();
});
});
it('should return status 404 for deleted entity', (done) => {
request(server)
.get('/companies/9')
.end((_, res) => {
expect(res.status).toBe(404);
done();
});
});
it('should return a deleted entity if include_deleted query param is specified', (done) => {
request(server)
.get('/companies/9?include_deleted=1')
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.id).toBe(9);
done();
});
});
it('should return an entity, 1', (done) => {
request(server)
.get('/companies/1')
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.id).toBe(1);
done();
});
});
it('should return an entity, 2', (done) => {
const query = qb.select(['domain']).query();
request(server)
.get('/companies/1')
.query(query)
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.id).toBe(1);
expect(res.body.domain).toBeTruthy();
done();
});
});
it('should return an entity with compound key', (done) => {
request(server)
.get('/users4/1/5')
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.id).toBe(5);
done();
});
});
it('should return an entity with and set cache', (done) => {
request(server)
.get('/companies/1/users/1')
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.id).toBe(1);
expect(res.body.companyId).toBe(1);
done();
});
});
it('should return an entity with its embedded entity properties', (done) => {
request(server)
.get('/companies/1/users/1')
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.id).toBe(1);
expect(res.body.name.first).toBe('firstname1');
expect(res.body.name.last).toBe('lastname1');
done();
});
});
});
describe('#createOneBase', () => {
it('should return status 400', (done) => {
request(server)
.post('/companies')
.send('')
.end((_, res) => {
expect(res.status).toBe(400);
done();
});
});
it('should return saved entity', (done) => {
const dto = {
name: 'test0',
domain: 'test0',
};
request(server)
.post('/companies')
.send(dto)
.end((_, res) => {
expect(res.status).toBe(201);
expect(res.body.id).toBeTruthy();
done();
});
});
it('should return saved entity with param', (done) => {
const dto: any = {
email: 'test@test.com',
isActive: true,
name: {
first: 'test',
last: 'last',
},
profile: {
name: 'testName',
},
};
request(server)
.post('/companies/1/users')
.send(dto)
.end((_, res) => {
expect(res.status).toBe(201);
expect(res.body.id).toBeTruthy();
expect(res.body.companyId).toBe(1);
done();
});
});
it('should return with `returnShallow`', (done) => {
const dto: any = { description: 'returnShallow is true' };
request(server)
.post('/devices')
.send(dto)
.end((_, res) => {
expect(res.status).toBe(201);
expect(res.body.deviceKey).toBeTruthy();
expect(res.body.description).toBeTruthy();
done();
});
});
});
describe('#createManyBase', () => {
it('should return status 400', (done) => {
const dto = { bulk: [] };
request(server)
.post('/companies/bulk')
.send(dto)
.end((_, res) => {
expect(res.status).toBe(400);
done();
});
});
it('should return created entities', (done) => {
const dto = {
bulk: [
{
name: 'test1',
domain: 'test1',
},
{
name: 'test2',
domain: 'test2',
},
],
};
request(server)
.post('/companies/bulk')
.send(dto)
.end((_, res) => {
expect(res.status).toBe(201);
expect(res.body[0].id).toBeTruthy();
expect(res.body[1].id).toBeTruthy();
done();
});
});
});
describe('#updateOneBase', () => {
it('should return status 404', (done) => {
const dto = { name: 'updated0' };
request(server)
.patch('/companies/333')
.send(dto)
.end((_, res) => {
expect(res.status).toBe(404);
done();
});
});
it('should return updated entity, 1', (done) => {
const dto = { name: 'updated0' };
request(server)
.patch('/companies/1')
.send(dto)
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.name).toBe('updated0');
done();
});
});
it('should return updated entity, 2', (done) => {
const dto = { isActive: false, companyId: 5 };
request(server)
.patch('/companies/1/users/22')
.send(dto)
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.isActive).toBe(false);
expect(res.body.companyId).toBe(1);
done();
});
});
});
describe('#replaceOneBase', () => {
it('should create entity', (done) => {
const dto = { name: 'updated0', domain: 'domain0' };
request(server)
.put('/companies/333')
.send(dto)
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.name).toBe('updated0');
done();
});
});
it('should return updated entity, 1', (done) => {
const dto = { name: 'updated0' };
request(server)
.put('/companies/1')
.send(dto)
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.name).toBe('updated0');
done();
});
});
});
describe('#deleteOneBase', () => {
it('should return status 404', (done) => {
request(server)
.delete('/companies/3333')
.end((_, res) => {
expect(res.status).toBe(404);
done();
});
});
it('should softly delete entity', (done) => {
request(server)
.delete('/companies/5')
.end((_, res) => {
expect(res.status).toBe(200);
done();
});
});
it('should not return softly deleted entity', (done) => {
request(server)
.get('/companies/5')
.end((_, res) => {
expect(res.status).toBe(404);
done();
});
});
it('should recover softly deleted entity', (done) => {
request(server)
.patch('/companies/5/recover')
.end((_, res) => {
expect(res.status).toBe(200);
done();
});
});
it('should return recovered entity', (done) => {
request(server)
.get('/companies/5')
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.id).toBe(5);
done();
});
});
it('should return deleted entity', (done) => {
request(server)
.delete('/companies/1/users/22')
.end((_, res) => {
expect(res.status).toBe(200);
expect(res.body.id).toBe(22);
expect(res.body.companyId).toBe(1);
done();
});
});
});
describe('join options: required', () => {
const users2 = () => request(server).get('/users2/21');
const users3 = () => request(server).get('/users3/21');
it('should return status 404', async () => {
await users2().expect(404);
});
it('should return status 200', async () => {
const res = await users3().expect(200);
expect(res.body.id).toBe(21);
expect(res.body.profile).toBe(null);
});
});
});
});
================================================
FILE: packages/crud-typeorm/test/d.crud-auth.spec.ts
================================================
import { Controller, INestApplication, Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { APP_FILTER, APP_GUARD } from '@nestjs/core';
import { Test } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Crud, CrudAuth } from '@nestjsx/crud';
import * as request from 'supertest';
import { withCache } from '../../../integration/crud-typeorm/orm.config';
import { User } from '../../../integration/crud-typeorm/users';
import { UserProfile } from '../../../integration/crud-typeorm/users-profiles';
import { Project } from '../../../integration/crud-typeorm/projects';
import { HttpExceptionFilter } from '../../../integration/shared/https-exception.filter';
import { UsersService } from './__fixture__/users.service';
import { ProjectsService } from './__fixture__/projects.service';
describe('#crud-typeorm', () => {
describe('#CrudAuth', () => {
const USER_REQUEST_KEY = 'user';
let app: INestApplication;
let server: request.SuperTest;
@Injectable()
class AuthGuard implements CanActivate {
constructor(private usersService: UsersService) {}
async canActivate(ctx: ExecutionContext): Promise {
const req = ctx.switchToHttp().getRequest();
req[USER_REQUEST_KEY] = await this.usersService.findOne({ where: { id: 1 } });
return true;
}
}
@Crud({
model: {
type: User,
},
routes: {
only: ['getOneBase', 'updateOneBase'],
},
params: {
id: {
primary: true,
disabled: true,
},
},
})
@CrudAuth({
property: USER_REQUEST_KEY,
filter: (user: User) => ({
id: user.id,
}),
persist: (user: User) => ({
email: user.email,
}),
})
@Controller('me')
class MeController {
constructor(public service: UsersService) {}
}
@Crud({
model: {
type: Project,
},
routes: {
only: ['createOneBase', 'deleteOneBase'],
},
})
@CrudAuth({
property: USER_REQUEST_KEY,
filter: (user: User) => ({
companyId: user.companyId,
}),
persist: (user: User) => ({
companyId: user.companyId,
}),
})
@Controller('projects')
class ProjectsController {
constructor(public service: ProjectsService) {}
}
beforeAll(async () => {
const fixture = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot({ ...withCache, logging: false }),
TypeOrmModule.forFeature([User, UserProfile, Project]),
],
controllers: [MeController, ProjectsController],
providers: [
{
provide: APP_GUARD,
useClass: AuthGuard,
},
{
provide: APP_FILTER,
useClass: HttpExceptionFilter,
},
UsersService,
ProjectsService,
],
}).compile();
app = fixture.createNestApplication();
await app.init();
server = request(app.getHttpServer());
});
afterAll(async () => {
await app.close();
});
describe('#getOneBase', () => {
it('should return a user with id 1', async () => {
const res = await server.get('/me').expect(200);
expect(res.body.id).toBe(1);
});
});
describe('#updateOneBase', () => {
it('should update user with auth persist, 1', async () => {
const res = await server
.patch('/me')
.send({
email: 'some@dot.com',
isActive: false,
})
.expect(200);
expect(res.body.id).toBe(1);
expect(res.body.email).toBe('1@email.com');
expect(res.body.isActive).toBe(false);
});
it('should update user with auth persist, 1', async () => {
const res = await server
.patch('/me')
.send({
email: 'some@dot.com',
isActive: true,
})
.expect(200);
expect(res.body.id).toBe(1);
expect(res.body.email).toBe('1@email.com');
expect(res.body.isActive).toBe(true);
});
});
describe('#createOneBase', () => {
it('should create an entity with auth persist', async () => {
const res = await server
.post('/projects')
.send({
name: 'Test',
description: 'foo',
isActive: false,
companyId: 10,
})
.expect(201);
expect(res.body.companyId).toBe(1);
});
});
describe('#deleteOneBase', () => {
it('should delete an entity with auth filter', async () => {
const res = await server.delete('/projects/21').expect(200);
});
it('should throw an error with auth filter', async () => {
const res = await server.delete('/projects/20').expect(404);
});
});
});
});
================================================
FILE: packages/crud-typeorm/tsconfig.json
================================================
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib"
},
"include": ["src"],
"exclude": ["lib"],
"references": [
{ "path": "../util" },
{ "path": "../crud-request" },
{ "path": "../crud" }
]
}
================================================
FILE: packages/util/README.md
================================================
CRUD (@nestjsx/util)
for RESTful APIs built with NestJs
We believe that everyone who's working with NestJs and building some RESTful services and especially some CRUD functionality will find `@nestjsx/crud` microframework very useful.
## Features
- Super easy to install and start using the full-featured controllers and services :point_right:
- DB and service agnostic extendable CRUD controllers
- Reach query parsing with filtering, pagination, sorting, relations, nested relations, cache, etc.
- Framework agnostic package with query builder for a frontend usage
- Query, path params and DTOs validation included
- Overriding controller methods with ease
- Tiny config (including globally)
- Additional helper decorators
- Swagger documentation
## Packages
- [**@nestjsx/crud**](https://www.npmjs.com/package/@nestjsx/crud) - core package which provides `@Crud()` decorator for endpoints generation, global configuration, validation, helper decorators ([docs](https://github.com/nestjsx/crud/wiki/Controllers#description))
- [**@nestjsx/crud-request**](https://www.npmjs.com/package/@nestjsx/crud-request) - request builder/parser package which provides `RequestQueryBuilder` class for a frontend usage and `RequestQueryParser` that is being used internally for handling and validating query/path params on a backend side ([docs](https://github.com/nestjsx/crud/wiki/Requests#frontend-usage))
- [**@nestjsx/crud-typeorm**](https://www.npmjs.com/package/@nestjsx/crud-typeorm) - TypeORM package which provides base `TypeOrmCrudService` with methods for CRUD database operations ([docs](https://github.com/nestjsx/crud/wiki/ServiceTypeorm))
## Documentation
- [General Information](https://github.com/nestjsx/crud/wiki#why)
- [CRUD Controllers](https://github.com/nestjsx/crud/wiki/Controllers#description)
- [CRUD ORM Services](https://github.com/nestjsx/crud/wiki/Services#description)
- [Handling Requests](https://github.com/nestjsx/crud/wiki/Requests#description)
## Support
Any support is welcome. At least you can give us a star.
## Contributors
### Code Contributors
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
### Financial Contributors
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/nestjsx#backer)]
#### Individuals
#### Organizations
Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/nestjsx#sponsor)]
## License
[MIT](LICENSE)
================================================
FILE: packages/util/package.json
================================================
{
"name": "@nestjsx/util",
"description": "NestJs CRUD for RESTful APIs - util",
"version": "5.0.0-alpha.3",
"license": "MIT",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
"publishConfig": {
"access": "public"
},
"files": [
"lib"
],
"repository": {
"type": "git",
"url": "https://github.com/nestjsx/crud.git"
},
"bugs": {
"url": "https://github.com/nestjsx/crud/issues"
},
"keywords": [
"typescript",
"typeorm",
"nest",
"nestjs",
"rest",
"restful",
"api",
"crud",
"crud-generator"
],
"author": {
"name": "Michael Yali",
"email": "mihon4ik@gmail.com"
},
"scripts": {
"build": "npx tsc -b"
}
}
================================================
FILE: packages/util/src/checks.util.ts
================================================
import { objKeys } from './obj.util';
export const isUndefined = (val: any): boolean => typeof val === 'undefined';
export const isNull = (val: any): boolean => val === null;
export const isNil = (val: any): boolean => isUndefined(val) || isNull(val);
export const isString = (val: any): boolean => typeof val === 'string';
export const hasLength = (val: any): boolean => val.length > 0;
export const isStringFull = (val: any): boolean => isString(val) && hasLength(val);
export const isArrayFull = (val: any): boolean => Array.isArray(val) && hasLength(val);
export const isArrayStrings = (val: any): boolean =>
isArrayFull(val) && (val as string[]).every((v) => isStringFull(v));
export const isObject = (val: any): boolean => typeof val === 'object' && !isNull(val);
export const isObjectFull = (val: any) => isObject(val) && hasLength(objKeys(val));
export const isNumber = (val: any): boolean =>
typeof val === 'number' && !Number.isNaN(val) && Number.isFinite(val);
export const isEqual = (val: any, eq: any): boolean => val === eq;
export const isFalse = (val: any): boolean => val === false;
export const isTrue = (val: any): boolean => val === true;
export const isIn = (val: any, arr: any[] = []): boolean =>
arr.some((o) => isEqual(val, o));
export const isBoolean = (val: any): boolean => typeof val === 'boolean';
export const isNumeric = (val: any): boolean => /^[+-]?([0-9]*[.])?[0-9]+$/.test(val);
export const isDateString = (val: any): boolean =>
isStringFull(val) &&
/^\d{4}-[01]\d-[0-3]\d(?:T[0-2]\d:[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[-+][0-2]\d(?::?[0-5]\d)?)?)?$/g.test(
val,
);
export const isDate = (val: any): val is Date => val instanceof Date;
export const isValue = (val: any): boolean =>
isStringFull(val) || isNumber(val) || isBoolean(val) || isDate(val);
export const hasValue = (val: any): boolean =>
isArrayFull(val) ? (val as any[]).every((o) => isValue(o)) : isValue(val);
export const isFunction = (val: any): boolean => typeof val === 'function';
================================================
FILE: packages/util/src/index.ts
================================================
export * from './checks.util';
export * from './obj.util';
export * from './types';
================================================
FILE: packages/util/src/obj.util.ts
================================================
export const objKeys = (val: any): string[] => Object.keys(val);
export const getOwnPropNames = (val: any): string[] => Object.getOwnPropertyNames(val);
================================================
FILE: packages/util/src/types/class.type.ts
================================================
export type ClassType = {
new (...args: any[]): T;
};
================================================
FILE: packages/util/src/types/index.ts
================================================
export * from './class.type';
export * from './object-literal.type';
================================================
FILE: packages/util/src/types/object-literal.type.ts
================================================
export type ObjectLiteral = {
[key: string]: any;
};
================================================
FILE: packages/util/test/checks.util.spec.ts
================================================
import {
hasLength,
hasValue,
isArrayFull,
isArrayStrings,
isBoolean,
isDate,
isDateString,
isEqual,
isFalse,
isFunction,
isIn,
isNil,
isNull,
isNumber,
isNumeric,
isObject,
isObjectFull,
isString,
isStringFull,
isTrue,
isUndefined,
isValue,
} from '../src/';
describe('#util', () => {
describe('#isUndefined', () => {
it('should return true', () => {
expect(isUndefined(undefined)).toBe(true);
});
it('should return false', () => {
expect(isUndefined(null)).toBe(false);
});
});
describe('#isNull', () => {
it('should reurn true', () => {
expect(isNull(null)).toBe(true);
});
it('should return false', () => {
expect(isNull(NaN)).toBe(false);
});
});
describe('#isNil', () => {
it('should return true', () => {
expect(isNil(null)).toBe(true);
expect(isNil(undefined)).toBe(true);
});
it('should return false', () => {
expect(isNil(NaN)).toBe(false);
});
});
describe('#isString', () => {
it('should return true', () => {
expect(isString('test')).toBe(true);
});
it('should return false', () => {
expect(isString(null)).toBe(false);
});
});
describe('#hasLength', () => {
it('should return true', () => {
expect(hasLength('test')).toBe(true);
expect(hasLength([1])).toBe(true);
});
it('should return false', () => {
expect(hasLength({})).toBe(false);
expect(hasLength('')).toBe(false);
expect(hasLength([])).toBe(false);
});
});
describe('#isStringFull', () => {
it('should return true', () => {
expect(isStringFull('test')).toBe(true);
});
it('should return false', () => {
expect(isStringFull('')).toBe(false);
expect(isStringFull([])).toBe(false);
});
});
describe('#isArrayFull', () => {
it('should return true', () => {
expect(isArrayFull([1])).toBe(true);
});
it('should return false', () => {
expect(isArrayFull([])).toBe(false);
});
});
describe('#isArrayStrings', () => {
it('should return true', () => {
expect(isArrayStrings(['1', 'true'])).toBe(true);
});
it('should return false', () => {
expect(isArrayStrings([])).toBe(false);
});
});
describe('#isObject', () => {
it('should return true', () => {
expect(isObject({})).toBe(true);
});
it('should return false', () => {
expect(isObject(1)).toBe(false);
expect(isObject(null)).toBe(false);
});
});
describe('#isObjectFull', () => {
it('should return true', () => {
expect(isObjectFull({ foo: 1 })).toBe(true);
});
it('should return false', () => {
expect(isObjectFull({})).toBe(false);
});
});
describe('#isNumber', () => {
it('should return true', () => {
expect(isNumber(1)).toBe(true);
});
it('should return false', () => {
expect(isNumber(true)).toBe(false);
expect(isNumber(NaN)).toBe(false);
expect(isNumber(Infinity)).toBe(false);
});
});
describe('#isEqual', () => {
it('should return true', () => {
expect(isEqual(1, 1)).toBe(true);
});
it('should return false', () => {
expect(isEqual(1, 2)).toBe(false);
});
});
describe('#isFalse', () => {
it('should return true', () => {
expect(isFalse(false)).toBe(true);
});
it('should return false', () => {
expect(isFalse(1)).toBe(false);
});
});
describe('#isTrue', () => {
it('should return true', () => {
expect(isTrue(true)).toBe(true);
});
it('should return false', () => {
expect(isTrue(1)).toBe(false);
});
});
describe('#isIn', () => {
it('should return true', () => {
expect(isIn(1, [1, 2])).toBe(true);
});
it('should return false', () => {
expect(isIn(1, [])).toBe(false);
expect(isIn(1)).toBe(false);
});
});
describe('#isBoolean', () => {
it('should return true', () => {
for (const val of [true, false]) {
expect(isBoolean(val)).toBe(true);
}
});
it('should return false', () => {
for (const val of [1, null, undefined, {}, [], NaN]) {
expect(isBoolean(val)).toBe(false);
}
});
});
describe('#isNumeric', () => {
it('should return true', () => {
for (const val of [1, 0, '123', '-9.6']) {
expect(isNumeric(val)).toBe(true);
}
});
it('should return false', () => {
for (const val of ['', [], {}, null, undefined]) {
expect(isNumeric(val)).toBe(false);
}
});
});
describe('#isDateString', () => {
it('should return true', () => {
for (const val of [
'2019-06-19',
'2019-06-19T12:30:00',
'2019-06-19T12:30:00+0800',
'2019-06-19T12:30:00-08:00',
'2019-06-19T00:00:00Z',
]) {
expect(isDateString(val)).toBe(true);
}
});
it('should return false', () => {
for (const val of ['product-123123', 'CG-7', '20190619', [], {}, null, undefined]) {
expect(isDateString(val)).toBe(false);
}
});
});
describe('#isDate', () => {
it('should return true', () => {
for (const val of [new Date()]) {
expect(isDate(val)).toBe(true);
}
});
it('should return false', () => {
for (const val of ['2019-06-19', [], {}, null, undefined]) {
expect(isDate(val)).toBe(false);
}
});
});
describe('#isValue', () => {
it('should return true', () => {
for (const val of [new Date(), 'test', -1, 0, true, false]) {
expect(isValue(val)).toBe(true);
}
});
it('should return false', () => {
for (const val of ['', [], {}, null, undefined]) {
expect(isValue(val)).toBe(false);
}
});
});
describe('#hasValue', () => {
it('should return true', () => {
expect(hasValue([new Date(), 'test', -1, 0, true, false])).toBe(true);
expect(hasValue(false)).toBe(true);
});
it('should return false', () => {
expect(hasValue(['', [], {}, null, undefined])).toBe(false);
expect(hasValue(null)).toBe(false);
});
});
describe('#isFunction', () => {
it('should return true', () => {
expect(isFunction(Date)).toBe(true);
});
it('should return false', () => {
expect(isFunction(new Date())).toBe(false);
});
});
});
================================================
FILE: packages/util/test/obj.util.spec.ts
================================================
import { objKeys, getOwnPropNames } from '../src';
describe('#util', () => {
describe('#objKeys', () => {
it('should return array of strings', () => {
const obj = { foo: 1, bar: 1 };
const keys = ['foo', 'bar'];
expect(objKeys(obj)).toMatchObject(keys);
});
});
describe('#getOwnPropNames', () => {
it('should return own properties', () => {
class Parent {
foo = 1;
}
class Child extends Parent {
bar = 1;
}
const expected = ['foo', 'bar'];
expect(getOwnPropNames(new Child())).toMatchObject(expected);
});
});
});
================================================
FILE: packages/util/tsconfig.json
================================================
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib"
},
"include": ["src"],
"exclude": ["lib"]
}
================================================
FILE: tsconfig.eslint.json
================================================
{
"extends": "./tsconfig.json",
"include": ["packages"]
}
================================================
FILE: tsconfig.jest.json
================================================
{
"extends": "./tsconfig.json",
"compilerOptions": {
"removeComments": false
}
}
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"noImplicitAny": false,
"noUnusedLocals": false,
"removeComments": true,
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es2018",
"sourceMap": true,
"allowJs": false,
"skipLibCheck": false,
"composite": true,
"baseUrl": "./packages",
"paths": {
"@nestjsx/crud": ["crud/src"],
"@nestjsx/crud-typeorm": ["crud-typeorm/src"],
"@nestjsx/crud-request": ["crud-request/src"],
"@nestjsx/util": ["util/src"],
"@nestjsx/crud/*": ["crud/src/*"],
"@nestjsx/crud-typeorm/*": ["crud-typeorm/src/*"],
"@nestjsx/crud-request/*": ["crud-request/src/*"],
"@nestjsx/util/*": ["util/src/*"]
}
},
"references": [
{ "path": "crud" },
{ "path": "crud-typeorm" },
{ "path": "crud-request" },
{ "path": "util" }
]
}
================================================
FILE: tslint.json
================================================
{
"defaultSeverity": "error",
"extends": ["tslint:latest", "tslint-config-prettier"],
"jsRules": {},
"rules": {
"object-literal-sort-keys": false,
"member-access": false,
"no-implicit-dependencies": false,
"member-ordering": false,
"prefer-for-of": false,
"no-submodule-imports": false,
"interface-name": false
},
"rulesDirectory": []
}