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
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..._

1
prisma/.env.example Normal file
View File

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

View File

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

View File

@ -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<Form> => {
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<Form[]> => {
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<FormSubmission>((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 = <Question[]>JSON.parse(params.questions)
const newForm: newForm = {
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
}
}
]
}
): Promise<ServerAnswer> => {
try {
const parsedQuestions = <UploadedQuestion[]>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<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
}
return createDBForm(db, newForm, id)
}
const submitAnswer = async (
db: PrismaClient,
{ answers, formId }: MutationFormSubmitArgs,
userId: number
) => {
const parsedAnswers = <Answer[]>JSON.parse(answers)
): Promise<ServerAnswer> => {
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 }

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 { findUserBy } from './user'
@ -6,9 +6,9 @@ export {
checkRightsAndResolve,
createFormFrom,
findUserBy,
genAndSendToken,
getForm,
getFormAuthor,
getForms,
sendTokenEmail,
submitAnswer
}

View File

@ -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({

View File

@ -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<T> {
controller: T
expected: expectedType
expected: ExpectedType
user: JwtPayloadType | null
}
@ -20,16 +21,32 @@ type CheckRightsAndResolve = <ReturnType, ControllerType extends Function>(
params: ICheckRightsAndResolve<ControllerType>
) => Promise<ReturnType>
type newForm = {
choisesQuestions: {
create: createChoises[]
}
type FormConstructor = {
choisesQuestions: { create: CreateChoises[] }
inputQuestions: { create: InputQuestion[] }
title: string
}
type createChoises = Omit<ChoisesQuestion, 'variants'> & {
type CreateChoises = Omit<ChoisesQuestion, 'variants'> & {
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 { 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<User> => {
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<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 }

View File

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

View File

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

View File

@ -66,8 +66,8 @@ const formsQuery: Resolver<Form[], {}, ApolloContextType> = 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
}

View File

@ -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<User, {}, ApolloContextType, QueryUserArgs> = 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 }

View File

@ -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<ApolloContextType> = {
const resolvers: Resolvers = {
Query: {
form,
forms,

View File

@ -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