184 行
7.2 KiB
TypeScript
184 行
7.2 KiB
TypeScript
import { int, mysqlEnum, mysqlTable, text, timestamp, varchar, json, float } from "drizzle-orm/mysql-core";
|
|
|
|
/**
|
|
* Core user table - supports both OAuth and simple username login
|
|
*/
|
|
export const users = mysqlTable("users", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
openId: varchar("openId", { length: 64 }).notNull().unique(),
|
|
name: text("name"),
|
|
email: varchar("email", { length: 320 }),
|
|
loginMethod: varchar("loginMethod", { length: 64 }),
|
|
role: mysqlEnum("role", ["user", "admin"]).default("user").notNull(),
|
|
/** Tennis skill level */
|
|
skillLevel: mysqlEnum("skillLevel", ["beginner", "intermediate", "advanced"]).default("beginner"),
|
|
/** User's training goals */
|
|
trainingGoals: text("trainingGoals"),
|
|
/** NTRP rating (1.0 - 5.0) */
|
|
ntrpRating: float("ntrpRating").default(1.5),
|
|
/** Total training sessions completed */
|
|
totalSessions: int("totalSessions").default(0),
|
|
/** Total training minutes */
|
|
totalMinutes: int("totalMinutes").default(0),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
lastSignedIn: timestamp("lastSignedIn").defaultNow().notNull(),
|
|
});
|
|
|
|
export type User = typeof users.$inferSelect;
|
|
export type InsertUser = typeof users.$inferInsert;
|
|
|
|
/**
|
|
* Simple username-based login accounts
|
|
*/
|
|
export const usernameAccounts = mysqlTable("username_accounts", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
username: varchar("username", { length: 64 }).notNull().unique(),
|
|
userId: int("userId").notNull(),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
});
|
|
|
|
export type UsernameAccount = typeof usernameAccounts.$inferSelect;
|
|
|
|
/**
|
|
* Training plans generated for users
|
|
*/
|
|
export const trainingPlans = mysqlTable("training_plans", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
userId: int("userId").notNull(),
|
|
title: varchar("title", { length: 256 }).notNull(),
|
|
skillLevel: mysqlEnum("skillLevel", ["beginner", "intermediate", "advanced"]).notNull(),
|
|
/** Plan duration in days */
|
|
durationDays: int("durationDays").notNull().default(7),
|
|
/** JSON array of training exercises */
|
|
exercises: json("exercises").notNull(),
|
|
/** Whether this plan is currently active */
|
|
isActive: int("isActive").notNull().default(1),
|
|
/** Auto-adjustment notes from AI analysis */
|
|
adjustmentNotes: text("adjustmentNotes"),
|
|
/** Plan generation version for tracking adjustments */
|
|
version: int("version").notNull().default(1),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
});
|
|
|
|
export type TrainingPlan = typeof trainingPlans.$inferSelect;
|
|
export type InsertTrainingPlan = typeof trainingPlans.$inferInsert;
|
|
|
|
/**
|
|
* Training videos uploaded by users
|
|
*/
|
|
export const trainingVideos = mysqlTable("training_videos", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
userId: int("userId").notNull(),
|
|
title: varchar("title", { length: 256 }).notNull(),
|
|
/** S3 file key */
|
|
fileKey: varchar("fileKey", { length: 512 }).notNull(),
|
|
/** CDN URL for the video */
|
|
url: text("url").notNull(),
|
|
/** Video format: webm or mp4 */
|
|
format: varchar("format", { length: 16 }).notNull(),
|
|
/** File size in bytes */
|
|
fileSize: int("fileSize"),
|
|
/** Duration in seconds */
|
|
duration: float("duration"),
|
|
/** Type of exercise in the video */
|
|
exerciseType: varchar("exerciseType", { length: 64 }),
|
|
/** Analysis status */
|
|
analysisStatus: mysqlEnum("analysisStatus", ["pending", "analyzing", "completed", "failed"]).default("pending"),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
});
|
|
|
|
export type TrainingVideo = typeof trainingVideos.$inferSelect;
|
|
export type InsertTrainingVideo = typeof trainingVideos.$inferInsert;
|
|
|
|
/**
|
|
* Pose analysis results from MediaPipe - enhanced with tennis_analysis features
|
|
*/
|
|
export const poseAnalyses = mysqlTable("pose_analyses", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
videoId: int("videoId").notNull(),
|
|
userId: int("userId").notNull(),
|
|
/** Overall pose score (0-100) */
|
|
overallScore: float("overallScore"),
|
|
/** JSON object with detailed joint angles and metrics */
|
|
poseMetrics: json("poseMetrics"),
|
|
/** JSON array of detected issues */
|
|
detectedIssues: json("detectedIssues"),
|
|
/** JSON array of correction suggestions */
|
|
corrections: json("corrections"),
|
|
/** Exercise type analyzed */
|
|
exerciseType: varchar("exerciseType", { length: 64 }),
|
|
/** Number of frames analyzed */
|
|
framesAnalyzed: int("framesAnalyzed"),
|
|
/** --- tennis_analysis inspired fields --- */
|
|
/** Number of swings/shots detected */
|
|
shotCount: int("shotCount").default(0),
|
|
/** Average swing speed (estimated from keypoint displacement, px/frame) */
|
|
avgSwingSpeed: float("avgSwingSpeed"),
|
|
/** Max swing speed detected */
|
|
maxSwingSpeed: float("maxSwingSpeed"),
|
|
/** Total body movement distance in pixels */
|
|
totalMovementDistance: float("totalMovementDistance"),
|
|
/** Stroke consistency score (0-100) */
|
|
strokeConsistency: float("strokeConsistency"),
|
|
/** Footwork score (0-100) */
|
|
footworkScore: float("footworkScore"),
|
|
/** Fluidity/smoothness score (0-100) */
|
|
fluidityScore: float("fluidityScore"),
|
|
/** JSON array of key moments [{frame, type, description}] */
|
|
keyMoments: json("keyMoments"),
|
|
/** JSON array of movement trajectory points [{x, y, frame}] */
|
|
movementTrajectory: json("movementTrajectory"),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
});
|
|
|
|
export type PoseAnalysis = typeof poseAnalyses.$inferSelect;
|
|
export type InsertPoseAnalysis = typeof poseAnalyses.$inferInsert;
|
|
|
|
/**
|
|
* Training session records for progress tracking
|
|
*/
|
|
export const trainingRecords = mysqlTable("training_records", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
userId: int("userId").notNull(),
|
|
planId: int("planId"),
|
|
/** Exercise name/type */
|
|
exerciseName: varchar("exerciseName", { length: 128 }).notNull(),
|
|
/** Duration in minutes */
|
|
durationMinutes: int("durationMinutes"),
|
|
/** Completion status */
|
|
completed: int("completed").notNull().default(0),
|
|
/** Optional notes */
|
|
notes: text("notes"),
|
|
/** Pose score if video was analyzed */
|
|
poseScore: float("poseScore"),
|
|
/** Date of training */
|
|
trainingDate: timestamp("trainingDate").defaultNow().notNull(),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
});
|
|
|
|
export type TrainingRecord = typeof trainingRecords.$inferSelect;
|
|
export type InsertTrainingRecord = typeof trainingRecords.$inferInsert;
|
|
|
|
/**
|
|
* NTRP Rating history - tracks rating changes over time
|
|
*/
|
|
export const ratingHistory = mysqlTable("rating_history", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
userId: int("userId").notNull(),
|
|
/** NTRP rating at this point */
|
|
rating: float("rating").notNull(),
|
|
/** What triggered this rating update */
|
|
reason: varchar("reason", { length: 256 }),
|
|
/** JSON breakdown of dimension scores */
|
|
dimensionScores: json("dimensionScores"),
|
|
/** Reference analysis ID if applicable */
|
|
analysisId: int("analysisId"),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
});
|
|
|
|
export type RatingHistory = typeof ratingHistory.$inferSelect;
|
|
export type InsertRatingHistory = typeof ratingHistory.$inferInsert;
|