282 lines
8.1 KiB
JavaScript
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;
|
|
}
|