Bỏ qua

Hướng dẫn GraphQL API

Tài liệu tham chiếu cho lớp GraphQL bổ sung tại POST /graphql trê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/:id nhưng thực tế không hề enforce (gap có sẵn từ trước). GraphQL cố tình enforce ADMIN ở đâ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 qua Booking.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.userIdField.ownerId là Keycloak subject id; OwnerApplication.userId lại là primary key nội bộ của user-service. Field user/owner củ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ì sao userId thô không khớp với REST path /users/:id khi 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ỏi field { 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_FOUND kèm giá trị null nghĩ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.