@@ -2,61 +2,7 @@ import { Routes } from '#common/types/route.type.js'
22import { createRoute , OpenAPIHono } from '@hono/zod-openapi'
33import { z } from 'zod'
44import { 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
617export 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