require('dotenv').config({ quiet: true }); const express = require('express'); const mongoose = require('mongoose'); const app = express(); const cors = require('cors'); const corsOrigins = (process.env.CORS_ORIGIN || 'http://localhost:5174') .split(',') .map((origin) => origin.trim()) .filter(Boolean); const isAllowedCorsOrigin = (origin) => { if (!origin) { return true; } if (corsOrigins.includes(origin)) { return true; } try { const { hostname } = new URL(origin); return hostname === 'localhost' || hostname === '127.0.0.1'; } catch { return false; } }; app.use(cors({ origin: (origin, callback) => { callback(null, isAllowedCorsOrigin(origin)); }, }), express.json()); const port = process.env.PORT || 5000; const { ObjectId } = mongoose.Types; const DB_HOST = process.env.DB_HOST || 'localhost'; const DB_PORT = process.env.DB_PORT || 27017; const DB_USERNAME = process.env.DB_USERNAME; const DB_PASSWORD = process.env.DB_PASSWORD; const DB_NAME = process.env.DB_NAME; const credentials = DB_USERNAME && DB_PASSWORD ? `${encodeURIComponent(DB_USERNAME)}:${encodeURIComponent(DB_PASSWORD)}@` : ''; const database = DB_NAME ? `/${DB_NAME}` : ''; const authSource = credentials && DB_NAME ? '?authSource=admin' : ''; const connectionString = `mongodb://${credentials}${DB_HOST}:${DB_PORT}${database}${authSource}`; mongoose.connect(connectionString).catch((error) => { console.error('Unable to connect to MongoDB.'); console.error(`Check DB_HOST, DB_PORT, DB_USERNAME, DB_PASSWORD, and DB_NAME. Current host: ${DB_HOST}:${DB_PORT}`); console.error(error); process.exit(1); }); const db = mongoose.connection; db.on('error', console.error.bind(console, 'connection error:')); db.once('open', function () { console.log(`connected to mongodb: ${DB_HOST}`); }); const asyncHandler = (handler) => async (request, response, next) => { try { await handler(request, response, next); } catch (error) { next(error); } }; const isValidObjectId = (id) => ObjectId.isValid(id); const removeUndefinedValues = (payload) => Object.fromEntries( Object.entries(payload).filter(([, value]) => value !== undefined) ); const getBoxPayload = (body) => removeUndefinedValues({ name: body.name, description: body.description, color: body.color, }); const getItemPayload = (body) => removeUndefinedValues({ name: body.name, description: body.description, quantity: body.quantity, }); const validateId = (id, response, label = 'ID') => { if (isValidObjectId(id)) { return true; } response.status(400).json({ error: `Invalid ${label}` }); return false; }; /*------------MODELS------------*/ //create schema const itemSchema = new mongoose.Schema({ name: { type: String, required: true, trim: true }, description: { type: String, default: '', trim: true }, quantity: { type: Number, required: true, min: 0, default: 1 }, }, { timestamps: true }); const boxSchema = new mongoose.Schema({ name: { type: String, required: true, trim: true }, description: { type: String, default: '', trim: true }, items: [itemSchema], sort: { type: Number, required: true, min: 0 }, color: { type: String, default: '#ffffff', trim: true }, }, { timestamps: true }); //compile schema into model const Box = mongoose.model('box', boxSchema, 'box'); /*------------/MODELS------------*/ /*------------CONTROLLER------------*/ app.listen(port, () => console.log(`Listening on port ${port}`)); // get all boxes app.get('/boxes', asyncHandler(async (request, response) => { const boxes = await getAllBoxes(); response.send(boxes); })); // create box app.post('/boxes', asyncHandler(async (request, response) => { const lastBox = await Box.findOne().sort({ sort: -1 }).select('sort'); const newBox = await Box.create({ ...getBoxPayload(request.body), sort: lastBox ? lastBox.sort + 1 : 0, }); response.status(201).json(newBox); })); // update box app.put('/boxes/:boxId', asyncHandler(async (request, response) => { const { boxId } = request.params; if (!validateId(boxId, response, 'box ID')) { return; } const updatedBox = await Box.findByIdAndUpdate( boxId, getBoxPayload(request.body), { returnDocument: 'after', runValidators: true } ); if (!updatedBox) { return response.status(404).json({ error: 'Box not found' }); } response.status(200).json(updatedBox); })); // delete box app.delete('/boxes/:boxId', asyncHandler(async (request, response) => { const { boxId } = request.params; if (!validateId(boxId, response, 'box ID')) { return; } const result = await Box.findByIdAndDelete(boxId); if (!result) { return response.status(404).json({ success: false, message: 'Box not found' }); } response.status(200).json({ success: true, message: 'Box deleted', data: result }); })); // create item app.post('/boxes/:boxId/items', asyncHandler(async (request, response) => { const { boxId } = request.params; if (!validateId(boxId, response, 'box ID')) { return; } const box = await Box.findById(boxId); if (!box) { return response.status(404).json({ error: 'Box not found' }); } const item = box.items.create(getItemPayload(request.body)); box.items.push(item); await box.save(); response.status(201).json(item); })); // edit item app.put('/boxes/:boxId/items/:itemId', asyncHandler(async (request, response) => { const { boxId, itemId } = request.params; if (!validateId(boxId, response, 'box ID') || !validateId(itemId, response, 'item ID')) { return; } const box = await Box.findById(boxId); if (!box) { return response.status(404).json({ error: 'Box not found' }); } const item = box.items.id(itemId); if (!item) { return response.status(404).json({ error: 'Item not found' }); } item.set(getItemPayload(request.body)); await box.save(); response.status(200).json(item); })); // delete item from box app.delete('/boxes/:boxId/items/:itemId', asyncHandler(async (request, response) => { const { boxId, itemId } = request.params; if (!validateId(boxId, response, 'box ID') || !validateId(itemId, response, 'item ID')) { return; } const box = await Box.findById(boxId); if (!box) { return response.status(404).json({ error: 'Box not found' }); } const item = box.items.id(itemId); if (!item) { return response.status(404).json({ error: 'Item not found' }); } item.deleteOne(); await box.save(); response.json(box); })); //move item app.patch('/items/:itemId', asyncHandler(async (req, res) => { const { itemId } = req.params; const { boxId: targetBoxId } = req.body; if (!targetBoxId) return res.status(400).json({ error: 'Target boxId is required' }); if (!validateId(itemId, res, 'item ID') || !validateId(targetBoxId, res, 'target box ID')) { return; } const sourceBox = await Box.findOne({ "items._id": itemId }); const targetBox = await Box.findById(targetBoxId); if (!targetBox) return res.status(404).json({ error: 'Target box not found' }); if (!sourceBox) return res.status(404).json({ error: 'Source box not found' }); if (sourceBox._id.equals(targetBox._id)) return res.status(204).send(); const item = sourceBox.items.id(itemId); if (!item) return res.status(404).json({ error: 'Item not found' }); const itemToMove = item.toObject(); item.deleteOne(); targetBox.items.push(itemToMove); await sourceBox.save(); await targetBox.save(); res.status(204).send(); })); app.use((error, request, response, next) => { console.error(error); if (error.name === 'ValidationError') { return response.status(400).json({ error: error.message }); } response.status(500).json({ error: 'Internal server error' }); }); /*--------/CONTROLLER------------*/ const getAllBoxes = async (queryParameters) => { queryParameters = queryParameters ?? {}; const data = await Box.find(queryParameters).sort({ sort: 1 }); return data; }