Merge pull request #13 from pawelmalak/snippet-tags

Added support for tags
This commit is contained in:
pawelmalak
2021-09-28 14:02:52 +02:00
committed by GitHub
27 changed files with 468 additions and 104 deletions

View File

@@ -1,3 +1,6 @@
### v1.2 (2021-09-28)
- Added support for tags ([#10](https://github.com/pawelmalak/snippet-box/issues/10))
### v1.1 (2021-09-24) ### v1.1 (2021-09-24)
- Added pin icon directly to snippet card ([#4](https://github.com/pawelmalak/snippet-box/issues/4)) - Added pin icon directly to snippet card ([#4](https://github.com/pawelmalak/snippet-box/issues/4))
- Fixed issue with copying snippets ([#6](https://github.com/pawelmalak/snippet-box/issues/6)) - Fixed issue with copying snippets ([#6](https://github.com/pawelmalak/snippet-box/issues/6))

View File

@@ -10,14 +10,8 @@ COPY . .
RUN mkdir -p ./public ./data RUN mkdir -p ./public ./data
# Build server code # Build
RUN npm run build RUN npm run build \
# Build client code
RUN cd ./client \
&& npm install \
&& npm run build \
&& cd .. \
&& mv ./client/build/* ./public && mv ./client/build/* ./public
# Clean up src files # Clean up src files

View File

@@ -12,14 +12,8 @@ COPY . .
RUN mkdir -p ./public ./data RUN mkdir -p ./public ./data
# Build server code # Build
RUN npm run build RUN npm run build \
# Build client code
RUN cd ./client \
&& npm install \
&& npm run build \
&& cd .. \
&& mv ./client/build/* ./public && mv ./client/build/* ./public
# Clean up src files # Clean up src files

View File

@@ -3,7 +3,7 @@ import { useHistory } from 'react-router-dom';
import { SnippetsContext } from '../../store'; import { SnippetsContext } from '../../store';
import { Snippet } from '../../typescript/interfaces'; import { Snippet } from '../../typescript/interfaces';
import { dateParser } from '../../utils'; import { dateParser } from '../../utils';
import { Button, Card } from '../UI'; import { Badge, Button, Card } from '../UI';
import copy from 'clipboard-copy'; import copy from 'clipboard-copy';
import { SnippetPin } from './SnippetPin'; import { SnippetPin } from './SnippetPin';
@@ -15,6 +15,7 @@ export const SnippetDetails = (props: Props): JSX.Element => {
const { const {
title, title,
language, language,
tags,
createdAt, createdAt,
updatedAt, updatedAt,
description, description,
@@ -61,6 +62,16 @@ export const SnippetDetails = (props: Props): JSX.Element => {
</div> </div>
<hr /> <hr />
{/* TAGS */}
<div>
{tags.map((tag, idx) => (
<span className='me-2' key={idx}>
<Badge text={tag} color='dark' />
</span>
))}
</div>
<hr />
{/* ACTIONS */} {/* ACTIONS */}
<div className='d-grid g-2' style={{ rowGap: '10px' }}> <div className='d-grid g-2' style={{ rowGap: '10px' }}>
<Button <Button

View File

@@ -25,7 +25,8 @@ export const SnippetForm = (props: Props): JSX.Element => {
language: '', language: '',
code: '', code: '',
docs: '', docs: '',
isPinned: false isPinned: false,
tags: []
}); });
useEffect(() => { useEffect(() => {
@@ -45,6 +46,18 @@ export const SnippetForm = (props: Props): JSX.Element => {
}); });
}; };
const stringToTags = (e: ChangeEvent<HTMLInputElement>) => {
const tags = e.target.value.split(',');
setFormData({
...formData,
tags
});
};
const tagsToString = (): string => {
return formData.tags.join(',');
};
const formHandler = (e: FormEvent) => { const formHandler = (e: FormEvent) => {
e.preventDefault(); e.preventDefault();
@@ -109,11 +122,31 @@ export const SnippetForm = (props: Props): JSX.Element => {
id='language' id='language'
name='language' name='language'
value={formData.language} value={formData.language}
placeholder='bash' placeholder='python'
required required
onChange={e => inputHandler(e)} onChange={e => inputHandler(e)}
/> />
</div> </div>
{/* TAGS */}
<div className='mb-3'>
<label htmlFor='tags' className='form-label'>
Tags
</label>
<input
type='text'
className='form-control'
id='tags'
name='tags'
value={tagsToString()}
placeholder='automation, files, loop'
onChange={e => stringToTags(e)}
/>
<div className='form-text'>
Tags should be separated with a comma. Language tag will be
added automatically
</div>
</div>
<hr /> <hr />
{/* CODE SECTION */} {/* CODE SECTION */}

View File

@@ -5,7 +5,7 @@ import { Button, Card, EmptyState, Layout } from '../components/UI';
import { Snippet } from '../typescript/interfaces'; import { Snippet } from '../typescript/interfaces';
export const Snippets = (): JSX.Element => { export const Snippets = (): JSX.Element => {
const { snippets, languageCount, getSnippets, countSnippets } = const { snippets, tagCount, getSnippets, countTags } =
useContext(SnippetsContext); useContext(SnippetsContext);
const [filter, setFilter] = useState<string | null>(null); const [filter, setFilter] = useState<string | null>(null);
@@ -13,16 +13,16 @@ export const Snippets = (): JSX.Element => {
useEffect(() => { useEffect(() => {
getSnippets(); getSnippets();
countSnippets(); countTags();
}, []); }, []);
useEffect(() => { useEffect(() => {
setLocalSnippets([...snippets]); setLocalSnippets([...snippets]);
}, [snippets]); }, [snippets]);
const filterHandler = (language: string) => { const filterHandler = (tag: string) => {
setFilter(language); setFilter(tag);
const filteredSnippets = snippets.filter(s => s.language === language); const filteredSnippets = snippets.filter(s => s.tags.includes(tag));
setLocalSnippets(filteredSnippets); setLocalSnippets(filteredSnippets);
}; };
@@ -44,21 +44,21 @@ export const Snippets = (): JSX.Element => {
<span>Total</span> <span>Total</span>
<span>{snippets.length}</span> <span>{snippets.length}</span>
</div> </div>
<h5 className='card-title'>Filter by language</h5> <h5 className='card-title'>Filter by tags</h5>
<Fragment> <Fragment>
{languageCount.map((el, idx) => { {tagCount.map((tag, idx) => {
const isActiveFilter = filter === el.language; const isActiveFilter = filter === tag.name;
return ( return (
<div <div
key={idx}
className={`d-flex justify-content-between cursor-pointer ${ className={`d-flex justify-content-between cursor-pointer ${
isActiveFilter && 'text-dark fw-bold' isActiveFilter && 'text-dark fw-bold'
}`} }`}
key={idx} onClick={() => filterHandler(tag.name)}
onClick={() => filterHandler(el.language)}
> >
<span>{el.language}</span> <span>{tag.name}</span>
<span>{el.count}</span> <span>{tag.count}</span>
</div> </div>
); );
})} })}

View File

@@ -5,14 +5,14 @@ import {
Context, Context,
Snippet, Snippet,
Response, Response,
LanguageCount, TagCount,
NewSnippet NewSnippet
} from '../typescript/interfaces'; } from '../typescript/interfaces';
export const SnippetsContext = createContext<Context>({ export const SnippetsContext = createContext<Context>({
snippets: [], snippets: [],
currentSnippet: null, currentSnippet: null,
languageCount: [], tagCount: [],
getSnippets: () => {}, getSnippets: () => {},
getSnippetById: (id: number) => {}, getSnippetById: (id: number) => {},
setSnippet: (id: number) => {}, setSnippet: (id: number) => {},
@@ -20,7 +20,7 @@ export const SnippetsContext = createContext<Context>({
updateSnippet: (snippet: NewSnippet, id: number, isLocal?: boolean) => {}, updateSnippet: (snippet: NewSnippet, id: number, isLocal?: boolean) => {},
deleteSnippet: (id: number) => {}, deleteSnippet: (id: number) => {},
toggleSnippetPin: (id: number) => {}, toggleSnippetPin: (id: number) => {},
countSnippets: () => {} countTags: () => {}
}); });
interface Props { interface Props {
@@ -30,7 +30,7 @@ interface Props {
export const SnippetsContextProvider = (props: Props): JSX.Element => { export const SnippetsContextProvider = (props: Props): JSX.Element => {
const [snippets, setSnippets] = useState<Snippet[]>([]); const [snippets, setSnippets] = useState<Snippet[]>([]);
const [currentSnippet, setCurrentSnippet] = useState<Snippet | null>(null); const [currentSnippet, setCurrentSnippet] = useState<Snippet | null>(null);
const [languageCount, setLanguageCount] = useState<LanguageCount[]>([]); const [tagCount, setTagCount] = useState<TagCount[]>([]);
const history = useHistory(); const history = useHistory();
@@ -53,13 +53,13 @@ export const SnippetsContextProvider = (props: Props): JSX.Element => {
}; };
const setSnippet = (id: number): void => { const setSnippet = (id: number): void => {
getSnippetById(id);
if (id < 0) { if (id < 0) {
setCurrentSnippet(null); setCurrentSnippet(null);
return; return;
} }
getSnippetById(id);
const snippet = snippets.find(s => s.id === id); const snippet = snippets.find(s => s.id === id);
if (snippet) { if (snippet) {
@@ -132,17 +132,17 @@ export const SnippetsContextProvider = (props: Props): JSX.Element => {
} }
}; };
const countSnippets = (): void => { const countTags = (): void => {
axios axios
.get<Response<LanguageCount[]>>('/api/snippets/statistics/count') .get<Response<TagCount[]>>('/api/snippets/statistics/count')
.then(res => setLanguageCount(res.data.data)) .then(res => setTagCount(res.data.data))
.catch(err => redirectOnError()); .catch(err => redirectOnError());
}; };
const context = { const context = {
snippets, snippets,
currentSnippet, currentSnippet,
languageCount, tagCount,
getSnippets, getSnippets,
getSnippetById, getSnippetById,
setSnippet, setSnippet,
@@ -150,7 +150,7 @@ export const SnippetsContextProvider = (props: Props): JSX.Element => {
updateSnippet, updateSnippet,
deleteSnippet, deleteSnippet,
toggleSnippetPin, toggleSnippetPin,
countSnippets countTags
}; };
return ( return (

View File

@@ -1,9 +1,9 @@
import { LanguageCount, NewSnippet, Snippet } from '.'; import { TagCount, NewSnippet, Snippet } from '.';
export interface Context { export interface Context {
snippets: Snippet[]; snippets: Snippet[];
currentSnippet: Snippet | null; currentSnippet: Snippet | null;
languageCount: LanguageCount[]; tagCount: TagCount[];
getSnippets: () => void; getSnippets: () => void;
getSnippetById: (id: number) => void; getSnippetById: (id: number) => void;
setSnippet: (id: number) => void; setSnippet: (id: number) => void;
@@ -11,5 +11,5 @@ export interface Context {
updateSnippet: (snippet: NewSnippet, id: number, isLocal?: boolean) => void; updateSnippet: (snippet: NewSnippet, id: number, isLocal?: boolean) => void;
deleteSnippet: (id: number) => void; deleteSnippet: (id: number) => void;
toggleSnippetPin: (id: number) => void; toggleSnippetPin: (id: number) => void;
countSnippets: () => void; countTags: () => void;
} }

View File

@@ -7,6 +7,7 @@ export interface NewSnippet {
code: string; code: string;
docs?: string; docs?: string;
isPinned: boolean; isPinned: boolean;
tags: string[];
} }
export interface Snippet extends Model, NewSnippet {} export interface Snippet extends Model, NewSnippet {}

View File

@@ -1,4 +1,4 @@
export interface LanguageCount { export interface TagCount {
count: number; count: number;
language: string; name: string;
} }

View File

@@ -10,6 +10,7 @@
"dev:client": "npm start --prefix=client", "dev:client": "npm start --prefix=client",
"dev:server": "nodemon", "dev:server": "nodemon",
"dev": "npm-run-all -n --parallel dev:**", "dev": "npm-run-all -n --parallel dev:**",
"build:client": "npm run build --prefix=client",
"build:clear": "rm -rf build", "build:clear": "rm -rf build",
"build:tsc": "tsc", "build:tsc": "tsc",
"build": "npm-run-all -n build:**" "build": "npm-run-all -n build:**"

View File

@@ -2,8 +2,17 @@ import { Request, Response, NextFunction } from 'express';
import { QueryTypes } from 'sequelize'; import { QueryTypes } from 'sequelize';
import { sequelize } from '../db'; import { sequelize } from '../db';
import { asyncWrapper } from '../middleware'; import { asyncWrapper } from '../middleware';
import { SnippetModel } from '../models'; import { SnippetModel, Snippet_TagModel } from '../models';
import { ErrorResponse } from '../utils'; import {
ErrorResponse,
getTags,
tagParser,
Logger,
createTags
} from '../utils';
import { Body } from '../typescript/interfaces';
const logger = new Logger('snippets-controller');
/** /**
* @description Create new snippet * @description Create new snippet
@@ -12,10 +21,30 @@ import { ErrorResponse } from '../utils';
*/ */
export const createSnippet = asyncWrapper( export const createSnippet = asyncWrapper(
async (req: Request, res: Response, next: NextFunction): Promise<void> => { async (req: Request, res: Response, next: NextFunction): Promise<void> => {
const snippet = await SnippetModel.create(req.body); // 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({ res.status(201).json({
data: snippet data: {
...rawSnippet,
tags: [...parsedRequestTags]
}
}); });
} }
); );
@@ -27,7 +56,22 @@ export const createSnippet = asyncWrapper(
*/ */
export const getAllSnippets = asyncWrapper( export const getAllSnippets = asyncWrapper(
async (req: Request, res: Response, next: NextFunction): Promise<void> => { async (req: Request, res: Response, next: NextFunction): Promise<void> => {
const snippets = await SnippetModel.findAll(); const snippets = await SnippetModel.findAll({
raw: true
});
await new Promise<void>(async resolve => {
try {
for await (let snippet of snippets) {
const tags = await getTags(+snippet.id);
snippet.tags = tags;
}
} catch (err) {
logger.log('Error while fetching tags', 'ERROR');
} finally {
resolve();
}
});
res.status(200).json({ res.status(200).json({
data: snippets data: snippets
@@ -36,14 +80,15 @@ export const getAllSnippets = asyncWrapper(
); );
/** /**
* @description Get single sinppet by id * @description Get single snippet by id
* @route /api/snippets/:id * @route /api/snippets/:id
* @request GET * @request GET
*/ */
export const getSnippet = asyncWrapper( export const getSnippet = asyncWrapper(
async (req: Request, res: Response, next: NextFunction): Promise<void> => { async (req: Request, res: Response, next: NextFunction): Promise<void> => {
const snippet = await SnippetModel.findOne({ const snippet = await SnippetModel.findOne({
where: { id: req.params.id } where: { id: req.params.id },
raw: true
}); });
if (!snippet) { if (!snippet) {
@@ -55,6 +100,9 @@ export const getSnippet = asyncWrapper(
); );
} }
const tags = await getTags(+req.params.id);
snippet.tags = tags;
res.status(200).json({ res.status(200).json({
data: snippet data: snippet
}); });
@@ -81,10 +129,28 @@ export const updateSnippet = asyncWrapper(
); );
} }
snippet = await snippet.update(req.body); // 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({ res.status(200).json({
data: snippet data: {
...rawSnippet,
tags: [...parsedRequestTags]
}
}); });
} }
); );
@@ -109,6 +175,7 @@ export const deleteSnippet = asyncWrapper(
); );
} }
await Snippet_TagModel.destroy({ where: { snippet_id: req.params.id } });
await snippet.destroy(); await snippet.destroy();
res.status(200).json({ res.status(200).json({
@@ -118,19 +185,20 @@ export const deleteSnippet = asyncWrapper(
); );
/** /**
* @description Count snippets by language * @description Count tags
* @route /api/snippets/statistics/count * @route /api/snippets/statistics/count
* @request GET * @request GET
*/ */
export const countSnippets = asyncWrapper( export const countTags = asyncWrapper(
async (req: Request, res: Response, next: NextFunction): Promise<void> => { async (req: Request, res: Response, next: NextFunction): Promise<void> => {
const result = await sequelize.query( const result = await sequelize.query(
`SELECT `SELECT
COUNT(language) AS count, COUNT(tags.name) as count,
language tags.name
FROM snippets FROM snippets_tags
GROUP BY language INNER JOIN tags ON snippets_tags.tag_id = tags.id
ORDER BY language ASC`, GROUP BY tags.name
ORDER BY name ASC`,
{ {
type: QueryTypes.SELECT type: QueryTypes.SELECT
} }

View File

@@ -0,0 +1,92 @@
import { Logger } from '../../utils';
import { DataTypes, QueryInterface } from 'sequelize';
import {
SnippetModel,
Snippet_TagModel,
TagInstance,
TagModel
} from '../../models';
const { STRING, INTEGER } = DataTypes;
const logger = new Logger('migration[02]');
export const up = async (queryInterface: QueryInterface): Promise<void> => {
await queryInterface.createTable('tags', {
id: {
type: INTEGER,
allowNull: false,
primaryKey: true,
autoIncrement: true
},
name: {
type: STRING,
allowNull: false,
unique: true
}
});
await queryInterface.createTable('snippets_tags', {
id: {
type: INTEGER,
allowNull: false,
primaryKey: true,
autoIncrement: true
},
snippet_id: {
type: INTEGER,
allowNull: false
},
tag_id: {
type: INTEGER,
allowNull: false
}
});
// Create new tags from language column
const snippets = await SnippetModel.findAll();
const languages = snippets.map(snippet => snippet.language);
const uniqueLanguages = [...new Set(languages)];
const tags: TagInstance[] = [];
if (snippets.length > 0) {
await new Promise<void>(resolve => {
uniqueLanguages.forEach(async language => {
try {
const tag = await TagModel.create({ name: language });
tags.push(tag);
} catch (err) {
logger.log('Error while creating new tags');
} finally {
if (uniqueLanguages.length == tags.length) {
resolve();
}
}
});
});
// Assign tag to snippet
await new Promise<void>(resolve => {
snippets.forEach(async snippet => {
try {
const tag = tags.find(tag => tag.name == snippet.language);
if (tag) {
await Snippet_TagModel.create({
snippet_id: snippet.id,
tag_id: tag.id
});
}
} catch (err) {
logger.log('Error while assigning tags to snippets');
} finally {
resolve();
}
});
});
}
};
export const down = async (queryInterface: QueryInterface): Promise<void> => {
await queryInterface.dropTable('tags');
await queryInterface.dropTable('snippets_tags');
};

View File

@@ -4,11 +4,13 @@ import { Snippet, SnippetCreationAttributes } from '../typescript/interfaces';
const { INTEGER, STRING, DATE, TEXT } = DataTypes; const { INTEGER, STRING, DATE, TEXT } = DataTypes;
interface SnippetInstance export interface SnippetInstance
extends Model<Snippet, SnippetCreationAttributes>, extends Model<Snippet, SnippetCreationAttributes>,
Snippet {} Snippet {}
export const SnippetModel = sequelize.define<SnippetInstance>('Snippet', { export const SnippetModel = sequelize.define<SnippetInstance>(
'Snippet',
{
id: { id: {
type: INTEGER, type: INTEGER,
primaryKey: true, primaryKey: true,
@@ -47,4 +49,8 @@ export const SnippetModel = sequelize.define<SnippetInstance>('Snippet', {
updatedAt: { updatedAt: {
type: DATE type: DATE
} }
}); },
{
tableName: 'snippets'
}
);

35
src/models/Snippet_Tag.ts Normal file
View File

@@ -0,0 +1,35 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../db';
import {
Snippet_Tag,
Snippet_TagCreationAttributes
} from '../typescript/interfaces';
const { INTEGER } = DataTypes;
export interface Snippet_TagInstance
extends Model<Snippet_Tag, Snippet_TagCreationAttributes>,
Snippet_Tag {}
export const Snippet_TagModel = sequelize.define<Snippet_TagInstance>(
'Snippet_Tag',
{
id: {
type: INTEGER,
primaryKey: true,
autoIncrement: true
},
snippet_id: {
type: INTEGER,
allowNull: false
},
tag_id: {
type: INTEGER,
allowNull: false
}
},
{
timestamps: false,
tableName: 'snippets_tags'
}
);

27
src/models/Tag.ts Normal file
View File

@@ -0,0 +1,27 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../db';
import { Tag, TagCreationAttributes } from '../typescript/interfaces';
const { INTEGER, STRING } = DataTypes;
export interface TagInstance extends Model<Tag, TagCreationAttributes>, Tag {}
export const TagModel = sequelize.define<TagInstance>(
'Tag',
{
id: {
type: INTEGER,
primaryKey: true,
autoIncrement: true
},
name: {
type: STRING,
allowNull: false,
unique: true
}
},
{
timestamps: false,
tableName: 'tags'
}
);

View File

@@ -1 +1,3 @@
export * from './Snippet'; export * from './Snippet';
export * from './Tag';
export * from './Snippet_Tag';

View File

@@ -1,6 +1,6 @@
import { Router } from 'express'; import { Router } from 'express';
import { import {
countSnippets, countTags,
createSnippet, createSnippet,
deleteSnippet, deleteSnippet,
getAllSnippets, getAllSnippets,
@@ -22,4 +22,4 @@ snippetRouter
.put(updateSnippet) .put(updateSnippet)
.delete(deleteSnippet); .delete(deleteSnippet);
snippetRouter.route('/statistics/count').get(countSnippets); snippetRouter.route('/statistics/count').get(countTags);

View File

@@ -0,0 +1,9 @@
export interface Body {
title: string;
description?: string;
language: string;
code: string;
docs?: string;
isPinned: boolean;
tags: string[];
}

View File

@@ -8,6 +8,7 @@ export interface Snippet extends Model {
code: string; code: string;
docs: string; docs: string;
isPinned: number; isPinned: number;
tags?: string[];
} }
export interface SnippetCreationAttributes export interface SnippetCreationAttributes

View File

@@ -0,0 +1,10 @@
import { Optional } from 'sequelize';
export interface Snippet_Tag {
id: number;
snippet_id: number;
tag_id: number;
}
export interface Snippet_TagCreationAttributes
extends Optional<Snippet_Tag, 'id'> {}

View File

@@ -0,0 +1,8 @@
import { Optional } from 'sequelize';
export interface Tag {
id: number;
name: string;
}
export interface TagCreationAttributes extends Optional<Tag, 'id'> {}

View File

@@ -1,2 +1,5 @@
export * from './Model'; export * from './Model';
export * from './Snippet'; export * from './Snippet';
export * from './Tag';
export * from './Snippet_Tag';
export * from './Body';

39
src/utils/createTags.ts Normal file
View File

@@ -0,0 +1,39 @@
import { sequelize } from '../db';
import { QueryTypes } from 'sequelize';
import { TagModel, Snippet_TagModel } from '../models';
export const createTags = async (
parsedTags: Set<string>,
snippetId: number
): Promise<void> => {
// Get all tags
const rawAllTags = await sequelize.query<{ id: number; name: string }>(
`SELECT * FROM tags`,
{ type: QueryTypes.SELECT }
);
const parsedAllTags = rawAllTags.map(tag => tag.name);
// Create array of new tags
const newTags = [...parsedTags].filter(tag => !parsedAllTags.includes(tag));
// Create new tags
if (newTags.length > 0) {
for (const tag of newTags) {
const { id, name } = await TagModel.create({ name: tag });
rawAllTags.push({ id, name });
}
}
// Associate tags with snippet
for (const tag of parsedTags) {
const tagObj = rawAllTags.find(t => t.name == tag);
if (tagObj) {
await Snippet_TagModel.create({
snippet_id: snippetId,
tag_id: tagObj.id
});
}
}
};

18
src/utils/getTags.ts Normal file
View File

@@ -0,0 +1,18 @@
import { sequelize } from '../db';
import { QueryTypes } from 'sequelize';
export const getTags = async (snippetId: number): Promise<string[]> => {
const tags = await sequelize.query<{ name: string }>(
`SELECT tags.name
FROM tags
INNER JOIN
snippets_tags ON tags.id = snippets_tags.tag_id
INNER JOIN
snippets ON snippets.id = snippets_tags.snippet_id
WHERE
snippets_tags.snippet_id = ${snippetId};`,
{ type: QueryTypes.SELECT }
);
return tags.map(tag => tag.name);
};

View File

@@ -1,2 +1,5 @@
export * from './Logger'; export * from './Logger';
export * from './ErrorResponse'; export * from './ErrorResponse';
export * from './tagParser';
export * from './getTags';
export * from './createTags';

6
src/utils/tagParser.ts Normal file
View File

@@ -0,0 +1,6 @@
export const tagParser = (tags: string[]): Set<string> => {
const parsedTags = tags.map(tag => tag.trim().toLowerCase()).filter(String);
const uniqueTags = new Set([...parsedTags]);
return uniqueTags;
};