From bfb1d8762a585e8bf7a768afbd0634bf9101cda6 Mon Sep 17 00:00:00 2001 From: seb Date: Sat, 1 Aug 2026 06:15:30 +0200 Subject: [PATCH] Add unit API and article unitamount/unitSi patch. Units use the selected company; article PATCH accepts name/unitamount/unitSi. --- docs/api/README.md | 3 + docs/api/openapi.json | 325 +++++++++++++++++++++++++- docs/api/routes/article/set-prices.md | 8 +- docs/api/routes/article/update.md | 11 +- docs/api/routes/unit/delete.md | 33 +++ docs/api/routes/unit/list.md | 30 +++ docs/api/routes/unit/upsert.md | 44 ++++ scripts/generateApiDocs.js | 1 + src/api/openapi.js | 16 ++ src/api/routes/article.js | 39 +++- src/api/routes/unit.js | 246 +++++++++++++++++++ src/db/store.js | 1 + src/db/unit.js | 16 ++ 13 files changed, 744 insertions(+), 29 deletions(-) create mode 100644 docs/api/routes/unit/delete.md create mode 100644 docs/api/routes/unit/list.md create mode 100644 docs/api/routes/unit/upsert.md create mode 100644 src/api/routes/unit.js create mode 100644 src/db/unit.js diff --git a/docs/api/README.md b/docs/api/README.md index 92bb451..13b97ba 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -53,6 +53,9 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route | [`routes/health.md`](routes/health.md) | `GET /health` | | [`routes/image/get.md`](routes/image/get.md) | `GET /image/{fullHash}` | | [`routes/thumb/get.md`](routes/thumb/get.md) | `GET /thumb/{fullHash}` | +| [`routes/unit/delete.md`](routes/unit/delete.md) | `DELETE /unit` | +| [`routes/unit/list.md`](routes/unit/list.md) | `GET /unit` | +| [`routes/unit/upsert.md`](routes/unit/upsert.md) | `PUT /unit` | Machine-readable spec: [`openapi.json`](openapi.json). diff --git a/docs/api/openapi.json b/docs/api/openapi.json index 0a50bca..00982f9 100644 --- a/docs/api/openapi.json +++ b/docs/api/openapi.json @@ -32,12 +32,280 @@ "name": "Company", "description": "Company CRUD" }, + { + "name": "Unit", + "description": "Company-scoped units of measure" + }, { "name": "Image", "description": "Content-addressed AVIF by fullHash" } ], "paths": { + "/unit": { + "get": { + "tags": [ + "Unit" + ], + "summary": "List units", + "description": "Lists units for the authenticated user's selected company (default-language symbol/name).", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "unit/list.md", + "responses": { + "200": { + "description": "Unit list", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "units": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Unit" + } + } + } + } + } + } + }, + "400": { + "description": "No company selected", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Not authorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Company not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "put": { + "tags": [ + "Unit" + ], + "summary": "Upsert unit", + "description": "Insert or update unit by `si` for the selected company. Upserts `unittext` for the company language.\n`basesifactor` must be finite and `> 0`.\n", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "unit/upsert.md", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "si", + "basesi", + "basesifactor", + "symbol", + "name" + ], + "properties": { + "si": { + "type": "string" + }, + "basesi": { + "type": "string" + }, + "basesifactor": { + "type": "number" + }, + "symbol": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "example": { + "si": "l", + "basesi": "l", + "basesifactor": 1, + "symbol": "L", + "name": "Liter" + } + } + } + } + }, + "responses": { + "200": { + "description": "Upserted unit", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "unit": { + "$ref": "#/components/schemas/Unit" + } + } + } + } + } + }, + "400": { + "description": "Invalid unit or no company selected", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Not authorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Company not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Unit" + ], + "summary": "Delete unit", + "description": "Deletes `si` for the selected company. Fails with `Unit in use` if articles still reference it.", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "unit/delete.md", + "parameters": [ + { + "in": "query", + "name": "si", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Deleted" + }, + "400": { + "description": "Missing si or no company selected", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Not authorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Company or unit not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Unit in use", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, "/image/{fullHash}": { "get": { "tags": [ @@ -1110,7 +1378,7 @@ "Article" ], "summary": "Update article by SKU", - "description": "Updates an article name by SKU; returns the same detail shape as GET.", + "description": "Patches article fields. At least one of `name`, `unitamount`, `unitSi` required.\n`unitSi` must already exist for the company. Returns the same detail shape as GET.\n", "security": [ { "cookieAuth": [] @@ -1134,16 +1402,22 @@ "application/json": { "schema": { "type": "object", - "required": [ - "name" - ], "properties": { "name": { "type": "string" + }, + "unitamount": { + "type": "number", + "description": "Must be finite and `> 0`" + }, + "unitSi": { + "type": "string", + "description": "Existing company unit `si`" } }, "example": { - "name": "Updated name" + "unitamount": 0.24, + "unitSi": "l" } } } @@ -1166,7 +1440,7 @@ } }, "400": { - "description": "Validation error", + "description": "Validation error (Name required / Invalid unitamount)", "content": { "application/json": { "schema": { @@ -1196,7 +1470,7 @@ } }, "404": { - "description": "Article not found", + "description": "Article or unit not found", "content": { "application/json": { "schema": { @@ -1283,7 +1557,7 @@ "Article" ], "summary": "Replace POS prices by SKU", - "description": "Replace-all prices for the company POS customer group.\nEach entry: `price` (gross), `tax` (rate %, ensured via tax.ensure), optional `minmultiplier` (default 1).\nStored as net = gross / (1 + tax/100). Empty `prices` clears all POS prices. Returns the same shape as GET by SKU.\n", + "description": "Replace-all prices for the company POS customer group.\nEach entry: `price` (net), `tax` (rate, ensured via tax.ensure), optional `minmultiplier` (default 1).\nEmpty `prices` clears all POS prices. Returns the same shape as GET by SKU.\n", "security": [ { "cookieAuth": [] @@ -1321,11 +1595,11 @@ "properties": { "price": { "type": "number", - "description": "Gross price (Brutto)" + "description": "Net price" }, "tax": { "type": "number", - "description": "Tax rate percent (e.g. 19)" + "description": "Tax rate (e.g. 19)" }, "minmultiplier": { "type": "number", @@ -1338,11 +1612,11 @@ "example": { "prices": [ { - "price": 11.9, + "price": 10.5, "tax": 19 }, { - "price": 99, + "price": 9.9, "tax": 19, "minmultiplier": 10 } @@ -2042,6 +2316,33 @@ } } }, + "Unit": { + "type": "object", + "required": [ + "si", + "basesi", + "basesifactor" + ], + "properties": { + "si": { + "type": "string" + }, + "basesi": { + "type": "string" + }, + "basesifactor": { + "type": "number" + }, + "symbol": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string", + "nullable": true + } + } + }, "ArticleSummary": { "type": "object", "properties": { diff --git a/docs/api/routes/article/set-prices.md b/docs/api/routes/article/set-prices.md index fe34cb4..f10249b 100644 --- a/docs/api/routes/article/set-prices.md +++ b/docs/api/routes/article/set-prices.md @@ -5,8 +5,8 @@ Protected. Requires a valid session cookie (`credentials: 'include'`). Replace POS prices by SKU Replace-all prices for the company POS customer group. -Each entry: `price` (gross), `tax` (rate %, ensured via tax.ensure), optional `minmultiplier` (default 1). -Stored as net = gross / (1 + tax/100). Empty `prices` clears all POS prices. Returns the same shape as GET by SKU. +Each entry: `price` (net), `tax` (rate, ensured via tax.ensure), optional `minmultiplier` (default 1). +Empty `prices` clears all POS prices. Returns the same shape as GET by SKU. ## Path parameters @@ -21,11 +21,11 @@ Stored as net = gross / (1 + tax/100). Empty `prices` clears all POS prices. Ret { "prices": [ { - "price": 11.9, + "price": 10.5, "tax": 19 }, { - "price": 99, + "price": 9.9, "tax": 19, "minmultiplier": 10 } diff --git a/docs/api/routes/article/update.md b/docs/api/routes/article/update.md index aa348c8..112015a 100644 --- a/docs/api/routes/article/update.md +++ b/docs/api/routes/article/update.md @@ -4,7 +4,9 @@ Protected. Requires a valid session cookie (`credentials: 'include'`). Update article by SKU -Updates an article name by SKU; returns the same detail shape as GET. +Patches article fields. At least one of `name`, `unitamount`, `unitSi` required. +`unitSi` must already exist for the company. Returns the same detail shape as GET. + ## Path parameters @@ -16,7 +18,8 @@ Updates an article name by SKU; returns the same detail shape as GET. ```json { - "name": "Updated name" + "unitamount": 0.24, + "unitSi": "l" } ``` @@ -34,10 +37,10 @@ Updated article detail | Status | Description | |--------|-------------| -| 400 | Validation error — `{"error":"…"}` | +| 400 | Validation error (Name required / Invalid unitamount) — `{"error":"…"}` | | 401 | Unauthorized — `{"error":"…"}` | | 403 | Not authorized — `{"error":"…"}` | -| 404 | Article not found — `{"error":"…"}` | +| 404 | Article or unit not found — `{"error":"…"}` | --- diff --git a/docs/api/routes/unit/delete.md b/docs/api/routes/unit/delete.md new file mode 100644 index 0000000..02a547d --- /dev/null +++ b/docs/api/routes/unit/delete.md @@ -0,0 +1,33 @@ +# `DELETE /unit` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Delete unit + +Deletes `si` for the selected company. Fails with `Unit in use` if articles still reference it. + +## Query parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `si` | yes | string | | + +## Response `204` + +Deleted + +_No content_ + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Missing si or no company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Company or unit not found — `{"error":"…"}` | +| 409 | Unit in use — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/unit/list.md b/docs/api/routes/unit/list.md new file mode 100644 index 0000000..16eb417 --- /dev/null +++ b/docs/api/routes/unit/list.md @@ -0,0 +1,30 @@ +# `GET /unit` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +List units + +Lists units for the authenticated user's selected company (default-language symbol/name). + +## Response `200` + +Unit list + +```json +{ + "units": [] +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | No company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Company not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/unit/upsert.md b/docs/api/routes/unit/upsert.md new file mode 100644 index 0000000..e3a038e --- /dev/null +++ b/docs/api/routes/unit/upsert.md @@ -0,0 +1,44 @@ +# `PUT /unit` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Upsert unit + +Insert or update unit by `si` for the selected company. Upserts `unittext` for the company language. +`basesifactor` must be finite and `> 0`. + + +## Body (JSON) + +```json +{ + "si": "l", + "basesi": "l", + "basesifactor": 1, + "symbol": "L", + "name": "Liter" +} +``` + +## Response `200` + +Upserted unit + +```json +{ + "unit": { … } +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Invalid unit or no company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Company not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/scripts/generateApiDocs.js b/scripts/generateApiDocs.js index 4516596..f61c659 100644 --- a/scripts/generateApiDocs.js +++ b/scripts/generateApiDocs.js @@ -33,6 +33,7 @@ const options = { { name: 'Article', description: 'Articles under categories' }, { name: 'Category', description: 'Category tree and membership' }, { name: 'Company', description: 'Company CRUD' }, + { name: 'Unit', description: 'Company-scoped units of measure' }, { name: 'Image', description: 'Content-addressed AVIF by fullHash' }, ], }, diff --git a/src/api/openapi.js b/src/api/openapi.js index 47623a3..b3bb4ac 100644 --- a/src/api/openapi.js +++ b/src/api/openapi.js @@ -57,6 +57,22 @@ * type: integer * name: * type: string + * Unit: + * type: object + * required: [si, basesi, basesifactor] + * properties: + * si: + * type: string + * basesi: + * type: string + * basesifactor: + * type: number + * symbol: + * type: string + * nullable: true + * name: + * type: string + * nullable: true * ArticleSummary: * type: object * properties: diff --git a/src/api/routes/article.js b/src/api/routes/article.js index 5630d75..262003c 100644 --- a/src/api/routes/article.js +++ b/src/api/routes/article.js @@ -36,6 +36,8 @@ function articleErrorStatus(error) { if (error.message === 'Article has children') return 400; if (error.message === 'Invalid prices') return 400; if (error.message === 'POS customer group required') return 400; + if (error.message === 'Invalid unitamount') return 400; + if (error.message === 'Unit not found') return 404; return 500; } @@ -185,7 +187,9 @@ export function register(app, basePath = '') { * patch: * tags: [Article] * summary: Update article by SKU - * description: Updates an article name by SKU; returns the same detail shape as GET. + * description: | + * Patches article fields. At least one of `name`, `unitamount`, `unitSi` required. + * `unitSi` must already exist for the company. Returns the same detail shape as GET. * security: * - cookieAuth: [] * x-markdown: article/update.md @@ -202,12 +206,18 @@ export function register(app, basePath = '') { * application/json: * schema: * type: object - * required: [name] * properties: * name: * type: string + * unitamount: + * type: number + * description: Must be finite and `> 0` + * unitSi: + * type: string + * description: Existing company unit `si` * example: - * name: Updated name + * unitamount: 0.24 + * unitSi: l * responses: * 200: * description: Updated article detail @@ -219,7 +229,7 @@ export function register(app, basePath = '') { * article: * $ref: '#/components/schemas/ArticleDetail' * 400: - * description: Validation error + * description: Validation error (Name required / Invalid unitamount) * content: * application/json: * schema: @@ -237,7 +247,7 @@ export function register(app, basePath = '') { * schema: * $ref: '#/components/schemas/Error' * 404: - * description: Article not found + * description: Article or unit not found * content: * application/json: * schema: @@ -309,13 +319,24 @@ export function register(app, basePath = '') { return res.status(400).json({ error: 'sku is required' }); } - const name = typeof req.body?.name === 'string' ? req.body.name.trim() : ''; - if (!name) { - return res.status(400).json({ error: 'name is required' }); + const body = req.body ?? {}; + const patch = {}; + if (Object.prototype.hasOwnProperty.call(body, 'name')) { + patch.name = body.name; + } + if (Object.prototype.hasOwnProperty.call(body, 'unitamount')) { + patch.unitamount = body.unitamount; + } + if (Object.prototype.hasOwnProperty.call(body, 'unitSi')) { + patch.unitSi = body.unitSi; + } + + if (!('name' in patch) && !('unitamount' in patch) && !('unitSi' in patch)) { + return res.status(400).json({ error: 'name, unitamount, or unitSi is required' }); } try { - const article = await db.updateBySku(req.user.id, decodeURIComponent(sku), { name }); + const article = await db.updateBySku(req.user.id, decodeURIComponent(sku), patch); res.status(200).json({ article }); } catch (error) { const status = articleErrorStatus(error); diff --git a/src/api/routes/unit.js b/src/api/routes/unit.js new file mode 100644 index 0000000..bce01f3 --- /dev/null +++ b/src/api/routes/unit.js @@ -0,0 +1,246 @@ +import * as db from '../../db/unit.js'; + +function requireSelectedCompany(req) { + const companyId = req.user.selectedCompanyId; + if (companyId == null) { + return { error: 'No company selected' }; + } + return { companyId }; +} + +function unitErrorStatus(error) { + if (error.message === 'Not authorized') return 403; + if (error.message === 'Company not found') return 404; + if (error.message === 'Unit not found') return 404; + if (error.message === 'Invalid unit') return 400; + if (error.message === 'Unit in use') return 409; + return 500; +} + +/** + * Registers unit routes for the authenticated user's selected company. + * @param {import('express').Application} app + * @param {string} [basePath=''] + */ +export function register(app, basePath = '') { + /** + * @openapi + * /unit: + * get: + * tags: [Unit] + * summary: List units + * description: Lists units for the authenticated user's selected company (default-language symbol/name). + * security: + * - cookieAuth: [] + * x-markdown: unit/list.md + * responses: + * 200: + * description: Unit list + * content: + * application/json: + * schema: + * type: object + * properties: + * units: + * type: array + * items: + * $ref: '#/components/schemas/Unit' + * 400: + * description: No company selected + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Not authorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 404: + * description: Company not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * put: + * tags: [Unit] + * summary: Upsert unit + * description: | + * Insert or update unit by `si` for the selected company. Upserts `unittext` for the company language. + * `basesifactor` must be finite and `> 0`. + * security: + * - cookieAuth: [] + * x-markdown: unit/upsert.md + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [si, basesi, basesifactor, symbol, name] + * properties: + * si: + * type: string + * basesi: + * type: string + * basesifactor: + * type: number + * symbol: + * type: string + * name: + * type: string + * example: + * si: l + * basesi: l + * basesifactor: 1 + * symbol: L + * name: Liter + * responses: + * 200: + * description: Upserted unit + * content: + * application/json: + * schema: + * type: object + * properties: + * unit: + * $ref: '#/components/schemas/Unit' + * 400: + * description: Invalid unit or no company selected + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Not authorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 404: + * description: Company not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * delete: + * tags: [Unit] + * summary: Delete unit + * description: Deletes `si` for the selected company. Fails with `Unit in use` if articles still reference it. + * security: + * - cookieAuth: [] + * x-markdown: unit/delete.md + * parameters: + * - in: query + * name: si + * required: true + * schema: + * type: string + * responses: + * 204: + * description: Deleted + * 400: + * description: Missing si or no company selected + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Not authorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 404: + * description: Company or unit not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 409: + * description: Unit in use + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + app.get(`${basePath}/unit`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + try { + const units = await db.list(req.user.id, selected.companyId); + res.status(200).json({ units }); + } catch (error) { + const status = unitErrorStatus(error); + if (status === 500) console.error('List units failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + app.put(`${basePath}/unit`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const { si, basesi, basesifactor, symbol, name } = req.body ?? {}; + + try { + const unit = await db.upsert(req.user.id, selected.companyId, { + si, + basesi, + basesifactor, + symbol, + name, + }); + res.status(200).json({ unit }); + } catch (error) { + const status = unitErrorStatus(error); + if (status === 500) console.error('Upsert unit failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + app.delete(`${basePath}/unit`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const si = typeof req.query.si === 'string' ? req.query.si.trim() : ''; + if (!si) { + return res.status(400).json({ error: 'si is required' }); + } + + try { + await db.remove(req.user.id, selected.companyId, si); + res.sendStatus(204); + } catch (error) { + const status = unitErrorStatus(error); + if (status === 500) console.error('Delete unit failed:', error); + res.status(status).json({ error: error.message }); + } + }); +} diff --git a/src/db/store.js b/src/db/store.js index a223112..45b91b4 100644 --- a/src/db/store.js +++ b/src/db/store.js @@ -13,6 +13,7 @@ const STORE_NAMESPACES = [ 'category', 'article', 'pic', + 'unit', 'lang', 'tax', 'google_auth', diff --git a/src/db/unit.js b/src/db/unit.js new file mode 100644 index 0000000..3479fac --- /dev/null +++ b/src/db/unit.js @@ -0,0 +1,16 @@ +import { getStore } from './store.js'; + +export async function list(googleAuthId, companyId) { + const store = await getStore(); + return store.unit.list(googleAuthId, companyId); +} + +export async function upsert(googleAuthId, companyId, data) { + const store = await getStore(); + return store.unit.upsert(googleAuthId, companyId, data); +} + +export async function remove(googleAuthId, companyId, si) { + const store = await getStore(); + return store.unit.delete(googleAuthId, companyId, si); +}