diff --git a/config/index.js b/config/index.js index 8ed5673..2ee4f79 100644 --- a/config/index.js +++ b/config/index.js @@ -46,6 +46,11 @@ async function query(text, params = []) { return request.query(sqlText); } +function isValidDate(dateStr) { + const d = new Date(dateStr); + return !isNaN(d.getTime()); // true kalau valid +} + /** * Build filter query */ @@ -71,10 +76,24 @@ function buildFilterQuery(filterQuery = [], fixedParams = []) { queryParams.push(f.param ? 1 : 0); whereConditions.push(`${f.column} = $${queryParams.length}`); break; + + case 'between': + if (Array.isArray(f.param) && f.param.length === 2) { + const from = f.param[0]; + const to = f.param[1]; + if (isValidDate(from) && isValidDate(to)) { + queryParams.push(from); + queryParams.push(to); + whereConditions.push( + `${f.column} BETWEEN $${queryParams.length - 1} AND $${queryParams.length}` + ); + } + } + break; } }); - return { whereConditions, queryParams }; + return { whereConditions, whereParamAnd: queryParams }; } /** @@ -96,13 +115,17 @@ function buildStringOrIlike(columnParam, criteria, fixedParams = []) { ? `AND (${orStringConditions.join(" OR ")})` : ""; - return { whereOrConditions: whereClause, whereParam: queryParams }; + return { whereOrConditions: whereClause, whereParamOr: queryParams }; } /** * Build dynamic UPDATE */ function buildDynamicUpdate(table, data, where) { + + data.updated_by = data.userId + delete data.userId; + const setParts = []; const values = []; let index = 1; @@ -118,8 +141,8 @@ function buildDynamicUpdate(table, data, where) { throw new Error("Tidak ada kolom untuk diupdate"); } - // updated_at otomatis pakai GETDATE() - setParts.push(`updated_at = GETDATE()`); + // updated_at otomatis pakai CURRENT_TIMESTAMP + setParts.push(`updated_at = CURRENT_TIMESTAMP`); const whereParts = []; for (const [key, value] of Object.entries(where)) { @@ -140,6 +163,11 @@ function buildDynamicUpdate(table, data, where) { * Build dynamic INSERT */ function buildDynamicInsert(table, data) { + + data.created_by = data.userId + data.updated_by = data.userId + delete data.userId; + const columns = []; const placeholders = []; const values = []; @@ -159,7 +187,7 @@ function buildDynamicInsert(table, data) { // created_at & updated_at otomatis columns.push("created_at", "updated_at"); - placeholders.push("GETDATE()", "GETDATE()"); + placeholders.push("CURRENT_TIMESTAMP", "CURRENT_TIMESTAMP"); const query = ` INSERT INTO ${table} (${columns.join(", ")}) diff --git a/controllers/device.controller.js b/controllers/device.controller.js index 2e45334..7b2481c 100644 --- a/controllers/device.controller.js +++ b/controllers/device.controller.js @@ -1,101 +1,70 @@ const DeviceService = require('../services/device.service'); -const { deviceSchema, deviceUpdateSchema } = require('../helpers/validation'); -const { setResponse } = require('../helpers/utils'); +const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils'); +const { insertDeviceSchema, updateDeviceSchema } = require('../validate/device.schema'); class DeviceController { // Get all devices - static async getAll(req, res) { - try { - const { search } = req.query; - const devices = await DeviceService.getAllDevices(search || ''); - return res.status(200).json(setResponse(devices, 'Devices retrieved successfully', 200)); - } catch (err) { - return res.status(err.statusCode || 500).json( - setResponse([], err.message || 'Failed to retrieve devices', err.statusCode || 500) - ); - } + static async getAll(req, res) { + const queryParams = req.query; + + const results = await DeviceService.getAllDevices(queryParams); + const response = await setResponsePaging(queryParams, results, 'Device found') + + res.status(response.statusCode).json(response); } // Get device by ID static async getById(req, res) { - try { - const { id } = req.params; - const device = await DeviceService.getDeviceById(id); - return res.status(200).json(setResponse(device, 'Device retrieved successfully', 200)); - } catch (err) { - return res.status(err.statusCode || 500).json( - setResponse([], err.message || 'Device not found', err.statusCode || 500) - ); - } + const { id } = req.params; + + const results = await DeviceService.getDeviceById(id); + const response = await setResponse(results, 'Device found') + + res.status(response.statusCode).json(response); } // Create device static async create(req, res) { - try { - const { error, value } = deviceSchema.validate(req.body || {}, { abortEarly: false }); - if (error) { - const errors = error.details.reduce((acc, cur) => { - const field = Array.isArray(cur.path) ? cur.path.join('.') : String(cur.path); - if (!acc[field]) acc[field] = []; - acc[field].push(cur.message); - return acc; - }, {}); - return res.status(400).json(setResponse(errors, 'Validation failed', 400)); - } + const { error, value } = await checkValidate(insertDeviceSchema, req) - const newDevice = await DeviceService.createDevice(value, req.user.user_id); - - return res.status(201).json( - setResponse(newDevice, 'Device created successfully', 201) - ); - } catch (err) { - return res.status(err.statusCode || 500).json( - setResponse([], err.message || 'Failed to create device', err.statusCode || 500) - ); + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); } + + value.userId = req.user.user_id + + const results = await DeviceService.createDevice(value); + const response = await setResponse(results, 'Device created successfully') + + return res.status(response.statusCode).json(response); } // Update device static async update(req, res) { - try { - const { id } = req.params; - const { error, value } = deviceUpdateSchema.validate(req.body || {}, { abortEarly: false }); - if (error) { - const errors = error.details.reduce((acc, cur) => { - const field = Array.isArray(cur.path) ? cur.path.join('.') : String(cur.path); - if (!acc[field]) acc[field] = []; - acc[field].push(cur.message); - return acc; - }, {}); - return res.status(400).json(setResponse(errors, 'Validation failed', 400)); - } + const { id } = req.params; - const updatedDevice = await DeviceService.updateDevice(id, value, req.user.user_Id); + const { error, value } = checkValidate(updateDeviceSchema, req) - return res.status(200).json( - setResponse(updatedDevice.data, 'Device updated successfully', 200) - ); - } catch (err) { - return res.status(err.statusCode || 500).json( - setResponse([], err.message || 'Failed to update device', err.statusCode || 500) - ); + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); } + + value.userId = req.user.user_id + + const results = await DeviceService.updateDevice(id, value); + const response = await setResponse(results, 'Device updated successfully') + + res.status(response.statusCode).json(response); } // Soft delete device static async delete(req, res) { - try { - const { id } = req.params; + const { id } = req.params; - await DeviceService.deleteDevice(id, req.user.userId); - return res.status(200).json( - setResponse([], 'Device deleted successfully', 200) - ); - } catch (err) { - return res.status(err.statusCode || 500).json( - setResponse([], err.message || 'Failed to delete device', err.statusCode || 500) - ); - } + const results = await DeviceService.deleteDevice(id, req.user.user_id); + const response = await setResponse(results, 'Device deleted successfully') + + res.status(response.statusCode).json(response); } } diff --git a/db/device.db.js b/db/device.db.js index d21e4a8..3d770ec 100644 --- a/db/device.db.js +++ b/db/device.db.js @@ -2,67 +2,97 @@ const pool = require("../config"); // Get all devices const getAllDevicesDb = async (searchParams = {}) => { - const { whereConditions, queryParams } = pool.buildFilterQuery([ - { column: "d.device_name", param: searchParams.name, type: "string" }, - { column: "d.device_code", param: searchParams.code, type: "string" }, - { column: "d.device_location", param: searchParams.location, type: "string" }, - { column: "b.brand_name", param: searchParams.brand, type: "string" }, - ]); - const whereClause = whereConditions.length - ? `AND ${whereConditions.join(" AND ")}` - : ""; + queryParams = [] + if (searchParams.limit) { + const page = Number(searchParams.page ?? 1) - 1 + queryParams = [Number(searchParams.limit ?? 10), page]; + } + + const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike([ + "a.device_name", + "a.device_code", + "a.device_location", + "a.ip_address", + "b.brand_name" + ], searchParams.criteria, queryParams); + + queryParams = whereParamOr ? whereParamOr : queryParams + + const { whereConditions, whereParamAnd } = pool.buildFilterQuery([ + { column: "a.device_name", param: searchParams.name, type: "string" }, + { column: "a.device_code", param: searchParams.code, type: "string" }, + { column: "a.device_location", param: searchParams.location, type: "string" }, + { column: "b.brand_name", param: searchParams.brand, type: "string" }, + ], queryParams); + + queryParams = whereParamAnd ? whereParamAnd : queryParams const queryText = ` - SELECT d.*, b.brand_name - FROM m_device d - LEFT JOIN m_brands b ON d.brand_id = b.brand_id - WHERE d.deleted_at IS NULL ${whereClause} - ORDER BY d.device_id ASC + SELECT COUNT(*) OVER() AS total_data, a.*, b.brand_name + FROM m_device a + LEFT JOIN m_brands b ON a.brand_id = b.brand_id + WHERE a.deleted_at IS NULL + ${whereConditions.length > 0 ? ` AND ${whereConditions.join(' AND ')}` : ''} + ${whereOrConditions ? whereOrConditions : ''} + ORDER BY a.device_id ASC + ${searchParams.limit ? `OFFSET $2 ROWS FETCH NEXT $1 ROWS ONLY` : ''}; `; const result = await pool.query(queryText, queryParams); - return result.recordset; + + // Menghitung total data. + const total = result?.recordset.length > 0 ? parseInt(result.recordset[0].total_data, 10) : 0; + + // Mengembalikan data dan total. + return { data: result.recordset, total }; }; const getDeviceByIdDb = async (id) => { const queryText = ` - SELECT d.*, b.brand_name - FROM m_device d - LEFT JOIN m_brands b ON d.brand_id = b.brand_id - WHERE d.device_id = $1 AND d.deleted_at IS NULL + SELECT a.*, b.brand_name + FROM m_device a + LEFT JOIN m_brands b ON a.brand_id = b.brand_id + WHERE a.device_id = $1 AND a.deleted_at IS NULL `; const result = await pool.query(queryText, [id]); - return result.recordset[0]; -}; - -const getDeviceByCodeDb = async (code) => { - const queryText = ` - SELECT d.*, b.brand_name - FROM m_device d - LEFT JOIN m_brands b ON d.brand_id = b.brand_id - WHERE d.device_code = $1 AND d.deleted_at IS NULL - `; - const result = await pool.query(queryText, [code]); - return result.recordset[0]; + return result.recordset; }; const createDeviceDb = async (data) => { - const { query: queryText, values } = pool.buildDynamicInsert("m_device", data); + + const newCode = await pool.generateKode("DVC", "m_device", "device_code") + + const store = { + ...data, + device_code: newCode, + } + + const { query: queryText, values } = pool.buildDynamicInsert("m_device", store); const result = await pool.query(queryText, values); const insertedId = result.recordset[0]?.inserted_id; return insertedId ? await getDeviceByIdDb(insertedId) : null; }; const updateDeviceDb = async (id, data) => { - const { query: queryText, values } = pool.buildDynamicUpdate("m_device", data, { device_id: id }); + + const store = { + ...data + } + + // Kondisi WHERE + const whereData = { + device_id: id + }; + + const { query: queryText, values } = pool.buildDynamicUpdate("m_device", store, whereData); await pool.query(`${queryText} AND deleted_at IS NULL`, values); return getDeviceByIdDb(id); }; -const softDeleteDeviceDb = async (id, deletedBy) => { +const deleteDeviceDb = async (id, deletedBy) => { const queryText = ` UPDATE m_device - SET deleted_at = GETDATE(), deleted_by = $1 + SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1 WHERE device_id = $2 AND deleted_at IS NULL `; await pool.query(queryText, [deletedBy, id]); @@ -72,8 +102,7 @@ const softDeleteDeviceDb = async (id, deletedBy) => { module.exports = { getAllDevicesDb, getDeviceByIdDb, - getDeviceByCodeDb, createDeviceDb, updateDeviceDb, - softDeleteDeviceDb, + deleteDeviceDb, }; diff --git a/helpers/utils.js b/helpers/utils.js index 716a5cc..7ed4a3e 100644 --- a/helpers/utils.js +++ b/helpers/utils.js @@ -2,42 +2,28 @@ const setResponse = (data = null, message = "success", statusCode = 200) => { const total = Array.isArray(data) ? data.length : null; return { - data, - total, message, - statusCode + statusCode, + rows: total, + data, }; }; -const setResponsePaging = async (data = [], total, limit, page, message = "success", statusCode = 200) => { +const setResponsePaging = async (queryParam, data = [], message = "success", statusCode = 200) => { - const totalPages = Math.ceil(total / limit); + const totalPages = Math.ceil(data?.total / Number(queryParam.limit ?? 0)); const response = { message, statusCode, - data, - total: data.length, + rows: data?.data?.length, paging: { - total, - limit, - page, - page_total: totalPages - } - } - - return response -}; - -const setPaging = async (total, limit, page) => { - - const totalPages = Math.ceil(total / limit); - - const response = { - total, - limit, - page, - page_total: totalPages + current_limit: Number(queryParam.limit ?? 0), + current_page: Number(queryParam.page ?? 0), + total_limit: data?.total, + total_page: totalPages + }, + data: data?.data ?? [] } return response @@ -86,4 +72,19 @@ function orderByClauseQuery(orderParams) { return orderByClause } -module.exports = { setResponse, setResponsePaging, setPaging, convertId, formatToYYYYMMDD, orderByClauseQuery }; +const checkValidate = (validateSchema, req) => { + const { error, value } = validateSchema.validate(req.body || {}, { abortEarly: false }); + if (error) { + const errors = error.details.reduce((acc, cur) => { + const field = Array.isArray(cur.path) ? cur.path.join('.') : String(cur.path); + if (!acc[field]) acc[field] = []; + acc[field].push(cur.message); + return acc; + }, {}); + return { error: errors, value } + } + + return { error, value } +} + +module.exports = { setResponse, setResponsePaging, convertId, formatToYYYYMMDD, orderByClauseQuery, checkValidate }; diff --git a/helpers/validation.js b/helpers/validation.js index 02a314e..a1c2436 100644 --- a/helpers/validation.js +++ b/helpers/validation.js @@ -46,36 +46,6 @@ const newPasswordSchema = Joi.object({ }) }) -// ======================== -// Device Validation -// ======================== -const deviceSchema = Joi.object({ - device_code: Joi.string().max(100).required(), - device_name: Joi.string().max(100).required(), - device_status: Joi.boolean().required(), - device_location: Joi.string().max(100).required(), - device_description: Joi.string().required(), - ip_address: Joi.string() - .ip({ version: ['ipv4', 'ipv6'] }) - .required() - .messages({ - 'string.ip': 'IP address must be a valid IPv4 or IPv6 address' - }) -}); - -const deviceUpdateSchema = Joi.object({ - device_code: Joi.string().max(100), - device_name: Joi.string().max(100), - device_status: Joi.boolean(), - device_location: Joi.string().max(100), - device_description: Joi.string(), - ip_address: Joi.string() - .ip({ version: ['ipv4', 'ipv6'] }) - .messages({ - 'string.ip': 'IP address must be a valid IPv4 or IPv6 address' - }) -}).min(1); - // ======================== // Users Validation // ======================== @@ -101,14 +71,12 @@ const userSchema = Joi.object({ 'string.min': 'Password must be at least 8 characters long', 'string.pattern.name': 'Password must contain at least one {#name}' }), - role_id : Joi.number().integer().min(1) + role_id: Joi.number().integer().min(1) }); module.exports = { registerSchema, loginSchema, newPasswordSchema, - deviceSchema, - deviceUpdateSchema, userSchema, }; diff --git a/index.js b/index.js index 552b3a5..45fc2ac 100644 --- a/index.js +++ b/index.js @@ -5,6 +5,6 @@ const { logger } = require("./utils/logger"); const server = http.createServer(app); -const PORT = process.env.PORT || 9524; +const PORT = process.env.PORT || 9530; server.listen(PORT, () => logger.info(`Magic happening on port: ${PORT}`)); diff --git a/routes/device.route.js b/routes/device.route.js index 5411b23..5c74061 100644 --- a/routes/device.route.js +++ b/routes/device.route.js @@ -5,10 +5,13 @@ const verifyAccess = require("../middleware/verifyAccess") const router = express.Router(); -router.get('/', verifyToken.verifyAccessToken, DeviceController.getAll); -router.get('/:id', verifyToken.verifyAccessToken, DeviceController.getById); -router.post('/', verifyToken.verifyAccessToken, verifyAccess(), DeviceController.create); -router.put('/:id', verifyToken.verifyAccessToken, verifyAccess(), DeviceController.update); -router.delete('/:id', verifyToken.verifyAccessToken, verifyAccess(), DeviceController.delete); +router.route("/") + .get(verifyToken.verifyAccessToken, DeviceController.getAll) + .post(verifyToken.verifyAccessToken, verifyAccess(), DeviceController.create); + +router.route("/:id") + .get(verifyToken.verifyAccessToken, DeviceController.getById) + .put(verifyToken.verifyAccessToken, verifyAccess(), DeviceController.update) + .delete(verifyToken.verifyAccessToken, verifyAccess(), DeviceController.delete); module.exports = router; \ No newline at end of file diff --git a/services/device.service.js b/services/device.service.js index b90c4ff..978b336 100644 --- a/services/device.service.js +++ b/services/device.service.js @@ -1,81 +1,87 @@ const { getAllDevicesDb, getDeviceByIdDb, - getDeviceByCodeDb, createDeviceDb, updateDeviceDb, - softDeleteDeviceDb, - searchDevicesDb + deleteDeviceDb } = require('../db/device.db'); const { ErrorHandler } = require('../helpers/error'); class DeviceService { // Get all devices - static async getAllDevices(search) { - if (!search || search.trim() === '') { - return await getAllDevicesDb(); + static async getAllDevices(param) { + try { + const results = await getAllDevicesDb(param); + + results.data.map(element => { + }); + + return results + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); } - return await searchDevicesDb(search); } // Get device by ID static async getDeviceById(id) { - const device = await getDeviceByIdDb(id); - if (!device) throw new ErrorHandler(404, 'Device not found'); - return device; - } + try { + const result = await getDeviceByIdDb(id); - // Get device by code - static async getDeviceByCode(code) { - const device = await getDeviceByCodeDb(code); - if (!device) throw new ErrorHandler(404, 'Device not found'); - return device; + if (result.length < 1) throw new ErrorHandler(404, 'Device not found'); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } } // Create device - static async createDevice(data, userId) { - if (!data || typeof data !== 'object') data = {}; + static async createDevice(data) { + try { + if (!data || typeof data !== 'object') data = {}; - data.created_by = userId; + const result = await createDeviceDb(data); - // Cek kode unik - const existingDevice = await getDeviceByCodeDb(data.device_code); - if (existingDevice) { - throw new ErrorHandler(400, 'Device code already exists'); + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); } - - const newDevice = await createDeviceDb(data); - return newDevice; } // Update device - static async updateDevice(id, data, user_Id) { - if (!data || typeof data !== 'object') data = {}; + static async updateDevice(id, data) { + try { + if (!data || typeof data !== 'object') data = {}; - const existingDevice = await getDeviceByIdDb(id); - if (!existingDevice) { - throw new ErrorHandler(404, 'Device not found'); + const dataExist = await getDeviceByIdDb(id); + + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Device not found'); + } + + const result = await updateDeviceDb(id, data); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); } - - data.updated_by = user_Id; - - const updatedDevice = await updateDeviceDb(id, data); - - return { - message: 'Device updated successfully', - data: updatedDevice, - }; } // Soft delete device static async deleteDevice(id, userId) { - const existingDevice = await getDeviceByIdDb(id); - if (!existingDevice) { - throw new ErrorHandler(404, 'Device not found'); - } + try { + const dataExist = await getDeviceByIdDb(id); - await softDeleteDeviceDb(id, userId); - return { message: 'Device deleted successfully' }; + if (dataExist.length < 1) { + throw new ErrorHandler(404, 'Device not found'); + } + + const result = await deleteDeviceDb(id, userId); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } } } diff --git a/validate/device.schema.js b/validate/device.schema.js new file mode 100644 index 0000000..a8e74e5 --- /dev/null +++ b/validate/device.schema.js @@ -0,0 +1,36 @@ +// ======================== +// Device Validation + +const Joi = require("joi"); + +// ======================== +const insertDeviceSchema = Joi.object({ + device_name: Joi.string().max(100).required(), + device_status: Joi.boolean().required(), + device_location: Joi.string().max(100).required(), + device_description: Joi.string().required(), + ip_address: Joi.string() + .ip({ version: ['ipv4', 'ipv6'] }) + .required() + .messages({ + 'string.ip': 'IP address must be a valid IPv4 or IPv6 address' + }) +}); + +const updateDeviceSchema = Joi.object({ + device_name: Joi.string().max(100), + device_status: Joi.boolean(), + device_location: Joi.string().max(100), + device_description: Joi.string(), + ip_address: Joi.string() + .ip({ version: ['ipv4', 'ipv6'] }) + .messages({ + 'string.ip': 'IP address must be a valid IPv4 or IPv6 address' + }) +}).min(1); + + +// ✅ Export dengan CommonJS +module.exports = { + insertDeviceSchema, updateDeviceSchema +}; \ No newline at end of file