Skip to content

Shared Utilities - Deep Dive Documentation

Generated: 2026-03-29 Scope: saas-boilerplate/src/shared/ Files Analyzed: 15 Lines of Code: ~926 Workflow Mode: Exhaustive Deep-Dive Issues Found: 108 (17 Critical, 39 High, 35 Medium, 17 Low) Dead Code: 1 file completely unused (pick.ts), 3 single-consumer candidates

Overview

The shared/ directory is the cross-cutting utility layer for the entire Express.js + TypeORM backend. Every module in the system depends on at least one shared utility. These 15 files provide: database access (repository factory), HTTP response standardization, async error handling, permission caching/normalization, request-scoped context, CORS configuration, content slug generation, image processing, query helpers, and general-purpose caching.

Purpose: Provide reusable, framework-level utilities that all 30 backend modules depend on Key Responsibilities: DB access, error handling, response formatting, permission infrastructure, caching, content utilities Integration Points: Used by 53+ service files (getDbRepository), all 32 controllers (catchAsync, sendResponse), middleware pipeline (requestContext, permissionCache, getCorsOrigins)


Consumer Dependency Summary

File Consumers Dead? Tier
getDbRepository.ts 53 NO Universal infrastructure
catchAsync.ts 32 NO HTTP layer
sendResponse.ts 29 NO HTTP layer
getSortQuery.ts 14 NO Business logic
generateUniqueSlug.ts 6 NO Content utilities
getDataSource.ts 6 NO DB access
normalizeImageSrc.ts 5 NO Image processing
cache.ts 3 NO Caching
requestContext.ts 2 NO Auth infrastructure
permissionCache.ts 2 NO Auth infrastructure
sanitizeImageName.ts 2 NO Image processing
validatePostTypeAndId.ts 1 CANDIDATE MenuItem only
permissionNormalizer.ts 1 CANDIDATE hasPermission only
getCorsOrigins.ts 1 CANDIDATE middleware only
pick.ts 0 DEAD Unused

Complete File Inventory

1. cache.ts (52 LOC)

Purpose: In-memory cache wrapper around NodeCache. Provides TTL-based caching for roles, menu items, and analytics data with prefix-based bulk invalidation.

What Future Contributors Must Know: The inline comment says "default TTL: 5 minutes" but the actual value is 3600 seconds (1 hour). revalidateCache() is NOT atomic (delete-then-set). All cache is lost on process restart and NOT shared across multiple Node instances.

Exports: - addCache<T>(key: string, value: T, ttl: number = 3600): boolean - Store value with TTL - getCache<T>(key: string): T | undefined - Retrieve cached value - revalidateCache<T>(key: string, value: T, ttl: number = 3600): boolean - Delete and re-set (non-atomic) - deleteCache(key: string): number - Delete single key - deleteCacheByPrefix(prefix: string): number - O(n) scan and delete by prefix

Dependencies: - node-cache (npm) - In-process memory cache with TTL support

Used By: - analytic.service.ts - Analytics data caching - role.service.ts - Role permission caching (7+ invalidation calls) - menuItem.service.ts - Menu item tree caching (10+ invalidation calls)

Key Implementation Details: - Single global NodeCache instance, no memory limits configured - checkperiod: 120 (expired keys cleaned every 2 min) - deleteCacheByPrefix iterates ALL keys via cache.keys().filter() — O(n) per call

Side Effects: - Global state mutation on every operation - No logging on any operation (silent)

Error Handling: None. NodeCache methods don't throw in normal use. No validation of keys or TTL values.

Testing: No test files. 0% coverage.

Issues:

# Severity Description
C1 Critical Comment says "5 minutes" but code sets 3600s (1 hour) — misleading documentation
C2 Critical revalidateCache is non-atomic: del() then set() — race condition causes phantom cache misses
H1 High No TTL validation — negative/zero TTL silently accepted with undefined behavior
H2 High deleteCacheByPrefix is O(n) on total cache size, called 17+ times per mutation cycle
H3 High Type safety leakage — stores via cache.set() but callers use JSON.parse() on retrieval
M1 Medium No memory limit configured — unbounded cache growth possible
M2 Medium Global singleton — not testable, state pollution between tests
M3 Medium deleteCache return value (0/1) ignored by all callers

2. catchAsync.ts (14 LOC)

Purpose: Higher-order function wrapping async Express route handlers. Catches rejected promises and passes errors to Express error middleware via next(err). Prevents unhandled promise rejections from crashing the server.

What Future Contributors Must Know: This ONLY catches async/promise rejections. If a handler throws synchronously before any await, the error may NOT be caught. Ensure all wrapped handlers are genuinely async.

Exports: - catchAsync(fn: RequestHandler): (req, res, next) => Promise<void> - Wrap async handler

Dependencies: - express types: NextFunction, Request, RequestHandler, Response

Used By: - All 32 controller files (every route handler in the system) - 3 middleware files: auth.ts, contextMiddleware.ts, permissionMiddleware.ts

Key Implementation Details: - try { await fn(req, res, next) } catch(err) { next(err) } pattern - No error transformation, logging, or context enrichment - Default export (not named export)

Error Handling: Delegates all errors to Express error middleware. No local logging or error wrapping.

Testing: No test files. 0% coverage.

Issues:

# Severity Description
H1 High Synchronous throws before any await may not be caught — server crash risk
H2 High No error context added — downstream error handler gets raw error with no handler/route info
M1 Medium If handler calls res.send() then awaits something that throws, next(err) fires after headers sent
M2 Medium No timeout/cancellation — long-running handlers hang indefinitely

3. getDataSource.ts (14 LOC)

Purpose: Guard function that retrieves the TypeORM DataSource singleton and validates it is initialized before returning. Used when raw DataSource access is needed beyond standard repository operations.

What Future Contributors Must Know: Error message references connectToDB() which lives in src/db/connection.ts. Must be called during app startup before any route handler executes. In serverless environments, ensure initialization happens in handler entry point.

Exports: - getDataSource(): DataSource - Returns initialized DataSource or throws

Dependencies: - getAppDataSource from ../config/dbConfig - DataSource singleton factory

Used By: - analytic.service.ts, auth/UserHandler.ts, auth/LoginAttemptHandler.ts, auth/OtpHandler.ts, payment.service.ts, purchase.service.ts

Key Implementation Details: - Calls getAppDataSource(), checks isInitialized, throws if false - Error message: "DataSource is not initialized. Call connectToDB() before using repositories." - Same error message duplicated in getDbRepository.ts (2 places)

Error Handling: Throws descriptive error if DB not initialized. No recovery path.

Testing: No test files. 0% coverage.

Issues:

# Severity Description
H1 High Error message references connectToDB() function that callers may not know how to locate
H2 High Duplicate error message across 3+ locations — maintenance burden
M1 Medium No explicit return type annotation — TypeScript infers any

4. getDbRepository.ts (23 LOC)

Purpose: Type-safe factory for TypeORM repositories. Provides generic getDbRepository<T>(Entity) for type-safe access and legacy dbRepository(tableName) for string-based access. Most heavily consumed utility in the entire backend (53 files).

What Future Contributors Must Know: Always prefer getDbRepository<Entity>(Entity) over dbRepository("tableName"). The string-based variant returns Repository<any> with no type safety. Both functions check DataSource initialization on every call.

Exports: - getDbRepository<T>(entity: { new(): T }): Repository<T> - Type-safe repository access - dbRepository(tableName: string): Repository<any> - String-based legacy access (likely dead code)

Dependencies: - getAppDataSource from ../config/dbConfig

Used By: - 53 files: all service layers, auth handlers, policies, seed files, middleware

Key Implementation Details: - Duplicate initialization check in both functions (lines 6-8 and 16-18) - { new(): T } constructor constraint requires zero-arg constructors (standard for TypeORM entities) - dbRepository() returns untyped Repository<any> — typos in table name cause runtime errors

Error Handling: Throws descriptive error if DataSource not initialized (same message as getDataSource.ts).

Testing: No test files. 0% coverage.

Issues:

# Severity Description
H1 High Duplicate initialization check in both functions — DRY violation
H2 High dbRepository(tableName: string) is completely untyped — runtime errors on typos
H3 High { new(): T } constructor constraint not validated at compile time for complex entities
M1 Medium dbRepository() likely dead code — 0 usages found in consumer analysis
M2 Medium Same error message duplicated across getDataSource.ts and getDbRepository.ts (3 places)

5. getSortQuery.ts (27 LOC)

Purpose: Parses and validates orderBy and order query parameters from HTTP requests. Returns normalized sort field + direction with safe defaults. Used in 14 service modules for list endpoints.

What Future Contributors Must Know: Returns raw orderBy string as field without validating it against entity column names. Caller must ensure field is a valid column to prevent SQL injection. Default sort is createdAt DESC.

Exports: - getSortQuery(query: Record<string, unknown>): { field: string; order: "ASC" | "DESC" } - Parse sort params

Dependencies: - httpStatus - HTTP status codes - ApiError - Custom error class

Used By: - 14 service files: User, Payment, Permission, Blog, Package, Subscription, Role, Public, Purchase, Menu, MenuItem, Contact, Cupon, GenericPage

Key Implementation Details: - Defaults to { field: "createdAt", order: "DESC" } when both params missing - Validates order is "ASC" or "DESC" (case-insensitive), throws 400 on invalid - Validates orderBy exists and is string type - Dead code in return statement: query.orderBy || "createdAt" — fallback can never trigger

Error Handling: Throws ApiError with 400 status for invalid sort params.

Testing: No test files. 0% coverage.

Issues:

# Severity Description
H1 High No SQL injection prevention — returns raw orderBy without column whitelist validation
H2 High Missing type coercion validation — order: true or order: {} silently coerced
M1 Medium Default field "createdAt" hardcoded — assumes all entities have this column
M2 Medium Line 12 mutates input query.order — unexpected side effect on caller's object
M3 Medium Dead code: fallback || "createdAt" in return can never execute

6. sendResponse.ts (22 LOC)

Purpose: Generic typed utility that sends standardized JSON responses from Express handlers. Enforces consistent response shape ({ success, message, meta, data }) across all 29+ controllers.

What Future Contributors Must Know: The meta field expression jsonData.meta || null || undefined evaluates to undefined when meta is not provided, causing the meta key to be stripped from JSON output entirely (not null). This is inconsistent — some responses have meta: null, others omit meta entirely.

Exports: - sendResponse<T>(res: Response, jsonData: { statusCode, success, message, meta?, data }): void - Send JSON response

Dependencies: - express types: Response

Used By: - 29 controller files + payment.route.ts

Key Implementation Details: - Sets HTTP status code then sends JSON with res.status(code).json() - Generic type <T> for data field — unconstrained, allows circular refs, functions, symbols - No explicit void return — callers may continue execution after response sent

Error Handling: None. No validation of statusCode range or success/statusCode consistency.

Testing: No test files. 0% coverage.

Issues:

# Severity Description
C1 Critical meta: jsonData.meta \|\| null \|\| undefined — meta key missing from response when not provided (should be null)
H1 High statusCode not validated — values like 999, -1, or string "200" cause undefined behavior
H2 High Generic <T> unconstrained — circular refs, functions, symbols silently drop from JSON
H3 High No explicit return — callers may execute code after response already sent
M1 Medium Allows contradictory statusCode: 500, success: true
M2 Medium data: T | null | undefined — inconsistent null contract

7. pick.ts (12 LOC) -- DEAD CODE

Purpose: Extracts subset of fields from object by key names. Typed version of lodash.pick() with hasOwnProperty guard against prototype pollution. Intended for sanitizing entities before API responses.

What Future Contributors Must Know: This file is completely unused — 0 consumers across the entire codebase. Consider removing it.

Exports: - pick<T, K extends keyof T>(obj: T, keys: K[]): Partial<T> - Extract object fields

Dependencies: None (pure function)

Used By: NONE — 0 consumers

Key Implementation Details: - Iterates keys array, checks Object.hasOwnProperty.call(obj, key) (prototype pollution safe) - Returns Partial<T> (overly loose — should be Pick<T, K>)

Testing: No test files. 0% coverage.

Issues:

# Severity Description
H1 High Completely dead code — 0 consumers, exported but never imported
H2 High No null/undefined check on obj parameter — silently returns {}
M1 Medium Return type Partial<T> too loose — should be Pick<T, K> for type accuracy

8. validatePostTypeAndId.ts (120 LOC)

Purpose: Validates that a content entity (blog, page, category, tag, package) exists in database by ID. Used by MenuItem service to ensure menu items only link to existing content. Also provides validatePostIfProvided() for optional validation.

What Future Contributors Must Know: The falsy check !postId on line 21 means postId: 0 (a valid DB ID) skips validation entirely and returns true. Also, queries fetch entire entity for a simple existence check — wasteful.

Exports: - validatePostTypeAndId(postType: "blogs"|"pages"|"categories"|"tags"|"packages", postId: number): Promise<boolean> - Strict validation - validatePostIfProvided(postType?: string|null, postId?: number|null): Promise<boolean> - Optional validation

Dependencies: - 5 entity imports: Blog, GenericPage, BlogCategory, BlogTag, Package - getDbRepository from ./getDbRepository - httpStatus, ApiError

Used By: - menuItem.service.ts only (1 consumer — dead code candidate)

Key Implementation Details: - Switch statement maps postType string to entity class - repository.findOne({ where: { id: postId } }) — fetches full entity for existence check - Return type is Promise<boolean> but never returns false — always returns true or throws - Soft-deleted entities excluded by default (TypeORM behavior)

Error Handling: Throws 404 if entity not found, 400 for invalid params. Catches and wraps DB errors as 500.

Testing: No test files. 0% coverage.

Issues:

# Severity Description
C1 Critical if (!postType \|\| !postId) return truepostId: 0 is falsy, skips validation entirely
H1 High No validation that postId is positive integer — negative, float, NaN, Infinity all query DB
H2 High Entity name inconsistency: import BlogTag from BlogTags.ts (plural file, singular class)
H3 High No error logging — DB errors (connection loss, timeouts) silently wrapped as 500
M1 Medium Fetches entire entity for existence check — should use select: ['id'] or count()
M2 Medium Soft-delete behavior undocumented — menu items can't reference soft-deleted posts (intended?)
M3 Medium Return type Promise<boolean> but never returns false — misleading signature

9. normalizeImageSrc.ts (28 LOC)

Purpose: Strips configured API base URL from absolute image paths to convert them to relative paths for frontend consumption. If URL doesn't match base, returns path as-is.

What Future Contributors Must Know: Contains a console.log({src}) debug statement that executes on EVERY call in production. Also, if api_base_url config has/lacks trailing slash, previously stored URLs with opposite format won't match.

Exports: - normalizeImageSrc(src: string): string - Strip base URL from image path

Dependencies: - config from ../config - App config with api_base_url

Used By: - blog.controller.ts, blog.service.ts, genericPage.controller.ts, genericPage.service.ts, user.service.ts

Key Implementation Details: - Checks if src starts with config.api_base_url - If match: strips base URL, ensures leading / - If no match: ensures leading / on relative paths, returns absolute URLs as-is

Testing: No test files. 0% coverage.

Issues:

# Severity Description
C1 Critical console.log({src}) left in production code — console spam on every image normalization
H1 High No trailing slash normalization on api_base_url — config change breaks URL matching
H2 High api_base_url type is string \| undefined — no explicit handling of undefined case
M1 Medium Protocol-relative URLs (//cdn.example.com) work by accident, not by design
M2 Medium No XSS validation on returned URLs — depends on callers for defense-in-depth

10. sanitizeImageName.ts (24 LOC)

Purpose: Converts filenames to URL-safe format: lowercase, spaces→hyphens, strip special characters, preserve extension.

What Future Contributors Must Know: The regex [^a-z0-9-] removes ALL non-alphanumeric characters including underscores and dots. This causes severe data loss: my_file_backup.pdfmyfilebackup.pdf, and two different filenames can collide: [email protected] and hello#world.jpg both become helloworld.jpg.

Exports: - sanitizeImageName(filename: string): string - Sanitize filename to URL-safe format

Dependencies: None (pure function)

Used By: - blog.controller.ts, genericPage.controller.ts

Key Implementation Details: - Splits filename at last . to separate name and extension - Lowercases, replaces spaces with hyphens - Strips everything except [a-z0-9-] (removes underscores, dots in name) - Reattaches extension

Testing: No test files. 0% coverage.

Issues:

# Severity Description
C1 Critical Aggressive [^a-z0-9-] removal causes filename data loss and collisions
H1 High Underscores stripped inconsistently (removed from name, could appear in extension)
M1 Medium No maximum filename length enforcement — filesystem limit (255 bytes) not checked
M2 Medium Race condition if two concurrent uploads produce same sanitized name
L1 Low Unicode characters silently stripped (cafe.jpg → caf.jpg)

11. generateUniqueSlug.ts (47 LOC)

Purpose: Generates database-unique URL slugs by checking for collisions and appending numeric suffixes (-1, -2, etc.). Uses slugify library for initial slug generation.

What Future Contributors Must Know: The collision-checking while loop is unbounded — if 10,000 slugs exist, it makes 10,001 sequential DB queries. Also has a race condition: two concurrent calls can both find a slug is "available" and both try to insert it. A duplicate implementation exists in blog.utils.ts.

Exports: - toSlug(text: string): string - Wrapper around slugify() library - generateUniqueSlug<T>(value: string, repo: Repository<T>, slugColumn: keyof T, excludeId?: number): Promise<string> - Generate unique slug with DB collision check

Dependencies: - typeorm: Not, ObjectLiteral, Repository - slugify (npm)

Used By: - blogCategory.service.ts, blogTag.service.ts, genericPage.service.ts, package.service.ts, menu.service.ts, slug.service.ts - Note: blog.service.ts uses a DUPLICATE implementation in blog.utils.ts

Key Implementation Details: - Numeric suffix removal: replace(/-\d+$/, '') misinterprets user content ("Product 2024" → "product") - N+1 query pattern: each collision = one additional sequential DB query - as any type cast suppresses TypeScript checking on column name

Testing: No test files. 0% coverage.

Issues:

# Severity Description
C1 Critical Unbounded while loop — unlimited sequential DB queries per slug generation (DoS vector)
C2 Critical Race condition — concurrent calls can both claim same slug, causing unique constraint violation
H1 High N+1 query pattern — should batch collision check into single query
H2 High as any suppresses type checking on column name — hides wrong-column bugs
H3 High Empty input not validated — generateUniqueSlug("") produces undefined behavior
M1 Medium replace(/-\d+$/, '') strips legitimate numeric suffixes ("Product 2024" → "product")
M2 Medium Duplicate implementation in blog.utils.ts — two codebases to maintain
L1 Low Typo in comment: "paractice" → "practice"

12. permissionCache.ts (183 LOC)

Purpose: Request-scoped permission caching layer built on requestContext.ts AsyncLocalStorage. Caches user permissions as a Set<string> to avoid repeated DB lookups during a single HTTP request. Provides initialization, lookup, mutation, validation, and stats APIs.

What Future Contributors Must Know: getCachedPermissions() returns a direct reference to the mutable Set<string> — callers can add/remove permissions by mutating the returned Set, enabling privilege escalation. Also, addPermissionToCache() is exported but NEVER called — dead code with untested mutation semantics.

Exports: - PermissionCacheError (class) - Custom error for cache operations - initializePermissionCache(user: RequestUser, roles: Role[]): PermissionCache - Build cache from roles - getCachedPermissions(): Set<string> | null - Get permission set (3-state: Set/null) - hasPermissionInCache(permission: string): boolean | null - Check permission (3-state: true/false/null) - getCachedRoles(): Role[] | null - Get cached roles - addPermissionToCache(permission: string): void - Add permission (DEAD CODE) - isCacheValid(): boolean - Check cache ownership - getCacheStats(): { permissionCount, rolesCount, age } | null - Debug/monitoring - invalidateCache(): void - Clear cache - validateCacheOwnership(): boolean - Security check

Dependencies: - Role, Permission from entity layer - requestContext.ts: PermissionCache, RequestUser, getPermissionCache, setPermissionCache, clearPermissionCache, getCurrentUser

Used By: - contextMiddleware.ts - Initializes cache per request - helper/hasPermission.ts - Checks permissions from cache

Key Implementation Details: - Flattens multi-role permissions into Set<string> (deduplicates by permission.route) - Every getter function repeats user ID ownership check (4 duplicate validation blocks) - Ownership check compares cache.userId with getCurrentUser().id — redundant since AsyncLocalStorage is request-isolated - 3-state return logic: boolean | null where null = cache miss

Testing: No test files. 0% coverage.

Issues:

# Severity Description
C1 Critical getCachedPermissions() returns direct mutable Set reference — privilege escalation via set.add()
C2 Critical User ID checks repeated 4 times across getters — redundant with AsyncLocalStorage isolation
C3 Critical addPermissionToCache() exported but never called — dead code mutation function
H1 High 3-state return boolean \| null is confusing — callers must check !== null explicitly
H2 High No cache invalidation strategy — no TTL, timestamp field unused except in stats
H3 High isCacheValid() incomplete — doesn't check roles populated, permissions non-empty, or timestamp
H4 High Repeated try/catch pattern swallows real errors (TypeError, DB errors) in 4 functions
M1 Medium No permission string normalization during initialization — format mismatches possible
M2 Medium Roles array stored by reference — mutations by caller reflect in cache
M3 Medium O(N) security checks on hot path — each hasPermission() call triggers 2+ getCurrentUser() calls
L1 Low Mutable Set<string> type should be ReadonlySet<string>
L2 Low No unit tests for defensive exception paths
L3 Low console.error logging not configurable — should use structured logger

13. permissionNormalizer.ts (215 LOC)

Purpose: Permission string format converter supporting route format (/users/create) and dot notation (users.create). Detects format, parses resource/action, normalizes to canonical route format for consistent DB lookups. Enables flexible permission input while storing a single canonical format.

What Future Contributors Must Know: The ACTION_MAPPING (maps updateedit, removedelete, etc.) is computed but completely ignored in convertDotToRoute() — commented-out code that should use the mapping was never uncommented. This is the root cause of permission format mismatch issues. Also, paths with 3+ segments silently discard middle segments.

Exports: - PermissionFormatError (class) - Custom error - PermissionFormat (enum) - ROUTE / DOT_NOTATION - NormalizedPermission (interface) - { original, normalized, format, resource, action } - detectPermissionFormat(permission: string): PermissionFormat - Heuristic format detection - parseRouteFormat(permission: string): { resource, action } - Parse /users/create - parseDotNotation(permission: string): { resource, action } - Parse users.create - convertDotToRoute(permission: string): string - Convert dot → route (BUGGY) - convertRouteToDot(permission: string): string - Convert route → dot - normalizePermission(permission: string): NormalizedPermission - Full normalization - normalizePermissionString(permission: string): string - Simplified normalization - isValidPermissionFormat(permission: string): boolean - Format validation - getPermissionVariants(permission: string): string[] - Generate all format variants - extractResource(permission: string): string - Get resource name - extractAction(permission: string): string - Get action name - ACTION_MAPPING (const) - Action alias map (unused)

Dependencies: None (pure utility module)

Used By: - helper/hasPermission.ts only (1 consumer)

Key Implementation Details: - Format detection heuristic: starts with / → ROUTE, contains . → DOT, default → ROUTE - Route parsing: 1 segment = resource (action defaults to "read"), 2 segments = resource/action, 3+ = first/last (middle lost) - ACTION_MAPPING maps aliases (updateedit, removedelete, view"", list"") - convertDotToRoute() computes ACTION_MAPPING lookup then discards result (line 92-100) - getPermissionVariants() generates incomplete variants — doesn't include action aliases

Testing: No test files. 0% coverage.

Issues:

# Severity Description
C1 Critical ACTION_MAPPING computed but ignored in convertDotToRoute() — root cause of format mismatches
C2 Critical ACTION_MAPPING incomplete: 4 actions map to empty string, semantics undocumented
H1 High Paths with 3+ segments silently lose middle segments — /users/posts/create → resource=users, action=create
H2 High Bidirectional conversion is lossy — convertDotToRoute + convertRouteToDot don't round-trip
H3 High getPermissionVariants() doesn't include action alias variants — cache misses expected
H4 High as keyof typeof ACTION_MAPPING type cast on user input — typos silently pass through
M1 Medium No resource name validation — path traversal strings accepted (e.g., ../../admin)
M2 Medium Format detection ambiguous for /users.create (has both / and .)
M3 Medium Incomplete action alias coverage — missing destroy, copy, export, bulk-*
L1 Low getPermissionVariants catch-all returns [permission] — masks normalization errors
L2 Low No documentation of format semantics, canonical format, or round-trip guarantees

14. requestContext.ts (122 LOC)

Purpose: Request-scoped context management using Node.js AsyncLocalStorage. Stores per-request user identity, permission cache, and request ID. Provides typed interfaces and error classes for thread-safe context access across async call stacks. Critical infrastructure for passing authenticated user data without explicit parameter threading.

What Future Contributors Must Know: AsyncLocalStorage is inherently request-isolated — the repeated cache.userId !== context.user.id security checks across permissionCache.ts and this file are redundant and can never trigger in normal operation. The RequestUser interface has role as a string (name) AND roleId as a number — these can desync if data is stale.

Exports: - RequestUser (interface) - { id, email, role, roleId, userRoles? } - PermissionCache (interface) - { userId, permissions: Set<string>, roles: Role[], timestamp } - RequestContext (interface) - { user, permissionCache?, requestId? } - RequestContextError, UserNotFoundError, ContextNotFoundError (error classes) - getRequestContext(): RequestContext | undefined - Get current context - getCurrentUser(): RequestUser - Get user or throw - runWithContext<T>(context, fn): T - Execute function within context - setPermissionCache(cache): void - Store permission cache - getPermissionCache(): PermissionCache | undefined - Retrieve permission cache - clearPermissionCache(): void - Clear cache (safe no-op) - hasRequestContext(): boolean - Check if in request context - getCurrentUserId(): number - Get user ID or throw - isCurrentUserSuperAdmin(): boolean - Check super_admin (graceful false on error)

Dependencies: - async_hooks (Node.js built-in): AsyncLocalStorage - Role from ../entity/Role

Used By: - contextMiddleware.ts - Establishes context per request via runWithContext() - helper/hasPermission.ts - Reads user and permissions from context

Key Implementation Details: - Single AsyncLocalStorage<RequestContext> instance (module scope) - runWithContext() wraps asyncLocalStorage.run() — context available to all async code within - setPermissionCache() validates cache.userId === context.user.id before storing - getPermissionCache() re-validates ownership on read and auto-clears on mismatch - isCurrentUserSuperAdmin() compares user.role === 'super_admin' (case-sensitive)

Testing: No test files. 0% coverage.

Issues:

# Severity Description
C1 Critical Phantom user ID mismatch checks — redundant with AsyncLocalStorage isolation, adds complexity
H1 High RequestUser.role (string) can desync with RequestUser.roleId (number) — no consistency guarantee
H2 High getPermissionCache() doesn't check context.user exists — crashes on context.user.id if user is null
H3 High setPermissionCache() doesn't validate cache.userId is a positive integer
M1 Medium isCurrentUserSuperAdmin() silently returns false on any error — masks bugs
M2 Medium clearPermissionCache() is silent no-op when no context — caller doesn't know if clear succeeded
L1 Low No context lifetime management — relies on Express request lifecycle for cleanup
L2 Low Only console.error for logging — should use structured logger

15. getCorsOrigins.ts (25 LOC)

Purpose: Parses CORS allowed origins from ALLOWED_ORIGINS environment variable. Supports two formats: JSON array (["http://localhost:3000"]) and comma-separated list (http://localhost:3000, http://example.com). Returns undefined for missing config, letting CORS middleware use defaults.

What Future Contributors Must Know: Malformed JSON with a typo (e.g., missing closing bracket) silently falls back to comma-split — the bracket becomes part of the origin string, CORS fails silently, and you spend hours debugging. Also, an explicit empty array [] (deny all) is returned as [] but there's no way to distinguish "not configured" (undefined) from "explicitly empty" in CORS middleware.

Exports: - getCorsOrigins(): string[] | undefined - Parse CORS origins from config

Dependencies: - config from ../config - App config with .cors property

Used By: - app/middlewares/middleware.ts only (1 consumer)

Key Implementation Details: - Try JSON.parse → if array, return mapped to strings - Catch → comma-split fallback with trim - If JSON parsed but NOT array (e.g., {}), falls through to return undefined - No origin format validation (accepts "localhost", "*", "javascript:alert()")

Testing: No test files. 0% coverage.

Issues:

# Severity Description
C1 Critical Malformed JSON falls back to comma-split without warning — bracket/quote chars become part of origin strings
H1 High Empty array [] indistinguishable from "not configured" — different CORS semantics
H2 High No validation of origin format — accepts invalid schemes, wildcards, javascript: URIs
M1 Medium No logging of parse path — impossible to debug CORS config issues
L1 Low Trailing empty entries from "origin1, , origin2" not filtered
L2 Low No type safety on config.cors — assumed string but no compile-time guarantee

Contributor Checklist

Risks & Gotchas

  • getDbRepository is the #1 dependency hotspot (53 consumers) — changes here affect the entire backend
  • catchAsync wraps ALL route handlers — a bug here crashes ALL endpoints
  • sendResponse shapes ALL API responses — format changes are breaking changes for all frontend consumers
  • permissionCache returns mutable Set — callers can escalate privileges by calling .add()
  • generateUniqueSlug has unbounded loop — can DoS database under adversarial input
  • cache.ts is process-local — does NOT work across multiple Node instances
  • normalizeImageSrc has production console.log — performance/log noise impact
  • Zero test coverage across ALL 15 files — no safety net for any changes

Pre-change Verification Steps

  1. Grep all consumers before modifying any shared utility (use consumer table above)
  2. Test both happy path AND error path — no existing tests to catch regressions
  3. Verify TypeORM repository behavior if modifying getDbRepository (test with actual DB)
  4. Check sendResponse changes against frontend API client expectations
  5. Validate permission changes with both super_admin and restricted-role users
  6. Test CORS changes in browser (not just curl) — browser CORS is stricter

Suggested Tests Before PR

  • cache.ts: Concurrent get + revalidate race condition, negative TTL, memory pressure
  • catchAsync.ts: Synchronous throw, async throw, throw-after-response-sent
  • generateUniqueSlug.ts: Concurrent slug generation, 1000+ collisions, empty input
  • permissionCache.ts: Mutation via returned Set reference, empty roles, missing context
  • getCorsOrigins.ts: Malformed JSON, empty array, invalid origins, missing config
  • validatePostTypeAndId.ts: postId: 0, negative ID, nonexistent entity type
  • sendResponse.ts: Invalid statusCode, circular data, meta undefined vs null

Architecture & Design Patterns

Code Organization

Flat directory with no subdirectories. All 15 files at same level. Files range from 12 LOC (pick.ts) to 215 LOC (permissionNormalizer.ts). No barrel/index file — consumers import individual files directly.

Design Patterns

  • Higher-Order Function: catchAsync wraps route handlers (decorator pattern)
  • Factory Pattern: getDbRepository creates typed repository instances
  • Guard Pattern: getDataSource validates initialization before returning
  • Facade Pattern: permissionCache wraps requestContext with permission-specific API
  • Singleton Pattern: cache.ts global NodeCache instance, requestContext.ts global AsyncLocalStorage
  • Strategy Pattern: getCorsOrigins tries JSON parse, falls back to CSV parse
  • Normalizer Pattern: permissionNormalizer canonicalizes permission strings

State Management Strategy

  • Process-level: cache.ts (NodeCache singleton — volatile, single-process)
  • Request-level: requestContext.ts (AsyncLocalStorage — scoped to request lifecycle)
  • None: All other files are stateless pure functions or guards

Error Handling Philosophy

Inconsistent across files: - Throw early: getDataSource, getDbRepository, validatePostTypeAndId, getCurrentUser - Silent null/undefined: getCache, getCachedPermissions, getCorsOrigins, clearPermissionCache - Catch-and-delegate: catchAsync passes to Express error middleware - Catch-and-swallow: isCurrentUserSuperAdmin, getPermissionVariants, extractResource - No centralized error strategy or logging convention

Testing Strategy

None. Zero test files exist for any shared utility. 0% coverage across all 15 files. This is the highest-risk gap in the codebase given these utilities are consumed by 53+ files.


Data Flow

Data Entry Points

  • HTTP Request: Query params → getSortQuery(), request body → controllers using catchAsync()
  • Environment Config: ALLOWED_ORIGINSgetCorsOrigins(), API_BASE_URLnormalizeImageSrc()
  • Database: Entity data → getDbRepository(), getDataSource()
  • Auth Middleware: Passport user → runWithContext()initializePermissionCache()

Data Transformations

  • Sort params: Raw query string → validated { field, order } tuple (getSortQuery)
  • Image URLs: Absolute URL → relative path (normalizeImageSrc)
  • Filenames: Raw upload name → URL-safe slug (sanitizeImageName)
  • Content titles: Text → unique DB slug with suffix (generateUniqueSlug)
  • Permission strings: Dot/route format → canonical route format (permissionNormalizer)
  • CORS config: Env string → origin array (getCorsOrigins)

Data Exit Points

  • HTTP Response: All data exits via sendResponse() → JSON body
  • Cache Storage: addCache() writes to in-memory NodeCache
  • AsyncLocalStorage: setPermissionCache() writes to request context
  • Database: Slug writes via TypeORM repositories from getDbRepository()

Integration Points

Shared State

  • NodeCache (cache.ts): Global, volatile, single-process. Accessed by: analytics, roles, menu items
  • AsyncLocalStorage (requestContext.ts): Per-request, auto-cleaned. Accessed by: contextMiddleware, hasPermission
  • TypeORM DataSource (getDataSource.ts): Global singleton. Accessed by: all 53+ service files

Database Access

  • getDbRepository: All 29 entities via typed Repository
  • getDataSource: Raw DataSource for QueryBuilder, transactions
  • generateUniqueSlug: Slug collision queries per content entity
  • validatePostTypeAndId: Existence queries for 5 entity types
  • permissionCache: Indirect (via initializePermissionCache which reads roles.permissions)

Dependency Graph

Internal Dependencies (shared/ files importing other shared/ files)

requestContext.ts ← permissionCache.ts (imports types + context functions)
getDbRepository.ts → (imports getAppDataSource from config - not shared/)
getDataSource.ts → (imports getAppDataSource from config - not shared/)
validatePostTypeAndId.ts → getDbRepository.ts (uses it to get repositories)
generateUniqueSlug.ts → (no shared/ imports, uses typeorm directly)
All other files → (no shared/ internal imports)

Entry Points (NOT imported by other shared/ files)

  • cache.ts
  • catchAsync.ts
  • getCorsOrigins.ts
  • getDataSource.ts
  • getSortQuery.ts
  • normalizeImageSrc.ts
  • permissionNormalizer.ts
  • pick.ts
  • sanitizeImageName.ts
  • sendResponse.ts

Leaf Nodes (don't import other shared/ files)

  • cache.ts
  • catchAsync.ts
  • getCorsOrigins.ts
  • getSortQuery.ts
  • normalizeImageSrc.ts
  • permissionNormalizer.ts
  • pick.ts
  • sanitizeImageName.ts
  • sendResponse.ts
  • generateUniqueSlug.ts

Internal Import Chain

requestContext.ts (leaf - no shared/ deps)
permissionCache.ts (imports requestContext)
[used by contextMiddleware.ts and hasPermission.ts]

getDbRepository.ts (leaf - no shared/ deps)
validatePostTypeAndId.ts (imports getDbRepository)

Circular Dependencies

No circular dependencies detected within shared/.


Testing Analysis

Test Coverage Summary

  • Statements: 0%
  • Branches: 0%
  • Functions: 0%
  • Lines: 0%

Test Files

None exist. Zero test files for any shared utility.

Testing Gaps

  • ALL 15 files have zero test coverage — highest-risk gap in the codebase
  • No integration tests for DB access utilities (getDbRepository, getDataSource)
  • No concurrency tests for cache operations or slug generation
  • No edge case tests for permission normalization
  • No browser-based CORS tests
  • No mutation tests for permission cache security

Duplicate Implementations Found

  • blog.utils.ts (lines 19-55): Contains duplicate toSlug() + generateUniqueSlug() — identical logic to shared/generateUniqueSlug.ts. BlogCategory already uses the shared version. Remove local copy, import from shared.
  • helper/paginationHelper.ts (lines 16-32): calculatePagination() returns {page, limit, skip, sortBy, sortOrder} — naming mismatch with shared/getSortQuery.ts which uses orderBy/order. Overlapping sort logic should be unified.
  • subscription.helpers.ts (line 33): Inline query builder reimplements BaseQueryBuilder patterns (filters, joins, pagination, sort). Should refactor to use existing QueryBuilder.ts.

Pattern Violations Found

  • Direct DataSource access bypassing getDbRepository():
  • app/builder/QueryBuilder.ts line 33: this.dataSource.getRepository(this.entity) — should use shared wrapper
  • subscription.helpers.ts line 33: dataSource.getRepository(Subscription) — should use shared wrapper
  • Direct res.json() bypassing sendResponse():
  • swaggerAuthMiddleware.ts (lines 56, 64-66, 129-132): Raw res.status().json()
  • swagger.route.ts (lines 40-41): Raw res.json(swaggerSpec)
  • google-oauth.controller.ts (lines 106-115, 117-120, 136-140): Raw res.json() and res.status(401).json()
  • Production console.log/console.error:
  • helper/hasPermission.ts lines 44, 68
  • google-oauth.controller.ts lines 22, 67
  • shared/normalizeImageSrc.ts (already documented above)

Dead Code Summary

File Status Action
pick.ts DEAD (0 consumers) Remove entirely
dbRepository() in getDbRepository.ts DEAD (0 consumers) Remove string-based function
addPermissionToCache() DEAD (0 callers) Remove or document intent
getCorsOrigins.ts Single consumer Consider inlining into middleware.ts
permissionNormalizer.ts Single consumer Consider merging into hasPermission.ts
validatePostTypeAndId.ts Single consumer Consider inlining into menuItem.service.ts

Known Issues — Full Severity Index

Critical (17 issues)

ID File Description
C1 cache.ts Comment/code TTL mismatch (says 5min, is 1hr)
C2 cache.ts revalidateCache non-atomic race condition
C3 sendResponse.ts meta field stripped from response when undefined
C4 validatePostTypeAndId.ts !postId skips validation for postId: 0
C5 normalizeImageSrc.ts console.log in production code
C6 sanitizeImageName.ts Aggressive character stripping causes filename collisions
C7 generateUniqueSlug.ts Unbounded while loop — DoS vector
C8 generateUniqueSlug.ts Race condition on concurrent slug generation
C9 permissionCache.ts Mutable Set returned — privilege escalation via .add()
C10 permissionCache.ts Redundant user ID checks (4 functions)
C11 permissionCache.ts addPermissionToCache() dead code
C12 permissionNormalizer.ts ACTION_MAPPING computed but ignored
C13 permissionNormalizer.ts ACTION_MAPPING incomplete/inconsistent
C14 requestContext.ts Phantom user ID mismatch checks
C15 getCorsOrigins.ts Malformed JSON silently falls back with corrupt origins
C16 pick.ts Entire file is dead code (0 consumers)
C17 getDbRepository.ts dbRepository() function is dead code (0 consumers)

High (39 issues)

See individual file sections above for complete list.

Medium (35 issues)

See individual file sections above for complete list.

Low (17 issues)

See individual file sections above for complete list.


Modification Guidance

To Add a New Shared Utility

  1. Create file in saas-boilerplate/src/shared/
  2. Use named exports (not default) for discoverability
  3. Add TypeScript types for all params and returns
  4. Follow existing patterns: pure functions preferred, no side effects when possible
  5. Add to this deep-dive document's file inventory

To Modify Existing Shared Utility

  1. Check consumer count in dependency table above
  2. For high-consumer files (getDbRepository: 53, catchAsync: 32, sendResponse: 29): treat as breaking change
  3. Test with both super_admin and restricted-role users for permission utilities
  4. Verify frontend API client compatibility for sendResponse changes

To Remove/Deprecate

  1. Confirm 0 consumers via grep (check both import and require)
  2. Safe to remove immediately: pick.ts, dbRepository() function, addPermissionToCache()
  3. Single-consumer utilities: inline into consumer before removing

Testing Checklist for Changes

  • All existing consumers still work (grep + manual check)
  • Error paths tested (missing config, invalid input, DB down)
  • Concurrent access tested (for cache, slug, permission utilities)
  • TypeScript types accurate (no any leakage)
  • No production console.log/console.error left in code
  • Permission changes tested with super_admin AND restricted roles
  • CORS changes tested in actual browser (not just API client)

Generated by document-project workflow (deep-dive mode) Base Documentation: docs/index.md Scan Date: 2026-03-29 Analysis Mode: Exhaustive