u
This commit is contained in:
@@ -70,7 +70,7 @@ function articleErrorStatus(error) {
|
||||
if (error.message === 'Unit not found') return 404;
|
||||
if (error.message === 'Invalid eans') return 400;
|
||||
if (error.message === 'EAN already in use' || error.message === 'EAN already assigned') return 409;
|
||||
if (error.message?.includes('UNIQUE constraint') || error.message?.includes('duplicate key') || error.message?.includes('Unique constraint')) return 409;
|
||||
if (error.message?.includes('UNIQUE constraint') || error.message?.includes('duplicate key') || error.message?.includes('Unique constraint') || error.message?.includes('already exists')) return 409;
|
||||
if (error.message === 'Company not found') return 404;
|
||||
if (error.message === 'Brand not found') return 404;
|
||||
if (error.message === 'Invalid brandId') return 400;
|
||||
@@ -222,6 +222,7 @@ export function register(app, basePath = '') {
|
||||
return res.status(400).json({ error: 'Invalid track_stock' });
|
||||
}
|
||||
const track_stock = rawTrack !== undefined ? Boolean(rawTrack) : true;
|
||||
const customSku = typeof req.body?.sku === 'string' && req.body.sku.trim() ? req.body.sku.trim() : undefined;
|
||||
|
||||
try {
|
||||
const created = await db.createUnderParent({
|
||||
@@ -229,6 +230,7 @@ export function register(app, basePath = '') {
|
||||
parentId,
|
||||
name,
|
||||
track_stock,
|
||||
sku: customSku,
|
||||
});
|
||||
emitArticlesSignal(req);
|
||||
const article = {
|
||||
|
||||
@@ -46,7 +46,7 @@ export function register(app, basePath = '') {
|
||||
* get:
|
||||
* tags: [LabelTemplate]
|
||||
* summary: List label templates
|
||||
* description: Lists all ZPL label templates for the selected company. The `thumb` field is returned as a base64-encoded PNG string when present. `content.objects` is omitted to reduce payload size.
|
||||
* description: Lists all ZPL label templates for the selected company. The `thumb` field is returned as a base64-encoded PNG string when present. The `content` field is omitted to reduce payload size (use GET /labeltemplate/{id} for it).
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* x-markdown: labeltemplate/list.md
|
||||
@@ -83,7 +83,10 @@ export function register(app, basePath = '') {
|
||||
|
||||
try {
|
||||
const templates = await db.list(req.user.id);
|
||||
res.status(200).json({ labeltemplates: templates.map(serializeTemplate) });
|
||||
res.status(200).json({ labeltemplates: templates.map((t) => {
|
||||
const { content, ...rest } = serializeTemplate(t);
|
||||
return rest;
|
||||
}) });
|
||||
} catch (error) {
|
||||
const status = labeltemplateErrorStatus(error);
|
||||
if (status === 500) console.error('List label templates failed:', error);
|
||||
|
||||
577
src/api/routes/modelconversation.js
Normal file
577
src/api/routes/modelconversation.js
Normal file
@@ -0,0 +1,577 @@
|
||||
import * as db from '../../db/modelconversation.js';
|
||||
import { emit } from '../sse.js';
|
||||
import { getOrCreateManager, removeManager } from '../../services/conversationManager.js';
|
||||
|
||||
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 modelConversationErrorStatus(error) {
|
||||
if (error.message === 'No company selected') return 400;
|
||||
if (error.message === 'Not authorized') return 403;
|
||||
if (error.message === 'Company not found') return 404;
|
||||
if (error.message === 'Conversation not found') return 404;
|
||||
if (error.message === 'Provider required') return 400;
|
||||
if (error.message === 'Model required') return 400;
|
||||
if (error.message === 'Message required') return 400;
|
||||
if (error.message === 'Conversation is not idle') return 409;
|
||||
return 500;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers model conversation routes. Conversations are private to the
|
||||
* authenticated user within their selected company.
|
||||
* @param {import('express').Application} app
|
||||
* @param {string} [basePath='']
|
||||
*/
|
||||
export function register(app, basePath = '') {
|
||||
/**
|
||||
* @openapi
|
||||
* /modelconversation:
|
||||
* get:
|
||||
* tags: [ModelConversation]
|
||||
* summary: List conversations
|
||||
* description: Lists the authenticated user's model conversations in the selected company, most recently updated first.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: List of conversations
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* conversations:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/ModelConversation'
|
||||
* 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'
|
||||
*/
|
||||
app.get(`${basePath}/modelconversation`, async (req, res) => {
|
||||
const selected = requireSelectedCompany(req);
|
||||
if (selected.error) {
|
||||
return res.status(400).json({ error: selected.error });
|
||||
}
|
||||
|
||||
try {
|
||||
const conversations = await db.list(req.user.id);
|
||||
res.status(200).json({ conversations });
|
||||
} catch (error) {
|
||||
const status = modelConversationErrorStatus(error);
|
||||
if (status === 500) console.error('List model conversations failed:', error);
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /modelconversation:
|
||||
* post:
|
||||
* tags: [ModelConversation]
|
||||
* summary: Create conversation
|
||||
* description: Creates a new conversation in state `idle`.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ModelConversationCreateInput'
|
||||
* responses:
|
||||
* 201:
|
||||
* description: Created conversation
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* conversation:
|
||||
* $ref: '#/components/schemas/ModelConversation'
|
||||
* 400:
|
||||
* description: Invalid input or 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'
|
||||
*/
|
||||
app.post(`${basePath}/modelconversation`, async (req, res) => {
|
||||
const selected = requireSelectedCompany(req);
|
||||
if (selected.error) {
|
||||
return res.status(400).json({ error: selected.error });
|
||||
}
|
||||
|
||||
const { provider, model } = req.body ?? {};
|
||||
|
||||
try {
|
||||
const conversation = await db.create(req.user.id, { provider, model });
|
||||
emit('modelconversation', { companyId: selected.companyId });
|
||||
res.status(201).json({ conversation });
|
||||
} catch (error) {
|
||||
const status = modelConversationErrorStatus(error);
|
||||
if (status === 500) console.error('Create model conversation failed:', error);
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /modelconversation/{id}:
|
||||
* get:
|
||||
* tags: [ModelConversation]
|
||||
* summary: Get conversation
|
||||
* description: Gets details and current state of one of the authenticated user's model conversations.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: Conversation ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Conversation details
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* conversation:
|
||||
* $ref: '#/components/schemas/ModelConversation'
|
||||
* 400:
|
||||
* description: Invalid ID or 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: Conversation not found
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
app.get(`${basePath}/modelconversation/:id`, async (req, res) => {
|
||||
const selected = requireSelectedCompany(req);
|
||||
if (selected.error) {
|
||||
return res.status(400).json({ error: selected.error });
|
||||
}
|
||||
|
||||
const parsedId = parsePositiveInt(req.params.id, 'id');
|
||||
if (parsedId.error) {
|
||||
return res.status(400).json({ error: parsedId.error });
|
||||
}
|
||||
|
||||
try {
|
||||
const conversation = await db.get(req.user.id, parsedId.id);
|
||||
res.status(200).json({ conversation });
|
||||
} catch (error) {
|
||||
const status = modelConversationErrorStatus(error);
|
||||
if (status === 500) console.error('Get model conversation failed:', error);
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /modelconversation/{id}:
|
||||
* delete:
|
||||
* tags: [ModelConversation]
|
||||
* summary: Delete conversation
|
||||
* description: Deletes one of the authenticated user's model conversations and all its messages.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: Conversation ID
|
||||
* responses:
|
||||
* 204:
|
||||
* description: Conversation deleted
|
||||
* 400:
|
||||
* description: Invalid ID or 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: Conversation not found
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
app.delete(`${basePath}/modelconversation/:id`, async (req, res) => {
|
||||
const selected = requireSelectedCompany(req);
|
||||
if (selected.error) {
|
||||
return res.status(400).json({ error: selected.error });
|
||||
}
|
||||
|
||||
const parsedId = parsePositiveInt(req.params.id, 'id');
|
||||
if (parsedId.error) {
|
||||
return res.status(400).json({ error: parsedId.error });
|
||||
}
|
||||
|
||||
try {
|
||||
await db.remove(req.user.id, parsedId.id);
|
||||
removeManager(parsedId.id);
|
||||
emit('modelconversation', { companyId: selected.companyId });
|
||||
res.sendStatus(204);
|
||||
} catch (error) {
|
||||
const status = modelConversationErrorStatus(error);
|
||||
if (status === 500) console.error('Delete model conversation failed:', error);
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /modelconversation/{id}/stream:
|
||||
* get:
|
||||
* tags: [ModelConversation]
|
||||
* summary: Stream conversation events (SSE)
|
||||
* description: |
|
||||
* Opens a Server-Sent Events stream for the specified conversation.
|
||||
* Streams `message` events (persisted user/assistant messages), live `delta`
|
||||
* text chunks of the model answer, and `state` events (`thinking`, `working`,
|
||||
* `done`). The SSE states are not stored; the database only knows `idle`/`busy`.
|
||||
* `done` carries `usage` (`input`, `cached`, `output`, `cost`) on success or
|
||||
* `error` (string) if the run failed.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: Conversation ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: |
|
||||
* SSE stream (`text/event-stream`). Emits `state`, `delta`, and `message` events.
|
||||
* content:
|
||||
* text/event-stream:
|
||||
* schema:
|
||||
* type: string
|
||||
* example: |
|
||||
* event: state
|
||||
* data: {"state":"thinking"}
|
||||
*
|
||||
* event: state
|
||||
* data: {"state":"working"}
|
||||
*
|
||||
* event: delta
|
||||
* data: {"delta":"Hel","text":"Hel"}
|
||||
*
|
||||
* event: message
|
||||
* data: {"id":2,"conversation_id":1,"created_at":"...","role":"assistant","message":"Hello there"}
|
||||
*
|
||||
* event: state
|
||||
* data: {"state":"done","usage":{"input":20,"cached":0,"output":3,"cost":0.0001}}
|
||||
* 400:
|
||||
* description: Invalid ID or 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: Conversation not found
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
app.get(`${basePath}/modelconversation/:id/stream`, async (req, res) => {
|
||||
const selected = requireSelectedCompany(req);
|
||||
if (selected.error) {
|
||||
return res.status(400).json({ error: selected.error });
|
||||
}
|
||||
|
||||
const parsedId = parsePositiveInt(req.params.id, 'id');
|
||||
if (parsedId.error) {
|
||||
return res.status(400).json({ error: parsedId.error });
|
||||
}
|
||||
|
||||
try {
|
||||
// Ensure conversation exists and user has access
|
||||
await db.get(req.user.id, parsedId.id);
|
||||
const manager = getOrCreateManager(parsedId.id, {
|
||||
companyId: selected.companyId,
|
||||
googleAuthId: req.user.id,
|
||||
});
|
||||
manager.subscribe(res, req);
|
||||
} catch (error) {
|
||||
const status = modelConversationErrorStatus(error);
|
||||
if (status === 500) console.error('Stream model conversation failed:', error);
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /modelconversation/{id}/message:
|
||||
* get:
|
||||
* tags: [ModelConversation]
|
||||
* summary: List conversation messages
|
||||
* description: Lists messages of one of the user's own conversations, oldest first.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: Conversation ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: List of messages
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* messages:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/ModelMessage'
|
||||
* 400:
|
||||
* description: Invalid ID or 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: Conversation not found
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
app.get(`${basePath}/modelconversation/:id/message`, async (req, res) => {
|
||||
const selected = requireSelectedCompany(req);
|
||||
if (selected.error) {
|
||||
return res.status(400).json({ error: selected.error });
|
||||
}
|
||||
|
||||
const parsedId = parsePositiveInt(req.params.id, 'id');
|
||||
if (parsedId.error) {
|
||||
return res.status(400).json({ error: parsedId.error });
|
||||
}
|
||||
|
||||
try {
|
||||
const messages = await db.listMessages(req.user.id, parsedId.id);
|
||||
res.status(200).json({ messages });
|
||||
} catch (error) {
|
||||
const status = modelConversationErrorStatus(error);
|
||||
if (status === 500) console.error('List model messages failed:', error);
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /modelconversation/{id}/message:
|
||||
* post:
|
||||
* tags: [ModelConversation]
|
||||
* summary: Post a user message
|
||||
* description: |
|
||||
* Appends a message with role `user` to the conversation.
|
||||
* Refused with 409 unless the conversation state is `idle`.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: Conversation ID
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ModelMessageCreateInput'
|
||||
* responses:
|
||||
* 201:
|
||||
* description: Created message
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* message:
|
||||
* $ref: '#/components/schemas/ModelMessage'
|
||||
* 400:
|
||||
* description: Invalid ID, empty message or 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: Conversation not found
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 409:
|
||||
* description: Conversation is not idle
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
app.post(`${basePath}/modelconversation/:id/message`, async (req, res) => {
|
||||
const selected = requireSelectedCompany(req);
|
||||
if (selected.error) {
|
||||
return res.status(400).json({ error: selected.error });
|
||||
}
|
||||
|
||||
const parsedId = parsePositiveInt(req.params.id, 'id');
|
||||
if (parsedId.error) {
|
||||
return res.status(400).json({ error: parsedId.error });
|
||||
}
|
||||
|
||||
const { message, tool_call_id, role, content, answer } = req.body ?? {};
|
||||
|
||||
try {
|
||||
const manager = getOrCreateManager(parsedId.id, {
|
||||
companyId: selected.companyId,
|
||||
googleAuthId: req.user.id,
|
||||
});
|
||||
|
||||
let created;
|
||||
if (role === 'tool' || tool_call_id) {
|
||||
if (!tool_call_id) {
|
||||
return res.status(400).json({ error: 'tool_call_id is required for tool response' });
|
||||
}
|
||||
created = await manager.handleToolMessage(req.user.id, {
|
||||
tool_call_id,
|
||||
content,
|
||||
answer: answer !== undefined ? answer : message,
|
||||
});
|
||||
} else {
|
||||
if (!message) {
|
||||
return res.status(400).json({ error: 'Message required' });
|
||||
}
|
||||
created = await manager.handleUserMessage(req.user.id, { message });
|
||||
}
|
||||
|
||||
emit(`modelconversation-${parsedId.id}`, { companyId: selected.companyId });
|
||||
res.status(201).json({ message: created });
|
||||
} catch (error) {
|
||||
const status = modelConversationErrorStatus(error);
|
||||
if (status === 500) console.error('Post model message failed:', error);
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
364
src/api/routes/publicCatalog.js
Normal file
364
src/api/routes/publicCatalog.js
Normal file
@@ -0,0 +1,364 @@
|
||||
import * as db from '../../db/publicCatalog.js';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'
|
||||
*/
|
||||
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 });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @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'
|
||||
*/
|
||||
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 });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @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
|
||||
* isFather:
|
||||
* type: boolean
|
||||
* fatherSku:
|
||||
* 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'
|
||||
*/
|
||||
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 });
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user