Commit 7c00ef84 authored by Phạm Quang Bảo's avatar Phạm Quang Bảo

feat(challenge_11): add api bulk enroll with ACID transaction

parent 8614f2ba
import { Req, Res } from "#interface/IApi"; import { Req, Res } from "#interfaces/IApi";
import { AuthService } from "#services/authService"; import { AuthService } from "#services/authService";
import { Application } from "express" import { Application } from "express"
import { Resource } from "express-automatic-routes" import { Resource } from "express-automatic-routes"
......
import { Req, Res } from "#interface/IApi"; import { Req, Res } from "#interfaces/IApi";
import { authMiddleware } from "#middlewares/authentication"; import { authMiddleware } from "#middlewares/authentication";
import { AuthService } from "#services/authService"; import { AuthService } from "#services/authService";
import { Application } from "express"; import { Application } from "express";
......
import { Req, Res } from "#interface/IApi"; import { Req, Res } from "#interfaces/IApi";
import { authMiddleware } from "#middlewares/authentication"; import { authMiddleware } from "#middlewares/authentication";
import { AuthService } from "#services/authService"; import { AuthService } from "#services/authService";
import { Application } from "express"; import { Application } from "express";
......
import { Req, Res } from "#interface/IApi"; import { Req, Res } from "#interfaces/IApi";
import { AuthService } from "#services/authService"; import { AuthService } from "#services/authService";
import { Application } from "express"; import { Application } from "express";
import { Resource } from "express-automatic-routes"; import { Resource } from "express-automatic-routes";
......
import { Req, Res } from "#interface/IApi"; import { Req, Res } from "#interfaces/IApi";
import { authMiddleware } from "#middlewares/authentication"; import { authMiddleware } from "#middlewares/authentication";
import { AuthService } from "#services/authService.js"; import { AuthService } from "#services/authService.js";
import { MailService } from "#services/mailService.js"; import { MailService } from "#services/mailService.js";
......
import { Req, Res } from "#interface/IApi"; import { Req, Res } from "#interfaces/IApi";
import { authMiddleware } from "#middlewares/authentication"; import { authMiddleware } from "#middlewares/authentication";
import { AuthService } from "#services/authService.js"; import { AuthService } from "#services/authService.js";
import { Application } from "express"; import { Application } from "express";
......
import type { Application } from "express"; import type { Application } from "express";
import type { Resource } from "express-automatic-routes"; import type { Resource } from "express-automatic-routes";
import { ClassesProvider } from "#providers/ClassesProvider"; import { ClassesProvider } from "#providers/ClassesProvider";
import { Req, Res } from "#interface/IApi"; import { Req, Res } from "#interfaces/IApi";
import queryModifier from "#middlewares/request"; import queryModifier from "#middlewares/request";
import { authorize } from "#middlewares/authorization"; import { authorize } from "#middlewares/authorization";
import { authMiddleware } from "#middlewares/authentication"; import { authMiddleware } from "#middlewares/authentication";
......
import type { Application } from "express"; import type { Application } from "express";
import type { Resource } from "express-automatic-routes"; import type { Resource } from "express-automatic-routes";
import { ClassesProvider } from "#providers/ClassesProvider.js"; import { ClassesProvider } from "#providers/ClassesProvider.js";
import { Req, Res } from "#interface/IApi"; import { Req, Res } from "#interfaces/IApi";
import { authorize } from "#middlewares/authorization"; import { authorize } from "#middlewares/authorization";
import queryModifier from "#middlewares/request"; import queryModifier from "#middlewares/request";
import { authMiddleware } from "#middlewares/authentication"; import { authMiddleware } from "#middlewares/authentication";
...@@ -72,7 +72,6 @@ export default (_express: Application) => { ...@@ -72,7 +72,6 @@ export default (_express: Application) => {
* required: true * required: true
* schema: * schema:
* type: string * type: string
*
* requestBody: * requestBody:
* required: true * required: true
* content: * content:
......
import { Req, Res } from "#interfaces/IApi.js";
import { authMiddleware } from "#middlewares/authentication.js";
import { authorize } from "#middlewares/authorization.js";
import queryModifier from "#middlewares/request.js";
import { ClassesProvider } from "#providers/ClassesProvider.js";
import { Application } from "express";
import { Resource } from "express-automatic-routes";
export default (_express: Application) => {
const classesProvider = new ClassesProvider();
return <Resource>{
/**
* @openapi
* /api/v1.0/classes/{id}/bulk-enroll:
* post:
* tags: [Classes]
* security:
* - bearerAuth: []
* description: Đăng ký hàng loạt học viên
* parameters:
* - name: id
* in: path
* required: true
* schema:
* type: string
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/BulkEnrollInput"
* responses:
* 201:
* description: Đăng ký tham gia lớp học thành công
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/BulkEnrollResponse"
*/
post: {
middleware: [queryModifier, authMiddleware, authorize('admin')],
handler: async (req: Req, res: Res) => {
try {
const { id } = req.params;
if (!id || typeof id !== 'string') {
return res.sendError({
message: "ID không hợp lệ!",
message_en: "Invalid ID!",
status: 400
});
}
const result = await classesProvider.bulkEnrollStudents(id, req.body.student_emails);
return res.sendOk({ data: result });
} catch (error: any) {
console.error('Error enrolling students:', error);
return res.sendError({
message: error.message || 'Đã xảy ra lỗi khi đăng ký tham gia lớp học',
message_en: 'An error occurred while enrolling students in the class',
status: 400
});
}
}
}
}
};
\ No newline at end of file
import type { Application } from "express"; import type { Application } from "express";
import type { Resource } from "express-automatic-routes"; import type { Resource } from "express-automatic-routes";
import { CoursesProvider } from "#providers/CoursesProvider.js"; import { CoursesProvider } from "#providers/CoursesProvider.js";
import { Req, Res } from "#interface/IApi"; import { Req, Res } from "#interfaces/IApi";
import queryModifier from "#middlewares/request"; import queryModifier from "#middlewares/request";
import { authorize } from "#middlewares/authorization"; import { authorize } from "#middlewares/authorization";
import { authMiddleware } from "#middlewares/authentication"; import { authMiddleware } from "#middlewares/authentication";
......
import type { Application } from "express"; import type { Application } from "express";
import type { Resource } from "express-automatic-routes"; import type { Resource } from "express-automatic-routes";
import { EnrollProvider } from "#providers/EnrollProvider.js"; import { EnrollProvider } from "#providers/EnrollProvider.js";
import { Req, Res } from "#interface/IApi"; import { Req, Res } from "#interfaces/IApi";
import queryModifier from "#middlewares/request"; import queryModifier from "#middlewares/request";
export default (_express: Application) => { export default (_express: Application) => {
......
...@@ -3,7 +3,7 @@ import type { Resource } from "express-automatic-routes"; ...@@ -3,7 +3,7 @@ import type { Resource } from "express-automatic-routes";
import { EnrollProvider } from "#providers/EnrollProvider.js"; import { EnrollProvider } from "#providers/EnrollProvider.js";
import { authorize } from "#middlewares/authorization"; import { authorize } from "#middlewares/authorization";
import { authMiddleware } from "#middlewares/authentication"; import { authMiddleware } from "#middlewares/authentication";
import { Req, Res } from "#interface/IApi"; import { Req, Res } from "#interfaces/IApi";
import queryModifier from "#middlewares/request"; import queryModifier from "#middlewares/request";
export default (_express: Application) => { export default (_express: Application) => {
......
import type { Application } from "express"; import type { Application } from "express";
import type { Resource } from "express-automatic-routes"; import type { Resource } from "express-automatic-routes";
import { EnrollProvider } from "#providers/EnrollProvider.js"; import { EnrollProvider } from "#providers/EnrollProvider.js";
import { authorize } from "#middlewares/authorization"; import { Req, Res } from "#interfaces/IApi";
import { Req, Res } from "#interface/IApi";
import queryModifier from "#middlewares/request"; import queryModifier from "#middlewares/request";
export default (_express: Application) => { export default (_express: Application) => {
......
import { Req, Res } from "#interface/IApi"; import { Req, Res } from "#interfaces/IApi";
import { RolesProvider } from "#providers/RolesProvider.js"; import { RolesProvider } from "#providers/RolesProvider.js";
import { Application } from "express"; import { Application } from "express";
import { Resource } from "express-automatic-routes"; import { Resource } from "express-automatic-routes";
......
import { Req, Res } from "#interface/IApi"; import { Req, Res } from "#interfaces/IApi";
import { RolesProvider } from "#providers/RolesProvider.js"; import { RolesProvider } from "#providers/RolesProvider.js";
import { Application } from "express"; import { Application } from "express";
import { Resource } from "express-automatic-routes"; import { Resource } from "express-automatic-routes";
......
...@@ -224,6 +224,94 @@ ...@@ -224,6 +224,94 @@
} }
} }
}, },
"BulkEnrollInput": {
"type": "object",
"required": [
"student_emails"
],
"properties": {
"student_emails": {
"type": "array",
"items": {
"type": "string",
"format": "email"
},
"example": [
"student1@example.com",
"student2@example.com",
"student3@example.com"
]
}
}
},
"BulkEnrollResponse": {
"type": "object",
"properties": {
"message": {
"type": "string",
"nullable": true
},
"message_en": {
"type": "string",
"nullable": true
},
"responseData": {
"type": "object",
"properties": {
"enrolled_students": {
"type": "array",
"items": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email"
},
"status": {
"type": "string"
}
}
},
"example": [
{
"email": "student1@example.com",
"status": "enrolled"
},
{
"email": "student2@example.com",
"status": "enrolled"
}
]
}
}
},
"status": {
"type": "string",
"example": "success"
},
"timeStamp": {
"type": "string",
"example": "2024-02-26 03:12:45"
},
"violations": {
"type": "array",
"items": {
"type": "object",
"properties": {
"code": {
"type": "number"
},
"message": {
"type": "string"
},
"action": {
"nullable": true
}
}
}
}
}
},
"Course": { "Course": {
"type": "object", "type": "object",
"example": { "example": {
...@@ -430,14 +518,14 @@ ...@@ -430,14 +518,14 @@
"LoginInput": { "LoginInput": {
"type": "object", "type": "object",
"example": { "example": {
"email": "phamquangbao@example.com", "email": "baobuibam2003@gmail.com",
"password": "123456" "password": "123456"
}, },
"properties": { "properties": {
"email": { "email": {
"type": "string", "type": "string",
"format": "email", "format": "email",
"example": "phamquangbao@example.com" "example": "baobuibam2003@gmail.com"
}, },
"password": { "password": {
"type": "string", "type": "string",
...@@ -1146,6 +1234,51 @@ ...@@ -1146,6 +1234,51 @@
} }
} }
}, },
"/api/v1.0/classes/{id}/bulk-enroll": {
"post": {
"tags": [
"Classes"
],
"security": [
{
"bearerAuth": []
}
],
"description": "Đăng ký hàng loạt học viên",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BulkEnrollInput"
}
}
}
},
"responses": {
"201": {
"description": "Đăng ký tham gia lớp học thành công",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BulkEnrollResponse"
}
}
}
}
}
}
},
"/api/v1.0/classes": { "/api/v1.0/classes": {
"get": { "get": {
"tags": [ "tags": [
......
import { payload } from '#interfaces/IApi'; import { payload } from '#interfaces/IApi';
import { models } from '#models/sequelize-config.js'; import { models, sequelize } from '#models/sequelize-config.js';
interface CreateClassInput { interface CreateClassInput {
name: string; name: string;
...@@ -59,4 +59,43 @@ export class ClassesProvider { ...@@ -59,4 +59,43 @@ export class ClassesProvider {
}); });
return deletedClass; return deletedClass;
} }
async bulkEnrollStudents(classId: string, student_emails: string[]) {
const result = await sequelize.transaction(async (t) => {
const ids = await models.users.findAll({
where: {
email: student_emails
},
attributes: ['id', 'email'],
raw: true,
transaction: t
});
if (!ids || ids.length !== student_emails.length) {
const foundEmails = ids.map(u => u.email);
const invalidEmails = student_emails.filter(email => !foundEmails.includes(email));
throw new Error(`Đăng ký thất bại. Có ${invalidEmails.length} email không hợp lệ: [${invalidEmails.join(', ')}]`);
}
const enrollmentsRaw = student_emails.map(email => ({
class_id: classId,
user_id: ids.find(u => u.email === email)?.id,
status: 'enrolled',
}));
const enrollments = enrollmentsRaw.filter(
(item): item is { class_id: string; user_id: string; status: string } => item.user_id !== undefined
);
if (enrollments.length === 0) return [];
const result = await models.enrollments.bulkCreate(enrollments, { transaction: t });
return result;
});
return result;
}
} }
\ No newline at end of file
...@@ -181,7 +181,95 @@ const classSchemas = { ...@@ -181,7 +181,95 @@ const classSchemas = {
example: '7c1a4d9c-3a2b-4c58-9df4-8e2c2d9a3c10', example: '7c1a4d9c-3a2b-4c58-9df4-8e2c2d9a3c10',
}, },
}, },
} },
BulkEnrollInput: {
type: 'object',
required: ['student_emails'],
properties: {
student_emails: {
type: 'array',
items: {
type: 'string',
format: 'email',
},
example: [
'student1@example.com',
'student2@example.com',
'student3@example.com',
],
},
},
},
BulkEnrollResponse: {
type: 'object',
properties: {
message: {
type: 'string',
nullable: true,
},
message_en: {
type: 'string',
nullable: true,
},
responseData: {
type: 'object',
properties: {
enrolled_students: {
type: 'array',
items: {
type: 'object',
properties: {
email: {
type: 'string',
format: 'email',
},
status: {
type: 'string',
},
},
},
example: [
{
email: 'student1@example.com',
status: 'enrolled',
},
{
email: 'student2@example.com',
status: 'enrolled',
},
],
},
},
},
status: {
type: 'string',
example: 'success',
},
timeStamp: {
type: 'string',
example: '2024-02-26 03:12:45',
},
violations: {
type: 'array',
items: {
type: 'object',
properties: {
code: {
type: 'number',
},
message: {
type: 'string',
},
action: {
nullable: true,
},
},
},
},
},
},
}; };
export default classSchemas; export default classSchemas;
\ No newline at end of file
...@@ -15,14 +15,14 @@ const loginSchemas = { ...@@ -15,14 +15,14 @@ const loginSchemas = {
LoginInput: { LoginInput: {
type: 'object', type: 'object',
example: { example: {
email: 'phamquangbao@example.com', email: 'baobuibam2003@gmail.com',
password: '123456', password: '123456',
}, },
properties: { properties: {
email: { email: {
type: 'string', type: 'string',
format: 'email', format: 'email',
example: 'phamquangbao@example.com', example: 'baobuibam2003@gmail.com',
}, },
password: { password: {
type: 'string', type: 'string',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment