Skip to content

Commit

Permalink
feature(gatsby-node): Add support for traversing nested arrays
Browse files Browse the repository at this point in the history
Before, if the objects holding your remote images were in an array, the `lodash .get` method could not traverse those arrays to target the correct objects.

These changes add opt-in support for this functionality by extending `lodash .get`'s syntax to indicate where the arrays need to be traversed. Adding an array literal in the `path` option on the node holding the array, like `myArrayNode[].imageUrl`, triggers the logic to create image nodes for each object in `myArrayNode`.

This does diverge from `lodash .get` syntax, but here were the considerations:
 * If we didn't have some explicit indication in the path, like `myArrayNode.imageUrl`, you would still have a value that was invalid to pass to `.get` as-is, but the code would internally have to hand-hold each node to see if it was an object or array before proceeding, essentially meaning you would have to reimplement `.get` and its safeguards.
 * The `[]` suffix is a pattern already found in other tools, like `TypeScript`, to indicate a node is an array, so it should be relatively recognizable to users
 * When no `[]` is supplied, the current logic reverts to the existing functionality, delegating object traversal entirely to `.get`

There has also been interest to support [leaf nodes which contain an array of urls](graysonhicks#8). These changes don't add this support, but it should be possible to achieve with a bit of rework without having to rethink the entire approach.
  • Loading branch information
bensbigolbeard committed Jul 22, 2019
1 parent 53b590a commit 50331bd
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 21 deletions.
56 changes: 54 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ module.exports = {
// This is generally the camelcased version of the word
// after the 'all' in GraphQL ie. allMyImages type is myImages
nodeType: 'myNodes',
// String that is path to the image you want to use, relative to the node.
// This uses lodash .get, see docs for accepted formats [here](https://lodash.com/docs/4.17.11#get).
// For simple object traversal, this is the string path to the image you
// want to use, relative to the node.
// This uses lodash .get, see [docs for accepted formats here](https://lodash.com/docs/4.17.11#get).
// For traversing objects with arrays at given depths, see [how to handle arrays below](#traversing-objects-with-arrays)
imagePath: 'path.to.image',
// ** ALL OPTIONAL BELOW HERE: **
//Name you want to give new image field on the node.
Expand Down Expand Up @@ -129,3 +131,53 @@ allMyNodes {
}
}
```
### Traversing objects with arrays

Since some GraphQL APIs will send back objects with nested arrays where your target data lives, `gatsby-plugin-remote-images` also supports traversing objects that have arrays at arbitrary depths. To opt in to this feature, add an array literal, `[]`, to the end of the node you want to indicate is an array.

##### Note: arrays of image urls at leaf nodes are currently not supported

Given an object structure like this:
```javascript
allMyNodes {
nodes: [
{
imageUrl: 'https://...'
},
...
]
}
```

To get the images and make them avabilable for the above example, your config should look like this:
```javascript
module.exports = {
plugins: [
{
resolve: `gatsby-plugin-remote-images`,
options: {
nodeType: 'myNodes',
imagePath: 'nodes[].imageUrl',
},
},
]
}
```

Now, if we query `allMyNodes` we can query as we would any gatsby-image node:

```graphql
allMyNodes {
nodes {
localImage {
childImageSharp {
fluid(maxWidth: 400, maxHeight: 250) {
...GatsbyImageSharpFluid
}
}
}
}
}
```

##### Note: While `lodash .get` doesn't natively support this syntax, it is still used to traverse the object structure, so [the documentation for `.get`](https://lodash.com/docs/4.17.11#get) still applies in full.
93 changes: 74 additions & 19 deletions src/gatsby-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,33 +13,88 @@ exports.onCreateNode = async (
auth = {},
ext = null,
} = options
const createImageNodeOptions = {
store,
cache,
createNode,
createNodeId,
auth,
ext,
name,
}

let fileNode
// Check if any part of the path indicates the node is an array and splits at those indicators
let imagePathSegments = []
if (imagePath.includes("[].")) {
imagePathSegments = imagePath.split("[].")
}
if (node.internal.type === nodeType) {

const url = ext ? `${get(node, imagePath)}${ext}` : get(node, imagePath)
if (!url) {
return
if (imagePathSegments.length) {
await createImageNodesInArrays(imagePathSegments[0], node, { imagePathSegments, ...createImageNodeOptions })
} else {
const url = getPath(node, path, ext)
await createImageNode(url, node, createImageNodeOptions)
}
}
}

try {
fileNode = await createRemoteFileNode({
url,
parentNodeId: node.id,
store,
cache,
createNode,
createNodeId,
auth,
ext,
})
} catch (e) {
console.error('gatsby-plugin-remote-images ERROR:', e)
}
// Returns value from path, adding extension when supplied
function getPath(node, path, ext = null) {
const value = get(node, path)

return ext ? value + ext : value
}

// Creates a file node and associates the parent node to its new child
async function createImageNode(url, node, options) {
const { name, ...restOfOptions } = options
let fileNode

if (!url) {
return
}

try {
fileNode = await createRemoteFileNode({
...restOfOptions,
url,
parentNodeId: node.id,
})
} catch (e) {
console.error('gatsby-plugin-remote-images ERROR:', e)
}
// Adds a field `localImage` or custom name to the node
// ___NODE appendix tells Gatsby that this field will link to another node
if (fileNode) {
node[`${name}___NODE`] = fileNode.id
}
}

// Recursively traverses objects/arrays at each path part, then operates on targeted leaf node
async function createImageNodesInArrays(path, node, options) {
if (!path || !node) {
return
}
const { imagePathSegments, ext } = options
const pathIndex = imagePathSegments.indexOf(path),
isPathToLeafNode = pathIndex === imagePathSegments.length - 1,
nextValue = getPath(node, path, isPathToLeafNode ? ext : null)

// grab the parent of the leaf node, if it's not the current value of `node` already
let nextNode = node
if (isPathToLeafNode && path.includes('.')) {
const pathToLastParent = path
.split('.')
.slice(0, -1)
.join('.')
nextNode = get(node, pathToLastParent)
}
return Array.isArray(nextValue)
// Recursively call function with next path segment for each array element
? Promise.all(
nextValue.map(item => createImageNodesInArrays(imagePathSegments[pathIndex + 1], item, options))
)
// otherwise, handle leaf node
: createImageNode(nextValue, nextNode, options)
}

0 comments on commit 50331bd

Please sign in to comment.