feat: complete MongoDB to CouchDB migration and deployment
- Remove all mongoose dependencies from backend - Convert Badge and PointTransaction models to CouchDB - Fix gamificationService for CouchDB architecture - Update Docker registry URLs to use HTTPS (port 443) - Fix ingress configuration for HAProxy - Successfully deploy multi-architecture images - Application fully running on Kubernetes with CouchDB 🤖 Generated with [AI Assistant] Co-Authored-By: AI Assistant <noreply@ai-assistant.com>
This commit is contained in:
@@ -1,57 +1,127 @@
|
||||
const mongoose = require("mongoose");
|
||||
const couchdbService = require("../services/couchdbService");
|
||||
|
||||
const BadgeSchema = new mongoose.Schema(
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
criteria: {
|
||||
type: {
|
||||
type: String,
|
||||
enum: [
|
||||
"street_adoptions",
|
||||
"task_completions",
|
||||
"post_creations",
|
||||
"event_participations",
|
||||
"points_earned",
|
||||
"consecutive_days",
|
||||
"special",
|
||||
],
|
||||
required: true,
|
||||
},
|
||||
threshold: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
rarity: {
|
||||
type: String,
|
||||
enum: ["common", "rare", "epic", "legendary"],
|
||||
default: "common",
|
||||
},
|
||||
order: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
},
|
||||
);
|
||||
class Badge {
|
||||
static async findAll() {
|
||||
try {
|
||||
const result = await couchdbService.find({
|
||||
selector: { type: 'badge' },
|
||||
sort: [{ order: 'asc' }]
|
||||
});
|
||||
return result.docs;
|
||||
} catch (error) {
|
||||
console.error('Error finding badges:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Index for efficient badge queries
|
||||
BadgeSchema.index({ "criteria.type": 1, "criteria.threshold": 1 });
|
||||
BadgeSchema.index({ rarity: 1 });
|
||||
BadgeSchema.index({ order: 1 });
|
||||
static async findById(id) {
|
||||
try {
|
||||
const badge = await couchdbService.get(id);
|
||||
if (badge.type !== 'badge') {
|
||||
return null;
|
||||
}
|
||||
return badge;
|
||||
} catch (error) {
|
||||
if (error.statusCode === 404) {
|
||||
return null;
|
||||
}
|
||||
console.error('Error finding badge by ID:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = mongoose.model("Badge", BadgeSchema);
|
||||
static async create(badgeData) {
|
||||
try {
|
||||
const badge = {
|
||||
_id: `badge_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
type: 'badge',
|
||||
name: badgeData.name,
|
||||
description: badgeData.description,
|
||||
icon: badgeData.icon,
|
||||
criteria: badgeData.criteria,
|
||||
rarity: badgeData.rarity || 'common',
|
||||
order: badgeData.order || 0,
|
||||
isActive: badgeData.isActive !== undefined ? badgeData.isActive : true,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
const result = await couchdbService.insert(badge);
|
||||
return { ...badge, _rev: result.rev };
|
||||
} catch (error) {
|
||||
console.error('Error creating badge:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async update(id, updateData) {
|
||||
try {
|
||||
const existingBadge = await couchdbService.get(id);
|
||||
if (existingBadge.type !== 'badge') {
|
||||
throw new Error('Document is not a badge');
|
||||
}
|
||||
|
||||
const updatedBadge = {
|
||||
...existingBadge,
|
||||
...updateData,
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
const result = await couchdbService.insert(updatedBadge);
|
||||
return { ...updatedBadge, _rev: result.rev };
|
||||
} catch (error) {
|
||||
console.error('Error updating badge:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async delete(id) {
|
||||
try {
|
||||
const badge = await couchdbService.get(id);
|
||||
if (badge.type !== 'badge') {
|
||||
throw new Error('Document is not a badge');
|
||||
}
|
||||
|
||||
await couchdbService.destroy(id, badge._rev);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error deleting badge:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async findByCriteria(criteriaType, threshold) {
|
||||
try {
|
||||
const result = await couchdbService.find({
|
||||
selector: {
|
||||
type: 'badge',
|
||||
'criteria.type': criteriaType,
|
||||
'criteria.threshold': { $lte: threshold }
|
||||
},
|
||||
sort: [{ 'criteria.threshold': 'desc' }]
|
||||
});
|
||||
return result.docs;
|
||||
} catch (error) {
|
||||
console.error('Error finding badges by criteria:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async findByRarity(rarity) {
|
||||
try {
|
||||
const result = await couchdbService.find({
|
||||
selector: {
|
||||
type: 'badge',
|
||||
rarity: rarity
|
||||
},
|
||||
sort: [{ order: 'asc' }]
|
||||
});
|
||||
return result.docs;
|
||||
} catch (error) {
|
||||
console.error('Error finding badges by rarity:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Badge;
|
||||
@@ -1,57 +1,165 @@
|
||||
const mongoose = require("mongoose");
|
||||
const couchdbService = require("../services/couchdbService");
|
||||
|
||||
const PointTransactionSchema = new mongoose.Schema(
|
||||
{
|
||||
user: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "User",
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
amount: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: [
|
||||
"street_adoption",
|
||||
"task_completion",
|
||||
"post_creation",
|
||||
"event_participation",
|
||||
"reward_redemption",
|
||||
"admin_adjustment",
|
||||
],
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
relatedEntity: {
|
||||
entityType: {
|
||||
type: String,
|
||||
enum: ["Street", "Task", "Post", "Event", "Reward"],
|
||||
},
|
||||
entityId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
},
|
||||
},
|
||||
balanceAfter: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
},
|
||||
);
|
||||
class PointTransaction {
|
||||
static async create(transactionData) {
|
||||
try {
|
||||
const transaction = {
|
||||
_id: `point_transaction_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
type: 'point_transaction',
|
||||
user: transactionData.user,
|
||||
amount: transactionData.amount,
|
||||
transactionType: transactionData.transactionType,
|
||||
description: transactionData.description,
|
||||
relatedEntity: transactionData.relatedEntity || null,
|
||||
balanceAfter: transactionData.balanceAfter,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Compound index for user transaction history queries
|
||||
PointTransactionSchema.index({ user: 1, createdAt: -1 });
|
||||
const result = await couchdbService.insert(transaction);
|
||||
return { ...transaction, _rev: result.rev };
|
||||
} catch (error) {
|
||||
console.error('Error creating point transaction:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Index for querying by transaction type
|
||||
PointTransactionSchema.index({ type: 1, createdAt: -1 });
|
||||
static async findByUser(userId, limit = 50, skip = 0) {
|
||||
try {
|
||||
const result = await couchdbService.find({
|
||||
selector: {
|
||||
type: 'point_transaction',
|
||||
user: userId
|
||||
},
|
||||
sort: [{ createdAt: 'desc' }],
|
||||
limit: limit,
|
||||
skip: skip
|
||||
});
|
||||
return result.docs;
|
||||
} catch (error) {
|
||||
console.error('Error finding point transactions by user:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = mongoose.model("PointTransaction", PointTransactionSchema);
|
||||
static async findByType(transactionType, limit = 50, skip = 0) {
|
||||
try {
|
||||
const result = await couchdbService.find({
|
||||
selector: {
|
||||
type: 'point_transaction',
|
||||
transactionType: transactionType
|
||||
},
|
||||
sort: [{ createdAt: 'desc' }],
|
||||
limit: limit,
|
||||
skip: skip
|
||||
});
|
||||
return result.docs;
|
||||
} catch (error) {
|
||||
console.error('Error finding point transactions by type:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async findById(id) {
|
||||
try {
|
||||
const transaction = await couchdbService.get(id);
|
||||
if (transaction.type !== 'point_transaction') {
|
||||
return null;
|
||||
}
|
||||
return transaction;
|
||||
} catch (error) {
|
||||
if (error.statusCode === 404) {
|
||||
return null;
|
||||
}
|
||||
console.error('Error finding point transaction by ID:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async getUserBalance(userId) {
|
||||
try {
|
||||
// Get the most recent transaction for the user to find current balance
|
||||
const result = await couchdbService.find({
|
||||
selector: {
|
||||
type: 'point_transaction',
|
||||
user: userId
|
||||
},
|
||||
sort: [{ createdAt: 'desc' }],
|
||||
limit: 1
|
||||
});
|
||||
|
||||
if (result.docs.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return result.docs[0].balanceAfter;
|
||||
} catch (error) {
|
||||
console.error('Error getting user balance:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async getUserTransactionHistory(userId, startDate, endDate) {
|
||||
try {
|
||||
const selector = {
|
||||
type: 'point_transaction',
|
||||
user: userId
|
||||
};
|
||||
|
||||
if (startDate || endDate) {
|
||||
selector.createdAt = {};
|
||||
if (startDate) {
|
||||
selector.createdAt.$gte = startDate;
|
||||
}
|
||||
if (endDate) {
|
||||
selector.createdAt.$lte = endDate;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await couchdbService.find({
|
||||
selector: selector,
|
||||
sort: [{ createdAt: 'desc' }]
|
||||
});
|
||||
|
||||
return result.docs;
|
||||
} catch (error) {
|
||||
console.error('Error getting user transaction history:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async getTransactionStats(userId, startDate, endDate) {
|
||||
try {
|
||||
const transactions = await this.getUserTransactionHistory(userId, startDate, endDate);
|
||||
|
||||
const stats = {
|
||||
totalEarned: 0,
|
||||
totalSpent: 0,
|
||||
transactionCount: transactions.length,
|
||||
transactionBreakdown: {}
|
||||
};
|
||||
|
||||
transactions.forEach(transaction => {
|
||||
if (transaction.amount > 0) {
|
||||
stats.totalEarned += transaction.amount;
|
||||
} else {
|
||||
stats.totalSpent += Math.abs(transaction.amount);
|
||||
}
|
||||
|
||||
const type = transaction.transactionType;
|
||||
if (!stats.transactionBreakdown[type]) {
|
||||
stats.transactionBreakdown[type] = { count: 0, total: 0 };
|
||||
}
|
||||
stats.transactionBreakdown[type].count++;
|
||||
stats.transactionBreakdown[type].total += transaction.amount;
|
||||
});
|
||||
|
||||
return stats;
|
||||
} catch (error) {
|
||||
console.error('Error getting transaction stats:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PointTransaction;
|
||||
Reference in New Issue
Block a user