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
|
*.pid
|
||||||
*.seed
|
*.seed
|
||||||
*.pid.lock
|
*.pid.lock
|
||||||
|
prisma/migrations/
|
||||||
1759
package-lock.json
generated
1759
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -30,6 +30,7 @@
|
|||||||
"@aws-sdk/client-s3": "^3.954.0",
|
"@aws-sdk/client-s3": "^3.954.0",
|
||||||
"@aws-sdk/lib-storage": "^3.954.0",
|
"@aws-sdk/lib-storage": "^3.954.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.954.0",
|
"@aws-sdk/s3-request-presigner": "^3.954.0",
|
||||||
|
"@keyv/redis": "^5.1.5",
|
||||||
"@nestjs-modules/mailer": "^2.0.2",
|
"@nestjs-modules/mailer": "^2.0.2",
|
||||||
"@nestjs/axios": "^4.0.1",
|
"@nestjs/axios": "^4.0.1",
|
||||||
"@nestjs/bull": "^11.0.4",
|
"@nestjs/bull": "^11.0.4",
|
||||||
@@ -47,15 +48,15 @@
|
|||||||
"@nestjs/terminus": "^11.0.0",
|
"@nestjs/terminus": "^11.0.0",
|
||||||
"@nestjs/throttler": "^6.5.0",
|
"@nestjs/throttler": "^6.5.0",
|
||||||
"@nestjs/websockets": "^11.1.9",
|
"@nestjs/websockets": "^11.1.9",
|
||||||
|
"@prisma/adapter-pg": "^7.2.0",
|
||||||
"@prisma/client": "^7.2.0",
|
"@prisma/client": "^7.2.0",
|
||||||
"@sendgrid/mail": "^8.1.6",
|
"@sendgrid/mail": "^8.1.6",
|
||||||
"argon2": "^0.44.0",
|
"argon2": "^0.44.0",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"bcrypt": "^6.0.0",
|
|
||||||
"bull": "^4.16.5",
|
"bull": "^4.16.5",
|
||||||
"bullmq": "^5.66.1",
|
"bullmq": "^5.66.1",
|
||||||
"cache-manager": "^7.2.7",
|
"cache-manager": "^6.3.2",
|
||||||
"cache-manager-redis-yet": "^5.1.5",
|
"cacheable": "^1.8.8",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.3",
|
"class-validator": "^0.14.3",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
@@ -63,11 +64,13 @@
|
|||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"decimal.js": "^10.6.0",
|
"decimal.js": "^10.6.0",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
"firebase-admin": "^13.6.0",
|
"firebase-admin": "^13.6.0",
|
||||||
"google-auth-library": "^10.5.0",
|
"google-auth-library": "^10.5.0",
|
||||||
"helmet": "^8.1.0",
|
"helmet": "^8.1.0",
|
||||||
"ioredis": "^5.8.2",
|
"ioredis": "^5.8.2",
|
||||||
"joi": "^18.0.2",
|
"joi": "^18.0.2",
|
||||||
|
"keyv": "^5.2.3",
|
||||||
"mime-types": "^3.0.2",
|
"mime-types": "^3.0.2",
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
"nest-winston": "^1.10.2",
|
"nest-winston": "^1.10.2",
|
||||||
@@ -97,7 +100,6 @@
|
|||||||
"@nestjs/cli": "^11.0.0",
|
"@nestjs/cli": "^11.0.0",
|
||||||
"@nestjs/schematics": "^11.0.0",
|
"@nestjs/schematics": "^11.0.0",
|
||||||
"@nestjs/testing": "^11.0.1",
|
"@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/express": "^5.0.0",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
@@ -106,6 +108,7 @@
|
|||||||
"@types/nodemailer": "^7.0.4",
|
"@types/nodemailer": "^7.0.4",
|
||||||
"@types/passport-google-oauth20": "^2.0.17",
|
"@types/passport-google-oauth20": "^2.0.17",
|
||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
|
"@types/pg": "^8.16.0",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"autocannon": "^8.0.0",
|
"autocannon": "^8.0.0",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.18.0",
|
||||||
@@ -139,5 +142,8 @@
|
|||||||
],
|
],
|
||||||
"coverageDirectory": "../coverage",
|
"coverageDirectory": "../coverage",
|
||||||
"testEnvironment": "node"
|
"testEnvironment": "node"
|
||||||
|
},
|
||||||
|
"prisma": {
|
||||||
|
"seed": "ts-node prisma/seed.ts"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export default defineConfig({
|
|||||||
schema: "prisma/schema.prisma",
|
schema: "prisma/schema.prisma",
|
||||||
migrations: {
|
migrations: {
|
||||||
path: "prisma/migrations",
|
path: "prisma/migrations",
|
||||||
|
seed: "npx ts-node prisma/seed.ts",
|
||||||
},
|
},
|
||||||
datasource: {
|
datasource: {
|
||||||
url: process.env["DATABASE_URL"],
|
url: process.env["DATABASE_URL"],
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ generator client {
|
|||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "postgresql"
|
provider = "postgresql"
|
||||||
url = env("DATABASE_URL")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===========================================
|
// ===========================================
|
||||||
@@ -33,24 +32,14 @@ enum AuthProvider {
|
|||||||
FACEBOOK
|
FACEBOOK
|
||||||
}
|
}
|
||||||
|
|
||||||
enum VerificationStatus {
|
|
||||||
PENDING
|
|
||||||
APPROVED
|
|
||||||
REJECTED
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===========================================
|
// ===========================================
|
||||||
// USER & AUTHENTICATION (Milestone 1)
|
// USER - Authentication Only
|
||||||
// ===========================================
|
// ===========================================
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
email String @unique
|
email String @unique
|
||||||
password String? // Null for social login users
|
password String? // Null for social login users
|
||||||
firstName String?
|
|
||||||
lastName String?
|
|
||||||
phone String?
|
|
||||||
avatar String?
|
|
||||||
role UserRole @default(USER)
|
role UserRole @default(USER)
|
||||||
status UserStatus @default(ACTIVE)
|
status UserStatus @default(ACTIVE)
|
||||||
emailVerified Boolean @default(false)
|
emailVerified Boolean @default(false)
|
||||||
@@ -66,10 +55,12 @@ model User {
|
|||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
lastLoginAt DateTime?
|
lastLoginAt DateTime?
|
||||||
|
|
||||||
// Relations (will be added in later milestones)
|
// Relations - Profile based on role
|
||||||
|
userProfile UserProfile?
|
||||||
agentProfile AgentProfile?
|
agentProfile AgentProfile?
|
||||||
|
|
||||||
|
// Auth Relations
|
||||||
sessions Session[]
|
sessions Session[]
|
||||||
passwordResets PasswordReset[]
|
|
||||||
|
|
||||||
@@index([email])
|
@@index([email])
|
||||||
@@index([role])
|
@@index([role])
|
||||||
@@ -77,6 +68,83 @@ model User {
|
|||||||
@@map("users")
|
@@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 {
|
model Session {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
userId String
|
userId String
|
||||||
@@ -93,55 +161,3 @@ model Session {
|
|||||||
@@index([token])
|
@@index([token])
|
||||||
@@map("sessions")
|
@@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 { ConfigService } from '@nestjs/config';
|
||||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||||
import helmet from 'helmet';
|
import helmet from 'helmet';
|
||||||
import * as cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
import { AllExceptionsFilter } from './common/filters';
|
import { AllExceptionsFilter } from './common/filters';
|
||||||
import { TransformInterceptor } from './common/interceptors';
|
import { TransformInterceptor } from './common/interceptors';
|
||||||
|
|||||||
@@ -1,18 +1,29 @@
|
|||||||
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import { PrismaPg } from '@prisma/adapter-pg';
|
||||||
|
import { Pool } from 'pg';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PrismaService
|
export class PrismaService
|
||||||
extends PrismaClient
|
extends PrismaClient
|
||||||
implements OnModuleInit, OnModuleDestroy
|
implements OnModuleInit, OnModuleDestroy
|
||||||
{
|
{
|
||||||
|
private pool: Pool;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
const connectionString = process.env.DATABASE_URL;
|
||||||
|
const pool = new Pool({ connectionString });
|
||||||
|
const adapter = new PrismaPg(pool);
|
||||||
|
|
||||||
super({
|
super({
|
||||||
|
adapter,
|
||||||
log:
|
log:
|
||||||
process.env.NODE_ENV === 'development'
|
process.env.NODE_ENV === 'development'
|
||||||
? ['query', 'info', 'warn', 'error']
|
? ['query', 'info', 'warn', 'error']
|
||||||
: ['error'],
|
: ['error'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.pool = pool;
|
||||||
}
|
}
|
||||||
|
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
@@ -21,6 +32,7 @@ export class PrismaService
|
|||||||
|
|
||||||
async onModuleDestroy() {
|
async onModuleDestroy() {
|
||||||
await this.$disconnect();
|
await this.$disconnect();
|
||||||
|
await this.pool.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
async cleanDatabase() {
|
async cleanDatabase() {
|
||||||
|
|||||||
Reference in New Issue
Block a user