diff --git a/CHANGELOG.md b/CHANGELOG.md
index a172248..0188a0e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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)
- 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))
diff --git a/Dockerfile b/Dockerfile
index d16175b..53f5176 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -10,14 +10,8 @@ COPY . .
RUN mkdir -p ./public ./data
-# Build server code
-RUN npm run build
-
-# Build client code
-RUN cd ./client \
- && npm install \
- && npm run build \
- && cd .. \
+# Build
+RUN npm run build \
&& mv ./client/build/* ./public
# Clean up src files
diff --git a/Dockerfile.arm b/Dockerfile.arm
index 5b3067d..244b0dd 100644
--- a/Dockerfile.arm
+++ b/Dockerfile.arm
@@ -12,14 +12,8 @@ COPY . .
RUN mkdir -p ./public ./data
-# Build server code
-RUN npm run build
-
-# Build client code
-RUN cd ./client \
- && npm install \
- && npm run build \
- && cd .. \
+# Build
+RUN npm run build \
&& mv ./client/build/* ./public
# Clean up src files
diff --git a/client/src/components/Snippets/SnippetDetails.tsx b/client/src/components/Snippets/SnippetDetails.tsx
index 9de585b..514f343 100644
--- a/client/src/components/Snippets/SnippetDetails.tsx
+++ b/client/src/components/Snippets/SnippetDetails.tsx
@@ -3,7 +3,7 @@ import { useHistory } from 'react-router-dom';
import { SnippetsContext } from '../../store';
import { Snippet } from '../../typescript/interfaces';
import { dateParser } from '../../utils';
-import { Button, Card } from '../UI';
+import { Badge, Button, Card } from '../UI';
import copy from 'clipboard-copy';
import { SnippetPin } from './SnippetPin';
@@ -15,6 +15,7 @@ export const SnippetDetails = (props: Props): JSX.Element => {
const {
title,
language,
+ tags,
createdAt,
updatedAt,
description,
@@ -61,6 +62,16 @@ export const SnippetDetails = (props: Props): JSX.Element => {
+ {/* TAGS */}
+
+ {tags.map((tag, idx) => (
+
+
+
+ ))}
+
+
+
{/* ACTIONS */}
+
+ {/* TAGS */}
+
+
+
stringToTags(e)}
+ />
+
+ Tags should be separated with a comma. Language tag will be
+ added automatically
+
+
{/* CODE SECTION */}
diff --git a/client/src/containers/Snippets.tsx b/client/src/containers/Snippets.tsx
index ffe8e31..72d5299 100644
--- a/client/src/containers/Snippets.tsx
+++ b/client/src/containers/Snippets.tsx
@@ -5,7 +5,7 @@ import { Button, Card, EmptyState, Layout } from '../components/UI';
import { Snippet } from '../typescript/interfaces';
export const Snippets = (): JSX.Element => {
- const { snippets, languageCount, getSnippets, countSnippets } =
+ const { snippets, tagCount, getSnippets, countTags } =
useContext(SnippetsContext);
const [filter, setFilter] = useState(null);
@@ -13,16 +13,16 @@ export const Snippets = (): JSX.Element => {
useEffect(() => {
getSnippets();
- countSnippets();
+ countTags();
}, []);
useEffect(() => {
setLocalSnippets([...snippets]);
}, [snippets]);
- const filterHandler = (language: string) => {
- setFilter(language);
- const filteredSnippets = snippets.filter(s => s.language === language);
+ const filterHandler = (tag: string) => {
+ setFilter(tag);
+ const filteredSnippets = snippets.filter(s => s.tags.includes(tag));
setLocalSnippets(filteredSnippets);
};
@@ -44,21 +44,21 @@ export const Snippets = (): JSX.Element => {
Total
{snippets.length}
- Filter by language
+ Filter by tags
- {languageCount.map((el, idx) => {
- const isActiveFilter = filter === el.language;
+ {tagCount.map((tag, idx) => {
+ const isActiveFilter = filter === tag.name;
return (
filterHandler(el.language)}
+ onClick={() => filterHandler(tag.name)}
>
- {el.language}
- {el.count}
+ {tag.name}
+ {tag.count}
);
})}
diff --git a/client/src/store/SnippetsContext.tsx b/client/src/store/SnippetsContext.tsx
index f73d7ee..b180f71 100644
--- a/client/src/store/SnippetsContext.tsx
+++ b/client/src/store/SnippetsContext.tsx
@@ -5,14 +5,14 @@ import {
Context,
Snippet,
Response,
- LanguageCount,
+ TagCount,
NewSnippet
} from '../typescript/interfaces';
export const SnippetsContext = createContext({
snippets: [],
currentSnippet: null,
- languageCount: [],
+ tagCount: [],
getSnippets: () => {},
getSnippetById: (id: number) => {},
setSnippet: (id: number) => {},
@@ -20,7 +20,7 @@ export const SnippetsContext = createContext({
updateSnippet: (snippet: NewSnippet, id: number, isLocal?: boolean) => {},
deleteSnippet: (id: number) => {},
toggleSnippetPin: (id: number) => {},
- countSnippets: () => {}
+ countTags: () => {}
});
interface Props {
@@ -30,7 +30,7 @@ interface Props {
export const SnippetsContextProvider = (props: Props): JSX.Element => {
const [snippets, setSnippets] = useState([]);
const [currentSnippet, setCurrentSnippet] = useState(null);
- const [languageCount, setLanguageCount] = useState([]);
+ const [tagCount, setTagCount] = useState([]);
const history = useHistory();
@@ -53,13 +53,13 @@ export const SnippetsContextProvider = (props: Props): JSX.Element => {
};
const setSnippet = (id: number): void => {
- getSnippetById(id);
-
if (id < 0) {
setCurrentSnippet(null);
return;
}
+ getSnippetById(id);
+
const snippet = snippets.find(s => s.id === id);
if (snippet) {
@@ -132,17 +132,17 @@ export const SnippetsContextProvider = (props: Props): JSX.Element => {
}
};
- const countSnippets = (): void => {
+ const countTags = (): void => {
axios
- .get>('/api/snippets/statistics/count')
- .then(res => setLanguageCount(res.data.data))
+ .get>('/api/snippets/statistics/count')
+ .then(res => setTagCount(res.data.data))
.catch(err => redirectOnError());
};
const context = {
snippets,
currentSnippet,
- languageCount,
+ tagCount,
getSnippets,
getSnippetById,
setSnippet,
@@ -150,7 +150,7 @@ export const SnippetsContextProvider = (props: Props): JSX.Element => {
updateSnippet,
deleteSnippet,
toggleSnippetPin,
- countSnippets
+ countTags
};
return (
diff --git a/client/src/typescript/interfaces/Context.ts b/client/src/typescript/interfaces/Context.ts
index 215674e..6e33734 100644
--- a/client/src/typescript/interfaces/Context.ts
+++ b/client/src/typescript/interfaces/Context.ts
@@ -1,9 +1,9 @@
-import { LanguageCount, NewSnippet, Snippet } from '.';
+import { TagCount, NewSnippet, Snippet } from '.';
export interface Context {
snippets: Snippet[];
currentSnippet: Snippet | null;
- languageCount: LanguageCount[];
+ tagCount: TagCount[];
getSnippets: () => void;
getSnippetById: (id: number) => void;
setSnippet: (id: number) => void;
@@ -11,5 +11,5 @@ export interface Context {
updateSnippet: (snippet: NewSnippet, id: number, isLocal?: boolean) => void;
deleteSnippet: (id: number) => void;
toggleSnippetPin: (id: number) => void;
- countSnippets: () => void;
+ countTags: () => void;
}
diff --git a/client/src/typescript/interfaces/Snippet.ts b/client/src/typescript/interfaces/Snippet.ts
index 99e517d..1973787 100644
--- a/client/src/typescript/interfaces/Snippet.ts
+++ b/client/src/typescript/interfaces/Snippet.ts
@@ -7,6 +7,7 @@ export interface NewSnippet {
code: string;
docs?: string;
isPinned: boolean;
+ tags: string[];
}
export interface Snippet extends Model, NewSnippet {}
diff --git a/client/src/typescript/interfaces/Statistics.ts b/client/src/typescript/interfaces/Statistics.ts
index 2bf242b..80cedcd 100644
--- a/client/src/typescript/interfaces/Statistics.ts
+++ b/client/src/typescript/interfaces/Statistics.ts
@@ -1,4 +1,4 @@
-export interface LanguageCount {
+export interface TagCount {
count: number;
- language: string;
+ name: string;
}
diff --git a/package.json b/package.json
index ead2fb6..0b2d4f3 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
"dev:client": "npm start --prefix=client",
"dev:server": "nodemon",
"dev": "npm-run-all -n --parallel dev:**",
+ "build:client": "npm run build --prefix=client",
"build:clear": "rm -rf build",
"build:tsc": "tsc",
"build": "npm-run-all -n build:**"
diff --git a/src/controllers/snippets.ts b/src/controllers/snippets.ts
index 67a524b..f8c4543 100644
--- a/src/controllers/snippets.ts
+++ b/src/controllers/snippets.ts
@@ -2,8 +2,17 @@ import { Request, Response, NextFunction } from 'express';
import { QueryTypes } from 'sequelize';
import { sequelize } from '../db';
import { asyncWrapper } from '../middleware';
-import { SnippetModel } from '../models';
-import { ErrorResponse } from '../utils';
+import { SnippetModel, Snippet_TagModel } from '../models';
+import {
+ ErrorResponse,
+ getTags,
+ tagParser,
+ Logger,
+ createTags
+} from '../utils';
+import { Body } from '../typescript/interfaces';
+
+const logger = new Logger('snippets-controller');
/**
* @description Create new snippet
@@ -12,10 +21,30 @@ import { ErrorResponse } from '../utils';
*/
export const createSnippet = asyncWrapper(
async (req: Request, res: Response, next: NextFunction): Promise => {
- const snippet = await SnippetModel.create(req.body);
+ // 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: snippet
+ data: {
+ ...rawSnippet,
+ tags: [...parsedRequestTags]
+ }
});
}
);
@@ -27,7 +56,22 @@ export const createSnippet = asyncWrapper(
*/
export const getAllSnippets = asyncWrapper(
async (req: Request, res: Response, next: NextFunction): Promise => {
- const snippets = await SnippetModel.findAll();
+ const snippets = await SnippetModel.findAll({
+ raw: true
+ });
+
+ await new Promise(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({
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
* @request GET
*/
export const getSnippet = asyncWrapper(
async (req: Request, res: Response, next: NextFunction): Promise => {
const snippet = await SnippetModel.findOne({
- where: { id: req.params.id }
+ where: { id: req.params.id },
+ raw: true
});
if (!snippet) {
@@ -55,6 +100,9 @@ export const getSnippet = asyncWrapper(
);
}
+ const tags = await getTags(+req.params.id);
+ snippet.tags = tags;
+
res.status(200).json({
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 } = 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: 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();
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
* @request GET
*/
-export const countSnippets = asyncWrapper(
+export const countTags = asyncWrapper(
async (req: Request, res: Response, next: NextFunction): Promise => {
const result = await sequelize.query(
`SELECT
- COUNT(language) AS count,
- language
- FROM snippets
- GROUP BY language
- ORDER BY language ASC`,
+ 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
}
diff --git a/src/db/migrations/02_tags.ts b/src/db/migrations/02_tags.ts
new file mode 100644
index 0000000..02d09a9
--- /dev/null
+++ b/src/db/migrations/02_tags.ts
@@ -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 => {
+ 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(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(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 => {
+ await queryInterface.dropTable('tags');
+ await queryInterface.dropTable('snippets_tags');
+};
diff --git a/src/models/Snippet.ts b/src/models/Snippet.ts
index a435188..0b23780 100644
--- a/src/models/Snippet.ts
+++ b/src/models/Snippet.ts
@@ -4,47 +4,53 @@ import { Snippet, SnippetCreationAttributes } from '../typescript/interfaces';
const { INTEGER, STRING, DATE, TEXT } = DataTypes;
-interface SnippetInstance
+export interface SnippetInstance
extends Model,
Snippet {}
-export const SnippetModel = sequelize.define('Snippet', {
- id: {
- type: INTEGER,
- primaryKey: true,
- autoIncrement: true
+export const SnippetModel = sequelize.define(
+ 'Snippet',
+ {
+ id: {
+ type: INTEGER,
+ primaryKey: true,
+ autoIncrement: true
+ },
+ title: {
+ type: STRING,
+ allowNull: false
+ },
+ description: {
+ type: TEXT,
+ allowNull: true,
+ defaultValue: ''
+ },
+ language: {
+ type: STRING,
+ allowNull: false
+ },
+ code: {
+ type: TEXT,
+ allowNull: false
+ },
+ docs: {
+ type: TEXT,
+ allowNull: true,
+ defaultValue: ''
+ },
+ isPinned: {
+ type: INTEGER,
+ allowNull: true,
+ defaultValue: 0
+ },
+ createdAt: {
+ type: DATE
+ },
+ updatedAt: {
+ type: DATE
+ }
},
- title: {
- type: STRING,
- allowNull: false
- },
- description: {
- type: TEXT,
- allowNull: true,
- defaultValue: ''
- },
- language: {
- type: STRING,
- allowNull: false
- },
- code: {
- type: TEXT,
- allowNull: false
- },
- docs: {
- type: TEXT,
- allowNull: true,
- defaultValue: ''
- },
- isPinned: {
- type: INTEGER,
- allowNull: true,
- defaultValue: 0
- },
- createdAt: {
- type: DATE
- },
- updatedAt: {
- type: DATE
+ {
+ tableName: 'snippets'
}
-});
+);
diff --git a/src/models/Snippet_Tag.ts b/src/models/Snippet_Tag.ts
new file mode 100644
index 0000000..0eaa25d
--- /dev/null
+++ b/src/models/Snippet_Tag.ts
@@ -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 {}
+
+export const Snippet_TagModel = sequelize.define(
+ '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'
+ }
+);
diff --git a/src/models/Tag.ts b/src/models/Tag.ts
new file mode 100644
index 0000000..759bd06
--- /dev/null
+++ b/src/models/Tag.ts
@@ -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 {}
+
+export const TagModel = sequelize.define(
+ 'Tag',
+ {
+ id: {
+ type: INTEGER,
+ primaryKey: true,
+ autoIncrement: true
+ },
+ name: {
+ type: STRING,
+ allowNull: false,
+ unique: true
+ }
+ },
+ {
+ timestamps: false,
+ tableName: 'tags'
+ }
+);
diff --git a/src/models/index.ts b/src/models/index.ts
index 02fc3fa..44b6eec 100644
--- a/src/models/index.ts
+++ b/src/models/index.ts
@@ -1 +1,3 @@
export * from './Snippet';
+export * from './Tag';
+export * from './Snippet_Tag';
diff --git a/src/routes/snippets.ts b/src/routes/snippets.ts
index 4b3727c..19ca584 100644
--- a/src/routes/snippets.ts
+++ b/src/routes/snippets.ts
@@ -1,6 +1,6 @@
import { Router } from 'express';
import {
- countSnippets,
+ countTags,
createSnippet,
deleteSnippet,
getAllSnippets,
@@ -22,4 +22,4 @@ snippetRouter
.put(updateSnippet)
.delete(deleteSnippet);
-snippetRouter.route('/statistics/count').get(countSnippets);
+snippetRouter.route('/statistics/count').get(countTags);
diff --git a/src/typescript/interfaces/Body.ts b/src/typescript/interfaces/Body.ts
new file mode 100644
index 0000000..6dcf8f7
--- /dev/null
+++ b/src/typescript/interfaces/Body.ts
@@ -0,0 +1,9 @@
+export interface Body {
+ title: string;
+ description?: string;
+ language: string;
+ code: string;
+ docs?: string;
+ isPinned: boolean;
+ tags: string[];
+}
diff --git a/src/typescript/interfaces/Snippet.ts b/src/typescript/interfaces/Snippet.ts
index 785ee09..2a4c637 100644
--- a/src/typescript/interfaces/Snippet.ts
+++ b/src/typescript/interfaces/Snippet.ts
@@ -8,6 +8,7 @@ export interface Snippet extends Model {
code: string;
docs: string;
isPinned: number;
+ tags?: string[];
}
export interface SnippetCreationAttributes
diff --git a/src/typescript/interfaces/Snippet_Tag.ts b/src/typescript/interfaces/Snippet_Tag.ts
new file mode 100644
index 0000000..936a301
--- /dev/null
+++ b/src/typescript/interfaces/Snippet_Tag.ts
@@ -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 {}
diff --git a/src/typescript/interfaces/Tag.ts b/src/typescript/interfaces/Tag.ts
new file mode 100644
index 0000000..923750f
--- /dev/null
+++ b/src/typescript/interfaces/Tag.ts
@@ -0,0 +1,8 @@
+import { Optional } from 'sequelize';
+
+export interface Tag {
+ id: number;
+ name: string;
+}
+
+export interface TagCreationAttributes extends Optional {}
diff --git a/src/typescript/interfaces/index.ts b/src/typescript/interfaces/index.ts
index 063adcb..b86dc4a 100644
--- a/src/typescript/interfaces/index.ts
+++ b/src/typescript/interfaces/index.ts
@@ -1,2 +1,5 @@
export * from './Model';
export * from './Snippet';
+export * from './Tag';
+export * from './Snippet_Tag';
+export * from './Body';
diff --git a/src/utils/createTags.ts b/src/utils/createTags.ts
new file mode 100644
index 0000000..1f53cc4
--- /dev/null
+++ b/src/utils/createTags.ts
@@ -0,0 +1,39 @@
+import { sequelize } from '../db';
+import { QueryTypes } from 'sequelize';
+import { TagModel, Snippet_TagModel } from '../models';
+
+export const createTags = async (
+ parsedTags: Set,
+ snippetId: number
+): Promise => {
+ // 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
+ });
+ }
+ }
+};
diff --git a/src/utils/getTags.ts b/src/utils/getTags.ts
new file mode 100644
index 0000000..f44a1c1
--- /dev/null
+++ b/src/utils/getTags.ts
@@ -0,0 +1,18 @@
+import { sequelize } from '../db';
+import { QueryTypes } from 'sequelize';
+
+export const getTags = async (snippetId: number): Promise => {
+ 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);
+};
diff --git a/src/utils/index.ts b/src/utils/index.ts
index e59e2fd..e9efd2a 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -1,2 +1,5 @@
export * from './Logger';
export * from './ErrorResponse';
+export * from './tagParser';
+export * from './getTags';
+export * from './createTags';
diff --git a/src/utils/tagParser.ts b/src/utils/tagParser.ts
new file mode 100644
index 0000000..9e66bcc
--- /dev/null
+++ b/src/utils/tagParser.ts
@@ -0,0 +1,6 @@
+export const tagParser = (tags: string[]): Set => {
+ const parsedTags = tags.map(tag => tag.trim().toLowerCase()).filter(String);
+ const uniqueTags = new Set([...parsedTags]);
+
+ return uniqueTags;
+};