📄 models.js
src/models/models.js
// COPY-PASTE INHERITANCE - ANTIPATTERN: Code duplication with subtle differences
// These classes were copy-pasted and modified, but not using actual inheritance
// Each version has bugs that were "fixed" differently
// ============================================================
// BASE USER (The original, full of bugs)
// ============================================================
export class UserV1 {
constructor(data) {
this.id = data.id
this.name = data.name
this.email = data.email
this.password = data.password // Plain text!
this.createdAt = new Date()
}
validate() {
if (!this.name) return false
if (!this.email) return false
if (!this.email.includes('@')) return false // "Validation"
if (!this.password) return false
return true
}
toJSON() {
return {
id: this.id,
name: this.name,
email: this.email,
password: this.password, // Exposes password!
createdAt: this.createdAt,
}
}
checkPassword(password) {
return this.password === password // Plain text comparison!
}
}
// ============================================================
// USER V2 (Copy-pasted, "fixed" some bugs, broke others)
// ============================================================
export class UserV2 {
constructor(data) {
this.id = data.id
this.name = data.name
this.email = data.email
this.password = data.password
this.createdAt = new Date()
this.updatedAt = new Date() // Added this
}
validate() {
if (!this.name) return { valid: false, error: 'Name required' } // Changed return type!
if (!this.email) return { valid: false, error: 'Email required' }
if (!this.email.includes('@')) return { valid: false, error: 'Invalid email' }
if (!this.password) return { valid: false, error: 'Password required' }
if (this.password.length < 6) return { valid: false, error: 'Password too short' }
return { valid: true } // Different return type than V1!
}
toJSON() {
return {
id: this.id,
name: this.name,
email: this.email,
// password: this.password, // Commented out to "fix" security
createdAt: this.createdAt,
updatedAt: this.updatedAt,
}
}
checkPassword(password) {
// "Fixed" to be case-insensitive (terrible idea)
return this.password.toLowerCase() === password.toLowerCase()
}
// New method that V1 doesn't have
getDisplayName() {
return this.name || this.email.split('@')[0]
}
}
// ============================================================
// USER V3 (Copy-pasted from V2, more changes)
// ============================================================
export class UserV3 {
constructor(data) {
this.id = data.id || data._id // Support both
this.name = data.name || data.username || data.displayName // Multiple field names
this.email = data.email || data.emailAddress
this.password = data.password || data.passwordHash || data.pwd
this.createdAt = data.createdAt || new Date()
this.updatedAt = data.updatedAt || new Date()
this.isActive = data.isActive !== false // Defaults to true
this.isAdmin = data.isAdmin === true
}
validate() {
const errors = []
if (!this.name) errors.push('Name required')
if (!this.email) errors.push('Email required')
if (this.email && !this.email.includes('@')) errors.push('Invalid email')
if (!this.password) errors.push('Password required')
if (this.password && this.password.length < 8) errors.push('Password too short') // Changed to 8
// Return format is different again!
return errors.length === 0 ? null : errors
}
toJSON() {
return {
id: this.id,
_id: this.id, // Both formats!
name: this.name,
username: this.name, // Alias
email: this.email,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
isActive: this.isActive,
isAdmin: this.isAdmin,
}
}
checkPassword(password) {
// "Fixed" to trim whitespace (different from V1 and V2)
return this.password.trim() === password.trim()
}
getDisplayName() {
// Slightly different from V2
return this.name || this.email?.split('@')[0] || 'Anonymous'
}
// New method
hasPermission(permission) {
if (this.isAdmin) return true // Admins can do anything
// No actual permission system implemented
return false
}
}
// ============================================================
// ADMIN USER (Copy-pasted from User with admin stuff)
// ============================================================
export class AdminUser {
constructor(data) {
// Copy-pasted but slightly different
this.id = data.id
this.name = data.name
this.email = data.email
this.password = data.password
this.createdAt = new Date()
this.updatedAt = new Date()
this.isAdmin = true // Always true
this.permissions = data.permissions || ['*'] // All permissions
this.superAdmin = data.superAdmin || false
}
validate() {
// Copy-pasted but stricter... or is it?
if (!this.name) return false // Back to V1 format!
if (!this.email) return false
if (!this.password) return false
if (this.password.length < 12) return false // Longer password required... but same check
return true
}
toJSON() {
return {
id: this.id,
name: this.name,
email: this.email,
// Still exposes password sometimes due to bug
isAdmin: this.isAdmin,
permissions: this.permissions,
superAdmin: this.superAdmin,
}
}
checkPassword(password) {
// Same as V1 (copy-paste error?)
return this.password === password
}
hasPermission(permission) {
if (this.superAdmin) return true
if (this.permissions.includes('*')) return true
return this.permissions.includes(permission)
}
}
// ============================================================
// PRODUCT CLASSES (Same pattern - copy-paste with changes)
// ============================================================
export class ProductV1 {
constructor(data) {
this.id = data.id
this.name = data.name
this.price = data.price // Could be number or string!
this.description = data.description
}
getPrice() {
return this.price
}
toJSON() {
return { ...this }
}
}
export class ProductV2 {
constructor(data) {
this.id = data.id
this.name = data.name
this.price = parseFloat(data.price) || 0 // Now always number
this.description = data.description
this.category = data.category // Added field
this.stock = data.stock || 0
}
getPrice() {
return this.price.toFixed(2) // Now returns string!
}
toJSON() {
return {
id: this.id,
name: this.name,
price: this.getPrice(), // Uses method (returns string now)
description: this.description,
category: this.category,
stock: this.stock,
}
}
isInStock() {
return this.stock > 0
}
}
export class ProductV3 {
constructor(data) {
this.id = data.id || data._id
this.name = data.name || data.title
this.price = typeof data.price === 'string'
? parseFloat(data.price.replace(/[$,]/g, ''))
: data.price || 0
this.description = data.description || data.desc
this.category = data.category || data.cat || 'uncategorized'
this.stock = data.stock ?? data.quantity ?? 0
this.active = data.active !== false
}
getPrice() {
// Returns Money object now (breaking change!)
return {
amount: this.price,
currency: 'USD',
formatted: `$${this.price.toFixed(2)}`,
}
}
toJSON() {
return {
id: this.id,
_id: this.id,
name: this.name,
title: this.name,
price: this.price, // Back to raw number
priceFormatted: `$${this.price.toFixed(2)}`,
description: this.description,
category: this.category,
stock: this.stock,
active: this.active,
}
}
isInStock() {
return this.stock > 0 && this.active
}
}
// ============================================================
// FACTORY FUNCTIONS (Different for each version!)
// ============================================================
export function createUser(data, version = 'v3') {
switch (version) {
case 'v1': return new UserV1(data)
case 'v2': return new UserV2(data)
case 'v3': return new UserV3(data)
case 'admin': return new AdminUser(data)
default: return new UserV3(data)
}
}
export function createProduct(data, version = 'v3') {
switch (version) {
case 'v1': return new ProductV1(data)
case 'v2': return new ProductV2(data)
case 'v3': return new ProductV3(data)
default: return new ProductV3(data)
}
}
// ============================================================
// USAGE NIGHTMARE
// ============================================================
// Code in different parts of the codebase uses different versions:
// - Legacy API uses V1
// - Main API uses V2
// - New API uses V3
// - Admin panel uses AdminUser
//
// Validation returns: false, { valid: boolean }, or string[]
// getPrice() returns: number, string, or { amount, currency, formatted }
// toJSON() sometimes includes password, sometimes doesn't
//
// Good luck maintaining this!
export default {
UserV1, UserV2, UserV3, AdminUser,
ProductV1, ProductV2, ProductV3,
createUser, createProduct,
}