eletrotupi / tcc/ commit / f74f10c

api: apply a better structure to handle zod errors

Pedro Lucas Porcellis porcellis@eletrotupi.com 29 days ago f74f10c623d080f34b937b43a9f16e47920c975b
Parents: 7e3a6c4
13 file(s) changed
  • api/nodemon.json +1 -2
  • api/src/controllers/auth.ts +5 -12
  • api/src/controllers/careActions.ts +3 -2
  • api/src/controllers/insights.ts +2 -4
  • api/src/controllers/medicineRegimens.ts +4 -3
  • api/src/controllers/moods.ts +2 -3
  • api/src/controllers/sleepRecords.ts +3 -6
  • api/src/controllers/triggerMoodLinks.ts +2 -1
  • api/src/controllers/triggers.ts +4 -7
  • api/src/controllers/users.ts +4 -7
  • api/src/lib/errors/validationError.ts +16 -0
  • api/tests/moods/create.test.ts +2 -1
  • api/tests/users/create.test.ts +4 -3
api/nodemon.json
1 1
{
2 2
  "watch": ["src"],
3 3
  "ext": "ts",
4 -
  "exec": "node -r ts-node/register -r tsconfig-paths/register ./src/index.ts"
4 +
  "exec": "tsx ./src/index.ts"
5 5
}
6 -

api/src/controllers/auth.ts
1 1
import { Request, Response, NextFunction } from "express";
2 2
import { AuthenticatedRequest } from "@app/middleware/auth";
3 3

4 4
import {
5 5
  login,
▸ 16 unchanged lines
22 22
  LoginSchema,
23 23
  PasswordResetSchema,
24 24
  PasswordResetRequestSchema,
25 25
  ActivateUserSchema,
26 26
} from "@app/schemas";
27 +
import { formatValidationError } from "@app/lib/errors/validationError";
27 28

28 29
import { addMinutes } from "date-fns";
29 30
import { getQueue, MailJobName } from "@app/lib/queue";
30 31
import crypto from "crypto";
31 32

▸ 1 unchanged lines
33 34
  login: async (req: Request, res: Response, next: NextFunction) => {
34 35
    try {
35 36
      const parsed = LoginSchema.safeParse(req.body);
36 37

37 38
      if (!parsed.success) {
38 -
        return res.status(400).json({
39 -
          errors: parsed.error!.issues,
40 -
        });
39 +
        return res.status(400).json(formatValidationError(parsed.error!));
41 40
      }
42 41

43 42
      const { user, token } = await login(parsed.data);
44 43

45 44
      if (
▸ 27 unchanged lines
73 72
  forgotPassword: async (req: Request, res: Response, next: NextFunction) => {
74 73
    try {
75 74
      const parsed = PasswordResetRequestSchema.safeParse(req.body);
76 75

77 76
      if (!parsed.success) {
78 -
        return res.status(400).json({
79 -
          errors: parsed.error!.issues,
80 -
        });
77 +
        return res.status(400).json(formatValidationError(parsed.error!));
81 78
      }
82 79

83 80
      const result = await requestPasswordReset(parsed.data);
84 81
      return res.status(200).json(result);
85 82
    } catch (err) {
▸ 4 unchanged lines
90 87
  resetPassword: async (req: Request, res: Response, next: NextFunction) => {
91 88
    try {
92 89
      const parsed = PasswordResetSchema.safeParse(req.body);
93 90

94 91
      if (!parsed.success) {
95 -
        return res.status(400).json({
96 -
          errors: parsed.error!.issues,
97 -
        });
92 +
        return res.status(400).json(formatValidationError(parsed.error!));
98 93
      }
99 94

100 95
      const result = await resetPassword(parsed.data);
101 96
      return res.status(200).json(result);
102 97
    } catch (err: any) {
▸ 54 unchanged lines
157 152
  activate: async (req: Request, res: Response, next: NextFunction) => {
158 153
    try {
159 154
      const parsed = ActivateUserSchema.safeParse(req.body);
160 155

161 156
      if (!parsed.success) {
162 -
        return res.status(400).json({
163 -
          errors: parsed.error!.issues,
164 -
        });
157 +
        return res.status(400).json(formatValidationError(parsed.error!));
165 158
      }
166 159

167 160
      const user = await findUserByActivationCode(String(parsed.data.code));
168 161

169 162
      if (!user) {
▸ 43 unchanged lines
213 206
    } catch (err: any) {
214 207
      next(err);
215 208
    }
216 209
  },
217 210
};
api/src/controllers/careActions.ts
1 1
import { Response, NextFunction } from 'express';
2 2
import { AuthenticatedRequest } from '@app/middleware/auth';
3 3
import { CareActionType } from '@prisma/client';
4 4
import { CreateCareActionSchema, PatchCareActionSchema } from '@app/schemas';
5 +
import { formatValidationError } from '@app/lib/errors/validationError';
5 6
import {
6 7
  getCareActionsByUserId,
7 8
  getCareActionById,
8 9
  createCareAction,
9 10
  destroyCareActionById,
▸ 40 unchanged lines
50 51

51 52
      req.log.info("Params: %s", req.body.careAction)
52 53
      req.log.info("Care action: %s", parsed.error)
53 54

54 55
      if (!parsed.success) {
55 -
        return res.status(400).json({ errors: parsed.error!.issues });
56 +
        return res.status(400).json(formatValidationError(parsed.error!));
56 57
      }
57 58

58 59
      const careAction = await createCareAction(req.userId!, parsed.data);
59 60

60 61
      return res.status(201).json({ careAction });
▸ 15 unchanged lines
76 77
  patch: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
77 78
    try {
78 79
      const parsed = PatchCareActionSchema.safeParse(req.body.careAction);
79 80

80 81
      if (!parsed.success) {
81 -
        return res.status(400).json({ errors: parsed.error.issues });
82 +
        return res.status(400).json(formatValidationError(parsed.error!));
82 83
      }
83 84

84 85
      const careAction = await patchCareActionById(
85 86
        req.userId!,
86 87
        Number(req.params.id),
▸ 8 unchanged lines
95 96
    } catch (err) {
96 97
      next(err);
97 98
    }
98 99
  },
99 100
};
api/src/controllers/insights.ts
1 1
import { Response, NextFunction } from "express";
2 2
import { AuthenticatedRequest } from "@app/middleware/auth";
3 3
import { getInsightsByUserId } from "@app/services/insight.service";
4 4
import { InsightsQuerySchema } from "@app/schemas";
5 +
import { formatValidationError } from "@app/lib/errors/validationError";
5 6

6 7
export const InsightsController = {
7 8
  index: async (
8 9
    req: AuthenticatedRequest,
9 10
    res: Response,
▸ 1 unchanged lines
11 12
  ) => {
12 13
    try {
13 14
      const parsedQuery = InsightsQuerySchema.safeParse(req.query);
14 15

15 16
      if (!parsedQuery.success) {
16 -
        return res.status(400).json({
17 -
          message: "Invalid query parameters",
18 -
          errors: parsedQuery.error.flatten(),
19 -
        });
17 +
        return res.status(400).json(formatValidationError(parsedQuery.error));
20 18
      }
21 19

22 20
      const insights = await getInsightsByUserId(req.userId!, {
23 21
        type: parsedQuery.data.type,
24 22
        period: parsedQuery.data.period,
▸ 4 unchanged lines
29 27
    } catch (err) {
30 28
      next(err);
31 29
    }
32 30
  },
33 31
};
api/src/controllers/medicineRegimens.ts
1 1
import { Response, NextFunction } from 'express';
2 2
import { AuthenticatedRequest } from '@app/middleware/auth';
3 3
import { CreateMedicineRegimenSchema, UpdateMedicineRegimenSchema } from '@app/schemas';
4 4
import { ToggleMedicineRegimenSchema } from '@app/schemas/medicineRegimen.schema';
5 +
import { formatValidationError } from '@app/lib/errors/validationError';
5 6
import {
6 7
  getMedicineRegimensByUserId,
7 8
  getMedicineRegimenById,
8 9
  createMedicineRegimen,
9 10
  updateMedicineRegimen,
▸ 38 unchanged lines
48 49
  create: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
49 50
    try {
50 51
      const parsed = CreateMedicineRegimenSchema.safeParse(req.body.regimen);
51 52

52 53
      if (!parsed.success) {
53 -
        return res.status(400).json({ errors: parsed.error!.issues });
54 +
        return res.status(400).json(formatValidationError(parsed.error!));
54 55
      }
55 56

56 57
      const regimen = await createMedicineRegimen(req.userId!, parsed.data);
57 58

58 59
      return res.status(201).json({ regimen });
▸ 5 unchanged lines
64 65
  update: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
65 66
    try {
66 67
      const parsed = UpdateMedicineRegimenSchema.safeParse(req.body.regimen);
67 68

68 69
      if (!parsed.success) {
69 -
        return res.status(400).json({ errors: parsed.error!.issues });
70 +
        return res.status(400).json(formatValidationError(parsed.error!));
70 71
      }
71 72

72 73
      const regimen = await updateMedicineRegimen(
73 74
        req.userId!,
74 75
        Number(req.params.id),
▸ 9 unchanged lines
84 85
  toggle: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
85 86
    try {
86 87
      const parsed = ToggleMedicineRegimenSchema.safeParse(req.body);
87 88

88 89
      if (!parsed.success) {
89 -
        return res.status(400).json({ errors: parsed.error!.issues });
90 +
        return res.status(400).json(formatValidationError(parsed.error!));
90 91
      }
91 92

92 93
      const regimen = await toggleMedicineRegimen(
93 94
        req.userId!,
94 95
        Number(req.params.id),
▸ 14 unchanged lines
109 110
    } catch (err) {
110 111
      next(err);
111 112
    }
112 113
  },
113 114
};
api/src/controllers/moods.ts
1 1
import { Request, Response, NextFunction } from 'express';
2 2
import { AuthenticatedRequest } from '@app/middleware/auth';
3 3
import { User } from '@prisma/client';
4 4
import {
5 5
  createMood,
▸ 6 unchanged lines
12 12
import { sendPushNotification } from '@app/lib/sendPushNotification';
13 13

14 14
import {
15 15
  CreateMoodSchema
16 16
} from '@app/schemas'
17 +
import { formatValidationError } from '@app/lib/errors/validationError'
17 18

18 19
export const MoodsController = {
19 20
  create: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
20 21
    try {
21 22
      const parsed = CreateMoodSchema.safeParse(req.body.mood);
22 23

23 24
      if (!parsed.success) {
24 -
        return res.status(400).json({
25 -
          errors: parsed.error!.issues
26 -
        });
25 +
        return res.status(400).json(formatValidationError(parsed.error!));
27 26
      }
28 27

29 28
      const mood = await createMood(req.userId!, parsed.data);
30 29

31 30
      return res.status(201).json({ mood });
▸ 68 unchanged lines
100 99
    } catch (err) {
101 100
      next(err);
102 101
    }
103 102
  }
104 103
};
api/src/controllers/sleepRecords.ts
1 1
import { Request, Response, NextFunction } from 'express';
2 2
import { AuthenticatedRequest } from '@app/middleware/auth';
3 3
import {
4 4
  createSleepRecord,
5 5
  getSleepRecordsByUserId,
▸ 4 unchanged lines
10 10

11 11
import {
12 12
  CreateSleepRecordSchema,
13 13
  UpdateSleepRecordSchema
14 14
} from '@app/schemas'
15 +
import { formatValidationError } from '@app/lib/errors/validationError'
15 16

16 17
export const SleepRecordsController = {
17 18
  create: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
18 19
    try {
19 20
      const parsed = CreateSleepRecordSchema.safeParse(req.body.sleepRecord);
20 21

21 22
      if (!parsed.success) {
22 -
        return res.status(400).json({
23 -
          errors: parsed.error!.issues
24 -
        });
23 +
        return res.status(400).json(formatValidationError(parsed.error!));
25 24
      }
26 25

27 26
      const sleepRecord = await createSleepRecord(req.userId!, parsed.data);
28 27

29 28
      return res.status(201).json({ sleepRecord });
▸ 42 unchanged lines
72 71
  update: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
73 72
    try {
74 73
      const parsed = UpdateSleepRecordSchema.safeParse(req.body.sleepRecord);
75 74

76 75
      if (!parsed.success) {
77 -
        return res.status(400).json({
78 -
          errors: parsed.error!.issues
79 -
        });
76 +
        return res.status(400).json(formatValidationError(parsed.error!));
80 77
      }
81 78

82 79
      const sleepRecord = await updateSleepRecordById(req.userId!, Number(req.params.id!), parsed.data);
83 80

84 81
      return res.status(200).json({ sleepRecord });
85 82
    } catch (err) {
86 83
      next(err);
87 84
    }
88 85
  }
89 86
};
api/src/controllers/triggerMoodLinks.ts
1 1
import { Response, NextFunction } from 'express';
2 2
import { AuthenticatedRequest } from '@app/middleware/auth';
3 3
import { LinkMoodSchema } from '@app/schemas';
4 +
import { formatValidationError } from '@app/lib/errors/validationError';
4 5
import {
5 6
  linkTriggerToMood,
6 7
  unlinkTriggerFromMood,
7 8
} from '@app/services/triggerMoodLink.service';
8 9

▸ 1 unchanged lines
10 11
  linkMood: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
11 12
    try {
12 13
      const parsed = LinkMoodSchema.safeParse(req.body);
13 14

14 15
      if (!parsed.success) {
15 -
        return res.status(400).json({ errors: parsed.error!.issues });
16 +
        return res.status(400).json(formatValidationError(parsed.error!));
16 17
      }
17 18

18 19
      const triggerId = Number(req.params.triggerId);
19 20
      const { moodId, perceivedImpact } = parsed.data;
20 21

▸ 16 unchanged lines
37 38
    } catch (err) {
38 39
      next(err);
39 40
    }
40 41
  },
41 42
};
api/src/controllers/triggers.ts
1 1
import { Request, Response, NextFunction } from 'express';
2 2
import { AuthenticatedRequest } from '@app/middleware/auth';
3 3
import {
4 4
  createTrigger,
5 5
  getTriggersByUserId,
▸ 9 unchanged lines
15 15
import {
16 16
  CreateTriggerSchema,
17 17
  UpdateTriggerSchema,
18 18
  LinkMoodSchema,
19 19
} from '@app/schemas'
20 +
import { formatValidationError } from '@app/lib/errors/validationError'
20 21

21 22
export const TriggersController = {
22 23
  create: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
23 24
    try {
24 25
      const parsed = CreateTriggerSchema.safeParse(req.body.trigger);
25 26

26 27
      if (!parsed.success) {
27 -
        return res.status(400).json({
28 -
          errors: parsed.error!.issues
29 -
        });
28 +
        return res.status(400).json(formatValidationError(parsed.error!));
30 29
      }
31 30

32 31
      const trigger = await createTrigger(req.userId!, parsed.data);
33 32

34 33
      return res.status(201).json({ trigger });
▸ 42 unchanged lines
77 76
  update: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
78 77
    try {
79 78
      const parsed = UpdateTriggerSchema.safeParse(req.body.trigger);
80 79

81 80
      if (!parsed.success) {
82 -
        return res.status(400).json({
83 -
          errors: parsed.error!.issues
84 -
        });
81 +
        return res.status(400).json(formatValidationError(parsed.error!));
85 82
      }
86 83

87 84
      const trigger = await updateTriggerById(req.userId!, Number(req.params.id!), parsed.data);
88 85

89 86
      return res.status(200).json({ trigger });
▸ 5 unchanged lines
95 92
  linkMood: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
96 93
    try {
97 94
      const parsed = LinkMoodSchema.safeParse(req.body);
98 95

99 96
      if (!parsed.success) {
100 -
        return res.status(400).json({ errors: parsed.error!.issues });
97 +
        return res.status(400).json(formatValidationError(parsed.error!));
101 98
      }
102 99

103 100
      const triggerId = Number(req.params.triggerId);
104 101
      const { moodId, perceivedImpact } = parsed.data;
105 102

▸ 16 unchanged lines
122 119
    } catch (err) {
123 120
      next(err);
124 121
    }
125 122
  },
126 123
};
api/src/controllers/users.ts
1 1
import { Request, Response, NextFunction } from 'express';
2 2
import {
3 3
  generateToken
4 4
} from '@app/services/auth.service';
5 5

▸ 2 unchanged lines
8 8
import {
9 9
  CreateUserSchema,
10 10
  UpdateUserSchema,
11 11
  UpdateUserPreferencesSchema,
12 12
} from '@app/schemas';
13 +
import { formatValidationError } from '@app/lib/errors/validationError';
13 14
import { syncDailyReminderJob } from '@app/services/dailyReminder.sync';
14 15

15 16
import {
16 17
  createUser,
17 18
  findUserById,
▸ 24 unchanged lines
42 43
  create: async (req: Request, res: Response, next: NextFunction) => {
43 44
    try {
44 45
      const parsed = CreateUserSchema.safeParse(req.body.user);
45 46

46 47
      if (!parsed.success) {
47 -
        return res.status(422).json({
48 -
          errors: parsed.error!.issues
49 -
        })
48 +
        return res.status(400).json(formatValidationError(parsed.error!));
50 49
      }
51 50

52 51
      const user = await createUser(parsed.data);
53 52
      const jwtToken = generateToken(user.id, user.email);
54 53

▸ 21 unchanged lines
76 75
    try {
77 76
      const { id } = req.params;
78 77

79 78
      const parsed = UpdateUserSchema.safeParse(req.body.user);
80 79
      if (!parsed.success) {
81 -
        return res.status(400).json({
82 -
          errors: parsed.error!.issues
83 -
        });
80 +
        return res.status(400).json(formatValidationError(parsed.error!));
84 81
      }
85 82

86 83
      const user = await findUserById(Number(id));
87 84

88 85
      if (!user) {
▸ 52 unchanged lines
141 138

142 139
  updatePreferences: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
143 140
    try {
144 141
      const parsed = UpdateUserPreferencesSchema.safeParse(req.body.user);
145 142
      if (!parsed.success) {
146 -
        return res.status(400).json({ errors: parsed.error!.issues });
143 +
        return res.status(400).json(formatValidationError(parsed.error!));
147 144
      }
148 145

149 146
      const user = await prisma.user.update({
150 147
        where: { id: req.userId },
151 148
        data: parsed.data,
▸ 59 unchanged lines
211 208
    return res.json({
212 209
      user
213 210
    })
214 211
  }
215 212
}
api/src/lib/errors/validationError.ts
@@ -0,0 +1,16 @@
1 + import { ZodError } from 'zod';
2 + import { fromZodError } from 'zod-validation-error';
3 +
4 + export function formatValidationError(err: ZodError) {
5 + const fields: Record<string, string> = {};
6 +
7 + for (const issue of err.issues) {
8 + const key = issue.path.join('.') || '_form';
9 + if (!fields[key]) fields[key] = issue.message;
10 + }
11 +
12 + return {
13 + error: fromZodError(err).message,
14 + fields,
15 + };
16 + }
api/tests/moods/create.test.ts
1 1
import { describe, it, expect, afterAll, beforeEach } from 'vitest'
2 2
import request from 'supertest'
3 3
import { createApp } from '@app/createApp'
4 4
import { buildPrisma, cleanupTestDb, createTestUser, generateTestToken } from '../test-helper'
5 5

▸ 39 unchanged lines
45 45
      .post('/moods')
46 46
      .set('Authorization', `Bearer ${token}`)
47 47
      .send({ mood: { selectedMood: 'GOOD' } })
48 48

49 49
    expect(res.status).toBe(400)
50 -
    expect(res.body.errors).toBeTruthy()
50 +
    expect(res.body.error).toBeTruthy()
51 +
    expect(res.body.fields).toBeTruthy()
51 52
  })
52 53

53 54
  it('returns 401 without token', async () => {
54 55
    const res = await request(app).post('/moods').send({ mood: validMood })
55 56
    expect(res.status).toBe(401)
56 57
  })
57 58
})
api/tests/users/create.test.ts
1 1
import { describe, it, expect, afterAll, beforeEach } from 'vitest'
2 2
import request from 'supertest'
3 3
import { createApp } from '@app/createApp'
4 4
import { buildPrisma, cleanupTestDb } from '../test-helper'
5 5

▸ 27 unchanged lines
33 33
      .send({ user: { email: 'dup@example.com', firstName: 'B', password: 'senha123' } })
34 34

35 35
    expect(res.status).toBe(409)
36 36
  })
37 37

38 -
  it('returns 422 for missing required fields', async () => {
38 +
  it('returns 400 for missing required fields', async () => {
39 39
    const res = await request(app)
40 40
      .post('/users')
41 41
      .send({ user: { email: 'x@example.com' } })
42 42

43 -
    expect(res.status).toBe(422)
44 -
    expect(res.body.errors).toBeTruthy()
43 +
    expect(res.status).toBe(400)
44 +
    expect(res.body.error).toBeTruthy()
45 +
    expect(res.body.fields).toBeTruthy()
45 46
  })
46 47
})