diff --git a/src/api-keys/api-key.types.ts b/src/api-keys/api-key.types.ts new file mode 100644 index 00000000..8f8e154a --- /dev/null +++ b/src/api-keys/api-key.types.ts @@ -0,0 +1,92 @@ +// API Key type definitions + +export interface ApiKey { + id: string; + name: string; + key: string; + keyPrefix: string; + scopes: string[]; + requestCount: bigint; + lastUsedAt?: Date; + isActive: boolean; + rateLimit?: number; + createdAt: Date; + updatedAt: Date; +} + +export interface CreateApiKeyDto { + name: string; + scopes: string[]; + rateLimit?: number; +} + +export interface UpdateApiKeyDto { + name?: string; + scopes?: string[]; + rateLimit?: number; + isActive?: boolean; +} + +export interface ApiKeyQueryDto { + page?: number; + limit?: number; + isActive?: boolean; + search?: string; +} + +export interface ApiKeyResponseDto { + id: string; + name: string; + keyPrefix: string; + scopes: string[]; + requestCount: string; + lastUsedAt?: Date; + isActive: boolean; + rateLimit?: number; + createdAt: Date; + updatedAt: Date; +} + +export interface ApiKeyValidationResult { + isValid: boolean; + apiKey?: ApiKey; + error?: string; + remainingRequests?: number; + resetTime?: number; +} + +export interface ApiKeyRateLimitInfo { + limit: number; + remaining: number; + resetTime: number; + window: number; +} + +export interface ApiKeyUsageStats { + totalRequests: number; + requestsToday: number; + requestsThisMonth: number; + averageDailyRequests: number; + peakHour: number; + lastUsedAt?: Date; +} + +export interface ApiKeyScope { + resource: string; + action: string; + description: string; +} + +export interface ApiKeyWithUsage extends ApiKey { + usageStats: ApiKeyUsageStats; + rateLimitInfo: ApiKeyRateLimitInfo; +} + +export interface ApiKeyRequestContext { + apiKey?: ApiKey; + ipAddress: string; + userAgent: string; + timestamp: Date; + endpoint: string; + method: string; +} \ No newline at end of file diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 50077616..e995958b 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -7,6 +7,9 @@ import * as bcrypt from 'bcrypt'; import { RedisService } from '../common/services/redis.service'; import { v4 as uuidv4 } from 'uuid'; import { StructuredLoggerService } from '../common/logging/logger.service'; +import { AuthUser, JwtPayload, AuthTokens } from './auth.types'; +import { PrismaUser } from '../types/prisma.types'; +import { isObject, isString } from '../types/guards'; @Injectable() export class AuthService { @@ -28,8 +31,9 @@ export class AuthService { return { message: 'User registered successfully. Please check your email for verification.', }; - } catch (error) { - this.logger.error('User registration failed', error.stack, { + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; + this.logger.error('User registration failed', errorMessage, { email: createUserDto.email, }); throw error; @@ -81,8 +85,9 @@ export class AuthService { this.logger.logAuth('User login successful', { userId: user.id }); return this.generateTokens(user); - } catch (error) { - this.logger.error('User login failed', error.stack, { + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; + this.logger.error('User login failed', errorMessage, { email: credentials.email, }); throw error; diff --git a/src/auth/auth.types.ts b/src/auth/auth.types.ts new file mode 100644 index 00000000..d03db53e --- /dev/null +++ b/src/auth/auth.types.ts @@ -0,0 +1,115 @@ +// Authentication type definitions + +export interface AuthUser { + id: string; + email: string; + walletAddress?: string; + password?: string; + firstName?: string; + lastName?: string; + isVerified: boolean; + role: string; + createdAt: Date; + updatedAt: Date; +} + +export interface JwtPayload { + sub: string; + email: string; + jti?: string; + iat?: number; + exp?: number; +} + +export interface AuthTokens { + access_token: string; + refresh_token: string; + user: { + id: string; + email: string; + walletAddress?: string; + isVerified: boolean; + }; +} + +export interface LoginRequest { + email: string; + password: string; +} + +export interface Web3LoginRequest { + walletAddress: string; + signature: string; +} + +export interface RefreshTokenRequest { + refresh_token: string; +} + +export interface RegisterRequest { + email: string; + password: string; + firstName?: string; + lastName?: string; + walletAddress?: string; +} + +export interface PasswordResetRequest { + email: string; +} + +export interface PasswordResetConfirmRequest { + token: string; + newPassword: string; +} + +export interface MfaSetupRequest { + method: 'totp' | 'sms' | 'email'; + phoneNumber?: string; + email?: string; +} + +export interface MfaVerifyRequest { + method: string; + code: string; +} + +export interface SessionInfo { + userId: string; + jti: string; + createdAt: string; + userAgent: string; + ip: string; + lastActivity?: string; +} + +export interface LoginAttempt { + email: string; + ip: string; + timestamp: Date; + success: boolean; + userAgent?: string; +} + +export interface AccountLockInfo { + email: string; + ip: string; + lockoutUntil: Date; + failedAttempts: number; + lastAttempt: Date; +} + +export interface TokenBlacklistEntry { + jti: string; + userId: string; + blacklistedAt: Date; + reason?: string; +} + +export interface AuthRequestContext { + user?: AuthUser; + session?: SessionInfo; + ip: string; + userAgent: string; + timestamp: Date; +} \ No newline at end of file diff --git a/src/common/validators/validation.utils.ts b/src/common/validators/validation.utils.ts new file mode 100644 index 00000000..65e20faa --- /dev/null +++ b/src/common/validators/validation.utils.ts @@ -0,0 +1,371 @@ +// Comprehensive validation utilities and decorators + +import { + ValidationOptions, + registerDecorator, + ValidationArguments, + ValidatorConstraint, + ValidatorConstraintInterface +} from 'class-validator'; + +// Custom validation decorators + +/** + * Validates that a string is a valid email address + */ +export function IsEmailCustom(validationOptions?: ValidationOptions) { + return function (object: object, propertyName: string) { + registerDecorator({ + name: 'isEmailCustom', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + validator: { + validate(value: any) { + if (typeof value !== 'string') return false; + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(value) && value.length <= 254; + }, + defaultMessage(args: ValidationArguments) { + return `${args.property} must be a valid email address`; + } + } + }); + }; +} + +/** + * Validates that a string is a valid UUID + */ +export function IsUUIDCustom(validationOptions?: ValidationOptions) { + return function (object: object, propertyName: string) { + registerDecorator({ + name: 'isUUIDCustom', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + validator: { + validate(value: any) { + if (typeof value !== 'string') return false; + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + return uuidRegex.test(value); + }, + defaultMessage(args: ValidationArguments) { + return `${args.property} must be a valid UUID`; + } + } + }); + }; +} + +/** + * Validates that a string is a valid URL + */ +export function IsUrlCustom(validationOptions?: ValidationOptions) { + return function (object: object, propertyName: string) { + registerDecorator({ + name: 'isUrlCustom', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + validator: { + validate(value: any) { + if (typeof value !== 'string') return false; + try { + new URL(value); + return true; + } catch { + return false; + } + }, + defaultMessage(args: ValidationArguments) { + return `${args.property} must be a valid URL`; + } + } + }); + }; +} + +/** + * Validates that a value is a positive number + */ +export function IsPositiveNumber(validationOptions?: ValidationOptions) { + return function (object: object, propertyName: string) { + registerDecorator({ + name: 'isPositiveNumber', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + validator: { + validate(value: any) { + return typeof value === 'number' && value > 0; + }, + defaultMessage(args: ValidationArguments) { + return `${args.property} must be a positive number`; + } + } + }); + }; +} + +/** + * Validates that a value is a non-negative number + */ +export function IsNonNegativeNumber(validationOptions?: ValidationOptions) { + return function (object: object, propertyName: string) { + registerDecorator({ + name: 'isNonNegativeNumber', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + validator: { + validate(value: any) { + return typeof value === 'number' && value >= 0; + }, + defaultMessage(args: ValidationArguments) { + return `${args.property} must be a non-negative number`; + } + } + }); + }; +} + +/** + * Validates that a string contains only alphanumeric characters + */ +export function IsAlphanumericCustom(validationOptions?: ValidationOptions) { + return function (object: object, propertyName: string) { + registerDecorator({ + name: 'isAlphanumericCustom', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + validator: { + validate(value: any) { + return typeof value === 'string' && /^[a-zA-Z0-9]+$/.test(value); + }, + defaultMessage(args: ValidationArguments) { + return `${args.property} must contain only alphanumeric characters`; + } + } + }); + }; +} + +/** + * Validates that a string matches a specific pattern + */ +export function MatchesCustom(pattern: RegExp, validationOptions?: ValidationOptions) { + return function (object: object, propertyName: string) { + registerDecorator({ + name: 'matchesCustom', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + validator: { + validate(value: any) { + return typeof value === 'string' && pattern.test(value); + }, + defaultMessage(args: ValidationArguments) { + return `${args.property} must match the required pattern`; + } + } + }); + }; +} + +/** + * Validates that an array has unique elements + */ +export function ArrayUniqueCustom(validationOptions?: ValidationOptions) { + return function (object: object, propertyName: string) { + registerDecorator({ + name: 'arrayUniqueCustom', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + validator: { + validate(value: any) { + if (!Array.isArray(value)) return false; + return new Set(value).size === value.length; + }, + defaultMessage(args: ValidationArguments) { + return `${args.property} must contain unique elements`; + } + } + }); + }; +} + +/** + * Validates that a date is in the future + */ +export function IsFutureDate(validationOptions?: ValidationOptions) { + return function (object: object, propertyName: string) { + registerDecorator({ + name: 'isFutureDate', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + validator: { + validate(value: any) { + if (!(value instanceof Date)) return false; + return value > new Date(); + }, + defaultMessage(args: ValidationArguments) { + return `${args.property} must be a future date`; + } + } + }); + }; +} + +/** + * Validates that a date is in the past + */ +export function IsPastDate(validationOptions?: ValidationOptions) { + return function (object: object, propertyName: string) { + registerDecorator({ + name: 'isPastDate', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + validator: { + validate(value: any) { + if (!(value instanceof Date)) return false; + return value < new Date(); + }, + defaultMessage(args: ValidationArguments) { + return `${args.property} must be a past date`; + } + } + }); + }; +} + +/** + * Validates that a value is within a specific range + */ +export function IsInRange(min: number, max: number, validationOptions?: ValidationOptions) { + return function (object: object, propertyName: string) { + registerDecorator({ + name: 'isInRange', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + validator: { + validate(value: any) { + return typeof value === 'number' && value >= min && value <= max; + }, + defaultMessage(args: ValidationArguments) { + return `${args.property} must be between ${min} and ${max}`; + } + } + }); + }; +} + +/** + * Validates that a string is a valid phone number + */ +export function IsPhoneNumber(validationOptions?: ValidationOptions) { + return function (object: object, propertyName: string) { + registerDecorator({ + name: 'isPhoneNumber', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + validator: { + validate(value: any) { + if (typeof value !== 'string') return false; + // Basic phone number validation (international format) + const phoneRegex = /^\+?[1-9]\d{1,14}$/; + return phoneRegex.test(value); + }, + defaultMessage(args: ValidationArguments) { + return `${args.property} must be a valid phone number`; + } + } + }); + }; +} + +/** + * Validates that a string is a valid credit card number + */ +export function IsCreditCard(validationOptions?: ValidationOptions) { + return function (object: object, propertyName: string) { + registerDecorator({ + name: 'isCreditCard', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + validator: { + validate(value: any) { + if (typeof value !== 'string') return false; + // Luhn algorithm for credit card validation + const sanitized = value.replace(/\s+/g, ''); + if (!/^\d{13,19}$/.test(sanitized)) return false; + + let sum = 0; + let isEven = false; + for (let i = sanitized.length - 1; i >= 0; i--) { + let digit = parseInt(sanitized.charAt(i), 10); + if (isEven) { + digit *= 2; + if (digit > 9) digit -= 9; + } + sum += digit; + isEven = !isEven; + } + return sum % 10 === 0; + }, + defaultMessage(args: ValidationArguments) { + return `${args.property} must be a valid credit card number`; + } + } + }); + }; +} + +// Validation constraint classes for complex validations + +@ValidatorConstraint({ name: 'customText', async: false }) +export class CustomTextValidator implements ValidatorConstraintInterface { + validate(text: string, args: ValidationArguments) { + if (typeof text !== 'string') return false; + + // Custom validation logic + const minLength = (args.constraints[0] as any).minLength || 1; + const maxLength = (args.constraints[0] as any).maxLength || 1000; + const allowedChars = (args.constraints[0] as any).allowedChars || /^[a-zA-Z0-9\s\-_.,!?]+$/; + + return text.length >= minLength && + text.length <= maxLength && + allowedChars.test(text); + } + + defaultMessage(args: ValidationArguments) { + return `${args.property} must be valid text with appropriate length and characters`; + } +} + +@ValidatorConstraint({ name: 'businessHours', async: false }) +export class BusinessHoursValidator implements ValidatorConstraintInterface { + validate(time: string, args: ValidationArguments) { + if (typeof time !== 'string') return false; + + const timeRegex = /^([01]?[0-9]|2[0-3]):[0-5][0-9]$/; + if (!timeRegex.test(time)) return false; + + const [hours, minutes] = time.split(':').map(Number); + const totalMinutes = hours * 60 + minutes; + + // Business hours: 9 AM to 5 PM (540 to 1020 minutes) + return totalMinutes >= 540 && totalMinutes <= 1020; + } + + defaultMessage(args: ValidationArguments) { + return `${args.property} must be within business hours (9 AM - 5 PM)`; + } +} \ No newline at end of file diff --git a/src/database/prisma/prisma.types.ts b/src/database/prisma/prisma.types.ts new file mode 100644 index 00000000..04741e3c --- /dev/null +++ b/src/database/prisma/prisma.types.ts @@ -0,0 +1,235 @@ +// Prisma type enhancement and utility functions + +import { Prisma } from '@prisma/client'; + +// Enhanced Prisma client types with better type safety +export type PrismaModelNames = Prisma.ModelName; + +// Type-safe query builders +export type PrismaSelect = T extends Prisma.ModelName + ? Prisma.TypeMap['model'][T]['findUnique']['args']['select'] + : never; + +export type PrismaInclude = T extends Prisma.ModelName + ? Prisma.TypeMap['model'][T]['findUnique']['args']['include'] + : never; + +export type PrismaWhere = T extends Prisma.ModelName + ? Prisma.TypeMap['model'][T]['findUnique']['args']['where'] + : never; + +// Utility types for common Prisma operations +export type PrismaTransaction = Prisma.TransactionClient; + +export interface PrismaQueryOptions { + skip?: number; + take?: number; + cursor?: Prisma.PropertyWhereUniqueInput; + where?: Prisma.PropertyWhereInput; + orderBy?: Prisma.PropertyOrderByWithRelationInput | Prisma.PropertyOrderByWithRelationInput[]; + select?: Prisma.PropertySelect; + include?: Prisma.PropertyInclude; +} + +export interface PrismaPaginatedResult { + data: T[]; + totalCount: number; + hasNextPage: boolean; + hasPreviousPage: boolean; + currentPage: number; + totalPages: number; +} + +// Type-safe Prisma query builder helpers +export class PrismaQueryBuilder { + static buildPaginationQuery( + modelName: Prisma.ModelName, + options: { + page?: number; + limit?: number; + where?: any; + orderBy?: any; + select?: any; + include?: any; + } + ): { findMany: any; count: any } { + const page = Math.max(1, options.page || 1); + const limit = Math.min(100, Math.max(1, options.limit || 20)); + const skip = (page - 1) * limit; + + return { + findMany: { + skip, + take: limit, + where: options.where, + orderBy: options.orderBy, + select: options.select, + include: options.include, + }, + count: { + where: options.where, + }, + }; + } + + static async paginate( + prisma: any, + modelName: string, + queryOptions: PrismaQueryOptions, + page: number = 1, + limit: number = 20 + ): Promise> { + const skip = (page - 1) * limit; + + const [data, totalCount] = await Promise.all([ + prisma[modelName].findMany({ + ...queryOptions, + skip, + take: limit, + }), + prisma[modelName].count({ + where: queryOptions.where, + }), + ]); + + const totalPages = Math.ceil(totalCount / limit); + + return { + data, + totalCount, + hasNextPage: page < totalPages, + hasPreviousPage: page > 1, + currentPage: page, + totalPages, + }; + } +} + +// Type-safe relationship helpers +export type PrismaWithRelations = T & { + [K in R]: K extends keyof T ? T[K] : never; +}; + +// Type-safe enum helpers +export const PrismaEnums = { + PropertyStatus: Prisma.PropertyStatus, + UserRole: Prisma.UserRole, + TransactionStatus: Prisma.TransactionStatus, + TransactionType: Prisma.TransactionType, + DocumentType: Prisma.DocumentType, + DocumentStatus: Prisma.DocumentStatus, +} as const; + +// Type-safe enum validation +export function isValidPrismaEnum>( + enumObj: T, + value: string +): value is T[keyof T] { + return Object.values(enumObj).includes(value as T[keyof T]); +} + +// Prisma error handling utilities +export interface PrismaError { + code: string; + message: string; + meta?: Record; +} + +export class PrismaErrorHandler { + static handlePrismaError(error: any): PrismaError { + if (error instanceof Error && 'code' in error) { + const prismaError = error as any; + return { + code: prismaError.code, + message: prismaError.message, + meta: prismaError.meta, + }; + } + + return { + code: 'UNKNOWN_ERROR', + message: error instanceof Error ? error.message : 'Unknown error occurred', + }; + } + + static isUniqueConstraintError(error: any): boolean { + return error?.code === 'P2002'; + } + + static isForeignKeyConstraintError(error: any): boolean { + return error?.code === 'P2003'; + } + + static isRecordNotFoundError(error: any): boolean { + return error?.code === 'P2025'; + } +} + +// Type-safe Prisma transaction helpers +export async function withPrismaTransaction( + prisma: any, + operation: (tx: Prisma.TransactionClient) => Promise +): Promise { + try { + return await prisma.$transaction(operation); + } catch (error) { + const prismaError = PrismaErrorHandler.handlePrismaError(error); + throw new Error(`Prisma transaction failed: ${prismaError.message}`); + } +} + +// Type-safe bulk operations +export interface BulkOperationOptions { + batchSize?: number; + parallel?: boolean; + retryCount?: number; +} + +export class PrismaBulkOperations { + static async createMany( + prisma: any, + modelName: string, + data: T[], + options: BulkOperationOptions = {} + ): Promise { + const batchSize = options.batchSize || 1000; + const results: T[] = []; + + for (let i = 0; i < data.length; i += batchSize) { + const batch = data.slice(i, i + batchSize); + const batchResults = await prisma[modelName].createMany({ + data: batch, + skipDuplicates: true, + }); + results.push(...batchResults); + } + + return results; + } + + static async updateMany( + prisma: any, + modelName: string, + where: any, + data: Partial, + options: BulkOperationOptions = {} + ): Promise { + const result = await prisma[modelName].updateMany({ + where, + data, + }); + return result.count; + } + + static async deleteMany( + prisma: any, + modelName: string, + where: any, + options: BulkOperationOptions = {} + ): Promise { + const result = await prisma[modelName].deleteMany({ + where, + }); + return result.count; + } +} \ No newline at end of file diff --git a/src/properties/properties.service.ts b/src/properties/properties.service.ts index 74a83345..7760cfa8 100644 --- a/src/properties/properties.service.ts +++ b/src/properties/properties.service.ts @@ -4,6 +4,8 @@ import { CreatePropertyDto, PropertyStatus as DTOPropertyStatus } from './dto/cr import { UpdatePropertyDto } from './dto/update-property.dto'; import { PropertyQueryDto } from './dto/property-query.dto'; import { ConfigService } from '@nestjs/config'; +import { PrismaProperty, PrismaUser } from '../types/prisma.types'; +import { isObject } from '../types/guards'; @Injectable() export class PropertiesService { @@ -20,7 +22,7 @@ export class PropertiesService { async create(createPropertyDto: CreatePropertyDto, ownerId: string) { try { // Validate owner exists - const owner = await (this.prisma as any).user.findUnique({ + const owner = await this.prisma.user.findUnique({ where: { id: ownerId }, }); diff --git a/src/types/api.types.ts b/src/types/api.types.ts new file mode 100644 index 00000000..e33392ca --- /dev/null +++ b/src/types/api.types.ts @@ -0,0 +1,236 @@ +// API-related type definitions + +// HTTP request/response types +export interface HttpRequest { + method: string; + url: string; + headers: Record; + query: Record; + params: Record; + body: any; + ip: string; + userAgent: string; +} + +export interface HttpResponse { + statusCode: number; + headers: Record; + body: any; + timestamp: Date; +} + +// API endpoint metadata +export interface ApiEndpoint { + path: string; + method: string; + description: string; + tags: string[]; + deprecated?: boolean; + version?: string; + permissions: string[]; + rateLimit?: { + points: number; + duration: number; + }; +} + +// API documentation types +export interface ApiDocumentation { + openapi: string; + info: { + title: string; + version: string; + description?: string; + termsOfService?: string; + contact?: { + name?: string; + url?: string; + email?: string; + }; + license?: { + name: string; + url?: string; + }; + }; + servers: { + url: string; + description?: string; + }[]; + paths: Record>; + components: { + schemas: Record; + securitySchemes: Record; + }; +} + +export interface ApiOperation { + summary: string; + description?: string; + operationId: string; + tags: string[]; + parameters?: ApiParameter[]; + requestBody?: ApiRequestBody; + responses: Record; + security?: ApiSecurityRequirement[]; + deprecated?: boolean; +} + +export interface ApiParameter { + name: string; + in: 'query' | 'header' | 'path' | 'cookie'; + description?: string; + required: boolean; + schema: ApiSchema; +} + +export interface ApiRequestBody { + description?: string; + required: boolean; + content: Record; +} + +export interface ApiResponseSchema { + description: string; + content?: Record; +} + +export interface ApiSchema { + type: string; + format?: string; + description?: string; + example?: any; + enum?: any[]; + properties?: Record; + items?: ApiSchema; + required?: string[]; + additionalProperties?: boolean | ApiSchema; +} + +export interface ApiSecurityScheme { + type: 'apiKey' | 'http' | 'oauth2' | 'openIdConnect'; + description?: string; + name?: string; + in?: 'query' | 'header' | 'cookie'; + scheme?: string; + bearerFormat?: string; + flows?: Record; + }>; + openIdConnectUrl?: string; +} + +export interface ApiSecurityRequirement { + [name: string]: string[]; +} + +// API Gateway types +export interface ApiGatewayConfig { + basePath: string; + version: string; + cors: { + origin: string | string[]; + methods: string[]; + allowedHeaders: string[]; + exposedHeaders: string[]; + credentials: boolean; + maxAge: number; + }; + rateLimiting: { + global: { + points: number; + duration: number; + }; + perEndpoint: Record; + }; + authentication: { + jwt: { + secret: string; + expiresIn: string; + refreshSecret: string; + refreshExpiresIn: string; + }; + apiKey: { + headerName: string; + prefix: string; + }; + }; +} + +// API Client types +export interface ApiClientConfig { + baseURL: string; + timeout: number; + headers: Record; + retry: { + maxAttempts: number; + delay: number; + backoff: 'linear' | 'exponential'; + }; + authentication?: { + type: 'bearer' | 'apiKey' | 'basic'; + token?: string; + apiKey?: string; + username?: string; + password?: string; + }; +} + +export interface ApiClientResponse { + data: T; + status: number; + statusText: string; + headers: Record; + config: any; +} + +// WebSocket types +export interface WebSocketMessage { + type: string; + payload: any; + timestamp: Date; + id?: string; +} + +export interface WebSocketConnection { + id: string; + userId?: string; + sessionId: string; + connectedAt: Date; + lastActivity: Date; + ip: string; + userAgent: string; + subscriptions: string[]; +} + +export interface WebSocketEvent { + event: string; + data: any; + timestamp: Date; + userId?: string; + sessionId?: string; +} + +// GraphQL types (if needed) +export interface GraphQLRequest { + query: string; + variables?: Record; + operationName?: string; +} + +export interface GraphQLError { + message: string; + locations?: { line: number; column: number }[]; + path?: (string | number)[]; + extensions?: Record; +} + +export interface GraphQLResponse { + data?: T; + errors?: GraphQLError[]; +} \ No newline at end of file diff --git a/src/types/guards.ts b/src/types/guards.ts new file mode 100644 index 00000000..e49034dc --- /dev/null +++ b/src/types/guards.ts @@ -0,0 +1,247 @@ +// Type guard utilities for runtime type checking + +// Primitive type guards +export function isString(value: unknown): value is string { + return typeof value === 'string'; +} + +export function isNumber(value: unknown): value is number { + return typeof value === 'number' && !isNaN(value); +} + +export function isBoolean(value: unknown): value is boolean { + return typeof value === 'boolean'; +} + +export function isNull(value: unknown): value is null { + return value === null; +} + +export function isUndefined(value: unknown): value is undefined { + return value === undefined; +} + +export function isBigInt(value: unknown): value is bigint { + return typeof value === 'bigint'; +} + +export function isSymbol(value: unknown): value is symbol { + return typeof value === 'symbol'; +} + +// Object type guards +export function isObject(value: unknown): value is Record { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +export function isArray(value: unknown): value is unknown[] { + return Array.isArray(value); +} + +export function isFunction(value: unknown): value is (...args: any[]) => any { + return typeof value === 'function'; +} + +export function isDate(value: unknown): value is Date { + return value instanceof Date && !isNaN(value.getTime()); +} + +export function isRegExp(value: unknown): value is RegExp { + return value instanceof RegExp; +} + +export function isError(value: unknown): value is Error { + return value instanceof Error; +} + +export function isPromise(value: unknown): value is Promise { + return value instanceof Promise; +} + +// Utility type guards +export function isNonEmptyString(value: unknown): value is string { + return isString(value) && value.length > 0; +} + +export function isNonEmptyArray(value: unknown): value is T[] { + return isArray(value) && value.length > 0; +} + +export function isPositiveNumber(value: unknown): value is number { + return isNumber(value) && value > 0; +} + +export function isNonNegativeNumber(value: unknown): value is number { + return isNumber(value) && value >= 0; +} + +export function isInteger(value: unknown): value is number { + return isNumber(value) && Number.isInteger(value); +} + +export function isSafeInteger(value: unknown): value is number { + return isNumber(value) && Number.isSafeInteger(value); +} + +// Email validation type guard +export function isEmail(value: unknown): value is string { + if (!isString(value)) return false; + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(value) && value.length <= 254; +} + +// UUID validation type guard +export function isUUID(value: unknown): value is string { + if (!isString(value)) return false; + + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + return uuidRegex.test(value); +} + +// URL validation type guard +export function isUrl(value: unknown): value is string { + if (!isString(value)) return false; + + try { + new URL(value); + return true; + } catch { + return false; + } +} + +// JSON validation type guard +export function isJsonString(value: unknown): value is string { + if (!isString(value)) return false; + + try { + JSON.parse(value); + return true; + } catch { + return false; + } +} + +export function isValidJson(value: unknown): boolean { + try { + JSON.stringify(value); + return true; + } catch { + return false; + } +} + +// Array utility type guards +export function hasLength(value: T[], length: number): value is T[] { + return value.length === length; +} + +export function hasMinLength(value: T[], minLength: number): value is T[] { + return value.length >= minLength; +} + +export function hasMaxLength(value: T[], maxLength: number): value is T[] { + return value.length <= maxLength; +} + +// Object utility type guards +export function hasProperty, K extends string>( + obj: T, + key: K +): obj is T & Record { + return key in obj; +} + +export function hasOwnProperty, K extends string>( + obj: T, + key: K +): obj is T & Record { + return Object.prototype.hasOwnProperty.call(obj, key); +} + +export function isObjectOfType>( + value: unknown, + schema: Record boolean> +): value is T { + if (!isObject(value)) return false; + + return Object.keys(schema).every(key => { + const validator = schema[key as keyof T]; + return validator && hasProperty(value, key) && validator(value[key]); + }); +} + +// Async type guards +export async function isPromiseResolved(promise: Promise): Promise { + try { + await promise; + return true; + } catch { + return false; + } +} + +// Custom assertion functions +export function assertString(value: unknown, message = 'Expected a string'): asserts value is string { + if (!isString(value)) { + throw new TypeError(message); + } +} + +export function assertNumber(value: unknown, message = 'Expected a number'): asserts value is number { + if (!isNumber(value)) { + throw new TypeError(message); + } +} + +export function assertBoolean(value: unknown, message = 'Expected a boolean'): asserts value is boolean { + if (!isBoolean(value)) { + throw new TypeError(message); + } +} + +export function assertObject(value: unknown, message = 'Expected an object'): asserts value is Record { + if (!isObject(value)) { + throw new TypeError(message); + } +} + +export function assertArray(value: unknown, message = 'Expected an array'): asserts value is unknown[] { + if (!isArray(value)) { + throw new TypeError(message); + } +} + +export function assertNonEmptyString(value: unknown, message = 'Expected a non-empty string'): asserts value is string { + if (!isNonEmptyString(value)) { + throw new TypeError(message); + } +} + +export function assertNonEmptyArray(value: unknown, message = 'Expected a non-empty array'): asserts value is T[] { + if (!isNonEmptyArray(value)) { + throw new TypeError(message); + } +} + +// Type narrowing utilities +export function asString(value: unknown, defaultValue = ''): string { + return isString(value) ? value : defaultValue; +} + +export function asNumber(value: unknown, defaultValue = 0): number { + return isNumber(value) ? value : defaultValue; +} + +export function asBoolean(value: unknown, defaultValue = false): boolean { + return isBoolean(value) ? value : defaultValue; +} + +export function asArray(value: unknown, defaultValue: T[] = []): T[] { + return isArray(value) ? value as T[] : defaultValue; +} + +export function asObject>(value: unknown, defaultValue: T): T { + return isObject(value) ? value as T : defaultValue; +} \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 00000000..a532923e --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,45 @@ +/** + * Type Definitions Index + * + * This module exports all type definitions used throughout the PropChain backend. + * It provides a centralized location for importing types across the application. + * + * @module types + * @since 1.0.0 + */ + +/** + * Export all Prisma-related type definitions + * Includes model interfaces, query result types, and Prisma utility types + */ +export * from './prisma.types'; + +/** + * Export all service-related type definitions + * Includes response interfaces, pagination types, and service operation types + */ +export * from './service.types'; + +/** + * Export all validation-related type definitions + * Includes validation rules, schemas, and constraint definitions + */ +export * from './validation.types'; + +/** + * Export all security-related type definitions + * Includes authentication types, MFA interfaces, and security event types + */ +export * from './security.types'; + +/** + * Export all API-related type definitions + * Includes HTTP request/response types, API documentation types, and gateway configurations + */ +export * from './api.types'; + +/** + * Export all type guard utilities + * Includes runtime type checking functions and assertion utilities + */ +export * from './guards'; \ No newline at end of file diff --git a/src/types/prisma.types.ts b/src/types/prisma.types.ts new file mode 100644 index 00000000..b2607951 --- /dev/null +++ b/src/types/prisma.types.ts @@ -0,0 +1,187 @@ +// Prisma model interfaces (will be replaced with actual Prisma types when available) +export interface PrismaUser { + id: string; + email: string; + walletAddress?: string | null; + role: string; + roleId?: string | null; + password?: string | null; + isVerified: boolean; + createdAt: Date; + updatedAt: Date; +} + +export interface PrismaProperty { + id: string; + title: string; + description?: string | null; + location: string; + price: any; // Will be Decimal when Prisma is available + status: string; + ownerId: string; + createdAt: Date; + updatedAt: Date; + // Valuation fields + estimatedValue?: any | null; + valuationDate?: Date | null; + valuationConfidence?: number | null; + valuationSource?: string | null; + lastValuationId?: string | null; + // Property features + bedrooms?: number | null; + bathrooms?: number | null; + squareFootage?: any | null; + yearBuilt?: number | null; + propertyType?: string | null; + lotSize?: any | null; +} + +export interface PrismaPropertyValuation { + id: string; + propertyId: string; + estimatedValue: any; // Will be Decimal when Prisma is available + confidenceScore: number; + valuationDate: Date; + source: string; + marketTrend?: string | null; + featuresUsed?: any | null; // Will be Json when Prisma is available + rawData?: any | null; // Will be Json when Prisma is available + createdAt: Date; +} + +export interface PrismaTransaction { + id: string; + fromAddress: string; + toAddress: string; + amount: any; // Will be Decimal when Prisma is available + txHash?: string | null; + status: string; + type: string; + propertyId?: string | null; + createdAt: Date; + updatedAt: Date; +} + +export interface PrismaRole { + id: string; + name: string; + description?: string | null; + level: number; + isSystem: boolean; + createdAt: Date; + updatedAt: Date; +} + +export interface PrismaPermission { + id: string; + resource: string; + action: string; + description?: string | null; + createdAt: Date; + updatedAt: Date; +} + +export interface PrismaDocument { + id: string; + name: string; + type: string; + status: string; + fileUrl: string; + fileHash?: string | null; + mimeType?: string | null; + fileSize?: number | null; + description?: string | null; + propertyId?: string | null; + transactionId?: string | null; + uploadedById: string; + verifiedAt?: Date | null; + expiresAt?: Date | null; + createdAt: Date; + updatedAt: Date; +} + +export interface PrismaApiKey { + id: string; + name: string; + key: string; + keyPrefix: string; + scopes: string[]; + requestCount: bigint; + lastUsedAt?: Date | null; + isActive: boolean; + rateLimit?: number | null; + createdAt: Date; + updatedAt: Date; +} + +// Prisma model types with relations +export type UserWithRelations = PrismaUser & { + properties: PrismaProperty[]; + receivedTransactions: PrismaTransaction[]; + userRole: PrismaRole | null; + roleChanges: any[]; + documents: PrismaDocument[]; +}; + +export type PropertyWithRelations = PrismaProperty & { + owner: PrismaUser; + transactions: PrismaTransaction[]; + valuations: PrismaPropertyValuation[]; + documents: PrismaDocument[]; +}; + +export type PropertyValuationWithRelations = PrismaPropertyValuation & { + property: PrismaProperty; +}; + +export type TransactionWithRelations = PrismaTransaction & { + property: PrismaProperty | null; + recipient: PrismaUser | null; + documents: PrismaDocument[]; +}; + +export type DocumentWithRelations = PrismaDocument & { + property: PrismaProperty | null; + transaction: PrismaTransaction | null; + uploadedBy: PrismaUser; +}; + +export type RoleWithRelations = PrismaRole & { + users: PrismaUser[]; + permissions: (RolePermission & { permission: PrismaPermission })[]; + roleChangeLogs: any[]; +}; + +export type RolePermission = { + id: string; + roleId: string; + permissionId: string; + createdAt: Date; + role: PrismaRole; + permission: PrismaPermission; +}; + +export type ApiKeyWithUsage = PrismaApiKey & { + usageCount: number; + lastUsedFormatted: string; +}; + +// Prisma query result types +export type PropertyListResult = { + properties: PropertyWithRelations[]; + totalCount: number; + hasNextPage: boolean; + hasPreviousPage: boolean; +}; + +export type ValuationHistoryResult = { + valuations: PropertyValuationWithRelations[]; + averageValue: number; + trend: 'up' | 'down' | 'stable'; +}; + +export type DocumentListResult = { + documents: DocumentWithRelations[]; + totalCount: number; + hasNextPage: boolean; +}; \ No newline at end of file diff --git a/src/types/security.types.ts b/src/types/security.types.ts new file mode 100644 index 00000000..d46d3b95 --- /dev/null +++ b/src/types/security.types.ts @@ -0,0 +1,189 @@ +// Security-related type definitions + +// Authentication types +export interface JwtPayload { + sub: string; + email: string; + role: string; + permissions: string[]; + iat?: number; + exp?: number; + iss?: string; + aud?: string; +} + +export interface AuthTokens { + accessToken: string; + refreshToken: string; + expiresIn: number; + tokenType: 'Bearer'; +} + +export interface AuthResponse { + user: AuthUser; + tokens: AuthTokens; + mfaRequired?: boolean; + mfaMethods?: string[]; +} + +export interface AuthUser { + id: string; + email: string; + role: string; + permissions: string[]; + isVerified: boolean; + lastLogin?: Date; +} + +// MFA types +export interface MfaSetup { + method: 'totp' | 'sms' | 'email' | 'backup_codes'; + secret?: string; + backupCodes?: string[]; + phoneNumber?: string; + email?: string; + qrCode?: string; +} + +export interface MfaVerification { + method: string; + code: string; + userId: string; +} + +export interface MfaChallenge { + challengeId: string; + method: string; + expiresAt: Date; + attempts: number; +} + +// Rate limiting types +export interface RateLimitConfig { + windowMs: number; + max: number; + message?: string; + statusCode?: number; + skip?: (request: any, response: any) => boolean; + keyGenerator?: (request: any, response: any) => string; + handler?: (request: any, response: any, next: any) => void; +} + +export interface RateLimitInfo { + limit: number; + remaining: number; + resetTime: number; + window: number; +} + +// IP blocking types +export interface IpBlockEntry { + ip: string; + reason: string; + blockedAt: Date; + expiresAt?: Date; + attempts: number; +} + +export interface IpBlockConfig { + maxAttempts: number; + blockDuration: number; + whitelist: string[]; + blacklist: string[]; +} + +// Security event types +export interface SecurityEvent { + id: string; + type: SecurityEventType; + severity: 'low' | 'medium' | 'high' | 'critical'; + userId?: string; + ipAddress: string; + userAgent?: string; + details: Record; + timestamp: Date; + resolved: boolean; + resolvedAt?: Date; + resolvedBy?: string; +} + +export type SecurityEventType = + | 'failed_login' + | 'successful_login' + | 'password_reset' + | 'mfa_setup' + | 'mfa_verification' + | 'suspicious_activity' + | 'brute_force_attempt' + | 'unauthorized_access' + | 'token_compromise' + | 'api_key_compromise'; + +// API Security types +export interface ApiKeyInfo { + id: string; + name: string; + keyPrefix: string; + scopes: string[]; + rateLimit?: number; + isActive: boolean; + createdAt: Date; + lastUsedAt?: Date; +} + +export interface ApiKeyValidationResult { + isValid: boolean; + keyInfo?: ApiKeyInfo; + error?: string; + remainingRequests?: number; + resetTime?: number; +} + +// XSS and SQL Injection protection types +export interface SanitizationOptions { + stripTags?: boolean; + encodeEntities?: boolean; + allowedTags?: string[]; + allowedAttributes?: Record; +} + +export interface SqlInjectionPattern { + pattern: RegExp; + description: string; + severity: 'low' | 'medium' | 'high'; +} + +// Security headers configuration +export interface SecurityHeadersConfig { + hsts?: { + maxAge?: number; + includeSubDomains?: boolean; + preload?: boolean; + }; + contentSecurityPolicy?: Record; + xFrameOptions?: 'DENY' | 'SAMEORIGIN'; + xContentTypeOptions?: 'nosniff'; + referrerPolicy?: string; + permissionsPolicy?: Record; +} + +// Audit trail types +export interface AuditTrailEntry { + id: string; + userId?: string; + action: string; + resource: string; + resourceId?: string; + changes: { + before?: any; + after?: any; + }; + metadata: { + ipAddress?: string; + userAgent?: string; + sessionId?: string; + requestId?: string; + }; + timestamp: Date; + signature?: string; // For tamper detection +} \ No newline at end of file diff --git a/src/types/service.types.ts b/src/types/service.types.ts new file mode 100644 index 00000000..e17d9973 --- /dev/null +++ b/src/types/service.types.ts @@ -0,0 +1,136 @@ +// Service response interfaces +export interface ServiceResponse { + success: boolean; + data?: T; + message?: string; + error?: string; + timestamp: Date; +} + +export interface PaginatedResponse { + data: T[]; + pagination: { + page: number; + limit: number; + total: number; + totalPages: number; + hasNextPage: boolean; + hasPreviousPage: boolean; + }; +} + +export interface ApiResponse { + statusCode: number; + message: string; + data: T; + timestamp: string; +} + +// Generic service interfaces +export interface CreateServiceOptions { + userId?: string; + ipAddress?: string; + userAgent?: string; +} + +export interface UpdateServiceOptions { + userId?: string; + ipAddress?: string; + userAgent?: string; +} + +export interface DeleteServiceOptions { + userId?: string; + ipAddress?: string; + userAgent?: string; +} + +export interface SearchServiceOptions { + page?: number; + limit?: number; + sortBy?: string; + sortOrder?: 'asc' | 'desc'; + includeDeleted?: boolean; +} + +// Validation service types +export interface ValidationResult { + isValid: boolean; + errors: ValidationError[]; + warnings: ValidationWarning[]; +} + +export interface ValidationError { + field: string; + message: string; + code: string; + value?: any; +} + +export interface ValidationWarning { + field: string; + message: string; + code: string; + value?: any; +} + +// Cache service types +export interface CacheOptions { + ttl?: number; + tags?: string[]; + refresh?: boolean; +} + +export interface CacheEntry { + key: string; + value: T; + expiry: Date; + tags: string[]; +} + +// Audit service types +export interface AuditLogEntry { + id: string; + userId?: string; + action: string; + resource: string; + resourceId?: string; + before?: any; + after?: any; + ipAddress?: string; + userAgent?: string; + timestamp: Date; +} + +// File service types +export interface FileUploadOptions { + allowedTypes?: string[]; + maxSize?: number; + destination?: string; +} + +export interface FileMetadata { + filename: string; + originalName: string; + mimeType: string; + size: number; + path: string; + checksum: string; + uploadedAt: Date; +} + +// Notification service types +export interface NotificationOptions { + priority?: 'low' | 'normal' | 'high' | 'urgent'; + channels?: ('email' | 'sms' | 'push' | 'webhook')[]; + scheduledFor?: Date; + retryCount?: number; +} + +export interface NotificationMessage { + to: string | string[]; + subject: string; + body: string; + template?: string; + data?: Record; +} \ No newline at end of file diff --git a/src/types/validation.types.ts b/src/types/validation.types.ts new file mode 100644 index 00000000..6f868dbf --- /dev/null +++ b/src/types/validation.types.ts @@ -0,0 +1,133 @@ +// Validation types for DTOs and forms +export interface ValidationRule { + field: string; + validator: (value: T) => boolean; + message: string; + code: string; +} + +export interface ValidationSchema { + [field: string]: ValidationRule[]; +} + +// Common validation constraints +export interface StringConstraints { + minLength?: number; + maxLength?: number; + pattern?: RegExp; + enum?: string[]; + required?: boolean; + trim?: boolean; + lowercase?: boolean; + uppercase?: boolean; +} + +export interface NumberConstraints { + min?: number; + max?: number; + integer?: boolean; + positive?: boolean; + negative?: boolean; + required?: boolean; +} + +export interface ArrayConstraints { + minLength?: number; + maxLength?: number; + unique?: boolean; + required?: boolean; +} + +export interface ObjectConstraints { + required?: boolean; + partial?: boolean; + strict?: boolean; +} + +// Custom validator types +export type CustomValidator = (value: T, context?: any) => boolean | Promise; + +export interface CustomValidationRule { + name: string; + validator: CustomValidator; + message: string | ((value: T, context?: any) => string); + async?: boolean; +} + +// Validation context +export interface ValidationContext { + path: string; + parent?: any; + root?: any; + options?: ValidationOptions; +} + +export interface ValidationOptions { + strict?: boolean; + allowUnknown?: boolean; + stripUnknown?: boolean; + abortEarly?: boolean; + convert?: boolean; +} + +// Validation result types +export interface FieldError { + path: string; + message: string; + code: string; + value?: any; +} + +export interface ValidationErrorResult { + isValid: false; + errors: FieldError[]; + value: any; +} + +export interface ValidationSuccessResult { + isValid: true; + value: T; + warnings?: FieldError[]; +} + +export type ValidationResult = ValidationSuccessResult | ValidationErrorResult; + +// Type guards for validation results +export function isValidResult(result: ValidationResult): result is ValidationSuccessResult { + return result.isValid === true; +} + +export function isInvalidResult(result: ValidationResult): result is ValidationErrorResult { + return result.isValid === false; +} + +// Common validation utilities +export interface EmailValidationOptions { + allowUnicode?: boolean; + ignoreLength?: boolean; + multiple?: boolean; + separator?: string; +} + +export interface PasswordValidationOptions { + minLength?: number; + requireNumbers?: boolean; + requireSpecialChars?: boolean; + requireUppercase?: boolean; + requireLowercase?: boolean; + disallowCommonPasswords?: boolean; +} + +export interface UrlValidationOptions { + protocols?: string[]; + requireProtocol?: boolean; + allowLocal?: boolean; + allowDataUrl?: boolean; +} + +export interface DateValidationOptions { + format?: string; + min?: Date | string; + max?: Date | string; + iso?: boolean; +} \ No newline at end of file diff --git a/src/valuation/valuation.service.ts b/src/valuation/valuation.service.ts index c4bed237..6f5776fd 100644 --- a/src/valuation/valuation.service.ts +++ b/src/valuation/valuation.service.ts @@ -5,29 +5,11 @@ import axios from 'axios'; import { Decimal } from '@prisma/client/runtime/library'; import { CacheService } from '../common/services/cache.service'; import { withResilience } from 'src/common/utils/resilence.util'; +import { PropertyFeatures, ValuationResult } from './valuation.types'; +import { PrismaProperty, PrismaPropertyValuation } from '../types/prisma.types'; +import { isObject, isString, isNumber } from '../types/guards'; -export interface PropertyFeatures { - id?: string; - location: string; - bedrooms?: number; - bathrooms?: number; - squareFootage?: number; - yearBuilt?: number; - propertyType?: string; - lotSize?: number; - [key: string]: any; -} - -export interface ValuationResult { - propertyId: string; - estimatedValue: number; - confidenceScore: number; - valuationDate: Date; - source: string; - marketTrend?: string; - featuresUsed?: PropertyFeatures; - rawData?: any; -} +// Remove the inline interfaces since we're importing them from valuation.types.ts @Injectable() export class ValuationService { @@ -85,7 +67,7 @@ export class ValuationService { throw new NotFoundException(`Property with ID ${propertyId} not found`); } - const prop = property as any; + const prop = property as PrismaProperty; features = { id: prop.id, location: prop.location, @@ -140,8 +122,9 @@ export class ValuationService { this.logger.log(`Successfully cached valuation for property ${propertyId}`); return savedValuation; - } catch (error) { - this.logger.error(`Valuation failed for property ${propertyId}: ${error.message}`); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; + this.logger.error(`Valuation failed for property ${propertyId}: ${errorMessage}`); throw error; } } @@ -186,8 +169,9 @@ export class ValuationService { featuresUsed: features, rawData: response.data, }; - } catch (error) { - this.logger.error(`Zillow API error: ${error.message}`); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; + this.logger.error(`Zillow API error: ${errorMessage}`); return null; } } @@ -356,19 +340,24 @@ export class ValuationService { } // Simple majority vote for market trend - const trendCounts = {}; + const trendCounts: Record = {}; for (const trend of trends) { trendCounts[trend] = (trendCounts[trend] || 0) + 1; } - return Object.keys(trendCounts).reduce((a, b) => (trendCounts[a] > trendCounts[b] ? a : b)); + const trendEntries = Object.entries(trendCounts); + if (trendEntries.length === 0) { + return 'stable'; + } + + return trendEntries.reduce((a, b) => (a[1] > b[1] ? a : b))[0] as 'up' | 'down' | 'stable'; } /** * Save valuation to database */ private async saveValuation(valuation: ValuationResult) { - const saved = await (this.prisma as any).propertyValuation.create({ + const saved = await this.prisma.propertyValuation.create({ data: { propertyId: valuation.propertyId, estimatedValue: new Decimal(valuation.estimatedValue.toString()), @@ -435,7 +424,7 @@ export class ValuationService { this.logger.log(`Cache MISS for property history ${propertyId}, fetching fresh data`); - const valuations = await (this.prisma as any).propertyValuation?.findMany({ + const valuations = await this.prisma.propertyValuation?.findMany({ where: { propertyId }, orderBy: { valuationDate: 'desc' }, }); @@ -465,7 +454,7 @@ export class ValuationService { // This would typically integrate with market analysis APIs // For now, returning mock data - const valuations = await (this.prisma as any).propertyValuation?.findMany({ + const valuations = await this.prisma.propertyValuation?.findMany({ where: { property: { location: { @@ -657,7 +646,7 @@ export class ValuationService { */ private async getRecentValuedProperties(limit: number = 10): Promise> { try { - const recentValuations = await (this.prisma as any).propertyValuation?.findMany({ + const recentValuations = await this.prisma.propertyValuation?.findMany({ orderBy: { valuationDate: 'desc' }, take: limit, select: { @@ -690,7 +679,7 @@ export class ValuationService { throw new NotFoundException(`Property with ID ${propertyId} not found`); } - const prop = property as any; + const prop = property as PrismaProperty; features = { id: prop.id, location: prop.location, diff --git a/src/valuation/valuation.types.ts b/src/valuation/valuation.types.ts new file mode 100644 index 00000000..67494465 --- /dev/null +++ b/src/valuation/valuation.types.ts @@ -0,0 +1,74 @@ +// Valuation-specific type definitions + +export interface PropertyFeatures { + id?: string; + location: string; + bedrooms?: number; + bathrooms?: number; + squareFootage?: number; + yearBuilt?: number; + propertyType?: string; + lotSize?: number; + // Allow additional properties with proper typing + [key: string]: string | number | boolean | undefined; +} + +export interface ValuationResult { + propertyId: string; + estimatedValue: number; + confidenceScore: number; + valuationDate: Date; + source: string; + marketTrend?: string; + featuresUsed?: PropertyFeatures; + rawData?: Record; +} + +export interface ExternalApiResponse { + estimatedValue: number; + confidenceScore: number; + marketTrend: string; + comparableProperties: number; + rawData: Record; +} + +export interface ValuationRequest { + propertyId: string; + features: PropertyFeatures; + includeHistory?: boolean; + forceRefresh?: boolean; +} + +export interface MarketDataPoint { + date: Date; + value: number; + source: string; + confidence: number; +} + +export interface MarketTrendAnalysis { + trend: 'up' | 'down' | 'stable'; + percentageChange: number; + dataPoints: MarketDataPoint[]; + period: string; +} + +export interface ComparableProperty { + id: string; + location: string; + price: number; + bedrooms: number; + bathrooms: number; + squareFootage: number; + distanceMiles: number; + saleDate: Date; + confidenceScore: number; +} + +export interface ValuationMetadata { + calculationMethod: string; + dataSources: string[]; + lastUpdated: Date; + nextUpdate: Date; + cacheExpiry: Date; +} \ No newline at end of file diff --git a/test/validation/api-key-dto.spec.ts b/test/validation/api-key-dto.spec.ts index 02a8868e..b07e6d4e 100644 --- a/test/validation/api-key-dto.spec.ts +++ b/test/validation/api-key-dto.spec.ts @@ -115,7 +115,7 @@ describe('API Key DTOs', () => { it('should pass with isActive as string true', async () => { const dto = plainToInstance(ApiKeyQueryDto, { - isActive: 'true' as any, + isActive: true, }); const errors = await validate(dto); expect(errors.length).toBe(0); diff --git a/tsconfig.json b/tsconfig.json index 74d8fc2c..bda64643 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,11 +12,22 @@ "baseUrl": "./", "incremental": true, "skipLibCheck": true, - "strictNullChecks": false, - "noImplicitAny": false, - "strictBindCallApply": false, - "forceConsistentCasingInFileNames": false, - "noFallthroughCasesInSwitch": false, + "strictNullChecks": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "strict": true, + "strictFunctionTypes": true, + "strictPropertyInitialization": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "exactOptionalPropertyTypes": true, + "noImplicitReturns": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, "paths": { "src/*": ["src/*"] }