From 01676c59e470f91ab13e21f08a3af2c116a71c96 Mon Sep 17 00:00:00 2001 From: Dm1tr1y147 Date: Sat, 10 Oct 2020 20:01:35 +0500 Subject: [PATCH] Lots of code refactors --- .env.example | 3 + LICENCE | 21 +++ README.md | 19 +- prisma/.env.example | 1 + src/controllers/auth.ts | 16 +- src/controllers/form.ts | 166 ++++++++++-------- src/controllers/index.ts | 4 +- .../index.ts => controllers/mailer.ts} | 2 +- src/controllers/types.ts | 33 +++- src/controllers/user.ts | 38 ++-- src/db/index.ts | 71 +++----- src/db/types.ts | 3 +- src/resolvers/Form.ts | 10 +- src/resolvers/User.ts | 27 ++- src/resolvers/index.ts | 13 +- src/typeDefs/typeDefs.gql | 2 +- 16 files changed, 255 insertions(+), 174 deletions(-) create mode 100644 .env.example create mode 100644 LICENCE create mode 100644 prisma/.env.example rename src/{mailer/index.ts => controllers/mailer.ts} (89%) diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8d665b3 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +SENDGRID_API_KEY= +JWT_SECRET= +SITE_URL=test.com \ No newline at end of file diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..8e95303 --- /dev/null +++ b/LICENCE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Ditriy Shishkov (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. \ No newline at end of file diff --git a/README.md b/README.md index fd2ba74..994b4b0 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,27 @@ # QuestionForm Backend -Backend used with QuestionForm application. +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..._ diff --git a/prisma/.env.example b/prisma/.env.example new file mode 100644 index 0000000..d78273c --- /dev/null +++ b/prisma/.env.example @@ -0,0 +1 @@ +DATABASE_URL="postgres://" diff --git a/src/controllers/auth.ts b/src/controllers/auth.ts index 5980a2b..fbceb73 100644 --- a/src/controllers/auth.ts +++ b/src/controllers/auth.ts @@ -4,13 +4,14 @@ import { AuthenticationError, ForbiddenError } from 'apollo-server-express' -import { CheckRightsAndResolve } from './types' -import { getDBFormAuthor } from '../db' import { PrismaClient } from '@prisma/client' -import { sendToken } from '../mailer' 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 @@ -26,7 +27,7 @@ const checkRightsAndResolve: CheckRightsAndResolve = async (params) => { const getFormAuthor = async (db: PrismaClient, id: number) => { const author = await getDBFormAuthor(db, id) - if (!author) throw new ApolloError('Not found') + if (!author) throw new ApolloError('Not found', 'NOTFOUND') const authorId = author.author.id @@ -40,7 +41,7 @@ const tokenGenerate = (email: string, id: number) => { }) } -const sendTokenEmail = async ( +const genAndSendToken = async ( email: string, user: { id: number; name: string } ) => { @@ -48,7 +49,8 @@ const sendTokenEmail = async ( const res = await sendToken(user.name, email, token) - if (res[0].statusCode != 202) return new ApolloError("Couldn't send email") + if (res[0].statusCode != 202) + return new ApolloError("Couldn't send email", 'EMAILSENDERROR') } -export { checkRightsAndResolve, getFormAuthor, sendTokenEmail, tokenGenerate } +export { checkRightsAndResolve, getFormAuthor, genAndSendToken } diff --git a/src/controllers/form.ts b/src/controllers/form.ts index 6b5f81f..818160d 100644 --- a/src/controllers/form.ts +++ b/src/controllers/form.ts @@ -1,15 +1,21 @@ -import { Answer, PrismaClient } from '@prisma/client' -import { ApolloError } from 'apollo-server-express' +import { Answer as DbAnswer, PrismaClient } from '@prisma/client' +import { ApolloError, UserInputError } from 'apollo-server-express' + import { - ChoisesQuestion, + Form, Form as GraphqlForm, - FormSubmission, InputQuestion, MutationCreateFormArgs, MutationFormSubmitArgs, - Question + ServerAnswer } from '../typeDefs/typeDefs.gen' -import { createChoises, newForm } from './types' +import { + CreateChoises, + FormConstructor, + UploadedChoisesQuestion, + UploadedInputQuestion, + UploadedQuestion +} from './types' import { createDBForm, getDBForm, @@ -21,103 +27,119 @@ const getForm = async ( db: PrismaClient, id: number, user: { requesterId: number; userId: number } -) => { - const dbForm = await getDBForm(db, id, user) +): Promise
=> { + try { + const dbForm = await getDBForm(db, id, user) - if (dbForm == null) throw new ApolloError('Not found') + 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: user.requesterId - ? dbForm.submissions.map((submission) => ({ - answers: submission.answers, - date: submission.date.toString(), - id: submission.id - })) - : undefined, - title: dbForm.title + 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 } - - return form } -const getForms = async (db: PrismaClient, userId: number) => { - const dbForms = await getDBFormsByUser(db, userId) +const getForms = async (db: PrismaClient, userId: number): Promise => { + try { + const dbForms = await getDBFormsByUser(db, userId) - const forms = [ - ...dbForms.map((form) => ({ + 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) => ({ + submissions: form.submissions.map((submission) => ({ answers: submission.answers, date: submission.date.toString(), id: submission.id })), title: form.title })) - ] - return forms + return forms + } catch (err) { + return err + } } const createFormFrom = async ( db: PrismaClient, params: MutationCreateFormArgs, id: number -) => { - const parsedQuestions = JSON.parse(params.questions) - const newForm: newForm = { - choisesQuestions: { - create: parsedQuestions.flatMap( - (val: InputQuestion | ChoisesQuestion, index) => { - if ('type' in val) { - return [ - { - number: index, - title: val.title, - type: val.type, - variants: { - create: val.variants - } - } - ] - } +): Promise => { + try { + const parsedQuestions = JSON.parse(params.questions) - { - return [] - } - } - ) - }, - inputQuestions: { - create: parsedQuestions.filter( - (val: InputQuestion | ChoisesQuestion, index) => { - if (!('type' in val)) - return { - number: index, - title: val.title - } - } - ) - }, - title: params.title + const newForm: FormConstructor = { + choisesQuestions: { + create: parsedQuestions.flatMap( + (uQuestion: UploadedChoisesQuestion | UploadedInputQuestion, index) => + 'type' in uQuestion + ? [ + { + number: index, + title: uQuestion.title, + type: uQuestion.type, + variants: { + create: uQuestion.variants + } + } + ] + : [] + ) + }, + inputQuestions: { + create: parsedQuestions.flatMap( + (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 } - - return createDBForm(db, newForm, id) } const submitAnswer = async ( db: PrismaClient, { answers, formId }: MutationFormSubmitArgs, userId: number -) => { - const parsedAnswers = JSON.parse(answers) +): Promise => { + try { + const parsedAnswers = JSON.parse(answers) - return submitDBAnswer(db, userId, formId, parsedAnswers) + 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 } diff --git a/src/controllers/index.ts b/src/controllers/index.ts index b3d9698..5944089 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,4 +1,4 @@ -import { checkRightsAndResolve, getFormAuthor, sendTokenEmail } from './auth' +import { checkRightsAndResolve, getFormAuthor, genAndSendToken } from './auth' import { createFormFrom, getForm, getForms, submitAnswer } from './form' import { findUserBy } from './user' @@ -6,9 +6,9 @@ export { checkRightsAndResolve, createFormFrom, findUserBy, + genAndSendToken, getForm, getFormAuthor, getForms, - sendTokenEmail, submitAnswer } diff --git a/src/mailer/index.ts b/src/controllers/mailer.ts similarity index 89% rename from src/mailer/index.ts rename to src/controllers/mailer.ts index bb717e8..585a031 100644 --- a/src/mailer/index.ts +++ b/src/controllers/mailer.ts @@ -2,7 +2,7 @@ import sgMail from '@sendgrid/mail' require('dotenv').config() -sgMail.setApiKey(process.env.SENDGRID_API_KEY!) +sgMail.setApiKey('' + process.env.SENDGRID_API_KEY) const sendToken = (username: string, email: string, token: string) => { return sgMail.send({ diff --git a/src/controllers/types.ts b/src/controllers/types.ts index bc009f4..a057db1 100644 --- a/src/controllers/types.ts +++ b/src/controllers/types.ts @@ -1,3 +1,4 @@ +import { ChoiseType } from '@prisma/client' import { ChoisesQuestion, InputQuestion, @@ -5,14 +6,14 @@ import { } from '../typeDefs/typeDefs.gen' import { JwtPayloadType } from '../types' -type expectedType = { +type ExpectedType = { id: number self: boolean } interface ICheckRightsAndResolve { controller: T - expected: expectedType + expected: ExpectedType user: JwtPayloadType | null } @@ -20,16 +21,32 @@ type CheckRightsAndResolve = ( params: ICheckRightsAndResolve ) => Promise -type newForm = { - choisesQuestions: { - create: createChoises[] - } +type FormConstructor = { + choisesQuestions: { create: CreateChoises[] } inputQuestions: { create: InputQuestion[] } title: string } -type createChoises = Omit & { +type CreateChoises = Omit & { variants: { create: Variant[] } } -export { CheckRightsAndResolve, createChoises, newForm } +type UploadedQuestion = { + title: string +} + +type UploadedChoisesQuestion = UploadedQuestion & { + type: ChoiseType + variants: Variant[] +} + +type UploadedInputQuestion = UploadedQuestion + +export { + CheckRightsAndResolve, + CreateChoises, + FormConstructor, + UploadedChoisesQuestion, + UploadedInputQuestion, + UploadedQuestion +} diff --git a/src/controllers/user.ts b/src/controllers/user.ts index 6f1d688..d4b740c 100644 --- a/src/controllers/user.ts +++ b/src/controllers/user.ts @@ -2,27 +2,43 @@ import { createDBUser, findDBUserBy } from '../db' import { IFindUserParams } from '../db/types' import { MutationRegisterArgs, User } from '../typeDefs/typeDefs.gen' import { PrismaClient } from '@prisma/client' -import { UserInputError } from 'apollo-server-express' +import { ApolloError, UserInputError } from 'apollo-server-express' const createUser = async ( db: PrismaClient, { email, name }: MutationRegisterArgs ): Promise => { - if (!email || !name) - throw new UserInputError( - 'Provide full user information', - [!email ? [email] : [], !name ? [name] : []].flat() - ) + try { + if (!email || !name) + throw new UserInputError( + 'Provide full user information', + [!email ? [email] : [], !name ? [name] : []].flat() + ) - return await createDBUser(db, { email, name }) + 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) => { - const user = await findDBUserBy(db, params) +const findUserBy = async ( + db: PrismaClient, + params: IFindUserParams +): Promise => { + try { + const user = await findDBUserBy(db, params) - if (!user) throw new UserInputError('No such user') + if (!user) throw new UserInputError('No such user') - return user + return user + } catch (err) { + return err + } } export { createUser, findUserBy } diff --git a/src/db/index.ts b/src/db/index.ts index 82bf47c..69c8bc6 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -1,8 +1,8 @@ +import { PrismaClient } from '@prisma/client' + import { Answer, MutationRegisterArgs } from '../typeDefs/typeDefs.gen' import { IFindUserParams } from './types' -import { newForm } from '../controllers/types' -import { PrismaClient } from '@prisma/client' -import { UserInputError } from 'apollo-server-express' +import { FormConstructor } from '../controllers/types' /** * Get form from DataBase @@ -14,15 +14,18 @@ import { UserInputError } from 'apollo-server-express' * @example * const form = await getDBForm(db, id, true) */ -const getDBForm = async ( +const getDBForm = ( db: PrismaClient, formId: number, - user?: { + { + requesterId, + userId + }: { requesterId: number userId: number } -) => { - return await db.form.findOne({ +) => + db.form.findOne({ include: { author: { select: { @@ -42,10 +45,10 @@ const getDBForm = async ( answers: true }, where: - user?.requesterId != user?.userId + requesterId != userId ? { user: { - id: user?.requesterId + id: requesterId } } : undefined @@ -55,7 +58,6 @@ const getDBForm = async ( id: formId } }) -} /** * Get all forms of user @@ -64,8 +66,8 @@ const getDBForm = async ( * @example * const forms = await getDBFormsByUser(db, userId) */ -const getDBFormsByUser = async (db: PrismaClient, id: number) => { - return await db.form.findMany({ +const getDBFormsByUser = (db: PrismaClient, id: number) => + db.form.findMany({ include: { choisesQuestions: { include: { @@ -85,10 +87,9 @@ const getDBFormsByUser = async (db: PrismaClient, id: number) => { } } }) -} -const getDBFormAuthor = async (db: PrismaClient, id: number) => { - return await db.form.findOne({ +const getDBFormAuthor = (db: PrismaClient, id: number) => + db.form.findOne({ select: { author: { select: { @@ -100,52 +101,39 @@ const getDBFormAuthor = async (db: PrismaClient, id: number) => { id } }) -} -const createDBUser = async ( +const createDBUser = ( db: PrismaClient, { email, name }: MutationRegisterArgs -) => { - return await db.user.create({ +) => + db.user.create({ data: { email, name } }) -} -const findDBUserBy = async (db: PrismaClient, params: IFindUserParams) => { - const user = await db.user.findOne({ +const findDBUserBy = (db: PrismaClient, params: IFindUserParams) => + db.user.findOne({ where: { ...params } }) - if (!user) throw new UserInputError('Not found') - return user -} - -const createDBForm = async ( - db: PrismaClient, - { title, inputQuestions, choisesQuestions }: newForm, - id: number -) => { - return await db.form.create({ +const createDBForm = (db: PrismaClient, form: FormConstructor, id: number) => + db.form.create({ data: { author: { connect: { id } }, - choisesQuestions, - inputQuestions, - title + ...form } }) -} -const submitDBAnswer = async ( +const submitDBAnswer = ( db: PrismaClient, userId: number, formId: number, formAnswers: Answer[] -) => { - const res = await db.formSubmission.create({ +) => + db.formSubmission.create({ data: { answers: { create: formAnswers @@ -163,11 +151,6 @@ const submitDBAnswer = async ( } }) - if (!res) throw new UserInputError("Can't submit form") - - return { success: true } -} - export { createDBForm, createDBUser, diff --git a/src/db/types.ts b/src/db/types.ts index f7f987c..2d3c845 100644 --- a/src/db/types.ts +++ b/src/db/types.ts @@ -1,6 +1,7 @@ -import { getDBForm } from '../db' import { PromiseReturnType } from '@prisma/client' +import { getDBForm } from '../db' + type FullForm = PromiseReturnType interface IFindUserParams { diff --git a/src/resolvers/Form.ts b/src/resolvers/Form.ts index 93a84ce..ba2f987 100644 --- a/src/resolvers/Form.ts +++ b/src/resolvers/Form.ts @@ -66,8 +66,8 @@ const formsQuery: Resolver = async ( } } -const createForm: Resolver< - Form, +const createFormMutation: Resolver< + ServerAnswer, {}, ApolloContextType, MutationCreateFormArgs @@ -84,7 +84,7 @@ const createForm: Resolver< }) } -const formSubmit: Resolver< +const formSubmitMutation: Resolver< ServerAnswer, {}, ApolloContextType, @@ -122,9 +122,9 @@ const AnswerResolver: AnswerResolvers = { export { AnswerResolver, - createForm, + createFormMutation, formQuery, formsQuery, - formSubmit, + formSubmitMutation, QuestionResolver } diff --git a/src/resolvers/User.ts b/src/resolvers/User.ts index 846160f..4c44200 100644 --- a/src/resolvers/User.ts +++ b/src/resolvers/User.ts @@ -1,7 +1,7 @@ import { checkRightsAndResolve, findUserBy, - sendTokenEmail + genAndSendToken } from '../controllers' import { createUser } from '../controllers/user' import { @@ -14,7 +14,7 @@ import { } from '../typeDefs/typeDefs.gen' import { ApolloContextType } from '../types' -const loginResolver: Resolver< +const loginMutation: Resolver< ServerAnswer, {}, ApolloContextType, @@ -23,7 +23,7 @@ const loginResolver: Resolver< try { const user = await findUserBy(db, { email }) - await sendTokenEmail(email, user) + await genAndSendToken(email, user) return { success: true } } catch (err) { @@ -31,7 +31,7 @@ const loginResolver: Resolver< } } -const registerResolver: Resolver< +const registerMutation: Resolver< ServerAnswer, {}, ApolloContextType, @@ -40,7 +40,7 @@ const registerResolver: Resolver< try { const user = await createUser(db, { email, name }) - await sendTokenEmail(email, user) + await genAndSendToken(email, user) return { success: true } } catch (err) { @@ -48,15 +48,14 @@ const registerResolver: Resolver< } } -const userResolver: Resolver< - User, - {}, - ApolloContextType, - QueryUserArgs -> = async (_, { id }, { db, user }) => { - const findUserById = (id: number) => findUserBy(db, { id }) - +const userQuery: Resolver = async ( + _, + { id }, + { db, user } +) => { try { + const findUserById = (id: number) => findUserBy(db, { id }) + return await checkRightsAndResolve({ controller: findUserById, expected: { @@ -70,4 +69,4 @@ const userResolver: Resolver< } } -export { loginResolver, registerResolver, userResolver } +export { loginMutation, registerMutation, userQuery } diff --git a/src/resolvers/index.ts b/src/resolvers/index.ts index dcdcd57..085a774 100644 --- a/src/resolvers/index.ts +++ b/src/resolvers/index.ts @@ -1,20 +1,19 @@ -import { ApolloContextType } from '../types' import { Resolvers } from '../typeDefs/typeDefs.gen' import { formQuery as form, QuestionResolver as Question, AnswerResolver as Answer, formsQuery as forms, - createForm, - formSubmit + createFormMutation as createForm, + formSubmitMutation as formSubmit } from './Form' import { - loginResolver as login, - registerResolver as register, - userResolver as user + loginMutation as login, + registerMutation as register, + userQuery as user } from './User' -const resolvers: Resolvers = { +const resolvers: Resolvers = { Query: { form, forms, diff --git a/src/typeDefs/typeDefs.gql b/src/typeDefs/typeDefs.gql index 12c6f06..dbcb68d 100644 --- a/src/typeDefs/typeDefs.gql +++ b/src/typeDefs/typeDefs.gql @@ -5,7 +5,7 @@ type Query { } type Mutation { - createForm(title: String!, questions: String!): Form! + createForm(title: String!, questions: String!): serverAnswer formSubmit(formId: Int!, answers: String!): serverAnswer login(email: String!): serverAnswer register(name: String!, email: String!): serverAnswer