From f55cbc73d820c0702542b8d312a3923b96de91df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Thu, 21 Oct 2021 11:43:05 +0200 Subject: [PATCH] Split snippet controllers into separate files --- src/controllers/snippets.ts | 298 --------------------- src/controllers/snippets/countTags.ts | 31 +++ src/controllers/snippets/createSnippet.ts | 48 ++++ src/controllers/snippets/deleteSnippet.ts | 50 ++++ src/controllers/snippets/getAllSnippets.ts | 43 +++ src/controllers/snippets/getRawCode.ts | 38 +++ src/controllers/snippets/getSnippet.ts | 66 +++++ src/controllers/snippets/index.ts | 8 + src/controllers/snippets/searchSnippets.ts | 72 +++++ src/controllers/snippets/updateSnippet.ts | 70 +++++ src/routes/snippets.ts | 25 +- src/typescript/interfaces/Request.ts | 3 +- src/typescript/interfaces/Snippet.ts | 4 +- src/utils/signToken.ts | 2 +- 14 files changed, 446 insertions(+), 312 deletions(-) delete mode 100644 src/controllers/snippets.ts create mode 100644 src/controllers/snippets/countTags.ts create mode 100644 src/controllers/snippets/createSnippet.ts create mode 100644 src/controllers/snippets/deleteSnippet.ts create mode 100644 src/controllers/snippets/getAllSnippets.ts create mode 100644 src/controllers/snippets/getRawCode.ts create mode 100644 src/controllers/snippets/getSnippet.ts create mode 100644 src/controllers/snippets/index.ts create mode 100644 src/controllers/snippets/searchSnippets.ts create mode 100644 src/controllers/snippets/updateSnippet.ts diff --git a/src/controllers/snippets.ts b/src/controllers/snippets.ts deleted file mode 100644 index ab10976..0000000 --- a/src/controllers/snippets.ts +++ /dev/null @@ -1,298 +0,0 @@ -import { Request, Response, NextFunction } from 'express'; -import { QueryTypes, Op } from 'sequelize'; -import { sequelize } from '../db'; -import { asyncWrapper } from '../middleware'; -import { SnippetModel, Snippet_TagModel, TagModel } from '../models'; -import { ErrorResponse, tagParser, Logger, createTags } from '../utils'; -import { Body, SearchQuery } from '../typescript/interfaces'; - -/** - * @description Create new snippet - * @route /api/snippets - * @request POST - */ -export const createSnippet = asyncWrapper( - async (req: Request, res: Response, next: NextFunction): Promise => { - // Get tags from request body - const { language, tags: requestTags } = req.body; - const parsedRequestTags = tagParser([ - ...requestTags, - language.toLowerCase() - ]); - - // Create snippet - const snippet = await SnippetModel.create({ - ...req.body, - tags: [...parsedRequestTags].join(',') - }); - - // Create tags - await createTags(parsedRequestTags, snippet.id); - - // Get raw snippet values - const rawSnippet = snippet.get({ plain: true }); - - res.status(201).json({ - data: { - ...rawSnippet, - tags: [...parsedRequestTags] - } - }); - } -); - -/** - * @description Get all snippets - * @route /api/snippets - * @request GET - */ -export const getAllSnippets = asyncWrapper( - async (req: Request, res: Response, next: NextFunction): Promise => { - const snippets = await SnippetModel.findAll({ - include: { - model: TagModel, - as: 'tags', - attributes: ['name'], - through: { - attributes: [] - } - } - }); - - const populatedSnippets = snippets.map(snippet => { - const rawSnippet = snippet.get({ plain: true }); - - return { - ...rawSnippet, - tags: rawSnippet.tags?.map(tag => tag.name) - }; - }); - - res.status(200).json({ - data: populatedSnippets - }); - } -); - -/** - * @description Get single snippet by id - * @route /api/snippets/:id - * @request GET - */ -export const getSnippet = asyncWrapper( - async (req: Request, res: Response, next: NextFunction): Promise => { - const snippet = await SnippetModel.findOne({ - where: { id: req.params.id }, - include: { - model: TagModel, - as: 'tags', - attributes: ['name'], - through: { - attributes: [] - } - } - }); - - if (!snippet) { - return next( - new ErrorResponse( - 404, - `Snippet with id of ${req.params.id} was not found` - ) - ); - } - - const rawSnippet = snippet.get({ plain: true }); - const populatedSnippet = { - ...rawSnippet, - tags: rawSnippet.tags?.map(tag => tag.name) - }; - - res.status(200).json({ - data: populatedSnippet - }); - } -); - -/** - * @description Update snippet - * @route /api/snippets/:id - * @request PUT - */ -export const updateSnippet = asyncWrapper( - async (req: Request, res: Response, next: NextFunction): Promise => { - let snippet = await SnippetModel.findOne({ - where: { id: req.params.id } - }); - - if (!snippet) { - return next( - new ErrorResponse( - 404, - `Snippet with id of ${req.params.id} was not found` - ) - ); - } - - // Get tags from request body - const { language, tags: requestTags } = req.body; - let parsedRequestTags = tagParser([...requestTags, language.toLowerCase()]); - - // Update snippet - snippet = await snippet.update({ - ...req.body, - tags: [...parsedRequestTags].join(',') - }); - - // Delete old tags and create new ones - await Snippet_TagModel.destroy({ where: { snippet_id: req.params.id } }); - await createTags(parsedRequestTags, snippet.id); - - // Get raw snippet values - const rawSnippet = snippet.get({ plain: true }); - - res.status(200).json({ - data: { - ...rawSnippet, - tags: [...parsedRequestTags] - } - }); - } -); - -/** - * @description Delete snippet - * @route /api/snippets/:id - * @request DELETE - */ -export const deleteSnippet = asyncWrapper( - async (req: Request, res: Response, next: NextFunction): Promise => { - const snippet = await SnippetModel.findOne({ - where: { id: req.params.id } - }); - - if (!snippet) { - return next( - new ErrorResponse( - 404, - `Snippet with id of ${req.params.id} was not found` - ) - ); - } - - await Snippet_TagModel.destroy({ where: { snippet_id: req.params.id } }); - await snippet.destroy(); - - res.status(200).json({ - data: {} - }); - } -); - -/** - * @description Count tags - * @route /api/snippets/statistics/count - * @request GET - */ -export const countTags = asyncWrapper( - async (req: Request, res: Response, next: NextFunction): Promise => { - const result = await sequelize.query( - `SELECT - COUNT(tags.name) as count, - tags.name - FROM snippets_tags - INNER JOIN tags ON snippets_tags.tag_id = tags.id - GROUP BY tags.name - ORDER BY name ASC`, - { - type: QueryTypes.SELECT - } - ); - - res.status(200).json({ - data: result - }); - } -); - -/** - * @description Get raw snippet code - * @route /api/snippets/raw/:id - * @request GET - */ -export const getRawCode = asyncWrapper( - async (req: Request, res: Response, next: NextFunction): Promise => { - const snippet = await SnippetModel.findOne({ - where: { id: req.params.id }, - raw: true - }); - - if (!snippet) { - return next( - new ErrorResponse( - 404, - `Snippet with id of ${req.params.id} was not found` - ) - ); - } - - res.status(200).send(snippet.code); - } -); - -/** - * @description Search snippets - * @route /api/snippets/search - * @request POST - */ -export const searchSnippets = asyncWrapper( - async (req: Request, res: Response, next: NextFunction): Promise => { - const { query, tags, languages } = req.body; - - // Check if query is empty - if (query === '' && !tags.length && !languages.length) { - res.status(200).json({ - data: [] - }); - - return; - } - - const languageFilter = languages.length - ? { [Op.in]: languages } - : { [Op.notIn]: languages }; - - const tagFilter = tags.length ? { [Op.in]: tags } : { [Op.notIn]: tags }; - - const snippets = await SnippetModel.findAll({ - where: { - [Op.and]: [ - { - [Op.or]: [ - { title: { [Op.substring]: `${query}` } }, - { description: { [Op.substring]: `${query}` } } - ] - }, - { - language: languageFilter - } - ] - }, - include: { - model: TagModel, - as: 'tags', - attributes: ['name'], - where: { - name: tagFilter - }, - through: { - attributes: [] - } - } - }); - - res.status(200).json({ - data: snippets - }); - } -); diff --git a/src/controllers/snippets/countTags.ts b/src/controllers/snippets/countTags.ts new file mode 100644 index 0000000..a06e36b --- /dev/null +++ b/src/controllers/snippets/countTags.ts @@ -0,0 +1,31 @@ +import { Response, NextFunction } from 'express'; +import { QueryTypes } from 'sequelize'; +import { sequelize } from '../../db'; +import { asyncWrapper } from '../../middleware'; + +/** + * @description Count tags + * @route /api/snippets/statistics/count + * @request GET + * @access Private + */ +export const countTags = asyncWrapper( + async (req: Request, res: Response, next: NextFunction): Promise => { + const result = await sequelize.query( + `SELECT + COUNT(tags.name) as count, + tags.name + FROM snippets_tags + INNER JOIN tags ON snippets_tags.tag_id = tags.id + GROUP BY tags.name + ORDER BY name ASC`, + { + type: QueryTypes.SELECT + } + ); + + res.status(200).json({ + data: result + }); + } +); diff --git a/src/controllers/snippets/createSnippet.ts b/src/controllers/snippets/createSnippet.ts new file mode 100644 index 0000000..06edb5a --- /dev/null +++ b/src/controllers/snippets/createSnippet.ts @@ -0,0 +1,48 @@ +import { Response, NextFunction } from 'express'; +import { asyncWrapper } from '../../middleware'; +import { SnippetModel } from '../../models'; +import { Snippet, UserInfoRequest } from '../../typescript/interfaces'; +import { tagParser, createTags } from '../../utils'; + +interface RequestBody extends Snippet {} + +/** + * @description Create new snippet + * @route /api/snippets + * @request POST + * @access Private + */ +export const createSnippet = asyncWrapper( + async ( + req: UserInfoRequest, + res: Response, + next: NextFunction + ): Promise => { + // Get tags from request body + const { language, tags: requestTags = [] } = req.body; + + const parsedRequestTags = tagParser([ + ...requestTags, + language.toLowerCase() + ]); + + // Create snippet + const snippet = await SnippetModel.create({ + ...req.body, + createdBy: req.user.id + }); + + // Create tags + await createTags(parsedRequestTags, snippet.id); + + // Get raw snippet values + const rawSnippet = snippet.get({ plain: true }); + + res.status(201).json({ + data: { + ...rawSnippet, + tags: [...parsedRequestTags] + } + }); + } +); diff --git a/src/controllers/snippets/deleteSnippet.ts b/src/controllers/snippets/deleteSnippet.ts new file mode 100644 index 0000000..f8efb8a --- /dev/null +++ b/src/controllers/snippets/deleteSnippet.ts @@ -0,0 +1,50 @@ +import { Response, NextFunction } from 'express'; +import { asyncWrapper } from '../../middleware'; +import { SnippetModel, Snippet_TagModel } from '../../models'; +import { Snippet, UserInfoRequest } from '../../typescript/interfaces'; +import { tagParser, createTags, ErrorResponse } from '../../utils'; + +interface Params { + id: number; +} + +/** + * @description Delete snippet + * @route /api/snippets/:id + * @request DELETE + * @access Private + */ +export const deleteSnippet = asyncWrapper( + async ( + req: UserInfoRequest<{}, Params>, + res: Response, + next: NextFunction + ): Promise => { + const snippet = await SnippetModel.findOne({ + where: { id: req.params.id } + }); + + if (!snippet) { + return next( + new ErrorResponse( + 404, + `Snippet with the id of ${req.params.id} was not found` + ) + ); + } + + if (snippet.createdBy != req.user.id && !req.user.isAdmin) { + return next( + new ErrorResponse(401, `You are not authorized to modify this resource`) + ); + } + + // Delete all snippet <> tag relations + await Snippet_TagModel.destroy({ where: { snippet_id: req.params.id } }); + await snippet.destroy(); + + res.status(200).json({ + data: {} + }); + } +); diff --git a/src/controllers/snippets/getAllSnippets.ts b/src/controllers/snippets/getAllSnippets.ts new file mode 100644 index 0000000..63b850b --- /dev/null +++ b/src/controllers/snippets/getAllSnippets.ts @@ -0,0 +1,43 @@ +import { Request, Response, NextFunction } from 'express'; +import { asyncWrapper } from '../../middleware'; +import { SnippetModel, TagModel } from '../../models'; + +/** + * @description Get all snippets + * @route /api/snippets + * @request GET + */ +export const getAllSnippets = asyncWrapper( + async (req: Request, res: Response, next: NextFunction): Promise => { + const snippets = await SnippetModel.findAll({ + include: { + model: TagModel, + as: 'tags', + attributes: ['name'], + through: { + attributes: [] + } + } + }); + + const populatedSnippets = snippets.map(snippet => { + const rawSnippet = snippet.get({ plain: true }); + let tags: string[] = []; + + if (rawSnippet.tags) { + // @ts-ignore + const rawTags = rawSnippet.tags as { name: string }[]; + tags = rawTags.map(tag => tag.name); + } + + return { + ...rawSnippet, + tags + }; + }); + + res.status(200).json({ + data: populatedSnippets + }); + } +); diff --git a/src/controllers/snippets/getRawCode.ts b/src/controllers/snippets/getRawCode.ts new file mode 100644 index 0000000..584c6da --- /dev/null +++ b/src/controllers/snippets/getRawCode.ts @@ -0,0 +1,38 @@ +import { Request, Response, NextFunction } from 'express'; +import { asyncWrapper } from '../../middleware'; +import { SnippetModel } from '../../models'; +import { ErrorResponse } from '../../utils'; + +interface Params { + id: number; +} + +/** + * @description Get raw snippet code + * @route /api/snippets/raw/:id + * @request GET + * @access Private + */ +export const getRawCode = asyncWrapper( + async ( + req: Request, + res: Response, + next: NextFunction + ): Promise => { + const snippet = await SnippetModel.findOne({ + where: { id: req.params.id }, + raw: true + }); + + if (!snippet) { + return next( + new ErrorResponse( + 404, + `Snippet with the id of ${req.params.id} was not found` + ) + ); + } + + res.status(200).send(snippet.code); + } +); diff --git a/src/controllers/snippets/getSnippet.ts b/src/controllers/snippets/getSnippet.ts new file mode 100644 index 0000000..8686a39 --- /dev/null +++ b/src/controllers/snippets/getSnippet.ts @@ -0,0 +1,66 @@ +import { Response, NextFunction } from 'express'; +import { asyncWrapper } from '../../middleware'; +import { SnippetModel, TagModel } from '../../models'; +import { UserInfoRequest } from '../../typescript/interfaces'; +import { ErrorResponse } from '../../utils'; + +interface Params { + id: number; +} + +/** + * @description Get single snippet by id + * @route /api/snippets/:id + * @request GET + * @access Private + */ +export const getSnippet = asyncWrapper( + async ( + req: UserInfoRequest<{}, Params>, + res: Response, + next: NextFunction + ): Promise => { + const snippet = await SnippetModel.findOne({ + where: { id: req.params.id }, + include: { + model: TagModel, + as: 'tags', + attributes: ['name'], + through: { + attributes: [] + } + } + }); + + if (!snippet) { + return next( + new ErrorResponse( + 404, + `Snippet with the id of ${req.params.id} was not found` + ) + ); + } + + if (snippet.createdBy != req.user.id && !req.user.isAdmin) { + return next( + new ErrorResponse(401, `You are not authorized to access this resource`) + ); + } + + const rawSnippet = snippet.get({ plain: true }); + + if (rawSnippet.tags) { + // @ts-ignore + const rawTags = rawSnippet.tags as { name: string }[]; + + const populatedSnippet = { + ...rawSnippet, + tags: rawTags.map(tag => tag.name) + }; + + res.status(200).json({ + data: populatedSnippet + }); + } + } +); diff --git a/src/controllers/snippets/index.ts b/src/controllers/snippets/index.ts new file mode 100644 index 0000000..3c7ea27 --- /dev/null +++ b/src/controllers/snippets/index.ts @@ -0,0 +1,8 @@ +export * from './createSnippet'; +export * from './getAllSnippets'; +export * from './getSnippet'; +export * from './deleteSnippet'; +export * from './countTags'; +export * from './getRawCode'; +export * from './searchSnippets'; +export * from './updateSnippet'; diff --git a/src/controllers/snippets/searchSnippets.ts b/src/controllers/snippets/searchSnippets.ts new file mode 100644 index 0000000..0658fa4 --- /dev/null +++ b/src/controllers/snippets/searchSnippets.ts @@ -0,0 +1,72 @@ +import { Request, Response, NextFunction } from 'express'; +import { asyncWrapper } from '../../middleware'; +import { SnippetModel, TagModel } from '../../models'; +import { Op } from 'sequelize'; + +interface Body { + query: string; + tags: string[]; + languages: string[]; +} + +/** + * @description Search snippets + * @route /api/snippets/search + * @request POST + * @access Private + */ +export const searchSnippets = asyncWrapper( + async ( + req: Request<{}, {}, Body>, + res: Response, + next: NextFunction + ): Promise => { + const { query, tags, languages } = req.body; + + // Check if query is empty + if (query === '' && !tags.length && !languages.length) { + res.status(200).json({ + data: [] + }); + + return; + } + + const languageFilter = languages.length + ? { [Op.in]: languages } + : { [Op.notIn]: languages }; + + const tagFilter = tags.length ? { [Op.in]: tags } : { [Op.notIn]: tags }; + + const snippets = await SnippetModel.findAll({ + where: { + [Op.and]: [ + { + [Op.or]: [ + { title: { [Op.substring]: `${query}` } }, + { description: { [Op.substring]: `${query}` } } + ] + }, + { + language: languageFilter + } + ] + }, + include: { + model: TagModel, + as: 'tags', + attributes: ['name'], + where: { + name: tagFilter + }, + through: { + attributes: [] + } + } + }); + + res.status(200).json({ + data: snippets + }); + } +); diff --git a/src/controllers/snippets/updateSnippet.ts b/src/controllers/snippets/updateSnippet.ts new file mode 100644 index 0000000..f7418c3 --- /dev/null +++ b/src/controllers/snippets/updateSnippet.ts @@ -0,0 +1,70 @@ +import { Response, NextFunction } from 'express'; +import { asyncWrapper } from '../../middleware'; +import { SnippetModel, Snippet_TagModel } from '../../models'; +import { ErrorResponse, tagParser, createTags } from '../../utils'; +import { Snippet, UserInfoRequest } from '../../typescript/interfaces'; + +interface Body extends Snippet {} + +interface Params { + id: number; +} + +/** + * @description Update snippet + * @route /api/snippets/:id + * @request PUT + * @access Private + */ +export const updateSnippet = asyncWrapper( + async ( + req: UserInfoRequest, + res: Response, + next: NextFunction + ): Promise => { + let snippet = await SnippetModel.findOne({ + where: { id: req.params.id } + }); + + if (!snippet) { + return next( + new ErrorResponse( + 404, + `Snippet with the id of ${req.params.id} was not found` + ) + ); + } + + if (snippet.createdBy != req.user.id && !req.user.isAdmin) { + return next( + new ErrorResponse(401, `You are not authorized to modify this resource`) + ); + } + + // Get tags from request body + const { language, tags: requestTags = [] } = req.body; + let parsedRequestTags = tagParser([...requestTags, language.toLowerCase()]); + + // todo + // check if tags and/or lang was changed + + // Update snippet + snippet = await snippet.update({ + ...req.body + }); + + // Delete old tags and create new ones + await Snippet_TagModel.destroy({ where: { snippet_id: req.params.id } }); + await createTags(parsedRequestTags, snippet.id); + + // Get raw snippet values + const rawSnippet = snippet.get({ plain: true }); + + res.status(200).json({ + data: { + ...rawSnippet, + tags: [...parsedRequestTags] + } + }); + } +); diff --git a/src/routes/snippets.ts b/src/routes/snippets.ts index 8e55c78..9d58474 100644 --- a/src/routes/snippets.ts +++ b/src/routes/snippets.ts @@ -1,29 +1,34 @@ import { Router } from 'express'; +import { authenticate, requireBody } from '../middleware'; + import { - countTags, createSnippet, - deleteSnippet, getAllSnippets, - getRawCode, getSnippet, + deleteSnippet, + countTags, + getRawCode, searchSnippets, updateSnippet -} from '../controllers/snippets'; -import { requireBody } from '../middleware'; +} from '../controllers/snippets/'; export const snippetRouter = Router(); snippetRouter .route('/') - .post(requireBody('title', 'language', 'code'), createSnippet) + .post( + authenticate, + requireBody('title', 'language', 'code', 'tags'), + createSnippet + ) .get(getAllSnippets); snippetRouter .route('/:id') - .get(getSnippet) - .put(updateSnippet) - .delete(deleteSnippet); + .get(authenticate, getSnippet) + .put(authenticate, updateSnippet) + .delete(authenticate, deleteSnippet); snippetRouter.route('/statistics/count').get(countTags); snippetRouter.route('/raw/:id').get(getRawCode); -snippetRouter.route('/search').post(searchSnippets); \ No newline at end of file +snippetRouter.route('/search').post(searchSnippets); diff --git a/src/typescript/interfaces/Request.ts b/src/typescript/interfaces/Request.ts index 374513f..c64abf9 100644 --- a/src/typescript/interfaces/Request.ts +++ b/src/typescript/interfaces/Request.ts @@ -1,6 +1,7 @@ import { Request } from 'express'; -export interface UserInfoRequest extends Request<{}, {}, body> { +export interface UserInfoRequest + extends Request { user: { id: number; email: string; diff --git a/src/typescript/interfaces/Snippet.ts b/src/typescript/interfaces/Snippet.ts index f63f68c..9ad0066 100644 --- a/src/typescript/interfaces/Snippet.ts +++ b/src/typescript/interfaces/Snippet.ts @@ -8,9 +8,9 @@ export interface Snippet extends Model { code: string; docs: string; isPinned: number; - tags?: { name: string }[]; + tags?: string[]; createdBy: number; } export interface SnippetCreationAttributes - extends Optional {} + extends Optional {} diff --git a/src/utils/signToken.ts b/src/utils/signToken.ts index b01ef00..f8f149c 100644 --- a/src/utils/signToken.ts +++ b/src/utils/signToken.ts @@ -5,7 +5,7 @@ export const signToken = (data: Token): string => { const secret = process.env.JWT_SECRET || 'secret'; const token = sign(data, secret, { - expiresIn: '30d' + expiresIn: '14d' }); return token;