Skip to content

API Reference

Excalibase GraphQL automatically generates a complete GraphQL schema from your database tables, views, and tracked functions. This page documents the shape of that schema.

Default endpoints: - PostgreSQL: http://localhost:10000/graphql - MySQL: http://localhost:10001/graphql


Schema Introspection

Explore the full auto-generated schema:

{
  __schema {
    types {
      name
      fields { name type { name kind ofType { name } } }
    }
  }
}

Or check what mutations are available:

{
  __type(name: "Mutation") {
    fields { name args { name type { name kind } } }
  }
}

Queries

For every table and view, three query fields are generated:

Field Description
{schema}{Table} List with filtering, sorting, pagination
{schema}{Table}Connection Cursor-based (Relay) pagination
{schema}{Table}Aggregate Aggregate functions (count, sum, avg, min, max)

List Query

{
  hanaCustomer(
    where: { active: { eq: true } }
    orderBy: { last_name: ASC }
    limit: 20
    offset: 0
  ) {
    customer_id
    first_name
    last_name
    email
  }
}

Arguments:

Argument Type Description
where {Table}WhereInput Filter conditions
orderBy {Table}OrderByInput Sort direction per column
limit Int Max rows to return
offset Int Rows to skip

Connection Query (Cursor Pagination)

{
  hanaCustomerConnection(first: 10, after: "cursor_value") {
    edges {
      node {
        customer_id
        first_name
        last_name
      }
      cursor
    }
    pageInfo {
      hasNextPage
      hasPreviousPage
      startCursor
      endCursor
    }
    totalCount
  }
}

Aggregate Query

PostgreSQL aggregate fields return nested per-column results over the numeric columns, narrowed by the same where as the list field (introspection shows both):

{
  hanaOrdersAggregate(where: { status: { eq: "paid" } }) {
    count
    sum { total_amount }
    avg { total_amount }
    min { total_amount }
    max { total_amount }
  }
}

An array relationship has the same aggregate under <relationship>Aggregate, over the related rows, with its own where (Hasura's <relationship>_aggregate). It exists where the role is allowed aggregations on the related table:

{
  hanaCustomers {
    name
    hanaOrdersAggregate(where: { status: { eq: "paid" } }) { count sum { total_amount } }
  }
}

MySQL aggregate fields return flat scalars:

{
  excalibaseOrdersAggregate {
    count
    sum
    avg
    min
    max
  }
}

Mutations

For every table, the following mutations are generated:

Mutation Description
create{Schema}{Table} Insert one row
createMany{Schema}{Table} Bulk insert
update{Schema}{Table} Update one row
delete{Schema}{Table} Delete one row, returns the deleted row
{schema}{FunctionName} Call a tracked VOLATILE function (see Functions)

Create

mutation {
  createHanaCustomer(input: {
    first_name: "Alice"
    last_name: "Smith"
    email: "alice@example.com"
  }) {
    customer_id
    first_name
  }
}

Bulk Create

mutation {
  createManyHanaCustomer(inputs: [
    { first_name: "Bob", last_name: "Jones", email: "bob@example.com" }
    { first_name: "Carol", last_name: "White", email: "carol@example.com" }
  ]) {
    customer_id
    first_name
  }
}

Update

PostgreSQL — PK is part of the input object:

mutation {
  updateHanaCustomer(input: { customer_id: 1, email: "new@example.com" }) {
    customer_id
    email
  }
}

MySQL — PK is a separate id argument:

mutation {
  updateExcalibaseCustomer(id: 1, input: { email: "new@example.com" }) {
    customer_id
    email
  }
}

Delete

Delete returns the deleted row so you can update your UI without a separate fetch:

mutation {
  deleteHanaCustomer(input: { customer_id: 1 }) {
    customer_id
    first_name
    last_name
  }
}

Tracked function call

A function the project tracks, and the role may call, is a root field: STABLE/IMMUTABLE functions on Query, VOLATILE ones on Mutation. It returns rows of its return table, filtered and projected by the role's select permission on that table. Procedures are never exposed.

mutation {
  hanaTransferBetweenWallets(p_from: 1, p_to: 2, p_amount: 200.00) {
    wallet_id
    balance
  }
}

See Functions → for full documentation.


Filtering

Every query accepts a where argument with per-column filter inputs.

Available Operators

Operator Types Example
eq All { status: { eq: "active" } }
neq All { status: { neq: "cancelled" } }
gt / gte Numeric, Date { total_amount: { gte: 100 } }
lt / lte Numeric, Date { price: { lte: 50 } }
in All { status: { in: ["pending", "shipped"] } }
notIn All { status: { notIn: ["cancelled"] } }
isNull All { email: { isNull: true } }
isNotNull All { email: { isNotNull: true } }
contains String { last_name: { contains: "smith" } }
startsWith String { email: { startsWith: "alice" } }
endsWith String { email: { endsWith: "@example.com" } }
like String { email: { like: "%@gmail%" } }
ilike String (PG) { name: { ilike: "%alice%" } }

JSON Operators (PostgreSQL)

Operator Description
hasKey JSON object contains this key
hasKeys JSON object contains all these keys
contains JSON is a superset of this value
containedBy JSON is a subset of this value
path JSON path exists
pathText JSON path value equals text
{
  hanaEnhancedTypes(where: {
    jsonb_col: {
      hasKey: "score"
      contains: "{\"active\": true}"
    }
  }) {
    id
    name
    jsonb_col
  }
}

Array Operators (PostgreSQL)

Operator Description
contains Array contains this element
hasAny Array overlaps with these values
hasAll Array contains all these values
{
  hanaEnhancedTypes(where: {
    text_array: { hasAny: ["postgresql", "graphql"] }
  }) {
    id
    name
    text_array
  }
}

OR Conditions

{
  hanaCustomer(or: [
    { first_name: { eq: "MARY" } }
    { first_name: { eq: "JOHN" } }
  ]) {
    customer_id
    first_name
    last_name
  }
}

Relationships

Forward (Many-to-One)

A FK column adds an object relationship named after the column without its _id (customer_id → hanaCustomer, home_team_id → publicHomeTeam):

{
  hanaOrders {
    order_id
    hanaCustomer {
      first_name
      last_name
    }
  }
}

The older name built from the whole column (hanaCustomerId) still answers and is marked deprecated in introspection. A name that would clash with a column or another relationship is not added.

Reverse (One-to-Many)

The referenced table gets a list field for the rows that point to it. It takes the same where, orderBy, limit and offset as a table's list (the role's own row limit still applies):

{
  hanaCustomer {
    customer_id
    first_name
    hanaOrders(where: { status: { eq: "paid" } }, orderBy: { order_date: DESC }, limit: 5) {
      order_id
      total_amount
    }
  }
}

When a table points at another through several FKs (matches.home_team_id, matches.away_team_id), each list is named by its key: publicMatchesByHomeTeam, publicMatchesByAwayTeam. The older names (publicMatchesHomeTeamId, ...) still answer and are deprecated.

Aliases

Aliases work at every depth, inside relationships, connections, aggregates and mutation results: { publicMatches { matchId: id home: publicHomeTeam { teamName: name } } }.

bigint and decimal

bigint and numeric/decimal columns are read as strings holding every digit ("9223372036854775807", "19.90"), in GraphQL and REST, on Postgres and MySQL. A JSON number past 2^53, or with more than about 15 significant digits, is rounded by JavaScript's parser. Filters and writes take either a string or a number. Sums and averages over these columns are strings too.

Composite Foreign Keys (PostgreSQL)

Multi-column FKs work the same way — include all FK columns:

{
  hanaChildTable {
    child_id
    parent_id1
    parent_id2
    description
    hanaParentTable {
      name
    }
  }
}

Computed Fields (PostgreSQL)

Functions of one table row (customer_full_name(c customer)) are reflected but not exposed for GA, to any role, service included: they are functions the project does not track, and an untracked function is unreachable. Selecting one answers Unknown field(s): <name>. Use a view, or a tracked function that returns rows of a table. A later step may let a function be tracked as a computed field.


Views (Read-Only)

Views and materialized views appear as read-only query fields (no mutations generated):

# PostgreSQL views from initdb.sql
{ hanaActiveCustomers { customer_id first_name last_name email } }
{ hanaPostsWithAuthors { id title author_username published } }
{ hanaEnhancedTypesSummary { id name json_name array_size } }

# MySQL views
{ excalibaseActiveCustomers { customer_id first_name last_name email } }
{ excalibaseOrdersSummary { customer_id first_name order_count total_spent } }
{ excalibaseHighValueOrders { order_id total status first_name last_name } }

Composite Primary Keys (PostgreSQL)

Tables with composite PKs work with all CRUD operations. All PK columns are required for update/delete:

# Create
mutation {
  createHanaOrderItems(input: { order_id: 1, product_id: 2, quantity: 3, price: 59.99 }) {
    order_id product_id quantity price
  }
}

# Update — all PK columns required
mutation {
  updateHanaOrderItems(input: { order_id: 1, product_id: 2, quantity: 5 }) {
    order_id product_id quantity
  }
}

# Delete — all PK columns required
mutation {
  deleteHanaOrderItems(input: { order_id: 1, product_id: 2 }) {
    order_id product_id
  }
}

Row-Level Security (PostgreSQL)

Send a JWT in the Authorization header. Excalibase verifies the token and sets the userId claim as the request.user_id PostgreSQL session variable before executing the query:

POST /graphql HTTP/1.1
Authorization: Bearer eyJhbGciOiJFUzI1NiJ9...
Content-Type: application/json

With this RLS policy in place:

CREATE POLICY user_isolation ON rls_orders
  FOR ALL USING (user_id = current_setting('request.user_id', true));

Each user sees only their own rows — no application-level filtering needed. Requires jwt-enabled: true and a configured auth.jwks-url or public key.

A mutation whose written rows fail the role's permission check (inserting a row the caller could not own, or reassigning a row out of their permission) rolls back whole and fails with a typed error instead of database text:

{
  "errors": [
    {
      "message": "A written row does not pass the role's permission check",
      "extensions": { "code": "permission_check_failed" }
    }
  ]
}

Match on extensions.code: permission_check_failed for a failed check, missing_session_variable / invalid_session_variable when a permission needs a session variable the request lacks or cannot convert. Reads never raise — filtered rows are simply absent.

See API permissions → for the model and the error contract.


Real-Time Subscriptions (PostgreSQL)

Excalibase uses PostgreSQL logical replication (CDC) to push live changes over WebSocket:

subscription {
  hanaCustomerChanges {
    operation
    data { customer_id first_name last_name }
  }
}

See Subscriptions → for setup and configuration.


Type Mappings

BIGINT and NUMERIC/DECIMAL are strings in GraphQL and REST alike: a 64-bit integer or an exact decimal does not fit a JavaScript number. Inputs take a string or a number.

PostgreSQL

DB Type GraphQL Type
INTEGER, SMALLINT Int
BIGINT BigInteger, sent as a string ("9007199254740993")
NUMERIC, DECIMAL BigDecimal, sent as a string ("19.90")
REAL, DOUBLE PRECISION Float
TEXT, VARCHAR, CHAR String
BOOLEAN Boolean
DATE, TIMESTAMP, TIMESTAMPTZ String
TIMETZ, INTERVAL String
JSON, JSONB JSON (custom scalar)
INTEGER[], TEXT[], etc. [Int], [String], etc.
INET, CIDR, MACADDR String
BYTEA String (hex-encoded)
XML String
BIT(n), VARBIT(n) String
UUID ID
Custom ENUM GraphQL enum type
Custom composite type GraphQL object type
Domain type Mapped to base type

MySQL

DB Type GraphQL Type
INT, BIGINT, SMALLINT, TINYINT Int
FLOAT, DOUBLE, DECIMAL Float
VARCHAR, TEXT, CHAR String
TINYINT(1) Boolean
DATE, DATETIME, TIMESTAMP String
JSON String (JSON-serialized)
ENUM(...) GraphQL enum type

Security

All queries use parameterized statements — SQL injection is not possible through the GraphQL interface. Input is validated against the generated type schema before execution.

Query depth is limited to prevent overly complex nested queries from reaching the database.