tududi/backend/routes/inbox.js
Chris 3796324675
Feat add inbox flow (#676)
* Replace quick capture

* Transfer to inbox

* Fix inbox edit

* fixup! Fix inbox edit

* fixup! fixup! Fix inbox edit

* fixup! fixup! fixup! Fix inbox edit

* fixup! fixup! fixup! fixup! Fix inbox edit

* fixup! fixup! fixup! fixup! fixup! Fix inbox edit

* Add long text

* fixup! Add long text

* fixup! fixup! Add long text

* fixup! fixup! fixup! Add long text

* fixup! fixup! fixup! fixup! Add long text

* fixup! fixup! fixup! fixup! fixup! Add long text

* fixup! fixup! fixup! fixup! fixup! fixup! Add long text

* fixup! fixup! fixup! fixup! fixup! fixup! fixup! Add long text

* fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Add long text

* fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Add long text

* fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Add long text

* fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Add long text

* fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Add long text
2025-12-08 09:21:35 +02:00

317 lines
9.5 KiB
JavaScript

const express = require('express');
const { InboxItem } = require('../models');
const { processInboxItem } = require('../services/inboxProcessingService');
const { isValidUid } = require('../utils/slug-utils');
const _ = require('lodash');
const { logError } = require('../services/logService');
const { getAuthenticatedUserId } = require('../utils/request-utils');
const router = express.Router();
const TITLE_MAX_LENGTH = 120;
const buildTitleFromContent = (text) => {
const normalized = text.trim();
if (normalized.length <= TITLE_MAX_LENGTH) {
return normalized;
}
return `${normalized.slice(0, TITLE_MAX_LENGTH).trim()}...`;
};
const getUserIdOrUnauthorized = (req, res) => {
const userId = getAuthenticatedUserId(req);
if (!userId) {
res.status(401).json({ error: 'Authentication required' });
return null;
}
return userId;
};
router.get('/inbox', async (req, res) => {
try {
const userId = getUserIdOrUnauthorized(req, res);
if (!userId) return;
// Check if pagination parameters are provided
const hasPagination =
!_.isEmpty(req.query.limit) || !_.isEmpty(req.query.offset);
if (hasPagination) {
// Parse pagination parameters
const limit = parseInt(req.query.limit, 10) || 20; // Default to 20 items
const offset = parseInt(req.query.offset, 10) || 0;
// Get total count for pagination info
const totalCount = await InboxItem.count({
where: {
user_id: userId,
status: 'added',
},
});
const items = await InboxItem.findAll({
where: {
user_id: userId,
status: 'added',
},
order: [['created_at', 'DESC']],
limit: limit,
offset: offset,
});
res.json({
items: items,
pagination: {
total: totalCount,
limit: limit,
offset: offset,
hasMore: offset + items.length < totalCount,
},
});
} else {
// Return simple array for backward compatibility (used by tests)
const items = await InboxItem.findAll({
where: {
user_id: userId,
status: 'added',
},
order: [['created_at', 'DESC']],
});
res.json(items);
}
} catch (error) {
logError('Error fetching inbox items:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
router.post('/inbox', async (req, res) => {
try {
const userId = getUserIdOrUnauthorized(req, res);
if (!userId) return;
const { content, source } = req.body;
if (
!content ||
typeof content !== 'string' ||
_.isEmpty(content.trim())
) {
return res.status(400).json({ error: 'Content is required' });
}
// Ensure source is never null/undefined
const finalSource = source && source.trim() ? source.trim() : 'manual';
const normalizedContent = content.trim();
if (!normalizedContent) {
return res.status(400).json({ error: 'Content cannot be empty' });
}
const generatedTitle = buildTitleFromContent(normalizedContent);
const item = await InboxItem.create({
content: normalizedContent,
title: generatedTitle,
source: finalSource,
user_id: userId,
});
res.status(201).json(
_.pick(item, [
'uid',
'title',
'content',
'status',
'source',
'created_at',
'updated_at',
])
);
} catch (error) {
logError('Error creating inbox item:', error);
res.status(400).json({
error: 'There was a problem creating the inbox item.',
details: error.errors
? error.errors.map((e) => e.message)
: [error.message],
});
}
});
// GET /api/inbox/:uid
router.get('/inbox/:uid', async (req, res) => {
try {
const userId = getUserIdOrUnauthorized(req, res);
if (!userId) return;
if (!isValidUid(req.params.uid)) {
return res.status(400).json({ error: 'Invalid UID' });
}
const item = await InboxItem.findOne({
where: { uid: req.params.uid, user_id: userId },
attributes: [
'uid',
'title',
'content',
'status',
'source',
'created_at',
'updated_at',
],
});
if (_.isEmpty(item)) {
return res.status(404).json({ error: 'Inbox item not found.' });
}
res.json(item);
} catch (error) {
logError('Error fetching inbox item:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// PATCH /api/inbox/:uid
router.patch('/inbox/:uid', async (req, res) => {
try {
const userId = getUserIdOrUnauthorized(req, res);
if (!userId) return;
if (!isValidUid(req.params.uid)) {
return res.status(400).json({ error: 'Invalid UID' });
}
const item = await InboxItem.findOne({
where: { uid: req.params.uid, user_id: userId },
});
if (_.isEmpty(item)) {
return res.status(404).json({ error: 'Inbox item not found.' });
}
const { content, status } = req.body;
const updateData = {};
if (content != null) {
if (typeof content !== 'string') {
return res
.status(400)
.json({ error: 'Content must be a string' });
}
const normalizedContent = content.trim();
if (!normalizedContent) {
return res
.status(400)
.json({ error: 'Content cannot be empty' });
}
updateData.content = normalizedContent;
updateData.title = buildTitleFromContent(normalizedContent);
}
if (status != null) updateData.status = status;
await item.update(updateData);
res.json(
_.pick(item, [
'uid',
'title',
'content',
'status',
'source',
'created_at',
'updated_at',
])
);
} catch (error) {
logError('Error updating inbox item:', error);
res.status(400).json({
error: 'There was a problem updating the inbox item.',
details: error.errors
? error.errors.map((e) => e.message)
: [error.message],
});
}
});
// DELETE /api/inbox/:uid
router.delete('/inbox/:uid', async (req, res) => {
try {
const userId = getUserIdOrUnauthorized(req, res);
if (!userId) return;
if (!isValidUid(req.params.uid)) {
return res.status(400).json({ error: 'Invalid UID' });
}
const item = await InboxItem.findOne({
where: { uid: req.params.uid, user_id: userId },
});
if (_.isEmpty(item)) {
return res.status(404).json({ error: 'Inbox item not found.' });
}
// Mark as deleted instead of actual deletion
await item.update({ status: 'deleted' });
res.json({ message: 'Inbox item successfully deleted' });
} catch (error) {
logError('Error deleting inbox item:', error);
res.status(400).json({
error: 'There was a problem deleting the inbox item.',
});
}
});
// PATCH /api/inbox/:uid/process
router.patch('/inbox/:uid/process', async (req, res) => {
try {
const userId = getUserIdOrUnauthorized(req, res);
if (!userId) return;
if (!isValidUid(req.params.uid)) {
return res.status(400).json({ error: 'Invalid UID' });
}
const item = await InboxItem.findOne({
where: { uid: req.params.uid, user_id: userId },
});
if (_.isEmpty(item)) {
return res.status(404).json({ error: 'Inbox item not found.' });
}
await item.update({ status: 'processed' });
res.json(
_.pick(item, [
'uid',
'title',
'content',
'status',
'source',
'created_at',
'updated_at',
])
);
} catch (error) {
logError('Error processing inbox item:', error);
res.status(400).json({
error: 'There was a problem processing the inbox item.',
});
}
});
// POST /api/inbox/analyze-text
router.post('/inbox/analyze-text', async (req, res) => {
try {
const { content } = req.body;
if (!content || typeof content !== 'string') {
return res
.status(400)
.json({ error: 'Content is required and must be a string' });
}
// Process the text using the inbox processing service
const result = processInboxItem(content);
res.json(result);
} catch (error) {
logError('Error analyzing inbox text:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
module.exports = router;