This commit is contained in:
seb
2026-10-04 11:25:17 +02:00
parent e5ff4ef307
commit 43fa8ea105
24 changed files with 1016 additions and 214 deletions

View File

@@ -100,6 +100,8 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route
| [`routes/category/_id/property/_propertyId.md`](routes/category/_id/property/_propertyId.md) | `DELETE /category/{id}/property/{propertyId}` | | [`routes/category/_id/property/_propertyId.md`](routes/category/_id/property/_propertyId.md) | `DELETE /category/{id}/property/{propertyId}` |
| [`routes/company/list.md`](routes/company/list.md) | `GET /company` | | [`routes/company/list.md`](routes/company/list.md) | `GET /company` |
| [`routes/company/create.md`](routes/company/create.md) | `POST /company` | | [`routes/company/create.md`](routes/company/create.md) | `POST /company` |
| [`routes/company/history.md`](routes/company/history.md) | `GET /company/history` |
| [`routes/company/addHistory.md`](routes/company/addHistory.md) | `POST /company/history` |
| [`routes/companysetting/list.md`](routes/companysetting/list.md) | `GET /company/setting` | | [`routes/companysetting/list.md`](routes/companysetting/list.md) | `GET /company/setting` |
| [`routes/companysetting/get.md`](routes/companysetting/get.md) | `GET /company/setting/{key}` | | [`routes/companysetting/get.md`](routes/companysetting/get.md) | `GET /company/setting/{key}` |
| [`routes/companysetting/set.md`](routes/companysetting/set.md) | `PUT /company/setting/{key}` | | [`routes/companysetting/set.md`](routes/companysetting/set.md) | `PUT /company/setting/{key}` |

View File

@@ -3030,6 +3030,66 @@
"responses": { "responses": {
"200": { "200": {
"description": "Deleted successfully" "description": "Deleted successfully"
},
"400": {
"description": "Bad request 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": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Property name not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"409": {
"description": "Property name in use",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Server error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
} }
} }
} }
@@ -9435,6 +9495,194 @@
} }
} }
}, },
"/company/history": {
"get": {
"tags": [
"Company"
],
"summary": "Get company history",
"description": "Returns `companyhistory` rows for the selected company, newest first (`date` DESC, `id` DESC).\n",
"security": [
{
"cookieAuth": []
}
],
"x-markdown": "company/history.md",
"parameters": [
{
"in": "query",
"name": "limit",
"required": false,
"schema": {
"type": "integer",
"minimum": 1,
"maximum": 1000
},
"description": "Maximum number of entries to return (newest first). Omit for all entries."
}
],
"responses": {
"200": {
"description": "Company history (newest first)",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"history",
"truncated"
],
"properties": {
"history": {
"type": "array",
"items": {
"$ref": "#/components/schemas/CompanyHistoryEntry"
}
},
"truncated": {
"type": "boolean",
"description": "True when a limit was applied and the result set was exactly that size (more entries may exist)."
}
}
}
}
}
},
"400": {
"description": "Invalid limit 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": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Server error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
},
"post": {
"tags": [
"Company"
],
"summary": "Add company history entry",
"description": "Appends an entry to `companyhistory` for the selected company.\n",
"security": [
{
"cookieAuth": []
}
],
"x-markdown": "company/addHistory.md",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"text"
],
"properties": {
"text": {
"type": "string",
"description": "History entry text or note"
}
}
}
}
}
},
"responses": {
"201": {
"description": "History entry created",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"history"
],
"properties": {
"history": {
"$ref": "#/components/schemas/CompanyHistoryEntry"
}
}
}
}
}
},
"400": {
"description": "Text required 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": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Server error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/company/{id}": { "/company/{id}": {
"put": { "put": {
"tags": [ "tags": [
@@ -9837,43 +10085,17 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"type": "object",
"required": [
"attributevalues"
],
"properties": {
"attributevalues": {
"type": "array", "type": "array",
"description": "Array of attribute value IDs (numbers) or objects with id or attributeValueId", "description": "Array of attribute value IDs. Array order defines sort.",
"items": { "items": {
"oneOf": [
{
"type": "integer",
"description": "Attribute value ID"
},
{
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "Attribute value ID"
},
"attributeValueId": {
"type": "integer",
"description": "Attribute value ID"
},
"sort": {
"type": "integer" "type": "integer"
} },
} "example": [
} 166
] ]
} }
} }
} }
}
}
}
}, },
"responses": { "responses": {
"200": { "200": {
@@ -11305,6 +11527,66 @@
"responses": { "responses": {
"200": { "200": {
"description": "Deleted successfully" "description": "Deleted successfully"
},
"400": {
"description": "Bad request 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": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Attribute value not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"409": {
"description": "Attribute value in use",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Server error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
} }
} }
} }
@@ -11521,6 +11803,66 @@
"responses": { "responses": {
"200": { "200": {
"description": "Deleted successfully" "description": "Deleted successfully"
},
"400": {
"description": "Bad request 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": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Attribute name not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"409": {
"description": "Attribute name in use",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Server error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
} }
} }
} }
@@ -12339,43 +12681,17 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"type": "object",
"required": [
"attributevalues"
],
"properties": {
"attributevalues": {
"type": "array", "type": "array",
"description": "Array of attribute value IDs (numbers) or objects with id or attributeValueId", "description": "Array of attribute value IDs. Array order defines sort.",
"items": { "items": {
"oneOf": [
{
"type": "integer",
"description": "Attribute value ID"
},
{
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "Attribute value ID"
},
"attributeValueId": {
"type": "integer",
"description": "Attribute value ID"
},
"sort": {
"type": "integer" "type": "integer"
} },
} "example": [
} 166
] ]
} }
} }
} }
}
}
}
}, },
"responses": { "responses": {
"200": { "200": {
@@ -15711,6 +16027,30 @@
} }
} }
}, },
"CompanyHistoryEntry": {
"type": "object",
"required": [
"id",
"date",
"text"
],
"properties": {
"id": {
"type": "integer"
},
"companyuserId": {
"type": "integer",
"nullable": true
},
"date": {
"type": "string",
"format": "date-time"
},
"text": {
"type": "string"
}
}
},
"ArticleSupplierPrice": { "ArticleSupplierPrice": {
"type": "object", "type": "object",
"required": [ "required": [

View File

@@ -1,6 +1,6 @@
# Dash API Routes Overview (All) # Dash API Routes Overview (All)
Overview of all 178 API endpoints across all modules. Overview of all 180 API endpoints across all modules.
### Parameter Notation ### Parameter Notation
@@ -20,7 +20,7 @@ Overview of all 178 API endpoints across all modules.
| `GET` | `/article/{sku}/articlelist` | `sku:string` | `200 { items: { id, articleId, sku, name, count, sort }[] }` | Get Stückliste (bundle items) for an article | | `GET` | `/article/{sku}/articlelist` | `sku:string` | `200 { items: { id, articleId, sku, name, count, sort }[] }` | Get Stückliste (bundle items) for an article |
| `PUT` | `/article/{sku}/articlelist` | `sku:string, body: { items: { articleId?, sku?: string, count?, sort? }[] }` | `200 { items: object[] }` | Set Stückliste (bundle items) for an article | | `PUT` | `/article/{sku}/articlelist` | `sku:string, body: { items: { articleId?, sku?: string, count?, sort? }[] }` | `200 { items: object[] }` | Set Stückliste (bundle items) for an article |
| `GET` | `/article/{sku}/attributevalue` | `sku:string` | `200 { attributevalues: { id, articleId, attributeId, attributeName, exclusive, attributeValueId, value, sort }[] }` | List article attribute values | | `GET` | `/article/{sku}/attributevalue` | `sku:string` | `200 { attributevalues: { id, articleId, attributeId, attributeName, exclusive, attributeValueId, value, sort }[] }` | List article attribute values |
| `PUT` | `/article/{sku}/attributevalue` | `sku:string, body: { attributevalues: any[] }` | `200 { attributevalues: { id, articleId, attributeId, attributeName, exclusive, attributeValueId, value, sort }[] }` | Bulk set article attribute values | | `PUT` | `/article/{sku}/attributevalue` | `sku:string, body: integer[]` | `200 { attributevalues: { id, articleId, attributeId, attributeName, exclusive, attributeValueId, value, sort }[] }` | Bulk set article attribute values |
| `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 |
@@ -72,7 +72,7 @@ Overview of all 178 API endpoints across all modules.
| `PATCH` | `/category/{id}` | `id:integer, body: { name?, parentId? }` | `200 { category: { id, name, slug, parentId, sort } }` | Update category (rename or move) | | `PATCH` | `/category/{id}` | `id:integer, body: { name?, parentId? }` | `200 { category: { id, name, slug, parentId, sort } }` | Update category (rename or move) |
| `GET` | `/category/{id}/articles` | `id:integer` | `200 { articles: { id, sku, name, slug, net, taxRate, track_stock, optionsCount, articleListCount, childCount, fullHash, thumbW, thumbH, thumbHash, brand }[] }` | Articles in category | | `GET` | `/category/{id}/articles` | `id:integer` | `200 { articles: { id, sku, name, slug, net, taxRate, track_stock, optionsCount, articleListCount, childCount, fullHash, thumbW, thumbH, thumbHash, brand }[] }` | Articles in category |
| `GET` | `/category/{id}/attributevalue` | `id:integer` | `200 { attributevalues: { id, categoryId, attributeId, attributeName, exclusive, attributeValueId, value, sort }[] }` | List category attribute values | | `GET` | `/category/{id}/attributevalue` | `id:integer` | `200 { attributevalues: { id, categoryId, attributeId, attributeName, exclusive, attributeValueId, value, sort }[] }` | List category attribute values |
| `PUT` | `/category/{id}/attributevalue` | `id:integer, body: { attributevalues: any[] }` | `200 { attributevalues: { id, categoryId, attributeId, attributeName, exclusive, attributeValueId, value, sort }[] }` | Bulk set category attribute values | | `PUT` | `/category/{id}/attributevalue` | `id:integer, body: integer[]` | `200 { attributevalues: { id, categoryId, attributeId, attributeName, exclusive, attributeValueId, value, sort }[] }` | Bulk set category attribute values |
| `DELETE` | `/category/{id}/image` | `id:integer` | `200 { category: { id, thumbW, thumbH, thumbHash } }` | Delete category image | | `DELETE` | `/category/{id}/image` | `id:integer` | `200 { category: { id, thumbW, thumbH, thumbHash } }` | Delete category image |
| `POST` | `/category/{id}/image` | `id:integer, image/avif` | `201 { category: { id, thumbW, thumbH, thumbHash } }` | Add category image | | `POST` | `/category/{id}/image` | `id:integer, image/avif` | `201 { category: { id, thumbW, thumbH, thumbHash } }` | Add category image |
| `GET` | `/category/{id}/property` | `id:integer` | `200 { properties: { id, categoryId, propertyId, name, exclusive, value, sort }[] }` | List category properties | | `GET` | `/category/{id}/property` | `id:integer` | `200 { properties: { id, categoryId, propertyId, name, exclusive, value, sort }[] }` | List category properties |
@@ -80,6 +80,8 @@ Overview of all 178 API endpoints across all modules.
| `DELETE` | `/category/{id}/property/{propertyId}` | `id:integer, propertyId:integer` | `200` | Delete category property | | `DELETE` | `/category/{id}/property/{propertyId}` | `id:integer, propertyId:integer` | `200` | Delete category property |
| `GET` | `/company` | — | `200 { companies: { id, iso639, name, taxrate, reducedtaxrate }[] }` | List companies | | `GET` | `/company` | — | `200 { companies: { id, iso639, name, taxrate, reducedtaxrate }[] }` | List companies |
| `POST` | `/company` | `body: { iso639, name, taxrate, reducedtaxrate }` | `201 { company, user }` | Create company | | `POST` | `/company` | `body: { iso639, name, taxrate, reducedtaxrate }` | `201 { company, user }` | Create company |
| `GET` | `/company/history` | `query: limit?:integer` | `200 { history: { id, companyuserId, date, text }[], truncated }` | Get company history |
| `POST` | `/company/history` | `body: { text: string }` | `201 { history: { id, companyuserId, date, text } }` | Add company history entry |
| `GET` | `/company/setting` | — | `200 { settings: { key, value }[] }` | List company settings | | `GET` | `/company/setting` | — | `200 { settings: { key, value }[] }` | List company settings |
| `GET` | `/company/setting/{key}` | `key:string` | `200 { setting: { key, value } }` | Get company setting by key | | `GET` | `/company/setting/{key}` | `key:string` | `200 { setting: { key, value } }` | Get company setting by key |
| `PUT` | `/company/setting/{key}` | `key:string, body: { value, frontend? }` | `200 { setting: { key, value } }` | Upsert company setting | | `PUT` | `/company/setting/{key}` | `key:string, body: { value, frontend? }` | `200 { setting: { key, value } }` | Upsert company setting |

View File

@@ -1,6 +1,6 @@
# Dash API Routes Overview # Dash API Routes Overview
Overview of all 142 API endpoints across all modules. Overview of all 144 API endpoints across all modules.
### Parameter Notation ### Parameter Notation
@@ -19,7 +19,7 @@ Overview of all 142 API endpoints across all modules.
| `GET` | `/article/{sku}/articlelist` | `sku:string` | `200 { items: { id, articleId, sku, name, count, sort }[] }` | Get Stückliste (bundle items) for an article | | `GET` | `/article/{sku}/articlelist` | `sku:string` | `200 { items: { id, articleId, sku, name, count, sort }[] }` | Get Stückliste (bundle items) for an article |
| `PUT` | `/article/{sku}/articlelist` | `sku:string, body: { items: { articleId?, sku?: string, count?, sort? }[] }` | `200 { items: object[] }` | Set Stückliste (bundle items) for an article | | `PUT` | `/article/{sku}/articlelist` | `sku:string, body: { items: { articleId?, sku?: string, count?, sort? }[] }` | `200 { items: object[] }` | Set Stückliste (bundle items) for an article |
| `GET` | `/article/{sku}/attributevalue` | `sku:string` | `200 { attributevalues: { id, articleId, attributeId, attributeName, exclusive, attributeValueId, value, sort }[] }` | List article attribute values | | `GET` | `/article/{sku}/attributevalue` | `sku:string` | `200 { attributevalues: { id, articleId, attributeId, attributeName, exclusive, attributeValueId, value, sort }[] }` | List article attribute values |
| `PUT` | `/article/{sku}/attributevalue` | `sku:string, body: { attributevalues: any[] }` | `200 { attributevalues: { id, articleId, attributeId, attributeName, exclusive, attributeValueId, value, sort }[] }` | Bulk set article attribute values | | `PUT` | `/article/{sku}/attributevalue` | `sku:string, body: integer[]` | `200 { attributevalues: { id, articleId, attributeId, attributeName, exclusive, attributeValueId, value, sort }[] }` | Bulk set article attribute values |
| `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 |
@@ -67,12 +67,14 @@ Overview of all 142 API endpoints across all modules.
| `PATCH` | `/category/{id}` | `id:integer, body: { name?, parentId? }` | `200 { category: { id, name, slug, parentId, sort } }` | Update category (rename or move) | | `PATCH` | `/category/{id}` | `id:integer, body: { name?, parentId? }` | `200 { category: { id, name, slug, parentId, sort } }` | Update category (rename or move) |
| `GET` | `/category/{id}/articles` | `id:integer` | `200 { articles: { id, sku, name, slug, net, taxRate, track_stock, optionsCount, articleListCount, childCount, fullHash, thumbW, thumbH, thumbHash, brand }[] }` | Articles in category | | `GET` | `/category/{id}/articles` | `id:integer` | `200 { articles: { id, sku, name, slug, net, taxRate, track_stock, optionsCount, articleListCount, childCount, fullHash, thumbW, thumbH, thumbHash, brand }[] }` | Articles in category |
| `GET` | `/category/{id}/attributevalue` | `id:integer` | `200 { attributevalues: { id, categoryId, attributeId, attributeName, exclusive, attributeValueId, value, sort }[] }` | List category attribute values | | `GET` | `/category/{id}/attributevalue` | `id:integer` | `200 { attributevalues: { id, categoryId, attributeId, attributeName, exclusive, attributeValueId, value, sort }[] }` | List category attribute values |
| `PUT` | `/category/{id}/attributevalue` | `id:integer, body: { attributevalues: any[] }` | `200 { attributevalues: { id, categoryId, attributeId, attributeName, exclusive, attributeValueId, value, sort }[] }` | Bulk set category attribute values | | `PUT` | `/category/{id}/attributevalue` | `id:integer, body: integer[]` | `200 { attributevalues: { id, categoryId, attributeId, attributeName, exclusive, attributeValueId, value, sort }[] }` | Bulk set category attribute values |
| `DELETE` | `/category/{id}/image` | `id:integer` | `200 { category: { id, thumbW, thumbH, thumbHash } }` | Delete category image | | `DELETE` | `/category/{id}/image` | `id:integer` | `200 { category: { id, thumbW, thumbH, thumbHash } }` | Delete category image |
| `GET` | `/category/{id}/property` | `id:integer` | `200 { properties: { id, categoryId, propertyId, name, exclusive, value, sort }[] }` | List category properties | | `GET` | `/category/{id}/property` | `id:integer` | `200 { properties: { id, categoryId, propertyId, name, exclusive, value, sort }[] }` | List category properties |
| `PUT` | `/category/{id}/property` | `id:integer, body: { properties: { propertyId, value, sort? }[] }` | `200 { properties: { id, categoryId, propertyId, name, exclusive, value, sort }[] }` | Bulk set category properties | | `PUT` | `/category/{id}/property` | `id:integer, body: { properties: { propertyId, value, sort? }[] }` | `200 { properties: { id, categoryId, propertyId, name, exclusive, value, sort }[] }` | Bulk set category properties |
| `DELETE` | `/category/{id}/property/{propertyId}` | `id:integer, propertyId:integer` | `200` | Delete category property | | `DELETE` | `/category/{id}/property/{propertyId}` | `id:integer, propertyId:integer` | `200` | Delete category property |
| `GET` | `/company` | — | `200 { companies: { id, iso639, name, taxrate, reducedtaxrate }[] }` | List companies | | `GET` | `/company` | — | `200 { companies: { id, iso639, name, taxrate, reducedtaxrate }[] }` | List companies |
| `GET` | `/company/history` | `query: limit?:integer` | `200 { history: { id, companyuserId, date, text }[], truncated }` | Get company history |
| `POST` | `/company/history` | `body: { text: string }` | `201 { history: { id, companyuserId, date, text } }` | Add company history entry |
| `GET` | `/company/setting` | — | `200 { settings: { key, value }[] }` | List company settings | | `GET` | `/company/setting` | — | `200 { settings: { key, value }[] }` | List company settings |
| `GET` | `/company/setting/{key}` | `key:string` | `200 { setting: { key, value } }` | Get company setting by key | | `GET` | `/company/setting/{key}` | `key:string` | `200 { setting: { key, value } }` | Get company setting by key |
| `PUT` | `/company/setting/{key}` | `key:string, body: { value, frontend? }` | `200 { setting: { key, value } }` | Upsert company setting | | `PUT` | `/company/setting/{key}` | `key:string, body: { value, frontend? }` | `200 { setting: { key, value } }` | Upsert company setting |

View File

@@ -14,16 +14,10 @@ Replaces all attribute values for an article. Enforces exclusive constraint (max
## Body (JSON) ## Body (JSON)
| Field | Required | Type | Description |
|-------|----------|------|-------------|
| `attributevalues` | yes | any[] | Array of attribute value IDs (numbers) or objects with id or attributeValueId |
```json ```json
{ [
"attributevalues": [ 166
"…"
] ]
}
``` ```
## Response `200` ## Response `200`

View File

@@ -16,6 +16,17 @@ Deletes an attribute name and cascades to attribute values and article assignmen
Deleted successfully Deleted successfully
## Errors
| Status | Description |
|--------|-------------|
| 400 | Bad request or no company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Forbidden — `{"error":"…"}` |
| 404 | Attribute name not found — `{"error":"…"}` |
| 409 | Attribute name in use — `{"error":"…"}` |
| 500 | Server error — `{"error":"…"}` |
--- ---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ _Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -16,6 +16,17 @@ Deletes an attribute value and cascades to article assignments.
Deleted successfully Deleted successfully
## Errors
| Status | Description |
|--------|-------------|
| 400 | Bad request or no company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Forbidden — `{"error":"…"}` |
| 404 | Attribute value not found — `{"error":"…"}` |
| 409 | Attribute value in use — `{"error":"…"}` |
| 500 | Server error — `{"error":"…"}` |
--- ---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ _Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -14,16 +14,10 @@ Replaces all attribute values for a category. Enforces exclusive constraint (max
## Body (JSON) ## Body (JSON)
| Field | Required | Type | Description |
|-------|----------|------|-------------|
| `attributevalues` | yes | any[] | Array of attribute value IDs (numbers) or objects with id or attributeValueId |
```json ```json
{ [
"attributevalues": [ 166
"…"
] ]
}
``` ```
## Response `200` ## Response `200`

View File

@@ -0,0 +1,48 @@
# `POST /company/history`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Add company history entry
Appends an entry to `companyhistory` for the selected company.
## Body (JSON)
| Field | Required | Type | Description |
|-------|----------|------|-------------|
| `text` | yes | string | History entry text or note |
```json
{
"text": "…"
}
```
## Response `201`
History entry created
```json
{
"history": {
"id": 1,
"companyuserId": 1,
"date": "2026-08-10T00:00:00.000Z",
"text": "…"
}
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | Text required or no company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Forbidden — `{"error":"…"}` |
| 500 | Server error — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,45 @@
# `GET /company/history`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Get company history
Returns `companyhistory` rows for the selected company, newest first (`date` DESC, `id` DESC).
## Query parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `limit` | no | integer | Maximum number of entries to return (newest first). Omit for all entries. |
## Response `200`
Company history (newest first)
```json
{
"history": [
{
"id": 1,
"companyuserId": 1,
"date": "2026-08-10T00:00:00.000Z",
"text": "…"
}
],
"truncated": true
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | Invalid limit or no company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Forbidden — `{"error":"…"}` |
| 500 | Server error — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -16,6 +16,17 @@ Deletes a property name and cascades to article properties.
Deleted successfully Deleted successfully
## Errors
| Status | Description |
|--------|-------------|
| 400 | Bad request or no company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Forbidden — `{"error":"…"}` |
| 404 | Property name not found — `{"error":"…"}` |
| 409 | Property name in use — `{"error":"…"}` |
| 500 | Server error — `{"error":"…"}` |
--- ---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ _Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

108
package-lock.json generated
View File

@@ -14,10 +14,10 @@
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"cross-env": "^10.1.0", "cross-env": "^10.1.0",
"crypto": "^1.0.1", "crypto": "^1.0.1",
"dotenv": "^18.0.4", "dotenv": "^18.0.5",
"express": "^5.2.1", "express": "^5.2.1",
"google-auth-library": "^11.1.0", "google-auth-library": "^11.1.0",
"ink": "^7.1.1", "ink": "^8.0.0",
"ink-text-input": "^6.0.0", "ink-text-input": "^6.0.0",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"jszip": "^3.10.2", "jszip": "^3.10.2",
@@ -41,9 +41,9 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@clack/prompts": "^1.8.1", "@clack/prompts": "^1.8.1",
"@hpcc-js/wasm-graphviz": "^1.29.1", "@hpcc-js/wasm-graphviz": "^1.29.2",
"chalk": "^6.0.1", "chalk": "^6.0.1",
"dotenv": "^18.0.4", "dotenv": "^18.0.5",
"pdfconvert-wasm": "^0.3.0", "pdfconvert-wasm": "^0.3.0",
"sharp": "^0.35.5", "sharp": "^0.35.5",
"thumbhash": "^0.1.1" "thumbhash": "^0.1.1"
@@ -66,7 +66,7 @@
"version": "0.1.0", "version": "0.1.0",
"license": "0BSD", "license": "0BSD",
"dependencies": { "dependencies": {
"dotenv": "^18.0.4" "dotenv": "^18.0.5"
}, },
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
@@ -2372,43 +2372,41 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/ink": { "node_modules/ink": {
"version": "7.1.1", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/ink/-/ink-7.1.1.tgz", "resolved": "https://registry.npmjs.org/ink/-/ink-8.0.0.tgz",
"integrity": "sha512-Y43xxa1ZSPvpmfLHcN5o+OdP8Rf8ykkNJEuKYOUNZKT8wXVNLFTtEm1nSDMQkfBH+YANF4Xuu0hhZ4ejqAtN2w==", "integrity": "sha512-M2aFwZOqmkFxCwedy6xrP4E+DGoK57iahAFnfTqNyOyJ54O7DZzu027z/NI8EGy44Sw9O9BcbTF4QEUS2GHAtg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@alcalzone/ansi-tokenize": "^0.3.0", "@alcalzone/ansi-tokenize": "^0.3.1",
"ansi-escapes": "^7.3.0", "ansi-escapes": "^7.3.0",
"ansi-styles": "^6.2.3", "ansi-styles": "^7.0.0",
"auto-bind": "^5.0.1", "auto-bind": "^5.0.1",
"chalk": "^5.6.2", "chalk": "^6.0.1",
"cli-boxes": "^4.0.1", "cli-boxes": "^4.0.1",
"cli-cursor": "^4.0.0", "cli-cursor": "^4.0.0",
"cli-truncate": "^6.0.0", "cli-truncate": "^6.0.0",
"code-excerpt": "^4.0.0", "code-excerpt": "^4.0.0",
"es-toolkit": "^1.45.1", "es-toolkit": "^1.52.0",
"indent-string": "^5.0.0", "indent-string": "^5.0.0",
"is-in-ci": "^2.0.0", "is-in-ci": "^2.0.0",
"patch-console": "^2.0.0", "patch-console": "^2.0.0",
"react-reconciler": "^0.33.0", "quick-lru": "^7.3.0",
"scheduler": "^0.27.0", "react-reconciler": "^0.34.0",
"signal-exit": "^3.0.7", "signal-exit": "^3.0.7",
"slice-ansi": "^9.0.0",
"stack-utils": "^2.0.6", "stack-utils": "^2.0.6",
"string-width": "^8.2.0", "string-width": "^8.3.0",
"terminal-size": "^4.0.1", "terminal-size": "^4.0.1",
"type-fest": "^5.5.0", "type-fest": "^5.10.0",
"widest-line": "^6.0.0", "widest-line": "^6.0.0",
"wrap-ansi": "^10.0.0", "wrap-ansi": "^10.0.2",
"ws": "^8.20.0",
"yoga-layout": "~3.2.1" "yoga-layout": "~3.2.1"
}, },
"engines": { "engines": {
"node": ">=22" "node": ">=22"
}, },
"peerDependencies": { "peerDependencies": {
"@types/react": ">=19.2.0", "@types/react": ">=19.3.0",
"react": ">=19.2.0", "react": ">=19.3.0",
"react-devtools-core": ">=6.1.2" "react-devtools-core": ">=6.1.2"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
@@ -2461,16 +2459,16 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/ink/node_modules/chalk": { "node_modules/ink/node_modules/ansi-styles": {
"version": "5.6.2", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-7.0.0.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "integrity": "sha512-kKvt3m4uwzqL0wlkPd09CmljPJGOZZ4D0fP65sqFSvPkMRKhNi+74MgIJ5QxE6SxqB4t4KyUFGg8+n5zjo6hew==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0" "node": ">=22"
}, },
"funding": { "funding": {
"url": "https://github.com/chalk/chalk?sponsor=1" "url": "https://github.com/chalk/ansi-styles?sponsor=1"
} }
}, },
"node_modules/ipaddr.js": { "node_modules/ipaddr.js": {
@@ -3324,6 +3322,18 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/quick-lru": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-7.3.0.tgz",
"integrity": "sha512-k9lSsjl36EJdK7I06v7APZCbyGT2vMTsYSRX1Q2nbYmnkBqgUhRkAuzH08Ciotteu/PLJmIF2+tti7o3C/ts2g==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/range-parser": { "node_modules/range-parser": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz",
@@ -3403,26 +3413,19 @@
"react": "^19.3.0" "react": "^19.3.0"
} }
}, },
"node_modules/react-dom/node_modules/scheduler": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz",
"integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==",
"license": "MIT",
"peer": true
},
"node_modules/react-reconciler": { "node_modules/react-reconciler": {
"version": "0.33.0", "version": "0.34.0",
"resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.33.0.tgz", "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.34.0.tgz",
"integrity": "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==", "integrity": "sha512-eVibvB1fc9j5Qf1KRkiM3GzRPBdjg2P6G6QV2f3P6Eb3OGMxRBhdg2rrUwjbV5JrQaJj8nOa8uajmmJA5tJWjQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.28.0"
}, },
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^19.2.0" "react": "^19.3.0"
} }
}, },
"node_modules/readable-stream": { "node_modules/readable-stream": {
@@ -3536,9 +3539,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/scheduler": { "node_modules/scheduler": {
"version": "0.27.0", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz",
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/selfsigned": { "node_modules/selfsigned": {
@@ -4216,27 +4219,6 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/ws": {
"version": "8.22.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz",
"integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/y18n": { "node_modules/y18n": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",

View File

@@ -19,10 +19,10 @@
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"cross-env": "^10.1.0", "cross-env": "^10.1.0",
"crypto": "^1.0.1", "crypto": "^1.0.1",
"dotenv": "^18.0.4", "dotenv": "^18.0.5",
"express": "^5.2.1", "express": "^5.2.1",
"google-auth-library": "^11.1.0", "google-auth-library": "^11.1.0",
"ink": "^7.1.1", "ink": "^8.0.0",
"ink-text-input": "^6.0.0", "ink-text-input": "^6.0.0",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"jszip": "^3.10.2", "jszip": "^3.10.2",

View File

@@ -217,6 +217,20 @@
* format: date-time * format: date-time
* text: * text:
* type: string * type: string
* CompanyHistoryEntry:
* type: object
* required: [id, date, text]
* properties:
* id:
* type: integer
* companyuserId:
* type: integer
* nullable: true
* date:
* type: string
* format: date-time
* text:
* type: string
* ArticleSupplierPrice: * ArticleSupplierPrice:
* type: object * type: object
* required: [net, taxId] * required: [net, taxId]

View File

@@ -84,26 +84,11 @@ export function register(app, basePath = '') {
* content: * content:
* application/json: * application/json:
* schema: * schema:
* type: object
* required: [attributevalues]
* properties:
* attributevalues:
* type: array * type: array
* description: Array of attribute value IDs (numbers) or objects with id or attributeValueId * description: Array of attribute value IDs. Array order defines sort.
* items: * items:
* oneOf:
* - type: integer
* description: Attribute value ID
* - type: object
* properties:
* id:
* type: integer
* description: Attribute value ID
* attributeValueId:
* type: integer
* description: Attribute value ID
* sort:
* type: integer * type: integer
* example: [166]
* responses: * responses:
* 200: * 200:
* description: Updated article attribute values * description: Updated article attribute values
@@ -147,13 +132,10 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: skuParsed.error }); return res.status(400).json({ error: skuParsed.error });
} }
const valuesInput = req.body?.attributevalues ?? req.body?.attributeValues ?? req.body?.attribute_values ?? req.body?.values ?? req.body; if (!Array.isArray(req.body)) return res.status(400).json({ error: 'attributevalues array required' });
if (!Array.isArray(valuesInput)) {
return res.status(400).json({ error: 'attributevalues array required' });
}
try { try {
const attributevalues = await db.setByArticleSku(req.user.id, skuParsed.sku, valuesInput); const attributevalues = await db.setByArticleSku(req.user.id, skuParsed.sku, req.body);
emitArticleSignal(req, skuParsed.sku); emitArticleSignal(req, skuParsed.sku);
res.json({ attributevalues }); res.json({ attributevalues });
} catch (error) { } catch (error) {

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 in use' || error.message?.includes('FOREIGN KEY constraint')) return 409;
return 500; return 500;
} }
@@ -200,6 +201,42 @@ export function register(app, basePath = '') {
* responses: * responses:
* 200: * 200:
* description: Deleted successfully * description: Deleted successfully
* 400:
* description: Bad request 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: Forbidden
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 404:
* description: Attribute name not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 409:
* description: Attribute name in use
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/ */
app.patch(`${basePath}/attributename/:id`, async (req, res) => { app.patch(`${basePath}/attributename/:id`, async (req, res) => {
const selected = requireSelectedCompany(req); const selected = requireSelectedCompany(req);
@@ -213,7 +250,9 @@ export function register(app, basePath = '') {
emit('attributename', { companyId: selected.companyId }); emit('attributename', { companyId: selected.companyId });
res.json({ attributename }); res.json({ attributename });
} catch (error) { } catch (error) {
res.status(attributeErrorStatus(error)).json({ error: error.message }); const status = attributeErrorStatus(error);
if (status === 500) console.error('Update attribute name failed:', error);
res.status(status).json({ error: error.message });
} }
}); });
@@ -229,7 +268,12 @@ export function register(app, basePath = '') {
emit('attributename', { companyId: selected.companyId }); emit('attributename', { companyId: selected.companyId });
res.json({ ok: true }); res.json({ ok: true });
} catch (error) { } catch (error) {
res.status(attributeErrorStatus(error)).json({ error: error.message }); const status = attributeErrorStatus(error);
const message = (error.message === 'Attribute name in use' || error.message?.includes('FOREIGN KEY constraint'))
? 'Attribute name in use'
: error.message;
if (status === 500) console.error('Delete attribute name failed:', error);
res.status(status).json({ error: message });
} }
}); });
} }

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 in use' || error.message?.includes('FOREIGN KEY constraint')) return 409;
return 500; return 500;
} }
@@ -216,6 +217,42 @@ export function register(app, basePath = '') {
* responses: * responses:
* 200: * 200:
* description: Deleted successfully * description: Deleted successfully
* 400:
* description: Bad request 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: Forbidden
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 404:
* description: Attribute value not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 409:
* description: Attribute value in use
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/ */
app.patch(`${basePath}/attributevalue/:id`, async (req, res) => { app.patch(`${basePath}/attributevalue/:id`, async (req, res) => {
const selected = requireSelectedCompany(req); const selected = requireSelectedCompany(req);
@@ -229,7 +266,9 @@ export function register(app, basePath = '') {
emit('attributename', { companyId: selected.companyId }); emit('attributename', { companyId: selected.companyId });
res.json({ attributevalue }); res.json({ attributevalue });
} catch (error) { } catch (error) {
res.status(attributeValueErrorStatus(error)).json({ error: error.message }); const status = attributeValueErrorStatus(error);
if (status === 500) console.error('Update attribute value failed:', error);
res.status(status).json({ error: error.message });
} }
}); });
@@ -245,7 +284,12 @@ export function register(app, basePath = '') {
emit('attributename', { companyId: selected.companyId }); emit('attributename', { companyId: selected.companyId });
res.json({ ok: true }); res.json({ ok: true });
} catch (error) { } catch (error) {
res.status(attributeValueErrorStatus(error)).json({ error: error.message }); const status = attributeValueErrorStatus(error);
const message = (error.message === 'Attribute value in use' || error.message?.includes('FOREIGN KEY constraint'))
? 'Attribute value in use'
: error.message;
if (status === 500) console.error('Delete attribute value failed:', error);
res.status(status).json({ error: message });
} }
}); });
} }

View File

@@ -400,6 +400,7 @@ export function register(app, basePath = '') {
.map((a) => ({ .map((a) => ({
...a, ...a,
childCount: Number(a.childCount ?? 0), childCount: Number(a.childCount ?? 0),
isFather: Boolean(a.isFather ?? a.is_father),
})); }));
res.status(200).json({ articles }); res.status(200).json({ articles });
} catch (error) { } catch (error) {

View File

@@ -85,26 +85,11 @@ export function register(app, basePath = '') {
* content: * content:
* application/json: * application/json:
* schema: * schema:
* type: object
* required: [attributevalues]
* properties:
* attributevalues:
* type: array * type: array
* description: Array of attribute value IDs (numbers) or objects with id or attributeValueId * description: Array of attribute value IDs. Array order defines sort.
* items: * items:
* oneOf:
* - type: integer
* description: Attribute value ID
* - type: object
* properties:
* id:
* type: integer
* description: Attribute value ID
* attributeValueId:
* type: integer
* description: Attribute value ID
* sort:
* type: integer * type: integer
* example: [166]
* responses: * responses:
* 200: * 200:
* description: Updated category attribute values * description: Updated category attribute values
@@ -148,13 +133,10 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: idParsed.error }); return res.status(400).json({ error: idParsed.error });
} }
const valuesInput = req.body?.attributevalues ?? req.body?.attributeValues ?? req.body?.attribute_values ?? req.body?.values ?? req.body; if (!Array.isArray(req.body)) return res.status(400).json({ error: 'attributevalues array required' });
if (!Array.isArray(valuesInput)) {
return res.status(400).json({ error: 'attributevalues array required' });
}
try { try {
const attributevalues = await db.setByCategoryId(req.user.id, idParsed.id, valuesInput); const attributevalues = await db.setByCategoryId(req.user.id, idParsed.id, req.body);
emitCategorySignal(req); emitCategorySignal(req);
res.json({ attributevalues }); res.json({ attributevalues });
} catch (error) { } catch (error) {

View File

@@ -32,10 +32,21 @@ function parseCompanyId(id) {
return { companyId }; return { companyId };
} }
function requireSelectedCompany(req) {
const id = req.user?.selectedCompanyId;
if (id == null) {
const err = new Error('No company selected');
err.statusCode = 400;
throw err;
}
return id;
}
function companyErrorStatus(error) { function companyErrorStatus(error) {
if (error.message === 'Company not found') return 404; if (error.message === 'Company not found') return 404;
if (error.message === 'Company name not available') return 409; if (error.message === 'Company name not available') return 409;
if (error.message === 'Invalid tax rate') return 400; if (error.message === 'Invalid tax rate' || error.message === 'Text required' || error.message === 'No company selected') return 400;
if (error.message === 'Not authorized') return 403;
return 500; return 500;
} }
@@ -167,6 +178,170 @@ export function register(app, basePath = '') {
} }
}); });
/**
* @openapi
* /company/history:
* get:
* tags: [Company]
* summary: Get company history
* description: |
* Returns `companyhistory` rows for the selected company, newest first (`date` DESC, `id` DESC).
* security:
* - cookieAuth: []
* x-markdown: company/history.md
* parameters:
* - in: query
* name: limit
* required: false
* schema:
* type: integer
* minimum: 1
* maximum: 1000
* description: Maximum number of entries to return (newest first). Omit for all entries.
* responses:
* 200:
* description: Company history (newest first)
* content:
* application/json:
* schema:
* type: object
* required: [history, truncated]
* properties:
* history:
* type: array
* items:
* $ref: '#/components/schemas/CompanyHistoryEntry'
* truncated:
* type: boolean
* description: True when a limit was applied and the result set was exactly that size (more entries may exist).
* 400:
* description: Invalid limit 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: Forbidden
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* post:
* tags: [Company]
* summary: Add company history entry
* description: |
* Appends an entry to `companyhistory` for the selected company.
* security:
* - cookieAuth: []
* x-markdown: company/addHistory.md
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required: [text]
* properties:
* text:
* type: string
* description: History entry text or note
* responses:
* 201:
* description: History entry created
* content:
* application/json:
* schema:
* type: object
* required: [history]
* properties:
* history:
* $ref: '#/components/schemas/CompanyHistoryEntry'
* 400:
* description: Text required 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: Forbidden
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
app.get(`${basePath}/company/history`, async (req, res) => {
try {
requireSelectedCompany(req);
} catch (error) {
return res.status(error.statusCode || 400).json({ error: error.message });
}
const rawLimit = req.query.limit;
let limit = null;
if (rawLimit !== undefined) {
limit = parseInt(rawLimit, 10);
if (!Number.isInteger(limit) || limit < 1 || limit > 1000) {
return res.status(400).json({ error: 'limit must be an integer between 1 and 1000' });
}
}
try {
const history = await db.getCompanyHistory(req.user.id, limit != null ? { limit } : undefined);
const truncated = limit != null && history.length === limit;
res.status(200).json({ history, truncated });
} catch (error) {
const status = companyErrorStatus(error);
if (status === 500) console.error('Get company history failed:', error);
res.status(status).json({ error: error.message });
}
});
app.post(`${basePath}/company/history`, async (req, res) => {
try {
requireSelectedCompany(req);
} catch (error) {
return res.status(error.statusCode || 400).json({ error: error.message });
}
const text = typeof req.body?.text === 'string' ? req.body.text.trim() : '';
if (!text) {
return res.status(400).json({ error: 'text is required' });
}
try {
const entry = await db.addCompanyHistory(req.user.id, { text });
res.status(201).json({ history: entry });
} catch (error) {
const status = companyErrorStatus(error);
if (status === 500) console.error('Add company history failed:', error);
res.status(status).json({ error: error.message });
}
});
/** /**
* @openapi * @openapi
* /company/{id}: * /company/{id}:

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 in use' || error.message?.includes('FOREIGN KEY constraint')) return 409;
return 500; return 500;
} }
@@ -214,6 +215,42 @@ export function register(app, basePath = '') {
* responses: * responses:
* 200: * 200:
* description: Deleted successfully * description: Deleted successfully
* 400:
* description: Bad request 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: Forbidden
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 404:
* description: Property name not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 409:
* description: Property name in use
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/ */
app.patch(`${basePath}/propertyname/:id`, async (req, res) => { app.patch(`${basePath}/propertyname/:id`, async (req, res) => {
const selected = requireSelectedCompany(req); const selected = requireSelectedCompany(req);
@@ -227,7 +264,9 @@ export function register(app, basePath = '') {
emit('propertyname', { companyId: selected.companyId }); emit('propertyname', { companyId: selected.companyId });
res.json({ propertyname }); res.json({ propertyname });
} catch (error) { } catch (error) {
res.status(propertyErrorStatus(error)).json({ error: error.message }); const status = propertyErrorStatus(error);
if (status === 500) console.error('Update property name failed:', error);
res.status(status).json({ error: error.message });
} }
}); });
@@ -243,7 +282,12 @@ export function register(app, basePath = '') {
emit('propertyname', { companyId: selected.companyId }); emit('propertyname', { companyId: selected.companyId });
res.json({ ok: true }); res.json({ ok: true });
} catch (error) { } catch (error) {
res.status(propertyErrorStatus(error)).json({ error: error.message }); const status = propertyErrorStatus(error);
const message = (error.message === 'Property name in use' || error.message?.includes('FOREIGN KEY constraint'))
? 'Property name in use'
: error.message;
if (status === 500) console.error('Delete property name failed:', error);
res.status(status).json({ error: message });
} }
}); });
} }

View File

@@ -24,3 +24,13 @@ export async function deleteCompany(companyId) {
const store = await getStore(); const store = await getStore();
await store.company.delete(companyId); await store.company.delete(companyId);
} }
export async function getCompanyHistory(googleAuthId, opts) {
const store = await getStore();
return store.company.getHistory(googleAuthId, opts);
}
export async function addCompanyHistory(googleAuthId, data) {
const store = await getStore();
return store.company.addHistory(googleAuthId, data);
}

View File

@@ -11,6 +11,7 @@ import { createDashbackRequestTool } from '../tools/dashback-request.js';
import { createAskUserTool } from '../tools/ask-user.js'; import { createAskUserTool } from '../tools/ask-user.js';
import { createJavascriptExecuteTool } from '../tools/javascript.js'; import { createJavascriptExecuteTool } from '../tools/javascript.js';
import { createResetVfsTool } from '../tools/reset-vfs.js'; import { createResetVfsTool } from '../tools/reset-vfs.js';
import { createApiSuggestionsOrIssuesTool } from '../tools/apisuggestionsorissues.js';
const KEEP_ALIVE_MS = 20_000; const KEEP_ALIVE_MS = 20_000;
@@ -38,6 +39,7 @@ function buildSystemPrompt() {
'2. ask_user: Ask clarifying questions or request decisions from the user if their intent is ambiguous or requires choices. When you call ask_user, execution pauses immediately and waits for the user to provide their response.', '2. ask_user: Ask clarifying questions or request decisions from the user if their intent is ambiguous or requires choices. When you call ask_user, execution pauses immediately and waits for the user to provide their response.',
'3. javascript_execute: Run JavaScript snippets in a sandbox to compute, aggregate, transform, or analyze data. Assign module.exports to return a value. With allow_network: true, snippets can directly call Dashback API routes using fetch() (e.g. await fetch("/article/2")) and search images using search_for_images(query). A sandboxed virtual filesystem (fs / vfs) is available to store and retrieve artifacts across runs.', '3. javascript_execute: Run JavaScript snippets in a sandbox to compute, aggregate, transform, or analyze data. Assign module.exports to return a value. With allow_network: true, snippets can directly call Dashback API routes using fetch() (e.g. await fetch("/article/2")) and search images using search_for_images(query). A sandboxed virtual filesystem (fs / vfs) is available to store and retrieve artifacts across runs.',
'4. reset_vfs: Delete all files, images, and artifacts stored in the virtual filesystem (VFS) to free up storage or clean up.', '4. reset_vfs: Delete all files, images, and artifacts stored in the virtual filesystem (VFS) to free up storage or clean up.',
'5. apisuggestionsorissues: Report missing API endpoints, bugs, or issues to be triaged. Automatically writes an entry to the company history table for review.',
'', '',
'OPERATING GUIDELINES:', 'OPERATING GUIDELINES:',
'- Refer to the DASHBACK API ROUTES documentation below to find appropriate endpoints, parameters, and HTTP methods.', '- Refer to the DASHBACK API ROUTES documentation below to find appropriate endpoints, parameters, and HTTP methods.',
@@ -404,6 +406,7 @@ export class ConversationManager {
createAskUserTool(), createAskUserTool(),
createJavascriptExecuteTool({ googleAuthId, companyId: this.companyId }), createJavascriptExecuteTool({ googleAuthId, companyId: this.companyId }),
createResetVfsTool({ googleAuthId, companyId: this.companyId }), createResetVfsTool({ googleAuthId, companyId: this.companyId }),
createApiSuggestionsOrIssuesTool({ googleAuthId, companyId: this.companyId }),
], ],
}); });

View File

@@ -0,0 +1,61 @@
import { defineTool } from 'libllmorbiter';
import * as db from '../db/company.js';
/**
* Creates the apisuggestionsorissues tool allowing the agent to report
* missing API endpoints, bugs, or general issues into the company history table.
* These reports are then visible to triage agents or administrators.
*
* @param {{ googleAuthId: number, companyId?: number }} ctx
*/
export function createApiSuggestionsOrIssuesTool({ googleAuthId, companyId }) {
return defineTool({
name: 'apisuggestionsorissues',
description:
'Report a missing API endpoint, feature request, bug, or general issue. ' +
'The report is recorded in the company history table for review by the triage agent. ' +
'Provide a clear description of the missing functionality or issue.',
strict: false,
parameters: {
type: 'object',
additionalProperties: false,
required: ['text'],
properties: {
text: {
type: 'string',
description: 'Detailed description of the missing API, bug, or issue.',
},
type: {
type: 'string',
enum: ['missing_api', 'bug', 'suggestion', 'issue', 'other'],
description: 'Category/type of the report.',
},
title: {
type: 'string',
description: 'Optional short summary or title.',
},
},
},
invoke: async ({ text, type, title }) => {
const trimmedText = typeof text === 'string' ? text.trim() : '';
if (!trimmedText) {
throw new Error('text is required');
}
const prefixParts = [];
if (type) prefixParts.push(`[${type.toUpperCase()}]`);
if (title) prefixParts.push(title.trim());
const formatted = prefixParts.length > 0
? `${prefixParts.join(' ')}\n${trimmedText}`
: trimmedText;
const entry = await db.addCompanyHistory(googleAuthId, { text: formatted });
return {
success: true,
message: 'Recorded in company history for triage agent review',
entry,
};
},
});
}