437 lines
16 KiB
JavaScript
437 lines
16 KiB
JavaScript
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:
|
|
* $ref: '#/components/schemas/Unit'
|
|
* nullable: true
|
|
* brand:
|
|
* $ref: '#/components/schemas/ArticleBrand'
|
|
* nullable: true
|
|
* price:
|
|
* type: object
|
|
* nullable: true
|
|
* properties:
|
|
* minmultiplier:
|
|
* type: number
|
|
* net:
|
|
* type: number
|
|
* nullable: true
|
|
* taxRate:
|
|
* type: number
|
|
* nullable: true
|
|
* gross:
|
|
* type: number
|
|
* nullable: true
|
|
* prices:
|
|
* type: array
|
|
* items:
|
|
* type: object
|
|
* properties:
|
|
* minmultiplier:
|
|
* type: number
|
|
* net:
|
|
* type: number
|
|
* nullable: true
|
|
* taxRate:
|
|
* type: number
|
|
* nullable: true
|
|
* gross:
|
|
* type: number
|
|
* nullable: true
|
|
* categories:
|
|
* type: array
|
|
* items:
|
|
* type: object
|
|
* properties:
|
|
* id:
|
|
* type: integer
|
|
* name:
|
|
* type: string
|
|
* slug:
|
|
* type: string
|
|
* photos:
|
|
* type: array
|
|
* items:
|
|
* $ref: '#/components/schemas/ArticlePhoto'
|
|
* eans:
|
|
* type: array
|
|
* items:
|
|
* type: string
|
|
* isParent:
|
|
* type: boolean
|
|
* parentSku:
|
|
* type: string
|
|
* nullable: true
|
|
* variants:
|
|
* type: array
|
|
* items:
|
|
* type: object
|
|
* properties:
|
|
* id:
|
|
* type: integer
|
|
* sku:
|
|
* type: string
|
|
* sort:
|
|
* type: integer
|
|
* name:
|
|
* type: string
|
|
* slug:
|
|
* type: string
|
|
* attributeValueId:
|
|
* type: integer
|
|
* nullable: true
|
|
* attributeValue:
|
|
* type: string
|
|
* nullable: true
|
|
* unitamount:
|
|
* type: number
|
|
* nullable: true
|
|
* weight:
|
|
* type: number
|
|
* nullable: true
|
|
* trackStock:
|
|
* type: boolean
|
|
* 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
|
|
}
|