192 lines
6.4 KiB
JavaScript
192 lines
6.4 KiB
JavaScript
const Sequelize = require('sequelize');
|
|
const path = require('path');
|
|
const bcrypt = require('bcryptjs');
|
|
|
|
// Initialize database
|
|
const dbPath = process.env.DATABASE_URL || 'sqlite://calorie_tracker.db';
|
|
const storagePath = dbPath.startsWith('sqlite://')
|
|
? dbPath.replace('sqlite://', '')
|
|
: path.join(__dirname, '../data/calorie_tracker.db');
|
|
|
|
// Ensure storage path is absolute or relative to cwd correctly
|
|
// For Docker, we use /app/data/calorie_tracker.db
|
|
const sequelize = new Sequelize({
|
|
dialect: 'sqlite',
|
|
storage: storagePath.startsWith('/') ? storagePath : path.join(process.cwd(), 'data', 'calorie_tracker.db'),
|
|
logging: false
|
|
});
|
|
|
|
const db = {};
|
|
|
|
// User Model
|
|
const User = sequelize.define('User', {
|
|
username: { type: Sequelize.STRING(80), unique: true, allowNull: false },
|
|
password: { type: Sequelize.STRING(200), allowNull: false },
|
|
name: Sequelize.STRING(100),
|
|
age: Sequelize.INTEGER,
|
|
gender: Sequelize.STRING(10),
|
|
height_cm: Sequelize.FLOAT,
|
|
weight_kg: Sequelize.FLOAT,
|
|
activity_level: { type: Sequelize.STRING(20), defaultValue: 'moderate' },
|
|
target_daily_calories: { type: Sequelize.INTEGER, defaultValue: 2000 }
|
|
});
|
|
|
|
// Method to verify password
|
|
User.prototype.validPassword = function(password) {
|
|
return bcrypt.compareSync(password, this.password);
|
|
};
|
|
|
|
// Hook to hash password
|
|
User.beforeCreate(async (user) => {
|
|
if (user.password) {
|
|
user.password = await bcrypt.hash(user.password, 10);
|
|
}
|
|
});
|
|
|
|
// FoodItem Model
|
|
const FoodItem = sequelize.define('FoodItem', {
|
|
name: { type: Sequelize.STRING(200), allowNull: false },
|
|
name_tagalog: Sequelize.STRING(200),
|
|
category: Sequelize.STRING(50),
|
|
calories: { type: Sequelize.FLOAT, allowNull: false },
|
|
protein_g: { type: Sequelize.FLOAT, defaultValue: 0 },
|
|
carbs_g: { type: Sequelize.FLOAT, defaultValue: 0 },
|
|
fat_g: { type: Sequelize.FLOAT, defaultValue: 0 },
|
|
fiber_g: { type: Sequelize.FLOAT, defaultValue: 0 },
|
|
sugar_g: { type: Sequelize.FLOAT, defaultValue: 0 },
|
|
sodium_mg: { type: Sequelize.FLOAT, defaultValue: 0 },
|
|
serving_size_g: { type: Sequelize.FLOAT, defaultValue: 100 },
|
|
serving_description: Sequelize.STRING(100),
|
|
source: { type: Sequelize.STRING(20), defaultValue: 'manual' },
|
|
is_filipino: { type: Sequelize.BOOLEAN, defaultValue: false },
|
|
is_favorite: { type: Sequelize.BOOLEAN, defaultValue: false },
|
|
api_data: Sequelize.TEXT,
|
|
last_updated: { type: Sequelize.DATE, defaultValue: Sequelize.NOW }
|
|
});
|
|
|
|
// Meal Model
|
|
const Meal = sequelize.define('Meal', {
|
|
date: { type: Sequelize.DATEONLY, allowNull: false, defaultValue: Sequelize.NOW },
|
|
meal_type: { type: Sequelize.STRING(20), allowNull: false },
|
|
time: Sequelize.TIME,
|
|
notes: Sequelize.TEXT
|
|
});
|
|
|
|
// MealFood Model
|
|
const MealFood = sequelize.define('MealFood', {
|
|
quantity: { type: Sequelize.FLOAT, allowNull: false, defaultValue: 1.0 },
|
|
quantity_grams: Sequelize.FLOAT,
|
|
calories_consumed: Sequelize.FLOAT,
|
|
protein_consumed: Sequelize.FLOAT,
|
|
carbs_consumed: Sequelize.FLOAT,
|
|
fat_consumed: Sequelize.FLOAT
|
|
});
|
|
|
|
// WaterLog Model
|
|
const WaterLog = sequelize.define('WaterLog', {
|
|
date: { type: Sequelize.DATEONLY, allowNull: false, defaultValue: Sequelize.NOW },
|
|
amount_ml: { type: Sequelize.INTEGER, allowNull: false },
|
|
time: { type: Sequelize.TIME, defaultValue: Sequelize.NOW }
|
|
});
|
|
|
|
// WeightLog Model
|
|
const WeightLog = sequelize.define('WeightLog', {
|
|
date: { type: Sequelize.DATEONLY, allowNull: false, defaultValue: Sequelize.NOW },
|
|
weight_kg: { type: Sequelize.FLOAT, allowNull: false },
|
|
body_fat_percentage: Sequelize.FLOAT,
|
|
notes: Sequelize.TEXT,
|
|
time: { type: Sequelize.TIME, defaultValue: Sequelize.NOW }
|
|
});
|
|
|
|
// MealPlan Model
|
|
const MealPlan = sequelize.define('MealPlan', {
|
|
date: { type: Sequelize.DATEONLY, allowNull: false },
|
|
meal_type: { type: Sequelize.STRING(20), allowNull: false },
|
|
is_completed: { type: Sequelize.BOOLEAN, defaultValue: false },
|
|
notes: Sequelize.TEXT
|
|
});
|
|
|
|
// PlannedFood Model
|
|
const PlannedFood = sequelize.define('PlannedFood', {
|
|
quantity: { type: Sequelize.FLOAT, allowNull: false, defaultValue: 1.0 }
|
|
});
|
|
|
|
// UserGoal Model
|
|
const UserGoal = sequelize.define('UserGoal', {
|
|
goal_type: { type: Sequelize.STRING(20), defaultValue: 'recomp' },
|
|
target_weight_kg: Sequelize.FLOAT,
|
|
weekly_goal_kg: { type: Sequelize.FLOAT, defaultValue: 0.5 },
|
|
target_protein_g: { type: Sequelize.INTEGER, defaultValue: 150 },
|
|
target_carbs_g: { type: Sequelize.INTEGER, defaultValue: 200 },
|
|
target_fat_g: { type: Sequelize.INTEGER, defaultValue: 60 },
|
|
target_water_ml: { type: Sequelize.INTEGER, defaultValue: 2000 }
|
|
});
|
|
|
|
// DailySummary Model
|
|
const DailySummary = sequelize.define('DailySummary', {
|
|
date: { type: Sequelize.DATEONLY, allowNull: false },
|
|
total_calories: { type: Sequelize.FLOAT, defaultValue: 0 },
|
|
total_protein_g: { type: Sequelize.FLOAT, defaultValue: 0 },
|
|
total_carbs_g: { type: Sequelize.FLOAT, defaultValue: 0 },
|
|
total_fat_g: { type: Sequelize.FLOAT, defaultValue: 0 },
|
|
total_water_ml: { type: Sequelize.INTEGER, defaultValue: 0 },
|
|
calories_remaining: Sequelize.FLOAT,
|
|
weight_kg: Sequelize.FLOAT,
|
|
notes: Sequelize.TEXT
|
|
});
|
|
|
|
// APICache Model
|
|
const APICache = sequelize.define('APICache', {
|
|
query: { type: Sequelize.STRING(200), allowNull: false, unique: true },
|
|
api_source: Sequelize.STRING(50),
|
|
response_json: Sequelize.TEXT,
|
|
cached_at: { type: Sequelize.DATE, defaultValue: Sequelize.NOW }
|
|
});
|
|
|
|
// Relationships
|
|
User.hasMany(Meal, { onDelete: 'CASCADE' });
|
|
Meal.belongsTo(User);
|
|
|
|
User.hasMany(WeightLog, { onDelete: 'CASCADE' });
|
|
WeightLog.belongsTo(User);
|
|
|
|
User.hasMany(WaterLog, { onDelete: 'CASCADE' });
|
|
WaterLog.belongsTo(User);
|
|
|
|
User.hasMany(MealPlan, { onDelete: 'CASCADE' });
|
|
MealPlan.belongsTo(User);
|
|
|
|
User.hasOne(UserGoal, { onDelete: 'CASCADE' });
|
|
UserGoal.belongsTo(User);
|
|
|
|
Meal.hasMany(MealFood, { onDelete: 'CASCADE' });
|
|
MealFood.belongsTo(Meal);
|
|
|
|
FoodItem.hasMany(MealFood);
|
|
MealFood.belongsTo(FoodItem);
|
|
|
|
MealPlan.hasMany(PlannedFood, { onDelete: 'CASCADE' });
|
|
PlannedFood.belongsTo(MealPlan);
|
|
|
|
FoodItem.hasMany(PlannedFood);
|
|
PlannedFood.belongsTo(FoodItem);
|
|
|
|
User.hasMany(DailySummary, { onDelete: 'CASCADE' });
|
|
DailySummary.belongsTo(User);
|
|
|
|
db.sequelize = sequelize;
|
|
db.Sequelize = Sequelize;
|
|
db.User = User;
|
|
db.FoodItem = FoodItem;
|
|
db.Meal = Meal;
|
|
db.MealFood = MealFood;
|
|
db.WaterLog = WaterLog;
|
|
db.WeightLog = WeightLog;
|
|
db.MealPlan = MealPlan;
|
|
db.PlannedFood = PlannedFood;
|
|
db.UserGoal = UserGoal;
|
|
db.DailySummary = DailySummary;
|
|
db.APICache = APICache;
|
|
|
|
module.exports = db;
|