Compare commits

...

2 Commits

Author SHA1 Message Date
seb
20fbea985e u 2026-09-30 21:12:29 +02:00
seb
d8b2524822 u 2026-09-30 21:04:53 +02:00
5 changed files with 86 additions and 60 deletions

View File

@@ -6,7 +6,7 @@ List supplier articles by SKUs
Lists article-supplier assignments for the specified supplier (entity ID),
optionally filtered by a list of SKUs (matching either article SKU or supplier SKU).
Includes linked article details (name, slug, photos/articlepic, eans/articleean).
Includes linked article details (name, slug, photos, eans).
## Path parameters
@@ -51,26 +51,14 @@ List of supplier articles
"photos": [
{
"sort": 1,
"thumbhash": "…",
"hash": "…",
"thumbw": 1,
"thumbh": 1
}
],
"articlepic": [
{
"sort": 1,
"thumbhash": "…",
"hash": "…",
"thumbw": 1,
"thumbh": 1
"fullHash": "…",
"thumbW": 1,
"thumbH": 1,
"thumbHash": "…"
}
],
"eans": [
"…"
],
"articleean": [
"…"
]
}
}

View File

@@ -6,7 +6,7 @@ Query supplier articles by SKUs (bulk)
Queries article-supplier assignments for the specified supplier (entity ID) with a
JSON list of SKUs in the request body, avoiding URL length limits for large SKU lists.
Includes linked article details (name, slug, photos/articlepic, eans/articleean).
Includes linked article details (name, slug, photos, eans).
## Path parameters
@@ -60,26 +60,14 @@ List of supplier articles
"photos": [
{
"sort": 1,
"thumbhash": "…",
"hash": "…",
"thumbw": 1,
"thumbh": 1
}
],
"articlepic": [
{
"sort": 1,
"thumbhash": "…",
"hash": "…",
"thumbw": 1,
"thumbh": 1
"fullHash": "…",
"thumbW": 1,
"thumbH": 1,
"thumbHash": "…"
}
],
"eans": [
"…"
],
"articleean": [
"…"
]
}
}

View File

@@ -213,18 +213,18 @@
* nullable: true
* SupplierArticlePhoto:
* type: object
* required: [sort, thumbhash, hash, thumbw, thumbh]
* required: [sort, fullHash, thumbW, thumbH, thumbHash]
* properties:
* sort:
* type: integer
* thumbhash:
* fullHash:
* type: string
* hash:
* thumbW:
* type: integer
* thumbH:
* type: integer
* thumbHash:
* type: string
* thumbw:
* type: integer
* thumbh:
* type: integer
* SupplierArticleInfo:
* type: object
* required: [id, sku]
@@ -243,18 +243,10 @@
* type: array
* items:
* $ref: '#/components/schemas/SupplierArticlePhoto'
* articlepic:
* type: array
* items:
* $ref: '#/components/schemas/SupplierArticlePhoto'
* eans:
* type: array
* items:
* type: string
* articleean:
* type: array
* items:
* type: string
* SupplierArticle:
* allOf:
* - $ref: '#/components/schemas/ArticleSupplier'

View File

@@ -1,3 +1,4 @@
import { validateObjects } from 'react-canvas-label-editor';
import * as db from '../../db/labeltemplate.js';
import { renderThumb } from '../../services/labelThumb.js';
import { emit } from '../sse.js';
@@ -29,6 +30,7 @@ function labeltemplateErrorStatus(error) {
if (error.message?.startsWith('Invalid dpi')) return 400;
if (error.message?.startsWith('Invalid type')) return 400;
if (error.message === 'Content required') return 400;
if (error.message?.startsWith('Invalid label objects')) return 400;
return 500;
}
@@ -209,8 +211,13 @@ export function register(app, basePath = '') {
const { name, width, height, dpi, type, content } = req.body ?? {};
const validation = validateLabelContent(content);
if (!validation.valid) {
return res.status(400).json({ error: validation.error, ...(validation.errors ? { errors: validation.errors } : {}) });
}
try {
const thumb = await buildThumb({ width, height, content });
const thumb = await buildThumb({ width, height, content, objects: validation.objects });
const template = await db.create(req.user.id, {
name,
width,
@@ -242,9 +249,18 @@ export function register(app, basePath = '') {
const { name, width, height, dpi, type, content } = req.body ?? {};
let validatedObjects;
if (content !== undefined) {
const validation = validateLabelContent(content);
if (!validation.valid) {
return res.status(400).json({ error: validation.error, ...(validation.errors ? { errors: validation.errors } : {}) });
}
validatedObjects = validation.objects;
}
try {
// Re-render thumb whenever content/dimensions are provided
const thumb = await buildThumb({ width, height, content });
const thumb = await buildThumb({ width, height, content, objects: validatedObjects });
const template = await db.update(req.user.id, parsedId.id, {
name,
width,
@@ -445,6 +461,45 @@ function serializeTemplate(template) {
};
}
/**
* Validates the label template content and its objects.
* Expects content to be an object with an `objects` array (or an array of objects).
*
* @param {any} content
* @returns {{ valid: true, objects: Array } | { valid: false, error: string, errors?: string[] }}
*/
function validateLabelContent(content) {
if (content == null) {
return { valid: false, error: 'Content required' };
}
let parsed = content;
if (typeof content === 'string') {
try {
parsed = JSON.parse(content);
} catch {
return { valid: false, error: 'Invalid JSON in content' };
}
}
const objects = Array.isArray(parsed?.objects)
? parsed.objects
: Array.isArray(parsed)
? parsed
: null;
if (!objects) {
return { valid: false, error: 'content.objects must be an array' };
}
const { valid, errors } = validateObjects(objects);
if (!valid) {
return { valid: false, error: `Invalid label objects: ${errors.join(', ')}`, errors };
}
return { valid: true, objects };
}
/**
* Attempts to render a thumbnail from content + dimensions.
* Returns null silently if content or dimensions are missing/invalid —
@@ -452,18 +507,20 @@ function serializeTemplate(template) {
*
* @returns {Promise<Buffer|null>}
*/
async function buildThumb({ width, height, content }) {
async function buildThumb({ width, height, content, objects: passedObjects }) {
try {
if (content == null) return null;
if (content == null && passedObjects == 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
: [];
const objects = passedObjects ?? (
Array.isArray(content?.objects)
? content.objects
: Array.isArray(content)
? content
: []
);
return await renderThumb({ width: w, height: h, objects });
} catch {
@@ -471,3 +528,4 @@ async function buildThumb({ width, height, content }) {
return null;
}
}

View File

@@ -349,7 +349,7 @@ export function register(app, basePath = '') {
* description: |
* Lists article-supplier assignments for the specified supplier (entity ID),
* optionally filtered by a list of SKUs (matching either article SKU or supplier SKU).
* Includes linked article details (name, slug, photos/articlepic, eans/articleean).
* Includes linked article details (name, slug, photos, eans).
* security:
* - cookieAuth: []
* x-markdown: supplier/articles.get.md
@@ -412,7 +412,7 @@ export function register(app, basePath = '') {
* description: |
* Queries article-supplier assignments for the specified supplier (entity ID) with a
* JSON list of SKUs in the request body, avoiding URL length limits for large SKU lists.
* Includes linked article details (name, slug, photos/articlepic, eans/articleean).
* Includes linked article details (name, slug, photos, eans).
* security:
* - cookieAuth: []
* x-markdown: supplier/articles.post.md