Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 12 additions & 13 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { ScheduleModule } from '@nestjs/schedule';
import { join } from 'path';
import { UsersModule } from './users/users.module';
import { AuthModule } from './auth/auth.module';
import { DashboardModule } from './dashboard/dashboard.module';
Expand Down Expand Up @@ -40,22 +37,18 @@ import { SupportTicketsModule } from './support-tickets/support-tickets.module';
import { AuditModule } from './audit/audit.module';
import { MetricsModule } from './metrics/metrics.module';
import { PropertyTaxModule } from './properties/tax/property-tax.module';
import { ResponseFormatInterceptor } from './common/interceptors/response-format.interceptor';
import { VersionHeaderInterceptor } from './versioning/version-header.interceptor';
import { DeprecationWarningInterceptor } from './versioning/deprecation-warning.interceptor';
import { RateLimitHeadersInterceptor } from './auth/interceptors/rate-limit-headers.interceptor';

@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ['.env.local', '.env'],
}),
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
sortSchema: true,
// playground removed: Apollo Server v5 defaults to Apollo Sandbox (uses @apollo/server@^5 peer)
subscriptions: {
'graphql-ws': true,
},
}),

ScheduleModule.forRoot(),
CacheModuleConfig,
AnalyticsModule,
Expand Down Expand Up @@ -91,6 +84,12 @@ import { PropertyTaxModule } from './properties/tax/property-tax.module';
],

controllers: [AppController],
providers: [
ResponseFormatInterceptor,
VersionHeaderInterceptor,
DeprecationWarningInterceptor,
RateLimitHeadersInterceptor,
],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
Expand All @@ -99,4 +98,4 @@ export class AppModule implements NestModule {
// regardless of underlying Express 5 / path-to-regexp v8 syntax changes.
consumer.apply(RequestIdMiddleware).forRoutes('*');
}
}
}
9 changes: 0 additions & 9 deletions src/auth/decorators/gql-user.decorator.ts

This file was deleted.

40 changes: 0 additions & 40 deletions src/auth/guards/gql-auth.guard.ts

This file was deleted.

131 changes: 131 additions & 0 deletions src/common/interceptors/response-format.interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// @ts-nocheck

/**
* Response Format Interceptor
* Standardizes all API responses into a consistent envelope format
*
* Success response format: { success: true, data, meta, timestamp }
* Error response format: { success: false, message, errors, timestamp }
* Pagination meta format: { page, limit, total, totalPages }
*/

import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
HttpException,
} from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
import { Response } from 'express';

interface PaginationMeta {
page: number;
limit: number;
total: number;
totalPages: number;
}

interface SuccessResponse<T> {
success: true;
data: T;
meta?: PaginationMeta | Record<string, any>;
timestamp: string;
}

interface ErrorResponse {
success: false;
message: string;
errors?: any[];
timestamp: string;
}

@Injectable()
export class ResponseFormatInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const timestamp = new Date().toISOString();
const ctx = context.switchToHttp();
const response = ctx.getResponse<Response>();

return next.handle().pipe(
map((data) => {
// If data already has our standard format, return it as-is
if (data && typeof data === 'object' && 'success' in data) {
return data;
}

// Handle paginated responses that already have { data, meta } structure
if (data && typeof data === 'object' && 'data' in data && 'meta' in data) {
const { data: responseData, meta } = data;
return {
success: true,
data: responseData,
meta: this.validatePaginationMeta(meta),
timestamp,
} as SuccessResponse<any>;
}

// Handle standard raw responses
return {
success: true,
data,
timestamp,
} as SuccessResponse<any>;
}),
catchError((error) => {
let statusCode = 500;
let message = 'Internal Server Error';
let errors: any[] | undefined;

if (error instanceof HttpException) {
statusCode = error.getStatus();
const errorResponse = error.getResponse();

if (typeof errorResponse === 'string') {
message = errorResponse;
} else if (typeof errorResponse === 'object') {
message = (errorResponse as any).message || message;
errors = (errorResponse as any).errors;
}
} else if (error instanceof Error) {
message = error.message;
}

response.status(statusCode);

const errorResponse: ErrorResponse = {
success: false,
message,
timestamp,
};

if (errors) {
errorResponse.errors = errors;
}

return throwError(() => ({
...errorResponse,
statusCode,
}));
}),
);
}

private validatePaginationMeta(meta: any): PaginationMeta | Record<string, any> {
// Check if it has pagination properties
const hasPaginationProps = 'page' in meta || 'limit' in meta || 'total' in meta || 'totalPages' in meta;

if (hasPaginationProps) {
return {
page: meta.page || 1,
limit: meta.limit || 10,
total: meta.total || 0,
totalPages: meta.totalPages || Math.ceil((meta.total || 0) / (meta.limit || 10)),
} as PaginationMeta;
}

// Return as-is if it's just regular meta
return meta;
}
}
60 changes: 46 additions & 14 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import { AppModule } from './app.module';
import { VersionHeaderInterceptor } from './versioning/version-header.interceptor';
import { DeprecationWarningInterceptor } from './versioning/deprecation-warning.interceptor';
import { CacheMetricsInterceptor } from './cache/cache-metrics.interceptor';
import { CacheMonitoringService } from './cache/cache-monitoring.service';
import { RateLimitGuard } from './auth/guards/rate-limit.guard';
import { RateLimitService } from './auth/rate-limit.service';
import { RateLimitHeadersInterceptor } from './auth/interceptors/rate-limit-headers.interceptor';
import { ResponseFormatInterceptor } from './common/interceptors/response-format.interceptor';
import { setupSwagger } from './config/swagger.config';
import { validateEnvironment } from './utils/validate-env';

Expand All @@ -22,26 +22,58 @@ async function bootstrap() {
// Node.js version check (#775, #754 NestJS 11 requires Node 20+)
const REQUIRED_NODE_MAJOR = 20;
const nodeMajor = parseInt(process.versions.node.split('.')[0], 10);

if (Number.isNaN(nodeMajor) || nodeMajor < REQUIRED_NODE_MAJOR) {
logger.error(
`Node.js >= ${REQUIRED_NODE_MAJOR} required, found ${process.versions.node}. ` +
`Please upgrade Node.js (see https://nodejs.org/).`,
);
process.exit(1);
}

// Setup Swagger documentation
setupSwagger(app);
const app = await NestFactory.create(AppModule);

app.enableShutdownHooks();
// Global validation pipe
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
}));

const port = process.env.PORT || 3000;
await app.listen(port);
logger.log(`PropChain API running on http://localhost:${port}`);
logger.log(`API Versioning enabled. Supported versions: v1, v2`);
logger.log(`πŸ“š Swagger UI available at http://localhost:${port}/api/docs`);
logger.log(`πŸ“‹ OpenAPI spec available at http://localhost:${port}/api/openapi.json`);
logger.log(`πŸ’Ύ Redis Caching enabled`);
logger.log(`πŸ›‘οΈ Rate Limiting enabled (per-user, per-endpoint, IP-based)`);
}
// Register global interceptors
const responseFormatInterceptor = app.get(ResponseFormatInterceptor);
const versionHeaderInterceptor = app.get(VersionHeaderInterceptor);
const deprecationWarningInterceptor = app.get(DeprecationWarningInterceptor);
const cacheMetricsInterceptor = app.get(CacheMetricsInterceptor);
const rateLimitHeadersInterceptor = app.get(RateLimitHeadersInterceptor);

app.useGlobalInterceptors(
responseFormatInterceptor,
versionHeaderInterceptor,
deprecationWarningInterceptor,
cacheMetricsInterceptor,
rateLimitHeadersInterceptor,
);

// Register global guards
const reflector = app.get(Reflector);
const rateLimitService = app.get(RateLimitService);
app.useGlobalGuards(new RateLimitGuard(rateLimitService, reflector));

bootstrap();
// Setup Swagger documentation
setupSwagger(app);

app.enableShutdownHooks();

const port = process.env.PORT || 3000;
await app.listen(port);
logger.log(`PropChain API running on http://localhost:${port}`);
logger.log(`API Versioning enabled. Supported versions: v1, v2`);
logger.log(`πŸ“š Swagger UI available at http://localhost:${port}/api/docs`);
logger.log(`πŸ“‹ OpenAPI spec available at http://localhost:${port}/api/openapi.json`);
logger.log(`πŸ’Ύ Redis Caching enabled`);
logger.log(`πŸ›‘οΈ Rate Limiting enabled (per-user, per-endpoint, IP-based)`);
logger.log(`βœ… Response format interceptor enabled - all API responses now follow standardized format`);
}

bootstrap();
10 changes: 2 additions & 8 deletions src/properties/properties.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ import { PropertyExpiryService } from './property-expiry.service';
import { PrismaModule } from '../database/prisma.module';
import { AuthModule } from '../auth/auth.module';
import { NotificationsModule } from '../notifications/notifications.module';
import { PropertiesResolver } from './properties.resolver';
import { PubSub } from 'graphql-subscriptions';

import { FraudModule } from '../fraud/fraud.module';
import { PropertyReportService } from './report/property-report.service';
import { CacheModuleConfig } from '../cache/cache.module';
Expand All @@ -25,13 +24,8 @@ import { CacheModuleConfig } from '../cache/cache.module';
PropertyImagesService,
GeocodingService,
PropertyExpiryService,
PropertiesResolver,
PropertyReportService,
{
provide: 'PUB_SUB',
useValue: new PubSub(),
},
],
exports: [PropertiesService, PropertyReportService, PropertyImagesService, GeocodingService, PropertyExpiryService],
})
export class PropertiesModule {}
export class PropertiesModule {}
Loading
Loading