# @theorchard/dataloader-cypher

A standard DataLoader for working with Neo4j. The returned value is typed as `unknown`, you should validate the response data before using it. Consider using `@theorchard/dataloader-zod` for that purpose.

## Usage

To construct a `CypherDataLoader` you need to provide a Neo4j Driver instance, a `BookmarkManager`, and a Cypher script that is designed for use with the `CypherDataLoader`. The Cypher script will be called with a parameter `$keys` which is an array of keys. The script should return exactly one row for each key, and each row should have two columns where the first is the key for that row and the second is the value to return.

A basic example:

```ts
const productByIdCypher = `
    UNWIND $keys AS key
    OPTIONAL MATCH (p:Orchard:Product { id: key })
    RETURN key, p.name
`;

const productByIdDataLoader = new CypherDataLoader<number>(
    neo4jDriver,
    bookmarkManager,
    databaseName,
    productByIdCypher
);
```

### BookmarkManager

The bookmark manager is used to enable multiple Cypher queries in a single request to maintain causal consistency. For more information see [the Neo4j documentation](https://neo4j.com/docs/javascript-manual/current/bookmarks/).

If you do not need causal consistency, you may pass `null` explicitly, but this is likely to be a rare use case.

### Compound keys

For compound/complex keys, you can pass them directly, but you may want to use a `cacheKeyFn` to properly deduplicate requests.
For example:

```ts
const productParticipationsByIdAndRoleCypher = `
    UNWIND $keys AS key
    OPTIONAL MATCH (p:Orchard:Product { id: key })
    OPTIONAL MATCH (p)<-[:PARTICIPATED_IN { participatedAs: 'performer' }]-(lp)
    RETURN key, collect(lp.uuid)
`;

const productParticipationsByIdAndRoleDataLoader = new CypherDataLoader<{
    id: number;
    participatedAs: string;
}>(neo4jDriver, productParticipationsByIdAndRoleCypher, {
    cacheKeyFn: ({ id, participatedAs }) => `${id}:${participatedAs}`,
});

// To call
productParticipationsByIdAndRoleDataLoader.load({
    id: 100,
    participatedAs: 'performer',
});
```

### Parameters

If you need to pass parameters that are not related you can pass a `parameters` key to the options:

```ts
const dataLoader = new CypherDataLoader(neo4jDriver, bookmarkManager, cypher, {
    parameters: {
        notForDistribution: 'N',
    },
});
```

You can then use these parameters in your cypher:

```cypher
UNWIND $keys AS key
OPTIONAL MATCH (p:Product:Orchard { id: key })
WHERE p.notForDistribution = $parameters.notForDistribution
RETURN id, p
```

### Returning multiple columns for one key

If you want to return multiple datapoints at once, you cannot return them as separate columns:

```Cypher
UNWIND $keys AS key
OPTIONAL MATCH (p:Orchard:Product { id: key })
RETURN key, p.title, p.upc
```

Doing so will yield an error.

Instead, you should construct an object to contain the data:

```
UNWIND $keys AS key
OPTIONAL MATCH (p:Orchard:Product { id: key })
RETURN key, CASE WHEN p IS null THEN null ELSE { name: p.name, upc: p.upc } END
```

### Returning multiple values for one key

If you need to return a variable number of data for one key, you should return an array, for example, to list the tracks for a product:

```
UNWIND $keys AS key
OPTIONAL MATCH (p:Orchard:Product { id: key })-[:INCLUDES]->(t:Track)
RETURN key, collect(t.id)
```

### Usage of `MATCH` may result in an error on failure to match

The `CypherDataLoader` requires you return a value for every key, so if you do something like the following with `MATCH` that discards the row on failure to match then you may end up with an error when the speific key doesn't exist.

```Cypher
UNWIND $keys AS key
MATCH (p:Orchard:Product { id: key })
RETURN key, p.title
```

The correct cypher would be to handle this with `OPTIONAL MATCH`:

```Cypher
UNWIND $keys AS key
OPTIONAL MATCH (p:Orchard:Product { id: key })
RETURN key, p.title
```

Note that for more complicated return types it may make more sense to use `CASE ... WHEN ... THEN ... ELSE ... END`, e.g.:

Instead of:

```
UNWIND $keys AS key
OPTIONAL MATCH (p:Orchard:Product { id: key })
RETURN key, { name: p.name, upc: p.upc }
```

Which will return a value like `{ name: null, upc: null }`
Use this instead:

```
UNWIND $keys AS key
OPTIONAL MATCH (p:Orchard:Product { id: key })
RETURN key, CASE WHEN p IS null THEN null ELSE { name: p.name, upc: p.upc } END
```

Which will return a value like `null`.

## Comparison to previous versions

A few GraphQL repositories have an early version of the CypherDataLoader, and there are a couple of differences to consider when porting between these implementations:

### `UNWIND` / `$ids` / `id`, `$keys`

The version in those repositories adds the `UNWIND` statement to the front of your cypher, and also uses the parameter name `$ids` and variable name `id`. In this version, the batch keys are passed in as the parameter `$keys` and you must handle the array yourself. This means the Cypher is not compatible between versions.

### Behavior when no row is returned for a key

The version in those repositories defaults to returning `null` when no row is returned for a specific key. In this version, failing to return the row will result in an error. See the usage section for more details.
