Start Building

First, we will install two Meldio dependencies: Node.js and MongoDB. Once Node.js and MongoDB are in place, Meldio setup is super simple.

Our goal is to create a simple GraphQL backend for the Rockets app. Our backend will incorporate two types - Rocket and Launch and two mutations - addRocket and launchRocket.

Requirements

  1. OS X or Linux system.
  2. Node.js 4.0 or newer. NVM is the recommended way to install Node.js.
  3. MongoDB 3.2 or newer. Homebrew is the recommended way to install MongoDB on OS X.

Node.js Setup

The recommended way to install Node.js is with NVM. Please consult nvm installation instructions and then install NVM with the following command:

curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.0/install.sh | bash

Once NVM is installed, open a new terminal window and install Node.js with the following:

nvm install node && nvm alias default node
This will install the latest version of Node.js and alias it as node. Open a new terminal window and confirm Node.js is set up correctly with node --version command. This should print the latest version of Node.js.

MongoDB Setup

The recommended way to install MongoDB on an OS X system is with brew package manager, like this:

brew update && brew install mongodb

It is easiest to configure the system to launch MongoDB on startup using the following commands:

ln -sfv /usr/local/opt/mongodb/*.plist ~/Library/LaunchAgents
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.mongodb.plist

Alternatively, use the following command to start the Mongo daemon as needed:

mongod --config /usr/local/etc/mongod.conf

For MongoDB installation on various Linux distributions, please consult the MongoDB documentation.

Meldio Setup

To install Meldio, run the following:

npm install -g meldio
If the system permissions require sudo for installing global npm modules, the command above will not work. Instead, run npm with sudo like this: sudo npm install -g meldio

New Application

We will now create a new Meldio application. Run the following command to initialize a new application in the rockets directory:

meldio init rockets

Meldio will prompt for the following settings:

  • Version (semver, so 0.0.1 is fine)
  • MongoDB connection URI (hit Enter to accept default)
  • Meldio server host (accept default)
  • Meldio server port (accept default)
  • Authentication providers. Hit Enter to proceed without authentication.

At this point, Meldio has created and initialized the application directory.

Application Directory

The application is now initialized, so let's enter the rockets directory and list the files:

cd rockets && ls -la

You should see a file listing that includes the following files and directories:

  • .env - file with OAuth secrets and JWT signature key. This file will be ignored by git
  • .eslintrc - default configuration for linting JavaScript and JSON files
  • .gitignore - default git ignore configuration. Ignores .env file and .build directory
  • hooks - directory with authentication hooks, stub implementations are provided by default
  • mutations - directory with mutation implementations, empty by default
  • package.json - standard package file. Meldio configuration is namespaced under config / meldio keys
  • permissions.js - functional permissions implementation
  • schema.sdl - schema definition

Start Meldio

Let's start Meldio in the watch mode. Meldio will watch the file system, validate schema, lint the files, transpile them and validate that hooks and permissions are okay on the fly as we are changing the files. Start Meldio in watch mode with:

meldio watch

Meldio will start with this:

 building...

 analyzing schema...
 successfully analyzed 2 schema elements.

 linting...
 successfully linted 5 files.

 transpiling...
 successfully transpiled 5 files.

 checking hooks...
 done with hooks checkup.

 checking permissions...
 done with permissions checkup.

 starting...

  __  __ ______ _      _____ _____ ____      
 |  \/  |  ____| |    |  __ \_   _/ __ \       app rockets ver 0.0.1
 | \  / | |__  | |    | |  | || || |  | |      http://localhost:9000/graphql
 | |\/| |  __| | |    | |  | || || |  | |      auth:  facebook    google
 | |  | | |____| |____| |__| || || |__| |             github      password
 |_|  |_|______|______|_____/_____\____/0.4.5

 watching...

Schema Definition

Open up schema.sdl, delete everything and add the schema definition for our Rockets app:

type Rocket implements Node @rootConnection(field: "allRockets") {
  id: ID!
  manufacturer: String!
  model: String!
  launches: NodeConnection(Launch, rocket)
}

type Launch implements Node {
  id: ID!
  payload: String!
  weight: Float!
  timestamp: String
  outcome: LaunchOutcome
  rocket: NodeConnection(Rocket, launches)
}

enum LaunchOutcome { SUCCESS, FAILURE }

Save the new schema definition and observe that Meldio immediately warns that we are missing permissions for Rocket and Launch types. However, since we are not using authentication, delete the permissions.js file and hooks directory with the following:

rm -rf hooks permissions.js

Node Interface

Rocket and Launch types implement Node interface. This makes rocket and launch objects directly addressable from the app using their globally unique identifiers. In a way, types that implement Node are analogous to tables in SQL or documents in NoSQL databases.

Node Connection

Note that launches and rocket fields within Rocket and Launch types are defined as NodeConnection.

NodeConnection is used to create a Relay-compliant cursor connection between two Node types. In this example, NodeConnection takes two parameters: 1) target type name and 2) field name in the target type that points back to the originating node (i.e. back-reference). NodeConnection can also specify properties on the edge between two nodes using type name as the third parameter.

Root Connection

Note that the @rootConnection directive on the Rocket type creates the allRockets root field which allows us to list all rockets.

Mutations

Meldio mutations are executed on the server and may incorporate multiple operations that update the state of the backend. In some ways, mutations are similar to stored procedures in SQL databases. The key advantage of using mutations is that they move the code from client applications to the backend, promoting code reuse for the multi-platform apps and improving backward compatibility for the rapidly evolving apps.

Our Rockets app backend will support two mutations:

  • addRocket - adds a new rocket
  • launchRocket - adds a new launch for the given rocket

Mutation Signatures

First, we will add the following mutation signatures to the schema definition file, right after the type definitions:

mutation addRocket(manufacturer: String!, model: String!) {
  rocket: Rocket
}

mutation launchRocket(
  rocketId: ID!
  payload: String!
  weight: Float!
  outcome: LaunchOutcome
) {
  launchEdge: Edge(Launch)
  rocket: Rocket
}

Here, the addRocket mutation simply takes rocket manufacturer and model as inputs, adds the Rocket object and returns that object back to the client app.

The second mutation, launchRocket, takes an id of the rocket that is about to launch, creates a new Launch object with the provided payload, weight and outcome, and creates a connection edge between the rocket and the newly created Launch object. The mutation returns that edge along with the updated Rocket object to allow the client app to query for aggregates (e.g. total launches) on the rocket object.

As soon as we save the schema, Meldio will report an error indicating that two mutations are defined in the schema but not implemented. Let's fix that now.

Mutation Implementations

Add two files under the mutations directory:

  • addRocket.js
  • launchRocket.js

Let's now implement addRocket.js:

// addRocket.js
export async function addRocket({ manufacturer, model }) {
  // get the Rocket type out of the model
  const { Rocket } = this.model;

  // add a new Rocket node, with manufacturer and model fields set
  const rocket = await Rocket.addNode({ manufacturer, model });

  // return the newly added rocket
  return { rocket };
}

Then, let's implement launchRocket.js:

// launchRocket.js
export async function launchRocket({rocketId, payload, weight, outcome}) {
  // get the Rocket and Launch types out of the model
  const { Rocket, Launch } = this.model;
  // get the current timestamp and convert it to String
  const timestamp = new Date(this.timestamp).toString();

  // if a rocket with the given id does not exist, return nulls
  if (!await Rocket.node(rocketId).exists()) {
    return { launchEdge: null, rocket: null };
  }

  // identify the rocket using id
  const rocket = Rocket.node(rocketId);

  // create a new launch object
  const launch = await Launch.addNode({ payload, weight, timestamp, outcome });

  // add a connection edge from the rocket to the newly created launch object
  const launchEdge = await rocket.launches.addEdge(launch);

  // return the launch edge and the rocket
  return { launchEdge, rocket };
}

We are all done building, so now let's play with some queries.

Query Editor

With Meldio running, open the endpoint URL, http://localhost:9000/graphql. When a Meldio endpoint is opened in the browser, it will display the query editor we can use to build and validate our queries. Let's add a rocket by copying and pasting the following mutation into the left-hand pane:

mutation AddRocket {
  addRocket(input: {manufacturer: "Space X", model: "Falcon 9", clientMutationId: "1"}) {
    rocket {
      id
      manufacturer
      model
    }
    clientMutationId
  }
}

Hit Control-Enter and Meldio will return the result similar to the following JSON object (id will be different):

{
  "data": {
    "addRocket": {
      "rocket": {
        "id": "-KDthG2xkQJdGktzG80T-hF3B5K",
        "manufacturer": "Space X",
        "model": "Falcon 9"
      },
      "clientMutationId": "1"
    }
  }
}

Now, let's launch that Falcon 9 we just added. Ready? Just replace the rocketId below with the id you received when you executed the addRocket mutation:

mutation LaunchRocket {
  launchRocket(input: {rocketId: "-KDthG2xkQJdGktzG80T-hF3B5K", payload: "SES-9", weight: 11621.0, outcome: SUCCESS, clientMutationId: "2"}) {
    launchEdge {
      node {
        id
        payload
        weight
        timestamp
        outcome
      }
    }
    rocket {
      launches {
        totalLaunches: count
        successfulLaunches: count(filterBy: {node: {outcome: {eq: SUCCESS}}})
        failedLaunches: count(filterBy: {node: {outcome: {eq: FAILURE}}})
      }
    }
  }
}

Since the launchRocket mutation returns an updated rocket object, we can query for aggregates on that object (e.g. total, successful and failed launches) to refresh that data on the client following execution of the mutation. All this happens in one round-trip to the backend.

Add a few more rockets and launches and then execute the following query to fetch all that data, all in one round-trip to the backend:

query GimmeAll {
  allRockets {
    numberOfRockets: count
    numberOfSpaceXRockets: count(filterBy: {node: {manufacturer: {eq: "Space X"}}})
    edges {
      node {
        id
        manufacturer
        model
        launches {
          totalLaunches: count
          successfulLaunches: count(filterBy: {node: {outcome: {eq: SUCCESS}}})
          failedLaunches: count(filterBy: {node: {outcome: {eq: FAILURE}}})
          totalWeightLifted: sum(node: weight)
          maxWeightLifted: max(node: weight)
          edges {
            node {
              id
              payload
              weight
              timestamp
              outcome
            }
          }
        }
      }
    }
  }
}

Now, let's move on to some cool example apps.