🤔 GraphQL có nên khác nhau cho các người dùng khác nhau?
GraphQL là một giao diện để lấy dữ liệu từ một nguồn nào đó, với GraphQL spec định nghĩa các yêu cầu cho giao diện đó. Miễn là các yêu cầu này được đáp ứng, GraphQL không quan tâm đến cách thực hiện. Do đó, server GraphQL có thể được triển khai bằng JavaScript sử dụng promises, bằng kiến trúc đồng thời dựa trên Golang, được ánh xạ tới một file Excel, hay bất cứ thứ gì khác, và tất cả những cách này đều có thể là các triển khai hợp lệ của GraphQL spec.

Cách engine của server được triển khai không quan trọng đối với việc thực thi thành công một yêu cầu GraphQL, vì sự tương tác giữa client và server luôn như nhau: thực hiện bằng cách gửi một query GraphQL sử dụng một cú pháp đã định nghĩa, và nhận về một phản hồi tương ứng ở định dạng JSON.
Bây giờ, khi tôi nói rằng việc triển khai không quan trọng, tôi muốn nói điều đó từ góc nhìn của người dùng API — người chỉ đơn giản muốn lấy dữ liệu từ server. Dữ liệu trả về được tạo ra như thế nào không phải là điều họ quan tâm.
Nhưng tình huống thay đổi đối với nhà phát triển phía server làm việc trên API — những người mà các chi tiết triển khai thực sự rất quan trọng. Nếu tôi viết code API GraphQL bằng PHP, tôi sẽ cố gắng hết sức để API của mình được xử lý hiệu quả nhất có thể, với thiết kế kiến trúc thanh lịch nhất có thể, sử dụng các khả năng mà PHP cung cấp.

Vậy là có thể phát sinh xung đột lợi ích giữa nhu cầu bảo vệ API và các khả năng kỳ vọng của các nhà phát triển làm việc trên API — những người không muốn bị tước bỏ các tính năng được hỗ trợ bởi ngôn ngữ nền tảng (chẳng hạn như khả năng thực thi code đệ quy).
Xung đột này đã trở nên rõ ràng trong issue #929: Allow recursive references in fragments, trong đó lập luận rằng GraphQL không nên cấm đệ quy trong các fragment.
Trong một buổi meetup trước đây của nhóm làm việc GraphQL, Roman — nhà phát triển đã đặt ra issue này — đã bày tỏ lý do tại sao ông không đồng ý với giới hạn mà spec đặt ra:
Tôi là một nhà phát triển phía server, và tôi cảm thấy rằng spec nói quá nhiều về việc thực thi phía server, trong khi lẽ ra nó nên tập trung vào những gì client muốn nhận được — không phải cách thực hiện
Quy tắc cấm đệ quy trong các fragment đã được biện minh dựa trên tiền đề giữ cho API công khai được an toàn. Suy cho cùng, GraphQL được Facebook tạo ra để cung cấp dữ liệu cho ứng dụng hướng đến người dùng của họ, và người dùng không nên có khả năng khai thác một lỗ hổng trong thiết kế API có thể làm sập dịch vụ.
Người tạo ra GraphQL, Lee Byron, đã bày tỏ ba mối lo ngại chính:
đệ quy vô hạn; các giới hạn sẽ không chỉ là quy định spec — nó phải dừng như thế nào và khi nào
xác thực dữ liệu; trả về cùng một giá trị nhiều lần, điều đó được biểu diễn như thế nào trong dữ liệu. Lý tưởng nhất là bạn muốn phát hiện ra nó là vòng lặp và dừng ngay, nhưng một số server không thể phát hiện điều này và có thể lặp nhiều lần trước khi phát hiện ra sự cố và dừng lại
chi phí của việc không có tính năng này là gì; nó có biện minh cho những vấn đề đó không? Không có; bạn luôn có thể chỉ định số cấp độ sâu trong query của mình — đó thực chất là phiên bản "desugared" của những gì chúng tôi sẽ làm nếu xử lý điều này trong GraphQL
Từ góc nhìn của từng người, cả Roman lẫn Lee đều có lý. Lee Byron lo lắng về sự an toàn của API GraphQL công khai. Việc tránh các fragment đệ quy là có lý để đảm bảo rằng không có kẻ tấn công nào có thể làm sập hệ thống bằng cách thực thi một vòng lặp đệ quy không bao giờ kết thúc trong query, và thậm chí loại bỏ khả năng một nhóm "tự DDoS", điều có thể xảy ra nếu họ vô tình xuất bản một query làm treo hệ thống.
Tuy nhiên, Roman lại lo lắng về những hạn chế đối với khả năng của chính ông trong việc tạo API GraphQL. Vì Roman có thể là người dùng duy nhất của API của mình (tức là một API riêng tư không được tiếp xúc với người dùng bên ngoài), hoặc vì server của ông có khả năng phát hiện và dừng các vòng lặp đệ quy, ông cho rằng giới hạn của GraphQL là có hại và không thể biện minh được.
Cốt lõi của cuộc thảo luận, vấn đề không phải là liệu các fragment đệ quy có nên được cho phép hay không, mà là điều gì đó cơ bản hơn: Mục tiêu của GraphQL là ai? Nếu không phải chỉ một nhóm, liệu một spec API duy nhất có thể thỏa mãn các yêu cầu từ tất cả các bên liên quan khác nhau không? Và nếu xung đột không thể ngăn chặn được, liệu nó có thể được giải quyết theo một cách nào đó không?
Hãy cùng khám phá những câu hỏi này.
GraphQL hướng đến ai?
GraphQL đang được sử dụng bởi các loại bên liên quan khác nhau, trong đó chúng ta có thể xác định:
1. Người dùng API: Những người sử dụng dữ liệu từ một endpoint GraphQL nào đó, vì bất kỳ lý do gì. Ví dụ, tất cả chúng ta đều có thể là người dùng API của API GraphQL công khai của GitHub, để lấy dữ liệu liên quan đến các repository GitHub của mình.
2. Nhà phát triển phía client: Những người tạo ra các ứng dụng phía client được hỗ trợ bởi một endpoint GraphQL nào đó. Ví dụ, các nhà phát triển xây dựng trang web với Gatsby dựa vào GraphQL để lấy nội dung cho trang.
3. Nhà phát triển backend: Những người tạo ra các resolver cho API GraphQL.
Ngoài ra, chúng ta cần lưu ý rằng API GraphQL có thể là công khai hoặc riêng tư:
API công khai: Vì bất kỳ ai cũng có quyền truy cập vào endpoint GraphQL, chúng ta cần lo lắng về các biện pháp bảo mật để tránh các cuộc tấn công bởi các tác nhân độc hại.
API riêng tư: Vì chỉ các tác nhân được phép mới có quyền truy cập API, không có rủi ro bảo mật cố hữu nào, và việc tự DDoS có thể dễ dàng tránh được bằng các thực hành code tốt.
Một spec API duy nhất có đáp ứng yêu cầu của tất cả các bên liên quan không?
Issue mà Roman đặt ra có thể được hiểu như thế này: "Nếu API GraphQL của tôi là riêng tư, và tôi biết chính xác mình đang làm gì (với 100% chắc chắn rằng code của tôi sẽ hoạt động như mong đợi và không có quá trình thực thi nào bị treo), thì tại sao tôi không thể sử dụng đệ quy trong các fragment?"

Một ví dụ về tình huống này xảy ra bất cứ khi nào chúng ta sử dụng một framework được hỗ trợ bởi GraphQL để xây dựng các trang web tĩnh (chẳng hạn như Gatsby, Next.js hoặc RedwoodJS), vì API GraphQL thường sẽ là riêng tư, và chúng ta không thể vô tình DDoS ứng dụng của mình và chịu hậu quả bất lợi (nhiều nhất là nó sẽ bị crash khi xây dựng trang tĩnh trong môi trường phát triển hoặc staging).
Các nhà phát triển sử dụng thiết lập trên hoàn toàn có thể thắc mắc tại sao GraphQL spec lại cấm họ sử dụng các tính năng hữu ích, mà không có hậu quả bất lợi nào đối với thiết lập của họ.
Kết luận, bằng cách cấm các fragment đệ quy, GraphQL spec đang áp đặt một biện pháp bảo mật áp dụng cho một lựa chọn trong tất cả các cách sử dụng tiềm năng của GraphQL, chứ không phải tất cả, để đảm bảo an toàn.
GraphQL spec có thể thỏa mãn tốt hơn tất cả các bên liên quan không?
Nếu các bên liên quan khác nhau có các yêu cầu khác nhau, GraphQL spec có thể thỏa mãn tất cả họ như thế nào? (Ý tưởng là tránh fork spec và tạo ra các phiên bản tùy chỉnh cho các mục tiêu cụ thể.)
Hãy khám phá một vài ý tưởng, trong đó ý tưởng đầu tiên sẽ cần phải đi qua quy trình đóng góp spec, trong khi ý tưởng thứ hai thì không.
Feature-toggle ở cấp độ GraphQL spec
Một lộ trình có thể thực hiện là để spec "gợi ý" nhưng không "áp đặt" các quy tắc. Trong trường hợp này, quy tắc cấm đệ quy trong các fragment có thể được gợi ý mạnh mẽ, nhưng tính năng vẫn sẽ được chấp nhận.
Bây giờ, giải pháp này sẽ thay đổi điều kiện mặc định của các fragment đệ quy từ "bắt buộc" sang "tùy chọn", điều này sẽ tạo ra hai hậu quả tiêu cực:
- API sẽ không an toàn theo mặc định (kịch bản mà Lee Byron muốn tránh)
- Nó sẽ tạo ra một breaking change, vì một query bị cấm sẽ được phép
Vì vậy, sẽ tốt hơn nếu đảo ngược tùy chọn, giữ cho đệ quy trong các fragment vẫn bị cấm theo mặc định nhưng cho phép bật một feature-flag để tắt hành vi này. Vì tính năng phải được tắt rõ ràng, nó sẽ chỉ được thực hiện bởi các quản trị viên biết mình đang làm gì.
Vì tính năng có giá trị nhất trong một số thiết lập nhất định, các server và framework GraphQL có thể quyết định liệu/cách/khi nào cung cấp cấu hình. Ví dụ, Gatsby có thể hiển thị nổi bật tùy chọn thông qua giao diện người dùng khi tạo các trang tĩnh, và ẩn nó đi trong các trường hợp khác.
Ý tưởng chung là GraphQL spec hỗ trợ "các tính năng được bật nhưng tùy chọn", có thể được bật/tắt thông qua cấu hình, và trạng thái mặc định của chúng là trạng thái mà chúng đã có trong spec.
Việc cấm các fragment đệ quy sẽ là một trong số đó, và cũng có thể có các tính năng như vậy khác, chẳng hạn như kiểu Map, mà Lee Byron đã không chấp nhận vào spec vì:
Có những đánh đổi đáng kể giữa kiểu Map và danh sách các cặp key/value. Một vấn đề là phân trang trên collection. Danh sách các giá trị có thể có các quy tắc phân trang rõ ràng trong khi các Map thường có các cặp key-value không có thứ tự thì khó phân trang hơn nhiều.
Một vấn đề khác là cách sử dụng. Thông thường Map được sử dụng trong các API nơi một trường của giá trị đang được lập chỉ mục, điều mà theo ý kiến của tôi là một anti-pattern của API vì việc lập chỉ mục là vấn đề lưu trữ và vấn đề caching phía client nhưng không phải vấn đề truyền tải. Anti-pattern này khiến tôi lo ngại. Mặc dù có một số cách sử dụng tốt cho Map trong các API, tôi lo rằng cách sử dụng phổ biến sẽ là cho những anti-pattern này, vì vậy tôi đề nghị tiến hành thận trọng.
Lee Byron đã bày tỏ lo ngại rằng tính năng này sẽ được sử dụng như một anti-pattern. Tuy nhiên, ông cũng thừa nhận rằng có những cách sử dụng tốt cho nó. Vậy thì, vì issue này đã nhận được nhiều sự ủng hộ từ cộng đồng (với hơn 150 👍), các nhà phát triển có thể được tùy chọn bật rõ ràng việc thêm kiểu Map vào các schema của họ, và tự xử lý các hậu quả.
Feature-toggle bởi các server GraphQL
Nếu đề xuất trên không nhận được sự ủng hộ vì nó quá rủi ro cho GraphQL spec, một giải pháp thay thế là triển khai nó ở cấp độ server GraphQL. Khi đó, các server GraphQL có thể cung cấp tính năng tùy chỉnh tắt đệ quy trong các fragment.
Tổng quát hóa ý tưởng, các server GraphQL có thể cung cấp việc tắt một số tính năng nhất định từ spec, và bật những tính năng khác còn thiếu trong spec. Để hành vi này không tạo ra sự bất ngờ, các server phải đảm bảo rằng trạng thái mặc định là trạng thái được yêu cầu bởi spec, và quản trị viên của API phải được thông báo đầy đủ về hậu quả của việc bật/tắt tính năng. (Đây là chiến lược mà Gato GraphQL theo đuổi cho "innovative features" của mình.)
Kết luận
Khi GraphQL ngày càng trở nên phổ biến hơn, các framework mới hỗ trợ các khả năng mới đã tích hợp nó vào stack của họ, và các bên liên quan mới (và các loại mới của họ) đã tham gia. Vậy là một spec ban đầu được Facebook tạo ra để định nghĩa cách các ứng dụng của họ sẽ lấy dữ liệu từ các server của họ ngày càng phải đối mặt với nhiều trường hợp sử dụng hơn.
Không thể tránh khỏi các xung đột nảy sinh, khi một nhóm bên liên quan cần một tính năng mà lại phản tác dụng, hoặc thậm chí có hại, đối với các bên liên quan khác, như trường hợp với các fragment đệ quy. Điều gì có thể được thực hiện để cải thiện tình hình và tránh để các bên liên quan không hài lòng không bị thất vọng với GraphQL?
Tôi đã lập luận rằng spec có thể cung cấp cơ hội để "tắt" một tính năng, cho phép các quản trị viên biết mình đang làm gì loại bỏ một số giới hạn để thỏa mãn yêu cầu của riêng họ. Bây giờ, bản thân tôi không đồng ý với giải pháp này, nhưng tôi vẫn đưa ra để thảo luận vì cuộc trò chuyện này cần được tiến hành. Vì ý tưởng này còn gây tranh cãi, một giải pháp thay thế tốt hơn là các server GraphQL cung cấp hành vi này thông qua các tính năng tùy chỉnh, cần được bật rõ ràng.