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 true — postId: 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.pdf → myfilebackup.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 update→edit, remove→delete, 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 (update→edit, remove→delete, 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¶
getDbRepositoryis the #1 dependency hotspot (53 consumers) — changes here affect the entire backendcatchAsyncwraps ALL route handlers — a bug here crashes ALL endpointssendResponseshapes ALL API responses — format changes are breaking changes for all frontend consumerspermissionCachereturns mutable Set — callers can escalate privileges by calling.add()generateUniqueSlughas unbounded loop — can DoS database under adversarial inputcache.tsis process-local — does NOT work across multiple Node instancesnormalizeImageSrchas productionconsole.log— performance/log noise impact- Zero test coverage across ALL 15 files — no safety net for any changes
Pre-change Verification Steps¶
- Grep all consumers before modifying any shared utility (use consumer table above)
- Test both happy path AND error path — no existing tests to catch regressions
- Verify TypeORM repository behavior if modifying
getDbRepository(test with actual DB) - Check
sendResponsechanges against frontend API client expectations - Validate permission changes with both super_admin and restricted-role users
- Test CORS changes in browser (not just curl) — browser CORS is stricter
Suggested Tests Before PR¶
cache.ts: Concurrentget+revalidaterace condition, negative TTL, memory pressurecatchAsync.ts: Synchronous throw, async throw, throw-after-response-sentgenerateUniqueSlug.ts: Concurrent slug generation, 1000+ collisions, empty inputpermissionCache.ts: Mutation via returned Set reference, empty roles, missing contextgetCorsOrigins.ts: Malformed JSON, empty array, invalid origins, missing configvalidatePostTypeAndId.ts:postId: 0, negative ID, nonexistent entity typesendResponse.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:
catchAsyncwraps route handlers (decorator pattern) - Factory Pattern:
getDbRepositorycreates typed repository instances - Guard Pattern:
getDataSourcevalidates initialization before returning - Facade Pattern:
permissionCachewrapsrequestContextwith permission-specific API - Singleton Pattern:
cache.tsglobal NodeCache instance,requestContext.tsglobal AsyncLocalStorage - Strategy Pattern:
getCorsOriginstries JSON parse, falls back to CSV parse - Normalizer Pattern:
permissionNormalizercanonicalizes 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 usingcatchAsync() - Environment Config:
ALLOWED_ORIGINS→getCorsOrigins(),API_BASE_URL→normalizeImageSrc() - 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 RepositorygetDataSource: Raw DataSource for QueryBuilder, transactionsgenerateUniqueSlug: Slug collision queries per content entityvalidatePostTypeAndId: Existence queries for 5 entity typespermissionCache: Indirect (viainitializePermissionCachewhich 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.tscatchAsync.tsgetCorsOrigins.tsgetDataSource.tsgetSortQuery.tsnormalizeImageSrc.tspermissionNormalizer.tspick.tssanitizeImageName.tssendResponse.ts
Leaf Nodes (don't import other shared/ files)¶
cache.tscatchAsync.tsgetCorsOrigins.tsgetSortQuery.tsnormalizeImageSrc.tspermissionNormalizer.tspick.tssanitizeImageName.tssendResponse.tsgenerateUniqueSlug.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
Related Code & Reuse Opportunities¶
Duplicate Implementations Found¶
blog.utils.ts(lines 19-55): Contains duplicatetoSlug()+generateUniqueSlug()— identical logic toshared/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 withshared/getSortQuery.tswhich usesorderBy/order. Overlapping sort logic should be unified.subscription.helpers.ts(line 33): Inline query builder reimplementsBaseQueryBuilderpatterns (filters, joins, pagination, sort). Should refactor to use existingQueryBuilder.ts.
Pattern Violations Found¶
- Direct DataSource access bypassing
getDbRepository(): app/builder/QueryBuilder.tsline 33:this.dataSource.getRepository(this.entity)— should use shared wrappersubscription.helpers.tsline 33:dataSource.getRepository(Subscription)— should use shared wrapper- Direct
res.json()bypassingsendResponse(): swaggerAuthMiddleware.ts(lines 56, 64-66, 129-132): Rawres.status().json()swagger.route.ts(lines 40-41): Rawres.json(swaggerSpec)google-oauth.controller.ts(lines 106-115, 117-120, 136-140): Rawres.json()andres.status(401).json()- Production console.log/console.error:
helper/hasPermission.tslines 44, 68google-oauth.controller.tslines 22, 67shared/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¶
- Create file in
saas-boilerplate/src/shared/ - Use named exports (not default) for discoverability
- Add TypeScript types for all params and returns
- Follow existing patterns: pure functions preferred, no side effects when possible
- Add to this deep-dive document's file inventory
To Modify Existing Shared Utility¶
- Check consumer count in dependency table above
- For high-consumer files (getDbRepository: 53, catchAsync: 32, sendResponse: 29): treat as breaking change
- Test with both super_admin and restricted-role users for permission utilities
- Verify frontend API client compatibility for sendResponse changes
To Remove/Deprecate¶
- Confirm 0 consumers via grep (check both
importandrequire) - Safe to remove immediately:
pick.ts,dbRepository()function,addPermissionToCache() - 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
anyleakage) - 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