Skip to content

/library/api# Core GraphQL Patterns

Overview

This guide covers the fundamental patterns you'll use throughout the CoCore GraphQL API. These patterns are consistent across all domains, making it easy to work with different resources once you understand the basics.

Keyset Pagination

Understanding Keyset Pagination

Unlike traditional offset-based pagination, keyset pagination provides:

  • Consistent results even when data changes
  • Better performance on large datasets
  • Bidirectional navigation with forward and backward cursors

Basic Usage

Every list query returns a KeysetPageOf[Type] structure:

graphql
type KeysetPageOfJob {
  count: Int              # Total records across all pages
  results: [Job!]         # Records in this page
  startKeyset: String     # Cursor for the first record
  endKeyset: String       # Cursor for the last record
}

Forward Pagination

Navigate through results from the beginning:

graphql
# First page - get the first 20 jobs
query FirstPage {
  listJobs(first: 20) {
    count
    results {
      id
      jobNumber
      name
    }
    endKeyset  # Save this for the next page
  }
}

# Next page - continue from where we left off
query NextPage {
  listJobs(first: 20, after: "eyJpZCI6MTIzfQ==") {
    count
    results {
      id
      jobNumber
      name
    }
    endKeyset
  }
}

Backward Pagination

Navigate backwards through results:

graphql
# Get the last 20 jobs
query LastPage {
  listJobs(last: 20) {
    count
    results {
      id
      jobNumber
      name
    }
    startKeyset  # Save this for the previous page
  }
}

# Previous page
query PreviousPage {
  listJobs(last: 20, before: "eyJpZCI6NDU2fQ==") {
    count
    results {
      id
      jobNumber
      name
    }
    startKeyset
  }
}

Pagination Parameters

All list queries support these pagination arguments:

ParameterTypeDescriptionMax Value
firstIntNumber of records from the beginning250
lastIntNumber of records from the end250
afterStringCursor to start after (forward)-
beforeStringCursor to start before (backward)-

Best Practices

  1. Always check for more pages:
graphql
query CheckForMore {
  listJobs(first: 20, after: $cursor) {
    count
    results { ... }
    endKeyset
    # If endKeyset is null, you've reached the end
  }
}
  1. Handle empty results gracefully:
javascript
const hasMore = response.endKeyset !== null;
const items = response.results || [];
  1. Use appropriate page sizes:
  • Small pages (10-20) for UI lists
  • Larger pages (50-100) for data processing
  • Maximum 250 for bulk operations

Filtering

Filter Structure

Each resource has a corresponding filter input type with field-specific operations:

graphql
input JobFilterInput {
  id: IDFilterInput
  state: JobStateEnumFilterInput
  createdAt: DateTimeFilterInput
  name: StringFilterInput
  customer: CustomerFilterInput
  # ... more fields
}

Common Filter Operations

String Filters

graphql
query StringFilterExamples {
  # Exact match
  jobsByName: listJobs(filter: {
    name: { eq: "Business Cards" }
  }) { ... }

  # Pattern matching
  jobsLike: listJobs(filter: {
    name: { like: "%Business%" }  # SQL LIKE pattern
  }) { ... }

  # Case-insensitive pattern
  jobsILike: listJobs(filter: {
    name: { ilike: "%business%" }
  }) { ... }

  # Multiple values
  jobsIn: listJobs(filter: {
    name: { in: ["Job A", "Job B", "Job C"] }
  }) { ... }
}

Numeric and Date Filters

graphql
query NumericFilterExamples {
  # Comparison operators
  recentJobs: listJobs(filter: {
    createdAt: {
      greaterThan: "2024-01-01T00:00:00Z"
      lessThanOrEqual: "2024-12-31T23:59:59Z"
    }
  }) { ... }

  # Range queries
  mediumQuantityJobs: listJobs(filter: {
    quantity: {
      greaterThanOrEqual: 100
      lessThan: 1000
    }
  }) { ... }
}

Enum Filters

graphql
query EnumFilterExamples {
  # Single state
  productionJobs: listJobs(filter: {
    state: { eq: IN_PRODUCTION }
  }) { ... }

  # Multiple states
  activeJobs: listJobs(filter: {
    state: { in: [IN_PRODUCTION, PROOFING, PENDING_APPROVAL] }
  }) { ... }

  # Exclude states
  notCompletedJobs: listJobs(filter: {
    state: { notEq: COMPLETED }
  }) { ... }
}

Relationship Filters

Filter based on related entities:

graphql
query RelationshipFilterExamples {
  # Jobs for a specific customer
  customerJobs: listJobs(filter: {
    customer: {
      id: { eq: "customer-123" }
    }
  }) { ... }

  # Jobs with specific component types
  coverJobs: listJobs(filter: {
    components: {
      kind: { eq: COVER }
    }
  }) { ... }

  # Orders with specific product requirements
  rushOrders: listOrders(filter: {
    requestedDeliveryDate: {
      lessThan: "2024-02-01"
    }
    orderLines: {
      rushOrder: { eq: true }
    }
  }) { ... }
}

Complex Filter Combinations

Combine multiple filters with AND logic:

graphql
query ComplexFilter {
  listJobs(filter: {
    state: { in: [IN_PRODUCTION, PROOFING] }
    createdAt: { greaterThan: "2024-01-01" }
    customer: {
      creditStatus: { eq: APPROVED }
    }
    components: {
      media: {
        type: { eq: PAPER }
        weight: {
          value: { greaterThanOrEqual: 80 }
        }
      }
    }
  }) {
    results {
      id
      jobNumber
      name
    }
  }
}

Sorting

Sort Structure

Sorting uses an array of sort specifications:

graphql
input JobSortInput {
  field: JobSortField!
  order: SortOrder!
}

enum JobSortField {
  ID
  CREATED_AT
  UPDATED_AT
  JOB_NUMBER
  NAME
  STATE
  PRIORITY
  DUE_DATE
}

enum SortOrder {
  ASC
  DESC
}

Single Field Sorting

graphql
query SortByCreatedDate {
  listJobs(
    sort: [{ field: CREATED_AT, order: DESC }]
  ) {
    results {
      id
      createdAt
      jobNumber
    }
  }
}

Multi-Level Sorting

Sort by multiple fields in priority order:

graphql
query MultiLevelSort {
  listJobs(
    sort: [
      { field: PRIORITY, order: DESC },      # First by priority
      { field: DUE_DATE, order: ASC },       # Then by due date
      { field: CREATED_AT, order: ASC }      # Finally by creation
    ]
  ) {
    results {
      id
      priority
      dueDate
      createdAt
    }
  }
}

Combining with Filters and Pagination

graphql
query CombinedQuery {
  listJobs(
    # Filter
    filter: {
      state: { eq: IN_PRODUCTION }
      priority: { greaterThanOrEqual: 5 }
    }
    # Sort
    sort: [
      { field: PRIORITY, order: DESC },
      { field: DUE_DATE, order: ASC }
    ]
    # Paginate
    first: 20
    after: $cursor
  ) {
    count
    results {
      id
      jobNumber
      priority
      dueDate
    }
    endKeyset
  }
}

Error Handling

Mutation Result Types

All mutations return a result type with consistent error handling:

graphql
type CreateJobResult {
  result: Job           # The created resource (if successful)
  errors: [MutationError!]  # List of errors (if any)
}

type MutationError {
  field: String         # Field that caused the error
  message: String!      # Human-readable error message
  code: String         # Machine-readable error code
}

Handling Success and Errors

graphql
mutation CreateJobWithErrorHandling {
  createJob(input: {
    name: "Test Job"
    quantity: 1000
  }) {
    result {
      id
      jobNumber
      name
    }
    errors {
      field
      message
      code
    }
  }
}

Client-side handling:

javascript
const response = await client.mutate({
  mutation: CREATE_JOB,
  variables: { input: jobData }
});

if (response.data.createJob.errors?.length > 0) {
  // Handle errors
  response.data.createJob.errors.forEach(error => {
    console.error(`Error in ${error.field}: ${error.message}`);
  });
} else {
  // Success
  const job = response.data.createJob.result;
  console.log(`Created job ${job.jobNumber}`);
}

Common Error Patterns

Validation Errors

json
{
  "errors": [
    {
      "field": "quantity",
      "message": "Quantity must be greater than 0",
      "code": "VALIDATION_ERROR"
    }
  ]
}

Authorization Errors

json
{
  "errors": [
    {
      "field": null,
      "message": "You don't have permission to create jobs",
      "code": "UNAUTHORIZED"
    }
  ]
}

Business Logic Errors

json
{
  "errors": [
    {
      "field": "customerId",
      "message": "Customer credit limit exceeded",
      "code": "CREDIT_LIMIT_EXCEEDED"
    }
  ]
}

Batch Operations

Multiple Mutations in One Request

GraphQL allows multiple operations in a single request:

graphql
mutation BatchCreate {
  job1: createJob(input: { name: "Job 1", quantity: 100 }) {
    result { id, jobNumber }
    errors { message }
  }

  job2: createJob(input: { name: "Job 2", quantity: 200 }) {
    result { id, jobNumber }
    errors { message }
  }

  component1: createComponent(input: {
    jobId: $jobId,
    kind: CONTENT
  }) {
    result { id }
    errors { message }
  }
}

Efficiently fetch related data in one query:

graphql
query JobWithAllDetails {
  getJob(id: $jobId) {
    id
    jobNumber
    name
    state

    # Related customer
    customer {
      id
      name
      creditStatus
    }

    # Components with pagination
    components(first: 10) {
      results {
        id
        kind
        media {
          name
          type
          weight { value, unit }
        }
        layout {
          pages
          sides
          closedDimensions {
            width { value, unit }
            height { value, unit }
          }
        }
      }
    }

    # Operations
    operations(
      filter: { state: { in: [PENDING, IN_PROGRESS] } }
      sort: [{ field: SEQUENCE, order: ASC }]
    ) {
      results {
        id
        name
        state
        sequence
        device { id, name }
      }
    }
  }
}

Field Selection Best Practices

Request Only What You Need

graphql
# Bad - Over-fetching
query OverFetch {
  listJobs {
    results {
      # Fetching everything even if not needed
      id
      externalId
      jobNumber
      name
      description
      state
      priority
      quantity
      createdAt
      updatedAt
      # ... many more fields
    }
  }
}

# Good - Precise selection
query PreciseFetch {
  listJobs {
    results {
      id
      jobNumber
      name
      state
    }
  }
}

Use Fragments for Reusable Selections

graphql
fragment JobBasics on Job {
  id
  jobNumber
  name
  state
}

fragment JobProduction on Job {
  priority
  dueDate
  quantity
  currentOperation {
    name
    state
  }
}

query JobsWithFragments {
  pendingJobs: listJobs(filter: { state: { eq: PENDING } }) {
    results {
      ...JobBasics
    }
  }

  productionJobs: listJobs(filter: { state: { eq: IN_PRODUCTION } }) {
    results {
      ...JobBasics
      ...JobProduction
    }
  }
}

Performance Considerations

1. Pagination Limits

  • Maximum 250 records per page
  • Use smaller pages for real-time UI
  • Larger pages for batch processing

2. Deep Nesting

  • Avoid deeply nested queries (>3-4 levels)
  • Use separate queries for complex relationships
  • Consider query complexity limits

3. Filter Optimization

  • Use indexed fields in filters when possible
  • Combine filters to reduce result sets early
  • Avoid expensive pattern matching on large datasets

4. Caching Strategies

  • Use stable IDs for cache keys
  • Leverage GraphQL client caching (Apollo, Relay)
  • Consider field-level cache policies

Next Steps

Now that you understand the core patterns, explore:

Connect. Combine. Collaborate.
The pioneering open integration platform, dedicated to transforming connectivity in the printing industry.