/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:
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:
# 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:
# 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:
| Parameter | Type | Description | Max Value |
|---|---|---|---|
first | Int | Number of records from the beginning | 250 |
last | Int | Number of records from the end | 250 |
after | String | Cursor to start after (forward) | - |
before | String | Cursor to start before (backward) | - |
Best Practices
- Always check for more pages:
query CheckForMore {
listJobs(first: 20, after: $cursor) {
count
results { ... }
endKeyset
# If endKeyset is null, you've reached the end
}
}- Handle empty results gracefully:
const hasMore = response.endKeyset !== null;
const items = response.results || [];- 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:
input JobFilterInput {
id: IDFilterInput
state: JobStateEnumFilterInput
createdAt: DateTimeFilterInput
name: StringFilterInput
customer: CustomerFilterInput
# ... more fields
}Common Filter Operations
String Filters
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
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
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:
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:
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:
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
query SortByCreatedDate {
listJobs(
sort: [{ field: CREATED_AT, order: DESC }]
) {
results {
id
createdAt
jobNumber
}
}
}Multi-Level Sorting
Sort by multiple fields in priority order:
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
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:
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
mutation CreateJobWithErrorHandling {
createJob(input: {
name: "Test Job"
quantity: 1000
}) {
result {
id
jobNumber
name
}
errors {
field
message
code
}
}
}Client-side handling:
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
{
"errors": [
{
"field": "quantity",
"message": "Quantity must be greater than 0",
"code": "VALIDATION_ERROR"
}
]
}Authorization Errors
{
"errors": [
{
"field": null,
"message": "You don't have permission to create jobs",
"code": "UNAUTHORIZED"
}
]
}Business Logic Errors
{
"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:
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 }
}
}Fetching Related Data
Efficiently fetch related data in one query:
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
# 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
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:
- Job Management - Creating and managing print jobs
- Components and Media - Working with job components