344 lines
No EOL
9.4 KiB
TypeScript
344 lines
No EOL
9.4 KiB
TypeScript
import { v4 as uuidv4 } from 'uuid'
|
|
import { vezaFaker } from '../utils/faker-config'
|
|
import { globalConfig } from '../config'
|
|
import type {
|
|
User,
|
|
UserRole,
|
|
UserStatus
|
|
} from '../schemas/database'
|
|
|
|
/**
|
|
* User generation options
|
|
*/
|
|
export interface UserGenerationOptions {
|
|
role?: UserRole
|
|
status?: UserStatus
|
|
isVerified?: boolean
|
|
emailVerified?: boolean
|
|
withAvatar?: boolean
|
|
withBio?: boolean
|
|
withStats?: boolean
|
|
createdAt?: Date
|
|
lastSeenAt?: Date
|
|
}
|
|
|
|
/**
|
|
* User relationship data
|
|
*/
|
|
export interface UserRelations {
|
|
followers: string[]
|
|
following: string[]
|
|
blockedUsers: string[]
|
|
mutedUsers: string[]
|
|
}
|
|
|
|
/**
|
|
* Advanced user generator for Veza Platform
|
|
*/
|
|
export class UserGenerator {
|
|
private static generatedUsers: Map<string, User> = new Map()
|
|
private static usedUsernames: Set<string> = new Set()
|
|
private static usedEmails: Set<string> = new Set()
|
|
|
|
/**
|
|
* Generate a single user with realistic data
|
|
*/
|
|
static generate(options: UserGenerationOptions = {}): User {
|
|
const createdAt = options.createdAt || vezaFaker.date.userCreation()
|
|
const firstName = vezaFaker.person.firstName()
|
|
const lastName = vezaFaker.person.lastName()
|
|
|
|
// Ensure unique username and email
|
|
let username = vezaFaker.user.username(firstName, lastName)
|
|
let attempts = 0
|
|
while (this.usedUsernames.has(username) && attempts < 10) {
|
|
username = vezaFaker.user.username(firstName, lastName)
|
|
attempts++
|
|
}
|
|
this.usedUsernames.add(username)
|
|
|
|
let email = `${username}@${vezaFaker.internet.domainName()}`
|
|
attempts = 0
|
|
while (this.usedEmails.has(email) && attempts < 10) {
|
|
email = `${username}${vezaFaker.number.int({ min: 1, max: 999 })}@${vezaFaker.internet.domainName()}`
|
|
attempts++
|
|
}
|
|
this.usedEmails.add(email)
|
|
|
|
const role = options.role || this.generateWeightedRole()
|
|
const status = options.status || 'active'
|
|
const isVerified = options.isVerified ?? this.shouldBeVerified(role)
|
|
const stats = options.withStats !== false ? vezaFaker.stats.userStats(role, createdAt) : {
|
|
tracksUploaded: 0,
|
|
playlistsCreated: 0,
|
|
followersCount: 0,
|
|
followingCount: 0,
|
|
totalPlays: 0,
|
|
}
|
|
|
|
const user: User = {
|
|
id: uuidv4(),
|
|
username,
|
|
email,
|
|
firstName,
|
|
lastName,
|
|
displayName: options.withBio !== false ? `${firstName} ${lastName}` : undefined,
|
|
role,
|
|
status,
|
|
avatar: options.withAvatar !== false ? vezaFaker.image.avatar() : undefined,
|
|
bio: options.withBio !== false ? vezaFaker.user.bio(role) : undefined,
|
|
location: vezaFaker.user.location(),
|
|
website: role === 'artist' || role === 'producer' ? vezaFaker.internet.url() : undefined,
|
|
isVerified,
|
|
emailVerified: options.emailVerified ?? vezaFaker.datatype.boolean({ probability: 0.8 }),
|
|
preferences: {
|
|
language: vezaFaker.helpers.arrayElement(['fr', 'en']),
|
|
theme: vezaFaker.helpers.arrayElement(['light', 'dark', 'system']),
|
|
notifications: {
|
|
email: vezaFaker.datatype.boolean({ probability: 0.7 }),
|
|
push: vezaFaker.datatype.boolean({ probability: 0.8 }),
|
|
desktop: vezaFaker.datatype.boolean({ probability: 0.3 }),
|
|
},
|
|
privacy: {
|
|
profileVisibility: vezaFaker.helpers.arrayElement(['public', 'friends', 'private']),
|
|
showOnlineStatus: vezaFaker.datatype.boolean({ probability: 0.6 }),
|
|
allowDirectMessages: vezaFaker.datatype.boolean({ probability: 0.9 }),
|
|
},
|
|
},
|
|
stats,
|
|
lastSeenAt: options.lastSeenAt || (status === 'active' ? vezaFaker.date.recentActivity() : undefined),
|
|
createdAt,
|
|
updatedAt: vezaFaker.date.between({ from: createdAt, to: new Date() }),
|
|
}
|
|
|
|
this.generatedUsers.set(user.id, user)
|
|
return user
|
|
}
|
|
|
|
/**
|
|
* Generate multiple users with distribution
|
|
*/
|
|
static generateBatch(count: number, options: UserGenerationOptions = {}): User[] {
|
|
const users: User[] = []
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
users.push(this.generate(options))
|
|
}
|
|
|
|
return users
|
|
}
|
|
|
|
/**
|
|
* Generate users with role distribution
|
|
*/
|
|
static generateWithDistribution(): User[] {
|
|
const config = globalConfig.generation.users
|
|
const users: User[] = []
|
|
|
|
// Generate admins
|
|
for (let i = 0; i < config.adminCount; i++) {
|
|
users.push(this.generate({
|
|
role: 'admin',
|
|
isVerified: true,
|
|
emailVerified: true,
|
|
withAvatar: true,
|
|
withBio: true,
|
|
status: 'active'
|
|
}))
|
|
}
|
|
|
|
// Generate artists
|
|
for (let i = 0; i < config.artistCount; i++) {
|
|
users.push(this.generate({
|
|
role: 'artist',
|
|
isVerified: vezaFaker.datatype.boolean({ probability: 0.6 }),
|
|
withAvatar: true,
|
|
withBio: true,
|
|
}))
|
|
}
|
|
|
|
// Generate producers
|
|
for (let i = 0; i < config.producerCount; i++) {
|
|
users.push(this.generate({
|
|
role: 'producer',
|
|
isVerified: vezaFaker.datatype.boolean({ probability: 0.7 }),
|
|
withAvatar: true,
|
|
withBio: true,
|
|
}))
|
|
}
|
|
|
|
// Generate moderators (small number)
|
|
const moderatorCount = Math.max(1, Math.floor(config.count * 0.02))
|
|
for (let i = 0; i < moderatorCount; i++) {
|
|
users.push(this.generate({
|
|
role: 'moderator',
|
|
isVerified: true,
|
|
emailVerified: true,
|
|
withAvatar: true,
|
|
withBio: true,
|
|
status: 'active'
|
|
}))
|
|
}
|
|
|
|
// Generate regular users
|
|
const regularUserCount = config.count - users.length
|
|
for (let i = 0; i < regularUserCount; i++) {
|
|
users.push(this.generate({
|
|
role: 'user',
|
|
withAvatar: vezaFaker.datatype.boolean({ probability: 0.4 }),
|
|
withBio: vezaFaker.datatype.boolean({ probability: 0.3 }),
|
|
}))
|
|
}
|
|
|
|
return users
|
|
}
|
|
|
|
/**
|
|
* Generate specific user types
|
|
*/
|
|
static generateAdmin(options: Partial<UserGenerationOptions> = {}): User {
|
|
return this.generate({
|
|
...options,
|
|
role: 'admin',
|
|
status: 'active',
|
|
isVerified: true,
|
|
emailVerified: true,
|
|
withAvatar: true,
|
|
withBio: true,
|
|
})
|
|
}
|
|
|
|
static generateArtist(options: Partial<UserGenerationOptions> = {}): User {
|
|
return this.generate({
|
|
...options,
|
|
role: 'artist',
|
|
isVerified: vezaFaker.datatype.boolean({ probability: 0.6 }),
|
|
withAvatar: true,
|
|
withBio: true,
|
|
})
|
|
}
|
|
|
|
static generateProducer(options: Partial<UserGenerationOptions> = {}): User {
|
|
return this.generate({
|
|
...options,
|
|
role: 'producer',
|
|
isVerified: vezaFaker.datatype.boolean({ probability: 0.7 }),
|
|
withAvatar: true,
|
|
withBio: true,
|
|
})
|
|
}
|
|
|
|
static generateTestUser(username?: string): User {
|
|
return this.generate({
|
|
role: 'user',
|
|
status: 'active',
|
|
isVerified: false,
|
|
emailVerified: true,
|
|
withAvatar: false,
|
|
withBio: false,
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Generate user relationships
|
|
*/
|
|
static generateRelationships(users: User[]): Map<string, UserRelations> {
|
|
const relationships = new Map<string, UserRelations>()
|
|
|
|
users.forEach(user => {
|
|
const maxFollowing = Math.min(user.stats.followingCount, users.length - 1)
|
|
const maxFollowers = Math.min(user.stats.followersCount, users.length - 1)
|
|
|
|
// Generate following relationships
|
|
const following = vezaFaker.helpers.arrayElements(
|
|
users.filter(u => u.id !== user.id).map(u => u.id),
|
|
Math.min(maxFollowing, 50)
|
|
)
|
|
|
|
// Generate followers (some overlap with following)
|
|
const potentialFollowers = users
|
|
.filter(u => u.id !== user.id && !following.includes(u.id))
|
|
.map(u => u.id)
|
|
|
|
const additionalFollowers = vezaFaker.helpers.arrayElements(
|
|
potentialFollowers,
|
|
Math.min(maxFollowers - following.length, potentialFollowers.length)
|
|
)
|
|
|
|
const followers = [...following.slice(0, Math.floor(following.length * 0.3)), ...additionalFollowers]
|
|
|
|
relationships.set(user.id, {
|
|
followers,
|
|
following,
|
|
blockedUsers: [],
|
|
mutedUsers: []
|
|
})
|
|
})
|
|
|
|
return relationships
|
|
}
|
|
|
|
/**
|
|
* Private helper methods
|
|
*/
|
|
private static generateWeightedRole(): UserRole {
|
|
return vezaFaker.utils.weightedChoice([
|
|
{ item: 'user', weight: 70 },
|
|
{ item: 'artist', weight: 20 },
|
|
{ item: 'producer', weight: 8 },
|
|
{ item: 'moderator', weight: 1.5 },
|
|
{ item: 'admin', weight: 0.5 }
|
|
])
|
|
}
|
|
|
|
private static shouldBeVerified(role: UserRole): boolean {
|
|
const verificationProbability = {
|
|
admin: 1.0,
|
|
moderator: 1.0,
|
|
producer: 0.7,
|
|
artist: 0.6,
|
|
user: 0.1
|
|
}
|
|
|
|
return vezaFaker.datatype.boolean({
|
|
probability: verificationProbability[role]
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Utility methods
|
|
*/
|
|
static getGeneratedUser(id: string): User | undefined {
|
|
return this.generatedUsers.get(id)
|
|
}
|
|
|
|
static getAllGeneratedUsers(): User[] {
|
|
return Array.from(this.generatedUsers.values())
|
|
}
|
|
|
|
static clearCache(): void {
|
|
this.generatedUsers.clear()
|
|
this.usedUsernames.clear()
|
|
this.usedEmails.clear()
|
|
}
|
|
|
|
static getStats(): { totalUsers: number; byRole: Record<UserRole, number> } {
|
|
const users = this.getAllGeneratedUsers()
|
|
const byRole: Record<UserRole, number> = {
|
|
admin: 0,
|
|
moderator: 0,
|
|
producer: 0,
|
|
artist: 0,
|
|
user: 0
|
|
}
|
|
|
|
users.forEach(user => {
|
|
byRole[user.role]++
|
|
})
|
|
|
|
return {
|
|
totalUsers: users.length,
|
|
byRole
|
|
}
|
|
}
|
|
} |