feat: API versioning, compound indexes, and new exports
- Add global /api/v1/ prefix with health/ready exclusions - Add compound indexes on Property and Listing for query optimization - Export CsrfMiddleware and UploadedFile type from shared infra - New Prisma migration for compound indexes Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import './instrument';
|
import './instrument';
|
||||||
|
|
||||||
import { ValidationPipe } from '@nestjs/common';
|
import { RequestMethod, ValidationPipe } from '@nestjs/common';
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
@@ -18,6 +18,14 @@ async function bootstrap() {
|
|||||||
const logger = app.get(LoggerService);
|
const logger = app.get(LoggerService);
|
||||||
app.useLogger(logger);
|
app.useLogger(logger);
|
||||||
|
|
||||||
|
// ── API Versioning — global /api/v1/ prefix ──
|
||||||
|
app.setGlobalPrefix('api/v1', {
|
||||||
|
exclude: [
|
||||||
|
{ path: 'health', method: RequestMethod.GET },
|
||||||
|
{ path: 'ready', method: RequestMethod.GET },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
// ── OpenAPI / Swagger ──
|
// ── OpenAPI / Swagger ──
|
||||||
const swaggerConfig = new DocumentBuilder()
|
const swaggerConfig = new DocumentBuilder()
|
||||||
.setTitle('Goodgo Platform API')
|
.setTitle('Goodgo Platform API')
|
||||||
@@ -37,7 +45,7 @@ async function bootstrap() {
|
|||||||
.addTag('analytics', 'Market reports & price analytics')
|
.addTag('analytics', 'Market reports & price analytics')
|
||||||
.build();
|
.build();
|
||||||
const document = SwaggerModule.createDocument(app, swaggerConfig);
|
const document = SwaggerModule.createDocument(app, swaggerConfig);
|
||||||
SwaggerModule.setup('api/docs', app, document, {
|
SwaggerModule.setup('api/v1/docs', app, document, {
|
||||||
swaggerOptions: { persistAuthorization: true },
|
swaggerOptions: { persistAuthorization: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -108,7 +116,7 @@ async function bootstrap() {
|
|||||||
|
|
||||||
const port = process.env['PORT'] ?? 3001;
|
const port = process.env['PORT'] ?? 3001;
|
||||||
await app.listen(port);
|
await app.listen(port);
|
||||||
logger.log(`API running on http://localhost:${port}`, 'Bootstrap');
|
logger.log(`API running on http://localhost:${port}/api/v1`, 'Bootstrap');
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ export { GlobalExceptionFilter } from './filters/global-exception.filter';
|
|||||||
export { CorrelationIdMiddleware } from './middleware/correlation-id.middleware';
|
export { CorrelationIdMiddleware } from './middleware/correlation-id.middleware';
|
||||||
export { RequestLoggingMiddleware } from './middleware/request-logging.middleware';
|
export { RequestLoggingMiddleware } from './middleware/request-logging.middleware';
|
||||||
export { SanitizeInputMiddleware } from './middleware/sanitize-input.middleware';
|
export { SanitizeInputMiddleware } from './middleware/sanitize-input.middleware';
|
||||||
|
export { CsrfMiddleware } from './middleware/csrf.middleware';
|
||||||
export { maskPii } from './pii-masker';
|
export { maskPii } from './pii-masker';
|
||||||
export { ThrottlerBehindProxyGuard } from './guards/throttler-behind-proxy.guard';
|
export { ThrottlerBehindProxyGuard } from './guards/throttler-behind-proxy.guard';
|
||||||
export { FileValidationPipe } from './pipes/file-validation.pipe';
|
export { FileValidationPipe } from './pipes/file-validation.pipe';
|
||||||
export type { FileValidationOptions } from './pipes/file-validation.pipe';
|
export type { FileValidationOptions, UploadedFile } from './pipes/file-validation.pipe';
|
||||||
export { validateEnv, validateJwtSecret } from './env-validation';
|
export { validateEnv, validateJwtSecret } from './env-validation';
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
-- AddCompoundIndexes: query optimization for Listing and Property tables
|
||||||
|
-- Addresses missing compound indexes identified in database audit (TEC-1566)
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- LISTING COMPOUND INDEXES
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
-- 1. Seller dashboard: WHERE sellerId = ? [AND status = ?] ORDER BY publishedAt DESC
|
||||||
|
-- Covers: findBySellerId(), getUserDetail() seller listings, admin user detail
|
||||||
|
-- Leading column sellerId has high cardinality; status + publishedAt enable
|
||||||
|
-- efficient range scans for "my active listings, newest first"
|
||||||
|
CREATE INDEX "Listing_sellerId_status_publishedAt_idx"
|
||||||
|
ON "Listing" ("sellerId", "status", "publishedAt" DESC NULLS LAST);
|
||||||
|
|
||||||
|
-- 2. Agent portfolio: WHERE agentId = ? AND status = ?
|
||||||
|
-- Covers: agent listing management, agent performance queries
|
||||||
|
-- agentId is nullable so Postgres skips NULLs in B-tree naturally
|
||||||
|
CREATE INDEX "Listing_agentId_status_idx"
|
||||||
|
ON "Listing" ("agentId", "status");
|
||||||
|
|
||||||
|
-- 3. Browse / search: WHERE status = ? ORDER BY createdAt DESC
|
||||||
|
-- Covers: search(), findByStatus(), getModerationQueue(), getDashboardStats() counts,
|
||||||
|
-- reindexAll() WHERE status = 'ACTIVE' ORDER BY publishedAt
|
||||||
|
-- Most listing queries filter on status first then paginate by time
|
||||||
|
CREATE INDEX "Listing_status_createdAt_idx"
|
||||||
|
ON "Listing" ("status", "createdAt" DESC);
|
||||||
|
|
||||||
|
-- 4. Active listings feed: WHERE status = 'ACTIVE' ORDER BY publishedAt DESC
|
||||||
|
-- The search indexer and public listing feeds sort by publishedAt for active listings
|
||||||
|
CREATE INDEX "Listing_status_publishedAt_idx"
|
||||||
|
ON "Listing" ("status", "publishedAt" DESC NULLS LAST);
|
||||||
|
|
||||||
|
-- 5. Transaction type filtering: WHERE transactionType = ? AND status = ? ORDER BY createdAt DESC
|
||||||
|
-- Search queries commonly filter by SALE/RENT combined with status
|
||||||
|
CREATE INDEX "Listing_transactionType_status_createdAt_idx"
|
||||||
|
ON "Listing" ("transactionType", "status", "createdAt" DESC);
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- PROPERTY COMPOUND INDEXES
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
-- 6. Location + type search: WHERE district = ? AND propertyType = ?
|
||||||
|
-- Extends existing (district, city) index for the very common search pattern
|
||||||
|
-- that filters by district AND propertyType (via Listing -> Property relation)
|
||||||
|
-- Note: district and propertyType live on Property, not Listing
|
||||||
|
CREATE INDEX "Property_district_propertyType_idx"
|
||||||
|
ON "Property" ("district", "propertyType");
|
||||||
|
|
||||||
|
-- 7. Full location + type search: WHERE district = ? AND city = ? AND propertyType = ?
|
||||||
|
-- Covers the search() query pattern that filters all three together
|
||||||
|
CREATE INDEX "Property_district_city_propertyType_idx"
|
||||||
|
ON "Property" ("district", "city", "propertyType");
|
||||||
@@ -193,9 +193,14 @@ model Property {
|
|||||||
valuations Valuation[]
|
valuations Valuation[]
|
||||||
media PropertyMedia[]
|
media PropertyMedia[]
|
||||||
|
|
||||||
|
// --- Single-column indexes ---
|
||||||
@@index([propertyType])
|
@@index([propertyType])
|
||||||
@@index([district, city])
|
@@index([district, city])
|
||||||
@@index([location], type: Gist)
|
@@index([location], type: Gist)
|
||||||
|
|
||||||
|
// --- Compound indexes (query optimization) ---
|
||||||
|
@@index([district, propertyType])
|
||||||
|
@@index([district, city, propertyType])
|
||||||
}
|
}
|
||||||
|
|
||||||
model PropertyMedia {
|
model PropertyMedia {
|
||||||
@@ -242,6 +247,7 @@ model Listing {
|
|||||||
transactions Transaction[]
|
transactions Transaction[]
|
||||||
inquiries Inquiry[]
|
inquiries Inquiry[]
|
||||||
|
|
||||||
|
// --- Single-column indexes ---
|
||||||
@@index([status])
|
@@index([status])
|
||||||
@@index([transactionType])
|
@@index([transactionType])
|
||||||
@@index([priceVND])
|
@@index([priceVND])
|
||||||
@@ -252,6 +258,13 @@ model Listing {
|
|||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
@@index([featuredUntil])
|
@@index([featuredUntil])
|
||||||
@@index([expiresAt])
|
@@index([expiresAt])
|
||||||
|
|
||||||
|
// --- Compound indexes (query optimization) ---
|
||||||
|
@@index([sellerId, status, publishedAt(sort: Desc)])
|
||||||
|
@@index([agentId, status])
|
||||||
|
@@index([status, createdAt(sort: Desc)])
|
||||||
|
@@index([status, publishedAt(sort: Desc)])
|
||||||
|
@@index([transactionType, status, createdAt(sort: Desc)])
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user