Split snippet controllers into separate files

This commit is contained in:
Paweł Malak
2021-10-21 11:43:05 +02:00
parent a1abee546a
commit f55cbc73d8
14 changed files with 446 additions and 312 deletions

View File

@@ -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<void> => {
// Get tags from request body
const { language, tags: requestTags } = <Body>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<void> => {
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<void> => {
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<void> => {
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 } = <Body>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<void> => {
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<void> => {
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<void> => {
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<void> => {
const { query, tags, languages } = <SearchQuery>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
});
}
);

View File

@@ -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<void> => {
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
});
}
);

View File

@@ -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<RequestBody>,
res: Response,
next: NextFunction
): Promise<void> => {
// 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]
}
});
}
);

View File

@@ -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<void> => {
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: {}
});
}
);

View File

@@ -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<void> => {
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
});
}
);

View File

@@ -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<Params>,
res: Response,
next: NextFunction
): Promise<void> => {
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);
}
);

View File

@@ -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<void> => {
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
});
}
}
);

View File

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

View File

@@ -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<void> => {
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
});
}
);

View File

@@ -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<Body, Params>,
res: Response,
next: NextFunction
): Promise<void> => {
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]
}
});
}
);

View File

@@ -1,28 +1,33 @@
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);

View File

@@ -1,6 +1,7 @@
import { Request } from 'express';
export interface UserInfoRequest<body = {}> extends Request<{}, {}, body> {
export interface UserInfoRequest<body = {}, params = {}>
extends Request<params, {}, body> {
user: {
id: number;
email: string;

View File

@@ -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<Snippet, 'id' | 'createdAt' | 'updatedAt'> {}
extends Optional<Snippet, 'id' | 'createdAt' | 'updatedAt' | 'tags'> {}

View File

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