feat(api): add GET and POST /supplier/:id/articles endpoints and openapi docs
This commit is contained in:
@@ -155,6 +155,58 @@
|
||||
* price:
|
||||
* $ref: '#/components/schemas/ArticleSupplierPrice'
|
||||
* nullable: true
|
||||
* SupplierArticlePhoto:
|
||||
* type: object
|
||||
* required: [sort, thumbhash, hash, thumbw, thumbh]
|
||||
* properties:
|
||||
* sort:
|
||||
* type: integer
|
||||
* thumbhash:
|
||||
* type: string
|
||||
* hash:
|
||||
* type: string
|
||||
* thumbw:
|
||||
* type: integer
|
||||
* thumbh:
|
||||
* type: integer
|
||||
* SupplierArticleInfo:
|
||||
* type: object
|
||||
* required: [id, sku]
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* sku:
|
||||
* type: string
|
||||
* name:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* slug:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* photos:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/SupplierArticlePhoto'
|
||||
* articlepic:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/SupplierArticlePhoto'
|
||||
* eans:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* articleean:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* SupplierArticle:
|
||||
* allOf:
|
||||
* - $ref: '#/components/schemas/ArticleSupplier'
|
||||
* - type: object
|
||||
* required: [article]
|
||||
* properties:
|
||||
* article:
|
||||
* $ref: '#/components/schemas/SupplierArticleInfo'
|
||||
* Brand:
|
||||
* type: object
|
||||
* required: [id, name]
|
||||
|
||||
@@ -13,10 +13,41 @@ function supplierErrorStatus(error) {
|
||||
if (error.message === 'Not authorized') return 403;
|
||||
if (error.message === 'Company not found') return 404;
|
||||
if (error.message === 'Supplier not found') return 404;
|
||||
if (error.message === 'Supplier required') return 400;
|
||||
if (error.message === 'Name required') return 400;
|
||||
return 500;
|
||||
}
|
||||
|
||||
function parsePositiveInt(value, field) {
|
||||
const id = Number(value);
|
||||
if (!Number.isInteger(id) || id <= 0) {
|
||||
return { error: `${field} is required` };
|
||||
}
|
||||
return { id };
|
||||
}
|
||||
|
||||
function parseSkus(source) {
|
||||
if (source == null) return undefined;
|
||||
if (Array.isArray(source)) return source;
|
||||
if (typeof source === 'string') {
|
||||
const trimmed = source.trim();
|
||||
if (!trimmed) return [];
|
||||
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
if (Array.isArray(parsed)) return parsed;
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
if (trimmed.includes(',')) {
|
||||
return trimmed.split(',').map((s) => s.trim()).filter(Boolean);
|
||||
}
|
||||
return [trimmed];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers supplier routes for the authenticated user's selected company.
|
||||
* @param {import('express').Application} app
|
||||
@@ -308,4 +339,190 @@ export function register(app, basePath = '') {
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /supplier/{id}/articles:
|
||||
* get:
|
||||
* tags: [Supplier]
|
||||
* summary: List supplier articles by SKUs
|
||||
* description: |
|
||||
* Lists article-supplier assignments for the specified supplier (entity ID),
|
||||
* optionally filtered by a list of SKUs (matching either article SKU or supplier SKU).
|
||||
* Includes linked article details (name, slug, photos/articlepic, eans/articleean).
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* x-markdown: supplier/articles.get.md
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: Supplier entity ID
|
||||
* - in: query
|
||||
* name: skus
|
||||
* required: false
|
||||
* schema:
|
||||
* oneOf:
|
||||
* - type: array
|
||||
* items:
|
||||
* type: string
|
||||
* - type: string
|
||||
* description: Optional SKU or comma-separated SKUs to filter by
|
||||
* responses:
|
||||
* 200:
|
||||
* description: List of supplier articles
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* articles:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/SupplierArticle'
|
||||
* 400:
|
||||
* description: No company selected or 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'
|
||||
* 404:
|
||||
* description: Supplier or company not found
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* post:
|
||||
* tags: [Supplier]
|
||||
* summary: Query supplier articles by SKUs (bulk)
|
||||
* description: |
|
||||
* Queries article-supplier assignments for the specified supplier (entity ID) with a
|
||||
* JSON list of SKUs in the request body, avoiding URL length limits for large SKU lists.
|
||||
* Includes linked article details (name, slug, photos/articlepic, eans/articleean).
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* x-markdown: supplier/articles.post.md
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: Supplier entity ID
|
||||
* requestBody:
|
||||
* required: false
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* skus:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* description: List of SKUs to filter by (empty array returns empty list)
|
||||
* example:
|
||||
* skus: ['SKU-1', 'SKU-2']
|
||||
* responses:
|
||||
* 200:
|
||||
* description: List of supplier articles
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* articles:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/SupplierArticle'
|
||||
* 400:
|
||||
* description: No company selected or 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'
|
||||
* 404:
|
||||
* description: Supplier or company not found
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
async function handleGetSupplierArticles(req, res) {
|
||||
const selected = requireSelectedCompany(req);
|
||||
if (selected.error) {
|
||||
return res.status(400).json({ error: selected.error });
|
||||
}
|
||||
|
||||
const idParsed = parsePositiveInt(req.params.id, 'id');
|
||||
if (idParsed.error) {
|
||||
return res.status(400).json({ error: idParsed.error });
|
||||
}
|
||||
|
||||
const skus = parseSkus(req.query.skus ?? req.query.sku);
|
||||
|
||||
try {
|
||||
const articles = await db.listArticlesBySkus(req.user.id, idParsed.id, skus);
|
||||
res.status(200).json({ articles });
|
||||
} catch (error) {
|
||||
const status = supplierErrorStatus(error);
|
||||
if (status === 500) console.error('List supplier articles failed:', error);
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePostSupplierArticles(req, res) {
|
||||
const selected = requireSelectedCompany(req);
|
||||
if (selected.error) {
|
||||
return res.status(400).json({ error: selected.error });
|
||||
}
|
||||
|
||||
const idParsed = parsePositiveInt(req.params.id, 'id');
|
||||
if (idParsed.error) {
|
||||
return res.status(400).json({ error: idParsed.error });
|
||||
}
|
||||
|
||||
const skus = parseSkus(req.body?.skus ?? req.body?.sku);
|
||||
|
||||
try {
|
||||
const articles = await db.listArticlesBySkus(req.user.id, idParsed.id, skus);
|
||||
res.status(200).json({ articles });
|
||||
} catch (error) {
|
||||
const status = supplierErrorStatus(error);
|
||||
if (status === 500) console.error('Query supplier articles failed:', error);
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
app.get(`${basePath}/supplier/:id/articles`, handleGetSupplierArticles);
|
||||
app.get(`${basePath}/supplier/:id/article`, handleGetSupplierArticles);
|
||||
app.post(`${basePath}/supplier/:id/articles`, handlePostSupplierArticles);
|
||||
app.post(`${basePath}/supplier/:id/article`, handlePostSupplierArticles);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user