9.5 KiB
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.
libdashstore (
Project Overview — dashback + dashdbschema
Multi-root workspace:
/home/seb/src/dashback— Express backend (dashback, ESM,type: module). Depends onlibdashstoreviafile:../dashdbschema./home/seb/src/dashdbschema—libdashstorelibrary: MSSQL schema + domain store API. One class, methods mirrorsrc/store/<domain>/<action>.js./home/seb/src/dashback/quixpos2jtl/— separate JTL/QuixPOS bridge (ownpackage.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 inqueries/<domain>.js, DDL inschema.js(IF NOT EXISTS,SCHEMA_SCRIPTS). UsesOUTPUT INSERTED.*,TOP 1,WITH (UPDLOCK, HOLDLOCK).src/providers/sqlite3/— dev/test (BetterSqlite3Provider,better-sqlite3). Mostlyimport * as source from '../../mssql/queries/<x>.js'+translateQueries(source, '<domain>'). DDL insqlite3/schema.js(RETURNING,LIMIT 1). Auto-migratesarticle.track_stock,ledger.created_at/warehouse_idinsqlite3/index.js.bindParams(sqlite): strips leading@/:$, mapsDate→ISO,boolean→1/0,null/undefined→null. Both@skuandskukeys work.src/store/runner.js(makeRunner(provider, tx)) — normalizes pool / tx / callable runner. Store code callsrunner.query(sql, params)orrunner(sql, params).runner.dialect === 'sqlite3'selects query set. MSSQL legacy pool wrapped viawrapMssql.
Store conventions
- Every store module:
export default function <action>(provider) { ...; return async (googleAuthId, input, tx) => {...} }. googleAuthIdis alwaysgoogle_auth.id, nevercompanyuser.id.- Company scoping: no
companyIdarg.resolveSelectedCompanyAccess(provider)readsgoogle_auth.selected_company→{ companyId, companyuserId, rootCategoryId }. Missing → throwNo company selected. Exceptions taking explicitcompanyId:setSelectedCompany,companyuser.add,company.delete, internals. - SKU resolution:
store/article/resolveArticleBySku.js(RESOLVE_ARTICLE_ACCESS_BY_SKU) checks company + membership. ThrowsArticle not found/Parent not found/Not authorized. - Errors are plain
Errorwith exact message strings; HTTP mapping happens indashbackroute*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 inmanifest, lines inmanifestarticle, helpers insrc/store/manifestLines.js(normalizeLines,insertLines,parseDate,shapeLine) andmanifestDoc/helpers.js.insertLinesrejects parent articles (A parent article cannot be booked on documents (SKU)), resolves name/pu, ensures tax. - Numbers:
store/numbers/nextNumber.js, typessku, entity, delivery, adjustment.
Ledger + stock (important)
ledgeris append-only, tamper-chained:hash_version=1,prev_hash = SHA-512(canonical JSON of prev row)orTODO_TSE_BASED_INITHASHfor genesis. Seestore/ledger/add.js(shape,computePrevHash,INIT_HASH).- Stock is derived:
SUM(ledger.amount) GROUP BY warehouse, joinedledger → manifestarticle → manifest, matched onma.sku = article.sku OR ma.sku IN (SELECT sku FROM articlesupplier WHERE article_id = ...), filtered bym.company_id. Seeproviders/*/queries/article.js(warehousesJson/stock_calc) andposterminal.js(fBestand). Nostockcolumn on article. store/stock/adjust.jscreates implicit manifesttype='adjustment'(number fromadjustmentscheme) + onemanifestarticleline per item (amount: abs(amount)) + oneledgerrow 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.jsenforces the same guard for any warehouse-scoped entry (warehouse_id != null+ line has SKU): looks uparticlebyskuorarticlesupplier.skuin same company, rejects if any match hastrack_stock==0. Non-warehouse entries (e.g.PAYMENT, memoNOTE) are still allowed. Guard runs beforeGET_LATEST_ROW/INSERT_ROW, inside the same runner/tx.POST /ledgerwithwarehouse_idis therefore also blocked for untracked articles; delivery receipts booked via ledger go through the same check.article.track_stock:BIT/INTEGER DEFAULT 1. Managed viaarticle.createUnderParent({track_stock}),updateBySku,setTrackStockBySku,POST /article(track_stock/trackStock, default true).toggleParentsetstrack_stock=0when becoming parent. POS surfaces it asuse_stock '1'/'0'(store/posterminal/getProductList.js).
dashback (Express API)
src/index.js→src/api/index.js(startApi/stopApi). Env insrc/config/env.js(.env:PORTdefault 4208,HTTPS_PORT,GOOGLE_CLIENT_ID,JWT_SECRET,CORS_ORIGINS,DB_PROVIDER=mssql|sqlite,SQLITE_PATH,MSSQL_*).- Routes auto-loaded:
loadRoutes()walkssrc/api/routes/**/*.js, callsregister(app, basePath). Each route file owns its@openapiJSDoc +*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>.jsthin 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.jscolors>=400. - Docs:
npm run docs:api(scripts/generateApiDocs.js+src/api/openapi.jsswagger) →docs/api/routes.md+docs/api/routes/. Per-routex-markdownhints. - SSE:
src/api/sse.js(emit),routes/events.js. Stock adjust emitsstock,ledger,articles,article-<sku>. src/v1/— legacy POS compat (/v1/...,posAuth, staticuse_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 viaincludes()), uniqueness/in-use→409, else 500 +console.error. Stock/ledger mapStock tracking is disabled...→409(fix for reported bug:POST /stock/adjustontrack_stock:falsereturned201and wrote manifest+ledger; now409before 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 withcd /home/seb/src/dashdbschema && node scripts/test-ledger.js. - Do NOT run
scripts/test-api.jsfor verification — it is an interactive@clack/promptsmenu (MSSQL) that hangs waiting for input. dashdbschemascripts:init-schema,purge-db,reset-db,benchmark,generate-api-doc.js,generate-schema-graph.js.dashbackscripts:manageUsers.js,generateApiDocs.js,testImageCrop.js.dashbackhas nonpm 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})→ assertstock.adjustthrows,ledger.listBySkuempty, tracked article still adjusts.
Agent tips
- Add stock invariants in
dashdbschema/src/store/(not routes) so bothstock.adjustand directledger.addare covered; mirror the error string indashback/src/api/routes/{stock,ledger,delivery}.jsmappers if a new status is needed. - When touching SQL, update both
mssql/queries/*and checksqlite3/queries/*translation; watchOUTPUT INSERTEDvsRETURNING,TOP 1vsLIMIT 1. - Prefer
runner.query('SELECT ..., track_stock FROM article WHERE id = @articleId', { articleId })for per-article flags; for ledger-by-SKU use thearticle OR articlesupplier.skulookup scoped bycompany_id. - Keep
API.*.mdindashdbschema/(generated vianpm run api-doc) in mind but edit JSDoc source, not the markdown.