diff --git a/e2e-tests/contentful/schema.gql b/e2e-tests/contentful/schema.gql index 2a8e30403f33d..7a501577d38cb 100644 --- a/e2e-tests/contentful/schema.gql +++ b/e2e-tests/contentful/schema.gql @@ -1,4 +1,4 @@ -### Type definitions saved at 2021-05-21T17:02:49.951Z ### +### Type definitions saved at 2021-09-25T11:33:25.217Z ### type File implements Node @dontInfer { sourceInstanceName: String! @@ -96,43 +96,23 @@ type SitePage implements Node @dontInfer { internalComponentName: String! componentChunkName: String! matchPath: String + pageContext: JSON } -type MarkdownHeading { - id: String - value: String - depth: Int -} - -enum MarkdownHeadingLevels { - h1 - h2 - h3 - h4 - h5 - h6 -} - -enum MarkdownExcerptFormats { - PLAIN - HTML - MARKDOWN -} - -type MarkdownWordCount { - paragraphs: Int - sentences: Int - words: Int -} - -type MarkdownRemark implements Node @childOf(mimeTypes: ["text/markdown", "text/x-markdown"], types: ["contentfulTextLongPlainTextNode", "contentfulTextLongMarkdownTextNode", "contentfulTextLongLocalizedTextNode"]) @derivedTypes @dontInfer { - frontmatter: MarkdownRemarkFrontmatter - excerpt: String - rawMarkdownBody: String +type SitePlugin implements Node @dontInfer { + resolve: String + name: String + version: String + nodeAPIs: [String] + browserAPIs: [String] + ssrAPIs: [String] + pluginFilepath: String + pluginOptions: JSON + packageJson: JSON } -type MarkdownRemarkFrontmatter { - title: String +type SiteBuildMetadata implements Node @dontInfer { + buildTime: Date @dateformat } interface ContentfulEntry implements Node { @@ -184,15 +164,16 @@ type ContentfulNumber implements ContentfulReference & ContentfulEntry & Node @d contentful_id: String! node_locale: String! title: String - decimal: Float + integerLocalized: Int spaceId: String createdAt: Date @dateformat updatedAt: Date @dateformat sys: ContentfulNumberSys + metadata: ContentfulNumberMetadata + decimal: Float integer: Int content_reference: [ContentfulContentReference] @link(by: "id", from: "content reference___NODE") @proxy(from: "content reference___NODE") decimalLocalized: Float - integerLocalized: Int } type ContentfulNumberSys @derivedTypes { @@ -211,22 +192,31 @@ type ContentfulNumberSysContentTypeSys { id: String } +type ContentfulNumberMetadata { + tags: [ContentfulTag] @link(by: "id", from: "tags___NODE") +} + +type ContentfulTag implements Node @dontInfer { + name: String! + contentful_id: String! +} + type ContentfulContentReference implements ContentfulReference & ContentfulEntry & Node @derivedTypes @dontInfer { contentful_id: String! node_locale: String! title: String - manyLocalized: [ContentfulNumberContentfulTextUnion] @link(by: "id", from: "manyLocalized___NODE") + one: ContentfulContentReferenceContentfulTextUnion @link(by: "id", from: "one___NODE") + content_reference: [ContentfulContentReference] @link(by: "id", from: "content reference___NODE") @proxy(from: "content reference___NODE") spaceId: String createdAt: Date @dateformat updatedAt: Date @dateformat sys: ContentfulContentReferenceSys oneLocalized: ContentfulNumber @link(by: "id", from: "oneLocalized___NODE") - one: ContentfulContentReferenceContentfulTextUnion @link(by: "id", from: "one___NODE") - content_reference: [ContentfulContentReference] @link(by: "id", from: "content reference___NODE") @proxy(from: "content reference___NODE") many: [ContentfulContentReferenceContentfulNumberContentfulTextUnion] @link(by: "id", from: "many___NODE") + manyLocalized: [ContentfulNumberContentfulTextUnion] @link(by: "id", from: "manyLocalized___NODE") } -union ContentfulNumberContentfulTextUnion = ContentfulNumber | ContentfulText +union ContentfulContentReferenceContentfulTextUnion = ContentfulContentReference | ContentfulText type ContentfulContentReferenceSys @derivedTypes { type: String @@ -244,10 +234,10 @@ type ContentfulContentReferenceSysContentTypeSys { id: String } -union ContentfulContentReferenceContentfulTextUnion = ContentfulContentReference | ContentfulText - union ContentfulContentReferenceContentfulNumberContentfulTextUnion = ContentfulContentReference | ContentfulNumber | ContentfulText +union ContentfulNumberContentfulTextUnion = ContentfulNumber | ContentfulText + type ContentfulText implements ContentfulReference & ContentfulEntry & Node @derivedTypes @dontInfer { contentful_id: String! node_locale: String! @@ -257,12 +247,12 @@ type ContentfulText implements ContentfulReference & ContentfulEntry & Node @der createdAt: Date @dateformat updatedAt: Date @dateformat sys: ContentfulTextSys - shortLocalized: String longMarkdown: contentfulTextLongMarkdownTextNode @link(by: "id", from: "longMarkdown___NODE") + shortLocalized: String longPlain: contentfulTextLongPlainTextNode @link(by: "id", from: "longPlain___NODE") + shortList: [String] short: String content_reference: [ContentfulContentReference] @link(by: "id", from: "content reference___NODE") @proxy(from: "content reference___NODE") - shortList: [String] } type contentfulTextLongLocalizedTextNode implements Node @derivedTypes @childOf(types: ["ContentfulText"]) @dontInfer { @@ -312,14 +302,14 @@ type ContentfulMediaReference implements ContentfulReference & ContentfulEntry & contentful_id: String! node_locale: String! title: String - manyLocalized: [ContentfulAsset] @link(by: "id", from: "manyLocalized___NODE") + one: ContentfulAsset @link(by: "id", from: "one___NODE") spaceId: String createdAt: Date @dateformat updatedAt: Date @dateformat sys: ContentfulMediaReferenceSys - many: [ContentfulAsset] @link(by: "id", from: "many___NODE") oneLocalized: ContentfulAsset @link(by: "id", from: "oneLocalized___NODE") - one: ContentfulAsset @link(by: "id", from: "one___NODE") + many: [ContentfulAsset] @link(by: "id", from: "many___NODE") + manyLocalized: [ContentfulAsset] @link(by: "id", from: "manyLocalized___NODE") } type ContentfulMediaReferenceSys @derivedTypes { @@ -370,14 +360,14 @@ type ContentfulDate implements ContentfulReference & ContentfulEntry & Node @der contentful_id: String! node_locale: String! title: String - dateLocalized: Date @dateformat + dateTimeTimezone: Date @dateformat spaceId: String createdAt: Date @dateformat updatedAt: Date @dateformat sys: ContentfulDateSys - dateTime: Date @dateformat - dateTimeTimezone: Date @dateformat date: Date @dateformat + dateLocalized: Date @dateformat + dateTime: Date @dateformat } type ContentfulDateSys @derivedTypes { @@ -409,8 +399,8 @@ type ContentfulLocation implements ContentfulReference & ContentfulEntry & Node } type ContentfulLocationLocationLocalized { - lat: Float lon: Float + lat: Float } type ContentfulLocationSys @derivedTypes { @@ -438,25 +428,39 @@ type ContentfulJson implements ContentfulReference & ContentfulEntry & Node @der contentful_id: String! node_locale: String! title: String - jsonLocalized: contentfulJsonJsonLocalizedJsonNode @link(by: "id", from: "jsonLocalized___NODE") + json: contentfulJsonJsonJsonNode @link(by: "id", from: "json___NODE") spaceId: String createdAt: Date @dateformat updatedAt: Date @dateformat sys: ContentfulJsonSys - json: contentfulJsonJsonJsonNode @link(by: "id", from: "json___NODE") + jsonLocalized: contentfulJsonJsonLocalizedJsonNode @link(by: "id", from: "jsonLocalized___NODE") } -type contentfulJsonJsonLocalizedJsonNode implements Node @derivedTypes @childOf(types: ["ContentfulJson"]) @dontInfer { +type contentfulJsonJsonJsonNode implements Node @derivedTypes @childOf(types: ["ContentfulJson"]) @dontInfer { age: Int city: String name: String - sys: contentfulJsonJsonLocalizedJsonNodeSys + sys: contentfulJsonJsonJsonNodeSys + Actors: [contentfulJsonJsonJsonNodeActors] } -type contentfulJsonJsonLocalizedJsonNodeSys { +type contentfulJsonJsonJsonNodeSys { type: String } +type contentfulJsonJsonJsonNodeActors { + age: Int + name: String + wife: String + photo: String + weight: Float + Born_At: String @proxy(from: "Born At") + children: [String] + Birthdate: String + hasChildren: Boolean + hasGreyHair: Boolean +} + type ContentfulJsonSys @derivedTypes { type: String revision: Int @@ -473,28 +477,14 @@ type ContentfulJsonSysContentTypeSys { id: String } -type contentfulJsonJsonJsonNode implements Node @derivedTypes @childOf(types: ["ContentfulJson"]) @dontInfer { - Actors: [contentfulJsonJsonJsonNodeActors] - sys: contentfulJsonJsonJsonNodeSys +type contentfulJsonJsonLocalizedJsonNode implements Node @derivedTypes @childOf(types: ["ContentfulJson"]) @dontInfer { name: String age: Int city: String + sys: contentfulJsonJsonLocalizedJsonNodeSys } -type contentfulJsonJsonJsonNodeActors { - name: String - age: Int - Born_At: String @proxy(from: "Born At") - Birthdate: String - photo: String - wife: String - weight: Float - hasChildren: Boolean - hasGreyHair: Boolean - children: [String] -} - -type contentfulJsonJsonJsonNodeSys { +type contentfulJsonJsonLocalizedJsonNodeSys { type: String } @@ -502,21 +492,21 @@ type ContentfulRichText implements ContentfulReference & ContentfulEntry & Node contentful_id: String! node_locale: String! title: String - richTextValidated: ContentfulRichTextRichTextValidated + richText: ContentfulRichTextRichText spaceId: String createdAt: Date @dateformat updatedAt: Date @dateformat sys: ContentfulRichTextSys + richTextValidated: ContentfulRichTextRichTextValidated richTextLocalized: ContentfulRichTextRichTextLocalized - richText: ContentfulRichTextRichText } -type ContentfulRichTextRichTextValidated { +type ContentfulRichTextRichText { raw: String - references: [ContentfulAssetContentfulLocationContentfulNumberContentfulTextUnion] @link(by: "id", from: "references___NODE") + references: [ContentfulAssetContentfulContentReferenceContentfulLocationContentfulTextUnion] @link(by: "id", from: "references___NODE") } -union ContentfulAssetContentfulLocationContentfulNumberContentfulTextUnion = ContentfulAsset | ContentfulLocation | ContentfulNumber | ContentfulText +union ContentfulAssetContentfulContentReferenceContentfulLocationContentfulTextUnion = ContentfulAsset | ContentfulContentReference | ContentfulLocation | ContentfulText type ContentfulRichTextSys @derivedTypes { type: String @@ -534,22 +524,59 @@ type ContentfulRichTextSysContentTypeSys { id: String } -type ContentfulRichTextRichTextLocalized { +type ContentfulRichTextRichTextValidated { raw: String + references: [ContentfulAssetContentfulLocationContentfulNumberContentfulTextUnion] @link(by: "id", from: "references___NODE") } -type ContentfulRichTextRichText { +union ContentfulAssetContentfulLocationContentfulNumberContentfulTextUnion = ContentfulAsset | ContentfulLocation | ContentfulNumber | ContentfulText + +type ContentfulRichTextRichTextLocalized { raw: String - references: [ContentfulAssetContentfulContentReferenceContentfulLocationContentfulTextUnion] @link(by: "id", from: "references___NODE") } -union ContentfulAssetContentfulContentReferenceContentfulLocationContentfulTextUnion = ContentfulAsset | ContentfulContentReference | ContentfulLocation | ContentfulText - type ContentfulValidatedContentReference implements ContentfulReference & ContentfulEntry & Node @dontInfer { contentful_id: String! node_locale: String! } +type MarkdownHeading { + id: String + value: String + depth: Int +} + +enum MarkdownHeadingLevels { + h1 + h2 + h3 + h4 + h5 + h6 +} + +enum MarkdownExcerptFormats { + PLAIN + HTML + MARKDOWN +} + +type MarkdownWordCount { + paragraphs: Int + sentences: Int + words: Int +} + +type MarkdownRemark implements Node @childOf(mimeTypes: ["text/markdown", "text/x-markdown"], types: ["contentfulTextLongPlainTextNode", "contentfulTextLongMarkdownTextNode", "contentfulTextLongLocalizedTextNode"]) @derivedTypes @dontInfer { + frontmatter: MarkdownRemarkFrontmatter + excerpt: String + rawMarkdownBody: String +} + +type MarkdownRemarkFrontmatter { + title: String +} + type ContentfulContentType implements Node @derivedTypes @dontInfer { name: String displayField: String diff --git a/e2e-tests/contentful/snapshots.js b/e2e-tests/contentful/snapshots.js index eebeb902df067..2207ca7c22e03 100644 --- a/e2e-tests/contentful/snapshots.js +++ b/e2e-tests/contentful/snapshots.js @@ -28,7 +28,7 @@ module.exports = { }, "rich-text": { "rich-text: All Features": { - "1": "
\n

Rich Text: All Features

\n

The European languages

\n

are members of the same family. Their separate existence is a myth. For:

\n \n

Europe uses the same vocabulary.

\n
\n
\"\"\n\n \n \n \n
\n

\n
\n

The languages only differ in:

\n
    \n
  1. \n

    their grammar

    \n
  2. \n
  3. \n

    their pronunciation

    \n
  4. \n
  5. \n

    their most common words

    \n
  6. \n
  7. \n

    [Inline-ContentfulText]\n Text: Short\n :\n The quick brown fox jumps over the lazy dog.

    \n
  8. \n
\n

Everyone realizes why a new common language would be desirable: one could\n refuse to pay expensive translators.

\n

{\n \"userId\": 1,\n \"id\": 1,\n \"title\": \"delectus aut autem\",\n \"completed\": false\n }

\n

To achieve this, it would be necessary to have uniform grammar,\n pronunciation and more common words.

\n

[ContentfulLocation] Lat:\n 52.51627\n , Long:\n 13.3777

\n
\n

If several languages coalesce, the grammar of the resulting language is\n more simple and regular than that of the individual languages.

\n
\n

The new common language will be more simple and regular than the existing\n European languages. It will be as simple as Occidental; in fact, it will be\n

\n
\n
" + "1": "
\n

Rich Text: All Features

\n

The European languages

\n

are members of the same family. Their separate existence is a myth. For:

\n \n

Europe uses the same vocabulary.

\n
\n
\"\"\n\n \n \n \n
\n

\n
\n

The languages only differ in:

\n
    \n
  1. \n

    their grammar

    \n
  2. \n
  3. \n

    their pronunciation

    \n
  4. \n
  5. \n

    their most common words

    \n
  6. \n
  7. \n

    [Inline-ContentfulText]\n Text: Short\n :\n The quick brown fox jumps over the lazy dog.

    \n
  8. \n
\n

Everyone realizes why a new common language would be desirable: one could\n refuse to pay expensive translators.

\n

{\n \"userId\": 1,\n \"id\": 1,\n \"title\": \"delectus aut autem\",\n \"completed\": false\n }

\n

To achieve this, it would be necessary to have uniform grammar,\n pronunciation and more common words.

\n

[ContentfulLocation] Lat:\n 52.51627\n , Long:\n 13.3777

\n
\n

If several languages coalesce, the grammar of the resulting language is\n more simple and regular than that of the individual languages.

\n
\n

The new common language will be more simple and regular than the existing\n European languages. It will be as simple as Occidental; in fact, it will be\n

\n
\n
" }, "rich-text: Basic": { "1": "
\n

Rich Text: Basic

\n

The European languages

\n

are members of the same family. Their separate existence is a myth. For:

\n \n

Europe uses the same vocabulary.

\n
\n

The languages only differ in:

\n
    \n
  1. \n

    their grammar

    \n
  2. \n
  3. \n

    their pronunciation

    \n
  4. \n
  5. \n

    their most common words

    \n
  6. \n
\n

Everyone realizes why a new common language would be desirable: one could\n refuse to pay expensive translators.

\n

{\n \"userId\": 1,\n \"id\": 1,\n \"title\": \"delectus aut autem\",\n \"completed\": false\n }

\n

To achieve this, it would be necessary to have uniform grammar,\n pronunciation and more common words.

\n
\n

If several languages coalesce, the grammar of the resulting language is\n more simple and regular than that of the individual languages.

\n
\n

The new common language will be more simple and regular than the existing\n European languages. It will be as simple as Occidental; in fact, it will be\n

\n
\n
" @@ -37,7 +37,7 @@ module.exports = { "1": "
\n

Rich Text: Embedded Entry

\n

Embedded Entry

\n

[ContentfulText]\n The quick brown fox jumps over the lazy dog.

\n

\n

\n
\n
" }, "rich-text: Embedded Asset": { - "1": "
\n

Rich Text: Embedded asset

\n

Embedded Asset

\n
\n
\n \n \n \n
\n

\n

\n

\n
\n
" + "1": "
\n

Rich Text: Embedded asset

\n

Embedded Asset

\n
\n
\n \n \n \n
\n

\n

\n

\n
\n
" }, "rich-text: Embedded Entry With Deep Reference Loop": { "1": "
\n

Rich Text: Embedded entry with deep reference loop

\n

Embedded entry with deep reference loop

\n

[ContentfulReference]\n Content Reference: Many (2nd level loop)\n : [\n Number: Integer, Text: Short, Content Reference: One (Loop A ->\n B)\n ]

\n

\n

\n
\n
" diff --git a/packages/gatsby-source-contentful/src/__fixtures__/restricted-content-type.js b/packages/gatsby-source-contentful/src/__fixtures__/restricted-content-type.js index 86c5f7a2a02d2..8adda16238442 100644 --- a/packages/gatsby-source-contentful/src/__fixtures__/restricted-content-type.js +++ b/packages/gatsby-source-contentful/src/__fixtures__/restricted-content-type.js @@ -1,3 +1,45 @@ +exports.contentTypeItems = () => [ + { + sys: { + space: { + sys: { + type: `Link`, + linkType: `Space`, + id: `uzfinxahlog0`, + contentful_id: `uzfinxahlog0`, + }, + }, + id: `reference`, + type: `ContentType`, + createdAt: `2020-06-03T14:17:18.696Z`, + updatedAt: `2020-06-03T14:17:18.696Z`, + environment: { + sys: { + id: `master`, + type: `Link`, + linkType: `Environment`, + }, + }, + revision: 1, + contentful_id: `person`, + }, + displayField: `name`, + name: `Reference`, + description: ``, + fields: [ + { + id: `name`, + name: `Name`, + type: `Symbol`, + localized: false, + required: true, + disabled: false, + omitted: false, + }, + ], + }, +] + exports.initialSync = () => { return { currentSyncData: { @@ -7,47 +49,6 @@ exports.initialSync = () => { deletedAssets: [], nextSyncToken: `12345`, }, - contentTypeItems: [ - { - sys: { - space: { - sys: { - type: `Link`, - linkType: `Space`, - id: `uzfinxahlog0`, - contentful_id: `uzfinxahlog0`, - }, - }, - id: `reference`, - type: `ContentType`, - createdAt: `2020-06-03T14:17:18.696Z`, - updatedAt: `2020-06-03T14:17:18.696Z`, - environment: { - sys: { - id: `master`, - type: `Link`, - linkType: `Environment`, - }, - }, - revision: 1, - contentful_id: `person`, - }, - displayField: `name`, - name: `Reference`, - description: ``, - fields: [ - { - id: `name`, - name: `Name`, - type: `Symbol`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - ], - }, - ], defaultLocale: `en-US`, locales: [ { diff --git a/packages/gatsby-source-contentful/src/__fixtures__/rich-text-data.js b/packages/gatsby-source-contentful/src/__fixtures__/rich-text-data.js index 6a0200de2dc4e..0be1eaa13ee55 100644 --- a/packages/gatsby-source-contentful/src/__fixtures__/rich-text-data.js +++ b/packages/gatsby-source-contentful/src/__fixtures__/rich-text-data.js @@ -1,3 +1,61 @@ +exports.contentTypeItems = () => [ + { + sys: { + space: { + sys: { + type: `Link`, + linkType: `Space`, + id: `ahntqop9oi7x`, + }, + }, + id: `page`, + type: `ContentType`, + createdAt: `2020-10-16T11:43:48.221Z`, + updatedAt: `2020-10-16T11:44:25.392Z`, + environment: { + sys: { + id: `master`, + type: `Link`, + linkType: `Environment`, + }, + }, + revision: 2, + }, + displayField: `title`, + name: `Page`, + description: ``, + fields: [ + { + id: `title`, + name: `Title`, + type: `Symbol`, + localized: false, + required: false, + disabled: false, + omitted: false, + }, + { + id: `slug`, + name: `Slug`, + type: `Symbol`, + localized: false, + required: true, + disabled: false, + omitted: false, + }, + { + id: `content`, + name: `Content`, + type: `RichText`, + localized: true, + required: false, + disabled: false, + omitted: false, + }, + ], + }, +] + exports.initialSync = () => { return { currentSyncData: { @@ -692,63 +750,7 @@ exports.initialSync = () => { deletedAssets: [], nextSyncToken: `FEnChMOBwr1Yw4TCqsK2LcKpCH3CjsORIyLDrGbDtgozw6xreMKCwpjCtlxATw3CmxolIsOxF10EMMOGCXM-IFrCrhc0LUPDvkjDkms7w5gLw4sqw4_CvxsiZMOFFsOawpM8R8OVPAhMJ8O1w6zCmg`, }, - contentTypeItems: [ - { - sys: { - space: { - sys: { - type: `Link`, - linkType: `Space`, - id: `ahntqop9oi7x`, - }, - }, - id: `page`, - type: `ContentType`, - createdAt: `2020-10-16T11:43:48.221Z`, - updatedAt: `2020-10-16T11:44:25.392Z`, - environment: { - sys: { - id: `master`, - type: `Link`, - linkType: `Environment`, - }, - }, - revision: 2, - }, - displayField: `title`, - name: `Page`, - description: ``, - fields: [ - { - id: `title`, - name: `Title`, - type: `Symbol`, - localized: false, - required: false, - disabled: false, - omitted: false, - }, - { - id: `slug`, - name: `Slug`, - type: `Symbol`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `content`, - name: `Content`, - type: `RichText`, - localized: true, - required: false, - disabled: false, - omitted: false, - }, - ], - }, - ], + defaultLocale: `en-US`, locales: [ { @@ -792,6 +794,8 @@ exports.initialSync = () => { tagItems: [], } } + +// @todo this fixture is unused exports.deleteLinkedPage = () => { return { currentSyncData: { @@ -826,63 +830,6 @@ exports.deleteLinkedPage = () => { deletedAssets: [], nextSyncToken: `FEnChMOBwr1Yw4TCqsK2LcKpCH3CjsORIyLDrGbDtgozw6xreMKCwpjCtlxATw3CqcO3w6XCrMKuITDDiEoQSMKvIMOYwrzCn3sHPH3CvsK3w4A9w6LCjsOVwrjCjGwbw4rCl0fDl8OhU8Oqw67DhMOCwozDmxrChsOtRD4`, }, - contentTypeItems: [ - { - sys: { - space: { - sys: { - type: `Link`, - linkType: `Space`, - id: `ahntqop9oi7x`, - }, - }, - id: `page`, - type: `ContentType`, - createdAt: `2020-10-16T11:43:48.221Z`, - updatedAt: `2020-10-16T11:44:25.392Z`, - environment: { - sys: { - id: `master`, - type: `Link`, - linkType: `Environment`, - }, - }, - revision: 2, - }, - displayField: `title`, - name: `Page`, - description: ``, - fields: [ - { - id: `title`, - name: `Title`, - type: `Symbol`, - localized: false, - required: false, - disabled: false, - omitted: false, - }, - { - id: `slug`, - name: `Slug`, - type: `Symbol`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `content`, - name: `Content`, - type: `RichText`, - localized: true, - required: false, - disabled: false, - omitted: false, - }, - ], - }, - ], defaultLocale: `en-US`, locales: [ { diff --git a/packages/gatsby-source-contentful/src/__fixtures__/starter-blog-data.js b/packages/gatsby-source-contentful/src/__fixtures__/starter-blog-data.js index eafa7a2723a4d..5808f74fe837a 100644 --- a/packages/gatsby-source-contentful/src/__fixtures__/starter-blog-data.js +++ b/packages/gatsby-source-contentful/src/__fixtures__/starter-blog-data.js @@ -1,3 +1,235 @@ +exports.contentTypeItems = () => [ + { + sys: { + space: { + sys: { + type: `Link`, + linkType: `Space`, + id: `uzfinxahlog0`, + contentful_id: `uzfinxahlog0`, + }, + }, + id: `person`, + type: `ContentType`, + createdAt: `2020-06-03T14:17:18.696Z`, + updatedAt: `2020-06-03T14:17:18.696Z`, + environment: { + sys: { + id: `master`, + type: `Link`, + linkType: `Environment`, + }, + }, + revision: 1, + contentful_id: `person`, + }, + displayField: `name`, + name: `Person`, + description: ``, + fields: [ + { + id: `name`, + name: `Name`, + type: `Symbol`, + localized: false, + required: true, + disabled: false, + omitted: false, + }, + { + id: `title`, + name: `Title`, + type: `Symbol`, + localized: false, + required: true, + disabled: false, + omitted: false, + }, + { + id: `company`, + name: `Company`, + type: `Symbol`, + localized: false, + required: true, + disabled: false, + omitted: false, + }, + { + id: `shortBio`, + name: `Short Bio`, + type: `Text`, + localized: false, + required: true, + disabled: false, + omitted: false, + }, + { + id: `email`, + name: `Email`, + type: `Symbol`, + localized: false, + required: false, + disabled: false, + omitted: false, + }, + { + id: `phone`, + name: `Phone`, + type: `Symbol`, + localized: false, + required: false, + disabled: false, + omitted: false, + }, + { + id: `facebook`, + name: `Facebook`, + type: `Symbol`, + localized: false, + required: false, + disabled: false, + omitted: false, + }, + { + id: `twitter`, + name: `Twitter`, + type: `Symbol`, + localized: false, + required: false, + disabled: false, + omitted: false, + }, + { + id: `github`, + name: `Github`, + type: `Symbol`, + localized: false, + required: false, + disabled: false, + omitted: false, + }, + { + id: `image`, + name: `Image`, + type: `Link`, + localized: false, + required: false, + disabled: false, + omitted: false, + linkType: `Asset`, + }, + ], + }, + { + sys: { + space: { + sys: { + type: `Link`, + linkType: `Space`, + id: `uzfinxahlog0`, + contentful_id: `uzfinxahlog0`, + }, + }, + id: `blogPost`, + type: `ContentType`, + createdAt: `2020-06-03T14:17:19.068Z`, + updatedAt: `2020-06-03T14:17:19.068Z`, + environment: { + sys: { + id: `master`, + type: `Link`, + linkType: `Environment`, + }, + }, + revision: 1, + contentful_id: `blogPost`, + }, + displayField: `title`, + name: `Blog Post`, + description: ``, + fields: [ + { + id: `title`, + name: `Title`, + type: `Symbol`, + localized: false, + required: true, + disabled: false, + omitted: false, + }, + { + id: `slug`, + name: `Slug`, + type: `Symbol`, + localized: false, + required: true, + disabled: false, + omitted: false, + }, + { + id: `heroImage`, + name: `Hero Image`, + type: `Link`, + localized: false, + required: true, + disabled: false, + omitted: false, + linkType: `Asset`, + }, + { + id: `description`, + name: `Description`, + type: `Text`, + localized: false, + required: true, + disabled: false, + omitted: false, + }, + { + id: `body`, + name: `Body`, + type: `Text`, + localized: false, + required: true, + disabled: false, + omitted: false, + }, + { + id: `author`, + name: `Author`, + type: `Link`, + localized: false, + required: false, + disabled: false, + omitted: false, + linkType: `Entry`, + }, + { + id: `publishDate`, + name: `Publish Date`, + type: `Date`, + localized: false, + required: true, + disabled: false, + omitted: false, + }, + { + id: `tags`, + name: `Tags`, + type: `Array`, + localized: false, + required: false, + disabled: false, + omitted: false, + items: { + type: `Symbol`, + validations: [{ in: [`general`, `javascript`, `static-sites`] }], + }, + }, + ], + }, +] + exports.initialSync = () => { return { currentSyncData: { @@ -420,237 +652,6 @@ exports.initialSync = () => { deletedAssets: [], nextSyncToken: `FEnChMOBwr1Yw 4TCqsK2LcKpCH3CjsORIyLDrGbDtgozw6xreMKCwpjCtlxATw3CswoUY34WECLCh152KsOLQcKwH8Kfw4kOQcOlw6TCr8OmEcKiwrhBZ8KhwrLCrcOsA8KYAMOFwo1kBMOZwrHDgCbDllcXVA`, }, - contentTypeItems: [ - { - sys: { - space: { - sys: { - type: `Link`, - linkType: `Space`, - id: `uzfinxahlog0`, - contentful_id: `uzfinxahlog0`, - }, - }, - id: `person`, - type: `ContentType`, - createdAt: `2020-06-03T14:17:18.696Z`, - updatedAt: `2020-06-03T14:17:18.696Z`, - environment: { - sys: { - id: `master`, - type: `Link`, - linkType: `Environment`, - }, - }, - revision: 1, - contentful_id: `person`, - }, - displayField: `name`, - name: `Person`, - description: ``, - fields: [ - { - id: `name`, - name: `Name`, - type: `Symbol`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `title`, - name: `Title`, - type: `Symbol`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `company`, - name: `Company`, - type: `Symbol`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `shortBio`, - name: `Short Bio`, - type: `Text`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `email`, - name: `Email`, - type: `Symbol`, - localized: false, - required: false, - disabled: false, - omitted: false, - }, - { - id: `phone`, - name: `Phone`, - type: `Symbol`, - localized: false, - required: false, - disabled: false, - omitted: false, - }, - { - id: `facebook`, - name: `Facebook`, - type: `Symbol`, - localized: false, - required: false, - disabled: false, - omitted: false, - }, - { - id: `twitter`, - name: `Twitter`, - type: `Symbol`, - localized: false, - required: false, - disabled: false, - omitted: false, - }, - { - id: `github`, - name: `Github`, - type: `Symbol`, - localized: false, - required: false, - disabled: false, - omitted: false, - }, - { - id: `image`, - name: `Image`, - type: `Link`, - localized: false, - required: false, - disabled: false, - omitted: false, - linkType: `Asset`, - }, - ], - }, - { - sys: { - space: { - sys: { - type: `Link`, - linkType: `Space`, - id: `uzfinxahlog0`, - contentful_id: `uzfinxahlog0`, - }, - }, - id: `blogPost`, - type: `ContentType`, - createdAt: `2020-06-03T14:17:19.068Z`, - updatedAt: `2020-06-03T14:17:19.068Z`, - environment: { - sys: { - id: `master`, - type: `Link`, - linkType: `Environment`, - }, - }, - revision: 1, - contentful_id: `blogPost`, - }, - displayField: `title`, - name: `Blog Post`, - description: ``, - fields: [ - { - id: `title`, - name: `Title`, - type: `Symbol`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `slug`, - name: `Slug`, - type: `Symbol`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `heroImage`, - name: `Hero Image`, - type: `Link`, - localized: false, - required: true, - disabled: false, - omitted: false, - linkType: `Asset`, - }, - { - id: `description`, - name: `Description`, - type: `Text`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `body`, - name: `Body`, - type: `Text`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `author`, - name: `Author`, - type: `Link`, - localized: false, - required: false, - disabled: false, - omitted: false, - linkType: `Entry`, - }, - { - id: `publishDate`, - name: `Publish Date`, - type: `Date`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `tags`, - name: `Tags`, - type: `Array`, - localized: false, - required: false, - disabled: false, - omitted: false, - items: { - type: `Symbol`, - validations: [{ in: [`general`, `javascript`, `static-sites`] }], - }, - }, - ], - }, - ], defaultLocale: `en-US`, locales: [ { @@ -810,237 +811,6 @@ exports.createBlogPost = () => { deletedAssets: [], nextSyncToken: `FEnChMOBwr1Yw4TCqsK2LcKpCH3CjsORIyLDrGbDtgozw6xreMKCwpjCtlxATw0YQMOfwrtZBsOQw41ww7xhJj8Ew4TChcOow6ZPPVVZaMOfOlFEwp7CpcOxwpd_YcKBw5jCkznDgMO6w4lsw73CrcOmwpILwqTClg`, }, - contentTypeItems: [ - { - sys: { - space: { - sys: { - type: `Link`, - linkType: `Space`, - id: `uzfinxahlog0`, - contentful_id: `uzfinxahlog0`, - }, - }, - id: `person`, - type: `ContentType`, - createdAt: `2020-06-03T14:17:18.696Z`, - updatedAt: `2020-06-03T14:17:18.696Z`, - environment: { - sys: { - id: `master`, - type: `Link`, - linkType: `Environment`, - }, - }, - revision: 1, - contentful_id: `person`, - }, - displayField: `name`, - name: `Person`, - description: ``, - fields: [ - { - id: `name`, - name: `Name`, - type: `Symbol`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `title`, - name: `Title`, - type: `Symbol`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `company`, - name: `Company`, - type: `Symbol`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `shortBio`, - name: `Short Bio`, - type: `Text`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `email`, - name: `Email`, - type: `Symbol`, - localized: false, - required: false, - disabled: false, - omitted: false, - }, - { - id: `phone`, - name: `Phone`, - type: `Symbol`, - localized: false, - required: false, - disabled: false, - omitted: false, - }, - { - id: `facebook`, - name: `Facebook`, - type: `Symbol`, - localized: false, - required: false, - disabled: false, - omitted: false, - }, - { - id: `twitter`, - name: `Twitter`, - type: `Symbol`, - localized: false, - required: false, - disabled: false, - omitted: false, - }, - { - id: `github`, - name: `Github`, - type: `Symbol`, - localized: false, - required: false, - disabled: false, - omitted: false, - }, - { - id: `image`, - name: `Image`, - type: `Link`, - localized: false, - required: false, - disabled: false, - omitted: false, - linkType: `Asset`, - }, - ], - }, - { - sys: { - space: { - sys: { - type: `Link`, - linkType: `Space`, - id: `uzfinxahlog0`, - contentful_id: `uzfinxahlog0`, - }, - }, - id: `blogPost`, - type: `ContentType`, - createdAt: `2020-06-03T14:17:19.068Z`, - updatedAt: `2020-06-03T14:17:19.068Z`, - environment: { - sys: { - id: `master`, - type: `Link`, - linkType: `Environment`, - }, - }, - revision: 1, - contentful_id: `blogPost`, - }, - displayField: `title`, - name: `Blog Post`, - description: ``, - fields: [ - { - id: `title`, - name: `Title`, - type: `Symbol`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `slug`, - name: `Slug`, - type: `Symbol`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `heroImage`, - name: `Hero Image`, - type: `Link`, - localized: false, - required: true, - disabled: false, - omitted: false, - linkType: `Asset`, - }, - { - id: `description`, - name: `Description`, - type: `Text`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `body`, - name: `Body`, - type: `Text`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `author`, - name: `Author`, - type: `Link`, - localized: false, - required: false, - disabled: false, - omitted: false, - linkType: `Entry`, - }, - { - id: `publishDate`, - name: `Publish Date`, - type: `Date`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `tags`, - name: `Tags`, - type: `Array`, - localized: false, - required: false, - disabled: false, - omitted: false, - items: { - type: `Symbol`, - validations: [{ in: [`general`, `javascript`, `static-sites`] }], - }, - }, - ], - }, - ], defaultLocale: `en-US`, locales: [ { @@ -1162,237 +932,6 @@ exports.updateBlogPost = () => { deletedAssets: [], nextSyncToken: `FEnChMOBwr1Yw4TCqsK2LcKpCH3CjsORIyLDrGbDtgozw6xreMKCwpjCtlxATw0OwpjDkMOywrZewqlOAMK_wp_DmcOzLRXDmlJ3wp5VextfJMKtw43CngvCrw7Cn07CgnNJw4XDscOsw5zCuXbDrMKnw7rCsTPCpxE`, }, - contentTypeItems: [ - { - sys: { - space: { - sys: { - type: `Link`, - linkType: `Space`, - id: `uzfinxahlog0`, - contentful_id: `uzfinxahlog0`, - }, - }, - id: `person`, - type: `ContentType`, - createdAt: `2020-06-03T14:17:18.696Z`, - updatedAt: `2020-06-03T14:17:18.696Z`, - environment: { - sys: { - id: `master`, - type: `Link`, - linkType: `Environment`, - }, - }, - revision: 1, - contentful_id: `person`, - }, - displayField: `name`, - name: `Person`, - description: ``, - fields: [ - { - id: `name`, - name: `Name`, - type: `Symbol`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `title`, - name: `Title`, - type: `Symbol`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `company`, - name: `Company`, - type: `Symbol`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `shortBio`, - name: `Short Bio`, - type: `Text`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `email`, - name: `Email`, - type: `Symbol`, - localized: false, - required: false, - disabled: false, - omitted: false, - }, - { - id: `phone`, - name: `Phone`, - type: `Symbol`, - localized: false, - required: false, - disabled: false, - omitted: false, - }, - { - id: `facebook`, - name: `Facebook`, - type: `Symbol`, - localized: false, - required: false, - disabled: false, - omitted: false, - }, - { - id: `twitter`, - name: `Twitter`, - type: `Symbol`, - localized: false, - required: false, - disabled: false, - omitted: false, - }, - { - id: `github`, - name: `Github`, - type: `Symbol`, - localized: false, - required: false, - disabled: false, - omitted: false, - }, - { - id: `image`, - name: `Image`, - type: `Link`, - localized: false, - required: false, - disabled: false, - omitted: false, - linkType: `Asset`, - }, - ], - }, - { - sys: { - space: { - sys: { - type: `Link`, - linkType: `Space`, - id: `uzfinxahlog0`, - contentful_id: `uzfinxahlog0`, - }, - }, - id: `blogPost`, - type: `ContentType`, - createdAt: `2020-06-03T14:17:19.068Z`, - updatedAt: `2020-06-03T14:17:19.068Z`, - environment: { - sys: { - id: `master`, - type: `Link`, - linkType: `Environment`, - }, - }, - revision: 1, - contentful_id: `blogPost`, - }, - displayField: `title`, - name: `Blog Post`, - description: ``, - fields: [ - { - id: `title`, - name: `Title`, - type: `Symbol`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `slug`, - name: `Slug`, - type: `Symbol`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `heroImage`, - name: `Hero Image`, - type: `Link`, - localized: false, - required: true, - disabled: false, - omitted: false, - linkType: `Asset`, - }, - { - id: `description`, - name: `Description`, - type: `Text`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `body`, - name: `Body`, - type: `Text`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `author`, - name: `Author`, - type: `Link`, - localized: false, - required: false, - disabled: false, - omitted: false, - linkType: `Entry`, - }, - { - id: `publishDate`, - name: `Publish Date`, - type: `Date`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `tags`, - name: `Tags`, - type: `Array`, - localized: false, - required: false, - disabled: false, - omitted: false, - items: { - type: `Symbol`, - validations: [{ in: [`general`, `javascript`, `static-sites`] }], - }, - }, - ], - }, - ], defaultLocale: `en-US`, locales: [ { @@ -1476,237 +1015,6 @@ exports.removeBlogPost = () => { deletedAssets: [], nextSyncToken: `FEnChMOBwr1Yw4TCqsK2LcKpCH3CjsORIyLDrGbDtgozw6xreMKCwpjCtlxATw13woJkDMK6fDpuN014SMKXw4MowpNDLcKVGQlXJiNSw53DlcKow4Fjw5HDqjthfQQrwo5MBlfDr3UfZjjCiMKi`, }, - contentTypeItems: [ - { - sys: { - space: { - sys: { - type: `Link`, - linkType: `Space`, - id: `uzfinxahlog0`, - contentful_id: `uzfinxahlog0`, - }, - }, - id: `person`, - type: `ContentType`, - createdAt: `2020-06-03T14:17:18.696Z`, - updatedAt: `2020-06-03T14:17:18.696Z`, - environment: { - sys: { - id: `master`, - type: `Link`, - linkType: `Environment`, - }, - }, - revision: 1, - contentful_id: `person`, - }, - displayField: `name`, - name: `Person`, - description: ``, - fields: [ - { - id: `name`, - name: `Name`, - type: `Symbol`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `title`, - name: `Title`, - type: `Symbol`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `company`, - name: `Company`, - type: `Symbol`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `shortBio`, - name: `Short Bio`, - type: `Text`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `email`, - name: `Email`, - type: `Symbol`, - localized: false, - required: false, - disabled: false, - omitted: false, - }, - { - id: `phone`, - name: `Phone`, - type: `Symbol`, - localized: false, - required: false, - disabled: false, - omitted: false, - }, - { - id: `facebook`, - name: `Facebook`, - type: `Symbol`, - localized: false, - required: false, - disabled: false, - omitted: false, - }, - { - id: `twitter`, - name: `Twitter`, - type: `Symbol`, - localized: false, - required: false, - disabled: false, - omitted: false, - }, - { - id: `github`, - name: `Github`, - type: `Symbol`, - localized: false, - required: false, - disabled: false, - omitted: false, - }, - { - id: `image`, - name: `Image`, - type: `Link`, - localized: false, - required: false, - disabled: false, - omitted: false, - linkType: `Asset`, - }, - ], - }, - { - sys: { - space: { - sys: { - type: `Link`, - linkType: `Space`, - id: `uzfinxahlog0`, - contentful_id: `uzfinxahlog0`, - }, - }, - id: `blogPost`, - type: `ContentType`, - createdAt: `2020-06-03T14:17:19.068Z`, - updatedAt: `2020-06-03T14:17:19.068Z`, - environment: { - sys: { - id: `master`, - type: `Link`, - linkType: `Environment`, - }, - }, - revision: 1, - contentful_id: `blogPost`, - }, - displayField: `title`, - name: `Blog Post`, - description: ``, - fields: [ - { - id: `title`, - name: `Title`, - type: `Symbol`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `slug`, - name: `Slug`, - type: `Symbol`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `heroImage`, - name: `Hero Image`, - type: `Link`, - localized: false, - required: true, - disabled: false, - omitted: false, - linkType: `Asset`, - }, - { - id: `description`, - name: `Description`, - type: `Text`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `body`, - name: `Body`, - type: `Text`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `author`, - name: `Author`, - type: `Link`, - localized: false, - required: false, - disabled: false, - omitted: false, - linkType: `Entry`, - }, - { - id: `publishDate`, - name: `Publish Date`, - type: `Date`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `tags`, - name: `Tags`, - type: `Array`, - localized: false, - required: false, - disabled: false, - omitted: false, - items: { - type: `Symbol`, - validations: [{ in: [`general`, `javascript`, `static-sites`] }], - }, - }, - ], - }, - ], defaultLocale: `en-US`, locales: [ { @@ -1790,237 +1098,6 @@ exports.removeAsset = () => { ], nextSyncToken: `FEnChMOBwr1Yw4TCqsK2LcKpCH3CjsORIyLDrGbDtgozw6xreMKCwpjCtlxATw0LNMOLwow1KMKwAW_Ci8OIwoPDgcK-Hn5Rw5XDvwXCsMK7wpPDk2jDtywiw6lyU8KEwprCojzDscOMwollMCbCicK_XTUEw7wZ`, }, - contentTypeItems: [ - { - sys: { - space: { - sys: { - type: `Link`, - linkType: `Space`, - id: `uzfinxahlog0`, - contentful_id: `uzfinxahlog0`, - }, - }, - id: `person`, - type: `ContentType`, - createdAt: `2020-06-03T14:17:18.696Z`, - updatedAt: `2020-06-03T14:17:18.696Z`, - environment: { - sys: { - id: `master`, - type: `Link`, - linkType: `Environment`, - }, - }, - revision: 1, - contentful_id: `person`, - }, - displayField: `name`, - name: `Person`, - description: ``, - fields: [ - { - id: `name`, - name: `Name`, - type: `Symbol`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `title`, - name: `Title`, - type: `Symbol`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `company`, - name: `Company`, - type: `Symbol`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `shortBio`, - name: `Short Bio`, - type: `Text`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `email`, - name: `Email`, - type: `Symbol`, - localized: false, - required: false, - disabled: false, - omitted: false, - }, - { - id: `phone`, - name: `Phone`, - type: `Symbol`, - localized: false, - required: false, - disabled: false, - omitted: false, - }, - { - id: `facebook`, - name: `Facebook`, - type: `Symbol`, - localized: false, - required: false, - disabled: false, - omitted: false, - }, - { - id: `twitter`, - name: `Twitter`, - type: `Symbol`, - localized: false, - required: false, - disabled: false, - omitted: false, - }, - { - id: `github`, - name: `Github`, - type: `Symbol`, - localized: false, - required: false, - disabled: false, - omitted: false, - }, - { - id: `image`, - name: `Image`, - type: `Link`, - localized: false, - required: false, - disabled: false, - omitted: false, - linkType: `Asset`, - }, - ], - }, - { - sys: { - space: { - sys: { - type: `Link`, - linkType: `Space`, - id: `uzfinxahlog0`, - contentful_id: `uzfinxahlog0`, - }, - }, - id: `blogPost`, - type: `ContentType`, - createdAt: `2020-06-03T14:17:19.068Z`, - updatedAt: `2020-06-03T14:17:19.068Z`, - environment: { - sys: { - id: `master`, - type: `Link`, - linkType: `Environment`, - }, - }, - revision: 1, - contentful_id: `blogPost`, - }, - displayField: `title`, - name: `Blog Post`, - description: ``, - fields: [ - { - id: `title`, - name: `Title`, - type: `Symbol`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `slug`, - name: `Slug`, - type: `Symbol`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `heroImage`, - name: `Hero Image`, - type: `Link`, - localized: false, - required: true, - disabled: false, - omitted: false, - linkType: `Asset`, - }, - { - id: `description`, - name: `Description`, - type: `Text`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `body`, - name: `Body`, - type: `Text`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `author`, - name: `Author`, - type: `Link`, - localized: false, - required: false, - disabled: false, - omitted: false, - linkType: `Entry`, - }, - { - id: `publishDate`, - name: `Publish Date`, - type: `Date`, - localized: false, - required: true, - disabled: false, - omitted: false, - }, - { - id: `tags`, - name: `Tags`, - type: `Array`, - localized: false, - required: false, - disabled: false, - omitted: false, - items: { - type: `Symbol`, - validations: [{ in: [`general`, `javascript`, `static-sites`] }], - }, - }, - ], - }, - ], defaultLocale: `en-US`, locales: [ { diff --git a/packages/gatsby-source-contentful/src/__tests__/download-contentful-assets.js b/packages/gatsby-source-contentful/src/__tests__/download-contentful-assets.js index de3a56eaeb667..4ba2962a312b3 100644 --- a/packages/gatsby-source-contentful/src/__tests__/download-contentful-assets.js +++ b/packages/gatsby-source-contentful/src/__tests__/download-contentful-assets.js @@ -54,7 +54,7 @@ const fixtures = [ }, ] -describe.only(`downloadContentfulAssets`, () => { +describe(`downloadContentfulAssets`, () => { it(`derives unique cache key from node locale and id`, async () => { const cache = { get: jest.fn(() => Promise.resolve(null)), diff --git a/packages/gatsby-source-contentful/src/__tests__/fetch-backoff.js b/packages/gatsby-source-contentful/src/__tests__/fetch-backoff.js index cc3658bcf5407..a05ddb7b41e52 100644 --- a/packages/gatsby-source-contentful/src/__tests__/fetch-backoff.js +++ b/packages/gatsby-source-contentful/src/__tests__/fetch-backoff.js @@ -3,7 +3,7 @@ */ import nock from "nock" -import fetchData from "../fetch" +import { fetchContent } from "../fetch" import { createPluginConfig } from "../plugin-options" const host = `localhost` @@ -82,13 +82,7 @@ describe(`fetch-backoff`, () => { `/spaces/${options.spaceId}/environments/master/sync?initial=true&limit=444` ) .reply(200, { items: [] }) - // Content types - .get( - `/spaces/${options.spaceId}/environments/master/content_types?skip=0&limit=1000&order=sys.createdAt` - ) - .reply(200, { items: [] }) - - await fetchData({ pluginConfig, reporter }) + await fetchContent({ pluginConfig, reporter }) expect(reporter.panic).not.toBeCalled() expect(reporter.warn.mock.calls).toMatchInlineSnapshot(` @@ -125,13 +119,8 @@ describe(`fetch-backoff`, () => { `/spaces/${options.spaceId}/environments/master/sync?initial=true&limit=1000` ) .reply(200, { items: [] }) - // Content types - .get( - `/spaces/${options.spaceId}/environments/master/content_types?skip=0&limit=1000&order=sys.createdAt` - ) - .reply(200, { items: [] }) - await fetchData({ pluginConfig, reporter }) + await fetchContent({ pluginConfig, reporter }) expect(reporter.panic).not.toBeCalled() expect(reporter.warn).not.toBeCalled() diff --git a/packages/gatsby-source-contentful/src/__tests__/fetch-network-errors.js b/packages/gatsby-source-contentful/src/__tests__/fetch-network-errors.js index 8f3d0a2db687a..6b22c5f47f3de 100644 --- a/packages/gatsby-source-contentful/src/__tests__/fetch-network-errors.js +++ b/packages/gatsby-source-contentful/src/__tests__/fetch-network-errors.js @@ -3,7 +3,7 @@ */ import nock from "nock" -import fetchData from "../fetch" +import { fetchContent } from "../fetch" import { createPluginConfig } from "../plugin-options" nock.disableNetConnect() @@ -65,13 +65,8 @@ describe(`fetch-retry`, () => { `/spaces/${options.spaceId}/environments/master/sync?initial=true&limit=1000` ) .reply(200, { items: [] }) - // Content types - .get( - `/spaces/${options.spaceId}/environments/master/content_types?skip=0&limit=1000&order=sys.createdAt` - ) - .reply(200, { items: [] }) - await fetchData({ pluginConfig, reporter }) + await fetchContent({ pluginConfig, reporter }) expect(reporter.panic).not.toBeCalled() expect(scope.isDone()).toBeTruthy() @@ -106,7 +101,7 @@ describe(`fetch-retry`, () => { ) try { - await fetchData({ pluginConfig, reporter }) + await fetchContent({ pluginConfig, reporter }) jest.fail() } catch (e) { const msg = expect(e.context.sourceMessage) @@ -132,7 +127,7 @@ describe(`fetch-network-errors`, () => { .get(`/spaces/${options.spaceId}/`) .replyWithError({ code: `ECONNRESET` }) try { - await fetchData({ + await fetchContent({ pluginConfig: createPluginConfig({ ...options, contentfulClientConfig: { retryOnError: false }, @@ -159,7 +154,7 @@ describe(`fetch-network-errors`, () => { .reply(502, `Bad Gateway`) try { - await fetchData({ + await fetchContent({ pluginConfig: createPluginConfig({ ...options, contentfulClientConfig: { retryOnError: false }, @@ -193,7 +188,7 @@ describe(`fetch-network-errors`, () => { }) try { - await fetchData({ + await fetchContent({ pluginConfig: createPluginConfig({ ...options, contentfulClientConfig: { retryOnError: false }, diff --git a/packages/gatsby-source-contentful/src/__tests__/fetch.js b/packages/gatsby-source-contentful/src/__tests__/fetch.js index d9bbbd4479047..229c1facf018b 100644 --- a/packages/gatsby-source-contentful/src/__tests__/fetch.js +++ b/packages/gatsby-source-contentful/src/__tests__/fetch.js @@ -59,7 +59,7 @@ jest.mock(`../plugin-options`, () => { global.console = { log: jest.fn(), time: jest.fn(), timeEnd: jest.fn() } const contentful = require(`contentful`) -const fetchData = require(`../fetch`) +const { fetchContent, fetchContentTypes } = require(`../fetch`) const { formatPluginOptionsForCLI, createPluginConfig, @@ -90,11 +90,13 @@ beforeAll(() => { }) const start = jest.fn() +const tick = jest.fn() const end = jest.fn() const mockActivity = { start, end, + tick, done: end, } @@ -119,7 +121,7 @@ afterAll(() => { }) it(`calls contentful.createClient with expected params`, async () => { - await fetchData({ pluginConfig, reporter }) + await fetchContent({ pluginConfig, reporter }) expect(reporter.panic).not.toBeCalled() expect(contentful.createClient).toBeCalledWith( expect.objectContaining({ @@ -133,7 +135,7 @@ it(`calls contentful.createClient with expected params`, async () => { }) it(`calls contentful.createClient with expected params and default fallbacks`, async () => { - await fetchData({ + await fetchContent({ pluginConfig: createPluginConfig({ accessToken: `6f35edf0db39085e9b9c19bd92943e4519c77e72c852d961968665f1324bfc94`, spaceId: `rocybtov1ozk`, @@ -153,7 +155,7 @@ it(`calls contentful.createClient with expected params and default fallbacks`, a }) it(`calls contentful.getContentTypes with default page limit`, async () => { - await fetchData({ + await fetchContentTypes({ pluginConfig: createPluginConfig({ accessToken: `6f35edf0db39085e9b9c19bd92943e4519c77e72c852d961968665f1324bfc94`, spaceId: `rocybtov1ozk`, @@ -170,7 +172,7 @@ it(`calls contentful.getContentTypes with default page limit`, async () => { }) it(`calls contentful.getContentTypes with custom plugin option page limit`, async () => { - await fetchData({ + await fetchContentTypes({ pluginConfig: createPluginConfig({ accessToken: `6f35edf0db39085e9b9c19bd92943e4519c77e72c852d961968665f1324bfc94`, spaceId: `rocybtov1ozk`, @@ -189,7 +191,7 @@ it(`calls contentful.getContentTypes with custom plugin option page limit`, asyn describe(`Tags feature`, () => { it(`tags are disabled by default`, async () => { - await fetchData({ + await fetchContent({ pluginConfig: createPluginConfig({ accessToken: `6f35edf0db39085e9b9c19bd92943e4519c77e72c852d961968665f1324bfc94`, spaceId: `rocybtov1ozk`, @@ -202,7 +204,7 @@ describe(`Tags feature`, () => { expect(mockClient.getTags).not.toBeCalled() }) it(`calls contentful.getTags when enabled`, async () => { - await fetchData({ + await fetchContent({ pluginConfig: createPluginConfig({ accessToken: `6f35edf0db39085e9b9c19bd92943e4519c77e72c852d961968665f1324bfc94`, spaceId: `rocybtov1ozk`, @@ -227,7 +229,7 @@ describe(`Displays troubleshooting tips and detailed plugin options on contentfu throw new Error(`error`) }) - await fetchData({ pluginConfig, reporter }) + await fetchContent({ pluginConfig, reporter }) expect(reporter.panic).toBeCalledWith( expect.objectContaining({ @@ -264,7 +266,7 @@ describe(`Displays troubleshooting tips and detailed plugin options on contentfu throw err }) - await fetchData({ pluginConfig, reporter }) + await fetchContent({ pluginConfig, reporter }) expect(reporter.panic).toBeCalledWith( expect.objectContaining({ @@ -301,7 +303,7 @@ describe(`Displays troubleshooting tips and detailed plugin options on contentfu const masterOptions = { ...options, environment: `master` } const masterConfig = createPluginConfig(masterOptions) - await fetchData({ + await fetchContent({ pluginConfig: masterConfig, reporter, }) @@ -344,7 +346,7 @@ describe(`Displays troubleshooting tips and detailed plugin options on contentfu throw err }) - await fetchData({ pluginConfig, reporter }) + await fetchContent({ pluginConfig, reporter }) expect(reporter.panic).toBeCalledWith( expect.objectContaining({ @@ -384,7 +386,7 @@ describe(`Displays troubleshooting tips and detailed plugin options on contentfu throw err }) - await fetchData({ pluginConfig, reporter }) + await fetchContent({ pluginConfig, reporter }) expect(reporter.panic).toBeCalledWith( expect.objectContaining({ diff --git a/packages/gatsby-source-contentful/src/__tests__/gatsby-node.js b/packages/gatsby-source-contentful/src/__tests__/gatsby-node.js index 1b6a3c2f488b5..d01874c6d637e 100644 --- a/packages/gatsby-source-contentful/src/__tests__/gatsby-node.js +++ b/packages/gatsby-source-contentful/src/__tests__/gatsby-node.js @@ -8,44 +8,85 @@ jest.mock(`gatsby-core-utils`, () => { }) const gatsbyNode = require(`../gatsby-node`) -const fetch = require(`../fetch`) +const { fetchContent, fetchContentTypes } = require(`../fetch`) const normalize = require(`../normalize`) +const _ = require(`lodash`) const startersBlogFixture = require(`../__fixtures__/starter-blog-data`) const richTextFixture = require(`../__fixtures__/rich-text-data`) const restrictedContentTypeFixture = require(`../__fixtures__/restricted-content-type`) -const pluginOptions = { spaceId: `testSpaceId` } +const defaultPluginOptions = { spaceId: `testSpaceId` } + +fetchContentTypes.mockImplementation(() => + startersBlogFixture.contentTypeItems() +) const createMockCache = () => { + const actualCacheMap = new Map() return { - get: jest.fn(), - set: jest.fn(), + get: jest.fn(key => _.cloneDeep(actualCacheMap.get(key))), + set: jest.fn((key, value) => actualCacheMap.set(key, value)), directory: __dirname, + actualMap: actualCacheMap, } } describe(`gatsby-node`, () => { - const actions = { createTypes: jest.fn() } - const schema = { buildObjectType: jest.fn() } - const store = {} + const actions = { createTypes: jest.fn(), setPluginStatus: jest.fn() } + const schema = { buildObjectType: jest.fn(), buildInterfaceType: jest.fn() } + const store = { + getState: jest.fn(() => { + return { program: { directory: process.cwd() }, status: {} } + }), + } const cache = createMockCache() - const getCache = jest.fn() + const getCache = jest.fn(() => cache) const reporter = { info: jest.fn(), verbose: jest.fn(), + panic: jest.fn(), activityTimer: () => { return { start: jest.fn(), end: jest.fn() } }, } + const parentSpan = {} const createNodeId = jest.fn(value => value) let currentNodeMap const getNodes = () => Array.from(currentNodeMap.values()) const getNode = id => currentNodeMap.get(id) + const getNodesByType = jest.fn() const getFieldValue = (value, locale, defaultLocale) => value[locale] ?? value[defaultLocale] + const simulateGatsbyBuild = async function ( + pluginOptions = defaultPluginOptions + ) { + await gatsbyNode.onPreBootstrap({ store }) + + await gatsbyNode.createSchemaCustomization( + { schema, actions, cache, reporter }, + pluginOptions + ) + + await gatsbyNode.sourceNodes( + { + actions, + getNode, + getNodes, + getNodesByType, + createNodeId, + store, + cache, + getCache, + reporter, + parentSpan, + }, + pluginOptions + ) + } + const testIfContentTypesExists = contentTypeItems => { contentTypeItems.forEach(contentType => { const contentTypeId = createNodeId(contentType.name) @@ -250,7 +291,8 @@ describe(`gatsby-node`, () => { } beforeEach(() => { - fetch.mockClear() + fetchContent.mockClear() + fetchContentTypes.mockClear() currentNodeMap = new Map() actions.createNode = jest.fn(async node => { node.internal.owner = `gatsby-source-contentful` @@ -262,38 +304,22 @@ describe(`gatsby-node`, () => { } actions.touchNode = jest.fn() actions.setPluginStatus = jest.fn() - store.getState = jest.fn(() => { - return { - status: {}, - } - }) + store.getState.mockClear() + cache.actualMap.clear() + cache.get.mockClear() + cache.set.mockClear() }) it(`should create nodes from initial payload`, async () => { - cache.get.mockClear() - cache.set.mockClear() - fetch.mockImplementationOnce(startersBlogFixture.initialSync) + fetchContent.mockImplementationOnce(startersBlogFixture.initialSync) const locales = [`en-US`, `nl`] - await gatsbyNode.sourceNodes( - { - actions, - store, - getNodes, - getNode, - reporter, - createNodeId, - cache, - getCache, - schema, - }, - pluginOptions - ) + await simulateGatsbyBuild() - testIfContentTypesExists(startersBlogFixture.initialSync().contentTypeItems) + testIfContentTypesExists(startersBlogFixture.contentTypeItems()) testIfEntriesExists( startersBlogFixture.initialSync().currentSyncData.entries, - startersBlogFixture.initialSync().contentTypeItems, + startersBlogFixture.contentTypeItems(), locales ) testIfAssetsExistsAndMatch( @@ -301,34 +327,55 @@ describe(`gatsby-node`, () => { locales ) + expect(store.getState).toHaveBeenCalled() + // Tries to load data from cache - expect(cache.get).toHaveBeenCalledWith( - `contentful-sync-token-testSpaceId-master` - ) expect(cache.get).toHaveBeenCalledWith( `contentful-sync-data-testSpaceId-master` ) - expect(cache.get.mock.calls.length).toBe(2) + + expect(cache.get.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "contentful-content-types-testSpaceId-master", + ], + Array [ + "contentful-sync-data-testSpaceId-master", + ], + ] + `) // Stores sync token and raw/unparsed data to the cache - expect(cache.set).toHaveBeenCalledWith( - `contentful-sync-token-testSpaceId-master`, - startersBlogFixture.initialSync().currentSyncData.nextSyncToken + expect(actions.setPluginStatus).toHaveBeenCalledWith({ + [`contentful-sync-token-testSpaceId-master`]: + startersBlogFixture.initialSync().currentSyncData.nextSyncToken, + }) + + // Check for valid cache data + const cacheCall = cache.set.mock.calls.filter( + args => args[0] === `contentful-sync-data-testSpaceId-master` ) - expect(cache.set).toHaveBeenCalledWith( - `contentful-sync-data-testSpaceId-master`, - { - entries: startersBlogFixture.initialSync().currentSyncData.entries, - assets: startersBlogFixture.initialSync().currentSyncData.assets, - } + + expect(cacheCall).toBeTruthy() + expect(cacheCall[0][1].entries).toHaveLength( + startersBlogFixture.initialSync().currentSyncData.entries.length ) - expect(cache.set.mock.calls.length).toBe(2) + expect(cacheCall[0][1].assets).toHaveLength( + startersBlogFixture.initialSync().currentSyncData.assets.length + ) + + expect(cache.set.mock.calls.map(v => v[0])).toMatchInlineSnapshot(` + Array [ + "contentful-content-types-testSpaceId-master", + "contentful-sync-data-testSpaceId-master", + ] + `) }) it(`should add a new blogpost and update linkedNodes`, async () => { const locales = [`en-US`, `nl`] - fetch + fetchContent .mockImplementationOnce(startersBlogFixture.initialSync) .mockImplementationOnce(startersBlogFixture.createBlogPost) @@ -345,20 +392,7 @@ describe(`gatsby-node`, () => { ) // initial sync - await gatsbyNode.sourceNodes( - { - actions, - store, - getNodes, - getNode, - reporter, - createNodeId, - cache, - getCache, - schema, - }, - pluginOptions - ) + await simulateGatsbyBuild() // check if blog posts do not exists createdBlogEntryIds.forEach(entryId => { @@ -366,26 +400,12 @@ describe(`gatsby-node`, () => { }) // add new blog post - await gatsbyNode.sourceNodes( - { - actions, - store, - getNodes, - getNode, - reporter, - createNodeId, - cache, - getCache, - schema, - }, - pluginOptions - ) - testIfContentTypesExists( - startersBlogFixture.createBlogPost().contentTypeItems - ) + await simulateGatsbyBuild() + + testIfContentTypesExists(startersBlogFixture.contentTypeItems()) testIfEntriesExists( startersBlogFixture.createBlogPost().currentSyncData.entries, - startersBlogFixture.createBlogPost().contentTypeItems, + startersBlogFixture.contentTypeItems(), locales ) testIfAssetsExistsAndMatch( @@ -402,7 +422,7 @@ describe(`gatsby-node`, () => { it(`should update a blogpost`, async () => { const locales = [`en-US`, `nl`] - fetch + fetchContent .mockImplementationOnce(startersBlogFixture.initialSync) .mockImplementationOnce(startersBlogFixture.createBlogPost) .mockImplementationOnce(startersBlogFixture.updateBlogPost) @@ -420,63 +440,22 @@ describe(`gatsby-node`, () => { ) // initial sync - await gatsbyNode.sourceNodes( - { - actions, - store, - getNodes, - getNode, - reporter, - createNodeId, - cache, - getCache, - schema, - }, - pluginOptions - ) + await simulateGatsbyBuild() // create blog post - await gatsbyNode.sourceNodes( - { - actions, - store, - getNodes, - getNode, - reporter, - createNodeId, - cache, - getCache, - schema, - }, - pluginOptions - ) + await simulateGatsbyBuild() updatedBlogEntryIds.forEach(blogEntryId => { expect(getNode(blogEntryId).title).toBe(`Integration tests`) }) // updated blog post - await gatsbyNode.sourceNodes( - { - actions, - store, - getNodes, - getNode, - reporter, - createNodeId, - cache, - getCache, - schema, - }, - pluginOptions - ) + await simulateGatsbyBuild() - testIfContentTypesExists( - startersBlogFixture.updateBlogPost().contentTypeItems - ) + testIfContentTypesExists(startersBlogFixture.contentTypeItems()) testIfEntriesExists( startersBlogFixture.updateBlogPost().currentSyncData.entries, - startersBlogFixture.updateBlogPost().contentTypeItems, + startersBlogFixture.contentTypeItems(), locales ) testIfAssetsExistsAndMatch( @@ -494,7 +473,7 @@ describe(`gatsby-node`, () => { it(`should remove a blogpost and update linkedNodes`, async () => { const locales = [`en-US`, `nl`] - fetch + fetchContent .mockImplementationOnce(startersBlogFixture.initialSync) .mockImplementationOnce(startersBlogFixture.createBlogPost) .mockImplementationOnce(startersBlogFixture.removeBlogPost) @@ -515,36 +494,10 @@ describe(`gatsby-node`, () => { ) // initial sync - await gatsbyNode.sourceNodes( - { - actions, - store, - getNodes, - getNode, - reporter, - createNodeId, - cache, - getCache, - schema, - }, - pluginOptions - ) + await simulateGatsbyBuild() // create blog post - await gatsbyNode.sourceNodes( - { - actions, - store, - getNodes, - getNode, - reporter, - createNodeId, - cache, - getCache, - schema, - }, - pluginOptions - ) + await simulateGatsbyBuild() const authorIds = [] // check if blog post exists @@ -555,24 +508,9 @@ describe(`gatsby-node`, () => { }) // remove blog post - await gatsbyNode.sourceNodes( - { - actions, - store, - getNodes, - getNode, - reporter, - createNodeId, - cache, - getCache, - schema, - }, - pluginOptions - ) + await simulateGatsbyBuild() - testIfContentTypesExists( - startersBlogFixture.removeBlogPost().contentTypeItems - ) + testIfContentTypesExists(startersBlogFixture.contentTypeItems()) testIfEntriesDeleted( startersBlogFixture.removeBlogPost().currentSyncData.assets, locales @@ -587,7 +525,7 @@ describe(`gatsby-node`, () => { it(`should remove an asset`, async () => { const locales = [`en-US`, `nl`] - fetch + fetchContent .mockImplementationOnce(startersBlogFixture.initialSync) .mockImplementationOnce(startersBlogFixture.createBlogPost) .mockImplementationOnce(startersBlogFixture.removeAsset) @@ -605,33 +543,10 @@ describe(`gatsby-node`, () => { ) // initial sync - await gatsbyNode.sourceNodes( - { - actions, - store, - getNodes, - getNode, - reporter, - createNodeId, - cache, - getCache, - schema, - }, - pluginOptions - ) + await simulateGatsbyBuild() // create blog post - await gatsbyNode.sourceNodes({ - actions, - store, - getNodes, - getNode, - reporter, - createNodeId, - cache, - getCache, - schema, - }) + await simulateGatsbyBuild() // check if blog post exists removedAssetEntryIds.forEach(assetId => { @@ -645,22 +560,9 @@ describe(`gatsby-node`, () => { ) // remove asset - await gatsbyNode.sourceNodes( - { - actions, - store, - getNodes, - getNode, - reporter, - createNodeId, - cache, - getCache, - schema, - }, - pluginOptions - ) + await simulateGatsbyBuild() - testIfContentTypesExists(startersBlogFixture.removeAsset().contentTypeItems) + testIfContentTypesExists(startersBlogFixture.contentTypeItems()) testIfEntriesExists( startersBlogFixture.removeAsset().currentSyncData.entries, startersBlogFixture.removeAsset().contentTypeItems, @@ -673,29 +575,18 @@ describe(`gatsby-node`, () => { }) it(`stores rich text as raw with references attached`, async () => { - fetch.mockImplementationOnce(richTextFixture.initialSync) + fetchContent.mockImplementationOnce(richTextFixture.initialSync) + fetchContentTypes.mockImplementationOnce(richTextFixture.contentTypeItems) // initial sync - await gatsbyNode.sourceNodes( - { - actions, - store, - getNodes, - getNode, - reporter, - createNodeId, - cache, - getCache, - schema, - }, - pluginOptions - ) + await simulateGatsbyBuild() const initNodes = getNodes() const homeNodes = initNodes.filter( ({ contentful_id: id }) => id === `6KpLS2NZyB3KAvDzWf4Ukh` ) + expect(homeNodes).toHaveLength(2) homeNodes.forEach(homeNode => { expect(homeNode.content.references___NODE).toStrictEqual([ ...new Set(homeNode.content.references___NODE), @@ -705,35 +596,15 @@ describe(`gatsby-node`, () => { }) it(`panics when localeFilter reduces locale list to 0`, async () => { - cache.get.mockClear() - cache.set.mockClear() - fetch.mockImplementationOnce(startersBlogFixture.initialSync) + fetchContent.mockImplementationOnce(startersBlogFixture.initialSync) const locales = [`en-US`, `nl`] - const mockPanicReporter = { - ...reporter, - panic: jest.fn(), - } - - await gatsbyNode.sourceNodes( - { - actions, - store, - getNodes, - getNode, - reporter: mockPanicReporter, - createNodeId, - cache, - getCache, - schema, - }, - { - ...pluginOptions, - localeFilter: () => false, - } - ) + await simulateGatsbyBuild({ + ...defaultPluginOptions, + localeFilter: () => false, + }) - expect(mockPanicReporter.panic).toBeCalledWith( + expect(reporter.panic).toBeCalledWith( expect.objectContaining({ context: { sourceMessage: `Please check if your localeFilter is configured properly. Locales '${locales.join( @@ -745,31 +616,16 @@ describe(`gatsby-node`, () => { }) it(`panics when response contains restricted content types`, async () => { - cache.get.mockClear() - cache.set.mockClear() - fetch.mockImplementationOnce(restrictedContentTypeFixture.initialSync) - - const mockPanicReporter = { - ...reporter, - panic: jest.fn(), - } - - await gatsbyNode.sourceNodes( - { - actions, - store, - getNodes, - getNode, - reporter: mockPanicReporter, - createNodeId, - cache, - getCache, - schema, - }, - pluginOptions + fetchContent.mockImplementationOnce( + restrictedContentTypeFixture.initialSync + ) + fetchContentTypes.mockImplementationOnce( + restrictedContentTypeFixture.contentTypeItems ) - expect(mockPanicReporter.panic).toBeCalledWith( + await simulateGatsbyBuild() + + expect(reporter.panic).toBeCalledWith( expect.objectContaining({ context: { sourceMessage: `Restricted ContentType name found. The name "reference" is not allowed.`, diff --git a/packages/gatsby-source-contentful/src/create-schema-customization.js b/packages/gatsby-source-contentful/src/create-schema-customization.js new file mode 100644 index 0000000000000..3d47845cb9346 --- /dev/null +++ b/packages/gatsby-source-contentful/src/create-schema-customization.js @@ -0,0 +1,145 @@ +// @ts-check +const _ = require(`lodash`) + +const { createPluginConfig } = require(`./plugin-options`) +const { fetchContentTypes } = require(`./fetch`) +const { CODES } = require(`./report`) + +async function getContentTypesFromContentFul({ + cache, + reporter, + pluginConfig, +}) { + // Get content type items from Contentful + const contentTypeItems = await fetchContentTypes({ pluginConfig, reporter }) + + // Check for restricted content type names and set id based on useNameForId + const useNameForId = pluginConfig.get(`useNameForId`) + const restrictedContentTypes = [`entity`, `reference`, `asset`] + + if (pluginConfig.get(`enableTags`)) { + restrictedContentTypes.push(`tags`) + } + + contentTypeItems.forEach(contentTypeItem => { + // Establish identifier for content type + // Use `name` if specified, otherwise, use internal id (usually a natural-language constant, + // but sometimes a base62 uuid generated by Contentful, hence the option) + let contentTypeItemId + if (useNameForId) { + contentTypeItemId = contentTypeItem.name.toLowerCase() + } else { + contentTypeItemId = contentTypeItem.sys.id.toLowerCase() + } + + if (restrictedContentTypes.includes(contentTypeItemId)) { + reporter.panic({ + id: CODES.FetchContentTypes, + context: { + sourceMessage: `Restricted ContentType name found. The name "${contentTypeItemId}" is not allowed.`, + }, + }) + } + }) + + // Store processed content types in cache for sourceNodes + const sourceId = `${pluginConfig.get(`spaceId`)}-${pluginConfig.get( + `environment` + )}` + const CACHE_CONTENT_TYPES = `contentful-content-types-${sourceId}` + await cache.set(CACHE_CONTENT_TYPES, contentTypeItems) + + return contentTypeItems +} + +export async function createSchemaCustomization( + { schema, actions, reporter, cache }, + pluginOptions +) { + const { createTypes } = actions + + const pluginConfig = createPluginConfig(pluginOptions) + + let contentTypeItems + if (process.env.GATSBY_WORKER_ID) { + const sourceId = `${pluginConfig.get(`spaceId`)}-${pluginConfig.get( + `environment` + )}` + contentTypeItems = await cache.get(`contentful-content-types-${sourceId}`) + } else { + contentTypeItems = await getContentTypesFromContentFul({ + cache, + reporter, + pluginConfig, + }) + } + + const contentfulTypes = [ + schema.buildInterfaceType({ + name: `ContentfulEntry`, + fields: { + contentful_id: { type: `String!` }, + id: { type: `ID!` }, + node_locale: { type: `String!` }, + }, + extensions: { infer: false }, + interfaces: [`Node`], + }), + schema.buildInterfaceType({ + name: `ContentfulReference`, + fields: { + contentful_id: { type: `String!` }, + id: { type: `ID!` }, + }, + extensions: { infer: false }, + }), + schema.buildObjectType({ + name: `ContentfulAsset`, + fields: { + contentful_id: { type: `String!` }, + id: { type: `ID!` }, + }, + interfaces: [`ContentfulReference`, `Node`], + }), + ] + + // Create types for each content type + contentTypeItems.forEach(contentTypeItem => + contentfulTypes.push( + schema.buildObjectType({ + name: _.upperFirst( + _.camelCase( + `Contentful ${ + pluginConfig.get(`useNameForId`) + ? contentTypeItem.name + : contentTypeItem.sys.id + }` + ) + ), + fields: { + contentful_id: { type: `String!` }, + id: { type: `ID!` }, + node_locale: { type: `String!` }, + }, + interfaces: [`ContentfulReference`, `ContentfulEntry`, `Node`], + }) + ) + ) + + if (pluginConfig.get(`enableTags`)) { + contentfulTypes.push( + schema.buildObjectType({ + name: `ContentfulTag`, + fields: { + name: { type: `String!` }, + contentful_id: { type: `String!` }, + id: { type: `ID!` }, + }, + interfaces: [`Node`], + extensions: { dontInfer: {} }, + }) + ) + } + + createTypes(contentfulTypes) +} diff --git a/packages/gatsby-source-contentful/src/fetch.js b/packages/gatsby-source-contentful/src/fetch.js index 136031150d68e..b0efa6488f036 100644 --- a/packages/gatsby-source-contentful/src/fetch.js +++ b/packages/gatsby-source-contentful/src/fetch.js @@ -72,15 +72,13 @@ const createContentfulErrorMessage = e => { return errorMessage } -module.exports = async function contentfulFetch({ - syncToken, +function createContentfulClientOptions({ pluginConfig, reporter, + syncProgress = { total: 0, tick: a => a }, }) { - // Fetch articles. - let syncProgress let syncItemCount = 0 - const pageLimit = pluginConfig.get(`pageLimit`) + const contentfulClientOptions = { space: pluginConfig.get(`spaceId`), accessToken: pluginConfig.get(`accessToken`), @@ -133,12 +131,95 @@ module.exports = async function contentfulFetch({ ...(pluginConfig.get(`contentfulClientConfig`) || {}), } + return contentfulClientOptions +} + +function handleContentfulError({ + e, + reporter, + contentfulClientOptions, + pluginConfig, +}) { + let details + let errors + if (e.code === `ENOTFOUND`) { + details = `You seem to be offline` + } else if (e.code === `SELF_SIGNED_CERT_IN_CHAIN`) { + reporter.panic({ + id: CODES.SelfSignedCertificate, + context: { + sourceMessage: `We couldn't make a secure connection to your contentful space. Please check if you have any self-signed SSL certificates installed.`, + }, + }) + } else if (e.responseData) { + if ( + e.responseData.status === 404 && + contentfulClientOptions.environment && + contentfulClientOptions.environment !== `master` + ) { + // environments need to have access to master + details = `Unable to access your space. Check if ${chalk.yellow( + `environment` + )} is correct and your ${chalk.yellow( + `accessToken` + )} has access to the ${chalk.yellow( + contentfulClientOptions.environment + )} and the ${chalk.yellow(`master`)} environments.` + errors = { + accessToken: `Check if setting is correct`, + environment: `Check if setting is correct`, + } + } else if (e.responseData.status === 404) { + // host and space used to generate url + details = `Endpoint not found. Check if ${chalk.yellow( + `host` + )} and ${chalk.yellow(`spaceId`)} settings are correct` + errors = { + host: `Check if setting is correct`, + spaceId: `Check if setting is correct`, + } + } else if (e.responseData.status === 401) { + // authorization error + details = `Authorization error. Check if ${chalk.yellow( + `accessToken` + )} and ${chalk.yellow(`environment`)} are correct` + errors = { + accessToken: `Check if setting is correct`, + environment: `Check if setting is correct`, + } + } + } + + reporter.panic({ + context: { + sourceMessage: `Accessing your Contentful space failed: ${createContentfulErrorMessage( + e + )} + +Try setting GATSBY_CONTENTFUL_OFFLINE=true to see if we can serve from cache. +${details ? `\n${details}\n` : ``} +Used options: +${formatPluginOptionsForCLI(pluginConfig.getOriginalPluginOptions(), errors)}`, + }, + }) +} + +/** + * Fetches: + * * Locales with default locale + * * Entries and assets + * * Tags + */ +async function fetchContent({ syncToken, pluginConfig, reporter }) { + // Fetch locales and check connectivity + const contentfulClientOptions = createContentfulClientOptions({ + pluginConfig, + reporter, + }) const client = contentful.createClient(contentfulClientOptions) // The sync API puts the locale in all fields in this format { fieldName: // {'locale': value} } so we need to get the space and its default local. - // - // We'll extend this soon to support multiple locales. let space let locales let defaultLocale = `en-US` @@ -151,83 +232,35 @@ module.exports = async function contentfulFetch({ `Default locale is: ${defaultLocale}. There are ${locales.length} locales in total.` ) } catch (e) { - let details - let errors - if (e.code === `ENOTFOUND`) { - details = `You seem to be offline` - } else if (e.code === `SELF_SIGNED_CERT_IN_CHAIN`) { - reporter.panic({ - id: CODES.SelfSignedCertificate, - context: { - sourceMessage: `We couldn't make a secure connection to your contentful space. Please check if you have any self-signed SSL certificates installed.`, - }, - }) - } else if (e.responseData) { - if ( - e.responseData.status === 404 && - contentfulClientOptions.environment && - contentfulClientOptions.environment !== `master` - ) { - // environments need to have access to master - details = `Unable to access your space. Check if ${chalk.yellow( - `environment` - )} is correct and your ${chalk.yellow( - `accessToken` - )} has access to the ${chalk.yellow( - contentfulClientOptions.environment - )} and the ${chalk.yellow(`master`)} environments.` - errors = { - accessToken: `Check if setting is correct`, - environment: `Check if setting is correct`, - } - } else if (e.responseData.status === 404) { - // host and space used to generate url - details = `Endpoint not found. Check if ${chalk.yellow( - `host` - )} and ${chalk.yellow(`spaceId`)} settings are correct` - errors = { - host: `Check if setting is correct`, - spaceId: `Check if setting is correct`, - } - } else if (e.responseData.status === 401) { - // authorization error - details = `Authorization error. Check if ${chalk.yellow( - `accessToken` - )} and ${chalk.yellow(`environment`)} are correct` - errors = { - accessToken: `Check if setting is correct`, - environment: `Check if setting is correct`, - } - } - } - - reporter.panic({ - context: { - sourceMessage: `Accessing your Contentful space failed: ${createContentfulErrorMessage( - e - )} - -Try setting GATSBY_CONTENTFUL_OFFLINE=true to see if we can serve from cache. -${details ? `\n${details}\n` : ``} -Used options: -${formatPluginOptionsForCLI(pluginConfig.getOriginalPluginOptions(), errors)}`, - }, + handleContentfulError({ + e, + reporter, + contentfulClientOptions, + pluginConfig, }) } + // Fetch entries and assets via Contentful CDA sync API + const pageLimit = pluginConfig.get(`pageLimit`) + reporter.verbose(`Contentful: Sync ${pageLimit} items per page.`) + const syncProgress = reporter.createProgress( + `Contentful: ${syncToken ? `Sync changed items` : `Sync all items`}`, + pageLimit, + 0 + ) + syncProgress.start() + const contentfulSyncClientOptions = createContentfulClientOptions({ + pluginConfig, + reporter, + syncProgress, + }) + const syncClient = contentful.createClient(contentfulSyncClientOptions) + let currentSyncData let currentPageLimit = pageLimit let lastCurrentPageLimit let syncSuccess = false try { - syncProgress = reporter.createProgress( - `Contentful: ${syncToken ? `Sync changed items` : `Sync all items`}`, - currentPageLimit, - 0 - ) - syncProgress.start() - reporter.verbose(`Contentful: Sync ${currentPageLimit} items per page.`) - while (!syncSuccess) { try { const basicSyncConfig = { @@ -237,7 +270,7 @@ ${formatPluginOptionsForCLI(pluginConfig.getOriginalPluginOptions(), errors)}`, const query = syncToken ? { nextSyncToken: syncToken, ...basicSyncConfig } : { initial: true, ...basicSyncConfig } - currentSyncData = await client.sync(query) + currentSyncData = await syncClient.sync(query) syncSuccess = true } catch (e) { // Back off page limit if responses content length exceeds Contentfuls limits. @@ -275,27 +308,20 @@ ${formatPluginOptionsForCLI(pluginConfig.getOriginalPluginOptions(), errors)}`, }, }) } finally { - syncProgress.done() - } + // Fix output when there was no new data in Contentful + if ( + currentSyncData?.entries.length + + currentSyncData?.assets.length + + currentSyncData?.deletedEntries.length + + currentSyncData?.deletedAssets.length === + 0 + ) { + syncProgress.tick() + syncProgress.total = 1 + } - // We need to fetch content types with the non-sync API as the sync API - // doesn't support this. - let contentTypes - try { - contentTypes = await pagedGet(client, `getContentTypes`, pageLimit) - } catch (e) { - reporter.panic({ - id: CODES.FetchContentTypes, - context: { - sourceMessage: `Error fetching content types: ${createContentfulErrorMessage( - e - )}`, - }, - }) + syncProgress.done() } - reporter.verbose(`Content types fetched ${contentTypes.items.length}`) - - const contentTypeItems = contentTypes.items // We need to fetch tags with the non-sync API as the sync API doesn't support this. let tagItems = [] @@ -318,7 +344,6 @@ ${formatPluginOptionsForCLI(pluginConfig.getOriginalPluginOptions(), errors)}`, const result = { currentSyncData, - contentTypeItems, tagItems, defaultLocale, locales, @@ -328,6 +353,54 @@ ${formatPluginOptionsForCLI(pluginConfig.getOriginalPluginOptions(), errors)}`, return result } +exports.fetchContent = fetchContent + +/** + * Fetches: + * * Content types + */ +async function fetchContentTypes({ pluginConfig, reporter }) { + const contentfulClientOptions = createContentfulClientOptions({ + pluginConfig, + reporter, + }) + const client = contentful.createClient(contentfulClientOptions) + const pageLimit = pluginConfig.get(`pageLimit`) + const sourceId = `${pluginConfig.get(`spaceId`)}-${pluginConfig.get( + `environment` + )}` + let contentTypes = null + + try { + reporter.verbose(`Fetching content types (${sourceId})`) + + // Fetch content types from CDA API + try { + contentTypes = await pagedGet(client, `getContentTypes`, pageLimit) + } catch (e) { + reporter.panic({ + id: CODES.FetchContentTypes, + context: { + sourceMessage: `Error fetching content types: ${createContentfulErrorMessage( + e + )}`, + }, + }) + } + reporter.verbose( + `Content types fetched ${contentTypes.items.length} (${sourceId})` + ) + + contentTypes = contentTypes.items + } catch (e) { + handleContentfulError(e) + } + + return contentTypes +} + +exports.fetchContentTypes = fetchContentTypes + /** * Gets all the existing entities based on pagination parameters. * The first call will have no aggregated response. Subsequent calls will diff --git a/packages/gatsby-source-contentful/src/gatsby-node.js b/packages/gatsby-source-contentful/src/gatsby-node.js index 5f83bc7996a96..dbc76acae4b6e 100644 --- a/packages/gatsby-source-contentful/src/gatsby-node.js +++ b/packages/gatsby-source-contentful/src/gatsby-node.js @@ -1,28 +1,20 @@ -const path = require(`path`) -const isOnline = require(`is-online`) +// @todo import syntax! const _ = require(`lodash`) -const fs = require(`fs-extra`) -const { createClient } = require(`contentful`) -const v8 = require(`v8`) const fetch = require(`@vercel/fetch-retry`)(require(`node-fetch`)) -const { CODES } = require(`./report`) -const normalize = require(`./normalize`) -const fetchData = require(`./fetch`) -const { createPluginConfig, maskText } = require(`./plugin-options`) -const { downloadContentfulAssets } = require(`./download-contentful-assets`) +const { createSchemaCustomization } = require(`./create-schema-customization`) +const { sourceNodes } = require(`./source-nodes`) +const { onPreBootstrap } = require(`./on-pre-bootstrap`) +const { maskText } = require(`./plugin-options`) -const conflictFieldPrefix = `contentful` +exports.setFieldsOnGraphQLNodeType = + require(`./extend-node-type`).extendNodeType + +exports.createSchemaCustomization = createSchemaCustomization + +exports.sourceNodes = sourceNodes -// restrictedNodeFields from here https://www.gatsbyjs.org/docs/node-interface/ -const restrictedNodeFields = [ - `children`, - `contentful_id`, - `fields`, - `id`, - `internal`, - `parent`, -] +exports.onPreBootstrap = onPreBootstrap exports.setFieldsOnGraphQLNodeType = require(`./extend-node-type`).extendNodeType @@ -96,11 +88,6 @@ For example, to filter locales on only germany \`localeFilter: locale => locale. List of locales and their codes can be found in Contentful app -> Settings -> Locales` ) .default(() => () => true), - forceFullSync: Joi.boolean() - .description( - `Prevents the use of sync tokens when accessing the Contentful API.` - ) - .default(false), pageLimit: Joi.number() .integer() .description( @@ -125,6 +112,11 @@ List of locales and their codes can be found in Contentful app -> Settings -> Lo .description( `Axios proxy configuration. See the [axios request config documentation](https://github.com/mzabriskie/axios#request-config) for further information about the supported values.` ), + enableTags: Joi.boolean() + .description( + `Enable the new tags feature. This will disallow the content type name "tags" till the next major version of this plugin.` + ) + .default(false), useNameForId: Joi.boolean() .description( `Use the content's \`name\` when generating the GraphQL schema e.g. a Content Type called \`[Component] Navigation bar\` will be named \`contentfulComponentNavigationBar\`. @@ -149,597 +141,3 @@ List of locales and their codes can be found in Contentful app -> Settings -> Lo .external(validateContentfulAccess) exports.pluginOptionsSchema = pluginOptionsSchema - -/*** - * Localization algorithm - * - * 1. Make list of all resolvable IDs worrying just about the default ids not - * localized ids - * 2. Make mapping between ids, again not worrying about localization. - * 3. When creating entries and assets, make the most localized version - * possible for each localized node i.e. get the localized field if it exists - * or the fallback field or the default field. - */ - -exports.sourceNodes = async ( - { - actions, - getNode, - getNodes, - getNodesByType, - createNodeId, - store, - cache, - getCache, - reporter, - schema, - parentSpan, - }, - pluginOptions -) => { - const { createNode, deleteNode, touchNode, createTypes } = actions - - let currentSyncData - let contentTypeItems - let tagItems - let defaultLocale - let locales - let space - if (process.env.GATSBY_CONTENTFUL_EXPERIMENTAL_FORCE_CACHE) { - reporter.info( - `GATSBY_CONTENTFUL_EXPERIMENTAL_FORCE_CACHE: Storing/loading remote data through \`` + - process.env.GATSBY_CONTENTFUL_EXPERIMENTAL_FORCE_CACHE + - `\` so Remote changes CAN NOT be detected!` - ) - } - const forceCache = await fs.exists( - process.env.GATSBY_CONTENTFUL_EXPERIMENTAL_FORCE_CACHE - ) - - const pluginConfig = createPluginConfig(pluginOptions) - const sourceId = `${pluginConfig.get(`spaceId`)}-${pluginConfig.get( - `environment` - )}` - - const CACHE_SYNC_TOKEN = `contentful-sync-token-${sourceId}` - const CACHE_SYNC_DATA = `contentful-sync-data-${sourceId}` - - /* - * Subsequent calls of Contentfuls sync API return only changed data. - * - * In some cases, especially when using rich-text fields, there can be data - * missing from referenced entries. This breaks the reference matching. - * - * To workround this, we cache the initial sync data and merge it - * with all data from subsequent syncs. Afterwards the references get - * resolved via the Contentful JS SDK. - */ - const syncToken = await cache.get(CACHE_SYNC_TOKEN) - let previousSyncData = { - assets: [], - entries: [], - } - const cachedData = await cache.get(CACHE_SYNC_DATA) - - if (cachedData) { - previousSyncData = cachedData - } - - const fetchActivity = reporter.activityTimer( - `Contentful: Fetch data (${sourceId})`, - { - parentSpan, - } - ) - - if (forceCache) { - // If the cache has data, use it. Otherwise do a remote fetch anyways and prime the cache now. - // If present, do NOT contact contentful, skip the round trips entirely - reporter.info( - `GATSBY_CONTENTFUL_EXPERIMENTAL_FORCE_CACHE was set. Skipping remote fetch, using data stored in \`${process.env.GATSBY_CONTENTFUL_EXPERIMENTAL_FORCE_CACHE}\`` - ) - ;({ - currentSyncData, - contentTypeItems, - tagItems, - defaultLocale, - locales, - space, - } = v8.deserialize( - fs.readFileSync(process.env.GATSBY_CONTENTFUL_EXPERIMENTAL_FORCE_CACHE) - )) - } else { - const online = await isOnline() - - // If the user knows they are offline, serve them cached result - // For prod builds though always fail if we can't get the latest data - if ( - !online && - process.env.GATSBY_CONTENTFUL_OFFLINE === `true` && - process.env.NODE_ENV !== `production` - ) { - getNodes().forEach(node => { - if (node.internal.owner !== `gatsby-source-contentful`) { - return - } - touchNode(node) - if (node.localFile___NODE) { - // Prevent GraphQL type inference from crashing on this property - touchNode(getNode(node.localFile___NODE)) - } - }) - - reporter.info(`Using Contentful Offline cache ⚠️`) - reporter.info( - `Cache may be invalidated if you edit package.json, gatsby-node.js or gatsby-config.js files` - ) - - return - } - if (process.env.GATSBY_CONTENTFUL_OFFLINE) { - reporter.info( - `Note: \`GATSBY_CONTENTFUL_OFFLINE\` was set but it either was not \`true\`, we _are_ online, or we are in production mode, so the flag is ignored.` - ) - } - - fetchActivity.start() - ;({ - currentSyncData, - contentTypeItems, - tagItems, - defaultLocale, - locales, - space, - } = await fetchData({ - syncToken, - reporter, - pluginConfig, - parentSpan, - })) - - if (process.env.GATSBY_CONTENTFUL_EXPERIMENTAL_FORCE_CACHE) { - reporter.info( - `GATSBY_CONTENTFUL_EXPERIMENTAL_FORCE_CACHE was set. Writing v8 serialized glob of remote data to: ` + - process.env.GATSBY_CONTENTFUL_EXPERIMENTAL_FORCE_CACHE - ) - fs.writeFileSync( - process.env.GATSBY_CONTENTFUL_EXPERIMENTAL_FORCE_CACHE, - v8.serialize({ - currentSyncData, - contentTypeItems, - defaultLocale, - locales, - space, - }) - ) - } - } - - // Check for restricted content type names - const useNameForId = pluginConfig.get(`useNameForId`) - const restrictedContentTypes = [`entity`, `reference`, `asset`] - - if (pluginConfig.get(`enableTags`)) { - restrictedContentTypes.push(`tags`) - } - - contentTypeItems.forEach(contentTypeItem => { - // Establish identifier for content type - // Use `name` if specified, otherwise, use internal id (usually a natural-language constant, - // but sometimes a base62 uuid generated by Contentful, hence the option) - let contentTypeItemId - if (useNameForId) { - contentTypeItemId = contentTypeItem.name.toLowerCase() - } else { - contentTypeItemId = contentTypeItem.sys.id.toLowerCase() - } - - if (restrictedContentTypes.includes(contentTypeItemId)) { - reporter.panic({ - id: CODES.FetchContentTypes, - context: { - sourceMessage: `Restricted ContentType name found. The name "${contentTypeItemId}" is not allowed.`, - }, - }) - } - }) - - const allLocales = locales - locales = locales.filter(pluginConfig.get(`localeFilter`)) - reporter.verbose( - `Default locale: ${defaultLocale}. All locales: ${allLocales - .map(({ code }) => code) - .join(`, `)}` - ) - if (allLocales.length !== locales.length) { - reporter.verbose( - `After plugin.options.localeFilter: ${locales - .map(({ code }) => code) - .join(`, `)}` - ) - } - if (locales.length === 0) { - reporter.panic({ - id: CODES.LocalesMissing, - context: { - sourceMessage: `Please check if your localeFilter is configured properly. Locales '${allLocales - .map(item => item.code) - .join(`,`)}' were found but were filtered down to none.`, - }, - }) - } - - if (pluginConfig.get(`enableTags`)) { - createTypes( - schema.buildObjectType({ - name: `ContentfulTag`, - fields: { - name: { type: `String!` }, - contentful_id: { type: `String!` }, - id: { type: `ID!` }, - }, - interfaces: [`Node`], - extensions: { dontInfer: {} }, - }) - ) - } - - createTypes(` - interface ContentfulEntry implements Node { - contentful_id: String! - id: ID! - node_locale: String! - } -`) - - createTypes(` - interface ContentfulReference { - contentful_id: String! - id: ID! - } -`) - - createTypes( - schema.buildObjectType({ - name: `ContentfulAsset`, - fields: { - contentful_id: { type: `String!` }, - id: { type: `ID!` }, - }, - interfaces: [`ContentfulReference`, `Node`], - }) - ) - - const gqlTypes = contentTypeItems.map(contentTypeItem => - schema.buildObjectType({ - name: _.upperFirst( - _.camelCase( - `Contentful ${ - pluginConfig.get(`useNameForId`) - ? contentTypeItem.name - : contentTypeItem.sys.id - }` - ) - ), - fields: { - contentful_id: { type: `String!` }, - id: { type: `ID!` }, - node_locale: { type: `String!` }, - }, - interfaces: [`ContentfulReference`, `ContentfulEntry`, `Node`], - }) - ) - - createTypes(gqlTypes) - - fetchActivity.end() - - const processingActivity = reporter.activityTimer( - `Contentful: Process data (${sourceId})`, - { - parentSpan, - } - ) - processingActivity.start() - - // Create a map of up to date entries and assets - function mergeSyncData(previous, current, deleted) { - const entryMap = new Map() - previous.forEach(e => !deleted.has(e.sys.id) && entryMap.set(e.sys.id, e)) - current.forEach(e => !deleted.has(e.sys.id) && entryMap.set(e.sys.id, e)) - return [...entryMap.values()] - } - - const deletedSet = new Set(currentSyncData.deletedEntries.map(e => e.sys.id)) - - const mergedSyncData = { - entries: mergeSyncData( - previousSyncData.entries, - currentSyncData.entries, - deletedSet - ), - assets: mergeSyncData( - previousSyncData.assets, - currentSyncData.assets, - deletedSet - ), - } - - // Store a raw and unresolved copy of the data for caching - const mergedSyncDataRaw = _.cloneDeep(mergedSyncData) - - // Use the JS-SDK to resolve the entries and assets - const res = createClient({ - space: `none`, - accessToken: `fake-access-token`, - }).parseEntries({ - items: mergedSyncData.entries, - includes: { - assets: mergedSyncData.assets, - entries: mergedSyncData.entries, - }, - }) - - mergedSyncData.entries = res.items - - // Inject raw API output to rich text fields - const richTextFieldMap = new Map() - contentTypeItems.forEach(contentType => { - richTextFieldMap.set( - contentType.sys.id, - contentType.fields - .filter(field => field.type === `RichText`) - .map(field => field.id) - ) - }) - - const rawEntries = new Map() - mergedSyncDataRaw.entries.forEach(rawEntry => - rawEntries.set(rawEntry.sys.id, rawEntry) - ) - - mergedSyncData.entries.forEach(entry => { - const contentTypeId = entry.sys.contentType.sys.id - const richTextFieldIds = richTextFieldMap.get(contentTypeId) - if (richTextFieldIds) { - richTextFieldIds.forEach(richTextFieldId => { - if (!entry.fields[richTextFieldId]) { - return - } - entry.fields[richTextFieldId] = rawEntries.get(entry.sys.id).fields[ - richTextFieldId - ] - }) - } - }) - - const entryList = normalize.buildEntryList({ - mergedSyncData, - contentTypeItems, - }) - - // Remove deleted entries & assets. - // TODO figure out if entries referencing now deleted entries/assets - // are "updated" so will get the now deleted reference removed. - - function deleteContentfulNode(node) { - const normalizedType = node.sys.type.startsWith(`Deleted`) - ? node.sys.type.substring(`Deleted`.length) - : node.sys.type - - const localizedNodes = locales - .map(locale => { - const nodeId = createNodeId( - normalize.makeId({ - spaceId: space.sys.id, - id: node.sys.id, - type: normalizedType, - currentLocale: locale.code, - defaultLocale, - }) - ) - return getNode(nodeId) - }) - .filter(node => node) - - localizedNodes.forEach(node => { - // touchNode first, to populate typeOwners & avoid erroring - touchNode(node) - deleteNode(node) - }) - } - - currentSyncData.deletedEntries.forEach(deleteContentfulNode) - currentSyncData.deletedAssets.forEach(deleteContentfulNode) - - const existingNodes = getNodes().filter( - n => - n.internal.owner === `gatsby-source-contentful` && - (n?.sys?.type === `Asset` || n?.sys?.type === `Entry`) - ) - existingNodes.forEach(n => touchNode(n)) - - const assets = mergedSyncData.assets - - reporter.info(`Updated entries ${currentSyncData.entries.length}`) - reporter.info(`Deleted entries ${currentSyncData.deletedEntries.length}`) - reporter.info(`Updated assets ${currentSyncData.assets.length}`) - reporter.info(`Deleted assets ${currentSyncData.deletedAssets.length}`) - - // Update syncToken - const nextSyncToken = currentSyncData.nextSyncToken - - await Promise.all([ - cache.set(CACHE_SYNC_DATA, mergedSyncDataRaw), - cache.set(CACHE_SYNC_TOKEN, nextSyncToken), - ]) - - reporter.verbose(`Building Contentful reference map`) - - // Create map of resolvable ids so we can check links against them while creating - // links. - const resolvable = normalize.buildResolvableSet({ - existingNodes, - entryList, - assets, - defaultLocale, - locales, - space, - }) - - // Build foreign reference map before starting to insert any nodes - const foreignReferenceMap = normalize.buildForeignReferenceMap({ - contentTypeItems, - entryList, - resolvable, - defaultLocale, - locales, - space, - useNameForId: pluginConfig.get(`useNameForId`), - }) - - reporter.verbose(`Resolving Contentful references`) - - const newOrUpdatedEntries = new Set() - entryList.forEach(entries => { - entries.forEach(entry => { - newOrUpdatedEntries.add(`${entry.sys.id}___${entry.sys.type}`) - }) - }) - - // Update existing entry nodes that weren't updated but that need reverse - // links added. - existingNodes - .filter(n => newOrUpdatedEntries.has(`${n.id}___${n.sys.type}`)) - .forEach(n => { - if (foreignReferenceMap[`${n.id}___${n.sys.type}`]) { - foreignReferenceMap[`${n.id}___${n.sys.type}`].forEach( - foreignReference => { - // Add reverse links - if (n[foreignReference.name]) { - n[foreignReference.name].push(foreignReference.id) - // It might already be there so we'll uniquify after pushing. - n[foreignReference.name] = _.uniq(n[foreignReference.name]) - } else { - // If is one foreign reference, there can always be many. - // Best to be safe and put it in an array to start with. - n[foreignReference.name] = [foreignReference.id] - } - } - ) - } - }) - - processingActivity.end() - - const creationActivity = reporter.activityTimer( - `Contentful: Create nodes (${sourceId})`, - { - parentSpan, - } - ) - creationActivity.start() - - for (let i = 0; i < contentTypeItems.length; i++) { - const contentTypeItem = contentTypeItems[i] - - if (entryList[i].length) { - reporter.info( - `Creating ${entryList[i].length} Contentful ${ - pluginConfig.get(`useNameForId`) - ? contentTypeItem.name - : contentTypeItem.sys.id - } nodes` - ) - } - - // A contentType can hold lots of entries which create nodes - // We wait until all nodes are created and processed until we handle the next one - // TODO add batching in gatsby-core - await Promise.all( - normalize.createNodesForContentType({ - contentTypeItem, - restrictedNodeFields, - conflictFieldPrefix, - entries: entryList[i], - createNode, - createNodeId, - getNode, - resolvable, - foreignReferenceMap, - defaultLocale, - locales, - space, - useNameForId: pluginConfig.get(`useNameForId`), - pluginConfig, - }) - ) - } - - if (assets.length) { - reporter.info(`Creating ${assets.length} Contentful Asset nodes`) - } - - for (let i = 0; i < assets.length; i++) { - // We wait for each asset to be process until handling the next one. - await Promise.all( - normalize.createAssetNodes({ - assetItem: assets[i], - createNode, - createNodeId, - defaultLocale, - locales, - space, - }) - ) - } - - // Create tags entities - if (tagItems.length) { - reporter.info(`Creating ${tagItems.length} Contentful Tag nodes`) - - for (const tag of tagItems) { - await createNode({ - id: createNodeId(`ContentfulTag__${space.sys.id}__${tag.sys.id}`), - name: tag.name, - contentful_id: tag.sys.id, - internal: { - type: `ContentfulTag`, - contentDigest: tag.sys.updatedAt, - }, - }) - } - } - - creationActivity.end() - - if (pluginConfig.get(`downloadLocal`)) { - reporter.info(`Download Contentful asset files`) - - await downloadContentfulAssets({ - actions, - createNodeId, - store, - cache, - getCache, - getNode, - getNodesByType, - reporter, - assetDownloadWorkers: pluginConfig.get(`assetDownloadWorkers`), - }) - } - - return -} - -// Check if there are any ContentfulAsset nodes and if gatsby-image is installed. If so, -// add fragments for ContentfulAsset and gatsby-image. The fragment will cause an error -// if there's not ContentfulAsset nodes and without gatsby-image, the fragment is useless. -exports.onPreExtractQueries = async ({ store }) => { - const program = store.getState().program - - const CACHE_DIR = path.resolve( - `${program.directory}/.cache/contentful/assets/` - ) - await fs.ensureDir(CACHE_DIR) -} diff --git a/packages/gatsby-source-contentful/src/on-pre-bootstrap.js b/packages/gatsby-source-contentful/src/on-pre-bootstrap.js new file mode 100644 index 0000000000000..63df556b0718a --- /dev/null +++ b/packages/gatsby-source-contentful/src/on-pre-bootstrap.js @@ -0,0 +1,14 @@ +// @ts-check +const fs = require(`fs-extra`) +const path = require(`path`) + +export async function onPreBootstrap({ store }) { + // Ensure cache dir exists for downloadLocal + const program = store.getState().program + + const CACHE_DIR = path.resolve( + `${program.directory}/.cache/contentful/assets/` + ) + + await fs.ensureDir(CACHE_DIR) +} diff --git a/packages/gatsby-source-contentful/src/plugin-options.js b/packages/gatsby-source-contentful/src/plugin-options.js index 5fe9d96fa3453..1a150c054d73f 100644 --- a/packages/gatsby-source-contentful/src/plugin-options.js +++ b/packages/gatsby-source-contentful/src/plugin-options.js @@ -9,7 +9,6 @@ const defaultOptions = { environment: `master`, downloadLocal: false, localeFilter: () => true, - forceFullSync: false, pageLimit: DEFAULT_PAGE_LIMIT, useNameForId: true, enableTags: false, diff --git a/packages/gatsby-source-contentful/src/source-nodes.js b/packages/gatsby-source-contentful/src/source-nodes.js new file mode 100644 index 0000000000000..b6217eb8a9f95 --- /dev/null +++ b/packages/gatsby-source-contentful/src/source-nodes.js @@ -0,0 +1,486 @@ +// @todo import syntax! +import _ from "lodash" +const { createClient } = require(`contentful`) + +import normalize from "./normalize" +const { createPluginConfig } = require(`./plugin-options`) +const { fetchContent } = require(`./fetch`) +const { CODES } = require(`./report`) +import { downloadContentfulAssets } from "./download-contentful-assets" + +const conflictFieldPrefix = `contentful` + +// restrictedNodeFields from here https://www.gatsbyjs.org/docs/node-interface/ +const restrictedNodeFields = [ + `children`, + `contentful_id`, + `fields`, + `id`, + `internal`, + `parent`, +] + +/*** + * Localization algorithm + * + * 1. Make list of all resolvable IDs worrying just about the default ids not + * localized ids + * 2. Make mapping between ids, again not worrying about localization. + * 3. When creating entries and assets, make the most localized version + * possible for each localized node i.e. get the localized field if it exists + * or the fallback field or the default field. + */ + +export async function sourceNodes( + { + actions, + getNode, + getNodes, + getNodesByType, + createNodeId, + store, + cache, + getCache, + reporter, + parentSpan, + }, + pluginOptions +) { + const { createNode, touchNode, deleteNode } = actions + const isOnline = require(`is-online`) + const online = await isOnline() + + if ( + !online && + process.env.GATSBY_CONTENTFUL_OFFLINE === `true` && + process.env.NODE_ENV !== `production` + ) { + getNodes().forEach(node => { + if (node.internal.owner !== `gatsby-source-contentful`) { + return + } + touchNode(node) + if (node.localFile___NODE) { + // Prevent GraphQL type inference from crashing on this property + touchNode(getNode(node.localFile___NODE)) + } + }) + + return + } + + const pluginConfig = createPluginConfig(pluginOptions) + const sourceId = `${pluginConfig.get(`spaceId`)}-${pluginConfig.get( + `environment` + )}` + + const fetchActivity = reporter.activityTimer( + `Contentful: Fetch data (${sourceId})`, + { + parentSpan, + } + ) + + // If the user knows they are offline, serve them cached result + // For prod builds though always fail if we can't get the latest data + if ( + !online && + process.env.GATSBY_CONTENTFUL_OFFLINE === `true` && + process.env.NODE_ENV !== `production` + ) { + reporter.info(`Using Contentful Offline cache ⚠️`) + reporter.info( + `Cache may be invalidated if you edit package.json, gatsby-node.js or gatsby-config.js files` + ) + + return + } + if (process.env.GATSBY_CONTENTFUL_OFFLINE) { + reporter.info( + `Note: \`GATSBY_CONTENTFUL_OFFLINE\` was set but it either was not \`true\`, we _are_ online, or we are in production mode, so the flag is ignored.` + ) + } + + fetchActivity.start() + + const CACHE_SYNC_TOKEN = `contentful-sync-token-${sourceId}` + const CACHE_SYNC_DATA = `contentful-sync-data-${sourceId}` + const CACHE_CONTENT_TYPES = `contentful-content-types-${sourceId}` + + /* + * Subsequent calls of Contentfuls sync API return only changed data. + * + * In some cases, especially when using rich-text fields, there can be data + * missing from referenced entries. This breaks the reference matching. + * + * To workround this, we cache the initial sync data and merge it + * with all data from subsequent syncs. Afterwards the references get + * resolved via the Contentful JS SDK. + */ + const syncToken = + store.getState().status.plugins?.[`gatsby-source-contentful`]?.[ + CACHE_SYNC_TOKEN + ] + + // Actual fetch of data from Contentful + const { + currentSyncData, + tagItems, + defaultLocale, + locales: allLocales, + space, + } = await fetchContent({ syncToken, pluginConfig, reporter }) + + const contentTypeItems = await cache.get(CACHE_CONTENT_TYPES) + + const locales = allLocales.filter(pluginConfig.get(`localeFilter`)) + reporter.verbose( + `Default locale: ${defaultLocale}. All locales: ${allLocales + .map(({ code }) => code) + .join(`, `)}` + ) + if (allLocales.length !== locales.length) { + reporter.verbose( + `After plugin.options.localeFilter: ${locales + .map(({ code }) => code) + .join(`, `)}` + ) + } + if (locales.length === 0) { + reporter.panic({ + id: CODES.LocalesMissing, + context: { + sourceMessage: `Please check if your localeFilter is configured properly. Locales '${allLocales + .map(item => item.code) + .join(`,`)}' were found but were filtered down to none.`, + }, + }) + } + + // Create a map of up to date entries and assets + function mergeSyncData(previous, current, deletedEntities) { + const deleted = new Set(deletedEntities.map(e => e.sys.id)) + const entryMap = new Map() + previous.forEach(e => !deleted.has(e.sys.id) && entryMap.set(e.sys.id, e)) + current.forEach(e => !deleted.has(e.sys.id) && entryMap.set(e.sys.id, e)) + return [...entryMap.values()] + } + + let previousSyncData = { + assets: [], + entries: [], + } + const cachedData = await cache.get(CACHE_SYNC_DATA) + + if (cachedData) { + previousSyncData = cachedData + } + + const mergedSyncData = { + entries: mergeSyncData( + previousSyncData.entries, + currentSyncData.entries, + currentSyncData.deletedEntries + ), + assets: mergeSyncData( + previousSyncData.assets, + currentSyncData.assets, + currentSyncData.deletedAssets + ), + } + + // @todo based on the sys metadata we should be able to differentiate new and updated entities + reporter.info( + `Contentful: ${currentSyncData.entries.length} new/updated entries` + ) + reporter.info( + `Contentful: ${currentSyncData.deletedEntries.length} deleted entries` + ) + reporter.info(`Contentful: ${previousSyncData.entries.length} cached entries`) + reporter.info( + `Contentful: ${currentSyncData.assets.length} new/updated assets` + ) + reporter.info(`Contentful: ${previousSyncData.assets.length} cached assets`) + reporter.info( + `Contentful: ${currentSyncData.deletedAssets.length} deleted assets` + ) + + // Update syncToken + const nextSyncToken = currentSyncData.nextSyncToken + + await cache.set(CACHE_SYNC_DATA, mergedSyncData) + actions.setPluginStatus({ + [CACHE_SYNC_TOKEN]: nextSyncToken, + }) + + fetchActivity.end() + + // Process data fetch results and turn them into GraphQL entities + const processingActivity = reporter.activityTimer( + `Contentful: Process data (${sourceId})`, + { + parentSpan, + } + ) + processingActivity.start() + + // Store a raw and unresolved copy of the data for caching + const mergedSyncDataRaw = _.cloneDeep(mergedSyncData) + + // Use the JS-SDK to resolve the entries and assets + const res = await createClient({ + space: `none`, + accessToken: `fake-access-token`, + }).parseEntries({ + items: mergedSyncData.entries, + includes: { + assets: mergedSyncData.assets, + entries: mergedSyncData.entries, + }, + }) + + mergedSyncData.entries = res.items + + // Inject raw API output to rich text fields + const richTextFieldMap = new Map() + contentTypeItems.forEach(contentType => { + richTextFieldMap.set( + contentType.sys.id, + contentType.fields + .filter(field => field.type === `RichText`) + .map(field => field.id) + ) + }) + + const rawEntries = new Map() + mergedSyncDataRaw.entries.forEach(rawEntry => + rawEntries.set(rawEntry.sys.id, rawEntry) + ) + + mergedSyncData.entries.forEach(entry => { + const contentTypeId = entry.sys.contentType.sys.id + const richTextFieldIds = richTextFieldMap.get(contentTypeId) + if (richTextFieldIds) { + richTextFieldIds.forEach(richTextFieldId => { + if (!entry.fields[richTextFieldId]) { + return + } + entry.fields[richTextFieldId] = rawEntries.get(entry.sys.id).fields[ + richTextFieldId + ] + }) + } + }) + + const { assets } = mergedSyncData + + const entryList = normalize.buildEntryList({ + mergedSyncData, + contentTypeItems, + }) + + const existingNodes = getNodes().filter( + n => + n.internal.owner === `gatsby-source-contentful` && + n.internal.type !== `ContentfulTag` + ) + existingNodes.forEach(n => touchNode(n)) + + reporter.verbose(`Building Contentful reference map`) + + // Create map of resolvable ids so we can check links against them while creating + // links. + const resolvable = normalize.buildResolvableSet({ + existingNodes, + entryList, + assets, + defaultLocale, + locales, + space, + }) + + // Build foreign reference map before starting to insert any nodes + const foreignReferenceMap = normalize.buildForeignReferenceMap({ + contentTypeItems, + entryList, + resolvable, + defaultLocale, + locales, + space, + useNameForId: pluginConfig.get(`useNameForId`), + }) + + reporter.verbose(`Resolving Contentful references`) + + const newOrUpdatedEntries = new Set() + entryList.forEach(entries => { + entries.forEach(entry => { + newOrUpdatedEntries.add(`${entry.sys.id}___${entry.sys.type}`) + }) + }) + + // Update existing entry nodes that weren't updated but that need reverse + // links added. + existingNodes + .filter(n => newOrUpdatedEntries.has(`${n.id}___${n.sys.type}`)) + .forEach(n => { + if (foreignReferenceMap[`${n.id}___${n.sys.type}`]) { + foreignReferenceMap[`${n.id}___${n.sys.type}`].forEach( + foreignReference => { + // Add reverse links + if (n[foreignReference.name]) { + n[foreignReference.name].push(foreignReference.id) + // It might already be there so we'll uniquify after pushing. + n[foreignReference.name] = _.uniq(n[foreignReference.name]) + } else { + // If is one foreign reference, there can always be many. + // Best to be safe and put it in an array to start with. + n[foreignReference.name] = [foreignReference.id] + } + } + ) + } + }) + + function deleteContentfulNode(node) { + const normalizedType = node.sys.type.startsWith(`Deleted`) + ? node.sys.type.substring(`Deleted`.length) + : node.sys.type + + const localizedNodes = locales + .map(locale => { + const nodeId = createNodeId( + normalize.makeId({ + spaceId: space.sys.id, + id: node.sys.id, + type: normalizedType, + currentLocale: locale.code, + defaultLocale, + }) + ) + return getNode(nodeId) + }) + .filter(node => node) + + localizedNodes.forEach(node => { + // touchNode first, to populate typeOwners & avoid erroring + touchNode(node) + deleteNode(node) + }) + } + + const { deletedEntries, deletedAssets } = currentSyncData + + if (deletedEntries.length || deletedAssets.length) { + const deletionActivity = reporter.activityTimer( + `Contentful: Deleting ${deletedEntries.length} nodes and ${deletedAssets.length} assets (${sourceId})`, + { + parentSpan, + } + ) + deletionActivity.start() + deletedEntries.forEach(deleteContentfulNode) + deletedAssets.forEach(deleteContentfulNode) + deletionActivity.end() + } + + const creationActivity = reporter.activityTimer( + `Contentful: Create nodes (${sourceId})`, + { + parentSpan, + } + ) + creationActivity.start() + + for (let i = 0; i < contentTypeItems.length; i++) { + const contentTypeItem = contentTypeItems[i] + + if (entryList[i].length) { + reporter.info( + `Creating ${entryList[i].length} Contentful ${ + pluginConfig.get(`useNameForId`) + ? contentTypeItem.name + : contentTypeItem.sys.id + } nodes` + ) + } + + // A contentType can hold lots of entries which create nodes + // We wait until all nodes are created and processed until we handle the next one + // TODO add batching in gatsby-core + await Promise.all( + normalize.createNodesForContentType({ + contentTypeItem, + contentTypeItems, + restrictedNodeFields, + conflictFieldPrefix, + entries: entryList[i], + createNode, + createNodeId, + getNode, + resolvable, + foreignReferenceMap, + defaultLocale, + locales, + space, + useNameForId: pluginConfig.get(`useNameForId`), + pluginConfig, + }) + ) + } + + if (assets.length) { + reporter.info(`Creating ${assets.length} Contentful asset nodes`) + } + + for (let i = 0; i < assets.length; i++) { + // We wait for each asset to be process until handling the next one. + await Promise.all( + normalize.createAssetNodes({ + assetItem: assets[i], + createNode, + createNodeId, + defaultLocale, + locales, + space, + }) + ) + } + + // Create tags entities + if (tagItems.length) { + reporter.info(`Creating ${tagItems.length} Contentful Tag nodes`) + + for (const tag of tagItems) { + await createNode({ + id: createNodeId(`ContentfulTag__${space.sys.id}__${tag.sys.id}`), + name: tag.name, + contentful_id: tag.sys.id, + internal: { + type: `ContentfulTag`, + contentDigest: tag.sys.updatedAt, + }, + }) + } + } + + creationActivity.end() + + // @todo add own activity! + + if (pluginConfig.get(`downloadLocal`)) { + reporter.info(`Download Contentful asset files`) + + await downloadContentfulAssets({ + actions, + createNodeId, + store, + cache, + getCache, + getNode, + getNodesByType, + reporter, + assetDownloadWorkers: pluginConfig.get(`assetDownloadWorkers`), + }) + } +}