Skip to content

Commit c83b85a

Browse files
gillkyleLB
andauthored
docs: update creating source plugins guide & add example repo (#22943)
* add example monorepo, make initial updates for source plugin guide and move some content to the transformer guide * update sample code in guide for proactive fetching * update READMEs and remove unused files * remove more unused files * Apply suggestions from code review Co-Authored-By: LB <laurie@gatsbyjs.com> * split terminal comments up * Apply suggestions from code review Co-Authored-By: LB <laurie@gatsbyjs.com> * code suggestions * more review suggestions Co-authored-by: LB <laurie@gatsbyjs.com>
1 parent f0ec0b5 commit c83b85a

File tree

17 files changed

+1194
-94
lines changed

17 files changed

+1194
-94
lines changed

docs/docs/creating-a-source-plugin.md

Lines changed: 405 additions & 83 deletions
Large diffs are not rendered by default.

docs/docs/creating-a-transformer-plugin.md

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ Now you have a `File` node to work with:
102102

103103
Now, transform the newly created `File` nodes by hooking into the `onCreateNode` API in `gatsby-node.js`.
104104

105+
#### Convert yaml into JSON for storage in Gatsby nodes
106+
105107
If you're following along in an example project, install the following packages:
106108

107109
```shell
@@ -137,41 +139,67 @@ File content:
137139
138140
Parsed YAML content:
139141
140-
```javascript
141-
;[
142+
```json
143+
[
142144
{
143-
id: "Jane Doe",
144-
bio: "Developer based in Somewhere, USA",
145+
"id": "Jane Doe",
146+
"bio": "Developer based in Somewhere, USA"
145147
},
146148
{
147-
id: "John Smith",
148-
bio: "Developer based in Maintown, USA",
149-
},
149+
"id": "John Smith",
150+
"bio": "Developer based in Maintown, USA"
151+
}
150152
]
151153
```
152154

153155
Now you'll write a helper function to transform the parsed YAML content into new Gatsby nodes:
154156

155-
```javascript
157+
```javascript:title=gatsby-node.js
156158
function transformObject(obj, id, type) {
157159
const yamlNode = {
158160
...obj,
159161
id,
160162
children: [],
161-
parent: node.id,
163+
parent: null,
162164
internal: {
163165
contentDigest: createContentDigest(obj),
164166
type,
165167
},
166168
}
167169
createNode(yamlNode)
168-
createParentChildLink({ parent: node, child: yamlNode })
169170
}
170171
```
171172

172173
Above, you create a `yamlNode` object with the shape expected by the [`createNode` action](/docs/actions/#createNode).
173174

174-
You then create a link between the parent node (file) and the child node (yaml content).
175+
#### Creating the transformer relationship
176+
177+
You then need to create a link between the parent node (file) and the child node (yaml content) using the `createParentChildLink` function after adding the parent node's id to the `yamlNode`:
178+
179+
```javascript:title=gatsby-node.js
180+
function transformObject(obj, id, type) {
181+
const yamlNode = {
182+
...obj,
183+
id,
184+
children: [],
185+
parent: node.id, // highlight-line
186+
internal: {
187+
contentDigest: createContentDigest(obj),
188+
type,
189+
},
190+
}
191+
createNode(yamlNode)
192+
createParentChildLink({ parent: node, child: yamlNode }) // highlight-line
193+
}
194+
```
195+
196+
Another example of a transformation relationship is the `gatsby-source-filesystem` plugin used with the `gatsby-transformer-remark` plugin. This combination transforms a parent `File` node's markdown string into a `MarkdownRemark` node. The remark transformer plugin adds its newly created child node as a child of the parent node using the action [`createParentChildLink`](/docs/actions/#createParentChildLink). Transformation relationships like this are used when a new node is _completely_ derived from a single parent node. E.g. the markdown node is derived from the parent `File` node and would not exist if the parent `File` node hadn't been created.
197+
198+
Because all children nodes are derived from their parent, when a parent node is deleted or changed, Gatsby deletes all of the child nodes (and their child nodes, and so on). Gatsby does so with the expectation that they'll be recreated again by transformer plugins. This is done to ensure there are no nodes left over that were derived from older versions of data but should no longer exist.
199+
200+
_For examples of other plugins creating transformation relationships, you can see the [`gatsby-transformer-remark` plugin](https://github.com/gatsbyjs/gatsby/blob/72077527b4acd3f2109ed5a2fcb780cddefee35a/packages/gatsby-transformer-remark/src/on-node-create.js#L39-L67) (from the above example) or the [`gatsby-transformer-sharp` plugin](https://github.com/gatsbyjs/gatsby/blob/1fb19f9ad16618acdac7eda33d295d8ceba7f393/packages/gatsby-transformer-sharp/src/on-node-create.js#L3-L25)._
201+
202+
#### Create new nodes from the derived data
175203

176204
In your updated `gatsby-node.js`, you'll then iterate through the parsed YAML content, using the helper function to transform each into a new node:
177205

@@ -227,6 +255,8 @@ async function onCreateNode({
227255
exports.onCreateNode = onCreateNode
228256
```
229257

258+
#### Query for the transformed data
259+
230260
Now you can query for your new nodes containing our transformed YAML data:
231261

232262
```graphql
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Creating First Class Gatsby Source Plugins
2+
3+
Create Gatsby plugins that leverage Gatsby's most impactful native features like remote image optimization, caching, customized GraphQL schemas and node relationships, and more.
4+
5+
This monorepo serves as an example of a site using a first class source plugin to pull in data from a Node.js API. It is meant to show the 3 pieces that work together when building a source plugin: the API, the site, and the source plugin.
6+
7+
## Setup
8+
9+
This monorepo uses yarn workspaces to manage the 3 indivdual projects:
10+
11+
- api: a Node.js API with in-memory data, and a Post and Author type, as well as support for subscriptions when Posts are mutated
12+
- example-site: a barebones Gatsby site that implements the source plugin
13+
- source-plugin: a plugin that uses several Gatsby APIs to source data from the API, create responsive/optimized images from remote locations, and link the nodes in the example site
14+
15+
To install dependencies for all projects run the install command in the root of the yarn workspace (which requires yarn to be installed):
16+
17+
```
18+
yarn install
19+
```
20+
21+
_Note: if you aren't using yarn, you can navigate into each of the 3 folders and run `npm install` instead_
22+
23+
Then you can run the api or example projects in separate terminal windows with the commands below.
24+
25+
For the API which runs at `localhost:4000`, use this command:
26+
27+
```
28+
yarn workspace api start
29+
```
30+
31+
And to run the example site with `gatsby develop` at `localhost:8000`, use this command:
32+
33+
```
34+
yarn workspace example-site develop
35+
```
36+
37+
Running the example site also runs the plugin because it is included in the site's config. You'll see output in the console for different functionality and then can open up the browser to `localhost:8000` to see the site.
38+
39+
## Developing and Experimenting
40+
41+
You can open up `localhost:4000` with the API running, which will load a GraphQL Playground, which is a GraphQL IDE (like GraphiQL, that Gatsby runs at `localhost:8000/___graphql`) for running queries and mutations on the data from the API.
42+
43+
You can test a query like this to see data returned:
44+
45+
```graphql
46+
query {
47+
posts {
48+
id
49+
slug
50+
}
51+
}
52+
```
53+
54+
This query will return the IDs for all posts in the API. You can copy one of these IDs and provide it as an argument to a mutation to update information about that post.
55+
56+
You can run 3 different mutations from the GraphQL Playground (at `localhost:4000`): `createPost`, `updatePost`, and `deletePost`. These methods would mimic CRUD operations happening on the API of the data source like a headless CMS. An example `updatePost` mutation is outlined below.
57+
58+
When you run a mutation on a post, a subscription event is published, which lets the plugin know it should respond and update nodes. The following mutation can be copied into the left side of the GraphQL playground so long as you replace "post-id" with a value returned for an ID from a query (like the one above).
59+
60+
```graphql
61+
mutation {
62+
updatePost(id: "post-id", description: "Some data!") {
63+
id
64+
slug
65+
description
66+
}
67+
}
68+
```
69+
70+
The website's homepage will update with any changes while the source plugin is subscribed to changes, which is when the `preview: true` is provided in the example site's `gatsby-config`.
71+
72+
You can also optionally listen for subscription events with this query in the playground which will display data when a mutation is run:
73+
74+
```graphql
75+
subscription {
76+
posts {
77+
id
78+
description
79+
}
80+
}
81+
```
82+
83+
A similar subscription is registered when the plugin is run, so you can also see subscription events logged when the plugin is running.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Example API
2+
3+
A small GraphQL server with in-memory data, powered by [graphql-yoga](https://github.com/graphcool/graphql-yoga) 🧘. See the root of the monorepo for details about running this API alongisde the `example-site` and `source-plugin`.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"name": "api",
3+
"description": "A simple GraphQL server example with in-memory data",
4+
"version": "1.0.0",
5+
"license": "MIT",
6+
"homepage": "https://general-repair.glitch.me",
7+
"author": {
8+
"name": "Risan Bagja Pradana",
9+
"email": "risanbagja@gmail.com",
10+
"url": "https://risan.io"
11+
},
12+
"main": "src/index.js",
13+
"repository": {
14+
"type": "git",
15+
"url": "git+https://github.com/risan/simple-graphql-server-example.git"
16+
},
17+
"bugs": {
18+
"url": "https://github.com/risan/simple-graphql-server-example/issues"
19+
},
20+
"keywords": [
21+
"graphql",
22+
"graphql-server",
23+
"graphql-yoga"
24+
],
25+
"scripts": {
26+
"start": "node src/index.js",
27+
"lint": "eslint *.js src",
28+
"lint-fix": "eslint *.js src --fix"
29+
},
30+
"dependencies": {
31+
"dotenv": "^5.0.1",
32+
"graphql-yoga": "^1.8.2",
33+
"uniqid": "^4.1.1"
34+
},
35+
"devDependencies": {
36+
"eslint": "^4.19.1",
37+
"eslint-config-airbnb-base": "^12.1.0",
38+
"eslint-config-prettier": "^2.9.0",
39+
"eslint-plugin-import": "^2.10.0",
40+
"eslint-plugin-prettier": "^2.6.0",
41+
"prettier": "^1.11.1"
42+
}
43+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
require("dotenv").config()
2+
const { GraphQLServer, PubSub } = require("graphql-yoga")
3+
const uniqid = require("uniqid")
4+
5+
const CREATED = "created"
6+
const UPDATED = "updated"
7+
const DELETED = "deleted"
8+
9+
const authors = [
10+
{
11+
id: 1,
12+
name: "Jay Gatsby",
13+
},
14+
{
15+
id: 2,
16+
name: "Daisy Buchanan",
17+
},
18+
]
19+
20+
const posts = [
21+
{
22+
id: uniqid(),
23+
slug: "hello-world",
24+
description: "Our first post on our site.",
25+
imgUrl: "https://images.unsplash.com/photo-1534432586043-ead5b99229fb",
26+
imgAlt: "Pug in a sweater",
27+
authorId: 1,
28+
},
29+
{
30+
id: uniqid(),
31+
slug: "company-vision",
32+
description: "Our vision for a welcoming company.",
33+
imgUrl: "https://images.unsplash.com/photo-1530041539828-114de669390e",
34+
imgAlt: "Pug in a rainjacket",
35+
authorId: 1,
36+
},
37+
{
38+
id: uniqid(),
39+
slug: "redesigning-our-logo",
40+
description: "What went into the new logo.",
41+
imgUrl: "https://images.unsplash.com/photo-1541364983171-a8ba01e95cfc",
42+
imgAlt: "Pug in glasses",
43+
authorId: 2,
44+
},
45+
]
46+
47+
const resolvers = {
48+
Query: {
49+
info: () => "A simple GraphQL server example with in-memory data.",
50+
posts: () => posts,
51+
authors: () => authors,
52+
},
53+
54+
Mutation: {
55+
createPost: (root, { slug, description }) => {
56+
const post = {
57+
id: uniqid(),
58+
slug,
59+
description,
60+
imgUrl: "https://images.unsplash.com/photo-1534432586043-ead5b99229fb",
61+
imgAlt: "pug in a sweater",
62+
authorId: 1,
63+
}
64+
65+
posts.push(post)
66+
pubsub.publish(CREATED, { posts: [{ status: CREATED, ...post }] })
67+
68+
return post
69+
},
70+
71+
updatePost: (root, { id, description }) => {
72+
const postIdx = posts.findIndex(p => id === p.id)
73+
74+
if (postIdx === null) {
75+
return null
76+
}
77+
78+
posts[postIdx] = { ...posts[postIdx], description }
79+
pubsub.publish(UPDATED, {
80+
posts: [{ status: UPDATED, ...posts[postIdx] }],
81+
})
82+
83+
return posts[postIdx]
84+
},
85+
86+
deletePost: (root, { id }) => {
87+
const postIdx = posts.findIndex(p => id === p.id)
88+
89+
if (postIdx === null) {
90+
return null
91+
}
92+
93+
const post = posts[postIdx]
94+
pubsub.publish(DELETED, {
95+
posts: [{ status: DELETED, ...posts[postIdx] }],
96+
})
97+
98+
posts.splice(postIdx, 1)
99+
100+
return post
101+
},
102+
},
103+
104+
Post: {
105+
id: root => root.id,
106+
slug: root => root.slug,
107+
description: root => root.description,
108+
author: root => authors.find(author => author.id === root.authorId),
109+
},
110+
111+
Author: {
112+
id: root => root.id,
113+
name: root => root.name,
114+
},
115+
116+
Subscription: {
117+
posts: {
118+
subscribe: (parent, args, { pubsub }) => {
119+
return pubsub.asyncIterator([CREATED, UPDATED, DELETED])
120+
},
121+
},
122+
},
123+
}
124+
125+
const pubsub = new PubSub()
126+
const server = new GraphQLServer({
127+
typeDefs: "./src/schema.graphql",
128+
resolvers,
129+
context: { pubsub },
130+
})
131+
132+
server.start(
133+
{
134+
port:
135+
(process.env.PORT ? parseInt(process.env.PORT, 10) : undefined) || 4000,
136+
},
137+
({ port }) => console.log(`🏃🏻‍ Server is running on port ${port}.`)
138+
)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
type Query {
2+
info: String!
3+
posts: [Post!]!
4+
authors: [Author!]!
5+
}
6+
7+
type Mutation {
8+
createPost(slug: String!, description: String!): Post!
9+
updatePost(id: ID!, description: String!): Post
10+
deletePost(id: ID!): Post
11+
}
12+
13+
type Post {
14+
id: ID!
15+
slug: String!
16+
description: String!
17+
imgUrl: String!
18+
imgAlt: String!
19+
author: Author!
20+
status: String
21+
}
22+
23+
type Author {
24+
id: ID!
25+
name: String!
26+
}
27+
28+
type Subscription {
29+
posts: [Post!]!
30+
}

0 commit comments

Comments
 (0)