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 @@
"#controllers/*": "./src/controllers/*",
"#services/*": "./src/services/*",
"#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": {
"@fast-csv/format": "^5.0.7",
"bcrypt": "^6.0.0",
"csv-parser": "^3.2.1",
"dotenv": "^17.4.2",
"express": "^5.2.1",
"express-automatic-routes": "^1.1.0",
"jsonwebtoken": "^9.0.3",
"module-alias": "^2.3.4",
"multer": "^2.1.1",
"nodemailer": "^8.0.7",
"pg": "^8.20.0",
"pg-hstore": "^2.3.4",
......@@ -37,6 +48,7 @@
"@types/bcrypt": "^6.0.0",
"@types/express": "^5.0.6",
"@types/jsonwebtoken": "^9.0.10",
"@types/multer": "^2.1.0",
"@types/node": "^25.7.0",
"@types/nodemailer": "^8.0.0",
"@types/swagger-jsdoc": "^6.0.4",
......
......@@ -8,9 +8,15 @@ importers:
.:
dependencies:
'@fast-csv/format':
specifier: ^5.0.7
version: 5.0.7
bcrypt:
specifier: ^6.0.0
version: 6.0.0
csv-parser:
specifier: ^3.2.1
version: 3.2.1
dotenv:
specifier: ^17.4.2
version: 17.4.2
......@@ -26,6 +32,9 @@ importers:
module-alias:
specifier: ^2.3.4
version: 2.3.4
multer:
specifier: ^2.1.1
version: 2.1.1
nodemailer:
specifier: ^8.0.7
version: 8.0.7
......@@ -57,6 +66,9 @@ importers:
'@types/jsonwebtoken':
specifier: ^9.0.10
version: 9.0.10
'@types/multer':
specifier: ^2.1.0
version: 2.1.0
'@types/node':
specifier: ^25.7.0
version: 25.7.0
......@@ -256,6 +268,9 @@ packages:
cpu: [x64]
os: [win32]
'@fast-csv/format@5.0.7':
resolution: {integrity: sha512-VdypoRxv7PF+LsyPouTMKdB0d76hync+gLpgdNqfqVK44MsgW4oiCJSdrki2FisWT7v2QGUYDHjp4L7w5oO6gw==}
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
......@@ -314,6 +329,9 @@ packages:
'@types/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
'@types/multer@2.1.0':
resolution: {integrity: sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==}
'@types/node@25.7.0':
resolution: {integrity: sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==}
......@@ -362,6 +380,9 @@ packages:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
append-field@1.0.0:
resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==}
arg@4.1.3:
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
......@@ -385,6 +406,13 @@ packages:
buffer-equal-constant-time@1.0.1:
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
busboy@1.6.0:
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
engines: {node: '>=10.16.0'}
bytes@3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'}
......@@ -421,6 +449,10 @@ packages:
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
concat-stream@2.0.0:
resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==}
engines: {'0': node >= 6.0}
content-disposition@1.1.0:
resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==}
engines: {node: '>=18'}
......@@ -440,6 +472,11 @@ packages:
create-require@1.1.1:
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
csv-parser@3.2.1:
resolution: {integrity: sha512-v8RPMSglouR9od735SnwSxLBbCJqEPSbgm1R5qfr8yIiMUCEFjox56kRZid0SvgHJEkxeIEu3+a9QS3YRh7CuA==}
engines: {node: '>= 10'}
hasBin: true
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
......@@ -623,6 +660,9 @@ packages:
jws@4.0.1:
resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==}
lodash.escaperegexp@4.1.2:
resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==}
lodash.get@4.4.2:
resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==}
deprecated: This package is deprecated. Use the optional chaining (?.) operator instead.
......@@ -665,6 +705,10 @@ packages:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
media-typer@0.3.0:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
engines: {node: '>= 0.6'}
media-typer@1.1.0:
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
engines: {node: '>= 0.8'}
......@@ -673,10 +717,18 @@ packages:
resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==}
engines: {node: '>=18'}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-db@1.54.0:
resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
engines: {node: '>= 0.6'}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
mime-types@3.0.2:
resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
engines: {node: '>=18'}
......@@ -701,6 +753,10 @@ packages:
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
multer@2.1.1:
resolution: {integrity: sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==}
engines: {node: '>= 10.16.0'}
negotiator@1.0.0:
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
engines: {node: '>= 0.6'}
......@@ -812,6 +868,10 @@ packages:
resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==}
engines: {node: '>= 0.10'}
readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
......@@ -919,10 +979,17 @@ packages:
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
engines: {node: '>= 0.8'}
streamsearch@1.1.0:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
......@@ -971,10 +1038,17 @@ packages:
engines: {node: '>=18.0.0'}
hasBin: true
type-is@1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
type-is@2.0.1:
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
engines: {node: '>= 0.6'}
typedarray@0.0.6:
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
typescript@6.0.3:
resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==}
engines: {node: '>=14.17'}
......@@ -990,6 +1064,9 @@ packages:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'}
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
uuid@8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
......@@ -1150,6 +1227,10 @@ snapshots:
'@esbuild/win32-x64@0.27.7':
optional: true
'@fast-csv/format@5.0.7':
dependencies:
lodash.escaperegexp: 4.1.2
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
......@@ -1212,6 +1293,10 @@ snapshots:
'@types/ms@2.1.0': {}
'@types/multer@2.1.0':
dependencies:
'@types/express': 5.0.6
'@types/node@25.7.0':
dependencies:
undici-types: 7.21.0
......@@ -1259,6 +1344,8 @@ snapshots:
dependencies:
color-convert: 2.0.1
append-field@1.0.0: {}
arg@4.1.3: {}
argparse@2.0.1: {}
......@@ -1291,6 +1378,12 @@ snapshots:
buffer-equal-constant-time@1.0.1: {}
buffer-from@1.1.2: {}
busboy@1.6.0:
dependencies:
streamsearch: 1.1.0
bytes@3.1.2: {}
call-bind-apply-helpers@1.0.2:
......@@ -1324,6 +1417,13 @@ snapshots:
concat-map@0.0.1: {}
concat-stream@2.0.0:
dependencies:
buffer-from: 1.1.2
inherits: 2.0.4
readable-stream: 3.6.2
typedarray: 0.0.6
content-disposition@1.1.0: {}
content-type@1.0.5: {}
......@@ -1334,6 +1434,8 @@ snapshots:
create-require@1.1.1: {}
csv-parser@3.2.1: {}
debug@4.4.3:
dependencies:
ms: 2.1.3
......@@ -1564,6 +1666,8 @@ snapshots:
jwa: 2.0.1
safe-buffer: 5.2.1
lodash.escaperegexp@4.1.2: {}
lodash.get@4.4.2: {}
lodash.includes@4.3.0: {}
......@@ -1590,12 +1694,20 @@ snapshots:
math-intrinsics@1.1.0: {}
media-typer@0.3.0: {}
media-typer@1.1.0: {}
merge-descriptors@2.0.0: {}
mime-db@1.52.0: {}
mime-db@1.54.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
mime-types@3.0.2:
dependencies:
mime-db: 1.54.0
......@@ -1616,6 +1728,13 @@ snapshots:
ms@2.1.3: {}
multer@2.1.1:
dependencies:
append-field: 1.0.0
busboy: 1.6.0
concat-stream: 2.0.0
type-is: 1.6.18
negotiator@1.0.0: {}
node-addon-api@8.7.0: {}
......@@ -1709,6 +1828,12 @@ snapshots:
iconv-lite: 0.7.2
unpipe: 1.0.0
readable-stream@3.6.2:
dependencies:
inherits: 2.0.4
string_decoder: 1.3.0
util-deprecate: 1.0.2
require-directory@2.1.1: {}
reserved-words@0.1.2: {}
......@@ -1826,12 +1951,18 @@ snapshots:
statuses@2.0.2: {}
streamsearch@1.1.0: {}
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
string_decoder@1.3.0:
dependencies:
safe-buffer: 5.2.1
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
......@@ -1891,12 +2022,19 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
type-is@1.6.18:
dependencies:
media-typer: 0.3.0
mime-types: 2.1.35
type-is@2.0.1:
dependencies:
content-type: 1.0.5
media-typer: 1.1.0
mime-types: 3.0.2
typedarray@0.0.6: {}
typescript@6.0.3: {}
underscore@1.13.8: {}
......@@ -1905,6 +2043,8 @@ snapshots:
unpipe@1.0.0: {}
util-deprecate@1.0.2: {}
uuid@8.3.2: {}
v8-compile-cache-lib@3.0.1: {}
......
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 { 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";
......@@ -69,7 +69,7 @@ export default (_express: Application) => {
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/CourseResponse"
* $ref: "#/components/schemas/CourseResponse"
*/
post: {
middleware: [queryModifier, authMiddleware, authorize('admin', 'instructor')],
......
......@@ -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": {
"get": {
"tags": [
......
......@@ -3,7 +3,7 @@ import { resolve } from 'path';
import _autoroutes from 'express-automatic-routes';
import swaggerUi from 'swagger-ui-express';
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';
dotenv.config();
......
import jwt from 'jsonwebtoken';
import { NextFunction } from 'express';
import { JWTPayload, Req, Res } from '#interface/IApi';
import { JWTPayload, Req, Res } from '#interfaces/IApi';
//demo
const JWT_SECRET = process.env.JWT_SECRET || '';
......
import { NextFunction } from 'express';
import { models } from '#models/sequelize-config.js';
import { Req, Res } from '#interface/IApi';
import { Req, Res } from '#interfaces/IApi';
export const authorize = (...allowedRoles: string[]) => {
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 { 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";
export default function (_req: Req, res: Res, next: NextFunction) {
......
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', {
host: 'localhost',
......
import { payload } from '#interface/IApi';
import { payload } from '#interfaces/IApi';
import { models } from '#models/sequelize-config.js';
interface CreateClassInput {
......
import { payload } from '#interface/IApi';
import { payload } from '#interfaces/IApi';
import { models } from '#models/sequelize-config.js';
interface CreateCourseInput {
......@@ -61,4 +61,19 @@ export class CoursesProvider {
});
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";
export class RolesProvider {
......
......@@ -3,7 +3,7 @@ import fs from 'node:fs';
import path from 'node:path';
// Nạp file cấu hình
import swaggerOptions from '#/templates/swagger/config.js';
import swaggerOptions from '#templates/swagger/config.js';
try {
console.log('--- Đang bắt đầu quét mã nguồn để tạo tài liệu API ---');
......
......@@ -28,6 +28,9 @@
"#providers/*": [
"./src/providers/*"
],
"#config/*": [
"./src/config/*"
],
"#services/*": [
"./src/services/*"
],
......@@ -46,7 +49,10 @@
"#docs/*": [
"./src/docs/*"
],
"#*": [
"#templates/*": [
"./src/templates/*"
],
"#/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