feat: Implement Prisma PG adapter, refactor user and agent profiles in schema, and add database seeding.

This commit is contained in:
pradeepkumar
2025-12-19 01:15:19 +05:30
parent c1fcf32eb5
commit 1812fd3c98
8 changed files with 867 additions and 1143 deletions

1
.gitignore vendored
View File

@@ -43,3 +43,4 @@ temp/
*.pid
*.seed
*.pid.lock
prisma/migrations/

1759
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -30,6 +30,7 @@
"@aws-sdk/client-s3": "^3.954.0",
"@aws-sdk/lib-storage": "^3.954.0",
"@aws-sdk/s3-request-presigner": "^3.954.0",
"@keyv/redis": "^5.1.5",
"@nestjs-modules/mailer": "^2.0.2",
"@nestjs/axios": "^4.0.1",
"@nestjs/bull": "^11.0.4",
@@ -47,15 +48,15 @@
"@nestjs/terminus": "^11.0.0",
"@nestjs/throttler": "^6.5.0",
"@nestjs/websockets": "^11.1.9",
"@prisma/adapter-pg": "^7.2.0",
"@prisma/client": "^7.2.0",
"@sendgrid/mail": "^8.1.6",
"argon2": "^0.44.0",
"axios": "^1.13.2",
"bcrypt": "^6.0.0",
"bull": "^4.16.5",
"bull": "^4.16.5",
"bullmq": "^5.66.1",
"cache-manager": "^7.2.7",
"cache-manager-redis-yet": "^5.1.5",
"cache-manager": "^6.3.2",
"cacheable": "^1.8.8",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.3",
"cookie-parser": "^1.4.7",
@@ -63,11 +64,13 @@
"date-fns": "^4.1.0",
"dayjs": "^1.11.19",
"decimal.js": "^10.6.0",
"dotenv": "^17.2.3",
"firebase-admin": "^13.6.0",
"google-auth-library": "^10.5.0",
"helmet": "^8.1.0",
"ioredis": "^5.8.2",
"joi": "^18.0.2",
"keyv": "^5.2.3",
"mime-types": "^3.0.2",
"multer": "^2.0.2",
"nest-winston": "^1.10.2",
@@ -97,8 +100,7 @@
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@types/bcrypt": "^6.0.0",
"@types/cookie-parser": "^1.4.10",
"@types/cookie-parser": "^1.4.10",
"@types/express": "^5.0.0",
"@types/jest": "^30.0.0",
"@types/multer": "^2.0.0",
@@ -106,6 +108,7 @@
"@types/nodemailer": "^7.0.4",
"@types/passport-google-oauth20": "^2.0.17",
"@types/passport-jwt": "^4.0.1",
"@types/pg": "^8.16.0",
"@types/supertest": "^6.0.2",
"autocannon": "^8.0.0",
"eslint": "^9.18.0",
@@ -139,5 +142,8 @@
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
},
"prisma": {
"seed": "ts-node prisma/seed.ts"
}
}

View File

@@ -7,6 +7,7 @@ export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
seed: "npx ts-node prisma/seed.ts",
},
datasource: {
url: process.env["DATABASE_URL"],

View File

@@ -7,7 +7,6 @@ generator client {
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// ===========================================
@@ -33,24 +32,14 @@ enum AuthProvider {
FACEBOOK
}
enum VerificationStatus {
PENDING
APPROVED
REJECTED
}
// ===========================================
// USER & AUTHENTICATION (Milestone 1)
// USER - Authentication Only
// ===========================================
model User {
id String @id @default(uuid())
email String @unique
password String? // Null for social login users
firstName String?
lastName String?
phone String?
avatar String?
role UserRole @default(USER)
status UserStatus @default(ACTIVE)
emailVerified Boolean @default(false)
@@ -66,10 +55,12 @@ model User {
updatedAt DateTime @updatedAt
lastLoginAt DateTime?
// Relations (will be added in later milestones)
// Relations - Profile based on role
userProfile UserProfile?
agentProfile AgentProfile?
// Auth Relations
sessions Session[]
passwordResets PasswordReset[]
@@index([email])
@@index([role])
@@ -77,6 +68,83 @@ model User {
@@map("users")
}
// ===========================================
// USER PROFILE - Normal Users
// ===========================================
model UserProfile {
id String @id @default(uuid())
userId String @unique
firstName String?
lastName String?
phone String?
avatar String?
// Location
city String?
state String?
country String?
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@map("user_profiles")
}
// ===========================================
// AGENT PROFILE - Agents/Subscribers
// ===========================================
model AgentProfile {
id String @id @default(uuid())
userId String @unique
slug String @unique
// Basic Info
firstName String?
lastName String?
phone String?
avatar String?
bio String?
headline String?
// Location
city String?
state String?
country String?
address String?
// Professional Info
yearsOfExperience Int?
licenseNumber String?
companyName String?
// Profile Status
isProfileComplete Boolean @default(false)
profileCompleteness Int @default(0)
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([slug])
@@index([city, state])
@@map("agent_profiles")
}
// ===========================================
// SESSION - JWT Token Management
// ===========================================
model Session {
id String @id @default(uuid())
userId String
@@ -93,55 +161,3 @@ model Session {
@@index([token])
@@map("sessions")
}
model PasswordReset {
id String @id @default(uuid())
userId String
token String @unique
expiresAt DateTime
usedAt DateTime?
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([token])
@@map("password_resets")
}
// ===========================================
// AGENT PROFILE (Milestone 2 - Placeholder)
// ===========================================
model AgentProfile {
id String @id @default(uuid())
userId String @unique
slug String @unique
bio String?
headline String?
location String?
city String?
state String?
country String?
yearsOfExperience Int?
licenseNumber String?
// Verification
verificationStatus VerificationStatus @default(PENDING)
verifiedAt DateTime?
// Profile completeness
isProfileComplete Boolean @default(false)
profileCompleteness Int @default(0)
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([slug])
@@index([verificationStatus])
@@index([city, state])
@@map("agent_profiles")
}

69
prisma/seed.ts Normal file
View File

@@ -0,0 +1,69 @@
import { PrismaClient, UserRole, UserStatus, AuthProvider } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
import { Pool } from 'pg';
import * as argon2 from 'argon2';
async function main() {
const connectionString = process.env.DATABASE_URL;
const pool = new Pool({ connectionString });
const adapter = new PrismaPg(pool);
const prisma = new PrismaClient({ adapter });
console.log('🌱 Starting database seeding...\n');
// Admin credentials
const adminEmail = process.env.ADMIN_EMAIL || 'admin@realestate.com';
const adminPassword = process.env.ADMIN_PASSWORD || 'Admin@123456';
// Hash password with Argon2 (more secure than bcrypt)
const hashedPassword = await argon2.hash(adminPassword);
// Check if admin already exists
const existingAdmin = await prisma.user.findUnique({
where: { email: adminEmail },
});
if (existingAdmin) {
console.log(`⚠️ Admin user already exists: ${adminEmail}`);
} else {
// Create admin user
const admin = await prisma.user.create({
data: {
email: adminEmail,
password: hashedPassword,
role: UserRole.ADMIN,
status: UserStatus.ACTIVE,
emailVerified: true,
emailVerifiedAt: new Date(),
authProvider: AuthProvider.LOCAL,
},
});
console.log('✅ Admin user created successfully!');
console.log(' ───────────────────────────────');
console.log(` 📧 Email: ${adminEmail}`);
console.log(` 🔑 Password: ${adminPassword}`);
console.log(` 👤 Role: ${admin.role}`);
console.log(` 🆔 ID: ${admin.id}`);
console.log(' ───────────────────────────────\n');
}
// Summary
const userCount = await prisma.user.count();
const adminCount = await prisma.user.count({ where: { role: UserRole.ADMIN } });
console.log('📊 Database Summary:');
console.log(` Total Users: ${userCount}`);
console.log(` Admins: ${adminCount}`);
console.log('\n🌱 Seeding completed!\n');
await prisma.$disconnect();
await pool.end();
}
main()
.catch((e) => {
console.error('❌ Seeding failed:', e);
process.exit(1);
});

View File

@@ -3,7 +3,7 @@ import { ValidationPipe, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import helmet from 'helmet';
import * as cookieParser from 'cookie-parser';
import cookieParser from 'cookie-parser';
import { AppModule } from './app.module';
import { AllExceptionsFilter } from './common/filters';
import { TransformInterceptor } from './common/interceptors';

View File

@@ -1,18 +1,29 @@
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
import { Pool } from 'pg';
@Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
private pool: Pool;
constructor() {
const connectionString = process.env.DATABASE_URL;
const pool = new Pool({ connectionString });
const adapter = new PrismaPg(pool);
super({
adapter,
log:
process.env.NODE_ENV === 'development'
? ['query', 'info', 'warn', 'error']
: ['error'],
});
this.pool = pool;
}
async onModuleInit() {
@@ -21,6 +32,7 @@ export class PrismaService
async onModuleDestroy() {
await this.$disconnect();
await this.pool.end();
}
async cleanDatabase() {