Skip to content

Commit 69fdc80

Browse files
author
Anmol Gangwar
committed
feat(endpoint): ⚡ Added New endpoints for exercises search many more!
1 parent 6c13852 commit 69fdc80

File tree

16 files changed

+335
-280
lines changed

16 files changed

+335
-280
lines changed

src/data/exercises.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@
432432
},
433433
{
434434
"exerciseId": "H1PESYI",
435-
"name": "stationary bike run v. 3",
435+
"name": "stationary bike run",
436436
"gifUrl": "https://v1.cdn.exercisedb.dev/media/H1PESYI.gif",
437437
"targetMuscles": ["cardiovascular system"],
438438
"bodyParts": ["cardio"],

src/modules/bodyparts/controllers/bodyPart.controller.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { createRoute, OpenAPIHono } from '@hono/zod-openapi'
33
import { z } from 'zod'
44
import { BodyPartService } from '../services'
55
import { BodyPartModel } from '../models/bodyPart.model'
6-
import { ExerciseModel } from '#modules/exercises/models/exercise.model.js'
76

87
export class BodyPartController implements Routes {
98
public controller: OpenAPIHono

src/modules/bodyparts/services/body-part.service.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
1-
import { GetExercisesUseCase } from '#modules/exercises/use-cases/get-exercise.usecase.js'
21
import { GetBodyPartsUseCase } from '../use-cases'
32

43
export class BodyPartService {
54
private readonly getBodyPartsUseCase: GetBodyPartsUseCase
6-
private readonly getExercisesUseCase: GetExercisesUseCase
75

86
constructor() {
97
this.getBodyPartsUseCase = new GetBodyPartsUseCase()
10-
this.getExercisesUseCase = new GetExercisesUseCase()
118
}
129

1310
getBodyParts = () => {
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
import { BodyPart } from "../../../data/types";
1+
import { BodyPart } from '../../../data/types'
22

33
export type FetchAllBodyPartRes = BodyPart[]

src/modules/bodyparts/use-cases/get-bodypart.usecase.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { IUseCase } from '#common/types/use-case.type.js'
2-
import { FileLoader } from "../../../data/load";
2+
import { FileLoader } from '../../../data/load'
33
import { FetchAllBodyPartRes } from '../types'
44

55
export class GetBodyPartsUseCase implements IUseCase<void, FetchAllBodyPartRes> {

src/modules/equipments/controllers/equipment.controller.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ import { createRoute, OpenAPIHono } from '@hono/zod-openapi'
33
import { z } from 'zod'
44
import { EquipmentModel } from '../models/equipment.model'
55
import { EquipmentService } from '../services'
6-
import { HTTPException } from 'hono/http-exception'
7-
import { ExerciseModel } from '#modules/exercises/models/exercise.model.js'
86

97
export class EquipmentController implements Routes {
108
public controller: OpenAPIHono

src/modules/equipments/services/equipment.service.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
1-
import { GetExerciseSerivceArgs } from '#modules/exercises/services/exercise.service.js'
2-
import { GetExercisesArgs, GetExercisesUseCase } from '#modules/exercises/use-cases/get-exercise.usecase.js'
31
import { GetEquipmentsUseCase } from '../use-cases'
42

53
export class EquipmentService {
64
private readonly getEquipmentUseCase: GetEquipmentsUseCase
7-
private readonly getExercisesUseCase: GetExercisesUseCase
85

96
constructor() {
107
this.getEquipmentUseCase = new GetEquipmentsUseCase()
11-
this.getExercisesUseCase = new GetExercisesUseCase()
128
}
139

1410
getEquipments = () => {

src/modules/equipments/use-cases/get-equipment.usecase.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { IUseCase } from '#common/types/use-case.type.js'
2-
import { FileLoader } from "../../../data/load";
2+
import { FileLoader } from '../../../data/load'
33
import { FetchAllEquipmentRes } from '../types'
44

55
export class GetEquipmentsUseCase implements IUseCase<void, FetchAllEquipmentRes> {

src/modules/exercises/controllers/exercise.controller.ts

Lines changed: 159 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -2,61 +2,7 @@ import { Routes } from '#common/types/route.type.js'
22
import { createRoute, OpenAPIHono } from '@hono/zod-openapi'
33
import { z } from 'zod'
44
import { ExerciseService } from '../services/exercise.service'
5-
import { ExerciseModel } from '../models/exercise.model'
6-
// Common pagination schema
7-
const PaginationQuerySchema = z.object({
8-
offset: z.coerce.number().nonnegative().optional().openapi({
9-
title: 'Offset',
10-
description:
11-
'The number of exercises to skip from the start of the list. Useful for pagination to fetch subsequent pages of results.',
12-
type: 'number',
13-
example: 0,
14-
default: 0
15-
}),
16-
limit: z.coerce.number().positive().max(100).optional().openapi({
17-
title: 'Limit',
18-
description:
19-
'The maximum number of exercises to return in the response. Limits the number of results for pagination purposes.',
20-
maximum: 25,
21-
minimum: 1,
22-
type: 'number',
23-
example: 10,
24-
default: 10
25-
})
26-
})
27-
28-
// Common response schema
29-
const ExerciseResponseSchema = z.object({
30-
success: z.literal(true).openapi({
31-
description: 'Indicates whether the request was successful',
32-
example: true
33-
}),
34-
metadata: z.object({
35-
totalExercises: z.number().openapi({
36-
description: 'Total number of exercises matching the criteria',
37-
example: 150
38-
}),
39-
totalPages: z.number().openapi({
40-
description: 'Total number of pages available',
41-
example: 15
42-
}),
43-
currentPage: z.number().openapi({
44-
description: 'Current page number',
45-
example: 1
46-
}),
47-
previousPage: z.string().nullable().openapi({
48-
description: 'URL for the previous page, null if on first page',
49-
example: '/api/exercises?offset=0&limit=10'
50-
}),
51-
nextPage: z.string().nullable().openapi({
52-
description: 'URL for the next page, null if on last page',
53-
example: '/api/exercises?offset=20&limit=10'
54-
})
55-
}),
56-
data: z.array(ExerciseModel).openapi({
57-
description: 'Array of exercises'
58-
})
59-
})
5+
import { ExerciseModel, ExerciseResponseSchema, PaginationQuerySchema } from '../models/exercise.model'
606

617
export class ExerciseController implements Routes {
628
public controller: OpenAPIHono
@@ -81,93 +27,170 @@ export class ExerciseController implements Routes {
8127
nextPage: currentPage < totalPages ? `${baseUrl}?offset=${currentPage * limit}&limit=${limit}${params}` : null
8228
}
8329
}
30+
8431
public initRoutes() {
85-
// this.controller.openapi(
86-
// createRoute({
87-
// method: 'get',
88-
// path: '/exercises/search',
89-
// tags: ['EXERCISES'],
90-
// summary: 'GetAllExercises',
91-
// operationId: 'getExercises',
92-
// request: {
93-
// query: z.object({
94-
// search: z.string().optional().openapi({
95-
// title: 'Search Query',
96-
// description:
97-
// 'A string to filter exercises based on a search term. This can be used to find specific exercises by name or description.',
98-
// type: 'string',
99-
// example: 'cardio',
100-
// default: ''
101-
// }),
102-
// offset: z.coerce.number().nonnegative().optional().openapi({
103-
// title: 'Offset',
104-
// description:
105-
// 'The number of exercises to skip from the start of the list. Useful for pagination to fetch subsequent pages of results.',
106-
// type: 'number',
107-
// example: 10,
108-
// default: 0
109-
// }),
110-
// limit: z.coerce.number().positive().max(100).optional().openapi({
111-
// title: 'Limit',
112-
// description:
113-
// 'The maximum number of exercises to return in the response. Limits the number of results for pagination purposes.',
114-
// maximum: 100,
115-
// minimum: 1,
116-
// type: 'number',
117-
// example: 10,
118-
// default: 10
119-
// })
120-
// })
121-
// },
122-
// responses: {
123-
// 200: {
124-
// description: 'Successful response with list of all exercises.',
125-
// content: {
126-
// 'application/json': {
127-
// schema: z.object({
128-
// success: z.boolean().openapi({
129-
// description: 'Indicates whether the request was successful',
130-
// type: 'boolean',
131-
// example: true
132-
// }),
133-
// data: z.array(ExerciseModel).openapi({
134-
// description: 'Array of Exercises.'
135-
// })
136-
// })
137-
// }
138-
// }
139-
// },
140-
// 500: {
141-
// description: 'Internal server error'
142-
// }
143-
// }
144-
// }),
145-
// async (ctx) => {
146-
// const { offset, limit = 10, search } = ctx.req.valid('query')
147-
// const { origin, pathname } = new URL(ctx.req.url)
148-
// const response = await this.exerciseService.getAllExercises({ offset, limit, search })
149-
// return ctx.json({
150-
// success: true,
151-
// data: {
152-
// previousPage:
153-
// response.currentPage > 1
154-
// ? `${origin}${pathname}?offset=${(response.currentPage - 2) * limit}&limit=${limit}`
155-
// : null,
156-
// nextPage:
157-
// response.currentPage < response.totalPages
158-
// ? `${origin}${pathname}?offset=${response.currentPage * limit}&limit=${limit}`
159-
// : null,
160-
// ...response
161-
// }
162-
// })
163-
// }
164-
// )
32+
this.controller.openapi(
33+
createRoute({
34+
method: 'get',
35+
path: '/exercises/search',
36+
tags: ['EXERCISES'],
37+
summary: 'Search exercises with fuzzy matching',
38+
description:
39+
"Search exercises using fuzzy matching across all fields (name, muscles, equipment, body parts). Perfect for when users don't know exact terms.",
40+
operationId: 'searchExercises',
41+
request: {
42+
query: PaginationQuerySchema.extend({
43+
q: z.string().min(1).openapi({
44+
title: 'Search Query',
45+
description:
46+
'Search term that will be fuzzy matched against exercise names, muscles, equipment, and body parts',
47+
type: 'string',
48+
example: 'chest push',
49+
default: ''
50+
}),
51+
threshold: z.coerce.number().min(0).max(1).optional().openapi({
52+
title: 'Search Threshold',
53+
description: 'Fuzzy search threshold (0 = exact match, 1 = very loose match)',
54+
type: 'number',
55+
example: 0.3,
56+
default: 0.3
57+
})
58+
})
59+
},
60+
responses: {
61+
200: {
62+
description: 'Successful response with fuzzy search results',
63+
content: {
64+
'application/json': {
65+
schema: ExerciseResponseSchema
66+
}
67+
}
68+
},
69+
500: {
70+
description: 'Internal server error'
71+
}
72+
}
73+
}),
74+
async (ctx) => {
75+
const { offset = 0, limit = 10, q, threshold = 0.3 } = ctx.req.valid('query')
76+
const { origin, pathname } = new URL(ctx.req.url)
77+
78+
const { totalExercises, currentPage, totalPages, exercises } = await this.exerciseService.searchExercises({
79+
offset,
80+
limit,
81+
query: q,
82+
threshold
83+
})
84+
85+
const { previousPage, nextPage } = this.buildPaginationUrls(
86+
origin,
87+
pathname,
88+
currentPage,
89+
totalPages,
90+
limit,
91+
`q=${encodeURIComponent(q)}&threshold=${threshold}`
92+
)
93+
94+
return ctx.json({
95+
success: true,
96+
metadata: {
97+
totalPages,
98+
totalExercises,
99+
currentPage,
100+
previousPage,
101+
nextPage
102+
},
103+
data: [...exercises]
104+
})
105+
}
106+
)
107+
165108
this.controller.openapi(
166109
createRoute({
167110
method: 'get',
168111
path: '/exercises',
169112
tags: ['EXERCISES'],
170-
summary: 'GetAllExercises',
113+
summary: 'Get all exercises with optional search',
114+
description: 'Retrieve all exercises with optional fuzzy search filtering',
115+
operationId: 'getExercises',
116+
request: {
117+
query: PaginationQuerySchema.extend({
118+
search: z.string().optional().openapi({
119+
title: 'Search Query',
120+
description: 'Optional search term for fuzzy matching across all exercise fields',
121+
type: 'string',
122+
example: 'cardio',
123+
default: ''
124+
}),
125+
sortBy: z.enum(['name', 'exerciseId', 'targetMuscles', 'bodyParts', 'equipments']).optional().openapi({
126+
title: 'Sort Field',
127+
description: 'Field to sort exercises by',
128+
example: 'targetMuscles',
129+
default: 'targetMuscles'
130+
}),
131+
sortOrder: z.enum(['asc', 'desc']).optional().openapi({
132+
title: 'Sort Order',
133+
description: 'Sort order (ascending or descending)',
134+
example: 'desc',
135+
default: 'desc'
136+
})
137+
})
138+
},
139+
responses: {
140+
200: {
141+
description: 'Successful response with exercises',
142+
content: {
143+
'application/json': {
144+
schema: ExerciseResponseSchema
145+
}
146+
}
147+
},
148+
500: {
149+
description: 'Internal server error'
150+
}
151+
}
152+
}),
153+
async (ctx) => {
154+
const { offset = 0, limit = 10, search, sortBy = 'targetMuscles', sortOrder = 'desc' } = ctx.req.valid('query')
155+
const { origin, pathname } = new URL(ctx.req.url)
156+
157+
const { totalExercises, totalPages, currentPage, exercises } = await this.exerciseService.getAllExercises({
158+
offset,
159+
limit,
160+
search,
161+
sort: { [sortBy]: sortOrder === 'asc' ? 1 : -1 }
162+
})
163+
164+
const searchParam = search ? `&search=${encodeURIComponent(search)}` : ''
165+
const sortParams = `&sortBy=${sortBy}&sortOrder=${sortOrder}`
166+
const { previousPage, nextPage } = this.buildPaginationUrls(
167+
origin,
168+
pathname,
169+
currentPage,
170+
totalPages,
171+
limit,
172+
`${searchParam}${sortParams}`
173+
)
174+
175+
return ctx.json({
176+
success: true,
177+
metadata: {
178+
totalPages,
179+
totalExercises,
180+
currentPage,
181+
previousPage,
182+
nextPage
183+
},
184+
data: [...exercises]
185+
})
186+
}
187+
)
188+
this.controller.openapi(
189+
createRoute({
190+
method: 'get',
191+
path: '/exercises/filter',
192+
tags: ['EXERCISES'],
193+
summary: 'Advanced exercise filtering',
171194
description: 'Advance Filter exercises by multiple criteria with fuzzy search support',
172195
operationId: 'filterExercises',
173196
request: {

0 commit comments

Comments
 (0)