Skip to content

Commit

Permalink
✨🐛 Fixed unexpected null/undefined fields
Browse files Browse the repository at this point in the history
closes #2

- This is the hacky workaround recommended by Gatsby
- Ref: gatsbyjs/gatsby#10856 (comment)
- It depends on private API, but is better than needing a data stub!
  • Loading branch information
ErisDS committed Jan 14, 2019
1 parent 7726abb commit 0143671
Show file tree
Hide file tree
Showing 5 changed files with 234 additions and 20 deletions.
54 changes: 51 additions & 3 deletions gatsby-node.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,39 @@
const Promise = require('bluebird');
const ContentAPI = require('./content-api');
const {PostNode, PageNode, TagNode, AuthorNode, SettingsNode} = require('./ghost-nodes');
const {PostNode, PageNode, TagNode, AuthorNode, SettingsNode, fakeNodes} = require('./ghost-nodes');

exports.sourceNodes = ({actions}, configOptions) => {
/**
* Create Temporary Fake Nodes
* Refs: https://github.com/gatsbyjs/gatsby/issues/10856#issuecomment-451701011
* Ensures that Gatsby knows about every field in the Ghost schema
*/
const createTemporaryFakeNodes = ({emitter, actions}) => {
// Setup our temporary fake nodes
fakeNodes.forEach((node) => {
// createTemporaryFakeNodes is called twice. The second time, the node already has an owner
// This triggers an error, so we clean the node before trying again
delete node.internal.owner;
actions.createNode(node);
});

const onSchemaUpdate = () => {
// Destroy our temporary fake nodes
fakeNodes.forEach((node) => {
actions.deleteNode({node});
});
emitter.off(`SET_SCHEMA`, onSchemaUpdate);
};

// Use a Gatsby internal API to cleanup our Fake Nodes
emitter.on(`SET_SCHEMA`, onSchemaUpdate);
};

/**
* Create Live Ghost Nodes
* Uses the Ghost Content API to fetch all posts, pages, tags, authors and settings
* Creates nodes for each record, so that they are all available to Gatsby
*/
const createLiveGhostNodes = ({actions}, configOptions) => {
const {createNode} = actions;

const api = ContentAPI.configure(configOptions);
Expand Down Expand Up @@ -40,7 +71,24 @@ exports.sourceNodes = ({actions}, configOptions) => {
});
});

const fetchSettings = api.settings.browse().then(setting => createNode(SettingsNode(setting)));
const fetchSettings = api.settings.browse().then((setting) => {
setting.id = 1;
createNode(SettingsNode(setting));
});

return Promise.all([fetchPosts, fetchPages, fetchTags, fetchAuthors, fetchSettings]);
};

// Standard way to create nodes
exports.sourceNodes = ({emitter, actions}, configOptions) => {
// These temporary nodes ensure that Gatsby knows about every field in the Ghost Schema
createTemporaryFakeNodes({emitter, actions});

// Go and fetch live data, and populate the nodes
return createLiveGhostNodes({actions}, configOptions);
};

// Secondary point in build where we have to create fake Nodes
exports.onPreExtractQueries = ({emitter, actions}) => {
createTemporaryFakeNodes({emitter, actions});
};
28 changes: 23 additions & 5 deletions ghost-nodes.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const createNodeHelpers = require('gatsby-node-helpers').default;
const schema = require('./ghost-schema');

const {
createNodeFactory
Expand All @@ -12,8 +13,25 @@ const TAG = 'Tag';
const AUTHOR = 'Author';
const SETTINGS = 'Settings';

module.exports.PostNode = createNodeFactory(POST);
module.exports.PageNode = createNodeFactory(PAGE);
module.exports.TagNode = createNodeFactory(TAG);
module.exports.AuthorNode = createNodeFactory(AUTHOR);
module.exports.SettingsNode = createNodeFactory(SETTINGS);
const PostNode = createNodeFactory(POST);
const PageNode = createNodeFactory(PAGE);
const TagNode = createNodeFactory(TAG);
const AuthorNode = createNodeFactory(AUTHOR);
const SettingsNode = createNodeFactory(SETTINGS);

const fakeNodes = [
PostNode(schema.post),
PageNode(schema.page),
TagNode(schema.tag),
AuthorNode(schema.author),
SettingsNode(schema.settings)
];

module.exports = {
PostNode,
PageNode,
TagNode,
AuthorNode,
SettingsNode,
fakeNodes
};
124 changes: 124 additions & 0 deletions ghost-schema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
const tag = {
id: 'a6fd74f5667245d9b678429bc35febbf',
name: 'Data schema primary',
slug: 'data-schema',
url: 'https://demo.ghost.io/tag/data-schema-tag/',
description: 'This is a data schema stub for Gatsby.js and is not used. It must exist for builds to function',
feature_image: 'https://images.unsplash.com/photo-1532630571098-79a3d222b00d?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ&s=a88235003c40468403f936719134519d',
visibility: 'public',
meta_title: 'Data schema primary',
meta_description: 'This is a data schema stub for Gatsby.js and is not used. It must exist for builds to function',
count: {posts: 1}
};
const author = {
id: '179e06da7ae846929bb30f19f3e82ecb',
name: 'Data Schema Author',
slug: 'data-schema-author',
url: 'https://demo.ghost.io/author/data-schema-author/',
profile_image: 'https://casper.ghost.org/v2.0.0/images/ghost.png',
cover_image: 'https://images.unsplash.com/photo-1532630571098-79a3d222b00d?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ&s=a88235003c40468403f936719134519d',
bio: 'This is a data schema stub for Gatsby.js and is not used. It must exist for builds to function',
website: 'https://ghost.org',
location: 'The Internet',
facebook: 'ghost',
twitter: '@tryghost',
meta_title: 'Data Schema Author',
meta_description: 'This is a data schema stub for Gatsby.js and is not used. It must exist for builds to function',
count: {posts: 1}
};

const post = {
id: '5bbafb3cb7ec4135e42fce56',
uuid: '472cd89d-953c-42ad-ae18-974b35444d03',
title: 'Data schema',
slug: 'data-schema',
url: 'https://demo.ghost.io/data-schema/',
mobiledoc: '{"version":"0.3.1","atoms":[],"cards":[],"markups":[],"sections":[[1,"p",[[0,[],0,"This is a data schema stub for Gatsby.js and is not used. It must exist for builds to function"]]]]}',
html: '<p>This is a data schema stub for Gatsby.js and is not used. It must exist for builds to function</p>',
comment_id: '5bb75b5a37361dae192eff1b',
plaintext: 'This is a data schema stub for Gatsby.js and is not used. It must exist for\nbuilds to function',
feature_image: 'https://images.unsplash.com/photo-1532630571098-79a3d222b00d?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ&s=a88235003c40468403f936719134519d',
featured: true,
page: false,
meta_title: 'Data schema',
meta_description: 'This is a data schema stub for Gatsby.js and is not used. It must exist for builds to function',
created_at: '2018-12-04T13:59:08.000+00:00',
updated_at: '2018-12-04T13:59:08.000+00:00',
published_at: '2018-12-04T13:59:14.000+00:00',
custom_excerpt: 'This is a data schema stub for Gatsby.js and is not used. It must exist for builds to function',
excerpt: 'This is a data schema stub for Gatsby.js and is not used. It must exist for builds to function',
codeinjection_head: '.some-class {\n}',
codeinjection_foot: '.some-class {\n}',
og_image: 'https://images.unsplash.com/photo-1532630571098-79a3d222b00d?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ&s=a88235003c40468403f936719134519d',
og_title: 'Data schema',
og_description: 'This is a data schema stub for Gatsby.js and is not used. It must exist for builds to function',
twitter_image: 'https://images.unsplash.com/photo-1532630571098-79a3d222b00d?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ&s=a88235003c40468403f936719134519d',
twitter_title: 'Data schema',
twitter_description: 'This is a data schema stub for Gatsby.js and is not used. It must exist for builds to function',
primary_author: author,
primary_tag: tag,
authors: [author],
tags: [tag]
};
const page = {
id: '5bbafb3cb7ec4135e42fce57',
uuid: '472cd89d-953c-42ad-ae18-974b35444d04',
title: 'Data schema',
slug: 'data-schema-page',
url: 'https://demo.ghost.io/data-schema-page/',
mobiledoc: '{"version":"0.3.1","atoms":[],"cards":[],"markups":[],"sections":[[1,"p",[[0,[],0,"This is a data schema stub for Gatsby.js and is not used. It must exist for builds to function"]]]]}',
html: '<p>This is a data schema stub for Gatsby.js and is not used. It must exist for builds to function</p>',
comment_id: '5bb75b5a37361dae192eff1b',
plaintext: 'This is a data schema stub for Gatsby.js and is not used. It must exist for\nbuilds to function',
feature_image: 'https://images.unsplash.com/photo-1532630571098-79a3d222b00d?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ&s=a88235003c40468403f936719134519d',
featured: false,
page: true,
meta_title: 'Data schema',
meta_description: 'This is a data schema stub for Gatsby.js and is not used. It must exist for builds to function',
created_at: '2018-12-04T13:59:08.000+00:00',
updated_at: '2018-12-04T13:59:08.000+00:00',
published_at: '2018-12-04T13:59:14.000+00:00',
custom_excerpt: 'This is a data schema stub for Gatsby.js and is not used. It must exist for builds to function',
excerpt: 'This is a data schema stub for Gatsby.js and is not used. It must exist for builds to function',
codeinjection_head: '.some-class {\n}',
codeinjection_foot: '.some-class {\n}',
og_image: 'https://images.unsplash.com/photo-1532630571098-79a3d222b00d?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ&s=a88235003c40468403f936719134519d',
og_title: 'Data schema',
og_description: 'This is a data schema stub for Gatsby.js and is not used. It must exist for builds to function',
twitter_image: 'https://images.unsplash.com/photo-1532630571098-79a3d222b00d?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ&s=a88235003c40468403f936719134519d',
twitter_title: 'Data schema',
twitter_description: 'This is a data schema stub for Gatsby.js and is not used. It must exist for builds to function',
custom_template: 'post.hbs',
primary_author: author,
primary_tag: tag,
authors: [author],
tags: [tag]
};

const settings = {
title: 'Ghost',
description: 'The professional publishing platform',
logo: 'https://static.ghost.org/v1.0.0/images/ghost-logo.svg',
icon: 'https://static.ghost.org/favicon.ico',
cover_image: 'https://static.ghost.org/v1.0.0/images/blog-cover.jpg',
facebook: 'ghost',
twitter: 'tryghost',
lang: 'en',
timezone: 'Etc/UTC',
ghost_head: '<script></script>',
ghost_foot: '<style></style>',
navigation: [
{label: 'Home', url: '/'},
{label: 'Tag', url: '/tag/getting-started/'},
{label: 'Author', url: '/author/ghost/'},
{label: 'Help', url: 'https://help.ghost.org'}
]
};

module.exports = {
post,
page,
tag,
author,
settings
};
36 changes: 25 additions & 11 deletions test/gatsby-node.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const testUtils = require('./utils');
const ContentAPI = require('../content-api');
const gatsbyNode = require('../gatsby-node');
const ghostSchema = require('../ghost-schema');

describe('Basic Functionality', function () {
beforeEach(() => {
Expand All @@ -11,24 +12,37 @@ describe('Basic Functionality', function () {
sinon.restore();
});

it('Gatsby Node does roughly the right thing', function (done) {
it('Gatsby Node is able to create fake and real nodes', function (done) {
const createNode = sinon.stub();
const deleteNode = sinon.stub();
const emitter = {
on: sinon.stub().callsArg(1),
off: sinon.stub()
};

gatsbyNode
.sourceNodes({actions: {createNode}}, {})
.sourceNodes({actions: {createNode, deleteNode}, emitter}, {})
.then(() => {
createNode.callCount.should.eql(7);
createNode.callCount.should.eql(12);
deleteNode.callCount.should.eql(5);

const getFirstArg = call => createNode.getCall(call).args[0];

// Check that we get the right type of node created
getFirstArg(0).should.be.a.ValidGatsbyNode('GhostPost');
getFirstArg(1).should.be.a.ValidGatsbyNode('GhostPage');
getFirstArg(2).should.be.a.ValidGatsbyNode('GhostTag');
getFirstArg(3).should.be.a.ValidGatsbyNode('GhostTag');
getFirstArg(4).should.be.a.ValidGatsbyNode('GhostAuthor');
getFirstArg(5).should.be.a.ValidGatsbyNode('GhostAuthor');
getFirstArg(6).should.be.a.ValidGatsbyNode('GhostSettings');
// Check Fake Nodes against schema
getFirstArg(0).should.be.a.ValidGatsbyNode('GhostPost', ghostSchema.post);
getFirstArg(1).should.be.a.ValidGatsbyNode('GhostPage', ghostSchema.page);
getFirstArg(2).should.be.a.ValidGatsbyNode('GhostTag', ghostSchema.tag);
getFirstArg(3).should.be.a.ValidGatsbyNode('GhostAuthor', ghostSchema.author);
getFirstArg(4).should.be.a.ValidGatsbyNode('GhostSettings', ghostSchema.settings);

// Check Real Nodes are created
getFirstArg(5).should.be.a.ValidGatsbyNode('GhostPost');
getFirstArg(6).should.be.a.ValidGatsbyNode('GhostPage');
getFirstArg(7).should.be.a.ValidGatsbyNode('GhostTag');
getFirstArg(8).should.be.a.ValidGatsbyNode('GhostTag');
getFirstArg(9).should.be.a.ValidGatsbyNode('GhostAuthor');
getFirstArg(10).should.be.a.ValidGatsbyNode('GhostAuthor');
getFirstArg(11).should.be.a.ValidGatsbyNode('GhostSettings');

done();
})
Expand Down
12 changes: 11 additions & 1 deletion test/utils/assertions.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Add any custom assertions to this file.
*/

should.Assertion.add('ValidGatsbyNode', function (type) {
should.Assertion.add('ValidGatsbyNode', function (type, schema) {
this.params = {operator: 'to be a valid Gatsby Node'};

// All Gatsby Nodes look like this...
Expand All @@ -13,4 +13,14 @@ should.Assertion.add('ValidGatsbyNode', function (type) {

// Assert our type
this.obj.internal.type.should.eql(type);

if (schema) {
Object.keys(schema).forEach((key) => {
// Gatsby overwrites the ID
if (key === 'id') {
return;
}
this.obj.should.have.property(key, schema[key]);
});
}
});

0 comments on commit 0143671

Please sign in to comment.