Files
2026-06-15 00:27:35 +00:00

282 lines
8.1 KiB
JavaScript

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;
}