Lots of code refactors

This commit is contained in:
Dmitriy Shishkov 2020-10-10 20:01:35 +05:00
parent 4f82c80922
commit 01676c59e4
16 changed files with 255 additions and 174 deletions

3
.env.example Normal file
View File

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

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,10 +1,27 @@
# QuestionForm Backend # QuestionForm Backend
Backend used with QuestionForm application. Backend for QuestionForm application.
# Built with: # Built with:
- Prisma - Prisma
- Graphql - Graphql
- Apollo Server - Apollo Server
- Graphql code generator
- SendGrid - 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..._

1
prisma/.env.example Normal file
View File

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

View File

@ -4,13 +4,14 @@ import {
AuthenticationError, AuthenticationError,
ForbiddenError ForbiddenError
} from 'apollo-server-express' } from 'apollo-server-express'
import { CheckRightsAndResolve } from './types'
import { getDBFormAuthor } from '../db'
import { PrismaClient } from '@prisma/client' import { PrismaClient } from '@prisma/client'
import { sendToken } from '../mailer'
require('dotenv').config() require('dotenv').config()
import { CheckRightsAndResolve } from './types'
import { getDBFormAuthor } from '../db'
import { sendToken } from './mailer'
const checkRightsAndResolve: CheckRightsAndResolve = async (params) => { const checkRightsAndResolve: CheckRightsAndResolve = async (params) => {
const { user, expected, controller } = params const { user, expected, controller } = params
@ -26,7 +27,7 @@ const checkRightsAndResolve: CheckRightsAndResolve = async (params) => {
const getFormAuthor = async (db: PrismaClient, id: number) => { const getFormAuthor = async (db: PrismaClient, id: number) => {
const author = await getDBFormAuthor(db, id) 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 const authorId = author.author.id
@ -40,7 +41,7 @@ const tokenGenerate = (email: string, id: number) => {
}) })
} }
const sendTokenEmail = async ( const genAndSendToken = async (
email: string, email: string,
user: { id: number; name: string } user: { id: number; name: string }
) => { ) => {
@ -48,7 +49,8 @@ const sendTokenEmail = async (
const res = await sendToken(user.name, email, token) 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 }

View File

@ -1,15 +1,21 @@
import { Answer, PrismaClient } from '@prisma/client' import { Answer as DbAnswer, PrismaClient } from '@prisma/client'
import { ApolloError } from 'apollo-server-express' import { ApolloError, UserInputError } from 'apollo-server-express'
import { import {
ChoisesQuestion, Form,
Form as GraphqlForm, Form as GraphqlForm,
FormSubmission,
InputQuestion, InputQuestion,
MutationCreateFormArgs, MutationCreateFormArgs,
MutationFormSubmitArgs, MutationFormSubmitArgs,
Question ServerAnswer
} from '../typeDefs/typeDefs.gen' } from '../typeDefs/typeDefs.gen'
import { createChoises, newForm } from './types' import {
CreateChoises,
FormConstructor,
UploadedChoisesQuestion,
UploadedInputQuestion,
UploadedQuestion
} from './types'
import { import {
createDBForm, createDBForm,
getDBForm, getDBForm,
@ -21,103 +27,119 @@ const getForm = async (
db: PrismaClient, db: PrismaClient,
id: number, id: number,
user: { requesterId: number; userId: number } user: { requesterId: number; userId: number }
) => { ): Promise<Form> => {
const dbForm = await getDBForm(db, id, user) 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 = { const form: GraphqlForm = {
author: dbForm.author, author: dbForm.author,
dateCreated: dbForm.dateCreated.toString(), dateCreated: dbForm.dateCreated.toString(),
id: dbForm.id, id: dbForm.id,
questions: [...dbForm.choisesQuestions, ...dbForm.inputQuestions], questions: [...dbForm.choisesQuestions, ...dbForm.inputQuestions],
submissions: user.requesterId submissions: dbForm.submissions.map((submission) => ({
? dbForm.submissions.map((submission) => ({ answers: submission.answers,
answers: submission.answers, date: submission.date.toString(),
date: submission.date.toString(), id: submission.id
id: submission.id })),
})) title: dbForm.title
: undefined, }
title: dbForm.title
return form
} catch (err) {
return err
} }
return form
} }
const getForms = async (db: PrismaClient, userId: number) => { const getForms = async (db: PrismaClient, userId: number): Promise<Form[]> => {
const dbForms = await getDBFormsByUser(db, userId) try {
const dbForms = await getDBFormsByUser(db, userId)
const forms = [ if (!dbForms) throw new ApolloError("Couldn't load forms", 'FETCHINGERROR')
...dbForms.map((form) => ({
const forms: Form[] = dbForms.map((form) => ({
dateCreated: form.dateCreated.toString(), dateCreated: form.dateCreated.toString(),
id: form.id, id: form.id,
questions: [...form.choisesQuestions, ...form.inputQuestions], questions: [...form.choisesQuestions, ...form.inputQuestions],
submissions: form.submissions.map<FormSubmission>((submission) => ({ submissions: form.submissions.map((submission) => ({
answers: submission.answers, answers: submission.answers,
date: submission.date.toString(), date: submission.date.toString(),
id: submission.id id: submission.id
})), })),
title: form.title title: form.title
})) }))
]
return forms return forms
} catch (err) {
return err
}
} }
const createFormFrom = async ( const createFormFrom = async (
db: PrismaClient, db: PrismaClient,
params: MutationCreateFormArgs, params: MutationCreateFormArgs,
id: number id: number
) => { ): Promise<ServerAnswer> => {
const parsedQuestions = <Question[]>JSON.parse(params.questions) try {
const newForm: newForm = { const parsedQuestions = <UploadedQuestion[]>JSON.parse(params.questions)
choisesQuestions: {
create: parsedQuestions.flatMap<createChoises>(
(val: InputQuestion | ChoisesQuestion, index) => {
if ('type' in val) {
return [
{
number: index,
title: val.title,
type: val.type,
variants: {
create: val.variants
}
}
]
}
{ const newForm: FormConstructor = {
return [] choisesQuestions: {
} create: parsedQuestions.flatMap<CreateChoises>(
} (uQuestion: UploadedChoisesQuestion | UploadedInputQuestion, index) =>
) 'type' in uQuestion
}, ? [
inputQuestions: { {
create: parsedQuestions.filter( number: index,
(val: InputQuestion | ChoisesQuestion, index) => { title: uQuestion.title,
if (!('type' in val)) type: uQuestion.type,
return { variants: {
number: index, create: uQuestion.variants
title: val.title }
} }
} ]
) : []
}, )
title: params.title },
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
} }
return createDBForm(db, newForm, id)
} }
const submitAnswer = async ( const submitAnswer = async (
db: PrismaClient, db: PrismaClient,
{ answers, formId }: MutationFormSubmitArgs, { answers, formId }: MutationFormSubmitArgs,
userId: number userId: number
) => { ): Promise<ServerAnswer> => {
const parsedAnswers = <Answer[]>JSON.parse(answers) try {
const parsedAnswers = <DbAnswer[]>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 } export { createFormFrom, getForm, getForms, submitAnswer }

View File

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

View File

@ -2,7 +2,7 @@ import sgMail from '@sendgrid/mail'
require('dotenv').config() 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) => { const sendToken = (username: string, email: string, token: string) => {
return sgMail.send({ return sgMail.send({

View File

@ -1,3 +1,4 @@
import { ChoiseType } from '@prisma/client'
import { import {
ChoisesQuestion, ChoisesQuestion,
InputQuestion, InputQuestion,
@ -5,14 +6,14 @@ import {
} from '../typeDefs/typeDefs.gen' } from '../typeDefs/typeDefs.gen'
import { JwtPayloadType } from '../types' import { JwtPayloadType } from '../types'
type expectedType = { type ExpectedType = {
id: number id: number
self: boolean self: boolean
} }
interface ICheckRightsAndResolve<T> { interface ICheckRightsAndResolve<T> {
controller: T controller: T
expected: expectedType expected: ExpectedType
user: JwtPayloadType | null user: JwtPayloadType | null
} }
@ -20,16 +21,32 @@ type CheckRightsAndResolve = <ReturnType, ControllerType extends Function>(
params: ICheckRightsAndResolve<ControllerType> params: ICheckRightsAndResolve<ControllerType>
) => Promise<ReturnType> ) => Promise<ReturnType>
type newForm = { type FormConstructor = {
choisesQuestions: { choisesQuestions: { create: CreateChoises[] }
create: createChoises[]
}
inputQuestions: { create: InputQuestion[] } inputQuestions: { create: InputQuestion[] }
title: string title: string
} }
type createChoises = Omit<ChoisesQuestion, 'variants'> & { type CreateChoises = Omit<ChoisesQuestion, 'variants'> & {
variants: { create: Variant[] } 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
}

View File

@ -2,27 +2,43 @@ import { createDBUser, findDBUserBy } from '../db'
import { IFindUserParams } from '../db/types' import { IFindUserParams } from '../db/types'
import { MutationRegisterArgs, User } from '../typeDefs/typeDefs.gen' import { MutationRegisterArgs, User } from '../typeDefs/typeDefs.gen'
import { PrismaClient } from '@prisma/client' import { PrismaClient } from '@prisma/client'
import { UserInputError } from 'apollo-server-express' import { ApolloError, UserInputError } from 'apollo-server-express'
const createUser = async ( const createUser = async (
db: PrismaClient, db: PrismaClient,
{ email, name }: MutationRegisterArgs { email, name }: MutationRegisterArgs
): Promise<User> => { ): Promise<User> => {
if (!email || !name) try {
throw new UserInputError( if (!email || !name)
'Provide full user information', throw new UserInputError(
[!email ? [email] : [], !name ? [name] : []].flat() '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 findUserBy = async (
const user = await findDBUserBy(db, params) db: PrismaClient,
params: IFindUserParams
): Promise<User> => {
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 } export { createUser, findUserBy }

View File

@ -1,8 +1,8 @@
import { PrismaClient } from '@prisma/client'
import { Answer, MutationRegisterArgs } from '../typeDefs/typeDefs.gen' import { Answer, MutationRegisterArgs } from '../typeDefs/typeDefs.gen'
import { IFindUserParams } from './types' import { IFindUserParams } from './types'
import { newForm } from '../controllers/types' import { FormConstructor } from '../controllers/types'
import { PrismaClient } from '@prisma/client'
import { UserInputError } from 'apollo-server-express'
/** /**
* Get form from DataBase * Get form from DataBase
@ -14,15 +14,18 @@ import { UserInputError } from 'apollo-server-express'
* @example * @example
* const form = await getDBForm(db, id, true) * const form = await getDBForm(db, id, true)
*/ */
const getDBForm = async ( const getDBForm = (
db: PrismaClient, db: PrismaClient,
formId: number, formId: number,
user?: { {
requesterId,
userId
}: {
requesterId: number requesterId: number
userId: number userId: number
} }
) => { ) =>
return await db.form.findOne({ db.form.findOne({
include: { include: {
author: { author: {
select: { select: {
@ -42,10 +45,10 @@ const getDBForm = async (
answers: true answers: true
}, },
where: where:
user?.requesterId != user?.userId requesterId != userId
? { ? {
user: { user: {
id: user?.requesterId id: requesterId
} }
} }
: undefined : undefined
@ -55,7 +58,6 @@ const getDBForm = async (
id: formId id: formId
} }
}) })
}
/** /**
* Get all forms of user * Get all forms of user
@ -64,8 +66,8 @@ const getDBForm = async (
* @example * @example
* const forms = await getDBFormsByUser(db, userId) * const forms = await getDBFormsByUser(db, userId)
*/ */
const getDBFormsByUser = async (db: PrismaClient, id: number) => { const getDBFormsByUser = (db: PrismaClient, id: number) =>
return await db.form.findMany({ db.form.findMany({
include: { include: {
choisesQuestions: { choisesQuestions: {
include: { include: {
@ -85,10 +87,9 @@ const getDBFormsByUser = async (db: PrismaClient, id: number) => {
} }
} }
}) })
}
const getDBFormAuthor = async (db: PrismaClient, id: number) => { const getDBFormAuthor = (db: PrismaClient, id: number) =>
return await db.form.findOne({ db.form.findOne({
select: { select: {
author: { author: {
select: { select: {
@ -100,52 +101,39 @@ const getDBFormAuthor = async (db: PrismaClient, id: number) => {
id id
} }
}) })
}
const createDBUser = async ( const createDBUser = (
db: PrismaClient, db: PrismaClient,
{ email, name }: MutationRegisterArgs { email, name }: MutationRegisterArgs
) => { ) =>
return await db.user.create({ db.user.create({
data: { email, name } data: { email, name }
}) })
}
const findDBUserBy = async (db: PrismaClient, params: IFindUserParams) => { const findDBUserBy = (db: PrismaClient, params: IFindUserParams) =>
const user = await db.user.findOne({ db.user.findOne({
where: { where: {
...params ...params
} }
}) })
if (!user) throw new UserInputError('Not found')
return user const createDBForm = (db: PrismaClient, form: FormConstructor, id: number) =>
} db.form.create({
const createDBForm = async (
db: PrismaClient,
{ title, inputQuestions, choisesQuestions }: newForm,
id: number
) => {
return await db.form.create({
data: { data: {
author: { author: {
connect: { id } connect: { id }
}, },
choisesQuestions, ...form
inputQuestions,
title
} }
}) })
}
const submitDBAnswer = async ( const submitDBAnswer = (
db: PrismaClient, db: PrismaClient,
userId: number, userId: number,
formId: number, formId: number,
formAnswers: Answer[] formAnswers: Answer[]
) => { ) =>
const res = await db.formSubmission.create({ db.formSubmission.create({
data: { data: {
answers: { answers: {
create: formAnswers create: formAnswers
@ -163,11 +151,6 @@ const submitDBAnswer = async (
} }
}) })
if (!res) throw new UserInputError("Can't submit form")
return { success: true }
}
export { export {
createDBForm, createDBForm,
createDBUser, createDBUser,

View File

@ -1,6 +1,7 @@
import { getDBForm } from '../db'
import { PromiseReturnType } from '@prisma/client' import { PromiseReturnType } from '@prisma/client'
import { getDBForm } from '../db'
type FullForm = PromiseReturnType<typeof getDBForm> type FullForm = PromiseReturnType<typeof getDBForm>
interface IFindUserParams { interface IFindUserParams {

View File

@ -66,8 +66,8 @@ const formsQuery: Resolver<Form[], {}, ApolloContextType> = async (
} }
} }
const createForm: Resolver< const createFormMutation: Resolver<
Form, ServerAnswer,
{}, {},
ApolloContextType, ApolloContextType,
MutationCreateFormArgs MutationCreateFormArgs
@ -84,7 +84,7 @@ const createForm: Resolver<
}) })
} }
const formSubmit: Resolver< const formSubmitMutation: Resolver<
ServerAnswer, ServerAnswer,
{}, {},
ApolloContextType, ApolloContextType,
@ -122,9 +122,9 @@ const AnswerResolver: AnswerResolvers = {
export { export {
AnswerResolver, AnswerResolver,
createForm, createFormMutation,
formQuery, formQuery,
formsQuery, formsQuery,
formSubmit, formSubmitMutation,
QuestionResolver QuestionResolver
} }

View File

@ -1,7 +1,7 @@
import { import {
checkRightsAndResolve, checkRightsAndResolve,
findUserBy, findUserBy,
sendTokenEmail genAndSendToken
} from '../controllers' } from '../controllers'
import { createUser } from '../controllers/user' import { createUser } from '../controllers/user'
import { import {
@ -14,7 +14,7 @@ import {
} from '../typeDefs/typeDefs.gen' } from '../typeDefs/typeDefs.gen'
import { ApolloContextType } from '../types' import { ApolloContextType } from '../types'
const loginResolver: Resolver< const loginMutation: Resolver<
ServerAnswer, ServerAnswer,
{}, {},
ApolloContextType, ApolloContextType,
@ -23,7 +23,7 @@ const loginResolver: Resolver<
try { try {
const user = await findUserBy(db, { email }) const user = await findUserBy(db, { email })
await sendTokenEmail(email, user) await genAndSendToken(email, user)
return { success: true } return { success: true }
} catch (err) { } catch (err) {
@ -31,7 +31,7 @@ const loginResolver: Resolver<
} }
} }
const registerResolver: Resolver< const registerMutation: Resolver<
ServerAnswer, ServerAnswer,
{}, {},
ApolloContextType, ApolloContextType,
@ -40,7 +40,7 @@ const registerResolver: Resolver<
try { try {
const user = await createUser(db, { email, name }) const user = await createUser(db, { email, name })
await sendTokenEmail(email, user) await genAndSendToken(email, user)
return { success: true } return { success: true }
} catch (err) { } catch (err) {
@ -48,15 +48,14 @@ const registerResolver: Resolver<
} }
} }
const userResolver: Resolver< const userQuery: Resolver<User, {}, ApolloContextType, QueryUserArgs> = async (
User, _,
{}, { id },
ApolloContextType, { db, user }
QueryUserArgs ) => {
> = async (_, { id }, { db, user }) => {
const findUserById = (id: number) => findUserBy(db, { id })
try { try {
const findUserById = (id: number) => findUserBy(db, { id })
return await checkRightsAndResolve({ return await checkRightsAndResolve({
controller: findUserById, controller: findUserById,
expected: { expected: {
@ -70,4 +69,4 @@ const userResolver: Resolver<
} }
} }
export { loginResolver, registerResolver, userResolver } export { loginMutation, registerMutation, userQuery }

View File

@ -1,20 +1,19 @@
import { ApolloContextType } from '../types'
import { Resolvers } from '../typeDefs/typeDefs.gen' import { Resolvers } from '../typeDefs/typeDefs.gen'
import { import {
formQuery as form, formQuery as form,
QuestionResolver as Question, QuestionResolver as Question,
AnswerResolver as Answer, AnswerResolver as Answer,
formsQuery as forms, formsQuery as forms,
createForm, createFormMutation as createForm,
formSubmit formSubmitMutation as formSubmit
} from './Form' } from './Form'
import { import {
loginResolver as login, loginMutation as login,
registerResolver as register, registerMutation as register,
userResolver as user userQuery as user
} from './User' } from './User'
const resolvers: Resolvers<ApolloContextType> = { const resolvers: Resolvers = {
Query: { Query: {
form, form,
forms, forms,

View File

@ -5,7 +5,7 @@ type Query {
} }
type Mutation { type Mutation {
createForm(title: String!, questions: String!): Form! createForm(title: String!, questions: String!): serverAnswer
formSubmit(formId: Int!, answers: String!): serverAnswer formSubmit(formId: Int!, answers: String!): serverAnswer
login(email: String!): serverAnswer login(email: String!): serverAnswer
register(name: String!, email: String!): serverAnswer register(name: String!, email: String!): serverAnswer