Compare commits
2 Commits
c28b30d92a
...
ea51f30a8d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea51f30a8d | ||
|
|
6b58947064 |
35
AGENTS.md
Normal file
35
AGENTS.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# AGENTS.md — dashback (Express API)
|
||||
|
||||
ESM (`type: module`), Express 5, Node. Depends on `libdashstore` via `file:../dashdbschema`.
|
||||
|
||||
## Architecture
|
||||
|
||||
- Entry: `src/index.js` → `src/api/index.js` (`startApi`/`stopApi`, `loadRoutes()` walks `src/api/routes/**/*.js` → `register(app, basePath)`).
|
||||
- Each route file owns its `@openapi` JSDoc + `*ErrorStatus(err)` mapper + `requireSelectedCompany(req)`. Source of truth is code.
|
||||
- DB: `src/db/store.js` lazy singleton provider+store (timing logs `libstore <ns>.<method>`); `src/db/<domain>.js` are thin wrappers around `store.<ns>.<method>`.
|
||||
- Config: `src/config/env.js` (`.env`; `PORT` 4208, `HTTPS_PORT`, `GOOGLE_CLIENT_ID`, `JWT_SECRET`, `CORS_ORIGINS`, `DB_PROVIDER=mssql|sqlite`, `SQLITE_PATH`, `MSSQL_*`).
|
||||
- Auth: `src/api/middleware/auth.js` (Google SSO + JWT cookie, `req.user.{id,selectedCompanyId}`); allowlist via `scripts/manageUsers.js`.
|
||||
- SSE: `src/api/sse.js` (`emit`), `routes/events.js`. Stock adjust emits `stock`, `ledger`, `articles`, `article-<sku>`.
|
||||
- `src/v1/` legacy POS compat — don't touch unless task is POS. `quixpos2jtl/` is a separate Electron/CLI bridge — ignore unless task mentions JTL.
|
||||
- Docs: generated, never hand-edit — see below.
|
||||
|
||||
## Build / Test / Docs
|
||||
|
||||
```bash
|
||||
cp .env.example .env && npm install
|
||||
npm start # PORT 4208 (HTTPS 8443)
|
||||
npm run docs:api # regenerates docs/api/routes.md + docs/api/routes/ + openapi.json
|
||||
npm run manage-users # email allowlist
|
||||
```
|
||||
|
||||
- No `npm test`. Verify with syntax check or a self-exiting run on a **different port**; never leave a listener running — user keeps their own dev server.
|
||||
- Store contract lives in sibling repo: `../dashdbschema/API.md` + `API.<module>.md`, `../dashdbschema/README.md`. Read-only.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Sibling repos (`../dashdbschema`, `../dashfront`) are **read-only**: may read docs/code + `git -C ../<repo> log --oneline -20`, never write/install/start. Missing contract → ask human, don't guess. No fallbacks/defaults. See `.cursor/rules/sibling-repos.mdc`, `.cursor/rules/dont-start-the-API-server.mdc`.
|
||||
- After any route change run `npm run docs:api`. Docs index: `docs/api/README.md`.
|
||||
- HTTP mapping per route file: `No company selected→400`, `Not authorized→403`, `*not found→404`, validation (`includes()`)→400, uniqueness/in-use→409, else 500 + `console.error`. `Stock tracking is disabled…→409`.
|
||||
- Keep error strings stable/greppable — mapping depends on exact messages from `libdashstore`.
|
||||
- Stock/ledger invariants belong in `dashdbschema/src/store/`, not routes, so both `stock.adjust` and direct `ledger.add` are covered.
|
||||
- Full backend/store conventions: `.agents/skills/project-overview/SKILL.md`.
|
||||
@@ -27,6 +27,7 @@ _No content_
|
||||
| 401 | Unauthorized — `{"error":"…"}` |
|
||||
| 403 | Not authorized — `{"error":"…"}` |
|
||||
| 404 | Delivery not found — `{"error":"…"}` |
|
||||
| 409 | Delivery in use in ledger — `{"error":"…"}` |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -102,6 +102,7 @@ Updated delivery
|
||||
| 401 | Unauthorized — `{"error":"…"}` |
|
||||
| 403 | Not authorized — `{"error":"…"}` |
|
||||
| 404 | Delivery, supplier, or company not found — `{"error":"…"}` |
|
||||
| 409 | Delivery lines already booked on ledger — `{"error":"…"}` |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -20,8 +20,12 @@ function captureResponseBody(res) {
|
||||
const originalSend = res.send.bind(res);
|
||||
|
||||
res.json = (payload) => {
|
||||
body = payload;
|
||||
return originalJson(payload);
|
||||
let out = payload;
|
||||
if (res.statusCode >= 500 && payload && typeof payload === 'object' && 'error' in payload) {
|
||||
out = { ...payload, error: 'Internal server error' };
|
||||
}
|
||||
body = out;
|
||||
return originalJson(out);
|
||||
};
|
||||
|
||||
res.send = (payload) => {
|
||||
|
||||
@@ -172,6 +172,15 @@ function deliveryErrorStatus(error) {
|
||||
if (error.message === 'Invalid JSON in "data" field') return 400;
|
||||
if (error.message === 'Only PDF or text files are accepted as attachments') return 400;
|
||||
if (error.message?.includes('father article') || error.message?.includes('A father article')) return 400;
|
||||
if (
|
||||
error.message === 'Delivery in use in ledger' ||
|
||||
error.message === 'Delivery lines already booked on ledger' ||
|
||||
error.message?.includes('FOREIGN KEY constraint failed') ||
|
||||
error.message?.includes('foreign key constraint') ||
|
||||
error.message?.includes('REFERENCE constraint') ||
|
||||
error.code === 'SQLITE_CONSTRAINT_FOREIGNKEY' ||
|
||||
error.number === 547
|
||||
) return 409;
|
||||
return 500;
|
||||
}
|
||||
|
||||
@@ -348,7 +357,7 @@ export function register(app, basePath = '') {
|
||||
res.locals.error = error;
|
||||
const status = deliveryErrorStatus(error);
|
||||
if (status === 500) console.error('List deliveries failed:', error);
|
||||
res.status(status).json({ error: error.message });
|
||||
res.status(status).json({ error: status === 500 ? 'Internal server error' : error.message });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -399,7 +408,7 @@ export function register(app, basePath = '') {
|
||||
console.error('[delivery diag] Failed to dump payload:', logErr.message);
|
||||
}
|
||||
}
|
||||
res.status(status).json({ error: error.message });
|
||||
res.status(status).json({ error: status === 500 ? 'Internal server error' : error.message });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -550,6 +559,12 @@ export function register(app, basePath = '') {
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 409:
|
||||
* description: Delivery lines already booked on ledger
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* delete:
|
||||
* tags: [Delivery]
|
||||
* summary: Delete delivery
|
||||
@@ -591,6 +606,12 @@ export function register(app, basePath = '') {
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 409:
|
||||
* description: Delivery in use in ledger
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
app.get(`${basePath}/delivery/:id`, async (req, res) => {
|
||||
const selected = requireSelectedCompany(req);
|
||||
@@ -610,7 +631,7 @@ export function register(app, basePath = '') {
|
||||
res.locals.error = error;
|
||||
const status = deliveryErrorStatus(error);
|
||||
if (status === 500) console.error('Get delivery failed:', error);
|
||||
res.status(status).json({ error: error.message });
|
||||
res.status(status).json({ error: status === 500 ? 'Internal server error' : error.message });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -656,7 +677,21 @@ export function register(app, basePath = '') {
|
||||
if (status === 500) {
|
||||
logStoreError('Update delivery failed', error);
|
||||
}
|
||||
res.status(status).json({ error: error.message });
|
||||
let errorMessage = error.message;
|
||||
if (status === 500) {
|
||||
errorMessage = 'Internal server error';
|
||||
} else if (
|
||||
status === 409 ||
|
||||
error.message === 'Delivery lines already booked on ledger' ||
|
||||
error.message?.includes('FOREIGN KEY constraint failed') ||
|
||||
error.message?.includes('foreign key constraint') ||
|
||||
error.message?.includes('REFERENCE constraint') ||
|
||||
error.code === 'SQLITE_CONSTRAINT_FOREIGNKEY' ||
|
||||
error.number === 547
|
||||
) {
|
||||
errorMessage = 'Delivery lines are already booked on the ledger and cannot be modified';
|
||||
}
|
||||
res.status(status).json({ error: errorMessage });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -678,7 +713,13 @@ export function register(app, basePath = '') {
|
||||
res.locals.error = error;
|
||||
const status = deliveryErrorStatus(error);
|
||||
if (status === 500) console.error('Delete delivery failed:', error);
|
||||
res.status(status).json({ error: error.message });
|
||||
let errorMessage = error.message;
|
||||
if (status === 500) {
|
||||
errorMessage = 'Internal server error';
|
||||
} else if (status === 409) {
|
||||
errorMessage = 'Delivery in use in ledger';
|
||||
}
|
||||
res.status(status).json({ error: errorMessage });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -822,7 +863,7 @@ export function register(app, basePath = '') {
|
||||
res.locals.error = error;
|
||||
const status = deliveryErrorStatus(error);
|
||||
if (status === 500) console.error('Get delivery file failed:', error);
|
||||
res.status(status).json({ error: error.message });
|
||||
res.status(status).json({ error: status === 500 ? 'Internal server error' : error.message });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user