Merge pull request #1763 from amiedes/1754-api-docs
API docs and sample Ruby scripts
This commit is contained in:
432
doc/api/api_en.md
Normal file
432
doc/api/api_en.md
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
# API Documentation
|
||||||
|
|
||||||
|
* [Characteristics](#characteristics)
|
||||||
|
* [GraphQL](#graphql)
|
||||||
|
* [Making API requests](#making-api-requests)
|
||||||
|
* [Supported clients](#supported-clients)
|
||||||
|
* [GraphiQL](#graphiql)
|
||||||
|
* [Postman](#postman)
|
||||||
|
* [HTTP libraries](#http-libraries)
|
||||||
|
* [Available information](#available-information)
|
||||||
|
* [Examples of queries](#examples-of-queries)
|
||||||
|
* [Request a single record from a collection](#request-a-single-record-from-a-collection)
|
||||||
|
* [Request a complete collection](#request-a-complete-collection)
|
||||||
|
* [Pagination](#pagination)
|
||||||
|
* [Accessing several resources in a single request](#accessing-several-resources-in-a-single-request)
|
||||||
|
* [Security limitations](#security-limitations)
|
||||||
|
* [Example of too deep query](#example-of-too-deep-query)
|
||||||
|
* [Example of too complex query](#example-of-too-complex-query)
|
||||||
|
* [Code examples](#code-examples)
|
||||||
|
|
||||||
|
## Characteristics
|
||||||
|
|
||||||
|
* Read-only API
|
||||||
|
* Public access, no authentication needed
|
||||||
|
* Uses GraphQL technology:
|
||||||
|
* Maximum page size (and the default) is 25 records
|
||||||
|
* Maximum query depth is set at 8 levels
|
||||||
|
* A maximum of two collections can be requested within the same query
|
||||||
|
* Support for GET requests (query must be inside the *query string*) and POST requests (query must be within the *body*, encoded as `application/json` or `application/graphql`)
|
||||||
|
|
||||||
|
## GraphQL
|
||||||
|
|
||||||
|
The Consul API uses GraphQL [http://graphql.org](http://graphql.org), the [Ruby implementation](http://graphql-ruby.org/), to be specific. If you're not familiar with this kind of APIs, it's recommended to make some research about GraphQL before.
|
||||||
|
|
||||||
|
One of the characteristics that differentiates a REST API from a GraphQL one is that with the last one it's possible for the client to build its own *custom queries*, so the server will only return information in which we're interested.
|
||||||
|
|
||||||
|
GraphQL queries are written following a standard which ressembles to JSON, for example:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
proposal(id: 1) {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
public_author {
|
||||||
|
id,
|
||||||
|
username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Responses are formatted in JSON:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"proposal": {
|
||||||
|
"id": 1,
|
||||||
|
"title": "Hacer las calles del centro de Madrid peatonales",
|
||||||
|
"public_author": {
|
||||||
|
"id": 2,
|
||||||
|
"username": "electrocronopio"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Making API requests
|
||||||
|
|
||||||
|
Following [the official recommendations](http://graphql.org/learn/serving-over-http/), the Consul API supports the following kind of requests:
|
||||||
|
|
||||||
|
* GET requests, with the query inside the *query string*.
|
||||||
|
* POST requests
|
||||||
|
* With the query inside the *body*, with `Content-Type: application/json`
|
||||||
|
* With the query inside the *body*, with `Content-Type: application/graphql`
|
||||||
|
|
||||||
|
### Supported clients
|
||||||
|
|
||||||
|
Because it's an API that works through HTTP, any tool capable of making this kind of requests is capable of querying the API.
|
||||||
|
|
||||||
|
This section presents a few examples about how to make requests using:
|
||||||
|
|
||||||
|
* GraphiQL
|
||||||
|
* Chrome extensions like Postman
|
||||||
|
* Any HTTP library
|
||||||
|
|
||||||
|
#### GraphiQL
|
||||||
|
|
||||||
|
[GraphiQL](https://github.com/graphql/graphiql) is a browser interface for making queries against a GraphQL API. It's also an additional source of documentation. It's deployed in the route `/graphiql` and it's the best way to get familiar with GraphQL-based APIs.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
It has three main panels:
|
||||||
|
|
||||||
|
* The left panel is used to write the query.
|
||||||
|
* The central panel shows the result of the request.
|
||||||
|
* The right panel (occultable) shows a documentation autogenerated from the models and fields exposed in the API.
|
||||||
|
|
||||||
|
#### Postman
|
||||||
|
|
||||||
|
Example of `GET` request, with the query as part of the *query string*:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Example of `POST` request, with the query as part of the *body* and encoded as `application/json`:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The query must be located inside a valid JSON document, as the value of the `"query"` key:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### HTTP libraries
|
||||||
|
|
||||||
|
Sure you can use any HTTP library available for most programming languages.
|
||||||
|
|
||||||
|
**IMPORTANT**: Due to security protocols from the Madrid City Council servers, it's necessary to include a *User Agent* header from a web browser so the request is not rejected. For example:
|
||||||
|
|
||||||
|
`User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36`
|
||||||
|
|
||||||
|
|
||||||
|
## Available information:
|
||||||
|
|
||||||
|
The [config/api.yml](../../config/api.yml) file contains a complete list of all the models (and their attributes) which are currently being exposed in the API.
|
||||||
|
|
||||||
|
The models are the following:
|
||||||
|
|
||||||
|
| Model | Description |
|
||||||
|
| ----------------------- | ----------------------------- |
|
||||||
|
| `User` | Users |
|
||||||
|
| `Debate` | Debates |
|
||||||
|
| `Proposal` | Proposals |
|
||||||
|
| `Comment` | Comments on debates, proposals and other comments |
|
||||||
|
| `Geozone` | Geozones (districts) |
|
||||||
|
| `ProposalNotification` | Notifications related to proposals |
|
||||||
|
| `ActsAsTaggableOn::Tag` | Tags on debates and proposals |
|
||||||
|
| `Vote` | Information related to votes |
|
||||||
|
|
||||||
|
## Examples of queries
|
||||||
|
|
||||||
|
### Request a single record from a collection
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
proposal(id: 2) {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
comments_count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"proposal": {
|
||||||
|
"id": 2,
|
||||||
|
"title": "Crear una zona cercada para perros en Las Tablas",
|
||||||
|
"comments_count": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Request a complete collection
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
proposals {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"proposals": {
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"title": "ELIMINACION DE ZONA APARCAMIENTO EXCLUSIVO FUNCIONARIOS EN MADRID"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"title": "iluminación de zonas deportivas"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Pagination
|
||||||
|
|
||||||
|
The maximum (and default) number of records that each page contains is set to 25. For navigating through the different pages it's necessary to request also information relative to the `endCursor`:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
proposals(first: 25) {
|
||||||
|
pageInfo {
|
||||||
|
hasNextPage
|
||||||
|
endCursor
|
||||||
|
}
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
The response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"proposals": {
|
||||||
|
"pageInfo": {
|
||||||
|
"hasNextPage": true,
|
||||||
|
"endCursor": "NQ=="
|
||||||
|
},
|
||||||
|
"edges": [
|
||||||
|
# ...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To retrieve the next page, you have to pass as a parameter the cursor received in the previous request, and so on:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
proposals(first: 25, after: "NQ==") {
|
||||||
|
pageInfo {
|
||||||
|
hasNextPage
|
||||||
|
endCursor
|
||||||
|
}
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Accessing several resources in a single request
|
||||||
|
|
||||||
|
This query requests information about several models in a single request: `Proposal`,` User`, `Geozone` and` Comment`:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
proposal(id: 15262) {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
public_author {
|
||||||
|
username
|
||||||
|
},
|
||||||
|
geozone {
|
||||||
|
name
|
||||||
|
},
|
||||||
|
comments(first: 2) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security limitations
|
||||||
|
|
||||||
|
Allowing a client to customize queries is a major risk factor. If too complex queries were allowed, it would be possible to perform a DoS attack against the server.
|
||||||
|
|
||||||
|
There are three main mechanisms to prevent such abuses:
|
||||||
|
|
||||||
|
* Pagination of results
|
||||||
|
* Limit the maximum depth of the queries
|
||||||
|
* Limit the amount of information that is possible to request in a query
|
||||||
|
|
||||||
|
### Example of too deep query
|
||||||
|
|
||||||
|
The maximum depth of queries is currently set at 8. Deeper queries (such as the following) will be rejected:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
user(id: 1) {
|
||||||
|
public_proposals {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
comments {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
body,
|
||||||
|
public_author {
|
||||||
|
username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The response will look something like this:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"message": "Query has depth of 9, which exceeds max depth of 8"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example of too complex query
|
||||||
|
|
||||||
|
The main risk factor is when multiple collections of resources are requested in the same query. The maximum number of collections that can appear in the same query is limited to 2. The following query requests information from the `users`,` debates` and `proposals` collections, so it will be rejected:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
users {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
public_debates {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
public_proposals {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The response will look something like this:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"message": "Query has complexity of 3008, which exceeds max complexity of 2500"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "Query has complexity of 3008, which exceeds max complexity of 2500"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "Query has complexity of 3008, which exceeds max complexity of 2500"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
However, it is possible to request information belonging to more than two models in a single query, as long as you do not try to access the entire collection. For example, the following query that accesses the `User`,` Proposal` and `Geozone` models is valid:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
user(id: 468501) {
|
||||||
|
id
|
||||||
|
public_proposals {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
title
|
||||||
|
geozone {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"user": {
|
||||||
|
"id": 468501,
|
||||||
|
"public_proposals": {
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"title": "Empadronamiento necesario para la admisión en GoFit Vallehermoso",
|
||||||
|
"geozone": {
|
||||||
|
"name": "Chamberí"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code examples
|
||||||
|
|
||||||
|
The [doc/api/examples](examples) directory contains examples of code to access the API.
|
||||||
432
doc/api/api_es.md
Normal file
432
doc/api/api_es.md
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
# Documentación de la API
|
||||||
|
|
||||||
|
* [Características](#características)
|
||||||
|
* [GraphQL](#graphql)
|
||||||
|
* [Haciendo peticiones a la API](#haciendo-peticiones-a-la-api)
|
||||||
|
* [Clientes soportados](#clientes-soportados)
|
||||||
|
* [GraphiQL](#graphiql)
|
||||||
|
* [Postman](#postman)
|
||||||
|
* [Librerías HTTP](#librerías-http)
|
||||||
|
* [Información disponible](#información-disponible)
|
||||||
|
* [Ejemplos de consultas](#ejemplos-de-consultas)
|
||||||
|
* [Recuperar un único elemento de una colección](#recuperar-un-único-elemento-de-una-colección)
|
||||||
|
* [Recuperar una colección completa](#recuperar-una-colección-completa)
|
||||||
|
* [Paginación](#paginación)
|
||||||
|
* [Acceder a varios recursos en una única petición](#acceder-a-varios-recursos-en-una-única-petición)
|
||||||
|
* [Limitaciones de seguridad](#limitaciones-de-seguridad)
|
||||||
|
* [Ejemplo de consulta demasiado profunda](#ejemplo-de-consulta-demasiado-profunda)
|
||||||
|
* [Ejemplo de consulta demasiado compleja](#ejemplo-de-consulta-demasiado-compleja)
|
||||||
|
* [Ejemplos de código](#ejemplos-de-código)
|
||||||
|
|
||||||
|
## Características
|
||||||
|
|
||||||
|
* API de sólo lectura
|
||||||
|
* Acceso público, sin autenticación
|
||||||
|
* Usa GraphQL por debajo
|
||||||
|
* El tamaño máximo (y por defecto) de página está establecido a 25
|
||||||
|
* La profundiad máxima de las consultas es de 8 niveles
|
||||||
|
* Como máximo se pueden solicitar 2 colecciones en una misma consulta
|
||||||
|
* Soporte para peticiones GET (consulta dentro del *query string*) y POST (consulta dentro del *body*, como `application/json` o `application/graphql`).
|
||||||
|
|
||||||
|
## GraphQL
|
||||||
|
|
||||||
|
La API de Consul utiliza GraphQL [http://graphql.org](https://graphql.org), en concreto la [implementación en Ruby](http://graphql-ruby.org/). Si no estás familiarizado con este tipo de APIs, es recomendable investigar un poco sobre GraphQL previamente.
|
||||||
|
|
||||||
|
Una de las caracteríticas que diferencian una API REST de una GraphQL es que con esta última es posible construir *consultas personalizadas*, de forma que el servidor nos devuelva únicamente la información en la que estamos interesados.
|
||||||
|
|
||||||
|
Las consultas en GraphQL están escritas siguiendo un estándar que presenta ciertas similitudes con el formato JSON, por ejemplo:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
proposal(id: 1) {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
public_author {
|
||||||
|
id,
|
||||||
|
username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Las respuestas son en formato JSON:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"proposal": {
|
||||||
|
"id": 1,
|
||||||
|
"title": "Hacer las calles del centro de Madrid peatonales",
|
||||||
|
"public_author": {
|
||||||
|
"id": 2,
|
||||||
|
"username": "electrocronopio"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Haciendo peticiones a la API
|
||||||
|
|
||||||
|
Siguiendo las [directrices oficiales](http://graphql.org/learn/serving-over-http/), la API de Consul soporta los siguientes tipos de peticiones:
|
||||||
|
|
||||||
|
* Peticiones GET, con la consulta dentro del *query string*.
|
||||||
|
* Peticiones POST
|
||||||
|
* Con la consulta dentro del *body*, con `Content-Type: application/json`
|
||||||
|
* Con la consulta dentro del *body*, con `Content-Type: application/graphql`
|
||||||
|
|
||||||
|
### Clientes soportados
|
||||||
|
|
||||||
|
Al ser una API que funciona a través de HTTP, cualquier herramienta capaz de realizar este tipo de peticiones resulta válida.
|
||||||
|
|
||||||
|
Esta sección contiene unos pequeños ejemplos sobre cómo hacer las peticiones a través de:
|
||||||
|
|
||||||
|
* GraphiQL
|
||||||
|
* Extensiones de Chrome como Postman
|
||||||
|
* Cualquier librería HTTP
|
||||||
|
|
||||||
|
#### GraphiQL
|
||||||
|
|
||||||
|
[GraphiQL](https://github.com/graphql/graphiql) es una interfaz de navegador para realizar consultas a una API GraphQL, así como una fuente adicional de documentación. Está desplegada en la ruta `/graphiql` y es la mejor forma de familiarizarse una API basada en GraphQL.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Tiene tres paneles principales:
|
||||||
|
|
||||||
|
* En el panel de la izquierda se escribe la consulta a realizar.
|
||||||
|
* El panel central muestra el resultado de la petición.
|
||||||
|
* El panel derecho (ocultable) contiene una documentación autogenerada a partir de la información expuesta en la API.
|
||||||
|
|
||||||
|
#### Postman
|
||||||
|
|
||||||
|
Ejemplo de petición `GET`, con la consulta como parte del *query string*:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Ejemplo de petición `POST`, con la consulta como parte del *body* y codificada como `application/json`:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
La consulta debe estar ubicada en un documento JSON válido, como valor de la clave `"query"`:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Librerías HTTP
|
||||||
|
|
||||||
|
Por supuesto es posible utilizar cualquier librería HTTP de lenguajes de programación.
|
||||||
|
|
||||||
|
**IMPORTANTE**: Debido a los protocolos de seguridad de los servidores del Ayuntamiento de Madrid, es necesario incluir un *User Agent* perteneciente a un navegador para que la petición no sea descartada. Por ejemplo:
|
||||||
|
|
||||||
|
`User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36`
|
||||||
|
|
||||||
|
|
||||||
|
## Información disponible
|
||||||
|
|
||||||
|
El fichero [config/api.yml](../../config/api.yml) contiene una lista completa de los modelos (y sus campos) que están expuestos actualmente en la API.
|
||||||
|
|
||||||
|
La lista de modelos es la siguiente:
|
||||||
|
|
||||||
|
| Modelo | Descripción |
|
||||||
|
| ----------------------- | ---------------------------- |
|
||||||
|
| `User` | Usuarios |
|
||||||
|
| `Debate` | Debates |
|
||||||
|
| `Proposal` | Propuestas |
|
||||||
|
| `Comment` | Comentarios en debates, propuestas y otros comentarios |
|
||||||
|
| `Geozone` | Geozonas (distritos) |
|
||||||
|
| `ProposalNotification` | Notificaciones asociadas a propuestas |
|
||||||
|
| `ActsAsTaggableOn::Tag` | Tags en debates y propuestas |
|
||||||
|
| `Vote` | Información sobre votos |
|
||||||
|
|
||||||
|
## Ejemplos de consultas
|
||||||
|
|
||||||
|
### Recuperar un único elemento de una colección
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
proposal(id: 2) {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
comments_count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Respuesta:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"proposal": {
|
||||||
|
"id": 2,
|
||||||
|
"title": "Crear una zona cercada para perros en Las Tablas",
|
||||||
|
"comments_count": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recuperar una colección completa
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
proposals {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Respuesta:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"proposals": {
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"title": "ELIMINACION DE ZONA APARCAMIENTO EXCLUSIVO FUNCIONARIOS EN MADRID"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"title": "iluminación de zonas deportivas"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Paginación
|
||||||
|
|
||||||
|
Actualmente el número máximo (y por defecto) de elementos que se devuelven en cada página está establecido a 25. Para poder navegar por las distintas páginas es necesario solicitar además información relativa al `endCursor`:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
proposals(first: 25) {
|
||||||
|
pageInfo {
|
||||||
|
hasNextPage
|
||||||
|
endCursor
|
||||||
|
}
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
La respuesta:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"proposals": {
|
||||||
|
"pageInfo": {
|
||||||
|
"hasNextPage": true,
|
||||||
|
"endCursor": "NQ=="
|
||||||
|
},
|
||||||
|
"edges": [
|
||||||
|
# ...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Para recuperar la siguiente página, hay que pasar como parámetro el cursor recibido en la petición anterior, y así sucesivamente:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
proposals(first: 25, after: "NQ==") {
|
||||||
|
pageInfo {
|
||||||
|
hasNextPage
|
||||||
|
endCursor
|
||||||
|
}
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Acceder a varios recursos en una única petición
|
||||||
|
|
||||||
|
Esta consulta solicita información relativa a varios modelos distintos en una única peticion: `Proposal`, `User`, `Geozone` y `Comment`:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
proposal(id: 15262) {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
public_author {
|
||||||
|
username
|
||||||
|
},
|
||||||
|
geozone {
|
||||||
|
name
|
||||||
|
},
|
||||||
|
comments(first: 2) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Limitaciones de seguridad
|
||||||
|
|
||||||
|
Permitir que un cliente personalice las consultas supone un factor de riesgo importante. Si se permitiesen consultas demasiado complejas, sería posible realizar un ataque DoS contra el servidor.
|
||||||
|
|
||||||
|
Existen tres mecanismos principales para evitar este tipo de abusos:
|
||||||
|
|
||||||
|
* Paginación de resultados
|
||||||
|
* Limitar la profundidad máxima de las consultas
|
||||||
|
* Limitar la cantidad de información que es posible solicitar en una consulta
|
||||||
|
|
||||||
|
### Ejemplo de consulta demasiado profunda
|
||||||
|
|
||||||
|
La profundidad máxima de las consultas está actualmente establecida en 8. Consultas más profundas (como la siguiente), serán rechazadas:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
user(id: 1) {
|
||||||
|
public_proposals {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
comments {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
body,
|
||||||
|
public_author {
|
||||||
|
username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
La respuesta obtenida tendrá el siguiente aspecto:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"message": "Query has depth of 9, which exceeds max depth of 8"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ejemplo de consulta demasiado compleja
|
||||||
|
|
||||||
|
El principal factor de riesgo se da cuando se solicitan varias colecciones de recursos en una misma consulta. El máximo número de colecciones que pueden aparecer en una misma consulta está limitado a 2. La siguiente consulta solicita información de las colecciónes `users`, `debates` y `proposals`, así que será rechazada:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
users {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
public_debates {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
public_proposals {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
La respuesta obtenida tendrá el siguiente aspecto:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"message": "Query has complexity of 3008, which exceeds max complexity of 2500"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "Query has complexity of 3008, which exceeds max complexity of 2500"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "Query has complexity of 3008, which exceeds max complexity of 2500"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
No obstante sí que es posible solicitar información perteneciente a más de dos modelos en una única consulta, siempre y cuando no se intente acceder a la colección completa. Por ejemplo, la siguiente consulta que accede a los modelos `User`, `Proposal` y `Geozone` es válida:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
user(id: 468501) {
|
||||||
|
id
|
||||||
|
public_proposals {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
title
|
||||||
|
geozone {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
La respuesta:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"user": {
|
||||||
|
"id": 468501,
|
||||||
|
"public_proposals": {
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"title": "Empadronamiento necesario para la admisión en GoFit Vallehermoso",
|
||||||
|
"geozone": {
|
||||||
|
"name": "Chamberí"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ejemplos de código
|
||||||
|
|
||||||
|
El directorio [doc/api/examples](examples) contiene ejemplos de código para acceder a la API.
|
||||||
26
doc/api/examples/ruby/example_1.rb
Normal file
26
doc/api/examples/ruby/example_1.rb
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
require 'http'
|
||||||
|
|
||||||
|
API_ENDPOINT = 'https://decide.madrid.es/graphql'
|
||||||
|
|
||||||
|
def make_request(query_string)
|
||||||
|
HTTP.headers('User-Agent' => 'Mozilla/5.0', accept: 'application/json')
|
||||||
|
.get(
|
||||||
|
API_ENDPOINT,
|
||||||
|
params: { query: query_string.gsub("\n", '').gsub(" ", '') }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
query = """
|
||||||
|
{
|
||||||
|
proposal(id: 1) {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
public_created_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = make_request(query)
|
||||||
|
|
||||||
|
puts "Response code: #{response.code}"
|
||||||
|
puts "Response body: #{response.body}"
|
||||||
65
doc/api/examples/ruby/example_2.rb
Normal file
65
doc/api/examples/ruby/example_2.rb
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
require 'http'
|
||||||
|
|
||||||
|
API_ENDPOINT = 'https://decide.madrid.es/graphql'
|
||||||
|
|
||||||
|
def make_request(query_string)
|
||||||
|
HTTP.headers('User-Agent' => 'Mozilla/5.0', accept: 'application/json')
|
||||||
|
.get(
|
||||||
|
API_ENDPOINT,
|
||||||
|
params: { query: query_string.gsub("\n", '').gsub(" ", '') }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_query(options = {})
|
||||||
|
page_size = options[:page_size] || 25
|
||||||
|
page_size_parameter = "first: #{page_size}"
|
||||||
|
|
||||||
|
page_number = options[:page_number] || 0
|
||||||
|
after_parameter = page_number > 0 ? ", after: \"#{options[:next_cursor]}\"" : ""
|
||||||
|
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
proposals(#{page_size_parameter}#{after_parameter}) {
|
||||||
|
pageInfo {
|
||||||
|
endCursor,
|
||||||
|
hasNextPage
|
||||||
|
},
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
public_created_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
page_number = 0
|
||||||
|
next_cursor = nil
|
||||||
|
proposals = []
|
||||||
|
|
||||||
|
loop do
|
||||||
|
|
||||||
|
puts "> Requesting page #{page_number}"
|
||||||
|
|
||||||
|
query = build_query(page_size: 25, page_number: page_number, next_cursor: next_cursor)
|
||||||
|
response = make_request(query)
|
||||||
|
|
||||||
|
response_hash = JSON.parse(response.body)
|
||||||
|
page_info = response_hash['data']['proposals']['pageInfo']
|
||||||
|
has_next_page = page_info['hasNextPage']
|
||||||
|
next_cursor = page_info['endCursor']
|
||||||
|
proposal_edges = response_hash['data']['proposals']['edges']
|
||||||
|
|
||||||
|
puts "\tHTTP code: #{response.code}"
|
||||||
|
|
||||||
|
proposal_edges.each do |edge|
|
||||||
|
proposals << edge['node']
|
||||||
|
end
|
||||||
|
|
||||||
|
page_number += 1
|
||||||
|
|
||||||
|
break if !has_next_page
|
||||||
|
end
|
||||||
BIN
doc/imgs/graphiql.png
Normal file
BIN
doc/imgs/graphiql.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 160 KiB |
BIN
doc/imgs/graphql-postman-get.png
Normal file
BIN
doc/imgs/graphql-postman-get.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
BIN
doc/imgs/graphql-postman-post-body.png
Normal file
BIN
doc/imgs/graphql-postman-post-body.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
BIN
doc/imgs/graphql-postman-post-headers.png
Normal file
BIN
doc/imgs/graphql-postman-post-headers.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
Reference in New Issue
Block a user