# getting-started-with-apollo-server-dataloader-knex.mo

## Owner: [Thomas Pucci](https://github.com/tpucci)

## Prerequisites (\~12min)

* Have [**Yarn**](https://yarnpkg.com/en/docs/install) installed (\~5min)
* Have [**Docker**](https://docs.docker.com/engine/installation/) and **Docker-compose** installed (\~5min)
* Have [**Postman**](https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop) installed (\~2min)

## Thanks

Thanks to [Tycho Tatitscheff](https://github.com/tychota) and [Yann Leflour](https://github.com/yleflour) for helping me with their great [BAM API repo](https://github.com/bamlab/bam-api)

## Context

During this standard, we will create a Heroes graphQL API. We will have a Hero model with superheroes real and hero names. We will add one example of association.

Our API will be lightly protected and use batching to minimise DB round-trips.

## Steps (\~61min)

> **Note**: You should commit between each step.

### Initialise a new project (\~6min)

* Create and go to a new directory for the project: `mkdir graphql_formation && cd graphql_formation`
* Init a git repository: `git init`
* Create two services with Docker-compose, one postgres database and one node server:

  * > For this step, notice that our final folder architecture looks like this:
    >
    > ```
    > 📂 graphql_formation
    > ├ 📂 api
    > │ └ 🗋 Dockerfile
    > ├ 📂 db
    > │ └ 🗋 Dockerfile
    > ├ 🗋 config.env
    > └ 🗋 docker-compose.yml
    > ```
  * > Make sure your local 3000 port is available as we will use this port to reach our API
  * In a new `api/Dockerfile` file, write all the commands to assemble the API image:

  ```
  FROM node:8.1.0-alpine

  WORKDIR /usr/src/api

  EXPOSE 3000
  CMD ["yarn", "run", "serve"]
  ```

  * In a new `db/Dockerfile` file, write all the commands to assemble the db image:

  ```
  FROM postgres:9.6.3
  ```

  * In a new `docker-compose.yml` file, declare the two services:

  ```
  version: '3'
  services:
    api:
      build:
        context: ./api
      image: heroes-api
      env_file: config.env
      volumes:
        - ./api:/usr/src/api
      ports:
        - 3000:3000
      links:
        - db:db
    db:
      build:
        context: ./db
      env_file: config.env
      image: heroes-db
      ports:
        - 5431:5432
  ```

  * In a new `config.env` file, declare your environnement variable for these Docker containers:

  ```
  POSTGRES_USER=heroesuser
  POSTGRES_PASSWORD=heroespassword
  POSTGRES_DB=heroesdb
  PGDATA=/data
  DB_HOST=db
  ```
* Build these services with the command: `docker-compose build`

> **CHECK 1**: Your terminal should prompt successively these lines confirming Docker images have been built:
>
> `Successfully tagged heroes-db:latest`
>
> `Successfully tagged heroes-api:latest`

### Install nodemon and run our project (\~5min)

* Add this to the project .gitignore: `echo "node_modules" > .gitignore`
* In the `api` folder, interactively create a `api/package.json` file: `cd api && yarn init`
* Add `nodemon`, `babel-cli`, `babel-plugin-transform-class-properties`, `babel-preset-flow` and `babel-preset-es2015` to our dev dependencies: `yarn add nodemon babel-cli babel-plugin-transform-class-properties babel-preset-es2015 babel-preset-flow -D`
* In a new `api/.babelrc` file, write the babel configuration:

  ```javascript
  {
  "presets":[
    "es2015",
    "flow"
  ],
  "plugins":[
    "transform-class-properties"
  ]
  }
  ```
* In our `api/package.json`, write the command to launch the server:

  ```javascript
  "scripts": {
  "serve": "nodemon index.js --exec babel-node"
  }
  ```
* Create a new empty file `api/index.js`
* Go back to the root of the project: `cd ..`
* Run the project: `docker-compose up`&#x20;

> **CHECK 1**: You terminal should prompt the logs of the two containers together with two different colors
>
> **CHECK 2**: From another terminal, you can access the API and see the following folder structure: `docker-compose exec api /bin/sh` then inside the container: `ls -lath`;
>
> ```
> drwxrwxr-x    3 node     node        4.0K Aug 17 12:37 .
> -rw-rw-r--    1 node     node           0 Aug 17 12:37 index.js
> drwxrwxr-x  222 node     node       12.0K Aug 17 12:37 node_modules
> -rw-rw-r--    1 node     node         426 Aug 17 12:37 package.json
> -rw-rw-r--    1 node     node       66.2K Aug 17 12:37 yarn.lock
> -rw-rw-r--    1 node     node          86 Aug 17 12:32 Dockerfile
> drwxr-xr-x    3 root     root        4.0K Aug  3 11:50 ..
> ```
>
> Exit with: `CTRL-D`
>
> **CHECK 3**: You can access the db and prompt the PostgreSQL version: `docker-compose exec db psql -U heroesuser -d heroesdb` then inside the container: `select version();`
>
> ```bash
> PostgreSQL 9.6.3 on x86_64-pc-linux-gnu, compiled by gcc (Debian 4.9.2-10) 4.9.2, 64-bit
> ```
>
> Exit with: `CTRL-D`

### Create a koa server (\~3min)

* Install koa and koa-router in our API: `cd api && yarn add koa koa-router`
* In the `index.js` file, create our server:

```javascript
import Koa from 'koa';
import koaRouter from 'koa-router';

const app = new Koa();
const router = new koaRouter();

router.get('/', ctx => {
  ctx.body = 'Hello World!';
});

app.use(router.routes());
app.use(router.allowedMethods());
app.listen(3000);

console.log('Server is up and running');
```

> **CHECK 1**: In your terminal which run docker-compose, you should see `Server is up and running`
>
> **CHECK 2**: Hitting `localhost:3000` should return `Hello World!`: `curl localhost:3000`

### Create a presentation layer with graphQL (\~6min)

> This layer will let our API know how to present data: what data one user can query? How should front end query this data (fields, root queries, sub queries...)?

* Install graphQL, graphQL Server Koa, graphQL tools and Koa body-parser: `yarn add graphql graphql-server-koa graphql-tools koa-bodyparser`
* In a new folder `api/presentation` add a new `schema.js` file describing a simple graphQL schema:

```javascript
import { makeExecutableSchema } from 'graphql-tools';

const typeDefs = [`
  type Hero {
    id: Int!
    firstName: String
    lastName: String
  }

  type Query {
    heroes: [Hero]
  }

  schema {
    query: Query
  }
`];

const resolvers = {
  Query: {
    heroes: () => ([
      {
        id: 1,
        firstName: 'Clark',
        lastName: 'Kent',
      },
      {
        id: 2,
        firstName: 'Bruce',
        lastName: 'Wayne',
      }
    ]),
  },
}

const schema = makeExecutableSchema({ typeDefs, resolvers });

export default schema;
```

* In the `api/index.js` file, add our `api` endpoint:

```javascript
import koaBody from 'koa-bodyparser';
import { graphqlKoa } from 'graphql-server-koa';
import schema from './presentation/schema';

...

router.post(
  '/api',
  graphqlKoa(async ctx => {
    return {
      schema: schema,
      context: {},
      debug: true,
    };
  })
);

...

// Write the following line before all other app.use(...) calls:
app.use(koaBody());
```

> **CHECK 1**: In **Postman**, making a *POST* request to `localhost:3000/api` which content-type is *JSON(application/json)* with the following raw body:
>
> ```javascript
> {
>   "query": "{heroes { firstName lastName }}"
> }
> ```
>
> ...should return our two heroes, Clark and Bruce: ![](/files/-LCDif2NwRfr_WD0BgnK)

* Install Koa graphiQL: `yarn add koa-graphiql`
* In the `index.js` file, let our API knows it should use Koa-graphiql:

```javascript
import graphiql from 'koa-graphiql';

...

// Write the following block after others router.verb(...) calls:
router.get('/graphiql', graphiql(async (ctx) => ({
  url: '/api',
})));
```

> **CHECK 2**: Hitting `localhost:3000/graphiql` should return graphiql interface and show the Docs
>
> **CHECK 3**: Using graphiql interface with the following query:
>
> ```
> {
>   heroes {
>     firstName
>     lastName
>   }
> }
> ```
>
> ...should return our two heroes, Clark and Bruce:

### Create a business layer (\~5min)

> This layer will contain all business logic: access controll, scoping / whitelisting, batching and caching and computed properties. More explanations can be found [here, in the bam-api repo](https://github.com/bamlab/bam-api). In this MO, we will only cover access control logic and batching / caching.

* In a new `api/business` folder add a new `hero.js` file describing our class for this business object:

```javascript
const mockedHeroes = [
  {
    id: 1,
    firstName: 'Clark',
    lastName: 'Kent',
  },
  {
    id: 2,
    firstName: 'Bruce',
    lastName: 'Wayne',
  }
];

class Hero {
  id: number;
  firstName: string;
  lastName: string;

  constructor(data) {
    this.id = data.id;
    this.firstName = data.firstName;
    this.lastName = data.lastName;
  }

  static async load(ctx, args) {
    const data = mockedHeroes[args.id];
    if (!data) return null;

    return new Hero(data);
  }

  static async loadAll(ctx, args) {
    const data = mockedHeroes;

    return data.map(row => new Hero(row));
  }

}

export default Hero;
```

* In our previous `presentation/schema.js` file, modify our mocked resolvers to use our business layer:

```diff
+import Hero from '../business/hero';

  type Query {
    heroes: [Hero]
+    hero(id: Int!): Hero
  }

const resolvers = {
  Query: {
-    heroes: () => ([
-      {
-        firstName: 'Clark',
-        lastName: 'Kent',
-      },
-      {
-        firstName: 'Bruce',
-        lastName: 'Wayne',
-      }
-    ]),
+    heroes: async (_, args, ctx) => Hero.loadAll(ctx, args),
+    hero: async (_, args, ctx) => Hero.load(ctx, args),
  }
}
```

> **CHECK 1**: Using graphiql interface with the following query:
>
> ```
> {
>   heroes {
>     id
>     firstName
>     lastName
>   }
> }
> ```
>
> ...should return our two heroes, Clark and Bruce.
>
> **CHECK 2**: Using graphiql interface with the following query:
>
> ```
> {
>   hero(id:0) {
>     id
>     firstName
>     lastName
>   }
> }
> ```
>
> ...should return Clark Kent with its `id: 1`.
>
> **CHECK 3**: Using graphiql interface with the following query:
>
> ```
> {
>   hero(id:1) {
>     id
>     firstName
>     lastName
>   }
> }
> ```
>
> ...should return Bruce Wayne with its `id: 2`.

### Seed our database (\~8min)

* Install `knex` and `pg` at the root of the project: `cd .. && yarn add knex pg`
* At the root of our project, add a `knexfile.js` file:

```javascript
module.exports = {
  development: {
    client: 'pg',
    connection: {
      host: 'localhost',
      port: 5431,
      user: 'heroesuser',
      password: 'heroespassword',
      database: 'heroesdb',
    },
    migrations: {
      directory: './api/db/migrations',
    },
    seeds: {
      directory: './api/db/seeds',
    },
  },
};
```

* Create a migration file: `yarn knex migrate:make add_heroes_table` and complete the new created file with this:

```javascript
exports.up = function(knex, Promise) {
  return knex.schema.createTableIfNotExists('Heroes', function(table) {
    table.increments('id');
    table.string('firstName');
    table.string('lastName');
    table.string('heroName');
  });
};

exports.down = function(knex, Promise) {
  return knex.schema.dropTableIfExists('Heroes');
};
```

* Create a seed file: `yarn knex seed:make heroes` and complete the new created file with this:

```javascript
exports.seed = function(knex, Promise) {
  return knex('Heroes').del()
    .then(function () {
      return knex('Heroes').insert([
        {id: 1, firstName: 'Clark', lastName: 'Kent', heroName: 'Superman'},
        {id: 2, firstName: 'Bruce', lastName: 'Wayne', heroName: 'Batman'},
        {id: 3, firstName: 'Peter', lastName: 'Parker', heroName: 'Spiderman'},
        {id: 4, firstName: 'Susan', lastName: 'Storm-Richards', heroName: 'Invisible Woman'},
      ]);
    });
};
```

* Run the migration and the seed: `yarn knex migrate:latest && yarn knex seed:run`

> **CHECK 1**: You can access the db and prompt content of the `Heroes` table: `docker-compose exec db psql -U heroesuser -d heroesdb` then inside the container: `select * from "Heroes";`;
>
> ```bash
>  id | firstName |    lastName    |    heroName     
> ----+-----------+----------------+-----------------
>   1 | Clark     | Kent           | Superman        
>   2 | Bruce     | Wayne          | Batman          
>   3 | Peter     | Parker         | Spiderman       
>   4 | Susan     | Storm-Richards | Invisible Woman 
> (4 rows)
> ```
>
> Exit with: `CTRL-D`

### Create a db layer with knex (\~6min)

> This layer let our API query the data using knex query builder.

* Install `knex` and `pg` in our API: `cd api && yarn add knex pg`
* In the `api/db` folder add a new `index.js` file:

```javascript
import knex from 'knex';

export default knex({
  client: 'pg',
  connection: {
    host: process.env.DB_HOST,
    user: process.env.POSTGRES_USER,
    password: process.env.POSTGRES_PASSWORD,
    database: process.env.POSTGRES_DB,
  },
  debug: true,
});
```

* In a new `api/db/queryBuilders` subfolder, create a new `hero.js` file and add these few methods to query our data:

```javascript
// @flow
import db from '..';

class Hero {
  static async getById(id: number) {
    return db
    .first()
    .table('Heroes')
    .where('id', id);
  }

  static async getByIds(ids: Array<number>) {
    return db
    .select()
    .table('Heroes')
    .whereIn('id', ids);
  }

  static async getAll() {
    return db
    .select()
    .table('Heroes');
  }
}

export default Hero;
```

* Modify the `api/db/queryBuilders/hero.js` file in our business layer this way:

```diff
-const heroes = [
-  {
-    id: 0,
-    firstName: 'Clark',
-    lastName: 'Kent',
-  },
-  {
-    id: 1,
-    firstName: 'Bruce',
-    lastName: 'Wayne',
-  }
-];
+import HeroDB from '../db/queryBuilders/hero';

 class Hero {

   static async load(ctx, args) {
-    const data = heroes[args.id];
+    const data = await HeroDB.getById(args.id);
     if (!data) return null;

     return new Hero(data);
   }

   static async loadAll({ authToken, dataLoaders }) {
-    const data = heroes;
+    const data = await HeroDB.getAll();

     return data.map(row => new Hero(row));
   }
```

> **CHECK 1**: Using graphiql interface with the following query:
>
> ```
> {
>   hero(id:1) {
>     id
>     firstName
>     lastName
>   }
> }
> ```
>
> ...should return Clark Kent with its `id: 1`.
>
> **CHECK 2**: Using graphiql interface with the following query:
>
> ```
> {
>   heroes {
>     id
>     firstName
>     lastName
>   }
> }
> ```
>
> ...should return all 4 heroes of our database.

### Add association to our API (\~6min)

> Association are made both in our db and in our API, in our presentation layer.

* Create a new migration: `cd .. && yarn knex migrate:make add_heroes_enemies`
* Complete the newly created migration file with this:

```javascript
exports.up = function(knex, Promise) {
  return knex.schema.table('Heroes', function(table) {
    table.integer('enemyId').references('id').inTable('Heroes');
  });
};

exports.down = function(knex, Promise) {
  return knex.schema.table('Heroes', function(table) {
    table.dropColumn('heroName');
  });
};
```

* Modify our `api/db/seeds/heroes.js` seeds:

```javascript
exports.seed = function(knex, Promise) {
  return knex('Heroes').del()
    .then(function () {
      return knex('Heroes').insert([
        {id: 1, firstName: 'Clark', lastName: 'Kent', heroName: 'Superman', enemyId: 2},
        {id: 2, firstName: 'Bruce', lastName: 'Wayne', heroName: 'Batman', enemyId: 1},
        {id: 3, firstName: 'Peter', lastName: 'Parker', heroName: 'Spiderman'},
        {id: 4, firstName: 'Susan', lastName: 'Storm-Richards', heroName: 'Invisible Woman'},
      ]);
    });
};
```

* Run these migrations: `yarn knex migrate:latest && yarn knex seed:run`
* In our business layer, modify `api/business/hero.js` this way:

```diff
class Hero {
  id: number;
  firstName: string;
  lastName: string;
+  heroName: string;
+  enemyId: number;

  constructor(data) {
    this.id = data.id;
    this.firstName = data.firstName;
    this.lastName = data.lastName;
+    this.heroName = data.heroName;
+    this.enemyId = data.enemyId;
  }
```

* In our API, in our presentation layer, modify our `api/presentation/schema.js`:

```diff
const typeDefs = [`
  type Hero {
    id: Int
    firstName: String
    lastName: String
+    heroName: String
+    enemy: Hero
  }
...
`];

const resolvers = {
  Query: {
...
  },
+  Hero: {
+    enemy: async (hero, args, ctx) => Hero.load(ctx, {id: hero.enemyId}),
+  },
}
```

> **CHECK 1**: Using graphiql interface with the following query:
>
> ```
> {
>   hero(id:1) {
>     id
>     firstName
>     lastName
>     heroName
>     enemy {
>       heroName
>     }
>   }
> }
> ```
>
> ...should return Clark Kent with its heroName and its enemy: Batman.

### Push your API to the next level: use caching with Dataloader (\~6min)

> Trying to query heroes and their enemies'heroName will show up a N+1 problem. Indeed, our API make 5 round-trips to our database! Try yourself:
>
> ```javascript
> {
>     "query": "{heroes { id firstName lastName heroName enemy { heroName } }}"
> }
> ```
>
> We can reduce these calls adding caching to our business layer

* Install `Dataloader`: `cd api && yarn add dataloader`
* Add a `getLoaders` method to our `api/business/hero.js` file in our business layer:

```javascript
import DataLoader from 'dataloader';


class Hero {
  //...

  static getLoaders() {
    const getById = new DataLoader(ids => HeroDB.getByIds(ids));
    const primeLoaders = (heroes) => {
      heroes.forEach(hero =>
        getById.clear(hero.id).prime(hero.id, hero))
      ;
    };
    return { getById, primeLoaders };
  }
  //...
}
```

* In our `api/index.js` file, add a new dataloader to our context for each query on `/api` route:

```diff
+import Hero from './business/hero';

router.post(
  '/api',
  graphqlKoa(async ctx => {
    return {
      schema: schema,
+      context: {
+        dataLoaders: {
+          hero: Hero.getLoaders(),
+        }
+      },
      debug: true,
    };
  })
);
```

* Back in our `api/business/hero.js` business layer file, modify `load` and `loadAll` methods to use our dataloader:

```diff
  static async load(ctx, args) {
+    const data = await ctx.dataLoaders.hero.getById.load(args.id);
    if (!data) return null;

    return new Hero(data);
  }

  static async loadAll(ctx, args) {
    const data = await HeroDB.getAll();
+    ctx.dataLoaders.hero.primeLoaders(data);

    return data.map(row => new Hero(row));
  }
```

* Protect `loader.load()` function call if no argument is supplied:

```diff
  static async load(ctx, args) {
+    if (!args.id) return null;
    const data = await ctx.dataLoaders.hero.getById.load(args.id);
    if (!data) return null;

    return new Hero(data);
  }
```

> **CHECK 1**: Using graphiql interface with the following query:
>
> ```
> {
>   heroes {
>     id
>     firstName
>     lastName
>     heroName
>     enemy {
>       heroName
>     }
>   }
> }
> ```
>
> ...should return all heroes and their enemies and your terminal should prompt only one request to the DB.
>
> **CHECK 2**: Using graphiql interface with the following query:
>
> ```
> {
>   h1: hero(id:1) {
>     id
>     firstName
>     lastName
>     heroName
>     enemy {
>       heroName
>     }
>   }
>   h2: hero(id:2) {
>     id
>     firstName
>     lastName
>     heroName
>     enemy {
>       heroName
>     }
>   }
> }
> ```
>
> ...should return Clark Kent and Bruce Wayne; and only one *SELECT* call should have beeen made to our DB.

### Add access control to our API (\~5min)

> This is a very simple example, for a more advanced solution, prefer using [Koa Jwt](https://github.com/koajs/jwt).

* In a new `api/utils.js` file, add these two methods to parse Authorization header and verify token:

```javascript
export const parseAuthorizationHeader = (req) => {
  const header = req.headers.authorization;

  if (typeof header === 'undefined' || header === 'null') {
    return null;
  }

  const [, scheme, token] = (/(\w+) ([\w.-]+)/g).exec(header);

  return token;
};

// Not production-ready: this is a simple example for the tutorial
export const verifyToken = token => new Promise((resolve, reject) => {
  if (token !== 'authorized') {
    const error = new Error('UNAUTHORIZED');
    error.code = 401;
    reject(error);
  }
  resolve();
});
```

* In our `api/index.js` file, parse authorization header and pass it to our context:

```diff
+import { parseAuthorizationHeader } from './utils';

router.post(
  '/api',
  graphqlKoa(async ctx => {
    return {
      schema: schema,
      context: {
+        authToken: parseAuthorizationHeader(ctx.req),
        dataLoaders: {
          hero: Hero.getLoaders(),
        }
      },
      debug: true,
    };
  })
);
```

* In our business layer, modify `api/business/hero.js`:

```diff
+import { verifyToken } from '../utils';


  static async load(ctx, args) {
+    await verifyToken(ctx.authToken);

  static async loadAll(ctx, args) {
+    await verifyToken(ctx.authToken);
```

> **CHECK 1**: In **Postman**, making a *POST* request to `localhost:3000/api` which content-type is *JSON(application/json)* with the following raw body:
>
> ```javascript
> {
>     "query": "{hero(id:1) { id firstName lastName heroName }}"
> }
> ```
>
> ...should return `UNAUTHORIZED`.
>
> **CHECK 2**: In **Postman**, making a *POST* request to `localhost:3000/api` which content-type is *JSON(application/json)* with the following raw body:
>
> ```javascript
> {
>     "query": "{heroes { id firstName lastName heroName }}"
> }
> ```
>
> ...should return `UNAUTHORIZED`.
>
> **CHECK 3**: In **Postman**, making a *POST* request to `localhost:3000/api` which content-type is *JSON(application/json)* and *Authorization Header* is `Bearer authorized` with the following raw body:
>
> ```javascript
> {
>     "query": "{hero(id:1) { firstName lastName }}"
> }
> ```
>
> ...should return Clark Kent.
>
> ![](/files/-LCDisyyOAWzmOy-jgVr)

### Troubleshooting: Accessing data by id in the correct order (\~5min)

> You should notice that in **Postman** making a *POST* request to `localhost:3000/api` which content-type is *JSON(application/json)* and *Authorization Header* is `Bearer authorized` with the following raw body:
>
> ```javascript
> {
>     "query": "{h1: hero(id:1) { id firstName lastName heroName enemy { heroName } } h2: hero(id:2) { id firstName lastName heroName enemy { heroName } }}"
> }
> ```
>
> ...returns the same than the following request (ids switched):
>
> ```javascript
> {
>     "query": "{h1: hero(id:2) { id firstName lastName heroName enemy { heroName } } h2: hero(id:1) { id firstName lastName heroName enemy { heroName } }}"
> }
> ```
>
> This is due to our DB query: `select * from "Heroes" where "id" in (1, 2)` return the same result than: `select * from "Heroes" where "id" in (2, 1)`.

* In `utils.js`, add the following method:

```javascript
export const orderByArgIdsOrder = ids => ("array_position(string_to_array(?, ',')::integer[], id)", ids.join(','));
```

* In our db layer, modify `api/db/queryBuilders/hero.js` like this:

```diff
+import { orderByArgIdsOrder } from '../../utils';

class Hero {

  static async getByIds(ids: Array<number>): Promise<Array<CostDBType>> {
    return db
    .select()
    .table('Heroes')
+    .whereIn('id', ids)
+    .orderByRaw(orderByArgIdsOrder(ids));
  }
```

> **CHECK 1**: In **Postman**, making a *POST* request to `localhost:3000/api` which content-type is *JSON(application/json)* and *Authorization Header* is `Bearer authorized` with the following raw body:
>
> ```javascript
> {
>     "query": "{h1: hero(id:2) { heroName } h2: hero(id:1) { heroName }}""
> }
> ```
>
> ...should return Batman (as `h1`) then Superman (as `h2`).

## Next steps

* Add [Koa Jwt](https://github.com/koajs/jwt)
* Add graphiQL with authorization header (get inspired by [BAM API](https://github.com/bamlab/bam-api))


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://bamtech.gitbook.io/dev-standards/backend/graphql-js/getting-started-with-apollo-server-dataloader-knex.mo.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
