diff --git a/.e2e-micro.env b/.e2e-micro.env new file mode 100644 index 00000000..866e5ea8 --- /dev/null +++ b/.e2e-micro.env @@ -0,0 +1,2 @@ +DB_LOGGING=0 +ORM_TYPE=microorm diff --git a/.e2e.env b/.e2e.env index f48c488a..eda3206d 100644 --- a/.e2e.env +++ b/.e2e.env @@ -1 +1,2 @@ DB_LOGGING=0 +ORM_TYPE=typeorm diff --git a/.env b/.env index eaa22638..4a927090 100644 --- a/.env +++ b/.env @@ -3,7 +3,8 @@ DB_LOGGING=1 DB_USERNAME="postgres" DB_PASSWORD="postgres" -DB_NAME="json-api-db" +#DB_NAME="json-api-db" +DB_NAME="postgres" DB_PORT=5432 DB_TYPE=postgres @@ -12,3 +13,7 @@ DB_TYPE=postgres #DB_NAME="example_new" #DB_PORT=3306 #DB_TYPE=mysql + + +ORM_TYPE=microorm +#ORM_TYPE=typeorm diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c3542bb..0820cc30 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,11 +30,11 @@ jobs: - name: Test env: NX_REJECT_UNKNOWN_LOCAL_CACHE: 0 - run: npx nx affected -t test --parallel=3 --exclude='json-api-front,json-api-server,json-api-server-e2e,json-shared-type,database,@nestjs-json-api/source,type-for-rpc' + run: npx nx affected -t test --parallel=3 --exclude='json-api-front,json-api-server,json-api-server-e2e,json-shared-type,microorm-database,typeorm-database,@nestjs-json-api/source,type-for-rpc' - name: Build env: NX_REJECT_UNKNOWN_LOCAL_CACHE: 0 - run: npx nx affected -t build --exclude='json-api-front,json-api-server,json-api-server-e2e,json-shared-type,database,@nestjs-json-api/source,type-for-rpc' + run: npx nx affected -t build --exclude='json-api-front,json-api-server,json-api-server-e2e,json-shared-type,microorm-database,typeorm-database,@nestjs-json-api/source,type-for-rpc' - name: Save cached .nx id: cache-dependencies-save uses: actions/cache/save@v4 @@ -85,6 +85,7 @@ jobs: - run: npm run typeorm migration:run - run: npm run seed:run - run: npx nx affected -t e2e --parallel=1 + - run: npx nx affected -t e2e-micro --parallel=1 - name: Save cached .nx id: cache-dependencies-save uses: actions/cache/save@v4 diff --git a/.test.env b/.test.env new file mode 100644 index 00000000..4f233b1b --- /dev/null +++ b/.test.env @@ -0,0 +1,2 @@ +NODE_OPTIONS=--experimental-vm-modules --disable-warning=ExperimentalWarning +DB_LOGGING=0 diff --git a/.verdaccio/config.yml b/.verdaccio/config.yml new file mode 100644 index 00000000..f74420f2 --- /dev/null +++ b/.verdaccio/config.yml @@ -0,0 +1,28 @@ +# path to a directory with all packages +storage: ../tmp/local-registry/storage + +# a list of other known repositories we can talk to +uplinks: + npmjs: + url: https://registry.npmjs.org/ + maxage: 60m + +packages: + '**': + # give all users (including non-authenticated users) full access + # because it is a local registry + access: $all + publish: $all + unpublish: $all + + # if package is not available locally, proxy requests to npm registry + proxy: npmjs + +# log settings +log: + type: stdout + format: pretty + level: warn + +publish: + allow_offline: true # set offline to true to allow publish offline diff --git a/README.md b/README.md index 219146a1..b268dff2 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,24 @@

- Json API plugins for - NestJS - framework +NestJS JSON API & JSON RPC Suite

+

- Tools to implement JSON API, such as, end point, query params, body params, validation and transformation response. + This monorepo contains a set of several libraries designed to simplify the development of server and client applications using NestJS. These tools help you work with two popular protocols:

-- *[json-api-nestjs](https://github.com/klerick/nestjs-json-api/tree/master/libs/json-api/json-api-nestjs)* - plugin for create CRUD overs JSON API -- *[json-api-nestjs-sdk](https://github.com/klerick/nestjs-json-api/tree/master/libs/json-api/json-api-nestjs-sdk)* - tool for client, call api over *json-api-nestjs* -- *[nestjs-json-rpc](https://github.com/klerick/nestjs-json-api/tree/master/libs/json-rpc/nestjs-json-rpc)* - plugin for create RPC server using [JSON-RPC](https://www.jsonrpc.org/) -- *[nestjs-json-rpc-sdk](https://github.com/klerick/nestjs-json-api/tree/master/libs/json-rpc/nestjs-json-rpc-sdk)* - tool for client, call RPC server *nestjs-json-rpc* -- *json-api-nestjs-acl* - tool for acl over *json-api-nestjs*(coming soon...) + +- **[JSON:API](https://jsonapi.org/)** – A specification for building RESTful APIs with standardized request and response formats. + + > **[json-api-nestjs](https://github.com/klerick/nestjs-json-api/tree/master/libs/json-api/json-api-nestjs)** - This package enables you to quickly set up a server API that adheres to the JSON:API specification, handling standard CRUD operations for your resources.
+ > **[json-api-nestjs-sdk](https://github.com/klerick/nestjs-json-api/tree/master/libs/json-api/json-api-nestjs-sdk)** - tool for client, call api over *json-api-nestjs* + + +- **[JSON-RPC](https://www.jsonrpc.org/)** – A protocol for remote procedure calls using JSON. + +> **[nestjs-json-rpc](https://github.com/klerick/nestjs-json-api/tree/master/libs/json-rpc/nestjs-json-rpc)** - Use this package to implement remote procedure call (RPC) functionality in your NestJS applications, enabling efficient inter-service communication.
+> **[nestjs-json-rpc-sdk](https://github.com/klerick/nestjs-json-api/tree/master/libs/json-rpc/nestjs-json-rpc-sdk)** - This tool offers a straightforward way to call remote procedures from your client-side code, ensuring smooth communication with your JSON-RPC server. + +- **ACL tools** - tool for acl over *json-api-nestjs*(coming soon...) ## Installation ```bash @@ -24,7 +31,7 @@ $ npm run seed:run ```bash # dev server -$ npm run demo:json-api +$ nx run json-api-server:serve:development ``` ## License diff --git a/apps/json-api-server-e2e/project.json b/apps/json-api-server-e2e/project.json index 94445c8f..4c159112 100644 --- a/apps/json-api-server-e2e/project.json +++ b/apps/json-api-server-e2e/project.json @@ -16,6 +16,17 @@ "passWithNoTests": true, "parallel": 1 } + }, + "e2e-micro": { + "executor": "@nx/jest:jest", + "outputs": [ + "{workspaceRoot}/coverage/{e2eProjectRoot}" + ], + "options": { + "jestConfig": "apps/json-api-server-e2e/jest.config.ts", + "passWithNoTests": true, + "parallel": 1 + } } } } diff --git a/apps/json-api-server-e2e/src/json-api/json-api-sdk/atomic-sdk.spec.ts b/apps/json-api-server-e2e/src/json-api/json-api-sdk/atomic-sdk.spec.ts index bb5fe060..b89a48c5 100644 --- a/apps/json-api-server-e2e/src/json-api/json-api-sdk/atomic-sdk.spec.ts +++ b/apps/json-api-server-e2e/src/json-api/json-api-sdk/atomic-sdk.spec.ts @@ -1,6 +1,12 @@ import { INestApplication } from '@nestjs/common'; -import { FilterOperand, JsonSdkPromise } from 'json-api-nestjs-sdk'; -import { Addresses, CommentKind, Comments, Roles, Users } from 'database'; +import { FilterOperand, JsonSdkPromise } from '@klerick/json-api-nestjs-sdk'; +import { + Addresses, + CommentKind, + Comments, + Roles, + Users, +} from '@nestjs-json-api/typeorm-database'; import { faker } from '@faker-js/faker'; import { getUser } from '../utils/data-utils'; import { run, creatSdk } from '../utils/run-application'; diff --git a/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-common-decorator.spec.ts b/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-common-decorator.spec.ts index 47b18d9f..969858db 100644 --- a/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-common-decorator.spec.ts +++ b/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-common-decorator.spec.ts @@ -1,7 +1,7 @@ import { INestApplication } from '@nestjs/common'; -import { FilterOperand, JsonSdkPromise } from 'json-api-nestjs-sdk'; +import { FilterOperand, JsonSdkPromise } from '@klerick/json-api-nestjs-sdk'; import { AxiosError } from 'axios'; -import { Users } from 'database'; +import { Users } from '@nestjs-json-api/typeorm-database'; import { run, creatSdk } from '../utils/run-application'; diff --git a/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-othe-call.spec.ts b/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-othe-call.spec.ts index 076dc277..d6d2957a 100644 --- a/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-othe-call.spec.ts +++ b/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-othe-call.spec.ts @@ -1,6 +1,6 @@ import { INestApplication } from '@nestjs/common'; -import { FilterOperand, JsonSdkPromise } from 'json-api-nestjs-sdk'; -import { BookList, Users } from 'database'; +import { FilterOperand, JsonSdkPromise } from '@klerick/json-api-nestjs-sdk'; +import { BookList, Users } from '@nestjs-json-api/typeorm-database'; import { AxiosError } from 'axios'; import { faker } from '@faker-js/faker'; import { lastValueFrom } from 'rxjs'; diff --git a/apps/json-api-server-e2e/src/json-api/json-api-sdk/get-method.spec.ts b/apps/json-api-server-e2e/src/json-api/json-api-sdk/get-method.spec.ts index 04889c41..50bbd940 100644 --- a/apps/json-api-server-e2e/src/json-api/json-api-sdk/get-method.spec.ts +++ b/apps/json-api-server-e2e/src/json-api/json-api-sdk/get-method.spec.ts @@ -1,11 +1,16 @@ import { INestApplication } from '@nestjs/common'; -import { Addresses, CommentKind, Comments, Roles, Users } from 'database'; +import { + Addresses, + CommentKind, + Comments, + Roles, + Users, +} from '@nestjs-json-api/typeorm-database'; import { faker } from '@faker-js/faker'; -import { FilterOperand, JsonSdkPromise } from 'json-api-nestjs-sdk'; +import { FilterOperand, JsonSdkPromise } from '@klerick/json-api-nestjs-sdk'; import { getUser } from '../utils/data-utils'; import { creatSdk, run } from '../utils/run-application'; -import { AxiosError } from 'axios'; let app: INestApplication; @@ -91,10 +96,10 @@ describe('GET method:', () => { return Promise.all(tmp); }) ); - await Promise.all(addressArray); + await Promise.all( - [...usersArray, ...commentsArray, ...rolesArray].map((i) => - jsonSdk.jonApiSdkService.deleteOne(i) + [...usersArray, ...commentsArray, ...rolesArray, ...addressArray].map( + (i) => jsonSdk.jonApiSdkService.deleteOne(i) ) ); }); @@ -334,6 +339,7 @@ describe('GET method:', () => { userItem.id, { include: ['addresses'] } ); + expect(result).toBe(`${resultGetOne.addresses.id}`); }); diff --git a/apps/json-api-server-e2e/src/json-api/json-api-sdk/patch-methode.spec.ts b/apps/json-api-server-e2e/src/json-api/json-api-sdk/patch-methode.spec.ts index 62deb265..09289a0f 100644 --- a/apps/json-api-server-e2e/src/json-api/json-api-sdk/patch-methode.spec.ts +++ b/apps/json-api-server-e2e/src/json-api/json-api-sdk/patch-methode.spec.ts @@ -1,7 +1,12 @@ import { INestApplication } from '@nestjs/common'; -import { Addresses, CommentKind, Comments, Users } from 'database'; +import { + Addresses, + CommentKind, + Comments, + Users, +} from '@nestjs-json-api/typeorm-database'; import { faker } from '@faker-js/faker'; -import { JsonSdkPromise } from 'json-api-nestjs-sdk'; +import { JsonSdkPromise } from '@klerick/json-api-nestjs-sdk'; import { creatSdk, run } from '../utils/run-application'; diff --git a/apps/json-api-server-e2e/src/json-api/json-api-sdk/post-method.spec.ts b/apps/json-api-server-e2e/src/json-api/json-api-sdk/post-method.spec.ts index ebc9b484..226947cf 100644 --- a/apps/json-api-server-e2e/src/json-api/json-api-sdk/post-method.spec.ts +++ b/apps/json-api-server-e2e/src/json-api/json-api-sdk/post-method.spec.ts @@ -1,6 +1,12 @@ -import { Addresses, BookList, CommentKind, Comments, Users } from 'database'; +import { + Addresses, + BookList, + CommentKind, + Comments, + Users, +} from '@nestjs-json-api/typeorm-database'; import { faker } from '@faker-js/faker'; -import { JsonSdkPromise } from 'json-api-nestjs-sdk'; +import { JsonSdkPromise } from '@klerick/json-api-nestjs-sdk'; import { creatSdk, run } from '../utils/run-application'; import { INestApplication } from '@nestjs/common'; diff --git a/apps/json-api-server-e2e/src/json-api/utils/data-utils.ts b/apps/json-api-server-e2e/src/json-api/utils/data-utils.ts index 89bc0937..7e4cef3b 100644 --- a/apps/json-api-server-e2e/src/json-api/utils/data-utils.ts +++ b/apps/json-api-server-e2e/src/json-api/utils/data-utils.ts @@ -1,4 +1,4 @@ -import { Users } from 'database'; +import { Users } from '@nestjs-json-api/typeorm-database'; import { faker } from '@faker-js/faker'; export const getUser = () => { diff --git a/apps/json-api-server-e2e/src/json-api/utils/run-application.ts b/apps/json-api-server-e2e/src/json-api/utils/run-application.ts index b4e57aca..74698971 100644 --- a/apps/json-api-server-e2e/src/json-api/utils/run-application.ts +++ b/apps/json-api-server-e2e/src/json-api/utils/run-application.ts @@ -1,6 +1,6 @@ import { Test } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; -import { adapterForAxios, JsonApiJs } from 'json-api-nestjs-sdk'; +import { adapterForAxios, JsonApiJs } from '@klerick/json-api-nestjs-sdk'; import { RpcFactory, axiosTransportFactory, diff --git a/apps/json-api-server/src/app/app.module.ts b/apps/json-api-server/src/app/app.module.ts index 45403244..28b47579 100644 --- a/apps/json-api-server/src/app/app.module.ts +++ b/apps/json-api-server/src/app/app.module.ts @@ -1,15 +1,27 @@ import { Module } from '@nestjs/common'; import { LoggerModule } from 'nestjs-pino'; -import { DatabaseModule } from 'database'; -import { ResourcesModule } from './resources/resources.module'; +import { TypeOrmDatabaseModule } from '@nestjs-json-api/typeorm-database'; +import { MicroOrmDatabaseModule } from '@nestjs-json-api/microorm-database'; +import { ResourcesTypeModule } from './resources/type-orm/resources-type.module'; +import { ResourcesMicroModule } from './resources/micro-orm/resources-micro.module'; import { RpcModule } from './rpc/rpc.module'; import * as process from 'process'; +const ormModule = + process.env['ORM_TYPE'] === 'typeorm' + ? TypeOrmDatabaseModule + : MicroOrmDatabaseModule; + +const resourceModule = + process.env['ORM_TYPE'] === 'typeorm' + ? ResourcesTypeModule + : ResourcesMicroModule; + @Module({ imports: [ - DatabaseModule, - ResourcesModule, + ormModule, + resourceModule, RpcModule, LoggerModule.forRoot({ pinoHttp: { diff --git a/apps/json-api-server/src/app/resources/micro-orm/controllers/extend-book-list/extend-book-list.controller.ts b/apps/json-api-server/src/app/resources/micro-orm/controllers/extend-book-list/extend-book-list.controller.ts new file mode 100644 index 00000000..ab49a56f --- /dev/null +++ b/apps/json-api-server/src/app/resources/micro-orm/controllers/extend-book-list/extend-book-list.controller.ts @@ -0,0 +1,10 @@ +import { ParseUUIDPipe } from '@nestjs/common'; +import { JsonApi, JsonBaseController } from '@klerick/json-api-nestjs'; + +import { BookList } from '@nestjs-json-api/microorm-database'; +@JsonApi(BookList, { + pipeForId: ParseUUIDPipe, + overrideRoute: 'override-book-list', + allowMethod: ['getOne', 'postOne', 'deleteOne'], +}) +export class ExtendBookListController extends JsonBaseController {} diff --git a/apps/json-api-server/src/app/resources/micro-orm/controllers/extend-user/extend-user.controller.ts b/apps/json-api-server/src/app/resources/micro-orm/controllers/extend-user/extend-user.controller.ts new file mode 100644 index 00000000..c2ba33f5 --- /dev/null +++ b/apps/json-api-server/src/app/resources/micro-orm/controllers/extend-user/extend-user.controller.ts @@ -0,0 +1,79 @@ +import { + Get, + Param, + Inject, + Query, + UseInterceptors, + UseFilters, + UseGuards, +} from '@nestjs/common'; + +import { + JsonApi, + JsonBaseController, + InjectService, + JsonApiService, + Query as QueryType, + QueryOne, + ResourceObject, + EntityRelation, + PatchRelationshipData, + ResourceObjectRelationships, + PostData, +} from '@klerick/json-api-nestjs'; +import { ExamplePipe } from '../../service/example.pipe'; +import { ExampleService } from '../../service/example.service'; +import { ControllerInterceptor } from '../../service/controller.interceptor'; +import { MethodInterceptor } from '../../service/method.interceptor'; +import { + HttpExceptionFilter, + HttpExceptionMethodFilter, +} from '../../service/http-exception.filter'; +import { GuardService, EntityName } from '../../service/guard.service'; + +import { Users } from '@nestjs-json-api/microorm-database'; +import { AtomicInterceptor } from '../../service/atomic.interceptor'; + +@UseGuards(GuardService) +@UseFilters(new HttpExceptionFilter()) +@UseInterceptors(ControllerInterceptor) +@JsonApi(Users) +export class ExtendUserController extends JsonBaseController { + @InjectService() public service: JsonApiService; + @Inject(ExampleService) protected exampleService: ExampleService; + override getOne( + id: string | number, + query: QueryOne + ): Promise> { + const t = query.fields?.target; + + return super.getOne(id, query); + } + + patchRelationship>( + id: string | number, + relName: Rel, + input: PatchRelationshipData + ): Promise> { + return super.patchRelationship(id, relName, input); + } + + // @UseInterceptors(AtomicInterceptor) + postOne(inputData: PostData): Promise> { + return super.postOne(inputData); + } + + @EntityName('Users') + @UseFilters(HttpExceptionMethodFilter) + @UseInterceptors(MethodInterceptor) + getAll( + @Query(ExamplePipe) query: QueryType + ): Promise> { + return super.getAll(query); + } + + @Get(':id/example') + testOne(@Param('id') id: string): string { + return this.exampleService.testMethode(id); + } +} diff --git a/apps/json-api-server/src/app/resources/micro-orm/resources-micro.module.ts b/apps/json-api-server/src/app/resources/micro-orm/resources-micro.module.ts new file mode 100644 index 00000000..88248808 --- /dev/null +++ b/apps/json-api-server/src/app/resources/micro-orm/resources-micro.module.ts @@ -0,0 +1,29 @@ +import { Module } from '@nestjs/common'; +import { JsonApiModule, MicroOrmJsonApiModule } from '@klerick/json-api-nestjs'; +import { + Users, + Addresses, + Comments, + Roles, + BookList, +} from '@nestjs-json-api/microorm-database'; + +import { ExtendBookListController } from './controllers/extend-book-list/extend-book-list.controller'; +import { ExtendUserController } from './controllers/extend-user/extend-user.controller'; +import { ExampleService } from './service/example.service'; + +@Module({ + imports: [ + JsonApiModule.forRoot(MicroOrmJsonApiModule, { + entities: [Users, Addresses, Comments, Roles, BookList], + controllers: [ExtendBookListController, ExtendUserController], + providers: [ExampleService], + options: { + debug: true, + requiredSelectField: false, + operationUrl: 'operation', + }, + }), + ], +}) +export class ResourcesMicroModule {} diff --git a/apps/json-api-server/src/app/resources/service/atomic.interceptor.ts b/apps/json-api-server/src/app/resources/micro-orm/service/atomic.interceptor.ts similarity index 100% rename from apps/json-api-server/src/app/resources/service/atomic.interceptor.ts rename to apps/json-api-server/src/app/resources/micro-orm/service/atomic.interceptor.ts diff --git a/apps/json-api-server/src/app/resources/service/controller.interceptor.ts b/apps/json-api-server/src/app/resources/micro-orm/service/controller.interceptor.ts similarity index 100% rename from apps/json-api-server/src/app/resources/service/controller.interceptor.ts rename to apps/json-api-server/src/app/resources/micro-orm/service/controller.interceptor.ts diff --git a/apps/json-api-server/src/app/resources/micro-orm/service/example.pipe.ts b/apps/json-api-server/src/app/resources/micro-orm/service/example.pipe.ts new file mode 100644 index 00000000..a7b49347 --- /dev/null +++ b/apps/json-api-server/src/app/resources/micro-orm/service/example.pipe.ts @@ -0,0 +1,22 @@ +import { + ArgumentMetadata, + BadRequestException, + PipeTransform, +} from '@nestjs/common'; + +import { Query } from '@klerick/json-api-nestjs'; +import { Users } from '@nestjs-json-api/microorm-database'; + +export class ExamplePipe implements PipeTransform, Query> { + transform(value: Query, metadata: ArgumentMetadata): Query { + if (value.filter.target?.firstName?.eq === 'testCustomPipe') { + const error = { + code: 'invalid_arguments', + message: `Custom query pipe error`, + path: [], + }; + throw new BadRequestException([error]); + } + return value; + } +} diff --git a/apps/json-api-server/src/app/resources/service/example.service.ts b/apps/json-api-server/src/app/resources/micro-orm/service/example.service.ts similarity index 100% rename from apps/json-api-server/src/app/resources/service/example.service.ts rename to apps/json-api-server/src/app/resources/micro-orm/service/example.service.ts diff --git a/apps/json-api-server/src/app/resources/service/guard.service.ts b/apps/json-api-server/src/app/resources/micro-orm/service/guard.service.ts similarity index 94% rename from apps/json-api-server/src/app/resources/service/guard.service.ts rename to apps/json-api-server/src/app/resources/micro-orm/service/guard.service.ts index 66714d6e..a8567bc2 100644 --- a/apps/json-api-server/src/app/resources/service/guard.service.ts +++ b/apps/json-api-server/src/app/resources/micro-orm/service/guard.service.ts @@ -6,7 +6,7 @@ import { Injectable, } from '@nestjs/common'; import { Observable } from 'rxjs'; -import { entityForClass } from 'json-api-nestjs'; +import { entityForClass } from '@klerick/json-api-nestjs'; import { Request } from 'express'; import { Reflector } from '@nestjs/core'; diff --git a/apps/json-api-server/src/app/resources/service/http-exception.filter.ts b/apps/json-api-server/src/app/resources/micro-orm/service/http-exception.filter.ts similarity index 100% rename from apps/json-api-server/src/app/resources/service/http-exception.filter.ts rename to apps/json-api-server/src/app/resources/micro-orm/service/http-exception.filter.ts diff --git a/apps/json-api-server/src/app/resources/service/method.interceptor.ts b/apps/json-api-server/src/app/resources/micro-orm/service/method.interceptor.ts similarity index 100% rename from apps/json-api-server/src/app/resources/service/method.interceptor.ts rename to apps/json-api-server/src/app/resources/micro-orm/service/method.interceptor.ts diff --git a/apps/json-api-server/src/app/resources/controllers/extend-book-list/extend-book-list.controller.ts b/apps/json-api-server/src/app/resources/type-orm/controllers/extend-book-list/extend-book-list.controller.ts similarity index 66% rename from apps/json-api-server/src/app/resources/controllers/extend-book-list/extend-book-list.controller.ts rename to apps/json-api-server/src/app/resources/type-orm/controllers/extend-book-list/extend-book-list.controller.ts index 971bba19..170dab5b 100644 --- a/apps/json-api-server/src/app/resources/controllers/extend-book-list/extend-book-list.controller.ts +++ b/apps/json-api-server/src/app/resources/type-orm/controllers/extend-book-list/extend-book-list.controller.ts @@ -1,7 +1,7 @@ import { ParseUUIDPipe } from '@nestjs/common'; -import { BookList } from 'database'; -import { JsonApi, JsonBaseController } from 'json-api-nestjs'; +import { JsonApi, JsonBaseController } from '@klerick/json-api-nestjs'; +import { BookList } from '@nestjs-json-api/typeorm-database'; @JsonApi(BookList, { pipeForId: ParseUUIDPipe, overrideRoute: 'override-book-list', diff --git a/apps/json-api-server/src/app/resources/controllers/extend-user/extend-user.controller.ts b/apps/json-api-server/src/app/resources/type-orm/controllers/extend-user/extend-user.controller.ts similarity index 92% rename from apps/json-api-server/src/app/resources/controllers/extend-user/extend-user.controller.ts rename to apps/json-api-server/src/app/resources/type-orm/controllers/extend-user/extend-user.controller.ts index 01acce2f..87b4f842 100644 --- a/apps/json-api-server/src/app/resources/controllers/extend-user/extend-user.controller.ts +++ b/apps/json-api-server/src/app/resources/type-orm/controllers/extend-user/extend-user.controller.ts @@ -7,19 +7,20 @@ import { UseFilters, UseGuards, } from '@nestjs/common'; -import { Users } from 'database'; + import { JsonApi, JsonBaseController, InjectService, JsonApiService, Query as QueryType, + QueryOne, ResourceObject, EntityRelation, PatchRelationshipData, ResourceObjectRelationships, PostData, -} from 'json-api-nestjs'; +} from '@klerick/json-api-nestjs'; import { ExamplePipe } from '../../service/example.pipe'; import { ExampleService } from '../../service/example.service'; import { ControllerInterceptor } from '../../service/controller.interceptor'; @@ -29,6 +30,8 @@ import { HttpExceptionMethodFilter, } from '../../service/http-exception.filter'; import { GuardService, EntityName } from '../../service/guard.service'; + +import { Users } from '@nestjs-json-api/typeorm-database'; import { AtomicInterceptor } from '../../service/atomic.interceptor'; @UseGuards(GuardService) @@ -38,9 +41,9 @@ import { AtomicInterceptor } from '../../service/atomic.interceptor'; export class ExtendUserController extends JsonBaseController { @InjectService() public service: JsonApiService; @Inject(ExampleService) protected exampleService: ExampleService; - getOne( + override getOne( id: string | number, - query: QueryType + query: QueryOne ): Promise> { return super.getOne(id, query); } diff --git a/apps/json-api-server/src/app/resources/resources.module.ts b/apps/json-api-server/src/app/resources/type-orm/resources-type.module.ts similarity index 69% rename from apps/json-api-server/src/app/resources/resources.module.ts rename to apps/json-api-server/src/app/resources/type-orm/resources-type.module.ts index 1c4d0e8d..397eb04b 100644 --- a/apps/json-api-server/src/app/resources/resources.module.ts +++ b/apps/json-api-server/src/app/resources/type-orm/resources-type.module.ts @@ -1,13 +1,20 @@ import { Module } from '@nestjs/common'; -import { JsonApiModule } from 'json-api-nestjs'; -import { Users, Addresses, Comments, Roles, BookList } from 'database'; +import { JsonApiModule, TypeOrmJsonApiModule } from '@klerick/json-api-nestjs'; +import { + Users, + Addresses, + Comments, + Roles, + BookList, +} from '@nestjs-json-api/typeorm-database'; + import { ExtendBookListController } from './controllers/extend-book-list/extend-book-list.controller'; import { ExtendUserController } from './controllers/extend-user/extend-user.controller'; import { ExampleService } from './service/example.service'; @Module({ imports: [ - JsonApiModule.forRoot({ + JsonApiModule.forRoot(TypeOrmJsonApiModule, { entities: [Users, Addresses, Comments, Roles, BookList], controllers: [ExtendBookListController, ExtendUserController], providers: [ExampleService], @@ -19,4 +26,4 @@ import { ExampleService } from './service/example.service'; }), ], }) -export class ResourcesModule {} +export class ResourcesTypeModule {} diff --git a/apps/json-api-server/src/app/resources/type-orm/service/atomic.interceptor.ts b/apps/json-api-server/src/app/resources/type-orm/service/atomic.interceptor.ts new file mode 100644 index 00000000..a52366ac --- /dev/null +++ b/apps/json-api-server/src/app/resources/type-orm/service/atomic.interceptor.ts @@ -0,0 +1,18 @@ +import { + CallHandler, + ExecutionContext, + NestInterceptor, + Injectable, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; + +@Injectable() +export class AtomicInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + const isAtomic = context.getArgByIndex(3); + if (isAtomic) { + console.log('call from atomic operation'); + } + return next.handle(); + } +} diff --git a/apps/json-api-server/src/app/resources/type-orm/service/controller.interceptor.ts b/apps/json-api-server/src/app/resources/type-orm/service/controller.interceptor.ts new file mode 100644 index 00000000..ee6db5ea --- /dev/null +++ b/apps/json-api-server/src/app/resources/type-orm/service/controller.interceptor.ts @@ -0,0 +1,39 @@ +import { + CallHandler, + ExecutionContext, + NestInterceptor, + Injectable, + BadRequestException, +} from '@nestjs/common'; +import { Observable, of, switchMap, throwError } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { Request } from 'express'; +import { ImATeapotException } from '@nestjs/common/exceptions/im-a-teapot.exception'; + +@Injectable() +export class ControllerInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + const query = context.switchToHttp().getRequest().query; + const typeCall = (query as any)?.filter?.firstName?.eq; + + return of(typeCall).pipe( + switchMap((r) => { + if (r === 'testControllerFilter') { + return throwError(() => new ImATeapotException()); + } + return next.handle(); + }), + map((r) => { + if (typeCall === 'testControllerInterceptor') { + const error = { + code: 'invalid_arguments', + message: `testControllerInterceptor error`, + path: [], + }; + throw new BadRequestException([error]); + } + return r; + }) + ); + } +} diff --git a/apps/json-api-server/src/app/resources/service/example.pipe.ts b/apps/json-api-server/src/app/resources/type-orm/service/example.pipe.ts similarity index 82% rename from apps/json-api-server/src/app/resources/service/example.pipe.ts rename to apps/json-api-server/src/app/resources/type-orm/service/example.pipe.ts index 63d0dc9f..9adda16d 100644 --- a/apps/json-api-server/src/app/resources/service/example.pipe.ts +++ b/apps/json-api-server/src/app/resources/type-orm/service/example.pipe.ts @@ -4,8 +4,8 @@ import { PipeTransform, } from '@nestjs/common'; -import { Query } from 'json-api-nestjs'; -import { Users } from 'database'; +import { Query } from '@klerick/json-api-nestjs'; +import { Users } from '@nestjs-json-api/typeorm-database'; export class ExamplePipe implements PipeTransform, Query> { transform(value: Query, metadata: ArgumentMetadata): Query { diff --git a/apps/json-api-server/src/app/resources/type-orm/service/example.service.ts b/apps/json-api-server/src/app/resources/type-orm/service/example.service.ts new file mode 100644 index 00000000..be8fc1e8 --- /dev/null +++ b/apps/json-api-server/src/app/resources/type-orm/service/example.service.ts @@ -0,0 +1,5 @@ +export class ExampleService { + testMethode(id: string): string { + return id; + } +} diff --git a/apps/json-api-server/src/app/resources/type-orm/service/guard.service.ts b/apps/json-api-server/src/app/resources/type-orm/service/guard.service.ts new file mode 100644 index 00000000..a8567bc2 --- /dev/null +++ b/apps/json-api-server/src/app/resources/type-orm/service/guard.service.ts @@ -0,0 +1,41 @@ +import { + BadRequestException, + CanActivate, + ExecutionContext, + ForbiddenException, + Injectable, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { entityForClass } from '@klerick/json-api-nestjs'; +import { Request } from 'express'; +import { Reflector } from '@nestjs/core'; + +export const EntityName = Reflector.createDecorator(); + +@Injectable() +export class GuardService implements CanActivate { + constructor(private reflector: Reflector) {} + + canActivate( + context: ExecutionContext + ): boolean | Promise | Observable { + const query = context.switchToHttp().getRequest().query; + const typeCall = (query as any)?.filter?.firstName?.eq; + + if (typeCall === 'testControllerGuard') { + return false; + } + + if (typeCall === 'testMethodeGuard') { + const entityName = this.reflector.get(EntityName, context.getHandler()); + if (!entityName) throw new BadRequestException(); + + // @ts-ignore + if (entityForClass(context.getClass()).name === entityName) { + throw new ForbiddenException('Not allow to ' + entityName); + } + } + + return true; + } +} diff --git a/apps/json-api-server/src/app/resources/type-orm/service/http-exception.filter.ts b/apps/json-api-server/src/app/resources/type-orm/service/http-exception.filter.ts new file mode 100644 index 00000000..125a3db5 --- /dev/null +++ b/apps/json-api-server/src/app/resources/type-orm/service/http-exception.filter.ts @@ -0,0 +1,38 @@ +import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common'; +import { ImATeapotException } from '@nestjs/common/exceptions/im-a-teapot.exception'; +import { Request, Response } from 'express'; +import { PreconditionFailedException } from '@nestjs/common/exceptions/precondition-failed.exception'; + +@Catch(ImATeapotException) +export class HttpExceptionFilter implements ExceptionFilter { + catch(exception: any, host: ArgumentsHost): any { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + const status = exception.getStatus(); + + response.status(status).json({ + statusCode: status, + timestamp: new Date().toISOString(), + path: request.url, + method: false, + }); + } +} + +@Catch(PreconditionFailedException) +export class HttpExceptionMethodFilter implements ExceptionFilter { + catch(exception: any, host: ArgumentsHost): any { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + const status = exception.getStatus(); + + response.status(status).json({ + statusCode: status, + timestamp: new Date().toISOString(), + path: request.url, + method: true, + }); + } +} diff --git a/apps/json-api-server/src/app/resources/type-orm/service/method.interceptor.ts b/apps/json-api-server/src/app/resources/type-orm/service/method.interceptor.ts new file mode 100644 index 00000000..37045f48 --- /dev/null +++ b/apps/json-api-server/src/app/resources/type-orm/service/method.interceptor.ts @@ -0,0 +1,40 @@ +import { + CallHandler, + ExecutionContext, + NestInterceptor, + Injectable, + BadRequestException, +} from '@nestjs/common'; +import { Observable, of, switchMap, throwError } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { Request } from 'express'; +import { ImATeapotException } from '@nestjs/common/exceptions/im-a-teapot.exception'; +import { PreconditionFailedException } from '@nestjs/common/exceptions/precondition-failed.exception'; + +@Injectable() +export class MethodInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + const query = context.switchToHttp().getRequest().query; + const typeCall = (query as any)?.filter?.firstName?.eq; + + return of(typeCall).pipe( + switchMap((r) => { + if (r === 'testMethodFilter') { + return throwError(() => new PreconditionFailedException()); + } + return next.handle(); + }), + map((r) => { + if (typeCall === 'testMethodInterceptor') { + const error = { + code: 'invalid_arguments', + message: `testMethodInterceptor error`, + path: [], + }; + throw new BadRequestException([error]); + } + return r; + }) + ); + } +} diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 00000000..52340133 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,51 @@ +version: '3.8' +services: +# api-gate: +# container_name: api-gate +# restart: always +# build: apps/api-gate/ +# ports: +# - "3000:3000" + postgres: + image: postgres:15.1-alpine + restart: always + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + ports: + - '5432:5432' + volumes: + - db:/var/lib/postgresql/data + pgadmin: + container_name: 'pgadmin' + image: 'dpage/pgadmin4:latest' + hostname: pgadmin + depends_on: + - 'postgres' + environment: + - PGADMIN_DEFAULT_PASSWORD=password + - PGADMIN_DEFAULT_EMAIL=pg.admin@email.com + volumes: + - pgadmin:/root/.pgadmin + ports: + - '8000:80' +# redis: +# image: redis:6.2-alpine +# restart: always +# ports: +# - '6379:6379' +# command: redis-server --save 20 1 --loglevel warning +# volumes: +# - redis:/data +# jaeger: +# image: jaegertracing/all-in-one:1.41 +# ports: +# - "16686:16686" +# - "14268:14268" +volumes: + db: + driver: local +# redis: +# driver: local + pgadmin: + driver: local diff --git a/libs/database/README.md b/libs/database/README.md deleted file mode 100644 index 8453478f..00000000 --- a/libs/database/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# database - -This library was generated with [Nx](https://nx.dev). - -## Running unit tests - -Run `nx test database` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/database/project.json b/libs/database/project.json deleted file mode 100644 index 43dc1750..00000000 --- a/libs/database/project.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "database", - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "libs/database/src", - "projectType": "library", - "targets": {}, - "tags": [] -} diff --git a/libs/database/src/index.ts b/libs/database/src/index.ts deleted file mode 100644 index 0207b998..00000000 --- a/libs/database/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './lib/database.module'; -export * from './lib/entities'; diff --git a/libs/json-api/json-api-nestjs-sdk/README.md b/libs/json-api/json-api-nestjs-sdk/README.md index 15f7fbc4..51cc5bf0 100644 --- a/libs/json-api/json-api-nestjs-sdk/README.md +++ b/libs/json-api/json-api-nestjs-sdk/README.md @@ -14,7 +14,7 @@ The plugin of client for help work with JSON API over [json-api-nestjs](https:// ## Installation ```bash $ -npm install json-api-nestjs-sdk +npm install @klerick/json-api-nestjs-sdk ``` ## Example @@ -27,7 +27,7 @@ import { FilterOperand, JsonApiJs, JsonSdkPromise, -} from 'json-api-nestjs-sdk'; +} from '@klerick/json-api-nestjs-sdk'; import { faker } from '@faker-js/faker'; import axios from 'axios'; @@ -93,7 +93,11 @@ const [addressPost, managerPost, rolesPost, userPost] = await jsonSdk ``` or you can use Angular module: ```typescript -import { provideJsonApi, AtomicFactory, JsonApiSdkService } from 'json-api-nestjs-sdk/ngModule'; +import { + provideJsonApi, + AtomicFactory, + JsonApiSdkService +} from '@klerick/json-api-nestjs-sdk/ngModule'; import { provideHttpClient, withFetch, diff --git a/libs/json-api/json-api-nestjs-sdk/package.json b/libs/json-api/json-api-nestjs-sdk/package.json index 98799c5f..fc882e54 100644 --- a/libs/json-api/json-api-nestjs-sdk/package.json +++ b/libs/json-api/json-api-nestjs-sdk/package.json @@ -1,5 +1,5 @@ { - "name": "json-api-nestjs-sdk", + "name": "@klerick/json-api-nestjs-sdk", "version": "8.0.0", "engines": { "node": ">= 16.0.0" diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/service/json-api-utils.service.spec.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/service/json-api-utils.service.spec.ts index 95feb008..dd04a10e 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/service/json-api-utils.service.spec.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/service/json-api-utils.service.spec.ts @@ -453,15 +453,19 @@ describe('JsonApiUtilsService', () => { }, relationships: { relatedEntity: { - type: 'related-entity', - id: '2', - }, - relatedEntities: [ - { - type: 'related-entities', - id: '3', + data: { + type: 'related-entity', + id: '2', }, - ], + }, + relatedEntities: { + data: [ + { + type: 'related-entities', + id: '3', + }, + ], + }, }, }); }); diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/service/json-api-utils.service.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/service/json-api-utils.service.ts index 0584fd3c..6f029dce 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/service/json-api-utils.service.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/service/json-api-utils.service.ts @@ -1,3 +1,5 @@ +import { createEntityInstance } from '../../shared'; + import { Attributes, Entity as EntityObject, @@ -19,7 +21,6 @@ import { HttpParams, isObject, isRelation, - kebabToCamel, ObjectTyped, } from '../utils'; import { ID_KEY } from '../constants'; @@ -260,8 +261,7 @@ export class JsonApiUtilsService { } createEntityInstance(name: string): E { - const entityName = kebabToCamel(name); - return Function('return new class ' + entityName + '{}')(); + return createEntityInstance(name); } private findIncludeEntity>>( @@ -272,6 +272,7 @@ export class JsonApiUtilsService { (includedItem) => includedItem.type === item.type && includedItem.id === item.id ); + if (!relatedIncluded) return; const entityObject = { @@ -338,7 +339,7 @@ export class JsonApiUtilsService { } return { ...acum, - [key]: data, + [key]: { data }, }; }, {} as Relationships); diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/types/entity.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/types/entity.ts index ecc3a2b1..682c399a 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/types/entity.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/types/entity.ts @@ -1,4 +1,4 @@ -import { EntityField, EntityProps } from 'json-shared-type'; +import { EntityField, EntityProps } from '../../shared'; export { EntityField, EntityProps }; diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/types/filter-operand.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/types/filter-operand.ts index ba47b6e3..c1f1a670 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/types/filter-operand.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/types/filter-operand.ts @@ -1,4 +1,4 @@ -import { FilterOperand } from 'json-shared-type'; +import { FilterOperand } from '../../shared'; export { FilterOperand }; diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/types/query-params.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/types/query-params.ts index 1450e19d..8a555ac8 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/types/query-params.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/types/query-params.ts @@ -1,4 +1,4 @@ -import { QueryField } from 'json-shared-type'; +import { QueryField } from '../../shared'; import { EntityProps, EntityRelation } from './entity'; import { TypeOfArray } from './utils'; import { Operands, OperandsRelation } from './filter-operand'; diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/types/response-body.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/types/response-body.ts index 33f67dab..24813506 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/types/response-body.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/types/response-body.ts @@ -10,4 +10,4 @@ export { ResourceData, ResourceObject, ResourceObjectRelationships, -} from 'json-shared-type'; +} from '../../shared'; diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/types/utils.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/types/utils.ts index 4ce83427..233d219b 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/types/utils.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/types/utils.ts @@ -1,4 +1,4 @@ -import { TypeOfArray } from 'json-shared-type'; +import { TypeOfArray } from '../../shared'; export { TypeOfArray }; type IntersectionToObj = { diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/utils/generate-atomic-body.spec.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/utils/generate-atomic-body.spec.ts index f768ed57..4a278865 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/utils/generate-atomic-body.spec.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/utils/generate-atomic-body.spec.ts @@ -69,12 +69,14 @@ describe('GenerateAtomicBody', () => { text: entity.text, }, relationships: { - users: [ - { - id: `${user.id}`, - type: 'users', - }, - ], + users: { + data: [ + { + id: `${user.id}`, + type: 'users', + }, + ], + }, }, type: 'book-list', }, @@ -102,12 +104,14 @@ describe('GenerateAtomicBody', () => { text: entity.text, }, relationships: { - users: [ - { - id: `${user.id}`, - type: 'users', - }, - ], + users: { + data: [ + { + id: `${user.id}`, + type: 'users', + }, + ], + }, }, type: 'book-list', }, @@ -147,12 +151,14 @@ describe('GenerateAtomicBody', () => { text: entity.text, }, relationships: { - users: [ - { - id: `${user.id}`, - type: 'users', - }, - ], + users: { + data: [ + { + id: `${user.id}`, + type: 'users', + }, + ], + }, }, type: 'book-list', }, diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/utils/index.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/utils/index.ts index 1367c3c2..495bd3c5 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/utils/index.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/utils/index.ts @@ -1,4 +1,4 @@ -import { camelToKebab } from 'shared-utils'; +import { camelToKebab } from '../../shared'; import { JsonApiSdkConfig, JsonSdkConfig } from '../types'; import { ID_KEY } from '../constants'; @@ -14,7 +14,7 @@ export { capitalizeFirstChar, kebabToCamel, isObject, -} from 'shared-utils'; +} from '../../shared'; export function resultConfig(partialConfig: JsonSdkConfig): JsonApiSdkConfig { return { diff --git a/libs/shared-utils/src/index.ts b/libs/json-api/json-api-nestjs-sdk/src/shared/index.ts similarity index 100% rename from libs/shared-utils/src/index.ts rename to libs/json-api/json-api-nestjs-sdk/src/shared/index.ts diff --git a/libs/json-api/json-shared-type/src/types/entity-type.ts b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/entity-type.ts similarity index 100% rename from libs/json-api/json-shared-type/src/types/entity-type.ts rename to libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/entity-type.ts diff --git a/libs/json-api/json-shared-type/src/types/index.ts b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/index.ts similarity index 71% rename from libs/json-api/json-shared-type/src/types/index.ts rename to libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/index.ts index 8b86a958..33b70ba6 100644 --- a/libs/json-api/json-shared-type/src/types/index.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/index.ts @@ -1,4 +1,4 @@ -export * from './entity-type'; +export * from './utils-string.type'; export * from './query-type'; -export * from './utils-type'; +export * from './entity-type'; export * from './response-body'; diff --git a/libs/json-api/json-shared-type/src/types/query-type.ts b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/query-type.ts similarity index 58% rename from libs/json-api/json-shared-type/src/types/query-type.ts rename to libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/query-type.ts index dba33ee2..ddeead7a 100644 --- a/libs/json-api/json-shared-type/src/types/query-type.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/query-type.ts @@ -10,12 +10,27 @@ export enum FilterOperand { eq = 'eq', gt = 'gt', gte = 'gte', - in = 'in', like = 'like', lt = 'lt', lte = 'lte', ne = 'ne', - nin = 'nin', regexp = 'regexp', + in = 'in', + nin = 'nin', some = 'some', } + +export enum FilterOperandOnlyInNin { + in = 'in', + nin = 'nin', +} +export enum FilterOperandOnlySimple { + eq = 'eq', + gt = 'gt', + gte = 'gte', + like = 'like', + lt = 'lt', + lte = 'lte', + ne = 'ne', + regexp = 'regexp', +} diff --git a/libs/json-api/json-shared-type/src/types/response-body.ts b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/response-body.ts similarity index 86% rename from libs/json-api/json-shared-type/src/types/response-body.ts rename to libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/response-body.ts index fbe91dde..85dbdb49 100644 --- a/libs/json-api/json-shared-type/src/types/response-body.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/response-body.ts @@ -5,6 +5,7 @@ import { TypeOfArray, ValueOf, } from '.'; +import { Collection } from '@mikro-orm/core'; export type PageProps = { totalItems: number; @@ -30,8 +31,14 @@ export type Attributes = { [P in EntityProps]?: D[P] extends EntityField ? D[P] : TypeOfArray; }; +export type DataResult = E extends unknown[] + ? MainData[] + : E extends Collection + ? MainData[] + : MainData | null; + export type Data = { - data?: E extends unknown[] ? MainData[] : MainData | null; + data?: DataResult; }; export type Relationships = { diff --git a/libs/shared-utils/src/lib/types/utils-string.type.ts b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/utils-string.type.ts similarity index 72% rename from libs/shared-utils/src/lib/types/utils-string.type.ts rename to libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/utils-string.type.ts index d5719e31..b861d238 100644 --- a/libs/shared-utils/src/lib/types/utils-string.type.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/utils-string.type.ts @@ -1,3 +1,5 @@ +import type { Collection } from '@mikro-orm/core'; + export type KebabCase = S extends `${infer C}${infer T}` ? KebabCase extends infer U ? U extends string @@ -14,3 +16,11 @@ export type KebabToCamelCase = : S extends `${infer T}-${infer U}` ? `${Capitalize}${Capitalize>}` : S; + +export type TypeOfArray = T extends (infer U)[] + ? U + : T extends Collection + ? U + : T; + +export type ValueOf = T[keyof T]; diff --git a/libs/shared-utils/src/lib/utils/index.ts b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/utils/index.ts similarity index 100% rename from libs/shared-utils/src/lib/utils/index.ts rename to libs/json-api/json-api-nestjs-sdk/src/shared/lib/utils/index.ts diff --git a/libs/shared-utils/src/lib/utils/object-utils.ts b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/utils/object-utils.ts similarity index 71% rename from libs/shared-utils/src/lib/utils/object-utils.ts rename to libs/json-api/json-api-nestjs-sdk/src/shared/lib/utils/object-utils.ts index 461455b3..5d4ad26d 100644 --- a/libs/shared-utils/src/lib/utils/object-utils.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/utils/object-utils.ts @@ -1,3 +1,5 @@ +import { kebabToCamel } from './string-utils'; + export const ObjectTyped = { keys: Object.keys as (yourObject: T) => Array, values: Object.values as (yourObject: U) => Array, @@ -12,3 +14,8 @@ export const ObjectTyped = { export function isObject(item: unknown): item is object { return typeof item === 'object' && !Array.isArray(item) && item !== null; } + +export function createEntityInstance(name: string): E { + const entityName = kebabToCamel(name); + return Function('return new class ' + entityName + '{}')(); +} diff --git a/libs/shared-utils/src/lib/utils/string-utils.spec.ts b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/utils/string-utils.spec.ts similarity index 91% rename from libs/shared-utils/src/lib/utils/string-utils.spec.ts rename to libs/json-api/json-api-nestjs-sdk/src/shared/lib/utils/string-utils.spec.ts index 437de652..4596cd21 100644 --- a/libs/shared-utils/src/lib/utils/string-utils.spec.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/utils/string-utils.spec.ts @@ -1,4 +1,9 @@ -import { camelToKebab, snakeToCamel, isString, kebabToCamel } from './'; +import { + camelToKebab, + snakeToCamel, + isString, + kebabToCamel, +} from './string-utils'; describe('Test utils', () => { it('camelToKebab', () => { diff --git a/libs/shared-utils/src/lib/utils/string-utils.ts b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/utils/string-utils.ts similarity index 100% rename from libs/shared-utils/src/lib/utils/string-utils.ts rename to libs/json-api/json-api-nestjs-sdk/src/shared/lib/utils/string-utils.ts diff --git a/libs/json-api/json-api-nestjs-shared/.eslintrc.json b/libs/json-api/json-api-nestjs-shared/.eslintrc.json new file mode 100644 index 00000000..0af28030 --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/.eslintrc.json @@ -0,0 +1,30 @@ +{ + "extends": ["../../../.eslintrc.base.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.json"], + "parser": "jsonc-eslint-parser", + "rules": { + "@nx/dependency-checks": [ + "error", + { + "ignoredFiles": ["{projectRoot}/eslint.config.{js,cjs,mjs}"] + } + ] + } + } + ] +} diff --git a/libs/json-api/json-api-nestjs-shared/README.md b/libs/json-api/json-api-nestjs-shared/README.md new file mode 100644 index 00000000..8703f1bf --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/README.md @@ -0,0 +1,11 @@ +# json-api-nestjs-shared + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build json-api-nestjs-shared` to build the library. + +## Running unit tests + +Run `nx test json-api-nestjs-shared` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/json-api/json-shared-type/jest.config.ts b/libs/json-api/json-api-nestjs-shared/jest.config.ts similarity index 63% rename from libs/json-api/json-shared-type/jest.config.ts rename to libs/json-api/json-api-nestjs-shared/jest.config.ts index 2c9b7818..67ade3e9 100644 --- a/libs/json-api/json-shared-type/jest.config.ts +++ b/libs/json-api/json-api-nestjs-shared/jest.config.ts @@ -1,11 +1,10 @@ -/* eslint-disable */ export default { - displayName: 'json-shared-type', + displayName: 'json-api-nestjs-shared', preset: '../../../jest.preset.js', testEnvironment: 'node', transform: { '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], }, moduleFileExtensions: ['ts', 'js', 'html'], - coverageDirectory: '../../../coverage/libs/json-api/json-shared-type', + coverageDirectory: '../../../coverage/libs/json-api/json-api-nestjs-shared', }; diff --git a/libs/json-api/json-api-nestjs-shared/package.json b/libs/json-api/json-api-nestjs-shared/package.json new file mode 100644 index 00000000..25918d9b --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/package.json @@ -0,0 +1,10 @@ +{ + "name": "@klerick/json-api-nestjs-shared", + "version": "0.0.1", + "dependencies": { + "tslib": "^2.3.0" + }, + "type": "commonjs", + "main": "./src/index.js", + "typings": "./src/index.d.ts" +} diff --git a/libs/json-api/json-api-nestjs-shared/project.json b/libs/json-api/json-api-nestjs-shared/project.json new file mode 100644 index 00000000..cdb32619 --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/project.json @@ -0,0 +1,33 @@ +{ + "name": "json-api-nestjs-shared", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/json-api/json-api-nestjs-shared/src", + "projectType": "library", + "release": { + "version": { + "generatorOptions": { + "packageRoot": "dist/{projectRoot}", + "currentVersionResolver": "git-tag" + } + } + }, + "tags": [], + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/json-api/json-api-nestjs-shared", + "tsConfig": "libs/json-api/json-api-nestjs-shared/tsconfig.lib.json", + "packageJson": "libs/json-api/json-api-nestjs-shared/package.json", + "main": "libs/json-api/json-api-nestjs-shared/src/index.ts", + "assets": ["libs/json-api/json-api-nestjs-shared/*.md"] + } + }, + "nx-release-publish": { + "options": { + "packageRoot": "dist/{projectRoot}" + } + } + } +} diff --git a/libs/json-api/json-api-nestjs-shared/src/index.ts b/libs/json-api/json-api-nestjs-shared/src/index.ts new file mode 100644 index 00000000..a0fe9b9f --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/utils'; +export * from './lib/types'; diff --git a/libs/json-api/json-api-nestjs-shared/src/lib/types/entity-type.ts b/libs/json-api/json-api-nestjs-shared/src/lib/types/entity-type.ts new file mode 100644 index 00000000..5fbdd04f --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/src/lib/types/entity-type.ts @@ -0,0 +1,17 @@ +export type EntityField = + | string + | number + | bigint + | boolean + | string[] + | number[] + | null + | Date; + +export type EntityProps = { + [P in keyof T]: T[P] extends EntityField ? P : never; +}[keyof T]; + +export type EntityRelation = { + [P in keyof T]: T[P] extends EntityField ? never : P; +}[keyof T]; diff --git a/libs/json-api/json-api-nestjs-shared/src/lib/types/index.ts b/libs/json-api/json-api-nestjs-shared/src/lib/types/index.ts new file mode 100644 index 00000000..33b70ba6 --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/src/lib/types/index.ts @@ -0,0 +1,4 @@ +export * from './utils-string.type'; +export * from './query-type'; +export * from './entity-type'; +export * from './response-body'; diff --git a/libs/json-api/json-api-nestjs-shared/src/lib/types/query-type.ts b/libs/json-api/json-api-nestjs-shared/src/lib/types/query-type.ts new file mode 100644 index 00000000..ddeead7a --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/src/lib/types/query-type.ts @@ -0,0 +1,36 @@ +export enum QueryField { + filter = 'filter', + sort = 'sort', + include = 'include', + page = 'page', + fields = 'fields', +} + +export enum FilterOperand { + eq = 'eq', + gt = 'gt', + gte = 'gte', + like = 'like', + lt = 'lt', + lte = 'lte', + ne = 'ne', + regexp = 'regexp', + in = 'in', + nin = 'nin', + some = 'some', +} + +export enum FilterOperandOnlyInNin { + in = 'in', + nin = 'nin', +} +export enum FilterOperandOnlySimple { + eq = 'eq', + gt = 'gt', + gte = 'gte', + like = 'like', + lt = 'lt', + lte = 'lte', + ne = 'ne', + regexp = 'regexp', +} diff --git a/libs/json-api/json-api-nestjs-shared/src/lib/types/response-body.ts b/libs/json-api/json-api-nestjs-shared/src/lib/types/response-body.ts new file mode 100644 index 00000000..85dbdb49 --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/src/lib/types/response-body.ts @@ -0,0 +1,76 @@ +import { + EntityField, + EntityProps, + EntityRelation, + TypeOfArray, + ValueOf, +} from '.'; +import { Collection } from '@mikro-orm/core'; + +export type PageProps = { + totalItems: number; + pageNumber: number; + pageSize: number; +}; + +export type DebugMetaProps = Partial<{ + time: number; +}>; + +export type MainData = { + type: T; + id: string; +}; + +export type Links = { + self: string; + related?: string; +}; + +export type Attributes = { + [P in EntityProps]?: D[P] extends EntityField ? D[P] : TypeOfArray; +}; + +export type DataResult = E extends unknown[] + ? MainData[] + : E extends Collection + ? MainData[] + : MainData | null; + +export type Data = { + data?: DataResult; +}; + +export type Relationships = { + [P in EntityRelation]?: { + links: Links; + } & Data; +}; + +export type Include = ValueOf<{ + [P in EntityRelation]: ResourceData>; +}>; + +export type ResourceData = MainData & { + attributes?: Attributes; + relationships?: Relationships; + links: Omit; +}; + +export type MetaProps = R extends null ? T : T & R; + +export type ResourceObject< + T, + R extends 'object' | 'array' = 'object', + M = null +> = { + meta: R extends 'array' + ? MetaProps + : MetaProps; + data: R extends 'array' ? ResourceData[] : ResourceData; + included?: Include[]; +}; + +export type ResourceObjectRelationships> = { + meta: DebugMetaProps; +} & Required>; diff --git a/libs/json-api/json-api-nestjs-shared/src/lib/types/utils-string.type.ts b/libs/json-api/json-api-nestjs-shared/src/lib/types/utils-string.type.ts new file mode 100644 index 00000000..b861d238 --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/src/lib/types/utils-string.type.ts @@ -0,0 +1,26 @@ +import type { Collection } from '@mikro-orm/core'; + +export type KebabCase = S extends `${infer C}${infer T}` + ? KebabCase extends infer U + ? U extends string + ? T extends Uncapitalize + ? `${Uncapitalize}${U}` + : `${Uncapitalize}-${U}` + : never + : never + : S; + +export type KebabToCamelCase = + S extends `${infer T}-${infer U}-${infer V}` + ? `${T}${Capitalize}${Capitalize>}` + : S extends `${infer T}-${infer U}` + ? `${Capitalize}${Capitalize>}` + : S; + +export type TypeOfArray = T extends (infer U)[] + ? U + : T extends Collection + ? U + : T; + +export type ValueOf = T[keyof T]; diff --git a/libs/json-api/json-api-nestjs-shared/src/lib/utils/index.ts b/libs/json-api/json-api-nestjs-shared/src/lib/utils/index.ts new file mode 100644 index 00000000..a7799257 --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/src/lib/utils/index.ts @@ -0,0 +1,2 @@ +export * from './string-utils'; +export * from './object-utils'; diff --git a/libs/json-api/json-api-nestjs-shared/src/lib/utils/object-utils.ts b/libs/json-api/json-api-nestjs-shared/src/lib/utils/object-utils.ts new file mode 100644 index 00000000..5d4ad26d --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/src/lib/utils/object-utils.ts @@ -0,0 +1,21 @@ +import { kebabToCamel } from './string-utils'; + +export const ObjectTyped = { + keys: Object.keys as (yourObject: T) => Array, + values: Object.values as (yourObject: U) => Array, + entries: Object.entries as ( + yourObject: O + ) => Array<[keyof O, O[keyof O]]>, + fromEntries: Object.fromEntries as ( + yourObjectEntries: [K, V][] + ) => Record, +}; + +export function isObject(item: unknown): item is object { + return typeof item === 'object' && !Array.isArray(item) && item !== null; +} + +export function createEntityInstance(name: string): E { + const entityName = kebabToCamel(name); + return Function('return new class ' + entityName + '{}')(); +} diff --git a/libs/json-api/json-api-nestjs-shared/src/lib/utils/string-utils.spec.ts b/libs/json-api/json-api-nestjs-shared/src/lib/utils/string-utils.spec.ts new file mode 100644 index 00000000..4596cd21 --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/src/lib/utils/string-utils.spec.ts @@ -0,0 +1,42 @@ +import { + camelToKebab, + snakeToCamel, + isString, + kebabToCamel, +} from './string-utils'; + +describe('Test utils', () => { + it('camelToKebab', () => { + const result = camelToKebab('ApproverGroups'); + const result1 = camelToKebab('Users'); + + expect(result).toBe('approver-groups'); + expect(result1).toBe('users'); + }); + + it('snakeToCamel', () => { + const result = snakeToCamel('test_test'); + const result1 = snakeToCamel('test-test'); + const result2 = snakeToCamel('testTest'); + const result3 = snakeToCamel('event_incident_typeFK'); + expect(result).toBe('testTest'); + expect(result1).toBe('testTest'); + expect(result2).toBe('testTest'); + expect(result3).toBe('eventIncidentTypeFK'); + }); + + it('isString', () => { + expect(isString('string')).toBe(true); + expect(isString(String('string'))).toBe(true); + expect(isString(new Date())).toBe(false); + expect(isString(class {})).toBe(false); + }); + + it('kebabToCamel', () => { + const type = 'users-group'; + const type1 = 'users'; + + expect(kebabToCamel(type)).toBe('UsersGroup'); + expect(kebabToCamel(type1)).toBe('Users'); + }); +}); diff --git a/libs/json-api/json-api-nestjs-shared/src/lib/utils/string-utils.ts b/libs/json-api/json-api-nestjs-shared/src/lib/utils/string-utils.ts new file mode 100644 index 00000000..3b678710 --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/src/lib/utils/string-utils.ts @@ -0,0 +1,38 @@ +import { KebabToCamelCase, KebabCase } from '../types'; + +export function isString(value: T): value is P { + return typeof value === 'string' || value instanceof String; +} + +export function snakeToCamel(str: string): string { + if (!str.match(/[\s_-]/g)) { + return str; + } + return str.replace(/([-_][a-z])/g, (group) => + group.toUpperCase().replace('-', '').replace('_', '') + ); +} + +export function camelToKebab(string: S): KebabCase { + return string + .replace(/((?<=[a-z\d])[A-Z]|(?<=[A-Z\d])[A-Z](?=[a-z]))/g, '-$1') + .toLowerCase() as KebabCase; +} + +export function upperFirstLetter(string: S): Capitalize { + return (string.charAt(0).toUpperCase() + string.slice(1)) as Capitalize; +} + +export function kebabToCamel(str: S): KebabToCamelCase { + return str + .split('-') + .map((i) => i.charAt(0).toUpperCase() + i.substring(1)) + .join('') as KebabToCamelCase; +} + +export function capitalizeFirstChar(str: string) { + return str + .split('-') + .map((i) => i.charAt(0).toUpperCase() + i.substring(1)) + .join(''); +} diff --git a/libs/json-api/json-shared-type/tsconfig.json b/libs/json-api/json-api-nestjs-shared/tsconfig.json similarity index 81% rename from libs/json-api/json-shared-type/tsconfig.json rename to libs/json-api/json-api-nestjs-shared/tsconfig.json index 8122543a..0dc79caa 100644 --- a/libs/json-api/json-shared-type/tsconfig.json +++ b/libs/json-api/json-api-nestjs-shared/tsconfig.json @@ -5,9 +5,9 @@ "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true }, "files": [], "include": [], diff --git a/libs/json-api/json-shared-type/tsconfig.lib.json b/libs/json-api/json-api-nestjs-shared/tsconfig.lib.json similarity index 50% rename from libs/json-api/json-shared-type/tsconfig.lib.json rename to libs/json-api/json-api-nestjs-shared/tsconfig.lib.json index 4befa7f0..dbf54fd7 100644 --- a/libs/json-api/json-shared-type/tsconfig.lib.json +++ b/libs/json-api/json-api-nestjs-shared/tsconfig.lib.json @@ -3,7 +3,13 @@ "compilerOptions": { "outDir": "../../../dist/out-tsc", "declaration": true, - "types": ["node"] + "types": ["node"], + "target": "es2021", + "strictNullChecks": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true }, "include": ["src/**/*.ts"], "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] diff --git a/libs/json-api/json-shared-type/tsconfig.spec.json b/libs/json-api/json-api-nestjs-shared/tsconfig.spec.json similarity index 88% rename from libs/json-api/json-shared-type/tsconfig.spec.json rename to libs/json-api/json-api-nestjs-shared/tsconfig.spec.json index 69a251f3..ab55b7c7 100644 --- a/libs/json-api/json-shared-type/tsconfig.spec.json +++ b/libs/json-api/json-api-nestjs-shared/tsconfig.spec.json @@ -3,6 +3,7 @@ "compilerOptions": { "outDir": "../../../dist/out-tsc", "module": "commonjs", + "moduleResolution": "node10", "types": ["jest", "node"] }, "include": [ diff --git a/libs/json-api/json-api-nestjs/.eslintrc.json b/libs/json-api/json-api-nestjs/.eslintrc.json index ac1a6002..0af28030 100644 --- a/libs/json-api/json-api-nestjs/.eslintrc.json +++ b/libs/json-api/json-api-nestjs/.eslintrc.json @@ -1,6 +1,6 @@ { - "extends": ["../../../.eslintrc.json"], - "ignorePatterns": ["!**/*", "**/*.spec.ts"], + "extends": ["../../../.eslintrc.base.json"], + "ignorePatterns": ["!**/*"], "overrides": [ { "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], @@ -18,7 +18,12 @@ "files": ["*.json"], "parser": "jsonc-eslint-parser", "rules": { - "@nx/dependency-checks": "error" + "@nx/dependency-checks": [ + "error", + { + "ignoredFiles": ["{projectRoot}/eslint.config.{js,cjs,mjs}"] + } + ] } } ] diff --git a/libs/json-api/json-api-nestjs/README.md b/libs/json-api/json-api-nestjs/README.md index cba8c946..ffa4bf6a 100644 --- a/libs/json-api/json-api-nestjs/README.md +++ b/libs/json-api/json-api-nestjs/README.md @@ -8,8 +8,8 @@ # json-api-nestjs -This plugin works upon TypeOrm library, which is used as the main database abstraction layer tool. The module -automatically generates an API according to JSON API specification from the database structure (TypeORM entities). It +This plugin works upon **TypeOrm** or **MicroOrm** library, which is used as the main database abstraction layer tool. The module +automatically generates an API according to JSON API specification from the database structure (**TypeOrm** or **MicroOrm** entities). It supports features such as requests validation based on database fields types, request filtering, endpoints extending, data relations control and much more. Our module significantly reduces the development time of REST services by removing the need to negotiate the mechanism of client-server interaction and implementing automatic API generation without the @@ -24,15 +24,32 @@ $ npm install json-api-nestjs ## Example Once the installation process is complete, we can import the **JsonApiModule** into the root **AppModule**. +### TypeOrm +```typescript +import {Module} from '@nestjs/common'; +import {JsonApiModule, TypeOrmJsonApiModule} from 'json-api-nestjs'; +import {Users} from 'type-orm/database'; +@Module({ + imports: [ + JsonApiModule.forRoot(TypeOrmJsonApiModule, { + entities: [Users] + }), + ], +}) +export class AppModule { +} +``` + +### MicroOrm ```typescript import {Module} from '@nestjs/common'; -import {JsonApiModule} from 'json-api-nestjs'; -import {Users} from 'database'; +import {JsonApiModule, MicroOrmJsonApiModule} from 'json-api-nestjs'; +import {Users} from 'micro-orm/database'; @Module({ imports: [ - JsonApiModule.forRoot({ + JsonApiModule.forRoot(MicroOrmJsonApiModule, { entities: [Users] }), ], @@ -69,11 +86,14 @@ export interface ModuleOptions { debug?: boolean; // Debug info in result object, like error message pipeForId?: Type // Nestjs pipe for validate id params, by default ParseIntPipe operationUrl?: string // Url for atomic operation https://jsonapi.org/ext/atomic/ + // TypeOrm useSoftDelete?: boolean // Use soft delete runInTransaction?: any>( isolationLevel: IsolationLevel, fn: Func ) => ReturnType // You can use cutom function for wrapping transaction in atomic operation, example: runInTransaction from https://github.com/Aliheym/typeorm-transactional + // MicroOrm + arrayType?: string[]; //Custom type for indicate of array }; } ``` @@ -254,7 +274,7 @@ Available query params: ```typescript type FilterOperand { -in:string[] // is equal to the conditional of query "WHERE 'attribute_name' IN ('value1', 'value2')" + in:string[] // is equal to the conditional of query "WHERE 'attribute_name' IN ('value1', 'value2')" nin: string[] // is equal to the conditional of query "WHERE 'attribute_name' NOT IN ('value1', 'value1')" eq: string // is equal to the conditional of query "WHERE 'attribute_name' = 'value1' ne: string // is equal to the conditional of query "WHERE 'attribute_name' <> 'value1' diff --git a/libs/json-api/json-api-nestjs/package.json b/libs/json-api/json-api-nestjs/package.json index 32a4b080..b2231f27 100644 --- a/libs/json-api/json-api-nestjs/package.json +++ b/libs/json-api/json-api-nestjs/package.json @@ -27,10 +27,24 @@ "jsonapi", "json-api", "typeorm", + "microorm", "CRUD" ], "peerDependencies": { "reflect-metadata": "^0.1.13", - "tslib": "^2.3.0" + "tslib": "^2.3.0", + "@mikro-orm/core": "^6.0.0 || ^6.0.0-dev.0", + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/swagger": "^7.3.0", + "@nestjs/typeorm": "^10.0.0", + "@mikro-orm/knex": "^6.0.0", + "typeorm": "^0.3.20" + }, + "dependencies": { + "@anatine/zod-openapi": "^2.0.0", + "zod": "^3.24.0", + "zod-validation-error": "^3.4.0", + "uuid": "^10.0.0" } } diff --git a/libs/json-api/json-api-nestjs/project.json b/libs/json-api/json-api-nestjs/project.json index f0cb4555..ad65b72e 100644 --- a/libs/json-api/json-api-nestjs/project.json +++ b/libs/json-api/json-api-nestjs/project.json @@ -3,47 +3,72 @@ "$schema": "../../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "libs/json-api/json-api-nestjs/src", "projectType": "library", + "release": { + "version": { + "generatorOptions": { + "packageRoot": "dist/{projectRoot}", + "currentVersionResolver": "git-tag" + } + } + }, + "tags": [], "targets": { - "build-ts": { + "build": { "executor": "@nx/js:tsc", - "outputs": ["{options.outputPath}"], + "outputs": [ + "{options.outputPath}" + ], "options": { "outputPath": "dist/libs/json-api/json-api-nestjs", - "main": "libs/json-api/json-api-nestjs/src/index.ts", "tsConfig": "libs/json-api/json-api-nestjs/tsconfig.lib.json", - "assets": ["libs/json-api/json-api-nestjs/README.md"], - "external": "none", - "updateBuildableProjectDepsInPackageJson": true, + "packageJson": "libs/json-api/json-api-nestjs/package.json", + "main": "libs/json-api/json-api-nestjs/src/index.ts", + "assets": [ + "libs/json-api/json-api-nestjs/*.md" + ], "buildableProjectDepsInPackageJsonType": "peerDependencies", "generateExportsField": true } }, - "build": { - "executor": "nx:run-commands", - "dependsOn": [ - "build-ts" + "build-npm": { + "executor": "@nx/js:tsc", + "outputs": [ + "{options.outputPath}" ], "options": { - "commands": ["rm -rf dist/libs/json-api/json-api-nestjs/libs"], - "cwd": "./", - "parallel": false + "outputPath": "node_modules/@klerick/json-api-nestjs", + "tsConfig": "libs/json-api/json-api-nestjs/tsconfig.lib.json", + "packageJson": "libs/json-api/json-api-nestjs/package.json", + "main": "libs/json-api/json-api-nestjs/src/index.ts", + "assets": [ + "libs/json-api/json-api-nestjs/*.md" + ], + "buildableProjectDepsInPackageJsonType": "peerDependencies", + "generateExportsField": true + } + }, + "nx-release-publish": { + "options": { + "packageRoot": "dist/{projectRoot}" } }, "publish": { "command": "node tools/scripts/publish.mjs json-api-nestjs {args.ver} {args.tag}", - "dependsOn": ["build"] - }, - "lint": { - "executor": "@nx/eslint:lint", - "outputs": ["{options.outputFile}"] + "dependsOn": [ + "build" + ] }, "test": { "executor": "@nx/jest:jest", - "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "outputs": [ + "{workspaceRoot}/coverage/{projectRoot}" + ], "options": { "jestConfig": "libs/json-api/json-api-nestjs/jest.config.ts", "codeCoverage": true, - "coverageReporters": ["json-summary"] + "coverageReporters": [ + "json-summary" + ] } }, "upload-badge": { @@ -54,17 +79,13 @@ } ], "options": { - "commands": ["node tools/scripts/upload-badge.mjs json-api-nestjs"], + "commands": [ + "node tools/scripts/upload-badge.mjs json-api-nestjs" + ], "cwd": "./", "parallel": false, "outputPath": "{workspaceRoot}/libs/json-api/json-api-nestjs" } - }, - "nx-release-publish": { - "options": { - "packageRoot": "dist/libs/json-api/json-api-nestjs" - } } - }, - "tags": [] + } } diff --git a/libs/json-api/json-api-nestjs/src/index.ts b/libs/json-api/json-api-nestjs/src/index.ts index 1a4e1274..fc0b6e76 100644 --- a/libs/json-api/json-api-nestjs/src/index.ts +++ b/libs/json-api/json-api-nestjs/src/index.ts @@ -1,19 +1,24 @@ export { JsonApiModule } from './lib/json-api.module'; -export { InjectService, JsonApi } from './lib/decorators'; -export { - EntityRelation, - TypeormService as JsonApiService, - ResourceObject, - ResourceObjectRelationships, -} from './lib/types'; -export { JsonBaseController } from './lib/mixin/controller/json-base.controller'; +export { TypeOrmJsonApiModule, MicroOrmJsonApiModule } from './lib/modules'; + +export { JsonApi, InjectService } from './lib/modules/mixin/decorators'; +export { OrmService as JsonApiService } from './lib/modules/mixin/types'; +export { JsonBaseController } from './lib/modules/mixin/controller/json-base.controller'; export { Query, PatchData, PostData, PostRelationshipData, PatchRelationshipData, + QueryOne, +} from './lib/modules/mixin/zod'; + +export { + EntityRelation, + ResourceObject, + ResourceObjectRelationships, QueryField, -} from './lib/helper/zod'; -export { excludeMethod } from './lib/config/bindings'; -export { entityForClass } from './lib/helper/utils'; +} from './lib/utils/nestjs-shared'; + +export { excludeMethod } from './lib/modules/mixin/config/bindings'; +export { entityForClass } from './lib/utils'; diff --git a/libs/json-api/json-api-nestjs/src/lib/constants/default.ts b/libs/json-api/json-api-nestjs/src/lib/constants/default.ts new file mode 100644 index 00000000..de75ca27 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/constants/default.ts @@ -0,0 +1,4 @@ +export const DEFAULT_CONNECTION_NAME = 'default'; + +export const DEFAULT_QUERY_PAGE = 1; +export const DEFAULT_PAGE_SIZE = 20; diff --git a/libs/json-api/json-api-nestjs/src/lib/constants/defaults.ts b/libs/json-api/json-api-nestjs/src/lib/constants/defaults.ts deleted file mode 100644 index 627c7834..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/constants/defaults.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ParseIntPipe } from '@nestjs/common'; -import { ConfigParam } from '../types'; - -export const DESC = 'DESC'; -export const ASC = 'ASC'; - -export const SORT_TYPE = [DESC, ASC] as const; - -export const DEFAULT_QUERY_PAGE = 1; -export const DEFAULT_PAGE_SIZE = 20; -export const DEFAULT_CONNECTION_NAME = 'default'; - -export const TYPEORM_SERVICE_PROPS = Symbol('typeormService'); - -export const SUB_QUERY_ALIAS_FOR_PAGINATION = 'subQueryWithLimitOffset'; -export const ALIAS_FOR_PAGINATION = 'aliasForPagination'; - -export const ConfigParamDefault: ConfigParam = { - debug: true, - requiredSelectField: true, - pipeForId: ParseIntPipe, - useSoftDelete: false, -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/constants/di.ts b/libs/json-api/json-api-nestjs/src/lib/constants/di.ts new file mode 100644 index 00000000..2d70e17d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/constants/di.ts @@ -0,0 +1,32 @@ +export const CURRENT_ENTITY_MANAGER_TOKEN = Symbol( + 'CURRENT_ENTITY_MANAGER_TOKEN' +); +export const GLOBAL_MODULE_OPTIONS_TOKEN = Symbol('GLOBAL_MODULE_OPTIONS'); +export const ORM_SERVICE = Symbol('ORM_SERVICE'); +export const ORM_SERVICE_PROPS = Symbol('ORM_SERVICE_PROPS'); + +export const PARAMS_FOR_ZOD_SCHEMA = Symbol('PARAMS_FOR_ZOD_SCHEMA'); +export const FIELD_FOR_ENTITY = Symbol('FIELD_FOR_ENTITY'); +export const CONTROL_OPTIONS_TOKEN = Symbol('CONTROL_OPTIONS_TOKEN'); +export const RUN_IN_TRANSACTION_FUNCTION = Symbol( + 'RUN_IN_TRANSACTION_FUNCTION' +); + +export const CURRENT_ENTITY = Symbol('CURRENT_ENTITY'); +export const FIND_ONE_ROW_ENTITY = Symbol('FIND_ONE_ROW_ENTITY'); +export const CHECK_RELATION_NAME = Symbol('CHECK_RELATION_NAME'); + +export const ZOD_INPUT_QUERY_SCHEMA = Symbol('ZOD_INPUT_QUERY_SCHEMA'); +export const ZOD_QUERY_SCHEMA = Symbol('ZOD_INPUT_QUERY_SCHEMA'); +export const ZOD_POST_SCHEMA = Symbol('ZOD_POST_SCHEMA'); +export const ZOD_PATCH_SCHEMA = Symbol('ZOD_PATCH_SCHEMA'); +export const ZOD_POST_RELATIONSHIP_SCHEMA = Symbol( + 'ZOD_POST_RELATIONSHIP_SCHEMA' +); +export const ZOD_PATCH_RELATIONSHIP_SCHEMA = Symbol( + 'ZOD_PATCH_RELATIONSHIP_SCHEMA' +); +export const CURRENT_DATA_SOURCE_TOKEN = Symbol('CURRENT_DATA_SOURCE_TOKEN'); +export const CURRENT_ENTITY_REPOSITORY = Symbol('CURRENT_ENTITY_REPOSITORY'); + +export const ENTITY_MAP_PROPS = Symbol('ENTITY_MAP_PROPS'); diff --git a/libs/json-api/json-api-nestjs/src/lib/constants/index.ts b/libs/json-api/json-api-nestjs/src/lib/constants/index.ts index 40c2a13a..83b9d181 100644 --- a/libs/json-api/json-api-nestjs/src/lib/constants/index.ts +++ b/libs/json-api/json-api-nestjs/src/lib/constants/index.ts @@ -1,3 +1,14 @@ -export * from './defaults'; +export * from './default'; +export * from './di'; export * from './reflection'; -export * from './postfix'; + +export const JSON_API_CONTROLLER_POSTFIX = 'JsonApiController'; +export const JSON_API_MODULE_POSTFIX = 'JsonApiModule'; + +export const PARAMS_RESOURCE_ID = 'id'; +export const PARAMS_RELATION_ID = 'relId'; +export const PARAMS_RELATION_NAME = 'relName'; + +export const DESC = 'DESC'; +export const ASC = 'ASC'; +export const SORT_TYPE = [DESC, ASC] as const; diff --git a/libs/json-api/json-api-nestjs/src/lib/constants/postfix.ts b/libs/json-api/json-api-nestjs/src/lib/constants/postfix.ts deleted file mode 100644 index 0086dee1..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/constants/postfix.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const JSON_API_SERVICE_POSTFIX = 'JsonApiService'; -export const CONFIG_PARAM_POSTFIX = 'JsonApiConfigParam'; -export const JSON_API_CONTROLLER_POSTFIX = 'JsonApiController'; -export const JSON_API_MODULE_POSTFIX = 'JsonApiModule'; -export const TYPEORM_UTILS_SERVICE_POSTFIX = 'TypeormUtilsService'; -export const TYPEORM_MIXIN_SERVICE_POSTFIX = 'JsonApiTypeormService'; -export const TRANSFORM_MIXIN_SERVICE_POSTFIX = 'JsonApiTransformService'; diff --git a/libs/json-api/json-api-nestjs/src/lib/constants/reflection.ts b/libs/json-api/json-api-nestjs/src/lib/constants/reflection.ts index 06af80da..6656d32f 100644 --- a/libs/json-api/json-api-nestjs/src/lib/constants/reflection.ts +++ b/libs/json-api/json-api-nestjs/src/lib/constants/reflection.ts @@ -1,24 +1,2 @@ export const JSON_API_DECORATOR_ENTITY = Symbol('JSON_API_ENTITY'); export const JSON_API_DECORATOR_OPTIONS = Symbol('JSON_API_OPTIONS'); -export const GLOBAL_MODULE_OPTIONS_TOKEN = Symbol('GLOBAL_MODULE_OPTIONS'); -export const CURRENT_DATA_SOURCE_TOKEN = Symbol('CURRENT_DATA_SOURCE_TOKEN'); -export const CURRENT_ENTITY_REPOSITORY = Symbol('CURRENT_ENTITY_REPOSITORY'); -export const ZOD_INPUT_QUERY_SCHEMA = Symbol('ZOD_INPUT_QUERY_SCHEMA'); -export const ZOD_QUERY_SCHEMA = Symbol('ZOD_INPUT_QUERY_SCHEMA'); -export const ZOD_POST_SCHEMA = Symbol('ZOD_POST_SCHEMA'); -export const SWAGGER_METHOD = Symbol('SWAGGER_METHOD'); -export const ZOD_POST_RELATIONSHIP_SCHEMA = Symbol( - 'ZOD_POST_RELATIONSHIP_SCHEMA' -); -export const ZOD_PATCH_RELATIONSHIP_SCHEMA = Symbol( - 'ZOD_PATCH_RELATIONSHIP_SCHEMA' -); -export const ZOD_PATCH_SCHEMA = Symbol('ZOD_PATCH_SCHEMA'); -export const TYPEORM_SERVICE = Symbol('TYPEORM_SERVICE'); -export const CONTROL_OPTIONS_TOKEN = Symbol('CONTROL_OPTIONS_TOKEN'); - -export const PARAMS_RESOURCE_ID = 'id'; -export const PARAMS_RELATION_ID = 'relId'; -export const PARAMS_RELATION_NAME = 'relName'; - -export const JSON_API_CONFIG = 'JSON_API_CONFIG'; diff --git a/libs/json-api/json-api-nestjs/src/lib/factory/data-source.factory.ts b/libs/json-api/json-api-nestjs/src/lib/factory/data-source.factory.ts deleted file mode 100644 index 4d284072..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/factory/data-source.factory.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { FactoryProvider } from '@nestjs/common'; -import { DataSource } from 'typeorm'; -import { CURRENT_DATA_SOURCE_TOKEN } from '../constants'; -import { getDataSourceToken } from '@nestjs/typeorm'; - -export function CurrentDataSourceProvider( - connectionName?: string -): FactoryProvider { - return { - provide: CURRENT_DATA_SOURCE_TOKEN, - useFactory: (dataSource: DataSource) => dataSource, - inject: [getDataSourceToken(connectionName)], - }; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/factory/entity-repository.factory.ts b/libs/json-api/json-api-nestjs/src/lib/factory/entity-repository.factory.ts deleted file mode 100644 index 7903f4e6..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/factory/entity-repository.factory.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { FactoryProvider } from '@nestjs/common'; -import { DataSource, Repository } from 'typeorm'; - -import { Entity } from '../types'; -import { - CURRENT_DATA_SOURCE_TOKEN, - CURRENT_ENTITY_REPOSITORY, -} from '../constants'; -import { EntityTarget } from 'typeorm/common/EntityTarget'; - -export function EntityRepositoryFactory( - entity: E -): FactoryProvider> { - return { - provide: CURRENT_ENTITY_REPOSITORY, - useFactory: (dataSource: DataSource) => - dataSource.getRepository(entity as EntityTarget), - inject: [ - { - token: CURRENT_DATA_SOURCE_TOKEN, - optional: false, - }, - ], - }; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/factory/index.ts b/libs/json-api/json-api-nestjs/src/lib/factory/index.ts deleted file mode 100644 index b529e2bf..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/factory/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './zod-validate.factory'; -export * from './data-source.factory'; -export * from './typeorm-service.factory'; -export * from './entity-repository.factory'; -export * from './swagger-bind-method'; diff --git a/libs/json-api/json-api-nestjs/src/lib/factory/swagger-bind-method.ts b/libs/json-api/json-api-nestjs/src/lib/factory/swagger-bind-method.ts deleted file mode 100644 index 0ab682cc..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/factory/swagger-bind-method.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ValueProvider } from '@nestjs/common'; -import { SwaggerMethod, swaggerMethod } from '../helper/swagger/method'; -import { SWAGGER_METHOD } from '../constants'; - -export const SwaggerBindMethod: ValueProvider = { - provide: SWAGGER_METHOD, - useValue: swaggerMethod, -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/factory/typeorm-service.factory.ts b/libs/json-api/json-api-nestjs/src/lib/factory/typeorm-service.factory.ts deleted file mode 100644 index 28304051..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/factory/typeorm-service.factory.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { FactoryProvider } from '@nestjs/common'; -import { DataSource, Repository } from 'typeorm'; - -import { - ConfigParam, - Entity, - TypeormService, - TypeormServiceObject, -} from '../types'; -import { - CONFIG_PARAM_POSTFIX, - TYPEORM_UTILS_SERVICE_POSTFIX, - CURRENT_DATA_SOURCE_TOKEN, - TYPEORM_SERVICE, - CURRENT_ENTITY_REPOSITORY, - CONTROL_OPTIONS_TOKEN, -} from '../constants'; - -import { - getEntityName, - getProviderName, - MethodsService, - ObjectTyped, -} from '../helper'; -import { TypeormUtilsService } from '../service'; -import { TransformDataService } from '../mixin/service'; - -function guardMethodsServiceName( - methodsService: R, - key: any -): asserts key is keyof R { - if (!(key in methodsService)) - throw new Error(`${key} is not methode of MethodsService`); -} - -export function TypeormServiceFactory( - entity: E -): FactoryProvider> { - const entityName = getEntityName(entity as any); - return { - provide: TYPEORM_SERVICE, - inject: [ - { - token: CURRENT_ENTITY_REPOSITORY, - optional: false, - }, - { - token: CONTROL_OPTIONS_TOKEN, - optional: false, - }, - TypeormUtilsService, - TransformDataService, - ], - useFactory: ( - repository: Repository, - config: ConfigParam, - typeormUtilsService: TypeormUtilsService, - transformDataService: TransformDataService - ) => { - const typeOrmObject: TypeormServiceObject = { - repository, - config, - typeormUtilsService, - transformDataService, - }; - - const bindMethods = ObjectTyped.entries(MethodsService).reduce( - (acum, [key, val]) => ({ - ...acum, - [key]: (val as any).bind(typeOrmObject) as typeof val, - }), - {} as typeof MethodsService - ); - - const target = {} as TypeormService; - return new Proxy>(target, { - get(target: {}, p: string | symbol, receiver: any): any { - try { - guardMethodsServiceName(bindMethods, p); - return bindMethods[p]; - } catch (e) { - return undefined; - } - }, - }); - }, - }; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/factory/zod-validate.factory.ts b/libs/json-api/json-api-nestjs/src/lib/factory/zod-validate.factory.ts deleted file mode 100644 index 52952be5..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/factory/zod-validate.factory.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { DataSource, Repository } from 'typeorm'; -import { Entity, ModuleOptions } from '../types'; -import { FactoryProvider, ValueProvider } from '@nestjs/common'; -import { - ZodInputQuerySchema, - zodInputQuerySchema, - ZodQuerySchema, - zodQuerySchema, - zodInputPostSchema, - ZodInputPostSchema, - zodInputPatchSchema, - ZodInputPatchSchema, - ZodInputPostRelationshipSchema as ZodInputPostRelationshipSchemaType, - ZodInputPatchRelationshipSchema as ZodInputPatchRelationshipSchemaType, - zodInputPostRelationshipSchema, - zodInputPatchRelationshipSchema, -} from '../helper/zod'; -import { - CURRENT_DATA_SOURCE_TOKEN, - ZOD_INPUT_QUERY_SCHEMA, - ZOD_POST_SCHEMA, - ZOD_PATCH_SCHEMA, - ZOD_POST_RELATIONSHIP_SCHEMA, - ZOD_PATCH_RELATIONSHIP_SCHEMA, - ZOD_QUERY_SCHEMA, -} from '../constants'; - -import { EntityTarget } from 'typeorm/common/EntityTarget'; - -export function ZodInputQuerySchema( - entity: E -): FactoryProvider> { - return { - provide: ZOD_INPUT_QUERY_SCHEMA, - inject: [ - { - token: CURRENT_DATA_SOURCE_TOKEN, - optional: false, - }, - ], - useFactory: (dataSource: DataSource) => - zodInputQuerySchema( - dataSource.getRepository(entity as EntityTarget) - ), - }; -} - -export function ZodQuerySchema( - entity: E -): FactoryProvider> { - return { - provide: ZOD_QUERY_SCHEMA, - inject: [ - { - token: CURRENT_DATA_SOURCE_TOKEN, - optional: false, - }, - ], - useFactory: (dataSource: DataSource) => - zodQuerySchema(dataSource.getRepository(entity as EntityTarget)), - }; -} - -export function ZodInputPostSchema( - entity: E -): FactoryProvider> { - return { - provide: ZOD_POST_SCHEMA, - inject: [ - { - token: CURRENT_DATA_SOURCE_TOKEN, - optional: false, - }, - ], - useFactory: (dataSource: DataSource) => - zodInputPostSchema( - dataSource.getRepository(entity as EntityTarget) - ), - }; -} - -export function ZodInputPatchSchema( - entity: E -): FactoryProvider> { - return { - provide: ZOD_PATCH_SCHEMA, - inject: [ - { - token: CURRENT_DATA_SOURCE_TOKEN, - optional: false, - }, - ], - useFactory: (dataSource: DataSource) => - zodInputPatchSchema( - dataSource.getRepository(entity as EntityTarget) - ), - }; -} - -export const ZodInputPostRelationshipSchema: ValueProvider = - { - provide: ZOD_POST_RELATIONSHIP_SCHEMA, - useValue: zodInputPostRelationshipSchema, - }; - -export const ZodInputPatchRelationshipSchema: ValueProvider = - { - provide: ZOD_PATCH_RELATIONSHIP_SCHEMA, - useValue: zodInputPatchRelationshipSchema, - }; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/error-database/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/error-database/index.ts deleted file mode 100644 index 07ef1b42..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/error-database/index.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { EntityMetadata } from 'typeorm'; -import { ValidateQueryError } from '../../types'; - -import { formErrorString } from './utils'; -import { - BadRequestException, - ConflictException, - NotAcceptableException, -} from '@nestjs/common'; - -const fieldNotNullOrDefault = ( - entityMetadata: EntityMetadata, - errorText: string, - detail?: string -) => { - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: formErrorString(entityMetadata, errorText), - path: ['data', 'attributes'], - }; - - return new BadRequestException([error]); -}; - -const duplicateItems = ( - entityMetadata: EntityMetadata, - errorText: string, - detail?: string -) => { - errorText = 'Duplicate value'; - if (detail) { - const matches = detail.match(/(?<=\().+?(?=\))/gm); - if (matches) { - errorText = `Duplicate value in the "${matches[0]}"`; - } - } - - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: detail ? formErrorString(entityMetadata, errorText) : errorText, - path: ['data', 'attributes'], - }; - - return new ConflictException([error]); -}; - -const invalidInputSyntax = ( - entityMetadata: EntityMetadata, - errorText: string, - detail?: string -) => { - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: errorText, - path: [], - }; - return new BadRequestException([error]); -}; - -const entityHasRelation = ( - entityMetadata: EntityMetadata, - errorText: string, - detail?: string -) => { - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: detail || errorText, - path: ['data', 'attributes'], - }; - return new NotAcceptableException([error]); -}; - -export const MysqlError = { - [1364]: fieldNotNullOrDefault, - [1062]: duplicateItems, - [1525]: invalidInputSyntax, -}; - -export const PostgresError = { - [23502]: fieldNotNullOrDefault, - [23505]: duplicateItems, - ['22P02']: invalidInputSyntax, - [22007]: invalidInputSyntax, - [22003]: invalidInputSyntax, - [23503]: entityHasRelation, -}; - -export type PostgresErrorCode = keyof typeof PostgresError; -export type MysqlErrorCode = keyof typeof MysqlError; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/error-database/utils.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/error-database/utils.spec.ts deleted file mode 100644 index 7fe1e600..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/error-database/utils.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { formErrorString } from './utils'; - -const metadata = { - tableName: 'users', - name: 'Users', - columns: [ - { - databaseName: 'created_at', - propertyName: 'createdAt', - }, - { - databaseName: 'addresses_id', - propertyName: 'addresses', - }, - ], -} as any; - -describe('utils', () => { - it('formErrorString', () => { - const result = formErrorString( - metadata, - `null value in column "${metadata.columns[1].propertyName}" of relation "${metadata.tableName}" violates not-null constraint` - ); - expect(result).toBe( - `null value in column "${metadata.columns[1].propertyName}" of relation "${metadata.name}" violates not-null constraint` - ); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/error-database/utils.ts b/libs/json-api/json-api-nestjs/src/lib/helper/error-database/utils.ts deleted file mode 100644 index 06e0f01a..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/error-database/utils.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { EntityMetadata } from 'typeorm'; - -export const formErrorString = ( - entityMetadata: EntityMetadata, - errorText: string -) => { - for (const column of entityMetadata.columns) { - const result = new RegExp(column.databaseName).test(errorText); - if (!result) continue; - - errorText = errorText.replace(column.databaseName, column.propertyName); - } - return errorText.replace(entityMetadata.tableName, entityMetadata.name); -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/index.ts deleted file mode 100644 index de801b8a..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from './utils'; -export * from './bind-controller'; -export * from './create-controller'; -export * from './zod/zod-helper'; -export * from './swagger'; -export { MethodsService } from './orm'; -export * from './error-database'; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/index.ts deleted file mode 100644 index b687d4dd..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './orm-helper'; -export * from './orm-type-asserts'; -export * from './methods'; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/index.ts deleted file mode 100644 index 866c347a..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/index.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { getAll } from './get-all/get-all'; -import { getOne } from './get-one/get-one'; -import { deleteOne } from './delete-one/delete-one'; -import { postOne } from './post-one/post-one'; -import { patchOne } from './patch-one/patch-one'; -import { getRelationship } from './get-relationship/get-relationship'; -import { postRelationship } from './post-relationship/post-relationship'; -import { deleteRelationship } from './delete-relationship/delete-relationship'; -import { patchRelationship } from './patch-relationship/patch-relationship'; -import { Entity, EntityRelation } from '../../../types'; - -export const MethodsService = { - getAll, - getOne, - deleteOne, - postOne, - patchOne, - getRelationship, - postRelationship, - deleteRelationship, - patchRelationship, -}; - -export type MethodsService = { - getAll: ( - ...arg: Parameters> - ) => ReturnType>; - getOne: ( - ...arg: Parameters> - ) => ReturnType>; - deleteOne: ( - ...arg: Parameters> - ) => ReturnType>; - postOne: ( - ...arg: Parameters> - ) => ReturnType>; - patchOne: ( - ...arg: Parameters> - ) => ReturnType>; - getRelationship: >( - ...arg: Parameters> - ) => ReturnType>; - postRelationship: >( - ...arg: Parameters> - ) => ReturnType>; - deleteRelationship: >( - ...arg: Parameters> - ) => ReturnType>; - patchRelationship: >( - ...arg: Parameters> - ) => ReturnType>; -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-type-asserts.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-type-asserts.ts deleted file mode 100644 index 779f339d..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-type-asserts.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Entity } from '../../types'; -import { - PropsNameResultField, - PropertyTarget, -} from './orm-helper'; - -export function guardKeyForPropertyTarget< - E extends Entity, - For extends PropsNameResultField, - R extends PropertyTarget ->(relationsTargets: R, key: any): asserts key is keyof R { - if (!(key in relationsTargets)) throw new Error('Type guard error'); -} - -export function guardIsKeyOfObject( - object: R, - key: string | number | symbol -): asserts key is keyof R { - if (typeof object === 'object' && object !== null && key in object) - return void 0; - - throw new Error('Type guard error'); -} - -export function guardIsArray(input: T|Array): input is Array{ - return Array.isArray(input) -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/index.ts deleted file mode 100644 index e0bff43c..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/index.ts +++ /dev/null @@ -1,54 +0,0 @@ -// import { Type } from '@nestjs/common'; -// import { DECORATORS } from '@nestjs/swagger/dist/constants'; -// import { ApiTags, ApiExtraModels } from '@nestjs/swagger'; -// // import { camelToKebab, ObjectTyped } from '../utils'; -// // import { FilterOperand } from './filter-operand-model'; -// // import { JSON_API_DECORATOR_OPTIONS } from '../../constants'; -// // import { DecoratorOptions, Entity, MethodName, ConfigParam } from '../../types'; -// import { Bindings } from '../../config/bindings'; -// import { swaggerMethod } from './method'; -// // import { createApiModels } from './utils'; -// -// import { Entity, ConfigParam } from '../../types'; -// import { ObjectTyped } from '../utils'; -// -// export function setSwaggerDecorator( -// controller: Type, -// entity: Entity, -// config: ConfigParam -// ) { -// // const apiTag = Reflect.getMetadata(DECORATORS.API_TAGS, controller); -// // if (!apiTag) { -// // const entityName = -// // entity instanceof Function ? entity.name : entity.options.name; -// // -// // ApiTags(config?.['overrideRoute'] || `${camelToKebab(entityName)}`)( -// // controller -// // ); -// // } -// // ApiExtraModels(FilterOperand)(controller); -// // ApiExtraModels(createApiModels(entity))(controller); -// // -// // const decoratorOptions: DecoratorOptions = Reflect.getMetadata( -// // JSON_API_DECORATOR_OPTIONS, -// // controller -// // ); -// // -// for (const method of ObjectTyped.keys(Bindings)) { -// // if (decoratorOptions) { -// // const { allowMethod = Object.keys(Bindings) } = decoratorOptions; -// // -// // if (!allowMethod.includes(method as MethodName)) { -// // continue; -// // } -// // } -// // -// if (method in swaggerMethod) { -// swaggerMethod[method](controller, entity, Bindings[method], config); -// } -// } -// } - -export * from './method'; -export * from './filter-operand-model'; -export { createApiModels } from './utils'; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/delete-one.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/delete-one.ts deleted file mode 100644 index 0d0b7f73..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/delete-one.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { ApiParam, ApiOperation, ApiResponse } from '@nestjs/swagger'; -import { ParseIntPipe, Type } from '@nestjs/common'; -import { Repository } from 'typeorm'; - -import { Binding, Entity, ConfigParam } from '../../../types'; -import { errorSchema } from '../utils'; - -export function deleteOne( - controller: Type, - repository: Repository, - binding: Binding<'deleteOne'>, - config: ConfigParam -) { - const entityName = repository.metadata.name; - - const descriptor = Reflect.getOwnPropertyDescriptor(controller, binding.name); - if (!descriptor) - throw new Error(`Descriptor for entity controller ${entityName} is empty`); - - ApiParam({ - name: 'id', - required: true, - type: config.pipeForId === ParseIntPipe ? 'integer' : 'string', - description: `ID of resource "${entityName}"`, - })(controller, binding.name, descriptor); - - ApiOperation({ - summary: `Delete item of resource "${entityName}"`, - operationId: `${controller.constructor.name}_${binding.name}`, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 404, - description: `Item of resource "${entityName}" not found`, - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 204, - description: `Item of resource "${entityName}" has been deleted`, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 400, - description: 'Wrong query parameters', - schema: errorSchema, - })(controller, binding.name, descriptor); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/delete-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/delete-relationship.ts deleted file mode 100644 index f8568432..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/delete-relationship.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { - ApiBody, - ApiExtraModels, - ApiOperation, - ApiParam, - ApiResponse, -} from '@nestjs/swagger'; -import { ParseIntPipe, Type } from '@nestjs/common'; -import { extendApi, generateSchema } from '@anatine/zod-openapi'; -import { createZodDto } from '@anatine/zod-nestjs'; -import { - ReferenceObject, - SchemaObject, -} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; -import { Repository } from 'typeorm'; - -import { Binding, ConfigParam, Entity } from '../../../types'; -import { errorSchema } from '../utils'; -import { getField } from '../../orm'; -import { zodInputPatchRelationshipSchema, zodInputPostSchema } from '../../zod'; - -export function deleteRelationship( - controller: Type, - repository: Repository, - binding: Binding<'deleteRelationship'>, - config: ConfigParam -) { - const entityName = repository.metadata.name; - - const descriptor = Reflect.getOwnPropertyDescriptor( - controller.constructor.prototype, - binding.name - ); - - if (!descriptor) - throw new Error(`Descriptor for entity controller ${entityName} is empty`); - - const { relations } = getField(repository); - - const classBodySchemaDto = createZodDto( - extendApi(zodInputPostSchema(repository)) - ); - Object.defineProperty(classBodySchemaDto, 'name', { - value: `${entityName}DeleteRelationship`, - }); - ApiExtraModels(classBodySchemaDto)(controller.constructor); - - ApiOperation({ - summary: `Delete list of relation for resource "${entityName}"`, - operationId: `${controller.name}_${binding.name}`, - })(controller, binding.name, descriptor); - - ApiParam({ - name: 'id', - required: true, - type: config.pipeForId === ParseIntPipe ? 'integer' : 'string', - description: `ID of resource "${entityName}"`, - })(controller, binding.name, descriptor); - - ApiParam({ - name: 'relName', - required: true, - type: 'string', - enum: relations, - description: `Relation name of resource "${entityName}"`, - })(controller, binding.name, descriptor); - - ApiBody({ - description: `Json api schema for delete "${entityName}" item`, - schema: generateSchema(zodInputPatchRelationshipSchema) as - | SchemaObject - | ReferenceObject, - required: true, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 400, - description: 'Wrong url parameters', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 422, - description: 'Incorrect type for relation', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 404, - description: 'Resource not found ', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 204, - description: `Item/s of relation for "${entityName}" has been deleted`, - })(controller, binding.name, descriptor); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-all.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-all.ts deleted file mode 100644 index c5506eba..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-all.ts +++ /dev/null @@ -1,232 +0,0 @@ -import { ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger'; -import { Type } from '@nestjs/common'; -import { Repository } from 'typeorm'; - -import { DEFAULT_PAGE_SIZE, DEFAULT_QUERY_PAGE } from '../../../constants'; -import { Binding, Entity, ConfigParam } from '../../../types'; -import { ObjectTyped } from '../../utils'; -import { errorSchema, jsonSchemaResponse } from '../utils'; - -import { - fromRelationTreeToArrayName, - getField, - getPrimaryColumnsForRelation, - getPropsTreeForRepository, -} from '../../orm'; - -export function getAll( - controller: Type, - repository: Repository, - binding: Binding<'getAll'>, - config: ConfigParam -): void { - const entityName = repository.metadata.name; - - const descriptor = Reflect.getOwnPropertyDescriptor(controller, binding.name); - if (!descriptor) - throw new Error(`Descriptor for entity controller ${entityName} is empty`); - - const { relations, field } = getField(repository); - const propsTree = getPropsTreeForRepository(repository); - const relationTree = fromRelationTreeToArrayName(propsTree); - const relationPrimaryColum = getPrimaryColumnsForRelation(repository); - const primaryColumn = repository.metadata.primaryColumns[0].propertyName; - - ApiOperation({ - summary: `Get list items of resource "${entityName}"`, - operationId: `${controller.constructor.name}_${binding.name}`, - })(controller, binding.name, descriptor); - - ApiQuery({ - name: 'fields', - required: false, - style: 'deepObject', - schema: { - type: 'object', - }, - examples: { - allField: { - summary: 'Select all field', - description: 'Select field for target and relation', - value: { - target: field.join(','), - ...ObjectTyped.entries(propsTree).reduce((acum, [name, props]) => { - acum[name.toString()] = props.join(','); - return acum; - }, {} as Record), - }, - }, - selectOnlyIdsTarget: { - summary: 'Select ids for target', - description: 'Select ids for target', - value: { - target: field.filter((i) => i === primaryColumn).join(','), - }, - }, - selectOnlyIds: { - summary: 'Select ids', - description: 'Select ids', - value: { - target: field.filter((i) => i === primaryColumn).join(','), - ...ObjectTyped.entries(relationPrimaryColum).reduce( - (acum, [name, props]) => { - acum[name.toString()] = props; - return acum; - }, - {} as Record - ), - }, - }, - }, - description: `Object of field for select field from "${entityName}" resource`, - })(controller, binding.name, descriptor); - - ApiQuery({ - name: 'filter', - required: false, - style: 'deepObject', - schema: { - type: 'object', - }, - examples: { - simpleExample: { - summary: 'Several conditional', - description: 'Get if relation is not null', - value: { - [field[0]]: { - in: '1,2,3', - }, - [field[1]]: { - lt: '1', - }, - [relationTree[0]]: { - eq: 'test', - }, - }, - }, - relationNull: { - summary: 'Get if relation is null', - description: 'Get if relation is null', - value: { - [relations[0]]: { - eq: null, - }, - }, - }, - relationNotNull: { - summary: 'Get if relation is not null', - description: 'Get if relation is not null', - value: { - [relations[0]]: { - ne: null, - }, - }, - }, - getRelationByConditional: { - summary: 'Get if relation field is', - description: 'Get if relation field is', - value: { - [relationTree[0]]: { - eq: 'test', - }, - }, - }, - }, - description: `Object of filter for select items from "${entityName}" resource`, - })(controller, binding.name, descriptor); - ApiQuery({ - name: 'include', - required: false, - enum: relations, - style: 'simple', - isArray: true, - description: `"${entityName}" resource item has been extended with existing relations`, - examples: { - withInclude: { - summary: 'Add all relation', - description: 'Add all realtion', - value: relations, - }, - without: { - summary: 'Without relation', - description: 'Without all realtion', - value: [], - }, - }, - })(controller, binding.name, descriptor); - ApiQuery({ - name: 'sort', - type: 'string', - required: false, - description: `Params for sorting of "${entityName}"`, - examples: { - sortAsc: { - summary: 'Sort field by ASC', - description: 'Sort field by ASC', - value: field[1], - }, - sortDesc: { - summary: 'Sort field by DESC', - description: 'Sort field by DESC', - value: `-${field[1]}`, - }, - sortAscRelation: { - summary: 'Sort field relation by ASC', - description: 'Sort field relation by ASC', - value: relationTree[2], - }, - sortDescRelation: { - summary: 'Sort field relation by DESC', - description: 'Sort field relation by DESC', - value: `-${relationTree[2]}`, - }, - sortSeveral: { - summary: 'Sort several field relation', - description: 'Sort several field relation', - value: `${field[1]},-${relationTree[2]},${relationTree[1]},-${field[0]}`, - }, - }, - })(controller, binding.name, descriptor); - - ApiQuery({ - name: 'page', - style: 'deepObject', - required: false, - schema: { - type: 'object', - properties: { - number: { - type: 'integer', - minimum: 1, - example: DEFAULT_QUERY_PAGE, - }, - size: { - type: 'integer', - minimum: 1, - example: DEFAULT_PAGE_SIZE, - maximum: 500, - }, - }, - additionalProperties: false, - }, - description: `"${entityName}" resource has been limit and offset with this params.`, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 404, - description: `Item of resource "${entityName}" not found`, - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 400, - description: 'Wrong query parameters', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 200, - description: 'Resource list received successfully', - schema: jsonSchemaResponse(repository, true), - })(controller, binding.name, descriptor); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-one.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-one.ts deleted file mode 100644 index 765238fe..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-one.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { ParseIntPipe, Type } from '@nestjs/common'; -import { ApiOperation, ApiParam, ApiQuery, ApiResponse } from '@nestjs/swagger'; -import { Repository } from 'typeorm'; - -import { Binding, Entity, ConfigParam } from '../../../types'; -import { jsonSchemaResponse, errorSchema } from '../utils'; - -import { - getField, - getPropsTreeForRepository, - getPrimaryColumnsForRelation, -} from '../../orm'; -import { ObjectTyped } from '../../utils'; - -export function getOne( - controller: Type, - repository: Repository, - binding: Binding<'getOne'>, - config: ConfigParam -) { - const entityName = repository.metadata.name; - const descriptor = Reflect.getOwnPropertyDescriptor(controller, binding.name); - if (!descriptor) - throw new Error( - `Descriptor for entity controller ${entityName}:${binding.name} is empty` - ); - - const { relations, field } = getField(repository); - const propsTree = getPropsTreeForRepository(repository); - const relationPrimaryColum = getPrimaryColumnsForRelation(repository); - const primaryColumn = repository.metadata.primaryColumns[0].propertyName; - - ApiOperation({ - summary: `Get one item of resource "${entityName}"`, - operationId: `${controller.constructor.name}_${binding.name}`, - })(controller, binding.name, descriptor); - - ApiParam({ - name: 'id', - required: true, - type: config.pipeForId === ParseIntPipe ? 'integer' : 'string', - description: `ID of resource "${entityName}"`, - })(controller, binding.name, descriptor); - - ApiQuery({ - name: 'fields', - required: false, - style: 'deepObject', - schema: { - type: 'object', - }, - examples: { - allField: { - summary: 'Select all field', - description: 'Select field for target and relation', - value: { - target: field.join(','), - ...ObjectTyped.entries(propsTree).reduce((acum, [name, props]) => { - acum[name.toString()] = props.join(','); - return acum; - }, {} as Record), - }, - }, - selectOnlyIdsTarget: { - summary: 'Select ids for target', - description: 'Select ids for target', - value: { - target: field.filter((i) => i === primaryColumn).join(','), - }, - }, - selectOnlyIds: { - summary: 'Select ids', - description: 'Select ids', - value: { - target: field.filter((i) => i === primaryColumn).join(','), - ...ObjectTyped.entries(relationPrimaryColum).reduce( - (acum, [name, props]) => { - acum[name.toString()] = props; - return acum; - }, - {} as Record - ), - }, - }, - }, - description: `Object of field for select field from "${entityName}" resource`, - })(controller, binding.name, descriptor); - ApiQuery({ - name: 'include', - required: false, - enum: relations, - style: 'simple', - isArray: true, - description: `"${entityName}" resource item has been extended with existing relations`, - examples: { - withInclude: { - summary: 'Add all relation', - description: 'Add all realtion', - value: relations, - }, - without: { - summary: 'Without relation', - description: 'Without all realtion', - value: [], - }, - }, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 404, - description: `Item of resource "${entityName}" not found`, - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 400, - description: 'Wrong query parameters', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 200, - description: 'Resource one item received successfully', - schema: jsonSchemaResponse(repository), - })(controller, binding.name, descriptor); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-relationship.ts deleted file mode 100644 index 31d817f4..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-relationship.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { ParseIntPipe, Type } from '@nestjs/common'; -import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; -import { Repository } from 'typeorm'; - -import { Binding, ConfigParam, Entity } from '../../../types'; -import { errorSchema, schemaTypeForRelation } from '../utils'; -import { getField } from '../../orm'; - -export function getRelationship( - controller: Type, - repository: Repository, - binding: Binding<'getRelationship'>, - config: ConfigParam -) { - const entityName = repository.metadata.name; - - const descriptor = Reflect.getOwnPropertyDescriptor(controller, binding.name); - - if (!descriptor) - throw new Error(`Descriptor for entity controller ${entityName} is empty`); - - const { relations } = getField(repository); - - ApiOperation({ - summary: `Get list of relation for resource "${entityName}"`, - operationId: `${controller.constructor.name}_${binding.name}`, - })(controller, binding.name, descriptor); - - ApiParam({ - name: 'id', - required: true, - type: config.pipeForId === ParseIntPipe ? 'integer' : 'string', - description: `ID of resource "${entityName}"`, - })(controller, binding.name, descriptor); - - ApiParam({ - name: 'relName', - required: true, - type: 'string', - enum: relations, - description: `Relation name of resource "${entityName}"`, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 200, - schema: schemaTypeForRelation, - description: `Item/s of relation for "${entityName}" has been created`, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 400, - description: 'Wrong url parameters', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 422, - description: 'Incorrect type for relation', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 404, - description: 'Resource not found ', - schema: errorSchema, - })(controller, binding.name, descriptor); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/index.ts deleted file mode 100644 index 232f6eff..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/index.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { getAll } from './get-all'; -import { getOne } from './get-one'; -import { deleteOne } from './delete-one'; -import { postOne } from './post-one'; -import { patchOne } from './patch-one'; -import { getRelationship } from './get-relationship'; -import { deleteRelationship } from './delete-relationship'; -import { postRelationship } from './post-relationship'; -import { patchRelationship } from './patch-relationship'; -import { MethodName } from '../../../types'; - -export type SwaggerMethod = { - [Key in MethodName]?: any; -}; - -export const swaggerMethod: SwaggerMethod = { - getAll, - getOne, - deleteOne, - postOne, - patchOne, - getRelationship, - deleteRelationship, - postRelationship, - patchRelationship, -}; - -export const errorSchema = { - type: 'object', - properties: { - status: { - type: 'number', - }, - errors: { - type: 'array', - items: { - type: 'object', - properties: { - detail: { - type: 'string', - }, - source: { - type: 'object', - properties: { - parameter: { - type: 'string', - }, - }, - }, - }, - required: ['detail'], - }, - }, - }, -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/patch-one.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/patch-one.ts deleted file mode 100644 index a4abf4aa..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/patch-one.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { - ReferenceObject, - SchemaObject, -} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; -import { - ApiBody, - ApiExtraModels, - ApiOperation, - ApiParam, - ApiResponse, -} from '@nestjs/swagger'; -import { ParseIntPipe, Type } from '@nestjs/common'; -import { extendApi, generateSchema } from '@anatine/zod-openapi'; -import { Repository } from 'typeorm'; - -import { jsonSchemaResponse, errorSchema } from '../utils'; - -import { zodInputPatchSchema, zodInputPostSchema } from '../../zod'; -import { Binding, ConfigParam, Entity } from '../../../types'; -import { createZodDto } from '@anatine/zod-nestjs'; - -export function patchOne( - controller: Type, - repository: Repository, - binding: Binding<'patchOne'>, - config: ConfigParam -) { - const entityName = repository.metadata.name; - - const descriptor = Reflect.getOwnPropertyDescriptor(controller, binding.name); - - if (!descriptor) - throw new Error(`Descriptor for entity controller ${entityName} is empty`); - - const classBodySchemaDto = createZodDto( - extendApi(zodInputPostSchema(repository)) - ); - Object.defineProperty(classBodySchemaDto, 'name', { - value: `${entityName}PatchOne`, - }); - ApiExtraModels(classBodySchemaDto)(controller.constructor); - - ApiOperation({ - summary: `Update item of resource "${entityName}"`, - operationId: `${controller.constructor.name}_${binding.name}`, - })(controller, binding.name, descriptor); - - ApiParam({ - name: 'id', - required: true, - type: config.pipeForId === ParseIntPipe ? 'integer' : 'string', - description: `ID of resource "${entityName}"`, - })(controller, binding.name, descriptor); - - ApiBody({ - description: `Json api schema for update "${entityName}" item`, - schema: generateSchema(zodInputPatchSchema(repository)) as - | SchemaObject - | ReferenceObject, - required: true, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 200, - description: `Item of resource "${entityName}" has been updated`, - schema: jsonSchemaResponse(repository), - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 400, - description: 'Wrong body parameters', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 422, - description: 'Unprocessable data', - schema: errorSchema, - })(controller, binding.name, descriptor); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/patch-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/patch-relationship.ts deleted file mode 100644 index b87b2cfa..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/patch-relationship.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { ParseIntPipe, Type } from '@nestjs/common'; -import { - ApiBody, - ApiExtraModels, - ApiOperation, - ApiParam, - ApiResponse, -} from '@nestjs/swagger'; -import { - ReferenceObject, - SchemaObject, -} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; -import { extendApi, generateSchema } from '@anatine/zod-openapi'; -import { Repository } from 'typeorm'; -import { Binding, ConfigParam, Entity } from '../../../types'; -import { schemaTypeForRelation, errorSchema } from '../utils'; -import { zodInputPatchRelationshipSchema, zodInputPostSchema } from '../../zod'; -import { getField } from '../../orm'; -import { createZodDto } from '@anatine/zod-nestjs'; - -export function patchRelationship( - controller: Type, - repository: Repository, - binding: Binding<'patchRelationship'>, - config: ConfigParam -) { - const entityName = repository.metadata.name; - - const descriptor = Reflect.getOwnPropertyDescriptor(controller, binding.name); - - if (!descriptor) - throw new Error(`Descriptor for entity controller ${entityName} is empty`); - - const { relations } = getField(repository); - - const classBodySchemaDto = createZodDto( - extendApi(zodInputPostSchema(repository)) - ); - Object.defineProperty(classBodySchemaDto, 'name', { - value: `${entityName}PatchRelationship`, - }); - ApiExtraModels(classBodySchemaDto)(controller.constructor); - - ApiOperation({ - summary: `Update list of relation for resource "${entityName}"`, - operationId: `${controller.constructor.name}_${binding.name}`, - })(controller.prototype, binding.name, descriptor); - - ApiParam({ - name: 'id', - required: true, - type: config.pipeForId === ParseIntPipe ? 'integer' : 'string', - description: `ID of resource "${entityName}"`, - })(controller, binding.name, descriptor); - - ApiParam({ - name: 'relName', - required: true, - type: 'string', - enum: relations, - description: `Relation name of resource "${entityName}"`, - })(controller.prototype, binding.name, descriptor); - - ApiBody({ - description: `Json api schema for update "${entityName}" item`, - schema: generateSchema(zodInputPatchRelationshipSchema) as - | SchemaObject - | ReferenceObject, - required: true, - })(controller.prototype, binding.name, descriptor); - - ApiResponse({ - status: 200, - schema: schemaTypeForRelation, - description: `Item/s of relation for "${entityName}" has been updated`, - })(controller.prototype, binding.name, descriptor); - - ApiResponse({ - status: 400, - description: 'Wrong url parameters', - schema: errorSchema, - })(controller.prototype, binding.name, descriptor); - - ApiResponse({ - status: 422, - description: 'Incorrect type for relation', - schema: errorSchema, - })(controller.prototype, binding.name, descriptor); - - ApiResponse({ - status: 404, - description: 'Resource not found ', - schema: errorSchema, - })(controller.prototype, binding.name, descriptor); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/post-one.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/post-one.ts deleted file mode 100644 index 608e0fae..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/post-one.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { - ApiOperation, - ApiResponse, - ApiBody, - ApiExtraModels, -} from '@nestjs/swagger'; - -import { Binding, ConfigParam, Entity } from '../../../types'; -import { jsonSchemaResponse } from '../utils'; - -import { errorSchema } from '../utils'; -import { Type } from '@nestjs/common'; -import { Repository } from 'typeorm'; - -import { generateSchema, extendApi } from '@anatine/zod-openapi'; - -import { zodInputPostSchema } from '../../zod'; -import { - ReferenceObject, - SchemaObject, -} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; -import { createZodDto } from '@anatine/zod-nestjs'; - -export function postOne( - controller: Type, - repository: Repository, - binding: Binding<'postOne'>, - config: ConfigParam -) { - const entityName = repository.metadata.name; - - const descriptor = Reflect.getOwnPropertyDescriptor(controller, binding.name); - - if (!descriptor) - throw new Error(`Descriptor for entity controller ${entityName} is empty`); - - const classBodySchemaDto = createZodDto( - extendApi(zodInputPostSchema(repository)) - ); - - Object.defineProperty(classBodySchemaDto, 'name', { - value: `${entityName}PostOne`, - }); - ApiExtraModels(classBodySchemaDto)(controller.constructor); - - ApiOperation({ - summary: `Create item of resource "${entityName}"`, - operationId: `${controller.constructor.name}_${binding.name}`, - })(controller, binding.name, descriptor); - - ApiBody({ - description: `Json api schema for new "${entityName}" item`, - schema: generateSchema(zodInputPostSchema(repository)) as - | SchemaObject - | ReferenceObject, - required: true, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 201, - description: `Item of resource "${entityName}" has been created`, - schema: jsonSchemaResponse(repository), - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 400, - description: 'Wrong body parameters', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 422, - description: 'Unprocessable data', - schema: errorSchema, - })(controller, binding.name, descriptor); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/post-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/post-relationship.ts deleted file mode 100644 index 6cdeb525..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/post-relationship.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { ParseIntPipe, Type } from '@nestjs/common'; -import { - ApiBody, - ApiExtraModels, - ApiOperation, - ApiParam, - ApiResponse, -} from '@nestjs/swagger'; -import { extendApi, generateSchema } from '@anatine/zod-openapi'; -import { - ReferenceObject, - SchemaObject, -} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; -import { Repository } from 'typeorm'; - -import { getField } from '../../orm'; -import { zodInputPostRelationshipSchema, zodInputPostSchema } from '../../zod'; -import { Binding, ConfigParam, Entity } from '../../../types'; -import { schemaTypeForRelation, errorSchema } from '../utils'; -import { createZodDto } from '@anatine/zod-nestjs'; - -export function postRelationship( - controller: Type, - repository: Repository, - binding: Binding<'postRelationship'>, - config: ConfigParam -) { - const entityName = repository.metadata.name; - - const descriptor = Reflect.getOwnPropertyDescriptor(controller, binding.name); - - if (!descriptor) - throw new Error(`Descriptor for entity controller ${entityName} is empty`); - - const { relations } = getField(repository); - - const classBodySchemaDto = createZodDto( - extendApi(zodInputPostSchema(repository)) - ); - Object.defineProperty(classBodySchemaDto, 'name', { - value: `${entityName}PostRelationship`, - }); - ApiExtraModels(classBodySchemaDto)(controller.constructor); - - ApiParam({ - name: 'id', - required: true, - type: config.pipeForId === ParseIntPipe ? 'integer' : 'string', - description: `ID of resource "${entityName}"`, - })(controller, binding.name, descriptor); - - ApiParam({ - name: 'relName', - required: true, - type: 'string', - enum: relations, - description: `Relation name of resource "${entityName}"`, - })(controller, binding.name, descriptor); - - ApiBody({ - description: `Json api schema for update "${entityName}" item`, - schema: generateSchema(zodInputPostRelationshipSchema) as - | SchemaObject - | ReferenceObject, - required: true, - })(controller, binding.name, descriptor); - - ApiOperation({ - summary: `Create list of relation for resource "${entityName}"`, - operationId: `${controller.constructor.name}_${binding.name}`, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 200, - schema: schemaTypeForRelation, - description: `Item/s of relation for "${entityName}" has been created`, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 400, - description: 'Wrong url parameters', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 422, - description: 'Incorrect type for relation', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 404, - description: 'Resource not found ', - schema: errorSchema, - })(controller, binding.name, descriptor); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/index.ts deleted file mode 100644 index b70e311c..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './zod-helper'; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-helper.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-helper.spec.ts deleted file mode 100644 index 3221b9bc..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-helper.spec.ts +++ /dev/null @@ -1,728 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { IMemoryDb } from 'pg-mem'; -import { z, ZodError } from 'zod'; - -import { - Addresses, - Comments, - createAndPullSchemaBase, - getRepository, - mockDBTestModule, - Notes, - providerEntities, - pullAllData, - pullUser, - Roles, - UserGroups, - Users, -} from '../../mock-utils'; -import { - QueryField, - zodInputQuerySchema, - ZodInputQuerySchema, - zodQuerySchema, - ZodQuerySchema, - zodInputPostSchema, - ZodInputPostSchema, - ZodInputPatchSchema, - zodInputPatchSchema, - zodInputPostRelationshipSchema, - zodInputPatchRelationshipSchema, -} from './zod-helper'; -import { DEFAULT_PAGE_SIZE, DEFAULT_QUERY_PAGE } from '../../constants'; - -const page = { - [QueryField.page]: { - size: DEFAULT_PAGE_SIZE, - number: DEFAULT_QUERY_PAGE, - }, -}; - -describe('zod-helper', () => { - let userRepository: Repository; - let addressesRepository: Repository; - let notesRepository: Repository; - let commentsRepository: Repository; - let rolesRepository: Repository; - let userGroupRepository: Repository; - let db: IMemoryDb; - let zodInputQuerySchemaTest: ZodInputQuerySchema; - type zodInputQuerySchema = z.infer; - type TypeFilterInputQuery = zodInputQuerySchema['filter']; - type TypeIncludeInputQuery = zodInputQuerySchema['include']; - type TypeFieldsInputQuery = zodInputQuerySchema['fields']; - type TypePageInputQuery = zodInputQuerySchema['page']; - - let zodQuerySchemaTest: ZodQuerySchema; - type zodQuerySchema = z.infer; - type TypeFilterQuery = zodQuerySchema['filter']; - type TypeIncludeQuery = zodQuerySchema['include']; - type TypeFieldsQuery = zodQuerySchema['fields']; - type TypePageQuery = zodQuerySchema['page']; - type TypeSortQuery = zodQuerySchema['sort']; - const dataCheckDefault = { - [QueryField.filter]: null, - [QueryField.fields]: null, - [QueryField.include]: null, - [QueryField.sort]: null, - [QueryField.page]: page[QueryField.page], - }; - - let zodInputPostSchemaTest: ZodInputPostSchema; - type zodInputPostSchema = z.infer; - - let zodInputPatchSchemaTest: ZodInputPatchSchema; - type zodInputPatchSchema = z.infer; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [...providerEntities(getDataSourceToken())], - }).compile(); - ({ - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository, - } = getRepository(module)); - - const user = await pullUser(userRepository); - const userWithRelation = await pullAllData( - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository - ); - zodInputQuerySchemaTest = zodInputQuerySchema(userRepository); - zodQuerySchemaTest = zodQuerySchema(userRepository); - zodInputPostSchemaTest = zodInputPostSchema(userRepository); - zodInputPatchSchemaTest = zodInputPatchSchema(userRepository); - }); - - describe('Test input query schema', () => { - it('Empty object is correct', () => { - const check = {}; - const result = zodInputQuerySchemaTest.parse(check); - expect(result).toEqual({ - ...check, - ...page, - }); - }); - - describe('Test filter', () => { - it('Valid schema', () => { - const check1: TypeFilterInputQuery = { - id: '1', - firstName: { - ne: 'dfs', - eq: 'sdf', - }, - isActive: { - in: 'sdf', - }, - }; - const check2: TypeFilterInputQuery = { - addresses: { - eq: 'null', - }, - manager: { - ne: 'null', - }, - }; - - const check3: TypeFilterInputQuery = { - 'addresses.createdAt': 'sdfsdf', - 'comments.createdAt': { - eq: 'sdfsd', - ne: 'sdfsdf', - like: 'sdfsdf', - }, - }; - const arrayCheck = [check1, check2, check3]; - - for (const item of arrayCheck) { - const dataCheck: zodInputQuerySchema = { - filter: item, - ...page, - }; - const result = zodInputQuerySchemaTest.parse(dataCheck); - expect(result).toEqual(dataCheck); - } - }); - it('In Valid schema', () => { - const check1 = {}; - const check2 = { - addresses: { - in: null, - }, - manager: { - like: null, - sdfsdf: 'sdfsdf', - }, - }; - const check3 = { - sdfsdf: 'sdfsdf', - }; - const check4 = { - id: '', - firstName: { - ne: '', - eq: '', - }, - isActive: { - in: '', - }, - }; - const arrayCheck = [check1, check2, check3, check4]; - - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - const dataCheck = { - filter: item, - }; - try { - zodInputQuerySchemaTest.parse(dataCheck); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); - - describe('Test include', () => { - it('Valid schema', () => { - const check1: TypeIncludeInputQuery = 'manager'; - const check2: TypeIncludeInputQuery = 'addresses,roles'; - - const arrayCheck = [check1, check2]; - - for (const item of arrayCheck) { - const dataCheck: zodInputQuerySchema = { - [QueryField.include]: item, - ...page, - }; - const result = zodInputQuerySchemaTest.parse(dataCheck); - expect(result).toEqual(dataCheck); - } - }); - it('In Valid schema', () => { - const check1 = [] as unknown; - const check2 = ['dfsdfsdf', 'addresses']; - const check3 = ['addresses', 'addresses']; - const check4 = {}; - const arrayCheck = [check1, check2, check3, check4]; - - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - const dataCheck = { - include: item, - }; - try { - zodInputQuerySchemaTest.parse(dataCheck); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); - - describe('Test select field', () => { - it('Valid schema', () => { - const check1: TypeFieldsInputQuery = { - target: 'sdfsdf', - manager: 'dfsdfsdf', - }; - const check2: TypeFieldsInputQuery = { - target: 'sdfsdf', - }; - const check3: TypeFieldsInputQuery = { - addresses: 'sdfsdf', - manager: 'sdfsdf', - }; - - const arrayCheck = [check1, check2, check3]; - - for (const item of arrayCheck) { - const dataCheck: zodInputQuerySchema = { - fields: item, - ...page, - }; - const result = zodInputQuerySchemaTest.parse(dataCheck); - expect(result).toEqual(dataCheck); - } - }); - it('In Valid schema', () => { - const check1 = [] as unknown[]; - const check2 = { - addresses: 'sdfsdf', - manager: 'sdfsdf', - otherField: 'dsfsdf', - }; - const check3 = { otherField: 'dsfsdf' }; - const check4 = 'sdfsdf'; - const check5 = {}; - const check6 = 'dssd'; - const arrayCheck = [check1, check2, check3, check4, check5, check6]; - - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - const dataCheck = { - fields: item, - }; - try { - zodInputQuerySchemaTest.parse(dataCheck); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); - - describe('Test page field', () => { - it('Valid schema', () => { - const check1 = page['page']; - const check2 = {}; - const check3 = undefined; - - const arrayCheck = [check1, check2, check3]; - - for (const item of arrayCheck) { - const dataCheck = { - [QueryField.page]: item, - }; - const result = zodInputQuerySchemaTest.parse(dataCheck); - expect(result).toEqual(page); - } - }); - }); - }); - - describe('test query schema', () => { - describe('Test filter', () => { - it('Valid schema', () => { - const check1: TypeFilterQuery = { - target: null, - relation: null, - }; - const check2: TypeFilterQuery = { - target: { - id: { - in: ['123'], - }, - }, - relation: { - addresses: { - arrayField: { - some: ['dsfsdf'], - }, - }, - roles: { - id: { - eq: '123', - }, - }, - }, - }; - const arrayCheck = [check1, check2]; - for (const item of arrayCheck) { - const dataCheck = { - ...dataCheckDefault, - [QueryField.filter]: item, - }; - const result = zodQuerySchemaTest.parse(dataCheck); - expect(result).toEqual(dataCheck); - } - }); - it('Invalid schema', () => { - const check1 = {}; - const check2 = ''; - const check3: unknown[] = []; - const check4 = null; - const check5 = undefined; - const check6 = 1; - const check7 = { - dsfsdf: 'sdf', - }; - const check8 = { - target: null, - }; - const check9 = { - target: {}, - relation: null, - }; - const check10 = { - target: '', - relation: null, - }; - const check11 = { - target: [], - relation: null, - }; - const check12 = { - target: undefined, - relation: null, - }; - const check13 = { - target: 1, - relation: null, - }; - const check14 = { - target: null, - relation: {}, - }; - const check15 = { - target: null, - relation: '', - }; - const check16 = { - target: null, - relation: [], - }; - const check17 = { - target: null, - relation: undefined, - }; - const check18 = { - target: null, - relation: 1, - }; - - const arrayCheck = [ - check1, - check2, - check3, - check4, - check5, - check6, - check7, - check8, - check9, - check10, - check11, - check12, - check13, - check14, - check15, - check16, - check17, - check18, - ]; - - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - const dataCheck = { - ...dataCheckDefault, - [QueryField.filter]: item, - }; - try { - zodQuerySchemaTest.parse(dataCheck); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); - }); - describe('test zodInputPostSchema', () => { - it('should be ok', () => { - const real = 123.123; - const date = new Date(); - const attributes = { - lastName: 'sdfsdf', - isActive: true, - testDate: date.toISOString(), - testReal: [`${real}`], - testArrayNull: null, - }; - const relationships = { - notes: [ - { - type: 'notes', - id: 'dsfsdf', - }, - ], - }; - const check = { - data: { - type: 'users', - attributes, - relationships, - }, - }; - const check2 = { - data: { - type: 'users', - attributes, - }, - }; - const check3 = { - data: { - id: '1', - type: 'users', - attributes, - }, - }; - - const checkResult = { - data: { - type: 'users', - attributes: { - ...attributes, - ['testDate']: date, - testReal: [real], - }, - relationships, - }, - }; - const checkResult2 = { - data: { - type: 'users', - attributes: { - ...attributes, - ['testDate']: date, - testReal: [real], - }, - }, - }; - const checkResult3 = { - data: { - id: '1', - type: 'users', - attributes: { - ...attributes, - ['testDate']: date, - testReal: [real], - }, - }, - }; - - expect(zodInputPostSchemaTest.parse(check)).toEqual(checkResult); - expect(zodInputPostSchemaTest.parse(check2)).toEqual(checkResult2); - expect(zodInputPostSchemaTest.parse(check3)).toEqual(checkResult3); - }); - - it('should be not ok', () => { - const check1 = {}; - const check2 = null; - const check3: unknown[] = []; - const check4 = ''; - const check5 = { - sdf: 'sdf', - }; - const check6 = { - data: {}, - }; - const check7 = { - data: { - type: 'users', - }, - }; - const check8 = { - data: { - type: 'users', - attributes: { - lastName: 'sdfsdf', - isActive: true, - }, - relationships: { - notes: [ - { - type: 'sdfsdf', - id: 'dsfsdf', - }, - ], - }, - }, - }; - const check9 = { - data: { - type: 'users', - attributes: { - lastName: 'sdfsdf', - id: 1, - }, - }, - }; - const arrayCheck = [ - check1, - check2, - check3, - check4, - check5, - check6, - check7, - check8, - check9, - ]; - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - try { - zodInputPostSchemaTest.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); - - describe('test zodInputPatchSchema', () => { - it('should be ok', () => { - const attributes = { - lastName: 'sdfsdf', - isActive: true, - }; - const relationships = { - notes: [], - manager: null, - }; - const check = { - data: { - id: '1', - type: 'users', - attributes, - relationships, - }, - }; - const check2 = { - data: { - id: '1', - type: 'users', - attributes, - }, - }; - const check3 = { - data: { - id: '1', - type: 'users', - relationships, - }, - }; - - expect(zodInputPatchSchemaTest.parse(check)).toEqual(check); - expect(zodInputPatchSchemaTest.parse(check2)).toEqual(check2); - expect(zodInputPatchSchemaTest.parse(check3)).toEqual({ - data: { - id: '1', - type: 'users', - relationships, - attributes: {}, - }, - }); - }); - - it('should be not ok', () => { - const check1 = { - data: { - type: 'users', - attributes: { - lastName: 'sdfsdf', - isActive: true, - }, - }, - }; - const check2 = { - data: { - id: 'sdfsdf', - type: 'users', - attributes: { - lastName: 'sdfsdf', - isActive: true, - }, - }, - }; - const check3 = { - data: { - id: '1', - type: 'users', - attributes: { - id: 1, - isActive: true, - }, - }, - }; - const check4 = { - data: { - id: '1', - type: 'users', - }, - }; - const arrayCheck = [check1, check2, check3, check4]; - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - try { - zodInputPatchSchemaTest.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); - describe('test zodInputPostRelationshipSchema', () => { - const check = { - data: [{ type: 'type', id: 'id' }], - }; - const check1 = { - data: { type: 'type', id: 'id' }, - }; - it('should be ok', () => { - expect(zodInputPostRelationshipSchema.parse(check)).toEqual(check); - expect(zodInputPostRelationshipSchema.parse(check1)).toEqual(check1); - }); - it('should be not ok', () => { - const check = {}; - const check1 = { - data: { type: 'type', id: 'id' }, - tes: 'sdfsdf', - }; - expect.assertions(2); - try { - zodInputPostRelationshipSchema.parse(check); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - try { - zodInputPostRelationshipSchema.parse(check1); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - }); - }); - - describe('test zodInputPatchRelationshipSchema', () => { - const check = { - data: [], - }; - const check1 = { - data: null, - }; - it('should be ok', () => { - expect(zodInputPatchRelationshipSchema.parse(check)).toEqual(check); - expect(zodInputPatchRelationshipSchema.parse(check1)).toEqual(check1); - }); - it('should be not ok', () => { - const check = {}; - const check1 = { - data: { type: 'type', id: 'id' }, - tes: 'sdfsdf', - }; - expect.assertions(2); - try { - zodInputPatchRelationshipSchema.parse(check); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - try { - zodInputPatchRelationshipSchema.parse(check1); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - }); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-helper.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-helper.ts deleted file mode 100644 index a2177f1b..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-helper.ts +++ /dev/null @@ -1,298 +0,0 @@ -import { Repository } from 'typeorm'; -import { z, ZodObject, ZodUnion } from 'zod'; -import { QueryField } from 'json-shared-type'; - -import { - getField, - getPropsTreeForRepository, - fromRelationTreeToArrayName, - ResultGetField, - getArrayPropsForEntity, - getRelationTypeArray, - getRelationTypeName, - getRelationTypePrimaryColumn, - getFieldWithType, - getTypePrimaryColumn, - getTypeForAllProps, - FieldWithType, - getPropsFromDb, -} from '../orm'; -import { Entity } from '../../types'; - -import { - ZodInputQueryShape, - zodSortInputQuerySchema, - zodPageInputQuerySchema, - zodIncludeInputQuerySchema, - zodSelectFieldsInputQuerySchema, - zodFilterInputQuerySchema, -} from './zod-input-query-schema'; - -import { - ZodQueryShape, - zodFilterQuerySchema, - zodSelectFieldsQuerySchema, - zodIncludeQuerySchema, - zodSortQuerySchema, - zodPageQuerySchema, -} from './zod-query-schema'; - -import { - PostShape, - zodAttributesSchema, - zodRelationshipsSchema, - zodTypeSchema, -} from './zod-input-post-schema'; - -import { - PatchShape, - PatchShapeDefault, - zodPatchRelationshipsSchema, -} from './zod-input-patch-schema'; - -import { - postRelationshipSchema, - PostRelationshipSchema, -} from './zod-input-post-relationship-schema'; - -import { - patchRelationshipSchema, - PatchRelationshipSchema, -} from './zod-input-patch-relationship-schema'; - -import { camelToKebab, getEntityName, ObjectTyped } from '../utils'; -import { zodIdSchema } from './zod-input-post-schema/id'; - -export { QueryField }; - -export type ZodInputQuerySchema = ZodObject< - ZodInputQueryShape, - 'strict' ->; - -export type InputQuery = z.infer>; - -export type ZodQuerySchema = ZodObject< - ZodQueryShape, - 'strict' ->; - -export type Query = z.infer>; - -export type TypeInputProps< - E extends Entity, - K extends keyof ZodInputQueryShape -> = z.infer[K]>; - -export type ZodInputPostSchema = ZodObject< - { - data: ZodObject, 'strict'>; - }, - 'strict' ->; - -export type PostData = z.infer>['data']; - -export type ZodInputPatchSchema = ZodObject< - { - data: ZodUnion< - [ - ZodObject, 'strict'>, - ZodObject, 'strict'> - ] - >; - }, - 'strict' ->; - -export type PatchData = z.infer< - ZodInputPatchSchema ->['data']; - -export type ZodInputPostRelationshipSchema = ZodObject< - { - data: PostRelationshipSchema; - }, - 'strict' ->; - -export type PostRelationshipData = - z.infer['data']; - -export type ZodInputPatchRelationshipSchema = ZodObject< - { - data: PatchRelationshipSchema; - }, - 'strict' ->; - -export type PatchRelationshipData = - z.infer['data']; - -export const zodInputQuerySchema = ( - repository: Repository -): ZodInputQuerySchema => { - const { field, relations } = getField(repository); - const relationTree = fromRelationTreeToArrayName( - getPropsTreeForRepository(repository) - ); - - const zodInputQueryShape: ZodInputQueryShape = { - [QueryField.filter]: zodFilterInputQuerySchema( - field, - relations, - relationTree - ).optional(), - [QueryField.fields]: - zodSelectFieldsInputQuerySchema(relations).optional(), - [QueryField.include]: zodIncludeInputQuerySchema.optional(), - [QueryField.sort]: zodSortInputQuerySchema.optional(), - [QueryField.page]: zodPageInputQuerySchema, - }; - return z - .object(zodInputQueryShape) - .strict( - `Query object should contain only allow params: "${Object.keys( - QueryField - ).join('"."')}"` - ); -}; - -export const zodQuerySchema = ( - repository: Repository -): ZodQuerySchema => { - const { field, relations } = getField(repository); - const relationTree = getPropsTreeForRepository(repository); - const propsArray = getArrayPropsForEntity(repository); - const typeProps = getTypeForAllProps(repository); - - const zodQueryShape: ZodQueryShape = { - [QueryField.filter]: zodFilterQuerySchema( - field, - relationTree, - propsArray, - typeProps - ), - [QueryField.fields]: zodSelectFieldsQuerySchema( - field, - relationTree - ).nullable(), - [QueryField.include]: - zodIncludeQuerySchema['relations']>( - relations - ).nullable(), - [QueryField.sort]: zodSortQuerySchema(field, relationTree).nullable(), - [QueryField.page]: zodPageQuerySchema, - }; - - return z.object(zodQueryShape).strict(); -}; - -export const zodInputPostSchema = ( - repository: Repository -): ZodInputPostSchema => { - const relationArrayProps = getRelationTypeArray(repository); - const relationPopsName = getRelationTypeName(repository); - const primaryColumnType = getRelationTypePrimaryColumn(repository); - const primaryType = getTypePrimaryColumn(repository); - const fieldWithType = ObjectTyped.entries(getFieldWithType(repository)) - .filter( - ([key]) => key !== repository.metadata.primaryColumns[0].propertyName - ) - .reduce( - (acum, [key, type]) => ({ - ...acum, - [key]: type, - }), - {} as FieldWithType - ); - const typeName = camelToKebab(getEntityName(repository.target)); - - const propsDb = getPropsFromDb(repository); - - const postShape: PostShape = { - id: zodIdSchema(primaryType).optional(), - type: zodTypeSchema(typeName), - attributes: zodAttributesSchema(fieldWithType, propsDb), - relationships: zodRelationshipsSchema( - relationArrayProps, - relationPopsName, - primaryColumnType - ).optional(), - }; - - return z - .object({ - data: z.object(postShape).strict(), - }) - .strict(); -}; - -export const zodInputPatchSchema = ( - repository: Repository -): ZodInputPatchSchema => { - const relationArrayProps = getRelationTypeArray(repository); - const relationPopsName = getRelationTypeName(repository); - const primaryColumnType = getRelationTypePrimaryColumn(repository); - const primaryType = getTypePrimaryColumn(repository); - - const fieldWithType = ObjectTyped.entries(getFieldWithType(repository)) - .filter( - ([key]) => key !== repository.metadata.primaryColumns[0].propertyName - ) - .reduce( - (acum, [key, type]) => ({ - ...acum, - [key]: type, - }), - {} as FieldWithType - ); - const typeName = camelToKebab(getEntityName(repository.target)); - const propsDb = getPropsFromDb(repository); - - const patchShapeDefault: PatchShapeDefault = { - id: zodIdSchema(primaryType), - type: zodTypeSchema(typeName), - attributes: zodAttributesSchema(fieldWithType, propsDb), - relationships: zodPatchRelationshipsSchema( - relationArrayProps, - relationPopsName, - primaryColumnType - ).optional(), - }; - - const patchShape: PatchShape = { - id: zodIdSchema(primaryType), - type: zodTypeSchema(typeName), - attributes: zodAttributesSchema(fieldWithType, propsDb) - .optional() - .default({} as any), - relationships: zodPatchRelationshipsSchema( - relationArrayProps, - relationPopsName, - primaryColumnType - ), - }; - - return z - .object({ - data: z.union([ - z.object(patchShapeDefault).strict(), - z.object(patchShape).strict(), - ]), - }) - .strict(); -}; - -export const zodInputPostRelationshipSchema: ZodInputPostRelationshipSchema = z - .object({ - data: postRelationshipSchema, - }) - .strict(); - -export const zodInputPatchRelationshipSchema: ZodInputPatchRelationshipSchema = - z - .object({ - data: patchRelationshipSchema, - }) - .strict(); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-relationship-schema/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-relationship-schema/index.spec.ts deleted file mode 100644 index a3cd25bd..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-relationship-schema/index.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { patchRelationshipSchema, PatchRelationshipSchema } from './'; -import { ZodError } from 'zod'; - -describe('zod-input-patch-relationship-schema', () => { - it('should be ok', () => { - const check = { - type: 'type', - id: 'id', - }; - const check1 = [ - { - type: 'type', - id: 'id', - }, - ]; - const check2 = null; - const check3: any[] = []; - expect(patchRelationshipSchema.parse(check)).toEqual(check); - expect(patchRelationshipSchema.parse(check1)).toEqual(check1); - expect(patchRelationshipSchema.parse(check2)).toEqual(check2); - expect(patchRelationshipSchema.parse(check3)).toEqual(check3); - }); - it('should be not ok', () => { - const check = { - asd: 'sdfsdf', - }; - - const check2 = { - sdfs: 'dsfsdf', - type: 'type', - id: 'id', - }; - const check4 = true; - const check5 = 'dsfsdf'; - - const checkArray = [check, check2, check4, check5]; - expect.assertions(checkArray.length); - for (const item of checkArray) { - try { - patchRelationshipSchema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-relationship-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-relationship-schema/index.ts deleted file mode 100644 index ae69387e..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-relationship-schema/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { z, ZodArray, ZodNullable, ZodUnion } from 'zod'; - -import { Data, data } from '../zod-input-post-relationship-schema'; - -export type PatchRelationshipSchema = ZodUnion< - [ZodNullable, ZodArray] ->; -export const patchRelationshipSchema: PatchRelationshipSchema = z.union([ - data.nullable(), - z.array(data), -]); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/index.ts deleted file mode 100644 index 6ce1600b..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Entity } from '../../../types'; -import { ZodAttributesSchema } from '../zod-input-post-schema/attributes'; -import { ZodTypeSchema } from '../zod-input-post-schema/type'; -import { ZodIdSchema } from '../zod-input-post-schema/id'; - -import { ZodDefault, ZodObject, ZodOptional } from 'zod'; -import { - ZodPatchRelationshipsSchema, - zodPatchRelationshipsSchema, -} from './relationships'; - -export type PatchShape = { - id: ZodIdSchema; - type: ZodTypeSchema; - attributes: ZodDefault>>; - relationships: ZodPatchRelationshipsSchema; -}; - -export type PatchShapeDefault = { - id: ZodIdSchema; - type: ZodTypeSchema; - attributes: ZodAttributesSchema; - relationships: ZodOptional>; -}; - -export type ZodPatchData = ZodObject, 'strict'>; -export { zodPatchRelationshipsSchema }; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/relationships.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/relationships.spec.ts deleted file mode 100644 index d04c1f23..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/relationships.spec.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { z, ZodError } from 'zod'; - -import { - zodPatchRelationshipsSchema, - ZodPatchRelationshipsSchema, -} from './relationships'; -import { Users } from '../../../mock-utils'; -import { - RelationPropsType, - RelationPropsTypeName, - RelationPrimaryColumnType, - TypeField, -} from '../../orm'; - -describe('zodRelationshipsSchema', () => { - const relationArrayProps: RelationPropsType = { - roles: true, - userGroup: false, - notes: true, - addresses: false, - comments: true, - manager: false, - }; - const relationPopsName: RelationPropsTypeName = { - roles: 'Roles', - userGroup: 'UserGroups', - notes: 'Notes', - addresses: 'Addresses', - comments: 'Comments', - manager: 'Users', - }; - - const primaryColumnType: RelationPrimaryColumnType = { - roles: TypeField.number, - userGroup: TypeField.number, - notes: TypeField.string, - addresses: TypeField.number, - comments: TypeField.number, - manager: TypeField.number, - }; - - let relationshipsSchema: ZodPatchRelationshipsSchema; - beforeAll(() => { - relationshipsSchema = zodPatchRelationshipsSchema( - relationArrayProps, - relationPopsName, - primaryColumnType - ); - }); - - it('Should be ok', () => { - const check = { - comments: [ - { - type: 'comments', - id: '1', - }, - ], - userGroup: { - type: 'user-groups', - id: '1', - }, - manager: { - type: 'users', - id: '1', - }, - notes: [ - { - type: 'notes', - id: 'id', - }, - ], - }; - const check2 = { - comments: [], - manager: null, - }; - const check3 = { - comments: { - data: [ - { - type: 'comments', - id: '1', - }, - ], - }, - userGroup: { - data: { - type: 'user-groups', - id: '1', - }, - }, - manager: { - data: { - type: 'users', - id: '1', - }, - }, - notes: { - data: [ - { - type: 'notes', - id: 'id', - }, - ], - }, - }; - - expect(relationshipsSchema.parse(check)).toEqual(check); - expect(relationshipsSchema.parse(check2)).toEqual(check2); - expect(relationshipsSchema.parse(check3)).toEqual(check); - }); - - it('should be not ok', () => { - const check1 = {}; - const check2 = ''; - const check3: any[] = []; - const check4 = true; - const check5 = { - sddsf: {}, - }; - const check6 = { - comments: null, - }; - const check7 = { - comments: {}, - }; - const check8 = { - comments: '', - }; - const check9 = { - comments: true, - }; - const check10 = { - comments: [ - { - sdsf: 'sdfsdf', - }, - ], - }; - const check11 = { - comments: [{}], - }; - const check12 = { - manager: {}, - }; - const check13 = { - manager: { - sdfs: 'sdsdf', - }, - }; - const check14 = { - manager: { - id: 'sdsdf', - type: 'users', - }, - }; - const check15 = { - manager: [], - }; - const check16 = { - comments: null, - }; - const arrayCheck = [ - check1, - check2, - check3, - check4, - check5, - check6, - check7, - check8, - check9, - check10, - check11, - check12, - check13, - check14, - check15, - check16, - ]; - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - try { - relationshipsSchema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/relationships.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/relationships.ts deleted file mode 100644 index 4e9e48dd..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/relationships.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { - ZodArray, - ZodEffects, - ZodObject, - ZodOptional, - z, - ZodNullable, -} from 'zod'; -import { Entity, EntityRelation } from '../../../types'; -import { - RelationPrimaryColumnType, - RelationPropsType, - RelationPropsTypeName, -} from '../../orm'; - -import { zodDataSchema, ZodDataSchema } from '../zod-input-post-schema/data'; -import { nonEmptyObject } from '../zod-utils'; -import { ObjectTyped, camelToKebab } from '../../utils'; - -export type DataArray = ZodArray>; - -export type DataItem = ZodOptional< - E extends true ? DataArray : ZodNullable> ->; - -export type ShapeRelationships = { - [K in keyof RelationPropsType]: DataItem[K]>; -}; - -export type ZodPatchRelationshipsSchema = ZodEffects< - ZodObject, 'strict'> ->; -export const zodPatchRelationshipsSchema = ( - relationArrayProps: RelationPropsType, - relationPopsName: RelationPropsTypeName, - primaryColumnType: RelationPrimaryColumnType -): ZodPatchRelationshipsSchema => { - const shape = ObjectTyped.entries(relationArrayProps).reduce( - (acum, [props, value]: [EntityRelation, boolean]) => { - const typeName = camelToKebab(relationPopsName[props]); - const primaryType = primaryColumnType[props]; - const zodDataSchemaObject = zodDataSchema(typeName, primaryType); - const dataItem: DataItem = ( - value ? z.array(zodDataSchemaObject) : zodDataSchemaObject.nullable() - ).optional(); - return { - ...acum, - [props]: z.union([ - dataItem, - z - .object({ data: dataItem }) - .strict() - .refine(nonEmptyObject()) - .transform((i) => { - const { data } = i; - return data; - }), - ]), - }; - }, - {} as ShapeRelationships - ); - return z.object(shape).strict().refine(nonEmptyObject()); -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-relationship-schema/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-relationship-schema/index.spec.ts deleted file mode 100644 index 3fe1aec4..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-relationship-schema/index.spec.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { postRelationshipSchema, PostRelationshipSchema } from './'; -import { ZodError } from 'zod'; - -describe('zod-input-post-relationship-schema', () => { - it('should be ok', () => { - const check = { - type: 'type', - id: 'id', - }; - const check1 = [ - { - type: 'type', - id: 'id', - }, - ]; - expect(postRelationshipSchema.parse(check)).toEqual(check); - expect(postRelationshipSchema.parse(check1)).toEqual(check1); - }); - it('should be not ok', () => { - const check = { - asd: 'sdfsdf', - }; - const check1: any[] = []; - const check2 = { - sdfs: 'dsfsdf', - type: 'type', - id: 'id', - }; - const check3 = null; - const check4 = true; - const check5 = 'dsfsdf'; - - const checkArray = [check, check1, check2, check3, check4, check5]; - expect.assertions(checkArray.length); - for (const item of checkArray) { - try { - postRelationshipSchema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-relationship-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-relationship-schema/index.ts deleted file mode 100644 index 0386b8b1..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-relationship-schema/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { z, ZodArray, ZodObject, ZodString, ZodUnion } from 'zod'; - -export type Data = ZodObject< - { - type: ZodString; - id: ZodString; - }, - 'strict' ->; -export const data: Data = z - .object({ - type: z.string(), - id: z.string(), - }) - .strict(); - -export type PostRelationshipSchema = ZodUnion< - [Data, ZodArray] ->; -export const postRelationshipSchema: PostRelationshipSchema = z.union([ - data, - z.array(data).nonempty(), -]); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/attributes.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/attributes.spec.ts deleted file mode 100644 index 9748a761..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/attributes.spec.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { z, ZodError } from 'zod'; -import { zodAttributesSchema, ZodAttributesSchema } from './attributes'; -import { Addresses, Users } from '../../../mock-utils'; -import { FieldWithType, PropsForField, TypeField } from '../../orm'; - -describe('attributes', () => { - let schemaUsers: ZodAttributesSchema; - let schemaAddresses: ZodAttributesSchema; - type SchemaTypeUsers = z.infer>; - type SchemaTypeAddresses = z.infer>; - beforeAll(() => { - const fieldTypeUsers: FieldWithType = { - id: TypeField.number, - isActive: TypeField.boolean, - firstName: TypeField.string, - createdAt: TypeField.date, - lastName: TypeField.string, - login: TypeField.string, - testDate: TypeField.date, - updatedAt: TypeField.date, - testReal: TypeField.array, - testArrayNull: TypeField.array, - }; - const propsDb: PropsForField = { - id: { type: Number, isArray: false, isNullable: false }, - login: { type: 'varchar', isArray: false, isNullable: false }, - firstName: { type: 'varchar', isArray: false, isNullable: true }, - testReal: { type: 'real', isArray: true, isNullable: false }, - testArrayNull: { type: 'real', isArray: true, isNullable: true }, - lastName: { type: 'varchar', isArray: false, isNullable: true }, - isActive: { type: 'boolean', isArray: false, isNullable: true }, - createdAt: { type: 'timestamp', isArray: false, isNullable: true }, - testDate: { type: 'timestamp', isArray: false, isNullable: true }, - updatedAt: { type: 'timestamp', isArray: false, isNullable: true }, - }; - const fieldTypeAddresses: FieldWithType = { - id: TypeField.number, - arrayField: TypeField.array, - state: TypeField.string, - city: TypeField.string, - createdAt: TypeField.date, - updatedAt: TypeField.date, - country: TypeField.string, - }; - schemaUsers = zodAttributesSchema(fieldTypeUsers, propsDb); - schemaAddresses = zodAttributesSchema( - fieldTypeAddresses, - {} as PropsForField - ); - }); - - it('should be ok', () => { - const check: SchemaTypeUsers = { - isActive: true, - lastName: 'sdsdf', - testReal: [123.123, 123.123], - }; - const check2: SchemaTypeAddresses = { - arrayField: ['test', 'test'], - }; - const date = new Date(); - const check3 = { - testDate: date.toISOString(), - }; - expect(schemaUsers.parse(check)).toEqual(check); - expect(schemaAddresses.parse(check2)).toEqual(check2); - expect(schemaUsers.parse(check3)).toEqual({ - testDate: date, - }); - }); - - it('should be not ok', () => { - const check = { - id: '1', - isActive: 'true', - lastName: 1, - }; - const check2 = { - arrayField: 'test', - }; - expect.assertions(2); - try { - expect(schemaUsers.parse(check)).toEqual(check); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - try { - schemaAddresses.parse(check2); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/attributes.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/attributes.ts deleted file mode 100644 index 58e0d2c4..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/attributes.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { - ZodArray, - ZodBoolean, - ZodEnum, - ZodNumber, - ZodString, - z, - ZodDate, - ZodObject, - ZodEffects, - ZodOptional, - ZodNullable, - ZodType, -} from 'zod'; - -import { Entity } from '../../../types'; -import { - FieldWithType, - PropsFieldItem, - PropsForField, - TypeField, -} from '../../orm'; -import { ObjectTyped } from '../../utils'; -import { nonEmptyObject } from '../zod-utils'; - -const literalSchema = z.union([z.string(), z.number(), z.boolean()]); - -const getZodSchemaForJson = (isNull: boolean) => { - const tmpSchema = isNull ? literalSchema.nullable() : literalSchema; - const jsonSchema: any = z.lazy(() => - z.union([ - tmpSchema, - z.array(jsonSchema.nullable()), - z.record(jsonSchema.nullable()), - ]) - ); - - return jsonSchema; -}; - -type Literal = ReturnType; - -type Json = Literal | { [key: string]: Json } | Json[]; - -type ZodTypeForArray = - | ZodString - | ZodDate - | ZodEffects - | ZodBoolean; -type ZodArrayType = - | ZodArray - | ZodNullable>; - -type TypeMapToZod = { - [TypeField.array]: ZodOptional; - [TypeField.date]: ZodOptional>; - [TypeField.number]: ZodOptional< - | ZodEffects - | ZodNullable> - >; - [TypeField.boolean]: ZodOptional>; - [TypeField.string]: ZodOptional< - | ZodString - | ZodEnum<[string, ...string[]]> - | ZodNullable> - >; - [TypeField.object]: ZodType | ZodNullable>; -}; - -type ZodShapeAttributes = Omit< - { - [K in keyof FieldWithType]: TypeMapToZod[FieldWithType[K]]; - }, - 'id' ->; - -export type ZodAttributesSchema = ZodEffects< - ZodObject, 'strict'> ->; - -function getZodSchemaForArray(props: PropsFieldItem): ZodTypeForArray { - if (!props) return z.string(); - let zodSchema: ZodTypeForArray; - switch (props.type) { - case 'number': - case 'real': - case 'integer': - case 'bigint': - case 'double': - case 'numeric': - case Number: - zodSchema = z.preprocess((x) => Number(x), z.number()); - break; - case 'date': - case Date: - zodSchema = z.coerce.date(); - break; - case 'boolean': - case Boolean: - zodSchema = z.boolean(); - break; - default: - zodSchema = z.string(); - } - - return zodSchema; -} - -export const zodAttributesSchema = ( - fieldWithType: FieldWithType, - propsDb: PropsForField -): ZodAttributesSchema => { - const shape = ObjectTyped.entries(fieldWithType).reduce( - (acum, [props, type]: [keyof FieldWithType, TypeField]) => { - let zodShema: TypeMapToZod[typeof type]; - const propsDbType = propsDb[props]; - switch (type) { - case TypeField.array: { - const tmpSchema = getZodSchemaForArray(propsDbType).array(); - zodShema = ( - propsDbType && propsDbType.isNullable - ? tmpSchema.nullable() - : tmpSchema - ).optional(); - break; - } - case TypeField.date: { - const tmpSchema = z.coerce.date(); - zodShema = ( - propsDbType && propsDbType.isNullable - ? tmpSchema.nullable() - : tmpSchema - ).optional(); - break; - } - case TypeField.number: { - const tmpSchema = z.preprocess((x) => Number(x), z.number()); - zodShema = ( - propsDbType && propsDbType.isNullable - ? tmpSchema.nullable() - : tmpSchema - ).optional(); - break; - } - case TypeField.boolean: { - const tmpSchema = z.boolean(); - zodShema = ( - propsDbType && propsDbType.isNullable - ? tmpSchema.nullable() - : tmpSchema - ).optional(); - break; - } - case TypeField.object: { - zodShema = getZodSchemaForJson(propsDbType.isNullable).optional(); - break; - } - case TypeField.string: { - const tmpSchema = z.string(); - zodShema = ( - propsDbType && propsDbType.isNullable - ? tmpSchema.nullable() - : tmpSchema - ).optional(); - break; - } - } - - return { - ...acum, - [props]: zodShema, - }; - }, - {} as ZodShapeAttributes - ); - - return z.object(shape).strict().refine(nonEmptyObject); -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/data.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/data.ts deleted file mode 100644 index 71608126..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/data.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { z, ZodEffects, ZodObject } from 'zod'; - -import { ZodTypeSchema, zodTypeSchema } from './type'; -import { ZodIdSchema, zodIdSchema } from './id'; -import { TypeForId } from '../../orm'; -import { nonEmptyObject } from '../zod-utils'; - -export type ZodDataSchema = ZodEffects< - ZodObject<{ - id: ZodIdSchema; - type: ZodTypeSchema; - }> ->; -export const zodDataSchema = ( - type: string, - typeId: TypeForId -): ZodDataSchema => - z - .object({ - id: zodIdSchema(typeId), - type: zodTypeSchema(type), - }) - .strict() - .refine(nonEmptyObject); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/id.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/id.ts deleted file mode 100644 index ef734c03..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/id.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { z, ZodString } from 'zod'; -import { TypeField, TypeForId } from '../../orm'; - -const reg = new RegExp('^-?\\d+$'); - -export type ZodIdSchema = ZodString; -export const zodIdSchema = (typeId: TypeForId): ZodIdSchema => { - let idSchema = z.string(); - if (typeId === TypeField.number) { - idSchema = idSchema.regex(reg); - } - - return idSchema; -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/index.ts deleted file mode 100644 index fb6289e0..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ZodObject, ZodOptional } from 'zod'; - -import { Entity } from '../../../types'; - -import { zodAttributesSchema, ZodAttributesSchema } from './attributes'; -import { zodTypeSchema, ZodTypeSchema } from './type'; -import { - zodRelationshipsSchema, - ZodRelationshipsSchema, -} from './relationships'; -import { ZodIdSchema } from './id'; - -export type PostShape = { - id: ZodOptional; - attributes: ZodAttributesSchema; - type: ZodTypeSchema; - relationships: ZodOptional>; -}; - -export type ZodPostData = ZodObject, 'strict'>; - -export { zodAttributesSchema, zodTypeSchema, zodRelationshipsSchema }; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/relationships.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/relationships.spec.ts deleted file mode 100644 index 54604aba..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/relationships.spec.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { z, ZodError } from 'zod'; - -import { - zodRelationshipsSchema, - ZodRelationshipsSchema, -} from './relationships'; -import { Users } from '../../../mock-utils'; -import { - RelationPropsType, - RelationPropsTypeName, - RelationPrimaryColumnType, - TypeField, -} from '../../orm'; - -describe('zodRelationshipsSchema', () => { - const relationArrayProps: RelationPropsType = { - roles: true, - userGroup: false, - notes: true, - addresses: false, - comments: true, - manager: false, - }; - const relationPopsName: RelationPropsTypeName = { - roles: 'Roles', - userGroup: 'UserGroups', - notes: 'Notes', - addresses: 'Addresses', - comments: 'Comments', - manager: 'Users', - }; - - const primaryColumnType: RelationPrimaryColumnType = { - roles: TypeField.number, - userGroup: TypeField.number, - notes: TypeField.string, - addresses: TypeField.number, - comments: TypeField.number, - manager: TypeField.number, - }; - - let relationshipsSchema: ZodRelationshipsSchema; - beforeAll(() => { - relationshipsSchema = zodRelationshipsSchema( - relationArrayProps, - relationPopsName, - primaryColumnType - ); - }); - - it('Should be ok', () => { - const check = { - comments: [ - { - type: 'comments', - id: '1', - }, - ], - userGroup: { - type: 'user-groups', - id: '1', - }, - manager: { - type: 'users', - id: '1', - }, - notes: [ - { - type: 'notes', - id: 'id', - }, - ], - }; - const check2 = { - comments: { - data: [ - { - type: 'comments', - id: '1', - }, - ], - }, - userGroup: { - data: { - type: 'user-groups', - id: '1', - }, - }, - manager: { - data: { - type: 'users', - id: '1', - }, - }, - notes: { - data: [ - { - type: 'notes', - id: 'id', - }, - ], - }, - }; - expect(relationshipsSchema.parse(check)).toEqual(check); - expect(relationshipsSchema.parse(check2)).toEqual(check); - }); - - it('should be not ok', () => { - const check1 = {}; - const check2 = ''; - const check3: any[] = []; - const check4 = true; - const check5 = { - sddsf: {}, - }; - const check6 = { - comments: [], - }; - const check7 = { - comments: {}, - }; - const check8 = { - comments: '', - }; - const check9 = { - comments: true, - }; - const check10 = { - comments: [ - { - sdsf: 'sdfsdf', - }, - ], - }; - const check11 = { - comments: [{}], - }; - const check12 = { - manager: {}, - }; - const check13 = { - manager: { - sdfs: 'sdsdf', - }, - }; - const check14 = { - manager: { - id: 'sdsdf', - type: 'users', - }, - }; - const check15 = { - manager: null, - }; - const check16 = { - manager: [], - }; - const arrayCheck = [ - check1, - check2, - check3, - check4, - check5, - check6, - check7, - check8, - check9, - check10, - check11, - check12, - check13, - check14, - check15, - ]; - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - try { - relationshipsSchema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/relationships.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/relationships.ts deleted file mode 100644 index 3cad63e6..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/relationships.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { ZodArray, ZodEffects, ZodObject, ZodOptional, z } from 'zod'; -import { Entity, EntityRelation } from '../../../types'; -import { - ArrayPropsForEntity, - RelationPrimaryColumnType, - RelationPropsType, - RelationPropsTypeName, -} from '../../orm'; - -import { zodDataSchema, ZodDataSchema } from './data'; -import { nonEmptyObject } from '../zod-utils'; -import { ObjectTyped, camelToKebab } from '../../utils'; - -export type PropsArray = Omit< - ArrayPropsForEntity, - 'target' ->; - -export type DataArray = ZodArray< - ZodDataSchema, - 'atleastone' ->; - -export type DataItem = ZodOptional< - E extends true ? DataArray : ZodDataSchema ->; - -export type ShapeRelationships = { - [K in keyof RelationPropsType]: DataItem[K]>; -}; - -export type ZodRelationshipsSchema = ZodEffects< - ZodObject, 'strict'> ->; -export const zodRelationshipsSchema = ( - relationArrayProps: RelationPropsType, - relationPopsName: RelationPropsTypeName, - primaryColumnType: RelationPrimaryColumnType -): ZodRelationshipsSchema => { - const shape = ObjectTyped.entries(relationArrayProps).reduce( - (acum, [props, value]: [EntityRelation, boolean]) => { - const typeName = camelToKebab(relationPopsName[props]); - const primaryType = primaryColumnType[props]; - const zodDataSchemaObject = zodDataSchema(typeName, primaryType); - const dataItem: DataItem = ( - value ? z.array(zodDataSchemaObject).nonempty() : zodDataSchemaObject - ).optional(); - return { - ...acum, - [props]: z.union([ - dataItem, - z - .object({ data: dataItem }) - .strict() - .refine(nonEmptyObject()) - .transform((i) => { - const { data } = i; - return data; - }), - ]), - }; - }, - {} as ShapeRelationships - ); - - return z.object(shape).strict().refine(nonEmptyObject()); -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/type.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/type.ts deleted file mode 100644 index 84290c0f..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/type.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { z, ZodLiteral } from 'zod'; - -export type ZodTypeSchema = ZodLiteral; -export const zodTypeSchema = (type: T) => z.literal(type); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/filter.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/filter.spec.ts deleted file mode 100644 index bb737d35..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/filter.spec.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { z, ZodError } from 'zod'; -import { - zodFilterFieldSchema, - zodFilterRelationSchema, - zodFilterSchema, -} from './filter'; - -describe('check "filter"', () => { - describe('Check "zodFilterRelationSchema"', () => { - it('Valid check', () => { - const check1: z.infer = { eq: 'null' }; - const check2: z.infer = { ne: 'null' }; - - const arrayCheck = [check1, check2]; - - for (const item of arrayCheck) { - const result = zodFilterRelationSchema.parse(item); - expect(result).toEqual(item); - } - }); - - it('Invalid check', () => { - const check1 = { eq: null, ne: null }; - const check2 = { eq: 123123 }; - const check3 = { eq: '123123' }; - const check4 = { ne: '123123' }; - const check5 = { ne: true }; - const check6 = { ne: 'true' }; - const check7 = { sdfsdf: 'true' }; - - const arrayCheck = [ - check1, - check2, - check3, - check4, - check5, - check6, - check7, - ]; - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - try { - zodFilterRelationSchema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); - - describe('Check "zodFilterFieldSchema:', () => { - it('Valid schema', () => { - const check1: z.infer = { - some: 'sdf', - ne: 'sdfsdf', - }; - const check2: z.infer = { - eq: 'sdf', - ne: 'sdfsdf', - in: 'sdsf', - }; - const check3: z.infer = { - like: 'sdsf', - }; - const check4: z.infer = 'dsfsdf'; - const arrayCheck = [check1, check2, check3, check4]; - for (const item of arrayCheck) { - const result = zodFilterFieldSchema.parse(item); - expect(result).toEqual(item); - } - }); - - it('Invalid schema', () => { - const check1 = { dfd: 'dfsdf' }; - const check2 = { dfd: 'dfsdf', like: 'sdsf' }; - const check3 = {}; - const check4 = [] as any[]; - const check5 = null; - const check6 = ''; - - const arrayCheck = [check1, check2, check3, check4, check5, check6]; - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - try { - zodFilterFieldSchema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); - - describe('Check "zodFilterSchema"', () => { - it('Valid schema', () => { - const check1: z.infer = { - some: 'sdf', - ne: 'sdfsdf', - }; - const check2: z.infer = { - eq: 'sdf', - ne: 'sdfsdf', - in: 'sdsf', - }; - const check3: z.infer = { - like: 'sdsf', - }; - const arrayCheck = [check1, check2, check3]; - for (const item of arrayCheck) { - const result = zodFilterSchema.parse(item); - expect(result).toEqual(item); - } - }); - - it('Invalid schema', () => { - const check1 = { dfd: 'dfsdf' }; - const check2 = { dfd: 'dfsdf', like: 'sdsf' }; - const check3 = {}; - const check4 = { in: '' }; - - const arrayCheck = [check1, check2, check3]; - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - try { - zodFilterSchema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/filter.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/filter.ts deleted file mode 100644 index f1c87d68..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/filter.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { - z, - ZodEffects, - ZodLiteral, - ZodObject, - ZodOptional, - ZodString, - ZodUnion, -} from 'zod'; -import { ZodFilterMap } from './select'; -import { Entity, FilterOperand, ValueOf } from '../../../types'; -import { ObjectTyped } from '../../utils'; -import { - getValidationErrorForStrict, - nonEmptyObject, - oneOf, - stringLongerThan, -} from '../zod-utils'; -import { ConcatRelation, ResultGetField } from '../../orm'; - -export type ZodFilterSchema = ZodEffects< - ZodObject< - { - [key in ValueOf]: ZodOptional< - ZodEffects - >; - }, - 'strict' - > ->; -export const zodFilterSchema: ZodFilterSchema = z - .object( - ObjectTyped.values(FilterOperand).reduce((acum, item) => { - acum[item] = z.string().refine(stringLongerThan()); - return acum; - }, {} as Record>) - ) - .partial() - .strict() - .refine(oneOf(Object.values(FilterOperand)), { - message: `Must have one of: "${Object.values(FilterOperand).join('","')}"`, - }); - -export type ZodFilterFieldSchema = ZodUnion< - [ZodEffects, ZodFilterSchema] ->; -export const zodFilterFieldSchema: ZodFilterFieldSchema = z.union([ - z.string().refine(stringLongerThan()), - zodFilterSchema, -]); - -export type ZodFilterRelationSchema = ZodUnion< - [ - ZodObject<{ - [FilterOperand.eq]: ZodLiteral<'null'>; - }>, - ZodObject<{ - [FilterOperand.ne]: ZodLiteral<'null'>; - }> - ] ->; -export const zodFilterRelationSchema: ZodFilterRelationSchema = z.union([ - z - .object({ - [FilterOperand.eq]: z.literal('null'), - }) - .strict(), - z - .object({ - [FilterOperand.ne]: z.literal('null'), - }) - .strict(), -]); - -export type ZodFilterInputMap = ZodFilterMap< - ResultGetField['field'], - ZodFilterFieldSchema -> & - ZodFilterMap['relations'], ZodFilterRelationSchema> & - ZodFilterMap, ZodFilterFieldSchema>; - -export const getObjectForFilter = < - E extends readonly [string, ...string[]], - R extends boolean ->( - fieldList: E, - isRelation: R -): ZodFilterMap< - E, - R extends true ? ZodFilterRelationSchema : ZodFilterFieldSchema -> => { - return fieldList.reduce( - (acum, item) => ({ - ...acum, - ...{ - [item]: isRelation - ? zodFilterRelationSchema.optional() - : zodFilterFieldSchema.optional(), - }, - }), - {} as ZodFilterMap< - E, - R extends true ? ZodFilterRelationSchema : ZodFilterFieldSchema - > - ); -}; - -export type ZodFilterInputQueryObject = ZodObject< - ZodFilterInputMap, - 'strict' ->; - -export type ZodFilterInputQuerySchema = ZodEffects< - ZodFilterInputQueryObject ->; - -export const zodFilterInputQuerySchema = ( - field: ResultGetField['field'], - relations: ResultGetField['relations'], - relationField: ConcatRelation -): ZodFilterInputQuerySchema => { - const filterMap: ZodFilterInputMap = { - ...getObjectForFilter(field, false), - ...getObjectForFilter(relations, true), - ...getObjectForFilter(relationField, false), - }; - - const zodFilterInputQueryObject: ZodFilterInputQueryObject = z - .object(filterMap) - .strict(getValidationErrorForStrict(Object.keys(filterMap), 'Filter')); - - return zodFilterInputQueryObject.refine(nonEmptyObject(), { - message: 'Validation error: Filter should be not empty', - }); -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/include.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/include.ts deleted file mode 100644 index 6b886e13..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/include.ts +++ /dev/null @@ -1,9 +0,0 @@ -// export { -// ZodIncludeQuerySchema as ZodIncludeInputQuerySchema, -// zodIncludeQuerySchema as zodIncludeInputQuerySchema, -// } from '../zod-query-schema/index'; - -import { z, ZodString } from 'zod'; - -export type ZodIncludeInputQuerySchema = ZodString; -export const zodIncludeInputQuerySchema = z.string().min(1); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/index.ts deleted file mode 100644 index 4189c7ab..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { ZodOptional } from 'zod'; - -import { zodFilterInputQuerySchema, ZodFilterInputQuerySchema } from './filter'; -import { - zodSelectFieldsInputQuerySchema, - ZodSelectFieldsInputQuerySchema, -} from './select'; -import { - zodIncludeInputQuerySchema, - ZodIncludeInputQuerySchema, -} from './include'; -import { zodSortInputQuerySchema, ZodSortInputQuerySchema } from './sort'; -import { zodPageInputQuerySchema, ZodPageInputQuerySchema } from './page'; - -import { Entity } from '../../../types'; -import { QueryField } from '../zod-helper'; - -export type ZodInputQueryShape = { - [QueryField.filter]: ZodOptional>; - [QueryField.fields]: ZodOptional>; - [QueryField.include]: ZodOptional; - [QueryField.sort]: ZodOptional; - [QueryField.page]: ZodPageInputQuerySchema; -}; - -export { - zodIncludeInputQuerySchema, - zodSortInputQuerySchema, - zodPageInputQuerySchema, - zodSelectFieldsInputQuerySchema, - zodFilterInputQuerySchema, -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/page.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/page.ts deleted file mode 100644 index 1d3b4900..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/page.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { - zodPageQuerySchema as zodPageInputQuerySchema, - ZodPageQuerySchema as ZodPageInputQuerySchema, -} from '../zod-query-schema/index'; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/select.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/select.spec.ts deleted file mode 100644 index 2cec1d24..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/select.spec.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { zodSelectFieldsInputQuerySchema } from './select'; -import { z, ZodError } from 'zod'; -import { ResultGetField } from '../../orm'; -import { Users } from '../../../mock-utils'; - -describe('Check "zodSelectFieldsInputQuerySchema"', () => { - const arrayForCheck: ResultGetField['relations'] = [ - 'userGroup', - 'notes', - 'comments', - 'roles', - 'manager', - 'addresses', - ]; - const zodSelectFieldsSchema = - zodSelectFieldsInputQuerySchema(arrayForCheck); - type TypeSelectField = z.infer; - it('Valid schema', () => { - const check1: TypeSelectField = { - target: 'sdfsdfsfd', - notes: 'sdfsdf', - comments: 'sdfsdf', - roles: 'sdfsdf', - }; - const check2: TypeSelectField = { - target: 'sdfsdfsfd', - }; - const check3: TypeSelectField = { - addresses: 'sdfsdf', - manager: 'sdfsdf', - }; - const arrayCheck = [check1, check2, check3]; - for (const item of arrayCheck) { - const result = zodSelectFieldsSchema.parse(item); - expect(result).toEqual(item); - } - }); - it('Invalid schema', () => { - const check1 = {}; - const check2 = { dfd: 'dfsdf', like: 'sdsf' }; - const check3 = null; - const check4 = [] as any[]; - const check5 = 'ssss'; - - const arrayCheck = [check1, check2, check3, check4, check5]; - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - try { - zodSelectFieldsSchema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/select.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/select.ts deleted file mode 100644 index f2386432..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/select.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { - z, - ZodEffects, - ZodObject, - ZodOptional, - ZodString, - ZodTypeAny, -} from 'zod'; - -import { ResultGetField } from '../../orm'; -import { Entity } from '../../../types'; -import { getValidationErrorForStrict, nonEmptyObject } from '../zod-utils'; - -export type ZodSelectFieldsInputQuerySchema = ZodEffects< - ZodObject< - ObjectForSelectField['relations'], ZodString>, - 'strict' - > ->; - -export type ZodFilterMap< - R extends readonly [string, ...string[]], - F extends ZodTypeAny -> = { - [K in R[number]]: ZodOptional; -}; - -type ObjectForSelectField< - R extends readonly [string, ...string[]], - K extends ZodTypeAny -> = { - target: ZodOptional; -} & ZodFilterMap; - -const objectForSelectField = ( - relationList: R -): ObjectForSelectField => { - const relation = relationList.reduce( - (acum, item) => ({ ...acum, ...{ [item]: z.string().optional() } }), - {} as ZodFilterMap - ); - - return { - target: z.string().optional(), - ...relation, - }; -}; - -export const zodSelectFieldsInputQuerySchema = ( - relationList: ResultGetField['relations'] -): ZodSelectFieldsInputQuerySchema => { - const resultShape: ObjectForSelectField< - ResultGetField['relations'], - ZodString - > = objectForSelectField['relations']>(relationList); - - return z - .object(resultShape) - .strict(getValidationErrorForStrict(['target', ...relationList], 'Fields')) - .refine(nonEmptyObject(), { - message: 'Validation error: Need select field for target or relation', - }); -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/sort.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/sort.ts deleted file mode 100644 index 7f44965e..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/sort.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { z, ZodString } from 'zod'; - -export type ZodSortInputQuerySchema = ZodString; -export const zodSortInputQuerySchema: ZodSortInputQuerySchema = z.string(); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/filter.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/filter.spec.ts deleted file mode 100644 index 36f36f9a..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/filter.spec.ts +++ /dev/null @@ -1,483 +0,0 @@ -import { z, ZodError } from 'zod'; - -import { ZodFilterQuerySchema, zodFilterQuerySchema } from './filter'; -import { - AllFieldWithTpe, - PropsArray, - RelationTree, - ResultGetField, - TypeField, -} from '../../orm'; -import { - Users, - Roles, - UserGroups, - Comments, - Notes, - Addresses, -} from '../../../mock-utils'; - -describe('Check "filter" zod schema', () => { - const relation: RelationTree = { - addresses: [ - 'arrayField', - 'country', - 'state', - 'city', - 'updatedAt', - 'createdAt', - 'id', - ], - manager: [ - 'testDate', - 'isActive', - 'lastName', - 'testArrayNull', - 'testReal', - 'firstName', - 'login', - 'updatedAt', - 'createdAt', - 'id', - ], - comments: ['kind', 'text', 'updatedAt', 'createdAt', 'id'], - notes: ['text', 'updatedAt', 'createdAt', 'id'], - roles: ['isDefault', 'key', 'name', 'updatedAt', 'createdAt', 'id'], - userGroup: ['label', 'id'], - }; - const fields: ResultGetField['field'] = [ - 'testDate', - 'isActive', - 'lastName', - 'testArrayNull', - 'testReal', - 'firstName', - 'login', - 'updatedAt', - 'createdAt', - 'id', - ]; - - const usersPropsArray: PropsArray = { - testArrayNull: true, - testReal: true, - }; - const rolesPropsArray: PropsArray = {}; - const userGroupPropsArray: PropsArray = {}; - const commentsPropsArray: PropsArray = {}; - const notesPropsArray: PropsArray = {}; - const addressesPropsArray: PropsArray = { - arrayField: true, - }; - - const propsType: AllFieldWithTpe = { - id: TypeField.number, - login: TypeField.string, - firstName: TypeField.string, - lastName: TypeField.string, - isActive: TypeField.boolean, - createdAt: TypeField.date, - testReal: TypeField.array, - testArrayNull: TypeField.array, - testDate: TypeField.date, - updatedAt: TypeField.date, - addresses: { - id: TypeField.number, - city: TypeField.string, - state: TypeField.string, - country: TypeField.string, - arrayField: TypeField.array, - createdAt: TypeField.date, - updatedAt: TypeField.date, - }, - manager: { - id: TypeField.number, - login: TypeField.string, - firstName: TypeField.string, - lastName: TypeField.string, - testReal: TypeField.array, - testArrayNull: TypeField.array, - isActive: TypeField.boolean, - createdAt: TypeField.date, - testDate: TypeField.date, - updatedAt: TypeField.date, - }, - roles: { - id: TypeField.number, - name: TypeField.string, - key: TypeField.string, - isDefault: TypeField.boolean, - createdAt: TypeField.date, - updatedAt: TypeField.date, - }, - comments: { - id: TypeField.number, - text: TypeField.string, - kind: TypeField.string, - createdAt: TypeField.date, - updatedAt: TypeField.date, - }, - notes: { - id: TypeField.string, - text: TypeField.string, - createdAt: TypeField.date, - updatedAt: TypeField.date, - }, - userGroup: { id: TypeField.number, label: TypeField.string }, - }; - - const filterQuerySchema = zodFilterQuerySchema( - fields, - relation, - { - target: usersPropsArray, - roles: rolesPropsArray, - userGroup: userGroupPropsArray, - comments: commentsPropsArray, - notes: notesPropsArray, - manager: usersPropsArray, - addresses: addressesPropsArray, - }, - propsType - ); - type FilterQuerySchema = z.infer>; - - it('Valid schema', () => { - const check1: FilterQuerySchema = { - target: { - id: { - gte: '1213', - ne: '12', - }, - }, - relation: null, - }; - const check2: FilterQuerySchema = { - target: { - id: { - gte: '1213', - }, - login: { - lt: 'sdfs', - }, - }, - relation: { - addresses: { - arrayField: { - some: ['sdfsdf', 'sdfsdf'], - }, - }, - }, - }; - const check3: FilterQuerySchema = { - target: null, - relation: null, - }; - const check4: FilterQuerySchema = { - target: null, - relation: { - comments: { - id: { - lte: '123', - }, - }, - manager: { - firstName: { - eq: 'sdfsdfsdf', - }, - }, - }, - }; - const check5: FilterQuerySchema = { - target: null, - relation: { - comments: { - id: { - in: ['1'], - }, - }, - manager: { - firstName: { - eq: 'sdfsdfsdf', - }, - }, - }, - }; - const check6: FilterQuerySchema = { - target: { - id: { - gte: '1213', - ne: '123', - }, - addresses: { - eq: 'null', - }, - }, - relation: null, - }; - const check7: FilterQuerySchema = { - target: { - isActive: { - eq: 'true', - }, - }, - relation: null, - }; - const check8: FilterQuerySchema = { - target: { - createdAt: { - eq: '2023-12-08T09:40:58.020Z', - }, - }, - relation: null, - }; - const check9: FilterQuerySchema = { - target: { - createdAt: { - eq: 'null', - }, - }, - relation: null, - }; - - const checkArray = [ - check1, - check2, - check3, - check4, - check5, - check6, - check7, - check8, - ]; - for (const check of checkArray) { - const result = filterQuerySchema.parse(check); - expect(result).toEqual(check); - } - const result = filterQuerySchema.parse(check9); - expect(result.target!.createdAt!.eq).toEqual(null); - result.target!.createdAt!.eq = 'null'; - expect(result).toEqual(check9); - }); - - it('Invalid schema', () => { - const check1 = null; - const check2 = {}; - const check3 = ''; - const check4 = 1; - const check5: any[] = []; - const check6 = { - target: null, - }; - const check7 = { - target: null, - relation: { - commentsasda: { - id: { - lte: 'sdfsdf', - }, - }, - manager: { - firstName: { - eq: 'sdfsdfsdf', - }, - }, - }, - }; - const check8 = { - target: null, - relation: { - comment: { - id: { - lte: 'sdfsdf', - }, - }, - manager: { - firstName: { - eq: 'sdfsdfsdf', - }, - }, - sdfsdf: {}, - }, - }; - const check9 = { - target: { - id: { - gte: '1213', - }, - createdAt: { - lt: 'sdfs', - }, - }, - relation: { - addresses: { - arrayField: { - eq: '1', - }, - }, - }, - }; - const check10 = { - target: { - id: '', - createdAt: { - lt: 'sdfs', - }, - }, - relation: { - addresses: { - arrayField: { - some: ['sdfsdf', 'sdfsdf'], - }, - }, - }, - }; - const check11 = { - target: { - createdAt: { - lt1: 'sdfs', - }, - }, - relation: { - addresses: { - arrayField: { - some: ['sdfsdf', 'sdfsdf'], - }, - }, - }, - }; - const check12 = { - target: { - createdAt: { - in: 'sdfs', - }, - }, - relation: null, - }; - const check13 = { - target: { - sdfsdf: { - eq: 'sdfs', - }, - }, - relation: null, - }; - const check14 = { - target: { - sdfsdf: { - eq: 'sdfs', - }, - }, - relation: { - addresses: { - sdfsdf: { - eq: 'dsfsdf', - }, - }, - }, - }; - const check15 = { - target: null, - relation: { - addresses: {}, - }, - }; - const check16 = { - target: { - id: { - gte: '1213', - ne: 'sdfsfdsf', - }, - addresses: { - eqa: 'null', - }, - }, - relation: null, - }; - const check17 = { - target: { - id: { - gte: '1213', - ne: 'sdfsfdsf', - }, - addresses: { - eq: 'sdfsdf', - }, - }, - relation: null, - }; - const check18 = { - target: { - id: { - gte: 'invalidType', - ne: 'sdfsfdsf', - }, - addresses: { - eq: 'sdfsdf', - }, - }, - relation: null, - }; - const check19 = { - target: null, - relation: { - comments: { - id: { - in: ['dsf'], - }, - }, - manager: { - firstName: { - eq: 'sdfsdfsdf', - }, - }, - }, - }; - const check20 = { - target: { - isActive: { - eq: 'sdfsdf', - }, - }, - }; - const check21: FilterQuerySchema = { - target: { - createdAt: { - eq: 'sdfasd', - }, - }, - relation: null, - }; - const checkArray = [ - check1, - check2, - check3, - check4, - check5, - check6, - check7, - check8, - check9, - check10, - check11, - check12, - check13, - check14, - check15, - check16, - check17, - check18, - check19, - check20, - check21, - ]; - expect.assertions(checkArray.length); - for (const check of checkArray) { - try { - filterQuerySchema.parse(check); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/filter.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/filter.ts deleted file mode 100644 index 99f9c7cb..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/filter.ts +++ /dev/null @@ -1,278 +0,0 @@ -import { - z, - ZodArray, - ZodEffects, - ZodLiteral, - ZodNullable, - ZodObject, - ZodOptional, - ZodString, - ZodUnion, -} from 'zod'; -import { - arrayItemStringLongerThan, - elementOfArrayMustBe, - nonEmptyObject, - oneOf, - stringLongerThan, - stringMustBe, -} from '../zod-utils'; -import { - CastProps, - Entity, - EntityProps, - FilterOperand, - IsArray, - TypeCast, - TypeOfArray, - ValueOf, -} from '../../../types'; -import { ObjectTyped } from '../../utils'; -import { - AllFieldWithTpe, - ArrayPropsForEntity, - guardIsKeyOfObject, - PropsArray, - RelationTree, - ResultGetField, - TypeField, -} from '../../orm'; - -import { - zodFilterRelationSchema, - ZodFilterRelationSchema, -} from '../zod-input-query-schema/filter'; - -type ZodForString = ZodUnion< - [ZodEffects, null, 'null'>, ZodEffects] ->; - -const zodForString: ZodForString = z.union([ - z.literal('null').transform(() => null), - z.string().refine(stringLongerThan(), { - message: 'String should be not empty', - }), -]); - -type ZodForStringArray = ZodEffects< - ZodArray, - [string | null, ...(string | null)[]], - [string, ...string[]] ->; -const zodForStringArray: ZodForStringArray = zodForString - .array() - .nonempty() - .refine(arrayItemStringLongerThan(0), { - message: 'Array should be not empty', - }); - -type OperandForString = Exclude< - ValueOf, - FilterOperand.in | FilterOperand.nin | FilterOperand.some ->; -type OperandForArray = Extract< - ValueOf, - FilterOperand.in | FilterOperand.nin ->; - -type OperandForArrayField = FilterOperand.some; - -type MapOperandForString = { - [K in OperandForString]: ZodOptional; -}; - -type MapOperandForArrayString = { - [K in OperandForArray]: ZodOptional; -}; - -type MapOperand = MapOperandForString & MapOperandForArrayString; -type ZodOperand = ZodEffects>; -type ZodOperandForArrayField = ZodObject< - { - [K in OperandForArrayField]: ZodForStringArray; - }, - 'strict' ->; - -const arrayOperand: { [K in OperandForArray]: boolean } = { - [FilterOperand.nin]: true, - [FilterOperand.in]: true, -}; - -const shapeMapOperand = (type: TypeField = TypeField.string): ZodOperand => - z - .object( - ObjectTyped.entries(FilterOperand) - .filter(([key]) => key !== FilterOperand.some) - .reduce( - (acum, [key, val]) => ({ - ...acum, - [val]: (Object.prototype.hasOwnProperty.call(arrayOperand, key) - ? zodForStringArray.refine(elementOfArrayMustBe(type), { - message: `String should be as ${type}`, - }) - : zodForString.refine(stringMustBe(type), { - message: `String should be as ${type}`, - }) - ).optional(), - }), - {} as MapOperand - ) - ) - .strict() - .refine( - oneOf( - Object.values(FilterOperand).filter((i) => i !== FilterOperand.some) - ), - { - message: `Must have one of: "${Object.values(FilterOperand) - .filter((i) => i !== FilterOperand.some) - .join('","')}"`, - } - ); - -const shapeForArrayField: ZodOperandForArrayField = z - .object({ [FilterOperand.some]: zodForStringArray }) - .strict(); - -type FilterProps = { - [Props in P[number]]: Props extends keyof E - ? IsArray extends true - ? ZodOptional - : ZodOptional - : never; -}; - -type FilterTargetRelation

= { - [Props in P[number]]: ZodOptional; -}; - -const getFilterProps = ( - field: ResultGetField['field'], - propsArray: PropsArray, - propsType: AllFieldWithTpe -): FilterProps['field']> => - field.reduce( - (acum, item) => ({ - ...acum, - [item]: (Reflect.get(propsArray, item) - ? shapeForArrayField - : shapeMapOperand(propsType[item as EntityProps]) - ).optional(), - }), - {} as FilterProps['field']> - ); - -type TargetRelation = FilterTargetRelation< - ResultGetField['relations'] ->; -type TargetProps = FilterProps< - E, - ResultGetField['field'] -> & - TargetRelation; - -type Target = { - target: ZodNullable, 'strict'>>>; -}; - -type RelationType = TypeCast< - TypeOfArray>, - Entity ->; - -type RelationFilter = ZodOptional< - ZodEffects['field']>, 'strict'>> ->; - -type Relation = { - [R in keyof RelationTree]: RelationFilter>; -}; - -type ZodObjectRelation = ZodNullable< - ZodEffects, 'strict'>> ->; - -type ShapeFilter = Target & { - relation: ZodObjectRelation; -}; - -export type ZodFilterQuerySchema = ZodEffects< - ZodObject> ->; - -export const zodFilterQuerySchema = ( - field: ResultGetField['field'], - relationTree: RelationTree, - propsArray: ArrayPropsForEntity, - propsType: AllFieldWithTpe -): ZodFilterQuerySchema => { - const { target: propsArrayTarget, ...relationPropsArray } = propsArray; - - const targetShape: FilterProps['field']> = - getFilterProps(field, propsArrayTarget, propsType); - - const targetRelation = ObjectTyped.keys(relationTree).reduce( - (acum, item) => ({ - ...acum, - [item]: zodFilterRelationSchema.optional(), - }), - {} as TargetRelation - ); - - const targetProps: TargetProps = { - ...targetShape, - ...targetRelation, - }; - - const target: Target = { - target: z.object(targetProps).strict().refine(nonEmptyObject()).nullable(), - }; - - const relationPlaceHolder = ObjectTyped.keys(relationTree).reduce( - (acum, item) => { - acum[item] = undefined; - return acum; - }, - {} as { [K in keyof RelationTree]: undefined } - ); - const relation = ObjectTyped.keys(relationTree).reduce((acum, name) => { - type F = typeof name; - type RT = RelationType; - type RTF = ResultGetField['field']; - - const relationField = relationTree[name] as RTF; - guardIsKeyOfObject(relationPropsArray, name); - - const propsArrayForRelation = relationPropsArray[name] as PropsArray; - - const filterProps = getFilterProps( - relationField, - propsArrayForRelation, - propsType[name] as AllFieldWithTpe - ); - const zodFilter: RelationFilter = z - .object(filterProps) - .strict() - .refine(nonEmptyObject()) - .optional(); - const newName: any = name; - guardIsKeyOfObject(relationPlaceHolder, newName); - - acum[newName] = zodFilter; - return acum; - }, {} as Relation); - - const zodObjectRelation: ZodObjectRelation = z - .object(relation) - .strict() - .refine(nonEmptyObject()) - .nullable(); - - const shapeFilter: ShapeFilter = { - ...target, - ['relation']: zodObjectRelation, - }; - - return z.object(shapeFilter).strict().refine(nonEmptyObject()); -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/include.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/include.spec.ts deleted file mode 100644 index 19f6e82b..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/include.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { zodIncludeQuerySchema } from './include'; -import { ZodError } from 'zod'; - -describe('Check "include" zod schema', () => { - const relations = [ - 'userGroup', - 'notes', - 'comments', - 'roles', - 'manager', - 'addresses', - ] as const; - const zodIncludeQuerySchemaResult = zodIncludeQuerySchema(relations); - - it('Valid schema', () => { - const check1 = ['addresses']; - const result1 = zodIncludeQuerySchemaResult.parse(check1); - expect(result1).toEqual(check1); - - const check2 = ['addresses', 'manager']; - const result2 = zodIncludeQuerySchemaResult.parse(check2); - expect(result2).toEqual(check2); - }); - - it('Invalid schema', () => { - const check1: string[] = []; - const check2: string[] = ['test']; - const check3: string[] = ['addresses', 'manager', 'manager']; - - const checkArray = [check1, check2, check3]; - expect.assertions(checkArray.length); - for (const check of checkArray) { - try { - zodIncludeQuerySchemaResult.parse(check); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/include.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/include.ts deleted file mode 100644 index eedc60e6..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/include.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Writeable, z, ZodArray, ZodEffects, ZodEnum } from 'zod'; -import { uniqueArray } from '../zod-utils'; - -export type ZodIncludeQuerySchema = - ZodEffects< - ZodArray>, 'atleastone'>, - [Writeable[number], ...Writeable[number][]], - [Writeable[number], ...Writeable[number][]] - >; - -export const zodIncludeQuerySchema = ( - relationList: U -): ZodIncludeQuerySchema => - z.enum(relationList).array().nonempty().refine(uniqueArray(), { - message: 'Include should have unique relation', - }); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/index.ts deleted file mode 100644 index 7102e7cb..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { zodFilterQuerySchema, ZodFilterQuerySchema } from './filter'; -import { - zodSelectFieldsQuerySchema, - ZodSelectFieldsQuerySchema, -} from './select'; -import { zodIncludeQuerySchema, ZodIncludeQuerySchema } from './include'; -import { zodSortQuerySchema, ZodSortQuerySchema } from './sort'; -import { zodPageQuerySchema, ZodPageQuerySchema } from './page'; -import { QueryField } from '../zod-helper'; -import { Entity } from '../../../types'; -import { ResultGetField } from '../../orm'; -import { ZodNullable } from 'zod'; - -export { - zodPageQuerySchema, - ZodPageQuerySchema, - zodIncludeQuerySchema, - ZodIncludeQuerySchema, - zodFilterQuerySchema, - ZodFilterQuerySchema, - zodSelectFieldsQuerySchema, - ZodSelectFieldsQuerySchema, - zodSortQuerySchema, - ZodSortQuerySchema, -}; - -export type ZodQueryShape = { - [QueryField.filter]: ZodFilterQuerySchema; - [QueryField.fields]: ZodNullable>; - [QueryField.include]: ZodNullable< - ZodIncludeQuerySchema['relations']> - >; - [QueryField.sort]: ZodNullable>; - [QueryField.page]: ZodPageQuerySchema; -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/page.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/page.spec.ts deleted file mode 100644 index 36a69cba..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/page.spec.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { zodPageQuerySchema } from './page'; -import { ZodError } from 'zod'; -import { DEFAULT_PAGE_SIZE, DEFAULT_QUERY_PAGE } from '../../../constants'; - -describe('Check "page" zod schema', () => { - it('Valid schema', () => { - const defaultPage = { size: DEFAULT_PAGE_SIZE, number: DEFAULT_QUERY_PAGE }; - const check1 = { size: 1, number: 1 }; - const check2 = { size: 1 }; - const check3 = undefined; - const check4 = { number: 1 }; - const check5 = {}; - const check6 = { size: '1', number: '1' }; - const result1 = zodPageQuerySchema.parse(check1); - const result2 = zodPageQuerySchema.parse(check2); - const result3 = zodPageQuerySchema.parse(check3); - const result4 = zodPageQuerySchema.parse(check4); - const result5 = zodPageQuerySchema.parse(check5); - const result6 = zodPageQuerySchema.parse(check6); - expect(result1).toEqual(check1); - expect(result2).toEqual({ - ...defaultPage, - ...check2, - }); - expect(result3).toEqual(defaultPage); - expect(result4).toEqual({ - ...defaultPage, - ...check4, - }); - expect(result5).toEqual(defaultPage); - expect(result6).toEqual(check1); - }); - - it('Invalid schema', () => { - const check1 = { size: 0 }; - const check2 = { size: -1 }; - const check3 = { size: 'sdfsdf' }; - const check4 = { size: -1, number: '21ad' }; - const check5 = { size: -1, number: -1 }; - const check6 = { size: 'sdsad', number: '21ad' }; - const check7 = { size: 1, number: 2, otherProps: 'dsfsdf' }; - const check8 = { size: 1, otherProps: 'dsfsdf' }; - - const checkArray = [ - check1, - check2, - check3, - check4, - check5, - check6, - check7, - check8, - ]; - - expect.assertions(checkArray.length); - for (const check of checkArray) { - try { - zodPageQuerySchema.parse(check1); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/page.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/page.ts deleted file mode 100644 index 3a35c0bf..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/page.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { z, ZodDefault, ZodEffects, ZodNumber, ZodObject } from 'zod'; -import { DEFAULT_PAGE_SIZE, DEFAULT_QUERY_PAGE } from '../../../constants'; - -export type ZodPageQuerySchema = ZodDefault< - ZodObject< - { - size: ZodEffects, number, unknown>; - number: ZodEffects, number, unknown>; - }, - 'strict' - > ->; - -const checkNumber = (a: unknown) => { - if (typeof a === 'string') { - return parseInt(a, 10); - } else if (typeof a === 'number') { - return a; - } else { - return undefined; - } -}; -export const zodPageQuerySchema: ZodPageQuerySchema = z - .object({ - size: z.preprocess( - checkNumber, - z.number().int().min(1).default(DEFAULT_PAGE_SIZE) - ), - number: z.preprocess( - checkNumber, - z.number().int().min(1).default(DEFAULT_QUERY_PAGE) - ), - }) - .strict() - .default({ - size: DEFAULT_PAGE_SIZE, - number: DEFAULT_QUERY_PAGE, - }); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/select.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/select.spec.ts deleted file mode 100644 index e493afe3..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/select.spec.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { RelationTree, ResultGetField } from '../../orm'; -import { Users } from '../../../mock-utils'; -import { - ZodSelectFieldsQuerySchema, - zodSelectFieldsQuerySchema, -} from './select'; -import { z, ZodError } from 'zod'; - -describe('Check "select" zod schema', () => { - const relation: RelationTree = { - addresses: [ - 'arrayField', - 'country', - 'state', - 'city', - 'updatedAt', - 'createdAt', - 'id', - ], - manager: [ - 'testDate', - 'isActive', - 'lastName', - 'testArrayNull', - 'testReal', - 'firstName', - 'login', - 'updatedAt', - 'createdAt', - 'id', - ], - comments: ['kind', 'text', 'updatedAt', 'createdAt', 'id'], - notes: ['text', 'updatedAt', 'createdAt', 'id'], - roles: ['isDefault', 'key', 'name', 'updatedAt', 'createdAt', 'id'], - userGroup: ['label', 'id'], - }; - const fields: ResultGetField['field'] = [ - 'testDate', - 'isActive', - 'lastName', - 'testArrayNull', - 'testReal', - 'firstName', - 'login', - 'updatedAt', - 'createdAt', - 'id', - ]; - - const selectQuerySchema = zodSelectFieldsQuerySchema(fields, relation); - type SelectTypeQuery = z.infer>; - - it('Valid schema', () => { - const check1: SelectTypeQuery = { - target: ['id', 'createdAt', 'isActive'], - userGroup: ['label'], - roles: ['createdAt'], - }; - const check2: SelectTypeQuery = { - userGroup: ['label'], - roles: ['createdAt'], - }; - const check3: SelectTypeQuery = { - addresses: ['city'], - }; - const checkArray = [check1, check2, check3]; - for (const check of checkArray) { - const result = selectQuerySchema.parse(check); - expect(result).toEqual(check); - } - }); - - it('Invalid schema', () => { - const check1 = {}; - const check2 = null; - const check3 = ''; - const check4: [] = []; - const check5 = { - target: ['id', 'createdAt', 'isActive'], - userGroup1: ['label'], - roles: ['createdAt', 'createdAt'], - }; - const check6 = { - target1: ['id', 'createdAt', 'isActive'], - userGroup: ['label'], - roles: ['createdAt', 'createdAt'], - }; - const check7 = { - target: ['id1', 'createdAt', 'isActive'], - userGroup: ['label'], - }; - const check8 = { - target: ['id1', 'createdAt', 'isActive'], - userGroup: ['label'], - roles: ['createdAt1', 'createdAt'], - }; - const check9 = { - target: '', - userGroup: ['label'], - }; - const check10 = { - target: {}, - userGroup: ['label'], - }; - const check11 = { - target: [], - userGroup: ['label'], - }; - const check12 = { - target: '', - userGroup: ['label'], - }; - const check13 = { - target: null, - userGroup: ['label'], - }; - const checkArray = [ - check1, - check2, - check3, - check4, - check5, - check6, - check7, - check8, - check9, - check10, - check11, - check12, - check13, - ]; - expect.assertions(checkArray.length); - for (const check of checkArray) { - try { - selectQuerySchema.parse(check); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/select.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/select.ts deleted file mode 100644 index ca3690be..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/select.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { - Writeable, - z, - ZodArray, - ZodEffects, - ZodEnum, - ZodObject, - ZodOptional, -} from 'zod'; - -import { Entity } from '../../../types'; -import { RelationTree, ResultGetField } from '../../orm'; -import { ObjectTyped } from '../../utils'; -import { nonEmptyObject, uniqueArray } from '../zod-utils'; - -type ZodField = ZodEffects< - ZodArray>, 'atleastone'>, - [Writeable[number], ...Writeable[number][]], - [Writeable[number], ...Writeable[number][]] ->; - -const zodFields = ( - fields: R -): ZodField => - z.enum(fields).array().nonempty().refine(uniqueArray(), { - message: 'Field should be unique', - }); - -type ZodTarget = { - target: ZodOptional>; -}; - -export const zodTarget = ( - fields: R -): ZodTarget => ({ - target: zodFields([...fields]).optional(), -}); - -type ZodRelation = { - [K in keyof RelationTree]: ZodOptional[K]>>; -}; - -type ZodResultShape = ZodTarget['field']> & - ZodRelation; - -export type ZodSelectFields = ZodEffects< - ZodObject, 'strict'> ->; - -export type ZodSelectFieldsQuerySchema = ZodEffects< - ZodSelectFields ->; -export const zodSelectFieldsQuerySchema = ( - fields: ResultGetField['field'], - relationList: RelationTree -): ZodSelectFieldsQuerySchema => { - const target: ZodTarget['field']> = zodTarget(fields); - - const relation = ObjectTyped.entries(relationList).reduce( - (acum, [key, val]) => { - acum[key] = zodFields(val).optional(); - return acum; - }, - {} as ZodRelation - ); - - const resultShape: ZodResultShape = { - ...target, - ...relation, - }; - const zodSelectFields = z - .object(resultShape) - .strict('Should be only target of relation '); - // @ts-ignore - return zodSelectFields.refine(nonEmptyObject(), { - message: 'Validation error: Select target or relation fields', - }) as unknown as ZodSelectFieldsQuerySchema; -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/sort.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/sort.spec.ts deleted file mode 100644 index cfb82568..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/sort.spec.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { z, ZodError } from 'zod'; - -import { zodSortQuerySchema, ZodSortQuerySchema } from './sort'; -import { RelationTree, ResultGetField } from '../../orm'; -import { Users } from '../../../mock-utils'; - -describe('Check "sort" zod schema', () => { - const relation: RelationTree = { - addresses: [ - 'arrayField', - 'country', - 'state', - 'city', - 'updatedAt', - 'createdAt', - 'id', - ], - manager: [ - 'testDate', - 'isActive', - 'lastName', - 'testArrayNull', - 'testReal', - 'firstName', - 'login', - 'updatedAt', - 'createdAt', - 'id', - ], - comments: ['kind', 'text', 'updatedAt', 'createdAt', 'id'], - notes: ['text', 'updatedAt', 'createdAt', 'id'], - roles: ['isDefault', 'key', 'name', 'updatedAt', 'createdAt', 'id'], - userGroup: ['label', 'id'], - }; - const fields: ResultGetField['field'] = [ - 'testDate', - 'isActive', - 'lastName', - 'testArrayNull', - 'testReal', - 'firstName', - 'login', - 'updatedAt', - 'createdAt', - 'id', - ]; - - const sortQuerySchema = zodSortQuerySchema(fields, relation); - type SortTypeQuery = z.infer>; - it('Valid schema', () => { - const check1: SortTypeQuery = { - target: { - id: 'DESC', - createdAt: 'ASC', - }, - }; - const check2: SortTypeQuery = { - comments: { - kind: 'ASC', - createdAt: 'ASC', - }, - roles: { - id: 'ASC', - }, - }; - const checkArray = [check1, check2]; - for (const check of checkArray) { - const result = sortQuerySchema.parse(check); - expect(result).toEqual(check); - } - }); - it('Invalid schema', () => { - const check1 = ''; - const check2 = null; - const check3: string[] = ['test']; - const check4: string[] = ['addresses', 'manager', 'manager']; - const check5 = {}; - const check6 = { - sdfsf: null, - }; - const check7 = { - sdfsf: null, - sfdsdf: null, - }; - - const check8 = { - comments: {}, - }; - const check9 = { - comments: null, - }; - const check10 = { - comments: 's', - }; - const check11 = { - comments: [], - }; - const check12 = { - comments: { - kindsdfsdf: 'ASC', - }, - }; - const check13 = { - comments: { - kind: '', - }, - }; - const check14 = { - comments: { - kind: 'sdfsdf', - }, - }; - const check15 = { - comments: { - kind: [], - }, - }; - const check16 = { - comments: { - kind: {}, - }, - }; - const check17 = { - target: {}, - }; - const check18 = { - target: null, - }; - const check19 = { - target: 's', - }; - const check20 = { - target: [], - }; - const check21 = { - target: { - idsdfsdf: 'ASC', - }, - }; - const check22 = { - target: { - id: '', - }, - }; - const check23 = { - target: { - id: 'sdfsdf', - }, - }; - const check24 = { - target: { - id: [], - }, - }; - const check25 = { - target: { - id: {}, - }, - }; - - const checkArray = [ - check1, - check2, - check3, - check4, - check5, - check6, - check7, - check8, - check9, - check10, - check11, - check12, - check13, - check14, - check15, - check16, - check17, - check18, - check19, - check20, - check21, - check22, - check23, - check24, - check25, - ]; - expect.assertions(checkArray.length); - for (const check of checkArray) { - try { - sortQuerySchema.parse(check); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/sort.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/sort.ts deleted file mode 100644 index a2d89c06..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/sort.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { Entity } from '../../../types'; -import { z, ZodEffects, ZodEnum, ZodObject, ZodOptional } from 'zod'; -import { RelationTree, ResultGetField } from '../../orm'; -import { SORT_TYPE } from '../../../constants'; -import { nonEmptyObject } from '../zod-utils'; - -import { ObjectTyped } from '../../utils'; - -export type SortTarget = { - target: SortEntityOptionalObject; -}; - -export const sortTarget = ( - fields: R -): SortTarget => ({ - target: z.object(sortEntity(fields)).strict().refine(nonEmptyObject()), -}); - -export const sortEntity = ( - fields: R -): SortEntityOptional => - fields.reduce( - (acum, item) => ({ - ...acum, - ...{ - [item]: z.enum(SORT_TYPE).optional(), - }, - }), - {} as SortEntityOptional - ); - -export type SortEnum = ZodEnum<['DESC', 'ASC']>; - -export type SortEntityOptional = { - [K in F[number]]: ZodOptional; -}; - -export type SortEntityOptionalObject = - ZodEffects, 'strict'>>; - -export type SortRelation = { - [k in keyof RelationTree]: SortEntityOptionalObject[k]>; -}; - -export type Sort = SortTarget['field']> & - SortRelation; - -export type OptionalSort = { - [k in keyof Sort]: ZodOptional[k]>; -}; - -type ZodBaseObjectSchema = ZodObject, 'strict'>; -type OptionalZodBaseObjectSchema = ZodObject< - OptionalSort, - 'strict' ->; -export type ZodSortQuerySchema = ZodEffects< - OptionalZodBaseObjectSchema ->; - -export const zodSortQuerySchema = ( - fields: ResultGetField['field'], - relation: RelationTree -): ZodSortQuerySchema => { - const sortTargetObject = sortTarget(fields); - - const sortRelationObject = ObjectTyped.entries(relation).reduce( - (acum, [item, fields]) => { - return { - ...acum, - ...{ - [item]: z - .object(sortEntity(fields)) - .strict() - .refine(nonEmptyObject()), - }, - }; - }, - {} as SortRelation - ); - - const sortMerge: Sort = { - ...sortTargetObject, - ...sortRelationObject, - }; - - const baseZodSchema: ZodBaseObjectSchema = z.object(sortMerge).strict(); - - const partialZod = baseZodSchema.partial() as OptionalZodBaseObjectSchema; - - return partialZod.refine( - nonEmptyObject() - ) as unknown as ZodSortQuerySchema; -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-utils.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-utils.spec.ts deleted file mode 100644 index a9bc2bb6..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-utils.spec.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { - oneOf, - stringLongerThan, - arrayItemStringLongerThan, - uniqueArray, - nonEmptyObject, -} from './zod-utils'; - -describe('zod-utils', () => { - let checkFunk: (arg: any) => boolean; - describe('oneOf', () => { - beforeAll(() => { - checkFunk = oneOf(['props', 'props1', 'props2']); - }); - - it('Should be ok', () => { - const result1 = checkFunk({ props: 1 }); - const result2 = checkFunk({ props: 1, other: 1 }); - const result3 = checkFunk({ props2: 1, other: 1 }); - expect(result1).toBe(true); - expect(result2).toBe(true); - expect(result3).toBe(true); - }); - - it('Should be not ok', () => { - const result1 = checkFunk({}); - const result2 = checkFunk({ other: 1 }); - const result3 = checkFunk({ props4: 1, other: 1 }); - expect(result1).toBe(false); - expect(result2).toBe(false); - expect(result3).toBe(false); - }); - }); - - describe('stringLongerThan', () => { - beforeAll(() => { - checkFunk = stringLongerThan(5); - }); - it('Should be ok', () => { - const result = checkFunk('123456'); - expect(result).toBe(true); - }); - - it('Should be not ok', () => { - const result = checkFunk('12345'); - const result2 = checkFunk('1234'); - expect(result).toBe(false); - expect(result2).toBe(false); - }); - }); - - describe('arrayItemStringLongerThan', () => { - beforeAll(() => { - checkFunk = arrayItemStringLongerThan(5); - }); - it('Should be ok', () => { - const result = checkFunk(['123456', '123456']); - expect(result).toBe(true); - }); - - it('Should be not ok', () => { - const result = checkFunk(['12345', '123456']); - const result2 = checkFunk(['1234', '123456']); - const result3 = checkFunk(['123456', '1234']); - expect(result).toBe(false); - expect(result2).toBe(false); - expect(result3).toBe(false); - }); - }); - - describe('uniqueArray', () => { - beforeAll(() => { - checkFunk = uniqueArray(); - }); - it('Should be ok', () => { - const result = checkFunk(['1', '2', '3', '4']); - const result2 = checkFunk([1, 2, 3, 4]); - expect(result).toBe(true); - expect(result2).toBe(true); - }); - - it('Should be not ok', () => { - const result = checkFunk(['1', '1', '123456']); - const result2 = checkFunk(['1', '123456', '1']); - const result3 = checkFunk([1, '1234', 1]); - expect(result).toBe(false); - expect(result2).toBe(false); - expect(result3).toBe(false); - }); - }); - - describe('nonEmptyObject', () => { - beforeAll(() => { - checkFunk = nonEmptyObject(); - }); - it('Should be ok', () => { - const result = checkFunk({ test: 1 }); - expect(result).toBe(true); - }); - - it('Should be not ok', () => { - const result = checkFunk({}); - const result2 = checkFunk(undefined); - expect(result).toBe(false); - expect(result2).toBe(false); - }); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/json-api-nestjs-common.module.ts b/libs/json-api/json-api-nestjs/src/lib/json-api-nestjs-common.module.ts deleted file mode 100644 index 641fb5c3..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/json-api-nestjs-common.module.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { DynamicModule, Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; - -import { ModuleOptions } from './types'; -import { GLOBAL_MODULE_OPTIONS_TOKEN } from './constants'; -import { CurrentDataSourceProvider, SwaggerBindMethod } from './factory'; -import { TransformInputService, EntityPropsMapService } from './service'; - -@Module({}) -export class JsonApiNestJsCommonModule { - static forRoot(options: ModuleOptions): DynamicModule { - const optionProvider = { - provide: GLOBAL_MODULE_OPTIONS_TOKEN, - useValue: options, - }; - - const currentDataSourceProvider = CurrentDataSourceProvider( - options.connectionName - ); - - const typeOrmModule = TypeOrmModule.forFeature( - options.entities, - options.connectionName - ); - - return { - module: JsonApiNestJsCommonModule, - imports: [typeOrmModule, ...(options.imports || [])], - providers: [ - ...(options.providers || []), - optionProvider, - currentDataSourceProvider, - TransformInputService, - EntityPropsMapService, - SwaggerBindMethod, - ], - exports: [ - ...(options.providers || []), - typeOrmModule, - optionProvider, - currentDataSourceProvider, - TransformInputService, - EntityPropsMapService, - SwaggerBindMethod, - ...(options.imports || []), - ], - }; - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/json-api.module.ts b/libs/json-api/json-api-nestjs/src/lib/json-api.module.ts index 9f54a008..0e9fad09 100644 --- a/libs/json-api/json-api-nestjs/src/lib/json-api.module.ts +++ b/libs/json-api/json-api-nestjs/src/lib/json-api.module.ts @@ -1,73 +1,80 @@ import { DynamicModule, Module } from '@nestjs/common'; -import { DiscoveryModule, RouterModule } from '@nestjs/core'; -import { ConfigParamDefault, DEFAULT_CONNECTION_NAME } from './constants'; -import { ModuleOptions } from './types'; -import { JsonApiNestJsCommonModule } from './json-api-nestjs-common.module'; -import { JSON_API_DECORATOR_ENTITY } from './constants'; -import { BaseModuleClass } from './mixin'; -import { AtomicOperationModule } from './modules/atomic-operation/atomic-operation.module'; +import { DiscoveryModule } from '@nestjs/core'; + +import { + AnyEntity, + EntityName, + TypeOrmDefaultOptions, + TypeOrmOptions, + MicroOrmOptions, + ResultModuleOptions, +} from './types'; +import { createMixinModule, prepareConfig, createAtomicModule } from './utils'; +import type { TypeOrmJsonApiModule, MicroOrmJsonApiModule } from './modules'; @Module({}) export class JsonApiModule { - private static connectionName = DEFAULT_CONNECTION_NAME; + public static forRoot( + module: typeof TypeOrmJsonApiModule, + options: TypeOrmOptions + ): DynamicModule; + public static forRoot( + module: typeof MicroOrmJsonApiModule, + options: MicroOrmOptions + ): DynamicModule; + /** + * @deprecated This type of method is deprecated and may be removed in future versions. + * Consider using newer alternatives or updated patterns for module registration. + */ + public static forRoot(options: TypeOrmDefaultOptions): DynamicModule; + public static forRoot( + first: + | typeof TypeOrmJsonApiModule + | typeof MicroOrmJsonApiModule + | TypeOrmDefaultOptions, + second?: TypeOrmOptions | MicroOrmOptions + ): DynamicModule { + let resultOption: ResultModuleOptions = {} as any; - public static forRoot(options: ModuleOptions): DynamicModule { - JsonApiModule.connectionName = - options.connectionName || JsonApiModule.connectionName; + if (second) { + const module = first as + | typeof TypeOrmJsonApiModule + | typeof MicroOrmJsonApiModule; + resultOption = { + ...prepareConfig(second, module.module), + type: module, + } as ResultModuleOptions; + } else { + const { + TypeOrmJsonApiModule, + } = require('./modules/type-orm/type-orm-json-api.module'); + resultOption = { + ...prepareConfig( + first as TypeOrmDefaultOptions, + TypeOrmJsonApiModule.module + ), + type: TypeOrmJsonApiModule as typeof TypeOrmJsonApiModule, + } as any; + } - options.connectionName = JsonApiModule.connectionName; - options.options = { - ...ConfigParamDefault, - ...options.options, - }; + resultOption.imports.unshift(DiscoveryModule); - const commonModule = JsonApiNestJsCommonModule.forRoot(options); + const commonOrmModule = resultOption.type.forRoot(resultOption); - const entityImport = options.entities.map((entity) => { - const controller = (options.controllers || []).find( - (item) => - item && - Reflect.getMetadata(JSON_API_DECORATOR_ENTITY, item) === entity - ); - const module = BaseModuleClass.forRoot({ - entity, - connectionName: JsonApiModule.connectionName, - controller, - config: { - ...ConfigParamDefault, - ...options.options, - }, - }); - module.imports = [ - DiscoveryModule, - commonModule, - ...(module.imports || []), - ]; - return module; - }); + const entitiesMixinModules = resultOption.entities.map( + (entity: EntityName) => + createMixinModule(entity, resultOption, commonOrmModule) + ); - const operationModuleImport = options.options?.operationUrl - ? [ - AtomicOperationModule.forRoot( - { - ...options, - connectionName: JsonApiModule.connectionName, - }, - entityImport, - commonModule - ), - RouterModule.register([ - { - module: AtomicOperationModule, - path: options.options.operationUrl, - }, - ]), - ] - : []; + const operationModuleImport = createAtomicModule( + resultOption, + entitiesMixinModules, + commonOrmModule + ); return { module: JsonApiModule, - imports: [...operationModuleImport, ...entityImport], + imports: [...operationModuleImport, ...entitiesMixinModules], }; } } diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/index.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/index.ts deleted file mode 100644 index b71f2177..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './module/module.mixin'; -export * from './controller/json-base.controller'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/interceptors/error.interceptors.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/interceptors/error.interceptors.ts deleted file mode 100644 index 53f35403..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/interceptors/error.interceptors.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { - InternalServerErrorException, - CallHandler, - ExecutionContext, - Inject, - Injectable, - NestInterceptor, -} from '@nestjs/common'; -import { catchError, Observable, throwError } from 'rxjs'; -import { QueryFailedError, Repository } from 'typeorm'; -import { DriverUtils } from 'typeorm/driver/DriverUtils'; - -import { - CONTROL_OPTIONS_TOKEN, - CURRENT_ENTITY_REPOSITORY, -} from '../../constants'; -import { ConfigParam, Entity, ValidateQueryError } from '../../types'; -import { - MysqlError, - MysqlErrorCode, - PostgresError, - PostgresErrorCode, -} from '../../helper'; -import { HttpException } from '@nestjs/common'; - -@Injectable() -export class ErrorInterceptors implements NestInterceptor { - @Inject(CURRENT_ENTITY_REPOSITORY) private repository!: Repository; - @Inject(CONTROL_OPTIONS_TOKEN) private config!: ConfigParam; - - intercept( - context: ExecutionContext, - next: CallHandler - ): Observable | Promise> { - return next.handle().pipe( - catchError((error) => { - if (error instanceof QueryFailedError) { - return throwError(() => this.prepareDataBaseError(error)); - } - - if (error instanceof HttpException) { - return throwError(() => error); - } - - const errorObject: ValidateQueryError = { - code: 'internal_error', - message: this.config.debug ? error.message : 'Internal Server Error', - path: [], - }; - const descriptionOrOptions = this.config.debug ? error : undefined; - return throwError( - () => - new InternalServerErrorException( - [errorObject], - descriptionOrOptions - ) - ); - }) - ); - } - - private prepareDataBaseError(error: QueryFailedError): HttpException { - const errorObject: ValidateQueryError = { - code: 'internal_error', - message: this.config.debug ? error.message : 'Internal Server Error', - path: [], - }; - - if (DriverUtils.isMySQLFamily(this.repository.manager.connection.driver)) { - const { errorCode, errorMsg } = this.prepareMysqlError(error.driverError); - if (MysqlError[errorCode]) { - return MysqlError[errorCode](this.repository.metadata, errorMsg); - } - } - - if ( - DriverUtils.isPostgresFamily(this.repository.manager.connection.driver) - ) { - const { errorCode, errorMsg, detail } = this.preparePostgresError( - error.driverError - ); - - if (PostgresError[errorCode]) { - return PostgresError[errorCode]( - this.repository.metadata, - errorMsg, - detail - ); - } - } - - return new InternalServerErrorException([errorObject]); - } - - private prepareMysqlError(error: any): { - errorCode: MysqlErrorCode; - errorMsg: string; - } { - return { - errorCode: error.errno, - errorMsg: error.message, - }; - } - - private preparePostgresError(error: any): { - errorCode: PostgresErrorCode; - errorMsg: string; - detail: string; - } { - return { - errorCode: error.code, - errorMsg: error.message, - detail: error.detail, - }; - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/interceptors/log-time.interceptors.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/interceptors/log-time.interceptors.ts deleted file mode 100644 index badabaed..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/interceptors/log-time.interceptors.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common'; -import { map, Observable } from 'rxjs'; - -import { Entity, ResourceObject } from '../../types'; - -export class LogTimeInterceptors implements NestInterceptor { - intercept( - context: ExecutionContext, - next: CallHandler> - ): Observable | Promise> { - const now = Date.now(); - return next.handle().pipe( - map((r) => { - const response = context.switchToHttp().getResponse(); - const time = Date.now() - now; - response.setHeader('x-response-time', time); - if (r && r.meta) { - r.meta['time'] = time; - } - - return r; - }) - ); - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/module/module.mixin.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/module/module.mixin.ts deleted file mode 100644 index 04225e97..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/module/module.mixin.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { DynamicModule } from '@nestjs/common'; - -import { BaseModuleOptions, ConfigParam, DecoratorOptions } from '../../types'; -import { - createController, - getProviderName, - nameIt, - bindController, -} from '../../helper'; -import { - JSON_API_DECORATOR_OPTIONS, - ConfigParamDefault, - JSON_API_MODULE_POSTFIX, - CONTROL_OPTIONS_TOKEN, -} from '../../constants'; -import { - ZodInputQuerySchema, - ZodQuerySchema, - TypeormServiceFactory, - EntityRepositoryFactory, - ZodInputPostSchema, - ZodInputPatchSchema, - ZodInputPostRelationshipSchema, - ZodInputPatchRelationshipSchema, -} from '../../factory'; -import { TypeormUtilsService } from '../service'; -import { TransformDataService } from '../service'; -import { SwaggerBindService } from '../service/swagger-bind.service'; - -export class BaseModuleClass { - static forRoot(options: BaseModuleOptions): DynamicModule { - const { entity, connectionName, controller } = options; - const controllerClass = createController(entity, controller); - - const decoratorOptions: DecoratorOptions = - Reflect.getMetadata(JSON_API_DECORATOR_OPTIONS, controllerClass) || {}; - - const moduleConfig: ConfigParam = { - ...ConfigParamDefault, - ...options.config, - ...decoratorOptions, - }; - - bindController(controllerClass, entity, connectionName, moduleConfig); - - const optionProvider = { - provide: CONTROL_OPTIONS_TOKEN, - useValue: moduleConfig, - }; - - return { - module: nameIt( - getProviderName(entity, JSON_API_MODULE_POSTFIX), - BaseModuleClass - ), - controllers: [controllerClass], - providers: [ - ZodInputQuerySchema(entity), - ZodQuerySchema(entity), - ZodInputPostSchema(entity), - ZodInputPatchSchema(entity), - ZodInputPostRelationshipSchema, - ZodInputPatchRelationshipSchema, - TypeormServiceFactory(entity), - EntityRepositoryFactory(entity), - optionProvider, - TypeormUtilsService, - TransformDataService, - SwaggerBindService, - ], - imports: [], - }; - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/check-item-entity/check-item-entity.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/check-item-entity/check-item-entity.pipe.spec.ts deleted file mode 100644 index 2e84898d..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/check-item-entity/check-item-entity.pipe.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { IMemoryDb } from 'pg-mem'; - -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, - pullUser, - Users, -} from '../../../mock-utils'; -import { CheckItemEntityPipe } from './check-item-entity.pipe'; -import { TypeormUtilsService } from '../../../service'; - -import { - CurrentDataSourceProvider, - EntityRepositoryFactory, -} from '../../../factory'; -import { - CURRENT_ENTITY_REPOSITORY, - DEFAULT_CONNECTION_NAME, -} from '../../../constants'; -import { Repository } from 'typeorm'; -import { NotFoundException } from '@nestjs/common'; - -describe('CheckItemEntityPipe', () => { - let db: IMemoryDb; - let checkItemEntityPipe: CheckItemEntityPipe; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - EntityRepositoryFactory(Users), - TypeormUtilsService, - CheckItemEntityPipe, - ], - }).compile(); - - await pullUser(module.get>(CURRENT_ENTITY_REPOSITORY)); - - checkItemEntityPipe = - module.get>(CheckItemEntityPipe); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('Should be correct', async () => { - const id = 1; - const result = await checkItemEntityPipe.transform(id); - expect(result).toBe(id); - }); - - it('Should be error', async () => { - expect.assertions(1); - try { - await checkItemEntityPipe.transform(11111); - } catch (e) { - expect(e).toBeInstanceOf(NotFoundException); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/check-item-entity/check-item-entity.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/check-item-entity/check-item-entity.pipe.ts deleted file mode 100644 index a42cb10d..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/check-item-entity/check-item-entity.pipe.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Inject, NotFoundException, PipeTransform } from '@nestjs/common'; -import { Repository } from 'typeorm'; - -import { CURRENT_ENTITY_REPOSITORY } from '../../../constants'; -import { Entity, ValidateQueryError } from '../../../types'; -import { TypeormUtilsService } from '../../service/typeorm-utils.service'; - -export class CheckItemEntityPipe - implements PipeTransform> -{ - @Inject(CURRENT_ENTITY_REPOSITORY) private repository!: Repository; - @Inject(TypeormUtilsService) - private typeormUtilsService!: TypeormUtilsService; - async transform(value: I): Promise { - const params = 'params'; - const result = await this.repository - .createQueryBuilder(this.typeormUtilsService.currentAlias) - .where( - `${this.typeormUtilsService.getAliasPath( - this.typeormUtilsService.currentPrimaryColumn - )} = :${params}` - ) - .setParameters({ - [params]: value, - }) - .getOne(); - if (!result) { - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: `Resource '${this.typeormUtilsService.currentAlias}' with id '${value}' does not exist`, - path: [], - }; - throw new NotFoundException([error]); - } - return value; - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/index.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/index.ts deleted file mode 100644 index fe926be1..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/index.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { Injectable, ParseIntPipe } from '@nestjs/common'; - -import { QueryPipe } from './query'; -import { QueryInputPipe } from './query-input'; -import { QueryFiledInIncludePipe } from './query-filed-on-include'; -import { QueryCheckSelectField } from './query-check-select-field'; - -import { ConfigParam, Entity, PipeMixin } from '../../types'; -import { nameIt } from '../../helper'; -import { CheckItemEntityPipe } from './check-item-entity'; -import { PostInputPipe } from './post-input'; -import { PatchInputPipe } from './patch-input'; -import { PostRelationshipPipe } from './post-relationship'; -import { ParseRelationshipNamePipe } from './parse-relationship-name'; -import { PatchRelationshipPipe } from './patch-relationship'; - -function factoryMixin(entity: Entity, connectionName: string, pipe: PipeMixin) { - const entityName = - entity instanceof Function ? entity.name : entity['options'].name; - - const pipeClass = nameIt( - `${entityName.charAt(0).toUpperCase() + entityName.slice(1)}${pipe.name}`, - pipe - ) as PipeMixin; - - Injectable()(pipeClass); - - return pipeClass; -} - -export function queryInputMixin( - entity: Entity, - connectionName: string -): PipeMixin { - return factoryMixin(entity, connectionName, QueryInputPipe); -} - -export function queryMixin(entity: Entity, connectionName: string): PipeMixin { - return factoryMixin(entity, connectionName, QueryPipe); -} - -export function queryFiledInIncludeMixin( - entity: Entity, - connectionName: string -): PipeMixin { - return factoryMixin(entity, connectionName, QueryFiledInIncludePipe); -} - -export function queryCheckSelectFieldMixin( - entity: Entity, - connectionName: string -): PipeMixin { - return factoryMixin(entity, connectionName, QueryCheckSelectField); -} - -export function idPipeMixin( - entity: Entity, - connectionName: string, - config?: ConfigParam -): PipeMixin { - return config && config.pipeForId ? config.pipeForId : ParseIntPipe; -} - -export function checkItemEntityPipeMixin( - entity: Entity, - connectionName: string -): PipeMixin { - return factoryMixin(entity, connectionName, CheckItemEntityPipe); -} -export function postInputPipeMixin( - entity: Entity, - connectionName: string -): PipeMixin { - return factoryMixin(entity, connectionName, PostInputPipe); -} - -export function patchInputPipeMixin( - entity: Entity, - connectionName: string -): PipeMixin { - return factoryMixin(entity, connectionName, PatchInputPipe); -} - -export function postRelationshipPipeMixin( - entity: Entity, - connectionName: string -): PipeMixin { - return factoryMixin(entity, connectionName, PostRelationshipPipe); -} - -export function patchRelationshipPipeMixin( - entity: Entity, - connectionName: string -): PipeMixin { - return factoryMixin(entity, connectionName, PatchRelationshipPipe); -} -export function parseRelationshipNamePipeMixin( - entity: Entity, - connectionName: string -): PipeMixin { - return factoryMixin(entity, connectionName, ParseRelationshipNamePipe); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.spec.ts deleted file mode 100644 index 11fbf094..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.spec.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { IMemoryDb } from 'pg-mem'; - -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, - Users, -} from '../../../mock-utils'; -import { ParseRelationshipNamePipe } from './parse-relationship-name.pipe'; -import { TypeormUtilsService } from '../../service/typeorm-utils.service'; - -import { - CurrentDataSourceProvider, - EntityRepositoryFactory, -} from '../../../factory'; -import { DEFAULT_CONNECTION_NAME } from '../../../constants'; -import { UnprocessableEntityException } from '@nestjs/common'; - -describe('ParseRelationshipNamePipe', () => { - let db: IMemoryDb; - let parseRelationshipNamePipe: ParseRelationshipNamePipe; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - EntityRepositoryFactory(Users), - TypeormUtilsService, - ParseRelationshipNamePipe, - ], - }).compile(); - - parseRelationshipNamePipe = module.get>( - ParseRelationshipNamePipe - ); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('Should be correct', async () => { - const relName = 'userGroup'; - const result = await parseRelationshipNamePipe.transform(relName); - expect(result).toBe(relName); - }); - - it('Should be error', async () => { - expect.assertions(1); - try { - await parseRelationshipNamePipe.transform('11111'); - } catch (e) { - expect(e).toBeInstanceOf(UnprocessableEntityException); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.ts deleted file mode 100644 index 3963f03e..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { - PipeTransform, - UnprocessableEntityException, - Inject, -} from '@nestjs/common'; -import { TypeormUtilsService } from '../../service/typeorm-utils.service'; -import { Entity, EntityRelation, ValidateQueryError } from '../../../types'; - -export class ParseRelationshipNamePipe - implements PipeTransform> -{ - @Inject(TypeormUtilsService) - private typeormUtilsService!: TypeormUtilsService; - - transform(value: string): EntityRelation { - this.checkRelName(value); - return value; - } - - checkRelName(value: unknown): asserts value is EntityRelation { - if (!this.typeormUtilsService.relationFields.find((i) => i === value)) { - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: `Relation '${value}' does not exist in resource '${this.typeormUtilsService.currentAlias}'`, - path: [], - }; - throw new UnprocessableEntityException([error]); - } - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-input/patch-input.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-input/patch-input.pipe.spec.ts deleted file mode 100644 index 817ad6bb..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-input/patch-input.pipe.spec.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { IMemoryDb } from 'pg-mem'; -import { Test, TestingModule } from '@nestjs/testing'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { - InternalServerErrorException, - BadRequestException, -} from '@nestjs/common'; -import { CurrentDataSourceProvider } from '../../../factory'; -import { DEFAULT_CONNECTION_NAME, ZOD_PATCH_SCHEMA } from '../../../constants'; - -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, - Users, -} from '../../../mock-utils'; - -import { PatchInputPipe } from './patch-input.pipe'; -import { ZodInputPostSchema } from '../../../helper/zod'; -import { ZodError } from 'zod'; - -describe('PatchInputPipe', () => { - let db: IMemoryDb; - let patchInputPipe: PatchInputPipe; - let zodInputPostSchema: ZodInputPostSchema; - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - { - provide: ZOD_PATCH_SCHEMA, - useValue: { - parse() {}, - }, - }, - PatchInputPipe, - ], - }).compile(); - - patchInputPipe = module.get>(PatchInputPipe); - zodInputPostSchema = - module.get>(ZOD_PATCH_SCHEMA); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('It should be ok', () => { - const data = { - some: 'data', - }; - const check = { - data, - }; - jest - .spyOn(zodInputPostSchema, 'parse') - .mockImplementationOnce(() => check as any); - expect(patchInputPipe.transform(check)).toEqual(data); - }); - - it('Should be not ok', () => { - jest.spyOn(zodInputPostSchema, 'parse').mockImplementationOnce(() => { - throw new ZodError([]); - }); - expect.assertions(1); - try { - patchInputPipe.transform({}); - } catch (e) { - expect(e).toBeInstanceOf(BadRequestException); - } - }); - - it('Should be 500', () => { - jest.spyOn(zodInputPostSchema, 'parse').mockImplementationOnce(() => { - throw new Error('Error mock'); - }); - expect.assertions(1); - - try { - patchInputPipe.transform({}); - } catch (e) { - expect(e).toBeInstanceOf(InternalServerErrorException); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-input/post-input.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-input/post-input.pipe.spec.ts deleted file mode 100644 index c61f5be1..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-input/post-input.pipe.spec.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { IMemoryDb } from 'pg-mem'; -import { Test, TestingModule } from '@nestjs/testing'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { - InternalServerErrorException, - BadRequestException, -} from '@nestjs/common'; -import { CurrentDataSourceProvider } from '../../../factory'; -import { DEFAULT_CONNECTION_NAME, ZOD_POST_SCHEMA } from '../../../constants'; - -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, - Users, -} from '../../../mock-utils'; - -import { PostInputPipe } from './post-input.pipe'; -import { ZodInputPostSchema } from '../../../helper/zod'; -import { ZodError } from 'zod'; - -describe('PostInputPipe', () => { - let db: IMemoryDb; - let postInputPipe: PostInputPipe; - let zodInputPostSchema: ZodInputPostSchema; - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - { - provide: ZOD_POST_SCHEMA, - useValue: { - parse() {}, - }, - }, - PostInputPipe, - ], - }).compile(); - - postInputPipe = module.get>(PostInputPipe); - zodInputPostSchema = module.get>(ZOD_POST_SCHEMA); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('It should be ok', () => { - const data = { - some: 'data', - }; - const check = { - data, - }; - jest - .spyOn(zodInputPostSchema, 'parse') - .mockImplementationOnce(() => check as any); - expect(postInputPipe.transform(check)).toEqual(data); - }); - - it('Should be not ok', () => { - jest.spyOn(zodInputPostSchema, 'parse').mockImplementationOnce(() => { - throw new ZodError([]); - }); - expect.assertions(1); - try { - postInputPipe.transform({}); - } catch (e) { - expect(e).toBeInstanceOf(BadRequestException); - } - }); - - it('Should be 500', () => { - jest.spyOn(zodInputPostSchema, 'parse').mockImplementationOnce(() => { - throw new Error('Error mock'); - }); - expect.assertions(1); - - try { - postInputPipe.transform({}); - } catch (e) { - expect(e).toBeInstanceOf(InternalServerErrorException); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-input/query-input.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-input/query-input.pipe.spec.ts deleted file mode 100644 index 115d88ee..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-input/query-input.pipe.spec.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { - InternalServerErrorException, - BadRequestException, -} from '@nestjs/common'; -import { IMemoryDb } from 'pg-mem'; - -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, - Users, -} from '../../../mock-utils'; - -import { - CurrentDataSourceProvider, - ZodInputQuerySchema, -} from '../../../factory'; -import { - DEFAULT_CONNECTION_NAME, - DEFAULT_PAGE_SIZE, - DEFAULT_QUERY_PAGE, - ZOD_INPUT_QUERY_SCHEMA, -} from '../../../constants'; -import { QueryInputPipe } from './query-input.pipe'; -import { - QueryField, - ZodInputQuerySchema as TypeZodInputQuerySchema, -} from '../../../helper'; - -const page = { - [QueryField.page]: { - size: DEFAULT_PAGE_SIZE, - number: DEFAULT_QUERY_PAGE, - }, -}; - -describe('QueryInputPipe', () => { - let db: IMemoryDb; - let queryInputPipe: QueryInputPipe; - let zodParse: TypeZodInputQuerySchema; - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - ZodInputQuerySchema(Users), - QueryInputPipe, - ], - }).compile(); - - queryInputPipe = module.get>(QueryInputPipe); - zodParse = module.get>( - ZOD_INPUT_QUERY_SCHEMA - ); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('Should be ok', () => { - const input = {}; - const result = queryInputPipe.transform(input); - - const input1 = { - [QueryField.page]: page[QueryField.page], - }; - const result1 = queryInputPipe.transform(input1); - - const input2 = { - ...page, - [QueryField.fields]: { target: 'test', addresses: '' }, - }; - const result2 = queryInputPipe.transform(input2); - - expect(result).toEqual(page); - expect(result1).toEqual(input1); - expect(result2).toEqual(input2); - }); - - it('Should be not ok', () => { - const input = { - [QueryField.page]: { - size: 0, - number: 'sdfsdf', - }, - sdaas: 'sdfsdf', - [QueryField.include]: null, - [QueryField.fields]: { - dsada: 'sdfsf', - sdfsdfsdfs: 'sdfsdf', - }, - [QueryField.sort]: null, - [QueryField.filter]: { - id: { - eq: 'null', - }, - }, - }; - expect.assertions(1); - try { - const result = queryInputPipe.transform(input); - } catch (e) { - expect(e).toBeInstanceOf(BadRequestException); - } - }); - - it('Should be 500', () => { - jest.spyOn(zodParse, 'parse').mockImplementationOnce(() => { - throw new Error('Error mock'); - }); - expect.assertions(1); - - try { - const result = queryInputPipe.transform({}); - } catch (e) { - expect(e).toBeInstanceOf(InternalServerErrorException); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query/query.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query/query.pipe.spec.ts deleted file mode 100644 index e874744a..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query/query.pipe.spec.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { IMemoryDb } from 'pg-mem'; -import { QueryPipe } from '../query'; -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, - Users, -} from '../../../mock-utils'; -import { - QueryField, - ZodQuerySchema as TypeZodQuerySchema, -} from '../../../helper'; - -import { CurrentDataSourceProvider, ZodQuerySchema } from '../../../factory'; -import { - DEFAULT_CONNECTION_NAME, - ZOD_QUERY_SCHEMA, - DEFAULT_PAGE_SIZE, - DEFAULT_QUERY_PAGE, -} from '../../../constants'; -import { TransformInputService } from '../../../service'; -import { - InternalServerErrorException, - BadRequestException, -} from '@nestjs/common'; - -describe('QueryPipe', () => { - let db: IMemoryDb; - let queryPipe: QueryPipe; - let zodParse: TypeZodQuerySchema; - let transformInputService: TransformInputService; - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - ZodQuerySchema(Users), - TransformInputService, - QueryPipe, - ], - }).compile(); - - queryPipe = module.get>(QueryPipe); - zodParse = module.get>(ZOD_QUERY_SCHEMA); - transformInputService = module.get>( - TransformInputService - ); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('Should be ok', () => { - const filter = { - target: null, - relation: null, - }; - - const result = queryPipe.transform({ - [QueryField.page]: { - size: DEFAULT_PAGE_SIZE, - number: DEFAULT_QUERY_PAGE, - }, - }); - expect(result).toEqual({ - [QueryField.filter]: filter, - [QueryField.fields]: null, - [QueryField.include]: null, - [QueryField.sort]: null, - [QueryField.page]: { - size: DEFAULT_PAGE_SIZE, - number: DEFAULT_QUERY_PAGE, - }, - }); - }); - - it('Should be not ok', () => { - const input = { - filter: { - test: '', - }, - [QueryField.page]: { - size: DEFAULT_PAGE_SIZE, - number: DEFAULT_QUERY_PAGE, - }, - }; - expect.assertions(1); - try { - const result = queryPipe.transform(input as any); - } catch (e) { - expect(e).toBeInstanceOf(BadRequestException); - } - }); - - it('Should be 500', () => { - jest - .spyOn(transformInputService, 'transformSort') - .mockImplementationOnce(() => { - throw new Error('Error mock'); - }); - expect.assertions(1); - - try { - const result = queryPipe.transform({} as any); - } catch (e) { - expect(e).toBeInstanceOf(InternalServerErrorException); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query/query.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query/query.pipe.ts deleted file mode 100644 index 1ea63146..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query/query.pipe.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { - InternalServerErrorException, - BadRequestException, - Inject, - PipeTransform, -} from '@nestjs/common'; -import { ZodError } from 'zod'; - -import { InputQuery, Query, QueryField, ZodQuerySchema } from '../../../helper'; -import { Entity, JSONValue } from '../../../types'; -import { ZOD_QUERY_SCHEMA } from '../../../constants'; -import { TransformInputService } from '../../../service'; - -export class QueryPipe - implements PipeTransform, Query> -{ - @Inject(ZOD_QUERY_SCHEMA) - private zodQuerySchema!: ZodQuerySchema; - - @Inject(TransformInputService) - private transformInputService!: TransformInputService; - - transform(value: InputQuery): Query { - try { - const { filter, page, sort, include, fields } = value; - const queryObject: JSONValue = { - [QueryField.filter]: this.transformInputService.transformFilter(filter), - [QueryField.fields]: this.transformInputService.transformFields(fields), - [QueryField.include]: - this.transformInputService.transformInclude(include), - [QueryField.sort]: this.transformInputService.transformSort(sort), - [QueryField.page]: page, - }; - return this.zodQuerySchema.parse(queryObject); - } catch (e) { - if (e instanceof ZodError) { - throw new BadRequestException(e.issues); - } - - throw new InternalServerErrorException(e); - } - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/service/index.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/service/index.ts deleted file mode 100644 index 442fee03..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/service/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './typeorm-utils.service'; -export * from './transform-data.service'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/service/swagger-bind.service.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/service/swagger-bind.service.ts deleted file mode 100644 index 6ffaf430..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/service/swagger-bind.service.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; -import { DiscoveryService } from '@nestjs/core'; -import { PARAMTYPES_METADATA } from '@nestjs/common/constants'; -import { ApiExtraModels, ApiTags } from '@nestjs/swagger'; -import { DECORATORS } from '@nestjs/swagger/dist/constants'; -import { JsonBaseController } from '../controller/json-base.controller'; -import { Repository } from 'typeorm'; -import { - CONTROL_OPTIONS_TOKEN, - CURRENT_ENTITY_REPOSITORY, - JSON_API_CONTROLLER_POSTFIX, - JSON_API_DECORATOR_ENTITY, - SWAGGER_METHOD, -} from '../../constants'; - -import { - createApiModels, - ObjectTyped, - swaggerMethod, - SwaggerMethod, - FilterOperand, - nameIt, - getProviderName, -} from '../../helper'; -import { Bindings } from '../../config/bindings'; -import { DecoratorOptions, Entity } from '../../types'; - -@Injectable() -export class SwaggerBindService implements OnModuleInit { - @Inject(CURRENT_ENTITY_REPOSITORY) private repository!: Repository; - @Inject(DiscoveryService) private discoveryService!: DiscoveryService; - @Inject(SWAGGER_METHOD) private swaggerMethod!: SwaggerMethod; - @Inject(CONTROL_OPTIONS_TOKEN) private config!: DecoratorOptions; - - public initSwagger() { - const controllerName = nameIt( - getProviderName(this.repository.target, JSON_API_CONTROLLER_POSTFIX), - JsonBaseController - ).name; - - const controllerInst = this.discoveryService - .getControllers() - .find( - (i) => - i.name === controllerName || - this.repository.target === - Reflect.getMetadata( - JSON_API_DECORATOR_ENTITY, - i.instance.constructor - ) - ); - if (!controllerInst) - throw new Error( - `Controller for ${this.repository.metadata.name} is empty` - ); - const controller = controllerInst.instance.constructor; - const apiTag = Reflect.getMetadata(DECORATORS.API_TAGS, controller); - if (!apiTag) { - ApiTags(this.config['overrideRoute'] || this.repository.metadata.name)( - controller - ); - } - - ApiTags(this.repository.metadata.name)(controller); - - ApiExtraModels(FilterOperand)(controller); - ApiExtraModels(createApiModels(this.repository))(controller); - - const { allowMethod = Object.keys(Bindings) } = this.config; - for (const method of ObjectTyped.keys(Bindings)) { - if (!allowMethod.includes(method)) continue; - - if (method in swaggerMethod) { - swaggerMethod[method]( - controller.prototype, - this.repository, - Bindings[method], - this.config - ); - } - Reflect.defineMetadata( - PARAMTYPES_METADATA, - [Object], - controller.prototype, - method - ); - } - } - - onModuleInit(): any { - this.initSwagger(); - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/service/transform-data.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/service/transform-data.service.spec.ts deleted file mode 100644 index 777d29a5..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/service/transform-data.service.spec.ts +++ /dev/null @@ -1,375 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { IMemoryDb } from 'pg-mem'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; - -import { - Addresses, - Comments, - createAndPullSchemaBase, - getRepository, - mockDBTestModule, - Notes, - providerEntities, - pullAllData, - Roles, - UserGroups, - Users, -} from '../../mock-utils'; -import { - CurrentDataSourceProvider, - EntityRepositoryFactory, -} from '../../factory'; -import { DEFAULT_CONNECTION_NAME } from '../../constants'; -import { TransformDataService } from './transform-data.service'; -import { TypeormUtilsService } from './typeorm-utils.service'; -import { ApplicationConfig } from '@nestjs/core'; -import { VersioningType } from '@nestjs/common'; -import { EntityPropsMapService } from '../../service'; -import { data } from '../../helper/zod/zod-input-post-relationship-schema'; - -describe('TransformDataService', () => { - let db: IMemoryDb; - let transformDataService: TransformDataService; - let userRepository: Repository; - let addressesRepository: Repository; - let notesRepository: Repository; - let commentsRepository: Repository; - let rolesRepository: Repository; - let userGroupRepository: Repository; - let applicationConfig: ApplicationConfig; - let entityPropsMapService: EntityPropsMapService; - let usersData: Users; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - TransformDataService, - EntityPropsMapService, - ], - }).compile(); - - ({ - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository, - } = getRepository(module)); - await pullAllData( - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository - ); - - // transformDataService = - // module.get>(TransformDataService); - entityPropsMapService = module.get( - EntityPropsMapService - ); - - const data = await userRepository.findOne({ - where: { - id: 1, - }, - relations: { - userGroup: true, - manager: true, - roles: true, - addresses: true, - comments: true, - notes: true, - }, - }); - if (!data) { - throw new Error('Not found user'); - } - usersData = data; - applicationConfig = module.get(ApplicationConfig); - }); - - beforeEach(() => { - transformDataService = new TransformDataService(); - Object.defineProperty(transformDataService, 'applicationConfig', { - value: applicationConfig, - }); - Object.defineProperty(transformDataService, 'entityPropsMapService', { - value: entityPropsMapService, - }); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('Url path', () => { - const prefix = 'api'; - applicationConfig.setGlobalPrefix(prefix); - - expect(transformDataService.urlPath).toEqual(['', prefix]); - }); - it('Url path with version', () => { - const prefix = 'api'; - const version = '1'; - - applicationConfig.setGlobalPrefix(prefix); - - jest - .spyOn(applicationConfig, 'getVersioning') - .mockImplementationOnce(() => ({ - type: VersioningType.URI, - defaultVersion: version, - })); - - expect(transformDataService.urlPath).toEqual(['', prefix, 'v1']); - }); - - it('check type, id and links', () => { - const result = transformDataService.transformData(usersData); - expect(result).toHaveProperty('data'); - const { data } = result; - expect(data.type).toBe('users'); - expect(data.id).toBe(usersData.id.toString()); - expect(data.links.self).toBe( - [...transformDataService.urlPath, 'users', usersData.id.toString()].join( - '/' - ) - ); - }); - - it('check attributes', async () => { - const result = transformDataService.transformData(usersData); - const { - id, - addresses, - comments, - manager, - roles, - notes, - userGroup, - ...other - } = usersData; - expect(result.data.attributes).toEqual(other); - const result1 = transformDataService.transformData([]); - expect(result1.data).toEqual([]); - - const data = await userRepository.findOne({ - select: { - id: true, - login: true, - isActive: true, - testDate: true, - }, - where: { - id: 1, - }, - }); - if (!data) { - throw new Error('Not found user'); - } - const result2 = transformDataService.transformData(data); - const { id: id2, ...other2 } = data; - - expect(result2.data.attributes).toEqual(other2); - }); - - it('check relationships', async () => { - const { - data: { relationships }, - } = transformDataService.transformData(usersData); - expect(relationships?.addresses?.data).toEqual({ - type: 'addresses', - id: usersData.addresses.id.toString(), - }); - expect(relationships?.addresses?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/addresses` - ); - expect(relationships?.manager?.data).toEqual({ - type: 'users', - id: usersData.manager.id.toString(), - }); - expect(relationships?.manager?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/manager` - ); - expect(relationships?.roles?.data).toEqual( - usersData.roles.map((i) => ({ - type: 'roles', - id: i.id.toString(), - })) - ); - expect(relationships?.roles?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/roles` - ); - expect(relationships?.comments?.data).toEqual( - usersData.comments.map((i) => ({ - type: 'comments', - id: i.id.toString(), - })) - ); - expect(relationships?.comments?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/comments` - ); - expect(relationships?.notes?.data).toEqual( - usersData.notes.map((i) => ({ - type: 'notes', - id: i.id.toString(), - })) - ); - expect(relationships?.notes?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/notes` - ); - expect(relationships?.userGroup?.data).toEqual({ - type: 'user-groups', - id: usersData.userGroup.id.toString(), - }); - expect(relationships?.userGroup?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/userGroup` - ); - }); - - it('check relationships again', async () => { - const data = await userRepository.findOne({ - where: { - id: 1, - }, - relations: { - userGroup: true, - roles: true, - }, - }); - if (!data) { - throw new Error('Not found user'); - } - - const { - data: { relationships }, - } = transformDataService.transformData(data); - - expect(relationships?.addresses).not.toHaveProperty('data'); - expect(relationships?.addresses?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/addresses` - ); - expect(relationships?.manager).not.toHaveProperty('data'); - expect(relationships?.manager?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/manager` - ); - expect(relationships?.roles?.data).toEqual( - usersData.roles.map((i) => ({ - type: 'roles', - id: i.id.toString(), - })) - ); - expect(relationships?.roles?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/roles` - ); - expect(relationships?.comments).not.toHaveProperty('data'); - expect(relationships?.comments?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/comments` - ); - expect(relationships?.notes).not.toHaveProperty('data'); - expect(relationships?.notes?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/notes` - ); - expect(relationships?.userGroup?.data).toEqual({ - type: 'user-groups', - id: usersData.userGroup.id.toString(), - }); - expect(relationships?.userGroup?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/userGroup` - ); - data.userGroup = null as any; - data.roles = []; - const { - data: { relationships: relationships2 }, - } = transformDataService.transformData(data); - - expect(relationships2?.addresses).not.toHaveProperty('data'); - expect(relationships2?.addresses?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/addresses` - ); - expect(relationships2?.manager).not.toHaveProperty('data'); - expect(relationships2?.manager?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/manager` - ); - - expect(relationships2?.roles?.data).toEqual([]); - expect(relationships2?.roles?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/roles` - ); - expect(relationships2?.comments).not.toHaveProperty('data'); - expect(relationships2?.comments?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/comments` - ); - expect(relationships2?.notes).not.toHaveProperty('data'); - expect(relationships2?.notes?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/notes` - ); - expect(relationships2?.userGroup?.data).toEqual(null); - expect(relationships2?.userGroup?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/userGroup` - ); - }); - - it('check include', async () => { - const data = await userRepository.findOne({ - where: { - id: 1, - }, - relations: { - userGroup: true, - roles: true, - }, - }); - if (!data) { - throw new Error('Not found user'); - } - - const { included } = transformDataService.transformData(data); - expect(included).not.toBe(undefined); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/service/transform-data.service.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/service/transform-data.service.ts deleted file mode 100644 index 7fac3b28..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/service/transform-data.service.ts +++ /dev/null @@ -1,258 +0,0 @@ -import { Inject, Injectable, VersioningType } from '@nestjs/common'; -import { ApplicationConfig } from '@nestjs/core'; - -import { - Attributes, - Data, - Entity, - EntityRelation, - MainData, - Relationships, - ResourceData, -} from '../../types'; -import { ResourceObject } from '../../types/response'; -import { camelToKebab, ObjectTyped } from '../../helper'; -import { RoutePathFactory } from '@nestjs/core/router/route-path-factory'; -import { EntityPropsMapService } from '../../service'; - -Injectable(); -export class TransformDataService { - @Inject(ApplicationConfig) private applicationConfig!: ApplicationConfig; - @Inject(EntityPropsMapService) - private entityPropsMapService!: EntityPropsMapService; - - private _urlPath!: string[]; - - get urlPath() { - if (this._urlPath) return [...this._urlPath]; - this._urlPath = ['']; - const prefix = this.applicationConfig.getGlobalPrefix(); - const version = this.applicationConfig.getVersioning(); - - const routePathFactory = new RoutePathFactory(this.applicationConfig); - - if (prefix) { - this._urlPath.push(this.applicationConfig.getGlobalPrefix()); - } - if (version && version.type === VersioningType.URI) { - const firstVersion = Array.isArray(version.defaultVersion) - ? version.defaultVersion[0] - : version.defaultVersion; - if (firstVersion) { - this._urlPath.push( - `${routePathFactory.getVersionPrefix( - version - )}${firstVersion.toString()}` - ); - } - } - return [...this._urlPath]; - } - private getLink(...partOfUrl: string[]) { - const urlPath = this.urlPath; - urlPath.push(...partOfUrl); - return urlPath.join('/'); - } - - public transformData(data: E): Pick, 'data' | 'included'>; - public transformData( - data: E[] - ): Pick, 'data' | 'included'>; - public transformData( - data: E | E[] - ): - | Pick, 'data' | 'included'> - | Pick, 'data' | 'included'> { - const dataResponse = Array.isArray(data) - ? data - .map((i) => this.getMainData(i)) - .filter((i: ResourceData | null): i is ResourceData => !!i) - : this.getMainData(data); - - let relationships: Relationships | undefined = undefined; - if (Array.isArray(dataResponse) && dataResponse.length > 0) { - relationships = dataResponse.reduce( - (acum, item) => ({ - ...acum, - ...item.relationships, - }), - {} as Relationships - ); - } - - if (!Array.isArray(dataResponse) && dataResponse !== null) { - relationships = dataResponse.relationships; - } - - if (relationships === undefined) { - return { data: dataResponse } as Pick< - ResourceObject, - 'data' | 'included' - >; - } - - const propsNeedForInclude = ObjectTyped.entries(relationships).reduce( - (acum, [key, val]) => { - if (!val || !('data' in val)) { - return acum; - } - if (Array.isArray(val.data)) { - acum.push(key); - return acum; - } - if (!Array.isArray(val.data)) { - acum.push(key); - } - return acum; - }, - [] as EntityRelation[] - ); - - const included = (Array.isArray(data) ? data : [data]) - .reduce((acum, item) => { - const tmp = propsNeedForInclude.reduce((a, props) => { - const currentItem = item[props]; - type CurrentItem = typeof currentItem; - const r = ( - Array.isArray(currentItem) ? currentItem : [currentItem] - ) as CurrentItem[]; - - a.push(...r); - return a; - }, [] as E[EntityRelation][]); - acum.push(...tmp); - return acum; - }, [] as E[EntityRelation][]) - .map((i) => { - return this.getMainData(i as E); - }) - .filter((i) => !!i); - - if (included.length > 0) { - return { data: dataResponse, included } as unknown as Pick< - ResourceObject, - 'data' | 'included' - >; - } - return { data: dataResponse } as unknown as Pick< - ResourceObject, - 'data' | 'included' - >; - } - - getMainData(data: E): ResourceData | null { - if (!data) return null; - const entity = data.constructor as E; - return { - ...this.getData( - camelToKebab(this.entityPropsMapService.getNameForEntity(entity)), - data[this.entityPropsMapService.getPrimaryColumnsForEntity(entity)] as - | string - | number - ), - attributes: ObjectTyped.entries(data).reduce((acum, [key, val]) => { - if ( - this.entityPropsMapService - .getRelPropsForEntity(entity) - .includes(key.toString()) || - key.toString() === - this.entityPropsMapService.getPrimaryColumnsForEntity(entity) - ) { - return acum; - } - acum[key] = val; - return acum; - }, {} as Record) as Attributes, - relationships: this.transformRelationshipsData(data), - links: { - self: this.getLink( - camelToKebab(this.entityPropsMapService.getNameForEntity(entity)), - data[ - this.entityPropsMapService.getPrimaryColumnsForEntity(entity) - ] as string - ), - }, - }; - } - - getRelationships>( - data: E, - rel: Rel - ): Data { - const entity = data.constructor as E; - const relation = data[rel]; - const target = this.entityPropsMapService.getRelationPropsType(entity, rel); - const typeName = camelToKebab( - this.entityPropsMapService.getNameForEntity(target) - ) as Rel; - - const primaryColumn = - this.entityPropsMapService.getPrimaryColumnsForEntity(target); - - const resultData = Array.isArray(relation) - ? (relation as E[Rel][]).map((i: E[Rel]) => - this.getData( - typeName, - i[primaryColumn as keyof E[Rel]] as string - ) - ) - : relation === null || !relation - ? null - : this.getData( - typeName, - relation[primaryColumn as keyof E[Rel]] as string - ); - - return { - data: resultData, - } as Data; - } - - transformRelationshipsData(data: E): Relationships { - const entity = data.constructor as E; - return this.entityPropsMapService - .getRelPropsForEntity(entity) - .reduce((acum, val) => { - const result: { - links: Record; - data?: Data], EntityRelation>['data']; - } = { - links: { - self: this.getLink( - camelToKebab(this.entityPropsMapService.getNameForEntity(entity)), - data[ - this.entityPropsMapService.getPrimaryColumnsForEntity( - entity - ) as keyof E - ] as string, - 'relationships', - val - ), - }, - }; - - if (val in data) { - const { data: dataRelationships } = this.getRelationships( - data, - val as EntityRelation - ); - if (Array.isArray(dataRelationships)) { - result['data'] = dataRelationships; - } - - if (!Array.isArray(dataRelationships)) { - result['data'] = dataRelationships; - } - } - acum[val.toString()] = result; - return acum; - }, {} as Record) as Relationships; - } - - getData(type: T, id: number | string): MainData { - return { - type, - id: id.toString(), - }; - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/index.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/index.ts index bd6b43f6..b9efca01 100644 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/index.ts +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/index.ts @@ -1,38 +1,34 @@ -import { TypeOrmModule } from '@nestjs/typeorm'; -import { DynamicModule } from '@nestjs/common'; import { DataType, IMemoryDb, newDb } from 'pg-mem'; import { readFileSync } from 'fs'; import { join } from 'path'; - -import { - Users, - Roles, - RequestsHavePodLocks, - Requests, - Pods, - Comments, - Addresses, - UserGroups, - Notes, -} from './entities'; -import { DataSource } from 'typeorm'; - import { v4 } from 'uuid'; - -export * from './entities'; -export * from './utils'; - -export const entities = [ - Users, - UserGroups, - Roles, - RequestsHavePodLocks, - Requests, - Pods, - Comments, - Addresses, - Notes, -]; +// @ts-ignore +import type { PGlite } from '@electric-sql/pglite'; + +export async function createAndPullSchemaBasePgLite(): Promise { + const db = await Promise.all([ + import('@electric-sql/pglite'), + // @ts-ignore + import('@electric-sql/pglite/contrib/uuid_ossp'), + ]).then( + ([{ PGlite }, { uuid_ossp }]) => + new PGlite({ + extensions: { uuid_ossp }, + database: 'pgLite', + username: 'postgres', + }) + ); + + // await db.exec( + // 'CREATE SCHEMA IF NOT EXISTS public; SET search_path TO public;' + // ); + + // const dump = readFileSync(join(__dirname, 'db-for-test'), { + // encoding: 'utf8', + // }); + // await db.exec(dump); + return db; +} export function createAndPullSchemaBase(): IMemoryDb { const dump = readFileSync(join(__dirname, 'db-for-test'), { @@ -64,31 +60,3 @@ export function createAndPullSchemaBase(): IMemoryDb { db.public.none(dump); return db; } -export function mockDBTestModule(db: IMemoryDb): DynamicModule { - return TypeOrmModule.forRootAsync({ - useFactory() { - return { - type: 'postgres', - // logging: true, - entities: [ - Users, - UserGroups, - Roles, - RequestsHavePodLocks, - Requests, - Pods, - Comments, - Addresses, - Notes, - ], - }; - }, - async dataSourceFactory(options) { - const dataSource: DataSource = await db.adapters.createTypeormDataSource( - options - ); - - return dataSource; - }, - }); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/addresses.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/addresses.ts new file mode 100644 index 00000000..7952b916 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/addresses.ts @@ -0,0 +1,72 @@ +import { + Entity, + PrimaryKey, + Property, + OneToOne, + ArrayType, +} from '@mikro-orm/core'; + +import { Users, IUsers } from '.'; + +export type IAddresses = Addresses; + +@Entity({ + tableName: 'addresses', +}) +export class Addresses { + @PrimaryKey({ + autoincrement: true, + }) + public id!: number; + + @Property({ + columnType: 'varchar', + length: 70, + nullable: true, + }) + public city!: string; + + @Property({ + columnType: 'varchar', + length: 70, + nullable: true, + }) + public state!: string; + + @Property({ + columnType: 'varchar', + length: 68, + nullable: true, + }) + public country!: string; + + @Property({ + name: 'array_field', + type: ArrayType, + columnType: 'varchar[]', + nullable: true, + }) + public arrayField!: string[]; + + @Property({ + length: 0, + name: 'created_at', + nullable: true, + defaultRaw: 'CURRENT_TIMESTAMP(0)', + columnType: 'timestamp(0) without time zone', + }) + createdAt: Date = new Date(); + + @Property({ + length: 0, + onUpdate: () => new Date(), + name: 'updated_at', + nullable: true, + columnType: 'timestamp(0) without time zone', + defaultRaw: 'CURRENT_TIMESTAMP(0)', + }) + updatedAt: Date = new Date(); + + @OneToOne(() => Users, (item) => item.addresses) + public user!: IUsers; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/comments.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/comments.ts new file mode 100644 index 00000000..1f42db64 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/comments.ts @@ -0,0 +1,55 @@ +import { Entity, PrimaryKey, Property, Enum, ManyToOne } from '@mikro-orm/core'; + +export enum CommentKind { + Comment = 'COMMENT', + Message = 'MESSAGE', + Note = 'NOTE', +} + +import { Users, IUsers } from '.'; + +export type IComments = Comments; + +@Entity({ + tableName: 'comments', +}) +export class Comments { + @PrimaryKey({ + autoincrement: true, + }) + public id!: number; + + @Property({ + columnType: 'text', + }) + public text!: string; + + @Enum({ items: () => CommentKind, nativeEnumName: 'comment_kind_enum' }) + public kind!: CommentKind; + + @Property({ + length: 0, + name: 'created_at', + nullable: true, + defaultRaw: 'CURRENT_TIMESTAMP(0)', + columnType: 'timestamp(0) without time zone', + }) + createdAt: Date = new Date(); + + @Property({ + length: 0, + onUpdate: () => new Date(), + name: 'updated_at', + nullable: true, + columnType: 'timestamp(0) without time zone', + defaultRaw: 'CURRENT_TIMESTAMP(0)', + }) + updatedAt: Date = new Date(); + + @ManyToOne(() => Users, { + // #TODO need add chaeck for nullable relation to zod + nullable: true, + fieldName: 'created_by', + }) + createdBy!: IUsers; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/index.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/index.ts new file mode 100644 index 00000000..451e1a81 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/index.ts @@ -0,0 +1,15 @@ +export * from './users'; +export * from './roles'; +export * from './comments'; +export * from './addresses'; +export * from './user-groups'; +export * from './notes'; + +import { Users } from './users'; +import { Roles } from './roles'; +import { Comments } from './comments'; +import { Addresses } from './addresses'; +import { UserGroups } from './user-groups'; +import { Notes } from './notes'; + +export const Entities = [Users, Roles, Comments, Addresses, UserGroups, Notes]; diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/notes.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/notes.ts new file mode 100644 index 00000000..b3ff70ce --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/notes.ts @@ -0,0 +1,44 @@ +import { PrimaryKey, Property, Entity, ManyToOne } from '@mikro-orm/core'; + +import { Users, IUsers } from './index'; + +@Entity({ + tableName: 'notes', +}) +export class Notes { + @PrimaryKey({ + type: 'uuid', + defaultRaw: 'gen_random_uuid()', + }) + public id!: string; + + @Property({ + type: 'text', + nullable: false, + }) + public text!: string; + + @Property({ + length: 0, + name: 'created_at', + nullable: true, + defaultRaw: 'CURRENT_TIMESTAMP(0)', + columnType: 'timestamp(0) without time zone', + }) + createdAt: Date = new Date(); + + @Property({ + length: 0, + onUpdate: () => new Date(), + name: 'updated_at', + nullable: true, + columnType: 'timestamp(0) without time zone', + defaultRaw: 'CURRENT_TIMESTAMP(0)', + }) + updatedAt: Date = new Date(); + + @ManyToOne(() => Users, { + fieldName: 'created_by', + }) + public createdBy!: IUsers; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/roles.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/roles.ts new file mode 100644 index 00000000..3dcd026d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/roles.ts @@ -0,0 +1,67 @@ +import { + Entity, + PrimaryKey, + Property, + ManyToMany, + Collection, +} from '@mikro-orm/core'; + +import { Users, IUsers } from '.'; + +export type IRoles = Roles; + +@Entity({ + tableName: 'roles', +}) +export class Roles { + @PrimaryKey({ + autoincrement: true, + }) + public id!: number; + + @Property({ + type: 'varchar', + length: 128, + nullable: true, + default: 'NULL', + }) + public name!: string; + + @Property({ + type: 'varchar', + length: 128, + nullable: false, + unique: true, + }) + public key!: string; + + @Property({ + name: 'is_default', + type: 'boolean', + default: false, + }) + public isDefault!: boolean; + + @Property({ + length: 0, + name: 'created_at', + nullable: true, + defaultRaw: 'CURRENT_TIMESTAMP(0)', + columnType: 'timestamp(0) without time zone', + type: 'timestamp', + }) + createdAt: Date = new Date(); + + @Property({ + length: 0, + onUpdate: () => new Date(), + name: 'updated_at', + nullable: true, + columnType: 'timestamp(0) without time zone', + defaultRaw: 'CURRENT_TIMESTAMP(0)', + }) + updatedAt: Date = new Date(); + + @ManyToMany(() => Users, (item) => item.roles) + public users = new Collection(this); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/user-groups.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/user-groups.ts new file mode 100644 index 00000000..b62ac057 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/user-groups.ts @@ -0,0 +1,30 @@ +import { + PrimaryKey, + OneToMany, + Entity, + Property, + Collection, +} from '@mikro-orm/core'; + +import { Users } from './index'; + +@Entity({ + tableName: 'user_groups', +}) +export class UserGroups { + @PrimaryKey({ + autoincrement: true, + }) + public id!: number; + + @Property({ + type: 'string', + length: 50, + unique: true, + columnType: 'varchar', + }) + public label!: string; + + @OneToMany(() => Users, (item) => item.userGroup) + public users = new Collection(this); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/users.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/users.ts new file mode 100644 index 00000000..08d1f31c --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/users.ts @@ -0,0 +1,146 @@ +import { + Entity, + PrimaryKey, + Property, + OneToOne, + Collection, + ManyToMany, + OneToMany, + ManyToOne, + ArrayType, + Type, +} from '@mikro-orm/core'; + +import { + Addresses, + Roles, + Comments, + Notes, + UserGroups, + IAddresses, +} from './index'; + +export type IUsers = Users; + +export class MyDateType extends Type {} + +@Entity({ + tableName: 'users', +}) +export class Users { + @PrimaryKey({ + autoincrement: true, + }) + public id!: number; + + @Property({ + type: 'string', + length: 100, + unique: true, + }) + public login!: string; + + @Property({ + name: 'first_name', + type: 'varchar', + length: 100, + nullable: true, + }) + public firstName!: string; + + @Property({ + name: 'test_real', + type: new ArrayType((i) => parseFloat(i)), + columnType: 'real[]', + defaultRaw: `ARRAY[]::real[]`, + default: [], + }) + public testReal: number[] = []; + + @Property({ + name: 'test_array_null', + type: new ArrayType((i) => parseFloat(i)), + columnType: 'real[]', + nullable: true, + }) + public testArrayNull!: number[] | null; + + @Property({ + name: 'last_name', + type: 'string', + columnType: 'varchar', + length: 100, + nullable: true, + }) + public lastName!: string; + + @Property({ + name: 'is_active', + type: 'boolean', + nullable: true, + default: false, + }) + public isActive!: boolean; + + @Property({ + name: 'test_date', + type: Date, + nullable: true, + defaultRaw: 'CURRENT_TIMESTAMP(0)', + columnType: 'timestamp(0) without time zone', + }) + public testDate!: Date; + + @Property({ + length: 0, + name: 'created_at', + nullable: true, + defaultRaw: 'CURRENT_TIMESTAMP(0)', + columnType: 'timestamp(0) without time zone', + }) + createdAt: Date = new Date(); + + @Property({ + length: 0, + onUpdate: () => new Date(), + name: 'updated_at', + nullable: true, + columnType: 'timestamp(0) without time zone', + defaultRaw: 'CURRENT_TIMESTAMP(0)', + }) + updatedAt: Date = new Date(); + + @OneToOne(() => Addresses, { + owner: true, + fieldName: 'addresses_id', + nullable: true, + }) + public addresses!: IAddresses; + + @OneToOne(() => Users, { + owner: true, + nullable: true, + fieldName: 'manager_id', + }) + public manager!: IUsers; + + @ManyToMany(() => Roles, (role) => role.users, { + owner: true, + pivotTable: 'users_have_roles', + inverseJoinColumn: 'role_id', + joinColumn: 'user_id', + }) + public roles = new Collection(this); + + @OneToMany(() => Comments, (comment) => comment.createdBy) + public comments = new Collection(this); + + @OneToMany(() => Notes, (item) => item.createdBy) + public notes = new Collection(this); + + @ManyToOne(() => UserGroups, { + fieldName: 'user_groups_id', + nullable: true, + }) + public userGroup!: UserGroups; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/index.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/index.ts new file mode 100644 index 00000000..ed7ef0d9 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/index.ts @@ -0,0 +1,108 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { MikroOrmModule } from '@mikro-orm/nestjs'; +import { EntityManager, MikroORM } from '@mikro-orm/core'; +import { QueryField } from '../../utils/nestjs-shared'; + +import { + Addresses, + Comments, + Notes, + Roles, + UserGroups, + Users, +} from './entities'; +import { ObjectLiteral } from '../../types'; +import { Query } from '../../modules/mixin/zod'; + +import { + CurrentEntityManager, + CurrentEntityMetadata, + CurrentEntityRepository, + CurrentMicroOrmProvider, + OrmServiceFactory, + EntityPropsMap, +} from '../../modules/micro-orm/factory'; +import { MicroOrmUtilService } from '../../modules/micro-orm/service/micro-orm-util.service'; +import { CURRENT_ENTITY, GLOBAL_MODULE_OPTIONS_TOKEN } from '../../constants'; + +export * from './entities'; +export * from './utils'; + +import { sharedConnect, initMikroOrm, pullAllData } from './utils'; +import { DEFAULT_ARRAY_TYPE } from '../../modules/micro-orm/constants'; +import { JsonApiTransformerService } from '../../modules/mixin/service/json-api-transformer.service'; + +export const entities = [Users, UserGroups, Roles, Comments, Addresses, Notes]; + +export function mockDbPgLiteTestModule(dbName = `test_db_${Date.now()}`) { + const mikroORM = { + provide: MikroORM, + useFactory: async () => { + const knexInst = await sharedConnect(); + return initMikroOrm(knexInst, dbName); + }, + }; + return { + module: MikroOrmModule, + providers: [mikroORM], + exports: [mikroORM], + }; +} + +const readOnlyDbName = `readonly_db_${Date.now()}`; + +export function dbRandomName(readOnly = false) { + if (readOnly) { + return readOnlyDbName; + } + return `test_db_${Date.now()}`; +} + +export async function pullData(em: EntityManager, count = 1) { + for (let i = 0; i < count; i++) { + await pullAllData(em); + } +} + +export function getModuleForPgLite( + entity: E, + dbName = `test_db_${Date.now()}` +): Promise { + return Test.createTestingModule({ + imports: [mockDbPgLiteTestModule(dbName)], + providers: [ + CurrentMicroOrmProvider(), + CurrentEntityManager(), + CurrentEntityMetadata(), + CurrentEntityRepository(entity), + MicroOrmUtilService, + { + provide: CURRENT_ENTITY, + useValue: entity, + }, + OrmServiceFactory(), + EntityPropsMap(entities as any), + { + provide: GLOBAL_MODULE_OPTIONS_TOKEN, + useValue: { options: { arrayType: DEFAULT_ARRAY_TYPE } }, + }, + JsonApiTransformerService, + ], + }).compile(); +} + +export function getDefaultQuery(): Query { + return { + [QueryField.filter]: { + relation: null, + target: null, + }, + [QueryField.fields]: null, + [QueryField.include]: null, + [QueryField.sort]: null, + [QueryField.page]: { + size: 1, + number: 1, + }, + } satisfies Query; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/index.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/index.ts new file mode 100644 index 00000000..fa93c59d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/index.ts @@ -0,0 +1,3 @@ +export * from './provider-entities'; +export * from './pull-data'; +export * from './init-db'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/init-db.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/init-db.ts new file mode 100644 index 00000000..a213e21c --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/init-db.ts @@ -0,0 +1,81 @@ +import { Knex as TypeKnex } from '@mikro-orm/knex'; +import { MikroORM } from '@mikro-orm/core'; +import { PostgreSqlDriver } from '@mikro-orm/postgresql'; +import { SqlHighlighter } from '@mikro-orm/sql-highlighter'; + +import Knex from 'knex'; +import * as ClientPgLite from 'knex-pglite'; + +import { + Addresses, + Comments, + Notes, + Roles, + UserGroups, + Users, +} from '../entities'; + +let knexInst: TypeKnex; + +export async function sharedConnect(): Promise { + // @ts-ignore + // return globalThis.pgLite; + + if (knexInst) { + return knexInst; + } + + const pgLite = await Promise.all([ + import('@electric-sql/pglite'), + // @ts-ignore + import('@electric-sql/pglite/contrib/uuid_ossp'), + ]).then( + ([{ PGlite }, { uuid_ossp }]) => + new PGlite({ + extensions: { uuid_ossp }, + }) + ); + + knexInst = Knex({ + // @ts-ignore + client: ClientPgLite, + dialect: 'postgres', + // @ts-ignore + connection: { pglite: pgLite }, + }); + + return knexInst; +} + +export async function initMikroOrm(knex: TypeKnex, testDbName: string) { + const result = await knex.raw( + `select 1 from pg_database where datname = '${testDbName}'` + ); + + if ((result['rows'] as []).length === 0) { + await knex.raw(`create database ??`, [testDbName]); + } + + const orm = await MikroORM.init({ + highlighter: new SqlHighlighter(), + driver: PostgreSqlDriver, + dbName: testDbName, + driverOptions: knexInst, + entities: [Users, UserGroups, Roles, Comments, Addresses, Notes], + allowGlobalContext: true, + schema: 'public', + debug: + process.env['DB_LOGGING'] !== '0' ? ['query', 'query-params'] : false, + }); + + if ((result['rows'] as []).length === 0) { + const sql = await orm.getSchemaGenerator().getCreateSchemaSQL(); + const statements = sql.split(';').filter((s) => s.trim().length > 0); // Разбиваем на отдельные команды + + for (const statement of statements) { + await orm.em.execute(statement); + } + } + + return orm; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/provider-entities.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/provider-entities.ts new file mode 100644 index 00000000..44087e7a --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/provider-entities.ts @@ -0,0 +1,30 @@ +import { TestingModule } from '@nestjs/testing'; +import { EntityManager } from '@mikro-orm/core'; +import { + Users, + Addresses, + Comments, + Roles, + UserGroups, + Notes, +} from '../entities'; + +export function getRepository(module: TestingModule, emToken: symbol) { + const em = module.get(emToken); + + const userRepository = em.getRepository(Users); + const addressesRepository = em.getRepository(Addresses); + const notesRepository = em.getRepository(Notes); + const commentsRepository = em.getRepository(Comments); + const rolesRepository = em.getRepository(Roles); + const userGroupRepository = em.getRepository(UserGroups); + + return { + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository, + }; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/pull-data.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/pull-data.ts new file mode 100644 index 00000000..5ea47448 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/pull-data.ts @@ -0,0 +1,135 @@ +import { EntityManager } from '@mikro-orm/core'; +import { faker } from '@faker-js/faker'; +import { + Addresses, + CommentKind, + Comments, + Notes, + Roles, + UserGroups, + Users, +} from '../entities'; + +export async function pullAddress() { + const address = new Addresses(); + address.city = faker.location.city(); + address.country = faker.location.country(); + address.arrayField = [ + faker.string.alphanumeric(5), + faker.string.alphanumeric(5), + ]; + address.state = faker.location.state(); + return address; +} + +export async function pullComment() { + const comment = new Comments(); + comment.text = faker.lorem.paragraph(faker.number.int(5)); + comment.kind = CommentKind.Comment; + return comment; +} + +export async function pullNote() { + const note = new Notes(); + note.text = faker.lorem.paragraph(faker.number.int(5)); + return note; +} + +export async function pullRole() { + const role = new Roles(); + role.key = faker.string.alphanumeric(5); + role.name = faker.word.words(); + return role; +} + +export async function pullUser() { + const user = new Users(); + user.firstName = faker.person.firstName(); + user.lastName = faker.person.lastName(); + user.isActive = faker.datatype.boolean(); + user.login = faker.internet.userName({ + lastName: user.lastName, + firstName: user.firstName, + }); + user.testReal = [faker.number.float({ fractionDigits: 4 })]; + user.testArrayNull = null; + + user.testDate = faker.date.anytime(); + + return user; +} + +export async function pullUserGroup() { + const userGroup = new UserGroups(); + userGroup.label = faker.string.alphanumeric(5); + return userGroup; +} + +export async function pullAllData(em: EntityManager) { + const user = await pullUser(); + + const address1 = await pullAddress(); + const address2 = await pullAddress(); + + const note1 = await pullNote(); + const note2 = await pullNote(); + const note3 = await pullNote(); + + const comment1 = await pullComment(); + const comment2 = await pullComment(); + const comment3 = await pullComment(); + const comment4 = await pullComment(); + + const userGroup1 = await pullUserGroup(); + const userGroup2 = await pullUserGroup(); + const userGroup3 = await pullUserGroup(); + + const role1 = await pullRole(); + const role2 = await pullRole(); + const role3 = await pullRole(); + + const roleX1 = await pullRole(); + const roleX2 = await pullRole(); + const roleX3 = await pullRole(); + + const managerUser = await pullUser(); + + user.addresses = address1; + address1.user = user; + user.notes.add(note1, note2, note3); + user.comments.add(comment1, comment2, comment3, comment4); + user.userGroup = userGroup1; + user.roles.add(roleX1, roleX2, roleX3); + user.manager = managerUser; + + managerUser.addresses = address2; + managerUser.userGroup = userGroup3; + managerUser.roles.add(role1, role2); + + await em.persistAndFlush([ + user, + address1, + address2, + note1, + note2, + note3, + comment1, + comment2, + comment3, + comment4, + userGroup1, + userGroup2, + userGroup3, + role1, + role2, + role3, + roleX1, + roleX2, + roleX3, + managerUser, + ]); + + await em.flush(); + + return user; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/addresses.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/addresses.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/addresses.ts rename to libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/addresses.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/comments.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/comments.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/comments.ts rename to libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/comments.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/index.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/index.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/index.ts rename to libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/index.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/notes.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/notes.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/notes.ts rename to libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/notes.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/pods.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/pods.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/pods.ts rename to libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/pods.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests-have-pod-locks.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/requests-have-pod-locks.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests-have-pod-locks.ts rename to libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/requests-have-pod-locks.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/requests.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests.ts rename to libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/requests.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/roles.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/roles.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/roles.ts rename to libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/roles.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/user-groups.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/user-groups.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/user-groups.ts rename to libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/user-groups.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/users.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/users.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/users.ts rename to libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/users.ts index bdf61878..070ae412 100644 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/users.ts +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/users.ts @@ -72,20 +72,20 @@ export class Users { public isActive!: boolean; @Column({ - name: 'created_at', + name: 'test_date', type: 'timestamp', nullable: true, default: 'CURRENT_TIMESTAMP', }) - public createdAt!: Date; + public testDate!: Date; @Column({ - name: 'test_date', + name: 'created_at', type: 'timestamp', nullable: true, default: 'CURRENT_TIMESTAMP', }) - public testDate!: Date; + public createdAt!: Date; @UpdateDateColumn({ name: 'updated_at', diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/index.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/index.ts new file mode 100644 index 00000000..f7eba83d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/index.ts @@ -0,0 +1,60 @@ +import { TypeOrmModule } from '@nestjs/typeorm'; +import { DynamicModule } from '@nestjs/common'; +import { IMemoryDb } from 'pg-mem'; + +import { + Addresses, + Comments, + Notes, + Pods, + Requests, + RequestsHavePodLocks, + Roles, + UserGroups, + Users, +} from './entities'; +import { DataSource } from 'typeorm'; + +export * from './entities'; +export * from './utils'; + +export const entities = [ + Users, + UserGroups, + Roles, + RequestsHavePodLocks, + Requests, + Pods, + Comments, + Addresses, + Notes, +]; + +export function mockDBTestModule(db: IMemoryDb): DynamicModule { + return TypeOrmModule.forRootAsync({ + useFactory() { + return { + type: 'postgres', + // logging: true, + entities: [ + Users, + UserGroups, + Roles, + RequestsHavePodLocks, + Requests, + Pods, + Comments, + Addresses, + Notes, + ], + }; + }, + async dataSourceFactory(options) { + const dataSource: DataSource = await db.adapters.createTypeormDataSource( + options + ); + + return dataSource; + }, + }); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/index.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/utils/index.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/index.ts rename to libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/utils/index.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/provider-entities.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/utils/provider-entities.ts similarity index 96% rename from libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/provider-entities.ts rename to libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/utils/provider-entities.ts index 4b47577b..f0bca9e1 100644 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/provider-entities.ts +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/utils/provider-entities.ts @@ -12,7 +12,7 @@ import { UserGroups, } from '../entities'; import { Users } from '../entities'; -import { DEFAULT_CONNECTION_NAME } from '../../constants'; +import { DEFAULT_CONNECTION_NAME } from '../../../constants'; import { TestingModule } from '@nestjs/testing'; export function providerEntities( diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/pull-data.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/utils/pull-data.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/pull-data.ts rename to libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/utils/pull-data.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/atomic-operation.module.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/atomic-operation.module.ts index ec16128c..46bf6797 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/atomic-operation.module.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/atomic-operation.module.ts @@ -17,13 +17,13 @@ import { ZodInputOperation, AsyncIterate, } from './factory'; -import { ModuleOptions } from '../../types'; +import { ResultModuleOptions } from '../../types'; import { MAP_CONTROLLER_INTERCEPTORS, OPTIONS } from './constants'; @Module({}) export class AtomicOperationModule implements NestModule { static forRoot( - options: ModuleOptions, + options: ResultModuleOptions, entityModules: DynamicModule[], commonModule: DynamicModule ): DynamicModule { @@ -37,7 +37,7 @@ export class AtomicOperationModule implements NestModule { AsyncIterate, MapControllerEntity(options.entities, entityModules), MapEntityNameToEntity(options.entities), - ZodInputOperation(options.connectionName), + ZodInputOperation(), { provide: MAP_CONTROLLER_INTERCEPTORS, useValue: new Map(), diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.spec.ts index af5511e2..1206658c 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.spec.ts @@ -8,13 +8,12 @@ import { IMemoryDb } from 'pg-mem'; import { OperationController } from './operation.controller'; import { ExecuteService, ExplorerService } from '../service'; import { InputArray, Operation } from '../utils'; -import { JsonBaseController } from '../../../mixin/controller/json-base.controller'; +import { JsonBaseController } from '../../mixin/controller/json-base.controller'; import { - createAndPullSchemaBase, mockDBTestModule, providerEntities, Users, -} from '../../../mock-utils'; +} from '../../../mock-utils/typeorm'; import { ASYNC_ITERATOR_FACTORY, @@ -26,10 +25,14 @@ import { OPTIONS, } from '../constants'; -import { CurrentDataSourceProvider } from '../../../factory'; -import { DEFAULT_CONNECTION_NAME } from '../../../constants'; import { OperationMethode } from '../types'; import { AsyncLocalStorage } from 'async_hooks'; +import { ObjectLiteral } from '../../../types'; +import { + CURRENT_DATA_SOURCE_TOKEN, + RUN_IN_TRANSACTION_FUNCTION, +} from '../../../constants'; +import { createAndPullSchemaBase } from '../../../mock-utils'; describe('OperationController', () => { let db: IMemoryDb; @@ -44,13 +47,20 @@ describe('OperationController', () => { controllers: [OperationController], providers: [ ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), + { + provide: CURRENT_DATA_SOURCE_TOKEN, + useValue: {}, + }, ExplorerService, ExecuteService, { provide: MAP_ENTITY, useValue: {}, }, + { + provide: RUN_IN_TRANSACTION_FUNCTION, + useValue: {}, + }, { provide: MAP_CONTROLLER_ENTITY, useValue: {}, @@ -117,7 +127,7 @@ describe('OperationController', () => { const getMethodNameByParamSpy = jest .spyOn(explorerService, 'getMethodNameByParam') .mockReturnValue( - paramsForExecuteMock[0].methodName as OperationMethode + paramsForExecuteMock[0].methodName as OperationMethode ); const getModulesByControllerSpy = jest .spyOn(explorerService, 'getParamsForMethod') @@ -150,7 +160,6 @@ describe('OperationController', () => { expect(getParamsForMethodSpy).toHaveBeenCalledWith( paramsForExecuteMock[0].controller ); - // expect(runSpy).toHaveBeenCalledWith(paramsForExecuteMock[0].module, paramsForExecuteMock[0].methodName, paramsForExecuteMock[0].params); expect(runSpy).toHaveBeenCalledWith(paramsForExecuteMock, []); }); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.ts index 4ce1510b..7042e289 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.ts @@ -14,8 +14,8 @@ import { InputOperationPipe } from '../pipes/input-operation.pipe'; import { ExecuteService, ExplorerService } from '../service'; import { KEY_MAIN_INPUT_SCHEMA, KEY_MAIN_OUTPUT_SCHEMA } from '../constants'; import { OperationMethode, ParamsForExecute } from '../types'; -import { JsonBaseController } from '../../../mixin/controller/json-base.controller'; -import { Entity, ValidateQueryError } from '../../../types'; +import { JsonBaseController } from '../../mixin/controller/json-base.controller'; +import { ObjectLiteral as Entity, ValidateQueryError } from '../../../types'; @Controller('/') export class OperationController { @@ -33,7 +33,7 @@ export class OperationController { } = dataInput; let controller: Type>; - let methodName: OperationMethode; + let methodName: OperationMethode; let module: Module; try { controller = this.explorerService.getControllerByEntityName(type); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-entity-name-to-entity.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-entity-name-to-entity.ts index 8149f47a..848eea94 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-entity-name-to-entity.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-entity-name-to-entity.ts @@ -1,8 +1,10 @@ import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type'; import { ValueProvider } from '@nestjs/common'; +import { camelToKebab } from '../../../utils/nestjs-shared'; import { MapEntity } from '../types'; import { MAP_ENTITY } from '../constants'; -import { camelToKebab, getEntityName } from '../../../helper'; +import { getEntityName } from '../../mixin/helper'; +import { AnyEntity, EntityTarget } from '../../../types'; export function MapEntityNameToEntity( entities: EntityClassOrSchema[] @@ -11,7 +13,7 @@ export function MapEntityNameToEntity( provide: MAP_ENTITY, useValue: entities.reduce( (acum, item) => acum.set(camelToKebab(getEntityName(item)), item), - new Map() + new Map>() ), }; } diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/zod-input-operation.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/zod-input-operation.ts index 21d58521..75ddeb40 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/zod-input-operation.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/zod-input-operation.ts @@ -1,30 +1,22 @@ -import { DataSource } from 'typeorm'; import { FactoryProvider } from '@nestjs/common'; import { MAP_CONTROLLER_ENTITY, ZOD_INPUT_OPERATION } from '../constants'; import { MapController } from '../types'; -import { CURRENT_DATA_SOURCE_TOKEN } from '../../../constants'; -import { - zodInputOperation, - ZodInputOperation as TypeZodInputOperation, -} from '../utils/zod/zod-helper'; +import { zodInputOperation, ZodInputOperation } from '../utils'; +import { ENTITY_MAP_PROPS } from '../../../constants'; +import { ZodEntityProps } from '../../mixin/types'; +import { EntityClass, ObjectLiteral } from '../../../types'; -export function ZodInputOperation( - connectionName?: string -): FactoryProvider { +export function ZodInputOperation(): FactoryProvider< + ZodInputOperation +> { return { provide: ZOD_INPUT_OPERATION, - useFactory(dataSource: DataSource, mapController: MapController) { - return zodInputOperation(dataSource, mapController); + useFactory( + mapController: MapController, + entityMapProps: Map, ZodEntityProps> + ) { + return zodInputOperation(mapController, entityMapProps); }, - inject: [ - { - token: CURRENT_DATA_SOURCE_TOKEN, - optional: false, - }, - { - token: MAP_CONTROLLER_ENTITY, - optional: false, - }, - ], + inject: [MAP_CONTROLLER_ENTITY, ENTITY_MAP_PROPS], }; } diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/index.ts new file mode 100644 index 00000000..81ce0c2a --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/index.ts @@ -0,0 +1 @@ +export * from './atomic-operation.module'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.ts index 11ca8652..a920101f 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.ts @@ -6,14 +6,15 @@ import { } from '@nestjs/common'; import { errorMap } from 'zod-validation-error'; import { ZodError } from 'zod'; -import { JSONValue } from '../../../types'; +import { JSONValue } from '../../mixin/types'; import { InputArray, ZodInputOperation } from '../utils'; import { KEY_MAIN_INPUT_SCHEMA, ZOD_INPUT_OPERATION } from '../constants'; export class InputOperationPipe implements PipeTransform { - @Inject(ZOD_INPUT_OPERATION) private zodInputOperation!: ZodInputOperation; + @Inject(ZOD_INPUT_OPERATION) + private zodInputOperation!: ZodInputOperation; transform(value: JSONValue): InputArray { try { diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.spec.ts index 5b2f6bb7..45723122 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.spec.ts @@ -1,7 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ModuleRef } from '@nestjs/core'; import { ROUTE_ARGS_METADATA } from '@nestjs/common/constants'; -import { DataSource } from 'typeorm'; import { ExecuteService, isZodError } from './execute.service'; import { IterateFactory } from '../factory'; import { @@ -10,7 +9,7 @@ import { MAP_CONTROLLER_INTERCEPTORS, OPTIONS, } from '../constants'; -import { CURRENT_DATA_SOURCE_TOKEN } from '../../../constants'; + import { HttpException, NotFoundException, @@ -19,22 +18,21 @@ import { } from '@nestjs/common'; import { ParamsForExecute } from '../types'; import { AsyncLocalStorage } from 'async_hooks'; +import { RUN_IN_TRANSACTION_FUNCTION } from '../../../constants'; describe('ExecuteService', () => { let service: ExecuteService; - let dataSource: DataSource; + let runInTransaction: jest.Mock; let moduleRef: ModuleRef; let asyncIteratorFactory: IterateFactory; - let mapControllerInterceptors = new Map(); + const mapControllerInterceptors = new Map(); beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ ExecuteService, { - provide: CURRENT_DATA_SOURCE_TOKEN, - useValue: { - createQueryRunner: () => {}, - }, + provide: RUN_IN_TRANSACTION_FUNCTION, + useValue: jest.fn(), }, { provide: ModuleRef, @@ -64,7 +62,7 @@ describe('ExecuteService', () => { }).compile(); service = module.get(ExecuteService); - dataSource = module.get(CURRENT_DATA_SOURCE_TOKEN); + runInTransaction = module.get(RUN_IN_TRANSACTION_FUNCTION); moduleRef = module.get(ModuleRef); asyncIteratorFactory = module.get(ASYNC_ITERATOR_FACTORY); mapControllerInterceptors.clear(); @@ -83,15 +81,7 @@ describe('ExecuteService', () => { }, ] as ParamsForExecute[]; - const queryRunnerMock = { - startTransaction: jest.fn(), - commitTransaction: jest.fn(), - rollbackTransaction: jest.fn(), - release: jest.fn(), - }; - jest - .spyOn(dataSource, 'createQueryRunner') - .mockReturnValue(queryRunnerMock as any); + runInTransaction.mockImplementationOnce((args: () => {}) => args()); jest.spyOn(service as any, 'executeOperations').mockImplementation(() => { throw new NotFoundException(); @@ -99,33 +89,18 @@ describe('ExecuteService', () => { await expect(service.run(params, [])).rejects.toThrow(NotFoundException); - expect(queryRunnerMock.rollbackTransaction).toHaveBeenCalled(); - expect(queryRunnerMock.release).toHaveBeenCalled(); - await expect(service.run(params, [])).rejects.toThrow(NotFoundException); + expect(runInTransaction).toHaveBeenCalled(); }); it('should return an empty array if no operations are executed', async () => { const params: ParamsForExecute[] = []; - const queryRunnerMock = { - startTransaction: jest.fn(), - commitTransaction: jest.fn(), - rollbackTransaction: jest.fn(), - release: jest.fn(), - }; - jest - .spyOn(dataSource, 'createQueryRunner') - .mockReturnValue(queryRunnerMock as any); - + runInTransaction.mockImplementationOnce((args: () => {}) => args()); jest.spyOn(service as any, 'executeOperations').mockReturnValue([]); const result = await service.run(params, []); expect(result).toEqual([]); - - expect(queryRunnerMock.startTransaction).toHaveBeenCalled(); - expect(queryRunnerMock.commitTransaction).toHaveBeenCalled(); - expect(queryRunnerMock.rollbackTransaction).not.toHaveBeenCalled(); - expect(queryRunnerMock.release).toHaveBeenCalled(); + expect(runInTransaction).toHaveBeenCalled(); }); }); @@ -475,4 +450,160 @@ describe('ExecuteService', () => { }); }); }); + + describe('ExecuteService - replaceTmpIds', () => { + let service: ExecuteService; + + beforeEach(() => { + service = new ExecuteService(); + }); + + it('should be return id first input params of array is undefined', () => { + const inputParams = [] as any; + const tmpIdsMap = {}; + + const result = service.replaceTmpIds(inputParams, tmpIdsMap); + expect(result).toEqual(inputParams); + }); + + it('should be return id first input params of array is string', () => { + const inputParams = ['string'] as any; + const tmpIdsMap = {}; + + const result = service.replaceTmpIds(inputParams, tmpIdsMap); + expect(result).toEqual(inputParams); + }); + + it('should be return id first input params of array is number', () => { + const inputParams = [1] as any; + const tmpIdsMap = {}; + + const result = service.replaceTmpIds(inputParams, tmpIdsMap); + expect(result).toEqual(inputParams); + }); + it('should be return id first input params of array is array', () => { + const inputParams = [[]] as any; + const tmpIdsMap = {}; + + const result = service.replaceTmpIds(inputParams, tmpIdsMap); + expect(result).toEqual(inputParams); + }); + + it('should be return id first input params of array is object', () => { + const inputParams = [{}] as any; + const tmpIdsMap = {}; + + const result = service.replaceTmpIds(inputParams, tmpIdsMap); + expect(result).toEqual(inputParams); + }); + + it('should be return id first input params of array is object with undefined of relationships', () => { + const inputParams = [{ relationships: undefined }] as any; + const tmpIdsMap = {}; + + const result = service.replaceTmpIds(inputParams, tmpIdsMap); + expect(result).toEqual(inputParams); + }); + + it('should be not replace of relation with object', () => { + const inputParams = [ + { + relationships: { + addresses: { data: { id: '1234', type: 'addresses' } }, + }, + }, + ] as any; + const tmpIdsMap = {}; + + const result = service.replaceTmpIds(inputParams, tmpIdsMap); + expect(result).toEqual(inputParams); + }); + + it('should be not replace of relation with array of object', () => { + const inputParams = [ + { + relationships: { + addresses: { + data: [ + { id: '1234', type: 'addresses' }, + { id: '1235', type: 'addresses' }, + ], + }, + }, + }, + ] as any; + const tmpIdsMap = {}; + + const result = service.replaceTmpIds(inputParams, tmpIdsMap); + expect(result).toEqual(inputParams); + }); + + it('should be replace of relation with object', () => { + const inputParams = [ + { + relationships: { + addresses: { data: { id: '1234', type: 'addresses' } }, + }, + }, + ] as any; + const newId = '4321'; + const tmpIdsMap = { '1234': newId }; + + const checkResult = [ + { + ...inputParams[0], + relationships: { + ...inputParams[0].relationships, + addresses: { + ...inputParams[0].relationships.addresses, + data: { + ...inputParams[0].relationships.addresses.data, + id: newId, + }, + }, + }, + }, + ]; + const result = service.replaceTmpIds(inputParams, tmpIdsMap); + expect(result).toEqual(checkResult); + }); + + it('should be replace of relation with array of object', () => { + const inputParams = [ + { + relationships: { + addresses: { + data: [ + { id: '12345', type: 'addresses' }, + { id: '1234', type: 'addresses' }, + ], + }, + }, + }, + ] as any; + const newId = '4321'; + const tmpIdsMap = { '1234': newId }; + + const checkResult = [ + { + ...inputParams[0], + relationships: { + ...inputParams[0].relationships, + addresses: { + ...inputParams[0].relationships.addresses, + data: [ + inputParams[0].relationships.addresses.data[0], + { + ...inputParams[0].relationships.addresses.data[1], + id: newId, + }, + ], + }, + }, + }, + ]; + const result = service.replaceTmpIds(inputParams, tmpIdsMap); + expect(result).toEqual(checkResult); + }); + }); }); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.ts index 8bec403f..a59606ed 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.ts @@ -13,25 +13,11 @@ import { import { Module } from '@nestjs/core/injector/module'; import { ArgumentMetadata } from '@nestjs/common/interfaces/features/pipe-transform.interface'; import { ApplicationConfig, ModuleRef, NestContainer } from '@nestjs/core'; -import { DataSource } from 'typeorm'; - -import { MapControllerInterceptor, ParamsForExecute } from '../types'; -import { CURRENT_DATA_SOURCE_TOKEN } from '../../../constants'; -import { - ASYNC_ITERATOR_FACTORY, - KEY_MAIN_INPUT_SCHEMA, - MAP_CONTROLLER_INTERCEPTORS, - OPTIONS, -} from '../constants'; -import { IterateFactory } from '../factory'; import { - ConfigParam, + ObjectTyped, ResourceObject, ResourceObjectRelationships, - TypeFromType, - ValidateQueryError, -} from '../../../types'; -import { ObjectTyped } from '../../../helper'; +} from '../../../utils/nestjs-shared'; import { InterceptorsConsumer, InterceptorsContextCreator, @@ -40,6 +26,17 @@ import { Controller } from '@nestjs/common/interfaces'; import { lastValueFrom } from 'rxjs'; import { AsyncLocalStorage } from 'async_hooks'; +import { MapControllerInterceptor, ParamsForExecute } from '../types'; +import { + ASYNC_ITERATOR_FACTORY, + KEY_MAIN_INPUT_SCHEMA, + MAP_CONTROLLER_INTERCEPTORS, +} from '../constants'; +import { IterateFactory } from '../factory'; +import { TypeFromType } from '../../mixin/types'; +import { RunInTransaction, ValidateQueryError } from '../../../types'; +import { RUN_IN_TRANSACTION_FUNCTION } from '../../../constants'; + export function isZodError( param: string | unknown ): param is { message: ValidateQueryError[] } { @@ -53,7 +50,7 @@ export function isZodError( @Injectable() export class ExecuteService { - @Inject(CURRENT_DATA_SOURCE_TOKEN) private readonly dataSource!: DataSource; + // @Inject(CURRENT_DATA_SOURCE_TOKEN) private readonly dataSource!: DataSource; @Inject(ModuleRef) private readonly moduleRef!: ModuleRef & { container: NestContainer; applicationConfig: ApplicationConfig; @@ -62,7 +59,10 @@ export class ExecuteService { @Inject(ASYNC_ITERATOR_FACTORY) private asyncIteratorFactory!: IterateFactory< ExecuteService['runOneOperation'] >; - @Inject(OPTIONS) private options!: ConfigParam; + @Inject(RUN_IN_TRANSACTION_FUNCTION) + private runInTransaction!: RunInTransaction< + () => ReturnType + >; @Inject(MAP_CONTROLLER_INTERCEPTORS) private mapControllerInterceptor!: MapControllerInterceptor; @@ -84,31 +84,10 @@ export class ExecuteService { private interceptorsConsumer = new InterceptorsConsumer(); async run(params: ParamsForExecute[], tmpIds: (string | number)[]) { - if ( - this.options.runInTransaction && - typeof this.options.runInTransaction === 'function' - ) { - return this.options.runInTransaction('READ UNCOMMITTED', () => { - return this.executeOperations(params, tmpIds); - }); - } - const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.startTransaction('READ UNCOMMITTED'); - try { - const resultArray = await this.executeOperations(params, tmpIds); - await queryRunner.commitTransaction(); - return resultArray; - } catch (e) { - await queryRunner.rollbackTransaction(); - throw e; - } finally { - await queryRunner.release(); - } - - return []; + return this.runInTransaction(() => this.executeOperations(params, tmpIds)); } - private async executeOperations( + protected async executeOperations( params: ParamsForExecute[], tmpIds: (string | number)[] = [] ) { @@ -216,7 +195,7 @@ export class ExecuteService { return resultInterceptors; } - private replaceTmpIds( + replaceTmpIds( inputParams: T, tmpIdsMap: Record ): T { @@ -235,30 +214,44 @@ export class ExecuteService { return inputParams; } - if (!('relationships' in bodyInput)) { + if (!(typeof bodyInput === 'object' && 'relationships' in bodyInput)) { return inputParams; } const { relationships } = bodyInput; + if (!relationships) { return inputParams; } bodyInput.relationships = ObjectTyped.entries(relationships).reduce( (acum, [name, val]) => { - if (Array.isArray(val)) { - acum[name] = (val as any[]).map((i) => { - i['id'] = tmpIdsMap[i['id']] ? tmpIdsMap[i['id']] : i['id']; - return i; - }) as never; + if (!val) throw new Error('Va; undefined'); + const { data } = val; + if (Array.isArray(data)) { + acum[name] = { + data: data.map((i) => { + if (i === null) return i; + return { + ...i, + id: tmpIdsMap[i['id']] ? `${tmpIdsMap[i['id']]}` : i['id'], + }; + }), + }; } else { - acum[name]['id'] = tmpIdsMap[val['id']] - ? (tmpIdsMap[val['id']] as never) - : acum[name]['id']; + if (!data) { + acum[name] = val; + } else { + data['id'] = tmpIdsMap[data['id']] + ? `${tmpIdsMap[data['id']]}` + : data['id']; + acum[name] = { + data, + }; + } } return acum; }, - // @ts-ignore { ...relationships } ); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.ts index 582421ef..57bc72b4 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.ts @@ -1,17 +1,18 @@ import { Inject, Injectable, Type } from '@nestjs/common'; import { Module } from '@nestjs/core/injector/module'; import { ModulesContainer } from '@nestjs/core'; +import { EntityRelation } from '../../../utils/nestjs-shared'; import { MAP_CONTROLLER_ENTITY, MAP_ENTITY } from '../constants'; import { MapController, MapEntity, OperationMethode } from '../types'; -import { Entity, EntityRelation } from '../../../types'; +import { ObjectLiteral as Entity } from '../../../types'; import { InputArray, Operation } from '../utils'; -import { JsonBaseController } from '../../../mixin/controller/json-base.controller'; +import { JsonBaseController } from '../../mixin/controller/json-base.controller'; import { PatchData, PatchRelationshipData, PostData, PostRelationshipData, -} from '../../../helper/zod'; +} from '../../mixin/zod'; @Injectable() export class ExplorerService { @@ -44,7 +45,7 @@ export class ExplorerService { operation: Operation, id?: string, rel?: string - ): OperationMethode { + ): OperationMethode { switch (operation) { case Operation.add: return id ? 'postRelationship' : 'postOne'; @@ -58,7 +59,7 @@ export class ExplorerService { } getParamsForMethod( - methodName: OperationMethode, + methodName: OperationMethode, data: InputArray[number] ): Parameters[typeof methodName]> { const { op, ref, ...other } = data; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/types/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/types/index.ts index f01175a9..57fe5ae5 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/types/index.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/types/index.ts @@ -1,25 +1,30 @@ import { NestInterceptor, Type } from '@nestjs/common'; import { Module } from '@nestjs/core/injector/module'; import { Controller } from '@nestjs/common/interfaces'; -import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type'; -import { Entity, MethodName } from '../../../types'; -import { JsonBaseController } from '../../../mixin/controller/json-base.controller'; +import { EntityTarget, ObjectLiteral } from '../../../types'; +import { JsonBaseController } from '../../mixin/controller/json-base.controller'; export type MapControllerInterceptor = Map< Controller, Map<(...arg: any) => any, NestInterceptor[]> >; -export type MapController = Map>; -export type MapEntity = Map; +export type MapController = Map< + EntityTarget, + Type +>; +export type MapEntity = Map< + string, + EntityTarget +>; -export type OperationMethode = keyof Omit< - { [k in MethodName]: string }, +export type OperationMethode = keyof Omit< + { [k in keyof JsonBaseController]: string }, 'getAll' | 'getOne' | 'getRelationship' >; export type ParamsForExecute< - E extends Entity = Entity, - O extends OperationMethode = OperationMethode + E extends ObjectLiteral = ObjectLiteral, + O extends OperationMethode = OperationMethode > = { methodName: O; controller: Type>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.spec.ts index 49d36c01..68a9977c 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.spec.ts @@ -1,7 +1,4 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { DataSource } from 'typeorm'; -import { IMemoryDb } from 'pg-mem'; import { z, ZodError } from 'zod'; import { Operation, @@ -16,16 +13,17 @@ import { zodUpdate, ZodUpdate, } from './zod-helper'; -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, - Users, -} from '../../../../mock-utils'; -import { DEFAULT_CONNECTION_NAME } from '../../../../constants'; -import { JsonBaseController } from '../../../../mixin/controller/json-base.controller'; +import { Users } from '../../../../mock-utils/typeorm'; +import { ENTITY_MAP_PROPS, FIELD_FOR_ENTITY } from '../../../../constants'; +import { JsonBaseController } from '../../../mixin/controller/json-base.controller'; import { MapController } from '../../types'; import { KEY_MAIN_INPUT_SCHEMA } from '../../constants'; +import { + GetFieldForEntity, + TupleOfEntityRelation, + ZodEntityProps, +} from '../../../mixin/types'; +import { EntityClass } from '../../../../types'; describe('ZodHelperSpec', () => { afterEach(() => { @@ -323,11 +321,16 @@ describe('ZodHelperSpec', () => { describe('zodOperationRel', () => { it('should be correct', () => { const user = 'user'; - const rel: ['address', 'notes'] = ['address', 'notes']; - const schema = zodOperationRel(user, rel, Operation.remove); - const check: z.infer< - ZodOperationRel<'user', ['address', 'notes'], Operation.remove> - > = { + const rel = [ + 'address', + 'notes', + ] as unknown as TupleOfEntityRelation; + const schema = zodOperationRel( + user, + rel, + Operation.remove + ); + const check: z.infer> = { op: Operation.remove, ref: { type: 'user', @@ -350,8 +353,15 @@ describe('ZodHelperSpec', () => { }); it('should be not correct', () => { const user = 'user'; - const rel: ['address', 'notes'] = ['address', 'notes']; - const schema = zodOperationRel(user, rel, Operation.remove); + const rel = [ + 'address', + 'notes', + ] as unknown as TupleOfEntityRelation; + const schema = zodOperationRel( + user, + rel, + Operation.remove + ); const check = { op: Operation.remove, @@ -439,25 +449,42 @@ describe('ZodHelperSpec', () => { }); }); describe('zodInputOperation', () => { - let db: IMemoryDb; - let dataSource: DataSource; + let getField: Map, ZodEntityProps>; beforeAll(async () => { - db = createAndPullSchemaBase(); const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [...providerEntities(getDataSourceToken())], + providers: [ + { + provide: ENTITY_MAP_PROPS, + useValue: new Map([ + [ + Users, + { + relations: [ + 'userGroup', + 'notes', + 'comments', + 'roles', + 'manager', + 'addresses', + ], + }, + ], + ]), + }, + ], }).compile(); - dataSource = module.get( - getDataSourceToken(DEFAULT_CONNECTION_NAME) - ); + getField = + module.get, ZodEntityProps>>( + ENTITY_MAP_PROPS + ); }); it('should be correct', () => { - const mapController: MapController = new Map([ + const mapController: MapController = new Map([ [Users as any, JsonBaseController], ]); - const schema = zodInputOperation(dataSource, mapController); - const check: z.infer = { + const schema = zodInputOperation(mapController, getField); + const check: z.infer> = { [KEY_MAIN_INPUT_SCHEMA]: [ { data: {}, @@ -496,10 +523,10 @@ describe('ZodHelperSpec', () => { }); it('incorrect input main data', () => { - const mapController: MapController = new Map([ + const mapController: MapController = new Map([ [Users as any, JsonBaseController], ]); - const schema = zodInputOperation(dataSource, mapController); + const schema = zodInputOperation(mapController, getField); const check = {}; const check1 = { ssdf: 'sdfsdf', @@ -533,9 +560,11 @@ describe('ZodHelperSpec', () => { return super.deleteOne(id); } } - const mapController: MapController = new Map([[Users as any, Test]]); - const schema = zodInputOperation(dataSource, mapController); - const check: z.infer = { + const mapController: MapController = new Map([ + [Users as any, Test], + ]); + const schema = zodInputOperation(mapController, getField); + const check: z.infer> = { [KEY_MAIN_INPUT_SCHEMA]: [ { data: {}, @@ -548,7 +577,7 @@ describe('ZodHelperSpec', () => { }, ], }; - const check1: z.infer = { + const check1: z.infer> = { [KEY_MAIN_INPUT_SCHEMA]: [ { data: {}, diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.ts index 19d6ce68..a1b83422 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.ts @@ -2,25 +2,24 @@ import { z, ZodArray, ZodLiteral, - ZodNullable, ZodNumber, ZodObject, ZodOptional, ZodString, ZodType, - ZodTypeAny, - ZodTypeDef, ZodUnion, } from 'zod'; -import { ZodUnionOptions } from 'zod/lib/types'; -import { DataSource } from 'typeorm'; +import { camelToKebab } from '../../../../utils/nestjs-shared'; import { KEY_MAIN_INPUT_SCHEMA } from '../../constants'; import { MapController } from '../../types'; -import { UnionToTupleMain } from '../../../../types'; -import { EntityTarget } from 'typeorm/common/EntityTarget'; -import { camelToKebab, getEntityName } from '../../../../helper'; -import { getField } from '../../../../helper/orm'; +import { + GetFieldForEntity, + TupleOfEntityRelation, + ZodEntityProps, +} from '../../../mixin/types'; +import { getEntityName } from '../../../mixin/helper'; +import { EntityClass, ObjectLiteral } from '../../../../types'; export enum Operation { add = 'add', @@ -32,22 +31,15 @@ const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); type Literal = z.infer; type Json = Literal | { [key: string]: Json } | Json[]; -const jsonSchema: ZodType = z.lazy(() => +const jsonSchema: ZodType = z.lazy(() => z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]) ); -type ZodGeneral = ZodNullable>; -const zodGeneralData: ZodGeneral = jsonSchema.nullable(); - -export type ZodAdd = ZodObject<{ - op: ZodLiteral; - ref: ZodObject<{ - type: ZodLiteral; - tmpId: ZodOptional>; - }>; - data: ZodGeneral; -}>; -export const zodAdd = (type: T): ZodAdd => +const zodGeneralData = jsonSchema.nullable(); +type ZodGeneral = typeof zodGeneralData; + +export type ZodAdd = ReturnType>; +export const zodAdd = (type: T) => z .object({ op: z.literal(Operation.add), @@ -61,15 +53,8 @@ export const zodAdd = (type: T): ZodAdd => }) .strict(); -export type ZodUpdate = ZodObject<{ - op: ZodLiteral; - ref: ZodObject<{ - type: ZodLiteral; - id: ZodString; - }>; - data: ZodGeneral; -}>; -export const zodUpdate = (type: T): ZodUpdate => +export type ZodUpdate = ReturnType>; +export const zodUpdate = (type: T) => z .object({ op: z.literal(Operation.update), @@ -82,14 +67,8 @@ export const zodUpdate = (type: T): ZodUpdate => data: zodGeneralData, }) .strict(); -export type ZodRemove = ZodObject<{ - op: ZodLiteral; - ref: ZodObject<{ - type: ZodLiteral; - id: ZodString; - }>; -}>; -export const zodRemove = (type: T): ZodRemove => +export type ZodRemove = ReturnType>; +export const zodRemove = (type: T) => z .object({ op: z.literal(Operation.remove), @@ -102,42 +81,16 @@ export const zodRemove = (type: T): ZodRemove => }) .strict(); -type RelToLiteralArray = UnionToTupleMain< - { - [K in Rel[number]]: ZodLiteral; - }[Rel[number]] ->; - -type RelLiteralArrayToUnion = T extends ZodUnionOptions - ? ZodUnion - : never; - -type ZodRelLiteral = RelLiteralArrayToUnion< - RelToLiteralArray ->; - export type ZodOperationRel< - T extends string, - Rel extends [string, ...string[]], - OP extends Operation -> = ZodObject<{ - op: ZodLiteral; - ref: ZodObject<{ - type: ZodLiteral; - id: ZodString; - relationship: ZodRelLiteral; - }>; - data: ZodGeneral; -}>; -export const zodOperationRel = < - T extends string, - R extends [string, ...string[]], - OP extends Operation ->( - type: T, - rel: R, - typeOperation: OP -): ZodOperationRel => { + E extends ObjectLiteral, + O extends Operation +> = ReturnType>; + +export const zodOperationRel = ( + type: string, + rel: TupleOfEntityRelation, + typeOperation: O +) => { const literalArray = rel.map((i) => z.literal(i)) as [ ZodLiteral, ZodLiteral, @@ -151,7 +104,7 @@ export const zodOperationRel = < .object({ type: z.literal(type), id: z.string(), - relationship: z.union(literalArray) as ZodRelLiteral, + relationship: z.union(literalArray), }) .strict(), data: zodGeneralData, @@ -171,28 +124,33 @@ export type ZodInputArray = ZodArray< }>, 'atleastone' >; -export type InputArray = z.infer; -export type ZodInputOperation = ZodObject< - { - [KEY_MAIN_INPUT_SCHEMA]: ZodInputArray; - }, - 'strict' +export type ZodInputOperation = + ReturnType>; +export type InputOperation = z.infer< + ZodInputOperation >; -export const zodInputOperation = ( - dataSource: DataSource, - mapController: MapController -): ZodInputOperation => { - const array: [ZodTypeAny, ZodTypeAny, ...ZodTypeAny[]] = [] as any; +export type InputArray = z.infer; + +export function zodInputOperation( + mapController: MapController, + entityMapProps: Map, ZodEntityProps> +) { + const array = [] as unknown as [ + ZodAdd, + ZodUpdate, + ZodRemove, + ZodOperationRel, + ZodOperationRel, + ZodOperationRel + ]; for (const [entity, controller] of mapController.entries()) { - type Entity = typeof entity; - const repository = dataSource.getRepository( - entity as EntityTarget - ); + const typeName = camelToKebab(getEntityName(entity)); + const entityMap = entityMapProps.get(entity as any); + if (!entityMap) throw new Error('Entity not found in map'); - const typeName = camelToKebab(getEntityName(repository.target)); - const { relations } = getField(repository); + const { relations } = entityMap; const hasOwnProperty = (props: string) => Object.prototype.hasOwnProperty.call(controller.prototype, props); @@ -221,5 +179,5 @@ export const zodInputOperation = ( .object({ [KEY_MAIN_INPUT_SCHEMA]: z.array(z.union(array)).nonempty(), }) - .strict() as unknown as ZodInputOperation; -}; + .strict(); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/index.ts new file mode 100644 index 00000000..06b783b6 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/index.ts @@ -0,0 +1,3 @@ +export * from './type-orm'; +export * from './micro-orm'; +export * from './atomic-operation'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/constants/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/constants/index.ts new file mode 100644 index 00000000..116b14aa --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/constants/index.ts @@ -0,0 +1,2 @@ +export const ENTITY_METADATA_TOKEN = Symbol('ENTITY_METADATA_TOKEN'); +export const DEFAULT_ARRAY_TYPE = ['ArrayType', 'EnumArrayType']; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/factory/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/factory/index.ts new file mode 100644 index 00000000..b8a13acc --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/factory/index.ts @@ -0,0 +1,128 @@ +import { FactoryProvider } from '@nestjs/common'; +import { + EntityManager, + MikroORM, + EntityRepository, + MetadataStorage, +} from '@mikro-orm/core'; +import { camelToKebab } from '../../../utils/nestjs-shared'; +import { getMikroORMToken } from '@mikro-orm/nestjs'; + +import { + CURRENT_DATA_SOURCE_TOKEN, + CURRENT_ENTITY_MANAGER_TOKEN, + CURRENT_ENTITY_REPOSITORY, + GLOBAL_MODULE_OPTIONS_TOKEN, + RUN_IN_TRANSACTION_FUNCTION, + ORM_SERVICE, + ENTITY_MAP_PROPS, +} from '../../../constants'; + +import { + EntityClass, + ObjectLiteral, + ResultMicroOrmModuleOptions, + RunInTransaction, +} from '../../../types'; +import { ZodEntityProps } from '../../mixin/types'; +import { + getProps, + getRelation, + getPropsType, + getPropsNullable, + getPrimaryColumnName, + getPrimaryColumnType, + getRelationProperty, +} from '../orm-helper'; + +import { getEntityName } from '../../mixin/helper'; +import { ENTITY_METADATA_TOKEN } from '../constants'; +import { MicroOrmService } from '../service'; + +export function CurrentMicroOrmProvider( + connectionName?: string +): FactoryProvider { + return { + provide: CURRENT_DATA_SOURCE_TOKEN, + useFactory: (mikroORM: MikroORM) => { + return mikroORM; + }, + inject: [connectionName ? getMikroORMToken(connectionName) : MikroORM], + }; +} + +export function CurrentEntityManager(): FactoryProvider { + return { + provide: CURRENT_ENTITY_MANAGER_TOKEN, + useFactory: (mikroORM: MikroORM) => mikroORM.em, + inject: [CURRENT_DATA_SOURCE_TOKEN], + }; +} + +export function CurrentEntityRepository( + entity: E +): FactoryProvider> { + return { + provide: CURRENT_ENTITY_REPOSITORY, + useFactory: (entityManager: EntityManager) => + entityManager.getRepository(entity as unknown as EntityClass), + inject: [CURRENT_ENTITY_MANAGER_TOKEN], + }; +} + +export function CurrentEntityMetadata(): FactoryProvider { + return { + provide: ENTITY_METADATA_TOKEN, + useFactory: (mikroORM: MikroORM) => mikroORM.getMetadata(), + inject: [CURRENT_DATA_SOURCE_TOKEN], + }; +} + +export function EntityPropsMap( + entities: EntityClass[] +) { + return { + provide: ENTITY_MAP_PROPS, + inject: [ENTITY_METADATA_TOKEN, GLOBAL_MODULE_OPTIONS_TOKEN], + useFactory: ( + metadataStorage: MetadataStorage, + config: ResultMicroOrmModuleOptions + ) => { + const mapProperty = new Map, ZodEntityProps>(); + const arrayConfig = config.options.arrayType; + for (const item of entities) { + const metadata = metadataStorage.get(item); + const className = getEntityName(item); + mapProperty.set(item, { + props: getProps(metadata), + propsType: getPropsType(metadata, arrayConfig), + propsNullable: getPropsNullable(metadata), + primaryColumnName: getPrimaryColumnName(metadata), + primaryColumnType: getPrimaryColumnType(metadata), + typeName: camelToKebab(className), + className: className, + relations: getRelation(metadata), + relationProperty: getRelationProperty(metadata), + }); + } + return mapProperty; + }, + }; +} + +export function RunInTransactionFactory(): FactoryProvider { + return { + provide: RUN_IN_TRANSACTION_FUNCTION, + inject: [], + useFactory() { + return async (callback) => callback(); + }, + }; +} + +export function OrmServiceFactory() { + return { + provide: ORM_SERVICE, + useClass: MicroOrmService, + }; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/index.ts new file mode 100644 index 00000000..a5d7b76f --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/index.ts @@ -0,0 +1,2 @@ +export * from './micro-orm-json-api.module'; +export * from './type'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/micro-orm-json-api.module.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/micro-orm-json-api.module.ts new file mode 100644 index 00000000..6e9e443f --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/micro-orm-json-api.module.ts @@ -0,0 +1,57 @@ +import { DynamicModule } from '@nestjs/common'; +import { MikroOrmModule } from '@mikro-orm/nestjs'; + +import { NestProvider, ResultModuleOptions, ObjectLiteral } from '../../types'; +import { GLOBAL_MODULE_OPTIONS_TOKEN } from '../../constants'; +import { + CurrentEntityManager, + CurrentEntityMetadata, + CurrentEntityRepository, + CurrentMicroOrmProvider, + OrmServiceFactory, + RunInTransactionFactory, + EntityPropsMap, +} from './factory'; +import { MicroOrmUtilService } from './service/micro-orm-util.service'; + +export class MicroOrmJsonApiModule { + static module = 'microOrm' as const; + static forRoot(options: ResultModuleOptions): DynamicModule { + const optionProvider = { + provide: GLOBAL_MODULE_OPTIONS_TOKEN, + useValue: options, + }; + + const microOrmModule = MikroOrmModule.forFeature( + options.entities, + options.connectionName + ); + + const currentProvider = [ + ...(options.providers || []), + optionProvider, + CurrentMicroOrmProvider(options.connectionName), + CurrentEntityManager(), + CurrentEntityMetadata(), + RunInTransactionFactory(), + EntityPropsMap(options.entities), + ]; + + const currentImport = [microOrmModule, ...(options.imports || [])]; + + return { + module: MicroOrmJsonApiModule, + imports: currentImport, + providers: currentProvider, + exports: [...currentProvider, ...currentImport], + }; + } + + static getUtilProviders(entity: ObjectLiteral): NestProvider { + return [ + CurrentEntityRepository(entity), + OrmServiceFactory(), + MicroOrmUtilService, + ]; + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-helper/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-helper/index.spec.ts new file mode 100644 index 00000000..fbb9fbdd --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-helper/index.spec.ts @@ -0,0 +1,177 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { MetadataStorage, MikroORM } from '@mikro-orm/core'; + +import { + Users, + Addresses, + Notes, + Roles, + dbRandomName, + mockDbPgLiteTestModule, + Comments, + UserGroups, +} from '../../../mock-utils/microrom'; +import { + CurrentMicroOrmProvider, + CurrentEntityManager, + CurrentEntityMetadata, +} from '../factory'; + +import { DEFAULT_ARRAY_TYPE, ENTITY_METADATA_TOKEN } from '../constants'; + +import { TypeField } from '../../mixin/types'; + +import { + getProps, + getPropsType, + getPropsNullable, + getPrimaryColumnName, + getPrimaryColumnType, + getRelation, + getRelationProperty, +} from './'; + +describe('microorm-orm-helper-for-map', () => { + let entityMetadataToken: MetadataStorage; + let mikroORM: MikroORM; + let dbName: string; + const config = DEFAULT_ARRAY_TYPE; + beforeAll(async () => { + dbName = dbRandomName(true); + const module: TestingModule = await Test.createTestingModule({ + imports: [mockDbPgLiteTestModule(dbName)], + providers: [ + CurrentMicroOrmProvider(), + CurrentEntityManager(), + CurrentEntityMetadata(), + ], + }).compile(); + + entityMetadataToken = module.get(ENTITY_METADATA_TOKEN); + + mikroORM = module.get(MikroORM); + }); + + afterAll(() => { + mikroORM.close(true); + }); + + it('getProps', () => { + const result = getProps(entityMetadataToken.get(Users)); + expect(result.includes('id')).toBe(true); + expect(result.includes('lastName')).toBe(true); + expect(result.includes('createdAt')).toBe(true); + expect(result.includes('updatedAt')).toBe(true); + expect(result.includes('isActive')).toBe(true); + expect(result.includes('login')).toBe(true); + expect(result.includes('firstName')).toBe(true); + expect(result.includes('testReal')).toBe(true); + expect(result.includes('testArrayNull')).toBe(true); + expect(result.includes('testDate')).toBe(true); + + expect(result.includes('userGroup' as any)).toBe(false); + expect(result.includes('notes' as any)).toBe(false); + expect(result.includes('comments' as any)).toBe(false); + expect(result.includes('roles' as any)).toBe(false); + expect(result.includes('manager' as any)).toBe(false); + expect(result.includes('addresses' as any)).toBe(false); + }); + + it('getPropsType', () => { + const result = getPropsType(entityMetadataToken.get(Users), config); + + expect(result).toEqual({ + createdAt: 'date', + firstName: 'string', + id: 'number', + isActive: 'boolean', + lastName: 'string', + login: 'string', + testArrayNull: 'array', + testDate: 'date', + testReal: 'array', + updatedAt: 'date', + }); + }); + + it('getPropsNullable', () => { + const result = getPropsNullable(entityMetadataToken.get(Users)); + expect(result).toEqual([ + 'firstName', + 'testReal', + 'testArrayNull', + 'lastName', + 'isActive', + 'testDate', + 'createdAt', + 'updatedAt', + ]); + }); + + it('getPrimaryColumnName', () => { + const result = getPrimaryColumnName(entityMetadataToken.get(Users)); + expect(result).toBe('id'); + }); + + it('getPrimaryColumnType', () => { + const result = getPrimaryColumnType(entityMetadataToken.get(Users)); + expect(result).toBe(TypeField.number); + }); + + it('getRelation', () => { + const result = getRelation(entityMetadataToken.get(Users)); + expect(result.includes('id' as any)).toBe(false); + expect(result.includes('lastName' as any)).toBe(false); + expect(result.includes('createdAt' as any)).toBe(false); + expect(result.includes('updatedAt' as any)).toBe(false); + expect(result.includes('isActive' as any)).toBe(false); + expect(result.includes('login' as any)).toBe(false); + expect(result.includes('firstName' as any)).toBe(false); + expect(result.includes('testReal' as any)).toBe(false); + expect(result.includes('testArrayNull' as any)).toBe(false); + expect(result.includes('testDate' as any)).toBe(false); + + expect(result.includes('userGroup')).toBe(true); + expect(result.includes('notes')).toBe(true); + expect(result.includes('comments')).toBe(true); + expect(result.includes('roles')).toBe(true); + expect(result.includes('manager')).toBe(true); + expect(result.includes('addresses')).toBe(true); + }); + + it('getRelationProperty', () => { + const result = getRelationProperty(entityMetadataToken.get(Users)); + expect(result).toEqual({ + addresses: { + entityClass: Addresses, + isArray: false, + nullable: true, + }, + comments: { + entityClass: Comments, + isArray: true, + nullable: false, + }, + manager: { + entityClass: Users, + isArray: false, + nullable: true, + }, + notes: { + entityClass: Notes, + isArray: true, + nullable: false, + }, + roles: { + entityClass: Roles, + isArray: true, + nullable: false, + }, + userGroup: { + entityClass: UserGroups, + isArray: false, + nullable: true, + }, + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-helper/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-helper/index.ts new file mode 100644 index 00000000..1ad4cffc --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-helper/index.ts @@ -0,0 +1,106 @@ +import { EntityKey, EntityMetadata } from '@mikro-orm/core'; +import { ObjectLiteral, ResultMicroOrmModuleOptions } from '../../../types'; +import { + FieldWithType, + FilterNullableProps, + RelationProperty, + TupleOfEntityProps, + TupleOfEntityRelation, + TypeField, + TypeForId, +} from '../../mixin/types'; + +export const getRelation = ( + entityMetadata: EntityMetadata +) => entityMetadata.relations.map((i) => i.name) as TupleOfEntityRelation; + +export const getProps = ( + entityMetadata: EntityMetadata +): TupleOfEntityProps => { + const relations = getRelation(entityMetadata); + + return entityMetadata.props + .map((i) => i.name) + .filter((i) => !relations.includes(i)) as TupleOfEntityProps; +}; + +export const getPropsType = ( + entityMetadata: EntityMetadata, + config: ResultMicroOrmModuleOptions['options']['arrayType'] +): FieldWithType => { + const field = getProps(entityMetadata); + + const result = {} as any; + for (const item of field) { + const props = + entityMetadata.properties[item as unknown as EntityKey]; + + let typeProps: TypeField = TypeField.string; + if (config.includes(props['type'])) { + result[item] = TypeField.array; + continue; + } + + switch (props.runtimeType) { + case 'Date': + typeProps = TypeField.date; + break; + case 'number': + typeProps = TypeField.number; + break; + case 'boolean': + typeProps = TypeField.boolean; + break; + case 'object': + typeProps = TypeField.object; + break; + default: + typeProps = TypeField.string; + } + result[item] = typeProps; + } + + return result; +}; + +export const getPropsNullable = ( + entityMetadata: EntityMetadata +): FilterNullableProps> => { + return getProps(entityMetadata) + .map((i) => { + // @ts-ignore + const props = entityMetadata.properties[i]; + return props.nullable || props.default !== undefined ? i : false; + }) + .filter((i) => !!i) as FilterNullableProps>; +}; + +export const getPrimaryColumnName = ( + entityMetadata: EntityMetadata +) => entityMetadata.getPrimaryProp().name.toString(); + +export const getPrimaryColumnType = ( + entityMetadata: EntityMetadata +): TypeForId => { + return entityMetadata.getPrimaryProp().runtimeType === 'number' + ? TypeField.number + : TypeField.string; +}; + +export const getRelationProperty = ( + entityMetadata: EntityMetadata +): RelationProperty => { + return entityMetadata.relations.reduce((acum, item) => { + // @ts-expect-error its dynamic creater + acum[item.name] = { + entityClass: item.entity() as any, + nullable: + item.kind === 'm:n' || item.kind === '1:m' + ? false + : (!!item.nullable as any), + isArray: item.kind === 'm:n' || item.kind === ('1:m' as any), + }; + + return acum; + }, {} as RelationProperty); +}; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-one/delete-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-one/delete-one.spec.ts new file mode 100644 index 00000000..7563183a --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-one/delete-one.spec.ts @@ -0,0 +1,61 @@ +import { EntityManager, MikroORM } from '@mikro-orm/core'; + +import { + dbRandomName, + getModuleForPgLite, + pullData, + Users, +} from '../../../../mock-utils/microrom'; +import { MicroOrmService } from '../../service'; + +import { + CURRENT_ENTITY_MANAGER_TOKEN, + ORM_SERVICE, +} from '../../../../constants'; +import { deleteOne } from './delete-one'; + +describe('delete-one', () => { + let mikroORMUsers: MikroORM; + let microOrmServiceUser: MicroOrmService; + let em: EntityManager; + let dbName: string; + beforeAll(async () => { + dbName = dbRandomName(); + const moduleUsers = await getModuleForPgLite(Users, dbName); + microOrmServiceUser = moduleUsers.get>(ORM_SERVICE); + mikroORMUsers = moduleUsers.get(MikroORM); + em = moduleUsers.get(CURRENT_ENTITY_MANAGER_TOKEN); + await pullData(em, 10); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + afterAll(() => { + mikroORMUsers.close(true); + }); + + it('Delete one item', async () => { + const checkData = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .limit(1) + .execute('get', true); + + await deleteOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, checkData.id); + + const result = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .where({ + id: checkData.id, + }) + .execute('get', true); + + expect(result).toBe(null); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-one/delete-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-one/delete-one.ts new file mode 100644 index 00000000..ccc07ab2 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-one/delete-one.ts @@ -0,0 +1,20 @@ +import { MicroOrmService } from '../../service'; +import { ObjectLiteral } from '../../../../types'; + +export async function deleteOne( + this: MicroOrmService, + id: number | string +): Promise { + const data = await this.microOrmUtilService + .queryBuilder() + .where({ + [this.microOrmUtilService.currentPrimaryColumn]: id, + }) + .getSingleResult(); + + if (!data) return void 0; + + await this.microOrmUtilService.entityManager.removeAndFlush(data); + + return void 0; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-relationship/delete-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-relationship/delete-relationship.spec.ts new file mode 100644 index 00000000..16004280 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-relationship/delete-relationship.spec.ts @@ -0,0 +1,181 @@ +import { Collection, EntityManager, MikroORM } from '@mikro-orm/core'; +import { faker } from '@faker-js/faker'; + +import { + Addresses, + dbRandomName, + getModuleForPgLite, + Notes, + pullData, + Roles, + UserGroups, + Users, + Comments, + pullAddress, +} from '../../../../mock-utils/microrom'; +import { MicroOrmService } from '../../service'; + +import { + CURRENT_ENTITY_MANAGER_TOKEN, + ORM_SERVICE, +} from '../../../../constants'; + +import { deleteRelationship } from './delete-relationship'; +import { EntityRelation } from '../../../../utils/nestjs-shared'; + +describe('delete-relationship', () => { + let mikroORMUsers: MikroORM; + let microOrmServiceUser: MicroOrmService; + let em: EntityManager; + let dbName: string; + let addressForTest: Addresses; + let addresses: Addresses; + let userGroup: UserGroups; + let notes: Collection; + let roles: Collection; + let comments: Collection; + let userObject: Users; + let newUser: Users; + beforeAll(async () => { + dbName = dbRandomName(); + const moduleUsers = await getModuleForPgLite(Users, dbName); + microOrmServiceUser = moduleUsers.get>(ORM_SERVICE); + mikroORMUsers = moduleUsers.get(MikroORM); + em = moduleUsers.get(CURRENT_ENTITY_MANAGER_TOKEN); + await pullData(em, 10); + }); + + beforeEach(async () => { + const data = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .leftJoinAndSelect('Users.addresses', 'Addresses_addresses', {}, ['id']) + .leftJoinAndSelect('Users.comments', 'Comments_comments', {}, ['id']) + .leftJoinAndSelect('Users.roles', 'Roles__roles', {}, ['id']) + .leftJoinAndSelect('Users.notes', 'Notes__notes', {}, ['id']) + .leftJoinAndSelect('Users.userGroup', 'UserGroups__userGroup', {}, ['id']) + .where({ + roles: { + $exists: true, + }, + userGroup: { + $exists: true, + }, + }) + .limit(1) + .getSingleResult(); + + if (!data) throw new Error(); + ({ roles, notes, userGroup, addresses, comments, ...userObject as any } = + data); + const firstName = faker.person.firstName(); + const lastName = faker.person.lastName(); + newUser = { + id: faker.number.int({ min: 0, max: 999999 }), + firstName: firstName, + lastName: lastName, + isActive: faker.datatype.boolean(), + login: faker.internet.userName({ + lastName: firstName, + firstName: lastName, + }), + testReal: [faker.number.float({ fractionDigits: 4 })], + testArrayNull: null, + testDate: faker.date.anytime(), + } as Users; + + addressForTest = await pullAddress(); + await em.persistAndFlush(addressForTest); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + afterAll(() => { + mikroORMUsers.close(true); + }); + + it('should be ok', async () => { + const saveCount = roles.length; + const [roles1, roles2] = roles; + await deleteRelationship.call< + MicroOrmService, + Parameters>>, + ReturnType>> + >(microOrmServiceUser, userObject.id, 'roles', [ + { type: 'roles', id: roles1.id.toString() }, + ]); + + await deleteRelationship.call< + MicroOrmService, + Parameters>>, + ReturnType>> + >(microOrmServiceUser, userObject.id, 'userGroup', { + type: 'user-groups', + id: userGroup.id.toString(), + }); + + const checkData = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .leftJoinAndSelect('Users.roles', 'Roles__roles', {}, ['id']) + .leftJoinAndSelect('Users.userGroup', 'UserGroups__userGroup', {}, ['id']) + .where({ + id: userObject.id, + }) + .getSingleResult(); + + expect(checkData?.roles.length).toBe(saveCount - 1); + expect(checkData?.roles.map((i) => i.id)).not.toContain(roles1.id); + expect(checkData?.userGroup).toBe(null); + }); + + it('should be error', async () => { + await expect( + deleteRelationship.call< + MicroOrmService, + Parameters>>, + ReturnType>> + >(microOrmServiceUser, userObject.id, 'roles', { + type: 'roles', + id: '1000', + }) + ).rejects.toThrow(); + + await expect( + deleteRelationship.call< + MicroOrmService, + Parameters>>, + ReturnType>> + >(microOrmServiceUser, userObject.id, 'roles', [ + { + type: 'roles', + id: '1000', + }, + ]) + ).rejects.toThrow(); + + await expect( + deleteRelationship.call< + MicroOrmService, + Parameters>>, + ReturnType>> + >(microOrmServiceUser, userObject.id, 'userGroup', [ + { + type: 'user-groups', + id: '10000', + }, + ]) + ).rejects.toThrow(); + await expect( + deleteRelationship.call< + MicroOrmService, + Parameters>>, + ReturnType>> + >(microOrmServiceUser, userObject.id, 'userGroup', { + type: 'user-groups', + id: '10000', + }) + ).rejects.toThrow(); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-relationship/delete-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-relationship/delete-relationship.ts new file mode 100644 index 00000000..1607e545 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-relationship/delete-relationship.ts @@ -0,0 +1,65 @@ +import { EntityRelation } from '../../../../utils/nestjs-shared'; + +import { ObjectLiteral, ValidateQueryError } from '../../../../types'; + +import { PostRelationshipData } from '../../../mixin/zod'; +import { MicroOrmService } from '../../service'; +import { NotFoundException } from '@nestjs/common'; + +export async function deleteRelationship< + E extends ObjectLiteral, + Rel extends EntityRelation +>( + this: MicroOrmService, + id: number | string, + rel: Rel, + input: PostRelationshipData +): Promise { + const idsResult = await this.microOrmUtilService.validateRelationInputData( + rel, + input + ); + + // const currentEntityRef = this.microOrmUtilService.entityManager.getReference( + // this.microOrmUtilService.entity, + // id as any + // ); + + const currentEntityRef = await this.microOrmUtilService + .queryBuilder() + .where({ + [this.microOrmUtilService.currentPrimaryColumn]: id, + }) + .getSingleResult(); + + if (!currentEntityRef) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + message: `Resource '${this.microOrmUtilService.currentAlias}' with id '${id}' does not exist`, + path: ['fields'], + }; + throw new NotFoundException([error]); + } + + await this.microOrmUtilService.entityManager.populate(currentEntityRef, [ + rel as any, + ]); + + if (Array.isArray(idsResult)) { + const relEntity = this.microOrmUtilService.getRelation(rel as any).entity(); + const relRef = idsResult.map((i) => + this.microOrmUtilService.entityManager.getReference(relEntity, i as any) + ); + currentEntityRef[rel].remove(...relRef); + } else { + if ( + currentEntityRef[rel][this.microOrmUtilService.getPrimaryNameFor(rel)] == + idsResult + ) { + // @ts-ignore + currentEntityRef[rel] = null; + } + } + + await this.microOrmUtilService.entityManager.flush(); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-all.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-all.spec.ts new file mode 100644 index 00000000..9a823b3f --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-all.spec.ts @@ -0,0 +1,957 @@ +import { EntityManager, MikroORM } from '@mikro-orm/core'; +import { faker } from '@faker-js/faker'; + +import { + Addresses, + dbRandomName, + getDefaultQuery, + getModuleForPgLite, + Notes, + pullData, + Roles, + UserGroups, + Users, +} from '../../../../mock-utils/microrom'; +import { MicroOrmService } from '../../service'; + +import { + CURRENT_ENTITY_MANAGER_TOKEN, + ORM_SERVICE, +} from '../../../../constants'; + +import { getAll } from './get-all'; + +describe('get-all', () => { + let mikroORMUsers: MikroORM; + let microOrmServiceUser: MicroOrmService; + let em: EntityManager; + let dbName: string; + beforeAll(async () => { + dbName = dbRandomName(); + const moduleUsers = await getModuleForPgLite(Users, dbName); + microOrmServiceUser = moduleUsers.get>(ORM_SERVICE); + mikroORMUsers = moduleUsers.get(MikroORM); + em = moduleUsers.get(CURRENT_ENTITY_MANAGER_TOKEN); + await pullData(em, 10); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + afterAll(() => { + mikroORMUsers.close(true); + }); + + describe('page', () => { + it('default', async () => { + const queryBuilder = + microOrmServiceUser.microOrmUtilService.queryBuilder(); + + const checkData = await queryBuilder.clone().limit(1).getResult(); + + const count = await queryBuilder.clone().count(); + const query = getDefaultQuery(); + + const { totalItems, items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + }); + + it('not default page', async () => { + const queryBuilder = + microOrmServiceUser.microOrmUtilService.queryBuilder(); + + const checkData = await queryBuilder.clone().limit(5, 5).getResult(); + const count = await queryBuilder.clone().count(); + const query = getDefaultQuery(); + query.page = { + size: 5, + number: 2, + }; + const { totalItems, items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + }); + }); + + describe('order', () => { + it('sort target', async () => { + const queryBuilder = + microOrmServiceUser.microOrmUtilService.queryBuilder(); + const checkData = await queryBuilder + .clone() + .orderBy({ + lastName: 'DESC', + }) + .limit(5, 5) + .getResult(); + const count = await queryBuilder.clone().count(); + + const query = getDefaultQuery(); + query.sort = { + target: { + lastName: 'DESC', + }, + }; + query.page = { + size: 5, + number: 2, + }; + const { totalItems, items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + }); + + it('sort relation 1:1', async () => { + const queryBuilder = microOrmServiceUser.microOrmUtilService + .queryBuilder() + .select(['id', 'login', 'firstName', 'lastName']) + .leftJoinAndSelect('Users.addresses', 'Addresses__addresses', {}, [ + 'state', + ]); + + const checkData = await queryBuilder + .clone() + .orderBy({ + addresses: { + state: 'ASC', + id: 'ASC', + }, + }) + .limit(5, 5) + .getResult(); + + const count = await queryBuilder.clone().count(); + + const query = getDefaultQuery(); + query.fields = { + target: ['login', 'firstName', 'lastName'], + addresses: ['state'], + }; + + query.sort = { + addresses: { + state: 'ASC', + id: 'ASC', + }, + }; + query.page = { + size: 5, + number: 2, + }; + + const { totalItems, items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + }); + + it('sort relation m:m', async () => { + const queryBuilder = + microOrmServiceUser.microOrmUtilService.queryBuilder(); + const checkData = await queryBuilder + .clone() + .orderBy({ + roles: { + name: 'DESC', + }, + id: 'ASC', + }) + .limit(5, 5) + .getResult(); + + const count = await queryBuilder + .clone() + .leftJoin('Users.roles', 'Roles__roles') + .distinct() + .count(); + + const query = getDefaultQuery(); + query.sort = { + roles: { + name: 'DESC', + }, + }; + query.page = { + size: 5, + number: 2, + }; + + const { totalItems, items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + }); + }); + + describe('select', () => { + it('default', async () => { + const queryBuilder = + microOrmServiceUser.microOrmUtilService.queryBuilder(); + const checkData = await queryBuilder.clone().limit(5).getResult(); + const count = await queryBuilder.clone().count(); + const query = getDefaultQuery(); + query.page = { + size: 5, + number: 1, + }; + const { totalItems, items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + }); + + it('target select field', async () => { + const select: ['login', 'firstName', 'lastName'] = [ + 'login', + 'firstName', + 'lastName', + ]; + const queryBuilder = + microOrmServiceUser.microOrmUtilService.queryBuilder(); + const checkData = await queryBuilder + .clone() + .select(['id', ...select]) + .limit(5) + .getResult(); + const count = await queryBuilder.clone().count(); + const query = getDefaultQuery(); + query.page = { + size: 5, + number: 1, + }; + query.fields = { + target: select, + }; + const { totalItems, items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + }); + + it('relation select field', async () => { + const select: ['login', 'firstName', 'lastName'] = [ + 'login', + 'firstName', + 'lastName', + ]; + const queryBuilder = microOrmServiceUser.microOrmUtilService + .queryBuilder('Users') + .select(['id', ...select]) + .leftJoinAndSelect('Users.addresses', 'Addresses_addresses', {}, [ + 'city', + 'state', + ]) + .leftJoinAndSelect('Users.roles', 'Roles__roles', {}, ['name', 'key']); + const checkData = await queryBuilder.clone().limit(5).getResult(); + const count = await queryBuilder.clone().count().distinct(); + const query = getDefaultQuery(); + query.page = { + size: 5, + number: 1, + }; + query.fields = { + target: select, + addresses: ['city', 'state'], + roles: ['name', 'key'], + }; + const { totalItems, items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + }); + }); + + describe('include', () => { + it('default', async () => { + const queryBuilder = microOrmServiceUser.microOrmUtilService + .queryBuilder('Users') + .leftJoinAndSelect('Users.roles', 'Roles__roles'); + + const checkData = await queryBuilder.clone().limit(5).getResult(); + const count = await queryBuilder.clone().count().distinct(); + + const query = getDefaultQuery(); + query.page = { + size: 5, + number: 1, + }; + query.include = ['roles']; + + const { totalItems, items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + }); + + it('include with select', async () => { + const queryBuilder = microOrmServiceUser.microOrmUtilService + .queryBuilder('Users') + .leftJoinAndSelect('Users.roles', 'Roles__roles') + .leftJoinAndSelect('Users.addresses', 'Addresses__addresses', {}, [ + 'city', + 'state', + ]); + + const checkData = await queryBuilder.clone().limit(5).getResult(); + const count = await queryBuilder.clone().count().distinct(); + + const query = getDefaultQuery(); + query.page = { + size: 5, + number: 1, + }; + query.include = ['roles']; + query.fields = { + addresses: ['city', 'state'], + }; + const { totalItems, items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + }); + }); + + describe('filter', () => { + let rolesData: Roles[]; + let addresses: Addresses[]; + let userGroups: UserGroups[]; + let users: Users[]; + let notes: Notes[]; + beforeAll(async () => { + rolesData = await microOrmServiceUser.microOrmUtilService + .queryBuilder(Roles) + .getResult(); + addresses = await microOrmServiceUser.microOrmUtilService + .queryBuilder(Addresses) + .getResult(); + + userGroups = await microOrmServiceUser.microOrmUtilService + .queryBuilder(UserGroups) + .getResult(); + users = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .getResult(); + notes = await microOrmServiceUser.microOrmUtilService + .queryBuilder(Notes) + .getResult(); + }); + + describe('target', () => { + it('simple filter on target', async () => { + const randUsers = faker.helpers.arrayElements(users); + const queryBuilder = microOrmServiceUser.microOrmUtilService + .queryBuilder('Users') + .where({ + login: { + $in: randUsers.map((i) => i.login), + }, + }); + + const checkData = await queryBuilder + .clone() + .limit(5) + .orderBy({ + login: 'DESC', + }) + .getResult(); + const count = await queryBuilder.clone().count(); + + const query = getDefaultQuery(); + query.page = { + size: 5, + number: 1, + }; + query.filter = { + target: { + login: { + in: randUsers.map((i) => i.login) as [string, ...string[]], + }, + }, + }; + query.sort = { + target: { + login: 'DESC', + }, + }; + const { totalItems, items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + }); + + it('Target relation is null 1:1', async () => { + const queryBuilder = microOrmServiceUser.microOrmUtilService + .queryBuilder('Users') + .where({ + manager: { + $exists: false, + }, + }); + + const checkData = await queryBuilder + .clone() + .limit(5) + .orderBy({ + manager: { + login: 'DESC', + }, + }) + .getResult(); + const count = await queryBuilder.clone().count(); + + const queryBuilder2 = microOrmServiceUser.microOrmUtilService + .queryBuilder('Users') + .where({ + addresses: { + $exists: true, + }, + }); + + const checkData2 = await queryBuilder2 + .clone() + .limit(5) + .orderBy({ + addresses: { + city: 'DESC', + }, + }) + .getResult(); + const count2 = await queryBuilder2.clone().count(); + + const query = getDefaultQuery(); + query.page = { + size: 5, + number: 1, + }; + query.filter = { + target: { + manager: { + eq: 'null', + }, + }, + }; + query.sort = { + manager: { + login: 'DESC', + }, + }; + + const query2 = getDefaultQuery(); + query2.page = { + size: 5, + number: 1, + }; + query2.filter = { + target: { + addresses: { + ne: 'null', + }, + }, + }; + query2.sort = { + addresses: { + city: 'DESC', + }, + }; + const { totalItems, items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + + const { totalItems: totalItems2, items: items2 } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query2); + + expect(totalItems2).toBe(count2); + expect(JSON.stringify(items2)).toBe(JSON.stringify(checkData2)); + }); + + it('Target relation is null m:1 & 1:m', async () => { + const queryBuilder = microOrmServiceUser.microOrmUtilService + .queryBuilder('Users') + .where({ + userGroup: { + $exists: false, + }, + }); + + const checkData = await queryBuilder + .clone() + .limit(5) + .orderBy({ + login: 'DESC', + }) + .getResult(); + const count = await queryBuilder.clone().count(); + + const query = getDefaultQuery(); + query.page = { + size: 5, + number: 1, + }; + query.filter = { + target: { + userGroup: { + eq: 'null', + }, + }, + }; + query.sort = { + target: { + login: 'DESC', + }, + }; + + const { totalItems, items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + + const checkData2 = await queryBuilder + .clone() + .limit(5) + .orderBy({ + userGroup: { + label: 'DESC', + }, + }) + .getResult(); + + const count2 = await queryBuilder.clone().count(); + + const query2 = getDefaultQuery(); + query2.page = { + size: 5, + number: 1, + }; + query2.filter = { + target: { + userGroup: { + eq: 'null', + }, + }, + }; + query2.sort = { + userGroup: { + label: 'DESC', + }, + }; + + const { totalItems: totalItems2, items: items2 } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query2); + + expect(totalItems2).toBe(count2); + expect(JSON.stringify(items2)).toBe(JSON.stringify(checkData2)); + + const queryBuilder2 = microOrmServiceUser.microOrmUtilService + .queryBuilder('Users') + .where({ + notes: { + $exists: true, + }, + }); + const checkData3 = await queryBuilder2 + .clone() + .limit(5) + .orderBy({ + notes: { + id: 'DESC', + }, + }) + .getResult(); + + const count3 = await queryBuilder2.clone().count().distinct(); + + const query3 = getDefaultQuery(); + query3.page = { + size: 5, + number: 1, + }; + query3.filter = { + target: { + notes: { + ne: 'null', + }, + }, + }; + query3.sort = { + notes: { + id: 'DESC', + }, + }; + + const { totalItems: totalItems3, items: items3 } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query3); + expect(totalItems3).toBe(count3); + expect(JSON.stringify(items3)).toBe(JSON.stringify(checkData3)); + + const checkData4 = await queryBuilder2 + .clone() + .limit(5) + .orderBy({ id: 'DESC' }) + .getResult(); + const count4 = await queryBuilder2.clone().count().distinct(); + + const query4 = getDefaultQuery(); + query4.page = { + size: 5, + number: 1, + }; + query4.filter = { + target: { + notes: { + ne: 'null', + }, + }, + }; + query4.sort = { + target: { + id: 'DESC', + }, + }; + + const { totalItems: totalItems4, items: items4 } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query4); + expect(totalItems4).toBe(count4); + expect(JSON.stringify(items4)).toBe(JSON.stringify(checkData4)); + }); + + it('Target relation is null m:m', async () => { + const queryBuilder = microOrmServiceUser.microOrmUtilService + .queryBuilder('Users') + .where({ + roles: { + $exists: false, + }, + }); + const checkData = await queryBuilder + .clone() + .limit(5) + .orderBy({ + manager: { + login: 'DESC', + }, + }) + .getResult(); + + const count = await queryBuilder.clone().count(); + + const query = getDefaultQuery(); + query.page = { + size: 5, + number: 1, + }; + query.filter = { + target: { + roles: { + eq: 'null', + }, + }, + }; + query.sort = { + manager: { + login: 'DESC', + }, + }; + const { totalItems: totalItems, items: items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + + const queryBuilder1 = microOrmServiceUser.microOrmUtilService + .queryBuilder('Users') + .where({ + roles: { + $exists: true, + }, + }); + + const checkData1 = await queryBuilder1 + .clone() + .limit(5) + .orderBy({ + roles: { + key: 'DESC', + }, + }) + .getResult(); + + const count1 = await queryBuilder1.clone().count().distinct(); + + const query1 = getDefaultQuery(); + query1.page = { + size: 5, + number: 1, + }; + query1.filter = { + target: { + roles: { + ne: 'null', + }, + }, + }; + query1.sort = { + roles: { + key: 'DESC', + }, + }; + const { totalItems: totalItems1, items: items1 } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query1); + + expect(totalItems1).toBe(count1); + expect(JSON.stringify(items1)).toBe(JSON.stringify(checkData1)); + }); + }); + + describe('relation', () => { + it('relation 1:1', async () => { + const randAddresses = faker.helpers.arrayElements(addresses); + + const queryBuilder = microOrmServiceUser.microOrmUtilService + .queryBuilder('Users') + .where({ + addresses: { + id: { + $in: randAddresses.map((i) => i.id), + }, + }, + }) + .joinAndSelect('Users.addresses', 'Addresses'); + + const checkData = await queryBuilder + .clone() + .limit(5) + .orderBy({ + addresses: { + city: 'DESC', + }, + }) + .getResult(); + + const count = await queryBuilder.clone().count(); + + const query = getDefaultQuery(); + query.page = { + size: 5, + number: 1, + }; + query.filter = { + relation: { + addresses: { + id: { + in: randAddresses.map((i) => `${i.id}`) as [ + string, + ...string[] + ], + }, + }, + }, + }; + query.sort = { + addresses: { + city: 'DESC', + }, + }; + query.include = ['addresses']; + const { totalItems: totalItems, items: items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + }); + + it('relation 1:m', async () => { + const randNotes = faker.helpers.arrayElements(notes, 3); + + const quweryBuilder = microOrmServiceUser.microOrmUtilService + .queryBuilder('Users') + .leftJoinAndSelect('Users.notes', 'Notes', { + id: { $in: randNotes.map((i) => i.id) }, + }) + .where({ + notes: { + id: { + $in: randNotes.map((i) => i.id), + }, + }, + }); + const checkData = await quweryBuilder + .clone() + .limit(5) + .orderBy({ + addresses: { + city: 'DESC', + }, + }) + .getResult(); + const count = await quweryBuilder.clone().count(); + const query = getDefaultQuery(); + query.page = { + size: 5, + number: 1, + }; + query.filter = { + relation: { + notes: { + id: { + in: randNotes.map((i) => `${i.id}`) as [string, ...string[]], + }, + }, + }, + }; + query.sort = { + addresses: { + city: 'DESC', + }, + }; + query.include = ['notes']; + const { totalItems: totalItems, items: items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + }); + + it('relation m:m', async () => { + const randRoles = faker.helpers.arrayElements(rolesData, 3); + + const queryBuilder = microOrmServiceUser.microOrmUtilService + .queryBuilder('Users') + .leftJoinAndSelect('Users.roles', 'Roles__roles', { + key: randRoles[0].key, + }) + .where({ + roles: { + key: { + $eq: randRoles[0].key, + }, + }, + }); + const checkData = await queryBuilder + .clone() + .limit(5) + .orderBy({ + addresses: { + city: 'DESC', + }, + }) + .getResult(); + + const count = await queryBuilder.clone().count(); + + const query = getDefaultQuery(); + query.page = { + size: 5, + number: 1, + }; + query.filter = { + relation: { + roles: { + key: { + eq: `${randRoles[0].key}`, + }, + }, + }, + }; + query.sort = { + addresses: { + city: 'DESC', + }, + }; + + query.include = ['roles']; + + const { totalItems: totalItems, items: items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + }); + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-all.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-all.ts new file mode 100644 index 00000000..e33a71e5 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-all.ts @@ -0,0 +1,91 @@ +import { QueryFlag, serialize, wrap } from '@mikro-orm/core'; + +import { ObjectLiteral } from '../../../../types'; +import { MicroOrmService } from '../../service'; +import { Query } from '../../../mixin/zod'; +import { getQueryForCount, getSortObject } from './get-query-for-count'; + +export async function getAll( + this: MicroOrmService, + query: Query +): Promise<{ + totalItems: number; + items: E[]; +}> { + const { page } = query; + const countSubQuery = getQueryForCount.call< + MicroOrmService, + Parameters>, + ReturnType> + >(this, ...[query]); + + const skip = (page.number - 1) * page.size; + const paginationQuery = countSubQuery + .clone() + .select(this.microOrmUtilService.currentPrimaryColumn) + .limit(page.size, skip); + + const collectIdsAlias = 'CollectIds'; + + const queryIdsQuery = this.microOrmUtilService + .queryBuilder(collectIdsAlias) + .select(this.microOrmUtilService.currentPrimaryColumn) + .join(paginationQuery, this.microOrmUtilService.currentAlias, { + [`${collectIdsAlias}.${this.microOrmUtilService.currentPrimaryColumn}`]: + this.microOrmUtilService + .getKnex() + .ref( + `${this.microOrmUtilService.currentAlias}.${this.microOrmUtilService.currentPrimaryColumn}` + ), + }); + + const queryCount = this.microOrmUtilService + .queryBuilder() + .from( + countSubQuery + .clone() + .select(this.microOrmUtilService.currentPrimaryColumn) + ) + .count(this.microOrmUtilService.currentPrimaryColumn, true); + + const resCount = await queryCount.execute('get'); + const count = resCount ? +resCount.count : 0; + + if (count === 0) { + return { + totalItems: count, + items: [], + }; + } + + const resIds = await queryIdsQuery + .distinct() + .setFlag(QueryFlag.DISABLE_PAGINATE) + .execute('all'); + + const idsArray = resIds.map( + (r) => r[this.microOrmUtilService.currentPrimaryColumn] + ); + const resultQueryBuilder = this.microOrmUtilService.queryBuilder().where({ + [this.microOrmUtilService.currentPrimaryColumn]: { + $in: idsArray, + }, + }); + + const sortObject = getSortObject(query); + const resultList = await this.microOrmUtilService + .prePareQueryBuilder(resultQueryBuilder, query) + .orderBy( + Object.keys(sortObject).length > 0 + ? sortObject + : { + [this.microOrmUtilService.currentPrimaryColumn]: 'ASC', + } + ) + .getResult(); + + return { + totalItems: count, + items: resultList.map((i) => wrap(i).toJSON() as E), + }; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-query-for-count.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-query-for-count.spec.ts new file mode 100644 index 00000000..3364e14e --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-query-for-count.spec.ts @@ -0,0 +1,242 @@ +import { EntityManager, MikroORM } from '@mikro-orm/core'; +import { FilterOperand } from '../../../../utils/nestjs-shared'; +import { + UserGroups, + Users, + pullAllData, + getDefaultQuery, + dbRandomName, + getModuleForPgLite, +} from '../../../../mock-utils/microrom'; + +import { + CURRENT_ENTITY_MANAGER_TOKEN, + ORM_SERVICE, +} from '../../../../constants'; + +import { getQueryForCount } from './get-query-for-count'; +import { MicroOrmService } from '../../service'; + +describe('get-query-for-count', () => { + let mikroORMUserGroup: MikroORM; + let mikroORMUsers: MikroORM; + let microOrmServiceUser: MicroOrmService; + let em: EntityManager; + let dbName: string; + beforeAll(async () => { + dbName = dbRandomName(true); + const moduleUserGroup = await getModuleForPgLite(UserGroups, dbName); + + mikroORMUserGroup = moduleUserGroup.get(MikroORM); + + const moduleUsers = await getModuleForPgLite(Users, dbName); + microOrmServiceUser = moduleUsers.get>(ORM_SERVICE); + mikroORMUsers = moduleUsers.get(MikroORM); + em = moduleUserGroup.get(CURRENT_ENTITY_MANAGER_TOKEN); + await pullAllData(em); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + afterAll(() => { + mikroORMUserGroup.close(true); + mikroORMUsers.close(true); + }); + + it('has only sort data', () => { + const query = getDefaultQuery(); + query.sort = { + target: { + id: 'ASC', + lastName: 'DESC', + }, + userGroup: { + id: 'ASC', + }, + roles: { + name: 'DESC', + }, + }; + + const result = getQueryForCount.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, ...[query]); + + expect(result.getFormattedQuery()).toBe( + `select "Users".* from "public"."users" as "Users" left join "public"."users_have_roles" as "u2" on "Users"."id" = "u2"."user_id" left join "public"."roles" as "u1" on "u2"."role_id" = "u1"."id" order by "Users"."id" asc, "Users"."last_name" desc, "Users"."user_groups_id" asc, "u1"."name" desc` + ); + }); + + it('has only filter data', () => { + const query = getDefaultQuery(); + query.filter = { + target: { + login: { + [FilterOperand.eq]: 'test', + [FilterOperand.ne]: 'test2', + }, + isActive: { + [FilterOperand.eq]: 'false', + }, + testReal: { + [FilterOperand.some]: ['test'], + }, + }, + }; + + const query1 = getDefaultQuery(); + query1.filter = { + target: { + login: { + [FilterOperand.eq]: 'test', + [FilterOperand.ne]: 'test2', + }, + isActive: { + [FilterOperand.eq]: 'false', + }, + }, + relation: { + comments: { + kind: { + [FilterOperand.eq]: 'COMMENT', + }, + }, + userGroup: { + label: { + [FilterOperand.eq]: 'test', + }, + }, + }, + }; + + const query2 = getDefaultQuery(); + query2.filter = { + target: { + login: { + [FilterOperand.eq]: 'test', + [FilterOperand.ne]: 'test2', + }, + isActive: { + [FilterOperand.eq]: 'false', + }, + }, + relation: { + manager: { + login: { + [FilterOperand.eq]: 'test', + }, + }, + addresses: { + city: { + [FilterOperand.eq]: 'test', + }, + }, + }, + }; + + const query3 = getDefaultQuery(); + query3.filter = { + target: { + login: { + [FilterOperand.eq]: 'test', + [FilterOperand.ne]: 'test2', + }, + isActive: { + [FilterOperand.eq]: 'false', + }, + }, + relation: { + roles: { + key: { + [FilterOperand.eq]: 'test', + [FilterOperand.ne]: 'test2', + }, + isDefault: { + [FilterOperand.eq]: 'false', + }, + }, + }, + }; + + const result = getQueryForCount.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, ...[query]); + + expect(result.getFormattedQuery()).toBe( + `select "Users".* from "public"."users" as "Users" where "Users"."login" = 'test' and "Users"."login" != 'test2' and "Users"."is_active" = 'false' and "Users"."test_real" && '{test}' order by "Users"."id" asc` + ); + + const result1 = getQueryForCount.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, ...[query1]); + + expect(result1.getFormattedQuery()).toBe( + `select "Users".* from "public"."users" as "Users" where "Users"."login" = 'test' and "Users"."login" != 'test2' and "Users"."is_active" = 'false' and (exists (select 1 from "public"."comments" as "Comments" where "Comments"."created_by" = "Users"."id" and "Comments"."kind" = 'COMMENT')) and (exists (select 1 from "public"."user_groups" as "UserGroups" where "UserGroups"."id" = "Users"."user_groups_id" and "UserGroups"."label" = 'test')) order by "Users"."id" asc` + ); + + const result2 = getQueryForCount.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, ...[query2]); + expect(result2.getFormattedQuery()).toBe( + `select "Users".* from "public"."users" as "Users" left join "public"."users" as "u1" on "Users"."manager_id" = "u1"."id" left join "public"."addresses" as "a2" on "Users"."addresses_id" = "a2"."id" where "Users"."login" = 'test' and "Users"."login" != 'test2' and "Users"."is_active" = 'false' and "u1"."login" = 'test' and "a2"."city" = 'test' order by "Users"."id" asc` + ); + + const result3 = getQueryForCount.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, ...[query3]); + expect(result3.getFormattedQuery()).toBe( + `select "Users".* from "public"."users" as "Users" where "Users"."login" = 'test' and "Users"."login" != 'test2' and "Users"."is_active" = 'false' and (exists (select 1 from "public"."users_have_roles" as "users_have_roles" left join "public"."roles" as "r1" on "users_have_roles"."role_id" = "r1"."id" where "users_have_roles"."user_id" = "Users"."id" and "r1"."key" = 'test' and "r1"."key" != 'test2' and "r1"."is_default" = 'false')) order by "Users"."id" asc` + ); + }); + + it('has only filter data with sort', () => { + const query1 = getDefaultQuery(); + query1.filter = { + relation: { + roles: { + key: { + [FilterOperand.eq]: 'test', + [FilterOperand.ne]: 'test2', + }, + isDefault: { + [FilterOperand.eq]: 'false', + }, + }, + }, + }; + query1.sort = { + target: { + id: 'ASC', + lastName: 'DESC', + }, + userGroup: { + id: 'ASC', + }, + roles: { + name: 'DESC', + }, + }; + + const result1 = getQueryForCount.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, ...[query1]); + expect(result1.getFormattedQuery()).toBe( + `select "Users".* from "public"."users" as "Users" left join "public"."users_have_roles" as "u2" on "Users"."id" = "u2"."user_id" left join "public"."roles" as "u1" on "u2"."role_id" = "u1"."id" where "u1"."key" = 'test' and "u1"."key" != 'test2' and "u1"."is_default" = 'false' order by "Users"."id" asc, "Users"."last_name" desc, "Users"."user_groups_id" asc, "u1"."name" desc` + ); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-query-for-count.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-query-for-count.ts new file mode 100644 index 00000000..ccc2dda6 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-query-for-count.ts @@ -0,0 +1,60 @@ +import { ObjectTyped } from '../../../../utils/nestjs-shared'; + +import { MicroOrmService } from '../../service'; +import { Query } from '../../../mixin/zod'; +import { ObjectLiteral } from '../../../../types'; +import type { QBQueryOrderMap, EntityKey } from '@mikro-orm/core'; + +export function getSortObject( + query: Query +): QBQueryOrderMap { + const { sort } = query; + const sortObject: QBQueryOrderMap = {}; + if (!sort) return sortObject; + + const { target = {}, ...relation } = sort; + for (const [filed, sortType] of ObjectTyped.entries(target)) { + sortObject[filed] = sortType; + } + + for (const [relationName, orderConfig = {}] of ObjectTyped.entries( + relation + )) { + const name = relationName as unknown as EntityKey; + sortObject[name] = {}; + for (const [field, sortType] of ObjectTyped.entries(orderConfig)) { + sortObject[name][field] = sortType; + } + } + return sortObject; +} + +export function getQueryForCount( + this: MicroOrmService, + query: Query +) { + const querySelect = this.microOrmUtilService.queryBuilder(); + const sortObject = getSortObject(query); + querySelect.orderBy( + Object.keys(sortObject).length > 0 + ? sortObject + : { + [this.microOrmUtilService.currentPrimaryColumn]: 'ASC', + } + ); + + const expressionArrayForTarget = + this.microOrmUtilService.getFilterExpressionForTarget(query); + const expressionArrayForRelation = + this.microOrmUtilService.getFilterExpressionForRelation(query); + + const resultExpression = [ + ...expressionArrayForTarget, + ...expressionArrayForRelation, + ]; + for (const expression of resultExpression) { + querySelect.andWhere(expression); + } + + return querySelect; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-one/get-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-one/get-one.spec.ts new file mode 100644 index 00000000..e6fcaf70 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-one/get-one.spec.ts @@ -0,0 +1,102 @@ +import { EntityManager, MikroORM } from '@mikro-orm/core'; + +import { + dbRandomName, + getDefaultQuery, + getModuleForPgLite, + pullData, + Users, +} from '../../../../mock-utils/microrom'; +import { MicroOrmService } from '../../service'; + +import { + CURRENT_ENTITY_MANAGER_TOKEN, + ORM_SERVICE, +} from '../../../../constants'; + +import { getOne } from './get-one'; +import { NotFoundException } from '@nestjs/common'; + +describe('get-one', () => { + let mikroORMUsers: MikroORM; + let microOrmServiceUser: MicroOrmService; + let em: EntityManager; + let dbName: string; + beforeAll(async () => { + dbName = dbRandomName(); + const moduleUsers = await getModuleForPgLite(Users, dbName); + microOrmServiceUser = moduleUsers.get>(ORM_SERVICE); + mikroORMUsers = moduleUsers.get(MikroORM); + em = moduleUsers.get(CURRENT_ENTITY_MANAGER_TOKEN); + await pullData(em, 10); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + afterAll(() => { + mikroORMUsers.close(true); + }); + + it('Get one item', async () => { + const checkData = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .limit(1) + .getSingleResult(); + const query = getDefaultQuery(); + if (!checkData) throw new Error('Result is null'); + const result = await getOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, checkData.id, query); + + expect(JSON.stringify(result)).toBe(JSON.stringify(checkData)); + }); + + it('Get one item with select', async () => { + const checkData = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .select(['id', 'firstName', 'isActive']) + .leftJoinAndSelect('Users.addresses', 'Addresses__addresses') + .leftJoinAndSelect('Users.comments', 'Comments__comments', {}, ['text']) + .leftJoinAndSelect('Users.manager', 'Users__manager', {}, ['login']) + .where({ + comments: { + $exists: true, + }, + }) + .limit(1) + .getSingleResult(); + if (!checkData) throw new Error('Result is null'); + const query = getDefaultQuery(); + query.include = ['addresses', 'comments', 'manager']; + query.fields = { + target: ['firstName', 'isActive'], + comments: ['text'], + manager: ['login'], + }; + const result = await getOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, checkData.id, query); + + expect(JSON.stringify(result)).toBe(JSON.stringify(checkData)); + }); + it('Should be error', async () => { + expect.assertions(1); + const query = getDefaultQuery(); + try { + await getOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, 1000, query); + } catch (e) { + expect(e).toBeInstanceOf(NotFoundException); + } + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-one/get-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-one/get-one.ts new file mode 100644 index 00000000..3e01eecb --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-one/get-one.ts @@ -0,0 +1,30 @@ +import { NotFoundException } from '@nestjs/common'; +import { ObjectLiteral, ValidateQueryError } from '../../../../types'; +import { QueryOne } from '../../../mixin/zod'; +import { MicroOrmService } from '../../service'; +import { serialize, wrap } from '@mikro-orm/core'; + +export async function getOne( + this: MicroOrmService, + id: number | string, + query: QueryOne +): Promise { + const queryBuilder = this.microOrmUtilService.queryBuilder().where({ + [this.microOrmUtilService.currentPrimaryColumn]: id, + }); + + const resultItem = await this.microOrmUtilService + .prePareQueryBuilder(queryBuilder, query) + .getSingleResult(); + + if (!resultItem) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + message: `Resource '${this.microOrmUtilService.currentAlias}' with id '${id}' does not exist`, + path: ['fields'], + }; + throw new NotFoundException([error]); + } + + return wrap(resultItem).toJSON() as E; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-relationship/get-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-relationship/get-relationship.spec.ts new file mode 100644 index 00000000..4454590c --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-relationship/get-relationship.spec.ts @@ -0,0 +1,144 @@ +import { + Collection, + EntityManager, + MikroORM, + NotFoundError, +} from '@mikro-orm/core'; +import { faker } from '@faker-js/faker'; + +import { + Addresses, + dbRandomName, + getModuleForPgLite, + Notes, + pullData, + Roles, + UserGroups, + Users, + Comments, + pullAddress, +} from '../../../../mock-utils/microrom'; +import { MicroOrmService } from '../../service'; + +import { + CURRENT_ENTITY_MANAGER_TOKEN, + ORM_SERVICE, +} from '../../../../constants'; + +import { getRelationship } from './get-relationship'; +import { NotFoundException } from '@nestjs/common'; +import { EntityRelation } from '../../../../utils/nestjs-shared'; + +describe('get-relationship', () => { + let mikroORMUsers: MikroORM; + let microOrmServiceUser: MicroOrmService; + let em: EntityManager; + let dbName: string; + let addressForTest: Addresses; + let addresses: Addresses; + let userGroup: UserGroups; + let notes: Collection; + let roles: Collection; + let comments: Collection; + let userObject: Users; + let newUser: Users; + beforeAll(async () => { + dbName = dbRandomName(); + const moduleUsers = await getModuleForPgLite(Users, dbName); + microOrmServiceUser = moduleUsers.get>(ORM_SERVICE); + mikroORMUsers = moduleUsers.get(MikroORM); + em = moduleUsers.get(CURRENT_ENTITY_MANAGER_TOKEN); + await pullData(em, 10); + }); + + beforeEach(async () => { + const data = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .leftJoinAndSelect('Users.addresses', 'Addresses_addresses', {}, ['id']) + .leftJoinAndSelect('Users.comments', 'Comments_comments', {}, ['id']) + .leftJoinAndSelect('Users.roles', 'Roles__roles', {}, ['id']) + .leftJoinAndSelect('Users.notes', 'Notes__notes', {}, ['id']) + .leftJoinAndSelect('Users.userGroup', 'UserGroups__userGroup', {}, ['id']) + .where({ + addresses: { + $exists: true, + }, + roles: { + $exists: true, + }, + userGroup: { + $exists: true, + }, + }) + .getSingleResult(); + + if (!data) throw new Error(); + + ({ roles, notes, userGroup, addresses, comments, ...userObject as any } = + data); + const firstName = faker.person.firstName(); + const lastName = faker.person.lastName(); + newUser = { + id: faker.number.int({ min: 0, max: 999999 }), + firstName: firstName, + lastName: lastName, + isActive: faker.datatype.boolean(), + login: faker.internet.userName({ + lastName: firstName, + firstName: lastName, + }), + testReal: [faker.number.float({ fractionDigits: 4 })], + testArrayNull: null, + testDate: faker.date.anytime(), + } as Users; + + addressForTest = await pullAddress(); + await em.persistAndFlush(addressForTest); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + afterAll(() => { + mikroORMUsers.close(true); + }); + + it('should be ger result', async () => { + const { addresses: addressesResult } = await getRelationship.call< + MicroOrmService, + Parameters>>, + ReturnType>> + >(microOrmServiceUser, userObject.id, 'addresses'); + + expect(addressesResult.id).toBe(addresses.id); + + const { userGroup: userGroupResult } = await getRelationship.call< + MicroOrmService, + Parameters>>, + ReturnType>> + >(microOrmServiceUser, userObject.id, 'userGroup'); + + expect(userGroupResult.id).toBe(userGroup.id); + + const { roles: rolesResult } = await getRelationship.call< + MicroOrmService, + Parameters>>, + ReturnType>> + >(microOrmServiceUser, userObject.id, 'roles'); + for (const i of roles.map((i) => i.id)) { + expect(rolesResult.map((i) => i.id)).toContain(i); + } + }); + + it('should be error', async () => { + await expect( + getRelationship.call< + MicroOrmService, + Parameters>>, + ReturnType>> + >(microOrmServiceUser, '20000', 'roles') + ).rejects.toThrow(NotFoundException); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-relationship/get-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-relationship/get-relationship.ts new file mode 100644 index 00000000..09dbd99e --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-relationship/get-relationship.ts @@ -0,0 +1,35 @@ +import { EntityRelation } from '../../../../utils/nestjs-shared'; +import { NotFoundException } from '@nestjs/common'; + +import { MicroOrmService } from '../../service'; +import { ObjectLiteral, ValidateQueryError } from '../../../../types'; +import { serialize } from '@mikro-orm/core'; + +export async function getRelationship< + E extends ObjectLiteral, + Rel extends EntityRelation +>(this: MicroOrmService, id: number | string, rel: Rel): Promise { + const result = await this.microOrmUtilService + .queryBuilder() + .leftJoinAndSelect( + `${this.microOrmUtilService.currentAlias}.${rel.toString()}`, + rel.toString(), + {}, + [this.microOrmUtilService.getPrimaryNameFor(rel)] + ) + .where({ + [this.microOrmUtilService.currentPrimaryColumn]: id, + }) + .getSingleResult(); + + if (!result) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + message: `Resource '${this.microOrmUtilService.currentAlias}' with id '${id}' does not exist`, + path: ['fields'], + }; + throw new NotFoundException([error]); + } + + return serialize(result, { forceObject: true }) as unknown as E; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/index.ts new file mode 100644 index 00000000..9c1365d1 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/index.ts @@ -0,0 +1,9 @@ +export * from './get-all/get-all'; +export * from './get-one/get-one'; +export * from './delete-one/delete-one'; +export * from './post-one/post-one'; +export * from './patch-one/patch-one'; +export * from './get-relationship/get-relationship'; +export * from './delete-relationship/delete-relationship'; +export * from './post-relationship/post-relationship'; +export * from './patch-relationship/patch-relationship'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-one/patch-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-one/patch-one.spec.ts new file mode 100644 index 00000000..222ea525 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-one/patch-one.spec.ts @@ -0,0 +1,234 @@ +import { Collection, EntityManager, MikroORM } from '@mikro-orm/core'; +import { faker } from '@faker-js/faker'; + +import { + Addresses, + dbRandomName, + getModuleForPgLite, + Notes, + pullData, + Roles, + UserGroups, + Users, + Comments, + pullAddress, +} from '../../../../mock-utils/microrom'; +import { MicroOrmService } from '../../service'; + +import { + CURRENT_ENTITY_MANAGER_TOKEN, + ORM_SERVICE, +} from '../../../../constants'; + +import { patchOne } from './patch-one'; + +import { + BadRequestException, + NotFoundException, + UnprocessableEntityException, +} from '@nestjs/common'; + +describe('patch-one', () => { + let mikroORMUsers: MikroORM; + let microOrmServiceUser: MicroOrmService; + let em: EntityManager; + let dbName: string; + let addresses: Addresses; + let addressForTest: Addresses; + let userGroup: UserGroups; + let notes: Collection; + let roles: Collection; + let comments: Collection; + let userObject: Users; + let newUser: Users; + beforeAll(async () => { + dbName = dbRandomName(); + const moduleUsers = await getModuleForPgLite(Users, dbName); + microOrmServiceUser = moduleUsers.get>(ORM_SERVICE); + mikroORMUsers = moduleUsers.get(MikroORM); + em = moduleUsers.get(CURRENT_ENTITY_MANAGER_TOKEN); + await pullData(em, 10); + }); + + beforeEach(async () => { + const data = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .leftJoinAndSelect('Users.addresses', 'Addresses_addresses', {}, ['id']) + .leftJoinAndSelect('Users.comments', 'Comments_comments', {}, ['id']) + .leftJoinAndSelect('Users.roles', 'Roles__roles', {}, ['id']) + .leftJoinAndSelect('Users.notes', 'Notes__notes', {}, ['id']) + .leftJoinAndSelect('Users.userGroup', 'UserGroups__userGroup', {}, ['id']) + .where({ + addresses: { + $exists: true, + }, + roles: { + $exists: true, + }, + notes: { + $exists: true, + }, + userGroup: { + $exists: true, + }, + comments: { + $exists: true, + }, + }) + .limit(1) + .execute('get', true); + + ({ roles, notes, userGroup, addresses, comments, ...userObject as any } = + data); + const firstName = faker.person.firstName(); + const lastName = faker.person.lastName(); + newUser = { + id: faker.number.int({ min: 0, max: 999999 }), + firstName: firstName, + lastName: lastName, + isActive: faker.datatype.boolean(), + login: faker.internet.userName({ + lastName: firstName, + firstName: lastName, + }), + testReal: [faker.number.float({ fractionDigits: 4 })], + testArrayNull: null, + testDate: faker.date.anytime(), + } as Users; + + addressForTest = await pullAddress(); + await em.persistAndFlush(addressForTest); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + afterAll(() => { + mikroORMUsers.close(true); + }); + + it('should be update attr', async () => { + const { + id, + manager: mewManager, + userGroup: newUserGroup, + addresses: newAddresses, + comments: newComments, + notes: newNotes, + roles: newRoles, + ...otherAttr + } = newUser; + + const result = await patchOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, userObject.id, { + id: userObject.id.toString(), + attributes: otherAttr, + type: 'users', + }); + + const fromDb = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .where({ + id: userObject.id, + }) + .leftJoinAndSelect('Users.addresses', 'Addresses_addresses') + .leftJoinAndSelect('Users.comments', 'Comments_comments') + .leftJoinAndSelect('Users.roles', 'Roles__roles') + .leftJoinAndSelect('Users.notes', 'Notes__notes') + .leftJoinAndSelect('Users.userGroup', 'UserGroups__userGroup') + .limit(1) + .getSingleResult(); + + expect(result).toEqual(fromDb); + }); + + it('should be update relation', async () => { + const { + id, + manager: mewManager, + userGroup: newUserGroup, + addresses: newAddresses, + comments: newComments, + notes: newNotes, + roles: newRoles, + ...otherAttr + } = newUser; + + const setRoles = await microOrmServiceUser.microOrmUtilService + .queryBuilder(Roles) + .limit(2, 5) + .getResult(); + + const result = await patchOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, userObject.id, { + id: userObject.id.toString(), + attributes: otherAttr, + type: 'users', + relationships: { + addresses: { + data: { + id: addressForTest.id.toString(), + type: 'addresses', + }, + }, + comments: { + data: [], + }, + userGroup: { + data: null, + }, + roles: { + data: setRoles.map((i) => ({ id: i.id.toString(), type: 'roles' })), + }, + }, + }); + + const fromDb = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .where({ + id: userObject.id, + }) + .leftJoinAndSelect('Users.addresses', 'Addresses_addresses') + .leftJoinAndSelect('Users.comments', 'Comments_comments') + .leftJoinAndSelect('Users.roles', 'Roles__roles') + .leftJoinAndSelect('Users.notes', 'Notes__notes') + .leftJoinAndSelect('Users.userGroup', 'UserGroups__userGroup') + .limit(1) + .getSingleResult(); + + expect(result).toEqual(fromDb); + }); + + it('should be error', async () => { + await expect( + patchOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, 1, { + attributes: {}, + type: 'users', + }) + ).rejects.toThrow(UnprocessableEntityException); + + await expect( + patchOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, 10000, { + id: '10000', + attributes: {}, + type: 'users', + }) + ).rejects.toThrow(NotFoundException); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-one/patch-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-one/patch-one.ts new file mode 100644 index 00000000..3897f3c9 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-one/patch-one.ts @@ -0,0 +1,52 @@ +import { + NotFoundException, + UnprocessableEntityException, +} from '@nestjs/common'; +import { ObjectTyped } from '../../../../utils/nestjs-shared'; +import { ObjectLiteral, ValidateQueryError } from '../../../../types'; +import { MicroOrmService } from '../../service'; +import { PatchData } from '../../../mixin/zod'; + +export async function patchOne( + this: MicroOrmService, + id: number | string, + inputData: PatchData +): Promise { + const { id: idBody, attributes, relationships } = inputData; + + if (`${id}` !== idBody) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + message: `Data 'id' must be equal to url param`, + path: ['data', 'id'], + }; + + throw new UnprocessableEntityException([error]); + } + + const existEntity = await this.microOrmUtilService + .queryBuilder() + .where({ + [this.microOrmUtilService.currentPrimaryColumn]: id, + }) + .getSingleResult(); + + if (!existEntity) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + message: `Resource '${this.microOrmUtilService.currentAlias}' with id '${id}' does not exist`, + path: ['data', 'id'], + }; + throw new NotFoundException([error]); + } + + if (attributes) { + const attrTarget = this.microOrmUtilService.createEntity(attributes as any); + + for (const [props, val] of ObjectTyped.entries(attrTarget)) { + if (!(props in attributes)) continue; + existEntity[props] = val; + } + } + return this.microOrmUtilService.saveEntity(existEntity, relationships); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-relationship/patch-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-relationship/patch-relationship.spec.ts new file mode 100644 index 00000000..5018a6b2 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-relationship/patch-relationship.spec.ts @@ -0,0 +1,134 @@ +import { Collection, EntityManager, MikroORM } from '@mikro-orm/core'; +import { faker } from '@faker-js/faker'; + +import { + Addresses, + dbRandomName, + getModuleForPgLite, + Notes, + pullData, + Roles, + UserGroups, + Users, + Comments, + pullAddress, +} from '../../../../mock-utils/microrom'; +import { MicroOrmService } from '../../service'; + +import { + CURRENT_ENTITY_MANAGER_TOKEN, + ORM_SERVICE, +} from '../../../../constants'; + +import { patchRelationship } from './patch-relationship'; +import { EntityRelation } from '../../../../utils/nestjs-shared'; + +describe('patch-relationship', () => { + let mikroORMUsers: MikroORM; + let microOrmServiceUser: MicroOrmService; + let em: EntityManager; + let dbName: string; + let addressForTest: Addresses; + let addresses: Addresses; + let userGroup: UserGroups[]; + let notes: Collection; + let roles: Roles[]; + let comments: Collection; + let userObject: Users; + let newUser: Users; + beforeAll(async () => { + dbName = dbRandomName(); + const moduleUsers = await getModuleForPgLite(Users, dbName); + microOrmServiceUser = moduleUsers.get>(ORM_SERVICE); + mikroORMUsers = moduleUsers.get(MikroORM); + em = moduleUsers.get(CURRENT_ENTITY_MANAGER_TOKEN); + await pullData(em, 10); + }); + + beforeEach(async () => { + userObject = (await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .limit(1) + .getSingleResult()) as Users; + + roles = await microOrmServiceUser.microOrmUtilService + .queryBuilder(Roles) + .getResult(); + + userGroup = await microOrmServiceUser.microOrmUtilService + .queryBuilder(UserGroups) + .getResult(); + + const firstName = faker.person.firstName(); + const lastName = faker.person.lastName(); + newUser = { + id: faker.number.int({ min: 0, max: 999999 }), + firstName: firstName, + lastName: lastName, + isActive: faker.datatype.boolean(), + login: faker.internet.userName({ + lastName: firstName, + firstName: lastName, + }), + testReal: [faker.number.float({ fractionDigits: 4 })], + testArrayNull: null, + testDate: faker.date.anytime(), + } as Users; + + addressForTest = await pullAddress(); + await em.persistAndFlush(addressForTest); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + afterAll(() => { + mikroORMUsers.close(true); + }); + + it('should be ok', async () => { + const roles1 = faker.helpers.arrayElement(roles); + const roles2 = faker.helpers.arrayElement(roles); + const roles3 = faker.helpers.arrayElement(roles); + const userGroup1 = faker.helpers.arrayElement(userGroup); + const saveIdUserGroup = userGroup1.id; + await patchRelationship.call< + MicroOrmService, + Parameters>>, + ReturnType>> + >(microOrmServiceUser, userObject.id, 'roles', [ + { type: 'roles', id: roles1.id.toString() }, + { type: 'roles', id: roles2.id.toString() }, + { type: 'roles', id: roles3.id.toString() }, + ]); + + await patchRelationship.call< + MicroOrmService, + Parameters>>, + ReturnType>> + >(microOrmServiceUser, userObject.id, 'userGroup', { + type: 'user-groups', + id: saveIdUserGroup.toString(), + }); + + const checkData = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .leftJoinAndSelect('Users.roles', 'Roles__roles', {}, ['id']) + .leftJoinAndSelect('Users.userGroup', 'UserGroups__userGroup', {}, ['id']) + .where({ + id: userObject.id, + }) + .getSingleResult(); + + expect(checkData?.roles.map((i) => i.id)).toEqual( + expect.arrayContaining([roles1.id, roles2.id, roles3.id]) + ); + + expect(checkData?.roles.map((i) => i.id)).toHaveLength( + [roles1.id, roles2.id, roles3.id].length + ); + expect(checkData?.userGroup.id).toBe(saveIdUserGroup); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-relationship/patch-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-relationship/patch-relationship.ts new file mode 100644 index 00000000..38074d38 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-relationship/patch-relationship.ts @@ -0,0 +1,52 @@ +import { EntityRelation } from '../../../../utils/nestjs-shared'; + +import { ObjectLiteral } from '../../../../types'; +import { + PatchRelationshipData, + PostRelationshipData, +} from '../../../mixin/zod'; +import { getRelationship } from '../get-relationship/get-relationship'; +import { MicroOrmService } from '../../service'; + +export async function patchRelationship< + E extends ObjectLiteral, + Rel extends EntityRelation +>( + this: MicroOrmService, + id: number | string, + rel: Rel, + input: PatchRelationshipData +): Promise { + const idsResult = await this.microOrmUtilService.validateRelationInputData( + rel, + input + ); + const currentEntityRef = this.microOrmUtilService.entityManager.getReference( + this.microOrmUtilService.entity, + id as any + ); + + const relEntity = this.microOrmUtilService.getRelation(rel as any).entity(); + + if (Array.isArray(idsResult)) { + const relRef = idsResult.map((i) => + this.microOrmUtilService.entityManager.getReference(relEntity, i as any) + ); + currentEntityRef[rel].removeAll(); + currentEntityRef[rel].add(...relRef); + } else { + // @ts-ignore + currentEntityRef[rel] = this.microOrmUtilService.entityManager.getReference( + relEntity, + idsResult as any + ); + } + + await this.microOrmUtilService.entityManager.flush(); + + return getRelationship.call< + MicroOrmService, + Parameters>, + ReturnType> + >(this, id, rel); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-one/post-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-one/post-one.spec.ts new file mode 100644 index 00000000..9132f0d6 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-one/post-one.spec.ts @@ -0,0 +1,338 @@ +import { Collection, EntityManager, MikroORM } from '@mikro-orm/core'; +import { faker } from '@faker-js/faker'; + +import { + Addresses, + dbRandomName, + getModuleForPgLite, + Notes, + pullData, + Roles, + UserGroups, + Users, + Comments, + pullAddress, +} from '../../../../mock-utils/microrom'; +import { MicroOrmService } from '../../service'; + +import { + CURRENT_ENTITY_MANAGER_TOKEN, + ORM_SERVICE, +} from '../../../../constants'; + +import { postOne } from './post-one'; +import { BadRequestException } from '@nestjs/common'; + +describe('post-one', () => { + let mikroORMUsers: MikroORM; + let microOrmServiceUser: MicroOrmService; + let em: EntityManager; + let dbName: string; + let addressForTest: Addresses; + let addresses: Addresses; + let userGroup: UserGroups; + let notes: Collection; + let roles: Collection; + let comments: Collection; + let userObject: Users; + let newUser: Users; + beforeAll(async () => { + dbName = dbRandomName(); + const moduleUsers = await getModuleForPgLite(Users, dbName); + microOrmServiceUser = moduleUsers.get>(ORM_SERVICE); + mikroORMUsers = moduleUsers.get(MikroORM); + em = moduleUsers.get(CURRENT_ENTITY_MANAGER_TOKEN); + await pullData(em, 10); + }); + + beforeEach(async () => { + const data = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .leftJoinAndSelect('Users.addresses', 'Addresses_addresses', {}, ['id']) + .leftJoinAndSelect('Users.comments', 'Comments_comments', {}, ['id']) + .leftJoinAndSelect('Users.roles', 'Roles__roles', {}, ['id']) + .leftJoinAndSelect('Users.notes', 'Notes__notes', {}, ['id']) + .leftJoinAndSelect('Users.userGroup', 'UserGroups__userGroup', {}, ['id']) + .where({ + addresses: { + $exists: true, + }, + roles: { + $exists: true, + }, + notes: { + $exists: true, + }, + userGroup: { + $exists: true, + }, + comments: { + $exists: true, + }, + }) + .limit(1) + .execute('get', true); + + ({ roles, notes, userGroup, addresses, comments, ...userObject as any } = + data); + const firstName = faker.person.firstName(); + const lastName = faker.person.lastName(); + newUser = { + id: faker.number.int({ min: 0, max: 999999 }), + firstName: firstName, + lastName: lastName, + isActive: faker.datatype.boolean(), + login: faker.internet.userName({ + lastName: firstName, + firstName: lastName, + }), + testReal: [faker.number.float({ fractionDigits: 4 })], + testArrayNull: null, + testDate: faker.date.anytime(), + } as Users; + + addressForTest = await pullAddress(); + await em.persistAndFlush(addressForTest); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + afterAll(() => { + mikroORMUsers.close(true); + }); + + it('simple create', async () => { + const { + id, + manager: mewManager, + userGroup: newUserGroup, + addresses: newAddresses, + comments: newComments, + notes: newNotes, + roles: newRoles, + ...otherAttr + } = newUser; + + const result = await postOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, { + attributes: otherAttr, + type: 'users', + }); + + const { id: newId } = result; + + const fromDb = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .where({ + id: newId, + }) + .leftJoinAndSelect('Users.addresses', 'Addresses_addresses') + .leftJoinAndSelect('Users.comments', 'Comments_comments') + .leftJoinAndSelect('Users.roles', 'Roles__roles') + .leftJoinAndSelect('Users.notes', 'Notes__notes') + .leftJoinAndSelect('Users.userGroup', 'UserGroups__userGroup') + .limit(1) + .getSingleResult(); + + expect(result).toEqual(fromDb); + }); + + it('simple create withId', async () => { + const { + id, + manager: mewManager, + userGroup: newUserGroup, + addresses: newAddresses, + comments: newComments, + notes: newNotes, + roles: newRoles, + ...otherAttr + } = newUser; + + const result = await postOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, { + id: id.toString(), + attributes: otherAttr, + type: 'users', + }); + + const { id: newId } = result; + + const fromDb = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .where({ + id: newId, + }) + .leftJoinAndSelect('Users.addresses', 'Addresses_addresses') + .leftJoinAndSelect('Users.comments', 'Comments_comments') + .leftJoinAndSelect('Users.roles', 'Roles__roles') + .leftJoinAndSelect('Users.notes', 'Notes__notes') + .leftJoinAndSelect('Users.userGroup', 'UserGroups__userGroup') + .limit(1) + .getSingleResult(); + + expect(newId).toBe(id.toString()); + expect(result).toEqual(fromDb); + }); + + it('create with relation', async () => { + const { + id, + manager: mewManager, + userGroup: newUserGroup, + addresses: newAddresses, + comments: newComments, + notes: newNotes, + roles: newRoles, + ...otherAttr + } = newUser; + + const result = await postOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, { + attributes: otherAttr, + type: 'users', + relationships: { + addresses: { + data: { + id: addressForTest.id.toString(), + type: 'addresses', + }, + }, + comments: { + data: comments.map((i) => ({ + id: i.id.toString(), + type: 'comments', + })), + }, + roles: { + data: roles.map((i) => ({ + id: i.id.toString(), + type: 'roles', + })), + }, + notes: { + data: notes.map((i) => ({ + id: i.id.toString(), + type: 'notes', + })), + }, + userGroup: { + data: { + id: userGroup.id.toString(), + type: 'user-groups', + }, + }, + manager: { + data: { + id: userObject.id.toString(), + type: 'users', + }, + }, + }, + }); + + const { id: newId } = result; + + const fromDb = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .where({ + id: newId, + }) + .leftJoinAndSelect('Users.addresses', 'Addresses_addresses') + .leftJoinAndSelect('Users.comments', 'Comments_comments') + .leftJoinAndSelect('Users.roles', 'Roles__roles') + .leftJoinAndSelect('Users.notes', 'Notes__notes') + .leftJoinAndSelect('Users.userGroup', 'UserGroups__userGroup') + .limit(1) + .getSingleResult(); + + expect(result).toEqual(fromDb); + }); + + it('should be error', async () => { + const { + id, + manager: mewManager, + userGroup: newUserGroup, + addresses: newAddresses, + comments: newComments, + notes: newNotes, + roles: newRoles, + ...otherAttr + } = newUser; + + await expect( + postOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, { + attributes: otherAttr, + type: 'users', + relationships: { + addresses: { + data: { + id: '999999', + type: 'addresses', + }, + }, + }, + }) + ).rejects.toThrow(BadRequestException); + + await expect( + postOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, { + attributes: otherAttr, + type: 'users', + relationships: { + roles: { + data: [ + { + id: '999999', + type: 'addresses', + }, + ], + }, + }, + }) + ).rejects.toThrow(BadRequestException); + + await expect( + postOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, { + attributes: otherAttr, + type: 'users', + relationships: { + // @ts-expect-error check run time error + incorrectRel: { + data: [ + { + id: '999999', + type: 'addresses', + }, + ], + }, + }, + }) + ).rejects.toThrow(BadRequestException); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-one/post-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-one/post-one.ts new file mode 100644 index 00000000..239733c6 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-one/post-one.ts @@ -0,0 +1,23 @@ +import { ObjectLiteral } from '../../../../types'; +import { MicroOrmService } from '../../service'; +import { PostData } from '../../../mixin/zod'; + +export async function postOne( + this: MicroOrmService, + inputData: PostData +): Promise { + const { attributes, relationships, id } = inputData; + + const idObject = id + ? { [this.microOrmUtilService.currentPrimaryColumn.toString()]: id } + : {}; + + const attributesObject = { + ...attributes, + ...idObject, + }; + + const entityIns = this.microOrmUtilService.createEntity(attributesObject); + + return this.microOrmUtilService.saveEntity(entityIns, relationships); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-relationship/post-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-relationship/post-relationship.spec.ts new file mode 100644 index 00000000..2444afa3 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-relationship/post-relationship.spec.ts @@ -0,0 +1,147 @@ +import { Collection, EntityManager, MikroORM } from '@mikro-orm/core'; +import { faker } from '@faker-js/faker'; + +import { + Addresses, + dbRandomName, + getModuleForPgLite, + Notes, + pullData, + Roles, + UserGroups, + Users, + Comments, + pullAddress, +} from '../../../../mock-utils/microrom'; +import { MicroOrmService } from '../../service'; + +import { + CURRENT_ENTITY_MANAGER_TOKEN, + ORM_SERVICE, +} from '../../../../constants'; + +import { postRelationship } from './post-relationship'; +import { BadRequestException } from '@nestjs/common'; +import { EntityRelation } from '../../../../utils/nestjs-shared'; + +describe('post-relationshipa', () => { + let mikroORMUsers: MikroORM; + let microOrmServiceUser: MicroOrmService; + let em: EntityManager; + let dbName: string; + let addressForTest: Addresses; + let addresses: Addresses; + let userGroup: UserGroups[]; + let notes: Collection; + let roles: Roles[]; + let comments: Collection; + let userObject: Users; + let newUser: Users; + beforeAll(async () => { + dbName = dbRandomName(); + const moduleUsers = await getModuleForPgLite(Users, dbName); + microOrmServiceUser = moduleUsers.get>(ORM_SERVICE); + mikroORMUsers = moduleUsers.get(MikroORM); + em = moduleUsers.get(CURRENT_ENTITY_MANAGER_TOKEN); + await pullData(em, 10); + }); + + beforeEach(async () => { + userObject = (await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .limit(1) + .getSingleResult()) as Users; + + roles = await microOrmServiceUser.microOrmUtilService + .queryBuilder(Roles) + .getResult(); + + userGroup = await microOrmServiceUser.microOrmUtilService + .queryBuilder(UserGroups) + .getResult(); + + const firstName = faker.person.firstName(); + const lastName = faker.person.lastName(); + newUser = { + id: faker.number.int({ min: 0, max: 999999 }), + firstName: firstName, + lastName: lastName, + isActive: faker.datatype.boolean(), + login: faker.internet.userName({ + lastName: firstName, + firstName: lastName, + }), + testReal: [faker.number.float({ fractionDigits: 4 })], + testArrayNull: null, + testDate: faker.date.anytime(), + } as Users; + + addressForTest = await pullAddress(); + await em.persistAndFlush(addressForTest); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + afterAll(() => { + mikroORMUsers.close(true); + }); + + it('should be ok', async () => { + const roles1 = faker.helpers.arrayElement(roles); + const roles2 = faker.helpers.arrayElement(roles); + const roles3 = faker.helpers.arrayElement(roles); + const userGroup1 = faker.helpers.arrayElement(userGroup); + const saveIdUserGroup = userGroup1.id; + + const checkDataBefore = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .leftJoinAndSelect('Users.roles', 'Roles__roles', {}, ['id']) + .leftJoinAndSelect('Users.userGroup', 'UserGroups__userGroup', {}, ['id']) + .where({ + id: userObject.id, + }) + .getSingleResult(); + + const saveRolesIds = checkDataBefore?.roles.map((i) => i.id) || []; + + await postRelationship.call< + MicroOrmService, + Parameters>>, + ReturnType>> + >(microOrmServiceUser, userObject.id, 'roles', [ + { type: 'roles', id: roles1.id.toString() }, + { type: 'roles', id: roles2.id.toString() }, + { type: 'roles', id: roles3.id.toString() }, + ]); + + await postRelationship.call< + MicroOrmService, + Parameters>>, + ReturnType>> + >(microOrmServiceUser, userObject.id, 'userGroup', { + type: 'user-groups', + id: saveIdUserGroup.toString(), + }); + + const checkData = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .leftJoinAndSelect('Users.roles', 'Roles__roles', {}, ['id']) + .leftJoinAndSelect('Users.userGroup', 'UserGroups__userGroup', {}, ['id']) + .where({ + id: userObject.id, + }) + .getSingleResult(); + + expect(checkData?.roles.map((i) => i.id)).toEqual( + expect.arrayContaining([roles1.id, roles2.id, roles3.id]) + ); + + expect(checkData?.roles.map((i) => i.id)).toHaveLength( + [roles1.id, roles2.id, roles3.id].length + saveRolesIds.length + ); + expect(checkData?.userGroup.id).toBe(saveIdUserGroup); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-relationship/post-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-relationship/post-relationship.ts new file mode 100644 index 00000000..77e8edcb --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-relationship/post-relationship.ts @@ -0,0 +1,49 @@ +import { EntityRelation } from '../../../../utils/nestjs-shared'; + +import { ObjectLiteral } from '../../../../types'; +import { PostRelationshipData } from '../../../mixin/zod'; +import { getRelationship } from '../get-relationship/get-relationship'; +import { MicroOrmService } from '../../service'; + +export async function postRelationship< + E extends ObjectLiteral, + Rel extends EntityRelation +>( + this: MicroOrmService, + id: number | string, + rel: Rel, + input: PostRelationshipData +): Promise { + const idsResult = await this.microOrmUtilService.validateRelationInputData( + rel, + input + ); + + const currentEntityRef = this.microOrmUtilService.entityManager.getReference( + this.microOrmUtilService.entity, + id as any + ); + + const relEntity = this.microOrmUtilService.getRelation(rel as any).entity(); + + if (Array.isArray(idsResult)) { + const relRef = idsResult.map((i) => + this.microOrmUtilService.entityManager.getReference(relEntity, i as any) + ); + currentEntityRef[rel].add(...relRef); + } else { + // @ts-ignore + currentEntityRef[rel] = this.microOrmUtilService.entityManager.getReference( + relEntity, + idsResult as any + ); + } + + await this.microOrmUtilService.entityManager.flush(); + + return getRelationship.call< + MicroOrmService, + Parameters>, + ReturnType> + >(this, id, rel); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/index.ts new file mode 100644 index 00000000..7fab7d49 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/index.ts @@ -0,0 +1 @@ +export * from './microorm-service'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.spec.ts new file mode 100644 index 00000000..49af1af9 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.spec.ts @@ -0,0 +1,571 @@ +import { FilterOperand } from '../../../utils/nestjs-shared'; +import { MikroORM, RawQueryFragment } from '@mikro-orm/core'; +import { TestingModule } from '@nestjs/testing/testing-module'; + +import { MicroOrmUtilService } from './micro-orm-util.service'; + +import { + dbRandomName, + getDefaultQuery, + getModuleForPgLite, + Roles, + UserGroups, + Users, +} from '../../../mock-utils/microrom'; + +describe('MicroOrmUtilService', () => { + let mikroORMUserGroup: MikroORM; + let mikroORMUsers: MikroORM; + let mikroORMRoles: MikroORM; + let microOrmUtilsServiceUserGroups: MicroOrmUtilService; + let microOrmUtilsServiceUsers: MicroOrmUtilService; + let microOrmUtilsServiceRoles: MicroOrmUtilService; + let moduleRoles: TestingModule; + let moduleUsers: TestingModule; + let moduleUserGroup: TestingModule; + + let dbName: string; + + beforeAll(async () => { + dbName = dbRandomName(true); + moduleUserGroup = await getModuleForPgLite(UserGroups, dbName); + + microOrmUtilsServiceUserGroups = + moduleUserGroup.get>(MicroOrmUtilService); + mikroORMUserGroup = moduleUserGroup.get(MikroORM); + + moduleUsers = await getModuleForPgLite(Users, dbName); + microOrmUtilsServiceUsers = + moduleUsers.get>(MicroOrmUtilService); + + mikroORMUsers = moduleUsers.get(MikroORM); + + moduleRoles = await getModuleForPgLite(Roles, dbName); + microOrmUtilsServiceRoles = + moduleRoles.get>(MicroOrmUtilService); + mikroORMRoles = moduleRoles.get(MikroORM); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + afterAll(async () => { + await mikroORMUserGroup.close(true); + await mikroORMUsers.close(true); + await mikroORMRoles.close(true); + }); + + it('currentAlias', () => { + expect(microOrmUtilsServiceUserGroups.currentAlias).toBe('UserGroups'); + }); + + it('currentPrimaryColumn', () => { + expect(microOrmUtilsServiceUserGroups.currentPrimaryColumn).toBe('id'); + }); + + it('getAliasForEntity relation', () => { + expect(() => + microOrmUtilsServiceUsers.getAliasForPivotTable(Users, 'login') + ).toThrowError( + expect.objectContaining({ + message: expect.stringContaining('relation not found'), + }) + ); + + expect(() => + microOrmUtilsServiceUsers.getAliasForPivotTable(Users, 'notes') + ).toThrowError( + expect.objectContaining({ + message: expect.stringContaining('Many to many relation expected'), + }) + ); + + expect( + microOrmUtilsServiceUsers.getAliasForPivotTable(Users, 'roles') + ).toBe('users_have_roles'); + }); + + it('queryBuilder', () => { + const resultUserGroups = microOrmUtilsServiceUserGroups.queryBuilder(); + expect(resultUserGroups.mainAlias.aliasName).toBe('UserGroups'); + expect(resultUserGroups.mainAlias.entityName).toBe('UserGroups'); + + const resultUsers = microOrmUtilsServiceUserGroups.queryBuilder(Users); + expect(resultUsers.mainAlias.aliasName).toBe('Users'); + expect(resultUsers.mainAlias.entityName).toBe('Users'); + + const resultTestUsersAlias = microOrmUtilsServiceUserGroups.queryBuilder( + Users, + 'TestUsers' + ); + expect(resultTestUsersAlias.mainAlias.entityName).toBe('Users'); + expect(resultTestUsersAlias.mainAlias.aliasName).toBe('TestUsers'); + + const resultTestUsersOnlyAlias = + microOrmUtilsServiceUserGroups.queryBuilder('TestUserGroups'); + expect(resultTestUsersOnlyAlias.mainAlias.entityName).toBe('UserGroups'); + expect(resultTestUsersOnlyAlias.mainAlias.aliasName).toBe('TestUserGroups'); + }); + + it('defaultOrder', () => { + expect(microOrmUtilsServiceUserGroups.defaultOrder).toEqual({ id: 'ASC' }); + }); + describe('getFilterExpressionForTarget', () => { + it('expression for target field, target is null, should be null expression array', () => { + const query = getDefaultQuery(); + const result = + microOrmUtilsServiceUsers.getFilterExpressionForTarget(query); + + expect(result).toEqual([]); + }); + it('expression for target field with target field', async () => { + const nullableField = 'id'; + const nullableFieldValue = null; + const notNullableField = 'login'; + const notNullableFieldValue = null; + const regexpField = 'firstName'; + const regexpFieldValue = 'firstName'; + + const otherFiled = 'lastName'; + const otherFiledValue1: [string, string, string] = ['1', '3', '4']; + const otherFiledValue2 = 'test'; + const otherFiledValue3 = 'test2'; + + const arrayField = 'testReal'; + const query = getDefaultQuery(); + query.filter.target = { + [nullableField]: { + [FilterOperand.eq]: nullableFieldValue, + }, + [notNullableField]: { + [FilterOperand.ne]: notNullableFieldValue, + }, + [regexpField]: { + [FilterOperand.regexp]: regexpFieldValue, + }, + [otherFiled]: { + [FilterOperand.in]: otherFiledValue1, + [FilterOperand.nin]: otherFiledValue1, + [FilterOperand.like]: otherFiledValue2, + [FilterOperand.gt]: otherFiledValue3, + }, + [arrayField]: { + [FilterOperand.some]: otherFiledValue1, + }, + }; + + const [ + id, + login, + regexpFieldConditional, + otherFiledConditional, + arrayFieldConditional, + ] = microOrmUtilsServiceUsers.getFilterExpressionForTarget(query); + + expect(id).toEqual({ + [nullableField]: { + ['$' + FilterOperand.eq]: nullableFieldValue, + }, + }); + + expect(login).toEqual({ + [notNullableField]: { + ['$' + FilterOperand.ne]: notNullableFieldValue, + }, + }); + + expect(regexpFieldConditional).toEqual({ + [regexpField]: { + ['$re']: regexpFieldValue, + }, + }); + expect(otherFiledConditional).toEqual({ + [otherFiled]: { + ['$' + FilterOperand.in]: otherFiledValue1, + ['$' + FilterOperand.nin]: otherFiledValue1, + ['$' + FilterOperand.like]: otherFiledValue2, + ['$' + FilterOperand.gt]: otherFiledValue3, + }, + }); + expect(arrayFieldConditional).toEqual({ + [arrayField]: { + ['$overlap']: otherFiledValue1, + }, + }); + }); + + it('expression for target field with relation field not exist', async () => { + const oneToOneToMyself = 'manager'; + const oneToOneToOther = 'addresses'; + const manyToMany = 'roles'; + const oneToMany = 'comments'; + const manyToOne = 'userGroup'; + + const query = getDefaultQuery(); + query.filter.target = { + [oneToOneToMyself]: { + [FilterOperand.eq]: 'null', + }, + [oneToOneToOther]: { + [FilterOperand.eq]: 'null', + }, + [manyToMany]: { + [FilterOperand.eq]: 'null', + }, + [oneToMany]: { + [FilterOperand.eq]: 'null', + }, + [manyToOne]: { + [FilterOperand.eq]: 'null', + }, + }; + + const [ + oneToOneToMyselfEq, + oneToOneToOtherEq, + manyToManyEq, + oneToManyEq, + manyToOneEq, + ] = microOrmUtilsServiceUsers.getFilterExpressionForTarget(query); + + expect(oneToOneToMyselfEq).toEqual({ + [oneToOneToMyself]: { $exists: false }, + }); + expect(oneToOneToOtherEq).toEqual({ + [oneToOneToOther]: { $exists: false }, + }); + expect(manyToOneEq).toEqual({ [manyToOne]: { $exists: false } }); + expect(manyToManyEq).toBeInstanceOf(RawQueryFragment); + expect(oneToManyEq).toBeInstanceOf(RawQueryFragment); + if (!(oneToManyEq instanceof RawQueryFragment)) + throw new Error('Is not RawQueryFragment'); + if (!(manyToManyEq instanceof RawQueryFragment)) + throw new Error('Is not RawQueryFragment'); + + expect(manyToManyEq.sql).toBe( + 'not exists (select 1 from "public"."users_have_roles" as "users_have_roles" where "users_have_roles"."user_id" = "Users"."id")' + ); + expect(oneToManyEq.sql).toBe( + 'not exists (select 1 from "public"."comments" as "Comments" where "Comments"."created_by" = "Users"."id")' + ); + }); + it('expression for target field with relation field exist', async () => { + const oneToOneToMyself = 'manager'; + const oneToOneToOther = 'addresses'; + const manyToMany = 'roles'; + const oneToMany = 'comments'; + const manyToOne = 'userGroup'; + + const query = getDefaultQuery(); + query.filter.target = { + [oneToOneToMyself]: { + [FilterOperand.ne]: 'null', + }, + [oneToOneToOther]: { + [FilterOperand.ne]: 'null', + }, + [manyToMany]: { + [FilterOperand.ne]: 'null', + }, + [oneToMany]: { + [FilterOperand.ne]: 'null', + }, + [manyToOne]: { + [FilterOperand.ne]: 'null', + }, + }; + + const [ + oneToOneToMyselfNe, + oneToOneToOtherNe, + manyToManyNe, + oneToManyNe, + manyToOneNe, + ] = microOrmUtilsServiceUsers.getFilterExpressionForTarget(query); + + expect(oneToOneToMyselfNe).toEqual({ + [oneToOneToMyself]: { $exists: true }, + }); + expect(oneToOneToOtherNe).toEqual({ + [oneToOneToOther]: { $exists: true }, + }); + expect(manyToOneNe).toEqual({ [manyToOne]: { $exists: true } }); + expect(manyToManyNe).toBeInstanceOf(RawQueryFragment); + expect(oneToManyNe).toBeInstanceOf(RawQueryFragment); + if (!(oneToManyNe instanceof RawQueryFragment)) + throw new Error('Is not RawQueryFragment'); + if (!(manyToManyNe instanceof RawQueryFragment)) + throw new Error('Is not RawQueryFragment'); + + expect(manyToManyNe.sql).toBe( + 'exists (select 1 from "public"."users_have_roles" as "users_have_roles" where "users_have_roles"."user_id" = "Users"."id")' + ); + expect(oneToManyNe.sql).toBe( + 'exists (select 1 from "public"."comments" as "Comments" where "Comments"."created_by" = "Users"."id")' + ); + }); + + it('expression for target field with relation field exist with sort and include', async () => { + const oneToOneToMyself = 'manager'; + const oneToOneToOther = 'addresses'; + const manyToMany = 'roles'; + const oneToMany = 'comments'; + const manyToOne = 'userGroup'; + + const query = getDefaultQuery(); + query.filter.target = { + [oneToOneToMyself]: { + [FilterOperand.ne]: 'null', + }, + [oneToOneToOther]: { + [FilterOperand.ne]: 'null', + }, + [manyToMany]: { + [FilterOperand.ne]: 'null', + }, + [oneToMany]: { + [FilterOperand.ne]: 'null', + }, + [manyToOne]: { + [FilterOperand.ne]: 'null', + }, + }; + + query.sort = { + [manyToMany]: { key: 'ASC' }, + }; + query.include = [oneToMany]; + + const [ + oneToOneToMyselfNe, + oneToOneToOtherNe, + manyToManyNe, + oneToManyNe, + manyToOneNe, + ] = microOrmUtilsServiceUsers.getFilterExpressionForTarget(query); + + expect(oneToOneToMyselfNe).toEqual({ + [oneToOneToMyself]: { $exists: true }, + }); + expect(oneToOneToOtherNe).toEqual({ + [oneToOneToOther]: { $exists: true }, + }); + expect(manyToOneNe).toEqual({ [manyToOne]: { $exists: true } }); + expect(manyToManyNe).toEqual({ roles: { $exists: true } }); + expect(oneToManyNe).toEqual({ comments: { $exists: true } }); + }); + }); + + describe('getFilterExpressionForRelation', () => { + it('expression for relation field OneToMany and ManyToOne', async () => { + const query = getDefaultQuery(); + + query.filter.relation = { + comments: { + kind: { + [FilterOperand.eq]: 'test', + }, + }, + userGroup: { + label: { + [FilterOperand.eq]: 'test', + }, + }, + }; + + const [comments, userGroup] = + microOrmUtilsServiceUsers.getFilterExpressionForRelation(query); + + if (!(userGroup instanceof RawQueryFragment)) + throw new Error('Is not RawQueryFragment'); + + expect(userGroup.sql).toBe( + `exists (select 1 from "public"."user_groups" as "UserGroups" where "UserGroups"."id" = "Users"."user_groups_id" and "UserGroups"."label" = 'test')` + ); + + if (!(comments instanceof RawQueryFragment)) + throw new Error('Is not RawQueryFragment'); + + expect(comments.sql).toBe( + `exists (select 1 from "public"."comments" as "Comments" where "Comments"."created_by" = "Users"."id" and "Comments"."kind" = 'test')` + ); + }); + + it('expression for relation field OneToMany and ManyToOne with sort', async () => { + const query = getDefaultQuery(); + query.sort = { + userGroup: { + label: 'ASC', + }, + comments: { + kind: 'ASC', + }, + }; + query.filter.relation = { + comments: { + kind: { + [FilterOperand.eq]: 'test', + }, + }, + userGroup: { + label: { + [FilterOperand.eq]: 'test', + }, + }, + }; + + const [comments, userGroup] = + microOrmUtilsServiceUsers.getFilterExpressionForRelation(query); + + expect(userGroup).toEqual({ userGroup: { label: { $eq: 'test' } } }); + + expect(comments).toEqual({ comments: { kind: { $eq: 'test' } } }); + }); + + it('expression for relation field OneToOne', async () => { + const query = getDefaultQuery(); + + query.filter.relation = { + manager: { + login: { + [FilterOperand.eq]: 'test', + }, + }, + addresses: { + city: { + [FilterOperand.eq]: 'test', + }, + }, + }; + const [manager, addresses] = + microOrmUtilsServiceUsers.getFilterExpressionForRelation(query); + expect(manager).toEqual({ manager: { login: { $eq: 'test' } } }); + expect(addresses).toEqual({ addresses: { city: { $eq: 'test' } } }); + }); + + it('expression for relation field ManyToMany', async () => { + const query = getDefaultQuery(); + const queryRoles = getDefaultQuery(); + + query.filter.relation = { + roles: { + key: { + [FilterOperand.eq]: 'test', + [FilterOperand.ne]: 'test2', + }, + isDefault: { + [FilterOperand.eq]: 'false', + }, + }, + }; + + queryRoles.filter.relation = { + users: { + login: { + [FilterOperand.eq]: 'test', + [FilterOperand.ne]: 'test2', + }, + isActive: { + [FilterOperand.eq]: 'false', + }, + }, + }; + + const [roles] = + microOrmUtilsServiceUsers.getFilterExpressionForRelation(query); + + if (!(roles instanceof RawQueryFragment)) + throw new Error('Is not RawQueryFragment'); + + expect(roles.sql).toBe( + `exists (select 1 from "public"."users_have_roles" as "users_have_roles" left join "public"."roles" as "r1" on "users_have_roles"."role_id" = "r1"."id" where "users_have_roles"."user_id" = "Users"."id" and "r1"."key" = 'test' and "r1"."key" != 'test2' and "r1"."is_default" = 'false')` + ); + + const [users] = + microOrmUtilsServiceRoles.getFilterExpressionForRelation(queryRoles); + + if (!(users instanceof RawQueryFragment)) + throw new Error('Is not RawQueryFragment'); + expect(users.sql).toBe( + `exists (select 1 from "public"."users_have_roles" as "users_have_roles" left join "public"."users" as "u1" on "users_have_roles"."user_id" = "u1"."id" where "users_have_roles"."role_id" = "Roles"."id" and "u1"."login" = 'test' and "u1"."login" != 'test2' and "u1"."is_active" = 'false')` + ); + }); + + it('expression for relation field ManyToMany with sort', async () => { + const query = getDefaultQuery(); + const queryRoles = getDefaultQuery(); + + query.filter.relation = { + roles: { + key: { + [FilterOperand.eq]: 'test', + [FilterOperand.ne]: 'test2', + }, + isDefault: { + [FilterOperand.eq]: 'false', + }, + }, + }; + query.sort = { + roles: { key: 'ASC' }, + }; + + queryRoles.filter.relation = { + users: { + login: { + [FilterOperand.eq]: 'test', + [FilterOperand.ne]: 'test2', + }, + isActive: { + [FilterOperand.eq]: 'false', + }, + }, + }; + + queryRoles.sort = { + users: { login: 'ASC' }, + }; + + const [roles] = + microOrmUtilsServiceUsers.getFilterExpressionForRelation(query); + + expect(roles).toEqual({ + roles: { + isDefault: { $eq: 'false' }, + key: { $eq: 'test', $ne: 'test2' }, + }, + }); + + const [users] = + microOrmUtilsServiceRoles.getFilterExpressionForRelation(queryRoles); + + expect(users).toEqual({ + users: { + isActive: { $eq: 'false' }, + login: { $eq: 'test', $ne: 'test2' }, + }, + }); + }); + + it('getConditionalForJoin', () => { + const query = getDefaultQuery(); + query.filter.relation = { + roles: { + key: { + [FilterOperand.eq]: 'test', + [FilterOperand.ne]: 'test2', + }, + }, + }; + const result = microOrmUtilsServiceRoles.getConditionalForJoin( + query, + 'roles' + ); + expect(result).toEqual({ + key: { $eq: 'test', $ne: 'test2' }, + }); + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.ts new file mode 100644 index 00000000..3401d88d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.ts @@ -0,0 +1,751 @@ +import { + BadRequestException, + Inject, + Injectable, + NotFoundException, + UnprocessableEntityException, +} from '@nestjs/common'; +import { + EntityClass, + EntityKey, + EntityProperty, + EntityRepository, + type QBFilterQuery, + QBQueryOrderMap, + raw, + ReferenceKind, + EntityData, + Collection, +} from '@mikro-orm/core'; +import { + Knex, + SqlEntityManager, + QueryBuilder, + type Field, +} from '@mikro-orm/knex'; +import { + camelToKebab, + EntityRelation, + FilterOperand, + ObjectTyped, +} from '../../../utils/nestjs-shared'; + +import { + ASC, + CURRENT_ENTITY, + CURRENT_ENTITY_MANAGER_TOKEN, + CURRENT_ENTITY_REPOSITORY, +} from '../../../constants'; + +import { ObjectLiteral, ValidateQueryError } from '../../../types'; +import { Query, QueryOne, Relationships } from '../../mixin/zod'; +import { In } from 'typeorm'; +import { TupleOfEntityRelation } from '../../mixin/types'; +import { InputValidateData, ValidateReturn } from '../../type-orm/service'; + +type RelationshipsResult = { + [K in EntityRelation]: E[K] extends Collection ? E[K] : E[K] | null; +}; + +function isRelationField( + relationField: string[], + field: any +): asserts field is EntityKey { + if (relationField.includes(field)) return; + const error: ValidateQueryError = { + code: 'unrecognized_keys', + path: ['data', 'relationships'], + message: `Resource for relation '${field.toString()}' does not exist`, + keys: [field], + }; + + throw new BadRequestException([error]); +} +const getErrorObject = ( + props: string, + message: string +): ValidateQueryError => ({ + code: 'invalid_arguments', + path: ['data', 'relationships'], + message, +}); + +Injectable(); +export class MicroOrmUtilService { + @Inject(CURRENT_ENTITY_MANAGER_TOKEN) + public readonly entityManager!: SqlEntityManager; + @Inject(CURRENT_ENTITY_REPOSITORY) + private entityRepository!: EntityRepository; + @Inject(CURRENT_ENTITY) public readonly entity!: EntityClass; + + private _relationsName!: EntityKey[]; + private _relationMap = new Map, EntityProperty>(); + private _relationPropsMap = new Map, EntityKey[]>(); + get relationsName() { + if (!this._relationsName) { + this._relationsName = this.metadata.relations.map((r) => r.name); + } + + return this._relationsName; + } + + getRelationProps(entity: EntityClass) { + const props = this._relationPropsMap.get(entity); + if (props) { + return props; + } + const relMetaData = this.entityManager.getMetadata(entity); + const relation = relMetaData.relations.map((i) => i.name); + const newProps = relMetaData.props + .filter((r) => !relation.includes(r.name)) + .map((r) => r.name); + this._relationPropsMap.set(entity, newProps); + + return newProps; + } + + getRelation(name: EntityKey) { + let relation = this._relationMap.get(name); + if (!relation) { + relation = this.metadata.relations.find((r) => r.name === name); + if (!relation) { + throw new Error(`Relation ${name} not found in ${this.metadata.name}`); + } + this._relationMap.set(name, relation); + } + + return relation; + } + + get currentAlias() { + return this.entityRepository.getEntityName(); + } + + get metadata() { + return this.entityManager.getMetadata().get(this.entity); + } + + get currentPrimaryColumn() { + return this.metadata.getPrimaryProp().name; + } + + get defaultOrder(): QBQueryOrderMap { + return { + [this.currentPrimaryColumn]: ASC, + }; + } + + getAliasForEntity(entity: EntityClass) { + return this.entityManager.getRepository(entity).getEntityName(); + } + + getAliasForPivotTable(relName: keyof T): string; + getAliasForPivotTable( + entity: EntityClass, + relName: keyof T + ): string; + getAliasForPivotTable( + entity: EntityClass | keyof T, + relName?: keyof T + ): string { + if (!relName) { + relName = entity as keyof T; + entity = this.entity; + } else { + entity = entity as EntityClass; + } + + const propsRelation = this.entityManager + .getMetadata() + .get(entity) + .relations.find((r) => r.name.toString() === relName.toString()); + if (!propsRelation) + throw new Error(`${relName.toString()} relation not found`); + if (propsRelation.kind !== ReferenceKind.MANY_TO_MANY) + throw new Error('Many to many relation expected'); + + return this.entityManager + .getRepository(propsRelation.pivotEntity) + .getEntityName(); + } + + queryBuilder( + entity: EntityClass, + alias: string + ): QueryBuilder; + queryBuilder( + entity: EntityClass + ): QueryBuilder; + queryBuilder( + alias: string + ): QueryBuilder; + queryBuilder(): QueryBuilder; + queryBuilder( + ...arg: + | [entity: EntityClass, alias: string] + | [entity: EntityClass] + | [alias: string] + | [undefined, undefined] + ): QueryBuilder { + let [entity, alias] = arg; + + if (entity && !alias) { + if (typeof entity === 'string') { + alias = entity; + entity = this.entity; + } else { + alias = this.getAliasForEntity(entity); + } + } + if (!entity) { + alias = this.currentAlias; + entity = this.entity; + } + + if (!entity || !alias) { + throw new Error('entity or alias not found'); + } + + return this.entityManager.createQueryBuilder( + entity, + alias + ); + } + + getFilterExpressionForTarget( + query: Query + ): QBFilterQuery[] { + const result: QBFilterQuery[] = []; + const filterTarget = this.getFilterObject(query, 'target'); + const { sort, include } = query; + if (!filterTarget) return result; + + for (const [fieldName, filter] of ObjectTyped.entries(filterTarget)) { + const tmpField = fieldName as unknown as EntityKey; + + if (filter === undefined) continue; + const filterObject: QBFilterQuery = { + [tmpField]: {}, + }; + let subQueryExpression: QBFilterQuery | undefined; + for (const entries of ObjectTyped.entries(filter)) { + const [operand, value] = entries as [FilterOperand, string]; + + if (!this.relationsName.includes(tmpField)) { + const operandForMiroOrmResult = this.extractedResultOperand(operand); + filterObject[tmpField.toString()][operandForMiroOrmResult] = value; + continue; + } + + const relation = this.getRelation(tmpField); + switch (relation.kind) { + case ReferenceKind.MANY_TO_MANY: + case ReferenceKind.ONE_TO_MANY: { + if (sort && tmpField in sort) { + filterObject[tmpField.toString()]['$exists'] = + operand === FilterOperand.ne; + break; + } + + if (include && include.includes(tmpField)) { + filterObject[tmpField.toString()]['$exists'] = + operand === FilterOperand.ne; + break; + } + + const subQuery = + this.getSubQueryForRelation(tmpField).getFormattedQuery(); + const type = operand === FilterOperand.ne ? 'exists' : 'not exists'; + const resultQuery = `${type} (${subQuery})`; + subQueryExpression = raw(resultQuery); + break; + } + default: + filterObject[tmpField.toString()]['$exists'] = + operand === FilterOperand.ne; + } + } + + result.push(subQueryExpression ? subQueryExpression : filterObject); + } + + return result; + } + + getConditionalForJoin( + query: Query, + key: string + ): QBFilterQuery { + const filterRelation = this.getFilterObject(query, 'relation'); + + if (!filterRelation) return {}; + if (!(key in filterRelation)) return {}; + + for (const [key, reletionConditional] of ObjectTyped.entries( + filterRelation + )) { + if (key !== key) continue; + if (!reletionConditional) continue; + + for (const [field, conditional] of ObjectTyped.entries( + reletionConditional + )) { + if (!conditional) continue; + return Object.entries(conditional).reduce((acum, [operand, value]) => { + acum[field.toString()] = { + ...(acum[field.toString()] || {}), + [this.extractedResultOperand(operand as FilterOperand)]: value, + }; + return acum; + }, {} as QBFilterQuery); + } + } + + return {}; + } + + private extractedResultOperand(operand: FilterOperand) { + return operand === 'regexp' + ? '$re' + : operand === 'some' + ? '$overlap' + : (('$' + operand) as `$${FilterOperand}`); + } + + getFilterExpressionForRelation( + query: Query + ): QBFilterQuery[] { + const result: QBFilterQuery[] = []; + const filterRelation = this.getFilterObject(query, 'relation'); + const sort = query.sort; + if (!filterRelation) return result; + + for (const [relationField, propsFilter] of ObjectTyped.entries( + filterRelation + )) { + const fieldName = relationField as unknown as EntityKey; + const relationProps = this.getRelation(fieldName); + if (!propsFilter) continue; + if (!this.relationsName.includes(fieldName)) continue; + const filterObject: QBFilterQuery = { + [relationField]: {}, + }; + let subQueryExpression: + | ReturnType + | undefined; + for (const [relationFieldProps, filter] of ObjectTyped.entries( + propsFilter + )) { + const fieldProps = relationFieldProps as unknown as EntityKey; + if (!filter) continue; + for (const entries of ObjectTyped.entries(filter)) { + const [operand, value] = entries as [FilterOperand, string]; + switch (relationProps.kind) { + case ReferenceKind.MANY_TO_MANY: + case ReferenceKind.ONE_TO_MANY: + case ReferenceKind.MANY_TO_ONE: + { + if (sort && relationField in sort) { + filterObject[fieldName][fieldProps] = filterObject[fieldName][ + fieldProps + ] = filterObject[fieldName][fieldProps] || {}; + filterObject[fieldName][fieldProps][ + this.extractedResultOperand(operand) + ] = value; + } else { + if (!subQueryExpression) { + subQueryExpression = this.getSubQueryForRelation(fieldName); + } + const expression = + relationProps.kind === ReferenceKind.MANY_TO_MANY + ? { + [this.getInverseFieldForManyToMany(fieldName)]: { + [fieldProps]: { + [this.extractedResultOperand(operand)]: value, + }, + }, + } + : { + [fieldProps]: { + [this.extractedResultOperand(operand)]: value, + }, + }; + subQueryExpression.where(expression, '$and'); + } + } + break; + default: + filterObject[fieldName][fieldProps] = + filterObject[fieldName][fieldProps] || {}; + filterObject[fieldName][fieldProps][ + this.extractedResultOperand(operand) + ] = value; + } + } + } + if (subQueryExpression) { + const resultQuery = `exists (${subQueryExpression.getFormattedQuery()})`; + result.push(raw(resultQuery)); + subQueryExpression = undefined; + } else { + result.push(filterObject); + } + } + + return result; + } + + getKnex(): Knex { + return this.entityManager.getKnex(); + } + + prePareQueryBuilder( + queryBuilder: QueryBuilder, + query: Query | QueryOne + ): QueryBuilder { + const { fields, include } = query; + const relationFields: Record = {}; + + if (fields) { + const { target, ...relations } = fields; + Object.assign(relationFields, relations); + if (target) { + if (!target.includes(this.currentPrimaryColumn)) { + target.unshift(this.currentPrimaryColumn); + } + queryBuilder.select(target as unknown as Field); + } + } + + const resultInclude = new Set([ + ...[...(include || []), ...ObjectTyped.keys(relationFields)], + ]); + + for (const item of resultInclude) { + const relationProps = this.getRelation( + item as unknown as EntityKey + ); + const relationEntity = relationProps.entity() as EntityClass; + const relationAlias = this.getAliasForEntity(relationEntity); + const mainAlias = this.currentAlias; + + const condition: QBFilterQuery = this.getConditionalForJoin( + query as Query, + item + ); + + let selectJoin: string[] = this.getRelationProps(relationEntity); + if (item in relationFields) { + const tmpSet = new Set([ + ...relationFields[item], + this.getPrimaryNameFor(item as any), + ]); + + selectJoin = [...tmpSet]; + } + + queryBuilder.leftJoinAndSelect( + `${mainAlias}.${item}`, + `${relationAlias}__${item}`, + condition, + selectJoin + ); + } + return queryBuilder; + } + + getPrimaryNameFor(rel: EntityRelation) { + const relationEntity = this.getRelation( + rel as unknown as EntityKey + ).entity() as EntityClass; + return this.entityManager.getMetadata().get(relationEntity).getPrimaryProp() + .name; + } + + private getFilterObject( + query: Query, + filterType: 'target' | 'relation' + ) { + const { filter } = query; + if (!filter) return null; + return filter[filterType]; + } + + private getSubQueryForRelation(propsName: EntityKey) { + const relation = this.getRelation(propsName); + + let pivotTableName; + let filedCheck: string | undefined; + let expressionColumnName: string | undefined = this.currentPrimaryColumn; + if (relation.kind === ReferenceKind.MANY_TO_MANY) { + pivotTableName = this.getAliasForPivotTable(propsName); + filedCheck = relation.joinColumns.at(0); + } else if (relation.kind === ReferenceKind.ONE_TO_MANY) { + pivotTableName = this.getAliasForEntity(relation.entity() as any); + filedCheck = relation.mappedBy; + } else { + expressionColumnName = relation.joinColumns.at(0); + pivotTableName = this.getAliasForEntity(relation.entity() as any); + filedCheck = relation.referencedColumnNames.at(0); + } + + if (!filedCheck) throw new Error('filedCheck not found'); + if (!expressionColumnName) + throw new Error('expressionColumnName not found'); + + return this.entityManager + .createQueryBuilder(pivotTableName, pivotTableName) + .select(raw('1')) + .from(pivotTableName) + .where({ + [filedCheck]: this.entityManager + .getKnex() + .ref(`${this.currentAlias}.${expressionColumnName}`), + }); + } + + private getInverseFieldForManyToMany(propsName: EntityKey) { + const relation = this.getRelation(propsName); + const pivotTableName = this.getAliasForPivotTable(propsName); + + const pivotMetaData = this.entityManager.getMetadata().get(pivotTableName); + + const props = pivotMetaData.props.find( + (prop) => + prop.targetMeta && + prop.targetMeta.properties[propsName] && + prop.targetMeta.properties[propsName].entity() === relation.entity() + ); + + if (!props) + throw new Error( + `ManyToMany relation ${propsName} not found in ${pivotTableName}` + ); + + return props.inversedBy || props.mappedBy; + } + + createEntity(params: EntityData) { + return this.entityManager.create(this.entity, params, { + partial: true, + persist: false, + }); + } + + private async *asyncIterateFindRelationships( + relationships: NonNullable> + ): AsyncGenerator> { + for (const entries of ObjectTyped.entries(relationships)) { + const [props, dataItem] = entries; + isRelationField(this.relationsName, props); + const propsKey = props as unknown as EntityKey; + if (dataItem === undefined) continue; + + const { data } = dataItem; + if (data === undefined) continue; + if (data === null) { + yield { [props]: null } as RelationshipsResult; + continue; + } + const isArray = Array.isArray(data); + if (isArray && data.length === 0) { + yield { [props]: [] } as RelationshipsResult; + continue; + } + + const condition = isArray + ? { + $in: data.map((i) => (i || {}).id), + } + : { + $eq: data['id'], + }; + + const relationProps = this.metadata.properties[propsKey]; + const relationEntity = relationProps.entity() as EntityClass; + const metadata = this.entityManager.getMetadata().get(relationEntity); + const primaryName = metadata.getPrimaryProp().name; + + const queryBuilder = this.queryBuilder( + relationEntity, + this.getAliasForEntity(relationEntity) + ) + .select([primaryName]) + .where({ + [primaryName]: condition, + }); + let result: typeof relationEntity | (typeof relationEntity)[]; + let error: BadRequestException | undefined = undefined; + if (isArray) { + const tmpResult = await queryBuilder.getResult(); + + if (tmpResult.length === 0 || data.length !== tmpResult.length) { + const msg = `Resource '${metadata.className}' with ids '${data + .map((i) => (i || {})['id']) + .filter((i) => !tmpResult.find((r) => r[primaryName] == i)) + .join(',')}' does not exist`; + error = new BadRequestException([getErrorObject(props, msg)]); + } + result = tmpResult; + } else { + const tmpResult = await queryBuilder.getSingleResult(); + if (!tmpResult) + error = new BadRequestException([ + getErrorObject( + props, + `Resource '${metadata.className}' with id '${data.id}' does not exist` + ), + ]); + result = tmpResult; + } + if (error) throw error; + yield { [props]: result } as RelationshipsResult; + } + } + + async saveEntity( + targetInstance: E, + relationships?: Relationships + ): Promise { + if (relationships) { + for await (const item of this.asyncIterateFindRelationships( + relationships + )) { + const itemProps = ObjectTyped.entries(item).at(0); + if (!itemProps) continue; + const [nameProps, data] = itemProps; + + if ((targetInstance[nameProps] as any) instanceof Collection) { + targetInstance[nameProps].removeAll(); + targetInstance[nameProps].add(...(data as [])); + } else { + Object.assign(targetInstance, item); + } + } + } + + await this.entityManager.persistAndFlush(targetInstance); + + return targetInstance; + } + + async validateRelationInputData< + Rel extends EntityRelation, + In extends InputValidateData | InputValidateData[] + >(rel: Rel, inputData: In): Promise>; + async validateRelationInputData< + Rel extends EntityRelation, + In extends InputValidateData | InputValidateData[] + >(rel: Rel, inputData: In): Promise>; + async validateRelationInputData< + Rel extends EntityRelation, + In extends null | InputValidateData | InputValidateData[] + >(rel: Rel, inputData: In): Promise>; + async validateRelationInputData< + Rel extends EntityRelation, + In extends null | InputValidateData | InputValidateData[] + >(rel: Rel, inputData: In): Promise> { + const property = Reflect.get(this.metadata.properties, rel); + const isArray = Array.isArray(inputData); + + if ( + [ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes( + property.kind + ) && + !isArray + ) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + path: ['data'], + message: 'Body data should be array', + }; + + throw new UnprocessableEntityException([error]); + } + + if ( + [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes( + property.kind + ) && + isArray + ) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + path: ['data'], + message: 'Body data should be object', + }; + + throw new UnprocessableEntityException([error]); + } + + if (inputData === null) { + const result = null; + return result as ValidateReturn; + } + if (isArray && inputData.length === 0) { + const result: any[] = []; + return result as ValidateReturn; + } + + const prepareData = isArray ? inputData : [inputData]; + + const errors: ValidateQueryError[] = []; + let i = 0; + const relationEntity = this.getRelation(rel as any).entity(); + const typeName = camelToKebab( + this.entityManager.getMetadata().get(relationEntity).className + ); + + for (const prepareItem of prepareData) { + if (prepareItem.type !== typeName) { + const path = isArray ? ['data', i.toString()] : ['data']; + errors.push({ + code: 'invalid_arguments', + path: path, + message: `Type should be equal to type of relName: "${rel.toString()}". Type of ${rel.toString()} is "${typeName}" but receive - "${ + prepareItem.type + }"`, + }); + } + i++; + } + if (errors.length) { + throw new UnprocessableEntityException(errors); + } + + const checkResult = await this.queryBuilder(relationEntity as any) + .where({ + [this.getPrimaryNameFor(rel)]: { + $in: prepareData.map((i) => i.id), + }, + }) + .getResult(); + + if (checkResult.length === prepareData.length) { + return ( + isArray ? inputData.map((i) => i.id) : inputData.id + ) as ValidateReturn; + } + + const resulDataMap = checkResult.reduce((acum, item) => { + acum[item[this.getPrimaryNameFor(rel)]] = true; + return acum; + }, {} as Record); + + i = 0; + for (const item of prepareData) { + if (!resulDataMap[item.id]) { + const path = isArray ? ['data', i.toString(), 'id'] : ['data', 'id']; + errors.push({ + code: 'invalid_arguments', + path: path, + message: `Not exist item "${ + item.id + }" in relation "${rel.toString()}"`, + }); + } + i++; + } + throw new NotFoundException(errors); + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/microorm-service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/microorm-service.ts new file mode 100644 index 00000000..2cf332fe --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/microorm-service.ts @@ -0,0 +1,218 @@ +import { + EntityRelation, + QueryField, + ResourceObject, + ResourceObjectRelationships, +} from '../../../utils/nestjs-shared'; +import { Inject } from '@nestjs/common'; + +import { ObjectLiteral } from '../../../types'; +import { OrmService } from '../../mixin/types'; +import { + PatchData, + PatchRelationshipData, + PostData, + PostRelationshipData, + Query, + QueryOne, +} from '../../mixin/zod'; + +import { + getAll, + getOne, + deleteOne, + postOne, + patchOne, + getRelationship, + deleteRelationship, + patchRelationship, + postRelationship, +} from '../orm-methods'; +import { MicroOrmUtilService } from './micro-orm-util.service'; +import { JsonApiTransformerService } from '../../mixin/service/json-api-transformer.service'; + +export class MicroOrmService implements OrmService { + @Inject(MicroOrmUtilService) microOrmUtilService!: MicroOrmUtilService; + @Inject(JsonApiTransformerService) + jsonApiTransformerService!: JsonApiTransformerService; + + async getAll(query: Query): Promise> { + const { page } = query; + const { totalItems, items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(this, query); + + const { data, included } = this.jsonApiTransformerService.transformData( + items, + query + ); + + const meta = { + totalItems: totalItems, + pageNumber: page.number, + pageSize: page.size, + }; + + return { + meta, + data, + ...(included ? { included } : {}), + }; + } + + async getOne( + id: number | string, + query: QueryOne + ): Promise> { + const result = await getOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(this, id, query); + const { data, included } = this.jsonApiTransformerService.transformData( + result, + query + ); + return { + meta: {}, + data, + ...(included ? { included } : {}), + }; + } + + async deleteOne(id: number | string): Promise { + await deleteOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(this, id); + } + + async postOne(inputData: PostData): Promise> { + const result = await postOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(this, inputData); + + const { relationships } = inputData; + const fakeQuery: Query = { + [QueryField.fields]: null, + [QueryField.include]: Object.keys(relationships || {}), + } as any; + + const resultForResponse = await getOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(this, result[this.microOrmUtilService.currentPrimaryColumn], fakeQuery); + + const { data, included } = this.jsonApiTransformerService.transformData( + resultForResponse, + fakeQuery + ); + + return { + meta: {}, + data, + ...(included ? { included } : {}), + }; + } + async patchOne( + id: number | string, + inputData: PatchData + ): Promise> { + await patchOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(this, id, inputData); + + const { relationships } = inputData; + const fakeQuery: Query = { + [QueryField.fields]: null, + [QueryField.include]: Object.keys(relationships || {}), + } as any; + + const resultForResponse = await getOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(this, id, fakeQuery); + + const { data, included } = this.jsonApiTransformerService.transformData( + resultForResponse, + fakeQuery + ); + + return { + meta: {}, + data, + ...(included ? { included } : {}), + }; + } + + async getRelationship>( + id: number | string, + rel: Rel + ): Promise> { + const result = await getRelationship.call< + MicroOrmService, + Parameters>, + ReturnType> + >(this, id, rel); + + return { + meta: {}, + data: this.jsonApiTransformerService.transformRel(result, rel), + }; + } + + async deleteRelationship>( + id: number | string, + rel: Rel, + input: PostRelationshipData + ): Promise { + await deleteRelationship.call< + MicroOrmService, + Parameters>, + ReturnType> + >(this, id, rel, input); + } + + async postRelationship>( + id: number | string, + rel: Rel, + input: PostRelationshipData + ): Promise> { + const result = await postRelationship.call< + MicroOrmService, + Parameters>, + ReturnType> + >(this, id, rel, input); + + return { + meta: {}, + data: this.jsonApiTransformerService.transformRel(result, rel), + }; + } + + async patchRelationship>( + id: number | string, + rel: Rel, + input: PatchRelationshipData + ): Promise> { + const result = await patchRelationship.call< + MicroOrmService, + Parameters>, + ReturnType> + >(this, id, rel, input); + + return { + meta: {}, + data: this.jsonApiTransformerService.transformRel(result, rel), + }; + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/type.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/type.ts new file mode 100644 index 00000000..8e1d71b9 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/type.ts @@ -0,0 +1,3 @@ +export type MicroOrmParam = { + arrayType?: string[]; +}; diff --git a/libs/json-api/json-api-nestjs/src/lib/config/bindings.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/config/bindings.spec.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/config/bindings.spec.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/config/bindings.spec.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/config/bindings.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/config/bindings.ts similarity index 95% rename from libs/json-api/json-api-nestjs/src/lib/config/bindings.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/config/bindings.ts index 282f0346..346a17df 100644 --- a/libs/json-api/json-api-nestjs/src/lib/config/bindings.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/config/bindings.ts @@ -1,10 +1,10 @@ import { Body, Param, Query, RequestMethod } from '@nestjs/common'; +import { ObjectTyped } from '../../../utils/nestjs-shared'; import { BindingsConfig, MethodName } from '../types'; -import { JsonBaseController } from '../mixin'; +import { JsonBaseController } from '../controller/json-base.controller'; +import { PARAMS_RELATION_NAME, PARAMS_RESOURCE_ID } from '../../../constants'; -import { PARAMS_RELATION_NAME, PARAMS_RESOURCE_ID } from '../constants'; -import { ObjectTyped } from '../helper'; import { queryInputMixin, queryMixin, @@ -17,7 +17,7 @@ import { parseRelationshipNamePipeMixin, postRelationshipPipeMixin, patchRelationshipPipeMixin, -} from '../mixin/pipe'; +} from '../pipe'; const Bindings: BindingsConfig = { getAll: { diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/controller/json-base.controller.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/controller/json-base.controller.ts similarity index 50% rename from libs/json-api/json-api-nestjs/src/lib/mixin/controller/json-base.controller.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/controller/json-base.controller.ts index 0581ba74..146d54eb 100644 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/controller/json-base.controller.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/controller/json-base.controller.ts @@ -1,62 +1,64 @@ import { - MethodName, - Entity, - TypeormService, EntityRelation, ResourceObject, ResourceObjectRelationships, -} from '../../types'; +} from '../../../utils/nestjs-shared'; + +import { ORM_SERVICE_PROPS } from '../../../constants'; +import { MethodName } from '../types'; +import { ObjectLiteral } from '../../../types'; import { - PostData, - Query, PatchData, - PostRelationshipData, PatchRelationshipData, -} from '../../helper'; -import { TYPEORM_SERVICE_PROPS } from '../../constants'; - -type RequestMethodeObject = { [k in MethodName]: (...arg: any[]) => any }; + PostData, + PostRelationshipData, + Query, + QueryOne, +} from '../zod'; +import { OrmService } from '../types'; -interface IJsonBaseController extends RequestMethodeObject {} +type RequestMethodeObject = { + [K in MethodName]: OrmService[K]; +}; -export class JsonBaseController - implements IJsonBaseController +export class JsonBaseController + implements RequestMethodeObject { - private [TYPEORM_SERVICE_PROPS]!: TypeormService; + private [ORM_SERVICE_PROPS]!: OrmService; - getOne(id: string | number, query: Query): Promise> { - return this[TYPEORM_SERVICE_PROPS].getOne(id, query); + getOne(id: string | number, query: QueryOne): Promise> { + return this[ORM_SERVICE_PROPS].getOne(id, query); } getAll(query: Query): Promise> { - return this[TYPEORM_SERVICE_PROPS].getAll(query); + return this[ORM_SERVICE_PROPS].getAll(query); } deleteOne(id: string | number): Promise { - return this[TYPEORM_SERVICE_PROPS].deleteOne(id); + return this[ORM_SERVICE_PROPS].deleteOne(id); } patchOne( id: string | number, inputData: PatchData ): Promise> { - return this[TYPEORM_SERVICE_PROPS].patchOne(id, inputData); + return this[ORM_SERVICE_PROPS].patchOne(id, inputData); } postOne(inputData: PostData): Promise> { - return this[TYPEORM_SERVICE_PROPS].postOne(inputData); + return this[ORM_SERVICE_PROPS].postOne(inputData); } getRelationship>( id: string | number, relName: Rel ): Promise> { - return this[TYPEORM_SERVICE_PROPS].getRelationship(id, relName); + return this[ORM_SERVICE_PROPS].getRelationship(id, relName); } postRelationship>( id: string | number, relName: Rel, input: PostRelationshipData ): Promise> { - return this[TYPEORM_SERVICE_PROPS].postRelationship(id, relName, input); + return this[ORM_SERVICE_PROPS].postRelationship(id, relName, input); } deleteRelationship>( @@ -64,7 +66,7 @@ export class JsonBaseController relName: Rel, input: PostRelationshipData ): Promise { - return this[TYPEORM_SERVICE_PROPS].deleteRelationship(id, relName, input); + return this[ORM_SERVICE_PROPS].deleteRelationship(id, relName, input); } patchRelationship>( @@ -72,6 +74,6 @@ export class JsonBaseController relName: Rel, input: PatchRelationshipData ): Promise> { - return this[TYPEORM_SERVICE_PROPS].patchRelationship(id, relName, input); + return this[ORM_SERVICE_PROPS].patchRelationship(id, relName, input); } } diff --git a/libs/json-api/json-api-nestjs/src/lib/decorators/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/index.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/decorators/index.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/index.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/decorators/inject-service/inject-service.decorator.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/inject-service/inject-service.decorator.spec.ts similarity index 73% rename from libs/json-api/json-api-nestjs/src/lib/decorators/inject-service/inject-service.decorator.spec.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/inject-service/inject-service.decorator.spec.ts index cf68012c..e1485972 100644 --- a/libs/json-api/json-api-nestjs/src/lib/decorators/inject-service/inject-service.decorator.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/inject-service/inject-service.decorator.spec.ts @@ -2,11 +2,10 @@ import { PROPERTY_DEPS_METADATA, SELF_DECLARED_DEPS_METADATA, } from '@nestjs/common/constants'; -import { getProviderName } from '../../helper'; import 'reflect-metadata'; import { InjectService } from './inject-service.decorator'; -import { TYPEORM_SERVICE } from '../../constants'; +import { ORM_SERVICE } from '../../../../constants'; describe('InjectServiceDecorator', () => { it('should save property key', () => { @@ -21,11 +20,11 @@ describe('InjectServiceDecorator', () => { SomeClass ); expect( - properties.find((item: any) => item.type === TYPEORM_SERVICE) + properties.find((item: any) => item.type === ORM_SERVICE) ).toBeDefined(); expect( - properties1.find((item: any) => item.param === TYPEORM_SERVICE) + properties1.find((item: any) => item.param === ORM_SERVICE) ).toBeDefined(); }); }); diff --git a/libs/json-api/json-api-nestjs/src/lib/decorators/inject-service/inject-service.decorator.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/inject-service/inject-service.decorator.ts similarity index 58% rename from libs/json-api/json-api-nestjs/src/lib/decorators/inject-service/inject-service.decorator.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/inject-service/inject-service.decorator.ts index a51b36a9..ab1c5157 100644 --- a/libs/json-api/json-api-nestjs/src/lib/decorators/inject-service/inject-service.decorator.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/inject-service/inject-service.decorator.ts @@ -1,7 +1,7 @@ import { Inject } from '@nestjs/common'; -import { TYPEORM_SERVICE } from '../../constants'; +import { ORM_SERVICE } from '../../../../constants'; export function InjectService(): PropertyDecorator & ParameterDecorator { - return Inject(TYPEORM_SERVICE); + return Inject(ORM_SERVICE); } diff --git a/libs/json-api/json-api-nestjs/src/lib/decorators/json-api/json-api.decorator.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/json-api/json-api.decorator.spec.ts similarity index 96% rename from libs/json-api/json-api-nestjs/src/lib/decorators/json-api/json-api.decorator.spec.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/json-api/json-api.decorator.spec.ts index 6f7ffe86..fd246eb9 100644 --- a/libs/json-api/json-api-nestjs/src/lib/decorators/json-api/json-api.decorator.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/json-api/json-api.decorator.spec.ts @@ -3,10 +3,11 @@ import 'reflect-metadata'; import { JSON_API_DECORATOR_ENTITY, JSON_API_DECORATOR_OPTIONS, -} from '../../constants/reflection'; +} from '../../../../constants'; import { JsonApi } from './json-api.decorator'; -import { DecoratorOptions } from '../../types'; + import { excludeMethod, Bindings } from '../../config/bindings'; +import { DecoratorOptions } from '../../types'; describe('InjectServiceDecorator', () => { it('should save entity in class', () => { @@ -56,7 +57,7 @@ describe('InjectServiceDecorator', () => { const testedEntity = class SomeEntity {}; const apiOptions: DecoratorOptions = { allowMethod: ['getAll', 'deleteRelationship'], - overrideRoute: '123' + overrideRoute: '123', }; @JsonApi(testedEntity, apiOptions) diff --git a/libs/json-api/json-api-nestjs/src/lib/decorators/json-api/json-api.decorator.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/json-api/json-api.decorator.ts similarity index 69% rename from libs/json-api/json-api-nestjs/src/lib/decorators/json-api/json-api.decorator.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/json-api/json-api.decorator.ts index f0297ded..c73f8594 100644 --- a/libs/json-api/json-api-nestjs/src/lib/decorators/json-api/json-api.decorator.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/json-api/json-api.decorator.ts @@ -1,12 +1,12 @@ import { JSON_API_DECORATOR_ENTITY, JSON_API_DECORATOR_OPTIONS, -} from '../../constants'; -import { Entity } from '../../types'; +} from '../../../../constants'; +import { EntityClass, ObjectLiteral } from '../../../../types'; import { DecoratorOptions } from '../../types'; -export function JsonApi( - entity: Entity, +export function JsonApi( + entity: EntityClass, options?: DecoratorOptions ): ClassDecorator { return (target): typeof target => { diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/factory/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/factory/index.ts new file mode 100644 index 00000000..0f09495c --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/factory/index.ts @@ -0,0 +1 @@ +export * from './zod-validate.factory'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/factory/zod-validate.factory.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/factory/zod-validate.factory.ts new file mode 100644 index 00000000..942c24c6 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/factory/zod-validate.factory.ts @@ -0,0 +1,284 @@ +import { FactoryProvider, ValueProvider } from '@nestjs/common'; + +import { + ZOD_INPUT_QUERY_SCHEMA, + ZOD_QUERY_SCHEMA, + ZOD_POST_SCHEMA, + ZOD_PATCH_SCHEMA, + ZOD_POST_RELATIONSHIP_SCHEMA, + ZOD_PATCH_RELATIONSHIP_SCHEMA, + ENTITY_MAP_PROPS, +} from '../../../constants'; + +import { + zodInputQuery, + ZodInputQuery, + zodQuery, + ZodQuery, + ZodPost, + zodPost, + zodPatch, + ZodPatch, + zodPostRelationship, + ZodPostRelationship, + zodPatchRelationship, + ZodPatchRelationship, +} from '../zod'; +import { EntityClass, ObjectLiteral } from '../../../types'; +import { + AllFieldWithType, + ArrayPropsForEntity, + EntityProps, + FieldWithType, + PropsForField, + RelationPrimaryColumnType, + RelationPropsArray, + RelationPropsTypeName, + RelationTree, + ResultGetField, + ZodEntityProps, +} from '../types'; +import { ObjectTyped } from '../../../utils/nestjs-shared'; + +function getEntityMap( + entityMapProps: Map, ZodEntityProps>, + entity: Function & { prototype: E } +) { + const entityMap = entityMapProps.get(entity); + if (!entityMap) throw new Error('Entity not found in map'); + return entityMap; +} + +export function getParamsForOatchANdPostZod( + entityMapProps: Map, ZodEntityProps>, + entity: Function & { prototype: E } +) { + const entityMap = getEntityMap(entityMapProps, entity); + + const { + primaryColumnType, + typeName, + propsType, + primaryColumnName, + propsNullable, + relationProperty, + } = entityMap; + const fieldWithType = {} as FieldWithType; + const propsDb = {} as PropsForField; + const relationArrayProps = {} as RelationPropsArray; + const relationPopsName = {} as RelationPropsTypeName; + const primaryColumnTypeForRel = {} as RelationPrimaryColumnType; + + for (const [name, type] of ObjectTyped.entries(propsType)) { + Reflect.set(propsDb, name, { + type: type, + isArray: type === 'array', + isNullable: (propsNullable as any[]).includes(name), + }); + if (name === primaryColumnName) continue; + Reflect.set(fieldWithType, name, type); + } + + for (const [name, value] of ObjectTyped.entries(relationProperty)) { + const relEntityMap = getEntityMap(entityMapProps, value.entityClass); + + Reflect.set(relationArrayProps, name, value.isArray); + Reflect.set(relationPopsName, name, relEntityMap.className); + Reflect.set(primaryColumnTypeForRel, name, relEntityMap.primaryColumnType); + } + + return { + primaryColumnType, + typeName: typeName as any, + fieldWithType, + propsDb: propsDb as PropsForField, + primaryColumnName: primaryColumnName as EntityProps, + relationArrayProps, + relationPopsName, + primaryColumnTypeForRel, + }; +} + +export function ZodInputQuerySchema( + entity: EntityClass +): FactoryProvider> { + return { + provide: ZOD_INPUT_QUERY_SCHEMA, + inject: [ + { + token: ENTITY_MAP_PROPS, + optional: false, + }, + ], + useFactory: (entityMapProps: Map, ZodEntityProps>) => { + const entityMap = getEntityMap(entityMapProps, entity); + + const { props, relations, relationProperty } = entityMap; + + const entityRelationStructure = ObjectTyped.entries( + relationProperty + ).reduce((acum, [name, value]) => { + const relMap = getEntityMap(entityMapProps, value.entityClass); + Reflect.set(acum, name, relMap.props); + return acum; + }, {} as RelationTree); + + return zodInputQuery( + { + field: props, + relations: relations, + } as ResultGetField, + entityRelationStructure + ); + }, + }; +} + +export function ZodQuerySchema( + entity: EntityClass +): FactoryProvider> { + return { + provide: ZOD_QUERY_SCHEMA, + inject: [ + { + token: ENTITY_MAP_PROPS, + optional: false, + }, + ], + useFactory: (entityMapProps: Map, ZodEntityProps>) => { + const entityMap = getEntityMap(entityMapProps, entity); + + const { + props, + relations, + relationProperty, + propsType: propsTypeEntity, + } = entityMap; + + const propsType = { ...propsTypeEntity } as AllFieldWithType; + + const propsArray = { target: {} } as ArrayPropsForEntity; + + for (const [name, type] of ObjectTyped.entries(propsTypeEntity)) { + if (type !== 'array') continue; + Reflect.set(propsArray.target, name, true); + } + + const entityRelationStructure = {} as RelationTree; + for (const [name, value] of ObjectTyped.entries(relationProperty)) { + const relMap = getEntityMap(entityMapProps, value.entityClass); + + if (!(name in propsArray)) { + Reflect.set(propsArray, name, {}); + } + for (const [relNameField, type] of ObjectTyped.entries( + relMap.propsType + )) { + if (type !== 'array') continue; + const propsArrayObject = Reflect.get(propsArray, name); + Reflect.set(propsArrayObject, relNameField, true); + } + + Reflect.set(propsType, name, relMap.propsType); + Reflect.set(entityRelationStructure, name, relMap.props); + } + const entityFieldsStructure = { + field: props, + relations: relations, + } as ResultGetField; + + return zodQuery( + entityFieldsStructure, + entityRelationStructure, + propsArray, + propsType + ); + }, + }; +} + +export function ZodPostSchema( + entity: EntityClass +): FactoryProvider> { + return { + provide: ZOD_POST_SCHEMA, + inject: [ + { + token: ENTITY_MAP_PROPS, + optional: false, + }, + ], + useFactory: (entityMapProps: Map, ZodEntityProps>) => { + const { + primaryColumnType, + typeName, + fieldWithType, + propsDb, + primaryColumnName, + relationArrayProps, + relationPopsName, + primaryColumnTypeForRel, + } = getParamsForOatchANdPostZod(entityMapProps, entity); + + return zodPost( + primaryColumnType, + typeName, + fieldWithType, + propsDb, + primaryColumnName, + relationArrayProps, + relationPopsName, + primaryColumnTypeForRel + ); + }, + }; +} + +export function ZodPatchSchema( + entity: EntityClass +): FactoryProvider> { + return { + provide: ZOD_PATCH_SCHEMA, + inject: [ + { + token: ENTITY_MAP_PROPS, + optional: false, + }, + ], + useFactory: (entityMapProps: Map, ZodEntityProps>) => { + const { + primaryColumnType, + typeName, + fieldWithType, + propsDb, + primaryColumnName, + relationArrayProps, + relationPopsName, + primaryColumnTypeForRel, + } = getParamsForOatchANdPostZod(entityMapProps, entity); + + return zodPatch( + primaryColumnType, + typeName as I, + fieldWithType, + propsDb, + primaryColumnName as EntityProps, + relationArrayProps, + relationPopsName, + primaryColumnTypeForRel + ); + }, + }; +} + +export const ZodInputPostRelationshipSchema: ValueProvider = + { + provide: ZOD_POST_RELATIONSHIP_SCHEMA, + useValue: zodPostRelationship, + }; + +export const ZodInputPatchRelationshipSchema: ValueProvider = + { + provide: ZOD_PATCH_RELATIONSHIP_SCHEMA, + useValue: zodPatchRelationship, + }; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/bind-controller.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/bind-controller.spec.ts similarity index 86% rename from libs/json-api/json-api-nestjs/src/lib/helper/bind-controller.spec.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/bind-controller.spec.ts index 4c35d4a5..bf60890e 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/bind-controller.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/bind-controller.spec.ts @@ -1,6 +1,14 @@ +import { + METHOD_METADATA, + PATH_METADATA, + ROUTE_ARGS_METADATA, +} from '@nestjs/common/constants'; +import { ObjectTyped } from '../../../utils/nestjs-shared'; +import { RouteParamtypes } from '@nestjs/common/enums/route-paramtypes.enum'; + import { bindController } from './bind-controller'; -import { Users } from '../mock-utils'; -import { DEFAULT_CONNECTION_NAME } from '../constants'; +import { Users } from '../../../mock-utils/typeorm'; +import { DEFAULT_CONNECTION_NAME } from '../../../constants'; import { ParseIntPipe, Query, @@ -9,19 +17,13 @@ import { PipeTransform, ArgumentMetadata, } from '@nestjs/common'; -import { TypeormService } from '../types'; -import { PatchData } from './zod'; +import { OrmService } from '../types'; +import { PatchData } from '../zod'; import { JsonApi } from '../decorators'; -import { JsonBaseController } from '../mixin/controller/json-base.controller'; +import { JsonBaseController } from '../controller/json-base.controller'; import { excludeMethod } from '../config/bindings'; -import { - METHOD_METADATA, - PATH_METADATA, - ROUTE_ARGS_METADATA, -} from '@nestjs/common/constants'; -import { ObjectTyped } from './utils'; + import { Bindings } from '../config/bindings'; -import { RouteParamtypes } from '@nestjs/common/enums/route-paramtypes.enum'; const mapParams = new Map(); mapParams.set(Query, RouteParamtypes.QUERY); @@ -36,8 +38,8 @@ describe('bindController', () => { pipeForId: ParseIntPipe, debug: false, useSoftDelete: false, - }; - bindController(Controller, Users, DEFAULT_CONNECTION_NAME, config); + } as any; + bindController(Controller, Users, config); expect(Object.getOwnPropertyNames(Controller.prototype)).toEqual([ 'constructor', @@ -83,7 +85,7 @@ describe('bindController', () => { expect(paramsMetadataItem).not.toEqual(undefined); expect(paramsMetadataItem.index).toBe(parseInt(params)); tmp.mixins.forEach((i, k) => { - expect(i(Users, DEFAULT_CONNECTION_NAME, config).name).toEqual( + expect(i(Users, config).name).toEqual( paramsMetadataItem.pipes[k].name ); }); @@ -101,8 +103,8 @@ describe('bindController', () => { pipeForId: ParseIntPipe, debug: false, useSoftDelete: false, - }; - bindController(Controller, Users, DEFAULT_CONNECTION_NAME, config); + } as any; + bindController(Controller, Users, config); expect(Object.getOwnPropertyNames(Controller.prototype)).toEqual([ 'constructor', 'getAll', @@ -125,7 +127,7 @@ describe('bindController', () => { override patchOne( @Param('id', SomePipes) id: string | number, @Body(SomePipes) inputData: PatchData - ): ReturnType['patchOne']> { + ): ReturnType['patchOne']> { return super.patchOne(id, inputData); } } @@ -134,8 +136,8 @@ describe('bindController', () => { pipeForId: SomePipes, debug: false, useSoftDelete: false, - }; - bindController(Controller, Users, DEFAULT_CONNECTION_NAME, config); + } as any; + bindController(Controller, Users, config); const paramsMetadata = Reflect.getMetadata( ROUTE_ARGS_METADATA, diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/bind-controller.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/bind-controller.ts similarity index 90% rename from libs/json-api/json-api-nestjs/src/lib/helper/bind-controller.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/bind-controller.ts index 28eff2c9..de5c6d66 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/bind-controller.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/bind-controller.ts @@ -13,21 +13,14 @@ import { ROUTE_ARGS_METADATA } from '@nestjs/common/constants'; import { RouteParamtypes } from '@nestjs/common/enums/route-paramtypes.enum'; import { Bindings } from '../config/bindings'; -import { - ConfigParam, - DecoratorOptions, - Entity, - ExtractNestType, - MethodName, - NestController, -} from '../types'; -import { JSON_API_DECORATOR_OPTIONS } from '../constants'; +import { DecoratorOptions, MixinOptions, MethodName } from '../types'; +import { NestController, ExtractNestType } from '../../../types'; +import { JSON_API_DECORATOR_OPTIONS } from '../../../constants'; export function bindController( controller: ExtractNestType, - entity: Entity, - connectionName: string, - config: ConfigParam + entity: MixinOptions['entity'], + config: MixinOptions['config'] ): void { for (const methodName in Bindings) { const { name, path, parameters, method, implementation } = @@ -94,12 +87,11 @@ export function bindController( controller.prototype.constructor, name ); + for (const key in parameters) { const parameter = parameters[key]; const { property, decorator, mixins } = parameter; - const resultMixin = mixins.map((mixin) => - mixin(entity, connectionName, config) - ); + const resultMixin = mixins.map((mixin) => mixin(entity, config)); if (paramsMetadata) { let typeDecorator: RouteParamtypes; @@ -113,6 +105,7 @@ export function bindController( case Body: typeDecorator = RouteParamtypes.BODY; } + const tmp = Object.entries(paramsMetadata) .filter(([k, v]) => k.split(':').at(0) === typeDecorator.toString()) .reduce( diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/create-controller.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/create-controller.spec.ts similarity index 83% rename from libs/json-api/json-api-nestjs/src/lib/helper/create-controller.spec.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/create-controller.spec.ts index 2759ce9c..9060696b 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/create-controller.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/create-controller.spec.ts @@ -5,15 +5,15 @@ import { PROPERTY_DEPS_METADATA, } from '@nestjs/common/constants'; import { createController } from './create-controller'; -import { Users } from '../mock-utils'; -import { JsonBaseController } from '../mixin/controller/json-base.controller'; +import { Users } from '../../../mock-utils/typeorm'; +import { JsonBaseController } from '../controller/json-base.controller'; import { JSON_API_CONTROLLER_POSTFIX, - TYPEORM_SERVICE, - TYPEORM_SERVICE_PROPS, -} from '../constants'; + ORM_SERVICE, + ORM_SERVICE_PROPS, +} from '../../../constants'; import { InjectService, JsonApi } from '../decorators'; -import { ErrorInterceptors, LogTimeInterceptors } from '../mixin/interceptors'; +import { ErrorInterceptors, LogTimeInterceptors } from '../interceptors'; describe('createController', () => { it('Should be error', () => { @@ -83,13 +83,13 @@ describe('createController', () => { expect(intecept).not.toBe(undefined); expect(intecept[0]).toEqual(LogTimeInterceptors); expect(intecept[1]).toEqual(ErrorInterceptors); - expect(check[0].key).toBe(TYPEORM_SERVICE_PROPS); - expect(check[0].type).toEqual(TYPEORM_SERVICE); + expect(check[0].key).toBe(ORM_SERVICE_PROPS); + expect(check[0].type).toEqual(ORM_SERVICE); expect(check1[0].key).toBe('tmp'); - expect(check1[0].type).toEqual(TYPEORM_SERVICE); + expect(check1[0].type).toEqual(ORM_SERVICE); - expect(check1[1].key).toBe(TYPEORM_SERVICE_PROPS); - expect(check1[1].type).toEqual(TYPEORM_SERVICE); + expect(check1[1].key).toBe(ORM_SERVICE_PROPS); + expect(check1[1].type).toEqual(ORM_SERVICE); }); }); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/create-controller.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/create-controller.ts similarity index 51% rename from libs/json-api/json-api-nestjs/src/lib/helper/create-controller.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/create-controller.ts index d4ca8078..7a10058a 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/create-controller.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/create-controller.ts @@ -1,21 +1,22 @@ import { Controller, Inject, Type, UseInterceptors } from '@nestjs/common'; -import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type'; -import { camelToKebab, getProviderName, nameIt } from './utils'; +import { camelToKebab } from '../../../utils/nestjs-shared'; + +import { getProviderName, nameIt } from './utils'; import { JSON_API_CONTROLLER_POSTFIX, JSON_API_DECORATOR_OPTIONS, - TYPEORM_SERVICE, - TYPEORM_SERVICE_PROPS, -} from '../constants'; -import { JsonBaseController } from '../mixin/controller/json-base.controller'; -import { ErrorInterceptors, LogTimeInterceptors } from '../mixin/interceptors'; + ORM_SERVICE, + ORM_SERVICE_PROPS, +} from '../../../constants'; +import { JsonBaseController } from '../controller/json-base.controller'; +import { ErrorInterceptors, LogTimeInterceptors } from '../interceptors'; -import { DecoratorOptions } from '../types'; +import { DecoratorOptions, MixinOptions } from '../types'; export function createController( - entity: EntityClassOrSchema, - controller?: Type + entity: MixinOptions['entity'], + controller?: MixinOptions['controller'] ): Type { const controllerClass = controller || @@ -24,8 +25,7 @@ export function createController( JsonBaseController ); - const entityName = - entity instanceof Function ? entity.name : entity.options.name; + const entityName = entity.name; if ( !Object.prototype.isPrototypeOf.call(JsonBaseController, controllerClass) @@ -40,11 +40,13 @@ export function createController( controllerClass ); - Controller( - decoratorOptions?.['overrideRoute'] || `${camelToKebab(entityName)}` - )(controllerClass); + const controllerPath = + decoratorOptions && decoratorOptions['overrideRoute'] + ? decoratorOptions['overrideRoute'].toString() + : `${camelToKebab(entityName)}`; + Controller(controllerPath)(controllerClass); - Inject(TYPEORM_SERVICE)(controllerClass.prototype, TYPEORM_SERVICE_PROPS); + Inject(ORM_SERVICE)(controllerClass.prototype, ORM_SERVICE_PROPS); UseInterceptors(LogTimeInterceptors)(controllerClass); UseInterceptors(ErrorInterceptors)(controllerClass); return controllerClass; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/index.ts new file mode 100644 index 00000000..7f0e8bc2 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/index.ts @@ -0,0 +1,3 @@ +export * from './utils'; +export * from './create-controller'; +export * from './bind-controller'; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/utils.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/utils.spec.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/helper/utils.spec.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/utils.spec.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/utils.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/utils.ts similarity index 51% rename from libs/json-api/json-api-nestjs/src/lib/helper/utils.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/utils.ts index 1ac82eee..88ebd7cc 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/utils.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/utils.ts @@ -1,20 +1,6 @@ -import { EntityTarget } from 'typeorm/common/EntityTarget'; -import { ObjectLiteral } from 'typeorm/common/ObjectLiteral'; -import { Type } from '@nestjs/common/interfaces'; -import { JSON_API_DECORATOR_ENTITY } from '../constants'; +import { EntityTarget, ObjectLiteral } from '../../../types'; -import { Entity } from '../types'; - -import { upperFirstLetter } from 'shared-utils'; - -export { - camelToKebab, - snakeToCamel, - kebabToCamel, - upperFirstLetter, - isString, - ObjectTyped, -} from 'shared-utils'; +import { upperFirstLetter } from '../../../utils/nestjs-shared'; export const nameIt = ( name: string, @@ -46,11 +32,10 @@ export const getEntityName = ( return `${entity}`; }; -export function getProviderName(entity: EntityTarget, name: string) { +export function getProviderName( + entity: EntityTarget, + name: string +) { const entityName = getEntityName(entity); return `${upperFirstLetter(entityName)}${name}`; } - -export function entityForClass(type: Type): Entity { - return Reflect.getMetadata(JSON_API_DECORATOR_ENTITY, type); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/error.interceptors.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/error.interceptors.ts new file mode 100644 index 00000000..9d5d086e --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/error.interceptors.ts @@ -0,0 +1,120 @@ +import { + InternalServerErrorException, + CallHandler, + ExecutionContext, + Inject, + Injectable, + NestInterceptor, +} from '@nestjs/common'; +import { catchError, Observable, throwError } from 'rxjs'; +import { ObjectLiteral } from '../../../types'; +// import { QueryFailedError, Repository } from 'typeorm'; +// import { DriverUtils } from 'typeorm/driver/DriverUtils'; +// +// import { +// CONTROL_OPTIONS_TOKEN, +// CURRENT_ENTITY_REPOSITORY, +// } from '../../constants'; +// import { ConfigParam, Entity, ValidateQueryError } from '../../types'; +// import { +// MysqlError, +// MysqlErrorCode, +// PostgresError, +// PostgresErrorCode, +// } from '../../helper'; +// import { HttpException } from '@nestjs/common'; +// #TODO need implement +@Injectable() +export class ErrorInterceptors + implements NestInterceptor +{ + // @Inject(CURRENT_ENTITY_REPOSITORY) private repository!: Repository; + // @Inject(CONTROL_OPTIONS_TOKEN) private config!: ConfigParam; + + intercept( + context: ExecutionContext, + next: CallHandler + ): Observable | Promise> { + return next.handle(); + // return next.handle().pipe( + // catchError((error) => { + // if (error instanceof QueryFailedError) { + // return throwError(() => this.prepareDataBaseError(error)); + // } + // + // if (error instanceof HttpException) { + // return throwError(() => error); + // } + // + // const errorObject: ValidateQueryError = { + // code: 'internal_error', + // message: this.config.debug ? error.message : 'Internal Server Error', + // path: [], + // }; + // const descriptionOrOptions = this.config.debug ? error : undefined; + // return throwError( + // () => + // new InternalServerErrorException( + // [errorObject], + // descriptionOrOptions + // ) + // ); + // }) + // ); + } + + // private prepareDataBaseError(error: QueryFailedError): HttpException { + // const errorObject: ValidateQueryError = { + // code: 'internal_error', + // message: this.config.debug ? error.message : 'Internal Server Error', + // path: [], + // }; + // + // if (DriverUtils.isMySQLFamily(this.repository.manager.connection.driver)) { + // const { errorCode, errorMsg } = this.prepareMysqlError(error.driverError); + // if (MysqlError[errorCode]) { + // return MysqlError[errorCode](this.repository.metadata, errorMsg); + // } + // } + // + // if ( + // DriverUtils.isPostgresFamily(this.repository.manager.connection.driver) + // ) { + // const { errorCode, errorMsg, detail } = this.preparePostgresError( + // error.driverError + // ); + // + // if (PostgresError[errorCode]) { + // return PostgresError[errorCode]( + // this.repository.metadata, + // errorMsg, + // detail + // ); + // } + // } + // + // return new InternalServerErrorException([errorObject]); + // } + // + // private prepareMysqlError(error: any): { + // errorCode: MysqlErrorCode; + // errorMsg: string; + // } { + // return { + // errorCode: error.errno, + // errorMsg: error.message, + // }; + // } + // + // private preparePostgresError(error: any): { + // errorCode: PostgresErrorCode; + // errorMsg: string; + // detail: string; + // } { + // return { + // errorCode: error.code, + // errorMsg: error.message, + // detail: error.detail, + // }; + // } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/interceptors/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/index.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mixin/interceptors/index.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/index.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/log-time.interceptors.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/log-time.interceptors.ts new file mode 100644 index 00000000..2b6b2797 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/log-time.interceptors.ts @@ -0,0 +1,30 @@ +import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common'; +import { map, Observable } from 'rxjs'; +import { ObjectLiteral } from '../../../types'; + +// import { Entity, ResourceObject } from '../../types'; + +export class LogTimeInterceptors + implements NestInterceptor +{ + intercept( + context: ExecutionContext, + // next: CallHandler> + next: CallHandler + ): Observable | Promise> { + const now = Date.now(); + return next.handle(); + // .pipe( + // map((r) => { + // const response = context.switchToHttp().getResponse(); + // const time = Date.now() - now; + // response.setHeader('x-response-time', time); + // if (r && r.meta) { + // r.meta['time'] = time; + // } + // + // return r; + // }) + // ); + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/mixin.module.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/mixin.module.ts new file mode 100644 index 00000000..0326a314 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/mixin.module.ts @@ -0,0 +1,88 @@ +import { DynamicModule } from '@nestjs/common'; + +import { MixinOptions, DecoratorOptions } from './types'; +import { createController } from './helper'; +import { + JSON_API_DECORATOR_OPTIONS, + CONTROL_OPTIONS_TOKEN, + JSON_API_MODULE_POSTFIX, + CURRENT_ENTITY, + FIND_ONE_ROW_ENTITY, + CHECK_RELATION_NAME, +} from '../../constants'; +import { ConfigParam, RequiredFromPartial } from '../../types'; +import { MicroOrmParam } from '../micro-orm'; +import { TypeOrmParam } from '../type-orm'; +import { bindController, getProviderName, nameIt } from './helper'; +import { + ZodInputQuerySchema, + ZodPostSchema, + ZodQuerySchema, + ZodPatchSchema, + ZodInputPatchRelationshipSchema, + ZodInputPostRelationshipSchema, +} from './factory'; +import { SwaggerBindService } from './swagger'; +import { JsonApiTransformerService } from './service/json-api-transformer.service'; + +export class MixinModule { + static forRoot(options: MixinOptions): DynamicModule { + const { entity, controller, imports, ormModule } = options; + const controllerClass = createController(entity, controller); + + const decoratorOptions: DecoratorOptions = + Reflect.getMetadata(JSON_API_DECORATOR_OPTIONS, controllerClass) || {}; + + const moduleConfig: RequiredFromPartial< + ConfigParam & (MicroOrmParam | TypeOrmParam) + > = { + ...options.config, + ...decoratorOptions, + }; + + bindController(controllerClass, entity, moduleConfig); + const optionProvider = { + provide: CONTROL_OPTIONS_TOKEN, + useValue: moduleConfig, + }; + + const currentEntityProvider = { + provide: CURRENT_ENTITY, + useValue: entity, + }; + + const findOneRowEntityProvider = { + provide: FIND_ONE_ROW_ENTITY, + useValue: undefined, + }; + + const checkRelationNameProvider = { + provide: CHECK_RELATION_NAME, + useValue: undefined, + }; + + return { + module: nameIt( + getProviderName(entity, JSON_API_MODULE_POSTFIX), + MixinModule + ), + controllers: [controllerClass], + providers: [ + optionProvider, + currentEntityProvider, + findOneRowEntityProvider, + checkRelationNameProvider, + JsonApiTransformerService, + ...ormModule.getUtilProviders(entity), + ZodInputQuerySchema(entity), + ZodQuerySchema(entity), + ZodPatchSchema(entity), + ZodPostSchema(entity), + SwaggerBindService, + ZodInputPatchRelationshipSchema, + ZodInputPostRelationshipSchema, + ], + imports: imports, + }; + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/check-item-entity/check-item-entity.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/check-item-entity/check-item-entity.pipe.spec.ts new file mode 100644 index 00000000..a8e52f7d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/check-item-entity/check-item-entity.pipe.spec.ts @@ -0,0 +1,55 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { NotFoundException } from '@nestjs/common'; +import { CheckItemEntityPipe } from './check-item-entity.pipe'; +import { CURRENT_ENTITY, FIND_ONE_ROW_ENTITY } from '../../../../constants'; +import { EntityTarget } from 'typeorm/common/EntityTarget'; + +describe('CheckItemEntityPipe', () => { + let pipe: CheckItemEntityPipe; + let mockFindOneRowEntity: jest.Mock; + let mockEntityTarget: EntityTarget; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + CheckItemEntityPipe, + { provide: CURRENT_ENTITY, useValue: {} }, + { provide: FIND_ONE_ROW_ENTITY, useValue: jest.fn() }, + ], + }).compile(); + + pipe = module.get>(CheckItemEntityPipe); + mockEntityTarget = module.get>(CURRENT_ENTITY); + mockFindOneRowEntity = module.get(FIND_ONE_ROW_ENTITY); + }); + + it('should call findOneRowEntity and return the entity', async () => { + const mockEntity = { id: 1, name: 'Test Entity' }; + const mockValue = 1; + + mockFindOneRowEntity.mockResolvedValue(mockEntity); + const result = await pipe.transform(mockValue); + + expect(mockFindOneRowEntity).toHaveBeenCalledTimes(1); + expect(mockFindOneRowEntity).toHaveBeenCalledWith( + mockEntityTarget, + mockValue + ); + + expect(result).toBe(mockValue); + }); + + it('should throw a NotFoundException if no entity is found', async () => { + const mockValue = 1; + + mockFindOneRowEntity.mockResolvedValue(null); + + await expect(pipe.transform(mockValue)).rejects.toThrow(NotFoundException); + + expect(mockFindOneRowEntity).toHaveBeenCalledTimes(1); + expect(mockFindOneRowEntity).toHaveBeenCalledWith( + mockEntityTarget, + mockValue + ); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/check-item-entity/check-item-entity.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/check-item-entity/check-item-entity.pipe.ts new file mode 100644 index 00000000..8244d3aa --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/check-item-entity/check-item-entity.pipe.ts @@ -0,0 +1,34 @@ +import { Inject, NotFoundException, PipeTransform } from '@nestjs/common'; +import { ValidateQueryError } from '../../../../types'; +import { CURRENT_ENTITY, FIND_ONE_ROW_ENTITY } from '../../../../constants'; +import { EntityTarget, ObjectLiteral } from '../../../../types'; +import { FindOneRowEntity } from '../../types'; +import { getEntityName } from '../../helper'; + +export class CheckItemEntityPipe< + E extends ObjectLiteral, + I extends string | number +> implements PipeTransform> +{ + @Inject(CURRENT_ENTITY) private currentEntity!: EntityTarget; + @Inject(FIND_ONE_ROW_ENTITY) private findOneRowEntity!: + | FindOneRowEntity + | undefined; + async transform(value: I): Promise { + if (!this.findOneRowEntity || typeof this.findOneRowEntity !== 'function') + return value; + + const result = await this.findOneRowEntity(this.currentEntity, value); + if (!result) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + message: `Resource '${getEntityName( + this.currentEntity + )}' with id '${value}' does not exist`, + path: [], + }; + throw new NotFoundException([error]); + } + return value; + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/check-item-entity/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/check-item-entity/index.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mixin/pipe/check-item-entity/index.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/check-item-entity/index.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/index.spec.ts new file mode 100644 index 00000000..6970b810 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/index.spec.ts @@ -0,0 +1,46 @@ +import { INJECTABLE_WATERMARK } from '@nestjs/common/constants'; +import { PipeTransform } from '@nestjs/common/interfaces'; + +import { MixinOptions } from '../types'; +import { factoryMixin } from './index'; + +describe('factoryMixin', () => { + class TestEntity {} + + class TestPipe implements PipeTransform { + name = 'TestPipe'; + + transform(value: any) { + return value; + } + } + + function isInjectable(cls: any): boolean { + return Reflect.getMetadata(INJECTABLE_WATERMARK, cls); + } + + it('should return a pipe class with a new name', () => { + const pipeClass = factoryMixin( + TestEntity as MixinOptions['entity'], + TestPipe + ); + expect(pipeClass.name).toBe('TestEntityTestPipe'); + }); + + it('should return a pipe class that is Injectable', () => { + const pipeClass = factoryMixin( + TestEntity as MixinOptions['entity'], + TestPipe + ); + expect(isInjectable(pipeClass)).toBe(true); + }); + + it('should preserve the behavior of the original pipe', () => { + const pipeClass = factoryMixin( + TestEntity as MixinOptions['entity'], + TestPipe + ); + const instance = new pipeClass(); + expect(instance.transform('testValue', {} as any)).toBe('testValue'); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/index.ts new file mode 100644 index 00000000..b7c0cc13 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/index.ts @@ -0,0 +1,89 @@ +import { Injectable, ParseIntPipe } from '@nestjs/common'; +import { upperFirstLetter } from '../../../utils/nestjs-shared'; + +import { PipeMixin } from '../../../types'; +import { MixinOptions } from '../types'; +import { nameIt } from '../helper'; + +import { QueryInputPipe } from './query-input'; +import { QueryPipe } from './query'; +import { QueryFiledInIncludePipe } from './query-filed-on-include'; +import { QueryCheckSelectField } from './query-check-select-field'; +import { CheckItemEntityPipe } from './check-item-entity'; +import { PostInputPipe } from './post-input'; +import { PatchInputPipe } from './patch-input'; +import { ParseRelationshipNamePipe } from './parse-relationship-name'; +import { PostRelationshipPipe } from './post-relationship'; +import { PatchRelationshipPipe } from './patch-relationship'; + +export function factoryMixin(entity: MixinOptions['entity'], pipe: PipeMixin) { + const entityName = entity.name; + + const pipeClass = nameIt( + `${upperFirstLetter(entityName)}${pipe.name}`, + pipe + ) as PipeMixin; + + Injectable()(pipeClass); + + return pipeClass; +} + +export function queryInputMixin(entity: MixinOptions['entity']): PipeMixin { + return factoryMixin(entity, QueryInputPipe); +} + +export function queryMixin(entity: MixinOptions['entity']): PipeMixin { + return factoryMixin(entity, QueryPipe); +} + +export function queryFiledInIncludeMixin( + entity: MixinOptions['entity'] +): PipeMixin { + return factoryMixin(entity, QueryFiledInIncludePipe); +} + +export function queryCheckSelectFieldMixin( + entity: MixinOptions['entity'] +): PipeMixin { + return factoryMixin(entity, QueryCheckSelectField); +} + +export function idPipeMixin( + entity: MixinOptions['entity'], + config?: MixinOptions['config'] +): PipeMixin { + return config && config.pipeForId ? config.pipeForId : (ParseIntPipe as any); +} + +export function checkItemEntityPipeMixin( + entity: MixinOptions['entity'] +): PipeMixin { + return factoryMixin(entity, CheckItemEntityPipe); +} + +export function postInputPipeMixin(entity: MixinOptions['entity']): PipeMixin { + return factoryMixin(entity, PostInputPipe); +} + +export function patchInputPipeMixin(entity: MixinOptions['entity']): PipeMixin { + return factoryMixin(entity, PatchInputPipe); +} + +export function postRelationshipPipeMixin( + entity: MixinOptions['entity'] +): PipeMixin { + return factoryMixin(entity, PostRelationshipPipe); +} + +export function patchRelationshipPipeMixin( + entity: MixinOptions['entity'] +): PipeMixin { + return factoryMixin(entity, PatchRelationshipPipe); +} + +export function parseRelationshipNamePipeMixin( + entity: MixinOptions['entity'] +): PipeMixin { + return factoryMixin(entity, ParseRelationshipNamePipe); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/parse-relationship-name/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/index.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mixin/pipe/parse-relationship-name/index.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/index.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.spec.ts new file mode 100644 index 00000000..2709180b --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.spec.ts @@ -0,0 +1,58 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UnprocessableEntityException } from '@nestjs/common'; +import { ParseRelationshipNamePipe } from './parse-relationship-name.pipe'; +import { CURRENT_ENTITY, CHECK_RELATION_NAME } from '../../../../constants'; +import { EntityTarget } from 'typeorm/common/EntityTarget'; + +describe('CheckItemEntityPipe', () => { + let pipe: ParseRelationshipNamePipe; + let checkRelationNameMock: jest.Mock; + let mockEntityTarget: EntityTarget; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ParseRelationshipNamePipe, + { provide: CURRENT_ENTITY, useValue: {} }, + { provide: CHECK_RELATION_NAME, useValue: jest.fn() }, + ], + }).compile(); + + pipe = module.get>( + ParseRelationshipNamePipe + ); + mockEntityTarget = module.get>(CURRENT_ENTITY); + checkRelationNameMock = module.get(CHECK_RELATION_NAME); + }); + + it('should call findOneRowEntity and return the entity', async () => { + const mockValue = 'name'; + + checkRelationNameMock.mockReturnValueOnce(true); + const result = await pipe.transform(mockValue); + + expect(checkRelationNameMock).toHaveBeenCalledTimes(1); + expect(checkRelationNameMock).toHaveBeenCalledWith( + mockEntityTarget, + mockValue + ); + + expect(result).toBe(mockValue); + }); + + it('should throw a UnprocessableEntityException if no entity is found', async () => { + const mockValue = 'name'; + + checkRelationNameMock.mockReturnValueOnce(false); + + expect(() => pipe.transform(mockValue)).toThrow( + UnprocessableEntityException + ); + + expect(checkRelationNameMock).toHaveBeenCalledTimes(1); + expect(checkRelationNameMock).toHaveBeenCalledWith( + mockEntityTarget, + mockValue + ); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.ts new file mode 100644 index 00000000..de555df3 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.ts @@ -0,0 +1,38 @@ +import { + PipeTransform, + UnprocessableEntityException, + Inject, +} from '@nestjs/common'; +import { EntityRelation } from '../../../../utils/nestjs-shared'; +import { ValidateQueryError } from '../../../../types'; +import { CHECK_RELATION_NAME, CURRENT_ENTITY } from '../../../../constants'; +import { EntityTarget, ObjectLiteral } from '../../../../types'; +import { CheckRelationNme } from '../../types'; +import { getEntityName } from '../../helper'; + +export class ParseRelationshipNamePipe< + E extends ObjectLiteral, + I extends EntityRelation +> implements PipeTransform +{ + @Inject(CURRENT_ENTITY) private currentEntity!: EntityTarget; + @Inject(CHECK_RELATION_NAME) private checkRelationName!: CheckRelationNme; + + transform(value: string): I { + if (!this.checkRelationName || typeof this.checkRelationName !== 'function') + return value as I; + + const result = this.checkRelationName(this.currentEntity, value); + if (!result) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + message: `Relation '${value}' does not exist in resource '${getEntityName( + this.currentEntity + )}'`, + path: [], + }; + throw new UnprocessableEntityException([error]); + } + return value as I; + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-input/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-input/index.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-input/index.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-input/index.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-input/patch-input.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-input/patch-input.pipe.spec.ts new file mode 100644 index 00000000..7e54641f --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-input/patch-input.pipe.spec.ts @@ -0,0 +1,69 @@ +import { ZodError } from 'zod'; +import { Test, TestingModule } from '@nestjs/testing'; +import { + BadRequestException, + InternalServerErrorException, +} from '@nestjs/common'; + +import { PatchInputPipe } from './patch-input.pipe'; +import { PostData, ZodPost } from '../../zod'; +import { JSONValue } from '../../types'; +import { ZOD_PATCH_SCHEMA } from '../../../../constants'; + +type MockEntity = { id: number; name: string }; + +describe('PostInputPipe', () => { + let pipe: PatchInputPipe; + let mockSchema: ZodPost; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + PatchInputPipe, + { + provide: ZOD_PATCH_SCHEMA, + useValue: { + parse: jest.fn(), + }, + }, + ], + }).compile(); + + pipe = module.get>(PatchInputPipe); + mockSchema = module.get>(ZOD_PATCH_SCHEMA); + }); + + it('should transform JSONValue to PostData on success', () => { + const input: JSONValue = { key: 'value' } as any; + const expectedData: PostData = { id: 1, key: 'value' } as any; + + jest + .spyOn(mockSchema, 'parse') + .mockReturnValue({ data: expectedData } as any); + + expect(pipe.transform(input)).toEqual(expectedData); + expect(mockSchema.parse).toHaveBeenCalledWith(input, { + errorMap: expect.any(Function), + }); + }); + + it('should throw BadRequestException if ZodError occurs', () => { + const input: JSONValue = { key: 'value' }; + + jest.spyOn(mockSchema, 'parse').mockImplementation(() => { + throw new ZodError([]); + }); + + expect(() => pipe.transform(input)).toThrow(BadRequestException); + }); + + it('should throw InternalServerErrorException for non-Zod errors', () => { + const input: JSONValue = { key: 'value' }; + + jest.spyOn(mockSchema, 'parse').mockImplementation(() => { + throw new Error('Unexpected Error'); + }); + + expect(() => pipe.transform(input)).toThrow(InternalServerErrorException); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-input/patch-input.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-input/patch-input.pipe.ts similarity index 67% rename from libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-input/patch-input.pipe.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-input/patch-input.pipe.ts index ec524211..3fd108af 100644 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-input/patch-input.pipe.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-input/patch-input.pipe.ts @@ -7,15 +7,16 @@ import { import { ZodError } from 'zod'; import { errorMap } from 'zod-validation-error'; -import { Entity, JSONValue } from '../../../types'; -import { PatchData, ZodInputPatchSchema } from '../../../helper/zod'; -import { ZOD_PATCH_SCHEMA } from '../../../constants'; +import { JSONValue } from '../../types'; +import { PatchData, ZodPatch } from '../../zod'; +import { ZOD_PATCH_SCHEMA } from '../../../../constants'; +import { ObjectLiteral } from '../../../../types'; -export class PatchInputPipe +export class PatchInputPipe implements PipeTransform> { @Inject(ZOD_PATCH_SCHEMA) - private zodInputPatchSchema!: ZodInputPatchSchema; + private zodInputPatchSchema!: ZodPatch; transform(value: JSONValue): PatchData { try { return this.zodInputPatchSchema.parse(value, { diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-relationship/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-relationship/index.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-relationship/index.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-relationship/index.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-relationship/patch-relationship.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-relationship/patch-relationship.pipe.spec.ts similarity index 70% rename from libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-relationship/patch-relationship.pipe.spec.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-relationship/patch-relationship.pipe.spec.ts index b5c13c6f..8c591419 100644 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-relationship/patch-relationship.pipe.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-relationship/patch-relationship.pipe.spec.ts @@ -1,37 +1,22 @@ import { IMemoryDb } from 'pg-mem'; import { Test, TestingModule } from '@nestjs/testing'; -import { getDataSourceToken } from '@nestjs/typeorm'; import { InternalServerErrorException, BadRequestException, } from '@nestjs/common'; -import { CurrentDataSourceProvider } from '../../../factory'; -import { - DEFAULT_CONNECTION_NAME, - ZOD_PATCH_RELATIONSHIP_SCHEMA, -} from '../../../constants'; -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, -} from '../../../mock-utils'; +import { ZOD_PATCH_RELATIONSHIP_SCHEMA } from '../../../../constants'; import { PatchRelationshipPipe } from './patch-relationship.pipe'; -import { ZodInputPostRelationshipSchema } from '../../../helper/zod'; +import { ZodPatchRelationship } from '../../zod'; import { ZodError } from 'zod'; describe('PatchInputPipe', () => { - let db: IMemoryDb; let patchRelationshipPipe: PatchRelationshipPipe; - let zodInputPatchRelationshipSchema: ZodInputPostRelationshipSchema; + let zodInputPatchRelationshipSchema: ZodPatchRelationship; beforeAll(async () => { - db = createAndPullSchemaBase(); const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), { provide: ZOD_PATCH_RELATIONSHIP_SCHEMA, useValue: { @@ -45,8 +30,9 @@ describe('PatchInputPipe', () => { patchRelationshipPipe = module.get( PatchRelationshipPipe ); - zodInputPatchRelationshipSchema = - module.get(ZOD_PATCH_RELATIONSHIP_SCHEMA); + zodInputPatchRelationshipSchema = module.get( + ZOD_PATCH_RELATIONSHIP_SCHEMA + ); }); afterEach(() => { diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-relationship/patch-relationship.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-relationship/patch-relationship.pipe.ts similarity index 70% rename from libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-relationship/patch-relationship.pipe.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-relationship/patch-relationship.pipe.ts index e200d2f5..bf23ae36 100644 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-relationship/patch-relationship.pipe.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-relationship/patch-relationship.pipe.ts @@ -7,18 +7,15 @@ import { import { ZodError } from 'zod'; import { errorMap } from 'zod-validation-error'; -import { JSONValue } from '../../../types'; -import { - PatchRelationshipData, - ZodInputPatchRelationshipSchema, -} from '../../../helper/zod'; -import { ZOD_PATCH_RELATIONSHIP_SCHEMA } from '../../../constants'; +import { JSONValue } from '../../types'; +import { PatchRelationshipData, ZodPatchRelationship } from '../../zod'; +import { ZOD_PATCH_RELATIONSHIP_SCHEMA } from '../../../../constants'; export class PatchRelationshipPipe implements PipeTransform { @Inject(ZOD_PATCH_RELATIONSHIP_SCHEMA) - private zodInputPatchRelationshipSchema!: ZodInputPatchRelationshipSchema; + private zodInputPatchRelationshipSchema!: ZodPatchRelationship; transform(value: JSONValue): PatchRelationshipData { try { return this.zodInputPatchRelationshipSchema.parse(value, { diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-input/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-input/index.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-input/index.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-input/index.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-input/post-input.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-input/post-input.pipe.spec.ts new file mode 100644 index 00000000..01481275 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-input/post-input.pipe.spec.ts @@ -0,0 +1,69 @@ +import { ZodError } from 'zod'; +import { Test, TestingModule } from '@nestjs/testing'; +import { + BadRequestException, + InternalServerErrorException, +} from '@nestjs/common'; + +import { PostInputPipe } from './post-input.pipe'; +import { PostData, ZodPost } from '../../zod'; +import { JSONValue } from '../../types'; +import { ZOD_POST_SCHEMA } from '../../../../constants'; + +type MockEntity = { id: number; name: string }; + +describe('PostInputPipe', () => { + let pipe: PostInputPipe; + let mockSchema: ZodPost; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + PostInputPipe, + { + provide: ZOD_POST_SCHEMA, + useValue: { + parse: jest.fn(), + }, + }, + ], + }).compile(); + + pipe = module.get>(PostInputPipe); + mockSchema = module.get>(ZOD_POST_SCHEMA); + }); + + it('should transform JSONValue to PostData on success', () => { + const input: JSONValue = { key: 'value' } as any; + const expectedData: PostData = { id: 1, key: 'value' } as any; + + jest + .spyOn(mockSchema, 'parse') + .mockReturnValue({ data: expectedData } as any); + + expect(pipe.transform(input)).toEqual(expectedData); + expect(mockSchema.parse).toHaveBeenCalledWith(input, { + errorMap: expect.any(Function), + }); + }); + + it('should throw BadRequestException if ZodError occurs', () => { + const input: JSONValue = { key: 'value' }; + + jest.spyOn(mockSchema, 'parse').mockImplementation(() => { + throw new ZodError([]); + }); + + expect(() => pipe.transform(input)).toThrow(BadRequestException); + }); + + it('should throw InternalServerErrorException for non-Zod errors', () => { + const input: JSONValue = { key: 'value' }; + + jest.spyOn(mockSchema, 'parse').mockImplementation(() => { + throw new Error('Unexpected Error'); + }); + + expect(() => pipe.transform(input)).toThrow(InternalServerErrorException); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-input/post-input.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-input/post-input.pipe.ts similarity index 64% rename from libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-input/post-input.pipe.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-input/post-input.pipe.ts index 2eac0b78..5059b8ce 100644 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-input/post-input.pipe.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-input/post-input.pipe.ts @@ -7,14 +7,15 @@ import { import { ZodError } from 'zod'; import { errorMap } from 'zod-validation-error'; -import { Entity, JSONValue } from '../../../types'; -import { PostData, ZodInputPostSchema } from '../../../helper/zod'; -import { ZOD_POST_SCHEMA } from '../../../constants'; +import { PostData, ZodPost } from '../../zod'; +import { ZOD_POST_SCHEMA } from '../../../../constants'; +import { ObjectLiteral } from '../../../../types'; +import { JSONValue } from '../../types'; -export class PostInputPipe +export class PostInputPipe implements PipeTransform> { - @Inject(ZOD_POST_SCHEMA) private zodInputPostSchema!: ZodInputPostSchema; + @Inject(ZOD_POST_SCHEMA) private zodInputPostSchema!: ZodPost; transform(value: JSONValue): PostData { try { return this.zodInputPostSchema.parse(value, { diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-relationship/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-relationship/index.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-relationship/index.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-relationship/index.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-relationship/post-relationship.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-relationship/post-relationship.pipe.spec.ts similarity index 73% rename from libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-relationship/post-relationship.pipe.spec.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-relationship/post-relationship.pipe.spec.ts index aa1b0c9a..8833734d 100644 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-relationship/post-relationship.pipe.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-relationship/post-relationship.pipe.spec.ts @@ -5,33 +5,18 @@ import { InternalServerErrorException, BadRequestException, } from '@nestjs/common'; -import { CurrentDataSourceProvider } from '../../../factory'; -import { - DEFAULT_CONNECTION_NAME, - ZOD_POST_RELATIONSHIP_SCHEMA, -} from '../../../constants'; - -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, -} from '../../../mock-utils'; +import { ZOD_POST_RELATIONSHIP_SCHEMA } from '../../../../constants'; import { PostRelationshipPipe } from './post-relationship.pipe'; -import { ZodInputPostRelationshipSchema } from '../../../helper/zod'; +import { ZodPostRelationship } from '../../zod'; import { ZodError } from 'zod'; describe('PostInputPipe', () => { - let db: IMemoryDb; let postRelationshipPipe: PostRelationshipPipe; - let zodInputPostRelationshipSchema: ZodInputPostRelationshipSchema; + let zodInputPostRelationshipSchema: ZodPostRelationship; beforeAll(async () => { - db = createAndPullSchemaBase(); const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), { provide: ZOD_POST_RELATIONSHIP_SCHEMA, useValue: { @@ -44,7 +29,7 @@ describe('PostInputPipe', () => { postRelationshipPipe = module.get(PostRelationshipPipe); - zodInputPostRelationshipSchema = module.get( + zodInputPostRelationshipSchema = module.get( ZOD_POST_RELATIONSHIP_SCHEMA ); }); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-relationship/post-relationship.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-relationship/post-relationship.pipe.ts similarity index 70% rename from libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-relationship/post-relationship.pipe.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-relationship/post-relationship.pipe.ts index 73f5a5e4..4a9b5820 100644 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-relationship/post-relationship.pipe.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-relationship/post-relationship.pipe.ts @@ -7,18 +7,15 @@ import { import { ZodError } from 'zod'; import { errorMap } from 'zod-validation-error'; -import { JSONValue } from '../../../types'; -import { - PostRelationshipData, - ZodInputPostRelationshipSchema, -} from '../../../helper/zod'; -import { ZOD_POST_RELATIONSHIP_SCHEMA } from '../../../constants'; +import { JSONValue } from '../../types'; +import { PostRelationshipData, ZodPostRelationship } from '../../zod'; +import { ZOD_POST_RELATIONSHIP_SCHEMA } from '../../../../constants'; export class PostRelationshipPipe implements PipeTransform { @Inject(ZOD_POST_RELATIONSHIP_SCHEMA) - private zodInputPostRelationshipSchema!: ZodInputPostRelationshipSchema; + private zodInputPostRelationshipSchema!: ZodPostRelationship; transform(value: JSONValue): PostRelationshipData { try { return this.zodInputPostRelationshipSchema.parse(value, { diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-check-select-field/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/index.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-check-select-field/index.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/index.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-check-select-field/query-check-select-field.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/query-check-select-field.spec.ts similarity index 83% rename from libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-check-select-field/query-check-select-field.spec.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/query-check-select-field.spec.ts index f2c089d9..161204f6 100644 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-check-select-field/query-check-select-field.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/query-check-select-field.spec.ts @@ -1,13 +1,14 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { BadRequestException } from '@nestjs/common'; +import { QueryField } from '../../../../utils/nestjs-shared'; import { QueryCheckSelectField } from './query-check-select-field'; -import { Users } from '../../../mock-utils'; -import { CONTROL_OPTIONS_TOKEN } from '../../../constants'; -import { Query, QueryField } from '../../../helper/zod'; -import { ConfigParam, Entity } from '../../../types'; -import { BadRequestException } from '@nestjs/common'; +import { Users } from '../../../../mock-utils/typeorm'; +import { CONTROL_OPTIONS_TOKEN } from '../../../../constants'; +import { Query } from '../../zod'; +import { ConfigParam, ObjectLiteral } from '../../../../types'; -function getDefaultQuery() { +function getDefaultQuery() { const filter = { relation: null, target: null, diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-check-select-field/query-check-select-field.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/query-check-select-field.ts similarity index 68% rename from libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-check-select-field/query-check-select-field.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/query-check-select-field.ts index 9e8579c2..7b98707f 100644 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-check-select-field/query-check-select-field.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/query-check-select-field.ts @@ -1,9 +1,13 @@ import { BadRequestException, Inject, PipeTransform } from '@nestjs/common'; -import { CONTROL_OPTIONS_TOKEN } from '../../../constants'; -import { ConfigParam, Entity, ValidateQueryError } from '../../../types'; -import { Query } from '../../../helper'; +import { CONTROL_OPTIONS_TOKEN } from '../../../../constants'; +import { + ConfigParam, + ObjectLiteral, + ValidateQueryError, +} from '../../../../types'; +import { Query } from '../../zod'; -export class QueryCheckSelectField +export class QueryCheckSelectField implements PipeTransform, Query> { @Inject(CONTROL_OPTIONS_TOKEN) private configParam!: ConfigParam; diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-filed-on-include/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/index.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-filed-on-include/index.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/index.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.spec.ts similarity index 94% rename from libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.spec.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.spec.ts index c056f542..71dc33e4 100644 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.spec.ts @@ -1,7 +1,8 @@ import { BadRequestException } from '@nestjs/common'; +import { QueryField } from '../../../../utils/nestjs-shared'; import { QueryFiledInIncludePipe } from './query-filed-in-include.pipe'; -import { Users } from '../../../mock-utils'; -import { Query, QueryField } from '../../../helper'; +import { Users } from '../../../../mock-utils/typeorm'; +import { Query } from '../../zod'; describe('QueryFiledInIncludePipe', () => { let queryFiledInIncludePipe: QueryFiledInIncludePipe; diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.ts similarity index 89% rename from libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.ts index 076cf0c7..8ce987eb 100644 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.ts @@ -1,7 +1,10 @@ import { BadRequestException, PipeTransform } from '@nestjs/common'; -import { Entity, ValidateQueryError } from '../../../types'; -import { ObjectTyped, Query } from '../../../helper'; -export class QueryFiledInIncludePipe +import { ObjectTyped } from '../../../../utils/nestjs-shared'; + +import { ObjectLiteral, ValidateQueryError } from '../../../../types'; +import { Query } from '../../zod'; + +export class QueryFiledInIncludePipe implements PipeTransform, Query> { transform(value: Query): Query { diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-input/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-input/index.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-input/index.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-input/index.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-input/query-input.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-input/query-input.pipe.spec.ts new file mode 100644 index 00000000..c35bebab --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-input/query-input.pipe.spec.ts @@ -0,0 +1,65 @@ +import { QueryInputPipe } from './query-input.pipe'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ZodError } from 'zod'; +import { + BadRequestException, + InternalServerErrorException, +} from '@nestjs/common'; + +import { ZOD_INPUT_QUERY_SCHEMA } from '../../../../constants'; + +class MockZodInputQuery { + parse(value: unknown, options?: unknown) { + return value; + } +} + +describe('QueryInputPipe', () => { + let pipe: QueryInputPipe; + let zodSchemaMock: MockZodInputQuery; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + QueryInputPipe, + { provide: ZOD_INPUT_QUERY_SCHEMA, useClass: MockZodInputQuery }, + ], + }).compile(); + + pipe = module.get>(QueryInputPipe); + zodSchemaMock = module.get(ZOD_INPUT_QUERY_SCHEMA); + }); + + it('should parse the input successfully', () => { + const input = { key: 'value' }; + jest.spyOn(zodSchemaMock, 'parse').mockReturnValue(input); + + const result = pipe.transform(input); + expect(result).toBe(input); + expect(zodSchemaMock.parse).toHaveBeenCalledWith(input, { + errorMap: expect.any(Function), + }); + }); + + it('should throw a BadRequestException when ZodError occurs', () => { + const input = { invalid: 'data' }; + const mockZodError = new ZodError([]); + + jest.spyOn(zodSchemaMock, 'parse').mockImplementation(() => { + throw mockZodError; + }); + + expect(() => pipe.transform(input)).toThrow(BadRequestException); + }); + + it('should throw an InternalServerErrorException for non-ZodError exceptions', () => { + const input = { key: 'value' }; + const mockError = new Error('Unexpected error'); + + jest.spyOn(zodSchemaMock, 'parse').mockImplementation(() => { + throw mockError; + }); + + expect(() => pipe.transform(input)).toThrow(InternalServerErrorException); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-input/query-input.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-input/query-input.pipe.ts similarity index 65% rename from libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-input/query-input.pipe.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-input/query-input.pipe.ts index 9d740098..c1129549 100644 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-input/query-input.pipe.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-input/query-input.pipe.ts @@ -6,15 +6,16 @@ import { } from '@nestjs/common'; import { ZodError } from 'zod'; import { errorMap } from 'zod-validation-error'; -import { ZOD_INPUT_QUERY_SCHEMA } from '../../../constants'; -import { ZodInputQuerySchema, InputQuery } from '../../../helper'; -import { Entity, JSONValue } from '../../../types'; +import { ZOD_INPUT_QUERY_SCHEMA } from '../../../../constants'; +import { ZodInputQuery, InputQuery } from '../../zod'; +import { JSONValue } from '../../types'; +import { ObjectLiteral } from '../../../../types'; -export class QueryInputPipe +export class QueryInputPipe implements PipeTransform> { @Inject(ZOD_INPUT_QUERY_SCHEMA) - private zodInputQuerySchema!: ZodInputQuerySchema; + private zodInputQuerySchema!: ZodInputQuery; transform(value: JSONValue): InputQuery { try { diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/index.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query/index.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/index.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.spec.ts new file mode 100644 index 00000000..6f62baa4 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.spec.ts @@ -0,0 +1,107 @@ +import { + BadRequestException, + InternalServerErrorException, +} from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ZodError } from 'zod'; + +import { QueryPipe } from './query.pipe'; +import { ASC, ZOD_QUERY_SCHEMA } from '../../../../constants'; +import { ZodQuery, InputQuery, Query } from '../../zod'; +import { FilterOperand, QueryField } from '../../../../utils/nestjs-shared'; + +type MockEntity = { id: number; name: string }; + +describe('QueryPipe', () => { + let queryPipe: QueryPipe; + let zodQuerySchemaMock: ZodQuery; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + QueryPipe, + { + provide: ZOD_QUERY_SCHEMA, + useValue: { + parse: jest.fn(), + }, + }, + ], + }).compile(); + + queryPipe = module.get>(QueryPipe); + zodQuerySchemaMock = module.get>(ZOD_QUERY_SCHEMA); + }); + + it('should parse the query successfully using the zod schema', () => { + const inputQuery: InputQuery = { + [QueryField.fields]: { + target: ['id', 'name'], + }, + [QueryField.filter]: { + relation: null, + target: { + id: { + [FilterOperand.eq]: '1', + }, + }, + }, + [QueryField.include]: null, + [QueryField.sort]: { target: { id: ASC } }, + [QueryField.page]: { number: 1, size: 10 }, + }; + const parsedQuery: Query = { + [QueryField.fields]: { + target: ['id', 'name'], + }, + [QueryField.filter]: { + relation: null, + target: { + id: { + eq: '1', + }, + }, + }, + [QueryField.include]: null, + [QueryField.sort]: { target: { id: ASC } }, + [QueryField.page]: { number: 1, size: 10 }, + }; + + jest.spyOn(zodQuerySchemaMock, 'parse').mockReturnValue(parsedQuery); + + const result = queryPipe.transform(inputQuery); + + expect(result).toEqual(parsedQuery); + expect(zodQuerySchemaMock.parse).toHaveBeenCalledWith(inputQuery); + }); + + it('should throw BadRequestException if ZodError is thrown', () => { + const inputQuery = { + id: 1, + name: 'Invalid', + } as unknown as InputQuery; + const zodError = new ZodError([]); + + jest.spyOn(zodQuerySchemaMock, 'parse').mockImplementation(() => { + throw zodError; + }); + + expect(() => queryPipe.transform(inputQuery)).toThrow(BadRequestException); + }); + + it('should throw InternalServerErrorException if an unknown error is thrown', () => { + const inputQuery = { + id: 1, + name: 'Invalid', + } as unknown as InputQuery; + const unexpectedError = new Error('Unexpected error'); + + jest.spyOn(zodQuerySchemaMock, 'parse').mockImplementation(() => { + throw unexpectedError; + }); + + expect(() => queryPipe.transform(inputQuery)).toThrow( + InternalServerErrorException + ); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.ts new file mode 100644 index 00000000..d4c5d359 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.ts @@ -0,0 +1,30 @@ +import { + InternalServerErrorException, + BadRequestException, + Inject, + PipeTransform, +} from '@nestjs/common'; +import { ZodError } from 'zod'; + +import { ZOD_QUERY_SCHEMA } from '../../../../constants'; +import { ZodQuery, Query, InputQuery } from '../../zod'; +import { ObjectLiteral } from '../../../../types'; + +export class QueryPipe + implements PipeTransform, Query> +{ + @Inject(ZOD_QUERY_SCHEMA) + private zodQuerySchema!: ZodQuery; + + transform(value: InputQuery): Query { + try { + return this.zodQuerySchema.parse(value); + } catch (e) { + if (e instanceof ZodError) { + throw new BadRequestException(e.issues); + } + + throw new InternalServerErrorException(e); + } + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/service/json-api-transformer.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/service/json-api-transformer.service.spec.ts new file mode 100644 index 00000000..3b9444f7 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/service/json-api-transformer.service.spec.ts @@ -0,0 +1,674 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ApplicationConfig } from '@nestjs/core'; + +import { JsonApiTransformerService } from './json-api-transformer.service'; +import { + Addresses, + Roles, + Users, + Notes, + Comments, + UserGroups, + dbRandomName, + mockDbPgLiteTestModule, +} from '../../../mock-utils/microrom'; + +import { faker } from '@faker-js/faker'; +import { Collection, MikroORM } from '@mikro-orm/core'; +import { + CurrentEntityManager, + CurrentEntityMetadata, + CurrentMicroOrmProvider, + EntityPropsMap, +} from '../../micro-orm/factory'; +import { + CURRENT_ENTITY, + ENTITY_MAP_PROPS, + GLOBAL_MODULE_OPTIONS_TOKEN, +} from '../../../constants'; +import { DEFAULT_ARRAY_TYPE } from '../../micro-orm/constants'; +import { EntityClass } from '../../../types'; +import { ZodEntityProps } from '../types'; + +describe('JsonApiTransformerService - extractAttributes', () => { + let service: JsonApiTransformerService; + const userObject: Users = {} as Users; + const urlPrefix = 'api'; + const version = '1'; + let mikroORM: MikroORM; + let dbName: string; + let mapProps: Map, ZodEntityProps>; + let mapPropsUser: ZodEntityProps; + beforeAll(async () => { + dbName = dbRandomName(true); + const module: TestingModule = await Test.createTestingModule({ + imports: [mockDbPgLiteTestModule(dbName)], + providers: [ + CurrentMicroOrmProvider(), + CurrentEntityManager(), + CurrentEntityMetadata(), + JsonApiTransformerService, + { + provide: ApplicationConfig, + useValue: { + getGlobalPrefix: () => urlPrefix, + getVersioning: () => ({ + defaultVersion: version, + type: 0, + }), + }, + }, + { + provide: CURRENT_ENTITY, + useValue: Users, + }, + EntityPropsMap([ + Addresses, + Roles, + Users, + Notes, + Comments, + UserGroups, + ] as any), + { + provide: GLOBAL_MODULE_OPTIONS_TOKEN, + useValue: { options: { arrayType: DEFAULT_ARRAY_TYPE } }, + }, + ], + }).compile(); + + service = module.get>( + JsonApiTransformerService + ); + mapProps = module.get(ENTITY_MAP_PROPS); + const mapPropsUserCheck = mapProps.get(Users); + if (!mapPropsUserCheck) throw new Error('Not found map property for Users'); + mapPropsUser = mapPropsUserCheck; + mikroORM = module.get(MikroORM); + }); + + afterAll(() => { + mikroORM.close(true); + }); + + beforeEach(async () => { + userObject.id = faker.number.int(); + userObject.firstName = faker.person.firstName(); + userObject.lastName = faker.person.lastName(); + userObject.isActive = faker.datatype.boolean(); + userObject.login = faker.internet.userName({ + lastName: userObject.lastName, + firstName: userObject.firstName, + }); + userObject.testReal = [faker.number.float({ fractionDigits: 4 })]; + userObject.testArrayNull = null; + + userObject.testDate = faker.date.anytime(); + }); + + describe('extractAttributes', () => { + it('should extract specified fields from an object', () => { + const fields: (keyof Users)[] = ['firstName', 'lastName', 'login']; + const result = service.extractAttributes(userObject, fields); + + expect(result).toEqual({ + firstName: userObject.firstName, + lastName: userObject.lastName, + login: userObject.login, + }); + }); + + it('should return an empty object if no fields match', () => { + const fields = ['nonExistentField'] as any; + const result = service.extractAttributes(userObject, fields); + + expect(result).toEqual({}); + }); + + it('should handle an empty fields array', () => { + const fields: any[] = []; + const result = service.extractAttributes(userObject, fields); + + expect(result).toEqual({}); + }); + + it('should handle an empty input object', () => { + const inputItem = {}; + const fields: any = ['name', 'description']; + const result = service.extractAttributes(inputItem, fields); + + expect(result).toEqual({}); + }); + + it('should not include fields not present in input object', () => { + const fields = ['firstName', 'lastName', 'login', 'description'] as any; + const result = service.extractAttributes(userObject, fields); + + expect(result).toEqual({ + firstName: userObject.firstName, + lastName: userObject.lastName, + login: userObject.login, + }); + }); + }); + describe('transformRelationships', () => { + beforeEach(() => { + userObject.addresses = { + id: faker.number.int(), + city: faker.location.city(), + country: faker.location.country(), + arrayField: [ + faker.string.alphanumeric(5), + faker.string.alphanumeric(5), + ], + state: faker.location.state(), + } as Addresses; + userObject.roles = [ + { + id: faker.number.int(), + key: faker.string.alphanumeric(5), + name: faker.word.words(), + }, + ] as unknown as Collection; + + userObject.manager = null as any; + userObject.notes = [] as any; + }); + + it('should transform relationships without "include" option enabled', () => { + const query = { include: [] } as any; + + const result = service.transformRelationships( + userObject, + mapPropsUser, + query + ); + const checkData = mapPropsUser.relations.reduce((acum, i) => { + acum[i] = { + links: { + self: `/${urlPrefix}/v${version}/${mapPropsUser.typeName}/${userObject.id}/relationships/${i}`, + }, + }; + return acum; + }, {} as Record); + expect(result).toEqual(checkData); + }); + + it('should transform relationships with "include" option enabled', () => { + const query = { include: ['roles'] } as any; + + const result = service.transformRelationships( + userObject, + mapPropsUser, + query + ); + + const checkData = mapPropsUser.relations.reduce((acum, i) => { + acum[i] = { + links: { + self: `/${urlPrefix}/v${version}/${mapPropsUser.typeName}/${userObject.id}/relationships/${i}`, + }, + }; + if (i === 'roles') { + acum[i]['data'] = userObject.roles.map((relName) => ({ + id: relName.id.toString(), + type: mapProps.get(Roles)?.typeName, + })); + } + return acum; + }, {} as Record); + expect(result).toEqual(checkData); + + query.include = ['addresses']; + const result1 = service.transformRelationships( + userObject, + mapPropsUser, + query + ); + const checkData1 = mapPropsUser.relations.reduce((acum, i) => { + acum[i] = { + links: { + self: `/${urlPrefix}/v${version}/${mapPropsUser.typeName}/${userObject.id}/relationships/${i}`, + }, + }; + if (i === 'addresses') { + acum[i]['data'] = { + id: userObject.addresses.id.toString(), + type: mapProps.get(Addresses)?.typeName, + }; + } + return acum; + }, {} as Record); + expect(result1).toEqual(checkData1); + + query.include = ['manager', 'notes']; + const result2 = service.transformRelationships( + userObject, + mapPropsUser, + query + ); + const checkData2 = mapPropsUser.relations.reduce((acum, i) => { + acum[i] = { + links: { + self: `/${urlPrefix}/v${version}/${mapPropsUser.typeName}/${userObject.id}/relationships/${i}`, + }, + }; + if (i === 'manager') { + acum[i]['data'] = null; + } + if (i === 'notes') { + acum[i]['data'] = []; + } + return acum; + }, {} as Record); + + expect(result2).toEqual(checkData2); + }); + + it('should return an empty object for empty relationships array', () => { + const query = { include: [] } as any; + + const result = service.transformRelationships( + userObject, + { ...mapPropsUser, relations: [] as any }, + query + ); + + expect(result).toEqual({}); + }); + }); + describe('extractIncluded', () => { + const roleFake = {} as Roles; + beforeEach(() => { + userObject.addresses = { + id: faker.number.int(), + city: faker.location.city(), + country: faker.location.country(), + arrayField: [ + faker.string.alphanumeric(5), + faker.string.alphanumeric(5), + ], + state: faker.location.state(), + } as Addresses; + roleFake.id = faker.number.int(); + roleFake.key = faker.string.alphanumeric(5); + roleFake.name = faker.word.words(); + + userObject.roles = [roleFake] as unknown as Collection; + + userObject.manager = null as any; + userObject.notes = [] as any; + }); + + it('should by include', () => { + const query = { + include: ['roles', 'addresses', 'manager', 'notes'], + } as any; + + const result = service.extractIncluded([userObject], query); + const rolesInclude = result.find( + (i) => i.type === mapProps.get(Roles)?.typeName + ); + const addressesInclude = result.find( + (i) => i.type === mapProps.get(Addresses)?.typeName + ); + const managerInclude = result.find( + (i) => i.type === mapProps.get(Users)?.typeName + ); + const notesInclude = result.find( + (i) => i.type === mapProps.get(Notes)?.typeName + ); + + expect(notesInclude).toBe(undefined); + expect(managerInclude).toBe(undefined); + + const { id: roleId, ...checkRolesAttr } = roleFake; + expect(rolesInclude).toEqual({ + id: roleId.toString(), + type: mapProps.get(Roles)?.typeName, + attributes: checkRolesAttr, + links: { + self: `/api/v1/${mapProps.get(Roles)?.typeName}/${roleId}`, + }, + relationships: { + users: { + links: { + self: `/api/v${version}/${ + mapProps.get(Roles)?.typeName + }/${roleId}/relationships/users`, + }, + }, + }, + }); + const { id: addressesId, ...addressesAttr } = userObject.addresses; + expect(addressesInclude).toEqual({ + id: addressesId.toString(), + type: mapProps.get(Addresses)?.typeName, + attributes: addressesAttr, + links: { + self: `/api/v1/${mapProps.get(Addresses)?.typeName}/${addressesId}`, + }, + relationships: { + user: { + links: { + self: `/api/v${version}/${ + mapProps.get(Addresses)?.typeName + }/${addressesId}/relationships/user`, + }, + }, + }, + }); + }); + + it('should ne include by custom select', () => { + const userObject = { + id: faker.number.int(), + firstName: faker.person.firstName(), + isActive: faker.datatype.boolean(), + } as any; + userObject.addresses = { + id: faker.number.int(), + city: faker.location.city(), + country: faker.location.country(), + arrayField: [ + faker.string.alphanumeric(5), + faker.string.alphanumeric(5), + ], + state: faker.location.state(), + } as Addresses; + userObject.comments = [ + { + id: faker.number.int(), + text: faker.lorem.text(), + kind: undefined, + createdAt: undefined, + updatedAt: undefined, + createdBy: undefined, + }, + ] as any; + + userObject.manager = { + id: 1, + login: faker.internet.userName({ + lastName: faker.person.lastName(), + firstName: faker.person.firstName(), + }), + firstName: undefined, + testReal: undefined, + testArrayNull: undefined, + lastName: undefined, + isActive: undefined, + testDate: undefined, + createdAt: undefined, + updatedAt: undefined, + addresses: undefined, + manager: undefined, + userGroup: undefined, + }; + + const query = { + include: ['addresses', 'comments', 'manager'], + fields: { + target: ['firstName', 'isActive'], + comments: ['text'], + manager: ['login'], + }, + } as any; + + const result = service.extractIncluded([userObject], query); + + const rolesInclude = result.find( + (i) => i.type === mapProps.get(Roles)?.typeName + ); + const addressesInclude = result.find( + (i) => i.type === mapProps.get(Addresses)?.typeName + ); + const managerInclude = result.find( + (i) => i.type === mapProps.get(Users)?.typeName + ); + const commentsInclude = result.find( + (i) => i.type === mapProps.get(Comments)?.typeName + ); + const notesInclude = result.find( + (i) => i.type === mapProps.get(Notes)?.typeName + ); + + expect(notesInclude).toBe(undefined); + expect(rolesInclude).toBe(undefined); + + const { id: addressesId, ...addressesAttr } = userObject.addresses; + expect(addressesInclude).toEqual({ + id: addressesId.toString(), + type: mapProps.get(Addresses)?.typeName, + attributes: addressesAttr, + links: { + self: `/api/v1/${mapProps.get(Addresses)?.typeName}/${addressesId}`, + }, + relationships: { + user: { + links: { + self: `/api/v${version}/${ + mapProps.get(Addresses)?.typeName + }/${addressesId}/relationships/user`, + }, + }, + }, + }); + + const { id: commentsId } = userObject.comments[0]; + expect(commentsInclude).toEqual({ + id: commentsId.toString(), + type: mapProps.get(Comments)?.typeName, + attributes: query.fields.comments.reduce((acum: any, field: any) => { + acum[field] = userObject.comments[0][field]; + return acum; + }, {}), + links: { + self: `/api/v1/${mapProps.get(Comments)?.typeName}/${commentsId}`, + }, + relationships: { + createdBy: { + links: { + self: `/api/v1/${ + mapProps.get(Comments)?.typeName + }/${commentsId}/relationships/createdBy`, + }, + }, + }, + }); + + const { id: managerId } = userObject.manager; + + expect(managerInclude).toEqual({ + id: managerId.toString(), + type: mapProps.get(Users)?.typeName, + attributes: query.fields.manager.reduce((acum: any, field: any) => { + acum[field] = userObject.manager[field]; + return acum; + }, {}), + links: { + self: `/api/v1/${mapProps.get(Users)?.typeName}/${managerId}`, + }, + relationships: { + addresses: { + links: { + self: `/api/v1/${ + mapProps.get(Users)?.typeName + }/${managerId}/relationships/addresses`, + }, + }, + manager: { + links: { + self: `/api/v1/${ + mapProps.get(Users)?.typeName + }/${managerId}/relationships/manager`, + }, + }, + roles: { + links: { + self: `/api/v1/${ + mapProps.get(Users)?.typeName + }/${managerId}/relationships/roles`, + }, + }, + userGroup: { + links: { + self: `/api/v1/${ + mapProps.get(Users)?.typeName + }/${managerId}/relationships/userGroup`, + }, + }, + comments: { + links: { + self: `/api/v1/${ + mapProps.get(Users)?.typeName + }/${managerId}/relationships/comments`, + }, + }, + notes: { + links: { + self: `/api/v1/${ + mapProps.get(Users)?.typeName + }/${managerId}/relationships/notes`, + }, + }, + }, + }); + }); + }); + describe('transformItem', () => { + it('should transform a single item', () => { + const query = { include: [] } as any; + const result = service.transformItem(userObject, mapPropsUser, query); + + const { id, manager, notes, roles, addresses, ...checkAttr } = userObject; + + expect(result).toEqual({ + id: userObject.id.toString(), + type: mapPropsUser.typeName, + attributes: checkAttr, + links: { + self: `/${urlPrefix}/v${version}/${mapPropsUser.typeName}/${userObject.id}`, + }, + relationships: { + addresses: { + links: { + self: `/api/v1/${mapPropsUser.typeName}/${userObject.id}/relationships/addresses`, + }, + }, + manager: { + links: { + self: `/api/v1/${mapPropsUser.typeName}/${userObject.id}/relationships/manager`, + }, + }, + roles: { + links: { + self: `/api/v1/${mapPropsUser.typeName}/${userObject.id}/relationships/roles`, + }, + }, + userGroup: { + links: { + self: `/api/v1/${mapPropsUser.typeName}/${userObject.id}/relationships/userGroup`, + }, + }, + comments: { + links: { + self: `/api/v1/${mapPropsUser.typeName}/${userObject.id}/relationships/comments`, + }, + }, + notes: { + links: { + self: `/api/v1/${mapPropsUser.typeName}/${userObject.id}/relationships/notes`, + }, + }, + }, + }); + }); + + it('should be transform with custom select field and include is null', () => { + const userObject = { + id: faker.number.int(), + firstName: faker.person.firstName(), + isActive: faker.datatype.boolean(), + } as any; + userObject.addresses = { + id: faker.number.int(), + city: faker.location.city(), + country: faker.location.country(), + arrayField: [ + faker.string.alphanumeric(5), + faker.string.alphanumeric(5), + ], + state: faker.location.state(), + } as Addresses; + userObject.comments = [] as any; + userObject.manager = null as any; + Object.assign(userObject, { + Comments__comments__id: null, + Comments__comments__text: null, + Users__manager__id: null, + Users__manager__login: null, + }); + + const query = { + include: ['addresses', 'comments', 'manager'], + fields: { + target: ['firstName', 'isActive'], + comments: ['text'], + manager: ['login'], + }, + } as any; + + const result = service.transformItem(userObject, mapPropsUser, query); + expect(result).toEqual({ + id: userObject.id.toString(), + type: mapPropsUser.typeName, + attributes: query.fields.target.reduce((acum: any, i: any) => { + acum[i] = userObject[i]; + return acum; + }, {} as any), + links: { + self: `/${urlPrefix}/v${version}/${mapPropsUser.typeName}/${userObject.id}`, + }, + relationships: { + addresses: { + data: { + id: userObject.addresses.id.toString(), + type: mapProps.get(Addresses)?.typeName, + }, + links: { + self: `/api/v1/${mapPropsUser.typeName}/${userObject.id}/relationships/addresses`, + }, + }, + manager: { + data: null, + links: { + self: `/api/v1/${mapPropsUser.typeName}/${userObject.id}/relationships/manager`, + }, + }, + comments: { + data: [], + links: { + self: `/api/v1/${mapPropsUser.typeName}/${userObject.id}/relationships/comments`, + }, + }, + roles: { + links: { + self: `/api/v1/${mapPropsUser.typeName}/${userObject.id}/relationships/roles`, + }, + }, + userGroup: { + links: { + self: `/api/v1/${mapPropsUser.typeName}/${userObject.id}/relationships/userGroup`, + }, + }, + notes: { + links: { + self: `/api/v1/${mapPropsUser.typeName}/${userObject.id}/relationships/notes`, + }, + }, + }, + }); + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/service/json-api-transformer.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/service/json-api-transformer.service.ts new file mode 100644 index 00000000..6cd7bc6b --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/service/json-api-transformer.service.ts @@ -0,0 +1,324 @@ +import { Inject, Injectable, VersioningType } from '@nestjs/common'; +import { ApplicationConfig } from '@nestjs/core'; +import { + Attributes, + camelToKebab, + Data, + DataResult, + EntityRelation, + Include, + MainData, + ObjectTyped, + Relationships, + ResourceData, + ResourceObject, +} from '../../../utils/nestjs-shared'; + +import { EntityClass, ObjectLiteral } from '../../../types'; +import { ENTITY_MAP_PROPS, CURRENT_ENTITY } from '../../../constants'; +import { + EntityProps, + RelationProperty, + ZodEntityProps, + ZodParams, +} from '../types'; +import { Query, QueryOne } from '../zod'; +import { RoutePathFactory } from '@nestjs/core/router/route-path-factory'; + +Injectable(); +export class JsonApiTransformerService { + @Inject(ApplicationConfig) private applicationConfig!: ApplicationConfig; + @Inject(ENTITY_MAP_PROPS) private entityMapProps!: Map< + EntityClass, + ZodEntityProps + >; + @Inject(CURRENT_ENTITY) private currentEntity!: EntityClass; + + private _urlPath!: string[]; + private _currentMapProps!: ZodEntityProps; + + get currentMapProps(): ZodEntityProps { + if (!this._currentMapProps) { + const result = this.entityMapProps.get(this.currentEntity); + if (!result) + throw new Error('Not found map for ' + this.currentEntity.name); + + this._currentMapProps = result; + } + + return this._currentMapProps; + } + + get urlPath() { + if (this._urlPath) return [...this._urlPath]; + this._urlPath = ['']; + const prefix = this.applicationConfig.getGlobalPrefix(); + const version = this.applicationConfig.getVersioning(); + + const routePathFactory = new RoutePathFactory(this.applicationConfig); + + if (prefix) { + this._urlPath.push(this.applicationConfig.getGlobalPrefix()); + } + if (version && version.type === VersioningType.URI) { + const firstVersion = Array.isArray(version.defaultVersion) + ? version.defaultVersion[0] + : version.defaultVersion; + if (firstVersion) { + this._urlPath.push( + `${routePathFactory.getVersionPrefix( + version + )}${firstVersion.toString()}` + ); + } + } + return [...this._urlPath]; + } + + public transformData( + data: E, + query: QueryOne + ): Pick, 'data' | 'included'>; + public transformData( + data: E[], + query: Query + ): Pick, 'data' | 'included'>; + public transformData( + data: E | E[], + query: Query + ): + | Pick, 'data' | 'included'> + | Pick, 'data' | 'included'> { + if (Array.isArray(data)) { + const resultData: Pick< + ResourceObject, + 'data' | 'included' + > = { + data: data.map((item) => + this.transformItem(item, this.currentMapProps, query) + ), + }; + + if (query.include) { + resultData.included = this.extractIncluded(data, query); + } + + return resultData; + } + + const resultData: Pick, 'data' | 'included'> = { + data: this.transformItem(data, this.currentMapProps, query), + }; + + if (query.include) { + resultData.included = this.extractIncluded([data], query); + } + + return resultData; + } + + public transformItem( + item: T, + mapProps: ZodEntityProps, + query: Query + ): ResourceData { + const { fields } = query; + const target = Reflect.get(fields || {}, 'target'); + return { + id: item[mapProps.primaryColumnName].toString(), + type: mapProps.typeName, + attributes: this.extractAttributes( + item, + mapProps.props.filter((i) => { + if (i === mapProps.primaryColumnName) { + return false; + } + if (!target) { + return true; + } + + return (target as string[]).includes(i); + }) + ), + links: { + self: this.getLink(mapProps.typeName, item[mapProps.primaryColumnName]), + }, + relationships: this.transformRelationships(item, mapProps, query), + }; + } + + public transformRel>( + item: E, + rel: Rel + ): DataResult { + const relProps = Reflect.get(this.currentMapProps.relationProperty, rel); + const relationMapPops = this.entityMapProps.get(relProps.entityClass); + if (!relationMapPops) + throw new Error('Not found props map for ' + relProps.entityClass); + const props = item[rel]; + + if (Array.isArray(props)) { + return props.map((i: any) => ({ + type: relationMapPops.typeName, + id: i[relationMapPops.primaryColumnName].toString(), + })); + } else { + return props + ? ({ + type: relationMapPops.typeName, + id: props[relationMapPops.primaryColumnName].toString(), + } as any) + : null; + } + } + + public transformRelationships( + item: T, + mapProps: ZodEntityProps, + query: Query + ): Relationships { + const { include } = query; + + const includeMap = new Map((include || []).map((i) => [i, true])); + + return mapProps.relations.reduce((acum, i: keyof RelationProperty) => { + acum[i as keyof Relationships] = { + links: { + self: this.getLink( + mapProps.typeName, + item[mapProps.primaryColumnName], + 'relationships', + i + ), + }, + }; + + if (includeMap.has(i)) { + const relationMapPops = this.entityMapProps.get( + mapProps.relationProperty[i].entityClass + ); + if (!relationMapPops) + throw new Error( + 'Not found props map for ' + + mapProps.relationProperty[i].entityClass.name + ); + if (mapProps.relationProperty[i].isArray) { + if (item[i] && Array.isArray(item[i]) && item[i].length > 0) { + // @ts-expect-error incorrect parse + acum[i as keyof Relationships]['data'] = item[i].map( + (rel: any) => ({ + id: rel[relationMapPops.primaryColumnName].toString(), + type: relationMapPops.typeName, + }) + ); + } else { + // @ts-expect-error incorrect parse + acum[i as keyof Relationships]['data'] = []; + } + } else { + // @ts-expect-error incorrect parse + acum[i as keyof Relationships]['data'] = item[i] + ? { + id: item[i][relationMapPops.primaryColumnName].toString(), + type: relationMapPops.typeName, + } + : null; + } + } + + return acum; + }, {} as Relationships); + } + + public extractAttributes( + item: T, + fields: (keyof T)[] + ): Attributes { + const mapFields = fields.reduce((acum, field) => { + acum[field.toString()] = true; + return acum; + }, {} as Record); + return ObjectTyped.entries(item).reduce((acum, [name, value]) => { + if (name in mapFields && mapFields[name.toString()]) { + // @ts-expect-error assign key to object entity + acum[name] = value; + } + return acum; + }, {} as Attributes); + } + + public extractIncluded( + data: T[], + query: Query + ): Include[] { + const includeArray: any[] = []; + const { include } = query; + if (!include) return []; + for (const relationPropsFromInclude of include) { + const relationProps = + this.currentMapProps.relationProperty[relationPropsFromInclude]; + if (!relationProps) continue; + const relationMapProp = this.entityMapProps.get( + relationProps.entityClass + ); + if (!relationMapProp) + throw new Error( + 'Not found props for relation ' + + relationPropsFromInclude + + 'in' + + this.currentEntity.name + ); + const { fields } = query; + + const selectFieldForInclude = Reflect.get( + fields || {}, + relationPropsFromInclude + ); + + const queryForInclude = { + ...query, + fields: { + target: + selectFieldForInclude && + Array.isArray(selectFieldForInclude) && + selectFieldForInclude.length > 0 + ? selectFieldForInclude + : null, + }, + include: null, + }; + + for (const dataItem of data) { + const propRel = dataItem[relationPropsFromInclude]; + if (!propRel) continue; + if (Array.isArray(propRel)) { + for (const i of propRel as any) { + includeArray.push( + this.transformItem( + i, + relationMapProp as unknown as ZodEntityProps, + queryForInclude as Query + ) + ); + } + } else { + includeArray.push( + this.transformItem( + propRel, + relationMapProp as unknown as ZodEntityProps, + queryForInclude as Query + ) + ); + } + } + } + + return includeArray; + } + + private getLink(...partOfUrl: string[]) { + const urlPath = this.urlPath; + urlPath.push(...partOfUrl); + return urlPath.join('/'); + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/filter-operand-model.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/filter-operand-model.ts similarity index 96% rename from libs/json-api/json-api-nestjs/src/lib/helper/swagger/filter-operand-model.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/filter-operand-model.ts index bba7eaba..c5ae1563 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/filter-operand-model.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/filter-operand-model.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { FilterOperand as FilterOperandType } from '../../types'; +import { FilterOperand as FilterOperandType } from '../../../utils/nestjs-shared'; const title = 'is equal to the conditional of query'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/index.ts new file mode 100644 index 00000000..fc7c3daa --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/index.ts @@ -0,0 +1 @@ +export * from './swagger-bind.service'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/delete-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/delete-one.ts new file mode 100644 index 00000000..0a1d527f --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/delete-one.ts @@ -0,0 +1,47 @@ +import { Type } from '@nestjs/common'; +import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; + +import { TypeField, ZodEntityProps } from '../../types'; +import { EntityClass, ObjectLiteral } from '../../../../types'; +import { errorSchema, getEntityMapProps } from '../utils'; + +export function deleteOne( + controller: Type, + descriptor: PropertyDescriptor, + entity: EntityClass, + mapEntity: Map, ZodEntityProps>, + methodName: string +) { + const entityName = entity.name; + + const { primaryColumnType } = getEntityMapProps(mapEntity, entity); + + ApiParam({ + name: 'id', + required: true, + type: primaryColumnType === TypeField.number ? 'integer' : 'string', + description: `ID of resource "${entityName}"`, + })(controller, methodName, descriptor); + + ApiOperation({ + summary: `Delete item of resource "${entityName}"`, + operationId: `${controller.constructor.name}_${methodName}`, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 404, + description: `Item of resource "${entityName}" not found`, + schema: errorSchema, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 204, + description: `Item of resource "${entityName}" has been deleted`, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 400, + description: 'Wrong query parameters', + schema: errorSchema, + })(controller, methodName, descriptor); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/delete-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/delete-relationship.ts new file mode 100644 index 00000000..2b1c2b8b --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/delete-relationship.ts @@ -0,0 +1,76 @@ +import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; +import { generateSchema } from '@anatine/zod-openapi'; +import { Type } from '@nestjs/common'; + +import { EntityClass, ObjectLiteral } from '../../../../types'; +import { TypeField, ZodEntityProps } from '../../types'; +import { zodPatchRelationship } from '../../zod'; +import { errorSchema, getEntityMapProps } from '../utils'; + +import { + ReferenceObject, + SchemaObject, +} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; + +export function deleteRelationship( + controller: Type, + descriptor: PropertyDescriptor, + entity: EntityClass, + mapEntity: Map, ZodEntityProps>, + methodName: string +) { + const entityName = entity.name; + + const { relations, primaryColumnType } = getEntityMapProps(mapEntity, entity); + + ApiOperation({ + summary: `Delete list of relation for resource "${entityName}"`, + operationId: `${controller.name}_${methodName}`, + })(controller, methodName, descriptor); + + ApiParam({ + name: 'id', + required: true, + type: primaryColumnType === TypeField.number ? 'integer' : 'string', + description: `ID of resource "${entityName}"`, + })(controller, methodName, descriptor); + + ApiParam({ + name: 'relName', + required: true, + type: 'string', + enum: relations, + description: `Relation name of resource "${entityName}"`, + })(controller, methodName, descriptor); + + ApiBody({ + description: `Json api schema for delete "${entityName}" item`, + schema: generateSchema(zodPatchRelationship) as + | SchemaObject + | ReferenceObject, + required: true, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 400, + description: 'Wrong url parameters', + schema: errorSchema, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 422, + description: 'Incorrect type for relation', + schema: errorSchema, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 404, + description: 'Resource not found ', + schema: errorSchema, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 204, + description: `Item/s of relation for "${entityName}" has been deleted`, + })(controller, methodName, descriptor); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-all.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-all.ts new file mode 100644 index 00000000..772290dd --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-all.ts @@ -0,0 +1,227 @@ +import { ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger'; +import { Type } from '@nestjs/common'; +import { ObjectTyped } from '../../../../utils/nestjs-shared'; + +import { EntityClass, ObjectLiteral } from '../../../../types'; +import { ZodEntityProps } from '../../types'; +import { errorSchema, getEntityMapProps, jsonSchemaResponse } from '../utils'; +import { DEFAULT_PAGE_SIZE, DEFAULT_QUERY_PAGE } from '../../../../constants'; + +export function getAll( + controller: Type, + descriptor: PropertyDescriptor, + entity: EntityClass, + mapEntity: Map, ZodEntityProps>, + methodName: string +): void { + const { props, relations, relationProperty, primaryColumnName } = + getEntityMapProps(mapEntity, entity); + const entityRelationStructure = {} as any; + const relationTree = ObjectTyped.entries(relationProperty).reduce( + (acum, [name, props]) => { + const relMap = getEntityMapProps(mapEntity, props.entityClass); + acum.push(...relMap.props.map((i) => `${name.toLocaleString()}.${i}`)); + entityRelationStructure[name] = relMap.props; + return acum; + }, + [] as string[] + ); + + ApiOperation({ + summary: `Get list items of resource "${entity.name}"`, + operationId: `${controller.constructor.name}_${methodName}`, + servers: undefined, + })(controller, methodName, descriptor); + + ApiQuery({ + name: 'fields', + required: false, + style: 'deepObject', + schema: { + type: 'object', + }, + examples: { + allField: { + summary: 'Select all field', + description: 'Select field for target and relation', + value: { + target: props.join(','), + ...ObjectTyped.entries(entityRelationStructure).reduce( + (acum, [name, props]) => { + acum[name.toString()] = props.join(','); + return acum; + }, + {} as Record + ), + }, + }, + selectOnlyIdsTarget: { + summary: 'Select ids for target', + description: 'Select ids for target', + value: { + target: props.filter((i) => i === primaryColumnName).join(','), + }, + }, + }, + description: `Object of field for select field from "${entity.name}" resource`, + })(controller, methodName, descriptor); + + ApiQuery({ + name: 'filter', + required: false, + style: 'deepObject', + schema: { + type: 'object', + }, + examples: { + simpleExample: { + summary: 'Several conditional', + description: 'Get if relation is not null', + value: { + [props[0]]: { + in: '1,2,3', + }, + [props[1]]: { + lt: '1', + }, + [relationTree[0]]: { + eq: 'test', + }, + }, + }, + relationNull: { + summary: 'Get if relation is null', + description: 'Get if relation is null', + value: { + [relations[0]]: { + eq: null, + }, + }, + }, + relationNotNull: { + summary: 'Get if relation is not null', + description: 'Get if relation is not null', + value: { + [relations[0]]: { + ne: null, + }, + }, + }, + getRelationByConditional: { + summary: 'Get if relation field is', + description: 'Get if relation field is', + value: { + [relationTree[0]]: { + eq: 'test', + }, + }, + }, + }, + description: `Object of filter for select items from "${entity.name}" resource`, + })(controller, methodName, descriptor); + + let sortAscRelation = {}; + let sortDescRelation = {}; + let sortSeveral = { + summary: 'Sort several field', + description: 'Sort several field', + value: `${props[1]},-${props[0]}`, + }; + + if (relations.length > 0) { + ApiQuery({ + name: 'include', + required: false, + enum: relations, + style: 'simple', + isArray: true, + description: `"${entity.name}" resource item has been extended with existing relations`, + examples: { + withInclude: { + summary: 'Add all relation', + description: 'Add all realtion', + value: relations, + }, + without: { + summary: 'Without relation', + description: 'Without all relation', + value: [], + }, + }, + })(controller, methodName, descriptor); + + sortAscRelation = { + summary: 'Sort field relation by ASC', + description: 'Sort field relation by ASC', + value: relationTree[2], + }; + sortDescRelation = { + summary: 'Sort field relation by DESC', + description: 'Sort field relation by DESC', + value: `-${relationTree[2]}`, + }; + sortSeveral = { + summary: 'Sort several field with relation', + description: 'Sort several field relation', + value: `${props[1]},-${relationTree[2]},${relationTree[1]},-${props[0]}`, + }; + } + + ApiQuery({ + name: 'sort', + type: 'string', + required: false, + description: `Params for sorting of "${entity.name}"`, + examples: { + sortAsc: { + summary: 'Sort field by ASC', + description: 'Sort field by ASC', + value: props[1], + }, + sortDesc: { + summary: 'Sort field by DESC', + description: 'Sort field by DESC', + value: `-${props[1]}`, + }, + sortAscRelation, + sortDescRelation, + sortSeveral, + }, + })(controller, methodName, descriptor); + + ApiQuery({ + name: 'page', + style: 'deepObject', + required: false, + schema: { + type: 'object', + properties: { + number: { + type: 'integer', + minimum: 1, + example: DEFAULT_QUERY_PAGE, + }, + size: { + type: 'integer', + minimum: 1, + example: DEFAULT_PAGE_SIZE, + maximum: 500, + }, + }, + additionalProperties: false, + }, + description: `"${entity.name}" resource has been limit and offset with this params.`, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 400, + description: 'Wrong query parameters', + schema: errorSchema, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 200, + description: 'Resource list received successfully', + schema: jsonSchemaResponse(entity, mapEntity, true), + })(controller, methodName, descriptor); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-one.ts new file mode 100644 index 00000000..05fdb3d7 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-one.ts @@ -0,0 +1,119 @@ +import { ApiOperation, ApiParam, ApiQuery, ApiResponse } from '@nestjs/swagger'; +import { ObjectTyped } from '../../../../utils/nestjs-shared'; +import { Type } from '@nestjs/common'; + +import { TypeField, ZodEntityProps } from '../../types'; +import { errorSchema, getEntityMapProps, jsonSchemaResponse } from '../utils'; +import { EntityClass, ObjectLiteral } from '../../../../types'; + +export function getOne( + controller: Type, + descriptor: PropertyDescriptor, + entity: EntityClass, + mapEntity: Map, ZodEntityProps>, + methodName: string +) { + const entityName = entity.name; + + const { + props, + relations, + relationProperty, + primaryColumnName, + primaryColumnType, + } = getEntityMapProps(mapEntity, entity); + + const entityRelationStructure = ObjectTyped.entries(relationProperty).reduce( + (acum, [name, props]) => { + const relMap = getEntityMapProps(mapEntity, props.entityClass); + acum[name] = relMap.props; + return acum; + }, + {} as any + ); + + ApiOperation({ + summary: `Get one item of resource "${entityName}"`, + operationId: `${controller.constructor.name}_${methodName}`, + })(controller, methodName, descriptor); + + ApiParam({ + name: 'id', + required: true, + type: primaryColumnType === TypeField.number ? 'integer' : 'string', + description: `ID of resource "${entityName}"`, + })(controller, methodName, descriptor); + + ApiQuery({ + name: 'fields', + required: false, + style: 'deepObject', + schema: { + type: 'object', + }, + examples: { + allField: { + summary: 'Select all field', + description: 'Select field for target and relation', + value: { + target: props.join(','), + ...ObjectTyped.entries(entityRelationStructure).reduce( + (acum, [name, props]) => { + acum[name.toString()] = props.join(','); + return acum; + }, + {} as Record + ), + }, + }, + selectOnlyIdsTarget: { + summary: 'Select ids for target', + description: 'Select ids for target', + value: { + target: props.filter((i) => i === primaryColumnName).join(','), + }, + }, + }, + description: `Object of field for select field from "${entity.name}" resource`, + })(controller, methodName, descriptor); + + if (relations.length > 0) { + ApiQuery({ + name: 'include', + required: false, + enum: relations, + style: 'simple', + isArray: true, + description: `"${entity.name}" resource item has been extended with existing relations`, + examples: { + withInclude: { + summary: 'Add all relation', + description: 'Add all realtion', + value: relations, + }, + without: { + summary: 'Without relation', + description: 'Without all relation', + value: [], + }, + }, + })(controller, methodName, descriptor); + } + ApiResponse({ + status: 404, + description: `Item of resource "${entityName}" not found`, + schema: errorSchema, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 400, + description: 'Wrong query parameters', + schema: errorSchema, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 200, + description: 'Resource one item received successfully', + schema: jsonSchemaResponse(entity, mapEntity), + })(controller, methodName, descriptor); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-relationship.ts new file mode 100644 index 00000000..ce881025 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-relationship.ts @@ -0,0 +1,66 @@ +import { Type } from '@nestjs/common'; +import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; + +import { EntityClass, ObjectLiteral } from '../../../../types'; +import { TypeField, ZodEntityProps } from '../../types'; +import { + errorSchema, + getEntityMapProps, + schemaTypeForRelation, +} from '../utils'; + +export function getRelationship( + controller: Type, + descriptor: PropertyDescriptor, + entity: EntityClass, + mapEntity: Map, ZodEntityProps>, + methodName: string +) { + const entityName = entity.name; + + const { relations, primaryColumnType } = getEntityMapProps(mapEntity, entity); + + ApiOperation({ + summary: `Get list of relation for resource "${entityName}"`, + operationId: `${controller.constructor.name}_${methodName}`, + })(controller, methodName, descriptor); + + ApiParam({ + name: 'id', + required: true, + type: primaryColumnType === TypeField.number ? 'integer' : 'string', + description: `ID of resource "${entityName}"`, + })(controller, methodName, descriptor); + + ApiParam({ + name: 'relName', + required: true, + type: 'string', + enum: relations, + description: `Relation name of resource "${entityName}"`, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 200, + schema: schemaTypeForRelation, + description: `Item/s of relation for "${entityName}" has been created`, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 400, + description: 'Wrong url parameters', + schema: errorSchema, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 422, + description: 'Incorrect type for relation', + schema: errorSchema, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 404, + description: 'Resource not found ', + schema: errorSchema, + })(controller, methodName, descriptor); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/index.ts new file mode 100644 index 00000000..61a4005f --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/index.ts @@ -0,0 +1,28 @@ +import { getAll } from './get-all'; +import { getOne } from './get-one'; +import { deleteOne } from './delete-one'; +import { postOne } from './post-one'; +import { patchOne } from './patch-one'; +import { getRelationship } from './get-relationship'; +import { deleteRelationship } from './delete-relationship'; +import { postRelationship } from './post-relationship'; +import { patchRelationship } from './patch-relationship'; + +import { OrmService } from '../../types'; +import { ObjectLiteral } from '../../../../types'; + +export const swaggerMethod = { + getAll, + getOne, + deleteOne, + postOne, + patchOne, + getRelationship, + deleteRelationship, + postRelationship, + patchRelationship, +} as const; + +export type SwaggerMethod = { + [Key in keyof OrmService]?: (typeof swaggerMethod)[Key]; +}; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/patch-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/patch-one.ts new file mode 100644 index 00000000..24113795 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/patch-one.ts @@ -0,0 +1,81 @@ +import { Type } from '@nestjs/common'; +import { + ReferenceObject, + SchemaObject, +} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; +import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; +import { generateSchema } from '@anatine/zod-openapi'; + +import { EntityClass, ObjectLiteral } from '../../../../types'; +import { TypeField, ZodEntityProps } from '../../types'; +import { errorSchema, jsonSchemaResponse } from '../utils'; +import { zodPatch } from '../../zod'; +import { getParamsForOatchANdPostZod } from '../../factory'; + +export function patchOne( + controller: Type, + descriptor: PropertyDescriptor, + entity: EntityClass, + mapEntity: Map, ZodEntityProps>, + methodName: string +) { + const entityName = entity.name; + + const { + primaryColumnType, + typeName, + fieldWithType, + propsDb, + primaryColumnName, + relationArrayProps, + relationPopsName, + primaryColumnTypeForRel, + } = getParamsForOatchANdPostZod(mapEntity, entity); + + ApiOperation({ + summary: `Update item of resource "${entityName}"`, + operationId: `${controller.constructor.name}_${methodName}`, + })(controller, methodName, descriptor); + + ApiParam({ + name: 'id', + required: true, + type: primaryColumnType === TypeField.number ? 'integer' : 'string', + description: `ID of resource "${entityName}"`, + })(controller, methodName, descriptor); + + ApiBody({ + description: `Json api schema for update "${entityName}" item`, + schema: generateSchema( + zodPatch( + primaryColumnType, + typeName, + fieldWithType, + propsDb, + primaryColumnName, + relationArrayProps, + relationPopsName, + primaryColumnTypeForRel + ) + ) as SchemaObject | ReferenceObject, + required: true, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 200, + description: `Item of resource "${entityName}" has been updated`, + schema: jsonSchemaResponse(entity, mapEntity), + })(controller, methodName, descriptor); + + ApiResponse({ + status: 400, + description: 'Wrong body parameters', + schema: errorSchema, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 422, + description: 'Unprocessable data', + schema: errorSchema, + })(controller, methodName, descriptor); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/patch-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/patch-relationship.ts new file mode 100644 index 00000000..9301fd62 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/patch-relationship.ts @@ -0,0 +1,80 @@ +import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; +import { + ReferenceObject, + SchemaObject, +} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; +import { generateSchema } from '@anatine/zod-openapi'; +import { Type } from '@nestjs/common'; + +import { EntityClass, ObjectLiteral } from '../../../../types'; +import { TypeField, ZodEntityProps } from '../../types'; +import { + errorSchema, + getEntityMapProps, + schemaTypeForRelation, +} from '../utils'; +import { zodPatchRelationship } from '../../zod'; + +export function patchRelationship( + controller: Type, + descriptor: PropertyDescriptor, + entity: EntityClass, + mapEntity: Map, ZodEntityProps>, + methodName: string +) { + const entityName = entity.name; + + const { relations, primaryColumnType } = getEntityMapProps(mapEntity, entity); + + ApiOperation({ + summary: `Update list of relation for resource "${entityName}"`, + operationId: `${controller.constructor.name}_${methodName}`, + })(controller.prototype, methodName, descriptor); + + ApiParam({ + name: 'id', + required: true, + type: primaryColumnType === TypeField.number ? 'integer' : 'string', + description: `ID of resource "${entityName}"`, + })(controller, methodName, descriptor); + + ApiParam({ + name: 'relName', + required: true, + type: 'string', + enum: relations, + description: `Relation name of resource "${entityName}"`, + })(controller.prototype, methodName, descriptor); + + ApiBody({ + description: `Json api schema for update "${entityName}" item`, + schema: generateSchema(zodPatchRelationship) as + | SchemaObject + | ReferenceObject, + required: true, + })(controller.prototype, methodName, descriptor); + + ApiResponse({ + status: 200, + schema: schemaTypeForRelation, + description: `Item/s of relation for "${entityName}" has been updated`, + })(controller.prototype, methodName, descriptor); + + ApiResponse({ + status: 400, + description: 'Wrong url parameters', + schema: errorSchema, + })(controller.prototype, methodName, descriptor); + + ApiResponse({ + status: 422, + description: 'Incorrect type for relation', + schema: errorSchema, + })(controller.prototype, methodName, descriptor); + + ApiResponse({ + status: 404, + description: 'Resource not found ', + schema: errorSchema, + })(controller.prototype, methodName, descriptor); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/post-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/post-one.ts new file mode 100644 index 00000000..10c35021 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/post-one.ts @@ -0,0 +1,73 @@ +import { ApiBody, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { generateSchema } from '@anatine/zod-openapi'; +import { + ReferenceObject, + SchemaObject, +} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; +import { Type } from '@nestjs/common'; + +import { EntityClass, ObjectLiteral } from '../../../../types'; +import { ZodEntityProps } from '../../types'; +import { errorSchema, jsonSchemaResponse } from '../utils'; +import { zodPost } from '../../zod'; +import { getParamsForOatchANdPostZod } from '../../factory'; + +export function postOne( + controller: Type, + descriptor: PropertyDescriptor, + entity: EntityClass, + mapEntity: Map, ZodEntityProps>, + methodName: string +) { + const entityName = entity.name; + const { + primaryColumnType, + typeName, + fieldWithType, + propsDb, + primaryColumnName, + relationArrayProps, + relationPopsName, + primaryColumnTypeForRel, + } = getParamsForOatchANdPostZod(mapEntity, entity); + + ApiOperation({ + summary: `Create item of resource "${entityName}"`, + operationId: `${controller.constructor.name}_${methodName}`, + })(controller, methodName, descriptor); + + ApiBody({ + description: `Json api schema for new "${entityName}" item`, + schema: generateSchema( + zodPost( + primaryColumnType, + typeName, + fieldWithType, + propsDb, + primaryColumnName, + relationArrayProps, + relationPopsName, + primaryColumnTypeForRel + ) + ) as SchemaObject | ReferenceObject, + required: true, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 201, + description: `Item of resource "${entityName}" has been created`, + schema: jsonSchemaResponse(entity, mapEntity), + })(controller, methodName, descriptor); + + ApiResponse({ + status: 400, + description: 'Wrong body parameters', + schema: errorSchema, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 422, + description: 'Unprocessable data', + schema: errorSchema, + })(controller, methodName, descriptor); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/post-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/post-relationship.ts new file mode 100644 index 00000000..3e9fe208 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/post-relationship.ts @@ -0,0 +1,80 @@ +import { Type } from '@nestjs/common'; +import { generateSchema } from '@anatine/zod-openapi'; +import { + ReferenceObject, + SchemaObject, +} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; +import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; + +import { + errorSchema, + getEntityMapProps, + schemaTypeForRelation, +} from '../utils'; +import { zodPatchRelationship } from '../../zod'; +import { TypeField, ZodEntityProps } from '../../types'; +import { EntityClass, ObjectLiteral } from '../../../../types'; + +export function postRelationship( + controller: Type, + descriptor: PropertyDescriptor, + entity: EntityClass, + mapEntity: Map, ZodEntityProps>, + methodName: string +) { + const entityName = entity.name; + + const { relations, primaryColumnType } = getEntityMapProps(mapEntity, entity); + + ApiParam({ + name: 'id', + required: true, + type: primaryColumnType === TypeField.number ? 'integer' : 'string', + description: `ID of resource "${entityName}"`, + })(controller, methodName, descriptor); + + ApiParam({ + name: 'relName', + required: true, + type: 'string', + enum: relations, + description: `Relation name of resource "${entityName}"`, + })(controller, methodName, descriptor); + + ApiBody({ + description: `Json api schema for update "${entityName}" item`, + schema: generateSchema(zodPatchRelationship) as + | SchemaObject + | ReferenceObject, + required: true, + })(controller, methodName, descriptor); + + ApiOperation({ + summary: `Create list of relation for resource "${entityName}"`, + operationId: `${controller.constructor.name}_${methodName}`, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 200, + schema: schemaTypeForRelation, + description: `Item/s of relation for "${entityName}" has been created`, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 400, + description: 'Wrong url parameters', + schema: errorSchema, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 422, + description: 'Incorrect type for relation', + schema: errorSchema, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 404, + description: 'Resource not found ', + schema: errorSchema, + })(controller, methodName, descriptor); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/swagger-bind.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/swagger-bind.service.ts new file mode 100644 index 00000000..383607b3 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/swagger-bind.service.ts @@ -0,0 +1,114 @@ +import { Injectable, OnModuleInit, Inject } from '@nestjs/common'; +import { DECORATORS } from '@nestjs/swagger/dist/constants'; +import { ApiExtraModels, ApiTags } from '@nestjs/swagger'; +import { DiscoveryService } from '@nestjs/core'; +import { ObjectTyped } from '../../../utils/nestjs-shared'; +import { PARAMTYPES_METADATA } from '@nestjs/common/constants'; + +import { + CONTROL_OPTIONS_TOKEN, + CURRENT_ENTITY, + JSON_API_CONTROLLER_POSTFIX, + JSON_API_DECORATOR_ENTITY, + PARAMS_FOR_ZOD_SCHEMA, + ENTITY_MAP_PROPS, +} from '../../../constants'; +import { getProviderName, nameIt } from '../helper'; +import { JsonBaseController } from '../controller/json-base.controller'; +import { EntityClass, ObjectLiteral } from '../../../types'; +import { + DecoratorOptions, + EntityProps, + ZodEntityProps, + ZodParams, +} from '../types'; +import { FilterOperand } from './filter-operand-model'; +import { createApiModels } from './utils'; +import { Bindings } from '../config/bindings'; + +import { swaggerMethod } from './method'; + +@Injectable() +export class SwaggerBindService + implements OnModuleInit +{ + @Inject(CURRENT_ENTITY) private entity!: EntityClass; + @Inject(DiscoveryService) private discoveryService!: DiscoveryService; + @Inject(CONTROL_OPTIONS_TOKEN) private config!: DecoratorOptions; + + @Inject(ENTITY_MAP_PROPS) private mapEntity!: Map< + EntityClass, + ZodEntityProps + >; + + onModuleInit(): any { + this.initSwagger(); + } + + public initSwagger() { + const controllerName = nameIt( + getProviderName(this.entity.name, JSON_API_CONTROLLER_POSTFIX), + JsonBaseController + ).name; + + const controllerInst = this.discoveryService + .getControllers() + .find( + (i) => + i.name === controllerName || + this.entity === + Reflect.getMetadata( + JSON_API_DECORATOR_ENTITY, + i.instance.constructor + ) + ); + if (!controllerInst) + throw new Error(`Controller for ${this.entity.name} is empty`); + + const mapProps = this.mapEntity.get(this.entity); + if (!mapProps) + throw new Error(`ZodEntityProps for ${this.entity.name} is empty`); + + const controller = controllerInst.instance.constructor; + const apiTag = Reflect.getMetadata(DECORATORS.API_TAGS, controller); + if (!apiTag) { + ApiTags(this.config['overrideRoute'] || this.entity.name)(controller); + } + + ApiTags(this.entity.name)(controller); + + ApiExtraModels(FilterOperand)(controller); + ApiExtraModels(createApiModels(this.entity, mapProps))(controller); + + const { allowMethod = ObjectTyped.keys(Bindings) } = this.config; + for (const method of ObjectTyped.keys(Bindings)) { + if (!allowMethod.includes(method)) continue; + + if (!(method in swaggerMethod)) continue; + + const descriptor = Reflect.getOwnPropertyDescriptor( + controller.prototype, + method + ); + if (!descriptor) + throw new Error( + `Descriptor for entity controller ${this.entity.name} is empty` + ); + + swaggerMethod[method]( + controller.prototype, + descriptor, + this.entity, + this.mapEntity, + method + ); + + Reflect.defineMetadata( + PARAMTYPES_METADATA, + [Object], + controller.prototype, + method + ); + } + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/utils.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/utils.ts similarity index 68% rename from libs/json-api/json-api-nestjs/src/lib/helper/swagger/utils.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/utils.ts index 58b91728..85607ea1 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/utils.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/utils.ts @@ -1,18 +1,13 @@ -import { SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; -import { Type } from '@nestjs/common'; import { ApiProperty } from '@nestjs/swagger'; -import { DeepPartial } from 'typeorm/common/DeepPartial'; -import { Repository } from 'typeorm'; - +import { SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; import { - getField, - getFieldWithType, - TypeField, - getIsArrayRelation, - getRelationTypeName, -} from '../orm'; -import { camelToKebab, nameIt, ObjectTyped } from '../utils'; -import { Entity, EntityRelation } from '../../types'; + ObjectTyped, + EntityRelation, + camelToKebab, +} from '../../../utils/nestjs-shared'; + +import { EntityProps, TypeField, ZodEntityProps, ZodParams } from '../types'; +import { EntityClass, ObjectLiteral } from '../../../types'; export const errorSchema = { type: 'object', @@ -53,30 +48,28 @@ export const errorSchema = { }, }; -export function jsonSchemaResponse( - repository: Repository, +export function jsonSchemaResponse( + entity: EntityClass, + mapEntity: Map, ZodEntityProps>, array = false ) { - const { relations } = getField(repository); - const fieldTypes = getFieldWithType(repository); - const arrayField = getIsArrayRelation(repository); - const relationTypeName = getRelationTypeName(repository); - const primaryColumn = repository.metadata.primaryColumns[0].propertyName; + const { propsType, relations, relationProperty, primaryColumnName } = + getEntityMapProps(mapEntity, entity); const dataType = { type: 'object', properties: { type: { type: 'string', - enum: [camelToKebab(repository.metadata.name)], + const: camelToKebab(entity.name), }, id: { type: 'string', }, attributes: { type: 'object', - properties: ObjectTyped.entries(fieldTypes) - .filter(([name]) => name !== primaryColumn) + properties: ObjectTyped.entries(propsType) + .filter(([name]) => name !== primaryColumnName) .reduce((acum, [name, type]) => { switch (type) { case TypeField.array: @@ -119,9 +112,10 @@ export function jsonSchemaResponse( properties: { type: { type: 'string', - enum: [ - camelToKebab(relationTypeName[name as EntityRelation]), - ], + const: getEntityMapProps( + mapEntity, + Reflect.get(relationProperty, name).entityClass + ).typeName, }, id: { type: 'string', @@ -145,7 +139,7 @@ export function jsonSchemaResponse( }, required: ['self'], }, - data: arrayField[name as EntityRelation] + data: Reflect.get(relationProperty, name).isArray ? dataArray : dataItem, }, @@ -227,55 +221,56 @@ export function jsonSchemaResponse( }; } -export function createApiModels( - repository: Repository -): Type { - const propsType = getFieldWithType(repository); - const relationTypeName = getRelationTypeName(repository); - const relationArray = getIsArrayRelation(repository); +export function createApiModels( + entity: EntityClass, + mapEntity: ZodEntityProps +): EntityClass { + const { propsType, props, relations, propsNullable, relationProperty } = + mapEntity; - const result = repository.create({ - ...propsType, - ...ObjectTyped.entries(relationTypeName).reduce((acum, [name, value]) => { - acum[name.toString()] = relationArray[name] ? [value] : value; - return acum; - }, {} as any), - } as DeepPartial); - - const newEntity = nameIt(repository.metadata.name, class {}) as Type; - for (const [name, value] of Object.entries(result)) { + for (const name of props) { let currentType: any; + let required = false; let isArray = false; - if (name in propsType) { - switch (value) { - case TypeField.array: - currentType = String; - isArray = true; - break; - case TypeField.date: - currentType = Date; - break; - case TypeField.number: - currentType = Number; - break; - case TypeField.boolean: - currentType = Boolean; - break; - default: - currentType = String; - } - } else { - currentType = relationTypeName[name as EntityRelation]; - isArray = Array.isArray(value); + required = !(propsNullable as any).includes(name); + const type = Reflect.get(propsType, name); + isArray = type === 'array'; + switch (type) { + case TypeField.date: + currentType = Date; + break; + case TypeField.number: + currentType = Number; + break; + case TypeField.boolean: + currentType = Boolean; + break; + default: + currentType = String; + } + if (relations.includes(name as string)) { + const propsRel = Reflect.get(relationProperty, name); + currentType = propsRel.entityClass; + isArray = propsRel.isArray; } + ApiProperty({ - required: false, - isArray: isArray, + required, + isArray, type: () => currentType, - })(newEntity.prototype, name); + })(entity.prototype, name.toString()); } - return newEntity; + for (const name of relations) { + const propsRel = Reflect.get(relationProperty, name); + ApiProperty({ + required: !propsRel.nullable, + isArray: propsRel.isArray, + type: propsRel.entityClass, + })(entity.prototype, name.toString()); + } + + return entity; } const dataType = { @@ -304,3 +299,12 @@ export const schemaTypeForRelation = { }, }, }; + +export function getEntityMapProps( + mapEntity: Map, ZodEntityProps>, + entity: EntityClass +) { + const entityMap = mapEntity.get(entity); + if (!entityMap) throw new Error('Entity not found in map'); + return entityMap; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/types/binding.types.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/binding.types.ts similarity index 86% rename from libs/json-api/json-api-nestjs/src/lib/types/binding.types.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/binding.types.ts index 21f6d4fb..eebff2fc 100644 --- a/libs/json-api/json-api-nestjs/src/lib/types/binding.types.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/binding.types.ts @@ -1,17 +1,18 @@ import { PipeTransform, RequestMethod } from '@nestjs/common'; import { Type } from '@nestjs/common/interfaces'; import { PipeFabric } from './module.types'; -import { JsonBaseController } from '../mixin'; +import { JsonBaseController } from '../controller/json-base.controller'; +import { ObjectLiteral } from '../../../types'; export type MethodName = | 'getAll' | 'getOne' + | 'postOne' + | 'patchOne' | 'getRelationship' | 'deleteOne' | 'deleteRelationship' - | 'postOne' | 'postRelationship' - | 'patchOne' | 'patchRelationship'; type MapNameToTypeMethod = { @@ -30,7 +31,7 @@ export interface Binding { path: string; method: MapNameToTypeMethod[T]; name: T; - implementation: JsonBaseController[T]; + implementation: JsonBaseController[T]; parameters: { decorator: ( property?: string, diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/decorator-options.types.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/decorator-options.types.ts new file mode 100644 index 00000000..4911b5dd --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/decorator-options.types.ts @@ -0,0 +1,11 @@ +import { MethodName } from './binding.types'; + +import { RequiredFromPartial, ConfigParam } from '../../../types'; +import { MicroOrmParam } from '../../micro-orm'; +import { TypeOrmParam } from '../../type-orm'; + +export type DecoratorOptions = Partial< + { + allowMethod: Array; + } & RequiredFromPartial +>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/index.ts new file mode 100644 index 00000000..d3876db6 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/index.ts @@ -0,0 +1,6 @@ +export * from './module.types'; +export * from './decorator-options.types'; +export * from './binding.types'; +export * from './utils'; +export * from './zod-types'; +export * from './orm-service.type'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/module.types.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/module.types.ts new file mode 100644 index 00000000..768387c6 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/module.types.ts @@ -0,0 +1,28 @@ +import { + AnyEntity, + EntityName, + NestImport, + NestController, + RequiredFromPartial, + ConfigParam, + PipeMixin, + ExtractNestType, + ResultModuleOptions, +} from '../../../types'; +import { MicroOrmParam } from '../../micro-orm'; +import { TypeOrmParam } from '../../type-orm'; + +type Controller = ExtractNestType; + +export interface MixinOptions { + entity: EntityName; + controller: Controller | undefined; + config: RequiredFromPartial; + imports: NestImport; + ormModule: ResultModuleOptions['type']; +} + +export type PipeFabric = >( + entity: Entity, + config?: MixinOptions['config'] +) => PipeMixin; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/orm-service.type.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/orm-service.type.ts new file mode 100644 index 00000000..546a8303 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/orm-service.type.ts @@ -0,0 +1,55 @@ +import { EntityTarget, ObjectLiteral } from '../../../types'; + +import { + EntityRelation, + ResourceObject, + ResourceObjectRelationships, +} from '../../../utils/nestjs-shared'; +import { + PatchData, + PatchRelationshipData, + PostData, + PostRelationshipData, + Query, + QueryOne, +} from '../zod'; + +export interface OrmService { + getAll(query: Query): Promise>; + getOne(id: number | string, query: QueryOne): Promise>; + deleteOne(id: number | string): Promise; + postOne(inputData: PostData): Promise>; + patchOne( + id: number | string, + inputData: PatchData + ): Promise>; + getRelationship>( + id: number | string, + rel: Rel + ): Promise>; + postRelationship>( + id: number | string, + rel: Rel, + input: PostRelationshipData + ): Promise>; + deleteRelationship>( + id: number | string, + rel: Rel, + input: PostRelationshipData + ): Promise; + patchRelationship>( + id: number | string, + rel: Rel, + input: PatchRelationshipData + ): Promise>; +} + +export type FindOneRowEntity = ( + entity: EntityTarget, + params: number | string +) => Promise; + +export type CheckRelationNme = ( + entity: EntityTarget, + params: string +) => boolean; diff --git a/libs/json-api/json-api-nestjs/src/lib/types/utils.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/utils.ts similarity index 85% rename from libs/json-api/json-api-nestjs/src/lib/types/utils.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/utils.ts index 87021b27..c254ca95 100644 --- a/libs/json-api/json-api-nestjs/src/lib/types/utils.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/utils.ts @@ -1,9 +1,15 @@ import { Type } from '@nestjs/common/interfaces'; -import { EntityField, EntityProps, EntityRelation } from 'json-shared-type'; -import { Entity } from './module.types'; +import { + EntityField, + EntityRelation, + TypeOfArray, + EntityProps, +} from '../../../utils/nestjs-shared'; -export { EntityField, EntityProps, EntityRelation }; +import { ObjectLiteral as Entity } from '../../../types'; + +export { EntityField, EntityProps, EntityRelation, TypeOfArray }; export type EntityPropsArray = { [P in keyof T]: T[P] extends EntityField @@ -34,8 +40,6 @@ export type UnionToTuple = UnionToTupleMain extends readonly [ export type TypeCast = A extends T ? A : never; -export type TypeOfArray = T extends (infer U)[] ? U : T; - export type CastProps = K extends keyof E ? E[K] : never; export type TypeFromType = T extends Type ? A : never; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/zod-types.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/zod-types.ts new file mode 100644 index 00000000..c6298320 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/zod-types.ts @@ -0,0 +1,179 @@ +import { Type } from '@nestjs/common'; +import { z } from 'zod'; + +import { + EntityProps, + EntityRelation, + UnionToTuple, + CastProps, + TypeOfArray, + EntityPropsArray, + IsArray, +} from './utils'; +import { + EntityTarget, + ObjectLiteral as Entity, + ObjectLiteral, +} from '../../../types'; +import { Collection } from '@mikro-orm/core'; + +export enum PropsNameResultField { + field = 'field', + relations = 'relations', +} + +export type ResultGetField = { + [PropsNameResultField.field]: TupleOfEntityProps; + [PropsNameResultField.relations]: TupleOfEntityRelation; +}; + +export type TupleOfEntityProps< + E, + Props = UnionToTuple> +> = Props extends readonly [string, ...string[]] ? Props : never; +export type TupleOfEntityRelation< + E, + Props = UnionToTuple> +> = Props extends readonly [string, ...string[]] ? Props : never; + +export type RelationTree = { + [K in keyof RelationType]: TypeOfArray extends Entity + ? ResultGetField>['field'] + : never; +}; + +export type RelationType = { + [K in EntityRelation]: Type>>; +}; + +export type ZodInfer any> = z.infer>; + +export type GetFieldForEntity = ( + entity: EntityTarget +) => ResultGetField; + +export type ZodParams< + E extends Entity, + P extends EntityProps, + I = string +> = { + entityFieldsStructure: ResultGetField; + entityRelationStructure: RelationTree; + propsArray: ArrayPropsForEntity; + propsType: AllFieldWithType; + typeId: TypeForId; + typeName: I; + fieldWithType: FieldWithType; + propsDb: PropsForField; + primaryColumn: P; + relationArrayProps: RelationPropsArray; + relationPopsName: RelationPropsTypeName; + primaryColumnType: RelationPrimaryColumnType; +}; + +export type PropsArray = { [K in EntityPropsArray]: true }; + +export type ArrayPropsForEntity = { + target: PropsArray; +} & { + [K in ResultGetField['relations'][number]]: PropsArray< + TypeOfArray> + >; +}; + +export enum TypeField { + array = 'array', + date = 'date', + number = 'number', + boolean = 'boolean', + string = 'string', + object = 'object', +} +export type TypeForId = Extract; + +export type FieldWithType = { + [K in EntityProps]: IsArray extends true + ? TypeField.array + : E[K] extends Date + ? TypeField.date + : E[K] extends number + ? TypeField.number + : E[K] extends boolean + ? TypeField.boolean + : E[K] extends object + ? TypeField.object + : TypeField.string; +}; + +export type AllFieldWithType = FieldWithType & { + [K in EntityRelation]: E[K] extends (infer U extends Entity)[] + ? FieldWithType + : E[K] extends Entity + ? FieldWithType + : never; +}; + +export type PropsForField = { + [K in EntityProps]: PropsFieldItem; +} & { + [K in EntityRelation]: PropsFieldItem; +}; + +export type ColumnType = + | T + | typeof Number + | typeof Date + | typeof Boolean; + +export type PropsFieldItem = { + type: ColumnType; + isArray: boolean; + isNullable: boolean; +}; + +export type RelationPropsArray = { + [K in EntityRelation]: E[K] extends unknown[] + ? true + : E[K] extends Collection> + ? true + : false; +}; + +export type RelationPropsTypeName = { + [K in EntityRelation]: string; +}; + +export type RelationPrimaryColumnType = { + [K in EntityRelation]: TypeForId; +}; + +export type FilterNullableProps< + T, + Props extends readonly (keyof T)[] +> = Props extends [infer Head, ...infer Tail] + ? Head extends keyof T + ? null extends T[Head] + ? [Head, ...FilterNullableProps] + : FilterNullableProps + : FilterNullableProps + : []; + +export type RelationProperty = { + [K in TupleOfEntityRelation[number]]: { + entityClass: TypeOfArray>; + nullable: [Extract] extends [never] ? false : true; + isArray: E[K] extends unknown[] ? true : false; + }; +}; + +export type ZodEntityProps = { + props: TupleOfEntityProps; + propsType: FieldWithType; + propsNullable: FilterNullableProps>; + primaryColumnName: I; + primaryColumnType: TypeForId; + typeName: string; + className: string; + relations: TupleOfEntityRelation; + relationProperty: RelationProperty; +}; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/index.ts new file mode 100644 index 00000000..110b963e --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/index.ts @@ -0,0 +1,8 @@ +export * from './zod-input-query-schema'; +export * from './zod-query-schema'; +export * from './zod-input-post-schema'; +export * from './zod-input-patch-schema'; +export * from './zod-input-post-relationship-schema'; +export * from './zod-input-patch-relationship-schema'; + +export { Relationships, Data, Attributes, Id, Type } from './zod-share'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-relationship-schema/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-relationship-schema/index.spec.ts new file mode 100644 index 00000000..0328c8be --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-relationship-schema/index.spec.ts @@ -0,0 +1,42 @@ +import { zodPatchRelationship } from './index'; + +describe('zodPatchRelationship', () => { + const schema = zodPatchRelationship; + + it('should validate an object with nullable data matching zodData', () => { + const validData = { data: { id: '123', type: 'example' } }; + + expect(() => schema.parse(validData)).not.toThrow(); + }); + + it('should validate an object with null as the value of data', () => { + const validData = { data: null }; + + expect(() => schema.parse(validData)).not.toThrow(); + }); + + it('should validate an object with empty array as the value of data', () => { + const validData = { data: [] }; + + expect(() => schema.parse(validData)).not.toThrow(); + }); + + it('should validate an object with an array of objects matching zodData', () => { + const validData = { + data: [ + { id: '123', type: 'example' }, + { id: '456', type: 'example2' }, + ], + }; + expect(() => schema.parse(validData)).not.toThrow(); + }); + + it('should throw an error for extra unknown properties', () => { + const invalidData = { + data: { id: '123', type: 'example' }, + extra: 'invalid', + }; + + expect(() => schema.parse(invalidData)).toThrow(); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-relationship-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-relationship-schema/index.ts new file mode 100644 index 00000000..de334b2d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-relationship-schema/index.ts @@ -0,0 +1,13 @@ +import { z } from 'zod'; + +import { zodData } from '../zod-share'; + +export const zodPatchRelationship = z + .object({ + data: z.union([zodData().nullable(), zodData().array()]), + }) + .strict(); + +export type ZodPatchRelationship = typeof zodPatchRelationship; +export type PatchRelationship = z.infer; +export type PatchRelationshipData = PatchRelationship['data']; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-schema/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-schema/index.spec.ts new file mode 100644 index 00000000..a8b89a19 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-schema/index.spec.ts @@ -0,0 +1,181 @@ +import { + EntityProps, + FieldWithType, + PropsForField, + RelationPrimaryColumnType, + RelationPropsArray, + RelationPropsTypeName, + TypeField, + TypeForId, +} from '../../types'; +import { Users } from '../../../../mock-utils/typeorm'; +import { zodPatch, PatchData } from './'; +import { ZodError } from 'zod'; + +import { + fieldTypeUsers as fieldWithType, + propsDb, + relationArrayProps, + relationPopsName, + primaryColumnType, +} from '../../../../utils/___test___/test.helper'; + +const typeId: TypeForId = TypeField.number; + +const primaryColumn: EntityProps = 'id'; + +const schema = zodPatch( + typeId, + 'users', + fieldWithType, + propsDb, + primaryColumn, + relationArrayProps, + relationPopsName, + primaryColumnType +); + +describe('zodPatch', () => { + it('should be ok', () => { + const real = 123.123; + const date = new Date(); + const attributes = { + login: 'login', + testDate: date.toISOString(), + testReal: [`${real}`], + testArrayNull: null, + }; + const relationships = { + notes: { + data: [ + { + type: 'notes', + id: 'dsfsdf', + }, + ], + }, + }; + + const check = { + data: { + id: '1', + type: 'users', + attributes, + relationships, + }, + }; + const check2 = { + data: { + id: '1', + type: 'users', + attributes, + }, + }; + + const checkResult = { + data: { + id: '1', + type: 'users', + attributes: { + ...attributes, + ['testDate']: date, + testReal: [real], + }, + relationships, + }, + }; + const checkResult2 = { + data: { + id: '1', + type: 'users', + attributes: { + ...attributes, + ['testDate']: date, + testReal: [real], + }, + }, + }; + + const checkResult3 = { + data: { + id: '1', + type: 'users', + }, + }; + + expect(schema.parse(check)).toEqual(checkResult); + expect(schema.parse(check2)).toEqual(checkResult2); + expect(schema.parse(checkResult3)).toEqual(checkResult3); + }); + it('should be not ok', () => { + const check1 = {}; + const check2 = null; + const check3: unknown[] = []; + const check4 = ''; + const check5 = { + sdf: 'sdf', + }; + const check6 = { + data: {}, + }; + const check7 = { + data: { + type: 'users', + }, + }; + const check8 = { + data: { + type: 'users', + attributes: { + lastName: 'sdfsdf', + isActive: true, + }, + relationships: { + notes: [ + { + type: 'sdfsdf', + id: 'dsfsdf', + }, + ], + }, + }, + }; + const check9 = { + data: { + type: 'users', + attributes: { + lastName: 'sdfsdf', + id: 1, + }, + }, + }; + const check10 = { + data: { + type: 'users', + attributes: { + lastName: 'sdfsdf', + }, + }, + }; + const arrayCheck = [ + check1, + check2, + check3, + check4, + check5, + check6, + check7, + check8, + check9, + check10, + ]; + expect.assertions(arrayCheck.length); + for (const item of arrayCheck) { + try { + schema.parse(item); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + } + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-schema/index.ts new file mode 100644 index 00000000..b1444e11 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-schema/index.ts @@ -0,0 +1,111 @@ +import { z, ZodObject, ZodOptional } from 'zod'; +import { + zodAttributes, + ZodAttributes, + zodId, + ZodId, + zodRelationships, + ZodRelationships, + zodType, + ZodType, +} from '../zod-share'; +import { ObjectLiteral } from '../../../../types'; +import { + EntityProps, + FieldWithType, + PropsForField, + RelationPrimaryColumnType, + RelationPropsArray, + RelationPropsTypeName, + TypeForId, +} from '../../types'; +import { ZodPost } from '../zod-input-post-schema'; + +type ZodPatchPatchShape = { + id: ZodId; + type: ZodType; + attributes: ZodOptional>; + relationships: ZodOptional>; +}; + +type ZodInputPatchSchema = ZodObject< + ZodPatchPatchShape, + 'strict' +>; + +type ZodInputPatchDataShape = { + data: ZodInputPatchSchema; +}; + +function getShape( + typeId: TypeForId, + typeName: N, + fieldWithType: FieldWithType, + propsDb: PropsForField, + primaryColumn: EntityProps, + relationArrayProps: RelationPropsArray, + relationPopsName: RelationPropsTypeName, + primaryColumnType: RelationPrimaryColumnType +): ZodInputPatchSchema { + const shape = { + id: zodId(typeId), + type: zodType(typeName), + attributes: zodAttributes( + fieldWithType, + propsDb, + primaryColumn, + true + ).optional(), + relationships: zodRelationships( + relationArrayProps, + relationPopsName, + primaryColumnType, + true + ).optional(), + }; + + return z.object(shape).strict(); +} + +function zodDataShape( + shape: ZodInputPatchSchema +): ZodPatch { + return z + .object({ + data: shape, + }) + .strict(); +} + +export function zodPatch( + typeId: TypeForId, + typeName: N, + fieldWithType: FieldWithType, + propsDb: PropsForField, + primaryColumn: EntityProps, + relationArrayProps: RelationPropsArray, + relationPopsName: RelationPropsTypeName, + primaryColumnType: RelationPrimaryColumnType +): ZodPatch { + const shape = getShape( + typeId, + typeName, + fieldWithType, + propsDb, + primaryColumn, + relationArrayProps, + relationPopsName, + primaryColumnType + ); + + return zodDataShape(shape); +} + +export type ZodPatch = ZodObject< + ZodInputPatchDataShape, + 'strict' +>; +export type PatchData< + E extends ObjectLiteral, + N extends string = string +> = z.infer>['data']; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-relationship-schema/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-relationship-schema/index.spec.ts new file mode 100644 index 00000000..9b084d58 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-relationship-schema/index.spec.ts @@ -0,0 +1,43 @@ +import { zodPostRelationship } from './index'; + +describe('zodPostRelationship', () => { + const schema = zodPostRelationship; + + it('should validate an object with a single valid data item', () => { + const validData = { data: { id: '1', type: 'example' } }; + expect(() => schema.parse(validData)).not.toThrow(); + }); + + it('should validate an object with a non-empty array of valid data items', () => { + const validData = { + data: [ + { id: '1', type: 'example1' }, + { id: '2', type: 'example2' }, + ], + }; + expect(() => schema.parse(validData)).not.toThrow(); + }); + + it('should throw an error when data is an empty array', () => { + const invalidData = { data: [] }; + expect(() => schema.parse(invalidData)).toThrow(); + }); + + it('should throw an error when data is null', () => { + const invalidData = { data: null }; + expect(() => schema.parse(invalidData)).toThrow(); + }); + + it('should throw an error when data is missing', () => { + const invalidData = {}; + expect(() => schema.parse(invalidData)).toThrow(); + }); + + it('should throw an error when additional properties are included', () => { + const invalidData = { + data: { id: '1', type: 'example' }, + extra: 'invalid', + }; + expect(() => schema.parse(invalidData)).toThrow(); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-relationship-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-relationship-schema/index.ts new file mode 100644 index 00000000..d0c245b9 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-relationship-schema/index.ts @@ -0,0 +1,13 @@ +import { z } from 'zod'; + +import { zodData } from '../zod-share'; + +export const zodPostRelationship = z + .object({ + data: z.union([zodData(), zodData().array().nonempty()]), + }) + .strict(); + +export type ZodPostRelationship = typeof zodPostRelationship; +export type PostRelationship = z.infer; +export type PostRelationshipData = PostRelationship['data']; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-schema/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-schema/index.spec.ts new file mode 100644 index 00000000..70e41e5d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-schema/index.spec.ts @@ -0,0 +1,171 @@ +import { EntityProps, TypeField, TypeForId } from '../../types'; +import { Users } from '../../../../mock-utils/typeorm'; +import { zodPost } from './'; +import { ZodError } from 'zod'; + +import { + fieldTypeUsers as fieldWithType, + propsDb, + relationArrayProps, + relationPopsName, + primaryColumnType, +} from '../../../../utils/___test___/test.helper'; + +const typeId: TypeForId = TypeField.number; + +const primaryColumn: EntityProps = 'id'; + +const schema = zodPost( + typeId, + 'users', + fieldWithType, + propsDb, + primaryColumn, + relationArrayProps, + relationPopsName, + primaryColumnType +); + +describe('zodPost', () => { + it('should be ok', () => { + const real = 123.123; + const date = new Date(); + const attributes = { + login: 'login', + lastName: 'sdfsdf', + isActive: true, + testDate: date.toISOString(), + testReal: [`${real}`], + testArrayNull: null, + }; + const relationships = { + notes: { + data: [ + { + type: 'notes', + id: 'dsfsdf', + }, + ], + }, + }; + const check = { + data: { + type: 'users', + attributes, + relationships, + }, + }; + const check2 = { + data: { + type: 'users', + attributes, + }, + }; + const check3 = { + data: { + id: '1', + type: 'users', + attributes, + }, + }; + + const checkResult = { + data: { + type: 'users', + attributes: { + ...attributes, + ['testDate']: date, + testReal: [real], + }, + relationships, + }, + }; + const checkResult2 = { + data: { + type: 'users', + attributes: { + ...attributes, + ['testDate']: date, + testReal: [real], + }, + }, + }; + const checkResult3 = { + data: { + id: '1', + type: 'users', + attributes: { + ...attributes, + ['testDate']: date, + testReal: [real], + }, + }, + }; + + expect(schema.parse(check)).toEqual(checkResult); + expect(schema.parse(check2)).toEqual(checkResult2); + expect(schema.parse(check3)).toEqual(checkResult3); + }); + it('should be not ok', () => { + const check1 = {}; + const check2 = null; + const check3: unknown[] = []; + const check4 = ''; + const check5 = { + sdf: 'sdf', + }; + const check6 = { + data: {}, + }; + const check7 = { + data: { + type: 'users', + }, + }; + const check8 = { + data: { + type: 'users', + attributes: { + lastName: 'sdfsdf', + isActive: true, + }, + relationships: { + notes: [ + { + type: 'sdfsdf', + id: 'dsfsdf', + }, + ], + }, + }, + }; + const check9 = { + data: { + type: 'users', + attributes: { + lastName: 'sdfsdf', + id: 1, + }, + }, + }; + const arrayCheck = [ + check1, + check2, + check3, + check4, + check5, + check6, + check7, + check8, + check9, + ]; + expect.assertions(arrayCheck.length); + for (const item of arrayCheck) { + try { + schema.parse(item); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + } + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-schema/index.ts new file mode 100644 index 00000000..74302679 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-schema/index.ts @@ -0,0 +1,109 @@ +import { z, ZodObject, ZodOptional } from 'zod'; + +import { ObjectLiteral } from '../../../../types'; +import { + ZodId, + zodId, + ZodType, + zodType, + ZodAttributes, + zodAttributes, + ZodRelationships, + zodRelationships, +} from '../zod-share'; +import { + EntityProps, + FieldWithType, + PropsForField, + RelationPrimaryColumnType, + RelationPropsArray, + RelationPropsTypeName, + TypeForId, +} from '../../types'; + +type ZodInputPostShape = { + id: ZodOptional; + type: ZodType; + attributes: ZodAttributes; + relationships: ZodOptional>; +}; + +type ZodInputPostSchema = ZodObject< + ZodInputPostShape, + 'strict' +>; + +type ZodInputPostDataShape = { + data: ZodInputPostSchema; +}; + +function getShape( + typeId: TypeForId, + typeName: N, + fieldWithType: FieldWithType, + propsDb: PropsForField, + primaryColumn: EntityProps, + relationArrayProps: RelationPropsArray, + relationPopsName: RelationPropsTypeName, + primaryColumnType: RelationPrimaryColumnType +): ZodInputPostSchema { + const shape = { + id: zodId(typeId).optional(), + type: zodType(typeName), + attributes: zodAttributes(fieldWithType, propsDb, primaryColumn, false), + relationships: zodRelationships( + relationArrayProps, + relationPopsName, + primaryColumnType, + false + ).optional(), + }; + + return z.object(shape).strict(); +} + +function zodDataShape( + shape: ZodInputPostSchema +): ZodPost { + return z + .object({ + data: shape, + }) + .strict(); +} + +export function zodPost( + typeId: TypeForId, + typeName: N, + fieldWithType: FieldWithType, + propsDb: PropsForField, + primaryColumn: EntityProps, + relationArrayProps: RelationPropsArray, + relationPopsName: RelationPropsTypeName, + primaryColumnType: RelationPrimaryColumnType +): ZodPost { + const shape = getShape( + typeId, + typeName, + fieldWithType, + propsDb, + primaryColumn, + relationArrayProps, + relationPopsName, + primaryColumnType + ); + + return zodDataShape(shape); +} + +export type ZodPost = ZodObject< + ZodInputPostDataShape, + 'strict' +>; +export type Post = z.infer< + ZodPost +>; +export type PostData = Post< + E, + N +>['data']; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/fields.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/fields.spec.ts new file mode 100644 index 00000000..eb999c51 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/fields.spec.ts @@ -0,0 +1,108 @@ +import { zodFieldsInputQuery } from './fields'; +import { ResultGetField } from '../../types'; +import { Users } from '../../../../mock-utils/typeorm'; + +import { userFieldsStructure } from '../../../../utils/___test___/test.helper'; + +const validRelationList: ResultGetField['relations'] = + userFieldsStructure['relations']; + +describe('zodFieldsInputQuerySchema', () => { + const schema = zodFieldsInputQuery(validRelationList); + + it('should validate successfully with a valid target and relation', () => { + const targetInput = 'field1,field2'; + const commentsInput = 'text,createdAt'; + const input = { + target: targetInput, + roles: '', + comments: commentsInput, + }; + const result = schema.safeParse(input); + expect.assertions(4); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toHaveProperty('target', targetInput.split(',')); + expect(result.data).toHaveProperty('comments', commentsInput.split(',')); + expect(result.data).not.toHaveProperty('roles'); + } + }); + + it('should be null result', () => { + const input = { + target: '', + }; + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toBe(null); + } + }); + + it('should throw error if target is missing', () => { + const input = { + posts: ['content'], + }; + + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toContain( + 'Validation error: Fields should be have only props' + ); + } + }); + + it('Not allow null', () => { + const input = { + target: 'inputString', + comments: null, + }; + + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toContain( + 'Expected string, received null' + ); + } + }); + + it('should return null if all fields in input are empty or null', () => { + const input = { + target: undefined, + }; + + expect(schema.parse(input)).toBeNull(); + }); + + it('should throw error if additional fields are present in the input', () => { + const input = { + target: null, + notes: false, + comments: 1, + addresses: ['invalidValue'], // Invalid field + }; + + const result = schema.safeParse(input); + expect.assertions(5); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toContain( + 'Expected string, received null' + ); + expect(result.error.issues[1].message).toContain( + 'Expected string, received boolean' + ); + expect(result.error.issues[2].message).toContain( + 'Expected string, received number' + ); + expect(result.error.issues[3].message).toContain( + 'Expected string, received array' + ); + } + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/fields.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/fields.ts new file mode 100644 index 00000000..93d94c81 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/fields.ts @@ -0,0 +1,59 @@ +import { ObjectTyped } from '../../../../utils/nestjs-shared'; +import { z } from 'zod'; + +import { ObjectLiteral } from '../../../../types'; +import { ResultGetField, ZodInfer } from '../../types'; +import { nonEmptyObject, getValidationErrorForStrict } from '../zod-utils'; + +function getZodRules() { + return z + .string() + .optional() + .transform((input) => (input ? input.split(',') : undefined)); +} + +type ZodRule = ReturnType; + +export function zodFieldsInputQuery( + relationList: ResultGetField['relations'] +) { + const target = z.object({ + target: getZodRules(), + }); + + const relation = relationList.reduce( + (acum, item) => ({ + ...acum, + [item]: getZodRules(), + }), + {} as { + [K in ResultGetField['relations'][number]]: ZodRule; + } + ); + + return target + .merge(z.object(relation)) + .strict(getValidationErrorForStrict(['target', ...relationList], 'Fields')) + .refine(nonEmptyObject(), { + message: 'Validation error: Need select field for target or relation', + }) + .optional() + .transform((input) => { + if (!input) return null; + + const result = ObjectTyped.entries(input).reduce((acum, [key, value]) => { + if (!value || value.length === 0) return acum; + + return { + ...acum, + [key]: value, + }; + }, {} as typeof input); + + return Object.keys(result).length > 0 ? result : null; + }); +} + +export type ZodFieldsInputQuery = ZodInfer< + typeof zodFieldsInputQuery +>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.spec.ts new file mode 100644 index 00000000..1455300a --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.spec.ts @@ -0,0 +1,89 @@ +import { zodFilterInputQuery } from './filter'; +import { Users } from '../../../../mock-utils/typeorm'; +import { ResultGetField } from '../../types'; + +import { + userFieldsStructure, + userRelations, +} from '../../../../utils/___test___/test.helper'; + +const userFields: ResultGetField['field'] = userFieldsStructure['field']; + +describe('zodFilterInputQuery', () => { + it('should return transformed result with relation and target when valid data is provided', () => { + const schema = zodFilterInputQuery(userFields, userRelations); + const input = { + login: { eq: 'johndoe' }, + addresses: { eq: 'null' }, + manager: { eq: null }, + userGroup: { ne: null }, + 'addresses.city': { eq: 'New York' }, + }; + + const result = schema.parse(input); + + expect(result).toEqual({ + relation: { + addresses: { city: { eq: 'New York' } }, + }, + target: { + addresses: { eq: null }, + manager: { eq: null }, + login: { eq: 'johndoe' }, + userGroup: { ne: null }, + }, + }); + }); + + it('should return null relation and target when no data is provided', () => { + const schema = zodFilterInputQuery(userFields, userRelations); + + const result = schema.parse({}); + expect(result).toEqual({ relation: null, target: null }); + }); + + it('should ignore invalid fields and not include them in the result', () => { + const schema = zodFilterInputQuery(userFields, userRelations); + const input = { + invalidField: { eq: 'should be ignored' }, + login: { eq: 'johndoe', gte: '123' }, + }; + + const result = schema.parse(input); + + expect(result).toEqual({ + relation: null, + target: { login: { eq: 'johndoe', gte: '123' } }, + }); + }); + + it('should handle nested relations correctly', () => { + const schema = zodFilterInputQuery(userFields, userRelations); + const input = { + 'manager.firstName': { like: 'Jane' }, + 'manager.lastName': { nin: 'Doe,Jim' }, + }; + + const result = schema.parse(input); + + expect(result).toEqual({ + relation: { + manager: { + firstName: { like: 'Jane' }, + lastName: { nin: ['Doe', 'Jim'] }, + }, + }, + target: null, + }); + }); + + it('should throw a validation error for invalid structures', () => { + const schema = zodFilterInputQuery(userFields, userRelations); + + const invalidInput = { + login: { unknownOperator: 'invalid' }, + }; + + expect(() => schema.parse(invalidInput)).toThrow(); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.ts new file mode 100644 index 00000000..53f69f68 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.ts @@ -0,0 +1,196 @@ +import { + FilterOperand, + ObjectTyped, + isString, +} from '../../../../utils/nestjs-shared'; +import { z } from 'zod'; + +import { + RelationTree, + ResultGetField, + UnionToTuple, + ZodInfer, +} from '../../types'; +import { ObjectLiteral } from '../../../../types'; + +import { oneOf, stringLongerThan } from '../zod-utils'; + +const arrayOp = { + [FilterOperand.in]: true, + [FilterOperand.nin]: true, + [FilterOperand.some]: true, +}; +function convertToFilterObject( + value: Record | string +): Partial<{ + [key in FilterOperand]: string | string[]; +}> { + if (isString | string, string>(value)) { + return { + [FilterOperand.eq]: value, + }; + } else { + return Object.entries(value).reduce((acum, [op, filed]) => { + if (op in arrayOp) { + acum[op] = (isString(filed) ? filed.split(',') : []).filter((i) => !!i); + } else { + acum[op] = filed; + } + return acum; + }, {} as Record); + } +} + +type FilterType = Partial<{ + [key in FilterOperand]: string | string[]; +}>; + +type OutPutFilter = { + relation: null | Record>; + target: null | Record; +}; + +function getZodRulesForRelation() { + return z + .union([ + z + .object({ + [FilterOperand.eq]: z + .union([z.literal('null'), z.null()]) + .transform(() => null), + }) + .strict(), + z + .object({ + [FilterOperand.ne]: z + .union([z.literal('null'), z.null()]) + .transform(() => null), + }) + .strict(), + ]) + .optional(); +} + +function getZodRulesForFilterOperator() { + const filterConditional = z + .union([z.string().refine(stringLongerThan()), z.null(), z.number()]) + .transform((r) => `${r}`); + + const conditional = z + .object( + ObjectTyped.values(FilterOperand).reduce((acum, item) => { + acum[item] = filterConditional; + return acum; + }, {} as Record) + ) + .strict() + .partial() + .refine(oneOf(Object.values(FilterOperand)), { + message: `Must have one of: "${Object.values(FilterOperand).join( + '","' + )}"`, + }); + + return z.union([filterConditional, conditional]).optional(); +} + +function shapeForArray< + R extends readonly [string, ...string[]], + Z extends ZodRulesForRelation | ZodRulesForFilterOperator +>(fields: R, zodSchema: Z) { + return fields.reduce( + (acum, item) => ({ + ...acum, + [item]: zodSchema, + }), + {} as { + [K in R[number]]: Z; + } + ); +} + +function getTupleConcatRelationFields( + relationList: RelationTree +): UnionToTuple< + { + [K in keyof RelationTree]: `${K & string}.${RelationTree[K][number]}`; + }[keyof RelationTree] +> { + const result: string[] = []; + + for (const [key, val] of ObjectTyped.entries(relationList)) { + const relName = key.toString(); + for (const v of val) { + result.push(`${relName}.${v}`); + } + } + + return result as any; +} + +type ZodRulesForRelation = ReturnType; +type ZodRulesForFilterOperator = ReturnType< + typeof getZodRulesForFilterOperator +>; + +export function zodFilterInputQuery( + fields: ResultGetField['field'], + relationList: RelationTree +) { + const target = z.object( + shapeForArray(fields, getZodRulesForFilterOperator()) + ); + + const relationTuple = ObjectTyped.keys(relationList) as UnionToTuple< + keyof RelationTree + >; + + const relations = z.object( + shapeForArray(relationTuple, getZodRulesForRelation()) + ); + + const relationFields = z.object( + shapeForArray( + getTupleConcatRelationFields(relationList), + getZodRulesForFilterOperator() + ) + ); + + return target + .merge(relations) + .merge(relationFields) + .optional() + .transform((data) => { + if (!data) { + return { + relation: null, + target: null, + }; + } + return Object.entries(data).reduce( + (acum, [field, value]: [string, any]) => { + const objectOperand = convertToFilterObject(value); + if (Object.keys(objectOperand).length === 0) { + return acum; + } + + if (field.indexOf('.') > -1) { + const [relation, fieldRelation] = field.split('.'); + acum['relation'] = !acum['relation'] ? {} : acum['relation']; + acum['relation'][relation] = acum['relation'][relation] || {}; + acum['relation'][relation][fieldRelation] = objectOperand; + } else { + acum['target'] = !acum['target'] ? {} : acum['target']; + acum['target'][field] = objectOperand; + } + + return acum; + }, + { relation: null, target: null } as OutPutFilter + ); + }); +} + +export type ZodFilterInputQuery = ZodInfer< + typeof zodFilterInputQuery +>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/include.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/include.spec.ts new file mode 100644 index 00000000..3a202259 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/include.spec.ts @@ -0,0 +1,32 @@ +import { zodIncludeInputQuery } from './include'; + +describe('zodIncludeInputQuerySchema', () => { + const schema = zodIncludeInputQuery(); + + it('should return null when input is undefined', () => { + const result = schema.parse(undefined); + expect(result).toBeNull(); + }); + + it('should return null when input is number, null, boolean, array', () => { + expect(() => schema.parse(123 as any)).toThrow(); + expect(() => schema.parse(null as any)).toThrow(); + expect(() => schema.parse(false as any)).toThrow(); + expect(() => schema.parse([] as any)).toThrow(); + }); + + it('should split a comma-separated string into an array of trimmed values', () => { + const result = schema.parse('a, b ,c , d'); + expect(result).toEqual(['a', 'b', 'c', 'd']); + }); + + it('should filter out empty values from the resulting array', () => { + const result = schema.parse('a, , b, , c'); + expect(result).toEqual(['a', 'b', 'c']); + }); + + it('should return null for an empty string', () => { + const result = schema.parse(''); + expect(result).toBeNull(); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/include.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/include.ts new file mode 100644 index 00000000..b5a72ad8 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/include.ts @@ -0,0 +1,18 @@ +import { z } from 'zod'; +import { isString } from '../../../../utils/nestjs-shared'; +import { ZodInfer } from '../../types'; + +export function zodIncludeInputQuery() { + return z + .string() + .optional() + .transform((data) => { + if (!data || !isString(data)) return null; + return data + .split(',') + .map((i) => i.trim()) + .filter((i) => !!i); + }); +} + +export type ZodIncludeInputQuery = ZodInfer; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.spec.ts new file mode 100644 index 00000000..dc0691c7 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.spec.ts @@ -0,0 +1,98 @@ +import { QueryField, ObjectTyped } from '../../../../utils/nestjs-shared'; +import { zodInputQuery } from './index'; + +import { ResultGetField, TupleOfEntityRelation } from '../../types'; +import { Users } from '../../../../mock-utils/typeorm'; + +import { + userFieldsStructure, + userRelations, +} from '../../../../utils/___test___/test.helper'; + +const userFields: ResultGetField['field'] = userFieldsStructure['field']; + +const entityFieldsStructure = { + field: userFields, + relations: ObjectTyped.keys(userRelations) as TupleOfEntityRelation, +}; + +describe('zodInputQuery', () => { + it('should validate a correct input query object', () => { + const schema = zodInputQuery(entityFieldsStructure, userRelations); + const input = { + [QueryField.fields]: { + target: 'login', + roles: 'name', + manager: 'firstName', + }, + [QueryField.filter]: { id: 1 }, + [QueryField.include]: 'manager,roles', + [QueryField.sort]: 'login', + [QueryField.page]: { number: 1, size: 10 }, + }; + try { + schema.parse(input); + } catch (e) { + console.log(e); + } + + expect(() => schema.parse(input)).not.toThrow(); + }); + + it('should throw an error for an invalid field in the input query', () => { + const schema = zodInputQuery(entityFieldsStructure, userRelations); + const input = { + [QueryField.fields]: ['invalidRelation.invalidField'], + [QueryField.filter]: { id: 1 }, + [QueryField.include]: ['addresses'], + [QueryField.sort]: [{ field: 'login', order: 'asc' }], + [QueryField.page]: { number: 2, size: 5 }, + }; + + expect(() => schema.parse(input)).toThrow(); + }); + + it('should throw an error if an unexpected key is present in the input query', () => { + const schema = zodInputQuery(entityFieldsStructure, userRelations); + const input = { + [QueryField.fields]: ['roles.name'], + [QueryField.filter]: { id: 1 }, + [QueryField.include]: ['roles'], + [QueryField.sort]: [{ field: 'login', order: 'asc' }], + [QueryField.page]: { number: 1, size: 10 }, + unexpectedKey: 'unexpectedValue', + }; + + expect(() => schema.parse(input)).toThrow(); + }); + + it('should throw an error when a required field is missing', () => { + const schema = zodInputQuery(entityFieldsStructure, userRelations); + const input = { + [QueryField.fields]: ['roles.name'], + [QueryField.include]: ['roles'], + [QueryField.sort]: [{ field: 'login', order: 'asc' }], + [QueryField.page]: { number: 1, size: 10 }, + }; + + expect(() => schema.parse(input)).toThrow(); + }); + + it('should validate input with empty but valid fields', () => { + const schema = zodInputQuery(entityFieldsStructure, userRelations); + const input = { + [QueryField.page]: { number: 1, size: 10 }, + }; + + expect(() => schema.parse(input)).not.toThrow(); + const result = schema.parse(input); + + expect(result).toEqual({ + fields: null, + filter: { relation: null, target: null }, + include: null, + sort: null, + page: { size: 10, number: 1 }, + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.ts new file mode 100644 index 00000000..ac58dd4d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.ts @@ -0,0 +1,39 @@ +import { QueryField } from '../../../../utils/nestjs-shared'; +import { z } from 'zod'; +import { RelationTree, ResultGetField } from '../../types'; +import { ObjectLiteral } from '../../../../types'; + +import { zodFieldsInputQuery } from './fields'; +import { zodFilterInputQuery } from './filter'; +import { zodIncludeInputQuery } from './include'; +import { zodSortInputQuery } from './sort'; +import { zodPageInputQuery } from '../zod-share'; + +export function zodInputQuery( + entityFieldsStructure: ResultGetField, + entityRelationStructure: RelationTree +) { + return z + .object({ + [QueryField.fields]: zodFieldsInputQuery( + entityFieldsStructure.relations + ), + [QueryField.filter]: zodFilterInputQuery( + entityFieldsStructure.field, + entityRelationStructure + ), + [QueryField.include]: zodIncludeInputQuery(), + [QueryField.sort]: zodSortInputQuery(), + [QueryField.page]: zodPageInputQuery(), + }) + .strict( + `Query object should contain only allow params: "${Object.keys( + QueryField + ).join('"."')}"` + ); +} + +export type ZodInputQuery = ReturnType< + typeof zodInputQuery +>; +export type InputQuery = z.infer>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/sort.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/sort.spec.ts new file mode 100644 index 00000000..682fa534 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/sort.spec.ts @@ -0,0 +1,56 @@ +import { zodSortInputQuery } from './sort'; +import { ASC, DESC } from '../../../../constants'; + +describe('zodSortInputQuerySchema', () => { + const schema = zodSortInputQuery(); + + it('should transform a single field sort to the correct format', () => { + const result = schema.parse('name'); + expect(result).toEqual({ target: { name: ASC } }); + }); + + it('should transform a descending field sort to the correct format', () => { + const result = schema.parse('-name'); + expect(result).toEqual({ target: { name: DESC } }); + }); + + it('should transform multiple fields sort to the correct format', () => { + const result = schema.parse('name,-age'); + expect(result).toEqual({ target: { name: ASC, age: DESC } }); + }); + + it('should handle nested fields sort properly', () => { + const result = schema.parse('user.name,-user.age'); + expect(result).toEqual({ + user: { name: ASC, age: DESC }, + }); + }); + + it('should ignore empty or invalid inputs between commas', () => { + const result = schema.parse('name,,,-age'); + expect(result).toEqual({ target: { name: ASC, age: DESC } }); + }); + + it('should return null for empty input', () => { + const result = schema.parse(''); + expect(result).toBeNull(); + }); + + it('should trim spaces and process input correctly', () => { + const result = schema.parse(' name , -age '); + expect(result).toEqual({ target: { name: ASC, age: DESC } }); + }); + + it('should handle a mix of nested and non-nested fields', () => { + const result = schema.parse('user.name,-age,user.email'); + expect(result).toEqual({ + user: { name: ASC, email: ASC }, + target: { age: DESC }, + }); + }); + + it('should return null if the input is undefined', () => { + const result = schema.parse(undefined); + expect(result).toBeNull(); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/sort.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/sort.ts new file mode 100644 index 00000000..105fab4e --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/sort.ts @@ -0,0 +1,37 @@ +import { z } from 'zod'; +import { ASC, DESC } from '../../../../constants'; +import { ZodInfer } from '../../types'; + +export function zodSortInputQuery() { + return z + .string() + .optional() + .transform((data) => { + if (!data) return null; + + return data + .split(',') + .map((i) => i.trim()) + .filter((i) => !!i) + .reduce((acum, field) => { + const fieldName = + field.charAt(0) === '-' ? field.substring(1) : field; + const sort = field.charAt(0) === '-' ? DESC : ASC; + if (fieldName.indexOf('.') > -1) { + const [relation, fieldRelation] = field.split('.'); + const relationName = + relation.charAt(0) === '-' ? relation.substring(1) : relation; + + acum[relationName] = acum[relationName] || {}; + acum[relationName][fieldRelation] = sort; + } else { + acum['target'] = acum['target'] || {}; + acum['target'][fieldName] = sort; + } + + return acum; + }, {} as Record>); + }); +} + +export type ZodSortInputQuery = ZodInfer; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.spec.ts new file mode 100644 index 00000000..704960e0 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.spec.ts @@ -0,0 +1,124 @@ +import { zodFieldsQuery } from './fields'; +import { Users } from '../../../../mock-utils/typeorm'; + +import { + userFields, + userRelations, +} from '../../../../utils/___test___/test.helper'; + +const schema = zodFieldsQuery(userFields, userRelations); +describe('zodFieldsQuerySchema', () => { + it('should validate a target field correctly', () => { + const input = { target: ['id'] }; + const result = schema.safeParse(input); + + expect(result.success).toBe(true); + }); + + it('should validate a nested relation field correctly', () => { + const input = { roles: ['isDefault', 'key'] }; + const result = schema.safeParse(input); + + expect(result.success).toBe(true); + }); + + it('should fail validation if input is empty', () => { + const input = {}; + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toBe( + 'Validation error: Select target or relation fields' + ); + } + }); + + it('should fail validation if an invalid target field is provided', () => { + const input = { target: 'invalid_field' }; + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toContain('Expected array'); + } + }); + + it('should fail validation if the relation object contains incorrect fields', () => { + const input = { posts: { invalidField: 'value' } }; + const result = schema.safeParse(input); + + expect(result.success).toBe(false); + }); + + it('should ensure the schema is strict and does not allow extra fields', () => { + const input = { target: ['id'], extraField: 'not_allowed' }; + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toContain( + 'Should be only target of relation' + ); + } + }); + + it('should ensure the schema is strict and does not allow extra fields', () => { + const input = { target: ['id', 'id'] }; + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toContain( + 'Field should be unique' + ); + } + }); + + it('should ensure the schema is strict and does not allow extra fields', () => { + const input = { target1: ['id', 'id'] }; + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toContain( + 'Should be only target of relation' + ); + } + }); + + it('should ensure the schema is strict and does not allow extra fields', () => { + const input = {}; + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toContain( + 'Validation error: Select target or relation fields' + ); + } + }); + it('should ensure the schema is strict and does not allow extra fields', () => { + const input: [] = []; + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toContain('Expected object'); + } + }); + it('should ensure the schema is strict and does not allow extra fields', () => { + const input = null; + const result = schema.safeParse(input); + expect(result.success).toBe(true); + }); + it('should ensure the schema is strict and does not allow extra fields', () => { + const input = ''; + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toContain('Expected object'); + } + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.ts new file mode 100644 index 00000000..5b8f65c8 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.ts @@ -0,0 +1,55 @@ +import { ObjectTyped } from '../../../../utils/nestjs-shared'; +import { z } from 'zod'; + +import { nonEmptyObject, uniqueArray } from '../zod-utils'; +import { ObjectLiteral } from '../../../../types'; +import { ResultGetField, RelationTree } from '../../types'; + +function getZodRules(fields: K) { + return z + .enum(fields) + .array() + .nonempty() + .refine(uniqueArray(), { + message: 'Field should be unique', + }) + .optional(); +} + +type ZodRule = ReturnType< + typeof getZodRules +>; + +type TargetRelationShape = { + [K in keyof RelationTree]: ZodRule[K]>; +}; + +export function zodFieldsQuery( + fields: ResultGetField['field'], + relationList: RelationTree +) { + const target = { + target: getZodRules(fields), + }; + + const relation = {} as TargetRelationShape; + + for (const [key, value] of ObjectTyped.entries(relationList)) { + relation[key] = getZodRules(value); + } + + return z + .object({ + ...target, + ...relation, + }) + .strict('Should be only target of relation') + .refine(nonEmptyObject(), { + message: 'Validation error: Select target or relation fields', + }) + .nullable(); +} +export type ZodFieldsQuery = ReturnType< + typeof zodFieldsQuery +>; +export type FieldsQuery = z.infer>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.spec.ts new file mode 100644 index 00000000..dbc637f8 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.spec.ts @@ -0,0 +1,281 @@ +import { zodFilterQuery } from './filter'; +import { Users } from '../../../../mock-utils/typeorm'; +import { ArrayPropsForEntity } from '../../types'; +import { ZodError } from 'zod'; +import { ZodFilterInputQuery } from '../zod-input-query-schema/filter'; + +import { + userFields, + userRelations, + propsType, +} from '../../../../utils/___test___/test.helper'; + +const propsArray: ArrayPropsForEntity = { + target: { + testArrayNull: true, + testReal: true, + }, + addresses: { + arrayField: true, + }, + userGroup: {}, + manager: { + testArrayNull: true, + testReal: true, + }, + comments: {}, + notes: {}, + roles: {}, +}; + +const schema = zodFilterQuery( + userFields, + userRelations, + propsArray, + propsType +); + +describe('Check "filter" zod schema', () => { + describe('Valid schema', () => { + it('Valid schema - check1', () => { + const check1: ZodFilterInputQuery = { + target: { + id: { + gte: '1213', + ne: '12', + }, + }, + relation: null, + }; + const result = schema.parse(check1); + expect(result).toEqual(check1); + }); + + it('Valid schema - check2', () => { + const check2: ZodFilterInputQuery = { + target: { + id: { + gte: '1213', + }, + login: { + lt: 'sdfs', + }, + }, + relation: { + addresses: { + arrayField: { + some: ['sdfsdf', 'sdfsdf'], + }, + }, + }, + }; + const result = schema.parse(check2); + expect(result).toEqual(check2); + }); + + it('Valid schema - check3', () => { + const check3: ZodFilterInputQuery = { + target: null, + relation: null, + }; + const result = schema.parse(check3); + expect(result).toEqual(check3); + }); + + it('Valid schema - check4', () => { + const check4: ZodFilterInputQuery = { + target: null, + relation: { + comments: { + id: { + lte: '123', + }, + }, + manager: { + firstName: { + eq: 'sdfsdfsdf', + }, + }, + }, + }; + const result = schema.parse(check4); + expect(result).toEqual(check4); + }); + + it('Valid schema - check5', () => { + const check5: ZodFilterInputQuery = { + target: null, + relation: { + comments: { + id: { + in: ['1'], + }, + }, + manager: { + firstName: { + eq: 'sdfsdfsdf', + }, + }, + }, + }; + const result = schema.parse(check5); + expect(result).toEqual(check5); + }); + + it('Valid schema - check6', () => { + const check6: ZodFilterInputQuery = { + target: { + id: { + gte: '1213', + ne: '123', + }, + addresses: { + eq: 'null', + }, + }, + relation: null, + }; + const result = schema.parse(check6); + expect(result).toEqual(check6); + }); + + it('Valid schema - check7', () => { + const check7: ZodFilterInputQuery = { + target: { + isActive: { + eq: 'true', + }, + }, + relation: null, + }; + const result = schema.parse(check7); + expect(result).toEqual(check7); + }); + + it('Valid schema - check8', () => { + const check8: ZodFilterInputQuery = { + target: { + createdAt: { + eq: '2023-12-08T09:40:58.020Z', + }, + }, + relation: null, + }; + const result = schema.parse(check8); + expect(result).toEqual(check8); + }); + + it('Valid schema - check9', () => { + const check9: ZodFilterInputQuery = { + target: { + createdAt: { + eq: 'null', + }, + }, + relation: null, + }; + const result = schema.parse(check9); + expect(result.target!.createdAt!.eq).toEqual(null); + result.target!.createdAt!.eq = 'null'; + expect(result).toEqual(check9); + }); + + it('Valid schema - check10', () => { + const check: ZodFilterInputQuery = { + target: { + id: { + gte: '1213', + ne: '123', + }, + addresses: { + eq: null as any, + }, + }, + relation: null, + }; + const result = schema.parse(check); + expect(result).toEqual({ + ...check, + target: { + ...check.target, + addresses: { + eq: 'null', + }, + }, + }); + }); + }); + + describe('Invalid schema', () => { + it('Invalid schema - check1', () => { + const check1 = null; + expect(() => schema.parse(check1)).toThrow(ZodError); + }); + + it('Invalid schema - check2', () => { + const check2 = {}; + expect(() => schema.parse(check2)).toThrow(ZodError); + }); + + it('Invalid schema - check3', () => { + const check3 = ''; + expect(() => schema.parse(check3)).toThrow(ZodError); + }); + + it('Invalid schema - check4', () => { + const check4 = 1; + expect(() => schema.parse(check4)).toThrow(ZodError); + }); + + it('Invalid schema - check5', () => { + const check5: any[] = []; + expect(() => schema.parse(check5)).toThrow(ZodError); + }); + + it('Invalid schema - check6', () => { + const check6 = { + target: null, + }; + expect(() => schema.parse(check6)).toThrow(ZodError); + }); + + it('Invalid schema - check7', () => { + const check7 = { + target: null, + relation: { + commentsasda: { + id: { + lte: 'sdfsdf', + }, + }, + manager: { + firstName: { + eq: 'sdfsdfsdf', + }, + }, + }, + }; + expect(() => schema.parse(check7)).toThrow(ZodError); + }); + + it('Invalid schema - check8', () => { + const check8 = { + target: null, + relation: { + comment: { + id: { + lte: 'sdfsdf', + }, + }, + manager: { + firstName: { + eq: 'sdfsdfsdf', + }, + }, + sdfsdf: {}, + }, + }; + expect(() => schema.parse(check8)).toThrow(ZodError); + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.ts new file mode 100644 index 00000000..8253787c --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.ts @@ -0,0 +1,245 @@ +import { z, ZodOptional } from 'zod'; +import { + FilterOperand, + ObjectTyped, + FilterOperandOnlyInNin, + FilterOperandOnlySimple, +} from '../../../../utils/nestjs-shared'; + +import { + AllFieldWithType, + ArrayPropsForEntity, + RelationTree, + ResultGetField, + IsArray, + TypeField, + EntityProps, + TypeOfArray, + CastProps, + TypeCast, + PropsArray, +} from '../../types'; +import { ObjectLiteral } from '../../../../types'; +import { + stringLongerThan, + arrayItemStringLongerThan, + stringMustBe, + elementOfArrayMustBe, + oneOf, + guardIsKeyOfObject, + nonEmptyObject, +} from '../zod-utils'; + +const zodRuleForString = z.union([ + z.literal('null').transform(() => null), + z.string().refine(stringLongerThan(), { + message: 'String should be not empty', + }), +]); + +const zodRuleStringArray = zodRuleForString + .array() + .nonempty() + .refine(arrayItemStringLongerThan(0), { + message: 'Array should be not empty', + }); + +const zodNullRule = z.union([ + z.literal('null'), + z.literal(null).transform((r) => 'null' as const), +]); + +const zodRuleFilterRelationSchema = z.union([ + z + .object({ + [FilterOperand.eq]: zodNullRule, + }) + .strict(), + z + .object({ + [FilterOperand.ne]: zodNullRule, + }) + .strict(), +]); +const zodRuleForArrayField = z + .object({ [FilterOperand.some]: zodRuleStringArray }) + .strict(); + +function getZodRulesForField(type: TypeField = TypeField.string) { + const simpleShape = ObjectTyped.entries(FilterOperandOnlySimple).reduce( + (acum, [key, val]) => ({ + ...acum, + [val]: zodRuleForString + .refine(stringMustBe(type), { + message: `String should be as ${type}`, + }) + .optional(), + }), + {} as { + [K in FilterOperandOnlySimple]: ZodOptional; + } + ); + + const ninInShape = ObjectTyped.entries(FilterOperandOnlyInNin).reduce( + (acum, [key, val]) => ({ + ...acum, + [val]: zodRuleStringArray + .refine(elementOfArrayMustBe(type), { + message: `String should be as ${type}`, + }) + .optional(), + }), + {} as { + [K in FilterOperandOnlyInNin]: ZodOptional; + } + ); + return z + .object({ + ...simpleShape, + ...ninInShape, + }) + .strict() + .refine( + oneOf( + Object.values(FilterOperand).filter((i) => i !== FilterOperand.some) + ), + { + message: `Must have one of: "${Object.values(FilterOperand) + .filter((i) => i !== FilterOperand.some) + .join('","')}"`, + } + ); +} + +function getFilterPropsShapeForEntity( + fields: ResultGetField['field'], + propsArrayTarget: PropsArray, + propsType: AllFieldWithType +) { + return fields.reduce( + (acum, field) => ({ + ...acum, + [field]: (Reflect.get(propsArrayTarget, field) + ? zodRuleForArrayField + : getZodRulesForField(propsType[field as EntityProps]) + ).optional(), + }), + {} as FilterProps['field']> + ); +} + +function getZodRulesForRelationShape( + shape: FilterProps['field']> +) { + return z.object(shape).strict().optional().refine(nonEmptyObject()); +} + +type ZodRuleForString = typeof zodRuleForString; +type ZodRuleStringArray = typeof zodRuleStringArray; +type ZodRuleFilterRelationSchema = typeof zodRuleFilterRelationSchema; +type ZodRuleForArrayField = typeof zodRuleForArrayField; +type ZodRulesForField = ReturnType; +type ZodRulesForRelationShape = ReturnType< + typeof getZodRulesForRelationShape +>; + +type FilterProps< + E extends ObjectLiteral, + P extends readonly [string, ...string[]] +> = { + [Props in P[number]]: Props extends keyof E + ? IsArray extends true + ? ZodOptional + : ZodOptional + : never; +}; + +type RelationType = TypeCast< + TypeOfArray>, + ObjectLiteral +>; + +type RelationFilterProps = { + [R in keyof RelationTree]: ZodRulesForRelationShape>; +}; + +type TargetRelationShape = { + [K in ResultGetField['relations'][number]]: ZodOptional; +}; + +export function zodFilterQuery( + fields: ResultGetField['field'], + relationTree: RelationTree, + propsArray: ArrayPropsForEntity, + propsType: AllFieldWithType +) { + const { target: propsArrayTarget, ...otherRelationPropsArray } = propsArray; + + const fieldsFilterProps = getFilterPropsShapeForEntity( + fields, + propsArrayTarget, + propsType + ); + + const targetRelation = ObjectTyped.keys(relationTree).reduce( + (acum, item) => ({ + ...acum, + [item]: zodRuleFilterRelationSchema.optional(), + }), + {} as TargetRelationShape + ); + + const relationFilterProps = ObjectTyped.keys(relationTree).reduce( + (acum, name) => { + type F = typeof name; + type RT = RelationType; + type RTF = ResultGetField['field']; + + guardIsKeyOfObject(otherRelationPropsArray, name); + const relationField = relationTree[name] as RTF; + const relationPropsArray = otherRelationPropsArray[ + name + ] as PropsArray; + const relationPropsType = propsType[name] as AllFieldWithType; + + const filterProps = getFilterPropsShapeForEntity( + relationField, + relationPropsArray, + relationPropsType + ); + + const zodFilter = getZodRulesForRelationShape(filterProps); + + return { + ...acum, + [name]: zodFilter.optional(), + }; + }, + {} as RelationFilterProps + ); + + const targetShapeFilter = { + target: z + .object({ + ...fieldsFilterProps, + ...targetRelation, + }) + .strict() + .optional() + .refine(nonEmptyObject()) + .nullable(), + relation: z + .object(relationFilterProps) + .strict() + .optional() + .refine(nonEmptyObject()) + .nullable(), + }; + + return z.object(targetShapeFilter).strict().refine(nonEmptyObject()); +} + +export type ZodFilterQuery = ReturnType< + typeof zodFilterQuery +>; +export type FilterQuery = z.infer>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/include.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/include.spec.ts new file mode 100644 index 00000000..97d00ecd --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/include.spec.ts @@ -0,0 +1,47 @@ +import { zodIncludeQuery } from './include'; + +import { Users } from '../../../../mock-utils/typeorm'; + +import { relationList } from '../../../../utils/___test___/test.helper'; + +const schema = zodIncludeQuery(relationList); + +describe('zodIncludeQuery', () => { + it('should validate an array of relations successfully', () => { + const result = schema.parse(['comments', 'addresses']); + + expect(result).toEqual(['comments', 'addresses']); + }); + + it('should return null if input is null', () => { + const result = schema.parse(null); + + expect(result).toBeNull(); + }); + + it('should throw an error if the array is empty', () => { + expect(() => schema.parse([])).toThrowError( + expect.objectContaining({ + message: expect.stringContaining( + 'Array must contain at least 1 element' + ), + }) + ); + }); + + it('should throw an error if the array has duplicate entries', () => { + expect(() => schema.parse(['addresses', 'addresses'])).toThrowError( + expect.objectContaining({ + message: expect.stringContaining('Include should have unique relation'), + }) + ); + }); + + it('should throw an error if the input contains invalid relations', () => { + expect(() => schema.parse(['invalid_relation'])).toThrowError( + expect.objectContaining({ + message: expect.stringContaining('Invalid enum value. Expected'), + }) + ); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/include.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/include.ts new file mode 100644 index 00000000..cf8e87a0 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/include.ts @@ -0,0 +1,22 @@ +import { z } from 'zod'; +import { ObjectLiteral } from '../../../../types'; +import { ResultGetField } from '../../types'; +import { uniqueArray } from '../zod-utils'; + +export function zodIncludeQuery( + relationList: ResultGetField['relations'] +) { + return z + .enum(relationList) + .array() + .nonempty() + .refine(uniqueArray(), { + message: 'Include should have unique relation', + }) + .nullable(); +} + +export type ZodIncludeQuery = ReturnType< + typeof zodIncludeQuery +>; +export type IncludeQuery = z.infer>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.spec.ts new file mode 100644 index 00000000..93ca9fc3 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.spec.ts @@ -0,0 +1,128 @@ +import { FilterOperand, QueryField } from '../../../../utils/nestjs-shared'; +import { zodQuery } from './index'; +import { ArrayPropsForEntity } from '../../types'; +import { Users } from '../../../../mock-utils/typeorm'; +import { InputQuery } from '../zod-input-query-schema'; +import { ASC } from '../../../../constants'; + +import { + userFieldsStructure as userFields, + userRelations, + propsType, +} from '../../../../utils/___test___/test.helper'; + +const propsArray: ArrayPropsForEntity = { + target: { + testArrayNull: true, + testReal: true, + }, + addresses: { + arrayField: true, + }, + userGroup: {}, + manager: { + testArrayNull: true, + testReal: true, + }, + comments: {}, + notes: {}, + roles: {}, +}; + +const schemaQuery = zodQuery( + userFields, + userRelations, + propsArray, + propsType +); + +describe('schemaQuery.parse', () => { + it('should successfully parse valid input', () => { + const validInput: InputQuery = { + [QueryField.fields]: { + target: [ + 'id', + 'login', + 'firstName', + 'lastName', + 'createdAt', + 'updatedAt', + ], + }, + [QueryField.filter]: { relation: null, target: null }, + [QueryField.include]: ['roles'], + [QueryField.sort]: { target: { id: ASC } }, + [QueryField.page]: { number: 1, size: 10 }, + }; + + expect(() => schemaQuery.parse(validInput)).not.toThrow(); + const result = schemaQuery.parse(validInput); + expect(result).toEqual(validInput); + }); + + it('should throw an error for invalid field values', () => { + const invalidInput = { + field: ['invalidField'], // Field not defined in userFields + relations: { + comments: ['text', 'id'], + }, + }; + + expect(() => schemaQuery.parse(invalidInput)).toThrow(); + }); + + it('should throw an error if a required relation field is missing', () => { + const invalidInput = { + field: ['updatedAt', 'createdAt', 'login'], + relations: {}, // Missing required relations + }; + + expect(() => schemaQuery.parse(invalidInput)).toThrowError(/invalid_type/i); + }); + + it('should handle nested relations', () => { + const validInput: InputQuery = { + [QueryField.fields]: { + target: [ + 'id', + 'login', + 'firstName', + 'lastName', + 'createdAt', + 'updatedAt', + ], + }, + [QueryField.filter]: { + relation: null, + target: { + id: { + [FilterOperand.in]: ['1', '2', '3'], + }, + }, + }, + [QueryField.include]: ['roles'], + [QueryField.sort]: { target: { id: ASC } }, + [QueryField.page]: { number: 1, size: 10 }, + }; + + expect(() => schemaQuery.parse(validInput)).not.toThrow(); + const result = schemaQuery.parse(validInput); + expect(result).toEqual(validInput); + }); + + it('should throw an error for invalid nested relations', () => { + const invalidInput = { + field: ['id', 'login'], + relations: { + manager: { + field: ['id', 'nonExistentField'], + relations: { + roles: ['id', 'name'], + }, + }, + }, + }; + + expect(() => schemaQuery.parse(invalidInput)).toThrow(); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.ts new file mode 100644 index 00000000..9483f211 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.ts @@ -0,0 +1,99 @@ +import { QueryField } from '../../../../utils/nestjs-shared'; +import { z, ZodObject } from 'zod'; + +import { ObjectLiteral } from '../../../../types'; +import { + AllFieldWithType, + ArrayPropsForEntity, + RelationTree, + ResultGetField, +} from '../../types'; + +import { zodFieldsQuery, ZodFieldsQuery } from './fields'; +import { zodFilterQuery, ZodFilterQuery } from './filter'; +import { zodSortQuery, ZodSortQuery } from './sort'; +import { zodIncludeQuery, ZodIncludeQuery } from './include'; +import { zodPageInputQuery, ZodPageInputQuery } from '../zod-share'; + +type Shape = { + [QueryField.fields]: ZodFieldsQuery; + [QueryField.filter]: ZodFilterQuery; + [QueryField.include]: ZodIncludeQuery; + [QueryField.sort]: ZodSortQuery; + [QueryField.page]: ZodPageInputQuery; +}; + +function getShape( + entityFieldsStructure: ResultGetField, + entityRelationStructure: RelationTree, + propsArray: ArrayPropsForEntity, + propsType: AllFieldWithType +): Shape { + return { + [QueryField.fields]: zodFieldsQuery( + entityFieldsStructure.field, + entityRelationStructure + ), + [QueryField.filter]: zodFilterQuery( + entityFieldsStructure.field, + entityRelationStructure, + propsArray, + propsType + ), + [QueryField.include]: zodIncludeQuery(entityFieldsStructure.relations), + [QueryField.sort]: zodSortQuery( + entityFieldsStructure.field, + entityRelationStructure + ), + [QueryField.page]: zodPageInputQuery(), + }; +} + +function getZodResultSchema( + shape: Shape +): ZodObject, 'strict'> { + return z.object(shape).strict(); +} + +export function zodQuery( + entityFieldsStructure: ResultGetField, + entityRelationStructure: RelationTree, + propsArray: ArrayPropsForEntity, + propsType: AllFieldWithType +): ZodResultSchema { + const shape = getShape( + entityFieldsStructure, + entityRelationStructure, + propsArray, + propsType + ); + return getZodResultSchema(shape); +} + +export type ZodResultSchema = ReturnType< + typeof getZodResultSchema +>; +export type ZodQuery = ReturnType>; +export type Query = z.infer>; + +function zodQueryOne( + entityFieldsStructure: ResultGetField, + entityRelationStructure: RelationTree, + propsArray: ArrayPropsForEntity, + propsType: AllFieldWithType +): ZodObject, QueryField.fields | QueryField.include>, 'strict'> { + return z + .object({ + [QueryField.fields]: zodFieldsQuery( + entityFieldsStructure.field, + entityRelationStructure + ), + [QueryField.include]: zodIncludeQuery(entityFieldsStructure.relations), + }) + .strict(); +} + +export type ZodQueryOne = ReturnType< + typeof zodQueryOne +>; +export type QueryOne = z.infer>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/sort.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/sort.spec.ts new file mode 100644 index 00000000..53ede399 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/sort.spec.ts @@ -0,0 +1,89 @@ +import { zodSortQuery } from './sort'; + +import { ASC, DESC } from '../../../../constants'; + +import { + userFields, + userRelations, +} from '../../../../utils/___test___/test.helper'; + +const schema = zodSortQuery(userFields, userRelations); +describe('zodSortQuery', () => { + it('should create a Zod schema with target and relations', () => { + const parsedData = schema.parse({ + target: { id: ASC }, + addresses: { country: DESC }, + manager: { lastName: ASC }, + roles: { name: DESC }, + comments: { kind: DESC }, + notes: { text: ASC }, + userGroup: { label: ASC }, + }); + + expect(parsedData).toEqual({ + target: { id: ASC }, + addresses: { country: DESC }, + manager: { lastName: ASC }, + roles: { name: DESC }, + comments: { kind: DESC }, + notes: { text: ASC }, + userGroup: { label: ASC }, + }); + }); + + it('should throw an error for an invalid field in target', () => { + const schema = zodSortQuery(userFields, userRelations); + + expect(() => { + schema.parse({ + target: 'invalid_value', + }); + }).toThrowError(); + }); + + it('should throw an error for invalid fields in relations', () => { + const schema = zodSortQuery(userFields, userRelations); + + expect(() => { + schema.parse({ + addresses: 'invalid_value', + }); + }).toThrowError(); + }); + + it('should allow partial relations and target', () => { + const schema = zodSortQuery(userFields, userRelations); + + const parsedData = schema.parse({ + target: { id: ASC }, + addresses: { country: DESC }, + }); + + expect(parsedData).toEqual({ + target: { id: ASC }, + addresses: { country: DESC }, + }); + }); + + it('should fail if an empty object is not allowed', () => { + const schema = zodSortQuery(userFields, userRelations); + + expect(() => { + schema.parse({}); + }).toThrowError(); + }); + it('null should be valid', () => { + expect(schema.parse(null)).toBe(null); + }); + + it('should fail if the input is not a valid object', () => { + const schema = zodSortQuery(userFields, userRelations); + + expect(() => { + schema.parse([]); + }).toThrowError(); + expect(() => { + schema.parse('invalid'); + }).toThrowError(); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/sort.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/sort.ts new file mode 100644 index 00000000..7951749e --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/sort.ts @@ -0,0 +1,60 @@ +import { ObjectTyped } from '../../../../utils/nestjs-shared'; +import { z } from 'zod'; + +import { RelationTree, ResultGetField } from '../../types'; +import { ObjectLiteral } from '../../../../types'; +import { SORT_TYPE } from '../../../../constants'; +import { nonEmptyObject } from '../zod-utils'; + +function getZodSortRule() { + return z.enum(SORT_TYPE).optional(); +} + +function getZodFieldRule(fields: F) { + const targetShape = fields.reduce( + (acum, item) => ({ + ...acum, + [item]: getZodSortRule(), + }), + {} as { [K in F[number]]: ZodSortRule } + ); + + return z.object(targetShape).strict().refine(nonEmptyObject()).optional(); +} + +type ZodSortRule = ReturnType; +type ZodFieldRule = ReturnType< + typeof getZodFieldRule +>; + +export function zodSortQuery( + fields: ResultGetField['field'], + relationList: RelationTree +) { + const zodRelationShape = {} as { + [K in keyof RelationTree]: ZodFieldRule[K]>; + }; + const zodTargetShape: { target: ZodFieldRule['field']> } = { + target: getZodFieldRule(fields), + }; + + for (const [key, val] of ObjectTyped.entries(relationList)) { + if (key === 'target') continue; + zodRelationShape[key] = getZodFieldRule(val); + } + + return z + .object({ + ...zodTargetShape, + ...zodRelationShape, + }) + .strict() + .partial() + .refine(nonEmptyObject()) + .nullable(); +} + +export type ZodSortQuery = ReturnType< + typeof zodSortQuery +>; +export type SortQuery = z.infer>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.spec.ts new file mode 100644 index 00000000..7011ef35 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.spec.ts @@ -0,0 +1,172 @@ +import { ZodError } from 'zod'; +import { zodAttributes, ZodAttributes, Attributes } from './attributes'; +import { Addresses, Users } from '../../../../mock-utils/typeorm'; +import { PropsForField } from '../../types'; + +import { + fieldTypeUsers, + propsDb, + fieldTypeAddresses, +} from '../../../../utils/___test___/test.helper'; + +describe('attributes', () => { + type SchemaTypeUsers = Attributes; + type SchemaTypeAddresses = Attributes; + + describe('Attributes for post', () => { + let schemaUsers: ZodAttributes; + let schemaAddresses: ZodAttributes; + beforeEach(() => { + schemaUsers = zodAttributes(fieldTypeUsers, propsDb, 'id', false); + schemaAddresses = zodAttributes( + fieldTypeAddresses, + {} as PropsForField, + 'id', + false + ); + }); + + it('should be ok', () => { + const date = new Date(); + const check: SchemaTypeUsers = { + login: 'login', + isActive: true, + lastName: 'sdsdf', + testReal: [123.123, 123.123], + testArrayNull: [], + testDate: date.toISOString() as any, + }; + + const check2: SchemaTypeAddresses = { + arrayField: ['test', 'test'], + state: 'state', + country: 'country', + city: 'city', + createdAt: date.toISOString() as any, + updatedAt: date.toISOString() as any, + }; + + const check3: SchemaTypeUsers = { + login: 'login', + isActive: true, + lastName: 'sdsdf', + testReal: [123.123, 123.123], + testArrayNull: null as any, + testDate: date.toISOString() as any, + }; + + expect(schemaUsers.parse(check)).toEqual({ + ...check, + testDate: date, + }); + expect(schemaAddresses.parse(check2)).toEqual({ + ...check2, + createdAt: date, + updatedAt: date, + }); + + expect(schemaUsers.parse(check3)).toEqual({ + ...check3, + testDate: date, + }); + }); + + it('should be not ok', () => { + const check = { + id: '1', + isActive: 'true', + lastName: 1, + }; + const check2 = { + arrayField: 'test', + }; + expect.assertions(2); + try { + expect(schemaUsers.parse(check)).toEqual(check); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + try { + schemaAddresses.parse(check2); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + }); + }); + + describe('Attributes for patch', () => { + let schemaUsers: ZodAttributes; + let schemaAddresses: ZodAttributes; + beforeEach(() => { + schemaUsers = zodAttributes( + fieldTypeUsers, + propsDb, + 'id', + true + ); + schemaAddresses = zodAttributes( + fieldTypeAddresses, + {} as PropsForField, + 'id', + true + ); + }); + + it('should be ok', () => { + const date = new Date(); + const check: SchemaTypeUsers = { + login: 'login', + isActive: true, + testDate: date.toISOString() as any, + }; + + const check2: SchemaTypeAddresses = { + arrayField: ['test', 'test'], + state: 'state', + createdAt: date.toISOString() as any, + updatedAt: date.toISOString() as any, + }; + + const check3: SchemaTypeUsers = { + testReal: [123.123, 123.123], + testArrayNull: null as any, + testDate: date.toISOString() as any, + }; + + expect(schemaUsers.parse(check)).toEqual({ + ...check, + testDate: date, + }); + expect(schemaAddresses.parse(check2)).toEqual({ + ...check2, + createdAt: date, + updatedAt: date, + }); + + expect(schemaUsers.parse(check3)).toEqual({ + ...check3, + testDate: date, + }); + }); + + it('should be not ok', () => { + const check = { + id: '1', + }; + const check2 = { + arrayField: 'test', + }; + expect.assertions(2); + try { + expect(schemaUsers.parse(check)).toEqual(check); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + try { + schemaAddresses.parse(check2); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.ts new file mode 100644 index 00000000..1f29bcdd --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.ts @@ -0,0 +1,202 @@ +import { + EntityProps, + ObjectTyped, + TypeOfArray, +} from '../../../../utils/nestjs-shared'; +import { z, ZodArray, ZodNullable } from 'zod'; + +import { ObjectLiteral } from '../../../../types'; +import { + FieldWithType, + PropsFieldItem, + PropsForField, + TypeField, +} from '../../types'; +import { nonEmptyObject } from '../zod-utils'; + +const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); +type Literal = z.infer; +type Json = Literal | { [key: string]: Json } | Json[]; + +function getZodRulesForNumber(isNullable: boolean) { + const schema = z.preprocess((x) => Number(x), z.number()); + if (isNullable) schema.nullable().optional(); + return isNullable ? schema.optional() : schema; +} + +function getZodRulesForString(isNullable: boolean) { + const schema = z.string(); + if (isNullable) schema.nullable().optional(); + return isNullable ? schema.optional() : schema; +} + +function getZodRulesForDate(isNullable: boolean) { + const schema = z.coerce.date(); + if (isNullable) schema.nullable().optional(); + return isNullable ? schema.optional() : schema; +} + +function getZodRulesForBoolean(isNullable: boolean) { + const schema = z.boolean(); + if (isNullable) schema.nullable(); + return isNullable ? schema.optional() : schema; +} + +function getZodSchemaForJson(isNullable: boolean) { + const tmpSchema = isNullable ? literalSchema.nullable() : literalSchema; + + const schema: z.ZodType = z.lazy(() => + z.union([tmpSchema, z.array(tmpSchema), z.record(tmpSchema)]) + ); + + return isNullable ? schema.optional() : schema; +} + +function getZodRulesForArray( + propsField: PropsFieldItem +): + | ZodArray, 'many'> + | ZodNullable, 'many'>> { + const type = propsField.type as T; + let schema: ZodRulesForArray; + + if (!propsField) { + schema = getZodRulesForString(false) as ZodRulesForArray; + } else { + switch (type) { + case 'number': + case 'real': + case 'integer': + case 'bigint': + case 'double': + case 'numeric': + case Number: + schema = getZodRulesForNumber(false) as ZodRulesForArray; + break; + case 'date': + case Date: + schema = getZodRulesForDate(false) as ZodRulesForArray; + break; + case 'boolean': + case Boolean: + schema = getZodRulesForBoolean(false) as ZodRulesForArray; + break; + default: + schema = getZodRulesForString(false) as ZodRulesForArray; + } + } + + if (propsField.isNullable) { + return schema.array().nullable() as ZodNullable< + ZodArray, 'many'> + >; + } + return schema.array() as ZodArray, 'many'>; +} + +type ZodRulesForArray = T extends number + ? ReturnType + : T extends Date + ? ReturnType + : T extends boolean + ? ReturnType + : ReturnType; + +type ZodRulesForType = ReturnType< + T extends TypeField.array + ? typeof getZodRulesForArray + : T extends TypeField.date + ? typeof getZodRulesForDate + : T extends TypeField.boolean + ? typeof getZodRulesForBoolean + : T extends TypeField.number + ? typeof getZodRulesForNumber + : T extends TypeField.object + ? typeof getZodSchemaForJson + : typeof getZodRulesForString +>; + +function buildSchema( + fieldType: T, + propsField: P +): ZodRulesForType { + let schema: ZodRulesForType; + switch (fieldType) { + case TypeField.array: + schema = getZodRulesForArray(propsField) as ZodRulesForType; + break; + case TypeField.date: + schema = getZodRulesForDate(propsField.isNullable) as ZodRulesForType< + T, + I + >; + break; + case TypeField.boolean: + schema = getZodRulesForBoolean(propsField.isNullable) as ZodRulesForType< + T, + I + >; + break; + case TypeField.number: + schema = getZodRulesForNumber(propsField.isNullable) as ZodRulesForType< + T, + I + >; + break; + case TypeField.object: + schema = getZodSchemaForJson(propsField.isNullable) as ZodRulesForType< + T, + I + >; + break; + default: + schema = getZodRulesForString(propsField.isNullable) as ZodRulesForType< + T, + I + >; + } + + return schema; +} + +export function zodAttributes< + E extends ObjectLiteral, + S extends true | false = false +>( + fieldWithType: FieldWithType, + propsDb: PropsForField, + primaryColumn: EntityProps, + isPatch: S +) { + const objectShape = {} as { + [K in keyof Omit, keyof EntityProps>]: ZodRulesForType< + FieldWithType[K], + TypeOfArray + >; + }; + + for (const [nameList, type] of ObjectTyped.entries(fieldWithType)) { + if (nameList === primaryColumn) continue; + const name = nameList as keyof Omit, keyof EntityProps>; + const propsField = propsDb[name]; + objectShape[name] = buildSchema< + typeof type, + typeof propsField, + TypeOfArray + >(type, propsField || {}); + } + const zodSchema = z.object(objectShape).strict(); + if (isPatch) { + return zodSchema.partial().refine(nonEmptyObject()); + } + return zodSchema.refine(nonEmptyObject()); +} + +export type ZodAttributes< + E extends ObjectLiteral, + K extends true | false = false +> = ReturnType>; +export type Attributes< + E extends ObjectLiteral, + K extends true | false = false +> = z.infer>; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/id.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/id.spec.ts similarity index 71% rename from libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/id.spec.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/id.spec.ts index 801a7c38..d5ba401b 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/id.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/id.spec.ts @@ -1,13 +1,14 @@ -import { zodIdSchema, ZodIdSchema } from './id'; -import { TypeField } from '../../orm'; import { ZodError } from 'zod'; +import { ZodId, zodId } from './id'; +import { TypeField } from '../../types'; + describe('zodIdSchema', () => { - let numberStringSchema: ZodIdSchema; - let stringSchema: ZodIdSchema; + let numberStringSchema: ZodId; + let stringSchema: ZodId; beforeAll(() => { - numberStringSchema = zodIdSchema(TypeField.number); - stringSchema = zodIdSchema(TypeField.string); + numberStringSchema = zodId(TypeField.number); + stringSchema = zodId(TypeField.string); }); it('Should be correct', () => { diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/id.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/id.ts new file mode 100644 index 00000000..030ed201 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/id.ts @@ -0,0 +1,16 @@ +import { z } from 'zod'; +import { TypeField, TypeForId } from '../../types'; + +const reg = new RegExp('^-?\\d+$'); + +export function zodId(typeId: TypeForId) { + let idSchema = z.string(); + if (typeId === TypeField.number) { + idSchema = idSchema.regex(reg); + } + + return idSchema; +} + +export type ZodId = ReturnType; +export type Id = z.infer; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/index.ts new file mode 100644 index 00000000..4b1fc19a --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/index.ts @@ -0,0 +1,6 @@ +export * from './page'; +export * from './id'; +export * from './type'; +export * from './attributes'; +export * from './rel-data'; +export * from './relationships'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/page.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/page.spec.ts new file mode 100644 index 00000000..6123a0c1 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/page.spec.ts @@ -0,0 +1,42 @@ +import { zodPageInputQuery } from './page'; +import { DEFAULT_PAGE_SIZE, DEFAULT_QUERY_PAGE } from '../../../../constants'; + +describe('zodPageInputQuerySchema', () => { + const schema = zodPageInputQuery(); + + it('should parse valid size and number as number string', () => { + const result = schema.parse({ size: '5', number: '2' }); + expect(result).toEqual({ size: 5, number: 2 }); + }); + + it('should parse valid size and number', () => { + const result = schema.parse({ size: 5, number: 2 }); + expect(result).toEqual({ size: 5, number: 2 }); + }); + + it('should use the default size and number if not provided', () => { + const result = schema.parse({}); + expect(result).toEqual({ + size: DEFAULT_PAGE_SIZE, + number: DEFAULT_QUERY_PAGE, + }); + }); + + it('should fail if size is less than 1', () => { + expect(() => schema.parse({ size: '0', number: '2' })).toThrow(); + }); + + it('should fail if number is less than 1', () => { + expect(() => schema.parse({ size: '5', number: '0' })).toThrow(); + }); + + it('should error size and number is number of float', () => { + expect(() => schema.parse({ size: '5.7', number: '2.9' })).toThrow(); + }); + + it('should error if has additional properties', () => { + expect(() => + schema.parse({ size: '5', number: '2', extra: 'ignored' }) + ).toThrow(); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/page.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/page.ts new file mode 100644 index 00000000..2682313b --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/page.ts @@ -0,0 +1,23 @@ +import { z } from 'zod'; +import { DEFAULT_PAGE_SIZE, DEFAULT_QUERY_PAGE } from '../../../../constants'; +import { ObjectLiteral } from '../../../../types'; + +export function zodPageInputQuery() { + return z + .object({ + size: z + .preprocess((x) => Number(x), z.number().int().min(1)) + .default(DEFAULT_PAGE_SIZE), + number: z + .preprocess((x) => Number(x), z.number().int().min(1)) + .default(DEFAULT_QUERY_PAGE), + }) + .strict() + .default({ + size: DEFAULT_PAGE_SIZE, + number: DEFAULT_QUERY_PAGE, + }); +} + +export type ZodPageInputQuery = ReturnType; +export type PageInputQuery = z.infer; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/data.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/rel-data.spec.ts similarity index 78% rename from libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/data.spec.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/rel-data.spec.ts index 00f409d7..fe3e3122 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/data.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/rel-data.spec.ts @@ -1,11 +1,11 @@ -import { zodDataSchema, ZodDataSchema } from './data'; -import { TypeField } from '../../orm'; +import { zodRelData, ZodRelData } from './rel-data'; +import { TypeField } from '../../types'; import { ZodError } from 'zod'; describe('zodDataSchema', () => { - let zodData: ZodDataSchema; + let zodData: ZodRelData; beforeAll(() => { - zodData = zodDataSchema('users', TypeField.string); + zodData = zodRelData('users', TypeField.string); }); it('Should be ok', () => { diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/rel-data.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/rel-data.ts new file mode 100644 index 00000000..619b9fc1 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/rel-data.ts @@ -0,0 +1,32 @@ +import { z } from 'zod'; + +import { TypeForId } from '../../types'; +import { nonEmptyObject } from '../zod-utils'; + +import { zodType, zodId } from './'; + +export function zodRelData(typeName: T, typeId: TypeForId) { + return z + .object({ + id: zodId(typeId), + type: zodType(typeName), + }) + .strict() + .refine(nonEmptyObject); +} + +export type ZodRelData = ReturnType>; +export type RelData = z.infer>; + +export function zodData() { + return z + .object({ + type: z.coerce.string(), + id: z.coerce.string(), + }) + .strict() + .refine(nonEmptyObject); +} + +export type ZodData = ReturnType; +export type Data = z.infer; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.spec.ts new file mode 100644 index 00000000..3f101754 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.spec.ts @@ -0,0 +1,244 @@ +import { z, ZodError } from 'zod'; + +import { zodRelationships, ZodRelationships } from './relationships'; +import { Users } from '../../../../mock-utils/typeorm'; + +import { + relationArrayProps, + relationPopsName, + primaryColumnType, +} from '../../../../utils/___test___/test.helper'; + +describe('zodRelationships', () => { + let relationshipsSchema: ZodRelationships; + + describe('POST', () => { + beforeAll(() => { + relationshipsSchema = zodRelationships( + relationArrayProps, + relationPopsName, + primaryColumnType, + false + ); + }); + + it('Should be ok', () => { + const check = { + comments: { + data: [ + { + type: 'comments', + id: '1', + }, + ], + }, + userGroup: { + data: { + type: 'user-groups', + id: '1', + }, + }, + manager: { + data: { + type: 'users', + id: '1', + }, + }, + notes: { + data: [ + { + type: 'notes', + id: 'id', + }, + ], + }, + }; + expect(relationshipsSchema.parse(check)).toEqual(check); + }); + + it('should be not ok', () => { + const check1 = {}; + const check2 = ''; + const check3: any[] = []; + const check4 = true; + const check5 = { + sddsf: {}, + }; + const check6 = { + comments: [], + }; + const check7 = { + comments: {}, + }; + const check8 = { + comments: '', + }; + const check9 = { + comments: true, + }; + const check10 = { + comments: [ + { + sdsf: 'sdfsdf', + }, + ], + }; + const check11 = { + comments: [{}], + }; + const check12 = { + manager: {}, + }; + const check13 = { + manager: { + sdfs: 'sdsdf', + }, + }; + const check14 = { + manager: { + id: 'sdsdf', + type: 'users', + }, + }; + const check15 = { + manager: null, + }; + const check16 = { + manager: [], + }; + const arrayCheck = [ + check1, + check2, + check3, + check4, + check5, + check6, + check7, + check8, + check9, + check10, + check11, + check12, + check13, + check14, + check15, + check16, + ]; + expect.assertions(arrayCheck.length); + for (const item of arrayCheck) { + try { + relationshipsSchema.parse(item); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + } + }); + }); + + describe('PATCH', () => { + beforeAll(() => { + relationshipsSchema = zodRelationships( + relationArrayProps, + relationPopsName, + primaryColumnType, + true + ); + }); + + it('Should be ok', () => { + const check = { + comments: { + data: [], + }, + userGroup: { + data: null, + }, + manager: { + data: { + type: 'users', + id: '1', + }, + }, + notes: { + data: [ + { + type: 'notes', + id: 'id', + }, + ], + }, + }; + expect(relationshipsSchema.parse(check)).toEqual(check); + }); + + it('should be not ok', () => { + const check1 = {}; + const check2 = ''; + const check3: any[] = []; + const check4 = true; + const check5 = { + sddsf: {}, + }; + const check6 = { + comments: [], + }; + const check7 = { + comments: {}, + }; + const check8 = { + comments: '', + }; + const check9 = { + comments: true, + }; + const check10 = { + comments: [ + { + sdsf: 'sdfsdf', + }, + ], + }; + const check11 = { + comments: [{}], + }; + const check12 = { + manager: {}, + }; + const check13 = { + manager: { + sdfs: 'sdsdf', + }, + }; + const check14 = { + manager: { + id: 'sdsdf', + type: 'users', + }, + }; + const arrayCheck = [ + check1, + check2, + check3, + check4, + check5, + check6, + check7, + check8, + check9, + check10, + check11, + check12, + check13, + check14, + ]; + expect.assertions(arrayCheck.length); + for (const item of arrayCheck) { + try { + relationshipsSchema.parse(item); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + } + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.ts new file mode 100644 index 00000000..3a6dd520 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.ts @@ -0,0 +1,157 @@ +import { z } from 'zod'; +import { + camelToKebab, + KebabCase, + ObjectTyped, +} from '../../../../utils/nestjs-shared'; + +import { ObjectLiteral } from '../../../../types'; + +import { + RelationPropsArray, + RelationPropsTypeName, + RelationPrimaryColumnType, + TypeForId, +} from '../../types'; +import { zodRelData } from './rel-data'; +import { nonEmptyObject } from '../zod-utils'; + +function getZodRuleForData< + K extends string, + P extends TypeForId, + T extends true | false = false +>(typeName: K, primaryType: P, isPatch: T) { + if (isPatch) { + return zodRelData(typeName, primaryType).nullable(); + } + return zodRelData(typeName, primaryType); +} + +function getZodRuleForArrayData< + K extends string, + P extends TypeForId, + T extends true | false = false +>(typeName: K, primaryType: P, isPatch: T) { + const dataArraySchema = getZodRuleForData( + typeName, + primaryType, + false + ).array(); + if (isPatch) { + return dataArraySchema; + } + return dataArraySchema.nonempty(); +} + +function getZodDataShape< + K extends string, + P extends TypeForId, + I extends true, + T extends true | false = false +>( + typeName: K, + primaryType: P, + isArray: I, + isPatch: T +): ReturnType>; +function getZodDataShape< + K extends string, + P extends TypeForId, + I extends false, + T extends true | false = false +>( + typeName: K, + primaryType: P, + isArray: I, + isPatch: T +): ReturnType>; +function getZodDataShape< + K extends string, + P extends TypeForId, + I extends boolean, + T extends true | false = false +>( + typeName: K, + primaryType: P, + isArray: I, + isPatch: T +): ReturnType< + typeof getZodRuleForArrayData | typeof getZodRuleForData +> { + return isArray + ? getZodRuleForArrayData(typeName, primaryType, isPatch) + : getZodRuleForData(typeName, primaryType, isPatch); +} + +function getZodResultData< + K extends string, + P extends TypeForId, + T extends true | false = false +>(typeName: K, primaryType: P, isPatch: T) { + return z + .object({ + data: getZodDataShape(typeName, primaryType, false, isPatch), + }) + .optional(); +} + +function getZodResultDataArray< + K extends string, + P extends TypeForId, + T extends true | false = false +>(typeName: K, primaryType: P, isPatch: T) { + return z + .object({ + data: getZodDataShape(typeName, primaryType, true, isPatch), + }) + .optional(); +} + +type ZodResultData< + K extends string, + P extends TypeForId, + I extends boolean, + T extends true | false = false +> = I extends true + ? ReturnType> + : ReturnType>; + +export function zodRelationships< + E extends ObjectLiteral, + S extends true | false = false +>( + relationArrayProps: RelationPropsArray, + relationPopsName: RelationPropsTypeName, + primaryColumnType: RelationPrimaryColumnType, + isPatch: S +) { + const shape = {} as { + [K in keyof RelationPropsArray]: ZodResultData< + KebabCase[K]>, + RelationPrimaryColumnType[K], + RelationPropsArray[K], + S + >; + }; + + for (const [props, value] of ObjectTyped.entries(relationArrayProps)) { + const typeName = camelToKebab(relationPopsName[props]); + const primaryType = primaryColumnType[props]; + shape[props] = ( + value === true + ? getZodResultDataArray(typeName, primaryType, isPatch) + : getZodResultData(typeName, primaryType, isPatch) + ) as ZodResultData; + } + + return z.object(shape).strict().refine(nonEmptyObject()); +} + +export type ZodRelationships< + T extends ObjectLiteral, + K extends true | false = false +> = ReturnType>; +export type Relationships< + T extends ObjectLiteral, + K extends true | false = false +> = z.infer>; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/type.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/type.spec.ts similarity index 71% rename from libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/type.spec.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/type.spec.ts index 9023573c..a5d5bd3b 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/type.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/type.spec.ts @@ -1,11 +1,11 @@ -import { zodTypeSchema, ZodTypeSchema } from './type'; +import { zodType, ZodType } from './type'; import { ZodError } from 'zod'; describe('type', () => { const literal = 'users'; - let userTypeSchema: ZodTypeSchema; + let userTypeSchema: ZodType; beforeAll(() => { - userTypeSchema = zodTypeSchema(literal); + userTypeSchema = zodType(literal); }); it('should be ok', () => { expect(userTypeSchema.parse(literal)).toEqual(literal); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/type.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/type.ts new file mode 100644 index 00000000..297952d1 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/type.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; + +export function zodType(type: T) { + return z.literal(type); +} + +export type ZodType = ReturnType>; +export type Type = z.infer>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-utils.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-utils.spec.ts new file mode 100644 index 00000000..33346a35 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-utils.spec.ts @@ -0,0 +1,353 @@ +import { + arrayItemStringLongerThan, + elementOfArrayMustBe, + getValidationErrorForStrict, + nonEmptyObject, + oneOf, + stringLongerThan, + stringMustBe, + guardIsKeyOfObject, +} from './zod-utils'; +import { TypeField } from '../types'; + +describe('zod-utils', () => { + describe('guardIsKeyOfObject', () => { + it('should not throw an error if the key exists in the object', () => { + const obj = { a: 1, b: 2, c: 3 }; + expect(() => guardIsKeyOfObject(obj, 'a')).not.toThrow(); + }); + + it('should throw an error if the key does not exist in the object', () => { + const obj = { a: 1, b: 2, c: 3 }; + expect(() => guardIsKeyOfObject(obj, 'z')).toThrow('Type guard error'); + }); + + it('should throw an error when the object is null', () => { + const obj = null; + expect(() => guardIsKeyOfObject(obj, 'a')).toThrow('Type guard error'); + }); + + it('should throw an error when the object is undefined', () => { + const obj = undefined; + expect(() => guardIsKeyOfObject(obj as any, 'a')).toThrow( + 'Type guard error' + ); + }); + + it('should throw an error for non-object types', () => { + const nonObject = 42; // A number instead of an object + expect(() => guardIsKeyOfObject(nonObject as any, 'a')).toThrow( + 'Type guard error' + ); + }); + + it('should work with symbol keys in the object', () => { + const symbolKey = Symbol('key'); + const obj = { [symbolKey]: 'value' }; + expect(() => guardIsKeyOfObject(obj, symbolKey)).not.toThrow(); + }); + + it('should throw an error if the symbol key does not exist', () => { + const existingSymbol = Symbol('existing'); + const missingSymbol = Symbol('missing'); + const obj = { [existingSymbol]: 'value' }; + expect(() => guardIsKeyOfObject(obj, missingSymbol)).toThrow( + 'Type guard error' + ); + }); + + it('should not throw an error if the number key exists in the object', () => { + const obj = { 1: 'one', 2: 'two' }; + expect(() => guardIsKeyOfObject(obj, 1)).not.toThrow(); + }); + + it('should throw an error if the number key does not exist in the object', () => { + const obj = { 1: 'one', 2: 'two' }; + expect(() => guardIsKeyOfObject(obj, 3)).toThrow('Type guard error'); + }); + + it('should not throw an error for empty object with no keys', () => { + const obj = {}; + expect(() => guardIsKeyOfObject(obj, 'a')).toThrow('Type guard error'); + }); + }); + describe('nonEmptyObject', () => { + it('should return true for a non-empty object', () => { + const isNonEmpty = nonEmptyObject()({ key: 'value' }); + expect(isNonEmpty).toBe(true); + }); + + it('should return false for an empty object', () => { + const isNonEmpty = nonEmptyObject()({}); + expect(isNonEmpty).toBe(false); + }); + + it('should return false for null value', () => { + const isNonEmpty = nonEmptyObject()(null); + expect(isNonEmpty).toBe(false); + }); + + it('should return false for undefined value', () => { + const isNonEmpty = nonEmptyObject()(undefined); + expect(isNonEmpty).toBe(false); + }); + + it('should return false for non-object values', () => { + const isNonEmptyString = nonEmptyObject()('string'); + expect(isNonEmptyString).toBe(false); + + const isNonEmptyNumber = nonEmptyObject()(42); + expect(isNonEmptyNumber).toBe(false); + const isNonEmptyArray = nonEmptyObject()([1, 2, 3]); + expect(isNonEmptyArray).toBe(true); + const isEmptyArray = nonEmptyObject()([]); + expect(isEmptyArray).toBe(false); + }); + }); + + describe('getValidationErrorForStrict', () => { + it('should return a validation error message for "Fields" with props', () => { + const message = getValidationErrorForStrict(['id', 'name'], 'Fields'); + expect(message).toBe( + 'Validation error: Fields should be have only props: ["id","name"]' + ); + }); + + it('should return a validation error message for "Filter" with props', () => { + const message = getValidationErrorForStrict(['age', 'gender'], 'Filter'); + expect(message).toBe( + 'Validation error: Filter should be have only props: ["age","gender"]' + ); + }); + + it('should return a message with an empty props array', () => { + const message = getValidationErrorForStrict([], 'Fields'); + expect(message).toBe( + 'Validation error: Fields should be have only props: [""]' + ); + }); + + it('should handle single prop correctly', () => { + const message = getValidationErrorForStrict(['id'], 'Fields'); + expect(message).toBe( + 'Validation error: Fields should be have only props: ["id"]' + ); + }); + + it('should handle a large props array', () => { + const props = ['a', 'b', 'c', 'd', 'e']; + const message = getValidationErrorForStrict(props, 'Filter'); + expect(message).toBe( + 'Validation error: Filter should be have only props: ["a","b","c","d","e"]' + ); + }); + }); + + describe('oneOf', () => { + it('should return true if at least one key exists in the object', () => { + const hasKey = oneOf(['key1', 'key2'])({ key1: 'value1' }); + expect(hasKey).toBe(true); + }); + + it('should return false if none of the keys exist in the object', () => { + const hasKey = oneOf(['missingKey1', 'missingKey2'])({ key1: 'value1' }); + expect(hasKey).toBe(false); + }); + + it('should return false for an empty object', () => { + const hasKey = oneOf(['key1', 'key2'])({}); + expect(hasKey).toBe(false); + }); + + it('should return false for null or undefined input', () => { + const hasKeyInNull = oneOf(['key1', 'key2'])(null); + expect(hasKeyInNull).toBe(false); + + const hasKeyInUndefined = oneOf(['key1', 'key2'])(undefined); + expect(hasKeyInUndefined).toBe(false); + }); + + it('should return false for an empty array of keys', () => { + const hasKey = oneOf([])({ key1: 'value1' }); + expect(hasKey).toBe(false); + }); + + it('should work with overlapping keys', () => { + const hasKey = oneOf(['key1', 'key2'])({ + key1: 'value1', + key2: 'value2', + }); + expect(hasKey).toBe(true); + }); + }); + + describe('stringLongerThan', () => { + it('should return true if the string is longer than the specified length', () => { + const isLongerThan = stringLongerThan(5)('testing'); + expect(isLongerThan).toBe(true); + }); + + it('should return false if the string length is equal to the specified length', () => { + const isLongerThan = stringLongerThan(7)('testing'); + expect(isLongerThan).toBe(false); + }); + + it('should return false if the string is shorter than the specified length', () => { + const isLongerThan = stringLongerThan(10)('test'); + expect(isLongerThan).toBe(false); + }); + + it('should return false for an empty string when length > 0', () => { + const isLongerThan = stringLongerThan(1)(''); + expect(isLongerThan).toBe(false); + }); + + it('should return true for length 0 with any non-empty string', () => { + const isLongerThan = stringLongerThan(0)('t'); + expect(isLongerThan).toBe(true); + }); + }); + + describe('arrayItemStringLongerThan', () => { + it('should return true if all strings in the array are longer than the specified length', () => { + const isAllLonger = arrayItemStringLongerThan(3)(['hello', 'world']); + expect(isAllLonger).toBe(true); + }); + + it('should return false if any string in the array is shorter than or equal to the specified length', () => { + const isAllLonger = arrayItemStringLongerThan(5)(['short', 'tiny']); + expect(isAllLonger).toBe(false); + }); + + it('should return false for an array with null values that fail the length check', () => { + const isAllLonger = arrayItemStringLongerThan(5)([null, 'tiny']); + expect(isAllLonger).toBe(false); + }); + + it('should return true for an array where all valid strings are longer than the specified length', () => { + const isAllLonger = arrayItemStringLongerThan(2)([null, 'hello', 'yes']); + expect(isAllLonger).toBe(true); + }); + + it('should return true if the array is empty', () => { + const isAllLonger = arrayItemStringLongerThan(3)([] as any); + expect(isAllLonger).toBe(true); + }); + + it('should return false if any string in the array is shorter and null does not interfere', () => { + const isAllLonger = arrayItemStringLongerThan(4)(['short', null, 'tiny']); + expect(isAllLonger).toBe(false); + }); + }); + + describe('stringMustBe', () => { + it('should return true for a null input', () => { + const isValid = stringMustBe()(null); + expect(isValid).toBe(true); + }); + + it('should return true for valid boolean strings', () => { + const isValidTrue = stringMustBe(TypeField.boolean)('true'); + const isValidFalse = stringMustBe(TypeField.boolean)('false'); + expect(isValidTrue).toBe(true); + expect(isValidFalse).toBe(true); + }); + + it('should return false for invalid boolean strings', () => { + const isValid = stringMustBe(TypeField.boolean)('yes'); + expect(isValid).toBe(false); + }); + + it('should return true for numeric strings', () => { + const isValid = stringMustBe(TypeField.number)('123'); + expect(isValid).toBe(true); + }); + + it('should return false for non-numeric strings when type is number', () => { + const isValid = stringMustBe(TypeField.number)('abc'); + expect(isValid).toBe(false); + }); + + it('should return true for valid ISO date strings', () => { + const isValid = stringMustBe(TypeField.date)('2023-10-10'); + expect(isValid).toBe(true); + }); + + it('should return false for invalid date strings', () => { + const isValid = stringMustBe(TypeField.date)('not-a-date'); + expect(isValid).toBe(false); + }); + + it('should return true for any input when type is string', () => { + const isValid = stringMustBe(TypeField.string)('any-value'); + expect(isValid).toBe(true); + }); + }); + + describe('elementOfArrayMustBe', () => { + it('should return true if all elements in the array are valid strings', () => { + const isValid = elementOfArrayMustBe(TypeField.string)([ + 'hello', + 'world', + ]); + expect(isValid).toBe(true); + }); + + it('should return false if any element is not a valid string, because before converted to string', () => { + const isValid = elementOfArrayMustBe(TypeField.string)(['hello', 123]); + expect(isValid).toBe(true); + }); + + it('should return true if all elements in the array are valid numbers', () => { + const isValid = elementOfArrayMustBe(TypeField.number)([1, 2, 3]); + expect(isValid).toBe(true); + }); + + it('should return false if any element is not a valid number', () => { + const isValid = elementOfArrayMustBe(TypeField.number)([1, 'two', 3]); + expect(isValid).toBe(false); + }); + + it('should return true if all elements are valid boolean strings', () => { + const isValid = elementOfArrayMustBe(TypeField.boolean)([ + 'true', + 'false', + ]); + expect(isValid).toBe(true); + }); + + it('should return false if any element is not a valid boolean string', () => { + const isValid = elementOfArrayMustBe(TypeField.boolean)([ + 'true', + 'hello', + ]); + expect(isValid).toBe(false); + }); + + it('should return true for an empty array', () => { + const isValid = elementOfArrayMustBe(TypeField.string)([]); + expect(isValid).toBe(true); + }); + + it('should return true if array contains null', () => { + const isValid = elementOfArrayMustBe(TypeField.string)(['hello', null]); + expect(isValid).toBe(true); + }); + + it('should return true if all elements are valid dates', () => { + const isValid = elementOfArrayMustBe(TypeField.date)([ + '2023-01-01', + '2023-10-10', + ]); + expect(isValid).toBe(true); + }); + + it('should return false if any element is not a valid date', () => { + const isValid = elementOfArrayMustBe(TypeField.date)([ + '2023-01-01', + 'not-a-date', + ]); + expect(isValid).toBe(false); + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-utils.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-utils.ts similarity index 79% rename from libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-utils.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-utils.ts index f265e25b..50dc7040 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-utils.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-utils.ts @@ -1,6 +1,22 @@ -import { TypeField } from '../orm'; +import { TypeField } from '../types'; + +export const nonEmptyObject = + () => + (object: T) => + !!(typeof object === 'object' && object && Object.keys(object).length > 0); + +export const uniqueArray = () => (e: string[]) => new Set(e).size === e.length; + +export const getValidationErrorForStrict = ( + props: string[], + name: 'Fields' | 'Filter' +) => + `Validation error: ${name} should be have only props: ["${props.join( + '","' + )}"]`; export const oneOf = (keys: string[]) => (val: any) => { + if (!val) return false; for (const k of keys) { if (val[k] !== undefined) return true; } @@ -34,23 +50,20 @@ export const stringMustBe = return true; } }; + export const elementOfArrayMustBe = (type: TypeField = TypeField.string) => (inputArray: unknown[]) => { const checkFunc = stringMustBe(type); return !inputArray.some((i) => !checkFunc(`${i}`)); }; -export const uniqueArray = () => (e: string[]) => new Set(e).size === e.length; -export const nonEmptyObject = - () => - (object: T) => - !!(object && Object.keys(object).length > 0); +export function guardIsKeyOfObject( + object: R, + key: string | number | symbol +): asserts key is keyof R { + if (typeof object === 'object' && object !== null && key in object) + return void 0; -export const getValidationErrorForStrict = ( - props: string[], - name: 'Fields' | 'Filter' -) => - `Validation error: ${name} should be have only props: ["${props.join( - '","' - )}"]`; + throw new Error('Type guard error'); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/constants/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/constants/index.ts new file mode 100644 index 00000000..b5faede6 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/constants/index.ts @@ -0,0 +1,2 @@ +export const SUB_QUERY_ALIAS_FOR_PAGINATION = 'subQueryWithLimitOffset'; +export const ALIAS_FOR_PAGINATION = 'aliasForPagination'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/factory/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/factory/index.ts new file mode 100644 index 00000000..bfc13543 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/factory/index.ts @@ -0,0 +1,191 @@ +import { FactoryProvider } from '@nestjs/common'; +import { getDataSourceToken } from '@nestjs/typeorm'; +import { camelToKebab } from '../../../utils/nestjs-shared'; +import { DataSource, EntityManager, Repository } from 'typeorm'; + +import { + CURRENT_ENTITY_MANAGER_TOKEN, + FIND_ONE_ROW_ENTITY, + CHECK_RELATION_NAME, + ORM_SERVICE, + RUN_IN_TRANSACTION_FUNCTION, + GLOBAL_MODULE_OPTIONS_TOKEN, + CURRENT_DATA_SOURCE_TOKEN, + CURRENT_ENTITY_REPOSITORY, + ENTITY_MAP_PROPS, +} from '../../../constants'; +import { + FindOneRowEntity, + CheckRelationNme, + ZodEntityProps, +} from '../../mixin/types'; +import { + ObjectLiteral, + EntityTarget, + ResultGeneralParam, + RequiredFromPartial, + ConfigParam, + RunInTransaction, + EntityClass, +} from '../../../types'; + +import { TypeOrmService, TypeormUtilsService } from '../service'; +import { getEntityName } from '../../mixin/helper'; +import { TypeOrmJsonApiModule } from '../type-orm-json-api.module'; +import { TypeOrmParam } from '../type'; + +import { + getProps, + getRelation, + getPropsType, + getPropsNullable, + getPrimaryColumnName, + getPrimaryColumnType, + getRelationProperty, +} from '../orm-helper'; + +export function CurrentDataSourceProvider( + connectionName?: string +): FactoryProvider { + return { + provide: CURRENT_DATA_SOURCE_TOKEN, + useFactory: (dataSource: DataSource) => dataSource, + inject: [getDataSourceToken(connectionName)], + }; +} + +export function CurrentEntityManager(): FactoryProvider { + return { + provide: CURRENT_ENTITY_MANAGER_TOKEN, + useFactory: (dataSource: DataSource) => dataSource.manager, + inject: [CURRENT_DATA_SOURCE_TOKEN], + }; +} + +export function CurrentEntityRepository( + entity: E +): FactoryProvider> { + return { + provide: CURRENT_ENTITY_REPOSITORY, + useFactory: (entityManager: EntityManager) => + entityManager.getRepository(entity as unknown as EntityTarget), + inject: [CURRENT_ENTITY_MANAGER_TOKEN], + }; +} + +export function EntityPropsMap( + entities: EntityClass[] +) { + return { + provide: ENTITY_MAP_PROPS, + inject: [CURRENT_ENTITY_MANAGER_TOKEN], + useFactory: (entityManager: EntityManager) => { + const mapProperty = new Map, ZodEntityProps>(); + + for (const item of entities) { + const entityRepo = entityManager.getRepository(item); + + const className = getEntityName(item); + mapProperty.set(item, { + props: getProps(entityRepo), + propsType: getPropsType(entityRepo), + propsNullable: getPropsNullable(entityRepo), + primaryColumnName: getPrimaryColumnName(entityRepo), + primaryColumnType: getPrimaryColumnType(entityRepo), + typeName: camelToKebab(className), + className: className, + relations: getRelation(entityRepo), + relationProperty: getRelationProperty(entityRepo), + }); + } + return mapProperty; + }, + }; +} + +export function FindOneRowEntityFactory< + E extends ObjectLiteral +>(): FactoryProvider> { + return { + provide: FIND_ONE_ROW_ENTITY, + inject: [CURRENT_ENTITY_REPOSITORY, TypeormUtilsService], + useFactory: ( + repository: Repository, + typeormUtilsService: TypeormUtilsService + ) => { + return async (entity, value) => { + const params = 'params'; + return await repository + .createQueryBuilder(typeormUtilsService.currentAlias) + .where( + `${typeormUtilsService.getAliasPath( + typeormUtilsService.currentPrimaryColumn + )} = :${params}` + ) + .setParameters({ + [params]: value, + }) + .getOne(); + }; + }, + }; +} + +export function CheckRelationNameFactory< + E extends ObjectLiteral +>(): FactoryProvider> { + return { + provide: CHECK_RELATION_NAME, + inject: [TypeormUtilsService], + useFactory(typeormUtilsService: TypeormUtilsService) { + return (entity, value) => + !!typeormUtilsService.relationFields.find((i) => i === value); + }, + }; +} + +export function RunInTransactionFactory(): FactoryProvider { + return { + provide: RUN_IN_TRANSACTION_FUNCTION, + inject: [GLOBAL_MODULE_OPTIONS_TOKEN, CURRENT_DATA_SOURCE_TOKEN], + useFactory( + options: ResultGeneralParam & { + type: typeof TypeOrmJsonApiModule; + options: RequiredFromPartial; + }, + dataSource: DataSource + ) { + const { + options: { runInTransaction }, + } = options; + + if (runInTransaction && typeof runInTransaction === 'function') { + return (callback) => + runInTransaction('READ COMMITTED', () => callback()); + } + + return async (callback) => { + const queryRunner = dataSource.createQueryRunner(); + await queryRunner.startTransaction('READ UNCOMMITTED'); + let result: unknown; + try { + result = await callback(); + await queryRunner.commitTransaction(); + } catch (e) { + await queryRunner.rollbackTransaction(); + throw e; + } finally { + await queryRunner.release(); + } + return result; + }; + }, + }; +} + +export function OrmServiceFactory() { + return { + provide: ORM_SERVICE, + useClass: TypeOrmService, + }; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/index.ts new file mode 100644 index 00000000..fd6d6101 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/index.ts @@ -0,0 +1,2 @@ +export * from './type-orm-json-api.module'; +export * from './type'; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-helper.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.spec.ts similarity index 59% rename from libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-helper.spec.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.spec.ts index 525a4cab..8a06a3df 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-helper.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.spec.ts @@ -1,11 +1,16 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getDataSourceToken } from '@nestjs/typeorm'; +import { + ObjectTyped, + EntityRelation, + TypeOfArray, + EntityProps, +} from '../../../utils/nestjs-shared'; import { Repository } from 'typeorm'; import { IMemoryDb } from 'pg-mem'; import { mockDBTestModule, - createAndPullSchemaBase, pullUser, pullAllData, providerEntities, @@ -16,30 +21,27 @@ import { Comments, Roles, UserGroups, -} from '../../mock-utils'; -import { EntityProps, EntityRelation, TypeOfArray } from '../../types'; +} from '../../../mock-utils/typeorm'; + import { getField, getPropsTreeForRepository, fromRelationTreeToArrayName, getArrayFields, - PropsArray, getArrayPropsForEntity, - ArrayPropsForEntity, getFieldWithType, + getTypeForAllProps, getRelationTypeArray, - getRelationTypeName, - getRelationTypePrimaryColumn, - TypeField, getTypePrimaryColumn, - getPrimaryColumnsForRelation, - getIsArrayRelation, - getTypeForAllProps, getPropsFromDb, -} from './orm-helper'; -import { ObjectTyped } from '../utils'; + getRelationTypeName, + getRelationTypePrimaryColumn, +} from './'; -describe('zod-helper', () => { +import { PropsArray, ArrayPropsForEntity, TypeField } from '../../mixin/types'; +import { createAndPullSchemaBase } from '../../../mock-utils'; + +describe('type-orm-helper', () => { let userRepository: Repository; let addressesRepository: Repository; let notesRepository: Repository; @@ -147,7 +149,7 @@ describe('zod-helper', () => { const checkArray = fromRelationTreeToArrayName(relationField); for (const key of relations) { - let resultKey = + const resultKey = key === 'manager' ? 'Users' : key === 'userGroup' ? 'UserGroups' : key; const relationsRepo = @@ -236,24 +238,6 @@ describe('zod-helper', () => { expect(getTypePrimaryColumn(notesRepository)).toBe(TypeField.string); }); - it('getPrimaryColumnsForRelation', () => { - const result = getPrimaryColumnsForRelation(userRepository); - expect(result.roles).toBe('id'); - expect(result.manager).toBe('id'); - }); - - it('geTisArrayRelation', () => { - const result = getIsArrayRelation(userRepository); - expect(result).toEqual({ - addresses: false, - manager: false, - roles: true, - comments: true, - notes: true, - userGroup: false, - }); - }); - it('getTypeForAllProps', () => { const result = getTypeForAllProps(userRepository); expect(result.manager.id).toBe(TypeField.number); @@ -264,10 +248,175 @@ describe('zod-helper', () => { it('getPropsFromDb', () => { const result = getPropsFromDb(userRepository); + // testReal has isNullable false but have default should be true expect(result['testReal']).toEqual({ type: 'real', isArray: true, + isNullable: true, + }); + + const result2 = getPropsFromDb(rolesRepository); + expect(result2['key']).toEqual({ + type: 'varchar', + isArray: false, isNullable: false, }); }); }); + +import { + getProps, + getRelation, + getPropsType, + getPropsNullable, + getPrimaryColumnName, + getPrimaryColumnType, + getRelationProperty, +} from './'; + +describe('typeorm-orm-helper-for-map', () => { + let userRepository: Repository; + let addressesRepository: Repository; + let notesRepository: Repository; + let commentsRepository: Repository; + let rolesRepository: Repository; + let userGroupRepository: Repository; + let db: IMemoryDb; + let user: Users; + let userWithRelation: Users; + beforeAll(async () => { + db = createAndPullSchemaBase(); + const module: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [...providerEntities(getDataSourceToken())], + }).compile(); + + ({ + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository, + } = getRepository(module)); + }); + + it('getProps', () => { + const result = getProps(userRepository); + expect(result.includes('id')).toBe(true); + expect(result.includes('lastName')).toBe(true); + expect(result.includes('createdAt')).toBe(true); + expect(result.includes('updatedAt')).toBe(true); + expect(result.includes('isActive')).toBe(true); + expect(result.includes('login')).toBe(true); + expect(result.includes('firstName')).toBe(true); + expect(result.includes('testReal')).toBe(true); + expect(result.includes('testArrayNull')).toBe(true); + expect(result.includes('testDate')).toBe(true); + + expect(result.includes('userGroup' as any)).toBe(false); + expect(result.includes('notes' as any)).toBe(false); + expect(result.includes('comments' as any)).toBe(false); + expect(result.includes('roles' as any)).toBe(false); + expect(result.includes('manager' as any)).toBe(false); + expect(result.includes('addresses' as any)).toBe(false); + }); + + it('getPropsType', () => { + const result = getPropsType(userRepository); + + expect(result).toEqual({ + createdAt: 'date', + firstName: 'string', + id: 'number', + isActive: 'boolean', + lastName: 'string', + login: 'string', + testArrayNull: 'array', + testDate: 'date', + testReal: 'array', + updatedAt: 'date', + }); + }); + + it('getPropsNullable', () => { + const result = getPropsNullable(userRepository); + expect(result).toEqual([ + 'firstName', + 'testReal', + 'testArrayNull', + 'lastName', + 'isActive', + 'testDate', + 'createdAt', + 'updatedAt', + ]); + }); + + it('getPrimaryColumnName', () => { + const result = getPrimaryColumnName(userRepository); + expect(result).toBe('id'); + }); + + it('getPrimaryColumnType', () => { + const result = getPrimaryColumnType(userRepository); + expect(result).toBe(TypeField.number); + }); + + it('getRelation', () => { + const result = getRelation(userRepository); + expect(result.includes('id' as any)).toBe(false); + expect(result.includes('lastName' as any)).toBe(false); + expect(result.includes('createdAt' as any)).toBe(false); + expect(result.includes('updatedAt' as any)).toBe(false); + expect(result.includes('isActive' as any)).toBe(false); + expect(result.includes('login' as any)).toBe(false); + expect(result.includes('firstName' as any)).toBe(false); + expect(result.includes('testReal' as any)).toBe(false); + expect(result.includes('testArrayNull' as any)).toBe(false); + expect(result.includes('testDate' as any)).toBe(false); + + expect(result.includes('userGroup')).toBe(true); + expect(result.includes('notes')).toBe(true); + expect(result.includes('comments')).toBe(true); + expect(result.includes('roles')).toBe(true); + expect(result.includes('manager')).toBe(true); + expect(result.includes('addresses')).toBe(true); + }); + + it('getRelationProperty', () => { + const result = getRelationProperty(userRepository); + expect(result).toEqual({ + addresses: { + entityClass: Addresses, + isArray: false, + nullable: true, + }, + comments: { + entityClass: Comments, + isArray: true, + nullable: false, + }, + manager: { + entityClass: Users, + isArray: false, + nullable: true, + }, + notes: { + entityClass: Notes, + isArray: true, + nullable: false, + }, + roles: { + entityClass: Roles, + isArray: true, + nullable: false, + }, + userGroup: { + entityClass: UserGroups, + isArray: false, + nullable: true, + }, + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-helper.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.ts similarity index 56% rename from libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-helper.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.ts index d73f0835..4657b4be 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-helper.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.ts @@ -1,46 +1,40 @@ -import { Repository } from 'typeorm'; -import { Type } from '@nestjs/common'; import { - CastProps, - Concat, - Entity, EntityProps, - EntityPropsArray, EntityRelation, - IsArray, + ObjectTyped, +} from '../../../utils/nestjs-shared'; +import { Repository } from 'typeorm'; + +import { ObjectLiteral, ResultMicroOrmModuleOptions } from '../../../types'; +import { + RelationTree, + ValueOf, + UnionToTuple, TypeCast, + Concat, TypeOfArray, - UnionToTuple, - ValueOf, -} from '../../types'; -import { getEntityName, ObjectTyped } from '../utils'; -import { guardKeyForPropertyTarget } from './orm-type-asserts'; -import { ColumnType } from 'typeorm/driver/types/ColumnTypes'; - -export enum PropsNameResultField { - field = 'field', - relations = 'relations', -} - -export type ResultGetField = { - [PropsNameResultField.field]: TupleOfEntityProps; - [PropsNameResultField.relations]: TupleOfEntityRelation; -}; - -export type TupleOfEntityProps< - E, - Props = UnionToTuple> -> = Props extends readonly [string, ...string[]] ? Props : never; -export type TupleOfEntityRelation< - E, - Props = UnionToTuple> -> = Props extends readonly [string, ...string[]] ? Props : never; - -export type RelationTree = { - [K in keyof RelationType]: TypeOfArray extends Entity - ? ResultGetField>['field'] - : never; -}; + CastProps, + PropsNameResultField, + PropsArray, + RelationType, + ResultGetField, + ArrayPropsForEntity, + AllFieldWithType, + TypeField, + FieldWithType, + RelationPropsArray, + TypeForId, + PropsForField, + ColumnType, + RelationPropsTypeName, + RelationPrimaryColumnType, + TupleOfEntityRelation, + TupleOfEntityProps, + FilterNullableProps, + RelationProperty, +} from '../../mixin/types'; +import { getEntityName } from '../../mixin/helper'; +import { EntityMetadata } from '@mikro-orm/core'; export type ConcatFieldWithRelation< R extends string, @@ -50,7 +44,7 @@ export type ConcatFieldWithRelation< }>; export type ConcatRelationUnion< - E extends Entity, + E extends ObjectLiteral, R = RelationTree > = ValueOf<{ [K in keyof R]: ConcatFieldWithRelation< @@ -59,55 +53,190 @@ export type ConcatRelationUnion< >; }>; -export type ConcatRelation = TypeCast< +export type ConcatRelation = TypeCast< UnionToTuple>, [string, ...string[]] >; -type RelationType = { - [K in EntityRelation]: Type>>; +export const getField = ( + repository: Repository +): ResultGetField => { + const relations = repository.metadata.relations.map((i) => { + return i.propertyName; + }); + + const field = repository.metadata.columns + .filter((i) => !relations.includes(i.propertyName)) + .map((r) => r.propertyName); + + return { + field, + relations, + } as unknown as ResultGetField; +}; + +export const fromRelationTreeToArrayName = ( + relationTree: RelationTree +): ConcatRelation => { + return ObjectTyped.entries(relationTree).reduce((acum, [name, filed]) => { + acum.push(...filed.map((i) => `${name.toLocaleString()}.${i}`)); + return acum; + }, [] as string[]) as unknown as ConcatRelation; }; -export enum TypeField { - array = 'array', - date = 'date', - number = 'number', - boolean = 'boolean', - string = 'string', - object = 'object', +export const getPropsTreeForRepository = ( + repository: Repository +): RelationTree => { + const dataSource = repository.metadata.connection; + + const relationType = repository.metadata.relations.reduce((acum, i) => { + acum[i.propertyName] = i.inverseEntityMetadata.target; + return acum; + }, {} as Record) as unknown as RelationType; + + return ObjectTyped.entries(relationType).reduce( + (acum, [key, value]) => ({ + ...acum, + ...{ [key]: getField(dataSource.getRepository(value))['field'] }, + }), + {} as RelationTree + ); +}; + +export type PropertyTarget< + E extends ObjectLiteral, + For extends PropsNameResultField +> = { + [K in ResultGetField[For][number]]: K extends keyof E + ? TypeOfArray + : never; +}; + +export function guardKeyForPropertyTarget< + E extends ObjectLiteral, + For extends PropsNameResultField, + R extends PropertyTarget +>(relationsTargets: R, key: any): asserts key is keyof R { + if (!(key in relationsTargets)) throw new Error('Type guard error'); } -export type TypeForId = Extract; +export const getArrayPropsForEntity = ( + repository: Repository +): ArrayPropsForEntity => { + const connection = repository.metadata.connection; + + const relationsTargets = repository.metadata.relations.reduce( + (acum, i) => ({ + ...acum, + [i.propertyName]: i.type, + }), + {} as Record + ) as PropertyTarget; -export type FieldWithType = { - [K in EntityProps]: IsArray extends true - ? TypeField.array - : E[K] extends Date - ? TypeField.date - : E[K] extends number - ? TypeField.number - : E[K] extends boolean - ? TypeField.boolean - : E[K] extends object - ? TypeField.object - : TypeField.string; + const { relations } = getField(repository); + const relationsArrayFields = relations.reduce( + (acum, item) => { + guardKeyForPropertyTarget(relationsTargets, item); + const target = relationsTargets[item] as TypeCast< + TypeOfArray>, + ObjectLiteral + >; + + const repository = connection.getRepository( + target as Function + ) as Repository; + + acum[item] = getArrayFields(repository) as PropsArray< + TypeOfArray> + >; + + return acum; + }, + {} as { + [K in ResultGetField['relations'][number]]: PropsArray< + TypeOfArray> + >; + } + ); + + return { + target: getArrayFields(repository), + ...relationsArrayFields, + }; }; -export type RelationPropsType = { - [K in EntityRelation]: E[K] extends unknown[] ? true : false; +export const getArrayFields = ( + repository: Repository +): PropsArray => { + const relations = repository.metadata.relations.map((i) => { + return i.propertyName; + }); + + return repository.metadata.columns + .filter((i) => !relations.includes(i.propertyName)) + .reduce((acum, metaData) => { + if (metaData.isArray) { + acum[metaData.propertyName] = true; + } + return acum; + }, {} as Record) as PropsArray; }; -export type RelationPropsTypeName = { - [K in EntityRelation]: string; +export const getTypeForAllProps = ( + repository: Repository +): AllFieldWithType => { + const targetField = getFieldWithType(repository); + + const relationField = repository.metadata.relations.reduce((acum, item) => { + acum[item.propertyName] = getFieldWithType( + repository.manager.getRepository(item.inverseEntityMetadata.target) + ); + return acum; + }, {} as any); + + return { + ...targetField, + ...relationField, + }; }; -export type RelationPrimaryColumnType = { - [K in EntityRelation]: TypeForId; +export const getFieldWithType = ( + repository: Repository +): FieldWithType => { + const { field } = getField(repository); + + const entity = repository.target as any; + const result = {} as any; + for (const item of field) { + let typeProps: TypeField = TypeField.string; + switch (Reflect.getMetadata('design:type', entity['prototype'], item)) { + case Array: + typeProps = TypeField.array; + break; + case Date: + typeProps = TypeField.date; + break; + case Number: + typeProps = TypeField.number; + break; + case Boolean: + typeProps = TypeField.boolean; + break; + case Object: + typeProps = TypeField.object; + break; + default: + typeProps = TypeField.string; + } + result[item] = typeProps; + } + + return result; }; -export const getRelationTypeArray = ( +export const getRelationTypeArray = ( repository: Repository -): RelationPropsType => { +): RelationPropsArray => { const { relations } = getField(repository); const entity = repository.target as any; @@ -119,7 +248,7 @@ export const getRelationTypeArray = ( return result; }; -export const getTypePrimaryColumn = ( +export const getTypePrimaryColumn = ( repository: Repository ): TypeForId => { const target = repository.target as any; @@ -134,7 +263,30 @@ export const getTypePrimaryColumn = ( : TypeField.string; }; -export const getRelationTypePrimaryColumn = ( +export const getPropsFromDb = ( + repository: Repository +): PropsForField => { + return repository.metadata.columns.reduce((acum, i) => { + const tmp = i.propertyName as unknown as EntityProps & EntityRelation; + acum[tmp] = { + type: i.type as ColumnType, + isArray: i.isArray, + isNullable: i.isNullable || i.default !== undefined, + }; + return acum; + }, {} as PropsForField); +}; + +export const getRelationTypeName = ( + repository: Repository +): RelationPropsTypeName => { + return repository.metadata.relations.reduce((acum, i) => { + acum[i.propertyName] = getEntityName(i.inverseEntityMetadata.target); + return acum; + }, {} as Record) as RelationPropsTypeName; +}; + +export const getRelationTypePrimaryColumn = ( repository: Repository ): RelationPrimaryColumnType => { return repository.metadata.relations.reduce((acum, i) => { @@ -149,36 +301,44 @@ export const getRelationTypePrimaryColumn = ( return acum; }, {} as Record) as RelationPrimaryColumnType; }; +// ----- -export const getPrimaryColumnsForRelation = ( +export const getRelation = ( repository: Repository -): RelationPropsTypeName => { - return repository.metadata.relations.reduce((acum, i) => { - const target = i.inverseEntityMetadata.target as any; - acum[i.propertyName] = - i.inverseEntityMetadata.primaryColumns[0].propertyName; - return acum; - }, {} as Record) as RelationPropsTypeName; -}; +) => + repository.metadata.relations.map((i) => { + return i.propertyName; + }) as TupleOfEntityRelation; -export const getRelationTypeName = ( +export const getProps = ( repository: Repository -): RelationPropsTypeName => { - return repository.metadata.relations.reduce((acum, i) => { - acum[i.propertyName] = getEntityName(i.inverseEntityMetadata.target); - return acum; - }, {} as Record) as RelationPropsTypeName; +): TupleOfEntityProps => { + const relations = getRelation(repository); + + return repository.metadata.columns + .filter((i) => !relations.includes(i.propertyName)) + .map((r) => r.propertyName) as TupleOfEntityProps; }; -export const getFieldWithType = ( +export const getPropsType = ( repository: Repository ): FieldWithType => { - const { field } = getField(repository); + const field = getProps(repository); const entity = repository.target as any; const result = {} as any; for (const item of field) { let typeProps: TypeField = TypeField.string; + + const fieldMetadata = repository.metadata.columns.find( + (i) => i.propertyName === item + ); + + if (fieldMetadata?.isArray) { + result[item] = TypeField.array; + continue; + } + switch (Reflect.getMetadata('design:type', entity['prototype'], item)) { case Array: typeProps = TypeField.array; @@ -198,201 +358,60 @@ export const getFieldWithType = ( default: typeProps = TypeField.string; } - result[item] = typeProps; + + result[item] = fieldMetadata?.isArray ? TypeField.array : typeProps; } return result; }; -export const getField = ( +export const getPropsNullable = ( repository: Repository -): ResultGetField => { - const relations = repository.metadata.relations.map((i) => { - return i.propertyName; - }); - - const field = repository.metadata.columns - .filter((i) => !relations.includes(i.propertyName)) - .map((r) => r.propertyName); - - return { - field, - relations, - } as unknown as ResultGetField; -}; - -export type PropsArray = { [K in EntityPropsArray]: true }; - -export const getArrayFields = ( - repository: Repository -): PropsArray => { - const relations = repository.metadata.relations.map((i) => { - return i.propertyName; - }); - +): FilterNullableProps> => { + const relation = getRelation(repository); return repository.metadata.columns - .filter((i) => !relations.includes(i.propertyName)) - .reduce((acum, metaData) => { - if (metaData.isArray) { - acum[metaData.propertyName] = true; - } - return acum; - }, {} as Record) as PropsArray; -}; - -export type ArrayPropsForEntity = { - target: PropsArray; -} & { - [K in ResultGetField['relations'][number]]: PropsArray< - TypeOfArray> - >; -}; - -export type PropertyTarget< - E extends Entity, - For extends PropsNameResultField -> = { - [K in ResultGetField[For][number]]: K extends keyof E - ? TypeOfArray - : never; -}; - -export const getArrayPropsForEntity = ( - repository: Repository -): ArrayPropsForEntity => { - const connection = repository.metadata.connection; - - const relationsTargets = repository.metadata.relations.reduce( - (acum, i) => ({ - ...acum, - [i.propertyName]: i.type, - }), - {} as Record - ) as PropertyTarget; - - const { relations } = getField(repository); - const relationsArrayFields = relations.reduce( - (acum, item) => { - guardKeyForPropertyTarget(relationsTargets, item); - const target = relationsTargets[item] as TypeCast< - TypeOfArray>, - Entity - >; - - const repository = connection.getRepository( - target as Function - ) as Repository; - - acum[item] = getArrayFields(repository) as PropsArray< - TypeOfArray> - >; - - return acum; - }, - {} as { - [K in ResultGetField['relations'][number]]: PropsArray< - TypeOfArray> - >; - } - ); - - return { - target: getArrayFields(repository), - ...relationsArrayFields, - }; + .filter((i) => !relation.includes(i.propertyName)) + .map((i) => + i.isNullable || i.default !== undefined ? i.propertyName : false + ) + .filter((i) => !!i) as FilterNullableProps>; }; -export const getPropsTreeForRepository = ( +export const getPrimaryColumnName = ( repository: Repository -): RelationTree => { - const dataSource = repository.metadata.connection; +) => { + const column = repository.metadata.primaryColumns.at(0); + if (!column) throw new Error('Primary column not found'); - const relationType = repository.metadata.relations.reduce((acum, i) => { - acum[i.propertyName] = i.inverseEntityMetadata.target; - return acum; - }, {} as Record) as unknown as RelationType; - - return ObjectTyped.entries(relationType).reduce( - (acum, [key, value]) => ({ - ...acum, - ...{ [key]: getField(dataSource.getRepository(value))['field'] }, - }), - {} as RelationTree - ); + return column.propertyName; }; -export const getIsArrayRelation = ( +export const getPrimaryColumnType = ( repository: Repository -): { - [K in EntityRelation]: E[K] extends unknown[] ? true : false; -} => { - return repository.metadata.relations.reduce((acum, i) => { - switch (i.relationType) { - case 'one-to-many': - case 'many-to-many': - acum[i.propertyName] = true; - break; - default: - acum[i.propertyName] = false; - } - return acum; - }, {} as any) as any; -}; - -export const fromRelationTreeToArrayName = ( - relationTree: RelationTree -): ConcatRelation => { - return ObjectTyped.entries(relationTree).reduce((acum, [name, filed]) => { - acum.push(...filed.map((i) => `${name.toLocaleString()}.${i}`)); - return acum; - }, [] as string[]) as unknown as ConcatRelation; -}; - -export type AllFieldWithTpe = FieldWithType & { - [K in EntityRelation]: E[K] extends (infer U extends Entity)[] - ? FieldWithType - : E[K] extends Entity - ? FieldWithType - : never; -}; - -export const getTypeForAllProps = ( - repository: Repository -): AllFieldWithTpe => { - const targetField = getFieldWithType(repository); - - const relationField = repository.metadata.relations.reduce((acum, item) => { - acum[item.propertyName] = getFieldWithType( - repository.manager.getRepository(item.inverseEntityMetadata.target) - ); - return acum; - }, {} as any); - - return { - ...targetField, - ...relationField, - }; -}; - -export type PropsFieldItem = { - type: ColumnType; - isArray: boolean; - isNullable: boolean; -}; +): TypeForId => { + const target = repository.target as any; + const primaryColumn = repository.metadata.primaryColumns[0].propertyName; -export type PropsForField = { - [K in EntityProps]: PropsFieldItem; + return Reflect.getMetadata( + 'design:type', + target['prototype'], + primaryColumn + ) === Number + ? TypeField.number + : TypeField.string; }; -export const getPropsFromDb = ( +export const getRelationProperty = ( repository: Repository -): PropsForField => { - return repository.metadata.columns.reduce((acum, i) => { - acum[i.propertyName as EntityProps] = { - type: i.type, - isArray: i.isArray, - isNullable: i.isNullable, +): RelationProperty => { + return repository.metadata.relations.reduce((acum, item) => { + // @ts-expect-error its dynamic creater + acum[item.propertyName] = { + entityClass: item.inverseEntityMetadata.target, + nullable: item.isManyToMany || item.isOneToMany ? false : item.isNullable, + isArray: item.isManyToMany || item.isOneToMany, }; + return acum; - }, {} as PropsForField); + }, {} as RelationProperty); }; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-one/delete-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-one/delete-one.spec.ts similarity index 51% rename from libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-one/delete-one.spec.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-one/delete-one.spec.ts index a52e18e5..c47d008e 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-one/delete-one.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-one/delete-one.spec.ts @@ -3,30 +3,34 @@ import { IMemoryDb } from 'pg-mem'; import { getDataSourceToken } from '@nestjs/typeorm'; import { - createAndPullSchemaBase, mockDBTestModule, providerEntities, -} from '../../../../mock-utils'; -import { CurrentDataSourceProvider } from '../../../../factory'; -import { DEFAULT_CONNECTION_NAME } from '../../../../constants'; -import { TypeormService } from '../../../../types'; -import { getRepository, pullUser, Users } from '../../../../mock-utils'; +} from '../../../../mock-utils/typeorm'; import { - TransformDataService, - TypeormUtilsService, -} from '../../../../mixin/service'; -import { Repository } from 'typeorm'; -import { CONTROL_OPTIONS_TOKEN, TYPEORM_SERVICE } from '../../../../constants'; + CurrentDataSourceProvider, + CurrentEntityManager, + CurrentEntityRepository, + OrmServiceFactory, +} from '../../factory'; +import { CURRENT_ENTITY, DEFAULT_CONNECTION_NAME } from '../../../../constants'; + import { - EntityRepositoryFactory, - TypeormServiceFactory, -} from '../../../../factory'; -import { EntityPropsMapService } from '../../../../service'; + getRepository, + pullUser, + Users, + entities, +} from '../../../../mock-utils/typeorm'; + +import { Repository } from 'typeorm'; +import { CONTROL_OPTIONS_TOKEN, ORM_SERVICE } from '../../../../constants'; +import { JsonApiTransformerService } from '../../../mixin/service/json-api-transformer.service'; +import { TypeOrmService, TypeormUtilsService } from '../../service'; +import { createAndPullSchemaBase } from '../../../../mock-utils'; +import { EntityPropsMap } from '../../factory'; describe('deleteOne', () => { let db: IMemoryDb; - let typeormService: TypeormService; - let transformDataService: TransformDataService; + let typeormService: TypeOrmService; let user: Users; let userRepository: Repository; @@ -38,6 +42,10 @@ describe('deleteOne', () => { providers: [ ...providerEntities(getDataSourceToken()), CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), + { + provide: CURRENT_ENTITY, + useValue: Users, + }, { provide: CONTROL_OPTIONS_TOKEN, useValue: { @@ -45,18 +53,17 @@ describe('deleteOne', () => { debug: false, }, }, - EntityRepositoryFactory(Users), + EntityPropsMap(entities as any), + CurrentEntityManager(), + CurrentEntityRepository(Users), TypeormUtilsService, - TransformDataService, - TypeormServiceFactory(Users), - EntityPropsMapService, + JsonApiTransformerService, + OrmServiceFactory(), ], }).compile(); ({ userRepository } = getRepository(module)); user = await pullUser(userRepository); - typeormService = module.get>(TYPEORM_SERVICE); - transformDataService = - module.get>(TransformDataService); + typeormService = module.get>(ORM_SERVICE); }); it('Should be ok', async () => { diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-one/delete-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-one/delete-one.ts similarity index 69% rename from libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-one/delete-one.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-one/delete-one.ts index 573a7ed4..1a6871d7 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-one/delete-one.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-one/delete-one.ts @@ -1,8 +1,9 @@ -import { Entity, TypeormServiceObject } from '../../../../types'; import { FindOptionsWhere } from 'typeorm'; +import { TypeOrmService } from '../../service'; +import { ObjectLiteral } from '../../../../types'; -export async function deleteOne( - this: TypeormServiceObject, +export async function deleteOne( + this: TypeOrmService, id: number | string ): Promise { const data = await this.repository.findOne({ diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-relationship/delete-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.spec.ts similarity index 80% rename from libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-relationship/delete-relationship.spec.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.spec.ts index 8c45cdc0..627f0917 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-relationship/delete-relationship.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.spec.ts @@ -3,11 +3,10 @@ import { Test, TestingModule } from '@nestjs/testing'; import { IMemoryDb } from 'pg-mem'; import { Repository } from 'typeorm'; -import { TypeormService } from '../../../../types'; import { Addresses, Comments, - createAndPullSchemaBase, + entities, getRepository, mockDBTestModule, Notes, @@ -16,28 +15,29 @@ import { Roles, UserGroups, Users, -} from '../../../../mock-utils'; -import { - TransformDataService, - TypeormUtilsService, -} from '../../../../mixin/service'; +} from '../../../../mock-utils/typeorm'; import { CONTROL_OPTIONS_TOKEN, + CURRENT_ENTITY, DEFAULT_CONNECTION_NAME, - TYPEORM_SERVICE, + ORM_SERVICE, } from '../../../../constants'; import { CurrentDataSourceProvider, - EntityRepositoryFactory, - TypeormServiceFactory, -} from '../../../../factory'; -import { EntityPropsMapService } from '../../../../service'; + CurrentEntityManager, + CurrentEntityRepository, + EntityPropsMap, + OrmServiceFactory, +} from '../../factory'; +import { JsonApiTransformerService } from '../../../mixin/service/json-api-transformer.service'; +import { TypeOrmService, TypeormUtilsService } from '../../service'; +import { createAndPullSchemaBase } from '../../../../mock-utils'; describe('deleteRelationship', () => { let db: IMemoryDb; - let typeormService: TypeormService; - let transformDataService: TransformDataService; + let typeormService: TypeOrmService; + let transformDataService: JsonApiTransformerService; let typeormUtilsService: TypeormUtilsService; let userRepository: Repository; let addressesRepository: Repository; @@ -60,11 +60,16 @@ describe('deleteRelationship', () => { debug: false, }, }, - EntityRepositoryFactory(Users), + { + provide: CURRENT_ENTITY, + useValue: Users, + }, + EntityPropsMap(entities as any), + CurrentEntityManager(), + CurrentEntityRepository(Users), TypeormUtilsService, - TransformDataService, - TypeormServiceFactory(Users), - EntityPropsMapService, + JsonApiTransformerService, + OrmServiceFactory(), ], }).compile(); ({ @@ -83,9 +88,10 @@ describe('deleteRelationship', () => { rolesRepository, userGroupRepository ); - typeormService = module.get>(TYPEORM_SERVICE); - transformDataService = - module.get>(TransformDataService); + typeormService = module.get>(ORM_SERVICE); + transformDataService = module.get>( + JsonApiTransformerService + ); typeormUtilsService = module.get>(TypeormUtilsService); }); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-relationship/delete-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.ts similarity index 64% rename from libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-relationship/delete-relationship.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.ts index 427ed6cb..947553b5 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-relationship/delete-relationship.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.ts @@ -1,16 +1,15 @@ -import { - Entity, - TypeormServiceObject, - EntityRelation, -} from '../../../../types'; +import { EntityRelation } from '../../../../utils/nestjs-shared'; -import { PostRelationshipData } from '../../../zod'; +import { ObjectLiteral } from '../../../../types'; + +import { PostRelationshipData } from '../../../mixin/zod'; +import { TypeOrmService } from '../../service'; export async function deleteRelationship< - E extends Entity, + E extends ObjectLiteral, Rel extends EntityRelation >( - this: TypeormServiceObject, + this: TypeOrmService, id: number | string, rel: Rel, input: PostRelationshipData diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-all/get-all.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.spec.ts similarity index 82% rename from libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-all/get-all.spec.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.spec.ts index 953d8b91..3dd8ea02 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-all/get-all.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.spec.ts @@ -1,11 +1,13 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { IMemoryDb } from 'pg-mem'; import { getDataSourceToken } from '@nestjs/typeorm'; +import { QueryField } from '../../../../utils/nestjs-shared'; +import { Equal, Repository } from 'typeorm'; +import { IMemoryDb } from 'pg-mem'; import { Addresses, Comments, - createAndPullSchemaBase, + entities, getRepository, mockDBTestModule, Notes, @@ -14,27 +16,29 @@ import { Roles, UserGroups, Users, -} from '../../../../mock-utils'; +} from '../../../../mock-utils/typeorm'; import { CurrentDataSourceProvider, - EntityRepositoryFactory, - TypeormServiceFactory, -} from '../../../../factory'; + CurrentEntityManager, + CurrentEntityRepository, + EntityPropsMap, + OrmServiceFactory, +} from '../../factory'; import { CONTROL_OPTIONS_TOKEN, DEFAULT_CONNECTION_NAME, - TYPEORM_SERVICE, + ORM_SERVICE, DEFAULT_QUERY_PAGE, DEFAULT_PAGE_SIZE, + CURRENT_ENTITY, } from '../../../../constants'; -import { Entity, TypeormService } from '../../../../types'; -import { Query, QueryField } from '../../../../helper'; -import { - TransformDataService, - TypeormUtilsService, -} from '../../../../mixin/service'; -import { Equal, IsNull, Repository } from 'typeorm'; -import { EntityPropsMapService } from '../../../../service'; +import { ObjectLiteral as Entity } from '../../../../types'; + +import { Query } from '../../../mixin/zod'; + +import { TypeOrmService, TypeormUtilsService } from '../../service'; +import { createAndPullSchemaBase } from '../../../../mock-utils'; +import { JsonApiTransformerService } from '../../../mixin/service/json-api-transformer.service'; function getDefaultQuery() { const filter = { @@ -57,8 +61,8 @@ function getDefaultQuery() { describe('getAll', () => { let db: IMemoryDb; - let typeormService: TypeormService; - let transformDataService: TransformDataService; + let typeormService: TypeOrmService; + let transformDataService: JsonApiTransformerService; let userRepository: Repository; let addressesRepository: Repository; @@ -81,11 +85,16 @@ describe('getAll', () => { debug: false, }, }, - EntityRepositoryFactory(Users), + { + provide: CURRENT_ENTITY, + useValue: Users, + }, + EntityPropsMap(entities as any), + CurrentEntityManager(), + CurrentEntityRepository(Users), TypeormUtilsService, - TransformDataService, - TypeormServiceFactory(Users), - EntityPropsMapService, + JsonApiTransformerService, + OrmServiceFactory(), ], }).compile(); ({ @@ -104,9 +113,10 @@ describe('getAll', () => { rolesRepository, userGroupRepository ); - typeormService = module.get>(TYPEORM_SERVICE); - transformDataService = - module.get>(TransformDataService); + typeormService = module.get>(ORM_SERVICE); + transformDataService = module.get>( + JsonApiTransformerService + ); }); afterEach(() => { @@ -144,7 +154,7 @@ describe('getAll', () => { }, }; await typeormService.getAll(query); - expect(spyOnTransformData).toBeCalledWith(checkData); + expect(spyOnTransformData).toBeCalledWith(checkData, query); }); it('include', async () => { @@ -171,7 +181,7 @@ describe('getAll', () => { }, }; await typeormService.getAll(query); - expect(spyOnTransformData).toBeCalledWith([checkData]); + expect(spyOnTransformData).toBeCalledWith([checkData], query); }); it('select', async () => { @@ -215,7 +225,7 @@ describe('getAll', () => { }, }; await typeormService.getAll(query); - expect(spyOnTransformData).toBeCalledWith([checkData]); + expect(spyOnTransformData).toBeCalledWith([checkData], query); }); describe('filter', () => { @@ -244,7 +254,7 @@ describe('getAll', () => { const query = getDefaultQuery(); query.filter.target = { id: { eq: '1' }, - firstName: {eq: null}, + firstName: { eq: null }, }; await typeormService.getAll(query); expect(spyOnTransformData).toHaveBeenCalledTimes(0); @@ -265,7 +275,7 @@ describe('getAll', () => { id: { eq: `${checkData?.id}` }, }; await typeormService.getAll(query); - expect(spyOnTransformData).toBeCalledWith([checkData]); + expect(spyOnTransformData).toBeCalledWith([checkData], query); }); it('Check relation with the same Entity', async () => { @@ -293,7 +303,7 @@ describe('getAll', () => { }, }; await typeormService.getAll(query); - expect(spyOnTransformData).toBeCalledWith([checkData]); + expect(spyOnTransformData).toBeCalledWith([checkData], query); }); // it('Target relation is null', async () => { @@ -335,7 +345,7 @@ describe('getAll', () => { }; query.include = ['manager']; await typeormService.getAll(query); - expect(spyOnTransformData).toBeCalledWith([checkData]); + expect(spyOnTransformData).toBeCalledWith([checkData], query); }); it('Relation one-to-many', async () => { @@ -363,7 +373,7 @@ describe('getAll', () => { }, }; await typeormService.getAll(query); - expect(spyOnTransformData).toBeCalledWith([checkData]); + expect(spyOnTransformData).toBeCalledWith([checkData], query); }); it('Relation many-to-many', async () => { @@ -393,7 +403,7 @@ describe('getAll', () => { }, }; const { data } = await typeormService.getAll(query); - expect(spyOnTransformData).not.toBeCalled(); + expect(spyOnTransformData).not.toHaveBeenCalled(); expect(data).toEqual([]); }); }); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-all/get-all.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.ts similarity index 92% rename from libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-all/get-all.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.ts index e0e60fb9..e6c9fa3e 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-all/get-all.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.ts @@ -1,14 +1,15 @@ -import { Entity, TypeormServiceObject } from '../../../../types'; -import { Query } from '../../../zod'; -import { ObjectTyped } from '../../../utils'; -import { TupleOfEntityRelation } from '../../orm-helper'; +import { ObjectTyped, ResourceObject } from '../../../../utils/nestjs-shared'; + +import { ObjectLiteral } from '../../../../types'; +import { Query } from '../../../mixin/zod'; +import { TypeOrmService } from '../../service'; +import { TupleOfEntityRelation } from '../../../mixin/types'; +import { ASC, DESC } from '../../../../constants'; + import { ALIAS_FOR_PAGINATION, - ASC, - DESC, SUB_QUERY_ALIAS_FOR_PAGINATION, -} from '../../../../constants'; -import { ResourceObject } from '../../../../types/response'; +} from '../../constants'; type OrderByCondition = Record; @@ -19,8 +20,8 @@ function getSortObject(params: any, relationName: string) { }, {} as OrderByCondition); } -export async function getAll( - this: TypeormServiceObject, +export async function getAll( + this: TypeOrmService, query: Query ): Promise> { const { fields, filter, include, sort, page } = query; @@ -249,8 +250,10 @@ export async function getAll( ); } const resultData = await resultQuery.getMany(); - const { included, data } = - this.transformDataService.transformData(resultData); + const { included, data } = this.transformDataService.transformData( + resultData, + query + ); return { meta: { pageNumber: page.number, diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-one/get-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.spec.ts similarity index 75% rename from libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-one/get-one.spec.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.spec.ts index 37dde9b2..97fb1f30 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-one/get-one.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.spec.ts @@ -1,13 +1,14 @@ import { getDataSourceToken } from '@nestjs/typeorm'; import { Test, TestingModule } from '@nestjs/testing'; +import { QueryField } from '../../../../utils/nestjs-shared'; import { IMemoryDb } from 'pg-mem'; import { Equal, Repository } from 'typeorm'; -import { Entity, TypeormService } from '../../../../types'; +import { ObjectLiteral as Entity } from '../../../../types'; import { Addresses, Comments, - createAndPullSchemaBase, + entities, getRepository, mockDBTestModule, Notes, @@ -16,27 +17,28 @@ import { Roles, UserGroups, Users, -} from '../../../../mock-utils'; -import { - TransformDataService, - TypeormUtilsService, -} from '../../../../mixin/service'; +} from '../../../../mock-utils/typeorm'; import { CONTROL_OPTIONS_TOKEN, + CURRENT_ENTITY, DEFAULT_CONNECTION_NAME, DEFAULT_PAGE_SIZE, DEFAULT_QUERY_PAGE, - TYPEORM_SERVICE, + ORM_SERVICE, } from '../../../../constants'; import { CurrentDataSourceProvider, - EntityRepositoryFactory, - TypeormServiceFactory, -} from '../../../../factory'; -import { Query, QueryField } from '../../../zod'; + CurrentEntityManager, + CurrentEntityRepository, + EntityPropsMap, + OrmServiceFactory, +} from '../../factory'; +import { Query } from '../../../mixin/zod'; import { NotFoundException } from '@nestjs/common'; -import { EntityPropsMapService } from '../../../../service'; +import { JsonApiTransformerService } from '../../../mixin/service/json-api-transformer.service'; +import { TypeOrmService, TypeormUtilsService } from '../../service'; +import { createAndPullSchemaBase } from '../../../../mock-utils'; function getDefaultQuery() { const defaultQuery: Query = { @@ -58,8 +60,8 @@ function getDefaultQuery() { describe('getOne', () => { let db: IMemoryDb; - let typeormService: TypeormService; - let transformDataService: TransformDataService; + let typeormService: TypeOrmService; + let transformDataService: JsonApiTransformerService; let userRepository: Repository; let addressesRepository: Repository; @@ -82,11 +84,16 @@ describe('getOne', () => { debug: false, }, }, - EntityRepositoryFactory(Users), + { + provide: CURRENT_ENTITY, + useValue: Users, + }, + EntityPropsMap(entities as any), + CurrentEntityManager(), + CurrentEntityRepository(Users), TypeormUtilsService, - TransformDataService, - TypeormServiceFactory(Users), - EntityPropsMapService, + JsonApiTransformerService, + OrmServiceFactory(), ], }).compile(); ({ @@ -105,9 +112,10 @@ describe('getOne', () => { rolesRepository, userGroupRepository ); - typeormService = module.get>(TYPEORM_SERVICE); - transformDataService = - module.get>(TransformDataService); + typeormService = module.get>(ORM_SERVICE); + transformDataService = module.get>( + JsonApiTransformerService + ); }); afterEach(() => { @@ -132,7 +140,7 @@ describe('getOne', () => { }); query.include = ['addresses', 'comments']; await typeormService.getOne('1', query); - expect(spyOnTransformData).toBeCalledWith(checkData); + expect(spyOnTransformData).toBeCalledWith(checkData, query); }); it('Get one item with select', async () => { const spyOnTransformData = jest.spyOn( @@ -173,7 +181,7 @@ describe('getOne', () => { manager: ['login'], }; await typeormService.getOne('1', query); - expect(spyOnTransformData).toBeCalledWith(checkData); + expect(spyOnTransformData).toBeCalledWith(checkData, query); }); it('Should be error', async () => { expect.assertions(1); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-one/get-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.ts similarity index 85% rename from libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-one/get-one.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.ts index 7e96c887..b092393f 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-one/get-one.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.ts @@ -1,17 +1,13 @@ import { NotFoundException } from '@nestjs/common'; -import { - Entity, - ResourceObject, - TypeormServiceObject, - ValidateQueryError, -} from '../../../../types'; -import { Query } from '../../../zod'; -import { ObjectTyped } from '../../../utils'; +import { ObjectTyped, ResourceObject } from '../../../../utils/nestjs-shared'; +import { ObjectLiteral, ValidateQueryError } from '../../../../types'; +import { QueryOne } from '../../../mixin/zod'; +import { TypeOrmService } from '../../service'; -export async function getOne( - this: TypeormServiceObject, +export async function getOne( + this: TypeOrmService, id: number | string, - query: Query + query: QueryOne ): Promise> { const { include, fields } = query; const selectFields = new Set(); @@ -90,7 +86,10 @@ export async function getOne( }; throw new NotFoundException([error]); } - const { included, data } = this.transformDataService.transformData(result); + const { included, data } = this.transformDataService.transformData( + result, + query + ); return { meta: {}, data, diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-relationship/get-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.spec.ts similarity index 70% rename from libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-relationship/get-relationship.spec.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.spec.ts index 068cef6b..979323d1 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-relationship/get-relationship.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.spec.ts @@ -3,11 +3,10 @@ import { Test, TestingModule } from '@nestjs/testing'; import { IMemoryDb } from 'pg-mem'; import { Repository } from 'typeorm'; -import { TypeormService } from '../../../../types'; import { Addresses, Comments, - createAndPullSchemaBase, + entities, getRepository, mockDBTestModule, Notes, @@ -16,30 +15,31 @@ import { Roles, UserGroups, Users, -} from '../../../../mock-utils'; -import { - TransformDataService, - TypeormUtilsService, -} from '../../../../mixin/service'; +} from '../../../../mock-utils/typeorm'; import { CONTROL_OPTIONS_TOKEN, + CURRENT_ENTITY, DEFAULT_CONNECTION_NAME, - TYPEORM_SERVICE, + ORM_SERVICE, } from '../../../../constants'; import { CurrentDataSourceProvider, - EntityRepositoryFactory, - TypeormServiceFactory, -} from '../../../../factory'; + CurrentEntityManager, + CurrentEntityRepository, + EntityPropsMap, + OrmServiceFactory, +} from '../../factory'; import { NotFoundException } from '@nestjs/common'; -import { EntityPropsMapService } from '../../../../service'; +import { TypeOrmService, TypeormUtilsService } from '../../service'; +import { createAndPullSchemaBase } from '../../../../mock-utils'; +import { JsonApiTransformerService } from '../../../mixin/service/json-api-transformer.service'; describe('getRelationship', () => { let db: IMemoryDb; - let typeormService: TypeormService; - let transformDataService: TransformDataService; + let typeormService: TypeOrmService; + let transformDataService: JsonApiTransformerService; let userRepository: Repository; let addressesRepository: Repository; @@ -62,11 +62,16 @@ describe('getRelationship', () => { debug: false, }, }, - EntityRepositoryFactory(Users), + { + provide: CURRENT_ENTITY, + useValue: Users, + }, + EntityPropsMap(entities as any), + CurrentEntityManager(), + CurrentEntityRepository(Users), TypeormUtilsService, - TransformDataService, - TypeormServiceFactory(Users), - EntityPropsMapService, + JsonApiTransformerService, + OrmServiceFactory(), ], }).compile(); ({ @@ -85,9 +90,10 @@ describe('getRelationship', () => { rolesRepository, userGroupRepository ); - typeormService = module.get>(TYPEORM_SERVICE); - transformDataService = - module.get>(TransformDataService); + typeormService = module.get>(ORM_SERVICE); + transformDataService = module.get>( + JsonApiTransformerService + ); }); afterEach(() => { @@ -96,10 +102,7 @@ describe('getRelationship', () => { }); it('Should be ok', async () => { - const spyOnTransformData = jest.spyOn( - transformDataService, - 'getRelationships' - ); + const spyOnTransformData = jest.spyOn(transformDataService, 'transformRel'); const id = 1; const rel = 'roles'; const check = await userRepository.findOne({ diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-relationship/get-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.ts similarity index 75% rename from libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-relationship/get-relationship.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.ts index 0478c488..be447e90 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-relationship/get-relationship.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.ts @@ -1,20 +1,20 @@ import { - Entity, - TypeormServiceObject, EntityRelation, - ValidateQueryError, ResourceObjectRelationships, -} from '../../../../types'; +} from '../../../../utils/nestjs-shared'; + import { InternalServerErrorException, NotFoundException, } from '@nestjs/common'; +import { TypeOrmService } from '../../service'; +import { ObjectLiteral, ValidateQueryError } from '../../../../types'; export async function getRelationship< - E extends Entity, + E extends ObjectLiteral, Rel extends EntityRelation >( - this: TypeormServiceObject, + this: TypeOrmService, id: number | string, rel: Rel ): Promise> { @@ -54,16 +54,8 @@ export async function getRelationship< }; throw new NotFoundException([error]); } + const data = this.transformDataService.transformRel(result, rel); - const { data } = this.transformDataService.getRelationships(result, rel); - if (data === undefined) { - const error: ValidateQueryError = { - code: 'custom', - message: `transformDataService.getRelationships return undefined`, - path: [], - }; - throw new InternalServerErrorException([error]); - } return { meta: {}, data, diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/index.ts new file mode 100644 index 00000000..92f7bb57 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/index.ts @@ -0,0 +1,9 @@ +export { getAll } from './get-all/get-all'; +export { getOne } from './get-one/get-one'; +export { deleteOne } from './delete-one/delete-one'; +export { postOne } from './post-one/post-one'; +export { patchOne } from './patch-one/patch-one'; +export { getRelationship } from './get-relationship/get-relationship'; +export { postRelationship } from './post-relationship/post-relationship'; +export { deleteRelationship } from './delete-relationship/delete-relationship'; +export { patchRelationship } from './patch-relationship/patch-relationship'; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-one/patch-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.spec.ts similarity index 65% rename from libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-one/patch-one.spec.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.spec.ts index 38cb8cf3..7e8ca456 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-one/patch-one.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.spec.ts @@ -6,7 +6,7 @@ import { Repository } from 'typeorm'; import { Addresses, Comments, - createAndPullSchemaBase, + entities, getRepository, mockDBTestModule, Notes, @@ -15,31 +15,31 @@ import { Roles, UserGroups, Users, -} from '../../../../mock-utils'; +} from '../../../../mock-utils/typeorm'; import { CurrentDataSourceProvider, - EntityRepositoryFactory, - TypeormServiceFactory, -} from '../../../../factory'; + CurrentEntityManager, + CurrentEntityRepository, + EntityPropsMap, + OrmServiceFactory, +} from '../../factory'; import { CONTROL_OPTIONS_TOKEN, + CURRENT_ENTITY, DEFAULT_CONNECTION_NAME, - TYPEORM_SERVICE, + ORM_SERVICE, } from '../../../../constants'; -import { TypeormService } from '../../../../types'; -import { PatchData, PostData } from '../../../../helper/zod'; -import { - TransformDataService, - TypeormUtilsService, -} from '../../../../mixin/service'; -import { EntityPropsMapService } from '../../../../service'; +import { PatchData, PostData } from '../../../mixin/zod'; +import { TypeOrmService, TypeormUtilsService } from '../../service'; +import { createAndPullSchemaBase } from '../../../../mock-utils'; +import { JsonApiTransformerService } from '../../../mixin/service/json-api-transformer.service'; describe('patchOne', () => { let db: IMemoryDb; let backaUp: IBackup; - let typeormService: TypeormService; - let transformDataService: TransformDataService; + let typeormService: TypeOrmService; + let transformDataService: JsonApiTransformerService; let userRepository: Repository; let addressesRepository: Repository; @@ -76,12 +76,16 @@ describe('patchOne', () => { debug: false, }, }, - EntityRepositoryFactory(Users), - EntityRepositoryFactory(Users), + { + provide: CURRENT_ENTITY, + useValue: Users, + }, + EntityPropsMap(entities as any), + CurrentEntityManager(), + CurrentEntityRepository(Users), TypeormUtilsService, - TransformDataService, - TypeormServiceFactory(Users), - EntityPropsMapService, + JsonApiTransformerService, + OrmServiceFactory(), ], }).compile(); @@ -102,9 +106,10 @@ describe('patchOne', () => { userGroupRepository ); - typeormService = module.get>(TYPEORM_SERVICE); - transformDataService = - module.get>(TransformDataService); + typeormService = module.get>(ORM_SERVICE); + transformDataService = module.get>( + JsonApiTransformerService + ); notes = await notesRepository.find(); users = await userRepository.find(); @@ -126,28 +131,38 @@ describe('patchOne', () => { }, relationships: { addresses: { - type: 'addresses', - id: addresses[0].id.toString(), - }, - notes: [ - { - type: 'notes', - id: notes[0].id, + data: { + type: 'addresses', + id: addresses[0].id.toString(), }, - ], - roles: [ - { - type: 'roles', - id: `${roles[0].id}`, - }, - ], + }, + notes: { + data: [ + { + type: 'notes', + id: notes[0].id, + }, + ], + }, + roles: { + data: [ + { + type: 'roles', + id: `${roles[0].id}`, + }, + ], + }, manager: { - type: 'users', - id: `${users[0].id}`, + data: { + type: 'users', + id: `${users[0].id}`, + }, }, userGroup: { - type: 'user-group', - id: `${userGroup[0].id}`, + data: { + type: 'user-group', + id: `${userGroup[0].id}`, + }, }, }, }; @@ -174,20 +189,28 @@ describe('patchOne', () => { newData.relationships = { ...newData.relationships, manager: { - type: 'users', - id: users[1].id.toString(), + data: { + type: 'users', + id: users[1].id.toString(), + }, }, - addresses: null, - userGroup: { - type: 'user-group', - id: `${userGroup[1].id}`, + addresses: { + data: null, }, - roles: [ - { - type: 'roles', - id: `${roles[1].id}`, + userGroup: { + data: { + type: 'user-group', + id: `${userGroup[1].id}`, }, - ], + }, + roles: { + data: [ + { + type: 'roles', + id: `${roles[1].id}`, + }, + ], + }, }; }); @@ -206,14 +229,17 @@ describe('patchOne', () => { const { relationships, ...withoutRelationships } = newData; const returnData = await typeormService.patchOne( - withoutRelationships.id, + withoutRelationships.id as string, withoutRelationships ); const result = await userRepository.findOneBy({ - id: parseInt(withoutRelationships.id, 10), + id: parseInt(withoutRelationships.id as string, 10), + }); + expect(spyOnTransformData).toBeCalledWith(result, { + fields: null, + include: [], }); - expect(spyOnTransformData).toBeCalledWith(result); expect(returnData).not.toHaveProperty('included'); }); @@ -225,11 +251,14 @@ describe('patchOne', () => { included: {} as any, })); - const returnData = await typeormService.patchOne(newData.id, newData); + const returnData = await typeormService.patchOne( + newData.id as string, + newData + ); const result = await userRepository.findOne({ where: { - id: parseInt(newData.id, 10), + id: parseInt(newData.id as string, 10), }, relations: { addresses: true, @@ -240,7 +269,10 @@ describe('patchOne', () => { }, }); - expect(spyOnTransformData).toBeCalledWith(result); + expect(spyOnTransformData).toBeCalledWith(result, { + fields: null, + include: ['addresses', 'notes', 'roles', 'manager', 'userGroup'], + }); expect(returnData).toHaveProperty('included'); }); @@ -254,15 +286,22 @@ describe('patchOne', () => { newData.relationships = { ...newData.relationships, - userGroup: null, - roles: [], + userGroup: { + data: null, + }, + roles: { + data: [], + }, }; - const returnData = await typeormService.patchOne(newData.id, newData); + const returnData = await typeormService.patchOne( + newData.id as string, + newData + ); const result = await userRepository.findOne({ where: { - id: parseInt(newData.id, 10), + id: parseInt(newData.id as string, 10), }, relations: { addresses: true, @@ -273,7 +312,10 @@ describe('patchOne', () => { }, }); - expect(spyOnTransformData).toBeCalledWith(result); + expect(spyOnTransformData).toBeCalledWith(result, { + fields: null, + include: ['addresses', 'notes', 'roles', 'manager', 'userGroup'], + }); expect(returnData).toHaveProperty('included'); }); }); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-one/patch-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.ts similarity index 76% rename from libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-one/patch-one.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.ts index 6045e02f..5551a31b 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-one/patch-one.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.ts @@ -4,16 +4,17 @@ import { } from '@nestjs/common'; import { DeepPartial } from 'typeorm'; import { - Entity, ResourceObject, - TypeormServiceObject, - ValidateQueryError, -} from '../../../../types'; -import { PatchData } from '../../../zod'; -import { ObjectTyped } from '../../../utils'; + ObjectTyped, + QueryField, +} from '../../../../utils/nestjs-shared'; -export async function patchOne( - this: TypeormServiceObject, +import { ObjectLiteral, ValidateQueryError } from '../../../../types'; +import { PatchData, Query } from '../../../mixin/zod'; +import { TypeOrmService } from '../../service'; + +export async function patchOne( + this: TypeOrmService, id: number | string, inputData: PatchData ): Promise> { @@ -66,7 +67,15 @@ export async function patchOne( relationships ); - const { data, included } = this.transformDataService.transformData(saveData); + const fakeQuery: Query = { + [QueryField.fields]: null, + [QueryField.include]: Object.keys(relationships || {}), + } as any; + + const { data, included } = this.transformDataService.transformData( + saveData, + fakeQuery + ); const includeData = included ? { included } : {}; return { meta: {}, diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-relationship/patch-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.spec.ts similarity index 83% rename from libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-relationship/patch-relationship.spec.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.spec.ts index a7c55db0..4e3c326a 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-relationship/patch-relationship.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.spec.ts @@ -3,11 +3,10 @@ import { Test, TestingModule } from '@nestjs/testing'; import { IMemoryDb } from 'pg-mem'; import { Repository } from 'typeorm'; -import { TypeormService } from '../../../../types'; import { Addresses, Comments, - createAndPullSchemaBase, + entities, getRepository, mockDBTestModule, Notes, @@ -16,28 +15,29 @@ import { Roles, UserGroups, Users, -} from '../../../../mock-utils'; -import { - TransformDataService, - TypeormUtilsService, -} from '../../../../mixin/service'; +} from '../../../../mock-utils/typeorm'; import { CONTROL_OPTIONS_TOKEN, + CURRENT_ENTITY, DEFAULT_CONNECTION_NAME, - TYPEORM_SERVICE, + ORM_SERVICE, } from '../../../../constants'; import { CurrentDataSourceProvider, - EntityRepositoryFactory, - TypeormServiceFactory, -} from '../../../../factory'; -import { EntityPropsMapService } from '../../../../service'; + CurrentEntityManager, + CurrentEntityRepository, + EntityPropsMap, + OrmServiceFactory, +} from '../../factory'; +import { TypeOrmService, TypeormUtilsService } from '../../service'; +import { createAndPullSchemaBase } from '../../../../mock-utils'; +import { JsonApiTransformerService } from '../../../mixin/service/json-api-transformer.service'; describe('patchRelationship', () => { let db: IMemoryDb; - let typeormService: TypeormService; - let transformDataService: TransformDataService; + let typeormService: TypeOrmService; + let transformDataService: JsonApiTransformerService; let typeormUtilsService: TypeormUtilsService; let userRepository: Repository; let addressesRepository: Repository; @@ -60,11 +60,16 @@ describe('patchRelationship', () => { debug: false, }, }, - EntityRepositoryFactory(Users), + { + provide: CURRENT_ENTITY, + useValue: Users, + }, + EntityPropsMap(entities as any), + CurrentEntityManager(), + CurrentEntityRepository(Users), TypeormUtilsService, - TransformDataService, - TypeormServiceFactory(Users), - EntityPropsMapService, + JsonApiTransformerService, + OrmServiceFactory(), ], }).compile(); ({ @@ -83,9 +88,10 @@ describe('patchRelationship', () => { rolesRepository, userGroupRepository ); - typeormService = module.get>(TYPEORM_SERVICE); - transformDataService = - module.get>(TransformDataService); + typeormService = module.get>(ORM_SERVICE); + transformDataService = module.get>( + JsonApiTransformerService + ); typeormUtilsService = module.get>(TypeormUtilsService); }); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-relationship/patch-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.ts similarity index 77% rename from libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-relationship/patch-relationship.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.ts index 13f77e8a..2ccbb0dd 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-relationship/patch-relationship.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.ts @@ -1,18 +1,19 @@ import { - Entity, - TypeormServiceObject, EntityRelation, ResourceObjectRelationships, -} from '../../../../types'; +} from '../../../../utils/nestjs-shared'; -import { PatchRelationshipData } from '../../../zod'; +import { ObjectLiteral } from '../../../../types'; + +import { PatchRelationshipData } from '../../../mixin/zod'; import { getRelationship } from '../get-relationship/get-relationship'; +import { TypeOrmService } from '../../service'; export async function patchRelationship< - E extends Entity, + E extends ObjectLiteral, Rel extends EntityRelation >( - this: TypeormServiceObject, + this: TypeOrmService, id: number | string, rel: Rel, input: PatchRelationshipData @@ -29,7 +30,7 @@ export async function patchRelationship< if (Array.isArray(idsResult)) { const data = await getRelationship.call< - TypeormServiceObject, + TypeOrmService, [number | string, Rel], Promise> >(this, id, rel); @@ -43,7 +44,7 @@ export async function patchRelationship< } return getRelationship.call< - TypeormServiceObject, + TypeOrmService, [number | string, Rel], Promise> >(this, id, rel); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-one/post-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.spec.ts similarity index 65% rename from libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-one/post-one.spec.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.spec.ts index 084e375f..e3deab69 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-one/post-one.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.spec.ts @@ -6,7 +6,7 @@ import { Repository } from 'typeorm'; import { Addresses, Comments, - createAndPullSchemaBase, + entities, getRepository, mockDBTestModule, Notes, @@ -16,35 +16,35 @@ import { Roles, UserGroups, Users, -} from '../../../../mock-utils'; -import { - CurrentDataSourceProvider, - EntityRepositoryFactory, - TypeormServiceFactory, -} from '../../../../factory'; +} from '../../../../mock-utils/typeorm'; import { CONTROL_OPTIONS_TOKEN, + CURRENT_ENTITY, DEFAULT_CONNECTION_NAME, - TYPEORM_SERVICE, + ORM_SERVICE, } from '../../../../constants'; -import { TypeormService } from '../../../../types'; -import { PostData } from '../../../../helper/zod'; +import { PostData } from '../../../mixin/zod'; import { - TransformDataService, - TypeormUtilsService, -} from '../../../../mixin/service'; -import { EntityPropsMapService } from '../../../../service'; + CurrentDataSourceProvider, + CurrentEntityManager, + CurrentEntityRepository, + EntityPropsMap, + OrmServiceFactory, +} from '../../factory'; +import { TypeOrmService, TypeormUtilsService } from '../../service'; +import { createAndPullSchemaBase } from '../../../../mock-utils'; +import { JsonApiTransformerService } from '../../../mixin/service/json-api-transformer.service'; describe('postOne', () => { let db: IMemoryDb; let backaUp: IBackup; - let typeormService: TypeormService; - let transformDataService: TransformDataService; + let typeormService: TypeOrmService; + let transformDataService: JsonApiTransformerService; let podsRepository: Repository; - let typeormServicePods: TypeormService; - let transformDataServicePods: TransformDataService; + let typeormServicePods: TypeOrmService; + let transformDataServicePods: JsonApiTransformerService; let userRepository: Repository; let addressesRepository: Repository; @@ -79,11 +79,16 @@ describe('postOne', () => { debug: false, }, }, - EntityRepositoryFactory(Users), + { + provide: CURRENT_ENTITY, + useValue: Users, + }, + EntityPropsMap(entities as any), + CurrentEntityManager(), + CurrentEntityRepository(Users), TypeormUtilsService, - TransformDataService, - TypeormServiceFactory(Users), - EntityPropsMapService, + JsonApiTransformerService, + OrmServiceFactory(), ], }).compile(); @@ -99,11 +104,16 @@ describe('postOne', () => { debug: false, }, }, - EntityRepositoryFactory(Pods), + { + provide: CURRENT_ENTITY, + useValue: Users, + }, + EntityPropsMap(entities as any), + CurrentEntityManager(), + CurrentEntityRepository(Pods), TypeormUtilsService, - TransformDataService, - TypeormServiceFactory(Pods), - EntityPropsMapService, + JsonApiTransformerService, + OrmServiceFactory(), ], }).compile(); @@ -125,13 +135,15 @@ describe('postOne', () => { userGroupRepository ); backaUp = db.backup(); - typeormService = module.get>(TYPEORM_SERVICE); - transformDataService = - module.get>(TransformDataService); + typeormService = module.get>(ORM_SERVICE); + transformDataService = module.get>( + JsonApiTransformerService + ); - typeormServicePods = modulePods.get>(TYPEORM_SERVICE); - transformDataServicePods = - modulePods.get>(TransformDataService); + typeormServicePods = modulePods.get>(ORM_SERVICE); + transformDataServicePods = modulePods.get>( + JsonApiTransformerService + ); notes = await notesRepository.find(); users = await userRepository.find(); @@ -147,25 +159,33 @@ describe('postOne', () => { login, }, relationships: { - notes: [ - { - type: 'notes', - id: notes[0].id, - }, - ], - roles: [ - { - type: 'roles', - id: `${roles[0].id}`, - }, - ], + notes: { + data: [ + { + type: 'notes', + id: notes[0].id, + }, + ], + }, + roles: { + data: [ + { + type: 'roles', + id: `${roles[0].id}`, + }, + ], + }, manager: { - type: 'users', - id: `${users[0].id}`, + data: { + type: 'users', + id: `${users[0].id}`, + }, }, userGroup: { - type: 'user-group', - id: `${userGroup[0].id}`, + data: { + type: 'user-group', + id: `${userGroup[0].id}`, + }, }, }, }; @@ -196,10 +216,13 @@ describe('postOne', () => { id, }); - expect(spyOnTransformData).toBeCalledWith({ - ...result, - id, - }); + expect(spyOnTransformData).toBeCalledWith( + { + ...result, + id, + }, + { fields: null, include: [] } + ); expect(returnData).not.toHaveProperty('included'); }); @@ -215,7 +238,10 @@ describe('postOne', () => { login, }); - expect(spyOnTransformData).toBeCalledWith(result); + expect(spyOnTransformData).toBeCalledWith(result, { + fields: null, + include: [], + }); expect(returnData).not.toHaveProperty('included'); }); @@ -239,7 +265,10 @@ describe('postOne', () => { }, }); - expect(spyOnTransformData).toBeCalledWith(result); + expect(spyOnTransformData).toBeCalledWith(result, { + fields: null, + include: ['notes', 'roles', 'manager', 'userGroup'], + }); expect(returnData).toHaveProperty('included'); }); }); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-one/post-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.ts similarity index 60% rename from libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-one/post-one.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.ts index 97ba0a7c..08ac0a1a 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-one/post-one.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.ts @@ -1,13 +1,11 @@ import { DeepPartial } from 'typeorm'; -import { - Entity, - ResourceObject, - TypeormServiceObject, -} from '../../../../types'; -import { PostData } from '../../../zod'; +import { QueryField, ResourceObject } from '../../../../utils/nestjs-shared'; +import { ObjectLiteral } from '../../../../types'; +import { PostData, Query } from '../../../mixin/zod'; +import { TypeOrmService } from '../../service'; -export async function postOne( - this: TypeormServiceObject, +export async function postOne( + this: TypeOrmService, inputData: PostData ): Promise> { const { attributes, relationships, id } = inputData; @@ -30,8 +28,15 @@ export async function postOne( entityTarget, relationships ); + const fakeQuery: Query = { + [QueryField.fields]: null, + [QueryField.include]: Object.keys(relationships || {}), + } as any; - const { data, included } = this.transformDataService.transformData(saveData); + const { data, included } = this.transformDataService.transformData( + saveData, + fakeQuery + ); const includeData = included ? { included } : {}; return { meta: {}, diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-relationship/post-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.spec.ts similarity index 81% rename from libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-relationship/post-relationship.spec.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.spec.ts index 966c7b0b..5a936228 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-relationship/post-relationship.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.spec.ts @@ -3,11 +3,10 @@ import { Test, TestingModule } from '@nestjs/testing'; import { IMemoryDb } from 'pg-mem'; import { Repository } from 'typeorm'; -import { TypeormService } from '../../../../types'; import { Addresses, Comments, - createAndPullSchemaBase, + entities, getRepository, mockDBTestModule, Notes, @@ -16,28 +15,29 @@ import { Roles, UserGroups, Users, -} from '../../../../mock-utils'; -import { - TransformDataService, - TypeormUtilsService, -} from '../../../../mixin/service'; -import { EntityPropsMapService } from '../../../../service'; +} from '../../../../mock-utils/typeorm'; import { CONTROL_OPTIONS_TOKEN, + CURRENT_ENTITY, DEFAULT_CONNECTION_NAME, - TYPEORM_SERVICE, + ORM_SERVICE, } from '../../../../constants'; import { CurrentDataSourceProvider, - EntityRepositoryFactory, - TypeormServiceFactory, -} from '../../../../factory'; + CurrentEntityManager, + CurrentEntityRepository, + EntityPropsMap, + OrmServiceFactory, +} from '../../factory'; +import { TypeOrmService, TypeormUtilsService } from '../../service'; +import { createAndPullSchemaBase } from '../../../../mock-utils'; +import { JsonApiTransformerService } from '../../../mixin/service/json-api-transformer.service'; describe('postRelationship', () => { let db: IMemoryDb; - let typeormService: TypeormService; - let transformDataService: TransformDataService; + let typeormService: TypeOrmService; + let transformDataService: JsonApiTransformerService; let typeormUtilsService: TypeormUtilsService; let userRepository: Repository; let addressesRepository: Repository; @@ -60,11 +60,16 @@ describe('postRelationship', () => { debug: false, }, }, - EntityRepositoryFactory(Users), + { + provide: CURRENT_ENTITY, + useValue: Users, + }, + EntityPropsMap(entities as any), + CurrentEntityManager(), + CurrentEntityRepository(Users), TypeormUtilsService, - TransformDataService, - TypeormServiceFactory(Users), - EntityPropsMapService, + JsonApiTransformerService, + OrmServiceFactory(), ], }).compile(); ({ @@ -83,9 +88,10 @@ describe('postRelationship', () => { rolesRepository, userGroupRepository ); - typeormService = module.get>(TYPEORM_SERVICE); - transformDataService = - module.get>(TransformDataService); + typeormService = module.get>(ORM_SERVICE); + transformDataService = module.get>( + JsonApiTransformerService + ); typeormUtilsService = module.get>(TypeormUtilsService); }); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-relationship/post-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.ts similarity index 73% rename from libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-relationship/post-relationship.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.ts index 11652e12..7719cea0 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-relationship/post-relationship.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.ts @@ -1,18 +1,18 @@ import { - Entity, - TypeormServiceObject, EntityRelation, ResourceObjectRelationships, -} from '../../../../types'; +} from '../../../../utils/nestjs-shared'; -import { PostRelationshipData } from '../../../zod'; +import { ObjectLiteral } from '../../../../types'; +import { PostRelationshipData } from '../../../mixin/zod'; import { getRelationship } from '../get-relationship/get-relationship'; +import { TypeOrmService } from '../../service'; export async function postRelationship< - E extends Entity, + E extends ObjectLiteral, Rel extends EntityRelation >( - this: TypeormServiceObject, + this: TypeOrmService, id: number | string, rel: Rel, input: PostRelationshipData @@ -33,7 +33,7 @@ export async function postRelationship< } return getRelationship.call< - TypeormServiceObject, + TypeOrmService, [number | string, Rel], Promise> >(this, id, rel); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/index.ts new file mode 100644 index 00000000..b8e93ec7 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/index.ts @@ -0,0 +1,2 @@ +export * from './type-orm.service'; +export * from './typeorm-utils.service'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/type-orm.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/type-orm.service.ts new file mode 100644 index 00000000..1e040512 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/type-orm.service.ts @@ -0,0 +1,137 @@ +import { + ResourceObject, + EntityRelation, + ResourceObjectRelationships, +} from '../../../utils/nestjs-shared'; +import { Inject } from '@nestjs/common'; +import { Repository } from 'typeorm'; + +import { OrmService } from '../../mixin/types'; +import { + PatchData, + PatchRelationshipData, + PostData, + PostRelationshipData, + Query, + QueryOne, +} from '../../mixin/zod'; +import { ConfigParam, ObjectLiteral } from '../../../types'; + +import { + getAll, + getOne, + deleteOne, + postOne, + patchOne, + getRelationship, + postRelationship, + deleteRelationship, + patchRelationship, +} from '../orm-methods'; + +import { TypeormUtilsService } from './typeorm-utils.service'; +import { JsonApiTransformerService } from '../../mixin/service/json-api-transformer.service'; +import { + CONTROL_OPTIONS_TOKEN, + CURRENT_ENTITY_REPOSITORY, +} from '../../../constants'; +import { TypeOrmParam } from '../type'; + +export class TypeOrmService implements OrmService { + @Inject(TypeormUtilsService) + public typeormUtilsService!: TypeormUtilsService; + @Inject(JsonApiTransformerService) + public transformDataService!: JsonApiTransformerService; + @Inject(CONTROL_OPTIONS_TOKEN) public config!: ConfigParam & TypeOrmParam; + @Inject(CURRENT_ENTITY_REPOSITORY) public repository!: Repository; + + getAll(query: Query): Promise> { + return getAll.call< + TypeOrmService, + Parameters>, + ReturnType> + >(this, query); + } + + deleteOne(id: number | string): Promise { + return deleteOne.call< + TypeOrmService, + Parameters>, + ReturnType> + >(this, id); + } + + deleteRelationship>( + id: number | string, + rel: Rel, + input: PostRelationshipData + ): Promise { + return deleteRelationship.call< + TypeOrmService, + Parameters>, + ReturnType> + >(this, id, rel, input); + } + + getOne(id: number | string, query: QueryOne): Promise> { + return getOne.call< + TypeOrmService, + Parameters>, + ReturnType> + >(this, id, query); + } + + getRelationship>( + id: number | string, + rel: Rel + ): Promise> { + return getRelationship.call< + TypeOrmService, + Parameters>, + ReturnType> + >(this, id, rel); + } + + patchOne( + id: number | string, + inputData: PatchData + ): Promise> { + return patchOne.call< + TypeOrmService, + Parameters>, + ReturnType> + >(this, id, inputData); + } + + patchRelationship>( + id: number | string, + rel: Rel, + input: PatchRelationshipData + ): Promise> { + return patchRelationship.call< + TypeOrmService, + Parameters>, + ReturnType> + >(this, id, rel, input); + } + + postOne(inputData: PostData): Promise> { + return postOne.call< + TypeOrmService, + Parameters>, + ReturnType> + >(this, inputData); + } + + postRelationship>( + id: number | string, + rel: Rel, + input: PostRelationshipData + ): Promise> { + return postRelationship.call< + TypeOrmService, + Parameters>, + ReturnType> + >(this, id, rel, input); + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/service/typeorm-utils.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.spec.ts similarity index 95% rename from libs/json-api/json-api-nestjs/src/lib/mixin/service/typeorm-utils.service.spec.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.spec.ts index 3682c037..c53036c5 100644 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/service/typeorm-utils.service.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.spec.ts @@ -1,10 +1,15 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { IMemoryDb } from 'pg-mem'; import { getDataSourceToken } from '@nestjs/typeorm'; +import { QueryField, FilterOperand } from '../../../utils/nestjs-shared'; +import { + BadRequestException, + NotFoundException, + UnprocessableEntityException, +} from '@nestjs/common'; +import { IMemoryDb } from 'pg-mem'; import { Repository } from 'typeorm'; import { - createAndPullSchemaBase, mockDBTestModule, providerEntities, UserGroups, @@ -15,33 +20,21 @@ import { Notes, getRepository, pullAllData, -} from '../../mock-utils'; +} from '../../../mock-utils/typeorm'; import { CurrentDataSourceProvider, - EntityRepositoryFactory, -} from '../../factory'; + CurrentEntityManager, + CurrentEntityRepository, +} from '../factory'; import { CURRENT_ENTITY_REPOSITORY, DEFAULT_CONNECTION_NAME, -} from '../../constants'; +} from '../../../constants'; import { TypeormUtilsService } from './typeorm-utils.service'; -import { - PostData, - PostRelationshipData, - Query, - QueryField, -} from '../../helper'; -import { - EXPRESSION, - FilterOperand, - OperandsMapExpression, - Entity, -} from '../../types'; -import { - BadRequestException, - NotFoundException, - UnprocessableEntityException, -} from '@nestjs/common'; +import { PostData, PostRelationshipData, Query } from '../../mixin/zod'; +import { EXPRESSION, OperandsMapExpression } from '../type'; +import { ObjectLiteral as Entity } from '../../../types'; +import { createAndPullSchemaBase } from '../../../mock-utils'; function getDefaultQuery() { const filter = { @@ -97,7 +90,8 @@ describe('TypeormUtilsService', () => { providers: [ ...providerEntities(getDataSourceToken()), CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - EntityRepositoryFactory(UserGroups), + CurrentEntityManager(), + CurrentEntityRepository(UserGroups), TypeormUtilsService, ], }).compile(); @@ -113,7 +107,8 @@ describe('TypeormUtilsService', () => { providers: [ ...providerEntities(getDataSourceToken()), CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - EntityRepositoryFactory(Users), + CurrentEntityManager(), + CurrentEntityRepository(Users), TypeormUtilsService, ], }).compile(); @@ -178,19 +173,25 @@ describe('TypeormUtilsService', () => { const userGroup = await userGroupRepository.find(); const data: PostData['relationships'] = { - notes: [ - { - type: 'notes', - id: notes[0].id, - }, - ], + notes: { + data: [ + { + type: 'notes', + id: notes[0].id, + }, + ], + }, manager: { - type: 'users', - id: '1', + data: { + type: 'users', + id: '1', + }, }, userGroup: { - type: 'users-group', - id: `${userGroup[0].id}`, + data: { + type: 'users-group', + id: `${userGroup[0].id}`, + }, }, }; @@ -231,8 +232,10 @@ describe('TypeormUtilsService', () => { it('should be error resource not found', async () => { const data: PostData['relationships'] = { manager: { - id: '1000', - type: 'users', + data: { + id: '1000', + type: 'users', + }, }, }; expect.assertions(1); @@ -248,9 +251,8 @@ describe('TypeormUtilsService', () => { describe('getFilterExpressionForTarget', () => { it('expression for target field with null', () => { - - const nullableField = 'id' - const notNullableField = 'login' + const nullableField = 'id'; + const notNullableField = 'login'; const query = getDefaultQuery(); query.filter.target = { [nullableField]: { @@ -273,7 +275,6 @@ describe('TypeormUtilsService', () => { typeormUtilsServiceUser.getFilterExpressionForTarget(query); const mainAliasCheck = 'Users'; - for (const item of result) { const { params, alias, expression, selectInclude } = item; expect(selectInclude).toBe(undefined); @@ -290,7 +291,7 @@ describe('TypeormUtilsService', () => { throw new Error('filterName in undefined from query'); } - expect(params).toBe(undefined) + expect(params).toBe(undefined); if (field === nullableField) { expect(expression).toBe('IS NULL'); @@ -304,7 +305,7 @@ describe('TypeormUtilsService', () => { throw new Error('filed is incorrect'); } - }) + }); it('expression for target field', () => { const valueTest = (filterOperand: FilterOperand) => `test for ${filterOperand}`; @@ -649,7 +650,7 @@ describe('TypeormUtilsService', () => { }; const managerData = { type: 'users', - id: usersData.manager.id.toString(), + id: usersData.manager?.id.toString(), }; const emptyRoles: { id: string; type: string }[] = []; const emptyManager = null; diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/service/typeorm-utils.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.ts similarity index 96% rename from libs/json-api/json-api-nestjs/src/lib/mixin/service/typeorm-utils.service.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.ts index 846a575e..ecdd35c9 100644 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/service/typeorm-utils.service.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.ts @@ -7,27 +7,25 @@ import { } from '@nestjs/common'; import { EntityMetadata, Equal, In, Repository } from 'typeorm'; import { RelationMetadata as TypeOrmRelationMetadata } from 'typeorm/metadata/RelationMetadata'; +import { + camelToKebab, + kebabToCamel, + ObjectTyped, + snakeToCamel, + FilterOperand, +} from '../../../utils/nestjs-shared'; +import { ObjectLiteral, ValidateQueryError } from '../../../types'; import { - Entity, - EntityRelation, EXPRESSION, - FilterOperand, OperandMapExpressionForNull, OperandsMapExpression, OperandsMapExpressionForNullRelation, - ValidateQueryError, -} from '../../types'; -import { - camelToKebab, - getEntityName, - kebabToCamel, - ObjectTyped, - snakeToCamel, -} from '../../helper/utils'; -import { PatchData, PostData, Query } from '../../helper/zod'; -import { CURRENT_ENTITY_REPOSITORY } from '../../constants'; -import { TupleOfEntityRelation } from '../../helper/orm'; +} from '../type'; +import { PatchData, PostData, Query } from '../../mixin/zod'; +import { TupleOfEntityRelation, EntityRelation } from '../../mixin/types'; +import { getEntityName } from '../../mixin/helper'; +import { CURRENT_ENTITY_REPOSITORY } from '../../../constants'; type RelationAlias = { [K in TupleOfEntityRelation[number]]: string; @@ -61,6 +59,8 @@ export type ValidateReturn = T extends unknown[] ? null : string; +type Entity = ObjectLiteral; + function isTargetField( relationField: TupleOfEntityRelation, field: any @@ -455,11 +455,13 @@ export class TypeormUtilsService { > ): AsyncGenerator> { for (const entries of ObjectTyped.entries(relationships)) { - const [props, data] = entries; + const [props, dataItem] = entries; isRelationField(this._relationFields, props); - if (data === undefined) continue; + if (dataItem === undefined) continue; + const { data } = dataItem; + if (data === undefined) continue; if (data === null) { yield { [props]: null } as RelationshipsResult; continue; @@ -469,9 +471,12 @@ export class TypeormUtilsService { yield { [props]: [] } as RelationshipsResult; continue; } - const condition = isArray ? In(data.map((i) => i.id)) : Equal(data['id']); + + const condition = isArray + ? In((data as any[]).map((i) => i.id)) + : Equal(data['id']); const relationsTypeName = kebabToCamel( - isArray ? data[0]['type'] : data['type'] + isArray ? (data as any[])[0]['type'] : data['type'] ); const primaryField = this.getPrimaryColumnForRel( props as TupleOfEntityRelation[number] @@ -495,7 +500,7 @@ export class TypeormUtilsService { (!isArray && result.length === 0) ) { const message = isArray - ? `Resource '${relationsTypeName}' with ids '${data + ? `Resource '${relationsTypeName}' with ids '${(data as any[]) .map((i) => i.id) .filter((i) => !result.find((r) => r[primaryField] == i)) .join(',')}' does not exist` diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type-orm-json-api.module.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type-orm-json-api.module.ts new file mode 100644 index 00000000..968e57a1 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type-orm-json-api.module.ts @@ -0,0 +1,60 @@ +import { DynamicModule } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type'; + +import { NestProvider, ObjectLiteral, ResultModuleOptions } from '../../types'; +import { + CurrentEntityManager, + CurrentDataSourceProvider, + CurrentEntityRepository, + FindOneRowEntityFactory, + CheckRelationNameFactory, + OrmServiceFactory, + RunInTransactionFactory, + EntityPropsMap, +} from './factory'; +import { TypeormUtilsService } from './service'; +import { GLOBAL_MODULE_OPTIONS_TOKEN } from '../../constants'; + +export class TypeOrmJsonApiModule { + static module = 'typeOrm' as const; + static forRoot(options: ResultModuleOptions): DynamicModule { + const optionProvider = { + provide: GLOBAL_MODULE_OPTIONS_TOKEN, + useValue: options, + }; + + const typeOrmModule = TypeOrmModule.forFeature( + options.entities as EntityClassOrSchema[], + options.connectionName + ); + + const currentProvider = [ + ...(options.providers || []), + optionProvider, + CurrentDataSourceProvider(options.connectionName), + CurrentEntityManager(), + EntityPropsMap(options.entities), + RunInTransactionFactory(), + ]; + + const currentImport = [typeOrmModule, ...(options.imports || [])]; + + return { + module: TypeOrmJsonApiModule, + imports: currentImport, + providers: currentProvider, + exports: [...currentProvider, ...currentImport], + }; + } + + static getUtilProviders(entity: ObjectLiteral): NestProvider { + return [ + CurrentEntityRepository(entity), + TypeormUtilsService, + OrmServiceFactory(), + FindOneRowEntityFactory(), + CheckRelationNameFactory(), + ]; + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/types/operand.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type.ts similarity index 72% rename from libs/json-api/json-api-nestjs/src/lib/types/operand.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type.ts index 10ace773..9d365343 100644 --- a/libs/json-api/json-api-nestjs/src/lib/types/operand.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type.ts @@ -1,6 +1,13 @@ -import { FilterOperand } from 'json-shared-type'; +import { IsolationLevel } from 'typeorm/driver/types/IsolationLevel'; +import { FilterOperand } from '../../utils/nestjs-shared'; +export type TypeOrmParam = { + useSoftDelete?: boolean; + runInTransaction?: any>( + isolationLevel: IsolationLevel, + fn: Func + ) => ReturnType; +}; -export { FilterOperand }; export const EXPRESSION = 'EXPRESSION'; export const OperandsMapExpression = { [FilterOperand.eq]: `= :${EXPRESSION}`, diff --git a/libs/json-api/json-api-nestjs/src/lib/service/entity-props-map.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/service/entity-props-map.service.spec.ts deleted file mode 100644 index 60a52e3a..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/service/entity-props-map.service.spec.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { IMemoryDb } from 'pg-mem'; -import { getDataSourceToken } from '@nestjs/typeorm'; - -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, - UserGroups, - Users, -} from '../mock-utils'; -import { CurrentDataSourceProvider } from '../factory'; -import { DEFAULT_CONNECTION_NAME } from '../constants'; -import { EntityPropsMapService } from './entity-props-map.service'; - -describe('EntityPropsMapService', () => { - let db: IMemoryDb; - let entityPropsMapService: EntityPropsMapService; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - EntityPropsMapService, - ], - }).compile(); - - entityPropsMapService = module.get( - EntityPropsMapService - ); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('getPropsForEntity', () => { - const result = entityPropsMapService.getRelPropsForEntity(Users); - expect(result).toEqual([ - 'addresses', - 'manager', - 'roles', - 'comments', - 'notes', - 'userGroup', - ]); - }); - - it('getPropsForEntity', () => { - const result = entityPropsMapService.getPropsForEntity(Users); - expect(result).toEqual([ - 'id', - 'login', - 'firstName', - 'testReal', - 'testArrayNull', - 'lastName', - 'isActive', - 'createdAt', - 'testDate', - 'updatedAt', - ]); - }); - - it('getPrimaryColumnsForEntity', () => { - expect(entityPropsMapService.getPrimaryColumnsForEntity(Users)).toBe('id'); - }); - - it('getNameForEntity', () => { - expect(entityPropsMapService.getNameForEntity(Users)).toBe('Users'); - expect(entityPropsMapService.getNameForEntity(UserGroups)).toBe( - 'UserGroups' - ); - }); - - it('getRelationPropsType', () => { - expect( - entityPropsMapService.getRelationPropsType(Users, 'userGroup' as any) - ).toEqual(UserGroups); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/service/entity-props-map.service.ts b/libs/json-api/json-api-nestjs/src/lib/service/entity-props-map.service.ts deleted file mode 100644 index 00263a92..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/service/entity-props-map.service.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { DataSource, EntityTarget } from 'typeorm'; - -import { CURRENT_DATA_SOURCE_TOKEN } from '../constants'; -import { Entity, EntityRelation } from '../types'; -import { - getField, - ResultGetField, - TupleOfEntityProps, - TupleOfEntityRelation, -} from '../helper/orm'; - -@Injectable() -export class EntityPropsMapService { - @Inject(CURRENT_DATA_SOURCE_TOKEN) private dataSource!: DataSource; - - private _propsForEntity: Map = new Map(); - private _relPropsForEntity: Map = new Map(); - private _relTypePropsForEntity: Map = new Map(); - private _primaryColumnsForEntity: Map = new Map(); - private _nameForEntity: Map = new Map(); - - getPropsForEntity(entity: E): TupleOfEntityProps { - const result = this._propsForEntity.get(entity); - if (result) { - return result; - } - - const { field } = this.pullPropsAndRelFoEntity(entity); - - return field; - } - - getRelPropsForEntity(entity: E): TupleOfEntityRelation { - const result = this._relPropsForEntity.get(entity); - if (result) { - return result; - } - - const { relations } = this.pullPropsAndRelFoEntity(entity); - - return relations; - } - - getRelationPropsType(entity: E, rel: EntityRelation) { - const result = this._relTypePropsForEntity.get(entity); - if (result) { - return result[rel]; - } - - const repo = this.dataSource.getRepository(entity as EntityTarget); - const relToType = repo.metadata.relations.reduce((acum, item) => { - acum[item.propertyName as keyof E] = item.inverseEntityMetadata.target; - return acum; - }, {} as Record); - - this._relTypePropsForEntity.set(entity, relToType); - return relToType[rel]; - } - - getPrimaryColumnsForEntity(entity: E): keyof E { - const result = this._primaryColumnsForEntity.get(entity); - if (result) { - return result as keyof E; - } - const primaryColumns = this.dataSource.getRepository( - entity as EntityTarget - ).metadata.primaryColumns[0].propertyName; - this._primaryColumnsForEntity.set(entity, primaryColumns); - - return primaryColumns as keyof E; - } - - getNameForEntity(entity: E): string { - const result = this._nameForEntity.get(entity); - if (result) { - return result; - } - const name = this.dataSource.getRepository(entity as EntityTarget) - .metadata.name; - this._nameForEntity.set(entity, name); - return name; - } - - private pullPropsAndRelFoEntity( - entity: E - ): ResultGetField { - const repo = this.dataSource.getRepository(entity as EntityTarget); - - const { relations, field } = getField(repo); - this._propsForEntity.set(entity, field); - this._relPropsForEntity.set(entity, relations); - - return { relations, field }; - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/service/index.ts b/libs/json-api/json-api-nestjs/src/lib/service/index.ts deleted file mode 100644 index 952eb04b..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/service/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './transform-input.service'; -export * from '../mixin/service/typeorm-utils.service'; -export * from './entity-props-map.service'; diff --git a/libs/json-api/json-api-nestjs/src/lib/service/transform-input.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/service/transform-input.service.spec.ts deleted file mode 100644 index 01fcf175..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/service/transform-input.service.spec.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { TransformInputService } from './transform-input.service'; -import { ASC, DESC } from '../constants'; -import { Users } from '../mock-utils'; -import { QueryField, TypeInputProps } from '../helper'; - -describe('TransformInputService', () => { - let transformInputService: TransformInputService; - - beforeAll(() => { - transformInputService = new TransformInputService(); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('TransformInputService.transformSort', () => { - const check = 'id,manager.id,-roles.id,updatedAt,-createdAt'; - const checkResult = { - target: { - id: ASC, - updatedAt: ASC, - createdAt: DESC, - }, - manager: { - id: ASC, - }, - roles: { - id: DESC, - }, - }; - - const check2 = 'id'; - const checkResult2 = { - target: { - id: ASC, - }, - }; - - const check3 = ''; - - const result = transformInputService.transformSort(check); - const result2 = transformInputService.transformSort(undefined); - const result3 = transformInputService.transformSort(check2); - const result4 = transformInputService.transformSort(check3); - expect(result).toEqual(checkResult); - expect(result2).toBe(null); - expect(result4).toBe(null); - }); - - it('TransformInputService.transformInclude', () => { - const check = 'manager,roles,,notes,'; - const checkResult = ['manager', 'roles', 'notes']; - - const check2 = ''; - - const check3 = 'manager'; - const checkResult3 = ['manager']; - - const result = transformInputService.transformInclude(check); - const result2 = transformInputService.transformInclude(undefined); - const result3 = transformInputService.transformInclude(check2); - const result4 = transformInputService.transformInclude(check3); - expect(result).toEqual(checkResult); - expect(result2).toBe(null); - expect(result3).toBe(null); - expect(result4).toEqual(checkResult3); - }); - - it('TransformInputService.transformFields', () => { - const check = { - target: 'id,updatedAt,createdAt', - manager: 'id', - roles: 'id,key', - }; - const check2 = { - target: 'id,updatedAt,createdAt', - }; - const check3 = { - roles: 'id,key', - }; - const check4 = { - target: '', - roles: '', - }; - const check5 = { - target: 'id,updatedAt,createdAt', - roles: '', - }; - const checkResult = { - target: ['id', 'updatedAt', 'createdAt'], - manager: ['id'], - roles: ['id', 'key'], - }; - const checkResult2 = { - target: ['id', 'updatedAt', 'createdAt'], - }; - const checkResult3 = { - roles: ['id', 'key'], - }; - - const checkResult5 = { - target: ['id', 'updatedAt', 'createdAt'], - }; - - const result = transformInputService.transformFields(check); - const result2 = transformInputService.transformFields(undefined); - const result3 = transformInputService.transformFields({}); - const result4 = transformInputService.transformFields(check2); - const result5 = transformInputService.transformFields(check3 as any); - const result6 = transformInputService.transformFields(check4 as any); - const result7 = transformInputService.transformFields(check5 as any); - expect(result).toEqual(checkResult); - expect(result2).toBe(null); - expect(result3).toBe(null); - expect(result4).toEqual(checkResult2); - expect(result5).toEqual(checkResult3); - expect(result6).toEqual(null); - expect(result7).toEqual(checkResult5); - }); - - it('TransformInputService.transformFilter', () => { - const check1: TypeInputProps = { - id: { - in: 'in-test', - nin: 'nin-test,nin-test2', - }, - isActive: 'true', - manager: { - eq: 'null', - }, - 'addresses.arrayField': { - some: 'some-test', - }, - 'addresses.createdAt': '2023-01-01', - 'userGroup.label': { - like: 'test', - }, - }; - const checkResult1 = { - target: { - id: { - in: ['in-test'], - nin: ['nin-test', 'nin-test2'], - }, - isActive: { - eq: 'true', - }, - manager: { - eq: 'null', - }, - }, - relation: { - addresses: { - arrayField: { - some: ['some-test'], - }, - createdAt: { - eq: '2023-01-01', - }, - }, - userGroup: { - label: { like: 'test' }, - }, - }, - }; - - const check2: TypeInputProps = { - id: '', - 'addresses.arrayField': {}, - 'addresses.createdAt': '2023-01-01', - 'userGroup.label': {}, - }; - const checkResult2 = { - target: { - id: { - eq: '', - }, - }, - relation: { - addresses: { - createdAt: { - eq: '2023-01-01', - }, - }, - }, - }; - - const result1 = transformInputService.transformFilter(check1); - expect(result1).toEqual(checkResult1); - - const result2 = transformInputService.transformFilter(check2); - expect(result2).toEqual(checkResult2); - - expect(transformInputService.transformFilter(undefined)).toEqual({ - target: null, - relation: null, - }); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/service/transform-input.service.ts b/libs/json-api/json-api-nestjs/src/lib/service/transform-input.service.ts deleted file mode 100644 index c9aadbde..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/service/transform-input.service.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { Entity, FilterOperand } from '../types'; -import { isString } from '../helper/utils'; -import { QueryField, TypeInputProps } from '../helper/zod'; - -import { ASC, DESC } from '../constants'; - -const arrayOp = { - [FilterOperand.in]: true, - [FilterOperand.nin]: true, - [FilterOperand.some]: true, -}; -function convertToFilterObject( - value: Record | string -): Partial<{ - [key in FilterOperand]: string | string[]; -}> { - if (isString | string, string>(value)) { - return { - [FilterOperand.eq]: value, - }; - } else { - return Object.entries(value).reduce((acum, [op, filed]) => { - if (op in arrayOp) { - acum[op] = (isString(filed) ? filed.split(',') : []).filter((i) => !!i); - } else { - acum[op] = filed; - } - return acum; - }, {} as Record); - } -} - -type OutPutFilter = { - relation: null | Record< - string, - Record< - string, - Partial<{ - [key in FilterOperand]: string | string[]; - }> - > - >; - target: null | Record< - string, - Partial<{ - [key in FilterOperand]: string | string[]; - }> - >; -}; - -Injectable(); -export class TransformInputService { - public transformSort( - data: TypeInputProps - ): Record> | null { - if (!data) return null; - return data - .split(',') - .map((i) => i.trim()) - .filter((i) => !!i) - .reduce((acum, field) => { - const fieldName = field.charAt(0) === '-' ? field.substring(1) : field; - const sort = field.charAt(0) === '-' ? DESC : ASC; - if (fieldName.indexOf('.') > -1) { - const [relation, fieldRelation] = field.split('.'); - const relationName = - relation.charAt(0) === '-' ? relation.substring(1) : relation; - - acum[relationName] = acum[relationName] || {}; - acum[relationName][fieldRelation] = sort; - } else { - acum['target'] = acum['target'] || {}; - acum['target'][fieldName] = sort; - } - - return acum; - }, {} as Record>); - } - - public transformInclude( - data: TypeInputProps - ): string[] | null { - if (!data || !isString(data)) return null; - return data - .split(',') - .map((i) => i.trim()) - .filter((i) => !!i); - } - - transformFields( - data: TypeInputProps - ): Record | null { - if (!data) return null; - - const prepareResult = Object.entries(data).reduce((acum, [key, val]) => { - acum[key] = val - .split(',') - .map((i: string) => i.trim()) - .filter((i: string) => !!i); - return acum; - }, {} as Record); - - const result = Object.entries(prepareResult).reduce( - (acum, [key, value]) => { - if (value.length > 0) { - acum[key] = value; - } - return acum; - }, - {} as Record - ); - - return Object.keys(result).length > 0 ? result : null; - } - - transformFilter( - data: TypeInputProps - ): OutPutFilter | null { - if (!data) { - return { - relation: null, - target: null, - }; - } - return Object.entries(data).reduce( - (acum, [field, value]: [string, any]) => { - const objectOperand = convertToFilterObject(value); - if (Object.keys(objectOperand).length === 0) { - return acum; - } - - if (field.indexOf('.') > -1) { - const [relation, fieldRelation] = field.split('.'); - acum['relation'] = !acum['relation'] ? {} : acum['relation']; - acum['relation'][relation] = acum['relation'][relation] || {}; - acum['relation'][relation][fieldRelation] = objectOperand; - } else { - acum['target'] = !acum['target'] ? {} : acum['target']; - acum['target'][field] = objectOperand; - } - - return acum; - }, - { relation: null, target: null } as OutPutFilter - ); - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/types/config-param.ts b/libs/json-api/json-api-nestjs/src/lib/types/config-param.ts new file mode 100644 index 00000000..4d3c05e5 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/types/config-param.ts @@ -0,0 +1,45 @@ +import { + AnyEntity, + EntityName, + NestController, + NestImport, + NestProvider, + PipeMixin, +} from './util-types'; +import { NonEmptyArray } from 'zod-validation-error'; +import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type'; + +export type ExtractNestType = + ArrayType extends readonly (infer ElementType)[] ? ElementType : never; + +export type ConfigParam = { + requiredSelectField: boolean; + debug: boolean; + pipeForId: PipeMixin; + operationUrl: string; + overrideRoute: string; +}; + +export type GeneralParam = { + connectionName?: string; + entities: NonEmptyArray>; + controllers?: NestController; + providers?: NestProvider; + imports?: NestImport; +}; + +export type ResultGeneralParam = { + connectionName: string; + entities: NonEmptyArray>; + controllers: NestController; + providers: NestProvider; + imports: NestImport; +}; + +export interface BaseModuleOptions { + entity: EntityClassOrSchema; + connectionName: string; + controller?: ExtractNestType; + config: ConfigParam; + imports?: NestImport; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/types/decorator-options.types.ts b/libs/json-api/json-api-nestjs/src/lib/types/decorator-options.types.ts deleted file mode 100644 index 680c34ce..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/types/decorator-options.types.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { MethodName } from './binding.types'; -import { ConfigParam } from './module.types'; - -export type DecoratorOptions = Partial< - { - allowMethod: Array; - } & ConfigParam ->; diff --git a/libs/json-api/json-api-nestjs/src/lib/types/index.ts b/libs/json-api/json-api-nestjs/src/lib/types/index.ts index 43941180..9d3aaed5 100644 --- a/libs/json-api/json-api-nestjs/src/lib/types/index.ts +++ b/libs/json-api/json-api-nestjs/src/lib/types/index.ts @@ -1,8 +1,4 @@ -export * from './module.types'; -export * from './binding.types'; -export * from './decorator-options.types'; -export * from './utils'; -export * from './operand'; +export * from './config-param'; +export * from './module-common.types'; +export * from './util-types'; export * from './error.types'; -export * from './typeorm-service.type'; -export * from './response'; diff --git a/libs/json-api/json-api-nestjs/src/lib/types/module-common.types.ts b/libs/json-api/json-api-nestjs/src/lib/types/module-common.types.ts new file mode 100644 index 00000000..731f4131 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/types/module-common.types.ts @@ -0,0 +1,34 @@ +import { + MicroOrmJsonApiModule, + TypeOrmJsonApiModule, + TypeOrmParam, + MicroOrmParam, +} from '../modules'; + +import { ConfigParam, GeneralParam, ResultGeneralParam } from './config-param'; +import { RequiredFromPartial } from './util-types'; + +export type TypeOrmConfigParam = ConfigParam & TypeOrmParam; + +export type TypeOrmDefaultOptions = GeneralParam & { + options: Partial; +}; +export type TypeOrmOptions = GeneralParam & { + options: Partial; +}; + +export type MicroOrmConfigParam = ConfigParam & MicroOrmParam; +export type MicroOrmOptions = GeneralParam & { + options: Partial; +}; + +export type ResultTypeOrmModuleOptions = ResultGeneralParam & { + type: typeof TypeOrmJsonApiModule; +} & TypeOrmOptions & { options: RequiredFromPartial }; +export type ResultMicroOrmModuleOptions = ResultGeneralParam & { + type: typeof MicroOrmJsonApiModule; +} & MicroOrmOptions & { options: RequiredFromPartial }; + +export type ResultModuleOptions = + | ResultTypeOrmModuleOptions + | ResultMicroOrmModuleOptions; diff --git a/libs/json-api/json-api-nestjs/src/lib/types/module.types.ts b/libs/json-api/json-api-nestjs/src/lib/types/module.types.ts deleted file mode 100644 index 083a56da..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/types/module.types.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { ModuleMetadata, Type, PipeTransform } from '@nestjs/common'; -import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type'; -import { ObjectLiteral } from 'typeorm/common/ObjectLiteral'; -import { IsolationLevel } from 'typeorm/driver/types/IsolationLevel'; - -export type NestController = ModuleMetadata['controllers']; -export type NestProvider = ModuleMetadata['providers']; -export type NestImport = ModuleMetadata['imports']; -export type PipeMixin = Type; -export type Entity = EntityClassOrSchema | ObjectLiteral; - -export type PipeFabric = ( - entity: Entity, - connectionName: string, - config?: ConfigParam -) => PipeMixin; - -export type ExtractNestType = - ArrayType extends readonly (infer ElementType)[] ? ElementType : never; - -export interface ConfigParam { - requiredSelectField: boolean; - debug: boolean; - useSoftDelete: boolean; - pipeForId: PipeMixin; - operationUrl?: string; - overrideRoute?: string; - runInTransaction?: any>( - isolationLevel: IsolationLevel, - fn: Func - ) => ReturnType; -} - -export interface ModuleOptions { - entities: EntityClassOrSchema[]; - controllers?: NestController; - connectionName?: string; - providers?: NestProvider; - options?: Partial; - imports?: NestImport; -} - -export interface BaseModuleOptions { - entity: EntityClassOrSchema; - connectionName: string; - controller?: ExtractNestType; - config: ConfigParam; - imports?: NestImport; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/types/response.ts b/libs/json-api/json-api-nestjs/src/lib/types/response.ts deleted file mode 100644 index 33f67dab..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/types/response.ts +++ /dev/null @@ -1,13 +0,0 @@ -export { - PageProps, - DebugMetaProps, - MainData, - Links, - Attributes, - Data, - Relationships, - Include, - ResourceData, - ResourceObject, - ResourceObjectRelationships, -} from 'json-shared-type'; diff --git a/libs/json-api/json-api-nestjs/src/lib/types/typeorm-service.type.ts b/libs/json-api/json-api-nestjs/src/lib/types/typeorm-service.type.ts deleted file mode 100644 index 8062c965..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/types/typeorm-service.type.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Repository } from 'typeorm'; - -import { ConfigParam, Entity } from './'; -import type { MethodsService } from '../helper'; -import { TypeormUtilsService } from '../service'; -import { TransformDataService } from '../mixin/service'; - -export type TypeormServiceObject = { - repository: Repository; - config: ConfigParam; - typeormUtilsService: TypeormUtilsService; - transformDataService: TransformDataService; -}; - -export type TypeormService = MethodsService; diff --git a/libs/json-api/json-api-nestjs/src/lib/types/util-types.ts b/libs/json-api/json-api-nestjs/src/lib/types/util-types.ts new file mode 100644 index 00000000..1954c168 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/types/util-types.ts @@ -0,0 +1,24 @@ +import { ModuleMetadata, PipeTransform, Type } from '@nestjs/common'; +import { EntityTarget as EntityTargetTypeOrm } from 'typeorm/common/EntityTarget'; + +export type AnyEntity = Partial; + +export type EntityClass = Function & { prototype: T }; +export type EntityName = EntityClass; +export type EntityTarget = EntityClass | EntityTargetTypeOrm; +export interface ObjectLiteral { + [key: string]: any; +} + +export type NestController = NonNullable; +export type NestProvider = NonNullable; +export type NestImport = NonNullable; +export type PipeMixin = Type; + +export type RequiredFromPartial = { + [P in keyof T]-?: T[P] extends infer U | undefined ? U | false : T[P]; +}; + +export type RunInTransaction< + F extends (...arg: any[]) => Promise = (...arg: any[]) => Promise +> = (arg: F) => ReturnType; diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/___test___/test.helper.ts b/libs/json-api/json-api-nestjs/src/lib/utils/___test___/test.helper.ts new file mode 100644 index 00000000..79477361 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/utils/___test___/test.helper.ts @@ -0,0 +1,211 @@ +// @ts-nocheck +import { + AllFieldWithType, + FieldWithType, + PropsForField, + RelationPrimaryColumnType, + RelationPropsArray, + RelationPropsTypeName, + RelationTree, + ResultGetField, + TypeField, +} from '../../modules/mixin/types'; +import { Addresses, Users } from '../../mock-utils/typeorm'; + +export const fieldTypeUsers: FieldWithType = { + id: TypeField.number, + isActive: TypeField.boolean, + firstName: TypeField.string, + createdAt: TypeField.date, + lastName: TypeField.string, + login: TypeField.string, + testDate: TypeField.date, + updatedAt: TypeField.date, + testReal: TypeField.array, + testArrayNull: TypeField.array, +}; +export const propsDb: PropsForField = { + id: { type: Number, isArray: false, isNullable: false }, + login: { type: 'varchar', isArray: false, isNullable: false }, + firstName: { type: 'varchar', isArray: false, isNullable: true }, + testReal: { type: 'real', isArray: true, isNullable: false }, + testArrayNull: { type: 'real', isArray: true, isNullable: true }, + lastName: { type: 'varchar', isArray: false, isNullable: true }, + isActive: { type: 'boolean', isArray: false, isNullable: true }, + createdAt: { type: 'timestamp', isArray: false, isNullable: true }, + testDate: { type: 'timestamp', isArray: false, isNullable: true }, + updatedAt: { type: 'timestamp', isArray: false, isNullable: true }, + notes: { type: 'string', isArray: false, isNullable: true }, + roles: { type: 'number', isArray: true, isNullable: true }, + addresses: { type: 'number', isArray: true, isNullable: true }, + userGroup: { type: 'number', isArray: false, isNullable: true }, + manager: { type: 'number', isArray: false, isNullable: true }, + comments: { type: 'number', isArray: true, isNullable: true }, +}; +export const fieldTypeAddresses: FieldWithType = { + id: TypeField.number, + arrayField: TypeField.array, + state: TypeField.string, + city: TypeField.string, + createdAt: TypeField.date, + updatedAt: TypeField.date, + country: TypeField.string, +}; + +export const relationArrayProps: RelationPropsArray = { + roles: true, + userGroup: false, + notes: true, + addresses: false, + comments: true, + manager: false, +}; +export const relationPopsName: RelationPropsTypeName = { + roles: 'Roles', + userGroup: 'UserGroups', + notes: 'Notes', + addresses: 'Addresses', + comments: 'Comments', + manager: 'Users', +}; + +export const primaryColumnType: RelationPrimaryColumnType = { + roles: TypeField.number, + userGroup: TypeField.number, + notes: TypeField.string, + addresses: TypeField.number, + comments: TypeField.number, + manager: TypeField.number, +}; + +export const userFields: ResultGetField['field'] = [ + 'updatedAt', + 'testDate', + 'createdAt', + 'isActive', + 'lastName', + 'testArrayNull', + 'testReal', + 'firstName', + 'login', + 'id', +]; + +export const userRelations: RelationTree = { + addresses: [ + 'arrayField', + 'country', + 'state', + 'city', + 'updatedAt', + 'createdAt', + 'id', + ], + manager: [ + 'updatedAt', + 'testDate', + 'createdAt', + 'isActive', + 'lastName', + 'testArrayNull', + 'testReal', + 'firstName', + 'login', + 'id', + ], + roles: ['isDefault', 'key', 'name', 'updatedAt', 'createdAt', 'id'], + comments: ['kind', 'text', 'updatedAt', 'createdAt', 'id'], + notes: ['text', 'updatedAt', 'createdAt', 'id'], + userGroup: ['label', 'id'], +}; + +export const propsType: AllFieldWithType = { + updatedAt: TypeField.date, + testDate: TypeField.date, + createdAt: TypeField.date, + isActive: TypeField.boolean, + lastName: TypeField.string, + testArrayNull: TypeField.array, + testReal: TypeField.array, + firstName: TypeField.string, + login: TypeField.string, + id: TypeField.number, + addresses: { + arrayField: TypeField.array, + country: TypeField.string, + state: TypeField.string, + city: TypeField.string, + updatedAt: TypeField.date, + createdAt: TypeField.date, + id: TypeField.number, + }, + manager: { + updatedAt: TypeField.date, + testDate: TypeField.date, + createdAt: TypeField.date, + isActive: TypeField.boolean, + lastName: TypeField.string, + testArrayNull: TypeField.array, + testReal: TypeField.array, + firstName: TypeField.string, + login: TypeField.string, + id: TypeField.number, + }, + roles: { + isDefault: TypeField.boolean, + key: TypeField.string, + name: TypeField.string, + updatedAt: TypeField.date, + createdAt: TypeField.date, + id: TypeField.number, + }, + comments: { + kind: TypeField.string, + text: TypeField.string, + updatedAt: TypeField.date, + createdAt: TypeField.date, + id: TypeField.number, + }, + notes: { + text: TypeField.string, + updatedAt: TypeField.date, + createdAt: TypeField.date, + id: TypeField.string, + }, + userGroup: { + label: TypeField.string, + id: TypeField.number, + }, +}; + +export const relationList: ResultGetField['relations'] = [ + 'userGroup', + 'notes', + 'comments', + 'roles', + 'manager', + 'addresses', +]; + +export const userFieldsStructure: ResultGetField = { + field: [ + 'updatedAt', + 'testDate', + 'createdAt', + 'isActive', + 'lastName', + 'testArrayNull', + 'testReal', + 'firstName', + 'login', + 'id', + ], + relations: [ + 'userGroup', + 'notes', + 'comments', + 'roles', + 'manager', + 'addresses', + ], +}; diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/helper.spec.ts b/libs/json-api/json-api-nestjs/src/lib/utils/helper.spec.ts new file mode 100644 index 00000000..767f90a8 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/utils/helper.spec.ts @@ -0,0 +1,184 @@ +import { DynamicModule, ParseIntPipe } from '@nestjs/common'; + +import { + MicroOrmJsonApiModule, + TypeOrmJsonApiModule, + TypeOrmParam, +} from '../modules'; +import { + ConfigParam, + RequiredFromPartial, + ResultModuleOptions, +} from '../types'; +import { prepareConfig, createMixinModule } from './helper'; +import { + DEFAULT_CONNECTION_NAME, + JSON_API_CONTROLLER_POSTFIX, +} from '../constants'; +import { JsonBaseController } from '../modules/mixin/controller/json-base.controller'; +import { JsonApi } from '../modules/mixin/decorators'; +import { getProviderName } from '../modules/mixin/helper'; + +class A {} +describe('Helper tests', () => { + describe('prepareConfig', () => { + it('should return default config when type is undefined', () => { + const result = prepareConfig( + { + entities: [A], + options: { debug: false, requiredSelectField: false }, + }, + 'typeOrm' + ); + + expect(Array.isArray(result.imports)).toBe(true); + expect(Array.isArray(result.controllers)).toBe(true); + expect(Array.isArray(result.providers)).toBe(true); + expect(result.options.debug).toBe(false); + expect(result.options.requiredSelectField).toBe(false); + expect(result.connectionName).toBe(DEFAULT_CONNECTION_NAME); + }); + + it('should return TypeOrm config when type is TypeOrmModule', () => { + const result = prepareConfig( + { + entities: [A], + options: { + debug: true, + requiredSelectField: true, + useSoftDelete: true, + }, + }, + 'typeOrm' + ); + + expect(result.options.debug).toBe(true); + expect( + (result.options as RequiredFromPartial) + .useSoftDelete + ).toBe(true); + }); + + it('should return MicroOrm config when type is MicroOrmModule', () => { + const result = prepareConfig( + { + entities: [A], + options: { debug: true, requiredSelectField: true }, + }, + 'microOrm' + ); + + expect(result.options.debug).toBe(true); + expect(result.options.requiredSelectField).toBe(true); + + // @ts-expect-error eed check run time + expect((result.options as ConfigParam).useSoftDelete).toBeUndefined(); + }); + + it('should use default values for pipeForId, operationUrl, and overrideRoute when not provided', () => { + const result = prepareConfig( + { + entities: [A], + options: {}, + }, + 'typeOrm' + ); + + expect(result.options.pipeForId).toBe(ParseIntPipe); + expect(result.options.operationUrl).toBe(false); + expect(result.options.overrideRoute).toBe(false); + }); + }); + + describe('createMixinModule', () => { + it('should create a MixinModule with the correct controller matching the entity', () => { + class TestEntity {} + @JsonApi(TestEntity) + class TestController extends JsonBaseController {} + const commonOrmModule = {} as DynamicModule; + const resultOptions = prepareConfig( + { + entities: [TestEntity], + controllers: [TestController], + connectionName: DEFAULT_CONNECTION_NAME, + options: { + debug: true, + requiredSelectField: true, + useSoftDelete: true, + }, + }, + 'typeOrm' + ); + + const result = createMixinModule( + TestEntity, + { + ...resultOptions, + type: TypeOrmJsonApiModule, + } as ResultModuleOptions, + commonOrmModule + ); + + expect(result).toHaveProperty('controllers', [TestController]); + expect(result).toHaveProperty('providers'); + expect(result.imports?.includes(commonOrmModule)).toBe(true); + }); + + it('should use undefined as controller if none match the entity', () => { + class TestEntity {} + const commonOrmModule = {} as DynamicModule; + const resultOptions = prepareConfig( + { + entities: [TestEntity], + controllers: [], + connectionName: 'test_connection', + options: { debug: false }, + imports: [], + }, + 'typeOrm' + ); + + const result = createMixinModule( + TestEntity, + { + ...resultOptions, + type: TypeOrmJsonApiModule, + } as ResultModuleOptions, + commonOrmModule + ); + + const controller = (result.controllers || []).at(0); + expect(controller?.name).toBe( + getProviderName(TestEntity, JSON_API_CONTROLLER_POSTFIX) + ); + }); + + it('should correctly construct the MixinModule using given ResultModuleOptions', () => { + class AnotherEntity {} + class SharedModule {} + const commonOrmModule = {} as DynamicModule; + const importTest = { module: SharedModule }; + + const resultOptions = prepareConfig( + { + entities: [AnotherEntity], + controllers: [], + connectionName: 'default_connection', + options: { debug: true, useSoftDelete: true }, + imports: [importTest], + }, + 'typeOrm' + ); + + const result = createMixinModule( + AnotherEntity, + { + ...resultOptions, + type: TypeOrmJsonApiModule, + } as ResultModuleOptions, + commonOrmModule + ); + expect(result.imports?.at(1)).toEqual(importTest); + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/helper.ts b/libs/json-api/json-api-nestjs/src/lib/utils/helper.ts new file mode 100644 index 00000000..7c7f1bd6 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/utils/helper.ts @@ -0,0 +1,122 @@ +import { DynamicModule, ParseIntPipe } from '@nestjs/common'; + +import { + AnyEntity, + ConfigParam, + EntityName, + MicroOrmOptions, + RequiredFromPartial, + ResultModuleOptions, + TypeOrmConfigParam, + MicroOrmConfigParam, + TypeOrmOptions, +} from '../types'; +import { + DEFAULT_CONNECTION_NAME, + JSON_API_DECORATOR_ENTITY, +} from '../constants'; +import { TypeOrmParam, AtomicOperationModule, MicroOrmParam } from '../modules'; +import { MixinModule } from '../modules/mixin/mixin.module'; +import { Type } from '@nestjs/common/interfaces'; +import { RouterModule } from '@nestjs/core'; +import { DEFAULT_ARRAY_TYPE } from '../modules/micro-orm/constants'; + +export function prepareConfig( + moduleOptions: TypeOrmOptions | MicroOrmOptions, + type: 'typeOrm' | 'microOrm' +): Omit { + const { options: inputOptions } = moduleOptions; + + let resulOptions: + | RequiredFromPartial + | RequiredFromPartial; + + const configParam: RequiredFromPartial = { + debug: !!inputOptions.debug, + requiredSelectField: !!inputOptions.requiredSelectField, + operationUrl: inputOptions.operationUrl || false, + overrideRoute: inputOptions.overrideRoute || false, + pipeForId: inputOptions.pipeForId || ParseIntPipe, + }; + + if (type === 'typeOrm') { + const { runInTransaction, useSoftDelete } = + moduleOptions.options as Partial; + + resulOptions = { + ...configParam, + useSoftDelete: useSoftDelete ? useSoftDelete : false, + runInTransaction: runInTransaction ? runInTransaction : false, + }; + } else { + const { arrayType } = moduleOptions.options as Partial< + ConfigParam & MicroOrmParam + >; + + resulOptions = { + ...configParam, + arrayType: [...DEFAULT_ARRAY_TYPE, ...(arrayType || [])], + }; + } + + return { + connectionName: + type === 'typeOrm' + ? moduleOptions.connectionName || DEFAULT_CONNECTION_NAME + : (moduleOptions.connectionName as any), + entities: moduleOptions.entities, + imports: moduleOptions.imports || [], + providers: moduleOptions.providers || [], + controllers: moduleOptions.controllers || [], + options: resulOptions as any, + } satisfies Omit; +} + +export function createMixinModule( + entity: EntityName, + resultOption: ResultModuleOptions, + commonOrmModule: DynamicModule +): DynamicModule { + const controller = (resultOption.controllers || []).find( + (item) => + item && Reflect.getMetadata(JSON_API_DECORATOR_ENTITY, item) === entity + ); + + return MixinModule.forRoot({ + entity, + controller, + config: resultOption.options, + imports: [commonOrmModule, ...resultOption.imports], + ormModule: resultOption.type, + }); +} + +export function createAtomicModule( + options: ResultModuleOptions, + entitiesMixinModules: DynamicModule[], + commonOrmModule: DynamicModule +): DynamicModule[] { + const { operationUrl } = options.options; + if (!operationUrl) return []; + + return [ + AtomicOperationModule.forRoot( + { + ...options, + connectionName: options.connectionName, + }, + entitiesMixinModules, + commonOrmModule + ), + RouterModule.register([ + { + module: AtomicOperationModule, + path: operationUrl, + }, + ]), + ]; +} + +export function entityForClass(type: Type): EntityName { + return Reflect.getMetadata(JSON_API_DECORATOR_ENTITY, type); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/index.ts b/libs/json-api/json-api-nestjs/src/lib/utils/index.ts new file mode 100644 index 00000000..d6d6f056 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/utils/index.ts @@ -0,0 +1 @@ +export * from './helper'; diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/index.ts b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/index.ts new file mode 100644 index 00000000..a0fe9b9f --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/index.ts @@ -0,0 +1,2 @@ +export * from './lib/utils'; +export * from './lib/types'; diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/entity-type.ts b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/entity-type.ts new file mode 100644 index 00000000..5fbdd04f --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/entity-type.ts @@ -0,0 +1,17 @@ +export type EntityField = + | string + | number + | bigint + | boolean + | string[] + | number[] + | null + | Date; + +export type EntityProps = { + [P in keyof T]: T[P] extends EntityField ? P : never; +}[keyof T]; + +export type EntityRelation = { + [P in keyof T]: T[P] extends EntityField ? never : P; +}[keyof T]; diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/index.ts b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/index.ts new file mode 100644 index 00000000..33b70ba6 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/index.ts @@ -0,0 +1,4 @@ +export * from './utils-string.type'; +export * from './query-type'; +export * from './entity-type'; +export * from './response-body'; diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/query-type.ts b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/query-type.ts new file mode 100644 index 00000000..ddeead7a --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/query-type.ts @@ -0,0 +1,36 @@ +export enum QueryField { + filter = 'filter', + sort = 'sort', + include = 'include', + page = 'page', + fields = 'fields', +} + +export enum FilterOperand { + eq = 'eq', + gt = 'gt', + gte = 'gte', + like = 'like', + lt = 'lt', + lte = 'lte', + ne = 'ne', + regexp = 'regexp', + in = 'in', + nin = 'nin', + some = 'some', +} + +export enum FilterOperandOnlyInNin { + in = 'in', + nin = 'nin', +} +export enum FilterOperandOnlySimple { + eq = 'eq', + gt = 'gt', + gte = 'gte', + like = 'like', + lt = 'lt', + lte = 'lte', + ne = 'ne', + regexp = 'regexp', +} diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/response-body.ts b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/response-body.ts new file mode 100644 index 00000000..85dbdb49 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/response-body.ts @@ -0,0 +1,76 @@ +import { + EntityField, + EntityProps, + EntityRelation, + TypeOfArray, + ValueOf, +} from '.'; +import { Collection } from '@mikro-orm/core'; + +export type PageProps = { + totalItems: number; + pageNumber: number; + pageSize: number; +}; + +export type DebugMetaProps = Partial<{ + time: number; +}>; + +export type MainData = { + type: T; + id: string; +}; + +export type Links = { + self: string; + related?: string; +}; + +export type Attributes = { + [P in EntityProps]?: D[P] extends EntityField ? D[P] : TypeOfArray; +}; + +export type DataResult = E extends unknown[] + ? MainData[] + : E extends Collection + ? MainData[] + : MainData | null; + +export type Data = { + data?: DataResult; +}; + +export type Relationships = { + [P in EntityRelation]?: { + links: Links; + } & Data; +}; + +export type Include = ValueOf<{ + [P in EntityRelation]: ResourceData>; +}>; + +export type ResourceData = MainData & { + attributes?: Attributes; + relationships?: Relationships; + links: Omit; +}; + +export type MetaProps = R extends null ? T : T & R; + +export type ResourceObject< + T, + R extends 'object' | 'array' = 'object', + M = null +> = { + meta: R extends 'array' + ? MetaProps + : MetaProps; + data: R extends 'array' ? ResourceData[] : ResourceData; + included?: Include[]; +}; + +export type ResourceObjectRelationships> = { + meta: DebugMetaProps; +} & Required>; diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/utils-string.type.ts b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/utils-string.type.ts new file mode 100644 index 00000000..b861d238 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/utils-string.type.ts @@ -0,0 +1,26 @@ +import type { Collection } from '@mikro-orm/core'; + +export type KebabCase = S extends `${infer C}${infer T}` + ? KebabCase extends infer U + ? U extends string + ? T extends Uncapitalize + ? `${Uncapitalize}${U}` + : `${Uncapitalize}-${U}` + : never + : never + : S; + +export type KebabToCamelCase = + S extends `${infer T}-${infer U}-${infer V}` + ? `${T}${Capitalize}${Capitalize>}` + : S extends `${infer T}-${infer U}` + ? `${Capitalize}${Capitalize>}` + : S; + +export type TypeOfArray = T extends (infer U)[] + ? U + : T extends Collection + ? U + : T; + +export type ValueOf = T[keyof T]; diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/utils/index.ts b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/utils/index.ts new file mode 100644 index 00000000..a7799257 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/utils/index.ts @@ -0,0 +1,2 @@ +export * from './string-utils'; +export * from './object-utils'; diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/utils/object-utils.ts b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/utils/object-utils.ts new file mode 100644 index 00000000..5d4ad26d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/utils/object-utils.ts @@ -0,0 +1,21 @@ +import { kebabToCamel } from './string-utils'; + +export const ObjectTyped = { + keys: Object.keys as (yourObject: T) => Array, + values: Object.values as (yourObject: U) => Array, + entries: Object.entries as ( + yourObject: O + ) => Array<[keyof O, O[keyof O]]>, + fromEntries: Object.fromEntries as ( + yourObjectEntries: [K, V][] + ) => Record, +}; + +export function isObject(item: unknown): item is object { + return typeof item === 'object' && !Array.isArray(item) && item !== null; +} + +export function createEntityInstance(name: string): E { + const entityName = kebabToCamel(name); + return Function('return new class ' + entityName + '{}')(); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/utils/string-utils.spec.ts b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/utils/string-utils.spec.ts new file mode 100644 index 00000000..4596cd21 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/utils/string-utils.spec.ts @@ -0,0 +1,42 @@ +import { + camelToKebab, + snakeToCamel, + isString, + kebabToCamel, +} from './string-utils'; + +describe('Test utils', () => { + it('camelToKebab', () => { + const result = camelToKebab('ApproverGroups'); + const result1 = camelToKebab('Users'); + + expect(result).toBe('approver-groups'); + expect(result1).toBe('users'); + }); + + it('snakeToCamel', () => { + const result = snakeToCamel('test_test'); + const result1 = snakeToCamel('test-test'); + const result2 = snakeToCamel('testTest'); + const result3 = snakeToCamel('event_incident_typeFK'); + expect(result).toBe('testTest'); + expect(result1).toBe('testTest'); + expect(result2).toBe('testTest'); + expect(result3).toBe('eventIncidentTypeFK'); + }); + + it('isString', () => { + expect(isString('string')).toBe(true); + expect(isString(String('string'))).toBe(true); + expect(isString(new Date())).toBe(false); + expect(isString(class {})).toBe(false); + }); + + it('kebabToCamel', () => { + const type = 'users-group'; + const type1 = 'users'; + + expect(kebabToCamel(type)).toBe('UsersGroup'); + expect(kebabToCamel(type1)).toBe('Users'); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/utils/string-utils.ts b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/utils/string-utils.ts new file mode 100644 index 00000000..3b678710 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/utils/string-utils.ts @@ -0,0 +1,38 @@ +import { KebabToCamelCase, KebabCase } from '../types'; + +export function isString(value: T): value is P { + return typeof value === 'string' || value instanceof String; +} + +export function snakeToCamel(str: string): string { + if (!str.match(/[\s_-]/g)) { + return str; + } + return str.replace(/([-_][a-z])/g, (group) => + group.toUpperCase().replace('-', '').replace('_', '') + ); +} + +export function camelToKebab(string: S): KebabCase { + return string + .replace(/((?<=[a-z\d])[A-Z]|(?<=[A-Z\d])[A-Z](?=[a-z]))/g, '-$1') + .toLowerCase() as KebabCase; +} + +export function upperFirstLetter(string: S): Capitalize { + return (string.charAt(0).toUpperCase() + string.slice(1)) as Capitalize; +} + +export function kebabToCamel(str: S): KebabToCamelCase { + return str + .split('-') + .map((i) => i.charAt(0).toUpperCase() + i.substring(1)) + .join('') as KebabToCamelCase; +} + +export function capitalizeFirstChar(str: string) { + return str + .split('-') + .map((i) => i.charAt(0).toUpperCase() + i.substring(1)) + .join(''); +} diff --git a/libs/json-api/json-api-nestjs/tsconfig-mjs.lib.json b/libs/json-api/json-api-nestjs/tsconfig-mjs.lib.json deleted file mode 100644 index 5857b698..00000000 --- a/libs/json-api/json-api-nestjs/tsconfig-mjs.lib.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "types": ["node"], - "module": "es2015", - "target": "ES2022", - "removeComments": false, - "declaration": true, - "declarationMap": true - }, - "include": ["src/**/*.ts"], - "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] -} diff --git a/libs/json-api/json-api-nestjs/tsconfig.json b/libs/json-api/json-api-nestjs/tsconfig.json index f22982be..0dc79caa 100644 --- a/libs/json-api/json-api-nestjs/tsconfig.json +++ b/libs/json-api/json-api-nestjs/tsconfig.json @@ -5,9 +5,9 @@ "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true }, "files": [], "include": [], @@ -15,9 +15,6 @@ { "path": "./tsconfig.lib.json" }, - { - "path": "./tsconfig-mjs.lib.json" - }, { "path": "./tsconfig.spec.json" } diff --git a/libs/json-api/json-api-nestjs/tsconfig.spec.json b/libs/json-api/json-api-nestjs/tsconfig.spec.json index 69a251f3..ebbb8e0d 100644 --- a/libs/json-api/json-api-nestjs/tsconfig.spec.json +++ b/libs/json-api/json-api-nestjs/tsconfig.spec.json @@ -3,6 +3,7 @@ "compilerOptions": { "outDir": "../../../dist/out-tsc", "module": "commonjs", + "moduleResolution": "node16", "types": ["jest", "node"] }, "include": [ diff --git a/libs/json-api/json-shared-type/project.json b/libs/json-api/json-shared-type/project.json deleted file mode 100644 index 6362de6b..00000000 --- a/libs/json-api/json-shared-type/project.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "json-shared-type", - "$schema": "../../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "libs/json-api/json-shared-type/src", - "projectType": "library", - "targets": { - "build1": { - "executor": "@nx/js:tsc", - "outputs": ["{options.outputPath}"], - "options": { - "outputPath": "dist/libs/shared-utils", - "tsConfig": "libs/shared-utils/tsconfig.lib.json", - "packageJson": "libs/shared-utils/package.json", - "main": "libs/shared-utils/src/index.ts", - "assets": ["libs/shared-utils/*.md"] - } - } - }, - "tags": [] -} diff --git a/libs/json-api/json-shared-type/src/index.ts b/libs/json-api/json-shared-type/src/index.ts deleted file mode 100644 index fcb073fe..00000000 --- a/libs/json-api/json-shared-type/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './types'; diff --git a/libs/json-api/json-shared-type/src/types/utils-type.ts b/libs/json-api/json-shared-type/src/types/utils-type.ts deleted file mode 100644 index ef480a55..00000000 --- a/libs/json-api/json-shared-type/src/types/utils-type.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type TypeOfArray = T extends (infer U)[] ? U : T; - -export type ValueOf = T[keyof T]; diff --git a/libs/database/.eslintrc.json b/libs/microorm-database/.eslintrc.json similarity index 85% rename from libs/database/.eslintrc.json rename to libs/microorm-database/.eslintrc.json index 9d9c0db5..8d4e2111 100644 --- a/libs/database/.eslintrc.json +++ b/libs/microorm-database/.eslintrc.json @@ -1,5 +1,5 @@ { - "extends": ["../../.eslintrc.json"], + "extends": ["../../.eslintrc.base.json"], "ignorePatterns": ["!**/*"], "overrides": [ { diff --git a/libs/microorm-database/README.md b/libs/microorm-database/README.md new file mode 100644 index 00000000..3011b956 --- /dev/null +++ b/libs/microorm-database/README.md @@ -0,0 +1,7 @@ +# microorm-database + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test microorm-database` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/shared-utils/jest.config.ts b/libs/microorm-database/jest.config.ts similarity index 67% rename from libs/shared-utils/jest.config.ts rename to libs/microorm-database/jest.config.ts index f2d2f024..2f7137c0 100644 --- a/libs/shared-utils/jest.config.ts +++ b/libs/microorm-database/jest.config.ts @@ -1,11 +1,10 @@ -/* eslint-disable */ export default { - displayName: 'shared-utils', + displayName: 'microorm-database', preset: '../../jest.preset.js', testEnvironment: 'node', transform: { '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], }, moduleFileExtensions: ['ts', 'js', 'html'], - coverageDirectory: '../../coverage/libs/shared-utils', + coverageDirectory: '../../coverage/libs/microorm-database', }; diff --git a/libs/microorm-database/project.json b/libs/microorm-database/project.json new file mode 100644 index 00000000..b72c2185 --- /dev/null +++ b/libs/microorm-database/project.json @@ -0,0 +1,9 @@ +{ + "name": "microorm-database", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/microorm-database/src", + "projectType": "library", + "tags": [], + "// targets": "to see all targets run: nx show project microorm-database --web", + "targets": {} +} diff --git a/libs/microorm-database/src/index.ts b/libs/microorm-database/src/index.ts new file mode 100644 index 00000000..84924fb7 --- /dev/null +++ b/libs/microorm-database/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/micro-orm-database.module'; +export * from './lib/entities'; diff --git a/libs/microorm-database/src/lib/config-cli.ts b/libs/microorm-database/src/lib/config-cli.ts new file mode 100644 index 00000000..20385027 --- /dev/null +++ b/libs/microorm-database/src/lib/config-cli.ts @@ -0,0 +1,45 @@ +import { Options as PgOptions, PostgreSqlDriver } from '@mikro-orm/postgresql'; +import { Options as MyOptions, MySqlDriver } from '@mikro-orm/mysql'; +import { TSMigrationGenerator } from '@mikro-orm/migrations'; +import { Options } from '@mikro-orm/core'; +import { join } from 'path'; + +import * as allEntities from './entities'; + +const entitiesArray = Object.values(allEntities).filter( + (maybeClass) => typeof maybeClass === 'function' +); + +const mySqlOptions: MyOptions = { + driver: MySqlDriver, +}; + +const pgSqlOptions: PgOptions = { + driver: PostgreSqlDriver, +}; + +const config: Options = { + dbName: process.env['DB_NAME'], + // dbName: 'microorm-test', + host: process.env['DB_HOST'], + port: parseInt(`${process.env['DB_PORT']}`, 10), + user: process.env['DB_USERNAME'], + password: process.env['DB_PASSWORD'], + entitiesTs: [join(__dirname, '/entities/**/*')], + entities: entitiesArray, + debug: process.env['DB_LOGGING'] === '1', + ...(process.env['DB_TYPE'] === 'mysql' ? mySqlOptions : pgSqlOptions), + migrations: { + tableName: 'migrations', + path: join(__dirname, '/migrations'), + glob: '!(*.d).{js,ts}', + transactional: false, + allOrNothing: true, + dropTables: true, + snapshot: true, + emit: 'ts', + generator: TSMigrationGenerator, + }, +}; + +export default config; diff --git a/libs/microorm-database/src/lib/config.ts b/libs/microorm-database/src/lib/config.ts new file mode 100644 index 00000000..cbdc17cb --- /dev/null +++ b/libs/microorm-database/src/lib/config.ts @@ -0,0 +1,10 @@ +import { Options } from '@mikro-orm/core'; + +import ormConfig from './config-cli'; + +const { entitiesTs, ...configOther } = ormConfig; + +export const config: Options = { + discovery: { requireEntitiesArray: true }, + ...configOther, +}; diff --git a/libs/microorm-database/src/lib/entities/addresses.ts b/libs/microorm-database/src/lib/entities/addresses.ts new file mode 100644 index 00000000..2ebf6446 --- /dev/null +++ b/libs/microorm-database/src/lib/entities/addresses.ts @@ -0,0 +1,58 @@ +import { Entity, PrimaryKey, Property, OneToOne } from '@mikro-orm/core'; + +import { Users, IUsers } from '.'; + +export type IAddresses = Addresses; + +@Entity({ + tableName: 'addresses', +}) +export class Addresses { + @PrimaryKey({ + autoincrement: true, + }) + public id!: number; + + @Property({ + columnType: 'varchar', + length: 70, + nullable: true, + }) + public city!: string; + + @Property({ + columnType: 'varchar', + length: 70, + nullable: true, + }) + public state!: string; + + @Property({ + columnType: 'varchar', + length: 68, + nullable: true, + }) + public country!: string; + + @Property({ + length: 0, + name: 'created_at', + nullable: true, + defaultRaw: 'CURRENT_TIMESTAMP(0)', + columnType: 'timestamp(0) without time zone', + }) + createdAt: Date = new Date(); + + @Property({ + length: 0, + onUpdate: () => new Date(), + name: 'updated_at', + nullable: true, + columnType: 'timestamp(0) without time zone', + defaultRaw: 'CURRENT_TIMESTAMP(0)', + }) + updatedAt: Date = new Date(); + + @OneToOne(() => Users, (item) => item.addresses) + public user!: IUsers; +} diff --git a/libs/microorm-database/src/lib/entities/book-list.ts b/libs/microorm-database/src/lib/entities/book-list.ts new file mode 100644 index 00000000..041059e3 --- /dev/null +++ b/libs/microorm-database/src/lib/entities/book-list.ts @@ -0,0 +1,51 @@ +import { + Entity, + PrimaryKey, + Property, + ManyToMany, + Collection, +} from '@mikro-orm/core'; + +import { IUsers, Users } from './users'; + +export type IBookList = BookList; + +@Entity({ + tableName: 'book_list', +}) +export class BookList { + @PrimaryKey({ + type: 'uuid', + defaultRaw: 'uuid_generate_v4()', + }) + public id!: string; + + @Property({ + type: 'text', + nullable: false, + }) + public text!: string; + + @Property({ + length: 0, + name: 'created_at', + nullable: true, + defaultRaw: 'CURRENT_TIMESTAMP(0)', + columnType: 'timestamp(0) without time zone', + type: 'timestamp', + }) + createdAt: Date = new Date(); + + @Property({ + length: 0, + onUpdate: () => new Date(), + name: 'updated_at', + nullable: true, + columnType: 'timestamp(0) without time zone', + defaultRaw: 'CURRENT_TIMESTAMP(0)', + }) + updatedAt: Date = new Date(); + + @ManyToMany(() => Users, (item) => item.books) + public users = new Collection(this); +} diff --git a/libs/microorm-database/src/lib/entities/comments.ts b/libs/microorm-database/src/lib/entities/comments.ts new file mode 100644 index 00000000..3f4be0e3 --- /dev/null +++ b/libs/microorm-database/src/lib/entities/comments.ts @@ -0,0 +1,54 @@ +import { Entity, PrimaryKey, Property, Enum, ManyToOne } from '@mikro-orm/core'; + +export enum CommentKind { + Comment = 'COMMENT', + Message = 'MESSAGE', + Note = 'NOTE', +} + +import { Users, IUsers } from '.'; + +export type IComments = Comments; + +@Entity({ + tableName: 'comments', +}) +export class Comments { + @PrimaryKey({ + autoincrement: true, + }) + public id!: number; + + @Property({ + columnType: 'text', + }) + public text!: string; + + @Enum({ items: () => CommentKind, nativeEnumName: 'comment_kind_enum' }) + public kind!: CommentKind; + + @Property({ + length: 0, + name: 'created_at', + nullable: true, + defaultRaw: 'CURRENT_TIMESTAMP(0)', + columnType: 'timestamp(0) without time zone', + }) + createdAt: Date = new Date(); + + @Property({ + length: 0, + onUpdate: () => new Date(), + name: 'updated_at', + nullable: true, + columnType: 'timestamp(0) without time zone', + defaultRaw: 'CURRENT_TIMESTAMP(0)', + }) + updatedAt: Date = new Date(); + + @ManyToOne(() => Users, { + fieldName: 'created_by', + nullable: true, + }) + createdBy!: IUsers; +} diff --git a/libs/microorm-database/src/lib/entities/index.ts b/libs/microorm-database/src/lib/entities/index.ts new file mode 100644 index 00000000..cd841496 --- /dev/null +++ b/libs/microorm-database/src/lib/entities/index.ts @@ -0,0 +1,5 @@ +export * from './users'; +export * from './roles'; +export * from './addresses'; +export * from './comments'; +export * from './book-list'; diff --git a/libs/microorm-database/src/lib/entities/roles.ts b/libs/microorm-database/src/lib/entities/roles.ts new file mode 100644 index 00000000..3dcd026d --- /dev/null +++ b/libs/microorm-database/src/lib/entities/roles.ts @@ -0,0 +1,67 @@ +import { + Entity, + PrimaryKey, + Property, + ManyToMany, + Collection, +} from '@mikro-orm/core'; + +import { Users, IUsers } from '.'; + +export type IRoles = Roles; + +@Entity({ + tableName: 'roles', +}) +export class Roles { + @PrimaryKey({ + autoincrement: true, + }) + public id!: number; + + @Property({ + type: 'varchar', + length: 128, + nullable: true, + default: 'NULL', + }) + public name!: string; + + @Property({ + type: 'varchar', + length: 128, + nullable: false, + unique: true, + }) + public key!: string; + + @Property({ + name: 'is_default', + type: 'boolean', + default: false, + }) + public isDefault!: boolean; + + @Property({ + length: 0, + name: 'created_at', + nullable: true, + defaultRaw: 'CURRENT_TIMESTAMP(0)', + columnType: 'timestamp(0) without time zone', + type: 'timestamp', + }) + createdAt: Date = new Date(); + + @Property({ + length: 0, + onUpdate: () => new Date(), + name: 'updated_at', + nullable: true, + columnType: 'timestamp(0) without time zone', + defaultRaw: 'CURRENT_TIMESTAMP(0)', + }) + updatedAt: Date = new Date(); + + @ManyToMany(() => Users, (item) => item.roles) + public users = new Collection(this); +} diff --git a/libs/microorm-database/src/lib/entities/users.ts b/libs/microorm-database/src/lib/entities/users.ts new file mode 100644 index 00000000..df71bb83 --- /dev/null +++ b/libs/microorm-database/src/lib/entities/users.ts @@ -0,0 +1,106 @@ +import { + Entity, + PrimaryKey, + Property, + ManyToMany, + OneToOne, + Collection, + OneToMany, + ArrayType, +} from '@mikro-orm/core'; + +import { Roles, Addresses, IAddresses, Comments, BookList } from './'; + +export type IUsers = Users; + +@Entity({ + tableName: 'users', +}) +export class Users { + @PrimaryKey({ + autoincrement: true, + }) + public id!: number; + + @Property({ + type: 'varchar', + length: 100, + nullable: false, + unique: true, + }) + public login!: string; + + @Property({ + name: 'first_name', + type: 'varchar', + length: 100, + nullable: true, + default: 'NULL', + }) + public firstName!: string; + + @Property({ + name: 'last_name', + type: 'varchar', + length: 100, + nullable: true, + default: 'NULL', + }) + public lastName!: string; + + @Property({ + name: 'is_active', + type: 'boolean', + nullable: true, + default: false, + }) + public isActive!: boolean; + + @Property({ + length: 0, + name: 'created_at', + nullable: true, + defaultRaw: 'CURRENT_TIMESTAMP(0)', + columnType: 'timestamp(0) without time zone', + type: 'timestamp', + }) + createdAt: Date = new Date(); + + @Property({ + length: 0, + onUpdate: () => new Date(), + name: 'updated_at', + nullable: true, + columnType: 'timestamp(0) without time zone', + defaultRaw: 'CURRENT_TIMESTAMP(0)', + }) + updatedAt: Date = new Date(); + + @ManyToMany(() => Roles, (role) => role.users, { + owner: true, + pivotTable: 'users_have_roles', + }) + public roles = new Collection(this); + + @OneToOne(() => Addresses, { + owner: true, + fieldName: 'addresses_id', + }) + public addresses!: IAddresses; + + @OneToOne(() => Users, { + owner: true, + nullable: true, + fieldName: 'manager_id', + }) + public manager!: IUsers; + + @OneToMany(() => Comments, (comment) => comment.createdBy) + comments = new Collection(this); + + @ManyToMany(() => BookList, (item) => item.users, { + owner: true, + pivotTable: 'users_have_book', + }) + public books = new Collection(this); +} diff --git a/libs/microorm-database/src/lib/micro-orm-database.module.ts b/libs/microorm-database/src/lib/micro-orm-database.module.ts new file mode 100644 index 00000000..a742dfdd --- /dev/null +++ b/libs/microorm-database/src/lib/micro-orm-database.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { MikroOrmModule } from '@mikro-orm/nestjs'; + +import { config } from './config'; + +@Module({ + imports: [MikroOrmModule.forRoot(config)], + exports: [MikroOrmModule], +}) +export class MicroOrmDatabaseModule {} diff --git a/libs/microorm-database/src/lib/migrations/.snapshot-microorm-test.json b/libs/microorm-database/src/lib/migrations/.snapshot-microorm-test.json new file mode 100644 index 00000000..52e0868e --- /dev/null +++ b/libs/microorm-database/src/lib/migrations/.snapshot-microorm-test.json @@ -0,0 +1,726 @@ +{ + "namespaces": [ + "public" + ], + "name": "public", + "tables": [ + { + "columns": { + "id": { + "name": "id", + "type": "serial", + "unsigned": false, + "autoincrement": true, + "primary": true, + "nullable": false, + "mappedType": "integer" + }, + "city": { + "name": "city", + "type": "varchar", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 70, + "mappedType": "string" + }, + "state": { + "name": "state", + "type": "varchar", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 70, + "mappedType": "string" + }, + "country": { + "name": "country", + "type": "varchar", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 68, + "mappedType": "string" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(0) without time zone", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 0, + "default": "current_timestamp(0)", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(0) without time zone", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 0, + "default": "current_timestamp(0)", + "mappedType": "datetime" + } + }, + "name": "addresses", + "schema": "public", + "indexes": [ + { + "keyName": "addresses_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "constraint": true, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": {}, + "nativeEnums": { + "comment_kind_enum": { + "name": "comment_kind_enum", + "schema": "public", + "items": [ + "COMMENT", + "MESSAGE", + "NOTE" + ] + } + } + }, + { + "columns": { + "id": { + "name": "id", + "type": "uuid", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "uuid_generate_v4()", + "mappedType": "uuid" + }, + "text": { + "name": "text", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(0) without time zone", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 0, + "default": "current_timestamp(0)", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(0) without time zone", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 0, + "default": "current_timestamp(0)", + "mappedType": "datetime" + } + }, + "name": "book_list", + "schema": "public", + "indexes": [ + { + "keyName": "book_list_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "constraint": true, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": {}, + "nativeEnums": { + "comment_kind_enum": { + "name": "comment_kind_enum", + "schema": "public", + "items": [ + "COMMENT", + "MESSAGE", + "NOTE" + ] + } + } + }, + { + "columns": { + "id": { + "name": "id", + "type": "serial", + "unsigned": false, + "autoincrement": true, + "primary": true, + "nullable": false, + "mappedType": "integer" + }, + "name": { + "name": "name", + "type": "varchar(128)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 128, + "default": "'NULL'", + "mappedType": "string" + }, + "key": { + "name": "key", + "type": "varchar(128)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 128, + "mappedType": "string" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "false", + "mappedType": "boolean" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(0) without time zone", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 0, + "default": "current_timestamp(0)", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(0) without time zone", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 0, + "default": "current_timestamp(0)", + "mappedType": "datetime" + } + }, + "name": "roles", + "schema": "public", + "indexes": [ + { + "columnNames": [ + "key" + ], + "composite": false, + "keyName": "roles_key_unique", + "constraint": true, + "primary": false, + "unique": true + }, + { + "keyName": "roles_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "constraint": true, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": {}, + "nativeEnums": { + "comment_kind_enum": { + "name": "comment_kind_enum", + "schema": "public", + "items": [ + "COMMENT", + "MESSAGE", + "NOTE" + ] + } + } + }, + { + "columns": { + "id": { + "name": "id", + "type": "serial", + "unsigned": false, + "autoincrement": true, + "primary": true, + "nullable": false, + "mappedType": "integer" + }, + "login": { + "name": "login", + "type": "varchar(100)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 100, + "mappedType": "string" + }, + "first_name": { + "name": "first_name", + "type": "varchar(100)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 100, + "default": "'NULL'", + "mappedType": "string" + }, + "last_name": { + "name": "last_name", + "type": "varchar(100)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 100, + "default": "'NULL'", + "mappedType": "string" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "default": "false", + "mappedType": "boolean" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(0) without time zone", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 0, + "default": "current_timestamp(0)", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(0) without time zone", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 0, + "default": "current_timestamp(0)", + "mappedType": "datetime" + }, + "addresses_id": { + "name": "addresses_id", + "type": "int", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "integer" + }, + "manager_id": { + "name": "manager_id", + "type": "int", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "integer" + } + }, + "name": "users", + "schema": "public", + "indexes": [ + { + "columnNames": [ + "login" + ], + "composite": false, + "keyName": "users_login_unique", + "constraint": true, + "primary": false, + "unique": true + }, + { + "columnNames": [ + "addresses_id" + ], + "composite": false, + "keyName": "users_addresses_id_unique", + "constraint": true, + "primary": false, + "unique": true + }, + { + "columnNames": [ + "manager_id" + ], + "composite": false, + "keyName": "users_manager_id_unique", + "constraint": true, + "primary": false, + "unique": true + }, + { + "keyName": "users_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "constraint": true, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "users_addresses_id_foreign": { + "constraintName": "users_addresses_id_foreign", + "columnNames": [ + "addresses_id" + ], + "localTableName": "public.users", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.addresses", + "updateRule": "cascade" + }, + "users_manager_id_foreign": { + "constraintName": "users_manager_id_foreign", + "columnNames": [ + "manager_id" + ], + "localTableName": "public.users", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.users", + "deleteRule": "set null", + "updateRule": "cascade" + } + }, + "nativeEnums": { + "comment_kind_enum": { + "name": "comment_kind_enum", + "schema": "public", + "items": [ + "COMMENT", + "MESSAGE", + "NOTE" + ] + } + } + }, + { + "columns": { + "id": { + "name": "id", + "type": "serial", + "unsigned": false, + "autoincrement": true, + "primary": true, + "nullable": false, + "mappedType": "integer" + }, + "text": { + "name": "text", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "kind": { + "name": "kind", + "type": "comment_kind_enum", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "nativeEnumName": "comment_kind_enum", + "enumItems": [ + "COMMENT", + "MESSAGE", + "NOTE" + ], + "mappedType": "enum" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(0) without time zone", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 0, + "default": "current_timestamp(0)", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(0) without time zone", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 0, + "default": "current_timestamp(0)", + "mappedType": "datetime" + }, + "created_by": { + "name": "created_by", + "type": "int", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "integer" + } + }, + "name": "comments", + "schema": "public", + "indexes": [ + { + "keyName": "comments_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "constraint": true, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "comments_created_by_foreign": { + "constraintName": "comments_created_by_foreign", + "columnNames": [ + "created_by" + ], + "localTableName": "public.comments", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.users", + "updateRule": "cascade" + } + }, + "nativeEnums": { + "comment_kind_enum": { + "name": "comment_kind_enum", + "schema": "public", + "items": [ + "COMMENT", + "MESSAGE", + "NOTE" + ] + } + } + }, + { + "columns": { + "users_id": { + "name": "users_id", + "type": "int", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "integer" + }, + "book_list_id": { + "name": "book_list_id", + "type": "uuid", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "uuid" + } + }, + "name": "users_have_book", + "schema": "public", + "indexes": [ + { + "keyName": "users_have_book_pkey", + "columnNames": [ + "users_id", + "book_list_id" + ], + "composite": true, + "constraint": true, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "users_have_book_users_id_foreign": { + "constraintName": "users_have_book_users_id_foreign", + "columnNames": [ + "users_id" + ], + "localTableName": "public.users_have_book", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.users", + "deleteRule": "cascade", + "updateRule": "cascade" + }, + "users_have_book_book_list_id_foreign": { + "constraintName": "users_have_book_book_list_id_foreign", + "columnNames": [ + "book_list_id" + ], + "localTableName": "public.users_have_book", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.book_list", + "deleteRule": "cascade", + "updateRule": "cascade" + } + }, + "nativeEnums": { + "comment_kind_enum": { + "name": "comment_kind_enum", + "schema": "public", + "items": [ + "COMMENT", + "MESSAGE", + "NOTE" + ] + } + } + }, + { + "columns": { + "users_id": { + "name": "users_id", + "type": "int", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "integer" + }, + "roles_id": { + "name": "roles_id", + "type": "int", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "integer" + } + }, + "name": "users_have_roles", + "schema": "public", + "indexes": [ + { + "keyName": "users_have_roles_pkey", + "columnNames": [ + "users_id", + "roles_id" + ], + "composite": true, + "constraint": true, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "users_have_roles_users_id_foreign": { + "constraintName": "users_have_roles_users_id_foreign", + "columnNames": [ + "users_id" + ], + "localTableName": "public.users_have_roles", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.users", + "deleteRule": "cascade", + "updateRule": "cascade" + }, + "users_have_roles_roles_id_foreign": { + "constraintName": "users_have_roles_roles_id_foreign", + "columnNames": [ + "roles_id" + ], + "localTableName": "public.users_have_roles", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.roles", + "deleteRule": "cascade", + "updateRule": "cascade" + } + }, + "nativeEnums": { + "comment_kind_enum": { + "name": "comment_kind_enum", + "schema": "public", + "items": [ + "COMMENT", + "MESSAGE", + "NOTE" + ] + } + } + } + ], + "nativeEnums": { + "comment_kind_enum": { + "name": "comment_kind_enum", + "schema": "public", + "items": [ + "COMMENT", + "MESSAGE", + "NOTE" + ] + } + } +} diff --git a/libs/microorm-database/src/lib/migrations/Migration20250123104848_CreateUsersTable.ts b/libs/microorm-database/src/lib/migrations/Migration20250123104848_CreateUsersTable.ts new file mode 100644 index 00000000..9b142690 --- /dev/null +++ b/libs/microorm-database/src/lib/migrations/Migration20250123104848_CreateUsersTable.ts @@ -0,0 +1,14 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20250123104848_CreateUsersTable extends Migration { + + override async up(): Promise { + this.addSql(`create table "users" ("id" serial primary key, "login" varchar(100) not null, "first_name" varchar(100) null default 'NULL', "last_name" varchar(100) null default 'NULL', "is_active" boolean null default false, "created_at" timestamp(0) without time zone null default current_timestamp(0), "updated_at" timestamp(0) without time zone null default current_timestamp(0));`); + this.addSql(`alter table "users" add constraint "users_login_unique" unique ("login");`); + } + + override async down(): Promise { + this.addSql(`drop table if exists "users" cascade;`); + } + +} diff --git a/libs/microorm-database/src/lib/migrations/Migration20250123105611_CreateAddressesTable.ts b/libs/microorm-database/src/lib/migrations/Migration20250123105611_CreateAddressesTable.ts new file mode 100644 index 00000000..087dc967 --- /dev/null +++ b/libs/microorm-database/src/lib/migrations/Migration20250123105611_CreateAddressesTable.ts @@ -0,0 +1,13 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20250123105611_CreateAddressesTable extends Migration { + + override async up(): Promise { + this.addSql(`create table "addresses" ("id" serial primary key, "city" varchar(70) null, "state" varchar(70) null, "country" varchar(68) null, "created_at" timestamp(0) without time zone null default current_timestamp(0), "updated_at" timestamp(0) without time zone null default current_timestamp(0));`); + } + + override async down(): Promise { + this.addSql(`drop table if exists "addresses" cascade;`); + } + +} diff --git a/libs/microorm-database/src/lib/migrations/Migration20250123110115_CreateRolesTable.ts b/libs/microorm-database/src/lib/migrations/Migration20250123110115_CreateRolesTable.ts new file mode 100644 index 00000000..e5fc0f89 --- /dev/null +++ b/libs/microorm-database/src/lib/migrations/Migration20250123110115_CreateRolesTable.ts @@ -0,0 +1,14 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20250123110115_CreateRolesTable extends Migration { + + override async up(): Promise { + this.addSql(`create table "roles" ("id" serial primary key, "name" varchar(128) null default 'NULL', "key" varchar(128) not null, "is_default" boolean not null default 'false', "created_at" timestamp(0) without time zone null default current_timestamp(0), "updated_at" timestamp(0) without time zone null default current_timestamp(0));`); + this.addSql(`alter table "roles" add constraint "roles_key_unique" unique ("key");`); + } + + override async down(): Promise { + this.addSql(`drop table if exists "roles" cascade;`); + } + +} diff --git a/libs/microorm-database/src/lib/migrations/Migration20250123111042_CreateCommentsTable.ts b/libs/microorm-database/src/lib/migrations/Migration20250123111042_CreateCommentsTable.ts new file mode 100644 index 00000000..84816d8c --- /dev/null +++ b/libs/microorm-database/src/lib/migrations/Migration20250123111042_CreateCommentsTable.ts @@ -0,0 +1,16 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20250123111042_CreateCommentsTable extends Migration { + + override async up(): Promise { + this.addSql(`create type "comment_kind_enum" as enum ('COMMENT', 'MESSAGE', 'NOTE');`); + this.addSql(`create table "comments" ("id" serial primary key, "text" text not null, "kind" "comment_kind_enum" not null, "created_at" timestamp(0) without time zone null default current_timestamp(0), "updated_at" timestamp(0) without time zone null default current_timestamp(0));`); + } + + override async down(): Promise { + this.addSql(`drop table if exists "comments" cascade;`); + + this.addSql(`drop type "comment_kind_enum";`); + } + +} diff --git a/libs/microorm-database/src/lib/migrations/Migration20250123123708_CreateUsersRolesRelations.ts b/libs/microorm-database/src/lib/migrations/Migration20250123123708_CreateUsersRolesRelations.ts new file mode 100644 index 00000000..f7e2577d --- /dev/null +++ b/libs/microorm-database/src/lib/migrations/Migration20250123123708_CreateUsersRolesRelations.ts @@ -0,0 +1,20 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20250123123708_CreateUsersRolesRelations extends Migration { + override async up(): Promise { + this.addSql( + `create table "users_have_roles" ("users_id" int not null, "roles_id" int not null, constraint "users_have_roles_pkey" primary key ("users_id", "roles_id"));` + ); + + this.addSql( + `alter table "users_have_roles" add constraint "users_have_roles_users_id_foreign" foreign key ("users_id") references "users" ("id") on update cascade on delete cascade;` + ); + this.addSql( + `alter table "users_have_roles" add constraint "users_have_roles_roles_id_foreign" foreign key ("roles_id") references "roles" ("id") on update cascade on delete cascade;` + ); + } + + override async down(): Promise { + this.addSql(`drop table if exists "users_have_roles" cascade;`); + } +} diff --git a/libs/microorm-database/src/lib/migrations/Migration20250123124745_CreateUsersUsersRelations.ts b/libs/microorm-database/src/lib/migrations/Migration20250123124745_CreateUsersUsersRelations.ts new file mode 100644 index 00000000..58cdf1a6 --- /dev/null +++ b/libs/microorm-database/src/lib/migrations/Migration20250123124745_CreateUsersUsersRelations.ts @@ -0,0 +1,18 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20250123124745_CreateUsersUsersRelations extends Migration { + + override async up(): Promise { + this.addSql(`alter table "users" add column "manager_id" int null;`); + this.addSql(`alter table "users" add constraint "users_manager_id_foreign" foreign key ("manager_id") references "users" ("id") on update cascade on delete set null;`); + this.addSql(`alter table "users" add constraint "users_manager_id_unique" unique ("manager_id");`); + } + + override async down(): Promise { + this.addSql(`alter table "users" drop constraint "users_manager_id_foreign";`); + + this.addSql(`alter table "users" drop constraint "users_manager_id_unique";`); + this.addSql(`alter table "users" drop column "manager_id";`); + } + +} diff --git a/libs/microorm-database/src/lib/migrations/Migration20250123125941_CreateUsersAddressRelations.ts b/libs/microorm-database/src/lib/migrations/Migration20250123125941_CreateUsersAddressRelations.ts new file mode 100644 index 00000000..c866f5b3 --- /dev/null +++ b/libs/microorm-database/src/lib/migrations/Migration20250123125941_CreateUsersAddressRelations.ts @@ -0,0 +1,18 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20250123125941_CreateUsersAddressRelations extends Migration { + + override async up(): Promise { + this.addSql(`alter table "users" add column "addresses_id" int not null;`); + this.addSql(`alter table "users" add constraint "users_addresses_id_foreign" foreign key ("addresses_id") references "addresses" ("id") on update cascade;`); + this.addSql(`alter table "users" add constraint "users_addresses_id_unique" unique ("addresses_id");`); + } + + override async down(): Promise { + this.addSql(`alter table "users" drop constraint "users_addresses_id_foreign";`); + + this.addSql(`alter table "users" drop constraint "users_addresses_id_unique";`); + this.addSql(`alter table "users" drop column "addresses_id";`); + } + +} diff --git a/libs/microorm-database/src/lib/migrations/Migration20250123130345_CreateUsersCommentsRelations.ts b/libs/microorm-database/src/lib/migrations/Migration20250123130345_CreateUsersCommentsRelations.ts new file mode 100644 index 00000000..223764d3 --- /dev/null +++ b/libs/microorm-database/src/lib/migrations/Migration20250123130345_CreateUsersCommentsRelations.ts @@ -0,0 +1,16 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20250123130345_CreateUsersCommentsRelations extends Migration { + + override async up(): Promise { + this.addSql(`alter table "comments" add column "created_by" int not null;`); + this.addSql(`alter table "comments" add constraint "comments_created_by_foreign" foreign key ("created_by") references "users" ("id") on update cascade;`); + } + + override async down(): Promise { + this.addSql(`alter table "comments" drop constraint "comments_created_by_foreign";`); + + this.addSql(`alter table "comments" drop column "created_by";`); + } + +} diff --git a/libs/microorm-database/src/lib/migrations/Migration20250123131039_CreateBookListTable.ts b/libs/microorm-database/src/lib/migrations/Migration20250123131039_CreateBookListTable.ts new file mode 100644 index 00000000..4392881d --- /dev/null +++ b/libs/microorm-database/src/lib/migrations/Migration20250123131039_CreateBookListTable.ts @@ -0,0 +1,14 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20250123131039_CreateBookListTable extends Migration { + override async up(): Promise { + this.addSql(`create extension if not exists "uuid-ossp";`); + this.addSql( + `create table "book_list" ("id" uuid not null default uuid_generate_v4(), "text" text not null, "created_at" timestamp(0) without time zone null default current_timestamp(0), "updated_at" timestamp(0) without time zone null default current_timestamp(0), constraint "book_list_pkey" primary key ("id"));` + ); + } + + override async down(): Promise { + this.addSql(`drop table if exists "book_list" cascade;`); + } +} diff --git a/libs/microorm-database/src/lib/migrations/Migration20250123131438_CreateUsersBookListRelations.ts b/libs/microorm-database/src/lib/migrations/Migration20250123131438_CreateUsersBookListRelations.ts new file mode 100644 index 00000000..35df79b8 --- /dev/null +++ b/libs/microorm-database/src/lib/migrations/Migration20250123131438_CreateUsersBookListRelations.ts @@ -0,0 +1,16 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20250123131438_CreateUsersBookListRelations extends Migration { + + override async up(): Promise { + this.addSql(`create table "users_have_book" ("users_id" int not null, "book_list_id" uuid not null, constraint "users_have_book_pkey" primary key ("users_id", "book_list_id"));`); + + this.addSql(`alter table "users_have_book" add constraint "users_have_book_users_id_foreign" foreign key ("users_id") references "users" ("id") on update cascade on delete cascade;`); + this.addSql(`alter table "users_have_book" add constraint "users_have_book_book_list_id_foreign" foreign key ("book_list_id") references "book_list" ("id") on update cascade on delete cascade;`); + } + + override async down(): Promise { + this.addSql(`drop table if exists "users_have_book" cascade;`); + } + +} diff --git a/libs/shared-utils/tsconfig.json b/libs/microorm-database/tsconfig.json similarity index 81% rename from libs/shared-utils/tsconfig.json rename to libs/microorm-database/tsconfig.json index f5b85657..6f7169a3 100644 --- a/libs/shared-utils/tsconfig.json +++ b/libs/microorm-database/tsconfig.json @@ -5,9 +5,9 @@ "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true }, "files": [], "include": [], diff --git a/libs/shared-utils/tsconfig.lib.json b/libs/microorm-database/tsconfig.lib.json similarity index 50% rename from libs/shared-utils/tsconfig.lib.json rename to libs/microorm-database/tsconfig.lib.json index 33eca2c2..c297a248 100644 --- a/libs/shared-utils/tsconfig.lib.json +++ b/libs/microorm-database/tsconfig.lib.json @@ -3,7 +3,13 @@ "compilerOptions": { "outDir": "../../dist/out-tsc", "declaration": true, - "types": ["node"] + "types": ["node"], + "target": "es2021", + "strictNullChecks": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true }, "include": ["src/**/*.ts"], "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] diff --git a/libs/shared-utils/tsconfig.spec.json b/libs/microorm-database/tsconfig.spec.json similarity index 88% rename from libs/shared-utils/tsconfig.spec.json rename to libs/microorm-database/tsconfig.spec.json index 9b2a121d..0d3c604e 100644 --- a/libs/shared-utils/tsconfig.spec.json +++ b/libs/microorm-database/tsconfig.spec.json @@ -3,6 +3,7 @@ "compilerOptions": { "outDir": "../../dist/out-tsc", "module": "commonjs", + "moduleResolution": "node10", "types": ["jest", "node"] }, "include": [ diff --git a/libs/shared-utils/.eslintrc.json b/libs/shared-utils/.eslintrc.json deleted file mode 100644 index 9d9c0db5..00000000 --- a/libs/shared-utils/.eslintrc.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": ["../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], - "overrides": [ - { - "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], - "rules": {} - }, - { - "files": ["*.ts", "*.tsx"], - "rules": {} - }, - { - "files": ["*.js", "*.jsx"], - "rules": {} - } - ] -} diff --git a/libs/shared-utils/README.md b/libs/shared-utils/README.md deleted file mode 100644 index 038826ff..00000000 --- a/libs/shared-utils/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# shared-utils - -This library was generated with [Nx](https://nx.dev). - -## Running unit tests - -Run `nx test shared-utils` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/shared-utils/project.json b/libs/shared-utils/project.json deleted file mode 100644 index f67eee36..00000000 --- a/libs/shared-utils/project.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "shared-utils", - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "libs/shared-utils/src", - "projectType": "library", - "targets": { - "build1": { - "executor": "@nx/js:tsc", - "outputs": ["{options.outputPath}"], - "options": { - "outputPath": "dist/libs/shared-utils", - "tsConfig": "libs/shared-utils/tsconfig.lib.json", - "packageJson": "libs/shared-utils/package.json", - "main": "libs/shared-utils/src/index.ts", - "assets": ["libs/shared-utils/*.md"] - } - }, - "test": { - "executor": "@nx/jest:jest", - "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], - "options": { - "jestConfig": "libs/shared-utils/jest.config.ts" - } - } - }, - "tags": [] -} diff --git a/libs/shared-utils/src/lib/types/index.ts b/libs/shared-utils/src/lib/types/index.ts deleted file mode 100644 index c0f762d2..00000000 --- a/libs/shared-utils/src/lib/types/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './utils-string.type'; diff --git a/libs/json-api/json-shared-type/.eslintrc.json b/libs/typeorm-database/.eslintrc.json similarity index 85% rename from libs/json-api/json-shared-type/.eslintrc.json rename to libs/typeorm-database/.eslintrc.json index 3456be9b..8d4e2111 100644 --- a/libs/json-api/json-shared-type/.eslintrc.json +++ b/libs/typeorm-database/.eslintrc.json @@ -1,5 +1,5 @@ { - "extends": ["../../../.eslintrc.json"], + "extends": ["../../.eslintrc.base.json"], "ignorePatterns": ["!**/*"], "overrides": [ { diff --git a/libs/json-api/json-shared-type/README.md b/libs/typeorm-database/README.md similarity index 55% rename from libs/json-api/json-shared-type/README.md rename to libs/typeorm-database/README.md index db1021f7..d231088b 100644 --- a/libs/json-api/json-shared-type/README.md +++ b/libs/typeorm-database/README.md @@ -1,7 +1,7 @@ -# json-shared-type +# typeorm-database This library was generated with [Nx](https://nx.dev). ## Running unit tests -Run `nx test json-shared-type` to execute the unit tests via [Jest](https://jestjs.io). +Run `nx test typeorm-database` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/database/jest.config.ts b/libs/typeorm-database/jest.config.ts similarity index 72% rename from libs/database/jest.config.ts rename to libs/typeorm-database/jest.config.ts index 6fef25b7..f4c0c3b5 100644 --- a/libs/database/jest.config.ts +++ b/libs/typeorm-database/jest.config.ts @@ -1,11 +1,11 @@ /* eslint-disable */ export default { - displayName: 'database', + displayName: 'typeorm-database', preset: '../../jest.preset.js', testEnvironment: 'node', transform: { '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], }, moduleFileExtensions: ['ts', 'js', 'html'], - coverageDirectory: '../../coverage/libs/database', + coverageDirectory: '../../coverage/libs/typeorm-database', }; diff --git a/libs/typeorm-database/project.json b/libs/typeorm-database/project.json new file mode 100644 index 00000000..58f32e84 --- /dev/null +++ b/libs/typeorm-database/project.json @@ -0,0 +1,9 @@ +{ + "name": "typeorm-database", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/typeorm-database/src", + "projectType": "library", + "tags": [], + "// targets": "to see all targets run: nx show project typeorm-database --web", + "targets": {} +} diff --git a/libs/typeorm-database/src/index.ts b/libs/typeorm-database/src/index.ts new file mode 100644 index 00000000..4f2a7e75 --- /dev/null +++ b/libs/typeorm-database/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/type-orm-database.module'; +export * from './lib/entities'; diff --git a/libs/database/src/lib/config-cli.ts b/libs/typeorm-database/src/lib/config-cli.ts similarity index 100% rename from libs/database/src/lib/config-cli.ts rename to libs/typeorm-database/src/lib/config-cli.ts diff --git a/libs/database/src/lib/config.ts b/libs/typeorm-database/src/lib/config.ts similarity index 100% rename from libs/database/src/lib/config.ts rename to libs/typeorm-database/src/lib/config.ts diff --git a/libs/database/src/lib/entities/addresses.ts b/libs/typeorm-database/src/lib/entities/addresses.ts similarity index 100% rename from libs/database/src/lib/entities/addresses.ts rename to libs/typeorm-database/src/lib/entities/addresses.ts diff --git a/libs/database/src/lib/entities/book-list.ts b/libs/typeorm-database/src/lib/entities/book-list.ts similarity index 100% rename from libs/database/src/lib/entities/book-list.ts rename to libs/typeorm-database/src/lib/entities/book-list.ts diff --git a/libs/database/src/lib/entities/comments.ts b/libs/typeorm-database/src/lib/entities/comments.ts similarity index 100% rename from libs/database/src/lib/entities/comments.ts rename to libs/typeorm-database/src/lib/entities/comments.ts diff --git a/libs/database/src/lib/entities/index.ts b/libs/typeorm-database/src/lib/entities/index.ts similarity index 100% rename from libs/database/src/lib/entities/index.ts rename to libs/typeorm-database/src/lib/entities/index.ts diff --git a/libs/database/src/lib/entities/roles.ts b/libs/typeorm-database/src/lib/entities/roles.ts similarity index 100% rename from libs/database/src/lib/entities/roles.ts rename to libs/typeorm-database/src/lib/entities/roles.ts diff --git a/libs/database/src/lib/entities/users-have-roles.ts b/libs/typeorm-database/src/lib/entities/users-have-roles.ts similarity index 94% rename from libs/database/src/lib/entities/users-have-roles.ts rename to libs/typeorm-database/src/lib/entities/users-have-roles.ts index 20d5d53c..38d400d2 100644 --- a/libs/database/src/lib/entities/users-have-roles.ts +++ b/libs/typeorm-database/src/lib/entities/users-have-roles.ts @@ -11,7 +11,7 @@ export class UsersHaveRoles { public id!: number; @Column({ - name: 'user_id', + name: 'users_id', type: 'int', nullable: false, unique: false, @@ -19,7 +19,7 @@ export class UsersHaveRoles { public userId!: number; @Column({ - name: 'role_id', + name: 'roles_id', type: 'int', nullable: false, unique: false, diff --git a/libs/database/src/lib/entities/users.ts b/libs/typeorm-database/src/lib/entities/users.ts similarity index 95% rename from libs/database/src/lib/entities/users.ts rename to libs/typeorm-database/src/lib/entities/users.ts index f0048c58..a709101e 100644 --- a/libs/database/src/lib/entities/users.ts +++ b/libs/typeorm-database/src/lib/entities/users.ts @@ -96,11 +96,11 @@ export class Users { name: 'users_have_roles', inverseJoinColumn: { referencedColumnName: 'id', - name: 'role_id', + name: 'roles_id', }, joinColumn: { referencedColumnName: 'id', - name: 'user_id', + name: 'users_id', }, }) public roles!: IRoles[]; @@ -113,11 +113,11 @@ export class Users { name: 'users_have_book', inverseJoinColumn: { referencedColumnName: 'id', - name: 'book_id', + name: 'book_list_id', }, joinColumn: { referencedColumnName: 'id', - name: 'user_id', + name: 'users_id', }, }) public books!: IBookList[]; diff --git a/libs/database/src/lib/migrations/1607701631900-CreateAddressesTable.ts b/libs/typeorm-database/src/lib/migrations/1607701631900-CreateAddressesTable.ts similarity index 100% rename from libs/database/src/lib/migrations/1607701631900-CreateAddressesTable.ts rename to libs/typeorm-database/src/lib/migrations/1607701631900-CreateAddressesTable.ts diff --git a/libs/database/src/lib/migrations/1607701632000-CreateUsersTable.ts b/libs/typeorm-database/src/lib/migrations/1607701632000-CreateUsersTable.ts similarity index 100% rename from libs/database/src/lib/migrations/1607701632000-CreateUsersTable.ts rename to libs/typeorm-database/src/lib/migrations/1607701632000-CreateUsersTable.ts diff --git a/libs/database/src/lib/migrations/1607701632200-CreateRolesTable.ts b/libs/typeorm-database/src/lib/migrations/1607701632200-CreateRolesTable.ts similarity index 100% rename from libs/database/src/lib/migrations/1607701632200-CreateRolesTable.ts rename to libs/typeorm-database/src/lib/migrations/1607701632200-CreateRolesTable.ts diff --git a/libs/database/src/lib/migrations/1607701632300-CreateUsersHaveRolesTable.ts b/libs/typeorm-database/src/lib/migrations/1607701632300-CreateUsersHaveRolesTable.ts similarity index 90% rename from libs/database/src/lib/migrations/1607701632300-CreateUsersHaveRolesTable.ts rename to libs/typeorm-database/src/lib/migrations/1607701632300-CreateUsersHaveRolesTable.ts index fcd5ff95..6c0f068e 100644 --- a/libs/database/src/lib/migrations/1607701632300-CreateUsersHaveRolesTable.ts +++ b/libs/typeorm-database/src/lib/migrations/1607701632300-CreateUsersHaveRolesTable.ts @@ -26,13 +26,13 @@ export class CreateUsersHaveRolesTable1607701632300 generationStrategy: 'increment', }), new TableColumn({ - name: 'user_id', + name: 'users_id', type: 'int', isNullable: false, unsigned: true, }), new TableColumn({ - name: 'role_id', + name: 'roles_id', type: 'int', isNullable: false, unsigned: true, @@ -54,17 +54,17 @@ export class CreateUsersHaveRolesTable1607701632300 new TableForeignKey({ referencedTableName: 'users', referencedColumnNames: ['id'], - columnNames: ['user_id'], + columnNames: ['users_id'], }), new TableForeignKey({ referencedTableName: 'roles', referencedColumnNames: ['id'], - columnNames: ['role_id'], + columnNames: ['roles_id'], }), ], indices: [ new TableIndex({ - columnNames: ['user_id', 'role_id'], + columnNames: ['users_id', 'roles_id'], isUnique: true, }), ], diff --git a/libs/database/src/lib/migrations/1607701632600-CreateCommentsTable.ts b/libs/typeorm-database/src/lib/migrations/1607701632600-CreateCommentsTable.ts similarity index 100% rename from libs/database/src/lib/migrations/1607701632600-CreateCommentsTable.ts rename to libs/typeorm-database/src/lib/migrations/1607701632600-CreateCommentsTable.ts diff --git a/libs/database/src/lib/migrations/1665469071344-CreateBookTable.ts b/libs/typeorm-database/src/lib/migrations/1665469071344-CreateBookTable.ts similarity index 100% rename from libs/database/src/lib/migrations/1665469071344-CreateBookTable.ts rename to libs/typeorm-database/src/lib/migrations/1665469071344-CreateBookTable.ts diff --git a/libs/database/src/lib/migrations/1665719467563-CreateUsersHasBookTable.ts b/libs/typeorm-database/src/lib/migrations/1665719467563-CreateUsersHasBookTable.ts similarity index 89% rename from libs/database/src/lib/migrations/1665719467563-CreateUsersHasBookTable.ts rename to libs/typeorm-database/src/lib/migrations/1665719467563-CreateUsersHasBookTable.ts index e713b4bf..d91ac132 100644 --- a/libs/database/src/lib/migrations/1665719467563-CreateUsersHasBookTable.ts +++ b/libs/typeorm-database/src/lib/migrations/1665719467563-CreateUsersHasBookTable.ts @@ -26,13 +26,13 @@ export class CreateUsersHaveBookTable1665719467563 generationStrategy: 'increment', }), new TableColumn({ - name: 'user_id', + name: 'users_id', type: 'int', isNullable: false, unsigned: true, }), new TableColumn({ - name: 'book_id', + name: 'book_list_id', type: 'uuid', isNullable: false, unsigned: true, @@ -54,17 +54,17 @@ export class CreateUsersHaveBookTable1665719467563 new TableForeignKey({ referencedTableName: 'users', referencedColumnNames: ['id'], - columnNames: ['user_id'], + columnNames: ['users_id'], }), new TableForeignKey({ referencedTableName: 'book_list', referencedColumnNames: ['id'], - columnNames: ['book_id'], + columnNames: ['book_list_id'], }), ], indices: [ new TableIndex({ - columnNames: ['user_id', 'book_id'], + columnNames: ['users_id', 'book_list_id'], isUnique: true, }), ], diff --git a/libs/database/src/lib/seeders/factory/addresses.factory.ts b/libs/typeorm-database/src/lib/seeders/factory/addresses.factory.ts similarity index 100% rename from libs/database/src/lib/seeders/factory/addresses.factory.ts rename to libs/typeorm-database/src/lib/seeders/factory/addresses.factory.ts diff --git a/libs/database/src/lib/seeders/factory/comments.factory.ts b/libs/typeorm-database/src/lib/seeders/factory/comments.factory.ts similarity index 100% rename from libs/database/src/lib/seeders/factory/comments.factory.ts rename to libs/typeorm-database/src/lib/seeders/factory/comments.factory.ts diff --git a/libs/database/src/lib/seeders/factory/index.ts b/libs/typeorm-database/src/lib/seeders/factory/index.ts similarity index 100% rename from libs/database/src/lib/seeders/factory/index.ts rename to libs/typeorm-database/src/lib/seeders/factory/index.ts diff --git a/libs/database/src/lib/seeders/factory/roles.factory.ts b/libs/typeorm-database/src/lib/seeders/factory/roles.factory.ts similarity index 100% rename from libs/database/src/lib/seeders/factory/roles.factory.ts rename to libs/typeorm-database/src/lib/seeders/factory/roles.factory.ts diff --git a/libs/database/src/lib/seeders/factory/user.factory.ts b/libs/typeorm-database/src/lib/seeders/factory/user.factory.ts similarity index 100% rename from libs/database/src/lib/seeders/factory/user.factory.ts rename to libs/typeorm-database/src/lib/seeders/factory/user.factory.ts diff --git a/libs/database/src/lib/seeders/root.seeder.ts b/libs/typeorm-database/src/lib/seeders/root.seeder.ts similarity index 100% rename from libs/database/src/lib/seeders/root.seeder.ts rename to libs/typeorm-database/src/lib/seeders/root.seeder.ts diff --git a/libs/database/src/lib/database.module.ts b/libs/typeorm-database/src/lib/type-orm-database.module.ts similarity index 84% rename from libs/database/src/lib/database.module.ts rename to libs/typeorm-database/src/lib/type-orm-database.module.ts index 7a796bf4..39a65f32 100644 --- a/libs/database/src/lib/database.module.ts +++ b/libs/typeorm-database/src/lib/type-orm-database.module.ts @@ -7,4 +7,4 @@ import { config } from './config'; imports: [TypeOrmModule.forRoot(config)], exports: [TypeOrmModule], }) -export class DatabaseModule {} +export class TypeOrmDatabaseModule {} diff --git a/libs/database/tsconfig.json b/libs/typeorm-database/tsconfig.json similarity index 100% rename from libs/database/tsconfig.json rename to libs/typeorm-database/tsconfig.json diff --git a/libs/database/tsconfig.lib.json b/libs/typeorm-database/tsconfig.lib.json similarity index 100% rename from libs/database/tsconfig.lib.json rename to libs/typeorm-database/tsconfig.lib.json diff --git a/libs/database/tsconfig.spec.json b/libs/typeorm-database/tsconfig.spec.json similarity index 100% rename from libs/database/tsconfig.spec.json rename to libs/typeorm-database/tsconfig.spec.json diff --git a/migrations.json b/migrations.json new file mode 100644 index 00000000..a153a5fe --- /dev/null +++ b/migrations.json @@ -0,0 +1,142 @@ +{ + "migrations": [ + { + "version": "20.0.0-beta.7", + "description": "Migration for v20.0.0-beta.7", + "implementation": "./src/migrations/update-20-0-0/move-use-daemon-process", + "package": "nx", + "name": "move-use-daemon-process" + }, + { + "version": "20.0.1", + "description": "Set `useLegacyCache` to true for migrating workspaces", + "implementation": "./src/migrations/update-20-0-1/use-legacy-cache", + "x-repair-skip": true, + "package": "nx", + "name": "use-legacy-cache" + }, + { + "cli": "nx", + "version": "20.0.0-beta.5", + "description": "replace getJestProjects with getJestProjectsAsync", + "implementation": "./src/migrations/update-20-0-0/replace-getJestProjects-with-getJestProjectsAsync", + "package": "@nx/jest", + "name": "replace-getJestProjects-with-getJestProjectsAsync" + }, + { + "version": "20.2.0-beta.5", + "description": "Update TypeScript ESLint packages to v8.13.0 if they are already on v8", + "implementation": "./src/migrations/update-20-2-0/update-typescript-eslint-v8-13-0", + "package": "@nx/eslint", + "name": "update-typescript-eslint-v8.13.0" + }, + { + "cli": "nx", + "version": "19.6.1-beta.0", + "description": "Ensure Target Defaults are set correctly for Module Federation.", + "factory": "./src/migrations/update-19-6-1/ensure-depends-on-for-mf", + "package": "@nx/angular", + "name": "update-19-6-1-ensure-module-federation-target-defaults" + }, + { + "cli": "nx", + "version": "20.2.0-beta.2", + "description": "Update the ModuleFederationConfig import use @nx/module-federation.", + "factory": "./src/migrations/update-20-2-0/migrate-mf-imports-to-new-package", + "package": "@nx/angular", + "name": "update-20-2-0-update-module-federation-config-import" + }, + { + "cli": "nx", + "version": "20.2.0-beta.2", + "description": "Update the withModuleFederation import use @nx/module-federation/angular.", + "factory": "./src/migrations/update-20-2-0/migrate-with-mf-import-to-new-package", + "package": "@nx/angular", + "name": "update-20-2-0-update-with-module-federation-import" + }, + { + "cli": "nx", + "version": "20.2.0-beta.5", + "requires": { "@angular/core": ">=19.0.0" }, + "description": "Update the @angular/cli package version to ~19.0.0.", + "factory": "./src/migrations/update-20-2-0/update-angular-cli", + "package": "@nx/angular", + "name": "update-angular-cli-version-19-0-0" + }, + { + "cli": "nx", + "version": "20.2.0-beta.5", + "requires": { "@angular/core": ">=19.0.0" }, + "description": "Add the '@angular/localize/init' polyfill to the 'polyfills' option of targets using esbuild-based executors.", + "factory": "./src/migrations/update-20-2-0/add-localize-polyfill-to-targets", + "package": "@nx/angular", + "name": "add-localize-polyfill-to-targets" + }, + { + "cli": "nx", + "version": "20.2.0-beta.5", + "requires": { "@angular/core": ">=19.0.0" }, + "description": "Update '@angular/ssr' import paths to use the new '/node' entry point when 'CommonEngine' is detected.", + "factory": "./src/migrations/update-20-2-0/update-angular-ssr-imports-to-use-node-entry-point", + "package": "@nx/angular", + "name": "update-angular-ssr-imports-to-use-node-entry-point" + }, + { + "cli": "nx", + "version": "20.2.0-beta.6", + "requires": { "@angular/core": ">=19.0.0" }, + "description": "Disable the Angular ESLint prefer-standalone rule if not set.", + "factory": "./src/migrations/update-20-2-0/disable-angular-eslint-prefer-standalone", + "package": "@nx/angular", + "name": "disable-angular-eslint-prefer-standalone" + }, + { + "cli": "nx", + "version": "20.2.0-beta.8", + "requires": { "@angular/core": ">=19.0.0" }, + "description": "Remove Angular ESLint rules that were removed in v19.0.0.", + "factory": "./src/migrations/update-20-2-0/remove-angular-eslint-rules", + "package": "@nx/angular", + "name": "remove-angular-eslint-rules" + }, + { + "cli": "nx", + "version": "20.2.0-beta.8", + "requires": { "@angular/core": ">=19.0.0" }, + "description": "Remove the deprecated 'tailwindConfig' option from ng-packagr executors. Tailwind CSS configurations located at the project or workspace root will be picked up automatically.", + "factory": "./src/migrations/update-20-2-0/remove-tailwind-config-from-ng-packagr-executors", + "package": "@nx/angular", + "name": "remove-tailwind-config-from-ng-packagr-executors" + }, + { + "cli": "nx", + "version": "19.6.3-beta.0", + "description": "Migrate proxy config files to match new format from webpack-dev-server v5.", + "implementation": "./src/migrations/update-19-6-3/proxy-config", + "package": "@nx/webpack", + "name": "update-19-6-3-proxy-config" + }, + { + "version": "19.0.0", + "description": "Updates non-standalone Directives, Component and Pipes to 'standalone:false' and removes 'standalone:true' from those who are standalone", + "factory": "./bundles/explicit-standalone-flag#migrate", + "package": "@angular/core", + "name": "explicit-standalone-flag" + }, + { + "version": "19.0.0", + "description": "Updates ExperimentalPendingTasks to PendingTasks", + "factory": "./bundles/pending-tasks#migrate", + "package": "@angular/core", + "name": "pending-tasks" + }, + { + "version": "19.0.0", + "description": "Replaces `APP_INITIALIZER`, `ENVIRONMENT_INITIALIZER` & `PLATFORM_INITIALIZER` respectively with `provideAppInitializer`, `provideEnvironmentInitializer` & `providePlatformInitializer`.", + "factory": "./bundles/provide-initializer#migrate", + "optional": true, + "package": "@angular/core", + "name": "provide-initializer" + } + ] +} diff --git a/nx.json b/nx.json index 4cd992f1..01229c6f 100644 --- a/nx.json +++ b/nx.json @@ -85,10 +85,12 @@ "!json-api-server", "!json-api-front", "!shared-utils", - "!database", + "!typeorm-database", + "!microorm-database", "!type-for-rpc" ], "version": { + "preVersionCommand": "npx nx run-many -t build", "conventionalCommits": true, "generatorOptions": { "fallbackCurrentVersionResolver": "1.0.0" diff --git a/package-lock.json b/package-lock.json index 1f80a0a5..2a46cce3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "0.0.0", "license": "MIT", "dependencies": { - "@anatine/zod-nestjs": "^2.0.5", "@anatine/zod-openapi": "^2.2.3", "@angular/animations": "19.0.3", "@angular/common": "19.0.3", @@ -19,6 +18,13 @@ "@angular/platform-browser": "19.0.3", "@angular/platform-browser-dynamic": "19.0.3", "@angular/router": "19.0.3", + "@mikro-orm/cli": "^6.4.3", + "@mikro-orm/core": "^6.4.3", + "@mikro-orm/migrations": "^6.4.3", + "@mikro-orm/mysql": "^6.4.3", + "@mikro-orm/nestjs": "^6.0.2", + "@mikro-orm/postgresql": "^6.4.3", + "@mikro-orm/sql-highlighter": "^1.0.1", "@nestjs/common": "^10.3.0", "@nestjs/core": "^10.3.0", "@nestjs/platform-express": "10.3.3", @@ -39,8 +45,8 @@ "tslib": "^2.3.0", "typeorm": "^0.3.20", "uuid": "^10.0.0", - "zod": "^3.22.4", - "zod-validation-error": "^3.0.2", + "zod": "^3.24.1", + "zod-validation-error": "^3.4.0", "zone.js": "0.15.0" }, "devDependencies": { @@ -53,6 +59,7 @@ "@angular/cli": "~19.0.0", "@angular/compiler-cli": "19.0.3", "@angular/language-service": "19.0.3", + "@electric-sql/pglite": "^0.2.16", "@faker-js/faker": "^8.4.1", "@jorgebodega/typeorm-factory": "^1.4.0", "@jorgebodega/typeorm-seeding": "^6.0.1", @@ -83,9 +90,10 @@ "eslint-config-prettier": "^9.0.0", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", - "jest-environment-node": "^29.4.1", + "jest-environment-node": "^29.7.0", "jest-preset-angular": "14.4.2", "jsonc-eslint-parser": "^2.1.0", + "knex-pglite": "^0.11.0", "ng-packagr": "19.0.1", "nx": "20.2.1", "pg-mem": "^3.0.2", @@ -94,6 +102,7 @@ "prettier": "^2.6.2", "ts-jest": "^29.1.0", "ts-node": "10.9.1", + "typeorm-pglite": "^0.3.2", "typescript": "5.6.3", "verdaccio": "^5.0.4", "webpack-cli": "^5.1.4" @@ -127,25 +136,10 @@ "node": ">=6.0.0" } }, - "node_modules/@anatine/zod-nestjs": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@anatine/zod-nestjs/-/zod-nestjs-2.0.9.tgz", - "integrity": "sha512-XEK+7wMXAxc4tOkzOpH/vav1MVZrVYeOwKpXmn7aFiTUoB08G1FzAP7rDQ90ZrIFOGSoC0hpJA9izPQxBRAIDg==", - "dependencies": { - "ts-deepmerge": "^6.1.0" - }, - "peerDependencies": { - "@anatine/zod-openapi": "^2.0.1", - "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", - "@nestjs/swagger": "^6.0.0 || ^7.0.0", - "openapi3-ts": "^4.1.2", - "zod": "^3.20.0" - } - }, "node_modules/@anatine/zod-openapi": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@anatine/zod-openapi/-/zod-openapi-2.2.6.tgz", - "integrity": "sha512-Z5sr2Nq2xifEpPbPdUcvyl776LY652oR3VHMV++WFSmRrRL8RDP2XTkbuGn+vgfVNOD7UrndYwCWnxaiw7IZog==", + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@anatine/zod-openapi/-/zod-openapi-2.2.7.tgz", + "integrity": "sha512-kv/bGowgSGHNY2d/KIzx941ym0/elc7xoBiPri31qEUqbDPOSIppiMOZ88AedaTtLk5J1K96++h0CEsHkgFFyQ==", "dependencies": { "ts-deepmerge": "^6.0.3" }, @@ -2742,6 +2736,12 @@ "node": ">=10.0.0" } }, + "node_modules/@electric-sql/pglite": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.2.16.tgz", + "integrity": "sha512-dCSHpoOKuTxecaYhWDRp2yFTN3XWcMPMrBVl5yOR8VZEUprz4+R3iuU7BipmlsqBnBDO/6l9H/C2ZwJdunkWyw==", + "dev": true + }, "node_modules/@emnapi/core": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.3.1.tgz", @@ -3755,6 +3755,31 @@ "node": ">=8" } }, + "node_modules/@jercle/yargonaut": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@jercle/yargonaut/-/yargonaut-1.1.5.tgz", + "integrity": "sha512-zBp2myVvBHp1UaJsNTyS6q4UDKT7eRiqTS4oNTS6VQMd6mpxYOdbeK4pY279cDCdakGy6hG0J3ejoXZVsPwHqw==", + "dependencies": { + "chalk": "^4.1.2", + "figlet": "^1.5.2", + "parent-require": "^1.0.0" + } + }, + "node_modules/@jercle/yargonaut/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/@jest/console": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", @@ -4469,6 +4494,261 @@ "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.0.tgz", "integrity": "sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA==" }, + "node_modules/@mikro-orm/cli": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/@mikro-orm/cli/-/cli-6.4.3.tgz", + "integrity": "sha512-DWnYNxoyMgU6L90TGBlT0eziTu6yl15ArnnFoq0kyOjp8JEMRjin+8cizSrKyQ3QiQZ5iop5fB0i9Sp+Hbgd8Q==", + "dependencies": { + "@jercle/yargonaut": "1.1.5", + "@mikro-orm/core": "6.4.3", + "@mikro-orm/knex": "6.4.3", + "fs-extra": "11.2.0", + "tsconfig-paths": "4.2.0", + "yargs": "17.7.2" + }, + "bin": { + "mikro-orm": "cli", + "mikro-orm-esm": "esm" + }, + "engines": { + "node": ">= 18.12.0" + } + }, + "node_modules/@mikro-orm/cli/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@mikro-orm/core": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/@mikro-orm/core/-/core-6.4.3.tgz", + "integrity": "sha512-UTaqKs1bomYtGmEEZ8sNBOmW2OqT5NcMh+pBV2iJ6WLM5MuiIEuNhDMuvvPE5gNEwUzc1HyRhUV87bRDhDIGRg==", + "dependencies": { + "dataloader": "2.2.3", + "dotenv": "16.4.7", + "esprima": "4.0.1", + "fs-extra": "11.2.0", + "globby": "11.1.0", + "mikro-orm": "6.4.3", + "reflect-metadata": "0.2.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/b4nan" + } + }, + "node_modules/@mikro-orm/core/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@mikro-orm/core/node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" + }, + "node_modules/@mikro-orm/knex": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/@mikro-orm/knex/-/knex-6.4.3.tgz", + "integrity": "sha512-gVkRD/cIn6qxk/P9nR+IufZxJwuCCdv0AtcGvShxXXvaoIrQPJYDV7HRxBOHCEyNygr6M3Fqpph1oPoT6aezTQ==", + "dependencies": { + "fs-extra": "11.2.0", + "knex": "3.1.0", + "sqlstring": "2.3.3" + }, + "engines": { + "node": ">= 18.12.0" + }, + "peerDependencies": { + "@mikro-orm/core": "^6.0.0", + "better-sqlite3": "*", + "libsql": "*", + "mariadb": "*" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "libsql": { + "optional": true + }, + "mariadb": { + "optional": true + } + } + }, + "node_modules/@mikro-orm/knex/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@mikro-orm/migrations": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/@mikro-orm/migrations/-/migrations-6.4.3.tgz", + "integrity": "sha512-VrsKq95esUBEMhwp9vVX+YUj2+/cNwb8UZ63HfgaqPo+pYj8r1RBSTboFOE9V0Md0n3ol9b5xByfPPa3qHmL0g==", + "dependencies": { + "@mikro-orm/knex": "6.4.3", + "fs-extra": "11.2.0", + "umzug": "3.8.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "peerDependencies": { + "@mikro-orm/core": "^6.0.0" + } + }, + "node_modules/@mikro-orm/migrations/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@mikro-orm/mysql": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/@mikro-orm/mysql/-/mysql-6.4.3.tgz", + "integrity": "sha512-ZkrrzOWE9ouifU331q70K9BfAOD9SFRiNLNnECnzVrvDPWnthMV0ahGium9HyHpG4nev0Ybg6vnvq9IQzW1brg==", + "dependencies": { + "@mikro-orm/knex": "6.4.3", + "mysql2": "3.12.0" + }, + "engines": { + "node": ">= 18.12.0" + }, + "peerDependencies": { + "@mikro-orm/core": "^6.0.0" + } + }, + "node_modules/@mikro-orm/nestjs": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@mikro-orm/nestjs/-/nestjs-6.0.2.tgz", + "integrity": "sha512-3bUBnJ0HwwIjsNPb0n7dfTWayqA5iSLHv/zM9juyBADSpJB2oBAd96QlUqDvNi/RckSg0m+ifLVT0Uw8e0+POg==", + "engines": { + "node": ">= 18.12.0" + }, + "peerDependencies": { + "@mikro-orm/core": "^6.0.0 || ^6.0.0-dev.0", + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0" + } + }, + "node_modules/@mikro-orm/postgresql": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/@mikro-orm/postgresql/-/postgresql-6.4.3.tgz", + "integrity": "sha512-3cGi1gW6ME3SyuRRiJmSBtzHFa6Kavy6bK9rsSAAfXz+Pso6UBsqvesATbruKxDF7/CLdQlIY3CZZHXksUIrQg==", + "dependencies": { + "@mikro-orm/knex": "6.4.3", + "pg": "8.13.1", + "postgres-array": "3.0.2", + "postgres-date": "2.1.0", + "postgres-interval": "4.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "peerDependencies": { + "@mikro-orm/core": "^6.0.0" + } + }, + "node_modules/@mikro-orm/postgresql/node_modules/pg": { + "version": "8.13.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.1.tgz", + "integrity": "sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==", + "dependencies": { + "pg-connection-string": "^2.7.0", + "pg-pool": "^3.7.0", + "pg-protocol": "^1.7.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/@mikro-orm/postgresql/node_modules/pg-connection-string": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", + "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==" + }, + "node_modules/@mikro-orm/postgresql/node_modules/postgres-array": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", + "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==", + "engines": { + "node": ">=12" + } + }, + "node_modules/@mikro-orm/postgresql/node_modules/postgres-date": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", + "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/@mikro-orm/postgresql/node_modules/postgres-interval": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-4.0.2.tgz", + "integrity": "sha512-EMsphSQ1YkQqKZL2cuG0zHkmjCCzQqQ71l2GXITqRwjhRleCdv00bDk/ktaSi0LnlaPzAc3535KTrjXsTdtx7A==", + "engines": { + "node": ">=12" + } + }, + "node_modules/@mikro-orm/sql-highlighter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@mikro-orm/sql-highlighter/-/sql-highlighter-1.0.1.tgz", + "integrity": "sha512-iO+FwRNuqNDVlIo5zfgOu2mMGVicX/FqzP+F/A0xpJLHyqvWyXzVwntgAMimBjQaxiX9Rpmc0u3Jq6/A6V6JQA==", + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/@module-federation/bridge-react-webpack-plugin": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/@module-federation/bridge-react-webpack-plugin/-/bridge-react-webpack-plugin-0.7.6.tgz", @@ -5554,7 +5834,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -5567,7 +5846,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "engines": { "node": ">= 8" } @@ -5576,7 +5854,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -8439,18 +8716,186 @@ "integrity": "sha512-mMhRFH0k2VjwHt3Jol9JkUsmI/4XlrAoBG3E0o7HoyoPYv1UFOWyqAflfANcUPgbYpvqmyLzDcO+3IT36LXnrA==", "dev": true, "dependencies": { - "@module-federation/runtime": "0.5.1", - "@module-federation/sdk": "0.5.1" + "@module-federation/runtime": "0.5.1", + "@module-federation/sdk": "0.5.1" + } + }, + "node_modules/@rspack/lite-tapable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rspack/lite-tapable/-/lite-tapable-1.0.1.tgz", + "integrity": "sha512-VynGOEsVw2s8TAlLf/uESfrgfrq2+rcXB1muPJYBWbsm1Oa6r5qVQhjA5ggM6z/coYPrsVMgovl3Ff7Q7OCp1w==", + "dev": true, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@rushstack/node-core-library": { + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.10.2.tgz", + "integrity": "sha512-xOF/2gVJZTfjTxbo4BDj9RtQq/HFnrrKdtem4JkyRLnwsRz2UDTg8gA1/et10fBx5RxmZD9bYVGST69W8ME5OQ==", + "dependencies": { + "ajv": "~8.13.0", + "ajv-draft-04": "~1.0.0", + "ajv-formats": "~3.0.1", + "fs-extra": "~7.0.1", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.22.1", + "semver": "~7.5.4" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/node-core-library/node_modules/ajv": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.13.0.tgz", + "integrity": "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.4.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@rushstack/node-core-library/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/@rushstack/terminal": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.14.5.tgz", + "integrity": "sha512-TEOpNwwmsZVrkp0omnuTUTGZRJKTr6n6m4OITiNjkqzLAkcazVpwR1SOtBg6uzpkIBLgrcNHETqI8rbw3uiUfw==", + "dependencies": { + "@rushstack/node-core-library": "5.10.2", + "supports-color": "~8.1.1" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/terminal/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/@rushstack/ts-command-line": { + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.23.3.tgz", + "integrity": "sha512-HazKL8fv4HMQMzrKJCrOrhyBPPdzk7iajUXgsASwjQ8ROo1cmgyqxt/k9+SdmrNLGE1zATgRqMUH3s/6smbRMA==", + "dependencies": { + "@rushstack/terminal": "0.14.5", + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "string-argv": "~0.3.1" + } + }, + "node_modules/@rushstack/ts-command-line/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" } }, - "node_modules/@rspack/lite-tapable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rspack/lite-tapable/-/lite-tapable-1.0.1.tgz", - "integrity": "sha512-VynGOEsVw2s8TAlLf/uESfrgfrq2+rcXB1muPJYBWbsm1Oa6r5qVQhjA5ggM6z/coYPrsVMgovl3Ff7Q7OCp1w==", - "dev": true, - "engines": { - "node": ">=16.0.0" - } + "node_modules/@rushstack/ts-command-line/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" }, "node_modules/@schematics/angular": { "version": "19.0.4", @@ -8962,6 +9407,11 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/argparse": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", + "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -10560,7 +11010,7 @@ "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dev": true, + "devOptional": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -10572,6 +11022,19 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ajv-formats": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", @@ -10605,7 +11068,6 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, "engines": { "node": ">=6" } @@ -10746,7 +11208,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, "engines": { "node": ">=8" } @@ -10843,6 +11304,14 @@ "node": "*" } }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/aws4": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", @@ -11409,7 +11878,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "dependencies": { "fill-range": "^7.1.1" }, @@ -12923,6 +13391,11 @@ "node": ">=12" } }, + "node_modules/dataloader": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.3.tgz", + "integrity": "sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==" + }, "node_modules/date-format": { "version": "4.0.14", "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", @@ -13062,6 +13535,14 @@ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "dev": true }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -13149,7 +13630,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, "dependencies": { "path-type": "^4.0.0" }, @@ -13256,9 +13736,9 @@ } }, "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", "engines": { "node": ">=12" }, @@ -13367,7 +13847,6 @@ "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, "engines": { "node": ">=12" }, @@ -14010,6 +14489,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "engines": { + "node": ">=6" + } + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -14031,7 +14518,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -14330,14 +14816,12 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -14393,7 +14877,6 @@ "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, "dependencies": { "reusify": "^1.0.4" } @@ -14419,6 +14902,17 @@ "bser": "2.1.1" } }, + "node_modules/figlet": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.8.0.tgz", + "integrity": "sha512-chzvGjd+Sp7KUvPHZv6EXV5Ir3Q7kYNpCr4aHrRW79qFtTefmQZNny+W1pW9kf5zeE6dikku2W50W/wAH2xWgw==", + "bin": { + "figlet": "bin/index.js" + }, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -14471,7 +14965,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -14997,6 +15490,14 @@ "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", "dev": true }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -15048,7 +15549,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, "engines": { "node": ">=8.0.0" } @@ -15065,6 +15565,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/getopts": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz", + "integrity": "sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==" + }, "node_modules/getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -15098,7 +15603,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -15195,7 +15699,6 @@ "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -15225,8 +15728,7 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, "node_modules/graphemer": { "version": "1.4.0", @@ -15788,7 +16290,6 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", - "dev": true, "engines": { "node": ">= 4" } @@ -15849,6 +16350,14 @@ "node": ">=4" } }, + "node_modules/import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "engines": { + "node": ">=8" + } + }, "node_modules/import-local": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", @@ -15962,7 +16471,6 @@ "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dev": true, "dependencies": { "hasown": "^2.0.0" }, @@ -15989,7 +16497,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -16030,7 +16537,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -16096,7 +16602,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "engines": { "node": ">=0.12.0" } @@ -16146,6 +16651,11 @@ "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", "dev": true }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -17352,6 +17862,11 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -17493,8 +18008,7 @@ "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "node_modules/json-stable-stringify": { "version": "1.1.1", @@ -17530,7 +18044,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "bin": { "json5": "lib/cli.js" }, @@ -17566,7 +18079,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, "dependencies": { "universalify": "^2.0.0" }, @@ -17723,6 +18235,87 @@ "node": ">= 8" } }, + "node_modules/knex": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/knex/-/knex-3.1.0.tgz", + "integrity": "sha512-GLoII6hR0c4ti243gMs5/1Rb3B+AjwMOfjYm97pu0FOQa7JH56hgBxYf5WK2525ceSbBY1cjeZ9yk99GPMB6Kw==", + "dependencies": { + "colorette": "2.0.19", + "commander": "^10.0.0", + "debug": "4.3.4", + "escalade": "^3.1.1", + "esm": "^3.2.25", + "get-package-type": "^0.1.0", + "getopts": "2.3.0", + "interpret": "^2.2.0", + "lodash": "^4.17.21", + "pg-connection-string": "2.6.2", + "rechoir": "^0.8.0", + "resolve-from": "^5.0.0", + "tarn": "^3.0.2", + "tildify": "2.0.0" + }, + "bin": { + "knex": "bin/cli.js" + }, + "engines": { + "node": ">=16" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "mysql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/knex-pglite": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/knex-pglite/-/knex-pglite-0.11.0.tgz", + "integrity": "sha512-Z3v+vaF8C/VMJll7J2NHqFZo31ijLfYBsHMKU8jTnjfIF0edUonAB3sbAZYmUbSeJDDGfZdJPsvcB4ZyGBERcA==", + "dev": true, + "dependencies": { + "@electric-sql/pglite": "^0.2.14", + "knex": "3.1.0" + } + }, + "node_modules/knex/node_modules/colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" + }, + "node_modules/knex/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "engines": { + "node": ">=14" + } + }, + "node_modules/knex/node_modules/interpret": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/koa": { "version": "2.15.3", "resolved": "https://registry.npmjs.org/koa/-/koa-2.15.3.tgz", @@ -18475,6 +19068,11 @@ "node": ">=8.0" } }, + "node_modules/long": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.4.tgz", + "integrity": "sha512-qtzLbJE8hq7VabR3mISmVGtoXP8KGc2Z/AT8OuqlYD7JTR3oqrgwdjnk07wpj1twXxYmgDXgoKVWUG/fReSzHg==" + }, "node_modules/long-timeout": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz", @@ -18528,6 +19126,20 @@ "yallist": "^3.0.2" } }, + "node_modules/lru.min": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.1.tgz", + "integrity": "sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q==", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/luxon": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", @@ -18666,7 +19278,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "engines": { "node": ">= 8" } @@ -18683,7 +19294,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -18696,7 +19306,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "engines": { "node": ">=8.6" }, @@ -18704,6 +19313,14 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/mikro-orm": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/mikro-orm/-/mikro-orm-6.4.3.tgz", + "integrity": "sha512-xDNzmLiL4EUTMOu9CbZ2d0sNIaUdH4RzDv4oqw27+u0/FPfvZTIagd+luxx1lWWqe/vg/iNtvqr5OcNQIYYrtQ==", + "engines": { + "node": ">= 18.12.0" + } + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -19183,6 +19800,36 @@ "rimraf": "bin.js" } }, + "node_modules/mysql2": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.12.0.tgz", + "integrity": "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -19193,6 +19840,25 @@ "thenify-all": "^1.0.0" } }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, "node_modules/nanoclone": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz", @@ -20241,9 +20907,9 @@ } }, "node_modules/openapi3-ts/node_modules/yaml": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", - "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", "peer": true, "bin": { "yaml": "bin.mjs" @@ -20527,6 +21193,14 @@ "node": ">=6" } }, + "node_modules/parent-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parent-require/-/parent-require-1.0.0.tgz", + "integrity": "sha512-2MXDNZC4aXdkkap+rBBMv0lUsfJqvX5/2FiYYnfCnorZt3Pk06/IOR5KeaoghgS2w07MLWgjbsnyaq6PdHn2LQ==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -20681,8 +21355,7 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-scurry": { "version": "1.11.1", @@ -20716,7 +21389,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, "engines": { "node": ">=8" } @@ -20851,17 +21523,17 @@ "dev": true }, "node_modules/pg-pool": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", - "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz", + "integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==", "peerDependencies": { "pg": ">=8.0" } }, "node_modules/pg-protocol": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", - "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz", + "integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==" }, "node_modules/pg-types": { "version": "2.2.0", @@ -21063,6 +21735,14 @@ "node": ">=4" } }, + "node_modules/pony-cause": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/pony-cause/-/pony-cause-2.1.11.tgz", + "integrity": "sha512-M7LhCsdNbNgiLYiP4WjsfLUuFmCfnjdF6jKe2R9NKl4WFN+HZPGHJZ9lnLP7f9ZnKe3U9nuWD0szirmj+migUg==", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/portfinder": { "version": "1.0.32", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", @@ -21924,7 +22604,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "engines": { "node": ">=6" } @@ -21969,7 +22648,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -22137,7 +22815,6 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", - "dev": true, "dependencies": { "resolve": "^1.20.0" }, @@ -22245,7 +22922,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -22260,7 +22936,6 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -22302,7 +22977,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, "engines": { "node": ">=8" } @@ -22390,7 +23064,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -22470,7 +23143,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -22720,6 +23392,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -22947,7 +23624,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, "engines": { "node": ">=8" } @@ -23295,6 +23971,14 @@ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "dev": true }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/sshpk": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", @@ -23449,6 +24133,14 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -23682,7 +24374,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -23876,6 +24567,14 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/tarn": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz", + "integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/terser": { "version": "5.36.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz", @@ -24099,6 +24798,14 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, + "node_modules/tildify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", + "integrity": "sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==", + "engines": { + "node": ">=8" + } + }, "node_modules/tmp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", @@ -24118,7 +24825,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -24347,7 +25053,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", - "dev": true, "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", @@ -24391,7 +25096,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, "engines": { "node": ">=4" } @@ -24632,6 +25336,15 @@ } } }, + "node_modules/typeorm-pglite": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/typeorm-pglite/-/typeorm-pglite-0.3.2.tgz", + "integrity": "sha512-/URkIx3MXaRqLKXjGfFY1uq8Fp6qSYTP09NjK6MAbsZhNwe7qAqnaWdTGtJ4aMr2e2TDPXT1rCt+w7xszMUzOg==", + "dev": true, + "peerDependencies": { + "@electric-sql/pglite": ">= 0.2.12" + } + }, "node_modules/typeorm/node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -24759,6 +25472,32 @@ "node": ">=8" } }, + "node_modules/umzug": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/umzug/-/umzug-3.8.2.tgz", + "integrity": "sha512-BEWEF8OJjTYVC56GjELeHl/1XjFejrD7aHzn+HldRJTx+pL1siBrKHZC8n4K/xL3bEzVA9o++qD1tK2CpZu4KA==", + "dependencies": { + "@rushstack/ts-command-line": "^4.12.2", + "emittery": "^0.13.0", + "fast-glob": "^3.3.2", + "pony-cause": "^2.1.4", + "type-fest": "^4.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/umzug/node_modules/type-fest": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.33.0.tgz", + "integrity": "sha512-s6zVrxuyKbbAsSAD5ZPTB77q4YIdRctkTbJ2/Dqlinwz+8ooH2gd+YA7VA6Pa93KML9GockVvoxjZ2vHP+mu8g==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", @@ -24851,7 +25590,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, "engines": { "node": ">= 10.0.0" } @@ -24914,7 +25652,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "dependencies": { "punycode": "^2.1.0" } @@ -26748,17 +27485,17 @@ } }, "node_modules/zod": { - "version": "3.22.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", - "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", "funding": { "url": "https://github.com/sponsors/colinhacks" } }, "node_modules/zod-validation-error": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.0.2.tgz", - "integrity": "sha512-21xGaDmnU7lJZ4J63n5GXWqi+rTzGy3gDHbuZ1jP6xrK/DEQGyOqs/xW7eH96tIfCOYm+ecCuT0bfajBRKEVUw==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.4.0.tgz", + "integrity": "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==", "engines": { "node": ">=18.0.0" }, diff --git a/package.json b/package.json index 7bba1b85..4d3bccfd 100644 --- a/package.json +++ b/package.json @@ -3,16 +3,17 @@ "version": "0.0.0", "license": "MIT", "scripts": { - "typeorm": "ts-node -r tsconfig-paths/register -r dotenv/config --project libs/database/tsconfig.lib.json ./node_modules/typeorm/cli.js -d libs/database/src/lib/config-cli.ts", + "typeorm": "ts-node -r tsconfig-paths/register -r dotenv/config --project libs/typeorm-database/tsconfig.lib.json ./node_modules/typeorm/cli.js -d libs/typeorm-database/src/lib/config-cli.ts", "typeorm:run": "npm run typeorm migration:run", "typeorm:revert": "npm run typeorm migration:revert", - "seed:run": "ts-node -r tsconfig-paths/register -r dotenv/config --project libs/database/tsconfig.lib.json ./node_modules/@jorgebodega/typeorm-seeding/dist/cli.js -d libs/database/src/lib/config-cli.ts seed libs/database/src/lib/seeders/root.seeder.ts", + "seed:run": "ts-node -r tsconfig-paths/register -r dotenv/config --project libs/typeorm-database/tsconfig.lib.json ./node_modules/@jorgebodega/typeorm-seeding/dist/cli.js -d libs/typeorm-database/src/lib/config-cli.ts seed libs/typeorm-database/src/lib/seeders/root.seeder.ts", + "microorm": "ts-node -r tsconfig-paths/register -r dotenv/config -P ./libs/microorm-database/tsconfig.lib.json ./node_modules/@mikro-orm/cli/cli.js", + "microorm:up": "npm run microorm migration:up", "demo:json-api": "nx run json-api-server:serve:development", "demo:json-api-front": "nx run json-api-front:serve:development" }, "private": true, "dependencies": { - "@anatine/zod-nestjs": "^2.0.5", "@anatine/zod-openapi": "^2.2.3", "@angular/animations": "19.0.3", "@angular/common": "19.0.3", @@ -22,6 +23,13 @@ "@angular/platform-browser": "19.0.3", "@angular/platform-browser-dynamic": "19.0.3", "@angular/router": "19.0.3", + "@mikro-orm/cli": "^6.4.3", + "@mikro-orm/core": "^6.4.3", + "@mikro-orm/migrations": "^6.4.3", + "@mikro-orm/mysql": "^6.4.3", + "@mikro-orm/nestjs": "^6.0.2", + "@mikro-orm/postgresql": "^6.4.3", + "@mikro-orm/sql-highlighter": "^1.0.1", "@nestjs/common": "^10.3.0", "@nestjs/core": "^10.3.0", "@nestjs/platform-express": "10.3.3", @@ -42,8 +50,8 @@ "tslib": "^2.3.0", "typeorm": "^0.3.20", "uuid": "^10.0.0", - "zod": "^3.22.4", - "zod-validation-error": "^3.0.2", + "zod": "^3.24.1", + "zod-validation-error": "^3.4.0", "zone.js": "0.15.0" }, "devDependencies": { @@ -56,6 +64,7 @@ "@angular/cli": "~19.0.0", "@angular/compiler-cli": "19.0.3", "@angular/language-service": "19.0.3", + "@electric-sql/pglite": "^0.2.16", "@faker-js/faker": "^8.4.1", "@jorgebodega/typeorm-factory": "^1.4.0", "@jorgebodega/typeorm-seeding": "^6.0.1", @@ -86,9 +95,10 @@ "eslint-config-prettier": "^9.0.0", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", - "jest-environment-node": "^29.4.1", + "jest-environment-node": "^29.7.0", "jest-preset-angular": "14.4.2", "jsonc-eslint-parser": "^2.1.0", + "knex-pglite": "^0.11.0", "ng-packagr": "19.0.1", "nx": "20.2.1", "pg-mem": "^3.0.2", @@ -97,11 +107,18 @@ "prettier": "^2.6.2", "ts-jest": "^29.1.0", "ts-node": "10.9.1", + "typeorm-pglite": "^0.3.2", "typescript": "5.6.3", "verdaccio": "^5.0.4", "webpack-cli": "^5.1.4" }, "nx": { "includedScripts": [] + }, + "mikro-orm": { + "configPaths": [ + "libs/microorm-database/src/lib/config-cli.ts" + ], + "tsConfigPath": "./libs/microorm-database/tsconfig.lib.json" } } diff --git a/project.json b/project.json new file mode 100644 index 00000000..5b9f7ecd --- /dev/null +++ b/project.json @@ -0,0 +1,14 @@ +{ + "name": "@nestjs-json-api/source", + "$schema": "node_modules/nx/schemas/project-schema.json", + "targets": { + "local-registry": { + "executor": "@nx/js:verdaccio", + "options": { + "port": 4873, + "config": ".verdaccio/config.yml", + "storage": "tmp/local-registry/storage" + } + } + } +} diff --git a/tsconfig.base.json b/tsconfig.base.json index ac808542..b4cd5ce3 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -15,6 +15,18 @@ "skipDefaultLibCheck": true, "baseUrl": ".", "paths": { + "@klerick/json-api-nestjs": [ + "libs/json-api/json-api-nestjs/src/index.ts" + ], + "@klerick/json-api-nestjs-sdk": [ + "libs/json-api/json-api-nestjs-sdk/src/index.ts" + ], + "@klerick/json-api-nestjs-sdk/ngModule": [ + "libs/json-api/json-api-nestjs-sdk/src/ngModule.ts" + ], + "@klerick/json-api-nestjs-shared": [ + "libs/json-api/json-api-nestjs-shared/src/index.ts" + ], "@klerick/nestjs-json-rpc": [ "libs/json-rpc/nestjs-json-rpc/src/index.ts" ], @@ -24,15 +36,13 @@ "@klerick/nestjs-json-rpc-sdk/ngModule": [ "libs/json-rpc/nestjs-json-rpc-sdk/src/ngModule.ts" ], - "@nestjs-json-api/type-for-rpc": ["libs/type-for-rpc/src/index.ts"], - "database": ["libs/database/src/index.ts"], - "json-api-nestjs": ["libs/json-api/json-api-nestjs/src/index.ts"], - "json-api-nestjs-sdk": ["libs/json-api/json-api-nestjs-sdk/src/index.ts"], - "json-api-nestjs-sdk/ngModule": [ - "libs/json-api/json-api-nestjs-sdk/src/ngModule.ts" + "@nestjs-json-api/microorm-database": [ + "libs/microorm-database/src/index.ts" ], - "json-shared-type": ["libs/json-api/json-shared-type/src/index.ts"], - "shared-utils": ["libs/shared-utils/src/index.ts"] + "@nestjs-json-api/type-for-rpc": ["libs/type-for-rpc/src/index.ts"], + "@nestjs-json-api/typeorm-database": [ + "libs/typeorm-database/src/index.ts" + ] } }, "exclude": ["node_modules", "tmp"]