Basic structure of the project
- Added a model with in-memory database, and routes, controller and middleware to enable making initial CRUD calls to endpoints; - Added jest and an initial test.
This commit is contained in:
parent
74c1ee1b7b
commit
9c4ea4ca90
11 changed files with 3841 additions and 1 deletions
11
jest.config.ts
Normal file
11
jest.config.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
moduleFileExtensions: ['ts', 'js'],
|
||||||
|
testMatch: ['**/tests/**/*.test.(ts|js)'],
|
||||||
|
globals: {
|
||||||
|
'ts-jest': {
|
||||||
|
tsconfig: 'tsconfig.json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
3646
package-lock.json
generated
3646
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -5,8 +5,9 @@
|
||||||
"main": "dist/app.js",
|
"main": "dist/app.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "tsc && node dist/app.js",
|
"start": "tsc && node dist/app.js",
|
||||||
|
"dev": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/app.ts",
|
||||||
"lint": "eslint . --ext .ts",
|
"lint": "eslint . --ext .ts",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "jest --watch"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -23,8 +24,12 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.23.0",
|
"@eslint/js": "^9.23.0",
|
||||||
"@types/express": "^5.0.1",
|
"@types/express": "^5.0.1",
|
||||||
|
"@types/jest": "^29.5.14",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.23.0",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"ts-jest": "^29.3.1",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.8.2",
|
"typescript": "^5.8.2",
|
||||||
"typescript-eslint": "^8.29.0"
|
"typescript-eslint": "^8.29.0"
|
||||||
},
|
},
|
||||||
|
|
11
src/app.ts
11
src/app.ts
|
@ -1,11 +1,22 @@
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
|
import topicRoutes from './routes/topicRoutes'
|
||||||
|
import { errorHandler } from './middleware/errorHandler'
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
const port = 3000
|
const port = 3000
|
||||||
|
|
||||||
|
app.use(express.json())
|
||||||
|
|
||||||
|
// Routes
|
||||||
app.get('/', (req, res) => {
|
app.get('/', (req, res) => {
|
||||||
res.send('Hello World!')
|
res.send('Hello World!')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.use('/api/topics', topicRoutes);
|
||||||
|
|
||||||
|
// Global error handler (should be after routes)
|
||||||
|
app.use(errorHandler);
|
||||||
|
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
return console.log(`Express is listening at http://localhost:${port}`)
|
return console.log(`Express is listening at http://localhost:${port}`)
|
||||||
})
|
})
|
||||||
|
|
79
src/controllers/topicController.ts
Normal file
79
src/controllers/topicController.ts
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { topics, Topic } from '../models/topic';
|
||||||
|
|
||||||
|
// Create a topic
|
||||||
|
export const createTopic = (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { name, content } = req.body;
|
||||||
|
const newTopic: Topic = {
|
||||||
|
id: Date.now(),
|
||||||
|
name,
|
||||||
|
content,
|
||||||
|
createdAt: new Date().toString(),
|
||||||
|
updatedAt: new Date().toString(),
|
||||||
|
version: 1,
|
||||||
|
parentTopicId: 1
|
||||||
|
};
|
||||||
|
topics.push(newTopic);
|
||||||
|
res.status(201).json(newTopic);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read all topics
|
||||||
|
export const getTopics = (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
res.json(topics);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read single topic
|
||||||
|
export const getTopicById = (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;
|
||||||
|
}
|
||||||
|
res.json(topic);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update a topic
|
||||||
|
export const updateTopic = (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const id = parseInt(req.params.id, 10);
|
||||||
|
const { name } = req.body;
|
||||||
|
const topicIndex = topics.findIndex((i) => i.id === id);
|
||||||
|
if (topicIndex === -1) {
|
||||||
|
res.status(404).json({ message: 'Topic not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
topics[topicIndex].name = name;
|
||||||
|
res.json(topics[topicIndex]);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete a topic
|
||||||
|
export const deleteTopic = (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const id = parseInt(req.params.id, 10);
|
||||||
|
const topicIndex = topics.findIndex((i) => i.id === id);
|
||||||
|
if (topicIndex === -1) {
|
||||||
|
res.status(404).json({ message: 'Topic not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const deletedTopic = topics.splice(topicIndex, 1)[0];
|
||||||
|
res.json(deletedTopic);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
17
src/middleware/errorHandler.ts
Normal file
17
src/middleware/errorHandler.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
|
export interface AppError extends Error {
|
||||||
|
status?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const errorHandler = (
|
||||||
|
err: AppError,
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) => {
|
||||||
|
console.error(err);
|
||||||
|
res.status(err.status || 500).json({
|
||||||
|
message: err.message || 'Internal Server Error',
|
||||||
|
});
|
||||||
|
};
|
11
src/models/resource.ts
Normal file
11
src/models/resource.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
export interface Resource {
|
||||||
|
id: number;
|
||||||
|
topicId: string;
|
||||||
|
url: string;
|
||||||
|
description: string;
|
||||||
|
type: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export let resources: Resource[] = [];
|
11
src/models/topic.ts
Normal file
11
src/models/topic.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
export interface Topic {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
content: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
version: number;
|
||||||
|
parentTopicId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export let topics: Topic[] = [];
|
9
src/models/user.ts
Normal file
9
src/models/user.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
export interface User {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export let users: User[] = []
|
18
src/routes/topicRoutes.ts
Normal file
18
src/routes/topicRoutes.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { Router } from 'express';
|
||||||
|
import {
|
||||||
|
createTopic,
|
||||||
|
getTopics,
|
||||||
|
getTopicById,
|
||||||
|
updateTopic,
|
||||||
|
deleteTopic,
|
||||||
|
} from '../controllers/topicController';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get('/', getTopics);
|
||||||
|
router.get('/:id', getTopicById);
|
||||||
|
router.post('/', createTopic);
|
||||||
|
router.put('/:id', updateTopic);
|
||||||
|
router.delete('/:id', deleteTopic);
|
||||||
|
|
||||||
|
export default router;
|
22
tests/topicController.test.ts
Normal file
22
tests/topicController.test.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { getTopics } from '../src/controllers/topicController';
|
||||||
|
import { topics } from '../src/models/topic';
|
||||||
|
|
||||||
|
describe('Topic Controller', () => {
|
||||||
|
it('should return an empty array when no topics exist', () => {
|
||||||
|
// Create mock objects for Request, Response, and NextFunction
|
||||||
|
const req = {} as Request;
|
||||||
|
const res = {
|
||||||
|
json: jest.fn(),
|
||||||
|
} as unknown as Response;
|
||||||
|
|
||||||
|
// Ensure that our in-memory store is empty
|
||||||
|
topics.length = 0;
|
||||||
|
|
||||||
|
// Execute our controller function
|
||||||
|
getTopics(req, res, jest.fn());
|
||||||
|
|
||||||
|
// Expect that res.json was called with an empty array
|
||||||
|
expect(res.json).toHaveBeenCalledWith([]);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Add table
Add a link
Reference in a new issue