บล็อก

🤔 GraphQL ควรจะแตกต่างกันสำหรับผู้ใช้แต่ละกลุ่มหรือไม่?

Leonardo Losoviz
โดย Leonardo Losoviz ·

GraphQL คือ interface สำหรับดึงข้อมูลจากต้นทาง โดยมี GraphQL spec เป็นตัวกำหนดข้อกำหนดของ interface นั้น ตราบใดที่ข้อกำหนดเหล่านี้ได้รับการปฏิบัติตาม GraphQL ก็ไม่สนใจว่าจะบรรลุผลด้วยวิธีใด GraphQL server สามารถถูกพัฒนาด้วย JavaScript โดยใช้ promises ด้วยสถาปัตยกรรม concurrent บน Golang แมปกับไฟล์ Excel หรือวิธีอื่น ๆ และทั้งหมดนี้ล้วนเป็นการพัฒนาที่ถูกต้องตาม GraphQL spec

GraphQL อยู่ระหว่าง client กับ backend services

วิธีที่ engine ของ server ถูกพัฒนานั้นไม่มีความสำคัญต่อการดำเนินการ GraphQL request ที่สำเร็จ เพราะการโต้ตอบระหว่าง client และ server นั้นเหมือนกันเสมอ คือการส่ง GraphQL query โดยใช้ syntax ที่กำหนด และรับ response ที่สอดคล้องกันในรูปแบบ JSON

เมื่อฉันพูดว่าการพัฒนาไม่สำคัญ หมายความว่าฉันมองจากมุมมองของผู้ใช้ API ซึ่งเพียงแค่ต้องการดึงข้อมูลจาก server ไม่มีความสนใจว่าข้อมูลที่ส่งคืนนั้นถูกสร้างขึ้นอย่างไร

แต่สถานการณ์จะเปลี่ยนไปสำหรับนักพัฒนา server-side ที่ทำงานบน API เพราะรายละเอียดของการพัฒนานั้นสำคัญมากสำหรับพวกเขา ถ้าฉันเขียน GraphQL API ด้วย PHP ฉันจะทำสิ่งที่ดีที่สุดเพื่อให้ API ของฉันถูก resolve อย่างมีประสิทธิภาพสูงสุดเท่าที่จะทำได้ และมีการออกแบบสถาปัตยกรรมที่สง่างามที่สุดเท่าที่จะเป็นไปได้ โดยใช้ความสามารถที่ PHP มอบให้

PHP vs Java vs JavaScript

จากนั้น เราก็พบความขัดแย้งผลประโยชน์ที่อาจเกิดขึ้นระหว่างความจำเป็นในการปกป้อง API และความสามารถที่คาดหวังโดยนักพัฒนาที่ทำงานบน API ซึ่งไม่ต้องการให้คุณสมบัติที่รองรับโดยภาษาพื้นฐานถูกพรากไปจากพวกเขา (เช่น ความสามารถในการรันโค้ดแบบ recursive)

ความขัดแย้งนี้ปรากฏชัดในประเด็น #929: Allow recursive references in fragments ซึ่งโต้แย้งว่า GraphQL ไม่ควรห้ามการ recursion ใน fragments

ในการประชุม GraphQL working group ครั้งหนึ่งที่ผ่านมา Roman ซึ่งเป็นนักพัฒนาที่ยกประเด็นนี้ขึ้นมา ได้แสดงเหตุผลว่าทำไมเขาจึงไม่เห็นด้วยกับข้อจำกัดที่กำหนดโดย spec:

ฉันเป็นนักพัฒนา server-side และรู้สึกว่า spec พูดถึงการดำเนินการ server-side มากเกินไป ในขณะที่ควรจะมุ่งเน้นไปที่สิ่งที่ client ต้องการได้รับ ไม่ใช่วิธีการ

กฎที่ห้าม recursion ใน fragments ได้รับการพิสูจน์บนพื้นฐานของการรักษาความปลอดภัยของ public API ท้ายที่สุด GraphQL ถูกสร้างโดย Facebook เพื่อส่งข้อมูลให้กับแอปพลิเคชันสาธารณะของพวกเขา และผู้ใช้ไม่ควรสามารถใช้ประโยชน์จากข้อบกพร่องในการออกแบบ API เพื่อทำให้บริการหยุดทำงานได้

Lee Byron ผู้สร้าง GraphQL ได้แสดงความกังวลหลักสามประการ:

infinite recursion; ข้อจำกัดไม่ได้อยู่แค่ที่ spec เท่านั้น — ควรหยุดอย่างไรและเมื่อใด

การตรวจสอบข้อมูล; การส่งคืนค่าเดิมหลายครั้ง จะแสดงอย่างไรในข้อมูล ตามอุดมคติคุณต้องการตรวจจับว่ามันเป็น cyclic และหยุดทันที แต่บางเซิร์ฟเวอร์ไม่สามารถตรวจจับสิ่งนี้ได้และอาจวนซ้ำหลายครั้งก่อนที่จะตรวจพบว่ามีบางอย่างผิดปกติและหยุด

ต้นทุนของการไม่มีสิ่งนี้คืออะไร; มันคุ้มค่ากับปัญหาเหล่านี้หรือ? ไม่คุ้ม; คุณสามารถระบุจำนวนระดับความลึกใน query ได้เสมอ — นั่นคือเวอร์ชัน desugared อย่างแท้จริงของสิ่งที่เราจะทำหาก GraphQL จัดการกับมัน

จากมุมมองของตัวเอง ทั้ง Roman และ Lee ต่างก็ถูกต้อง Lee Byron กังวลเรื่องความปลอดภัยของ public GraphQL API การหลีกเลี่ยง recursive fragments ได้รับการพิสูจน์เพื่อให้แน่ใจว่าผู้กระทำที่เป็นอันตรายไม่สามารถทำให้ระบบหยุดทำงานโดยการรัน cyclic loop ที่ไม่มีที่สิ้นสุดใน query และยังขจัดโอกาสที่ทีมจะ "self-DDoS" ซึ่งอาจเกิดขึ้นหากพวกเขาเผยแพร่ query ที่ทำให้ระบบหยุดโดยไม่ตั้งใจ

อย่างไรก็ตาม Roman กังวลเกี่ยวกับข้อจำกัดต่อความสามารถของตัวเองในการสร้าง GraphQL API เพราะ Roman อาจเป็นผู้ใช้ API เพียงคนเดียว (กล่าวคือ private API ที่ไม่ได้เปิดเผยต่อผู้ใช้) หรือเพราะ server ของเขาอาจมีความสามารถในการตรวจจับและหยุด recurring cycles ดังนั้นเขาจึงเชื่อว่าข้อจำกัดของ GraphQL นั้นเป็นอันตรายและไม่สามารถพิสูจน์ได้

ที่แก่นของการอภิปราย ประเด็นไม่ใช่ว่าควรอนุญาต recursive fragments หรือไม่ แต่เป็นสิ่งที่พื้นฐานกว่านั้น: GraphQL มีเป้าหมายเพื่อใคร? หากไม่ใช่กลุ่มเดียว spec ของ API เพียงชุดเดียวจะตอบสนองความต้องการของผู้มีส่วนได้ส่วนเสียที่แตกต่างกันทั้งหมดได้หรือไม่? และหากความขัดแย้งไม่สามารถป้องกันได้ อย่างน้อยสามารถบรรเทาได้บ้างหรือเปล่า?

ลองสำรวจคำถามเหล่านี้กัน

GraphQL มีเป้าหมายเพื่อใคร?

GraphQL ถูกใช้โดยผู้มีส่วนได้ส่วนเสียหลายประเภท ซึ่งเราสามารถระบุได้ดังนี้:

1. ผู้ใช้ API: ผู้ที่ดึงข้อมูลจาก GraphQL endpoint ด้วยเหตุผลใดก็ตาม ตัวอย่างเช่น เราทุกคนสามารถเป็นผู้ใช้ API ของ public GraphQL API ของ GitHub เพื่อดึงข้อมูลเกี่ยวกับ GitHub repos ของเรา

2. นักพัฒนา client-side: ผู้ที่สร้างแอปพลิเคชัน client-side ที่ขับเคลื่อนด้วย GraphQL endpoint นักพัฒนาที่สร้างไซต์ด้วย Gatsby ตัวอย่างเช่น พึ่งพา GraphQL ในการดึงเนื้อหาสำหรับไซต์

3. นักพัฒนา backend: ผู้ที่สร้าง resolvers สำหรับ GraphQL API

นอกจากนี้ เราต้องสังเกตว่า GraphQL API สามารถเป็น public หรือ private ได้:

Public API: เพราะทุกคนสามารถเข้าถึง GraphQL endpoint ได้ เราต้องกังวลเรื่องมาตรการรักษาความปลอดภัยเพื่อหลีกเลี่ยงการโจมตีจากผู้กระทำที่เป็นอันตราย

Private API: เพราะเฉพาะผู้กระทำที่ตั้งใจเท่านั้นที่ได้รับสิทธิ์เข้าถึง API จึงไม่มีความเสี่ยงด้านความปลอดภัยโดยธรรมชาติ และการ self-DDoS สามารถหลีกเลี่ยงได้อย่างง่ายดายด้วยแนวทางการเขียนโค้ดที่ดี

spec ของ API เพียงชุดเดียวตอบสนองความต้องการของผู้มีส่วนได้ส่วนเสียทั้งหมดได้หรือไม่?

ประเด็นที่ Roman ยกขึ้นสามารถตีความได้ดังนี้: "ถ้า GraphQL API ของฉันเป็น private และฉันรู้ว่ากำลังทำอะไรอยู่ (มั่นใจ 100% ว่าโค้ดจะทำงานตามที่คาดหวังและไม่มีการหยุดทำงาน) แล้วทำไมฉันถึงไม่สามารถใช้ recursion ใน fragments ได้?"

Recursions @ xkcd

ตัวอย่างของสถานการณ์นี้เกิดขึ้นเมื่อเราใช้ framework ที่รองรับ GraphQL สำหรับสร้างไซต์แบบ static (เช่น Gatsby, Next.js หรือ RedwoodJS) เพราะ GraphQL API มักจะเป็น private และเราไม่สามารถ DDoS แอปพลิเคชันของเราโดยไม่ตั้งใจและได้รับผลกระทบที่ไม่พึงประสงค์ (อย่างมากก็แค่ crash เมื่อสร้างไซต์ static บน development หรือ staging environment)

นักพัฒนาที่ใช้การตั้งค่าดังกล่าวอาจสงสัยอย่างเต็มที่ว่าทำไม GraphQL spec จึงห้ามพวกเขาใช้คุณสมบัติที่มีประโยชน์ ซึ่งไม่มีผลกระทบที่ไม่พึงประสงค์ใด ๆ เลย สำหรับการตั้งค่าของพวกเขา

สรุปแล้ว ด้วยการห้าม recursive fragments GraphQL spec กำลังกำหนดมาตรการรักษาความปลอดภัยที่ใช้กับการใช้งาน GraphQL ที่เป็นไปได้ทั้งหมดเพียงบางส่วน ไม่ใช่ทั้งหมด เพื่อความปลอดภัย

GraphQL spec สามารถตอบสนองผู้มีส่วนได้ส่วนเสียทั้งหมดได้ดีขึ้นหรือไม่?

หากผู้มีส่วนได้ส่วนเสียที่แตกต่างกันมีความต้องการที่แตกต่างกัน GraphQL spec จะสามารถตอบสนองทุกคนได้อย่างไร? (แนวคิดคือเพื่อหลีกเลี่ยงการ fork spec และสร้างเวอร์ชันที่ปรับแต่งสำหรับเป้าหมายเฉพาะ)

ลองสำรวจแนวคิดสองประการ โดยประการแรกจะต้องผ่านกระบวนการ spec-contribution ในขณะที่ประการที่สองจะไม่

Feature-toggle ในระดับ GraphQL spec

เส้นทางหนึ่งที่เป็นไปได้คือให้ spec "แนะนำ" แต่ไม่ "บังคับ" กฎ ในกรณีนี้ กฎที่ห้าม recursion ใน fragments อาจถูกแนะนำอย่างแข็งกร้าว แต่คุณสมบัตินั้นก็ยังคงได้รับการยอมรับ

อย่างไรก็ตาม วิธีแก้ปัญหานี้จะเปลี่ยนเงื่อนไขเริ่มต้นของ recursive fragments จาก "บังคับ" เป็น "ตัวเลือก" ซึ่งจะก่อให้เกิดผลกระทบเชิงลบสองประการ:

  • API จะไม่ปลอดภัยโดยค่าเริ่มต้น (สถานการณ์ที่ Lee Byron ต้องการหลีกเลี่ยง)
  • จะก่อให้เกิด breaking change เนื่องจาก query ที่ถูกห้ามจะได้รับอนุญาต

ดังนั้น จะดีกว่าถ้ากลับตัวเลือกนั้น โดยให้ recursion ใน fragments ยังคงถูกห้ามโดยค่าเริ่มต้น แต่ให้ความเป็นไปได้ในการเปิด feature-flag ที่ปิดการทำงานนี้ เนื่องจากคุณสมบัตินั้นต้องถูกปิดใช้งานอย่างชัดเจน จึงจะทำได้เฉพาะโดยผู้ดูแลระบบที่รู้ว่ากำลังทำอะไรอยู่เท่านั้น

เนื่องจากคุณสมบัตินี้มีคุณค่ามากที่สุดภายใต้การตั้งค่าบางอย่าง GraphQL servers และ frameworks สามารถตัดสินใจได้ว่าจะเสนอการกำหนดค่าหรือไม่ อย่างไร และเมื่อใด ตัวอย่างเช่น Gatsby สามารถแสดงตัวเลือกผ่าน UI อย่างเด่นชัดเมื่อสร้างไซต์ static และซ่อนไว้เมื่อไม่ได้สร้าง

แนวคิดทั่วไปคือให้ GraphQL spec รองรับ "คุณสมบัติที่เปิดใช้งานแต่เป็นทางเลือก" ซึ่งสามารถเปิด/ปิดผ่านการกำหนดค่า และสถานะเริ่มต้นคือสถานะที่มีอยู่แล้วใน spec

การห้าม recursive fragments จะเป็นหนึ่งในนั้น และอาจมีคุณสมบัติอื่น ๆ เช่นกัน เช่น Map type ซึ่งไม่ได้รับการยอมรับสำหรับ spec โดย Lee Byron เพราะ:

มีข้อแลกเปลี่ยนที่สำคัญระหว่าง Map type กับ list ของ key/value pairs ปัญหาหนึ่งคือการ paginate ผ่าน collection รายการค่าสามารถมีกฎ pagination ที่ชัดเจน ในขณะที่ Maps ที่มักมี key-value pairs ที่ไม่เรียงลำดับนั้นยาก pagination กว่ามาก

ปัญหาอีกประการหนึ่งคือการใช้งาน ส่วนใหญ่แล้ว Map ถูกใช้ภายใน APIs ที่ field หนึ่งของค่าถูก index ซึ่งในความเห็นของฉันเป็น API anti-pattern เนื่องจาก indexing เป็นปัญหาของ storage และปัญหาของ client caching แต่ไม่ใช่ปัญหาของ transport anti-pattern นี้เป็นที่น่ากังวล แม้จะมีการใช้งาน Map ที่ดีใน APIs แต่ฉันกลัวว่าการใช้งานทั่วไปจะเพื่อ anti-patterns เหล่านี้ ดังนั้นฉันจึงแนะนำให้ดำเนินการด้วยความระมัดระวัง

Lee Byron แสดงความกลัวว่าคุณสมบัตินั้นจะถูกใช้เป็น anti-pattern อย่างไรก็ตาม เขายังยอมรับว่ามีการใช้งานที่ดีสำหรับมัน ดังนั้น เนื่องจากประเด็นนี้ได้รับการสนับสนุนจากชุมชนจำนวนมาก (กว่า 150 👍) นักพัฒนาสามารถได้รับตัวเลือกในการเปิดใช้งานการเพิ่ม Map type ให้กับ schemas ของพวกเขาอย่างชัดเจน และจัดการกับผลที่ตามมา

Feature-toggle โดย GraphQL servers

หากข้อเสนอข้างต้นไม่ได้รับการสนับสนุนเนื่องจากมีความเสี่ยงสูงเกินไปสำหรับ GraphQL spec ทางเลือกคือการดำเนินการในระดับ GraphQL server GraphQL servers สามารถให้คุณสมบัติที่กำหนดเองซึ่งปิดการใช้งาน recursion ใน fragments

เมื่อทำให้แนวคิดเป็นแบบทั่วไป GraphQL servers สามารถเสนอการปิดใช้งานคุณสมบัติบางอย่างจาก spec และเปิดใช้งานคุณสมบัติอื่น ๆ ที่ขาดหายไปจาก spec เพื่อไม่ให้พฤติกรรมนี้ก่อให้เกิดความประหลาดใจ servers ต้องแน่ใจว่าสถานะเริ่มต้นคือสิ่งที่ spec กำหนด และผู้ดูแลระบบของ API ต้องทราบถึงผลที่ตามมาของการเปิด/ปิดคุณสมบัตินั้นอย่างเต็มที่ (นี่คือกลยุทธ์ที่ Gato GraphQL ใช้สำหรับ "innovative features" ของมัน)

สรุป

เมื่อ GraphQL ได้รับความนิยมเพิ่มขึ้นเรื่อย ๆ frameworks ใหม่ที่รองรับความสามารถใหม่ได้นำมันเป็นส่วนหนึ่งของ stack และผู้มีส่วนได้ส่วนเสียใหม่ (และประเภทใหม่ของพวกเขา) ก็ได้เข้ามามีส่วนร่วม ดังนั้น spec ที่สร้างขึ้นโดย Facebook เพื่อกำหนดว่าแอปพลิเคชันของพวกเขาจะดึงข้อมูลจาก servers อย่างไร จึงต้องรับมือกับ use cases มากขึ้นเรื่อย ๆ

มันหลีกเลี่ยงไม่ได้ที่ความขัดแย้งจะเกิดขึ้น เมื่อกลุ่มผู้มีส่วนได้ส่วนเสียต้องการคุณสมบัติที่ขัดประโยชน์ หรือแม้แต่เป็นอันตรายต่อผู้มีส่วนได้ส่วนเสียอื่น ๆ เช่นในกรณีของ recursive fragments สิ่งที่สามารถทำเพื่อปรับปรุงสถานการณ์ และหลีกเลี่ยงไม่ให้ผู้มีส่วนได้ส่วนเสียที่ไม่พอใจผิดหวังกับ GraphQL?

ฉันได้โต้แย้งว่า spec สามารถเสนอโอกาสในการ "ปิดใช้งาน" คุณสมบัติ ช่วยให้ผู้ดูแลระบบที่รู้ว่ากำลังทำอะไรอยู่สามารถลบข้อจำกัดบางอย่างเพื่อตอบสนองความต้องการของตัวเอง อย่างไรก็ตาม ฉันเองก็ไม่เห็นด้วยกับวิธีแก้ปัญหานี้ แต่ฉันก็นำมันออกสู่สาธารณะเพราะการอภิปรายนี้จำเป็นต้องเกิดขึ้น เนื่องจากแนวคิดนี้ก่อให้เกิดความขัดแย้ง ทางเลือกที่ดีกว่าคือให้ GraphQL servers ให้พฤติกรรมนี้ผ่านคุณสมบัติที่กำหนดเอง ซึ่งต้องถูกเปิดใช้งานอย่างชัดเจน


สมัครรับจดหมายข่าวของเรา

ติดตามการอัปเดตทั้งหมดของ Gato GraphQL