Skip to content

Instantly share code, notes, and snippets.

@lalunamel
Created May 26, 2024 13:11
Show Gist options
  • Save lalunamel/5c7a107c65e234690d6aeabc5303ed4d to your computer and use it in GitHub Desktop.
Save lalunamel/5c7a107c65e234690d6aeabc5303ed4d to your computer and use it in GitHub Desktop.
Notes on my reading of the GraphQL spec

https://spec.graphql.org/October2021/

https://youtu.be/O9ikgN4Kk-Y

GraphQL Spec Notes

GraphQL - query language, used to build client applications, describe data requirements. Defines capabilites - does not do computation.

Vocab

  • query language - a language that describes a query?
  • graphql service - something that can respond to graphql requests
  • documents - may contain operations, describes a complete file or request string operated on by a GraphQL service or client. Can contain multiple definitions, either executable OR representative of a GraphQL type system. If a document contains one operation, that operation may be unnamed. If that single operation is a query without variables or directives, you can omit the query keyword and the operation name.
  • operation - query, mutation, or subscription
  • fragments - a unit of composition allowing for reuse
  • syntatic grammar - higher level ruleset for how a language works
  • lexical grammar - lower level ruleset for how a language works
  • SourceCharacter - tab, end of line, carriage return, space
  • Token lexical grammar - interesting everyday text - letters, numbers, punctionation, etc
  • Selection set - The set of information that an operation opts to receive. Only this set of information is returned as the result of an operation.
  • Resolve/Resolver - A function that takes a parent object, a context, and a field name and returns the value for the field on that parent object.
  • Request error - An error produced by the GraphQL system before the request is attempted to be executed
  • Field error - An error produced by the GraphQL system during the resolution or coercion of a particular field

Overview

Design Principles

  • starts from a frontend perspective
  • hierarchical - in the same way that react is
  • strongly typed
  • client focused - only return what the client wants
  • you can inspect the type system represented/exposed by graphql

Language

Comments

Comments are signified with #

Commas

Commas are ignored? Used as list delimiters.

Document

Operations

  • query – a read-only fetch.
  • mutation – a write followed by a fetch. Performs side effects on the underlying data system.
  • subscription – a long-lived request that fetches data in response to source events.

You can do a shorthand notation if you are representing a single query in a document and that query does not take variables.

Names and Selection sets

Operations are made up of a name and a selection set.

mutation {
  likeStory(storyID: 12345) {
    story {
      likeCount
    }
  }
}

name is likeStory and the selection set is all the stuff in the curly braces below the name.

A selection set can contain a field, a fragment spread (e.g. ...FragmentName), or an inline fragment.

Fields

A field describes one discrete piece of information available to request within a selection set. Can contain a selection set, allowing you to nest requests recursively.

When specifying an operation, you must specify selection sets down to scalar values - you can't just select a field that represents e.g. a graphql object that contains other fields.

Fields can take arguments specified like: field(argName: value, argName2: value). The order doesn't matter.

Fields can be aliased so that they have different names in the resulting data that is returned:

{
  user(id: 4) {
    id
    name
    smallPic: profilePic(size: 64)
    bigPic: profilePic(size: 1024)
  }
}

Fragments

Fragments define units of reuse within graphql.

You define them like so:

fragment UserInfo on User {
  id
  name
}

You use them with the spread (...) operator like so:

query GetUsers {
  users(first: 5) {
    photo
    ...UserInfo
  }
}

Fragments must specify the type they apply to. If you request data that could be many types, the fragment will only operate on the type it is specified to. You specifiy a fragment type with the on keyword.

You can specify a fragment inline. It's basically the same thing as using the spread operator, except you don't write the fragment name.

Input values

  • variable
  • int
  • float
  • boolean
  • string
  • null
  • enum
  • list
  • object

Variables

You can declare variables on a operation. If a fragment uses a variable, the root note that transitively includes the fragment must make sure to declare the variable.

Directives

Directives are special tags to tell the server to do something interesting. You specify them using @.

Seems dangerous. If you're using them you better know what you're doing.

Type System

  • describes the capabilities of a GraphQL service
  • determines if a request operation is valid
  • guarantees the type of response results
  • describes input types

Descriptions

Descriptions represent documentation. Documentation is a first-class concept in GraphQL.

Schema

Describes the capabilities of a GraphQL service. Defined in terms of types it supports as well as root operation types: query, mutation, subscription. All types in the schema must have unique names.

Root Operation Types

  • query - required - defines what fields are available at the top level of a GraphQL query operation.
  • mutation - optional
  • subscription - optional

All defined as "Object" types.

Types

  • scalar - represent leaf nodes in the GraphQL hierarchy. Usually are things like Int, Float, String, Boolean, ID, though you can define your own.
  • object - represent intermediate nodes as a list of named fields and their associated values
  • interface - just like an interface in any other normal type system. Allows you to give a name to a bundle of fields.
  • union - give a name to a list of types joined by OR
  • enum - represent leaf nodes in the GraphQL hierarchy that describe a set of possible values.
  • input object - types the define inputs. Sorta look like Objects. Can only contain scalars, enums, or input objects.

Wrapping Types

  • list - represents many of a particular type
  • non-null - denotes that the type it wraps will never be null

Input and Output

  • scalar, enum, input object can only be used as inputs
  • object, interface, union can only be used as outputs
  • lists, non-null can be used as both

Custom Scalars

Can define custom scalars like URL or UUID or anything else.

Objects

Represent a list of named fields, each of which yield a value of a specific type. Essentially an ordered map.

If you are querying an object, you must select at least one field within that object.

What defines a valid object

Object fields may take arguments to specify their output because they are conceptually just functions.

You can mark fields as depreceated.

Non-Null

All types in GraphQL are nullable by default. A trailing exclamation mark is used to denote non-null types like so: name: String!

Directives

Annotations which tell e.g. a validator, executor, or client tool to evaluate something differently.

  • @skip - takes a "if" boolean - skip the line if the arg is true
  • @inlucde - the inverse of @skip it seems. Both are essentially "if" statements
  • @deprecated - indicates deprecated sections
  • @specifiedBy - provides a place to put a URL that defines a scalar specification

Can define custom directives. Seems like a very advanced use-case.

Introspection

Introspection allows queries to find out about the schema of a GraphQL service. So, for example, you can ask "What's the type of this object and what fields does it have?"

Reserved names

Names provided by GraphQL start with __ two underscores. There must not be any other names that start with __ two underscores.

__typename

You can query any object's typename by asking for the __typename field. It exists on any object except the root field of a subscription operation.

Documentation

All types have the ability to return a description field that is a string. The string can contain markdown following the CommonMark specification.

Deprecation

Fields can be deprecated with fields isDeprecated and deprecationReason.

Types & TypeKind

  • scalar - int, string, boolean, custom scalars
  • object - sets of fields
  • interface - declares a common set of fields. Any implementing type must define all those fields
  • union - a thing representing one of several possible types
  • enum - defines a set of values
  • input_object - defines a set of named input values
  • list - sequence of values
  • non_null - a type modifier indicating a type can not be null

Validation

A GraphQL service should validate a GraphQL request before executing it. It should not execute the request if it is invalid.

  • Documents
    • A GraphQL service does not/can not take into account types that are defined client-side.
  • Operations
    • Must have a unique name in the context of the document
    • Can be defined without a name as long as there's only one operation in the document
    • Subscription operations must have only one root field
    • Subscription operations must not define their root field as __typename
  • Fields
    • Object, interface, and union types must select one or more fields
    • Fields must exist on the type they're being used in
    • Fields can not be selected on a union - they must be selected indirectly via a fragment on that union
    • Selecting the same field with the same argument(s) more than once is valid and will be merged by MergeSelectionSets and CollectFields
    • You aren't able to select fields on scalars or enums
  • Arguments
    • Must only reference defined arguments, must not have multiple arguments with the same name
  • Fragments
    • Each fragment's name must be unique within a document
    • Fragments can only be declared on unions, interfaces, and objects. They can not be declared on leaf nodes: scalars or enums.
    • If a fragment is defined within a document, it must be used
    • Fragment spreads must not form cycles
    • Fragments are declared on a particular type and will only ever be valid for use on that type
  • Values
    • Literal values must be of the correct type
    • Value names are unique
  • Directives
    • Directives can only be used where they are supported
  • Variables
    • Must have unique names
    • Variables must be of type Input
    • Only those variables that are defined can be used. Variables that are used must be defined. There can be no extra variable definitions.

Execution

GraphQL generates a response by executing stuff.

A GraphQL request contains:

  • the schema to use (usually provided by the GraphQL service)
  • a document that contains an operation
  • (optional) the name of the operation in the document to execute
  • (optional) values for variables in the operation
  • ?an initial value corresponding to the root type being executed?

Executing Requests

  1. Get the operation
  2. Get the variables used in the operation
  3. Validate the request
  4. Coerce input variables
  5. Execute the operation

Executing Operations

Each operation to execute begins with a root object: query, mutation, or subscription.

Query fields may be executed in parallel. Mutation fields mut be executed serially.

Subscriptions

Subscription operations return an event stream (aka Response Stream). Each event is the result of executing the oparation for each new event in an underlying "Source Stream"

Pub-sub model. Transport mechanisms, serialization, etc are all implementation details of the system and not described in the spec.

Publish -> Source Stream Subscribe -> Event Stream / Response Stream

Executing Selection Sets

To execute a selection set:

  1. Collect all the fields for a selection set on an object into a map from field names to (sub)selection sets
  2. For every field in that map, resolve/execute the field and produce a value 2a. Recursively execute the selection set for the field
  3. Ensure the value produced is of the correc type for the field
Nullability

If a field marked as non-null produces an error, then the error will propagate up to the parent object recursively until there is an object that can be null.

Executing In Parallel

Mutations must be run in serial. Everything else can be run in parallel.

Executing Fields

  1. Create an entry in the response map where the key is the field name (or alias)
  2. Coerce argument values
  3. Resolve field value
  4. recursively execute innner selection set OR coerce a scalar value and add it to the response

If two selection sets are defined for a field of the same name, the field will be resolved, then the selection sets will be merged, and finally the merged selection set will be executed.

Response

Describes:

  • a successful operation
  • any errors encountered
  • might contain a partial response along with errors encountered

Is a map:

{
  errors: [{}, ...], (only present if there are errors)
  data: {}, (only present if the request executed successfully, might be null [which is a valid value])
  extensions: {
    (optional, intended for developers to extend protocol)
  }
}

Data

Contains the successful response. The output will be an object of type query root if the operation was a query. The output will be an object of type mutation root if the operation was a mutation.

Errors

Request error - error parsing the request before execution. Probably the fault of the requesting client. Think malformed request. Field error - error resolving a particular field. Probably the result of the service. Think record not found execption.

Error format

It's a map.

{
  message: "String description of th error intended for the developer",
  locations: [{ line: , column:  }, ...],
  path: ["field", "subfield", ...], (optional, describes the path of the response field that caused the error)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment