1506 lines
52 KiB
JavaScript
1506 lines
52 KiB
JavaScript
import express from 'express';
|
||
import JSZip from 'jszip';
|
||
import * as db from '../../db/article.js';
|
||
import * as picDb from '../../db/pic.js';
|
||
import { emit } from '../sse.js';
|
||
import { buildArtikelCsv, toWindows1252 } from '../../util/artikelCsv.js';
|
||
|
||
const AVIF_BODY_LIMIT = '20mb';
|
||
const FULL_HASH_RE = /^[a-fA-F0-9]{64}$/;
|
||
|
||
/** Notify SSE subscribers after create / rename-ish list-level article changes. */
|
||
function emitArticlesSignal(req) {
|
||
const companyId = req.user?.selectedCompanyId;
|
||
if (companyId == null) return;
|
||
emit('articles', { companyId });
|
||
}
|
||
|
||
/** Notify SSE subscribers of a single-article detail change. */
|
||
function emitArticleSignal(req, sku) {
|
||
const companyId = req.user?.selectedCompanyId;
|
||
if (companyId == null) return;
|
||
emit(`article-${sku}`, { companyId });
|
||
}
|
||
|
||
function parsePositiveInt(value, field) {
|
||
const id = Number(value);
|
||
if (!Number.isInteger(id) || id <= 0) {
|
||
return { error: `${field} is required` };
|
||
}
|
||
return { id };
|
||
}
|
||
|
||
function parseSku(value) {
|
||
const sku = typeof value === 'string' ? value.trim() : '';
|
||
if (!sku) return { error: 'sku is required' };
|
||
return { sku: decodeURIComponent(sku) };
|
||
}
|
||
|
||
function parseFullHash(value) {
|
||
const fullHash = typeof value === 'string' ? value.trim() : '';
|
||
if (!FULL_HASH_RE.test(fullHash)) {
|
||
return { error: 'fullHash must be a 64-char hex SHA-256' };
|
||
}
|
||
return { fullHash: fullHash.toLowerCase() };
|
||
}
|
||
|
||
function articleErrorStatus(error) {
|
||
if (error.message === 'No company selected') return 400;
|
||
if (error.message === 'Not authorized') return 403;
|
||
if (error.message === 'Parent not found') return 404;
|
||
if (error.message === 'Article not found') return 404;
|
||
if (error.message === 'Image not found') return 404;
|
||
if (error.message === 'Name required') return 400;
|
||
if (error.message === 'Invalid image order') return 400;
|
||
if (error.message === 'Article has children') return 400;
|
||
if (error.message === 'Invalid prices') return 400;
|
||
if (error.message === 'POS customer group required') return 400;
|
||
if (error.message === 'Invalid unitamount') return 400;
|
||
if (error.message === 'Unit not found') return 404;
|
||
if (error.message === 'Invalid eans') return 400;
|
||
if (error.message === 'Company not found') return 404;
|
||
if (error.message === 'Brand not found') return 404;
|
||
if (error.message === 'Invalid weight') return 400;
|
||
if (error.message === 'Invalid track_stock') return 400;
|
||
if (error.message === 'Invalid categoryIds') return 400;
|
||
if (error.message?.includes('already exists') || error.code === 'SQLITE_CONSTRAINT_UNIQUE') return 409;
|
||
return 500;
|
||
}
|
||
|
||
/**
|
||
* Registers article 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
|
||
* /article:
|
||
* post:
|
||
* tags: [Article]
|
||
* summary: Create article
|
||
* description: Creates an article under an authorized category (`parentId`).
|
||
* security:
|
||
* - cookieAuth: []
|
||
* x-signals: [articles]
|
||
* x-markdown: article/create.md
|
||
* requestBody:
|
||
* required: true
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* required: [parentId, name]
|
||
* properties:
|
||
* parentId:
|
||
* type: integer
|
||
* description: Category id
|
||
* name:
|
||
* type: string
|
||
* track_stock:
|
||
* type: boolean
|
||
* description: Whether to track inventory stock (defaults to true)
|
||
* example:
|
||
* parentId: 10
|
||
* name: New article
|
||
* responses:
|
||
* 201:
|
||
* description: Created
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* properties:
|
||
* article:
|
||
* $ref: '#/components/schemas/ArticleSummary'
|
||
* 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 for parent category
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* $ref: '#/components/schemas/Error'
|
||
* 404:
|
||
* description: Parent not found
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* $ref: '#/components/schemas/Error'
|
||
* 409:
|
||
* description: SKU already exists
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* $ref: '#/components/schemas/Error'
|
||
*/
|
||
app.post(`${basePath}/article`, 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' });
|
||
}
|
||
|
||
const rawTrack = req.body?.track_stock ?? req.body?.trackStock;
|
||
if (rawTrack !== undefined && typeof rawTrack !== 'boolean' && rawTrack !== 0 && rawTrack !== 1) {
|
||
return res.status(400).json({ error: 'Invalid track_stock' });
|
||
}
|
||
const track_stock = rawTrack !== undefined ? Boolean(rawTrack) : true;
|
||
|
||
try {
|
||
const article = await db.createUnderParent({
|
||
googleAuthId: req.user.id,
|
||
parentId: parent.id,
|
||
name,
|
||
track_stock,
|
||
});
|
||
emitArticlesSignal(req);
|
||
res.status(201).json({ article });
|
||
} catch (error) {
|
||
const status = articleErrorStatus(error);
|
||
if (status === 500) console.error('Create article failed:', error);
|
||
res.status(status).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
/**
|
||
* @openapi
|
||
* /article/export.zip:
|
||
* get:
|
||
* tags: [Article]
|
||
* summary: Export articles as ZIP
|
||
* description: |
|
||
* Builds an `artikel.zip` for the authenticated user's selected company.
|
||
* `categoryId` (optional) restricts the export to that category and below;
|
||
* it must be reachable via the company's root category. Defaults to the
|
||
* whole company root tree.
|
||
* The ZIP contains `artikel.csv` (semicolon-separated, Windows-1252)
|
||
* plus one AVIF entry per article image, named `<fullHash>.avif`
|
||
* (content-addressed, so shared hashes are deduplicated).
|
||
* security:
|
||
* - cookieAuth: []
|
||
* parameters:
|
||
* - in: query
|
||
* name: categoryId
|
||
* required: false
|
||
* schema:
|
||
* type: integer
|
||
* description: Restrict export to this category subtree
|
||
* responses:
|
||
* 200:
|
||
* description: ZIP archive
|
||
* content:
|
||
* application/zip:
|
||
* schema:
|
||
* type: string
|
||
* format: binary
|
||
* 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}/article/export.zip`, async (req, res) => {
|
||
const companyId = req.user.selectedCompanyId;
|
||
if (companyId == null) {
|
||
return res.status(400).json({ error: 'No company selected' });
|
||
}
|
||
|
||
let categoryId = null;
|
||
if (req.query.categoryId != null && req.query.categoryId !== '') {
|
||
const parsed = parsePositiveInt(req.query.categoryId, 'categoryId');
|
||
if (parsed.error) {
|
||
return res.status(400).json({ error: parsed.error });
|
||
}
|
||
categoryId = parsed.id;
|
||
}
|
||
|
||
try {
|
||
const rows = await db.listExport(req.user.id, categoryId);
|
||
|
||
const csv = buildArtikelCsv(rows);
|
||
|
||
const zip = new JSZip();
|
||
zip.file('artikel.csv', toWindows1252(csv));
|
||
|
||
const seen = new Set();
|
||
for (const row of rows) {
|
||
for (const fullHash of row.fullHashes ?? []) {
|
||
if (!fullHash || seen.has(fullHash)) continue;
|
||
seen.add(fullHash);
|
||
try {
|
||
const { buffer } = await picDb.getImageByHash(req.user.id, fullHash);
|
||
zip.file(`${fullHash}.avif`, buffer);
|
||
} catch (error) {
|
||
// Skip images this user cannot access; CSV already carries the hash.
|
||
}
|
||
}
|
||
}
|
||
|
||
const content = await zip.generateAsync({ type: 'nodebuffer' });
|
||
res.setHeader('Content-Type', 'application/zip');
|
||
res.setHeader('Content-Disposition', 'attachment; filename="artikel.zip"');
|
||
res.status(200).send(content);
|
||
} catch (error) {
|
||
const status = articleErrorStatus(error);
|
||
if (status === 500) console.error('Export articles failed:', error);
|
||
res.status(status).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
/**
|
||
* @openapi
|
||
* /article/{sku}:
|
||
* get:
|
||
* tags: [Article]
|
||
* summary: Get article by SKU
|
||
* description: Returns article detail by SKU if the user may access its category.
|
||
* security:
|
||
* - cookieAuth: []
|
||
* x-markdown: article/get.md
|
||
* parameters:
|
||
* - 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:
|
||
* $ref: '#/components/schemas/ArticleDetail'
|
||
* 400:
|
||
* description: Missing sku
|
||
* 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: Article not found
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* $ref: '#/components/schemas/Error'
|
||
* patch:
|
||
* tags: [Article]
|
||
* summary: Update article by SKU
|
||
* description: |
|
||
* Patches article fields. At least one of `name`, `unitamount`, `unitSi` required.
|
||
* `unitSi` must already exist for the company. Returns the same detail shape as GET.
|
||
* security:
|
||
* - cookieAuth: []
|
||
* x-signals: [articles]
|
||
* x-markdown: article/update.md
|
||
* parameters:
|
||
* - in: path
|
||
* name: sku
|
||
* required: true
|
||
* schema:
|
||
* type: string
|
||
* description: Article SKU
|
||
* requestBody:
|
||
* required: true
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* properties:
|
||
* name:
|
||
* type: string
|
||
* unitamount:
|
||
* type: number
|
||
* description: Must be finite and `> 0`
|
||
* unitSi:
|
||
* type: string
|
||
* description: Existing company unit `si`
|
||
* track_stock:
|
||
* type: boolean
|
||
* description: Whether to track inventory stock
|
||
* example:
|
||
* unitamount: 0.24
|
||
* unitSi: l
|
||
* responses:
|
||
* 200:
|
||
* description: Updated article detail
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* properties:
|
||
* article:
|
||
* $ref: '#/components/schemas/ArticleDetail'
|
||
* 400:
|
||
* description: Validation error (Name required / Invalid unitamount)
|
||
* 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: Article or unit not found
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* $ref: '#/components/schemas/Error'
|
||
* delete:
|
||
* tags: [Article]
|
||
* summary: Delete article by SKU
|
||
* description: |
|
||
* Deletes the article when it has no child articles. Cascades related rows;
|
||
* nulls `manifestarticle.article_id` and appends a delete comment.
|
||
* Auth like GET by SKU.
|
||
* security:
|
||
* - cookieAuth: []
|
||
* x-signals: [articles]
|
||
* x-markdown: article/delete.md
|
||
* parameters:
|
||
* - in: path
|
||
* name: sku
|
||
* required: true
|
||
* schema:
|
||
* type: string
|
||
* description: Article SKU
|
||
* responses:
|
||
* 204:
|
||
* description: Deleted
|
||
* 400:
|
||
* description: Article has children
|
||
* 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: Article not found
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* $ref: '#/components/schemas/Error'
|
||
*/
|
||
app.get(`${basePath}/article/:sku`, async (req, res) => {
|
||
const sku = typeof req.params.sku === 'string' ? req.params.sku.trim() : '';
|
||
if (!sku) {
|
||
return res.status(400).json({ error: 'sku is required' });
|
||
}
|
||
|
||
try {
|
||
const { article } = await db.getBySku(req.user.id, decodeURIComponent(sku));
|
||
res.status(200).json({ article });
|
||
} catch (error) {
|
||
const status = articleErrorStatus(error);
|
||
if (status === 500) console.error('Get article by sku failed:', error);
|
||
res.status(status).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
/**
|
||
* @openapi
|
||
* /article/{sku}/history:
|
||
* get:
|
||
* tags: [Article]
|
||
* summary: Get article history by SKU
|
||
* description: |
|
||
* Returns `articlehistory` rows for the article, newest first (`date` DESC, `id` DESC).
|
||
* Auth like GET by SKU.
|
||
* security:
|
||
* - cookieAuth: []
|
||
* x-markdown: article/history.md
|
||
* parameters:
|
||
* - in: path
|
||
* name: sku
|
||
* required: true
|
||
* schema:
|
||
* type: string
|
||
* description: Article SKU
|
||
* responses:
|
||
* 200:
|
||
* description: Article history (newest first)
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* required: [history]
|
||
* properties:
|
||
* history:
|
||
* type: array
|
||
* items:
|
||
* $ref: '#/components/schemas/ArticleHistoryEntry'
|
||
* example:
|
||
* history:
|
||
* - id: 42
|
||
* date: '2026-08-05T07:15:00.000Z'
|
||
* text: "Name geändert von \"Tee\" auf \"Grüner Tee\"\nEinheit geändert von \"500g\" auf \"1kg\""
|
||
* - id: 41
|
||
* date: '2026-08-01T09:00:00.000Z'
|
||
* text: 'Name geändert von "" auf "Tee"'
|
||
* 400:
|
||
* description: Missing sku
|
||
* 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: Article not found
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* $ref: '#/components/schemas/Error'
|
||
*/
|
||
app.get(`${basePath}/article/:sku/history`, async (req, res) => {
|
||
const sku = typeof req.params.sku === 'string' ? req.params.sku.trim() : '';
|
||
if (!sku) {
|
||
return res.status(400).json({ error: 'sku is required' });
|
||
}
|
||
|
||
try {
|
||
const history = await db.getHistoryBySku(req.user.id, decodeURIComponent(sku));
|
||
res.status(200).json({ history });
|
||
} catch (error) {
|
||
const status = articleErrorStatus(error);
|
||
if (status === 500) console.error('Get article history by sku failed:', error);
|
||
res.status(status).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
app.patch(`${basePath}/article/:sku`, async (req, res) => {
|
||
const sku = typeof req.params.sku === 'string' ? req.params.sku.trim() : '';
|
||
if (!sku) {
|
||
return res.status(400).json({ error: 'sku is required' });
|
||
}
|
||
|
||
const body = req.body ?? {};
|
||
const patch = {};
|
||
if (Object.prototype.hasOwnProperty.call(body, 'name')) {
|
||
patch.name = body.name;
|
||
}
|
||
if (Object.prototype.hasOwnProperty.call(body, 'unitamount')) {
|
||
patch.unitamount = body.unitamount;
|
||
}
|
||
if (Object.prototype.hasOwnProperty.call(body, 'unitSi')) {
|
||
patch.unitSi = body.unitSi;
|
||
}
|
||
if (Object.prototype.hasOwnProperty.call(body, 'track_stock')) {
|
||
patch.track_stock = body.track_stock;
|
||
} else if (Object.prototype.hasOwnProperty.call(body, 'trackStock')) {
|
||
patch.track_stock = body.trackStock;
|
||
}
|
||
|
||
if (!('name' in patch) && !('unitamount' in patch) && !('unitSi' in patch) && !('track_stock' in patch)) {
|
||
return res.status(400).json({ error: 'name, unitamount, unitSi, or track_stock is required' });
|
||
}
|
||
|
||
try {
|
||
const decodedSku = decodeURIComponent(sku);
|
||
const article = await db.updateBySku(req.user.id, decodedSku, patch);
|
||
emitArticlesSignal(req);
|
||
emitArticleSignal(req, decodedSku);
|
||
res.status(200).json({ article });
|
||
} catch (error) {
|
||
const status = articleErrorStatus(error);
|
||
if (status === 500) console.error('Update article by sku failed:', error);
|
||
res.status(status).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
app.delete(`${basePath}/article/:sku`, async (req, res) => {
|
||
const parsed = parseSku(req.params.sku);
|
||
if (parsed.error) {
|
||
return res.status(400).json({ error: parsed.error });
|
||
}
|
||
|
||
try {
|
||
await db.remove(req.user.id, parsed.sku);
|
||
emitArticlesSignal(req);
|
||
res.sendStatus(204);
|
||
} catch (error) {
|
||
const status = articleErrorStatus(error);
|
||
if (status === 500) console.error('Delete article failed:', error);
|
||
res.status(status).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
/**
|
||
* @openapi
|
||
* /article/{sku}/prices:
|
||
* put:
|
||
* tags: [Article]
|
||
* summary: Replace POS prices by SKU
|
||
* description: |
|
||
* Replace-all prices for the company POS customer group.
|
||
* Each entry: `price` (net), `tax` (rate, ensured via tax.ensure), optional `minmultiplier` (default 1).
|
||
* Empty `prices` clears all POS prices. Returns the same shape as GET by SKU.
|
||
* security:
|
||
* - cookieAuth: []
|
||
* x-signals: [article-<sku>]
|
||
* x-markdown: article/set-prices.md
|
||
* parameters:
|
||
* - in: path
|
||
* name: sku
|
||
* required: true
|
||
* schema:
|
||
* type: string
|
||
* requestBody:
|
||
* required: true
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* required: [prices]
|
||
* properties:
|
||
* prices:
|
||
* type: array
|
||
* items:
|
||
* type: object
|
||
* required: [price, tax]
|
||
* properties:
|
||
* price:
|
||
* type: number
|
||
* description: Net price
|
||
* tax:
|
||
* type: number
|
||
* description: Tax rate (e.g. 19)
|
||
* minmultiplier:
|
||
* type: number
|
||
* description: Defaults to 1; must be unique per request
|
||
* example:
|
||
* prices:
|
||
* - price: 10.5
|
||
* tax: 19
|
||
* - price: 9.9
|
||
* tax: 19
|
||
* minmultiplier: 10
|
||
* responses:
|
||
* 200:
|
||
* description: Updated article detail
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* properties:
|
||
* article:
|
||
* $ref: '#/components/schemas/ArticleDetail'
|
||
* 400:
|
||
* description: Invalid prices or POS customer group required
|
||
* 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: Article not found
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* $ref: '#/components/schemas/Error'
|
||
*/
|
||
app.put(`${basePath}/article/:sku/prices`, async (req, res) => {
|
||
const skuParsed = parseSku(req.params.sku);
|
||
if (skuParsed.error) {
|
||
return res.status(400).json({ error: skuParsed.error });
|
||
}
|
||
|
||
if (!Array.isArray(req.body?.prices)) {
|
||
return res.status(400).json({ error: 'prices is required' });
|
||
}
|
||
|
||
try {
|
||
const article = await db.setPricesBySku(req.user.id, skuParsed.sku, req.body.prices);
|
||
emitArticleSignal(req, skuParsed.sku);
|
||
res.status(200).json({ article });
|
||
} catch (error) {
|
||
const status = articleErrorStatus(error);
|
||
if (status === 500) console.error('Set article prices failed:', error);
|
||
res.status(status).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
/**
|
||
* @openapi
|
||
* /article/{sku}/image:
|
||
* post:
|
||
* tags: [Article]
|
||
* summary: Add article image
|
||
* description: |
|
||
* Accepts a full-size AVIF body (`Content-Type: image/avif`).
|
||
* Full: stored as-is when already AVIF and ≤1000×1000; otherwise scaled to max 1000×1000.
|
||
* Thumb always scaled to max 200×200. Stores hash/thumbhash, links `articlepic`.
|
||
* Auth like GET by SKU.
|
||
* security:
|
||
* - cookieAuth: []
|
||
* x-signals: [article-<sku>]
|
||
* x-markdown: article/add-image.md
|
||
* parameters:
|
||
* - in: path
|
||
* name: sku
|
||
* required: true
|
||
* schema:
|
||
* type: string
|
||
* description: Article SKU
|
||
* requestBody:
|
||
* required: true
|
||
* content:
|
||
* image/avif:
|
||
* schema:
|
||
* type: string
|
||
* format: binary
|
||
* responses:
|
||
* 201:
|
||
* description: Image stored and linked
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* properties:
|
||
* image:
|
||
* $ref: '#/components/schemas/ArticleImage'
|
||
* 400:
|
||
* description: Missing body or invalid image
|
||
* 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: Article not found
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* $ref: '#/components/schemas/Error'
|
||
*/
|
||
app.post(
|
||
`${basePath}/article/:sku/image`,
|
||
express.raw({ type: ['image/avif', 'application/octet-stream'], limit: AVIF_BODY_LIMIT }),
|
||
async (req, res) => {
|
||
const sku = typeof req.params.sku === 'string' ? req.params.sku.trim() : '';
|
||
if (!sku) {
|
||
return res.status(400).json({ error: 'sku is required' });
|
||
}
|
||
|
||
const imageBuffer = Buffer.isBuffer(req.body) ? req.body : null;
|
||
if (!imageBuffer?.length) {
|
||
return res.status(400).json({ error: 'AVIF image body is required' });
|
||
}
|
||
|
||
try {
|
||
const decodedSku = decodeURIComponent(sku);
|
||
const image = await db.addImage(req.user.id, decodedSku, imageBuffer);
|
||
emitArticleSignal(req, decodedSku);
|
||
res.status(201).json({ image });
|
||
} catch (error) {
|
||
const status = articleErrorStatus(error);
|
||
if (status === 500) console.error('Add article image failed:', error);
|
||
res.status(status).json({ error: error.message });
|
||
}
|
||
},
|
||
);
|
||
|
||
/**
|
||
* @openapi
|
||
* /article/{sku}/image/{fullHash}:
|
||
* delete:
|
||
* tags: [Article]
|
||
* summary: Delete article image
|
||
* description: Removes `articlepic` and the `pic` row for this article (`fullHash`). Auth like GET by SKU.
|
||
* security:
|
||
* - cookieAuth: []
|
||
* x-signals: [article-<sku>]
|
||
* x-markdown: article/delete-image.md
|
||
* parameters:
|
||
* - in: path
|
||
* name: sku
|
||
* required: true
|
||
* schema:
|
||
* type: string
|
||
* - in: path
|
||
* name: fullHash
|
||
* required: true
|
||
* schema:
|
||
* type: string
|
||
* pattern: '^[a-fA-F0-9]{64}$'
|
||
* responses:
|
||
* 204:
|
||
* description: Deleted
|
||
* 400:
|
||
* description: Invalid hash
|
||
* 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: Article or image not found
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* $ref: '#/components/schemas/Error'
|
||
*/
|
||
app.delete(`${basePath}/article/:sku/image/:fullHash`, async (req, res) => {
|
||
const skuParsed = parseSku(req.params.sku);
|
||
if (skuParsed.error) {
|
||
return res.status(400).json({ error: skuParsed.error });
|
||
}
|
||
const hashParsed = parseFullHash(req.params.fullHash);
|
||
if (hashParsed.error) {
|
||
return res.status(400).json({ error: hashParsed.error });
|
||
}
|
||
|
||
try {
|
||
await db.deleteImage(req.user.id, skuParsed.sku, hashParsed.fullHash);
|
||
emitArticleSignal(req, skuParsed.sku);
|
||
res.sendStatus(204);
|
||
} catch (error) {
|
||
const status = articleErrorStatus(error);
|
||
if (status === 500) console.error('Delete article image failed:', error);
|
||
res.status(status).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
/**
|
||
* @openapi
|
||
* /article/{sku}/images/order:
|
||
* put:
|
||
* tags: [Article]
|
||
* summary: Reorder article images
|
||
* description: |
|
||
* Sets `articlepic.sort` to `0..n-1` in the given order.
|
||
* `fullHashes` must list every image of the article exactly once.
|
||
* security:
|
||
* - cookieAuth: []
|
||
* x-signals: [article-<sku>]
|
||
* x-markdown: article/order-images.md
|
||
* parameters:
|
||
* - in: path
|
||
* name: sku
|
||
* required: true
|
||
* schema:
|
||
* type: string
|
||
* requestBody:
|
||
* required: true
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* required: [fullHashes]
|
||
* properties:
|
||
* fullHashes:
|
||
* type: array
|
||
* items:
|
||
* type: string
|
||
* pattern: '^[a-fA-F0-9]{64}$'
|
||
* example:
|
||
* fullHashes:
|
||
* - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||
* - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||
* responses:
|
||
* 200:
|
||
* description: New order
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* properties:
|
||
* fullHashes:
|
||
* type: array
|
||
* items:
|
||
* type: string
|
||
* 400:
|
||
* description: Invalid image order
|
||
* 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: Article not found
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* $ref: '#/components/schemas/Error'
|
||
*/
|
||
app.put(`${basePath}/article/:sku/images/order`, async (req, res) => {
|
||
const skuParsed = parseSku(req.params.sku);
|
||
if (skuParsed.error) {
|
||
return res.status(400).json({ error: skuParsed.error });
|
||
}
|
||
|
||
const rawHashes = req.body?.fullHashes;
|
||
if (!Array.isArray(rawHashes)) {
|
||
return res.status(400).json({ error: 'fullHashes is required' });
|
||
}
|
||
|
||
const fullHashes = [];
|
||
for (const value of rawHashes) {
|
||
const parsed = parseFullHash(value);
|
||
if (parsed.error) {
|
||
return res.status(400).json({ error: parsed.error });
|
||
}
|
||
fullHashes.push(parsed.fullHash);
|
||
}
|
||
|
||
try {
|
||
const result = await db.orderImages(req.user.id, skuParsed.sku, { fullHashes });
|
||
emitArticleSignal(req, skuParsed.sku);
|
||
res.status(200).json(result);
|
||
} catch (error) {
|
||
const status = articleErrorStatus(error);
|
||
if (status === 500) console.error('Order article images failed:', error);
|
||
res.status(status).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
/**
|
||
* @openapi
|
||
* /article/{sku}/eans:
|
||
* put:
|
||
* tags: [Article]
|
||
* summary: Replace article EANs by SKU
|
||
* description: |
|
||
* Replace-all EANs for the article. EANs must be non-empty unique strings;
|
||
* empty array clears all EANs. Returns the same shape as GET by SKU.
|
||
* security:
|
||
* - cookieAuth: []
|
||
* x-signals: [article-<sku>]
|
||
* x-markdown: article/set-eans.md
|
||
* parameters:
|
||
* - in: path
|
||
* name: sku
|
||
* required: true
|
||
* schema:
|
||
* type: string
|
||
* requestBody:
|
||
* required: true
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* required: [eans]
|
||
* properties:
|
||
* eans:
|
||
* type: array
|
||
* items:
|
||
* type: string
|
||
* example:
|
||
* eans: ["4001234567890"]
|
||
* responses:
|
||
* 200:
|
||
* description: Updated article detail
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* properties:
|
||
* article:
|
||
* $ref: '#/components/schemas/ArticleDetail'
|
||
* 400:
|
||
* description: Invalid eans
|
||
* 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: Article not found
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* $ref: '#/components/schemas/Error'
|
||
*/
|
||
app.put(`${basePath}/article/:sku/eans`, async (req, res) => {
|
||
const skuParsed = parseSku(req.params.sku);
|
||
if (skuParsed.error) {
|
||
return res.status(400).json({ error: skuParsed.error });
|
||
}
|
||
|
||
if (!Array.isArray(req.body?.eans)) {
|
||
return res.status(400).json({ error: 'eans is required' });
|
||
}
|
||
|
||
try {
|
||
const article = await db.setEansBySku(req.user.id, skuParsed.sku, req.body.eans);
|
||
emitArticleSignal(req, skuParsed.sku);
|
||
res.status(200).json({ article });
|
||
} catch (error) {
|
||
const status = articleErrorStatus(error);
|
||
if (status === 500) console.error('Set article eans failed:', error);
|
||
res.status(status).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
/**
|
||
* @openapi
|
||
* /article/{sku}/brand:
|
||
* put:
|
||
* tags: [Article]
|
||
* summary: Set or clear article brand by SKU
|
||
* description: |
|
||
* Sets the article's `brandId` (must belong to the article's company).
|
||
* Pass `null` to clear the brand. Returns the same shape as GET by SKU.
|
||
* security:
|
||
* - cookieAuth: []
|
||
* x-signals: [article-<sku>]
|
||
* x-markdown: article/set-brand.md
|
||
* parameters:
|
||
* - in: path
|
||
* name: sku
|
||
* required: true
|
||
* schema:
|
||
* type: string
|
||
* requestBody:
|
||
* required: true
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* required: [brandId]
|
||
* properties:
|
||
* brandId:
|
||
* type: integer
|
||
* nullable: true
|
||
* example:
|
||
* brandId: 3
|
||
* responses:
|
||
* 200:
|
||
* description: Updated article detail
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* properties:
|
||
* article:
|
||
* $ref: '#/components/schemas/ArticleDetail'
|
||
* 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: Article or brand not found
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* $ref: '#/components/schemas/Error'
|
||
*/
|
||
app.put(`${basePath}/article/:sku/brand`, async (req, res) => {
|
||
const skuParsed = parseSku(req.params.sku);
|
||
if (skuParsed.error) {
|
||
return res.status(400).json({ error: skuParsed.error });
|
||
}
|
||
|
||
const brandId = req.body?.brandId ?? null;
|
||
|
||
try {
|
||
const article = await db.setBrandIdBySku(req.user.id, skuParsed.sku, brandId);
|
||
emitArticleSignal(req, skuParsed.sku);
|
||
res.status(200).json({ article });
|
||
} catch (error) {
|
||
const status = articleErrorStatus(error);
|
||
if (status === 500) console.error('Set article brand failed:', error);
|
||
res.status(status).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
/**
|
||
* @openapi
|
||
* /article/{sku}/weight:
|
||
* put:
|
||
* tags: [Article]
|
||
* summary: Set article weight by SKU
|
||
* description: |
|
||
* Sets the article's `weight`. Must be finite and `>= 0`.
|
||
* Returns the same shape as GET by SKU.
|
||
* security:
|
||
* - cookieAuth: []
|
||
* x-signals: [article-<sku>]
|
||
* x-markdown: article/set-weight.md
|
||
* parameters:
|
||
* - in: path
|
||
* name: sku
|
||
* required: true
|
||
* schema:
|
||
* type: string
|
||
* requestBody:
|
||
* required: true
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* required: [weight]
|
||
* properties:
|
||
* weight:
|
||
* type: number
|
||
* example:
|
||
* weight: 1.5
|
||
* responses:
|
||
* 200:
|
||
* description: Updated article detail
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* properties:
|
||
* article:
|
||
* $ref: '#/components/schemas/ArticleDetail'
|
||
* 400:
|
||
* description: Invalid weight
|
||
* 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: Article not found
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* $ref: '#/components/schemas/Error'
|
||
*/
|
||
app.put(`${basePath}/article/:sku/weight`, async (req, res) => {
|
||
const skuParsed = parseSku(req.params.sku);
|
||
if (skuParsed.error) {
|
||
return res.status(400).json({ error: skuParsed.error });
|
||
}
|
||
|
||
const weight = req.body?.weight;
|
||
|
||
try {
|
||
const article = await db.setWeightBySku(req.user.id, skuParsed.sku, weight);
|
||
emitArticleSignal(req, skuParsed.sku);
|
||
res.status(200).json({ article });
|
||
} catch (error) {
|
||
const status = articleErrorStatus(error);
|
||
if (status === 500) console.error('Set article weight failed:', error);
|
||
res.status(status).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
/**
|
||
* @openapi
|
||
* /article/{sku}/track_stock:
|
||
* put:
|
||
* tags: [Article]
|
||
* summary: Set article track_stock flag by SKU
|
||
* description: |
|
||
* Enables or disables inventory stock tracking for this article.
|
||
* security:
|
||
* - cookieAuth: []
|
||
* x-signals: [article-<sku>]
|
||
* parameters:
|
||
* - in: path
|
||
* name: sku
|
||
* required: true
|
||
* schema:
|
||
* type: string
|
||
* description: Article SKU
|
||
* requestBody:
|
||
* required: true
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* required: [track_stock]
|
||
* properties:
|
||
* track_stock:
|
||
* type: boolean
|
||
* description: Whether to track inventory stock for this article
|
||
* example:
|
||
* track_stock: true
|
||
* responses:
|
||
* 200:
|
||
* description: Updated article detail
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* properties:
|
||
* article:
|
||
* $ref: '#/components/schemas/ArticleDetail'
|
||
* 400:
|
||
* description: Invalid track_stock
|
||
* 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: Article not found
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* $ref: '#/components/schemas/Error'
|
||
*/
|
||
async function handleSetTrackStock(req, res) {
|
||
const skuParsed = parseSku(req.params.sku);
|
||
if (skuParsed.error) {
|
||
return res.status(400).json({ error: skuParsed.error });
|
||
}
|
||
|
||
const raw = req.body?.track_stock ?? req.body?.trackStock;
|
||
if (raw === undefined || (typeof raw !== 'boolean' && raw !== 0 && raw !== 1)) {
|
||
return res.status(400).json({ error: 'Invalid track_stock' });
|
||
}
|
||
|
||
try {
|
||
const article = await db.setTrackStockBySku(req.user.id, skuParsed.sku, Boolean(raw));
|
||
emitArticleSignal(req, skuParsed.sku);
|
||
res.status(200).json({ article });
|
||
} catch (error) {
|
||
const status = articleErrorStatus(error);
|
||
if (status === 500) console.error('Set article track_stock failed:', error);
|
||
res.status(status).json({ error: error.message });
|
||
}
|
||
}
|
||
|
||
app.put(`${basePath}/article/:sku/track_stock`, handleSetTrackStock);
|
||
app.put(`${basePath}/article/:sku/track-stock`, handleSetTrackStock);
|
||
|
||
/**
|
||
* @openapi
|
||
* /article/{sku}/categories:
|
||
* put:
|
||
* tags: [Article]
|
||
* summary: Replace article categories by SKU
|
||
* description: |
|
||
* Replace-all the article's category list. Each category must belong to
|
||
* the article's company; ids must be unique. Empty array removes the
|
||
* article from all categories. Returns the same shape as GET by SKU.
|
||
* security:
|
||
* - cookieAuth: []
|
||
* x-signals: [article-<sku>]
|
||
* x-markdown: article/set-categories.md
|
||
* parameters:
|
||
* - in: path
|
||
* name: sku
|
||
* required: true
|
||
* schema:
|
||
* type: string
|
||
* requestBody:
|
||
* required: true
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* required: [categoryIds]
|
||
* properties:
|
||
* categoryIds:
|
||
* type: array
|
||
* items:
|
||
* type: integer
|
||
* example:
|
||
* categoryIds: [4, 7]
|
||
* responses:
|
||
* 200:
|
||
* description: Updated article detail
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* properties:
|
||
* article:
|
||
* $ref: '#/components/schemas/ArticleDetail'
|
||
* 400:
|
||
* description: Invalid categoryIds
|
||
* 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: Article not found
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* $ref: '#/components/schemas/Error'
|
||
*/
|
||
app.put(`${basePath}/article/:sku/categories`, async (req, res) => {
|
||
const skuParsed = parseSku(req.params.sku);
|
||
if (skuParsed.error) {
|
||
return res.status(400).json({ error: skuParsed.error });
|
||
}
|
||
|
||
if (!Array.isArray(req.body?.categoryIds)) {
|
||
return res.status(400).json({ error: 'categoryIds is required' });
|
||
}
|
||
|
||
try {
|
||
const article = await db.setCategoryIdsBySku(
|
||
req.user.id,
|
||
skuParsed.sku,
|
||
req.body.categoryIds,
|
||
);
|
||
emitArticleSignal(req, skuParsed.sku);
|
||
res.status(200).json({ article });
|
||
} catch (error) {
|
||
const status = articleErrorStatus(error);
|
||
if (status === 500) console.error('Set article categories failed:', error);
|
||
res.status(status).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
/**
|
||
* @openapi
|
||
* /articleIndex:
|
||
* get:
|
||
* tags: [Article]
|
||
* summary: List article search index
|
||
* description: |
|
||
* Returns every article reachable via the company's category tree.
|
||
* Designed for client-side search / autocomplete preload.
|
||
* Auth via company access.
|
||
* security:
|
||
* - cookieAuth: []
|
||
* responses:
|
||
* 200:
|
||
* description: Search index
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* properties:
|
||
* articles:
|
||
* type: array
|
||
* items:
|
||
* type: object
|
||
* properties:
|
||
* id:
|
||
* type: integer
|
||
* sku:
|
||
* type: string
|
||
* name:
|
||
* type: string
|
||
* nullable: true
|
||
* eans:
|
||
* type: array
|
||
* items:
|
||
* type: string
|
||
* categoryIds:
|
||
* type: array
|
||
* items:
|
||
* type: integer
|
||
* 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}/articleIndex`, async (req, res) => {
|
||
const companyId = req.user.selectedCompanyId;
|
||
if (companyId == null) {
|
||
return res.status(400).json({ error: 'No company selected' });
|
||
}
|
||
|
||
try {
|
||
const articles = await db.listSearchIndex(req.user.id);
|
||
res.status(200).json({ articles });
|
||
} catch (error) {
|
||
const status = articleErrorStatus(error);
|
||
if (status === 500) console.error('List article search index failed:', error);
|
||
res.status(status).json({ error: error.message });
|
||
}
|
||
});
|
||
}
|