This commit is contained in:
seb
2026-09-29 13:44:14 +02:00
parent 6966c5f092
commit b92e2deb41
6 changed files with 43 additions and 12 deletions

View File

@@ -8949,7 +8949,7 @@
"Category" "Category"
], ],
"summary": "Add category image", "summary": "Add category image",
"description": "Accepts a full-size AVIF body (`Content-Type: image/avif`).\nFull: stored as-is when already AVIF and ≤1000×1000; otherwise scaled to max 1000×1000.\nThumb always scaled to max 200×200. Stores hash/thumbhash, links `categorypic`.\nAuth like GET /category/tree — user must be authorized for the category's company.\n", "description": "Accepts a full-size AVIF body (`Content-Type: image/avif`) or WebP body (`Content-Type: image/webp`).\nWebP images are automatically converted to AVIF on the server.\nFull: stored as-is when already AVIF and ≤1000×1000; otherwise scaled to max 1000×1000.\nThumb always scaled to max 200×200. Stores hash/thumbhash, links `categorypic`.\nAuth like GET /category/tree — user must be authorized for the category's company.\n",
"security": [ "security": [
{ {
"cookieAuth": [] "cookieAuth": []
@@ -8978,6 +8978,12 @@
"type": "string", "type": "string",
"format": "binary" "format": "binary"
} }
},
"image/webp": {
"schema": {
"type": "string",
"format": "binary"
}
} }
} }
}, },
@@ -10687,7 +10693,7 @@
"Article" "Article"
], ],
"summary": "Add article image", "summary": "Add article image",
"description": "Accepts a full-size AVIF body (`Content-Type: image/avif`).\nFull: stored as-is when already AVIF and ≤1000×1000; otherwise scaled to max 1000×1000.\nThumb always scaled to max 200×200. Stores hash/thumbhash, links `articlepic`.\nAuth like GET by SKU.\n", "description": "Accepts a full-size AVIF body (`Content-Type: image/avif`) or WebP body (`Content-Type: image/webp`).\nWebP images are automatically converted to AVIF on the server.\nFull: stored as-is when already AVIF and ≤1000×1000; otherwise scaled to max 1000×1000.\nThumb always scaled to max 200×200. Stores hash/thumbhash, links `articlepic`.\nAuth like GET by SKU.\n",
"security": [ "security": [
{ {
"cookieAuth": [] "cookieAuth": []
@@ -10716,6 +10722,12 @@
"type": "string", "type": "string",
"format": "binary" "format": "binary"
} }
},
"image/webp": {
"schema": {
"type": "string",
"format": "binary"
}
} }
} }
}, },

View File

@@ -4,7 +4,8 @@ Protected. Requires a valid session cookie (`credentials: 'include'`).
Add article image Add article image
Accepts a full-size AVIF body (`Content-Type: image/avif`). Accepts a full-size AVIF body (`Content-Type: image/avif`) or WebP body (`Content-Type: image/webp`).
WebP images are automatically converted to AVIF on the server.
Full: stored as-is when already AVIF and ≤1000×1000; otherwise scaled to max 1000×1000. Full: stored as-is when already AVIF and ≤1000×1000; otherwise scaled to max 1000×1000.
Thumb always scaled to max 200×200. Stores hash/thumbhash, links `articlepic`. Thumb always scaled to max 200×200. Stores hash/thumbhash, links `articlepic`.
Auth like GET by SKU. Auth like GET by SKU.
@@ -20,6 +21,8 @@ Auth like GET by SKU.
Raw binary (`image/avif`). Raw binary (`image/avif`).
Raw binary (`image/webp`).
## Signals ## Signals
On success, emits SSE signal: `article-<sku>` (see [`GET /events`](../events.md)). On success, emits SSE signal: `article-<sku>` (see [`GET /events`](../events.md)).

View File

@@ -4,7 +4,8 @@ Protected. Requires a valid session cookie (`credentials: 'include'`).
Add category image Add category image
Accepts a full-size AVIF body (`Content-Type: image/avif`). Accepts a full-size AVIF body (`Content-Type: image/avif`) or WebP body (`Content-Type: image/webp`).
WebP images are automatically converted to AVIF on the server.
Full: stored as-is when already AVIF and ≤1000×1000; otherwise scaled to max 1000×1000. Full: stored as-is when already AVIF and ≤1000×1000; otherwise scaled to max 1000×1000.
Thumb always scaled to max 200×200. Stores hash/thumbhash, links `categorypic`. Thumb always scaled to max 200×200. Stores hash/thumbhash, links `categorypic`.
Auth like GET /category/tree — user must be authorized for the category's company. Auth like GET /category/tree — user must be authorized for the category's company.
@@ -20,6 +21,8 @@ Auth like GET /category/tree — user must be authorized for the category's comp
Raw binary (`image/avif`). Raw binary (`image/avif`).
Raw binary (`image/webp`).
## Signals ## Signals
On success, emits SSE signal: `category` (see [`GET /events`](../events.md)). On success, emits SSE signal: `category` (see [`GET /events`](../events.md)).

View File

@@ -51,6 +51,7 @@ function parseFullHash(value) {
function articleErrorStatus(error) { function articleErrorStatus(error) {
if (error.message === 'No company selected') return 400; if (error.message === 'No company selected') return 400;
if (error.message === 'Unsupported image format' || error.message?.includes('unsupported image format')) return 400;
if (error.message === 'Not authorized') return 403; if (error.message === 'Not authorized') return 403;
if (error.message === 'Parent not found') return 404; if (error.message === 'Parent not found') return 404;
if (error.message === 'Article not found') return 404; if (error.message === 'Article not found') return 404;
@@ -738,7 +739,8 @@ export function register(app, basePath = '') {
* tags: [Article] * tags: [Article]
* summary: Add article image * summary: Add article image
* description: | * description: |
* Accepts a full-size AVIF body (`Content-Type: image/avif`). * Accepts a full-size AVIF body (`Content-Type: image/avif`) or WebP body (`Content-Type: image/webp`).
* WebP images are automatically converted to AVIF on the server.
* Full: stored as-is when already AVIF and ≤1000×1000; otherwise scaled to max 1000×1000. * Full: stored as-is when already AVIF and ≤1000×1000; otherwise scaled to max 1000×1000.
* Thumb always scaled to max 200×200. Stores hash/thumbhash, links `articlepic`. * Thumb always scaled to max 200×200. Stores hash/thumbhash, links `articlepic`.
* Auth like GET by SKU. * Auth like GET by SKU.
@@ -760,6 +762,10 @@ export function register(app, basePath = '') {
* schema: * schema:
* type: string * type: string
* format: binary * format: binary
* image/webp:
* schema:
* type: string
* format: binary
* responses: * responses:
* 201: * 201:
* description: Image stored and linked * description: Image stored and linked
@@ -797,7 +803,7 @@ export function register(app, basePath = '') {
*/ */
app.post( app.post(
`${basePath}/article/:sku/image`, `${basePath}/article/:sku/image`,
express.raw({ type: ['image/avif', 'application/octet-stream'], limit: AVIF_BODY_LIMIT }), express.raw({ type: ['image/avif', 'image/webp', 'application/octet-stream'], limit: AVIF_BODY_LIMIT }),
async (req, res) => { async (req, res) => {
const sku = typeof req.params.sku === 'string' ? req.params.sku.trim() : ''; const sku = typeof req.params.sku === 'string' ? req.params.sku.trim() : '';
if (!sku) { if (!sku) {
@@ -806,7 +812,7 @@ export function register(app, basePath = '') {
const imageBuffer = Buffer.isBuffer(req.body) ? req.body : null; const imageBuffer = Buffer.isBuffer(req.body) ? req.body : null;
if (!imageBuffer?.length) { if (!imageBuffer?.length) {
return res.status(400).json({ error: 'AVIF image body is required' }); return res.status(400).json({ error: 'AVIF or WebP image body is required' });
} }
try { try {

View File

@@ -13,6 +13,7 @@ function requireSelectedCompany(req) {
function brandErrorStatus(error) { function brandErrorStatus(error) {
if (error.message === 'No company selected') return 400; if (error.message === 'No company selected') return 400;
if (error.message === 'Unsupported image format' || error.message?.includes('unsupported image format')) return 400;
if (error.message === 'Not authorized') return 403; if (error.message === 'Not authorized') return 403;
if (error.message === 'Company not found') return 404; if (error.message === 'Company not found') return 404;
if (error.message === 'Brand not found') return 404; if (error.message === 'Brand not found') return 404;
@@ -244,7 +245,7 @@ export function register(app, basePath = '') {
app.post( app.post(
`${basePath}/brand/:id/image`, `${basePath}/brand/:id/image`,
express.raw({ type: ['image/avif', 'application/octet-stream'], limit: AVIF_BODY_LIMIT }), express.raw({ type: ['image/avif', 'image/webp', 'application/octet-stream'], limit: AVIF_BODY_LIMIT }),
async (req, res) => { async (req, res) => {
const selected = requireSelectedCompany(req); const selected = requireSelectedCompany(req);
if (selected.error) { if (selected.error) {
@@ -258,7 +259,7 @@ export function register(app, basePath = '') {
const imageBuffer = Buffer.isBuffer(req.body) ? req.body : null; const imageBuffer = Buffer.isBuffer(req.body) ? req.body : null;
if (!imageBuffer?.length) { if (!imageBuffer?.length) {
return res.status(400).json({ error: 'AVIF image body is required' }); return res.status(400).json({ error: 'AVIF or WebP image body is required' });
} }
try { try {

View File

@@ -24,6 +24,7 @@ function parsePositiveInt(value, field) {
function categoryErrorStatus(error) { function categoryErrorStatus(error) {
if (error.message === 'No company selected') return 400; if (error.message === 'No company selected') return 400;
if (error.message === 'Unsupported image format' || error.message?.includes('unsupported image format')) return 400;
if (error.message === 'Not authorized') return 403; if (error.message === 'Not authorized') return 403;
if (error.message === 'Company not found') return 404; if (error.message === 'Company not found') return 404;
if (error.message === 'Parent not found') return 404; if (error.message === 'Parent not found') return 404;
@@ -647,7 +648,8 @@ export function register(app, basePath = '') {
* tags: [Category] * tags: [Category]
* summary: Add category image * summary: Add category image
* description: | * description: |
* Accepts a full-size AVIF body (`Content-Type: image/avif`). * Accepts a full-size AVIF body (`Content-Type: image/avif`) or WebP body (`Content-Type: image/webp`).
* WebP images are automatically converted to AVIF on the server.
* Full: stored as-is when already AVIF and ≤1000×1000; otherwise scaled to max 1000×1000. * Full: stored as-is when already AVIF and ≤1000×1000; otherwise scaled to max 1000×1000.
* Thumb always scaled to max 200×200. Stores hash/thumbhash, links `categorypic`. * Thumb always scaled to max 200×200. Stores hash/thumbhash, links `categorypic`.
* Auth like GET /category/tree — user must be authorized for the category's company. * Auth like GET /category/tree — user must be authorized for the category's company.
@@ -669,6 +671,10 @@ export function register(app, basePath = '') {
* schema: * schema:
* type: string * type: string
* format: binary * format: binary
* image/webp:
* schema:
* type: string
* format: binary
* responses: * responses:
* 201: * 201:
* description: Image stored and linked * description: Image stored and linked
@@ -772,7 +778,7 @@ export function register(app, basePath = '') {
*/ */
app.post( app.post(
`${basePath}/category/:id/image`, `${basePath}/category/:id/image`,
express.raw({ type: ['image/avif', 'application/octet-stream'], limit: AVIF_BODY_LIMIT }), express.raw({ type: ['image/avif', 'image/webp', 'application/octet-stream'], limit: AVIF_BODY_LIMIT }),
async (req, res) => { async (req, res) => {
const parsed = parsePositiveInt(req.params.id, 'id'); const parsed = parsePositiveInt(req.params.id, 'id');
if (parsed.error) { if (parsed.error) {
@@ -781,7 +787,7 @@ export function register(app, basePath = '') {
const imageBuffer = Buffer.isBuffer(req.body) ? req.body : null; const imageBuffer = Buffer.isBuffer(req.body) ? req.body : null;
if (!imageBuffer?.length) { if (!imageBuffer?.length) {
return res.status(400).json({ error: 'AVIF image body is required' }); return res.status(400).json({ error: 'AVIF or WebP image body is required' });
} }
try { try {