u
This commit is contained in:
23
README.md
23
README.md
@@ -31,6 +31,29 @@ npm run docs:api
|
||||
|
||||
That writes `docs/api/openapi.json` and per-route Markdown under `docs/api/routes/`.
|
||||
|
||||
### Brands (company-scoped, `selectedCompanyId` aus der Session)
|
||||
|
||||
| Methode | Route | Store-Methode |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/api/brand` | `brand.list` |
|
||||
| `PUT` | `/api/brand` | `brand.upsert` |
|
||||
| `DELETE` | `/api/brand/:id` | `brand.delete` |
|
||||
|
||||
### Artikel-Felder (liefern dasselbe Shape wie `GET /api/article/:sku`)
|
||||
|
||||
| Methode | Route | Store-Methode |
|
||||
| --- | --- | --- |
|
||||
| `PUT` | `/api/article/:sku/eans` | `article.setEansBySku` |
|
||||
| `PUT` | `/api/article/:sku/brand` | `article.setBrandIdBySku` |
|
||||
| `PUT` | `/api/article/:sku/weight` | `article.setWeightBySku` |
|
||||
| `PUT` | `/api/article/:sku/categories` | `article.setCategoryIdsBySku` |
|
||||
|
||||
### Kategorien
|
||||
|
||||
| Methode | Route | Store-Methode |
|
||||
| --- | --- | --- |
|
||||
| `PATCH` | `/api/category/:id` | `category.rename` (Name + Slug in Firmensprache) |
|
||||
|
||||
## Users
|
||||
|
||||
Manage the email allowlist:
|
||||
|
||||
30
TODO.md
Normal file
30
TODO.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# TODO — nächster Schritt (Brands / Artikelfelder)
|
||||
|
||||
Geschwister: **dashdbschema** hat `libdashstore` um folgende Store-Methoden erweitert (alle authenticated, `googleAuthId` = eingeloggter User). Die Brand-Store-API existiert bereits. Das HTTP-Layer + OpenAPI-Doku hier in dashback ist **umgesetzt** — **dashfront** kann die UI bauen.
|
||||
|
||||
## Store-Methoden + HTTP-Routen (in dashback verdrahtet)
|
||||
|
||||
**`companyId` kommt immer aus der Auth-Session** (`req.user.selectedCompanyId`) — keine explizite `companyId` in Pfad/Query/Body, wie bei den Unit-Routen.
|
||||
|
||||
### `article` (alle liefern dasselbe Shape wie `getBySku`)
|
||||
| Store-Methode | HTTP-Route | Args |
|
||||
| --- | --- | --- |
|
||||
| `setEansBySku` | `PUT /api/article/:sku/eans` | `(googleAuthId, sku, string[])` — replace-all |
|
||||
| `setBrandIdBySku` | `PUT /api/article/:sku/brand` | `(googleAuthId, sku, brandId \| null)` |
|
||||
| `setWeightBySku` | `PUT /api/article/:sku/weight` | `(googleAuthId, sku, weight)` |
|
||||
| `setCategoryIdsBySku` | `PUT /api/article/:sku/categories` | `(googleAuthId, sku, number[])` — replace-all |
|
||||
| `getBySku` | `GET /api/article/:sku` | liefert `eans`, `categoryIds`, `weight`, `brand` bereits mit |
|
||||
|
||||
### `brand` (verdrahtet)
|
||||
| Store-Methode | HTTP-Route | Args |
|
||||
| --- | --- | --- |
|
||||
| `brand.list` | `GET /api/brand` | `(googleAuthId, selectedCompanyId)` |
|
||||
| `brand.upsert` | `PUT /api/brand` | `(googleAuthId, selectedCompanyId, { name, gsprpinfo? })` |
|
||||
| `brand.delete` | `DELETE /api/brand/:id` | `(googleAuthId, selectedCompanyId, id)` — Fehler `Brand in use`, wenn noch Artikel referenzieren |
|
||||
|
||||
## Umgesetzt
|
||||
1. Routen unter `docs/api/routes/` + OpenAPI via `@openapi` JSDoc angelegt (stil wie existing routes).
|
||||
2. Auth-Integration: `googleAuthId` aus Session bezogen (wie bestehende Routen).
|
||||
3. Company-Scope: `selectedCompanyId` aus Session; Methoden validieren selbst (`Company not found` / `Not authorized` / etc.).
|
||||
4. `npm run docs:api` regenerieren.
|
||||
5. `README.md` (dashback) mit neuen Endpoints ergänzen.
|
||||
@@ -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` |
|
||||
|
||||
@@ -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": [
|
||||
|
||||
46
docs/api/routes/article/set-brand.md
Normal file
46
docs/api/routes/article/set-brand.md
Normal 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`._
|
||||
50
docs/api/routes/article/set-categories.md
Normal file
50
docs/api/routes/article/set-categories.md
Normal 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`._
|
||||
48
docs/api/routes/article/set-eans.md
Normal file
48
docs/api/routes/article/set-eans.md
Normal 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`._
|
||||
46
docs/api/routes/article/set-weight.md
Normal file
46
docs/api/routes/article/set-weight.md
Normal 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`._
|
||||
35
docs/api/routes/brand/delete.md
Normal file
35
docs/api/routes/brand/delete.md
Normal 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`._
|
||||
30
docs/api/routes/brand/list.md
Normal file
30
docs/api/routes/brand/list.md
Normal 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`._
|
||||
40
docs/api/routes/brand/upsert.md
Normal file
40
docs/api/routes/brand/upsert.md
Normal 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`._
|
||||
44
docs/api/routes/category/rename.md
Normal file
44
docs/api/routes/category/rename.md
Normal 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`._
|
||||
@@ -34,6 +34,7 @@ const options = {
|
||||
{ name: 'Category', description: 'Category tree and membership' },
|
||||
{ name: 'Company', description: 'Company CRUD' },
|
||||
{ name: 'Unit', description: 'Company-scoped units of measure' },
|
||||
{ name: 'Brand', description: 'Company-scoped brands' },
|
||||
{ name: 'Image', description: 'Content-addressed AVIF by fullHash' },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -88,6 +88,17 @@
|
||||
* type: object
|
||||
* 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: [picId, sort, full, thumb, hash, thumbHash]
|
||||
|
||||
@@ -38,6 +38,10 @@ function articleErrorStatus(error) {
|
||||
if (error.message === 'POS customer group required') return 400;
|
||||
if (error.message === 'Invalid unitamount') return 400;
|
||||
if (error.message === 'Unit not found') return 404;
|
||||
if (error.message === 'Invalid eans') return 400;
|
||||
if (error.message === 'Brand not found') return 404;
|
||||
if (error.message === 'Invalid weight') return 400;
|
||||
if (error.message === 'Invalid categoryIds') return 400;
|
||||
return 500;
|
||||
}
|
||||
|
||||
@@ -724,4 +728,350 @@ export function register(app, basePath = '') {
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /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;
|
||||
* empty array clears all EANs. Returns the same shape as GET by SKU.
|
||||
* 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'
|
||||
*/
|
||||
app.put(`${basePath}/article/:sku/eans`, async (req, res) => {
|
||||
const skuParsed = parseSku(req.params.sku);
|
||||
if (skuParsed.error) {
|
||||
return res.status(400).json({ error: skuParsed.error });
|
||||
}
|
||||
|
||||
if (!Array.isArray(req.body?.eans)) {
|
||||
return res.status(400).json({ error: 'eans is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const article = await db.setEansBySku(req.user.id, skuParsed.sku, req.body.eans);
|
||||
res.status(200).json({ article });
|
||||
} catch (error) {
|
||||
const status = articleErrorStatus(error);
|
||||
if (status === 500) console.error('Set article eans failed:', error);
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /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).
|
||||
* Pass `null` to clear the brand. Returns the same shape as GET by SKU.
|
||||
* 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'
|
||||
*/
|
||||
app.put(`${basePath}/article/:sku/brand`, async (req, res) => {
|
||||
const skuParsed = parseSku(req.params.sku);
|
||||
if (skuParsed.error) {
|
||||
return res.status(400).json({ error: skuParsed.error });
|
||||
}
|
||||
|
||||
const brandId = req.body?.brandId ?? null;
|
||||
|
||||
try {
|
||||
const article = await db.setBrandIdBySku(req.user.id, skuParsed.sku, brandId);
|
||||
res.status(200).json({ article });
|
||||
} catch (error) {
|
||||
const status = articleErrorStatus(error);
|
||||
if (status === 500) console.error('Set article brand failed:', error);
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /article/{sku}/weight:
|
||||
* put:
|
||||
* tags: [Article]
|
||||
* summary: Set article weight by SKU
|
||||
* description: |
|
||||
* Sets the article's `weight`. Must be finite and `>= 0`.
|
||||
* Returns the same shape as GET by SKU.
|
||||
* 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'
|
||||
*/
|
||||
app.put(`${basePath}/article/:sku/weight`, async (req, res) => {
|
||||
const skuParsed = parseSku(req.params.sku);
|
||||
if (skuParsed.error) {
|
||||
return res.status(400).json({ error: skuParsed.error });
|
||||
}
|
||||
|
||||
const weight = req.body?.weight;
|
||||
|
||||
try {
|
||||
const article = await db.setWeightBySku(req.user.id, skuParsed.sku, weight);
|
||||
res.status(200).json({ article });
|
||||
} catch (error) {
|
||||
const status = articleErrorStatus(error);
|
||||
if (status === 500) console.error('Set article weight failed:', error);
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /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
|
||||
* the article's company; ids must be unique. Empty array removes the
|
||||
* article from all categories. Returns the same shape as GET by SKU.
|
||||
* 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'
|
||||
*/
|
||||
app.put(`${basePath}/article/:sku/categories`, async (req, res) => {
|
||||
const skuParsed = parseSku(req.params.sku);
|
||||
if (skuParsed.error) {
|
||||
return res.status(400).json({ error: skuParsed.error });
|
||||
}
|
||||
|
||||
if (!Array.isArray(req.body?.categoryIds)) {
|
||||
return res.status(400).json({ error: 'categoryIds is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const article = await db.setCategoryIdsBySku(
|
||||
req.user.id,
|
||||
skuParsed.sku,
|
||||
req.body.categoryIds,
|
||||
);
|
||||
res.status(200).json({ article });
|
||||
} catch (error) {
|
||||
const status = articleErrorStatus(error);
|
||||
if (status === 500) console.error('Set article categories failed:', error);
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
239
src/api/routes/brand.js
Normal file
239
src/api/routes/brand.js
Normal file
@@ -0,0 +1,239 @@
|
||||
import * as db from '../../db/brand.js';
|
||||
|
||||
function requireSelectedCompany(req) {
|
||||
const companyId = req.user.selectedCompanyId;
|
||||
if (companyId == null) {
|
||||
return { error: 'No company selected' };
|
||||
}
|
||||
return { companyId };
|
||||
}
|
||||
|
||||
function brandErrorStatus(error) {
|
||||
if (error.message === 'Not authorized') return 403;
|
||||
if (error.message === 'Company not found') return 404;
|
||||
if (error.message === 'Brand not found') return 404;
|
||||
if (error.message === 'Invalid brand') return 400;
|
||||
if (error.message === 'Brand in use') return 409;
|
||||
return 500;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers brand routes for the authenticated user's selected company.
|
||||
* @param {import('express').Application} app
|
||||
* @param {string} [basePath='']
|
||||
*/
|
||||
export function register(app, basePath = '') {
|
||||
/**
|
||||
* @openapi
|
||||
* /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.
|
||||
* 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'
|
||||
*/
|
||||
app.get(`${basePath}/brand`, async (req, res) => {
|
||||
const selected = requireSelectedCompany(req);
|
||||
if (selected.error) {
|
||||
return res.status(400).json({ error: selected.error });
|
||||
}
|
||||
|
||||
try {
|
||||
const brands = await db.list(req.user.id, selected.companyId);
|
||||
res.status(200).json({ brands });
|
||||
} catch (error) {
|
||||
const status = brandErrorStatus(error);
|
||||
if (status === 500) console.error('List brands failed:', error);
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.put(`${basePath}/brand`, async (req, res) => {
|
||||
const selected = requireSelectedCompany(req);
|
||||
if (selected.error) {
|
||||
return res.status(400).json({ error: selected.error });
|
||||
}
|
||||
|
||||
const { name, gsprpinfo } = req.body ?? {};
|
||||
|
||||
try {
|
||||
const brand = await db.upsert(req.user.id, selected.companyId, {
|
||||
name,
|
||||
gsprpinfo,
|
||||
});
|
||||
res.status(200).json({ brand });
|
||||
} catch (error) {
|
||||
const status = brandErrorStatus(error);
|
||||
if (status === 500) console.error('Upsert brand failed:', error);
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /brand/{id}:
|
||||
* delete:
|
||||
* tags: [Brand]
|
||||
* summary: Delete brand
|
||||
* description: |
|
||||
* Deletes the brand for the selected company. Fails with `Brand in use`
|
||||
* if any article still references it.
|
||||
* 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'
|
||||
*/
|
||||
app.delete(`${basePath}/brand/:id`, async (req, res) => {
|
||||
const selected = requireSelectedCompany(req);
|
||||
if (selected.error) {
|
||||
return res.status(400).json({ error: selected.error });
|
||||
}
|
||||
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id) || id <= 0) {
|
||||
return res.status(400).json({ error: 'id is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
await db.remove(req.user.id, selected.companyId, id);
|
||||
res.sendStatus(204);
|
||||
} catch (error) {
|
||||
const status = brandErrorStatus(error);
|
||||
if (status === 500) console.error('Delete brand failed:', error);
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -13,6 +13,8 @@ function categoryErrorStatus(error) {
|
||||
if (error.message === 'Company not found') return 404;
|
||||
if (error.message === 'Parent not found') return 404;
|
||||
if (error.message === 'Cannot delete root category') return 400;
|
||||
if (error.message === 'Name required') return 400;
|
||||
if (error.message === 'Category not found') return 404;
|
||||
return 500;
|
||||
}
|
||||
|
||||
@@ -246,6 +248,74 @@ export function register(app, basePath = '') {
|
||||
/**
|
||||
* @openapi
|
||||
* /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]
|
||||
* summary: Delete category
|
||||
@@ -300,4 +370,29 @@ export function register(app, basePath = '') {
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.patch(`${basePath}/category/:id`, async (req, res) => {
|
||||
const parsed = parsePositiveInt(req.params.id, 'id');
|
||||
if (parsed.error) {
|
||||
return res.status(400).json({ error: parsed.error });
|
||||
}
|
||||
|
||||
const name = typeof req.body?.name === 'string' ? req.body.name.trim() : '';
|
||||
if (!name) {
|
||||
return res.status(400).json({ error: 'name is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const category = await db.rename({
|
||||
googleAuthId: req.user.id,
|
||||
id: parsed.id,
|
||||
name,
|
||||
});
|
||||
res.status(200).json({ category });
|
||||
} catch (error) {
|
||||
const status = categoryErrorStatus(error);
|
||||
if (status === 500) console.error('Rename category failed:', error);
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -39,3 +39,23 @@ export async function setPricesBySku(googleAuthId, sku, prices) {
|
||||
const store = await getStore();
|
||||
return store.article.setPricesBySku(googleAuthId, sku, prices);
|
||||
}
|
||||
|
||||
export async function setEansBySku(googleAuthId, sku, eans) {
|
||||
const store = await getStore();
|
||||
return store.article.setEansBySku(googleAuthId, sku, eans);
|
||||
}
|
||||
|
||||
export async function setBrandIdBySku(googleAuthId, sku, brandId) {
|
||||
const store = await getStore();
|
||||
return store.article.setBrandIdBySku(googleAuthId, sku, brandId);
|
||||
}
|
||||
|
||||
export async function setWeightBySku(googleAuthId, sku, weight) {
|
||||
const store = await getStore();
|
||||
return store.article.setWeightBySku(googleAuthId, sku, weight);
|
||||
}
|
||||
|
||||
export async function setCategoryIdsBySku(googleAuthId, sku, categoryIds) {
|
||||
const store = await getStore();
|
||||
return store.article.setCategoryIdsBySku(googleAuthId, sku, categoryIds);
|
||||
}
|
||||
|
||||
16
src/db/brand.js
Normal file
16
src/db/brand.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { getStore } from './store.js';
|
||||
|
||||
export async function list(googleAuthId, companyId) {
|
||||
const store = await getStore();
|
||||
return store.brand.list(googleAuthId, companyId);
|
||||
}
|
||||
|
||||
export async function upsert(googleAuthId, companyId, data) {
|
||||
const store = await getStore();
|
||||
return store.brand.upsert(googleAuthId, companyId, data);
|
||||
}
|
||||
|
||||
export async function remove(googleAuthId, companyId, id) {
|
||||
const store = await getStore();
|
||||
return store.brand.delete(googleAuthId, companyId, id);
|
||||
}
|
||||
@@ -19,3 +19,8 @@ export async function getCategoryArticles(googleAuthId, categoryId) {
|
||||
const store = await getStore();
|
||||
return store.category.getCategoryArticles(googleAuthId, categoryId);
|
||||
}
|
||||
|
||||
export async function rename({ googleAuthId, id, name }) {
|
||||
const store = await getStore();
|
||||
return store.category.rename({ googleAuthId, id, name });
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ const STORE_NAMESPACES = [
|
||||
'article',
|
||||
'pic',
|
||||
'unit',
|
||||
'brand',
|
||||
'lang',
|
||||
'tax',
|
||||
'google_auth',
|
||||
|
||||
Reference in New Issue
Block a user