# GraphQL Fields Projection

- [Why using this?](#why-using-this)
- [Install](#install)
- [How to](#how-to)
  - [Example 1: simplest usecase](#example-1-simplest-usecase)
  - [Example 2: Get more fields](#example-2-get-more-fields)
  - [Example 3: Get child path](#example-3-get-child-path)
  - [Example 4: returnTypes](#example-4-returntypes)
  - [Example 5: Using with Dataloader](#example-5-using-with-dataloader)

This library create field projection from GraphQL query

> NOTE: Since version 1.1.0 the default [returnType](#example-4-returntypes) is **string**

## Why using this?

There are many libraries can do the same function. However, starting in MongoDB 4.4, the [Path Collision Restrictions](mongodb.com/docs/v4.4/release-notes/4.4-compatibility/#path-collision-restrictions) are introduced . And it is illegal to project an embedded document with any of the embedded document's fields:

```js
db.inventory.find({}, { size: 1, "size.uom": 1 }); // Invalid starting in 4.4
```

- And this library is created to remove the collision.
- This library can work with [dataloader](#example-5-using-with-dataloader) also

## Install

```sh
npm install graphql-fields-projection
```

## How to

Please see the following examples

### Example 1: simplest usecase

Given the following query

```graphql
query user {
  user(id: 123) {
    id
    address
    info {
      firstName
      lastName
    }
  }
}
```

```js
const { createSelectedFields } = require('graphql-fields-projection');

resolve(parent, args, context, info){
  const selectedFields = createSelectedFields(info); // 'id address info.firstName info.lastName'
}
```

### Example 2: Get more fields

Given the following query

```graphql
query user {
  user(id: 123) {
    id
    address
    info {
      firstName
      lastName
    }
  }
}
```

```js
const { createSelectedFields } = require('graphql-fields-projection');

resolve(parent, args, context, info){
  // Now you like to get more fields for further resolve: `timezone`, and `info` object
  const selectedFields = createSelectedFields(info, { additionalFields: ['info', 'address', 'timezone'] }); // 'id info timezone'
}
```

### Example 3: Get child path

Given the following query

```graphql
query purchase {
  purchase(id: 123) {
    id
    buyer {
      id
      address
      info {
        firstName
        lastName
      }
    }
    product {
      id
      # ...others
    }
  }
}
```

```js
const { createSelectedFields } = require('graphql-fields-projection');

resolve(parent, args, context, info){
  // Now you like to get selected fields of `buyer`
  const selectedFields = createSelectedFields(info, { path: 'buyer' }); // 'id address info.firstName info.lastName'

  // OR with additionalFields
const selectedFields2 = createSelectedFields(info, {
  path: 'buyer', additionalFields: ['info', 'address', 'timezone'],
}); // 'id info timezone'
}
```

### Example 4: returnTypes

> NOTE: Since version 1.1.0 the default [returnType](#example-4-returntypes) is **string**

By the default the return result will be an **string** of projected fields. But you can also get the **array** or **object**

```graphql
query user {
  user(id: 123) {
    id
    address
    info {
      firstName
      lastName
    }
  }
}
```

```js
const { createSelectedFields } = require('graphql-fields-projection');

resolve(parent, args, context, info){
  const resultString = createSelectedFields(info); // 'id address info.firstName info.lastName'
  const resultString = createSelectedFields(info, { returnType : 'string' } ); // 'id address info.firstName info.lastName'
  const resultArray2 = createSelectedFields(info, { returnType : 'array' }); // [ 'id', 'address', 'info.firstName', 'info.lastName' ]
  const resultObject = createSelectedFields(info, { returnType : 'object' }); // { id: 1, address: 1, 'info.firstName': 1, 'info.lastName': 1 }
}
```

### Example 5: Using with [Dataloader](https://github.com/graphql/dataloader)

```graphql
query purchase {
  purchase(id: 123) {
    id
    buyer {
      id
      address
      info {
        firstName
        lastName
      }
    }
    products {
      id
      sku
      name
      price
    }
  }
}
```

```js
const { createSelectedFields, createMergedSelectedFields } = require('graphql-fields-projection');

// This is an example with Apollo Federation, but you can run with any resolvers
__resolveReference(parent, context, info) {
  const { loaders } = context;
  const selectedFields = createSelectedFields(info, { returnType : 'array' });
  return loaders.product.load(JSON.stringify({ id: parent.id, selectedFields }));
}

// The implementation of `loaders.product()`
async function batchProducts(keys) {
  const { ids: productIds, selectedFields } = createMergedSelectedFields(keys);
  const products = await Product.find({ _id: { $in: productIds } })
    .select(selectedFields)
    .lean();

  // Don't forget mapping results
  // ...

  return products;
}
```

The function `createMergedSelectedFields()` supports the following options:

- [additionalFields](#example-2-get-more-fields)
- [returnType](#example-4-returntypes)
