From 7e3f4ca2293a76121c4780f87c112338d192967c Mon Sep 17 00:00:00 2001 From: "amplication[bot]" Date: Mon, 4 Dec 2023 11:59:00 +0000 Subject: [PATCH] Amplication build # clpquybh702b8kc01cafujgl7 Congratulations on your first commit with Amplication! We encourage you to continue exploring the many ways Amplication can supercharge your development. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If you find Amplication useful, please show your support and give our GitHub repo a star ⭐️ This simple action helps our open-source project grow and reach more developers like you. Thank you and happy coding! Build URL: [https://app.amplication.com/clpquqhfb0r51ie01iy7zn1ct/clpquqhrf0r54ie0131612h27/clpquu1do0r59ie01scpeycqg/builds/clpquybh702b8kc01cafujgl7](https://app.amplication.com/clpquqhfb0r51ie01iy7zn1ct/clpquqhrf0r54ie0131612h27/clpquu1do0r59ie01scpeycqg/builds/clpquybh702b8kc01cafujgl7) --- server/.dockerignore | 8 + server/.env | 8 + server/.gitignore | 5 + server/.prettierignore | 5 + server/Dockerfile | 68 ++++ server/README.md | 64 ++++ server/docker-compose.dev.yml | 13 + server/docker-compose.yml | 47 +++ server/nest-cli.json | 6 + server/package.json | 73 ++++ server/prisma/schema.prisma | 23 ++ server/scripts/customSeed.ts | 7 + server/scripts/seed.ts | 25 ++ server/src/app.module.ts | 43 +++ server/src/author/author.controller.ts | 12 + server/src/author/author.module.ts | 13 + server/src/author/author.resolver.ts | 11 + server/src/author/author.service.ts | 10 + server/src/author/base/Author.ts | 46 +++ server/src/author/base/AuthorCountArgs.ts | 28 ++ server/src/author/base/AuthorCreateInput.ts | 41 +++ server/src/author/base/AuthorFindManyArgs.ts | 62 ++++ .../src/author/base/AuthorFindUniqueArgs.ts | 30 ++ .../author/base/AuthorListRelationFilter.ts | 56 +++ server/src/author/base/AuthorOrderByInput.ts | 45 +++ server/src/author/base/AuthorUpdateInput.ts | 44 +++ server/src/author/base/AuthorWhereInput.ts | 57 ++++ .../src/author/base/AuthorWhereUniqueInput.ts | 31 ++ ...BookCreateNestedManyWithoutAuthorsInput.ts | 28 ++ .../base/BookUpdateManyWithoutAuthorsInput.ts | 46 +++ server/src/author/base/CreateAuthorArgs.ts | 30 ++ server/src/author/base/DeleteAuthorArgs.ts | 30 ++ server/src/author/base/UpdateAuthorArgs.ts | 40 +++ .../base/author.controller.base.spec.ts | 168 +++++++++ .../src/author/base/author.controller.base.ts | 209 ++++++++++++ server/src/author/base/author.module.base.ts | 18 + .../src/author/base/author.resolver.base.ts | 111 ++++++ server/src/author/base/author.service.base.ts | 60 ++++ server/src/book/base/Book.ts | 61 ++++ server/src/book/base/BookCountArgs.ts | 28 ++ server/src/book/base/BookCreateInput.ts | 54 +++ server/src/book/base/BookFindManyArgs.ts | 62 ++++ server/src/book/base/BookFindUniqueArgs.ts | 30 ++ .../src/book/base/BookListRelationFilter.ts | 56 +++ server/src/book/base/BookOrderByInput.ts | 78 +++++ server/src/book/base/BookUpdateInput.ts | 66 ++++ server/src/book/base/BookWhereInput.ts | 80 +++++ server/src/book/base/BookWhereUniqueInput.ts | 31 ++ server/src/book/base/CreateBookArgs.ts | 30 ++ server/src/book/base/DeleteBookArgs.ts | 30 ++ server/src/book/base/UpdateBookArgs.ts | 40 +++ .../book/base/book.controller.base.spec.ts | 190 +++++++++++ server/src/book/base/book.controller.base.ts | 177 ++++++++++ server/src/book/base/book.module.base.ts | 18 + server/src/book/base/book.resolver.base.ts | 117 +++++++ server/src/book/base/book.service.base.ts | 57 ++++ server/src/book/book.controller.ts | 12 + server/src/book/book.module.ts | 13 + server/src/book/book.resolver.ts | 11 + server/src/book/book.service.ts | 10 + server/src/connectMicroservices.ts | 6 + .../decorators/api-nested-query.decorator.ts | 80 +++++ server/src/decorators/public.decorator.ts | 10 + server/src/errors.ts | 16 + server/src/filters/HttpExceptions.filter.ts | 89 +++++ .../src/health/base/health.controller.base.ts | 19 ++ server/src/health/base/health.service.base.ts | 15 + server/src/health/health.controller.ts | 10 + server/src/health/health.module.ts | 10 + server/src/health/health.service.ts | 10 + server/src/main.ts | 53 +++ server/src/prisma.util.spec.ts | 23 ++ server/src/prisma.util.ts | 29 ++ server/src/prisma/prisma.module.ts | 9 + server/src/prisma/prisma.service.ts | 9 + .../base/secretsManager.service.base.spec.ts | 41 +++ .../base/secretsManager.service.base.ts | 17 + .../secrets/secretsManager.module.ts | 8 + .../secrets/secretsManager.service.ts | 10 + .../providers/secrets/secretsNameKey.enum.ts | 1 + server/src/serveStaticOptions.service.ts | 39 +++ server/src/swagger.ts | 20 ++ server/src/swagger/favicon.png | Bin 0 -> 2498 bytes server/src/swagger/logo-amplication-white.svg | 15 + server/src/swagger/swagger.css | 321 ++++++++++++++++++ .../src/tests/health/health.service.spec.ts | 36 ++ server/src/types.ts | 3 + server/src/util/BooleanFilter.ts | 32 ++ server/src/util/BooleanNullableFilter.ts | 31 ++ server/src/util/DateTimeFilter.ts | 97 ++++++ server/src/util/DateTimeNullableFilter.ts | 97 ++++++ server/src/util/FloatFilter.ts | 98 ++++++ server/src/util/FloatNullableFilter.ts | 98 ++++++ server/src/util/IntFilter.ts | 98 ++++++ server/src/util/IntNullableFilter.ts | 98 ++++++ server/src/util/JsonFilter.ts | 31 ++ server/src/util/JsonNullableFilter.ts | 31 ++ server/src/util/MetaQueryPayload.ts | 13 + server/src/util/QueryMode.ts | 10 + server/src/util/SortOrder.ts | 10 + server/src/util/StringFilter.ts | 141 ++++++++ server/src/util/StringNullableFilter.ts | 141 ++++++++ server/src/validators/index.ts | 1 + .../is-json-value-validator.spec.ts | 44 +++ .../src/validators/is-json-value-validator.ts | 29 ++ server/tsconfig.build.json | 4 + server/tsconfig.json | 24 ++ 107 files changed, 4899 insertions(+) create mode 100644 server/.dockerignore create mode 100644 server/.env create mode 100644 server/.gitignore create mode 100644 server/.prettierignore create mode 100644 server/Dockerfile create mode 100644 server/README.md create mode 100644 server/docker-compose.dev.yml create mode 100644 server/docker-compose.yml create mode 100644 server/nest-cli.json create mode 100644 server/package.json create mode 100644 server/prisma/schema.prisma create mode 100644 server/scripts/customSeed.ts create mode 100644 server/scripts/seed.ts create mode 100644 server/src/app.module.ts create mode 100644 server/src/author/author.controller.ts create mode 100644 server/src/author/author.module.ts create mode 100644 server/src/author/author.resolver.ts create mode 100644 server/src/author/author.service.ts create mode 100644 server/src/author/base/Author.ts create mode 100644 server/src/author/base/AuthorCountArgs.ts create mode 100644 server/src/author/base/AuthorCreateInput.ts create mode 100644 server/src/author/base/AuthorFindManyArgs.ts create mode 100644 server/src/author/base/AuthorFindUniqueArgs.ts create mode 100644 server/src/author/base/AuthorListRelationFilter.ts create mode 100644 server/src/author/base/AuthorOrderByInput.ts create mode 100644 server/src/author/base/AuthorUpdateInput.ts create mode 100644 server/src/author/base/AuthorWhereInput.ts create mode 100644 server/src/author/base/AuthorWhereUniqueInput.ts create mode 100644 server/src/author/base/BookCreateNestedManyWithoutAuthorsInput.ts create mode 100644 server/src/author/base/BookUpdateManyWithoutAuthorsInput.ts create mode 100644 server/src/author/base/CreateAuthorArgs.ts create mode 100644 server/src/author/base/DeleteAuthorArgs.ts create mode 100644 server/src/author/base/UpdateAuthorArgs.ts create mode 100644 server/src/author/base/author.controller.base.spec.ts create mode 100644 server/src/author/base/author.controller.base.ts create mode 100644 server/src/author/base/author.module.base.ts create mode 100644 server/src/author/base/author.resolver.base.ts create mode 100644 server/src/author/base/author.service.base.ts create mode 100644 server/src/book/base/Book.ts create mode 100644 server/src/book/base/BookCountArgs.ts create mode 100644 server/src/book/base/BookCreateInput.ts create mode 100644 server/src/book/base/BookFindManyArgs.ts create mode 100644 server/src/book/base/BookFindUniqueArgs.ts create mode 100644 server/src/book/base/BookListRelationFilter.ts create mode 100644 server/src/book/base/BookOrderByInput.ts create mode 100644 server/src/book/base/BookUpdateInput.ts create mode 100644 server/src/book/base/BookWhereInput.ts create mode 100644 server/src/book/base/BookWhereUniqueInput.ts create mode 100644 server/src/book/base/CreateBookArgs.ts create mode 100644 server/src/book/base/DeleteBookArgs.ts create mode 100644 server/src/book/base/UpdateBookArgs.ts create mode 100644 server/src/book/base/book.controller.base.spec.ts create mode 100644 server/src/book/base/book.controller.base.ts create mode 100644 server/src/book/base/book.module.base.ts create mode 100644 server/src/book/base/book.resolver.base.ts create mode 100644 server/src/book/base/book.service.base.ts create mode 100644 server/src/book/book.controller.ts create mode 100644 server/src/book/book.module.ts create mode 100644 server/src/book/book.resolver.ts create mode 100644 server/src/book/book.service.ts create mode 100644 server/src/connectMicroservices.ts create mode 100644 server/src/decorators/api-nested-query.decorator.ts create mode 100644 server/src/decorators/public.decorator.ts create mode 100644 server/src/errors.ts create mode 100644 server/src/filters/HttpExceptions.filter.ts create mode 100644 server/src/health/base/health.controller.base.ts create mode 100644 server/src/health/base/health.service.base.ts create mode 100644 server/src/health/health.controller.ts create mode 100644 server/src/health/health.module.ts create mode 100644 server/src/health/health.service.ts create mode 100644 server/src/main.ts create mode 100644 server/src/prisma.util.spec.ts create mode 100644 server/src/prisma.util.ts create mode 100644 server/src/prisma/prisma.module.ts create mode 100644 server/src/prisma/prisma.service.ts create mode 100644 server/src/providers/secrets/base/secretsManager.service.base.spec.ts create mode 100644 server/src/providers/secrets/base/secretsManager.service.base.ts create mode 100644 server/src/providers/secrets/secretsManager.module.ts create mode 100644 server/src/providers/secrets/secretsManager.service.ts create mode 100644 server/src/providers/secrets/secretsNameKey.enum.ts create mode 100644 server/src/serveStaticOptions.service.ts create mode 100644 server/src/swagger.ts create mode 100644 server/src/swagger/favicon.png create mode 100644 server/src/swagger/logo-amplication-white.svg create mode 100644 server/src/swagger/swagger.css create mode 100644 server/src/tests/health/health.service.spec.ts create mode 100644 server/src/types.ts create mode 100644 server/src/util/BooleanFilter.ts create mode 100644 server/src/util/BooleanNullableFilter.ts create mode 100644 server/src/util/DateTimeFilter.ts create mode 100644 server/src/util/DateTimeNullableFilter.ts create mode 100644 server/src/util/FloatFilter.ts create mode 100644 server/src/util/FloatNullableFilter.ts create mode 100644 server/src/util/IntFilter.ts create mode 100644 server/src/util/IntNullableFilter.ts create mode 100644 server/src/util/JsonFilter.ts create mode 100644 server/src/util/JsonNullableFilter.ts create mode 100644 server/src/util/MetaQueryPayload.ts create mode 100644 server/src/util/QueryMode.ts create mode 100644 server/src/util/SortOrder.ts create mode 100644 server/src/util/StringFilter.ts create mode 100644 server/src/util/StringNullableFilter.ts create mode 100644 server/src/validators/index.ts create mode 100644 server/src/validators/is-json-value-validator.spec.ts create mode 100644 server/src/validators/is-json-value-validator.ts create mode 100644 server/tsconfig.build.json create mode 100644 server/tsconfig.json diff --git a/server/.dockerignore b/server/.dockerignore new file mode 100644 index 0000000..cb5c30b --- /dev/null +++ b/server/.dockerignore @@ -0,0 +1,8 @@ +.dockerignore +docker-compose.yml +Dockerfile +dist/ +node_modules +.env +.gitignore +.prettierignore \ No newline at end of file diff --git a/server/.env b/server/.env new file mode 100644 index 0000000..39ec7c6 --- /dev/null +++ b/server/.env @@ -0,0 +1,8 @@ +BCRYPT_SALT=10 +COMPOSE_PROJECT_NAME=amp_clpquu1do0r59ie01scpeycqg +PORT=3000 +DB_URL=postgres://admin:admin@localhost:5432/my-db +DB_USER=admin +DB_PASSWORD=admin +DB_PORT=5432 +DB_NAME=my-db \ No newline at end of file diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..08c9980 --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,5 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +/node_modules +/dist +.DS_Store diff --git a/server/.prettierignore b/server/.prettierignore new file mode 100644 index 0000000..e48f355 --- /dev/null +++ b/server/.prettierignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +prisma/migrations/ +package-lock.json +coverage/ \ No newline at end of file diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..80dd8d3 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,68 @@ +# multi-stage: base (build) +FROM node:18.13.0 AS base + +# create directory where the application will be built +WORKDIR /app + +# copy over the dependency manifests, both the package.json +# and the package-lock.json are copied over +COPY package*.json ./ + +# installs packages and their dependencies +RUN npm install + +# copy over the prisma schema +COPY prisma/schema.prisma ./prisma/ + +# generate the prisma client based on the schema +RUN npm run prisma:generate + +# copy over the code base +COPY . . + +# create the bundle of the application +RUN npm run build + +# multi-stage: production (runtime) +FROM node:18.13.0-slim AS production + +# create arguments of builds time variables +ARG user=amplication +ARG group=${user} +ARG uid=1001 +ARG gid=$uid + +# [temporary] work around to be able to run prisma +RUN apt-get update -y && apt-get install -y openssl + +# create directory where the application will be executed from +WORKDIR /app + +# add the user and group +RUN groupadd --gid ${gid} ${user} +RUN useradd --uid ${uid} --gid ${gid} -m ${user} + +# copy over the bundled code from the build stage +COPY --from=base /app/node_modules/ ./node_modules +COPY --from=base /app/package.json ./package.json +COPY --from=base /app/dist ./dist +COPY --from=base /app/prisma ./prisma +COPY --from=base /app/scripts ./scripts +COPY --from=base /app/src ./src +COPY --from=base /app/tsconfig* ./ + +# change ownership of the workspace directory +RUN chown -R ${uid}:${gid} /app/ + +# get rid of the development dependencies +RUN npm install --production + +# set user to the created non-privileged user +USER ${user} + +# expose a specific port on the docker container +ENV PORT=3000 +EXPOSE ${PORT} + +# start the server using the previously build application +CMD [ "node", "./dist/main.js" ] diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..6db8e75 --- /dev/null +++ b/server/README.md @@ -0,0 +1,64 @@ +

+ + amplication-logo + +

+ +# Introduction + +This service was generated with Amplication. The server-side of the generated project. This component provides the different backend services - i.e., REST API, GraphQL API, authentication, authorization, logging, data validation and the connection to the database. Additional information about the server component and the architecture around it, can be found on the [documentation](https://docs.amplication.com/guides/getting-started) site. + +# Getting started + +## Step 1: Configuration + +Configuration for the server component can be provided through the use of environment variables. These can be passed to the application via the use of the `.env` file in the base directory of the generated service. Below a table can be found which show the different variables that can be passed - these are the variables which exist by default, through the use of plugins additional integrations could require additional values. These values are provided default values after generation, change them to the desired values. + +| Variable | Description | Value | +| -------------------- | -------------------------------------------- | ------------------------------------------------------------------- | +| BCRYPT_SALT | the string used for hashing | [random-string] | +| COMPOSE_PROJECT_NAME | the identifier of the service plus prefix | amp_[service-identifier] | +| PORT | the port on which to run the server | 3000 | +| DB_URL | the connection url for the database | [db-provider]://[username]:[password]@localhost:[db-port]/[db-name] | +| DB_PORT | the port used by the database instance | [db-provider-port] | +| DB_USER | the username used to connect to the database | [username] | +| DB_PASSWORD | the password used to connect to the database | [password] | +| DB_NAME | the name of the database | [service-name] / [project-name] | +| JWT_SECRET_KEY | the secret used to sign the json-web token | [secret] | +| JWT_EXPIRATION | the expiration time for the json-web token | 2d | + +> **Note** +> Amplication generates default values and stores them under the .env file. It is advised to use some form of secrets manager/vault solution when using in production. + +## Step 2.1: Scripts - pre-requisites + +After configuration of the server the next step would be to run the application. Before running the server side of the component, make sure that the different pre-requisites are met - i.e., node.js [^16.x], npm, docker. After the setup of the pre-requisites the server component can be started. + +```sh +# installation of the dependencies +$ npm install + +# generate the prisma client +$ npm run prisma:generate +``` + +## Step 2.2: Scripts - local development + +```sh +# start the database where the server component will connect to +$ npm run docker:dev + +# initialize the database +$ npm run db:init + +# start the server component +$ npm run start +``` +By default, your app comes with one user with the username "admin" and password "admin". + +## Step 2.2: Scripts - container based development + +```shell +# start the server component as a docker container +$ npm run compose:up +``` diff --git a/server/docker-compose.dev.yml b/server/docker-compose.dev.yml new file mode 100644 index 0000000..8d7c358 --- /dev/null +++ b/server/docker-compose.dev.yml @@ -0,0 +1,13 @@ +version: "3" +services: + db: + image: postgres:12 + ports: + - ${DB_PORT}:5432 + environment: + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + volumes: + - postgres:/var/lib/postgresql/data +volumes: + postgres: ~ diff --git a/server/docker-compose.yml b/server/docker-compose.yml new file mode 100644 index 0000000..ef3a7a5 --- /dev/null +++ b/server/docker-compose.yml @@ -0,0 +1,47 @@ +version: "3" +services: + server: + build: + context: . + args: + NPM_LOG_LEVEL: notice + ports: + - ${PORT}:3000 + environment: + BCRYPT_SALT: ${BCRYPT_SALT} + DB_URL: postgres://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME} + depends_on: + - migrate + restart: on-failure + migrate: + build: + context: . + args: + NPM_LOG_LEVEL: notice + command: npm run db:init + working_dir: /app/server + environment: + BCRYPT_SALT: ${BCRYPT_SALT} + DB_URL: postgres://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME} + depends_on: + db: + condition: service_healthy + db: + image: postgres:12 + ports: + - ${DB_PORT}:5432 + environment: + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_DB: ${DB_NAME} + volumes: + - postgres:/var/lib/postgresql/data + healthcheck: + test: + - CMD-SHELL + - pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER} + timeout: 45s + interval: 10s + retries: 10 +volumes: + postgres: ~ diff --git a/server/nest-cli.json b/server/nest-cli.json new file mode 100644 index 0000000..fe51713 --- /dev/null +++ b/server/nest-cli.json @@ -0,0 +1,6 @@ +{ + "sourceRoot": "src", + "compilerOptions": { + "assets": ["swagger"] + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..a6558b0 --- /dev/null +++ b/server/package.json @@ -0,0 +1,73 @@ +{ + "name": "@bookstore/server", + "private": true, + "scripts": { + "start": "nest start", + "start:watch": "nest start --watch", + "start:debug": "nest start --debug --watch", + "build": "nest build", + "test": "jest", + "seed": "ts-node scripts/seed.ts", + "db:migrate-save": "prisma migrate dev", + "db:migrate-up": "prisma migrate deploy", + "db:clean": "prisma migrate reset", + "db:init": "run-s \"db:migrate-save -- --name 'initial version'\" db:migrate-up seed", + "prisma:generate": "prisma generate", + "docker:dev": "docker-compose -f docker-compose.dev.yml up -d", + "package:container": "docker build .", + "compose:up": "docker-compose up -d", + "compose:down": "docker-compose down --volumes" + }, + "dependencies": { + "@apollo/server": "^4.9.4", + "@nestjs/apollo": "12.0.9", + "@nestjs/common": "10.2.7", + "@nestjs/config": "3.1.1", + "@nestjs/core": "10.2.7", + "@nestjs/graphql": "12.0.9", + "@nestjs/jwt": "^10.1.1", + "@nestjs/passport": "^10.0.2", + "@nestjs/platform-express": "10.2.7", + "@nestjs/serve-static": "4.0.0", + "@nestjs/swagger": "7.1.13", + "@prisma/client": "^5.4.2", + "@types/bcrypt": "5.0.0", + "bcrypt": "5.1.1", + "class-transformer": "0.5.1", + "class-validator": "0.14.0", + "dotenv": "16.3.1", + "graphql": "^16.8.1", + "graphql-type-json": "0.3.2", + "npm-run-all": "4.1.5", + "passport": "0.6.0", + "passport-http": "0.3.0", + "passport-jwt": "4.0.1", + "reflect-metadata": "0.1.13", + "ts-node": "10.9.1", + "type-fest": "2.19.0", + "validator": "13.11.0" + }, + "devDependencies": { + "@nestjs/cli": "^10.1.18", + "@nestjs/testing": "^10.2.7", + "@types/express": "^4.17.19", + "@types/graphql-type-json": "0.3.3", + "@types/jest": "^29.5.5", + "@types/normalize-path": "3.0.0", + "@types/passport-http": "0.3.9", + "@types/passport-jwt": "3.0.10", + "@types/supertest": "^2.0.14", + "@types/validator": "^13.11.2", + "jest": "^29.7.0", + "jest-mock-extended": "^3.0.5", + "prisma": "^5.4.2", + "supertest": "^6.3.3", + "ts-jest": "^29.1.1", + "typescript": "^5.2.2" + }, + "jest": { + "preset": "ts-jest", + "testEnvironment": "node", + "modulePathIgnorePatterns": ["/dist/"] + } +} diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma new file mode 100644 index 0000000..655f64d --- /dev/null +++ b/server/prisma/schema.prisma @@ -0,0 +1,23 @@ +datasource db { + provider = "postgresql" + url = env("DB_URL") +} + +generator client { + provider = "prisma-client-js" +} + +model Author { + books Book[] + id Int @id @default(autoincrement()) + name String +} + +model Book { + author Author @relation(fields: [authorId], references: [id]) + authorId Int + genre String + id Int @id @default(autoincrement()) + published DateTime + title String +} diff --git a/server/scripts/customSeed.ts b/server/scripts/customSeed.ts new file mode 100644 index 0000000..26ccaf4 --- /dev/null +++ b/server/scripts/customSeed.ts @@ -0,0 +1,7 @@ +import { PrismaClient } from "@prisma/client"; + +export async function customSeed() { + const client = new PrismaClient(); + + client.$disconnect(); +} diff --git a/server/scripts/seed.ts b/server/scripts/seed.ts new file mode 100644 index 0000000..04cee65 --- /dev/null +++ b/server/scripts/seed.ts @@ -0,0 +1,25 @@ +import * as dotenv from "dotenv"; +import { PrismaClient } from "@prisma/client"; +import { customSeed } from "./customSeed"; + +if (require.main === module) { + dotenv.config(); + + const { BCRYPT_SALT } = process.env; + + if (!BCRYPT_SALT) { + throw new Error("BCRYPT_SALT environment variable must be defined"); + } +} + +async function seed() { + console.info("Seeding database..."); + + const client = new PrismaClient(); + void client.$disconnect(); + + console.info("Seeding database with custom seed..."); + customSeed(); + + console.info("Seeded database successfully"); +} diff --git a/server/src/app.module.ts b/server/src/app.module.ts new file mode 100644 index 0000000..28397a7 --- /dev/null +++ b/server/src/app.module.ts @@ -0,0 +1,43 @@ +import { Module } from "@nestjs/common"; +import { AuthorModule } from "./author/author.module"; +import { BookModule } from "./book/book.module"; +import { HealthModule } from "./health/health.module"; +import { PrismaModule } from "./prisma/prisma.module"; +import { SecretsManagerModule } from "./providers/secrets/secretsManager.module"; +import { ServeStaticModule } from "@nestjs/serve-static"; +import { ServeStaticOptionsService } from "./serveStaticOptions.service"; +import { ConfigModule, ConfigService } from "@nestjs/config"; +import { GraphQLModule } from "@nestjs/graphql"; +import { ApolloDriver, ApolloDriverConfig } from "@nestjs/apollo"; + +@Module({ + controllers: [], + imports: [ + AuthorModule, + BookModule, + HealthModule, + PrismaModule, + SecretsManagerModule, + ConfigModule.forRoot({ isGlobal: true }), + ServeStaticModule.forRootAsync({ + useClass: ServeStaticOptionsService, + }), + GraphQLModule.forRootAsync({ + driver: ApolloDriver, + useFactory: (configService: ConfigService) => { + const playground = configService.get("GRAPHQL_PLAYGROUND"); + const introspection = configService.get("GRAPHQL_INTROSPECTION"); + return { + autoSchemaFile: "schema.graphql", + sortSchema: true, + playground, + introspection: playground || introspection, + }; + }, + inject: [ConfigService], + imports: [ConfigModule], + }), + ], + providers: [], +}) +export class AppModule {} diff --git a/server/src/author/author.controller.ts b/server/src/author/author.controller.ts new file mode 100644 index 0000000..0665741 --- /dev/null +++ b/server/src/author/author.controller.ts @@ -0,0 +1,12 @@ +import * as common from "@nestjs/common"; +import * as swagger from "@nestjs/swagger"; +import { AuthorService } from "./author.service"; +import { AuthorControllerBase } from "./base/author.controller.base"; + +@swagger.ApiTags("authors") +@common.Controller("authors") +export class AuthorController extends AuthorControllerBase { + constructor(protected readonly service: AuthorService) { + super(service); + } +} diff --git a/server/src/author/author.module.ts b/server/src/author/author.module.ts new file mode 100644 index 0000000..8f8ee2c --- /dev/null +++ b/server/src/author/author.module.ts @@ -0,0 +1,13 @@ +import { Module } from "@nestjs/common"; +import { AuthorModuleBase } from "./base/author.module.base"; +import { AuthorService } from "./author.service"; +import { AuthorController } from "./author.controller"; +import { AuthorResolver } from "./author.resolver"; + +@Module({ + imports: [AuthorModuleBase], + controllers: [AuthorController], + providers: [AuthorService, AuthorResolver], + exports: [AuthorService], +}) +export class AuthorModule {} diff --git a/server/src/author/author.resolver.ts b/server/src/author/author.resolver.ts new file mode 100644 index 0000000..4bfda54 --- /dev/null +++ b/server/src/author/author.resolver.ts @@ -0,0 +1,11 @@ +import * as graphql from "@nestjs/graphql"; +import { AuthorResolverBase } from "./base/author.resolver.base"; +import { Author } from "./base/Author"; +import { AuthorService } from "./author.service"; + +@graphql.Resolver(() => Author) +export class AuthorResolver extends AuthorResolverBase { + constructor(protected readonly service: AuthorService) { + super(service); + } +} diff --git a/server/src/author/author.service.ts b/server/src/author/author.service.ts new file mode 100644 index 0000000..39d5ceb --- /dev/null +++ b/server/src/author/author.service.ts @@ -0,0 +1,10 @@ +import { Injectable } from "@nestjs/common"; +import { PrismaService } from "../prisma/prisma.service"; +import { AuthorServiceBase } from "./base/author.service.base"; + +@Injectable() +export class AuthorService extends AuthorServiceBase { + constructor(protected readonly prisma: PrismaService) { + super(prisma); + } +} diff --git a/server/src/author/base/Author.ts b/server/src/author/base/Author.ts new file mode 100644 index 0000000..e372c4f --- /dev/null +++ b/server/src/author/base/Author.ts @@ -0,0 +1,46 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ObjectType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { Book } from "../../book/base/Book"; +import { ValidateNested, IsOptional, IsInt, IsString } from "class-validator"; +import { Type } from "class-transformer"; + +@ObjectType() +class Author { + @ApiProperty({ + required: false, + type: () => [Book], + }) + @ValidateNested() + @Type(() => Book) + @IsOptional() + books?: Array; + + @ApiProperty({ + required: true, + type: Number, + }) + @IsInt() + @Field(() => Number) + id!: number; + + @ApiProperty({ + required: true, + type: String, + }) + @IsString() + @Field(() => String) + name!: string; +} + +export { Author as Author }; diff --git a/server/src/author/base/AuthorCountArgs.ts b/server/src/author/base/AuthorCountArgs.ts new file mode 100644 index 0000000..1547039 --- /dev/null +++ b/server/src/author/base/AuthorCountArgs.ts @@ -0,0 +1,28 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { AuthorWhereInput } from "./AuthorWhereInput"; +import { Type } from "class-transformer"; + +@ArgsType() +class AuthorCountArgs { + @ApiProperty({ + required: false, + type: () => AuthorWhereInput, + }) + @Field(() => AuthorWhereInput, { nullable: true }) + @Type(() => AuthorWhereInput) + where?: AuthorWhereInput; +} + +export { AuthorCountArgs as AuthorCountArgs }; diff --git a/server/src/author/base/AuthorCreateInput.ts b/server/src/author/base/AuthorCreateInput.ts new file mode 100644 index 0000000..9b295b0 --- /dev/null +++ b/server/src/author/base/AuthorCreateInput.ts @@ -0,0 +1,41 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { BookCreateNestedManyWithoutAuthorsInput } from "./BookCreateNestedManyWithoutAuthorsInput"; +import { ValidateNested, IsOptional, IsString } from "class-validator"; +import { Type } from "class-transformer"; + +@InputType() +class AuthorCreateInput { + @ApiProperty({ + required: false, + type: () => BookCreateNestedManyWithoutAuthorsInput, + }) + @ValidateNested() + @Type(() => BookCreateNestedManyWithoutAuthorsInput) + @IsOptional() + @Field(() => BookCreateNestedManyWithoutAuthorsInput, { + nullable: true, + }) + books?: BookCreateNestedManyWithoutAuthorsInput; + + @ApiProperty({ + required: true, + type: String, + }) + @IsString() + @Field(() => String) + name!: string; +} + +export { AuthorCreateInput as AuthorCreateInput }; diff --git a/server/src/author/base/AuthorFindManyArgs.ts b/server/src/author/base/AuthorFindManyArgs.ts new file mode 100644 index 0000000..90244fb --- /dev/null +++ b/server/src/author/base/AuthorFindManyArgs.ts @@ -0,0 +1,62 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { AuthorWhereInput } from "./AuthorWhereInput"; +import { IsOptional, ValidateNested, IsInt } from "class-validator"; +import { Type } from "class-transformer"; +import { AuthorOrderByInput } from "./AuthorOrderByInput"; + +@ArgsType() +class AuthorFindManyArgs { + @ApiProperty({ + required: false, + type: () => AuthorWhereInput, + }) + @IsOptional() + @ValidateNested() + @Field(() => AuthorWhereInput, { nullable: true }) + @Type(() => AuthorWhereInput) + where?: AuthorWhereInput; + + @ApiProperty({ + required: false, + type: [AuthorOrderByInput], + }) + @IsOptional() + @ValidateNested({ each: true }) + @Field(() => [AuthorOrderByInput], { nullable: true }) + @Type(() => AuthorOrderByInput) + orderBy?: Array; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @IsInt() + @Field(() => Number, { nullable: true }) + @Type(() => Number) + skip?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @IsInt() + @Field(() => Number, { nullable: true }) + @Type(() => Number) + take?: number; +} + +export { AuthorFindManyArgs as AuthorFindManyArgs }; diff --git a/server/src/author/base/AuthorFindUniqueArgs.ts b/server/src/author/base/AuthorFindUniqueArgs.ts new file mode 100644 index 0000000..ac653b2 --- /dev/null +++ b/server/src/author/base/AuthorFindUniqueArgs.ts @@ -0,0 +1,30 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { AuthorWhereUniqueInput } from "./AuthorWhereUniqueInput"; +import { ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; + +@ArgsType() +class AuthorFindUniqueArgs { + @ApiProperty({ + required: true, + type: () => AuthorWhereUniqueInput, + }) + @ValidateNested() + @Type(() => AuthorWhereUniqueInput) + @Field(() => AuthorWhereUniqueInput, { nullable: false }) + where!: AuthorWhereUniqueInput; +} + +export { AuthorFindUniqueArgs as AuthorFindUniqueArgs }; diff --git a/server/src/author/base/AuthorListRelationFilter.ts b/server/src/author/base/AuthorListRelationFilter.ts new file mode 100644 index 0000000..e4722ea --- /dev/null +++ b/server/src/author/base/AuthorListRelationFilter.ts @@ -0,0 +1,56 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { AuthorWhereInput } from "./AuthorWhereInput"; +import { ValidateNested, IsOptional } from "class-validator"; +import { Type } from "class-transformer"; + +@InputType() +class AuthorListRelationFilter { + @ApiProperty({ + required: false, + type: () => AuthorWhereInput, + }) + @ValidateNested() + @Type(() => AuthorWhereInput) + @IsOptional() + @Field(() => AuthorWhereInput, { + nullable: true, + }) + every?: AuthorWhereInput; + + @ApiProperty({ + required: false, + type: () => AuthorWhereInput, + }) + @ValidateNested() + @Type(() => AuthorWhereInput) + @IsOptional() + @Field(() => AuthorWhereInput, { + nullable: true, + }) + some?: AuthorWhereInput; + + @ApiProperty({ + required: false, + type: () => AuthorWhereInput, + }) + @ValidateNested() + @Type(() => AuthorWhereInput) + @IsOptional() + @Field(() => AuthorWhereInput, { + nullable: true, + }) + none?: AuthorWhereInput; +} +export { AuthorListRelationFilter as AuthorListRelationFilter }; diff --git a/server/src/author/base/AuthorOrderByInput.ts b/server/src/author/base/AuthorOrderByInput.ts new file mode 100644 index 0000000..519d1c3 --- /dev/null +++ b/server/src/author/base/AuthorOrderByInput.ts @@ -0,0 +1,45 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional, IsEnum } from "class-validator"; +import { SortOrder } from "../../util/SortOrder"; + +@InputType({ + isAbstract: true, + description: undefined, +}) +class AuthorOrderByInput { + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + id?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + name?: SortOrder; +} + +export { AuthorOrderByInput as AuthorOrderByInput }; diff --git a/server/src/author/base/AuthorUpdateInput.ts b/server/src/author/base/AuthorUpdateInput.ts new file mode 100644 index 0000000..d4be291 --- /dev/null +++ b/server/src/author/base/AuthorUpdateInput.ts @@ -0,0 +1,44 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { BookUpdateManyWithoutAuthorsInput } from "./BookUpdateManyWithoutAuthorsInput"; +import { ValidateNested, IsOptional, IsString } from "class-validator"; +import { Type } from "class-transformer"; + +@InputType() +class AuthorUpdateInput { + @ApiProperty({ + required: false, + type: () => BookUpdateManyWithoutAuthorsInput, + }) + @ValidateNested() + @Type(() => BookUpdateManyWithoutAuthorsInput) + @IsOptional() + @Field(() => BookUpdateManyWithoutAuthorsInput, { + nullable: true, + }) + books?: BookUpdateManyWithoutAuthorsInput; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @IsOptional() + @Field(() => String, { + nullable: true, + }) + name?: string; +} + +export { AuthorUpdateInput as AuthorUpdateInput }; diff --git a/server/src/author/base/AuthorWhereInput.ts b/server/src/author/base/AuthorWhereInput.ts new file mode 100644 index 0000000..fc9236d --- /dev/null +++ b/server/src/author/base/AuthorWhereInput.ts @@ -0,0 +1,57 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { BookListRelationFilter } from "../../book/base/BookListRelationFilter"; +import { ValidateNested, IsOptional } from "class-validator"; +import { Type } from "class-transformer"; +import { IntFilter } from "../../util/IntFilter"; +import { StringFilter } from "../../util/StringFilter"; + +@InputType() +class AuthorWhereInput { + @ApiProperty({ + required: false, + type: () => BookListRelationFilter, + }) + @ValidateNested() + @Type(() => BookListRelationFilter) + @IsOptional() + @Field(() => BookListRelationFilter, { + nullable: true, + }) + books?: BookListRelationFilter; + + @ApiProperty({ + required: false, + type: IntFilter, + }) + @Type(() => IntFilter) + @IsOptional() + @Field(() => IntFilter, { + nullable: true, + }) + id?: IntFilter; + + @ApiProperty({ + required: false, + type: StringFilter, + }) + @Type(() => StringFilter) + @IsOptional() + @Field(() => StringFilter, { + nullable: true, + }) + name?: StringFilter; +} + +export { AuthorWhereInput as AuthorWhereInput }; diff --git a/server/src/author/base/AuthorWhereUniqueInput.ts b/server/src/author/base/AuthorWhereUniqueInput.ts new file mode 100644 index 0000000..13ca44d --- /dev/null +++ b/server/src/author/base/AuthorWhereUniqueInput.ts @@ -0,0 +1,31 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsInt } from "class-validator"; +import { Transform } from "class-transformer"; + +@InputType() +class AuthorWhereUniqueInput { + @ApiProperty({ + required: true, + type: Number, + }) + @IsInt() + @Transform((prop) => parseInt(prop.value), { + toClassOnly: true, + }) + @Field(() => Number) + id!: number; +} + +export { AuthorWhereUniqueInput as AuthorWhereUniqueInput }; diff --git a/server/src/author/base/BookCreateNestedManyWithoutAuthorsInput.ts b/server/src/author/base/BookCreateNestedManyWithoutAuthorsInput.ts new file mode 100644 index 0000000..406028a --- /dev/null +++ b/server/src/author/base/BookCreateNestedManyWithoutAuthorsInput.ts @@ -0,0 +1,28 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { BookWhereUniqueInput } from "../../book/base/BookWhereUniqueInput"; +import { ApiProperty } from "@nestjs/swagger"; + +@InputType() +class BookCreateNestedManyWithoutAuthorsInput { + @Field(() => [BookWhereUniqueInput], { + nullable: true, + }) + @ApiProperty({ + required: false, + type: () => [BookWhereUniqueInput], + }) + connect?: Array; +} + +export { BookCreateNestedManyWithoutAuthorsInput as BookCreateNestedManyWithoutAuthorsInput }; diff --git a/server/src/author/base/BookUpdateManyWithoutAuthorsInput.ts b/server/src/author/base/BookUpdateManyWithoutAuthorsInput.ts new file mode 100644 index 0000000..a13db77 --- /dev/null +++ b/server/src/author/base/BookUpdateManyWithoutAuthorsInput.ts @@ -0,0 +1,46 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { BookWhereUniqueInput } from "../../book/base/BookWhereUniqueInput"; +import { ApiProperty } from "@nestjs/swagger"; + +@InputType() +class BookUpdateManyWithoutAuthorsInput { + @Field(() => [BookWhereUniqueInput], { + nullable: true, + }) + @ApiProperty({ + required: false, + type: () => [BookWhereUniqueInput], + }) + connect?: Array; + + @Field(() => [BookWhereUniqueInput], { + nullable: true, + }) + @ApiProperty({ + required: false, + type: () => [BookWhereUniqueInput], + }) + disconnect?: Array; + + @Field(() => [BookWhereUniqueInput], { + nullable: true, + }) + @ApiProperty({ + required: false, + type: () => [BookWhereUniqueInput], + }) + set?: Array; +} + +export { BookUpdateManyWithoutAuthorsInput as BookUpdateManyWithoutAuthorsInput }; diff --git a/server/src/author/base/CreateAuthorArgs.ts b/server/src/author/base/CreateAuthorArgs.ts new file mode 100644 index 0000000..d28949c --- /dev/null +++ b/server/src/author/base/CreateAuthorArgs.ts @@ -0,0 +1,30 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { AuthorCreateInput } from "./AuthorCreateInput"; +import { ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; + +@ArgsType() +class CreateAuthorArgs { + @ApiProperty({ + required: true, + type: () => AuthorCreateInput, + }) + @ValidateNested() + @Type(() => AuthorCreateInput) + @Field(() => AuthorCreateInput, { nullable: false }) + data!: AuthorCreateInput; +} + +export { CreateAuthorArgs as CreateAuthorArgs }; diff --git a/server/src/author/base/DeleteAuthorArgs.ts b/server/src/author/base/DeleteAuthorArgs.ts new file mode 100644 index 0000000..ebde458 --- /dev/null +++ b/server/src/author/base/DeleteAuthorArgs.ts @@ -0,0 +1,30 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { AuthorWhereUniqueInput } from "./AuthorWhereUniqueInput"; +import { ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; + +@ArgsType() +class DeleteAuthorArgs { + @ApiProperty({ + required: true, + type: () => AuthorWhereUniqueInput, + }) + @ValidateNested() + @Type(() => AuthorWhereUniqueInput) + @Field(() => AuthorWhereUniqueInput, { nullable: false }) + where!: AuthorWhereUniqueInput; +} + +export { DeleteAuthorArgs as DeleteAuthorArgs }; diff --git a/server/src/author/base/UpdateAuthorArgs.ts b/server/src/author/base/UpdateAuthorArgs.ts new file mode 100644 index 0000000..9441329 --- /dev/null +++ b/server/src/author/base/UpdateAuthorArgs.ts @@ -0,0 +1,40 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { AuthorWhereUniqueInput } from "./AuthorWhereUniqueInput"; +import { ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; +import { AuthorUpdateInput } from "./AuthorUpdateInput"; + +@ArgsType() +class UpdateAuthorArgs { + @ApiProperty({ + required: true, + type: () => AuthorWhereUniqueInput, + }) + @ValidateNested() + @Type(() => AuthorWhereUniqueInput) + @Field(() => AuthorWhereUniqueInput, { nullable: false }) + where!: AuthorWhereUniqueInput; + + @ApiProperty({ + required: true, + type: () => AuthorUpdateInput, + }) + @ValidateNested() + @Type(() => AuthorUpdateInput) + @Field(() => AuthorUpdateInput, { nullable: false }) + data!: AuthorUpdateInput; +} + +export { UpdateAuthorArgs as UpdateAuthorArgs }; diff --git a/server/src/author/base/author.controller.base.spec.ts b/server/src/author/base/author.controller.base.spec.ts new file mode 100644 index 0000000..98ae561 --- /dev/null +++ b/server/src/author/base/author.controller.base.spec.ts @@ -0,0 +1,168 @@ +import { Test } from "@nestjs/testing"; +import { + INestApplication, + HttpStatus, + ExecutionContext, + CallHandler, +} from "@nestjs/common"; +import request from "supertest"; +import { ACGuard } from "nest-access-control"; +import { DefaultAuthGuard } from "../../auth/defaultAuth.guard"; +import { ACLModule } from "../../auth/acl.module"; +import { AclFilterResponseInterceptor } from "../../interceptors/aclFilterResponse.interceptor"; +import { AclValidateRequestInterceptor } from "../../interceptors/aclValidateRequest.interceptor"; +import { map } from "rxjs"; +import { AuthorController } from "../author.controller"; +import { AuthorService } from "../author.service"; + +const nonExistingId = "nonExistingId"; +const existingId = "existingId"; +const CREATE_INPUT = { + id: 42, + name: "exampleName", +}; +const CREATE_RESULT = { + id: 42, + name: "exampleName", +}; +const FIND_MANY_RESULT = [ + { + id: 42, + name: "exampleName", + }, +]; +const FIND_ONE_RESULT = { + id: 42, + name: "exampleName", +}; + +const service = { + create() { + return CREATE_RESULT; + }, + findMany: () => FIND_MANY_RESULT, + findOne: ({ where }: { where: { id: string } }) => { + switch (where.id) { + case existingId: + return FIND_ONE_RESULT; + case nonExistingId: + return null; + } + }, +}; + +const basicAuthGuard = { + canActivate: (context: ExecutionContext) => { + const argumentHost = context.switchToHttp(); + const request = argumentHost.getRequest(); + request.user = { + roles: ["user"], + }; + return true; + }, +}; + +const acGuard = { + canActivate: () => { + return true; + }, +}; + +const aclFilterResponseInterceptor = { + intercept: (context: ExecutionContext, next: CallHandler) => { + return next.handle().pipe( + map((data) => { + return data; + }) + ); + }, +}; +const aclValidateRequestInterceptor = { + intercept: (context: ExecutionContext, next: CallHandler) => { + return next.handle(); + }, +}; + +describe("Author", () => { + let app: INestApplication; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [ + { + provide: AuthorService, + useValue: service, + }, + ], + controllers: [AuthorController], + imports: [ACLModule], + }) + .overrideGuard(DefaultAuthGuard) + .useValue(basicAuthGuard) + .overrideGuard(ACGuard) + .useValue(acGuard) + .overrideInterceptor(AclFilterResponseInterceptor) + .useValue(aclFilterResponseInterceptor) + .overrideInterceptor(AclValidateRequestInterceptor) + .useValue(aclValidateRequestInterceptor) + .compile(); + + app = moduleRef.createNestApplication(); + await app.init(); + }); + + test("POST /authors", async () => { + await request(app.getHttpServer()) + .post("/authors") + .send(CREATE_INPUT) + .expect(HttpStatus.CREATED) + .expect(CREATE_RESULT); + }); + + test("GET /authors", async () => { + await request(app.getHttpServer()) + .get("/authors") + .expect(HttpStatus.OK) + .expect([FIND_MANY_RESULT[0]]); + }); + + test("GET /authors/:id non existing", async () => { + await request(app.getHttpServer()) + .get(`${"/authors"}/${nonExistingId}`) + .expect(HttpStatus.NOT_FOUND) + .expect({ + statusCode: HttpStatus.NOT_FOUND, + message: `No resource was found for {"${"id"}":"${nonExistingId}"}`, + error: "Not Found", + }); + }); + + test("GET /authors/:id existing", async () => { + await request(app.getHttpServer()) + .get(`${"/authors"}/${existingId}`) + .expect(HttpStatus.OK) + .expect(FIND_ONE_RESULT); + }); + + test("POST /authors existing resource", async () => { + const agent = request(app.getHttpServer()); + await agent + .post("/authors") + .send(CREATE_INPUT) + .expect(HttpStatus.CREATED) + .expect(CREATE_RESULT) + .then(function () { + agent + .post("/authors") + .send(CREATE_INPUT) + .expect(HttpStatus.CONFLICT) + .expect({ + statusCode: HttpStatus.CONFLICT, + }); + }); + }); + + afterAll(async () => { + await app.close(); + }); +}); diff --git a/server/src/author/base/author.controller.base.ts b/server/src/author/base/author.controller.base.ts new file mode 100644 index 0000000..f6552e3 --- /dev/null +++ b/server/src/author/base/author.controller.base.ts @@ -0,0 +1,209 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import * as common from "@nestjs/common"; +import * as swagger from "@nestjs/swagger"; +import { isRecordNotFoundError } from "../../prisma.util"; +import * as errors from "../../errors"; +import { Request } from "express"; +import { plainToClass } from "class-transformer"; +import { ApiNestedQuery } from "../../decorators/api-nested-query.decorator"; +import { AuthorService } from "../author.service"; +import { AuthorCreateInput } from "./AuthorCreateInput"; +import { AuthorWhereInput } from "./AuthorWhereInput"; +import { AuthorWhereUniqueInput } from "./AuthorWhereUniqueInput"; +import { AuthorFindManyArgs } from "./AuthorFindManyArgs"; +import { AuthorUpdateInput } from "./AuthorUpdateInput"; +import { Author } from "./Author"; +import { BookFindManyArgs } from "../../book/base/BookFindManyArgs"; +import { Book } from "../../book/base/Book"; +import { BookWhereUniqueInput } from "../../book/base/BookWhereUniqueInput"; + +export class AuthorControllerBase { + constructor(protected readonly service: AuthorService) {} + @common.Post() + @swagger.ApiCreatedResponse({ type: Author }) + async create(@common.Body() data: AuthorCreateInput): Promise { + return await this.service.create({ + data: data, + select: { + id: true, + name: true, + }, + }); + } + + @common.Get() + @swagger.ApiOkResponse({ type: [Author] }) + @ApiNestedQuery(AuthorFindManyArgs) + async findMany(@common.Req() request: Request): Promise { + const args = plainToClass(AuthorFindManyArgs, request.query); + return this.service.findMany({ + ...args, + select: { + id: true, + name: true, + }, + }); + } + + @common.Get("/:id") + @swagger.ApiOkResponse({ type: Author }) + @swagger.ApiNotFoundResponse({ type: errors.NotFoundException }) + async findOne( + @common.Param() params: AuthorWhereUniqueInput + ): Promise { + const result = await this.service.findOne({ + where: params, + select: { + id: true, + name: true, + }, + }); + if (result === null) { + throw new errors.NotFoundException( + `No resource was found for ${JSON.stringify(params)}` + ); + } + return result; + } + + @common.Patch("/:id") + @swagger.ApiOkResponse({ type: Author }) + @swagger.ApiNotFoundResponse({ type: errors.NotFoundException }) + async update( + @common.Param() params: AuthorWhereUniqueInput, + @common.Body() data: AuthorUpdateInput + ): Promise { + try { + return await this.service.update({ + where: params, + data: data, + select: { + id: true, + name: true, + }, + }); + } catch (error) { + if (isRecordNotFoundError(error)) { + throw new errors.NotFoundException( + `No resource was found for ${JSON.stringify(params)}` + ); + } + throw error; + } + } + + @common.Delete("/:id") + @swagger.ApiOkResponse({ type: Author }) + @swagger.ApiNotFoundResponse({ type: errors.NotFoundException }) + async delete( + @common.Param() params: AuthorWhereUniqueInput + ): Promise { + try { + return await this.service.delete({ + where: params, + select: { + id: true, + name: true, + }, + }); + } catch (error) { + if (isRecordNotFoundError(error)) { + throw new errors.NotFoundException( + `No resource was found for ${JSON.stringify(params)}` + ); + } + throw error; + } + } + + @common.Get("/:id/books") + @ApiNestedQuery(BookFindManyArgs) + async findManyBooks( + @common.Req() request: Request, + @common.Param() params: AuthorWhereUniqueInput + ): Promise { + const query = plainToClass(BookFindManyArgs, request.query); + const results = await this.service.findBooks(params.id, { + ...query, + select: { + author: { + select: { + id: true, + }, + }, + + genre: true, + id: true, + published: true, + title: true, + }, + }); + if (results === null) { + throw new errors.NotFoundException( + `No resource was found for ${JSON.stringify(params)}` + ); + } + return results; + } + + @common.Post("/:id/books") + async connectBooks( + @common.Param() params: AuthorWhereUniqueInput, + @common.Body() body: BookWhereUniqueInput[] + ): Promise { + const data = { + books: { + connect: body, + }, + }; + await this.service.update({ + where: params, + data, + select: { id: true }, + }); + } + + @common.Patch("/:id/books") + async updateBooks( + @common.Param() params: AuthorWhereUniqueInput, + @common.Body() body: BookWhereUniqueInput[] + ): Promise { + const data = { + books: { + set: body, + }, + }; + await this.service.update({ + where: params, + data, + select: { id: true }, + }); + } + + @common.Delete("/:id/books") + async disconnectBooks( + @common.Param() params: AuthorWhereUniqueInput, + @common.Body() body: BookWhereUniqueInput[] + ): Promise { + const data = { + books: { + disconnect: body, + }, + }; + await this.service.update({ + where: params, + data, + select: { id: true }, + }); + } +} diff --git a/server/src/author/base/author.module.base.ts b/server/src/author/base/author.module.base.ts new file mode 100644 index 0000000..0055f14 --- /dev/null +++ b/server/src/author/base/author.module.base.ts @@ -0,0 +1,18 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { Module } from "@nestjs/common"; + +@Module({ + imports: [], + exports: [], +}) +export class AuthorModuleBase {} diff --git a/server/src/author/base/author.resolver.base.ts b/server/src/author/base/author.resolver.base.ts new file mode 100644 index 0000000..ce5e291 --- /dev/null +++ b/server/src/author/base/author.resolver.base.ts @@ -0,0 +1,111 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import * as graphql from "@nestjs/graphql"; +import { GraphQLError } from "graphql"; +import { isRecordNotFoundError } from "../../prisma.util"; +import { MetaQueryPayload } from "../../util/MetaQueryPayload"; +import { CreateAuthorArgs } from "./CreateAuthorArgs"; +import { UpdateAuthorArgs } from "./UpdateAuthorArgs"; +import { DeleteAuthorArgs } from "./DeleteAuthorArgs"; +import { AuthorCountArgs } from "./AuthorCountArgs"; +import { AuthorFindManyArgs } from "./AuthorFindManyArgs"; +import { AuthorFindUniqueArgs } from "./AuthorFindUniqueArgs"; +import { Author } from "./Author"; +import { BookFindManyArgs } from "../../book/base/BookFindManyArgs"; +import { Book } from "../../book/base/Book"; +import { AuthorService } from "../author.service"; +@graphql.Resolver(() => Author) +export class AuthorResolverBase { + constructor(protected readonly service: AuthorService) {} + + async _authorsMeta( + @graphql.Args() args: AuthorCountArgs + ): Promise { + const result = await this.service.count(args); + return { + count: result, + }; + } + + @graphql.Query(() => [Author]) + async authors(@graphql.Args() args: AuthorFindManyArgs): Promise { + return this.service.findMany(args); + } + + @graphql.Query(() => Author, { nullable: true }) + async author( + @graphql.Args() args: AuthorFindUniqueArgs + ): Promise { + const result = await this.service.findOne(args); + if (result === null) { + return null; + } + return result; + } + + @graphql.Mutation(() => Author) + async createAuthor(@graphql.Args() args: CreateAuthorArgs): Promise { + return await this.service.create({ + ...args, + data: args.data, + }); + } + + @graphql.Mutation(() => Author) + async updateAuthor( + @graphql.Args() args: UpdateAuthorArgs + ): Promise { + try { + return await this.service.update({ + ...args, + data: args.data, + }); + } catch (error) { + if (isRecordNotFoundError(error)) { + throw new GraphQLError( + `No resource was found for ${JSON.stringify(args.where)}` + ); + } + throw error; + } + } + + @graphql.Mutation(() => Author) + async deleteAuthor( + @graphql.Args() args: DeleteAuthorArgs + ): Promise { + try { + return await this.service.delete(args); + } catch (error) { + if (isRecordNotFoundError(error)) { + throw new GraphQLError( + `No resource was found for ${JSON.stringify(args.where)}` + ); + } + throw error; + } + } + + @graphql.ResolveField(() => [Book], { name: "books" }) + async resolveFieldBooks( + @graphql.Parent() parent: Author, + @graphql.Args() args: BookFindManyArgs + ): Promise { + const results = await this.service.findBooks(parent.id, args); + + if (!results) { + return []; + } + + return results; + } +} diff --git a/server/src/author/base/author.service.base.ts b/server/src/author/base/author.service.base.ts new file mode 100644 index 0000000..f69307c --- /dev/null +++ b/server/src/author/base/author.service.base.ts @@ -0,0 +1,60 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { PrismaService } from "../../prisma/prisma.service"; +import { Prisma, Author, Book } from "@prisma/client"; + +export class AuthorServiceBase { + constructor(protected readonly prisma: PrismaService) {} + + async count( + args: Prisma.SelectSubset + ): Promise { + return this.prisma.author.count(args); + } + + async findMany( + args: Prisma.SelectSubset + ): Promise { + return this.prisma.author.findMany(args); + } + async findOne( + args: Prisma.SelectSubset + ): Promise { + return this.prisma.author.findUnique(args); + } + async create( + args: Prisma.SelectSubset + ): Promise { + return this.prisma.author.create(args); + } + async update( + args: Prisma.SelectSubset + ): Promise { + return this.prisma.author.update(args); + } + async delete( + args: Prisma.SelectSubset + ): Promise { + return this.prisma.author.delete(args); + } + + async findBooks( + parentId: number, + args: Prisma.BookFindManyArgs + ): Promise { + return this.prisma.author + .findUniqueOrThrow({ + where: { id: parentId }, + }) + .books(args); + } +} diff --git a/server/src/book/base/Book.ts b/server/src/book/base/Book.ts new file mode 100644 index 0000000..e9322cc --- /dev/null +++ b/server/src/book/base/Book.ts @@ -0,0 +1,61 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ObjectType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { Author } from "../../author/base/Author"; +import { ValidateNested, IsString, IsInt, IsDate } from "class-validator"; +import { Type } from "class-transformer"; + +@ObjectType() +class Book { + @ApiProperty({ + required: true, + type: () => Author, + }) + @ValidateNested() + @Type(() => Author) + author?: Author; + + @ApiProperty({ + required: true, + type: String, + }) + @IsString() + @Field(() => String) + genre!: string; + + @ApiProperty({ + required: true, + type: Number, + }) + @IsInt() + @Field(() => Number) + id!: number; + + @ApiProperty({ + required: true, + }) + @IsDate() + @Type(() => Date) + @Field(() => Date) + published!: Date; + + @ApiProperty({ + required: true, + type: String, + }) + @IsString() + @Field(() => String) + title!: string; +} + +export { Book as Book }; diff --git a/server/src/book/base/BookCountArgs.ts b/server/src/book/base/BookCountArgs.ts new file mode 100644 index 0000000..672b0d0 --- /dev/null +++ b/server/src/book/base/BookCountArgs.ts @@ -0,0 +1,28 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { BookWhereInput } from "./BookWhereInput"; +import { Type } from "class-transformer"; + +@ArgsType() +class BookCountArgs { + @ApiProperty({ + required: false, + type: () => BookWhereInput, + }) + @Field(() => BookWhereInput, { nullable: true }) + @Type(() => BookWhereInput) + where?: BookWhereInput; +} + +export { BookCountArgs as BookCountArgs }; diff --git a/server/src/book/base/BookCreateInput.ts b/server/src/book/base/BookCreateInput.ts new file mode 100644 index 0000000..5598a0f --- /dev/null +++ b/server/src/book/base/BookCreateInput.ts @@ -0,0 +1,54 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { AuthorWhereUniqueInput } from "../../author/base/AuthorWhereUniqueInput"; +import { ValidateNested, IsString, IsDate } from "class-validator"; +import { Type } from "class-transformer"; + +@InputType() +class BookCreateInput { + @ApiProperty({ + required: true, + type: () => AuthorWhereUniqueInput, + }) + @ValidateNested() + @Type(() => AuthorWhereUniqueInput) + @Field(() => AuthorWhereUniqueInput) + author!: AuthorWhereUniqueInput; + + @ApiProperty({ + required: true, + type: String, + }) + @IsString() + @Field(() => String) + genre!: string; + + @ApiProperty({ + required: true, + }) + @IsDate() + @Type(() => Date) + @Field(() => Date) + published!: Date; + + @ApiProperty({ + required: true, + type: String, + }) + @IsString() + @Field(() => String) + title!: string; +} + +export { BookCreateInput as BookCreateInput }; diff --git a/server/src/book/base/BookFindManyArgs.ts b/server/src/book/base/BookFindManyArgs.ts new file mode 100644 index 0000000..b8b17c2 --- /dev/null +++ b/server/src/book/base/BookFindManyArgs.ts @@ -0,0 +1,62 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { BookWhereInput } from "./BookWhereInput"; +import { IsOptional, ValidateNested, IsInt } from "class-validator"; +import { Type } from "class-transformer"; +import { BookOrderByInput } from "./BookOrderByInput"; + +@ArgsType() +class BookFindManyArgs { + @ApiProperty({ + required: false, + type: () => BookWhereInput, + }) + @IsOptional() + @ValidateNested() + @Field(() => BookWhereInput, { nullable: true }) + @Type(() => BookWhereInput) + where?: BookWhereInput; + + @ApiProperty({ + required: false, + type: [BookOrderByInput], + }) + @IsOptional() + @ValidateNested({ each: true }) + @Field(() => [BookOrderByInput], { nullable: true }) + @Type(() => BookOrderByInput) + orderBy?: Array; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @IsInt() + @Field(() => Number, { nullable: true }) + @Type(() => Number) + skip?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @IsInt() + @Field(() => Number, { nullable: true }) + @Type(() => Number) + take?: number; +} + +export { BookFindManyArgs as BookFindManyArgs }; diff --git a/server/src/book/base/BookFindUniqueArgs.ts b/server/src/book/base/BookFindUniqueArgs.ts new file mode 100644 index 0000000..1f686c8 --- /dev/null +++ b/server/src/book/base/BookFindUniqueArgs.ts @@ -0,0 +1,30 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { BookWhereUniqueInput } from "./BookWhereUniqueInput"; +import { ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; + +@ArgsType() +class BookFindUniqueArgs { + @ApiProperty({ + required: true, + type: () => BookWhereUniqueInput, + }) + @ValidateNested() + @Type(() => BookWhereUniqueInput) + @Field(() => BookWhereUniqueInput, { nullable: false }) + where!: BookWhereUniqueInput; +} + +export { BookFindUniqueArgs as BookFindUniqueArgs }; diff --git a/server/src/book/base/BookListRelationFilter.ts b/server/src/book/base/BookListRelationFilter.ts new file mode 100644 index 0000000..66df153 --- /dev/null +++ b/server/src/book/base/BookListRelationFilter.ts @@ -0,0 +1,56 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { BookWhereInput } from "./BookWhereInput"; +import { ValidateNested, IsOptional } from "class-validator"; +import { Type } from "class-transformer"; + +@InputType() +class BookListRelationFilter { + @ApiProperty({ + required: false, + type: () => BookWhereInput, + }) + @ValidateNested() + @Type(() => BookWhereInput) + @IsOptional() + @Field(() => BookWhereInput, { + nullable: true, + }) + every?: BookWhereInput; + + @ApiProperty({ + required: false, + type: () => BookWhereInput, + }) + @ValidateNested() + @Type(() => BookWhereInput) + @IsOptional() + @Field(() => BookWhereInput, { + nullable: true, + }) + some?: BookWhereInput; + + @ApiProperty({ + required: false, + type: () => BookWhereInput, + }) + @ValidateNested() + @Type(() => BookWhereInput) + @IsOptional() + @Field(() => BookWhereInput, { + nullable: true, + }) + none?: BookWhereInput; +} +export { BookListRelationFilter as BookListRelationFilter }; diff --git a/server/src/book/base/BookOrderByInput.ts b/server/src/book/base/BookOrderByInput.ts new file mode 100644 index 0000000..24ef873 --- /dev/null +++ b/server/src/book/base/BookOrderByInput.ts @@ -0,0 +1,78 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional, IsEnum } from "class-validator"; +import { SortOrder } from "../../util/SortOrder"; + +@InputType({ + isAbstract: true, + description: undefined, +}) +class BookOrderByInput { + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + authorId?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + genre?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + id?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + published?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + title?: SortOrder; +} + +export { BookOrderByInput as BookOrderByInput }; diff --git a/server/src/book/base/BookUpdateInput.ts b/server/src/book/base/BookUpdateInput.ts new file mode 100644 index 0000000..328ed29 --- /dev/null +++ b/server/src/book/base/BookUpdateInput.ts @@ -0,0 +1,66 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { AuthorWhereUniqueInput } from "../../author/base/AuthorWhereUniqueInput"; +import { ValidateNested, IsOptional, IsString, IsDate } from "class-validator"; +import { Type } from "class-transformer"; + +@InputType() +class BookUpdateInput { + @ApiProperty({ + required: false, + type: () => AuthorWhereUniqueInput, + }) + @ValidateNested() + @Type(() => AuthorWhereUniqueInput) + @IsOptional() + @Field(() => AuthorWhereUniqueInput, { + nullable: true, + }) + author?: AuthorWhereUniqueInput; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @IsOptional() + @Field(() => String, { + nullable: true, + }) + genre?: string; + + @ApiProperty({ + required: false, + }) + @IsDate() + @Type(() => Date) + @IsOptional() + @Field(() => Date, { + nullable: true, + }) + published?: Date; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @IsOptional() + @Field(() => String, { + nullable: true, + }) + title?: string; +} + +export { BookUpdateInput as BookUpdateInput }; diff --git a/server/src/book/base/BookWhereInput.ts b/server/src/book/base/BookWhereInput.ts new file mode 100644 index 0000000..f9d309b --- /dev/null +++ b/server/src/book/base/BookWhereInput.ts @@ -0,0 +1,80 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { AuthorWhereUniqueInput } from "../../author/base/AuthorWhereUniqueInput"; +import { ValidateNested, IsOptional } from "class-validator"; +import { Type } from "class-transformer"; +import { StringFilter } from "../../util/StringFilter"; +import { IntFilter } from "../../util/IntFilter"; +import { DateTimeFilter } from "../../util/DateTimeFilter"; + +@InputType() +class BookWhereInput { + @ApiProperty({ + required: false, + type: () => AuthorWhereUniqueInput, + }) + @ValidateNested() + @Type(() => AuthorWhereUniqueInput) + @IsOptional() + @Field(() => AuthorWhereUniqueInput, { + nullable: true, + }) + author?: AuthorWhereUniqueInput; + + @ApiProperty({ + required: false, + type: StringFilter, + }) + @Type(() => StringFilter) + @IsOptional() + @Field(() => StringFilter, { + nullable: true, + }) + genre?: StringFilter; + + @ApiProperty({ + required: false, + type: IntFilter, + }) + @Type(() => IntFilter) + @IsOptional() + @Field(() => IntFilter, { + nullable: true, + }) + id?: IntFilter; + + @ApiProperty({ + required: false, + type: DateTimeFilter, + }) + @Type(() => DateTimeFilter) + @IsOptional() + @Field(() => DateTimeFilter, { + nullable: true, + }) + published?: DateTimeFilter; + + @ApiProperty({ + required: false, + type: StringFilter, + }) + @Type(() => StringFilter) + @IsOptional() + @Field(() => StringFilter, { + nullable: true, + }) + title?: StringFilter; +} + +export { BookWhereInput as BookWhereInput }; diff --git a/server/src/book/base/BookWhereUniqueInput.ts b/server/src/book/base/BookWhereUniqueInput.ts new file mode 100644 index 0000000..0795261 --- /dev/null +++ b/server/src/book/base/BookWhereUniqueInput.ts @@ -0,0 +1,31 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsInt } from "class-validator"; +import { Transform } from "class-transformer"; + +@InputType() +class BookWhereUniqueInput { + @ApiProperty({ + required: true, + type: Number, + }) + @IsInt() + @Transform((prop) => parseInt(prop.value), { + toClassOnly: true, + }) + @Field(() => Number) + id!: number; +} + +export { BookWhereUniqueInput as BookWhereUniqueInput }; diff --git a/server/src/book/base/CreateBookArgs.ts b/server/src/book/base/CreateBookArgs.ts new file mode 100644 index 0000000..63612ef --- /dev/null +++ b/server/src/book/base/CreateBookArgs.ts @@ -0,0 +1,30 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { BookCreateInput } from "./BookCreateInput"; +import { ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; + +@ArgsType() +class CreateBookArgs { + @ApiProperty({ + required: true, + type: () => BookCreateInput, + }) + @ValidateNested() + @Type(() => BookCreateInput) + @Field(() => BookCreateInput, { nullable: false }) + data!: BookCreateInput; +} + +export { CreateBookArgs as CreateBookArgs }; diff --git a/server/src/book/base/DeleteBookArgs.ts b/server/src/book/base/DeleteBookArgs.ts new file mode 100644 index 0000000..bb5ce98 --- /dev/null +++ b/server/src/book/base/DeleteBookArgs.ts @@ -0,0 +1,30 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { BookWhereUniqueInput } from "./BookWhereUniqueInput"; +import { ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; + +@ArgsType() +class DeleteBookArgs { + @ApiProperty({ + required: true, + type: () => BookWhereUniqueInput, + }) + @ValidateNested() + @Type(() => BookWhereUniqueInput) + @Field(() => BookWhereUniqueInput, { nullable: false }) + where!: BookWhereUniqueInput; +} + +export { DeleteBookArgs as DeleteBookArgs }; diff --git a/server/src/book/base/UpdateBookArgs.ts b/server/src/book/base/UpdateBookArgs.ts new file mode 100644 index 0000000..68be586 --- /dev/null +++ b/server/src/book/base/UpdateBookArgs.ts @@ -0,0 +1,40 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { BookWhereUniqueInput } from "./BookWhereUniqueInput"; +import { ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; +import { BookUpdateInput } from "./BookUpdateInput"; + +@ArgsType() +class UpdateBookArgs { + @ApiProperty({ + required: true, + type: () => BookWhereUniqueInput, + }) + @ValidateNested() + @Type(() => BookWhereUniqueInput) + @Field(() => BookWhereUniqueInput, { nullable: false }) + where!: BookWhereUniqueInput; + + @ApiProperty({ + required: true, + type: () => BookUpdateInput, + }) + @ValidateNested() + @Type(() => BookUpdateInput) + @Field(() => BookUpdateInput, { nullable: false }) + data!: BookUpdateInput; +} + +export { UpdateBookArgs as UpdateBookArgs }; diff --git a/server/src/book/base/book.controller.base.spec.ts b/server/src/book/base/book.controller.base.spec.ts new file mode 100644 index 0000000..0dba5c2 --- /dev/null +++ b/server/src/book/base/book.controller.base.spec.ts @@ -0,0 +1,190 @@ +import { Test } from "@nestjs/testing"; +import { + INestApplication, + HttpStatus, + ExecutionContext, + CallHandler, +} from "@nestjs/common"; +import request from "supertest"; +import { ACGuard } from "nest-access-control"; +import { DefaultAuthGuard } from "../../auth/defaultAuth.guard"; +import { ACLModule } from "../../auth/acl.module"; +import { AclFilterResponseInterceptor } from "../../interceptors/aclFilterResponse.interceptor"; +import { AclValidateRequestInterceptor } from "../../interceptors/aclValidateRequest.interceptor"; +import { map } from "rxjs"; +import { BookController } from "../book.controller"; +import { BookService } from "../book.service"; + +const nonExistingId = "nonExistingId"; +const existingId = "existingId"; +const CREATE_INPUT = { + genre: "exampleGenre", + id: 42, + published: new Date(), + title: "exampleTitle", +}; +const CREATE_RESULT = { + genre: "exampleGenre", + id: 42, + published: new Date(), + title: "exampleTitle", +}; +const FIND_MANY_RESULT = [ + { + genre: "exampleGenre", + id: 42, + published: new Date(), + title: "exampleTitle", + }, +]; +const FIND_ONE_RESULT = { + genre: "exampleGenre", + id: 42, + published: new Date(), + title: "exampleTitle", +}; + +const service = { + create() { + return CREATE_RESULT; + }, + findMany: () => FIND_MANY_RESULT, + findOne: ({ where }: { where: { id: string } }) => { + switch (where.id) { + case existingId: + return FIND_ONE_RESULT; + case nonExistingId: + return null; + } + }, +}; + +const basicAuthGuard = { + canActivate: (context: ExecutionContext) => { + const argumentHost = context.switchToHttp(); + const request = argumentHost.getRequest(); + request.user = { + roles: ["user"], + }; + return true; + }, +}; + +const acGuard = { + canActivate: () => { + return true; + }, +}; + +const aclFilterResponseInterceptor = { + intercept: (context: ExecutionContext, next: CallHandler) => { + return next.handle().pipe( + map((data) => { + return data; + }) + ); + }, +}; +const aclValidateRequestInterceptor = { + intercept: (context: ExecutionContext, next: CallHandler) => { + return next.handle(); + }, +}; + +describe("Book", () => { + let app: INestApplication; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [ + { + provide: BookService, + useValue: service, + }, + ], + controllers: [BookController], + imports: [ACLModule], + }) + .overrideGuard(DefaultAuthGuard) + .useValue(basicAuthGuard) + .overrideGuard(ACGuard) + .useValue(acGuard) + .overrideInterceptor(AclFilterResponseInterceptor) + .useValue(aclFilterResponseInterceptor) + .overrideInterceptor(AclValidateRequestInterceptor) + .useValue(aclValidateRequestInterceptor) + .compile(); + + app = moduleRef.createNestApplication(); + await app.init(); + }); + + test("POST /books", async () => { + await request(app.getHttpServer()) + .post("/books") + .send(CREATE_INPUT) + .expect(HttpStatus.CREATED) + .expect({ + ...CREATE_RESULT, + published: CREATE_RESULT.published.toISOString(), + }); + }); + + test("GET /books", async () => { + await request(app.getHttpServer()) + .get("/books") + .expect(HttpStatus.OK) + .expect([ + { + ...FIND_MANY_RESULT[0], + published: FIND_MANY_RESULT[0].published.toISOString(), + }, + ]); + }); + + test("GET /books/:id non existing", async () => { + await request(app.getHttpServer()) + .get(`${"/books"}/${nonExistingId}`) + .expect(HttpStatus.NOT_FOUND) + .expect({ + statusCode: HttpStatus.NOT_FOUND, + message: `No resource was found for {"${"id"}":"${nonExistingId}"}`, + error: "Not Found", + }); + }); + + test("GET /books/:id existing", async () => { + await request(app.getHttpServer()) + .get(`${"/books"}/${existingId}`) + .expect(HttpStatus.OK) + .expect({ + ...FIND_ONE_RESULT, + published: FIND_ONE_RESULT.published.toISOString(), + }); + }); + + test("POST /books existing resource", async () => { + const agent = request(app.getHttpServer()); + await agent + .post("/books") + .send(CREATE_INPUT) + .expect(HttpStatus.CREATED) + .expect({ + ...CREATE_RESULT, + published: CREATE_RESULT.published.toISOString(), + }) + .then(function () { + agent + .post("/books") + .send(CREATE_INPUT) + .expect(HttpStatus.CONFLICT) + .expect({ + statusCode: HttpStatus.CONFLICT, + }); + }); + }); + + afterAll(async () => { + await app.close(); + }); +}); diff --git a/server/src/book/base/book.controller.base.ts b/server/src/book/base/book.controller.base.ts new file mode 100644 index 0000000..321a70d --- /dev/null +++ b/server/src/book/base/book.controller.base.ts @@ -0,0 +1,177 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import * as common from "@nestjs/common"; +import * as swagger from "@nestjs/swagger"; +import { isRecordNotFoundError } from "../../prisma.util"; +import * as errors from "../../errors"; +import { Request } from "express"; +import { plainToClass } from "class-transformer"; +import { ApiNestedQuery } from "../../decorators/api-nested-query.decorator"; +import { BookService } from "../book.service"; +import { BookCreateInput } from "./BookCreateInput"; +import { BookWhereInput } from "./BookWhereInput"; +import { BookWhereUniqueInput } from "./BookWhereUniqueInput"; +import { BookFindManyArgs } from "./BookFindManyArgs"; +import { BookUpdateInput } from "./BookUpdateInput"; +import { Book } from "./Book"; + +export class BookControllerBase { + constructor(protected readonly service: BookService) {} + @common.Post() + @swagger.ApiCreatedResponse({ type: Book }) + async create(@common.Body() data: BookCreateInput): Promise { + return await this.service.create({ + data: { + ...data, + + author: { + connect: data.author, + }, + }, + select: { + author: { + select: { + id: true, + }, + }, + + genre: true, + id: true, + published: true, + title: true, + }, + }); + } + + @common.Get() + @swagger.ApiOkResponse({ type: [Book] }) + @ApiNestedQuery(BookFindManyArgs) + async findMany(@common.Req() request: Request): Promise { + const args = plainToClass(BookFindManyArgs, request.query); + return this.service.findMany({ + ...args, + select: { + author: { + select: { + id: true, + }, + }, + + genre: true, + id: true, + published: true, + title: true, + }, + }); + } + + @common.Get("/:id") + @swagger.ApiOkResponse({ type: Book }) + @swagger.ApiNotFoundResponse({ type: errors.NotFoundException }) + async findOne( + @common.Param() params: BookWhereUniqueInput + ): Promise { + const result = await this.service.findOne({ + where: params, + select: { + author: { + select: { + id: true, + }, + }, + + genre: true, + id: true, + published: true, + title: true, + }, + }); + if (result === null) { + throw new errors.NotFoundException( + `No resource was found for ${JSON.stringify(params)}` + ); + } + return result; + } + + @common.Patch("/:id") + @swagger.ApiOkResponse({ type: Book }) + @swagger.ApiNotFoundResponse({ type: errors.NotFoundException }) + async update( + @common.Param() params: BookWhereUniqueInput, + @common.Body() data: BookUpdateInput + ): Promise { + try { + return await this.service.update({ + where: params, + data: { + ...data, + + author: { + connect: data.author, + }, + }, + select: { + author: { + select: { + id: true, + }, + }, + + genre: true, + id: true, + published: true, + title: true, + }, + }); + } catch (error) { + if (isRecordNotFoundError(error)) { + throw new errors.NotFoundException( + `No resource was found for ${JSON.stringify(params)}` + ); + } + throw error; + } + } + + @common.Delete("/:id") + @swagger.ApiOkResponse({ type: Book }) + @swagger.ApiNotFoundResponse({ type: errors.NotFoundException }) + async delete( + @common.Param() params: BookWhereUniqueInput + ): Promise { + try { + return await this.service.delete({ + where: params, + select: { + author: { + select: { + id: true, + }, + }, + + genre: true, + id: true, + published: true, + title: true, + }, + }); + } catch (error) { + if (isRecordNotFoundError(error)) { + throw new errors.NotFoundException( + `No resource was found for ${JSON.stringify(params)}` + ); + } + throw error; + } + } +} diff --git a/server/src/book/base/book.module.base.ts b/server/src/book/base/book.module.base.ts new file mode 100644 index 0000000..aaacffb --- /dev/null +++ b/server/src/book/base/book.module.base.ts @@ -0,0 +1,18 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { Module } from "@nestjs/common"; + +@Module({ + imports: [], + exports: [], +}) +export class BookModuleBase {} diff --git a/server/src/book/base/book.resolver.base.ts b/server/src/book/base/book.resolver.base.ts new file mode 100644 index 0000000..b5d7f26 --- /dev/null +++ b/server/src/book/base/book.resolver.base.ts @@ -0,0 +1,117 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import * as graphql from "@nestjs/graphql"; +import { GraphQLError } from "graphql"; +import { isRecordNotFoundError } from "../../prisma.util"; +import { MetaQueryPayload } from "../../util/MetaQueryPayload"; +import { CreateBookArgs } from "./CreateBookArgs"; +import { UpdateBookArgs } from "./UpdateBookArgs"; +import { DeleteBookArgs } from "./DeleteBookArgs"; +import { BookCountArgs } from "./BookCountArgs"; +import { BookFindManyArgs } from "./BookFindManyArgs"; +import { BookFindUniqueArgs } from "./BookFindUniqueArgs"; +import { Book } from "./Book"; +import { Author } from "../../author/base/Author"; +import { BookService } from "../book.service"; +@graphql.Resolver(() => Book) +export class BookResolverBase { + constructor(protected readonly service: BookService) {} + + async _booksMeta( + @graphql.Args() args: BookCountArgs + ): Promise { + const result = await this.service.count(args); + return { + count: result, + }; + } + + @graphql.Query(() => [Book]) + async books(@graphql.Args() args: BookFindManyArgs): Promise { + return this.service.findMany(args); + } + + @graphql.Query(() => Book, { nullable: true }) + async book(@graphql.Args() args: BookFindUniqueArgs): Promise { + const result = await this.service.findOne(args); + if (result === null) { + return null; + } + return result; + } + + @graphql.Mutation(() => Book) + async createBook(@graphql.Args() args: CreateBookArgs): Promise { + return await this.service.create({ + ...args, + data: { + ...args.data, + + author: { + connect: args.data.author, + }, + }, + }); + } + + @graphql.Mutation(() => Book) + async updateBook(@graphql.Args() args: UpdateBookArgs): Promise { + try { + return await this.service.update({ + ...args, + data: { + ...args.data, + + author: { + connect: args.data.author, + }, + }, + }); + } catch (error) { + if (isRecordNotFoundError(error)) { + throw new GraphQLError( + `No resource was found for ${JSON.stringify(args.where)}` + ); + } + throw error; + } + } + + @graphql.Mutation(() => Book) + async deleteBook(@graphql.Args() args: DeleteBookArgs): Promise { + try { + return await this.service.delete(args); + } catch (error) { + if (isRecordNotFoundError(error)) { + throw new GraphQLError( + `No resource was found for ${JSON.stringify(args.where)}` + ); + } + throw error; + } + } + + @graphql.ResolveField(() => Author, { + nullable: true, + name: "author", + }) + async resolveFieldAuthor( + @graphql.Parent() parent: Book + ): Promise { + const result = await this.service.getAuthor(parent.id); + + if (!result) { + return null; + } + return result; + } +} diff --git a/server/src/book/base/book.service.base.ts b/server/src/book/base/book.service.base.ts new file mode 100644 index 0000000..e98a39d --- /dev/null +++ b/server/src/book/base/book.service.base.ts @@ -0,0 +1,57 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { PrismaService } from "../../prisma/prisma.service"; +import { Prisma, Book, Author } from "@prisma/client"; + +export class BookServiceBase { + constructor(protected readonly prisma: PrismaService) {} + + async count( + args: Prisma.SelectSubset + ): Promise { + return this.prisma.book.count(args); + } + + async findMany( + args: Prisma.SelectSubset + ): Promise { + return this.prisma.book.findMany(args); + } + async findOne( + args: Prisma.SelectSubset + ): Promise { + return this.prisma.book.findUnique(args); + } + async create( + args: Prisma.SelectSubset + ): Promise { + return this.prisma.book.create(args); + } + async update( + args: Prisma.SelectSubset + ): Promise { + return this.prisma.book.update(args); + } + async delete( + args: Prisma.SelectSubset + ): Promise { + return this.prisma.book.delete(args); + } + + async getAuthor(parentId: number): Promise { + return this.prisma.book + .findUnique({ + where: { id: parentId }, + }) + .author(); + } +} diff --git a/server/src/book/book.controller.ts b/server/src/book/book.controller.ts new file mode 100644 index 0000000..8788748 --- /dev/null +++ b/server/src/book/book.controller.ts @@ -0,0 +1,12 @@ +import * as common from "@nestjs/common"; +import * as swagger from "@nestjs/swagger"; +import { BookService } from "./book.service"; +import { BookControllerBase } from "./base/book.controller.base"; + +@swagger.ApiTags("books") +@common.Controller("books") +export class BookController extends BookControllerBase { + constructor(protected readonly service: BookService) { + super(service); + } +} diff --git a/server/src/book/book.module.ts b/server/src/book/book.module.ts new file mode 100644 index 0000000..49a58e0 --- /dev/null +++ b/server/src/book/book.module.ts @@ -0,0 +1,13 @@ +import { Module } from "@nestjs/common"; +import { BookModuleBase } from "./base/book.module.base"; +import { BookService } from "./book.service"; +import { BookController } from "./book.controller"; +import { BookResolver } from "./book.resolver"; + +@Module({ + imports: [BookModuleBase], + controllers: [BookController], + providers: [BookService, BookResolver], + exports: [BookService], +}) +export class BookModule {} diff --git a/server/src/book/book.resolver.ts b/server/src/book/book.resolver.ts new file mode 100644 index 0000000..c25b722 --- /dev/null +++ b/server/src/book/book.resolver.ts @@ -0,0 +1,11 @@ +import * as graphql from "@nestjs/graphql"; +import { BookResolverBase } from "./base/book.resolver.base"; +import { Book } from "./base/Book"; +import { BookService } from "./book.service"; + +@graphql.Resolver(() => Book) +export class BookResolver extends BookResolverBase { + constructor(protected readonly service: BookService) { + super(service); + } +} diff --git a/server/src/book/book.service.ts b/server/src/book/book.service.ts new file mode 100644 index 0000000..0931c02 --- /dev/null +++ b/server/src/book/book.service.ts @@ -0,0 +1,10 @@ +import { Injectable } from "@nestjs/common"; +import { PrismaService } from "../prisma/prisma.service"; +import { BookServiceBase } from "./base/book.service.base"; + +@Injectable() +export class BookService extends BookServiceBase { + constructor(protected readonly prisma: PrismaService) { + super(prisma); + } +} diff --git a/server/src/connectMicroservices.ts b/server/src/connectMicroservices.ts new file mode 100644 index 0000000..068fa5f --- /dev/null +++ b/server/src/connectMicroservices.ts @@ -0,0 +1,6 @@ +import { INestApplication } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; + +export async function connectMicroservices(app: INestApplication) { + const configService = app.get(ConfigService); +} diff --git a/server/src/decorators/api-nested-query.decorator.ts b/server/src/decorators/api-nested-query.decorator.ts new file mode 100644 index 0000000..9fd5ba3 --- /dev/null +++ b/server/src/decorators/api-nested-query.decorator.ts @@ -0,0 +1,80 @@ +import { applyDecorators } from "@nestjs/common"; +import { + ApiExtraModels, + ApiQuery, + ApiQueryOptions, + getSchemaPath, +} from "@nestjs/swagger"; +import "reflect-metadata"; + +const generateApiQueryObject = ( + prop: any, + propType: any, + required: boolean, + isArray: boolean +): ApiQueryOptions => { + if (propType === Number) { + return { + required, + name: prop, + style: "deepObject", + explode: true, + type: "number", + isArray, + }; + } else if (propType === String) { + return { + required, + name: prop, + style: "deepObject", + explode: true, + type: "string", + isArray, + }; + } else { + return { + required, + name: prop, + style: "deepObject", + explode: true, + type: "object", + isArray, + schema: { + $ref: getSchemaPath(propType), + }, + }; + } +}; + +// eslint-disable-next-line @typescript-eslint/ban-types,@typescript-eslint/explicit-module-boundary-types,@typescript-eslint/naming-convention +export function ApiNestedQuery(query: Function) { + const constructor = query.prototype; + const properties = Reflect.getMetadata( + "swagger/apiModelPropertiesArray", + constructor + ).map((prop: any) => prop.slice(1)); + + const decorators = properties + .map((property: any) => { + const { required, isArray } = Reflect.getMetadata( + "swagger/apiModelProperties", + constructor, + property + ); + const propertyType = Reflect.getMetadata( + "design:type", + constructor, + property + ); + const typedQuery = generateApiQueryObject( + property, + propertyType, + required, + isArray + ); + return [ApiExtraModels(propertyType), ApiQuery(typedQuery)]; + }) + .flat(); + + return applyDecorators(...decorators); +} diff --git a/server/src/decorators/public.decorator.ts b/server/src/decorators/public.decorator.ts new file mode 100644 index 0000000..9eab4e0 --- /dev/null +++ b/server/src/decorators/public.decorator.ts @@ -0,0 +1,10 @@ +import { applyDecorators, SetMetadata } from "@nestjs/common"; + +export const IS_PUBLIC_KEY = "isPublic"; + +const PublicAuthMiddleware = SetMetadata(IS_PUBLIC_KEY, true); +const PublicAuthSwagger = SetMetadata("swagger/apiSecurity", ["isPublic"]); + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export const Public = () => + applyDecorators(PublicAuthMiddleware, PublicAuthSwagger); diff --git a/server/src/errors.ts b/server/src/errors.ts new file mode 100644 index 0000000..bd1aa6d --- /dev/null +++ b/server/src/errors.ts @@ -0,0 +1,16 @@ +import * as common from "@nestjs/common"; +import { ApiProperty } from "@nestjs/swagger"; + +export class ForbiddenException extends common.ForbiddenException { + @ApiProperty() + statusCode!: number; + @ApiProperty() + message!: string; +} + +export class NotFoundException extends common.NotFoundException { + @ApiProperty() + statusCode!: number; + @ApiProperty() + message!: string; +} diff --git a/server/src/filters/HttpExceptions.filter.ts b/server/src/filters/HttpExceptions.filter.ts new file mode 100644 index 0000000..f5eda8e --- /dev/null +++ b/server/src/filters/HttpExceptions.filter.ts @@ -0,0 +1,89 @@ +import { + ArgumentsHost, + Catch, + HttpException, + HttpServer, + HttpStatus, +} from "@nestjs/common"; +import { BaseExceptionFilter } from "@nestjs/core"; +import { Prisma } from "@prisma/client"; +import { Response } from "express"; + +export type ErrorCodesStatusMapping = { + [key: string]: number; +}; + +/** + * {@link PrismaClientExceptionFilter} handling {@link Prisma.PrismaClientKnownRequestError} exceptions. + */ +@Catch(Prisma?.PrismaClientKnownRequestError) +export class HttpExceptionFilter extends BaseExceptionFilter { + /** + * default error codes mapping + * + * Error codes definition for Prisma Client (Query Engine) + * @see https://www.prisma.io/docs/reference/api-reference/error-reference#prisma-client-query-engine + */ + private errorCodesStatusMapping: ErrorCodesStatusMapping = { + P2000: HttpStatus.BAD_REQUEST, + P2002: HttpStatus.CONFLICT, + P2025: HttpStatus.NOT_FOUND, + }; + + /** + * @param applicationRef + */ + // eslint-disable-next-line @typescript-eslint/no-useless-constructor + constructor(applicationRef?: HttpServer) { + super(applicationRef); + } + + /** + * @param exception + * @param host + * @returns + */ + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + catch(exception: Prisma.PrismaClientKnownRequestError, host: ArgumentsHost) { + const statusCode = this.errorCodesStatusMapping[exception.code]; + let message; + if (host.getType() === "http") { + // for http requests (REST) + // Todo : Add all other exception types and also add mapping + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + if (exception.code === "P2002") { + // Handling Unique Key Constraint Violation Error + const fields = (exception.meta as { target: string[] }).target; + message = `Another record with the requested (${fields.join( + ", " + )}) already exists`; + } else { + message = + `[${exception.code}]: ` + + this.exceptionShortMessage(exception.message); + } + if (!Object.keys(this.errorCodesStatusMapping).includes(exception.code)) { + return super.catch(exception, host); + } + const errorResponse = { + message: message, + statusCode: statusCode, + }; + response.status(statusCode).send(errorResponse); + } + return new HttpException({ statusCode, message }, statusCode); + } + + /** + * @param exception + * @returns short message for the exception + */ + exceptionShortMessage(message: string): string { + const shortMessage = message.substring(message.indexOf("→")); + return shortMessage + .substring(shortMessage.indexOf("\n")) + .replace(/\n/g, "") + .trim(); + } +} diff --git a/server/src/health/base/health.controller.base.ts b/server/src/health/base/health.controller.base.ts new file mode 100644 index 0000000..afd9e0d --- /dev/null +++ b/server/src/health/base/health.controller.base.ts @@ -0,0 +1,19 @@ +import { Get, HttpStatus, Res } from "@nestjs/common"; +import { Response } from "express"; +import { HealthService } from "../health.service"; + +export class HealthControllerBase { + constructor(protected readonly healthService: HealthService) {} + @Get("live") + healthLive(@Res() response: Response): Response { + return response.status(HttpStatus.NO_CONTENT).send(); + } + @Get("ready") + async healthReady(@Res() response: Response): Promise> { + const dbConnection = await this.healthService.isDbReady(); + if (!dbConnection) { + return response.status(HttpStatus.NOT_FOUND).send(); + } + return response.status(HttpStatus.NO_CONTENT).send(); + } +} diff --git a/server/src/health/base/health.service.base.ts b/server/src/health/base/health.service.base.ts new file mode 100644 index 0000000..49a93a5 --- /dev/null +++ b/server/src/health/base/health.service.base.ts @@ -0,0 +1,15 @@ +import { Injectable } from "@nestjs/common"; +import { PrismaService } from "../../prisma/prisma.service"; + +@Injectable() +export class HealthServiceBase { + constructor(protected readonly prisma: PrismaService) {} + async isDbReady(): Promise { + try { + await this.prisma.$queryRaw`SELECT 1`; + return true; + } catch (error) { + return false; + } + } +} diff --git a/server/src/health/health.controller.ts b/server/src/health/health.controller.ts new file mode 100644 index 0000000..ff484e7 --- /dev/null +++ b/server/src/health/health.controller.ts @@ -0,0 +1,10 @@ +import { Controller } from "@nestjs/common"; +import { HealthControllerBase } from "./base/health.controller.base"; +import { HealthService } from "./health.service"; + +@Controller("_health") +export class HealthController extends HealthControllerBase { + constructor(protected readonly healthService: HealthService) { + super(healthService); + } +} diff --git a/server/src/health/health.module.ts b/server/src/health/health.module.ts new file mode 100644 index 0000000..39eff7f --- /dev/null +++ b/server/src/health/health.module.ts @@ -0,0 +1,10 @@ +import { Module } from "@nestjs/common"; +import { HealthController } from "./health.controller"; +import { HealthService } from "./health.service"; + +@Module({ + controllers: [HealthController], + providers: [HealthService], + exports: [HealthService], +}) +export class HealthModule {} diff --git a/server/src/health/health.service.ts b/server/src/health/health.service.ts new file mode 100644 index 0000000..44d9343 --- /dev/null +++ b/server/src/health/health.service.ts @@ -0,0 +1,10 @@ +import { Injectable } from "@nestjs/common"; +import { PrismaService } from "../prisma/prisma.service"; +import { HealthServiceBase } from "./base/health.service.base"; + +@Injectable() +export class HealthService extends HealthServiceBase { + constructor(protected readonly prisma: PrismaService) { + super(prisma); + } +} diff --git a/server/src/main.ts b/server/src/main.ts new file mode 100644 index 0000000..474eead --- /dev/null +++ b/server/src/main.ts @@ -0,0 +1,53 @@ +import { ValidationPipe } from "@nestjs/common"; +import { HttpAdapterHost, NestFactory } from "@nestjs/core"; +import { OpenAPIObject, SwaggerModule } from "@nestjs/swagger"; +import { HttpExceptionFilter } from "./filters/HttpExceptions.filter"; +import { AppModule } from "./app.module"; +import { connectMicroservices } from "./connectMicroservices"; +import { + swaggerPath, + swaggerDocumentOptions, + swaggerSetupOptions, +} from "./swagger"; + +const { PORT = 3000 } = process.env; + +async function main() { + const app = await NestFactory.create(AppModule, { cors: true }); + + app.setGlobalPrefix("api"); + app.useGlobalPipes( + new ValidationPipe({ + transform: true, + forbidUnknownValues: false, + }) + ); + + const document = SwaggerModule.createDocument(app, swaggerDocumentOptions); + + /** check if there is Public decorator for each path (action) and its method (findMany / findOne) on each controller */ + Object.values((document as OpenAPIObject).paths).forEach((path: any) => { + Object.values(path).forEach((method: any) => { + if ( + Array.isArray(method.security) && + method.security.includes("isPublic") + ) { + method.security = []; + } + }); + }); + + await connectMicroservices(app); + await app.startAllMicroservices(); + + SwaggerModule.setup(swaggerPath, app, document, swaggerSetupOptions); + + const { httpAdapter } = app.get(HttpAdapterHost); + app.useGlobalFilters(new HttpExceptionFilter(httpAdapter)); + + void app.listen(PORT); + + return app; +} + +module.exports = main(); diff --git a/server/src/prisma.util.spec.ts b/server/src/prisma.util.spec.ts new file mode 100644 index 0000000..0aa308e --- /dev/null +++ b/server/src/prisma.util.spec.ts @@ -0,0 +1,23 @@ +import { + isRecordNotFoundError, + PRISMA_QUERY_INTERPRETATION_ERROR, +} from "./prisma.util"; + +describe("isRecordNotFoundError", () => { + test("returns true for record not found error", () => { + expect( + isRecordNotFoundError( + Object.assign( + new Error(`Error occurred during query execution: + InterpretationError("Error for binding '0': RecordNotFound("Record to update not found.")")`), + { + code: PRISMA_QUERY_INTERPRETATION_ERROR, + } + ) + ) + ).toBe(true); + }); + test("returns false for any other error", () => { + expect(isRecordNotFoundError(new Error())).toBe(false); + }); +}); diff --git a/server/src/prisma.util.ts b/server/src/prisma.util.ts new file mode 100644 index 0000000..029b98a --- /dev/null +++ b/server/src/prisma.util.ts @@ -0,0 +1,29 @@ +export const PRISMA_QUERY_INTERPRETATION_ERROR = "P2016"; +export const PRISMA_RECORD_NOT_FOUND = "RecordNotFound"; + +export function isRecordNotFoundError(error: any): boolean { + return ( + error instanceof Error && + "code" in error && + error.code === PRISMA_QUERY_INTERPRETATION_ERROR && + error.message.includes(PRISMA_RECORD_NOT_FOUND) + ); +} + +export async function transformStringFieldUpdateInput< + T extends undefined | string | { set?: string } +>(input: T, transform: (input: string) => Promise): Promise { + if (typeof input === "object" && typeof input?.set === "string") { + return { set: await transform(input.set) } as T; + } + if (typeof input === "object") { + if (typeof input.set === "string") { + return { set: await transform(input.set) } as T; + } + return input; + } + if (typeof input === "string") { + return (await transform(input)) as T; + } + return input; +} diff --git a/server/src/prisma/prisma.module.ts b/server/src/prisma/prisma.module.ts new file mode 100644 index 0000000..1edbf95 --- /dev/null +++ b/server/src/prisma/prisma.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from "@nestjs/common"; +import { PrismaService } from "./prisma.service"; + +@Global() +@Module({ + providers: [PrismaService], + exports: [PrismaService], +}) +export class PrismaModule {} diff --git a/server/src/prisma/prisma.service.ts b/server/src/prisma/prisma.service.ts new file mode 100644 index 0000000..79ea4fa --- /dev/null +++ b/server/src/prisma/prisma.service.ts @@ -0,0 +1,9 @@ +import { Injectable, OnModuleInit, INestApplication } from "@nestjs/common"; +import { PrismaClient } from "@prisma/client"; + +@Injectable() +export class PrismaService extends PrismaClient implements OnModuleInit { + async onModuleInit() { + await this.$connect(); + } +} diff --git a/server/src/providers/secrets/base/secretsManager.service.base.spec.ts b/server/src/providers/secrets/base/secretsManager.service.base.spec.ts new file mode 100644 index 0000000..f161172 --- /dev/null +++ b/server/src/providers/secrets/base/secretsManager.service.base.spec.ts @@ -0,0 +1,41 @@ +import { ConfigService } from "@nestjs/config"; +import { mock } from "jest-mock-extended"; +import { SecretsManagerServiceBase } from "./secretsManager.service.base"; +import { EnumSecretsNameKey } from "../secretsNameKey.enum"; + +describe("Testing the secrets manager base class", () => { + const SECRET_KEY = "SECRET_KEY"; + const SECRET_VALUE = "SECRET_VALUE"; + const configService = mock(); + const secretsManagerServiceBase = new SecretsManagerServiceBase( + configService + ); + beforeEach(() => { + configService.get.mockClear(); + }); + it("should return value from env", async () => { + //ARRANGE + configService.get.mockReturnValue(SECRET_VALUE); + //ACT + const result = await secretsManagerServiceBase.getSecret( + SECRET_KEY as unknown as EnumSecretsNameKey + ); + //ASSERT + expect(result).toBe(SECRET_VALUE); + }); + it("should return null for unknown keys", async () => { + //ARRANGE + configService.get.mockReturnValue(undefined); + //ACT + const result = await secretsManagerServiceBase.getSecret( + SECRET_KEY as unknown as EnumSecretsNameKey + ); + //ASSERT + expect(result).toBeNull(); + }); + it("should throw an exception if getting null key", () => { + return expect( + secretsManagerServiceBase.getSecret(null as unknown as EnumSecretsNameKey) + ).rejects.toThrow(); + }); +}); diff --git a/server/src/providers/secrets/base/secretsManager.service.base.ts b/server/src/providers/secrets/base/secretsManager.service.base.ts new file mode 100644 index 0000000..340818c --- /dev/null +++ b/server/src/providers/secrets/base/secretsManager.service.base.ts @@ -0,0 +1,17 @@ +import { ConfigService } from "@nestjs/config"; +import { EnumSecretsNameKey } from "../secretsNameKey.enum"; + +export interface ISecretsManager { + getSecret: (key: EnumSecretsNameKey) => Promise; +} + +export class SecretsManagerServiceBase implements ISecretsManager { + constructor(protected readonly configService: ConfigService) {} + async getSecret(key: EnumSecretsNameKey): Promise { + const value = this.configService.get(key.toString()); + if (value) { + return value; + } + return null; + } +} diff --git a/server/src/providers/secrets/secretsManager.module.ts b/server/src/providers/secrets/secretsManager.module.ts new file mode 100644 index 0000000..3a621e4 --- /dev/null +++ b/server/src/providers/secrets/secretsManager.module.ts @@ -0,0 +1,8 @@ +import { Module } from "@nestjs/common"; +import { SecretsManagerService } from "./secretsManager.service"; + +@Module({ + providers: [SecretsManagerService], + exports: [SecretsManagerService], +}) +export class SecretsManagerModule {} diff --git a/server/src/providers/secrets/secretsManager.service.ts b/server/src/providers/secrets/secretsManager.service.ts new file mode 100644 index 0000000..89907c3 --- /dev/null +++ b/server/src/providers/secrets/secretsManager.service.ts @@ -0,0 +1,10 @@ +import { Injectable } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { SecretsManagerServiceBase } from "./base/secretsManager.service.base"; + +@Injectable() +export class SecretsManagerService extends SecretsManagerServiceBase { + constructor(protected readonly configService: ConfigService) { + super(configService); + } +} diff --git a/server/src/providers/secrets/secretsNameKey.enum.ts b/server/src/providers/secrets/secretsNameKey.enum.ts new file mode 100644 index 0000000..e225d65 --- /dev/null +++ b/server/src/providers/secrets/secretsNameKey.enum.ts @@ -0,0 +1 @@ +export enum EnumSecretsNameKey {} \ No newline at end of file diff --git a/server/src/serveStaticOptions.service.ts b/server/src/serveStaticOptions.service.ts new file mode 100644 index 0000000..390248b --- /dev/null +++ b/server/src/serveStaticOptions.service.ts @@ -0,0 +1,39 @@ +import * as path from "path"; +import { Injectable, Logger } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { + ServeStaticModuleOptions, + ServeStaticModuleOptionsFactory, +} from "@nestjs/serve-static"; + +const SERVE_STATIC_ROOT_PATH_VAR = "SERVE_STATIC_ROOT_PATH"; +const DEFAULT_STATIC_MODULE_OPTIONS_LIST: ServeStaticModuleOptions[] = [ + { + serveRoot: "/swagger", + rootPath: path.join(__dirname, "swagger"), + }, +]; + +@Injectable() +export class ServeStaticOptionsService + implements ServeStaticModuleOptionsFactory +{ + private readonly logger = new Logger(ServeStaticOptionsService.name); + + constructor(private readonly configService: ConfigService) {} + + createLoggerOptions(): ServeStaticModuleOptions[] { + const serveStaticRootPath = this.configService.get( + SERVE_STATIC_ROOT_PATH_VAR + ); + if (serveStaticRootPath) { + const resolvedPath = path.resolve(serveStaticRootPath); + this.logger.log(`Serving static files from ${resolvedPath}`); + return [ + ...DEFAULT_STATIC_MODULE_OPTIONS_LIST, + { rootPath: resolvedPath, exclude: ["/api*", "/graphql"] }, + ]; + } + return DEFAULT_STATIC_MODULE_OPTIONS_LIST; + } +} diff --git a/server/src/swagger.ts b/server/src/swagger.ts new file mode 100644 index 0000000..889f868 --- /dev/null +++ b/server/src/swagger.ts @@ -0,0 +1,20 @@ +import { DocumentBuilder, SwaggerCustomOptions } from "@nestjs/swagger"; + +export const swaggerPath = "api"; + +export const swaggerDocumentOptions = new DocumentBuilder() + .setTitle("bookstore") + .setDescription( + '\n\n## Congratulations! Your service resource is ready.\n \nPlease note that all endpoints are secured with JWT Bearer authentication.\nBy default, your service resource comes with one user with the username "admin" and password "admin".\nLearn more in [our docs](https://docs.amplication.com)' + ) + .addBearerAuth() + .build(); + +export const swaggerSetupOptions: SwaggerCustomOptions = { + swaggerOptions: { + persistAuthorization: true, + }, + customCssUrl: "../swagger/swagger.css", + customfavIcon: "../swagger/favicon.png", + customSiteTitle: "bookstore", +}; diff --git a/server/src/swagger/favicon.png b/server/src/swagger/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..a79882d55bdcd1d04939ed7a42257c20ac32f17c GIT binary patch literal 2498 zcmZ`*>rdNd6#kVvl#bgdw{@jJ2c2M{$hfWC29(Qyav88OF5?my1-7meXrU!j6F<&G z-ABd5FD8azVwPxROPCY=pqU9IVsw7-3sY^_r^PK0L_t)QhIp_Dnt0#Nz zUY8*$H%ZepLuW_p0DL|zAr9;d1YSMa)3n&PI$N8EMqLPi?xpdO9FbuTLR}6=B34ZB zy#t_5cb?U4GfMi3xYNne*$y3abW!UWxa^9V{q33d1Q|_*bMr~9z|sXrD};qn7-fYX zJ9r$ZL*gj`_q@8O&Pu=ZcALiC%(#YrvYl=>*7ZL+_uZ1LLR8y1ma@|zuo%${e}1{$Q(P*)dWk2g>f3H@$e(CwZ3uE+aLqK z?07P=SW=y8g0XIce%v;Tj76T%H!`q$&8i7QLkLwB>*hDih!$ad2k`gKV+Pkitia~M zciiE|BnaTf0h>7ReDWpmD!&3CPh79gG4=X4tM|)TnEm_>i}Abag~Lx}tm`7YKN~bC z2{6lGV;gQrDqkGf&e8+xJ?wsU@KBR)f`SwY&8}tmC>XE`v$1gLl&@O}9~kZ&FoBJm70?rd0NJ!Xul;7G7}#yNDdgkg_Qo+ur=9{H$f ze=%oW^lx>uo#SAYe4KT#cr^dPgEz_>PRfAnQcB_5%T5S^nd|4zfPjJ11J(BA>a!xj zd>>m~WlmZJx$`IcjVorK2~>kx1SKnXpd5bWTC z`X(#^Yv^;X_dI6bMM&fAqrxc-3ApTslzP0rD-r_&kecO5$Gw z)fhF90)A5MFm65xcD&1q@Gry-n6-b6S5E#+4flMxno9we)N5k?ddS}oaJLCE1mJHG$` literal 0 HcmV?d00001 diff --git a/server/src/swagger/logo-amplication-white.svg b/server/src/swagger/logo-amplication-white.svg new file mode 100644 index 0000000..0054cd4 --- /dev/null +++ b/server/src/swagger/logo-amplication-white.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/server/src/swagger/swagger.css b/server/src/swagger/swagger.css new file mode 100644 index 0000000..b7c4037 --- /dev/null +++ b/server/src/swagger/swagger.css @@ -0,0 +1,321 @@ +html, +body { + background-color: #f4f4f7; +} + +body { + margin: auto; + line-height: 1.6; + font-family: "Poppins", -apple-system, BlinkMacSystemFont, "Segoe UI", + "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", + "Helvetica Neue", sans-serif; + color: #121242; +} + +.swagger-ui { + font-family: "Poppins", -apple-system, BlinkMacSystemFont, "Segoe UI", + "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", + "Helvetica Neue", sans-serif; +} + +.swagger-ui button, +.swagger-ui input, +.swagger-ui optgroup, +.swagger-ui select, +.swagger-ui textarea, +.swagger-ui .parameter__name, +.swagger-ui .parameters-col_name > *, +.swagger-ui label { + font-family: "Poppins", -apple-system, BlinkMacSystemFont, "Segoe UI", + "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", + "Helvetica Neue", sans-serif; + font-weight: normal; + font-size: 12px; + outline: none; +} + +.swagger-ui textarea { + border: 1px solid #d0d0d9; + min-height: 100px; +} + +.swagger-ui input[type="email"], +.swagger-ui input[type="file"], +.swagger-ui input[type="password"], +.swagger-ui input[type="search"], +.swagger-ui input[type="text"], +.swagger-ui textarea { + border-radius: 3px; +} + +.swagger-ui input[disabled], +.swagger-ui select[disabled], +.swagger-ui textarea[disabled] { + background: #f4f4f7; + color: #b8b8c6; +} + +.swagger-ui .btn { + font-family: "Poppins", -apple-system, BlinkMacSystemFont, "Segoe UI", + "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", + "Helvetica Neue", sans-serif; + font-weight: 500; + box-shadow: none; + border: 1px solid #d0d0d9; + height: 28px; + border-radius: 14px; + background-color: #fff; + color: #7950ed; +} + +.swagger-ui .btn:hover { + box-shadow: none; +} + +/* topbar */ + +.swagger-ui .topbar { + background-color: #7950ed; + height: 80px; + padding: 0; + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; +} + +.swagger-ui .topbar-wrapper a { + display: block; + width: 206px; + height: 35px; + background-image: url("logo-amplication-white.svg"); + background-repeat: no-repeat; + background-size: contain; +} + +.swagger-ui .topbar-wrapper svg, +.swagger-ui .topbar-wrapper img { + display: none; +} + +/* title */ +.swagger-ui .info { + margin: 0; +} + +.swagger-ui .info .title { + font-family: "Poppins", -apple-system, BlinkMacSystemFont, "Segoe UI", + "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", + "Helvetica Neue", sans-serif; + font-size: 32px; + font-weight: 600; +} + +.swagger-ui .information-container { + padding-top: 50px; + padding-bottom: 20px; + position: relative; +} + +.swagger-ui .info .title small.version-stamp { + display: none; +} + +.swagger-ui .info .title small { + background-color: #a787ff; +} + +.swagger-ui .info .description p { + max-width: 1000px; + margin: 0; +} + +.swagger-ui .info .description p, +.swagger-ui .info .description a { + font-size: 1rem; +} + +.swagger-ui .information-container section { + position: relative; +} + +.swagger-ui .scheme-container { + box-shadow: none; + background-color: transparent; + position: relative; + margin: 0; + margin-top: 20px; + margin-bottom: 20px; + padding: 0; +} + +.swagger-ui .scheme-container .auth-wrapper { + justify-content: flex-start; +} + +.swagger-ui .btn.authorize { + box-shadow: none; + border: 1px solid #d0d0d9; + height: 40px; + border-radius: 20px; + background-color: #fff; + color: #7950ed; +} + +.swagger-ui .btn.authorize svg { + fill: #7950ed; +} + +/* content */ + +.swagger-ui .opblock-tag { + font-family: "Poppins", -apple-system, BlinkMacSystemFont, "Segoe UI", + "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", + "Helvetica Neue", sans-serif; + font-size: 28px; + font-weight: 600; +} + +.swagger-ui .opblock.is-open .opblock-summary { + border-color: #e7e7ec !important; + border-bottom: none; +} + +.swagger-ui .opblock .opblock-section-header { + background-color: #fff; + border: none; + border-top: 1px solid #e7e7ec; + border-bottom: 1px solid #e7e7ec; + box-shadow: none; +} + +.swagger-ui .opblock .tab-header .tab-item.active h4 span:after { + display: none; +} + +.swagger-ui .opblock.opblock-post { + border: 1px solid #e7e7ec; + background: #f9f9fa; + box-shadow: none; + color: #fff; +} + +.swagger-ui .opblock.opblock-post:hover, +.swagger-ui .opblock.opblock-post.is-open { + border-color: #31c587; +} + +.swagger-ui .opblock.opblock-post .opblock-summary-method { + background-color: #31c587; +} + +.swagger-ui .opblock.opblock-get { + border: 1px solid #e7e7ec; + background: #f9f9fa; + box-shadow: none; +} +.swagger-ui .opblock.opblock-get:hover, +.swagger-ui .opblock.opblock-get.is-open { + border-color: #20a4f3; +} +.swagger-ui .opblock.opblock-get .opblock-summary-method { + background-color: #20a4f3; +} + +.swagger-ui .opblock.opblock-delete { + border: 1px solid #e7e7ec; + background: #f9f9fa; + box-shadow: none; +} +.swagger-ui .opblock.opblock-delete:hover, +.swagger-ui .opblock.opblock-delete.is-open { + border-color: #e93c51; +} +.swagger-ui .opblock.opblock-delete .opblock-summary-method { + background-color: #e93c51; +} + +.swagger-ui .opblock.opblock-patch { + border: 1px solid #e7e7ec; + background: #f9f9fa; + box-shadow: none; +} +.swagger-ui .opblock.opblock-patch:hover, +.swagger-ui .opblock.opblock-patch.is-open { + border-color: #41cadd; +} +.swagger-ui .opblock.opblock-patch .opblock-summary-method { + background-color: #41cadd; +} + +.swagger-ui .opblock-body pre { + background-color: #121242 !important; +} + +.swagger-ui select, +.swagger-ui .response-control-media-type--accept-controller select { + border: 1px solid #d0d0d9; + box-shadow: none; + outline: none; +} + +/* models */ + +.swagger-ui section.models { + background-color: #fff; + border: 1px solid #e7e7ec; +} + +.swagger-ui section.models.is-open h4 { + border-bottom: 1px solid #e7e7ec; +} + +.swagger-ui section.models .model-container, +.swagger-ui section.models .model-container:hover { + background-color: #f4f4f7; + color: #121242; +} + +.swagger-ui .model-title { + font-family: "Poppins", -apple-system, BlinkMacSystemFont, "Segoe UI", + "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", + "Helvetica Neue", sans-serif; + font-weight: normal; + font-size: 15px; + color: #121242; +} + +/* modal */ + +.swagger-ui .dialog-ux .modal-ux-header h3, +.swagger-ui .dialog-ux .modal-ux-content h4, +.swagger-ui .dialog-ux .modal-ux-content h4 code { + font-family: "Poppins", -apple-system, BlinkMacSystemFont, "Segoe UI", + "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", + "Helvetica Neue", sans-serif; + font-weight: normal; + font-size: 15px; + color: #121242; +} + +.swagger-ui .dialog-ux .modal-ux-content .btn.authorize { + height: 28px; + border-radius: 14px; +} + +.swagger-ui .auth-btn-wrapper { + display: flex; + flex-direction: row-reverse; + align-items: center; + justify-content: flex-start; +} + +.swagger-ui .auth-btn-wrapper .btn-done { + border: none; + color: #121242; + margin-right: 0; +} + +.swagger-ui .authorization__btn { + fill: #414168; +} diff --git a/server/src/tests/health/health.service.spec.ts b/server/src/tests/health/health.service.spec.ts new file mode 100644 index 0000000..4b191f1 --- /dev/null +++ b/server/src/tests/health/health.service.spec.ts @@ -0,0 +1,36 @@ +import { mock } from "jest-mock-extended"; +import { PrismaService } from "../../prisma/prisma.service"; +import { HealthServiceBase } from "../../health/base/health.service.base"; + +describe("Testing the HealthServiceBase", () => { + //ARRANGE + let prismaService: PrismaService; + let healthServiceBase: HealthServiceBase; + + describe("Testing the isDbReady function in HealthServiceBase class", () => { + beforeEach(() => { + prismaService = mock(); + healthServiceBase = new HealthServiceBase(prismaService); + }); + it("should return true if allow connection to db", async () => { + //ARRANGE + (prismaService.$queryRaw as jest.Mock).mockReturnValue( + Promise.resolve(true) + ); + //ACT + const response = await healthServiceBase.isDbReady(); + //ASSERT + expect(response).toBe(true); + }); + it("should return false if db is not available", async () => { + //ARRANGE + (prismaService.$queryRaw as jest.Mock).mockReturnValue( + Promise.reject(false) + ); + //ACT + const response = await healthServiceBase.isDbReady(); + //ASSERT + expect(response).toBe(false); + }); + }); +}); diff --git a/server/src/types.ts b/server/src/types.ts new file mode 100644 index 0000000..f762a5d --- /dev/null +++ b/server/src/types.ts @@ -0,0 +1,3 @@ +import type { JsonValue } from "type-fest"; + +export type InputJsonValue = Omit; diff --git a/server/src/util/BooleanFilter.ts b/server/src/util/BooleanFilter.ts new file mode 100644 index 0000000..75f4e34 --- /dev/null +++ b/server/src/util/BooleanFilter.ts @@ -0,0 +1,32 @@ +import { Field, InputType } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional } from "class-validator"; +import { Type } from "class-transformer"; + +@InputType({ + isAbstract: true, + description: undefined, +}) +export class BooleanFilter { + @ApiProperty({ + required: false, + type: Boolean, + }) + @IsOptional() + @Field(() => Boolean, { + nullable: true, + }) + @Type(() => Boolean) + equals?: boolean; + + @ApiProperty({ + required: false, + type: Boolean, + }) + @IsOptional() + @Field(() => Boolean, { + nullable: true, + }) + @Type(() => Boolean) + not?: boolean; +} diff --git a/server/src/util/BooleanNullableFilter.ts b/server/src/util/BooleanNullableFilter.ts new file mode 100644 index 0000000..9f48ac1 --- /dev/null +++ b/server/src/util/BooleanNullableFilter.ts @@ -0,0 +1,31 @@ +import { Field, InputType } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional } from "class-validator"; +import { Type } from "class-transformer"; +@InputType({ + isAbstract: true, + description: undefined, +}) +export class BooleanNullableFilter { + @ApiProperty({ + required: false, + type: Boolean, + }) + @IsOptional() + @Field(() => Boolean, { + nullable: true, + }) + @Type(() => Boolean) + equals?: boolean | null; + + @ApiProperty({ + required: false, + type: Boolean, + }) + @IsOptional() + @Field(() => Boolean, { + nullable: true, + }) + @Type(() => Boolean) + not?: boolean | null; +} diff --git a/server/src/util/DateTimeFilter.ts b/server/src/util/DateTimeFilter.ts new file mode 100644 index 0000000..d2b6dfb --- /dev/null +++ b/server/src/util/DateTimeFilter.ts @@ -0,0 +1,97 @@ +import { Field, InputType } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional } from "class-validator"; +import { Type } from "class-transformer"; +@InputType({ + isAbstract: true, + description: undefined, +}) +export class DateTimeFilter { + @ApiProperty({ + required: false, + type: Date, + }) + @IsOptional() + @Field(() => Date, { + nullable: true, + }) + @Type(() => Date) + equals?: Date; + + @ApiProperty({ + required: false, + type: Date, + }) + @IsOptional() + @Field(() => Date, { + nullable: true, + }) + @Type(() => Date) + not?: Date; + + @ApiProperty({ + required: false, + type: [Date], + }) + @IsOptional() + @Field(() => [Date], { + nullable: true, + }) + @Type(() => Date) + in?: Date[]; + + @ApiProperty({ + required: false, + type: [Date], + }) + @IsOptional() + @Field(() => [Date], { + nullable: true, + }) + @Type(() => Date) + notIn?: Date[]; + + @ApiProperty({ + required: false, + type: Date, + }) + @IsOptional() + @Field(() => Date, { + nullable: true, + }) + @Type(() => Date) + lt?: Date; + + @ApiProperty({ + required: false, + type: Date, + }) + @IsOptional() + @Field(() => Date, { + nullable: true, + }) + @Type(() => Date) + lte?: Date; + + @ApiProperty({ + required: false, + type: Date, + }) + @IsOptional() + @Field(() => Date, { + nullable: true, + }) + @Type(() => Date) + gt?: Date; + + @ApiProperty({ + required: false, + type: Date, + }) + @IsOptional() + @Field(() => Date, { + nullable: true, + }) + @Type(() => Date) + gte?: Date; +} diff --git a/server/src/util/DateTimeNullableFilter.ts b/server/src/util/DateTimeNullableFilter.ts new file mode 100644 index 0000000..ccc00a5 --- /dev/null +++ b/server/src/util/DateTimeNullableFilter.ts @@ -0,0 +1,97 @@ +import { Field, InputType } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional } from "class-validator"; +import { Type } from "class-transformer"; +@InputType({ + isAbstract: true, + description: undefined, +}) +export class DateTimeNullableFilter { + @ApiProperty({ + required: false, + type: Date, + }) + @IsOptional() + @Field(() => Date, { + nullable: true, + }) + @Type(() => Date) + equals?: Date | null; + + @ApiProperty({ + required: false, + type: [Date], + }) + @IsOptional() + @Field(() => [Date], { + nullable: true, + }) + @Type(() => Date) + in?: Date[] | null; + + @ApiProperty({ + required: false, + type: [Date], + }) + @IsOptional() + @Field(() => [Date], { + nullable: true, + }) + @Type(() => Date) + notIn?: Date[] | null; + + @ApiProperty({ + required: false, + type: Date, + }) + @IsOptional() + @Field(() => Date, { + nullable: true, + }) + @Type(() => Date) + lt?: Date; + + @ApiProperty({ + required: false, + type: Date, + }) + @IsOptional() + @Field(() => Date, { + nullable: true, + }) + @Type(() => Date) + lte?: Date; + + @ApiProperty({ + required: false, + type: Date, + }) + @IsOptional() + @Field(() => Date, { + nullable: true, + }) + @Type(() => Date) + gt?: Date; + + @ApiProperty({ + required: false, + type: Date, + }) + @IsOptional() + @Field(() => Date, { + nullable: true, + }) + @Type(() => Date) + gte?: Date; + + @ApiProperty({ + required: false, + type: Date, + }) + @IsOptional() + @Field(() => Date, { + nullable: true, + }) + @Type(() => Date) + not?: Date; +} diff --git a/server/src/util/FloatFilter.ts b/server/src/util/FloatFilter.ts new file mode 100644 index 0000000..a3266d2 --- /dev/null +++ b/server/src/util/FloatFilter.ts @@ -0,0 +1,98 @@ +import { Field, InputType, Float } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional } from "class-validator"; +import { Type } from "class-transformer"; + +@InputType({ + isAbstract: true, + description: undefined, +}) +export class FloatFilter { + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Float, { + nullable: true, + }) + @Type(() => Number) + equals?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => [Float], { + nullable: true, + }) + @Type(() => Number) + in?: number[]; + + @ApiProperty({ + required: false, + type: [Number], + }) + @IsOptional() + @Field(() => [Float], { + nullable: true, + }) + @Type(() => Number) + notIn?: number[]; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Float, { + nullable: true, + }) + @Type(() => Number) + lt?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Float, { + nullable: true, + }) + @Type(() => Number) + lte?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Float, { + nullable: true, + }) + @Type(() => Number) + gt?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Float, { + nullable: true, + }) + @Type(() => Number) + gte?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Float, { + nullable: true, + }) + @Type(() => Number) + not?: number; +} diff --git a/server/src/util/FloatNullableFilter.ts b/server/src/util/FloatNullableFilter.ts new file mode 100644 index 0000000..feb0fc5 --- /dev/null +++ b/server/src/util/FloatNullableFilter.ts @@ -0,0 +1,98 @@ +import { Field, InputType, Float } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional } from "class-validator"; +import { Type } from "class-transformer"; + +@InputType({ + isAbstract: true, + description: undefined, +}) +export class FloatNullableFilter { + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Float, { + nullable: true, + }) + @Type(() => Number) + equals?: number | null; + + @ApiProperty({ + required: false, + type: [Number], + }) + @IsOptional() + @Field(() => [Float], { + nullable: true, + }) + @Type(() => Number) + in?: number[] | null; + + @ApiProperty({ + required: false, + type: [Number], + }) + @IsOptional() + @Field(() => [Float], { + nullable: true, + }) + @Type(() => Number) + notIn?: number[] | null; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Float, { + nullable: true, + }) + @Type(() => Number) + lt?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Float, { + nullable: true, + }) + @Type(() => Number) + lte?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Float, { + nullable: true, + }) + @Type(() => Number) + gt?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Float, { + nullable: true, + }) + @Type(() => Number) + gte?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Float, { + nullable: true, + }) + @Type(() => Number) + not?: number; +} diff --git a/server/src/util/IntFilter.ts b/server/src/util/IntFilter.ts new file mode 100644 index 0000000..f6880e7 --- /dev/null +++ b/server/src/util/IntFilter.ts @@ -0,0 +1,98 @@ +import { Field, InputType, Int } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional } from "class-validator"; +import { Type } from "class-transformer"; + +@InputType({ + isAbstract: true, + description: undefined, +}) +export class IntFilter { + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Int, { + nullable: true, + }) + @Type(() => Number) + equals?: number; + + @ApiProperty({ + required: false, + type: [Number], + }) + @IsOptional() + @Field(() => [Int], { + nullable: true, + }) + @Type(() => Number) + in?: number[]; + + @ApiProperty({ + required: false, + type: [Number], + }) + @IsOptional() + @Field(() => [Int], { + nullable: true, + }) + @Type(() => Number) + notIn?: number[]; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Int, { + nullable: true, + }) + @Type(() => Number) + lt?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Int, { + nullable: true, + }) + @Type(() => Number) + lte?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Int, { + nullable: true, + }) + @Type(() => Number) + gt?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Int, { + nullable: true, + }) + @Type(() => Number) + gte?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Int, { + nullable: true, + }) + @Type(() => Number) + not?: number; +} diff --git a/server/src/util/IntNullableFilter.ts b/server/src/util/IntNullableFilter.ts new file mode 100644 index 0000000..e3b71a3 --- /dev/null +++ b/server/src/util/IntNullableFilter.ts @@ -0,0 +1,98 @@ +import { Field, InputType, Int } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional } from "class-validator"; +import { Type } from "class-transformer"; + +@InputType({ + isAbstract: true, + description: undefined, +}) +export class IntNullableFilter { + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Int, { + nullable: true, + }) + @Type(() => Number) + equals?: number | null; + + @ApiProperty({ + required: false, + type: [Number], + }) + @IsOptional() + @Field(() => [Int], { + nullable: true, + }) + @Type(() => Number) + in?: number[] | null; + + @ApiProperty({ + required: false, + type: [Number], + }) + @IsOptional() + @Field(() => [Int], { + nullable: true, + }) + @Type(() => Number) + notIn?: number[] | null; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Int, { + nullable: true, + }) + @Type(() => Number) + lt?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Int, { + nullable: true, + }) + @Type(() => Number) + lte?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Int, { + nullable: true, + }) + @Type(() => Number) + gt?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Int, { + nullable: true, + }) + @Type(() => Number) + gte?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Int, { + nullable: true, + }) + @Type(() => Number) + not?: number; +} diff --git a/server/src/util/JsonFilter.ts b/server/src/util/JsonFilter.ts new file mode 100644 index 0000000..7040b74 --- /dev/null +++ b/server/src/util/JsonFilter.ts @@ -0,0 +1,31 @@ +import { Field, InputType } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional } from "class-validator"; +import { GraphQLJSONObject } from "graphql-type-json"; +import { InputJsonValue } from "../types"; + +@InputType({ + isAbstract: true, + description: undefined, +}) +export class JsonFilter { + @ApiProperty({ + required: false, + type: GraphQLJSONObject, + }) + @IsOptional() + @Field(() => GraphQLJSONObject, { + nullable: true, + }) + equals?: InputJsonValue; + + @ApiProperty({ + required: false, + type: GraphQLJSONObject, + }) + @IsOptional() + @Field(() => GraphQLJSONObject, { + nullable: true, + }) + not?: InputJsonValue; +} diff --git a/server/src/util/JsonNullableFilter.ts b/server/src/util/JsonNullableFilter.ts new file mode 100644 index 0000000..3381d52 --- /dev/null +++ b/server/src/util/JsonNullableFilter.ts @@ -0,0 +1,31 @@ +import type { JsonValue } from "type-fest"; +import { Field, InputType } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional } from "class-validator"; +import { GraphQLJSONObject } from "graphql-type-json"; + +@InputType({ + isAbstract: true, + description: undefined, +}) +export class JsonNullableFilter { + @ApiProperty({ + required: false, + type: GraphQLJSONObject, + }) + @IsOptional() + @Field(() => GraphQLJSONObject, { + nullable: true, + }) + equals?: JsonValue; + + @ApiProperty({ + required: false, + type: GraphQLJSONObject, + }) + @IsOptional() + @Field(() => GraphQLJSONObject, { + nullable: true, + }) + not?: JsonValue; +} diff --git a/server/src/util/MetaQueryPayload.ts b/server/src/util/MetaQueryPayload.ts new file mode 100644 index 0000000..fc30531 --- /dev/null +++ b/server/src/util/MetaQueryPayload.ts @@ -0,0 +1,13 @@ +import { ObjectType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; + +@ObjectType() +class MetaQueryPayload { + @ApiProperty({ + required: true, + type: [Number], + }) + @Field(() => Number) + count!: number; +} +export { MetaQueryPayload }; diff --git a/server/src/util/QueryMode.ts b/server/src/util/QueryMode.ts new file mode 100644 index 0000000..f9b1653 --- /dev/null +++ b/server/src/util/QueryMode.ts @@ -0,0 +1,10 @@ +import { registerEnumType } from "@nestjs/graphql"; + +export enum QueryMode { + Default = "default", + Insensitive = "insensitive", +} +registerEnumType(QueryMode, { + name: "QueryMode", + description: undefined, +}); diff --git a/server/src/util/SortOrder.ts b/server/src/util/SortOrder.ts new file mode 100644 index 0000000..d4108e6 --- /dev/null +++ b/server/src/util/SortOrder.ts @@ -0,0 +1,10 @@ +import { registerEnumType } from "@nestjs/graphql"; + +export enum SortOrder { + Asc = "asc", + Desc = "desc", +} +registerEnumType(SortOrder, { + name: "SortOrder", + description: undefined, +}); diff --git a/server/src/util/StringFilter.ts b/server/src/util/StringFilter.ts new file mode 100644 index 0000000..80b573d --- /dev/null +++ b/server/src/util/StringFilter.ts @@ -0,0 +1,141 @@ +import { Field, InputType } from "@nestjs/graphql"; +import { QueryMode } from "./QueryMode"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional } from "class-validator"; +import { Type } from "class-transformer"; + +@InputType({ + isAbstract: true, +}) +export class StringFilter { + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + equals?: string; + + @ApiProperty({ + required: false, + type: [String], + }) + @IsOptional() + @Field(() => [String], { + nullable: true, + }) + @Type(() => String) + in?: string[]; + + @ApiProperty({ + required: false, + type: [String], + }) + @IsOptional() + @Field(() => [String], { + nullable: true, + }) + @Type(() => String) + notIn?: string[]; + + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + lt?: string; + + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + lte?: string; + + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + gt?: string; + + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + gte?: string; + + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + contains?: string; + + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + startsWith?: string; + + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + endsWith?: string; + + @ApiProperty({ + required: false, + enum: ["Default", "Insensitive"], + }) + @IsOptional() + @Field(() => QueryMode, { + nullable: true, + }) + mode?: QueryMode; + + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + not?: string; +} diff --git a/server/src/util/StringNullableFilter.ts b/server/src/util/StringNullableFilter.ts new file mode 100644 index 0000000..01b399c --- /dev/null +++ b/server/src/util/StringNullableFilter.ts @@ -0,0 +1,141 @@ +import { Field, InputType } from "@nestjs/graphql"; +import { QueryMode } from "./QueryMode"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional } from "class-validator"; +import { Type } from "class-transformer"; + +@InputType({ + isAbstract: true, +}) +export class StringNullableFilter { + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + equals?: string | null; + + @ApiProperty({ + required: false, + type: [String], + }) + @IsOptional() + @Field(() => [String], { + nullable: true, + }) + @Type(() => String) + in?: string[] | null; + + @ApiProperty({ + required: false, + type: [String], + }) + @IsOptional() + @Field(() => [String], { + nullable: true, + }) + @Type(() => String) + notIn?: string[] | null; + + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + lt?: string; + + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + lte?: string; + + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + gt?: string; + + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + gte?: string; + + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + contains?: string; + + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + startsWith?: string; + + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + endsWith?: string; + + @ApiProperty({ + required: false, + enum: ["Default", "Insensitive"], + }) + @IsOptional() + @Field(() => QueryMode, { + nullable: true, + }) + mode?: QueryMode; + + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + not?: string; +} diff --git a/server/src/validators/index.ts b/server/src/validators/index.ts new file mode 100644 index 0000000..7f62d84 --- /dev/null +++ b/server/src/validators/index.ts @@ -0,0 +1 @@ +export * from "./is-json-value-validator"; diff --git a/server/src/validators/is-json-value-validator.spec.ts b/server/src/validators/is-json-value-validator.spec.ts new file mode 100644 index 0000000..5a77824 --- /dev/null +++ b/server/src/validators/is-json-value-validator.spec.ts @@ -0,0 +1,44 @@ +import { validate, ValidationError } from "class-validator"; +import { IsJSONValue } from "./is-json-value-validator"; + +class TestClass { + @IsJSONValue() + jsonProperty: unknown; +} + +describe("IsJSONValue", () => { + it("should validate a valid JSON string", async () => { + const testObj = new TestClass(); + testObj.jsonProperty = '{"name": "John", "age": 30}'; + const errors: ValidationError[] = await validate(testObj); + expect(errors.length).toBe(0); + }); + + it("should not validate an invalid JSON string", async () => { + const testObj = new TestClass(); + testObj.jsonProperty = '{name: "John", age: 30}'; + const errors: ValidationError[] = await validate(testObj); + expect(errors.length).toBe(1); + }); + + it("should not validate an invalid JSON string", async () => { + const testObj = new TestClass(); + testObj.jsonProperty = "John"; + const errors: ValidationError[] = await validate(testObj); + expect(errors.length).toBe(1); + }); + + it("should validate a valid JSON object", async () => { + const testObj = new TestClass(); + testObj.jsonProperty = { name: "John", age: 30 }; + const errors: ValidationError[] = await validate(testObj); + expect(errors.length).toBe(0); + }); + + it("should validate a valid JSON array", async () => { + const testObj = new TestClass(); + testObj.jsonProperty = ["John", "30"]; + const errors: ValidationError[] = await validate(testObj); + expect(errors.length).toBe(0); + }); +}); diff --git a/server/src/validators/is-json-value-validator.ts b/server/src/validators/is-json-value-validator.ts new file mode 100644 index 0000000..1002540 --- /dev/null +++ b/server/src/validators/is-json-value-validator.ts @@ -0,0 +1,29 @@ +import { + ValidationArguments, + registerDecorator, + ValidationOptions, +} from "class-validator"; +import isJSONValidator from "validator/lib/isJSON"; + +export function IsJSONValue(validationOptions?: ValidationOptions) { + return function (object: Record, propertyName: string) { + registerDecorator({ + name: "IsJSONValue", + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + validator: { + validate(value: any, args: ValidationArguments) { + if (typeof value === "string") { + return isJSONValidator(value); + } + + return isJSONValidator(JSON.stringify(value)); + }, + defaultMessage(args: ValidationArguments): string { + return `${args.property} must be a valid json`; + }, + }, + }); + }; +} diff --git a/server/tsconfig.build.json b/server/tsconfig.build.json new file mode 100644 index 0000000..e579401 --- /dev/null +++ b/server/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "prisma", "test", "dist", "**/*spec.ts", "admin"] +} diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 0000000..f6c463b --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "baseUrl": "./", + "module": "commonjs", + "declaration": false, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es2022", + "lib": ["es2023"], + "sourceMap": true, + "outDir": "./dist", + "incremental": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "paths": { + "@app/custom-validators": ["src/validators"] + } + }, + "include": ["src"] +}