Hozifa Elgharbawy commited on
Commit
1524ed4
·
2 Parent(s): 86f4808 844f343

Merge branch 'main' of https://github.com/Modarb-Ai-Trainer/modarb-backend

Browse files
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
- name: string;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  }
7
 
8
  const exerciseSchema = new Schema({
9
- name: { type: String, required: true, unique: true, dropDups: true },
10
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  });
12
 
13
  export type ExerciseDocument = IExercise & mongoose.Document;
14
 
15
- export const Exercise = mongoose.model<ExerciseDocument>("exercises", exerciseSchema);
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: Record<string, any> | Record<string, any>[];
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
+ });