import * as db from '../../db/category.js'; function parsePositiveInt(value, field) { const id = Number(value); if (!Number.isInteger(id) || id <= 0) { return { error: `${field} is required` }; } return { id }; } function categoryErrorStatus(error) { if (error.message === 'Not authorized') return 403; if (error.message === 'Company not found') return 404; if (error.message === 'Parent not found') return 404; if (error.message === 'Cannot delete root category') return 400; return 500; } /** * Registers category routes (requires auth middleware). * @param {import('express').Application} app - Express app * @param {string} [basePath=''] - Optional URL prefix for all routes in this module */ export function register(app, basePath = '') { /** * @openapi * /category/tree: * get: * tags: [Category] * summary: Category tree * description: Returns the category tree for the authenticated user's selected company. * security: * - cookieAuth: [] * x-markdown: category/tree.md * responses: * 200: * description: Tree and articles for the selected company * content: * application/json: * schema: * type: object * properties: * languageId: * type: integer * tree: * type: array * items: * type: object * additionalProperties: true * articles: * type: array * items: * type: object * additionalProperties: true * 400: * description: No company selected * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 401: * description: Unauthorized * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 403: * description: Not authorized * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 404: * description: Company not found * content: * application/json: * schema: * $ref: '#/components/schemas/Error' */ app.get(`${basePath}/category/tree`, async (req, res) => { const companyId = req.user.selectedCompanyId; if (companyId == null) { return res.status(400).json({ error: 'No company selected' }); } try { const result = await db.getTreeForCompany(req.user.id, companyId); res.status(200).json(result); } catch (error) { const status = categoryErrorStatus(error); if (status === 500) console.error('Get category tree failed:', error); res.status(status).json({ error: error.message }); } }); /** * @openapi * /category: * post: * tags: [Category] * summary: Create category * description: Creates a category under an authorized parent. * security: * - cookieAuth: [] * x-markdown: category/create.md * requestBody: * required: true * content: * application/json: * schema: * type: object * required: [parentId, name] * properties: * parentId: * type: integer * name: * type: string * example: * parentId: 1 * name: New category * responses: * 201: * description: Created * content: * application/json: * schema: * type: object * properties: * category: * $ref: '#/components/schemas/Category' * 400: * description: Validation error * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 401: * description: Unauthorized * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 403: * description: Not authorized * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 404: * description: Parent not found * content: * application/json: * schema: * $ref: '#/components/schemas/Error' */ app.post(`${basePath}/category`, async (req, res) => { const parent = parsePositiveInt(req.body?.parentId, 'parentId'); if (parent.error) { return res.status(400).json({ error: parent.error }); } const name = typeof req.body?.name === 'string' ? req.body.name.trim() : ''; if (!name) { return res.status(400).json({ error: 'name is required' }); } try { const category = await db.createUnderParent({ googleAuthId: req.user.id, parentId: parent.id, name, }); res.status(201).json({ category }); } catch (error) { const status = categoryErrorStatus(error); if (status === 500) console.error('Create category failed:', error); res.status(status).json({ error: error.message }); } }); /** * @openapi * /category/{id}/articles: * get: * tags: [Category] * summary: Articles in category * description: Returns articles in a category the user is authorized for. * security: * - cookieAuth: [] * x-markdown: category/articles.md * parameters: * - in: path * name: id * required: true * schema: * type: integer * description: Category id * responses: * 200: * description: Articles list * content: * application/json: * schema: * type: object * properties: * articles: * type: array * items: * type: object * additionalProperties: true * 400: * description: Invalid id * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 401: * description: Unauthorized * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 403: * description: Not authorized * content: * application/json: * schema: * $ref: '#/components/schemas/Error' */ app.get(`${basePath}/category/:id/articles`, async (req, res) => { const parsed = parsePositiveInt(req.params.id, 'id'); if (parsed.error) { return res.status(400).json({ error: parsed.error }); } try { const articles = await db.getCategoryArticles(req.user.id, parsed.id); res.status(200).json({ articles }); } catch (error) { const status = categoryErrorStatus(error); if (status === 500) console.error('Get category articles failed:', error); res.status(status).json({ error: error.message }); } }); /** * @openapi * /category/{id}: * delete: * tags: [Category] * summary: Delete category * description: Deletes a category the user is authorized for (not the company root). * security: * - cookieAuth: [] * x-markdown: category/delete.md * parameters: * - in: path * name: id * required: true * schema: * type: integer * description: Category id * responses: * 204: * description: Deleted * 400: * description: Invalid id or cannot delete root * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 401: * description: Unauthorized * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 403: * description: Not authorized * content: * application/json: * schema: * $ref: '#/components/schemas/Error' */ app.delete(`${basePath}/category/:id`, async (req, res) => { const parsed = parsePositiveInt(req.params.id, 'id'); if (parsed.error) { return res.status(400).json({ error: parsed.error }); } try { await db.deleteCategory({ googleAuthId: req.user.id, id: parsed.id, }); res.sendStatus(204); } catch (error) { const status = categoryErrorStatus(error); if (status === 500) console.error('Delete category failed:', error); res.status(status).json({ error: error.message }); } }); }