first commit
This commit is contained in:
264
src/models/mongodb/Product.js
Normal file
264
src/models/mongodb/Product.js
Normal file
@@ -0,0 +1,264 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const variantSchema = new mongoose.Schema({
|
||||
size: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
sku: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
price: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 0,
|
||||
},
|
||||
compareAtPrice: {
|
||||
type: Number,
|
||||
min: 0,
|
||||
},
|
||||
inventory: {
|
||||
quantity: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
min: 0,
|
||||
},
|
||||
trackInventory: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
images: [String],
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const productSchema = new mongoose.Schema({
|
||||
// Basic Information
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
slug: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
lowercase: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
shortDescription: {
|
||||
type: String,
|
||||
maxLength: 500,
|
||||
},
|
||||
|
||||
// Categorization
|
||||
category: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
subcategory: String,
|
||||
tags: [String],
|
||||
brand: String,
|
||||
|
||||
// Pricing & Inventory
|
||||
basePrice: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 0,
|
||||
},
|
||||
compareAtPrice: {
|
||||
type: Number,
|
||||
min: 0,
|
||||
},
|
||||
costPrice: {
|
||||
type: Number,
|
||||
min: 0,
|
||||
},
|
||||
|
||||
// Variants
|
||||
variants: [variantSchema],
|
||||
hasVariants: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
// Media
|
||||
images: {
|
||||
primary: String,
|
||||
gallery: [String],
|
||||
videos: [String],
|
||||
},
|
||||
|
||||
// SEO
|
||||
metaTitle: String,
|
||||
metaDescription: String,
|
||||
metaKeywords: [String],
|
||||
|
||||
// Status & Visibility
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['draft', 'active', 'inactive', 'archived'],
|
||||
default: 'draft',
|
||||
},
|
||||
isFeatured: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isDigital: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
// Physical Attributes
|
||||
weight: {
|
||||
value: Number,
|
||||
unit: {
|
||||
type: String,
|
||||
enum: ['g', 'kg', 'lb', 'oz'],
|
||||
default: 'g',
|
||||
},
|
||||
},
|
||||
dimensions: {
|
||||
length: Number,
|
||||
width: Number,
|
||||
height: Number,
|
||||
unit: {
|
||||
type: String,
|
||||
enum: ['cm', 'in'],
|
||||
default: 'cm',
|
||||
},
|
||||
},
|
||||
|
||||
// Analytics
|
||||
viewCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
purchaseCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
|
||||
// AI Generated Tags
|
||||
aiTags: [String],
|
||||
aiGeneratedDescription: String,
|
||||
|
||||
// Timestamps
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
},
|
||||
updatedAt: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
},
|
||||
publishedAt: Date,
|
||||
});
|
||||
|
||||
// Indexes for better performance
|
||||
productSchema.index({ slug: 1 });
|
||||
productSchema.index({ category: 1, status: 1 });
|
||||
productSchema.index({ brand: 1 });
|
||||
productSchema.index({ tags: 1 });
|
||||
productSchema.index({ 'variants.sku': 1 });
|
||||
productSchema.index({ status: 1, isFeatured: 1 });
|
||||
productSchema.index({ createdAt: -1 });
|
||||
|
||||
// Text search index
|
||||
productSchema.index({
|
||||
name: 'text',
|
||||
description: 'text',
|
||||
tags: 'text',
|
||||
brand: 'text',
|
||||
});
|
||||
|
||||
// Virtual for average rating (if implementing ratings)
|
||||
productSchema.virtual('averageRating').get(function() {
|
||||
// This would be calculated from reviews in PostgreSQL
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Pre-save middleware
|
||||
productSchema.pre('save', function(next) {
|
||||
this.updatedAt = new Date();
|
||||
|
||||
// Auto-generate slug if not provided
|
||||
if (!this.slug && this.name) {
|
||||
this.slug = this.name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/(^-|-$)/g, '');
|
||||
}
|
||||
|
||||
// Set published date when status changes to active
|
||||
if (this.isModified('status') && this.status === 'active' && !this.publishedAt) {
|
||||
this.publishedAt = new Date();
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
// Instance methods
|
||||
productSchema.methods.incrementViewCount = function() {
|
||||
this.viewCount += 1;
|
||||
return this.save();
|
||||
};
|
||||
|
||||
productSchema.methods.incrementPurchaseCount = function(quantity = 1) {
|
||||
this.purchaseCount += quantity;
|
||||
return this.save();
|
||||
};
|
||||
|
||||
productSchema.methods.getAvailableVariants = function() {
|
||||
return this.variants.filter(variant =>
|
||||
variant.isActive &&
|
||||
(!variant.inventory.trackInventory || variant.inventory.quantity > 0)
|
||||
);
|
||||
};
|
||||
|
||||
// Static methods
|
||||
productSchema.statics.findBySlug = function(slug) {
|
||||
return this.findOne({ slug, status: 'active' });
|
||||
};
|
||||
|
||||
productSchema.statics.findByCategory = function(category, limit = 20, skip = 0) {
|
||||
return this.find({ category, status: 'active' })
|
||||
.limit(limit)
|
||||
.skip(skip)
|
||||
.sort({ createdAt: -1 });
|
||||
};
|
||||
|
||||
productSchema.statics.searchProducts = function(query, options = {}) {
|
||||
const { category, brand, minPrice, maxPrice, limit = 20, skip = 0 } = options;
|
||||
|
||||
const searchQuery = {
|
||||
$text: { $search: query },
|
||||
status: 'active',
|
||||
};
|
||||
|
||||
if (category) searchQuery.category = category;
|
||||
if (brand) searchQuery.brand = brand;
|
||||
if (minPrice || maxPrice) {
|
||||
searchQuery.basePrice = {};
|
||||
if (minPrice) searchQuery.basePrice.$gte = minPrice;
|
||||
if (maxPrice) searchQuery.basePrice.$lte = maxPrice;
|
||||
}
|
||||
|
||||
return this.find(searchQuery)
|
||||
.limit(limit)
|
||||
.skip(skip)
|
||||
.sort({ score: { $meta: 'textScore' } });
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('Product', productSchema);
|
||||
254
src/models/mongodb/Wardrobe.js
Normal file
254
src/models/mongodb/Wardrobe.js
Normal file
@@ -0,0 +1,254 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const wardrobeItemSchema = new mongoose.Schema({
|
||||
// Basic Information
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
description: String,
|
||||
|
||||
// Category & Type
|
||||
category: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['tops', 'bottoms', 'dresses', 'outerwear', 'shoes', 'accessories', 'other'],
|
||||
},
|
||||
subcategory: String,
|
||||
brand: String,
|
||||
color: String,
|
||||
|
||||
// Images
|
||||
images: [{
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isPrimary: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
uploadedAt: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
},
|
||||
}],
|
||||
|
||||
// AI Generated Tags
|
||||
aiTags: [String],
|
||||
aiColorPalette: [String],
|
||||
aiStyleTags: [String],
|
||||
|
||||
// User Tags
|
||||
userTags: [String],
|
||||
|
||||
// Physical Attributes
|
||||
size: String,
|
||||
material: String,
|
||||
condition: {
|
||||
type: String,
|
||||
enum: ['new', 'like-new', 'good', 'fair', 'poor'],
|
||||
default: 'good',
|
||||
},
|
||||
|
||||
// Status
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
|
||||
// Timestamps
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
},
|
||||
updatedAt: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
},
|
||||
});
|
||||
|
||||
const wardrobeSchema = new mongoose.Schema({
|
||||
userId: {
|
||||
type: String,
|
||||
required: true,
|
||||
ref: 'User', // Reference to PostgreSQL User
|
||||
},
|
||||
|
||||
// Wardrobe Information
|
||||
name: {
|
||||
type: String,
|
||||
default: 'My Wardrobe',
|
||||
},
|
||||
description: String,
|
||||
|
||||
// Items
|
||||
items: [wardrobeItemSchema],
|
||||
|
||||
// Statistics
|
||||
totalItems: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
categoryCounts: {
|
||||
tops: { type: Number, default: 0 },
|
||||
bottoms: { type: Number, default: 0 },
|
||||
dresses: { type: Number, default: 0 },
|
||||
outerwear: { type: Number, default: 0 },
|
||||
shoes: { type: Number, default: 0 },
|
||||
accessories: { type: Number, default: 0 },
|
||||
other: { type: Number, default: 0 },
|
||||
},
|
||||
|
||||
// AI Analysis
|
||||
aiAnalysis: {
|
||||
lastAnalyzed: Date,
|
||||
dominantColors: [String],
|
||||
styleProfile: {
|
||||
casual: Number,
|
||||
formal: Number,
|
||||
trendy: Number,
|
||||
classic: Number,
|
||||
bohemian: Number,
|
||||
minimalist: Number,
|
||||
},
|
||||
recommendations: [String],
|
||||
},
|
||||
|
||||
// Privacy Settings
|
||||
isPublic: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
shareSettings: {
|
||||
allowViewing: Boolean,
|
||||
allowRecommendations: Boolean,
|
||||
},
|
||||
|
||||
// Timestamps
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
},
|
||||
updatedAt: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
},
|
||||
});
|
||||
|
||||
// Indexes
|
||||
wardrobeSchema.index({ userId: 1 });
|
||||
wardrobeSchema.index({ isPublic: 1 });
|
||||
wardrobeSchema.index({ 'items.category': 1 });
|
||||
wardrobeSchema.index({ 'items.aiTags': 1 });
|
||||
|
||||
// Text search index
|
||||
wardrobeSchema.index({
|
||||
name: 'text',
|
||||
description: 'text',
|
||||
'items.name': 'text',
|
||||
'items.brand': 'text',
|
||||
'items.userTags': 'text',
|
||||
});
|
||||
|
||||
// Pre-save middleware
|
||||
wardrobeSchema.pre('save', function(next) {
|
||||
this.updatedAt = new Date();
|
||||
|
||||
// Update statistics
|
||||
this.totalItems = this.items.length;
|
||||
|
||||
// Reset category counts
|
||||
this.categoryCounts = {
|
||||
tops: 0,
|
||||
bottoms: 0,
|
||||
dresses: 0,
|
||||
outerwear: 0,
|
||||
shoes: 0,
|
||||
accessories: 0,
|
||||
other: 0,
|
||||
};
|
||||
|
||||
// Count items by category
|
||||
this.items.forEach(item => {
|
||||
if (this.categoryCounts[item.category] !== undefined) {
|
||||
this.categoryCounts[item.category]++;
|
||||
}
|
||||
});
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
// Instance methods
|
||||
wardrobeSchema.methods.addItem = function(itemData) {
|
||||
this.items.push(itemData);
|
||||
return this.save();
|
||||
};
|
||||
|
||||
wardrobeSchema.methods.removeItem = function(itemId) {
|
||||
this.items = this.items.filter(item => item._id.toString() !== itemId);
|
||||
return this.save();
|
||||
};
|
||||
|
||||
wardrobeSchema.methods.updateItem = function(itemId, updateData) {
|
||||
const item = this.items.id(itemId);
|
||||
if (item) {
|
||||
Object.assign(item, updateData);
|
||||
item.updatedAt = new Date();
|
||||
return this.save();
|
||||
}
|
||||
throw new Error('Item not found');
|
||||
};
|
||||
|
||||
wardrobeSchema.methods.getItemsByCategory = function(category) {
|
||||
return this.items.filter(item => item.category === category && item.isActive);
|
||||
};
|
||||
|
||||
wardrobeSchema.methods.getItemsByTags = function(tags) {
|
||||
return this.items.filter(item =>
|
||||
item.isActive &&
|
||||
tags.some(tag =>
|
||||
item.aiTags.includes(tag) ||
|
||||
item.userTags.includes(tag)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
wardrobeSchema.methods.generateOutfitRecommendations = function() {
|
||||
// This would integrate with the recommendation service
|
||||
const tops = this.getItemsByCategory('tops');
|
||||
const bottoms = this.getItemsByCategory('bottoms');
|
||||
const shoes = this.getItemsByCategory('shoes');
|
||||
const accessories = this.getItemsByCategory('accessories');
|
||||
|
||||
const recommendations = [];
|
||||
|
||||
// Simple outfit combination logic
|
||||
for (let i = 0; i < Math.min(3, tops.length); i++) {
|
||||
for (let j = 0; j < Math.min(3, bottoms.length); j++) {
|
||||
for (let k = 0; k < Math.min(2, shoes.length); k++) {
|
||||
recommendations.push({
|
||||
id: `${tops[i]._id}-${bottoms[j]._id}-${shoes[k]._id}`,
|
||||
items: [tops[i], bottoms[j], shoes[k]],
|
||||
confidence: Math.random(), // This would come from AI analysis
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return recommendations.slice(0, 10); // Limit to 10 recommendations
|
||||
};
|
||||
|
||||
// Static methods
|
||||
wardrobeSchema.statics.findByUserId = function(userId) {
|
||||
return this.findOne({ userId });
|
||||
};
|
||||
|
||||
wardrobeSchema.statics.findPublicWardrobes = function(limit = 20, skip = 0) {
|
||||
return this.find({ isPublic: true })
|
||||
.limit(limit)
|
||||
.skip(skip)
|
||||
.sort({ updatedAt: -1 });
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('Wardrobe', wardrobeSchema);
|
||||
Reference in New Issue
Block a user