Added forms property fetching to user query and some minor fixes

This commit is contained in:
Dmitriy Shishkov 2020-10-18 22:09:40 +05:00
parent 01676c59e4
commit 673f42fd0f
16 changed files with 167 additions and 102 deletions

2
.gitignore vendored
View File

@ -1,6 +1,6 @@
/node_modules /node_modules
/build /dist
*.gen.ts *.gen.ts

View File

@ -1,5 +1,5 @@
overwrite: true overwrite: true
schema: "src/typeDefs/typeDefs.gql" schema: 'src/typeDefs/typeDefs.gql'
documents: null documents: null
generates: generates:
src/typeDefs/typeDefs.gen.ts: src/typeDefs/typeDefs.gen.ts:
@ -8,5 +8,5 @@ generates:
wrapFieldDefinitions: true wrapFieldDefinitions: true
enumsAsTypes: true enumsAsTypes: true
plugins: plugins:
- "typescript" - 'typescript'
- "typescript-resolvers" - 'typescript-resolvers'

View File

@ -2,6 +2,7 @@
"name": "backend", "name": "backend",
"version": "1.0.0", "version": "1.0.0",
"main": "src/index.ts", "main": "src/index.ts",
"private": "true",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@prisma/client": "^2.7.1", "@prisma/client": "^2.7.1",
@ -17,8 +18,11 @@
"scripts": { "scripts": {
"dev": "nodemon", "dev": "nodemon",
"start": "ts-node src/index.ts", "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", "codegen": "graphql-codegen --config codegen.yml",
"lint": "eslint" "lint": "eslint",
"test": "echo \"Error: no test specified\" && exit 1"
}, },
"devDependencies": { "devDependencies": {
"@graphql-codegen/cli": "1.17.10", "@graphql-codegen/cli": "1.17.10",

View File

@ -2,7 +2,7 @@ import jwt from 'jsonwebtoken'
import { import {
ApolloError, ApolloError,
AuthenticationError, AuthenticationError,
ForbiddenError ForbiddenError,
} from 'apollo-server-express' } from 'apollo-server-express'
import { PrismaClient } from '@prisma/client' import { PrismaClient } from '@prisma/client'
@ -37,7 +37,7 @@ const getFormAuthor = async (db: PrismaClient, id: number) => {
const tokenGenerate = (email: string, id: number) => { const tokenGenerate = (email: string, id: number) => {
return jwt.sign({ email, id }, '' + process.env.JWT_SECRET, { return jwt.sign({ email, id }, '' + process.env.JWT_SECRET, {
algorithm: 'HS256', algorithm: 'HS256',
expiresIn: '7 days' expiresIn: '7 days',
}) })
} }

View File

@ -1,33 +1,35 @@
import { Answer as DbAnswer, PrismaClient } from '@prisma/client' import { Answer as DbAnswer, PrismaClient, Form } from '@prisma/client'
import { ApolloError, UserInputError } from 'apollo-server-express' import { ApolloError, UserInputError } from 'apollo-server-express'
import { import {
Form, ChoisesQuestion,
Form as GraphqlForm, Form as GraphqlForm,
FormSubmission,
InputQuestion, InputQuestion,
MutationCreateFormArgs, MutationCreateFormArgs,
MutationFormSubmitArgs, MutationFormSubmitArgs,
ServerAnswer ServerAnswer,
Variant,
} from '../typeDefs/typeDefs.gen' } from '../typeDefs/typeDefs.gen'
import { import {
CreateChoises, CreateChoises,
FormConstructor, FormConstructor,
UploadedChoisesQuestion, UploadedChoisesQuestion,
UploadedInputQuestion, UploadedInputQuestion,
UploadedQuestion UploadedQuestion,
} from './types' } from './types'
import { import {
createDBForm, createDBForm,
getDBForm, getDBForm,
getDBFormsByUser, getDBFormsByUser,
submitDBAnswer submitDBAnswer,
} from '../db' } from '../db'
const getForm = async ( const getForm = async (
db: PrismaClient, db: PrismaClient,
id: number, id: number,
user: { requesterId: number; userId: number } user: { requesterId: number; userId: number }
): Promise<Form> => { ): Promise<GraphqlForm> => {
try { try {
const dbForm = await getDBForm(db, id, user) const dbForm = await getDBForm(db, id, user)
@ -41,9 +43,9 @@ const getForm = async (
submissions: dbForm.submissions.map((submission) => ({ submissions: 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 title: dbForm.title,
} }
return form return form
@ -52,22 +54,25 @@ const getForm = async (
} }
} }
const getForms = async (db: PrismaClient, userId: number): Promise<Form[]> => { const getForms = async (
db: PrismaClient,
userId: number
): Promise<GraphqlForm[]> => {
try { try {
const dbForms = await getDBFormsByUser(db, userId) const dbForms = await getDBFormsByUser(db, userId)
if (!dbForms) throw new ApolloError("Couldn't load forms", 'FETCHINGERROR') if (!dbForms) throw new ApolloError("Couldn't load forms", 'FETCHINGERROR')
const forms: Form[] = dbForms.map((form) => ({ const forms: GraphqlForm[] = 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((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
@ -95,12 +100,12 @@ const createFormFrom = async (
title: uQuestion.title, title: uQuestion.title,
type: uQuestion.type, type: uQuestion.type,
variants: { variants: {
create: uQuestion.variants create: uQuestion.variants,
} },
} },
] ]
: [] : []
) ),
}, },
inputQuestions: { inputQuestions: {
create: parsedQuestions.flatMap<InputQuestion>( create: parsedQuestions.flatMap<InputQuestion>(
@ -108,9 +113,9 @@ const createFormFrom = async (
!('type' in uQuestion) !('type' in uQuestion)
? [{ number: index, title: uQuestion.title }] ? [{ number: index, title: uQuestion.title }]
: [] : []
) ),
}, },
title: params.title title: params.title,
} }
const res = await createDBForm(db, newForm, id) const res = await createDBForm(db, newForm, id)
@ -142,4 +147,25 @@ const submitAnswer = async (
} }
} }
export { createFormFrom, getForm, getForms, submitAnswer } 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 }

View File

@ -10,5 +10,5 @@ export {
getForm, getForm,
getFormAuthor, getFormAuthor,
getForms, getForms,
submitAnswer submitAnswer,
} }

View File

@ -9,12 +9,12 @@ const sendToken = (username: string, email: string, token: string) => {
dynamicTemplateData: { dynamicTemplateData: {
siteUrl: process.env.SITE_URL, siteUrl: process.env.SITE_URL,
token: token, token: token,
username: username username: username,
}, },
from: 'me@dmitriy.icu', from: 'me@dmitriy.icu',
subject: 'Login link', subject: 'Login link',
templateId: 'd-a9275a4437bf4dd2b9e858f3a57f85d5', templateId: 'd-a9275a4437bf4dd2b9e858f3a57f85d5',
to: email to: email,
}) })
} }

View File

@ -2,7 +2,7 @@ import { ChoiseType } from '@prisma/client'
import { import {
ChoisesQuestion, ChoisesQuestion,
InputQuestion, InputQuestion,
Variant Variant,
} from '../typeDefs/typeDefs.gen' } from '../typeDefs/typeDefs.gen'
import { JwtPayloadType } from '../types' import { JwtPayloadType } from '../types'
@ -48,5 +48,5 @@ export {
FormConstructor, FormConstructor,
UploadedChoisesQuestion, UploadedChoisesQuestion,
UploadedInputQuestion, UploadedInputQuestion,
UploadedQuestion UploadedQuestion,
} }

View File

@ -3,6 +3,7 @@ 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 { ApolloError, UserInputError } from 'apollo-server-express' import { ApolloError, UserInputError } from 'apollo-server-express'
import { formatForms } from './form'
const createUser = async ( const createUser = async (
db: PrismaClient, db: PrismaClient,
@ -31,9 +32,14 @@ const findUserBy = async (
params: IFindUserParams params: IFindUserParams
): Promise<User> => { ): Promise<User> => {
try { try {
const user = await findDBUserBy(db, params) const dbUser = await findDBUserBy(db, params)
if (!user) throw new UserInputError('No such user') if (!dbUser) throw new UserInputError('No such user')
const user = {
...dbUser,
forms: formatForms(dbUser.forms),
}
return user return user
} catch (err) { } catch (err) {

View File

@ -19,7 +19,7 @@ const getDBForm = (
formId: number, formId: number,
{ {
requesterId, requesterId,
userId userId,
}: { }: {
requesterId: number requesterId: number
userId: number userId: number
@ -31,32 +31,32 @@ const getDBForm = (
select: { select: {
email: true, email: true,
id: true, id: true,
name: true name: true,
} },
}, },
choisesQuestions: { choisesQuestions: {
include: { include: {
variants: true variants: true,
} },
}, },
inputQuestions: true, inputQuestions: true,
submissions: { submissions: {
include: { include: {
answers: true answers: true,
}, },
where: where:
requesterId != userId requesterId != userId
? { ? {
user: { user: {
id: requesterId id: requesterId,
} },
} }
: undefined : undefined,
} },
}, },
where: { where: {
id: formId id: formId,
} },
}) })
/** /**
@ -71,21 +71,21 @@ const getDBFormsByUser = (db: PrismaClient, id: number) =>
include: { include: {
choisesQuestions: { choisesQuestions: {
include: { include: {
variants: true variants: true,
} },
}, },
inputQuestions: true, inputQuestions: true,
submissions: { submissions: {
include: { include: {
answers: true answers: true,
} },
} },
}, },
where: { where: {
author: { author: {
id id,
} },
} },
}) })
const getDBFormAuthor = (db: PrismaClient, id: number) => const getDBFormAuthor = (db: PrismaClient, id: number) =>
@ -93,13 +93,13 @@ const getDBFormAuthor = (db: PrismaClient, id: number) =>
select: { select: {
author: { author: {
select: { select: {
id: true id: true,
} },
} },
}, },
where: { where: {
id id,
} },
}) })
const createDBUser = ( const createDBUser = (
@ -107,24 +107,46 @@ const createDBUser = (
{ email, name }: MutationRegisterArgs { email, name }: MutationRegisterArgs
) => ) =>
db.user.create({ db.user.create({
data: { email, name } data: { email, name },
}) })
const findDBUserBy = (db: PrismaClient, params: IFindUserParams) => const findDBUserBy = (db: PrismaClient, params: IFindUserParams) =>
db.user.findOne({ db.user.findOne({
where: { where: {
...params ...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) => const createDBForm = (db: PrismaClient, form: FormConstructor, id: number) =>
db.form.create({ db.form.create({
data: { data: {
author: { author: {
connect: { id } connect: { id },
}, },
...form ...form,
} },
}) })
const submitDBAnswer = ( const submitDBAnswer = (
@ -136,19 +158,19 @@ const submitDBAnswer = (
db.formSubmission.create({ db.formSubmission.create({
data: { data: {
answers: { answers: {
create: formAnswers create: formAnswers,
}, },
Form: { Form: {
connect: { connect: {
id: formId id: formId,
} },
}, },
user: { user: {
connect: { connect: {
id: userId id: userId,
} },
} },
} },
}) })
export { export {
@ -158,5 +180,5 @@ export {
getDBForm, getDBForm,
getDBFormAuthor, getDBFormAuthor,
getDBFormsByUser, getDBFormsByUser,
submitDBAnswer submitDBAnswer,
} }

View File

@ -14,30 +14,31 @@ app.use(
expressJwt({ expressJwt({
algorithms: ['HS256'], algorithms: ['HS256'],
credentialsRequired: false, credentialsRequired: false,
secret: '' + process.env.JWT_SECRET secret: '' + process.env.JWT_SECRET,
}) })
) )
const db = new PrismaClient()
const server = new ApolloServer({ const server = new ApolloServer({
context: async ({ context: async ({
req req,
}: { }: {
req: Request & { req: Request & {
user: JwtPayloadType user: JwtPayloadType
} }
}): Promise<ApolloContextType> => { }): Promise<ApolloContextType> => {
const db = new PrismaClient()
const user = req.user || null const user = req.user || null
return { return {
db, db,
user user,
} }
}, },
debug: false, debug: false,
schema: makeExecutableSchema({ schema: makeExecutableSchema({
resolvers, resolvers,
typeDefs typeDefs,
}) }),
}) })
server.applyMiddleware({ app }) server.applyMiddleware({ app })

View File

@ -6,7 +6,7 @@ import {
QueryFormArgs, QueryFormArgs,
QuestionResolvers, QuestionResolvers,
Resolver, Resolver,
ServerAnswer ServerAnswer,
} from '../typeDefs/typeDefs.gen' } from '../typeDefs/typeDefs.gen'
import { ApolloContextType } from '../types' import { ApolloContextType } from '../types'
import { import {
@ -15,7 +15,7 @@ import {
getForm, getForm,
getFormAuthor, getFormAuthor,
getForms, getForms,
submitAnswer submitAnswer,
} from '../controllers' } from '../controllers'
const formQuery: Resolver<Form, {}, ApolloContextType, QueryFormArgs> = async ( const formQuery: Resolver<Form, {}, ApolloContextType, QueryFormArgs> = async (
@ -33,9 +33,9 @@ const formQuery: Resolver<Form, {}, ApolloContextType, QueryFormArgs> = async (
controller: getFormById, controller: getFormById,
expected: { expected: {
id: 0, id: 0,
self: true self: true,
}, },
user user,
}) })
} catch (err) { } catch (err) {
return err return err
@ -57,9 +57,9 @@ const formsQuery: Resolver<Form[], {}, ApolloContextType> = async (
controller: getFormsByUserId, controller: getFormsByUserId,
expected: { expected: {
id: 0, id: 0,
self: true self: true,
}, },
user user,
}) })
} catch (err) { } catch (err) {
return err return err
@ -78,9 +78,9 @@ const createFormMutation: Resolver<
controller: createNewForm, controller: createNewForm,
expected: { expected: {
id: 0, id: 0,
self: true self: true,
}, },
user user,
}) })
} }
@ -96,9 +96,9 @@ const formSubmitMutation: Resolver<
controller: submitNewAnswer, controller: submitNewAnswer,
expected: { expected: {
id: 0, id: 0,
self: true self: true,
}, },
user user,
}) })
} }
@ -108,7 +108,7 @@ const QuestionResolver: QuestionResolvers = {
return 'ChoisesQuestion' return 'ChoisesQuestion'
} }
return 'InputQuestion' return 'InputQuestion'
} },
} }
const AnswerResolver: AnswerResolvers = { const AnswerResolver: AnswerResolvers = {
@ -117,7 +117,7 @@ const AnswerResolver: AnswerResolvers = {
if (obj.type == 'INPUT') return 'InputAnswer' if (obj.type == 'INPUT') return 'InputAnswer'
return null return null
} },
} }
export { export {
@ -126,5 +126,5 @@ export {
formQuery, formQuery,
formsQuery, formsQuery,
formSubmitMutation, formSubmitMutation,
QuestionResolver QuestionResolver,
} }

View File

@ -1,7 +1,7 @@
import { import {
checkRightsAndResolve, checkRightsAndResolve,
findUserBy, findUserBy,
genAndSendToken genAndSendToken,
} from '../controllers' } from '../controllers'
import { createUser } from '../controllers/user' import { createUser } from '../controllers/user'
import { import {
@ -10,7 +10,7 @@ import {
Resolver, Resolver,
ServerAnswer, ServerAnswer,
User, User,
QueryUserArgs QueryUserArgs,
} from '../typeDefs/typeDefs.gen' } from '../typeDefs/typeDefs.gen'
import { ApolloContextType } from '../types' import { ApolloContextType } from '../types'
@ -23,6 +23,8 @@ const loginMutation: Resolver<
try { try {
const user = await findUserBy(db, { email }) const user = await findUserBy(db, { email })
if (user instanceof Error) throw user // Needed to a strange error
await genAndSendToken(email, user) await genAndSendToken(email, user)
return { success: true } return { success: true }
@ -40,6 +42,8 @@ const registerMutation: Resolver<
try { try {
const user = await createUser(db, { email, name }) const user = await createUser(db, { email, name })
if (user instanceof Error) throw user // Needed to a strange error
await genAndSendToken(email, user) await genAndSendToken(email, user)
return { success: true } return { success: true }
@ -60,9 +64,9 @@ const userQuery: Resolver<User, {}, ApolloContextType, QueryUserArgs> = async (
controller: findUserById, controller: findUserById,
expected: { expected: {
id: id || 0, id: id || 0,
self: true self: true,
}, },
user user,
}) })
} catch (err) { } catch (err) {
return err return err

View File

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

View File

@ -4,11 +4,13 @@
"module": "CommonJS" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', '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. */, "strict": true /* Enable all strict type-checking options. */,
"outDir": "dist", "outDir": "dist",
"noImplicitAny": true,
"moduleResolution": "Node", "moduleResolution": "Node",
"baseUrl": "src",
"incremental": true, "incremental": true,
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
"skipLibCheck": true /* Skip type checking of declaration files. */, "skipLibCheck": true /* Skip type checking of declaration files. */,
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
}, },
"include": ["src"] "include": ["src/**/*"]
} }