Skip to main content

Common Patterns

This guide covers common patterns you’ll use throughout the SensorUp GraphQL API.

Pagination

The API uses Relay-style cursor-based pagination for all list queries. This provides consistent forward and backward pagination with cursor stability.

Connection Structure

type Connection {
  edges: [Edge]
  pageInfo: PageInfo!
}

type Edge {
  node: NodeType
  cursor: String
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

Forward Pagination

query GetAssets($first: Int!, $after: String) {
  assetProfiles {
    byId(profile: "your-profile") {
      assets(first: $first, after: $after) {
        edges {
          node {
            id
            displayName
          }
          cursor
        }
        pageInfo {
          hasNextPage
          endCursor
        }
      }
    }
  }
}
First page:
{
  "first": 20
}
Next page:
{
  "first": 20,
  "after": "cursor-from-endCursor"
}

Backward Pagination

query GetAssets($last: Int!, $before: String) {
  assetProfiles {
    byId(profile: "your-profile") {
      assets(last: $last, before: $before) {
        edges {
          node {
            id
            displayName
          }
        }
        pageInfo {
          hasPreviousPage
          startCursor
        }
      }
    }
  }
}

Pagination Best Practices

  1. Use reasonable page sizes: 10-100 items per page depending on data size
  2. Store cursors, not offsets: Cursors remain valid even when data changes
  3. Check hasNextPage: Don’t assume there’s always a next page
  4. Handle missing cursors: Some connections may not return cursors for every edge

Filtering

Most list queries support filtering through typed input objects.

Time-Based Filtering

query GetRecentAssets($filter: AssetFilter!) {
  assetProfiles {
    byId(profile: "your-profile") {
      assets(first: 20, filter: $filter) {
        edges {
          node {
            id
            displayName
            modifiedAt
          }
        }
      }
    }
  }
}
Variables:
{
  "filter": {
    "modifiedAfter": "2025-11-01T00:00:00Z"
  }
}

Property-Based Filtering

Some queries allow filtering by specific properties:
query FilteredObservations(
  $startTime: DateTime,
  $endTime: DateTime,
  $properties: [String!]
) {
  assetProfiles {
    byId(profile: "your-profile") {
      upQueryObservations(
        startTime: $startTime,
        endTime: $endTime,
        properties: $properties
      )
    }
  }
}
Variables:
{
  "startTime": "2025-11-01T00:00:00Z",
  "endTime": "2025-11-05T00:00:00Z",
  "properties": ["temperature", "pressure"]
}

Error Handling

The API uses two error patterns: GraphQL-level errors and mutation-level errors.

GraphQL Errors

GraphQL errors appear in the errors array at the top level:
{
  "errors": [
    {
      "message": "Cannot query field 'invalidField' on type 'Asset'",
      "locations": [{ "line": 3, "column": 5 }],
      "path": ["assetProfiles", "byId", "invalidField"]
    }
  ],
  "data": null
}
Common GraphQL error types:
  • Validation errors: Invalid query syntax or schema violations
  • Authentication errors: Missing or invalid credentials
  • Authorization errors: Insufficient permissions
  • Internal errors: Server-side issues

Mutation Errors

Mutations return errors in their result type using the MutationError interface:
interface MutationError {
  message: String!
  type: String!
}
Example mutation with errors:
mutation CreateAssetFilter($input: CreateAssetFilterInput!) {
  createAssetFilter(input: $input) {
    assetFilter {
      id
      displayName
    }
    correlationId
    errors {
      message
      type
    }
  }
}
Response with validation error:
{
  "data": {
    "createAssetFilter": {
      "assetFilter": null,
      "correlationId": "abc-123",
      "errors": [
        {
          "message": "Display name is required",
          "type": "VALIDATION_ERROR"
        }
      ]
    }
  }
}

Error Handling Pattern

const result = await executeQuery(mutation, variables)

// Check for GraphQL-level errors
if (result.errors) {
  console.error('GraphQL errors:', result.errors)
  return
}

// Check for mutation-level errors
const { assetFilter, errors } = result.data.createAssetFilter
if (errors && errors.length > 0) {
  console.error('Mutation errors:', errors)
  return
}

// Success
console.log('Created asset filter:', assetFilter)

GeoJSON Support

The API has comprehensive GeoJSON support for geospatial data.

GeoJSON Types

enum GeoJSONType {
  Point
  MultiPoint
  LineString
  MultiLineString
  Polygon
  MultiPolygon
  GeometryCollection
  Feature
  FeatureCollection
}

Querying Geometry

query GetAssetGeometry($profileId: ID!, $assetId: ID!) {
  assetProfiles {
    byId(profile: $profileId) {
      assetById(id: $assetId) {
        id
        displayName
        geometry {
          type
          coordinates
          bbox
        }
        geoJSONFeature
      }
    }
  }
}
Response:
{
  "data": {
    "assetProfiles": {
      "byId": {
        "assetById": {
          "id": "asset-123",
          "displayName": "Well Site A",
          "geometry": {
            "type": "Point",
            "coordinates": [-114.0719, 51.0447],
            "bbox": null
          },
          "geoJSONFeature": {
            "type": "Feature",
            "geometry": {
              "type": "Point",
              "coordinates": [-114.0719, 51.0447]
            },
            "properties": {
              "name": "Well Site A"
            },
            "id": "asset-123"
          }
        }
      }
    }
  }
}

GeoJSON Input

input GeoJSONGeometryInput {
  type: GeoJSONType!
  coordinates: GeoJSONCoordinates
  bbox: [Float]
}
Example usage in mutations:
{
  "geometry": {
    "type": "Point",
    "coordinates": [-114.0719, 51.0447]
  }
}

Coordinate Systems

  • All coordinates use WGS84 (EPSG:4326)
  • Coordinate order: [longitude, latitude] (GeoJSON standard)
  • Elevations can be included as third coordinate: [lon, lat, elevation]

Audit Trails

Many types include audit information for tracking changes.

Audit Type

type Audit {
  createdTime: DateTime!
  updatedTime: DateTime!
  createdBy: UserReference
  updatedBy: UserReference
}

Querying Audit Information

query GetIssueAudit($issueId: ID!) {
  issues {
    byId(id: $issueId) {
      id
      displayName
      audit {
        createdTime
        updatedTime
        createdBy {
          userId
        }
        updatedBy {
          userId
        }
      }
    }
  }
}
Response:
{
  "data": {
    "issues": {
      "byId": {
        "id": "issue-123",
        "displayName": "Equipment Failure",
        "audit": {
          "createdTime": "2025-11-01T10:00:00Z",
          "updatedTime": "2025-11-03T14:30:00Z",
          "createdBy": {
            "userId": "user-1"
          },
          "updatedBy": {
            "userId": "user-2"
          }
        }
      }
    }
  }
}

Entity References

Apollo Federation enables cross-subgraph entity resolution.

User References

type UserReference @key(fields: "userId") {
  userId: ID!
}
Query user references:
query GetUserDetails {
  issues {
    byId(id: "issue-123") {
      audit {
        createdBy {
          userId
          # Additional user fields resolved by su-user subgraph
        }
      }
    }
  }
}

Asset References

type AssetReference @key(fields: "profile assetId") {
  profile: ID
  assetId: ID!
  asset: Asset
}

DateTime Handling

All timestamps use ISO-8601 format in UTC.

DateTime Format

2025-11-05T14:30:00Z
2025-11-05T14:30:00.123Z  (with milliseconds)

Querying with DateTime

query GetRecentData($startTime: DateTime!, $endTime: DateTime!) {
  assetProfiles {
    byId(profile: "your-profile") {
      upQueryObservations(
        startTime: $startTime,
        endTime: $endTime
      )
    }
  }
}
Variables:
{
  "startTime": "2025-11-01T00:00:00Z",
  "endTime": "2025-11-05T23:59:59Z"
}

Relative Time Queries

Some fields support relative time strings:
query GetLiveData {
  assetProfiles {
    byId(profile: "your-profile") {
      # Historical: "7 days ago" to "now"
      upQuery(startTime: "7 days ago", endTime: "now")

      # Live data: "now"
      upQuery(startTime: "now")
    }
  }
}

JSONObject Type

The JSONObject scalar represents arbitrary JSON data.

Querying JSON Properties

query GetAssetProperties($profileId: ID!, $assetId: ID!) {
  assetProfiles {
    byId(profile: $profileId) {
      assetById(id: $assetId) {
        id
        properties
        observation
      }
    }
  }
}
Response:
{
  "data": {
    "assetProfiles": {
      "byId": {
        "assetById": {
          "id": "asset-123",
          "properties": {
            "name": "Well Site A",
            "operator": "Company XYZ",
            "capacity": 1000
          },
          "observation": {
            "phenomenonTime": "2025-11-05T14:00:00Z",
            "result": {
              "pressure": 150.5,
              "temperature": 22.3
            }
          }
        }
      }
    }
  }
}

Filtering JSON Properties

query GetSelectedProperties(
  $profileId: ID!,
  $assetId: ID!,
  $properties: [String!]
) {
  assetProfiles {
    byId(profile: $profileId) {
      assetById(id: $assetId) {
        id
        properties(only: $properties)
      }
    }
  }
}
Variables:
{
  "profileId": "wells",
  "assetId": "well-123",
  "properties": ["name", "operator"]
}

Correlation IDs

All mutations return a correlationId for tracking and debugging.
mutation CreateAssetFilter($input: CreateAssetFilterInput!, $correlationId: String) {
  createAssetFilter(input: $input, correlationId: $correlationId) {
    assetFilter {
      id
    }
    correlationId
    errors {
      message
    }
  }
}
Providing your own correlation ID:
{
  "input": { ... },
  "correlationId": "my-unique-id-123"
}
Use correlation IDs for:
  • Request tracking across systems
  • Debugging with support teams
  • Audit logging
  • Idempotency (some mutations)

Best Practices

  1. Request only needed fields: GraphQL allows precise field selection - use it to reduce payload size
  2. Use fragments for reusability: Define reusable fragments for common field sets
  3. Batch related queries: Use GraphQL’s ability to query multiple resources in one request
  4. Handle errors at both levels: Check for GraphQL errors AND mutation errors
  5. Store cursors for pagination: Cursors remain stable across data changes
  6. Use correlation IDs: Include them for better debugging and tracking
  7. Respect rate limits: Implement exponential backoff for retries
  8. Cache appropriately: Consider caching strategies based on data volatility

Next Steps