From dbfca705b27da558d79d0ea29bd6a85c3f3d7f35 Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Thu, 9 Nov 2023 13:38:17 +0100 Subject: [PATCH 01/32] feat(rest): Created rest package --- examples/project.json | 22 ++- jest.preset.js | 3 +- package.json | 1 + packages/query-rest/.babelrc | 10 ++ packages/query-rest/.eslintrc.json | 36 +++++ packages/query-rest/README.md | 23 +++ packages/query-rest/jest.config.ts | 18 +++ packages/query-rest/package.json | 49 ++++++ packages/query-rest/project.json | 48 ++++++ packages/query-rest/src/auth/authorizer.ts | 47 ++++++ .../src/auth/default-crud.authorizer.ts | 99 ++++++++++++ packages/query-rest/src/auth/index.ts | 3 + packages/query-rest/src/auth/tokens.ts | 4 + packages/query-rest/src/common/dto.utils.ts | 35 ++++ packages/query-rest/src/common/index.ts | 3 + .../query-rest/src/common/object.utils.ts | 9 ++ .../query-rest/src/common/resolver.utils.ts | 19 +++ packages/query-rest/src/connection/index.ts | 1 + .../query-rest/src/connection/interfaces.ts | 58 +++++++ .../offset/offset-connection.type.ts | 59 +++++++ .../offset/offset-page-info.type.ts | 39 +++++ .../src/connection/offset/pager/index.ts | 2 + .../src/connection/offset/pager/interfaces.ts | 17 ++ .../src/connection/offset/pager/pager.ts | 83 ++++++++++ .../decorators/authorize-filter.decorator.ts | 91 +++++++++++ .../src/decorators/authorizer.decorator.ts | 24 +++ .../query-rest/src/decorators/constants.ts | 11 ++ .../controller-methods.decorator.ts | 148 +++++++++++++++++ .../src/decorators/decorator.utils.ts | 22 +++ .../src/decorators/field.decorator.ts | 92 +++++++++++ .../decorators/filterable-field.decorator.ts | 111 +++++++++++++ .../src/decorators/hook-args.decorator.ts | 69 ++++++++ .../src/decorators/hook.decorator.ts | 52 ++++++ packages/query-rest/src/decorators/index.ts | 12 ++ .../decorators/inject-authorizer.decorator.ts | 6 + .../src/decorators/query-options.decorator.ts | 18 +++ .../decorators/resolver-method.decorator.ts | 91 +++++++++++ .../decorators/resolver-query.decorator.ts | 5 + .../src/decorators/skip-if.decorator.ts | 14 ++ packages/query-rest/src/hooks/default.hook.ts | 13 ++ packages/query-rest/src/hooks/hooks.ts | 34 ++++ packages/query-rest/src/hooks/index.ts | 4 + packages/query-rest/src/hooks/tokens.ts | 5 + packages/query-rest/src/hooks/types.ts | 10 ++ packages/query-rest/src/index.ts | 7 + .../interceptors/authorizer.interceptor.ts | 29 ++++ .../src/interceptors/hook.interceptor.ts | 43 +++++ packages/query-rest/src/interceptors/index.ts | 2 + .../src/interfaces/return-type-func.ts | 6 + packages/query-rest/src/module.ts | 73 +++++++++ .../src/providers/authorizer.provider.ts | 34 ++++ .../query-rest/src/providers/hook.provider.ts | 49 ++++++ packages/query-rest/src/providers/index.ts | 1 + .../src/providers/resolver.provider.ts | 149 ++++++++++++++++++ .../src/resolvers/create.resolver.ts | 129 +++++++++++++++ .../query-rest/src/resolvers/crud.resolver.ts | 126 +++++++++++++++ .../src/resolvers/delete.resolver.ts | 72 +++++++++ packages/query-rest/src/resolvers/index.ts | 6 + .../query-rest/src/resolvers/read.resolver.ts | 149 ++++++++++++++++++ .../src/resolvers/resolver.interface.ts | 62 ++++++++ .../src/resolvers/update.resolver.ts | 105 ++++++++++++ .../src/types/create-one-input.type.ts | 30 ++++ packages/query-rest/src/types/index.ts | 6 + .../src/types/mutation-args.type.ts | 18 +++ .../query-rest/src/types/query-args.type.ts | 48 ++++++ .../src/types/query/buildable-query.type.ts | 5 + packages/query-rest/src/types/query/index.ts | 3 + .../src/types/query/offset-paging.type.ts | 28 ++++ .../src/types/query/paging/constants.ts | 5 + .../src/types/query/paging/index.ts | 3 + .../src/types/query/paging/interfaces.ts | 14 ++ .../types/query/paging/none-paging.type.ts | 19 +++ .../src/types/query/query-args/constants.ts | 6 + .../src/types/query/query-args/index.ts | 2 + .../src/types/query/query-args/interfaces.ts | 61 +++++++ .../query-args/offset-query-args.type.ts | 57 +++++++ .../query-rest/src/types/rest-query.type.ts | 5 + .../src/types/update-one-input.type.ts | 32 ++++ .../query-rest/src/types/validators/index.ts | 1 + .../validators/is-undefined.validator.ts | 8 + packages/query-rest/tsconfig.json | 13 ++ packages/query-rest/tsconfig.lib.json | 20 +++ packages/query-rest/tsconfig.spec.json | 23 +++ tsconfig.json | 3 +- yarn.lock | 72 +++++++++ 85 files changed, 3038 insertions(+), 6 deletions(-) create mode 100644 packages/query-rest/.babelrc create mode 100644 packages/query-rest/.eslintrc.json create mode 100644 packages/query-rest/README.md create mode 100644 packages/query-rest/jest.config.ts create mode 100644 packages/query-rest/package.json create mode 100644 packages/query-rest/project.json create mode 100644 packages/query-rest/src/auth/authorizer.ts create mode 100644 packages/query-rest/src/auth/default-crud.authorizer.ts create mode 100644 packages/query-rest/src/auth/index.ts create mode 100644 packages/query-rest/src/auth/tokens.ts create mode 100644 packages/query-rest/src/common/dto.utils.ts create mode 100644 packages/query-rest/src/common/index.ts create mode 100644 packages/query-rest/src/common/object.utils.ts create mode 100644 packages/query-rest/src/common/resolver.utils.ts create mode 100644 packages/query-rest/src/connection/index.ts create mode 100644 packages/query-rest/src/connection/interfaces.ts create mode 100644 packages/query-rest/src/connection/offset/offset-connection.type.ts create mode 100644 packages/query-rest/src/connection/offset/offset-page-info.type.ts create mode 100644 packages/query-rest/src/connection/offset/pager/index.ts create mode 100644 packages/query-rest/src/connection/offset/pager/interfaces.ts create mode 100644 packages/query-rest/src/connection/offset/pager/pager.ts create mode 100644 packages/query-rest/src/decorators/authorize-filter.decorator.ts create mode 100644 packages/query-rest/src/decorators/authorizer.decorator.ts create mode 100644 packages/query-rest/src/decorators/constants.ts create mode 100644 packages/query-rest/src/decorators/controller-methods.decorator.ts create mode 100644 packages/query-rest/src/decorators/decorator.utils.ts create mode 100644 packages/query-rest/src/decorators/field.decorator.ts create mode 100644 packages/query-rest/src/decorators/filterable-field.decorator.ts create mode 100644 packages/query-rest/src/decorators/hook-args.decorator.ts create mode 100644 packages/query-rest/src/decorators/hook.decorator.ts create mode 100644 packages/query-rest/src/decorators/index.ts create mode 100644 packages/query-rest/src/decorators/inject-authorizer.decorator.ts create mode 100644 packages/query-rest/src/decorators/query-options.decorator.ts create mode 100644 packages/query-rest/src/decorators/resolver-method.decorator.ts create mode 100644 packages/query-rest/src/decorators/resolver-query.decorator.ts create mode 100644 packages/query-rest/src/decorators/skip-if.decorator.ts create mode 100644 packages/query-rest/src/hooks/default.hook.ts create mode 100644 packages/query-rest/src/hooks/hooks.ts create mode 100644 packages/query-rest/src/hooks/index.ts create mode 100644 packages/query-rest/src/hooks/tokens.ts create mode 100644 packages/query-rest/src/hooks/types.ts create mode 100644 packages/query-rest/src/index.ts create mode 100644 packages/query-rest/src/interceptors/authorizer.interceptor.ts create mode 100644 packages/query-rest/src/interceptors/hook.interceptor.ts create mode 100644 packages/query-rest/src/interceptors/index.ts create mode 100644 packages/query-rest/src/interfaces/return-type-func.ts create mode 100644 packages/query-rest/src/module.ts create mode 100644 packages/query-rest/src/providers/authorizer.provider.ts create mode 100644 packages/query-rest/src/providers/hook.provider.ts create mode 100644 packages/query-rest/src/providers/index.ts create mode 100644 packages/query-rest/src/providers/resolver.provider.ts create mode 100644 packages/query-rest/src/resolvers/create.resolver.ts create mode 100644 packages/query-rest/src/resolvers/crud.resolver.ts create mode 100644 packages/query-rest/src/resolvers/delete.resolver.ts create mode 100644 packages/query-rest/src/resolvers/index.ts create mode 100644 packages/query-rest/src/resolvers/read.resolver.ts create mode 100644 packages/query-rest/src/resolvers/resolver.interface.ts create mode 100644 packages/query-rest/src/resolvers/update.resolver.ts create mode 100644 packages/query-rest/src/types/create-one-input.type.ts create mode 100644 packages/query-rest/src/types/index.ts create mode 100644 packages/query-rest/src/types/mutation-args.type.ts create mode 100644 packages/query-rest/src/types/query-args.type.ts create mode 100644 packages/query-rest/src/types/query/buildable-query.type.ts create mode 100644 packages/query-rest/src/types/query/index.ts create mode 100644 packages/query-rest/src/types/query/offset-paging.type.ts create mode 100644 packages/query-rest/src/types/query/paging/constants.ts create mode 100644 packages/query-rest/src/types/query/paging/index.ts create mode 100644 packages/query-rest/src/types/query/paging/interfaces.ts create mode 100644 packages/query-rest/src/types/query/paging/none-paging.type.ts create mode 100644 packages/query-rest/src/types/query/query-args/constants.ts create mode 100644 packages/query-rest/src/types/query/query-args/index.ts create mode 100644 packages/query-rest/src/types/query/query-args/interfaces.ts create mode 100644 packages/query-rest/src/types/query/query-args/offset-query-args.type.ts create mode 100644 packages/query-rest/src/types/rest-query.type.ts create mode 100644 packages/query-rest/src/types/update-one-input.type.ts create mode 100644 packages/query-rest/src/types/validators/index.ts create mode 100644 packages/query-rest/src/types/validators/is-undefined.validator.ts create mode 100644 packages/query-rest/tsconfig.json create mode 100644 packages/query-rest/tsconfig.lib.json create mode 100644 packages/query-rest/tsconfig.spec.json diff --git a/examples/project.json b/examples/project.json index d0e922186..073658d84 100644 --- a/examples/project.json +++ b/examples/project.json @@ -6,14 +6,20 @@ "targets": { "lint": { "executor": "@nx/eslint:lint", - "outputs": ["{options.outputFile}"], + "outputs": [ + "{options.outputFile}" + ], "options": { - "lintFilePatterns": ["examples/**/*.ts"] + "lintFilePatterns": [ + "examples/**/*.ts" + ] } }, "e2e": { "executor": "@nx/jest:jest", - "outputs": ["{workspaceRoot}/coverage/examples"], + "outputs": [ + "{workspaceRoot}/coverage/examples" + ], "options": { "jestConfig": "jest.e2e.ts", "runInBand": true @@ -21,5 +27,13 @@ } }, "tags": [], - "implicitDependencies": ["core", "query-graphql", "query-mongoose", "query-sequelize", "query-typegoose", "query-typeorm"] + "implicitDependencies": [ + "core", + "query-graphql", + "query-mongoose", + "query-sequelize", + "query-typegoose", + "query-typeorm", + "query-rest" + ] } diff --git a/jest.preset.js b/jest.preset.js index 27385b49a..ff813b023 100644 --- a/jest.preset.js +++ b/jest.preset.js @@ -19,7 +19,8 @@ module.exports = { '@ptc-org/nestjs-query-typeorm': process.cwd() + '/packages/query-typeorm/src', '@ptc-org/nestjs-query-sequelize': process.cwd() + '/packages/query-sequelize/src', '@ptc-org/nestjs-query-typegoose': process.cwd() + '/packages/query-typegoose/src', - '@ptc-org/nestjs-query-mongoose': process.cwd() + '/packages/query-mongoose/src' + '@ptc-org/nestjs-query-mongoose': process.cwd() + '/packages/query-mongoose/src', + '@ptc-org/nestjs-query-rest': process.cwd() + '/packages/query-rest/src' }, testEnvironment: 'node', setupFilesAfterEnv: ['jest-extended'], diff --git a/package.json b/package.json index d99faba4f..344a94c49 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@nestjs/passport": "10.0.2", "@nestjs/platform-express": "10.2.7", "@nestjs/sequelize": "10.0.0", + "@nestjs/swagger": "^7.1.15", "@nestjs/typeorm": "^10.0.0", "class-validator": "0.14.0", "clsx": "^2.0.0", diff --git a/packages/query-rest/.babelrc b/packages/query-rest/.babelrc new file mode 100644 index 000000000..e24a5465f --- /dev/null +++ b/packages/query-rest/.babelrc @@ -0,0 +1,10 @@ +{ + "presets": [ + [ + "@nrwl/web/babel", + { + "useBuiltIns": "usage" + } + ] + ] +} diff --git a/packages/query-rest/.eslintrc.json b/packages/query-rest/.eslintrc.json new file mode 100644 index 000000000..10e609fcf --- /dev/null +++ b/packages/query-rest/.eslintrc.json @@ -0,0 +1,36 @@ +{ + "extends": [ + "../../.eslintrc.json" + ], + "ignorePatterns": [ + "!**/*" + ], + "parserOptions": { + "project": "./tsconfig.json" + }, + "overrides": [ + { + "files": [ + "*.ts", + "*.tsx", + "*.js", + "*.jsx" + ], + "rules": {} + }, + { + "files": [ + "*.ts", + "*.tsx" + ], + "rules": {} + }, + { + "files": [ + "*.js", + "*.jsx" + ], + "rules": {} + } + ] +} diff --git a/packages/query-rest/README.md b/packages/query-rest/README.md new file mode 100644 index 000000000..83e5cbbe1 --- /dev/null +++ b/packages/query-rest/README.md @@ -0,0 +1,23 @@ +

+ Nestjs-query Logo +

+ +[![npm version](https://img.shields.io/npm/v/@ptc-org/nestjs-query-rest.svg)](https://www.npmjs.org/package/@ptc-org/nestjs-query-rest) +[![Test](https://github.com/tripss/nestjs-query/workflows/Test/badge.svg?branch=master)](https://github.com/tripss/nestjs-query/actions?query=workflow%3ATest+and+branch%3Amaster+) +[![Coverage Status](https://codecov.io/gh/TriPSs/nestjs-query/branch/master/graph/badge.svg?token=29EX71ID2P)](https://codecov.io/gh/TriPSs/nestjs-query) +[![Known Vulnerabilities](https://snyk.io/test/github/tripss/nestjs-query/badge.svg?targetFile=packages/query-rest/package.json)](https://snyk.io/test/github/tripss/nestjs-query?targetFile=packages/query-rest/package.json) + +# `@ptc-org/nestjs-query-rest` + +This package provides a code first implementation of rest CRUD endpoints. It is built on top of +of [nestjs](https://nestjs.com/). + +## Installation + +[Install Guide](https://tripss.github.io/nestjs-query/docs/introduction/install) + +## Getting Started + +The get started with the `@ptc-org/nestjs-query-rest` package checkout +the [Getting Started](https://tripss.github.io/nestjs-query/docs/rest/getting-started) docs. + diff --git a/packages/query-rest/jest.config.ts b/packages/query-rest/jest.config.ts new file mode 100644 index 000000000..1b27dbfb9 --- /dev/null +++ b/packages/query-rest/jest.config.ts @@ -0,0 +1,18 @@ +/* eslint-disable */ +// eslint-disable-next-line import/no-default-export +export default { + displayName: 'query-rest', + preset: '../../jest.preset.js', + globals: {}, + testEnvironment: 'node', + transform: { + '^.+\\.[tj]sx?$': [ + 'ts-jest', + { + tsconfig: '/tsconfig.spec.json' + } + ] + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../coverage/packages/query-rest' +} diff --git a/packages/query-rest/package.json b/packages/query-rest/package.json new file mode 100644 index 000000000..8bce4526e --- /dev/null +++ b/packages/query-rest/package.json @@ -0,0 +1,49 @@ +{ + "name": "@ptc-org/nestjs-query-rest", + "version": "4.3.0", + "description": "Nestjs rest query adapter", + "author": "doug-martin ", + "homepage": "https://github.com/tripss/nestjs-query#readme", + "keywords": [ + "reset", + "crud", + "nestjs" + ], + "license": "MIT", + "main": "src/index.js", + "types": "src/index.d.ts", + "directories": { + "lib": "src", + "test": "__tests__" + }, + "files": [ + "src/**" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tripss/nestjs-query.git", + "directory": "packages/query-rest" + }, + "bugs": { + "url": "https://github.com/tripss/nestjs-query/issues" + }, + "dependencies": { + "lodash.omit": "^4.5.0", + "lower-case-first": "^2.0.2", + "pluralize": "^8.0.0", + "tslib": "^2.6.2", + "upper-case-first": "^2.0.2" + }, + "peerDependencies": { + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0", + "@nestjs/graphql": "^11.0.0 || ^12.0.0", + "@nestjs/swagger": "^7.0.0", + "class-transformer": "^0.5", + "class-validator": "^0.14.0", + "ts-morph": "^19.0.0" + } +} diff --git a/packages/query-rest/project.json b/packages/query-rest/project.json new file mode 100644 index 000000000..b29c34864 --- /dev/null +++ b/packages/query-rest/project.json @@ -0,0 +1,48 @@ +{ + "name": "query-rest", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/query-rest/src", + "projectType": "library", + "targets": { + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["packages/query-rest/**/*.ts"] + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/packages/query-rest"], + "options": { + "jestConfig": "packages/query-rest/jest.config.ts", + "passWithNoTests": true + } + }, + "build": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/packages/query-rest", + "tsConfig": "packages/query-rest/tsconfig.lib.json", + "packageJson": "packages/query-rest/package.json", + "main": "packages/query-rest/src/index.ts", + "assets": ["packages/query-rest/*.md"], + "updateBuildableProjectDepsInPackageJson": true, + "buildableProjectDepsInPackageJsonType": "dependencies" + } + }, + "version": { + "executor": "@jscutlery/semver:version", + "options": {} + }, + "publish": { + "executor": "nx:run-commands", + "options": { + "command": "npm publish ./dist/packages/query-rest --access public" + } + } + }, + "tags": [], + "implicitDependencies": ["core"] +} diff --git a/packages/query-rest/src/auth/authorizer.ts b/packages/query-rest/src/auth/authorizer.ts new file mode 100644 index 000000000..06f57dcc7 --- /dev/null +++ b/packages/query-rest/src/auth/authorizer.ts @@ -0,0 +1,47 @@ +import { Filter } from '@ptc-org/nestjs-query-core' + +export enum OperationGroup { + READ = 'read', + AGGREGATE = 'aggregate', + CREATE = 'create', + UPDATE = 'update', + DELETE = 'delete' +} + +export interface AuthorizationContext { + /** The name of the method that uses the @AuthorizeFilter decorator */ + readonly operationName: string + + /** The group this operation belongs to */ + readonly operationGroup: OperationGroup + + /** If the operation does not modify any entities */ + readonly readonly: boolean + + /** If the operation can affect multiple entities */ + readonly many: boolean +} + +export interface CustomAuthorizer { + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types,@typescript-eslint/no-explicit-any + authorize(context: any, authorizerContext: AuthorizationContext): Promise> + + authorizeRelation?( + relationName: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + context: any, + authorizerContext: AuthorizationContext + ): Promise | undefined> +} + +export interface Authorizer extends CustomAuthorizer { + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types,@typescript-eslint/no-explicit-any + authorize(context: any, authorizerContext: AuthorizationContext): Promise> + + authorizeRelation( + relationName: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + context: any, + authorizerContext: AuthorizationContext + ): Promise | undefined> +} diff --git a/packages/query-rest/src/auth/default-crud.authorizer.ts b/packages/query-rest/src/auth/default-crud.authorizer.ts new file mode 100644 index 000000000..2392ce2c5 --- /dev/null +++ b/packages/query-rest/src/auth/default-crud.authorizer.ts @@ -0,0 +1,99 @@ +import { Inject, Injectable, Optional } from '@nestjs/common' +import { ModuleRef } from '@nestjs/core' +import { Class, Filter } from '@ptc-org/nestjs-query-core' + +// import { getAuthorizer } from '../decorators' +// import { ResolverRelation } from '../resolvers/relations' +import { AuthorizationContext, Authorizer, CustomAuthorizer } from './authorizer' +import { getCustomAuthorizerToken } from './tokens' + +export interface AuthorizerOptions { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + authorize: (context: any, authorizationContext: AuthorizationContext) => Filter | Promise> +} + +// const createRelationAuthorizer = (opts: AuthorizerOptions): Authorizer => ({ +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// async authorize(context: any, authorizationContext: AuthorizationContext): Promise> { +// return opts.authorize(context, authorizationContext) ?? {} +// }, +// authorizeRelation(): Promise> { +// return Promise.reject(new Error('Not implemented')) +// } +// }) + +export function createDefaultAuthorizer( + DTOClass: Class, + opts?: CustomAuthorizer | AuthorizerOptions // instance of class or authorizer options +): Class> { + @Injectable() + class DefaultAuthorizer implements Authorizer { + readonly authOptions?: AuthorizerOptions | CustomAuthorizer = opts + + readonly relationsAuthorizers: Map | undefined> + + // private readonly relations: Map> + + constructor( + private readonly moduleRef: ModuleRef, + @Optional() @Inject(getCustomAuthorizerToken(DTOClass)) private readonly customAuthorizer?: CustomAuthorizer + ) { + this.relationsAuthorizers = new Map | undefined>() + // this.relations = this.getRelations() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public async authorize(context: any, authorizationContext: AuthorizationContext): Promise> { + return ( + this.customAuthorizer?.authorize(context, authorizationContext) ?? + this.authOptions?.authorize(context, authorizationContext) ?? + {} + ) + } + + public async authorizeRelation( + relationName: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + context: any, + authorizationContext: AuthorizationContext + ): Promise> { + if (this.customAuthorizer && typeof this.customAuthorizer.authorizeRelation === 'function') { + const filterFromCustomAuthorizer = await this.customAuthorizer.authorizeRelation( + relationName, + context, + authorizationContext + ) + + if (filterFromCustomAuthorizer) { + return filterFromCustomAuthorizer + } + } + return {} + // this.addRelationAuthorizerIfNotExist(relationName) + // return this.relationsAuthorizers.get(relationName)?.authorize(context, authorizationContext) ?? {} + } + + private addRelationAuthorizerIfNotExist(relationName: string) { + if (!this.relationsAuthorizers.has(relationName)) { + return + // const relation = this.relations.get(relationName) + // if (!relation) return + // if (relation.auth) { + // this.relationsAuthorizers.set(relationName, createRelationAuthorizer(relation.auth)) + // } else if (getAuthorizer(relation.DTO)) { + // this.relationsAuthorizers.set(relationName, this.moduleRef.get(getAuthorizerToken(relation.DTO), { strict: false })) + // } + } + } + + // private getRelations(): Map> { + // const { many = {}, one = {} } = {}// getRelations(DTOClass) + // const relationsMap = new Map>() + // Object.keys(many).forEach((relation) => relationsMap.set(relation, many[relation])) + // Object.keys(one).forEach((relation) => relationsMap.set(relation, one[relation])) + // return relationsMap + // } + } + + return DefaultAuthorizer +} diff --git a/packages/query-rest/src/auth/index.ts b/packages/query-rest/src/auth/index.ts new file mode 100644 index 000000000..0081c7a62 --- /dev/null +++ b/packages/query-rest/src/auth/index.ts @@ -0,0 +1,3 @@ +export * from './authorizer' +export * from './default-crud.authorizer' +export * from './tokens' diff --git a/packages/query-rest/src/auth/tokens.ts b/packages/query-rest/src/auth/tokens.ts new file mode 100644 index 000000000..7617f969b --- /dev/null +++ b/packages/query-rest/src/auth/tokens.ts @@ -0,0 +1,4 @@ +import { Class } from '@ptc-org/nestjs-query-core' + +export const getAuthorizerToken = (DTOClass: Class): string => `${DTOClass.name}Authorizer` +export const getCustomAuthorizerToken = (DTOClass: Class): string => `${DTOClass.name}CustomAuthorizer` diff --git a/packages/query-rest/src/common/dto.utils.ts b/packages/query-rest/src/common/dto.utils.ts new file mode 100644 index 000000000..8f4133892 --- /dev/null +++ b/packages/query-rest/src/common/dto.utils.ts @@ -0,0 +1,35 @@ +import { Class } from '@ptc-org/nestjs-query-core' +import { lowerCaseFirst } from 'lower-case-first' +import { plural } from 'pluralize' +import { upperCaseFirst } from 'upper-case-first' + +export interface DTONamesOpts { + dtoName?: string +} + +/** @internal */ +export interface DTONames { + baseName: string + baseNameLower: string + pluralBaseName: string + pluralBaseNameLower: string + endpointName: string +} + +const kebabize = (str: string) => str.replace(/[A-Z]+(?![a-z])|[A-Z]/g, ($, ofs) => (ofs ? '-' : '') + $.toLowerCase()) + +/** @internal */ +export const getDTONames = (DTOClass: Class, opts?: DTONamesOpts): DTONames => { + const baseName = upperCaseFirst(opts?.dtoName ?? DTOClass.name) + const pluralBaseName = plural(baseName) + const baseNameLower = lowerCaseFirst(baseName) + const pluralBaseNameLower = plural(baseNameLower) + + return { + baseName, + baseNameLower, + pluralBaseName, + pluralBaseNameLower, + endpointName: kebabize(pluralBaseName) + } +} diff --git a/packages/query-rest/src/common/index.ts b/packages/query-rest/src/common/index.ts new file mode 100644 index 000000000..c61c52166 --- /dev/null +++ b/packages/query-rest/src/common/index.ts @@ -0,0 +1,3 @@ +export * from './dto.utils' +export * from './object.utils' +export * from './resolver.utils' diff --git a/packages/query-rest/src/common/object.utils.ts b/packages/query-rest/src/common/object.utils.ts new file mode 100644 index 000000000..7cadf891b --- /dev/null +++ b/packages/query-rest/src/common/object.utils.ts @@ -0,0 +1,9 @@ +export const removeUndefinedValues = (obj: T): T => { + const keys = Object.keys(obj) as (keyof T)[] + return keys.reduce((cleansed: T, key) => { + if (obj[key] === undefined) { + return cleansed + } + return { ...cleansed, [key]: obj[key] } + }, {} as T) +} diff --git a/packages/query-rest/src/common/resolver.utils.ts b/packages/query-rest/src/common/resolver.utils.ts new file mode 100644 index 000000000..71e850201 --- /dev/null +++ b/packages/query-rest/src/common/resolver.utils.ts @@ -0,0 +1,19 @@ +import { BaseResolverOptions } from '../decorators' + +const mergeArrays = (arr1?: T[], arr2?: T[]): T[] | undefined => { + if (arr1 || arr2) { + return [...(arr1 ?? []), ...(arr2 ?? [])] + } + return undefined +} + +export const mergeBaseResolverOpts = (into: Into, from: BaseResolverOptions): Into => { + const guards = mergeArrays(from.guards, into.guards) + const interceptors = mergeArrays(from.interceptors, into.interceptors) + const pipes = mergeArrays(from.pipes, into.pipes) + const filters = mergeArrays(from.filters, into.filters) + const decorators = mergeArrays(from.decorators, into.decorators) + const tags = mergeArrays(from.tags, into.tags) + + return { ...into, tags, guards, interceptors, pipes, filters, decorators } +} diff --git a/packages/query-rest/src/connection/index.ts b/packages/query-rest/src/connection/index.ts new file mode 100644 index 000000000..d92fd9c9d --- /dev/null +++ b/packages/query-rest/src/connection/index.ts @@ -0,0 +1 @@ +export { ArrayConnectionType, ConnectionType, OffsetConnectionType, OffsetPageInfoType } from './interfaces' diff --git a/packages/query-rest/src/connection/interfaces.ts b/packages/query-rest/src/connection/interfaces.ts new file mode 100644 index 000000000..bf585ee09 --- /dev/null +++ b/packages/query-rest/src/connection/interfaces.ts @@ -0,0 +1,58 @@ +import { Class, Filter, Query } from '@ptc-org/nestjs-query-core' + +import { PagingStrategies } from '../types' + +interface BaseConnectionOptions { + enableTotalCount?: boolean + connectionName?: string +} + +export interface OffsetConnectionOptions extends BaseConnectionOptions { + pagingStrategy?: PagingStrategies.OFFSET +} + +export interface ArrayConnectionOptions extends BaseConnectionOptions { + pagingStrategy?: PagingStrategies.NONE +} + +export type ConnectionOptions = OffsetConnectionOptions | ArrayConnectionOptions + +export interface OffsetPageInfoType { + hasNextPage: boolean + hasPreviousPage: boolean +} + +export type OffsetConnectionType = { + pageInfo: OffsetPageInfoType + totalCount?: number + nodes: DTO[] +} + +export type ArrayConnectionType = DTO[] + +export type ConnectionType = OffsetConnectionType | ArrayConnectionType + +export type QueryMany> = (query: Q) => Promise +export type Count = (filter: Filter) => Promise + +export type PagerResult = { + totalCount?: number +} + +export interface Pager { + page>(queryMany: QueryMany, query: Q, count?: Count): Promise +} + +export type InferConnectionTypeFromStrategy = S extends PagingStrategies.NONE + ? ArrayConnectionType + : S extends PagingStrategies.OFFSET + ? OffsetConnectionType + : never + +export interface StaticConnectionType extends Class> { + createFromPromise>( + queryMany: QueryMany, + query: Q, + count?: Count + ): Promise> +} diff --git a/packages/query-rest/src/connection/offset/offset-connection.type.ts b/packages/query-rest/src/connection/offset/offset-connection.type.ts new file mode 100644 index 000000000..0d030c813 --- /dev/null +++ b/packages/query-rest/src/connection/offset/offset-connection.type.ts @@ -0,0 +1,59 @@ +import { Class, MapReflector, Query } from '@ptc-org/nestjs-query-core' +import { plainToInstance } from 'class-transformer' + +import { Field } from '../../decorators' +import { OffsetQueryArgsTypeOpts, PagingStrategies } from '../../types' +import { Count, OffsetConnectionType, OffsetPageInfoType, QueryMany, StaticConnectionType } from '../interfaces' +import { getOrCreateOffsetPageInfoType } from './offset-page-info.type' +import { OffsetPager } from './pager' + +const reflector = new MapReflector('nestjs-query:offset-connection-type') + +export function getOrCreateOffsetConnectionType( + TItemClass: Class, + opts: OffsetQueryArgsTypeOpts +): StaticConnectionType { + const connectionName = opts?.connectionName || `${TItemClass.name}OffsetConnection` + + return reflector.memoize(TItemClass, connectionName, () => { + const PIT = getOrCreateOffsetPageInfoType() + const pager = new OffsetPager() + + class OffsetConnection implements OffsetConnectionType { + public static async createFromPromise>( + queryMany: QueryMany, + query: Query, + count?: Count + ): Promise { + const { pageInfo, nodes, totalCount } = await pager.page(queryMany, query, count) + + return new OffsetConnection(new PIT(pageInfo.hasNextPage, pageInfo.hasPreviousPage), nodes, totalCount) + } + + constructor(pageInfo?: OffsetPageInfoType, nodes?: DTO[], totalCount?: number) { + this.pageInfo = pageInfo ?? { hasNextPage: false, hasPreviousPage: false } + this.nodes = plainToInstance(TItemClass, nodes ?? [], { excludeExtraneousValues: true }) + this.totalCount = totalCount + } + + @Field(() => PIT, { + description: 'Paging information' + }) + public pageInfo!: OffsetPageInfoType + + @Field({ + description: 'Total amount of records.' + }) + public totalCount?: number + + @Field(() => [TItemClass], { + description: 'Array of nodes.' + }) + public nodes!: DTO[] + } + + Object.defineProperty(OffsetConnection, 'name', { value: connectionName, writable: false }) + + return OffsetConnection + }) +} diff --git a/packages/query-rest/src/connection/offset/offset-page-info.type.ts b/packages/query-rest/src/connection/offset/offset-page-info.type.ts new file mode 100644 index 000000000..23241c325 --- /dev/null +++ b/packages/query-rest/src/connection/offset/offset-page-info.type.ts @@ -0,0 +1,39 @@ +import { Class } from '@ptc-org/nestjs-query-core' + +import { Field } from '../../decorators' +import { OffsetPageInfoType } from '../interfaces' + +export interface OffsetPageInfoTypeConstructor { + new (hasNextPage: boolean, hasPreviousPage: boolean): OffsetPageInfoType +} + +/** @internal */ +let pageInfoType: Class | null = null +// eslint-disable-next-line @typescript-eslint/no-redeclare -- intentional +export const getOrCreateOffsetPageInfoType = (): OffsetPageInfoTypeConstructor => { + if (pageInfoType) { + return pageInfoType + } + + class PageInfoType implements OffsetPageInfoType { + constructor(hasNextPage: boolean, hasPreviousPage: boolean) { + this.hasNextPage = hasNextPage + this.hasPreviousPage = hasPreviousPage + } + + @Field({ + description: 'true if paging forward and there are more records.', + nullable: true + }) + hasNextPage: boolean + + @Field({ + description: 'true if paging backwards and there are more records.', + nullable: true + }) + hasPreviousPage: boolean + } + + pageInfoType = PageInfoType + return pageInfoType +} diff --git a/packages/query-rest/src/connection/offset/pager/index.ts b/packages/query-rest/src/connection/offset/pager/index.ts new file mode 100644 index 000000000..aa7e1409d --- /dev/null +++ b/packages/query-rest/src/connection/offset/pager/index.ts @@ -0,0 +1,2 @@ +export { OffsetPagerResult } from './interfaces' +export { OffsetPager } from './pager' diff --git a/packages/query-rest/src/connection/offset/pager/interfaces.ts b/packages/query-rest/src/connection/offset/pager/interfaces.ts new file mode 100644 index 000000000..6aef51884 --- /dev/null +++ b/packages/query-rest/src/connection/offset/pager/interfaces.ts @@ -0,0 +1,17 @@ +import { Paging, Query } from '@ptc-org/nestjs-query-core' + +import { OffsetConnectionType, PagerResult } from '../../interfaces' + +export type OffsetPagingOpts = Required + +export interface OffsetPagingMeta { + opts: OffsetPagingOpts + query: Query +} + +export interface QueryResults { + nodes: DTO[] + hasExtraNode: boolean +} + +export type OffsetPagerResult = PagerResult & Omit, 'totalCount'> diff --git a/packages/query-rest/src/connection/offset/pager/pager.ts b/packages/query-rest/src/connection/offset/pager/pager.ts new file mode 100644 index 000000000..63a088318 --- /dev/null +++ b/packages/query-rest/src/connection/offset/pager/pager.ts @@ -0,0 +1,83 @@ +import { Filter, Query } from '@ptc-org/nestjs-query-core' + +import { Count, Pager, QueryMany } from '../../interfaces' +import { OffsetPagerResult, OffsetPagingMeta, OffsetPagingOpts, QueryResults } from './interfaces' + +const EMPTY_PAGING_RESULTS = (): OffsetPagerResult => ({ + nodes: [], + pageInfo: { hasNextPage: false, hasPreviousPage: false }, + totalCount: undefined +}) + +const DEFAULT_PAGING_META = (query: Query): OffsetPagingMeta => ({ + opts: { offset: 0, limit: 0 }, + query +}) + +export class OffsetPager implements Pager> { + public async page>( + queryMany: QueryMany, + query: Q, + count?: Count + ): Promise> { + const pagingMeta = this.getPageMeta(query) + if (!this.isValidPaging(pagingMeta)) { + return EMPTY_PAGING_RESULTS() + } + const results = await this.runQuery(queryMany, query, pagingMeta) + return this.createPagingResult(results, pagingMeta, await count?.(query.filter)) + } + + private isValidPaging(pagingMeta: OffsetPagingMeta): boolean { + return pagingMeta.opts.limit > 0 + } + + private async runQuery>( + queryMany: QueryMany, + query: Q, + pagingMeta: OffsetPagingMeta + ): Promise> { + const windowedQuery = this.createQuery(query, pagingMeta) + const nodes = await queryMany(windowedQuery) + const returnNodes = this.checkForExtraNode(nodes, pagingMeta.opts) + const hasExtraNode = returnNodes.length !== nodes.length + return { nodes: returnNodes, hasExtraNode } + } + + private getPageMeta(query: Query): OffsetPagingMeta { + const { limit = 25, offset = 0 } = query.paging ?? {} + if (!limit) { + return DEFAULT_PAGING_META(query) + } + return { opts: { limit, offset }, query } + } + + private createPagingResult( + results: QueryResults, + pagingMeta: OffsetPagingMeta, + totalCount?: number + ): OffsetPagerResult { + const { nodes, hasExtraNode } = results + const pageInfo = { + hasNextPage: hasExtraNode, + // we have a previous page if we are going backwards and have an extra node. + hasPreviousPage: pagingMeta.opts.offset > 0 + } + + return { nodes, pageInfo, totalCount } + } + + private createQuery>(query: Q, pagingMeta: OffsetPagingMeta): Q { + const { limit, offset } = pagingMeta.opts + return { ...query, paging: { limit: limit + 1, offset } } + } + + private checkForExtraNode(nodes: DTO[], opts: OffsetPagingOpts): DTO[] { + const returnNodes = [...nodes] + const hasExtraNode = nodes.length > opts.limit + if (hasExtraNode) { + returnNodes.pop() + } + return returnNodes + } +} diff --git a/packages/query-rest/src/decorators/authorize-filter.decorator.ts b/packages/query-rest/src/decorators/authorize-filter.decorator.ts new file mode 100644 index 000000000..1bceeec4e --- /dev/null +++ b/packages/query-rest/src/decorators/authorize-filter.decorator.ts @@ -0,0 +1,91 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common' + +import { AuthorizationContext, OperationGroup } from '../auth' +import { AuthorizerContext } from '../interceptors' + +type PartialAuthorizationContext = Partial & Pick + +function getContext(executionContext: ExecutionContext): C { + return executionContext.switchToHttp().getRequest() +} + +function getAuthorizerFilter>(context: C, authorizationContext: AuthorizationContext) { + if (!context.authorizer) { + return undefined + } + return context.authorizer.authorize(context, authorizationContext) +} + +// function getRelationAuthFilter>( +// context: C, +// relationName: string, +// authorizationContext: AuthorizationContext +// ) { +// if (!context.authorizer) { +// return undefined +// } +// return context.authorizer.authorizeRelation(relationName, context, authorizationContext) +// } + +function getAuthorizationContext( + methodName: string | symbol, + partialAuthContext?: PartialAuthorizationContext +): AuthorizationContext { + if (!partialAuthContext) + return new Proxy({} as AuthorizationContext, { + get: () => { + throw new Error( + `No AuthorizationContext available for method ${methodName.toString()}! Make sure that you provide an AuthorizationContext to your custom methods as argument of the @AuthorizerFilter decorator.` + ) + } + }) + + return { + operationName: methodName.toString(), + readonly: + partialAuthContext.operationGroup === OperationGroup.READ || partialAuthContext.operationGroup === OperationGroup.AGGREGATE, + ...partialAuthContext + } +} + +export function AuthorizerFilter(partialAuthContext?: PartialAuthorizationContext): ParameterDecorator { + // eslint-disable-next-line @typescript-eslint/ban-types + return (target: Object, propertyKey: string | symbol, parameterIndex: number) => { + const authorizationContext = getAuthorizationContext(propertyKey, partialAuthContext) + return createParamDecorator((data: unknown, executionContext: ExecutionContext) => + getAuthorizerFilter(getContext>(executionContext), authorizationContext) + )()(target, propertyKey, parameterIndex) + } +} + +// export function RelationAuthorizerFilter( +// relationName: string, +// partialAuthContext?: PartialAuthorizationContext +// ): ParameterDecorator { +// // eslint-disable-next-line @typescript-eslint/ban-types +// return (target: Object, propertyKey: string | symbol, parameterIndex: number) => { +// const authorizationContext = getAuthorizationContext(propertyKey, partialAuthContext) +// return createParamDecorator((data: unknown, executionContext: ExecutionContext) => +// getRelationAuthFilter(getContext>(executionContext), relationName, authorizationContext) +// )()(target, propertyKey, parameterIndex) +// } +// } +// +// export function ModifyRelationAuthorizerFilter( +// relationName: string, +// partialAuthContext?: PartialAuthorizationContext +// ): ParameterDecorator { +// // eslint-disable-next-line @typescript-eslint/ban-types +// return (target: Object, propertyKey: string | symbol, parameterIndex: number) => { +// const authorizationContext = getAuthorizationContext(propertyKey, partialAuthContext) +// return createParamDecorator( +// async (data: unknown, executionContext: ExecutionContext): Promise> => { +// const context = getContext>(executionContext) +// return { +// filter: await getAuthorizerFilter(context, authorizationContext), +// relationFilter: await getRelationAuthFilter(context, relationName, authorizationContext) +// } +// } +// )()(target, propertyKey, parameterIndex) +// } +// } diff --git a/packages/query-rest/src/decorators/authorizer.decorator.ts b/packages/query-rest/src/decorators/authorizer.decorator.ts new file mode 100644 index 000000000..c7653f72a --- /dev/null +++ b/packages/query-rest/src/decorators/authorizer.decorator.ts @@ -0,0 +1,24 @@ +import { Class, MetaValue, ValueReflector } from '@ptc-org/nestjs-query-core' + +import { Authorizer, AuthorizerOptions, createDefaultAuthorizer, CustomAuthorizer } from '../auth' +import { AUTHORIZER_KEY, CUSTOM_AUTHORIZER_KEY } from './constants' + +const reflector = new ValueReflector(AUTHORIZER_KEY) +const customAuthorizerReflector = new ValueReflector(CUSTOM_AUTHORIZER_KEY) + +export function Authorize( + optsOrAuthorizerOrClass: Class> | CustomAuthorizer | AuthorizerOptions +) { + return (DTOClass: Class): void => { + if (!('authorize' in optsOrAuthorizerOrClass)) { + // If the user provided a class, provide the custom authorizer and create a default authorizer that injects the custom authorizer + customAuthorizerReflector.set(DTOClass, optsOrAuthorizerOrClass) + return reflector.set(DTOClass, createDefaultAuthorizer(DTOClass)) + } + return reflector.set(DTOClass, createDefaultAuthorizer(DTOClass, optsOrAuthorizerOrClass)) + } +} + +export const getAuthorizer = (DTOClass: Class): MetaValue>> => reflector.get(DTOClass) +export const getCustomAuthorizer = (DTOClass: Class): MetaValue>> => + customAuthorizerReflector.get(DTOClass) diff --git a/packages/query-rest/src/decorators/constants.ts b/packages/query-rest/src/decorators/constants.ts new file mode 100644 index 000000000..4cf88eed8 --- /dev/null +++ b/packages/query-rest/src/decorators/constants.ts @@ -0,0 +1,11 @@ +export const FILTERABLE_FIELD_KEY = 'nestjs-query:filterable-field' +export const ID_FIELD_KEY = 'nestjs-query:id-field' +export const RELATION_KEY = 'nestjs-query:relation' +export const REFERENCE_KEY = 'nestjs-query:reference' + +export const AUTHORIZER_KEY = 'nestjs-query:authorizer' +export const CUSTOM_AUTHORIZER_KEY = 'nestjs-query:custom-authorizer' + +export const KEY_SET_KEY = 'nestjs-query:key-set' + +export const QUERY_OPTIONS_KEY = 'nestjs-query:query-options' diff --git a/packages/query-rest/src/decorators/controller-methods.decorator.ts b/packages/query-rest/src/decorators/controller-methods.decorator.ts new file mode 100644 index 000000000..eb2c825c8 --- /dev/null +++ b/packages/query-rest/src/decorators/controller-methods.decorator.ts @@ -0,0 +1,148 @@ +import { + applyDecorators, + ClassSerializerInterceptor, + Delete as NestDelete, + Get as NestGet, + Post as NestPost, + Put as NestPut, + SerializeOptions, + UseInterceptors +} from '@nestjs/common' +import { ApiBody, ApiBodyOptions, ApiOperation, ApiOperationOptions, ApiResponse } from '@nestjs/swagger' + +import { ReturnTypeFunc } from '../interfaces/return-type-func' +import { isDisabled, ResolverMethod, ResolverMethodOpts } from './resolver-method.decorator' + +interface MethodDecoratorArg extends ResolverMethodOpts { + path?: string | string[] + operation?: ApiOperationOptions +} + +interface MutationMethodDecoratorArg extends MethodDecoratorArg { + body?: ApiBodyOptions +} + +const methodDecorator = (method: (path?: string | string[]) => MethodDecorator) => { + return ( + returnTypeFuncOrOptions?: ReturnTypeFunc | MethodDecoratorArg | MutationMethodDecoratorArg, + maybeOptions: MethodDecoratorArg | MutationMethodDecoratorArg = {}, + ...resolverOpts: (MethodDecoratorArg | MutationMethodDecoratorArg)[] + ): MethodDecorator | PropertyDecorator => { + let returnTypeFunc: ReturnTypeFunc | undefined + let options = maybeOptions + + if (typeof returnTypeFuncOrOptions === 'object') { + options = returnTypeFuncOrOptions + returnTypeFuncOrOptions = null + } else { + returnTypeFunc = returnTypeFuncOrOptions + } + + if (isDisabled([options, ...resolverOpts])) { + return (): void => {} + } + + const decorators = [method(options?.path), ResolverMethod(options, ...resolverOpts)] + + if (returnTypeFunc) { + const returnedType = returnTypeFunc() + const isArray = Array.isArray(returnedType) + const type = isArray ? returnedType[0] : returnedType + + decorators.push( + ApiResponse({ + status: 200, + type, + isArray + }) + ) + + decorators.push( + SerializeOptions({ + type, + excludeExtraneousValues: true + }), + UseInterceptors(ClassSerializerInterceptor) + ) + } + + if (options.operation) { + decorators.push(ApiOperation(options.operation)) + } + + if ((options as MutationMethodDecoratorArg).body) { + decorators.push(ApiBody((options as MutationMethodDecoratorArg).body)) + } + + return applyDecorators(...decorators) + } +} + +export function Get(options: MethodDecoratorArg, ...resolverOpts: ResolverMethodOpts[]): PropertyDecorator & MethodDecorator +export function Get( + returnTypeFunction?: ReturnTypeFunc, + options?: MethodDecoratorArg, + ...resolverOpts: ResolverMethodOpts[] +): PropertyDecorator & MethodDecorator + +export function Get( + returnTypeFuncOrOptions?: ReturnTypeFunc | MethodDecoratorArg, + maybeOptions?: MethodDecoratorArg, + ...resolverOpts: ResolverMethodOpts[] +): MethodDecorator | PropertyDecorator { + return methodDecorator(NestGet)(returnTypeFuncOrOptions, maybeOptions, ...resolverOpts) +} + +export function Post( + options: MutationMethodDecoratorArg, + ...resolverOpts: ResolverMethodOpts[] +): PropertyDecorator & MethodDecorator +export function Post( + returnTypeFunction?: ReturnTypeFunc, + options?: MutationMethodDecoratorArg, + ...resolverOpts: ResolverMethodOpts[] +): PropertyDecorator & MethodDecorator + +export function Post( + returnTypeFuncOrOptions?: ReturnTypeFunc | MutationMethodDecoratorArg, + maybeOptions?: MutationMethodDecoratorArg, + ...resolverOpts: ResolverMethodOpts[] +): MethodDecorator | PropertyDecorator { + return methodDecorator(NestPost)(returnTypeFuncOrOptions, maybeOptions, ...resolverOpts) +} + +export function Put( + options: MutationMethodDecoratorArg, + ...resolverOpts: ResolverMethodOpts[] +): PropertyDecorator & MethodDecorator +export function Put( + returnTypeFunction?: ReturnTypeFunc, + options?: MutationMethodDecoratorArg, + ...resolverOpts: ResolverMethodOpts[] +): PropertyDecorator & MethodDecorator + +export function Put( + returnTypeFuncOrOptions?: ReturnTypeFunc | MutationMethodDecoratorArg, + maybeOptions?: MutationMethodDecoratorArg, + ...resolverOpts: ResolverMethodOpts[] +): MethodDecorator | PropertyDecorator { + return methodDecorator(NestPut)(returnTypeFuncOrOptions, maybeOptions, ...resolverOpts) +} + +export function Delete( + options: MutationMethodDecoratorArg, + ...resolverOpts: ResolverMethodOpts[] +): PropertyDecorator & MethodDecorator +export function Delete( + returnTypeFunction?: ReturnTypeFunc, + options?: MutationMethodDecoratorArg, + ...resolverOpts: ResolverMethodOpts[] +): PropertyDecorator & MethodDecorator + +export function Delete( + returnTypeFuncOrOptions?: ReturnTypeFunc | MutationMethodDecoratorArg, + maybeOptions?: MutationMethodDecoratorArg, + ...resolverOpts: ResolverMethodOpts[] +): MethodDecorator | PropertyDecorator { + return methodDecorator(NestDelete)(returnTypeFuncOrOptions, maybeOptions, ...resolverOpts) +} diff --git a/packages/query-rest/src/decorators/decorator.utils.ts b/packages/query-rest/src/decorators/decorator.utils.ts new file mode 100644 index 000000000..60695c844 --- /dev/null +++ b/packages/query-rest/src/decorators/decorator.utils.ts @@ -0,0 +1,22 @@ +export type ComposableDecorator = MethodDecorator | PropertyDecorator | ClassDecorator | ParameterDecorator +export type ComposedDecorator = MethodDecorator & PropertyDecorator & ClassDecorator & ParameterDecorator + +export function composeDecorators(...decorators: ComposableDecorator[]): ComposedDecorator { + // eslint-disable-next-line @typescript-eslint/ban-types + return ( + // eslint-disable-next-line @typescript-eslint/ban-types + target: TFunction | object, + propertyKey?: string | symbol, + descriptorOrIndex?: TypedPropertyDescriptor | number + ) => { + decorators.forEach((decorator) => { + if (target instanceof Function && !descriptorOrIndex) { + return (decorator as ClassDecorator)(target) + } + if (typeof descriptorOrIndex === 'number') { + return (decorator as ParameterDecorator)(target, propertyKey, descriptorOrIndex) + } + return (decorator as MethodDecorator | PropertyDecorator)(target, propertyKey, descriptorOrIndex) + }) + } +} diff --git a/packages/query-rest/src/decorators/field.decorator.ts b/packages/query-rest/src/decorators/field.decorator.ts new file mode 100644 index 000000000..fff48eac8 --- /dev/null +++ b/packages/query-rest/src/decorators/field.decorator.ts @@ -0,0 +1,92 @@ +import { applyDecorators } from '@nestjs/common' +import { ApiProperty, ApiPropertyOptions } from '@nestjs/swagger' +import { Expose, Type } from 'class-transformer' +import { IsNotEmpty, IsOptional } from 'class-validator' + +import { ReturnTypeFunc } from '../interfaces/return-type-func' + +export type FieldOptions = Omit + +/** + * Decorator for Fields that should be filterable through a [[FilterType]] + * + * @example + * + * In the following DTO `id`, `title` and `completed` are filterable. + * + * ```ts + * import { FilterableField } from '@ptc-org/nestjs-query-graphql'; + * import { ObjectType, ID, GraphQLISODateTime, Field } from '@nestjs/graphql'; + * + * @ObjectType('TodoItem') + * export class TodoItemDTO { + * @FilterableField(() => ID) + * id!: string; + * + * @FilterableField() + * title!: string; + * + * @FilterableField() + * completed!: boolean; + * + * @Field(() => GraphQLISODateTime) + * created!: Date; + * + * @Field(() => GraphQLISODateTime) + * updated!: Date; + * } + * ``` + */ +export function Field(): PropertyDecorator & MethodDecorator +export function Field(options: FieldOptions): PropertyDecorator & MethodDecorator +export function Field(returnTypeFunction?: ReturnTypeFunc, options?: FieldOptions): PropertyDecorator & MethodDecorator +export function Field( + returnTypeFuncOrOptions?: ReturnTypeFunc | FieldOptions, + maybeOptions?: FieldOptions +): MethodDecorator | PropertyDecorator { + let returnTypeFunc: ReturnTypeFunc | undefined + let advancedOptions: FieldOptions | undefined + if (typeof returnTypeFuncOrOptions === 'function') { + returnTypeFunc = returnTypeFuncOrOptions + advancedOptions = maybeOptions + } else if (typeof returnTypeFuncOrOptions === 'object') { + advancedOptions = returnTypeFuncOrOptions + } else if (typeof maybeOptions === 'object') { + advancedOptions = maybeOptions + } + + const returnedType = returnTypeFunc?.() + const isArray = returnedType && Array.isArray(returnedType) + const type = isArray ? returnedType[0] : returnedType + + if ( + advancedOptions !== undefined && + advancedOptions.required === undefined && + (advancedOptions.nullable || advancedOptions.default !== undefined) + ) { + advancedOptions.required = false + } + + const decorators = [ + Expose(), + ApiProperty({ + type, + isArray, + ...advancedOptions + }) + ] + + if (advancedOptions !== undefined && advancedOptions.required !== undefined) { + if (advancedOptions.required) { + decorators.push(IsNotEmpty()) + } else { + decorators.push(IsOptional()) + } + } + + if (type) { + decorators.push(Type(() => type)) + } + + return applyDecorators(...decorators) +} diff --git a/packages/query-rest/src/decorators/filterable-field.decorator.ts b/packages/query-rest/src/decorators/filterable-field.decorator.ts new file mode 100644 index 000000000..a7448e2f1 --- /dev/null +++ b/packages/query-rest/src/decorators/filterable-field.decorator.ts @@ -0,0 +1,111 @@ +import { applyDecorators } from '@nestjs/common' +import { ApiProperty, ApiPropertyOptions } from '@nestjs/swagger' +import { ArrayReflector, Class, getPrototypeChain } from '@ptc-org/nestjs-query-core' +import { Expose } from 'class-transformer' + +import { ReturnTypeFunc, ReturnTypeFuncValue } from '../interfaces/return-type-func' +import { FILTERABLE_FIELD_KEY } from './constants' + +const reflector = new ArrayReflector(FILTERABLE_FIELD_KEY) +export type FilterableFieldOptions = { + allowedComparisons?: ['=', '!='] + filterRequired?: boolean + filterOnly?: boolean + filterDecorators?: PropertyDecorator[] +} & ApiPropertyOptions + +export interface FilterableFieldDescriptor { + propertyName: string + schemaName: string + target: Class + returnTypeFunc?: ReturnTypeFunc + advancedOptions?: FilterableFieldOptions +} + +/** + * Decorator for Fields that should be filterable through a [[FilterType]] + * + * @example + * + * In the following DTO `id`, `title` and `completed` are filterable. + * + * ```ts + * import { FilterableField } from '@ptc-org/nestjs-query-graphql'; + * import { ObjectType, ID, GraphQLISODateTime, Field } from '@nestjs/graphql'; + * + * @ObjectType('TodoItem') + * export class TodoItemDTO { + * @FilterableField(() => ID) + * id!: string; + * + * @FilterableField() + * title!: string; + * + * @FilterableField() + * completed!: boolean; + * + * @Field(() => GraphQLISODateTime) + * created!: Date; + * + * @Field(() => GraphQLISODateTime) + * updated!: Date; + * } + * ``` + */ +export function FilterableField(): PropertyDecorator & MethodDecorator +export function FilterableField(options: FilterableFieldOptions): PropertyDecorator & MethodDecorator +export function FilterableField( + returnTypeFunction?: ReturnTypeFunc, + options?: FilterableFieldOptions +): PropertyDecorator & MethodDecorator +export function FilterableField( + returnTypeFuncOrOptions?: ReturnTypeFunc | FilterableFieldOptions, + maybeOptions?: FilterableFieldOptions +): MethodDecorator | PropertyDecorator { + let returnTypeFunc: ReturnTypeFunc | undefined + let advancedOptions: FilterableFieldOptions | undefined + if (typeof returnTypeFuncOrOptions === 'function') { + returnTypeFunc = returnTypeFuncOrOptions + advancedOptions = maybeOptions + } else if (typeof returnTypeFuncOrOptions === 'object') { + advancedOptions = returnTypeFuncOrOptions + } else if (typeof maybeOptions === 'object') { + advancedOptions = maybeOptions + } + return ( + // eslint-disable-next-line @typescript-eslint/ban-types + target: Object, + propertyName: string | symbol, + descriptor: TypedPropertyDescriptor + ): TypedPropertyDescriptor | void => { + const Ctx = Reflect.getMetadata('design:type', target, propertyName) as Class + reflector.append(target.constructor as Class, { + propertyName: propertyName.toString(), + schemaName: propertyName.toString(), + target: Ctx, + returnTypeFunc, + advancedOptions + }) + + if (advancedOptions?.filterOnly) { + return undefined + } + + applyDecorators( + Expose(), + ApiProperty({ + type: returnTypeFunc, + ...advancedOptions + }) + )(target, propertyName, descriptor) + } +} + +export function getFilterableFields(DTOClass: Class): FilterableFieldDescriptor[] { + return getPrototypeChain(DTOClass).reduce((fields, Cls) => { + const existingFieldNames = fields.map((t) => t.propertyName) + const typeFields = reflector.get(Cls) ?? [] + const newFields = typeFields.filter((t) => !existingFieldNames.includes(t.propertyName)) + return [...newFields, ...fields] + }, [] as FilterableFieldDescriptor[]) +} diff --git a/packages/query-rest/src/decorators/hook-args.decorator.ts b/packages/query-rest/src/decorators/hook-args.decorator.ts new file mode 100644 index 000000000..1d0c4da5f --- /dev/null +++ b/packages/query-rest/src/decorators/hook-args.decorator.ts @@ -0,0 +1,69 @@ +import { ArgumentMetadata, Body as NestBody, Inject, PipeTransform, Query as NestQuery } from '@nestjs/common' +import { REQUEST } from '@nestjs/core' +import { Class, Query } from '@ptc-org/nestjs-query-core' +import { plainToInstance } from 'class-transformer' + +import { Hook } from '../hooks' +import { HookContext } from '../interceptors' +import { MutationArgsType } from '../types' +import { BuildableQueryType } from '../types/query/buildable-query.type' + +class HooksTransformer implements PipeTransform { + @Inject(REQUEST) protected readonly request: Request + + public async transform(value: T, metadata: ArgumentMetadata): Promise | Query> { + const transformedValue = this.transformValue(value, metadata.metatype) + + if (metadata.type === 'query') { + return this.runQueryHooks(transformedValue as BuildableQueryType) + } + + return this.runMutationHooks(transformedValue) + } + + private transformValue(value: T, type?: Class): T { + if (!type || value instanceof type) { + return value + } + + return plainToInstance(type, value, { excludeExtraneousValues: true }) + } + + private async runMutationHooks(data: T): Promise> { + const hooks = (this.request as HookContext>).hooks + + if (hooks && hooks.length > 0) { + let hookedArgs = { input: data } + for (const hook of hooks) { + hookedArgs = (await hook.run(hookedArgs, this.request)) as MutationArgsType + } + + return hookedArgs + } + + return { input: data } + } + + private async runQueryHooks(data: BuildableQueryType): Promise> { + const hooks = (this.request as HookContext>).hooks + + if (hooks && hooks.length > 0) { + let hookedArgs = data.buildQuery() + for (const hook of hooks) { + hookedArgs = (await hook.run(hookedArgs, this.request)) as Query + } + + return hookedArgs + } + + return data as Query + } +} + +export const QueryHookArgs = >(): ParameterDecorator => { + return NestQuery(HooksTransformer) +} + +export const BodyHookArgs = >(): ParameterDecorator => { + return NestBody(HooksTransformer) +} diff --git a/packages/query-rest/src/decorators/hook.decorator.ts b/packages/query-rest/src/decorators/hook.decorator.ts new file mode 100644 index 000000000..0f723fd93 --- /dev/null +++ b/packages/query-rest/src/decorators/hook.decorator.ts @@ -0,0 +1,52 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Class, getClassMetadata, MetaValue } from '@ptc-org/nestjs-query-core' + +import { + BeforeCreateOneHook, + BeforeQueryManyHook, + BeforeUpdateOneHook, + createDefaultHook, + Hook, + HookTypes, + isHookClass +} from '../hooks' + +export type HookMetaValue> = MetaValue[]> +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type HookDecoratorArg> = Class | H['run'] + +const hookMetaDataKey = (hookType: HookTypes): string => `nestjs-query:${hookType}` + +const hookDecorator = >(hookType: HookTypes) => { + const key = hookMetaDataKey(hookType) + + const getHook = (hook: HookDecoratorArg) => { + if (isHookClass(hook)) { + return hook + } + return createDefaultHook(hook) + } + + // eslint-disable-next-line @typescript-eslint/ban-types + return (...data: HookDecoratorArg[]) => + // eslint-disable-next-line @typescript-eslint/ban-types + (target: Function): void => { + return Reflect.defineMetadata( + key, + data.map((d) => getHook(d)), + target + ) + } +} + +export const BeforeCreateOne = hookDecorator>(HookTypes.BEFORE_CREATE_ONE) +// export const BeforeCreateMany = hookDecorator>(HookTypes.BEFORE_CREATE_MANY) +export const BeforeUpdateOne = hookDecorator>(HookTypes.BEFORE_UPDATE_ONE) +// export const BeforeUpdateMany = hookDecorator>(HookTypes.BEFORE_UPDATE_MANY) +// export const BeforeDeleteOne = hookDecorator(HookTypes.BEFORE_DELETE_ONE) +// export const BeforeDeleteMany = hookDecorator>(HookTypes.BEFORE_DELETE_MANY) +export const BeforeQueryMany = hookDecorator>(HookTypes.BEFORE_QUERY_MANY) +// export const BeforeFindOne = hookDecorator(HookTypes.BEFORE_FIND_ONE) + +export const getHooksForType = >(hookType: HookTypes, DTOClass: Class): HookMetaValue => + getClassMetadata(DTOClass, hookMetaDataKey(hookType), true) diff --git a/packages/query-rest/src/decorators/index.ts b/packages/query-rest/src/decorators/index.ts new file mode 100644 index 000000000..2a90911b9 --- /dev/null +++ b/packages/query-rest/src/decorators/index.ts @@ -0,0 +1,12 @@ +export * from './authorize-filter.decorator' +export * from './authorizer.decorator' +export * from './controller-methods.decorator' +export * from './field.decorator' +export * from './filterable-field.decorator' +export * from './hook.decorator' +export * from './hook-args.decorator' +export * from './inject-authorizer.decorator' +export * from './query-options.decorator' +export * from './resolver-method.decorator' +export * from './resolver-query.decorator' +export * from './skip-if.decorator' diff --git a/packages/query-rest/src/decorators/inject-authorizer.decorator.ts b/packages/query-rest/src/decorators/inject-authorizer.decorator.ts new file mode 100644 index 000000000..a1d006295 --- /dev/null +++ b/packages/query-rest/src/decorators/inject-authorizer.decorator.ts @@ -0,0 +1,6 @@ +import { Inject } from '@nestjs/common' +import { Class } from '@ptc-org/nestjs-query-core' + +import { getAuthorizerToken } from '../auth' + +export const InjectAuthorizer = (DTOClass: Class): ParameterDecorator => Inject(getAuthorizerToken(DTOClass)) diff --git a/packages/query-rest/src/decorators/query-options.decorator.ts b/packages/query-rest/src/decorators/query-options.decorator.ts new file mode 100644 index 000000000..d614cd850 --- /dev/null +++ b/packages/query-rest/src/decorators/query-options.decorator.ts @@ -0,0 +1,18 @@ +import { Class, MetaValue, ValueReflector } from '@ptc-org/nestjs-query-core' + +import { QueryArgsTypeOpts } from '../types' +import { QUERY_OPTIONS_KEY } from './constants' + +const valueReflector = new ValueReflector(QUERY_OPTIONS_KEY) + +export type QueryOptionsDecoratorOpts = QueryArgsTypeOpts + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function QueryOptions(opts: QueryOptionsDecoratorOpts) { + return (target: Class): void => { + valueReflector.set(target, opts) + } +} + +export const getQueryOptions = (DTOClass: Class): MetaValue> => + valueReflector.get(DTOClass) diff --git a/packages/query-rest/src/decorators/resolver-method.decorator.ts b/packages/query-rest/src/decorators/resolver-method.decorator.ts new file mode 100644 index 000000000..bbc471402 --- /dev/null +++ b/packages/query-rest/src/decorators/resolver-method.decorator.ts @@ -0,0 +1,91 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + applyDecorators, + CanActivate, + ExceptionFilter, + NestInterceptor, + PipeTransform, + UseFilters, + UseGuards, + UseInterceptors, + UsePipes +} from '@nestjs/common' +import { Class } from '@ptc-org/nestjs-query-core' + +export interface BaseResolverOptions { + /** An array of `nestjs` guards to apply to a endpoint */ + guards?: (Class | CanActivate)[] + /** An array of `nestjs` interceptors to apply to a endpoint */ + interceptors?: Class>[] + /** An array of `nestjs` pipes to apply to a endpoint */ + pipes?: Class>[] + /** An array of `nestjs` error filters to apply to a endpoint */ + filters?: Class>[] + /** An array of additional decorators to apply to the endpoint * */ + decorators?: (PropertyDecorator | MethodDecorator)[] + /** + * Tags to register for the endpoint + */ + tags?: string[] +} + +/** + * Options for resolver methods. + */ +export interface ResolverMethodOpts extends BaseResolverOptions { + /** Set to true to disable the endpoint */ + disabled?: boolean +} + +/** + * Options for relation resolver methods. + */ +export interface ResolverRelationMethodOpts extends BaseResolverOptions { + /** Set to true to enable the endpoint */ + enabled?: boolean +} + +/** + * @internal + * Creates a unique set of items. + * @param arrs - An array of arrays to de duplicate. + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function createSetArray(...arrs: T[][]): T[] { + const set: Set = new Set(arrs.reduce((acc: T[], arr: T[]): T[] => [...acc, ...arr], [])) + return [...set] +} + +/** + * @internal + * Returns true if any of the [[ResolverMethodOpts]] are disabled. + * @param opts - The array of [[ResolverMethodOpts]] to check. + */ +export function isDisabled(opts: ResolverMethodOpts[]): boolean { + return !!opts.find((o) => o.disabled) +} + +/** + * @internal + * Returns true if any of the [[ResolverRelationMethodOpts]] are disabled. + * @param opts - The array of [[ResolverRelationMethodOpts]] to check. + */ +export function isEnabled(opts: ResolverRelationMethodOpts[]): boolean { + return opts.some((o) => o.enabled) +} + +/** + * @internal + * Decorator for all ResolverMethods + * + * @param opts - the [[ResolverMethodOpts]] to apply. + */ +export function ResolverMethod(...opts: ResolverMethodOpts[]): MethodDecorator { + return applyDecorators( + UseGuards(...createSetArray | CanActivate>(...opts.map((o) => o.guards ?? []))), + UseInterceptors(...createSetArray>(...opts.map((o) => o.interceptors ?? []))), + UsePipes(...createSetArray>(...opts.map((o) => o.pipes ?? []))), + UseFilters(...createSetArray>(...opts.map((o) => o.filters ?? []))), + ...createSetArray(...opts.map((o) => o.decorators ?? [])) + ) +} diff --git a/packages/query-rest/src/decorators/resolver-query.decorator.ts b/packages/query-rest/src/decorators/resolver-query.decorator.ts new file mode 100644 index 000000000..fc3952da9 --- /dev/null +++ b/packages/query-rest/src/decorators/resolver-query.decorator.ts @@ -0,0 +1,5 @@ +import { ResolverMethodOpts } from './resolver-method.decorator' + +export interface QueryResolverMethodOpts extends ResolverMethodOpts { + withDeleted?: boolean +} diff --git a/packages/query-rest/src/decorators/skip-if.decorator.ts b/packages/query-rest/src/decorators/skip-if.decorator.ts new file mode 100644 index 000000000..415ddb0ee --- /dev/null +++ b/packages/query-rest/src/decorators/skip-if.decorator.ts @@ -0,0 +1,14 @@ +import { ComposableDecorator, ComposedDecorator, composeDecorators } from './decorator.utils' + +/** + * @internal + * Wraps Args to allow skipping decorating + * @param check - checker to run. + * @param decorators - The decorators to apply + */ +export function SkipIf(check: () => boolean, ...decorators: ComposableDecorator[]): ComposedDecorator { + if (check()) { + return (): void => {} + } + return composeDecorators(...decorators) +} diff --git a/packages/query-rest/src/hooks/default.hook.ts b/packages/query-rest/src/hooks/default.hook.ts new file mode 100644 index 000000000..778987b81 --- /dev/null +++ b/packages/query-rest/src/hooks/default.hook.ts @@ -0,0 +1,13 @@ +import { Class } from '@ptc-org/nestjs-query-core' + +import { Hook } from './hooks' + +export const createDefaultHook = (func: Hook['run']): Class> => { + class DefaultHook implements Hook { + get run() { + return func + } + } + + return DefaultHook +} diff --git a/packages/query-rest/src/hooks/hooks.ts b/packages/query-rest/src/hooks/hooks.ts new file mode 100644 index 000000000..7f9ca01c5 --- /dev/null +++ b/packages/query-rest/src/hooks/hooks.ts @@ -0,0 +1,34 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Class, Query } from '@ptc-org/nestjs-query-core' + +import { + // CreateManyInputType, + CreateOneInputType, UpdateOneInputType + // DeleteManyInputType, + // DeleteOneInputType, + // FindOneArgsType, + // UpdateManyInputType, + // UpdateOneInputType +} from '../types' + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface Hook { + run(instance: T, context: Context): T | Promise +} + +export function isHookClass(hook: unknown): hook is Class> { + return typeof hook === 'function' && 'prototype' in hook && 'run' in hook.prototype +} + +export type BeforeCreateOneHook = Hook, Context> +// export type BeforeCreateManyHook = Hook, Context> +// +export type BeforeUpdateOneHook = Hook, Context> +// export type BeforeUpdateManyHook = Hook, Context> +// +// export type BeforeDeleteOneHook = Hook +// export type BeforeDeleteManyHook = Hook, Context> +// +export type BeforeQueryManyHook = Hook, Context> +// +// export type BeforeFindOneHook = Hook diff --git a/packages/query-rest/src/hooks/index.ts b/packages/query-rest/src/hooks/index.ts new file mode 100644 index 000000000..5d8d36820 --- /dev/null +++ b/packages/query-rest/src/hooks/index.ts @@ -0,0 +1,4 @@ +export { createDefaultHook } from './default.hook' +export * from './hooks' +export * from './tokens' +export * from './types' diff --git a/packages/query-rest/src/hooks/tokens.ts b/packages/query-rest/src/hooks/tokens.ts new file mode 100644 index 000000000..272afe5fd --- /dev/null +++ b/packages/query-rest/src/hooks/tokens.ts @@ -0,0 +1,5 @@ +import { Class } from '@ptc-org/nestjs-query-core' + +import { HookTypes } from './types' + +export const getHookToken = (hookType: HookTypes, DTOClass: Class): string => `${DTOClass.name}${hookType}Hook` diff --git a/packages/query-rest/src/hooks/types.ts b/packages/query-rest/src/hooks/types.ts new file mode 100644 index 000000000..feb8e4435 --- /dev/null +++ b/packages/query-rest/src/hooks/types.ts @@ -0,0 +1,10 @@ +export enum HookTypes { + BEFORE_CREATE_ONE = 'BeforeCreateOne', + BEFORE_CREATE_MANY = 'BeforeCreateMany', + BEFORE_UPDATE_ONE = 'BeforeUpdateOne', + BEFORE_UPDATE_MANY = 'BeforeUpdateMany', + BEFORE_DELETE_ONE = 'BeforeDeleteOne', + BEFORE_DELETE_MANY = 'BeforeDeleteMany', + BEFORE_QUERY_MANY = 'BeforeQueryMany', + BEFORE_FIND_ONE = 'BeforeFindOne' +} diff --git a/packages/query-rest/src/index.ts b/packages/query-rest/src/index.ts new file mode 100644 index 000000000..61069e50b --- /dev/null +++ b/packages/query-rest/src/index.ts @@ -0,0 +1,7 @@ +export { AuthorizationContext, Authorizer, AuthorizerOptions, CustomAuthorizer, OperationGroup } from './auth' +export * from './connection' +export * from './decorators' +export * from './hooks' +export * from './interceptors' +export { NestjsQueryRestModule } from './module' +export * from './types' diff --git a/packages/query-rest/src/interceptors/authorizer.interceptor.ts b/packages/query-rest/src/interceptors/authorizer.interceptor.ts new file mode 100644 index 000000000..bf4ff9a4b --- /dev/null +++ b/packages/query-rest/src/interceptors/authorizer.interceptor.ts @@ -0,0 +1,29 @@ +import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common' +import { Class } from '@ptc-org/nestjs-query-core' + +import { Authorizer } from '../auth' +import { InjectAuthorizer } from '../decorators' + +export type AuthorizerContext = { authorizer: Authorizer } + +export function AuthorizerInterceptor(DTOClass: Class): Class { + @Injectable() + class Interceptor implements NestInterceptor { + constructor(@InjectAuthorizer(DTOClass) readonly authorizer: Authorizer) {} + + public intercept(context: ExecutionContext, next: CallHandler) { + const request = context.switchToHttp().getRequest() + request.authorizer = this.authorizer + + return next.handle() + } + } + + Object.defineProperty(Interceptor, 'name', { + writable: false, + // set a unique name otherwise DI does not inject a unique one for each request + value: `${DTOClass.name}AuthorizerInterceptor` + }) + + return Interceptor +} diff --git a/packages/query-rest/src/interceptors/hook.interceptor.ts b/packages/query-rest/src/interceptors/hook.interceptor.ts new file mode 100644 index 000000000..72e4f89f0 --- /dev/null +++ b/packages/query-rest/src/interceptors/hook.interceptor.ts @@ -0,0 +1,43 @@ +import { CallHandler, ExecutionContext, Inject, Injectable, NestInterceptor } from '@nestjs/common' +import { Class } from '@ptc-org/nestjs-query-core' + +import { getHooksForType } from '../decorators' +import { getHookToken, Hook, HookTypes } from '../hooks' + +export type HookContext> = { + hooks?: H[] +} + +class DefaultHookInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler) { + return next.handle() + } +} + +export function HookInterceptor(type: HookTypes, ...DTOClasses: Class[]): Class { + const HookedClasses = DTOClasses.find((Cls) => getHooksForType(type, Cls)) + if (!HookedClasses) { + return DefaultHookInterceptor + } + const hookToken = getHookToken(type, HookedClasses) + + @Injectable() + class Interceptor implements NestInterceptor { + constructor(@Inject(hookToken) readonly hooks: Hook[]) {} + + public intercept(context: ExecutionContext, next: CallHandler) { + const request = context.switchToHttp().getRequest() + request.hooks = this.hooks + + return next.handle() + } + } + + Object.defineProperty(Interceptor, 'name', { + writable: false, + // set a unique name otherwise DI does not inject a unique one for each request + value: `${DTOClasses[0].name}${type}HookInterceptor` + }) + + return Interceptor +} diff --git a/packages/query-rest/src/interceptors/index.ts b/packages/query-rest/src/interceptors/index.ts new file mode 100644 index 000000000..93a26d136 --- /dev/null +++ b/packages/query-rest/src/interceptors/index.ts @@ -0,0 +1,2 @@ +export * from './authorizer.interceptor' +export * from './hook.interceptor' diff --git a/packages/query-rest/src/interfaces/return-type-func.ts b/packages/query-rest/src/interfaces/return-type-func.ts new file mode 100644 index 000000000..12e7b5c85 --- /dev/null +++ b/packages/query-rest/src/interfaces/return-type-func.ts @@ -0,0 +1,6 @@ +import { Type } from '@nestjs/common' + +// eslint-disable-next-line @typescript-eslint/ban-types +export type ReturnTypeFuncValue = Type | Function | object | symbol; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ReturnTypeFunc = (returns?: void) => T; diff --git a/packages/query-rest/src/module.ts b/packages/query-rest/src/module.ts new file mode 100644 index 000000000..f8afea3cb --- /dev/null +++ b/packages/query-rest/src/module.ts @@ -0,0 +1,73 @@ +import { DynamicModule, ForwardReference, Provider, Type } from '@nestjs/common' +import { Assembler, Class, NestjsQueryCoreModule } from '@ptc-org/nestjs-query-core' + +import { createAuthorizerProviders } from './providers' +import { createHookProviders } from './providers/hook.provider' +import { AutoResolverOpts, createEndpoints } from './providers/resolver.provider' +import { ReadResolverOpts } from './resolvers' +import { PagingStrategies } from './types' + +interface DTOModuleOpts { + DTOClass: Class +} + +export interface NestjsQueryRestModuleFeatureOpts { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + imports?: Array | DynamicModule | Promise | ForwardReference> + services?: Provider[] + // eslint-disable-next-line @typescript-eslint/no-explicit-any + assemblers?: Class>[] + endpoints?: AutoResolverOpts, PagingStrategies>[] + // eslint-disable-next-line @typescript-eslint/no-explicit-any + // resolvers?: AutoResolverOpts, PagingStrategies>[] + dtos?: DTOModuleOpts[] + // eslint-disable-next-line @typescript-eslint/no-explicit-any + controllers?: Array> + // pubSub?: Provider +} + +export class NestjsQueryRestModule { + public static forFeature(opts: NestjsQueryRestModuleFeatureOpts): DynamicModule { + const coreModule = this.getCoreModule(opts) + const providers = this.getProviders(opts) + const imports = opts.imports ?? [] + const controllers = opts.controllers ?? [] + + return { + module: NestjsQueryRestModule, + imports: [...imports, coreModule], + providers: [...providers], + exports: [...providers, ...imports, coreModule], + controllers: [...this.getEndpointProviders(opts), ...controllers] + } + } + + private static getCoreModule(opts: NestjsQueryRestModuleFeatureOpts): DynamicModule { + return NestjsQueryCoreModule.forFeature({ + assemblers: opts.assemblers, + imports: opts.imports ?? [] + }) + } + + private static getProviders(opts: NestjsQueryRestModuleFeatureOpts): Provider[] { + return [...this.getServicesProviders(opts), ...this.getAuthorizerProviders(opts), ...this.getHookProviders(opts)] + } + + private static getServicesProviders(opts: NestjsQueryRestModuleFeatureOpts): Provider[] { + return opts.services ?? [] + } + + private static getAuthorizerProviders(opts: NestjsQueryRestModuleFeatureOpts): Provider[] { + const endpointDTOs = opts.endpoints?.map((r) => r.DTOClass) ?? [] + const dtos = opts.dtos?.map((o) => o.DTOClass) ?? [] + return createAuthorizerProviders([...endpointDTOs, ...dtos]) + } + + private static getEndpointProviders(opts: NestjsQueryRestModuleFeatureOpts): Type[] { + return createEndpoints(opts.endpoints ?? []) + } + + private static getHookProviders(opts: NestjsQueryRestModuleFeatureOpts): Provider[] { + return createHookProviders([...(opts.endpoints ?? []), ...(opts.dtos ?? [])]) + } +} diff --git a/packages/query-rest/src/providers/authorizer.provider.ts b/packages/query-rest/src/providers/authorizer.provider.ts new file mode 100644 index 000000000..e06e50f4c --- /dev/null +++ b/packages/query-rest/src/providers/authorizer.provider.ts @@ -0,0 +1,34 @@ +import { Provider } from '@nestjs/common' +import { Class } from '@ptc-org/nestjs-query-core' + +import { createDefaultAuthorizer, getAuthorizerToken, getCustomAuthorizerToken } from '../auth' +import { getAuthorizer, getCustomAuthorizer } from '../decorators' + +function createServiceProvider(DTOClass: Class): Provider { + const token = getAuthorizerToken(DTOClass) + const authorizer = getAuthorizer(DTOClass) + if (!authorizer) { + // create default authorizer in case any relations have an authorizers + return { provide: token, useClass: createDefaultAuthorizer(DTOClass, { authorize: () => ({}) }) } + } + return { provide: token, useClass: authorizer } +} + +function createCustomAuthorizerProvider(DTOClass: Class): Provider | undefined { + const token = getCustomAuthorizerToken(DTOClass) + const customAuthorizer = getCustomAuthorizer(DTOClass) + if (customAuthorizer) { + return { provide: token, useClass: customAuthorizer } + } + return undefined +} + +export const createAuthorizerProviders = (DTOClasses: Class[]): Provider[] => + DTOClasses.reduce((providers, DTOClass) => { + const p = createCustomAuthorizerProvider(DTOClass) + if (p) { + providers.push(p) + } + providers.push(createServiceProvider(DTOClass)) + return providers + }, []) diff --git a/packages/query-rest/src/providers/hook.provider.ts b/packages/query-rest/src/providers/hook.provider.ts new file mode 100644 index 000000000..8a155784d --- /dev/null +++ b/packages/query-rest/src/providers/hook.provider.ts @@ -0,0 +1,49 @@ +import { Provider } from '@nestjs/common' +import { Class } from '@ptc-org/nestjs-query-core' + +import { getHooksForType } from '../decorators' +import { getHookToken, HookTypes } from '../hooks' +import { PagingStrategies } from '../types' +import { CRUDAutoResolverOpts } from './resolver.provider' + +export type HookProviderOptions = Pick< + CRUDAutoResolverOpts, + 'DTOClass' | 'CreateDTOClass' | 'UpdateDTOClass' +> + +function createHookProvider(hookType: HookTypes, ...DTOClass: Class[]): Provider[] | undefined { + return DTOClass.reduce((p: Provider[] | undefined, cls) => { + if (p && p.length > 0) { + return p + } + const maybeHooks = getHooksForType(hookType, cls) + if (maybeHooks) { + return [ + ...maybeHooks, + { + provide: getHookToken(hookType, cls), + useFactory: (...providers: Provider[]) => providers, + inject: maybeHooks + } + ] + } + return [] + }, []) +} + +function getHookProviders(opts: HookProviderOptions): Provider[] { + const { DTOClass, CreateDTOClass = DTOClass, UpdateDTOClass = DTOClass } = opts + return [ + ...createHookProvider(HookTypes.BEFORE_CREATE_ONE, CreateDTOClass, DTOClass), + ...createHookProvider(HookTypes.BEFORE_CREATE_MANY, CreateDTOClass, DTOClass), + ...createHookProvider(HookTypes.BEFORE_UPDATE_ONE, UpdateDTOClass, DTOClass), + ...createHookProvider(HookTypes.BEFORE_UPDATE_MANY, UpdateDTOClass, DTOClass), + ...createHookProvider(HookTypes.BEFORE_DELETE_ONE, DTOClass), + ...createHookProvider(HookTypes.BEFORE_DELETE_MANY, DTOClass), + ...createHookProvider(HookTypes.BEFORE_QUERY_MANY, DTOClass), + ...createHookProvider(HookTypes.BEFORE_FIND_ONE, DTOClass) + ].filter((p) => !!p) +} + +export const createHookProviders = (opts: HookProviderOptions[]): Provider[] => + opts.reduce((ps: Provider[], opt) => [...ps, ...getHookProviders(opt)], []) diff --git a/packages/query-rest/src/providers/index.ts b/packages/query-rest/src/providers/index.ts new file mode 100644 index 000000000..e01983edc --- /dev/null +++ b/packages/query-rest/src/providers/index.ts @@ -0,0 +1 @@ +export * from './authorizer.provider' diff --git a/packages/query-rest/src/providers/resolver.provider.ts b/packages/query-rest/src/providers/resolver.provider.ts new file mode 100644 index 000000000..6b868f31c --- /dev/null +++ b/packages/query-rest/src/providers/resolver.provider.ts @@ -0,0 +1,149 @@ +import { Controller, Inject, Type } from '@nestjs/common' +import { + Assembler, + AssemblerFactory, + AssemblerQueryService, + Class, + DeepPartial, + InjectAssemblerQueryService, + InjectQueryService, + QueryService +} from '@ptc-org/nestjs-query-core' + +import { getDTONames } from '../common' +import { CRUDResolver, CRUDResolverOpts } from '../resolvers' +import { PagingStrategies } from '../types' + +export type CRUDAutoResolverOpts = CRUDResolverOpts & { + DTOClass: Class +} + +export type EntityCRUDAutoResolverOpts = CRUDAutoResolverOpts< + DTO, + C, + U, + R, + PS +> & { + EntityClass: Class +} + +export type AssemblerCRUDAutoResolverOpts = CRUDAutoResolverOpts< + DTO, + C, + U, + R, + PS +> & { + AssemblerClass: Class +} + +export type ServiceCRUDAutoResolverOpts = CRUDAutoResolverOpts< + DTO, + C, + U, + R, + PS +> & { + ServiceClass: Class +} + +export type AutoResolverOpts = + | EntityCRUDAutoResolverOpts + | AssemblerCRUDAutoResolverOpts + | ServiceCRUDAutoResolverOpts + +export const isServiceCRUDAutoResolverOpts = ( + opts: AutoResolverOpts +): opts is ServiceCRUDAutoResolverOpts => 'DTOClass' in opts && 'ServiceClass' in opts + +export const isAssemblerCRUDAutoResolverOpts = ( + opts: AutoResolverOpts +): opts is AssemblerCRUDAutoResolverOpts => 'DTOClass' in opts && 'AssemblerClass' in opts + +const getEndpointToken = (DTOClass: Class): string => `${DTOClass.name}AutoEndpoint` + +function createEntityAutoResolver, C, U, R, PS extends PagingStrategies>( + resolverOpts: EntityCRUDAutoResolverOpts +): Type { + const { DTOClass, EntityClass } = resolverOpts + const { endpointName } = getDTONames(DTOClass) + + class Service extends AssemblerQueryService { + constructor(service: QueryService) { + const assembler = AssemblerFactory.getAssembler(DTOClass, EntityClass) + super(assembler, service) + } + } + + @Controller(endpointName) + class AutoResolver extends CRUDResolver(DTOClass, resolverOpts) { + constructor(@InjectQueryService(EntityClass) service: QueryService) { + super(new Service(service)) + } + } + + // need to set class name so DI works properly + Object.defineProperty(AutoResolver, 'name', { value: getEndpointToken(DTOClass), writable: false }) + return AutoResolver +} + +function createAssemblerAutoResolver( + resolverOpts: AssemblerCRUDAutoResolverOpts +): Type { + const { DTOClass, AssemblerClass } = resolverOpts + const { endpointName } = getDTONames(DTOClass) + + @Controller(endpointName) + class AutoResolver extends CRUDResolver(DTOClass, resolverOpts) { + constructor( + @InjectAssemblerQueryService(AssemblerClass as unknown as Class>) + service: QueryService + ) { + super(service) + } + } + + // need to set class name so DI works properly + Object.defineProperty(AutoResolver, 'name', { value: getEndpointToken(DTOClass), writable: false }) + return AutoResolver +} + +function createServiceAutoResolver( + resolverOpts: ServiceCRUDAutoResolverOpts +): Type { + const { DTOClass, ServiceClass } = resolverOpts + const { endpointName } = getDTONames(DTOClass) + + @Controller(endpointName) + class AutoResolver extends CRUDResolver(DTOClass, resolverOpts) { + constructor(@Inject(ServiceClass) service: QueryService) { + super(service) + } + } + + // need to set class name so DI works properly + Object.defineProperty(AutoResolver, 'name', { value: getEndpointToken(DTOClass), writable: false }) + return AutoResolver +} + +function createEndpoint< + DTO, + EntityServiceOrAssembler extends DeepPartial, + C, + U, + R, + PS extends PagingStrategies +>(resolverOpts: AutoResolverOpts): Type { + if (isAssemblerCRUDAutoResolverOpts(resolverOpts)) { + return createAssemblerAutoResolver(resolverOpts) + } else if (isServiceCRUDAutoResolverOpts(resolverOpts)) { + return createServiceAutoResolver(resolverOpts) + } + + return createEntityAutoResolver(resolverOpts) +} + +export const createEndpoints = ( + opts: AutoResolverOpts[] +): Type[] => opts.map((opt) => createEndpoint(opt)) diff --git a/packages/query-rest/src/resolvers/create.resolver.ts b/packages/query-rest/src/resolvers/create.resolver.ts new file mode 100644 index 000000000..afb0069f8 --- /dev/null +++ b/packages/query-rest/src/resolvers/create.resolver.ts @@ -0,0 +1,129 @@ +// eslint-disable-next-line max-classes-per-file +import { OmitType } from '@nestjs/swagger' +import { Class, DeepPartial, Filter, QueryService } from '@ptc-org/nestjs-query-core' +import omit from 'lodash.omit' + +import { DTONames, getDTONames } from '../common' +import { BodyHookArgs, Post } from '../decorators' +import { HookTypes } from '../hooks' +import { AuthorizerInterceptor, HookInterceptor } from '../interceptors' +import { CreateOneInputType, MutationArgsType } from '../types' +import { BaseServiceResolver, MutationOpts, ResolverClass, ServiceResolver } from './resolver.interface' + +export interface CreateResolverOpts> extends MutationOpts { + /** + * The Input DTO that should be used to create records. + */ + CreateDTOClass?: Class + /** + * The class to be used for `createOne` input. + */ + CreateOneInput?: Class> +} + +export interface CreateResolver> extends ServiceResolver { + createOne(input: MutationArgsType>, authorizeFilter?: Filter): Promise +} + +/** @internal */ +const defaultCreateDTO = (dtoNames: DTONames, DTOClass: Class): Class => { + const DefaultCreateDTO = OmitType(DTOClass, []) + + Object.defineProperty(DefaultCreateDTO, 'name', { + writable: false, + // set a unique name otherwise DI does not inject a unique one for each request + value: `Create${DTOClass.name}` + }) + + return DefaultCreateDTO as Class +} + +/** @internal */ +const defaultCreateOneInput = (DTOClass: Class, InputDTO: Class): Class> => { + return CreateOneInputType(DTOClass, InputDTO) +} + +/** + * @internal + * Mixin to add `create` graphql endpoints. + */ +export const Creatable = + >(DTOClass: Class, opts: CreateResolverOpts) => + >>(BaseClass: B): Class> & B => { + if (opts.disabled) { + return BaseClass as never + } + + const dtoNames = getDTONames(DTOClass, opts) + + const { + CreateDTOClass = defaultCreateDTO(dtoNames, DTOClass), + CreateOneInput = defaultCreateOneInput(DTOClass, CreateDTOClass) + } = opts + + const commonResolverOpts = omit(opts, 'dtoName', 'one', 'many', 'CreateDTOClass', 'CreateOneInput', 'CreateManyInput') + + class COI extends MutationArgsType(CreateOneInput) {} + + class CreateControllerBase extends BaseClass { + @Post( + () => DTOClass, + { + disabled: opts.disabled, + path: opts.one?.path, + operation: { + operationId: `${dtoNames.pluralBaseNameLower}.createOne`, + tags: [...(opts.tags || []), ...(opts.one?.tags ?? [])], + description: opts?.one?.description, + ...opts?.one?.operationOptions + }, + body: { + type: CreateDTOClass + } + }, + { + interceptors: [HookInterceptor(HookTypes.BEFORE_CREATE_ONE, CreateDTOClass, DTOClass), AuthorizerInterceptor(DTOClass)] + }, + commonResolverOpts, + opts.one ?? {} + ) + public async createOne(@BodyHookArgs() { input }: COI): Promise { + // Ignore `authorizeFilter` for now but give users the ability to throw an UnauthorizedException + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return this.service.createOne(input) + } + } + + return CreateControllerBase + } + +/** + * Factory to create a new abstract class that can be extended to add `create` endpoints. + * + * Assume we have `TodoItemDTO`, you can create a resolver with `createOneTodoItem` and `createManyTodoItems` graphql + * query endpoints using the following code. + * + * ```ts + * @Controller() + * export class TodoItemResolver extends CreateResolver(TodoItemDTO) { + * constructor(readonly service: TodoItemService) { + * super(service); + * } + * } + * ``` + * + * @param DTOClass - The DTO class that should be returned from the `createOne` and `createMany` endpoint. + * @param opts - Options to customize endpoints. + * @typeparam DTO - The type of DTO that should be created. + * @typeparam C - The create DTO type. + */ +// eslint-disable-next-line @typescript-eslint/no-redeclare -- intentional +export const CreateResolver = < + DTO, + C = DeepPartial, + QS extends QueryService = QueryService +>( + DTOClass: Class, + opts: CreateResolverOpts = {} +): ResolverClass> => Creatable(DTOClass, opts)(BaseServiceResolver) diff --git a/packages/query-rest/src/resolvers/crud.resolver.ts b/packages/query-rest/src/resolvers/crud.resolver.ts new file mode 100644 index 000000000..737dc6209 --- /dev/null +++ b/packages/query-rest/src/resolvers/crud.resolver.ts @@ -0,0 +1,126 @@ +import { Class, DeepPartial, QueryService } from '@ptc-org/nestjs-query-core' + +import { mergeBaseResolverOpts } from '../common' +import { ConnectionOptions } from '../connection/interfaces' +import { BaseResolverOptions } from '../decorators' +import { PagingStrategies } from '../types' +import { CreateResolver, CreateResolverOpts } from './create.resolver' +import { Deletable, DeleteResolverOpts } from './delete.resolver' +import { Readable, ReadResolverFromOpts, ReadResolverOpts } from './read.resolver' +import { MergePagingStrategyOpts, ResolverClass } from './resolver.interface' +import { Updateable, UpdateResolver, UpdateResolverOpts } from './update.resolver' + +export interface CRUDResolverOpts< + DTO, + C = DeepPartial, + U = DeepPartial, + R = ReadResolverOpts, + PS extends PagingStrategies = PagingStrategies.NONE +> extends BaseResolverOptions, + Pick { + /** + * The DTO that should be used as input for create endpoints. + */ + CreateDTOClass?: Class + /** + * The DTO that should be used as input for update endpoints. + */ + UpdateDTOClass?: Class + /** + * The DTO that should be used for filter of the aggregate endpoint. + */ + // AggregateDTOClass?: Class + pagingStrategy?: PS + create?: CreateResolverOpts + read?: R + update?: UpdateResolverOpts + delete?: DeleteResolverOpts + + tags?: string[] +} + +export interface CRUDResolver< + DTO, + C, + U, + R extends ReadResolverOpts, + QS extends QueryService = QueryService +> extends CreateResolver, + ReadResolverFromOpts, + UpdateResolver {} + +// DeleteResolver, +// AggregateResolver { + +// function extractAggregateResolverOpts( +// opts: CRUDResolverOpts, PagingStrategies> +// ): AggregateResolverOpts { +// const { AggregateDTOClass, enableAggregate, aggregate } = opts +// return mergeBaseResolverOpts>({ enabled: enableAggregate, AggregateDTOClass, ...aggregate }, opts) +// } + +function extractCreateResolverOpts( + opts: CRUDResolverOpts, PagingStrategies> +): CreateResolverOpts { + const { CreateDTOClass, create } = opts + return mergeBaseResolverOpts>({ CreateDTOClass, ...create }, opts) +} + +function extractReadResolverOpts, PS extends PagingStrategies>( + opts: CRUDResolverOpts +): MergePagingStrategyOpts { + const { enableTotalCount, pagingStrategy, read } = opts + return mergeBaseResolverOpts({ enableTotalCount, pagingStrategy, ...read } as MergePagingStrategyOpts, opts) +} + +function extractUpdateResolverOpts( + opts: CRUDResolverOpts, PagingStrategies> +): UpdateResolverOpts { + const { UpdateDTOClass, update } = opts + return mergeBaseResolverOpts>({ UpdateDTOClass, ...update }, opts) +} + +function extractDeleteResolverOpts( + opts: CRUDResolverOpts, PagingStrategies> +): DeleteResolverOpts { + const { delete: deleteArgs } = opts + return mergeBaseResolverOpts>(deleteArgs, opts) +} + +/** + * Factory to create a resolver that includes all CRUD methods from [[CreateResolver]], [[ReadResolver]], + * [[UpdateResolver]], and [[DeleteResolver]]. + * + * ```ts + * import { CRUDResolver } from '@ptc-org/nestjs-query-graphql'; + * import { Resolver } from '@nestjs/graphql'; + * import { TodoItemDTO } from './dto/todo-item.dto'; + * import { TodoItemService } from './todo-item.service'; + * + * @Resolver() + * export class TodoItemResolver extends CRUDResolver(TodoItemDTO) { + * constructor(readonly service: TodoItemService) { + * super(service); + * } + * } + * ``` + * @param DTOClass - The DTO Class that the resolver is for. All methods will use types derived from this class. + * @param opts - Options to customize the resolver. + */ +// eslint-disable-next-line @typescript-eslint/no-redeclare -- intentional +export const CRUDResolver = < + DTO, + C = DeepPartial, + U = DeepPartial, + R extends ReadResolverOpts = ReadResolverOpts, + PS extends PagingStrategies = PagingStrategies.NONE +>( + DTOClass: Class, + opts: CRUDResolverOpts = {} +): ResolverClass, CRUDResolver>> => { + const readable = Readable(DTOClass, extractReadResolverOpts(opts)) + const updatable = Updateable(DTOClass, extractUpdateResolverOpts(opts)) + const deleteResolver = Deletable(DTOClass, extractDeleteResolverOpts(opts)) + + return readable(deleteResolver(updatable(CreateResolver(DTOClass, extractCreateResolverOpts(opts))))) +} diff --git a/packages/query-rest/src/resolvers/delete.resolver.ts b/packages/query-rest/src/resolvers/delete.resolver.ts new file mode 100644 index 000000000..9d98c5e0c --- /dev/null +++ b/packages/query-rest/src/resolvers/delete.resolver.ts @@ -0,0 +1,72 @@ +// eslint-disable-next-line max-classes-per-file +import { Param } from '@nestjs/common' +import { Class, Filter, QueryService } from '@ptc-org/nestjs-query-core' +import omit from 'lodash.omit' + +import { OperationGroup } from '../auth' +import { getDTONames } from '../common' +import { AuthorizerFilter, Delete } from '../decorators' +import { AuthorizerInterceptor } from '../interceptors' +import { BaseServiceResolver, MutationOpts, ResolverClass, ServiceResolver } from './resolver.interface' + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export interface DeleteResolverOpts extends MutationOpts { + /** + * Use soft delete when doing delete mutation + */ + useSoftDelete?: boolean +} + +export interface DeleteResolver> extends ServiceResolver { + deleteOne(id: string, authorizeFilter?: Filter): Promise> +} + +/** + * @internal + * Mixin to add `delete` graphql endpoints. + */ +export const Deletable = + >(DTOClass: Class, opts: DeleteResolverOpts) => + >>(BaseClass: B): Class> & B => { + const dtoNames = getDTONames(DTOClass, opts) + + const commonResolverOpts = omit(opts, 'dtoName', 'one', 'many', 'DeleteOneInput', 'DeleteManyInput', 'useSoftDelete') + + class DeleteResolverBase extends BaseClass { + @Delete( + () => DTOClass, + { + path: opts?.one?.path ?? ':id', + operation: { + operationId: `${dtoNames.pluralBaseNameLower}.deleteOne`, + tags: [...(opts.tags || []), ...(opts.one?.tags ?? [])], + description: opts?.one?.description, + ...opts?.one?.operationOptions + } + }, + { interceptors: [AuthorizerInterceptor(DTOClass)] }, + commonResolverOpts, + opts.one ?? {} + ) + async deleteOne( + @Param('id') id: string, + @AuthorizerFilter({ + operationGroup: OperationGroup.DELETE, + many: false + }) + authorizeFilter?: Filter + ): Promise> { + return this.service.deleteOne(id, { + filter: authorizeFilter ?? {}, + useSoftDelete: opts?.useSoftDelete ?? false + }) + } + } + + return DeleteResolverBase + } +// eslint-disable-next-line @typescript-eslint/no-redeclare -- intentional +export const DeleteResolver = = QueryService>( + DTOClass: Class, + opts: DeleteResolverOpts = {} +): ResolverClass> => Deletable(DTOClass, opts)(BaseServiceResolver) diff --git a/packages/query-rest/src/resolvers/index.ts b/packages/query-rest/src/resolvers/index.ts new file mode 100644 index 000000000..ca1fd25d6 --- /dev/null +++ b/packages/query-rest/src/resolvers/index.ts @@ -0,0 +1,6 @@ +export { CreateResolver, CreateResolverOpts } from './create.resolver' +export { CRUDResolver, CRUDResolverOpts } from './crud.resolver' +// export { DeleteResolver, DeleteResolverOpts } from './delete.resolver' +export { ReadResolver, ReadResolverOpts } from './read.resolver' +export { ResolverOpts } from './resolver.interface' +// export { UpdateResolver, UpdateResolverOpts } from './update.resolver' diff --git a/packages/query-rest/src/resolvers/read.resolver.ts b/packages/query-rest/src/resolvers/read.resolver.ts new file mode 100644 index 000000000..fcb65d390 --- /dev/null +++ b/packages/query-rest/src/resolvers/read.resolver.ts @@ -0,0 +1,149 @@ +import { BadRequestException, Param } from '@nestjs/common' +import { Class, Filter, mergeQuery, QueryService } from '@ptc-org/nestjs-query-core' +import omit from 'lodash.omit' + +import { OperationGroup } from '../auth' +import { getDTONames } from '../common' +import { ConnectionOptions, InferConnectionTypeFromStrategy } from '../connection/interfaces' +import { AuthorizerFilter, Get, QueryHookArgs } from '../decorators' +import { HookTypes } from '../hooks' +import { AuthorizerInterceptor, HookInterceptor } from '../interceptors' +import { OffsetQueryArgsTypeOpts, PagingStrategies, QueryArgsType, QueryArgsTypeOpts, QueryType, StaticQueryType } from '../types' +import { BaseServiceResolver, ExtractPagingStrategy, ResolverClass, ResolverOpts, ServiceResolver } from './resolver.interface' + +export type ReadResolverFromOpts< + DTO, + Opts extends ReadResolverOpts, + QS extends QueryService +> = ReadResolver, QS> + +export type ReadResolverOpts = { + QueryArgs?: StaticQueryType + + /** + * DTO to return with finding one record + */ + FindDTOClass?: Class +} & ResolverOpts & + QueryArgsTypeOpts & + Pick + +export interface ReadResolver> + extends ServiceResolver { + queryMany( + query: QueryType, + authorizeFilter?: Filter + ): Promise> + + findById(id: string, authorizeFilter?: Filter): Promise +} + +/** + * @internal + * Mixin to add `read` graphql endpoints. + */ +export const Readable = + , QS extends QueryService>( + DTOClass: Class, + opts: ReadOpts + ) => + >>(BaseClass: B): Class> & B => { + const dtoNames = getDTONames(DTOClass, opts) + const { + QueryArgs = QueryArgsType(DTOClass, { ...opts, connectionName: `${dtoNames.baseName}Connection` }), + FindDTOClass = DTOClass + } = opts + + const commonResolverOpts = omit(opts, 'dtoName', 'one', 'many', 'QueryArgs', 'FindDTOClass', 'Connection', 'withDeleted') + + class QA extends QueryArgs {} + + Object.defineProperty(QA, 'name', { + writable: false, + // set a unique name otherwise DI does not inject a unique one for each request + value: `${DTOClass.name}QueryArgs` + }) + + class ReadResolverBase extends BaseClass { + @Get( + () => FindDTOClass, + { + path: opts?.one?.path ?? ':id', + operation: { + operationId: `${dtoNames.pluralBaseNameLower}.findById`, + tags: [...(opts.tags || []), ...(opts.one?.tags ?? [])], + description: opts?.one?.description, + ...opts?.one?.operationOptions + } + }, + { interceptors: [HookInterceptor(HookTypes.BEFORE_FIND_ONE, DTOClass), AuthorizerInterceptor(DTOClass)] }, + commonResolverOpts, + opts.one ?? {} + ) + public async findById( + @Param('id') id: string, + @AuthorizerFilter({ + operationGroup: OperationGroup.READ, + many: false + }) + authorizeFilter?: Filter + ): Promise { + if (!id) { + throw new BadRequestException('id is missing from the request!') + } + + return this.service.getById(id, { + filter: authorizeFilter, + withDeleted: opts?.one?.withDeleted + }) + } + + @Get( + () => QueryArgs.ConnectionType, + { + path: opts?.many?.path, + operation: { + operationId: `${dtoNames.pluralBaseNameLower}.queryMany`, + tags: [...(opts.tags || []), ...(opts.many?.tags ?? [])], + description: opts?.many?.description, + ...opts?.many?.operationOptions + } + }, + { interceptors: [HookInterceptor(HookTypes.BEFORE_QUERY_MANY, DTOClass), AuthorizerInterceptor(DTOClass)] }, + commonResolverOpts, + opts.many ?? {} + ) + public async queryMany( + @QueryHookArgs() query: QA, + @AuthorizerFilter({ + operationGroup: OperationGroup.READ, + many: true + }) + authorizeFilter?: Filter + ): Promise> { + return QueryArgs.ConnectionType.createFromPromise( + (q) => + this.service.query(q, { + withDeleted: opts?.many?.withDeleted + }), + mergeQuery(query, { filter: authorizeFilter }), + (filter) => + this.service.count(filter, { + withDeleted: opts?.many?.withDeleted + }) + ) + } + } + + return ReadResolverBase as Class> & B + } + +// eslint-disable-next-line @typescript-eslint/no-redeclare -- intentional +export const ReadResolver = < + DTO, + ReadOpts extends ReadResolverOpts = OffsetQueryArgsTypeOpts, + QS extends QueryService = QueryService +>( + DTOClass: Class, + opts: ReadOpts = {} as ReadOpts +): ResolverClass> => Readable(DTOClass, opts)(BaseServiceResolver) diff --git a/packages/query-rest/src/resolvers/resolver.interface.ts b/packages/query-rest/src/resolvers/resolver.interface.ts new file mode 100644 index 000000000..27c8cde10 --- /dev/null +++ b/packages/query-rest/src/resolvers/resolver.interface.ts @@ -0,0 +1,62 @@ +import { ApiOperationOptions } from '@nestjs/swagger' +import { QueryService } from '@ptc-org/nestjs-query-core' + +import { DTONamesOpts } from '../common' +import { QueryOptionsDecoratorOpts, QueryResolverMethodOpts } from '../decorators' +import { PagingStrategies, QueryArgsTypeOpts } from '../types' + +export type NamedEndpoint = { + /** Specify to override the name of the graphql query or mutation * */ + path?: string + /** Specify a description for the graphql query or mutation* */ + description?: string + operationOptions?: ApiOperationOptions +} + +export interface ResolverOpts extends QueryResolverMethodOpts, DTONamesOpts { + /** + * Options for single record graphql endpoints + */ + one?: QueryResolverMethodOpts & NamedEndpoint + /** + * Options for multiple record graphql endpoints + */ + many?: QueryResolverMethodOpts & NamedEndpoint +} + +export type MutationOpts = Omit + +/** @internal */ +export interface ServiceResolver> { + service: QS +} + +/** @internal */ +export interface ResolverClass, Resolver extends ServiceResolver> { + new (service: QS): Resolver +} + +/** + * @internal + * Base Resolver that takes in a service as a constructor argument. + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export class BaseServiceResolver { + constructor(readonly service: QS) {} +} + +export type ExtractPagingStrategy> = Opts['pagingStrategy'] extends PagingStrategies + ? Opts['pagingStrategy'] + : PagingStrategies.NONE + +export type MergePagingStrategyOpts< + DTO, + Opts extends QueryOptionsDecoratorOpts, + S extends PagingStrategies = PagingStrategies.NONE +> = Opts['pagingStrategy'] extends PagingStrategies + ? Opts + : S extends PagingStrategies + ? Omit & { + pagingStrategy: S + } + : Opts diff --git a/packages/query-rest/src/resolvers/update.resolver.ts b/packages/query-rest/src/resolvers/update.resolver.ts new file mode 100644 index 000000000..8e94b5c65 --- /dev/null +++ b/packages/query-rest/src/resolvers/update.resolver.ts @@ -0,0 +1,105 @@ +// eslint-disable-next-line max-classes-per-file +import { Param } from '@nestjs/common' +import { PartialType } from '@nestjs/mapped-types' +import { Class, DeepPartial, Filter, QueryService } from '@ptc-org/nestjs-query-core' +import omit from 'lodash.omit' + +import { OperationGroup } from '../auth' +import { getDTONames } from '../common' +import { AuthorizerFilter, BodyHookArgs, Put } from '../decorators' +import { HookTypes } from '../hooks' +import { AuthorizerInterceptor, HookInterceptor } from '../interceptors' +import { MutationArgsType, UpdateOneInputType } from '../types' +import { BaseServiceResolver, MutationOpts, ResolverClass, ServiceResolver } from './resolver.interface' + +export interface UpdateResolverOpts> extends MutationOpts { + UpdateDTOClass?: Class + UpdateOneInput?: Class> +} + +export interface UpdateResolver> extends ServiceResolver { + updateOne(id: string, input: MutationArgsType>, authFilter?: Filter): Promise +} + +/** @internal */ +const defaultUpdateDTO = (DTOClass: Class): Class => { + const DefaultUpdateDTO = PartialType(DTOClass) as Class + + Object.defineProperty(DefaultUpdateDTO, 'name', { + writable: false, + // set a unique name otherwise DI does not inject a unique one for each request + value: `Update${DTOClass.name}` + }) + + return DefaultUpdateDTO +} + +const defaultUpdateOneInput = (DTOClass: Class, UpdateDTO: Class): Class> => { + return UpdateOneInputType(DTOClass, UpdateDTO) +} + +/** + * @internal + * Mixin to add `update` graphql endpoints. + */ +export const Updateable = + >(DTOClass: Class, opts: UpdateResolverOpts) => + >>(BaseClass: B): Class> & B => { + if (opts.disabled) { + return BaseClass as never + } + + const dtoNames = getDTONames(DTOClass, opts) + + const { UpdateDTOClass = defaultUpdateDTO(DTOClass), UpdateOneInput = defaultUpdateOneInput(DTOClass, UpdateDTOClass) } = opts + + const commonResolverOpts = omit(opts, 'dtoName', 'one', 'many', 'UpdateDTOClass', 'UpdateOneInput', 'UpdateManyInput') + + class UOI extends MutationArgsType(UpdateOneInput) {} + + class UpdateResolverBase extends BaseClass { + @Put( + () => DTOClass, + { + path: opts?.one?.path ?? ':id', + operation: { + operationId: `${dtoNames.pluralBaseNameLower}.updateOne`, + tags: [...(opts.tags || []), ...(opts.one?.tags ?? [])], + description: opts?.one?.description, + ...opts?.one?.operationOptions + }, + body: { + type: UpdateDTOClass + } + }, + { + interceptors: [HookInterceptor(HookTypes.BEFORE_UPDATE_ONE, UpdateDTOClass, DTOClass), AuthorizerInterceptor(DTOClass)] + }, + commonResolverOpts, + opts?.one ?? {} + ) + public updateOne( + @Param('id') id: string, + @BodyHookArgs() { input }: UOI, + @AuthorizerFilter({ + operationGroup: OperationGroup.UPDATE, + many: false + }) + authorizeFilter?: Filter + ): Promise { + return this.service.updateOne(id, input.update, { filter: authorizeFilter ?? {} }) + } + } + + return UpdateResolverBase + } + +// eslint-disable-next-line @typescript-eslint/no-redeclare -- intentional +export const UpdateResolver = < + DTO, + U = DeepPartial, + QS extends QueryService = QueryService +>( + DTOClass: Class, + opts: UpdateResolverOpts = {} +): ResolverClass> => Updateable(DTOClass, opts)(BaseServiceResolver) diff --git a/packages/query-rest/src/types/create-one-input.type.ts b/packages/query-rest/src/types/create-one-input.type.ts new file mode 100644 index 000000000..f340ad764 --- /dev/null +++ b/packages/query-rest/src/types/create-one-input.type.ts @@ -0,0 +1,30 @@ +import { Class } from '@ptc-org/nestjs-query-core' + +export interface CreateOneInputType { + input: C +} + +/** + * The abstract input type for create one operations. + * + * @param fieldName - The name of the field to be exposed in the graphql schema + * @param InputClass - The InputType to be used. + */ +// eslint-disable-next-line @typescript-eslint/no-redeclare -- intentional +export function CreateOneInputType(DTOClass: Class, InputClass: Class): Class> { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + class CreateOneInput extends InputClass implements CreateOneInputType { + public get input() { + return this as never as C + } + } + + Object.defineProperty(InputClass, 'name', { + writable: false, + // set a unique name otherwise DI does not inject a unique one for each request + value: `Create${DTOClass.name}` + }) + + return CreateOneInput +} diff --git a/packages/query-rest/src/types/index.ts b/packages/query-rest/src/types/index.ts new file mode 100644 index 000000000..b88f04daf --- /dev/null +++ b/packages/query-rest/src/types/index.ts @@ -0,0 +1,6 @@ +export * from './create-one-input.type' +export * from './mutation-args.type' +export * from './query' +export * from './query-args.type' +export * from './rest-query.type' +export * from './update-one-input.type' diff --git a/packages/query-rest/src/types/mutation-args.type.ts b/packages/query-rest/src/types/mutation-args.type.ts new file mode 100644 index 000000000..13487180e --- /dev/null +++ b/packages/query-rest/src/types/mutation-args.type.ts @@ -0,0 +1,18 @@ +import { Type } from '@nestjs/common' +import { Class } from '@ptc-org/nestjs-query-core' + +export interface MutationArgsType { + input: Input +} + +// eslint-disable-next-line @typescript-eslint/no-redeclare -- intentional +export function MutationArgsType(InputClass: Class): Class> { + + class MutationArgs extends (InputClass as Type) implements MutationArgsType { + public get input() { + return this as never as Input + } + } + + return MutationArgs +} diff --git a/packages/query-rest/src/types/query-args.type.ts b/packages/query-rest/src/types/query-args.type.ts new file mode 100644 index 000000000..d6422e776 --- /dev/null +++ b/packages/query-rest/src/types/query-args.type.ts @@ -0,0 +1,48 @@ +import { Class } from '@ptc-org/nestjs-query-core' + +import { removeUndefinedValues } from '../common' +import { getQueryOptions } from '../decorators' +import { + DEFAULT_QUERY_OPTS, NonePagingQueryArgsTypeOpts, + OffsetQueryArgsTypeOpts, + PagingStrategies, + QueryArgsTypeOpts, + StaticQueryType +} from './query' +import { createOffsetQueryArgs } from './query/query-args/offset-query-args.type' + +const getMergedQueryOpts = (DTOClass: Class, opts?: QueryArgsTypeOpts): QueryArgsTypeOpts => { + const decoratorOpts = getQueryOptions(DTOClass) + return { + ...DEFAULT_QUERY_OPTS, + pagingStrategy: PagingStrategies.OFFSET, + ...removeUndefinedValues(decoratorOpts ?? {}), + ...removeUndefinedValues(opts ?? {}) + } +} + +// tests if the object is a QueryArgs Class +// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/explicit-module-boundary-types +export const isStaticQueryArgsType = (obj: any): obj is StaticQueryType => + typeof obj === 'function' && 'PageType' in obj && 'SortType' in obj && 'FilterType' in obj + +export function QueryArgsType( + DTOClass: Class, + opts: OffsetQueryArgsTypeOpts +): StaticQueryType +export function QueryArgsType( + DTOClass: Class, + opts: NonePagingQueryArgsTypeOpts +): StaticQueryType + +export function QueryArgsType(DTOClass: Class, opts?: QueryArgsTypeOpts): StaticQueryType +export function QueryArgsType(DTOClass: Class, opts?: QueryArgsTypeOpts): StaticQueryType { + // override any options from the DTO with the options passed in + const mergedOpts = getMergedQueryOpts(DTOClass, opts) + if (mergedOpts.pagingStrategy === PagingStrategies.OFFSET) { + return createOffsetQueryArgs(DTOClass, mergedOpts) + } + + return createOffsetQueryArgs(DTOClass, mergedOpts as any) + // return createNonePagingQueryArgsType(DTOClass, mergedOpts) +} diff --git a/packages/query-rest/src/types/query/buildable-query.type.ts b/packages/query-rest/src/types/query/buildable-query.type.ts new file mode 100644 index 000000000..f8a923a69 --- /dev/null +++ b/packages/query-rest/src/types/query/buildable-query.type.ts @@ -0,0 +1,5 @@ +import { RestQuery } from '../rest-query.type' + +export interface BuildableQueryType { + buildQuery(): RestQuery +} diff --git a/packages/query-rest/src/types/query/index.ts b/packages/query-rest/src/types/query/index.ts new file mode 100644 index 000000000..6afae4c17 --- /dev/null +++ b/packages/query-rest/src/types/query/index.ts @@ -0,0 +1,3 @@ +export * from './offset-paging.type' +export * from './paging' +export * from './query-args' diff --git a/packages/query-rest/src/types/query/offset-paging.type.ts b/packages/query-rest/src/types/query/offset-paging.type.ts new file mode 100644 index 000000000..73812ee6e --- /dev/null +++ b/packages/query-rest/src/types/query/offset-paging.type.ts @@ -0,0 +1,28 @@ +import { ApiProperty } from '@nestjs/swagger' +import { Paging } from '@ptc-org/nestjs-query-core' +import { Expose, Type } from 'class-transformer' +import { IsNumber, IsOptional, Max, Min } from 'class-validator' + +export class OffsetPaging implements Paging { + @Expose() + @IsOptional() + @IsNumber() + @Type(() => Number) + @Min(1) + @Max(50) + @ApiProperty({ + nullable: true, + required: false + }) + limit?: number + + @Expose() + @IsOptional() + @IsNumber() + @Type(() => Number) + @ApiProperty({ + nullable: true, + required: false + }) + offset?: number +} diff --git a/packages/query-rest/src/types/query/paging/constants.ts b/packages/query-rest/src/types/query/paging/constants.ts new file mode 100644 index 000000000..4994dbdee --- /dev/null +++ b/packages/query-rest/src/types/query/paging/constants.ts @@ -0,0 +1,5 @@ +export enum PagingStrategies { + // CURSOR = 'cursor', + OFFSET = 'offset', + NONE = 'none' +} diff --git a/packages/query-rest/src/types/query/paging/index.ts b/packages/query-rest/src/types/query/paging/index.ts new file mode 100644 index 000000000..8a3d08dba --- /dev/null +++ b/packages/query-rest/src/types/query/paging/index.ts @@ -0,0 +1,3 @@ +export { PagingStrategies } from './constants' +export { InferPagingTypeFromStrategy, NonePagingType, OffsetPagingType, PagingTypes } from './interfaces' +export { getOrCreateNonePagingType } from './none-paging.type' diff --git a/packages/query-rest/src/types/query/paging/interfaces.ts b/packages/query-rest/src/types/query/paging/interfaces.ts new file mode 100644 index 000000000..1f6a6c9ee --- /dev/null +++ b/packages/query-rest/src/types/query/paging/interfaces.ts @@ -0,0 +1,14 @@ +import { Paging } from '@ptc-org/nestjs-query-core' + +import { PagingStrategies } from './constants' + +export type NonePagingType = Paging +export type OffsetPagingType = Paging + +// eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents +export type PagingTypes = OffsetPagingType | NonePagingType +export type InferPagingTypeFromStrategy = PS extends PagingStrategies.OFFSET + ? OffsetPagingType + : PS extends PagingStrategies.NONE + ? NonePagingType + : never diff --git a/packages/query-rest/src/types/query/paging/none-paging.type.ts b/packages/query-rest/src/types/query/paging/none-paging.type.ts new file mode 100644 index 000000000..2b0ba988e --- /dev/null +++ b/packages/query-rest/src/types/query/paging/none-paging.type.ts @@ -0,0 +1,19 @@ +import { Class } from '@ptc-org/nestjs-query-core' + +import { PagingStrategies } from './constants' +import { NonePagingType } from './interfaces' + +let graphQLPaging: Class | null = null +// eslint-disable-next-line @typescript-eslint/no-redeclare -- intentional +export const getOrCreateNonePagingType = (): Class => { + if (graphQLPaging) { + return graphQLPaging + } + + class GraphQLPagingImpl implements NonePagingType { + static strategy: PagingStrategies.NONE = PagingStrategies.NONE + } + + graphQLPaging = GraphQLPagingImpl + return graphQLPaging +} diff --git a/packages/query-rest/src/types/query/query-args/constants.ts b/packages/query-rest/src/types/query/query-args/constants.ts new file mode 100644 index 000000000..0e91249ac --- /dev/null +++ b/packages/query-rest/src/types/query/query-args/constants.ts @@ -0,0 +1,6 @@ +export const DEFAULT_QUERY_OPTS = { + defaultResultSize: 10, + maxResultsSize: 50, + defaultSort: [], + defaultFilter: {} +} diff --git a/packages/query-rest/src/types/query/query-args/index.ts b/packages/query-rest/src/types/query/query-args/index.ts new file mode 100644 index 000000000..52cd7a516 --- /dev/null +++ b/packages/query-rest/src/types/query/query-args/index.ts @@ -0,0 +1,2 @@ +export * from './constants' +export * from './interfaces' diff --git a/packages/query-rest/src/types/query/query-args/interfaces.ts b/packages/query-rest/src/types/query/query-args/interfaces.ts new file mode 100644 index 000000000..7c63b7367 --- /dev/null +++ b/packages/query-rest/src/types/query/query-args/interfaces.ts @@ -0,0 +1,61 @@ +import { Class, Filter, Query, SortField } from '@ptc-org/nestjs-query-core' + +import { ArrayConnectionOptions, OffsetConnectionOptions, StaticConnectionType } from '../../../connection/interfaces' +import { InferPagingTypeFromStrategy, PagingStrategies } from '../paging' + +export type BaseQueryArgsTypeOpts = { + /** + * Support the `query=term` query param which can be used inside the before query many + * to build an filter + */ + enableSearch?: boolean + /** + * The default number of results to return. + * [Default=10] + */ + defaultResultSize?: number + /** + * The maximum number of results that can be returned from a query. + * [Default=50] + */ + maxResultsSize?: number + /** + * The default sort for queries. + * [Default=[]] + */ + defaultSort?: SortField[] + /** + * Disable the sorting + */ + disableSort?: boolean + /** + * Default filter. + * [Default=\{\}] + */ + defaultFilter?: Filter + /** + * Disable the filtering + */ + disableFilter?: boolean +} + +export interface OffsetQueryArgsTypeOpts extends BaseQueryArgsTypeOpts, OffsetConnectionOptions { + pagingStrategy?: PagingStrategies.OFFSET +} + +export interface NonePagingQueryArgsTypeOpts extends BaseQueryArgsTypeOpts, ArrayConnectionOptions { + pagingStrategy?: PagingStrategies.NONE +} + +export type QueryArgsTypeOpts = OffsetQueryArgsTypeOpts | NonePagingQueryArgsTypeOpts + +export interface StaticQueryType extends Class> { + SortType: Class> + PageType: Class> + FilterType: Class> + ConnectionType: StaticConnectionType +} + +export interface QueryType extends Query { + paging?: InferPagingTypeFromStrategy +} diff --git a/packages/query-rest/src/types/query/query-args/offset-query-args.type.ts b/packages/query-rest/src/types/query/query-args/offset-query-args.type.ts new file mode 100644 index 000000000..8d853f1b2 --- /dev/null +++ b/packages/query-rest/src/types/query/query-args/offset-query-args.type.ts @@ -0,0 +1,57 @@ +import { Class } from '@ptc-org/nestjs-query-core' + +import { getOrCreateOffsetConnectionType } from '../../../connection/offset/offset-connection.type' +import { Field, SkipIf } from '../../../decorators' +import { RestQuery } from '../../rest-query.type' +import { BuildableQueryType } from '../buildable-query.type' +import { OffsetPaging } from '../offset-paging.type' +import { PagingStrategies } from '../paging' +import { DEFAULT_QUERY_OPTS } from './constants' +import { OffsetQueryArgsTypeOpts, StaticQueryType } from './interfaces' + +export function createOffsetQueryArgs( + DTOClass: Class, + opts: OffsetQueryArgsTypeOpts = { ...DEFAULT_QUERY_OPTS, pagingStrategy: PagingStrategies.OFFSET } +): StaticQueryType { + // const F = FilterType(DTOClass) + // const S = getOrCreateSortType(DTOClass) + const ConnectionType = getOrCreateOffsetConnectionType(DTOClass, opts) + + class QueryArgs extends OffsetPaging implements BuildableQueryType { + static SortType = null + + static FilterType = null + + static ConnectionType = ConnectionType + + static PageType = OffsetPaging + + public sorting = opts.defaultSort + + public filter = opts.defaultFilter + + @SkipIf( + () => !opts.enableSearch, + Field({ + nullable: true, + required: false + }) + ) + public query?: string + + public buildQuery(): RestQuery { + return { + query: this.query, + paging: { + limit: this.limit || opts.maxResultsSize, + offset: this.offset + }, + filter: this.filter, + sorting: this.sorting, + relations: [] + } + } + } + + return QueryArgs +} diff --git a/packages/query-rest/src/types/rest-query.type.ts b/packages/query-rest/src/types/rest-query.type.ts new file mode 100644 index 000000000..d6d616fd5 --- /dev/null +++ b/packages/query-rest/src/types/rest-query.type.ts @@ -0,0 +1,5 @@ +import { Query } from '@ptc-org/nestjs-query-core' + +export interface RestQuery extends Query { + query?: string +} diff --git a/packages/query-rest/src/types/update-one-input.type.ts b/packages/query-rest/src/types/update-one-input.type.ts new file mode 100644 index 000000000..d1b7a8cae --- /dev/null +++ b/packages/query-rest/src/types/update-one-input.type.ts @@ -0,0 +1,32 @@ +import { Class } from '@ptc-org/nestjs-query-core' + +export interface UpdateOneInputType { + update: U +} + +/** + * The abstract input type for create one operations. + * + * @param fieldName - The name of the field to be exposed in the graphql schema + * @param UpdateClass - The InputType to be used. + */ +// eslint-disable-next-line @typescript-eslint/no-redeclare -- intentional +export function UpdateOneInputType(DTOClass: Class, UpdateClass: Class): Class> { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + class UpdateOneInput extends UpdateClass implements UpdateOneInputType { + + public get update() { + return this as never as U + } + + } + + Object.defineProperty(UpdateOneInput, 'name', { + writable: false, + // set a unique name otherwise DI does not inject a unique one for each request + value: `Update${DTOClass.name}` + }) + + return UpdateOneInput +} diff --git a/packages/query-rest/src/types/validators/index.ts b/packages/query-rest/src/types/validators/index.ts new file mode 100644 index 000000000..3a1aa92fc --- /dev/null +++ b/packages/query-rest/src/types/validators/index.ts @@ -0,0 +1 @@ +export * from './is-undefined.validator' diff --git a/packages/query-rest/src/types/validators/is-undefined.validator.ts b/packages/query-rest/src/types/validators/is-undefined.validator.ts new file mode 100644 index 000000000..ec33663f3 --- /dev/null +++ b/packages/query-rest/src/types/validators/is-undefined.validator.ts @@ -0,0 +1,8 @@ +import { ValidateIf, ValidationOptions } from 'class-validator' + +/** @internal */ +export function IsUndefined(validationOptions?: ValidationOptions) { + // eslint-disable-next-line @typescript-eslint/ban-types + return (obj: Object, property: string) => + ValidateIf((o: Record) => o[property] !== undefined, validationOptions)(obj, property) +} diff --git a/packages/query-rest/tsconfig.json b/packages/query-rest/tsconfig.json new file mode 100644 index 000000000..546e940e2 --- /dev/null +++ b/packages/query-rest/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/query-rest/tsconfig.lib.json b/packages/query-rest/tsconfig.lib.json new file mode 100644 index 000000000..84752388e --- /dev/null +++ b/packages/query-rest/tsconfig.lib.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": [ + "node" + ] + }, + "exclude": [ + "jest.config.ts", + "**/*.spec.ts", + "**/*.test.ts", + "__tests__" + ], + "include": [ + "**/*.ts" + ] +} diff --git a/packages/query-rest/tsconfig.spec.json b/packages/query-rest/tsconfig.spec.json new file mode 100644 index 000000000..ee46e96db --- /dev/null +++ b/packages/query-rest/tsconfig.spec.json @@ -0,0 +1,23 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "jest.config.ts", + "**/*.test.ts", + "**/*.spec.ts", + "**/*.test.tsx", + "**/*.spec.tsx", + "**/*.test.js", + "**/*.spec.js", + "**/*.test.jsx", + "**/*.spec.jsx", + "**/*.d.ts" + ] +} diff --git a/tsconfig.json b/tsconfig.json index a32e59990..b7c1202f6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,7 +23,8 @@ "@ptc-org/nestjs-query-mongoose": ["packages/query-mongoose/src/index.ts"], "@ptc-org/nestjs-query-sequelize": ["packages/query-sequelize/src/index.ts"], "@ptc-org/nestjs-query-typegoose": ["packages/query-typegoose/src/index.ts"], - "@ptc-org/nestjs-query-typeorm": ["packages/query-typeorm/src/index.ts"] + "@ptc-org/nestjs-query-typeorm": ["packages/query-typeorm/src/index.ts"], + "@ptc-org/nestjs-query-rest": ["packages/query-rest/src/index.ts"] } } } diff --git a/yarn.lock b/yarn.lock index 77eb91369..1c3ed7a12 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4665,6 +4665,23 @@ __metadata: languageName: node linkType: hard +"@nestjs/mapped-types@npm:2.0.3": + version: 2.0.3 + resolution: "@nestjs/mapped-types@npm:2.0.3" + peerDependencies: + "@nestjs/common": ^8.0.0 || ^9.0.0 || ^10.0.0 + class-transformer: ^0.4.0 || ^0.5.0 + class-validator: ^0.13.0 || ^0.14.0 + reflect-metadata: ^0.1.12 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + checksum: 4f9ab9fc753cb2ef4a0f929b5ad93b737bfcbe5a2847e684ff095ed7ea4b4c00d0a980d68249edb2b25874abf4d931274f6dc5ad93b2bd65456f3880938b7cf2 + languageName: node + linkType: hard + "@nestjs/mongoose@npm:10.0.1": version: 10.0.1 resolution: "@nestjs/mongoose@npm:10.0.1" @@ -4750,6 +4767,33 @@ __metadata: languageName: node linkType: hard +"@nestjs/swagger@npm:^7.1.15": + version: 7.1.15 + resolution: "@nestjs/swagger@npm:7.1.15" + dependencies: + "@nestjs/mapped-types": 2.0.3 + js-yaml: 4.1.0 + lodash: 4.17.21 + path-to-regexp: 3.2.0 + swagger-ui-dist: 5.9.1 + peerDependencies: + "@fastify/static": ^6.0.0 + "@nestjs/common": ^9.0.0 || ^10.0.0 + "@nestjs/core": ^9.0.0 || ^10.0.0 + class-transformer: "*" + class-validator: "*" + reflect-metadata: ^0.1.12 + peerDependenciesMeta: + "@fastify/static": + optional: true + class-transformer: + optional: true + class-validator: + optional: true + checksum: f23b9c3ebc9827440a071c79d16542c643a59433c84a99885989d4e4aa652676105c1c7023851d0696aacfadae5433d4799e595d09c3e6ea3bb78921a61d5816 + languageName: node + linkType: hard + "@nestjs/testing@npm:^10.2.7": version: 10.2.7 resolution: "@nestjs/testing@npm:10.2.7" @@ -5355,6 +5399,26 @@ __metadata: languageName: unknown linkType: soft +"@ptc-org/nestjs-query-rest@workspace:packages/query-rest": + version: 0.0.0-use.local + resolution: "@ptc-org/nestjs-query-rest@workspace:packages/query-rest" + dependencies: + lodash.omit: ^4.5.0 + lower-case-first: ^2.0.2 + pluralize: ^8.0.0 + tslib: ^2.6.2 + upper-case-first: ^2.0.2 + peerDependencies: + "@nestjs/common": ^9.0.0 || ^10.0.0 + "@nestjs/core": ^9.0.0 || ^10.0.0 + "@nestjs/graphql": ^11.0.0 || ^12.0.0 + "@nestjs/swagger": ^7.0.0 + class-transformer: ^0.5 + class-validator: ^0.14.0 + ts-morph: ^19.0.0 + languageName: unknown + linkType: soft + "@ptc-org/nestjs-query-sequelize@workspace:packages/query-sequelize": version: 0.0.0-use.local resolution: "@ptc-org/nestjs-query-sequelize@workspace:packages/query-sequelize" @@ -16056,6 +16120,7 @@ __metadata: "@nestjs/platform-express": 10.2.7 "@nestjs/schematics": 10.0.3 "@nestjs/sequelize": 10.0.0 + "@nestjs/swagger": ^7.1.15 "@nestjs/testing": ^10.2.7 "@nestjs/typeorm": ^10.0.0 "@nx-plus/docusaurus": ^15.0.0-rc.0 @@ -20196,6 +20261,13 @@ __metadata: languageName: node linkType: hard +"swagger-ui-dist@npm:5.9.1": + version: 5.9.1 + resolution: "swagger-ui-dist@npm:5.9.1" + checksum: 4e5cf278ef1725b0770124ef4ecc9f75cb6fbab5d3e68aefffa024131aaf7b904514dc07000bb6b6ecdb606892fe5fa6e5f55d8fec978a838a18d25a010dab5b + languageName: node + linkType: hard + "symbol-observable@npm:4.0.0, symbol-observable@npm:^4.0.0": version: 4.0.0 resolution: "symbol-observable@npm:4.0.0" From 49cd3a424bd5c0810c25fc1758a887ae17287132 Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Thu, 9 Nov 2023 13:45:42 +0100 Subject: [PATCH 02/32] refactor(rest): Linting --- packages/query-rest/package.json | 2 +- packages/query-rest/src/connection/offset/pager/pager.ts | 2 +- packages/query-rest/src/decorators/field.decorator.ts | 2 +- packages/query-rest/src/decorators/hook-args.decorator.ts | 4 ++-- packages/query-rest/src/hooks/hooks.ts | 3 ++- .../query-rest/src/interceptors/authorizer.interceptor.ts | 2 +- packages/query-rest/src/interceptors/hook.interceptor.ts | 2 +- packages/query-rest/src/interfaces/return-type-func.ts | 4 ++-- packages/query-rest/src/module.ts | 1 + packages/query-rest/src/resolvers/update.resolver.ts | 2 +- packages/query-rest/src/types/mutation-args.type.ts | 1 - packages/query-rest/src/types/query-args.type.ts | 6 ++++-- packages/query-rest/src/types/update-one-input.type.ts | 2 -- 13 files changed, 17 insertions(+), 16 deletions(-) diff --git a/packages/query-rest/package.json b/packages/query-rest/package.json index 8bce4526e..053b7250d 100644 --- a/packages/query-rest/package.json +++ b/packages/query-rest/package.json @@ -5,7 +5,7 @@ "author": "doug-martin ", "homepage": "https://github.com/tripss/nestjs-query#readme", "keywords": [ - "reset", + "rest", "crud", "nestjs" ], diff --git a/packages/query-rest/src/connection/offset/pager/pager.ts b/packages/query-rest/src/connection/offset/pager/pager.ts index 63a088318..9a8ed328d 100644 --- a/packages/query-rest/src/connection/offset/pager/pager.ts +++ b/packages/query-rest/src/connection/offset/pager/pager.ts @@ -1,4 +1,4 @@ -import { Filter, Query } from '@ptc-org/nestjs-query-core' +import { Query } from '@ptc-org/nestjs-query-core' import { Count, Pager, QueryMany } from '../../interfaces' import { OffsetPagerResult, OffsetPagingMeta, OffsetPagingOpts, QueryResults } from './interfaces' diff --git a/packages/query-rest/src/decorators/field.decorator.ts b/packages/query-rest/src/decorators/field.decorator.ts index fff48eac8..afe3a679e 100644 --- a/packages/query-rest/src/decorators/field.decorator.ts +++ b/packages/query-rest/src/decorators/field.decorator.ts @@ -57,7 +57,7 @@ export function Field( const returnedType = returnTypeFunc?.() const isArray = returnedType && Array.isArray(returnedType) - const type = isArray ? returnedType[0] : returnedType + const type = (isArray ? returnedType[0] : returnedType) as never if ( advancedOptions !== undefined && diff --git a/packages/query-rest/src/decorators/hook-args.decorator.ts b/packages/query-rest/src/decorators/hook-args.decorator.ts index 1d0c4da5f..99635bf4a 100644 --- a/packages/query-rest/src/decorators/hook-args.decorator.ts +++ b/packages/query-rest/src/decorators/hook-args.decorator.ts @@ -12,7 +12,7 @@ class HooksTransformer implements PipeTransform { @Inject(REQUEST) protected readonly request: Request public async transform(value: T, metadata: ArgumentMetadata): Promise | Query> { - const transformedValue = this.transformValue(value, metadata.metatype) + const transformedValue = this.transformValue(value, metadata.metatype) if (metadata.type === 'query') { return this.runQueryHooks(transformedValue as BuildableQueryType) @@ -21,7 +21,7 @@ class HooksTransformer implements PipeTransform { return this.runMutationHooks(transformedValue) } - private transformValue(value: T, type?: Class): T { + private transformValue(value: T, type?: Class): T { if (!type || value instanceof type) { return value } diff --git a/packages/query-rest/src/hooks/hooks.ts b/packages/query-rest/src/hooks/hooks.ts index 7f9ca01c5..c1cddefa9 100644 --- a/packages/query-rest/src/hooks/hooks.ts +++ b/packages/query-rest/src/hooks/hooks.ts @@ -3,7 +3,8 @@ import { Class, Query } from '@ptc-org/nestjs-query-core' import { // CreateManyInputType, - CreateOneInputType, UpdateOneInputType + CreateOneInputType, + UpdateOneInputType // DeleteManyInputType, // DeleteOneInputType, // FindOneArgsType, diff --git a/packages/query-rest/src/interceptors/authorizer.interceptor.ts b/packages/query-rest/src/interceptors/authorizer.interceptor.ts index bf4ff9a4b..f2da249b3 100644 --- a/packages/query-rest/src/interceptors/authorizer.interceptor.ts +++ b/packages/query-rest/src/interceptors/authorizer.interceptor.ts @@ -12,7 +12,7 @@ export function AuthorizerInterceptor(DTOClass: Class): Class) {} public intercept(context: ExecutionContext, next: CallHandler) { - const request = context.switchToHttp().getRequest() + const request = context.switchToHttp().getRequest>() request.authorizer = this.authorizer return next.handle() diff --git a/packages/query-rest/src/interceptors/hook.interceptor.ts b/packages/query-rest/src/interceptors/hook.interceptor.ts index 72e4f89f0..4b9fbeed2 100644 --- a/packages/query-rest/src/interceptors/hook.interceptor.ts +++ b/packages/query-rest/src/interceptors/hook.interceptor.ts @@ -26,7 +26,7 @@ export function HookInterceptor(type: HookTypes, ...DTOClasses: Class[] constructor(@Inject(hookToken) readonly hooks: Hook[]) {} public intercept(context: ExecutionContext, next: CallHandler) { - const request = context.switchToHttp().getRequest() + const request = context.switchToHttp().getRequest>>() request.hooks = this.hooks return next.handle() diff --git a/packages/query-rest/src/interfaces/return-type-func.ts b/packages/query-rest/src/interfaces/return-type-func.ts index 12e7b5c85..eced9a628 100644 --- a/packages/query-rest/src/interfaces/return-type-func.ts +++ b/packages/query-rest/src/interfaces/return-type-func.ts @@ -1,6 +1,6 @@ import { Type } from '@nestjs/common' // eslint-disable-next-line @typescript-eslint/ban-types -export type ReturnTypeFuncValue = Type | Function | object | symbol; +export type ReturnTypeFuncValue = Type | Function | object | symbol // eslint-disable-next-line @typescript-eslint/no-explicit-any -export type ReturnTypeFunc = (returns?: void) => T; +export type ReturnTypeFunc = (returns?: void) => T diff --git a/packages/query-rest/src/module.ts b/packages/query-rest/src/module.ts index f8afea3cb..3b0104b94 100644 --- a/packages/query-rest/src/module.ts +++ b/packages/query-rest/src/module.ts @@ -17,6 +17,7 @@ export interface NestjsQueryRestModuleFeatureOpts { services?: Provider[] // eslint-disable-next-line @typescript-eslint/no-explicit-any assemblers?: Class>[] + // eslint-disable-next-line @typescript-eslint/no-explicit-any endpoints?: AutoResolverOpts, PagingStrategies>[] // eslint-disable-next-line @typescript-eslint/no-explicit-any // resolvers?: AutoResolverOpts, PagingStrategies>[] diff --git a/packages/query-rest/src/resolvers/update.resolver.ts b/packages/query-rest/src/resolvers/update.resolver.ts index 8e94b5c65..1226f6679 100644 --- a/packages/query-rest/src/resolvers/update.resolver.ts +++ b/packages/query-rest/src/resolvers/update.resolver.ts @@ -1,6 +1,6 @@ // eslint-disable-next-line max-classes-per-file import { Param } from '@nestjs/common' -import { PartialType } from '@nestjs/mapped-types' +import { PartialType } from '@nestjs/swagger' import { Class, DeepPartial, Filter, QueryService } from '@ptc-org/nestjs-query-core' import omit from 'lodash.omit' diff --git a/packages/query-rest/src/types/mutation-args.type.ts b/packages/query-rest/src/types/mutation-args.type.ts index 13487180e..90f8a7ce2 100644 --- a/packages/query-rest/src/types/mutation-args.type.ts +++ b/packages/query-rest/src/types/mutation-args.type.ts @@ -7,7 +7,6 @@ export interface MutationArgsType { // eslint-disable-next-line @typescript-eslint/no-redeclare -- intentional export function MutationArgsType(InputClass: Class): Class> { - class MutationArgs extends (InputClass as Type) implements MutationArgsType { public get input() { return this as never as Input diff --git a/packages/query-rest/src/types/query-args.type.ts b/packages/query-rest/src/types/query-args.type.ts index d6422e776..365d204bb 100644 --- a/packages/query-rest/src/types/query-args.type.ts +++ b/packages/query-rest/src/types/query-args.type.ts @@ -3,7 +3,8 @@ import { Class } from '@ptc-org/nestjs-query-core' import { removeUndefinedValues } from '../common' import { getQueryOptions } from '../decorators' import { - DEFAULT_QUERY_OPTS, NonePagingQueryArgsTypeOpts, + DEFAULT_QUERY_OPTS, + NonePagingQueryArgsTypeOpts, OffsetQueryArgsTypeOpts, PagingStrategies, QueryArgsTypeOpts, @@ -43,6 +44,7 @@ export function QueryArgsType(DTOClass: Class, opts?: QueryArgsTypeOpt return createOffsetQueryArgs(DTOClass, mergedOpts) } - return createOffsetQueryArgs(DTOClass, mergedOpts as any) + // TODO:: Support none paging type + return createOffsetQueryArgs(DTOClass, mergedOpts as never) // return createNonePagingQueryArgsType(DTOClass, mergedOpts) } diff --git a/packages/query-rest/src/types/update-one-input.type.ts b/packages/query-rest/src/types/update-one-input.type.ts index d1b7a8cae..c4938e6f8 100644 --- a/packages/query-rest/src/types/update-one-input.type.ts +++ b/packages/query-rest/src/types/update-one-input.type.ts @@ -15,11 +15,9 @@ export function UpdateOneInputType(DTOClass: Class, UpdateClass: Cl // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore class UpdateOneInput extends UpdateClass implements UpdateOneInputType { - public get update() { return this as never as U } - } Object.defineProperty(UpdateOneInput, 'name', { From 488a39cf739b64f6754b0fdcfd6d1abd2842db01 Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Fri, 1 Dec 2023 09:27:00 +0100 Subject: [PATCH 03/32] refactor(rest): Improved `@Field` decorator --- .../src/decorators/field.decorator.ts | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/packages/query-rest/src/decorators/field.decorator.ts b/packages/query-rest/src/decorators/field.decorator.ts index afe3a679e..e84215451 100644 --- a/packages/query-rest/src/decorators/field.decorator.ts +++ b/packages/query-rest/src/decorators/field.decorator.ts @@ -1,11 +1,14 @@ import { applyDecorators } from '@nestjs/common' import { ApiProperty, ApiPropertyOptions } from '@nestjs/swagger' import { Expose, Type } from 'class-transformer' -import { IsNotEmpty, IsOptional } from 'class-validator' +import { IsEnum, IsNotEmpty, IsOptional, ValidateNested } from 'class-validator' import { ReturnTypeFunc } from '../interfaces/return-type-func' -export type FieldOptions = Omit +export type FieldOptions = ApiPropertyOptions & { + // prevents the IsEnum decorator from being added + skipIsEnum?: boolean +} /** * Decorator for Fields that should be filterable through a [[FilterType]] @@ -57,14 +60,11 @@ export function Field( const returnedType = returnTypeFunc?.() const isArray = returnedType && Array.isArray(returnedType) - const type = (isArray ? returnedType[0] : returnedType) as never + const type = isArray ? returnedType[0] : returnedType - if ( - advancedOptions !== undefined && - advancedOptions.required === undefined && - (advancedOptions.nullable || advancedOptions.default !== undefined) - ) { - advancedOptions.required = false + const options = { + required: !advancedOptions?.nullable && advancedOptions?.default === undefined, + ...advancedOptions } const decorators = [ @@ -72,20 +72,26 @@ export function Field( ApiProperty({ type, isArray, - ...advancedOptions + ...options }) ] - if (advancedOptions !== undefined && advancedOptions.required !== undefined) { - if (advancedOptions.required) { - decorators.push(IsNotEmpty()) - } else { - decorators.push(IsOptional()) - } + if (options.required) { + decorators.push(IsNotEmpty()) + } else { + decorators.push(IsOptional()) } if (type) { decorators.push(Type(() => type)) + + if (typeof type === 'function') { + decorators.push(ValidateNested()) + } + } + + if (options.enum && options.skipIsEnum) { + decorators.push(IsEnum(options.enum)) } return applyDecorators(...decorators) From 24a619655d70196419a77d27d114f083278351fd Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Mon, 4 Dec 2023 17:50:36 +0100 Subject: [PATCH 04/32] refactor(rest): Improved `@Field` decorator --- packages/query-rest/src/decorators/field.decorator.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/query-rest/src/decorators/field.decorator.ts b/packages/query-rest/src/decorators/field.decorator.ts index e84215451..cff61e3c0 100644 --- a/packages/query-rest/src/decorators/field.decorator.ts +++ b/packages/query-rest/src/decorators/field.decorator.ts @@ -64,6 +64,7 @@ export function Field( const options = { required: !advancedOptions?.nullable && advancedOptions?.default === undefined, + example: advancedOptions?.default, ...advancedOptions } From 8178f02df493ef3be238e4213b3b2627ca921605 Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Tue, 5 Dec 2023 10:34:40 +0100 Subject: [PATCH 05/32] refactor(rest): Improved `@Field` and `@FilterableField` decorators --- .../query-rest/src/decorators/field.decorator.ts | 2 +- .../src/decorators/filterable-field.decorator.ts | 12 +++--------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/query-rest/src/decorators/field.decorator.ts b/packages/query-rest/src/decorators/field.decorator.ts index cff61e3c0..b9298b9d1 100644 --- a/packages/query-rest/src/decorators/field.decorator.ts +++ b/packages/query-rest/src/decorators/field.decorator.ts @@ -84,7 +84,7 @@ export function Field( } if (type) { - decorators.push(Type(() => type)) + decorators.push(Type(() => type as never)) if (typeof type === 'function') { decorators.push(ValidateNested()) diff --git a/packages/query-rest/src/decorators/filterable-field.decorator.ts b/packages/query-rest/src/decorators/filterable-field.decorator.ts index a7448e2f1..c4eded8f8 100644 --- a/packages/query-rest/src/decorators/filterable-field.decorator.ts +++ b/packages/query-rest/src/decorators/filterable-field.decorator.ts @@ -1,10 +1,10 @@ import { applyDecorators } from '@nestjs/common' -import { ApiProperty, ApiPropertyOptions } from '@nestjs/swagger' +import { ApiPropertyOptions } from '@nestjs/swagger' import { ArrayReflector, Class, getPrototypeChain } from '@ptc-org/nestjs-query-core' -import { Expose } from 'class-transformer' import { ReturnTypeFunc, ReturnTypeFuncValue } from '../interfaces/return-type-func' import { FILTERABLE_FIELD_KEY } from './constants' +import { Field } from './field.decorator' const reflector = new ArrayReflector(FILTERABLE_FIELD_KEY) export type FilterableFieldOptions = { @@ -91,13 +91,7 @@ export function FilterableField( return undefined } - applyDecorators( - Expose(), - ApiProperty({ - type: returnTypeFunc, - ...advancedOptions - }) - )(target, propertyName, descriptor) + applyDecorators(Field(() => returnTypeFunc, advancedOptions))(target, propertyName, descriptor) } } From 4529d186e282e683d19cf346b23e76d1186b0bc1 Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Sat, 9 Dec 2023 14:59:32 +0100 Subject: [PATCH 06/32] ci: Added dependabot.yml and stale.yml --- .github/dependabot.yml | 11 +++++++++++ .github/stale.yml | 15 +++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/stale.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..06a85ca16 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 000000000..b49e28696 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,15 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 7 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 3 +# Only issues or pull requests with all of these labels are checked if stale. Defaults to `[]` (disabled) +onlyLabels: + - "more info needed" +# Label to use when marking an issue as stale +staleLabel: "auto closed" +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false \ No newline at end of file From f16f6c63f142940de17093372278945b754f26b2 Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Sat, 9 Dec 2023 15:04:33 +0100 Subject: [PATCH 07/32] refactor(rest): Improved `@Field` decorator --- .../src/decorators/field.decorator.ts | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/packages/query-rest/src/decorators/field.decorator.ts b/packages/query-rest/src/decorators/field.decorator.ts index b9298b9d1..1c5b35273 100644 --- a/packages/query-rest/src/decorators/field.decorator.ts +++ b/packages/query-rest/src/decorators/field.decorator.ts @@ -1,12 +1,14 @@ -import { applyDecorators } from '@nestjs/common' +import { applyDecorators, Type as NestjsType } from '@nestjs/common' import { ApiProperty, ApiPropertyOptions } from '@nestjs/swagger' import { Expose, Type } from 'class-transformer' -import { IsEnum, IsNotEmpty, IsOptional, ValidateNested } from 'class-validator' +import { ArrayMaxSize, IsEnum, IsNotEmpty, IsObject, IsOptional, MaxLength, MinLength, ValidateNested } from 'class-validator' -import { ReturnTypeFunc } from '../interfaces/return-type-func' +// eslint-disable-next-line @typescript-eslint/ban-types +export type ReturnTypeFuncValue = NestjsType | Function | object | symbol +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ReturnTypeFunc = (returns?: void) => T -export type FieldOptions = ApiPropertyOptions & { - // prevents the IsEnum decorator from being added +export type FieldOptions = Omit & { skipIsEnum?: boolean } @@ -77,6 +79,18 @@ export function Field( }) ] + if (isArray && options.maxItems !== undefined) { + decorators.push(ArrayMaxSize(options.maxItems)) + } + + if (options.minLength) { + decorators.push(MinLength(options.minLength)) + } + + if (options.maxLength) { + decorators.push(MaxLength(options.maxLength)) + } + if (options.required) { decorators.push(IsNotEmpty()) } else { @@ -84,14 +98,18 @@ export function Field( } if (type) { - decorators.push(Type(() => type as never)) + decorators.push(Type(() => type)) if (typeof type === 'function') { decorators.push(ValidateNested()) + + if (!isArray) { + decorators.push(IsObject()) + } } } - if (options.enum && options.skipIsEnum) { + if (options.enum && !options.skipIsEnum) { decorators.push(IsEnum(options.enum)) } From 612a43a2a5d78b79fac2376e081da532fee26fcb Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Sat, 23 Dec 2023 16:14:23 +0100 Subject: [PATCH 08/32] refactor(rest): Updated lint --- packages/query-rest/src/decorators/field.decorator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/query-rest/src/decorators/field.decorator.ts b/packages/query-rest/src/decorators/field.decorator.ts index 1c5b35273..6244105b8 100644 --- a/packages/query-rest/src/decorators/field.decorator.ts +++ b/packages/query-rest/src/decorators/field.decorator.ts @@ -98,7 +98,7 @@ export function Field( } if (type) { - decorators.push(Type(() => type)) + decorators.push(Type(() => type as never)) if (typeof type === 'function') { decorators.push(ValidateNested()) From 0d0b9f5784d5037b8216df57120ef9276e9a47db Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Sat, 23 Dec 2023 16:57:05 +0100 Subject: [PATCH 09/32] test(rest): Added basic test case for one endpoint --- examples/basic-rest/e2e/fixtures.ts | 47 ++ examples/basic-rest/e2e/tag.endpoint.spec.ts | 59 ++ examples/basic-rest/open-api.json | 769 ++++++++++++++++++ examples/basic-rest/src/app.module.ts | 12 + .../src/sub-task/dto/sub-task.dto.ts | 24 + .../src/sub-task/dto/subtask-input.dto.ts | 23 + .../src/sub-task/dto/subtask-update.dto.ts | 26 + .../src/sub-task/sub-task.entity.ts | 43 + .../src/sub-task/sub-task.module.ts | 25 + .../basic-rest/src/tag/dto/tag-input.dto.ts | 9 + examples/basic-rest/src/tag/dto/tag.dto.ts | 15 + examples/basic-rest/src/tag/tag.entity.ts | 21 + examples/basic-rest/src/tag/tag.module.ts | 24 + .../src/todo-item/dto/todo-item-input.dto.ts | 13 + .../src/todo-item/dto/todo-item-update.dto.ts | 15 + .../src/todo-item/dto/todo-item.dto.ts | 23 + .../src/todo-item/todo-item.entity.ts | 41 + .../src/todo-item/todo-item.module.ts | 25 + examples/helpers/generate-openapi-spec.ts | 19 + .../src/decorators/field.decorator.ts | 10 +- .../src/providers/resolver.provider.ts | 12 +- .../query-rest/src/resolvers/crud.resolver.ts | 3 +- .../query-rest/src/resolvers/read.resolver.ts | 3 +- .../src/resolvers/resolver.interface.ts | 2 +- 24 files changed, 1248 insertions(+), 15 deletions(-) create mode 100644 examples/basic-rest/e2e/fixtures.ts create mode 100644 examples/basic-rest/e2e/tag.endpoint.spec.ts create mode 100644 examples/basic-rest/open-api.json create mode 100644 examples/basic-rest/src/app.module.ts create mode 100644 examples/basic-rest/src/sub-task/dto/sub-task.dto.ts create mode 100644 examples/basic-rest/src/sub-task/dto/subtask-input.dto.ts create mode 100644 examples/basic-rest/src/sub-task/dto/subtask-update.dto.ts create mode 100644 examples/basic-rest/src/sub-task/sub-task.entity.ts create mode 100644 examples/basic-rest/src/sub-task/sub-task.module.ts create mode 100644 examples/basic-rest/src/tag/dto/tag-input.dto.ts create mode 100644 examples/basic-rest/src/tag/dto/tag.dto.ts create mode 100644 examples/basic-rest/src/tag/tag.entity.ts create mode 100644 examples/basic-rest/src/tag/tag.module.ts create mode 100644 examples/basic-rest/src/todo-item/dto/todo-item-input.dto.ts create mode 100644 examples/basic-rest/src/todo-item/dto/todo-item-update.dto.ts create mode 100644 examples/basic-rest/src/todo-item/dto/todo-item.dto.ts create mode 100644 examples/basic-rest/src/todo-item/todo-item.entity.ts create mode 100644 examples/basic-rest/src/todo-item/todo-item.module.ts create mode 100644 examples/helpers/generate-openapi-spec.ts diff --git a/examples/basic-rest/e2e/fixtures.ts b/examples/basic-rest/e2e/fixtures.ts new file mode 100644 index 000000000..8c3fefe88 --- /dev/null +++ b/examples/basic-rest/e2e/fixtures.ts @@ -0,0 +1,47 @@ +import { Connection } from 'typeorm' + +import { executeTruncate } from '../../helpers' +import { SubTaskEntity } from '../src/sub-task/sub-task.entity' +import { TagEntity } from '../src/tag/tag.entity' +import { TodoItemEntity } from '../src/todo-item/todo-item.entity' + +const tables = ['todo_item', 'sub_task', 'tag'] +export const truncate = async (connection: Connection): Promise => executeTruncate(connection, tables) + +export const refresh = async (connection: Connection): Promise => { + await truncate(connection) + + const todoRepo = connection.getRepository(TodoItemEntity) + const subTaskRepo = connection.getRepository(SubTaskEntity) + const tagsRepo = connection.getRepository(TagEntity) + + const urgentTag = await tagsRepo.save({ name: 'Urgent' }) + const homeTag = await tagsRepo.save({ name: 'Home' }) + const workTag = await tagsRepo.save({ name: 'Work' }) + const questionTag = await tagsRepo.save({ name: 'Question' }) + const blockedTag = await tagsRepo.save({ name: 'Blocked' }) + + const todoItems = await todoRepo.save([ + { title: 'Create Nest App', completed: true, tags: [urgentTag, homeTag] }, + { title: 'Create Entity', completed: false, tags: [urgentTag, workTag] }, + { title: 'Create Entity Service', completed: false, tags: [blockedTag, workTag] }, + { title: 'Add Todo Item Resolver', completed: false, tags: [blockedTag, homeTag] }, + { + title: 'How to create item With Sub Tasks', + completed: false, + tags: [questionTag, blockedTag] + } + ]) + + await subTaskRepo.save( + todoItems.reduce( + (subTasks, todo) => [ + ...subTasks, + { completed: true, title: `${todo.title} - Sub Task 1`, todoItem: todo }, + { completed: false, title: `${todo.title} - Sub Task 2`, todoItem: todo }, + { completed: false, title: `${todo.title} - Sub Task 3`, todoItem: todo } + ], + [] as Partial[] + ) + ) +} diff --git a/examples/basic-rest/e2e/tag.endpoint.spec.ts b/examples/basic-rest/e2e/tag.endpoint.spec.ts new file mode 100644 index 000000000..ede0fede9 --- /dev/null +++ b/examples/basic-rest/e2e/tag.endpoint.spec.ts @@ -0,0 +1,59 @@ +import { INestApplication, ValidationPipe } from '@nestjs/common' +import { Test } from '@nestjs/testing' +import { OffsetConnectionType } from '@ptc-org/nestjs-query-rest' +import request from 'supertest' +import { Connection } from 'typeorm' + +import { generateOpenapiSpec } from '../../helpers/generate-openapi-spec' +import { AppModule } from '../src/app.module' +import { TagDTO } from '../src/tag/dto/tag.dto' +import { refresh } from './fixtures' + +describe('TagResolver (basic rest - e2e)', () => { + let app: INestApplication + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [AppModule] + }).compile() + + app = moduleRef.createNestApplication() + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + forbidUnknownValues: false + }) + ) + + generateOpenapiSpec(app, __dirname) + + await app.init() + await refresh(app.get(Connection)) + }) + + afterAll(() => refresh(app.get(Connection))) + + const tags = [ + { id: 1, name: 'Urgent' }, + { id: 2, name: 'Home' }, + { id: 3, name: 'Work' }, + { id: 4, name: 'Question' }, + { id: 5, name: 'Blocked' } + ] + + describe('query', () => { + it(`should return a connection`, () => + request(app.getHttpServer()) + .get('/tag-dtos') + .expect(200) + .then(({ body }) => { + const { nodes, pageInfo }: OffsetConnectionType = body + expect(nodes).toHaveLength(5) + expect(nodes.map((e) => e)).toMatchObject(tags) + })) + }) + + afterAll(async () => { + await app.close() + }) +}) diff --git a/examples/basic-rest/open-api.json b/examples/basic-rest/open-api.json new file mode 100644 index 000000000..a90b1accf --- /dev/null +++ b/examples/basic-rest/open-api.json @@ -0,0 +1,769 @@ +{ + "openapi": "3.0.0", + "paths": { + "/sub-task-dtos/{id}": { + "get": { + "operationId": "subTaskDTOS.findById", + "summary": "", + "tags": [], + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubTaskDTO" + } + } + } + } + } + }, + "delete": { + "operationId": "subTaskDTOS.deleteOne", + "summary": "", + "tags": [], + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubTaskDTO" + } + } + } + } + } + }, + "put": { + "operationId": "subTaskDTOS.updateOne", + "summary": "", + "tags": [], + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubTaskUpdateDTO" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubTaskDTO" + } + } + } + } + } + } + }, + "/sub-task-dtos": { + "get": { + "operationId": "subTaskDTOS.queryMany", + "summary": "", + "tags": [], + "parameters": [ + { + "name": "limit", + "required": false, + "in": "query", + "schema": { + "nullable": true, + "type": "number" + } + }, + { + "name": "offset", + "required": false, + "in": "query", + "schema": { + "nullable": true, + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubTaskDTOConnection" + } + } + } + } + } + }, + "post": { + "operationId": "subTaskDTOS.createOne", + "summary": "", + "tags": [], + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSubTaskDTO" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubTaskDTO" + } + } + } + } + } + } + }, + "/todo-item-dtos/{id}": { + "get": { + "operationId": "todoItemDTOS.findById", + "summary": "", + "tags": [], + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TodoItemDTO" + } + } + } + } + } + }, + "delete": { + "operationId": "todoItemDTOS.deleteOne", + "summary": "", + "tags": [], + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TodoItemDTO" + } + } + } + } + } + }, + "put": { + "operationId": "todoItemDTOS.updateOne", + "summary": "", + "tags": [], + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TodoItemUpdateDTO" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TodoItemDTO" + } + } + } + } + } + } + }, + "/todo-item-dtos": { + "get": { + "operationId": "todoItemDTOS.queryMany", + "summary": "", + "tags": [], + "parameters": [ + { + "name": "limit", + "required": false, + "in": "query", + "schema": { + "nullable": true, + "type": "number" + } + }, + { + "name": "offset", + "required": false, + "in": "query", + "schema": { + "nullable": true, + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TodoItemDTOConnection" + } + } + } + } + } + }, + "post": { + "operationId": "todoItemDTOS.createOne", + "summary": "", + "tags": [], + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTodoItemDTO" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TodoItemDTO" + } + } + } + } + } + } + }, + "/tag-dtos/{id}": { + "get": { + "operationId": "tagDTOS.findById", + "summary": "", + "tags": [], + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagDTO" + } + } + } + } + } + }, + "delete": { + "operationId": "tagDTOS.deleteOne", + "summary": "", + "tags": [], + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagDTO" + } + } + } + } + } + }, + "put": { + "operationId": "tagDTOS.updateOne", + "summary": "", + "tags": [], + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTagDTO" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagDTO" + } + } + } + } + } + } + }, + "/tag-dtos": { + "get": { + "operationId": "tagDTOS.queryMany", + "summary": "", + "tags": [], + "parameters": [ + { + "name": "limit", + "required": false, + "in": "query", + "schema": { + "nullable": true, + "type": "number" + } + }, + { + "name": "offset", + "required": false, + "in": "query", + "schema": { + "nullable": true, + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagDTOConnection" + } + } + } + } + } + }, + "post": { + "operationId": "tagDTOS.createOne", + "summary": "", + "tags": [], + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTagDTO" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagDTO" + } + } + } + } + } + } + } + }, + "info": { + "title": "", + "description": "", + "version": "1.0.0", + "contact": {} + }, + "tags": [], + "servers": [], + "components": { + "schemas": { + "SubTaskDTO": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "completed": { + "type": "boolean" + }, + "created": { + "format": "date-time", + "type": "string" + }, + "updated": { + "format": "date-time", + "type": "string" + }, + "todoItemId": { + "type": "string" + } + }, + "required": [ + "id", + "title", + "completed", + "created", + "updated", + "todoItemId" + ] + }, + "PageInfoType": { + "type": "object", + "properties": { + "hasNextPage": { + "type": "boolean", + "description": "true if paging forward and there are more records.", + "nullable": true + }, + "hasPreviousPage": { + "type": "boolean", + "description": "true if paging backwards and there are more records.", + "nullable": true + } + } + }, + "SubTaskDTOConnection": { + "type": "object", + "properties": { + "pageInfo": { + "description": "Paging information", + "allOf": [ + { + "$ref": "#/components/schemas/PageInfoType" + } + ] + }, + "totalCount": { + "type": "number", + "description": "Total amount of records." + }, + "nodes": { + "description": "Array of nodes.", + "type": "array", + "items": { + "$ref": "#/components/schemas/SubTaskDTO" + } + } + }, + "required": [ + "pageInfo", + "totalCount", + "nodes" + ] + }, + "SubTaskUpdateDTO": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "completed": { + "type": "boolean", + "nullable": true + }, + "todoItemId": { + "type": "string", + "nullable": true + } + }, + "required": [ + "title" + ] + }, + "CreateSubTaskDTO": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "completed": { + "type": "boolean" + }, + "todoItemId": { + "type": "string" + } + }, + "required": [ + "title", + "completed", + "todoItemId" + ] + }, + "TodoItemDTO": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "isCompleted": { + "type": "boolean" + } + }, + "required": [ + "id", + "title", + "isCompleted" + ] + }, + "TodoItemDTOConnection": { + "type": "object", + "properties": { + "pageInfo": { + "description": "Paging information", + "allOf": [ + { + "$ref": "#/components/schemas/PageInfoType" + } + ] + }, + "totalCount": { + "type": "number", + "description": "Total amount of records." + }, + "nodes": { + "description": "Array of nodes.", + "type": "array", + "items": { + "$ref": "#/components/schemas/TodoItemDTO" + } + } + }, + "required": [ + "pageInfo", + "totalCount", + "nodes" + ] + }, + "TodoItemUpdateDTO": { + "type": "object", + "properties": { + "title": { + "type": "string", + "nullable": true + }, + "completed": { + "type": "boolean", + "nullable": true + } + } + }, + "CreateTodoItemDTO": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "completed": { + "type": "boolean" + } + }, + "required": [ + "title", + "completed" + ] + }, + "TagDTO": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "name": { + "type": "string" + }, + "created": { + "format": "date-time", + "type": "string" + }, + "updated": { + "format": "date-time", + "type": "string" + } + }, + "required": [ + "id", + "name", + "created", + "updated" + ] + }, + "TagDTOConnection": { + "type": "object", + "properties": { + "pageInfo": { + "description": "Paging information", + "allOf": [ + { + "$ref": "#/components/schemas/PageInfoType" + } + ] + }, + "totalCount": { + "type": "number", + "description": "Total amount of records." + }, + "nodes": { + "description": "Array of nodes.", + "type": "array", + "items": { + "$ref": "#/components/schemas/TagDTO" + } + } + }, + "required": [ + "pageInfo", + "totalCount", + "nodes" + ] + }, + "CreateTagDTO": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} \ No newline at end of file diff --git a/examples/basic-rest/src/app.module.ts b/examples/basic-rest/src/app.module.ts new file mode 100644 index 000000000..e2d6d7b77 --- /dev/null +++ b/examples/basic-rest/src/app.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common' +import { TypeOrmModule } from '@nestjs/typeorm' + +import { typeormOrmConfig } from '../../helpers' +import { SubTaskModule } from './sub-task/sub-task.module' +import { TagModule } from './tag/tag.module' +import { TodoItemModule } from './todo-item/todo-item.module' + +@Module({ + imports: [TypeOrmModule.forRoot(typeormOrmConfig('basic')), SubTaskModule, TodoItemModule, TagModule] +}) +export class AppModule {} diff --git a/examples/basic-rest/src/sub-task/dto/sub-task.dto.ts b/examples/basic-rest/src/sub-task/dto/sub-task.dto.ts new file mode 100644 index 000000000..f3f7df5e5 --- /dev/null +++ b/examples/basic-rest/src/sub-task/dto/sub-task.dto.ts @@ -0,0 +1,24 @@ +import { FilterableField } from '@ptc-org/nestjs-query-rest' + +export class SubTaskDTO { + @FilterableField() + id!: number + + @FilterableField() + title!: string + + @FilterableField({ nullable: true }) + description?: string + + @FilterableField() + completed!: boolean + + @FilterableField() + created!: Date + + @FilterableField() + updated!: Date + + @FilterableField() + todoItemId!: string +} diff --git a/examples/basic-rest/src/sub-task/dto/subtask-input.dto.ts b/examples/basic-rest/src/sub-task/dto/subtask-input.dto.ts new file mode 100644 index 000000000..f8202f59d --- /dev/null +++ b/examples/basic-rest/src/sub-task/dto/subtask-input.dto.ts @@ -0,0 +1,23 @@ +import { Field } from '@ptc-org/nestjs-query-rest' +import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator' + +export class CreateSubTaskDTO { + @Field() + @IsString() + @IsNotEmpty() + title!: string + + @Field({ nullable: true }) + @IsOptional() + @IsString() + @IsNotEmpty() + description?: string + + @Field() + @IsBoolean() + completed!: boolean + + @Field() + @IsNotEmpty() + todoItemId!: string +} diff --git a/examples/basic-rest/src/sub-task/dto/subtask-update.dto.ts b/examples/basic-rest/src/sub-task/dto/subtask-update.dto.ts new file mode 100644 index 000000000..b23e146f6 --- /dev/null +++ b/examples/basic-rest/src/sub-task/dto/subtask-update.dto.ts @@ -0,0 +1,26 @@ +import { Field } from '@ptc-org/nestjs-query-rest' +import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator' + +export class SubTaskUpdateDTO { + @Field() + @IsOptional() + @IsNotEmpty() + @IsString() + title?: string + + @Field({ nullable: true }) + @IsOptional() + @IsNotEmpty() + @IsString() + description?: string + + @Field({ nullable: true }) + @IsOptional() + @IsBoolean() + completed?: boolean + + @Field({ nullable: true }) + @IsOptional() + @IsNotEmpty() + todoItemId?: string +} diff --git a/examples/basic-rest/src/sub-task/sub-task.entity.ts b/examples/basic-rest/src/sub-task/sub-task.entity.ts new file mode 100644 index 000000000..6d5cc62da --- /dev/null +++ b/examples/basic-rest/src/sub-task/sub-task.entity.ts @@ -0,0 +1,43 @@ +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + ObjectType, + PrimaryGeneratedColumn, + UpdateDateColumn +} from 'typeorm' + +import { TodoItemEntity } from '../todo-item/todo-item.entity' + +@Entity({ name: 'sub_task' }) +export class SubTaskEntity { + @PrimaryGeneratedColumn() + id!: number + + @Column() + title!: string + + @Column({ nullable: true }) + description?: string + + @Column() + completed!: boolean + + @Column({ nullable: false, name: 'todo_item_id' }) + todoItemId!: string + + @ManyToOne((): ObjectType => TodoItemEntity, (td) => td.subTasks, { + onDelete: 'CASCADE', + nullable: false + }) + @JoinColumn({ name: 'todo_item_id' }) + todoItem!: TodoItemEntity + + @CreateDateColumn() + created!: Date + + @UpdateDateColumn() + updated!: Date +} diff --git a/examples/basic-rest/src/sub-task/sub-task.module.ts b/examples/basic-rest/src/sub-task/sub-task.module.ts new file mode 100644 index 000000000..536f9a46a --- /dev/null +++ b/examples/basic-rest/src/sub-task/sub-task.module.ts @@ -0,0 +1,25 @@ +import { Module } from '@nestjs/common' +import { NestjsQueryRestModule } from '@ptc-org/nestjs-query-rest' +import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm' + +import { SubTaskDTO } from './dto/sub-task.dto' +import { CreateSubTaskDTO } from './dto/subtask-input.dto' +import { SubTaskUpdateDTO } from './dto/subtask-update.dto' +import { SubTaskEntity } from './sub-task.entity' + +@Module({ + imports: [ + NestjsQueryRestModule.forFeature({ + imports: [NestjsQueryTypeOrmModule.forFeature([SubTaskEntity])], + endpoints: [ + { + DTOClass: SubTaskDTO, + EntityClass: SubTaskEntity, + CreateDTOClass: CreateSubTaskDTO, + UpdateDTOClass: SubTaskUpdateDTO + } + ] + }) + ] +}) +export class SubTaskModule {} diff --git a/examples/basic-rest/src/tag/dto/tag-input.dto.ts b/examples/basic-rest/src/tag/dto/tag-input.dto.ts new file mode 100644 index 000000000..4006fa80a --- /dev/null +++ b/examples/basic-rest/src/tag/dto/tag-input.dto.ts @@ -0,0 +1,9 @@ +import { Field } from '@ptc-org/nestjs-query-rest' +import { IsNotEmpty, IsString } from 'class-validator' + +export class TagInputDTO { + @Field() + @IsString() + @IsNotEmpty() + name!: string +} diff --git a/examples/basic-rest/src/tag/dto/tag.dto.ts b/examples/basic-rest/src/tag/dto/tag.dto.ts new file mode 100644 index 000000000..25560c870 --- /dev/null +++ b/examples/basic-rest/src/tag/dto/tag.dto.ts @@ -0,0 +1,15 @@ +import { FilterableField } from '@ptc-org/nestjs-query-rest' + +export class TagDTO { + @FilterableField() + id!: number + + @FilterableField() + name!: string + + @FilterableField() + created!: Date + + @FilterableField() + updated!: Date +} diff --git a/examples/basic-rest/src/tag/tag.entity.ts b/examples/basic-rest/src/tag/tag.entity.ts new file mode 100644 index 000000000..3938b8cb5 --- /dev/null +++ b/examples/basic-rest/src/tag/tag.entity.ts @@ -0,0 +1,21 @@ +import { Column, CreateDateColumn, Entity, ManyToMany, ObjectType, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm' + +import { TodoItemEntity } from '../todo-item/todo-item.entity' + +@Entity({ name: 'tag' }) +export class TagEntity { + @PrimaryGeneratedColumn() + id!: number + + @Column() + name!: string + + @CreateDateColumn() + created!: Date + + @UpdateDateColumn() + updated!: Date + + @ManyToMany((): ObjectType => TodoItemEntity, (td) => td.tags) + todoItems!: TodoItemEntity[] +} diff --git a/examples/basic-rest/src/tag/tag.module.ts b/examples/basic-rest/src/tag/tag.module.ts new file mode 100644 index 000000000..5faf351e3 --- /dev/null +++ b/examples/basic-rest/src/tag/tag.module.ts @@ -0,0 +1,24 @@ +import { Module } from '@nestjs/common' +import { NestjsQueryRestModule } from '@ptc-org/nestjs-query-rest' +import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm' + +import { TagDTO } from './dto/tag.dto' +import { TagInputDTO } from './dto/tag-input.dto' +import { TagEntity } from './tag.entity' + +@Module({ + imports: [ + NestjsQueryRestModule.forFeature({ + imports: [NestjsQueryTypeOrmModule.forFeature([TagEntity])], + endpoints: [ + { + DTOClass: TagDTO, + EntityClass: TagEntity, + CreateDTOClass: TagInputDTO, + UpdateDTOClass: TagInputDTO + } + ] + }) + ] +}) +export class TagModule {} diff --git a/examples/basic-rest/src/todo-item/dto/todo-item-input.dto.ts b/examples/basic-rest/src/todo-item/dto/todo-item-input.dto.ts new file mode 100644 index 000000000..f30f42b5f --- /dev/null +++ b/examples/basic-rest/src/todo-item/dto/todo-item-input.dto.ts @@ -0,0 +1,13 @@ +import { Field } from '@ptc-org/nestjs-query-rest' +import { IsBoolean, IsString, MaxLength } from 'class-validator' + +export class TodoItemInputDTO { + @IsString() + @MaxLength(20) + @Field() + title!: string + + @IsBoolean() + @Field() + completed!: boolean +} diff --git a/examples/basic-rest/src/todo-item/dto/todo-item-update.dto.ts b/examples/basic-rest/src/todo-item/dto/todo-item-update.dto.ts new file mode 100644 index 000000000..c31a6258a --- /dev/null +++ b/examples/basic-rest/src/todo-item/dto/todo-item-update.dto.ts @@ -0,0 +1,15 @@ +import { Field } from '@ptc-org/nestjs-query-rest' +import { IsBoolean, IsOptional, IsString, MaxLength } from 'class-validator' + +export class TodoItemUpdateDTO { + @IsOptional() + @IsString() + @MaxLength(20) + @Field({ nullable: true }) + title?: string + + @IsOptional() + @IsBoolean() + @Field({ nullable: true }) + completed?: boolean +} diff --git a/examples/basic-rest/src/todo-item/dto/todo-item.dto.ts b/examples/basic-rest/src/todo-item/dto/todo-item.dto.ts new file mode 100644 index 000000000..13a5175da --- /dev/null +++ b/examples/basic-rest/src/todo-item/dto/todo-item.dto.ts @@ -0,0 +1,23 @@ +import { FilterableField } from '@ptc-org/nestjs-query-rest' + +export class TodoItemDTO { + @FilterableField() + id!: number + + @FilterableField() + title!: string + + @FilterableField({ nullable: true }) + description?: string + + @FilterableField({ + name: 'isCompleted' + }) + completed!: boolean + + @FilterableField({ filterOnly: true }) + created!: Date + + @FilterableField({ filterOnly: true }) + updated!: Date +} diff --git a/examples/basic-rest/src/todo-item/todo-item.entity.ts b/examples/basic-rest/src/todo-item/todo-item.entity.ts new file mode 100644 index 000000000..c018f7643 --- /dev/null +++ b/examples/basic-rest/src/todo-item/todo-item.entity.ts @@ -0,0 +1,41 @@ +import { + Column, + CreateDateColumn, + Entity, + JoinTable, + ManyToMany, + OneToMany, + PrimaryGeneratedColumn, + UpdateDateColumn +} from 'typeorm' + +import { SubTaskEntity } from '../sub-task/sub-task.entity' +import { TagEntity } from '../tag/tag.entity' + +@Entity({ name: 'todo_item' }) +export class TodoItemEntity { + @PrimaryGeneratedColumn() + id!: number + + @Column() + title!: string + + @Column({ nullable: true }) + description?: string + + @Column() + completed!: boolean + + @OneToMany(() => SubTaskEntity, (subTask) => subTask.todoItem) + subTasks!: SubTaskEntity[] + + @CreateDateColumn() + created!: Date + + @UpdateDateColumn() + updated!: Date + + @ManyToMany(() => TagEntity, (tag) => tag.todoItems) + @JoinTable() + tags!: TagEntity[] +} diff --git a/examples/basic-rest/src/todo-item/todo-item.module.ts b/examples/basic-rest/src/todo-item/todo-item.module.ts new file mode 100644 index 000000000..3895ee4ff --- /dev/null +++ b/examples/basic-rest/src/todo-item/todo-item.module.ts @@ -0,0 +1,25 @@ +import { Module } from '@nestjs/common' +import { NestjsQueryRestModule } from '@ptc-org/nestjs-query-rest' +import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm' + +import { TodoItemDTO } from './dto/todo-item.dto' +import { TodoItemInputDTO } from './dto/todo-item-input.dto' +import { TodoItemUpdateDTO } from './dto/todo-item-update.dto' +import { TodoItemEntity } from './todo-item.entity' + +@Module({ + imports: [ + NestjsQueryRestModule.forFeature({ + imports: [NestjsQueryTypeOrmModule.forFeature([TodoItemEntity])], + endpoints: [ + { + DTOClass: TodoItemDTO, + EntityClass: TodoItemEntity, + CreateDTOClass: TodoItemInputDTO, + UpdateDTOClass: TodoItemUpdateDTO + } + ] + }) + ] +}) +export class TodoItemModule {} diff --git a/examples/helpers/generate-openapi-spec.ts b/examples/helpers/generate-openapi-spec.ts new file mode 100644 index 000000000..ce35ec16d --- /dev/null +++ b/examples/helpers/generate-openapi-spec.ts @@ -0,0 +1,19 @@ +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger' +import { existsSync, unlinkSync, writeFileSync } from 'node:fs' +import { resolve } from 'node:path' + +import type { INestApplication } from '@nestjs/common' + +export function generateOpenapiSpec(app: INestApplication, dirname: string) { + // Generate the document in development + const config = new DocumentBuilder().build() + + const document = SwaggerModule.createDocument(app, config) + + const openApiLocation = resolve(dirname, '../open-api.json') + if (existsSync(openApiLocation)) { + unlinkSync(openApiLocation) + } + + writeFileSync(openApiLocation, JSON.stringify(document, null, 2)) +} diff --git a/packages/query-rest/src/decorators/field.decorator.ts b/packages/query-rest/src/decorators/field.decorator.ts index 6244105b8..8ab54a28e 100644 --- a/packages/query-rest/src/decorators/field.decorator.ts +++ b/packages/query-rest/src/decorators/field.decorator.ts @@ -1,14 +1,12 @@ -import { applyDecorators, Type as NestjsType } from '@nestjs/common' +import { applyDecorators } from '@nestjs/common' import { ApiProperty, ApiPropertyOptions } from '@nestjs/swagger' import { Expose, Type } from 'class-transformer' import { ArrayMaxSize, IsEnum, IsNotEmpty, IsObject, IsOptional, MaxLength, MinLength, ValidateNested } from 'class-validator' -// eslint-disable-next-line @typescript-eslint/ban-types -export type ReturnTypeFuncValue = NestjsType | Function | object | symbol -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type ReturnTypeFunc = (returns?: void) => T +import { ReturnTypeFunc } from '../interfaces/return-type-func' -export type FieldOptions = Omit & { +export type FieldOptions = ApiPropertyOptions & { + // prevents the IsEnum decorator from being added skipIsEnum?: boolean } diff --git a/packages/query-rest/src/providers/resolver.provider.ts b/packages/query-rest/src/providers/resolver.provider.ts index 6b868f31c..9cceabff3 100644 --- a/packages/query-rest/src/providers/resolver.provider.ts +++ b/packages/query-rest/src/providers/resolver.provider.ts @@ -66,7 +66,7 @@ const getEndpointToken = (DTOClass: Class): string => `${DTOClass.name function createEntityAutoResolver, C, U, R, PS extends PagingStrategies>( resolverOpts: EntityCRUDAutoResolverOpts ): Type { - const { DTOClass, EntityClass } = resolverOpts + const { DTOClass, EntityClass, basePath } = resolverOpts const { endpointName } = getDTONames(DTOClass) class Service extends AssemblerQueryService { @@ -76,7 +76,7 @@ function createEntityAutoResolver, C, U, } } - @Controller(endpointName) + @Controller(basePath || endpointName) class AutoResolver extends CRUDResolver(DTOClass, resolverOpts) { constructor(@InjectQueryService(EntityClass) service: QueryService) { super(new Service(service)) @@ -91,10 +91,10 @@ function createEntityAutoResolver, C, U, function createAssemblerAutoResolver( resolverOpts: AssemblerCRUDAutoResolverOpts ): Type { - const { DTOClass, AssemblerClass } = resolverOpts + const { DTOClass, AssemblerClass, basePath } = resolverOpts const { endpointName } = getDTONames(DTOClass) - @Controller(endpointName) + @Controller(basePath || endpointName) class AutoResolver extends CRUDResolver(DTOClass, resolverOpts) { constructor( @InjectAssemblerQueryService(AssemblerClass as unknown as Class>) @@ -112,10 +112,10 @@ function createAssemblerAutoResolver( resolverOpts: ServiceCRUDAutoResolverOpts ): Type { - const { DTOClass, ServiceClass } = resolverOpts + const { DTOClass, ServiceClass, basePath } = resolverOpts const { endpointName } = getDTONames(DTOClass) - @Controller(endpointName) + @Controller(basePath || endpointName) class AutoResolver extends CRUDResolver(DTOClass, resolverOpts) { constructor(@Inject(ServiceClass) service: QueryService) { super(service) diff --git a/packages/query-rest/src/resolvers/crud.resolver.ts b/packages/query-rest/src/resolvers/crud.resolver.ts index 737dc6209..cbf3318a2 100644 --- a/packages/query-rest/src/resolvers/crud.resolver.ts +++ b/packages/query-rest/src/resolvers/crud.resolver.ts @@ -36,6 +36,7 @@ export interface CRUDResolverOpts< update?: UpdateResolverOpts delete?: DeleteResolverOpts + basePath?: string tags?: string[] } @@ -83,7 +84,7 @@ function extractUpdateResolverOpts( function extractDeleteResolverOpts( opts: CRUDResolverOpts, PagingStrategies> ): DeleteResolverOpts { - const { delete: deleteArgs } = opts + const { delete: deleteArgs = {} } = opts return mergeBaseResolverOpts>(deleteArgs, opts) } diff --git a/packages/query-rest/src/resolvers/read.resolver.ts b/packages/query-rest/src/resolvers/read.resolver.ts index fcb65d390..a022cc82b 100644 --- a/packages/query-rest/src/resolvers/read.resolver.ts +++ b/packages/query-rest/src/resolvers/read.resolver.ts @@ -8,7 +8,8 @@ import { ConnectionOptions, InferConnectionTypeFromStrategy } from '../connectio import { AuthorizerFilter, Get, QueryHookArgs } from '../decorators' import { HookTypes } from '../hooks' import { AuthorizerInterceptor, HookInterceptor } from '../interceptors' -import { OffsetQueryArgsTypeOpts, PagingStrategies, QueryArgsType, QueryArgsTypeOpts, QueryType, StaticQueryType } from '../types' +import { QueryArgsType } from '../types' +import { OffsetQueryArgsTypeOpts, PagingStrategies, QueryArgsTypeOpts, QueryType, StaticQueryType } from '../types/query' import { BaseServiceResolver, ExtractPagingStrategy, ResolverClass, ResolverOpts, ServiceResolver } from './resolver.interface' export type ReadResolverFromOpts< diff --git a/packages/query-rest/src/resolvers/resolver.interface.ts b/packages/query-rest/src/resolvers/resolver.interface.ts index 27c8cde10..629a6fef2 100644 --- a/packages/query-rest/src/resolvers/resolver.interface.ts +++ b/packages/query-rest/src/resolvers/resolver.interface.ts @@ -3,7 +3,7 @@ import { QueryService } from '@ptc-org/nestjs-query-core' import { DTONamesOpts } from '../common' import { QueryOptionsDecoratorOpts, QueryResolverMethodOpts } from '../decorators' -import { PagingStrategies, QueryArgsTypeOpts } from '../types' +import { PagingStrategies, QueryArgsTypeOpts } from '../types/query' export type NamedEndpoint = { /** Specify to override the name of the graphql query or mutation * */ From 221ca7b4ceee055fc498a7053d2bb56c1a0ceb25 Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Sat, 23 Dec 2023 17:04:23 +0100 Subject: [PATCH 10/32] refactor: Lint examples --- examples/basic-rest/e2e/tag.endpoint.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/basic-rest/e2e/tag.endpoint.spec.ts b/examples/basic-rest/e2e/tag.endpoint.spec.ts index ede0fede9..cd1b385d2 100644 --- a/examples/basic-rest/e2e/tag.endpoint.spec.ts +++ b/examples/basic-rest/e2e/tag.endpoint.spec.ts @@ -47,7 +47,7 @@ describe('TagResolver (basic rest - e2e)', () => { .get('/tag-dtos') .expect(200) .then(({ body }) => { - const { nodes, pageInfo }: OffsetConnectionType = body + const { nodes }: OffsetConnectionType = body expect(nodes).toHaveLength(5) expect(nodes.map((e) => e)).toMatchObject(tags) })) From f8daf99fdee3a47dbcd233c1ff6c6e1baf32938b Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Thu, 25 Jan 2024 10:03:38 +0100 Subject: [PATCH 11/32] refactor(rest): Improved `@Field` decorator --- .../src/decorators/field.decorator.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/query-rest/src/decorators/field.decorator.ts b/packages/query-rest/src/decorators/field.decorator.ts index 8ab54a28e..37663d9d9 100644 --- a/packages/query-rest/src/decorators/field.decorator.ts +++ b/packages/query-rest/src/decorators/field.decorator.ts @@ -1,7 +1,17 @@ import { applyDecorators } from '@nestjs/common' import { ApiProperty, ApiPropertyOptions } from '@nestjs/swagger' import { Expose, Type } from 'class-transformer' -import { ArrayMaxSize, IsEnum, IsNotEmpty, IsObject, IsOptional, MaxLength, MinLength, ValidateNested } from 'class-validator' +import { + ArrayMaxSize, + IsArray, + IsEnum, + IsNotEmpty, + IsObject, + IsOptional, + MaxLength, + MinLength, + ValidateNested +} from 'class-validator' import { ReturnTypeFunc } from '../interfaces/return-type-func' @@ -96,12 +106,14 @@ export function Field( } if (type) { - decorators.push(Type(() => type as never)) + decorators.push(Type(() => type)) if (typeof type === 'function') { decorators.push(ValidateNested()) - if (!isArray) { + if (isArray) { + decorators.push(IsArray()) + } else { decorators.push(IsObject()) } } From 49b16fdc58b4050a4b9a88ef9b582745e6e245c2 Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Thu, 1 Feb 2024 17:33:04 +0100 Subject: [PATCH 12/32] chore: Merge dev --- yarn.lock | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/yarn.lock b/yarn.lock index f9bf61483..3abf9d1e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4820,6 +4820,23 @@ __metadata: languageName: node linkType: hard +"@nestjs/mapped-types@npm:2.0.4": + version: 2.0.4 + resolution: "@nestjs/mapped-types@npm:2.0.4" + peerDependencies: + "@nestjs/common": ^8.0.0 || ^9.0.0 || ^10.0.0 + class-transformer: ^0.4.0 || ^0.5.0 + class-validator: ^0.13.0 || ^0.14.0 + reflect-metadata: ^0.1.12 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + checksum: 0e8e7fdd281e341c3606d350821dacbaed1575dad9109695bfb318cd74e3ad4466751feddd84dc1723b7043a2de6e68362ef2b0fb6f47479af86dca6491e4f27 + languageName: node + linkType: hard + "@nestjs/mongoose@npm:10.0.2": version: 10.0.2 resolution: "@nestjs/mongoose@npm:10.0.2" @@ -4905,6 +4922,33 @@ __metadata: languageName: node linkType: hard +"@nestjs/swagger@npm:^7.1.15": + version: 7.2.0 + resolution: "@nestjs/swagger@npm:7.2.0" + dependencies: + "@nestjs/mapped-types": 2.0.4 + js-yaml: 4.1.0 + lodash: 4.17.21 + path-to-regexp: 3.2.0 + swagger-ui-dist: 5.11.0 + peerDependencies: + "@fastify/static": ^6.0.0 + "@nestjs/common": ^9.0.0 || ^10.0.0 + "@nestjs/core": ^9.0.0 || ^10.0.0 + class-transformer: "*" + class-validator: "*" + reflect-metadata: ^0.1.12 + peerDependenciesMeta: + "@fastify/static": + optional: true + class-transformer: + optional: true + class-validator: + optional: true + checksum: 1bf6588fede748a76cc18f71ab4b1542733b2e7c17767cdb91695f5d194b077b8968ec353881cf8eab61ae340d451d34f9d6400907f0367f6fe52c2201ff0424 + languageName: node + linkType: hard + "@nestjs/testing@npm:^10.3.0": version: 10.3.0 resolution: "@nestjs/testing@npm:10.3.0" @@ -5534,6 +5578,26 @@ __metadata: languageName: unknown linkType: soft +"@ptc-org/nestjs-query-rest@workspace:packages/query-rest": + version: 0.0.0-use.local + resolution: "@ptc-org/nestjs-query-rest@workspace:packages/query-rest" + dependencies: + lodash.omit: ^4.5.0 + lower-case-first: ^2.0.2 + pluralize: ^8.0.0 + tslib: ^2.6.2 + upper-case-first: ^2.0.2 + peerDependencies: + "@nestjs/common": ^9.0.0 || ^10.0.0 + "@nestjs/core": ^9.0.0 || ^10.0.0 + "@nestjs/graphql": ^11.0.0 || ^12.0.0 + "@nestjs/swagger": ^7.0.0 + class-transformer: ^0.5 + class-validator: ^0.14.0 + ts-morph: ^19.0.0 + languageName: unknown + linkType: soft + "@ptc-org/nestjs-query-sequelize@workspace:packages/query-sequelize": version: 0.0.0-use.local resolution: "@ptc-org/nestjs-query-sequelize@workspace:packages/query-sequelize" @@ -17234,6 +17298,7 @@ __metadata: "@nestjs/platform-express": 10.3.0 "@nestjs/schematics": 10.1.0 "@nestjs/sequelize": 10.0.0 + "@nestjs/swagger": ^7.1.15 "@nestjs/testing": ^10.3.0 "@nestjs/typeorm": ^10.0.1 "@nx-plus/docusaurus": ^15.0.0-rc.0 @@ -21395,6 +21460,13 @@ __metadata: languageName: node linkType: hard +"swagger-ui-dist@npm:5.11.0": + version: 5.11.0 + resolution: "swagger-ui-dist@npm:5.11.0" + checksum: 9f53b58b2917201a0a7997acb18005371ef06689350eeeac71aac33ba316764713182ac64bd2fbc8394e1f0c8197a6c6f4256b9dfc18db8bc67dd340afedf2f0 + languageName: node + linkType: hard + "symbol-observable@npm:4.0.0, symbol-observable@npm:^4.0.0": version: 4.0.0 resolution: "symbol-observable@npm:4.0.0" From 5bd8ccbf463e4f892b681b19b466c857df95d371 Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Thu, 8 Feb 2024 14:34:16 +0100 Subject: [PATCH 13/32] refactor(rest): Improved controller methods swagger implementation --- .../controller-methods.decorator.ts | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/packages/query-rest/src/decorators/controller-methods.decorator.ts b/packages/query-rest/src/decorators/controller-methods.decorator.ts index eb2c825c8..381ce83e1 100644 --- a/packages/query-rest/src/decorators/controller-methods.decorator.ts +++ b/packages/query-rest/src/decorators/controller-methods.decorator.ts @@ -8,7 +8,8 @@ import { SerializeOptions, UseInterceptors } from '@nestjs/common' -import { ApiBody, ApiBodyOptions, ApiOperation, ApiOperationOptions, ApiResponse } from '@nestjs/swagger' +import { ApiBody, ApiBodyOptions, ApiOperation, ApiOperationOptions, ApiParam, ApiResponse } from '@nestjs/swagger' +import { isArray } from 'class-validator' import { ReturnTypeFunc } from '../interfaces/return-type-func' import { isDisabled, ResolverMethod, ResolverMethodOpts } from './resolver-method.decorator' @@ -42,18 +43,45 @@ const methodDecorator = (method: (path?: string | string[]) => MethodDecorator) return (): void => {} } - const decorators = [method(options?.path), ResolverMethod(options, ...resolverOpts)] + if (!options.path) { + options.path = [] + } + + const paths: string[] = options.path && !isArray(options.path) ? ([options.path] as string[]) : (options.path as string[]) + + const decorators = [method(paths), ResolverMethod(options, ...resolverOpts)] + // Add all params to the swagger definition + .concat( + paths.reduce( + (params, path) => + params.concat( + path + .split('/') + .filter((partialPath) => partialPath.startsWith(':')) + .map((param) => param.replace(':', '')) + .filter((param) => param !== 'id') + .map((param) => + ApiParam({ + name: param, + type: 'string', + required: true + }) + ) + ), + [] as MethodDecorator[] + ) + ) if (returnTypeFunc) { const returnedType = returnTypeFunc() - const isArray = Array.isArray(returnedType) - const type = isArray ? returnedType[0] : returnedType + const returnTypeIsArray = Array.isArray(returnedType) + const type = returnTypeIsArray ? returnedType[0] : returnedType decorators.push( ApiResponse({ status: 200, type, - isArray + isArray: returnTypeIsArray }) ) From dc97f26808e8bc1e2b907e2c6cc179570db085a6 Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Fri, 14 Jun 2024 11:52:35 +0200 Subject: [PATCH 14/32] chore: Merge master --- yarn.lock | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 8745079c6..a92fc6da1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5303,7 +5303,7 @@ __metadata: languageName: node linkType: hard -"@microsoft/tsdoc@npm:0.14.2": +"@microsoft/tsdoc@npm:0.14.2, @microsoft/tsdoc@npm:^0.14.2": version: 0.14.2 resolution: "@microsoft/tsdoc@npm:0.14.2" checksum: b167c89e916ba73ee20b9c9d5dba6aa3a0de25ed3d50050e8a344dca7cd43cb2e1059bd515c820369b6e708901dd3fda476a42bc643ca74a35671ce77f724a3a @@ -5598,6 +5598,34 @@ __metadata: languageName: node linkType: hard +"@nestjs/swagger@npm:^7.1.15": + version: 7.3.1 + resolution: "@nestjs/swagger@npm:7.3.1" + dependencies: + "@microsoft/tsdoc": ^0.14.2 + "@nestjs/mapped-types": 2.0.5 + js-yaml: 4.1.0 + lodash: 4.17.21 + path-to-regexp: 3.2.0 + swagger-ui-dist: 5.11.2 + peerDependencies: + "@fastify/static": ^6.0.0 || ^7.0.0 + "@nestjs/common": ^9.0.0 || ^10.0.0 + "@nestjs/core": ^9.0.0 || ^10.0.0 + class-transformer: "*" + class-validator: "*" + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + "@fastify/static": + optional: true + class-transformer: + optional: true + class-validator: + optional: true + checksum: a051481dc48d303c8e3ab5b8a0443e3075f1a52c77bd6f5b4801847e693dc246206a260b2a520889bc3661a9f123774ecd62f0c623a2b00589516cc34ff7c880 + languageName: node + linkType: hard + "@nestjs/testing@npm:^10.3.8": version: 10.3.8 resolution: "@nestjs/testing@npm:10.3.8" @@ -6219,6 +6247,26 @@ __metadata: languageName: unknown linkType: soft +"@ptc-org/nestjs-query-rest@workspace:packages/query-rest": + version: 0.0.0-use.local + resolution: "@ptc-org/nestjs-query-rest@workspace:packages/query-rest" + dependencies: + lodash.omit: ^4.5.0 + lower-case-first: ^2.0.2 + pluralize: ^8.0.0 + tslib: ^2.6.2 + upper-case-first: ^2.0.2 + peerDependencies: + "@nestjs/common": ^9.0.0 || ^10.0.0 + "@nestjs/core": ^9.0.0 || ^10.0.0 + "@nestjs/graphql": ^11.0.0 || ^12.0.0 + "@nestjs/swagger": ^7.0.0 + class-transformer: ^0.5 + class-validator: ^0.14.0 + ts-morph: ^19.0.0 + languageName: unknown + linkType: soft + "@ptc-org/nestjs-query-sequelize@workspace:packages/query-sequelize": version: 0.0.0-use.local resolution: "@ptc-org/nestjs-query-sequelize@workspace:packages/query-sequelize" @@ -17805,6 +17853,7 @@ __metadata: "@nestjs/platform-express": 10.3.8 "@nestjs/schematics": 10.1.1 "@nestjs/sequelize": 10.0.1 + "@nestjs/swagger": ^7.1.15 "@nestjs/testing": ^10.3.8 "@nestjs/typeorm": ^10.0.2 "@nx-plus/docusaurus": ^15.0.0-rc.0 @@ -21946,6 +21995,13 @@ __metadata: languageName: node linkType: hard +"swagger-ui-dist@npm:5.11.2": + version: 5.11.2 + resolution: "swagger-ui-dist@npm:5.11.2" + checksum: 3f30ce7749481c1a3ba6c6db865f181f16927204a4aa1ae14438b4a7418afb13c49f1bcfcdc06a412bbce22d401563a698f711b3ce80b66c99735df2eb32d4b8 + languageName: node + linkType: hard + "symbol-observable@npm:4.0.0, symbol-observable@npm:^4.0.0": version: 4.0.0 resolution: "symbol-observable@npm:4.0.0" From 887910bc1f8b0cffea58d7780d99bbc0760906d8 Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Fri, 14 Jun 2024 11:53:20 +0200 Subject: [PATCH 15/32] fix(query-rest): Fixed paging not working when there are no additional hooks --- packages/query-rest/src/decorators/hook-args.decorator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/query-rest/src/decorators/hook-args.decorator.ts b/packages/query-rest/src/decorators/hook-args.decorator.ts index 99635bf4a..aa46de021 100644 --- a/packages/query-rest/src/decorators/hook-args.decorator.ts +++ b/packages/query-rest/src/decorators/hook-args.decorator.ts @@ -46,9 +46,9 @@ class HooksTransformer implements PipeTransform { private async runQueryHooks(data: BuildableQueryType): Promise> { const hooks = (this.request as HookContext>).hooks + let hookedArgs = data.buildQuery() if (hooks && hooks.length > 0) { - let hookedArgs = data.buildQuery() for (const hook of hooks) { hookedArgs = (await hook.run(hookedArgs, this.request)) as Query } @@ -56,7 +56,7 @@ class HooksTransformer implements PipeTransform { return hookedArgs } - return data as Query + return hookedArgs } } From 3eb2454dc7b76ae4982c26c5b35ed3116b6967b3 Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Fri, 14 Jun 2024 15:35:03 +0200 Subject: [PATCH 16/32] fix(query-rest): Added `IsArray` validation to `@Field` if field is array --- packages/query-rest/src/decorators/field.decorator.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/query-rest/src/decorators/field.decorator.ts b/packages/query-rest/src/decorators/field.decorator.ts index 37663d9d9..7b502e53c 100644 --- a/packages/query-rest/src/decorators/field.decorator.ts +++ b/packages/query-rest/src/decorators/field.decorator.ts @@ -106,14 +106,12 @@ export function Field( } if (type) { - decorators.push(Type(() => type)) + decorators.push(Type(() => type as never)) if (typeof type === 'function') { decorators.push(ValidateNested()) - if (isArray) { - decorators.push(IsArray()) - } else { + if (!isArray) { decorators.push(IsObject()) } } From 45932c96088354c270df54b1e7f8d676be36ea47 Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Fri, 14 Jun 2024 15:35:22 +0200 Subject: [PATCH 17/32] feat(query-rest): Added `forceArray` option to to `@Field` decorator --- .../src/decorators/field.decorator.ts | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/packages/query-rest/src/decorators/field.decorator.ts b/packages/query-rest/src/decorators/field.decorator.ts index 7b502e53c..dfa45f5ad 100644 --- a/packages/query-rest/src/decorators/field.decorator.ts +++ b/packages/query-rest/src/decorators/field.decorator.ts @@ -1,23 +1,14 @@ import { applyDecorators } from '@nestjs/common' import { ApiProperty, ApiPropertyOptions } from '@nestjs/swagger' -import { Expose, Type } from 'class-transformer' -import { - ArrayMaxSize, - IsArray, - IsEnum, - IsNotEmpty, - IsObject, - IsOptional, - MaxLength, - MinLength, - ValidateNested -} from 'class-validator' +import { Expose, Transform, Type } from 'class-transformer' +import { ArrayMaxSize, IsEnum, IsNotEmpty, IsObject, IsOptional, MaxLength, MinLength, ValidateNested } from 'class-validator' import { ReturnTypeFunc } from '../interfaces/return-type-func' export type FieldOptions = ApiPropertyOptions & { // prevents the IsEnum decorator from being added skipIsEnum?: boolean + forceArray?: boolean } /** @@ -91,6 +82,10 @@ export function Field( decorators.push(ArrayMaxSize(options.maxItems)) } + if (isArray && options.forceArray) { + decorators.push(Transform(({ value }) => (Array.isArray(value) ? value : [value]))) + } + if (options.minLength) { decorators.push(MinLength(options.minLength)) } From 85393ad1652af6ad82dc9fd747778c3efa697cee Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Fri, 14 Jun 2024 17:28:22 +0200 Subject: [PATCH 18/32] fix(query-rest): Prevent non-valid options from being passed to swagger in `@Field` decorator --- packages/query-rest/src/decorators/field.decorator.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/query-rest/src/decorators/field.decorator.ts b/packages/query-rest/src/decorators/field.decorator.ts index dfa45f5ad..fad8429d7 100644 --- a/packages/query-rest/src/decorators/field.decorator.ts +++ b/packages/query-rest/src/decorators/field.decorator.ts @@ -69,6 +69,10 @@ export function Field( ...advancedOptions } + // Remove non-valid options + delete options.forceArray + delete options.skipIsEnum + const decorators = [ Expose(), ApiProperty({ From 7245c1951a00625af919d423106941bceda6bef3 Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Fri, 21 Jun 2024 11:22:42 +0200 Subject: [PATCH 19/32] chore: Updated lockfile --- yarn.lock | 842 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 476 insertions(+), 366 deletions(-) diff --git a/yarn.lock b/yarn.lock index a92fc6da1..30940475e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3866,9 +3866,9 @@ __metadata: languageName: node linkType: hard -"@docusaurus/core@npm:3.3.2": - version: 3.3.2 - resolution: "@docusaurus/core@npm:3.3.2" +"@docusaurus/core@npm:3.4.0": + version: 3.4.0 + resolution: "@docusaurus/core@npm:3.4.0" dependencies: "@babel/core": ^7.23.3 "@babel/generator": ^7.23.3 @@ -3880,12 +3880,12 @@ __metadata: "@babel/runtime": ^7.22.6 "@babel/runtime-corejs3": ^7.22.6 "@babel/traverse": ^7.22.8 - "@docusaurus/cssnano-preset": 3.3.2 - "@docusaurus/logger": 3.3.2 - "@docusaurus/mdx-loader": 3.3.2 - "@docusaurus/utils": 3.3.2 - "@docusaurus/utils-common": 3.3.2 - "@docusaurus/utils-validation": 3.3.2 + "@docusaurus/cssnano-preset": 3.4.0 + "@docusaurus/logger": 3.4.0 + "@docusaurus/mdx-loader": 3.4.0 + "@docusaurus/utils": 3.4.0 + "@docusaurus/utils-common": 3.4.0 + "@docusaurus/utils-validation": 3.4.0 autoprefixer: ^10.4.14 babel-loader: ^9.1.3 babel-plugin-dynamic-import-node: ^2.3.3 @@ -3943,39 +3943,39 @@ __metadata: react-dom: ^18.0.0 bin: docusaurus: bin/docusaurus.mjs - checksum: 4b5100c0695f896f53a2a2103a3cd7d1685cf9708982dc13c391a2cae73d6f32dd76e9357b1771c18d3b08df4f90f3ee135b9260a5941e01e3211934dedfd93e + checksum: b7417648fedd1b0821332b86c7e64e68c70bfe02f393db032b50942856706a0b02d2d9e713d3cc979c4129a88e80319007f60263d48f1ede2499611ae20be8ec languageName: node linkType: hard -"@docusaurus/cssnano-preset@npm:3.3.2": - version: 3.3.2 - resolution: "@docusaurus/cssnano-preset@npm:3.3.2" +"@docusaurus/cssnano-preset@npm:3.4.0": + version: 3.4.0 + resolution: "@docusaurus/cssnano-preset@npm:3.4.0" dependencies: cssnano-preset-advanced: ^6.1.2 postcss: ^8.4.38 postcss-sort-media-queries: ^5.2.0 tslib: ^2.6.0 - checksum: cdb7b09a879e3f20faa2cd274bf37cb6b9d760a66268799f384be583e327b2269559e0fc2ee7d77ee5febe7293a4c4866edee8dd439efcef4d5545362e802838 + checksum: cc1892257cd49d752df615f6194b32ce810cdbf71b4fd32aa268cbd4b41071991d5573fca77417cc1f4cc1f85a9097d8cbc6d8eb413292a5d38b8d062e39489a languageName: node linkType: hard -"@docusaurus/logger@npm:3.3.2": - version: 3.3.2 - resolution: "@docusaurus/logger@npm:3.3.2" +"@docusaurus/logger@npm:3.4.0": + version: 3.4.0 + resolution: "@docusaurus/logger@npm:3.4.0" dependencies: chalk: ^4.1.2 tslib: ^2.6.0 - checksum: 8d45a67d55d6e829a3edcc49673864247e4ce0a6a0a342728d1b3afe48eeb4226202513dc1ef63d2b1229fd86f0bc0fdc11bba982e0fa444f2805395eab44e43 + checksum: 7da9bc96d47ab70674d6d17d1653fc3cef2366dd8a900078c9a1b43dfbc2edc500febbcbc4976afd21c26f3c6812af28baf2bba832f3ed4c1a106b4599e3febe languageName: node linkType: hard -"@docusaurus/mdx-loader@npm:3.3.2": - version: 3.3.2 - resolution: "@docusaurus/mdx-loader@npm:3.3.2" +"@docusaurus/mdx-loader@npm:3.4.0": + version: 3.4.0 + resolution: "@docusaurus/mdx-loader@npm:3.4.0" dependencies: - "@docusaurus/logger": 3.3.2 - "@docusaurus/utils": 3.3.2 - "@docusaurus/utils-validation": 3.3.2 + "@docusaurus/logger": 3.4.0 + "@docusaurus/utils": 3.4.0 + "@docusaurus/utils-validation": 3.4.0 "@mdx-js/mdx": ^3.0.0 "@slorber/remark-comment": ^1.0.0 escape-html: ^1.0.3 @@ -4000,15 +4000,15 @@ __metadata: peerDependencies: react: ^18.0.0 react-dom: ^18.0.0 - checksum: 98a800ec05cf9d5da85d3109a5cb62eb5876d25126209fe9502ef45e8a9742e0a7d6d63c8a9a35f09001555828210660232f5500b9ea248d813db146ab2d4571 + checksum: d85781ef53fe78b56dc1db305be8b7619392ebfaeaa95cfc3297d639b8e6a77415c81e80065a93c7bc24cc95211879eb1548457d3ebcc700523171a192e2959e languageName: node linkType: hard -"@docusaurus/module-type-aliases@npm:3.3.2": - version: 3.3.2 - resolution: "@docusaurus/module-type-aliases@npm:3.3.2" +"@docusaurus/module-type-aliases@npm:3.4.0": + version: 3.4.0 + resolution: "@docusaurus/module-type-aliases@npm:3.4.0" dependencies: - "@docusaurus/types": 3.3.2 + "@docusaurus/types": 3.4.0 "@types/history": ^4.7.11 "@types/react": "*" "@types/react-router-config": "*" @@ -4018,21 +4018,21 @@ __metadata: peerDependencies: react: "*" react-dom: "*" - checksum: 858f734379daac5622dfc04fa7f6b90bcffd33a5e7972d2f47c3b820564ea62f3e0b27bc1a55bc47ea3a23a52342bec844ce0db0bfcfcaf12e39490acd34e72a + checksum: d2054c07455cb19bbb48aa8d00c9409066172716bf0a00780b9044975c1250c65de6eb25368392085f2f52f9d882ee8c9285ae65bc2e6e7bbaefa5ebb7866edd languageName: node linkType: hard -"@docusaurus/plugin-content-blog@npm:3.3.2": - version: 3.3.2 - resolution: "@docusaurus/plugin-content-blog@npm:3.3.2" - dependencies: - "@docusaurus/core": 3.3.2 - "@docusaurus/logger": 3.3.2 - "@docusaurus/mdx-loader": 3.3.2 - "@docusaurus/types": 3.3.2 - "@docusaurus/utils": 3.3.2 - "@docusaurus/utils-common": 3.3.2 - "@docusaurus/utils-validation": 3.3.2 +"@docusaurus/plugin-content-blog@npm:3.4.0": + version: 3.4.0 + resolution: "@docusaurus/plugin-content-blog@npm:3.4.0" + dependencies: + "@docusaurus/core": 3.4.0 + "@docusaurus/logger": 3.4.0 + "@docusaurus/mdx-loader": 3.4.0 + "@docusaurus/types": 3.4.0 + "@docusaurus/utils": 3.4.0 + "@docusaurus/utils-common": 3.4.0 + "@docusaurus/utils-validation": 3.4.0 cheerio: ^1.0.0-rc.12 feed: ^4.2.2 fs-extra: ^11.1.1 @@ -4046,22 +4046,22 @@ __metadata: peerDependencies: react: ^18.0.0 react-dom: ^18.0.0 - checksum: e8a10b6b68dbc1396d60df3bff300f1e7220a46a24febb310b4432ed882cd3dd97c8c9bf9740d4a67935ae66d9826f87e2c5cbd037c8fa0a9e88819e5548f3b4 + checksum: d1ee5a8df0f078a2ac0b176ea29ed2714beaef51b79762797b5d70191ef931a91dceb12c3f6f2d51fb083b1fe6a0c41693dd65dce4e3a5bef6a0c76b3415c4d1 languageName: node linkType: hard -"@docusaurus/plugin-content-docs@npm:3.3.2": - version: 3.3.2 - resolution: "@docusaurus/plugin-content-docs@npm:3.3.2" - dependencies: - "@docusaurus/core": 3.3.2 - "@docusaurus/logger": 3.3.2 - "@docusaurus/mdx-loader": 3.3.2 - "@docusaurus/module-type-aliases": 3.3.2 - "@docusaurus/types": 3.3.2 - "@docusaurus/utils": 3.3.2 - "@docusaurus/utils-common": 3.3.2 - "@docusaurus/utils-validation": 3.3.2 +"@docusaurus/plugin-content-docs@npm:3.4.0": + version: 3.4.0 + resolution: "@docusaurus/plugin-content-docs@npm:3.4.0" + dependencies: + "@docusaurus/core": 3.4.0 + "@docusaurus/logger": 3.4.0 + "@docusaurus/mdx-loader": 3.4.0 + "@docusaurus/module-type-aliases": 3.4.0 + "@docusaurus/types": 3.4.0 + "@docusaurus/utils": 3.4.0 + "@docusaurus/utils-common": 3.4.0 + "@docusaurus/utils-validation": 3.4.0 "@types/react-router-config": ^5.0.7 combine-promises: ^1.1.0 fs-extra: ^11.1.1 @@ -4073,152 +4073,152 @@ __metadata: peerDependencies: react: ^18.0.0 react-dom: ^18.0.0 - checksum: 410b223268d50878e96dc072a6172eaf5d4fb53d7a6f23465029abc1e201acd0a4b8da4b05af2df2dafb0172642c35b2ee19a49107a8c63ab1dd10e1a11f15f7 + checksum: fcdffe6a8270f4ac5803fdbc46860b3454675d2c280c6c518c3934f055d6d78542e69968d8a8bc10515f53514331813a6f5c0c18ceccec2d7fdd185352072f81 languageName: node linkType: hard -"@docusaurus/plugin-content-pages@npm:3.3.2": - version: 3.3.2 - resolution: "@docusaurus/plugin-content-pages@npm:3.3.2" +"@docusaurus/plugin-content-pages@npm:3.4.0": + version: 3.4.0 + resolution: "@docusaurus/plugin-content-pages@npm:3.4.0" dependencies: - "@docusaurus/core": 3.3.2 - "@docusaurus/mdx-loader": 3.3.2 - "@docusaurus/types": 3.3.2 - "@docusaurus/utils": 3.3.2 - "@docusaurus/utils-validation": 3.3.2 + "@docusaurus/core": 3.4.0 + "@docusaurus/mdx-loader": 3.4.0 + "@docusaurus/types": 3.4.0 + "@docusaurus/utils": 3.4.0 + "@docusaurus/utils-validation": 3.4.0 fs-extra: ^11.1.1 tslib: ^2.6.0 webpack: ^5.88.1 peerDependencies: react: ^18.0.0 react-dom: ^18.0.0 - checksum: 185ba1cb6abe4feea03724015185cd1ef585db2dad9be182cb19f1205e32b19abcc8ea3d96bd1ab4c85190255c682e5c190628b2bf19e2bd66e2903e4cf7146b + checksum: bc175e5ddd0ea77d3a3efc4c2e530e1dc8024ae135f8ed4877f98bbaa5f19713bd80b9576530e39f856dfbf91578433873c89c851e64d175473fbf4a36eb36a1 languageName: node linkType: hard -"@docusaurus/plugin-debug@npm:3.3.2": - version: 3.3.2 - resolution: "@docusaurus/plugin-debug@npm:3.3.2" +"@docusaurus/plugin-debug@npm:3.4.0": + version: 3.4.0 + resolution: "@docusaurus/plugin-debug@npm:3.4.0" dependencies: - "@docusaurus/core": 3.3.2 - "@docusaurus/types": 3.3.2 - "@docusaurus/utils": 3.3.2 + "@docusaurus/core": 3.4.0 + "@docusaurus/types": 3.4.0 + "@docusaurus/utils": 3.4.0 fs-extra: ^11.1.1 react-json-view-lite: ^1.2.0 tslib: ^2.6.0 peerDependencies: react: ^18.0.0 react-dom: ^18.0.0 - checksum: 043a025c160fb1cfb0149a8b939d0b8c54e2e79754fba5430112f476dc5f21650de54df8bab7bbf55f4677b914b5e115df8c24d6e2aca08345d54fd3784d1336 + checksum: f07caeed0608a62c447b1a495e48fa8fcc8248d9c385887e2e814bb7610ff09b4ab2d7b10ba8ecc5024880a4e37d958fcd99aa4684406a946d92121ab94149c1 languageName: node linkType: hard -"@docusaurus/plugin-google-analytics@npm:3.3.2": - version: 3.3.2 - resolution: "@docusaurus/plugin-google-analytics@npm:3.3.2" +"@docusaurus/plugin-google-analytics@npm:3.4.0": + version: 3.4.0 + resolution: "@docusaurus/plugin-google-analytics@npm:3.4.0" dependencies: - "@docusaurus/core": 3.3.2 - "@docusaurus/types": 3.3.2 - "@docusaurus/utils-validation": 3.3.2 + "@docusaurus/core": 3.4.0 + "@docusaurus/types": 3.4.0 + "@docusaurus/utils-validation": 3.4.0 tslib: ^2.6.0 peerDependencies: react: ^18.0.0 react-dom: ^18.0.0 - checksum: a472f5c94114a298b73e39cda5ba09dde0cc00e85943de7dbb9294ddc650e33bc7ce9999df7946b9f56488cb1843393c389c95c5958ce3a36a366dc663046641 + checksum: ade51012397c12dbe7d0563ad7b2e345e3acbbd7729bf490b6d0f0cc2527b91abdd41b31392786c4697591d5b1f066f9ad257f483deaa2f2ea5194e33e3cd821 languageName: node linkType: hard -"@docusaurus/plugin-google-gtag@npm:3.3.2": - version: 3.3.2 - resolution: "@docusaurus/plugin-google-gtag@npm:3.3.2" +"@docusaurus/plugin-google-gtag@npm:3.4.0": + version: 3.4.0 + resolution: "@docusaurus/plugin-google-gtag@npm:3.4.0" dependencies: - "@docusaurus/core": 3.3.2 - "@docusaurus/types": 3.3.2 - "@docusaurus/utils-validation": 3.3.2 + "@docusaurus/core": 3.4.0 + "@docusaurus/types": 3.4.0 + "@docusaurus/utils-validation": 3.4.0 "@types/gtag.js": ^0.0.12 tslib: ^2.6.0 peerDependencies: react: ^18.0.0 react-dom: ^18.0.0 - checksum: 8597cc183ce7432af0aeedfb1bc042f41c6a2b87b6968dd8ce0767419b4088e02e96fd3c1710e74821404155a04627907182a11f31a5dc4f525a4a4ed8593196 + checksum: bab50eecf16d41b3a6896f0f222477495be63a195d012725042df6ea43a25281ca6929422b3b1ca901ae4127cf2000c05432afd01c69430fe973dc5a9ad35b9d languageName: node linkType: hard -"@docusaurus/plugin-google-tag-manager@npm:3.3.2": - version: 3.3.2 - resolution: "@docusaurus/plugin-google-tag-manager@npm:3.3.2" +"@docusaurus/plugin-google-tag-manager@npm:3.4.0": + version: 3.4.0 + resolution: "@docusaurus/plugin-google-tag-manager@npm:3.4.0" dependencies: - "@docusaurus/core": 3.3.2 - "@docusaurus/types": 3.3.2 - "@docusaurus/utils-validation": 3.3.2 + "@docusaurus/core": 3.4.0 + "@docusaurus/types": 3.4.0 + "@docusaurus/utils-validation": 3.4.0 tslib: ^2.6.0 peerDependencies: react: ^18.0.0 react-dom: ^18.0.0 - checksum: 9df287d72e0c56fea6fb1ffca0bf32423a91736ce95c79e52575295aafd0a5422463672b4ca9cbb185d220526756422ead477dc2bedb57230f73a853cba8f28d + checksum: 0b3e98856b81d66ba756fb2504bf54dbe24372fca0b4c298b6e83339be7c7c970c759bce3a4321b73c117d5eeef962f3395651100832bb3618f6cdb87f133b15 languageName: node linkType: hard -"@docusaurus/plugin-sitemap@npm:3.3.2": - version: 3.3.2 - resolution: "@docusaurus/plugin-sitemap@npm:3.3.2" - dependencies: - "@docusaurus/core": 3.3.2 - "@docusaurus/logger": 3.3.2 - "@docusaurus/types": 3.3.2 - "@docusaurus/utils": 3.3.2 - "@docusaurus/utils-common": 3.3.2 - "@docusaurus/utils-validation": 3.3.2 +"@docusaurus/plugin-sitemap@npm:3.4.0": + version: 3.4.0 + resolution: "@docusaurus/plugin-sitemap@npm:3.4.0" + dependencies: + "@docusaurus/core": 3.4.0 + "@docusaurus/logger": 3.4.0 + "@docusaurus/types": 3.4.0 + "@docusaurus/utils": 3.4.0 + "@docusaurus/utils-common": 3.4.0 + "@docusaurus/utils-validation": 3.4.0 fs-extra: ^11.1.1 sitemap: ^7.1.1 tslib: ^2.6.0 peerDependencies: react: ^18.0.0 react-dom: ^18.0.0 - checksum: 65f813901476c81e94003e87f839c2e85d9662808215f0148a2aad39da2b97f43ab213543e5d40221bfb71e642b953e39601447d92f294a437b390e20c556ab4 + checksum: fb2163fbedbdf7952e1ac35faad98e730519a03cc620d371b7e76e5376e8344f903f3612297a76f09593b9fb94256035f47c0bd7c8c5e908a2cdbfd9fc44516f languageName: node linkType: hard -"@docusaurus/preset-classic@npm:3.3.2": - version: 3.3.2 - resolution: "@docusaurus/preset-classic@npm:3.3.2" - dependencies: - "@docusaurus/core": 3.3.2 - "@docusaurus/plugin-content-blog": 3.3.2 - "@docusaurus/plugin-content-docs": 3.3.2 - "@docusaurus/plugin-content-pages": 3.3.2 - "@docusaurus/plugin-debug": 3.3.2 - "@docusaurus/plugin-google-analytics": 3.3.2 - "@docusaurus/plugin-google-gtag": 3.3.2 - "@docusaurus/plugin-google-tag-manager": 3.3.2 - "@docusaurus/plugin-sitemap": 3.3.2 - "@docusaurus/theme-classic": 3.3.2 - "@docusaurus/theme-common": 3.3.2 - "@docusaurus/theme-search-algolia": 3.3.2 - "@docusaurus/types": 3.3.2 +"@docusaurus/preset-classic@npm:3.4.0": + version: 3.4.0 + resolution: "@docusaurus/preset-classic@npm:3.4.0" + dependencies: + "@docusaurus/core": 3.4.0 + "@docusaurus/plugin-content-blog": 3.4.0 + "@docusaurus/plugin-content-docs": 3.4.0 + "@docusaurus/plugin-content-pages": 3.4.0 + "@docusaurus/plugin-debug": 3.4.0 + "@docusaurus/plugin-google-analytics": 3.4.0 + "@docusaurus/plugin-google-gtag": 3.4.0 + "@docusaurus/plugin-google-tag-manager": 3.4.0 + "@docusaurus/plugin-sitemap": 3.4.0 + "@docusaurus/theme-classic": 3.4.0 + "@docusaurus/theme-common": 3.4.0 + "@docusaurus/theme-search-algolia": 3.4.0 + "@docusaurus/types": 3.4.0 peerDependencies: react: ^18.0.0 react-dom: ^18.0.0 - checksum: fe98e457990a8d3d9966247ca02ad4f960aea0c9200e76ee1180942daea8165352dfa12b52bf3d8bd1ad89c16e849b36170f584aef838f16aaf3daab26ebbb3f + checksum: 968833af162303685bebce71baed4ae662ce4c67957ceba0c9d697459c5e7996455a8a7ecb1b5c1a5978b475d5aa71605f8ec6b5b90459940310ffb2f4f5b6f1 languageName: node linkType: hard -"@docusaurus/theme-classic@npm:3.3.2": - version: 3.3.2 - resolution: "@docusaurus/theme-classic@npm:3.3.2" - dependencies: - "@docusaurus/core": 3.3.2 - "@docusaurus/mdx-loader": 3.3.2 - "@docusaurus/module-type-aliases": 3.3.2 - "@docusaurus/plugin-content-blog": 3.3.2 - "@docusaurus/plugin-content-docs": 3.3.2 - "@docusaurus/plugin-content-pages": 3.3.2 - "@docusaurus/theme-common": 3.3.2 - "@docusaurus/theme-translations": 3.3.2 - "@docusaurus/types": 3.3.2 - "@docusaurus/utils": 3.3.2 - "@docusaurus/utils-common": 3.3.2 - "@docusaurus/utils-validation": 3.3.2 +"@docusaurus/theme-classic@npm:3.4.0": + version: 3.4.0 + resolution: "@docusaurus/theme-classic@npm:3.4.0" + dependencies: + "@docusaurus/core": 3.4.0 + "@docusaurus/mdx-loader": 3.4.0 + "@docusaurus/module-type-aliases": 3.4.0 + "@docusaurus/plugin-content-blog": 3.4.0 + "@docusaurus/plugin-content-docs": 3.4.0 + "@docusaurus/plugin-content-pages": 3.4.0 + "@docusaurus/theme-common": 3.4.0 + "@docusaurus/theme-translations": 3.4.0 + "@docusaurus/types": 3.4.0 + "@docusaurus/utils": 3.4.0 + "@docusaurus/utils-common": 3.4.0 + "@docusaurus/utils-validation": 3.4.0 "@mdx-js/react": ^3.0.0 clsx: ^2.0.0 copy-text-to-clipboard: ^3.2.0 @@ -4235,21 +4235,21 @@ __metadata: peerDependencies: react: ^18.0.0 react-dom: ^18.0.0 - checksum: af3e6ede0574fba04e44560a6126ab380ab21e32ee13482cf4f21230818d58f3e794ff5ac9e715091612ff5f52561c3fcc8c59e3a69c75b663b70e00ffdfadbe + checksum: 3c8aaa8c31d86683909d242e3c1fa01732a10b6f62f27eda0768048a0598ed69da70824553da4e437271ac4ddbe5098c11b2012dbb09c9f97c0f40fb21e41843 languageName: node linkType: hard -"@docusaurus/theme-common@npm:3.3.2": - version: 3.3.2 - resolution: "@docusaurus/theme-common@npm:3.3.2" - dependencies: - "@docusaurus/mdx-loader": 3.3.2 - "@docusaurus/module-type-aliases": 3.3.2 - "@docusaurus/plugin-content-blog": 3.3.2 - "@docusaurus/plugin-content-docs": 3.3.2 - "@docusaurus/plugin-content-pages": 3.3.2 - "@docusaurus/utils": 3.3.2 - "@docusaurus/utils-common": 3.3.2 +"@docusaurus/theme-common@npm:3.4.0": + version: 3.4.0 + resolution: "@docusaurus/theme-common@npm:3.4.0" + dependencies: + "@docusaurus/mdx-loader": 3.4.0 + "@docusaurus/module-type-aliases": 3.4.0 + "@docusaurus/plugin-content-blog": 3.4.0 + "@docusaurus/plugin-content-docs": 3.4.0 + "@docusaurus/plugin-content-pages": 3.4.0 + "@docusaurus/utils": 3.4.0 + "@docusaurus/utils-common": 3.4.0 "@types/history": ^4.7.11 "@types/react": "*" "@types/react-router-config": "*" @@ -4261,22 +4261,22 @@ __metadata: peerDependencies: react: ^18.0.0 react-dom: ^18.0.0 - checksum: b88ebf0cdeabdb766f98179d5ba4f54fddb24a3db6e498d01eeadf6d59c5f2d83588f30075551076f171a78ea9704f398d64e06d2af625f3232d7f261c90af10 + checksum: e8231b931775225dd313b3a46e1857cb038fb1567437fd0b6327a944246f380309bdefc7a512517797b9f13cdca282eb6a08a2c5ea2e59c002ad9f459942b943 languageName: node linkType: hard -"@docusaurus/theme-search-algolia@npm:3.3.2": - version: 3.3.2 - resolution: "@docusaurus/theme-search-algolia@npm:3.3.2" +"@docusaurus/theme-search-algolia@npm:3.4.0": + version: 3.4.0 + resolution: "@docusaurus/theme-search-algolia@npm:3.4.0" dependencies: "@docsearch/react": ^3.5.2 - "@docusaurus/core": 3.3.2 - "@docusaurus/logger": 3.3.2 - "@docusaurus/plugin-content-docs": 3.3.2 - "@docusaurus/theme-common": 3.3.2 - "@docusaurus/theme-translations": 3.3.2 - "@docusaurus/utils": 3.3.2 - "@docusaurus/utils-validation": 3.3.2 + "@docusaurus/core": 3.4.0 + "@docusaurus/logger": 3.4.0 + "@docusaurus/plugin-content-docs": 3.4.0 + "@docusaurus/theme-common": 3.4.0 + "@docusaurus/theme-translations": 3.4.0 + "@docusaurus/utils": 3.4.0 + "@docusaurus/utils-validation": 3.4.0 algoliasearch: ^4.18.0 algoliasearch-helper: ^3.13.3 clsx: ^2.0.0 @@ -4288,23 +4288,30 @@ __metadata: peerDependencies: react: ^18.0.0 react-dom: ^18.0.0 - checksum: 1a922620399d69199ef2e32bded18b11ff2b1e5f93d074b46489052cab8e1f70d2c975b033bf74712756b051351b57807caae7630ed19d28de333a9fd02304e8 + checksum: 173c8a2d600a681d736f5097529c3dcb46cdd8f717a6875cd7701b762010f209e1703537fcc475fc344323ec0213a2709180cfdb8c18a334c063bc74789d749b languageName: node linkType: hard -"@docusaurus/theme-translations@npm:3.3.2": - version: 3.3.2 - resolution: "@docusaurus/theme-translations@npm:3.3.2" +"@docusaurus/theme-translations@npm:3.4.0": + version: 3.4.0 + resolution: "@docusaurus/theme-translations@npm:3.4.0" dependencies: fs-extra: ^11.1.1 tslib: ^2.6.0 - checksum: a20ba46d36a7ac10d43ac0bc66ab3e137dd7d9529d26eee76513366f9fa7ae6fde61a85ce304c88bfa39136a90ffd53ee18358efd338a33e462999861bb01cdc + checksum: 599cdedf90da0f6fdd75088f358dd045a69a5b00904100a931bfea4f514c8282e1093b4f62c0af96be422b528a1addfc24043cba857db6357010ff92020795b6 languageName: node linkType: hard -"@docusaurus/types@npm:3.3.2": - version: 3.3.2 - resolution: "@docusaurus/types@npm:3.3.2" +"@docusaurus/tsconfig@npm:3.4.0": + version: 3.4.0 + resolution: "@docusaurus/tsconfig@npm:3.4.0" + checksum: 96d0cee56186256f04405c7b1cc45103a7d13862a8e4f1fc083c2b8e46d03a654ff6ec58ff154141ba2b30cdc3f708130f7ac88749fc1e0a88bf37ded6defb97 + languageName: node + linkType: hard + +"@docusaurus/types@npm:3.4.0": + version: 3.4.0 + resolution: "@docusaurus/types@npm:3.4.0" dependencies: "@mdx-js/mdx": ^3.0.0 "@types/history": ^4.7.11 @@ -4318,13 +4325,13 @@ __metadata: peerDependencies: react: ^18.0.0 react-dom: ^18.0.0 - checksum: 6da53038547d94cf5e2a8b14224972f83e1779e45453fcaf237e4e2b5f4c380534a04332cfa2029ceaae72e3d4a93a544d2b07c0bd280a365cb2b77516620628 + checksum: a3d50dd22db201711894ac73bfc9816ea4c01962063a91cdb7933597c621a794a85aa322d4c0bc8cacf9029902cdfc5a9c027b603fa9197bc4cd1917270b5da2 languageName: node linkType: hard -"@docusaurus/utils-common@npm:3.3.2": - version: 3.3.2 - resolution: "@docusaurus/utils-common@npm:3.3.2" +"@docusaurus/utils-common@npm:3.4.0": + version: 3.4.0 + resolution: "@docusaurus/utils-common@npm:3.4.0" dependencies: tslib: ^2.6.0 peerDependencies: @@ -4332,30 +4339,32 @@ __metadata: peerDependenciesMeta: "@docusaurus/types": optional: true - checksum: cb745c0b912babee39a78bbf592cc622b35de9044bc5b99a8b59fad1e278ef447b03aac39a18d6112a74d9053f04ab9ec4b3d2f2bc77014a8dd6d212e4e48b21 + checksum: a3d17e3e504e22972a3344215da9e97b5c5d9813a826146b5aad159953f92e1cd9cfecc9d1e2da22ee6df5be3f4b0cbd8f58071f979d0805b1b680d8e6bd571c languageName: node linkType: hard -"@docusaurus/utils-validation@npm:3.3.2": - version: 3.3.2 - resolution: "@docusaurus/utils-validation@npm:3.3.2" +"@docusaurus/utils-validation@npm:3.4.0": + version: 3.4.0 + resolution: "@docusaurus/utils-validation@npm:3.4.0" dependencies: - "@docusaurus/logger": 3.3.2 - "@docusaurus/utils": 3.3.2 - "@docusaurus/utils-common": 3.3.2 + "@docusaurus/logger": 3.4.0 + "@docusaurus/utils": 3.4.0 + "@docusaurus/utils-common": 3.4.0 + fs-extra: ^11.2.0 joi: ^17.9.2 js-yaml: ^4.1.0 + lodash: ^4.17.21 tslib: ^2.6.0 - checksum: 2635d233a34919bb8a2970eec63cbf79788104e7c821d63488c6c1ed466b450019ffa640ddd6d0560b99f17829275c9735de6fc0154ad2e2de5d841083a852c1 + checksum: 8130455f9448351102d94d366dc3781e21682d8b9760d8e86a01549c8e5db0e3035caca1ee2dc577362cd0942d15895409a170553a368654a08f4b53e4788aea languageName: node linkType: hard -"@docusaurus/utils@npm:3.3.2": - version: 3.3.2 - resolution: "@docusaurus/utils@npm:3.3.2" +"@docusaurus/utils@npm:3.4.0": + version: 3.4.0 + resolution: "@docusaurus/utils@npm:3.4.0" dependencies: - "@docusaurus/logger": 3.3.2 - "@docusaurus/utils-common": 3.3.2 + "@docusaurus/logger": 3.4.0 + "@docusaurus/utils-common": 3.4.0 "@svgr/webpack": ^8.1.0 escape-string-regexp: ^4.0.0 file-loader: ^6.2.0 @@ -4372,13 +4381,14 @@ __metadata: shelljs: ^0.8.5 tslib: ^2.6.0 url-loader: ^4.1.1 + utility-types: ^3.10.0 webpack: ^5.88.1 peerDependencies: "@docusaurus/types": "*" peerDependenciesMeta: "@docusaurus/types": optional: true - checksum: 5757e9cb7d70a5b9fbb2a6cde6b66e36b335b77a2349eaa88c3dca5819ac680d5491b2df992223f3ae7fb6ffeb957f6929a9145fd6f154c761c89536450d745c + checksum: 2be7c435797120456528bcf476b68a96d39a9faaaa955f045f37f111cee0d64357abad160a99b7c3aaf9ac2744722c02641610dfaf123c25fbc42b3b5af7d0fb languageName: node linkType: hard @@ -5396,9 +5406,9 @@ __metadata: languageName: node linkType: hard -"@nestjs/common@npm:10.3.8": - version: 10.3.8 - resolution: "@nestjs/common@npm:10.3.8" +"@nestjs/common@npm:10.3.9": + version: 10.3.9 + resolution: "@nestjs/common@npm:10.3.9" dependencies: iterare: 1.2.1 tslib: 2.6.2 @@ -5413,13 +5423,13 @@ __metadata: optional: true class-validator: optional: true - checksum: 1bae37914196a5287249c26b7d381b834a4f3924c86ddb46500e66876ff8a9c92cf7e0356138af5c3a5d8b866096c7d8b69ef68dbe8770c4780794a1cc16371b + checksum: c362f2717888626ea3360b280dd42d8d173e3d6640615b9f76c6b202b33534daac362a224ee0be8c16f1270b9891172c3684396a02b06b8309a84342fd1177bc languageName: node linkType: hard -"@nestjs/core@npm:10.3.8": - version: 10.3.8 - resolution: "@nestjs/core@npm:10.3.8" +"@nestjs/core@npm:10.3.9": + version: 10.3.9 + resolution: "@nestjs/core@npm:10.3.9" dependencies: "@nuxtjs/opencollective": 0.3.2 fast-safe-stringify: 2.1.1 @@ -5441,7 +5451,7 @@ __metadata: optional: true "@nestjs/websockets": optional: true - checksum: f63b53b3bb576bb1054c5f81d22829ff4e393dcfeb6eb3736e66b44590dd08392be6236906d5187e5af64dbf947c2244278b05b0bdf36139ed61b6c36e28cb9c + checksum: de6f111f5363c1682f1d28da2b33f57c3fa581958c4a2b056f28b3e930923260a21ac97063ae0193bbc1b3a81f7570738af5454147c08abbcc71ef478cbcbef4 languageName: node linkType: hard @@ -5536,9 +5546,9 @@ __metadata: languageName: node linkType: hard -"@nestjs/platform-express@npm:10.3.8": - version: 10.3.8 - resolution: "@nestjs/platform-express@npm:10.3.8" +"@nestjs/platform-express@npm:10.3.9": + version: 10.3.9 + resolution: "@nestjs/platform-express@npm:10.3.9" dependencies: body-parser: 1.20.2 cors: 2.8.5 @@ -5548,7 +5558,7 @@ __metadata: peerDependencies: "@nestjs/common": ^10.0.0 "@nestjs/core": ^10.0.0 - checksum: 678384fe60f4f1255ad70926cef05f349229739a6e3b5cdf13a6c26d2f915220c41ce21e08cffc3a2fa9dfb237b275a06fa3f405f49a1056fc3f3558938b724f + checksum: 843523067566b9ebb83ace847f594df37f1238afb9f59131b090a3491a08df207784c1a4e6cacea28bcc6b85a7d7c09c603fa9e0da34c402880580ed579093e5 languageName: node linkType: hard @@ -5626,9 +5636,9 @@ __metadata: languageName: node linkType: hard -"@nestjs/testing@npm:^10.3.8": - version: 10.3.8 - resolution: "@nestjs/testing@npm:10.3.8" +"@nestjs/testing@npm:^10.3.9": + version: 10.3.9 + resolution: "@nestjs/testing@npm:10.3.9" dependencies: tslib: 2.6.2 peerDependencies: @@ -5641,7 +5651,7 @@ __metadata: optional: true "@nestjs/platform-express": optional: true - checksum: 435e1f71288384c79b32de6abf73a84428e22a62df30e8c2b336bc55566b96ebc931236aafe733a53603db59640e81621f06bb81a0904f24999e77ff3a6c89f3 + checksum: b8408c9fe6ccb82a75b6459155388ee4471432d20134789788a5f9a8a2c0df2680882b0d4350629aef0fa4cac653fa5875ad053e0d4503647c7276665e094c89 languageName: node linkType: hard @@ -5725,18 +5735,12 @@ __metadata: languageName: node linkType: hard -"@nrwl/devkit@npm:^15.0.0": - version: 15.9.4 - resolution: "@nrwl/devkit@npm:15.9.4" +"@nrwl/devkit@npm:19.3.0": + version: 19.3.0 + resolution: "@nrwl/devkit@npm:19.3.0" dependencies: - ejs: ^3.1.7 - ignore: ^5.0.4 - semver: 7.3.4 - tmp: ~0.2.1 - tslib: ^2.3.0 - peerDependencies: - nx: ">= 14.1 <= 16" - checksum: 4207edab94384315bc80da673ae5c31bd63a8944c69ad1a2a2834d0e6f9ef5eb8a4118a1943ae855c6da889e326490cc4e5df718cb8df5853263e3b3c44d0148 + "@nx/devkit": 19.3.0 + checksum: 47ceb34813cd5a3dc11d3dbfdcf76fd1dfd09b315045197fdb45b651cc67ef476b46d289da94c37614cd109806a975c938e64ebe2e6c0b7e9100c05a51f27e4e languageName: node linkType: hard @@ -5810,14 +5814,23 @@ __metadata: languageName: node linkType: hard -"@nx-plus/docusaurus@npm:^15.0.0-rc.0": - version: 15.0.0-rc.0 - resolution: "@nx-plus/docusaurus@npm:15.0.0-rc.0" +"@nx-extend/docusaurus@npm:^1.0.0": + version: 1.0.0 + resolution: "@nx-extend/docusaurus@npm:1.0.0" dependencies: - "@nrwl/devkit": ^15.0.0 - peerDependencies: - "@nrwl/workspace": ^15.0.0 - checksum: 351480ff324a576d52929968c2d23de39861543bcb7324108e4fee351656ae60486cbaa6193d2038cde7e6932ad7c35c9f31a11e4fe52a88aa62b07d59acd8da + "@docusaurus/core": 3.4.0 + "@docusaurus/module-type-aliases": 3.4.0 + "@docusaurus/preset-classic": 3.4.0 + "@docusaurus/tsconfig": 3.4.0 + "@docusaurus/types": 3.4.0 + "@mdx-js/react": ^3.0.0 + "@nx/devkit": 19.3.0 + clsx: ^2.0.0 + prism-react-renderer: ^2.3.0 + react: ^18.0.0 + react-dom: ^18.0.0 + tslib: 2.6.2 + checksum: f45cd672b7526a2452e3ee62a8fab502555c88fd75c8460e8d8ca90bdc42e1a2a87b52b40da528c0e51cb34e7da3b1f247eab3dcf4037a170f50930bd994eda2 languageName: node linkType: hard @@ -5839,6 +5852,25 @@ __metadata: languageName: node linkType: hard +"@nx/devkit@npm:19.3.0": + version: 19.3.0 + resolution: "@nx/devkit@npm:19.3.0" + dependencies: + "@nrwl/devkit": 19.3.0 + ejs: ^3.1.7 + enquirer: ~2.3.6 + ignore: ^5.0.4 + minimatch: 9.0.3 + semver: ^7.5.3 + tmp: ~0.2.1 + tslib: ^2.3.0 + yargs-parser: 21.1.1 + peerDependencies: + nx: ">= 17 <= 20" + checksum: 32538ea477361f195aa5af2160371a89169c4a557ea0baccf6b74753817fd983ca976f82b46815c14040301f8ea5d2bae3d979fb1a669bc96966ad5cfa5d77a9 + languageName: node + linkType: hard + "@nx/eslint-plugin@npm:18.3.4": version: 18.3.4 resolution: "@nx/eslint-plugin@npm:18.3.4" @@ -6201,7 +6233,7 @@ __metadata: dependencies: lodash.merge: ^4.6.2 reflect-metadata: ^0.2.2 - tslib: ^2.6.2 + tslib: ^2.6.3 peerDependencies: "@nestjs/common": ^9.0.0 || ^10.0.0 class-transformer: ^0.5 @@ -6217,7 +6249,7 @@ __metadata: lodash.omit: ^4.5.0 lower-case-first: ^2.0.2 pluralize: ^8.0.0 - tslib: ^2.6.2 + tslib: ^2.6.3 upper-case-first: ^2.0.2 peerDependencies: "@apollo/gateway": ^0.44.1 || ^0.46.0 || ^0.48.0 || ^0.49.0 || ^0.50.0 || ^2.0.0 @@ -6239,7 +6271,7 @@ __metadata: camel-case: ^4.1.2 lodash.escaperegexp: ^4.1.2 lodash.merge: ^4.6.2 - tslib: ^2.6.2 + tslib: ^2.6.3 peerDependencies: "@nestjs/common": ^9.0.0 || ^10.0.0 "@nestjs/mongoose": ^9.0.0 || ^10.0.0 @@ -6273,7 +6305,7 @@ __metadata: dependencies: camel-case: ^4.1.2 lodash.pick: 4.4.0 - tslib: ^2.6.2 + tslib: ^2.6.3 peerDependencies: "@nestjs/common": ^9.0.0 || ^10.0.0 "@nestjs/sequelize": ^9.0.0 || ^10.0.0 @@ -6290,7 +6322,7 @@ __metadata: is-class: 0.0.9 lodash.escaperegexp: ^4.1.2 lodash.merge: ^4.6.2 - tslib: ^2.6.2 + tslib: ^2.6.3 peerDependencies: "@m8a/nestjs-typegoose": ^9.0.0 || ^10.0.0 || ^11.0.0 "@nestjs/common": ^9.0.0 || ^10.0.0 @@ -6307,8 +6339,8 @@ __metadata: lodash.filter: ^4.6.0 lodash.merge: ^4.6.2 lodash.omit: ^4.5.0 - tslib: ^2.6.2 - uuid: ^9.0.1 + tslib: ^2.6.3 + uuid: ^10.0.0 peerDependencies: "@nestjs/common": ^9.0.0 || ^10.0.0 "@nestjs/typeorm": ^9.0.0 || ^10.0.0 @@ -6623,18 +6655,18 @@ __metadata: languageName: node linkType: hard -"@typegoose/typegoose@npm:^12.4.0": - version: 12.4.0 - resolution: "@typegoose/typegoose@npm:12.4.0" +"@typegoose/typegoose@npm:^12.5.0": + version: 12.5.0 + resolution: "@typegoose/typegoose@npm:12.5.0" dependencies: lodash: ^4.17.20 loglevel: ^1.9.1 reflect-metadata: ^0.2.2 - semver: ^7.6.0 + semver: ^7.6.2 tslib: ^2.6.2 peerDependencies: - mongoose: ~8.3.1 - checksum: 60bbde7fa8469bd4555884c67f0790338f954ed6664025010e5fd81fca608da7873ce6fa45a5fcb922c3ba49921439f65b993cdc1651956a583c2533f2ed420c + mongoose: ~8.4.0 + checksum: dbae07a7dd3afe0b27e21c34445cc342a330f9cb5b56a99c7e35179421f4c9b0c5fb547d89c50c0112e2173b671577c57115185ba80f3200c97799f94145e80f languageName: node linkType: hard @@ -7095,12 +7127,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:20.12.8": - version: 20.12.8 - resolution: "@types/node@npm:20.12.8" +"@types/node@npm:20.14.2": + version: 20.14.2 + resolution: "@types/node@npm:20.14.2" dependencies: undici-types: ~5.26.4 - checksum: 84d4876d95792a9567d2cc5a2c48db7028c397820e842cd65e2f848c23dd4b868b7131bda8eb66a1729d4944289070153d6180048de5bd155b35de421283c433 + checksum: 265362479b8f3b50fcd1e3f9e9af6121feb01a478dff0335ae67cccc3babfe45d0f12209d3d350595eebd7e67471762697b877c380513f8e5d27a238fa50c805 languageName: node linkType: hard @@ -7467,20 +7499,18 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:7.8.0": - version: 7.8.0 - resolution: "@typescript-eslint/eslint-plugin@npm:7.8.0" +"@typescript-eslint/eslint-plugin@npm:7.13.0": + version: 7.13.0 + resolution: "@typescript-eslint/eslint-plugin@npm:7.13.0" dependencies: "@eslint-community/regexpp": ^4.10.0 - "@typescript-eslint/scope-manager": 7.8.0 - "@typescript-eslint/type-utils": 7.8.0 - "@typescript-eslint/utils": 7.8.0 - "@typescript-eslint/visitor-keys": 7.8.0 - debug: ^4.3.4 + "@typescript-eslint/scope-manager": 7.13.0 + "@typescript-eslint/type-utils": 7.13.0 + "@typescript-eslint/utils": 7.13.0 + "@typescript-eslint/visitor-keys": 7.13.0 graphemer: ^1.4.0 ignore: ^5.3.1 natural-compare: ^1.4.0 - semver: ^7.6.0 ts-api-utils: ^1.3.0 peerDependencies: "@typescript-eslint/parser": ^7.0.0 @@ -7488,25 +7518,35 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 2a95bcbd2467892a56f4b0eb262c411abeb15f8d6b581d132fc2a57aa47eb4edc751f02e1a8ac88b7a3330c770a61cdaf6456aa7837b0ee50b5468397324b3fb + checksum: 8bb62f7d4ab3af3656e564c0dd164316e1518475e34a65495b8b2ff816ce24e6df9b1b1d3616bc128fe1d6f26247a04b01513d99e69e2cf0a8048f32b67d58c5 languageName: node linkType: hard -"@typescript-eslint/parser@npm:7.8.0": - version: 7.8.0 - resolution: "@typescript-eslint/parser@npm:7.8.0" +"@typescript-eslint/parser@npm:7.13.0": + version: 7.13.0 + resolution: "@typescript-eslint/parser@npm:7.13.0" dependencies: - "@typescript-eslint/scope-manager": 7.8.0 - "@typescript-eslint/types": 7.8.0 - "@typescript-eslint/typescript-estree": 7.8.0 - "@typescript-eslint/visitor-keys": 7.8.0 + "@typescript-eslint/scope-manager": 7.13.0 + "@typescript-eslint/types": 7.13.0 + "@typescript-eslint/typescript-estree": 7.13.0 + "@typescript-eslint/visitor-keys": 7.13.0 debug: ^4.3.4 peerDependencies: eslint: ^8.56.0 peerDependenciesMeta: typescript: optional: true - checksum: fd077b7f7e1348e64b739a1579dcaebb6933392635614d27008d5a521809992df7b93771dd54efe809b320d224c10ff024ea7ef7c7c578f673a7a937e869c314 + checksum: dd7ef8380d954bb073b9d5d9f785fdc46a109d2938691f9b5fa6c227bd808bb64d8afc6ccccf217d3499deb8947d2f22ed51862e2e9563987ba3e225c58583a3 + languageName: node + linkType: hard + +"@typescript-eslint/scope-manager@npm:7.13.0": + version: 7.13.0 + resolution: "@typescript-eslint/scope-manager@npm:7.13.0" + dependencies: + "@typescript-eslint/types": 7.13.0 + "@typescript-eslint/visitor-keys": 7.13.0 + checksum: fb9663f414985e0fecd0952a9c5ff2a2e2b975cc7eb07a3fa13243b30d8aa67f9b707d636aa050b673b50a6b63aa8b5ba78a64f712e801e23f9c86e1896c3f21 languageName: node linkType: hard @@ -7520,7 +7560,24 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:7.8.0, @typescript-eslint/type-utils@npm:^7.3.0": +"@typescript-eslint/type-utils@npm:7.13.0": + version: 7.13.0 + resolution: "@typescript-eslint/type-utils@npm:7.13.0" + dependencies: + "@typescript-eslint/typescript-estree": 7.13.0 + "@typescript-eslint/utils": 7.13.0 + debug: ^4.3.4 + ts-api-utils: ^1.3.0 + peerDependencies: + eslint: ^8.56.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 5f325fc325b166853444354e254c0d7fbb15dde2a61bbf63313cc58cb7a0546023241848671f216c268f1b87dce9c1e40b89dccae1846f2662e2cf2c99a83aef + languageName: node + linkType: hard + +"@typescript-eslint/type-utils@npm:^7.3.0": version: 7.8.0 resolution: "@typescript-eslint/type-utils@npm:7.8.0" dependencies: @@ -7537,6 +7594,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:7.13.0": + version: 7.13.0 + resolution: "@typescript-eslint/types@npm:7.13.0" + checksum: 1b81398bf4d0cb2602220d3a64f3bb74cd7b1e3e75fc1aecd28b9a6d6d20314ed7dffe057db3526ef3bdaa951e401443bb82e034cdebee79b28ea3b4ca9ff50f + languageName: node + linkType: hard + "@typescript-eslint/types@npm:7.8.0": version: 7.8.0 resolution: "@typescript-eslint/types@npm:7.8.0" @@ -7544,6 +7608,25 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:7.13.0": + version: 7.13.0 + resolution: "@typescript-eslint/typescript-estree@npm:7.13.0" + dependencies: + "@typescript-eslint/types": 7.13.0 + "@typescript-eslint/visitor-keys": 7.13.0 + debug: ^4.3.4 + globby: ^11.1.0 + is-glob: ^4.0.3 + minimatch: ^9.0.4 + semver: ^7.6.0 + ts-api-utils: ^1.3.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 5a410db27ddb514344414a579e9f81a0db0e7e9f579aa624ace223655b905705a37510992a94924d9ead3c4c84c9357cf5358599036f7c44f50b56b54a791d82 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:7.8.0": version: 7.8.0 resolution: "@typescript-eslint/typescript-estree@npm:7.8.0" @@ -7563,6 +7646,20 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/utils@npm:7.13.0": + version: 7.13.0 + resolution: "@typescript-eslint/utils@npm:7.13.0" + dependencies: + "@eslint-community/eslint-utils": ^4.4.0 + "@typescript-eslint/scope-manager": 7.13.0 + "@typescript-eslint/types": 7.13.0 + "@typescript-eslint/typescript-estree": 7.13.0 + peerDependencies: + eslint: ^8.56.0 + checksum: d57c60767949e3ea9d9b33de69a18396fea0e5d2eeb13ef8bbdcfe9c8fae62bf5af25f571b1b7c480362ddb9ccd8f811df4330af595b32a46bf0b8b8ce9b598e + languageName: node + linkType: hard + "@typescript-eslint/utils@npm:7.8.0, @typescript-eslint/utils@npm:^6.0.0 || ^7.0.0, @typescript-eslint/utils@npm:^7.3.0": version: 7.8.0 resolution: "@typescript-eslint/utils@npm:7.8.0" @@ -7580,6 +7677,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:7.13.0": + version: 7.13.0 + resolution: "@typescript-eslint/visitor-keys@npm:7.13.0" + dependencies: + "@typescript-eslint/types": 7.13.0 + eslint-visitor-keys: ^3.4.3 + checksum: 23d48e1c1b9e16e5a867615ffe7111f637224b79bd738f94282b610a0b6b7bf5e436e1422e82395243d0f58714353fc613a11ea671bb217ea99d710f93ab6a26 + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:7.8.0": version: 7.8.0 resolution: "@typescript-eslint/visitor-keys@npm:7.8.0" @@ -8960,7 +9067,7 @@ __metadata: languageName: node linkType: hard -"bson@npm:^6.4.0, bson@npm:^6.5.0": +"bson@npm:^6.7.0": version: 6.7.0 resolution: "bson@npm:6.7.0" checksum: f77b7001e2ec603b1058e9f2d99b642be4673e0356adf4fbdc463afd89de434d3be9d81305c1befbcda9bf8616e70a8f7ec0c8ec7a79154ca40ba455b73ea280 @@ -11572,9 +11679,9 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-jest@npm:28.5.0": - version: 28.5.0 - resolution: "eslint-plugin-jest@npm:28.5.0" +"eslint-plugin-jest@npm:28.6.0": + version: 28.6.0 + resolution: "eslint-plugin-jest@npm:28.6.0" dependencies: "@typescript-eslint/utils": ^6.0.0 || ^7.0.0 peerDependencies: @@ -11586,7 +11693,7 @@ __metadata: optional: true jest: optional: true - checksum: 73ba168fb028db0765027c9e7844fe37cb6b660125929e72c5cf5c8e0fd4e67e7136409583256cf6267e066e1aa0be498c0ee729d1f3babaef4a3e7bdac428a8 + checksum: 5abcef6933445ae0017dcea6cafacb4aaab6619f8660d1097667dd040129b4efa3f1284ded1c2605c7d14c11c976c725912f660dcdec8278c6f6ce793ff9dab6 languageName: node linkType: hard @@ -12545,7 +12652,7 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:^11.1.1": +"fs-extra@npm:^11.1.1, fs-extra@npm:^11.2.0": version: 11.2.0 resolution: "fs-extra@npm:11.2.0" dependencies: @@ -12858,7 +12965,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:10.3.10, glob@npm:^10.3.7": +"glob@npm:10.3.10": version: 10.3.10 resolution: "glob@npm:10.3.10" dependencies: @@ -13087,14 +13194,14 @@ __metadata: languageName: node linkType: hard -"graphql-query-complexity@npm:0.12.0": - version: 0.12.0 - resolution: "graphql-query-complexity@npm:0.12.0" +"graphql-query-complexity@npm:1.0.0": + version: 1.0.0 + resolution: "graphql-query-complexity@npm:1.0.0" dependencies: lodash.get: ^4.4.2 peerDependencies: graphql: ^14.6.0 || ^15.0.0 || ^16.0.0 - checksum: 9323bebd2a3cc5d85c34a8426f6312721a5bf5f465eb00c1fceb5fd2779a1f26edc48c119a3db6cf8e000390aacfe2126de3b316a159bd6742877ae97519df2f + checksum: 1cc9ce5379922f69acf3fbb63a88500d759340a179c93df845d385f1835b59aad7153bc15fce6d654af04954414b689b9fa6d7ba576470c4e17fefe2955bd23a languageName: node linkType: hard @@ -13145,10 +13252,10 @@ __metadata: languageName: node linkType: hard -"graphql@npm:16.8.1": - version: 16.8.1 - resolution: "graphql@npm:16.8.1" - checksum: 8d304b7b6f708c8c5cc164b06e92467dfe36aff6d4f2cf31dd19c4c2905a0e7b89edac4b7e225871131fd24e21460836b369de0c06532644d15b461d55b1ccc0 +"graphql@npm:16.8.2": + version: 16.8.2 + resolution: "graphql@npm:16.8.2" + checksum: 1a5ba8087b3ffb60627ab4b71565e77049f621da49144985d3b1e35a9c70bf846476a7b5912342eda72d70bd71f9427a3d73712b4250e0175d458b8011c9deba languageName: node linkType: hard @@ -17533,9 +17640,9 @@ __metadata: languageName: node linkType: hard -"mongodb-memory-server-core@npm:9.2.0": - version: 9.2.0 - resolution: "mongodb-memory-server-core@npm:9.2.0" +"mongodb-memory-server-core@npm:9.3.0": + version: 9.3.0 + resolution: "mongodb-memory-server-core@npm:9.3.0" dependencies: async-mutex: ^0.4.0 camelcase: ^6.3.0 @@ -17545,30 +17652,30 @@ __metadata: https-proxy-agent: ^7.0.4 mongodb: ^5.9.1 new-find-package-json: ^2.0.0 - semver: ^7.6.0 + semver: ^7.6.2 tar-stream: ^3.1.7 tslib: ^2.6.2 yauzl: ^3.1.3 - checksum: 980a9a3bac6462f05ede9d1f5efee8c22e45b629ff2910e09743ae286580abd4e792e666410faa7a0718de55ea955736fb1b4ffbd139c88fd85d27dba60d96d5 + checksum: 93179d10c784d2d202032b2485b16597dec12fc9d93ab8e706aedf154431045860c3562023c7fb2fedb3307eb4ac42718925a0503be59a2e974612128f552741 languageName: node linkType: hard -"mongodb-memory-server@npm:9.2.0": - version: 9.2.0 - resolution: "mongodb-memory-server@npm:9.2.0" +"mongodb-memory-server@npm:9.3.0": + version: 9.3.0 + resolution: "mongodb-memory-server@npm:9.3.0" dependencies: - mongodb-memory-server-core: 9.2.0 + mongodb-memory-server-core: 9.3.0 tslib: ^2.6.2 - checksum: dd90fa12ded7614b8b88fbe68eb203c32bfc49fc27807163ace930b49a83ed2064a18bf53d0f60b21b06b7135ca87f7d7b8fabaff1db8b2111250399c0bd5bd8 + checksum: e3d32546c1b04be4e417151ff7cd3a92a3e01289e60193dcfd6fd81d6b63672c9f31e124c8b1110b0434526115d7d68ad140ae8ad310b6a233af7123adb1582c languageName: node linkType: hard -"mongodb@npm:6.5.0": - version: 6.5.0 - resolution: "mongodb@npm:6.5.0" +"mongodb@npm:6.6.2": + version: 6.6.2 + resolution: "mongodb@npm:6.6.2" dependencies: "@mongodb-js/saslprep": ^1.1.5 - bson: ^6.4.0 + bson: ^6.7.0 mongodb-connection-string-url: ^3.0.0 peerDependencies: "@aws-sdk/credential-providers": ^3.188.0 @@ -17593,7 +17700,7 @@ __metadata: optional: true socks: optional: true - checksum: 5774dfdd02d8d8e6bb70bf870f19bfad332da612fa6685d182b2e09da4f1306995ddf54f13ae0f6c3936e74444741689c04e7c009e7a847473f08f522ad16542 + checksum: 865cad2ef2e8300d84bae3022c3440e2085fd09b5dad11efb27e1955b63a747772ae78cfcde7c314717fdc92704c99b8507bb204dbe8bf4406bb46bc7df9b4dd languageName: node linkType: hard @@ -17629,18 +17736,18 @@ __metadata: languageName: node linkType: hard -"mongoose@npm:^8.3.3": - version: 8.3.3 - resolution: "mongoose@npm:8.3.3" +"mongoose@npm:^8.4.1": + version: 8.4.3 + resolution: "mongoose@npm:8.4.3" dependencies: - bson: ^6.5.0 + bson: ^6.7.0 kareem: 2.6.3 - mongodb: 6.5.0 + mongodb: 6.6.2 mpath: 0.9.0 mquery: 5.0.0 ms: 2.1.3 - sift: 16.0.1 - checksum: fcdf0548c42beca4b3ab78a38f6d6779de5e28a52cdcc70988d2ccadffe22213f8b4ab9e175cc169f337685a804e4eb6570d9f0d0ebd9c694ed887aae4aa8e12 + sift: 17.1.3 + checksum: dac8d1608e2e0e7580a19daa15ffe1a24987a35bdadc6c141358ea2530f1f825b01949ea0829508713e886ebfc3f7e76751dfa149273376a9d8617d8c53aebc8 languageName: node linkType: hard @@ -17736,9 +17843,9 @@ __metadata: languageName: node linkType: hard -"mysql2@npm:3.9.7": - version: 3.9.7 - resolution: "mysql2@npm:3.9.7" +"mysql2@npm:3.10.1": + version: 3.10.1 + resolution: "mysql2@npm:3.10.1" dependencies: denque: ^2.1.0 generate-function: ^2.3.1 @@ -17748,7 +17855,7 @@ __metadata: named-placeholders: ^1.1.3 seq-queue: ^0.0.5 sqlstring: ^2.3.2 - checksum: 535261d076f840f0966788b3f33a5ff7872e5da321240c2359be5c9e7ec19197ed5f6e01f0bc7beae06dd291d03eb2bde00f474461a578debcb85fcd98e347d3 + checksum: 293e1a76bfdbde146285ee7028336da7693b5dfb507a7c264216fdb3fe2088ba0f5d5a45a0a9e7c4de4f39c3f44edbe890fb05b28299d6c731fc2e4a281836e3 languageName: node linkType: hard @@ -17832,37 +17939,37 @@ __metadata: dependencies: "@actions/core": ^1.10.1 "@apollo/federation": 0.38.1 - "@apollo/gateway": 2.7.5 + "@apollo/gateway": 2.8.0 "@apollo/server": ^4.10.4 - "@apollo/subgraph": 2.7.5 + "@apollo/subgraph": 2.8.0 "@commitlint/cli": 19.3.0 "@commitlint/config-conventional": ^19.2.2 - "@docusaurus/core": 3.3.2 - "@docusaurus/module-type-aliases": 3.3.2 - "@docusaurus/preset-classic": 3.3.2 + "@docusaurus/core": 3.4.0 + "@docusaurus/module-type-aliases": 3.4.0 + "@docusaurus/preset-classic": 3.4.0 "@jscutlery/semver": 5.2.2 "@m8a/nestjs-typegoose": 12.0.0 "@nestjs/apollo": ^12.1.0 "@nestjs/cli": 10.3.2 - "@nestjs/common": 10.3.8 - "@nestjs/core": 10.3.8 + "@nestjs/common": 10.3.9 + "@nestjs/core": 10.3.9 "@nestjs/graphql": ^12.1.1 "@nestjs/jwt": 10.2.0 "@nestjs/mongoose": 10.0.6 "@nestjs/passport": 10.0.3 - "@nestjs/platform-express": 10.3.8 + "@nestjs/platform-express": 10.3.9 "@nestjs/schematics": 10.1.1 "@nestjs/sequelize": 10.0.1 "@nestjs/swagger": ^7.1.15 - "@nestjs/testing": ^10.3.8 + "@nestjs/testing": ^10.3.9 "@nestjs/typeorm": ^10.0.2 - "@nx-plus/docusaurus": ^15.0.0-rc.0 + "@nx-extend/docusaurus": ^1.0.0 "@nx/eslint": 18.3.4 "@nx/eslint-plugin": 18.3.4 "@nx/jest": 18.3.4 "@nx/js": 18.3.4 "@nx/node": 18.3.4 - "@typegoose/typegoose": ^12.4.0 + "@typegoose/typegoose": ^12.5.0 "@types/express": 4.17.21 "@types/jest": 29.5.12 "@types/lodash.escaperegexp": 4.1.9 @@ -17870,15 +17977,15 @@ __metadata: "@types/lodash.merge": 4.6.9 "@types/lodash.omit": 4.5.9 "@types/lodash.pick": 4.4.9 - "@types/node": 20.12.8 + "@types/node": 20.14.2 "@types/passport-jwt": 4.0.1 "@types/passport-local": 1.0.38 "@types/pluralize": 0.0.33 "@types/supertest": 6.0.2 "@types/uuid": 9.0.8 "@types/ws": 8.5.10 - "@typescript-eslint/eslint-plugin": 7.8.0 - "@typescript-eslint/parser": 7.8.0 + "@typescript-eslint/eslint-plugin": 7.13.0 + "@typescript-eslint/parser": 7.13.0 class-transformer: 0.5.1 class-validator: 0.14.1 clsx: ^2.1.1 @@ -17889,45 +17996,42 @@ __metadata: eslint-config-prettier: 9.1.0 eslint-import-resolver-typescript: 3.6.1 eslint-plugin-import: 2.29.1 - eslint-plugin-jest: 28.5.0 + eslint-plugin-jest: 28.6.0 eslint-plugin-prettier: 5.1.3 eslint-plugin-simple-import-sort: ^12.1.0 eslint-plugin-tsdoc: 0.2.17 - graphql: 16.8.1 - graphql-query-complexity: 0.12.0 + graphql: 16.8.2 + graphql-query-complexity: 1.0.0 graphql-subscriptions: 2.0.0 graphql-tools: 9.0.1 husky: 9.0.11 jest: 29.7.0 jest-extended: 4.0.2 - mongodb-memory-server: 9.2.0 - mongoose: ^8.3.3 - mysql2: 3.9.7 + mongodb-memory-server: 9.3.0 + mongoose: ^8.4.1 + mysql2: 3.10.1 nx: 18.3.4 passport: 0.7.0 passport-jwt: 4.0.1 passport-local: 1.0.0 - pg: 8.11.5 - prettier: 3.2.5 - prism-react-renderer: ^2.3.1 + pg: 8.12.0 + prettier: 3.3.2 react: ^18.3.1 react-dom: ^18.3.1 reflect-metadata: ^0.2.2 - rimraf: 5.0.5 - rxjs: 7.8.1 sequelize: 6.37.3 sequelize-typescript: 2.1.6 sql-formatter: ^15.3.1 sqlite3: ^5.1.7 supertest: 7.0.0 - ts-jest: 29.1.2 + ts-jest: 29.1.5 ts-loader: 9.5.1 ts-mockito: 2.6.1 ts-morph: ^22.0.0 ts-node: 10.9.2 tsconfig-extends: 1.0.1 tsconfig-paths: 4.2.0 - tslib: ^2.6.2 + tslib: ^2.6.3 typeorm: ^0.3.17 typescript: 5.4.5 languageName: unknown @@ -18979,9 +19083,9 @@ __metadata: languageName: node linkType: hard -"pg@npm:8.11.5": - version: 8.11.5 - resolution: "pg@npm:8.11.5" +"pg@npm:8.12.0": + version: 8.12.0 + resolution: "pg@npm:8.12.0" dependencies: pg-cloudflare: ^1.1.1 pg-connection-string: ^2.6.4 @@ -18997,7 +19101,7 @@ __metadata: peerDependenciesMeta: pg-native: optional: true - checksum: 2317bcc7080f116ced761620b8cbc98175080cacf1e8a894e14f468bcd9c996a7bd5ef36c9db91d380a772667508eb72c42b10206a90cd82b0b4a7669a19f9e4 + checksum: 8450b61c787f360e22182aa853548f834f13622714868d0789a60f63743d66ae28930cdca0ef0251bfc89b04679e9074c1398f172c2937bf59b5a360337f4149 languageName: node linkType: hard @@ -19605,12 +19709,12 @@ __metadata: languageName: node linkType: hard -"prettier@npm:3.2.5": - version: 3.2.5 - resolution: "prettier@npm:3.2.5" +"prettier@npm:3.3.2": + version: 3.3.2 + resolution: "prettier@npm:3.3.2" bin: prettier: bin/prettier.cjs - checksum: 2ee4e1417572372afb7a13bb446b34f20f1bf1747db77cf6ccaf57a9be005f2f15c40f903d41a6b79eec3f57fff14d32a20fb6dee1f126da48908926fe43c311 + checksum: 5557d8caed0b182f68123c2e1e370ef105251d1dd75800fadaece3d061daf96b1389141634febf776050f9d732c7ae8fd444ff0b4a61b20535e7610552f32c69 languageName: node linkType: hard @@ -19653,7 +19757,7 @@ __metadata: languageName: node linkType: hard -"prism-react-renderer@npm:^2.3.0, prism-react-renderer@npm:^2.3.1": +"prism-react-renderer@npm:^2.3.0": version: 2.3.1 resolution: "prism-react-renderer@npm:2.3.1" dependencies: @@ -19948,7 +20052,7 @@ __metadata: languageName: node linkType: hard -"react-dom@npm:^18.3.1": +"react-dom@npm:^18.0.0, react-dom@npm:^18.3.1": version: 18.3.1 resolution: "react-dom@npm:18.3.1" dependencies: @@ -20084,7 +20188,7 @@ __metadata: languageName: node linkType: hard -"react@npm:^18.3.1": +"react@npm:^18.0.0, react@npm:^18.3.1": version: 18.3.1 resolution: "react@npm:18.3.1" dependencies: @@ -20674,17 +20778,6 @@ __metadata: languageName: node linkType: hard -"rimraf@npm:5.0.5": - version: 5.0.5 - resolution: "rimraf@npm:5.0.5" - dependencies: - glob: ^10.3.7 - bin: - rimraf: dist/esm/bin.mjs - checksum: d66eef829b2e23b16445f34e73d75c7b7cf4cbc8834b04720def1c8f298eb0753c3d76df77325fad79d0a2c60470525d95f89c2475283ad985fd7441c32732d1 - languageName: node - linkType: hard - "rimraf@npm:^3.0.0, rimraf@npm:^3.0.2": version: 3.0.2 resolution: "rimraf@npm:3.0.2" @@ -20890,17 +20983,6 @@ __metadata: languageName: node linkType: hard -"semver@npm:7.3.4": - version: 7.3.4 - resolution: "semver@npm:7.3.4" - dependencies: - lru-cache: ^6.0.0 - bin: - semver: bin/semver.js - checksum: 96451bfd7cba9b60ee87571959dc47e87c95b2fe58a9312a926340fee9907fc7bc062c352efdaf5bb24b2dff59c145e14faf7eb9d718a84b4751312531b39f43 - languageName: node - linkType: hard - "semver@npm:^6.0.0, semver@npm:^6.3.0, semver@npm:^6.3.1": version: 6.3.1 resolution: "semver@npm:6.3.1" @@ -20932,6 +21014,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.6.2": + version: 7.6.2 + resolution: "semver@npm:7.6.2" + bin: + semver: bin/semver.js + checksum: 40f6a95101e8d854357a644da1b8dd9d93ce786d5c6a77227bc69dbb17bea83d0d1d1d7c4cd5920a6df909f48e8bd8a5909869535007f90278289f2451d0292d + languageName: node + linkType: hard + "send@npm:0.18.0": version: 0.18.0 resolution: "send@npm:0.18.0" @@ -21209,10 +21300,10 @@ __metadata: languageName: node linkType: hard -"sift@npm:16.0.1": - version: 16.0.1 - resolution: "sift@npm:16.0.1" - checksum: 5fe18a517a20c35e0c05238797cc605094a6cb602b08c4661268c415b71a10f1a55ee4cc8728552e390e7cb4683a33bcbd68d7971eb44645cc6211e2f00dd233 +"sift@npm:17.1.3": + version: 17.1.3 + resolution: "sift@npm:17.1.3" + checksum: 56d09c72720cd75f757dad31fc13cc84461c06c0416d23c1dc05e64276676fa1fecaddb055f0d2aa714d36a93c2acaad8cb2f2ef6d06d8c8bb1af84657de2046 languageName: node linkType: hard @@ -22382,9 +22473,9 @@ __metadata: languageName: node linkType: hard -"ts-jest@npm:29.1.2": - version: 29.1.2 - resolution: "ts-jest@npm:29.1.2" +"ts-jest@npm:29.1.5": + version: 29.1.5 + resolution: "ts-jest@npm:29.1.5" dependencies: bs-logger: 0.x fast-json-stable-stringify: 2.x @@ -22396,6 +22487,7 @@ __metadata: yargs-parser: ^21.0.1 peerDependencies: "@babel/core": ">=7.0.0-beta.0 <8" + "@jest/transform": ^29.0.0 "@jest/types": ^29.0.0 babel-jest: ^29.0.0 jest: ^29.0.0 @@ -22403,6 +22495,8 @@ __metadata: peerDependenciesMeta: "@babel/core": optional: true + "@jest/transform": + optional: true "@jest/types": optional: true babel-jest: @@ -22411,7 +22505,7 @@ __metadata: optional: true bin: ts-jest: cli.js - checksum: a0ce0affc1b716c78c9ab55837829c42cb04b753d174a5c796bb1ddf9f0379fc20647b76fbe30edb30d9b23181908138d6b4c51ef2ae5e187b66635c295cefd5 + checksum: 96bfdea46d7faa83457c2647806a31a86f28656f703515fee9f6d2ff1ccfc58ccfbbe3ae9283f40141a85af0def30afe887843be5b002c08ed5d5189c941eab1 languageName: node linkType: hard @@ -22581,6 +22675,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:^2.6.3": + version: 2.6.3 + resolution: "tslib@npm:2.6.3" + checksum: 74fce0e100f1ebd95b8995fbbd0e6c91bdd8f4c35c00d4da62e285a3363aaa534de40a80db30ecfd388ed7c313c42d930ee0eaf108e8114214b180eec3dbe6f5 + languageName: node + linkType: hard + "tunnel-agent@npm:^0.6.0": version: 0.6.0 resolution: "tunnel-agent@npm:0.6.0" @@ -23184,7 +23285,7 @@ __metadata: languageName: node linkType: hard -"uuid@npm:9.0.1, uuid@npm:^9.0.1": +"uuid@npm:9.0.1": version: 9.0.1 resolution: "uuid@npm:9.0.1" bin: @@ -23193,6 +23294,15 @@ __metadata: languageName: node linkType: hard +"uuid@npm:^10.0.0": + version: 10.0.0 + resolution: "uuid@npm:10.0.0" + bin: + uuid: dist/bin/uuid + checksum: 4b81611ade2885d2313ddd8dc865d93d8dccc13ddf901745edca8f86d99bc46d7a330d678e7532e7ebf93ce616679fb19b2e3568873ac0c14c999032acb25869 + languageName: node + linkType: hard + "uuid@npm:^8.3.2": version: 8.3.2 resolution: "uuid@npm:8.3.2" From ad5c987ed5acbc5c7b68006e5956b6cbdf71ee16 Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Fri, 21 Jun 2024 12:04:28 +0200 Subject: [PATCH 20/32] feat(rest): Added support for `@FilterableField` --- .../decorators/filterable-field.decorator.ts | 11 +- .../query-rest/src/types/query/filter.type.ts | 104 ++++++++++++++++++ .../src/types/query/query-args/interfaces.ts | 2 - .../query-args/offset-query-args.type.ts | 17 ++- 4 files changed, 120 insertions(+), 14 deletions(-) create mode 100644 packages/query-rest/src/types/query/filter.type.ts diff --git a/packages/query-rest/src/decorators/filterable-field.decorator.ts b/packages/query-rest/src/decorators/filterable-field.decorator.ts index c4eded8f8..2b75d2760 100644 --- a/packages/query-rest/src/decorators/filterable-field.decorator.ts +++ b/packages/query-rest/src/decorators/filterable-field.decorator.ts @@ -4,7 +4,7 @@ import { ArrayReflector, Class, getPrototypeChain } from '@ptc-org/nestjs-query- import { ReturnTypeFunc, ReturnTypeFuncValue } from '../interfaces/return-type-func' import { FILTERABLE_FIELD_KEY } from './constants' -import { Field } from './field.decorator' +import { Field, FieldOptions } from './field.decorator' const reflector = new ArrayReflector(FILTERABLE_FIELD_KEY) export type FilterableFieldOptions = { @@ -22,6 +22,13 @@ export interface FilterableFieldDescriptor { advancedOptions?: FilterableFieldOptions } +export function filterableFieldOptionsToField(advancedOptions: FilterableFieldOptions): FieldOptions { + // Remove fields that are not needed in the Field decorator + const { filterRequired, filterDecorators, filterOnly, ...fieldOptions } = advancedOptions + + return fieldOptions +} + /** * Decorator for Fields that should be filterable through a [[FilterType]] * @@ -91,7 +98,7 @@ export function FilterableField( return undefined } - applyDecorators(Field(() => returnTypeFunc, advancedOptions))(target, propertyName, descriptor) + applyDecorators(Field(() => returnTypeFunc, filterableFieldOptionsToField(advancedOptions)))(target, propertyName, descriptor) } } diff --git a/packages/query-rest/src/types/query/filter.type.ts b/packages/query-rest/src/types/query/filter.type.ts new file mode 100644 index 000000000..c217b7efc --- /dev/null +++ b/packages/query-rest/src/types/query/filter.type.ts @@ -0,0 +1,104 @@ +import { applyDecorators } from '@nestjs/common' +import { Class, Filter, MapReflector } from '@ptc-org/nestjs-query-core' + +import { Field, filterableFieldOptionsToField, getFilterableFields } from '../../decorators' + +const reflector = new MapReflector('nestjs-query:filter-type') +// internal cache is used to exit early if the same filter is requested multiple times +// e.g. if there is a circular reference in the relations +// `User -> Post -> User-> Post -> ...` +const internalCache = new Map, Map>>() + +export interface FilterConstructor { + hasRequiredFilters: boolean + + new (): Filter +} + +function getOrCreateFilterType( + TClass: Class, + prefix: string | null, + suffix: string | null, + BaseClass: Class +): FilterConstructor { + const $prefix = prefix ?? '' + const $suffix = suffix ?? '' + + const name = `${$prefix}${TClass.name}${$suffix}` + const typeName = `${name}Filter` + + return reflector.memoize(TClass, typeName, () => { + const fields = getFilterableFields(TClass) + + // if the filter is already in the cache, exist early and return it + // otherwise add it to the cache early so we don't get into an infinite loop + let TClassCache = internalCache.get(TClass) + + if (TClassCache && TClassCache.has(typeName)) { + return TClassCache.get(typeName) as FilterConstructor + } + + const hasRequiredFilters = fields.some((f) => f.advancedOptions?.filterRequired === true) + + class QueryFilter extends BaseClass { + static hasRequiredFilters: boolean = hasRequiredFilters + + public get filter(): Filter { + const filters = fields.reduce((filter, field) => { + if (this[field.schemaName]) { + filter[field.schemaName] = { eq: this[field.schemaName] } + } + + return filter + }, {} as Filter) + + if (Object.keys(filters).length > 0) { + return filters + } + + return super.filter + } + } + + fields.forEach(({ schemaName, advancedOptions }) => { + applyDecorators( + Field( + filterableFieldOptionsToField({ + ...advancedOptions, + nullable: + typeof advancedOptions.filterRequired !== 'undefined' ? !advancedOptions.filterRequired : advancedOptions.nullable, + required: advancedOptions.filterRequired + }) + ), + ...(advancedOptions.filterDecorators || []) + )(QueryFilter.prototype, schemaName) + }) + + TClassCache = TClassCache ?? new Map() + + TClassCache.set(typeName, QueryFilter) + internalCache.set(TClass, TClassCache) + + return QueryFilter as never as FilterConstructor + }) +} + +export function FilterType(TClass: Class, BaseClass: Class): FilterConstructor { + return getOrCreateFilterType(TClass, null, null, BaseClass) +} + +// export function DeleteFilterType(TClass: Class, BaseClass: Class): FilterConstructor { +// return getOrCreateFilterType(TClass, null, 'Delete', BaseClass) +// } +// +// export function UpdateFilterType(TClass: Class, BaseClass: Class): FilterConstructor { +// return getOrCreateFilterType(TClass, null, 'Update', BaseClass) +// } +// +// export function SubscriptionFilterType(TClass: Class, BaseClass: Class): FilterConstructor { +// return getOrCreateFilterType(TClass, null, 'Subscription', BaseClass) +// } +// +// export function AggregateFilterType(TClass: Class, BaseClass: Class): FilterConstructor { +// return getOrCreateFilterType(TClass, null, 'Aggregate', BaseClass) +// } diff --git a/packages/query-rest/src/types/query/query-args/interfaces.ts b/packages/query-rest/src/types/query/query-args/interfaces.ts index 7c63b7367..1ae4634eb 100644 --- a/packages/query-rest/src/types/query/query-args/interfaces.ts +++ b/packages/query-rest/src/types/query/query-args/interfaces.ts @@ -50,8 +50,6 @@ export interface NonePagingQueryArgsTypeOpts extends BaseQueryArgsTypeOpts< export type QueryArgsTypeOpts = OffsetQueryArgsTypeOpts | NonePagingQueryArgsTypeOpts export interface StaticQueryType extends Class> { - SortType: Class> - PageType: Class> FilterType: Class> ConnectionType: StaticConnectionType } diff --git a/packages/query-rest/src/types/query/query-args/offset-query-args.type.ts b/packages/query-rest/src/types/query/query-args/offset-query-args.type.ts index 8d853f1b2..fea2c3ef2 100644 --- a/packages/query-rest/src/types/query/query-args/offset-query-args.type.ts +++ b/packages/query-rest/src/types/query/query-args/offset-query-args.type.ts @@ -1,9 +1,10 @@ import { Class } from '@ptc-org/nestjs-query-core' +import { RestQuery } from '@ptc-org/nestjs-query-rest' import { getOrCreateOffsetConnectionType } from '../../../connection/offset/offset-connection.type' import { Field, SkipIf } from '../../../decorators' -import { RestQuery } from '../../rest-query.type' import { BuildableQueryType } from '../buildable-query.type' +import { FilterType } from '../filter.type' import { OffsetPaging } from '../offset-paging.type' import { PagingStrategies } from '../paging' import { DEFAULT_QUERY_OPTS } from './constants' @@ -13,22 +14,18 @@ export function createOffsetQueryArgs( DTOClass: Class, opts: OffsetQueryArgsTypeOpts = { ...DEFAULT_QUERY_OPTS, pagingStrategy: PagingStrategies.OFFSET } ): StaticQueryType { - // const F = FilterType(DTOClass) // const S = getOrCreateSortType(DTOClass) + const ConnectionType = getOrCreateOffsetConnectionType(DTOClass, opts) class QueryArgs extends OffsetPaging implements BuildableQueryType { - static SortType = null - - static FilterType = null - static ConnectionType = ConnectionType - static PageType = OffsetPaging - public sorting = opts.defaultSort - public filter = opts.defaultFilter + public get filter() { + return opts.defaultFilter + } @SkipIf( () => !opts.enableSearch, @@ -53,5 +50,5 @@ export function createOffsetQueryArgs( } } - return QueryArgs + return FilterType(DTOClass, QueryArgs) as never as StaticQueryType } From f1ef57f5303c3c8fb2091acfb3430cc08639743a Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Fri, 21 Jun 2024 12:59:53 +0200 Subject: [PATCH 21/32] fix(rest): Fixed filterable fields always required --- packages/query-rest/src/types/query/filter.type.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/query-rest/src/types/query/filter.type.ts b/packages/query-rest/src/types/query/filter.type.ts index c217b7efc..c47c2677d 100644 --- a/packages/query-rest/src/types/query/filter.type.ts +++ b/packages/query-rest/src/types/query/filter.type.ts @@ -67,7 +67,9 @@ function getOrCreateFilterType( ...advancedOptions, nullable: typeof advancedOptions.filterRequired !== 'undefined' ? !advancedOptions.filterRequired : advancedOptions.nullable, - required: advancedOptions.filterRequired + required: Boolean( + typeof advancedOptions.filterRequired !== 'undefined' ? advancedOptions.filterRequired : advancedOptions.required + ) }) ), ...(advancedOptions.filterDecorators || []) From 67a919668b2810e53ba59ab2163b4510b312748d Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Fri, 21 Jun 2024 14:32:40 +0200 Subject: [PATCH 22/32] fix(rest): Fixed `forceArray` and `skipIsEnum` not working for `@Field` --- packages/query-rest/src/decorators/field.decorator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/query-rest/src/decorators/field.decorator.ts b/packages/query-rest/src/decorators/field.decorator.ts index fad8429d7..e834893ab 100644 --- a/packages/query-rest/src/decorators/field.decorator.ts +++ b/packages/query-rest/src/decorators/field.decorator.ts @@ -86,7 +86,7 @@ export function Field( decorators.push(ArrayMaxSize(options.maxItems)) } - if (isArray && options.forceArray) { + if (isArray && advancedOptions?.forceArray) { decorators.push(Transform(({ value }) => (Array.isArray(value) ? value : [value]))) } @@ -116,7 +116,7 @@ export function Field( } } - if (options.enum && !options.skipIsEnum) { + if (options.enum && !advancedOptions?.skipIsEnum) { decorators.push(IsEnum(options.enum)) } From b70eacdf97261bb951a61a41df6983575297fdfd Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Wed, 21 Aug 2024 17:00:40 +0200 Subject: [PATCH 23/32] fix(query-rest): Add min and max validation to field decorator --- .../src/decorators/field.decorator.ts | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/query-rest/src/decorators/field.decorator.ts b/packages/query-rest/src/decorators/field.decorator.ts index e834893ab..c60ab558a 100644 --- a/packages/query-rest/src/decorators/field.decorator.ts +++ b/packages/query-rest/src/decorators/field.decorator.ts @@ -1,7 +1,18 @@ import { applyDecorators } from '@nestjs/common' import { ApiProperty, ApiPropertyOptions } from '@nestjs/swagger' import { Expose, Transform, Type } from 'class-transformer' -import { ArrayMaxSize, IsEnum, IsNotEmpty, IsObject, IsOptional, MaxLength, MinLength, ValidateNested } from 'class-validator' +import { + ArrayMaxSize, + IsEnum, + IsNotEmpty, + IsObject, + IsOptional, + Max, + MaxLength, + Min, + MinLength, + ValidateNested +} from 'class-validator' import { ReturnTypeFunc } from '../interfaces/return-type-func' @@ -98,6 +109,14 @@ export function Field( decorators.push(MaxLength(options.maxLength)) } + if (options.minimum !== undefined) { + decorators.push(Min(options.minimum)) + } + + if (options.maximum !== undefined) { + decorators.push(Max(options.maximum)) + } + if (options.required) { decorators.push(IsNotEmpty()) } else { From 697e90f87a347d95646ee17b0f7ff5a6aca825bf Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Tue, 8 Oct 2024 13:12:31 +0200 Subject: [PATCH 24/32] feat(query-rest): Introduce `FindOneArgsType` and refactor resolvers to use it - Added `FindOneArgsType` for standardizing ID parameter handling. - Updated delete, update, and read resolvers to use `FindOneArgsType`. - Introduced new `id-field.decorator` for marking ID fields. - Removed redundant `BadRequestException` in read resolver. --- .../src/decorators/id-field.decorator.ts | 48 +++++++++++++++++++ packages/query-rest/src/decorators/index.ts | 1 + .../src/resolvers/delete.resolver.ts | 15 ++++-- .../query-rest/src/resolvers/read.resolver.ts | 21 ++++---- .../src/resolvers/update.resolver.ts | 15 ++++-- .../src/types/find-one-args.type.ts | 22 +++++++++ 6 files changed, 108 insertions(+), 14 deletions(-) create mode 100644 packages/query-rest/src/decorators/id-field.decorator.ts create mode 100644 packages/query-rest/src/types/find-one-args.type.ts diff --git a/packages/query-rest/src/decorators/id-field.decorator.ts b/packages/query-rest/src/decorators/id-field.decorator.ts new file mode 100644 index 000000000..75fce2985 --- /dev/null +++ b/packages/query-rest/src/decorators/id-field.decorator.ts @@ -0,0 +1,48 @@ +import { Class, MetaValue, ValueReflector } from '@ptc-org/nestjs-query-core' + +import { Field, FieldOptions } from '../index' +import { ID_FIELD_KEY } from './constants' + +const reflector = new ValueReflector(ID_FIELD_KEY) +export type IDFieldOptions = FieldOptions + +export interface IDFieldDescriptor { + propertyName: string +} + +/** + * Decorator for Fields that should be filterable through a [[FilterType]] + * + * @example + * + * In the following DTO `id`, `title` and `completed` are filterable. + * + * ```ts + * import { IDField } from '@ptc-org/nestjs-query-rest'; + * + * export class TodoItemDTO { + * @IDField() + * id!: string; + * } + * ``` + */ +export function IDField(options?: IDFieldOptions): PropertyDecorator & MethodDecorator { + return ( + target: object, + propertyName: string | symbol, + descriptor?: TypedPropertyDescriptor + ): TypedPropertyDescriptor | void => { + reflector.set(target.constructor as Class, { + propertyName: propertyName.toString() + }) + + if (descriptor) { + return Field(options)(target, propertyName, descriptor) + } + return Field(options)(target, propertyName) + } +} + +export function getIDField(DTOClass: Class): MetaValue { + return reflector.get(DTOClass, true) +} diff --git a/packages/query-rest/src/decorators/index.ts b/packages/query-rest/src/decorators/index.ts index 2a90911b9..bbd1d9172 100644 --- a/packages/query-rest/src/decorators/index.ts +++ b/packages/query-rest/src/decorators/index.ts @@ -5,6 +5,7 @@ export * from './field.decorator' export * from './filterable-field.decorator' export * from './hook.decorator' export * from './hook-args.decorator' +export * from './id-field.decorator' export * from './inject-authorizer.decorator' export * from './query-options.decorator' export * from './resolver-method.decorator' diff --git a/packages/query-rest/src/resolvers/delete.resolver.ts b/packages/query-rest/src/resolvers/delete.resolver.ts index 9d98c5e0c..a66998af7 100644 --- a/packages/query-rest/src/resolvers/delete.resolver.ts +++ b/packages/query-rest/src/resolvers/delete.resolver.ts @@ -7,6 +7,7 @@ import { OperationGroup } from '../auth' import { getDTONames } from '../common' import { AuthorizerFilter, Delete } from '../decorators' import { AuthorizerInterceptor } from '../interceptors' +import { FindOneArgsType } from '../types/find-one-args.type' import { BaseServiceResolver, MutationOpts, ResolverClass, ServiceResolver } from './resolver.interface' // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -18,7 +19,7 @@ export interface DeleteResolverOpts extends MutationOpts { } export interface DeleteResolver> extends ServiceResolver { - deleteOne(id: string, authorizeFilter?: Filter): Promise> + deleteOne(id: FindOneArgsType, authorizeFilter?: Filter): Promise> } /** @@ -32,6 +33,14 @@ export const Deletable = const commonResolverOpts = omit(opts, 'dtoName', 'one', 'many', 'DeleteOneInput', 'DeleteManyInput', 'useSoftDelete') + class DOP extends FindOneArgsType(DTOClass) {} + + Object.defineProperty(DOP, 'name', { + writable: false, + // set a unique name otherwise DI does not inject a unique one for each request + value: `${DTOClass.name}Params` + }) + class DeleteResolverBase extends BaseClass { @Delete( () => DTOClass, @@ -49,14 +58,14 @@ export const Deletable = opts.one ?? {} ) async deleteOne( - @Param('id') id: string, + @Param() params: DOP, @AuthorizerFilter({ operationGroup: OperationGroup.DELETE, many: false }) authorizeFilter?: Filter ): Promise> { - return this.service.deleteOne(id, { + return this.service.deleteOne(params.id, { filter: authorizeFilter ?? {}, useSoftDelete: opts?.useSoftDelete ?? false }) diff --git a/packages/query-rest/src/resolvers/read.resolver.ts b/packages/query-rest/src/resolvers/read.resolver.ts index a022cc82b..f300f99a9 100644 --- a/packages/query-rest/src/resolvers/read.resolver.ts +++ b/packages/query-rest/src/resolvers/read.resolver.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Param } from '@nestjs/common' +import { Param } from '@nestjs/common' import { Class, Filter, mergeQuery, QueryService } from '@ptc-org/nestjs-query-core' import omit from 'lodash.omit' @@ -9,6 +9,7 @@ import { AuthorizerFilter, Get, QueryHookArgs } from '../decorators' import { HookTypes } from '../hooks' import { AuthorizerInterceptor, HookInterceptor } from '../interceptors' import { QueryArgsType } from '../types' +import { FindOneArgsType } from '../types/find-one-args.type' import { OffsetQueryArgsTypeOpts, PagingStrategies, QueryArgsTypeOpts, QueryType, StaticQueryType } from '../types/query' import { BaseServiceResolver, ExtractPagingStrategy, ResolverClass, ResolverOpts, ServiceResolver } from './resolver.interface' @@ -36,7 +37,7 @@ export interface ReadResolver ): Promise> - findById(id: string, authorizeFilter?: Filter): Promise + findById(id: FindOneArgsType, authorizeFilter?: Filter): Promise } /** @@ -59,12 +60,20 @@ export const Readable = class QA extends QueryArgs {} + class FOP extends FindOneArgsType(DTOClass) {} + Object.defineProperty(QA, 'name', { writable: false, // set a unique name otherwise DI does not inject a unique one for each request value: `${DTOClass.name}QueryArgs` }) + Object.defineProperty(FOP, 'name', { + writable: false, + // set a unique name otherwise DI does not inject a unique one for each request + value: `${DTOClass.name}Params` + }) + class ReadResolverBase extends BaseClass { @Get( () => FindDTOClass, @@ -82,18 +91,14 @@ export const Readable = opts.one ?? {} ) public async findById( - @Param('id') id: string, + @Param() params: FOP, @AuthorizerFilter({ operationGroup: OperationGroup.READ, many: false }) authorizeFilter?: Filter ): Promise { - if (!id) { - throw new BadRequestException('id is missing from the request!') - } - - return this.service.getById(id, { + return this.service.getById(params.id, { filter: authorizeFilter, withDeleted: opts?.one?.withDeleted }) diff --git a/packages/query-rest/src/resolvers/update.resolver.ts b/packages/query-rest/src/resolvers/update.resolver.ts index 1226f6679..dc776044e 100644 --- a/packages/query-rest/src/resolvers/update.resolver.ts +++ b/packages/query-rest/src/resolvers/update.resolver.ts @@ -10,6 +10,7 @@ import { AuthorizerFilter, BodyHookArgs, Put } from '../decorators' import { HookTypes } from '../hooks' import { AuthorizerInterceptor, HookInterceptor } from '../interceptors' import { MutationArgsType, UpdateOneInputType } from '../types' +import { FindOneArgsType } from '../types/find-one-args.type' import { BaseServiceResolver, MutationOpts, ResolverClass, ServiceResolver } from './resolver.interface' export interface UpdateResolverOpts> extends MutationOpts { @@ -18,7 +19,7 @@ export interface UpdateResolverOpts> extends MutationO } export interface UpdateResolver> extends ServiceResolver { - updateOne(id: string, input: MutationArgsType>, authFilter?: Filter): Promise + updateOne(id: FindOneArgsType, input: MutationArgsType>, authFilter?: Filter): Promise } /** @internal */ @@ -57,6 +58,14 @@ export const Updateable = class UOI extends MutationArgsType(UpdateOneInput) {} + class UOP extends FindOneArgsType(DTOClass) {} + + Object.defineProperty(UOP, 'name', { + writable: false, + // set a unique name otherwise DI does not inject a unique one for each request + value: `${DTOClass.name}UpdateParams` + }) + class UpdateResolverBase extends BaseClass { @Put( () => DTOClass, @@ -79,7 +88,7 @@ export const Updateable = opts?.one ?? {} ) public updateOne( - @Param('id') id: string, + @Param() params: UOP, @BodyHookArgs() { input }: UOI, @AuthorizerFilter({ operationGroup: OperationGroup.UPDATE, @@ -87,7 +96,7 @@ export const Updateable = }) authorizeFilter?: Filter ): Promise { - return this.service.updateOne(id, input.update, { filter: authorizeFilter ?? {} }) + return this.service.updateOne(params.id, input.update, { filter: authorizeFilter ?? {} }) } } diff --git a/packages/query-rest/src/types/find-one-args.type.ts b/packages/query-rest/src/types/find-one-args.type.ts new file mode 100644 index 000000000..09e7f67ae --- /dev/null +++ b/packages/query-rest/src/types/find-one-args.type.ts @@ -0,0 +1,22 @@ +import { PickType } from '@nestjs/swagger' +import { Class } from '@ptc-org/nestjs-query-core' + +import { getIDField } from '../decorators' + +export interface FindOneArgsType { + id: string | number +} + +/** + * The input type for "one" endpoints. + */ +// eslint-disable-next-line @typescript-eslint/no-redeclare -- intentional +export function FindOneArgsType(DTOClass: Class): Class { + const dtoWithIDField = getIDField(DTOClass) + + class FindOneArgs extends PickType(DTOClass, [dtoWithIDField.propertyName] as never) implements FindOneArgsType { + id: string | number + } + + return FindOneArgs +} From 87b7c057ca93b6fb616502c65096ca5ab452b625 Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Wed, 9 Oct 2024 11:08:30 +0200 Subject: [PATCH 25/32] fix(query-rest): Read resolver not respecting `disabled` opt --- packages/query-rest/src/resolvers/read.resolver.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/query-rest/src/resolvers/read.resolver.ts b/packages/query-rest/src/resolvers/read.resolver.ts index f300f99a9..6c31846e8 100644 --- a/packages/query-rest/src/resolvers/read.resolver.ts +++ b/packages/query-rest/src/resolvers/read.resolver.ts @@ -50,6 +50,10 @@ export const Readable = opts: ReadOpts ) => >>(BaseClass: B): Class> & B => { + if (opts.disabled) { + return BaseClass as never + } + const dtoNames = getDTONames(DTOClass, opts) const { QueryArgs = QueryArgsType(DTOClass, { ...opts, connectionName: `${dtoNames.baseName}Connection` }), From 4111575e3c0f7f64febc12239261d6b89c7f7b15 Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Wed, 9 Oct 2024 11:54:17 +0200 Subject: [PATCH 26/32] feat(query-rest): Add `idOnly` option to IDFieldOptions in id-field.decorator --- packages/query-rest/src/decorators/id-field.decorator.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/query-rest/src/decorators/id-field.decorator.ts b/packages/query-rest/src/decorators/id-field.decorator.ts index 75fce2985..f4eb894d7 100644 --- a/packages/query-rest/src/decorators/id-field.decorator.ts +++ b/packages/query-rest/src/decorators/id-field.decorator.ts @@ -4,7 +4,10 @@ import { Field, FieldOptions } from '../index' import { ID_FIELD_KEY } from './constants' const reflector = new ValueReflector(ID_FIELD_KEY) -export type IDFieldOptions = FieldOptions + +export interface IDFieldOptions extends FieldOptions { + idOnly?: boolean +} export interface IDFieldDescriptor { propertyName: string @@ -36,6 +39,10 @@ export function IDField(options?: IDFieldOptions): PropertyDecorator & MethodDec propertyName: propertyName.toString() }) + if (options?.idOnly) { + return + } + if (descriptor) { return Field(options)(target, propertyName, descriptor) } From 7d46be2aad8656d67f0de7389ad5bd8303461a83 Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Thu, 10 Oct 2024 11:47:01 +0200 Subject: [PATCH 27/32] fix(query-rest): Add name option to Expose decorator in field decorator --- packages/query-rest/src/decorators/field.decorator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/query-rest/src/decorators/field.decorator.ts b/packages/query-rest/src/decorators/field.decorator.ts index c60ab558a..533b8d22c 100644 --- a/packages/query-rest/src/decorators/field.decorator.ts +++ b/packages/query-rest/src/decorators/field.decorator.ts @@ -85,7 +85,7 @@ export function Field( delete options.skipIsEnum const decorators = [ - Expose(), + Expose({ name: advancedOptions?.name }), ApiProperty({ type, isArray, From ee816fb926adcb5e4ad429348df4420354512b85 Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Thu, 10 Oct 2024 14:41:09 +0200 Subject: [PATCH 28/32] refactor(query-rest): Update unique name values for DTO classes to include "Args" suffix --- packages/query-rest/src/resolvers/create.resolver.ts | 2 +- packages/query-rest/src/resolvers/delete.resolver.ts | 2 +- packages/query-rest/src/resolvers/read.resolver.ts | 4 ++-- packages/query-rest/src/resolvers/update.resolver.ts | 2 +- packages/query-rest/src/types/create-one-input.type.ts | 2 +- packages/query-rest/src/types/update-one-input.type.ts | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/query-rest/src/resolvers/create.resolver.ts b/packages/query-rest/src/resolvers/create.resolver.ts index afb0069f8..19e5f0f07 100644 --- a/packages/query-rest/src/resolvers/create.resolver.ts +++ b/packages/query-rest/src/resolvers/create.resolver.ts @@ -32,7 +32,7 @@ const defaultCreateDTO = (dtoNames: DTONames, DTOClass: Class): Cla Object.defineProperty(DefaultCreateDTO, 'name', { writable: false, // set a unique name otherwise DI does not inject a unique one for each request - value: `Create${DTOClass.name}` + value: `Create${DTOClass.name}Args` }) return DefaultCreateDTO as Class diff --git a/packages/query-rest/src/resolvers/delete.resolver.ts b/packages/query-rest/src/resolvers/delete.resolver.ts index a66998af7..d460618f9 100644 --- a/packages/query-rest/src/resolvers/delete.resolver.ts +++ b/packages/query-rest/src/resolvers/delete.resolver.ts @@ -38,7 +38,7 @@ export const Deletable = Object.defineProperty(DOP, 'name', { writable: false, // set a unique name otherwise DI does not inject a unique one for each request - value: `${DTOClass.name}Params` + value: `FindDelete${DTOClass.name}Args` }) class DeleteResolverBase extends BaseClass { diff --git a/packages/query-rest/src/resolvers/read.resolver.ts b/packages/query-rest/src/resolvers/read.resolver.ts index 6c31846e8..aa046df9f 100644 --- a/packages/query-rest/src/resolvers/read.resolver.ts +++ b/packages/query-rest/src/resolvers/read.resolver.ts @@ -69,13 +69,13 @@ export const Readable = Object.defineProperty(QA, 'name', { writable: false, // set a unique name otherwise DI does not inject a unique one for each request - value: `${DTOClass.name}QueryArgs` + value: `Query${DTOClass.name}Args` }) Object.defineProperty(FOP, 'name', { writable: false, // set a unique name otherwise DI does not inject a unique one for each request - value: `${DTOClass.name}Params` + value: `Find${DTOClass.name}Args` }) class ReadResolverBase extends BaseClass { diff --git a/packages/query-rest/src/resolvers/update.resolver.ts b/packages/query-rest/src/resolvers/update.resolver.ts index dc776044e..4d1f4e53e 100644 --- a/packages/query-rest/src/resolvers/update.resolver.ts +++ b/packages/query-rest/src/resolvers/update.resolver.ts @@ -63,7 +63,7 @@ export const Updateable = Object.defineProperty(UOP, 'name', { writable: false, // set a unique name otherwise DI does not inject a unique one for each request - value: `${DTOClass.name}UpdateParams` + value: `FindUpdate${DTOClass.name}Args` }) class UpdateResolverBase extends BaseClass { diff --git a/packages/query-rest/src/types/create-one-input.type.ts b/packages/query-rest/src/types/create-one-input.type.ts index f340ad764..9c33e8b0e 100644 --- a/packages/query-rest/src/types/create-one-input.type.ts +++ b/packages/query-rest/src/types/create-one-input.type.ts @@ -23,7 +23,7 @@ export function CreateOneInputType(DTOClass: Class, InputClass: Cla Object.defineProperty(InputClass, 'name', { writable: false, // set a unique name otherwise DI does not inject a unique one for each request - value: `Create${DTOClass.name}` + value: `Create${DTOClass.name}Args` }) return CreateOneInput diff --git a/packages/query-rest/src/types/update-one-input.type.ts b/packages/query-rest/src/types/update-one-input.type.ts index c4938e6f8..80117b34b 100644 --- a/packages/query-rest/src/types/update-one-input.type.ts +++ b/packages/query-rest/src/types/update-one-input.type.ts @@ -23,7 +23,7 @@ export function UpdateOneInputType(DTOClass: Class, UpdateClass: Cl Object.defineProperty(UpdateOneInput, 'name', { writable: false, // set a unique name otherwise DI does not inject a unique one for each request - value: `Update${DTOClass.name}` + value: `Update${DTOClass.name}Args` }) return UpdateOneInput From 84a6b2ed859a0b98dbc900e3db434203cb380b0a Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Thu, 10 Oct 2024 14:41:53 +0200 Subject: [PATCH 29/32] chore: Updated lockfile --- yarn.lock | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/yarn.lock b/yarn.lock index 8d3b36335..fb57e44c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5313,6 +5313,13 @@ __metadata: languageName: node linkType: hard +"@microsoft/tsdoc@npm:^0.15.0": + version: 0.15.0 + resolution: "@microsoft/tsdoc@npm:0.15.0" + checksum: 10/fd025e5e3966248cd5477b9ddad4e9aa0dd69291f372a207f18a686b3097dcf5ecf38325caf0f4ad2697f1f39fd45b536e4ada6756008b8bcc5eccbc3201313d + languageName: node + linkType: hard + "@mongodb-js/saslprep@npm:^1.1.0": version: 1.1.0 resolution: "@mongodb-js/saslprep@npm:1.1.0" @@ -5598,6 +5605,34 @@ __metadata: languageName: node linkType: hard +"@nestjs/swagger@npm:^7.1.15": + version: 7.4.2 + resolution: "@nestjs/swagger@npm:7.4.2" + dependencies: + "@microsoft/tsdoc": "npm:^0.15.0" + "@nestjs/mapped-types": "npm:2.0.5" + js-yaml: "npm:4.1.0" + lodash: "npm:4.17.21" + path-to-regexp: "npm:3.3.0" + swagger-ui-dist: "npm:5.17.14" + peerDependencies: + "@fastify/static": ^6.0.0 || ^7.0.0 + "@nestjs/common": ^9.0.0 || ^10.0.0 + "@nestjs/core": ^9.0.0 || ^10.0.0 + class-transformer: "*" + class-validator: "*" + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + "@fastify/static": + optional: true + class-transformer: + optional: true + class-validator: + optional: true + checksum: 10/e3f9cac6a092442461fe7e4edd45b8af3377a02c626bb2b9f7da2e0ffe4999c3c20f32240aa1a67e9999a919946e6ed6c0da01c703e7d83d49823f1d7b9caf09 + languageName: node + linkType: hard + "@nestjs/testing@npm:^10.4.1": version: 10.4.1 resolution: "@nestjs/testing@npm:10.4.1" @@ -6214,6 +6249,26 @@ __metadata: languageName: unknown linkType: soft +"@ptc-org/nestjs-query-rest@workspace:packages/query-rest": + version: 0.0.0-use.local + resolution: "@ptc-org/nestjs-query-rest@workspace:packages/query-rest" + dependencies: + lodash.omit: "npm:^4.5.0" + lower-case-first: "npm:^2.0.2" + pluralize: "npm:^8.0.0" + tslib: "npm:^2.6.2" + upper-case-first: "npm:^2.0.2" + peerDependencies: + "@nestjs/common": ^9.0.0 || ^10.0.0 + "@nestjs/core": ^9.0.0 || ^10.0.0 + "@nestjs/graphql": ^11.0.0 || ^12.0.0 + "@nestjs/swagger": ^7.0.0 + class-transformer: ^0.5 + class-validator: ^0.14.0 + ts-morph: ^19.0.0 + languageName: unknown + linkType: soft + "@ptc-org/nestjs-query-sequelize@workspace:packages/query-sequelize": version: 0.0.0-use.local resolution: "@ptc-org/nestjs-query-sequelize@workspace:packages/query-sequelize" @@ -18055,6 +18110,7 @@ __metadata: "@nestjs/platform-express": "npm:10.4.1" "@nestjs/schematics": "npm:10.1.4" "@nestjs/sequelize": "npm:10.0.1" + "@nestjs/swagger": "npm:^7.1.15" "@nestjs/testing": "npm:^10.4.1" "@nestjs/typeorm": "npm:^10.0.2" "@nx-extend/docusaurus": "npm:^2.0.1" @@ -19096,6 +19152,13 @@ __metadata: languageName: node linkType: hard +"path-to-regexp@npm:3.3.0": + version: 3.3.0 + resolution: "path-to-regexp@npm:3.3.0" + checksum: 10/8d256383af8db66233ee9027cfcbf8f5a68155efbb4f55e784279d3ab206dcaee554ddb72ff0dae97dd2882af9f7fa802634bb7cffa2e796927977e31b829259 + languageName: node + linkType: hard + "path-to-regexp@npm:^1.7.0": version: 1.8.0 resolution: "path-to-regexp@npm:1.8.0" @@ -22186,6 +22249,13 @@ __metadata: languageName: node linkType: hard +"swagger-ui-dist@npm:5.17.14": + version: 5.17.14 + resolution: "swagger-ui-dist@npm:5.17.14" + checksum: 10/b9e62d7ecb64e837849252c9f82af654b26cae60ebd551cff96495d826166d3ed866ebae40f22a2c61d307330151945d79d995e50659ae17eea6cf4ece788f9d + languageName: node + linkType: hard + "symbol-observable@npm:4.0.0, symbol-observable@npm:^4.0.0": version: 4.0.0 resolution: "symbol-observable@npm:4.0.0" From d3cfb9f9607236c2af19b66394916807b6781086 Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Thu, 10 Oct 2024 15:47:35 +0200 Subject: [PATCH 30/32] refactor(query-rest): Simplify usage of DTOClass parameters and add ApiSchema decorator --- .../src/decorators/api-schema.decorator.ts | 21 +++++++++++ packages/query-rest/src/decorators/index.ts | 1 + .../src/resolvers/create.resolver.ts | 21 ++++++----- .../src/resolvers/update.resolver.ts | 35 ++++++++----------- .../src/types/create-one-input.type.ts | 8 +---- .../src/types/update-one-input.type.ts | 8 +---- 6 files changed, 49 insertions(+), 45 deletions(-) create mode 100644 packages/query-rest/src/decorators/api-schema.decorator.ts diff --git a/packages/query-rest/src/decorators/api-schema.decorator.ts b/packages/query-rest/src/decorators/api-schema.decorator.ts new file mode 100644 index 000000000..098e72acb --- /dev/null +++ b/packages/query-rest/src/decorators/api-schema.decorator.ts @@ -0,0 +1,21 @@ +import type { Class } from '@ptc-org/nestjs-query-core' + +interface ApiSchemaOptions { + name?: string +} + +export function ApiSchema(options?: ApiSchemaOptions) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (constructor: Class) => { + const wrapper = class extends constructor {} + + if (options?.name) { + Object.defineProperty(wrapper, 'name', { + value: options.name, + writable: false + }) + } + + return wrapper + } +} diff --git a/packages/query-rest/src/decorators/index.ts b/packages/query-rest/src/decorators/index.ts index bbd1d9172..08b7aa578 100644 --- a/packages/query-rest/src/decorators/index.ts +++ b/packages/query-rest/src/decorators/index.ts @@ -1,3 +1,4 @@ +export * from './api-schema.decorator' export * from './authorize-filter.decorator' export * from './authorizer.decorator' export * from './controller-methods.decorator' diff --git a/packages/query-rest/src/resolvers/create.resolver.ts b/packages/query-rest/src/resolvers/create.resolver.ts index 19e5f0f07..02c81b867 100644 --- a/packages/query-rest/src/resolvers/create.resolver.ts +++ b/packages/query-rest/src/resolvers/create.resolver.ts @@ -4,7 +4,7 @@ import { Class, DeepPartial, Filter, QueryService } from '@ptc-org/nestjs-query- import omit from 'lodash.omit' import { DTONames, getDTONames } from '../common' -import { BodyHookArgs, Post } from '../decorators' +import { ApiSchema, BodyHookArgs, Post } from '../decorators' import { HookTypes } from '../hooks' import { AuthorizerInterceptor, HookInterceptor } from '../interceptors' import { CreateOneInputType, MutationArgsType } from '../types' @@ -27,20 +27,19 @@ export interface CreateResolver /** @internal */ const defaultCreateDTO = (dtoNames: DTONames, DTOClass: Class): Class => { - const DefaultCreateDTO = OmitType(DTOClass, []) - - Object.defineProperty(DefaultCreateDTO, 'name', { - writable: false, - // set a unique name otherwise DI does not inject a unique one for each request - value: `Create${DTOClass.name}Args` - }) + @ApiSchema({ name: `Create${dtoNames.baseName}` }) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + class DefaultCreateDTO extends OmitType(DTOClass, []) {} return DefaultCreateDTO as Class } /** @internal */ -const defaultCreateOneInput = (DTOClass: Class, InputDTO: Class): Class> => { - return CreateOneInputType(DTOClass, InputDTO) +const defaultCreateOneInput = (dtoNames: DTONames, InputDTO: Class): Class> => { + class CO extends CreateOneInputType(InputDTO) {} + + return CO } /** @@ -58,7 +57,7 @@ export const Creatable = const { CreateDTOClass = defaultCreateDTO(dtoNames, DTOClass), - CreateOneInput = defaultCreateOneInput(DTOClass, CreateDTOClass) + CreateOneInput = defaultCreateOneInput(dtoNames, CreateDTOClass) } = opts const commonResolverOpts = omit(opts, 'dtoName', 'one', 'many', 'CreateDTOClass', 'CreateOneInput', 'CreateManyInput') diff --git a/packages/query-rest/src/resolvers/update.resolver.ts b/packages/query-rest/src/resolvers/update.resolver.ts index 4d1f4e53e..7197827c9 100644 --- a/packages/query-rest/src/resolvers/update.resolver.ts +++ b/packages/query-rest/src/resolvers/update.resolver.ts @@ -5,8 +5,8 @@ import { Class, DeepPartial, Filter, QueryService } from '@ptc-org/nestjs-query- import omit from 'lodash.omit' import { OperationGroup } from '../auth' -import { getDTONames } from '../common' -import { AuthorizerFilter, BodyHookArgs, Put } from '../decorators' +import { DTONames, getDTONames } from '../common' +import { ApiSchema, AuthorizerFilter, BodyHookArgs, Put } from '../decorators' import { HookTypes } from '../hooks' import { AuthorizerInterceptor, HookInterceptor } from '../interceptors' import { MutationArgsType, UpdateOneInputType } from '../types' @@ -23,20 +23,17 @@ export interface UpdateResolver } /** @internal */ -const defaultUpdateDTO = (DTOClass: Class): Class => { - const DefaultUpdateDTO = PartialType(DTOClass) as Class +const defaultUpdateDTO = (dtoNames: DTONames, DTOClass: Class): Class => { + @ApiSchema({ name: `Update${dtoNames.baseName}` }) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + class DefaultUpdateDTO extends PartialType(DTOClass) {} - Object.defineProperty(DefaultUpdateDTO, 'name', { - writable: false, - // set a unique name otherwise DI does not inject a unique one for each request - value: `Update${DTOClass.name}` - }) - - return DefaultUpdateDTO + return DefaultUpdateDTO as Class } -const defaultUpdateOneInput = (DTOClass: Class, UpdateDTO: Class): Class> => { - return UpdateOneInputType(DTOClass, UpdateDTO) +const defaultUpdateOneInput = (dtoNames: DTONames, UpdateDTO: Class): Class> => { + return UpdateOneInputType(UpdateDTO) } /** @@ -52,20 +49,18 @@ export const Updateable = const dtoNames = getDTONames(DTOClass, opts) - const { UpdateDTOClass = defaultUpdateDTO(DTOClass), UpdateOneInput = defaultUpdateOneInput(DTOClass, UpdateDTOClass) } = opts + const { + UpdateDTOClass = defaultUpdateDTO(dtoNames, DTOClass), + UpdateOneInput = defaultUpdateOneInput(dtoNames, UpdateDTOClass) + } = opts const commonResolverOpts = omit(opts, 'dtoName', 'one', 'many', 'UpdateDTOClass', 'UpdateOneInput', 'UpdateManyInput') class UOI extends MutationArgsType(UpdateOneInput) {} + @ApiSchema({ name: `FindUpdate${DTOClass.name}Args` }) class UOP extends FindOneArgsType(DTOClass) {} - Object.defineProperty(UOP, 'name', { - writable: false, - // set a unique name otherwise DI does not inject a unique one for each request - value: `FindUpdate${DTOClass.name}Args` - }) - class UpdateResolverBase extends BaseClass { @Put( () => DTOClass, diff --git a/packages/query-rest/src/types/create-one-input.type.ts b/packages/query-rest/src/types/create-one-input.type.ts index 9c33e8b0e..cc6d914a0 100644 --- a/packages/query-rest/src/types/create-one-input.type.ts +++ b/packages/query-rest/src/types/create-one-input.type.ts @@ -11,7 +11,7 @@ export interface CreateOneInputType { * @param InputClass - The InputType to be used. */ // eslint-disable-next-line @typescript-eslint/no-redeclare -- intentional -export function CreateOneInputType(DTOClass: Class, InputClass: Class): Class> { +export function CreateOneInputType(InputClass: Class): Class> { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore class CreateOneInput extends InputClass implements CreateOneInputType { @@ -20,11 +20,5 @@ export function CreateOneInputType(DTOClass: Class, InputClass: Cla } } - Object.defineProperty(InputClass, 'name', { - writable: false, - // set a unique name otherwise DI does not inject a unique one for each request - value: `Create${DTOClass.name}Args` - }) - return CreateOneInput } diff --git a/packages/query-rest/src/types/update-one-input.type.ts b/packages/query-rest/src/types/update-one-input.type.ts index 80117b34b..cc675fa46 100644 --- a/packages/query-rest/src/types/update-one-input.type.ts +++ b/packages/query-rest/src/types/update-one-input.type.ts @@ -11,7 +11,7 @@ export interface UpdateOneInputType { * @param UpdateClass - The InputType to be used. */ // eslint-disable-next-line @typescript-eslint/no-redeclare -- intentional -export function UpdateOneInputType(DTOClass: Class, UpdateClass: Class): Class> { +export function UpdateOneInputType(UpdateClass: Class): Class> { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore class UpdateOneInput extends UpdateClass implements UpdateOneInputType { @@ -20,11 +20,5 @@ export function UpdateOneInputType(DTOClass: Class, UpdateClass: Cl } } - Object.defineProperty(UpdateOneInput, 'name', { - writable: false, - // set a unique name otherwise DI does not inject a unique one for each request - value: `Update${DTOClass.name}Args` - }) - return UpdateOneInput } From 1a33964dea21f1560c75a7da0f20b964f1a278f8 Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Mon, 14 Oct 2024 16:04:25 +0200 Subject: [PATCH 31/32] fix(nestjs-rest): Fix decorator metadata retrieval and class naming - Correct `FindOneArgsType` parameter in `read.resolver.ts` - Enhance `field.decorator.ts` to properly retrieve metadata - Add `IsNumber` and `IsString` validators for specific types - Improve handling of array options and validation --- .../src/decorators/field.decorator.ts | 124 ++++++++++-------- .../query-rest/src/resolvers/read.resolver.ts | 2 +- 2 files changed, 70 insertions(+), 56 deletions(-) diff --git a/packages/query-rest/src/decorators/field.decorator.ts b/packages/query-rest/src/decorators/field.decorator.ts index 533b8d22c..c92bc1158 100644 --- a/packages/query-rest/src/decorators/field.decorator.ts +++ b/packages/query-rest/src/decorators/field.decorator.ts @@ -5,8 +5,10 @@ import { ArrayMaxSize, IsEnum, IsNotEmpty, + IsNumber, IsObject, IsOptional, + IsString, Max, MaxLength, Min, @@ -70,74 +72,86 @@ export function Field( advancedOptions = maybeOptions } - const returnedType = returnTypeFunc?.() - const isArray = returnedType && Array.isArray(returnedType) - const type = isArray ? returnedType[0] : returnedType + return (target: object, propertyKey: string, descriptor: TypedPropertyDescriptor) => { + const returnedType = !returnTypeFunc + ? (target?.constructor?.[METADATA_FACTORY_NAME]?.()[propertyKey]?.type ?? + Reflect.getMetadata('design:type', target, propertyKey)) + : returnTypeFunc() - const options = { - required: !advancedOptions?.nullable && advancedOptions?.default === undefined, - example: advancedOptions?.default, - ...advancedOptions - } + const isArray = returnedType && Array.isArray(returnedType) + const type = isArray ? returnedType[0] : returnedType - // Remove non-valid options - delete options.forceArray - delete options.skipIsEnum - - const decorators = [ - Expose({ name: advancedOptions?.name }), - ApiProperty({ - type, - isArray, - ...options - }) - ] - - if (isArray && options.maxItems !== undefined) { - decorators.push(ArrayMaxSize(options.maxItems)) - } + const options = { + required: !advancedOptions?.nullable && advancedOptions?.default === undefined, + example: advancedOptions?.default, + ...advancedOptions + } - if (isArray && advancedOptions?.forceArray) { - decorators.push(Transform(({ value }) => (Array.isArray(value) ? value : [value]))) - } + // Remove non-valid options + delete options.forceArray + delete options.skipIsEnum + + const decorators = [ + Expose({ name: advancedOptions?.name }), + ApiProperty({ + type, + isArray, + ...options + }) + ] + + if (isArray && options.maxItems !== undefined) { + decorators.push(ArrayMaxSize(options.maxItems)) + } - if (options.minLength) { - decorators.push(MinLength(options.minLength)) - } + if (isArray && advancedOptions?.forceArray) { + decorators.push(Transform(({ value }) => (Array.isArray(value) ? value : [value]))) + } - if (options.maxLength) { - decorators.push(MaxLength(options.maxLength)) - } + if (options.minLength) { + decorators.push(MinLength(options.minLength)) + } - if (options.minimum !== undefined) { - decorators.push(Min(options.minimum)) - } + if (options.maxLength) { + decorators.push(MaxLength(options.maxLength)) + } - if (options.maximum !== undefined) { - decorators.push(Max(options.maximum)) - } + if (options.minimum !== undefined) { + decorators.push(Min(options.minimum)) + } - if (options.required) { - decorators.push(IsNotEmpty()) - } else { - decorators.push(IsOptional()) - } + if (options.maximum !== undefined) { + decorators.push(Max(options.maximum)) + } - if (type) { - decorators.push(Type(() => type as never)) + if (options.required) { + decorators.push(IsNotEmpty()) + } else { + decorators.push(IsOptional()) + } - if (typeof type === 'function') { - decorators.push(ValidateNested()) + if (type) { + decorators.push(Type(() => type as never)) - if (!isArray) { - decorators.push(IsObject()) + if (type === String) { + decorators.push(IsString()) + } else if (type === Number) { + decorators.push(IsNumber()) + } + + if (returnTypeFunc && typeof type === 'function') { + decorators.push(ValidateNested()) + + if (!isArray) { + decorators.push(IsObject()) + } } } - } - if (options.enum && !advancedOptions?.skipIsEnum) { - decorators.push(IsEnum(options.enum)) - } + if (options.enum && !advancedOptions?.skipIsEnum) { + decorators.push(IsEnum(options.enum)) + } - return applyDecorators(...decorators) + return applyDecorators(...decorators)(target, propertyKey, descriptor) + } } diff --git a/packages/query-rest/src/resolvers/read.resolver.ts b/packages/query-rest/src/resolvers/read.resolver.ts index aa046df9f..e788bb16a 100644 --- a/packages/query-rest/src/resolvers/read.resolver.ts +++ b/packages/query-rest/src/resolvers/read.resolver.ts @@ -64,7 +64,7 @@ export const Readable = class QA extends QueryArgs {} - class FOP extends FindOneArgsType(DTOClass) {} + class FOP extends FindOneArgsType(FindDTOClass) {} Object.defineProperty(QA, 'name', { writable: false, From e2353ec725e87ff392b3b91eb9c5bdd95b282df3 Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Mon, 14 Oct 2024 16:09:01 +0200 Subject: [PATCH 32/32] feat(query-rest): Add `IsDate` validation to field decorator --- packages/query-rest/src/decorators/field.decorator.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/query-rest/src/decorators/field.decorator.ts b/packages/query-rest/src/decorators/field.decorator.ts index c92bc1158..68458b8f0 100644 --- a/packages/query-rest/src/decorators/field.decorator.ts +++ b/packages/query-rest/src/decorators/field.decorator.ts @@ -3,6 +3,7 @@ import { ApiProperty, ApiPropertyOptions } from '@nestjs/swagger' import { Expose, Transform, Type } from 'class-transformer' import { ArrayMaxSize, + IsDate, IsEnum, IsNotEmpty, IsNumber, @@ -137,6 +138,8 @@ export function Field( decorators.push(IsString()) } else if (type === Number) { decorators.push(IsNumber()) + } else if (type === Date) { + decorators.push(IsDate()) } if (returnTypeFunc && typeof type === 'function') {