/** * Account model representing a user account */ class Account { /** * Create a new Account instance * @param {Object} data Initial account data */ constructor(data = {}) { this.profileId = data.profileId || null; this.userId = data.userId || null; this.username = data.username || null; this.nickname = data.nickname || null; this.name = data.name || null; this.email = data.email || null; this.password = data.password || null; this.ticket = data.ticket || null; this.avatar = data.avatar || null; this.country = data.country || null; this.platformId = data.platformId || null; this.alias = data.alias || null; this.aliasGender = data.aliasGender || null; this.jdPoints = data.jdPoints || 0; this.portraitBorder = data.portraitBorder || null; this.rank = data.rank || 0; this.scores = data.scores || {}; // Map of mapName to score data this.favorites = data.favorites || {}; // User's favorite maps this.songsPlayed = data.songsPlayed || []; // Array of map names this.progression = data.progression || {}; this.history = data.history || {}; // Example: {"MapName": playCount} // New fields from extended JSON structures (assuming these were added from previous step) this.skin = data.skin || null; this.diamondPoints = data.diamondPoints || 0; this.unlockedAvatars = data.unlockedAvatars || []; this.unlockedSkins = data.unlockedSkins || []; this.unlockedAliases = data.unlockedAliases || []; this.unlockedPortraitBorders = data.unlockedPortraitBorders || []; this.wdfRank = data.wdfRank || 0; this.stars = data.stars || 0; this.unlocks = data.unlocks || 0; this.populations = data.populations || []; this.inProgressAliases = data.inProgressAliases || []; this.language = data.language || null; this.firstPartyEnv = data.firstPartyEnv || null; this.syncVersions = data.syncVersions || {}; this.otherPids = data.otherPids || []; this.stats = data.stats || {}; this.mapHistory = data.mapHistory || { classic: [], kids: [] }; this.createdAt = data.createdAt || new Date().toISOString(); this.updatedAt = data.updatedAt || new Date().toISOString(); } /** * Update account properties * @param {Object} data Data to update * @returns {Account} Updated account instance */ update(data) { // Helper: Check if a value is a plain object const _isObject = (item) => { return item && typeof item === 'object' && !Array.isArray(item); }; // Helper: Deeply merge source object's properties into target object const _deepMergeObjects = (target, source) => { const output = { ...target }; for (const key in source) { if (source.hasOwnProperty(key)) { const sourceVal = source[key]; const targetVal = output[key]; if (_isObject(sourceVal)) { if (_isObject(targetVal)) { output[key] = _deepMergeObjects(targetVal, sourceVal); } else { // If target's property is not an object, or doesn't exist, // clone the source object property. output[key] = _deepMergeObjects({}, sourceVal); } } else { // For non-object properties (primitives, arrays), source overwrites target. // Specific array merging is handled in the main update loop. output[key] = sourceVal; } } } return output; }; const simpleOverwriteKeys = [ 'profileId', 'userId', 'username', 'nickname', 'name', 'email', 'password', 'avatar', 'country', 'platformId', 'alias', 'aliasGender', 'jdPoints', 'portraitBorder', 'rank', 'skin', 'diamondPoints', 'wdfRank', 'stars', 'unlocks', 'language', 'firstPartyEnv' ]; const deepMergeObjectKeys = [ 'scores', 'progression', 'history', 'favorites', 'stats', 'syncVersions' ]; const unionArrayKeys = [ // Arrays of unique primitive values 'unlockedAvatars', 'unlockedSkins', 'unlockedAliases', 'unlockedPortraitBorders', 'otherPids', 'songsPlayed' ]; for (const key in data) { if (data.hasOwnProperty(key)) { const incomingValue = data[key]; if (simpleOverwriteKeys.includes(key)) { this[key] = incomingValue; } else if (deepMergeObjectKeys.includes(key)) { if (_isObject(incomingValue)) { this[key] = _deepMergeObjects(this[key] || {}, incomingValue); } else { // If incoming data for a deep-merge key is not an object, overwrite. this[key] = incomingValue; } } else if (unionArrayKeys.includes(key)) { if (Array.isArray(incomingValue)) { this[key] = [...new Set([...(this[key] || []), ...incomingValue])]; } else { // If incoming data for a union-array key is not an array, overwrite. this[key] = incomingValue; } } else if (key === 'populations') { if (Array.isArray(incomingValue)) { const existingItems = this.populations || []; const mergedItems = [...existingItems]; incomingValue.forEach(newItem => { const index = mergedItems.findIndex(ep => ep.subject === newItem.subject && ep.spaceId === newItem.spaceId); if (index !== -1) mergedItems[index] = _deepMergeObjects(mergedItems[index], newItem); else mergedItems.push(newItem); }); this.populations = mergedItems; } else this.populations = incomingValue; } else if (key === 'inProgressAliases') { if (Array.isArray(incomingValue)) { const existingItems = this.inProgressAliases || []; const mergedItems = [...existingItems]; incomingValue.forEach(newItem => { const index = mergedItems.findIndex(ea => ea.id === newItem.id); if (index !== -1) mergedItems[index] = _deepMergeObjects(mergedItems[index], newItem); else mergedItems.push(newItem); }); this.inProgressAliases = mergedItems; } else this.inProgressAliases = incomingValue; } else if (key === 'mapHistory') { if (_isObject(incomingValue)) { const currentMapHistory = this.mapHistory || { classic: [], kids: [] }; this.mapHistory = { classic: [...new Set([...(currentMapHistory.classic || []), ...(incomingValue.classic || [])])], kids: [...new Set([...(currentMapHistory.kids || []), ...(incomingValue.kids || [])])] }; } else this.mapHistory = incomingValue; } else if (this.hasOwnProperty(key)) { // Default for other existing properties not specially handled: overwrite this[key] = incomingValue; } } } this.updatedAt = new Date().toISOString(); return this; } /** * Add or update a score for a specific map * @param {string} mapName The map name * @param {Object} scoreData Score data to save */ updateScore(mapName, scoreData) { if (!this.scores) { this.scores = {}; } this.scores[mapName] = { ...this.scores[mapName], ...scoreData, updatedAt: new Date().toISOString() }; this.updatedAt = new Date().toISOString(); } /** * Add a map to favorites * @param {string} mapName The map name to favorite */ addFavorite(mapName) { if (!this.favorites) { this.favorites = {}; } this.favorites[mapName] = { addedAt: new Date().toISOString() }; this.updatedAt = new Date().toISOString(); } /** * Remove a map from favorites * @param {string} mapName The map name to remove from favorites */ removeFavorite(mapName) { if (this.favorites && this.favorites[mapName]) { delete this.favorites[mapName]; this.updatedAt = new Date().toISOString(); } } /** * Convert to plain object for storage * @returns {Object} Plain object representation */ toJSON() { const data = { ...this }; delete data.ticket; return data; } /** * Convert to plain object for public API responses, excluding sensitive data. * @returns {Object} Sanitized plain object representation */ toPublicJSON() { const publicData = this.toJSON(); // Explicitly remove sensitive fields if they were somehow added delete publicData.email; delete publicData.password; delete publicData.ticket; return publicData; } } module.exports = Account;