12 Commits

41 changed files with 3050 additions and 119 deletions

3
.env.example Normal file
View File

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

6
.gitignore vendored
View File

@ -1,8 +1,10 @@
/node_modules
/build
/dist
.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.

27
README.md Normal file
View File

@ -0,0 +1,27 @@
# QuestionForm Backend
Backend for QuestionForm application.
# Built with:
- Prisma
- Graphql
- 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"
}

View File

@ -1,21 +1,37 @@
{
"name": "backend",
"version": "1.0.0",
"main": "index.js",
"repository": "https://github.com/Dm1tr1y147/questionForm_backend.git",
"author": "Dm1tr1y147 <me@dmitriy.icu>",
"main": "src/index.ts",
"private": "true",
"license": "MIT",
"devDependencies": {
"@types/express": "^4.17.8",
"@types/mongoose": "^5.7.36",
"@types/node": "^14.11.2",
"ts-node-dev": "^1.0.0-pre.63",
"typescript": "^4.0.3"
"dependencies": {
"@prisma/client": "^2.7.1",
"@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": {
"start": "ts-node-dev src/main.ts"
"dev": "nodemon",
"start": "ts-node src/index.ts",
"copy-assets": "cp src/typeDefs/typeDefs.gql dist/typeDefs/typeDefs.gql && cp .env.example dist/.env && vi dist/.env",
"build": "tsc && npm copy-assets",
"codegen": "graphql-codegen --config codegen.yml",
"lint": "eslint",
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"mongoose": "^5.10.7"
"devDependencies": {
"@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://"

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,759 @@
{
"version": "0.3.14-fixed",
"steps": [
{
"tag": "CreateEnum",
"enum": "ChoiseType",
"values": [
"SELECT",
"CHECK",
"CHOOSE"
]
},
{
"tag": "CreateEnum",
"enum": "AnswerType",
"values": [
"INPUT",
"CHOISE"
]
},
{
"tag": "CreateSource",
"source": "db"
},
{
"tag": "CreateArgument",
"location": {
"tag": "Source",
"source": "db"
},
"argument": "provider",
"value": "\"postgres\""
},
{
"tag": "CreateArgument",
"location": {
"tag": "Source",
"source": "db"
},
"argument": "url",
"value": "\"***\""
},
{
"tag": "CreateModel",
"model": "Form"
},
{
"tag": "CreateField",
"model": "Form",
"field": "title",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateField",
"model": "Form",
"field": "choisesQuestions",
"type": "ChoisesQuestion",
"arity": "List"
},
{
"tag": "CreateField",
"model": "Form",
"field": "inputQuestions",
"type": "InputQuestion",
"arity": "List"
},
{
"tag": "CreateField",
"model": "Form",
"field": "submissions",
"type": "FormSubmission",
"arity": "List"
},
{
"tag": "CreateField",
"model": "Form",
"field": "dateCreated",
"type": "DateTime",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Form",
"field": "dateCreated"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Form",
"field": "dateCreated"
},
"directive": "default"
},
"argument": "",
"value": "now()"
},
{
"tag": "CreateField",
"model": "Form",
"field": "id",
"type": "Int",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Form",
"field": "id"
},
"directive": "id"
}
},
{
"tag": "CreateField",
"model": "Form",
"field": "userId",
"type": "Int",
"arity": "Required"
},
{
"tag": "CreateField",
"model": "Form",
"field": "author",
"type": "User",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Form",
"field": "author"
},
"directive": "relation"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Form",
"field": "author"
},
"directive": "relation"
},
"argument": "fields",
"value": "[userId]"
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Form",
"field": "author"
},
"directive": "relation"
},
"argument": "references",
"value": "[id]"
},
{
"tag": "CreateModel",
"model": "ChoisesQuestion"
},
{
"tag": "CreateField",
"model": "ChoisesQuestion",
"field": "title",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateField",
"model": "ChoisesQuestion",
"field": "variants",
"type": "Variant",
"arity": "List"
},
{
"tag": "CreateField",
"model": "ChoisesQuestion",
"field": "type",
"type": "ChoiseType",
"arity": "Required"
},
{
"tag": "CreateField",
"model": "ChoisesQuestion",
"field": "number",
"type": "Int",
"arity": "Required"
},
{
"tag": "CreateField",
"model": "ChoisesQuestion",
"field": "id",
"type": "Int",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "ChoisesQuestion",
"field": "id"
},
"directive": "id"
}
},
{
"tag": "CreateField",
"model": "ChoisesQuestion",
"field": "Form",
"type": "Form",
"arity": "Optional"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "ChoisesQuestion",
"field": "Form"
},
"directive": "relation"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "ChoisesQuestion",
"field": "Form"
},
"directive": "relation"
},
"argument": "fields",
"value": "[formId]"
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "ChoisesQuestion",
"field": "Form"
},
"directive": "relation"
},
"argument": "references",
"value": "[id]"
},
{
"tag": "CreateField",
"model": "ChoisesQuestion",
"field": "formId",
"type": "Int",
"arity": "Optional"
},
{
"tag": "CreateModel",
"model": "Variant"
},
{
"tag": "CreateField",
"model": "Variant",
"field": "text",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateField",
"model": "Variant",
"field": "id",
"type": "Int",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Variant",
"field": "id"
},
"directive": "id"
}
},
{
"tag": "CreateField",
"model": "Variant",
"field": "ChoisesQuestion",
"type": "ChoisesQuestion",
"arity": "Optional"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Variant",
"field": "ChoisesQuestion"
},
"directive": "relation"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Variant",
"field": "ChoisesQuestion"
},
"directive": "relation"
},
"argument": "fields",
"value": "[choisesQuestionId]"
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Variant",
"field": "ChoisesQuestion"
},
"directive": "relation"
},
"argument": "references",
"value": "[id]"
},
{
"tag": "CreateField",
"model": "Variant",
"field": "choisesQuestionId",
"type": "Int",
"arity": "Optional"
},
{
"tag": "CreateModel",
"model": "InputQuestion"
},
{
"tag": "CreateField",
"model": "InputQuestion",
"field": "title",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateField",
"model": "InputQuestion",
"field": "number",
"type": "Int",
"arity": "Required"
},
{
"tag": "CreateField",
"model": "InputQuestion",
"field": "id",
"type": "Int",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "InputQuestion",
"field": "id"
},
"directive": "id"
}
},
{
"tag": "CreateField",
"model": "InputQuestion",
"field": "Form",
"type": "Form",
"arity": "Optional"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "InputQuestion",
"field": "Form"
},
"directive": "relation"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "InputQuestion",
"field": "Form"
},
"directive": "relation"
},
"argument": "fields",
"value": "[formId]"
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "InputQuestion",
"field": "Form"
},
"directive": "relation"
},
"argument": "references",
"value": "[id]"
},
{
"tag": "CreateField",
"model": "InputQuestion",
"field": "formId",
"type": "Int",
"arity": "Optional"
},
{
"tag": "CreateModel",
"model": "FormSubmission"
},
{
"tag": "CreateField",
"model": "FormSubmission",
"field": "answers",
"type": "Answer",
"arity": "List"
},
{
"tag": "CreateField",
"model": "FormSubmission",
"field": "date",
"type": "DateTime",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "FormSubmission",
"field": "date"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "FormSubmission",
"field": "date"
},
"directive": "default"
},
"argument": "",
"value": "now()"
},
{
"tag": "CreateField",
"model": "FormSubmission",
"field": "id",
"type": "Int",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "FormSubmission",
"field": "id"
},
"directive": "id"
}
},
{
"tag": "CreateField",
"model": "FormSubmission",
"field": "user",
"type": "User",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "FormSubmission",
"field": "user"
},
"directive": "relation"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "FormSubmission",
"field": "user"
},
"directive": "relation"
},
"argument": "fields",
"value": "[userId]"
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "FormSubmission",
"field": "user"
},
"directive": "relation"
},
"argument": "references",
"value": "[id]"
},
{
"tag": "CreateField",
"model": "FormSubmission",
"field": "Form",
"type": "Form",
"arity": "Optional"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "FormSubmission",
"field": "Form"
},
"directive": "relation"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "FormSubmission",
"field": "Form"
},
"directive": "relation"
},
"argument": "fields",
"value": "[formId]"
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "FormSubmission",
"field": "Form"
},
"directive": "relation"
},
"argument": "references",
"value": "[id]"
},
{
"tag": "CreateField",
"model": "FormSubmission",
"field": "formId",
"type": "Int",
"arity": "Optional"
},
{
"tag": "CreateField",
"model": "FormSubmission",
"field": "userId",
"type": "Int",
"arity": "Required"
},
{
"tag": "CreateModel",
"model": "Answer"
},
{
"tag": "CreateField",
"model": "Answer",
"field": "id",
"type": "Int",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Answer",
"field": "id"
},
"directive": "id"
}
},
{
"tag": "CreateField",
"model": "Answer",
"field": "userInput",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateField",
"model": "Answer",
"field": "userChoise",
"type": "Int",
"arity": "Required"
},
{
"tag": "CreateField",
"model": "Answer",
"field": "type",
"type": "AnswerType",
"arity": "Required"
},
{
"tag": "CreateField",
"model": "Answer",
"field": "FormSubmission",
"type": "FormSubmission",
"arity": "Optional"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Answer",
"field": "FormSubmission"
},
"directive": "relation"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Answer",
"field": "FormSubmission"
},
"directive": "relation"
},
"argument": "fields",
"value": "[formSubmissionId]"
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Answer",
"field": "FormSubmission"
},
"directive": "relation"
},
"argument": "references",
"value": "[id]"
},
{
"tag": "CreateField",
"model": "Answer",
"field": "formSubmissionId",
"type": "Int",
"arity": "Optional"
},
{
"tag": "CreateModel",
"model": "User"
},
{
"tag": "CreateField",
"model": "User",
"field": "id",
"type": "Int",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "User",
"field": "id"
},
"directive": "id"
}
},
{
"tag": "CreateField",
"model": "User",
"field": "name",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateField",
"model": "User",
"field": "forms",
"type": "Form",
"arity": "List"
},
{
"tag": "CreateField",
"model": "User",
"field": "formsSubmissions",
"type": "FormSubmission",
"arity": "List"
}
]
}

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

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

92
prisma/schema.prisma Normal file
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 = env("DATABASE_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
}

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 }

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

@ -0,0 +1,171 @@
import { Answer as DbAnswer, PrismaClient, Form } from '@prisma/client'
import { ApolloError, UserInputError } from 'apollo-server-express'
import {
ChoisesQuestion,
Form as GraphqlForm,
FormSubmission,
InputQuestion,
MutationCreateFormArgs,
MutationFormSubmitArgs,
ServerAnswer,
Variant,
} 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<GraphqlForm> => {
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<GraphqlForm[]> => {
try {
const dbForms = await getDBFormsByUser(db, userId)
if (!dbForms) throw new ApolloError("Couldn't load forms", 'FETCHINGERROR')
const forms: GraphqlForm[] = 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
}
}
const formatForms = (
forms: (Form & {
choisesQuestions: (ChoisesQuestion & {
variants: Variant[]
})[]
inputQuestions: InputQuestion[]
submissions: (Omit<FormSubmission, 'date'> & { date: Date })[]
})[]
) =>
forms.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,
}))
export { createFormFrom, getForm, getForms, submitAnswer, formatForms }

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,
}

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

@ -0,0 +1,50 @@
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'
import { formatForms } from './form'
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 dbUser = await findDBUserBy(db, params)
if (!dbUser) throw new UserInputError('No such user')
const user = {
...dbUser,
forms: formatForms(dbUser.forms),
}
return user
} catch (err) {
return err
}
}
export { createUser, findUserBy }

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

@ -0,0 +1,184 @@
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,
},
include: {
forms: {
include: {
choisesQuestions: {
include: {
variants: true,
},
},
inputQuestions: true,
submissions: {
include: {
answers: true,
},
},
},
},
formsSubmissions: {
include: {
answers: true,
},
},
},
})
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 }

48
src/index.ts Normal file
View File

@ -0,0 +1,48 @@
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'
require('dotenv').config()
const app = express()
app.use(
expressJwt({
algorithms: ['HS256'],
credentialsRequired: false,
secret: '' + process.env.JWT_SECRET,
})
)
const db = new PrismaClient()
const server = new ApolloServer({
context: async ({
req,
}: {
req: Request & {
user: JwtPayloadType
}
}): Promise<ApolloContextType> => {
const user = req.user || null
return {
db,
user,
}
},
debug: false,
schema: makeExecutableSchema({
resolvers,
typeDefs,
}),
})
server.applyMiddleware({ app })
app.listen(4000, () => {
console.log('Server ready at http://localhost:4000')
})

View File

@ -1,16 +0,0 @@
import express from "express"
import bodyParser from "body-parser"
import { router } from "./router"
import { logger } from "./middlewares"
const app = express()
app.use(bodyParser.json())
app.use(logger)
app.use(router)
app.listen(3000, () => {
console.log("server is listening on port 3000")
})

View File

@ -1,8 +0,0 @@
import { RequestHandler } from "express"
const logger: RequestHandler = (req, res, next) => {
console.log(`sent ${req.method} request to ${req.hostname}${req.url}`)
next()
}
export { logger }

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,
}

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

@ -0,0 +1,76 @@
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 })
if (user instanceof Error) throw user // Needed to a strange error
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 })
if (user instanceof Error) throw user // Needed to a strange error
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

View File

@ -1,19 +0,0 @@
import express from "express"
const router = express.Router()
router.get(
"/api/test",
(req: express.Request, res: express.Response) => {
return res.send(`Hello, ${req.query.name}`)
}
)
router.post(
"/api/test",
async (req: express.Request, res: express.Response) => {
return res.send(`Hello, hidden ${req.body.name}`)
}
)
export { router }

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
}

View File

@ -1,69 +1,16 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es5" /* 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'. */,
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
// "outDir": "./", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"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. */,
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"outDir": "dist",
"noImplicitAny": true,
"moduleResolution": "Node",
"baseUrl": "src",
"incremental": true,
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true /* Skip type checking of declaration files. */,
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
}
},
"include": ["src/**/*"]
}