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:
- Fails if
nameis taken (Company name not available) - Ensures language by
iso639(get or create) - Ensures tax rows for
taxrate,reducedtaxrate, and0→taxfreerateId - Creates root category (
parentId = null) - Creates POS
customergroup(namePOS), then inserts company withposcustomergroup_id(NOT NULL), then links the group viacustomergroup.company_id - 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:
- Fails if company missing / name taken by another company
- Ensures lang + tax rates
- 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)
parentIdexists, elseParent not found- Walk ancestors to root; find
companywithroot_category_id= root companyuserfor(googleAuthId, companyId), elseNot authorized- Insert
category(next siblingsort) +categorytext(name, slugifiedslug, 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) |
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 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.
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 }) |
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.