diff --git a/README.md b/README.md index 9b76601..bfa01b2 100644 --- a/README.md +++ b/README.md @@ -17,3 +17,69 @@ Start project: ```sh npm run start ``` + +## Usage + +You can use Postman or Curl to make calls to the API. The examples below use Curl. + +### Create a topic: + +```sh +curl -X POST http://localhost:3000/api/topics \ + -H "Content-Type: application/json" \ + -d '{"name": "A topic", "content": "Topic content"}' +``` + +### Get all topics: + +```sh +curl -X GET http://localhost:3000/api/topics +``` + +### Get specific topic: + +```sh +curl -X GET http://localhost:3000/api/topics/1234567890 +``` + +> Replace `1234567890` with the topic id. + +### Update a topic: + +```sh +curl -X PUT http://localhost:3000/api/topics/1234567890 \ + -H "Content-Type: application/json" \ + -d '{"name": "Updated topic"}' +``` + +> Replace `1234567890` with the topic id. + +### Get specific version of a topic: + +```sh +curl -X GET http://localhost:3000/api/topics/1234567890/1 +``` + +### Create a child topic: + +```sh +curl -X POST http://localhost:3000/api/topics \ + -H "Content-Type: application/json" \ + -d '{"name": "A topic", "content": "Topic content", "parentTopicId": 1234567890 }' +``` + +### Delete a topic: + +```sh +curl -X DELETE http://localhost:3000/api/topics/1234567890 +``` + +> Replace `1234567890` with the topic id. + +## Tests + +Run tests with: + +```sh +npm run test +``` diff --git a/src/controllers/topicController.ts b/src/controllers/topicController.ts index 84c28a5..d361f45 100644 --- a/src/controllers/topicController.ts +++ b/src/controllers/topicController.ts @@ -1,5 +1,6 @@ import { Request, Response, NextFunction } from 'express'; import { topics, Topic } from '../models/topic'; +import { finder } from '../utils' // Create a topic export const createTopic = (req: Request, res: Response, next: NextFunction) => { @@ -24,7 +25,7 @@ export const createTopic = (req: Request, res: Response, next: NextFunction) => } }; -// Read all topics +// Retrieve all topics export const getTopics = (req: Request, res: Response, next: NextFunction) => { try { res.json(topics); @@ -33,7 +34,7 @@ export const getTopics = (req: Request, res: Response, next: NextFunction) => { } }; -// Read single topic +// Retrieve single topic export const getTopicById = (req: Request, res: Response, next: NextFunction) => { try { const id = parseInt(req.params.id, 10); @@ -48,6 +49,39 @@ export const getTopicById = (req: Request, res: Response, next: NextFunction) => } }; +// Retrieve single version of a topic +export const getTopicByIdVersion = (req: Request, res: Response, next: NextFunction) => { + try { + const id = parseInt(req.params.id, 10); + const version = parseInt(req.params.version, 10) + const topic = topics.find((i) => i.id === id && i.version === version); + if (!topic) { + res.status(404).json({ message: 'Topic not found' }); + return; + } + res.json(topic); + } catch (error) { + next(error); + } +}; + +// Retrive topic and all subtopics recursively +export const getTopicByIdRecursive = (req: Request, res: Response, next: NextFunction) => { + try { + const id = parseInt(req.params.id, 10); + const topic = topics.find((i) => i.id === id); + if (!topic) { + res.status(404).json({ message: 'Topic not found' }); + return; + } + const result = finder(topic.id) + + res.json(result); + } catch (error) { + next(error); + } +}; + // Update a topic (create nenw version) export const updateTopic = (req: Request, res: Response, next: NextFunction) => { try { diff --git a/src/routes/topicRoutes.ts b/src/routes/topicRoutes.ts index 9d9c1d4..ffad0ee 100644 --- a/src/routes/topicRoutes.ts +++ b/src/routes/topicRoutes.ts @@ -3,14 +3,18 @@ import { createTopic, getTopics, getTopicById, + getTopicByIdVersion, + getTopicByIdRecursive, updateTopic, - deleteTopic, + deleteTopic } from '../controllers/topicController'; const router = Router(); -router.get('/', getTopics); +router.get('/recursive/:id', getTopicByIdRecursive); router.get('/:id', getTopicById); +router.get('/:id/:version', getTopicByIdVersion); +router.get('/', getTopics); router.post('/', createTopic); router.put('/:id', updateTopic); router.delete('/:id', deleteTopic); diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..e346873 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,33 @@ +import { topics, Topic } from './models/topic' + +class TopicNode { + node: Topic + childNode?: [TopicNode?] + + constructor(node: Topic, childNode: TopicNode) { + this.node = node + this.childNode = childNode ? [childNode] : [] + } +} + +// Search all topics' children and return them as a tree +export const finder = (id: number) => { + const recursive = (topics: Topic[], id: number, node?: TopicNode) => { + const topic = topics.find((i) => i.id === id) + if (!node) { + node = new TopicNode(topic, null) + } + const topicChildren = topics.filter((i) => i.parentTopicId === id) + + topicChildren.forEach(t => { + const turn = recursive(topics, t.id) + node.childNode.push(turn) + }) + + return (node) + } + + const result = recursive(topics, id) + + return (result) +} diff --git a/tests/mocks.ts b/tests/mocks.ts new file mode 100644 index 0000000..f3f0733 --- /dev/null +++ b/tests/mocks.ts @@ -0,0 +1,101 @@ +export interface Topic { + id: number + name: string + content: string + createdAt: string + updatedAt: string + version: number + parentTopicId?: number +} + +export const topicMocks: Topic[] = [ + { + "id": 1743738847018, + "name": "First topic", + "content": "Topic content", + "createdAt": "Fri Apr 04 2025 00:54:07 GMT-0300 (Brasilia Standard Time)", + "updatedAt": "Fri Apr 04 2025 00:54:07 GMT-0300 (Brasilia Standard Time)", + "version": 1 + }, + { + "id": 1743738900320, + "name": "Second topic", + "content": "Topic content", + "createdAt": "Fri Apr 04 2025 00:55:00 GMT-0300 (Brasilia Standard Time)", + "updatedAt": "Fri Apr 04 2025 00:55:00 GMT-0300 (Brasilia Standard Time)", + "version": 1 + }, + { + "id": 1743738900599, + "name": "Third topic", + "content": "Topic content", + "createdAt": "Fri Apr 04 2025 00:55:00 GMT-0300 (Brasilia Standard Time)", + "updatedAt": "Fri Apr 04 2025 00:55:00 GMT-0300 (Brasilia Standard Time)", + "version": 1, + "parentTopicId": 1743738900320 + }, + { + "id": 1743738900800, + "name": "Fourth topic", + "content": "Topic content", + "createdAt": "Fri Apr 04 2025 00:55:00 GMT-0300 (Brasilia Standard Time)", + "updatedAt": "Fri Apr 04 2025 00:55:00 GMT-0300 (Brasilia Standard Time)", + "version": 1, + "parentTopicId": 1743738900599 + }, + { + "id": 1743739100103, + "name": "Fifth topic", + "content": "Topic content", + "createdAt": "Fri Apr 04 2025 00:55:00 GMT-0300 (Brasilia Standard Time)", + "updatedAt": "Fri Apr 04 2025 00:55:00 GMT-0300 (Brasilia Standard Time)", + "version": 1, + "parentTopicId": 1743738900320 + } +]; + +export const treeMock = [ + { + id: 1743738847018, + name: 'First topic', + content: 'Topic content', + createdAt: 'Fri Apr 04 2025 00:54:07 GMT-0300 (Brasilia Standard Time)', + updatedAt: 'Fri Apr 04 2025 00:54:07 GMT-0300 (Brasilia Standard Time)', + version: 1 + }, + { + id: 1743738900320, + name: 'Second topic', + content: 'Topic content', + createdAt: 'Fri Apr 04 2025 00:55:00 GMT-0300 (Brasilia Standard Time)', + updatedAt: 'Fri Apr 04 2025 00:55:00 GMT-0300 (Brasilia Standard Time)', + version: 1 + }, + { + id: 1743738900599, + name: 'Third topic', + content: 'Topic content', + createdAt: 'Fri Apr 04 2025 00:55:00 GMT-0300 (Brasilia Standard Time)', + updatedAt: 'Fri Apr 04 2025 00:55:00 GMT-0300 (Brasilia Standard Time)', + version: 1, + parentTopicId: 1743738900320 + }, + { + id: 1743738900800, + name: 'Fourth topic', + content: 'Topic content', + createdAt: 'Fri Apr 04 2025 00:55:00 GMT-0300 (Brasilia Standard Time)', + updatedAt: 'Fri Apr 04 2025 00:55:00 GMT-0300 (Brasilia Standard Time)', + version: 1, + parentTopicId: 1743738900599 + }, + { + id: 1743739100103, + name: 'Fifth topic', + content: 'Topic content', + createdAt: 'Fri Apr 04 2025 00:55:00 GMT-0300 (Brasilia Standard Time)', + updatedAt: 'Fri Apr 04 2025 00:55:00 GMT-0300 (Brasilia Standard Time)', + version: 1, + parentTopicId: 1743738900320 + } +] diff --git a/tests/topicController.test.ts b/tests/topicController.test.ts index 378ed4c..360bdb6 100644 --- a/tests/topicController.test.ts +++ b/tests/topicController.test.ts @@ -1,6 +1,7 @@ import { Request, Response } from 'express'; -import { getTopics } from '../src/controllers/topicController'; +import { getTopics, getTopicByIdRecursive } from '../src/controllers/topicController'; import { topics } from '../src/models/topic'; +import { topicMocks, treeMock } from './mocks' describe('Topic Controller', () => { it('should return an empty array when no topics exist', () => { @@ -19,4 +20,21 @@ describe('Topic Controller', () => { // Expect that res.json was called with an empty array expect(res.json).toHaveBeenCalledWith([]); }); + + it('should return a recursive tree of a topic and its children', () => { + // Create mock objects for Request, Response, and NextFunction + const req = {} as Request; + const res = { + json: topicMocks + } as unknown as Response; + + // Load mock values into in-memory store + topicMocks.forEach(t => topics.push(t)) + + // Retrieve recursive tree + req.params = { id: topicMocks[1].id.toString() } + getTopicByIdRecursive(req, res, jest.fn()) + + expect(res.json).toMatchObject(treeMock) + }) });