# @keystone-alpha/mongo-join-builder

> Programatically build JOIN-like Mongo aggregations based on arbitrarily nested relationships.

Latest version **4.0.0** (published 2019-10-15) · MIT license · 0 weekly downloads

> **Deprecated.** This package is deprecated.

## Install

```sh
npm install @keystone-alpha/mongo-join-builder
pnpm add @keystone-alpha/mongo-join-builder
yarn add @keystone-alpha/mongo-join-builder
bun add @keystone-alpha/mongo-join-builder
```

## Health

**Score 10/100 (F)** — status: deprecated.

Negative: deprecated.

## Facts

| | |
|---|---|
| Version | 4.0.0 |
| Published | 2019-10-15 |
| First published | 2019-03-07 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | none |
| Module format | CommonJS |
| Node | >=8.4.0 |
| Dependencies | 3 |
| Unpacked size | 30.8 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Author | The KeystoneJS Development Team |
| Maintainers | dominikwilkowski, jedwatson, jesstelford, mitchellhamilton, molomby, timl |

## Links

- npm: https://www.npmjs.com/package/@keystone-alpha/mongo-join-builder
- npm.io page: https://npm.io/package/@keystone-alpha/mongo-join-builder

## Dependencies (3)

- [cuid](https://npm.io/package/cuid.md) ^2.1.6
- [lodash.omitby](https://npm.io/package/lodash.omitby.md) ^4.6.0
- [@keystone-alpha/utils](https://npm.io/package/@keystone-alpha/utils.md) ^3.2.0

## Recent versions

- 4.0.0 (latest) — 2019-10-15
- 3.0.1 — 2019-10-02
- 3.0.0 — 2019-10-02
- 2.0.3 — 2019-07-19
- 2.0.2 — 2019-06-14
- 2.0.1 — 2019-05-13
- 2.0.0 — 2019-04-09
- 1.0.3 — 2019-03-23
- 1.0.2 — 2019-03-18
- 1.0.1 — 2019-03-12
- 1.0.0 — 2019-03-07

## README

<!--[meta]
title: Mongo Join Builder
[meta]-->

# Mongo Join Builder

Perform JOINs in Mongo with a simplified query syntax.

## Examples

Given this dataset:

```javascript
db.items.insert([
  { _id: 1, name: 'almonds', stock: [1, 3] },
  { _id: 2, name: 'pecans', stock: [2] },
  { _id: 3, name: 'cookies', stock: [4, 5] },
]);

// Only item 2 & 3 have stock. Item 2 only has stock in warehouse A.Item 3 used
// to have stock in warehouse B, but now only has stock in warehouse A.
db.warehouses.insert([
  { _id: 1, item: 1, warehouse: 'A', stock: 0 },
  { _id: 2, item: 2, warehouse: 'A', stock: 80 },
  { _id: 3, item: 1, warehouse: 'B', stock: 0 },
  { _id: 4, item: 3, warehouse: 'B', stock: 0 },
  { _id: 5, item: 3, warehouse: 'A', stock: 40 },
]);

db.orders.insert([
  { _id: 1, items: [1], price: 12, ordered: 2, fulfilled: false },
  { _id: 2, items: [2], price: 20, ordered: 1, fulfilled: false },
  { _id: 3, items: [3, 1], price: 10, ordered: 60, fulfilled: false },
  { _id: 4, items: [1], price: 10, ordered: 60, fulfilled: true },
]);
```

### Find unfulfilled orders with items containing 'a' in the name

We can write this query like so:

```javascript
{
  fulfilled: false,
  items_every: {
    name_contains: 'a'
  }
}
```

Can also be written with an explicit `AND`:

```javascript
{
  AND: [
    { fulfilled: false },
    {
      items_every: {
        name_contains: 'a',
      },
    },
  ];
}
```

We'd expect the following results:

```javascript
[
  {
    id: 1,
    items: [
      {
        id: 1,
        name: 'almonds',
        stock: [{ id: 1, warehouse: 'A', stock: 0 }, { id: 3, warehouse: 'B', stock: 0 }],
      },
    ],
  },
  {
    id: 2,
    items: [
      {
        id: 2,
        name: 'pecans',
        stock: [{ id: 2, warehouse: 'A', stock: 80 }],
      },
    ],
  },
];
```

The raw MongoDB query to do this is complex.

<details>
<summary>See the MongoDB query</summary>

```js
db.orders.aggregate([
  {
    $match: {
      $and: [{ fulfilled: { $eq: false } }],
    },
  },
  {
    $lookup: {
      from: 'items',
      as: 'abc123_items',
      let: {
        abc123_items_items: '$items',
      },
      pipeline: [
        {
          $match: {
            $and: [{ name: { $regex: /a/ } }, { $expr: { $in: ['$_id', '$$abc123_items_items'] } }],
          },
        },
        {
          $addFields: {
            id: '$_id',
          },
        },
      ],
    },
  },
  {
    $addFields: {
      abc123_items_every: { $eq: [{ $size: '$abc123_items' }, { $size: '$items' }] },
      abc123_items_none: { $eq: [{ $size: '$abc123_items' }, 0] },
      abc123_items_some: {
        $and: [
          { $gt: [{ $size: '$abc123_items' }, 0] },
          { $lte: [{ $size: '$abc123_items' }, { $size: '$items' }] },
        ],
      },
    },
  },
  {
    $match: {
      $and: [{ abc123_items_every: { $eq: true } }],
    },
  },
  {
    $addFields: {
      id: '$_id',
    },
  },
]);
```

</details>

Instead, we can use `mongo-join-builder`!

_NOTE: This example is incomplete, and only for documentation purposes. See the examples directory for complete examples._

```javascript
const { mongoJoinBuilder } = require('@keystone-alpha/mongo-join-builder');
const database = require('./my-mongodb-connection');

const builder = mongoJoinBuilder({
  tokenizer: {
    // executed for simple query components (eg; 'fulfilled: false' / name: 'a')
    simple: (qyer, key, path) => ({
      // ... mongo specific syntax for filtering items
      [key]: { $eq: query[key] },
    }),

    // executed for complex query components (eg; items: { ... })
    relationship: (query, key, path, uid) => {
      return {
        from: 'items', // the collection name to join with
        field: 'items', // The field on the 'orders' collection
        // A mutation to run on the data post-join. Useful for merging joined
        // data back into the original object.
        // Executed on a depth-first basis for nested relationships.
        postQueryMutation: (parentObj, keyOfRelationship, rootObj, pathToParent) => {
          // For this example, we want the joined items to overwrite the array
          //of IDs
          return {
            ...parentObj,
            items: parentObj[keyOfRelationship],
          };
        },
        // The conditions under which an item from the 'orders' collection is
        // considered a match and included in the end result
        // All the keys on an 'order' are available, plus 3 special keys:
        // 1) <uid>_<field>_every - is `true` when every joined item matches the
        //    query
        // 2) <uid>_<field>_some - is `true` when some joined item matches the
        //    query
        // 3) <uid>_<field>_none - is `true` when none of the joined items match
        //    the query
        match: { [`${uid}_items_every`]: true },
        // Flag this is a to-many relationship
        many: true,
      };
    },
  },
});

const query = {
  fulfilled: false,
  items: {
    name: 'a',
  },
};

const result = await builder(query, database, 'orders');
```

## Limitations

Due to [a limitation in `mongo@<4.x`](https://jira.mongodb.org/browse/SERVER-22781), relationship
queries will fail silently with 0 results unless IDs are stored as `ObjectId`s.

---
_Source: https://npm.io/package/@keystone-alpha/mongo-join-builder · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
