329 lines
10 KiB
JavaScript
329 lines
10 KiB
JavaScript
const express = require('express');
|
|
const { Note, Tag, Project, sequelize } = require('../models');
|
|
const { Op } = require('sequelize');
|
|
const { extractUidFromSlug } = require('../utils/slug-utils');
|
|
const { validateTagName } = require('../utils/validation');
|
|
const router = express.Router();
|
|
|
|
// Helper function to update note tags
|
|
async function updateNoteTags(note, tagsArray, userId) {
|
|
if (!tagsArray || tagsArray.length === 0) {
|
|
await note.setTags([]);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Validate and filter tag names
|
|
const validTagNames = [];
|
|
const invalidTags = [];
|
|
|
|
for (const name of tagsArray) {
|
|
const validation = validateTagName(name);
|
|
if (validation.valid) {
|
|
// Check for duplicates
|
|
if (!validTagNames.includes(validation.name)) {
|
|
validTagNames.push(validation.name);
|
|
}
|
|
} else {
|
|
invalidTags.push({ name, error: validation.error });
|
|
}
|
|
}
|
|
|
|
// If there are invalid tags, throw an error
|
|
if (invalidTags.length > 0) {
|
|
throw new Error(
|
|
`Invalid tag names: ${invalidTags.map((t) => `"${t.name}" (${t.error})`).join(', ')}`
|
|
);
|
|
}
|
|
|
|
const tags = await Promise.all(
|
|
validTagNames.map(async (name) => {
|
|
const [tag] = await Tag.findOrCreate({
|
|
where: { name, user_id: userId },
|
|
defaults: { name, user_id: userId },
|
|
});
|
|
return tag;
|
|
})
|
|
);
|
|
await note.setTags(tags);
|
|
} catch (error) {
|
|
console.error('Failed to update tags:', error.message);
|
|
throw error; // Re-throw to handle at route level
|
|
}
|
|
}
|
|
|
|
// GET /api/notes
|
|
router.get('/notes', async (req, res) => {
|
|
try {
|
|
if (!req.session || !req.session.userId) {
|
|
return res.status(401).json({ error: 'Authentication required' });
|
|
}
|
|
|
|
const orderBy = req.query.order_by || 'title:asc';
|
|
const [orderColumn, orderDirection] = orderBy.split(':');
|
|
|
|
let whereClause = { user_id: req.session.userId };
|
|
let includeClause = [
|
|
{
|
|
model: Tag,
|
|
attributes: ['id', 'name', 'uid'],
|
|
through: { attributes: [] },
|
|
},
|
|
{
|
|
model: Project,
|
|
required: false,
|
|
attributes: ['id', 'name', 'uid'],
|
|
},
|
|
];
|
|
|
|
// Filter by tag
|
|
if (req.query.tag) {
|
|
includeClause[0].where = { name: req.query.tag };
|
|
includeClause[0].required = true;
|
|
}
|
|
|
|
const notes = await Note.findAll({
|
|
where: whereClause,
|
|
include: includeClause,
|
|
order: [[orderColumn, orderDirection.toUpperCase()]],
|
|
distinct: true,
|
|
});
|
|
|
|
res.json(notes);
|
|
} catch (error) {
|
|
console.error('Error fetching notes:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
// GET /api/note/:id (supports both numeric ID and uid-slug)
|
|
router.get('/note/:id', async (req, res) => {
|
|
try {
|
|
if (!req.session || !req.session.userId) {
|
|
return res.status(401).json({ error: 'Authentication required' });
|
|
}
|
|
|
|
const identifier = req.params.id;
|
|
let whereClause;
|
|
|
|
// Check if identifier is numeric (regular ID) or uid-slug
|
|
if (/^\d+$/.test(identifier)) {
|
|
// It's a numeric ID
|
|
whereClause = {
|
|
id: parseInt(identifier),
|
|
user_id: req.session.userId,
|
|
};
|
|
} else {
|
|
// It's a uid-slug, extract the uid
|
|
const uid = extractUidFromSlug(identifier);
|
|
if (!uid) {
|
|
return res
|
|
.status(400)
|
|
.json({ error: 'Invalid note identifier' });
|
|
}
|
|
whereClause = { uid: uid, user_id: req.session.userId };
|
|
}
|
|
|
|
const note = await Note.findOne({
|
|
where: whereClause,
|
|
include: [
|
|
{
|
|
model: Tag,
|
|
attributes: ['id', 'name', 'uid'],
|
|
through: { attributes: [] },
|
|
},
|
|
{
|
|
model: Project,
|
|
required: false,
|
|
attributes: ['id', 'name', 'uid'],
|
|
},
|
|
],
|
|
});
|
|
|
|
if (!note) {
|
|
return res.status(404).json({ error: 'Note not found.' });
|
|
}
|
|
|
|
res.json(note);
|
|
} catch (error) {
|
|
console.error('Error fetching note:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
// POST /api/note
|
|
router.post('/note', async (req, res) => {
|
|
try {
|
|
if (!req.session || !req.session.userId) {
|
|
return res.status(401).json({ error: 'Authentication required' });
|
|
}
|
|
|
|
const { title, content, project_id, tags } = req.body;
|
|
|
|
const noteAttributes = {
|
|
title,
|
|
content,
|
|
user_id: req.session.userId,
|
|
};
|
|
|
|
// Handle project assignment
|
|
if (project_id && project_id.toString().trim()) {
|
|
const project = await Project.findOne({
|
|
where: { id: project_id, user_id: req.session.userId },
|
|
});
|
|
if (!project) {
|
|
return res.status(400).json({ error: 'Invalid project.' });
|
|
}
|
|
noteAttributes.project_id = project_id;
|
|
}
|
|
|
|
const note = await Note.create(noteAttributes);
|
|
|
|
// Handle tags - can be array of strings or array of objects with name property
|
|
let tagNames = [];
|
|
if (Array.isArray(tags)) {
|
|
if (tags.every((t) => typeof t === 'string')) {
|
|
tagNames = tags;
|
|
} else if (tags.every((t) => typeof t === 'object' && t.name)) {
|
|
tagNames = tags.map((t) => t.name);
|
|
}
|
|
}
|
|
|
|
await updateNoteTags(note, tagNames, req.session.userId);
|
|
|
|
// Reload note with associations
|
|
const noteWithAssociations = await Note.findByPk(note.id, {
|
|
include: [
|
|
{
|
|
model: Tag,
|
|
attributes: ['id', 'name', 'uid'],
|
|
through: { attributes: [] },
|
|
},
|
|
{
|
|
model: Project,
|
|
required: false,
|
|
attributes: ['id', 'name', 'uid'],
|
|
},
|
|
],
|
|
});
|
|
|
|
res.status(201).json({
|
|
...noteWithAssociations.toJSON(),
|
|
uid: noteWithAssociations.uid, // Explicitly include uid
|
|
});
|
|
} catch (error) {
|
|
console.error('Error creating note:', error);
|
|
res.status(400).json({
|
|
error: 'There was a problem creating the note.',
|
|
details: error.errors
|
|
? error.errors.map((e) => e.message)
|
|
: [error.message],
|
|
});
|
|
}
|
|
});
|
|
|
|
// PATCH /api/note/:id
|
|
router.patch('/note/:id', async (req, res) => {
|
|
try {
|
|
if (!req.session || !req.session.userId) {
|
|
return res.status(401).json({ error: 'Authentication required' });
|
|
}
|
|
|
|
const note = await Note.findOne({
|
|
where: { id: req.params.id, user_id: req.session.userId },
|
|
});
|
|
|
|
if (!note) {
|
|
return res.status(404).json({ error: 'Note not found.' });
|
|
}
|
|
|
|
const { title, content, project_id, tags } = req.body;
|
|
|
|
const updateData = {};
|
|
if (title !== undefined) updateData.title = title;
|
|
if (content !== undefined) updateData.content = content;
|
|
|
|
// Handle project assignment
|
|
if (project_id !== undefined) {
|
|
if (project_id && project_id.toString().trim()) {
|
|
const project = await Project.findOne({
|
|
where: { id: project_id, user_id: req.session.userId },
|
|
});
|
|
if (!project) {
|
|
return res.status(400).json({ error: 'Invalid project.' });
|
|
}
|
|
updateData.project_id = project_id;
|
|
} else {
|
|
updateData.project_id = null;
|
|
}
|
|
}
|
|
|
|
await note.update(updateData);
|
|
|
|
// Handle tags if provided
|
|
if (tags !== undefined) {
|
|
let tagNames = [];
|
|
if (Array.isArray(tags)) {
|
|
if (tags.every((t) => typeof t === 'string')) {
|
|
tagNames = tags;
|
|
} else if (tags.every((t) => typeof t === 'object' && t.name)) {
|
|
tagNames = tags.map((t) => t.name);
|
|
}
|
|
}
|
|
await updateNoteTags(note, tagNames, req.session.userId);
|
|
}
|
|
|
|
// Reload note with associations
|
|
const noteWithAssociations = await Note.findByPk(note.id, {
|
|
include: [
|
|
{
|
|
model: Tag,
|
|
attributes: ['id', 'name', 'uid'],
|
|
through: { attributes: [] },
|
|
},
|
|
{
|
|
model: Project,
|
|
required: false,
|
|
attributes: ['id', 'name', 'uid'],
|
|
},
|
|
],
|
|
});
|
|
|
|
res.json(noteWithAssociations);
|
|
} catch (error) {
|
|
console.error('Error updating note:', error);
|
|
res.status(400).json({
|
|
error: 'There was a problem updating the note.',
|
|
details: error.errors
|
|
? error.errors.map((e) => e.message)
|
|
: [error.message],
|
|
});
|
|
}
|
|
});
|
|
|
|
// DELETE /api/note/:id
|
|
router.delete('/note/:id', async (req, res) => {
|
|
try {
|
|
if (!req.session || !req.session.userId) {
|
|
return res.status(401).json({ error: 'Authentication required' });
|
|
}
|
|
|
|
const note = await Note.findOne({
|
|
where: { id: req.params.id, user_id: req.session.userId },
|
|
});
|
|
|
|
if (!note) {
|
|
return res.status(404).json({ error: 'Note not found.' });
|
|
}
|
|
|
|
await note.destroy();
|
|
res.json({ message: 'Note deleted successfully.' });
|
|
} catch (error) {
|
|
console.error('Error deleting note:', error);
|
|
res.status(400).json({
|
|
error: 'There was a problem deleting the note.',
|
|
});
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|