Kodax

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();
}
اختبر فهمك