• בלוג
  • בואו נכתוב שרת GraphQL ב Node.JS כדי לראות איך זה עובד

בואו נכתוב שרת GraphQL ב Node.JS כדי לראות איך זה עובד

פוסט זה מכיל טיפ קצר על עבודה עם Node.JS. כדי ללמוד Node.JS הרבה יותר לעומק אני ממליץ לכם לבדוק את קורס Node.JS כאן באתר. הקורס כולל עשרות שיעורי וידאו מוקלטים והמון תרגול שילוו אתכם מהצעד הראשון בעבודה עם Node.JS ועד הנושאים המתקדמים.

גרף קיו אל הוא הדבר הגדול הבא מאז שהמציאו את הלחם החתוך. הוא פותר את כל הבעיות של REST ואז עוד קצת וכבר כתבתי עליו מספר פוסטים בעבר. אם אתם לא מכירים את GraphQL בכלל שווה להתחיל במבוא שכתבתי כאן: שלום GraphQL.

בפוסט היום אני רוצה לכתוב שרת GraphQL ב Node.JS ו Express כדי לראות איך זה עובד. הפרויקט הוא API למערכת ניהול משימות שמאפשר לנו לראות את המשימות הפתוחות במערכת ולעדכן סטטוס "בוצע" של כל משימה.

העליתי את כל הקוד בפוסט לפרויקט דוגמה בגיטהאב בקישור:

https://github.com/ynonp/express-graphql-demo

1. שכבת הנתונים

מאפיין חשוב של GraphQL הוא ההפרדה בין שכבת הנתונים לשכבת התקשורת. יש לנו בצד אחד את הקוד שיודע לשלוף מבסיס הנתונים ולעדכן שם את המידע, וקוד זה יכול להיות כתוב בכל ספריה שתרצו לדוגמה Mongo, Mongoose, Sequelize או כמו שאני אכתוב בדוגמה היום ב knex.js.

אחריו יש לנו שכבת חיבור שנקראת Resolver או Root Value. שכבת החיבור אחראית על מיפוי הערכים מהשאילתה שמשתמש שלח לפונקציות הגישה לבסיס הנתונים.

ובסוף יש לנו את הסכימה שמתארת החוצה איזה מידע אנחנו יכולים לספק.

ההפרדה בין השכבות תאפשר לנו בעתיד לשנות בקלות את שיכבת הנתונים שלנו ואפילו להחליף בסיס נתונים לחלוטין ועדיין להשאיר את הממשק החוצה ללקוחות ללא שינוי. מסיבה זו אנחנו מאוד אוהבים להשתמש ב GraphQL בארכיטקטורת Micro Services כדי לבנות את התקשורת בין הסרביסים השונים בצורה יציבה.

נתחיל בבניית שכבת הנתונים ובפרויקט הדוגמה בחרתי ב knex.js. הקוד שאחראי על השאילתות הוא בסך הכל קובץ קצר של פחות מ 20 שורות:

// dl/tasks.js
const knex = require('../db/knex');

exports.getTasks = function getTasks() {
  return knex.from('tasks').select('*');
}

exports.getTask = function getTask(id) {
  return knex.from('tasks').where({ id: id }).first('*');
}

exports.getTasksBy = function getTasksBy(attributes) {
  return knex.from('tasks').where(attributes).select('*');
}

exports.toggleTask = function toggleTask(id) {
  return knex('tasks')
  .where({ id })
  .update({ done: knex.raw("CASE WHEN done = 0 THEN 1 ELSE 0 END") });
}

המימוש של הפונקציות הספציפיות פחות חשוב בשביל הדוגמה שלנו. שימוש בבסיס נתונים אחר או בספריית גישה אחרת לבסיס הנתונים היו מייצרים מימושים אחרים, אבל זה בסדר גמור. כל עוד שמות הפונקציות והפרמטרים זהים כל שאר הקוד לא ישתנה.

2. הסכימה

נעבור לצד השני של העולם בשביל לדבר על הסכימה. סכימה ב GraphQL היא פשוט תיאור של האוביקטים, המאפיינים שלהם ואם יש להם אוביקטים מקוננים אז גם תיאור של אותם שדות פנימיים. הסכימה מתארת גרף ויכולה להתאים למשפטים כמו:

  1. לכל משימה יש שדה "סטטוס" מסוג בוליאני ושדה "תיאור" מסוג טקסט

  2. לכל משתמש במערכת יש שדה "שם" עם השם שלו

  3. לכל משתמש במערכת יש 1 או יותר משימות

  4. לכל משימה יש 1 או יותר תגיות

חישבו על הסכימה בדיוק כמו על סכימה של בסיס נתונים, רק בלי לדבר על בסיס הנתונים שמאחוריה. ל GraphQL יש שפה מעניינת לכתיבת סכימות ואפשר לקרוא עליה כאן. הנה הסכימה המאוד פשוטה שאני כתבתי לתיאור המשימות והדברים שאנחנו יכולים לעשות עם אותן משימות:

  type Query {
    tasks: [Task]
    task(id: Int!): Task
  }

  type Task {
    title: String
    description: String
    done: Boolean
  }

  type Mutation {
    toggleTask(id: Int!): Task
  }

הסכימה מורכבת מ-3 טיפוסים:

  1. הטיפוס המיוחד Query (הוא מיוחד בגלל השם, Query זה שם שמור ב GraphQL) כולל שדה בשם tasks שצריך להחזיר רשימת משימות, ושדה בשם task שמקבל id מסוג מספר ומחזיר משימה בודדת. וכשאני כותב "שדה" תחשבו בלב "פונקציה", כי זה מה שבאמת ישב בשדות האלה.

  2. הטיפוס המיוחד Mutation (שוב מיוחד בגלל השם, Mutation הוא שם שמור ב GraphQL) כולל שדה יחיד בשם toggleTask. שדה כזכור הוא פונקציה והפונקציה כאן מקבלת מזהה ומחזירה משימה.

  3. הטיפוס Task מתאר מהי משימה. הוא מספר לנו שלמשימה יש שלושה שדות: title, description ו done ואת הסוגים שלהם.

בשביל לבנות אוביקט סכימה של GraphQL אני מעביר את התיאור הטקסטואלי הזה לפונקציה בשם buildSchema.

3. ה Resolver

החלק האחרון במסלול שלנו נקרא ה Resolver או ה rootValue. זה המימוש של הסכימה, כלומר אוביקט ה JavaScript שמחבר בין הסכימה לשכבת הנתונים. כך הוא נראה במנהל המשימות שלי:

const db = require('../dl/tasks');

module.exports = {
  tasks: (args) => {
    return db.getTasks();
  },
  task: (args) => {
    const { id } = args;
    return db.getTask(id);
  },
  toggleTask: async (args) => {
    const { id } = args;
    await db.toggleTask(id);
    return await db.getTask(id);
  }
};

שלושת הפונקציות מתאימות לשלושת השדות באוביקטים המיוחדים Query ו Mutation. הסכימה הגדירה ש Task הוא עלה (leaf) ושיש לו את השדות title, description ו done, ולכן GraphQL מצפה לקבל בחזרה מהפונקציות אוביקט או אוביקטים עם שדות אלה. אגב הוא מוכן להתפשר ולקבל Promise שיחזיר אוביקט עם שדות אלה, וזה מה שאנחנו עושים בדוגמה כאן.

4. איך מריצים ושאילתות לדוגמה

בפוסט הדבקתי רק את הקוד שנראה לי מעניין בשביל להבין את המנגנון. אתם מוזמנים להסתכל בגיטהאב כדי לראות את הפרויקט המלא ואפילו להריץ אותו.

אם תורידו אליכם את הפרויקט אז אחרי הרצת npm install תוכלו להפעיל אצלכם את המערכת בהאזנה על פורט 5000 עם הפקודה:

$ PORT=5000 node bin/www

אחרי ההפעלה אפשר לגלוש ל http://localhost:5000/graphql ולקבל ממשק בדיקה של ה API. בממשק אני כותב שאילתות בצד שמאל ומקבל את התשובות בצד ימין. לדוגמה השאילתה:

query {
  tasks { 
    title
    description
    done
  }
}

תחזיר את התשובה:

{
  "data": {
    "tasks": [
      {
        "title": "task1",
        "description": "write some code",
        "done": false
      },
      {
        "title": "task2",
        "description": "test it",
        "done": false
      },
      {
        "title": "task3",
        "description": "deploy",
        "done": false
      }
    ]
  }
}

בשביל לסיים משימה אני יכול להשתמש בפונקציה toggleTask שכתבתי עם השאילתה הבאה:

mutation {
  toggleTask(id: 2) { 
    title
    done
  }
}

ולקבל בחזרה:

{
  "data": {
    "toggleTask": {
      "title": "task2",
      "done": true
    }
  }
}

שזה מצב העניינים באוביקט אחרי השינוי.

טיפ למטיבי לכת: ואחרי שבניתם את ה API, תוכלו לחזור לפוסט חיבור אפליקציית ריאקט ל GraphQL שם אני מסביר איך לחבר קוד ריאקט לשרת GraphQL בעזרת ספריית Relay.