Files
dashback/src/api/routes/labeltemplate.js
2026-09-27 22:01:47 +02:00

468 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import * as db from '../../db/labeltemplate.js';
import { renderThumb } from '../../services/labelThumb.js';
import { emit } from '../sse.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 labeltemplateErrorStatus(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 === 'Label template not found') return 404;
if (error.message === 'Name required') return 400;
if (error.message === 'Invalid width') return 400;
if (error.message === 'Invalid height') return 400;
if (error.message?.startsWith('Invalid dpi')) return 400;
if (error.message?.startsWith('Invalid type')) return 400;
if (error.message === 'Content required') return 400;
return 500;
}
/**
* Registers label template routes for the authenticated user's selected company.
* @param {import('express').Application} app
* @param {string} [basePath='']
*/
export function register(app, basePath = '') {
/**
* @openapi
* /labeltemplate:
* 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.
* security:
* - cookieAuth: []
* responses:
* 200:
* description: List of label templates
* content:
* application/json:
* schema:
* type: object
* properties:
* labeltemplates:
* type: array
* items:
* $ref: '#/components/schemas/LabelTemplate'
* 400:
* description: No company selected
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
app.get(`${basePath}/labeltemplate`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
try {
const templates = await db.list(req.user.id);
res.status(200).json({ labeltemplates: templates.map(serializeTemplate) });
} catch (error) {
const status = labeltemplateErrorStatus(error);
if (status === 500) console.error('List label templates failed:', error);
res.status(status).json({ error: error.message });
}
});
/**
* @openapi
* /labeltemplate/{id}:
* get:
* tags: [LabelTemplate]
* summary: Get label template by ID
* description: Retrieves a single label template for the authenticated user's selected company.
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: Label template ID
* responses:
* 200:
* description: Label template details
* content:
* application/json:
* schema:
* type: object
* properties:
* labeltemplate:
* $ref: '#/components/schemas/LabelTemplate'
* 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'
* 404:
* description: Label template not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
app.get(`${basePath}/labeltemplate/: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 template = await db.getById(req.user.id, parsedId.id);
if (!template) {
return res.status(404).json({ error: 'Label template not found' });
}
res.status(200).json({ labeltemplate: serializeTemplate(template) });
} catch (error) {
const status = labeltemplateErrorStatus(error);
if (status === 500) console.error('Get label template failed:', error);
res.status(status).json({ error: error.message });
}
});
/**
* @openapi
* /labeltemplate:
* post:
* tags: [LabelTemplate]
* summary: Create label template
* description: |
* Creates a new ZPL label template for the selected company.
* A 50×50 1-bit PNG thumbnail is automatically rendered from the content
* and stored with the template.
* security:
* - cookieAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/LabelTemplateCreateInput'
* responses:
* 201:
* description: Created label template
* content:
* application/json:
* schema:
* type: object
* properties:
* labeltemplate:
* $ref: '#/components/schemas/LabelTemplate'
* 400:
* description: Invalid input or missing fields
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
app.post(`${basePath}/labeltemplate`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
const { name, width, height, dpi, type, content } = req.body ?? {};
try {
const thumb = await buildThumb({ width, height, content });
const template = await db.create(req.user.id, {
name,
width,
height,
dpi,
type,
content,
thumb,
});
emit('labeltemplate', { companyId: selected.companyId });
res.status(201).json({ labeltemplate: serializeTemplate(template) });
} catch (error) {
const status = labeltemplateErrorStatus(error);
if (status === 500) console.error('Create label template failed:', error);
res.status(status).json({ error: error.message });
}
});
async function handleUpdate(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 { name, width, height, dpi, type, content } = req.body ?? {};
try {
// Re-render thumb whenever content/dimensions are provided
const thumb = await buildThumb({ width, height, content });
const template = await db.update(req.user.id, parsedId.id, {
name,
width,
height,
dpi,
type,
content,
...(thumb !== null ? { thumb } : {}),
});
emit('labeltemplate', { companyId: selected.companyId });
res.status(200).json({ labeltemplate: serializeTemplate(template) });
} catch (error) {
const status = labeltemplateErrorStatus(error);
if (status === 500) console.error('Update label template failed:', error);
res.status(status).json({ error: error.message });
}
}
/**
* @openapi
* /labeltemplate/{id}:
* patch:
* tags: [LabelTemplate]
* summary: Update label template (partial)
* description: |
* Updates specified fields of an existing label template.
* When `content` (or `width`/`height`) is provided, the thumbnail is automatically re-rendered.
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: Label template ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/LabelTemplateUpdateInput'
* responses:
* 200:
* description: Updated label template
* content:
* application/json:
* schema:
* type: object
* properties:
* labeltemplate:
* $ref: '#/components/schemas/LabelTemplate'
* 400:
* description: Invalid input
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 404:
* description: Label template not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* put:
* tags: [LabelTemplate]
* summary: Update label template
* description: |
* Replaces an existing label template.
* When `content` (or `width`/`height`) is provided, the thumbnail is automatically re-rendered.
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: Label template ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/LabelTemplateUpdateInput'
* responses:
* 200:
* description: Updated label template
* content:
* application/json:
* schema:
* type: object
* properties:
* labeltemplate:
* $ref: '#/components/schemas/LabelTemplate'
* 400:
* description: Invalid input
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 404:
* description: Label template not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
app.patch(`${basePath}/labeltemplate/:id`, handleUpdate);
app.put(`${basePath}/labeltemplate/:id`, handleUpdate);
/**
* @openapi
* /labeltemplate/{id}:
* delete:
* tags: [LabelTemplate]
* summary: Delete label template
* description: Deletes a label template for the authenticated user's selected company.
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: Label template ID
* responses:
* 204:
* description: Label template 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'
* 404:
* description: Label template not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
app.delete(`${basePath}/labeltemplate/: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);
emit('labeltemplate', { companyId: selected.companyId });
res.sendStatus(204);
} catch (error) {
const status = labeltemplateErrorStatus(error);
if (status === 500) console.error('Delete label template failed:', error);
res.status(status).json({ error: error.message });
}
});
}
/**
* Serializes a label template row for the API response.
* Converts the thumb Buffer to a base64 string (or null).
*/
function serializeTemplate(template) {
return {
...template,
thumb: template.thumb
? template.thumb.toString('base64')
: null,
};
}
/**
* Attempts to render a thumbnail from content + dimensions.
* Returns null silently if content or dimensions are missing/invalid —
* the store will reject the request with a proper error anyway.
*
* @returns {Promise<Buffer|null>}
*/
async function buildThumb({ width, height, content }) {
try {
if (content == null) return null;
const w = Number(width);
const h = Number(height);
if (!Number.isInteger(w) || w <= 0 || !Number.isInteger(h) || h <= 0) return null;
const objects = Array.isArray(content?.objects)
? content.objects
: Array.isArray(content)
? content
: [];
return await renderThumb({ width: w, height: h, objects });
} catch {
// Thumbnail rendering failure is non-fatal
return null;
}
}