Spaces:
Running
Running
Merge branch 'main' of https://github.com/Modarb-Ai-Trainer/modarb-backend
Browse files- src/common/models/exercise.model.ts +42 -5
- src/common/serializers/exercise.serialization.ts +1 -2
- src/lib/responses/json-response.ts +1 -1
- src/lib/responses/json-responses-params.d.ts +1 -1
- src/modules/console/modules/exercises/controllers/exercises.controller.ts +97 -0
- src/modules/console/modules/exercises/services/exercises.service.ts +4 -0
- src/modules/console/modules/exercises/validations/create-excercise.validation.ts +85 -0
- src/modules/console/modules/exercises/validations/update-excercise.validation.ts +85 -0
src/common/models/exercise.model.ts
CHANGED
@@ -2,15 +2,52 @@ import mongoose from "mongoose";
|
|
2 |
const { Schema } = mongoose;
|
3 |
|
4 |
export interface IExercise {
|
5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
}
|
7 |
|
8 |
const exerciseSchema = new Schema({
|
9 |
-
|
10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
11 |
});
|
12 |
|
13 |
export type ExerciseDocument = IExercise & mongoose.Document;
|
14 |
|
15 |
-
export const Exercise = mongoose.model<ExerciseDocument>(
|
16 |
-
|
|
|
|
|
|
2 |
const { Schema } = mongoose;
|
3 |
|
4 |
export interface IExercise {
|
5 |
+
name: string;
|
6 |
+
category: string;
|
7 |
+
duration?: number | null;
|
8 |
+
expectedDurationRange: {
|
9 |
+
min: number;
|
10 |
+
max: number;
|
11 |
+
};
|
12 |
+
reps: number;
|
13 |
+
sets: number;
|
14 |
+
instructions: string;
|
15 |
+
benefits: string;
|
16 |
+
targetMuscles: string[]; // refs
|
17 |
+
equipments: string[]; // refs
|
18 |
+
media: {
|
19 |
+
type: "image" | "video";
|
20 |
+
url: string;
|
21 |
+
};
|
22 |
}
|
23 |
|
24 |
const exerciseSchema = new Schema({
|
25 |
+
name: { type: String, required: true, unique: true, dropDups: true },
|
26 |
+
category: { type: String, required: true },
|
27 |
+
duration: { type: Number, required: false },
|
28 |
+
expectedDurationRange: {
|
29 |
+
min: { type: Number, required: true },
|
30 |
+
max: { type: Number, required: true },
|
31 |
+
},
|
32 |
+
reps: { type: Number, required: true },
|
33 |
+
sets: { type: Number, required: true },
|
34 |
+
instructions: { type: String, required: true },
|
35 |
+
benefits: { type: String, required: true },
|
36 |
+
targetMuscles: [{ type: Schema.Types.ObjectId, ref: "muscles" }],
|
37 |
+
equipments: [{ type: Schema.Types.ObjectId, ref: "equipments" }],
|
38 |
+
media: {
|
39 |
+
type: {
|
40 |
+
type: String,
|
41 |
+
enum: ["image", "video"],
|
42 |
+
required: true,
|
43 |
+
},
|
44 |
+
url: { type: String, required: true },
|
45 |
+
},
|
46 |
});
|
47 |
|
48 |
export type ExerciseDocument = IExercise & mongoose.Document;
|
49 |
|
50 |
+
export const Exercise = mongoose.model<ExerciseDocument>(
|
51 |
+
"exercises",
|
52 |
+
exerciseSchema
|
53 |
+
);
|
src/common/serializers/exercise.serialization.ts
CHANGED
@@ -6,5 +6,4 @@ export class ExerciseSerialization {
|
|
6 |
|
7 |
@Expose()
|
8 |
name: string;
|
9 |
-
|
10 |
-
}
|
|
|
6 |
|
7 |
@Expose()
|
8 |
name: string;
|
9 |
+
}
|
|
src/lib/responses/json-response.ts
CHANGED
@@ -35,7 +35,7 @@ export abstract class JsonResponse {
|
|
35 |
const data = {
|
36 |
status: props.status || 200,
|
37 |
message: props.message || "Success",
|
38 |
-
data: props.data,
|
39 |
meta: props.meta,
|
40 |
} satisfies IJSONSuccessResponse;
|
41 |
|
|
|
35 |
const data = {
|
36 |
status: props.status || 200,
|
37 |
message: props.message || "Success",
|
38 |
+
data: props.data || null,
|
39 |
meta: props.meta,
|
40 |
} satisfies IJSONSuccessResponse;
|
41 |
|
src/lib/responses/json-responses-params.d.ts
CHANGED
@@ -1,7 +1,7 @@
|
|
1 |
export interface IJSONSuccessResponseProps {
|
2 |
status?: number;
|
3 |
message?: string;
|
4 |
-
data
|
5 |
meta?: {
|
6 |
total: number;
|
7 |
page: number;
|
|
|
1 |
export interface IJSONSuccessResponseProps {
|
2 |
status?: number;
|
3 |
message?: string;
|
4 |
+
data?: Record<string, any> | Record<string, any>[] | null;
|
5 |
meta?: {
|
6 |
total: number;
|
7 |
page: number;
|
src/modules/console/modules/exercises/controllers/exercises.controller.ts
ADDED
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ExerciseSerialization } from "@common/serializers/exercise.serialization";
|
2 |
+
import { asyncHandler } from "@helpers/async-handler";
|
3 |
+
import { parsePaginationQuery } from "@helpers/pagination";
|
4 |
+
import { paramsValidator, bodyValidator } from "@helpers/validation.helper";
|
5 |
+
import { BaseController } from "@lib/controllers/controller.base";
|
6 |
+
import { ControllerMiddleware } from "@lib/decorators/controller-middleware.decorator";
|
7 |
+
import { Prefix } from "@lib/decorators/prefix.decorator";
|
8 |
+
import { JsonResponse } from "@lib/responses/json-response";
|
9 |
+
import { AdminGuardMiddleware } from "modules/console/common/guards/admins.guard";
|
10 |
+
import { ExercisesService } from "../services/exercises.service";
|
11 |
+
import { Request, Response } from "express";
|
12 |
+
import { serialize } from "@helpers/serialize";
|
13 |
+
import { createExerciseSchema } from "../validations/create-excercise.validation";
|
14 |
+
import { updateExerciseSchema } from "../validations/update-excercise.validation";
|
15 |
+
|
16 |
+
@Prefix("/console/exercises")
|
17 |
+
@ControllerMiddleware(AdminGuardMiddleware({}))
|
18 |
+
export class ExercisesController extends BaseController {
|
19 |
+
private exercisesService = new ExercisesService();
|
20 |
+
|
21 |
+
setRoutes() {
|
22 |
+
this.router.get("/", asyncHandler(this.list));
|
23 |
+
this.router.get("/:id", paramsValidator("id"), asyncHandler(this.get));
|
24 |
+
this.router.post(
|
25 |
+
"/",
|
26 |
+
bodyValidator(createExerciseSchema),
|
27 |
+
asyncHandler(this.create)
|
28 |
+
);
|
29 |
+
this.router.patch(
|
30 |
+
"/:id",
|
31 |
+
paramsValidator("id"),
|
32 |
+
bodyValidator(updateExerciseSchema),
|
33 |
+
asyncHandler(this.update)
|
34 |
+
);
|
35 |
+
this.router.delete(
|
36 |
+
"/:id",
|
37 |
+
paramsValidator("id"),
|
38 |
+
asyncHandler(this.delete)
|
39 |
+
);
|
40 |
+
}
|
41 |
+
|
42 |
+
list = async (req: Request, res: Response) => {
|
43 |
+
const paginationQuery = parsePaginationQuery(req.query);
|
44 |
+
const { docs, paginationData } = await this.exercisesService.list(
|
45 |
+
{},
|
46 |
+
paginationQuery
|
47 |
+
);
|
48 |
+
|
49 |
+
return JsonResponse.success(
|
50 |
+
{
|
51 |
+
data: serialize(docs, ExerciseSerialization),
|
52 |
+
meta: paginationData,
|
53 |
+
},
|
54 |
+
res
|
55 |
+
);
|
56 |
+
};
|
57 |
+
|
58 |
+
get = async (req: Request, res: Response) => {
|
59 |
+
const data = await this.exercisesService.findOneOrFail({
|
60 |
+
_id: req.params.id,
|
61 |
+
});
|
62 |
+
return JsonResponse.success(
|
63 |
+
{
|
64 |
+
data: serialize(data.toJSON(), ExerciseSerialization),
|
65 |
+
},
|
66 |
+
res
|
67 |
+
);
|
68 |
+
};
|
69 |
+
|
70 |
+
create = async (req: Request, res: Response) => {
|
71 |
+
const data = await this.exercisesService.create(req.body);
|
72 |
+
return JsonResponse.success(
|
73 |
+
{
|
74 |
+
data: serialize(data.toJSON(), ExerciseSerialization),
|
75 |
+
},
|
76 |
+
res
|
77 |
+
);
|
78 |
+
};
|
79 |
+
|
80 |
+
update = async (req: Request, res: Response) => {
|
81 |
+
const data = await this.exercisesService.updateOne(
|
82 |
+
{ _id: req.params.id },
|
83 |
+
req.body
|
84 |
+
);
|
85 |
+
return JsonResponse.success(
|
86 |
+
{
|
87 |
+
data: serialize(data.toJSON(), ExerciseSerialization),
|
88 |
+
},
|
89 |
+
res
|
90 |
+
);
|
91 |
+
};
|
92 |
+
|
93 |
+
delete = async (req: Request, res: Response) => {
|
94 |
+
await this.exercisesService.deleteOne({ _id: req.params.id });
|
95 |
+
return JsonResponse.success({}, res);
|
96 |
+
};
|
97 |
+
}
|
src/modules/console/modules/exercises/services/exercises.service.ts
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Exercise } from "@common/models/exercise.model";
|
2 |
+
import { CrudService } from "@lib/services/crud.service";
|
3 |
+
|
4 |
+
export class ExercisesService extends CrudService(Exercise) {}
|
src/modules/console/modules/exercises/validations/create-excercise.validation.ts
ADDED
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as joi from "joi";
|
2 |
+
import { createSchema } from "@helpers/create-schema";
|
3 |
+
|
4 |
+
export interface ICreateExercise {
|
5 |
+
name: string;
|
6 |
+
category: string;
|
7 |
+
duration?: number | null;
|
8 |
+
expectedDurationRange: {
|
9 |
+
min: number;
|
10 |
+
max: number;
|
11 |
+
};
|
12 |
+
reps: number;
|
13 |
+
sets: number;
|
14 |
+
instructions: string;
|
15 |
+
benefits: string;
|
16 |
+
targetMuscles: string[]; // refs
|
17 |
+
equipments: string[]; // refs
|
18 |
+
media: {
|
19 |
+
type: "image" | "video";
|
20 |
+
url: string;
|
21 |
+
};
|
22 |
+
}
|
23 |
+
|
24 |
+
export const createExerciseSchema = createSchema<ICreateExercise>({
|
25 |
+
name: joi.string().empty().required().messages({
|
26 |
+
"string.base": "please enter a valid name",
|
27 |
+
"any.required": "name is required",
|
28 |
+
"string.empty": "name can not be empty",
|
29 |
+
}),
|
30 |
+
category: joi.string().empty().required().messages({
|
31 |
+
"string.base": "please enter a valid category",
|
32 |
+
"any.required": "category is required",
|
33 |
+
"string.empty": "category can not be empty",
|
34 |
+
}),
|
35 |
+
duration: joi.number().empty().optional().messages({
|
36 |
+
"number.base": "please enter a valid duration",
|
37 |
+
}),
|
38 |
+
expectedDurationRange: joi.object().keys({
|
39 |
+
min: joi.number().required().messages({
|
40 |
+
"number.base": "please enter a valid min duration",
|
41 |
+
"any.required": "min duration is required",
|
42 |
+
}),
|
43 |
+
max: joi.number().required().messages({
|
44 |
+
"number.base": "please enter a valid max duration",
|
45 |
+
"any.required": "max duration is required",
|
46 |
+
}),
|
47 |
+
}),
|
48 |
+
reps: joi.number().empty().required().messages({
|
49 |
+
"number.base": "please enter a valid reps",
|
50 |
+
"any.required": "reps is required",
|
51 |
+
}),
|
52 |
+
sets: joi.number().empty().required().messages({
|
53 |
+
"number.base": "please enter a valid sets",
|
54 |
+
"any.required": "sets is required",
|
55 |
+
}),
|
56 |
+
instructions: joi.string().empty().required().messages({
|
57 |
+
"string.base": "please enter a valid instructions",
|
58 |
+
"any.required": "instructions is required",
|
59 |
+
"string.empty": "instructions can not be empty",
|
60 |
+
}),
|
61 |
+
benefits: joi.string().empty().required().messages({
|
62 |
+
"string.base": "please enter a valid benefits",
|
63 |
+
"any.required": "benefits is required",
|
64 |
+
"string.empty": "benefits can not be empty",
|
65 |
+
}),
|
66 |
+
targetMuscles: joi.array().items(joi.string()).empty().required().messages({
|
67 |
+
"array.base": "please enter a valid target muscles",
|
68 |
+
"any.required": "target muscles is required",
|
69 |
+
}),
|
70 |
+
equipments: joi.array().items(joi.string()).empty().required().messages({
|
71 |
+
"array.base": "please enter a valid equipments",
|
72 |
+
"any.required": "equipments is required",
|
73 |
+
}),
|
74 |
+
media: joi.object().keys({
|
75 |
+
type: joi.string().valid("image", "video").required().messages({
|
76 |
+
"string.base": "please enter a valid media type",
|
77 |
+
"any.required": "media type is required",
|
78 |
+
}),
|
79 |
+
url: joi.string().empty().required().messages({
|
80 |
+
"string.base": "please enter a valid media url",
|
81 |
+
"any.required": "media url is required",
|
82 |
+
"string.empty": "media url can not be empty",
|
83 |
+
}),
|
84 |
+
}),
|
85 |
+
});
|
src/modules/console/modules/exercises/validations/update-excercise.validation.ts
ADDED
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as joi from "joi";
|
2 |
+
import { createSchema } from "@helpers/create-schema";
|
3 |
+
|
4 |
+
export interface IUpdateExercise {
|
5 |
+
name?: string;
|
6 |
+
category?: string;
|
7 |
+
duration?: number | null;
|
8 |
+
expectedDurationRange?: {
|
9 |
+
min: number;
|
10 |
+
max: number;
|
11 |
+
};
|
12 |
+
reps: number;
|
13 |
+
sets?: number;
|
14 |
+
instructions?: string;
|
15 |
+
benefits?: string;
|
16 |
+
targetMuscles?: string[]; // refs
|
17 |
+
equipments?: string[]; // refs
|
18 |
+
media?: {
|
19 |
+
type: "image" | "video";
|
20 |
+
url: string;
|
21 |
+
};
|
22 |
+
}
|
23 |
+
|
24 |
+
export const updateExerciseSchema = createSchema<IUpdateExercise>({
|
25 |
+
name: joi.string().empty().optional().messages({
|
26 |
+
"string.base": "please enter a valid name",
|
27 |
+
"any.required": "name is required",
|
28 |
+
"string.empty": "name can not be empty",
|
29 |
+
}),
|
30 |
+
category: joi.string().empty().optional().messages({
|
31 |
+
"string.base": "please enter a valid category",
|
32 |
+
"any.required": "category is required",
|
33 |
+
"string.empty": "category can not be empty",
|
34 |
+
}),
|
35 |
+
duration: joi.number().empty().optional().messages({
|
36 |
+
"number.base": "please enter a valid duration",
|
37 |
+
}),
|
38 |
+
expectedDurationRange: joi.object().keys({
|
39 |
+
min: joi.number().optional().messages({
|
40 |
+
"number.base": "please enter a valid min duration",
|
41 |
+
"any.required": "min duration is required",
|
42 |
+
}),
|
43 |
+
max: joi.number().optional().messages({
|
44 |
+
"number.base": "please enter a valid max duration",
|
45 |
+
"any.required": "max duration is required",
|
46 |
+
}),
|
47 |
+
}),
|
48 |
+
reps: joi.number().empty().optional().messages({
|
49 |
+
"number.base": "please enter a valid reps",
|
50 |
+
"any.required": "reps is required",
|
51 |
+
}),
|
52 |
+
sets: joi.number().empty().optional().messages({
|
53 |
+
"number.base": "please enter a valid sets",
|
54 |
+
"any.required": "sets is required",
|
55 |
+
}),
|
56 |
+
instructions: joi.string().empty().optional().messages({
|
57 |
+
"string.base": "please enter a valid instructions",
|
58 |
+
"any.required": "instructions is required",
|
59 |
+
"string.empty": "instructions can not be empty",
|
60 |
+
}),
|
61 |
+
benefits: joi.string().empty().optional().messages({
|
62 |
+
"string.base": "please enter a valid benefits",
|
63 |
+
"any.required": "benefits is required",
|
64 |
+
"string.empty": "benefits can not be empty",
|
65 |
+
}),
|
66 |
+
targetMuscles: joi.array().items(joi.string()).empty().optional().messages({
|
67 |
+
"array.base": "please enter a valid target muscles",
|
68 |
+
"any.required": "target muscles is required",
|
69 |
+
}),
|
70 |
+
equipments: joi.array().items(joi.string()).empty().optional().messages({
|
71 |
+
"array.base": "please enter a valid equipments",
|
72 |
+
"any.required": "equipments is required",
|
73 |
+
}),
|
74 |
+
media: joi.object().keys({
|
75 |
+
type: joi.string().valid("image", "video").optional().messages({
|
76 |
+
"string.base": "please enter a valid media type",
|
77 |
+
"any.required": "media type is required",
|
78 |
+
}),
|
79 |
+
url: joi.string().empty().optional().messages({
|
80 |
+
"string.base": "please enter a valid media url",
|
81 |
+
"any.required": "media url is required",
|
82 |
+
"string.empty": "media url can not be empty",
|
83 |
+
}),
|
84 |
+
}),
|
85 |
+
});
|