Mongoose ODM — MongoDB
Mongoose ODM
MongoDB نفسه لا يفرض شكلاً للمستندات: مستند فيه email وآخر فيه mail وثالث بلا بريد — كلها مقبولة. Mongoose يضيف مخططاً (Schema) في التطبيق: أنواع، وتحقق، وقيم افتراضية، ودوال.
المخطط والنموذج
import mongoose from "mongoose";
const userSchema = new mongoose.Schema(
{
name: { type: String, required: [true, "الاسم مطلوب"], trim: true, maxlength: 50 },
email: { type: String, required: true, unique: true, lowercase: true, match: /.+@.+\..+/ },
age: { type: Number, min: 13, max: 120 },
role: { type: String, enum: ["user", "admin"], default: "user" },
passwordHash: { type: String, required: true, select: false }, // لا يُعاد افتراضياً
tags: [String],
},
{ timestamps: true } // createdAt و updatedAt تلقائياً
);
export const User = mongoose.model("User", userSchema); // مجموعة "users"| الخيار | الأثر |
|---|---|
| required و min و max و enum و match | تحقق قبل الحفظ |
| trim و lowercase | تنظيف تلقائي |
| default | قيمة افتراضية |
| select: false | لا يُعاد في الاستعلامات إلا بطلب صريح .select("+passwordHash") |
| unique: true | يُنشئ فهرساً فريداً — ليس تحققاً: الخطأ يأتي من MongoDB (الرمز 11000) |
التحقق يعمل عند الحفظ
create و save تتحقق تلقائياً. أما updateOne و findByIdAndUpdate فلا تتحقق إلا مع { runValidators: true } — مصدر شائع لبيانات فاسدة.
الوسائط (Middleware / Hooks)
import mongoose from "mongoose";
const postSchema = new mongoose.Schema({ title: String, slug: String });
postSchema.pre("save", function () {
if (this.isModified("title")) {
this.slug = this.title.trim().toLowerCase().replace(/\s+/g, "-");
}
});استخدام شائع: تجزئة كلمة المرور قبل الحفظ عند تعديلها فقط (isModified("password")).
دوال المثيل والدوال الساكنة
- userSchema.methods.isAdult = function () {...} ← user.isAdult().
- userSchema.statics.findByEmail = function (email) {...} ← User.findByEmail(...).
إخفاء الحقول عند التحويل لـ JSON
toJSON: { transform(doc, ret) { delete ret.passwordHash; return ret; } } في خيارات المخطط — طبقة حماية ثانية مع select: false.
الاتصال
await mongoose.connect(process.env.MONGODB_URI) مرة واحدة عند بدء التطبيق. خيارات مثل useNewUrlParser و useUnifiedTopology التي تراها في الشروحات القديمة لم تعد لازمة منذ Mongoose 6.
تمرين
أضف إلى المثال الكامل مخطط Product بحقول: اسم (إلزامي)، سعر (≥ 0)، فئة من قائمة محددة، و stock افتراضيه 0. جرّب إنشاء منتج بسعر سالب واقرأ رسالة الخطأ.
// npm install mongoose
import mongoose from "mongoose";
await mongoose.connect(process.env.MONGODB_URI ?? "mongodb://127.0.0.1:27017/kodax_demo");
mongoose.set("sanitizeFilter", true); // يحيّد عوامل $ القادمة من مدخلات المستخدم
const userSchema = new mongoose.Schema(
{
name: { type: String, required: [true, "الاسم مطلوب"], trim: true, maxlength: 50 },
email: { type: String, required: true, unique: true, lowercase: true, trim: true },
age: { type: Number, min: [13, "العمر الأدنى 13"], max: 120 },
role: { type: String, enum: ["user", "admin"], default: "user" },
passwordHash: { type: String, required: true, select: false },
},
{
timestamps: true,
toJSON: {
transform(doc, ret) {
delete ret.passwordHash;
return ret;
},
},
}
);
userSchema.methods.isAdult = function () {
return (this.age ?? 0) >= 18;
};
userSchema.statics.findByEmail = function (email) {
return this.findOne({ email: String(email).toLowerCase() });
};
const User = mongoose.models.User ?? mongoose.model("User", userSchema);
try {
const user = await User.create({ name: " سارة ", email: "Sara@Example.com", age: 21, passwordHash: "hash-from-bcrypt" });
console.log("أُنشئ:", user.name, user.email, "بالغ؟", user.isAdult());
const found = await User.findByEmail("SARA@example.com");
console.log("passwordHash مُعاد؟", found.passwordHash !== undefined); // false بسبب select: false
await User.create({ name: "ع", email: "kid@example.com", age: 9, passwordHash: "x" });
} catch (err) {
if (err.name === "ValidationError") {
console.log("فشل التحقق:", Object.values(err.errors).map((e) => e.message));
} else if (err.code === 11000) {
console.log("البريد مستخدم مسبقاً");
} else {
throw err;
}
} finally {
await mongoose.disconnect();
}اختبر فهمك