import * as db from '../../db/publicCatalog.js'; // #region Helpers function parseCompanyId(id) { const companyId = Number(id); if (!Number.isInteger(companyId) || companyId <= 0) { return { error: 'Invalid company id' }; } return { companyId }; } function parseSku(value) { const sku = typeof value === 'string' ? value.trim() : ''; if (!sku) return { error: 'sku is required' }; return { sku: decodeURIComponent(sku) }; } function publicErrorStatus(error) { if (error.message === 'Company not found') return 404; if (error.message === 'Article not found' || error.message?.startsWith('Article not found')) return 404; if (error.message === 'Category not found') return 404; if (error.message?.includes('required') || error.message?.includes('Invalid')) return 400; return 500; } // #endregion /** * Registers public catalog routes (no session/cookie auth required). * @param {import('express').Application} app * @param {string} [basePath=''] */ export function register(app, basePath = '') { /** * @openapi * /public/{companyId}/categories: * get: * tags: [PublicCatalog] * summary: List public categories * description: Returns the category tree for the specified company. * security: [] * parameters: * - in: path * name: companyId * required: true * schema: * type: integer * description: Company ID * responses: * 200: * description: Category list * content: * application/json: * schema: * type: object * properties: * categories: * type: array * items: * type: object * properties: * id: * type: integer * parentId: * type: integer * nullable: true * sort: * type: integer * name: * type: string * slug: * type: string * fullHash: * type: string * nullable: true * thumbW: * type: integer * nullable: true * thumbH: * type: integer * nullable: true * thumbHash: * type: string * nullable: true * 400: * description: Invalid parameters * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 404: * description: Company not found * content: * application/json: * schema: * $ref: '#/components/schemas/Error' */ // #region GET /public/:companyId/categories app.get(`${basePath}/public/:companyId/categories`, async (req, res) => { const parsed = parseCompanyId(req.params.companyId); if (parsed.error) { return res.status(400).json({ error: parsed.error }); } try { const categories = await db.listCategories(parsed.companyId); res.setHeader('Cache-Control', 'public, max-age=60, s-maxage=300'); res.status(200).json({ categories }); } catch (error) { const status = publicErrorStatus(error); if (status === 500) console.error('Public listCategories failed:', error); res.status(status).json({ error: error.message }); } }); // #endregion /** * @openapi * /public/{companyId}/articles: * get: * tags: [PublicCatalog] * summary: List public articles * description: Returns a paginated list of catalog articles for the specified company, optionally filtered by category. * security: [] * parameters: * - in: path * name: companyId * required: true * schema: * type: integer * description: Company ID * - in: query * name: categoryId * schema: * type: integer * description: Optional category ID to filter by * - in: query * name: search * schema: * type: string * description: Search query for name or SKU * - in: query * name: limit * schema: * type: integer * default: 50 * description: Number of articles to return (max 200) * - in: query * name: offset * schema: * type: integer * default: 0 * description: Number of articles to skip * responses: * 200: * description: Articles list * content: * application/json: * schema: * type: object * properties: * articles: * type: array * items: * type: object * properties: * id: * type: integer * sku: * type: string * name: * type: string * slug: * type: string * net: * type: number * nullable: true * taxRate: * type: number * nullable: true * gross: * type: number * nullable: true * trackStock: * type: boolean * brand: * type: object * nullable: true * properties: * id: * type: integer * nullable: true * name: * type: string * fullHash: * type: string * nullable: true * thumbW: * type: integer * nullable: true * thumbH: * type: integer * nullable: true * thumbHash: * type: string * nullable: true * 400: * description: Invalid parameters * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 404: * description: Company not found * content: * application/json: * schema: * $ref: '#/components/schemas/Error' */ // #region GET /public/:companyId/articles app.get(`${basePath}/public/:companyId/articles`, async (req, res) => { const parsed = parseCompanyId(req.params.companyId); if (parsed.error) { return res.status(400).json({ error: parsed.error }); } const categoryId = req.query.categoryId != null ? Number(req.query.categoryId) : undefined; if (categoryId !== undefined && (!Number.isInteger(categoryId) || categoryId <= 0)) { return res.status(400).json({ error: 'Invalid categoryId' }); } const limit = req.query.limit != null ? Number(req.query.limit) : undefined; const offset = req.query.offset != null ? Number(req.query.offset) : undefined; const search = typeof req.query.search === 'string' ? req.query.search : undefined; try { const articles = await db.listArticles(parsed.companyId, { categoryId, limit, offset, search, }); res.setHeader('Cache-Control', 'public, max-age=60, s-maxage=300'); res.status(200).json({ articles }); } catch (error) { const status = publicErrorStatus(error); if (status === 500) console.error('Public listArticles failed:', error); res.status(status).json({ error: error.message }); } }); // #endregion /** * @openapi * /public/{companyId}/articles/{sku}: * get: * tags: [PublicCatalog] * summary: Get public article detail * description: Returns detailed public catalog data for a single article identified by SKU. * security: [] * parameters: * - in: path * name: companyId * required: true * schema: * type: integer * description: Company ID * - in: path * name: sku * required: true * schema: * type: string * description: Article SKU * responses: * 200: * description: Article detail * content: * application/json: * schema: * type: object * properties: * article: * type: object * properties: * id: * type: integer * sku: * type: string * name: * type: string * slug: * type: string * description: * type: string * unitamount: * type: number * nullable: true * weight: * type: number * nullable: true * trackStock: * type: boolean * unit: * type: object * nullable: true * brand: * type: object * nullable: true * price: * type: object * nullable: true * prices: * type: array * items: * type: object * categories: * type: array * items: * type: object * photos: * type: array * items: * type: object * eans: * type: array * items: * type: string * isParent: * type: boolean * parentSku: * type: string * nullable: true * variants: * type: array * items: * type: object * 400: * description: Invalid parameters * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 404: * description: Article or Company not found * content: * application/json: * schema: * $ref: '#/components/schemas/Error' */ // #region GET /public/:companyId/articles/:sku app.get(`${basePath}/public/:companyId/articles/:sku`, async (req, res) => { const parsedCompany = parseCompanyId(req.params.companyId); if (parsedCompany.error) { return res.status(400).json({ error: parsedCompany.error }); } const parsedSku = parseSku(req.params.sku); if (parsedSku.error) { return res.status(400).json({ error: parsedSku.error }); } try { const article = await db.getArticleBySku(parsedCompany.companyId, parsedSku.sku); res.setHeader('Cache-Control', 'public, max-age=60, s-maxage=300'); res.status(200).json({ article }); } catch (error) { const status = publicErrorStatus(error); if (status === 500) console.error('Public getArticleBySku failed:', error); res.status(status).json({ error: error.message }); } }); // #endregion }