u
This commit is contained in:
467
src/api/routes/labeltemplate.js
Normal file
467
src/api/routes/labeltemplate.js
Normal file
@@ -0,0 +1,467 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user