feat (level): Implement CRUD function for table level with upload file logic + get and edit route field only

This commit is contained in:
elangptra 2024-08-16 14:22:19 +07:00
parent 935232a01d
commit a27ea19c5b
17 changed files with 819 additions and 41 deletions

397
controllers/level.js Normal file
View File

@ -0,0 +1,397 @@
import response from "../response.js";
import models from "../models/index.js";
import {
clearFileBuffers,
saveFileToDisk,
generateHash,
} from "../middlewares/uploadLevel.js";
import fs from "fs";
import path from "path";
import crypto from "crypto";
export const getAllLevels = async (req, res) => {
try {
const levels = await models.Level.findAll();
response(200, levels, "Success", res);
} catch (error) {
console.log(error);
response(500, null, "Error retrieving levels data!", res);
}
};
export const getAllLevelById = async (req, res) => {
try {
const { id } = req.params;
const level = await models.Level.findByPk(id);
if (!level) {
return response(404, null, "Level not found", res);
}
response(200, level, "Success", res);
} catch (error) {
console.log(error);
res.status(500).json({ message: "Internal Server Error" });
}
};
export const getLevels = async (req, res) => {
try {
const levels = await models.Level.findAll({
attributes: {
exclude: ["route1", "route2", "route3", "route4"],
},
});
response(200, levels, "Success", res);
} catch (error) {
console.log(error);
res.status(500).json({ message: "Internal Server Error" });
}
};
export const getLevelById = async (req, res) => {
try {
const { id } = req.params;
const level = await models.Level.findByPk(id, {
attributes: {
exclude: ["route1", "route2", "route3", "route4"],
},
});
if (!level) {
return response(404, null, "Level not found", res);
}
response(200, level, "Success", res);
} catch (error) {
console.log(error);
res.status(500).json({ message: "Internal Server Error" });
}
};
export const createLevel = async (req, res) => {
const { title, subject_id, topic_id, is_pretest, content, youtube } =
req.body;
// Files to be saved if everything else is okay
const { video, audio, image } = req.filesToSave || {};
// Validate title
if (!title) {
clearFileBuffers({ video, audio, image });
return response(400, null, "Title is required", res);
}
// Validate subject_id
if (!subject_id) {
clearFileBuffers({ video, audio, image });
return response(400, null, "Subject ID is required", res);
}
// Validate topic_id
if (!topic_id) {
clearFileBuffers({ video, audio, image });
return response(400, null, "Topic ID is required", res);
}
try {
// Check if the title already exists under the same topic_id
const existingLevel = await models.Level.findOne({
where: { title, topic_id },
});
if (existingLevel) {
clearFileBuffers({ video, audio, image });
return response(
409,
null,
"A level with this title already exists under this topic",
res
); // 409 Conflict
}
// Save files to disk
const videoFilename = video
? saveFileToDisk(video, "video", title, topic_id, subject_id)
: null;
const audioFilename = audio
? saveFileToDisk(audio, "audio", title, topic_id, subject_id)
: null;
const imageFilename = image
? saveFileToDisk(image, "image", title, topic_id, subject_id)
: null;
// Create the new level
const newLevel = await models.Level.create({
title,
subject_id,
topic_id,
is_pretest: is_pretest || 0,
content,
video: videoFilename,
audio: audioFilename,
image: imageFilename,
youtube,
route1: 0,
route2: 0,
route3: 0,
route4: 0,
});
// Update routes with the newly created level's ID
await newLevel.update({
route1: newLevel.id,
route2: newLevel.id,
route3: newLevel.id,
route4: newLevel.id,
});
response(201, newLevel, "Level created successfully", res);
} catch (error) {
console.log(error);
clearFileBuffers({ video, audio, image });
return response(500, null, "Internal Server Error", res);
}
};
export const updateLevelById = async (req, res) => {
const { id } = req.params;
const { title, subject_id, topic_id, is_pretest, content, youtube } =
req.body;
// Files to be saved if everything else is okay
const { video, audio, image } = req.filesToSave || {};
try {
// Find the existing level by ID
const level = await models.Level.findByPk(id);
if (!level) {
clearFileBuffers({ video, audio, image });
return response(404, null, "Level not found", res);
}
// Check if a level with the same title under the same topic already exists
if (title && topic_id) {
const existingLevel = await models.Level.findOne({
where: {
title,
topic_id,
id: { [models.Sequelize.Op.ne]: id }, // Exclude the current level from the check
},
});
if (existingLevel) {
clearFileBuffers({ video, audio, image });
return response(
409,
null,
"A level with this title already exists under this topic",
res
); // 409 Conflict
}
}
// Update level fields
if (title) level.title = title;
if (subject_id) level.subject_id = subject_id;
if (topic_id) level.topic_id = topic_id;
if (is_pretest !== undefined) level.is_pretest = is_pretest;
if (content) level.content = content;
if (youtube) level.youtube = youtube;
// Handle video update
if (video) {
if (level.video) {
const oldVideoPath = path.join(
"public/uploads/level/video",
level.video
);
if (fs.existsSync(oldVideoPath)) {
fs.unlinkSync(oldVideoPath);
}
}
level.video = saveFileToDisk(
video,
"video",
title || level.title,
topic_id || level.topic_id,
subject_id || level.subject_id
);
}
// Handle audio update
if (audio) {
if (level.audio) {
const oldAudioPath = path.join(
"public/uploads/level/audio",
level.audio
);
if (fs.existsSync(oldAudioPath)) {
fs.unlinkSync(oldAudioPath);
}
}
level.audio = saveFileToDisk(
audio,
"audio",
title || level.title,
topic_id || level.topic_id,
subject_id || level.subject_id
);
}
// Handle image update
if (image) {
if (level.image) {
const oldImagePath = path.join(
"public/uploads/level/image",
level.image
);
if (fs.existsSync(oldImagePath)) {
fs.unlinkSync(oldImagePath);
}
}
level.image = saveFileToDisk(
image,
"image",
title || level.title,
topic_id || level.topic_id,
subject_id || level.subject_id
);
}
await level.save();
response(200, level, "Level updated successfully", res);
} catch (error) {
console.log(error);
clearFileBuffers({ video, audio, image });
return response(500, null, "Internal Server Error", res);
}
};
export const deleteLevelById = async (req, res) => {
const { id } = req.params;
try {
// Find the existing level by ID
const level = await models.Level.findByPk(id);
if (!level) {
return response(404, null, "Level not found", res);
}
// Delete associated files from disk if they exist
const deleteFile = (filePath) => {
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
};
if (level.video) {
const videoPath = path.join("public/uploads/level/video", level.video);
deleteFile(videoPath);
}
if (level.audio) {
const audioPath = path.join("public/uploads/level/audio", level.audio);
deleteFile(audioPath);
}
if (level.image) {
const imagePath = path.join("public/uploads/level/image", level.image);
deleteFile(imagePath);
}
// Delete the level from the database
await level.destroy();
response(200, null, "Level deleted successfully", res);
} catch (error) {
console.log(error);
return response(500, null, "Internal Server Error", res);
}
};
export const getRoutes = async (req, res) => {
try {
const levels = await models.Level.findAll({
attributes: {
exclude: [
"subject_id",
"topic_id",
"is_pretest",
"content",
"video",
"audio",
"image",
"youtube",
"ts_entri",
],
},
});
response(200, levels, "Success", res);
} catch (error) {
console.log(error);
res.status(500).json({ message: "Internal Server Error" });
}
};
export const getRouteById = async (req, res) => {
try {
const { id } = req.params;
const level = await models.Level.findByPk(id, {
attributes: {
exclude: [
"subject_id",
"topic_id",
"is_pretest",
"content",
"video",
"audio",
"image",
"youtube",
"ts_entri",
],
},
});
if (!level) {
return response(404, null, "Level not found", res);
}
response(200, level, "Success", res);
} catch (error) {
console.log(error);
res.status(500).json({ message: "Internal Server Error" });
}
};
export const updateRouteById = async (req, res) => {
const { id } = req.params;
const { route1, route2, route3, route4 } = req.body;
try {
// Find the existing level by ID
const level = await models.Level.findByPk(id);
if (!level) {
return response(404, null, "Level not found", res);
}
// Update only the route fields
await level.update({
route1: route1 !== undefined ? route1 : level.route1,
route2: route2 !== undefined ? route2 : level.route2,
route3: route3 !== undefined ? route3 : level.route3,
route4: route4 !== undefined ? route4 : level.route4,
});
response(200, level, "Routes updated successfully", res);
} catch (error) {
console.log(error);
return response(500, null, "Internal Server Error", res);
}
};

View File

@ -2,6 +2,10 @@ import response from "../response.js";
import models from "../models/index.js";
import fs from "fs";
import path from "path";
import {
clearFileBuffers,
saveFileToDisk,
} from "../middlewares/uploadSubject.js";
export const getSubjects = async (req, res) => {
try {
@ -30,29 +34,42 @@ export const getSubjectById = async (req, res) => {
};
export const createSubject = async (req, res) => {
const { name, description, icon, thumbnail } = req.body;
const { name, description } = req.body;
// Files to be saved if everything else is okay
const { icon, thumbnail } = req.filesToSave || {};
// Validate name
if (!name) {
clearFileBuffers({ icon, thumbnail });
return response(400, null, "Name is required", res);
}
// Validate description
if (!description) {
clearFileBuffers({ icon, thumbnail });
return response(400, null, "Description is required", res);
}
try {
const iconFilename = icon
? saveFileToDisk(icon, `${name}-icon`, name)
: null;
const thumbnailFilename = thumbnail
? saveFileToDisk(thumbnail, `${name}-thumbnail`, name)
: null;
const newSubject = await models.Subject.create({
name,
description,
icon,
thumbnail,
icon: iconFilename,
thumbnail: thumbnailFilename,
});
response(201, newSubject, "Subject created successfully", res);
} catch (error) {
console.log(error);
clearFileBuffers({ icon, thumbnail });
res.status(500).json({ message: "Internal Server Error" });
}
};
@ -60,38 +77,45 @@ export const createSubject = async (req, res) => {
export const updateSubjectById = async (req, res) => {
const { id } = req.params;
const { name, description } = req.body;
const icon = req.body.icon;
const thumbnail = req.body.thumbnail;
// Files to be saved if everything else is okay
const { icon, thumbnail } = req.filesToSave || {};
try {
const subject = await models.Subject.findByPk(id);
if (!subject) {
clearFileBuffers({ icon, thumbnail });
return response(404, null, "Subject not found", res);
}
// Update subject fields
if (name) subject.name = name;
if (description) subject.description = description;
// Handle icon update
if (icon) {
// Remove old icon if it exists
if (
subject.icon &&
fs.existsSync(path.join("public/uploads", subject.icon))
) {
fs.unlinkSync(path.join("public/uploads", subject.icon));
if (subject.icon) {
const oldIconPath = path.join("public/uploads/subject", subject.icon);
if (fs.existsSync(oldIconPath)) {
fs.unlinkSync(oldIconPath);
}
subject.icon = icon;
}
subject.icon = saveFileToDisk(icon, `${name}-icon`, name);
}
// Handle thumbnail update
if (thumbnail) {
// Remove old thumbnail if it exists
if (
subject.thumbnail &&
fs.existsSync(path.join("public/uploads", subject.thumbnail))
) {
fs.unlinkSync(path.join("public/uploads", subject.thumbnail));
if (subject.thumbnail) {
const oldThumbnailPath = path.join(
"public/uploads/subject",
subject.thumbnail
);
if (fs.existsSync(oldThumbnailPath)) {
fs.unlinkSync(oldThumbnailPath);
}
subject.thumbnail = thumbnail;
}
subject.thumbnail = saveFileToDisk(thumbnail, `${name}-thumbnail`, name);
}
await subject.save();
@ -99,6 +123,7 @@ export const updateSubjectById = async (req, res) => {
response(200, subject, "Subject updated successfully", res);
} catch (error) {
console.log(error);
clearFileBuffers({ icon, thumbnail });
response(500, null, "Internal Server Error", res);
}
};
@ -113,18 +138,23 @@ export const deleteSubjectById = async (req, res) => {
return response(404, null, "Subject not found", res);
}
// Remove associated files if they exist
if (
subject.icon &&
fs.existsSync(path.join("public/uploads", subject.icon))
) {
fs.unlinkSync(path.join("public/uploads", subject.icon));
// Remove associated icon if it exists
if (subject.icon) {
const iconPath = path.join("public/uploads/subject", subject.icon);
if (fs.existsSync(iconPath)) {
fs.unlinkSync(iconPath);
}
}
// Remove associated thumbnail if it exists
if (subject.thumbnail) {
const thumbnailPath = path.join(
"public/uploads/subject",
subject.thumbnail
);
if (fs.existsSync(thumbnailPath)) {
fs.unlinkSync(thumbnailPath);
}
if (
subject.thumbnail &&
fs.existsSync(path.join("public/uploads", subject.thumbnail))
) {
fs.unlinkSync(path.join("public/uploads", subject.thumbnail));
}
await subject.destroy();

View File

@ -4,7 +4,11 @@ import bcrypt from "bcrypt";
export const getUsers = async (req, res) => {
try {
const users = await models.User.findAll();
const users = await models.User.findAll({
attributes: {
exclude: ["password"],
},
});
response(200, users, "Success", res);
} catch (error) {
console.log(error);
@ -15,7 +19,11 @@ export const getUsers = async (req, res) => {
export const getUserById = async (req, res) => {
try {
const { id } = req.params;
const user = await models.User.findByPk(id);
const user = await models.User.findByPk(id, {
attributes: {
exclude: ["password"],
},
});
if (!user) {
return response(404, null, "User not found", res);

View File

@ -11,6 +11,7 @@ const app = express();
app.use(cors());
app.use(cookieParser());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(router);
// Serve static files from the uploads directory

View File

@ -15,7 +15,11 @@ export const verifyLoginUser = async (req, res, next) => {
const decoded = jwt.verify(accessToken, process.env.ACCESS_TOKEN_SECRET);
// Cari user berdasarkan id yang ada di token
const user = await models.User.findByPk(decoded.id);
const user = await models.User.findByPk(decoded.id, {
attributes: {
exclude: ["password"],
},
});
if (!user) {
return res.status(404).json({ message: "User not found!" });

29
middlewares/checkLevel.js Normal file
View File

@ -0,0 +1,29 @@
import models from "../models/index.js";
import response from "../response.js";
export const checkMaxLevelsPerTopic = async (req, res, next) => {
const { topic_id } = req.body;
try {
// Hitung jumlah level yang ada pada topic_id yang diberikan
const levelCount = await models.Level.count({
where: { topic_id },
});
// Periksa apakah jumlah level sudah mencapai 5
if (levelCount >= 5) {
return response(
400,
null,
"Cannot add more than 5 levels to a single topic",
res
);
}
// Lanjutkan ke middleware atau route handler berikutnya jika belum mencapai 5
next();
} catch (error) {
console.log(error);
return response(500, null, "Internal Server Error", res);
}
};

171
middlewares/uploadLevel.js Normal file
View File

@ -0,0 +1,171 @@
import multer from "multer";
import crypto from "crypto";
import path from "path";
import fs from "fs";
import response from "../response.js";
// Setup memory storage for Multer
const memoryStorage = multer.memoryStorage();
// Filter untuk membatasi tipe file dan ukuran file
const fileFilter = (req, file, cb) => {
const ext = path.extname(file.originalname).toLowerCase();
switch (file.fieldname) {
case "video":
if (ext === ".mp4") {
cb(null, true);
} else {
cb(
new Error(
"Invalid file type, only .mp4 files are allowed for video!"
),
false
);
}
break;
case "audio":
if (ext === ".mp3") {
cb(null, true);
} else {
cb(
new Error(
"Invalid file type, only .mp3 files are allowed for audio!"
),
false
);
}
break;
case "image":
if (ext === ".jpg" || ext === ".jpeg" || ext === ".png") {
cb(null, true);
} else {
cb(
new Error(
"Invalid file type, only .jpg, .jpeg, and .png files are allowed for image!"
),
false
);
}
break;
default:
cb(new Error("Invalid file type!"), false);
}
};
// Set up Multer untuk menangani upload
const upload = multer({
storage: memoryStorage,
fileFilter,
limits: {
fileSize: 100 * 1024 * 1024, // Total file size limit if needed
},
}).fields([
{ name: "video", maxCount: 1 },
{ name: "audio", maxCount: 1 },
{ name: "image", maxCount: 1 },
]);
// Middleware untuk menangani upload dan pengecekan file size
const handleUpload = (req, res, next) => {
upload(req, res, (err) => {
if (err) {
return response(400, null, err.message, res);
}
const files = req.files;
const video = files?.video ? files.video[0] : null;
const audio = files?.audio ? files.audio[0] : null;
const image = files?.image ? files.image[0] : null;
try {
let validFiles = true;
let errorMessages = [];
// Validate file sizes
if (video && video.size > 30 * 1024 * 1024) {
validFiles = false;
video.buffer = null;
errorMessages.push("Video file exceeds the size limit of 30MB");
}
if (audio && audio.size > 10 * 1024 * 1024) {
validFiles = false;
audio.buffer = null;
errorMessages.push("Audio file exceeds the size limit of 10MB");
}
if (image && image.size > 5 * 1024 * 1024) {
validFiles = false;
image.buffer = null;
errorMessages.push("Image file exceeds the size limit of 5MB");
}
if (validFiles) {
// Attach files to the request object for further processing
req.filesToSave = { video, audio, image };
next();
} else {
// Clear file buffers and return error response with specific messages
clearFileBuffers({ video, audio, image });
return response(400, null, errorMessages.join("; "), res);
}
} catch (error) {
console.log(error);
clearFileBuffers({ video, audio, image });
return response(500, null, "Internal Server Error", res);
}
});
};
// Function to clear file buffers
export const clearFileBuffers = (files) => {
for (const file of Object.values(files)) {
if (file && file.buffer) {
file.buffer = null;
}
}
};
export const generateHash = (subjectId, filename, bufferLength) => {
return crypto
.createHash("md5")
.update(subjectId + filename + bufferLength)
.digest("hex");
};
// Function to save files to disk
export const saveFileToDisk = (file, type, title, topicId, subjectId) => {
const formattedTitle = title.replace(/\s+/g, '').toLowerCase();
const ext = path.extname(file.originalname);
const hash = generateHash(subjectId, file.originalname, file.buffer.length);
const filename = `${topicId}-${formattedTitle}-${type}-${hash}${ext}`;
let folderPath;
switch (type) {
case "video":
folderPath = path.join("public/uploads/level/video");
break;
case "audio":
folderPath = path.join("public/uploads/level/audio");
break;
case "image":
folderPath = path.join("public/uploads/level/image");
break;
default:
folderPath = path.join("public/uploads/level");
}
if (!fs.existsSync(folderPath)) {
fs.mkdirSync(folderPath, { recursive: true });
}
const filepath = path.join(folderPath, filename);
fs.writeFileSync(filepath, file.buffer);
return filename;
};
export default handleUpload;

View File

@ -2,11 +2,18 @@ import { Sequelize } from "sequelize";
import UserModel from "./userModel.js";
import SubjectModel from "./subjectModel.js";
import TopicModel from "./topicModel.js";
import LevelModel from "./levelModel.js";
// Impor operator Op
const Op = Sequelize.Op;
const models = {
User: UserModel(Sequelize.DataTypes),
Subject: SubjectModel(Sequelize.DataTypes),
Topic: TopicModel(Sequelize.DataTypes),
Level: LevelModel(Sequelize.DataTypes),
Sequelize,
Op,
};
export default models;

103
models/levelModel.js Normal file
View File

@ -0,0 +1,103 @@
import db from "../database/db.js";
const LevelModel = (DataTypes) => {
const Levels = db.define(
"m_levels",
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
validate: {
notEmpty: true,
},
},
title: {
type: DataTypes.STRING(100),
allowNull: false,
validate: {
notEmpty: true,
},
},
subject_id: {
type: DataTypes.INTEGER,
allowNull: false,
validate: {
notEmpty: true,
},
references: {
model: 'm_subjects', // Name of the referenced table
key: 'id', // Key in the referenced table
},
},
topic_id: {
type: DataTypes.INTEGER,
allowNull: false,
validate: {
notEmpty: true,
},
references: {
model: 'm_topics', // Name of the referenced table
key: 'id', // Key in the referenced table
},
},
is_pretest: {
type: DataTypes.TINYINT(1),
allowNull: true,
defaultValue: 0,
validate: {
min: 0,
max: 1,
},
},
content: {
type: DataTypes.STRING(200),
allowNull: true,
},
video: {
type: DataTypes.STRING(200),
allowNull: true,
},
audio: {
type: DataTypes.STRING(200),
allowNull: true,
},
image: {
type: DataTypes.STRING(200),
allowNull: true,
},
youtube: {
type: DataTypes.STRING(200),
allowNull: true,
},
route1: {
type: DataTypes.INTEGER,
allowNull: false,
},
route2: {
type: DataTypes.INTEGER,
allowNull: false,
},
route3: {
type: DataTypes.INTEGER,
allowNull: false,
},
route4: {
type: DataTypes.INTEGER,
allowNull: false,
},
ts_entri: {
type: DataTypes.DATE,
allowNull: true,
defaultValue: DataTypes.NOW,
},
},
{
timestamps: false, // Disable Sequelize's automatic timestamp fields (createdAt, updatedAt)
tableName: "m_levels", // Ensure the table name matches the actual table name
}
);
return Levels;
};
export default LevelModel;

View File

@ -5,9 +5,10 @@ const UserModel = (DataTypes) => {
"users",
{
id: {
type: DataTypes.BIGINT,
type: DataTypes.UUID,
primaryKey: true,
autoIncrement: true,
defaultValue: DataTypes.UUIDV4,
allowNull: false,
validate: {
notEmpty: true,
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@ -2,11 +2,6 @@ const response = (statusCode, data, message, res) => {
res.status(statusCode).json({
payload: data,
message: message,
pagination: {
prev: "",
next: "",
max: "",
},
});
};

View File

@ -3,11 +3,13 @@ import user_routes from "./user.js";
import auth_routes from "./auth.js";
import subject_routes from "./subject.js";
import topic_routes from "./topic.js";
import level_routes from "./level.js";
const route = express();
route.use(user_routes);
route.use(auth_routes);
route.use(subject_routes);
route.use(topic_routes);
route.use(level_routes);
export default route;

30
routes/level.js Normal file
View File

@ -0,0 +1,30 @@
import express from "express";
import { getAllLevels, getAllLevelById, getLevels, getLevelById, createLevel, updateLevelById, deleteLevelById, getRoutes, getRouteById, updateRouteById } from "../controllers/level.js";
import { verifyLoginUser, adminOnly, teacherOnly } from "../middlewares/authUser.js";
import handleUpload from '../middlewares/uploadLevel.js';
import {checkMaxLevelsPerTopic } from '../middlewares/checkLevel.js';
const router = express.Router();
router.get("/levels", getAllLevels);
router.get("/levels/:id", getAllLevelById);
router.get("/level", getLevels);
router.get("/level/:id", getLevelById);
router.post("/level", handleUpload, checkMaxLevelsPerTopic, createLevel);
router.put("/level/:id", handleUpload, updateLevelById);
router.delete("/level/:id", deleteLevelById);
router.get("/route", getRoutes);
router.get("/route/:id", getRouteById);
router.put("/route/:id", updateRouteById);
export default router

View File

@ -5,7 +5,7 @@ import { verifyLoginUser, adminOnly, teacherOnly } from "../middlewares/authUser
const router = express.Router();
router.get("/user", verifyLoginUser, teacherOnly, getUsers);
router.get("/user", verifyLoginUser, adminOnly, getUsers);
router.get("/user/:id", getUserById);