Bỏ qua

GraphQL API Guide

Reference for the additive GraphQL layer at POST /graphql on the api-gateway. REST endpoints are untouched — GraphQL sits alongside them. See the "GraphQL Adoption Plan" for the rollout background.

🇻🇳 Vietnamese version: GRAPHQL_API_GUIDE_VI.md

1. Getting started

Everything goes through a single endpoint:

POST /graphql
Content-Type: application/json
Authorization: Bearer <keycloak-jwt>   (omit for public queries)

Interactive explorer:

GET /graphql   (opens GraphQL Playground in a browser — non-production only)

Static, Swagger-style reference (generated from the schema):

GET /graphql/docs   (non-production only — run `yarn docs:graphql` first to generate it)

Getting a token for manual testing (dev realm only):

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. How to read the tables

Column Meaning
Auth Public = no token needed. Auth = any valid JWT. ADMIN / FIELD_OWNER, ADMIN = role required (FORBIDDEN if the role doesn't match, UNAUTHENTICATED if no token at all).
Args GraphQL argument name and type. ! means required.
Returns GraphQL return type. [X!]! = a non-null list of non-null X. A bare X (no !) is nullable — typically returned as null on a downstream 404 alongside a NOT_FOUND error.

3. Root Query reference

3.1 Booking

Query Args Returns Auth Description
booking id: ID! Booking Auth Get one booking by id, with field/user/payment resolved on demand.
myBookings status: String, page: Int, limit: Int [Booking!]! Auth The current user's own bookings. Replaces GET /bookings/me.
bookingsAdmin status: String, page: Int, limit: Int [Booking!]! ADMIN All bookings across all users. Replaces GET /bookings and its alias GET /admin/bookings.

3.2 Field

Query Args Returns Auth Description
field id: ID! Field Public Get one field by id, with owner/reviews/images/scheduleGrid/availability/bookingStats resolved on demand.
fields filter: FieldFilterInput [Field!]! Public Search/list active fields — mirrors GET /fields (no filter) and GET /fields/search (with filter): name, address, geo radius, price range, rating, amenities, sort, paging.
chatbotSearchFields filter: ChatbotFieldFilterInput [Field!]! Public Separate, uncached, word-by-word search tuned for chatbot tool-calls. Replaces GET /fields/chatbot/search.
pendingFields page: Int, limit: Int [Field!]! ADMIN Fields awaiting admin approval. Replaces GET /admin/fields/pending.

Field's nested fields:

Field Args Returns Auth Description
owner User Public The field's owner.
reviews page: Int, limit: Int [FieldReview!]! Public Customer reviews for this field. Replaces GET /fields/:id/reviews (not previously proxied by the gateway at all — new capability).
images [FieldImage!]! Public Field photos in display order. Replaces GET /fields/:id/images.
scheduleGrid date: String!, openTime: String, closeTime: String, slotMinutes: Int ScheduleGrid Public Slot-level booked/available grid for one date. Replaces GET /bookings/fields/:id/schedule-grid.
availability date: String!, openTime: String, closeTime: String FieldAvailability Public Available time slots for one date. Replaces GET /bookings/fields/:id/availability.
bookingStats startDate: String!, endDate: String! FieldBookingStats Auth Booking/revenue statistics for this field over a date range. Replaces GET /bookings/fields/:id/statistics.

3.3 Admin dashboard

Query Args Returns Auth Description
adminDashboard AdminDashboard! ADMIN Root marker object — pendingFieldApprovals/today/users below each resolve (and fail) independently. Replaces GET /admin/dashboard.

AdminDashboard's nested fields:

Field Returns Description
pendingFieldApprovals Float Count of fields awaiting approval (count only — use pendingFields above for the full list).
today AdminDashboardToday Today's booking count, revenue, cancellation rate.
users AdminDashboardUsers Total users, breakdown by role/status.

3.4 Owner dashboard

Query Args Returns Auth Description
ownerDashboard OwnerDashboard! FIELD_OWNER, ADMIN Fetches the caller's field ids once; bookings/revenue below reuse them instead of each re-fetching. Replaces the GET /bookings/owner/bookings + GET /bookings/owner/revenue waterfall.

OwnerDashboard's nested fields:

Field Args Returns Description
fields [OwnerFieldSummary!]! The caller's own fields (lightweight: id/name/status only).
bookings status: String, page: Int, limit: Int [Booking!]! Bookings across all the caller's fields.
revenue startDate: String!, endDate: String! OwnerRevenue Revenue + monthly breakdown across all the caller's fields.

3.5 Payment

Query Args Returns Auth Description
payments page: Int, limit: Int [Payment!]! ADMIN All payments across all users. Replaces GET /payments.
payment id: ID! Payment ADMIN One payment by id. Replaces GET /payments/:id.

Note — REST documents a 403-ADMIN response on GET /payments/GET /payments/:id but never actually enforces it (a pre-existing gap). GraphQL enforces ADMIN here on purpose rather than carrying that gap forward. A customer's own payment is already reachable via Booking.payment.

Payment's nested fields:

Field Returns Description
transactions [PaymentTransaction!]! Provider webhook/audit trail for this payment. Replaces GET /payments/:id/transactions.

3.6 Owner applications

Query Args Returns Auth Description
myOwnerApplications [OwnerApplication!]! Auth The current user's own field-owner applications. Replaces GET /owner-applications/my.
ownerApplications [OwnerApplication!]! ADMIN All applications, with the applicant's user resolved. Replaces GET /owner-applications.

OwnerApplication's nested fields:

Field Returns Description
user User The applicant. Batched by internal user id (not Keycloak id — see §5).

3.7 Admin user directory

Query Args Returns Auth Description
users filter: UserFilterInput (role, status, search, page, limit) [User!]! ADMIN Admin user directory with role/status/search filters. Replaces GET /admin/users.
user id: ID! User ADMIN One user by internal id. Replaces GET /admin/users/:id.

3.8 Locations (reference data)

Query Args Returns Auth Description
provinces [Province!]! Public All 34 provinces/cities (post-1/7/2025 merger). Replaces GET /locations/provinces.
districts provinceCode: String! [District!]! Public Districts within a province. Replaces GET /locations/provinces/:code/districts.

3.9 Health

Query Args Returns Auth Description
health String! Public Liveness check for the GraphQL layer itself — not a REST replacement.

4. Example queries

Booking detail in one round trip:

{
  booking(id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890") {
    id
    status
    totalPrice
    field {
      name
      address
      hourlyRate
    }
    user {
      name
      email
    }
    payment {
      status
      paymentMethod
    }
  }
}

Field detail with photos, reviews and today's schedule:

{
  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, one request instead of two:

{
  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 with partial-failure tolerance:

{
  adminDashboard {
    pendingFieldApprovals
    today {
      bookings
      revenue
      cancellationRate
    }
    users {
      total
      byRole
      byStatus
    }
  }
}

If the users service is down, adminDashboard.users comes back null with its own error entry — pendingFieldApprovals/today still resolve.

5. Notes worth knowing

  • Booking.userId and Field.ownerId are Keycloak subject ids; OwnerApplication.userId is user-service's own internal primary key. Their user/owner fields batch through two different DataLoaders accordingly — this is an implementation detail, not something callers need to think about, but it explains why a raw userId won't match user-service's /users/:id REST path for a booking/field.
  • Every list-nested lookup (Booking.field/user/payment, Field.owner) goes through a per-request DataLoader — asking for field { name } on 50 bookings costs one batched downstream call, not 50.
  • A NOT_FOUND GraphQL error alongside a null value means "this id genuinely doesn't exist" (REST 404 parity) — it is not a bug and not the same as an unrequested/absent field.

6. Error codes

extensions.code Meaning
UNAUTHENTICATED No/invalid JWT.
FORBIDDEN Valid JWT, wrong role.
NOT_FOUND Downstream returned 404 — the id doesn't exist.
BAD_USER_INPUT Downstream returned 400 — invalid argument value.
SERVICE_UNAVAILABLE A downstream service is unreachable (connection refused).
DOWNSTREAM_ERROR Downstream returned some other non-2xx status.

7. What's intentionally REST-only

Auth flows (/auth/*), payment provider webhooks (/webhooks/*), file uploads (avatar, field images, review photos), health probes, and rate-limit admin tooling stay REST forever — see "GraphQL Adoption Plan" for why.