├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── gatsby-node.js
├── lib
├── create-and-configure-axios.js
├── create-common-interface.js
├── fetch-and-validate-sitemap.js
├── handle-foreign-key-fields.js
├── load-remote-files.js
├── load-umbraco-nodes.js
├── typeRegistry.js
└── validate-and-prep-options.js
├── package-lock.json
├── package.json
└── util
├── asyncForEach.js
├── formatMessage.js
├── getGatsbyReferenceKey.js
└── typecheck.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Dependency directories
9 | node_modules/
10 |
11 | # Optional npm cache directory
12 | .npm
13 |
14 | # Mac files
15 | .DS_Store
16 |
17 | # Yarn
18 | yarn-error.log
19 | .pnp/
20 | .pnp.js
21 |
22 | # Yarn Integrity file
23 | .yarn-integrity
24 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "endOfLine": "lf",
3 | "semi": false,
4 | "singleQuote": false,
5 | "tabWidth": 2,
6 | "trailingComma": "es5"
7 | }
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 We are you
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Gatsby source Umbraco · [](https://github.com/weareyou/gatsby-source-umbraco/releases) [](#license)  [](https://github.com/weareyou/gatsby-source-umbraco/issues) 
2 |
3 | A source plugin for pulling content and media from [Umbraco](https://umbraco.com/) into [Gatsby](https://www.gatsbyjs.org/).
4 |
5 | This plugin is meant to be used when building a custom API on top of Umbraco—it requires the API to follow some [minimal conventions](#api-design). The plugin works great in combination with something like [Umbraco HeadRest](https://github.com/mattbrailsford/umbraco-headrest).
6 |
7 | ## Contents
8 |
9 | - [Basic usage](#basic-usage)
10 | - [Installation](#installation)
11 | - [Advanced usage](#advanced-usage)
12 | - [Overlapping interface](#overlapping-interface)
13 | - [Loading remote files](#loading-remote-files)
14 | - [Relationships between nodes](#relationships-between-nodes)
15 | - [Namespacing types](#namespacing-types)
16 | - [Verbose logging](#verbose-logging)
17 | - [How it works](#how-it-works)
18 | - [Loading content](#loading-content)
19 | - [Loading remote files](#loading-remote-files-1)
20 | - [Relationships between nodes](#relationships-between-nodes-1)
21 | - [Node metadata](#node-metadata)
22 | - [Overlapping interface](#overlapping-interface-1)
23 | - [Configuration](#configuration)
24 | - [API design](#api-design)
25 | - [Sitemap](#sitemap)
26 | - [Content items](#content-items)
27 | - [Limitations](#limitations)
28 | - [Type discovery](#type-discovery)
29 | - [Multilingual](#multilingual)
30 | - [Protected API](#protected-api)
31 | - [Advanced usage of rich text editor](#advanced-usage-of-rich-text-editor)
32 | - [Recipes](#recipes)
33 | - [Creating pages for content items](#creating-pages-for-content-items)
34 | - [Tips](#tips)
35 | - [Absolute Media URL provider](#absolute-media-url-provider)
36 | - [Group content items using GraphQL interfaces](#group-content-items-using-graphql-interfaces)
37 | - [License](#license)
38 |
39 | ## Basic usage
40 |
41 | Like every [Gatsby plugin](https://www.gatsbyjs.org/docs/plugins/), the plugin has to be added and configured in the `plugins` array of the `gatsby-config.js` file. The source-umbraco plugin has only one required option: the URL of the Umbraco API (`url`). It is recommended to store this Umbraco API URL in an [environment variable](https://www.gatsbyjs.org/docs/environment-variables/).
42 |
43 | ```js
44 | /* in gatsby-config.js */
45 | module.exports = {
46 | plugins: [
47 | {
48 | resolve: "gatsby-source-umbraco",
49 | options: {
50 | url: process.env.UMBRACO_API_URL,
51 | },
52 | },
53 | ],
54 | }
55 | ```
56 |
57 | Provided that the API follows the [conventions](#api-design) required by the plugin, this will load the available content from the API and make it accessible to query via [Gatsby's GraphQL endpoint](https://www.gatsbyjs.org/docs/graphql/). You can now query your data by document type. Pro tip—use the [_GraphiQL_ interface]( https://www.gatsbyjs.org/docs/running-queries-with-graphiql/ ) available when running `gatsby develop` to explore your data and schema.
58 |
59 | Consider the following example query. It assumes you have a document type `Textpage` with `title` and `subtitle` fields.
60 |
61 | ```graphql
62 | query {
63 | allTextpage {
64 | nodes {
65 | title
66 | subtitle
67 | }
68 | }
69 | }
70 | ```
71 |
72 | Using this queries like this, you can [programmatically create pages](https://www.gatsbyjs.org/docs/creating-and-modifying-pages/) based on your data.
73 |
74 | ## Installation
75 |
76 | The plugin is currently ***not*** available to install via a package manager. In Gatsby it is possible to load a plugin from a local directory. (See: [loading plugins from your local plugins folder](https://www.gatsbyjs.org/docs/loading-plugins-from-your-local-plugins-folder/).) You could pull this plugin's source code into a `gatsby-source-umbraco/` folder in the `plugins/` folder of your Gatsby project.
77 |
78 | Perhaps make use of a [Git submodule](https://github.blog/2016-02-01-working-with-submodules/) in place of a proper package manager—for now.
79 |
80 | ## Advanced usage
81 |
82 | Aside from simple data loading the plugin has several features to improve the integration between Umbraco and Gatsby. The following explains these features from an "implementation" point of view. For a more detailed explanation see: [how it works](#how-it-works).
83 |
84 | ### Overlapping interface
85 |
86 | The plugin bases the [GraphQL types](https://graphql.org/learn/schema/) for the content items on their [document type](https://our.umbraco.com/documentation/tutorials/creating-basic-site/document-types/). The plugin also makes all types implement a common [GraphQL interface](https://graphql.org/learn/schema/#interfaces), this allows you to query all content items from Umbraco and access their common fields on a single type. Note that you need to configure the common fields (see [configuration](#configuration)) since this is dependent on the API implementation.
87 |
88 | Consider the following example, it assumes that the API outputs `name` and `slug` fields for all content items. On the Umbraco side of things, this data could come from the `Name` and `Url` properties of `IPublishedContent`. The plugin configuration in the example specifies that the interface (named `"UmbracoNode"` by default) has these two fields.
89 |
90 | ```js
91 | {
92 | url: process.env.UMBRACO_API_URL,
93 | commonInterface: {
94 | fields: {
95 | name: "String",
96 | slug: "String",
97 | },
98 | },
99 | }
100 | ```
101 |
102 | The following query then selects these two fields for all the content nodes from Umbraco.
103 |
104 | ```graphql
105 | query {
106 | allUmbracoNode {
107 | nodes {
108 | name
109 | slug
110 | }
111 | }
112 | }
113 | ```
114 |
115 | ### Loading remote files
116 |
117 | The plugin can download remote files, and turn them into [`File` nodes](https://www.gatsbyjs.org/packages/gatsby-source-filesystem/). This exposes the files to be transformed by other Gatsby plugins. A great example of this is the combination of image files and [gatsby-image]( https://www.gatsbyjs.org/packages/gatsby-image/ ). Downloading remote files also removes your site's dependency on the remote server.
118 |
119 | While this is perfect for downloading and using [Umbraco media]( https://our.umbraco.com/documentation/Getting-Started/Backoffice/#media) in Gatsby, it can be used for files from any source. All you have to do is supply the URL for the file in your data. To tell the plugin that a key contains a file you have to suffix it with a special `remoteFileSuffix` (default is `___FILE` [note the 3 underscores], see [configuration](#configuration)). For example:
120 |
121 | ```json
122 | {
123 | "image___FILE": "https://source.unsplash.com/43E513RKDug"
124 | }
125 | ```
126 |
127 | The plugin will download the file and make it available as a [`File` node](https://www.gatsbyjs.org/packages/gatsby-source-filesystem/). You can query for the field without the suffix, since the plugin will remove that. Consider the following query, it selects some metadata about the file as well as it's public URL, which can be used to render the image. (Although it is recommended to use [gatsby-image]( https://www.gatsbyjs.org/packages/gatsby-image/ ) for images.)
128 |
129 | ```graphql
130 | query {
131 | someUmbracoType {
132 | image {
133 | name
134 | ext
135 | publicURL
136 | }
137 | }
138 | }
139 | ```
140 |
141 | This query would return:
142 |
143 | ```json
144 | {
145 | "data": {
146 | "someUmbracoType": {
147 | "image": {
148 | "name": "someimage",
149 | "ext": ".jpg",
150 | "publicURL": "/static/filename.jpg"
151 | }
152 | }
153 | }
154 | }
155 | ```
156 |
157 | The plugin also knows how to handle arrays of files.
158 |
159 | ```json
160 | {
161 | "reports___FILE": [
162 | "http://umbraco.local/media/zxodm2ol/report-v0.1.pdf",
163 | "http://umbraco.local/media/zxodm2ol/report-v0.2.pdf",
164 | "http://umbraco.local/media/zxodm2ol/report-v1.pdf"
165 | ]
166 | }
167 | ```
168 |
169 | These can be queried in a similar way.
170 |
171 | ```graphql
172 | query {
173 | someUmbracoType {
174 | reports {
175 | publicURL
176 | }
177 | }
178 | }
179 | ```
180 |
181 | This query would return something like:
182 |
183 | ```json
184 | {
185 | "data": {
186 | "someUmbracoType": {
187 | "reports": [
188 | { "publicURL": "/static/report-v0.1.jpg" },
189 | { "publicURL": "/static/report-v0.2.jpg" },
190 | { "publicURL": "/static/report-v1.jpg" }
191 | ]
192 | }
193 | }
194 | }
195 | ```
196 |
197 | Note that the URL to a file must be absolute, to enforce absolute URLs for all media items in Umbraco you could implement a custom `MediaUrlProvider`. (See [outbound request pipeline]( https://our.umbraco.com/documentation/reference/routing/request-pipeline/outbound-pipeline#urls ) on Our Umbraco.)
198 |
199 | ### Relationships between nodes
200 |
201 | The plugin can create [foreign key relationships]( https://www.gatsbyjs.org/docs/node-creation/#node-relationship-storage-model ) between content items. This is useful in combination with Umbraco [property editors](https://our.umbraco.com/documentation/Getting-Started/Backoffice/Property-Editors/) like the [Content Picker](https://our.umbraco.com/documentation/Getting-Started/Backoffice/Property-Editors/Built-in-Property-Editors/Content-Picker/) or [Multinode Treepicker]( https://our.umbraco.com/documentation/Getting-Started/Backoffice/Property-Editors/Built-in-Property-Editors/Multinode-Treepicker/ ). It creates this relationship based on the ID of a content item, omitting the need to output data about the related content item in multiple places.
202 |
203 | Consider the following example, it assumes you have two document types `Author` and `Article` that are set up in such a way that an article has 1 author, and an author can have 0 or more articles. To indicate that a field contains the ID of another content item that you want to link to you have to suffix it with a special `foreignKeySuffix` (default is `___ID` [note the 3 underscores], see [configuration](#configuration)). The data output for these content items might look like this:
204 |
205 | ```json
206 | /* Article */
207 | {
208 | "title": "A great little article",
209 | "author___ID": 123456
210 | }
211 |
212 | /* Author */
213 | {
214 | "name": "John Appleseed",
215 | "articles___ID": [
216 | 654321,
217 | 123654,
218 | 321456
219 | ]
220 | }
221 | ```
222 |
223 | Provided that all linked content items are imported into Gatsby by the plugin, it will link the items together. You can query for the fields without the suffix, since the plugin will remove it. Consider the following query, it finds the author and article by ID and selects some fields of the related items.
224 |
225 | ```graphql
226 | query SampleQuery($authorID: ID!, $articleID: ID!) {
227 | author(id: {eq: $authorID}) {
228 | name
229 | articles {
230 | title
231 | }
232 | }
233 | article(id: {eq: $articleID}) {
234 | title
235 | author {
236 | name
237 | }
238 | }
239 | }
240 | ```
241 |
242 | The query would return something like:
243 |
244 | ```json
245 | {
246 | "data": {
247 | "author": {
248 | "name": "John Appleseed",
249 | "articles": [
250 | { "title": "A great little article" },
251 | { "title": "..." },
252 | { "title": "..." }
253 | ]
254 | },
255 | "article": {
256 | "title": "A great little article",
257 | "author": {
258 | "name": "John Appleseed"
259 | }
260 | }
261 | }
262 | }
263 | ```
264 |
265 | Notice how this linking works both for singular and plural relationships.
266 |
267 | ### Namespacing types
268 |
269 | The plugin bases the [GraphQL types](https://graphql.org/learn/schema/) for the content items on their [document type](https://our.umbraco.com/documentation/tutorials/creating-basic-site/document-types/). To avoid name clashes with types built in to Gatsby or created by other plugins, you can configure a `typePrefix`. The plugin will prepend every type name with this prefix.
270 |
271 | See: [configuration](#configuration).
272 |
273 | Consider the following example, in which the type prefix is configured to be "Umbraco". The plugin configuration will look like:
274 |
275 | ```js
276 | {
277 | url: process.env.UMBRACO_API_URL,
278 | typePrefix: "Umbraco"
279 | }
280 | ```
281 |
282 | Given that you have a document type `Textpage` that has a field `name` you could run the following query:
283 |
284 | ```graphql
285 | {
286 | umbracoTextpage {
287 | name
288 | }
289 | allUmbracoTextpage {
290 | nodes {
291 | name
292 | }
293 | }
294 | }
295 | ```
296 |
297 | ### Verbose logging
298 |
299 | By default the plugin only reports errors during the build process. For debugging purposes it is sometimes very useful to get more context around an error. For this purpose the plugin outputs verbose logging messages, these are hidden unless running the build/develop command using the `--verbose` flag.
300 |
301 | ```shell
302 | gatsby develop --verbose
303 | # or
304 | gatsby build --verbose
305 | ```
306 |
307 | ## How it works
308 |
309 | The following sequence diagram outlines the process of pulling content from Umbraco and supplying it to Gatsby. Below you'll find a more detailed explanation of the steps. In this diagram, "Umbraco" refers to your API.
310 |
311 |
312 |
313 |
314 |
315 | #### Loading content
316 |
317 | In Umbraco, content is structured as nodes in the document tree. Every node is of a document type, and every document type defines properties of specific data types. The fact that the nodes are structured in a tree means that every node can have parent and child nodes.
318 |
319 | In Gatsby, content is available as nodes via a GraphQL API. A source plugin can create new nodes, often (as is the case with this one), a plugin will create nodes based on data it downloads from a remote API. Once a node has been registered with Gatsby, it is available via the GraphQL API.
320 |
321 | Note that the term node can mean two different things here. Going forward: nodes in Umbraco will mostly be referred to as Umbraco nodes, and nodes in Gatsby will just be referred to as nodes.
322 |
323 | To pull the content from Umbraco into Gatsby, the plugin needs to know what nodes exist in Umbraco and how it can load the data (properties/fields) of these nodes. The plugin solves this using a sitemap (of sorts) that contains basic data for every Umbraco node.
324 |
325 | ```json
326 | {
327 | "root": {
328 | "id": 1062,
329 | "urlSegment": "",
330 | "type": "homepage",
331 | "children": [
332 | {
333 | "id": 1066,
334 | "urlSegment": "about",
335 | "type": "textpage",
336 | "children": []
337 | }
338 | ]
339 | }
340 | }
341 | ```
342 |
343 | Notice how the sitemap resembles the structure of the document tree: every node can have a set of child nodes, starting from a root node. The path from which to load the data of each node is constructed using the `urlSegment` properties. This path structure resembles [the one native to Umbraco](https://our.umbraco.com/Documentation/Reference/Routing/Request-Pipeline/) and makes this plugin work very well with [Umbraco HeadRest](https://github.com/mattbrailsford/umbraco-headrest).
344 |
345 | Once the plugin knows what Umbraco nodes exist and how to load them, it can start fetching. The data can be any valid JSON object, for example:
346 |
347 | ```json
348 | {
349 | "title": "Hello World!",
350 | "meta": {
351 | "stars": 1000,
352 | "private": false
353 | },
354 | "contributors": [
355 | "John Appleseed",
356 | "Jane Appleseed"
357 | ]
358 | }
359 | ```
360 |
361 | Before the node is registered with Gatsby, the plugin loads remote files and handles foreign key fields.
362 |
363 | #### Loading remote files
364 |
365 | To load remote files the plugin scans the incoming data for key names suffixed by the `remoteFileSuffix`. The plugin scans nested arrays and objects too. When it encounters such a field it downloads the file and turns it into a `File` node using [`createRemoteFileNode`](https://www.gatsbyjs.org/packages/gatsby-source-filesystem/#createremotefilenode), as outlined in the diagram below. The field is then replaced with a link to the created `File` node.
366 |
367 |
368 |
369 |
370 |
371 | For example, the data for the following example Umbraco node contains a URL to an image file (`image___FILE`). The file is downloaded and the field is replaced with a [reference](https://www.gatsbyjs.org/docs/schema-gql-type#foreign-key-reference-___node) to the created `File` node (`image___NODE`).
372 |
373 | ```json
374 | {
375 | "title": "Gatsby source Umbraco",
376 | "image___FILE": "https://source.unsplash.com/43E513RKDug"
377 | }
378 | ```
379 |
380 | If a remote file field contains an array of file URLs, the plugin attempts to download and transform each file. For example, the following remote file field would be valid:
381 |
382 | ```json
383 | {
384 | "title": "Gatsby source Umbraco",
385 | "media___FILE": [
386 | "https://source.unsplash.com/43E513RKDug",
387 | "https://source.unsplash.com/qL25vQrdd3Y",
388 | "https://source.unsplash.com/5oyFrBF33Q4"
389 | ]
390 | }
391 | ```
392 |
393 | #### Relationships between nodes
394 |
395 | The plugin scans the incoming data for key names suffixed by the `foreignKeySuffix` too. When it encounters such a field, the plugin assumes it contains a Umbraco node ID and replaces the field with a link to the node in Gatsby. To do this, the plugin has to transform the ID from Umbraco the same way it transforms the ID when registering the node in the first place.
396 |
397 | For example, the data for the following example Umbraco node contains a foreign key field `author___ID`. This ID is transformed and the field is replaced with a [reference](https://www.gatsbyjs.org/docs/schema-gql-type#foreign-key-reference-___node) to the node at that ID (`author___NODE`).
398 |
399 | ```json
400 | {
401 | "title": "Building your own Gatsby plugin",
402 | "publishDate": "2019-10-17T00:00:00",
403 | "author___ID": 123456
404 | }
405 | ```
406 |
407 | Just as with remote files, this works for arrays of IDs too. For example:
408 |
409 | ```json
410 | {
411 | "name": "John Appleseed",
412 | "articles___ID": [
413 | 654321,
414 | 123123,
415 | 321654
416 | ]
417 | }
418 | ```
419 |
420 | Note that in order for this to work, the node that is referenced to must be available to Gatsby. This means that it should be loaded by the plugin and thus part of the sitemap etc. (See [loading content](#loading-content).)
421 |
422 | #### Node metadata
423 |
424 | The `id` and `type` properties from the sitemap are used for the ID and type of the node, respectively—that way, these properties don't have to be included in the data of each node. To make sure the ID is unique, it is passed through Gatsby's [`createNodeId`](https://www.gatsbyjs.org/docs/node-api-helpers/#createNodeId) helper function. The type name is capitalized and prepended by the `typePrefix`.
425 |
426 | After the preparation steps, the data of a node, returned by the Umbraco API, is merged with the node metadata. Note that the key names used by [Gatsby's Node interface](https://www.gatsbyjs.org/docs/node-interface/) are reserved.
427 |
428 | #### Overlapping interface
429 |
430 | While creating and registering nodes, the plugin keeps track of all the types it encounters. Once all of the nodes are in the system, the plugin creates a [GraphQL interface](https://graphql.org/learn/schema/#interfaces) and makes sure all types registered by the plugin implement it. The interface is set up to be a [`@nodeInterface`](https://www.gatsbyjs.org/docs/schema-customization/#queryable-interfaces-with-the-nodeinterface-extension), meaning it is treated as a regular top-level node type. The name and fields of the interface can be configured.
431 |
432 | ## Configuration
433 |
434 | The plugin allows for advanced configuration; the table below describes all configurable items. Most options have a default value and are therefore optional, allowing for a simple initial setup.
435 |
436 | | Option | Description | Type | Default |
437 | | :----------------------- | :----------------------------------------------------------- | :------: | :-------------: |
438 | | `url` | The base URL of your Umbraco API. Relative URLs are prepended with this base URL. Under the hood, this is passed to [axios](https://github.com/axios/axios). The URL must be valid and reachable. | `String` | - |
439 | | `sitemapRoute` | The URL from which to load the sitemap data, it must be a valid route or URL. | `String` | `"sitemap"` |
440 | | `typePrefix` | String to prefix all type names with. This prefix can be used to avoid name clashes with types native to Gatsby or types created by other plugins. This prefix does not impact `commonInterface.name`. | `String` | `""` |
441 | | `commonInterface.name` | The name of the common interface, it must be a string of at least one character. | `String` | `"UmbracoNode"` |
442 | | `commonInterface.fields` | An object containing field definitions for the common interface. Every key is used as the field name and every value as the field type. Types must be valid [GraphQL types](https://graphql.org/learn/schema/). Note that the plugin only validates the types to be strings, issues with the types will be reported by Gatsby/GraphQL. | `Object` | `{}` |
443 | | `remoteFileSuffix` | The suffix used to identify remote file fields in the data returned by the API, it must be a string of at least one character. | `String` | `"___FILE"` |
444 | | `foreignKeySuffix` | The suffix used to identify remote file fields in the data returned by the API, it must be a string of at least one character. | `String` | `"___ID"` |
445 |
446 | Consider the following code snippet, it contains an example configuration that specifies all of the available options:
447 |
448 | ```js
449 | {
450 | url: "https://your-umbraco-api.io",
451 | sitemapRoute: "sitemap",
452 | typePrefix: "Umbraco",
453 | commonInterface: {
454 | name: "UmbracoNode",
455 | fields: {
456 | name: "String",
457 | slug: "String"
458 | }
459 | },
460 | remoteFileSuffix: "___FILE",
461 | foreignKeySuffix: "___ID"
462 | }
463 | ```
464 |
465 | ## API design
466 |
467 | As mentioned in the introduction, this plugin is meant to be used when building a custom API on top of Umbraco—it requires the API to follow some conventions. The plugin attempts to keep these conventions as minimal as possible to allow flexibility. That being said, the plugin was build using [Umbraco HeadRest]( https://github.com/mattbrailsford/umbraco-headrest ) to implement the API on the backend (to have a prototype to test against), this may have impacted some design decisions.
468 |
469 | **Note that the plugin expects all data to be in JSON format.**
470 |
471 | #### Sitemap
472 |
473 | As explained in the "[how it works](#loading-content)" section, the plugin expects a sitemap of sorts to indicate what nodes exist in Umbraco and how it can load the data for these nodes. Data from this sitemap is also used as metadata of when creating Gatsby nodes (see: [node metadata](#node-metadata)). The route from which this data is loaded is configurable and defaults to `/sitemap`. (See: [configuration](#configuration).)
474 |
475 | The format for this sitemap is based on Umbraco's document tree. The plugin expects each node in the sitemap to contain `id`, `type`, and `urlSegment` properties. When a node in Umbraco has children they should be output as sitemap nodes as well.
476 |
477 | The sitemap must start at a `root` property. Every node (starting at `root` going down each level of `children`) in the sitemap is validated against the following rules:
478 |
479 | - `id` must be either a number, or a string of at least 1 character.
480 | - `type` must be a string
481 | - `urlSegment` must be a string
482 | - `children` must be an array of objects, these objects are later validated against the same rules.
483 |
484 | Consider the following sitemap, it contains two nodes, a root node of type `Homepage` and a node of type `Textpage` nested below it.
485 |
486 | ```json
487 | {
488 | "root": {
489 | "id": 1062,
490 | "urlSegment": "",
491 | "type": "homepage",
492 | "children": [
493 | {
494 | "id": 1066,
495 | "urlSegment": "about",
496 | "type": "textpage",
497 | "children": []
498 | }
499 | ]
500 | }
501 | }
502 | ```
503 |
504 | #### Content items
505 |
506 | As explained in the "[how it works](#how-it-works)" section, the data for each content item is loaded from a URL. The returned JSON object becomes the data of the node in Gatsby's GraphQL API. Consider the following best practices when building your API.
507 |
508 | - Your API should not nest the data of a content item in some unnecessarily.
509 | - The endpoint for a content item should only return data for that specific content item.
510 |
511 | - Content items of the same type should have (roughly) the same properties. Gatsby will try to infer the shape of a type based on the data it encounters, if two items of the same type have different properties the GraphQL schema will think every item of that type has _all_ properties which might be weird.
512 |
513 | Consider the following example data.
514 |
515 | ```json
516 | {
517 | "name": "Home",
518 | "slug": "/",
519 | "title": "Gatsby source Umbraco",
520 | "navigationLinks": [
521 | {
522 | "name": "Home",
523 | "url": "/"
524 | },
525 | {
526 | "name": "About",
527 | "url": "/about/"
528 | }
529 | ]
530 | }
531 | ```
532 |
533 | Note that it is OK to have nested objects and arrays in the data.
534 |
535 | Again, this plugin works great in combination with [Umbraco HeadRest]( https://github.com/mattbrailsford/umbraco-headrest ), which "converts the Umbraco front-end into a REST API by passing content models through a mapping to create serializable view models." In this case the view model describes the data structure of a content item, and with that the structure of the node in Gatsby's GraphQL schema.
536 |
537 | ## Limitations
538 |
539 | ### Type discovery
540 |
541 | As explained in the "[how it works](#loading-content)" section, the plugin creates GraphQL types based on the data available from the API. The plugin loads each item listed on the sitemap and creates a node of the appropriate type for it. The data shape of a type is [inferred by Gatsby](https://www.gatsbyjs.org/docs/schema-gql-type/), based on the available data.
542 |
543 | The plugin only knows of types that it encounters. If there are no content items of a document type (yet) the type is not picked up by Gatsby, meaning the type is not available to query. Querying for a type that does not exist will cause an error that stops Gatsby's build process.
544 |
545 | Consider the following situation in which this problem occurs:
546 |
547 | Let's say you are building a website that has a blog. In your local environment, you start by creating the document types, then some example blog posts for testing. You then move on to implement the frontend: you start your Gatsby development environment which syncs the data from Umbraco, making the new type available. You create a template for your blog post (which includes a query). Everything works well.
548 |
549 | You decide to deploy the backend changes to a staging environment, here, the customer is responsible for writing content so you don't sync the example blog posts to this environment. You attempt to build the Gatsby frontend, which points to this staging environment of Umbraco, but this fails. The type you created is not available to Gatsby. The staging environment will not reflect the latest changes until a blog post is added.
550 |
551 | A similar problem can occur with nullable fields. If no content items of a given type have a value for a certain field, the field is never encountered by Gatsby and therefore not included in the type definition. Querying for a field that does not exist will cause an error that stops Gatsby's build process.
552 |
553 | ##### Workaround
554 |
555 | This limitation is inherent to the way this plugin works. One way to get around it would be to add a new feature to this plugin that communicates with the Umbraco API about the available types. This would require some sort of data format. Programmatically getting and outputting this data on the backend might be a difficult. Requiring type definitions will increase the amount of time needed to get up and running.
556 |
557 | A simpler workaround is to just tell Gatsby what the types will look like. This can be done by implementing the [`createSchemaCustomization`](https://www.gatsbyjs.org/docs/node-apis/#createSchemaCustomization) hook in the `gatsby-node.js` file in the root of a Gatsby project. Consider the following example which assumes you have a document type `BlogPost` and adds an explicit type definition for it.
558 |
559 | ```js
560 | /* in gatsby-node.js */
561 | exports.createSchemaCustomization = gatsby => {
562 | const typeDefs = `
563 | type BlogPost @dontInfer {
564 | id: ID!
565 | title: String
566 | ...
567 | }
568 | `
569 | gatsby.actions.createTypes(typeDefs)
570 | }
571 | ```
572 |
573 | Note the [`@dontInfer`](https://www.gatsbyjs.org/docs/schema-customization/#opting-out-of-type-inference) directive, this tells Gatsby to not use [automatic type inference](https://www.gatsbyjs.org/docs/schema-customization/#automatic-type-inference) for this type.
574 |
575 | By default Gatsby handles creating relational properties and formatting date properties automatically using conventions. When explicitly defining types Gatsby can't take care of this. You can use the `@link` and `@dateformat` extensions to make this easy. Consider the following type definition, it contains a date and a relational property.
576 |
577 | ```graphql
578 | type ExampleType {
579 | publishDate: Date @dateformat
580 | image: File @link(from: "image___NODE")
581 | }
582 | ```
583 |
584 | See: "[schema customization > extensions and directives](https://www.gatsbyjs.org/docs/schema-customization/#extensions-and-directives)" and the [`createTypes`](https://www.gatsbyjs.org/docs/actions/#createTypes) documentation.
585 |
586 | It is possible to build a custom way to load type definitions from your Umbraco API outside of the plugin. A very simple example of this would be to download a file containing the type definitions in the GraphQL schema definition language and pass that to Gatsby inside a project's `gatsby-node.js` file.
587 |
588 | ### Multilingual
589 |
590 | Umbraco 8 has introduces an awesome way to handle multilingual content called [Language Variants](https://our.umbraco.com/documentation/getting-started/Backoffice/Variants/). How this might work in combination with Gatsby and this plugin has not been thought out yet. It is currently not possible for a node to exist in multiple languages, and therefore this plugin is ***not*** compatible with [Umbraco Language Variants](https://our.umbraco.com/documentation/getting-started/Backoffice/Variants/).
591 |
592 | ### Protected API
593 |
594 | Currently, there is no way to configure the plugin to use any form of auth credentials. If your API is protected by some form of authentication, the plugin will likely encounter some [4xx HTTP errors]( https://httpstatuses.com/ ) (like 401 unauthorized, or 403 forbidden) and crash.
595 |
596 | ### Advanced usage of rich text editor
597 |
598 | Umbraco has a built-in [Rich Text Editor]( https://our.umbraco.com/documentation/getting-started/backoffice/property-editors/Built-in-Property-Editors/Rich-Text-Editor/ ). While simpler usage of the rich text editor works fine, more advanced usage might cause some unexpected behavior.
599 |
600 | A concrete example of such a limitation is with images. As discussed in the "[advanced usage](#loading-remote-files)" section, the plugin can make download files and make them to Gatsby. The plugin can't, however, do the same for images that are inlined in the rich text editor.
601 |
602 | ## Recipes
603 |
604 | ### Creating pages for content items
605 |
606 | This plugin takes care of loading content and media from a custom Umbraco API into Gatsby. Once the data is available you probably want to create pages. In Gatsby you can [programmatically create pages](https://www.gatsbyjs.org/docs/programmatically-create-pages-from-data/). When doing so you have access to Gatsby's GraphQL schema, and with that, the all the data loaded by the plugin.
607 |
608 | ##### Prerequisites
609 |
610 | * Every content item output by your Umbraco API contains a `slug` field. The plugin is configured so that the [common interface](#overlapping-interface) contains this `slug` field.
611 | * There are `BlogPost` and `Textpage` document types in Umbraco and these are output correctly by your Umbraco API. At least 1 content item per type is available via your Umbraco API.
612 |
613 | ##### Directions
614 |
615 | To programmatically create pages in Gatsby you have to implement the [`createPages`]( https://www.gatsbyjs.org/docs/node-apis/#createPages ) hook in your sites' `gatsby-node.js` file. Within that function you have access to the `graphql` function to query for the appropriate data, and the [`createPage`]( https://www.gatsbyjs.org/docs/actions/#createPage ) action.
616 |
617 | The `createPage` action expects the path at which to create a page, a [template component]( https://www.gatsbyjs.org/docs/building-with-components/#page-template-components ), and some context for the page. Consider the example `gatsby-node.js` file below, which creates pages for all `UmbracoNode`s.
618 |
619 | * All the `UmbracoNode`s have a `slug` field (see prerequisites) that can be used for the path of the page.
620 | * The template for a page is dependent on the type of content, it is possible to query for this type and use it to determine what template is selected, in the example a `templates` object is used for mapping. `path.resolve` is used to turn a relative path into an absolute one, which is required by the [`createPage`]( https://www.gatsbyjs.org/docs/actions/#createPage ) action. Note that this code will crash if it encounters a `type` that is not present as a key in the `templates` object.
621 | * The ID of the node is passed in the context object of the created page.
622 |
623 | ```js
624 | /* in gatsby-node.js */
625 |
626 | const path = require("path")
627 |
628 | const templates = {
629 | Textpage: path.resolve("./src/templates/textpage.js"),
630 | BlogPost: path.resolve("./src/templates/blogPost.js"),
631 | }
632 |
633 | exports.createPages = async ({ graphql, actions }) => {
634 | const { createPage } = actions
635 |
636 | const result = await graphql(`
637 | query {
638 | allUmbracoNode {
639 | nodes {
640 | id
641 | slug
642 | type: __typename
643 | }
644 | }
645 | }
646 | `)
647 |
648 | const nodes = result.data.allUmbracoNode.nodes
649 |
650 | for (const node of nodes) {
651 | createPage({
652 | path: node.slug,
653 | context: { id: node.id },
654 | component: templates[node.type],
655 | })
656 | }
657 | }
658 | ```
659 |
660 | A page template consists of a query (see: [Querying Data in Pages with GraphQL](https://www.gatsbyjs.org/docs/page-query/)) and a [component](https://www.gatsbyjs.org/docs/building-with-components/#page-template-components) (React). The result of the query is passed to the component via the `data` [prop](https://reactjs.org/docs/components-and-props.html).
661 |
662 | Gatsby uses the context object of a page for variables in page queries (see: [how to add query variables to a page query]( https://www.gatsbyjs.org/docs/page-query/#how-to-add-query-variables-to-a-page-query )). In the `gatsby-node.js` file, the `id` of a node is passed as the context, therefore it is accessible as a variable in the query (see: [GraphQL query variables]( https://graphql.org/learn/queries/#variables )). The query for the `Textpage` template uses the `$id` variable as an [argument]( https://graphql.org/learn/queries/#arguments ) on `textpage` to select a specific `Textpage` and ensure the page contains the correct data.
663 |
664 | ```jsx
665 | /* in src/templates/textpage.js */
666 |
667 | export default ({ data }) => {
668 | const { page } = data
669 | return (
670 |
671 |
{page.title}
672 |
673 |
674 | )
675 | }
676 |
677 | export const query = graphql`
678 | query($id: ID!) {
679 | page: textpage(id: { eq: $id }) {
680 | title
681 | content
682 | }
683 | }
684 | `
685 | ```
686 |
687 | A similar template component for `BlogPost` in `src/templates/blogPost.js` is required, but omitted here for brevity.
688 |
689 | ## Tips
690 |
691 | ### Absolute Media URL provider
692 |
693 | As mentioned in the "[loading remote files](#loading-remote-files)" section, the URL to a remote file must be absolute. By default, the `Url()` getter on a media item in Umbraco will return a relative URL. One way to get around this is by passing `UrlMode.Absolute` to the `.Url()` getter. Doing this everywhere in your code, however, isn't ideal.
694 |
695 | Since Umbraco 8.1 it is possible to create a custom [`MediaUrlProvider`]( https://github.com/umbraco/Umbraco-CMS/pull/5282 ). You can create such a provider that hardcodes the `UrlMode` to be `Absolute`. Consider the following `AbsoluteMediaUrlProvider`, notice how `UrlMode.Absolute` is passed to the base class.
696 |
697 | ```csharp
698 | namespace Way.Beheaded.Core.Composing.Providers
699 | {
700 | using System;
701 | using Umbraco.Core.Models.PublishedContent;
702 | using Umbraco.Web;
703 | using Umbraco.Web.Routing;
704 |
705 | public class AbsoluteMediaUrlProvider : DefaultMediaUrlProvider
706 | {
707 | public override UrlInfo GetMediaUrl(UmbracoContext umbracoContext, IPublishedContent content, string propertyAlias, UrlMode mode, string culture, Uri current)
708 | {
709 | return base.GetMediaUrl(umbracoContext, content, propertyAlias, UrlMode.Absolute, culture, current);
710 | }
711 | }
712 | }
713 | ```
714 |
715 | Note that you still have to implement a [`composer`]( https://our.umbraco.com/documentation/Implementation/Composing/ ) to register this `AbsoluteMediaUrlProvider`.
716 |
717 | ### Group content items using GraphQL interfaces
718 |
719 | The use of grouping Gatsby nodes using [GraphQL interfaces](https://graphql.org/learn/schema/#interfaces) is, in part, demonstrated by the [overlapping `UmbracoNode` interface](#overlapping-interface) that is already created by this plugin. In some scenario's it can be useful to group a subset of content items from Umbraco too. A concrete example of such a scenario is when you have a portfolio that contains several types of items like blog posts and case studies, but all with similar previews on a `/portfolio` page. In this case you could use a `PortfolioItem` interface so you can query for `allPortfolioItem`.
720 |
721 | Another use case for creating extra GraphQL interfaces is with [`compositions`]( https://our.umbraco.com/documentation/Getting-Started/Data/Defining-content/#creating-a-document-type ). In Umbraco you can use compositions to share common fields between document types. In combination with [regular-](https://graphql.org/learn/queries/#fragments) or [inline fragments](https://graphql.org/learn/queries/#inline-fragments), an interface for such a composition could save a bunch of repetitive code.
722 |
723 | You can use Gatsby's [`createSchemaCustomization `]( https://www.gatsbyjs.org/docs/node-apis/#createSchemaCustomization ) hook to define extra interfaces and make the appropriate types implement the appropriate interfaces. This is pretty straight forwards if you are already explicitly defining your types, as is the suggested workaround to the [type discovery limitation](#type-discovery) of this plugin, but it works just as well if you're not.
724 |
725 | Consider the following example, in which an `UmbracoPage` interface is created and added to the `UmbracoHomepage` and `UmbracoTextpage` types.
726 |
727 | ```js
728 | exports.createSchemaCustomization = gatsby => {
729 | const typeDefs = `
730 | interface UmbracoPage @nodeInterface {
731 | id: ID!
732 | slug: String
733 | }
734 |
735 | type UmbracoHomepage implements UmbracoPage @infer {
736 | id: ID!
737 | slug: String
738 | }
739 |
740 | type UmbracoTextpage implements UmbracoPage @infer {
741 | id: ID!
742 | slug: String
743 | }
744 | `
745 | gatsby.actions.createTypes(typeDefs)
746 | }
747 | ```
748 |
749 | Notice the `@infer` directive used on the type definitions. This directive tells Gatsby to continue using [automatic type inference]( https://www.gatsbyjs.org/docs/schema-customization/#automatic-type-inference ) for a type, that way your type definitions don't have to include all of the fields for each type, which keeps the code a bit more concise. The [`@nodeInterface`]( https://www.gatsbyjs.org/docs/schema-customization/#queryable-interfaces-with-the-nodeinterface-extension ) directive tells Gatsby to treat this interface as if it were a top-level node type, allowing you to query for it.
750 |
751 | ## License
752 |
753 | gatsby-source-umbraco is [MIT licensed](./LICENSE).
754 |
--------------------------------------------------------------------------------
/gatsby-node.js:
--------------------------------------------------------------------------------
1 | const loadUmbracoNodes = require("./lib/load-umbraco-nodes")
2 | const createAndRegisterCommonInterface = require("./lib/create-common-interface")
3 | const validateAndPrepOptions = require("./lib/validate-and-prep-options")
4 | const createAndConfigureAxios = require("./lib/create-and-configure-axios")
5 |
6 | exports.sourceNodes = async (gatsby, pluginOptions) => {
7 | const options = await validateAndPrepOptions(pluginOptions, gatsby.reporter)
8 | const axios = createAndConfigureAxios(gatsby, options)
9 |
10 | const helpers = {
11 | gatsby,
12 | axios,
13 | options,
14 | }
15 |
16 | await loadUmbracoNodes(helpers)
17 | createAndRegisterCommonInterface(gatsby, options)
18 | }
19 |
20 | exports.setFieldsOnGraphQLNodeType = require("gatsby-source-filesystem/extend-file-node")
21 |
--------------------------------------------------------------------------------
/lib/create-and-configure-axios.js:
--------------------------------------------------------------------------------
1 | const axios = require("axios").default
2 | const formatMessage = require("../util/formatMessage")
3 |
4 | module.exports = function createAndConfigureAxios(gatsby, options) {
5 | const config = {
6 | baseURL: options.url,
7 | }
8 | const instance = axios.create(config)
9 | configureInterceptors(gatsby, instance)
10 | return instance
11 | }
12 |
13 | function configureInterceptors(gatsby, axios) {
14 | axios.interceptors.request.use(logRequest)
15 | axios.interceptors.response.use(logResponse, logError)
16 |
17 | function logRequest(config) {
18 | const message = formatMessage(`axios → ${config.method} ${config.url}`)
19 | gatsby.reporter.verbose(message)
20 | return config
21 | }
22 |
23 | function logResponse(response) {
24 | logResponseMessage(response)
25 | return response
26 | }
27 |
28 | function logError(error) {
29 | logResponseMessage(error.response)
30 | return Promise.reject(error)
31 | }
32 |
33 | function logResponseMessage(response) {
34 | const message = formatMessage(
35 | `axios ← ${response.status} (${response.config.method} ${response.config.url})`
36 | )
37 | gatsby.reporter.verbose(message)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/lib/create-common-interface.js:
--------------------------------------------------------------------------------
1 | const { allTypes } = require("./typeRegistry")
2 | const formatMessage = require("../util/formatMessage")
3 |
4 | module.exports = (gatsby, options) => {
5 | const { name: interface, fields } = options.commonInterface
6 | gatsby.reporter.verbose(formatMessage(`Creating common interface "${interface}"`))
7 | const interfaceType = createInterfaceTypeDefinition(interface, fields)
8 | const concreteTypes = allTypes().map(type => {
9 | gatsby.reporter.verbose(formatMessage(`Adding interface to type "${type}"`))
10 | return buildObjectTypeWithInterface(type, interface, gatsby.schema)
11 | })
12 | gatsby.actions.createTypes([interfaceType, ...concreteTypes])
13 | }
14 |
15 | function buildObjectTypeWithInterface(name, interface, schema) {
16 | return schema.buildObjectType({
17 | name,
18 | interfaces: ["Node", interface],
19 | })
20 | }
21 |
22 | function createInterfaceTypeDefinition(name, fields) {
23 | const fieldDefinitions = createFieldDefinitions(fields)
24 | return `
25 | interface ${name} @nodeInterface {
26 | id: ID!
27 | parent: Node
28 | children: [Node!]!
29 | internal: Internal!
30 | ${fieldDefinitions}
31 | }
32 | `
33 | }
34 |
35 | /**
36 | * Transform object of GraphQL fields (name:"type") to string representation ("name: type")
37 | * @param {Object} fields
38 | */
39 | function createFieldDefinitions(fields) {
40 | return Object.entries(fields)
41 | .map(([name, type]) => `${name}: ${type}`)
42 | .reduce((all, definition) => (all += definition + "\n"), "")
43 | }
44 |
--------------------------------------------------------------------------------
/lib/fetch-and-validate-sitemap.js:
--------------------------------------------------------------------------------
1 | const { isArray, isString, isObject, isNumber } = require("../util/typecheck")
2 | const formatMessage = require("../util/formatMessage")
3 |
4 | module.exports = async helpers => {
5 | const route = helpers.options.sitemapRoute
6 | const reporter = helpers.gatsby.reporter
7 |
8 | reporter.verbose(formatMessage(`Fetching sitemap from "${route}" route`))
9 | let sitemap = await fetchSitemap(helpers, route)
10 | reporter.verbose(formatMessage(`Validating and prepping sitemap`))
11 | sitemap = validateAndPrepSitemapRecursive(helpers, sitemap.root)
12 | return { root: sitemap }
13 | }
14 |
15 | async function fetchSitemap({ gatsby, axios }, route) {
16 | try {
17 | const { data: sitemap } = await axios.get(route)
18 | return sitemap
19 | } catch (error) {
20 | const message = formatMessage(
21 | `Problem loading sitemap from "${route}" route.`
22 | )
23 | gatsby.reporter.panic(message, error)
24 | }
25 | }
26 |
27 | function validateAndPrepSitemapRecursive(helpers, sitemapNode, parent = {}) {
28 | validateSitemapNode(helpers, sitemapNode)
29 | const node = prepSitemapNode(helpers, sitemapNode, parent)
30 | const children = sitemapNode.children.map(child =>
31 | validateAndPrepSitemapRecursive(helpers, child, node)
32 | )
33 | return {
34 | ...node,
35 | children,
36 | }
37 | }
38 |
39 | /* ============
40 | * Validation
41 | * ============ */
42 |
43 | function validateSitemapNode(helpers, sitemapNode) {
44 | if (!isValidID(sitemapNode.id))
45 | throwValidationError(helpers, "id", sitemapNode.id, sitemapNode)
46 |
47 | if (!isString(sitemapNode.urlSegment))
48 | throwValidationError(
49 | helpers,
50 | "urlSegment",
51 | sitemapNode.urlSegment,
52 | sitemapNode
53 | )
54 |
55 | if (!isString(sitemapNode.type) || sitemapNode.type.length < 1)
56 | throwValidationError(helpers, "type", sitemapNode.type, sitemapNode)
57 |
58 | if (!isValidChildren(sitemapNode.children))
59 | throwValidationError(helpers, "children", sitemapNode.children, sitemapNode)
60 | }
61 |
62 | function isValidID(id) {
63 | return (isString(id) && id.length > 0) || isNumber(id)
64 | }
65 |
66 | function isValidChildren(children) {
67 | return isArray(children) && children.every(isObject)
68 | }
69 |
70 | function throwValidationError(
71 | { gatsby },
72 | propertyName,
73 | propertyValue,
74 | sitemapNode
75 | ) {
76 | const message = [
77 | `Encountered invalid node in sitemap. Property [${propertyName}] is invalid or missing.`,
78 | ``,
79 | ` Current value of [${propertyName}]: ${JSON.stringify(propertyValue)}`,
80 | ` The sitemap node: ${JSON.stringify(sitemapNode)}`,
81 | ].join("\n")
82 | gatsby.reporter.panic(message)
83 | }
84 |
85 | /* =============
86 | * Preparation
87 | * ============= */
88 |
89 | function prepSitemapNode(helpers, sitemapNode, parent = {}) {
90 | const { options } = helpers
91 | const path = getPathForSitemapNode(sitemapNode, parent)
92 | const type = options.typePrefix + normalizeType(sitemapNode.type)
93 | return {
94 | ...sitemapNode,
95 | path,
96 | type,
97 | }
98 | }
99 |
100 | function getPathForSitemapNode(sitemapNode, parent) {
101 | let path = (parent.path || "") + "/" + sitemapNode.urlSegment
102 | if (path.indexOf("//") == 0) path = path.substr(1)
103 | return path
104 | }
105 |
106 | function normalizeType(type) {
107 | return type.charAt(0).toUpperCase() + type.slice(1)
108 | }
109 |
--------------------------------------------------------------------------------
/lib/handle-foreign-key-fields.js:
--------------------------------------------------------------------------------
1 | const { isString, isNumber, isArray, isObject } = require("../util/typecheck")
2 | const formatMessage = require("../util/formatMessage")
3 | const getGatsbyReferenceKey = require("../util/getGatsbyReferenceKey")
4 |
5 | module.exports = (helpers, fields) => {
6 | helpers.gatsby.reporter.verbose(
7 | formatMessage(`Looking for foreign key fields on node`)
8 | )
9 | return handleForeignKeyFieldsOnObject(helpers, (object = fields))
10 | }
11 |
12 | /**
13 | * Transform all foreign key (FK) fields to Gatsby relational fields for an object.
14 | *
15 | * For all keys that end in the FK suffix (configured as foreignKeySuffx) the
16 | * ID is transformed into a Gatsby ID. The field is then replaced with a Gatsby
17 | * foreign key field.
18 | *
19 | * Arrays of FK ids are also supported, every item of an array in a FK field
20 | * is transformed into a Gatsby ID.
21 | *
22 | * This function also checks nested objects, and objects in nested arrays, to
23 | * make sure all FK fields are transformed.
24 | *
25 | * @param {*} helpers
26 | * @param {Object} object
27 | */
28 | function handleForeignKeyFieldsOnObject(helpers, object) {
29 | const foreignKeySuffix = helpers.options.foreignKeySuffix
30 | const obj = { ...object }
31 | const entries = Object.entries(obj)
32 |
33 | for (const [key, value] of entries) {
34 | if (key.endsWith(foreignKeySuffix)) {
35 | handleForeignKeyFieldInObject(helpers, obj, key)
36 | } else if (isObject(value)) {
37 | obj[key] = handleForeignKeyFieldsOnObject(helpers, (object = value))
38 | } else if (isArray(value)) {
39 | obj[key] = handleForeignKeyFieldsForArray(helpers, (array = value))
40 | }
41 | }
42 |
43 | return obj
44 | }
45 |
46 | /**
47 | * Transform FK fields for objects in an array.
48 | *
49 | * @param {*} helpers
50 | * @param {Array} array
51 | */
52 | function handleForeignKeyFieldsForArray(helpers, array) {
53 | const newArray = []
54 |
55 | for (const value of array) {
56 | let newValue = value
57 | if (isObject(value)) {
58 | newValue = handleForeignKeyFieldsOnObject(helpers, (object = value))
59 | }
60 | else if (isArray(value)) {
61 | newValue = handleForeignKeyFieldsForArray(helpers, (array = value))
62 | }
63 | newArray.push(newValue)
64 | }
65 |
66 | return newArray
67 | }
68 |
69 | /**
70 | * Replace a FK field with a Gatsby FK field, transforming the id(s)
71 | * with Gatsby id(s).
72 | *
73 | * @param {*} helpers
74 | * @param {Object} object
75 | * @param {String} key key of the FK field
76 | */
77 | function handleForeignKeyFieldInObject(helpers, object, key) {
78 | const { gatsby, options } = helpers
79 | const value = object[key]
80 | let replacement
81 |
82 | if (isIdType(value)) {
83 | gatsby.reporter.verbose(
84 | formatMessage(
85 | `Handling foreign key field (key: "${key}", value: "${value}")`
86 | )
87 | )
88 | replacement = gatsby.createNodeId(value)
89 | } else if (isArray(value) && value.every(isIdType)) {
90 | gatsby.reporter.verbose(
91 | formatMessage(
92 | `Handling foreign key array (key: "${key}", value: ${JSON.stringify(
93 | value
94 | )})`
95 | )
96 | )
97 | replacement = value.map(id => gatsby.createNodeId(id))
98 | } else {
99 | panicOnInvalidForeignKeyType(helpers, key, value)
100 | }
101 |
102 | delete object[key]
103 | const referenceKey = getGatsbyReferenceKey(key, options.foreignKeySuffix)
104 | object[referenceKey] = replacement
105 | }
106 |
107 | /**
108 | * Stop the build with an error message regarding invalid type in
109 | * FK field/array.
110 | *
111 | * @param {*} helpers
112 | * @param {String} key
113 | * @param {*} value
114 | */
115 | function panicOnInvalidForeignKeyType({ gatsby }, key, value) {
116 | let wrongTypeValue = value
117 | if (isArray(value)) wrongTypeValue = value.filter(x => !isIdType(x)).shift()
118 | const type = Object.prototype.toString.call(wrongTypeValue)
119 |
120 | const message = formatMessage(
121 | `Encountered invalid type in foreign key ${
122 | isArray(value) ? `array` : `field`
123 | }:`,
124 | ``,
125 | `key: ${key}`,
126 | `invalid value: ${JSON.stringify(wrongTypeValue)}`,
127 | `invalid type: ${type}`
128 | )
129 |
130 | gatsby.reporter.panic(message)
131 | }
132 |
133 | function isIdType(value) {
134 | return isString(value) || isNumber(value)
135 | }
136 |
--------------------------------------------------------------------------------
/lib/load-remote-files.js:
--------------------------------------------------------------------------------
1 | const { createRemoteFileNode } = require("gatsby-source-filesystem")
2 | const asyncForEach = require("../util/asyncForEach")
3 | const formatMessage = require("../util/formatMessage")
4 | const { isObject, isArray, isString } = require("../util/typecheck")
5 | const getGatsbyReferenceKey = require("../util/getGatsbyReferenceKey")
6 |
7 | module.exports = async (helpers, fields, meta) => {
8 | helpers.gatsby.reporter.verbose(
9 | formatMessage(`Looking for remote file fields on node`)
10 | )
11 | return loadRemoteFilesForObject(helpers, fields, meta)
12 | }
13 |
14 | /**
15 | * Load all remote files for an object.
16 | *
17 | * For all keys that end in the remote file suffix (configured as remoteFileSuffix) the
18 | * file is downloaded from the URL and transformed into a File node, which is then linked
19 | * to the original node using Gatsby's foreign key reference mechanism.
20 | *
21 | * Arrays of remote files are also supported, every item of an array in a remote file field
22 | * is turned into a File node.
23 | *
24 | * This function also checks nested objects, and objects in nested arrays, to make sure
25 | * all remote files are loaded.
26 | *
27 | * @param {*} helpers
28 | * @param {Object} object
29 | * @param {Object} meta gatsby node meta
30 | * @returns {Promise