This commit is contained in:
seb
2026-08-02 19:04:56 +02:00
parent bfb1d8762a
commit aed0857f23
21 changed files with 1953 additions and 0 deletions

View File

@@ -34,17 +34,25 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route
| [`routes/article/delete.md`](routes/article/delete.md) | `DELETE /article/{sku}` |
| [`routes/article/get.md`](routes/article/get.md) | `GET /article/{sku}` |
| [`routes/article/update.md`](routes/article/update.md) | `PATCH /article/{sku}` |
| [`routes/article/set-brand.md`](routes/article/set-brand.md) | `PUT /article/{sku}/brand` |
| [`routes/article/set-categories.md`](routes/article/set-categories.md) | `PUT /article/{sku}/categories` |
| [`routes/article/set-eans.md`](routes/article/set-eans.md) | `PUT /article/{sku}/eans` |
| [`routes/article/add-image.md`](routes/article/add-image.md) | `POST /article/{sku}/image` |
| [`routes/article/delete-image.md`](routes/article/delete-image.md) | `DELETE /article/{sku}/image/{fullHash}` |
| [`routes/article/order-images.md`](routes/article/order-images.md) | `PUT /article/{sku}/images/order` |
| [`routes/article/set-prices.md`](routes/article/set-prices.md) | `PUT /article/{sku}/prices` |
| [`routes/article/set-weight.md`](routes/article/set-weight.md) | `PUT /article/{sku}/weight` |
| [`routes/auth/login.md`](routes/auth/login.md) | `POST /auth/login` |
| [`routes/auth/logout.md`](routes/auth/logout.md) | `POST /auth/logout` |
| [`routes/auth/me.md`](routes/auth/me.md) | `GET /auth/me` |
| [`routes/auth/selected-company.md`](routes/auth/selected-company.md) | `PUT /auth/selected-company` |
| [`routes/brand/list.md`](routes/brand/list.md) | `GET /brand` |
| [`routes/brand/upsert.md`](routes/brand/upsert.md) | `PUT /brand` |
| [`routes/brand/delete.md`](routes/brand/delete.md) | `DELETE /brand/{id}` |
| [`routes/category/create.md`](routes/category/create.md) | `POST /category` |
| [`routes/category/tree.md`](routes/category/tree.md) | `GET /category/tree` |
| [`routes/category/delete.md`](routes/category/delete.md) | `DELETE /category/{id}` |
| [`routes/category/rename.md`](routes/category/rename.md) | `PATCH /category/{id}` |
| [`routes/category/articles.md`](routes/category/articles.md) | `GET /category/{id}/articles` |
| [`routes/company/list.md`](routes/company/list.md) | `GET /company` |
| [`routes/company/create.md`](routes/company/create.md) | `POST /company` |

View File

@@ -36,6 +36,10 @@
"name": "Unit",
"description": "Company-scoped units of measure"
},
{
"name": "Brand",
"description": "Company-scoped brands"
},
{
"name": "Image",
"description": "Content-addressed AVIF by fullHash"
@@ -1132,6 +1136,124 @@
}
},
"/category/{id}": {
"patch": {
"tags": [
"Category"
],
"summary": "Rename category",
"description": "Renames a category the user is authorized for (updates name and re-slugifies in the company default language).",
"security": [
{
"cookieAuth": []
}
],
"x-markdown": "category/rename.md",
"parameters": [
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "integer"
},
"description": "Category id"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string"
}
},
"example": {
"name": "Renamed category"
}
}
}
}
},
"responses": {
"200": {
"description": "Renamed category",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"category": {
"type": "object",
"required": [
"id",
"name",
"slug"
],
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"slug": {
"type": "string"
}
}
}
}
}
}
}
},
"400": {
"description": "Validation error (name required)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Not authorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Category not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
},
"delete": {
"tags": [
"Category"
@@ -1192,6 +1314,256 @@
}
}
},
"/brand": {
"get": {
"tags": [
"Brand"
],
"summary": "List brands",
"description": "Lists brands for the authenticated user's selected company, sorted by name.",
"security": [
{
"cookieAuth": []
}
],
"x-markdown": "brand/list.md",
"responses": {
"200": {
"description": "Brand list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"brands": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Brand"
}
}
}
}
}
}
},
"400": {
"description": "No company selected",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Not authorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Company not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
},
"put": {
"tags": [
"Brand"
],
"summary": "Upsert brand",
"description": "Insert or update brand by `name` for the selected company. `gsprpinfo` optional.\n",
"security": [
{
"cookieAuth": []
}
],
"x-markdown": "brand/upsert.md",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string"
},
"gsprpinfo": {
"type": "string"
}
},
"example": {
"name": "Acme",
"gsprpinfo": "Acme GmbH"
}
}
}
}
},
"responses": {
"200": {
"description": "Upserted brand",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"brand": {
"$ref": "#/components/schemas/Brand"
}
}
}
}
}
},
"400": {
"description": "Invalid brand or no company selected",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Not authorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Company not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/brand/{id}": {
"delete": {
"tags": [
"Brand"
],
"summary": "Delete brand",
"description": "Deletes the brand for the selected company. Fails with `Brand in use`\nif any article still references it.\n",
"security": [
{
"cookieAuth": []
}
],
"x-markdown": "brand/delete.md",
"parameters": [
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "integer"
}
}
],
"responses": {
"204": {
"description": "Deleted"
},
"400": {
"description": "No company selected",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Not authorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Brand not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"409": {
"description": "Brand in use",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/article": {
"post": {
"tags": [
@@ -1971,6 +2343,430 @@
}
}
},
"/article/{sku}/eans": {
"put": {
"tags": [
"Article"
],
"summary": "Replace article EANs by SKU",
"description": "Replace-all EANs for the article. EANs must be non-empty unique strings;\nempty array clears all EANs. Returns the same shape as GET by SKU.\n",
"security": [
{
"cookieAuth": []
}
],
"x-markdown": "article/set-eans.md",
"parameters": [
{
"in": "path",
"name": "sku",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"eans"
],
"properties": {
"eans": {
"type": "array",
"items": {
"type": "string"
}
}
},
"example": {
"eans": [
"4001234567890"
]
}
}
}
}
},
"responses": {
"200": {
"description": "Updated article detail",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"article": {
"$ref": "#/components/schemas/ArticleDetail"
}
}
}
}
}
},
"400": {
"description": "Invalid eans",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Not authorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Article not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/article/{sku}/brand": {
"put": {
"tags": [
"Article"
],
"summary": "Set or clear article brand by SKU",
"description": "Sets the article's `brandId` (must belong to the article's company).\nPass `null` to clear the brand. Returns the same shape as GET by SKU.\n",
"security": [
{
"cookieAuth": []
}
],
"x-markdown": "article/set-brand.md",
"parameters": [
{
"in": "path",
"name": "sku",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"brandId"
],
"properties": {
"brandId": {
"type": "integer",
"nullable": true
}
},
"example": {
"brandId": 3
}
}
}
}
},
"responses": {
"200": {
"description": "Updated article detail",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"article": {
"$ref": "#/components/schemas/ArticleDetail"
}
}
}
}
}
},
"400": {
"description": "Validation error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Not authorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Article or brand not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/article/{sku}/weight": {
"put": {
"tags": [
"Article"
],
"summary": "Set article weight by SKU",
"description": "Sets the article's `weight`. Must be finite and `>= 0`.\nReturns the same shape as GET by SKU.\n",
"security": [
{
"cookieAuth": []
}
],
"x-markdown": "article/set-weight.md",
"parameters": [
{
"in": "path",
"name": "sku",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"weight"
],
"properties": {
"weight": {
"type": "number"
}
},
"example": {
"weight": 1.5
}
}
}
}
},
"responses": {
"200": {
"description": "Updated article detail",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"article": {
"$ref": "#/components/schemas/ArticleDetail"
}
}
}
}
}
},
"400": {
"description": "Invalid weight",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Not authorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Article not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/article/{sku}/categories": {
"put": {
"tags": [
"Article"
],
"summary": "Replace article categories by SKU",
"description": "Replace-all the article's category list. Each category must belong to\nthe article's company; ids must be unique. Empty array removes the\narticle from all categories. Returns the same shape as GET by SKU.\n",
"security": [
{
"cookieAuth": []
}
],
"x-markdown": "article/set-categories.md",
"parameters": [
{
"in": "path",
"name": "sku",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"categoryIds"
],
"properties": {
"categoryIds": {
"type": "array",
"items": {
"type": "integer"
}
}
},
"example": {
"categoryIds": [
4,
7
]
}
}
}
}
},
"responses": {
"200": {
"description": "Updated article detail",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"article": {
"$ref": "#/components/schemas/ArticleDetail"
}
}
}
}
}
},
"400": {
"description": "Invalid categoryIds",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Not authorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Article not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/auth/selected-company": {
"put": {
"tags": [
@@ -2365,6 +3161,25 @@
"description": "Full article payload (unit, brand, categoryIds, photos, eans, prices, …)",
"additionalProperties": true
},
"Brand": {
"type": "object",
"required": [
"id",
"name"
],
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"gsprpinfo": {
"type": "string",
"nullable": true
}
}
},
"ArticleImage": {
"type": "object",
"required": [

View File

@@ -0,0 +1,46 @@
# `PUT /article/{sku}/brand`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Set or clear article brand by SKU
Sets the article's `brandId` (must belong to the article's company).
Pass `null` to clear the brand. Returns the same shape as GET by SKU.
## Path parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `sku` | yes | string | |
## Body (JSON)
```json
{
"brandId": 3
}
```
## Response `200`
Updated article detail
```json
{
"article": { … }
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | Validation error — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized — `{"error":"…"}` |
| 404 | Article or brand not found — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,50 @@
# `PUT /article/{sku}/categories`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Replace article categories by SKU
Replace-all the article's category list. Each category must belong to
the article's company; ids must be unique. Empty array removes the
article from all categories. Returns the same shape as GET by SKU.
## Path parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `sku` | yes | string | |
## Body (JSON)
```json
{
"categoryIds": [
4,
7
]
}
```
## Response `200`
Updated article detail
```json
{
"article": { … }
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | Invalid categoryIds — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized — `{"error":"…"}` |
| 404 | Article not found — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,48 @@
# `PUT /article/{sku}/eans`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Replace article EANs by SKU
Replace-all EANs for the article. EANs must be non-empty unique strings;
empty array clears all EANs. Returns the same shape as GET by SKU.
## Path parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `sku` | yes | string | |
## Body (JSON)
```json
{
"eans": [
"4001234567890"
]
}
```
## Response `200`
Updated article detail
```json
{
"article": { … }
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | Invalid eans — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized — `{"error":"…"}` |
| 404 | Article not found — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,46 @@
# `PUT /article/{sku}/weight`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Set article weight by SKU
Sets the article's `weight`. Must be finite and `>= 0`.
Returns the same shape as GET by SKU.
## Path parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `sku` | yes | string | |
## Body (JSON)
```json
{
"weight": 1.5
}
```
## Response `200`
Updated article detail
```json
{
"article": { … }
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | Invalid weight — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized — `{"error":"…"}` |
| 404 | Article not found — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,35 @@
# `DELETE /brand/{id}`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Delete brand
Deletes the brand for the selected company. Fails with `Brand in use`
if any article still references it.
## Path parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `id` | yes | integer | |
## Response `204`
Deleted
_No content_
## Errors
| Status | Description |
|--------|-------------|
| 400 | No company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized — `{"error":"…"}` |
| 404 | Brand not found — `{"error":"…"}` |
| 409 | Brand in use — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,30 @@
# `GET /brand`
Protected. Requires a valid session cookie (`credentials: 'include'`).
List brands
Lists brands for the authenticated user's selected company, sorted by name.
## Response `200`
Brand list
```json
{
"brands": []
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | No company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized — `{"error":"…"}` |
| 404 | Company not found — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,40 @@
# `PUT /brand`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Upsert brand
Insert or update brand by `name` for the selected company. `gsprpinfo` optional.
## Body (JSON)
```json
{
"name": "Acme",
"gsprpinfo": "Acme GmbH"
}
```
## Response `200`
Upserted brand
```json
{
"brand": { … }
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | Invalid brand or no company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized — `{"error":"…"}` |
| 404 | Company not found — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,44 @@
# `PATCH /category/{id}`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Rename category
Renames a category the user is authorized for (updates name and re-slugifies in the company default language).
## Path parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `id` | yes | integer | Category id |
## Body (JSON)
```json
{
"name": "Renamed category"
}
```
## Response `200`
Renamed category
```json
{
"category": { … }
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | Validation error (name required) — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized — `{"error":"…"}` |
| 404 | Category not found — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._