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

feat(challenge_9): add api import export course

parent 712345b5
...@@ -16,15 +16,26 @@ ...@@ -16,15 +16,26 @@
"#controllers/*": "./src/controllers/*", "#controllers/*": "./src/controllers/*",
"#services/*": "./src/services/*", "#services/*": "./src/services/*",
"#providers/*": "./src/providers/*", "#providers/*": "./src/providers/*",
"#docs/*": "./src/docs/*" "#docs/*": "./src/docs/*",
"#utils/*": "./src/utils/*",
"#config/*": "./src/config/*",
"#middlewares/*": "./src/middlewares/*",
"#routes/*": "./src/routes/*",
"#scripts/*": "./src/scripts/*",
"#types/*": "./src/types/*",
"#interfaces/*": "./src/interfaces/*",
"#templates/*": "./src/templates/*"
}, },
"dependencies": { "dependencies": {
"@fast-csv/format": "^5.0.7",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"csv-parser": "^3.2.1",
"dotenv": "^17.4.2", "dotenv": "^17.4.2",
"express": "^5.2.1", "express": "^5.2.1",
"express-automatic-routes": "^1.1.0", "express-automatic-routes": "^1.1.0",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"module-alias": "^2.3.4", "module-alias": "^2.3.4",
"multer": "^2.1.1",
"nodemailer": "^8.0.7", "nodemailer": "^8.0.7",
"pg": "^8.20.0", "pg": "^8.20.0",
"pg-hstore": "^2.3.4", "pg-hstore": "^2.3.4",
...@@ -37,6 +48,7 @@ ...@@ -37,6 +48,7 @@
"@types/bcrypt": "^6.0.0", "@types/bcrypt": "^6.0.0",
"@types/express": "^5.0.6", "@types/express": "^5.0.6",
"@types/jsonwebtoken": "^9.0.10", "@types/jsonwebtoken": "^9.0.10",
"@types/multer": "^2.1.0",
"@types/node": "^25.7.0", "@types/node": "^25.7.0",
"@types/nodemailer": "^8.0.0", "@types/nodemailer": "^8.0.0",
"@types/swagger-jsdoc": "^6.0.4", "@types/swagger-jsdoc": "^6.0.4",
......
This diff is collapsed.
import multer from "multer";
const memoryStorage = multer.memoryStorage();
const uploadCSV = multer({
storage: memoryStorage,
limits: {
fileSize: 2 * 1024 * 1024, // 2 MB
},
});
export default uploadCSV;
\ No newline at end of file
import { Application } from "express";
import { Resource } from "express-automatic-routes";
import * as fastcsv from '@fast-csv/format';
import { Req, Res } from "#interfaces/IApi.js";
import { CoursesProvider } from "#providers/CoursesProvider.js";
import queryModifier from "#middlewares/request";
import { authMiddleware } from "#middlewares/authentication";
import { authorize } from "#middlewares/authorization";
export default (_express: Application) => {
const coursesProvider = new CoursesProvider();
return <Resource>{
/**
* @openapi
* /api/v1.0/courses/export:
* get:
* tags: [Courses]
* description: Xuất danh sách các khoá học ra file CSV.
* parameters:
* - $ref: '#/components/parameters/filters'
* - $ref: '#/components/parameters/sort'
* - $ref: '#/components/parameters/page'
* - $ref: '#/components/parameters/pageSize'
* responses:
* 200:
* description: File CSV được tạo thành công
*/
get: {
middleware: [queryModifier],
handler: async (req: Req, res: Res) => {
try {
const filename = `courses-export-${Date.now()}.csv`;
const courses = await coursesProvider.exportCourses(req.payload);
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
res.write('\uFEFF');
const csvStream = fastcsv.format({
headers: true,
alwaysWriteHeaders: true
});
csvStream.pipe(res);
for (const course of courses) {
const rowData = {
"Id": course.id,
"Name": course.name,
"Description": course.description,
"Status": course.status,
"Created At": course.created_at,
"Created By": course.created_by
};
csvStream.write(rowData);
}
csvStream.end();
} catch (error) {
console.error('Error exporting courses:', error);
return res.sendError({
message: "Lỗi khi xuất khóa học!",
message_en: "Error occurred while exporting courses!",
status: 500,
});
}
}
}
}
}
\ No newline at end of file
import { Application } from "express";
import { Readable } from 'stream';
import { Resource } from "express-automatic-routes";
import uploadCSV from "#config/multer.config.js";
import csv from 'csv-parser';
import { Req, Res } from "#interfaces/IApi.js";
import { CoursesProvider } from "#providers/CoursesProvider.js";
export default (_express: Application) => {
const coursesProvider = new CoursesProvider();
return <Resource>{
/**
* @openapi
* /api/v1.0/courses/import:
* post:
* tags: [Courses]
* description: Nhập các khoá học từ file CSV.
* requestBody:
* required: true
* content:
* multipart/form-data:
* schema:
* type: object
* required:
* - file
* properties:
* file:
* type: string
* format: binary
* description: File CSV (Giới hạn dưới 2MB)
* responses:
* 201:
* description: Tạo khóa học mới thành công
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/CourseResponse"
*/
post: {
middleware: [uploadCSV.single('file')],
handler: async (req: Req, res: Res) => {
try {
if (!req.file) {
return res.sendError({
message: "Không có file nào được tải lên!",
message_en: "No file uploaded!",
status: 400
});
}
// csv đọc file từ buffer và chuyển đổi
const results = await new Promise<any[]>((resolve, reject) => {
const rows: any[] = [];
const stream = Readable.from(req.file!.buffer);
stream
.pipe(csv())
.on('data', (row) => {
rows.push(row);
})
.on('end', () => {
resolve(rows);
})
.on('error', (error) => {
reject(error);
});
});
const importedCourses = await coursesProvider.importCourses(results);
return res.sendOk({
message: "Khóa học đã được import thành công!",
message_en: "Courses imported successfully!",
data: importedCourses
});
} catch (error) {
console.error('Error importing courses:', error);
return res.sendError({
message: "Lỗi khi import khóa học!",
message_en: "Error occurred while importing courses!",
status: 500
});
}
}
}
}
}
\ 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";
......
...@@ -1326,6 +1326,73 @@ ...@@ -1326,6 +1326,73 @@
} }
} }
}, },
"/api/v1.0/courses/export": {
"get": {
"tags": [
"Courses"
],
"description": "Xuất danh sách các khoá học ra file CSV.",
"parameters": [
{
"$ref": "#/components/parameters/filters"
},
{
"$ref": "#/components/parameters/sort"
},
{
"$ref": "#/components/parameters/page"
},
{
"$ref": "#/components/parameters/pageSize"
}
],
"responses": {
"200": {
"description": "File CSV được tạo thành công"
}
}
}
},
"/api/v1.0/courses/import": {
"post": {
"tags": [
"Courses"
],
"description": "Nhập các khoá học từ file CSV.",
"requestBody": {
"required": true,
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"required": [
"file"
],
"properties": {
"file": {
"type": "string",
"format": "binary",
"description": "File CSV (Giới hạn dưới 2MB)"
}
}
}
}
}
},
"responses": {
"201": {
"description": "Tạo khóa học mới thành công",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CourseResponse"
}
}
}
}
}
}
},
"/api/v1.0/courses": { "/api/v1.0/courses": {
"get": { "get": {
"tags": [ "tags": [
......
...@@ -3,7 +3,7 @@ import { resolve } from 'path'; ...@@ -3,7 +3,7 @@ import { resolve } from 'path';
import _autoroutes from 'express-automatic-routes'; import _autoroutes from 'express-automatic-routes';
import swaggerUi from 'swagger-ui-express'; import swaggerUi from 'swagger-ui-express';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import swaggerFile from '#/docs/swagger/swagger-output.json'; import swaggerFile from '#docs/swagger/swagger-output.json';
import response from '#middlewares/response'; import response from '#middlewares/response';
dotenv.config(); dotenv.config();
......
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { NextFunction } from 'express'; import { NextFunction } from 'express';
import { JWTPayload, Req, Res } from '#interface/IApi'; import { JWTPayload, Req, Res } from '#interfaces/IApi';
//demo //demo
const JWT_SECRET = process.env.JWT_SECRET || ''; const JWT_SECRET = process.env.JWT_SECRET || '';
......
import { NextFunction } from 'express'; import { NextFunction } from 'express';
import { models } from '#models/sequelize-config.js'; import { models } from '#models/sequelize-config.js';
import { Req, Res } from '#interface/IApi'; import { Req, Res } from '#interfaces/IApi';
export const authorize = (...allowedRoles: string[]) => { export const authorize = (...allowedRoles: string[]) => {
return async (req: Req, res: Res, next: NextFunction) => { return async (req: Req, res: Res, next: NextFunction) => {
......
import { Req, Res } from "#interface/IApi"; import { Req, Res } from "#interfaces/IApi";
import { NextFunction } from "express"; import { NextFunction } from "express";
import { Op } from "sequelize"; import { Op } from "sequelize";
......
import { ErrorParams, OkParams, Req, Res, ResponseDTO, ViolationDTO } from "#interface/IApi"; import { ErrorParams, OkParams, Req, Res, ResponseDTO, ViolationDTO } from "#interfaces/IApi";
import { NextFunction } from "express"; import { NextFunction } from "express";
export default function (_req: Req, res: Res, next: NextFunction) { export default function (_req: Req, res: Res, next: NextFunction) {
......
import { Sequelize } from 'sequelize'; import { Sequelize } from 'sequelize';
import { initModels } from '#/models/init-models.js'; import { initModels } from '#models/init-models.js';
const sequelize = new Sequelize('challenge_db', 'postgres', '123456', { const sequelize = new Sequelize('challenge_db', 'postgres', '123456', {
host: 'localhost', host: 'localhost',
......
import { payload } from '#interface/IApi'; import { payload } from '#interfaces/IApi';
import { models } from '#models/sequelize-config.js'; import { models } from '#models/sequelize-config.js';
interface CreateClassInput { interface CreateClassInput {
......
import { payload } from '#interface/IApi'; import { payload } from '#interfaces/IApi';
import { models } from '#models/sequelize-config.js'; import { models } from '#models/sequelize-config.js';
interface CreateCourseInput { interface CreateCourseInput {
...@@ -61,4 +61,19 @@ export class CoursesProvider { ...@@ -61,4 +61,19 @@ export class CoursesProvider {
}); });
return deletedCourse; return deletedCourse;
} }
async importCourses(courses: CreateCourseInput[]) {
const importedCourses = await models.courses.bulkCreate(courses);
return importedCourses;
}
async exportCourses(params: payload) {
const exportedCourses = await models.courses.findAll({
attributes: ['id', 'name', 'description', 'status', 'created_at', 'created_by'],
where: params.filters,
order: params.sortBy ? [[params.sortBy, params.sortOrder]] : [['created_at', 'DESC']],
raw: true,
});
return exportedCourses;
}
} }
\ No newline at end of file
import { payload } from "#interface/IApi"; import { payload } from "#interfaces/IApi";
import { models } from "#models/sequelize-config.js"; import { models } from "#models/sequelize-config.js";
export class RolesProvider { export class RolesProvider {
......
...@@ -3,7 +3,7 @@ import fs from 'node:fs'; ...@@ -3,7 +3,7 @@ import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
// Nạp file cấu hình // Nạp file cấu hình
import swaggerOptions from '#/templates/swagger/config.js'; import swaggerOptions from '#templates/swagger/config.js';
try { try {
console.log('--- Đang bắt đầu quét mã nguồn để tạo tài liệu API ---'); console.log('--- Đang bắt đầu quét mã nguồn để tạo tài liệu API ---');
......
...@@ -28,6 +28,9 @@ ...@@ -28,6 +28,9 @@
"#providers/*": [ "#providers/*": [
"./src/providers/*" "./src/providers/*"
], ],
"#config/*": [
"./src/config/*"
],
"#services/*": [ "#services/*": [
"./src/services/*" "./src/services/*"
], ],
...@@ -46,7 +49,10 @@ ...@@ -46,7 +49,10 @@
"#docs/*": [ "#docs/*": [
"./src/docs/*" "./src/docs/*"
], ],
"#*": [ "#templates/*": [
"./src/templates/*"
],
"#/src/*": [
"./src/*" "./src/*"
] ]
}, },
......
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