Skip to main content

Query Builder

Build GraphQL queries and mutations programmatically with the fluent Query Builder API. This approach is useful when you need dynamic query construction, code reuse, or want to avoid string concatenation.

Overview

The Query Builder provides a programmatic way to construct GraphQL operations:

FeatureDescription
Fluent APIChain methods for readable query construction
Type SafeFull TypeScript support for variables and fields
ReusableBuild fragments and compose queries
DynamicConstruct queries based on runtime conditions

Basic Usage

Creating a Query

import { createGraphQLClient } from 'wdio-api-runner'

const graphql = createGraphQLClient({
endpoint: 'https://api.example.com/graphql'
})

// Build a query programmatically
const query = graphql.buildQuery('GetUsers')
.select('users', {
fields: ['id', 'name', 'email']
})
.build()

// Execute the built query
const response = await graphql.execute(query)

Generated Query

The above produces:

query GetUsers {
users {
id
name
email
}
}

Building Queries

Simple Selection

Select fields from a root field:

const query = graphql.buildQuery('GetUser')
.addVariable('id', 'ID!')
.select('user', {
args: { id: '$id' },
fields: ['id', 'name', 'email']
})
.build()

// Generates:
// query GetUser($id: ID!) {
// user(id: $id) {
// id
// name
// email
// }
// }

// Execute with variables
const response = await graphql.execute(query, { id: '123' })

Nested Selections

Select nested objects and their fields:

const query = graphql.buildQuery('GetUserWithPosts')
.addVariable('id', 'ID!')
.select('user', {
args: { id: '$id' },
fields: [
'id',
'name',
'email',
{
// Nested object
posts: {
fields: ['id', 'title', 'content', 'publishedAt']
}
},
{
// Another nested object
profile: {
fields: ['bio', 'avatar', 'website']
}
}
]
})
.build()

// Generates:
// query GetUserWithPosts($id: ID!) {
// user(id: $id) {
// id
// name
// email
// posts {
// id
// title
// content
// publishedAt
// }
// profile {
// bio
// avatar
// website
// }
// }
// }

Deeply Nested Selections

Handle multiple levels of nesting:

const query = graphql.buildQuery('GetOrganization')
.addVariable('id', 'ID!')
.select('organization', {
args: { id: '$id' },
fields: [
'id',
'name',
{
departments: {
fields: [
'id',
'name',
{
employees: {
fields: [
'id',
'name',
{
manager: {
fields: ['id', 'name']
}
}
]
}
}
]
}
}
]
})
.build()

Multiple Root Selections

Select multiple root fields in a single query:

const query = graphql.buildQuery('Dashboard')
.select('currentUser', {
fields: ['id', 'name', 'avatar']
})
.select('notifications', {
fields: ['id', 'message', 'read', 'createdAt']
})
.select('stats', {
fields: ['totalUsers', 'activeUsers', 'newUsersToday']
})
.select('recentActivity', {
fields: ['id', 'type', 'description', 'timestamp']
})
.build()

// Generates:
// query Dashboard {
// currentUser { id name avatar }
// notifications { id message read createdAt }
// stats { totalUsers activeUsers newUsersToday }
// recentActivity { id type description timestamp }
// }

Building Mutations

Basic Mutation

const mutation = graphql.buildMutation('CreateUser')
.addVariable('input', 'CreateUserInput!')
.select('createUser', {
args: { input: '$input' },
fields: ['id', 'name', 'email', 'createdAt']
})
.build()

const response = await graphql.execute(mutation, {
input: {
name: 'John Doe',
email: 'john@example.com',
password: 'secure123'
}
})

// Generates:
// mutation CreateUser($input: CreateUserInput!) {
// createUser(input: $input) {
// id
// name
// email
// createdAt
// }
// }

Update Mutation

const mutation = graphql.buildMutation('UpdateUser')
.addVariable('id', 'ID!')
.addVariable('input', 'UpdateUserInput!')
.select('updateUser', {
args: { id: '$id', input: '$input' },
fields: ['id', 'name', 'email', 'updatedAt']
})
.build()

const response = await graphql.execute(mutation, {
id: '123',
input: {
name: 'Jane Doe Updated'
}
})

Delete Mutation

const mutation = graphql.buildMutation('DeleteUser')
.addVariable('id', 'ID!')
.select('deleteUser', {
args: { id: '$id' },
fields: ['success', 'message']
})
.build()

const response = await graphql.execute(mutation, { id: '123' })

Variables

Adding Variables

Define variables with their GraphQL types:

const query = graphql.buildQuery('SearchUsers')
.addVariable('query', 'String!') // Required string
.addVariable('limit', 'Int', 10) // Int with default value
.addVariable('offset', 'Int', 0) // Int with default value
.addVariable('sortBy', 'SortField') // Optional enum
.select('searchUsers', {
args: {
query: '$query',
limit: '$limit',
offset: '$offset',
sortBy: '$sortBy'
},
fields: ['id', 'name', 'relevanceScore']
})
.build()

// Generates:
// query SearchUsers($query: String!, $limit: Int = 10, $offset: Int = 0, $sortBy: SortField) {
// searchUsers(query: $query, limit: $limit, offset: $offset, sortBy: $sortBy) {
// id
// name
// relevanceScore
// }
// }

Variable Types Reference

// Required variable
.addVariable('id', 'ID!')

// Optional variable
.addVariable('name', 'String')

// With default value
.addVariable('limit', 'Int', 10)
.addVariable('status', 'Status', 'ACTIVE')

// Non-null list
.addVariable('ids', '[ID!]!')

// Nullable list with non-null items
.addVariable('tags', '[String!]')

// Input type
.addVariable('input', 'CreateUserInput!')

// Custom scalar
.addVariable('date', 'DateTime!')

Arguments

Static Arguments

Pass literal values as arguments:

const query = graphql.buildQuery('GetActiveUsers')
.select('users', {
args: {
limit: 10,
status: 'ACTIVE',
verified: true
},
fields: ['id', 'name']
})
.build()

// Generates:
// query GetActiveUsers {
// users(limit: 10, status: "ACTIVE", verified: true) {
// id
// name
// }
// }

Variable Arguments

Reference variables in arguments:

const query = graphql.buildQuery('GetUser')
.addVariable('userId', 'ID!')
.select('user', {
args: { id: '$userId' }, // References the variable
fields: ['id', 'name']
})
.build()

Enum Arguments

Pass enum values (not quoted strings):

const query = graphql.buildQuery('GetUsersByStatus')
.select('users', {
args: {
status: { __enum: 'ACTIVE' },
sortBy: { __enum: 'CREATED_AT' },
sortOrder: { __enum: 'DESC' }
},
fields: ['id', 'name', 'status']
})
.build()

// Generates:
// query GetUsersByStatus {
// users(status: ACTIVE, sortBy: CREATED_AT, sortOrder: DESC) {
// id
// name
// status
// }
// }

Mixed Arguments

Combine static, variable, and enum arguments:

const query = graphql.buildQuery('SearchProducts')
.addVariable('searchTerm', 'String!')
.addVariable('maxPrice', 'Float')
.select('products', {
args: {
query: '$searchTerm',
maxPrice: '$maxPrice',
inStock: true,
category: { __enum: 'ELECTRONICS' }
},
fields: ['id', 'name', 'price']
})
.build()

Fragments

Defining Fragments

Create reusable field sets:

const query = graphql.buildQuery('GetUsers')
.addFragment('UserFields', 'User', ['id', 'name', 'email', 'avatar'])
.select('users', {
fields: ['...UserFields', 'createdAt']
})
.select('admins', {
fields: ['...UserFields', 'role', 'permissions']
})
.build()

// Generates:
// query GetUsers {
// users { ...UserFields createdAt }
// admins { ...UserFields role permissions }
// }
// fragment UserFields on User {
// id
// name
// email
// avatar
// }

Fragments with Nested Fields

const query = graphql.buildQuery('GetTeams')
.addFragment('MemberInfo', 'User', [
'id',
'name',
{
profile: {
fields: ['avatar', 'title']
}
}
])
.select('teams', {
fields: [
'id',
'name',
{
members: {
fields: ['...MemberInfo']
}
},
{
lead: {
fields: ['...MemberInfo']
}
}
]
})
.build()

Multiple Fragments

const query = graphql.buildQuery('GetDashboard')
.addFragment('UserBasic', 'User', ['id', 'name'])
.addFragment('PostPreview', 'Post', ['id', 'title', 'excerpt'])
.addFragment('CommentSummary', 'Comment', ['id', 'text', 'author { ...UserBasic }'])
.select('recentPosts', {
fields: [
'...PostPreview',
{ author: { fields: ['...UserBasic'] } },
{ comments: { fields: ['...CommentSummary'] } }
]
})
.build()

Aliases

Use aliases to query the same field with different arguments:

const query = graphql.buildQuery('GetMultipleUsers')
.addVariable('id1', 'ID!')
.addVariable('id2', 'ID!')
.addVariable('id3', 'ID!')
.select('user', {
alias: 'firstUser',
args: { id: '$id1' },
fields: ['id', 'name', 'email']
})
.select('user', {
alias: 'secondUser',
args: { id: '$id2' },
fields: ['id', 'name', 'email']
})
.select('user', {
alias: 'thirdUser',
args: { id: '$id3' },
fields: ['id', 'name', 'email']
})
.build()

// Generates:
// query GetMultipleUsers($id1: ID!, $id2: ID!, $id3: ID!) {
// firstUser: user(id: $id1) { id name email }
// secondUser: user(id: $id2) { id name email }
// thirdUser: user(id: $id3) { id name email }
// }

// Access response
const response = await graphql.execute(query, {
id1: '1',
id2: '2',
id3: '3'
})
const { firstUser, secondUser, thirdUser } = response.data.data

Directives

Include Directive

Conditionally include fields:

const query = graphql.buildQuery('GetUser')
.addVariable('id', 'ID!')
.addVariable('includeEmail', 'Boolean!')
.addVariable('includePosts', 'Boolean!')
.select('user', {
args: { id: '$id' },
fields: [
'id',
'name',
{ email: { directive: '@include(if: $includeEmail)' } },
{
posts: {
directive: '@include(if: $includePosts)',
fields: ['id', 'title']
}
}
]
})
.build()

// Generates:
// query GetUser($id: ID!, $includeEmail: Boolean!, $includePosts: Boolean!) {
// user(id: $id) {
// id
// name
// email @include(if: $includeEmail)
// posts @include(if: $includePosts) {
// id
// title
// }
// }
// }

// Execute with directives
const response = await graphql.execute(query, {
id: '123',
includeEmail: true,
includePosts: false
})

Skip Directive

Conditionally skip fields:

const query = graphql.buildQuery('GetUser')
.addVariable('id', 'ID!')
.addVariable('skipSensitive', 'Boolean!')
.select('user', {
args: { id: '$id' },
fields: [
'id',
'name',
{ email: { directive: '@skip(if: $skipSensitive)' } },
{ phone: { directive: '@skip(if: $skipSensitive)' } }
]
})
.build()

Multiple Directives

const query = graphql.buildQuery('GetUser')
.addVariable('id', 'ID!')
.addVariable('includeDetails', 'Boolean!')
.select('user', {
args: { id: '$id' },
fields: [
'id',
'name',
{
profile: {
directive: '@include(if: $includeDetails) @deprecated(reason: "Use details instead")',
fields: ['bio']
}
}
]
})
.build()

Complex Examples

Complete Dashboard Query

const query = graphql.buildQuery('GetOrganizationDashboard')
.addVariable('orgId', 'ID!')
.addVariable('memberLimit', 'Int', 10)
.addVariable('includeProjects', 'Boolean!')
.addVariable('includeStats', 'Boolean!')
.addFragment('MemberFields', 'User', ['id', 'name', 'role', 'avatar'])
.addFragment('ProjectFields', 'Project', ['id', 'name', 'status', 'deadline'])
.select('organization', {
args: { id: '$orgId' },
fields: [
'id',
'name',
'createdAt',
{
members: {
args: { limit: '$memberLimit' },
fields: ['...MemberFields']
}
},
{
projects: {
directive: '@include(if: $includeProjects)',
fields: [
'...ProjectFields',
{
team: {
fields: ['...MemberFields']
}
}
]
}
},
{
stats: {
directive: '@include(if: $includeStats)',
fields: ['totalMembers', 'activeProjects', 'completedProjects']
}
}
]
})
.build()

const response = await graphql.execute(query, {
orgId: 'org-123',
memberLimit: 5,
includeProjects: true,
includeStats: true
})

Dynamic Query Building

Build queries based on runtime conditions:

function buildUserQuery(options: {
includePosts?: boolean
includeComments?: boolean
includeFollowers?: boolean
}) {
const builder = graphql.buildQuery('GetUser')
.addVariable('id', 'ID!')

const fields: any[] = ['id', 'name', 'email']

if (options.includePosts) {
fields.push({
posts: {
fields: ['id', 'title', 'publishedAt']
}
})
}

if (options.includeComments) {
fields.push({
comments: {
fields: ['id', 'text', 'createdAt']
}
})
}

if (options.includeFollowers) {
fields.push({
followers: {
fields: ['id', 'name']
}
})
}

return builder
.select('user', {
args: { id: '$id' },
fields
})
.build()
}

// Usage
const query = buildUserQuery({
includePosts: true,
includeFollowers: true
})
const response = await graphql.execute(query, { id: '123' })

Pagination Query Builder

function buildPaginatedQuery<T>(
fieldName: string,
itemFields: string[],
options?: { sortable?: boolean }
) {
const builder = graphql.buildQuery(`Get${fieldName}`)
.addVariable('page', 'Int', 1)
.addVariable('limit', 'Int', 20)

if (options?.sortable) {
builder
.addVariable('sortBy', 'String')
.addVariable('sortOrder', 'SortOrder')
}

const args: Record<string, string> = {
page: '$page',
limit: '$limit'
}

if (options?.sortable) {
args.sortBy = '$sortBy'
args.sortOrder = '$sortOrder'
}

return builder
.select(fieldName.toLowerCase(), {
args,
fields: [
{
items: {
fields: itemFields
}
},
'total',
'hasMore',
'currentPage'
]
})
.build()
}

// Usage
const usersQuery = buildPaginatedQuery('Users', ['id', 'name', 'email'], { sortable: true })
const response = await graphql.execute(usersQuery, {
page: 1,
limit: 10,
sortBy: 'name',
sortOrder: 'ASC'
})

Testing with Query Builder

describe('User Queries', () => {
let query: string

beforeAll(() => {
query = graphql.buildQuery('GetUser')
.addVariable('id', 'ID!')
.select('user', {
args: { id: '$id' },
fields: ['id', 'name', 'email']
})
.build()
})

it('should fetch user with valid ID', async () => {
const response = await graphql.execute(query, { id: '123' })

expect(response.isSuccess).toBe(true)
expect(response.data.data.user.id).toBe('123')
})

it('should return error for invalid ID', async () => {
const response = await graphql.execute(query, { id: 'nonexistent' })

expect(response.isError).toBe(true)
})
})

Best Practices

1. Name Your Operations

Always provide meaningful operation names:

// Good
graphql.buildQuery('GetUserProfile')
graphql.buildMutation('UpdateUserSettings')

// Avoid
graphql.buildQuery('Query1')

2. Use Fragments for Reusability

Extract common field sets:

// Define once
.addFragment('UserBasic', 'User', ['id', 'name', 'avatar'])

// Use everywhere
.select('author', { fields: ['...UserBasic'] })
.select('reviewer', { fields: ['...UserBasic'] })

3. Build Queries Once, Execute Many Times

// Build once
const getUserQuery = graphql.buildQuery('GetUser')
.addVariable('id', 'ID!')
.select('user', { args: { id: '$id' }, fields: ['id', 'name'] })
.build()

// Execute many times
const user1 = await graphql.execute(getUserQuery, { id: '1' })
const user2 = await graphql.execute(getUserQuery, { id: '2' })

4. Use TypeScript for Variable Types

interface GetUserVars {
id: string
}

const response = await graphql.execute<UserResponse, GetUserVars>(query, {
id: '123' // TypeScript validates this
})

Next Steps