feat: Implement Prisma PG adapter, refactor user and agent profiles in schema, and add database seeding.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -43,3 +43,4 @@ temp/
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
prisma/migrations/
|
||||
1759
package-lock.json
generated
1759
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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
69
prisma/seed.ts
Normal 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);
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user