From 27ab311741ed702fe32bc9e79fe4127225f1b7ca Mon Sep 17 00:00:00 2001 From: danila Date: Mon, 14 Oct 2024 23:46:07 +0200 Subject: [PATCH 01/11] init graphql --- backend/.env.sample | 5 + backend/controllers/book.api.ts | 5 +- backend/controllers/chapter.api.ts | 2 +- backend/graphql/resolvers.ts | 25 +++++ backend/graphql/schema.ts | 49 ++++++++++ backend/package-lock.json | 44 +++++++-- backend/package.json | 6 +- backend/routes/auth.ts | 2 +- backend/server.ts | 150 +++++++++++++++-------------- 9 files changed, 202 insertions(+), 86 deletions(-) create mode 100644 backend/.env.sample create mode 100644 backend/graphql/resolvers.ts create mode 100644 backend/graphql/schema.ts diff --git a/backend/.env.sample b/backend/.env.sample new file mode 100644 index 0000000..a55aca8 --- /dev/null +++ b/backend/.env.sample @@ -0,0 +1,5 @@ +DATABASE_URL= +PORT= +DPWC_TOKEN= +AWS_LAMBDA_NEWUSER_URL= +SECRET= \ No newline at end of file diff --git a/backend/controllers/book.api.ts b/backend/controllers/book.api.ts index 9158e08..5b71002 100644 --- a/backend/controllers/book.api.ts +++ b/backend/controllers/book.api.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from 'express'; import mongoose from 'mongoose'; -import { getOptions } from './../helpers/config'; +import { getOptions } from '../helpers/config'; import { BookModel } from '../models/book.model'; import { ChapterModel } from '../models/chapter.model'; @@ -18,7 +18,8 @@ export const bookController = { getBook: async (req: Request, res: Response, next: NextFunction) => { const options = await getOptions(req); - + console.log('options', options); + console.log(req, res, next); try { const id = req.params.id; const book = await BookModel.paginate({ _id: id }, options); diff --git a/backend/controllers/chapter.api.ts b/backend/controllers/chapter.api.ts index e2a6930..4f500e8 100644 --- a/backend/controllers/chapter.api.ts +++ b/backend/controllers/chapter.api.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from 'express'; -import { getOptions } from './../helpers/config'; +import { getOptions } from '../helpers/config'; import { ChapterModel } from '../models/chapter.model'; export const chapterController = { diff --git a/backend/graphql/resolvers.ts b/backend/graphql/resolvers.ts new file mode 100644 index 0000000..5c7000e --- /dev/null +++ b/backend/graphql/resolvers.ts @@ -0,0 +1,25 @@ +import {bookController} from '../controllers/book.api' +import {chapterController} from '../controllers/chapter.api' +import {movieController} from '../controllers/movie.api' +import {characterController} from '../controllers/character.api' +import {quoteController} from '../controllers/quote.api' + +const resolvers = { + // books: () => bookController.getBooks(), + book: (a:any, body:{id:number}, context:any) => { + + }, + // chaptersByBook: ({ bookId }: {bookId:number}) => bookController.getChaptersByBook({ params: { id: bookId } }), + // chapters: () => chapterController.getChapters(), + // chapter: ({ id }: {id:number}) => chapterController.getChapter({ params: { id } }), + // movies: () => movieController.getMovies(), + // movie: ({ id }: {id:number}) => movieController.getMovie({ params: { id } }), + // quoteByMovie: ({ movieId }: {movieId:number}) => movieController.getQuoteByMovie({ params: { id: movieId } }), + // characters: () => characterController.getCharacters(), + // character: ({ id }: {id:number}) => characterController.getCharacter({ params: { id } }), + // quoteByCharacter: ({ characterId }: {characterId:number}) => characterController.getQuoteByCharacter({ params: { id: characterId } }), + // quotes: () => quoteController.getQuotes(), + // quote: ({ id }: {id:number}) => quoteController.getQuote({ params: { id } }) +} + +export default resolvers \ No newline at end of file diff --git a/backend/graphql/schema.ts b/backend/graphql/schema.ts new file mode 100644 index 0000000..0d543c0 --- /dev/null +++ b/backend/graphql/schema.ts @@ -0,0 +1,49 @@ +import {buildSchema} from "graphql/utilities"; + +const schema = buildSchema(` + type Query { + books: [Book] + book(id: ID!): Book + chaptersByBook(bookId: ID!): [Chapter] + chapters: [Chapter] + chapter(id: ID!): Chapter + movies: [Movie] + movie(id: ID!): Movie + quoteByMovie(movieId: ID!): [Quote] + characters: [Character] + character(id: ID!): Character + quoteByCharacter(characterId: ID!): [Quote] + quotes: [Quote] + quote(id: ID!): Quote + } + + type Book { + id: ID + name: String + author: String + } + + type Chapter { + id: ID + title: String + content: String + } + + type Movie { + id: ID + title: String + director: String + } + + type Character { + id: ID + name: String + } + + type Quote { + id: ID + text: String + } +`); + +export default schema; \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json index 62574a8..763bcc0 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -13,8 +13,10 @@ "bcryptjs": "^2.4.3", "cors": "^2.8.5", "dotenv": "^8.6.0", - "express": "^4.21.0", + "express": "^4.21.1", "express-rate-limit": "^5.5.1", + "graphql": "^16.9.0", + "graphql-http": "^1.22.1", "helmet": "^4.6.0", "jsonwebtoken": "^9.0.2", "mongoose": "^5.13.21", @@ -3883,9 +3885,10 @@ "dev": true }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -4263,16 +4266,17 @@ } }, "node_modules/express": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", - "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -4626,6 +4630,30 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, + "node_modules/graphql": { + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz", + "integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==", + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/graphql-http": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/graphql-http/-/graphql-http-1.22.1.tgz", + "integrity": "sha512-4Jor+LRbA7SfSaw7dfDUs2UBzvWg3cKrykfHRgKsOIvQaLuf+QOcG2t3Mx5N9GzSNJcuqMqJWz0ta5+BryEmXg==", + "license": "MIT", + "workspaces": [ + "implementations/**/*" + ], + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "graphql": ">=0.11 <=16" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", diff --git a/backend/package.json b/backend/package.json index 75389f0..4a90bd9 100644 --- a/backend/package.json +++ b/backend/package.json @@ -22,8 +22,10 @@ "bcryptjs": "^2.4.3", "cors": "^2.8.5", "dotenv": "^8.6.0", - "express": "^4.21.0", + "express": "^4.21.1", "express-rate-limit": "^5.5.1", + "graphql": "^16.9.0", + "graphql-http": "^1.22.1", "helmet": "^4.6.0", "jsonwebtoken": "^9.0.2", "mongoose": "^5.13.21", @@ -68,4 +70,4 @@ "ts-node": "^10.9.1", "typescript": "^5.3.2" } -} \ No newline at end of file +} diff --git a/backend/routes/auth.ts b/backend/routes/auth.ts index 270f92c..bc64f9f 100644 --- a/backend/routes/auth.ts +++ b/backend/routes/auth.ts @@ -1,6 +1,6 @@ import { Router } from 'express'; import { authController } from '../controllers/auth'; -import { passportHelpers } from './../helpers/passport'; +import { passportHelpers } from '../helpers/passport'; import authLimiter from '../middleware/auth.limiter'; import { HttpCode, notFoundResponse } from '../helpers/constants'; diff --git a/backend/server.ts b/backend/server.ts index 7af6d72..f48d6d7 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -8,23 +8,25 @@ import cors from 'cors'; import helmet from 'helmet'; import path from 'path'; -import { connectDb } from './helpers/db'; -import { apiLimiter } from './middleware/api.limiter'; -import { UserModel } from './models/user.model'; -import { Strategy as LocalStrategy } from 'passport-local'; -import { Strategy as BearerStrategy } from 'passport-http-bearer'; - -import { HttpCode } from './helpers/constants'; +import {connectDb} from './helpers/db'; +import {apiLimiter} from './middleware/api.limiter'; +import {UserModel} from './models/user.model'; +import {Strategy as LocalStrategy} from 'passport-local'; +import {Strategy as BearerStrategy} from 'passport-http-bearer'; +import {HttpCode} from './helpers/constants'; import apiRoutes from './routes/api'; import authRoutes from './routes/auth'; -import { User } from './helpers/interfaces'; +import {User} from './helpers/interfaces'; +import {createHandler} from 'graphql-http'; +import schema from './graphql/schema'; +import root from './graphql/resolvers'; const app = express(); const dpwcToken = process.env.DPWC_TOKEN || ''; app.use(helmet()); app.use(express.json()); -app.use(express.urlencoded({ extended: false })); +app.use(express.urlencoded({extended: false})); app.use(cors()); app.use(express.static(path.join(__dirname, '/../__BUILD'))); // React build @@ -32,86 +34,90 @@ app.use(express.static(path.join(__dirname, '/../__BUILD'))); // React build const server_port = process.env.PORT || 3001; passport.use( - new BearerStrategy(async (token, done) => { - try { - if (dpwcToken === token) { - return done(null, token, { message: 'Token valid.', scope: 'read' }); - } - await UserModel.findOne({ access_token: token }, async function (err: unknown, user: User) { - if (err) { - return done(err, false, { message: 'Invalid token.', scope: 'read' }); - } - if (!user) { - return done(err, false, { message: 'Wrong token.', scope: 'read' }); - } - return done(null, token, { message: 'Token valid.', scope: 'read' }); - }); - } catch (error) { - done(error); - } - }) + new BearerStrategy(async (token, done) => { + try { + if (dpwcToken === token) { + return done(null, token, {message: 'Token valid.', scope: 'read'}); + } + await UserModel.findOne({access_token: token}, async function (err: unknown, user: User) { + if (err) { + return done(err, false, {message: 'Invalid token.', scope: 'read'}); + } + if (!user) { + return done(err, false, {message: 'Wrong token.', scope: 'read'}); + } + return done(null, token, {message: 'Token valid.', scope: 'read'}); + }); + } catch (error) { + done(error); + } + }) ); // prepare jwt login passport.use( - 'login', - new LocalStrategy( - { - usernameField: 'email', - passwordField: 'password' - }, - async (username: string, password: string, done: Function) => { - try { - await UserModel.findOne({ email: username }, async function (err: unknown, user: User) { - if (err) { - return done(err); - } - if (!user) { - return done(null, false, { message: 'Incorrect username.' }); - } - let passwordMatch = await bcrypt.compare(password, user.password); - if (passwordMatch) { - return done(null, user, { message: 'Logged in Successfully' }); - } else { - return done(null, false, { message: 'Incorrect password.' }); - } - }); - } catch (error) { - done(error); - } - } - ) + 'login', + new LocalStrategy( + { + usernameField: 'email', + passwordField: 'password' + }, + async (username: string, password: string, done: Function) => { + try { + await UserModel.findOne({email: username}, async function (err: unknown, user: User) { + if (err) { + return done(err); + } + if (!user) { + return done(null, false, {message: 'Incorrect username.'}); + } + let passwordMatch = await bcrypt.compare(password, user.password); + if (passwordMatch) { + return done(null, user, {message: 'Logged in Successfully'}); + } else { + return done(null, false, {message: 'Incorrect password.'}); + } + }); + } catch (error) { + done(error); + } + } + ) ); app.use((req, res, next) => { - const path = req.path; - if (path.startsWith('/v2') || path.startsWith('/auth')) { - const connected = mongoose.connection.readyState === 1 ? true : false; - if (connected) { - next(); - } else { - return res.status(HttpCode.SERVER_ERROR).send({ - success: false, - message: 'Service currently not available.' - }); - } - } else { - next(); - } + const path = req.path; + if (path.startsWith('/v2') || path.startsWith('/auth') || path.startsWith('/graphql')) { + const connected = mongoose.connection.readyState === 1 ? true : false; + if (connected) { + next(); + } else { + return res.status(HttpCode.SERVER_ERROR).send({ + success: false, + message: 'Service currently not available.' + }); + } + } else { + next(); + } }); - +app.all("/graphql", createHandler({ + schema: schema, + rootValue: root, + context:(req) => ({req}) +})) app.use('/v2/', apiLimiter); app.use('/v2', apiRoutes); app.use('/auth', authRoutes); // Handles React frontend requests app.get('*', (req, res) => { - res.sendFile(path.join(__dirname + '/../__BUILD/index.html')); + res.sendFile(path.join(__dirname + '/../__BUILD/index.html')); }); async function start() { - await connectDb(); - app.listen(server_port, () => console.log(`LotR backend listening on port ${server_port}!`)); + await connectDb(); + app.listen(server_port, () => console.log(`LotR backend listening on port ${server_port}!`)); } start(); From 30a48c9c62f20cbf185edbb48c447674c1706ec7 Mon Sep 17 00:00:00 2001 From: danila Date: Thu, 17 Oct 2024 00:13:54 +0200 Subject: [PATCH 02/11] add graphql to rest relay --- backend/controllers/book.api.ts | 2 - backend/graphql/resolvers.ts | 12 ++-- backend/graphql/schema.ts | 99 ++++++++++++++++++++----- backend/graphql/types.ts | 0 backend/helpers/config.ts | 123 ++++++++++++++++++++------------ backend/server.ts | 8 +-- 6 files changed, 172 insertions(+), 72 deletions(-) create mode 100644 backend/graphql/types.ts diff --git a/backend/controllers/book.api.ts b/backend/controllers/book.api.ts index 5b71002..12036a1 100644 --- a/backend/controllers/book.api.ts +++ b/backend/controllers/book.api.ts @@ -18,8 +18,6 @@ export const bookController = { getBook: async (req: Request, res: Response, next: NextFunction) => { const options = await getOptions(req); - console.log('options', options); - console.log(req, res, next); try { const id = req.params.id; const book = await BookModel.paginate({ _id: id }, options); diff --git a/backend/graphql/resolvers.ts b/backend/graphql/resolvers.ts index 5c7000e..9703086 100644 --- a/backend/graphql/resolvers.ts +++ b/backend/graphql/resolvers.ts @@ -3,12 +3,16 @@ import {chapterController} from '../controllers/chapter.api' import {movieController} from '../controllers/movie.api' import {characterController} from '../controllers/character.api' import {quoteController} from '../controllers/quote.api' - +import {IGraphQLContext, GraphQLBody} from "./schema"; +import {createRESTArgumentsFromGraphqlRequest} from '../helpers/config' const resolvers = { // books: () => bookController.getBooks(), - book: (a:any, body:{id:number}, context:any) => { - - }, + book: async (body:GraphQLBody<{id:string}>, context:IGraphQLContext) => { + const {req,res,next} = createRESTArgumentsFromGraphqlRequest(context, body, 'book'); + const data = await bookController.getBook(req,res,next); + console.log(data) + return data + } // chaptersByBook: ({ bookId }: {bookId:number}) => bookController.getChaptersByBook({ params: { id: bookId } }), // chapters: () => chapterController.getChapters(), // chapter: ({ id }: {id:number}) => chapterController.getChapter({ params: { id } }), diff --git a/backend/graphql/schema.ts b/backend/graphql/schema.ts index 0d543c0..6a2c002 100644 --- a/backend/graphql/schema.ts +++ b/backend/graphql/schema.ts @@ -2,25 +2,60 @@ import {buildSchema} from "graphql/utilities"; const schema = buildSchema(` type Query { - books: [Book] - book(id: ID!): Book - chaptersByBook(bookId: ID!): [Chapter] + books: BooksResponse + book(id: ID!, pagination:PaginationInput): BooksResponse + chaptersByBook(bookId: ID!): ChaptersResponse + chapters: ChaptersResponse + chapter(id: ID!): ChaptersResponse + movies: MoviesResponse + movie(id: ID!): MoviesResponse + quoteByMovie(movieId: ID!): QuotesResponse + characters: CharactersResponse + character(id: ID!): CharactersResponse + quoteByCharacter(characterId: ID!): QuotesResponse + quotes: QuotesResponse + quote(id: ID!): QuotesResponse + } + type BooksResponse { + _id: ID + name: String + total: Int + limit: Int + page: Int + pages: Int + } + type ChaptersResponse { chapters: [Chapter] - chapter(id: ID!): Chapter - movies: [Movie] - movie(id: ID!): Movie - quoteByMovie(movieId: ID!): [Quote] - characters: [Character] - character(id: ID!): Character - quoteByCharacter(characterId: ID!): [Quote] - quotes: [Quote] - quote(id: ID!): Quote + total: Int + limit: Int + page: Int + pages: Int } - + type MoviesResponse { + movies: [Movie] + total: Int + limit: Int + page: Int + pages: Int + } + type CharactersResponse { + characters: [Character] + total: Int + limit: Int + page: Int + pages: Int + } + type QuotesResponse { + quotes: [Quote] + total: Int + limit: Int + page: Int + pages: Int + } + type Book { - id: ID + _id: ID name: String - author: String } type Chapter { @@ -44,6 +79,38 @@ const schema = buildSchema(` id: ID text: String } + type Pagination { + total: Int + limit: Int + page: Int + pages: Int + } + input PaginationInput { + sort: String + limit: Int + page: Int + offset: Int + } `); -export default schema; \ No newline at end of file + +export default schema; + +export interface IGraphQLContext { + requestInfo: { + req: Express.Request; + context: { + res: Express.Response; + } + } +} + +export type GraphQLBody = T & { + sort?: string + limit?: string + page?: string + offset?: string +} + + +export type DataNames = 'books' | 'book' | 'chaptersByBook' | 'chapters' | 'chapter' | 'movies' | 'movie' | 'quoteByMovie' | 'characters' | 'character' | 'quoteByCharacter' | 'quotes' | 'quote' \ No newline at end of file diff --git a/backend/graphql/types.ts b/backend/graphql/types.ts new file mode 100644 index 0000000..e69de29 diff --git a/backend/helpers/config.ts b/backend/helpers/config.ts index 6678654..afe42be 100644 --- a/backend/helpers/config.ts +++ b/backend/helpers/config.ts @@ -1,50 +1,83 @@ -import { Request } from 'express'; -import { MongooseQueryParser } from 'mongoose-query-parser'; - -import { PaginateOptions } from './interfaces'; - +import {Request} from 'express'; +import {MongooseQueryParser} from 'mongoose-query-parser'; +import {PaginateOptions} from './interfaces'; +import {IGraphQLContext, DataNames} from "../graphql/schema"; +import {ParamsDictionary} from "express-serve-static-core"; +import {ParsedQs} from "qs"; const ascending = 'asc'; const maxLimit = 1000; export const getOptions = async (req: Request): Promise => { - let options: PaginateOptions = { filter: {} }; - - const sort = req?.query?.sort as string; - const page = req?.query?.page as string; - const limit = req?.query?.limit as string; - const offset = req?.query?.offset as string; - - // Express does not offer a handy way to get the raw query strings - // so let's parse it and drop the leading `?` for the parser - const url = new URL(req.protocol + '://' + req.hostname + req.originalUrl); - const rawQueryParams = url.search.slice(1); - - const parser = new MongooseQueryParser({ - blacklist: ['offset', 'page', 'limit', 'sort'] - }); - const parsed = parser.parse(rawQueryParams); - options.filter = parsed.filter; - - if (sort) { - const fields = sort.split(':'); - const sorter = fields[0]; - const direction = fields[1]; - options.sort = { [sorter]: direction === ascending ? 1 : -1 }; - } - - if (limit) { - options.limit = parseInt(limit); - } else { - options.limit = maxLimit; - } - - if (page) { - options.page = parseInt(page); - } - - if (offset) { - options.offset = parseInt(offset); - } - - return options; + let options: PaginateOptions = {filter: {}}; + + const sort = req?.query?.sort as string; + const page = req?.query?.page as string; + const limit = req?.query?.limit as string; + const offset = req?.query?.offset as string; + + // Express does not offer a handy way to get the raw query strings + // so let's parse it and drop the leading `?` for the parser + const url = new URL(req.protocol + '://' + req.hostname + req.originalUrl); + const rawQueryParams = url.search.slice(1); + + const parser = new MongooseQueryParser({ + blacklist: ['offset', 'page', 'limit', 'sort'] + }); + const parsed = parser.parse(rawQueryParams); + options.filter = parsed.filter; + + if (sort) { + const fields = sort.split(':'); + const sorter = fields[0]; + const direction = fields[1]; + options.sort = {[sorter]: direction === ascending ? 1 : -1}; + } + + if (limit) { + options.limit = parseInt(limit); + } else { + options.limit = maxLimit; + } + + if (page) { + options.page = parseInt(page); + } + + if (offset) { + options.offset = parseInt(offset); + } + + return options; }; + +export const createRESTArgumentsFromGraphqlRequest = (context: IGraphQLContext, bodyPayload: any, dataName: DataNames) => { + const req = { + ...context.requestInfo.req, + params: bodyPayload + } as Request<{params:typeof bodyPayload}, any, any, ParsedQs, Record>; + function isPlural(dataName: DataNames): boolean { + return dataName.endsWith('s'); + } + const res = { + ...context.requestInfo.context.res, + json: (data: any) => { + console.log(data.docs[0].toObject()) + const targetData = isPlural(dataName) ? {[dataName]:data.docs} : data.docs[0].toObject(); + const {pages, page, offset, limit, total} = data + const returnData = { + [dataName]: targetData, + pages, + page, + offset, + limit, + total + }; + return returnData + } + } as any; + const next = (err: any) => { + throw new Error(err); + }; + return {req, res, next}; +} + diff --git a/backend/server.ts b/backend/server.ts index f48d6d7..770e561 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -1,5 +1,4 @@ require('dotenv').config(); - import mongoose from 'mongoose'; import express from 'express'; import bcrypt from 'bcrypt'; @@ -17,13 +16,12 @@ import {HttpCode} from './helpers/constants'; import apiRoutes from './routes/api'; import authRoutes from './routes/auth'; import {User} from './helpers/interfaces'; -import {createHandler} from 'graphql-http'; +import {createHandler} from 'graphql-http/lib/use/express'; import schema from './graphql/schema'; import root from './graphql/resolvers'; const app = express(); const dpwcToken = process.env.DPWC_TOKEN || ''; - app.use(helmet()); app.use(express.json()); app.use(express.urlencoded({extended: false})); @@ -88,7 +86,7 @@ passport.use( app.use((req, res, next) => { const path = req.path; if (path.startsWith('/v2') || path.startsWith('/auth') || path.startsWith('/graphql')) { - const connected = mongoose.connection.readyState === 1 ? true : false; + const connected = mongoose.connection.readyState === 1; if (connected) { next(); } else { @@ -104,7 +102,7 @@ app.use((req, res, next) => { app.all("/graphql", createHandler({ schema: schema, rootValue: root, - context:(req) => ({req}) + context: (req) => ({ requestInfo:req }) })) app.use('/v2/', apiLimiter); app.use('/v2', apiRoutes); From 25f8cb6a90440c40ec41a8da6bdf3d29a38d2c9e Mon Sep 17 00:00:00 2001 From: danila Date: Thu, 17 Oct 2024 00:46:29 +0200 Subject: [PATCH 03/11] add schema for books --- backend/graphql/resolvers.ts | 9 ++++-- backend/graphql/schema.ts | 59 +++++++++++++++++++++++++++++++++--- backend/helpers/config.ts | 3 +- 3 files changed, 62 insertions(+), 9 deletions(-) diff --git a/backend/graphql/resolvers.ts b/backend/graphql/resolvers.ts index 9703086..8378e94 100644 --- a/backend/graphql/resolvers.ts +++ b/backend/graphql/resolvers.ts @@ -6,13 +6,18 @@ import {quoteController} from '../controllers/quote.api' import {IGraphQLContext, GraphQLBody} from "./schema"; import {createRESTArgumentsFromGraphqlRequest} from '../helpers/config' const resolvers = { - // books: () => bookController.getBooks(), book: async (body:GraphQLBody<{id:string}>, context:IGraphQLContext) => { const {req,res,next} = createRESTArgumentsFromGraphqlRequest(context, body, 'book'); const data = await bookController.getBook(req,res,next); console.log(data) return data - } + }, + books: async (body:GraphQLBody<{}>, context:IGraphQLContext) => { + const {req,res,next} = createRESTArgumentsFromGraphqlRequest(context, body, 'books'); + const data = await bookController.getBooks(req,res,next); + console.log(data) + return data + }, // chaptersByBook: ({ bookId }: {bookId:number}) => bookController.getChaptersByBook({ params: { id: bookId } }), // chapters: () => chapterController.getChapters(), // chapter: ({ id }: {id:number}) => chapterController.getChapter({ params: { id } }), diff --git a/backend/graphql/schema.ts b/backend/graphql/schema.ts index 6a2c002..b508889 100644 --- a/backend/graphql/schema.ts +++ b/backend/graphql/schema.ts @@ -3,7 +3,7 @@ import {buildSchema} from "graphql/utilities"; const schema = buildSchema(` type Query { books: BooksResponse - book(id: ID!, pagination:PaginationInput): BooksResponse + book(id: ID!, pagination:PaginationInput): BookResponse chaptersByBook(bookId: ID!): ChaptersResponse chapters: ChaptersResponse chapter(id: ID!): ChaptersResponse @@ -16,9 +16,22 @@ const schema = buildSchema(` quotes: QuotesResponse quote(id: ID!): QuotesResponse } - type BooksResponse { - _id: ID - name: String + type BooksResponse { + books: [Book] + total: Int + limit: Int + page: Int + pages: Int + } + type BookResponse { + book: Book + total: Int + limit: Int + page: Int + pages: Int + } + type ChapterResponse { + chapter: Chapter total: Int limit: Int page: Int @@ -31,6 +44,13 @@ const schema = buildSchema(` page: Int pages: Int } + type MovieResponse { + movie: Movie + limit: Int + page: Int + pages: Int + } + type MoviesResponse { movies: [Movie] total: Int @@ -38,6 +58,13 @@ const schema = buildSchema(` page: Int pages: Int } + type CharacterResponse { + character: Character + total: Int + limit: Int + page: Int + pages: Int + } type CharactersResponse { characters: [Character] total: Int @@ -45,6 +72,15 @@ const schema = buildSchema(` page: Int pages: Int } + type QuoteResponse { + quote: Quote + total: Int + limit: Int + page: Int + pages: Int + } + + type QuotesResponse { quotes: [Quote] total: Int @@ -113,4 +149,17 @@ export type GraphQLBody = T & { } -export type DataNames = 'books' | 'book' | 'chaptersByBook' | 'chapters' | 'chapter' | 'movies' | 'movie' | 'quoteByMovie' | 'characters' | 'character' | 'quoteByCharacter' | 'quotes' | 'quote' \ No newline at end of file +export type DataNames = + 'books' + | 'book' + | 'chaptersByBook' + | 'chapters' + | 'chapter' + | 'movies' + | 'movie' + | 'quoteByMovie' + | 'characters' + | 'character' + | 'quoteByCharacter' + | 'quotes' + | 'quote' \ No newline at end of file diff --git a/backend/helpers/config.ts b/backend/helpers/config.ts index afe42be..48507b7 100644 --- a/backend/helpers/config.ts +++ b/backend/helpers/config.ts @@ -61,8 +61,7 @@ export const createRESTArgumentsFromGraphqlRequest = (context: IGraphQLContext, const res = { ...context.requestInfo.context.res, json: (data: any) => { - console.log(data.docs[0].toObject()) - const targetData = isPlural(dataName) ? {[dataName]:data.docs} : data.docs[0].toObject(); + const targetData = isPlural(dataName) ? data.docs : data.docs[0].toObject(); const {pages, page, offset, limit, total} = data const returnData = { [dataName]: targetData, From 14e9ab1ba9b46aada9b9a54152601ef3b660cffe Mon Sep 17 00:00:00 2001 From: danila Date: Fri, 18 Oct 2024 00:45:46 +0200 Subject: [PATCH 04/11] keep adding resolvers, appending to the corresponding data types connected data from other tables --- backend/graphql/resolvers.ts | 177 +++++++++++++++++++++++++++++------ backend/graphql/root.ts | 18 ++++ backend/graphql/schema.ts | 87 ++++++++++++++--- backend/helpers/config.ts | 9 +- backend/server.ts | 2 +- 5 files changed, 252 insertions(+), 41 deletions(-) create mode 100644 backend/graphql/root.ts diff --git a/backend/graphql/resolvers.ts b/backend/graphql/resolvers.ts index 8378e94..7533232 100644 --- a/backend/graphql/resolvers.ts +++ b/backend/graphql/resolvers.ts @@ -3,32 +3,153 @@ import {chapterController} from '../controllers/chapter.api' import {movieController} from '../controllers/movie.api' import {characterController} from '../controllers/character.api' import {quoteController} from '../controllers/quote.api' -import {IGraphQLContext, GraphQLBody} from "./schema"; +import { + IGraphQLContext, + GraphQLBody, + GraphQLResponse, + Book, + Chapter, + Character, + Movie, + Quote, + DataNames +} from "./schema"; import {createRESTArgumentsFromGraphqlRequest} from '../helpers/config' -const resolvers = { - book: async (body:GraphQLBody<{id:string}>, context:IGraphQLContext) => { - const {req,res,next} = createRESTArgumentsFromGraphqlRequest(context, body, 'book'); - const data = await bookController.getBook(req,res,next); - console.log(data) - return data - }, - books: async (body:GraphQLBody<{}>, context:IGraphQLContext) => { - const {req,res,next} = createRESTArgumentsFromGraphqlRequest(context, body, 'books'); - const data = await bookController.getBooks(req,res,next); - console.log(data) - return data - }, - // chaptersByBook: ({ bookId }: {bookId:number}) => bookController.getChaptersByBook({ params: { id: bookId } }), - // chapters: () => chapterController.getChapters(), - // chapter: ({ id }: {id:number}) => chapterController.getChapter({ params: { id } }), - // movies: () => movieController.getMovies(), - // movie: ({ id }: {id:number}) => movieController.getMovie({ params: { id } }), - // quoteByMovie: ({ movieId }: {movieId:number}) => movieController.getQuoteByMovie({ params: { id: movieId } }), - // characters: () => characterController.getCharacters(), - // character: ({ id }: {id:number}) => characterController.getCharacter({ params: { id } }), - // quoteByCharacter: ({ characterId }: {characterId:number}) => characterController.getQuoteByCharacter({ params: { id: characterId } }), - // quotes: () => quoteController.getQuotes(), - // quote: ({ id }: {id:number}) => quoteController.getQuote({ params: { id } }) -} - -export default resolvers \ No newline at end of file + +export async function getBook(body: GraphQLBody<{ id: string }>, context: IGraphQLContext) { + const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, body, 'book'); + const data = await bookController.getBook(req, res, next); + return data +} + +export async function getBooks(body: GraphQLBody<{}>, context: IGraphQLContext) { + const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, body, 'books'); + const data = await bookController.getBooks(req, res, next) + return data +} + +export async function getChaptersByBook(body: GraphQLBody<{ id: string }>, context: IGraphQLContext) { + const { + req: ChaptersReq, + res: ChaptersRes, + next: ChaptersNext + } = createRESTArgumentsFromGraphqlRequest(context, body, 'chapters') + const chapters = await bookController.getChaptersByBook(ChaptersReq, ChaptersRes, ChaptersNext) as GraphQLResponse<{ + chapters: Chapter[] + }> | void + if(!chapters) { + return chapters + } else { + const response = await addBooksToChapters(chapters.chapters, context) + return { + ...chapters, + chapters: response + } + } +} + +export async function getChapters(body: GraphQLBody<{ id: string } | {}>, context: IGraphQLContext) { + const { + req: ChaptersReq, + res: ChaptersRes, + next: ChaptersNext + } = createRESTArgumentsFromGraphqlRequest(context, body, 'chapters') + const chapters = await bookController.getChaptersByBook(ChaptersReq, ChaptersRes, ChaptersNext) as GraphQLResponse<{ + chapters: Chapter[] + }> | void + if(!chapters) { + return chapters + } else { + const response = await addBooksToChapters(chapters.chapters, context) + return { + ...chapters, + chapters: response + } + } +} + +export async function getChapter(body: GraphQLBody<{ id: string }>, context: IGraphQLContext) { + const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, body, 'chapter'); + const chapter = await chapterController.getChapter(req, res, next) as GraphQLResponse<{ + chapter: Chapter + }> | void + if(!chapter) { + return chapter + } else { + const response = await addBookToChapter(chapter.chapter, context) + return { + ...response, + chapter: response + } + } +} + + +async function getMovies(body: GraphQLBody<{}>, context: IGraphQLContext) { + const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, body, 'movies'); + const data = await movieController.getMovies(req, res, next); + return data +} + +async function getQuoteByMovie(body: GraphQLBody<{ movieId: string }>, context: IGraphQLContext) { + +} + + +// movie: ({ id }: {id:number}) => movieController.getMovie({ params: { id } }), +// quoteByMovie: ({ movieId }: {movieId:number}) => movieController.getQuoteByMovie({ params: { id: movieId } }), +// characters: () => characterController.getCharacters(), +// character: ({ id }: {id:number}) => characterController.getCharacter({ params: { id } }), +// quoteByCharacter: ({ characterId }: {characterId:number}) => characterController.getQuoteByCharacter({ params: { id: characterId } }), +// quotes: () => quoteController.getQuotes(), +// quote: ({ id }: {id:number}) => quoteController.getQuote({ params: { id } }) + +async function addBooksToChapters(chapters: Chapter[], context: IGraphQLContext) { + const chapterPromises: Promise[] = chapters.map(async (chapter) => { + if (chapter.book) { + const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, {id: chapter.book._id}, 'book'); + const book = await bookController.getBook(req, res, next) as Book | void; + if (book) { + return {...chapter, book}; + } + } + return chapter; + }); + return await Promise.all(chapterPromises).then((chapters) => chapters); +} + +async function addBookToChapter(chapter: Chapter, context: IGraphQLContext) { + if (chapter.book) { + const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, {id: chapter.book._id}, 'book'); + const book = await bookController.getBook(req, res, next) as Book | void; + if (book) { + return {...chapter, book}; + } + } + return chapter; +} + +async function addMoviesToQuotes(quotes: Quote[], context: IGraphQLContext) { + const quotePromises: Promise[] = quotes.map(async (quote) => { + if (quote.movie) { + const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, {id: quote.movie._id}, 'movie'); + const movie = await movieController.getMovie(req, res, next) as Movie | void; + if (movie) { + return {...quote, movie}; + } + } + return quote; + }); + return await Promise.all(quotePromises).then((quotes) => quotes); +} + +async function addCharacterToQuote(quote: Quote, context: IGraphQLContext) { + if (quote.character) { + const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, {id: quote.character._id}, 'character'); + const character = await characterController.getCharacter(req, res, next) as Character | void; + if (character) { + return {...quote, character}; + } + } + return quote; +} diff --git a/backend/graphql/root.ts b/backend/graphql/root.ts new file mode 100644 index 0000000..e2a0685 --- /dev/null +++ b/backend/graphql/root.ts @@ -0,0 +1,18 @@ +import {} from './resolvers' +const root = { + book: + books: + chaptersByBook:, + chapters: + // chapter: ({ id }: {id:number}) => chapterController.getChapter({ params: { id } }), + // movies: () => movieController.getMovies(), + // movie: ({ id }: {id:number}) => movieController.getMovie({ params: { id } }), + // quoteByMovie: ({ movieId }: {movieId:number}) => movieController.getQuoteByMovie({ params: { id: movieId } }), + // characters: () => characterController.getCharacters(), + // character: ({ id }: {id:number}) => characterController.getCharacter({ params: { id } }), + // quoteByCharacter: ({ characterId }: {characterId:number}) => characterController.getQuoteByCharacter({ params: { id: characterId } }), + // quotes: () => quoteController.getQuotes(), + // quote: ({ id }: {id:number}) => quoteController.getQuote({ params: { id } }) +} + +export default root \ No newline at end of file diff --git a/backend/graphql/schema.ts b/backend/graphql/schema.ts index b508889..403505a 100644 --- a/backend/graphql/schema.ts +++ b/backend/graphql/schema.ts @@ -31,7 +31,7 @@ const schema = buildSchema(` pages: Int } type ChapterResponse { - chapter: Chapter + chapter: Chapter total: Int limit: Int page: Int @@ -66,7 +66,7 @@ const schema = buildSchema(` pages: Int } type CharactersResponse { - characters: [Character] + characters: [Character] total: Int limit: Int page: Int @@ -95,25 +95,42 @@ const schema = buildSchema(` } type Chapter { - id: ID + _id: ID title: String content: String + Book: Book } type Movie { - id: ID - title: String - director: String + _id: ID + name: String + runtimeInMinutes: Int + budgetInMillions: Int + boxOfficeRevenueInMillions: Int + academyAwardNominations: Int + academyAwardWins: Int + rottenTomatoesScore: Int } type Character { - id: ID + _id: ID + height: String + race: String + gender: String + birth: String + spouse: String + death: String + realm: String + hair: String name: String + wikiUrl: String } - + type Quote { - id: ID - text: String + _id: ID + dialog: String + movie: Movie + Character: Character } type Pagination { total: Int @@ -147,12 +164,60 @@ export type GraphQLBody = T & { page?: string offset?: string } +export type GraphQLResponse = T & { + total: number + limit: number + page: number + pages: number +} +export type Book = { + _id: string + name: string +} +export type Chapter = { + _id: string + title: string + content: string + book: Book | undefined +} +export type Movie = { + _id: string + name: string + runtimeInMinutes: number + budgetInMillions: number + boxOfficeRevenueInMillions: number + academyAwardNominations: number + academyAwardWins: number + rottenTomatoesScore: number +} +export type Character = { + _id: string + height: string + race: string + gender: string + birth: string + spouse: string + death: string + realm: string + hair: string + name: string + wikiUrl: string +} + +export type Quote = { + _id: string + dialog: string + movie: Movie + Character: Character +} + + + export type DataNames = 'books' | 'book' - | 'chaptersByBook' | 'chapters' | 'chapter' | 'movies' diff --git a/backend/helpers/config.ts b/backend/helpers/config.ts index 48507b7..1893296 100644 --- a/backend/helpers/config.ts +++ b/backend/helpers/config.ts @@ -60,7 +60,14 @@ export const createRESTArgumentsFromGraphqlRequest = (context: IGraphQLContext, } const res = { ...context.requestInfo.context.res, - json: (data: any) => { + json: (data: any):{ + [dataName: string]: any, + pages: number, + page: number, + offset: number, + limit: number, + total: number + } => { const targetData = isPlural(dataName) ? data.docs : data.docs[0].toObject(); const {pages, page, offset, limit, total} = data const returnData = { diff --git a/backend/server.ts b/backend/server.ts index 770e561..dfd05e8 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -19,7 +19,7 @@ import {User} from './helpers/interfaces'; import {createHandler} from 'graphql-http/lib/use/express'; import schema from './graphql/schema'; import root from './graphql/resolvers'; - +import jwt from 'jsonwebtoken'; const app = express(); const dpwcToken = process.env.DPWC_TOKEN || ''; app.use(helmet()); From 8f7564ee1165146866ceaee37f8ca08f12b8ae81 Mon Sep 17 00:00:00 2001 From: danila Date: Fri, 18 Oct 2024 16:26:44 +0200 Subject: [PATCH 05/11] debugging & testing --- backend/graphql/resolvers.ts | 227 ++++++++++++++++++++++++++--------- backend/graphql/root.ts | 45 ++++--- backend/graphql/schema.ts | 36 +++--- backend/helpers/config.ts | 32 ++--- backend/server.ts | 2 +- 5 files changed, 240 insertions(+), 102 deletions(-) diff --git a/backend/graphql/resolvers.ts b/backend/graphql/resolvers.ts index 7533232..f92bf0e 100644 --- a/backend/graphql/resolvers.ts +++ b/backend/graphql/resolvers.ts @@ -12,7 +12,7 @@ import { Character, Movie, Quote, - DataNames + DataNames, ChapterWithBookId } from "./schema"; import {createRESTArgumentsFromGraphqlRequest} from '../helpers/config' @@ -34,13 +34,19 @@ export async function getChaptersByBook(body: GraphQLBody<{ id: string }>, conte res: ChaptersRes, next: ChaptersNext } = createRESTArgumentsFromGraphqlRequest(context, body, 'chapters') - const chapters = await bookController.getChaptersByBook(ChaptersReq, ChaptersRes, ChaptersNext) as GraphQLResponse<{ + let chapters = await bookController.getChaptersByBook(ChaptersReq, ChaptersRes, ChaptersNext) as GraphQLResponse<{ chapters: Chapter[] }> | void - if(!chapters) { + if (!chapters) { return chapters } else { - const response = await addBooksToChapters(chapters.chapters, context) + const chaptersWithBookId = chapters.chapters.map((chapter) => { + return { + ...chapter, + book: body.id + } + }) + const response = await addBooksToChapters(chaptersWithBookId, context) return { ...chapters, chapters: response @@ -48,16 +54,16 @@ export async function getChaptersByBook(body: GraphQLBody<{ id: string }>, conte } } -export async function getChapters(body: GraphQLBody<{ id: string } | {}>, context: IGraphQLContext) { +export async function getChapters(body: GraphQLBody<{}>, context: IGraphQLContext) { const { req: ChaptersReq, res: ChaptersRes, next: ChaptersNext } = createRESTArgumentsFromGraphqlRequest(context, body, 'chapters') - const chapters = await bookController.getChaptersByBook(ChaptersReq, ChaptersRes, ChaptersNext) as GraphQLResponse<{ + const chapters = await chapterController.getChapters(ChaptersReq, ChaptersRes, ChaptersNext) as GraphQLResponse<{ chapters: Chapter[] }> | void - if(!chapters) { + if (!chapters) { return chapters } else { const response = await addBooksToChapters(chapters.chapters, context) @@ -73,83 +79,196 @@ export async function getChapter(body: GraphQLBody<{ id: string }>, context: IGr const chapter = await chapterController.getChapter(req, res, next) as GraphQLResponse<{ chapter: Chapter }> | void - if(!chapter) { + if (!chapter) { return chapter } else { - const response = await addBookToChapter(chapter.chapter, context) + const response = await addBooksToChapters(chapter.chapter, context) return { - ...response, + ...chapter, chapter: response } } } +export async function getMovie(body: GraphQLBody<{ id: string }>, context: IGraphQLContext) { + const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, body, 'movie'); + const data = await movieController.getMovie(req, res, next); + return data +} -async function getMovies(body: GraphQLBody<{}>, context: IGraphQLContext) { +export async function getMovies(body: GraphQLBody<{}>, context: IGraphQLContext) { const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, body, 'movies'); const data = await movieController.getMovies(req, res, next); return data } -async function getQuoteByMovie(body: GraphQLBody<{ movieId: string }>, context: IGraphQLContext) { - +export async function getQuoteByMovie(body: GraphQLBody<{ movieId: string }>, context: IGraphQLContext) { + const { + req: QuotesReq, + res: QuotesRes, + next: QuotesNext + } = createRESTArgumentsFromGraphqlRequest(context, body, 'quotes') + const quotes = await movieController.getQuoteByMovie(QuotesReq, QuotesRes, QuotesNext) as GraphQLResponse<{ + quotes: Quote[] + }> | void + if (!quotes) { + return quotes + } else { + let response = await addMoviesToQuotes(quotes.quotes, context) + response = await addCharacterToQuotes(response, context) + return { + ...quotes, + quotes: response + } + } } +export async function getCharacter(body: GraphQLBody<{ id: string }>, context: IGraphQLContext) { + const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, body, 'character'); + const data = await characterController.getCharacter(req, res, next); + return data +} -// movie: ({ id }: {id:number}) => movieController.getMovie({ params: { id } }), -// quoteByMovie: ({ movieId }: {movieId:number}) => movieController.getQuoteByMovie({ params: { id: movieId } }), -// characters: () => characterController.getCharacters(), -// character: ({ id }: {id:number}) => characterController.getCharacter({ params: { id } }), -// quoteByCharacter: ({ characterId }: {characterId:number}) => characterController.getQuoteByCharacter({ params: { id: characterId } }), -// quotes: () => quoteController.getQuotes(), -// quote: ({ id }: {id:number}) => quoteController.getQuote({ params: { id } }) +export async function getCharacters(body: GraphQLBody<{}>, context: IGraphQLContext) { + const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, body, 'characters'); + const data = await characterController.getCharacters(req, res, next); + return data +} -async function addBooksToChapters(chapters: Chapter[], context: IGraphQLContext) { - const chapterPromises: Promise[] = chapters.map(async (chapter) => { - if (chapter.book) { - const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, {id: chapter.book._id}, 'book'); - const book = await bookController.getBook(req, res, next) as Book | void; - if (book) { - return {...chapter, book}; - } +export async function getQuoteByCharacter(body: GraphQLBody<{ characterId: string }>, context: IGraphQLContext) { + const { + req: QuotesReq, + res: QuotesRes, + next: QuotesNext + } = createRESTArgumentsFromGraphqlRequest(context, body, 'quotes') + const quotes = await characterController.getQuoteByCharacter(QuotesReq, QuotesRes, QuotesNext) as GraphQLResponse<{ + quotes: Quote[] + }> | void + if (!quotes) { + return quotes + } else { + let response = await addMoviesToQuotes(quotes.quotes, context) + response = await addCharacterToQuotes(response, context) + return { + ...quotes, + quotes: response } - return chapter; - }); - return await Promise.all(chapterPromises).then((chapters) => chapters); + } } -async function addBookToChapter(chapter: Chapter, context: IGraphQLContext) { - if (chapter.book) { - const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, {id: chapter.book._id}, 'book'); - const book = await bookController.getBook(req, res, next) as Book | void; - if (book) { - return {...chapter, book}; +export async function getQuote(body: GraphQLBody<{ id: string }>, context: IGraphQLContext) { + const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, body, 'quote'); + let quote = await quoteController.getQuote(req, res, next) as GraphQLResponse<{ + quote: Quote + }> | void + if (!quote) { + return quote + } + let response = await addMoviesToQuotes(quote.quote, context) + response = await addCharacterToQuotes(response, context) + return { + ...quote, + quote: response + } +} + +export async function getQuotes(body: GraphQLBody<{}>, context: IGraphQLContext) { + const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, body, 'quotes'); + let quotes = await quoteController.getQuotes(req, res, next) as GraphQLResponse<{ + quotes: Quote[] + }> | void + if (!quotes) { + return quotes + } + let response = await addMoviesToQuotes(quotes.quotes, context) + response = await addCharacterToQuotes(response, context) + return { + ...quotes, + quotes: response + } +} + + +async function addBooksToChapters(chapters: ChapterWithBookId[] | ChapterWithBookId, context: IGraphQLContext) { + if (!Array.isArray(chapters)) { + if (chapters.book) { + const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, {id: chapters.book}, 'book', false); + const book = await bookController.getBook(req, res, next) as {book:Book} | void; + if (book) { + return {...chapters, book:book.book}; + } } + return chapters + } else { + const chapterPromises: Promise[] = chapters.map(async (chapter) => { + if (chapter.book) { + const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, {id: chapter.book}, 'book', false); + const book = await bookController.getBook(req, res, next) as {book:Book} | void; + if (book) { + return {...chapter, book:book.book}; + } + } + return chapter; + }); + return await Promise.all(chapterPromises).then((chapters) => chapters); } - return chapter; } -async function addMoviesToQuotes(quotes: Quote[], context: IGraphQLContext) { - const quotePromises: Promise[] = quotes.map(async (quote) => { - if (quote.movie) { - const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, {id: quote.movie._id}, 'movie'); - const movie = await movieController.getMovie(req, res, next) as Movie | void; + +async function addMoviesToQuotes(quotes: Quote[] | Quote, context: IGraphQLContext) { + if (!Array.isArray(quotes)) { + if (quotes.movie) { + const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, {id: quotes.movie._id}, 'movie', false); + const movie = await movieController.getMovie(req, res, next) as {movie:Movie} | void; if (movie) { - return {...quote, movie}; + return {...quotes, movie:movie.movie} } } - return quote; - }); - return await Promise.all(quotePromises).then((quotes) => quotes); + return quotes + } else { + const quotePromises: Promise[] = quotes.map(async (quote) => { + if (quote.movie) { + const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, {id: quote.movie._id}, 'movie', false); + const movie = await movieController.getMovie(req, res, next) as {movie:Movie} | void; + if (movie) { + return {...quote, movie:movie.movie}; + } + } + return quote; + }); + return await Promise.all(quotePromises).then((quotes) => quotes); + } } -async function addCharacterToQuote(quote: Quote, context: IGraphQLContext) { - if (quote.character) { - const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, {id: quote.character._id}, 'character'); - const character = await characterController.getCharacter(req, res, next) as Character | void; - if (character) { - return {...quote, character}; +async function addCharacterToQuotes(quote: Quote[] | Quote, context: IGraphQLContext) { + if (!Array.isArray(quote)) { + if (quote.character) { + const { + req, + res, + next + } = createRESTArgumentsFromGraphqlRequest(context, {id: quote.character._id}, 'character', false); + const character = await characterController.getCharacter(req, res, next) as {character: Character } | void; + if (character) { + return {...quote, character:character.character}; + } } + return quote; + } else { + const quotePromises: Promise[] = quote.map(async (quote) => { + if (quote.character) { + const { + req, + res, + next + } = createRESTArgumentsFromGraphqlRequest(context, {id: quote.character._id}, 'character', false); + const character = await characterController.getCharacter(req, res, next) as {character: Character } | void; + if (character) { + return {...quote, character:character.character}; + } + } + return quote; + }); + return await Promise.all(quotePromises).then((quotes) => quotes); } - return quote; } diff --git a/backend/graphql/root.ts b/backend/graphql/root.ts index e2a0685..d367bfc 100644 --- a/backend/graphql/root.ts +++ b/backend/graphql/root.ts @@ -1,18 +1,33 @@ -import {} from './resolvers' +import { + getChapter, + getChapters, + getChaptersByBook, + getBooks, + getBook, + getCharacters, + getCharacter, + getQuoteByCharacter, + getQuote, + getQuotes, + getQuoteByMovie, + getMovies, + getMovie +} from './resolvers' + const root = { - book: - books: - chaptersByBook:, - chapters: - // chapter: ({ id }: {id:number}) => chapterController.getChapter({ params: { id } }), - // movies: () => movieController.getMovies(), - // movie: ({ id }: {id:number}) => movieController.getMovie({ params: { id } }), - // quoteByMovie: ({ movieId }: {movieId:number}) => movieController.getQuoteByMovie({ params: { id: movieId } }), - // characters: () => characterController.getCharacters(), - // character: ({ id }: {id:number}) => characterController.getCharacter({ params: { id } }), - // quoteByCharacter: ({ characterId }: {characterId:number}) => characterController.getQuoteByCharacter({ params: { id: characterId } }), - // quotes: () => quoteController.getQuotes(), - // quote: ({ id }: {id:number}) => quoteController.getQuote({ params: { id } }) + book: getBook, + books: getBooks, + chaptersByBook: getChaptersByBook, + chapters: getChapters, + chapter: getChapter, + movies: getMovies, + movie: getMovie, + quoteByMovie: getQuoteByMovie, + characters: getCharacters, + character: getCharacter, + quoteByCharacter: getQuoteByCharacter, + quotes: getQuotes, + quote: getQuote } -export default root \ No newline at end of file +export default root \ No newline at end of file diff --git a/backend/graphql/schema.ts b/backend/graphql/schema.ts index 403505a..d251770 100644 --- a/backend/graphql/schema.ts +++ b/backend/graphql/schema.ts @@ -4,15 +4,15 @@ const schema = buildSchema(` type Query { books: BooksResponse book(id: ID!, pagination:PaginationInput): BookResponse - chaptersByBook(bookId: ID!): ChaptersResponse + chaptersByBook(id: ID!): ChaptersResponse chapters: ChaptersResponse - chapter(id: ID!): ChaptersResponse + chapter(id: ID!): ChapterResponse movies: MoviesResponse - movie(id: ID!): MoviesResponse - quoteByMovie(movieId: ID!): QuotesResponse + movie(id: ID!): MovieResponse + quoteByMovie(id: ID!): QuotesResponse characters: CharactersResponse - character(id: ID!): CharactersResponse - quoteByCharacter(characterId: ID!): QuotesResponse + character(id: ID!): CharacterResponse + quoteByCharacter(id: ID!): QuotesResponse quotes: QuotesResponse quote(id: ID!): QuotesResponse } @@ -96,20 +96,19 @@ const schema = buildSchema(` type Chapter { _id: ID - title: String - content: String - Book: Book + chapterName: String + book: Book } type Movie { _id: ID name: String - runtimeInMinutes: Int - budgetInMillions: Int - boxOfficeRevenueInMillions: Int + runtimeInMinutes: Float + budgetInMillions: Float + boxOfficeRevenueInMillions: Float academyAwardNominations: Int academyAwardWins: Int - rottenTomatoesScore: Int + rottenTomatoesScore: Float } type Character { @@ -176,9 +175,10 @@ export type Book = { } export type Chapter = { _id: string - title: string - content: string - book: Book | undefined + chapterName: string +} +export type ChapterWithBookId = Chapter & { + book:string } export type Movie = { _id: string @@ -207,8 +207,8 @@ export type Character = { export type Quote = { _id: string dialog: string - movie: Movie - Character: Character + movie?: Movie + character?: Character } diff --git a/backend/helpers/config.ts b/backend/helpers/config.ts index 1893296..cddd7e5 100644 --- a/backend/helpers/config.ts +++ b/backend/helpers/config.ts @@ -50,7 +50,7 @@ export const getOptions = async (req: Request): Promise => { return options; }; -export const createRESTArgumentsFromGraphqlRequest = (context: IGraphQLContext, bodyPayload: any, dataName: DataNames) => { +export const createRESTArgumentsFromGraphqlRequest = (context: IGraphQLContext, bodyPayload: any, dataName: DataNames, addPaginationData:boolean = true) => { const req = { ...context.requestInfo.req, params: bodyPayload @@ -62,22 +62,26 @@ export const createRESTArgumentsFromGraphqlRequest = (context: IGraphQLContext, ...context.requestInfo.context.res, json: (data: any):{ [dataName: string]: any, - pages: number, - page: number, - offset: number, - limit: number, - total: number + pages?: number, + page?: number, + offset?: number, + limit?: number, + total?: number } => { - const targetData = isPlural(dataName) ? data.docs : data.docs[0].toObject(); - const {pages, page, offset, limit, total} = data - const returnData = { + const targetData = isPlural(dataName) ? data.docs.map((el:any)=>el.toObject()) : data.docs[0] ? data.docs[0].toObject() : [] + let returnData = { [dataName]: targetData, - pages, - page, - offset, - limit, - total }; + if(addPaginationData) { + returnData = { + ...returnData, + pages: data.pages, + page: data.page, + offset: data.offset, + limit: data.limit, + total: data.total + } + } return returnData } } as any; diff --git a/backend/server.ts b/backend/server.ts index dfd05e8..1079ab3 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -18,7 +18,7 @@ import authRoutes from './routes/auth'; import {User} from './helpers/interfaces'; import {createHandler} from 'graphql-http/lib/use/express'; import schema from './graphql/schema'; -import root from './graphql/resolvers'; +import root from './graphql/root'; import jwt from 'jsonwebtoken'; const app = express(); const dpwcToken = process.env.DPWC_TOKEN || ''; From 4925528a13d25ceba8b26aa135fce4ef25f5e1ed Mon Sep 17 00:00:00 2001 From: danila Date: Fri, 18 Oct 2024 17:53:24 +0200 Subject: [PATCH 06/11] final tweaks after testing --- backend/graphql/resolvers.ts | 6 +++--- backend/graphql/schema.ts | 7 ++++--- backend/server.ts | 1 - 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/graphql/resolvers.ts b/backend/graphql/resolvers.ts index f92bf0e..f1aed6e 100644 --- a/backend/graphql/resolvers.ts +++ b/backend/graphql/resolvers.ts @@ -34,7 +34,7 @@ export async function getChaptersByBook(body: GraphQLBody<{ id: string }>, conte res: ChaptersRes, next: ChaptersNext } = createRESTArgumentsFromGraphqlRequest(context, body, 'chapters') - let chapters = await bookController.getChaptersByBook(ChaptersReq, ChaptersRes, ChaptersNext) as GraphQLResponse<{ + const chapters = await bookController.getChaptersByBook(ChaptersReq, ChaptersRes, ChaptersNext) as GraphQLResponse<{ chapters: Chapter[] }> | void if (!chapters) { @@ -61,7 +61,7 @@ export async function getChapters(body: GraphQLBody<{}>, context: IGraphQLContex next: ChaptersNext } = createRESTArgumentsFromGraphqlRequest(context, body, 'chapters') const chapters = await chapterController.getChapters(ChaptersReq, ChaptersRes, ChaptersNext) as GraphQLResponse<{ - chapters: Chapter[] + chapters: ChapterWithBookId[] }> | void if (!chapters) { return chapters @@ -77,7 +77,7 @@ export async function getChapters(body: GraphQLBody<{}>, context: IGraphQLContex export async function getChapter(body: GraphQLBody<{ id: string }>, context: IGraphQLContext) { const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, body, 'chapter'); const chapter = await chapterController.getChapter(req, res, next) as GraphQLResponse<{ - chapter: Chapter + chapter: ChapterWithBookId }> | void if (!chapter) { return chapter diff --git a/backend/graphql/schema.ts b/backend/graphql/schema.ts index d251770..09f2db1 100644 --- a/backend/graphql/schema.ts +++ b/backend/graphql/schema.ts @@ -14,7 +14,7 @@ const schema = buildSchema(` character(id: ID!): CharacterResponse quoteByCharacter(id: ID!): QuotesResponse quotes: QuotesResponse - quote(id: ID!): QuotesResponse + quote(id: ID!): QuoteResponse } type BooksResponse { books: [Book] @@ -49,6 +49,7 @@ const schema = buildSchema(` limit: Int page: Int pages: Int + total: Int } type MoviesResponse { @@ -74,7 +75,7 @@ const schema = buildSchema(` } type QuoteResponse { quote: Quote - total: Int + total: Int limit: Int page: Int pages: Int @@ -129,7 +130,7 @@ const schema = buildSchema(` _id: ID dialog: String movie: Movie - Character: Character + character: Character } type Pagination { total: Int diff --git a/backend/server.ts b/backend/server.ts index 1079ab3..55cd25c 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -19,7 +19,6 @@ import {User} from './helpers/interfaces'; import {createHandler} from 'graphql-http/lib/use/express'; import schema from './graphql/schema'; import root from './graphql/root'; -import jwt from 'jsonwebtoken'; const app = express(); const dpwcToken = process.env.DPWC_TOKEN || ''; app.use(helmet()); From 39ca623f11c06cb7f79b3e1a3dca957aa6e3d718 Mon Sep 17 00:00:00 2001 From: danila Date: Fri, 18 Oct 2024 17:54:44 +0200 Subject: [PATCH 07/11] delete unused files and prettify --- backend/graphql/resolvers.ts | 44 ++++++++++++++++++++++++------------ backend/graphql/schema.ts | 6 +---- backend/graphql/types.ts | 0 backend/server.ts | 3 ++- 4 files changed, 32 insertions(+), 21 deletions(-) delete mode 100644 backend/graphql/types.ts diff --git a/backend/graphql/resolvers.ts b/backend/graphql/resolvers.ts index f1aed6e..e6e116f 100644 --- a/backend/graphql/resolvers.ts +++ b/backend/graphql/resolvers.ts @@ -193,19 +193,23 @@ async function addBooksToChapters(chapters: ChapterWithBookId[] | ChapterWithBoo if (!Array.isArray(chapters)) { if (chapters.book) { const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, {id: chapters.book}, 'book', false); - const book = await bookController.getBook(req, res, next) as {book:Book} | void; + const book = await bookController.getBook(req, res, next) as { book: Book } | void; if (book) { - return {...chapters, book:book.book}; + return {...chapters, book: book.book}; } } return chapters } else { const chapterPromises: Promise[] = chapters.map(async (chapter) => { if (chapter.book) { - const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, {id: chapter.book}, 'book', false); - const book = await bookController.getBook(req, res, next) as {book:Book} | void; + const { + req, + res, + next + } = createRESTArgumentsFromGraphqlRequest(context, {id: chapter.book}, 'book', false); + const book = await bookController.getBook(req, res, next) as { book: Book } | void; if (book) { - return {...chapter, book:book.book}; + return {...chapter, book: book.book}; } } return chapter; @@ -218,20 +222,28 @@ async function addBooksToChapters(chapters: ChapterWithBookId[] | ChapterWithBoo async function addMoviesToQuotes(quotes: Quote[] | Quote, context: IGraphQLContext) { if (!Array.isArray(quotes)) { if (quotes.movie) { - const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, {id: quotes.movie._id}, 'movie', false); - const movie = await movieController.getMovie(req, res, next) as {movie:Movie} | void; + const { + req, + res, + next + } = createRESTArgumentsFromGraphqlRequest(context, {id: quotes.movie._id}, 'movie', false); + const movie = await movieController.getMovie(req, res, next) as { movie: Movie } | void; if (movie) { - return {...quotes, movie:movie.movie} + return {...quotes, movie: movie.movie} } } return quotes } else { const quotePromises: Promise[] = quotes.map(async (quote) => { if (quote.movie) { - const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, {id: quote.movie._id}, 'movie', false); - const movie = await movieController.getMovie(req, res, next) as {movie:Movie} | void; + const { + req, + res, + next + } = createRESTArgumentsFromGraphqlRequest(context, {id: quote.movie._id}, 'movie', false); + const movie = await movieController.getMovie(req, res, next) as { movie: Movie } | void; if (movie) { - return {...quote, movie:movie.movie}; + return {...quote, movie: movie.movie}; } } return quote; @@ -248,9 +260,9 @@ async function addCharacterToQuotes(quote: Quote[] | Quote, context: IGraphQLCon res, next } = createRESTArgumentsFromGraphqlRequest(context, {id: quote.character._id}, 'character', false); - const character = await characterController.getCharacter(req, res, next) as {character: Character } | void; + const character = await characterController.getCharacter(req, res, next) as { character: Character } | void; if (character) { - return {...quote, character:character.character}; + return {...quote, character: character.character}; } } return quote; @@ -262,9 +274,11 @@ async function addCharacterToQuotes(quote: Quote[] | Quote, context: IGraphQLCon res, next } = createRESTArgumentsFromGraphqlRequest(context, {id: quote.character._id}, 'character', false); - const character = await characterController.getCharacter(req, res, next) as {character: Character } | void; + const character = await characterController.getCharacter(req, res, next) as { + character: Character + } | void; if (character) { - return {...quote, character:character.character}; + return {...quote, character: character.character}; } } return quote; diff --git a/backend/graphql/schema.ts b/backend/graphql/schema.ts index 09f2db1..d650cd7 100644 --- a/backend/graphql/schema.ts +++ b/backend/graphql/schema.ts @@ -179,7 +179,7 @@ export type Chapter = { chapterName: string } export type ChapterWithBookId = Chapter & { - book:string + book: string } export type Movie = { _id: string @@ -212,10 +212,6 @@ export type Quote = { character?: Character } - - - - export type DataNames = 'books' | 'book' diff --git a/backend/graphql/types.ts b/backend/graphql/types.ts deleted file mode 100644 index e69de29..0000000 diff --git a/backend/server.ts b/backend/server.ts index 55cd25c..a9a080e 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -19,6 +19,7 @@ import {User} from './helpers/interfaces'; import {createHandler} from 'graphql-http/lib/use/express'; import schema from './graphql/schema'; import root from './graphql/root'; + const app = express(); const dpwcToken = process.env.DPWC_TOKEN || ''; app.use(helmet()); @@ -101,7 +102,7 @@ app.use((req, res, next) => { app.all("/graphql", createHandler({ schema: schema, rootValue: root, - context: (req) => ({ requestInfo:req }) + context: (req) => ({requestInfo: req}) })) app.use('/v2/', apiLimiter); app.use('/v2', apiRoutes); From 3ee7350126bc81b5dd3cb25e328f4dc7cb6b0905 Mon Sep 17 00:00:00 2001 From: danila Date: Mon, 21 Oct 2024 22:53:26 +0200 Subject: [PATCH 08/11] add pagination --- backend/graphql/resolvers.ts | 27 ++++++++++++++------------- backend/graphql/schema.ts | 31 +++++++++++++++++++------------ backend/helpers/config.ts | 8 +++++++- 3 files changed, 40 insertions(+), 26 deletions(-) diff --git a/backend/graphql/resolvers.ts b/backend/graphql/resolvers.ts index e6e116f..29e46d4 100644 --- a/backend/graphql/resolvers.ts +++ b/backend/graphql/resolvers.ts @@ -12,23 +12,24 @@ import { Character, Movie, Quote, + Pagination, DataNames, ChapterWithBookId } from "./schema"; import {createRESTArgumentsFromGraphqlRequest} from '../helpers/config' -export async function getBook(body: GraphQLBody<{ id: string }>, context: IGraphQLContext) { +export async function getBook(body: GraphQLBody<{ id: string, pagination?:Pagination }>, context: IGraphQLContext) { const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, body, 'book'); const data = await bookController.getBook(req, res, next); return data } -export async function getBooks(body: GraphQLBody<{}>, context: IGraphQLContext) { +export async function getBooks(body: GraphQLBody<{ pagination?:Pagination }>, context: IGraphQLContext) { const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, body, 'books'); const data = await bookController.getBooks(req, res, next) return data } -export async function getChaptersByBook(body: GraphQLBody<{ id: string }>, context: IGraphQLContext) { +export async function getChaptersByBook(body: GraphQLBody<{ id: string, pagination?:Pagination }>, context: IGraphQLContext) { const { req: ChaptersReq, res: ChaptersRes, @@ -54,7 +55,7 @@ export async function getChaptersByBook(body: GraphQLBody<{ id: string }>, conte } } -export async function getChapters(body: GraphQLBody<{}>, context: IGraphQLContext) { +export async function getChapters(body: GraphQLBody<{ pagination?:Pagination }>, context: IGraphQLContext) { const { req: ChaptersReq, res: ChaptersRes, @@ -74,7 +75,7 @@ export async function getChapters(body: GraphQLBody<{}>, context: IGraphQLContex } } -export async function getChapter(body: GraphQLBody<{ id: string }>, context: IGraphQLContext) { +export async function getChapter(body: GraphQLBody<{ id: string, pagination?:Pagination } & Pagination>, context: IGraphQLContext) { const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, body, 'chapter'); const chapter = await chapterController.getChapter(req, res, next) as GraphQLResponse<{ chapter: ChapterWithBookId @@ -90,19 +91,19 @@ export async function getChapter(body: GraphQLBody<{ id: string }>, context: IGr } } -export async function getMovie(body: GraphQLBody<{ id: string }>, context: IGraphQLContext) { +export async function getMovie(body: GraphQLBody<{ id: string, pagination?:Pagination }>, context: IGraphQLContext) { const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, body, 'movie'); const data = await movieController.getMovie(req, res, next); return data } -export async function getMovies(body: GraphQLBody<{}>, context: IGraphQLContext) { +export async function getMovies(body: GraphQLBody<{ pagination?:Pagination }>, context: IGraphQLContext) { const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, body, 'movies'); const data = await movieController.getMovies(req, res, next); return data } -export async function getQuoteByMovie(body: GraphQLBody<{ movieId: string }>, context: IGraphQLContext) { +export async function getQuoteByMovie(body: GraphQLBody<{ movieId: string, pagination?:Pagination }>, context: IGraphQLContext) { const { req: QuotesReq, res: QuotesRes, @@ -123,19 +124,19 @@ export async function getQuoteByMovie(body: GraphQLBody<{ movieId: string }>, co } } -export async function getCharacter(body: GraphQLBody<{ id: string }>, context: IGraphQLContext) { +export async function getCharacter(body: GraphQLBody<{ id: string, pagination?:Pagination }>, context: IGraphQLContext) { const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, body, 'character'); const data = await characterController.getCharacter(req, res, next); return data } -export async function getCharacters(body: GraphQLBody<{}>, context: IGraphQLContext) { +export async function getCharacters(body: GraphQLBody<{ pagination?:Pagination }>, context: IGraphQLContext) { const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, body, 'characters'); const data = await characterController.getCharacters(req, res, next); return data } -export async function getQuoteByCharacter(body: GraphQLBody<{ characterId: string }>, context: IGraphQLContext) { +export async function getQuoteByCharacter(body: GraphQLBody<{ characterId: string, pagination?:Pagination }>, context: IGraphQLContext) { const { req: QuotesReq, res: QuotesRes, @@ -156,7 +157,7 @@ export async function getQuoteByCharacter(body: GraphQLBody<{ characterId: strin } } -export async function getQuote(body: GraphQLBody<{ id: string }>, context: IGraphQLContext) { +export async function getQuote(body: GraphQLBody<{ id: string, pagination?:Pagination }>, context: IGraphQLContext) { const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, body, 'quote'); let quote = await quoteController.getQuote(req, res, next) as GraphQLResponse<{ quote: Quote @@ -172,7 +173,7 @@ export async function getQuote(body: GraphQLBody<{ id: string }>, context: IGrap } } -export async function getQuotes(body: GraphQLBody<{}>, context: IGraphQLContext) { +export async function getQuotes(body: GraphQLBody<{ pagination?:Pagination }>, context: IGraphQLContext) { const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, body, 'quotes'); let quotes = await quoteController.getQuotes(req, res, next) as GraphQLResponse<{ quotes: Quote[] diff --git a/backend/graphql/schema.ts b/backend/graphql/schema.ts index d650cd7..76f7d0a 100644 --- a/backend/graphql/schema.ts +++ b/backend/graphql/schema.ts @@ -2,19 +2,19 @@ import {buildSchema} from "graphql/utilities"; const schema = buildSchema(` type Query { - books: BooksResponse + books(pagination:PaginationInput): BooksResponse book(id: ID!, pagination:PaginationInput): BookResponse - chaptersByBook(id: ID!): ChaptersResponse - chapters: ChaptersResponse - chapter(id: ID!): ChapterResponse - movies: MoviesResponse - movie(id: ID!): MovieResponse - quoteByMovie(id: ID!): QuotesResponse - characters: CharactersResponse - character(id: ID!): CharacterResponse - quoteByCharacter(id: ID!): QuotesResponse - quotes: QuotesResponse - quote(id: ID!): QuoteResponse + chaptersByBook(id: ID!, pagination:PaginationInput): ChaptersResponse + chapters(pagination:PaginationInput): ChaptersResponse + chapter(id: ID!, pagination:PaginationInput): ChapterResponse + movies(pagination:PaginationInput): MoviesResponse + movie(id: ID!, pagination:PaginationInput): MovieResponse + quoteByMovie(id: ID!, pagination:PaginationInput): QuotesResponse + characters(pagination:PaginationInput): CharactersResponse + character(id: ID!, pagination:PaginationInput): CharacterResponse + quoteByCharacter(id: ID!, pagination:PaginationInput): QuotesResponse + quotes(pagination:PaginationInput): QuotesResponse + quote(id: ID!, pagination:PaginationInput): QuoteResponse } type BooksResponse { books: [Book] @@ -212,6 +212,13 @@ export type Quote = { character?: Character } +export type Pagination = { + total?: number + limit?: number + page?: number + pages?: number +} + export type DataNames = 'books' | 'book' diff --git a/backend/helpers/config.ts b/backend/helpers/config.ts index cddd7e5..ffa59dd 100644 --- a/backend/helpers/config.ts +++ b/backend/helpers/config.ts @@ -51,9 +51,15 @@ export const getOptions = async (req: Request): Promise => { }; export const createRESTArgumentsFromGraphqlRequest = (context: IGraphQLContext, bodyPayload: any, dataName: DataNames, addPaginationData:boolean = true) => { + const { pagination, ...body} = bodyPayload const req = { ...context.requestInfo.req, - params: bodyPayload + query: { + ...pagination + }, + params: { + ...body + } } as Request<{params:typeof bodyPayload}, any, any, ParsedQs, Record>; function isPlural(dataName: DataNames): boolean { return dataName.endsWith('s'); From 02d77077c2b6ac88771393c18a9bf37b36e69a45 Mon Sep 17 00:00:00 2001 From: danila Date: Mon, 21 Oct 2024 23:27:02 +0200 Subject: [PATCH 09/11] add authorization --- backend/graphql/resolvers.ts | 2 ++ backend/helpers/passport.ts | 12 ++++++++++-- backend/server.ts | 4 +++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/backend/graphql/resolvers.ts b/backend/graphql/resolvers.ts index 29e46d4..d0b16db 100644 --- a/backend/graphql/resolvers.ts +++ b/backend/graphql/resolvers.ts @@ -16,6 +16,7 @@ import { DataNames, ChapterWithBookId } from "./schema"; import {createRESTArgumentsFromGraphqlRequest} from '../helpers/config' +import {passportHelpers} from "../helpers/passport"; export async function getBook(body: GraphQLBody<{ id: string, pagination?:Pagination }>, context: IGraphQLContext) { const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, body, 'book'); @@ -25,6 +26,7 @@ export async function getBook(body: GraphQLBody<{ id: string, pagination?:Pagina export async function getBooks(body: GraphQLBody<{ pagination?:Pagination }>, context: IGraphQLContext) { const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, body, 'books'); + await passportHelpers.authenticate(req, res, next) const data = await bookController.getBooks(req, res, next) return data } diff --git a/backend/helpers/passport.ts b/backend/helpers/passport.ts index ce62cfa..4d8bfe0 100644 --- a/backend/helpers/passport.ts +++ b/backend/helpers/passport.ts @@ -6,6 +6,15 @@ import { HttpCode } from './constants'; export const passportHelpers = { authenticate: async (req: Request, res: Response, next: NextFunction) => { passport.authenticate('bearer', { session: false }, async function (err: Error, token: string) { + if (err || !token) { + return new Error('Unauthorized'); + } else { + next(); + } + })(req, res, next); + }, + graphqlAuthenticate: (req: Request, res: Response, next:NextFunction) => { + passport.authenticate('bearer', { session: false }, function (err: Error, token: string) { try { if (err || !token) { return res.status(HttpCode.UNAUTHORIZED).send({ @@ -17,9 +26,8 @@ export const passportHelpers = { } catch (err) { console.log(err); } - })(req, res, next); + })(req, res); }, - login: async (req: Request, res: Response, next: NextFunction) => { passport.authenticate('login', async function (err: Error, user: any, info: any) { try { diff --git a/backend/server.ts b/backend/server.ts index a9a080e..662e7fd 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -1,3 +1,5 @@ +import {passportHelpers} from "./helpers/passport"; + require('dotenv').config(); import mongoose from 'mongoose'; import express from 'express'; @@ -99,7 +101,7 @@ app.use((req, res, next) => { next(); } }); -app.all("/graphql", createHandler({ +app.all("/graphql", passportHelpers.graphqlAuthenticate, createHandler({ schema: schema, rootValue: root, context: (req) => ({requestInfo: req}) From b45d0edec09e6e6c1b0a02804b748d73f786ad52 Mon Sep 17 00:00:00 2001 From: danila Date: Mon, 21 Oct 2024 23:35:18 +0200 Subject: [PATCH 10/11] add whitelist of allowed queries within the graphql authorization middleware --- backend/helpers/passport.ts | 5 +++-- backend/server.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/helpers/passport.ts b/backend/helpers/passport.ts index 4d8bfe0..596ee5a 100644 --- a/backend/helpers/passport.ts +++ b/backend/helpers/passport.ts @@ -13,10 +13,11 @@ export const passportHelpers = { } })(req, res, next); }, - graphqlAuthenticate: (req: Request, res: Response, next:NextFunction) => { + graphqlAuthentication: (req: Request, res: Response, next:NextFunction) => { + const allowedOperations = ['getBooks', 'book', 'chaptersByBook', 'IntrospectionQuery']; passport.authenticate('bearer', { session: false }, function (err: Error, token: string) { try { - if (err || !token) { + if (err || !token || !allowedOperations.includes(req.body.operationName)) { return res.status(HttpCode.UNAUTHORIZED).send({ success: false, message: 'Unauthorized.' diff --git a/backend/server.ts b/backend/server.ts index 662e7fd..73e7557 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -101,7 +101,7 @@ app.use((req, res, next) => { next(); } }); -app.all("/graphql", passportHelpers.graphqlAuthenticate, createHandler({ +app.all("/graphql", passportHelpers.graphqlAuthentication, createHandler({ schema: schema, rootValue: root, context: (req) => ({requestInfo: req}) From c48fcbf788e32c4e02276086e0c9b09ff7a6971e Mon Sep 17 00:00:00 2001 From: Rike Date: Sun, 2 Nov 2025 17:51:51 +0100 Subject: [PATCH 11/11] fix: format code to repo's usual formatting (no code changes) --- backend/.env.sample | 5 - backend/graphql/resolvers.ts | 511 ++++++++++++++++++----------------- backend/graphql/root.ts | 58 ++-- backend/graphql/schema.ts | 137 +++++----- backend/helpers/config.ts | 179 ++++++------ backend/helpers/passport.ts | 12 +- backend/server.ts | 158 +++++------ 7 files changed, 534 insertions(+), 526 deletions(-) delete mode 100644 backend/.env.sample diff --git a/backend/.env.sample b/backend/.env.sample deleted file mode 100644 index a55aca8..0000000 --- a/backend/.env.sample +++ /dev/null @@ -1,5 +0,0 @@ -DATABASE_URL= -PORT= -DPWC_TOKEN= -AWS_LAMBDA_NEWUSER_URL= -SECRET= \ No newline at end of file diff --git a/backend/graphql/resolvers.ts b/backend/graphql/resolvers.ts index d0b16db..090ce95 100644 --- a/backend/graphql/resolvers.ts +++ b/backend/graphql/resolvers.ts @@ -1,291 +1,292 @@ -import {bookController} from '../controllers/book.api' -import {chapterController} from '../controllers/chapter.api' -import {movieController} from '../controllers/movie.api' -import {characterController} from '../controllers/character.api' -import {quoteController} from '../controllers/quote.api' +import { bookController } from '../controllers/book.api'; +import { chapterController } from '../controllers/chapter.api'; +import { movieController } from '../controllers/movie.api'; +import { characterController } from '../controllers/character.api'; +import { quoteController } from '../controllers/quote.api'; import { - IGraphQLContext, - GraphQLBody, - GraphQLResponse, - Book, - Chapter, - Character, - Movie, - Quote, - Pagination, - DataNames, ChapterWithBookId -} from "./schema"; -import {createRESTArgumentsFromGraphqlRequest} from '../helpers/config' -import {passportHelpers} from "../helpers/passport"; + IGraphQLContext, + GraphQLBody, + GraphQLResponse, + Book, + Chapter, + Character, + Movie, + Quote, + Pagination, + DataNames, + ChapterWithBookId +} from './schema'; +import { createRESTArgumentsFromGraphqlRequest } from '../helpers/config'; +import { passportHelpers } from '../helpers/passport'; -export async function getBook(body: GraphQLBody<{ id: string, pagination?:Pagination }>, context: IGraphQLContext) { - const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, body, 'book'); - const data = await bookController.getBook(req, res, next); - return data +export async function getBook(body: GraphQLBody<{ id: string; pagination?: Pagination }>, context: IGraphQLContext) { + const { req, res, next } = createRESTArgumentsFromGraphqlRequest(context, body, 'book'); + const data = await bookController.getBook(req, res, next); + return data; } -export async function getBooks(body: GraphQLBody<{ pagination?:Pagination }>, context: IGraphQLContext) { - const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, body, 'books'); - await passportHelpers.authenticate(req, res, next) - const data = await bookController.getBooks(req, res, next) - return data +export async function getBooks(body: GraphQLBody<{ pagination?: Pagination }>, context: IGraphQLContext) { + const { req, res, next } = createRESTArgumentsFromGraphqlRequest(context, body, 'books'); + await passportHelpers.authenticate(req, res, next); + const data = await bookController.getBooks(req, res, next); + return data; } -export async function getChaptersByBook(body: GraphQLBody<{ id: string, pagination?:Pagination }>, context: IGraphQLContext) { - const { - req: ChaptersReq, - res: ChaptersRes, - next: ChaptersNext - } = createRESTArgumentsFromGraphqlRequest(context, body, 'chapters') - const chapters = await bookController.getChaptersByBook(ChaptersReq, ChaptersRes, ChaptersNext) as GraphQLResponse<{ - chapters: Chapter[] - }> | void - if (!chapters) { - return chapters - } else { - const chaptersWithBookId = chapters.chapters.map((chapter) => { - return { - ...chapter, - book: body.id - } - }) - const response = await addBooksToChapters(chaptersWithBookId, context) - return { - ...chapters, - chapters: response - } - } +export async function getChaptersByBook( + body: GraphQLBody<{ id: string; pagination?: Pagination }>, + context: IGraphQLContext +) { + const { + req: ChaptersReq, + res: ChaptersRes, + next: ChaptersNext + } = createRESTArgumentsFromGraphqlRequest(context, body, 'chapters'); + const chapters = (await bookController.getChaptersByBook(ChaptersReq, ChaptersRes, ChaptersNext)) as GraphQLResponse<{ + chapters: Chapter[]; + }> | void; + if (!chapters) { + return chapters; + } else { + const chaptersWithBookId = chapters.chapters.map((chapter) => { + return { + ...chapter, + book: body.id + }; + }); + const response = await addBooksToChapters(chaptersWithBookId, context); + return { + ...chapters, + chapters: response + }; + } } -export async function getChapters(body: GraphQLBody<{ pagination?:Pagination }>, context: IGraphQLContext) { - const { - req: ChaptersReq, - res: ChaptersRes, - next: ChaptersNext - } = createRESTArgumentsFromGraphqlRequest(context, body, 'chapters') - const chapters = await chapterController.getChapters(ChaptersReq, ChaptersRes, ChaptersNext) as GraphQLResponse<{ - chapters: ChapterWithBookId[] - }> | void - if (!chapters) { - return chapters - } else { - const response = await addBooksToChapters(chapters.chapters, context) - return { - ...chapters, - chapters: response - } - } +export async function getChapters(body: GraphQLBody<{ pagination?: Pagination }>, context: IGraphQLContext) { + const { + req: ChaptersReq, + res: ChaptersRes, + next: ChaptersNext + } = createRESTArgumentsFromGraphqlRequest(context, body, 'chapters'); + const chapters = (await chapterController.getChapters(ChaptersReq, ChaptersRes, ChaptersNext)) as GraphQLResponse<{ + chapters: ChapterWithBookId[]; + }> | void; + if (!chapters) { + return chapters; + } else { + const response = await addBooksToChapters(chapters.chapters, context); + return { + ...chapters, + chapters: response + }; + } } -export async function getChapter(body: GraphQLBody<{ id: string, pagination?:Pagination } & Pagination>, context: IGraphQLContext) { - const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, body, 'chapter'); - const chapter = await chapterController.getChapter(req, res, next) as GraphQLResponse<{ - chapter: ChapterWithBookId - }> | void - if (!chapter) { - return chapter - } else { - const response = await addBooksToChapters(chapter.chapter, context) - return { - ...chapter, - chapter: response - } - } +export async function getChapter( + body: GraphQLBody<{ id: string; pagination?: Pagination } & Pagination>, + context: IGraphQLContext +) { + const { req, res, next } = createRESTArgumentsFromGraphqlRequest(context, body, 'chapter'); + const chapter = (await chapterController.getChapter(req, res, next)) as GraphQLResponse<{ + chapter: ChapterWithBookId; + }> | void; + if (!chapter) { + return chapter; + } else { + const response = await addBooksToChapters(chapter.chapter, context); + return { + ...chapter, + chapter: response + }; + } } -export async function getMovie(body: GraphQLBody<{ id: string, pagination?:Pagination }>, context: IGraphQLContext) { - const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, body, 'movie'); - const data = await movieController.getMovie(req, res, next); - return data +export async function getMovie(body: GraphQLBody<{ id: string; pagination?: Pagination }>, context: IGraphQLContext) { + const { req, res, next } = createRESTArgumentsFromGraphqlRequest(context, body, 'movie'); + const data = await movieController.getMovie(req, res, next); + return data; } -export async function getMovies(body: GraphQLBody<{ pagination?:Pagination }>, context: IGraphQLContext) { - const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, body, 'movies'); - const data = await movieController.getMovies(req, res, next); - return data +export async function getMovies(body: GraphQLBody<{ pagination?: Pagination }>, context: IGraphQLContext) { + const { req, res, next } = createRESTArgumentsFromGraphqlRequest(context, body, 'movies'); + const data = await movieController.getMovies(req, res, next); + return data; } -export async function getQuoteByMovie(body: GraphQLBody<{ movieId: string, pagination?:Pagination }>, context: IGraphQLContext) { - const { - req: QuotesReq, - res: QuotesRes, - next: QuotesNext - } = createRESTArgumentsFromGraphqlRequest(context, body, 'quotes') - const quotes = await movieController.getQuoteByMovie(QuotesReq, QuotesRes, QuotesNext) as GraphQLResponse<{ - quotes: Quote[] - }> | void - if (!quotes) { - return quotes - } else { - let response = await addMoviesToQuotes(quotes.quotes, context) - response = await addCharacterToQuotes(response, context) - return { - ...quotes, - quotes: response - } - } +export async function getQuoteByMovie( + body: GraphQLBody<{ movieId: string; pagination?: Pagination }>, + context: IGraphQLContext +) { + const { + req: QuotesReq, + res: QuotesRes, + next: QuotesNext + } = createRESTArgumentsFromGraphqlRequest(context, body, 'quotes'); + const quotes = (await movieController.getQuoteByMovie(QuotesReq, QuotesRes, QuotesNext)) as GraphQLResponse<{ + quotes: Quote[]; + }> | void; + if (!quotes) { + return quotes; + } else { + let response = await addMoviesToQuotes(quotes.quotes, context); + response = await addCharacterToQuotes(response, context); + return { + ...quotes, + quotes: response + }; + } } -export async function getCharacter(body: GraphQLBody<{ id: string, pagination?:Pagination }>, context: IGraphQLContext) { - const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, body, 'character'); - const data = await characterController.getCharacter(req, res, next); - return data +export async function getCharacter(body: GraphQLBody<{ id: string; pagination?: Pagination }>, context: IGraphQLContext) { + const { req, res, next } = createRESTArgumentsFromGraphqlRequest(context, body, 'character'); + const data = await characterController.getCharacter(req, res, next); + return data; } -export async function getCharacters(body: GraphQLBody<{ pagination?:Pagination }>, context: IGraphQLContext) { - const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, body, 'characters'); - const data = await characterController.getCharacters(req, res, next); - return data +export async function getCharacters(body: GraphQLBody<{ pagination?: Pagination }>, context: IGraphQLContext) { + const { req, res, next } = createRESTArgumentsFromGraphqlRequest(context, body, 'characters'); + const data = await characterController.getCharacters(req, res, next); + return data; } -export async function getQuoteByCharacter(body: GraphQLBody<{ characterId: string, pagination?:Pagination }>, context: IGraphQLContext) { - const { - req: QuotesReq, - res: QuotesRes, - next: QuotesNext - } = createRESTArgumentsFromGraphqlRequest(context, body, 'quotes') - const quotes = await characterController.getQuoteByCharacter(QuotesReq, QuotesRes, QuotesNext) as GraphQLResponse<{ - quotes: Quote[] - }> | void - if (!quotes) { - return quotes - } else { - let response = await addMoviesToQuotes(quotes.quotes, context) - response = await addCharacterToQuotes(response, context) - return { - ...quotes, - quotes: response - } - } +export async function getQuoteByCharacter( + body: GraphQLBody<{ characterId: string; pagination?: Pagination }>, + context: IGraphQLContext +) { + const { + req: QuotesReq, + res: QuotesRes, + next: QuotesNext + } = createRESTArgumentsFromGraphqlRequest(context, body, 'quotes'); + const quotes = (await characterController.getQuoteByCharacter(QuotesReq, QuotesRes, QuotesNext)) as GraphQLResponse<{ + quotes: Quote[]; + }> | void; + if (!quotes) { + return quotes; + } else { + let response = await addMoviesToQuotes(quotes.quotes, context); + response = await addCharacterToQuotes(response, context); + return { + ...quotes, + quotes: response + }; + } } -export async function getQuote(body: GraphQLBody<{ id: string, pagination?:Pagination }>, context: IGraphQLContext) { - const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, body, 'quote'); - let quote = await quoteController.getQuote(req, res, next) as GraphQLResponse<{ - quote: Quote - }> | void - if (!quote) { - return quote - } - let response = await addMoviesToQuotes(quote.quote, context) - response = await addCharacterToQuotes(response, context) - return { - ...quote, - quote: response - } +export async function getQuote(body: GraphQLBody<{ id: string; pagination?: Pagination }>, context: IGraphQLContext) { + const { req, res, next } = createRESTArgumentsFromGraphqlRequest(context, body, 'quote'); + let quote = (await quoteController.getQuote(req, res, next)) as GraphQLResponse<{ + quote: Quote; + }> | void; + if (!quote) { + return quote; + } + let response = await addMoviesToQuotes(quote.quote, context); + response = await addCharacterToQuotes(response, context); + return { + ...quote, + quote: response + }; } -export async function getQuotes(body: GraphQLBody<{ pagination?:Pagination }>, context: IGraphQLContext) { - const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, body, 'quotes'); - let quotes = await quoteController.getQuotes(req, res, next) as GraphQLResponse<{ - quotes: Quote[] - }> | void - if (!quotes) { - return quotes - } - let response = await addMoviesToQuotes(quotes.quotes, context) - response = await addCharacterToQuotes(response, context) - return { - ...quotes, - quotes: response - } +export async function getQuotes(body: GraphQLBody<{ pagination?: Pagination }>, context: IGraphQLContext) { + const { req, res, next } = createRESTArgumentsFromGraphqlRequest(context, body, 'quotes'); + let quotes = (await quoteController.getQuotes(req, res, next)) as GraphQLResponse<{ + quotes: Quote[]; + }> | void; + if (!quotes) { + return quotes; + } + let response = await addMoviesToQuotes(quotes.quotes, context); + response = await addCharacterToQuotes(response, context); + return { + ...quotes, + quotes: response + }; } - async function addBooksToChapters(chapters: ChapterWithBookId[] | ChapterWithBookId, context: IGraphQLContext) { - if (!Array.isArray(chapters)) { - if (chapters.book) { - const {req, res, next} = createRESTArgumentsFromGraphqlRequest(context, {id: chapters.book}, 'book', false); - const book = await bookController.getBook(req, res, next) as { book: Book } | void; - if (book) { - return {...chapters, book: book.book}; - } - } - return chapters - } else { - const chapterPromises: Promise[] = chapters.map(async (chapter) => { - if (chapter.book) { - const { - req, - res, - next - } = createRESTArgumentsFromGraphqlRequest(context, {id: chapter.book}, 'book', false); - const book = await bookController.getBook(req, res, next) as { book: Book } | void; - if (book) { - return {...chapter, book: book.book}; - } - } - return chapter; - }); - return await Promise.all(chapterPromises).then((chapters) => chapters); - } + if (!Array.isArray(chapters)) { + if (chapters.book) { + const { req, res, next } = createRESTArgumentsFromGraphqlRequest(context, { id: chapters.book }, 'book', false); + const book = (await bookController.getBook(req, res, next)) as { book: Book } | void; + if (book) { + return { ...chapters, book: book.book }; + } + } + return chapters; + } else { + const chapterPromises: Promise[] = chapters.map(async (chapter) => { + if (chapter.book) { + const { req, res, next } = createRESTArgumentsFromGraphqlRequest(context, { id: chapter.book }, 'book', false); + const book = (await bookController.getBook(req, res, next)) as { book: Book } | void; + if (book) { + return { ...chapter, book: book.book }; + } + } + return chapter; + }); + return await Promise.all(chapterPromises).then((chapters) => chapters); + } } - async function addMoviesToQuotes(quotes: Quote[] | Quote, context: IGraphQLContext) { - if (!Array.isArray(quotes)) { - if (quotes.movie) { - const { - req, - res, - next - } = createRESTArgumentsFromGraphqlRequest(context, {id: quotes.movie._id}, 'movie', false); - const movie = await movieController.getMovie(req, res, next) as { movie: Movie } | void; - if (movie) { - return {...quotes, movie: movie.movie} - } - } - return quotes - } else { - const quotePromises: Promise[] = quotes.map(async (quote) => { - if (quote.movie) { - const { - req, - res, - next - } = createRESTArgumentsFromGraphqlRequest(context, {id: quote.movie._id}, 'movie', false); - const movie = await movieController.getMovie(req, res, next) as { movie: Movie } | void; - if (movie) { - return {...quote, movie: movie.movie}; - } - } - return quote; - }); - return await Promise.all(quotePromises).then((quotes) => quotes); - } + if (!Array.isArray(quotes)) { + if (quotes.movie) { + const { req, res, next } = createRESTArgumentsFromGraphqlRequest(context, { id: quotes.movie._id }, 'movie', false); + const movie = (await movieController.getMovie(req, res, next)) as { movie: Movie } | void; + if (movie) { + return { ...quotes, movie: movie.movie }; + } + } + return quotes; + } else { + const quotePromises: Promise[] = quotes.map(async (quote) => { + if (quote.movie) { + const { req, res, next } = createRESTArgumentsFromGraphqlRequest(context, { id: quote.movie._id }, 'movie', false); + const movie = (await movieController.getMovie(req, res, next)) as { movie: Movie } | void; + if (movie) { + return { ...quote, movie: movie.movie }; + } + } + return quote; + }); + return await Promise.all(quotePromises).then((quotes) => quotes); + } } async function addCharacterToQuotes(quote: Quote[] | Quote, context: IGraphQLContext) { - if (!Array.isArray(quote)) { - if (quote.character) { - const { - req, - res, - next - } = createRESTArgumentsFromGraphqlRequest(context, {id: quote.character._id}, 'character', false); - const character = await characterController.getCharacter(req, res, next) as { character: Character } | void; - if (character) { - return {...quote, character: character.character}; - } - } - return quote; - } else { - const quotePromises: Promise[] = quote.map(async (quote) => { - if (quote.character) { - const { - req, - res, - next - } = createRESTArgumentsFromGraphqlRequest(context, {id: quote.character._id}, 'character', false); - const character = await characterController.getCharacter(req, res, next) as { - character: Character - } | void; - if (character) { - return {...quote, character: character.character}; - } - } - return quote; - }); - return await Promise.all(quotePromises).then((quotes) => quotes); - } + if (!Array.isArray(quote)) { + if (quote.character) { + const { req, res, next } = createRESTArgumentsFromGraphqlRequest( + context, + { id: quote.character._id }, + 'character', + false + ); + const character = (await characterController.getCharacter(req, res, next)) as { character: Character } | void; + if (character) { + return { ...quote, character: character.character }; + } + } + return quote; + } else { + const quotePromises: Promise[] = quote.map(async (quote) => { + if (quote.character) { + const { req, res, next } = createRESTArgumentsFromGraphqlRequest( + context, + { id: quote.character._id }, + 'character', + false + ); + const character = (await characterController.getCharacter(req, res, next)) as { + character: Character; + } | void; + if (character) { + return { ...quote, character: character.character }; + } + } + return quote; + }); + return await Promise.all(quotePromises).then((quotes) => quotes); + } } diff --git a/backend/graphql/root.ts b/backend/graphql/root.ts index d367bfc..35c1c7c 100644 --- a/backend/graphql/root.ts +++ b/backend/graphql/root.ts @@ -1,33 +1,33 @@ import { - getChapter, - getChapters, - getChaptersByBook, - getBooks, - getBook, - getCharacters, - getCharacter, - getQuoteByCharacter, - getQuote, - getQuotes, - getQuoteByMovie, - getMovies, - getMovie -} from './resolvers' + getChapter, + getChapters, + getChaptersByBook, + getBooks, + getBook, + getCharacters, + getCharacter, + getQuoteByCharacter, + getQuote, + getQuotes, + getQuoteByMovie, + getMovies, + getMovie +} from './resolvers'; const root = { - book: getBook, - books: getBooks, - chaptersByBook: getChaptersByBook, - chapters: getChapters, - chapter: getChapter, - movies: getMovies, - movie: getMovie, - quoteByMovie: getQuoteByMovie, - characters: getCharacters, - character: getCharacter, - quoteByCharacter: getQuoteByCharacter, - quotes: getQuotes, - quote: getQuote -} + book: getBook, + books: getBooks, + chaptersByBook: getChaptersByBook, + chapters: getChapters, + chapter: getChapter, + movies: getMovies, + movie: getMovie, + quoteByMovie: getQuoteByMovie, + characters: getCharacters, + character: getCharacter, + quoteByCharacter: getQuoteByCharacter, + quotes: getQuotes, + quote: getQuote +}; -export default root \ No newline at end of file +export default root; diff --git a/backend/graphql/schema.ts b/backend/graphql/schema.ts index 76f7d0a..a4154d4 100644 --- a/backend/graphql/schema.ts +++ b/backend/graphql/schema.ts @@ -1,4 +1,4 @@ -import {buildSchema} from "graphql/utilities"; +import { buildSchema } from 'graphql/utilities'; const schema = buildSchema(` type Query { @@ -146,89 +146,88 @@ const schema = buildSchema(` } `); - export default schema; export interface IGraphQLContext { - requestInfo: { - req: Express.Request; - context: { - res: Express.Response; - } - } + requestInfo: { + req: Express.Request; + context: { + res: Express.Response; + }; + }; } export type GraphQLBody = T & { - sort?: string - limit?: string - page?: string - offset?: string -} + sort?: string; + limit?: string; + page?: string; + offset?: string; +}; export type GraphQLResponse = T & { - total: number - limit: number - page: number - pages: number -} + total: number; + limit: number; + page: number; + pages: number; +}; export type Book = { - _id: string - name: string -} + _id: string; + name: string; +}; export type Chapter = { - _id: string - chapterName: string -} + _id: string; + chapterName: string; +}; export type ChapterWithBookId = Chapter & { - book: string -} + book: string; +}; export type Movie = { - _id: string - name: string - runtimeInMinutes: number - budgetInMillions: number - boxOfficeRevenueInMillions: number - academyAwardNominations: number - academyAwardWins: number - rottenTomatoesScore: number -} + _id: string; + name: string; + runtimeInMinutes: number; + budgetInMillions: number; + boxOfficeRevenueInMillions: number; + academyAwardNominations: number; + academyAwardWins: number; + rottenTomatoesScore: number; +}; export type Character = { - _id: string - height: string - race: string - gender: string - birth: string - spouse: string - death: string - realm: string - hair: string - name: string - wikiUrl: string -} + _id: string; + height: string; + race: string; + gender: string; + birth: string; + spouse: string; + death: string; + realm: string; + hair: string; + name: string; + wikiUrl: string; +}; export type Quote = { - _id: string - dialog: string - movie?: Movie - character?: Character -} + _id: string; + dialog: string; + movie?: Movie; + character?: Character; +}; export type Pagination = { - total?: number - limit?: number - page?: number - pages?: number -} + total?: number; + limit?: number; + page?: number; + pages?: number; +}; export type DataNames = - 'books' - | 'book' - | 'chapters' - | 'chapter' - | 'movies' - | 'movie' - | 'quoteByMovie' - | 'characters' - | 'character' - | 'quoteByCharacter' - | 'quotes' - | 'quote' \ No newline at end of file + | 'books' + | 'book' + | 'chapters' + | 'chapter' + | 'movies' + | 'movie' + | 'quoteByMovie' + | 'characters' + | 'character' + | 'quoteByCharacter' + | 'quotes' + | 'quote'; diff --git a/backend/helpers/config.ts b/backend/helpers/config.ts index ffa59dd..4c913d6 100644 --- a/backend/helpers/config.ts +++ b/backend/helpers/config.ts @@ -1,99 +1,108 @@ -import {Request} from 'express'; -import {MongooseQueryParser} from 'mongoose-query-parser'; -import {PaginateOptions} from './interfaces'; -import {IGraphQLContext, DataNames} from "../graphql/schema"; -import {ParamsDictionary} from "express-serve-static-core"; -import {ParsedQs} from "qs"; +import { Request } from 'express'; +import { MongooseQueryParser } from 'mongoose-query-parser'; +import { PaginateOptions } from './interfaces'; +import { IGraphQLContext, DataNames } from '../graphql/schema'; +import { ParsedQs } from 'qs'; const ascending = 'asc'; const maxLimit = 1000; export const getOptions = async (req: Request): Promise => { - let options: PaginateOptions = {filter: {}}; + let options: PaginateOptions = { filter: {} }; - const sort = req?.query?.sort as string; - const page = req?.query?.page as string; - const limit = req?.query?.limit as string; - const offset = req?.query?.offset as string; + const sort = req?.query?.sort as string; + const page = req?.query?.page as string; + const limit = req?.query?.limit as string; + const offset = req?.query?.offset as string; - // Express does not offer a handy way to get the raw query strings - // so let's parse it and drop the leading `?` for the parser - const url = new URL(req.protocol + '://' + req.hostname + req.originalUrl); - const rawQueryParams = url.search.slice(1); + // Express does not offer a handy way to get the raw query strings + // so let's parse it and drop the leading `?` for the parser + const url = new URL(req.protocol + '://' + req.hostname + req.originalUrl); + const rawQueryParams = url.search.slice(1); - const parser = new MongooseQueryParser({ - blacklist: ['offset', 'page', 'limit', 'sort'] - }); - const parsed = parser.parse(rawQueryParams); - options.filter = parsed.filter; + const parser = new MongooseQueryParser({ + blacklist: ['offset', 'page', 'limit', 'sort'] + }); + const parsed = parser.parse(rawQueryParams); + options.filter = parsed.filter; - if (sort) { - const fields = sort.split(':'); - const sorter = fields[0]; - const direction = fields[1]; - options.sort = {[sorter]: direction === ascending ? 1 : -1}; - } + if (sort) { + const fields = sort.split(':'); + const sorter = fields[0]; + const direction = fields[1]; + options.sort = { [sorter]: direction === ascending ? 1 : -1 }; + } - if (limit) { - options.limit = parseInt(limit); - } else { - options.limit = maxLimit; - } + if (limit) { + options.limit = parseInt(limit); + } else { + options.limit = maxLimit; + } - if (page) { - options.page = parseInt(page); - } + if (page) { + options.page = parseInt(page); + } - if (offset) { - options.offset = parseInt(offset); - } + if (offset) { + options.offset = parseInt(offset); + } - return options; + return options; }; -export const createRESTArgumentsFromGraphqlRequest = (context: IGraphQLContext, bodyPayload: any, dataName: DataNames, addPaginationData:boolean = true) => { - const { pagination, ...body} = bodyPayload - const req = { - ...context.requestInfo.req, - query: { - ...pagination - }, - params: { - ...body - } - } as Request<{params:typeof bodyPayload}, any, any, ParsedQs, Record>; - function isPlural(dataName: DataNames): boolean { - return dataName.endsWith('s'); - } - const res = { - ...context.requestInfo.context.res, - json: (data: any):{ - [dataName: string]: any, - pages?: number, - page?: number, - offset?: number, - limit?: number, - total?: number - } => { - const targetData = isPlural(dataName) ? data.docs.map((el:any)=>el.toObject()) : data.docs[0] ? data.docs[0].toObject() : [] - let returnData = { - [dataName]: targetData, - }; - if(addPaginationData) { - returnData = { - ...returnData, - pages: data.pages, - page: data.page, - offset: data.offset, - limit: data.limit, - total: data.total - } - } - return returnData - } - } as any; - const next = (err: any) => { - throw new Error(err); - }; - return {req, res, next}; -} - +export const createRESTArgumentsFromGraphqlRequest = ( + context: IGraphQLContext, + bodyPayload: any, + dataName: DataNames, + addPaginationData: boolean = true +) => { + const { pagination, ...body } = bodyPayload; + const req = { + ...context.requestInfo.req, + query: { + ...pagination + }, + params: { + ...body + } + } as Request<{ params: typeof bodyPayload }, any, any, ParsedQs, Record>; + function isPlural(dataName: DataNames): boolean { + return dataName.endsWith('s'); + } + const res = { + ...context.requestInfo.context.res, + json: ( + data: any + ): { + [dataName: string]: any; + pages?: number; + page?: number; + offset?: number; + limit?: number; + total?: number; + } => { + const targetData = isPlural(dataName) + ? data.docs.map((el: any) => el.toObject()) + : data.docs[0] + ? data.docs[0].toObject() + : []; + let returnData = { + [dataName]: targetData + }; + if (addPaginationData) { + returnData = { + ...returnData, + pages: data.pages, + page: data.page, + offset: data.offset, + limit: data.limit, + total: data.total + }; + } + return returnData; + } + } as any; + const next = (err: any) => { + throw new Error(err); + }; + return { req, res, next }; +}; diff --git a/backend/helpers/passport.ts b/backend/helpers/passport.ts index 596ee5a..28dd04e 100644 --- a/backend/helpers/passport.ts +++ b/backend/helpers/passport.ts @@ -6,14 +6,14 @@ import { HttpCode } from './constants'; export const passportHelpers = { authenticate: async (req: Request, res: Response, next: NextFunction) => { passport.authenticate('bearer', { session: false }, async function (err: Error, token: string) { - if (err || !token) { - return new Error('Unauthorized'); - } else { - next(); - } + if (err || !token) { + return new Error('Unauthorized'); + } else { + next(); + } })(req, res, next); }, - graphqlAuthentication: (req: Request, res: Response, next:NextFunction) => { + graphqlAuthentication: (req: Request, res: Response, next: NextFunction) => { const allowedOperations = ['getBooks', 'book', 'chaptersByBook', 'IntrospectionQuery']; passport.authenticate('bearer', { session: false }, function (err: Error, token: string) { try { diff --git a/backend/server.ts b/backend/server.ts index 73e7557..b8a37ee 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -1,4 +1,4 @@ -import {passportHelpers} from "./helpers/passport"; +import { passportHelpers } from './helpers/passport'; require('dotenv').config(); import mongoose from 'mongoose'; @@ -9,16 +9,16 @@ import cors from 'cors'; import helmet from 'helmet'; import path from 'path'; -import {connectDb} from './helpers/db'; -import {apiLimiter} from './middleware/api.limiter'; -import {UserModel} from './models/user.model'; -import {Strategy as LocalStrategy} from 'passport-local'; -import {Strategy as BearerStrategy} from 'passport-http-bearer'; -import {HttpCode} from './helpers/constants'; +import { connectDb } from './helpers/db'; +import { apiLimiter } from './middleware/api.limiter'; +import { UserModel } from './models/user.model'; +import { Strategy as LocalStrategy } from 'passport-local'; +import { Strategy as BearerStrategy } from 'passport-http-bearer'; +import { HttpCode } from './helpers/constants'; import apiRoutes from './routes/api'; import authRoutes from './routes/auth'; -import {User} from './helpers/interfaces'; -import {createHandler} from 'graphql-http/lib/use/express'; +import { User } from './helpers/interfaces'; +import { createHandler } from 'graphql-http/lib/use/express'; import schema from './graphql/schema'; import root from './graphql/root'; @@ -26,7 +26,7 @@ const app = express(); const dpwcToken = process.env.DPWC_TOKEN || ''; app.use(helmet()); app.use(express.json()); -app.use(express.urlencoded({extended: false})); +app.use(express.urlencoded({ extended: false })); app.use(cors()); app.use(express.static(path.join(__dirname, '/../__BUILD'))); // React build @@ -34,90 +34,94 @@ app.use(express.static(path.join(__dirname, '/../__BUILD'))); // React build const server_port = process.env.PORT || 3001; passport.use( - new BearerStrategy(async (token, done) => { - try { - if (dpwcToken === token) { - return done(null, token, {message: 'Token valid.', scope: 'read'}); - } - await UserModel.findOne({access_token: token}, async function (err: unknown, user: User) { - if (err) { - return done(err, false, {message: 'Invalid token.', scope: 'read'}); - } - if (!user) { - return done(err, false, {message: 'Wrong token.', scope: 'read'}); - } - return done(null, token, {message: 'Token valid.', scope: 'read'}); - }); - } catch (error) { - done(error); - } - }) + new BearerStrategy(async (token, done) => { + try { + if (dpwcToken === token) { + return done(null, token, { message: 'Token valid.', scope: 'read' }); + } + await UserModel.findOne({ access_token: token }, async function (err: unknown, user: User) { + if (err) { + return done(err, false, { message: 'Invalid token.', scope: 'read' }); + } + if (!user) { + return done(err, false, { message: 'Wrong token.', scope: 'read' }); + } + return done(null, token, { message: 'Token valid.', scope: 'read' }); + }); + } catch (error) { + done(error); + } + }) ); // prepare jwt login passport.use( - 'login', - new LocalStrategy( - { - usernameField: 'email', - passwordField: 'password' - }, - async (username: string, password: string, done: Function) => { - try { - await UserModel.findOne({email: username}, async function (err: unknown, user: User) { - if (err) { - return done(err); - } - if (!user) { - return done(null, false, {message: 'Incorrect username.'}); - } - let passwordMatch = await bcrypt.compare(password, user.password); - if (passwordMatch) { - return done(null, user, {message: 'Logged in Successfully'}); - } else { - return done(null, false, {message: 'Incorrect password.'}); - } - }); - } catch (error) { - done(error); - } - } - ) + 'login', + new LocalStrategy( + { + usernameField: 'email', + passwordField: 'password' + }, + async (username: string, password: string, done: Function) => { + try { + await UserModel.findOne({ email: username }, async function (err: unknown, user: User) { + if (err) { + return done(err); + } + if (!user) { + return done(null, false, { message: 'Incorrect username.' }); + } + let passwordMatch = await bcrypt.compare(password, user.password); + if (passwordMatch) { + return done(null, user, { message: 'Logged in Successfully' }); + } else { + return done(null, false, { message: 'Incorrect password.' }); + } + }); + } catch (error) { + done(error); + } + } + ) ); app.use((req, res, next) => { - const path = req.path; - if (path.startsWith('/v2') || path.startsWith('/auth') || path.startsWith('/graphql')) { - const connected = mongoose.connection.readyState === 1; - if (connected) { - next(); - } else { - return res.status(HttpCode.SERVER_ERROR).send({ - success: false, - message: 'Service currently not available.' - }); - } - } else { - next(); - } + const path = req.path; + if (path.startsWith('/v2') || path.startsWith('/auth') || path.startsWith('/graphql')) { + const connected = mongoose.connection.readyState === 1; + if (connected) { + next(); + } else { + return res.status(HttpCode.SERVER_ERROR).send({ + success: false, + message: 'Service currently not available.' + }); + } + } else { + next(); + } }); -app.all("/graphql", passportHelpers.graphqlAuthentication, createHandler({ - schema: schema, - rootValue: root, - context: (req) => ({requestInfo: req}) -})) +app.all( + '/graphql', + passportHelpers.graphqlAuthentication, + createHandler({ + schema: schema, + rootValue: root, + context: (req) => ({ requestInfo: req }) + }) +); app.use('/v2/', apiLimiter); app.use('/v2', apiRoutes); app.use('/auth', authRoutes); // Handles React frontend requests app.get('*', (req, res) => { - res.sendFile(path.join(__dirname + '/../__BUILD/index.html')); + res.sendFile(path.join(__dirname + '/../__BUILD/index.html')); }); async function start() { - await connectDb(); - app.listen(server_port, () => console.log(`LotR backend listening on port ${server_port}!`)); + await connectDb(); + app.listen(server_port, () => console.log(`LotR backend listening on port ${server_port}!`)); } start();