Metadatos e introspección en GraphQL

Escrito por el .
java planeta-codigo
Comentarios

Una API REST no ofrece introspección y por tanto hay que recurrir a un sistema de documentación que puede estar desactualizado y hay que mantener para conocer como usar la API y cuales son sus tipos y parámetros. Por el contrario GraphQL incorpora un sistema de introspección que permite conocer sus tipos y campos, a través del editor GrapiQL o si fuese necesario de forma automtizada con código.

GraphQL

Una de las cosas que me gustan de GraphQL sobre REST es que la API de un servicio se define en un esquema. Tanto las operaciones de consulta, de modificación con sus nombres de parámetros, tipos y si son requeridos o no. Esta información es básica para hacer un buen uso de esa API y conocer cual es su contrato. Además con la herramienta GraphiQL se pueden crear y realizar consultas con un pequeño IDE con asistencia de código. GraphQL genera los metadatos e ofrece la instrospección a partir únicamente de la definición del esquema del servicio sin ningún esfuerzo adicional por parte del creador del servicio.

En el ejemplo de esta serie de artículos sobre GraphQL he usado el siguiente esquema que utiliza como modelo de datos el de una librería.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
scalar LocalDate

type Book {
    id: Long
    title: String
    author: Author
    isbn: String
    date: LocalDate
    comments(after: String, limit: Long): CommentsConnection

    batchedIsbn: String
    batchedComments(after: String, limit: Long): CommentsConnection
}

type Magazine {
    id: Long
    name: String
    pages: Long
}

type Comment {
    id: Long
    text: String
}

type Author {
    id: Long
    name: String
}

input BookFilter {
    title: String
}

type CommentsConnection {
    edges: [CommentEdge]
    pageInfo: PageInfo
}

type CommentEdge {
    node: Comment
    cursor: String
}

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

union Publication = Book | Magazine

type Query {
    books(filter: BookFilter): [Book]!
    publications: [Publication]!
    book(id: Long): Book!
    authors: [Author]!
    author(id: Long): Author!
}

type Mutation {
    addBook(title: String, author: Long): Book
}

schema {
    query: Query
    mutation: Mutation
}

Si no se conoce el esquema qué operaciones, tipos y nombres ofrece la API GraphQL permite introspección y con únicamente el endpoint se puede averiguar esta información.

Por ejemplo, con la siguiente consulta se puede conocer qué tipos contiene el esquema de una API. Los que comienzan con dos barras bajas o __ son tipos parte del sistema de introspección. Entre los que están el que representa un libro y autor pero también está Query que es un punto de acceso a la API.

1
2
3
4
5
6
7
{
 __schema {
   types {
     name
   }
 }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
{
  "data": {
    "__schema": {
      "types": [
        {
          "name": "Author"
        },
        {
          "name": "Book"
        },
        {
          "name": "BookFilter"
        },
        {
          "name": "Boolean"
        },
        {
          "name": "Comment"
        },
        {
          "name": "CommentEdge"
        },
        {
          "name": "CommentsConnection"
        },
        {
          "name": "LocalDate"
        },
        {
          "name": "Long"
        },
        {
          "name": "Magazine"
        },
        {
          "name": "Mutation"
        },
        {
          "name": "PageInfo"
        },
        {
          "name": "Publication"
        },
        {
          "name": "Query"
        },
        {
          "name": "String"
        },
        {
          "name": "__Directive"
        },
        {
          "name": "__DirectiveLocation"
        },
        {
          "name": "__EnumValue"
        },
        {
          "name": "__Field"
        },
        {
          "name": "__InputValue"
        },
        {
          "name": "__Schema"
        },
        {
          "name": "__Type"
        },
        {
          "name": "__TypeKind"
        }
      ]
    }
  }
}

Conocer el tipo de las consultas de lectura y que consultas se pueden realizar inspeccionando el tipo Query.

1
2
3
4
5
6
7
{
 __schema {
   queryType {
     name
   }
 }
}
1
2
3
4
5
6
7
8
{
 __type(name: "Query") {
   name
   fields {
     name
   }
 }
}
1
2
3
4
5
6
7
8
9
{
  "data": {
    "__schema": {
      "queryType": {
        "name": "Query"
      }
    }
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
  "data": {
    "__type": {
      "name": "Query",
      "fields": [
        {
          "name": "author"
        },
        {
          "name": "authors"
        },
        {
          "name": "book"
        },
        {
          "name": "books"
        },
        {
          "name": "publications"
        }
      ]
    }
  }
}

Por la propiedad de GraphQL de que se pueden realizar varias consultas en una única petición se pueden obtener ambos resultados a la vez.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
 __schema {
   queryType {
     name
   }
 }
 __type(name: "Query") {
   name
   fields {
     name
   }
 }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
  "data": {
    "__schema": {
      "queryType": {
        "name": "Query"
      }
    },
    "__type": {
      "name": "Query",
      "fields": [
        {
          "name": "author"
        },
        {
          "name": "authors"
        },
        {
          "name": "book"
        },
        {
          "name": "books"
        },
        {
          "name": "publications"
        }
      ]
    }
  }
}

Se puede obtener más en detalle los campos que contiene un tipo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
 book: __type(name: "Book") {
   name
   fields {
     name
     type {
       name
       kind
     }
   }
 }
 author: __type(name: "Author") {
   name
   fields {
     name
     type {
       name
       kind
     }
   }
 }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
{
  "data": {
    "book": {
      "name": "Book",
      "fields": [
        {
          "name": "author",
          "type": {
            "name": "Author",
            "kind": "OBJECT"
          }
        },
        {
          "name": "batchedComments",
          "type": {
            "name": "CommentsConnection",
            "kind": "OBJECT"
          }
        },
        {
          "name": "batchedIsbn",
          "type": {
            "name": "String",
            "kind": "SCALAR"
          }
        },
        {
          "name": "comments",
          "type": {
            "name": "CommentsConnection",
            "kind": "OBJECT"
          }
        },
        {
          "name": "date",
          "type": {
            "name": "LocalDate",
            "kind": "SCALAR"
          }
        },
        {
          "name": "id",
          "type": {
            "name": "Long",
            "kind": "SCALAR"
          }
        },
        {
          "name": "isbn",
          "type": {
            "name": "String",
            "kind": "SCALAR"
          }
        },
        {
          "name": "title",
          "type": {
            "name": "String",
            "kind": "SCALAR"
          }
        }
      ]
    },
    "author": {
      "name": "Author",
      "fields": [
        {
          "name": "id",
          "type": {
            "name": "Long",
            "kind": "SCALAR"
          }
        },
        {
          "name": "name",
          "type": {
            "name": "String",
            "kind": "SCALAR"
          }
        }
      ]
    }
  }
}

Incluso se puede inspeccionar los tipos del sistema de instrospección. Con las descripciones de los campos o parámetros de entrada si los tuviese.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
 __type(name: "__Type") {
   name
   fields {
     name
     type {
       name
       kind
     }
   }
 }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
{
  "data": {
    "__type": {
      "name": "__Type",
      "fields": [
        {
          "name": "description",
          "type": {
            "name": "String",
            "kind": "SCALAR"
          }
        },
        {
          "name": "enumValues",
          "type": {
            "name": null,
            "kind": "LIST"
          }
        },
        {
          "name": "fields",
          "type": {
            "name": null,
            "kind": "LIST"
          }
        },
        {
          "name": "inputFields",
          "type": {
            "name": null,
            "kind": "LIST"
          }
        },
        {
          "name": "interfaces",
          "type": {
            "name": null,
            "kind": "LIST"
          }
        },
        {
          "name": "kind",
          "type": {
            "name": null,
            "kind": "NON_NULL"
          }
        },
        {
          "name": "name",
          "type": {
            "name": "String",
            "kind": "SCALAR"
          }
        },
        {
          "name": "ofType",
          "type": {
            "name": "__Type",
            "kind": "OBJECT"
          }
        },
        {
          "name": "possibleTypes",
          "type": {
            "name": null,
            "kind": "LIST"
          }
        }
      ]
    }
  }
}

Conocer cuales son los campos de un tipo puede utilizarse para validar una API, comprobando que no se han eliminado campos necesarios. Es útil en el caso de querer automatizar esta validación de una API de GraphQL que se consuma ayudando a detectar de forma temprana problemas de compatibilidad al publicarse una nueva versión que no está bajo propiedad del que la usa.

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando el comando ./gradlew run.