Files
picpeak/backend/src/utils/schemaCache.js
T
Luca d543949188 feat(crm): backend code — services + routes + utilities + tests
Brings in the full backend CRM stack on top of the consolidated
migration (60abe8c).

Services (CRM)
  - quoteService — full lifecycle (draft → sent → accepted → converted
    to event/invoice), Skonto + Storno + reissue paths
  - invoiceService — spawnInstallmentInvoices, updateInstallmentPlan,
    monthly-billing accumulator, payment-check tokens, dunning ladder
  - contractService — block-composable contract editor, in-browser
    signature flow, wet-PDF upload path, integrity check, audit trail
  - customerHoursService — per-entry locking, billing integration
  - dealsService — cross-document lineage (deal_uuid)
  - taxReportService — quarterly aggregates + CSV/PDF export
  - eventReminderService — pre-event customer reminder cron pass
  - _renderContext — shared issuer/recipient blocks across PDF types
  - pdfService extensions — custom-font registration, font picker

Routes (admin + public)
  - adminQuotes, adminInvoices, adminContracts, adminCalendar,
    adminDeals, adminTaxReport, adminDev, adminBusinessProfile
  - publicQuotes (accept/decline), publicContracts (sign),
    publicPaymentCheck
  - Extensions on adminEvents, adminCustomers, adminSettings,
    adminEmail, adminFeatureFlags, adminThumbnails, adminPhotos,
    adminCategories, adminUsers, adminArchives, adminDashboard
  - server.js wires the new mounts (kept upstream's noStoreCache on
    customer routes per 3-way merge)

Utilities
  - schemaCache (cached hasColumn lookups across services)
  - documentSequences (atomic gap-free numbering — §14 UStG)
  - safePath (path-containment guards at fs stream boundaries)
  - clientIp (sanctioned XFF reader for audit logs)
  - publicTokenGuards (pre-multer token validation + attempt counters)
  - numericHelpers (ensureInt / ensureNumber consolidation)
  - dateFormatter (formatShortDate + dateInputLang)
  - dbCompat extensions, iban + pdfFilename helpers, resolveLogoFile

Infrastructure
  - Bundled PDF fonts (Comic-Neue / IBM-Plex-Sans / Inter / Jost /
    Montserrat / Noto-Sans / Playfair-Display / Poppins)
  - Backend package.json + lock updates (pdfkit, signature_pad,
    qrcode, et al.)
  - Sample storage layout under storage/business-docs/quote/

Tests
  - 14 new test files covering quote/invoice/contract lifecycle,
    installment plan reshape, line-item hierarchy, customer hours,
    payment check, tax report PDF, IBAN parsing, filename sanitiser
2026-05-26 18:18:51 +02:00

97 lines
3.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* schemaCache — process-local memoisation for `db.schema.hasColumn`.
*
* **Why this exists**
*
* The CRM services on `feat/crm` are riddled with `hasColumn` guards
* because the schema has been drifting fast (every doc-feature
* migration adds a column that older installs may not yet have).
* Each call hits information_schema (Postgres) or sqlite_master
* (SQLite). On hot paths — `recordCustomerSignature`,
* `recordAdminCountersignature`, `getContractById`, the monthly
* billing pass — we issue 48 hasColumn checks per request, all
* for columns whose presence cannot change at runtime.
*
* The audit flagged this as a perf medium. Caching is safe because:
*
* 1. Schema-changing operations (migrations, `ALTER TABLE`) only
* run at boot via `run-migrations-safe.js`, BEFORE any service
* module accepts traffic. The cache is populated lazily after
* boot finishes, so the entries reflect post-migration state.
*
* 2. The Node process is the only schema authority. There's no
* sibling process sneaking in `ALTER TABLE` while we serve
* requests.
*
* 3. If a future migration path needs to run mid-flight, it can
* call `invalidateSchemaCache()` after the schema change.
*
* **What we cache**
*
* Just the boolean answer to `(table, column)`. A miss means the
* column doesn't exist on this install; a hit means it does. The
* cache key is `${table}.${column}`. There's no TTL — the entry is
* valid for the lifetime of the Node process.
*
* **What we DON'T cache**
*
* Negative results from `hasTable` failures (the table simply isn't
* there) — those go through the underlying call each time. That
* scenario is exceptional (table truly missing during a half-applied
* migration window) and we want it to surface, not get masked by a
* stale cache.
*
* **API**
*
* const { hasColumnCached, invalidateSchemaCache } = require('../utils/schemaCache');
* if (await hasColumnCached('contracts', 'signed_pdf_render_failed_at')) {
* ...
* }
*
* Drop-in replacement for `db.schema.hasColumn(...)` calls. The
* existing helper signature returns a Promise<boolean> so async
* call-sites need no shape change.
*/
const { db } = require('../database/db');
const cache = new Map();
async function hasColumnCached(table, column) {
const key = `${table}.${column}`;
if (cache.has(key)) return cache.get(key);
// Resolve via the underlying schema API. We deliberately don't
// catch errors here — if the call throws (e.g. DB connection lost
// mid-boot), the error surfaces to the caller exactly as it would
// have without the cache.
const present = await db.schema.hasColumn(table, column);
cache.set(key, present);
return present;
}
/**
* Drop every cached entry. Call this after a runtime schema change
* (rare — only the dev tooling does this today). Safe to call any
* time; the next hasColumnCached lookup will re-resolve.
*/
function invalidateSchemaCache() {
cache.clear();
}
/**
* Drop entries for a single table. Useful when only one table was
* altered and other tables' caches are still valid.
*/
function invalidateSchemaCacheForTable(table) {
const prefix = `${table}.`;
for (const k of cache.keys()) {
if (k.startsWith(prefix)) cache.delete(k);
}
}
module.exports = {
hasColumnCached,
invalidateSchemaCache,
invalidateSchemaCacheForTable,
};