11 Commits

43 changed files with 2889 additions and 758 deletions

3
.env.example Normal file
View File

@ -0,0 +1,3 @@
SENDGRID_API_KEY=
JWT_SECRET=
SITE_URL=test.com

4
.gitignore vendored
View File

@ -2,7 +2,9 @@
/build
.env.local
*.gen.ts
*.local*
npm-debug.log*
yarn-debug.log*

21
LICENCE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Ditriy Shishkov <me@dmitriy.com> (https://dmitriy.icu)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,9 +1,27 @@
# QuestionForm Backend
Backend used with QuestionForm application.
Backend for QuestionForm application.
# Built with:
- Prisma
- Graphql
- Graphql-yoga
- Apollo Server
- Graphql code generator
- SendGrid
# Setting up development environment
```bash
$ git clone https://github.com/Dm1tr1y147/questionForm_backend
$ yarn
$ mv .env.example .env && vim .env
$ mv prisma/.env.example prisma/.env && vim .env
$ prisma migrate save --experimental && prisma migrate up --experimental
$ prisma generate
$ yarn dev
```
# API
_...coming soon..._

12
codegen.yml Normal file
View File

@ -0,0 +1,12 @@
overwrite: true
schema: "src/typeDefs/typeDefs.gql"
documents: null
generates:
src/typeDefs/typeDefs.gen.ts:
config:
useIndexSignature: true
wrapFieldDefinitions: true
enumsAsTypes: true
plugins:
- "typescript"
- "typescript-resolvers"

6
nodemon.json Normal file
View File

@ -0,0 +1,6 @@
{
"ignore": ["**/*.test.ts", "**/*.spec.ts", "node_modules"],
"watch": ["src"],
"exec": "yarn start",
"ext": "ts"
}

73
package-lock.json generated
View File

@ -1,73 +0,0 @@
{
"name": "backend",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@prisma/cli": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@prisma/cli/-/cli-2.7.1.tgz",
"integrity": "sha512-0uA+gWkNQ35DveVHDPltiTCTr4wcXtEhnPs463IEM+Xn8dTv9x0gtZiYHSuQM3t7uwlOxj1rurBsqSbiljynfQ==",
"dev": true
},
"@prisma/client": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-2.7.1.tgz",
"integrity": "sha512-IEWDCuvIaQTira8/jAyf+uY+AuPPUFDIXMSN4zEA/gvoJv2woq7RmkaubS+NQVgDbbyOR6F3UcXLiFTYQDzZkQ==",
"requires": {
"pkg-up": "^3.1.0"
}
},
"find-up": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
"requires": {
"locate-path": "^3.0.0"
}
},
"locate-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
"requires": {
"p-locate": "^3.0.0",
"path-exists": "^3.0.0"
}
},
"p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"requires": {
"p-try": "^2.0.0"
}
},
"p-locate": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
"requires": {
"p-limit": "^2.0.0"
}
},
"p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
},
"path-exists": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
},
"pkg-up": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz",
"integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==",
"requires": {
"find-up": "^3.0.0"
}
}
}
}

View File

@ -5,13 +5,28 @@
"license": "MIT",
"dependencies": {
"@prisma/client": "^2.7.1",
"graphql-yoga": "^1.18.3"
"@sendgrid/mail": "^7.2.6",
"@types/jsonwebtoken": "^8.5.0",
"apollo-server-express": "^2.18.2",
"express-jwt": "^6.0.0",
"graphql": "^15.3.0",
"jsonwebtoken": "^8.5.1",
"jwks-rsa": "^1.10.1",
"nodemon": "^2.0.4"
},
"scripts": {
"dev": "ts-node src/index.ts"
"dev": "nodemon",
"start": "ts-node src/index.ts",
"codegen": "graphql-codegen --config codegen.yml",
"lint": "eslint"
},
"devDependencies": {
"@prisma/cli": "^2.7.1",
"@graphql-codegen/cli": "1.17.10",
"@graphql-codegen/introspection": "1.18.0",
"@graphql-codegen/typescript": "1.17.10",
"@graphql-codegen/typescript-resolvers": "1.17.10",
"@types/dotenv": "^8.2.0",
"dotenv": "^8.2.0",
"ts-node": "^9.0.0",
"typescript": "^4.0.3"
}

1
prisma/.env.example Normal file
View File

@ -0,0 +1 @@
DATABASE_URL="postgres://"

Binary file not shown.

View File

@ -1,103 +0,0 @@
# Migration `20201003131039-initial-structure`
This migration has been generated by Dm1tr1y147 at 10/3/2020, 6:10:39 PM.
You can check out the [state of the schema](./schema.prisma) after the migration.
## Database Steps
```sql
CREATE TABLE "User" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"email" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
)
CREATE TABLE "Poll" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"userId" INTEGER,
"title" TEXT NOT NULL,
"description" TEXT,
"slug" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE
)
CREATE TABLE "Question" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"pollId" INTEGER,
"title" TEXT NOT NULL,
FOREIGN KEY ("pollId") REFERENCES "Poll"("id") ON DELETE SET NULL ON UPDATE CASCADE
)
CREATE TABLE "Variant" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"questionId" INTEGER,
"text" TEXT NOT NULL,
"count" INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY ("questionId") REFERENCES "Question"("id") ON DELETE SET NULL ON UPDATE CASCADE
)
CREATE UNIQUE INDEX "Poll.slug_unique" ON "Poll"("slug")
```
## Changes
```diff
diff --git schema.prisma schema.prisma
migration ..20201003131039-initial-structure
--- datamodel.dml
+++ datamodel.dml
@@ -1,0 +1,46 @@
+// This is your Prisma schema file,
+// learn more about it in the docs: https://pris.ly/d/prisma-schema
+
+datasource db {
+ provider = "sqlite"
+ url = "***"
+}
+
+generator client {
+ provider = "prisma-client-js"
+}
+
+model User {
+ id Int @id @default(autoincrement())
+ name String
+ email String?
+ createdAt DateTime @default(now())
+ polls Poll[]
+}
+
+model Poll {
+ id Int @id @default(autoincrement())
+ User User? @relation(fields: [userId], references: [id])
+ userId Int?
+ title String
+ description String?
+ slug String @unique
+ questions Question[]
+ createdAt DateTime @default(now())
+}
+
+model Question {
+ id Int @id @default(autoincrement())
+ Poll Poll? @relation(fields: [pollId], references: [id])
+ pollId Int?
+ title String
+ variants Variant[]
+}
+
+model Variant {
+ id Int @id @default(autoincrement())
+ Question Question? @relation(fields: [questionId], references: [id])
+ questionId Int?
+ text String
+ count Int @default(0)
+}
```

View File

@ -1,46 +0,0 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
datasource db {
provider = "sqlite"
url = "***"
}
generator client {
provider = "prisma-client-js"
}
model User {
id Int @id @default(autoincrement())
name String
email String?
createdAt DateTime @default(now())
polls Poll[]
}
model Poll {
id Int @id @default(autoincrement())
User User? @relation(fields: [userId], references: [id])
userId Int?
title String
description String?
slug String @unique
questions Question[]
createdAt DateTime @default(now())
}
model Question {
id Int @id @default(autoincrement())
Poll Poll? @relation(fields: [pollId], references: [id])
pollId Int?
title String
variants Variant[]
}
model Variant {
id Int @id @default(autoincrement())
Question Question? @relation(fields: [questionId], references: [id])
questionId Int?
text String
count Int @default(0)
}

View File

@ -0,0 +1,219 @@
# Migration `20201006125838-initial-migration`
This migration has been generated by Dm1tr1y147 at 10/6/2020, 5:58:38 PM.
You can check out the [state of the schema](./schema.prisma) after the migration.
## Database Steps
```sql
CREATE TYPE "public"."ChoiseType" AS ENUM ('SELECT', 'CHECK', 'CHOOSE')
CREATE TYPE "public"."AnswerType" AS ENUM ('INPUT', 'CHOISE')
CREATE TABLE "public"."Form" (
"title" text NOT NULL ,
"dateCreated" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"id" integer NOT NULL ,
"userId" integer NOT NULL ,
PRIMARY KEY ("id")
)
CREATE TABLE "public"."ChoisesQuestion" (
"title" text NOT NULL ,
"type" "ChoiseType" NOT NULL ,
"number" integer NOT NULL ,
"id" integer NOT NULL ,
"formId" integer ,
PRIMARY KEY ("id")
)
CREATE TABLE "public"."Variant" (
"text" text NOT NULL ,
"id" integer NOT NULL ,
"choisesQuestionId" integer ,
PRIMARY KEY ("id")
)
CREATE TABLE "public"."InputQuestion" (
"title" text NOT NULL ,
"number" integer NOT NULL ,
"id" integer NOT NULL ,
"formId" integer ,
PRIMARY KEY ("id")
)
CREATE TABLE "public"."FormSubmission" (
"date" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"id" integer NOT NULL ,
"formId" integer ,
"userId" integer NOT NULL ,
PRIMARY KEY ("id")
)
CREATE TABLE "public"."Answer" (
"id" integer NOT NULL ,
"userInput" text NOT NULL ,
"userChoise" integer NOT NULL ,
"type" "AnswerType" NOT NULL ,
"formSubmissionId" integer ,
PRIMARY KEY ("id")
)
CREATE TABLE "public"."User" (
"id" integer NOT NULL ,
"name" text NOT NULL ,
PRIMARY KEY ("id")
)
ALTER TABLE "public"."Form" ADD FOREIGN KEY ("userId")REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE
ALTER TABLE "public"."ChoisesQuestion" ADD FOREIGN KEY ("formId")REFERENCES "public"."Form"("id") ON DELETE SET NULL ON UPDATE CASCADE
ALTER TABLE "public"."Variant" ADD FOREIGN KEY ("choisesQuestionId")REFERENCES "public"."ChoisesQuestion"("id") ON DELETE SET NULL ON UPDATE CASCADE
ALTER TABLE "public"."InputQuestion" ADD FOREIGN KEY ("formId")REFERENCES "public"."Form"("id") ON DELETE SET NULL ON UPDATE CASCADE
ALTER TABLE "public"."FormSubmission" ADD FOREIGN KEY ("userId")REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE
ALTER TABLE "public"."FormSubmission" ADD FOREIGN KEY ("formId")REFERENCES "public"."Form"("id") ON DELETE SET NULL ON UPDATE CASCADE
ALTER TABLE "public"."Answer" ADD FOREIGN KEY ("formSubmissionId")REFERENCES "public"."FormSubmission"("id") ON DELETE SET NULL ON UPDATE CASCADE
```
## Changes
```diff
diff --git schema.prisma schema.prisma
migration ..20201006125838-initial-migration
--- datamodel.dml
+++ datamodel.dml
@@ -1,0 +1,126 @@
+// This is your Prisma schema file,
+// learn more about it in the docs: https://pris.ly/d/prisma-schema
+
+datasource db {
+ provider = "postgres"
+ url = "***"
+}
+
+generator client {
+ provider = "prisma-client-js"
+}
+
+// model User {
+// id Int @id @default(autoincrement())
+// name String
+// email String?
+// createdAt DateTime @default(now())
+// polls Poll[]
+// }
+
+// model Poll {
+// id Int @id @default(autoincrement())
+// User User? @relation(fields: [userId], references: [id])
+// userId Int?
+// title String
+// description String?
+// slug String @unique @default(cuid())
+// questions Question[]
+// createdAt DateTime @default(now())
+// }
+
+// model Question {
+// id Int @id @default(autoincrement())
+// Poll Poll? @relation(fields: [pollId], references: [id])
+// pollId Int?
+// title String
+// variants Variant[]
+// }
+
+// model Variant {
+// id Int @id @default(autoincrement())
+// Question Question? @relation(fields: [questionId], references: [id])
+// questionId Int?
+// text String
+// count Int @default(0)
+// }
+
+model Form {
+ title String
+ choisesQuestions ChoisesQuestion[]
+ inputQuestions InputQuestion[]
+ submissions FormSubmission[]
+ dateCreated DateTime @default(now())
+
+ id Int @id
+ userId Int
+ author User @relation(fields: [userId], references: [id])
+}
+
+model ChoisesQuestion {
+ title String
+ variants Variant[]
+ type ChoiseType
+ number Int
+
+ id Int @id
+ Form Form? @relation(fields: [formId], references: [id])
+ formId Int?
+}
+
+model Variant {
+ text String
+
+ id Int @id
+ ChoisesQuestion ChoisesQuestion? @relation(fields: [choisesQuestionId], references: [id])
+ choisesQuestionId Int?
+}
+
+model InputQuestion {
+ title String
+ number Int
+
+ id Int @id
+ Form Form? @relation(fields: [formId], references: [id])
+ formId Int?
+}
+
+model FormSubmission {
+ answers Answer[]
+ date DateTime @default(now())
+
+
+ id Int @id
+ user User @relation(fields: [userId], references: [id])
+ Form Form? @relation(fields: [formId], references: [id])
+ formId Int?
+ userId Int
+}
+
+model Answer {
+ id Int @id
+ userInput String
+ userChoise Int
+ type AnswerType
+ FormSubmission FormSubmission? @relation(fields: [formSubmissionId], references: [id])
+ formSubmissionId Int?
+}
+
+enum ChoiseType {
+ SELECT
+ CHECK
+ CHOOSE
+}
+
+enum AnswerType {
+ INPUT
+ CHOISE
+}
+
+model User {
+ id Int @id
+ name String
+
+ forms Form[]
+ formsSubmissions FormSubmission[]
+}
```

View File

@ -0,0 +1,126 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
datasource db {
provider = "postgres"
url = "***"
}
generator client {
provider = "prisma-client-js"
}
// model User {
// id Int @id @default(autoincrement())
// name String
// email String?
// createdAt DateTime @default(now())
// polls Poll[]
// }
// model Poll {
// id Int @id @default(autoincrement())
// User User? @relation(fields: [userId], references: [id])
// userId Int?
// title String
// description String?
// slug String @unique @default(cuid())
// questions Question[]
// createdAt DateTime @default(now())
// }
// model Question {
// id Int @id @default(autoincrement())
// Poll Poll? @relation(fields: [pollId], references: [id])
// pollId Int?
// title String
// variants Variant[]
// }
// model Variant {
// id Int @id @default(autoincrement())
// Question Question? @relation(fields: [questionId], references: [id])
// questionId Int?
// text String
// count Int @default(0)
// }
model Form {
title String
choisesQuestions ChoisesQuestion[]
inputQuestions InputQuestion[]
submissions FormSubmission[]
dateCreated DateTime @default(now())
id Int @id
userId Int
author User @relation(fields: [userId], references: [id])
}
model ChoisesQuestion {
title String
variants Variant[]
type ChoiseType
number Int
id Int @id
Form Form? @relation(fields: [formId], references: [id])
formId Int?
}
model Variant {
text String
id Int @id
ChoisesQuestion ChoisesQuestion? @relation(fields: [choisesQuestionId], references: [id])
choisesQuestionId Int?
}
model InputQuestion {
title String
number Int
id Int @id
Form Form? @relation(fields: [formId], references: [id])
formId Int?
}
model FormSubmission {
answers Answer[]
date DateTime @default(now())
id Int @id
user User @relation(fields: [userId], references: [id])
Form Form? @relation(fields: [formId], references: [id])
formId Int?
userId Int
}
model Answer {
id Int @id
userInput String
userChoise Int
type AnswerType
FormSubmission FormSubmission? @relation(fields: [formSubmissionId], references: [id])
formSubmissionId Int?
}
enum ChoiseType {
SELECT
CHECK
CHOOSE
}
enum AnswerType {
INPUT
CHOISE
}
model User {
id Int @id
name String
forms Form[]
formsSubmissions FormSubmission[]
}

View File

@ -0,0 +1,187 @@
# Migration `20201006185953-improved-schema-structure`
This migration has been generated by Dm1tr1y147 at 10/6/2020, 11:59:53 PM.
You can check out the [state of the schema](./schema.prisma) after the migration.
## Database Steps
```sql
CREATE SEQUENCE "answer_id_seq";
ALTER TABLE "public"."Answer" ALTER COLUMN "id" SET DEFAULT nextval('answer_id_seq');
ALTER SEQUENCE "answer_id_seq" OWNED BY "public"."Answer"."id"
CREATE SEQUENCE "choisesquestion_id_seq";
ALTER TABLE "public"."ChoisesQuestion" ALTER COLUMN "id" SET DEFAULT nextval('choisesquestion_id_seq');
ALTER SEQUENCE "choisesquestion_id_seq" OWNED BY "public"."ChoisesQuestion"."id"
CREATE SEQUENCE "form_id_seq";
ALTER TABLE "public"."Form" ALTER COLUMN "id" SET DEFAULT nextval('form_id_seq');
ALTER SEQUENCE "form_id_seq" OWNED BY "public"."Form"."id"
CREATE SEQUENCE "formsubmission_id_seq";
ALTER TABLE "public"."FormSubmission" ALTER COLUMN "id" SET DEFAULT nextval('formsubmission_id_seq');
ALTER SEQUENCE "formsubmission_id_seq" OWNED BY "public"."FormSubmission"."id"
CREATE SEQUENCE "inputquestion_id_seq";
ALTER TABLE "public"."InputQuestion" ALTER COLUMN "id" SET DEFAULT nextval('inputquestion_id_seq');
ALTER SEQUENCE "inputquestion_id_seq" OWNED BY "public"."InputQuestion"."id"
CREATE SEQUENCE "user_id_seq";
ALTER TABLE "public"."User" ALTER COLUMN "id" SET DEFAULT nextval('user_id_seq');
ALTER SEQUENCE "user_id_seq" OWNED BY "public"."User"."id"
CREATE SEQUENCE "variant_id_seq";
ALTER TABLE "public"."Variant" ALTER COLUMN "id" SET DEFAULT nextval('variant_id_seq');
ALTER SEQUENCE "variant_id_seq" OWNED BY "public"."Variant"."id"
```
## Changes
```diff
diff --git schema.prisma schema.prisma
migration 20201006125838-initial-migration..20201006185953-improved-schema-structure
--- datamodel.dml
+++ datamodel.dml
@@ -2,125 +2,90 @@
// learn more about it in the docs: https://pris.ly/d/prisma-schema
datasource db {
provider = "postgres"
- url = "***"
+ url = "***"
}
generator client {
provider = "prisma-client-js"
}
-// model User {
-// id Int @id @default(autoincrement())
-// name String
-// email String?
-// createdAt DateTime @default(now())
-// polls Poll[]
-// }
-
-// model Poll {
-// id Int @id @default(autoincrement())
-// User User? @relation(fields: [userId], references: [id])
-// userId Int?
-// title String
-// description String?
-// slug String @unique @default(cuid())
-// questions Question[]
-// createdAt DateTime @default(now())
-// }
-
-// model Question {
-// id Int @id @default(autoincrement())
-// Poll Poll? @relation(fields: [pollId], references: [id])
-// pollId Int?
-// title String
-// variants Variant[]
-// }
-
-// model Variant {
-// id Int @id @default(autoincrement())
-// Question Question? @relation(fields: [questionId], references: [id])
-// questionId Int?
-// text String
-// count Int @default(0)
-// }
-
model Form {
title String
choisesQuestions ChoisesQuestion[]
inputQuestions InputQuestion[]
submissions FormSubmission[]
dateCreated DateTime @default(now())
+ author User @relation(fields: [userId], references: [id])
- id Int @id
+ id Int @id @default(autoincrement())
userId Int
- author User @relation(fields: [userId], references: [id])
}
model ChoisesQuestion {
title String
variants Variant[]
type ChoiseType
number Int
- id Int @id
+ id Int @id @default(autoincrement())
Form Form? @relation(fields: [formId], references: [id])
formId Int?
}
model Variant {
text String
- id Int @id
+ id Int @id @default(autoincrement())
ChoisesQuestion ChoisesQuestion? @relation(fields: [choisesQuestionId], references: [id])
choisesQuestionId Int?
}
model InputQuestion {
title String
number Int
- id Int @id
+ id Int @id @default(autoincrement())
Form Form? @relation(fields: [formId], references: [id])
formId Int?
}
+enum ChoiseType {
+ SELECT
+ CHECK
+ CHOOSE
+}
+
+model User {
+ name String
+ forms Form[]
+
+ id Int @id @default(autoincrement())
+ formsSubmissions FormSubmission[]
+}
+
model FormSubmission {
answers Answer[]
date DateTime @default(now())
+ user User @relation(fields: [userId], references: [id])
-
- id Int @id
- user User @relation(fields: [userId], references: [id])
+ id Int @id @default(autoincrement())
+ userId Int
Form Form? @relation(fields: [formId], references: [id])
formId Int?
- userId Int
}
model Answer {
- id Int @id
- userInput String
- userChoise Int
- type AnswerType
+ userInput String
+ userChoise Int
+ type AnswerType
+
+ id Int @id @default(autoincrement())
FormSubmission FormSubmission? @relation(fields: [formSubmissionId], references: [id])
formSubmissionId Int?
}
-enum ChoiseType {
- SELECT
- CHECK
- CHOOSE
-}
-
enum AnswerType {
INPUT
CHOISE
}
-
-model User {
- id Int @id
- name String
-
- forms Form[]
- formsSubmissions FormSubmission[]
-}
```

View File

@ -0,0 +1,91 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
datasource db {
provider = "postgres"
url = "***"
}
generator client {
provider = "prisma-client-js"
}
model Form {
title String
choisesQuestions ChoisesQuestion[]
inputQuestions InputQuestion[]
submissions FormSubmission[]
dateCreated DateTime @default(now())
author User @relation(fields: [userId], references: [id])
id Int @id @default(autoincrement())
userId Int
}
model ChoisesQuestion {
title String
variants Variant[]
type ChoiseType
number Int
id Int @id @default(autoincrement())
Form Form? @relation(fields: [formId], references: [id])
formId Int?
}
model Variant {
text String
id Int @id @default(autoincrement())
ChoisesQuestion ChoisesQuestion? @relation(fields: [choisesQuestionId], references: [id])
choisesQuestionId Int?
}
model InputQuestion {
title String
number Int
id Int @id @default(autoincrement())
Form Form? @relation(fields: [formId], references: [id])
formId Int?
}
enum ChoiseType {
SELECT
CHECK
CHOOSE
}
model User {
name String
forms Form[]
id Int @id @default(autoincrement())
formsSubmissions FormSubmission[]
}
model FormSubmission {
answers Answer[]
date DateTime @default(now())
user User @relation(fields: [userId], references: [id])
id Int @id @default(autoincrement())
userId Int
Form Form? @relation(fields: [formId], references: [id])
formId Int?
}
model Answer {
userInput String
userChoise Int
type AnswerType
id Int @id @default(autoincrement())
FormSubmission FormSubmission? @relation(fields: [formSubmissionId], references: [id])
formSubmissionId Int?
}
enum AnswerType {
INPUT
CHOISE
}

View File

@ -0,0 +1,180 @@
{
"version": "0.3.14-fixed",
"steps": [
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Form",
"field": "id"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Form",
"field": "id"
},
"directive": "default"
},
"argument": "",
"value": "autoincrement()"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "ChoisesQuestion",
"field": "id"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "ChoisesQuestion",
"field": "id"
},
"directive": "default"
},
"argument": "",
"value": "autoincrement()"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Variant",
"field": "id"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Variant",
"field": "id"
},
"directive": "default"
},
"argument": "",
"value": "autoincrement()"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "InputQuestion",
"field": "id"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "InputQuestion",
"field": "id"
},
"directive": "default"
},
"argument": "",
"value": "autoincrement()"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "FormSubmission",
"field": "id"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "FormSubmission",
"field": "id"
},
"directive": "default"
},
"argument": "",
"value": "autoincrement()"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Answer",
"field": "id"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Answer",
"field": "id"
},
"directive": "default"
},
"argument": "",
"value": "autoincrement()"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "User",
"field": "id"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "User",
"field": "id"
},
"directive": "default"
},
"argument": "",
"value": "autoincrement()"
}
]
}

View File

@ -0,0 +1,42 @@
# Migration `20201007134933-fix-optional-values`
This migration has been generated by Dm1tr1y147 at 10/7/2020, 6:49:33 PM.
You can check out the [state of the schema](./schema.prisma) after the migration.
## Database Steps
```sql
ALTER TABLE "public"."Answer" ALTER COLUMN "userInput" DROP NOT NULL,
ALTER COLUMN "userChoise" DROP NOT NULL
```
## Changes
```diff
diff --git schema.prisma schema.prisma
migration 20201006185953-improved-schema-structure..20201007134933-fix-optional-values
--- datamodel.dml
+++ datamodel.dml
@@ -2,9 +2,9 @@
// learn more about it in the docs: https://pris.ly/d/prisma-schema
datasource db {
provider = "postgres"
- url = "***"
+ url = "***"
}
generator client {
provider = "prisma-client-js"
@@ -75,10 +75,10 @@
formId Int?
}
model Answer {
- userInput String
- userChoise Int
+ userInput String?
+ userChoise Int?
type AnswerType
id Int @id @default(autoincrement())
FormSubmission FormSubmission? @relation(fields: [formSubmissionId], references: [id])
```

View File

@ -0,0 +1,91 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
datasource db {
provider = "postgres"
url = "***"
}
generator client {
provider = "prisma-client-js"
}
model Form {
title String
choisesQuestions ChoisesQuestion[]
inputQuestions InputQuestion[]
submissions FormSubmission[]
dateCreated DateTime @default(now())
author User @relation(fields: [userId], references: [id])
id Int @id @default(autoincrement())
userId Int
}
model ChoisesQuestion {
title String
variants Variant[]
type ChoiseType
number Int
id Int @id @default(autoincrement())
Form Form? @relation(fields: [formId], references: [id])
formId Int?
}
model Variant {
text String
id Int @id @default(autoincrement())
ChoisesQuestion ChoisesQuestion? @relation(fields: [choisesQuestionId], references: [id])
choisesQuestionId Int?
}
model InputQuestion {
title String
number Int
id Int @id @default(autoincrement())
Form Form? @relation(fields: [formId], references: [id])
formId Int?
}
enum ChoiseType {
SELECT
CHECK
CHOOSE
}
model User {
name String
forms Form[]
id Int @id @default(autoincrement())
formsSubmissions FormSubmission[]
}
model FormSubmission {
answers Answer[]
date DateTime @default(now())
user User @relation(fields: [userId], references: [id])
id Int @id @default(autoincrement())
userId Int
Form Form? @relation(fields: [formId], references: [id])
formId Int?
}
model Answer {
userInput String?
userChoise Int?
type AnswerType
id Int @id @default(autoincrement())
FormSubmission FormSubmission? @relation(fields: [formSubmissionId], references: [id])
formSubmissionId Int?
}
enum AnswerType {
INPUT
CHOISE
}

View File

@ -0,0 +1,17 @@
{
"version": "0.3.14-fixed",
"steps": [
{
"tag": "UpdateField",
"model": "Answer",
"field": "userInput",
"arity": "Optional"
},
{
"tag": "UpdateField",
"model": "Answer",
"field": "userChoise",
"arity": "Optional"
}
]
}

View File

@ -0,0 +1,40 @@
# Migration `20201009145620-add-user-email`
This migration has been generated by Dm1tr1y147 at 10/9/2020, 7:56:20 PM.
You can check out the [state of the schema](./schema.prisma) after the migration.
## Database Steps
```sql
ALTER TABLE "public"."User" ADD COLUMN "email" text NOT NULL DEFAULT E'test@mail.com'
CREATE UNIQUE INDEX "User.email_unique" ON "public"."User"("email")
```
## Changes
```diff
diff --git schema.prisma schema.prisma
migration 20201007134933-fix-optional-values..20201009145620-add-user-email
--- datamodel.dml
+++ datamodel.dml
@@ -2,9 +2,9 @@
// learn more about it in the docs: https://pris.ly/d/prisma-schema
datasource db {
provider = "postgres"
- url = "***"
+ url = "***"
}
generator client {
provider = "prisma-client-js"
@@ -57,8 +57,9 @@
}
model User {
name String
+ email String @unique @default("test@mail.com")
forms Form[]
id Int @id @default(autoincrement())
formsSubmissions FormSubmission[]
```

View File

@ -0,0 +1,92 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
datasource db {
provider = "postgres"
url = "***"
}
generator client {
provider = "prisma-client-js"
}
model Form {
title String
choisesQuestions ChoisesQuestion[]
inputQuestions InputQuestion[]
submissions FormSubmission[]
dateCreated DateTime @default(now())
author User @relation(fields: [userId], references: [id])
id Int @id @default(autoincrement())
userId Int
}
model ChoisesQuestion {
title String
variants Variant[]
type ChoiseType
number Int
id Int @id @default(autoincrement())
Form Form? @relation(fields: [formId], references: [id])
formId Int?
}
model Variant {
text String
id Int @id @default(autoincrement())
ChoisesQuestion ChoisesQuestion? @relation(fields: [choisesQuestionId], references: [id])
choisesQuestionId Int?
}
model InputQuestion {
title String
number Int
id Int @id @default(autoincrement())
Form Form? @relation(fields: [formId], references: [id])
formId Int?
}
enum ChoiseType {
SELECT
CHECK
CHOOSE
}
model User {
name String
email String @unique @default("test@mail.com")
forms Form[]
id Int @id @default(autoincrement())
formsSubmissions FormSubmission[]
}
model FormSubmission {
answers Answer[]
date DateTime @default(now())
user User @relation(fields: [userId], references: [id])
id Int @id @default(autoincrement())
userId Int
Form Form? @relation(fields: [formId], references: [id])
formId Int?
}
model Answer {
userInput String?
userChoise Int?
type AnswerType
id Int @id @default(autoincrement())
FormSubmission FormSubmission? @relation(fields: [formSubmissionId], references: [id])
formSubmissionId Int?
}
enum AnswerType {
INPUT
CHOISE
}

View File

@ -0,0 +1,48 @@
{
"version": "0.3.14-fixed",
"steps": [
{
"tag": "CreateField",
"model": "User",
"field": "email",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "User",
"field": "email"
},
"directive": "unique"
}
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "User",
"field": "email"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "User",
"field": "email"
},
"directive": "default"
},
"argument": "",
"value": "\"test@mail.com\""
}
]
}

View File

@ -1,3 +1,6 @@
# Prisma Migrate lockfile v1
20201003131039-initial-structure
20201006125838-initial-migration
20201006185953-improved-schema-structure
20201007134933-fix-optional-values
20201009145620-add-user-email

View File

@ -2,45 +2,91 @@
// learn more about it in the docs: https://pris.ly/d/prisma-schema
datasource db {
provider = "sqlite"
url = "file:./dev.db"
provider = "postgres"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id Int @id @default(autoincrement())
name String
email String?
createdAt DateTime @default(now())
polls Poll[]
}
model Poll {
id Int @id @default(autoincrement())
User User? @relation(fields: [userId], references: [id])
userId Int?
model Form {
title String
description String?
slug String @unique @default(cuid())
questions Question[]
createdAt DateTime @default(now())
choisesQuestions ChoisesQuestion[]
inputQuestions InputQuestion[]
submissions FormSubmission[]
dateCreated DateTime @default(now())
author User @relation(fields: [userId], references: [id])
id Int @id @default(autoincrement())
userId Int
}
model Question {
id Int @id @default(autoincrement())
Poll Poll? @relation(fields: [pollId], references: [id])
pollId Int?
model ChoisesQuestion {
title String
variants Variant[]
type ChoiseType
number Int
id Int @id @default(autoincrement())
Form Form? @relation(fields: [formId], references: [id])
formId Int?
}
model Variant {
id Int @id @default(autoincrement())
Question Question? @relation(fields: [questionId], references: [id])
questionId Int?
text String
count Int @default(0)
id Int @id @default(autoincrement())
ChoisesQuestion ChoisesQuestion? @relation(fields: [choisesQuestionId], references: [id])
choisesQuestionId Int?
}
model InputQuestion {
title String
number Int
id Int @id @default(autoincrement())
Form Form? @relation(fields: [formId], references: [id])
formId Int?
}
enum ChoiseType {
SELECT
CHECK
CHOOSE
}
model User {
name String
email String @unique @default("test@mail.com")
forms Form[]
id Int @id @default(autoincrement())
formsSubmissions FormSubmission[]
}
model FormSubmission {
answers Answer[]
date DateTime @default(now())
user User @relation(fields: [userId], references: [id])
id Int @id @default(autoincrement())
userId Int
Form Form? @relation(fields: [formId], references: [id])
formId Int?
}
model Answer {
userInput String?
userChoise Int?
type AnswerType
id Int @id @default(autoincrement())
FormSubmission FormSubmission? @relation(fields: [formSubmissionId], references: [id])
formSubmissionId Int?
}
enum AnswerType {
INPUT
CHOISE
}

56
src/controllers/auth.ts Normal file
View File

@ -0,0 +1,56 @@
import jwt from 'jsonwebtoken'
import {
ApolloError,
AuthenticationError,
ForbiddenError
} from 'apollo-server-express'
import { PrismaClient } from '@prisma/client'
require('dotenv').config()
import { CheckRightsAndResolve } from './types'
import { getDBFormAuthor } from '../db'
import { sendToken } from './mailer'
const checkRightsAndResolve: CheckRightsAndResolve = async (params) => {
const { user, expected, controller } = params
if (!user) throw new AuthenticationError('Authorization required')
if (expected.id && expected.self) return controller(expected.id || user.id)
if (expected.self) return controller(user.id)
if (!expected.id || user.id == expected.id) return controller()
throw new ForbiddenError('Access denied')
}
const getFormAuthor = async (db: PrismaClient, id: number) => {
const author = await getDBFormAuthor(db, id)
if (!author) throw new ApolloError('Not found', 'NOTFOUND')
const authorId = author.author.id
return authorId
}
const tokenGenerate = (email: string, id: number) => {
return jwt.sign({ email, id }, '' + process.env.JWT_SECRET, {
algorithm: 'HS256',
expiresIn: '7 days'
})
}
const genAndSendToken = async (
email: string,
user: { id: number; name: string }
) => {
const token = tokenGenerate(email, user.id)
const res = await sendToken(user.name, email, token)
if (res[0].statusCode != 202)
return new ApolloError("Couldn't send email", 'EMAILSENDERROR')
}
export { checkRightsAndResolve, getFormAuthor, genAndSendToken }

145
src/controllers/form.ts Normal file
View File

@ -0,0 +1,145 @@
import { Answer as DbAnswer, PrismaClient } from '@prisma/client'
import { ApolloError, UserInputError } from 'apollo-server-express'
import {
Form,
Form as GraphqlForm,
InputQuestion,
MutationCreateFormArgs,
MutationFormSubmitArgs,
ServerAnswer
} from '../typeDefs/typeDefs.gen'
import {
CreateChoises,
FormConstructor,
UploadedChoisesQuestion,
UploadedInputQuestion,
UploadedQuestion
} from './types'
import {
createDBForm,
getDBForm,
getDBFormsByUser,
submitDBAnswer
} from '../db'
const getForm = async (
db: PrismaClient,
id: number,
user: { requesterId: number; userId: number }
): Promise<Form> => {
try {
const dbForm = await getDBForm(db, id, user)
if (!dbForm) throw new ApolloError('Not found', 'NOTFOUND')
const form: GraphqlForm = {
author: dbForm.author,
dateCreated: dbForm.dateCreated.toString(),
id: dbForm.id,
questions: [...dbForm.choisesQuestions, ...dbForm.inputQuestions],
submissions: dbForm.submissions.map((submission) => ({
answers: submission.answers,
date: submission.date.toString(),
id: submission.id
})),
title: dbForm.title
}
return form
} catch (err) {
return err
}
}
const getForms = async (db: PrismaClient, userId: number): Promise<Form[]> => {
try {
const dbForms = await getDBFormsByUser(db, userId)
if (!dbForms) throw new ApolloError("Couldn't load forms", 'FETCHINGERROR')
const forms: Form[] = dbForms.map((form) => ({
dateCreated: form.dateCreated.toString(),
id: form.id,
questions: [...form.choisesQuestions, ...form.inputQuestions],
submissions: form.submissions.map((submission) => ({
answers: submission.answers,
date: submission.date.toString(),
id: submission.id
})),
title: form.title
}))
return forms
} catch (err) {
return err
}
}
const createFormFrom = async (
db: PrismaClient,
params: MutationCreateFormArgs,
id: number
): Promise<ServerAnswer> => {
try {
const parsedQuestions = <UploadedQuestion[]>JSON.parse(params.questions)
const newForm: FormConstructor = {
choisesQuestions: {
create: parsedQuestions.flatMap<CreateChoises>(
(uQuestion: UploadedChoisesQuestion | UploadedInputQuestion, index) =>
'type' in uQuestion
? [
{
number: index,
title: uQuestion.title,
type: uQuestion.type,
variants: {
create: uQuestion.variants
}
}
]
: []
)
},
inputQuestions: {
create: parsedQuestions.flatMap<InputQuestion>(
(uQuestion: UploadedChoisesQuestion | UploadedInputQuestion, index) =>
!('type' in uQuestion)
? [{ number: index, title: uQuestion.title }]
: []
)
},
title: params.title
}
const res = await createDBForm(db, newForm, id)
if (!res)
throw new ApolloError("Couldn't create new form", 'FORMCREATIONERROR')
return { success: true }
} catch (err) {
return err
}
}
const submitAnswer = async (
db: PrismaClient,
{ answers, formId }: MutationFormSubmitArgs,
userId: number
): Promise<ServerAnswer> => {
try {
const parsedAnswers = <DbAnswer[]>JSON.parse(answers)
const res = await submitDBAnswer(db, userId, formId, parsedAnswers)
if (!res) throw new UserInputError("Can't submit form")
return { success: true }
} catch (err) {
return err
}
}
export { createFormFrom, getForm, getForms, submitAnswer }

14
src/controllers/index.ts Normal file
View File

@ -0,0 +1,14 @@
import { checkRightsAndResolve, getFormAuthor, genAndSendToken } from './auth'
import { createFormFrom, getForm, getForms, submitAnswer } from './form'
import { findUserBy } from './user'
export {
checkRightsAndResolve,
createFormFrom,
findUserBy,
genAndSendToken,
getForm,
getFormAuthor,
getForms,
submitAnswer
}

21
src/controllers/mailer.ts Normal file
View File

@ -0,0 +1,21 @@
import sgMail from '@sendgrid/mail'
require('dotenv').config()
sgMail.setApiKey('' + process.env.SENDGRID_API_KEY)
const sendToken = (username: string, email: string, token: string) => {
return sgMail.send({
dynamicTemplateData: {
siteUrl: process.env.SITE_URL,
token: token,
username: username
},
from: 'me@dmitriy.icu',
subject: 'Login link',
templateId: 'd-a9275a4437bf4dd2b9e858f3a57f85d5',
to: email
})
}
export { sendToken }

52
src/controllers/types.ts Normal file
View File

@ -0,0 +1,52 @@
import { ChoiseType } from '@prisma/client'
import {
ChoisesQuestion,
InputQuestion,
Variant
} from '../typeDefs/typeDefs.gen'
import { JwtPayloadType } from '../types'
type ExpectedType = {
id: number
self: boolean
}
interface ICheckRightsAndResolve<T> {
controller: T
expected: ExpectedType
user: JwtPayloadType | null
}
type CheckRightsAndResolve = <ReturnType, ControllerType extends Function>(
params: ICheckRightsAndResolve<ControllerType>
) => Promise<ReturnType>
type FormConstructor = {
choisesQuestions: { create: CreateChoises[] }
inputQuestions: { create: InputQuestion[] }
title: string
}
type CreateChoises = Omit<ChoisesQuestion, 'variants'> & {
variants: { create: Variant[] }
}
type UploadedQuestion = {
title: string
}
type UploadedChoisesQuestion = UploadedQuestion & {
type: ChoiseType
variants: Variant[]
}
type UploadedInputQuestion = UploadedQuestion
export {
CheckRightsAndResolve,
CreateChoises,
FormConstructor,
UploadedChoisesQuestion,
UploadedInputQuestion,
UploadedQuestion
}

44
src/controllers/user.ts Normal file
View File

@ -0,0 +1,44 @@
import { createDBUser, findDBUserBy } from '../db'
import { IFindUserParams } from '../db/types'
import { MutationRegisterArgs, User } from '../typeDefs/typeDefs.gen'
import { PrismaClient } from '@prisma/client'
import { ApolloError, UserInputError } from 'apollo-server-express'
const createUser = async (
db: PrismaClient,
{ email, name }: MutationRegisterArgs
): Promise<User> => {
try {
if (!email || !name)
throw new UserInputError(
'Provide full user information',
[!email ? [email] : [], !name ? [name] : []].flat()
)
const newUser = await createDBUser(db, { email, name })
if (!newUser)
throw new ApolloError("Couldn't create user", 'USERCREATIONERROR')
return newUser
} catch (err) {
return err
}
}
const findUserBy = async (
db: PrismaClient,
params: IFindUserParams
): Promise<User> => {
try {
const user = await findDBUserBy(db, params)
if (!user) throw new UserInputError('No such user')
return user
} catch (err) {
return err
}
}
export { createUser, findUserBy }

162
src/db/index.ts Normal file
View File

@ -0,0 +1,162 @@
import { PrismaClient } from '@prisma/client'
import { Answer, MutationRegisterArgs } from '../typeDefs/typeDefs.gen'
import { IFindUserParams } from './types'
import { FormConstructor } from '../controllers/types'
/**
* Get form from DataBase
*
* @async
* @param db {PrismaClient} Prisma client object
* @param formId {number} Form ID
* @param getSubmissions {boolean} Set to true if want to also get form submissions
* @example
* const form = await getDBForm(db, id, true)
*/
const getDBForm = (
db: PrismaClient,
formId: number,
{
requesterId,
userId
}: {
requesterId: number
userId: number
}
) =>
db.form.findOne({
include: {
author: {
select: {
email: true,
id: true,
name: true
}
},
choisesQuestions: {
include: {
variants: true
}
},
inputQuestions: true,
submissions: {
include: {
answers: true
},
where:
requesterId != userId
? {
user: {
id: requesterId
}
}
: undefined
}
},
where: {
id: formId
}
})
/**
* Get all forms of user
* @param db {PrismaClient} Prisma client object
* @param id {number} User ID
* @example
* const forms = await getDBFormsByUser(db, userId)
*/
const getDBFormsByUser = (db: PrismaClient, id: number) =>
db.form.findMany({
include: {
choisesQuestions: {
include: {
variants: true
}
},
inputQuestions: true,
submissions: {
include: {
answers: true
}
}
},
where: {
author: {
id
}
}
})
const getDBFormAuthor = (db: PrismaClient, id: number) =>
db.form.findOne({
select: {
author: {
select: {
id: true
}
}
},
where: {
id
}
})
const createDBUser = (
db: PrismaClient,
{ email, name }: MutationRegisterArgs
) =>
db.user.create({
data: { email, name }
})
const findDBUserBy = (db: PrismaClient, params: IFindUserParams) =>
db.user.findOne({
where: {
...params
}
})
const createDBForm = (db: PrismaClient, form: FormConstructor, id: number) =>
db.form.create({
data: {
author: {
connect: { id }
},
...form
}
})
const submitDBAnswer = (
db: PrismaClient,
userId: number,
formId: number,
formAnswers: Answer[]
) =>
db.formSubmission.create({
data: {
answers: {
create: formAnswers
},
Form: {
connect: {
id: formId
}
},
user: {
connect: {
id: userId
}
}
}
})
export {
createDBForm,
createDBUser,
findDBUserBy,
getDBForm,
getDBFormAuthor,
getDBFormsByUser,
submitDBAnswer
}

12
src/db/types.ts Normal file
View File

@ -0,0 +1,12 @@
import { PromiseReturnType } from '@prisma/client'
import { getDBForm } from '../db'
type FullForm = PromiseReturnType<typeof getDBForm>
interface IFindUserParams {
email?: string
id?: number
}
export { FullForm, IFindUserParams }

View File

@ -1,14 +1,47 @@
import { GraphQLServer } from "graphql-yoga"
import express from 'express'
import expressJwt from 'express-jwt'
import resolvers from './resolvers'
import typeDefs from './typeDefs'
import { ApolloContextType, JwtPayloadType } from './types'
import { ApolloServer, makeExecutableSchema } from 'apollo-server-express'
import { PrismaClient } from '@prisma/client'
const typeDefs = ``
require('dotenv').config()
const resolvers = {}
const app = express()
const server = new GraphQLServer({
typeDefs,
app.use(
expressJwt({
algorithms: ['HS256'],
credentialsRequired: false,
secret: '' + process.env.JWT_SECRET
})
)
const server = new ApolloServer({
context: async ({
req
}: {
req: Request & {
user: JwtPayloadType
}
}): Promise<ApolloContextType> => {
const db = new PrismaClient()
const user = req.user || null
return {
db,
user
}
},
debug: false,
schema: makeExecutableSchema({
resolvers,
typeDefs
})
})
server.start(({ port }) =>
console.log(`Server is running on http://localhost:${port}`)
)
server.applyMiddleware({ app })
app.listen(4000, () => {
console.log('Server ready at http://localhost:4000')
})

View File

@ -1,5 +0,0 @@
[
{
}
]

130
src/resolvers/Form.ts Normal file
View File

@ -0,0 +1,130 @@
import {
AnswerResolvers,
Form,
MutationCreateFormArgs,
MutationFormSubmitArgs,
QueryFormArgs,
QuestionResolvers,
Resolver,
ServerAnswer
} from '../typeDefs/typeDefs.gen'
import { ApolloContextType } from '../types'
import {
checkRightsAndResolve,
createFormFrom,
getForm,
getFormAuthor,
getForms,
submitAnswer
} from '../controllers'
const formQuery: Resolver<Form, {}, ApolloContextType, QueryFormArgs> = async (
_,
{ id },
{ db, user }
) => {
try {
const ownerId = await getFormAuthor(db, id)
const getFormById = (userId: number) =>
getForm(db, id, { requesterId: userId, userId: ownerId })
return await checkRightsAndResolve({
controller: getFormById,
expected: {
id: 0,
self: true
},
user
})
} catch (err) {
return err
}
}
const formsQuery: Resolver<Form[], {}, ApolloContextType> = async (
_,
__,
{ db, user }
) => {
try {
const getFormsByUserId = (userId: number) => getForms(db, userId)
return await checkRightsAndResolve<
Form[],
(userId: number) => Promise<Form[] | null>
>({
controller: getFormsByUserId,
expected: {
id: 0,
self: true
},
user
})
} catch (err) {
return err
}
}
const createFormMutation: Resolver<
ServerAnswer,
{},
ApolloContextType,
MutationCreateFormArgs
> = async (_, params, { db, user }) => {
const createNewForm = (id: number) => createFormFrom(db, params, id)
return await checkRightsAndResolve({
controller: createNewForm,
expected: {
id: 0,
self: true
},
user
})
}
const formSubmitMutation: Resolver<
ServerAnswer,
{},
ApolloContextType,
MutationFormSubmitArgs
> = async (_, params, { db, user }) => {
const submitNewAnswer = (userId: number) => submitAnswer(db, params, userId)
return await checkRightsAndResolve({
controller: submitNewAnswer,
expected: {
id: 0,
self: true
},
user
})
}
const QuestionResolver: QuestionResolvers = {
__resolveType(obj: any) {
if (obj.type) {
return 'ChoisesQuestion'
}
return 'InputQuestion'
}
}
const AnswerResolver: AnswerResolvers = {
__resolveType(obj) {
if (obj.type == 'CHOISE') return 'ChoiseAnswer'
if (obj.type == 'INPUT') return 'InputAnswer'
return null
}
}
export {
AnswerResolver,
createFormMutation,
formQuery,
formsQuery,
formSubmitMutation,
QuestionResolver
}

72
src/resolvers/User.ts Normal file
View File

@ -0,0 +1,72 @@
import {
checkRightsAndResolve,
findUserBy,
genAndSendToken
} from '../controllers'
import { createUser } from '../controllers/user'
import {
MutationLoginArgs,
MutationRegisterArgs,
Resolver,
ServerAnswer,
User,
QueryUserArgs
} from '../typeDefs/typeDefs.gen'
import { ApolloContextType } from '../types'
const loginMutation: Resolver<
ServerAnswer,
{},
ApolloContextType,
MutationLoginArgs
> = async (_, { email }, { db }) => {
try {
const user = await findUserBy(db, { email })
await genAndSendToken(email, user)
return { success: true }
} catch (err) {
return err
}
}
const registerMutation: Resolver<
ServerAnswer,
{},
ApolloContextType,
MutationRegisterArgs
> = async (_, { email, name }, { db }) => {
try {
const user = await createUser(db, { email, name })
await genAndSendToken(email, user)
return { success: true }
} catch (err) {
return err
}
}
const userQuery: Resolver<User, {}, ApolloContextType, QueryUserArgs> = async (
_,
{ id },
{ db, user }
) => {
try {
const findUserById = (id: number) => findUserBy(db, { id })
return await checkRightsAndResolve({
controller: findUserById,
expected: {
id: id || 0,
self: true
},
user
})
} catch (err) {
return err
}
}
export { loginMutation, registerMutation, userQuery }

32
src/resolvers/index.ts Normal file
View File

@ -0,0 +1,32 @@
import { Resolvers } from '../typeDefs/typeDefs.gen'
import {
formQuery as form,
QuestionResolver as Question,
AnswerResolver as Answer,
formsQuery as forms,
createFormMutation as createForm,
formSubmitMutation as formSubmit
} from './Form'
import {
loginMutation as login,
registerMutation as register,
userQuery as user
} from './User'
const resolvers: Resolvers = {
Query: {
form,
forms,
user
},
Mutation: {
login,
register,
createForm,
formSubmit
},
Question,
Answer
}
export default resolvers

8
src/typeDefs/index.ts Normal file
View File

@ -0,0 +1,8 @@
import fs from 'fs'
import { gql } from 'apollo-server-express'
const typeDefs = gql(
fs.readFileSync(__dirname.concat('/typeDefs.gql'), { encoding: 'utf-8' })
)
export default typeDefs

84
src/typeDefs/typeDefs.gql Normal file
View File

@ -0,0 +1,84 @@
type Query {
form(id: Int!): Form
forms: [Form!]!
user(id: Int): User
}
type Mutation {
createForm(title: String!, questions: String!): serverAnswer
formSubmit(formId: Int!, answers: String!): serverAnswer
login(email: String!): serverAnswer
register(name: String!, email: String!): serverAnswer
}
type Form {
author: User
dateCreated: String!
id: Int!
questions: [Question!]!
submissions: [FormSubmission!]
title: String!
}
interface Question {
number: Int!
title: String!
}
type ChoisesQuestion implements Question {
number: Int!
title: String!
type: ChoiseType!
variants: [Variant!]!
}
type Variant {
text: String!
}
type InputQuestion implements Question {
number: Int!
title: String!
}
type FormSubmission {
answers: [Answer!]!
date: String!
id: Int!
}
interface Answer {
type: AnswerType!
}
type InputAnswer implements Answer {
type: AnswerType!
userInput: String
}
type ChoiseAnswer implements Answer {
type: AnswerType!
userChoise: Int!
}
enum ChoiseType {
CHECK
CHOOSE
SELECT
}
enum AnswerType {
CHOISE
INPUT
}
type User {
email: String!
forms: [Form!]
id: Int!
name: String!
}
type serverAnswer {
success: Boolean!
}

11
src/types.ts Normal file
View File

@ -0,0 +1,11 @@
import { PrismaClient } from '@prisma/client'
export type ApolloContextType = {
db: PrismaClient
user: JwtPayloadType | null
}
export type JwtPayloadType = {
id: number
email: string
}

14
tsconfig.json Normal file
View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
"module": "CommonJS" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
"strict": true /* Enable all strict type-checking options. */,
"outDir": "dist",
"moduleResolution": "Node",
"incremental": true,
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
"skipLibCheck": true /* Skip type checking of declaration files. */,
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
},
"include": ["src"]
}