Files
dashlibstorage/README.md
2026-08-01 01:50:22 +02:00

243 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# libdashstore
MSSQL schema + domain API for Dash. One class, methods mirror the file path under `src/store/`.
**`mssql` is a peer dependency** — the host app (e.g. dashback) owns `ConnectionPool` and `import sql from 'mssql'`. libdashstore store code uses only the passed `pool` (no own `mssql` install at runtime). Scripts here (`init-schema`, `test-api`, …) use `mssql` from `devDependencies`.
```js
import sql from 'mssql';
import LibDashStore from 'libdashstore';
const pool = await sql.connect({ /* … */ });
const client = await new LibDashStore(pool); // runs initialize() (ensure schema, no purge)
```
SQL lives in `src/sql/<table>.js`. Schema DDL: `src/schema.js` / `import 'libdashstore/schema'`.
ID fields in the JS API use camelCase `…Id` (DB columns stay snake_case). `googleAuthId` is always `google_auth.id` (the logged-in user), not `companyuser.id`.
---
## Schema
**Kein Migrations-Code:** Wir gehen von einer frischen DB aus (`npm run reset-db`). Schema nur in `src/schema.js`; Anpassungen an einer laufenden DB manuell per `sqlcmd`, nicht im Repo.
| Method | Description |
| --- | --- |
| `initialize()` | Runs all `SCHEMA_SCRIPTS` (`IF NOT EXISTS`). Does **not** purge. |
| `purge()` | Drops all known tables + number procedure. |
---
## `company`
| Method | Signature | Returns |
| --- | --- | --- |
| `create` | `({ iso639, name, taxrate, reducedtaxrate, googleAuthId })` | `{ id, rootCategoryId, languageId, taxrateId, reducedtaxrateId, taxfreerateId }` |
| `update` | `(id, { iso639, name, taxrate, reducedtaxrate })` | `{ id, rootCategoryId, languageId, taxrateId, reducedtaxrateId, taxfreerateId }` |
| `delete` | `(companyId)` | `void` |
| `list` | `(googleAuthId)` | `{ id, name, rootCategoryId, languageId, taxrate, reducedtaxrate, taxfreerate }[]` |
### `create` (transaction)
For `POST /api/company`:
1. Fails if `name` is taken (`Company name not available`)
2. Ensures language by `iso639` (get or create)
3. Ensures tax rows for `taxrate`, `reducedtaxrate`, and `0` → `taxfreerateId`
4. Creates root category (`parentId = null`)
5. Inserts company with all FKs set
6. Links creator as `companyuser` (`googleAuthId` → `company_id`)
`company.poscustomergroup_id` (optional) points at the `customergroup` used for POS pricing in `getTreeForCompany` articles.
### `list`
Returns only companies where `googleAuthId` has a `companyuser` row.
### `update` (transaction)
For `PATCH /api/company/:id`:
1. Fails if company missing / name taken by another company
2. Ensures lang + tax rates
3. Updates `name`, `default_language_id`, `taxrate`, `reducedtaxrate`
(`taxfreerateId` / root category unchanged)
---
## `google_auth`
| Method | Signature | Returns |
| --- | --- | --- |
| `add` | `(email)` | `id` |
| `getByEmail` | `(email)` | `{ id, email, createdAt, selectedCompanyId } \| undefined` |
| `listAllowedEmails` | `()` | `{ id, email, createdAt, selectedCompanyId }[]` |
| `removeAllowedEmail` | `(email)` | `void` |
| `setSelectedCompany` | `(googleAuthId, companyId)` | `void` |
Backend mapping:
- Login: `getByEmail(email)`
- CLI user manager: `listAllowedEmails()` / `removeAllowedEmail(email)` / `add(email)`
- `PUT /api/auth/selected-company`: `setSelectedCompany(googleAuthId, companyId)`
---
## `companyuser`
| Method | Signature | Returns |
| --- | --- | --- |
| `add` | `(companyId, googleAuthId)` | `id` |
| `getByEmail` | `(email)` | `id[]` |
| `delete` | `(id)` | `void` |
---
## `lang`
| Method | Signature | Returns |
| --- | --- | --- |
| `create` | `(iso639)` | `id` |
| `getByIso639` | `(iso639)` | `{ id, iso639 } \| undefined` |
| `ensure` | `(iso639, tx?)` | `id` |
| `list` | `()` | `{ id, iso639 }[]` |
| `delete` | `(id)` | `void` |
---
## `tax`
| Method | Signature | Returns |
| --- | --- | --- |
| `create` | `(rate)` | `id` |
| `getByRate` | `(rate)` | `{ id, rate } \| undefined` |
| `ensure` | `(rate, tx?)` | `id` |
| `list` | `()` | `{ id, rate }[]` |
---
## `category`
| Method | Signature | Returns |
| --- | --- | --- |
| `createUnderParent` | `({ googleAuthId, parentId, name })` | `{ id, parentId, sort, name }` |
| `delete` | `({ googleAuthId, id })` | `void` — blocked by FK if `categoryarticle` or child categories remain |
| `getTreeForCompany` | `(googleAuthId, companyId)` | `{ languageId, tree, articles }` |
| `getCategoryArticles` | `(googleAuthId, categoryId)` | `{ id, sku, name, slug, net, taxRate, optionsCount, articleListCount, childCount, fatherArticleId, fatherSku, thumbW, thumbH, thumbHash }[]` |
### `createUnderParent` (transaction)
1. `parentId` exists, else `Parent not found`
2. Walk ancestors to root; find `company` with `root_category_id` = root
3. `companyuser` for `(googleAuthId, companyId)`, else `Not authorized`
4. Insert `category` (next sibling `sort`) + `categorytext` (`name`, slugified `slug`, company default language)
`getTreeForCompany` throws `Not authorized` if `googleAuthId` is not a `companyuser` of `companyId`, and `Company not found` if the company is missing, has no `root_category_id`, or no `default_language_id`.
---
## `article`
| Method | Signature | Returns |
| --- | --- | --- |
| `createUnderParent` | `({ googleAuthId, parentId, name })` | `{ id, parentId, name, sku }` |
| `getBySku` | `(googleAuthId, sku)` | detail object (see below) |
| `updateBySku` | `(googleAuthId, sku, { name })` | same shape as `getBySku` |
| `addImage` | `(googleAuthId, sku, imageBuffer)` | image result (see below) |
`addImage` accepts a full-size AVIF `Buffer`. Full image: if already AVIF and ≤1000×1000, stored as-is; otherwise scaled to max 1000×1000 (`pic.fullbinary`). Thumb always scaled to max 200×200 (`pic.thumbbinary`) via `sharp`. Computes `hash` / `thumbhash`, links `articlepic`. Auth like `getBySku`. Errors: `Article not found`, `Not authorized`.
`addImage` returns:
```js
{
picId,
sort,
full: { w, h, size }, // AVIF, max 1000×1000 (stored as-is when already AVIF and within bounds)
thumb: { w, h }, // scaled AVIF, max 200×200 (fit inside)
hash, // SHA-256 hex of full image bytes
thumbHash, // base64 thumbhash of thumb preview
}
```
`getBySku` / `updateBySku` return:
```js
{
id, sku, name, slug,
unitamount, weight,
unit: { si },
brand: { name, thumbW, thumbH, thumbHash } | null,
categoryIds: number[],
photos: { sort, fullHash, thumbW, thumbH, thumbHash }[],
eans: string[],
prices: { minmultiplier, net, taxRate, customergroupId }[],
optionsCount, articleListCount, childCount,
fatherArticleId, fatherSku,
}
```
`getCategoryArticles` / `getTreeForCompany().articles[]` keep the flat list shape (`net`, `taxRate`, single `thumbW`/`thumbH`/`thumbHash`).
`parentId` is the category id. Creates `article` + `articletext` (company default language) + `categoryarticle` in one transaction. Allocates `sku` via `getnextnumberwithprefixandpadding` (`type` `sku`; creates `numbers` row with empty prefix if missing). Default unit `pc` is ensured if missing (`unitamount` 1, `weight` 0). Auth via `resolveCategoryAccess(googleAuthId, parentId)` — `Parent not found` / `Not authorized`.
Auth via the article's `categoryarticle` category + `resolveCategoryAccess`. Errors: `Article not found`, `Not authorized`.
`updateBySku` updates `articletext` (company default language) in a transaction (`name` + slugified `slug`), then returns the same object as `getBySku`. Errors: `Article not found`, `Not authorized`, `Name required`.
---
## `pic`
Lookup by `pic.hash` (SHA-256 hex from `addImage`), not `thumbhash`.
| Method | Signature | Returns |
| --- | --- | --- |
| `getImageByHash` | `(googleAuthId, hash)` | `{ buffer, w, h, size, contentType }` |
| `getThumbByHash` | `(googleAuthId, hash)` | `{ buffer, w, h, contentType }` |
Auth via `articlepic` → `categoryarticle` → `resolveCategoryAccess`. Errors: `Image not found`, `Not authorized`.
`contentType` is always `image/avif`. In dashback:
```js
const { buffer, contentType } = await store.pic.getImageByHash(req.user.id, hash);
res.set('Content-Type', contentType).send(buffer);
```
---
## Backend route cheatsheet
| Route | Store call |
| --- | --- |
| `POST /api/company` | `company.create({ iso639, name, taxrate, reducedtaxrate, googleAuthId })` |
| `GET /api/company` | `company.list(googleAuthId)` |
| `PATCH /api/company/:id` | `company.update(id, { iso639, name, taxrate, reducedtaxrate })` |
| `DELETE /api/company/:id` | `company.delete(id)` |
| `PUT /api/auth/selected-company` | `google_auth.setSelectedCompany(googleAuthId, companyId)` |
| `GET /api/category/tree?companyId=` | `category.getTreeForCompany(googleAuthId, companyId)` |
| `POST /api/category` | `category.createUnderParent({ googleAuthId, parentId, name })` |
| `DELETE /api/category/:id` | `category.delete({ googleAuthId, id })` |
| `POST /api/article` | `article.createUnderParent({ googleAuthId, parentId, name })` |
| `GET /api/article/:sku` | `article.getBySku(googleAuthId, sku)` |
| `PATCH /api/article/:sku` | `article.updateBySku(googleAuthId, sku, { name })` |
| `POST /api/article/:sku/image` | `article.addImage(googleAuthId, sku, imageBuffer)` |
| `GET /api/image/:hash` | `pic.getImageByHash(googleAuthId, hash)` |
| `GET /api/image/:hash/thumb` | `pic.getThumbByHash(googleAuthId, hash)` |
---
## Scripts
```bash
npm run purge-db # drop all tables + procedure (wipe data)
npm run reset-db # purge + init-schema (empty DB, fresh schema)
npm run init-schema # ensure schema only (does NOT delete data)
npm run test:api # interactive API tester
npm run schema-graph # regenerate schema-graph.svg
```
Env: `MSSQL_SERVER`, `MSSQL_DATABASE`, `MSSQL_USER`, `MSSQL_PASSWORD`.