Sistemas y Tecnologías Web: Servidor

Master de II. ULL. 1er cuatrimestre


Organization ULL-MII-SYTWS-2122   Classroom ULL-MII-SYTWS-2122   Campus Virtual SYTWS   Chat Chat   Profesor Casiano

GraphQL-Simple-Server

Requisitos

Usando los módulos npm express, express-graphql y graphql escriba un servicio web con una API GraphQL y pruébela usando GraphiQL.

Set up

Para hacer esta práctica empezaremos instalando los módulos que necesitamos y luego en index.js importamos las correspondientes funciones:

1
2
3
const express = require("express")
const { graphqlHTTP } = require("express-graphql")
const { buildSchema } = require("graphql")

Puede aprovechar cualquier hoja de cálculo que tenga a mano y la exporta a CSV, para usarla como datos de entrada para hacer las pruebas en esta práctica.

Después, puede usar el módulo csvtojson para convertir los datos a un objeto JS.

1
2
3
4
const csv=require('csvtojson')
const port = process.argv[2] || 4006;
const csvFilePath = process.argv[3] || 'SYTWS-2122.csv'
const data = String(fs.readFileSync(csvFilePath))

Para hacer el parsing del fichero CSV podemos llamar a csv().fromFile(<file>) o bien puede usar el ejecutable de línea de comandos que provee $ csvtojson [options] <csv file path>.

1
2
3
4
async function main () {
    let classroom = await csv().fromFile(csvFilePath);
    ...
}

Esto deja en classroom un array con las filas del CSV. En este caso de ejemplo, la información sobre las calificaciones de los estudiantes.

Uno de los primeros pasos a la hora de construir un servicio GraphQL es definir el esquema GraphQL usando el lenguaje SDL.

GraphQL Schema

A GraphQL schema1 is at the center of any GraphQL server implementation and describes the functionality available to the clients which connect to it. An Schema is written using the Schema Definition Language (SDL)2, that defines the syntax for writing GraphQL Schemas. It is otherwise known as Interface Definition Language. It is the lingua franca shared for building GraphQL APIs regardless of the programming language chosen.

Here is an example of a GraphQL Schema written in SDL:

  type Student {
      AluXXXX: String!
      Nombre: String!
      markdown: String
  }

  type Query {
      students: [ Student ]
      student(AluXXXX: String!): Student
  }

  type Mutation {
      addStudent(AluXXXX: String!, Nombre: String!): Student
      setMarkdown(AluXXXX: String!, markdown: String!): Student 
  }

In addition to queries and mutations, GraphQL supports a third operation type: subscriptions

Like queries, subscriptions enable you to fetch data. Unlike queries, subscriptions are long-lasting operations that can change their result over time. They can maintain an active connection to your GraphQL server (most commonly via WebSocket), enabling the server to push updates to the subscription’s result.

GraphQL SDL is a typed language. Types can be Scalar or can be composed as the Student type in the former example.

GraphQL ships with some scalar types out of the box; Int, Float, String, Boolean and ID.

The fields whose types have an exclamation mark, !, next to them are non-null fields. These are fields that won’t return a null value when you query them.

The function buildSchema provided by the graphql module has the signature:

1
function buildSchema(source: string | Source): GraphQLSchema

Creates a GraphQLSchema object from GraphQL schema language. The schema will use default resolvers3.

1
const AluSchema = buildSchema(StringWithMySchemaDefinition)

Resolvers

A resolver is a function that connects schema fields and types to various backends. Resolvers provide the instructions for turning a GraphQL operation into data.

A resolver can retrieve data from or write data to anywhere, including a SQL, No-SQL, or graph database, a micro-service, and a REST API. Resolvers can also return strings, ints, null, and other types.

To define our resolvers we create now the object root mapping the schema fields (students, student, addStudent, setMarkdown) to their corresponding functions:

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
async function main () {
    let classroom=await csv().fromFile(csvFilePath);
        const root = {
        students: () => classroom,
        student: ({AluXXXX}) => {
            let result = classroom.find(s => {
                return s["AluXXXX"] == AluXXXX
            });
            return result
        },
        addStudent: (obj, args, context, info) => {
            const {AluXXXX, Nombre} = obj; 

            let result = classroom.find(s => {
                return s["AluXXXX"] == AluXXXX
            });
            if (!result) {
                let alu = {AluXXXX : AluXXXX, Nombre: Nombre}
                console.log(`Not found ${Nombre}! Inserting ${AluXXXX}`)
                classroom.push(alu)
                return alu    
            }
            return result;
        },
        setMarkdown: ({AluXXXX, markdown}) => {
            let result = classroom.findIndex(s => s["AluXXXX"] === AluXXXX)
            if (result === -1) {
              let message = `${AluXXXX} not found!`
              console.log(message);
              return null;
            } 
            classroom[result].markdown = markdown
            return classroom[result]
        }
    }
      
    ... // Set the express app to work
}

main();

Observe how setMarkDown and addStudent sometimes return null since it is allowed by the schema we have previously set.

 setMarkdown(AluXXXX: String!, markdown: String!): Student

There is no exclamation ! at the value returned in the declaration of the setMarkDown mutation.

Every GraphQL query goes through these phases:

  1. Queries are parsed into an abstract syntax tree (or AST). See https://astexplorer.net/
  2. Validated: Checks for query correctness and check if the fields exist.
  3. Executed: The runtime walks through the AST,
    1. Descending from the root of the tree,
    2. Invoking resolvers,
    3. Collecting up results, and
    4. Emiting the final JSON

In this example, the root Query type is the entry point to the AST and contains two fields, user and album. The user and album resolvers are usually executed in parallel or in no particular order.

The AST is traversed breadth-first, meaning user must be resolved before its children name and email are visited.

If the user resolver is asynchronous, the user branch delays until its resolved. Once all leaf nodes, name, email, title, are resolved, execution is complete.

Typically, fields are executed in the order they appear in the query, but it’s not safe to assume that. Because fields can be executed in parallel, they are assumed to be atomic, idempotent, and side-effect free.

A resolver is a function that resolves a value for a type or field in a schema.

  • Resolvers can return objects or scalars like Strings, Numbers, Booleans, etc.
  • If an Object is returned, execution continues to the next child field.
  • If a scalar is returned (typically at a leaf node of the AST), execution completes.
  • If null is returned, execution halts and does not continue.

It’s worth noting that a GraphQL server has built-in default resolvers, so you don’t have to specify a resolver function for every field. A default resolver will look in root to find a property with the same name as the field. An implementation likely looks like this:

1
2
3
4
5
6
7
8
export default {
    Student: {
        AluXXXX: (root, args, context, info) => root.AluXXXX,
        Nombre: (root, args, context, info) => root.Nombre,
        markdown: (root, args, context, info) => root.markdown

    }
}

This is the reason why there was no need to implement the resolvers for these fields.

Starting the express-graphql middleware

Now what remains is to set the graphqlHTTP the express middleware provided by the module express-graphql to work

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const app = express()

async function main () {
    let classroom = await csv().fromFile(csvFilePath);
    const root = { ... }
      
    app.use(
        '/graphql',
        graphqlHTTP({
          schema: AluSchema,
          rootValue: root,
          graphiql: true,
        }),
      );
      
      app.listen(port);
      console.log("Running at port "+port)
}

It has the following properties:

  • schema, our GraphQL schema
  • rootValue, our resolver functions
  • graphiql, a boolean stating whether to use graphiql, we want that so we pass true here

Testing with GraphiQL

We can now run the app and open the browser at the url http://localhost:4000/graphql to make graphql queries using GraphiQL.

Use GraphiQL to test your API. GraphiQL is an in-browser IDE for GraphQL development and workflow. Para ello vea este video:

References

FootNotes

  1. For more detail on the GraphQL schema language, see the schema language docs 

  2. Schema language cheat sheet 

  3. Root fields & resolvers 

Comment with GitHub Utterances