Learn more about Mintlify
Enter your email to receive updates about new features and product releases.
Mongoose schemas and models for the Horse Trust platform
import { Schema, model } from "mongoose";
import bcrypt from "bcryptjs";
const userSchema = new Schema<IUser>(
{
email: {
type: String,
required: [true, "Email is required"],
unique: true,
lowercase: true,
trim: true,
match: [/^[^\s@]+@[^\s@]+\.[^\s@]+$/, "Invalid email format"],
},
password_hash: { type: String, required: true },
role: { type: String, enum: ["admin", "seller"], required: true },
full_name: { type: String, required: [true, "Full name is required"], trim: true },
phone: {
type: String,
trim: true,
match: [/^\+?[1-9][0-9]{7,14}$/, "Invalid phone format. Use international format: +5491112345678"],
},
is_email_verified: { type: Boolean, default: false },
is_phone_verified: { type: Boolean, default: false },
email_verification_token: { type: String },
profile_picture_url: { type: String },
seller_profile: { type: sellerProfileSchema, default: null },
is_active: { type: Boolean, default: true },
last_login: { type: Date },
},
{
timestamps: { createdAt: "created_at", updatedAt: "updated_at" },
}
);
const sellerProfileSchema = new Schema(
{
identity_document: { type: String },
selfie_url: { type: String },
verification_status: {
type: String,
enum: ["pending", "verified", "rejected"],
default: "pending"
},
verification_method: { type: String, enum: ["manual", "automatic"] },
verified_at: { type: Date },
verified_by: { type: Schema.Types.ObjectId, ref: "User" },
rejection_reason: { type: String },
is_verified_badge: { type: Boolean, default: false },
},
{ _id: false }
);
userSchema.pre("save", async function (next) {
// Only hash when password_hash field is modified
if (!this.isModified("password_hash")) return;
const salt = await bcrypt.genSalt(Number(process.env.BCRYPT_SALT_ROUNDS) || 12);
this.password_hash = await bcrypt.hash(this.password_hash, salt);
});
// Compare password for authentication
userSchema.methods.comparePassword = async function (candidatePassword: string): Promise<boolean> {
return bcrypt.compare(candidatePassword, this.password_hash);
};
// Remove sensitive fields from JSON output
userSchema.methods.toJSON = function () {
const obj = this.toObject();
delete obj.password_hash;
delete obj.email_verification_token;
return obj;
};
const horseSchema = new Schema<IHorse>(
{
seller_id: { type: Schema.Types.ObjectId, ref: "User", required: true, index: true },
name: { type: String, required: [true, "Horse name is required"], trim: true },
age: { type: Number, required: true, min: 0, max: 40 },
breed: { type: String, required: [true, "Breed is required"], trim: true },
discipline: { type: String, required: [true, "Discipline is required"], trim: true },
pedigree: { type: String },
location: { type: locationSchema, required: true },
price: { type: Number, min: 0 },
currency: { type: String, enum: ["USD", "EUR", "ARS", "BRL", "MXN"], default: "USD" },
photos: {
type: [photoSchema],
validate: {
validator: (photos: unknown[]) => photos.length >= 3,
message: "At least 3 photos are required",
},
},
videos: { type: [videoSchema], default: [] },
status: { type: String, enum: ["active", "sold", "paused", "draft"], default: "draft" },
views_count: { type: Number, default: 0 },
},
{
timestamps: { createdAt: "created_at", updatedAt: "updated_at" },
}
);
const locationSchema = new Schema(
{
country: { type: String, required: true },
region: { type: String, required: true },
city: { type: String },
coordinates: {
lat: { type: Number },
lng: { type: Number },
},
},
{ _id: false }
);
const photoSchema = new Schema(
{
url: { type: String, required: true },
caption: { type: String },
is_cover: { type: Boolean, default: false },
uploaded_at: { type: Date, default: () => new Date() },
},
{ _id: true }
);
const videoSchema = new Schema(
{
url: { type: String, required: true },
embed_url: { type: String },
video_type: { type: String, enum: ["training", "competition", "other"], required: true },
title: { type: String },
description: { type: String },
recorded_at: { type: Date, required: true },
uploaded_at: { type: Date, default: () => new Date() },
},
{ _id: true }
);
horseSchema.index(
{ name: "text", breed: "text", discipline: "text", pedigree: "text" },
{ default_language: "spanish" }
);
horseSchema.pre("save", function (next) {
this.videos = this.videos.map((video) => {
if (!video.embed_url && video.url) {
video.embed_url = toEmbedUrl(video.url);
}
return video;
});
});
function toEmbedUrl(url: string): string {
// YouTube
const ytMatch = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&\s]+)/);
if (ytMatch) return `https://www.youtube.com/embed/${ytMatch[1]}`;
// Vimeo
const vimeoMatch = url.match(/vimeo\.com\/(\d+)/);
if (vimeoMatch) return `https://player.vimeo.com/video/${vimeoMatch[1]}`;
return url;
}
const vetRecordSchema = new Schema<IVetRecord>(
{
horse_id: { type: Schema.Types.ObjectId, ref: "Horse", required: true, index: true },
vet_id: { type: Schema.Types.ObjectId, ref: "User" },
review_date: { type: Date, required: true },
health_status: { type: String, required: true },
certificates: { type: [certificateSchema], default: [] },
vaccines: { type: [vaccineSchema], default: [] },
validation_status: {
type: String,
enum: ["pending", "validated", "rejected"],
default: "pending"
},
validated_by: { type: Schema.Types.ObjectId, ref: "User" },
validated_at: { type: Date },
rejection_reason: { type: String },
notes: { type: String },
},
{
timestamps: { createdAt: "created_at", updatedAt: "updated_at" },
}
);
const vaccineSchema = new Schema(
{
name: { type: String, required: true },
applied_at: { type: Date, required: true },
next_due_at: { type: Date },
batch_number: { type: String },
},
{ _id: false }
);
const certificateSchema = new Schema(
{
url: { type: String, required: true },
title: { type: String },
uploaded_at: { type: Date, default: () => new Date() },
},
{ _id: true }
);
const conversationSchema = new Schema<IConversation>(
{
participants: {
type: [{ type: Schema.Types.ObjectId, ref: "User" }],
required: true,
validate: {
validator: (arr: unknown[]) => arr.length === 2,
message: "A conversation requires exactly 2 participants",
},
},
horse_id: { type: Schema.Types.ObjectId, ref: "Horse" },
last_message: { type: lastMessageSchema },
},
{
timestamps: { createdAt: "created_at", updatedAt: "updated_at" },
}
);
conversationSchema.index({ participants: 1 });
const lastMessageSchema = new Schema(
{
text: { type: String },
sender_id: { type: Schema.Types.ObjectId, ref: "User" },
sent_at: { type: Date },
is_read: { type: Boolean, default: false },
},
{ _id: false }
);
const messageSchema = new Schema<IMessage>(
{
conversation_id: {
type: Schema.Types.ObjectId,
ref: "Conversation",
required: true,
index: true
},
sender_id: { type: Schema.Types.ObjectId, ref: "User", required: true },
text: { type: String, required: true, maxlength: 2000 },
is_read: { type: Boolean, default: false },
read_at: { type: Date },
deleted_at: { type: Date },
},
{
timestamps: { createdAt: "sent_at", updatedAt: false },
}
);