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 { Application } from "express"
import { Resource } from "express-automatic-routes"
......
import { Req, Res } from "#interface/IApi";
import { Req, Res } from "#interfaces/IApi";
import { authMiddleware } from "#middlewares/authentication";
import { AuthService } from "#services/authService";
import { Application } from "express";
......
import { Req, Res } from "#interface/IApi";
import { Req, Res } from "#interfaces/IApi";
import { authMiddleware } from "#middlewares/authentication";
import { AuthService } from "#services/authService";
import { Application } from "express";
......
import { Req, Res } from "#interface/IApi";
import { Req, Res } from "#interfaces/IApi";
import { AuthService } from "#services/authService";
import { Application } from "express";
import { Resource } from "express-automatic-routes";
......
import { Req, Res } from "#interface/IApi";
import { Req, Res } from "#interfaces/IApi";
import { authMiddleware } from "#middlewares/authentication";
import { AuthService } from "#services/authService.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 { AuthService } from "#services/authService.js";
import { Application } from "express";
......
import type { Application } from "express";
import type { Resource } from "express-automatic-routes";
import { ClassesProvider } from "#providers/ClassesProvider";
import { Req, Res } from "#interface/IApi";
import { Req, Res } from "#interfaces/IApi";
import queryModifier from "#middlewares/request";
import { authorize } from "#middlewares/authorization";
import { authMiddleware } from "#middlewares/authentication";
......@@ -28,7 +28,7 @@ export default (_express: Application) => {
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/ClassListResponse"
* $ref: "#/components/schemas/ClassListResponse"
*/
get: {
middleware: [queryModifier, authMiddleware, authorize('admin', 'instructor')],
......
import type { Application } from "express";
import type { Resource } from "express-automatic-routes";
import { ClassesProvider } from "#providers/ClassesProvider.js";
import { Req, Res } from "#interface/IApi";
import { Req, Res } from "#interfaces/IApi";
import { authorize } from "#middlewares/authorization";
import queryModifier from "#middlewares/request";
import { authMiddleware } from "#middlewares/authentication";
......@@ -72,7 +72,6 @@ export default (_express: Application) => {
* required: true
* schema:
* type: string
*
* requestBody:
* required: true
* 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 { Resource } from "express-automatic-routes";
import { CoursesProvider } from "#providers/CoursesProvider.js";
import { Req, Res } from "#interface/IApi";
import { Req, Res } from "#interfaces/IApi";
import queryModifier from "#middlewares/request";
import { authorize } from "#middlewares/authorization";
import { authMiddleware } from "#middlewares/authentication";
......
import type { Application } from "express";
import type { Resource } from "express-automatic-routes";
import { EnrollProvider } from "#providers/EnrollProvider.js";
import { Req, Res } from "#interface/IApi";
import { Req, Res } from "#interfaces/IApi";
import queryModifier from "#middlewares/request";
export default (_express: Application) => {
......
......@@ -3,7 +3,7 @@ import type { Resource } from "express-automatic-routes";
import { EnrollProvider } from "#providers/EnrollProvider.js";
import { authorize } from "#middlewares/authorization";
import { authMiddleware } from "#middlewares/authentication";
import { Req, Res } from "#interface/IApi";
import { Req, Res } from "#interfaces/IApi";
import queryModifier from "#middlewares/request";
export default (_express: Application) => {
......
import type { Application } from "express";
import type { Resource } from "express-automatic-routes";
import { EnrollProvider } from "#providers/EnrollProvider.js";
import { authorize } from "#middlewares/authorization";
import { Req, Res } from "#interface/IApi";
import { Req, Res } from "#interfaces/IApi";
import queryModifier from "#middlewares/request";
export default (_express: Application) => {
......
import { Req, Res } from "#interface/IApi";
import { Req, Res } from "#interfaces/IApi";
import { RolesProvider } from "#providers/RolesProvider.js";
import { Application } from "express";
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 { Application } from "express";
import { Resource } from "express-automatic-routes";
......
......@@ -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": {
"type": "object",
"example": {
......@@ -430,14 +518,14 @@
"LoginInput": {
"type": "object",
"example": {
"email": "phamquangbao@example.com",
"email": "baobuibam2003@gmail.com",
"password": "123456"
},
"properties": {
"email": {
"type": "string",
"format": "email",
"example": "phamquangbao@example.com"
"example": "baobuibam2003@gmail.com"
},
"password": {
"type": "string",
......@@ -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": {
"get": {
"tags": [
......
import { payload } from '#interfaces/IApi';
import { models } from '#models/sequelize-config.js';
import { models, sequelize } from '#models/sequelize-config.js';
interface CreateClassInput {
name: string;
......@@ -59,4 +59,43 @@ export class ClassesProvider {
});
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 = {
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;
\ No newline at end of file
......@@ -15,14 +15,14 @@ const loginSchemas = {
LoginInput: {
type: 'object',
example: {
email: 'phamquangbao@example.com',
email: 'baobuibam2003@gmail.com',
password: '123456',
},
properties: {
email: {
type: 'string',
format: 'email',
example: 'phamquangbao@example.com',
example: 'baobuibam2003@gmail.com',
},
password: {
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