From 43fa8ea1057682d56cb674ddd9291c8131822bf2 Mon Sep 17 00:00:00 2001 From: seb Date: Sun, 4 Oct 2026 11:25:17 +0200 Subject: [PATCH] u --- docs/api/README.md | 2 + docs/api/openapi.json | 476 +++++++++++++++--- docs/api/routes-all.md | 8 +- docs/api/routes.md | 8 +- .../routes/article/_sku/attributevalue.put.md | 12 +- docs/api/routes/attributename/_id.delete.md | 11 + docs/api/routes/attributevalue/_id.delete.md | 11 + .../routes/category/_id/attributevalue.put.md | 12 +- docs/api/routes/company/addHistory.md | 48 ++ docs/api/routes/company/history.md | 45 ++ docs/api/routes/propertyname/_id.delete.md | 11 + package-lock.json | 108 ++-- package.json | 4 +- src/api/openapi.js | 14 + src/api/routes/articleattributevalue.js | 32 +- src/api/routes/attributename.js | 48 +- src/api/routes/attributevalue.js | 48 +- src/api/routes/category.js | 1 + src/api/routes/categoryattributevalue.js | 32 +- src/api/routes/company.js | 177 ++++++- src/api/routes/propertyname.js | 48 +- src/db/company.js | 10 + src/services/conversationManager.js | 3 + src/tools/apisuggestionsorissues.js | 61 +++ 24 files changed, 1016 insertions(+), 214 deletions(-) create mode 100644 docs/api/routes/company/addHistory.md create mode 100644 docs/api/routes/company/history.md create mode 100644 src/tools/apisuggestionsorissues.js diff --git a/docs/api/README.md b/docs/api/README.md index c659e43..c749af0 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -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/company/list.md`](routes/company/list.md) | `GET /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/get.md`](routes/companysetting/get.md) | `GET /company/setting/{key}` | | [`routes/companysetting/set.md`](routes/companysetting/set.md) | `PUT /company/setting/{key}` | diff --git a/docs/api/openapi.json b/docs/api/openapi.json index 0689a86..5d61a62 100644 --- a/docs/api/openapi.json +++ b/docs/api/openapi.json @@ -3030,6 +3030,66 @@ "responses": { "200": { "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}": { "put": { "tags": [ @@ -9837,40 +10085,14 @@ "content": { "application/json": { "schema": { - "type": "object", - "required": [ - "attributevalues" - ], - "properties": { - "attributevalues": { - "type": "array", - "description": "Array of attribute value IDs (numbers) or objects with id or attributeValueId", - "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": "array", + "description": "Array of attribute value IDs. Array order defines sort.", + "items": { + "type": "integer" + }, + "example": [ + 166 + ] } } } @@ -11305,6 +11527,66 @@ "responses": { "200": { "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": { "200": { "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,40 +12681,14 @@ "content": { "application/json": { "schema": { - "type": "object", - "required": [ - "attributevalues" - ], - "properties": { - "attributevalues": { - "type": "array", - "description": "Array of attribute value IDs (numbers) or objects with id or attributeValueId", - "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": "array", + "description": "Array of attribute value IDs. Array order defines sort.", + "items": { + "type": "integer" + }, + "example": [ + 166 + ] } } } @@ -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": { "type": "object", "required": [ diff --git a/docs/api/routes-all.md b/docs/api/routes-all.md index 67b5998..14b5e3f 100644 --- a/docs/api/routes-all.md +++ b/docs/api/routes-all.md @@ -1,6 +1,6 @@ # 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 @@ -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 | | `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 | -| `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}/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 | @@ -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) | | `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 | -| `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 | | `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 | @@ -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 | | `GET` | `/company` | — | `200 { companies: { id, iso639, name, taxrate, reducedtaxrate }[] }` | List companies | | `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/{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 | diff --git a/docs/api/routes.md b/docs/api/routes.md index c7f4328..76776d8 100644 --- a/docs/api/routes.md +++ b/docs/api/routes.md @@ -1,6 +1,6 @@ # Dash API Routes Overview -Overview of all 142 API endpoints across all modules. +Overview of all 144 API endpoints across all modules. ### 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 | | `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 | -| `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}/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 | @@ -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) | | `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 | -| `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 | | `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 | | `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/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/{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 | diff --git a/docs/api/routes/article/_sku/attributevalue.put.md b/docs/api/routes/article/_sku/attributevalue.put.md index a77a8e0..d325126 100644 --- a/docs/api/routes/article/_sku/attributevalue.put.md +++ b/docs/api/routes/article/_sku/attributevalue.put.md @@ -14,16 +14,10 @@ Replaces all attribute values for an article. Enforces exclusive constraint (max ## Body (JSON) -| Field | Required | Type | Description | -|-------|----------|------|-------------| -| `attributevalues` | yes | any[] | Array of attribute value IDs (numbers) or objects with id or attributeValueId | - ```json -{ - "attributevalues": [ - "…" - ] -} +[ + 166 +] ``` ## Response `200` diff --git a/docs/api/routes/attributename/_id.delete.md b/docs/api/routes/attributename/_id.delete.md index af65d50..414b797 100644 --- a/docs/api/routes/attributename/_id.delete.md +++ b/docs/api/routes/attributename/_id.delete.md @@ -16,6 +16,17 @@ Deletes an attribute name and cascades to attribute values and article assignmen 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`._ diff --git a/docs/api/routes/attributevalue/_id.delete.md b/docs/api/routes/attributevalue/_id.delete.md index 0cf3c2b..4ff81a8 100644 --- a/docs/api/routes/attributevalue/_id.delete.md +++ b/docs/api/routes/attributevalue/_id.delete.md @@ -16,6 +16,17 @@ Deletes an attribute value and cascades to article assignments. 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`._ diff --git a/docs/api/routes/category/_id/attributevalue.put.md b/docs/api/routes/category/_id/attributevalue.put.md index 46219db..3fc630c 100644 --- a/docs/api/routes/category/_id/attributevalue.put.md +++ b/docs/api/routes/category/_id/attributevalue.put.md @@ -14,16 +14,10 @@ Replaces all attribute values for a category. Enforces exclusive constraint (max ## Body (JSON) -| Field | Required | Type | Description | -|-------|----------|------|-------------| -| `attributevalues` | yes | any[] | Array of attribute value IDs (numbers) or objects with id or attributeValueId | - ```json -{ - "attributevalues": [ - "…" - ] -} +[ + 166 +] ``` ## Response `200` diff --git a/docs/api/routes/company/addHistory.md b/docs/api/routes/company/addHistory.md new file mode 100644 index 0000000..208f501 --- /dev/null +++ b/docs/api/routes/company/addHistory.md @@ -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`._ diff --git a/docs/api/routes/company/history.md b/docs/api/routes/company/history.md new file mode 100644 index 0000000..d44c609 --- /dev/null +++ b/docs/api/routes/company/history.md @@ -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`._ diff --git a/docs/api/routes/propertyname/_id.delete.md b/docs/api/routes/propertyname/_id.delete.md index 78012cd..f4c3175 100644 --- a/docs/api/routes/propertyname/_id.delete.md +++ b/docs/api/routes/propertyname/_id.delete.md @@ -16,6 +16,17 @@ Deletes a property name and cascades to article properties. 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`._ diff --git a/package-lock.json b/package-lock.json index 0891974..796f8a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,10 +14,10 @@ "cookie-parser": "^1.4.7", "cross-env": "^10.1.0", "crypto": "^1.0.1", - "dotenv": "^18.0.4", + "dotenv": "^18.0.5", "express": "^5.2.1", "google-auth-library": "^11.1.0", - "ink": "^7.1.1", + "ink": "^8.0.0", "ink-text-input": "^6.0.0", "jsonwebtoken": "^9.0.3", "jszip": "^3.10.2", @@ -41,9 +41,9 @@ "license": "ISC", "dependencies": { "@clack/prompts": "^1.8.1", - "@hpcc-js/wasm-graphviz": "^1.29.1", + "@hpcc-js/wasm-graphviz": "^1.29.2", "chalk": "^6.0.1", - "dotenv": "^18.0.4", + "dotenv": "^18.0.5", "pdfconvert-wasm": "^0.3.0", "sharp": "^0.35.5", "thumbhash": "^0.1.1" @@ -66,7 +66,7 @@ "version": "0.1.0", "license": "0BSD", "dependencies": { - "dotenv": "^18.0.4" + "dotenv": "^18.0.5" }, "engines": { "node": ">=18.0.0" @@ -2372,43 +2372,41 @@ "license": "ISC" }, "node_modules/ink": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/ink/-/ink-7.1.1.tgz", - "integrity": "sha512-Y43xxa1ZSPvpmfLHcN5o+OdP8Rf8ykkNJEuKYOUNZKT8wXVNLFTtEm1nSDMQkfBH+YANF4Xuu0hhZ4ejqAtN2w==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ink/-/ink-8.0.0.tgz", + "integrity": "sha512-M2aFwZOqmkFxCwedy6xrP4E+DGoK57iahAFnfTqNyOyJ54O7DZzu027z/NI8EGy44Sw9O9BcbTF4QEUS2GHAtg==", "license": "MIT", "dependencies": { - "@alcalzone/ansi-tokenize": "^0.3.0", + "@alcalzone/ansi-tokenize": "^0.3.1", "ansi-escapes": "^7.3.0", - "ansi-styles": "^6.2.3", + "ansi-styles": "^7.0.0", "auto-bind": "^5.0.1", - "chalk": "^5.6.2", + "chalk": "^6.0.1", "cli-boxes": "^4.0.1", "cli-cursor": "^4.0.0", "cli-truncate": "^6.0.0", "code-excerpt": "^4.0.0", - "es-toolkit": "^1.45.1", + "es-toolkit": "^1.52.0", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", - "react-reconciler": "^0.33.0", - "scheduler": "^0.27.0", + "quick-lru": "^7.3.0", + "react-reconciler": "^0.34.0", "signal-exit": "^3.0.7", - "slice-ansi": "^9.0.0", "stack-utils": "^2.0.6", - "string-width": "^8.2.0", + "string-width": "^8.3.0", "terminal-size": "^4.0.1", - "type-fest": "^5.5.0", + "type-fest": "^5.10.0", "widest-line": "^6.0.0", - "wrap-ansi": "^10.0.0", - "ws": "^8.20.0", + "wrap-ansi": "^10.0.2", "yoga-layout": "~3.2.1" }, "engines": { "node": ">=22" }, "peerDependencies": { - "@types/react": ">=19.2.0", - "react": ">=19.2.0", + "@types/react": ">=19.3.0", + "react": ">=19.3.0", "react-devtools-core": ">=6.1.2" }, "peerDependenciesMeta": { @@ -2461,16 +2459,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ink/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "node_modules/ink/node_modules/ansi-styles": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-7.0.0.tgz", + "integrity": "sha512-kKvt3m4uwzqL0wlkPd09CmljPJGOZZ4D0fP65sqFSvPkMRKhNi+74MgIJ5QxE6SxqB4t4KyUFGg8+n5zjo6hew==", "license": "MIT", "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "node": ">=22" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/ipaddr.js": { @@ -3324,6 +3322,18 @@ "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": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", @@ -3403,26 +3413,19 @@ "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": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.33.0.tgz", - "integrity": "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==", + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.34.0.tgz", + "integrity": "sha512-eVibvB1fc9j5Qf1KRkiM3GzRPBdjg2P6G6QV2f3P6Eb3OGMxRBhdg2rrUwjbV5JrQaJj8nOa8uajmmJA5tJWjQ==", "license": "MIT", "dependencies": { - "scheduler": "^0.27.0" + "scheduler": "^0.28.0" }, "engines": { "node": ">=0.10.0" }, "peerDependencies": { - "react": "^19.2.0" + "react": "^19.3.0" } }, "node_modules/readable-stream": { @@ -3536,9 +3539,9 @@ "license": "MIT" }, "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "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" }, "node_modules/selfsigned": { @@ -4216,27 +4219,6 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "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": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", diff --git a/package.json b/package.json index 7fb8570..6dd2308 100644 --- a/package.json +++ b/package.json @@ -19,10 +19,10 @@ "cookie-parser": "^1.4.7", "cross-env": "^10.1.0", "crypto": "^1.0.1", - "dotenv": "^18.0.4", + "dotenv": "^18.0.5", "express": "^5.2.1", "google-auth-library": "^11.1.0", - "ink": "^7.1.1", + "ink": "^8.0.0", "ink-text-input": "^6.0.0", "jsonwebtoken": "^9.0.3", "jszip": "^3.10.2", diff --git a/src/api/openapi.js b/src/api/openapi.js index e587256..44ac969 100644 --- a/src/api/openapi.js +++ b/src/api/openapi.js @@ -217,6 +217,20 @@ * format: date-time * text: * 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: * type: object * required: [net, taxId] diff --git a/src/api/routes/articleattributevalue.js b/src/api/routes/articleattributevalue.js index 96200cb..bb6bf35 100644 --- a/src/api/routes/articleattributevalue.js +++ b/src/api/routes/articleattributevalue.js @@ -84,26 +84,11 @@ export function register(app, basePath = '') { * content: * application/json: * schema: - * type: object - * required: [attributevalues] - * properties: - * attributevalues: - * type: array - * description: Array of attribute value IDs (numbers) or objects with id or attributeValueId - * 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: array + * description: Array of attribute value IDs. Array order defines sort. + * items: + * type: integer + * example: [166] * responses: * 200: * description: Updated article attribute values @@ -147,13 +132,10 @@ export function register(app, basePath = '') { 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(valuesInput)) { - return res.status(400).json({ error: 'attributevalues array required' }); - } + if (!Array.isArray(req.body)) return res.status(400).json({ error: 'attributevalues array required' }); 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); res.json({ attributevalues }); } catch (error) { diff --git a/src/api/routes/attributename.js b/src/api/routes/attributename.js index 51997f0..0b47abd 100644 --- a/src/api/routes/attributename.js +++ b/src/api/routes/attributename.js @@ -17,6 +17,7 @@ function attributeErrorStatus(error) { if (error.message === 'Name required') return 400; if (error.message === 'Invalid attribute name order') 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; } @@ -200,6 +201,42 @@ export function register(app, basePath = '') { * responses: * 200: * 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) => { const selected = requireSelectedCompany(req); @@ -213,7 +250,9 @@ export function register(app, basePath = '') { emit('attributename', { companyId: selected.companyId }); res.json({ attributename }); } 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 }); res.json({ ok: true }); } 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 }); } }); } diff --git a/src/api/routes/attributevalue.js b/src/api/routes/attributevalue.js index 84a6bde..277cab9 100644 --- a/src/api/routes/attributevalue.js +++ b/src/api/routes/attributevalue.js @@ -16,6 +16,7 @@ function attributeValueErrorStatus(error) { if (error.message === 'Value required') 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 === 'Attribute value in use' || error.message?.includes('FOREIGN KEY constraint')) return 409; return 500; } @@ -216,6 +217,42 @@ export function register(app, basePath = '') { * responses: * 200: * 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) => { const selected = requireSelectedCompany(req); @@ -229,7 +266,9 @@ export function register(app, basePath = '') { emit('attributename', { companyId: selected.companyId }); res.json({ attributevalue }); } 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 }); res.json({ ok: true }); } 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 }); } }); } diff --git a/src/api/routes/category.js b/src/api/routes/category.js index 3be3b16..5378744 100644 --- a/src/api/routes/category.js +++ b/src/api/routes/category.js @@ -400,6 +400,7 @@ export function register(app, basePath = '') { .map((a) => ({ ...a, childCount: Number(a.childCount ?? 0), + isFather: Boolean(a.isFather ?? a.is_father), })); res.status(200).json({ articles }); } catch (error) { diff --git a/src/api/routes/categoryattributevalue.js b/src/api/routes/categoryattributevalue.js index 79951fe..9f306e6 100644 --- a/src/api/routes/categoryattributevalue.js +++ b/src/api/routes/categoryattributevalue.js @@ -85,26 +85,11 @@ export function register(app, basePath = '') { * content: * application/json: * schema: - * type: object - * required: [attributevalues] - * properties: - * attributevalues: - * type: array - * description: Array of attribute value IDs (numbers) or objects with id or attributeValueId - * 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: array + * description: Array of attribute value IDs. Array order defines sort. + * items: + * type: integer + * example: [166] * responses: * 200: * description: Updated category attribute values @@ -148,13 +133,10 @@ export function register(app, basePath = '') { 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(valuesInput)) { - return res.status(400).json({ error: 'attributevalues array required' }); - } + if (!Array.isArray(req.body)) return res.status(400).json({ error: 'attributevalues array required' }); 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); res.json({ attributevalues }); } catch (error) { diff --git a/src/api/routes/company.js b/src/api/routes/company.js index bce0dc3..f035b39 100644 --- a/src/api/routes/company.js +++ b/src/api/routes/company.js @@ -32,10 +32,21 @@ function parseCompanyId(id) { 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) { if (error.message === 'Company not found') return 404; 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; } @@ -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 * /company/{id}: diff --git a/src/api/routes/propertyname.js b/src/api/routes/propertyname.js index f9d1f60..76188e9 100644 --- a/src/api/routes/propertyname.js +++ b/src/api/routes/propertyname.js @@ -17,6 +17,7 @@ function propertyErrorStatus(error) { if (error.message === 'Name required') return 400; if (error.message === 'Invalid property name order') 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; } @@ -214,6 +215,42 @@ export function register(app, basePath = '') { * responses: * 200: * 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) => { const selected = requireSelectedCompany(req); @@ -227,7 +264,9 @@ export function register(app, basePath = '') { emit('propertyname', { companyId: selected.companyId }); res.json({ propertyname }); } 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 }); res.json({ ok: true }); } 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 }); } }); } diff --git a/src/db/company.js b/src/db/company.js index daf862f..935802d 100644 --- a/src/db/company.js +++ b/src/db/company.js @@ -24,3 +24,13 @@ export async function deleteCompany(companyId) { const store = await getStore(); 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); +} diff --git a/src/services/conversationManager.js b/src/services/conversationManager.js index eb90998..9153fc6 100644 --- a/src/services/conversationManager.js +++ b/src/services/conversationManager.js @@ -11,6 +11,7 @@ import { createDashbackRequestTool } from '../tools/dashback-request.js'; import { createAskUserTool } from '../tools/ask-user.js'; import { createJavascriptExecuteTool } from '../tools/javascript.js'; import { createResetVfsTool } from '../tools/reset-vfs.js'; +import { createApiSuggestionsOrIssuesTool } from '../tools/apisuggestionsorissues.js'; 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.', '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.', + '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:', '- Refer to the DASHBACK API ROUTES documentation below to find appropriate endpoints, parameters, and HTTP methods.', @@ -404,6 +406,7 @@ export class ConversationManager { createAskUserTool(), createJavascriptExecuteTool({ googleAuthId, companyId: this.companyId }), createResetVfsTool({ googleAuthId, companyId: this.companyId }), + createApiSuggestionsOrIssuesTool({ googleAuthId, companyId: this.companyId }), ], }); diff --git a/src/tools/apisuggestionsorissues.js b/src/tools/apisuggestionsorissues.js new file mode 100644 index 0000000..0853721 --- /dev/null +++ b/src/tools/apisuggestionsorissues.js @@ -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, + }; + }, + }); +}