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

View File

@@ -213,18 +213,18 @@
* nullable: true * nullable: true
* SupplierArticlePhoto: * SupplierArticlePhoto:
* type: object * type: object
* required: [sort, thumbhash, hash, thumbw, thumbh] * required: [sort, fullHash, thumbW, thumbH, thumbHash]
* properties: * properties:
* sort: * sort:
* type: integer * type: integer
* thumbhash: * fullHash:
* type: string * type: string
* hash: * thumbW:
* type: integer
* thumbH:
* type: integer
* thumbHash:
* type: string * type: string
* thumbw:
* type: integer
* thumbh:
* type: integer
* SupplierArticleInfo: * SupplierArticleInfo:
* type: object * type: object
* required: [id, sku] * required: [id, sku]
@@ -243,18 +243,10 @@
* type: array * type: array
* items: * items:
* $ref: '#/components/schemas/SupplierArticlePhoto' * $ref: '#/components/schemas/SupplierArticlePhoto'
* articlepic:
* type: array
* items:
* $ref: '#/components/schemas/SupplierArticlePhoto'
* eans: * eans:
* type: array * type: array
* items: * items:
* type: string * type: string
* articleean:
* type: array
* items:
* type: string
* SupplierArticle: * SupplierArticle:
* allOf: * allOf:
* - $ref: '#/components/schemas/ArticleSupplier' * - $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 * as db from '../../db/labeltemplate.js';
import { renderThumb } from '../../services/labelThumb.js'; import { renderThumb } from '../../services/labelThumb.js';
import { emit } from '../sse.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 dpi')) return 400;
if (error.message?.startsWith('Invalid type')) return 400; if (error.message?.startsWith('Invalid type')) return 400;
if (error.message === 'Content required') return 400; if (error.message === 'Content required') return 400;
if (error.message?.startsWith('Invalid label objects')) return 400;
return 500; return 500;
} }
@@ -209,8 +211,13 @@ export function register(app, basePath = '') {
const { name, width, height, dpi, type, content } = req.body ?? {}; 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 { 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, { const template = await db.create(req.user.id, {
name, name,
width, width,
@@ -242,9 +249,18 @@ export function register(app, basePath = '') {
const { name, width, height, dpi, type, content } = req.body ?? {}; 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 { try {
// Re-render thumb whenever content/dimensions are provided // 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, { const template = await db.update(req.user.id, parsedId.id, {
name, name,
width, 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. * Attempts to render a thumbnail from content + dimensions.
* Returns null silently if content or dimensions are missing/invalid — * Returns null silently if content or dimensions are missing/invalid —
@@ -452,18 +507,20 @@ function serializeTemplate(template) {
* *
* @returns {Promise<Buffer|null>} * @returns {Promise<Buffer|null>}
*/ */
async function buildThumb({ width, height, content }) { async function buildThumb({ width, height, content, objects: passedObjects }) {
try { try {
if (content == null) return null; if (content == null && passedObjects == null) return null;
const w = Number(width); const w = Number(width);
const h = Number(height); const h = Number(height);
if (!Number.isInteger(w) || w <= 0 || !Number.isInteger(h) || h <= 0) return null; if (!Number.isInteger(w) || w <= 0 || !Number.isInteger(h) || h <= 0) return null;
const objects = Array.isArray(content?.objects) const objects = passedObjects ?? (
? content.objects Array.isArray(content?.objects)
: Array.isArray(content) ? content.objects
? content : Array.isArray(content)
: []; ? content
: []
);
return await renderThumb({ width: w, height: h, objects }); return await renderThumb({ width: w, height: h, objects });
} catch { } catch {
@@ -471,3 +528,4 @@ async function buildThumb({ width, height, content }) {
return null; return null;
} }
} }

View File

@@ -349,7 +349,7 @@ export function register(app, basePath = '') {
* description: | * description: |
* Lists article-supplier assignments for the specified supplier (entity ID), * Lists article-supplier assignments for the specified supplier (entity ID),
* optionally filtered by a list of SKUs (matching either article SKU or supplier SKU). * 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: * security:
* - cookieAuth: [] * - cookieAuth: []
* x-markdown: supplier/articles.get.md * x-markdown: supplier/articles.get.md
@@ -412,7 +412,7 @@ export function register(app, basePath = '') {
* description: | * description: |
* Queries article-supplier assignments for the specified supplier (entity ID) with a * 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. * 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: * security:
* - cookieAuth: [] * - cookieAuth: []
* x-markdown: supplier/articles.post.md * x-markdown: supplier/articles.post.md