seb 54700a145e Add article.setPricesBySku to replace POS prices for a SKU.
Gives dashback a single store call that atomically replaces all price rows for the article’s poscustomergroup.
2026-08-01 05:16:50 +02:00
2026-08-01 03:46:08 +02:00
u
2026-07-30 20:35:53 +02:00
u
2026-07-31 15:36:48 +02:00
u
2026-07-31 19:49:07 +02:00
u
2026-07-31 19:49:07 +02:00
u
2026-07-31 15:36:48 +02:00
u
2026-07-31 15:36:48 +02:00

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.

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, posCustomerGroupId }
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. Creates POS customergroup (name POS), then inserts company with poscustomergroup_id (NOT NULL), then links the group via customergroup.company_id
  6. Links creator as companyuser (googleAuthId → company_id)

company.poscustomergroup_id is required (POS pricing in getTreeForCompany / category articles). Circular FK with customergroup.company_id: the group is inserted first with company_id null, then linked after the company row exists.

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, fullHash, 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
setPricesBySku (googleAuthId, sku, [{ price, tax, minmultiplier? }]) same shape as getBySku
addImage (googleAuthId, sku, imageBuffer) image result (see below)
deleteImage (googleAuthId, sku, fullHash) void
orderImages (googleAuthId, sku, { fullHashes }) { fullHashes }
delete (googleAuthId, sku) void

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. Assigns random fullHash (pic.hash, unique), computes thumbhash, links articlepic. Auth like getBySku. Errors: Article not found, Not authorized.

addImage returns:

{
  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)
  fullHash,               // random 64-char hex id (pic.hash, unique)
  thumbHash,              // base64 thumbhash of thumb preview
}

getBySku / updateBySku / setPricesBySku return:

{
  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 fullHash/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.

setPricesBySku replaces all price rows for the article’s company poscustomergroup_id in one transaction. Each entry: price → net, tax → tax rate (ensured via tax.ensure), minmultiplier defaults to 1. Empty array clears all POS prices. Returns the same object as getBySku. Errors: Article not found, Not authorized, POS customer group required, Invalid prices.

deleteImage removes articlepic and the pic row for this article (fullHash). Errors: Article not found, Image not found, Not authorized.

orderImages sets articlepic.sort to 0..n-1 in the given order. fullHashes must list every image of the article exactly once. Errors: Article not found, Invalid image order, Not authorized.

delete removes the article when it has no child articles (fatherarticle_id FK is ON DELETE NO ACTION). Cascades: articlehistory, articleean, categoryarticle, price, articletext, articlelistarticle, optionslistarticle, articlepic; deletes linked pic rows and owned articlelist / optionslist. manifestarticle.article_id is set to NULL and comment gets article <name> deleted appended. Errors: Article not found, Article has children, Not authorized.


pic

Lookup by pic.hash (fullHash from addImage.fullHash), not thumbhash. Random 64-char hex (crypto.randomBytes(32)), unique in DB.

Method Signature Returns
getImageByHash (googleAuthId, fullHash) { buffer, w, h, size, contentType }
getThumbByHash (googleAuthId, fullHash) { buffer, w, h, contentType }

Auth via articlepic → categoryarticle → resolveCategoryAccess. Errors: Image not found, Not authorized.

contentType is always image/avif.

// GET /api/image/:fullHash
const { buffer, contentType } = await store.pic.getImageByHash(req.user.id, fullHash);
res.set('Content-Type', contentType).send(buffer);

// GET /api/thumb/:fullHash
const thumb = await store.pic.getThumbByHash(req.user.id, fullHash);
res.set('Content-Type', thumb.contentType).send(thumb.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 })
PUT /api/article/:sku/prices article.setPricesBySku(googleAuthId, sku, [{ price, tax, minmultiplier? }])
DELETE /api/article/:sku article.delete(googleAuthId, sku)
POST /api/article/:sku/image article.addImage(googleAuthId, sku, imageBuffer)
DELETE /api/article/:sku/image/:fullHash article.deleteImage(googleAuthId, sku, fullHash)
PUT /api/article/:sku/images/order article.orderImages(googleAuthId, sku, { fullHashes })
GET /api/image/:fullHash pic.getImageByHash(googleAuthId, fullHash)
GET /api/thumb/:fullHash pic.getThumbByHash(googleAuthId, fullHash)

Scripts

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.

Description
No description provided
Readme 7.5 MiB
Languages
JavaScript 100%