# Field/Directive-based Versioning

Fields and directives can be independently versioned, and the version to use can be specified in the query through the field/directive argument versionConstraint, and in the API endpoint through params fieldVersionConstraints[], directiveVersionConstraints[] and versionConstraint.

The engine follows this order to obtain the versioning for a field:

  1. Field argument in the query
  2. Endpoint param fieldVersionConstraints[], on the namespaced type and field
  3. Endpoint param fieldVersionConstraints[], on the non-namespaced type and field
  4. Endpoint param versionConstraint
  5. Default versioning set in the requested schema

For directives it is similar, but in only 4 steps since they are not namespaced:

  1. Directive argument in the query
  2. Endpoint param directiveVersionConstraints[], on the directive
  3. Endpoint param versionConstraint
  4. Default versioning set in the requested schema

If no versioning is found on any of these steps, then the field or directive is not versioned.

To select the version for the field/directive, GraphQL by PoP uses the same semver version constraints employed by Composer.

# Why

The evolution strategy adopted by GraphQL has a problem: when deprecating a field (as to replace it with a newer implementation), the new field will need to have a new field name. Then, if the deprecated field cannot be removed (for instance, because some clients are still accessing it, from queries that were never revised), then all these fields for a same functionality tend to accumulate in the schema, and the new implementation of the field will not have an optimal name (indeed, it will be worse than the deprecated field's name).

Evolution alone, over time, tends to pollute the schema with undesired definitions. Versioning the schema on a field/directive basis can solve this problem.

# Targeted versioning through the query

Pass the constraint to the field or directive, through argument versionConstraint:

# Selecting version for fields
query {
  #This will produce version 0.1.0
  firstVersion: userServiceURLs(versionConstraint: "^0.1")
  # This will produce version 0.2.0
  secondVersion: userServiceURLs(versionConstraint: ">0.1")
  # This will produce version 0.2.0
  thirdVersion: userServiceURLs(versionConstraint: "^0.2")
}

# Selecting version for directives
query {
  post(id:1) {
    titleCase: title @makeTitle(versionConstraint: "^0.1")
    upperCase: title @makeTitle(versionConstraint: "^0.2")
  }
}

[View results: query #1, query #2]

# Targeted versioning through the endpoint

Provide the version constraints via the endpoint, through URL params, setting the combination of versioning for the elements in the schema. This allows to:

  • Run instrospection queries against a granularly-defined version of the schema
  • Have a client coded in TypeScript be generated by pointing to the exact required schema
  • Point to a different version of the schema just by modifying the endpoint (eg: useful to wait for feature flags to kick in)
  • Visualize a specific schema in GraphQL Voyager

The URL params to pass to the endpoint are:

  • fieldVersionConstraints[]
  • directiveVersionConstraints[]

These params are arrays, so they can be defined multiple times in the URL as to version more than 1 field or directive. Directives are referenced directly by their name (without the @), like this:

?directiveVersionConstraints[makeTitle]=^0.1&directiveVersionConstraints[upperCase]=~0.2

Fields are referenced as a combination of their type and their name, separated with a dot, like Post.title or User.name:

?fieldVersionConstraints[Post.title]=0.3.1&fieldVersionConstraints[User.name]=0.2|0.3

The type name can be namespaced or not, it will work in either case, with automatic namespacing enabled or not.

Let's see some examples. In the links to execute the queries below, the URL parameters fieldVersionConstraints[] and directiveVersionConstraints[] were added to the GraphiQL URL, which then passes them to endpoint /api/graphql/ (you can check it out using the Firefox/Chrome DevTools' Network tab).

In this query, field userServiceURLs is queried with version 0.1.0 set by field argument, and with version 0.2.0 by default, as set through URL param fieldVersionConstraints[Root.userServiceURLs]=^0.2:

query {
  versionByFieldArg: userServiceURLs(versionConstraint:"0.1.0")
  versionByURLParam: userServiceURLs
}

...producing response:

{
  "data": {
    "versionByFieldArg": {
      "github": "https://api.github.com/repos/leoloso/PoP"
    },
    "versionByURLParam": {
      "github": "https://api.github.com/repos/getpop/component-model"
    }
  }
}

In this query, directive makeTitle is queried with version 0.2.0 set by directive argument, and with version 0.1.0 by default, as set through URL param directiveVersionConstraints[makeTitle]=^0.1:

query {
  posts(limit:3) {
    versionByURLParam: title @makeTitle
    versionByFieldArg: title @makeTitle(versionConstraint:"^0.2")
  }
}

...producing response:

{
  "data": {
    "posts": [
      {
        "versionByURLParam": "Scheduled By Leo",
        "versionByFieldArg": "SCHEDULED BY LEO"
      },
      {
        "versionByURLParam": "COPE With WordPress: Post Demo Containing Plenty Of Blocks",
        "versionByFieldArg": "COPE WITH WORDPRESS: POST DEMO CONTAINING PLENTY OF BLOCKS"
      },
      {
        "versionByURLParam": "A Lovely Tango, Not With Leo",
        "versionByFieldArg": "A LOVELY TANGO, NOT WITH LEO"
      }
    ]
  }
}

Several fields and directives can be independently version, as in this query, which sets a different version for fields userServiceURLs and userServiceData, and directive makeTitle.

# General versioning through the endpoint

Adding the versionConstraint parameter in the GraphQL endpoint itself will define that version constraint in all fields. If any field does not support versioning, or doesn't satisfy that version constraint, will then ignore it.

For instance, this GraphiQL client has been versioned with constraint ^0.2.

Any field can still override this default value with its own versionConstraint. Running this query:

query {
  #This will produce version 0.2.0
  implicitVersion: userServiceURLs
  #This will produce version 0.1.0
  explicitVersion: userServiceURLs(versionConstraint: "^0.1")
}

Will produce different results, from the 2 different versions of the same field:

{
  "data": {
    "implicitVersion": {
      "github": "https://api.github.com/repos/getpop/component-model"
    },
    "explicitVersion": {
      "github": "https://api.github.com/repos/leoloso/PoP"
    }
  }
}

# Visualizing the schema for a specific version

TIP

In order to know which version of the field and directive is the one being used, GraphQL by PoP outputs the field's version as part of the field's description:

Field description with version in GraphiQL

This is a temporary solution until GraphQL allows to query the extensions field through introspection (over which the versioning can be made available).

GraphQL Voyager allows to visualize the schema for the different versions. In the default schema:

GraphQL default interactive schema

...field userServiceURLs has the following signature, which corresponds to version 0.1.0:

Field description for version 0.1.0

However, when adding ?versionConstraint=^0.2 to the URL (which in turn sets this parameter on the endpoint), we can visualize the schema for that version constraint. Then, field userServiceURLs has this different signature, corresponding to version 0.2.0:

Field description for version 0.2.0

Last Updated: 4/20/2020, 1:58:06 PM