Hướng dẫn GraphQL API
Tài liệu tham chiếu cho lớp GraphQL bổ sung tại
POST /graphqltrên api-gateway. Các REST endpoint không bị thay đổi — GraphQL chỉ là lớp bổ sung song song. Xem "GraphQL Adoption Plan" để biết bối cảnh triển khai.🇬🇧 Bản tiếng Anh: GRAPHQL_API_GUIDE_EN.md
1. Bắt đầu
Mọi thứ đi qua một endpoint duy nhất:
POST /graphql
Content-Type: application/json
Authorization: Bearer <keycloak-jwt> (bỏ qua nếu là query public)
Trình khám phá tương tác:
GET /graphql (mở GraphQL Playground trên trình duyệt — chỉ ở môi trường non-production)
Trang tham chiếu tĩnh kiểu Swagger (tự sinh từ schema):
GET /graphql/docs (chỉ non-production — chạy `yarn docs:graphql` trước để sinh trang)
Lấy token để test thủ công (chỉ realm dev):
curl -s -X POST "http://localhost:8080/realms/football-booking/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password" -d "client_id=api-gateway" -d "client_secret=api-gateway-secret-key-12345" \
-d "username=customer1" -d "password=customer123"
2. Cách đọc bảng
| Cột | Ý nghĩa |
|---|---|
| Auth | Public = không cần token. Auth = cần JWT hợp lệ bất kỳ. ADMIN / FIELD_OWNER, ADMIN = cần đúng role (trả FORBIDDEN nếu sai role, UNAUTHENTICATED nếu không có token). |
| Args | Tên và kiểu tham số GraphQL. ! nghĩa là bắt buộc. |
| Returns | Kiểu trả về GraphQL. [X!]! = danh sách không null gồm các X không null. X (không có !) là nullable — thường trả null kèm lỗi NOT_FOUND khi tài nguyên không tồn tại. |
3. Tham chiếu Root Query
3.1 Booking
| Query | Args | Returns | Auth | Mô tả |
|---|---|---|---|---|
booking |
id: ID! |
Booking |
Auth | Lấy 1 booking theo id, field/user/payment chỉ resolve khi được yêu cầu. |
myBookings |
status: String, page: Int, limit: Int |
[Booking!]! |
Auth | Danh sách booking của chính user hiện tại. Thay thế GET /bookings/me. |
bookingsAdmin |
status: String, page: Int, limit: Int |
[Booking!]! |
ADMIN | Toàn bộ booking của mọi user. Thay thế GET /bookings và alias GET /admin/bookings. |
3.2 Field
| Query | Args | Returns | Auth | Mô tả |
|---|---|---|---|---|
field |
id: ID! |
Field |
Public | Lấy 1 sân theo id, các field lồng (owner/reviews/images/scheduleGrid/availability/bookingStats) chỉ resolve khi được yêu cầu. |
fields |
filter: FieldFilterInput |
[Field!]! |
Public | Tìm/liệt kê sân đang hoạt động — tương ứng GET /fields (không filter) và GET /fields/search (có filter): tên, địa chỉ, bán kính, khoảng giá, rating, tiện ích, sắp xếp, phân trang. |
chatbotSearchFields |
filter: ChatbotFieldFilterInput |
[Field!]! |
Public | Search riêng, không cache, khớp từng từ, tối ưu cho chatbot gọi tool. Thay thế GET /fields/chatbot/search. |
pendingFields |
page: Int, limit: Int |
[Field!]! |
ADMIN | Danh sách sân đang chờ admin duyệt. Thay thế GET /admin/fields/pending. |
Các field lồng của Field:
| Field | Args | Returns | Auth | Mô tả |
|---|---|---|---|---|
owner |
— | User |
Public | Chủ sân. |
reviews |
page: Int, limit: Int |
[FieldReview!]! |
Public | Đánh giá của khách hàng cho sân này. Thay thế GET /fields/:id/reviews (trước đây gateway chưa từng proxy REST endpoint này — năng lực hoàn toàn mới). |
images |
— | [FieldImage!]! |
Public | Ảnh sân theo thứ tự hiển thị. Thay thế GET /fields/:id/images. |
scheduleGrid |
date: String!, openTime: String, closeTime: String, slotMinutes: Int |
ScheduleGrid |
Public | Lưới slot đặt/trống theo từng khung giờ cho 1 ngày. Thay thế GET /bookings/fields/:id/schedule-grid. |
availability |
date: String!, openTime: String, closeTime: String |
FieldAvailability |
Public | Các khung giờ còn trống trong 1 ngày. Thay thế GET /bookings/fields/:id/availability. |
bookingStats |
startDate: String!, endDate: String! |
FieldBookingStats |
Auth | Thống kê booking/doanh thu của sân trong khoảng ngày. Thay thế GET /bookings/fields/:id/statistics. |
3.3 Admin dashboard
| Query | Args | Returns | Auth | Mô tả |
|---|---|---|---|---|
adminDashboard |
— | AdminDashboard! |
ADMIN | Object gốc chỉ để neo — pendingFieldApprovals/today/users bên dưới tự resolve (và tự fail) độc lập. Thay thế GET /admin/dashboard. |
Các field lồng của AdminDashboard:
| Field | Returns | Mô tả |
|---|---|---|
pendingFieldApprovals |
Float |
Số lượng sân đang chờ duyệt (chỉ đếm — dùng pendingFields ở trên để lấy danh sách đầy đủ). |
today |
AdminDashboardToday |
Số booking, doanh thu, tỉ lệ hủy trong ngày hôm nay. |
users |
AdminDashboardUsers |
Tổng số user, phân theo role/status. |
3.4 Owner dashboard
| Query | Args | Returns | Auth | Mô tả |
|---|---|---|---|---|
ownerDashboard |
— | OwnerDashboard! |
FIELD_OWNER, ADMIN | Lấy field id của caller đúng 1 lần; bookings/revenue bên dưới dùng lại thay vì tự gọi lại. Thay thế chuỗi 2 bước GET /bookings/owner/bookings + GET /bookings/owner/revenue. |
Các field lồng của OwnerDashboard:
| Field | Args | Returns | Mô tả |
|---|---|---|---|
fields |
— | [OwnerFieldSummary!]! |
Các sân của chính caller (dạng nhẹ: chỉ id/tên/trạng thái). |
bookings |
status: String, page: Int, limit: Int |
[Booking!]! |
Booking trên tất cả sân của caller. |
revenue |
startDate: String!, endDate: String! |
OwnerRevenue |
Doanh thu + phân theo tháng trên tất cả sân của caller. |
3.5 Payment
| Query | Args | Returns | Auth | Mô tả |
|---|---|---|---|---|
payments |
page: Int, limit: Int |
[Payment!]! |
ADMIN | Toàn bộ giao dịch thanh toán của mọi user. Thay thế GET /payments. |
payment |
id: ID! |
Payment |
ADMIN | Một giao dịch theo id. Thay thế GET /payments/:id. |
Lưu ý — REST có ghi tài liệu trả 403-ADMIN cho
GET /payments/GET /payments/:idnhưng thực tế không hề enforce (gap có sẵn từ trước). GraphQL cố tình enforceADMINở đây thay vì mang gap đó sang lớp mới. Payment của chính khách hàng vẫn truy cập được quaBooking.payment.
Các field lồng của Payment:
| Field | Returns | Mô tả |
|---|---|---|
transactions |
[PaymentTransaction!]! |
Lịch sử webhook/audit trail của giao dịch này. Thay thế GET /payments/:id/transactions. |
3.6 Đơn đăng ký field-owner
| Query | Args | Returns | Auth | Mô tả |
|---|---|---|---|---|
myOwnerApplications |
— | [OwnerApplication!]! |
Auth | Đơn đăng ký field-owner của chính user hiện tại. Thay thế GET /owner-applications/my. |
ownerApplications |
— | [OwnerApplication!]! |
ADMIN | Toàn bộ đơn đăng ký, kèm user của người nộp đơn. Thay thế GET /owner-applications. |
Các field lồng của OwnerApplication:
| Field | Returns | Mô tả |
|---|---|---|
user |
User |
Người nộp đơn. Batch theo internal user id (không phải Keycloak id — xem §5). |
3.7 Danh bạ user cho admin
| Query | Args | Returns | Auth | Mô tả |
|---|---|---|---|---|
users |
filter: UserFilterInput (role, status, search, page, limit) |
[User!]! |
ADMIN | Danh bạ user cho admin, lọc theo role/status/từ khóa. Thay thế GET /admin/users. |
user |
id: ID! |
User |
ADMIN | Một user theo internal id. Thay thế GET /admin/users/:id. |
3.8 Địa danh (dữ liệu tham chiếu)
| Query | Args | Returns | Auth | Mô tả |
|---|---|---|---|---|
provinces |
— | [Province!]! |
Public | Toàn bộ 34 tỉnh/thành (sau sáp nhập 1/7/2025). Thay thế GET /locations/provinces. |
districts |
provinceCode: String! |
[District!]! |
Public | Quận/huyện thuộc 1 tỉnh/thành. Thay thế GET /locations/provinces/:code/districts. |
3.9 Health
| Query | Args | Returns | Auth | Mô tả |
|---|---|---|---|---|
health |
— | String! |
Public | Kiểm tra sống cho chính lớp GraphQL — không thay thế REST nào. |
4. Ví dụ query
Chi tiết booking trong 1 lần gọi:
{
booking(id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890") {
id
status
totalPrice
field {
name
address
hourlyRate
}
user {
name
email
}
payment {
status
paymentMethod
}
}
}
Chi tiết sân kèm ảnh, đánh giá, lịch hôm nay:
{
field(id: "b2c3d4e5-f6a7-8901-bcde-f12345678901") {
name
address
hourlyRate
images {
url
isPrimary
}
reviews(limit: 5) {
rating
comment
userName
}
availability(date: "2026-08-01") {
availableSlots {
startTime
endTime
}
}
}
}
Owner dashboard, 1 request thay vì 2:
{
ownerDashboard {
fields {
id
name
status
}
bookings(status: "confirmed", limit: 10) {
id
startTime
totalPrice
}
revenue(startDate: "2026-01-01", endDate: "2026-12-31") {
totalRevenue
monthly {
month
revenue
bookings
}
}
}
}
Admin dashboard chịu lỗi từng phần:
{
adminDashboard {
pendingFieldApprovals
today {
bookings
revenue
cancellationRate
}
users {
total
byRole
byStatus
}
}
}
Nếu service users đang down, adminDashboard.users trả về null kèm lỗi riêng — pendingFieldApprovals/today vẫn resolve bình thường.
5. Lưu ý đáng biết
Booking.userIdvàField.ownerIdlà Keycloak subject id;OwnerApplication.userIdlại là primary key nội bộ của user-service. Fielduser/ownercủa chúng batch qua 2 DataLoader khác nhau tương ứng — đây là chi tiết implementation, người gọi API không cần quan tâm, nhưng giải thích vì saouserIdthô không khớp với REST path/users/:idkhi tra theo booking/field.- Mọi lookup lồng trong danh sách (
Booking.field/user/payment,Field.owner) đều qua DataLoader theo từng request — hỏifield { name }cho 50 booking chỉ tốn 1 lần gọi downstream đã batch, không phải 50 lần. - Lỗi GraphQL
NOT_FOUNDkèm giá trịnullnghĩa là "id này thực sự không tồn tại" (tương ứng REST 404) — không phải bug và khác với field không được yêu cầu.
6. Mã lỗi
extensions.code |
Ý nghĩa |
|---|---|
UNAUTHENTICATED |
Không có/JWT không hợp lệ. |
FORBIDDEN |
JWT hợp lệ nhưng sai role. |
NOT_FOUND |
Downstream trả 404 — id không tồn tại. |
BAD_USER_INPUT |
Downstream trả 400 — giá trị tham số không hợp lệ. |
SERVICE_UNAVAILABLE |
Một downstream service không thể kết nối được. |
DOWNSTREAM_ERROR |
Downstream trả một mã lỗi khác không phải 2xx. |
7. Những gì cố tình chỉ để REST
Luồng đăng nhập (/auth/*), webhook cổng thanh toán (/webhooks/*), upload file (avatar, ảnh sân, ảnh review), health probe, và công cụ quản trị rate-limit sẽ mãi mãi là REST — xem "GraphQL Adoption Plan" để biết lý do.