import * as db from '../../db/categoryproperty.js'; import { emit } from '../sse.js'; // #region Helpers function requireSelectedCompany(req) { const companyId = req.user?.selectedCompanyId; if (companyId == null) { return { error: 'No company selected' }; } return { companyId }; } function parsePositiveInt(value, field) { const id = Number(value); if (!Number.isInteger(id) || id <= 0) { return { error: `${field} is required` }; } return { id }; } function emitCategorySignal(req) { const companyId = req.user?.selectedCompanyId; if (companyId == null) return; emit('category', { companyId }); } function categoryPropertyErrorStatus(error) { if (error.message === 'No company selected') return 400; if (error.message === 'Not authorized') return 403; if (error.message === 'Company not found' || error.message === 'Category not found' || error.message === 'Parent not found') return 404; if (error.message?.startsWith('Property not found')) return 404; if (error.message?.includes('exclusive')) return 400; if (error.message === 'Invalid properties array' || error.message === 'Invalid propertyId' || error.message === 'Duplicate propertyId') return 400; return 500; } // #endregion /** * Registers categoryproperty routes under `/category/:id/property`. * @param {import('express').Application} app * @param {string} [basePath=''] */ export function register(app, basePath = '') { /** * @openapi * /category/{id}/property: * get: * tags: [Category] * summary: List category properties * description: Fetches all free-form properties set on a category. * security: * - cookieAuth: [] * parameters: * - in: path * name: id * required: true * schema: * type: integer * description: Category ID * responses: * 200: * description: List of properties for this category * content: * application/json: * schema: * type: object * properties: * properties: * type: array * items: * $ref: '#/components/schemas/CategoryProperty' * put: * tags: [Category] * summary: Bulk set category properties * description: Replaces all properties for a category. * security: * - cookieAuth: [] * parameters: * - in: path * name: id * required: true * schema: * type: integer * description: Category ID * requestBody: * required: true * content: * application/json: * schema: * type: object * required: [properties] * properties: * properties: * type: array * items: * type: object * required: [propertyId, value] * properties: * propertyId: * type: integer * value: * type: string * sort: * type: integer * responses: * 200: * description: Updated category properties * content: * application/json: * schema: * type: object * properties: * properties: * type: array * items: * $ref: '#/components/schemas/CategoryProperty' */ // #region GET /category/:id/property app.get(`${basePath}/category/:id/property`, async (req, res) => { const selected = requireSelectedCompany(req); if (selected.error) { return res.status(400).json({ error: selected.error }); } const idParsed = parsePositiveInt(req.params.id, 'categoryId'); if (idParsed.error) { return res.status(400).json({ error: idParsed.error }); } try { const properties = await db.listByCategoryId(req.user.id, idParsed.id); res.json({ properties }); } catch (error) { res.status(categoryPropertyErrorStatus(error)).json({ error: error.message }); } }); // #endregion // #region PUT /category/:id/property app.put(`${basePath}/category/:id/property`, async (req, res) => { const selected = requireSelectedCompany(req); if (selected.error) { return res.status(400).json({ error: selected.error }); } const idParsed = parsePositiveInt(req.params.id, 'categoryId'); if (idParsed.error) { return res.status(400).json({ error: idParsed.error }); } const propertiesInput = req.body?.properties ?? req.body; if (!Array.isArray(propertiesInput)) { return res.status(400).json({ error: 'properties array required' }); } try { const properties = await db.setByCategoryId(req.user.id, idParsed.id, propertiesInput); emitCategorySignal(req); res.json({ properties }); } catch (error) { res.status(categoryPropertyErrorStatus(error)).json({ error: error.message }); } }); // #endregion /** * @openapi * /category/{id}/property/{propertyId}: * delete: * tags: [Category] * summary: Delete category property * description: Removes a single property from a category. * security: * - cookieAuth: [] * parameters: * - in: path * name: id * required: true * schema: * type: integer * - in: path * name: propertyId * required: true * schema: * type: integer * responses: * 200: * description: Deleted successfully * content: * application/json: * schema: * $ref: '#/components/schemas/SuccessResponse' */ // #region DELETE /category/:id/property/:propertyId app.delete(`${basePath}/category/:id/property/:propertyId`, async (req, res) => { const selected = requireSelectedCompany(req); if (selected.error) { return res.status(400).json({ error: selected.error }); } const idParsed = parsePositiveInt(req.params.id, 'categoryId'); if (idParsed.error) { return res.status(400).json({ error: idParsed.error }); } try { const propertyId = Number(req.params.propertyId); await db.remove(req.user.id, idParsed.id, propertyId); emitCategorySignal(req); res.json({ ok: true }); } catch (error) { res.status(categoryPropertyErrorStatus(error)).json({ error: error.message }); } }); // #endregion }