Files
dashback/.agents/skills/project-overview/SKILL.md
2026-10-04 21:22:02 +02:00

9.5 KiB

name: project-overview description: Architecture and conventions for the dashback + dashdbschema (libdashstore) monorepo. Use for any backend/store task: providers, store modules, routes, auth, ledger/stock, error mapping, testing.

Project Overview — dashback + dashdbschema

Multi-root workspace:

  • /home/seb/src/dashback — Express backend (dashback, ESM, type: module). Depends on libdashstore via file:../dashdbschema.
  • /home/seb/src/dashdbschema — libdashstore library: MSSQL schema + domain store API. One class, methods mirror src/store/<domain>/<action>.js.
  • /home/seb/src/dashback/quixpos2jtl/ — separate JTL/QuixPOS bridge (own package.json, Electron/CLI). Ignore unless task mentions JTL.

libdashstore (dashdbschema)

Entry: src/index.js → export default class LibDashStore. Constructor requires a provider with query/transaction/initialize/purge. Namespaces are wired there, e.g. this.stock = { adjust: stockAdjust(provider) }, this.ledger = { add, listByManifestarticleId, listBySku }, plus article, category, company, companyuser, delivery, order, quote, warehouse, ledger, stock, numbers, printer, labeltemplate, posterminal, supplier, articlesupplier, entity*, tax, unit, brand, lang, pic, manifestattachment, propertyname/articleproperty/categoryproperty, attributename/attributevalue/... , articlelist, optionslist.

Provider abstraction

  • src/providers/provider.js — contract (query(sql, params), transaction(fn), initialize(), purge()).
  • src/providers/mssql/ — production. Raw SQL in queries/<domain>.js, DDL in schema.js (IF NOT EXISTS, SCHEMA_SCRIPTS). Uses OUTPUT INSERTED.*, TOP 1, WITH (UPDLOCK, HOLDLOCK).
  • src/providers/sqlite3/ — dev/test (BetterSqlite3Provider, better-sqlite3). Mostly import * as source from '../../mssql/queries/<x>.js' + translateQueries(source, '<domain>'). DDL in sqlite3/schema.js (RETURNING, LIMIT 1). Auto-migrates article.track_stock, ledger.created_at/warehouse_id in sqlite3/index.js.
  • bindParams (sqlite): strips leading @/:$, maps Date→ISO, boolean→1/0, null/undefined→null. Both @sku and sku keys work.
  • src/store/runner.js (makeRunner(provider, tx)) — normalizes pool / tx / callable runner. Store code calls runner.query(sql, params) or runner(sql, params). runner.dialect === 'sqlite3' selects query set. MSSQL legacy pool wrapped via wrapMssql.

Store conventions

  • Every store module: export default function <action>(provider) { ...; return async (googleAuthId, input, tx) => {...} }.
  • googleAuthId is always google_auth.id, never companyuser.id.
  • Company scoping: no companyId arg. resolveSelectedCompanyAccess(provider) reads google_auth.selected_company → { companyId, companyuserId, rootCategoryId }. Missing → throw No company selected. Exceptions taking explicit companyId: setSelectedCompany, companyuser.add, company.delete, internals.
  • SKU resolution: store/article/resolveArticleBySku.js (RESOLVE_ARTICLE_ACCESS_BY_SKU) checks company + membership. Throws Article not found / Parent not found / Not authorized.
  • Errors are plain Error with exact message strings; HTTP mapping happens in dashback route *ErrorStatus() functions. Keep messages stable/greppable. Conflict → 409 (e.g. EAN already in use, Warehouse in use in ledger, Stock tracking is disabled...).
  • Manifest docs (delivery/order/quote/adjustment): header in manifest, lines in manifestarticle, helpers in src/store/manifestLines.js (normalizeLines, insertLines, parseDate, shapeLine) and manifestDoc/helpers.js. insertLines rejects parent articles (A parent article cannot be booked on documents (SKU)), resolves name/pu, ensures tax.
  • Numbers: store/numbers/nextNumber.js, types sku, entity, delivery, adjustment.

Ledger + stock (important)

  • ledger is append-only, tamper-chained: hash_version=1, prev_hash = SHA-512(canonical JSON of prev row) or TODO_TSE_BASED_INITHASH for genesis. See store/ledger/add.js (shape, computePrevHash, INIT_HASH).
  • Stock is derived: SUM(ledger.amount) GROUP BY warehouse, joined ledger → manifestarticle → manifest, matched on ma.sku = article.sku OR ma.sku IN (SELECT sku FROM articlesupplier WHERE article_id = ...), filtered by m.company_id. See providers/*/queries/article.js (warehousesJson/stock_calc) and posterminal.js (fBestand). No stock column on article.
  • store/stock/adjust.js creates implicit manifest type='adjustment' (number from adjustment scheme) + one manifestarticle line per item (amount: abs(amount)) + one ledger row per item (amount: signed) + articlehistory (Bestandskorrektur ±n (text)). Validates: parent → A parent article cannot have stock (SKU); Stückliste (articlelist_id != null) → A Stückliste cannot have manual stock adjustments (SKU); track_stock==0 → Stock tracking is disabled for article (SKU) (added as bugfix; checked before number allocation/manifest write, so batch is all-or-nothing).
  • store/ledger/add.js enforces the same guard for any warehouse-scoped entry (warehouse_id != null + line has SKU): looks up article by sku or articlesupplier.sku in same company, rejects if any match has track_stock==0. Non-warehouse entries (e.g. PAYMENT, memo NOTE) are still allowed. Guard runs before GET_LATEST_ROW/INSERT_ROW, inside the same runner/tx.
  • POST /ledger with warehouse_id is therefore also blocked for untracked articles; delivery receipts booked via ledger go through the same check.
  • article.track_stock: BIT/INTEGER DEFAULT 1. Managed via article.createUnderParent({track_stock}), updateBySku, setTrackStockBySku, POST /article (track_stock/trackStock, default true). toggleParent sets track_stock=0 when becoming parent. POS surfaces it as use_stock '1'/'0' (store/posterminal/getProductList.js).

dashback (Express API)

  • src/index.js → src/api/index.js (startApi/stopApi). Env in src/config/env.js (.env: PORT default 4208, HTTPS_PORT, GOOGLE_CLIENT_ID, JWT_SECRET, CORS_ORIGINS, DB_PROVIDER=mssql|sqlite, SQLITE_PATH, MSSQL_*).
  • Routes auto-loaded: loadRoutes() walks src/api/routes/**/*.js, calls register(app, basePath). Each route file owns its @openapi JSDoc + *ErrorStatus(error) mapper + requireSelectedCompany(req).
  • src/db/store.js — lazy singleton provider+store (ensureConnection), wraps every namespace method with timing logs (libstore <ns>.<method> (Nms) / FAILED). src/db/<domain>.js thin wrappers (store.stock.adjust, store.ledger.add, ...).
  • Auth: src/api/middleware/auth.js (Google SSO + JWT cookie/session, req.user.{id, selectedCompanyId}), src/api/routes/auth/*, scripts/manageUsers.js (allowlist). src/api/middleware/logger.js colors >=400.
  • Docs: npm run docs:api (scripts/generateApiDocs.js + src/api/openapi.js swagger) → docs/api/routes.md + docs/api/routes/. Per-route x-markdown hints.
  • SSE: src/api/sse.js (emit), routes/events.js. Stock adjust emits stock, ledger, articles, article-<sku>.
  • src/v1/ — legacy POS compat (/v1/..., posAuth, static use_stock: '0' defaults in some stubs). src/util/ — cert.js (self-signed), errorLog.js. certs/, data/ (sqlite + storage/content|fullbinary|thumbavif|thumbbinary), logs/errors/.
  • HTTP status conventions per route file: No company selected→400, Not authorized→403, *not found→404, validation→400 (often via includes()), uniqueness/in-use→409, else 500 + console.error. Stock/ledger map Stock tracking is disabled...→409 (fix for reported bug: POST /stock/adjust on track_stock:false returned 201 and wrote manifest+ledger; now 409 before any write).

Tests / scripts

  • Automated (sqlite temp DB, exit cleanly): dashdbschema/scripts/test-ledger.js, test-stueckliste-stock.js, test-sqlite-storage.js, test-variant-flow.js, test-tombstone.js, etc. Run with cd /home/seb/src/dashdbschema && node scripts/test-ledger.js.
  • Do NOT run scripts/test-api.js for verification — it is an interactive @clack/prompts menu (MSSQL) that hangs waiting for input.
  • dashdbschema scripts: init-schema, purge-db, reset-db, benchmark, generate-api-doc.js, generate-schema-graph.js. dashback scripts: manageUsers.js, generateApiDocs.js, testImageCrop.js. dashback has no npm test.
  • Quick regression pattern (sqlite in-memory/temp): create provider → new LibDashStore(provider) → google_auth.add → company.create → setSelectedCompany → unit.upsert → warehouse.create → article.createUnderParent({track_stock:false}) → assert stock.adjust throws, ledger.listBySku empty, tracked article still adjusts.

Agent tips

  • Add stock invariants in dashdbschema/src/store/ (not routes) so both stock.adjust and direct ledger.add are covered; mirror the error string in dashback/src/api/routes/{stock,ledger,delivery}.js mappers if a new status is needed.
  • When touching SQL, update both mssql/queries/* and check sqlite3/queries/* translation; watch OUTPUT INSERTED vs RETURNING, TOP 1 vs LIMIT 1.
  • Prefer runner.query('SELECT ..., track_stock FROM article WHERE id = @articleId', { articleId }) for per-article flags; for ledger-by-SKU use the article OR articlesupplier.sku lookup scoped by company_id.
  • Keep API.*.md in dashdbschema/ (generated via npm run api-doc) in mind but edit JSDoc source, not the markdown.