Skip to content

Instantly share code, notes, and snippets.

@binaryseed
Last active February 10, 2020 17:53
Show Gist options
  • Save binaryseed/f2dd63d1a1406124be70c17e2e796891 to your computer and use it in GitHub Desktop.
Save binaryseed/f2dd63d1a1406124be70c17e2e796891 to your computer and use it in GitHub Desktop.
Input Union Use Cases

inputunion Use Cases

Many of the use cases for the inputunion are direct mirrors of the ability to leverage the interface / union in a Query. We have a number of APIs that consist of a configuration side and a query side.

Note that these examples are trimmed down, the data structures are larger than what is displayed here for convienience.

Cloud Integrations

We have the ability to link your New Relic account to a set of Cloud Providers to automatically gather metrics from them. There is an API to setup those configurations as well as to retrieve them.

Each different Integration has a different set of config parameters. There are a huge number of these, dozens for each AWS, Azure, Google, etc.

Ideally we'd have a nice flat list of intgrations with a symetrical return structure:

With Input Union:

mutation {
  cloudConfigureIntegration (
    accountId: 123,
    integrations: [
      {
        __inputname: AwsSqsIntegrationConfig
        linkedAccountId: 456
        metricsPollingInterval: 20
        awsRegions: ["r1", "r2"]
      },
      {
        __inputname: AzureFunctionsIntegrationConfig
        linkedAccountId: 456
        metricsPollingInterval: 20
        resourceGroups: ["r1", "r2"]
      }
    ]
  ) {
    configuredIntegrations {
      __typename
      name
      linkedAccountId
      ... on AwsSqsIntegration {
         awsRegions
      }
      ... on AzureFunctionsIntegration {
         resourceGroups
      }
    }
  }
}

Without inputunion, we have to introduce a kludge of "container" input objects that let us submit lists of things that are all of the same type. We litter our schema with possibly hundreds of objects who's only purpose is to provide a "path" to a list of a single type. The other alternative was to have a different mutation for every single type, but that would also lead to many hundreds of mutations just for this operation.. There are other operations like update and delete

Without Input Union:

mutation {
  cloudConfigureIntegration(
    accountId: 123,
    integrations: {
      aws: {
        sqs: [
          {
            linkedAccountId: 456
            metricsPollingInterval: 20
            awsRegions: ["r1", "r2"]
          }
        ]
      }
      azure: {
        functions: [
          {
            linkedAccountId: 789
            resourceGroups: ["g1", "g2"]
          }
        ]
      }
    }
  ) {
    integrations {
      __typename
      name
      ... on AwsSqsIntegration {
         awsRegions
      }
      ... on AzureFunctionsIntegration {
         resourceGroups
      }
    }
  }
}

We leverage the interface on the queries for these structures, which is nice. However it means that the data structures for mutations and queries are quite different, which is a confusing user experience.

Dashboards

One of our main features is dashboards, and inside a dashboard there are many "widget" types..

Ideally, the mutation to create a dashboard just contained a list of widgets, each of their own type. Widgets share some structure but have important differences based on visualization, data source, etc.

With Input Union:

mutation {
  insightsCreateDashboard(
    accountId: 123
    dashboard: {
      description: "My Cool Dashboard"
      widgets: [
        {
          __inputname: FacetChartWidget
          layout: {column: 1, row: 1}
          nrqlQuery: "SELECT count(*) FROM Transaction FACET host"
        },
        {
          __inputname: MetricChartWidget
          layout: {column: 1, row: 2}
          metrics: ["Errors/all"]
        }
      ]
    }
  ) {
    dashboard {
      description
      widgets {
        __typename
        layout
        .. on FacetChartWidget {
          nrqlQuery
        }
        .. on MetricChartWidget {
          metrics
        }
      }
    }
  }
}

Without inputunion, we have to use the same hack of the container object, which leads to a fairly strange API where you have to group widgets according to their type.

The option of different mutations per type isn't feasable here, there's nothing in existance that you could addFacetWidgetToDashboard to when you are just creating a new dashboard.

Without Input Union:

mutation {
  insightsCreateDashboard(
    accountId: 123
    dashboard: {
      description: "My Cool Dashboard"
      widgets: {
        facetChart: [
          {
            layout: {column: 1, row: 1}
            nrqlQuery: "SELECT count(*) FROM Transaction FACET host"
          }
        ],
        metricChart: [
          {
            layout: {column: 1, row: 2}
            metrics: ["Errors/all"]
          }
        ]
      }
    }
  ) {
    dashboard {
      description
      widgets {
        facetChart {
          layout {column row}
          nrqlQuery
        }
        metricChart {
          layout {column row}
          metrics
        }
      }
    }
  }
}

And here we wanted to enable a user to easily query a full dashboard, transform some of it, and then update it or create a new dashboard based on it. So we had to mirror the container object structure in the query types, which means we couldn't even leverage the interface :(

Recording metrics

Another similar case is when we want to record some metrics of a few different types. There are counters, summaries, gauges, etc. These structures are nested deeply into a common data structure, and would be very painful to use if we had a different mutation for each of type.

mutation {
  recordMetric(
    startTime: 1531414060739
    endTime: 1531414060739
    dataPoints: [
      {
        group: {
          __inputname: CuratedGroupType
          type: TRANSACTION_SUMMARY
        }
        dimensions: [
          {
            key: "request.method"
            value: "GET"
          },
          {
            key: "httpResponseCode"
            value: "200"
          }
        ]
        metrics: [
          {
            __inputname: CounterMetric
            name: "errorCount"
            ticks: 1
            total: 1
          },
          {
            __inputname: SummaryMetric
            name: "duration"
            count: 5
            total: 0.004382655
            min: 0.0005093
            max: 0.001708826
          }
        ]
      }
    ]
  ) {
    responseStatus
  }
}

This one is interesting because there actually could be multiple uses of an inputunion here. The group field might be a set of known ENUM values or a custom value. I don't have a solution here - splitting this into fully typed mutations as GraphQL is now would lead to a giant explosion in the quantity of mutations.

enum GroupType {
  TRANSACTION_SUMMARY
  HOST_SUMMARY
}
type CuratedGroupType {
  type: GroupType
}
type CustomGroupType {
  type: String
}
inputunion MetricGroup = CuratedGroupType | CustomGroupType
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment