From 7ec9ffce646938e6fb9addceffd96c47bb234958 Mon Sep 17 00:00:00 2001 From: narayansharma-21 <97.sharman@gmail.com> Date: Mon, 18 Nov 2024 20:35:46 -0500 Subject: [PATCH 1/8] init commit --- backend/src/controllers/fish/get.ts | 23 +++++++++++++++++++++++ backend/src/middlewares/authMiddleware.ts | 11 ++++++----- backend/src/models/fish.ts | 2 ++ backend/src/routes/fish.ts | 2 ++ 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/backend/src/controllers/fish/get.ts b/backend/src/controllers/fish/get.ts index ffeaab9c..bfac994d 100644 --- a/backend/src/controllers/fish/get.ts +++ b/backend/src/controllers/fish/get.ts @@ -7,3 +7,26 @@ export const getById = async (req: express.Request, res: express.Response) => { if (!fish) return res.status(404).json({ message: 'Fish not found' }); return res.status(200).json(fish); }; + +export const queryFish = async (req: express.Request, res: express.Response) => { + const query = req.query.q ? req.query.q.toString().trim() : ''; + try { + let fish; + if (query) { + fish = await Fish.find({ $text: { $search: query } }) + .sort({ score: { $meta: 'textScore' } }) + .limit(5); + } else { + // Default: Get the top 5 fish sorted alphabetically + fish = await Fish.find({}) + .sort({ commonName: 1 }) + .limit(5); + } + return res.status(200).json(fish); + } catch (error) { + console.error(error); + return res.status(500).json({ message: 'Internal server error' }); + } +}; + + diff --git a/backend/src/middlewares/authMiddleware.ts b/backend/src/middlewares/authMiddleware.ts index 3496ef27..73ddb71a 100644 --- a/backend/src/middlewares/authMiddleware.ts +++ b/backend/src/middlewares/authMiddleware.ts @@ -5,9 +5,10 @@ export const isAuthenticated = ( res: express.Response, next: express.NextFunction, ) => { - if (req.session && req.session.userId) { - next(); - } else { - res.status(401).json({ error: 'Unauthorized' }); - } + // if (req.session && req.session.userId) { + // next(); + // } else { + // res.status(401).json({ error: 'Unauthorized' }); + // } + next(); }; diff --git a/backend/src/models/fish.ts b/backend/src/models/fish.ts index 8405bedf..3c2452ec 100644 --- a/backend/src/models/fish.ts +++ b/backend/src/models/fish.ts @@ -7,4 +7,6 @@ const FishSchema = new mongoose.Schema({ sightingsCount: { type: Number, default: 0 }, }); +FishSchema.index({ commonName: 'text', scientificName: 'text' }); + export const Fish = mongoose.model('Fish', FishSchema); diff --git a/backend/src/routes/fish.ts b/backend/src/routes/fish.ts index a0d49d96..ae22ee09 100644 --- a/backend/src/routes/fish.ts +++ b/backend/src/routes/fish.ts @@ -1,6 +1,7 @@ import express from 'express'; import { isAuthenticated } from '../middlewares/authMiddleware'; import { getById } from '../controllers/fish/get'; +import { queryFish } from '../controllers/fish/get'; /** * @swagger @@ -25,4 +26,5 @@ import { getById } from '../controllers/fish/get'; */ export default (router: express.Router) => { router.get('/fish/:id', isAuthenticated, getById); + router.get('/fish/query', queryFish); }; From c4a78abe5adbbda9a386952daee1829f8c617c1e Mon Sep 17 00:00:00 2001 From: narayansharma-21 <97.sharman@gmail.com> Date: Mon, 18 Nov 2024 20:53:51 -0500 Subject: [PATCH 2/8] working --- backend/src/controllers/fish/get.ts | 32 -------------------------- backend/src/controllers/species/get.ts | 22 ++++++++++++++++++ backend/src/models/fish.ts | 12 ---------- backend/src/models/species.ts | 2 ++ backend/src/routes/fish.ts | 30 ------------------------ backend/src/routes/species.ts | 3 ++- 6 files changed, 26 insertions(+), 75 deletions(-) delete mode 100644 backend/src/controllers/fish/get.ts delete mode 100644 backend/src/models/fish.ts delete mode 100644 backend/src/routes/fish.ts diff --git a/backend/src/controllers/fish/get.ts b/backend/src/controllers/fish/get.ts deleted file mode 100644 index bfac994d..00000000 --- a/backend/src/controllers/fish/get.ts +++ /dev/null @@ -1,32 +0,0 @@ -import express from 'express'; -import { Fish } from '../../models/fish'; - -export const getById = async (req: express.Request, res: express.Response) => { - const id = req.params.id; - const fish = await Fish.findById(id); - if (!fish) return res.status(404).json({ message: 'Fish not found' }); - return res.status(200).json(fish); -}; - -export const queryFish = async (req: express.Request, res: express.Response) => { - const query = req.query.q ? req.query.q.toString().trim() : ''; - try { - let fish; - if (query) { - fish = await Fish.find({ $text: { $search: query } }) - .sort({ score: { $meta: 'textScore' } }) - .limit(5); - } else { - // Default: Get the top 5 fish sorted alphabetically - fish = await Fish.find({}) - .sort({ commonName: 1 }) - .limit(5); - } - return res.status(200).json(fish); - } catch (error) { - console.error(error); - return res.status(500).json({ message: 'Internal server error' }); - } -}; - - diff --git a/backend/src/controllers/species/get.ts b/backend/src/controllers/species/get.ts index 36428e91..e74e4d85 100644 --- a/backend/src/controllers/species/get.ts +++ b/backend/src/controllers/species/get.ts @@ -17,3 +17,25 @@ export const getByScientificName = async ( if (!species) return res.status(404).json({ message: 'Species not found' }); return res.status(200).json(species); }; + +export const querySpecies = async (req: express.Request, res: express.Response) => { + const query = req.query.q ? req.query.q.toString().trim() : ''; + + try { + let species; + if (query) { + species = await Species.find({ $text: { $search: query } }) + .sort({ score: { $meta: 'textScore' } }) // Sort by relevance + .limit(5); // Limit to 5 results + } else { + // Default: Get the top 5 species sorted alphabetically by scientificName + species = await Species.find({}) + .sort({ scientificName: 1 }) + .limit(5); + } + return res.status(200).json(species); + } catch (error) { + console.error('Error querying species:', error); + return res.status(500).json({ message: 'Internal server error' }); + } +}; diff --git a/backend/src/models/fish.ts b/backend/src/models/fish.ts deleted file mode 100644 index 3c2452ec..00000000 --- a/backend/src/models/fish.ts +++ /dev/null @@ -1,12 +0,0 @@ -import mongoose from 'mongoose'; - -const FishSchema = new mongoose.Schema({ - commonName: { type: String, required: true }, - wikipediaUrl: { type: String }, - rarity: { type: String, enum: ['RARE', 'UNCOMMON', 'LEGENDARY', 'COMMON'] }, - sightingsCount: { type: Number, default: 0 }, -}); - -FishSchema.index({ commonName: 'text', scientificName: 'text' }); - -export const Fish = mongoose.model('Fish', FishSchema); diff --git a/backend/src/models/species.ts b/backend/src/models/species.ts index 1d6ca1da..e039b56a 100644 --- a/backend/src/models/species.ts +++ b/backend/src/models/species.ts @@ -10,4 +10,6 @@ const SpeciesSchema = new mongoose.Schema({ imageUrls: [String], }); +SpeciesSchema.index({ commonNames: 'text', scientificName: 'text' }); + export const Species = mongoose.model('Species', SpeciesSchema); diff --git a/backend/src/routes/fish.ts b/backend/src/routes/fish.ts deleted file mode 100644 index ae22ee09..00000000 --- a/backend/src/routes/fish.ts +++ /dev/null @@ -1,30 +0,0 @@ -import express from 'express'; -import { isAuthenticated } from '../middlewares/authMiddleware'; -import { getById } from '../controllers/fish/get'; -import { queryFish } from '../controllers/fish/get'; - -/** - * @swagger - * /fish/{id}: - * get: - * summary: Get fish by ID - * description: Retrieve a fish by its unique ID. - * parameters: - * - in: path - * name: id - * required: true - * description: ID of the fish to retrieve - * schema: - * type: string - * responses: - * 200: - * description: Successfully retrieved fish - * 401: - * description: Unauthorized - * 404: - * description: Fish not found - */ -export default (router: express.Router) => { - router.get('/fish/:id', isAuthenticated, getById); - router.get('/fish/query', queryFish); -}; diff --git a/backend/src/routes/species.ts b/backend/src/routes/species.ts index a92bc066..cda1a18f 100644 --- a/backend/src/routes/species.ts +++ b/backend/src/routes/species.ts @@ -1,6 +1,6 @@ import express from 'express'; import { isAuthenticated } from '../middlewares/authMiddleware'; -import { getById, getByScientificName } from '../controllers/species/get'; +import { getById, getByScientificName, querySpecies } from '../controllers/species/get'; /** * @swagger @@ -48,4 +48,5 @@ export default (router: express.Router) => { isAuthenticated, getByScientificName, ); + router.get('/species/query', querySpecies); }; From b5b7c394e81d0200fe57651df14aa61740ddcd7e Mon Sep 17 00:00:00 2001 From: narayansharma-21 <97.sharman@gmail.com> Date: Mon, 18 Nov 2024 20:54:24 -0500 Subject: [PATCH 3/8] Added authentication --- backend/src/routes/species.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/routes/species.ts b/backend/src/routes/species.ts index cda1a18f..2de9a683 100644 --- a/backend/src/routes/species.ts +++ b/backend/src/routes/species.ts @@ -48,5 +48,5 @@ export default (router: express.Router) => { isAuthenticated, getByScientificName, ); - router.get('/species/query', querySpecies); -}; + router.get('/species/query', isAuthenticated, querySpecies); +}; \ No newline at end of file From 23198f7d32098c58d67aa5ff2a250ee237844220 Mon Sep 17 00:00:00 2001 From: narayansharma-21 <97.sharman@gmail.com> Date: Mon, 18 Nov 2024 20:55:57 -0500 Subject: [PATCH 4/8] Changed naming --- backend/src/controllers/species/get.ts | 2 +- backend/src/routes/species.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/controllers/species/get.ts b/backend/src/controllers/species/get.ts index e74e4d85..fd4b93ce 100644 --- a/backend/src/controllers/species/get.ts +++ b/backend/src/controllers/species/get.ts @@ -18,7 +18,7 @@ export const getByScientificName = async ( return res.status(200).json(species); }; -export const querySpecies = async (req: express.Request, res: express.Response) => { +export const getSpeciesBySearch = async (req: express.Request, res: express.Response) => { const query = req.query.q ? req.query.q.toString().trim() : ''; try { diff --git a/backend/src/routes/species.ts b/backend/src/routes/species.ts index 2de9a683..d7933610 100644 --- a/backend/src/routes/species.ts +++ b/backend/src/routes/species.ts @@ -1,6 +1,6 @@ import express from 'express'; import { isAuthenticated } from '../middlewares/authMiddleware'; -import { getById, getByScientificName, querySpecies } from '../controllers/species/get'; +import { getById, getByScientificName, getSpeciesBySearch } from '../controllers/species/get'; /** * @swagger @@ -48,5 +48,5 @@ export default (router: express.Router) => { isAuthenticated, getByScientificName, ); - router.get('/species/query', isAuthenticated, querySpecies); + router.get('/species/query', isAuthenticated, getSpeciesBySearch); }; \ No newline at end of file From a34f7c7f3924967e487c0244945bcd420b00b8a1 Mon Sep 17 00:00:00 2001 From: narayansharma-21 <97.sharman@gmail.com> Date: Mon, 18 Nov 2024 21:01:44 -0500 Subject: [PATCH 5/8] added swagger docs --- backend/src/routes/species.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/backend/src/routes/species.ts b/backend/src/routes/species.ts index d7933610..999709bd 100644 --- a/backend/src/routes/species.ts +++ b/backend/src/routes/species.ts @@ -40,6 +40,24 @@ import { getById, getByScientificName, getSpeciesBySearch } from '../controllers * description: Unauthorized * 404: * description: species not found + * /species/query: + * get: + * summary: Query species + * description: Search for species by a query string or retrieve top results alphabetically. + * parameters: + * - in: query + * name: q + * required: false + * description: Query string to search for species + * schema: + * type: string + * responses: + * 200: + * description: Successfully retrieved species results + * 401: + * description: Unauthorized + * 500: + * description: Internal server error */ export default (router: express.Router) => { router.get('/species/id/:id', isAuthenticated, getById); From df60ede91a9f3c4abc1cb06b3cd291cb3433b99d Mon Sep 17 00:00:00 2001 From: narayansharma-21 <97.sharman@gmail.com> Date: Mon, 18 Nov 2024 21:21:27 -0500 Subject: [PATCH 6/8] added tests --- backend/src/__tests__/species.ts | 53 ++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/backend/src/__tests__/species.ts b/backend/src/__tests__/species.ts index 8eab6af1..eac7ab98 100644 --- a/backend/src/__tests__/species.ts +++ b/backend/src/__tests__/species.ts @@ -22,6 +22,7 @@ species(router); app.use(router); const speciesMock = Species.findById as jest.Mock; const speciesFindMock = Species.findOne as jest.Mock; +const speciesFindQueryMock = Species.find as jest.Mock; describe('GET /species/id', () => { beforeEach(() => { @@ -65,4 +66,56 @@ describe('GET /species/scientific/id', () => { expect(res.status).toBe(200); expect(res.body).toEqual(species); }); +} +); + +describe('GET /species/query', () => { + beforeEach(() => { + speciesFindQueryMock.mockReset(); + }); + + it('Returns species results for a valid query string', async () => { + const queryString = 'salmon'; + const mockResults = [ + { _id: new mongoose.Types.ObjectId().toString(), scientificName: 'Salmo salar', commonNames: ['Atlantic salmon'] }, + { _id: new mongoose.Types.ObjectId().toString(), scientificName: 'Oncorhynchus tshawytscha', commonNames: ['Chinook salmon'] }, + ]; + + speciesFindQueryMock.mockReturnValue({ + sort: jest.fn().mockReturnValue({ + limit: jest.fn().mockResolvedValue(mockResults), + }), + }); + + const res = await request(app).get(`/species/query?q=${queryString}`); + expect(res.status).toBe(200); + expect(res.body).toEqual(mockResults); + }); + + it('Returns top alphabetically sorted species when no query is provided', async () => { + const mockResults = [ + { _id: new mongoose.Types.ObjectId().toString(), scientificName: 'Abramis brama', commonNames: ['Bream'] }, + { _id: new mongoose.Types.ObjectId().toString(), scientificName: 'Alosa alosa', commonNames: ['Allis shad'] }, + ]; + + speciesFindQueryMock.mockReturnValue({ + sort: jest.fn().mockReturnValue({ + limit: jest.fn().mockResolvedValue(mockResults), + }), + }); + + const res = await request(app).get('/species/query'); + expect(res.status).toBe(200); + expect(res.body).toEqual(mockResults); + }); + + it('Handles internal server errors gracefully', async () => { + speciesFindQueryMock.mockImplementation(() => { + throw new Error('Database error'); + }); + + const res = await request(app).get('/species/query?q=salmon'); + expect(res.status).toBe(500); + expect(res.body).toEqual({ message: 'Internal server error' }); + }); }); From 3de71d96bf6c0d75f724b45f285fb982cace437a Mon Sep 17 00:00:00 2001 From: narayansharma-21 <97.sharman@gmail.com> Date: Thu, 21 Nov 2024 18:29:19 -0500 Subject: [PATCH 7/8] final commit --- backend/src/middlewares/authMiddleware.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/src/middlewares/authMiddleware.ts b/backend/src/middlewares/authMiddleware.ts index 73ddb71a..f6b9e44f 100644 --- a/backend/src/middlewares/authMiddleware.ts +++ b/backend/src/middlewares/authMiddleware.ts @@ -5,10 +5,10 @@ export const isAuthenticated = ( res: express.Response, next: express.NextFunction, ) => { - // if (req.session && req.session.userId) { - // next(); - // } else { - // res.status(401).json({ error: 'Unauthorized' }); - // } + if (req.session && req.session.userId) { + next(); + } else { + res.status(401).json({ error: 'Unauthorized' }); + } next(); }; From fd605485fbfc9d7c8284cdfffba2a2fb8d36e44e Mon Sep 17 00:00:00 2001 From: narayansharma-21 <97.sharman@gmail.com> Date: Thu, 21 Nov 2024 18:34:27 -0500 Subject: [PATCH 8/8] prettier --- backend/src/__tests__/species.ts | 27 ++++++++++++++++++++------ backend/src/controllers/species/get.ts | 9 +++++---- backend/src/routes/species.ts | 10 +++++++--- 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/backend/src/__tests__/species.ts b/backend/src/__tests__/species.ts index eac7ab98..a4e40baf 100644 --- a/backend/src/__tests__/species.ts +++ b/backend/src/__tests__/species.ts @@ -66,8 +66,7 @@ describe('GET /species/scientific/id', () => { expect(res.status).toBe(200); expect(res.body).toEqual(species); }); -} -); +}); describe('GET /species/query', () => { beforeEach(() => { @@ -77,8 +76,16 @@ describe('GET /species/query', () => { it('Returns species results for a valid query string', async () => { const queryString = 'salmon'; const mockResults = [ - { _id: new mongoose.Types.ObjectId().toString(), scientificName: 'Salmo salar', commonNames: ['Atlantic salmon'] }, - { _id: new mongoose.Types.ObjectId().toString(), scientificName: 'Oncorhynchus tshawytscha', commonNames: ['Chinook salmon'] }, + { + _id: new mongoose.Types.ObjectId().toString(), + scientificName: 'Salmo salar', + commonNames: ['Atlantic salmon'], + }, + { + _id: new mongoose.Types.ObjectId().toString(), + scientificName: 'Oncorhynchus tshawytscha', + commonNames: ['Chinook salmon'], + }, ]; speciesFindQueryMock.mockReturnValue({ @@ -94,8 +101,16 @@ describe('GET /species/query', () => { it('Returns top alphabetically sorted species when no query is provided', async () => { const mockResults = [ - { _id: new mongoose.Types.ObjectId().toString(), scientificName: 'Abramis brama', commonNames: ['Bream'] }, - { _id: new mongoose.Types.ObjectId().toString(), scientificName: 'Alosa alosa', commonNames: ['Allis shad'] }, + { + _id: new mongoose.Types.ObjectId().toString(), + scientificName: 'Abramis brama', + commonNames: ['Bream'], + }, + { + _id: new mongoose.Types.ObjectId().toString(), + scientificName: 'Alosa alosa', + commonNames: ['Allis shad'], + }, ]; speciesFindQueryMock.mockReturnValue({ diff --git a/backend/src/controllers/species/get.ts b/backend/src/controllers/species/get.ts index fd4b93ce..dbc54a49 100644 --- a/backend/src/controllers/species/get.ts +++ b/backend/src/controllers/species/get.ts @@ -18,7 +18,10 @@ export const getByScientificName = async ( return res.status(200).json(species); }; -export const getSpeciesBySearch = async (req: express.Request, res: express.Response) => { +export const getSpeciesBySearch = async ( + req: express.Request, + res: express.Response, +) => { const query = req.query.q ? req.query.q.toString().trim() : ''; try { @@ -29,9 +32,7 @@ export const getSpeciesBySearch = async (req: express.Request, res: express.Resp .limit(5); // Limit to 5 results } else { // Default: Get the top 5 species sorted alphabetically by scientificName - species = await Species.find({}) - .sort({ scientificName: 1 }) - .limit(5); + species = await Species.find({}).sort({ scientificName: 1 }).limit(5); } return res.status(200).json(species); } catch (error) { diff --git a/backend/src/routes/species.ts b/backend/src/routes/species.ts index 999709bd..9b12a983 100644 --- a/backend/src/routes/species.ts +++ b/backend/src/routes/species.ts @@ -1,6 +1,10 @@ import express from 'express'; import { isAuthenticated } from '../middlewares/authMiddleware'; -import { getById, getByScientificName, getSpeciesBySearch } from '../controllers/species/get'; +import { + getById, + getByScientificName, + getSpeciesBySearch, +} from '../controllers/species/get'; /** * @swagger @@ -66,5 +70,5 @@ export default (router: express.Router) => { isAuthenticated, getByScientificName, ); - router.get('/species/query', isAuthenticated, getSpeciesBySearch); -}; \ No newline at end of file + router.get('/species/query', isAuthenticated, getSpeciesBySearch); +};