This commit is contained in:
seb
2026-10-04 19:12:33 +02:00
parent a8e51724f8
commit a3926d3547
9 changed files with 165 additions and 17 deletions

View File

@@ -45,6 +45,7 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route
| [`routes/article/set-categories.md`](routes/article/set-categories.md) | `PUT /article/{sku}/categories` | | [`routes/article/set-categories.md`](routes/article/set-categories.md) | `PUT /article/{sku}/categories` |
| [`routes/article/_sku/children.get.md`](routes/article/_sku/children.get.md) | `GET /article/{sku}/children` | | [`routes/article/_sku/children.get.md`](routes/article/_sku/children.get.md) | `GET /article/{sku}/children` |
| [`routes/article/_sku/children.post.md`](routes/article/_sku/children.post.md) | `POST /article/{sku}/children` | | [`routes/article/_sku/children.post.md`](routes/article/_sku/children.post.md) | `POST /article/{sku}/children` |
| [`routes/article/_sku/children.put.md`](routes/article/_sku/children.put.md) | `PUT /article/{sku}/children` |
| [`routes/article/_sku/children/_childSku.md`](routes/article/_sku/children/_childSku.md) | `DELETE /article/{sku}/children/{childSku}` | | [`routes/article/_sku/children/_childSku.md`](routes/article/_sku/children/_childSku.md) | `DELETE /article/{sku}/children/{childSku}` |
| [`routes/article/set-eans.md`](routes/article/set-eans.md) | `PUT /article/{sku}/eans` | | [`routes/article/set-eans.md`](routes/article/set-eans.md) | `PUT /article/{sku}/eans` |
| [`routes/article/_sku/father.md`](routes/article/_sku/father.md) | `POST /article/{sku}/father` | | [`routes/article/_sku/father.md`](routes/article/_sku/father.md) | `POST /article/{sku}/father` |

View File

@@ -14615,8 +14615,8 @@
} }
}, },
"post": { "post": {
"summary": "Add a child article to a father article", "summary": "Create and attach a new child article to a father article",
"description": "Automatically generates SKU as fatherSku-nextFreeIndex (or accepts custom sku) and assigns the variant attribute.", "description": "Automatically generates SKU as fatherSku-nextFreeIndex and name from father name + value. Rejects a caller-supplied sku.",
"tags": [ "tags": [
"Article" "Article"
], ],
@@ -14654,10 +14654,6 @@
"name": { "name": {
"type": "string", "type": "string",
"description": "Optional custom variant name" "description": "Optional custom variant name"
},
"sku": {
"type": "string",
"description": "Optional custom SKU for the child article"
} }
} }
} }
@@ -14681,7 +14677,93 @@
} }
}, },
"400": { "400": {
"description": "Father not configured as father, missing value, duplicate variant, or child SKU already exists" "description": "Father not configured as father, missing value, duplicate variant, or sku was provided in body"
}
}
},
"put": {
"summary": "Attach an existing article as a child to a father article",
"description": "Attaches an existing article to this father article. Idempotent if already a child. Returns 409 if the article already belongs to another father.",
"tags": [
"Article"
],
"security": [
{
"cookieAuth": []
}
],
"parameters": [
{
"in": "path",
"name": "sku",
"required": true,
"schema": {
"type": "string"
},
"description": "Father article SKU"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"sku"
],
"properties": {
"sku": {
"type": "string",
"description": "SKU of the existing article to attach as child"
},
"attributeValueId": {
"type": "integer",
"description": "ID of existing attribute value for the father's attribute"
},
"value": {
"type": "string",
"description": "String value to find or create under the father's attribute"
},
"reparent": {
"type": "boolean",
"description": "Whether to allow re-parenting if child already belongs to another father"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Attached child article and list of children",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"article": {
"$ref": "#/components/schemas/Article"
},
"children": {
"type": "array",
"items": {
"type": "object"
}
}
}
}
}
}
},
"400": {
"description": "Missing sku or attribute value, or invalid input"
},
"404": {
"description": "Father or child article not found"
},
"409": {
"description": "Article already belongs to another father, or variant value already exists"
} }
} }
} }

View File

@@ -1,6 +1,6 @@
# Dash API Routes Overview (All) # Dash API Routes Overview (All)
Overview of all 180 API endpoints across all modules. Overview of all 181 API endpoints across all modules.
### Parameter Notation ### Parameter Notation
@@ -24,7 +24,8 @@ Overview of all 180 API endpoints across all modules.
| `PUT` | `/article/{sku}/brand` | `sku:string, body: { brandId: integer }` | `200 { article: { id, sku, name, slug, track_stock, unitamount, weight, unit, brand, categoryIds: integer[], photos: { sort, fullHash, fullW, fullH, fullSize, thumbW, thumbH, thumbHash }[], eans: string[], prices: { minmultiplier, net, taxRate, customergroupId }[], suppliers: { id, articleId, entityId, supplierName, sku, pu, stock, sort, priceId, price }[] } }` | Set or clear article brand by SKU | | `PUT` | `/article/{sku}/brand` | `sku:string, body: { brandId: integer }` | `200 { article: { id, sku, name, slug, track_stock, unitamount, weight, unit, brand, categoryIds: integer[], photos: { sort, fullHash, fullW, fullH, fullSize, thumbW, thumbH, thumbHash }[], eans: string[], prices: { minmultiplier, net, taxRate, customergroupId }[], suppliers: { id, articleId, entityId, supplierName, sku, pu, stock, sort, priceId, price }[] } }` | Set or clear article brand by SKU |
| `PUT` | `/article/{sku}/categories` | `sku:string, body: { categoryIds: integer[] }` | `200 { article: { id, sku, name, slug, track_stock, unitamount, weight, unit, brand, categoryIds: integer[], photos: { sort, fullHash, fullW, fullH, fullSize, thumbW, thumbH, thumbHash }[], eans: string[], prices: { minmultiplier, net, taxRate, customergroupId }[], suppliers: { id, articleId, entityId, supplierName, sku, pu, stock, sort, priceId, price }[] } }` | Replace article categories by SKU | | `PUT` | `/article/{sku}/categories` | `sku:string, body: { categoryIds: integer[] }` | `200 { article: { id, sku, name, slug, track_stock, unitamount, weight, unit, brand, categoryIds: integer[], photos: { sort, fullHash, fullW, fullH, fullSize, thumbW, thumbH, thumbHash }[], eans: string[], prices: { minmultiplier, net, taxRate, customergroupId }[], suppliers: { id, articleId, entityId, supplierName, sku, pu, stock, sort, priceId, price }[] } }` | Replace article categories by SKU |
| `GET` | `/article/{sku}/children` | `sku:string` | `200 { children: { id, sku, name, fathersort, attributeValueId, attributeValue }[] }` | List child articles of a father article | | `GET` | `/article/{sku}/children` | `sku:string` | `200 { children: { id, sku, name, fathersort, attributeValueId, attributeValue }[] }` | List child articles of a father article |
| `POST` | `/article/{sku}/children` | `sku:string, body: { attributeValueId?, value?, name?, sku?: string }` | `201 { article: { id, categoryId, name, sku, track_stock } }` | Add a child article to a father article | | `POST` | `/article/{sku}/children` | `sku:string, body: { attributeValueId?, value?, name? }` | `201 { article: { id, categoryId, name, sku, track_stock } }` | Create and attach a new child article to a father article |
| `PUT` | `/article/{sku}/children` | `sku:string, body: { sku: string, attributeValueId?, value?, reparent? }` | `200 { article, children: object[] }` | Attach an existing article as a child to a father article |
| `DELETE` | `/article/{sku}/children/{childSku}` | `sku:string, childSku:string, query: deleteArticle?:boolean` | `200 { success, deleted }` | Remove a child article from a father article | | `DELETE` | `/article/{sku}/children/{childSku}` | `sku:string, childSku:string, query: deleteArticle?:boolean` | `200 { success, deleted }` | Remove a child article from a father article |
| `PUT` | `/article/{sku}/eans` | `sku:string, body: { eans: string[] }` | `200 { article: { id, sku, name, slug, track_stock, unitamount, weight, unit, brand, categoryIds: integer[], photos: { sort, fullHash, fullW, fullH, fullSize, thumbW, thumbH, thumbHash }[], eans: string[], prices: { minmultiplier, net, taxRate, customergroupId }[], suppliers: { id, articleId, entityId, supplierName, sku, pu, stock, sort, priceId, price }[] } }` | Replace article EANs by SKU | | `PUT` | `/article/{sku}/eans` | `sku:string, body: { eans: string[] }` | `200 { article: { id, sku, name, slug, track_stock, unitamount, weight, unit, brand, categoryIds: integer[], photos: { sort, fullHash, fullW, fullH, fullSize, thumbW, thumbH, thumbHash }[], eans: string[], prices: { minmultiplier, net, taxRate, customergroupId }[], suppliers: { id, articleId, entityId, supplierName, sku, pu, stock, sort, priceId, price }[] } }` | Replace article EANs by SKU |
| `POST` | `/article/{sku}/father` | `sku:string, body: { isFather, attributeNameId? }` | `200 { article: { id, categoryId, name, sku, track_stock } }` | Toggle father mode for an article | | `POST` | `/article/{sku}/father` | `sku:string, body: { isFather, attributeNameId? }` | `200 { article: { id, categoryId, name, sku, track_stock } }` | Toggle father mode for an article |

View File

@@ -1,6 +1,6 @@
# Dash API Routes Overview # Dash API Routes Overview
Overview of all 144 API endpoints across all modules. Overview of all 145 API endpoints across all modules.
### Parameter Notation ### Parameter Notation
@@ -23,7 +23,8 @@ Overview of all 144 API endpoints across all modules.
| `PUT` | `/article/{sku}/brand` | `sku:string, body: { brandId: integer }` | `200 { article: { id, sku, name, slug, track_stock, unitamount, weight, unit, brand, categoryIds: integer[], photos: { sort, fullHash, fullW, fullH, fullSize, thumbW, thumbH, thumbHash }[], eans: string[], prices: { minmultiplier, net, taxRate, customergroupId }[], suppliers: { id, articleId, entityId, supplierName, sku, pu, stock, sort, priceId, price }[] } }` | Set or clear article brand by SKU | | `PUT` | `/article/{sku}/brand` | `sku:string, body: { brandId: integer }` | `200 { article: { id, sku, name, slug, track_stock, unitamount, weight, unit, brand, categoryIds: integer[], photos: { sort, fullHash, fullW, fullH, fullSize, thumbW, thumbH, thumbHash }[], eans: string[], prices: { minmultiplier, net, taxRate, customergroupId }[], suppliers: { id, articleId, entityId, supplierName, sku, pu, stock, sort, priceId, price }[] } }` | Set or clear article brand by SKU |
| `PUT` | `/article/{sku}/categories` | `sku:string, body: { categoryIds: integer[] }` | `200 { article: { id, sku, name, slug, track_stock, unitamount, weight, unit, brand, categoryIds: integer[], photos: { sort, fullHash, fullW, fullH, fullSize, thumbW, thumbH, thumbHash }[], eans: string[], prices: { minmultiplier, net, taxRate, customergroupId }[], suppliers: { id, articleId, entityId, supplierName, sku, pu, stock, sort, priceId, price }[] } }` | Replace article categories by SKU | | `PUT` | `/article/{sku}/categories` | `sku:string, body: { categoryIds: integer[] }` | `200 { article: { id, sku, name, slug, track_stock, unitamount, weight, unit, brand, categoryIds: integer[], photos: { sort, fullHash, fullW, fullH, fullSize, thumbW, thumbH, thumbHash }[], eans: string[], prices: { minmultiplier, net, taxRate, customergroupId }[], suppliers: { id, articleId, entityId, supplierName, sku, pu, stock, sort, priceId, price }[] } }` | Replace article categories by SKU |
| `GET` | `/article/{sku}/children` | `sku:string` | `200 { children: { id, sku, name, fathersort, attributeValueId, attributeValue }[] }` | List child articles of a father article | | `GET` | `/article/{sku}/children` | `sku:string` | `200 { children: { id, sku, name, fathersort, attributeValueId, attributeValue }[] }` | List child articles of a father article |
| `POST` | `/article/{sku}/children` | `sku:string, body: { attributeValueId?, value?, name?, sku?: string }` | `201 { article: { id, categoryId, name, sku, track_stock } }` | Add a child article to a father article | | `POST` | `/article/{sku}/children` | `sku:string, body: { attributeValueId?, value?, name? }` | `201 { article: { id, categoryId, name, sku, track_stock } }` | Create and attach a new child article to a father article |
| `PUT` | `/article/{sku}/children` | `sku:string, body: { sku: string, attributeValueId?, value?, reparent? }` | `200 { article, children: object[] }` | Attach an existing article as a child to a father article |
| `DELETE` | `/article/{sku}/children/{childSku}` | `sku:string, childSku:string, query: deleteArticle?:boolean` | `200 { success, deleted }` | Remove a child article from a father article | | `DELETE` | `/article/{sku}/children/{childSku}` | `sku:string, childSku:string, query: deleteArticle?:boolean` | `200 { success, deleted }` | Remove a child article from a father article |
| `PUT` | `/article/{sku}/eans` | `sku:string, body: { eans: string[] }` | `200 { article: { id, sku, name, slug, track_stock, unitamount, weight, unit, brand, categoryIds: integer[], photos: { sort, fullHash, fullW, fullH, fullSize, thumbW, thumbH, thumbHash }[], eans: string[], prices: { minmultiplier, net, taxRate, customergroupId }[], suppliers: { id, articleId, entityId, supplierName, sku, pu, stock, sort, priceId, price }[] } }` | Replace article EANs by SKU | | `PUT` | `/article/{sku}/eans` | `sku:string, body: { eans: string[] }` | `200 { article: { id, sku, name, slug, track_stock, unitamount, weight, unit, brand, categoryIds: integer[], photos: { sort, fullHash, fullW, fullH, fullSize, thumbW, thumbH, thumbHash }[], eans: string[], prices: { minmultiplier, net, taxRate, customergroupId }[], suppliers: { id, articleId, entityId, supplierName, sku, pu, stock, sort, priceId, price }[] } }` | Replace article EANs by SKU |
| `POST` | `/article/{sku}/father` | `sku:string, body: { isFather, attributeNameId? }` | `200 { article: { id, categoryId, name, sku, track_stock } }` | Toggle father mode for an article | | `POST` | `/article/{sku}/father` | `sku:string, body: { isFather, attributeNameId? }` | `200 { article: { id, categoryId, name, sku, track_stock } }` | Toggle father mode for an article |

View File

@@ -2,9 +2,9 @@
Protected. Requires a valid session cookie (`credentials: 'include'`). Protected. Requires a valid session cookie (`credentials: 'include'`).
Add a child article to a father article Create and attach a new child article to a father article
Automatically generates SKU as fatherSku-nextFreeIndex (or accepts custom sku) and assigns the variant attribute. Automatically generates SKU as fatherSku-nextFreeIndex and name from father name + value. Rejects a caller-supplied sku.
## Path parameters ## Path parameters
@@ -19,14 +19,12 @@ Automatically generates SKU as fatherSku-nextFreeIndex (or accepts custom sku) a
| `attributeValueId` | no | integer | ID of existing attribute value for the father's attribute | | `attributeValueId` | no | integer | ID of existing attribute value for the father's attribute |
| `value` | no | string | String value to find or create under the father's attribute | | `value` | no | string | String value to find or create under the father's attribute |
| `name` | no | string | Optional custom variant name | | `name` | no | string | Optional custom variant name |
| `sku` | no | string | Optional custom SKU for the child article |
```json ```json
{ {
"attributeValueId": 1, "attributeValueId": 1,
"value": "…", "value": "…",
"name": "…", "name": "…"
"sku": "…"
} }
``` ```
@@ -50,7 +48,7 @@ Created child article
| Status | Description | | Status | Description |
|--------|-------------| |--------|-------------|
| 400 | Father not configured as father, missing value, duplicate variant, or child SKU already exists | | 400 | Father not configured as father, missing value, duplicate variant, or sku was provided in body |
--- ---

View File

@@ -0,0 +1,62 @@
# `PUT /article/{sku}/children`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Attach an existing article as a child to a father article
Attaches an existing article to this father article. Idempotent if already a child. Returns 409 if the article already belongs to another father.
## Path parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `sku` | yes | string | Father article SKU |
## Body (JSON)
| Field | Required | Type | Description |
|-------|----------|------|-------------|
| `sku` | yes | string | SKU of the existing article to attach as child |
| `attributeValueId` | no | integer | ID of existing attribute value for the father's attribute |
| `value` | no | string | String value to find or create under the father's attribute |
| `reparent` | no | boolean | Whether to allow re-parenting if child already belongs to another father |
```json
{
"sku": "…",
"attributeValueId": 1,
"value": "…",
"reparent": true
}
```
## Response `200`
Attached child article and list of children
```json
{
"article": {
"id": 1,
"categoryId": 1,
"name": "…",
"sku": "…",
"track_stock": true
},
"children": [
{}
]
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | Missing sku or attribute value, or invalid input |
| 404 | Father or child article not found |
| 409 | Article already belongs to another father, or variant value already exists |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -17,6 +17,7 @@ function attributeErrorStatus(error) {
if (error.message === 'Name required') return 400; if (error.message === 'Name required') return 400;
if (error.message === 'Invalid attribute name order') return 400; if (error.message === 'Invalid attribute name order') return 400;
if (error.message === 'Invalid id') return 400; if (error.message === 'Invalid id') return 400;
if (error.message === 'Attribute name already exists') return 409;
if (error.message === 'Attribute name in use' || error.message?.includes('FOREIGN KEY constraint')) return 409; if (error.message === 'Attribute name in use' || error.message?.includes('FOREIGN KEY constraint')) return 409;
return 500; return 500;
} }

View File

@@ -16,6 +16,7 @@ function attributeValueErrorStatus(error) {
if (error.message === 'Value required') return 400; if (error.message === 'Value required') return 400;
if (error.message === 'Invalid attribute value order') return 400; if (error.message === 'Invalid attribute value order') return 400;
if (error.message === 'Invalid id' || error.message === 'Invalid attributeId') return 400; if (error.message === 'Invalid id' || error.message === 'Invalid attributeId') return 400;
if (error.message === 'Attribute value already exists') return 409;
if (error.message === 'Attribute value in use' || error.message?.includes('FOREIGN KEY constraint')) return 409; if (error.message === 'Attribute value in use' || error.message?.includes('FOREIGN KEY constraint')) return 409;
return 500; return 500;
} }

View File

@@ -17,6 +17,7 @@ function propertyErrorStatus(error) {
if (error.message === 'Name required') return 400; if (error.message === 'Name required') return 400;
if (error.message === 'Invalid property name order') return 400; if (error.message === 'Invalid property name order') return 400;
if (error.message === 'Invalid id') return 400; if (error.message === 'Invalid id') return 400;
if (error.message === 'Property name already exists') return 409;
if (error.message === 'Property name in use' || error.message?.startsWith('Cannot set property exclusive') || error.message?.includes('FOREIGN KEY constraint')) return 409; if (error.message === 'Property name in use' || error.message?.startsWith('Cannot set property exclusive') || error.message?.includes('FOREIGN KEY constraint')) return 409;
return 500; return 500;
} }