برام مواردی پیش اومدن که لازم بوده برای کسی در مورد گرافکیوال یا 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.313 }14 },15 "specs": {16 "ram": 16,17 "cpu": 3.5,18 "cpu_count": 8,19 "thread_count": 820 },21 "price": 699.9922 },23 {...}24]
خب اینا اطلاعات یه سری لپتاپ هستن (مثلا) و ما میخوایم اینارو تو صفحه محصولاتمون نشون بدیم. اما ایده اینه که تو صفحهی اصلی، تمام اطلاعات لپتاپها رو نذاریم، بلکه فقط مواردی که مورد نیاز ما هستن رو نشون میدیم. مثلا اسم لپتاپ و قیمتش. بعد که یوزر روی محصول کلیک کرد، مابقی اطلاعات رو نشون بدیم.
اینجا به دوتا نکته توجه کنید:
- ما تمام اطلاعاتی که تو پاسخ به ما داده شده رو نیاز نداریم، فقط یک بخشیش مورد نیاز ماست و
- ما برای صفحه اختصاصی محصول، باید یک درخواست دیگه به مثلا
GET /products/1234
بفرستیم تا اطلاعات محصول رو مجدد دریافت کنیم.
اینجا یکی از جاهاییه که گرافکیوال به کمک ما میاد!
مقدمه
فقط چیزهایی که لازم هست رو درخواست میدیم
گرافکیوال یک زبان کوئریه. یعنی باید بهش بگید دقیقا چی میخواید تا اونهم دقیقا همون رو بهتون بده. اما چطور این کار رو انجام بدیم؟ این دقیقا همون ویژگی اصلی گرافکیوال هست، یعنی شما انتخاب میکنی که چه فیلدهایی باید برای شما ارسال بشن. فکر کنم یه مثال بتونه خوب نشون بده که چطور میشه. تجسم کنید که ما توی گرافکیوال میتونیم چنین چیزی رو برای سرورمون بفرستیم:
1query {2 products {3 id4 name5 price6 }7}
و در جوابش سرور گرافکیوال به ما این رو میده:
1[2 {3 "id": 1234,4 "name": "Aspire M5",5 "price": 699.996 },7 {8 ...9 }10]
به همین سادگی! اما سوال اینه که چطور میشه اطلاعات فقط یک لپتاپ رو بدست آورد؟
اینم خیلی سادست!
1query {2 product(id: "1234") {3 id4 name5 brand6 category7 price8 features {9 weight10 dimensions {11 width12 height13 length14 }15 }16 specs {17 ram18 cpu19 cpu_count20 thread_count21 }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.313 }14 },15 "specs": {16 "ram": 16,17 "cpu": 3.5,18 "cpu_count": 8,19 "thread_count": 820 }21}
اما یک نکته دیگه راجع به گرافکیوال موند.
فقط یه یک آدرس درخواست میدیم!
این قسمت خوب قضیست. توی گرافکیوال، دقیقا بر عکس ایپیآیهای REST، ما فقط یک Endpoint داریم که بهش درخواست میدیم. همینطور همهی این درخواستها رو فقط با یک Verb میفرستیم! یعنی دیگه خبری از Get
برای گرفتن اطلاعات، Post
برای ساختشون و مابقی برای بقیهی کارها نیست!
توی گرافکیوال همهی درخواستها به همچین آدرسی ارسال میشن:
1POST /graphql
و نوع درخواست ما، یا همون Content-Type
که توی Header میذاریم، به جای application/json
، application/graphql
میشه.
در نتیجه سرور متوجه میشه که با یک درخواست گرافکیوال طرفه.
آشنایی با گرافکیوال
گرافکیوال، یک زبان مخصوص برای تعریف شکل دادههاست که سمت سرور اجرا میشه و خروجی لازم رو تحویل میده. چیزی که مهمه اینه که گرافکیوال برای خودش یک Type System داره که باید در موردش حتما بدونیم.
یک سرور گرافکیوال، اصولا با ساختن تایپها یا types شروع میشه که توی هر تایپ، فیلدهای خاصی وجود داره. در نهایت به هر فیلد، یک فانکشن تخصیص داده میشه که این اتفاق سمت موتور میوفته، ما فقط فانکشنها رو تعریف میکنیم و بهشون لاجیک میدیم.
برای مثلا، یک سرور گرافکیوال که که بهمون اطلاعات یک پست وبلاگ رو میده، حتما این دو تایپ رو داره:
1type Query {2 post: PostType3}45type PostType {6 title: String7 comments_count: Int8}
که شما در نهایت با فرستادن یک درخواست مثل
1{2 post {3 comments_count4 }5}
میتونید یک جواب شبیه به
1{2 "post": {3 "comments_count": 434 }5}
رو دریافت کنید.
درخواستها یا Queries
GraphQL به عنوان یک استاندارد جدید برای جابهجایی دادهها، وارد جریان اصلی شرکتهای و کسب و کارها شده. الان صحبتهای خیلی زیادی تو جوامع مختلف میشه که چطور باید این تکنولوژی به سمت جلو حرکت کنه. یکی از بهترین قسمتهای GraphQL اینه که به عنوان یک زبان مشترک بسیار خوب با تیم شما ارتباط برقرار میکنه. اما چطوری باید در مورد خود زبان و فناوری اصلیش صحبت کرد؟
یکی از منابع خیلی خوب، خود GraphQL specification هست که فیسبوک اون رو کنترل میکنه. اما بزرگترین مشکل این Specها اینه که خیلی طولانی هستن و برای کسی که تازه وارد این موضوع شده، جذابیت زیادی ندارن. این هم چیزیه که من سعی دارم اینجا بهش بپردازم.
کوئریهای ابتدایی در گرافکیوال
اکثر برنامهنویسها به هر چیزی که توی گرافکیوال اتفاق میوفته «کوئری» میگن. اما حقیقت اینه که شما ممکنه به یه سرور گرافکیوال:
- کوئری بزنید (توضیح میدم)،
- میوتیت (Mutate) یا (توضیح میدم)
- سابسکرایب (Subscribe) کنید (تو این پست توضیح نمیدم).
پس اول از همه دوتا کانسپت رو توی ذهنمون داشته باشیم:
- داکیومنت گرافکیوال یا GraphQL Document: یه رشتهی استرینگ که به گرافکیوال نوشته شده (نمیگم زبان چون کلمهی زبان توی گرافکیوال جا داره) و یک یا چند دستور یا Operation رو تعریف میکنه
- دستور یا Operation: که میتونه یک یا چند Query، Mutation و یا Subscription باشه که موتور گرافکیوال اونا رو بشناسه.
حالا بریم و اجزای تشکیل دهندهی یک Operation رو ببینیم:
1query basicQuery {2 user(id: 1234) {3 name4 email5 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 totalCount5 }6 data {7 title8 id9 user {10 name11 }12 }13 }14}
خب، برای شروع ما داریم یک query
بدون اسم میفرستیم. به آرگومانهای فیلد posts
دقت کنید! این روش یک راه برای فیلتر کردن جوابهای سرور هست. مثلا من اینجا به posts
گفتم که جوابهاش رو slice کنه، یا در واقع برش بده و در حقیقت تعدادشون [جوابها] رو فقط سه تا کنه.
چیزی که هست اینه که این ساختار رو خود برنامهنویسها برای این سرویس درست کردن، یعنی الزاما شما به (مثلا) آرگومان slice
تو یه سرور گرافکیوال دیگه دسترسی ندارید.
خب حالا بیاید و یکم این دستور رو دستکاری کنیم. بیاین تجسم کنیم که مثلا میخواستیم دو سری post دریافت کنیم. سری اول اونایی که آیدیشون بین ۱ تا ۳ هست، و سری دوم ۴تا از آخرین پستها. فرضا این درخواست مشتری ما بوده یا لازم بوده چنین چیزی فرستاده بشه تا صفحهی اول رو طراحی کنیم!
خب میتونیم همچین چیزی رو ارسال کنیم (حتما انجام بدید چون میخوام نتیجه رو ببینید)
1{2 posts(options: { slice: { start: 1, end: 3 } }) {3 data {4 title5 }6 }78 posts(options: { slice: { limit: 4 }, sort: { order: DESC } }) {9 data {10 title11 }12 }13}
این رو ارسال کنید، یکم صبر میکنم…
دیدید؟ ارور داد! اما چرا؟ دلیلش اینه که شما به سرور دارید میگید که دقیقا یک فیلد رو میخواید اما با دوتا آرگومان کاملا متفاوت! و خب سرور با خودش میگه: بالاخره من کدوم رو باید بدم؟
اصلا فرض کنیم سرور جواب هم داد، جواب آخر یک JSON هست که دقیقا دوتا کلید posts
کنار هم داره! این JSON اساسا دیگه درست یا Valid نیست!
اما راه حل چیه؟
آلیاس یا Alias
آلیاس کردن تو گرافکیوال به این معنیه که شما میتونید اسم واقعی یک فیلد رو، به اون چیزی که میخواید تغییر بدید تا جوابتون رو دریافت کنید. بیاین با یک مثال نشونتون بدم:
1{2 p1: posts(options: { slice: { start: 1, end: 3 } }) {3 data {4 title5 }6 }78 p2: posts(options: { slice: { limit: 4 }, sort: { order: DESC } }) {9 data {10 title11 }12 }13}
اینجا به p1
و p2
کنار کلمات posts
دقت کنید. این دوتا در حال حاضر آلیاسهای posts
هستن. الان مجدد درخواست رو بفرستید و نتیجه رو ببینید.
توی جوابی که گرفتید، اینبار به جای posts
، کلیدهای p1
و p2
رو دارید.
آلیاس کردن رو میتونید روی همهی فیلدها انجام بدید!
خب، تا اینجا که با آلیاسینگ آشنا شدید. حالا بیاین تجسم کنیم که مثلا قرار بود سه جور پست رو دریافت کنیم، دقیقا مشابه درخواست بالا، فقط یه پست دیگه هم اضافه میشه:
1{2 p1: posts(options: { slice: { start: 1, end: 3 } }) {3 data {4 title5 }6 }78 p2: posts(options: { slice: { limit: 4 }, sort: { order: DESC } }) {9 data {10 title11 }12 }1314 p3: posts(options: { slice: { start: 1, end: 3 }, sort: { order: DESC } }) {15 data {16 title17 }18 }19}
خب، تا اینجا ما سه جور پست رو دریافت میکنیم.
برنامهی ما ساخته میشه و مردم ازش استفاده میکنن، اما توی بیزینس یه تغییر ایجاد میشه و لازم میشه که برای پستها، در کنار عنوانشون، مثلا اسم نویسندشون هم نوشته بشه.
توی این حالت، ما باید بریم و درخواست بالا رو اینطور تغییر بدیم:
1{2 p1: posts(options: { slice: { start: 1, end: 3 } }) {3 data {4 title5 user {6 name7 }8 }9 }1011 p2: posts(options: { slice: { limit: 4 }, sort: { order: DESC } }) {12 data {13 title14 user {15 name16 }17 }18 }1920 p3: posts(options: { slice: { start: 1, end: 3 }, sort: { order: DESC } }) {21 data {22 title23 user {24 name25 }26 }27 }28}
من به همهی پستها این رو اضافه کردم:
1user {2 name3}
میتونید تست کنید و نتیجه رو ببینید. اما یه مشکل کوچیک وجود داره: درسته که اضافه کردن سه تا خط واقعا کار سختی نبود، اما تو دنیای واقعی، معمولا اضافهکردن میتونه از این بزرگتر باشه، اما سوای این، اصولا تکرار کردن کار درستی نیست و مخالف اصول DRY (Don’t repeat yourself) یا همون «کار تکراری نکن» هست.
اما چطور میشه اینجا از تکرار جلوگیری کرد؟
فرَگمِنتها یا Fragments:
فرگمنتها برای جلوگیری از تکرا ایدهآل هستن. استفاده ازشون هم به نسبت کار سادهایه. در واقع ایده اینه که شما یک یا چند فرگمنت رو تعریف میکنید و در جای خودشون استفاده میکنید. یک نمونه از یک فرگمنت میتونه این شکلی باشه:
1fragment postsFragment on PostsPage {2 data {3 title4 user {5 name6 }7 }8}
fragment
: یه کلمهی کلیدی مثلquery
،mutation
وsubscription
که مشخص میکنه شما میخواین یه فرگمنت رو تعریف کنید.postsFragment
: اسم فرگمنت که میخواید بسازید.on Post
: شرطی که فرگمنت باید روش اعمال بشه. دقت کنید که اینجا،Post
اسم فیلد نیست، بلکه type فیلده.- مابقی، تمامی فیلدهایی هستن که شما میخواین روی این تایپ استفاده کنید.
خب، کوئری رو به شکل زیر تغییر بدید و بفرستید و نتیجه رو ببینید:
1{2 p1: posts(options: { slice: { start: 1, end: 3 } }) {3 ...postsFragment4 }56 p2: posts(options: { slice: { limit: 4 }, sort: { order: DESC } }) {7 ...postsFragment8 }910 p3: posts(options: { slice: { start: 1, end: 3 }, sort: { order: DESC } }) {11 ...postsFragment12 }13}1415fragment postsFragment on PostsPage {16 data {17 title18 user {19 name20 }21 }22}
متغیرها یا Variables:
خب تا اینجا ما با کوئریهای مختلفی کار کردیم، و در حقیقت، یه استرینگ رو برای سرور فرستادیم تا اون هم به ما جواب بده (یادمون نره که داریم با پروتکل HTTP کار میکنیم). اما تو دنیای واقعی، اکثر برنامهها کوئریها رو بر اساس متغیرها مسازن، یا حداقل بخشیش رو. برای مثال، وقتی که میخواید به یوزر امکان فیلتر کردن رو بدید.
این ایده هم خوب نیست که مستقیما این کوئری رو دستکاری کنیم تا هرچی کاربر بهش میده رو بفرسته برای سرور. برای اینکار، خود گرافکیوال اومده و متغیرها رو برای خودش تعریف کرده.
وقتی که میخوایم با متغیرها کار کنیم، ۳ کار رو باید حتما انجام بدیم:
- مقدار ثابتی که توی کوئری هست رو با
$esmeMoteghayer
عوض کنیم، - متغیر
$esmeMoteghayer
رو به عنوان یکی از پارامترهای کوئری بهش پاس بدیم و - مقدار
esmeMoteghayer
(از عمد علامت دلار رو برداشتم) رو به شکل JSON به همراه کوئری ارسال کنیم.
تو نگاه اول ممکنه سخت و حتی عجیب برسه، اما واقعیت اینه که آسونتر از چیزیه که به نظر میاد! کوئری بالا رو دوباره مینویسم:
1query getPosts($start: Int, $end: Int, $limit: Int, $order: SortOrderEnum) {2 p1: posts(options: { slice: { start: $start, end: $end } }) {3 ...postsFragment4 }56 p2: posts(options: { slice: { limit: $limit }, sort: { order: $order } }) {7 ...postsFragment8 }910 p3: posts(11 options: { slice: { start: $start, end: $end }, sort: { order: $order } }12 ) {13 ...postsFragment14 }15}1617fragment postsFragment on PostsPage {18 data {19 title20 user {21 name22 }23 }24}
تا اینجا قدم ۱ و ۲ رو انجام دادیم. حالا برای شماره ۳، پایین صفحه رو ببینید. میتونید تو قسمت Query Variables مقادیر رو به شکل JSON بنویسید. مثل شکل زیر:
تا اینجا با کوئریها آشنا شدیم، اما چی میشه اگه بخوایم چیزها رو اینبار تغییر بدیم؟
تغییرات یا 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 id6 title7 body8 }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: Query3 mutation: Mutation4}
و بعد
1type Query {2 products: [Product!]!3 product(id: Int!): Product!4}56type 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}78type Query {9 products: [Product!]!10 product(id: Int!): Product!11}1213type Mutation {14 createProduct(input: CreateProductInput): Product!15 updateProduct(input: UpdateProductInput): Product!16 deleteProduct(input: DeleteProductInput): Product!17}1819schema {20 query: Query21 mutation: Mutation22}
گرچه برای تست کردنش لازم هست که سرور خودمون رو بسازیم، اما به این هم خواهیم رسید! اما اینجا ما در واقع شِمای یک گراف رو ساختیم که بتونیم ازش مثلا اینطوری استفاده کنیم:
1{2 products {3 name4 price(exchange: TOMAN)5 }6}
دقت کنید که این مثال هنوز کامل نیست و بخشهاییش رو لازمه بعدا بسازیم! (مثل CreateProductInput
)
تایپهای اسکالر یا Scalar
تایپهای اسکالر، تایپهای ساده و جزئی هستن که توی گرافکیوال استفاده میشن تا انواع مختلفی رو تعریف کنن. این تایپها کلا به دو دسته تقسیم میشن، تایپهای استاندارد و تایپهای دستی یا Manual
تایپهای استاندارد
Int
: عدد صحیح (۳۲ بیت)Float
: عدد اعشاریString
: رشتهی کاراکتری مبتنی بر UTF-8Boolean
: عدد ۲ بیتی،true
یاfalse
ID
: یه تایپ اسکالر که به یه نوع یونیک یا unique اشاره داره که با وجود اینکه باهاش مثل String برخورد میشه، اما الزاما قابل خوندن برای انسان نیست.
تایپهای دستی
گاهی لازم میشه که ما تایپهای خاص خودمون رو بسازیم، برای همین میشه از دستور زیر استفاده کرد:
1scalar Date
که در نهایت میشه موقع ایمپلمنت کردن برنامه، تعیین کرد که حین برخورد با این تایپ باید چه کرد.
اینامها یا Enumerations
نوع خاصی از تایپهای اسکالر هستن که مقادیر خاصی رو میتونن داشته باشن. یعنی خارج از مجموعهی خودشون، نمیشه بهشون مقدار دیگهای داد، تو مثالی که قبلا زدم، میشه به ExchangeType
اشاره کرد که در واقع یک enum
بوده:
1enum ExchangeType {2 EUR3 TOMAN4 USD5}
پس در واقع، یک قدم کاملتر شدهی شِمای ما میشه:
1enum ExchangeType {2 EUR3 TOMAN4 USD5}67type Product {8 id: ID!9 name: String!10 price(exchange: ExchangeType = EUR): Int!11 tags: [String!]!12}1314type Query {15 products: [Product!]!16 product(id: Int!): Product!17}1819type Mutation {20 createProduct(input: CreateProductInput): Product!21 updateProduct(input: UpdateProductInput): Product!22 deleteProduct(input: DeleteProductInput): Product!23}2425schema {26 query: Query27 mutation: Mutation28}
اینترفیسها یا 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}67input UpdateProductInput {8 name: String9 price: Int10 tags: [String!]11}1213input DeleteProductInput {14 id: ID!15}
پس باز هم با یک تغییر تو شِما مواجه میشیم:
1enum ExchangeType {2 EUR3 TOMAN4 USD5}67input CreateProductInput {8 name: String!9 price: Int!10 tags: [String!]!11}1213input UpdateProductInput {14 name: String15 price: Int16 tags: [String!]17}1819input DeleteProductInput {20 id: ID!21}2223type Product implements Model {24 id: ID!25 name: String!26 price(exchange: ExchangeType = EUR): Int!27 tags: [String!]!28}2930type Query {31 products: [Product!]!32 product(id: Int!): Product!33}3435type Mutation {36 createProduct(input: CreateProductInput): Product!37 updateProduct(input: UpdateProductInput): Product!38 deleteProduct(input: DeleteProductInput): Product!39}4041schema {42 query: Query43 mutation: Mutation44}
تا اینجا با اساس کار گرافکیوال آشنا شدیم، خیلی خلاصه البته، ولی دیگه تئوری بسه! بریم که دستمون رو به کد آلوده کنیم.
یک سرور گرافکیوال بسازیم!
تا اینجا با ساختار گرافکیوال آشنا شدیم، اما واقعا تا وقتی کد نزنیم، تئوری هیچ کاربردی نداره (یا حداقل کاربرد زیادی نداره). من اینجا تمرکزم روی ساخت یه سرور گرافکیوال با جاوااسکریپته، گرچه پروسه واسه زبانهای دیگه هم یکسانه، اما چون سرعت کار با جاوااسکریپت بالا میره، منم رو این زبان تمرکز میکنم.
سادهترین شیوه برای شروع، استفاده از کتابخونهی 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 EUR3 TOMAN4 USD5}67interface Model {8 id: ID!9}1011input CreateProductInput {12 name: String!13 price: Int!14 tags: [String!]!15}1617input UpdateProductInput {18 id: ID!19 name: String20 price: Int21 tags: [String!]22}2324input DeleteProductInput {25 id: ID!26}2728type Product implements Model {29 id: ID!30 name: String!31 price(exchange: ExchangeType = EUR): Int!32 tags: [String!]!33}3435type Query {36 products: [Product!]!37 product(id: Int!): Product!38}3940type Mutation {41 createProduct(input: CreateProductInput): Product!42 updateProduct(input: UpdateProductInput): Product!43 deleteProduct(input: DeleteProductInput): Int!44}4546schema {47 query: Query48 mutation: Mutation49}
مدل سازی
یکی از کارهایی که من معمولا تو طراحی سیستمهای گرافکیوال انجام میدم، اینه که دقیقا مدلهایی که تو دیتابیسهستن رو با گرافکیوال تعریف میکنم. مثلا ما تو شِما، یکی از مدلهایی که داریم، همین مدل 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 NULL6);78INSERT 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")34const db = new sqlite.Database("./db/db.sqlite")56if (process.env.INIT_DB) {7 const data = fs.readFileSync("./db/init.sql", {8 encoding: "utf8",9 flag: "r",10 })1112 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")45const db = new sqlite.Database("./db/db.sqlite")67if (process.env.INIT_DB) {8 const data = fs.readFileSync("./db/init.sql", {9 encoding: "utf8",10 flag: "r",11 })1213 console.log("running database migration")14 db.exec(data)15}1617const app = express()18const PORT = process.env.PORT || 30001920app.get("/status", (req, res) => {21 res.json({22 ok: true,23 })24})2526app.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")89const db = new sqlite.Database("./db/db.sqlite")1011if (process.env.INIT_DB) {12 const data = fs.readFileSync("./db/init.sql", {13 encoding: "utf8",14 flag: "r",15 })1617 console.log("running database migration")18 db.exec(data)19}2021const app = express()22const PORT = process.env.PORT || 30002324const typeDefinitions = fs.readFileSync("./schema.graphql")25const schema = makeExecutableSchema({26 typeDefs: String(typeDefinitions),27 resolvers,28})2930app.use(31 "/graphql",32 graphqlHTTP({33 schema,34 graphiql: true,35 })36)3738app.get("/status", (req, res) => {39 res.json({40 ok: true,41 })42})4344app.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)1314module.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")89if (process.env.INIT_DB) {10 const data = fs.readFileSync("./db/init.sql", {11 encoding: "utf8",12 flag: "r",13 })1415 console.log("running database migration")16 db.exec(String(data))17}1819const app = express()20const PORT = process.env.PORT || 30002122const typeDefinitions = fs.readFileSync("./schema.graphql")23const schema = makeExecutableSchema({24 typeDefs: String(typeDefinitions),25 resolvers,26})2728app.use(29 "/graphql",30 graphqlHTTP({31 schema,32 graphiql: true,33 })34)3536app.get("/status", (req, res) => {37 res.json({38 ok: true,39 })40})4142app.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")23module.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 }))1617 resolve(result)18 })19 })20 },21 },22}
تا اینجا چه اتفاقی افتاد؟
ما اول از همه دیتابیس رو require کردیم. اونچیزی که قراره در نهایت ساخته بشه، باید همون چیزی رو دنبال کنه که قرار هست شِمای ما باشه. چون تو شِما، تو فیلد query، ما products رو داریم، اینجا هم باید براش یه فانکشن بسازیم. ایده اینه که هربار که یوزر این فیلد رو کوئری میکنه، ما اینطرف از دیتابیس اطلاعات رو میگیریم و برمیگردونیم.
برای اینکار، گرافکیوال احتیاج داره تا یک Promise رو دریافت کنه، که هرزمان که لازم بود جوابها رو برگردونه. برای همین من اینجا یه Promise رو میسازم. داخل این Promise ما به دیتابیس میگیم که بره همهی ردیفها رو از جدول product بگیره و یک کالبک رو صدا بزنه، این کالبک دوتا پارامتر داره که اولشی اروره، و دومیش همون جوابی که میگیریم یا ستونها.
اینجا یه مشکل کوچیک وجود داره، و این برمیگرده به نحوهی دخیره تو ستون tags تو دیتابیسه. این ستون در واقع یه استرینگ ذخیره میکنه به جای آرایه (فکر میکنم یه راهی وجود داشته باشه تو Sqlite) که ما احتیاج هست یکبار این رو تغییر بدیم. در واقع اونجا که از map استفاده کردم برای حل این مشکل بود.
حالا برنامه رو با دستور زیر اجرا کنید:
1node index.js
و توی گرافیکیوال، کوئری زیر رو بفرستید:
1{2 products {3 id4 name5 price6 tags7 }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 }89 const result = rows10 result.tags = result.tags.split(",")1112 resolve(result)13 })14 })15}
و برای اینکه تستش کنید، این کوئری رو بفرستید:
1{2 product(id: 1) {3 id4 name5 price6 tags7 }8}
بریم سراغ میوتیشنها، من هر آنچه که باید تا انتها نوشته بشه رو اینجا میذارم:
1const db = require("./db/db")23module.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 }))1617 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 }2829 const result = rows30 result.tags = result.tags.split(",")3132 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.name43 }", ${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 product61 SET name = "${args.input.name}",62 price = ${args.input.price},63 tags = "${args.input.tags.join(",")}"64 WHERE65 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 product80 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 id4 name5 }6}78query getOneProduct {9 product(id: 1) {10 id11 name12 }13}1415mutation createNewProduct {16 createProduct(input: { name: "Test", price: 123, tags: ["tag1", "tag2"] }) {17 id18 name19 }20}2122mutation updateAProduct {23 updateProduct(24 input: { id: 1, name: "New Name", price: 9876543, tags: ["new tags"] }25 ) {26 id27 name28 }29}3031mutation deleteAProduct {32 deleteProduct(input: { id: 1 })33}
تست کنید و نتیجه رو ببینید.
خب، ما تونستیم با هم یه سرور کامل گرافکیوال رو راه بندازیم. لازمه که بدم، صرفا برای آموزش، من بخشهای پیادهسازی صحیح رو جا انداختم چون از ابعاد موضوع خارج بودن. اما شما چه چیزهایی به نظرتون میرسه؟ فکر میکنید کجاها چه ایرادایی وجود دارن؟ فکر میکنید میتونید پیداشون کنید؟
حتما سعی میکنم که تو یه تایم بعدی، در مورد این ایرادات و کمبودها و نحوهی درست پیادهسازیشون بنویسم.
ضمنا، برای نوشتن این مطلب زمان زیادی رو گذاشتم تا با شما به اشتراک بذارمش. اگر از این مطلب خوشتون اومده و خواستید جایی منتشرش کنید، اسم من رو فراموش نکنید.