From 01436718081520a223087c580efc6fc7d92b4060 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Mon, 14 Jan 2019 21:52:06 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=F0=9F=90=9B=20Fixed=20unexpected=20nu?= =?UTF-8?q?ll/undefined=20fields?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes #2 - This is the hacky workaround recommended by Gatsby - Ref: https://github.com/gatsbyjs/gatsby/issues/10856#issuecomment-451701011 - It depends on private API, but is better than needing a data stub! --- gatsby-node.js | 54 ++++++++++++++++- ghost-nodes.js | 28 +++++++-- ghost-schema.js | 124 +++++++++++++++++++++++++++++++++++++++ test/gatsby-node.test.js | 36 ++++++++---- test/utils/assertions.js | 12 +++- 5 files changed, 234 insertions(+), 20 deletions(-) create mode 100644 ghost-schema.js diff --git a/gatsby-node.js b/gatsby-node.js index cde6bfb..a2aa32c 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -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); @@ -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}); +}; diff --git a/ghost-nodes.js b/ghost-nodes.js index e82ff42..87f39d2 100644 --- a/ghost-nodes.js +++ b/ghost-nodes.js @@ -1,4 +1,5 @@ const createNodeHelpers = require('gatsby-node-helpers').default; +const schema = require('./ghost-schema'); const { createNodeFactory @@ -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 +}; diff --git a/ghost-schema.js b/ghost-schema.js new file mode 100644 index 0000000..966f47c --- /dev/null +++ b/ghost-schema.js @@ -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: '

This is a data schema stub for Gatsby.js and is not used. It must exist for builds to function

', + 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: '

This is a data schema stub for Gatsby.js and is not used. It must exist for builds to function

', + 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: '', + ghost_foot: '', + 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 +}; diff --git a/test/gatsby-node.test.js b/test/gatsby-node.test.js index cd2524a..5c0db47 100644 --- a/test/gatsby-node.test.js +++ b/test/gatsby-node.test.js @@ -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(() => { @@ -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(); }) diff --git a/test/utils/assertions.js b/test/utils/assertions.js index ac20036..8dd05c9 100644 --- a/test/utils/assertions.js +++ b/test/utils/assertions.js @@ -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... @@ -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]); + }); + } });