From 7f3558b4cbcde2ae43a74f4d98e62047fb914dec Mon Sep 17 00:00:00 2001 From: seb Date: Tue, 29 Sep 2026 16:16:03 +0200 Subject: [PATCH] u --- docs/api/README.md | 20 + docs/api/openapi.json | 1116 +++++++++++++++++ .../routes/article/_sku/attributevalue.get.md | 38 + .../routes/article/_sku/attributevalue.put.md | 48 + docs/api/routes/article/_sku/property.get.md | 36 + docs/api/routes/article/_sku/property.put.md | 50 + .../article/_sku/property/_propertyId.md | 22 + docs/api/routes/attributename.get.md | 36 + docs/api/routes/attributename.post.md | 44 + .../attributename/_attributeId/value.get.md | 34 + .../attributename/_attributeId/value.post.md | 41 + .../attributename/_attributeId/value/order.md | 31 + docs/api/routes/attributename/_id.delete.md | 21 + docs/api/routes/attributename/_id.patch.md | 50 + docs/api/routes/attributename/order.md | 25 + docs/api/routes/attributevalue/_id.delete.md | 21 + docs/api/routes/attributevalue/_id.patch.md | 41 + docs/api/routes/propertyname.get.md | 33 + docs/api/routes/propertyname.post.md | 40 + docs/api/routes/propertyname/_id.delete.md | 21 + docs/api/routes/propertyname/_id.patch.md | 40 + docs/api/routes/propertyname/order.md | 31 + src/api/openapi.js | 74 ++ src/api/routes/articleattributevalue.js | 157 +++ src/api/routes/articleproperty.js | 201 +++ src/api/routes/attributename.js | 235 ++++ src/api/routes/attributevalue.js | 251 ++++ src/api/routes/propertyname.js | 245 ++++ src/db/articleattributevalue.js | 11 + src/db/articleproperty.js | 16 + src/db/attributename.js | 26 + src/db/attributevalue.js | 26 + src/db/propertyname.js | 26 + src/db/store.js | 5 + 34 files changed, 3112 insertions(+) create mode 100644 docs/api/routes/article/_sku/attributevalue.get.md create mode 100644 docs/api/routes/article/_sku/attributevalue.put.md create mode 100644 docs/api/routes/article/_sku/property.get.md create mode 100644 docs/api/routes/article/_sku/property.put.md create mode 100644 docs/api/routes/article/_sku/property/_propertyId.md create mode 100644 docs/api/routes/attributename.get.md create mode 100644 docs/api/routes/attributename.post.md create mode 100644 docs/api/routes/attributename/_attributeId/value.get.md create mode 100644 docs/api/routes/attributename/_attributeId/value.post.md create mode 100644 docs/api/routes/attributename/_attributeId/value/order.md create mode 100644 docs/api/routes/attributename/_id.delete.md create mode 100644 docs/api/routes/attributename/_id.patch.md create mode 100644 docs/api/routes/attributename/order.md create mode 100644 docs/api/routes/attributevalue/_id.delete.md create mode 100644 docs/api/routes/attributevalue/_id.patch.md create mode 100644 docs/api/routes/propertyname.get.md create mode 100644 docs/api/routes/propertyname.post.md create mode 100644 docs/api/routes/propertyname/_id.delete.md create mode 100644 docs/api/routes/propertyname/_id.patch.md create mode 100644 docs/api/routes/propertyname/order.md create mode 100644 src/api/routes/articleattributevalue.js create mode 100644 src/api/routes/articleproperty.js create mode 100644 src/api/routes/attributename.js create mode 100644 src/api/routes/attributevalue.js create mode 100644 src/api/routes/propertyname.js create mode 100644 src/db/articleattributevalue.js create mode 100644 src/db/articleproperty.js create mode 100644 src/db/attributename.js create mode 100644 src/db/attributevalue.js create mode 100644 src/db/propertyname.js diff --git a/docs/api/README.md b/docs/api/README.md index 0f65c71..aa940f4 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -37,6 +37,8 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route | [`routes/article/delete.md`](routes/article/delete.md) | `DELETE /article/{sku}` | | [`routes/article/get.md`](routes/article/get.md) | `GET /article/{sku}` | | [`routes/article/update.md`](routes/article/update.md) | `PATCH /article/{sku}` | +| [`routes/article/_sku/attributevalue.get.md`](routes/article/_sku/attributevalue.get.md) | `GET /article/{sku}/attributevalue` | +| [`routes/article/_sku/attributevalue.put.md`](routes/article/_sku/attributevalue.put.md) | `PUT /article/{sku}/attributevalue` | | [`routes/article/set-brand.md`](routes/article/set-brand.md) | `PUT /article/{sku}/brand` | | [`routes/article/set-categories.md`](routes/article/set-categories.md) | `PUT /article/{sku}/categories` | | [`routes/article/set-eans.md`](routes/article/set-eans.md) | `PUT /article/{sku}/eans` | @@ -46,6 +48,9 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route | [`routes/article/order-images.md`](routes/article/order-images.md) | `PUT /article/{sku}/images/order` | | [`routes/article/set-prices.md`](routes/article/set-prices.md) | `PUT /article/{sku}/prices` | | [`routes/article/_sku/print.md`](routes/article/_sku/print.md) | `POST /article/{sku}/print` | +| [`routes/article/_sku/property.get.md`](routes/article/_sku/property.get.md) | `GET /article/{sku}/property` | +| [`routes/article/_sku/property.put.md`](routes/article/_sku/property.put.md) | `PUT /article/{sku}/property` | +| [`routes/article/_sku/property/_propertyId.md`](routes/article/_sku/property/_propertyId.md) | `DELETE /article/{sku}/property/{propertyId}` | | [`routes/article/supplier/list.md`](routes/article/supplier/list.md) | `GET /article/{sku}/supplier` | | [`routes/article/supplier/create.md`](routes/article/supplier/create.md) | `POST /article/{sku}/supplier` | | [`routes/article/supplier/order.md`](routes/article/supplier/order.md) | `PUT /article/{sku}/supplier/order` | @@ -54,6 +59,16 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route | [`routes/article/_sku/track_stock.md`](routes/article/_sku/track_stock.md) | `PUT /article/{sku}/track_stock` | | [`routes/article/set-weight.md`](routes/article/set-weight.md) | `PUT /article/{sku}/weight` | | [`routes/articleIndex.md`](routes/articleIndex.md) | `GET /articleIndex` | +| [`routes/attributename.get.md`](routes/attributename.get.md) | `GET /attributename` | +| [`routes/attributename.post.md`](routes/attributename.post.md) | `POST /attributename` | +| [`routes/attributename/order.md`](routes/attributename/order.md) | `PUT /attributename/order` | +| [`routes/attributename/_attributeId/value.get.md`](routes/attributename/_attributeId/value.get.md) | `GET /attributename/{attributeId}/value` | +| [`routes/attributename/_attributeId/value.post.md`](routes/attributename/_attributeId/value.post.md) | `POST /attributename/{attributeId}/value` | +| [`routes/attributename/_attributeId/value/order.md`](routes/attributename/_attributeId/value/order.md) | `PUT /attributename/{attributeId}/value/order` | +| [`routes/attributename/_id.delete.md`](routes/attributename/_id.delete.md) | `DELETE /attributename/{id}` | +| [`routes/attributename/_id.patch.md`](routes/attributename/_id.patch.md) | `PATCH /attributename/{id}` | +| [`routes/attributevalue/_id.delete.md`](routes/attributevalue/_id.delete.md) | `DELETE /attributevalue/{id}` | +| [`routes/attributevalue/_id.patch.md`](routes/attributevalue/_id.patch.md) | `PATCH /attributevalue/{id}` | | [`routes/auth/login.md`](routes/auth/login.md) | `POST /auth/login` | | [`routes/auth/logout.md`](routes/auth/logout.md) | `POST /auth/logout` | | [`routes/auth/me.md`](routes/auth/me.md) | `GET /auth/me` | @@ -137,6 +152,11 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route | [`routes/printer/get.md`](routes/printer/get.md) | `GET /printer/{id}` | | [`routes/printer/update.md`](routes/printer/update.md) | `PATCH /printer/{id}` | | [`routes/printer/_id.put.md`](routes/printer/_id.put.md) | `PUT /printer/{id}` | +| [`routes/propertyname.get.md`](routes/propertyname.get.md) | `GET /propertyname` | +| [`routes/propertyname.post.md`](routes/propertyname.post.md) | `POST /propertyname` | +| [`routes/propertyname/order.md`](routes/propertyname/order.md) | `PUT /propertyname/order` | +| [`routes/propertyname/_id.delete.md`](routes/propertyname/_id.delete.md) | `DELETE /propertyname/{id}` | +| [`routes/propertyname/_id.patch.md`](routes/propertyname/_id.patch.md) | `PATCH /propertyname/{id}` | | [`routes/quote/list.md`](routes/quote/list.md) | `GET /quote` | | [`routes/quote/create.md`](routes/quote/create.md) | `POST /quote` | | [`routes/quote/delete.md`](routes/quote/delete.md) | `DELETE /quote/{id}` | diff --git a/docs/api/openapi.json b/docs/api/openapi.json index ec79638..4a14166 100644 --- a/docs/api/openapi.json +++ b/docs/api/openapi.json @@ -2300,6 +2300,239 @@ } } }, + "/propertyname": { + "get": { + "tags": [ + "Article" + ], + "summary": "List property names", + "description": "Lists all property names defined for the selected company, ordered by sort.", + "security": [ + { + "cookieAuth": [] + } + ], + "responses": { + "200": { + "description": "List of property names", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "propertynames": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Propertyname" + } + } + } + } + } + } + }, + "400": { + "description": "No company selected", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "tags": [ + "Article" + ], + "summary": "Create property name", + "description": "Creates a new property name for the selected company.", + "security": [ + { + "cookieAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "sort": { + "type": "integer" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Created property name", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "propertyname": { + "$ref": "#/components/schemas/Propertyname" + } + } + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/propertyname/order": { + "put": { + "tags": [ + "Article" + ], + "summary": "Reorder property names", + "description": "Updates the display sort order of property names for the selected company.", + "security": [ + { + "cookieAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "propertynameIds" + ], + "properties": { + "propertynameIds": { + "type": "array", + "items": { + "type": "integer" + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Reordered successfully" + }, + "400": { + "description": "Bad request" + } + } + } + }, + "/propertyname/{id}": { + "patch": { + "tags": [ + "Article" + ], + "summary": "Update property name", + "description": "Updates name and/or sort for a property name.", + "security": [ + { + "cookieAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "sort": { + "type": "integer" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated property name", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "propertyname": { + "$ref": "#/components/schemas/Propertyname" + } + } + } + } + } + } + } + }, + "delete": { + "tags": [ + "Article" + ], + "summary": "Delete property name", + "description": "Deletes a property name and cascades to article properties.", + "security": [ + { + "cookieAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Deleted successfully" + } + } + } + }, "/printer": { "get": { "tags": [ @@ -9401,6 +9634,462 @@ } } }, + "/attributename/{attributeId}/value": { + "get": { + "tags": [ + "Article" + ], + "summary": "List values for an attribute", + "description": "Lists all values for a specific attribute.", + "security": [ + { + "cookieAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "attributeId", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "List of attribute values", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "values": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AttributeValue" + } + } + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Article" + ], + "summary": "Create value for an attribute", + "description": "Adds a new predefined value to an attribute.", + "security": [ + { + "cookieAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "attributeId", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "string" + }, + "sort": { + "type": "integer" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Created attribute value", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "attributevalue": { + "$ref": "#/components/schemas/AttributeValue" + } + } + } + } + } + } + } + } + }, + "/attributename/{attributeId}/value/order": { + "put": { + "tags": [ + "Article" + ], + "summary": "Reorder values of an attribute", + "description": "Updates the display sort order of values belonging to an attribute.", + "security": [ + { + "cookieAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "attributeId", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "attributevalueIds" + ], + "properties": { + "attributevalueIds": { + "type": "array", + "items": { + "type": "integer" + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Reordered successfully" + } + } + } + }, + "/attributevalue/{id}": { + "patch": { + "tags": [ + "Article" + ], + "summary": "Update attribute value", + "description": "Updates value text and/or sort for an attribute value.", + "security": [ + { + "cookieAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "sort": { + "type": "integer" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated attribute value", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "attributevalue": { + "$ref": "#/components/schemas/AttributeValue" + } + } + } + } + } + } + } + }, + "delete": { + "tags": [ + "Article" + ], + "summary": "Delete attribute value", + "description": "Deletes an attribute value and cascades to article assignments.", + "security": [ + { + "cookieAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Deleted successfully" + } + } + } + }, + "/attributename": { + "get": { + "tags": [ + "Article" + ], + "summary": "List attribute names", + "description": "Lists all attribute names defined for the selected company, including nested values.", + "security": [ + { + "cookieAuth": [] + } + ], + "responses": { + "200": { + "description": "List of attribute names", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "attributenames": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Attributename" + } + } + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Article" + ], + "summary": "Create attribute name", + "description": "Creates an attribute name for the selected company.", + "security": [ + { + "cookieAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "sort": { + "type": "integer" + }, + "exclusive": { + "type": "boolean" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Created attribute name", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "attributename": { + "$ref": "#/components/schemas/Attributename" + } + } + } + } + } + } + } + } + }, + "/attributename/order": { + "put": { + "tags": [ + "Article" + ], + "summary": "Reorder attribute names", + "description": "Updates the display sort order of attribute names for the selected company.", + "security": [ + { + "cookieAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "attributenameIds" + ], + "properties": { + "attributenameIds": { + "type": "array", + "items": { + "type": "integer" + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Reordered successfully" + } + } + } + }, + "/attributename/{id}": { + "patch": { + "tags": [ + "Article" + ], + "summary": "Update attribute name", + "description": "Updates name, sort, and/or exclusive flag for an attribute name.", + "security": [ + { + "cookieAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "sort": { + "type": "integer" + }, + "exclusive": { + "type": "boolean" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated attribute name", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "attributename": { + "$ref": "#/components/schemas/Attributename" + } + } + } + } + } + } + } + }, + "delete": { + "tags": [ + "Article" + ], + "summary": "Delete attribute name", + "description": "Deletes an attribute name and cascades to attribute values and article assignments.", + "security": [ + { + "cookieAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Deleted successfully" + } + } + } + }, "/article/{sku}/supplier": { "get": { "tags": [ @@ -9983,6 +10672,294 @@ } } }, + "/article/{sku}/property": { + "get": { + "tags": [ + "Article" + ], + "summary": "List article properties", + "description": "Fetches all free-form properties set on an article.", + "security": [ + { + "cookieAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "sku", + "required": true, + "schema": { + "type": "string" + }, + "description": "Article SKU" + } + ], + "responses": { + "200": { + "description": "List of properties for this article", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "properties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ArticleProperty" + } + } + } + } + } + } + } + } + }, + "put": { + "tags": [ + "Article" + ], + "summary": "Bulk set article properties", + "description": "Replaces all properties for an article.", + "security": [ + { + "cookieAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "sku", + "required": true, + "schema": { + "type": "string" + }, + "description": "Article SKU" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "properties" + ], + "properties": { + "properties": { + "type": "array", + "items": { + "type": "object", + "required": [ + "propertyId", + "value" + ], + "properties": { + "propertyId": { + "type": "integer" + }, + "value": { + "type": "string" + }, + "sort": { + "type": "integer" + } + } + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated article properties", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "properties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ArticleProperty" + } + } + } + } + } + } + } + } + } + }, + "/article/{sku}/property/{propertyId}": { + "delete": { + "tags": [ + "Article" + ], + "summary": "Delete article property", + "description": "Removes a single property from an article.", + "security": [ + { + "cookieAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "sku", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "propertyId", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Deleted successfully" + } + } + } + }, + "/article/{sku}/attributevalue": { + "get": { + "tags": [ + "Article" + ], + "summary": "List article attribute values", + "description": "Fetches all attribute values assigned to an article.", + "security": [ + { + "cookieAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "sku", + "required": true, + "schema": { + "type": "string" + }, + "description": "Article SKU" + } + ], + "responses": { + "200": { + "description": "List of attribute values assigned to this article", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "attributevalues": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ArticleAttributeValue" + } + } + } + } + } + } + } + } + }, + "put": { + "tags": [ + "Article" + ], + "summary": "Bulk set article attribute values", + "description": "Replaces all attribute values for an article. Enforces exclusive constraint (max one value per exclusive attribute).", + "security": [ + { + "cookieAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "sku", + "required": true, + "schema": { + "type": "string" + }, + "description": "Article SKU" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "attributevalues" + ], + "properties": { + "attributevalues": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "object", + "required": [ + "attributeValueId" + ], + "properties": { + "attributeValueId": { + "type": "integer" + }, + "sort": { + "type": "integer" + } + } + } + ] + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated article attribute values", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "attributevalues": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ArticleAttributeValue" + } + } + } + } + } + } + } + } + } + }, "/article": { "post": { "tags": [ @@ -13566,6 +14543,145 @@ } } } + }, + "Propertyname": { + "type": "object", + "required": [ + "id", + "name", + "sort" + ], + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "sort": { + "type": "integer" + } + } + }, + "ArticleProperty": { + "type": "object", + "required": [ + "id", + "propertyId", + "name", + "value", + "sort" + ], + "properties": { + "id": { + "type": "integer" + }, + "articleId": { + "type": "integer" + }, + "propertyId": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "value": { + "type": "string" + }, + "sort": { + "type": "integer" + } + } + }, + "AttributeValue": { + "type": "object", + "required": [ + "id", + "attributeId", + "value", + "sort" + ], + "properties": { + "id": { + "type": "integer" + }, + "attributeId": { + "type": "integer" + }, + "value": { + "type": "string" + }, + "sort": { + "type": "integer" + } + } + }, + "Attributename": { + "type": "object", + "required": [ + "id", + "name", + "sort", + "exclusive", + "values" + ], + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "sort": { + "type": "integer" + }, + "exclusive": { + "type": "boolean" + }, + "values": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AttributeValue" + } + } + } + }, + "ArticleAttributeValue": { + "type": "object", + "required": [ + "id", + "attributeId", + "attributeName", + "attributeValueId", + "value", + "sort" + ], + "properties": { + "id": { + "type": "integer" + }, + "articleId": { + "type": "integer" + }, + "attributeId": { + "type": "integer" + }, + "attributeName": { + "type": "string" + }, + "exclusive": { + "type": "boolean" + }, + "attributeValueId": { + "type": "integer" + }, + "value": { + "type": "string" + }, + "sort": { + "type": "integer" + } + } } } } diff --git a/docs/api/routes/article/_sku/attributevalue.get.md b/docs/api/routes/article/_sku/attributevalue.get.md new file mode 100644 index 0000000..f9db9e0 --- /dev/null +++ b/docs/api/routes/article/_sku/attributevalue.get.md @@ -0,0 +1,38 @@ +# `GET /article/{sku}/attributevalue` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +List article attribute values + +Fetches all attribute values assigned to an article. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `sku` | yes | string | Article SKU | + +## Response `200` + +List of attribute values assigned to this article + +```json +{ + "attributevalues": [ + { + "id": 1, + "articleId": 1, + "attributeId": 1, + "attributeName": "…", + "exclusive": true, + "attributeValueId": 1, + "value": "…", + "sort": 1 + } + ] +} +``` + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/article/_sku/attributevalue.put.md b/docs/api/routes/article/_sku/attributevalue.put.md new file mode 100644 index 0000000..0ebdf54 --- /dev/null +++ b/docs/api/routes/article/_sku/attributevalue.put.md @@ -0,0 +1,48 @@ +# `PUT /article/{sku}/attributevalue` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Bulk set article attribute values + +Replaces all attribute values for an article. Enforces exclusive constraint (max one value per exclusive attribute). + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `sku` | yes | string | Article SKU | + +## Body (JSON) + +```json +{ + "attributevalues": [ + "…" + ] +} +``` + +## Response `200` + +Updated article attribute values + +```json +{ + "attributevalues": [ + { + "id": 1, + "articleId": 1, + "attributeId": 1, + "attributeName": "…", + "exclusive": true, + "attributeValueId": 1, + "value": "…", + "sort": 1 + } + ] +} +``` + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/article/_sku/property.get.md b/docs/api/routes/article/_sku/property.get.md new file mode 100644 index 0000000..00aa248 --- /dev/null +++ b/docs/api/routes/article/_sku/property.get.md @@ -0,0 +1,36 @@ +# `GET /article/{sku}/property` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +List article properties + +Fetches all free-form properties set on an article. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `sku` | yes | string | Article SKU | + +## Response `200` + +List of properties for this article + +```json +{ + "properties": [ + { + "id": 1, + "articleId": 1, + "propertyId": 1, + "name": "…", + "value": "…", + "sort": 1 + } + ] +} +``` + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/article/_sku/property.put.md b/docs/api/routes/article/_sku/property.put.md new file mode 100644 index 0000000..a8bacb9 --- /dev/null +++ b/docs/api/routes/article/_sku/property.put.md @@ -0,0 +1,50 @@ +# `PUT /article/{sku}/property` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Bulk set article properties + +Replaces all properties for an article. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `sku` | yes | string | Article SKU | + +## Body (JSON) + +```json +{ + "properties": [ + { + "propertyId": 1, + "value": "…", + "sort": 1 + } + ] +} +``` + +## Response `200` + +Updated article properties + +```json +{ + "properties": [ + { + "id": 1, + "articleId": 1, + "propertyId": 1, + "name": "…", + "value": "…", + "sort": 1 + } + ] +} +``` + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/article/_sku/property/_propertyId.md b/docs/api/routes/article/_sku/property/_propertyId.md new file mode 100644 index 0000000..495a060 --- /dev/null +++ b/docs/api/routes/article/_sku/property/_propertyId.md @@ -0,0 +1,22 @@ +# `DELETE /article/{sku}/property/{propertyId}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Delete article property + +Removes a single property from an article. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `sku` | yes | string | | +| `propertyId` | yes | integer | | + +## Response `200` + +Deleted successfully + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/attributename.get.md b/docs/api/routes/attributename.get.md new file mode 100644 index 0000000..f4feaa2 --- /dev/null +++ b/docs/api/routes/attributename.get.md @@ -0,0 +1,36 @@ +# `GET /attributename` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +List attribute names + +Lists all attribute names defined for the selected company, including nested values. + +## Response `200` + +List of attribute names + +```json +{ + "attributenames": [ + { + "id": 1, + "name": "…", + "sort": 1, + "exclusive": true, + "values": [ + { + "id": 1, + "attributeId": 1, + "value": "…", + "sort": 1 + } + ] + } + ] +} +``` + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/attributename.post.md b/docs/api/routes/attributename.post.md new file mode 100644 index 0000000..6e1a210 --- /dev/null +++ b/docs/api/routes/attributename.post.md @@ -0,0 +1,44 @@ +# `POST /attributename` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Create attribute name + +Creates an attribute name for the selected company. + +## Body (JSON) + +```json +{ + "name": "…", + "sort": 1, + "exclusive": true +} +``` + +## Response `201` + +Created attribute name + +```json +{ + "attributename": { + "id": 1, + "name": "…", + "sort": 1, + "exclusive": true, + "values": [ + { + "id": 1, + "attributeId": 1, + "value": "…", + "sort": 1 + } + ] + } +} +``` + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/attributename/_attributeId/value.get.md b/docs/api/routes/attributename/_attributeId/value.get.md new file mode 100644 index 0000000..c3f8ae0 --- /dev/null +++ b/docs/api/routes/attributename/_attributeId/value.get.md @@ -0,0 +1,34 @@ +# `GET /attributename/{attributeId}/value` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +List values for an attribute + +Lists all values for a specific attribute. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `attributeId` | yes | integer | | + +## Response `200` + +List of attribute values + +```json +{ + "values": [ + { + "id": 1, + "attributeId": 1, + "value": "…", + "sort": 1 + } + ] +} +``` + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/attributename/_attributeId/value.post.md b/docs/api/routes/attributename/_attributeId/value.post.md new file mode 100644 index 0000000..d169876 --- /dev/null +++ b/docs/api/routes/attributename/_attributeId/value.post.md @@ -0,0 +1,41 @@ +# `POST /attributename/{attributeId}/value` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Create value for an attribute + +Adds a new predefined value to an attribute. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `attributeId` | yes | integer | | + +## Body (JSON) + +```json +{ + "value": "…", + "sort": 1 +} +``` + +## Response `201` + +Created attribute value + +```json +{ + "attributevalue": { + "id": 1, + "attributeId": 1, + "value": "…", + "sort": 1 + } +} +``` + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/attributename/_attributeId/value/order.md b/docs/api/routes/attributename/_attributeId/value/order.md new file mode 100644 index 0000000..73e1e25 --- /dev/null +++ b/docs/api/routes/attributename/_attributeId/value/order.md @@ -0,0 +1,31 @@ +# `PUT /attributename/{attributeId}/value/order` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Reorder values of an attribute + +Updates the display sort order of values belonging to an attribute. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `attributeId` | yes | integer | | + +## Body (JSON) + +```json +{ + "attributevalueIds": [ + 1 + ] +} +``` + +## Response `200` + +Reordered successfully + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/attributename/_id.delete.md b/docs/api/routes/attributename/_id.delete.md new file mode 100644 index 0000000..af65d50 --- /dev/null +++ b/docs/api/routes/attributename/_id.delete.md @@ -0,0 +1,21 @@ +# `DELETE /attributename/{id}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Delete attribute name + +Deletes an attribute name and cascades to attribute values and article assignments. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | | + +## Response `200` + +Deleted successfully + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/attributename/_id.patch.md b/docs/api/routes/attributename/_id.patch.md new file mode 100644 index 0000000..5fe3ec6 --- /dev/null +++ b/docs/api/routes/attributename/_id.patch.md @@ -0,0 +1,50 @@ +# `PATCH /attributename/{id}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Update attribute name + +Updates name, sort, and/or exclusive flag for an attribute name. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | | + +## Body (JSON) + +```json +{ + "name": "…", + "sort": 1, + "exclusive": true +} +``` + +## Response `200` + +Updated attribute name + +```json +{ + "attributename": { + "id": 1, + "name": "…", + "sort": 1, + "exclusive": true, + "values": [ + { + "id": 1, + "attributeId": 1, + "value": "…", + "sort": 1 + } + ] + } +} +``` + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/attributename/order.md b/docs/api/routes/attributename/order.md new file mode 100644 index 0000000..8084ca6 --- /dev/null +++ b/docs/api/routes/attributename/order.md @@ -0,0 +1,25 @@ +# `PUT /attributename/order` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Reorder attribute names + +Updates the display sort order of attribute names for the selected company. + +## Body (JSON) + +```json +{ + "attributenameIds": [ + 1 + ] +} +``` + +## Response `200` + +Reordered successfully + +--- + +_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 new file mode 100644 index 0000000..0cf3c2b --- /dev/null +++ b/docs/api/routes/attributevalue/_id.delete.md @@ -0,0 +1,21 @@ +# `DELETE /attributevalue/{id}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Delete attribute value + +Deletes an attribute value and cascades to article assignments. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | | + +## Response `200` + +Deleted successfully + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/attributevalue/_id.patch.md b/docs/api/routes/attributevalue/_id.patch.md new file mode 100644 index 0000000..2423e39 --- /dev/null +++ b/docs/api/routes/attributevalue/_id.patch.md @@ -0,0 +1,41 @@ +# `PATCH /attributevalue/{id}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Update attribute value + +Updates value text and/or sort for an attribute value. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | | + +## Body (JSON) + +```json +{ + "value": "…", + "sort": 1 +} +``` + +## Response `200` + +Updated attribute value + +```json +{ + "attributevalue": { + "id": 1, + "attributeId": 1, + "value": "…", + "sort": 1 + } +} +``` + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/propertyname.get.md b/docs/api/routes/propertyname.get.md new file mode 100644 index 0000000..55f018c --- /dev/null +++ b/docs/api/routes/propertyname.get.md @@ -0,0 +1,33 @@ +# `GET /propertyname` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +List property names + +Lists all property names defined for the selected company, ordered by sort. + +## Response `200` + +List of property names + +```json +{ + "propertynames": [ + { + "id": 1, + "name": "…", + "sort": 1 + } + ] +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | No company selected — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/propertyname.post.md b/docs/api/routes/propertyname.post.md new file mode 100644 index 0000000..92e920e --- /dev/null +++ b/docs/api/routes/propertyname.post.md @@ -0,0 +1,40 @@ +# `POST /propertyname` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Create property name + +Creates a new property name for the selected company. + +## Body (JSON) + +```json +{ + "name": "…", + "sort": 1 +} +``` + +## Response `201` + +Created property name + +```json +{ + "propertyname": { + "id": 1, + "name": "…", + "sort": 1 + } +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Bad request — `{"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 new file mode 100644 index 0000000..78012cd --- /dev/null +++ b/docs/api/routes/propertyname/_id.delete.md @@ -0,0 +1,21 @@ +# `DELETE /propertyname/{id}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Delete property name + +Deletes a property name and cascades to article properties. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | | + +## Response `200` + +Deleted successfully + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/propertyname/_id.patch.md b/docs/api/routes/propertyname/_id.patch.md new file mode 100644 index 0000000..003f37b --- /dev/null +++ b/docs/api/routes/propertyname/_id.patch.md @@ -0,0 +1,40 @@ +# `PATCH /propertyname/{id}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Update property name + +Updates name and/or sort for a property name. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | | + +## Body (JSON) + +```json +{ + "name": "…", + "sort": 1 +} +``` + +## Response `200` + +Updated property name + +```json +{ + "propertyname": { + "id": 1, + "name": "…", + "sort": 1 + } +} +``` + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/propertyname/order.md b/docs/api/routes/propertyname/order.md new file mode 100644 index 0000000..12ef5d2 --- /dev/null +++ b/docs/api/routes/propertyname/order.md @@ -0,0 +1,31 @@ +# `PUT /propertyname/order` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Reorder property names + +Updates the display sort order of property names for the selected company. + +## Body (JSON) + +```json +{ + "propertynameIds": [ + 1 + ] +} +``` + +## Response `200` + +Reordered successfully + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Bad request | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/src/api/openapi.js b/src/api/openapi.js index 6ac13bd..d0141ac 100644 --- a/src/api/openapi.js +++ b/src/api/openapi.js @@ -910,6 +910,80 @@ * type: array * items: * $ref: '#/components/schemas/LedgerEntry' + * Propertyname: + * type: object + * required: [id, name, sort] + * properties: + * id: + * type: integer + * name: + * type: string + * sort: + * type: integer + * ArticleProperty: + * type: object + * required: [id, propertyId, name, value, sort] + * properties: + * id: + * type: integer + * articleId: + * type: integer + * propertyId: + * type: integer + * name: + * type: string + * value: + * type: string + * sort: + * type: integer + * AttributeValue: + * type: object + * required: [id, attributeId, value, sort] + * properties: + * id: + * type: integer + * attributeId: + * type: integer + * value: + * type: string + * sort: + * type: integer + * Attributename: + * type: object + * required: [id, name, sort, exclusive, values] + * properties: + * id: + * type: integer + * name: + * type: string + * sort: + * type: integer + * exclusive: + * type: boolean + * values: + * type: array + * items: + * $ref: '#/components/schemas/AttributeValue' + * ArticleAttributeValue: + * type: object + * required: [id, attributeId, attributeName, attributeValueId, value, sort] + * properties: + * id: + * type: integer + * articleId: + * type: integer + * attributeId: + * type: integer + * attributeName: + * type: string + * exclusive: + * type: boolean + * attributeValueId: + * type: integer + * value: + * type: string + * sort: + * type: integer */ export {}; diff --git a/src/api/routes/articleattributevalue.js b/src/api/routes/articleattributevalue.js new file mode 100644 index 0000000..23d1fbe --- /dev/null +++ b/src/api/routes/articleattributevalue.js @@ -0,0 +1,157 @@ +import * as db from '../../db/articleattributevalue.js'; +import { emit } from '../sse.js'; + +function requireSelectedCompany(req) { + const companyId = req.user?.selectedCompanyId; + if (companyId == null) { + return { error: 'No company selected' }; + } + return { companyId }; +} + +function parseSku(value) { + const sku = typeof value === 'string' ? value.trim() : ''; + if (!sku) return { error: 'sku is required' }; + return { sku: decodeURIComponent(sku) }; +} + +function emitArticleSignal(req, sku) { + const companyId = req.user?.selectedCompanyId; + if (companyId == null) return; + emit(`article-${sku}`, { companyId }); +} + +function articleAttributeValueErrorStatus(error) { + if (error.message === 'No company selected') return 400; + if (error.message === 'Not authorized') return 403; + if (error.message === 'Company not found' || error.message === 'Article not found') return 404; + if (error.message?.startsWith('Attribute value not found')) return 404; + if (error.message?.includes('exclusive')) return 400; + if (error.message === 'Invalid attribute values array' || error.message === 'Invalid attributeValueId' || error.message === 'Duplicate attributeValueId') return 400; + return 500; +} + +/** + * Registers articleattributevalue routes under `/article/:sku/attributevalue`. + * @param {import('express').Application} app + * @param {string} [basePath=''] + */ +export function register(app, basePath = '') { + /** + * @openapi + * /article/{sku}/attributevalue: + * get: + * tags: [Article] + * summary: List article attribute values + * description: Fetches all attribute values assigned to an article. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: sku + * required: true + * schema: + * type: string + * description: Article SKU + * responses: + * 200: + * description: List of attribute values assigned to this article + * content: + * application/json: + * schema: + * type: object + * properties: + * attributevalues: + * type: array + * items: + * $ref: '#/components/schemas/ArticleAttributeValue' + * put: + * tags: [Article] + * summary: Bulk set article attribute values + * description: Replaces all attribute values for an article. Enforces exclusive constraint (max one value per exclusive attribute). + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: sku + * required: true + * schema: + * type: string + * description: Article SKU + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [attributevalues] + * properties: + * attributevalues: + * type: array + * items: + * oneOf: + * - type: integer + * - type: object + * required: [attributeValueId] + * properties: + * attributeValueId: + * type: integer + * sort: + * type: integer + * responses: + * 200: + * description: Updated article attribute values + * content: + * application/json: + * schema: + * type: object + * properties: + * attributevalues: + * type: array + * items: + * $ref: '#/components/schemas/ArticleAttributeValue' + */ + app.get(`${basePath}/article/:sku/attributevalue`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const skuParsed = parseSku(req.params.sku); + if (skuParsed.error) { + return res.status(400).json({ error: skuParsed.error }); + } + + try { + const attributevalues = await db.listByArticleSku(req.user.id, skuParsed.sku); + res.json({ attributevalues }); + } catch (error) { + res.status(articleAttributeValueErrorStatus(error)).json({ error: error.message }); + } + }); + + app.put(`${basePath}/article/:sku/attributevalue`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const skuParsed = parseSku(req.params.sku); + if (skuParsed.error) { + return res.status(400).json({ error: skuParsed.error }); + } + + const valuesInput = req.body?.attributevalues ?? req.body; + if (!Array.isArray(valuesInput)) { + return res.status(400).json({ error: 'attributevalues array required' }); + } + + try { + const attributevalues = await db.setByArticleSku(req.user.id, skuParsed.sku, valuesInput); + emitArticleSignal(req, skuParsed.sku); + res.json({ attributevalues }); + } catch (error) { + res.status(articleAttributeValueErrorStatus(error)).json({ error: error.message }); + } + }); +} diff --git a/src/api/routes/articleproperty.js b/src/api/routes/articleproperty.js new file mode 100644 index 0000000..9ecfa63 --- /dev/null +++ b/src/api/routes/articleproperty.js @@ -0,0 +1,201 @@ +import * as db from '../../db/articleproperty.js'; +import { emit } from '../sse.js'; + +function requireSelectedCompany(req) { + const companyId = req.user?.selectedCompanyId; + if (companyId == null) { + return { error: 'No company selected' }; + } + return { companyId }; +} + +function parseSku(value) { + const sku = typeof value === 'string' ? value.trim() : ''; + if (!sku) return { error: 'sku is required' }; + return { sku: decodeURIComponent(sku) }; +} + +function emitArticleSignal(req, sku) { + const companyId = req.user?.selectedCompanyId; + if (companyId == null) return; + emit(`article-${sku}`, { companyId }); +} + +function articlePropertyErrorStatus(error) { + if (error.message === 'No company selected') return 400; + if (error.message === 'Not authorized') return 403; + if (error.message === 'Company not found' || error.message === 'Article not found') return 404; + if (error.message?.startsWith('Property not found')) return 404; + if (error.message === 'Invalid properties array' || error.message === 'Invalid propertyId' || error.message === 'Duplicate propertyId') return 400; + return 500; +} + +/** + * Registers articleproperty routes under `/article/:sku/property`. + * @param {import('express').Application} app + * @param {string} [basePath=''] + */ +export function register(app, basePath = '') { + /** + * @openapi + * /article/{sku}/property: + * get: + * tags: [Article] + * summary: List article properties + * description: Fetches all free-form properties set on an article. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: sku + * required: true + * schema: + * type: string + * description: Article SKU + * responses: + * 200: + * description: List of properties for this article + * content: + * application/json: + * schema: + * type: object + * properties: + * properties: + * type: array + * items: + * $ref: '#/components/schemas/ArticleProperty' + * put: + * tags: [Article] + * summary: Bulk set article properties + * description: Replaces all properties for an article. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: sku + * required: true + * schema: + * type: string + * description: Article SKU + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [properties] + * properties: + * properties: + * type: array + * items: + * type: object + * required: [propertyId, value] + * properties: + * propertyId: + * type: integer + * value: + * type: string + * sort: + * type: integer + * responses: + * 200: + * description: Updated article properties + * content: + * application/json: + * schema: + * type: object + * properties: + * properties: + * type: array + * items: + * $ref: '#/components/schemas/ArticleProperty' + */ + app.get(`${basePath}/article/:sku/property`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const skuParsed = parseSku(req.params.sku); + if (skuParsed.error) { + return res.status(400).json({ error: skuParsed.error }); + } + + try { + const properties = await db.listByArticleSku(req.user.id, skuParsed.sku); + res.json({ properties }); + } catch (error) { + res.status(articlePropertyErrorStatus(error)).json({ error: error.message }); + } + }); + + app.put(`${basePath}/article/:sku/property`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const skuParsed = parseSku(req.params.sku); + if (skuParsed.error) { + return res.status(400).json({ error: skuParsed.error }); + } + + const propertiesInput = req.body?.properties ?? req.body; + if (!Array.isArray(propertiesInput)) { + return res.status(400).json({ error: 'properties array required' }); + } + + try { + const properties = await db.setByArticleSku(req.user.id, skuParsed.sku, propertiesInput); + emitArticleSignal(req, skuParsed.sku); + res.json({ properties }); + } catch (error) { + res.status(articlePropertyErrorStatus(error)).json({ error: error.message }); + } + }); + + /** + * @openapi + * /article/{sku}/property/{propertyId}: + * delete: + * tags: [Article] + * summary: Delete article property + * description: Removes a single property from an article. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: sku + * required: true + * schema: + * type: string + * - in: path + * name: propertyId + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Deleted successfully + */ + app.delete(`${basePath}/article/:sku/property/:propertyId`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const skuParsed = parseSku(req.params.sku); + if (skuParsed.error) { + return res.status(400).json({ error: skuParsed.error }); + } + + try { + const propertyId = Number(req.params.propertyId); + await db.remove(req.user.id, skuParsed.sku, propertyId); + emitArticleSignal(req, skuParsed.sku); + res.json({ ok: true }); + } catch (error) { + res.status(articlePropertyErrorStatus(error)).json({ error: error.message }); + } + }); +} diff --git a/src/api/routes/attributename.js b/src/api/routes/attributename.js new file mode 100644 index 0000000..51997f0 --- /dev/null +++ b/src/api/routes/attributename.js @@ -0,0 +1,235 @@ +import * as db from '../../db/attributename.js'; +import { emit } from '../sse.js'; + +function requireSelectedCompany(req) { + const companyId = req.user?.selectedCompanyId; + if (companyId == null) { + return { error: 'No company selected' }; + } + return { companyId }; +} + +function attributeErrorStatus(error) { + if (error.message === 'No company selected') return 400; + if (error.message === 'Not authorized') return 403; + if (error.message === 'Company not found') return 404; + if (error.message === 'Attribute name not found') return 404; + if (error.message === 'Name required') return 400; + if (error.message === 'Invalid attribute name order') return 400; + if (error.message === 'Invalid id') return 400; + return 500; +} + +/** + * Registers attributename routes. + * @param {import('express').Application} app + * @param {string} [basePath=''] + */ +export function register(app, basePath = '') { + /** + * @openapi + * /attributename: + * get: + * tags: [Article] + * summary: List attribute names + * description: Lists all attribute names defined for the selected company, including nested values. + * security: + * - cookieAuth: [] + * responses: + * 200: + * description: List of attribute names + * content: + * application/json: + * schema: + * type: object + * properties: + * attributenames: + * type: array + * items: + * $ref: '#/components/schemas/Attributename' + * post: + * tags: [Article] + * summary: Create attribute name + * description: Creates an attribute name for the selected company. + * security: + * - cookieAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [name] + * properties: + * name: + * type: string + * sort: + * type: integer + * exclusive: + * type: boolean + * responses: + * 201: + * description: Created attribute name + * content: + * application/json: + * schema: + * type: object + * properties: + * attributename: + * $ref: '#/components/schemas/Attributename' + */ + app.get(`${basePath}/attributename`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + try { + const attributenames = await db.list(req.user.id); + res.json({ attributenames }); + } catch (error) { + res.status(attributeErrorStatus(error)).json({ error: error.message }); + } + }); + + app.post(`${basePath}/attributename`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + try { + const attributename = await db.create(req.user.id, req.body ?? {}); + emit('attributename', { companyId: selected.companyId }); + res.status(201).json({ attributename }); + } catch (error) { + res.status(attributeErrorStatus(error)).json({ error: error.message }); + } + }); + + /** + * @openapi + * /attributename/order: + * put: + * tags: [Article] + * summary: Reorder attribute names + * description: Updates the display sort order of attribute names for the selected company. + * security: + * - cookieAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [attributenameIds] + * properties: + * attributenameIds: + * type: array + * items: + * type: integer + * responses: + * 200: + * description: Reordered successfully + */ + app.put(`${basePath}/attributename/order`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + try { + await db.order(req.user.id, req.body ?? {}); + emit('attributename', { companyId: selected.companyId }); + res.json({ ok: true }); + } catch (error) { + res.status(attributeErrorStatus(error)).json({ error: error.message }); + } + }); + + /** + * @openapi + * /attributename/{id}: + * patch: + * tags: [Article] + * summary: Update attribute name + * description: Updates name, sort, and/or exclusive flag for an attribute name. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * sort: + * type: integer + * exclusive: + * type: boolean + * responses: + * 200: + * description: Updated attribute name + * content: + * application/json: + * schema: + * type: object + * properties: + * attributename: + * $ref: '#/components/schemas/Attributename' + * delete: + * tags: [Article] + * summary: Delete attribute name + * description: Deletes an attribute name and cascades to attribute values and article assignments. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Deleted successfully + */ + app.patch(`${basePath}/attributename/:id`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + try { + const id = Number(req.params.id); + const attributename = await db.update(req.user.id, id, req.body ?? {}); + emit('attributename', { companyId: selected.companyId }); + res.json({ attributename }); + } catch (error) { + res.status(attributeErrorStatus(error)).json({ error: error.message }); + } + }); + + app.delete(`${basePath}/attributename/:id`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + try { + const id = Number(req.params.id); + await db.remove(req.user.id, id); + emit('attributename', { companyId: selected.companyId }); + res.json({ ok: true }); + } catch (error) { + res.status(attributeErrorStatus(error)).json({ error: error.message }); + } + }); +} diff --git a/src/api/routes/attributevalue.js b/src/api/routes/attributevalue.js new file mode 100644 index 0000000..84a6bde --- /dev/null +++ b/src/api/routes/attributevalue.js @@ -0,0 +1,251 @@ +import * as db from '../../db/attributevalue.js'; +import { emit } from '../sse.js'; + +function requireSelectedCompany(req) { + const companyId = req.user?.selectedCompanyId; + if (companyId == null) { + return { error: 'No company selected' }; + } + return { companyId }; +} + +function attributeValueErrorStatus(error) { + if (error.message === 'No company selected') return 400; + if (error.message === 'Not authorized') return 403; + if (error.message === 'Company not found' || error.message === 'Attribute name not found' || error.message === 'Attribute value not found') return 404; + 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; + return 500; +} + +/** + * Registers attributevalue routes under `/attributename/:attributeId/value` and `/attributevalue/:id`. + * @param {import('express').Application} app + * @param {string} [basePath=''] + */ +export function register(app, basePath = '') { + /** + * @openapi + * /attributename/{attributeId}/value: + * get: + * tags: [Article] + * summary: List values for an attribute + * description: Lists all values for a specific attribute. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: attributeId + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: List of attribute values + * content: + * application/json: + * schema: + * type: object + * properties: + * values: + * type: array + * items: + * $ref: '#/components/schemas/AttributeValue' + * post: + * tags: [Article] + * summary: Create value for an attribute + * description: Adds a new predefined value to an attribute. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: attributeId + * required: true + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [value] + * properties: + * value: + * type: string + * sort: + * type: integer + * responses: + * 201: + * description: Created attribute value + * content: + * application/json: + * schema: + * type: object + * properties: + * attributevalue: + * $ref: '#/components/schemas/AttributeValue' + */ + app.get(`${basePath}/attributename/:attributeId/value`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + try { + const attributeId = Number(req.params.attributeId); + const values = await db.listByAttributeId(req.user.id, attributeId); + res.json({ values }); + } catch (error) { + res.status(attributeValueErrorStatus(error)).json({ error: error.message }); + } + }); + + app.post(`${basePath}/attributename/:attributeId/value`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + try { + const attributeId = Number(req.params.attributeId); + const attributevalue = await db.create(req.user.id, attributeId, req.body ?? {}); + emit('attributename', { companyId: selected.companyId }); + res.status(201).json({ attributevalue }); + } catch (error) { + res.status(attributeValueErrorStatus(error)).json({ error: error.message }); + } + }); + + /** + * @openapi + * /attributename/{attributeId}/value/order: + * put: + * tags: [Article] + * summary: Reorder values of an attribute + * description: Updates the display sort order of values belonging to an attribute. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: attributeId + * required: true + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [attributevalueIds] + * properties: + * attributevalueIds: + * type: array + * items: + * type: integer + * responses: + * 200: + * description: Reordered successfully + */ + app.put(`${basePath}/attributename/:attributeId/value/order`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + try { + const attributeId = Number(req.params.attributeId); + await db.order(req.user.id, attributeId, req.body ?? {}); + emit('attributename', { companyId: selected.companyId }); + res.json({ ok: true }); + } catch (error) { + res.status(attributeValueErrorStatus(error)).json({ error: error.message }); + } + }); + + /** + * @openapi + * /attributevalue/{id}: + * patch: + * tags: [Article] + * summary: Update attribute value + * description: Updates value text and/or sort for an attribute value. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * value: + * type: string + * sort: + * type: integer + * responses: + * 200: + * description: Updated attribute value + * content: + * application/json: + * schema: + * type: object + * properties: + * attributevalue: + * $ref: '#/components/schemas/AttributeValue' + * delete: + * tags: [Article] + * summary: Delete attribute value + * description: Deletes an attribute value and cascades to article assignments. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Deleted successfully + */ + app.patch(`${basePath}/attributevalue/:id`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + try { + const id = Number(req.params.id); + const attributevalue = await db.update(req.user.id, id, req.body ?? {}); + emit('attributename', { companyId: selected.companyId }); + res.json({ attributevalue }); + } catch (error) { + res.status(attributeValueErrorStatus(error)).json({ error: error.message }); + } + }); + + app.delete(`${basePath}/attributevalue/:id`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + try { + const id = Number(req.params.id); + await db.remove(req.user.id, id); + emit('attributename', { companyId: selected.companyId }); + res.json({ ok: true }); + } catch (error) { + res.status(attributeValueErrorStatus(error)).json({ error: error.message }); + } + }); +} diff --git a/src/api/routes/propertyname.js b/src/api/routes/propertyname.js new file mode 100644 index 0000000..b28dd8d --- /dev/null +++ b/src/api/routes/propertyname.js @@ -0,0 +1,245 @@ +import * as db from '../../db/propertyname.js'; +import { emit } from '../sse.js'; + +function requireSelectedCompany(req) { + const companyId = req.user?.selectedCompanyId; + if (companyId == null) { + return { error: 'No company selected' }; + } + return { companyId }; +} + +function propertyErrorStatus(error) { + if (error.message === 'No company selected') return 400; + if (error.message === 'Not authorized') return 403; + if (error.message === 'Company not found') return 404; + if (error.message === 'Property name not found') return 404; + if (error.message === 'Name required') return 400; + if (error.message === 'Invalid property name order') return 400; + if (error.message === 'Invalid id') return 400; + return 500; +} + +/** + * Registers propertyname routes. + * @param {import('express').Application} app + * @param {string} [basePath=''] + */ +export function register(app, basePath = '') { + /** + * @openapi + * /propertyname: + * get: + * tags: [Article] + * summary: List property names + * description: Lists all property names defined for the selected company, ordered by sort. + * security: + * - cookieAuth: [] + * responses: + * 200: + * description: List of property names + * content: + * application/json: + * schema: + * type: object + * properties: + * propertynames: + * type: array + * items: + * $ref: '#/components/schemas/Propertyname' + * 400: + * description: No company selected + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * post: + * tags: [Article] + * summary: Create property name + * description: Creates a new property name for the selected company. + * security: + * - cookieAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [name] + * properties: + * name: + * type: string + * sort: + * type: integer + * responses: + * 201: + * description: Created property name + * content: + * application/json: + * schema: + * type: object + * properties: + * propertyname: + * $ref: '#/components/schemas/Propertyname' + * 400: + * description: Bad request + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + app.get(`${basePath}/propertyname`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + try { + const propertynames = await db.list(req.user.id); + res.json({ propertynames }); + } catch (error) { + res.status(propertyErrorStatus(error)).json({ error: error.message }); + } + }); + + app.post(`${basePath}/propertyname`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + try { + const propertyname = await db.create(req.user.id, req.body ?? {}); + emit('propertyname', { companyId: selected.companyId }); + res.status(201).json({ propertyname }); + } catch (error) { + res.status(propertyErrorStatus(error)).json({ error: error.message }); + } + }); + + /** + * @openapi + * /propertyname/order: + * put: + * tags: [Article] + * summary: Reorder property names + * description: Updates the display sort order of property names for the selected company. + * security: + * - cookieAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [propertynameIds] + * properties: + * propertynameIds: + * type: array + * items: + * type: integer + * responses: + * 200: + * description: Reordered successfully + * 400: + * description: Bad request + */ + app.put(`${basePath}/propertyname/order`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + try { + await db.order(req.user.id, req.body ?? {}); + emit('propertyname', { companyId: selected.companyId }); + res.json({ ok: true }); + } catch (error) { + res.status(propertyErrorStatus(error)).json({ error: error.message }); + } + }); + + /** + * @openapi + * /propertyname/{id}: + * patch: + * tags: [Article] + * summary: Update property name + * description: Updates name and/or sort for a property name. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * sort: + * type: integer + * responses: + * 200: + * description: Updated property name + * content: + * application/json: + * schema: + * type: object + * properties: + * propertyname: + * $ref: '#/components/schemas/Propertyname' + * delete: + * tags: [Article] + * summary: Delete property name + * description: Deletes a property name and cascades to article properties. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Deleted successfully + */ + app.patch(`${basePath}/propertyname/:id`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + try { + const id = Number(req.params.id); + const propertyname = await db.update(req.user.id, id, req.body ?? {}); + emit('propertyname', { companyId: selected.companyId }); + res.json({ propertyname }); + } catch (error) { + res.status(propertyErrorStatus(error)).json({ error: error.message }); + } + }); + + app.delete(`${basePath}/propertyname/:id`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + try { + const id = Number(req.params.id); + await db.remove(req.user.id, id); + emit('propertyname', { companyId: selected.companyId }); + res.json({ ok: true }); + } catch (error) { + res.status(propertyErrorStatus(error)).json({ error: error.message }); + } + }); +} diff --git a/src/db/articleattributevalue.js b/src/db/articleattributevalue.js new file mode 100644 index 0000000..ff8db18 --- /dev/null +++ b/src/db/articleattributevalue.js @@ -0,0 +1,11 @@ +import { getStore } from './store.js'; + +export async function listByArticleSku(googleAuthId, sku) { + const store = await getStore(); + return store.articleattributevalue.listByArticleSku(googleAuthId, sku); +} + +export async function setByArticleSku(googleAuthId, sku, values) { + const store = await getStore(); + return store.articleattributevalue.setByArticleSku(googleAuthId, sku, values); +} diff --git a/src/db/articleproperty.js b/src/db/articleproperty.js new file mode 100644 index 0000000..c312377 --- /dev/null +++ b/src/db/articleproperty.js @@ -0,0 +1,16 @@ +import { getStore } from './store.js'; + +export async function listByArticleSku(googleAuthId, sku) { + const store = await getStore(); + return store.articleproperty.listByArticleSku(googleAuthId, sku); +} + +export async function setByArticleSku(googleAuthId, sku, properties) { + const store = await getStore(); + return store.articleproperty.setByArticleSku(googleAuthId, sku, properties); +} + +export async function remove(googleAuthId, sku, propertyId) { + const store = await getStore(); + return store.articleproperty.delete(googleAuthId, sku, propertyId); +} diff --git a/src/db/attributename.js b/src/db/attributename.js new file mode 100644 index 0000000..f003ef4 --- /dev/null +++ b/src/db/attributename.js @@ -0,0 +1,26 @@ +import { getStore } from './store.js'; + +export async function list(googleAuthId) { + const store = await getStore(); + return store.attributename.list(googleAuthId); +} + +export async function create(googleAuthId, data) { + const store = await getStore(); + return store.attributename.create(googleAuthId, data); +} + +export async function update(googleAuthId, id, data) { + const store = await getStore(); + return store.attributename.update(googleAuthId, id, data); +} + +export async function order(googleAuthId, data) { + const store = await getStore(); + return store.attributename.order(googleAuthId, data); +} + +export async function remove(googleAuthId, id) { + const store = await getStore(); + return store.attributename.delete(googleAuthId, id); +} diff --git a/src/db/attributevalue.js b/src/db/attributevalue.js new file mode 100644 index 0000000..a9a3031 --- /dev/null +++ b/src/db/attributevalue.js @@ -0,0 +1,26 @@ +import { getStore } from './store.js'; + +export async function listByAttributeId(googleAuthId, attributeId) { + const store = await getStore(); + return store.attributevalue.listByAttributeId(googleAuthId, attributeId); +} + +export async function create(googleAuthId, attributeId, data) { + const store = await getStore(); + return store.attributevalue.create(googleAuthId, attributeId, data); +} + +export async function update(googleAuthId, id, data) { + const store = await getStore(); + return store.attributevalue.update(googleAuthId, id, data); +} + +export async function order(googleAuthId, attributeId, data) { + const store = await getStore(); + return store.attributevalue.order(googleAuthId, attributeId, data); +} + +export async function remove(googleAuthId, id) { + const store = await getStore(); + return store.attributevalue.delete(googleAuthId, id); +} diff --git a/src/db/propertyname.js b/src/db/propertyname.js new file mode 100644 index 0000000..9c7aa20 --- /dev/null +++ b/src/db/propertyname.js @@ -0,0 +1,26 @@ +import { getStore } from './store.js'; + +export async function list(googleAuthId) { + const store = await getStore(); + return store.propertyname.list(googleAuthId); +} + +export async function create(googleAuthId, data) { + const store = await getStore(); + return store.propertyname.create(googleAuthId, data); +} + +export async function update(googleAuthId, id, data) { + const store = await getStore(); + return store.propertyname.update(googleAuthId, id, data); +} + +export async function order(googleAuthId, data) { + const store = await getStore(); + return store.propertyname.order(googleAuthId, data); +} + +export async function remove(googleAuthId, id) { + const store = await getStore(); + return store.propertyname.delete(googleAuthId, id); +} diff --git a/src/db/store.js b/src/db/store.js index 5262756..12eac9c 100644 --- a/src/db/store.js +++ b/src/db/store.js @@ -37,6 +37,11 @@ const STORE_NAMESPACES = [ 'printer', 'labeltemplate', 'stock', + 'propertyname', + 'articleproperty', + 'attributename', + 'attributevalue', + 'articleattributevalue', ]; /**