-
Notifications
You must be signed in to change notification settings - Fork 0
implement email service with zod and nodemailer #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| export const EMAIL_TYPE = { | ||
| ATTENDANCE_UPDATE: 'ATTENDANCE_UPDATE', | ||
| } as const; | ||
|
|
||
| export const ATTENDANCE_STATUS = { | ||
| COMING: 'COMING', | ||
| NOT_COMING: 'NOT_COMING', | ||
| } as const; | ||
|
|
||
| export const EMAIL_SUBJECTS = { | ||
| ATTENDANCE_COMING: 'אישור הגעה לטיפול', | ||
| ATTENDANCE_NOT_COMING: 'ביטול הגעה לטיפול', | ||
| } as const; | ||
|
|
||
| export const EMAIL_TEXT = { | ||
| dearCaregiver: 'מטפל יקר', | ||
| patientNumber: 'מספר מטופל', | ||
| willCome: 'יגיע לטיפול', | ||
| willNotCome: 'לא יוכל להגיע לטיפול', | ||
| treatmentTime: 'בשעה', | ||
| cell: 'תא', | ||
| building: 'בניין', | ||
| rescheduleMessage: | ||
| 'אנא פנה למטופל לקביעת מועד חדש ועדכן במערכת כי התפנה כיסא.', | ||
| automaticMessage: 'הודעה זו נשלחה אוטומטית ממערכת Sagol360.', | ||
| patientNameLabel: 'שם מטופל', | ||
| patientNumberLabel: 'מספר מטופל', | ||
| attendanceStatusLabel: 'סטטוס הגעה', | ||
| timeLabel: 'שעה', | ||
| cellLabel: 'תא', | ||
| buildingLabel: 'בניין', | ||
| comingStatusLabel: 'מגיע לטיפול', | ||
| notComingStatusLabel: 'לא מגיע לטיפול', | ||
| } as const; | ||
|
|
||
| export const EMAIL_ERRORS = { | ||
| invalidEmailData: 'Invalid email data', | ||
| unsupportedEmailType: 'Unsupported email type', | ||
| missingEmailEnvironmentVariables: 'Missing email environment variables', | ||
| emailWasNotSent: 'Email was not sent', | ||
| } as const; | ||
|
|
||
| export const EMAIL_SUCCESS_MESSAGES = { | ||
| emailSentSuccessfully: 'Email sent successfully', | ||
| } as const; | ||
|
|
||
| export const EMAIL_COLORS = { | ||
| comingHeader: '#1f7a4d', | ||
| notComingHeader: '#9f2f2f', | ||
| background: '#f4f6f8', | ||
| cardBackground: '#ffffff', | ||
| border: '#e0e0e0', | ||
| text: '#222222', | ||
| mutedText: '#777777', | ||
| tableLabelBackground: '#f8f9fa', | ||
| } as const; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import { Request, Response } from 'express'; | ||
| import { StatusCodes } from 'http-status-codes'; | ||
| import { sendEmail as serviceSendEmail } from '@/services/email/email.service'; | ||
| import { EMAIL_ERRORS, EMAIL_SUCCESS_MESSAGES } from '@/constants/email/email.constants'; | ||
|
|
||
| export const sendEmail = async (req: Request, res: Response) => { | ||
| try { | ||
| await serviceSendEmail(req.body); | ||
|
|
||
| res.status(StatusCodes.OK).json({ | ||
| message: EMAIL_SUCCESS_MESSAGES.emailSentSuccessfully, | ||
| }); | ||
| } catch (error) { | ||
| const errorMessage = error instanceof Error ? error.message : EMAIL_ERRORS.emailWasNotSent; | ||
|
|
||
| const statusCode = | ||
| errorMessage === EMAIL_ERRORS.invalidEmailData || | ||
| errorMessage === EMAIL_ERRORS.unsupportedEmailType | ||
| ? StatusCodes.BAD_REQUEST | ||
| : StatusCodes.INTERNAL_SERVER_ERROR; | ||
|
|
||
| res.status(statusCode).json({ | ||
| message: errorMessage, | ||
| }); | ||
| } | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| import { sendEmail } from '@/controllers/email/email.controller'; | ||
| import { Router } from 'express'; | ||
|
|
||
| const router = Router(); | ||
|
|
||
| /** | ||
| * @swagger | ||
| * /api/email: | ||
| * post: | ||
| * summary: Send an email | ||
| * description: Sends an attendance update email according to the provided email type and payload. | ||
| * tags: | ||
| * requestBody: | ||
| * required: true | ||
| * content: | ||
| * application/json: | ||
| * schema: | ||
| * type: object | ||
| * required: | ||
| * - emailType | ||
| * - payload | ||
| * properties: | ||
| * emailType: | ||
| * type: string | ||
| * enum: | ||
| * - ATTENDANCE_UPDATE | ||
| * example: ATTENDANCE_UPDATE | ||
| * email: | ||
| * type: string | ||
| * format: email | ||
| * example: secretary@example.com | ||
| * payload: | ||
| * type: object | ||
| * required: | ||
| * - patientName | ||
| * - patientNumber | ||
| * - attendanceStatus | ||
| * - time | ||
| * - cell | ||
| * - building | ||
| * properties: | ||
| * patientName: | ||
| * type: string | ||
| * example: ישראל ישראלי | ||
| * patientNumber: | ||
| * type: string | ||
| * example: "123456" | ||
| * attendanceStatus: | ||
| * type: string | ||
| * enum: | ||
| * - COMING | ||
| * - NOT_COMING | ||
| * example: NOT_COMING | ||
| * time: | ||
| * type: string | ||
| * example: "18:00" | ||
| * cell: | ||
| * type: string | ||
| * example: כתום | ||
| * building: | ||
| * type: string | ||
| * example: אריסון | ||
| * responses: | ||
| * 200: | ||
| * description: Email sent successfully | ||
| * content: | ||
| * application/json: | ||
| * schema: | ||
| * type: object | ||
| * properties: | ||
| * message: | ||
| * type: string | ||
| * example: Email sent successfully | ||
| * 400: | ||
| * description: Invalid email data or unsupported email type | ||
| * content: | ||
| * application/json: | ||
| * schema: | ||
| * type: object | ||
| * properties: | ||
| * message: | ||
| * type: string | ||
| * example: Invalid email data | ||
| * 500: | ||
| * description: Email was not sent or server email configuration is missing | ||
| * content: | ||
| * application/json: | ||
| * schema: | ||
| * type: object | ||
| * properties: | ||
| * message: | ||
| * type: string | ||
| * example: Email was not sent | ||
| */ | ||
| router.post('/', sendEmail); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Protect this endpoint before exposing it publicly.
🤖 Prompt for AI Agents |
||
|
|
||
| export default router; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import { ATTENDANCE_STATUS, EMAIL_TYPE } from '@/constants/email/email.constants'; | ||
| import { sendEmailSchema } from '@/services/email/email.service'; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Verify whether TS config preserves non-type imports and confirm dependency cycle edges.
fd -i 'tsconfig*.json' .
rg -n --no-heading -e 'verbatimModuleSyntax|preserveValueImports|importsNotUsedAsValues' $(fd -i 'tsconfig*.json' .)
# Confirm cross-imports between schema and service files.
rg -n --no-heading -e "from '`@/services/email/email.service`'" -e "from '`@/schemas/email/attendance-email.schema`'" src/services/email/email.service.ts src/schemas/email/attendance-email.schema.tsRepository: ColmanDevClubORG/Sagol360ManagementServer Length of output: 333 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "=== tsconfig.json (relevant compilerOptions) ==="
cat -n tsconfig.json | sed -n '1,220p' | rg -n "compilerOptions|verbatimModuleSyntax|preserveValueImports|importsNotUsedAsValues|module|target|moduleResolution|esModuleInterop|isolatedModules|useDefineForClassFields" || true
echo
echo "=== attendance-email.schema.ts (imports + SendEmailData) ==="
nl -ba src/schemas/email/attendance-email.schema.ts | sed -n '1,120p'
echo
echo "=== email.service.ts (imports + attendanceEmailSchema usage) ==="
nl -ba src/services/email/email.service.ts | sed -n '1,160p'Repository: ColmanDevClubORG/Sagol360ManagementServer Length of output: 440 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "=== tsconfig.json (compilerOptions excerpt) ==="
# Print the compilerOptions block only (small)
python3 - <<'PY'
import json
p='tsconfig.json'
with open(p,'r',encoding='utf-8') as f: data=json.load(f)
co=data.get('compilerOptions',{})
keys=[
'verbatimModuleSyntax',
'preserveValueImports',
'importHelpers',
'importsNotUsedAsValues',
'isolatedModules',
'module',
'moduleResolution',
'target',
'esModuleInterop',
'useUnknownInCatchVariables',
'strict',
'requireEmitForEachFunction',
'importsNotUsedAsValues'
]
print(json.dumps({k:co.get(k) for k in keys if k in co or k in ['module','moduleResolution','target','esModuleInterop']},indent=2))
PY
echo
echo "=== attendance-email.schema.ts (first 80 lines) ==="
cat -n src/schemas/email/attendance-email.schema.ts | sed -n '1,120p'
echo
echo "=== email.service.ts (first 140 lines) ==="
cat -n src/services/email/email.service.ts | sed -n '1,160p'Repository: ColmanDevClubORG/Sagol360ManagementServer Length of output: 3089 Remove the schema↔service import coupling by deriving
Suggested change import { ATTENDANCE_STATUS, EMAIL_TYPE } from '`@/constants/email/email.constants`';
-import { sendEmailSchema } from '`@/services/email/email.service`';
import { z } from 'zod';
@@
-export type SendEmailData = z.infer<typeof sendEmailSchema>;
+export type SendEmailData = z.infer<typeof attendanceEmailSchema>;🤖 Prompt for AI Agents |
||
| import { z } from 'zod'; | ||
|
|
||
| export const attendanceEmailSchema = z.object({ | ||
| emailType: z.literal(EMAIL_TYPE.ATTENDANCE_UPDATE), | ||
| email: z.string().trim().pipe(z.email()), | ||
| payload: z.object({ | ||
| patientName: z.string().trim().min(1), | ||
| patientNumber: z.string().trim().min(1), | ||
| attendanceStatus: z.enum([ATTENDANCE_STATUS.COMING, ATTENDANCE_STATUS.NOT_COMING]), | ||
| time: z.string().trim().min(1), | ||
| cell: z.string().trim().min(1), | ||
| building: z.string().trim().min(1), | ||
| }), | ||
| }); | ||
|
|
||
| export type SendEmailData = z.infer<typeof sendEmailSchema>; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import { buildAttendanceEmailContent, EmailContent } from '@/services/email/email.service.helpers'; | ||
| import { SendEmailData } from './attendance-email.schema'; | ||
| import { EMAIL_ERRORS, EMAIL_TYPE } from '@/constants/email/email.constants'; | ||
|
|
||
| export const buildEmailContent = (data: SendEmailData): EmailContent => { | ||
| switch (data.emailType) { | ||
| case EMAIL_TYPE.ATTENDANCE_UPDATE: | ||
| return buildAttendanceEmailContent(data.payload); | ||
|
|
||
| default: | ||
| throw new Error(EMAIL_ERRORS.unsupportedEmailType); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Run Prettier on this file before merge.
These spots already trip
prettier/prettier, so lint will stay red until the wrapped string and trailing newline are normalized.Also applies to: 56-56
🧰 Tools
🪛 ESLint
[error] 23-24: Delete
⏎···(prettier/prettier)
🤖 Prompt for AI Agents