תיאור הבעיה
על פי רוב בממשקי API, האובייקטים שה-API שולח משקפים במידה רבה את המבנה של ה-DB. עד כדי כך, שלרוב התשובה של ה-API הוא פשוט ייצוג JSON של שורות ב-DB.
מתכנתים עצלנים כמו רוב עמך (כולל אני) בד"כ פשוט עונים לבקשות API עם תבנית זו:
- שאילתת DB לקבל את הישויות הרלוונטיות
- הרצת לוגיקה על היישויות, כמו בדיקת הרשאות, או עדכון נתונים וכו'
- סיריאליזציה של ישות ה-DB שרלוונטי לתשובה ל-JSON, ושליחה לקליינט
הבעיה היא שקורה הרבה שהשדות שנצרכים עבור שלב 2 שונים מהשדות שרוצים לשלוח בשלב 3
למשל בבקשת login בד"כ שולפים שורה שמייצגת משתמש, ועבור שלב 2 רוצים לבדוק שהסיסמה נכונה, ולכן שולפים את השדה שמייצג את ההאש של הסיסמה, אבל בשלב 3 בד"כ לא רוצים לשלוח את זה חזרה לקליינט
עוד דוגמה: אפליקציה ששומרת מפתח API של המשתמש כדי להריץ עבורו פעולות מול צד שלישי, רוצה לשלוף את המידע הזה מה-DB עבור שלב 2 כדי לטפל בבקשות שונות, אבל לא רוצה לשלוח את זה חזרה לקליינט
וכהנה רבות
מצו"ב קוד קצר לטיפול נוח בנושא זה עבור משתמשי Sequelize
הסבר על הפתרון
בספריית Sequelize, כאשר הופכים מודל ל-JSON, מנוע JS משתמש מתחת למכסה בפונקציית Sequelize.Model.prototype.toJSON
הרעיון הוא שנוסיף שדה סטטית להגדרת המודלים שלנו שמכיל מערך של שדות שצריך תמיד להשמיט מהסיריאליזציה, ונכתוב toJSON
מותאם אישית שלנו שיכבד רשימה זו
כדי שזה יעבוד גם על מודלים מקוננים, נקרא לפונקצייה זו בצורה ריקורסיבית על מודלים מקוננים
הנה הקוד
// Override the builtin `toJSON` method to allow hiding certain fields from the client
// To hide a field, add it to the `hidden` array on the model's prototype
// We recursively call `toJSON` on all included models
Sequelize.Model.prototype.toJSON = function () {
const includes = this._options.includeNames ?? [];
const hiddenKeys = this.hidden ?? [];
const values = Object.assign({}, this.get());
for (const hiddenKey of hiddenKeys) {
delete values[hiddenKey];
}
for (const include of includes) {
if (Array.isArray(values[include])) {
values[include] = values[include].map((value) => value.toJSON());
} else if (typeof values[include].toJSON === 'function') {
values[include] = values[include].toJSON();
}
}
return values;
};
והנה הגדרת מודל לדוגמה:
const User = sequelize.define(
'User',
{
firstName: {
type: DataTypes.STRING,
allowNull: false,
},
lastName: {
type: DataTypes.STRING,
},
passwordHash: {
type: DataTypes.STRING,
}
});
User.prototype.hidden = ['passwordHash']
או אם אתה מעדיף קלאסים:
class User extends Model {
static hidden = ['passwordHash']
}
User.init(
{
firstName: {
type: DataTypes.STRING,
allowNull: false,
},
lastName: {
type: DataTypes.STRING,
},
passwordHash: {
type: DataTypes.STRING,
}
},
{
sequelize,
modelName: 'User',
},
);
נ.ב. הקוד נכתב עבור sequelize v6. אני משתמש בשדה פנימית של Model
(ה-_options.includeNames
). אין הבטחה שהקוד ימשיך לעבוד בגירסאות הבאות של sequelize
עוד הערה:
אני לא חושב שקוד זה מספיק טוב לאפליקציה רצינית
נראה לי שאפליקציה רצינית אמורה להפריד בין הייצוג הפנימי לאובייקטים החיצוניים שנשלחים לקליינט, ולהשתמש בשכבה שיודע להמיר בין אובייקטים פנימיים לאובקטים "חיצוניים"
בפרוייקט אחד שלי השתמשתי ב-https://github.com/typestack/class-transformer לצורך זה