آیین سعیدی، یه برنامه‌نویس و بلاگر

برگردید به صفحه‌ی اصلی

گراف‌کیو‌ال چیست؟

February 05, 202116 دقیقه مطالعهبرای باتجربه‌ترهای علاقه‌مند به وب
  • #گراف‌کیو‌ال
  • #graphql

برام مواردی پیش اومدن که لازم بوده برای کسی در مورد گراف‌کیو‌ال یا GraphQL توضیح بدم. اینکه چرا خوبه و کجاها میتونه خیلی کمک کنه.

من اینجا اول در مورد خود گراف‌کیو‌ال توضیح میدم، و تو قدم بعدی نحوه‌ی پیاده سازی یک سرور گراف‌کیو‌ال رو.

گراف کیو اِل چی هست؟

خیلی خلاصه «یه زبان کوئری گرفتن برای ای‌پی‌آی». ولی این توضیح هم خیلی کوتاهه هم اطلاعات کافی نمیده، مگه اینه اول بدونیم کامل خود گراف‌کیو‌ال چی هست.

گراف‌کیو‌ال یه زبانه برای ای‌پی‌آی که با دادن یک توضیح کامل از دیتایی که مد نظرتون هست، اونچه که خواستید رو در اختیارتون میذاره. در واقع به کلاینت کمک میکنه تا دقیقا بگه که چه چیزی رو میخواد و دقیقا همونچیز رو برمیگردونه.

یک مثال

فرض کنیم که یه ای‌پی‌آی مبتنی بر REST داریم. همچین چیزی:

1GET /products

برای یادآوری، این سبک نوشتن یعنی شما یک درخواست GET رو مثلا به یه آدرسی مثل http://localhost:3000/products میفرستید.

درخواست رو براش میفرستیم و همچین جوابی میگیریم:

1[
2 {
3 "id": 1234,
4 "name": "Aspire M5",
5 "brand": "Acer",
6 "category": "Laptop and Computers",
7 "features": {
8 "weight": 1.23,
9 "dimensions": {
10 "height": 23.4,
11 "width": 18.5,
12 "length": 18.3
13 }
14 },
15 "specs": {
16 "ram": 16,
17 "cpu": 3.5,
18 "cpu_count": 8,
19 "thread_count": 8
20 },
21 "price": 699.99
22 },
23 {...}
24]

خب اینا اطلاعات یه سری لپتاپ هستن (مثلا) و ما میخوایم اینارو تو صفحه محصولاتمون نشون بدیم. اما ایده اینه که تو صفحه‌ی اصلی، تمام اطلاعات لپتاپ‌ها رو نذاریم، بلکه فقط مواردی که مورد نیاز ما هستن رو نشون میدیم. مثلا اسم لپتاپ و قیمتش. بعد که یوزر روی محصول کلیک کرد، مابقی اطلاعات رو نشون بدیم.

اینجا به دوتا نکته توجه کنید:

  1. ما تمام اطلاعاتی که تو پاسخ به ما داده شده رو نیاز نداریم، فقط یک بخشیش مورد نیاز ماست و
  2. ما برای صفحه اختصاصی محصول، باید یک درخواست دیگه به مثلا GET /products/1234بفرستیم تا اطلاعات محصول رو مجدد دریافت کنیم.

اینجا یکی از جاهاییه که گراف‌کیو‌ال به کمک ما میاد!

مقدمه

فقط چیزهایی که لازم هست رو درخواست میدیم

گراف‌کیو‌ال یک زبان کوئریه. یعنی باید بهش بگید دقیقا چی میخواید تا اونهم دقیقا همون رو بهتون بده. اما چطور این کار رو انجام بدیم؟ این دقیقا همون ویژگی اصلی گراف‌کیو‌ال هست، یعنی شما انتخاب میکنی که چه فیلدهایی باید برای شما ارسال بشن. فکر کنم یه مثال بتونه خوب نشون بده که چطور میشه. تجسم کنید که ما توی گراف‌کیو‌ال میتونیم چنین چیزی رو برای سرورمون بفرستیم:

1query {
2 products {
3 id
4 name
5 price
6 }
7}

و در جوابش سرور گراف‌کیو‌ال به ما این رو میده:

1[
2 {
3 "id": 1234,
4 "name": "Aspire M5",
5 "price": 699.99
6 },
7 {
8 ...
9 }
10]

به همین سادگی! اما سوال اینه که چطور میشه اطلاعات فقط یک لپتاپ رو بدست آورد؟

اینم خیلی سادست!

1query {
2 product(id: "1234") {
3 id
4 name
5 brand
6 category
7 price
8 features {
9 weight
10 dimensions {
11 width
12 height
13 length
14 }
15 }
16 specs {
17 ram
18 cpu
19 cpu_count
20 thread_count
21 }
22 }
23}

و جوابش:

1{
2 "id": 1234,
3 "name": "Aspire M5",
4 "brand": "Acer",
5 "category": "Laptop and Computers",
6 "price": 699.99,
7 "features": {
8 "weight": 1.23,
9 "dimensions": {
10 "width": 18.5,
11 "height": 23.4,
12 "length": 18.3
13 }
14 },
15 "specs": {
16 "ram": 16,
17 "cpu": 3.5,
18 "cpu_count": 8,
19 "thread_count": 8
20 }
21}

اما یک نکته دیگه راجع به گراف‌کیو‌ال موند.

فقط یه یک آدرس درخواست میدیم!

این قسمت خوب قضیست. توی گراف‌کیو‌ال، دقیقا بر عکس ای‌پی‌آی‌های REST، ما فقط یک Endpoint داریم که بهش درخواست میدیم. همینطور همه‌ی این درخواست‌ها رو فقط با یک Verb میفرستیم! یعنی دیگه خبری از Get برای گرفتن اطلاعات، Post برای ساختشون و مابقی برای بقیه‌ی کارها نیست!

توی گراف‌کیو‌ال همه‌ی درخواست‌ها به همچین آدرسی ارسال میشن:

1POST /graphql

و نوع درخواست ما، یا همون Content-Typeکه توی Header میذاریم، به جای application/json، application/graphql میشه.

در نتیجه سرور متوجه میشه که با یک درخواست گراف‌کیو‌ال طرفه.

آشنایی با گراف‌کیو‌ال

گراف‌کیو‌ال، یک زبان مخصوص برای تعریف شکل داده‌هاست که سمت سرور اجرا میشه و خروجی لازم رو تحویل میده. چیزی که مهمه اینه که گراف‌کیو‌ال برای خودش یک Type System داره که باید در موردش حتما بدونیم.

یک سرور گراف‌کیو‌ال، اصولا با ساختن تایپ‌ها یا types شروع میشه که توی هر تایپ، فیلدهای خاصی وجود داره. در نهایت به هر فیلد، یک فانکشن تخصیص داده میشه که این اتفاق سمت موتور میوفته، ما فقط فانکشن‌ها رو تعریف میکنیم و بهشون لاجیک میدیم.

برای مثلا، یک سرور گراف‌کیو‌ال که که بهمون اطلاعات یک پست وبلاگ رو میده، حتما این دو تایپ رو داره:

1type Query {
2 post: PostType
3}
4
5type PostType {
6 title: String
7 comments_count: Int
8}

که شما در نهایت با فرستادن یک درخواست مثل

1{
2 post {
3 comments_count
4 }
5}

میتونید یک جواب شبیه به

1{
2 "post": {
3 "comments_count": 43
4 }
5}

رو دریافت کنید.

درخواستها یا Queries

GraphQL به عنوان یک استاندارد جدید برای جابه‌جایی داده‌ها، وارد جریان اصلی شرکت‌های و کسب و کارها شده. الان صحبت‌های خیلی زیادی تو جوامع مختلف میشه که چطور باید این تکنولوژی به سمت جلو حرکت کنه. یکی از بهترین قسمتهای GraphQL اینه که به عنوان یک زبان مشترک بسیار خوب با تیم شما ارتباط برقرار میکنه. اما چطوری باید در مورد خود زبان و فناوری اصلیش صحبت کرد؟

یکی از منابع خیلی خوب، خود GraphQL specification هست که فیسبوک اون رو کنترل میکنه. اما بزرگترین مشکل این Specها اینه که خیلی طولانی هستن و برای کسی که تازه وارد این موضوع شده، جذابیت زیادی ندارن. این هم چیزیه که من سعی دارم اینجا بهش بپردازم.

کوئری‌های ابتدایی در گراف‌کیو‌ال

اکثر برنامه‌نویس‌ها به هر چیزی که توی گراف‌کیو‌ال اتفاق میوفته «کوئری» میگن. اما حقیقت اینه که شما ممکنه به یه سرور گراف‌کیو‌ال:

  • کوئری بزنید (توضیح میدم)،
  • میوتیت (Mutate) یا (توضیح میدم)
  • سابسکرایب (Subscribe) کنید (تو این پست توضیح نمیدم).

پس اول از همه دوتا کانسپت رو توی ذهنمون داشته باشیم:

  • داکیومنت گراف‌کیو‌ال یا GraphQL Document: یه رشته‌ی استرینگ که به گراف‌کیو‌ال نوشته شده (نمیگم زبان چون کلمه‌ی زبان توی گراف‌کیو‌ال جا داره) و یک یا چند دستور یا Operation رو تعریف میکنه
  • دستور یا Operation: که میتونه یک یا چند Query، Mutation و یا Subscription باشه که موتور گراف‌کیو‌ال اونا رو بشناسه.

حالا بریم و اجزای تشکیل دهنده‌ی یک Operation رو ببینیم:

1query basicQuery {
2 user(id: 1234) {
3 name
4 email
5 avatar(size: SMALL)
6 }
7}

بریم که کلمه به کلمه بررسی کنیم:

  • query - نوع دستور یا Operation Type: نوع دستور میتونه یکی از query، mutation یا subscription باشه. خیلی خلاصه، نوع دستور در واقع به موتور میگه که شما چه کاری رو میخواید انجام بدید. این رو هم بگم که اگر چیزی بجاش نذارید، موتور به صورت اتوماتیک query رو در نظر میگیره.
  • basicQuery - نام دستور یا Operation name: هرچیزی میتونه باشه، فقط برای راهنمایی و شناخت خود شماست. مجبور هم نیست حتما یه چیزی باشه.
  • user، name، email و avatar - فیلد یا Field: یه تیکه‌ای از داده‌ای که درخواستش رو دادید. یادتون باشه که این‌ها همون فیلد‌هایی هستن که در نهایت شما تو پاسخ از سرور دریافت میکنید.
  • (id: 1234)، (size: SMALL) - آرگومان‌ها یا Arguments: یک ست از کلید-مقدارها (key-value sets) که به یک فیلد میچسبن. اونا سمت سرور اجرا میشن و میتونن باعث تغییر تو جواب فیلدشون بشن. گرچه درست اینه که بهشون بگیم دایرکتیو یا Directive.

یه چیز مهم اینه که، یه فیلد موقع پیاده‌سازی سمت سرور، معادل یه فانکشن میشه. یعنی ما سمت سرور برای هر فیلد یه فانکشن جدا میسازیم.

این دستور گراف‌کیو‌ال که بالا اومده، یه چیز خیلی کلی ولی باز هم با یه دقت خوب از آنچه که یه دستور گراف‌کیو‌ال میتونه باشه رو نشون میده.

با یک نمونه جلو بریم

خب برای اینکه بهتر متوجه بشید، بریم سراغ یک نمونه از قبل آماده شده. میتونید به آدرس https://graphqlzero.almansi.me/api برید و اونجا Playground گراف‌کیو‌ال رو ببینید.

یه موردی که لازمه بگم، اینه که این Playground قالبا رو سرورهای گراف‌کیو‌ال هست و برنامه‌نویس‌ها میتونن خودشون به دلخواه روشن یا خاموششون کنن. با این حال، تا به مثال خودمون نرسیدیم، با ای‌پی‌آی‌های این سرویس جلو میریم.

برای شروع دستور زیر رو اونجا وارد کنید و جوابش رو ببینید:

1{
2 posts(options: { slice: { limit: 3 } }) {
3 meta {
4 totalCount
5 }
6 data {
7 title
8 id
9 user {
10 name
11 }
12 }
13 }
14}

خب، برای شروع ما داریم یک query بدون اسم میفرستیم. به آرگومان‌های فیلد posts دقت کنید! این روش یک راه برای فیلتر کردن جواب‌های سرور هست. مثلا من اینجا به posts گفتم که جواب‌هاش رو slice کنه، یا در واقع برش بده و در حقیقت تعدادشون [جواب‌ها] رو فقط سه تا کنه.

چیزی که هست اینه که این ساختار رو خود برنامه‌نویس‌ها برای این سرویس درست کردن، یعنی الزاما شما به (مثلا) آرگومان slice تو یه سرور گراف‌کیو‌ال دیگه دسترسی ندارید.

خب حالا بیاید و یکم این دستور رو دستکاری کنیم. بیاین تجسم کنیم که مثلا میخواستیم دو سری post دریافت کنیم. سری اول اونایی که آی‌دی‌شون بین ۱ تا ۳ هست، و سری دوم ۴تا از آخرین پست‌ها. فرضا این درخواست مشتری ما بوده یا لازم بوده چنین چیزی فرستاده بشه تا صفحه‌ی اول رو طراحی کنیم!

خب میتونیم همچین چیزی رو ارسال کنیم (حتما انجام بدید چون میخوام نتیجه رو ببینید)

1{
2 posts(options: { slice: { start: 1, end: 3 } }) {
3 data {
4 title
5 }
6 }
7
8 posts(options: { slice: { limit: 4 }, sort: { order: DESC } }) {
9 data {
10 title
11 }
12 }
13}

این رو ارسال کنید، یکم صبر میکنم…

دیدید؟ ارور داد! اما چرا؟ دلیلش اینه که شما به سرور دارید میگید که دقیقا یک فیلد رو میخواید اما با دوتا آرگومان کاملا متفاوت! و خب سرور با خودش میگه: بالاخره من کدوم رو باید بدم؟

اصلا فرض کنیم سرور جواب هم داد، جواب آخر یک JSON هست که دقیقا دوتا کلید posts کنار هم داره! این JSON اساسا دیگه درست یا Valid نیست!

اما راه حل چیه؟

آلیاس یا Alias

آلیاس کردن تو گراف‌کیو‌ال به این معنیه که شما میتونید اسم واقعی یک فیلد رو، به اون چیزی که میخواید تغییر بدید تا جوابتون رو دریافت کنید. بیاین با یک مثال نشونتون بدم:

1{
2 p1: posts(options: { slice: { start: 1, end: 3 } }) {
3 data {
4 title
5 }
6 }
7
8 p2: posts(options: { slice: { limit: 4 }, sort: { order: DESC } }) {
9 data {
10 title
11 }
12 }
13}

اینجا به p1 و p2 کنار کلمات posts دقت کنید. این دوتا در حال حاضر آلیاس‌های posts هستن. الان مجدد درخواست رو بفرستید و نتیجه رو ببینید.

توی جوابی که گرفتید، اینبار به جای posts، کلیدهای p1 و p2 رو دارید.

آلیاس کردن رو میتونید روی همه‌ی فیلدها انجام بدید!

خب، تا اینجا که با آلیاسینگ آشنا شدید. حالا بیاین تجسم کنیم که مثلا قرار بود سه جور پست رو دریافت کنیم، دقیقا مشابه درخواست بالا، فقط یه پست دیگه هم اضافه میشه:

1{
2 p1: posts(options: { slice: { start: 1, end: 3 } }) {
3 data {
4 title
5 }
6 }
7
8 p2: posts(options: { slice: { limit: 4 }, sort: { order: DESC } }) {
9 data {
10 title
11 }
12 }
13
14 p3: posts(options: { slice: { start: 1, end: 3 }, sort: { order: DESC } }) {
15 data {
16 title
17 }
18 }
19}

خب، تا اینجا ما سه جور پست رو دریافت میکنیم.

برنامه‌ی ما ساخته میشه و مردم ازش استفاده میکنن، اما توی بیزینس یه تغییر ایجاد میشه و لازم میشه که برای پست‌ها، در کنار عنوانشون، مثلا اسم نویسندشون هم نوشته بشه.

توی این حالت، ما باید بریم و درخواست بالا رو اینطور تغییر بدیم:

1{
2 p1: posts(options: { slice: { start: 1, end: 3 } }) {
3 data {
4 title
5 user {
6 name
7 }
8 }
9 }
10
11 p2: posts(options: { slice: { limit: 4 }, sort: { order: DESC } }) {
12 data {
13 title
14 user {
15 name
16 }
17 }
18 }
19
20 p3: posts(options: { slice: { start: 1, end: 3 }, sort: { order: DESC } }) {
21 data {
22 title
23 user {
24 name
25 }
26 }
27 }
28}

من به همه‌ی پست‌ها این رو اضافه کردم:

1user {
2 name
3}

میتونید تست کنید و نتیجه رو ببینید. اما یه مشکل کوچیک وجود داره: درسته که اضافه کردن سه تا خط واقعا کار سختی نبود، اما تو دنیای واقعی، معمولا اضافه‌کردن میتونه از این بزرگتر باشه، اما سوای این، اصولا تکرار کردن کار درستی نیست و مخالف اصول DRY (Don’t repeat yourself) یا همون «کار تکراری نکن» هست.

اما چطور میشه اینجا از تکرار جلوگیری کرد؟

فرَگمِنت‌ها یا Fragments:

فرگمنت‌ها برای جلوگیری از تکرا ایده‌آل هستن. استفاده ازشون هم به نسبت کار ساده‌ایه. در واقع ایده اینه که شما یک یا چند فرگمنت رو تعریف میکنید و در جای خودشون استفاده میکنید. یک نمونه از یک فرگمنت میتونه این شکلی باشه:

1fragment postsFragment on PostsPage {
2 data {
3 title
4 user {
5 name
6 }
7 }
8}
  • fragment: یه کلمه‌ی کلیدی مثل query، mutation و subscription که مشخص میکنه شما میخواین یه فرگمنت رو تعریف کنید.
  • postsFragment: اسم فرگمنت که میخواید بسازید.
  • on Post: شرطی که فرگمنت باید روش اعمال بشه. دقت کنید که اینجا، Post اسم فیلد نیست، بلکه type فیلده.
  • مابقی، تمامی فیلد‌هایی هستن که شما میخواین روی این تایپ استفاده کنید.

خب، کوئری رو به شکل زیر تغییر بدید و بفرستید و نتیجه رو ببینید:

1{
2 p1: posts(options: { slice: { start: 1, end: 3 } }) {
3 ...postsFragment
4 }
5
6 p2: posts(options: { slice: { limit: 4 }, sort: { order: DESC } }) {
7 ...postsFragment
8 }
9
10 p3: posts(options: { slice: { start: 1, end: 3 }, sort: { order: DESC } }) {
11 ...postsFragment
12 }
13}
14
15fragment postsFragment on PostsPage {
16 data {
17 title
18 user {
19 name
20 }
21 }
22}

متغیر‌ها یا Variables:

خب تا اینجا ما با کوئری‌های مختلفی کار کردیم، و در حقیقت، یه استرینگ رو برای سرور فرستادیم تا اون هم به ما جواب بده (یادمون نره که داریم با پروتکل HTTP کار میکنیم). اما تو دنیای واقعی، اکثر برنامه‌ها کوئری‌ها رو بر اساس متغیر‌ها مسازن، یا حداقل بخشیش رو. برای مثال، وقتی که میخواید به یوزر امکان فیلتر کردن رو بدید.

این ایده هم خوب نیست که مستقیما این کوئری رو دستکاری کنیم تا هرچی کاربر بهش میده رو بفرسته برای سرور. برای اینکار، خود گراف‌کیو‌ال اومده و متغیر‌ها رو برای خودش تعریف کرده.

وقتی که میخوایم با متغیرها کار کنیم، ۳ کار رو باید حتما انجام بدیم:

  1. مقدار ثابتی که توی کوئری هست رو با $esmeMoteghayer عوض کنیم،
  2. متغیر $esmeMoteghayer رو به عنوان یکی از پارامترهای کوئری بهش پاس بدیم و
  3. مقدار esmeMoteghayer (از عمد علامت دلار رو برداشتم) رو به شکل JSON به همراه کوئری ارسال کنیم.

تو نگاه اول ممکنه سخت و حتی عجیب برسه، اما واقعیت اینه که آسونتر از چیزیه که به نظر میاد! کوئری بالا رو دوباره مینویسم:

1query getPosts($start: Int, $end: Int, $limit: Int, $order: SortOrderEnum) {
2 p1: posts(options: { slice: { start: $start, end: $end } }) {
3 ...postsFragment
4 }
5
6 p2: posts(options: { slice: { limit: $limit }, sort: { order: $order } }) {
7 ...postsFragment
8 }
9
10 p3: posts(
11 options: { slice: { start: $start, end: $end }, sort: { order: $order } }
12 ) {
13 ...postsFragment
14 }
15}
16
17fragment postsFragment on PostsPage {
18 data {
19 title
20 user {
21 name
22 }
23 }
24}

تا اینجا قدم ۱ و ۲ رو انجام دادیم. حالا برای شماره ۳، پایین صفحه رو ببینید. میتونید تو قسمت Query Variables مقادیر رو به شکل JSON بنویسید. مثل شکل زیر:

image-20210130133919755

تا اینجا با کوئری‌ها آشنا شدیم، اما چی میشه اگه بخوایم چیزها رو اینبار تغییر بدیم؟

تغییرات یا Mutations

تو ساختار REST، وقتی میخوایم اطلاعاتی رو از سرور بگیریم، از GET و همینطور اگر بخوایم چیزی رو تغییر بدیم (بسازیم، تغییر بدیم و پاک کنیم) از POST، PUT و DELETE استفاده میکنیم. معادل همه‌ی اینا تو گراف‌کیو‌ال میشه Queryها و Mutationها.

بیاین خیلی ساده، اولین متغییر رو ایجاد کنیم:

1mutation newMutation {
2 createPost(
3 input: { title: "Man onvan hastamn", body: "Va in ham matne poste" }
4 ) {
5 id
6 title
7 body
8 }
9}

مثل کوئری گرفتن، ما میتونیم فیلد‌های اون شئ که ساختیم یا تغییر دادیم رو همونجا درخواست بدیم. یه نکته اینکه تمام ویژگی‌های کوئری کردن، اینجا هم برقرارن مثل استفاده از متغیرها!

تایپ سیستم در گراف‌کیو‌ال

سرورهای گراف‌کیو‌ال رو با هر زبانی میشه ساخت، از اینجاست که نمیشه به تایپ سیستم زبان‌ها برای خود گراف‌کیو‌ال اتکا کرد، به این منظور که خب آخرش کی داره درست میگه؟‌ استاندارد کی باید رعایت بشه؟

برای همین هم گراف‌کیو‌ال تایپ سیستم خودش رو ساخته که من تو این بخش به اون میرسم.

اشیا یا ‌Objects

اساسی‌ترین قسمت تشکیل‌دهنده‌ی گراف‌کیو‌ال اشیا هستن. اشیا همون چیزهایی هستن که شما میخواین از سرور دریافت کنید، که خب واضحه به همین دلیل اشیا، شامل فیلد‌ها هم میشن، مثلا:

1type Product {
2 name: String!
3 price: Int!
4 tags: [String!]!
5}

بریم ببینیم اینا چی هستن:

  • Product: یک شی گراف‌کیو‌ال هست.
  • name، price و tags: فیلدهایی هستن که شی ما توی خودش داره.
  • String: یک تایپ استاندارد تو خود گراف‌کیو‌ال هست (جز تایپ‌های Scalar).
  • String! و Int!: به این معنیه که این فیلد یک استرینگه یا عدد صحیح که خالی نیست و به اصطلاح non-nullable هست، یعنی سرور حتما به شما یه چیزی رو برمیگردونه.
  • [String!]!: یک آرایه از استرینگ‌ها، و چون خالی نیست، میشه همیشه انتظار یک آرایه رو در پاسخ داشت (با هیچ یا چند استرینگ داخلش).

آرگومان‌ها یا Arguments

این امکان برای هر فیلد وجود داره که بتونه هیچ یا چند آرگومان داشته باشه که اونا هم میتونن هرکدومشون اجباری یا دلخواه باشن. این آرگومان‌ها کمک میکنن تا سمت سرور تغییرات لازم روی فیلد اعمال بشه و جواب مورد نظر برگرده. یک نمونش میتونه این باشه:

1type Product {
2 name: String!
3 price(exchange: ExchangeType = EUR): Int!
4 tags: [String!]!
5}

تو مثال بالا، price یک آرگومان دلخواه داره به اسم exchange از نوع ExchangeType (یه تایپ فرضی) که در حالت عادی، یا درواقع وقتی داده نشه مقدارش EUR هست. مثلا قراره که ما بر مبنای مقدار این آرگومان، ثیمت محصول رو به ارزهای مختلف برگردونیم. اگر اینجا EUR رو نمیذاشتیم، یعنی این آرگومان اجباری بود و باید مقدار دهی میشد.

تایپ Query و Mutation

یک تایپ مخصوص تو گراف‌کیو‌ال وجود داره به اسم schema که در واقع نقطه شروع گراف‌کیو‌ال محسوب میشه. ما معمولا تو این شی کوئری‌ها و میوتیشن‌ها رو میذاریم:

1schema {
2 query: Query
3 mutation: Mutation
4}

و بعد

1type Query {
2 products: [Product!]!
3 product(id: Int!): Product!
4}
5
6type Mutation {
7 createProduct(input: CreateProductInput): Product!
8 updateProduct(input: UpdateProductInput): Product!
9 deleteProduct(input: DeleteProductInput): Product!
10}

در نهایت میشه گفت که چیزی که زیر مینویسم (که هنوز ناقصه و تو مسیرمون کاملش میکنیم)، ساختار و شِمای گراف‌کیو‌ال محسوب میشه:

1type Product {
2 id: ID!
3 name: String!
4 price(exchange: ExchangeType = EUR): Int!
5 tags: [String!]!
6}
7
8type Query {
9 products: [Product!]!
10 product(id: Int!): Product!
11}
12
13type Mutation {
14 createProduct(input: CreateProductInput): Product!
15 updateProduct(input: UpdateProductInput): Product!
16 deleteProduct(input: DeleteProductInput): Product!
17}
18
19schema {
20 query: Query
21 mutation: Mutation
22}

گرچه برای تست کردنش لازم هست که سرور خودمون رو بسازیم، اما به این هم خواهیم رسید! اما اینجا ما در واقع شِمای یک گراف رو ساختیم که بتونیم ازش مثلا اینطوری استفاده کنیم:

1{
2 products {
3 name
4 price(exchange: TOMAN)
5 }
6}

دقت کنید که این مثال هنوز کامل نیست و بخشهاییش رو لازمه بعدا بسازیم! (مثل CreateProductInput)

تایپ‌های اسکالر یا Scalar

تایپ‌های اسکالر، تایپ‌های ساده و جزئی هستن که توی گراف‌کیو‌ال استفاده میشن تا انواع مختلفی رو تعریف کنن. این تایپ‌ها کلا به دو دسته تقسیم میشن، تایپ‌های استاندارد و تایپ‌های دستی یا Manual

تایپ‌های استاندارد

  • Int: عدد صحیح (۳۲ بیت)
  • Float: عدد اعشاری
  • String: رشته‌ی کاراکتری مبتنی بر UTF-8
  • Boolean: عدد ۲ بیتی، true یا false
  • ID: یه تایپ اسکالر که به یه نوع یونیک یا unique اشاره داره که با وجود اینکه باهاش مثل String برخورد میشه، اما الزاما قابل خوندن برای انسان نیست.

تایپ‌های دستی

گاهی لازم میشه که ما تایپ‌های خاص خودمون رو بسازیم، برای همین میشه از دستور زیر استفاده کرد:

1scalar Date

که در نهایت میشه موقع ایمپلمنت کردن برنامه، تعیین کرد که حین برخورد با این تایپ باید چه کرد.

اینام‌ها یا Enumerations

نوع خاصی از تایپ‌های اسکالر هستن که مقادیر خاصی رو میتونن داشته باشن. یعنی خارج از مجموعه‌ی خودشون، نمیشه بهشون مقدار دیگه‌ای داد، تو مثالی که قبلا زدم، میشه به ExchangeType اشاره کرد که در واقع یک enum بوده:

1enum ExchangeType {
2 EUR
3 TOMAN
4 USD
5}

پس در واقع، یک قدم کاملتر شده‌ی شِمای ما میشه:

1enum ExchangeType {
2 EUR
3 TOMAN
4 USD
5}
6
7type Product {
8 id: ID!
9 name: String!
10 price(exchange: ExchangeType = EUR): Int!
11 tags: [String!]!
12}
13
14type Query {
15 products: [Product!]!
16 product(id: Int!): Product!
17}
18
19type Mutation {
20 createProduct(input: CreateProductInput): Product!
21 updateProduct(input: UpdateProductInput): Product!
22 deleteProduct(input: DeleteProductInput): Product!
23}
24
25schema {
26 query: Query
27 mutation: Mutation
28}

اینترفیس‌ها یا Interfaces

مثل خیلی از تایپ‌سیستم‌های دیگه، گراف‌کیو‌ال از اینترفیس‌ها هم پشتیبانی میکنه. اینترفیس شبیه به یکجور قرارداد میمونه که، اگه با زبان‌های OOP آشنا باشید، باعث میشه هرچیزی که ازش استفاده کنه، ناچار به رعایت قوانین اون میشه. میتونید یه سری به این مطلب ستاره بهزادی بزنید تا با مفهومش بیشتر آشنا بشید.

اینترفیس‌ها در گراف‌کیو‌ال مجموعه‌ای از فیلدها هستن که وقتی به آبجکتی که ازشون استفاده کنه، به نوعی تحمیل میشن. فکر کنم یه مثال منظورم رو بهتر برسونه:

1interface Model {
2 id: ID!
3}

برای مثال، این اینترفیس یک مدل پایه رو تعریف میکنه که حتما آی‌دی داره. حالا من میتونم در مورد تایپ Product اینطور بگم:

1type Product implements Model {
2 id: ID!
3 name: String!
4 price(exchange: ExchangeType = EUR): Int!
5 tags: [String!]!
6}

یعنی هر تایپی که اصطلاحا Model رو پیاده‌سازی یا implement کنه، باید حتما id: ID! رو توی خودش داشته باشه.

حالا تو این مثالی که در مورد محصولات زدم، چون مدل‌ها کلا ساده هستن شاید استفاده از اینترفیس ایده‌ی خیلی خوبی نباشه، اما برای اینکه باهاش آشنا باشید کافیه.

تایپ‌های ورودی یا Input Types

تا اینجا تمام چیزهایی که نشونت دادم، مربوط به کوئری‌ها بودن. اینجا میخوام درمورد تایپ‌های ورودی بنویسم که بیشترین کاربردشون تو Mutationهاست. inputها دقیقا مثل typeها تعریف میشن، با این تفاوت که به جای type از input استفاده میشه براشون:

1input CreateProductInput {
2 name: String!
3 price: Int!
4 tags: [String!]!
5}
6
7input UpdateProductInput {
8 name: String
9 price: Int
10 tags: [String!]
11}
12
13input DeleteProductInput {
14 id: ID!
15}

پس باز هم با یک تغییر تو شِما مواجه میشیم:

1enum ExchangeType {
2 EUR
3 TOMAN
4 USD
5}
6
7input CreateProductInput {
8 name: String!
9 price: Int!
10 tags: [String!]!
11}
12
13input UpdateProductInput {
14 name: String
15 price: Int
16 tags: [String!]
17}
18
19input DeleteProductInput {
20 id: ID!
21}
22
23type Product implements Model {
24 id: ID!
25 name: String!
26 price(exchange: ExchangeType = EUR): Int!
27 tags: [String!]!
28}
29
30type Query {
31 products: [Product!]!
32 product(id: Int!): Product!
33}
34
35type Mutation {
36 createProduct(input: CreateProductInput): Product!
37 updateProduct(input: UpdateProductInput): Product!
38 deleteProduct(input: DeleteProductInput): Product!
39}
40
41schema {
42 query: Query
43 mutation: Mutation
44}

تا اینجا با اساس کار گراف‌کیو‌ال آشنا شدیم، خیلی خلاصه البته، ولی دیگه تئوری بسه! بریم که دستمون رو به کد آلوده کنیم.

یک سرور گراف‌کیو‌ال بسازیم!

تا اینجا با ساختار گراف‌کیو‌ال آشنا شدیم، اما واقعا تا وقتی کد نزنیم، تئوری هیچ کاربردی نداره (یا حداقل کاربرد زیادی نداره). من اینجا تمرکزم روی ساخت یه سرور گراف‌کیو‌ال با جاوا‌اسکریپته، گرچه پروسه واسه زبان‌های دیگه هم یکسانه، اما چون سرعت کار با جاوا‌اسکریپت بالا میره، منم رو این زبان تمرکز میکنم.

ساده‌ترین شیوه برای شروع، استفاده از کتاب‌خونه‌ی Express هست که حدس میزنم باهاش آشنایی دارید. اگر نه، میتونید توی کامنت‌ها برام بنویسید تا بعد در موردش توضیح بدم. بیاین خیلی ساده از ساخت خود فولدر پروژه شروع کنیم:

1mkdir my-express-app && cd my-express-app

تو قدم بعد، یه پروژه‌ی npm رو راه‌اندازی میکنیم. من از آرگومان y استفاده میکنم که سریع پروژه ساخته بشه، میتونید اگر دوست دارید این آرگومان رو نذارید.

1npm init -y

خب، تو گام بعدی، پکیج‌هایی که نیاز داریم رو نصب میکنیم. پکیج‌هایی که لازم داریم:

  • express: که خود فریم‌ورک ماست و همه‌چیز رو این بستر اجرا میشه،
  • graphql: موتور اصلی گراف‌کیو‌ال،
  • graphql-tools: ابزاریه که کمک میکنه شما schema رو تعریف کنید و تو کد ازش استفاده کنید،
  • express-graphql: یه سرور http برای گراف‌کیو‌ال میسازه و اون رو به عنوان یه middleware ارائه میده،
  • sqlite3: بایندیگ‌ها یا Bindings برای دیتابیس sqlite رو بستر node.
1npm install express graphql graphql-tools express-graphql sqlite3

تا اینجا همه‌چیز خوب بود. از اینجا به بعد میریم سراغ کد زدن، هدفم اینه که قدم به قدم چیزهایی که میخوایم رو بسازیم، بجای اینکه از بالای فایل شروع کنیم بیایم پایین.

ساخت شِما

برای شروع، یه فایل schema.graphql بسازید و این رو داخلش بذارید:

1enum ExchangeType {
2 EUR
3 TOMAN
4 USD
5}
6
7interface Model {
8 id: ID!
9}
10
11input CreateProductInput {
12 name: String!
13 price: Int!
14 tags: [String!]!
15}
16
17input UpdateProductInput {
18 id: ID!
19 name: String
20 price: Int
21 tags: [String!]
22}
23
24input DeleteProductInput {
25 id: ID!
26}
27
28type Product implements Model {
29 id: ID!
30 name: String!
31 price(exchange: ExchangeType = EUR): Int!
32 tags: [String!]!
33}
34
35type Query {
36 products: [Product!]!
37 product(id: Int!): Product!
38}
39
40type Mutation {
41 createProduct(input: CreateProductInput): Product!
42 updateProduct(input: UpdateProductInput): Product!
43 deleteProduct(input: DeleteProductInput): Int!
44}
45
46schema {
47 query: Query
48 mutation: Mutation
49}

مدل سازی

یکی از کارهایی که من معمولا تو طراحی سیستم‌های گراف‌کیو‌ال انجام میدم، اینه که دقیقا مدل‌هایی که تو دیتابیس‌هستن رو با گراف‌کیو‌ال تعریف میکنم. مثلا ما تو شِما، یکی از مدل‌هایی که داریم، همین مدل Product هست. از دید ما Product این ستو‌ها رو تو دیتابیس داره:

  • id: از جنس int که auto increment باید باشه،
  • name: از جنس string،
  • price: از جنس int و
  • tags: یه آرایه از stringها.

برای ساخت مدل، و پر کردن دیتابیس، من از یه سرویس که دیتای تست یا Mock میسازه استفاده کردم. مدل رو تو فایل ./db/init.sql میسازیم:

1CREATE TABLE IF NOT EXISTS `product` (
2 `id` INTEGER PRIMARY KEY,
3 `name` TEXT default NULL,
4 `price` mediumint default NULL,
5 `tags` varchar(255) default NULL
6);
7
8INSERT INTO `product` (`id`,`name`,`price`,`tags`) VALUES (1,"auctor",4661,"tag8, tag1, tag9"),(2,"rutrum. Fusce",4860,"tag9, tag1, tag3, tag7"),(3,"dui. Cras pellentesque.",8395,"tag2, tag7, tag6, tag1"),(4,"feugiat. Lorem",923,"tag4, tag3, tag5"),(5,"auctor, velit eget",2164,"tag5, tag6"),(6,"ultrices sit amet,",7765,"tag0, tag2"),(7,"at pretium aliquet,",6624,"tag9"),(8,"tempus scelerisque, lorem",9379,""),(9,"sit amet",7451,"tag9, tag6, tag0, tag5"),(10,"venenatis vel,",7578,"tag0, tag9");
9INSERT INTO `product` (`id`,`name`,`price`,`tags`) VALUES (11,"euismod mauris",777,"tag9, tag8"),(12,"consequat",9275,"tag1, tag6, tag7"),(13,"porttitor tellus non",1896,"tag4, tag2, tag9"),(14,"odio tristique",2227,"tag2, tag4, tag0"),(15,"Phasellus at augue",2491,"tag2, tag9"),(16,"tempor",8325,"tag2"),(17,"quam. Curabitur",5642,"tag1, tag0, tag8, tag3"),(18,"malesuada id, erat.",2222,"tag3, tag0, tag1"),(19,"lorem,",1591,"tag9, tag5, tag2"),(20,"nisi. Mauris",4577,"tag4");
10INSERT INTO `product` (`id`,`name`,`price`,`tags`) VALUES (21,"arcu.",9465,""),(22,"aliquet lobortis, nisi",1820,"tag1, tag5, tag7, tag3"),(23,"justo. Proin non",6295,"tag3, tag5, tag1, tag0"),(24,"mi",4510,"tag7"),(25,"nisl.",5086,"tag8, tag6, tag1, tag2"),(26,"semper auctor.",6952,"tag0, tag1, tag6"),(27,"elit sed consequat",6675,"tag1, tag3, tag4"),(28,"habitant morbi",4536,"tag5"),(29,"arcu. Sed",3924,""),(30,"tincidunt",8435,"");
11INSERT INTO `product` (`id`,`name`,`price`,`tags`) VALUES (31,"neque venenatis lacus.",6481,"tag9, tag0, tag1, tag2"),(32,"consectetuer mauris",4949,"tag9, tag0"),(33,"sit amet",6355,""),(34,"blandit enim",4081,"tag6, tag0"),(35,"tristique neque",2594,"tag6"),(36,"mus. Proin",5844,"tag6"),(37,"dolor.",3543,""),(38,"feugiat. Sed",1226,"tag6"),(39,"lectus.",4872,""),(40,"Phasellus at augue",1969,"");
12INSERT INTO `product` (`id`,`name`,`price`,`tags`) VALUES (41,"ligula. Aliquam erat",3324,"tag1, tag0, tag6"),(42,"tincidunt adipiscing.",924,"tag2"),(43,"suscipit nonummy. Fusce",6192,"tag5, tag3"),(44,"Phasellus nulla. Integer",7742,"tag4, tag7, tag2, tag1"),(45,"urna",2770,"tag3, tag9, tag5"),(46,"enim. Nunc",1122,"tag5, tag1, tag8"),(47,"cursus",9120,"tag0, tag5, tag1, tag9"),(48,"Morbi non",1652,"tag8"),(49,"in faucibus orci",4071,"tag8"),(50,"Pellentesque",3361,"tag7");
13INSERT INTO `product` (`id`,`name`,`price`,`tags`) VALUES (51,"Proin",2320,"tag2, tag1, tag0"),(52,"at",5180,""),(53,"arcu. Aliquam",1238,"tag2, tag5, tag7, tag8"),(54,"pharetra nibh.",2584,"tag3, tag1, tag5, tag4"),(55,"arcu imperdiet",9808,""),(56,"non lorem",604,"tag0, tag6, tag3, tag9"),(57,"a, enim. Suspendisse",5202,"tag8, tag4, tag0, tag9"),(58,"lacus.",912,"tag0, tag5, tag3, tag6"),(59,"sapien. Nunc",8340,""),(60,"leo, in",4262,"tag0, tag9, tag7, tag4");
14INSERT INTO `product` (`id`,`name`,`price`,`tags`) VALUES (61,"morbi tristique senectus",3676,"tag5, tag6"),(62,"imperdiet",8327,"tag9, tag1"),(63,"libero. Morbi",7886,"tag3, tag2"),(64,"quis",6284,""),(65,"ut",6209,""),(66,"Proin sed turpis",4068,"tag7, tag0, tag9"),(67,"lobortis. Class aptent",6906,"tag8"),(68,"tempus",5612,"tag2, tag1, tag3"),(69,"sem,",4759,"tag5, tag2, tag8, tag7"),(70,"senectus et",509,"tag1, tag4, tag6");
15INSERT INTO `product` (`id`,`name`,`price`,`tags`) VALUES (71,"nec,",5804,"tag4"),(72,"ligula consectetuer rhoncus.",7564,"tag2, tag0, tag8"),(73,"et pede.",3503,"tag0, tag8, tag4, tag9"),(74,"commodo ipsum. Suspendisse",1923,""),(75,"Nunc lectus",6269,"tag3, tag1, tag8, tag7"),(76,"euismod",2400,"tag5, tag7, tag6, tag4"),(77,"mattis. Integer",5604,"tag5, tag2, tag8"),(78,"orci. Phasellus dapibus",6330,"tag9, tag6"),(79,"magna. Sed eu",4409,""),(80,"placerat eget, venenatis",4096,"tag8, tag7, tag6");
16INSERT INTO `product` (`id`,`name`,`price`,`tags`) VALUES (81,"ac mattis ornare,",2950,"tag8, tag5"),(82,"nonummy ut,",5955,"tag8"),(83,"a, enim. Suspendisse",2660,"tag4, tag8"),(84,"mus.",4906,"tag1, tag3, tag9, tag2"),(85,"non quam.",9946,"tag8, tag9, tag2"),(86,"a neque. Nullam",8442,"tag2, tag8, tag0"),(87,"enim. Mauris quis",233,"tag5, tag4, tag2, tag6"),(88,"dui,",8889,"tag8"),(89,"ligula. Aenean gravida",5359,"tag6, tag0"),(90,"ut odio",4962,"tag4, tag8, tag3");
17INSERT INTO `product` (`id`,`name`,`price`,`tags`) VALUES (91,"leo.",5818,""),(92,"bibendum fermentum metus.",1530,""),(93,"a, scelerisque sed,",4578,"tag9, tag4, tag8, tag0"),(94,"et,",785,"tag8, tag3, tag2"),(95,"volutpat",203,""),(96,"at auctor ullamcorper,",2136,"tag1, tag7, tag6, tag3"),(97,"Phasellus dolor",1247,"tag6, tag1, tag9"),(98,"at, egestas",5202,"tag3, tag9, tag2"),(99,"est",5585,"tag5"),(100,"Proin eget",9560,"tag8, tag9");

فایل index.js رو بسازید و مدل‌سازی رو از اونجا شروع کنید:

1const sqlite = require("sqlite3").verbose()
2const fs = require("fs")
3
4const db = new sqlite.Database("./db/db.sqlite")
5
6if (process.env.INIT_DB) {
7 const data = fs.readFileSync("./db/init.sql", {
8 encoding: "utf8",
9 flag: "r",
10 })
11
12 console.log("running database migration")
13 db.exec(data)
14}

دلیل اینکه من از process.env.INIT_DB استفاده می‌کنم، اینه که میخوام هرموقع که از دستور زیر رو زدم، جدول Product ساخته بشه:

1INIT_DB=true node index.js

اگر INIT_DB=true نباشه، و فقط دستور node index.js رو بزنیم، برنامه عادی اجرا میشه و تغییری تو دیتابیس اتفاق نمیوفته.

خب تا اینجا مدل‌سازی انجام شد و ما یه دیتابیس برای شروع داریم. حتما یه بار دستور بالا رو اجرا کنید!

ساخت سرور

خب بریم که یه سرور ساده رو بسازیم. فایل index.js رو یه آپدیت میکنیم:

1const sqlite = require("sqlite3").verbose()
2const fs = require("fs")
3const express = require("express")
4
5const db = new sqlite.Database("./db/db.sqlite")
6
7if (process.env.INIT_DB) {
8 const data = fs.readFileSync("./db/init.sql", {
9 encoding: "utf8",
10 flag: "r",
11 })
12
13 console.log("running database migration")
14 db.exec(data)
15}
16
17const app = express()
18const PORT = process.env.PORT || 3000
19
20app.get("/status", (req, res) => {
21 res.json({
22 ok: true,
23 })
24})
25
26app.listen(PORT, () => {
27 console.log(`server started on http://localhost:${PORT}`)
28})

برای آزمایش، http://localhost:3000 رو باز کنید ببینیم آیا سیستم کار میکنه یا نه. اگر احیانا به مشکلی خوردید، توی کامنت‌ها برام بنویسین.

کاری که کردیم، این بوده که یه سرور اکسپرس رو ساختیم و یه مسیر /status رو گذاشتیم که فقط ببینیم آیا سیستم کار میکنه یا نه.

تنظیمات برای گراف‌کیو‌ال

یکی از اسم‌هایی که برای شِما میذارن، Type Definition، تایپ دِفینیشِن یا تعریف تایپ (؟) هست. کاری که تو این مرحله انجام میدیم، اون فایل شِما رو میخونیم و تو یه متغیر میذاریم، یه ریزالوِر یا Resolver درست میکنیم و در نهایت این دو (متغیر و ریزالوِر) رو به هم مَپ یا map میکنیم و در نهایت تو تنظیمات گراف‌کیو‌ال میذاریمشون.

ریزالور، مجموعه‌ی تمام فانکشن‌هایی هست که برای فیلد‌ها استفاده میشن. چه کوئری چه میوتیشن.

بریم شروع کنیم، یه فایل جدید به اسم resolvers.js بسازید و اینا رو توش بنویسید:

1module.exports = {
2 Query: {
3 products: (root, args, context) => {
4 return []
5 },
6 },
7}

این فایل رو توی index.js اضافه میکنیم، تایپ‌دفینیشن‌ها رو بهشون مَپ میکنیم و میدیمشون به اکسپرس:

1const sqlite = require("sqlite3").verbose()
2const fs = require("fs")
3const express = require("express")
4const { makeExecutableSchema } = require("graphql-tools")
5const resolvers = require("./resolvers")
6const graphql = require("graphql")
7const { graphqlHTTP } = require("express-graphql")
8
9const db = new sqlite.Database("./db/db.sqlite")
10
11if (process.env.INIT_DB) {
12 const data = fs.readFileSync("./db/init.sql", {
13 encoding: "utf8",
14 flag: "r",
15 })
16
17 console.log("running database migration")
18 db.exec(data)
19}
20
21const app = express()
22const PORT = process.env.PORT || 3000
23
24const typeDefinitions = fs.readFileSync("./schema.graphql")
25const schema = makeExecutableSchema({
26 typeDefs: String(typeDefinitions),
27 resolvers,
28})
29
30app.use(
31 "/graphql",
32 graphqlHTTP({
33 schema,
34 graphiql: true,
35 })
36)
37
38app.get("/status", (req, res) => {
39 res.json({
40 ok: true,
41 })
42})
43
44app.listen(PORT, () => {
45 console.log(`server started on http://localhost:${PORT}`)
46 console.log(`graphql server can be found in http://localhost:${PORT}/graphql`)
47})

خب، اینجا چی شد؟ ما اول هر آنچه که توی فایل resolvers داشتیم رو اینجا require کردیم. بعد محتويات فایل schema.graphql رو ریختیم تو متغیر typeDefinition. بعد از فانکشن makeExecutableSchema استفاده کردیم، که تایپ‌دفینیشن‌ها رو به ریزالورها مَپ کنیم. این فانکشن کارش اینه که بعد از مَپ کردن، خروجی بسازه که گراف‌کیو‌ال بتونه ازش استفاده کنه.

تو قدم بعد، از فانکشن graphqlHTTP استفاده کردیم، که یه middleware میسازه برای گراف‌کیو‌ال و میده به اکسپرس. یه آبجکت برای تنظیماتش میگیره، یک یکی از پراپرتی‌های این آبجکت، همون خروجی makeExecutableSchema هست.

اما چیزی که اینجا شاید براتون جالب باشه، پراپرتی graphiql هست.graphiql یا گرافی‌کیو‌ال، یه IDE تحت وب برای برای گراف‌کیو‌ال که خصوصا برای تست ساخته شده. معمولا درستش اینه که برای تنظیمات سرور اینطور ازش استفاده بشه:

1app.use(
2 "/graphql",
3 graphqlHTTP({
4 schema,
5 graphiql: process.env.NODE_ENV === "development",
6 })
7)

خب. میتونید سرور رو اجرا کنید، آدرس http://localhost:3000/graphql رو تو مروگرتون باز کنید و محیط رو یکم تست کنید. اگر مشکلی بود، میتونید توی کامنتها بنویسید.

کار روی ریزالورها

خب، تا اینجا ما یه سرور کارآمد گراف‌کیو‌ال داریم. از این مرحله به بعد، کار ما تو فایل resolvers.js ادامه پیدا میکنه. فقط قبلش یه تغییر کوچیک باید به سیستم بدیم. برای دیتا‌بیس باید یه فایل جداگانه درست کنیم، یا در واقع ماژول جدا بسازیم که تو همه‌ی فایلا بشه ازش استفاده کرد. فایل db/db.js رو بسازید و این تغییرات رو توش بدید:

1const path = require("path")
2const sqlite = require("sqlite3").verbose()
3const db = new sqlite.Database(
4 path.resolve(__dirname, "db.sqlite"),
5 sqlite.OPEN_READWRITE,
6 err => {
7 if (err) {
8 console.log(err)
9 }
10 console.log("connected to the database")
11 }
12)
13
14module.exports = db

خب، و یک تغییر تو index.js:

1const fs = require("fs")
2const express = require("express")
3const { makeExecutableSchema } = require("graphql-tools")
4const graphql = require("graphql")
5const { graphqlHTTP } = require("express-graphql")
6const db = require("./db/db")
7const resolvers = require("./resolvers")
8
9if (process.env.INIT_DB) {
10 const data = fs.readFileSync("./db/init.sql", {
11 encoding: "utf8",
12 flag: "r",
13 })
14
15 console.log("running database migration")
16 db.exec(String(data))
17}
18
19const app = express()
20const PORT = process.env.PORT || 3000
21
22const typeDefinitions = fs.readFileSync("./schema.graphql")
23const schema = makeExecutableSchema({
24 typeDefs: String(typeDefinitions),
25 resolvers,
26})
27
28app.use(
29 "/graphql",
30 graphqlHTTP({
31 schema,
32 graphiql: true,
33 })
34)
35
36app.get("/status", (req, res) => {
37 res.json({
38 ok: true,
39 })
40})
41
42app.listen(PORT, () => {
43 console.log(`server started on http://localhost:${PORT}`)
44 console.log(`graphql server can be found in http://localhost:${PORT}/graphql`)
45})

حالا یه آپدیت روی resolvers.js میریم:

1const db = require("./db/db")
2
3module.exports = {
4 Query: {
5 products: (root, args, context) => {
6 return new Promise((resolve, reject) => {
7 db.all("SELECT * FROM product;", (err, rows) => {
8 if (err) {
9 console.log(err)
10 reject([])
11 }
12 const result = rows.map(row => ({
13 ...row,
14 tags: row.tags.split(","),
15 }))
16
17 resolve(result)
18 })
19 })
20 },
21 },
22}

تا اینجا چه اتفاقی افتاد؟

ما اول از همه دیتا‌بیس رو require کردیم. اونچیزی که قراره در نهایت ساخته بشه، باید همون چیزی رو دنبال کنه که قرار هست شِمای ما باشه. چون تو شِما، تو فیلد query، ما products رو داریم، اینجا هم باید براش یه فانکشن بسازیم. ایده اینه که هربار که یوزر این فیلد رو کوئری میکنه، ما اینطرف از دیتابیس اطلاعات رو میگیریم و برمیگردونیم.

برای اینکار، گراف‌کیو‌ال احتیاج داره تا یک Promise رو دریافت کنه، که هرزمان که لازم بود جواب‌ها رو برگردونه. برای همین من اینجا یه Promise رو میسازم. داخل این Promise ما به دیتابیس میگیم که بره همه‌ی ردیف‌ها رو از جدول product بگیره و یک کال‌بک رو صدا بزنه، این کال‌بک دوتا پارامتر داره که اولشی اروره، و دومیش همون جوابی که میگیریم یا ستون‌ها.

اینجا یه مشکل کوچیک وجود داره، و این برمیگرده به نحوه‌ی دخیره تو ستون tags تو دیتابیسه. این ستون در واقع یه استرینگ ذخیره میکنه به جای آرایه (فکر میکنم یه راهی وجود داشته باشه تو Sqlite) که ما احتیاج هست یکبار این رو تغییر بدیم. در واقع اونجا که از map استفاده کردم برای حل این مشکل بود.

حالا برنامه رو با دستور زیر اجرا کنید:

1node index.js

و توی گرافی‌کیو‌ال، کوئری زیر رو بفرستید:

1{
2 products {
3 id
4 name
5 price
6 tags
7 }
8}

اگر مشکلی بود، توی کامنتا بنویسید.

تموم کردن ریزالور

واقعیت اینه که از اینجا به بعد، همه‌چیز به همین شکل ادامه پیدا میکنه. یعنی ایده همینه. من برای product کار رو ادامه میدم:

1product: (root, args, context) => {
2 return new Promise((resolve, reject) => {
3 db.get(`SELECT * FROM product WHERE id=${args.id}`, (err, rows) => {
4 if (err) {
5 console.log(err)
6 reject({})
7 }
8
9 const result = rows
10 result.tags = result.tags.split(",")
11
12 resolve(result)
13 })
14 })
15}

و برای اینکه تستش کنید، این کوئری رو بفرستید:

1{
2 product(id: 1) {
3 id
4 name
5 price
6 tags
7 }
8}

بریم سراغ میوتیشن‌ها، من هر آنچه که باید تا انتها نوشته بشه رو اینجا میذارم:

1const db = require("./db/db")
2
3module.exports = {
4 Query: {
5 products: (root, args, context) => {
6 return new Promise((resolve, reject) => {
7 db.all("SELECT * FROM product;", (err, rows) => {
8 if (err) {
9 console.log(err)
10 reject([])
11 }
12 const result = rows.map(row => ({
13 ...row,
14 tags: row.tags.split(","),
15 }))
16
17 resolve(result)
18 })
19 })
20 },
21 product: (root, args, context) => {
22 return new Promise((resolve, reject) => {
23 db.get(`SELECT * FROM product WHERE id=${args.id}`, (err, rows) => {
24 if (err) {
25 console.log(err)
26 reject({})
27 }
28
29 const result = rows
30 result.tags = result.tags.split(",")
31
32 resolve(result)
33 })
34 })
35 },
36 },
37 Mutation: {
38 createProduct: (root, args, context) => {
39 return new Promise((resolve, reject) => {
40 db.get(
41 `INSERT INTO "product"("name", "price", "tags") VALUES("${
42 args.input.name
43 }", ${args.input.price}, "${args.input.tags.join(",")}")`,
44 err => {
45 if (err) {
46 console.log(err)
47 reject({})
48 }
49 resolve({
50 ...args.input,
51 id: -1,
52 })
53 }
54 )
55 })
56 },
57 updateProduct: (root, args, context) => {
58 return new Promise((resolve, reject) => {
59 db.exec(
60 `UPDATE product
61 SET name = "${args.input.name}",
62 price = ${args.input.price},
63 tags = "${args.input.tags.join(",")}"
64 WHERE
65 id = ${args.input.id}`,
66 err => {
67 if (err) {
68 console.log(err)
69 reject({})
70 }
71 resolve(args.input)
72 }
73 )
74 })
75 },
76 deleteProduct: (root, args, context) => {
77 return new Promise((resolve, reject) => {
78 db.exec(
79 `DELETE FROM product
80 WHERE id = ${args.input.id};`,
81 err => {
82 if (err) {
83 console.log(err)
84 reject({})
85 }
86 resolve(args.input.id)
87 }
88 )
89 })
90 },
91 },
92}

من یه نمونه از کوئری که میتونید حالا بفرستید رو هم اینجا میذارم:

1query getAllProducts {
2 products {
3 id
4 name
5 }
6}
7
8query getOneProduct {
9 product(id: 1) {
10 id
11 name
12 }
13}
14
15mutation createNewProduct {
16 createProduct(input: { name: "Test", price: 123, tags: ["tag1", "tag2"] }) {
17 id
18 name
19 }
20}
21
22mutation updateAProduct {
23 updateProduct(
24 input: { id: 1, name: "New Name", price: 9876543, tags: ["new tags"] }
25 ) {
26 id
27 name
28 }
29}
30
31mutation deleteAProduct {
32 deleteProduct(input: { id: 1 })
33}

تست کنید و نتیجه رو ببینید.

خب، ما تونستیم با هم یه سرور کامل گراف‌کیو‌ال رو راه بندازیم. لازمه که بدم، صرفا برای آموزش، من بخش‌های پیاده‌سازی صحیح رو جا انداختم چون از ابعاد موضوع خارج بودن. اما شما چه چیزهایی به نظرتون میرسه؟ فکر میکنید کجاها چه ایرادایی وجود دارن؟ فکر میکنید میتونید پیداشون کنید؟

حتما سعی میکنم که تو یه تایم بعدی، در مورد این ایرادات و کمبود‌ها و نحوه‌ی درست پیاده‌سازیشون بنویسم.

ضمنا، برای نوشتن این مطلب زمان زیادی رو گذاشتم تا با شما به اشتراک بذارمش. اگر از این مطلب خوشتون اومده و خواستید جایی منتشرش کنید، اسم من رو فراموش نکنید.

میخوای همیشه بروز باشی؟

کافیه خیلی ساده ایمیلت رو اینجا بدی و من به محض انتشار یه پست جدید بهت خبر میدم. راستی، ایمیل رو با کسی به اشتراک نمیذارم و قرار نیست اسپم دریافت کنی.

کپی‌رایت 2021، حقوق معنوی محفوظ هست، ولی میتونید با ذکر منبع مطالب رو منتشر کنید.
Link to $https://twitter.com/aientechLink to $https://you.aien.me/joinLink to $https://www.instagram.com/aientech/Link to $https://github.com/AienTech