feat: implement backend API with models and seed data for OctoFit application

This commit is contained in:
2026-06-20 01:49:15 +00:00
parent 39ef098873
commit f8c4a57e78
17 changed files with 669 additions and 5 deletions
+93
View File
@@ -0,0 +1,93 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = __importDefault(require("express"));
const mongoose_1 = __importDefault(require("mongoose"));
const Activity_1 = require("./models/Activity");
const Leaderboard_1 = require("./models/Leaderboard");
const Team_1 = require("./models/Team");
const User_1 = require("./models/User");
const Workout_1 = require("./models/Workout");
const app = (0, express_1.default)();
const port = Number(process.env.PORT) || 8000;
const mongoUri = process.env.MONGO_URI || "mongodb://127.0.0.1:27017/octofit_db";
const codespaceName = process.env.CODESPACE_NAME;
const baseUrl = codespaceName
? `https://${codespaceName}-8000.app.github.dev`
: `http://localhost:${port}`;
app.use(express_1.default.json());
app.get("/health", (_req, res) => {
res.status(200).json({ status: "ok", baseUrl });
});
app.get("/api/users/", async (_req, res) => {
try {
const items = await User_1.UserModel.find().sort({ createdAt: -1 }).lean();
res.status(200).json({ resource: "users", count: items.length, items });
}
catch (error) {
res.status(500).json({ message: "Failed to fetch users", error });
}
});
app.get("/api/teams/", async (_req, res) => {
try {
const items = await Team_1.TeamModel.find()
.populate("captain", "username email")
.populate("members", "username email")
.sort({ createdAt: -1 })
.lean();
res.status(200).json({ resource: "teams", count: items.length, items });
}
catch (error) {
res.status(500).json({ message: "Failed to fetch teams", error });
}
});
app.get("/api/activities/", async (_req, res) => {
try {
const items = await Activity_1.ActivityModel.find()
.populate("user", "username email")
.populate("team", "name city")
.sort({ completedAt: -1 })
.lean();
res.status(200).json({ resource: "activities", count: items.length, items });
}
catch (error) {
res.status(500).json({ message: "Failed to fetch activities", error });
}
});
app.get("/api/leaderboard/", async (_req, res) => {
try {
const items = await Leaderboard_1.LeaderboardModel.find()
.populate("entries.user", "username email")
.populate("entries.team", "name city")
.sort({ generatedAt: -1 })
.lean();
res.status(200).json({ resource: "leaderboard", count: items.length, items });
}
catch (error) {
res.status(500).json({ message: "Failed to fetch leaderboard", error });
}
});
app.get("/api/workouts/", async (_req, res) => {
try {
const items = await Workout_1.WorkoutModel.find().sort({ createdAt: -1 }).lean();
res.status(200).json({ resource: "workouts", count: items.length, items });
}
catch (error) {
res.status(500).json({ message: "Failed to fetch workouts", error });
}
});
const start = async () => {
try {
await mongoose_1.default.connect(mongoUri);
app.listen(port, () => {
console.log(`OctoFit backend listening on ${baseUrl}`);
});
}
catch (error) {
console.error("Failed to start backend:", error);
process.exit(1);
}
};
void start();
+15
View File
@@ -0,0 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ActivityModel = void 0;
const mongoose_1 = require("mongoose");
const activitySchema = new mongoose_1.Schema({
user: { type: mongoose_1.Types.ObjectId, ref: "User", required: true },
team: { type: mongoose_1.Types.ObjectId, ref: "Team", required: true },
type: { type: String, required: true, trim: true },
durationMinutes: { type: Number, required: true, min: 1 },
caloriesBurned: { type: Number, required: true, min: 1 },
completedAt: { type: Date, required: true },
}, {
timestamps: true,
});
exports.ActivityModel = (0, mongoose_1.model)("Activity", activitySchema);
+18
View File
@@ -0,0 +1,18 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LeaderboardModel = void 0;
const mongoose_1 = require("mongoose");
const leaderboardEntrySchema = new mongoose_1.Schema({
user: { type: mongoose_1.Types.ObjectId, ref: "User", required: true },
team: { type: mongoose_1.Types.ObjectId, ref: "Team", required: true },
points: { type: Number, required: true, min: 0 },
rank: { type: Number, required: true, min: 1 },
}, { _id: false });
const leaderboardSchema = new mongoose_1.Schema({
period: { type: String, required: true, trim: true },
generatedAt: { type: Date, required: true },
entries: { type: [leaderboardEntrySchema], required: true },
}, {
timestamps: true,
});
exports.LeaderboardModel = (0, mongoose_1.model)("Leaderboard", leaderboardSchema);
+13
View File
@@ -0,0 +1,13 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TeamModel = void 0;
const mongoose_1 = require("mongoose");
const teamSchema = new mongoose_1.Schema({
name: { type: String, required: true, trim: true },
city: { type: String, required: true, trim: true },
captain: { type: mongoose_1.Types.ObjectId, ref: "User", required: true },
members: [{ type: mongoose_1.Types.ObjectId, ref: "User", required: true }],
}, {
timestamps: true,
});
exports.TeamModel = (0, mongoose_1.model)("Team", teamSchema);
+15
View File
@@ -0,0 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.UserModel = void 0;
const mongoose_1 = require("mongoose");
const userSchema = new mongoose_1.Schema({
username: { type: String, required: true, trim: true },
email: { type: String, required: true, unique: true, lowercase: true, trim: true },
age: { type: Number, required: true, min: 13 },
heightCm: { type: Number, required: true, min: 100 },
weightKg: { type: Number, required: true, min: 30 },
fitnessGoal: { type: String, required: true, trim: true },
}, {
timestamps: true,
});
exports.UserModel = (0, mongoose_1.model)("User", userSchema);
+15
View File
@@ -0,0 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WorkoutModel = void 0;
const mongoose_1 = require("mongoose");
const workoutSchema = new mongoose_1.Schema({
title: { type: String, required: true, trim: true },
category: { type: String, required: true, trim: true },
difficulty: { type: String, required: true, trim: true },
durationMinutes: { type: Number, required: true, min: 1 },
equipment: { type: [String], required: true },
targetMuscles: { type: [String], required: true },
}, {
timestamps: true,
});
exports.WorkoutModel = (0, mongoose_1.model)("Workout", workoutSchema);
+161
View File
@@ -0,0 +1,161 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const mongoose_1 = __importDefault(require("mongoose"));
const Activity_1 = require("../models/Activity");
const Leaderboard_1 = require("../models/Leaderboard");
const Team_1 = require("../models/Team");
const User_1 = require("../models/User");
const Workout_1 = require("../models/Workout");
const mongoUri = process.env.MONGO_URI || "mongodb://127.0.0.1:27017/octofit_db";
const seed = async () => {
try {
await mongoose_1.default.connect(mongoUri);
console.log("Seed the octofit_db database with test data");
await Promise.all([
Activity_1.ActivityModel.deleteMany({}),
Leaderboard_1.LeaderboardModel.deleteMany({}),
Team_1.TeamModel.deleteMany({}),
User_1.UserModel.deleteMany({}),
Workout_1.WorkoutModel.deleteMany({}),
]);
const users = await User_1.UserModel.insertMany([
{
username: "alex_runner",
email: "alex.runner@example.com",
age: 29,
heightCm: 178,
weightKg: 74,
fitnessGoal: "Half marathon in under 1h40",
},
{
username: "maya_lifter",
email: "maya.lifter@example.com",
age: 33,
heightCm: 165,
weightKg: 62,
fitnessGoal: "Increase deadlift max to 140kg",
},
{
username: "liam_hiker",
email: "liam.hiker@example.com",
age: 26,
heightCm: 182,
weightKg: 79,
fitnessGoal: "Improve endurance for alpine trekking",
},
{
username: "sofia_cycle",
email: "sofia.cycle@example.com",
age: 31,
heightCm: 170,
weightKg: 66,
fitnessGoal: "Ride a 100km gran fondo",
},
]);
const teams = await Team_1.TeamModel.insertMany([
{
name: "Pulse Pacers",
city: "Cluj-Napoca",
captain: users[0]._id,
members: [users[0]._id, users[2]._id],
},
{
name: "Iron Orbit",
city: "Bucharest",
captain: users[1]._id,
members: [users[1]._id, users[3]._id],
},
]);
await Workout_1.WorkoutModel.insertMany([
{
title: "Tempo Run 8K",
category: "Cardio",
difficulty: "Intermediate",
durationMinutes: 50,
equipment: ["Running shoes", "Sports watch"],
targetMuscles: ["Quadriceps", "Hamstrings", "Calves", "Core"],
},
{
title: "Barbell Strength Circuit",
category: "Strength",
difficulty: "Advanced",
durationMinutes: 60,
equipment: ["Barbell", "Weight plates", "Bench"],
targetMuscles: ["Glutes", "Back", "Chest", "Shoulders"],
},
{
title: "Mobility and Recovery Flow",
category: "Mobility",
difficulty: "Beginner",
durationMinutes: 30,
equipment: ["Yoga mat", "Resistance band"],
targetMuscles: ["Hips", "Lower back", "Shoulders"],
},
]);
await Activity_1.ActivityModel.insertMany([
{
user: users[0]._id,
team: teams[0]._id,
type: "Interval Run",
durationMinutes: 42,
caloriesBurned: 520,
completedAt: new Date("2026-06-18T06:40:00.000Z"),
},
{
user: users[1]._id,
team: teams[1]._id,
type: "Heavy Strength Session",
durationMinutes: 64,
caloriesBurned: 610,
completedAt: new Date("2026-06-18T18:20:00.000Z"),
},
{
user: users[2]._id,
team: teams[0]._id,
type: "Hill Hike",
durationMinutes: 95,
caloriesBurned: 780,
completedAt: new Date("2026-06-19T07:10:00.000Z"),
},
{
user: users[3]._id,
team: teams[1]._id,
type: "Road Cycling",
durationMinutes: 88,
caloriesBurned: 845,
completedAt: new Date("2026-06-19T16:05:00.000Z"),
},
]);
await Leaderboard_1.LeaderboardModel.insertMany([
{
period: "weekly-2026-W25",
generatedAt: new Date("2026-06-20T00:00:00.000Z"),
entries: [
{ user: users[3]._id, team: teams[1]._id, points: 1280, rank: 1 },
{ user: users[2]._id, team: teams[0]._id, points: 1225, rank: 2 },
{ user: users[1]._id, team: teams[1]._id, points: 1160, rank: 3 },
{ user: users[0]._id, team: teams[0]._id, points: 1095, rank: 4 },
],
},
]);
const counts = {
users: await User_1.UserModel.countDocuments(),
teams: await Team_1.TeamModel.countDocuments(),
activities: await Activity_1.ActivityModel.countDocuments(),
leaderboard: await Leaderboard_1.LeaderboardModel.countDocuments(),
workouts: await Workout_1.WorkoutModel.countDocuments(),
};
console.log("Seed complete", counts);
}
catch (error) {
console.error("Seed failed", error);
process.exitCode = 1;
}
finally {
await mongoose_1.default.disconnect();
}
};
void seed();
+5 -1
View File
@@ -1,9 +1,12 @@
{
"name": "backend",
"name": "octofit-tracker-backend",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "octofit-tracker-backend",
"version": "1.0.0",
"dependencies": {
"express": "^5.2.1",
"mongoose": "^9.7.1"
@@ -11,6 +14,7 @@
"devDependencies": {
"@types/express": "^5.0.6",
"@types/node": "^26.0.0",
"ts-node": "^10.9.2",
"ts-node-dev": "^2.0.0",
"typescript": "^6.0.3"
}
+3 -1
View File
@@ -6,7 +6,8 @@
"scripts": {
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
"build": "tsc -p tsconfig.json",
"start": "node dist/index.js"
"start": "node dist/index.js",
"seed": "ts-node src/scripts/seed.ts"
},
"dependencies": {
"express": "^5.2.1",
@@ -15,6 +16,7 @@
"devDependencies": {
"@types/express": "^5.0.6",
"@types/node": "^26.0.0",
"ts-node": "^10.9.2",
"ts-node-dev": "^2.0.0",
"typescript": "^6.0.3"
}
+69 -3
View File
@@ -1,21 +1,87 @@
import express from "express";
import mongoose from "mongoose";
import { ActivityModel } from "./models/Activity";
import { LeaderboardModel } from "./models/Leaderboard";
import { TeamModel } from "./models/Team";
import { UserModel } from "./models/User";
import { WorkoutModel } from "./models/Workout";
const app = express();
const port = Number(process.env.PORT) || 8000;
const mongoUri = process.env.MONGO_URI || "mongodb://127.0.0.1:27017/octofit_tracker";
const mongoUri = process.env.MONGO_URI || "mongodb://127.0.0.1:27017/octofit_db";
const codespaceName = process.env.CODESPACE_NAME;
const baseUrl = codespaceName
? `https://${codespaceName}-8000.app.github.dev`
: `http://localhost:${port}`;
app.use(express.json());
app.get("/health", (_req, res) => {
res.status(200).json({ status: "ok" });
res.status(200).json({ status: "ok", baseUrl });
});
app.get("/api/users/", async (_req, res) => {
try {
const items = await UserModel.find().sort({ createdAt: -1 }).lean();
res.status(200).json({ resource: "users", count: items.length, items });
} catch (error) {
res.status(500).json({ message: "Failed to fetch users", error });
}
});
app.get("/api/teams/", async (_req, res) => {
try {
const items = await TeamModel.find()
.populate("captain", "username email")
.populate("members", "username email")
.sort({ createdAt: -1 })
.lean();
res.status(200).json({ resource: "teams", count: items.length, items });
} catch (error) {
res.status(500).json({ message: "Failed to fetch teams", error });
}
});
app.get("/api/activities/", async (_req, res) => {
try {
const items = await ActivityModel.find()
.populate("user", "username email")
.populate("team", "name city")
.sort({ completedAt: -1 })
.lean();
res.status(200).json({ resource: "activities", count: items.length, items });
} catch (error) {
res.status(500).json({ message: "Failed to fetch activities", error });
}
});
app.get("/api/leaderboard/", async (_req, res) => {
try {
const items = await LeaderboardModel.find()
.populate("entries.user", "username email")
.populate("entries.team", "name city")
.sort({ generatedAt: -1 })
.lean();
res.status(200).json({ resource: "leaderboard", count: items.length, items });
} catch (error) {
res.status(500).json({ message: "Failed to fetch leaderboard", error });
}
});
app.get("/api/workouts/", async (_req, res) => {
try {
const items = await WorkoutModel.find().sort({ createdAt: -1 }).lean();
res.status(200).json({ resource: "workouts", count: items.length, items });
} catch (error) {
res.status(500).json({ message: "Failed to fetch workouts", error });
}
});
const start = async () => {
try {
await mongoose.connect(mongoUri);
app.listen(port, () => {
console.log(`OctoFit backend listening on http://localhost:${port}`);
console.log(`OctoFit backend listening on ${baseUrl}`);
});
} catch (error) {
console.error("Failed to start backend:", error);
@@ -0,0 +1,18 @@
import { Schema, model, type InferSchemaType, Types } from "mongoose";
const activitySchema = new Schema(
{
user: { type: Types.ObjectId, ref: "User", required: true },
team: { type: Types.ObjectId, ref: "Team", required: true },
type: { type: String, required: true, trim: true },
durationMinutes: { type: Number, required: true, min: 1 },
caloriesBurned: { type: Number, required: true, min: 1 },
completedAt: { type: Date, required: true },
},
{
timestamps: true,
},
);
export type ActivityDocument = InferSchemaType<typeof activitySchema>;
export const ActivityModel = model("Activity", activitySchema);
@@ -0,0 +1,25 @@
import { Schema, model, type InferSchemaType, Types } from "mongoose";
const leaderboardEntrySchema = new Schema(
{
user: { type: Types.ObjectId, ref: "User", required: true },
team: { type: Types.ObjectId, ref: "Team", required: true },
points: { type: Number, required: true, min: 0 },
rank: { type: Number, required: true, min: 1 },
},
{ _id: false },
);
const leaderboardSchema = new Schema(
{
period: { type: String, required: true, trim: true },
generatedAt: { type: Date, required: true },
entries: { type: [leaderboardEntrySchema], required: true },
},
{
timestamps: true,
},
);
export type LeaderboardDocument = InferSchemaType<typeof leaderboardSchema>;
export const LeaderboardModel = model("Leaderboard", leaderboardSchema);
@@ -0,0 +1,16 @@
import { Schema, model, type InferSchemaType, Types } from "mongoose";
const teamSchema = new Schema(
{
name: { type: String, required: true, trim: true },
city: { type: String, required: true, trim: true },
captain: { type: Types.ObjectId, ref: "User", required: true },
members: [{ type: Types.ObjectId, ref: "User", required: true }],
},
{
timestamps: true,
},
);
export type TeamDocument = InferSchemaType<typeof teamSchema>;
export const TeamModel = model("Team", teamSchema);
@@ -0,0 +1,18 @@
import { Schema, model, type InferSchemaType } from "mongoose";
const userSchema = new Schema(
{
username: { type: String, required: true, trim: true },
email: { type: String, required: true, unique: true, lowercase: true, trim: true },
age: { type: Number, required: true, min: 13 },
heightCm: { type: Number, required: true, min: 100 },
weightKg: { type: Number, required: true, min: 30 },
fitnessGoal: { type: String, required: true, trim: true },
},
{
timestamps: true,
},
);
export type UserDocument = InferSchemaType<typeof userSchema>;
export const UserModel = model("User", userSchema);
@@ -0,0 +1,18 @@
import { Schema, model, type InferSchemaType } from "mongoose";
const workoutSchema = new Schema(
{
title: { type: String, required: true, trim: true },
category: { type: String, required: true, trim: true },
difficulty: { type: String, required: true, trim: true },
durationMinutes: { type: Number, required: true, min: 1 },
equipment: { type: [String], required: true },
targetMuscles: { type: [String], required: true },
},
{
timestamps: true,
},
);
export type WorkoutDocument = InferSchemaType<typeof workoutSchema>;
export const WorkoutModel = model("Workout", workoutSchema);
+166
View File
@@ -0,0 +1,166 @@
import mongoose from "mongoose";
import { ActivityModel } from "../models/Activity";
import { LeaderboardModel } from "../models/Leaderboard";
import { TeamModel } from "../models/Team";
import { UserModel } from "../models/User";
import { WorkoutModel } from "../models/Workout";
const mongoUri = process.env.MONGO_URI || "mongodb://127.0.0.1:27017/octofit_db";
const seed = async () => {
try {
await mongoose.connect(mongoUri);
console.log("Seed the octofit_db database with test data");
await Promise.all([
ActivityModel.deleteMany({}),
LeaderboardModel.deleteMany({}),
TeamModel.deleteMany({}),
UserModel.deleteMany({}),
WorkoutModel.deleteMany({}),
]);
const users = await UserModel.insertMany([
{
username: "alex_runner",
email: "alex.runner@example.com",
age: 29,
heightCm: 178,
weightKg: 74,
fitnessGoal: "Half marathon in under 1h40",
},
{
username: "maya_lifter",
email: "maya.lifter@example.com",
age: 33,
heightCm: 165,
weightKg: 62,
fitnessGoal: "Increase deadlift max to 140kg",
},
{
username: "liam_hiker",
email: "liam.hiker@example.com",
age: 26,
heightCm: 182,
weightKg: 79,
fitnessGoal: "Improve endurance for alpine trekking",
},
{
username: "sofia_cycle",
email: "sofia.cycle@example.com",
age: 31,
heightCm: 170,
weightKg: 66,
fitnessGoal: "Ride a 100km gran fondo",
},
]);
const teams = await TeamModel.insertMany([
{
name: "Pulse Pacers",
city: "Cluj-Napoca",
captain: users[0]._id,
members: [users[0]._id, users[2]._id],
},
{
name: "Iron Orbit",
city: "Bucharest",
captain: users[1]._id,
members: [users[1]._id, users[3]._id],
},
]);
await WorkoutModel.insertMany([
{
title: "Tempo Run 8K",
category: "Cardio",
difficulty: "Intermediate",
durationMinutes: 50,
equipment: ["Running shoes", "Sports watch"],
targetMuscles: ["Quadriceps", "Hamstrings", "Calves", "Core"],
},
{
title: "Barbell Strength Circuit",
category: "Strength",
difficulty: "Advanced",
durationMinutes: 60,
equipment: ["Barbell", "Weight plates", "Bench"],
targetMuscles: ["Glutes", "Back", "Chest", "Shoulders"],
},
{
title: "Mobility and Recovery Flow",
category: "Mobility",
difficulty: "Beginner",
durationMinutes: 30,
equipment: ["Yoga mat", "Resistance band"],
targetMuscles: ["Hips", "Lower back", "Shoulders"],
},
]);
await ActivityModel.insertMany([
{
user: users[0]._id,
team: teams[0]._id,
type: "Interval Run",
durationMinutes: 42,
caloriesBurned: 520,
completedAt: new Date("2026-06-18T06:40:00.000Z"),
},
{
user: users[1]._id,
team: teams[1]._id,
type: "Heavy Strength Session",
durationMinutes: 64,
caloriesBurned: 610,
completedAt: new Date("2026-06-18T18:20:00.000Z"),
},
{
user: users[2]._id,
team: teams[0]._id,
type: "Hill Hike",
durationMinutes: 95,
caloriesBurned: 780,
completedAt: new Date("2026-06-19T07:10:00.000Z"),
},
{
user: users[3]._id,
team: teams[1]._id,
type: "Road Cycling",
durationMinutes: 88,
caloriesBurned: 845,
completedAt: new Date("2026-06-19T16:05:00.000Z"),
},
]);
await LeaderboardModel.insertMany([
{
period: "weekly-2026-W25",
generatedAt: new Date("2026-06-20T00:00:00.000Z"),
entries: [
{ user: users[3]._id, team: teams[1]._id, points: 1280, rank: 1 },
{ user: users[2]._id, team: teams[0]._id, points: 1225, rank: 2 },
{ user: users[1]._id, team: teams[1]._id, points: 1160, rank: 3 },
{ user: users[0]._id, team: teams[0]._id, points: 1095, rank: 4 },
],
},
]);
const counts = {
users: await UserModel.countDocuments(),
teams: await TeamModel.countDocuments(),
activities: await ActivityModel.countDocuments(),
leaderboard: await LeaderboardModel.countDocuments(),
workouts: await WorkoutModel.countDocuments(),
};
console.log("Seed complete", counts);
} catch (error) {
console.error("Seed failed", error);
process.exitCode = 1;
} finally {
await mongoose.disconnect();
}
};
void seed();
+1
View File
@@ -3,6 +3,7 @@
"target": "ES2020",
"module": "Node16",
"moduleResolution": "Node16",
"types": ["node"],
"outDir": "dist",
"rootDir": "src",
"strict": true,