This commit is contained in:
seb
2026-09-30 18:45:30 +02:00
parent caacbe4968
commit d6fe6ff1d2
5 changed files with 298 additions and 15609 deletions

View File

@@ -29,7 +29,7 @@ Docs are generated from `@openapi` JSDoc on route handlers:
npm run docs:api
```
That writes `docs/api/openapi.json` and per-route Markdown under `docs/api/routes/`.
That writes `docs/api/routes.md` and per-route Markdown under `docs/api/routes/`.
### Brands (company-scoped, `selectedCompanyId` aus der Session)

View File

@@ -195,7 +195,7 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route
| [`routes/warehouse/update.md`](routes/warehouse/update.md) | `PATCH /warehouse/{id}` |
| [`routes/warehouse/_id.put.md`](routes/warehouse/_id.put.md) | `PUT /warehouse/{id}` |
Machine-readable spec: [`openapi.json`](openapi.json).
Route overview: [`routes.md`](routes.md).
## Signals (SSE)

File diff suppressed because it is too large Load Diff

168
docs/api/routes.md Normal file
View File

@@ -0,0 +1,168 @@
# Dash API Routes Overview
Overview of all 162 API endpoints across all modules.
| Method | Route | Params | Return Type | Summary | Docs |
|:---|:---|:---|:---|:---|:---|
| `POST` | `/article` | `body: { parentId, name, track_stock }` | `201 { article: ArticleSummary }` | Create article | [`routes/article/create.md`](routes/article/create.md) |
| `GET` | `/article/export.zip` | `?categoryId:integer` | `200 zip` | Export articles as ZIP | [`routes/article/export.zip.md`](routes/article/export.zip.md) |
| `DELETE` | `/article/{sku}` | `sku:string` | `204 void` | Delete article by SKU | [`routes/article/delete.md`](routes/article/delete.md) |
| `GET` | `/article/{sku}` | `sku:string` | `200 { article: ArticleDetail }` | Get article by SKU | [`routes/article/get.md`](routes/article/get.md) |
| `PATCH` | `/article/{sku}` | `sku:string, body: { name, unitamount, unitSi, … }` | `200 { article: ArticleDetail }` | Update article by SKU | [`routes/article/update.md`](routes/article/update.md) |
| `GET` | `/article/{sku}/articlelist` | `sku:string` | `200 { items: { id, articleId, sku, … }[] }` | Get Stückliste (bundle items) for an article | [`routes/article/_sku/articlelist.get.md`](routes/article/_sku/articlelist.get.md) |
| `PUT` | `/article/{sku}/articlelist` | `sku:string, body: { items: { articleId, sku, count, … }[] }` | `200 { items: object[] }` | Set Stückliste (bundle items) for an article | [`routes/article/_sku/articlelist.put.md`](routes/article/_sku/articlelist.put.md) |
| `GET` | `/article/{sku}/attributevalue` | `sku:string` | `200 { attributevalues: ArticleAttributeValue[] }` | List article attribute values | [`routes/article/_sku/attributevalue.get.md`](routes/article/_sku/attributevalue.get.md) |
| `PUT` | `/article/{sku}/attributevalue` | `sku:string, body: { attributevalues: any[] }` | `200 { attributevalues: ArticleAttributeValue[] }` | Bulk set article attribute values | [`routes/article/_sku/attributevalue.put.md`](routes/article/_sku/attributevalue.put.md) |
| `PUT` | `/article/{sku}/brand` | `sku:string, body: { brandId: integer }` | `200 { article: ArticleDetail }` | Set or clear article brand by SKU | [`routes/article/set-brand.md`](routes/article/set-brand.md) |
| `PUT` | `/article/{sku}/categories` | `sku:string, body: { categoryIds: integer[] }` | `200 { article: ArticleDetail }` | Replace article categories by SKU | [`routes/article/set-categories.md`](routes/article/set-categories.md) |
| `GET` | `/article/{sku}/children` | `sku:string` | `200 { children: { id, sku, name, … }[] }` | List child articles of a father article | [`routes/article/_sku/children.get.md`](routes/article/_sku/children.get.md) |
| `POST` | `/article/{sku}/children` | `sku:string, body: { attributeValueId, value, name }` | `201 { article: Article }` | Add a child article to a father article | [`routes/article/_sku/children.post.md`](routes/article/_sku/children.post.md) |
| `DELETE` | `/article/{sku}/children/{childSku}` | `sku:string, childSku:string, ?deleteArticle:boolean` | `200 { success, deleted }` | Remove a child article from a father article | [`routes/article/_sku/children/_childSku.md`](routes/article/_sku/children/_childSku.md) |
| `PUT` | `/article/{sku}/eans` | `sku:string, body: { eans: string[] }` | `200 { article: ArticleDetail }` | Replace article EANs by SKU | [`routes/article/set-eans.md`](routes/article/set-eans.md) |
| `POST` | `/article/{sku}/father` | `sku:string, body: { isFather, attributeNameId }` | `200 { article: Article }` | Toggle father mode for an article | [`routes/article/_sku/father.md`](routes/article/_sku/father.md) |
| `GET` | `/article/{sku}/history` | `sku:string` | `200 { history: ArticleHistoryEntry[] }` | Get article history by SKU | [`routes/article/history.md`](routes/article/history.md) |
| `POST` | `/article/{sku}/image` | `sku:string, image/avif` | `201 { image: ArticleImage }` | Add article image | [`routes/article/add-image.md`](routes/article/add-image.md) |
| `DELETE` | `/article/{sku}/image/{fullHash}` | `sku:string, fullHash:string` | `204 void` | Delete article image | [`routes/article/delete-image.md`](routes/article/delete-image.md) |
| `PUT` | `/article/{sku}/images/order` | `sku:string, body: { fullHashes: string[] }` | `200 { fullHashes: string[] }` | Reorder article images | [`routes/article/order-images.md`](routes/article/order-images.md) |
| `GET` | `/article/{sku}/optionslist` | `sku:string` | `200 { options: { id, articleId, sku, … }[] }` | Get options list for an article | [`routes/article/_sku/optionslist.get.md`](routes/article/_sku/optionslist.get.md) |
| `PUT` | `/article/{sku}/optionslist` | `sku:string, body: { options: { articleId, sku, count, … }[] }` | `200 { options: object[] }` | Set options list for an article | [`routes/article/_sku/optionslist.put.md`](routes/article/_sku/optionslist.put.md) |
| `PUT` | `/article/{sku}/prices` | `sku:string, body: { prices: { price, tax, minmultiplier }[] }` | `200 { article: ArticleDetail }` | Replace POS prices by SKU | [`routes/article/set-prices.md`](routes/article/set-prices.md) |
| `POST` | `/article/{sku}/print` | `sku:string, body: { templateId, printerId, quantity }` | `200` | Print article label | [`routes/article/_sku/print.md`](routes/article/_sku/print.md) |
| `GET` | `/article/{sku}/property` | `sku:string` | `200 { properties: ArticleProperty[] }` | List article properties | [`routes/article/_sku/property.get.md`](routes/article/_sku/property.get.md) |
| `PUT` | `/article/{sku}/property` | `sku:string, body: { properties: { propertyId, value, sort }[] }` | `200 { properties: ArticleProperty[] }` | Bulk set article properties | [`routes/article/_sku/property.put.md`](routes/article/_sku/property.put.md) |
| `DELETE` | `/article/{sku}/property/{propertyId}` | `sku:string, propertyId:integer` | `200` | Delete article property | [`routes/article/_sku/property/_propertyId.md`](routes/article/_sku/property/_propertyId.md) |
| `GET` | `/article/{sku}/supplier` | `sku:string` | `200 { suppliers: ArticleSupplier[] }` | List article suppliers | [`routes/article/supplier/list.md`](routes/article/supplier/list.md) |
| `POST` | `/article/{sku}/supplier` | `sku:string, body: { entityId, sku, pu, … }` | `201 { supplier: ArticleSupplier }` | Assign supplier to article | [`routes/article/supplier/create.md`](routes/article/supplier/create.md) |
| `PUT` | `/article/{sku}/supplier/order` | `sku:string, body: { ids: integer[] }` | `200 { ids: integer[] }` | Reorder article suppliers | [`routes/article/supplier/order.md`](routes/article/supplier/order.md) |
| `DELETE` | `/article/{sku}/supplier/{id}` | `sku:string, id:integer` | `204 void` | Remove supplier assignment from article | [`routes/article/supplier/delete.md`](routes/article/supplier/delete.md) |
| `PATCH` | `/article/{sku}/supplier/{id}` | `sku:string, id:integer, body: { sku, pu, stock, … }` | `200 { supplier: ArticleSupplier }` | Update article supplier assignment | [`routes/article/supplier/update.md`](routes/article/supplier/update.md) |
| `PUT` | `/article/{sku}/track_stock` | `sku:string, body: { track_stock: boolean }` | `200 { article: ArticleDetail }` | Set article track_stock flag by SKU | [`routes/article/_sku/track_stock.md`](routes/article/_sku/track_stock.md) |
| `PUT` | `/article/{sku}/weight` | `sku:string, body: { weight: number }` | `200 { article: ArticleDetail }` | Set article weight by SKU | [`routes/article/set-weight.md`](routes/article/set-weight.md) |
| `GET` | `/articleIndex` | — | `200 { articles: { id, sku, name, … }[] }` | List article search index | [`routes/articleIndex.md`](routes/articleIndex.md) |
| `GET` | `/attributename` | — | `200 { attributenames: Attributename[] }` | List attribute names | [`routes/attributename.get.md`](routes/attributename.get.md) |
| `POST` | `/attributename` | `body: { name, sort, exclusive }` | `201 { attributename: Attributename }` | Create attribute name | [`routes/attributename.post.md`](routes/attributename.post.md) |
| `PUT` | `/attributename/order` | `body: { attributenameIds: integer[] }` | `200` | Reorder attribute names | [`routes/attributename/order.md`](routes/attributename/order.md) |
| `GET` | `/attributename/{attributeId}/value` | `attributeId:integer` | `200 { values: AttributeValue[] }` | List values for an attribute | [`routes/attributename/_attributeId/value.get.md`](routes/attributename/_attributeId/value.get.md) |
| `POST` | `/attributename/{attributeId}/value` | `attributeId:integer, body: { value, sort }` | `201 { attributevalue: AttributeValue }` | Create value for an attribute | [`routes/attributename/_attributeId/value.post.md`](routes/attributename/_attributeId/value.post.md) |
| `PUT` | `/attributename/{attributeId}/value/order` | `attributeId:integer, body: { attributevalueIds: integer[] }` | `200` | Reorder values of an attribute | [`routes/attributename/_attributeId/value/order.md`](routes/attributename/_attributeId/value/order.md) |
| `DELETE` | `/attributename/{id}` | `id:integer` | `200` | Delete attribute name | [`routes/attributename/_id.delete.md`](routes/attributename/_id.delete.md) |
| `PATCH` | `/attributename/{id}` | `id:integer, body: { name, sort, exclusive }` | `200 { attributename: Attributename }` | Update attribute name | [`routes/attributename/_id.patch.md`](routes/attributename/_id.patch.md) |
| `DELETE` | `/attributevalue/{id}` | `id:integer` | `200` | Delete attribute value | [`routes/attributevalue/_id.delete.md`](routes/attributevalue/_id.delete.md) |
| `PATCH` | `/attributevalue/{id}` | `id:integer, body: { value, sort }` | `200 { attributevalue: AttributeValue }` | Update attribute value | [`routes/attributevalue/_id.patch.md`](routes/attributevalue/_id.patch.md) |
| `POST` | `/auth/login` | `body: { idToken: string }` | `200 { message, user }` | Login with Google ID token | [`routes/auth/login.md`](routes/auth/login.md) |
| `POST` | `/auth/logout` | — | `200 { message: string }` | Logout | [`routes/auth/logout.md`](routes/auth/logout.md) |
| `GET` | `/auth/me` | — | `200 { user, googleClientId }` | Current user | [`routes/auth/me.md`](routes/auth/me.md) |
| `PUT` | `/auth/selected-company` | `body: { companyId: integer }` | `200 { user: User }` | Set selected company | [`routes/auth/selected-company.md`](routes/auth/selected-company.md) |
| `GET` | `/brand` | — | `200 { brands: Brand[] }` | List brands | [`routes/brand/list.md`](routes/brand/list.md) |
| `PUT` | `/brand` | `body: { name, gsprpinfo }` | `200 { brand: Brand }` | Upsert brand | [`routes/brand/upsert.md`](routes/brand/upsert.md) |
| `DELETE` | `/brand/{id}` | `id:integer` | `204 void` | Delete brand | [`routes/brand/delete.md`](routes/brand/delete.md) |
| `POST` | `/category` | `body: { parentId, name }` | `201 { category: Category }` | Create category | [`routes/category/create.md`](routes/category/create.md) |
| `GET` | `/category/export.zip` | — | `200 zip` | Export categories as ZIP | [`routes/category/export.zip.md`](routes/category/export.zip.md) |
| `PUT` | `/category/order` | `body: { parentId, categoryIds }` | `200 { categoryIds: integer[] }` | Reorder sibling categories | [`routes/category/order.md`](routes/category/order.md) |
| `GET` | `/category/tree` | — | `200 { languageId, tree, articles }` | Category tree | [`routes/category/tree.md`](routes/category/tree.md) |
| `DELETE` | `/category/{id}` | `id:integer` | `204 void` | Delete category | [`routes/category/delete.md`](routes/category/delete.md) |
| `PATCH` | `/category/{id}` | `id:integer, body: { name, parentId }` | `200 { category: { id, name, slug, … } }` | Update category (rename or move) | [`routes/category/update.md`](routes/category/update.md) |
| `GET` | `/category/{id}/articles` | `id:integer` | `200 { articles: { id, sku, name, … }[] }` | Articles in category | [`routes/category/articles.md`](routes/category/articles.md) |
| `DELETE` | `/category/{id}/image` | `id:integer` | `200 { category: { id, thumbW, thumbH, … } }` | Delete category image | [`routes/category/delete-image.md`](routes/category/delete-image.md) |
| `POST` | `/category/{id}/image` | `id:integer, image/avif` | `201 { category: { id, thumbW, thumbH, … } }` | Add category image | [`routes/category/add-image.md`](routes/category/add-image.md) |
| `GET` | `/company` | — | `200 { companies: Company[] }` | List companies | [`routes/company/list.md`](routes/company/list.md) |
| `POST` | `/company` | `body: { iso639, name, taxrate, … }` | `201 { company, user }` | Create company | [`routes/company/create.md`](routes/company/create.md) |
| `GET` | `/company/setting` | — | `200 { settings: CompanySetting[] }` | List company settings | [`routes/companysetting/list.md`](routes/companysetting/list.md) |
| `GET` | `/company/setting/{key}` | `key:string` | `200 { setting: CompanySetting }` | Get company setting by key | [`routes/companysetting/get.md`](routes/companysetting/get.md) |
| `PUT` | `/company/setting/{key}` | `key:string, body: { value, frontend }` | `200 { setting: CompanySetting }` | Upsert company setting | [`routes/companysetting/set.md`](routes/companysetting/set.md) |
| `DELETE` | `/company/{id}` | `id:integer` | `204 void` | Delete company | [`routes/company/delete.md`](routes/company/delete.md) |
| `PUT` | `/company/{id}` | `id:integer, body: { iso639, name, taxrate, … }` | `200 { company: Company }` | Update company | [`routes/company/update.md`](routes/company/update.md) |
| `GET` | `/customergroup` | — | `200 { customergroups: CustomergroupListItem[] }` | List customer groups | [`routes/customergroup/list.md`](routes/customergroup/list.md) |
| `POST` | `/customergroup` | `body: { name: string }` | `201 { customergroup: Customergroup }` | Create customer group | [`routes/customergroup/create.md`](routes/customergroup/create.md) |
| `PUT` | `/customergroup/order` | `body: { customergroupIds: integer[] }` | `200 { customergroupIds: integer[] }` | Reorder customer groups | [`routes/customergroup/order.md`](routes/customergroup/order.md) |
| `DELETE` | `/customergroup/{id}` | `id:integer` | `204 void` | Delete customer group | [`routes/customergroup/delete.md`](routes/customergroup/delete.md) |
| `PATCH` | `/customergroup/{id}` | `id:integer, body: { name: string }` | `200 { customergroup: Customergroup }` | Update customer group | [`routes/customergroup/update.md`](routes/customergroup/update.md) |
| `GET` | `/delivery` | — | `200 { deliveries: DeliverySummary[] }` | List deliveries | [`routes/delivery/list.md`](routes/delivery/list.md) |
| `POST` | `/delivery` | `body: { externalNumber, date, supplierId, … }` | `201 { delivery: Delivery }` | Create delivery | [`routes/delivery/create.md`](routes/delivery/create.md) |
| `POST` | `/delivery/parse` | `form: { file }` | `200 SSE` | Parse document (SSE) | [`routes/delivery/parse.md`](routes/delivery/parse.md) |
| `DELETE` | `/delivery/{id}` | `id:integer` | `204 void` | Delete delivery | [`routes/delivery/delete.md`](routes/delivery/delete.md) |
| `GET` | `/delivery/{id}` | `id:integer` | `200 { delivery: Delivery }` | Get delivery | [`routes/delivery/get.md`](routes/delivery/get.md) |
| `PATCH` | `/delivery/{id}` | `id:integer, body: { externalNumber, date, supplierId, … }` | `200 { delivery: Delivery }` | Update delivery | [`routes/delivery/update.md`](routes/delivery/update.md) |
| `GET` | `/delivery/{id}/file/{fileId}` | `id:integer, fileId:integer, ?download:boolean` | `200 string` | Get delivery attachment file content | [`routes/delivery/file.md`](routes/delivery/file.md) |
| `GET` | `/entity` | `?customergroupId:integer, ?role:string` | `200 { entities: Entity[] }` | List entities | [`routes/entity/list.md`](routes/entity/list.md) |
| `POST` | `/entity` | `body: { customergroupId, name, firstName, … }` | `201 { entity: Entity }` | Create entity | [`routes/entity/create.md`](routes/entity/create.md) |
| `GET` | `/entity/next-number` | — | `200 { number: string }` | Peek next customer number | [`routes/entity/next-number.md`](routes/entity/next-number.md) |
| `GET` | `/entity/{entityId}/address` | `entityId:integer` | `200 { addresses: EntityAddress[] }` | List entity addresses | [`routes/entity/address/list.md`](routes/entity/address/list.md) |
| `POST` | `/entity/{entityId}/address` | `entityId:integer, body: { addressType, firstName, name, … }` | `201 { address: EntityAddress }` | Create entity address | [`routes/entity/address/create.md`](routes/entity/address/create.md) |
| `DELETE` | `/entity/{entityId}/address/{id}` | `entityId:integer, id:integer` | `204 void` | Delete entity address | [`routes/entity/address/delete.md`](routes/entity/address/delete.md) |
| `PATCH` | `/entity/{entityId}/address/{id}` | `entityId:integer, id:integer, body: { addressType, firstName, name, … }` | `200 { address: EntityAddress }` | Update entity address | [`routes/entity/address/update.md`](routes/entity/address/update.md) |
| `GET` | `/entity/{entityId}/contact` | `entityId:integer` | `200 { contacts: EntityContact[] }` | List entity contacts | [`routes/entity/contact/list.md`](routes/entity/contact/list.md) |
| `POST` | `/entity/{entityId}/contact` | `entityId:integer, body: { contactType, contact }` | `201 { contact: EntityContact }` | Create entity contact | [`routes/entity/contact/create.md`](routes/entity/contact/create.md) |
| `DELETE` | `/entity/{entityId}/contact/{id}` | `entityId:integer, id:integer` | `204 void` | Delete entity contact | [`routes/entity/contact/delete.md`](routes/entity/contact/delete.md) |
| `PATCH` | `/entity/{entityId}/contact/{id}` | `entityId:integer, id:integer, body: { contactType, contact }` | `200 { contact: EntityContact }` | Update entity contact | [`routes/entity/contact/update.md`](routes/entity/contact/update.md) |
| `DELETE` | `/entity/{id}` | `id:integer` | `204 void` | Delete entity | [`routes/entity/delete.md`](routes/entity/delete.md) |
| `GET` | `/entity/{id}` | `id:integer` | `200 { entity: Entity }` | Get entity | [`routes/entity/get.md`](routes/entity/get.md) |
| `PATCH` | `/entity/{id}` | `id:integer, body: { name, firstName, number }` | `200 { entity: Entity }` | Update entity | [`routes/entity/update.md`](routes/entity/update.md) |
| `GET` | `/events` | — | `200 SSE` | Subscribe to update signals (SSE) | [`routes/events.md`](routes/events.md) |
| `GET` | `/health` | — | `200 { ok: boolean }` | Database health check | [`routes/health.md`](routes/health.md) |
| `GET` | `/image/{fullHash}` | `fullHash:string` | `200 avif` | Full image by hash | [`routes/image/get.md`](routes/image/get.md) |
| `GET` | `/labeltemplate` | — | `200 { labeltemplates: LabelTemplate[] }` | List label templates | [`routes/labeltemplate/list.md`](routes/labeltemplate/list.md) |
| `POST` | `/labeltemplate` | `body: LabelTemplateCreateInput` | `201 { labeltemplate: LabelTemplate }` | Create label template | [`routes/labeltemplate/create.md`](routes/labeltemplate/create.md) |
| `DELETE` | `/labeltemplate/{id}` | `id:integer` | `204 void` | Delete label template | [`routes/labeltemplate/delete.md`](routes/labeltemplate/delete.md) |
| `GET` | `/labeltemplate/{id}` | `id:integer` | `200 { labeltemplate: LabelTemplate }` | Get label template by ID | [`routes/labeltemplate/get.md`](routes/labeltemplate/get.md) |
| `PATCH` | `/labeltemplate/{id}` | `id:integer, body: LabelTemplateUpdateInput` | `200 { labeltemplate: LabelTemplate }` | Update label template (partial) | [`routes/labeltemplate/update.md`](routes/labeltemplate/update.md) |
| `PUT` | `/labeltemplate/{id}` | `id:integer, body: LabelTemplateUpdateInput` | `200 { labeltemplate: LabelTemplate }` | Update label template | [`routes/labeltemplate/update.md`](routes/labeltemplate/update.md) |
| `GET` | `/ledger` | `?manifestarticle_id:integer, ?sku:string, ?transaction_type:string` | `200 { ledger: LedgerEntry[] }` | Query ledger by manifest article ID or SKU | [`routes/ledger.get.md`](routes/ledger.get.md) |
| `POST` | `/ledger` | `body: LedgerEntryInput` | `201 { ledger: LedgerEntry }` | Add ledger entry | [`routes/ledger.post.md`](routes/ledger.post.md) |
| `GET` | `/ledger/manifestarticle/{manifestarticleId}` | `manifestarticleId:integer, ?transaction_type:string` | `200 { ledger: LedgerEntry[] }` | Query ledger by manifest article ID | [`routes/ledger/manifestarticle/_manifestarticleId.md`](routes/ledger/manifestarticle/_manifestarticleId.md) |
| `GET` | `/ledger/sku/{sku}` | `sku:string, ?transaction_type:string` | `200 { ledger: LedgerEntry[] }` | Query ledger by article SKU | [`routes/ledger/sku/_sku.md`](routes/ledger/sku/_sku.md) |
| `GET` | `/number` | — | `200 { numbers: NumberSequence[] }` | List number sequences | [`routes/number/list.md`](routes/number/list.md) |
| `PATCH` | `/number/{id}` | `id:integer, body: { prefix, padding, next }` | `200 { number: NumberSequence }` | Update number sequence | [`routes/number/update.md`](routes/number/update.md) |
| `GET` | `/order` | — | `200 { orders: OrderSummary[] }` | List orders | [`routes/order/list.md`](routes/order/list.md) |
| `POST` | `/order` | `body: { number, date, entityId, … }` | `201 { order: Order }` | Create order | [`routes/order/create.md`](routes/order/create.md) |
| `DELETE` | `/order/{id}` | `id:integer` | `204 void` | Delete order | [`routes/order/delete.md`](routes/order/delete.md) |
| `GET` | `/order/{id}` | `id:integer` | `200 { order: Order }` | Get order | [`routes/order/get.md`](routes/order/get.md) |
| `PATCH` | `/order/{id}` | `id:integer, body: { number, date, entityId, … }` | `200 { order: Order }` | Update order | [`routes/order/update.md`](routes/order/update.md) |
| `GET` | `/posterminal` | — | `200 { posterminals: Posterminal[] }` | List POS terminals | [`routes/posterminal/list.md`](routes/posterminal/list.md) |
| `PUT` | `/posterminal` | `body: { name: string }` | `200 { posterminal: Posterminal }` | Create POS terminal | [`routes/posterminal/upsert.md`](routes/posterminal/upsert.md) |
| `DELETE` | `/posterminal/{id}` | `id:integer` | `204 void` | Delete POS terminal | [`routes/posterminal/delete.md`](routes/posterminal/delete.md) |
| `DELETE` | `/posterminal/{id}/all` | `id:integer` | `200 { id, categoryIds }` | Disable all categories on terminal | [`routes/posterminal/setOffAll.md`](routes/posterminal/setOffAll.md) |
| `PUT` | `/posterminal/{id}/all` | `id:integer` | `200 { id, categoryIds }` | Enable all categories on terminal | [`routes/posterminal/setOnAll.md`](routes/posterminal/setOnAll.md) |
| `DELETE` | `/posterminal/{id}/category/{categoryId}` | `id:integer, categoryId:integer` | `200 { id, categoryIds }` | Disable category on terminal | [`routes/posterminal/setOff.md`](routes/posterminal/setOff.md) |
| `PUT` | `/posterminal/{id}/category/{categoryId}` | `id:integer, categoryId:integer` | `200 { id, categoryIds }` | Enable category on terminal | [`routes/posterminal/setOn.md`](routes/posterminal/setOn.md) |
| `GET` | `/printer` | — | `200 { printers: Printer[] }` | List printers | [`routes/printer/list.md`](routes/printer/list.md) |
| `POST` | `/printer` | `body: PrinterCreateInput` | `201 { printer: Printer }` | Create printer | [`routes/printer/create.md`](routes/printer/create.md) |
| `PUT` | `/printer/order` | `body: { printerIds: integer[] }` | `200 { printerIds: integer[] }` | Reorder printers | [`routes/printer/order.md`](routes/printer/order.md) |
| `DELETE` | `/printer/{id}` | `id:integer` | `204 void` | Delete printer | [`routes/printer/delete.md`](routes/printer/delete.md) |
| `GET` | `/printer/{id}` | `id:integer` | `200 { printer: Printer }` | Get printer by ID | [`routes/printer/get.md`](routes/printer/get.md) |
| `PATCH` | `/printer/{id}` | `id:integer, body: PrinterUpdateInput` | `200 { printer: Printer }` | Update printer (partial) | [`routes/printer/update.md`](routes/printer/update.md) |
| `PUT` | `/printer/{id}` | `id:integer, body: PrinterUpdateInput` | `200 { printer: Printer }` | Update printer | [`routes/printer/_id.put.md`](routes/printer/_id.put.md) |
| `GET` | `/propertyname` | — | `200 { propertynames: Propertyname[] }` | List property names | [`routes/propertyname.get.md`](routes/propertyname.get.md) |
| `POST` | `/propertyname` | `body: { name, sort, exclusive }` | `201 { propertyname: Propertyname }` | Create property name | [`routes/propertyname.post.md`](routes/propertyname.post.md) |
| `PUT` | `/propertyname/order` | `body: { propertynameIds: integer[] }` | `200` | Reorder property names | [`routes/propertyname/order.md`](routes/propertyname/order.md) |
| `DELETE` | `/propertyname/{id}` | `id:integer` | `200` | Delete property name | [`routes/propertyname/_id.delete.md`](routes/propertyname/_id.delete.md) |
| `PATCH` | `/propertyname/{id}` | `id:integer, body: { name, sort, exclusive }` | `200 { propertyname: Propertyname }` | Update property name | [`routes/propertyname/_id.patch.md`](routes/propertyname/_id.patch.md) |
| `GET` | `/quote` | — | `200 { quotes: QuoteSummary[] }` | List quotes | [`routes/quote/list.md`](routes/quote/list.md) |
| `POST` | `/quote` | `body: { number, date, entityId, … }` | `201 { quote: Quote }` | Create quote | [`routes/quote/create.md`](routes/quote/create.md) |
| `DELETE` | `/quote/{id}` | `id:integer` | `204 void` | Delete quote | [`routes/quote/delete.md`](routes/quote/delete.md) |
| `GET` | `/quote/{id}` | `id:integer` | `200 { quote: Quote }` | Get quote | [`routes/quote/get.md`](routes/quote/get.md) |
| `PATCH` | `/quote/{id}` | `id:integer, body: { number, date, entityId, … }` | `200 { quote: Quote }` | Update quote | [`routes/quote/update.md`](routes/quote/update.md) |
| `POST` | `/serper/image` | `body: { url: string }` | `200 avif` | Crawl image from URL, crop white/transparency borders, and return AVIF | [`routes/serper/image.md`](routes/serper/image.md) |
| `POST` | `/serper/images` | `body: { q, gl, hl, … }` | `200 SSE` | Search images via Serper.dev and stream WebP thumbnails (SSE) | [`routes/serper/images.md`](routes/serper/images.md) |
| `POST` | `/stock/adjust` | `body: StockAdjustmentInput` | `201 StockAdjustmentResult` | Adjust stock manually | [`routes/stock/adjust.md`](routes/stock/adjust.md) |
| `GET` | `/supplier` | — | `200 { suppliers: Supplier[] }` | List suppliers | [`routes/supplier/list.md`](routes/supplier/list.md) |
| `POST` | `/supplier` | `body: { name: string }` | `201 { supplier: Supplier }` | Create supplier | [`routes/supplier/create.md`](routes/supplier/create.md) |
| `DELETE` | `/supplier/{id}` | `id:integer` | `204 void` | Delete supplier | [`routes/supplier/delete.md`](routes/supplier/delete.md) |
| `PATCH` | `/supplier/{id}` | `id:integer, body: { name: string }` | `200 { supplier: Supplier }` | Update supplier | [`routes/supplier/update.md`](routes/supplier/update.md) |
| `GET` | `/supplier/{id}/articles` | `id:integer, ?skus` | `200 { articles: SupplierArticle[] }` | List supplier articles by SKUs | [`routes/supplier/articles.get.md`](routes/supplier/articles.get.md) |
| `POST` | `/supplier/{id}/articles` | `id:integer, body: { skus: string[] }` | `200 { articles: SupplierArticle[] }` | Query supplier articles by SKUs (bulk) | [`routes/supplier/articles.post.md`](routes/supplier/articles.post.md) |
| `GET` | `/thumb/{fullHash}` | `fullHash:string` | `200 avif` | Thumb image by hash | [`routes/thumb/get.md`](routes/thumb/get.md) |
| `DELETE` | `/unit` | `?si:string` | `204 void` | Delete unit | [`routes/unit/delete.md`](routes/unit/delete.md) |
| `GET` | `/unit` | — | `200 { units: Unit[] }` | List units | [`routes/unit/list.md`](routes/unit/list.md) |
| `PUT` | `/unit` | `body: { si, basesi, basesifactor, … }` | `200 { unit: Unit }` | Upsert unit | [`routes/unit/upsert.md`](routes/unit/upsert.md) |
| `GET` | `/v1/category` | `?lastChangedCategory:string, ?limit:integer` | `200 object[]` | JTL-POS category synchronization | [`routes/v1/category.md`](routes/v1/category.md) |
| `GET` | `/v1/client` | `?authCode:string, ?name:string` | `200 { authCode, authToken, certificateFingerprint, … }` | POS discovery and pairing | [`routes/pos/client.md`](routes/pos/client.md) |
| `GET` | `/v1/init` | `?lastChangedProduct:integer, ?lastChangedCategory:integer, ?lastChangedCustomer:integer, ?lastChangedCustomerGroup:integer, ?lastChangedCompositeProduct:integer, ?lastChangedDeletedEntity:integer` | `200 { version, product_count, category_count, … }` | POS sync initialization counts | [`routes/pos/init.md`](routes/pos/init.md) |
| `GET` | `/v1/product` | `?lastChangedProduct:string, ?limit:integer` | `200 object[]` | JTL-POS product catalog synchronization | [`routes/v1/product.md`](routes/v1/product.md) |
| `GET` | `/warehouse` | — | `200 { warehouses: Warehouse[] }` | List warehouses | [`routes/warehouse/list.md`](routes/warehouse/list.md) |
| `POST` | `/warehouse` | `body: WarehouseCreateInput` | `201 { warehouse: Warehouse }` | Create warehouse | [`routes/warehouse/create.md`](routes/warehouse/create.md) |
| `PUT` | `/warehouse/order` | `body: { warehouseIds: integer[] }` | `200 { warehouseIds: integer[] }` | Reorder warehouses | [`routes/warehouse/order.md`](routes/warehouse/order.md) |
| `DELETE` | `/warehouse/{id}` | `id:integer` | `204 void` | Delete warehouse | [`routes/warehouse/delete.md`](routes/warehouse/delete.md) |
| `GET` | `/warehouse/{id}` | `id:integer` | `200 { warehouse: Warehouse }` | Get warehouse by ID | [`routes/warehouse/get.md`](routes/warehouse/get.md) |
| `PATCH` | `/warehouse/{id}` | `id:integer, body: WarehouseUpdateInput` | `200 { warehouse: Warehouse }` | Update warehouse (partial) | [`routes/warehouse/update.md`](routes/warehouse/update.md) |
| `PUT` | `/warehouse/{id}` | `id:integer, body: WarehouseUpdateInput` | `200 { warehouse: Warehouse }` | Update warehouse | [`routes/warehouse/_id.put.md`](routes/warehouse/_id.put.md) |

View File

@@ -393,7 +393,7 @@ function renderReadme(operations) {
lines.push(`| [\`routes/${op.file}\`](routes/${op.file}) | \`${op.method.toUpperCase()} ${op.path}\` |`);
}
lines.push('');
lines.push('Machine-readable spec: [`openapi.json`](openapi.json).');
lines.push('Route overview: [`routes.md`](routes.md).');
lines.push('');
lines.push('## Signals (SSE)');
lines.push('');
@@ -436,13 +436,127 @@ function renderReadme(operations) {
return lines.join('\n');
}
function summarizeSchemaCompact(schema) {
if (!schema) return '';
if (schema.$ref) return schemaRefName(schema);
if (schema.type === 'array') {
const item = summarizeSchemaCompact(schema.items) || 'any';
return `${item}[]`;
}
if (schema.type === 'object' || schema.properties) {
const props = Object.keys(schema.properties || {});
if (props.length === 0) return 'object';
if (props.length === 1) {
const k = props[0];
const v = summarizeSchemaCompact(schema.properties[k]);
return v ? `{ ${k}: ${v} }` : `{ ${k} }`;
}
if (props.length <= 3) return `{ ${props.join(', ')} }`;
return `{ ${props.slice(0, 3).join(', ')}, … }`;
}
if (schema.type) return schema.type;
return '';
}
function summarizeOperationParams(operation) {
const parts = [];
const params = operation.parameters || [];
for (const p of params) {
if (p.in === 'header') continue;
const prefix = p.in === 'query' ? '?' : '';
const type = p.schema?.type ? `:${p.schema.type}` : '';
parts.push(`${prefix}${p.name}${type}`);
}
if (operation.requestBody?.content) {
const content = operation.requestBody.content;
if (content['application/json']?.schema) {
const s = content['application/json'].schema;
const ref = schemaRefName(s);
if (ref) {
parts.push(`body: ${ref}`);
} else {
const sub = summarizeSchemaCompact(s);
parts.push(sub ? `body: ${sub}` : 'body: json');
}
} else if (content['multipart/form-data']) {
const props = Object.keys(content['multipart/form-data'].schema?.properties || {});
parts.push(props.length ? `form: { ${props.join(', ')} }` : 'form-data');
} else {
const ct = Object.keys(content)[0];
parts.push(ct);
}
}
return parts.length ? parts.join(', ') : '—';
}
function summarizeOperationReturns(operation) {
const responses = operation.responses || {};
const successCodes = Object.keys(responses)
.filter((c) => c.startsWith('2'))
.sort();
if (!successCodes.length) return '—';
const parts = [];
for (const code of successCodes) {
const resp = responses[code];
if (code === '204') {
parts.push('204 void');
continue;
}
if (!resp?.content) {
parts.push(code);
continue;
}
const ct = Object.keys(resp.content)[0];
if (ct === 'text/event-stream') {
parts.push(`${code} SSE`);
continue;
}
if (ct.startsWith('image/')) {
parts.push(`${code} ${ct.split('/')[1]}`);
continue;
}
if (ct === 'application/zip') {
parts.push(`${code} zip`);
continue;
}
const s = resp.content[ct]?.schema;
const sub = summarizeSchemaCompact(s);
if (sub) {
parts.push(`${code} ${sub}`);
} else {
parts.push(code);
}
}
return parts.join(', ');
}
function renderRoutesOverview(operations) {
const lines = [];
lines.push('# Dash API Routes Overview');
lines.push('');
lines.push(`Overview of all ${operations.length} API endpoints across all modules.`);
lines.push('');
lines.push('| Method | Route | Params | Return Type | Summary | Docs |');
lines.push('|:---|:---|:---|:---|:---|:---|');
for (const op of operations) {
const method = op.method.toUpperCase();
const summary = (op.summary || '').replace(/[\r\n|]+/g, ' ').trim();
const paramsStr = op.params === '—' ? '—' : `\`${op.params.replace(/\|/g, '\\|')}\``;
const returnsStr = op.returns === '—' ? '—' : `\`${op.returns.replace(/\|/g, '\\|')}\``;
lines.push(`| \`${method}\` | \`${op.path}\` | ${paramsStr} | ${returnsStr} | ${summary} | [\`routes/${op.file}\`](routes/${op.file}) |`);
}
lines.push('');
return lines.join('\n');
}
// --- generate ---
ensureDir(docsDir);
rmDirContents(routesDocsDir);
ensureDir(routesDocsDir);
fs.writeFileSync(path.join(docsDir, 'openapi.json'), JSON.stringify(spec, null, 2) + '\n');
const operations = [];
const paths = Object.keys(spec.paths || {}).sort();
@@ -455,10 +569,19 @@ for (const apiPath of paths) {
const absFile = path.join(routesDocsDir, relFile);
ensureDir(path.dirname(absFile));
fs.writeFileSync(absFile, renderOperation(apiPath, method, operation));
operations.push({ path: apiPath, method, file: relFile.replace(/\\/g, '/') });
operations.push({
path: apiPath,
method,
file: relFile.replace(/\\/g, '/'),
summary: operation.summary || '',
tags: operation.tags || [],
params: summarizeOperationParams(operation),
returns: summarizeOperationReturns(operation),
});
}
}
fs.writeFileSync(path.join(docsDir, 'routes.md'), renderRoutesOverview(operations));
fs.writeFileSync(path.join(docsDir, 'README.md'), renderReadme(operations));
console.log(`Wrote ${operations.length} route docs + openapi.json → docs/api/`);
console.log(`Wrote ${operations.length} route docs + routes.md → docs/api/`);