From fe1449c8d83b957d9b292bf202b6f8f8aec62789 Mon Sep 17 00:00:00 2001 From: Dima-Mitrich Date: Mon, 29 Nov 2021 15:32:30 +0300 Subject: [PATCH 1/9] TR-9: added query and mutation for Quiz entity. Added methods for creating Quiz, Questions, Answers to services. Added models and dto for entities --- .compose.env | 6 - docker/.dockerignore => .dockerignore | 1 - .env | 7 - .gitignore | 2 +- Dockerfile | 7 + .../docker-compose.yml => docker-compose.yml | 15 +- docker/Dockerfile | 7 - package-lock.json | 668 ++++++++++++++++-- package.json | 4 + schema.gql | 53 ++ src/answers/answers.module.ts | 12 + src/answers/answers.resolver.ts | 7 + src/answers/answers.service.ts | 17 + src/answers/dto/new-answer.input.ts | 16 + src/answers/models/answer.model.ts | 36 + src/app.controller.spec.ts | 22 - src/app.controller.ts | 12 - src/app.module.ts | 29 +- src/app.service.ts | 8 - src/questions/dto/new-question.input.ts | 28 + src/questions/models/question.model.ts | 36 + src/questions/questions.module.ts | 16 + src/questions/questions.resolver.ts | 22 + src/questions/questions.service.ts | 28 + src/quizzes/dto/new-quiz.input.ts | 15 + src/quizzes/models/quiz.model.ts | 26 + src/quizzes/quizzes.module.ts | 15 + src/quizzes/quizzes.resolver.ts | 19 + src/quizzes/quizzes.service.ts | 26 + 29 files changed, 1022 insertions(+), 138 deletions(-) delete mode 100644 .compose.env rename docker/.dockerignore => .dockerignore (71%) delete mode 100644 .env create mode 100644 Dockerfile rename docker/docker-compose.yml => docker-compose.yml (64%) delete mode 100644 docker/Dockerfile create mode 100644 schema.gql create mode 100644 src/answers/answers.module.ts create mode 100644 src/answers/answers.resolver.ts create mode 100644 src/answers/answers.service.ts create mode 100644 src/answers/dto/new-answer.input.ts create mode 100644 src/answers/models/answer.model.ts delete mode 100644 src/app.controller.spec.ts delete mode 100644 src/app.controller.ts delete mode 100644 src/app.service.ts create mode 100644 src/questions/dto/new-question.input.ts create mode 100644 src/questions/models/question.model.ts create mode 100644 src/questions/questions.module.ts create mode 100644 src/questions/questions.resolver.ts create mode 100644 src/questions/questions.service.ts create mode 100644 src/quizzes/dto/new-quiz.input.ts create mode 100644 src/quizzes/models/quiz.model.ts create mode 100644 src/quizzes/quizzes.module.ts create mode 100644 src/quizzes/quizzes.resolver.ts create mode 100644 src/quizzes/quizzes.service.ts diff --git a/.compose.env b/.compose.env deleted file mode 100644 index 3188c6d..0000000 --- a/.compose.env +++ /dev/null @@ -1,6 +0,0 @@ -PORT=3000 -POSTGRES_HOST=postgres -POSTGRES_USER=postgres -POSTGRES_DB=kahoot -POSTGRES_PASSWORD=root -POSTGRES_PORT=5432 diff --git a/docker/.dockerignore b/.dockerignore similarity index 71% rename from docker/.dockerignore rename to .dockerignore index 847f49b..6edd850 100644 --- a/docker/.dockerignore +++ b/.dockerignore @@ -1,3 +1,2 @@ /node_modules npm-debug.log -.gitignore diff --git a/.env b/.env deleted file mode 100644 index 760f4f3..0000000 --- a/.env +++ /dev/null @@ -1,7 +0,0 @@ -PORT=3000 -POSTGRES_HOST=127.0.0.2 -POSTGRES_USER=postgres -POSTGRES_DB=kahoot -POSTGRES_PASSWORD=root -POSTGRES_PORT=5433 - diff --git a/.gitignore b/.gitignore index 33d9fa3..5e5939e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -.env # compiled output /dist /node_modules @@ -36,3 +35,4 @@ lerna-debug.log* !.vscode/launch.json !.vscode/extensions.json +!/.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5cc3c92 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM node:14-alpine +WORKDIR /kahoot-app +COPY package*.json ./ +RUN npm install +COPY . . +EXPOSE 3000 +CMD [ "npm", "start"] diff --git a/docker/docker-compose.yml b/docker-compose.yml similarity index 64% rename from docker/docker-compose.yml rename to docker-compose.yml index aedcdfe..a674fd5 100644 --- a/docker/docker-compose.yml +++ b/docker-compose.yml @@ -3,9 +3,11 @@ version: '3.8' services: main: container_name: kahoot-app - image: dima95/kahoot-app + build: + context: . + dockerfile: Dockerfile env_file: - - ../.compose.env + - .env volumes: - ./:/kahoot-app ports: @@ -13,18 +15,15 @@ services: command: npm run start:dev depends_on: - postgres - restart: always postgres: container_name: postgres image: postgres - environment: - - POSTGRES_PASSWORD=root - - POSTGRES_ROOT_PASSWORD=root - - POSTGRES_DATABASE=kahoot + env_file: + - .env ports: - "5433:5432" volumes: - pgdata:/var/lib/postgresql/data - restart: always volumes: pgdata: + diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index d7b9829..0000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM node:14 -WORKDIR /app -COPY package*.json ./ -RUN npm install -COPY . . -EXPOSE 3000 -CMD [ "npm", "run", "start:dev"] diff --git a/package-lock.json b/package-lock.json index acc3333..f309c75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -103,6 +103,61 @@ } } }, + "@apollo/protobufjs": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@apollo/protobufjs/-/protobufjs-1.2.2.tgz", + "integrity": "sha512-vF+zxhPiLtkwxONs6YanSt1EpwpGilThpneExUN5K3tCymuxNnVq2yojTvnpRjv2QfsEIt/n7ozPIIzBLwGIDQ==", + "requires": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.0", + "@types/node": "^10.1.0", + "long": "^4.0.0" + }, + "dependencies": { + "@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==" + } + } + }, + "@apollographql/apollo-tools": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@apollographql/apollo-tools/-/apollo-tools-0.5.2.tgz", + "integrity": "sha512-KxZiw0Us3k1d0YkJDhOpVH5rJ+mBfjXcgoRoCcslbgirjgLotKMzOcx4PZ7YTEvvEROmvG7X3Aon41GvMmyGsw==" + }, + "@apollographql/graphql-playground-html": { + "version": "1.6.29", + "resolved": "https://registry.npmjs.org/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.29.tgz", + "integrity": "sha512-xCcXpoz52rI4ksJSdOCxeOCn2DLocxwHf9dVT/Q90Pte1LX+LY+91SFtJF3KXVHH8kEin+g1KKCQPKBjZJfWNA==", + "requires": { + "xss": "^1.0.8" + } + }, + "@ardatan/aggregate-error": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@ardatan/aggregate-error/-/aggregate-error-0.0.6.tgz", + "integrity": "sha512-vyrkEHG1jrukmzTPtyWB4NLPauUw5bQeg4uhn8f+1SSynmrOcyvlb1GKQjjgoBzElLdfXCRYX8UnBlhklOHYRQ==", + "requires": { + "tslib": "~2.0.1" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + } + } + }, "@babel/code-frame": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.0.tgz", @@ -675,6 +730,103 @@ } } }, + "@graphql-tools/merge": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-6.2.5.tgz", + "integrity": "sha512-T2UEm7L5MeS1ggbGKBkdV9kTqLqSHQM13RrjPzIAYzkFL/mK837sf+oq8h2+R8B+senuHX8akUhMTcU85kcMvw==", + "requires": { + "@graphql-tools/schema": "^7.0.0", + "@graphql-tools/utils": "^7.0.0", + "tslib": "~2.0.1" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + } + } + }, + "@graphql-tools/mock": { + "version": "8.4.3", + "resolved": "https://registry.npmjs.org/@graphql-tools/mock/-/mock-8.4.3.tgz", + "integrity": "sha512-jj7obzDz4FAfmIGSh1Mo6cUs9d8MSaN6TH/iju3Qyuz6CZ6NLuJrWOg50ysEUgkT4Y/Aey8SlWOf/U15Z7qWYw==", + "requires": { + "@graphql-tools/schema": "^8.3.1", + "@graphql-tools/utils": "^8.5.1", + "fast-json-stable-stringify": "^2.1.0", + "tslib": "~2.3.0" + }, + "dependencies": { + "@graphql-tools/merge": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.2.1.tgz", + "integrity": "sha512-Q240kcUszhXiAYudjuJgNuLgy9CryDP3wp83NOZQezfA6h3ByYKU7xI6DiKrdjyVaGpYN3ppUmdj0uf5GaXzMA==", + "requires": { + "@graphql-tools/utils": "^8.5.1", + "tslib": "~2.3.0" + } + }, + "@graphql-tools/schema": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-8.3.1.tgz", + "integrity": "sha512-3R0AJFe715p4GwF067G5i0KCr/XIdvSfDLvTLEiTDQ8V/hwbOHEKHKWlEBHGRQwkG5lwFQlW1aOn7VnlPERnWQ==", + "requires": { + "@graphql-tools/merge": "^8.2.1", + "@graphql-tools/utils": "^8.5.1", + "tslib": "~2.3.0", + "value-or-promise": "1.0.11" + } + }, + "@graphql-tools/utils": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.5.3.tgz", + "integrity": "sha512-HDNGWFVa8QQkoQB0H1lftvaO1X5xUaUDk1zr1qDe0xN1NL0E/CrQdJ5UKLqOvH4hkqVUPxQsyOoAZFkaH6rLHg==", + "requires": { + "tslib": "~2.3.0" + } + }, + "value-or-promise": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.11.tgz", + "integrity": "sha512-41BrgH+dIbCFXClcSapVs5M6GkENd3gQOJpEfPDNa71LsUGMXDL0jMWpI/Rh7WhX+Aalfz2TTS3Zt5pUsbnhLg==" + } + } + }, + "@graphql-tools/schema": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-7.1.5.tgz", + "integrity": "sha512-uyn3HSNSckf4mvQSq0Q07CPaVZMNFCYEVxroApOaw802m9DcZPgf9XVPy/gda5GWj9AhbijfRYVTZQgHnJ4CXA==", + "requires": { + "@graphql-tools/utils": "^7.1.2", + "tslib": "~2.2.0", + "value-or-promise": "1.0.6" + }, + "dependencies": { + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" + } + } + }, + "@graphql-tools/utils": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-7.10.0.tgz", + "integrity": "sha512-d334r6bo9mxdSqZW6zWboEnnOOFRrAPVQJ7LkU8/6grglrbcu6WhwCLzHb90E94JI3TD3ricC3YGbUqIi9Xg0w==", + "requires": { + "@ardatan/aggregate-error": "0.0.6", + "camel-case": "4.1.2", + "tslib": "~2.2.0" + }, + "dependencies": { + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" + } + } + }, "@humanwhocodes/config-array": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.6.0.tgz", @@ -970,6 +1122,11 @@ "chalk": "^4.0.0" } }, + "@josephg/resolvable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@josephg/resolvable/-/resolvable-1.0.1.tgz", + "integrity": "sha512-CtzORUwWTTOTqfVtHaKRJ0I1kNQd1bpn3sUh8I3nJDVY+5/M/Oe1DnEWzPQvqq/xPIIkzzzIP7mfCoAjFRvDhg==" + }, "@nestjs/cli": { "version": "8.1.5", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-8.1.5.tgz", @@ -1079,6 +1236,39 @@ "uuid": "8.3.2" } }, + "@nestjs/graphql": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/graphql/-/graphql-9.1.1.tgz", + "integrity": "sha512-NHMR+VRHFeHBa8/ACy9RnyMVv/581VvXDWenpPFW5RFnZiy4aNeUDCv6sdCh/EydE4CpWzmdliuUei+NrlLWEQ==", + "requires": { + "@graphql-tools/merge": "6.2.5", + "@graphql-tools/schema": "7.1.5", + "@graphql-tools/utils": "7.10.0", + "@nestjs/mapped-types": "1.0.0", + "chokidar": "3.5.2", + "fast-glob": "3.2.7", + "graphql-ws": "5.5.1", + "iterall": "1.3.0", + "lodash": "4.17.21", + "normalize-path": "3.0.0", + "subscriptions-transport-ws": "0.9.19", + "tslib": "2.3.1", + "uuid": "8.3.2", + "ws": "8.2.3" + }, + "dependencies": { + "ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==" + } + } + }, + "@nestjs/mapped-types": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-1.0.0.tgz", + "integrity": "sha512-26AW5jHadLXtvHs+M+Agd9KZ92dDlBrmD0rORlBlvn2KvsWs4JRaKl2mUsrW7YsdZeAu3Hc4ukqyYyDdyCmMWQ==" + }, "@nestjs/platform-express": { "version": "8.2.2", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-8.2.2.tgz", @@ -1170,7 +1360,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "requires": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -1179,14 +1368,12 @@ "@nodelib/fs.stat": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==" }, "@nodelib/fs.walk": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "requires": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -1202,6 +1389,60 @@ "node-fetch": "^2.6.1" } }, + "@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" + }, + "@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "requires": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" + }, "@sinonjs/commons": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", @@ -1255,6 +1496,14 @@ "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", "dev": true }, + "@types/accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==", + "requires": { + "@types/node": "*" + } + }, "@types/babel__core": { "version": "7.1.16", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.16.tgz", @@ -1300,7 +1549,6 @@ "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", - "dev": true, "requires": { "@types/connect": "*", "@types/node": "*" @@ -1310,7 +1558,6 @@ "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", - "dev": true, "requires": { "@types/node": "*" } @@ -1321,6 +1568,11 @@ "integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==", "dev": true }, + "@types/cors": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", + "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" + }, "@types/eslint": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.2.0.tgz", @@ -1351,7 +1603,6 @@ "version": "4.17.13", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", - "dev": true, "requires": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.18", @@ -1363,7 +1614,6 @@ "version": "4.17.25", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.25.tgz", "integrity": "sha512-OUJIVfRMFijZukGGwTpKNFprqCCXk5WjNGvUgB/CxxBR40QWSjsNK86+yvGKlCOGc7sbwfHLaXhkG+NsytwBaQ==", - "dev": true, "requires": { "@types/node": "*", "@types/qs": "*", @@ -1425,17 +1675,20 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "@types/long": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" + }, "@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", - "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", - "dev": true + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" }, "@types/node": { "version": "16.11.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.9.tgz", - "integrity": "sha512-MKmdASMf3LtPzwLyRrFjtFFZ48cMf8jmX5VRYrDQiJa8Ybu5VAmkqBWqKU8fdCwD8ysw4mQ9nrEHvzg6gunR7A==", - "dev": true + "integrity": "sha512-MKmdASMf3LtPzwLyRrFjtFFZ48cMf8jmX5VRYrDQiJa8Ybu5VAmkqBWqKU8fdCwD8ysw4mQ9nrEHvzg6gunR7A==" }, "@types/parse-json": { "version": "4.0.0", @@ -1452,20 +1705,17 @@ "@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", - "dev": true + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" }, "@types/range-parser": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", - "dev": true + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" }, "@types/serve-static": { "version": "1.13.10", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", - "dev": true, "requires": { "@types/mime": "^1", "@types/node": "*" @@ -1955,12 +2205,179 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dev": true, "requires": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, + "apollo-datasource": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/apollo-datasource/-/apollo-datasource-3.3.0.tgz", + "integrity": "sha512-It8POTZTOCAnedRj2izEVeySN06LIfojigZjWaOY7voLe0DIgtvhql91xr27fuIWsR/Ew9twO3dLBjjvy34J4Q==", + "requires": { + "apollo-server-caching": "^3.3.0", + "apollo-server-env": "^4.2.0" + } + }, + "apollo-graphql": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/apollo-graphql/-/apollo-graphql-0.9.5.tgz", + "integrity": "sha512-RGt5k2JeBqrmnwRM0VOgWFiGKlGJMfmiif/4JvdaEqhMJ+xqe/9cfDYzXfn33ke2eWixsAbjEbRfy8XbaN9nTw==", + "requires": { + "core-js-pure": "^3.10.2", + "lodash.sortby": "^4.7.0", + "sha.js": "^2.4.11" + } + }, + "apollo-reporting-protobuf": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/apollo-reporting-protobuf/-/apollo-reporting-protobuf-3.2.0.tgz", + "integrity": "sha512-2v/5IRJeTGakCJo8kS2LeKUcLsgqxO/HpEyu1EaW79F0CsvrIk10tOIGxouoOgtVl5e1wfGePJ849CUWWczx2A==", + "requires": { + "@apollo/protobufjs": "1.2.2" + } + }, + "apollo-server-caching": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/apollo-server-caching/-/apollo-server-caching-3.3.0.tgz", + "integrity": "sha512-Wgcb0ArjZ5DjQ7ID+tvxUcZ7Yxdbk5l1MxZL8D8gkyjooOkhPNzjRVQ7ubPoXqO54PrOMOTm1ejVhsF+AfIirQ==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "apollo-server-core": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-3.5.0.tgz", + "integrity": "sha512-c3wEnPSnzvWvYvRJq1B+yIpa+vBvm0kq0tvD4j/IOw/F1s3sadu43Xr4FiLw++UfeLyh3aS5Wk68hjvrW1ceiQ==", + "requires": { + "@apollographql/apollo-tools": "^0.5.1", + "@apollographql/graphql-playground-html": "1.6.29", + "@graphql-tools/mock": "^8.1.2", + "@graphql-tools/schema": "^8.0.0", + "@graphql-tools/utils": "^8.0.0", + "@josephg/resolvable": "^1.0.0", + "apollo-datasource": "^3.3.0", + "apollo-graphql": "^0.9.0", + "apollo-reporting-protobuf": "^3.2.0", + "apollo-server-caching": "^3.3.0", + "apollo-server-env": "^4.2.0", + "apollo-server-errors": "^3.3.0", + "apollo-server-plugin-base": "^3.4.0", + "apollo-server-types": "^3.4.0", + "async-retry": "^1.2.1", + "fast-json-stable-stringify": "^2.1.0", + "graphql-tag": "^2.11.0", + "loglevel": "^1.6.8", + "lru-cache": "^6.0.0", + "sha.js": "^2.4.11", + "uuid": "^8.0.0" + }, + "dependencies": { + "@graphql-tools/merge": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.2.1.tgz", + "integrity": "sha512-Q240kcUszhXiAYudjuJgNuLgy9CryDP3wp83NOZQezfA6h3ByYKU7xI6DiKrdjyVaGpYN3ppUmdj0uf5GaXzMA==", + "requires": { + "@graphql-tools/utils": "^8.5.1", + "tslib": "~2.3.0" + } + }, + "@graphql-tools/schema": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-8.3.1.tgz", + "integrity": "sha512-3R0AJFe715p4GwF067G5i0KCr/XIdvSfDLvTLEiTDQ8V/hwbOHEKHKWlEBHGRQwkG5lwFQlW1aOn7VnlPERnWQ==", + "requires": { + "@graphql-tools/merge": "^8.2.1", + "@graphql-tools/utils": "^8.5.1", + "tslib": "~2.3.0", + "value-or-promise": "1.0.11" + } + }, + "@graphql-tools/utils": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.5.3.tgz", + "integrity": "sha512-HDNGWFVa8QQkoQB0H1lftvaO1X5xUaUDk1zr1qDe0xN1NL0E/CrQdJ5UKLqOvH4hkqVUPxQsyOoAZFkaH6rLHg==", + "requires": { + "tslib": "~2.3.0" + } + }, + "value-or-promise": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.11.tgz", + "integrity": "sha512-41BrgH+dIbCFXClcSapVs5M6GkENd3gQOJpEfPDNa71LsUGMXDL0jMWpI/Rh7WhX+Aalfz2TTS3Zt5pUsbnhLg==" + } + } + }, + "apollo-server-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/apollo-server-env/-/apollo-server-env-4.2.0.tgz", + "integrity": "sha512-4xJ+PCoWsFLj4rU6iXrIhqD7nI42goi4Iqrhsof9680ljSzkzd+PCwZsja3mHOFXKUQQUvJ7StVSgwaiRu45+A==", + "requires": { + "node-fetch": "^2.6.1" + } + }, + "apollo-server-errors": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/apollo-server-errors/-/apollo-server-errors-3.3.0.tgz", + "integrity": "sha512-9/MNlPZBbEjcCdJcUSbKbVEBT9xZS8GSpX7T/TyzcxHSbsXJszSDSipQNGC+PRKTKAUnv61IONScVyLKEZ5XEQ==" + }, + "apollo-server-express": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-3.5.0.tgz", + "integrity": "sha512-eFyBC4ate/g5GrvxM+HrtiElxCEbvG+CiJ0/R1i62L+wzXDhgD6MU0SW17ceS1mpBJgDxURu/VS5hUSNyWMa3Q==", + "requires": { + "@types/accepts": "^1.3.5", + "@types/body-parser": "1.19.1", + "@types/cors": "2.8.12", + "@types/express": "4.17.13", + "@types/express-serve-static-core": "4.17.24", + "accepts": "^1.3.5", + "apollo-server-core": "^3.5.0", + "apollo-server-types": "^3.4.0", + "body-parser": "^1.19.0", + "cors": "^2.8.5", + "parseurl": "^1.3.3" + }, + "dependencies": { + "@types/body-parser": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.1.tgz", + "integrity": "sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz", + "integrity": "sha512-3UJuW+Qxhzwjq3xhwXm2onQcFHn76frIYVbTu+kn24LFxI+dEhdfISDFovPB8VpEgW8oQCTpRuCe+0zJxB7NEA==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + } + } + }, + "apollo-server-plugin-base": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/apollo-server-plugin-base/-/apollo-server-plugin-base-3.4.0.tgz", + "integrity": "sha512-Z9musk7Z/1v+Db6aOoxcHfmsgej2yEBzBz5kVGOc81/XAtdv6bjasKSLC3RiySAUzWSLBJRUeEGIEVhhk/j2Zg==", + "requires": { + "apollo-server-types": "^3.4.0" + } + }, + "apollo-server-types": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/apollo-server-types/-/apollo-server-types-3.4.0.tgz", + "integrity": "sha512-iFNRENtxDoFWoY+KxpGP+TYyRnqUPqUTubMJVgiXPDvOPFL8dzqGGmqq1g/VCeWFHRJTPBLWhOfQU7ktwDEjnQ==", + "requires": { + "apollo-reporting-protobuf": "^3.2.0", + "apollo-server-caching": "^3.3.0", + "apollo-server-env": "^4.2.0" + } + }, "app-root-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.0.0.tgz", @@ -1993,6 +2410,14 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, + "async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "requires": { + "retry": "0.13.1" + } + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2105,6 +2530,11 @@ "babel-preset-current-node-syntax": "^1.0.0" } }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2118,8 +2548,7 @@ "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" }, "bl": { "version": "4.1.0", @@ -2196,7 +2625,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, "requires": { "fill-range": "^7.0.1" } @@ -2288,6 +2716,15 @@ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true }, + "camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "requires": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -2325,7 +2762,6 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", - "dev": true, "requires": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -2355,6 +2791,15 @@ "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", "dev": true }, + "class-validator": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.13.2.tgz", + "integrity": "sha512-yBUcQy07FPlGzUjoLuUfIOXzgynnQPPruyK1Ge2B74k9ROwnle1E+NxLWnUv5OLU8hA/qL5leAE9XnXq3byaBw==", + "requires": { + "libphonenumber-js": "^1.9.43", + "validator": "^13.7.0" + } + }, "cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -2564,6 +3009,11 @@ "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==", "dev": true }, + "core-js-pure": { + "version": "3.19.1", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.19.1.tgz", + "integrity": "sha512-Q0Knr8Es84vtv62ei6/6jXH/7izKmOrtrxH9WJTHLCMAVeU+8TF8z8Nr08CsH4Ot0oJKzBzJJL9SJBYIv7WlfQ==" + }, "core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -2608,6 +3058,11 @@ "which": "^2.0.1" } }, + "cssfilter": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", + "integrity": "sha1-xtJnJjKi5cg+AT5oZKQs6N79IK4=" + }, "cssom": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", @@ -3194,6 +3649,11 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, + "eventemitter3": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" + }, "events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -3316,7 +3776,6 @@ "version": "3.2.7", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", - "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -3328,8 +3787,7 @@ "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "fast-levenshtein": { "version": "2.0.6", @@ -3346,7 +3804,6 @@ "version": "1.13.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", - "dev": true, "requires": { "reusify": "^1.0.4" } @@ -3382,7 +3839,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, "requires": { "to-regex-range": "^5.0.1" } @@ -3520,7 +3976,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "optional": true }, "function-bind": { @@ -3589,7 +4044,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "requires": { "is-glob": "^4.0.1" } @@ -3637,6 +4091,24 @@ "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", "dev": true }, + "graphql": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.7.2.tgz", + "integrity": "sha512-AnnKk7hFQFmU/2I9YSQf3xw44ctnSFCfp3zE0N6W174gqe9fWG/2rKaKxROK7CcI3XtERpjEKFqts8o319Kf7A==" + }, + "graphql-tag": { + "version": "2.12.6", + "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", + "integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==", + "requires": { + "tslib": "^2.1.0" + } + }, + "graphql-ws": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.5.1.tgz", + "integrity": "sha512-QP6OvCnqkdazSoZ6QboWVkCvEmDCOe6u41MzuhkCkYg+jZZbYafUlt3uC/7mvKLrlIyR44QgHOMALqBgg+Ab/w==" + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -3868,7 +4340,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "requires": { "binary-extensions": "^2.0.0" } @@ -3885,8 +4356,7 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" }, "is-fullwidth-code-point": { "version": "3.0.0", @@ -3903,7 +4373,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "requires": { "is-extglob": "^2.1.1" } @@ -3917,8 +4386,7 @@ "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" }, "is-potential-custom-element-name": { "version": "1.0.1", @@ -4036,6 +4504,11 @@ "istanbul-lib-report": "^3.0.0" } }, + "iterall": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz", + "integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==" + }, "iterare": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", @@ -4748,6 +5221,11 @@ "type-check": "~0.4.0" } }, + "libphonenumber-js": { + "version": "1.9.43", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.9.43.tgz", + "integrity": "sha512-tNB87ZutAiAkl3DE/Bo0Mxqn/XZbNxhPg4v9bYBwQQW4dlhBGqXl1vtmPxeDWbrijzwOA9vRjOOFm5V9SK/W3w==" + }, "lines-and-columns": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", @@ -4772,8 +5250,7 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash.get": { "version": "4.4.2", @@ -4802,6 +5279,11 @@ "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=" }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" + }, "log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -4812,11 +5294,28 @@ "is-unicode-supported": "^0.1.0" } }, + "loglevel": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.0.tgz", + "integrity": "sha512-G6A/nJLRgWOuuwdNuA6koovfEV1YpqqAG4pRUlFaz3jj2QNZ8M4vBqnVA+HBTmU/AMNUtlOsMmSpF6NyOjztbA==" + }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, + "lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "requires": { + "tslib": "^2.0.3" + } + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "requires": { "yallist": "^4.0.0" } @@ -4896,8 +5395,7 @@ "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" }, "methods": { "version": "1.1.2", @@ -4908,7 +5406,6 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, "requires": { "braces": "^3.0.1", "picomatch": "^2.2.3" @@ -5012,6 +5509,15 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "requires": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, "node-emoji": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", @@ -5050,8 +5556,7 @@ "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" }, "npm-run-path": { "version": "4.0.1", @@ -5230,6 +5735,15 @@ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5327,8 +5841,7 @@ "picomatch": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", - "dev": true + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==" }, "pirates": { "version": "4.0.1", @@ -5478,8 +5991,7 @@ "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" }, "randombytes": { "version": "2.1.0", @@ -5527,7 +6039,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, "requires": { "picomatch": "^2.2.1" } @@ -5612,11 +6123,15 @@ "signal-exit": "^3.0.2" } }, + "retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==" + }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" }, "rimraf": { "version": "3.0.2", @@ -5636,7 +6151,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "requires": { "queue-microtask": "^1.2.2" } @@ -5983,6 +6497,25 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, + "subscriptions-transport-ws": { + "version": "0.9.19", + "resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.19.tgz", + "integrity": "sha512-dxdemxFFB0ppCLg10FTtRqH/31FNRL1y1BQv8209MK5I4CwALb7iihQg+7p65lFcIl8MHatINWBLOqpgU4Kyyw==", + "requires": { + "backo2": "^1.0.2", + "eventemitter3": "^3.1.0", + "iterall": "^1.2.1", + "symbol-observable": "^1.0.4", + "ws": "^5.2.0 || ^6.0.0 || ^7.0.0" + }, + "dependencies": { + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + } + } + }, "superagent": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/superagent/-/superagent-6.1.0.tgz", @@ -6255,7 +6788,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "requires": { "is-number": "^7.0.0" } @@ -6575,6 +7107,16 @@ "source-map": "^0.7.3" } }, + "validator": { + "version": "13.7.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", + "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==" + }, + "value-or-promise": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.6.tgz", + "integrity": "sha512-9r0wQsWD8z/BxPOvnwbPf05ZvFngXyouE9EKB+5GbYix+BYnAwrIChCUyFIinfbf2FL/U71z+CPpbnmTdxrwBg==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -6790,8 +7332,7 @@ "ws": { "version": "7.5.5", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz", - "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==", - "dev": true + "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==" }, "xml-name-validator": { "version": "3.0.0", @@ -6819,6 +7360,22 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, + "xss": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.10.tgz", + "integrity": "sha512-qmoqrRksmzqSKvgqzN0055UFWY7OKx1/9JWeRswwEVX9fCG5jcYRxa/A2DHcmZX6VJvjzHRQ2STeeVcQkrmLSw==", + "requires": { + "commander": "^2.20.3", + "cssfilter": "0.0.10" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + } + } + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -6832,8 +7389,7 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yaml": { "version": "1.10.2", diff --git a/package.json b/package.json index 298ad4d..8a14835 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,12 @@ "@nestjs/common": "^8.0.0", "@nestjs/config": "^1.1.0", "@nestjs/core": "^8.0.0", + "@nestjs/graphql": "^9.1.1", "@nestjs/platform-express": "^8.0.0", "@nestjs/typeorm": "^8.0.2", + "apollo-server-express": "^3.5.0", + "class-validator": "^0.13.2", + "graphql": "^15.7.2", "pg": "^8.7.1", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", diff --git a/schema.gql b/schema.gql new file mode 100644 index 0000000..6780fcc --- /dev/null +++ b/schema.gql @@ -0,0 +1,53 @@ +# ------------------------------------------------------ +# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) +# ------------------------------------------------------ + +"""answer """ +type Answer { + id: ID! + text: String! + imgURL: String + isCorrect: Boolean! + question: Question! +} + +"""question """ +type Question { + id: ID! + content: String! + imgUrl: String + quiz: Quiz! + answers: [Answer!]! +} + +"""quiz """ +type Quiz { + id: ID! + name: String! + questions: [Question!]! +} + +type Query { + quizzes: [Quiz!]! +} + +type Mutation { + addQuiz(newQuizData: NewQuizInput!): Quiz! +} + +input NewQuizInput { + name: String! + questions: [NewQuestionInput!]! +} + +input NewQuestionInput { + content: String! + imgURL: String = null + answers: [NewAnswerInput!]! +} + +input NewAnswerInput { + text: String! + imgURL: String + isCorrect: Boolean = false +} diff --git a/src/answers/answers.module.ts b/src/answers/answers.module.ts new file mode 100644 index 0000000..da34a5a --- /dev/null +++ b/src/answers/answers.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { AnswersService } from './answers.service'; +import { AnswersResolver } from './answers.resolver'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Answer } from './models/answer.model'; + +@Module({ + providers: [AnswersService, AnswersResolver], + imports: [TypeOrmModule.forFeature([Answer])], + exports: [AnswersService], +}) +export class AnswersModule {} diff --git a/src/answers/answers.resolver.ts b/src/answers/answers.resolver.ts new file mode 100644 index 0000000..f0dba46 --- /dev/null +++ b/src/answers/answers.resolver.ts @@ -0,0 +1,7 @@ +import { Resolver } from '@nestjs/graphql'; +import { AnswersService } from './answers.service'; + +@Resolver() +export class AnswersResolver { + constructor(private readonly answersService: AnswersService) {} +} diff --git a/src/answers/answers.service.ts b/src/answers/answers.service.ts new file mode 100644 index 0000000..f5d3ab2 --- /dev/null +++ b/src/answers/answers.service.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Answer } from './models/answer.model'; +import { NewAnswerInput } from './dto/new-answer.input'; + +@Injectable() +export class AnswersService { + constructor( + @InjectRepository(Answer) + private readonly answersRepository: Repository, + ) {} + + async createAnswer(data: NewAnswerInput[]) { + return await this.answersRepository.save(data); + } +} diff --git a/src/answers/dto/new-answer.input.ts b/src/answers/dto/new-answer.input.ts new file mode 100644 index 0000000..2908785 --- /dev/null +++ b/src/answers/dto/new-answer.input.ts @@ -0,0 +1,16 @@ +import { Field, InputType } from '@nestjs/graphql'; +import { IsOptional, Length } from 'class-validator'; + +@InputType() +export class NewAnswerInput { + @Field({ nullable: false }) + @Length(1, 30) + text: string; + + @Field({ nullable: true }) + @IsOptional() + imgURL?: string; + + @Field({ nullable: false, defaultValue: false }) + isCorrect: boolean; +} diff --git a/src/answers/models/answer.model.ts b/src/answers/models/answer.model.ts new file mode 100644 index 0000000..340fadf --- /dev/null +++ b/src/answers/models/answer.model.ts @@ -0,0 +1,36 @@ +import { Field, ID, ObjectType } from '@nestjs/graphql'; +import { + Column, + Entity, + Generated, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { Question } from '../../questions/models/question.model'; +import { IsOptional } from 'class-validator'; + +@ObjectType({ description: 'answer ' }) +@Entity() +export class Answer { + @Field((type) => ID) + @PrimaryGeneratedColumn('uuid') + @Generated('uuid') + id: string; + + @Field() + @Column() + text: string; + + @Field({ nullable: true, defaultValue: null }) + @IsOptional() + @Column({ nullable: true }) + imgURL?: string | null; + + @Field({ nullable: false }) + @Column({ nullable: false }) + isCorrect: boolean; + + @Field((type) => Question, { nullable: false }) + @ManyToOne(() => Question, (question) => question.answers) + question: Question; +} diff --git a/src/app.controller.spec.ts b/src/app.controller.spec.ts deleted file mode 100644 index d22f389..0000000 --- a/src/app.controller.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; - -describe('AppController', () => { - let appController: AppController; - - beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [AppService], - }).compile(); - - appController = app.get(AppController); - }); - - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); - }); - }); -}); diff --git a/src/app.controller.ts b/src/app.controller.ts deleted file mode 100644 index cce879e..0000000 --- a/src/app.controller.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { AppService } from './app.service'; - -@Controller() -export class AppController { - constructor(private readonly appService: AppService) {} - - @Get() - getHello(): string { - return this.appService.getHello(); - } -} diff --git a/src/app.module.ts b/src/app.module.ts index 8719927..35f80d9 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,26 +1,35 @@ import { Module } from '@nestjs/common'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule } from '@nestjs/config'; +import { GraphQLModule } from '@nestjs/graphql'; +import { QuizzesModule } from './quizzes/quizzes.module'; +import { Quiz } from './quizzes/models/quiz.model'; +import { QuestionsModule } from './questions/questions.module'; +import { AnswersModule } from './answers/answers.module'; +import { Question } from './questions/models/question.model'; +import { Answer } from './answers/models/answer.model'; @Module({ imports: [ + GraphQLModule.forRoot({ + autoSchemaFile: 'schema.gql', + }), ConfigModule.forRoot({ envFilePath: '.env', }), TypeOrmModule.forRoot({ type: 'postgres', - host: process.env.POSTGRES_HOST, - port: Number(process.env.POSTGRES_PORT), - username: process.env.POSTGRES_USER, - password: process.env.POSTGRES_PASSWORD, - database: process.env.POSTGRES_DB, - entities: [], + host: process.env.POSTGRES_HOST || 'postgres', + port: Number(process.env.POSTGRES_PORT) || 5433, + username: process.env.POSTGRES_USER || 'postgres', + password: process.env.POSTGRES_PASSWORD || 'root', + database: process.env.POSTGRES_DB || 'kahoot', + entities: [Quiz, Question, Answer], synchronize: true, }), + QuizzesModule, + QuestionsModule, + AnswersModule, ], - controllers: [AppController], - providers: [AppService], }) export class AppModule {} diff --git a/src/app.service.ts b/src/app.service.ts deleted file mode 100644 index 927d7cc..0000000 --- a/src/app.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class AppService { - getHello(): string { - return 'Hello World!'; - } -} diff --git a/src/questions/dto/new-question.input.ts b/src/questions/dto/new-question.input.ts new file mode 100644 index 0000000..505817e --- /dev/null +++ b/src/questions/dto/new-question.input.ts @@ -0,0 +1,28 @@ +import { Field, InputType } from '@nestjs/graphql'; +import { + ArrayMaxSize, + ArrayMinSize, + IsArray, + IsOptional, + Length, + MinLength, +} from 'class-validator'; +import { NewAnswerInput } from '../../answers/dto/new-answer.input'; + +@InputType() +export class NewQuestionInput { + @Field({ nullable: false }) + @Length(2, 30) + content: string; + + @Field({ nullable: true, defaultValue: null }) + @IsOptional() + @MinLength(5) + imgURL?: string; + + @Field((type) => [NewAnswerInput], { nullable: false }) + @IsArray() + @ArrayMinSize(2) + @ArrayMaxSize(8) + answers: NewAnswerInput[]; +} diff --git a/src/questions/models/question.model.ts b/src/questions/models/question.model.ts new file mode 100644 index 0000000..f1a840a --- /dev/null +++ b/src/questions/models/question.model.ts @@ -0,0 +1,36 @@ +import { Field, ID, ObjectType } from '@nestjs/graphql'; +import { + Column, + Entity, + Generated, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { Quiz } from '../../quizzes/models/quiz.model'; +import { Answer } from '../../answers/models/answer.model'; + +@ObjectType({ description: 'question ' }) +@Entity() +export class Question { + @Field((type) => ID) + @PrimaryGeneratedColumn('uuid') + @Generated('uuid') + id: string; + + @Field({ nullable: false }) + @Column() + content: string; + + @Field({ nullable: true }) + @Column({ nullable: true }) + imgUrl?: string | null; + + @Field((type) => Quiz) + @ManyToOne(() => Quiz, (quiz) => quiz.questions) + quiz: Quiz; + + @Field((type) => [Answer]) + @OneToMany(() => Answer, (answer) => answer.question) + answers: Answer[]; +} diff --git a/src/questions/questions.module.ts b/src/questions/questions.module.ts new file mode 100644 index 0000000..4b6296b --- /dev/null +++ b/src/questions/questions.module.ts @@ -0,0 +1,16 @@ +import { forwardRef, Module } from '@nestjs/common'; +import { QuestionsService } from './questions.service'; +import { QuestionsResolver } from './questions.resolver'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Question } from './models/question.model'; +import { AnswersModule } from '../answers/answers.module'; + +@Module({ + providers: [QuestionsService, QuestionsResolver], + imports: [ + TypeOrmModule.forFeature([Question]), + forwardRef(() => AnswersModule), + ], + exports: [QuestionsService], +}) +export class QuestionsModule {} diff --git a/src/questions/questions.resolver.ts b/src/questions/questions.resolver.ts new file mode 100644 index 0000000..fbc65ca --- /dev/null +++ b/src/questions/questions.resolver.ts @@ -0,0 +1,22 @@ +import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; +import { Question } from './models/question.model'; +import { QuestionsService } from './questions.service'; +import { NewQuestionInput } from './dto/new-question.input'; + +@Resolver((of: Question) => Question) +export class QuestionsResolver { + constructor(private readonly questionsService: QuestionsService) {} + + // @Query((returns) => Question) + // async getQuestionById(@Args('id') id: string): Promise { + // const res: Question = await this.questionsService.getById(id); + // return res; + // } + + // @Mutation((returns) => Question) + // async addQuestion( + // @Args('newQuestionData') newQuestionData: NewQuestionInput, + // ): Promise { + // return await this.questionsService.createNewQuestion(newQuestionData); + // } +} diff --git a/src/questions/questions.service.ts b/src/questions/questions.service.ts new file mode 100644 index 0000000..fbd7eab --- /dev/null +++ b/src/questions/questions.service.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Question } from './models/question.model'; +import { Repository } from 'typeorm'; +import { NewQuestionInput } from './dto/new-question.input'; +import { AnswersService } from '../answers/answers.service'; + +@Injectable() +export class QuestionsService { + constructor( + @InjectRepository(Question) + private readonly questionRepository: Repository, + private readonly answerService: AnswersService, + ) {} + + // async getById(id: string): Promise { + // return await this.questionRepository.findOne({ + // where: id, + // }); + // } + + async createNewQuestion(data: NewQuestionInput[]) { + for (const elem of data) { + await this.answerService.createAnswer(elem.answers); + } + return await this.questionRepository.save(data); + } +} diff --git a/src/quizzes/dto/new-quiz.input.ts b/src/quizzes/dto/new-quiz.input.ts new file mode 100644 index 0000000..6e5bac3 --- /dev/null +++ b/src/quizzes/dto/new-quiz.input.ts @@ -0,0 +1,15 @@ +import { Field, InputType } from '@nestjs/graphql'; +import { ArrayMinSize, IsArray, Length } from 'class-validator'; +import { NewQuestionInput } from '../../questions/dto/new-question.input'; + +@InputType() +export class NewQuizInput { + @Field({ nullable: false }) + @Length(2, 30) + name: string; + + @Field((type) => [NewQuestionInput], { nullable: false }) + @IsArray() + @ArrayMinSize(1) + questions: NewQuestionInput[]; +} diff --git a/src/quizzes/models/quiz.model.ts b/src/quizzes/models/quiz.model.ts new file mode 100644 index 0000000..5d0645f --- /dev/null +++ b/src/quizzes/models/quiz.model.ts @@ -0,0 +1,26 @@ +import { Field, ID, ObjectType } from '@nestjs/graphql'; +import { + Column, + Entity, + Generated, + OneToMany, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { Question } from '../../questions/models/question.model'; + +@ObjectType({ description: 'quiz ' }) +@Entity() +export class Quiz { + @Field((type) => ID) + @PrimaryGeneratedColumn('uuid') + @Generated('uuid') + id: string; + + @Field((type) => String) + @Column() + name: string; + + @Field((type) => [Question]) + @OneToMany(() => Question, (question) => question.quiz) + questions: Question[]; +} diff --git a/src/quizzes/quizzes.module.ts b/src/quizzes/quizzes.module.ts new file mode 100644 index 0000000..eb71a4e --- /dev/null +++ b/src/quizzes/quizzes.module.ts @@ -0,0 +1,15 @@ +import { forwardRef, Module } from '@nestjs/common'; +import { QuizzesService } from './quizzes.service'; +import { QuizzesResolver } from './quizzes.resolver'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Quiz } from './models/quiz.model'; +import { QuestionsModule } from '../questions/questions.module'; + +@Module({ + providers: [QuizzesService, QuizzesResolver], + imports: [ + TypeOrmModule.forFeature([Quiz]), + forwardRef(() => QuestionsModule), + ], +}) +export class QuizzesModule {} diff --git a/src/quizzes/quizzes.resolver.ts b/src/quizzes/quizzes.resolver.ts new file mode 100644 index 0000000..b68229c --- /dev/null +++ b/src/quizzes/quizzes.resolver.ts @@ -0,0 +1,19 @@ +import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; +import { Quiz } from './models/quiz.model'; +import { QuizzesService } from './quizzes.service'; +import { NewQuizInput } from './dto/new-quiz.input'; + +@Resolver((of: Quiz) => Quiz) +export class QuizzesResolver { + constructor(private readonly quizzesService: QuizzesService) {} + + @Query((returns) => [Quiz]) + async quizzes(): Promise { + return await this.quizzesService.findAll(); + } + + @Mutation((returns) => Quiz) + async addQuiz(@Args('newQuizData') newQuizData: NewQuizInput) { + return await this.quizzesService.addQuiz(newQuizData); + } +} diff --git a/src/quizzes/quizzes.service.ts b/src/quizzes/quizzes.service.ts new file mode 100644 index 0000000..27a239c --- /dev/null +++ b/src/quizzes/quizzes.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@nestjs/common'; +import { Repository } from 'typeorm'; +import { Quiz } from './models/quiz.model'; +import { NewQuizInput } from './dto/new-quiz.input'; +import { InjectRepository } from '@nestjs/typeorm'; +import { QuestionsService } from '../questions/questions.service'; + +@Injectable() +export class QuizzesService { + constructor( + @InjectRepository(Quiz) + private readonly quizRepository: Repository, + private readonly questionsService: QuestionsService, + ) {} + + async findAll(): Promise { + return await this.quizRepository.find({ + relations: ['questions', 'questions.answers'], + }); + } + + async addQuiz(data: NewQuizInput) { + await this.questionsService.createNewQuestion(data.questions); + return await this.quizRepository.save(data); + } +} -- GitLab From 1000c37d70f161ef692cd00fc80642a898207f74 Mon Sep 17 00:00:00 2001 From: Dima-Mitrich Date: Mon, 29 Nov 2021 23:54:34 +0300 Subject: [PATCH 2/9] TR-12: added query for getting question by id and mutation for deleting quizzes --- schema.gql | 8 ++++++++ src/answers/models/answer.model.ts | 4 +++- src/app.module.ts | 2 +- src/common/models/status.model.ts | 7 +++++++ src/questions/models/question.model.ts | 8 ++++++-- src/questions/questions.resolver.ts | 19 +++++-------------- src/questions/questions.service.ts | 8 +++----- src/quizzes/models/quiz.model.ts | 4 +++- src/quizzes/quizzes.resolver.ts | 12 ++++++++++++ src/quizzes/quizzes.service.ts | 13 ++++++++++++- 10 files changed, 60 insertions(+), 25 deletions(-) create mode 100644 src/common/models/status.model.ts diff --git a/schema.gql b/schema.gql index 6780fcc..5b31cd5 100644 --- a/schema.gql +++ b/schema.gql @@ -27,11 +27,19 @@ type Quiz { questions: [Question!]! } +"""deleting status""" +type StatusModel { + success: Boolean! +} + type Query { quizzes: [Quiz!]! + getQuizById(id: String!): Quiz! + getQuestionById(id: String!): Question! } type Mutation { + deleteQuizById(id: String!): StatusModel! addQuiz(newQuizData: NewQuizInput!): Quiz! } diff --git a/src/answers/models/answer.model.ts b/src/answers/models/answer.model.ts index 340fadf..febdbca 100644 --- a/src/answers/models/answer.model.ts +++ b/src/answers/models/answer.model.ts @@ -31,6 +31,8 @@ export class Answer { isCorrect: boolean; @Field((type) => Question, { nullable: false }) - @ManyToOne(() => Question, (question) => question.answers) + @ManyToOne(() => Question, (question) => question.answers, { + onDelete: 'CASCADE', + }) question: Question; } diff --git a/src/app.module.ts b/src/app.module.ts index 35f80d9..75843f1 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -19,7 +19,7 @@ import { Answer } from './answers/models/answer.model'; }), TypeOrmModule.forRoot({ type: 'postgres', - host: process.env.POSTGRES_HOST || 'postgres', + host: process.env.POSTGRES_HOST || '127.0.0.1', port: Number(process.env.POSTGRES_PORT) || 5433, username: process.env.POSTGRES_USER || 'postgres', password: process.env.POSTGRES_PASSWORD || 'root', diff --git a/src/common/models/status.model.ts b/src/common/models/status.model.ts new file mode 100644 index 0000000..6bcdd3b --- /dev/null +++ b/src/common/models/status.model.ts @@ -0,0 +1,7 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +@ObjectType({ description: 'deleting status' }) +export class StatusModel { + @Field({ nullable: false }) + success: boolean; +} diff --git a/src/questions/models/question.model.ts b/src/questions/models/question.model.ts index f1a840a..5637efe 100644 --- a/src/questions/models/question.model.ts +++ b/src/questions/models/question.model.ts @@ -27,10 +27,14 @@ export class Question { imgUrl?: string | null; @Field((type) => Quiz) - @ManyToOne(() => Quiz, (quiz) => quiz.questions) + @ManyToOne(() => Quiz, (quiz) => quiz.questions, { + onDelete: 'CASCADE', + }) quiz: Quiz; @Field((type) => [Answer]) - @OneToMany(() => Answer, (answer) => answer.question) + @OneToMany(() => Answer, (answer) => answer.question, { + onDelete: 'CASCADE', + }) answers: Answer[]; } diff --git a/src/questions/questions.resolver.ts b/src/questions/questions.resolver.ts index fbc65ca..e76d7ec 100644 --- a/src/questions/questions.resolver.ts +++ b/src/questions/questions.resolver.ts @@ -1,22 +1,13 @@ -import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; +import { Args, Query, Resolver } from '@nestjs/graphql'; import { Question } from './models/question.model'; import { QuestionsService } from './questions.service'; -import { NewQuestionInput } from './dto/new-question.input'; @Resolver((of: Question) => Question) export class QuestionsResolver { constructor(private readonly questionsService: QuestionsService) {} - // @Query((returns) => Question) - // async getQuestionById(@Args('id') id: string): Promise { - // const res: Question = await this.questionsService.getById(id); - // return res; - // } - - // @Mutation((returns) => Question) - // async addQuestion( - // @Args('newQuestionData') newQuestionData: NewQuestionInput, - // ): Promise { - // return await this.questionsService.createNewQuestion(newQuestionData); - // } + @Query((returns) => Question) + async getQuestionById(@Args('id') id: string): Promise { + return await this.questionsService.getById(id); + } } diff --git a/src/questions/questions.service.ts b/src/questions/questions.service.ts index fbd7eab..728b086 100644 --- a/src/questions/questions.service.ts +++ b/src/questions/questions.service.ts @@ -13,11 +13,9 @@ export class QuestionsService { private readonly answerService: AnswersService, ) {} - // async getById(id: string): Promise { - // return await this.questionRepository.findOne({ - // where: id, - // }); - // } + async getById(id: string): Promise { + return await this.questionRepository.findOne(id); + } async createNewQuestion(data: NewQuestionInput[]) { for (const elem of data) { diff --git a/src/quizzes/models/quiz.model.ts b/src/quizzes/models/quiz.model.ts index 5d0645f..22cbf1e 100644 --- a/src/quizzes/models/quiz.model.ts +++ b/src/quizzes/models/quiz.model.ts @@ -21,6 +21,8 @@ export class Quiz { name: string; @Field((type) => [Question]) - @OneToMany(() => Question, (question) => question.quiz) + @OneToMany(() => Question, (question) => question.quiz, { + onDelete: 'CASCADE', + }) questions: Question[]; } diff --git a/src/quizzes/quizzes.resolver.ts b/src/quizzes/quizzes.resolver.ts index b68229c..175ca85 100644 --- a/src/quizzes/quizzes.resolver.ts +++ b/src/quizzes/quizzes.resolver.ts @@ -2,6 +2,7 @@ import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { Quiz } from './models/quiz.model'; import { QuizzesService } from './quizzes.service'; import { NewQuizInput } from './dto/new-quiz.input'; +import { StatusModel } from '../common/models/status.model'; @Resolver((of: Quiz) => Quiz) export class QuizzesResolver { @@ -12,6 +13,17 @@ export class QuizzesResolver { return await this.quizzesService.findAll(); } + @Query((returns) => Quiz) + async getQuizById(@Args('id') id: string) { + return await this.quizzesService.getQuizById(id); + } + + @Mutation((returns) => StatusModel) + async deleteQuizById(@Args('id') id: string) { + const res = await this.quizzesService.deleteQuizById(id); + return { success: res }; + } + @Mutation((returns) => Quiz) async addQuiz(@Args('newQuizData') newQuizData: NewQuizInput) { return await this.quizzesService.addQuiz(newQuizData); diff --git a/src/quizzes/quizzes.service.ts b/src/quizzes/quizzes.service.ts index 27a239c..261f121 100644 --- a/src/quizzes/quizzes.service.ts +++ b/src/quizzes/quizzes.service.ts @@ -19,8 +19,19 @@ export class QuizzesService { }); } - async addQuiz(data: NewQuizInput) { + async addQuiz(data: NewQuizInput): Promise { await this.questionsService.createNewQuestion(data.questions); return await this.quizRepository.save(data); } + + async getQuizById(id: string): Promise { + return await this.quizRepository.findOne(id, { + relations: ['questions', 'questions.answers'], + }); + } + + async deleteQuizById(id: string): Promise { + const { affected } = await this.quizRepository.delete(id); + return !!affected; + } } -- GitLab From 2e3474b9999350b22e2035413555e38f93a50bf1 Mon Sep 17 00:00:00 2001 From: Dima-Mitrich Date: Tue, 30 Nov 2021 14:26:06 +0300 Subject: [PATCH 3/9] TR-12: added Read, Update, Delete methods for Quiz, Question and Answer --- schema.gql | 34 +++++++++++++++---- src/answers/answers.resolver.ts | 18 +++++++++- src/answers/answers.service.ts | 16 +++++++-- ...answer.input.ts => create-answer.input.ts} | 2 +- src/answers/dto/update-answer.input.ts | 17 ++++++++++ ...tion.input.ts => create-question.input.ts} | 8 ++--- src/questions/dto/update-question.input.ts | 18 ++++++++++ src/questions/questions.resolver.ts | 17 +++++++++- src/questions/questions.service.ts | 20 +++++++++-- ...new-quiz.input.ts => create-quiz.input.ts} | 8 ++--- src/quizzes/dto/update-quiz.input.ts | 13 +++++++ src/quizzes/quizzes.resolver.ts | 12 +++++-- src/quizzes/quizzes.service.ts | 11 ++++-- to-ask | 4 +++ 14 files changed, 172 insertions(+), 26 deletions(-) rename src/answers/dto/{new-answer.input.ts => create-answer.input.ts} (90%) create mode 100644 src/answers/dto/update-answer.input.ts rename src/questions/dto/{new-question.input.ts => create-question.input.ts} (66%) create mode 100644 src/questions/dto/update-question.input.ts rename src/quizzes/dto/{new-quiz.input.ts => create-quiz.input.ts} (52%) create mode 100644 src/quizzes/dto/update-quiz.input.ts create mode 100644 to-ask diff --git a/schema.gql b/schema.gql index 5b31cd5..0c05308 100644 --- a/schema.gql +++ b/schema.gql @@ -40,22 +40,44 @@ type Query { type Mutation { deleteQuizById(id: String!): StatusModel! - addQuiz(newQuizData: NewQuizInput!): Quiz! + addQuiz(newQuizData: CreateQuizInput!): Quiz! + updateQuizById(updateQuizData: UpdateQuizInput!): Quiz! + deleteQuestionById(id: String!): StatusModel! + updateQuestionById(updateQuestionData: UpdateQuestionInput!): Question! + deleteAnswerById(id: String!): StatusModel! + updateAnswerById(updateAnsData: UpdateAnswerInput!): Answer! } -input NewQuizInput { +input CreateQuizInput { name: String! - questions: [NewQuestionInput!]! + questions: [CreateQuestionInput!]! } -input NewQuestionInput { +input CreateQuestionInput { content: String! imgURL: String = null - answers: [NewAnswerInput!]! + answers: [CreateAnswerInput!]! } -input NewAnswerInput { +input CreateAnswerInput { text: String! imgURL: String isCorrect: Boolean = false } + +input UpdateQuizInput { + id: String! + name: String! +} + +input UpdateQuestionInput { + id: String! + content: String! + imgURL: String = null +} + +input UpdateAnswerInput { + id: String! + text: String! + imgURL: String +} diff --git a/src/answers/answers.resolver.ts b/src/answers/answers.resolver.ts index f0dba46..155aafb 100644 --- a/src/answers/answers.resolver.ts +++ b/src/answers/answers.resolver.ts @@ -1,7 +1,23 @@ -import { Resolver } from '@nestjs/graphql'; +import { Args, Mutation, Resolver } from '@nestjs/graphql'; import { AnswersService } from './answers.service'; +import { StatusModel } from '../common/models/status.model'; +import { Answer } from './models/answer.model'; +import { UpdateAnswerInput } from './dto/update-answer.input'; @Resolver() export class AnswersResolver { constructor(private readonly answersService: AnswersService) {} + + @Mutation((returns) => StatusModel) + async deleteAnswerById(@Args('id') id: string): Promise { + const status = await this.answersService.deleteAnswerById(id); + return { success: status }; + } + + @Mutation((returns) => Answer) + async updateAnswerById( + @Args('updateAnsData') updateAnsData: UpdateAnswerInput, + ): Promise { + return await this.answersService.updateAnswerById(updateAnsData); + } } diff --git a/src/answers/answers.service.ts b/src/answers/answers.service.ts index f5d3ab2..d409401 100644 --- a/src/answers/answers.service.ts +++ b/src/answers/answers.service.ts @@ -2,7 +2,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Answer } from './models/answer.model'; -import { NewAnswerInput } from './dto/new-answer.input'; +import { CreateAnswerInput } from './dto/create-answer.input'; +import { UpdateAnswerInput } from './dto/update-answer.input'; @Injectable() export class AnswersService { @@ -11,7 +12,18 @@ export class AnswersService { private readonly answersRepository: Repository, ) {} - async createAnswer(data: NewAnswerInput[]) { + async createAnswer(data: CreateAnswerInput[]) { return await this.answersRepository.save(data); } + + async deleteAnswerById(id: string): Promise { + const { affected } = await this.answersRepository.delete(id); + return !!affected; + } + + async updateAnswerById(data: UpdateAnswerInput): Promise { + return await this.answersRepository.save({ + ...data, + }); + } } diff --git a/src/answers/dto/new-answer.input.ts b/src/answers/dto/create-answer.input.ts similarity index 90% rename from src/answers/dto/new-answer.input.ts rename to src/answers/dto/create-answer.input.ts index 2908785..9451e89 100644 --- a/src/answers/dto/new-answer.input.ts +++ b/src/answers/dto/create-answer.input.ts @@ -2,7 +2,7 @@ import { Field, InputType } from '@nestjs/graphql'; import { IsOptional, Length } from 'class-validator'; @InputType() -export class NewAnswerInput { +export class CreateAnswerInput { @Field({ nullable: false }) @Length(1, 30) text: string; diff --git a/src/answers/dto/update-answer.input.ts b/src/answers/dto/update-answer.input.ts new file mode 100644 index 0000000..6daa9fa --- /dev/null +++ b/src/answers/dto/update-answer.input.ts @@ -0,0 +1,17 @@ +import { Field, InputType } from '@nestjs/graphql'; +import { IsOptional, IsUUID, Length } from 'class-validator'; + +@InputType() +export class UpdateAnswerInput { + @Field({ nullable: false }) + @IsUUID('all') + id: string; + + @Field({ nullable: false }) + @Length(1, 30) + text: string; + + @Field({ nullable: true }) + @IsOptional() + imgURL?: string; +} diff --git a/src/questions/dto/new-question.input.ts b/src/questions/dto/create-question.input.ts similarity index 66% rename from src/questions/dto/new-question.input.ts rename to src/questions/dto/create-question.input.ts index 505817e..e8e0afc 100644 --- a/src/questions/dto/new-question.input.ts +++ b/src/questions/dto/create-question.input.ts @@ -7,10 +7,10 @@ import { Length, MinLength, } from 'class-validator'; -import { NewAnswerInput } from '../../answers/dto/new-answer.input'; +import { CreateAnswerInput } from '../../answers/dto/create-answer.input'; @InputType() -export class NewQuestionInput { +export class CreateQuestionInput { @Field({ nullable: false }) @Length(2, 30) content: string; @@ -20,9 +20,9 @@ export class NewQuestionInput { @MinLength(5) imgURL?: string; - @Field((type) => [NewAnswerInput], { nullable: false }) + @Field((type) => [CreateAnswerInput], { nullable: false }) @IsArray() @ArrayMinSize(2) @ArrayMaxSize(8) - answers: NewAnswerInput[]; + answers: CreateAnswerInput[]; } diff --git a/src/questions/dto/update-question.input.ts b/src/questions/dto/update-question.input.ts new file mode 100644 index 0000000..b07c27d --- /dev/null +++ b/src/questions/dto/update-question.input.ts @@ -0,0 +1,18 @@ +import { Field, InputType } from '@nestjs/graphql'; +import { IsOptional, IsUUID, Length, MinLength } from 'class-validator'; + +@InputType() +export class UpdateQuestionInput { + @Field({ nullable: false }) + @IsUUID('all') + id: string; + + @Field({ nullable: false }) + @Length(2, 30) + content: string; + + @Field({ nullable: true, defaultValue: null }) + @IsOptional() + @MinLength(5) + imgURL?: string; +} diff --git a/src/questions/questions.resolver.ts b/src/questions/questions.resolver.ts index e76d7ec..9f300df 100644 --- a/src/questions/questions.resolver.ts +++ b/src/questions/questions.resolver.ts @@ -1,6 +1,8 @@ -import { Args, Query, Resolver } from '@nestjs/graphql'; +import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { Question } from './models/question.model'; import { QuestionsService } from './questions.service'; +import { StatusModel } from '../common/models/status.model'; +import { UpdateQuestionInput } from './dto/update-question.input'; @Resolver((of: Question) => Question) export class QuestionsResolver { @@ -10,4 +12,17 @@ export class QuestionsResolver { async getQuestionById(@Args('id') id: string): Promise { return await this.questionsService.getById(id); } + + @Mutation((returns) => StatusModel) + async deleteQuestionById(@Args('id') id: string): Promise { + const status = await this.questionsService.deleteQuestionById(id); + return { success: status }; + } + + @Mutation((returns) => Question) + async updateQuestionById( + @Args('updateQuestionData') updateQuestionData: UpdateQuestionInput, + ): Promise { + return await this.questionsService.updateQuestionById(updateQuestionData); + } } diff --git a/src/questions/questions.service.ts b/src/questions/questions.service.ts index 728b086..1e1eeca 100644 --- a/src/questions/questions.service.ts +++ b/src/questions/questions.service.ts @@ -2,8 +2,9 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Question } from './models/question.model'; import { Repository } from 'typeorm'; -import { NewQuestionInput } from './dto/new-question.input'; +import { CreateQuestionInput } from './dto/create-question.input'; import { AnswersService } from '../answers/answers.service'; +import { UpdateQuestionInput } from './dto/update-question.input'; @Injectable() export class QuestionsService { @@ -14,13 +15,26 @@ export class QuestionsService { ) {} async getById(id: string): Promise { - return await this.questionRepository.findOne(id); + return await this.questionRepository.findOne(id, { + relations: ['answers'], + }); } - async createNewQuestion(data: NewQuestionInput[]) { + async createNewQuestion(data: CreateQuestionInput[]) { for (const elem of data) { await this.answerService.createAnswer(elem.answers); } return await this.questionRepository.save(data); } + + async deleteQuestionById(id: string): Promise { + const { affected } = await this.questionRepository.delete(id); + return !!affected; + } + + async updateQuestionById(data: UpdateQuestionInput): Promise { + return await this.questionRepository.save({ + ...data, + }); + } } diff --git a/src/quizzes/dto/new-quiz.input.ts b/src/quizzes/dto/create-quiz.input.ts similarity index 52% rename from src/quizzes/dto/new-quiz.input.ts rename to src/quizzes/dto/create-quiz.input.ts index 6e5bac3..71e0cef 100644 --- a/src/quizzes/dto/new-quiz.input.ts +++ b/src/quizzes/dto/create-quiz.input.ts @@ -1,15 +1,15 @@ import { Field, InputType } from '@nestjs/graphql'; import { ArrayMinSize, IsArray, Length } from 'class-validator'; -import { NewQuestionInput } from '../../questions/dto/new-question.input'; +import { CreateQuestionInput } from '../../questions/dto/create-question.input'; @InputType() -export class NewQuizInput { +export class CreateQuizInput { @Field({ nullable: false }) @Length(2, 30) name: string; - @Field((type) => [NewQuestionInput], { nullable: false }) + @Field((type) => [CreateQuestionInput], { nullable: false }) @IsArray() @ArrayMinSize(1) - questions: NewQuestionInput[]; + questions: CreateQuestionInput[]; } diff --git a/src/quizzes/dto/update-quiz.input.ts b/src/quizzes/dto/update-quiz.input.ts new file mode 100644 index 0000000..73c4160 --- /dev/null +++ b/src/quizzes/dto/update-quiz.input.ts @@ -0,0 +1,13 @@ +import { Field, InputType } from '@nestjs/graphql'; +import { IsUUID, Length } from 'class-validator'; + +@InputType() +export class UpdateQuizInput { + @Field({ nullable: false }) + @IsUUID('all') + id: string; + + @Field({ nullable: false }) + @Length(2, 30) + name: string; +} diff --git a/src/quizzes/quizzes.resolver.ts b/src/quizzes/quizzes.resolver.ts index 175ca85..24bb633 100644 --- a/src/quizzes/quizzes.resolver.ts +++ b/src/quizzes/quizzes.resolver.ts @@ -1,8 +1,9 @@ import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { Quiz } from './models/quiz.model'; import { QuizzesService } from './quizzes.service'; -import { NewQuizInput } from './dto/new-quiz.input'; +import { CreateQuizInput } from './dto/create-quiz.input'; import { StatusModel } from '../common/models/status.model'; +import { UpdateQuizInput } from './dto/update-quiz.input'; @Resolver((of: Quiz) => Quiz) export class QuizzesResolver { @@ -25,7 +26,14 @@ export class QuizzesResolver { } @Mutation((returns) => Quiz) - async addQuiz(@Args('newQuizData') newQuizData: NewQuizInput) { + async addQuiz(@Args('newQuizData') newQuizData: CreateQuizInput) { return await this.quizzesService.addQuiz(newQuizData); } + + @Mutation((returns) => Quiz) + async updateQuizById( + @Args('updateQuizData') updateQuizData: UpdateQuizInput, + ) { + return this.quizzesService.updateQuizById(updateQuizData); + } } diff --git a/src/quizzes/quizzes.service.ts b/src/quizzes/quizzes.service.ts index 261f121..73bfcc8 100644 --- a/src/quizzes/quizzes.service.ts +++ b/src/quizzes/quizzes.service.ts @@ -1,9 +1,10 @@ import { Injectable } from '@nestjs/common'; import { Repository } from 'typeorm'; import { Quiz } from './models/quiz.model'; -import { NewQuizInput } from './dto/new-quiz.input'; +import { CreateQuizInput } from './dto/create-quiz.input'; import { InjectRepository } from '@nestjs/typeorm'; import { QuestionsService } from '../questions/questions.service'; +import { UpdateQuizInput } from './dto/update-quiz.input'; @Injectable() export class QuizzesService { @@ -19,7 +20,7 @@ export class QuizzesService { }); } - async addQuiz(data: NewQuizInput): Promise { + async addQuiz(data: CreateQuizInput): Promise { await this.questionsService.createNewQuestion(data.questions); return await this.quizRepository.save(data); } @@ -34,4 +35,10 @@ export class QuizzesService { const { affected } = await this.quizRepository.delete(id); return !!affected; } + + async updateQuizById(data: UpdateQuizInput): Promise { + return await this.quizRepository.save({ + ...data, + }); + } } diff --git a/to-ask b/to-ask new file mode 100644 index 0000000..32fbe4b --- /dev/null +++ b/to-ask @@ -0,0 +1,4 @@ +- как редактировать правльный ли ответ, чтобы не оказалось что правильный ответов вообще нет. +- норм ли редактировать через save в type orm. Можно насоздавать в теории +- стоит ли создавать отдельный эндпонит для добавления вопросов к тесту и ответов к вопросу (думаю да) +- удаление валиадция. Чтобы не удалить вообще все -- GitLab From a57c948affcc5976347e1b37d0ef7f16e3d7181a Mon Sep 17 00:00:00 2001 From: Dima-Mitrich Date: Wed, 1 Dec 2021 15:14:57 +0300 Subject: [PATCH 4/9] TR-12: added validation for delete and update methods --- schema.gql | 5 +++-- src/answers/answers.resolver.ts | 5 +++++ src/answers/answers.service.ts | 26 ++++++++++++++++++++++++++ src/questions/questions.module.ts | 2 ++ src/questions/questions.resolver.ts | 2 +- src/questions/questions.service.ts | 17 ++++++++++++++++- src/quizzes/dto/create-quiz.input.ts | 3 ++- src/quizzes/quizzes.module.ts | 1 + src/quizzes/quizzes.resolver.ts | 4 ++-- to-ask | 7 ++++--- 10 files changed, 62 insertions(+), 10 deletions(-) diff --git a/schema.gql b/schema.gql index 0c05308..95c03f6 100644 --- a/schema.gql +++ b/schema.gql @@ -34,8 +34,8 @@ type StatusModel { type Query { quizzes: [Quiz!]! - getQuizById(id: String!): Quiz! - getQuestionById(id: String!): Question! + getQuizById(id: String!): Quiz + getQuestionById(id: String!): Question } type Mutation { @@ -46,6 +46,7 @@ type Mutation { updateQuestionById(updateQuestionData: UpdateQuestionInput!): Question! deleteAnswerById(id: String!): StatusModel! updateAnswerById(updateAnsData: UpdateAnswerInput!): Answer! + updateRightAnswer(id: String!): [Answer!]! } input CreateQuizInput { diff --git a/src/answers/answers.resolver.ts b/src/answers/answers.resolver.ts index 155aafb..e6a6fd0 100644 --- a/src/answers/answers.resolver.ts +++ b/src/answers/answers.resolver.ts @@ -20,4 +20,9 @@ export class AnswersResolver { ): Promise { return await this.answersService.updateAnswerById(updateAnsData); } + + @Mutation((returns) => [Answer]) + async updateRightAnswer(@Args('id') id: string): Promise { + return await this.answersService.updateRightAnswer(id); + } } diff --git a/src/answers/answers.service.ts b/src/answers/answers.service.ts index d409401..e55db8b 100644 --- a/src/answers/answers.service.ts +++ b/src/answers/answers.service.ts @@ -17,13 +17,39 @@ export class AnswersService { } async deleteAnswerById(id: string): Promise { + const answer = await this.answersRepository.findOne(id, { + relations: ['question', 'question.answers'], + }); + + if (!answer) throw new Error('no such answer'); + else if (answer.isCorrect) throw new Error('you cant delete right answer'); + else if (answer.question.answers.length < 3) + throw new Error('create new answer before delete'); + const { affected } = await this.answersRepository.delete(id); return !!affected; } async updateAnswerById(data: UpdateAnswerInput): Promise { + const answer = await this.answersRepository.findOne(data.id); + if (!answer) throw new Error('no such answer'); + return await this.answersRepository.save({ + ...answer, ...data, }); } + + async updateRightAnswer(id: string): Promise { + const answer = await this.answersRepository.findOne(id, { + relations: ['question', 'question.answers'], + }); + const changedAnswer = answer.question.answers.find( + (elem) => elem.isCorrect === true, + ); + changedAnswer.isCorrect = false; + answer.isCorrect = true; + + return await this.answersRepository.save([answer, changedAnswer]); + } } diff --git a/src/questions/questions.module.ts b/src/questions/questions.module.ts index 4b6296b..fb2cf38 100644 --- a/src/questions/questions.module.ts +++ b/src/questions/questions.module.ts @@ -4,12 +4,14 @@ import { QuestionsResolver } from './questions.resolver'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Question } from './models/question.model'; import { AnswersModule } from '../answers/answers.module'; +import { QuizzesModule } from '../quizzes/quizzes.module'; @Module({ providers: [QuestionsService, QuestionsResolver], imports: [ TypeOrmModule.forFeature([Question]), forwardRef(() => AnswersModule), + forwardRef(() => QuizzesModule), ], exports: [QuestionsService], }) diff --git a/src/questions/questions.resolver.ts b/src/questions/questions.resolver.ts index 9f300df..241bea4 100644 --- a/src/questions/questions.resolver.ts +++ b/src/questions/questions.resolver.ts @@ -8,7 +8,7 @@ import { UpdateQuestionInput } from './dto/update-question.input'; export class QuestionsResolver { constructor(private readonly questionsService: QuestionsService) {} - @Query((returns) => Question) + @Query((returns) => Question, { nullable: true }) async getQuestionById(@Args('id') id: string): Promise { return await this.questionsService.getById(id); } diff --git a/src/questions/questions.service.ts b/src/questions/questions.service.ts index 1e1eeca..ee71a7b 100644 --- a/src/questions/questions.service.ts +++ b/src/questions/questions.service.ts @@ -1,10 +1,11 @@ -import { Injectable } from '@nestjs/common'; +import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Question } from './models/question.model'; import { Repository } from 'typeorm'; import { CreateQuestionInput } from './dto/create-question.input'; import { AnswersService } from '../answers/answers.service'; import { UpdateQuestionInput } from './dto/update-question.input'; +import { QuizzesService } from '../quizzes/quizzes.service'; @Injectable() export class QuestionsService { @@ -12,6 +13,8 @@ export class QuestionsService { @InjectRepository(Question) private readonly questionRepository: Repository, private readonly answerService: AnswersService, + @Inject(forwardRef(() => QuizzesService)) + private readonly quizService: QuizzesService, ) {} async getById(id: string): Promise { @@ -28,12 +31,24 @@ export class QuestionsService { } async deleteQuestionById(id: string): Promise { + const question = await this.questionRepository.findOne(id, { + relations: ['quiz', 'quiz.questions'], + }); + + if (!question) throw new Error('no such question'); + else if (question.quiz.questions.length < 2) + throw new Error('create new question before delete'); + const { affected } = await this.questionRepository.delete(id); return !!affected; } async updateQuestionById(data: UpdateQuestionInput): Promise { + const question = await this.questionRepository.findOne(data.id); + if (!question) throw new Error('no such question'); + return await this.questionRepository.save({ + ...question, ...data, }); } diff --git a/src/quizzes/dto/create-quiz.input.ts b/src/quizzes/dto/create-quiz.input.ts index 71e0cef..a4819e4 100644 --- a/src/quizzes/dto/create-quiz.input.ts +++ b/src/quizzes/dto/create-quiz.input.ts @@ -1,5 +1,5 @@ import { Field, InputType } from '@nestjs/graphql'; -import { ArrayMinSize, IsArray, Length } from 'class-validator'; +import { ArrayMaxSize, ArrayMinSize, IsArray, Length } from 'class-validator'; import { CreateQuestionInput } from '../../questions/dto/create-question.input'; @InputType() @@ -11,5 +11,6 @@ export class CreateQuizInput { @Field((type) => [CreateQuestionInput], { nullable: false }) @IsArray() @ArrayMinSize(1) + @ArrayMaxSize(30) questions: CreateQuestionInput[]; } diff --git a/src/quizzes/quizzes.module.ts b/src/quizzes/quizzes.module.ts index eb71a4e..eddb1bf 100644 --- a/src/quizzes/quizzes.module.ts +++ b/src/quizzes/quizzes.module.ts @@ -11,5 +11,6 @@ import { QuestionsModule } from '../questions/questions.module'; TypeOrmModule.forFeature([Quiz]), forwardRef(() => QuestionsModule), ], + exports: [QuizzesService], }) export class QuizzesModule {} diff --git a/src/quizzes/quizzes.resolver.ts b/src/quizzes/quizzes.resolver.ts index 24bb633..197ea49 100644 --- a/src/quizzes/quizzes.resolver.ts +++ b/src/quizzes/quizzes.resolver.ts @@ -9,12 +9,12 @@ import { UpdateQuizInput } from './dto/update-quiz.input'; export class QuizzesResolver { constructor(private readonly quizzesService: QuizzesService) {} - @Query((returns) => [Quiz]) + @Query((returns) => [Quiz], { nullable: false }) async quizzes(): Promise { return await this.quizzesService.findAll(); } - @Query((returns) => Quiz) + @Query((returns) => Quiz, { nullable: true }) async getQuizById(@Args('id') id: string) { return await this.quizzesService.getQuizById(id); } diff --git a/to-ask b/to-ask index 32fbe4b..0ff2298 100644 --- a/to-ask +++ b/to-ask @@ -1,4 +1,5 @@ -- как редактировать правльный ли ответ, чтобы не оказалось что правильный ответов вообще нет. -- норм ли редактировать через save в type orm. Можно насоздавать в теории +- как редактировать правльный ли ответ, чтобы не оказалось что правильный ответов вообще нет. - done +- норм ли редактировать через save в type orm. Можно насоздавать в теории - done - стоит ли создавать отдельный эндпонит для добавления вопросов к тесту и ответов к вопросу (думаю да) -- удаление валиадция. Чтобы не удалить вообще все +- удаление валиадция. Чтобы не удалить вообще все - done +- нельзя удалить правильный ответ! - done -- GitLab From 91f657e0f169e3628e32a5782b22289209e23c1b Mon Sep 17 00:00:00 2001 From: Dima-Mitrich Date: Wed, 1 Dec 2021 16:38:22 +0300 Subject: [PATCH 5/9] TR-12: resolved merge conflits --- src/answers/dto/new-answer.input.ts | 16 -------------- src/questions/dto/new-question.input.ts | 28 ------------------------- src/quizzes/dto/new-quiz.input.ts | 15 ------------- 3 files changed, 59 deletions(-) delete mode 100644 src/answers/dto/new-answer.input.ts delete mode 100644 src/questions/dto/new-question.input.ts delete mode 100644 src/quizzes/dto/new-quiz.input.ts diff --git a/src/answers/dto/new-answer.input.ts b/src/answers/dto/new-answer.input.ts deleted file mode 100644 index 2908785..0000000 --- a/src/answers/dto/new-answer.input.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Field, InputType } from '@nestjs/graphql'; -import { IsOptional, Length } from 'class-validator'; - -@InputType() -export class NewAnswerInput { - @Field({ nullable: false }) - @Length(1, 30) - text: string; - - @Field({ nullable: true }) - @IsOptional() - imgURL?: string; - - @Field({ nullable: false, defaultValue: false }) - isCorrect: boolean; -} diff --git a/src/questions/dto/new-question.input.ts b/src/questions/dto/new-question.input.ts deleted file mode 100644 index 505817e..0000000 --- a/src/questions/dto/new-question.input.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Field, InputType } from '@nestjs/graphql'; -import { - ArrayMaxSize, - ArrayMinSize, - IsArray, - IsOptional, - Length, - MinLength, -} from 'class-validator'; -import { NewAnswerInput } from '../../answers/dto/new-answer.input'; - -@InputType() -export class NewQuestionInput { - @Field({ nullable: false }) - @Length(2, 30) - content: string; - - @Field({ nullable: true, defaultValue: null }) - @IsOptional() - @MinLength(5) - imgURL?: string; - - @Field((type) => [NewAnswerInput], { nullable: false }) - @IsArray() - @ArrayMinSize(2) - @ArrayMaxSize(8) - answers: NewAnswerInput[]; -} diff --git a/src/quizzes/dto/new-quiz.input.ts b/src/quizzes/dto/new-quiz.input.ts deleted file mode 100644 index 6e5bac3..0000000 --- a/src/quizzes/dto/new-quiz.input.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Field, InputType } from '@nestjs/graphql'; -import { ArrayMinSize, IsArray, Length } from 'class-validator'; -import { NewQuestionInput } from '../../questions/dto/new-question.input'; - -@InputType() -export class NewQuizInput { - @Field({ nullable: false }) - @Length(2, 30) - name: string; - - @Field((type) => [NewQuestionInput], { nullable: false }) - @IsArray() - @ArrayMinSize(1) - questions: NewQuestionInput[]; -} -- GitLab From a6d8b2be1a01cc3bfa536b232f8eac9e2a832cf4 Mon Sep 17 00:00:00 2001 From: Dima-Mitrich Date: Fri, 3 Dec 2021 15:16:13 +0300 Subject: [PATCH 6/9] TR-12: added entities, queries and mutations for game process --- schema.gql | 38 +++++++- src/answers/answers.service.ts | 6 ++ src/answers/models/answer.model.ts | 9 +- src/app.module.ts | 8 +- src/common/models/status.model.ts | 2 +- src/games/dto/answer-question.args.ts | 17 ++++ src/games/dto/delete-player.args.ts | 13 +++ src/games/dto/join-player.input.ts | 13 +++ src/games/enums/statuses.enum.ts | 11 +++ src/games/games.module.ts | 19 ++++ src/games/games.resolver.ts | 66 ++++++++++++++ src/games/games.service.ts | 120 +++++++++++++++++++++++++ src/games/models/game.model.ts | 44 +++++++++ src/players/models/player.model.ts | 37 ++++++++ src/players/players.module.ts | 12 +++ src/players/players.resolver.ts | 4 + src/players/players.service.ts | 33 +++++++ src/questions/models/question.model.ts | 4 +- src/quizzes/models/quiz.model.ts | 17 ++-- to-ask | 2 + 20 files changed, 454 insertions(+), 21 deletions(-) create mode 100644 src/games/dto/answer-question.args.ts create mode 100644 src/games/dto/delete-player.args.ts create mode 100644 src/games/dto/join-player.input.ts create mode 100644 src/games/enums/statuses.enum.ts create mode 100644 src/games/games.module.ts create mode 100644 src/games/games.resolver.ts create mode 100644 src/games/games.service.ts create mode 100644 src/games/models/game.model.ts create mode 100644 src/players/models/player.model.ts create mode 100644 src/players/players.module.ts create mode 100644 src/players/players.resolver.ts create mode 100644 src/players/players.service.ts diff --git a/schema.gql b/schema.gql index 95c03f6..663470b 100644 --- a/schema.gql +++ b/schema.gql @@ -20,6 +20,28 @@ type Question { answers: [Answer!]! } +"""player """ +type Player { + id: ID! + name: String! + game: Game! + answers: [Answer!]! +} + +"""game """ +type Game { + CODE: String! + status: GameStatus! + quiz: Quiz! + players: [Player!] +} + +enum GameStatus { + WAITING_FOR_PLAYERS + PLAYING + FINISHED +} + """quiz """ type Quiz { id: ID! @@ -27,7 +49,7 @@ type Quiz { questions: [Question!]! } -"""deleting status""" +"""delete status""" type StatusModel { success: Boolean! } @@ -36,6 +58,9 @@ type Query { quizzes: [Quiz!]! getQuizById(id: String!): Quiz getQuestionById(id: String!): Question + activatedGames: [Game!]! + activatedGamesByCode(code: String!): Game + reportGameByCode(code: String!): Game! } type Mutation { @@ -47,6 +72,12 @@ type Mutation { deleteAnswerById(id: String!): StatusModel! updateAnswerById(updateAnsData: UpdateAnswerInput!): Answer! updateRightAnswer(id: String!): [Answer!]! + activateGame(quizId: String!): Game! + deactivateGameByCode(code: String!): StatusModel! + startGameByCode(code: String!): Game! + deletePlayerFromGame(gameCode: String!, playerId: String!): StatusModel! + joinPlayerToGame(joinPlayerInput: JoinPlayerInput!): Player! + answerQuestionById(questionId: String!, playerId: String!, answerId: String!): Boolean! } input CreateQuizInput { @@ -82,3 +113,8 @@ input UpdateAnswerInput { text: String! imgURL: String } + +input JoinPlayerInput { + gameCode: String! + name: String! +} diff --git a/src/answers/answers.service.ts b/src/answers/answers.service.ts index e55db8b..218dccb 100644 --- a/src/answers/answers.service.ts +++ b/src/answers/answers.service.ts @@ -52,4 +52,10 @@ export class AnswersService { return await this.answersRepository.save([answer, changedAnswer]); } + + async getAnswerById(id: string): Promise { + return await this.answersRepository.findOne(id, { + relations: ['question'], + }); + } } diff --git a/src/answers/models/answer.model.ts b/src/answers/models/answer.model.ts index febdbca..be831eb 100644 --- a/src/answers/models/answer.model.ts +++ b/src/answers/models/answer.model.ts @@ -1,11 +1,5 @@ import { Field, ID, ObjectType } from '@nestjs/graphql'; -import { - Column, - Entity, - Generated, - ManyToOne, - PrimaryGeneratedColumn, -} from 'typeorm'; +import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; import { Question } from '../../questions/models/question.model'; import { IsOptional } from 'class-validator'; @@ -14,7 +8,6 @@ import { IsOptional } from 'class-validator'; export class Answer { @Field((type) => ID) @PrimaryGeneratedColumn('uuid') - @Generated('uuid') id: string; @Field() diff --git a/src/app.module.ts b/src/app.module.ts index 75843f1..86c4605 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,6 +8,10 @@ import { QuestionsModule } from './questions/questions.module'; import { AnswersModule } from './answers/answers.module'; import { Question } from './questions/models/question.model'; import { Answer } from './answers/models/answer.model'; +import { GamesModule } from './games/games.module'; +import { Game } from './games/models/game.model'; +import { PlayersModule } from './players/players.module'; +import { Player } from './players/models/player.model'; @Module({ imports: [ @@ -24,12 +28,14 @@ import { Answer } from './answers/models/answer.model'; username: process.env.POSTGRES_USER || 'postgres', password: process.env.POSTGRES_PASSWORD || 'root', database: process.env.POSTGRES_DB || 'kahoot', - entities: [Quiz, Question, Answer], + entities: [Quiz, Question, Answer, Game, Player], synchronize: true, }), QuizzesModule, QuestionsModule, AnswersModule, + GamesModule, + PlayersModule, ], }) export class AppModule {} diff --git a/src/common/models/status.model.ts b/src/common/models/status.model.ts index 6bcdd3b..e3fb493 100644 --- a/src/common/models/status.model.ts +++ b/src/common/models/status.model.ts @@ -1,6 +1,6 @@ import { Field, ObjectType } from '@nestjs/graphql'; -@ObjectType({ description: 'deleting status' }) +@ObjectType({ description: 'delete status' }) export class StatusModel { @Field({ nullable: false }) success: boolean; diff --git a/src/games/dto/answer-question.args.ts b/src/games/dto/answer-question.args.ts new file mode 100644 index 0000000..7a3a480 --- /dev/null +++ b/src/games/dto/answer-question.args.ts @@ -0,0 +1,17 @@ +import { ArgsType, Field } from '@nestjs/graphql'; +import { IsUUID } from 'class-validator'; + +@ArgsType() +export class AnswerQuestionArgs { + @Field((type) => String, { nullable: false }) + @IsUUID('all') + questionId: string; + + @Field((type) => String, { nullable: false }) + @IsUUID('all') + playerId: string; + + @Field((type) => String, { nullable: false }) + @IsUUID('all') + answerId: string; +} diff --git a/src/games/dto/delete-player.args.ts b/src/games/dto/delete-player.args.ts new file mode 100644 index 0000000..d890da5 --- /dev/null +++ b/src/games/dto/delete-player.args.ts @@ -0,0 +1,13 @@ +import { ArgsType, Field } from '@nestjs/graphql'; +import { IsUUID } from 'class-validator'; + +@ArgsType() +export class DeletePlayerArgs { + @Field((type) => String, { nullable: false }) + @IsUUID('all') + gameCode: string; + + @Field((type) => String, { nullable: false }) + @IsUUID('all') + playerId: string; +} diff --git a/src/games/dto/join-player.input.ts b/src/games/dto/join-player.input.ts new file mode 100644 index 0000000..271df00 --- /dev/null +++ b/src/games/dto/join-player.input.ts @@ -0,0 +1,13 @@ +import { Field, InputType } from '@nestjs/graphql'; +import { IsUUID, Length } from 'class-validator'; + +@InputType() +export class JoinPlayerInput { + @Field({ nullable: false }) + @IsUUID('all') + gameCode: string; + + @Field({ nullable: false }) + @Length(2, 10) + name: string; +} diff --git a/src/games/enums/statuses.enum.ts b/src/games/enums/statuses.enum.ts new file mode 100644 index 0000000..eb9b353 --- /dev/null +++ b/src/games/enums/statuses.enum.ts @@ -0,0 +1,11 @@ +import { registerEnumType } from '@nestjs/graphql'; + +export enum GameStatus { + WAITING_FOR_PLAYERS, + PLAYING, + FINISHED, +} + +registerEnumType(GameStatus, { + name: 'GameStatus', +}); diff --git a/src/games/games.module.ts b/src/games/games.module.ts new file mode 100644 index 0000000..f44ccb1 --- /dev/null +++ b/src/games/games.module.ts @@ -0,0 +1,19 @@ +import { forwardRef, Module } from '@nestjs/common'; +import { GamesService } from './games.service'; +import { GamesResolver } from './games.resolver'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Game } from './models/game.model'; +import { QuizzesModule } from '../quizzes/quizzes.module'; +import { PlayersModule } from '../players/players.module'; +import { AnswersModule } from '../answers/answers.module'; + +@Module({ + providers: [GamesService, GamesResolver], + imports: [ + TypeOrmModule.forFeature([Game]), + forwardRef(() => QuizzesModule), + forwardRef(() => PlayersModule), + forwardRef(() => AnswersModule), + ], +}) +export class GamesModule {} diff --git a/src/games/games.resolver.ts b/src/games/games.resolver.ts new file mode 100644 index 0000000..858b69f --- /dev/null +++ b/src/games/games.resolver.ts @@ -0,0 +1,66 @@ +import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; +import { GamesService } from './games.service'; +import { Game } from './models/game.model'; +import { StatusModel } from '../common/models/status.model'; +import { Player } from '../players/models/player.model'; +import { JoinPlayerInput } from './dto/join-player.input'; +import { DeletePlayerArgs } from './dto/delete-player.args'; +import { AnswerQuestionArgs } from './dto/answer-question.args'; + +@Resolver() +export class GamesResolver { + constructor(private readonly gamesService: GamesService) {} + + @Mutation((returns) => Game) + async activateGame(@Args('quizId') quizId: string): Promise { + return await this.gamesService.createGame(quizId); + } + + @Mutation((returns) => StatusModel) + async deactivateGameByCode(@Args('code') code: string) { + const success = await this.gamesService.deleteGame(code); + return { success }; + } + + @Mutation((returns) => Game) + async startGameByCode(@Args('code') code: string): Promise { + return await this.gamesService.startGameByCode(code); + } + + @Mutation((returns) => StatusModel) + async deletePlayerFromGame( + @Args() deletePlayerData: DeletePlayerArgs, + ): Promise { + const success = await this.gamesService.deletePlayer(deletePlayerData); + return { success }; + } + + @Mutation((returns) => Player) + async joinPlayerToGame( + @Args('joinPlayerInput') joinPlayerInput: JoinPlayerInput, + ): Promise { + return await this.gamesService.joinPlayer(joinPlayerInput); + } + + @Mutation((returns) => Boolean) + async answerQuestionById( + @Args() answerQuestionData: AnswerQuestionArgs, + ): Promise { + return this.gamesService.answerQuestion(answerQuestionData); + } + + @Query((returns) => [Game]) + async activatedGames(): Promise { + return await this.gamesService.getAllGames(); + } + + @Query((returns) => Game, { nullable: true }) + async activatedGamesByCode(@Args('code') code: string): Promise { + return await this.gamesService.getGameByCode(code); + } + + @Query((returns) => Game) + async reportGameByCode(@Args('code') code: string): Promise { + return await this.gamesService.getGameResult(code); + } +} diff --git a/src/games/games.service.ts b/src/games/games.service.ts new file mode 100644 index 0000000..83edfd8 --- /dev/null +++ b/src/games/games.service.ts @@ -0,0 +1,120 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Game } from './models/game.model'; +import { Repository } from 'typeorm'; +import { QuizzesService } from '../quizzes/quizzes.service'; +import { GameStatus } from './enums/statuses.enum'; +import { Player } from '../players/models/player.model'; +import { PlayersService } from '../players/players.service'; +import { JoinPlayerInput } from './dto/join-player.input'; +import { DeletePlayerArgs } from './dto/delete-player.args'; +import { AnswerQuestionArgs } from './dto/answer-question.args'; +import { AnswersService } from '../answers/answers.service'; + +@Injectable() +export class GamesService { + constructor( + @InjectRepository(Game) + private readonly gamesRepository: Repository, + private readonly quizzesService: QuizzesService, + private readonly playerService: PlayersService, + private readonly answerService: AnswersService, + ) {} + + async createGame(quizId: string): Promise { + const quiz = await this.quizzesService.getQuizById(quizId); + if (!quiz) throw new Error('no such quiz'); + + const game = new Game(); + game.quiz = quiz; + return this.gamesRepository.save(game); + } + + async deleteGame(code: string): Promise { + const { affected } = await this.gamesRepository.delete({ CODE: code }); + return !!affected; + } + + async getAllGames(): Promise { + return await this.gamesRepository.find({ + relations: ['players'], + }); + } + + async getGameByCode(code: string): Promise { + return await this.gamesRepository.findOne( + { CODE: code }, + { + relations: ['players'], + }, + ); + } + + async startGameByCode(code: string): Promise { + const game = await this.getGameByCode(code); + if (!game) throw new Error('no such game'); + else if (game.status === GameStatus.PLAYING) + throw new Error('the game has already started'); + + game.status = GameStatus.PLAYING; + return await this.gamesRepository.save(game); + } + + async joinPlayer(data: JoinPlayerInput): Promise { + const game = await this.getGameByCode(data.gameCode); + if (!game) throw new Error('no such game'); + else if (game.status !== GameStatus.WAITING_FOR_PLAYERS) + throw new Error('the game is not activated'); + + const player = await this.playerService.createPlayer(data.name); + game.players = [player]; + await this.gamesRepository.save(game); + + return player; + } + + async deletePlayer(data: DeletePlayerArgs): Promise { + const game = await this.getGameByCode(data.gameCode); + const isPlayerInGame = game.players.find( + (elem) => elem.id === data.playerId, + ); + if (!isPlayerInGame) throw new Error('no such player in the game'); + + return await this.playerService.deletePlayer(data.playerId); + } + + async answerQuestion(data: AnswerQuestionArgs): Promise { + const player = await this.playerService.getPlayerById(data.playerId); + if (!player) throw new Error('no such player'); + else if (player.game.status !== GameStatus.PLAYING) + throw new Error('the game is not being played now'); + + const wasAnswered = player.answers.find( + (elem) => elem.question.id === data.questionId, + ); + if (wasAnswered) + throw new Error('player has already answered the question'); + + const answer = await this.answerService.getAnswerById(data.answerId); + if (!answer || answer.question.id !== data.questionId) + throw new Error('no such answer'); + + player.answers.push(answer); + const updated = await this.playerService.updatePlayer(player); + if (!updated) throw new Error('something went wrong'); + + return answer.isCorrect; + } + + async getGameResult(code: string): Promise { + const game = await this.gamesRepository.findOne( + { CODE: code }, + { relations: ['players', 'players.answers'] }, + ); + if (!game) throw new Error('no such game'); + // else if (game.status !== GameStatus.FINISHED) + // throw new Error('the game is not finished yet'); + + return game; + } +} diff --git a/src/games/models/game.model.ts b/src/games/models/game.model.ts new file mode 100644 index 0000000..2990657 --- /dev/null +++ b/src/games/models/game.model.ts @@ -0,0 +1,44 @@ +import { Field, ObjectType } from '@nestjs/graphql'; +import { + Column, + Entity, + Generated, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { IsInt } from 'class-validator'; +import { Quiz } from '../../quizzes/models/quiz.model'; +import { Player } from '../../players/models/player.model'; +import { GameStatus } from '../enums/statuses.enum'; + +@ObjectType({ description: 'game ' }) +@Entity() +export class Game { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Field({ nullable: false }) + @Column({ unique: true }) + @Generated('uuid') + CODE: string; + + @Field((type) => GameStatus, { nullable: false, defaultValue: 0 }) + @Column({ nullable: false, default: 0 }) + @IsInt() + status: number; + + @Field((type) => Quiz) + @ManyToOne(() => Quiz, (quiz: Quiz) => quiz.games, { + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }) + quiz: Quiz; + + @Field((type) => [Player], { nullable: true }) + @OneToMany(() => Player, (player: Player) => player.game, { + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }) + players?: Player[] | null; +} diff --git a/src/players/models/player.model.ts b/src/players/models/player.model.ts new file mode 100644 index 0000000..4750b4a --- /dev/null +++ b/src/players/models/player.model.ts @@ -0,0 +1,37 @@ +import { Field, ID, ObjectType } from '@nestjs/graphql'; +import { + Column, + Entity, + ManyToMany, + ManyToOne, + PrimaryGeneratedColumn, + JoinTable, +} from 'typeorm'; +import { Length } from 'class-validator'; +import { Game } from '../../games/models/game.model'; +import { Answer } from '../../answers/models/answer.model'; + +@ObjectType({ description: 'player ' }) +@Entity() +export class Player { + @Field((type) => ID) + @PrimaryGeneratedColumn('uuid') + id: string; + + @Field({ nullable: false }) + @Column({ nullable: false }) + @Length(2, 10) + name: string; + + @Field((type) => Game) + @ManyToOne(() => Game, (game: Game) => game.players, { + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }) + game: Game; + + @Field((type) => [Answer]) + @ManyToMany(() => Answer) + @JoinTable() + answers: Answer[]; +} diff --git a/src/players/players.module.ts b/src/players/players.module.ts new file mode 100644 index 0000000..5b025b0 --- /dev/null +++ b/src/players/players.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { PlayersService } from './players.service'; +import { PlayersResolver } from './players.resolver'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Player } from './models/player.model'; + +@Module({ + providers: [PlayersService, PlayersResolver], + imports: [TypeOrmModule.forFeature([Player])], + exports: [PlayersService], +}) +export class PlayersModule {} diff --git a/src/players/players.resolver.ts b/src/players/players.resolver.ts new file mode 100644 index 0000000..fbb10f2 --- /dev/null +++ b/src/players/players.resolver.ts @@ -0,0 +1,4 @@ +import { Resolver } from '@nestjs/graphql'; + +@Resolver() +export class PlayersResolver {} diff --git a/src/players/players.service.ts b/src/players/players.service.ts new file mode 100644 index 0000000..ad03d9c --- /dev/null +++ b/src/players/players.service.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Player } from './models/player.model'; +import { Repository } from 'typeorm'; + +@Injectable() +export class PlayersService { + constructor( + @InjectRepository(Player) + private readonly playersRepository: Repository, + ) {} + + async createPlayer(name: string): Promise { + const player = new Player(); + player.name = name; + return await this.playersRepository.save(player); + } + + async deletePlayer(id: string): Promise { + const { affected } = await this.playersRepository.delete(id); + return !!affected; + } + + async getPlayerById(id: string): Promise { + return this.playersRepository.findOne(id, { + relations: ['game', 'answers', 'answers.question'], + }); + } + + async updatePlayer(data: Player): Promise { + return await this.playersRepository.save(data); + } +} diff --git a/src/questions/models/question.model.ts b/src/questions/models/question.model.ts index 5637efe..3f0437b 100644 --- a/src/questions/models/question.model.ts +++ b/src/questions/models/question.model.ts @@ -2,7 +2,6 @@ import { Field, ID, ObjectType } from '@nestjs/graphql'; import { Column, Entity, - Generated, ManyToOne, OneToMany, PrimaryGeneratedColumn, @@ -15,7 +14,6 @@ import { Answer } from '../../answers/models/answer.model'; export class Question { @Field((type) => ID) @PrimaryGeneratedColumn('uuid') - @Generated('uuid') id: string; @Field({ nullable: false }) @@ -29,12 +27,14 @@ export class Question { @Field((type) => Quiz) @ManyToOne(() => Quiz, (quiz) => quiz.questions, { onDelete: 'CASCADE', + onUpdate: 'CASCADE', }) quiz: Quiz; @Field((type) => [Answer]) @OneToMany(() => Answer, (answer) => answer.question, { onDelete: 'CASCADE', + onUpdate: 'CASCADE', }) answers: Answer[]; } diff --git a/src/quizzes/models/quiz.model.ts b/src/quizzes/models/quiz.model.ts index 22cbf1e..d28a2f9 100644 --- a/src/quizzes/models/quiz.model.ts +++ b/src/quizzes/models/quiz.model.ts @@ -1,19 +1,13 @@ import { Field, ID, ObjectType } from '@nestjs/graphql'; -import { - Column, - Entity, - Generated, - OneToMany, - PrimaryGeneratedColumn, -} from 'typeorm'; +import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; import { Question } from '../../questions/models/question.model'; +import { Game } from '../../games/models/game.model'; @ObjectType({ description: 'quiz ' }) @Entity() export class Quiz { @Field((type) => ID) @PrimaryGeneratedColumn('uuid') - @Generated('uuid') id: string; @Field((type) => String) @@ -23,6 +17,13 @@ export class Quiz { @Field((type) => [Question]) @OneToMany(() => Question, (question) => question.quiz, { onDelete: 'CASCADE', + onUpdate: 'CASCADE', }) questions: Question[]; + + @OneToMany(() => Game, (game: Game) => game.quiz, { + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }) + games: Game[]; } diff --git a/to-ask b/to-ask index 0ff2298..b661fd5 100644 --- a/to-ask +++ b/to-ask @@ -3,3 +3,5 @@ - стоит ли создавать отдельный эндпонит для добавления вопросов к тесту и ответов к вопросу (думаю да) - удаление валиадция. Чтобы не удалить вообще все - done - нельзя удалить правильный ответ! - done +- проверить на повторяемость имен +- не давать создавать пустой тест - done -- GitLab From 63c6454c28fd6954ae3137c37dec9e5ed6bd1c28 Mon Sep 17 00:00:00 2001 From: Dima-Mitrich Date: Fri, 3 Dec 2021 15:16:13 +0300 Subject: [PATCH 7/9] TR-13: added entities, queries and mutations for game process --- schema.gql | 38 +++++++- src/answers/answers.service.ts | 6 ++ src/answers/models/answer.model.ts | 9 +- src/app.module.ts | 8 +- src/common/models/status.model.ts | 2 +- src/games/dto/answer-question.args.ts | 17 ++++ src/games/dto/delete-player.args.ts | 13 +++ src/games/dto/join-player.input.ts | 13 +++ src/games/enums/statuses.enum.ts | 11 +++ src/games/games.module.ts | 19 ++++ src/games/games.resolver.ts | 66 ++++++++++++++ src/games/games.service.ts | 120 +++++++++++++++++++++++++ src/games/models/game.model.ts | 44 +++++++++ src/players/models/player.model.ts | 37 ++++++++ src/players/players.module.ts | 12 +++ src/players/players.resolver.ts | 4 + src/players/players.service.ts | 33 +++++++ src/questions/models/question.model.ts | 4 +- src/quizzes/models/quiz.model.ts | 17 ++-- to-ask | 2 + 20 files changed, 454 insertions(+), 21 deletions(-) create mode 100644 src/games/dto/answer-question.args.ts create mode 100644 src/games/dto/delete-player.args.ts create mode 100644 src/games/dto/join-player.input.ts create mode 100644 src/games/enums/statuses.enum.ts create mode 100644 src/games/games.module.ts create mode 100644 src/games/games.resolver.ts create mode 100644 src/games/games.service.ts create mode 100644 src/games/models/game.model.ts create mode 100644 src/players/models/player.model.ts create mode 100644 src/players/players.module.ts create mode 100644 src/players/players.resolver.ts create mode 100644 src/players/players.service.ts diff --git a/schema.gql b/schema.gql index 95c03f6..663470b 100644 --- a/schema.gql +++ b/schema.gql @@ -20,6 +20,28 @@ type Question { answers: [Answer!]! } +"""player """ +type Player { + id: ID! + name: String! + game: Game! + answers: [Answer!]! +} + +"""game """ +type Game { + CODE: String! + status: GameStatus! + quiz: Quiz! + players: [Player!] +} + +enum GameStatus { + WAITING_FOR_PLAYERS + PLAYING + FINISHED +} + """quiz """ type Quiz { id: ID! @@ -27,7 +49,7 @@ type Quiz { questions: [Question!]! } -"""deleting status""" +"""delete status""" type StatusModel { success: Boolean! } @@ -36,6 +58,9 @@ type Query { quizzes: [Quiz!]! getQuizById(id: String!): Quiz getQuestionById(id: String!): Question + activatedGames: [Game!]! + activatedGamesByCode(code: String!): Game + reportGameByCode(code: String!): Game! } type Mutation { @@ -47,6 +72,12 @@ type Mutation { deleteAnswerById(id: String!): StatusModel! updateAnswerById(updateAnsData: UpdateAnswerInput!): Answer! updateRightAnswer(id: String!): [Answer!]! + activateGame(quizId: String!): Game! + deactivateGameByCode(code: String!): StatusModel! + startGameByCode(code: String!): Game! + deletePlayerFromGame(gameCode: String!, playerId: String!): StatusModel! + joinPlayerToGame(joinPlayerInput: JoinPlayerInput!): Player! + answerQuestionById(questionId: String!, playerId: String!, answerId: String!): Boolean! } input CreateQuizInput { @@ -82,3 +113,8 @@ input UpdateAnswerInput { text: String! imgURL: String } + +input JoinPlayerInput { + gameCode: String! + name: String! +} diff --git a/src/answers/answers.service.ts b/src/answers/answers.service.ts index e55db8b..218dccb 100644 --- a/src/answers/answers.service.ts +++ b/src/answers/answers.service.ts @@ -52,4 +52,10 @@ export class AnswersService { return await this.answersRepository.save([answer, changedAnswer]); } + + async getAnswerById(id: string): Promise { + return await this.answersRepository.findOne(id, { + relations: ['question'], + }); + } } diff --git a/src/answers/models/answer.model.ts b/src/answers/models/answer.model.ts index febdbca..be831eb 100644 --- a/src/answers/models/answer.model.ts +++ b/src/answers/models/answer.model.ts @@ -1,11 +1,5 @@ import { Field, ID, ObjectType } from '@nestjs/graphql'; -import { - Column, - Entity, - Generated, - ManyToOne, - PrimaryGeneratedColumn, -} from 'typeorm'; +import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; import { Question } from '../../questions/models/question.model'; import { IsOptional } from 'class-validator'; @@ -14,7 +8,6 @@ import { IsOptional } from 'class-validator'; export class Answer { @Field((type) => ID) @PrimaryGeneratedColumn('uuid') - @Generated('uuid') id: string; @Field() diff --git a/src/app.module.ts b/src/app.module.ts index 75843f1..86c4605 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,6 +8,10 @@ import { QuestionsModule } from './questions/questions.module'; import { AnswersModule } from './answers/answers.module'; import { Question } from './questions/models/question.model'; import { Answer } from './answers/models/answer.model'; +import { GamesModule } from './games/games.module'; +import { Game } from './games/models/game.model'; +import { PlayersModule } from './players/players.module'; +import { Player } from './players/models/player.model'; @Module({ imports: [ @@ -24,12 +28,14 @@ import { Answer } from './answers/models/answer.model'; username: process.env.POSTGRES_USER || 'postgres', password: process.env.POSTGRES_PASSWORD || 'root', database: process.env.POSTGRES_DB || 'kahoot', - entities: [Quiz, Question, Answer], + entities: [Quiz, Question, Answer, Game, Player], synchronize: true, }), QuizzesModule, QuestionsModule, AnswersModule, + GamesModule, + PlayersModule, ], }) export class AppModule {} diff --git a/src/common/models/status.model.ts b/src/common/models/status.model.ts index 6bcdd3b..e3fb493 100644 --- a/src/common/models/status.model.ts +++ b/src/common/models/status.model.ts @@ -1,6 +1,6 @@ import { Field, ObjectType } from '@nestjs/graphql'; -@ObjectType({ description: 'deleting status' }) +@ObjectType({ description: 'delete status' }) export class StatusModel { @Field({ nullable: false }) success: boolean; diff --git a/src/games/dto/answer-question.args.ts b/src/games/dto/answer-question.args.ts new file mode 100644 index 0000000..7a3a480 --- /dev/null +++ b/src/games/dto/answer-question.args.ts @@ -0,0 +1,17 @@ +import { ArgsType, Field } from '@nestjs/graphql'; +import { IsUUID } from 'class-validator'; + +@ArgsType() +export class AnswerQuestionArgs { + @Field((type) => String, { nullable: false }) + @IsUUID('all') + questionId: string; + + @Field((type) => String, { nullable: false }) + @IsUUID('all') + playerId: string; + + @Field((type) => String, { nullable: false }) + @IsUUID('all') + answerId: string; +} diff --git a/src/games/dto/delete-player.args.ts b/src/games/dto/delete-player.args.ts new file mode 100644 index 0000000..d890da5 --- /dev/null +++ b/src/games/dto/delete-player.args.ts @@ -0,0 +1,13 @@ +import { ArgsType, Field } from '@nestjs/graphql'; +import { IsUUID } from 'class-validator'; + +@ArgsType() +export class DeletePlayerArgs { + @Field((type) => String, { nullable: false }) + @IsUUID('all') + gameCode: string; + + @Field((type) => String, { nullable: false }) + @IsUUID('all') + playerId: string; +} diff --git a/src/games/dto/join-player.input.ts b/src/games/dto/join-player.input.ts new file mode 100644 index 0000000..271df00 --- /dev/null +++ b/src/games/dto/join-player.input.ts @@ -0,0 +1,13 @@ +import { Field, InputType } from '@nestjs/graphql'; +import { IsUUID, Length } from 'class-validator'; + +@InputType() +export class JoinPlayerInput { + @Field({ nullable: false }) + @IsUUID('all') + gameCode: string; + + @Field({ nullable: false }) + @Length(2, 10) + name: string; +} diff --git a/src/games/enums/statuses.enum.ts b/src/games/enums/statuses.enum.ts new file mode 100644 index 0000000..eb9b353 --- /dev/null +++ b/src/games/enums/statuses.enum.ts @@ -0,0 +1,11 @@ +import { registerEnumType } from '@nestjs/graphql'; + +export enum GameStatus { + WAITING_FOR_PLAYERS, + PLAYING, + FINISHED, +} + +registerEnumType(GameStatus, { + name: 'GameStatus', +}); diff --git a/src/games/games.module.ts b/src/games/games.module.ts new file mode 100644 index 0000000..f44ccb1 --- /dev/null +++ b/src/games/games.module.ts @@ -0,0 +1,19 @@ +import { forwardRef, Module } from '@nestjs/common'; +import { GamesService } from './games.service'; +import { GamesResolver } from './games.resolver'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Game } from './models/game.model'; +import { QuizzesModule } from '../quizzes/quizzes.module'; +import { PlayersModule } from '../players/players.module'; +import { AnswersModule } from '../answers/answers.module'; + +@Module({ + providers: [GamesService, GamesResolver], + imports: [ + TypeOrmModule.forFeature([Game]), + forwardRef(() => QuizzesModule), + forwardRef(() => PlayersModule), + forwardRef(() => AnswersModule), + ], +}) +export class GamesModule {} diff --git a/src/games/games.resolver.ts b/src/games/games.resolver.ts new file mode 100644 index 0000000..858b69f --- /dev/null +++ b/src/games/games.resolver.ts @@ -0,0 +1,66 @@ +import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; +import { GamesService } from './games.service'; +import { Game } from './models/game.model'; +import { StatusModel } from '../common/models/status.model'; +import { Player } from '../players/models/player.model'; +import { JoinPlayerInput } from './dto/join-player.input'; +import { DeletePlayerArgs } from './dto/delete-player.args'; +import { AnswerQuestionArgs } from './dto/answer-question.args'; + +@Resolver() +export class GamesResolver { + constructor(private readonly gamesService: GamesService) {} + + @Mutation((returns) => Game) + async activateGame(@Args('quizId') quizId: string): Promise { + return await this.gamesService.createGame(quizId); + } + + @Mutation((returns) => StatusModel) + async deactivateGameByCode(@Args('code') code: string) { + const success = await this.gamesService.deleteGame(code); + return { success }; + } + + @Mutation((returns) => Game) + async startGameByCode(@Args('code') code: string): Promise { + return await this.gamesService.startGameByCode(code); + } + + @Mutation((returns) => StatusModel) + async deletePlayerFromGame( + @Args() deletePlayerData: DeletePlayerArgs, + ): Promise { + const success = await this.gamesService.deletePlayer(deletePlayerData); + return { success }; + } + + @Mutation((returns) => Player) + async joinPlayerToGame( + @Args('joinPlayerInput') joinPlayerInput: JoinPlayerInput, + ): Promise { + return await this.gamesService.joinPlayer(joinPlayerInput); + } + + @Mutation((returns) => Boolean) + async answerQuestionById( + @Args() answerQuestionData: AnswerQuestionArgs, + ): Promise { + return this.gamesService.answerQuestion(answerQuestionData); + } + + @Query((returns) => [Game]) + async activatedGames(): Promise { + return await this.gamesService.getAllGames(); + } + + @Query((returns) => Game, { nullable: true }) + async activatedGamesByCode(@Args('code') code: string): Promise { + return await this.gamesService.getGameByCode(code); + } + + @Query((returns) => Game) + async reportGameByCode(@Args('code') code: string): Promise { + return await this.gamesService.getGameResult(code); + } +} diff --git a/src/games/games.service.ts b/src/games/games.service.ts new file mode 100644 index 0000000..83edfd8 --- /dev/null +++ b/src/games/games.service.ts @@ -0,0 +1,120 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Game } from './models/game.model'; +import { Repository } from 'typeorm'; +import { QuizzesService } from '../quizzes/quizzes.service'; +import { GameStatus } from './enums/statuses.enum'; +import { Player } from '../players/models/player.model'; +import { PlayersService } from '../players/players.service'; +import { JoinPlayerInput } from './dto/join-player.input'; +import { DeletePlayerArgs } from './dto/delete-player.args'; +import { AnswerQuestionArgs } from './dto/answer-question.args'; +import { AnswersService } from '../answers/answers.service'; + +@Injectable() +export class GamesService { + constructor( + @InjectRepository(Game) + private readonly gamesRepository: Repository, + private readonly quizzesService: QuizzesService, + private readonly playerService: PlayersService, + private readonly answerService: AnswersService, + ) {} + + async createGame(quizId: string): Promise { + const quiz = await this.quizzesService.getQuizById(quizId); + if (!quiz) throw new Error('no such quiz'); + + const game = new Game(); + game.quiz = quiz; + return this.gamesRepository.save(game); + } + + async deleteGame(code: string): Promise { + const { affected } = await this.gamesRepository.delete({ CODE: code }); + return !!affected; + } + + async getAllGames(): Promise { + return await this.gamesRepository.find({ + relations: ['players'], + }); + } + + async getGameByCode(code: string): Promise { + return await this.gamesRepository.findOne( + { CODE: code }, + { + relations: ['players'], + }, + ); + } + + async startGameByCode(code: string): Promise { + const game = await this.getGameByCode(code); + if (!game) throw new Error('no such game'); + else if (game.status === GameStatus.PLAYING) + throw new Error('the game has already started'); + + game.status = GameStatus.PLAYING; + return await this.gamesRepository.save(game); + } + + async joinPlayer(data: JoinPlayerInput): Promise { + const game = await this.getGameByCode(data.gameCode); + if (!game) throw new Error('no such game'); + else if (game.status !== GameStatus.WAITING_FOR_PLAYERS) + throw new Error('the game is not activated'); + + const player = await this.playerService.createPlayer(data.name); + game.players = [player]; + await this.gamesRepository.save(game); + + return player; + } + + async deletePlayer(data: DeletePlayerArgs): Promise { + const game = await this.getGameByCode(data.gameCode); + const isPlayerInGame = game.players.find( + (elem) => elem.id === data.playerId, + ); + if (!isPlayerInGame) throw new Error('no such player in the game'); + + return await this.playerService.deletePlayer(data.playerId); + } + + async answerQuestion(data: AnswerQuestionArgs): Promise { + const player = await this.playerService.getPlayerById(data.playerId); + if (!player) throw new Error('no such player'); + else if (player.game.status !== GameStatus.PLAYING) + throw new Error('the game is not being played now'); + + const wasAnswered = player.answers.find( + (elem) => elem.question.id === data.questionId, + ); + if (wasAnswered) + throw new Error('player has already answered the question'); + + const answer = await this.answerService.getAnswerById(data.answerId); + if (!answer || answer.question.id !== data.questionId) + throw new Error('no such answer'); + + player.answers.push(answer); + const updated = await this.playerService.updatePlayer(player); + if (!updated) throw new Error('something went wrong'); + + return answer.isCorrect; + } + + async getGameResult(code: string): Promise { + const game = await this.gamesRepository.findOne( + { CODE: code }, + { relations: ['players', 'players.answers'] }, + ); + if (!game) throw new Error('no such game'); + // else if (game.status !== GameStatus.FINISHED) + // throw new Error('the game is not finished yet'); + + return game; + } +} diff --git a/src/games/models/game.model.ts b/src/games/models/game.model.ts new file mode 100644 index 0000000..2990657 --- /dev/null +++ b/src/games/models/game.model.ts @@ -0,0 +1,44 @@ +import { Field, ObjectType } from '@nestjs/graphql'; +import { + Column, + Entity, + Generated, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { IsInt } from 'class-validator'; +import { Quiz } from '../../quizzes/models/quiz.model'; +import { Player } from '../../players/models/player.model'; +import { GameStatus } from '../enums/statuses.enum'; + +@ObjectType({ description: 'game ' }) +@Entity() +export class Game { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Field({ nullable: false }) + @Column({ unique: true }) + @Generated('uuid') + CODE: string; + + @Field((type) => GameStatus, { nullable: false, defaultValue: 0 }) + @Column({ nullable: false, default: 0 }) + @IsInt() + status: number; + + @Field((type) => Quiz) + @ManyToOne(() => Quiz, (quiz: Quiz) => quiz.games, { + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }) + quiz: Quiz; + + @Field((type) => [Player], { nullable: true }) + @OneToMany(() => Player, (player: Player) => player.game, { + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }) + players?: Player[] | null; +} diff --git a/src/players/models/player.model.ts b/src/players/models/player.model.ts new file mode 100644 index 0000000..4750b4a --- /dev/null +++ b/src/players/models/player.model.ts @@ -0,0 +1,37 @@ +import { Field, ID, ObjectType } from '@nestjs/graphql'; +import { + Column, + Entity, + ManyToMany, + ManyToOne, + PrimaryGeneratedColumn, + JoinTable, +} from 'typeorm'; +import { Length } from 'class-validator'; +import { Game } from '../../games/models/game.model'; +import { Answer } from '../../answers/models/answer.model'; + +@ObjectType({ description: 'player ' }) +@Entity() +export class Player { + @Field((type) => ID) + @PrimaryGeneratedColumn('uuid') + id: string; + + @Field({ nullable: false }) + @Column({ nullable: false }) + @Length(2, 10) + name: string; + + @Field((type) => Game) + @ManyToOne(() => Game, (game: Game) => game.players, { + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }) + game: Game; + + @Field((type) => [Answer]) + @ManyToMany(() => Answer) + @JoinTable() + answers: Answer[]; +} diff --git a/src/players/players.module.ts b/src/players/players.module.ts new file mode 100644 index 0000000..5b025b0 --- /dev/null +++ b/src/players/players.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { PlayersService } from './players.service'; +import { PlayersResolver } from './players.resolver'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Player } from './models/player.model'; + +@Module({ + providers: [PlayersService, PlayersResolver], + imports: [TypeOrmModule.forFeature([Player])], + exports: [PlayersService], +}) +export class PlayersModule {} diff --git a/src/players/players.resolver.ts b/src/players/players.resolver.ts new file mode 100644 index 0000000..fbb10f2 --- /dev/null +++ b/src/players/players.resolver.ts @@ -0,0 +1,4 @@ +import { Resolver } from '@nestjs/graphql'; + +@Resolver() +export class PlayersResolver {} diff --git a/src/players/players.service.ts b/src/players/players.service.ts new file mode 100644 index 0000000..ad03d9c --- /dev/null +++ b/src/players/players.service.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Player } from './models/player.model'; +import { Repository } from 'typeorm'; + +@Injectable() +export class PlayersService { + constructor( + @InjectRepository(Player) + private readonly playersRepository: Repository, + ) {} + + async createPlayer(name: string): Promise { + const player = new Player(); + player.name = name; + return await this.playersRepository.save(player); + } + + async deletePlayer(id: string): Promise { + const { affected } = await this.playersRepository.delete(id); + return !!affected; + } + + async getPlayerById(id: string): Promise { + return this.playersRepository.findOne(id, { + relations: ['game', 'answers', 'answers.question'], + }); + } + + async updatePlayer(data: Player): Promise { + return await this.playersRepository.save(data); + } +} diff --git a/src/questions/models/question.model.ts b/src/questions/models/question.model.ts index 5637efe..3f0437b 100644 --- a/src/questions/models/question.model.ts +++ b/src/questions/models/question.model.ts @@ -2,7 +2,6 @@ import { Field, ID, ObjectType } from '@nestjs/graphql'; import { Column, Entity, - Generated, ManyToOne, OneToMany, PrimaryGeneratedColumn, @@ -15,7 +14,6 @@ import { Answer } from '../../answers/models/answer.model'; export class Question { @Field((type) => ID) @PrimaryGeneratedColumn('uuid') - @Generated('uuid') id: string; @Field({ nullable: false }) @@ -29,12 +27,14 @@ export class Question { @Field((type) => Quiz) @ManyToOne(() => Quiz, (quiz) => quiz.questions, { onDelete: 'CASCADE', + onUpdate: 'CASCADE', }) quiz: Quiz; @Field((type) => [Answer]) @OneToMany(() => Answer, (answer) => answer.question, { onDelete: 'CASCADE', + onUpdate: 'CASCADE', }) answers: Answer[]; } diff --git a/src/quizzes/models/quiz.model.ts b/src/quizzes/models/quiz.model.ts index 22cbf1e..d28a2f9 100644 --- a/src/quizzes/models/quiz.model.ts +++ b/src/quizzes/models/quiz.model.ts @@ -1,19 +1,13 @@ import { Field, ID, ObjectType } from '@nestjs/graphql'; -import { - Column, - Entity, - Generated, - OneToMany, - PrimaryGeneratedColumn, -} from 'typeorm'; +import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; import { Question } from '../../questions/models/question.model'; +import { Game } from '../../games/models/game.model'; @ObjectType({ description: 'quiz ' }) @Entity() export class Quiz { @Field((type) => ID) @PrimaryGeneratedColumn('uuid') - @Generated('uuid') id: string; @Field((type) => String) @@ -23,6 +17,13 @@ export class Quiz { @Field((type) => [Question]) @OneToMany(() => Question, (question) => question.quiz, { onDelete: 'CASCADE', + onUpdate: 'CASCADE', }) questions: Question[]; + + @OneToMany(() => Game, (game: Game) => game.quiz, { + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }) + games: Game[]; } diff --git a/to-ask b/to-ask index 0ff2298..b661fd5 100644 --- a/to-ask +++ b/to-ask @@ -3,3 +3,5 @@ - стоит ли создавать отдельный эндпонит для добавления вопросов к тесту и ответов к вопросу (думаю да) - удаление валиадция. Чтобы не удалить вообще все - done - нельзя удалить правильный ответ! - done +- проверить на повторяемость имен +- не давать создавать пустой тест - done -- GitLab From 43a9598d236da0248b19baa901f62db8f2c28254 Mon Sep 17 00:00:00 2001 From: Dima-Mitrich Date: Fri, 3 Dec 2021 17:52:03 +0300 Subject: [PATCH 8/9] TR-13: fixed according comments: added constants and braces to IF statement --- src/answers/answers.resolver.ts | 4 +-- src/answers/answers.service.ts | 16 ++++++--- src/constants.ts | 14 ++++++++ src/games/games.service.ts | 56 ++++++++++++++++++++---------- src/questions/questions.service.ts | 10 +++--- to-ask | 7 ---- 6 files changed, 70 insertions(+), 37 deletions(-) create mode 100644 src/constants.ts delete mode 100644 to-ask diff --git a/src/answers/answers.resolver.ts b/src/answers/answers.resolver.ts index e6a6fd0..f139965 100644 --- a/src/answers/answers.resolver.ts +++ b/src/answers/answers.resolver.ts @@ -10,8 +10,8 @@ export class AnswersResolver { @Mutation((returns) => StatusModel) async deleteAnswerById(@Args('id') id: string): Promise { - const status = await this.answersService.deleteAnswerById(id); - return { success: status }; + const success = await this.answersService.deleteAnswerById(id); + return { success }; } @Mutation((returns) => Answer) diff --git a/src/answers/answers.service.ts b/src/answers/answers.service.ts index 218dccb..d420625 100644 --- a/src/answers/answers.service.ts +++ b/src/answers/answers.service.ts @@ -4,6 +4,11 @@ import { Repository } from 'typeorm'; import { Answer } from './models/answer.model'; import { CreateAnswerInput } from './dto/create-answer.input'; import { UpdateAnswerInput } from './dto/update-answer.input'; +import { + CREATE_ANSWER_BEFORE_DELETE, + DELETE_CORRECT_ANSWER_ERROR, + NO_ANSWER, +} from '../constants'; @Injectable() export class AnswersService { @@ -21,10 +26,11 @@ export class AnswersService { relations: ['question', 'question.answers'], }); - if (!answer) throw new Error('no such answer'); - else if (answer.isCorrect) throw new Error('you cant delete right answer'); - else if (answer.question.answers.length < 3) - throw new Error('create new answer before delete'); + if (!answer) throw new Error(NO_ANSWER); + else if (answer.isCorrect) throw new Error(DELETE_CORRECT_ANSWER_ERROR); + else if (answer.question.answers.length < 3) { + throw new Error(CREATE_ANSWER_BEFORE_DELETE); + } const { affected } = await this.answersRepository.delete(id); return !!affected; @@ -32,7 +38,7 @@ export class AnswersService { async updateAnswerById(data: UpdateAnswerInput): Promise { const answer = await this.answersRepository.findOne(data.id); - if (!answer) throw new Error('no such answer'); + if (!answer) throw new Error(NO_ANSWER); return await this.answersRepository.save({ ...answer, diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..0f417d0 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,14 @@ +export const NO_ANSWER = 'no such answer'; +export const DELETE_CORRECT_ANSWER_ERROR = 'you cant delete right answer'; +export const CREATE_ANSWER_BEFORE_DELETE = 'create new answer before delete'; +export const NO_QUIZ = 'no such quiz'; +export const NO_GAME = 'no such game'; +export const GAME_HAS_STARTED = 'the game has already started'; +export const GAME_NOT_ACTIVATED = 'the game is not activated'; +export const NO_PLAYER = 'no such player in the game'; +export const GAME_IS_NOT_PLAYING = 'the game is not being played now'; +export const ALREADY_ANSWERED = 'player has already answered the question'; +export const SMTHNG_WENT_WRONG = 'something went wrong'; +export const GAME_NOT_FINISHED = 'the game is not finished yet'; +export const CREAT_QUESTION_BEFORE_DELETE = 'create new question before delete'; +export const NO_QUESTION = 'no such question'; diff --git a/src/games/games.service.ts b/src/games/games.service.ts index 83edfd8..8510e8a 100644 --- a/src/games/games.service.ts +++ b/src/games/games.service.ts @@ -10,6 +10,18 @@ import { JoinPlayerInput } from './dto/join-player.input'; import { DeletePlayerArgs } from './dto/delete-player.args'; import { AnswerQuestionArgs } from './dto/answer-question.args'; import { AnswersService } from '../answers/answers.service'; +import { + ALREADY_ANSWERED, + GAME_HAS_STARTED, + GAME_IS_NOT_PLAYING, + GAME_NOT_ACTIVATED, + GAME_NOT_FINISHED, + NO_ANSWER, + NO_GAME, + NO_PLAYER, + NO_QUIZ, + SMTHNG_WENT_WRONG, +} from '../constants'; @Injectable() export class GamesService { @@ -23,7 +35,7 @@ export class GamesService { async createGame(quizId: string): Promise { const quiz = await this.quizzesService.getQuizById(quizId); - if (!quiz) throw new Error('no such quiz'); + if (!quiz) throw new Error(NO_QUIZ); const game = new Game(); game.quiz = quiz; @@ -52,9 +64,10 @@ export class GamesService { async startGameByCode(code: string): Promise { const game = await this.getGameByCode(code); - if (!game) throw new Error('no such game'); - else if (game.status === GameStatus.PLAYING) - throw new Error('the game has already started'); + if (!game) throw new Error(NO_GAME); + else if (game.status === GameStatus.PLAYING) { + throw new Error(GAME_HAS_STARTED); + } game.status = GameStatus.PLAYING; return await this.gamesRepository.save(game); @@ -62,9 +75,10 @@ export class GamesService { async joinPlayer(data: JoinPlayerInput): Promise { const game = await this.getGameByCode(data.gameCode); - if (!game) throw new Error('no such game'); - else if (game.status !== GameStatus.WAITING_FOR_PLAYERS) - throw new Error('the game is not activated'); + if (!game) throw new Error(NO_GAME); + else if (game.status !== GameStatus.WAITING_FOR_PLAYERS) { + throw new Error(GAME_NOT_ACTIVATED); + } const player = await this.playerService.createPlayer(data.name); game.players = [player]; @@ -78,30 +92,33 @@ export class GamesService { const isPlayerInGame = game.players.find( (elem) => elem.id === data.playerId, ); - if (!isPlayerInGame) throw new Error('no such player in the game'); + if (!isPlayerInGame) throw new Error(NO_PLAYER); return await this.playerService.deletePlayer(data.playerId); } async answerQuestion(data: AnswerQuestionArgs): Promise { const player = await this.playerService.getPlayerById(data.playerId); - if (!player) throw new Error('no such player'); - else if (player.game.status !== GameStatus.PLAYING) - throw new Error('the game is not being played now'); + if (!player) throw new Error(NO_PLAYER); + else if (player.game.status !== GameStatus.PLAYING) { + throw new Error(GAME_IS_NOT_PLAYING); + } const wasAnswered = player.answers.find( (elem) => elem.question.id === data.questionId, ); - if (wasAnswered) - throw new Error('player has already answered the question'); + if (wasAnswered) { + throw new Error(ALREADY_ANSWERED); + } const answer = await this.answerService.getAnswerById(data.answerId); - if (!answer || answer.question.id !== data.questionId) - throw new Error('no such answer'); + if (!answer || answer.question.id !== data.questionId) { + throw new Error(NO_ANSWER); + } player.answers.push(answer); const updated = await this.playerService.updatePlayer(player); - if (!updated) throw new Error('something went wrong'); + if (!updated) throw new Error(SMTHNG_WENT_WRONG); return answer.isCorrect; } @@ -111,9 +128,10 @@ export class GamesService { { CODE: code }, { relations: ['players', 'players.answers'] }, ); - if (!game) throw new Error('no such game'); - // else if (game.status !== GameStatus.FINISHED) - // throw new Error('the game is not finished yet'); + if (!game) throw new Error(NO_GAME); + else if (game.status !== GameStatus.FINISHED) { + throw new Error(GAME_NOT_FINISHED); + } return game; } diff --git a/src/questions/questions.service.ts b/src/questions/questions.service.ts index ee71a7b..dc5d0ff 100644 --- a/src/questions/questions.service.ts +++ b/src/questions/questions.service.ts @@ -6,6 +6,7 @@ import { CreateQuestionInput } from './dto/create-question.input'; import { AnswersService } from '../answers/answers.service'; import { UpdateQuestionInput } from './dto/update-question.input'; import { QuizzesService } from '../quizzes/quizzes.service'; +import { CREAT_QUESTION_BEFORE_DELETE, NO_QUESTION } from '../constants'; @Injectable() export class QuestionsService { @@ -35,9 +36,10 @@ export class QuestionsService { relations: ['quiz', 'quiz.questions'], }); - if (!question) throw new Error('no such question'); - else if (question.quiz.questions.length < 2) - throw new Error('create new question before delete'); + if (!question) throw new Error(NO_QUESTION); + else if (question.quiz.questions.length < 2) { + throw new Error(CREAT_QUESTION_BEFORE_DELETE); + } const { affected } = await this.questionRepository.delete(id); return !!affected; @@ -45,7 +47,7 @@ export class QuestionsService { async updateQuestionById(data: UpdateQuestionInput): Promise { const question = await this.questionRepository.findOne(data.id); - if (!question) throw new Error('no such question'); + if (!question) throw new Error(NO_QUESTION); return await this.questionRepository.save({ ...question, diff --git a/to-ask b/to-ask deleted file mode 100644 index b661fd5..0000000 --- a/to-ask +++ /dev/null @@ -1,7 +0,0 @@ -- как редактировать правльный ли ответ, чтобы не оказалось что правильный ответов вообще нет. - done -- норм ли редактировать через save в type orm. Можно насоздавать в теории - done -- стоит ли создавать отдельный эндпонит для добавления вопросов к тесту и ответов к вопросу (думаю да) -- удаление валиадция. Чтобы не удалить вообще все - done -- нельзя удалить правильный ответ! - done -- проверить на повторяемость имен -- не давать создавать пустой тест - done -- GitLab From e35e313168358d07cd8c7bc4dd821b1ef305999b Mon Sep 17 00:00:00 2001 From: Dima-Mitrich Date: Mon, 6 Dec 2021 13:42:36 +0300 Subject: [PATCH 9/9] Revert "Merge branch 'develop' into feature/13-create-game-process" This reverts commit 7782c20bbe664b4df0b18b830573d3294e2f8bf9, reversing changes made to 43a9598d236da0248b19baa901f62db8f2c28254. --- src/answers/answers.service.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/answers/answers.service.ts b/src/answers/answers.service.ts index 623dbde..d420625 100644 --- a/src/answers/answers.service.ts +++ b/src/answers/answers.service.ts @@ -58,4 +58,10 @@ export class AnswersService { return await this.answersRepository.save([answer, changedAnswer]); } + + async getAnswerById(id: string): Promise { + return await this.answersRepository.findOne(id, { + relations: ['question'], + }); + } } -- GitLab