Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
B
BACKEND CHALLENGES
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Phạm Quang Bảo
BACKEND CHALLENGES
Commits
7c00ef84
Commit
7c00ef84
authored
May 25, 2026
by
Phạm Quang Bảo
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(challenge_11): add api bulk enroll with ACID transaction
parent
8614f2ba
Changes
19
Hide whitespace changes
Inline
Side-by-side
Showing
19 changed files
with
350 additions
and
23 deletions
+350
-23
index.ts
code/src/controllers/api/v1.0/auth/login/index.ts
+1
-1
index.ts
code/src/controllers/api/v1.0/auth/logout/index.ts
+1
-1
index.ts
code/src/controllers/api/v1.0/auth/profile/index.ts
+1
-1
index.ts
code/src/controllers/api/v1.0/auth/register/index.ts
+1
-1
index.ts
code/src/controllers/api/v1.0/auth/send-otp/index.ts
+1
-1
index.ts
code/src/controllers/api/v1.0/auth/verify-otp/index.ts
+1
-1
index.ts
code/src/controllers/api/v1.0/classes/index.ts
+2
-2
{id}.ts
code/src/controllers/api/v1.0/classes/{id}.ts
+1
-2
bulk-enroll.ts
code/src/controllers/api/v1.0/classes/{id}/bulk-enroll.ts
+69
-0
{id}.ts
code/src/controllers/api/v1.0/courses/{id}.ts
+1
-1
index.ts
...ollers/api/v1.0/enrollments/all-student-in-class/index.ts
+1
-1
index.ts
code/src/controllers/api/v1.0/enrollments/enroll/index.ts
+1
-1
index.ts
code/src/controllers/api/v1.0/enrollments/unenroll/index.ts
+1
-2
index.ts
code/src/controllers/api/v1.0/roles/index.ts
+1
-1
index.ts
code/src/controllers/api/v1.0/roles/set-role/index.ts
+1
-1
swagger-output.json
code/src/docs/swagger/swagger-output.json
+135
-2
ClassesProvider.ts
code/src/providers/ClassesProvider.ts
+40
-1
schemas.ts
code/src/templates/swagger/classes/schemas.ts
+89
-1
schemas.ts
code/src/templates/swagger/login/schemas.ts
+2
-2
No files found.
code/src/controllers/api/v1.0/auth/login/index.ts
View file @
7c00ef84
import
{
Req
,
Res
}
from
"#interface/IApi"
;
import
{
Req
,
Res
}
from
"#interface
s
/IApi"
;
import
{
AuthService
}
from
"#services/authService"
;
import
{
Application
}
from
"express"
import
{
Resource
}
from
"express-automatic-routes"
...
...
code/src/controllers/api/v1.0/auth/logout/index.ts
View file @
7c00ef84
import
{
Req
,
Res
}
from
"#interface/IApi"
;
import
{
Req
,
Res
}
from
"#interface
s
/IApi"
;
import
{
authMiddleware
}
from
"#middlewares/authentication"
;
import
{
AuthService
}
from
"#services/authService"
;
import
{
Application
}
from
"express"
;
...
...
code/src/controllers/api/v1.0/auth/profile/index.ts
View file @
7c00ef84
import
{
Req
,
Res
}
from
"#interface/IApi"
;
import
{
Req
,
Res
}
from
"#interface
s
/IApi"
;
import
{
authMiddleware
}
from
"#middlewares/authentication"
;
import
{
AuthService
}
from
"#services/authService"
;
import
{
Application
}
from
"express"
;
...
...
code/src/controllers/api/v1.0/auth/register/index.ts
View file @
7c00ef84
import
{
Req
,
Res
}
from
"#interface/IApi"
;
import
{
Req
,
Res
}
from
"#interface
s
/IApi"
;
import
{
AuthService
}
from
"#services/authService"
;
import
{
Application
}
from
"express"
;
import
{
Resource
}
from
"express-automatic-routes"
;
...
...
code/src/controllers/api/v1.0/auth/send-otp/index.ts
View file @
7c00ef84
import
{
Req
,
Res
}
from
"#interface/IApi"
;
import
{
Req
,
Res
}
from
"#interface
s
/IApi"
;
import
{
authMiddleware
}
from
"#middlewares/authentication"
;
import
{
AuthService
}
from
"#services/authService.js"
;
import
{
MailService
}
from
"#services/mailService.js"
;
...
...
code/src/controllers/api/v1.0/auth/verify-otp/index.ts
View file @
7c00ef84
import
{
Req
,
Res
}
from
"#interface/IApi"
;
import
{
Req
,
Res
}
from
"#interface
s
/IApi"
;
import
{
authMiddleware
}
from
"#middlewares/authentication"
;
import
{
AuthService
}
from
"#services/authService.js"
;
import
{
Application
}
from
"express"
;
...
...
code/src/controllers/api/v1.0/classes/index.ts
View file @
7c00ef84
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
"#interface
s
/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'
)],
...
...
code/src/controllers/api/v1.0/classes/{id}.ts
View file @
7c00ef84
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
"#interface
s
/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:
...
...
code/src/controllers/api/v1.0/classes/{id}/bulk-enroll.ts
0 → 100644
View file @
7c00ef84
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
code/src/controllers/api/v1.0/courses/{id}.ts
View file @
7c00ef84
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
"#interface
s
/IApi"
;
import
queryModifier
from
"#middlewares/request"
;
import
{
authorize
}
from
"#middlewares/authorization"
;
import
{
authMiddleware
}
from
"#middlewares/authentication"
;
...
...
code/src/controllers/api/v1.0/enrollments/all-student-in-class/index.ts
View file @
7c00ef84
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
"#interface
s
/IApi"
;
import
queryModifier
from
"#middlewares/request"
;
export
default
(
_express
:
Application
)
=>
{
...
...
code/src/controllers/api/v1.0/enrollments/enroll/index.ts
View file @
7c00ef84
...
...
@@ -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
"#interface
s
/IApi"
;
import
queryModifier
from
"#middlewares/request"
;
export
default
(
_express
:
Application
)
=>
{
...
...
code/src/controllers/api/v1.0/enrollments/unenroll/index.ts
View file @
7c00ef84
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
)
=>
{
...
...
code/src/controllers/api/v1.0/roles/index.ts
View file @
7c00ef84
import
{
Req
,
Res
}
from
"#interface/IApi"
;
import
{
Req
,
Res
}
from
"#interface
s
/IApi"
;
import
{
RolesProvider
}
from
"#providers/RolesProvider.js"
;
import
{
Application
}
from
"express"
;
import
{
Resource
}
from
"express-automatic-routes"
;
...
...
code/src/controllers/api/v1.0/roles/set-role/index.ts
View file @
7c00ef84
import
{
Req
,
Res
}
from
"#interface/IApi"
;
import
{
Req
,
Res
}
from
"#interface
s
/IApi"
;
import
{
RolesProvider
}
from
"#providers/RolesProvider.js"
;
import
{
Application
}
from
"express"
;
import
{
Resource
}
from
"express-automatic-routes"
;
...
...
code/src/docs/swagger/swagger-output.json
View file @
7c00ef84
...
...
@@ -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"
:
[
...
...
code/src/providers/ClassesProvider.ts
View file @
7c00ef84
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
code/src/templates/swagger/classes/schemas.ts
View file @
7c00ef84
...
...
@@ -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
code/src/templates/swagger/login/schemas.ts
View file @
7c00ef84
...
...
@@ -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'
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment