858 lines
27 KiB
Markdown
858 lines
27 KiB
Markdown
# Utilities Module
|
|
|
|
## Overview
|
|
|
|
The Utilities module provides centralized helper functions, error handling, logging, environment validation, and i18n message management. Utilities are organized across several locations based on their domain.
|
|
|
|
**Locations:**
|
|
- `backend/src/utils/` - Core utilities (errors, logging, env validation, request context)
|
|
- `backend/src/helpers.ts` - Request helpers (async wrapper, error handler, JWT)
|
|
- `backend/src/db/utils.ts` - Database utilities
|
|
- `backend/src/services/notifications/` - i18n messages and legacy error classes
|
|
|
|
---
|
|
|
|
## Architecture
|
|
|
|
```
|
|
backend/src/
|
|
├── utils/
|
|
│ ├── index.ts # Re-exports
|
|
│ ├── errors.ts # Error classes
|
|
│ ├── logger.ts # Pino logging
|
|
│ ├── request-context.ts # Request-scoped context storage
|
|
│ └── env-validation.ts # Joi env schema
|
|
├── helpers.ts # Request helpers
|
|
├── db/
|
|
│ └── utils.ts # DB utilities
|
|
└── services/notifications/
|
|
├── list.ts # i18n message catalog
|
|
├── helpers.ts # Message formatting
|
|
└── errors/
|
|
├── forbidden.ts # ForbiddenError
|
|
└── validation.ts # ValidationError
|
|
```
|
|
|
|
---
|
|
|
|
## Core Utilities (`utils/`)
|
|
|
|
### Error Classes (`utils/errors.js`)
|
|
|
|
Modern error hierarchy for consistent HTTP error responses.
|
|
|
|
```javascript
|
|
class AppError extends Error {
|
|
constructor(message, statusCode = 500, details = null) {
|
|
super(message);
|
|
this.statusCode = statusCode;
|
|
this.details = details;
|
|
this.isOperational = true; // Distinguishes from programming errors
|
|
Error.captureStackTrace(this, this.constructor);
|
|
}
|
|
}
|
|
```
|
|
|
|
**Error Types:**
|
|
|
|
| Class | Status Code | Default Message | Usage |
|
|
|-------|-------------|-----------------|-------|
|
|
| `AppError` | 500 | (custom) | Base class |
|
|
| `NotFoundError` | 404 | `{resource} not found` | Missing resources |
|
|
| `ValidationError` | 400 | (custom) | Invalid input |
|
|
| `ForbiddenError` | 403 | `Access denied` | Permission denied |
|
|
| `UnauthorizedError` | 401 | `Unauthorized` | Auth required |
|
|
| `ConflictError` | 409 | `Resource conflict` | Duplicate resources |
|
|
|
|
**Usage:**
|
|
```javascript
|
|
const { NotFoundError, ValidationError, ForbiddenError } = require('./utils');
|
|
|
|
// In route handler
|
|
if (!user) {
|
|
throw new NotFoundError('User');
|
|
}
|
|
|
|
const currentUser = getCurrentUser(req);
|
|
if (!currentUser) {
|
|
throw new ForbiddenError();
|
|
}
|
|
|
|
if (errors.length > 0) {
|
|
throw new ValidationError('Invalid data', errors);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Structured Logging (`utils/logger.ts`)
|
|
|
|
Pino-based logging with request correlation and environment-aware formatting.
|
|
Logger initialization is a bootstrap exception where direct `process.env`
|
|
access is allowed; runtime services should use `config.ts` instead.
|
|
|
|
```javascript
|
|
const pino = require('pino');
|
|
const crypto = require('crypto');
|
|
|
|
const isDevelopment = process.env.NODE_ENV === 'development';
|
|
|
|
const logger = pino({
|
|
level: process.env.LOG_LEVEL || 'info',
|
|
transport: isDevelopment
|
|
? { target: 'pino-pretty', options: { colorize: true } }
|
|
: undefined,
|
|
base: {
|
|
service: 'tour-builder-api',
|
|
env: process.env.NODE_ENV || 'development',
|
|
},
|
|
});
|
|
```
|
|
|
|
**Log Levels:**
|
|
- `fatal` - Unrecoverable errors
|
|
- `error` - Errors requiring attention
|
|
- `warn` - Warning conditions (400-499 responses)
|
|
- `info` - Normal operations (default)
|
|
- `debug` - Detailed debugging
|
|
- `trace` - Very detailed tracing
|
|
|
|
**Process-Level Failure Logging:**
|
|
|
|
`registerProcessErrorHandlers()` is called from `src/index.ts` during backend
|
|
startup. It installs handlers for `uncaughtException` and `unhandledRejection`,
|
|
logs them through Pino at `fatal`, normalizes non-`Error` rejection reasons into
|
|
`Error` instances with `cause`, flushes Pino, and then exits with status `1`.
|
|
These failures happen outside the Express request lifecycle, so route error
|
|
middleware cannot catch them.
|
|
|
|
```typescript
|
|
registerProcessErrorHandlers();
|
|
|
|
process.on('unhandledRejection', (reason) => {
|
|
logger.fatal(
|
|
{ err: normalizeLoggedError(reason) },
|
|
'Unhandled promise rejection, shutting down process',
|
|
);
|
|
exitAfterLogging();
|
|
});
|
|
```
|
|
|
|
**Request Logger Middleware:**
|
|
```javascript
|
|
function requestLogger(req, res, next) {
|
|
// Generate or use existing request ID
|
|
const requestId = req.headers['x-request-id'] || crypto.randomUUID();
|
|
|
|
// Store child logger in request context
|
|
setRequestLogger(req, logger.child({ requestId }));
|
|
setRequestId(req, requestId);
|
|
res.setHeader('X-Request-Id', requestId);
|
|
|
|
const start = Date.now();
|
|
res.on('finish', () => {
|
|
const duration = Date.now() - start;
|
|
const logData = {
|
|
method: req.method,
|
|
url: req.originalUrl || req.url,
|
|
status: res.statusCode,
|
|
duration,
|
|
userAgent: req.headers['user-agent'],
|
|
};
|
|
|
|
// Log level based on status code
|
|
if (res.statusCode >= 500) {
|
|
getRequestLogger(req)?.error(logData, 'Request completed with server error');
|
|
} else if (res.statusCode >= 400) {
|
|
getRequestLogger(req)?.warn(logData, 'Request completed with client error');
|
|
} else {
|
|
getRequestLogger(req)?.info(logData, 'Request completed');
|
|
}
|
|
});
|
|
|
|
next();
|
|
}
|
|
```
|
|
|
|
**Log Output Examples:**
|
|
|
|
Development (pino-pretty):
|
|
```
|
|
[12:34:56.789] INFO (tour-builder-api): Request completed
|
|
requestId: "abc-123"
|
|
method: "GET"
|
|
url: "/api/users"
|
|
status: 200
|
|
duration: 45
|
|
```
|
|
|
|
Production (JSON):
|
|
```json
|
|
{"level":30,"time":1711723456789,"service":"tour-builder-api","env":"production","requestId":"abc-123","method":"GET","url":"/api/users","status":200,"duration":45,"msg":"Request completed"}
|
|
```
|
|
|
|
**Usage:**
|
|
```javascript
|
|
const { logger, requestLogger } = require('./utils/logger');
|
|
|
|
// App setup
|
|
app.use(requestLogger);
|
|
|
|
// Manual logging
|
|
logger.info({ userId: user.id }, 'User logged in');
|
|
logger.error({ err }, 'Database connection failed');
|
|
|
|
// Request-scoped logging (in routes)
|
|
getRequestLogger(req)?.info({ data }, 'Processing request');
|
|
```
|
|
|
|
---
|
|
|
|
### Request Context (`utils/request-context.ts`)
|
|
|
|
Request-scoped data is stored outside the Express `Request` object through a
|
|
`WeakMap<Request, AppRequestContext>`. Do not add global
|
|
`Express.Request` augmentation for project fields such as `currentUser`,
|
|
`runtimeContext`, `log`, or runtime-public flags. Middleware should write with
|
|
`setCurrentUser`, `setRuntimeContext`, `setRequestLogger`, and related helpers;
|
|
routes/services should read through `getCurrentUser`, `getRuntimeContext`,
|
|
`getRequestLogger`, and `getRouteServiceContext`.
|
|
|
|
### Environment Validation (`utils/env-validation.ts`)
|
|
|
|
Joi-based validation ensuring all required environment variables are present with correct types.
|
|
|
|
**Schema Definition:**
|
|
```javascript
|
|
const Joi = require('joi');
|
|
|
|
const envSchema = Joi.object({
|
|
// Server
|
|
NODE_ENV: Joi.string()
|
|
.valid('development', 'test', 'production', 'dev_stage')
|
|
.default('development'),
|
|
PORT: Joi.number().default(8080),
|
|
|
|
// Database
|
|
DB_HOST: Joi.string().default('localhost'),
|
|
DB_PORT: Joi.number().default(5432),
|
|
DB_NAME: Joi.string().default('db_tour_builder_platform'),
|
|
DB_USER: Joi.string().default('postgres'),
|
|
DB_PASS: Joi.string().allow('').default(''),
|
|
|
|
// Authentication
|
|
SECRET_KEY: Joi.string()
|
|
.min(16)
|
|
.default('88dbeaf8-e906-405e-9e41-c3baadeda5c6'),
|
|
ADMIN_PASS: Joi.string().default('88dbeaf8'),
|
|
USER_PASS: Joi.string().default('c3baadeda5c6'),
|
|
ADMIN_EMAIL: Joi.string().email().default('admin@flatlogic.com'),
|
|
|
|
// OAuth
|
|
GOOGLE_CLIENT_ID: Joi.string().allow('').default(''),
|
|
GOOGLE_CLIENT_SECRET: Joi.string().allow('').default(''),
|
|
MS_CLIENT_ID: Joi.string().allow('').default(''),
|
|
MS_CLIENT_SECRET: Joi.string().allow('').default(''),
|
|
|
|
// AWS S3
|
|
AWS_ACCESS_KEY_ID: Joi.string().allow('').default(''),
|
|
AWS_SECRET_ACCESS_KEY: Joi.string().allow('').default(''),
|
|
AWS_S3_BUCKET: Joi.string().allow('').default(''),
|
|
AWS_S3_REGION: Joi.string().default('us-east-1'),
|
|
AWS_S3_PREFIX: Joi.string().default('afeefb9d49f5b7977577876b99532ac7'),
|
|
|
|
// Email
|
|
EMAIL_USER: Joi.string().allow('').default(''),
|
|
EMAIL_PASS: Joi.string().allow('').default(''),
|
|
EMAIL_TLS_REJECT_UNAUTHORIZED: Joi.string()
|
|
.valid('true', 'false')
|
|
.default('true'),
|
|
|
|
// External APIs
|
|
PEXELS_KEY: Joi.string().allow('').default(''),
|
|
|
|
// Logging
|
|
LOG_LEVEL: Joi.string()
|
|
.valid('fatal', 'error', 'warn', 'info', 'debug', 'trace')
|
|
.default('info'),
|
|
}).unknown(true); // Allow additional env vars
|
|
```
|
|
|
|
**Validation Function:**
|
|
```javascript
|
|
function validateEnv() {
|
|
const { error, value } = envSchema.validate(process.env, {
|
|
abortEarly: false, // Report all errors
|
|
stripUnknown: false, // Keep unknown vars
|
|
});
|
|
|
|
if (error) {
|
|
const messages = error.details.map((d) => ` - ${d.message}`);
|
|
logger.error({ errors: messages }, 'Environment validation failed');
|
|
|
|
// Strict in production, lenient in development
|
|
if (process.env.NODE_ENV === 'production') {
|
|
process.exit(1);
|
|
} else {
|
|
logger.warn('Continuing with default values in non-production mode');
|
|
}
|
|
}
|
|
|
|
return value; // Returns validated/defaulted values
|
|
}
|
|
```
|
|
|
|
**Environment Variable Categories:**
|
|
|
|
| Category | Variables | Required |
|
|
|----------|-----------|----------|
|
|
| **Server** | `NODE_ENV`, `PORT` | Defaults |
|
|
| **Database** | `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASS` | Defaults |
|
|
| **Auth** | `SECRET_KEY`, `ADMIN_PASS`, `USER_PASS`, `ADMIN_EMAIL` | Defaults |
|
|
| **OAuth** | `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `MS_CLIENT_ID`, `MS_CLIENT_SECRET` | Optional |
|
|
| **AWS S3** | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_S3_BUCKET`, `AWS_S3_REGION`, `AWS_S3_PREFIX` | Optional |
|
|
| **Email** | `EMAIL_USER`, `EMAIL_PASS`, `EMAIL_TLS_REJECT_UNAUTHORIZED` | Optional |
|
|
| **External APIs** | `PEXELS_KEY` | Optional |
|
|
| **Logging** | `LOG_LEVEL` | Defaults |
|
|
|
|
---
|
|
|
|
### Index Re-exports (`utils/index.js`)
|
|
|
|
Convenient re-export of utilities:
|
|
|
|
```javascript
|
|
module.exports = {
|
|
...require('./errors'),
|
|
...require('./logger'),
|
|
envValidation: require('./env-validation'),
|
|
};
|
|
```
|
|
|
|
**Exported:**
|
|
- `AppError`, `NotFoundError`, `ValidationError`, `ForbiddenError`, `UnauthorizedError`, `ConflictError`
|
|
- `logger`, `requestLogger`, `registerProcessErrorHandlers`,
|
|
`exitAfterLogging`,
|
|
`normalizeLoggedError`
|
|
- `envValidation.validateEnv`, `envValidation.envSchema`
|
|
|
|
---
|
|
|
|
## Request Helpers (`helpers.js`)
|
|
|
|
Core helper class for Express route handling.
|
|
|
|
```javascript
|
|
const jwt = require('jsonwebtoken');
|
|
const config = require('./config');
|
|
|
|
module.exports = class Helpers {
|
|
// Wrap async route handlers to catch errors
|
|
static wrapAsync(fn) {
|
|
return function (req, res, next) {
|
|
fn(req, res, next).catch(next);
|
|
};
|
|
}
|
|
|
|
// Centralized error response handler
|
|
static commonErrorHandler(error, req, res, _next) {
|
|
const statusCode = error.code || error.status;
|
|
|
|
if ([400, 401, 403, 404, 409, 422].includes(statusCode)) {
|
|
return res.status(statusCode).send(error.message);
|
|
}
|
|
|
|
console.error(error);
|
|
return res.status(500).send('Internal server error');
|
|
}
|
|
|
|
// Generate JWT token
|
|
static jwtSign(data) {
|
|
return jwt.sign(data, config.secret_key, { expiresIn: '6h' });
|
|
}
|
|
|
|
// Validate UUID v4 format
|
|
static isUuidV4(value) {
|
|
return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
|
|
value,
|
|
);
|
|
}
|
|
};
|
|
```
|
|
|
|
**Functions:**
|
|
|
|
| Function | Purpose | Usage |
|
|
|----------|---------|-------|
|
|
| `wrapAsync(fn)` | Wraps async handlers to propagate errors | All async route handlers |
|
|
| `commonErrorHandler(err, req, res, next)` | Standardizes error responses | Route error middleware |
|
|
| `jwtSign(data)` | Creates JWT with 6h expiry | Auth service |
|
|
| `isUuidV4(value)` | Validates UUID v4 format | Route parameter validation |
|
|
|
|
## Request Validation
|
|
|
|
Request validation is centralized in:
|
|
|
|
- `src/middlewares/validate-request.ts` - Joi middleware for `params`, `query`, and `body`
|
|
- `src/validators/request-schemas.ts` - shared schemas for CRUD, auth, users, projects, tour pages, publish, and file upload endpoints
|
|
|
|
New external routes must validate all incoming `params`, `query`, and `body` before calling services. Factory CRUD routes use default schemas automatically and can override them through the `validation` option.
|
|
|
|
```typescript
|
|
import { validateRequest } from '../middlewares/validate-request.ts';
|
|
import { publish as publishSchemas } from '../validators/request-schemas.ts';
|
|
|
|
router.post(
|
|
'/save-to-stage',
|
|
validateRequest(publishSchemas.saveToStage),
|
|
wrapAsync(async (req, res) => {
|
|
const result = await PublishService.saveToStage(
|
|
req.body.projectId,
|
|
getCurrentUser(req),
|
|
);
|
|
res.status(200).json(result);
|
|
}),
|
|
);
|
|
```
|
|
|
|
Request validation errors return JSON:
|
|
|
|
```json
|
|
{
|
|
"error": "Invalid request",
|
|
"details": [
|
|
{
|
|
"path": "projectId",
|
|
"message": "\"projectId\" must be a valid GUID",
|
|
"type": "string.guid"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
Service/domain `ValidationError` responses keep the legacy plain-text format unless they are raised by request validation middleware.
|
|
|
|
**Usage Pattern:**
|
|
```javascript
|
|
const { wrapAsync, commonErrorHandler, isUuidV4 } = require('../helpers');
|
|
|
|
// Async route handler
|
|
router.get('/users/:id', wrapAsync(async (req, res) => {
|
|
if (!isUuidV4(req.params.id)) {
|
|
return res.status(400).send('Invalid ID format');
|
|
}
|
|
|
|
const user = await UserService.findOne(req.params.id);
|
|
res.json(user);
|
|
}));
|
|
|
|
// Register error handler at end of router
|
|
router.use('/', commonErrorHandler);
|
|
```
|
|
|
|
---
|
|
|
|
## Database Utilities (`db/utils.js`)
|
|
|
|
Utilities specific to Sequelize database operations, including clean UUID validation functions.
|
|
|
|
```javascript
|
|
const validator = require('validator');
|
|
const { v4: uuidv4 } = require('uuid');
|
|
const Sequelize = require('./models').Sequelize;
|
|
|
|
module.exports = class Utils {
|
|
// Check if value is a valid UUID
|
|
static isValidUuid(value) {
|
|
return Boolean(value && validator.isUUID(String(value)));
|
|
}
|
|
|
|
// Generate a new UUID v4
|
|
static generateUuid() {
|
|
return uuidv4();
|
|
}
|
|
|
|
// Filter array to only valid UUIDs
|
|
static filterValidUuids(values) {
|
|
return values.filter((v) => this.isValidUuid(v));
|
|
}
|
|
|
|
// Case-insensitive LIKE query
|
|
static ilike(model, column, value) {
|
|
return Sequelize.where(
|
|
Sequelize.fn('lower', Sequelize.col(`${model}.${column}`)),
|
|
{ [Sequelize.Op.like]: `%${value}%`.toLowerCase() },
|
|
);
|
|
}
|
|
};
|
|
```
|
|
|
|
**Functions:**
|
|
|
|
| Function | Purpose | Returns |
|
|
|----------|---------|---------|
|
|
| `isValidUuid(value)` | Check if value is a valid UUID | `boolean` |
|
|
| `generateUuid()` | Generate a new UUID v4 | `string` |
|
|
| `filterValidUuids(values)` | Filter array to only valid UUIDs | `string[]` |
|
|
| `ilike(model, column, value)` | Case-insensitive LIKE search | Sequelize where clause |
|
|
|
|
**UUID Validation Behavior:**
|
|
- Invalid single ID filter (`?id=xxx`) → returns `{ rows: [], count: 0 }` immediately
|
|
- Invalid UUIDs in relation filters (`?project=uuid|name`) → filtered out for ID search, kept for text search
|
|
- Invalid UUID field filter (`?projectId=xxx`) → returns `{ rows: [], count: 0 }` immediately
|
|
|
|
**Usage in DB API:**
|
|
```javascript
|
|
const Utils = require('../utils');
|
|
|
|
class GenericDBApi {
|
|
async findAll(filter = {}, options = {}) {
|
|
// Single ID validation
|
|
if (filter.id) {
|
|
if (!Utils.isValidUuid(filter.id)) {
|
|
return { rows: [], count: 0 };
|
|
}
|
|
where.id = filter.id;
|
|
}
|
|
|
|
// Relation filter with mixed UUID/text search
|
|
for (const rel of this.RELATION_FILTERS) {
|
|
if (filter[rel.filterKey]) {
|
|
const searchTerms = filter[rel.filterKey].split('|');
|
|
const validUuids = Utils.filterValidUuids(searchTerms);
|
|
|
|
// UUID search: only valid UUIDs
|
|
// Text search: all terms
|
|
}
|
|
}
|
|
}
|
|
|
|
async findAllAutocomplete({ query, limit, offset }) {
|
|
const orConditions = [
|
|
Utils.ilike(this.TABLE_NAME, this.AUTOCOMPLETE_FIELD, query),
|
|
];
|
|
|
|
// Only add UUID search if query is a valid UUID
|
|
if (Utils.isValidUuid(query)) {
|
|
orConditions.unshift({ id: query });
|
|
}
|
|
|
|
// ...
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Notifications Module (`services/notifications/`)
|
|
|
|
i18n message management and legacy error classes.
|
|
|
|
### Message Catalog (`list.js`)
|
|
|
|
Centralized message definitions with placeholders:
|
|
|
|
```javascript
|
|
const errors = {
|
|
app: {
|
|
title: 'Tour Builder Platform',
|
|
},
|
|
|
|
auth: {
|
|
userDisabled: 'Your account is disabled',
|
|
forbidden: 'Forbidden',
|
|
unauthorized: 'Unauthorized',
|
|
userNotFound: `Sorry, we don't recognize your credentials`,
|
|
wrongPassword: `Sorry, we don't recognize your credentials`,
|
|
weakPassword: 'This password is too weak',
|
|
emailAlreadyInUse: 'Email is already in use',
|
|
invalidEmail: 'Please provide a valid email',
|
|
passwordReset: {
|
|
invalidToken: 'Password reset link is invalid or has expired',
|
|
error: `Email not recognized`,
|
|
},
|
|
passwordUpdate: {
|
|
samePassword: `You can't use the same password. Please create new password`,
|
|
},
|
|
userNotVerified: `Sorry, your email has not been verified yet`,
|
|
emailAddressVerificationEmail: {
|
|
invalidToken: 'Email verification link is invalid or has expired',
|
|
error: `Email not recognized`,
|
|
},
|
|
},
|
|
|
|
iam: {
|
|
errors: {
|
|
userAlreadyExists: 'User with this email already exists',
|
|
userNotFound: 'User not found',
|
|
disablingHimself: `You can't disable yourself`,
|
|
revokingOwnPermission: `You can't revoke your own owner permission`,
|
|
deletingHimself: `You can't delete yourself`,
|
|
emailRequired: 'Email is required',
|
|
},
|
|
},
|
|
|
|
importer: {
|
|
errors: {
|
|
invalidFileEmpty: 'The file is empty',
|
|
invalidFileExcel: 'Only excel (.xlsx) files are allowed',
|
|
invalidFileUpload: 'Invalid file. Make sure you are using the last version of the template.',
|
|
importHashRequired: 'Import hash is required',
|
|
importHashExistent: 'Data has already been imported',
|
|
userEmailMissing: 'Some items in the CSV do not have an email',
|
|
},
|
|
},
|
|
|
|
errors: {
|
|
forbidden: { message: 'Forbidden' },
|
|
validation: { message: 'An error occurred' },
|
|
searchQueryRequired: { message: 'Search query is required' },
|
|
},
|
|
|
|
emails: {
|
|
invitation: {
|
|
subject: `You've been invited to {0}`,
|
|
body: `<p>Hello,</p><p>You've been invited to {0}...</p>`,
|
|
},
|
|
emailAddressVerification: {
|
|
subject: `Verify your email for {0}`,
|
|
body: `<p>Hello,</p><p>Follow this link to verify...</p>`,
|
|
},
|
|
passwordReset: {
|
|
subject: `Reset your password for {0}`,
|
|
body: `<p>Hello,</p><p>Follow this link to reset...</p>`,
|
|
},
|
|
},
|
|
};
|
|
```
|
|
|
|
**Message Categories:**
|
|
|
|
| Category | Purpose | Examples |
|
|
|----------|---------|----------|
|
|
| `app` | Application metadata | `app.title` |
|
|
| `auth` | Authentication errors | `auth.userDisabled`, `auth.wrongPassword` |
|
|
| `iam` | User management errors | `iam.errors.userAlreadyExists` |
|
|
| `importer` | Import/export errors | `importer.errors.invalidFileEmpty` |
|
|
| `errors` | Generic errors | `errors.forbidden.message` |
|
|
| `emails` | Email templates | `emails.invitation.subject` |
|
|
|
|
---
|
|
|
|
### Message Helpers (`helpers.js`)
|
|
|
|
Functions for message formatting and lookup:
|
|
|
|
```javascript
|
|
const _get = require('lodash/get');
|
|
const errors = require('./list');
|
|
|
|
// Format message with placeholder substitution
|
|
function format(message, args) {
|
|
if (!message) return null;
|
|
return message.replace(/{(\d+)}/g, function (match, number) {
|
|
return typeof args[number] != 'undefined' ? args[number] : match;
|
|
});
|
|
}
|
|
|
|
// Check if key exists in catalog
|
|
const isNotification = (key) => {
|
|
const message = _get(errors, key);
|
|
return !!message;
|
|
};
|
|
|
|
// Get formatted message by key
|
|
const getNotification = (key, ...args) => {
|
|
const message = _get(errors, key);
|
|
if (!message) return key;
|
|
return format(message, args);
|
|
};
|
|
```
|
|
|
|
**Usage:**
|
|
```javascript
|
|
const { getNotification, isNotification } = require('./helpers');
|
|
|
|
// Lookup message
|
|
getNotification('auth.userDisabled');
|
|
// → 'Your account is disabled'
|
|
|
|
// Format with placeholders
|
|
getNotification('emails.invitation.subject', 'Tour Builder');
|
|
// → "You've been invited to Tour Builder"
|
|
|
|
// Check existence
|
|
isNotification('auth.userDisabled'); // → true
|
|
isNotification('unknown.key'); // → false
|
|
```
|
|
|
|
---
|
|
|
|
### Legacy Error Classes (`errors/`)
|
|
|
|
i18n-aware error classes (legacy pattern, prefer `utils/errors.js`):
|
|
|
|
**ForbiddenError:**
|
|
```javascript
|
|
const { getNotification, isNotification } = require('../helpers');
|
|
|
|
module.exports = class ForbiddenError extends Error {
|
|
constructor(messageCode) {
|
|
let message;
|
|
|
|
if (messageCode && isNotification(messageCode)) {
|
|
message = getNotification(messageCode);
|
|
}
|
|
|
|
message = message || getNotification('errors.forbidden.message');
|
|
|
|
super(message);
|
|
this.code = 403;
|
|
}
|
|
};
|
|
```
|
|
|
|
**ValidationError:**
|
|
```javascript
|
|
module.exports = class ValidationError extends Error {
|
|
constructor(messageCode) {
|
|
let message;
|
|
|
|
if (messageCode && isNotification(messageCode)) {
|
|
message = getNotification(messageCode);
|
|
}
|
|
|
|
message = message || getNotification('errors.validation.message');
|
|
|
|
super(message);
|
|
this.code = 400;
|
|
}
|
|
};
|
|
```
|
|
|
|
**Usage:**
|
|
```javascript
|
|
const ForbiddenError = require('./services/notifications/errors/forbidden');
|
|
const ValidationError = require('./services/notifications/errors/validation');
|
|
|
|
// With i18n key
|
|
throw new ForbiddenError('auth.forbidden');
|
|
throw new ValidationError('iam.errors.emailRequired');
|
|
|
|
// With default message
|
|
throw new ForbiddenError(); // → 'Forbidden'
|
|
throw new ValidationError(); // → 'An error occurred'
|
|
```
|
|
|
|
---
|
|
|
|
## Error Handling Flow
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ Route Handler │
|
|
│ │
|
|
│ router.get('/users/:id', wrapAsync(async (req, res) => { │
|
|
│ throw new NotFoundError('User'); │
|
|
│ })); │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
wrapAsync catches
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ commonErrorHandler │
|
|
│ │
|
|
│ - Checks error.code or error.status │
|
|
│ - Known codes (400-422): returns error.message │
|
|
│ - Unknown: returns 'Internal server error' │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ HTTP Response │
|
|
│ │
|
|
│ HTTP/1.1 404 Not Found │
|
|
│ Content-Type: text/plain │
|
|
│ │
|
|
│ User not found │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Usage Summary
|
|
|
|
### Recommended Imports
|
|
|
|
```javascript
|
|
// Modern error classes
|
|
const { NotFoundError, ValidationError, ForbiddenError } = require('./utils');
|
|
|
|
// Logging
|
|
const { logger, requestLogger } = require('./utils/logger');
|
|
|
|
// Request helpers
|
|
const { wrapAsync, commonErrorHandler, isUuidV4 } = require('./helpers');
|
|
|
|
// Database utilities
|
|
const Utils = require('./db/utils');
|
|
|
|
// i18n messages
|
|
const { getNotification } = require('./services/notifications/helpers');
|
|
```
|
|
|
|
### Error Class Selection
|
|
|
|
| Scenario | Recommended Class |
|
|
|----------|-------------------|
|
|
| Resource not found | `NotFoundError` from `utils/errors.js` |
|
|
| Invalid input | `ValidationError` from `utils/errors.js` |
|
|
| Permission denied | `ForbiddenError` from `utils/errors.js` |
|
|
| Auth required | `UnauthorizedError` from `utils/errors.js` |
|
|
| Duplicate resource | `ConflictError` from `utils/errors.js` |
|
|
| i18n error message | Legacy classes from `notifications/errors/` |
|
|
|
|
---
|
|
|
|
## Environment Variables Reference
|
|
|
|
| Variable | Type | Default | Description |
|
|
|----------|------|---------|-------------|
|
|
| `NODE_ENV` | string | `development` | `development`, `test`, `production`, `dev_stage` |
|
|
| `PORT` | number | `8080` | Server port |
|
|
| `DB_HOST` | string | `localhost` | PostgreSQL host |
|
|
| `DB_PORT` | number | `5432` | PostgreSQL port |
|
|
| `DB_NAME` | string | `db_tour_builder_platform` | Database name |
|
|
| `DB_USER` | string | `postgres` | Database user |
|
|
| `DB_PASS` | string | `` | Database password |
|
|
| `SECRET_KEY` | string | UUID | JWT signing key (min 16 chars) |
|
|
| `ADMIN_EMAIL` | email | `admin@flatlogic.com` | Admin account email |
|
|
| `ADMIN_PASS` | string | `88dbeaf8` | Admin account password |
|
|
| `USER_PASS` | string | `c3baadeda5c6` | Default user password |
|
|
| `GOOGLE_CLIENT_ID` | string | `` | Google OAuth client ID |
|
|
| `GOOGLE_CLIENT_SECRET` | string | `` | Google OAuth client secret |
|
|
| `MS_CLIENT_ID` | string | `` | Microsoft OAuth client ID |
|
|
| `MS_CLIENT_SECRET` | string | `` | Microsoft OAuth client secret |
|
|
| `AWS_ACCESS_KEY_ID` | string | `` | AWS access key |
|
|
| `AWS_SECRET_ACCESS_KEY` | string | `` | AWS secret key |
|
|
| `AWS_S3_BUCKET` | string | `` | S3 bucket name |
|
|
| `AWS_S3_REGION` | string | `us-east-1` | S3 region |
|
|
| `AWS_S3_PREFIX` | string | UUID | S3 key prefix |
|
|
| `EMAIL_USER` | string | `` | SMTP username |
|
|
| `EMAIL_PASS` | string | `` | SMTP password |
|
|
| `EMAIL_TLS_REJECT_UNAUTHORIZED` | string | `true` | TLS cert validation |
|
|
| `PEXELS_KEY` | string | `` | Pexels API key |
|
|
| `LOG_LEVEL` | string | `info` | Pino log level |
|
|
|
|
---
|
|
|
|
## Related Documentation
|
|
|
|
- [Backend Architecture](../backend-architecture.md) - Overall backend structure
|
|
- [Auth Module](./auth.md) - Authentication using JWT helpers
|
|
- [Middleware Module](./middleware.md) - Request middleware
|
|
- [Routes Module](./routes.md) - Route handlers using wrapAsync
|
|
- [Services Module](./services.md) - Business logic services
|
|
- [DB Config Module](./db-config.md) - Database configuration
|