گرافکیوال چیست؟
گرافکیوال رو یکبار برای همیشه یاد بگیریم

برام مواردی پیش اومدن که لازم بوده برای کسی در مورد گرافکیوال یا GraphQL توضیح بدم. اینکه چرا خوبه و کجاها میتونه خیلی کمک کنه.
من اینجا اول در مورد خود گرافکیوال توضیح میدم، و تو قدم بعدی نحوهی پیاده سازی یک سرور گرافکیوال رو.
گراف کیو اِل چی هست؟
خیلی خلاصه «یه زبان کوئری گرفتن برای ایپیآی». ولی این توضیح هم خیلی کوتاهه هم اطلاعات کافی نمیده، مگه اینه اول بدونیم کامل خود گرافکیوال چی هست.
گرافکیوال یه زبانه برای ایپیآی که با دادن یک توضیح کامل از دیتایی که مد نظرتون هست، اونچه که خواستید رو در اختیارتون میذاره. در واقع به کلاینت کمک میکنه تا دقیقا بگه که چه چیزی رو میخواد و دقیقا همونچیز رو برمیگردونه.
یک مثال
فرض کنیم که یه ایپیآی مبتنی بر REST داریم. همچین چیزی:
GET /products
برای یادآوری، این سبک نوشتن یعنی شما یک درخواست GET رو مثلا به یه آدرسی مثل http://localhost:3000/products میفرستید.
درخواست رو براش میفرستیم و همچین جوابی میگیریم:
[
{
"id": 1234,
"name": "Aspire M5",
"brand": "Acer",
"category": "Laptop and Computers",
"features": {
"weight": 1.23,
"dimensions": {
"height": 23.4,
"width": 18.5,
"length": 18.3
}
},
"specs": {
"ram": 16,
"cpu": 3.5,
"cpu_count": 8,
"thread_count": 8
},
"price": 699.99
},
{...}
]
خب اینا اطلاعات یه سری لپتاپ هستن (مثلا) و ما میخوایم اینارو تو صفحه محصولاتمون نشون بدیم. اما ایده اینه که تو صفحهی اصلی، تمام اطلاعات لپتاپها رو نذاریم، بلکه فقط مواردی که مورد نیاز ما هستن رو نشون میدیم. مثلا اسم لپتاپ و قیمتش. بعد که یوزر روی محصول کلیک کرد، مابقی اطلاعات رو نشون بدیم.
اینجا به دوتا نکته توجه کنید:
- ما تمام اطلاعاتی که تو پاسخ به ما داده شده رو نیاز نداریم، فقط یک بخشیش مورد نیاز ماست و
- ما برای صفحه اختصاصی محصول، باید یک درخواست دیگه به مثلا
GET /products/1234
بفرستیم تا اطلاعات محصول رو مجدد دریافت کنیم.
اینجا یکی از جاهاییه که گرافکیوال به کمک ما میاد!
مقدمه
فقط چیزهایی که لازم هست رو درخواست میدیم
گرافکیوال یک زبان کوئریه. یعنی باید بهش بگید دقیقا چی میخواید تا اونهم دقیقا همون رو بهتون بده. اما چطور این کار رو انجام بدیم؟ این دقیقا همون ویژگی اصلی گرافکیوال هست، یعنی شما انتخاب میکنی که چه فیلدهایی باید برای شما ارسال بشن. فکر کنم یه مثال بتونه خوب نشون بده که چطور میشه. تجسم کنید که ما توی گرافکیوال میتونیم چنین چیزی رو برای سرورمون بفرستیم:
query {
products {
id
name
price
}
}
و در جوابش سرور گرافکیوال به ما این رو میده:
[
{
"id": 1234,
"name": "Aspire M5",
"price": 699.99
},
{
...
}
]
به همین سادگی! اما سوال اینه که چطور میشه اطلاعات فقط یک لپتاپ رو بدست آورد؟
اینم خیلی سادست!
query {
product(id: "1234") {
id
name
brand
category
price
features {
weight
dimensions {
width
height
length
}
}
specs {
ram
cpu
cpu_count
thread_count
}
}
}
و جوابش:
{
"id": 1234,
"name": "Aspire M5",
"brand": "Acer",
"category": "Laptop and Computers",
"price": 699.99,
"features": {
"weight": 1.23,
"dimensions": {
"width": 18.5,
"height": 23.4,
"length": 18.3
}
},
"specs": {
"ram": 16,
"cpu": 3.5,
"cpu_count": 8,
"thread_count": 8
}
}
اما یک نکته دیگه راجع به گرافکیوال موند.
فقط یه یک آدرس درخواست میدیم!
این قسمت خوب قضیست. توی گرافکیوال، دقیقا بر عکس ایپیآیهای REST، ما فقط یک Endpoint داریم که بهش درخواست میدیم. همینطور همهی این درخواستها رو فقط با یک Verb میفرستیم! یعنی دیگه خبری از Get
برای گرفتن اطلاعات، Post
برای ساختشون و مابقی برای بقیهی کارها نیست!
توی گرافکیوال همهی درخواستها به همچین آدرسی ارسال میشن:
POST /graphql
و نوع درخواست ما، یا همون Content-Type
که توی Header میذاریم، به جای application/json
، application/graphql
میشه.
در نتیجه سرور متوجه میشه که با یک درخواست گرافکیوال طرفه.
آشنایی با گرافکیوال
گرافکیوال، یک زبان مخصوص برای تعریف شکل دادههاست که سمت سرور اجرا میشه و خروجی لازم رو تحویل میده. چیزی که مهمه اینه که گرافکیوال برای خودش یک Type System داره که باید در موردش حتما بدونیم.
یک سرور گرافکیوال، اصولا با ساختن تایپها یا types شروع میشه که توی هر تایپ، فیلدهای خاصی وجود داره. در نهایت به هر فیلد، یک فانکشن تخصیص داده میشه که این اتفاق سمت موتور میوفته، ما فقط فانکشنها رو تعریف میکنیم و بهشون لاجیک میدیم.
برای مثلا، یک سرور گرافکیوال که که بهمون اطلاعات یک پست وبلاگ رو میده، حتما این دو تایپ رو داره:
type Query {
post: PostType
}
type PostType {
title: String
comments_count: Int
}
که شما در نهایت با فرستادن یک درخواست مثل
{
post {
comments_count
}
}
میتونید یک جواب شبیه به
{
"post": {
"comments_count": 43
}
}
رو دریافت کنید.
درخواستها یا Queries
GraphQL به عنوان یک استاندارد جدید برای جابهجایی دادهها، وارد جریان اصلی شرکتهای و کسب و کارها شده. الان صحبتهای خیلی زیادی تو جوامع مختلف میشه که چطور باید این تکنولوژی به سمت جلو حرکت کنه. یکی از بهترین قسمتهای GraphQL اینه که به عنوان یک زبان مشترک بسیار خوب با تیم شما ارتباط برقرار میکنه. اما چطوری باید در مورد خود زبان و فناوری اصلیش صحبت کرد؟
یکی از منابع خیلی خوب، خود GraphQL specification هست که فیسبوک اون رو کنترل میکنه. اما بزرگترین مشکل این Specها اینه که خیلی طولانی هستن و برای کسی که تازه وارد این موضوع شده، جذابیت زیادی ندارن. این هم چیزیه که من سعی دارم اینجا بهش بپردازم.
کوئریهای ابتدایی در گرافکیوال
اکثر برنامهنویسها به هر چیزی که توی گرافکیوال اتفاق میوفته «کوئری» میگن. اما حقیقت اینه که شما ممکنه به یه سرور گرافکیوال:
- کوئری بزنید (توضیح میدم)،
- میوتیت (Mutate) یا (توضیح میدم)
- سابسکرایب (Subscribe) کنید (تو این پست توضیح نمیدم).
پس اول از همه دوتا کانسپت رو توی ذهنمون داشته باشیم:
- داکیومنت گرافکیوال یا GraphQL Document: یه رشتهی استرینگ که به گرافکیوال نوشته شده (نمیگم زبان چون کلمهی زبان توی گرافکیوال جا داره) و یک یا چند دستور یا Operation رو تعریف میکنه
- دستور یا Operation: که میتونه یک یا چند Query، Mutation و یا Subscription باشه که موتور گرافکیوال اونا رو بشناسه.
حالا بریم و اجزای تشکیل دهندهی یک Operation رو ببینیم:
query basicQuery {
user(id: 1234) {
name
email
avatar(size: SMALL)
}
}
بریم که کلمه به کلمه بررسی کنیم:
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 قالبا رو سرورهای گرافکیوال هست و برنامهنویسها میتونن خودشون به دلخواه روشن یا خاموششون کنن. با این حال، تا به مثال خودمون نرسیدیم، با ایپیآیهای این سرویس جلو میریم.
برای شروع دستور زیر رو اونجا وارد کنید و جوابش رو ببینید:
{
posts(options: { slice: { limit: 3 } }) {
meta {
totalCount
}
data {
title
id
user {
name
}
}
}
}
خب، برای شروع ما داریم یک query
بدون اسم میفرستیم. به آرگومانهای فیلد posts
دقت کنید! این روش یک راه برای فیلتر کردن جوابهای سرور هست. مثلا من اینجا به posts
گفتم که جوابهاش رو slice کنه، یا در واقع برش بده و در حقیقت تعدادشون [جوابها] رو فقط سه تا کنه.
چیزی که هست اینه که این ساختار رو خود برنامهنویسها برای این سرویس درست کردن، یعنی الزاما شما به (مثلا) آرگومان slice
تو یه سرور گرافکیوال دیگه دسترسی ندارید.
خب حالا بیاید و یکم این دستور رو دستکاری کنیم. بیاین تجسم کنیم که مثلا میخواستیم دو سری post دریافت کنیم. سری اول اونایی که آیدیشون بین ۱ تا ۳ هست، و سری دوم ۴تا از آخرین پستها. فرضا این درخواست مشتری ما بوده یا لازم بوده چنین چیزی فرستاده بشه تا صفحهی اول رو طراحی کنیم!
خب میتونیم همچین چیزی رو ارسال کنیم (حتما انجام بدید چون میخوام نتیجه رو ببینید)
{
posts(options: { slice: { start: 1, end: 3 } }) {
data {
title
}
}
posts(options: { slice: { limit: 4 }, sort: { order: DESC } }) {
data {
title
}
}
}
این رو ارسال کنید، یکم صبر میکنم...
دیدید؟ ارور داد! اما چرا؟ دلیلش اینه که شما به سرور دارید میگید که دقیقا یک فیلد رو میخواید اما با دوتا آرگومان کاملا متفاوت! و خب سرور با خودش میگه: بالاخره من کدوم رو باید بدم؟
اصلا فرض کنیم سرور جواب هم داد، جواب آخر یک JSON هست که دقیقا دوتا کلید posts
کنار هم داره! این JSON اساسا دیگه درست یا Valid نیست!
اما راه حل چیه؟
آلیاس یا Alias
آلیاس کردن تو گرافکیوال به این معنیه که شما میتونید اسم واقعی یک فیلد رو، به اون چیزی که میخواید تغییر بدید تا جوابتون رو دریافت کنید. بیاین با یک مثال نشونتون بدم:
{
p1: posts(options: { slice: { start: 1, end: 3 } }) {
data {
title
}
}
p2: posts(options: { slice: { limit: 4 }, sort: { order: DESC } }) {
data {
title
}
}
}
اینجا به p1
و p2
کنار کلمات posts
دقت کنید. این دوتا در حال حاضر آلیاسهای posts
هستن. الان مجدد درخواست رو بفرستید و نتیجه رو ببینید.
توی جوابی که گرفتید، اینبار به جای posts
، کلیدهای p1
و p2
رو دارید.
آلیاس کردن رو میتونید روی همهی فیلدها انجام بدید!
خب، تا اینجا که با آلیاسینگ آشنا شدید. حالا بیاین تجسم کنیم که مثلا قرار بود سه جور پست رو دریافت کنیم، دقیقا مشابه درخواست بالا، فقط یه پست دیگه هم اضافه میشه:
{
p1: posts(options: { slice: { start: 1, end: 3 } }) {
data {
title
}
}
p2: posts(options: { slice: { limit: 4 }, sort: { order: DESC } }) {
data {
title
}
}
p3: posts(options: { slice: { start: 1, end: 3 }, sort: { order: DESC } }) {
data {
title
}
}
}
خب، تا اینجا ما سه جور پست رو دریافت میکنیم.
برنامهی ما ساخته میشه و مردم ازش استفاده میکنن، اما توی بیزینس یه تغییر ایجاد میشه و لازم میشه که برای پستها، در کنار عنوانشون، مثلا اسم نویسندشون هم نوشته بشه.
توی این حالت، ما باید بریم و درخواست بالا رو اینطور تغییر بدیم:
{
p1: posts(options: { slice: { start: 1, end: 3 } }) {
data {
title
user {
name
}
}
}
p2: posts(options: { slice: { limit: 4 }, sort: { order: DESC } }) {
data {
title
user {
name
}
}
}
p3: posts(options: { slice: { start: 1, end: 3 }, sort: { order: DESC } }) {
data {
title
user {
name
}
}
}
}
من به همهی پستها این رو اضافه کردم:
user {
name
}
میتونید تست کنید و نتیجه رو ببینید. اما یه مشکل کوچیک وجود داره: درسته که اضافه کردن سه تا خط واقعا کار سختی نبود، اما تو دنیای واقعی، معمولا اضافهکردن میتونه از این بزرگتر باشه، اما سوای این، اصولا تکرار کردن کار درستی نیست و مخالف اصول DRY (Don’t repeat yourself) یا همون «کار تکراری نکن» هست.
اما چطور میشه اینجا از تکرار جلوگیری کرد؟
فرَگمِنتها یا Fragments:
فرگمنتها برای جلوگیری از تکرا ایدهآل هستن. استفاده ازشون هم به نسبت کار سادهایه. در واقع ایده اینه که شما یک یا چند فرگمنت رو تعریف میکنید و در جای خودشون استفاده میکنید. یک نمونه از یک فرگمنت میتونه این شکلی باشه:
fragment postsFragment on PostsPage {
data {
title
user {
name
}
}
}
fragment
: یه کلمهی کلیدی مثلquery
،mutation
وsubscription
که مشخص میکنه شما میخواین یه فرگمنت رو تعریف کنید.postsFragment
: اسم فرگمنت که میخواید بسازید.on Post
: شرطی که فرگمنت باید روش اعمال بشه. دقت کنید که اینجا،Post
اسم فیلد نیست، بلکه type فیلده.- مابقی، تمامی فیلدهایی هستن که شما میخواین روی این تایپ استفاده کنید.
خب، کوئری رو به شکل زیر تغییر بدید و بفرستید و نتیجه رو ببینید:
{
p1: posts(options: { slice: { start: 1, end: 3 } }) {
...postsFragment
}
p2: posts(options: { slice: { limit: 4 }, sort: { order: DESC } }) {
...postsFragment
}
p3: posts(options: { slice: { start: 1, end: 3 }, sort: { order: DESC } }) {
...postsFragment
}
}
fragment postsFragment on PostsPage {
data {
title
user {
name
}
}
}
متغیرها یا Variables:
خب تا اینجا ما با کوئریهای مختلفی کار کردیم، و در حقیقت، یه استرینگ رو برای سرور فرستادیم تا اون هم به ما جواب بده (یادمون نره که داریم با پروتکل HTTP کار میکنیم). اما تو دنیای واقعی، اکثر برنامهها کوئریها رو بر اساس متغیرها مسازن، یا حداقل بخشیش رو. برای مثال، وقتی که میخواید به یوزر امکان فیلتر کردن رو بدید.
این ایده هم خوب نیست که مستقیما این کوئری رو دستکاری کنیم تا هرچی کاربر بهش میده رو بفرسته برای سرور. برای اینکار، خود گرافکیوال اومده و متغیرها رو برای خودش تعریف کرده.
وقتی که میخوایم با متغیرها کار کنیم، ۳ کار رو باید حتما انجام بدیم:
- مقدار ثابتی که توی کوئری هست رو با
$esmeMoteghayer
عوض کنیم، - متغیر
$esmeMoteghayer
رو به عنوان یکی از پارامترهای کوئری بهش پاس بدیم و - مقدار
esmeMoteghayer
(از عمد علامت دلار رو برداشتم) رو به شکل JSON به همراه کوئری ارسال کنیم.
تو نگاه اول ممکنه سخت و حتی عجیب برسه، اما واقعیت اینه که آسونتر از چیزیه که به نظر میاد! کوئری بالا رو دوباره مینویسم:
query getPosts($start: Int, $end: Int, $limit: Int, $order: SortOrderEnum) {
p1: posts(options: { slice: { start: $start, end: $end } }) {
...postsFragment
}
p2: posts(options: { slice: { limit: $limit }, sort: { order: $order } }) {
...postsFragment
}
p3: posts(
options: { slice: { start: $start, end: $end }, sort: { order: $order } }
) {
...postsFragment
}
}
fragment postsFragment on PostsPage {
data {
title
user {
name
}
}
}
تا اینجا قدم ۱ و ۲ رو انجام دادیم. حالا برای شماره ۳، پایین صفحه رو ببینید. میتونید تو قسمت Query Variables مقادیر رو به شکل JSON بنویسید. مثل شکل زیر:
تا اینجا با کوئریها آشنا شدیم، اما چی میشه اگه بخوایم چیزها رو اینبار تغییر بدیم؟
تغییرات یا Mutations
تو ساختار REST، وقتی میخوایم اطلاعاتی رو از سرور بگیریم، از GET
و همینطور اگر بخوایم چیزی رو تغییر بدیم (بسازیم، تغییر بدیم و پاک کنیم) از POST
، PUT
و DELETE
استفاده میکنیم. معادل همهی اینا تو گرافکیوال میشه Query
ها و Mutation
ها.
بیاین خیلی ساده، اولین متغییر رو ایجاد کنیم:
mutation newMutation {
createPost(
input: { title: "Man onvan hastamn", body: "Va in ham matne poste" }
) {
id
title
body
}
}
مثل کوئری گرفتن، ما میتونیم فیلدهای اون شئ که ساختیم یا تغییر دادیم رو همونجا درخواست بدیم. یه نکته اینکه تمام ویژگیهای کوئری کردن، اینجا هم برقرارن مثل استفاده از متغیرها!
تایپ سیستم در گرافکیوال
سرورهای گرافکیوال رو با هر زبانی میشه ساخت، از اینجاست که نمیشه به تایپ سیستم زبانها برای خود گرافکیوال اتکا کرد، به این منظور که خب آخرش کی داره درست میگه؟ استاندارد کی باید رعایت بشه؟
برای همین هم گرافکیوال تایپ سیستم خودش رو ساخته که من تو این بخش به اون میرسم.
اشیا یا Objects
اساسیترین قسمت تشکیلدهندهی گرافکیوال اشیا هستن. اشیا همون چیزهایی هستن که شما میخواین از سرور دریافت کنید، که خب واضحه به همین دلیل اشیا، شامل فیلدها هم میشن، مثلا:
type Product {
name: String!
price: Int!
tags: [String!]!
}
بریم ببینیم اینا چی هستن:
Product
: یک شی گرافکیوال هست.name
،price
وtags
: فیلدهایی هستن که شی ما توی خودش داره.String
: یک تایپ استاندارد تو خود گرافکیوال هست (جز تایپهای Scalar).String!
وInt!
: به این معنیه که این فیلد یک استرینگه یا عدد صحیح که خالی نیست و به اصطلاح non-nullable هست، یعنی سرور حتما به شما یه چیزی رو برمیگردونه.[String!]!
: یک آرایه از استرینگها، و چون خالی نیست، میشه همیشه انتظار یک آرایه رو در پاسخ داشت (با هیچ یا چند استرینگ داخلش).
آرگومانها یا Arguments
این امکان برای هر فیلد وجود داره که بتونه هیچ یا چند آرگومان داشته باشه که اونا هم میتونن هرکدومشون اجباری یا دلخواه باشن. این آرگومانها کمک میکنن تا سمت سرور تغییرات لازم روی فیلد اعمال بشه و جواب مورد نظر برگرده. یک نمونش میتونه این باشه:
type Product {
name: String!
price(exchange: ExchangeType = EUR): Int!
tags: [String!]!
}
تو مثال بالا، price
یک آرگومان دلخواه داره به اسم exchange
از نوع ExchangeType
(یه تایپ فرضی) که در حالت عادی، یا درواقع وقتی داده نشه مقدارش EUR
هست. مثلا قراره که ما بر مبنای مقدار این آرگومان، ثیمت محصول رو به ارزهای مختلف برگردونیم. اگر اینجا EUR رو نمیذاشتیم، یعنی این آرگومان اجباری بود و باید مقدار دهی میشد.
تایپ Query و Mutation
یک تایپ مخصوص تو گرافکیوال وجود داره به اسم schema که در واقع نقطه شروع گرافکیوال محسوب میشه. ما معمولا تو این شی کوئریها و میوتیشنها رو میذاریم:
schema {
query: Query
mutation: Mutation
}
و بعد
type Query {
products: [Product!]!
product(id: Int!): Product!
}
type Mutation {
createProduct(input: CreateProductInput): Product!
updateProduct(input: UpdateProductInput): Product!
deleteProduct(input: DeleteProductInput): Product!
}
در نهایت میشه گفت که چیزی که زیر مینویسم (که هنوز ناقصه و تو مسیرمون کاملش میکنیم)، ساختار و شِمای گرافکیوال محسوب میشه:
type Product {
id: ID!
name: String!
price(exchange: ExchangeType = EUR): Int!
tags: [String!]!
}
type Query {
products: [Product!]!
product(id: Int!): Product!
}
type Mutation {
createProduct(input: CreateProductInput): Product!
updateProduct(input: UpdateProductInput): Product!
deleteProduct(input: DeleteProductInput): Product!
}
schema {
query: Query
mutation: Mutation
}
گرچه برای تست کردنش لازم هست که سرور خودمون رو بسازیم، اما به این هم خواهیم رسید! اما اینجا ما در واقع شِمای یک گراف رو ساختیم که بتونیم ازش مثلا اینطوری استفاده کنیم:
{
products {
name
price(exchange: TOMAN)
}
}
دقت کنید که این مثال هنوز کامل نیست و بخشهاییش رو لازمه بعدا بسازیم! (مثل CreateProductInput
)
تایپهای اسکالر یا Scalar
تایپهای اسکالر، تایپهای ساده و جزئی هستن که توی گرافکیوال استفاده میشن تا انواع مختلفی رو تعریف کنن. این تایپها کلا به دو دسته تقسیم میشن، تایپهای استاندارد و تایپهای دستی یا Manual
تایپهای استاندارد
Int
: عدد صحیح (۳۲ بیت)Float
: عدد اعشاریString
: رشتهی کاراکتری مبتنی بر UTF-8Boolean
: عدد ۲ بیتی،true
یاfalse
ID
: یه تایپ اسکالر که به یه نوع یونیک یا unique اشاره داره که با وجود اینکه باهاش مثل String برخورد میشه، اما الزاما قابل خوندن برای انسان نیست.
تایپهای دستی
گاهی لازم میشه که ما تایپهای خاص خودمون رو بسازیم، برای همین میشه از دستور زیر استفاده کرد:
scalar Date
که در نهایت میشه موقع ایمپلمنت کردن برنامه، تعیین کرد که حین برخورد با این تایپ باید چه کرد.
اینامها یا Enumerations
نوع خاصی از تایپهای اسکالر هستن که مقادیر خاصی رو میتونن داشته باشن. یعنی خارج از مجموعهی خودشون، نمیشه بهشون مقدار دیگهای داد، تو مثالی که قبلا زدم، میشه به ExchangeType
اشاره کرد که در واقع یک enum
بوده:
enum ExchangeType {
EUR
TOMAN
USD
}
پس در واقع، یک قدم کاملتر شدهی شِمای ما میشه:
enum ExchangeType {
EUR
TOMAN
USD
}
type Product {
id: ID!
name: String!
price(exchange: ExchangeType = EUR): Int!
tags: [String!]!
}
type Query {
products: [Product!]!
product(id: Int!): Product!
}
type Mutation {
createProduct(input: CreateProductInput): Product!
updateProduct(input: UpdateProductInput): Product!
deleteProduct(input: DeleteProductInput): Product!
}
schema {
query: Query
mutation: Mutation
}
اینترفیسها یا Interfaces
مثل خیلی از تایپسیستمهای دیگه، گرافکیوال از اینترفیسها هم پشتیبانی میکنه. اینترفیس شبیه به یکجور قرارداد میمونه که، اگه با زبانهای OOP آشنا باشید، باعث میشه هرچیزی که ازش استفاده کنه، ناچار به رعایت قوانین اون میشه. میتونید یه سری به این مطلب ستاره بهزادی بزنید تا با مفهومش بیشتر آشنا بشید.
اینترفیسها در گرافکیوال مجموعهای از فیلدها هستن که وقتی به آبجکتی که ازشون استفاده کنه، به نوعی تحمیل میشن. فکر کنم یه مثال منظورم رو بهتر برسونه:
interface Model {
id: ID!
}
برای مثال، این اینترفیس یک مدل پایه رو تعریف میکنه که حتما آیدی داره. حالا من میتونم در مورد تایپ Product
اینطور بگم:
type Product implements Model {
id: ID!
name: String!
price(exchange: ExchangeType = EUR): Int!
tags: [String!]!
}
یعنی هر تایپی که اصطلاحا Model
رو پیادهسازی یا implement کنه، باید حتما id: ID!
رو توی خودش داشته باشه.
حالا تو این مثالی که در مورد محصولات زدم، چون مدلها کلا ساده هستن شاید استفاده از اینترفیس ایدهی خیلی خوبی نباشه، اما برای اینکه باهاش آشنا باشید کافیه.
تایپهای ورودی یا Input Types
تا اینجا تمام چیزهایی که نشونت دادم، مربوط به کوئریها بودن. اینجا میخوام درمورد تایپهای ورودی بنویسم که بیشترین کاربردشون تو Mutationهاست. inputها دقیقا مثل typeها تعریف میشن، با این تفاوت که به جای type از input استفاده میشه براشون:
input CreateProductInput {
name: String!
price: Int!
tags: [String!]!
}
input UpdateProductInput {
name: String
price: Int
tags: [String!]
}
input DeleteProductInput {
id: ID!
}
پس باز هم با یک تغییر تو شِما مواجه میشیم:
enum ExchangeType {
EUR
TOMAN
USD
}
input CreateProductInput {
name: String!
price: Int!
tags: [String!]!
}
input UpdateProductInput {
name: String
price: Int
tags: [String!]
}
input DeleteProductInput {
id: ID!
}
type Product implements Model {
id: ID!
name: String!
price(exchange: ExchangeType = EUR): Int!
tags: [String!]!
}
type Query {
products: [Product!]!
product(id: Int!): Product!
}
type Mutation {
createProduct(input: CreateProductInput): Product!
updateProduct(input: UpdateProductInput): Product!
deleteProduct(input: DeleteProductInput): Product!
}
schema {
query: Query
mutation: Mutation
}
تا اینجا با اساس کار گرافکیوال آشنا شدیم، خیلی خلاصه البته، ولی دیگه تئوری بسه! بریم که دستمون رو به کد آلوده کنیم.
یک سرور گرافکیوال بسازیم!
تا اینجا با ساختار گرافکیوال آشنا شدیم، اما واقعا تا وقتی کد نزنیم، تئوری هیچ کاربردی نداره (یا حداقل کاربرد زیادی نداره). من اینجا تمرکزم روی ساخت یه سرور گرافکیوال با جاوااسکریپته، گرچه پروسه واسه زبانهای دیگه هم یکسانه، اما چون سرعت کار با جاوااسکریپت بالا میره، منم رو این زبان تمرکز میکنم.
سادهترین شیوه برای شروع، استفاده از کتابخونهی Express هست که حدس میزنم باهاش آشنایی دارید. اگر نه، میتونید توی کامنتها برام بنویسید تا بعد در موردش توضیح بدم. بیاین خیلی ساده از ساخت خود فولدر پروژه شروع کنیم:
mkdir my-express-app && cd my-express-app
تو قدم بعد، یه پروژهی npm رو راهاندازی میکنیم. من از آرگومان y
استفاده میکنم که سریع پروژه ساخته بشه، میتونید اگر دوست دارید این آرگومان رو نذارید.
npm init -y
خب، تو گام بعدی، پکیجهایی که نیاز داریم رو نصب میکنیم. پکیجهایی که لازم داریم:
express
: که خود فریمورک ماست و همهچیز رو این بستر اجرا میشه،graphql
: موتور اصلی گرافکیوال،graphql-tools
: ابزاریه که کمک میکنه شما schema رو تعریف کنید و تو کد ازش استفاده کنید،express-graphql
: یه سرور http برای گرافکیوال میسازه و اون رو به عنوان یه middleware ارائه میده،sqlite3
: بایندیگها یا Bindings برای دیتابیس sqlite رو بستر node.
npm install express graphql graphql-tools express-graphql sqlite3
تا اینجا همهچیز خوب بود. از اینجا به بعد میریم سراغ کد زدن، هدفم اینه که قدم به قدم چیزهایی که میخوایم رو بسازیم، بجای اینکه از بالای فایل شروع کنیم بیایم پایین.
ساخت شِما
برای شروع، یه فایل schema.graphql بسازید و این رو داخلش بذارید:
enum ExchangeType {
EUR
TOMAN
USD
}
interface Model {
id: ID!
}
input CreateProductInput {
name: String!
price: Int!
tags: [String!]!
}
input UpdateProductInput {
id: ID!
name: String
price: Int
tags: [String!]
}
input DeleteProductInput {
id: ID!
}
type Product implements Model {
id: ID!
name: String!
price(exchange: ExchangeType = EUR): Int!
tags: [String!]!
}
type Query {
products: [Product!]!
product(id: Int!): Product!
}
type Mutation {
createProduct(input: CreateProductInput): Product!
updateProduct(input: UpdateProductInput): Product!
deleteProduct(input: DeleteProductInput): Int!
}
schema {
query: Query
mutation: Mutation
}
مدل سازی
یکی از کارهایی که من معمولا تو طراحی سیستمهای گرافکیوال انجام میدم، اینه که دقیقا مدلهایی که تو دیتابیسهستن رو با گرافکیوال تعریف میکنم. مثلا ما تو شِما، یکی از مدلهایی که داریم، همین مدل Product هست. از دید ما Product این ستوها رو تو دیتابیس داره:
- id: از جنس int که auto increment باید باشه،
- name: از جنس string،
- price: از جنس int و
- tags: یه آرایه از stringها.
برای ساخت مدل، و پر کردن دیتابیس، من از یه سرویس که دیتای تست یا Mock میسازه استفاده کردم. مدل رو تو فایل ./db/init.sql
میسازیم:
CREATE TABLE IF NOT EXISTS `product` (
`id` INTEGER PRIMARY KEY,
`name` TEXT default NULL,
`price` mediumint default NULL,
`tags` varchar(255) default NULL
);
INSERT 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");
INSERT 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");
INSERT 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,"");
INSERT 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,"");
INSERT 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");
INSERT 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");
INSERT 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");
INSERT 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");
INSERT 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");
INSERT 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
رو بسازید و مدلسازی رو از اونجا شروع کنید:
const sqlite = require("sqlite3").verbose()
const fs = require("fs")
const db = new sqlite.Database("./db/db.sqlite")
if (process.env.INIT_DB) {
const data = fs.readFileSync("./db/init.sql", {
encoding: "utf8",
flag: "r",
})
console.log("running database migration")
db.exec(data)
}
دلیل اینکه من از process.env.INIT_DB
استفاده میکنم، اینه که میخوام هرموقع که از دستور زیر رو زدم، جدول Product ساخته بشه:
INIT_DB=true node index.js
اگر INIT_DB=true
نباشه، و فقط دستور node index.js
رو بزنیم، برنامه عادی اجرا میشه و تغییری تو دیتابیس اتفاق نمیوفته.
خب تا اینجا مدلسازی انجام شد و ما یه دیتابیس برای شروع داریم. حتما یه بار دستور بالا رو اجرا کنید!
ساخت سرور
خب بریم که یه سرور ساده رو بسازیم. فایل index.js رو یه آپدیت میکنیم:
const sqlite = require("sqlite3").verbose()
const fs = require("fs")
const express = require("express")
const db = new sqlite.Database("./db/db.sqlite")
if (process.env.INIT_DB) {
const data = fs.readFileSync("./db/init.sql", {
encoding: "utf8",
flag: "r",
})
console.log("running database migration")
db.exec(data)
}
const app = express()
const PORT = process.env.PORT || 3000
app.get("/status", (req, res) => {
res.json({
ok: true,
})
})
app.listen(PORT, () => {
console.log(`server started on http://localhost:${PORT}`)
})
برای آزمایش، http://localhost:3000 رو باز کنید ببینیم آیا سیستم کار میکنه یا نه. اگر احیانا به مشکلی خوردید، توی کامنتها برام بنویسین.
کاری که کردیم، این بوده که یه سرور اکسپرس رو ساختیم و یه مسیر /status
رو گذاشتیم که فقط ببینیم آیا سیستم کار میکنه یا نه.
تنظیمات برای گرافکیوال
یکی از اسمهایی که برای شِما میذارن، Type Definition، تایپ دِفینیشِن یا تعریف تایپ (؟) هست. کاری که تو این مرحله انجام میدیم، اون فایل شِما رو میخونیم و تو یه متغیر میذاریم، یه ریزالوِر یا Resolver درست میکنیم و در نهایت این دو (متغیر و ریزالوِر) رو به هم مَپ یا map میکنیم و در نهایت تو تنظیمات گرافکیوال میذاریمشون.
ریزالور، مجموعهی تمام فانکشنهایی هست که برای فیلدها استفاده میشن. چه کوئری چه میوتیشن.
بریم شروع کنیم، یه فایل جدید به اسم resolvers.js بسازید و اینا رو توش بنویسید:
module.exports = {
Query: {
products: (root, args, context) => {
return []
},
},
}
این فایل رو توی index.js اضافه میکنیم، تایپدفینیشنها رو بهشون مَپ میکنیم و میدیمشون به اکسپرس:
const sqlite = require("sqlite3").verbose()
const fs = require("fs")
const express = require("express")
const { makeExecutableSchema } = require("graphql-tools")
const resolvers = require("./resolvers")
const graphql = require("graphql")
const { graphqlHTTP } = require("express-graphql")
const db = new sqlite.Database("./db/db.sqlite")
if (process.env.INIT_DB) {
const data = fs.readFileSync("./db/init.sql", {
encoding: "utf8",
flag: "r",
})
console.log("running database migration")
db.exec(data)
}
const app = express()
const PORT = process.env.PORT || 3000
const typeDefinitions = fs.readFileSync("./schema.graphql")
const schema = makeExecutableSchema({
typeDefs: String(typeDefinitions),
resolvers,
})
app.use(
"/graphql",
graphqlHTTP({
schema,
graphiql: true,
})
)
app.get("/status", (req, res) => {
res.json({
ok: true,
})
})
app.listen(PORT, () => {
console.log(`server started on http://localhost:${PORT}`)
console.log(`graphql server can be found in http://localhost:${PORT}/graphql`)
})
خب، اینجا چی شد؟ ما اول هر آنچه که توی فایل resolvers داشتیم رو اینجا require
کردیم. بعد محتويات فایل schema.graphql
رو ریختیم تو متغیر typeDefinition
. بعد از فانکشن makeExecutableSchema
استفاده کردیم، که تایپدفینیشنها رو به ریزالورها مَپ کنیم. این فانکشن کارش اینه که بعد از مَپ کردن، خروجی بسازه که گرافکیوال بتونه ازش استفاده کنه.
تو قدم بعد، از فانکشن graphqlHTTP استفاده کردیم، که یه middleware میسازه برای گرافکیوال و میده به اکسپرس. یه آبجکت برای تنظیماتش میگیره، یک یکی از پراپرتیهای این آبجکت، همون خروجی makeExecutableSchema
هست.
اما چیزی که اینجا شاید براتون جالب باشه، پراپرتی graphiql
هست.graphiql یا گرافیکیوال، یه IDE تحت وب برای برای گرافکیوال که خصوصا برای تست ساخته شده. معمولا درستش اینه که برای تنظیمات سرور اینطور ازش استفاده بشه:
app.use(
"/graphql",
graphqlHTTP({
schema,
graphiql: process.env.NODE_ENV === "development",
})
)
خب. میتونید سرور رو اجرا کنید، آدرس http://localhost:3000/graphql رو تو مروگرتون باز کنید و محیط رو یکم تست کنید. اگر مشکلی بود، میتونید توی کامنتها بنویسید.
کار روی ریزالورها
خب، تا اینجا ما یه سرور کارآمد گرافکیوال داریم. از این مرحله به بعد، کار ما تو فایل resolvers.js
ادامه پیدا میکنه. فقط قبلش یه تغییر کوچیک باید به سیستم بدیم. برای دیتابیس باید یه فایل جداگانه درست کنیم، یا در واقع ماژول جدا بسازیم که تو همهی فایلا بشه ازش استفاده کرد. فایل db/db.js
رو بسازید و این تغییرات رو توش بدید:
const path = require("path")
const sqlite = require("sqlite3").verbose()
const db = new sqlite.Database(
path.resolve(__dirname, "db.sqlite"),
sqlite.OPEN_READWRITE,
err => {
if (err) {
console.log(err)
}
console.log("connected to the database")
}
)
module.exports = db
خب، و یک تغییر تو index.js
:
const fs = require("fs")
const express = require("express")
const { makeExecutableSchema } = require("graphql-tools")
const graphql = require("graphql")
const { graphqlHTTP } = require("express-graphql")
const db = require("./db/db")
const resolvers = require("./resolvers")
if (process.env.INIT_DB) {
const data = fs.readFileSync("./db/init.sql", {
encoding: "utf8",
flag: "r",
})
console.log("running database migration")
db.exec(String(data))
}
const app = express()
const PORT = process.env.PORT || 3000
const typeDefinitions = fs.readFileSync("./schema.graphql")
const schema = makeExecutableSchema({
typeDefs: String(typeDefinitions),
resolvers,
})
app.use(
"/graphql",
graphqlHTTP({
schema,
graphiql: true,
})
)
app.get("/status", (req, res) => {
res.json({
ok: true,
})
})
app.listen(PORT, () => {
console.log(`server started on http://localhost:${PORT}`)
console.log(`graphql server can be found in http://localhost:${PORT}/graphql`)
})
حالا یه آپدیت روی resolvers.js
میریم:
const db = require("./db/db")
module.exports = {
Query: {
products: (root, args, context) => {
return new Promise((resolve, reject) => {
db.all("SELECT * FROM product;", (err, rows) => {
if (err) {
console.log(err)
reject([])
}
const result = rows.map(row => ({
...row,
tags: row.tags.split(","),
}))
resolve(result)
})
})
},
},
}
تا اینجا چه اتفاقی افتاد؟
ما اول از همه دیتابیس رو require کردیم. اونچیزی که قراره در نهایت ساخته بشه، باید همون چیزی رو دنبال کنه که قرار هست شِمای ما باشه. چون تو شِما، تو فیلد query، ما products رو داریم، اینجا هم باید براش یه فانکشن بسازیم. ایده اینه که هربار که یوزر این فیلد رو کوئری میکنه، ما اینطرف از دیتابیس اطلاعات رو میگیریم و برمیگردونیم.
برای اینکار، گرافکیوال احتیاج داره تا یک Promise رو دریافت کنه، که هرزمان که لازم بود جوابها رو برگردونه. برای همین من اینجا یه Promise رو میسازم. داخل این Promise ما به دیتابیس میگیم که بره همهی ردیفها رو از جدول product بگیره و یک کالبک رو صدا بزنه، این کالبک دوتا پارامتر داره که اولشی اروره، و دومیش همون جوابی که میگیریم یا ستونها.
اینجا یه مشکل کوچیک وجود داره، و این برمیگرده به نحوهی دخیره تو ستون tags تو دیتابیسه. این ستون در واقع یه استرینگ ذخیره میکنه به جای آرایه (فکر میکنم یه راهی وجود داشته باشه تو Sqlite) که ما احتیاج هست یکبار این رو تغییر بدیم. در واقع اونجا که از map استفاده کردم برای حل این مشکل بود.
حالا برنامه رو با دستور زیر اجرا کنید:
node index.js
و توی گرافیکیوال، کوئری زیر رو بفرستید:
{
products {
id
name
price
tags
}
}
اگر مشکلی بود، توی کامنتا بنویسید.
تموم کردن ریزالور
واقعیت اینه که از اینجا به بعد، همهچیز به همین شکل ادامه پیدا میکنه. یعنی ایده همینه. من برای product کار رو ادامه میدم:
product: (root, args, context) => {
return new Promise((resolve, reject) => {
db.get(`SELECT * FROM product WHERE id=${args.id}`, (err, rows) => {
if (err) {
console.log(err)
reject({})
}
const result = rows
result.tags = result.tags.split(",")
resolve(result)
})
})
}
و برای اینکه تستش کنید، این کوئری رو بفرستید:
{
product(id: 1) {
id
name
price
tags
}
}
بریم سراغ میوتیشنها، من هر آنچه که باید تا انتها نوشته بشه رو اینجا میذارم:
const db = require("./db/db")
module.exports = {
Query: {
products: (root, args, context) => {
return new Promise((resolve, reject) => {
db.all("SELECT * FROM product;", (err, rows) => {
if (err) {
console.log(err)
reject([])
}
const result = rows.map(row => ({
...row,
tags: row.tags.split(","),
}))
resolve(result)
})
})
},
product: (root, args, context) => {
return new Promise((resolve, reject) => {
db.get(`SELECT * FROM product WHERE id=${args.id}`, (err, rows) => {
if (err) {
console.log(err)
reject({})
}
const result = rows
result.tags = result.tags.split(",")
resolve(result)
})
})
},
},
Mutation: {
createProduct: (root, args, context) => {
return new Promise((resolve, reject) => {
db.get(
`INSERT INTO "product"("name", "price", "tags") VALUES("${
args.input.name
}", ${args.input.price}, "${args.input.tags.join(",")}")`,
err => {
if (err) {
console.log(err)
reject({})
}
resolve({
...args.input,
id: -1,
})
}
)
})
},
updateProduct: (root, args, context) => {
return new Promise((resolve, reject) => {
db.exec(
`UPDATE product
SET name = "${args.input.name}",
price = ${args.input.price},
tags = "${args.input.tags.join(",")}"
WHERE
id = ${args.input.id}`,
err => {
if (err) {
console.log(err)
reject({})
}
resolve(args.input)
}
)
})
},
deleteProduct: (root, args, context) => {
return new Promise((resolve, reject) => {
db.exec(
`DELETE FROM product
WHERE id = ${args.input.id};`,
err => {
if (err) {
console.log(err)
reject({})
}
resolve(args.input.id)
}
)
})
},
},
}
من یه نمونه از کوئری که میتونید حالا بفرستید رو هم اینجا میذارم:
query getAllProducts {
products {
id
name
}
}
query getOneProduct {
product(id: 1) {
id
name
}
}
mutation createNewProduct {
createProduct(input: { name: "Test", price: 123, tags: ["tag1", "tag2"] }) {
id
name
}
}
mutation updateAProduct {
updateProduct(
input: { id: 1, name: "New Name", price: 9876543, tags: ["new tags"] }
) {
id
name
}
}
mutation deleteAProduct {
deleteProduct(input: { id: 1 })
}
تست کنید و نتیجه رو ببینید.
خب، ما تونستیم با هم یه سرور کامل گرافکیوال رو راه بندازیم. لازمه که بدم، صرفا برای آموزش، من بخشهای پیادهسازی صحیح رو جا انداختم چون از ابعاد موضوع خارج بودن. اما شما چه چیزهایی به نظرتون میرسه؟ فکر میکنید کجاها چه ایرادایی وجود دارن؟ فکر میکنید میتونید پیداشون کنید؟
حتما سعی میکنم که تو یه تایم بعدی، در مورد این ایرادات و کمبودها و نحوهی درست پیادهسازیشون بنویسم.
ضمنا، برای نوشتن این مطلب زمان زیادی رو گذاشتم تا با شما به اشتراک بذارمش. اگر از این مطلب خوشتون اومده و خواستید جایی منتشرش کنید، اسم من رو فراموش نکنید.