diff --git a/.eslintignore b/.eslintignore index b97b3a4bfc442..8b999d9c690ad 100644 --- a/.eslintignore +++ b/.eslintignore @@ -24,6 +24,7 @@ packages/gatsby-source-shopify/**/*.js packages/gatsby-plugin-preload-fonts/prepare/*.js packages/gatsby/cache-dir/commonjs/**/* packages/gatsby-codemods/transforms +packages/gatsby-source-contentful/src/types/contentful-js-sdk packages/gatsby-source-graphql/batching packages/gatsby-plugin-gatsby-cloud/components packages/gatsby-plugin-gatsby-cloud/context diff --git a/e2e-tests/contentful/cypress.config.ts b/e2e-tests/contentful/cypress.config.ts index 47dfbf5428ffe..653fe79a4f7bf 100644 --- a/e2e-tests/contentful/cypress.config.ts +++ b/e2e-tests/contentful/cypress.config.ts @@ -1,30 +1,35 @@ import { defineConfig } from "cypress" import { addMatchImageSnapshotPlugin } from "@simonsmith/cypress-image-snapshot/plugin" +const specFolder = process.env.CTF_API === "preview" ? "preview" : "delivery" + export default defineConfig({ e2e: { baseUrl: `http://localhost:9000`, - specPattern: `cypress/integration/**/*.{js,ts}`, + specPattern: `cypress/integration/${specFolder}/**/*.{js,ts}`, projectId: `2193cm`, viewportWidth: 1440, viewportHeight: 900, retries: { runMode: 0, - openMode: 0 + openMode: 0, }, videoUploadOnPasses: false, setupNodeEvents(on, config) { addMatchImageSnapshotPlugin(on, config) - on("before:browser:launch", (browser = {} as Cypress.Browser, launchOptions) => { - if (browser.family === "chromium" || browser.name === "chrome") { - // Make retina screens run at 1x density so they match the versions in CI - launchOptions.args.push("--force-device-scale-factor=1") + on( + "before:browser:launch", + (browser = {} as Cypress.Browser, launchOptions) => { + if (browser.family === "chromium" || browser.name === "chrome") { + // Make retina screens run at 1x density so they match the versions in CI + launchOptions.args.push("--force-device-scale-factor=1") + } + return launchOptions } - return launchOptions - }) + ) }, }, env: { requireSnapshots: true, - } + }, }) \ No newline at end of file diff --git a/e2e-tests/contentful/cypress/integration/boolean.js b/e2e-tests/contentful/cypress/integration/delivery/boolean.js similarity index 100% rename from e2e-tests/contentful/cypress/integration/boolean.js rename to e2e-tests/contentful/cypress/integration/delivery/boolean.js diff --git a/e2e-tests/contentful/cypress/integration/delivery/content-reference.js b/e2e-tests/contentful/cypress/integration/delivery/content-reference.js new file mode 100644 index 0000000000000..f3edfa5c44056 --- /dev/null +++ b/e2e-tests/contentful/cypress/integration/delivery/content-reference.js @@ -0,0 +1,68 @@ +// Make sure to keep test from this file in sync with previews/content-reverence.js +describe(`content-reference`, () => { + beforeEach(() => { + cy.visit("/content-reference").waitForRouteChange() + }) + it(`content-reference-many-2nd-level-loop`, () => { + cy.get( + '[data-cy-id="default-content-reference-many-2nd-level-loop"]' + ).snapshot() + }) + it(`content-reference-many-loop-a-greater-b`, () => { + cy.get( + '[data-cy-id="default-content-reference-many-loop-a-greater-b"]' + ).snapshot() + }) + it(`content-reference-many-loop-b-greater-a`, () => { + cy.get( + '[data-cy-id="default-content-reference-many-loop-b-greater-a"]' + ).snapshot() + }) + it(`content-reference-many-self-reference`, () => { + cy.get( + '[data-cy-id="default-content-reference-many-self-reference"]' + ).snapshot() + }) + it(`content-reference-one`, () => { + cy.get('[data-cy-id="default-content-reference-one"]').snapshot() + }) + it(`content-reference-one-loop-a-greater-b`, () => { + cy.get( + '[data-cy-id="default-content-reference-one-loop-a-greater-b"]' + ).snapshot() + }) + it(`content-reference-one-loop-b-greater-a`, () => { + cy.get( + '[data-cy-id="default-content-reference-one-loop-b-greater-a"]' + ).snapshot() + }) + it(`content-reference-one-self-reference`, () => { + cy.get( + '[data-cy-id="default-content-reference-one-self-reference"]' + ).snapshot() + }) + // Previe api test. Content should not exist on delivery + it(`content-reference-preview-api-test`, () => { + cy.get( + '[data-cy-id="default-preview-api-test-unpublished-and-linking-to-non-existing-entry"]' + ).should("not.exist") + }) +}) + +describe(`content-reference localized`, () => { + beforeEach(() => { + cy.visit("/content-reference").waitForRouteChange() + }) + it(`english-content-reference-one-localized`, () => { + cy.get('[data-cy-id="english-content-reference-one-localized"]').snapshot() + }) + it(`english-content-reference-many-localized`, () => { + cy.get('[data-cy-id="english-content-reference-many-localized"]').snapshot() + }) + it(`german-content-reference-one-localized`, () => { + cy.get('[data-cy-id="german-content-reference-one-localized"]').snapshot() + }) + it(`german-content-reference-many-localized`, () => { + cy.get('[data-cy-id="german-content-reference-many-localized"]').snapshot() + }) +}) diff --git a/e2e-tests/contentful/cypress/integration/delivery/custom-fields.js b/e2e-tests/contentful/cypress/integration/delivery/custom-fields.js new file mode 100644 index 0000000000000..337d5964eb097 --- /dev/null +++ b/e2e-tests/contentful/cypress/integration/delivery/custom-fields.js @@ -0,0 +1,19 @@ +describe(`custom-fields`, () => { + beforeEach(() => { + cy.visit("/custom-fields").waitForRouteChange() + }) + + it(`custom-fields: custom field`, () => { + cy.get(`[data-cy-id="field"] [data-cy-value]`).should( + `have.text`, + `customFieldValue` + ) + }) + + it(`custom-fields: custom resolver`, () => { + cy.get(`[data-cy-id="resolver"] [data-cy-value]`).should( + `have.text`, + `customResolverResult` + ) + }) +}) diff --git a/e2e-tests/contentful/cypress/integration/date.js b/e2e-tests/contentful/cypress/integration/delivery/date.js similarity index 100% rename from e2e-tests/contentful/cypress/integration/date.js rename to e2e-tests/contentful/cypress/integration/delivery/date.js diff --git a/e2e-tests/contentful/cypress/integration/download-local.js b/e2e-tests/contentful/cypress/integration/delivery/download-local.js similarity index 100% rename from e2e-tests/contentful/cypress/integration/download-local.js rename to e2e-tests/contentful/cypress/integration/delivery/download-local.js diff --git a/e2e-tests/contentful/cypress/integration/engines.js b/e2e-tests/contentful/cypress/integration/delivery/engines.js similarity index 100% rename from e2e-tests/contentful/cypress/integration/engines.js rename to e2e-tests/contentful/cypress/integration/delivery/engines.js diff --git a/e2e-tests/contentful/cypress/integration/gatsby-image-cdn.js b/e2e-tests/contentful/cypress/integration/delivery/gatsby-image-cdn.js similarity index 100% rename from e2e-tests/contentful/cypress/integration/gatsby-image-cdn.js rename to e2e-tests/contentful/cypress/integration/delivery/gatsby-image-cdn.js diff --git a/e2e-tests/contentful/cypress/integration/gatsby-plugin-image.js b/e2e-tests/contentful/cypress/integration/delivery/gatsby-plugin-image.js similarity index 97% rename from e2e-tests/contentful/cypress/integration/gatsby-plugin-image.js rename to e2e-tests/contentful/cypress/integration/delivery/gatsby-plugin-image.js index 1678cea7af2d2..eba57e3049717 100644 --- a/e2e-tests/contentful/cypress/integration/gatsby-plugin-image.js +++ b/e2e-tests/contentful/cypress/integration/delivery/gatsby-plugin-image.js @@ -77,9 +77,6 @@ describe(`gatsby-plugin-image`, () => { it(`dominant-color`, testConfig, () => testGatsbyPluginImage(`dominant-color`, hasColorPlaceholder) ) - it(`traced`, testConfig, () => - testGatsbyPluginImage(`traced`, hasColorPlaceholder) - ) it(`blurred`, testConfig, () => testGatsbyPluginImage(`blurred`, hasBase64Placeholder) ) diff --git a/e2e-tests/contentful/cypress/integration/json.js b/e2e-tests/contentful/cypress/integration/delivery/json.js similarity index 100% rename from e2e-tests/contentful/cypress/integration/json.js rename to e2e-tests/contentful/cypress/integration/delivery/json.js diff --git a/e2e-tests/contentful/cypress/integration/location.js b/e2e-tests/contentful/cypress/integration/delivery/location.js similarity index 100% rename from e2e-tests/contentful/cypress/integration/location.js rename to e2e-tests/contentful/cypress/integration/delivery/location.js diff --git a/e2e-tests/contentful/cypress/integration/media-reference.js b/e2e-tests/contentful/cypress/integration/delivery/media-reference.js similarity index 92% rename from e2e-tests/contentful/cypress/integration/media-reference.js rename to e2e-tests/contentful/cypress/integration/delivery/media-reference.js index 7f0a38496e25c..4cf2e785c064d 100644 --- a/e2e-tests/contentful/cypress/integration/media-reference.js +++ b/e2e-tests/contentful/cypress/integration/delivery/media-reference.js @@ -7,7 +7,7 @@ describe(`media-reference`, () => { cy.get("img") .should("have.length", 2) .should("have.attr", "src") - .should("match", /^\/\/images\.ctfassets\.net/) + .should("match", /^https:\/\/images\.ctfassets\.net/) }) }) @@ -15,7 +15,7 @@ describe(`media-reference`, () => { cy.get('[data-cy-id="media-reference-one"]').within(() => { cy.get("img") .should("have.attr", "src") - .should("match", /^\/\/images\.ctfassets\.net/) + .should("match", /^https:\/\/images\.ctfassets\.net/) }) }) }) diff --git a/e2e-tests/contentful/cypress/integration/number.js b/e2e-tests/contentful/cypress/integration/delivery/number.js similarity index 100% rename from e2e-tests/contentful/cypress/integration/number.js rename to e2e-tests/contentful/cypress/integration/delivery/number.js diff --git a/e2e-tests/contentful/cypress/integration/rich-text.js b/e2e-tests/contentful/cypress/integration/delivery/rich-text.js similarity index 100% rename from e2e-tests/contentful/cypress/integration/rich-text.js rename to e2e-tests/contentful/cypress/integration/delivery/rich-text.js diff --git a/e2e-tests/contentful/cypress/integration/tags.js b/e2e-tests/contentful/cypress/integration/delivery/tags.js similarity index 100% rename from e2e-tests/contentful/cypress/integration/tags.js rename to e2e-tests/contentful/cypress/integration/delivery/tags.js diff --git a/e2e-tests/contentful/cypress/integration/text.js b/e2e-tests/contentful/cypress/integration/delivery/text.js similarity index 100% rename from e2e-tests/contentful/cypress/integration/text.js rename to e2e-tests/contentful/cypress/integration/delivery/text.js diff --git a/e2e-tests/contentful/cypress/integration/content-reference.js b/e2e-tests/contentful/cypress/integration/preview/content-reference.js similarity index 87% rename from e2e-tests/contentful/cypress/integration/content-reference.js rename to e2e-tests/contentful/cypress/integration/preview/content-reference.js index a7b476d2e98f5..ba104cdfdf889 100644 --- a/e2e-tests/contentful/cypress/integration/content-reference.js +++ b/e2e-tests/contentful/cypress/integration/preview/content-reference.js @@ -1,4 +1,4 @@ -describe(`content-reference`, () => { +describe(`prewview-content-reference`, () => { beforeEach(() => { cy.visit("/content-reference").waitForRouteChange() }) @@ -40,6 +40,12 @@ describe(`content-reference`, () => { '[data-cy-id="default-content-reference-one-self-reference"]' ).snapshot() }) + // Most relevant preview api test (the others are still good to test!) + it(`content-reference-preview-api-test`, () => { + cy.get( + '[data-cy-id="default-preview-api-test-unpublished-and-linking-to-non-existing-entry"]' + ).snapshot() + }) }) describe(`content-reference localized`, () => { diff --git a/e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-image-cdn.js/gatsby-image-cdn-0.snap.png b/e2e-tests/contentful/cypress/snapshots/cypress/integration/delivery/gatsby-image-cdn.js/gatsby-image-cdn-0.snap.png similarity index 100% rename from e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-image-cdn.js/gatsby-image-cdn-0.snap.png rename to e2e-tests/contentful/cypress/snapshots/cypress/integration/delivery/gatsby-image-cdn.js/gatsby-image-cdn-0.snap.png diff --git a/e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/blurred-0.snap.png b/e2e-tests/contentful/cypress/snapshots/cypress/integration/delivery/gatsby-plugin-image.js/blurred-0.snap.png similarity index 100% rename from e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/blurred-0.snap.png rename to e2e-tests/contentful/cypress/snapshots/cypress/integration/delivery/gatsby-plugin-image.js/blurred-0.snap.png diff --git a/e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/blurred-1.snap.png b/e2e-tests/contentful/cypress/snapshots/cypress/integration/delivery/gatsby-plugin-image.js/blurred-1.snap.png similarity index 100% rename from e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/blurred-1.snap.png rename to e2e-tests/contentful/cypress/snapshots/cypress/integration/delivery/gatsby-plugin-image.js/blurred-1.snap.png diff --git a/e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/constrained-0.snap.png b/e2e-tests/contentful/cypress/snapshots/cypress/integration/delivery/gatsby-plugin-image.js/constrained-0.snap.png similarity index 100% rename from e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/constrained-0.snap.png rename to e2e-tests/contentful/cypress/snapshots/cypress/integration/delivery/gatsby-plugin-image.js/constrained-0.snap.png diff --git a/e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/constrained-1.snap.png b/e2e-tests/contentful/cypress/snapshots/cypress/integration/delivery/gatsby-plugin-image.js/constrained-1.snap.png similarity index 100% rename from e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/constrained-1.snap.png rename to e2e-tests/contentful/cypress/snapshots/cypress/integration/delivery/gatsby-plugin-image.js/constrained-1.snap.png diff --git a/e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/dominant-color-0.snap.png b/e2e-tests/contentful/cypress/snapshots/cypress/integration/delivery/gatsby-plugin-image.js/dominant-color-0.snap.png similarity index 100% rename from e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/dominant-color-0.snap.png rename to e2e-tests/contentful/cypress/snapshots/cypress/integration/delivery/gatsby-plugin-image.js/dominant-color-0.snap.png diff --git a/e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/dominant-color-1.snap.png b/e2e-tests/contentful/cypress/snapshots/cypress/integration/delivery/gatsby-plugin-image.js/dominant-color-1.snap.png similarity index 100% rename from e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/dominant-color-1.snap.png rename to e2e-tests/contentful/cypress/snapshots/cypress/integration/delivery/gatsby-plugin-image.js/dominant-color-1.snap.png diff --git a/e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/english-0.snap.png b/e2e-tests/contentful/cypress/snapshots/cypress/integration/delivery/gatsby-plugin-image.js/english-0.snap.png similarity index 100% rename from e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/english-0.snap.png rename to e2e-tests/contentful/cypress/snapshots/cypress/integration/delivery/gatsby-plugin-image.js/english-0.snap.png diff --git a/e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/fixed-0.snap.png b/e2e-tests/contentful/cypress/snapshots/cypress/integration/delivery/gatsby-plugin-image.js/fixed-0.snap.png similarity index 100% rename from e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/fixed-0.snap.png rename to e2e-tests/contentful/cypress/snapshots/cypress/integration/delivery/gatsby-plugin-image.js/fixed-0.snap.png diff --git a/e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/fixed-1.snap.png b/e2e-tests/contentful/cypress/snapshots/cypress/integration/delivery/gatsby-plugin-image.js/fixed-1.snap.png similarity index 100% rename from e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/fixed-1.snap.png rename to e2e-tests/contentful/cypress/snapshots/cypress/integration/delivery/gatsby-plugin-image.js/fixed-1.snap.png diff --git a/e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/full-width-0.snap.png b/e2e-tests/contentful/cypress/snapshots/cypress/integration/delivery/gatsby-plugin-image.js/full-width-0.snap.png similarity index 100% rename from e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/full-width-0.snap.png rename to e2e-tests/contentful/cypress/snapshots/cypress/integration/delivery/gatsby-plugin-image.js/full-width-0.snap.png diff --git a/e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/full-width-1.snap.png b/e2e-tests/contentful/cypress/snapshots/cypress/integration/delivery/gatsby-plugin-image.js/full-width-1.snap.png similarity index 100% rename from e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/full-width-1.snap.png rename to e2e-tests/contentful/cypress/snapshots/cypress/integration/delivery/gatsby-plugin-image.js/full-width-1.snap.png diff --git a/e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/german-0.snap.png b/e2e-tests/contentful/cypress/snapshots/cypress/integration/delivery/gatsby-plugin-image.js/german-0.snap.png similarity index 100% rename from e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/german-0.snap.png rename to e2e-tests/contentful/cypress/snapshots/cypress/integration/delivery/gatsby-plugin-image.js/german-0.snap.png diff --git a/e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/sqip-0.snap.png b/e2e-tests/contentful/cypress/snapshots/cypress/integration/delivery/gatsby-plugin-image.js/sqip-0.snap.png similarity index 100% rename from e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/sqip-0.snap.png rename to e2e-tests/contentful/cypress/snapshots/cypress/integration/delivery/gatsby-plugin-image.js/sqip-0.snap.png diff --git a/e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/sqip-1.snap.png b/e2e-tests/contentful/cypress/snapshots/cypress/integration/delivery/gatsby-plugin-image.js/sqip-1.snap.png similarity index 100% rename from e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/sqip-1.snap.png rename to e2e-tests/contentful/cypress/snapshots/cypress/integration/delivery/gatsby-plugin-image.js/sqip-1.snap.png diff --git a/e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/traced-0.snap.png b/e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/traced-0.snap.png deleted file mode 100644 index 93d687d8b98fb..0000000000000 Binary files a/e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/traced-0.snap.png and /dev/null differ diff --git a/e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/traced-1.snap.png b/e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/traced-1.snap.png deleted file mode 100644 index 953dab32137ee..0000000000000 Binary files a/e2e-tests/contentful/cypress/snapshots/cypress/integration/gatsby-plugin-image.js/traced-1.snap.png and /dev/null differ diff --git a/e2e-tests/contentful/gatsby-config.js b/e2e-tests/contentful/gatsby-config.js index afb02a147b78e..cddc47663dfa4 100644 --- a/e2e-tests/contentful/gatsby-config.js +++ b/e2e-tests/contentful/gatsby-config.js @@ -1,3 +1,17 @@ +const ctfOptions = { + // This space is for testing purposes only. + // Never store your Contentful credentials in your projects config file. + // Use: https://www.gatsbyjs.com/docs/how-to/local-development/environment-variables/ + spaceId: `k8iqpp6u0ior`, + accessToken: `hO_7N0bLaCJFbu5nL3QVekwNeB_TNtg6tOCB_9qzKUw`, + downloadLocal: true, +} + +if (process.env.CTF_API === "preview") { + ctfOptions.host = `preview.contentful.com` + ctfOptions.accessToken = `IkSw3qEFt8NHKQUko2hIVDgu6x3AMtAkNecQZvg2034` +} + module.exports = { siteMetadata: { title: `Gatsby Contentful e2e`, @@ -5,15 +19,7 @@ module.exports = { plugins: [ { resolve: `gatsby-source-contentful`, - options: { - // This space is for testing purposes only. - // Never store your Contentful credentials in your projects config file. - // Use: https://www.gatsbyjs.com/docs/how-to/local-development/environment-variables/ - spaceId: `k8iqpp6u0ior`, - accessToken: `hO_7N0bLaCJFbu5nL3QVekwNeB_TNtg6tOCB_9qzKUw`, - enableTags: true, - downloadLocal: true, - }, + options: ctfOptions, }, `gatsby-transformer-remark`, `gatsby-transformer-sharp`, diff --git a/e2e-tests/contentful/gatsby-node.js b/e2e-tests/contentful/gatsby-node.js new file mode 100644 index 0000000000000..4a90b408c7c57 --- /dev/null +++ b/e2e-tests/contentful/gatsby-node.js @@ -0,0 +1,39 @@ +exports.onCreateNode = ({ node, actions }) => { + const { createNodeField } = actions + + if (node.internal.type === "ContentfulContentTypeText") { + createNodeField({ + node, + name: "customField", + value: "customFieldValue", + }) + } +} + +exports.createSchemaCustomization = ({ actions, schema }) => { + const { createTypes } = actions + + const typeDefs = ` + type ContentfulContentTypeTextFields { + customField: String! + } + type ContentfulContentTypeText { + fields: ContentfulContentTypeTextFields! + } + ` + + createTypes(typeDefs) +} + +exports.createResolvers = ({ createResolvers }) => { + createResolvers({ + ContentfulContentTypeText: { + customResolver: { + type: 'String!', + resolve(source, args, context, info) { + return "customResolverResult" + }, + }, + }, + }) +} \ No newline at end of file diff --git a/e2e-tests/contentful/package.json b/e2e-tests/contentful/package.json index b79b74b616dc7..4aa4b2bd691d5 100644 --- a/e2e-tests/contentful/package.json +++ b/e2e-tests/contentful/package.json @@ -24,6 +24,7 @@ "cross-env": "^7.0.3", "cypress": "^12.16.0", "gatsby-cypress": "next", + "npm-run-all": "^4.1.5", "prettier": "^2.8.8", "srcset": "^5.0.0", "start-server-and-test": "^2.0.3", @@ -38,7 +39,9 @@ "build": "gatsby build", "develop": "gatsby develop", "format": "prettier --write '**/*.js'", - "test": "cross-env CYPRESS_SUPPORT=y npm run build && npm run start-server-and-test", + "test": "npm-run-all -c -s test:delivery test:preview", + "test:delivery": "gatsby clean && cross-env CYPRESS_SUPPORT=y CTF_API=delivery npm run build && CTF_API=delivery npm run start-server-and-test", + "test:preview": "gatsby clean && cross-env CYPRESS_SUPPORT=y CTF_API=preview npm run build && CTF_API=preview npm run start-server-and-test", "start-server-and-test": "start-server-and-test serve http://localhost:9000 cy:run", "serve": "gatsby serve", "cy:open": "cypress open --browser chrome --e2e", diff --git a/e2e-tests/contentful/schema.gql b/e2e-tests/contentful/schema.gql index 7a501577d38cb..ee6b281d3c8da 100644 --- a/e2e-tests/contentful/schema.gql +++ b/e2e-tests/contentful/schema.gql @@ -1,4 +1,188 @@ -### Type definitions saved at 2021-09-25T11:33:25.217Z ### +### Type definitions saved at 2023-06-16T10:42:44.280Z ### + +enum RemoteFileFit { + COVER + FILL + OUTSIDE + CONTAIN +} + +enum RemoteFileFormat { + AUTO + JPG + PNG + WEBP + AVIF +} + +enum RemoteFileLayout { + FIXED + FULL_WIDTH + CONSTRAINED +} + +enum RemoteFilePlaceholder { + DOMINANT_COLOR + BLURRED + TRACED_SVG + NONE +} + +enum RemoteFileCropFocus { + CENTER + TOP + RIGHT + BOTTOM + LEFT + ENTROPY + EDGES + FACES +} + +type RemoteFileResize { + width: Int + height: Int + src: String +} + +"""Remote Interface""" +interface RemoteFile { + id: ID! + mimeType: String! + filename: String! + filesize: Int + width: Int + height: Int + publicUrl: String! + resize( + width: Int + height: Int + aspectRatio: Float + fit: RemoteFileFit = COVER + + """ + + The image formats to generate. Valid values are AUTO (meaning the same + format as the source image), JPG, PNG, WEBP and AVIF. + The default value is [AUTO, WEBP, AVIF], and you should rarely need to + change this. Take care if you specify JPG or PNG when you do + not know the formats of the source images, as this could lead to unwanted + results such as converting JPEGs to PNGs. Specifying + both PNG and JPG is not supported and will be ignored. + """ + format: RemoteFileFormat = AUTO + cropFocus: [RemoteFileCropFocus] + quality: Int = 75 + ): RemoteFileResize + + """ + Data used in the component. See https://gatsby.dev/img for more info. + """ + gatsbyImage( + """ + + The layout for the image. + FIXED: A static image sized, that does not resize according to the screen width + FULL_WIDTH: The image resizes to fit its container. Pass a "sizes" option if + it isn't going to be the full width of the screen. + CONSTRAINED: Resizes to fit its container, up to a maximum width, at which point it will remain fixed in size. + + """ + layout: RemoteFileLayout = CONSTRAINED + + """ + + The display width of the generated image for layout = FIXED, and the display + width of the largest image for layout = CONSTRAINED. + The actual largest image resolution will be this value multiplied by the largest value in outputPixelDensities + Ignored if layout = FLUID. + + """ + width: Int + + """ + + If set, the height of the generated image. If omitted, it is calculated from + the supplied width, matching the aspect ratio of the source image. + """ + height: Int + + """ + + Format of generated placeholder image, displayed while the main image loads. + BLURRED: a blurred, low resolution image, encoded as a base64 data URI + DOMINANT_COLOR: a solid color, calculated from the dominant color of the image (default). + TRACED_SVG: deprecated. Will use DOMINANT_COLOR. + NONE: no placeholder. Set the argument "backgroundColor" to use a fixed background color. + """ + placeholder: RemoteFilePlaceholder = DOMINANT_COLOR + + """ + + If set along with width or height, this will set the value of the other + dimension to match the provided aspect ratio, cropping the image if needed. + If neither width or height is provided, height will be set based on the intrinsic width of the source image. + + """ + aspectRatio: Float + + """ + + The image formats to generate. Valid values are AUTO (meaning the same + format as the source image), JPG, PNG, WEBP and AVIF. + The default value is [AUTO, WEBP, AVIF], and you should rarely need to + change this. Take care if you specify JPG or PNG when you do + not know the formats of the source images, as this could lead to unwanted + results such as converting JPEGs to PNGs. Specifying + both PNG and JPG is not supported and will be ignored. + + """ + formats: [RemoteFileFormat!] = [AUTO, WEBP, AVIF] + + """ + + A list of image pixel densities to generate for FIXED and CONSTRAINED + images. You should rarely need to change this. It will never generate images + larger than the source, and will always include a 1x image. + Default is [ 1, 2 ] for fixed images, meaning 1x, 2x, and [0.25, 0.5, 1, 2] + for fluid. In this case, an image with a fluid layout and width = 400 would + generate images at 100, 200, 400 and 800px wide. + + """ + outputPixelDensities: [Float] = [0.25, 0.5, 1, 2] + + """ + + Specifies the image widths to generate. You should rarely need to change + this. For FIXED and CONSTRAINED images it is better to allow these to be + determined automatically, + based on the image size. For FULL_WIDTH images this can be used to override + the default, which is [750, 1080, 1366, 1920]. + It will never generate any images larger than the source. + + """ + breakpoints: [Int] = [750, 1080, 1366, 1920] + + """ + + The "sizes" property, passed to the img tag. This describes the display size of the image. + This does not affect the generated images, but is used by the browser to + decide which images to download. You can leave this blank for fixed images, + or if the responsive image + container will be the full width of the screen. In these cases we will generate an appropriate value. + + """ + sizes: String + + """ + Background color applied to the wrapper, or when "letterboxing" an image to another aspect ratio. + """ + backgroundColor: String + fit: RemoteFileFit = COVER + cropFocus: [RemoteFileCropFocus] + quality: Int = 75 + ): GatsbyImageData +} type File implements Node @dontInfer { sourceInstanceName: String! @@ -32,6 +216,9 @@ type File implements Node @dontInfer { ctime: Date! @dateformat birthtime: Date @deprecated(reason: "Use `birthTime` instead") birthtimeMs: Float @deprecated(reason: "Use `birthTime` instead") + blksize: Int + blocks: Int + url: String } type Directory implements Node @dontInfer { @@ -71,8 +258,13 @@ type Directory implements Node @dontInfer { type Site implements Node @dontInfer { buildTime: Date @dateformat siteMetadata: SiteSiteMetadata + port: Int + host: String polyfill: Boolean pathPrefix: String + jsxRuntime: String + trailingSlash: String + graphqlTypegen: Boolean } type SiteSiteMetadata { @@ -96,7 +288,8 @@ type SitePage implements Node @dontInfer { internalComponentName: String! componentChunkName: String! matchPath: String - pageContext: JSON + pageContext: JSON @proxy(from: "context", fromNode: false) + pluginCreator: SitePlugin @link(by: "id", from: "pluginCreatorId") } type SitePlugin implements Node @dontInfer { @@ -115,429 +308,372 @@ type SiteBuildMetadata implements Node @dontInfer { buildTime: Date @dateformat } -interface ContentfulEntry implements Node { - contentful_id: String! +interface ContentfulEntity implements Node { id: ID! - node_locale: String! + sys: ContentfulSys! + metadata: ContentfulMetadata! } -interface ContentfulReference { - contentful_id: String! - id: ID! +type ContentfulSys implements Node @dontInfer { + type: String! + spaceId: String! + environmentId: String! + contentType: ContentfulContentType @link(by: "id", from: "sys.contentType") + firstPublishedAt: Date! + publishedAt: Date! + publishedVersion: Int! + locale: String! } -type ContentfulAsset implements ContentfulReference & Node @derivedTypes @dontInfer { - contentful_id: String! - spaceId: String - createdAt: Date @dateformat - updatedAt: Date @dateformat - file: ContentfulAssetFile - title: String - description: String - node_locale: String - sys: ContentfulAssetSys +type ContentfulContentType implements Node @dontInfer { + name: String! + displayField: String! + description: String! } -type ContentfulAssetFile @derivedTypes { - url: String - details: ContentfulAssetFileDetails - fileName: String - contentType: String +type ContentfulMetadata @dontInfer { + tags: [ContentfulTag]! @link(by: "id", from: "tags") } -type ContentfulAssetFileDetails @derivedTypes { - size: Int - image: ContentfulAssetFileDetailsImage +type ContentfulTag implements Node @dontInfer { + name: String! + contentful_id: String! } -type ContentfulAssetFileDetailsImage { - width: Int - height: Int +interface ContentfulEntry implements ContentfulEntity & Node { + id: ID! + sys: ContentfulSys! + metadata: ContentfulMetadata! + linkedFrom: ContentfulLinkedFrom } -type ContentfulAssetSys { - type: String - revision: Int +type ContentfulLinkedFrom @dontInfer { + ContentfulContentTypeMediaReference: [ContentfulContentTypeMediaReference] @link(by: "id", from: "ContentfulContentTypeMediaReference") + ContentfulContentTypeContentReference: [ContentfulContentTypeContentReference] @link(by: "id", from: "ContentfulContentTypeContentReference") + ContentfulContentTypeValidatedContentReference: [ContentfulContentTypeValidatedContentReference] @link(by: "id", from: "ContentfulContentTypeValidatedContentReference") } -type ContentfulNumber implements ContentfulReference & ContentfulEntry & Node @derivedTypes @dontInfer { - contentful_id: String! - node_locale: String! +type ContentfulContentTypeMediaReference implements ContentfulEntity & ContentfulEntry & Node @dontInfer { + sys: ContentfulSys! + metadata: ContentfulMetadata! title: String - 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 + one: ContentfulAsset @link(by: "id", from: "one") + oneLocalized: ContentfulAsset @link(by: "id", from: "oneLocalized") + many: [ContentfulAsset] @link(by: "id", from: "many") + manyLocalized: [ContentfulAsset] @link(by: "id", from: "manyLocalized") + linkedFrom: ContentfulLinkedFrom +} + +type ContentfulAsset implements ContentfulEntity & Node & RemoteFile @dontInfer { + sys: ContentfulSys! + metadata: ContentfulMetadata! + gatsbyImageData( + """ + The layout for the image. + FIXED: A static image sized, that does not resize according to the screen width + FULL_WIDTH: The image resizes to fit its container. Pass a "sizes" option if + it isn't going to be the full width of the screen. + CONSTRAINED: Resizes to fit its container, up to a maximum width, at which point it will remain fixed in size. + """ + layout: GatsbyImageLayout + + """ + The display width of the generated image for layout = FIXED, and the display + width of the largest image for layout = CONSTRAINED. + The actual largest image resolution will be this value multiplied by the largest value in outputPixelDensities + Ignored if layout = FLUID. + """ + width: Int + + """ + If set, the height of the generated image. If omitted, it is calculated from + the supplied width, matching the aspect ratio of the source image. + """ + height: Int + + """ + If set along with width or height, this will set the value of the other + dimension to match the provided aspect ratio, cropping the image if needed. + If neither width or height is provided, height will be set based on the intrinsic width of the source image. + """ + aspectRatio: Float + + """ + Format of generated placeholder image, displayed while the main image loads. + BLURRED: a blurred, low resolution image, encoded as a base64 data URI. + DOMINANT_COLOR: a solid color, calculated from the dominant color of the image (default). + TRACED_SVG: deprecated. Will use DOMINANT_COLOR. + NONE: no placeholder. Set the argument "backgroundColor" to use a fixed background color. + """ + placeholder: GatsbyImagePlaceholder + + """ + The image formats to generate. Valid values are AUTO (meaning the same + format as the source image), JPG, PNG, WEBP and AVIF. + The default value is [AUTO, WEBP], and you should rarely need to change + this. Take care if you specify JPG or PNG when you do + not know the formats of the source images, as this could lead to unwanted + results such as converting JPEGs to PNGs. Specifying + both PNG and JPG is not supported and will be ignored. + """ + formats: [GatsbyImageFormat] = [NO_CHANGE, WEBP] + + """ + A list of image pixel densities to generate for FIXED and CONSTRAINED + images. You should rarely need to change this. It will never generate images + larger than the source, and will always include a 1x image. + Default is [ 1, 2 ] for fixed images, meaning 1x, 2x, 3x, and [0.25, 0.5, 1, + 2] for fluid. In this case, an image with a fluid layout and width = 400 + would generate images at 100, 200, 400 and 800px wide. + """ + outputPixelDensities: [Float] + + """ + Specifies the image widths to generate. You should rarely need to change + this. For FIXED and CONSTRAINED images it is better to allow these to be + determined automatically, + based on the image size. For FULL_WIDTH images this can be used to override + the default, which is [750, 1080, 1366, 1920]. + It will never generate any images larger than the source. + """ + breakpoints: [Int] + + """ + The "sizes" property, passed to the img tag. This describes the display size of the image. + This does not affect the generated images, but is used by the browser to + decide which images to download. You can leave this blank for fixed images, + or if the responsive image + container will be the full width of the screen. In these cases we will generate an appropriate value. + """ + sizes: String + + """ + Background color applied to the wrapper, or when "letterboxing" an image to another aspect ratio. + """ + backgroundColor: String + jpegProgressive: Boolean = true + resizingBehavior: ImageResizingBehavior + cropFocus: ContentfulImageCropFocus + + """ + Desired corner radius in pixels. Results in an image with rounded corners. + Pass `-1` for a full circle/ellipse. + """ + cornerRadius: Int + quality: Int = 50 + ): JSON + localFile: File @link(from: "fields.localFile", by: "id") + title: String + description: String + contentType: String! + mimeType: String! + filename: String! + url: String! + size: Int + width: Int + height: Int + linkedFrom: ContentfulLinkedFrom } -type ContentfulNumberSys @derivedTypes { - type: String - revision: Int - contentType: ContentfulNumberSysContentType +enum GatsbyImageLayout { + FIXED + FULL_WIDTH + CONSTRAINED } -type ContentfulNumberSysContentType @derivedTypes { - sys: ContentfulNumberSysContentTypeSys +enum GatsbyImagePlaceholder { + DOMINANT_COLOR + TRACED_SVG + BLURRED + NONE } -type ContentfulNumberSysContentTypeSys { - type: String - linkType: String - id: String +enum GatsbyImageFormat { + NO_CHANGE + AUTO + JPG + PNG + WEBP + AVIF } -type ContentfulNumberMetadata { - tags: [ContentfulTag] @link(by: "id", from: "tags___NODE") -} +enum ImageResizingBehavior { + NO_CHANGE -type ContentfulTag implements Node @dontInfer { - name: String! - contentful_id: String! -} + """ + Same as the default resizing, but adds padding so that the generated image has the specified dimensions. + """ + PAD -type ContentfulContentReference implements ContentfulReference & ContentfulEntry & Node @derivedTypes @dontInfer { - contentful_id: String! - node_locale: String! - title: String - 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") - many: [ContentfulContentReferenceContentfulNumberContentfulTextUnion] @link(by: "id", from: "many___NODE") - manyLocalized: [ContentfulNumberContentfulTextUnion] @link(by: "id", from: "manyLocalized___NODE") -} + """Crop a part of the original image to match the specified size.""" + CROP -union ContentfulContentReferenceContentfulTextUnion = ContentfulContentReference | ContentfulText + """ + Crop the image to the specified dimensions, if the original image is smaller + than these dimensions, then the image will be upscaled. + """ + FILL -type ContentfulContentReferenceSys @derivedTypes { - type: String - revision: Int - contentType: ContentfulContentReferenceSysContentType -} + """ + When used in association with the f parameter below, creates a thumbnail from the image based on a focus area. + """ + THUMB -type ContentfulContentReferenceSysContentType @derivedTypes { - sys: ContentfulContentReferenceSysContentTypeSys + """Scale the image regardless of the original aspect ratio.""" + SCALE } -type ContentfulContentReferenceSysContentTypeSys { - type: String - linkType: String - id: String +enum ContentfulImageCropFocus { + TOP + TOP_LEFT + TOP_RIGHT + BOTTOM + BOTTOM_RIGHT + BOTTOM_LEFT + RIGHT + LEFT + FACE + FACES + CENTER } -union ContentfulContentReferenceContentfulNumberContentfulTextUnion = ContentfulContentReference | ContentfulNumber | ContentfulText +type ContentfulContentTypeContentReference implements ContentfulEntity & ContentfulEntry & Node @isPlaceholder @dontInfer { + sys: ContentfulSys! + metadata: ContentfulMetadata! + title: String + one: ContentfulEntry @link(by: "id", from: "one") + oneLocalized: ContentfulEntry @link(by: "id", from: "oneLocalized") + many: [UnionContentfulContentReferenceNumberText] @link(by: "id", from: "many") + manyLocalized: [ContentfulEntry] @link(by: "id", from: "manyLocalized") + linkedFrom: ContentfulLinkedFrom +} -union ContentfulNumberContentfulTextUnion = ContentfulNumber | ContentfulText +union UnionContentfulContentReferenceNumberText = ContentfulContentTypeContentReference | ContentfulContentTypeNumber | ContentfulContentTypeText -type ContentfulText implements ContentfulReference & ContentfulEntry & Node @derivedTypes @dontInfer { - contentful_id: String! - node_locale: String! +type ContentfulContentTypeValidatedContentReference implements ContentfulEntity & ContentfulEntry & Node @dontInfer { + sys: ContentfulSys! + metadata: ContentfulMetadata! title: String - longLocalized: contentfulTextLongLocalizedTextNode @link(by: "id", from: "longLocalized___NODE") - spaceId: String - createdAt: Date @dateformat - updatedAt: Date @dateformat - sys: ContentfulTextSys - longMarkdown: contentfulTextLongMarkdownTextNode @link(by: "id", from: "longMarkdown___NODE") + oneItemSingleType: ContentfulContentTypeText @link(by: "id", from: "oneItemSingleType") + oneItemManyTypes: UnionContentfulNumberText @link(by: "id", from: "oneItemManyTypes") + oneItemAllTypes: ContentfulEntry @link(by: "id", from: "oneItemAllTypes") + multipleItemsSingleType: [ContentfulContentTypeText] @link(by: "id", from: "multipleItemsSingleType") + multipleItemsManyTypes: [UnionContentfulNumberText] @link(by: "id", from: "multipleItemsManyTypes") + multipleItemsAllTypes: [ContentfulEntry] @link(by: "id", from: "multipleItemsAllTypes") + linkedFrom: ContentfulLinkedFrom +} + +type ContentfulContentTypeText implements ContentfulEntity & ContentfulEntry & Node @dontInfer { + sys: ContentfulSys! + metadata: ContentfulMetadata! + title: String + short: String 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") + shortListLocalized: [String] + longPlain: ContentfulText @link(by: "id", from: "longPlain") + longMarkdown: ContentfulText @link(by: "id", from: "longMarkdown") + longLocalized: ContentfulText @link(by: "id", from: "longLocalized") + linkedFrom: ContentfulLinkedFrom } -type contentfulTextLongLocalizedTextNode implements Node @derivedTypes @childOf(types: ["ContentfulText"]) @dontInfer { - longLocalized: String - sys: contentfulTextLongLocalizedTextNodeSys +type ContentfulText implements Node @dontInfer { + raw: String! } -type contentfulTextLongLocalizedTextNodeSys { - type: String -} - -type ContentfulTextSys @derivedTypes { - type: String - revision: Int - contentType: ContentfulTextSysContentType -} - -type ContentfulTextSysContentType @derivedTypes { - sys: ContentfulTextSysContentTypeSys -} +union UnionContentfulNumberText = ContentfulContentTypeNumber | ContentfulContentTypeText -type ContentfulTextSysContentTypeSys { - type: String - linkType: String - id: String +type ContentfulRichTextAssets { + block: [ContentfulAsset]! + hyperlink: [ContentfulAsset]! } -type contentfulTextLongMarkdownTextNode implements Node @derivedTypes @childOf(types: ["ContentfulText"]) @dontInfer { - longMarkdown: String - sys: contentfulTextLongMarkdownTextNodeSys +type ContentfulRichTextEntries { + inline: [ContentfulEntry]! + block: [ContentfulEntry]! + hyperlink: [ContentfulEntry]! } -type contentfulTextLongMarkdownTextNodeSys { - type: String +type ContentfulRichTextLinks { + assets: ContentfulRichTextAssets + entries: ContentfulRichTextEntries } -type contentfulTextLongPlainTextNode implements Node @derivedTypes @childOf(types: ["ContentfulText"]) @dontInfer { - longPlain: String - sys: contentfulTextLongPlainTextNodeSys +type ContentfulRichText @dontInfer { + json: JSON + links: ContentfulRichTextLinks } -type contentfulTextLongPlainTextNodeSys { - type: String +type ContentfulLocation @dontInfer { + lat: Float! + lon: Float! } -type ContentfulMediaReference implements ContentfulReference & ContentfulEntry & Node @derivedTypes @dontInfer { - contentful_id: String! - node_locale: String! +type ContentfulContentTypeRequiredFields implements ContentfulEntity & ContentfulEntry & Node @dontInfer { + sys: ContentfulSys! + metadata: ContentfulMetadata! title: String - one: ContentfulAsset @link(by: "id", from: "one___NODE") - spaceId: String - createdAt: Date @dateformat - updatedAt: Date @dateformat - sys: ContentfulMediaReferenceSys - oneLocalized: ContentfulAsset @link(by: "id", from: "oneLocalized___NODE") - many: [ContentfulAsset] @link(by: "id", from: "many___NODE") - manyLocalized: [ContentfulAsset] @link(by: "id", from: "manyLocalized___NODE") -} - -type ContentfulMediaReferenceSys @derivedTypes { - type: String - revision: Int - contentType: ContentfulMediaReferenceSysContentType -} - -type ContentfulMediaReferenceSysContentType @derivedTypes { - sys: ContentfulMediaReferenceSysContentTypeSys + fillMe: String + linkedFrom: ContentfulLinkedFrom } -type ContentfulMediaReferenceSysContentTypeSys { - type: String - linkType: String - id: String -} - -type ContentfulBoolean implements ContentfulReference & ContentfulEntry & Node @derivedTypes @dontInfer { - contentful_id: String! - node_locale: String! +type ContentfulContentTypeRichText implements ContentfulEntity & ContentfulEntry & Node @dontInfer { + sys: ContentfulSys! + metadata: ContentfulMetadata! title: String - booleanLocalized: Boolean - spaceId: String - createdAt: Date @dateformat - updatedAt: Date @dateformat - sys: ContentfulBooleanSys - boolean: Boolean -} - -type ContentfulBooleanSys @derivedTypes { - type: String - revision: Int - contentType: ContentfulBooleanSysContentType + richText: ContentfulRichText + richTextLocalized: ContentfulRichText + richTextValidated: ContentfulRichText + linkedFrom: ContentfulLinkedFrom } -type ContentfulBooleanSysContentType @derivedTypes { - sys: ContentfulBooleanSysContentTypeSys -} - -type ContentfulBooleanSysContentTypeSys { - type: String - linkType: String - id: String -} - -type ContentfulDate implements ContentfulReference & ContentfulEntry & Node @derivedTypes @dontInfer { - contentful_id: String! - node_locale: String! +type ContentfulContentTypeNumber implements ContentfulEntity & ContentfulEntry & Node @dontInfer { + sys: ContentfulSys! + metadata: ContentfulMetadata! title: String - dateTimeTimezone: Date @dateformat - spaceId: String - createdAt: Date @dateformat - updatedAt: Date @dateformat - sys: ContentfulDateSys - date: Date @dateformat - dateLocalized: Date @dateformat - dateTime: Date @dateformat -} - -type ContentfulDateSys @derivedTypes { - type: String - revision: Int - contentType: ContentfulDateSysContentType -} - -type ContentfulDateSysContentType @derivedTypes { - sys: ContentfulDateSysContentTypeSys -} - -type ContentfulDateSysContentTypeSys { - type: String - linkType: String - id: String + integer: Int + integerLocalized: Int + decimal: Float + decimalLocalized: Float + linkedFrom: ContentfulLinkedFrom } -type ContentfulLocation implements ContentfulReference & ContentfulEntry & Node @derivedTypes @dontInfer { - contentful_id: String! - node_locale: String! +type ContentfulContentTypeLocation implements ContentfulEntity & ContentfulEntry & Node @dontInfer { + sys: ContentfulSys! + metadata: ContentfulMetadata! title: String - locationLocalized: ContentfulLocationLocationLocalized - spaceId: String - createdAt: Date @dateformat - updatedAt: Date @dateformat - sys: ContentfulLocationSys - location: ContentfulLocationLocation -} - -type ContentfulLocationLocationLocalized { - lon: Float - lat: Float -} - -type ContentfulLocationSys @derivedTypes { - type: String - revision: Int - contentType: ContentfulLocationSysContentType -} - -type ContentfulLocationSysContentType @derivedTypes { - sys: ContentfulLocationSysContentTypeSys -} - -type ContentfulLocationSysContentTypeSys { - type: String - linkType: String - id: String -} - -type ContentfulLocationLocation { - lat: Float - lon: Float + location: ContentfulLocation + locationLocalized: ContentfulLocation + linkedFrom: ContentfulLinkedFrom } -type ContentfulJson implements ContentfulReference & ContentfulEntry & Node @derivedTypes @dontInfer { - contentful_id: String! - node_locale: String! +type ContentfulContentTypeJson implements ContentfulEntity & ContentfulEntry & Node @dontInfer { + sys: ContentfulSys! + metadata: ContentfulMetadata! title: String - json: contentfulJsonJsonJsonNode @link(by: "id", from: "json___NODE") - spaceId: String - createdAt: Date @dateformat - updatedAt: Date @dateformat - sys: ContentfulJsonSys - jsonLocalized: contentfulJsonJsonLocalizedJsonNode @link(by: "id", from: "jsonLocalized___NODE") -} - -type contentfulJsonJsonJsonNode implements Node @derivedTypes @childOf(types: ["ContentfulJson"]) @dontInfer { - age: Int - city: String - name: String - sys: contentfulJsonJsonJsonNodeSys - Actors: [contentfulJsonJsonJsonNodeActors] -} - -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 - contentType: ContentfulJsonSysContentType -} - -type ContentfulJsonSysContentType @derivedTypes { - sys: ContentfulJsonSysContentTypeSys -} - -type ContentfulJsonSysContentTypeSys { - type: String - linkType: String - id: String -} - -type contentfulJsonJsonLocalizedJsonNode implements Node @derivedTypes @childOf(types: ["ContentfulJson"]) @dontInfer { - name: String - age: Int - city: String - sys: contentfulJsonJsonLocalizedJsonNodeSys + json: JSON + jsonLocalized: JSON + linkedFrom: ContentfulLinkedFrom } -type contentfulJsonJsonLocalizedJsonNodeSys { - type: String -} - -type ContentfulRichText implements ContentfulReference & ContentfulEntry & Node @derivedTypes @dontInfer { - contentful_id: String! - node_locale: String! +type ContentfulContentTypeBoolean implements ContentfulEntity & ContentfulEntry & Node @dontInfer { + sys: ContentfulSys! + metadata: ContentfulMetadata! title: String - richText: ContentfulRichTextRichText - spaceId: String - createdAt: Date @dateformat - updatedAt: Date @dateformat - sys: ContentfulRichTextSys - richTextValidated: ContentfulRichTextRichTextValidated - richTextLocalized: ContentfulRichTextRichTextLocalized -} - -type ContentfulRichTextRichText { - raw: String - references: [ContentfulAssetContentfulContentReferenceContentfulLocationContentfulTextUnion] @link(by: "id", from: "references___NODE") -} - -union ContentfulAssetContentfulContentReferenceContentfulLocationContentfulTextUnion = ContentfulAsset | ContentfulContentReference | ContentfulLocation | ContentfulText - -type ContentfulRichTextSys @derivedTypes { - type: String - revision: Int - contentType: ContentfulRichTextSysContentType -} - -type ContentfulRichTextSysContentType @derivedTypes { - sys: ContentfulRichTextSysContentTypeSys -} - -type ContentfulRichTextSysContentTypeSys { - type: String - linkType: String - id: String -} - -type ContentfulRichTextRichTextValidated { - raw: String - references: [ContentfulAssetContentfulLocationContentfulNumberContentfulTextUnion] @link(by: "id", from: "references___NODE") -} - -union ContentfulAssetContentfulLocationContentfulNumberContentfulTextUnion = ContentfulAsset | ContentfulLocation | ContentfulNumber | ContentfulText - -type ContentfulRichTextRichTextLocalized { - raw: String + boolean: Boolean + booleanLocalized: Boolean + linkedFrom: ContentfulLinkedFrom } -type ContentfulValidatedContentReference implements ContentfulReference & ContentfulEntry & Node @dontInfer { - contentful_id: String! - node_locale: String! +type ContentfulContentTypeDate implements ContentfulEntity & ContentfulEntry & Node @dontInfer { + sys: ContentfulSys! + metadata: ContentfulMetadata! + title: String + date: Date @dateformat + dateTime: Date @dateformat + dateTimeTimezone: Date @dateformat + dateLocalized: Date @dateformat + linkedFrom: ContentfulLinkedFrom } type MarkdownHeading { @@ -567,7 +703,7 @@ type MarkdownWordCount { words: Int } -type MarkdownRemark implements Node @childOf(mimeTypes: ["text/markdown", "text/x-markdown"], types: ["contentfulTextLongPlainTextNode", "contentfulTextLongMarkdownTextNode", "contentfulTextLongLocalizedTextNode"]) @derivedTypes @dontInfer { +type MarkdownRemark implements Node @childOf(mimeTypes: ["text/markdown", "text/x-markdown"], types: ["ContentfulText"]) @derivedTypes @dontInfer { frontmatter: MarkdownRemarkFrontmatter excerpt: String rawMarkdownBody: String @@ -577,13 +713,305 @@ type MarkdownRemarkFrontmatter { title: String } -type ContentfulContentType implements Node @derivedTypes @dontInfer { - name: String - displayField: String - description: String - sys: ContentfulContentTypeSys +enum ImageFormat { + NO_CHANGE + AUTO + JPG + PNG + WEBP + AVIF +} + +enum ImageFit { + COVER + CONTAIN + FILL + INSIDE + OUTSIDE +} + +enum ImageLayout { + FIXED + FULL_WIDTH + CONSTRAINED +} + +enum ImageCropFocus { + CENTER + NORTH + NORTHEAST + EAST + SOUTHEAST + SOUTH + SOUTHWEST + WEST + NORTHWEST + ENTROPY + ATTENTION +} + +input DuotoneGradient { + highlight: String! + shadow: String! + opacity: Int +} + +enum PotraceTurnPolicy { + TURNPOLICY_BLACK + TURNPOLICY_WHITE + TURNPOLICY_LEFT + TURNPOLICY_RIGHT + TURNPOLICY_MINORITY + TURNPOLICY_MAJORITY +} + +input Potrace { + turnPolicy: PotraceTurnPolicy + turdSize: Float + alphaMax: Float + optCurve: Boolean + optTolerance: Float + threshold: Int + blackOnWhite: Boolean + color: String + background: String +} + +type ImageSharpFixed { + base64: String + tracedSVG: String + aspectRatio: Float + width: Float! + height: Float! + src: String! + srcSet: String! + srcWebp: String + srcSetWebp: String + originalName: String +} + +type ImageSharpFluid { + base64: String + tracedSVG: String + aspectRatio: Float! + src: String! + srcSet: String! + srcWebp: String + srcSetWebp: String + sizes: String! + originalImg: String + originalName: String + presentationWidth: Int! + presentationHeight: Int! +} + +enum ImagePlaceholder { + DOMINANT_COLOR + TRACED_SVG + BLURRED + NONE +} + +input BlurredOptions { + """Width of the generated low-res preview. Default is 20px""" + width: Int + + """ + Force the output format for the low-res preview. Default is to use the same + format as the input. You should rarely need to change this + """ + toFormat: ImageFormat +} + +input JPGOptions { + quality: Int + progressive: Boolean = true +} + +input PNGOptions { + quality: Int + compressionSpeed: Int = 4 +} + +input WebPOptions { + quality: Int } -type ContentfulContentTypeSys { - type: String +input AVIFOptions { + quality: Int + lossless: Boolean + speed: Int +} + +input TransformOptions { + grayscale: Boolean + duotone: DuotoneGradient + rotate: Int + trim: Float + cropFocus: ImageCropFocus = ATTENTION + fit: ImageFit = COVER +} + +type ImageSharpOriginal { + width: Float + height: Float + src: String +} + +type ImageSharpResize { + src: String + tracedSVG: String + width: Int + height: Int + aspectRatio: Float + originalName: String +} + +type ImageSharp implements Node @childOf(types: ["File"]) @dontInfer { + fixed(width: Int, height: Int, base64Width: Int, jpegProgressive: Boolean = true, pngCompressionSpeed: Int = 4, grayscale: Boolean, duotone: DuotoneGradient, traceSVG: Potrace, quality: Int, jpegQuality: Int, pngQuality: Int, webpQuality: Int, toFormat: ImageFormat, toFormatBase64: ImageFormat, cropFocus: ImageCropFocus = ATTENTION, fit: ImageFit = COVER, background: String = "rgba(0,0,0,1)", rotate: Int, trim: Float): ImageSharpFixed + fluid( + maxWidth: Int + maxHeight: Int + base64Width: Int + grayscale: Boolean + jpegProgressive: Boolean = true + pngCompressionSpeed: Int = 4 + duotone: DuotoneGradient + traceSVG: Potrace + quality: Int + jpegQuality: Int + pngQuality: Int + webpQuality: Int + toFormat: ImageFormat + toFormatBase64: ImageFormat + cropFocus: ImageCropFocus = ATTENTION + fit: ImageFit = COVER + background: String = "rgba(0,0,0,1)" + rotate: Int + trim: Float + sizes: String + + """ + A list of image widths to be generated. Example: [ 200, 340, 520, 890 ] + """ + srcSetBreakpoints: [Int] = [] + ): ImageSharpFluid + gatsbyImageData( + """ + The layout for the image. + FIXED: A static image sized, that does not resize according to the screen width + FULL_WIDTH: The image resizes to fit its container. Pass a "sizes" option if + it isn't going to be the full width of the screen. + CONSTRAINED: Resizes to fit its container, up to a maximum width, at which point it will remain fixed in size. + """ + layout: ImageLayout = CONSTRAINED + + """ + The display width of the generated image for layout = FIXED, and the maximum + display width of the largest image for layout = CONSTRAINED. + Ignored if layout = FLUID. + """ + width: Int + + """ + The display height of the generated image for layout = FIXED, and the + maximum display height of the largest image for layout = CONSTRAINED. + The image will be cropped if the aspect ratio does not match the source + image. If omitted, it is calculated from the supplied width, + matching the aspect ratio of the source image. + """ + height: Int + + """ + If set along with width or height, this will set the value of the other + dimension to match the provided aspect ratio, cropping the image if needed. + If neither width or height is provided, height will be set based on the intrinsic width of the source image. + """ + aspectRatio: Float + + """ + Format of generated placeholder image, displayed while the main image loads. + BLURRED: a blurred, low resolution image, encoded as a base64 data URI + DOMINANT_COLOR: a solid color, calculated from the dominant color of the image (default). + TRACED_SVG: deprecated. Will use DOMINANT_COLOR. + NONE: no placeholder. Set "background" to use a fixed background color. + """ + placeholder: ImagePlaceholder + + """ + Options for the low-resolution placeholder image. Set placeholder to "BLURRED" to use this + """ + blurredOptions: BlurredOptions + + """ + Options for traced placeholder SVGs. You also should set placeholder to "TRACED_SVG". + """ + tracedSVGOptions: Potrace + + """ + The image formats to generate. Valid values are "AUTO" (meaning the same + format as the source image), "JPG", "PNG", "WEBP" and "AVIF". + The default value is [AUTO, WEBP], and you should rarely need to change + this. Take care if you specify JPG or PNG when you do + not know the formats of the source images, as this could lead to unwanted + results such as converting JPEGs to PNGs. Specifying + both PNG and JPG is not supported and will be ignored. + """ + formats: [ImageFormat] + + """ + A list of image pixel densities to generate. It will never generate images + larger than the source, and will always include a 1x image. + Default is [ 1, 2 ] for FIXED images, meaning 1x and 2x and [0.25, 0.5, 1, + 2] for CONSTRAINED. In this case, an image with a constrained layout + and width = 400 would generate images at 100, 200, 400 and 800px wide. + Ignored for FULL_WIDTH images, which use breakpoints instead + """ + outputPixelDensities: [Float] + + """ + Specifies the image widths to generate. For FIXED and CONSTRAINED images it + is better to allow these to be determined automatically, + based on the image size. For FULL_WIDTH images this can be used to override + the default, which is [750, 1080, 1366, 1920]. + It will never generate any images larger than the source. + """ + breakpoints: [Int] + + """ + The "sizes" property, passed to the img tag. This describes the display size of the image. + This does not affect the generated images, but is used by the browser to decide which images to download. + You should usually leave this blank, and a suitable value will be calculated. The exception is if a FULL_WIDTH image + does not actually span the full width of the screen, in which case you should pass the correct size here. + """ + sizes: String + + """The default quality. This is overridden by any format-specific options""" + quality: Int + + """Options to pass to sharp when generating JPG images.""" + jpgOptions: JPGOptions + + """Options to pass to sharp when generating PNG images.""" + pngOptions: PNGOptions + + """Options to pass to sharp when generating WebP images.""" + webpOptions: WebPOptions + + """Options to pass to sharp when generating AVIF images.""" + avifOptions: AVIFOptions + + """ + Options to pass to sharp to control cropping and other image manipulations. + """ + transformOptions: TransformOptions + + """ + Background color applied to the wrapper. Also passed to sharp to use as a + background when "letterboxing" an image to another aspect ratio. + """ + backgroundColor: String + ): GatsbyImageData! + original: ImageSharpOriginal + resize(width: Int, height: Int, quality: Int, jpegQuality: Int, pngQuality: Int, webpQuality: Int, jpegProgressive: Boolean = true, pngCompressionLevel: Int = 9, pngCompressionSpeed: Int = 4, grayscale: Boolean, duotone: DuotoneGradient, base64: Boolean, traceSVG: Potrace, toFormat: ImageFormat, cropFocus: ImageCropFocus = ATTENTION, fit: ImageFit = COVER, background: String = "rgba(0,0,0,1)", rotate: Int, trim: Float): ImageSharpResize } \ No newline at end of file diff --git a/e2e-tests/contentful/snapshots.js b/e2e-tests/contentful/snapshots.js index c6aa33e43d0ce..3ab7f26d0f4cd 100644 --- a/e2e-tests/contentful/snapshots.js +++ b/e2e-tests/contentful/snapshots.js @@ -1,58 +1,58 @@ module.exports = { - "__version": "9.7.0", + "__version": "12.14.0", "content-reference": { "content-reference-many-2nd-level-loop": { - "1": "
\n

Content Reference: Many (2nd level loop)

\n

[ContentfulNumber]\n 42

\n

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

\n

[ContentfulReference]\n Content Reference: One (Loop A -> B)\n : [\n Content Reference: One (Loop B -> A)\n ]

\n
" + "1": "
\n

Content Reference: Many (2nd level loop)\n (\n 4R29nQaAkgJFB5pkruYW9i\n )

\n

[ContentfulContentTypeNumber]\n 42

\n

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

\n

[ContentfulContentTypeContentReference]\n Content Reference: One (Loop A -> B)\n : [\n Content Reference: One (Loop B -> A)\n ]

\n

Linked from:

\n
{\n  \"ContentfulContentTypeContentReference\": null\n}
\n
" }, "content-reference-many-loop-a-greater-b": { - "1": "
\n

Content Reference: Many (Loop A -> B)

\n

[ContentfulNumber]\n 42

\n

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

\n

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

\n
" + "1": "
\n

Content Reference: Many (Loop A -> B)\n (\n 4NHKxzHnMwSjjMLSdUrcFo\n )

\n

[ContentfulContentTypeNumber]\n 42

\n

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

\n

[ContentfulContentTypeContentReference]\n Content Reference: Many (Loop B -> A)\n : [\n Number: Integer, Text: Short, Content Reference: Many (Loop A ->\n B)\n ]

\n

Linked from:

\n
{\n  \"ContentfulContentTypeContentReference\": [\n    {\n      \"sys\": {\n        \"id\": \"7IrXy5KcEujLUsaWVJAK69\"\n      }\n    }\n  ]\n}
\n
" }, "content-reference-many-loop-b-greater-a": { - "1": "
\n

Content Reference: Many (Loop B -> A)

\n

[ContentfulNumber]\n 42

\n

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

\n

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

\n
" + "1": "
\n

Content Reference: Many (Loop B -> A)\n (\n 7IrXy5KcEujLUsaWVJAK69\n )

\n

[ContentfulContentTypeNumber]\n 42

\n

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

\n

[ContentfulContentTypeContentReference]\n Content Reference: Many (Loop A -> B)\n : [\n Number: Integer, Text: Short, Content Reference: Many (Loop B ->\n A)\n ]

\n

Linked from:

\n
{\n  \"ContentfulContentTypeContentReference\": [\n    {\n      \"sys\": {\n        \"id\": \"4NHKxzHnMwSjjMLSdUrcFo\"\n      }\n    }\n  ]\n}
\n
" }, "content-reference-many-self-reference": { - "1": "
\n

Content Reference: Many (Self Reference)

\n

[ContentfulNumber]\n 42

\n

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

\n

[ContentfulReference]\n Content Reference: Many (Self Reference)\n : [\n Number: Integer, Text: Short, Content Reference: Many (Self\n Reference)\n ]

\n
" + "1": "
\n

Content Reference: Many (Self Reference)\n (\n 50SBswk2hZU4G4ITVIPuuU\n )

\n

[ContentfulContentTypeNumber]\n 42

\n

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

\n

[ContentfulContentTypeContentReference]\n Content Reference: Many (Self Reference)\n : [\n Number: Integer, Text: Short, Content Reference: Many (Self\n Reference)\n ]

\n

Linked from:

\n
{\n  \"ContentfulContentTypeContentReference\": [\n    {\n      \"sys\": {\n        \"id\": \"50SBswk2hZU4G4ITVIPuuU\"\n      }\n    }\n  ]\n}
\n
" }, "content-reference-one": { - "1": "
\n

Content Reference: One

\n

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

\n
" + "1": "
\n

Content Reference: One\n (\n 77UBlVUq3oeVidhtAhrByt\n )

\n

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

\n

Linked from:

\n
{\n  \"ContentfulContentTypeContentReference\": null\n}
\n
" }, "content-reference-one-loop-a-greater-b": { - "1": "
\n

Content Reference: One (Loop A -> B)

\n

[ContentfulReference]\n Content Reference: One (Loop B -> A)\n : [\n Content Reference: One (Loop A -> B)\n ]

\n
" + "1": "
\n

Content Reference: One (Loop A -> B)\n (\n LkOtAXoxqqX8Y9cRRPjRb\n )

\n

[ContentfulContentTypeContentReference]\n Content Reference: One (Loop B -> A)\n : [\n Content Reference: One (Loop A -> B)\n ]

\n

Linked from:

\n
{\n  \"ContentfulContentTypeContentReference\": [\n    {\n      \"sys\": {\n        \"id\": \"4R29nQaAkgJFB5pkruYW9i\"\n      }\n    },\n    {\n      \"sys\": {\n        \"id\": \"37m4vschoGYmwUMlgxAa74\"\n      }\n    }\n  ]\n}
\n
" }, "content-reference-one-loop-b-greater-a": { - "1": "
\n

Content Reference: One (Loop B -> A)

\n

[ContentfulReference]\n Content Reference: One (Loop A -> B)\n : [\n Content Reference: One (Loop B -> A)\n ]

\n
" + "1": "
\n

Content Reference: One (Loop B -> A)\n (\n 37m4vschoGYmwUMlgxAa74\n )

\n

[ContentfulContentTypeContentReference]\n Content Reference: One (Loop A -> B)\n : [\n Content Reference: One (Loop B -> A)\n ]

\n

Linked from:

\n
{\n  \"ContentfulContentTypeContentReference\": [\n    {\n      \"sys\": {\n        \"id\": \"LkOtAXoxqqX8Y9cRRPjRb\"\n      }\n    }\n  ]\n}
\n
" }, "content-reference-one-self-reference": { - "1": "
\n

Content Reference: One (Self Reference)

\n

[ContentfulReference]\n Content Reference: One (Self Reference)\n : [\n Content Reference: One (Self Reference)\n ]

\n
" + "1": "
\n

Content Reference: One (Self Reference)\n (\n 3Lq0a8ymtbGueurkbuM2xN\n )

\n

[ContentfulContentTypeContentReference]\n Content Reference: One (Self Reference)\n : [\n Content Reference: One (Self Reference)\n ]

\n

Linked from:

\n
{\n  \"ContentfulContentTypeContentReference\": [\n    {\n      \"sys\": {\n        \"id\": \"3Lq0a8ymtbGueurkbuM2xN\"\n      }\n    }\n  ]\n}
\n
" } }, "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 \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 \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-ContentfulContentTypeText]\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

[ContentfulContentTypeLocation] 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
" }, "rich-text: Embedded Entry": { - "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
" + "1": "
\n

Rich Text: Embedded Entry

\n

Embedded Entry

\n

[ContentfulContentTypeText]\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 \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
" + "1": "
\n

Rich Text: Embedded entry with deep reference loop

\n

Embedded entry with deep reference loop

\n

[ContentfulContentTypeContentReference]\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
" }, "rich-text: Embedded Entry With Reference Loop": { - "1": "
\n

Rich Text: Embedded entry with reference loop

\n

Embedded entry with reference loop

\n

[ContentfulReference]\n Content Reference: One (Loop B -> A)\n : [\n Content Reference: One (Loop A -> B)\n ]

\n

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

Rich Text: Embedded entry with reference loop

\n

Embedded entry with reference loop

\n

[ContentfulContentTypeContentReference]\n Content Reference: One (Loop B -> A)\n : [\n Content Reference: One (Loop A -> B)\n ]

\n

\n
\n
" }, "rich-text: Inline Entry": { - "1": "
\n

Rich Text: Inline entry

\n

Inline entry with reference loop

\n

Should be rendered after this [Inline-ContentfulText]\n Text: Short\n :\n The quick brown fox jumps over the lazy dog. and before\n that

\n

\n

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

Rich Text: Inline entry

\n

Inline entry with reference loop

\n

Should be rendered after this [Inline-ContentfulContentTypeText]\n The quick brown fox jumps over the lazy dog. and before\n that

\n

\n

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

Rich Text: Inline entry with deep reference loop

\n

Inline entry with deep reference loop

\n

Should be rendered after this [Inline-\n ContentfulContentReference\n ]\n Content Reference: Many (2nd level loop) and before that\n

\n

\n

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

Rich Text: Inline entry with deep reference loop

\n

Inline entry with deep reference loop

\n

Should be rendered after this [Inline-\n ContentfulContentTypeContentReference\n ] and before that

\n

\n

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

Rich Text: Inline entry with reference loop

\n

Inline entry with reference loop

\n

Should be rendered after this [Inline-\n ContentfulContentReference\n ]\n Content Reference: One (Loop A -> B) and before that

\n

\n

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

Rich Text: Inline entry with reference loop

\n

Inline entry with reference loop

\n

Should be rendered after this [Inline-\n ContentfulContentTypeContentReference\n ] and before that

\n

\n

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

Rich Text: Localized

\n

Rich Text in English

\n
\n
", @@ -85,34 +85,63 @@ module.exports = { }, "content-reference localized": { "english-content-reference-one-localized": { - "1": "
\n

Content Reference: One Localized

\n

[ContentfulNumber]\n 42

\n
" + "1": "
\n

Content Reference: One Localized

\n

[ContentfulContentTypeNumber]\n 42

\n
" }, "english-content-reference-many-localized": { - "1": "
\n

Content Reference: Many Localized

\n

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

\n

[ContentfulNumber]\n 42

\n
" + "1": "
\n

Content Reference: Many Localized

\n

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

\n

[ContentfulContentTypeNumber]\n 42

\n
" }, "german-content-reference-one-localized": { - "1": "
\n

Content Reference: One Localized

\n

[ContentfulNumber]\n 4.2

\n
" + "1": "
\n

Content Reference: One Localized

\n

[ContentfulContentTypeNumber]\n 4.2

\n
" }, "german-content-reference-many-localized": { - "1": "
\n

Content Reference: Many Localized

\n

[ContentfulNumber]\n 4.2

\n

[ContentfulText]\n The European languages are members of the same family. Their\n separate existence is a myth. For science, music, sport, etc, Europe uses\n the same vocabulary.\n\n The languages only differ in their grammar, their pronunciation and their\n most common words. Everyone realizes why a new common language would be\n desirable: one could refuse to pay expensive translators.\n\n To achieve this, it would be necessary to have uniform grammar,\n pronunciation and more common words. If several languages coalesce, the\n grammar of the resulting language is more simple and regular than that of\n the individual languages. The new common language will be more simple and\n regular than the existing European languages. It will be as simple as\n Occidental; in fact, it will be.

\n
" + "1": "
\n

Content Reference: Many Localized

\n

[ContentfulContentTypeNumber]\n 4.2

\n

[ContentfulContentTypeText]\n The European languages are members of the same family. Their\n separate existence is a myth. For science, music, sport, etc, Europe uses\n the same vocabulary.\n\n The languages only differ in their grammar, their pronunciation and their\n most common words. Everyone realizes why a new common language would be\n desirable: one could refuse to pay expensive translators.\n\n To achieve this, it would be necessary to have uniform grammar,\n pronunciation and more common words. If several languages coalesce, the\n grammar of the resulting language is more simple and regular than that of\n the individual languages. The new common language will be more simple and\n regular than the existing European languages. It will be as simple as\n Occidental; in fact, it will be.

\n
" } }, "media-reference localized": { "media-reference: many with localized field": { - "1": "
\n

Media Reference: Many Localized Field

\n
", - "2": "
\n

Media Reference: Many Localized Field

\n
" + "1": "
\n

Media Reference: Many Localized Field

\n
", + "2": "
\n

Media Reference: Many Localized Field

\n
" }, "media-reference: many with localized asset": { - "1": "
\n

Media Reference: Many With Localized Asset

\n
", - "2": "
\n

Media Reference: Many With Localized Asset

\n
" + "1": "
\n

Media Reference: Many With Localized Asset

\n
", + "2": "
\n

Media Reference: Many With Localized Asset

\n
" }, "media-reference: one with localized asset": { - "1": "
\n

Media Reference: One Localized Asset

\n
", - "2": "
\n

Media Reference: One Localized Asset

\n
" + "1": "
\n

Media Reference: One Localized Asset

\n
", + "2": "
\n

Media Reference: One Localized Asset

\n
" }, "media-reference: one with localized field": { - "1": "
\n

Media Reference: One Localized Field

\n
", - "2": "
\n

Media Reference: One Localized Field

\n
" + "1": "
\n

Media Reference: One Localized Field

\n
", + "2": "
\n

Media Reference: One Localized Field

\n
" + } + }, + "prewview-content-reference": { + "content-reference-many-2nd-level-loop": { + "1": "
\n

Content Reference: Many (2nd level loop)\n (\n 4R29nQaAkgJFB5pkruYW9i\n )

\n

[ContentfulContentTypeNumber]\n 42

\n

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

\n

[ContentfulContentTypeContentReference]\n Content Reference: One (Loop A -> B)\n : [\n Content Reference: One (Loop B -> A)\n ]

\n

Linked from:

\n
{\n  \"ContentfulContentTypeContentReference\": null\n}
\n
" + }, + "content-reference-many-loop-a-greater-b": { + "1": "
\n

Content Reference: Many (Loop A -> B)\n (\n 4NHKxzHnMwSjjMLSdUrcFo\n )

\n

[ContentfulContentTypeNumber]\n 42

\n

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

\n

[ContentfulContentTypeContentReference]\n Content Reference: Many (Loop B -> A)\n : [\n Number: Integer, Text: Short, Content Reference: Many (Loop A ->\n B)\n ]

\n

Linked from:

\n
{\n  \"ContentfulContentTypeContentReference\": [\n    {\n      \"sys\": {\n        \"id\": \"7IrXy5KcEujLUsaWVJAK69\"\n      }\n    }\n  ]\n}
\n
" + }, + "content-reference-many-loop-b-greater-a": { + "1": "
\n

Content Reference: Many (Loop B -> A)\n (\n 7IrXy5KcEujLUsaWVJAK69\n )

\n

[ContentfulContentTypeNumber]\n 42

\n

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

\n

[ContentfulContentTypeContentReference]\n Content Reference: Many (Loop A -> B)\n : [\n Number: Integer, Text: Short, Content Reference: Many (Loop B ->\n A)\n ]

\n

Linked from:

\n
{\n  \"ContentfulContentTypeContentReference\": [\n    {\n      \"sys\": {\n        \"id\": \"4NHKxzHnMwSjjMLSdUrcFo\"\n      }\n    }\n  ]\n}
\n
" + }, + "content-reference-many-self-reference": { + "1": "
\n

Content Reference: Many (Self Reference)\n (\n 50SBswk2hZU4G4ITVIPuuU\n )

\n

[ContentfulContentTypeNumber]\n 42

\n

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

\n

[ContentfulContentTypeContentReference]\n Content Reference: Many (Self Reference)\n : [\n Number: Integer, Text: Short, Content Reference: Many (Self\n Reference)\n ]

\n

Linked from:

\n
{\n  \"ContentfulContentTypeContentReference\": [\n    {\n      \"sys\": {\n        \"id\": \"50SBswk2hZU4G4ITVIPuuU\"\n      }\n    }\n  ]\n}
\n
" + }, + "content-reference-one": { + "1": "
\n

Content Reference: One\n (\n 77UBlVUq3oeVidhtAhrByt\n )

\n

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

\n

Linked from:

\n
{\n  \"ContentfulContentTypeContentReference\": null\n}
\n
" + }, + "content-reference-one-loop-a-greater-b": { + "1": "
\n

Content Reference: One (Loop A -> B)\n (\n LkOtAXoxqqX8Y9cRRPjRb\n )

\n

[ContentfulContentTypeContentReference]\n Content Reference: One (Loop B -> A)\n : [\n Content Reference: One (Loop A -> B)\n ]

\n

Linked from:

\n
{\n  \"ContentfulContentTypeContentReference\": [\n    {\n      \"sys\": {\n        \"id\": \"4R29nQaAkgJFB5pkruYW9i\"\n      }\n    },\n    {\n      \"sys\": {\n        \"id\": \"37m4vschoGYmwUMlgxAa74\"\n      }\n    }\n  ]\n}
\n
" + }, + "content-reference-one-loop-b-greater-a": { + "1": "
\n

Content Reference: One (Loop B -> A)\n (\n 37m4vschoGYmwUMlgxAa74\n )

\n

[ContentfulContentTypeContentReference]\n Content Reference: One (Loop A -> B)\n : [\n Content Reference: One (Loop B -> A)\n ]

\n

Linked from:

\n
{\n  \"ContentfulContentTypeContentReference\": [\n    {\n      \"sys\": {\n        \"id\": \"LkOtAXoxqqX8Y9cRRPjRb\"\n      }\n    }\n  ]\n}
\n
" + }, + "content-reference-one-self-reference": { + "1": "
\n

Content Reference: One (Self Reference)\n (\n 3Lq0a8ymtbGueurkbuM2xN\n )

\n

[ContentfulContentTypeContentReference]\n Content Reference: One (Self Reference)\n : [\n Content Reference: One (Self Reference)\n ]

\n

Linked from:

\n
{\n  \"ContentfulContentTypeContentReference\": [\n    {\n      \"sys\": {\n        \"id\": \"3Lq0a8ymtbGueurkbuM2xN\"\n      }\n    }\n  ]\n}
\n
" + }, + "content-reference-preview-api-test": { + "1": "\n

Preview API Test - Unpublished and linking to non-existing entry\n (\n 6XvKge4VlNLkgTWZTXbwvh\n )

\n

Linked from:

\n
{\n  \"ContentfulContentTypeContentReference\": null\n}
\n" } } } diff --git a/e2e-tests/contentful/src/components/references/content-reference.js b/e2e-tests/contentful/src/components/references/content-reference.js index f32ca3a656dde..60dcacd74512e 100644 --- a/e2e-tests/contentful/src/components/references/content-reference.js +++ b/e2e-tests/contentful/src/components/references/content-reference.js @@ -1,19 +1,10 @@ import React from "react" -export const ContentfulContentReference = ({ - one, - many, - content_reference, - title, -}) => { - const references = [ - one, - ...(many || []), - ...(content_reference || []), - ].filter(Boolean) +export const ContentfulContentTypeContentReference = ({ one, many, title }) => { + const references = [one, ...(many || [])].filter(Boolean) return (

- [ContentfulReference] {title}: [ + [ContentfulContentTypeContentReference] {title}: [ {references.map(ref => ref.title).join(", ")}]

) diff --git a/e2e-tests/contentful/src/components/references/index.js b/e2e-tests/contentful/src/components/references/index.js index a9d0675bb1577..f9e79b30222d6 100644 --- a/e2e-tests/contentful/src/components/references/index.js +++ b/e2e-tests/contentful/src/components/references/index.js @@ -1,4 +1,4 @@ -export { ContentfulContentReference } from "./content-reference" -export { ContentfulLocation } from "./location" -export { ContentfulNumber } from "./number" -export { ContentfulText } from "./text" +export { ContentfulContentTypeContentReference } from "./content-reference" +export { ContentfulContentTypeLocation } from "./location" +export { ContentfulContentTypeNumber } from "./number" +export { ContentfulContentTypeText } from "./text" diff --git a/e2e-tests/contentful/src/components/references/location.js b/e2e-tests/contentful/src/components/references/location.js index d25a79e529402..2f67d64026e2a 100644 --- a/e2e-tests/contentful/src/components/references/location.js +++ b/e2e-tests/contentful/src/components/references/location.js @@ -1,7 +1,7 @@ import React from "react" -export const ContentfulLocation = ({ location }) => ( +export const ContentfulContentTypeLocation = ({ location }) => (

- [ContentfulLocation] Lat: {location.lat}, Long: {location.lon} + [ContentfulContentTypeLocation] Lat: {location.lat}, Long: {location.lon}

) diff --git a/e2e-tests/contentful/src/components/references/number.js b/e2e-tests/contentful/src/components/references/number.js index db7e048b14636..9d728fe93f554 100644 --- a/e2e-tests/contentful/src/components/references/number.js +++ b/e2e-tests/contentful/src/components/references/number.js @@ -1,5 +1,5 @@ import React from "react" -export const ContentfulNumber = ({ integer, decimal }) => ( -

[ContentfulNumber] {integer || decimal}

+export const ContentfulContentTypeNumber = ({ integer, decimal }) => ( +

[ContentfulContentTypeNumber] {integer || decimal}

) diff --git a/e2e-tests/contentful/src/components/references/text.js b/e2e-tests/contentful/src/components/references/text.js index 8bcf361eb99c8..eb4ac609836e8 100644 --- a/e2e-tests/contentful/src/components/references/text.js +++ b/e2e-tests/contentful/src/components/references/text.js @@ -1,5 +1,5 @@ import React from "react" -export const ContentfulText = ({ short, longPlain }) => ( -

[ContentfulText] {short || longPlain?.longPlain}

+export const ContentfulContentTypeText = ({ short, longPlain }) => ( +

[ContentfulContentTypeText] {short || longPlain?.raw}

) diff --git a/e2e-tests/contentful/src/pages/boolean.js b/e2e-tests/contentful/src/pages/boolean.js index 410065f7074bd..f3cd3ee6cb3cf 100644 --- a/e2e-tests/contentful/src/pages/boolean.js +++ b/e2e-tests/contentful/src/pages/boolean.js @@ -49,27 +49,36 @@ export default BooleanPage export const pageQuery = graphql` query BooleanQuery { - default: allContentfulBoolean( - sort: { fields: contentful_id } - filter: { node_locale: { eq: "en-US" }, booleanLocalized: { eq: null } } + default: allContentfulContentTypeBoolean( + sort: { sys: { id: ASC } } + filter: { + sys: { locale: { eq: "en-US" } } + booleanLocalized: { eq: null } + } ) { nodes { title boolean } } - english: allContentfulBoolean( - sort: { fields: contentful_id } - filter: { node_locale: { eq: "en-US" }, booleanLocalized: { ne: null } } + english: allContentfulContentTypeBoolean( + sort: { sys: { id: ASC } } + filter: { + sys: { locale: { eq: "en-US" } } + booleanLocalized: { ne: null } + } ) { nodes { title booleanLocalized } } - german: allContentfulBoolean( - sort: { fields: contentful_id } - filter: { node_locale: { eq: "de-DE" }, booleanLocalized: { ne: null } } + german: allContentfulContentTypeBoolean( + sort: { sys: { id: ASC } } + filter: { + sys: { locale: { eq: "de-DE" } } + booleanLocalized: { ne: null } + } ) { nodes { title diff --git a/e2e-tests/contentful/src/pages/content-reference.js b/e2e-tests/contentful/src/pages/content-reference.js index 642722e2a92d6..129c9406bff02 100644 --- a/e2e-tests/contentful/src/pages/content-reference.js +++ b/e2e-tests/contentful/src/pages/content-reference.js @@ -24,7 +24,7 @@ const ContentReferencePage = ({ data }) => { return (

Default

- {defaultEntries.map(({ contentful_id, title, one, many }) => { + {defaultEntries.map(({ sys: { id }, title, one, many, linkedFrom }) => { const slug = slugify(title, { strict: true, lower: true }) let content = null @@ -37,15 +37,21 @@ const ContentReferencePage = ({ data }) => { } return ( -
-

{title}

+
+

+ {title} ({id}) +

{content} +

Linked from:

+
+              {JSON.stringify(linkedFrom, null, 2)}
+            
) })}

English Locale

{englishEntries.map( - ({ contentful_id, title, oneLocalized, manyLocalized }) => { + ({ sys: { id }, title, oneLocalized, manyLocalized }) => { const slug = slugify(title, { strict: true, lower: true }) let content = null @@ -58,7 +64,7 @@ const ContentReferencePage = ({ data }) => { } return ( -
+

{title}

{content}
@@ -67,7 +73,7 @@ const ContentReferencePage = ({ data }) => { )}

German Locale

{germanEntries.map( - ({ contentful_id, title, oneLocalized, manyLocalized }) => { + ({ sys: { id }, title, oneLocalized, manyLocalized }) => { const slug = slugify(title, { strict: true, lower: true }) let content = null @@ -80,7 +86,7 @@ const ContentReferencePage = ({ data }) => { } return ( -
+

{title}

{content}
@@ -95,42 +101,65 @@ export default ContentReferencePage export const pageQuery = graphql` query ContentReferenceQuery { - default: allContentfulContentReference( - sort: { fields: title } - filter: { node_locale: { eq: "en-US" }, title: { glob: "!*Localized*" } } + default: allContentfulContentTypeContentReference( + sort: { title: ASC } + filter: { + sys: { locale: { eq: "en-US" } } + title: { glob: "!*Localized*" } + } ) { nodes { title - contentful_id + sys { + id + } + linkedFrom { + ContentfulContentTypeContentReference { + sys { + id + } + } + } one { __typename - ... on ContentfulText { - contentful_id + ... on ContentfulEntry { + sys { + id + } + } + ... on ContentfulContentTypeText { title short } - ... on ContentfulContentReference { - contentful_id + ... on ContentfulContentTypeNumber { + title + integer + } + ... on ContentfulContentTypeContentReference { title one { - ... on ContentfulText { + ... on ContentfulContentTypeText { title short } - ... on ContentfulContentReference { + ... on ContentfulContentTypeNumber { + title + integer + } + ... on ContentfulContentTypeContentReference { title } } many { - ... on ContentfulText { + ... on ContentfulContentTypeText { title short } - ... on ContentfulNumber { + ... on ContentfulContentTypeNumber { title integer } - ... on ContentfulContentReference { + ... on ContentfulContentTypeContentReference { title } } @@ -138,38 +167,49 @@ export const pageQuery = graphql` } many { __typename - ... on ContentfulText { - contentful_id + ... on ContentfulEntry { + sys { + id + } + } + ... on ContentfulContentTypeText { title short } - ... on ContentfulNumber { - contentful_id + ... on ContentfulContentTypeNumber { title integer } - ... on ContentfulContentReference { - contentful_id + ... on ContentfulContentTypeContentReference { title + ... on ContentfulEntry { + sys { + id + } + } one { - ... on ContentfulText { + ... on ContentfulContentTypeText { title short } - ... on ContentfulContentReference { + ... on ContentfulContentTypeNumber { + title + integer + } + ... on ContentfulContentTypeContentReference { title } } many { - ... on ContentfulText { + ... on ContentfulContentTypeText { title short } - ... on ContentfulNumber { + ... on ContentfulContentTypeNumber { title integer } - ... on ContentfulContentReference { + ... on ContentfulContentTypeContentReference { title } } @@ -177,61 +217,75 @@ export const pageQuery = graphql` } } } - english: allContentfulContentReference( - sort: { fields: title } - filter: { node_locale: { eq: "en-US" }, title: { glob: "*Localized*" } } + english: allContentfulContentTypeContentReference( + sort: { title: ASC } + filter: { + sys: { locale: { eq: "en-US" } } + title: { glob: "*Localized*" } + } ) { nodes { title - contentful_id + sys { + id + } oneLocalized { __typename - title - decimal - integer + ... on ContentfulContentTypeNumber { + title + decimal + integer + } } manyLocalized { __typename - ... on ContentfulNumber { + ... on ContentfulContentTypeNumber { title decimal integer } - ... on ContentfulText { + ... on ContentfulContentTypeText { title short longPlain { - longPlain + raw } } } } } - german: allContentfulContentReference( - sort: { fields: title } - filter: { node_locale: { eq: "de-DE" }, title: { glob: "*Localized*" } } + german: allContentfulContentTypeContentReference( + sort: { title: ASC } + filter: { + sys: { locale: { eq: "de-DE" } } + title: { glob: "*Localized*" } + } ) { nodes { title - contentful_id + sys { + id + } oneLocalized { __typename - title - decimal - integer + ... on ContentfulContentTypeNumber { + title + decimal + integer + } } manyLocalized { __typename - ... on ContentfulNumber { + ... on ContentfulContentTypeNumber { title decimal integer } - ... on ContentfulText { + ... on ContentfulContentTypeText { title short longPlain { - longPlain + raw } } } diff --git a/e2e-tests/contentful/src/pages/custom-fields.js b/e2e-tests/contentful/src/pages/custom-fields.js new file mode 100644 index 0000000000000..7d665ec629973 --- /dev/null +++ b/e2e-tests/contentful/src/pages/custom-fields.js @@ -0,0 +1,35 @@ +import { graphql } from "gatsby" +import * as React from "react" + +import Layout from "../components/layout" + +const CustomFieldsPage = ({ data }) => { + const { + contentfulContentTypeText + } = data + return ( + +

Custom Field:

+
+

{contentfulContentTypeText.fields.customField}

+
+

Custom Resolver:

+
+

{contentfulContentTypeText.customResolver}

+
+
+ ) +} + +export default CustomFieldsPage + +export const pageQuery = graphql` + query TextQuery { + contentfulContentTypeText { + fields { + customField + } + customResolver + } + } +` diff --git a/e2e-tests/contentful/src/pages/date.js b/e2e-tests/contentful/src/pages/date.js index bb3a0ff554a38..81a2c72d0990e 100644 --- a/e2e-tests/contentful/src/pages/date.js +++ b/e2e-tests/contentful/src/pages/date.js @@ -32,34 +32,36 @@ export default DatePage export const pageQuery = graphql` query DateQuery { - dateTime: contentfulDate(contentful_id: { eq: "38akBjGb3T1t4AjB87wQjo" }) { + dateTime: contentfulContentTypeDate( + sys: { id: { eq: "38akBjGb3T1t4AjB87wQjo" } } + ) { title date: dateTime formatted: dateTime(formatString: "D.M.YYYY - hh:mm") } - dateTimeTimezone: contentfulDate( - contentful_id: { eq: "6dZ8pK4tFWZDZPHgSC0tNS" } + dateTimeTimezone: contentfulContentTypeDate( + sys: { id: { eq: "6dZ8pK4tFWZDZPHgSC0tNS" } } ) { title date: dateTimeTimezone formatted: dateTimeTimezone(formatString: "D.M.YYYY - hh:mm (z)") } - date: contentfulDate(contentful_id: { eq: "5FuULz0jl0rKoKUKp2rshf" }) { + date: contentfulContentTypeDate( + sys: { id: { eq: "5FuULz0jl0rKoKUKp2rshf" } } + ) { title date formatted: date(formatString: "D.M.YYYY") } - dateEnglish: contentfulDate( - contentful_id: { eq: "1ERWZvDiYELryAZEP1dmKG" } - node_locale: { eq: "en-US" } + dateEnglish: contentfulContentTypeDate( + sys: { id: { eq: "1ERWZvDiYELryAZEP1dmKG" }, locale: { eq: "en-US" } } ) { title date: dateLocalized formatted: dateLocalized(formatString: "D.M.YYYY - HH:mm:ss") } - dateGerman: contentfulDate( - contentful_id: { eq: "1ERWZvDiYELryAZEP1dmKG" } - node_locale: { eq: "de-DE" } + dateGerman: contentfulContentTypeDate( + sys: { id: { eq: "1ERWZvDiYELryAZEP1dmKG" }, locale: { eq: "de-DE" } } ) { title date: dateLocalized diff --git a/e2e-tests/contentful/src/pages/download-local.js b/e2e-tests/contentful/src/pages/download-local.js index a5bdddb8a18f2..e5401942fb130 100644 --- a/e2e-tests/contentful/src/pages/download-local.js +++ b/e2e-tests/contentful/src/pages/download-local.js @@ -20,9 +20,7 @@ export default DownloadLocalPage export const pageQuery = graphql` query DownloadLocalQuery { - contentfulAsset(contentful_id: { eq: "3BSI9CgDdAn1JchXmY5IJi" }) { - contentful_id - title + contentfulAsset(sys: { id: { eq: "3BSI9CgDdAn1JchXmY5IJi" } }) { localFile { absolutePath childImageSharp { diff --git a/e2e-tests/contentful/src/pages/gatsby-image-cdn.js b/e2e-tests/contentful/src/pages/gatsby-image-cdn.js index 873f7155826ba..d207507f02baf 100644 --- a/e2e-tests/contentful/src/pages/gatsby-image-cdn.js +++ b/e2e-tests/contentful/src/pages/gatsby-image-cdn.js @@ -22,15 +22,12 @@ export default GatsbyPluginImagePage export const pageQuery = graphql` query GatsbyImageCDNQuery { default: contentfulAsset( - contentful_id: { eq: "3BSI9CgDdAn1JchXmY5IJi" } - node_locale: { eq: "en-US" } + sys: { id: { eq: "3BSI9CgDdAn1JchXmY5IJi" }, locale: { eq: "en-US" } } ) { title description - file { - fileName - url - } + filename + url gatsbyImage(width: 420) } } diff --git a/e2e-tests/contentful/src/pages/gatsby-plugin-image.js b/e2e-tests/contentful/src/pages/gatsby-plugin-image.js index ead0cc27082f0..7b1c78db99c7b 100644 --- a/e2e-tests/contentful/src/pages/gatsby-plugin-image.js +++ b/e2e-tests/contentful/src/pages/gatsby-plugin-image.js @@ -1,7 +1,7 @@ import { graphql } from "gatsby" import { GatsbyImage } from "gatsby-plugin-image" import * as React from "react" -import { useContentfulImage } from "gatsby-source-contentful/hooks" +import { useContentfulImage } from "gatsby-source-contentful" import Layout from "../components/layout" import Grid from "../components/grid" @@ -25,14 +25,14 @@ const GatsbyPluginImagePage = ({ data }) => {

- {node.title} ({node.file.fileName.split(".").pop()}) + {node.title} ({node.filename.split(".").pop()})

{node.description &&

{node.description}

} {node.constrained ? ( ) : ( - + )}
))} @@ -43,14 +43,14 @@ const GatsbyPluginImagePage = ({ data }) => {

- {node.title} ({node.file.fileName.split(".").pop()}) + {node.title} ({node.filename.split(".").pop()})

{node.description &&

{node.description}

} {node.fullWidth ? ( ) : ( - + )}
))} @@ -62,14 +62,14 @@ const GatsbyPluginImagePage = ({ data }) => {

- {node.title} ({node.file.fileName.split(".").pop()}) + {node.title} ({node.filename.split(".").pop()})

{node.description &&

{node.description}

} {node.fixed ? ( ) : ( - + )}
))} @@ -81,35 +81,14 @@ const GatsbyPluginImagePage = ({ data }) => {

- {node.title} ({node.file.fileName.split(".").pop()}) + {node.title} ({node.filename.split(".").pop()})

{node.description &&

{node.description}

} {node.dominantColor ? ( ) : ( - - )} -
- ))} - - -

- gatsby-plugin-image: Traced SVG Placeholder (fallback to DOMINANT_COLOR) -

- - {data.default.nodes.map(node => ( -
-

- - {node.title} ({node.file.fileName.split(".").pop()}) - -

- {node.description &&

{node.description}

} - {node.traced ? ( - - ) : ( - + )}
))} @@ -121,14 +100,14 @@ const GatsbyPluginImagePage = ({ data }) => {

- {node.title} ({node.file.fileName.split(".").pop()}) + {node.title} ({node.filename.split(".").pop()})

{node.description &&

{node.description}

} {node.blurred ? ( ) : ( - + )}
))} @@ -140,14 +119,14 @@ const GatsbyPluginImagePage = ({ data }) => {

- {node.title} ({node.file.fileName.split(".").pop()}) + {node.title} ({node.filename.split(".").pop()})

{node.description &&

{node.description}

} {node.customImageFormats ? ( ) : ( - + )}
))} @@ -159,7 +138,7 @@ const GatsbyPluginImagePage = ({ data }) => {

- {node.title} ({node.file.fileName.split(".").pop()}) + {node.title} ({node.filename.split(".").pop()})

{node.description &&

{node.description}

} @@ -174,7 +153,7 @@ const GatsbyPluginImagePage = ({ data }) => { alt={node.title} /> ) : ( - + )}
))} @@ -186,14 +165,14 @@ const GatsbyPluginImagePage = ({ data }) => {

- {node.title} ({node.file.fileName.split(".").pop()}) + {node.title} ({node.filename.split(".").pop()})

{node.description &&

{node.description}

} {node.constrained ? ( ) : ( - + )}
))} @@ -205,14 +184,14 @@ const GatsbyPluginImagePage = ({ data }) => {

- {node.title} ({node.file.fileName.split(".").pop()}) + {node.title} ({node.filename.split(".").pop()})

{node.description &&

{node.description}

} {node.constrained ? ( ) : ( - + )}
))} @@ -235,24 +214,24 @@ export const pageQuery = graphql` query GatsbyPluginImageQuery { default: allContentfulAsset( filter: { - contentful_id: { - in: [ - "3ljGfnpegOnBTFGhV07iC1" - "3BSI9CgDdAn1JchXmY5IJi" - "65syuRuRVeKi03HvRsOkkb" - ] + sys: { + id: { + in: [ + "3ljGfnpegOnBTFGhV07iC1" + "3BSI9CgDdAn1JchXmY5IJi" + "65syuRuRVeKi03HvRsOkkb" + ] + } + locale: { eq: "en-US" } } - node_locale: { eq: "en-US" } } - sort: { fields: contentful_id } + sort: { sys: { id: ASC } } ) { nodes { title description - file { - fileName - url - } + filename + url constrained: gatsbyImageData(width: 420) fullWidth: gatsbyImageData(width: 200, layout: FIXED) fixed: gatsbyImageData(width: 200, layout: FIXED) @@ -261,11 +240,6 @@ export const pageQuery = graphql` layout: FIXED placeholder: DOMINANT_COLOR ) - traced: gatsbyImageData( - width: 200 - layout: FIXED - placeholder: TRACED_SVG - ) blurred: gatsbyImageData( width: 200 layout: FIXED @@ -279,35 +253,29 @@ export const pageQuery = graphql` } english: allContentfulAsset( filter: { - contentful_id: { in: ["4FwygYxkL3rAteERtoxxNC"] } - node_locale: { eq: "en-US" } + sys: { id: { in: ["4FwygYxkL3rAteERtoxxNC"] }, locale: { eq: "en-US" } } } - sort: { fields: contentful_id } + sort: { sys: { id: ASC } } ) { nodes { title description - file { - fileName - url - } + filename + url constrained: gatsbyImageData(width: 420) } } german: allContentfulAsset( filter: { - contentful_id: { in: ["4FwygYxkL3rAteERtoxxNC"] } - node_locale: { eq: "de-DE" } + sys: { id: { in: ["4FwygYxkL3rAteERtoxxNC"] }, locale: { eq: "de-DE" } } } - sort: { fields: contentful_id } + sort: { sys: { id: ASC } } ) { nodes { title description - file { - fileName - url - } + filename + url constrained: gatsbyImageData(width: 420) } } diff --git a/e2e-tests/contentful/src/pages/index.js b/e2e-tests/contentful/src/pages/index.js index 8a097000793b4..867dccd3922b0 100644 --- a/e2e-tests/contentful/src/pages/index.js +++ b/e2e-tests/contentful/src/pages/index.js @@ -54,6 +54,15 @@ const IndexPage = () => ( /tags +

Gatsby

+
    +
  • + SSR +
  • +
  • + Custom Fields & Resolver +
  • +
) diff --git a/e2e-tests/contentful/src/pages/json.js b/e2e-tests/contentful/src/pages/json.js index b41d1bf54501e..548bce9c30608 100644 --- a/e2e-tests/contentful/src/pages/json.js +++ b/e2e-tests/contentful/src/pages/json.js @@ -31,7 +31,7 @@ const JSONPage = ({ data }) => {

Name: {actor.name}

Photo: {actor.photo}

Birthdate: {actor.Birthdate}

-

Born at: {actor.Born_At}

+

Born at: {actor["Born At"]}

Weight: {actor.weight}

Age: {actor.age}

Wife: {actor.wife}

@@ -61,50 +61,27 @@ export default JSONPage export const pageQuery = graphql` query JSONQuery { - simple: contentfulJson(contentful_id: { eq: "2r6tNjP8brkyy5yLR39hhh" }) { - json { - name - city - age - } + simple: contentfulContentTypeJson( + sys: { id: { eq: "2r6tNjP8brkyy5yLR39hhh" } } + ) { + json } - complex: contentfulJson(contentful_id: { eq: "2y71nV0cpW9vzTmJybq571" }) { - json { - Actors { - name - photo - Birthdate - Born_At - weight - age - wife - children - hasChildren - hasGreyHair - } - } + complex: contentfulContentTypeJson( + sys: { id: { eq: "2y71nV0cpW9vzTmJybq571" } } + ) { + json } - english: contentfulJson( - node_locale: { eq: "en-US" } - jsonLocalized: { id: { ne: null } } + english: contentfulContentTypeJson( + sys: { id: { eq: "7DvTBEPg5P6TRC7dI9zXuO" }, locale: { eq: "en-US" } } ) { title - jsonLocalized { - age - city - name - } + jsonLocalized } - german: contentfulJson( - node_locale: { eq: "de-DE" } - jsonLocalized: { id: { ne: null } } + german: contentfulContentTypeJson( + sys: { id: { eq: "7DvTBEPg5P6TRC7dI9zXuO" }, locale: { eq: "de-DE" } } ) { title - jsonLocalized { - age - city - name - } + jsonLocalized } } ` diff --git a/e2e-tests/contentful/src/pages/location.js b/e2e-tests/contentful/src/pages/location.js index 813f6cf9ed7dd..a6e2d79cc2678 100644 --- a/e2e-tests/contentful/src/pages/location.js +++ b/e2e-tests/contentful/src/pages/location.js @@ -56,9 +56,12 @@ export default LocationPage export const pageQuery = graphql` query LocationQuery { - default: allContentfulLocation( - sort: { fields: contentful_id } - filter: { title: { glob: "!*Localized*" }, node_locale: { eq: "en-US" } } + default: allContentfulContentTypeLocation( + sort: { sys: { id: ASC } } + filter: { + title: { glob: "!*Localized*" } + sys: { locale: { eq: "en-US" } } + } ) { nodes { title @@ -68,9 +71,12 @@ export const pageQuery = graphql` } } } - english: allContentfulLocation( - sort: { fields: contentful_id } - filter: { title: { glob: "*Localized*" }, node_locale: { eq: "en-US" } } + english: allContentfulContentTypeLocation( + sort: { sys: { id: ASC } } + filter: { + title: { glob: "*Localized*" } + sys: { locale: { eq: "en-US" } } + } ) { nodes { title @@ -80,9 +86,12 @@ export const pageQuery = graphql` } } } - german: allContentfulLocation( - sort: { fields: contentful_id } - filter: { title: { glob: "*Localized*" }, node_locale: { eq: "de-DE" } } + german: allContentfulContentTypeLocation( + sort: { sys: { id: ASC } } + filter: { + title: { glob: "*Localized*" } + sys: { locale: { eq: "de-DE" } } + } ) { nodes { title diff --git a/e2e-tests/contentful/src/pages/media-reference.js b/e2e-tests/contentful/src/pages/media-reference.js index f6bf077d006e1..ba42852411f3f 100644 --- a/e2e-tests/contentful/src/pages/media-reference.js +++ b/e2e-tests/contentful/src/pages/media-reference.js @@ -10,24 +10,22 @@ const MediaReferencePage = ({ data }) => { const germanEntries = data.german.nodes return ( - {defaultEntries.map(({ contentful_id, title, one, many }) => { + {defaultEntries.map(({ sys: { id }, title, one, many }) => { const slug = slugify(title, { strict: true, lower: true }) let content = null if (many) { content = many.map(imageData => ( - {title} + {title} )) } if (one) { - content = ( - {title} - ) + content = {title} } return ( -
+

{title}

{content}
@@ -35,48 +33,34 @@ const MediaReferencePage = ({ data }) => { })}

English Locale

{englishEntries.map( - ({ contentful_id, title, one, oneLocalized, many, manyLocalized }) => { + ({ sys: { id }, title, one, oneLocalized, many, manyLocalized }) => { const slug = slugify(title, { strict: true, lower: true }) let content = null if (manyLocalized) { content = manyLocalized.map(imageData => ( - {title} + {title} )) } if (oneLocalized) { content = ( - {title} + {title} ) } if (many) { content = many.map(imageData => ( - {title} + {title} )) } if (one) { - content = ( - {title} - ) + content = {title} } return ( -
+

{title}

{content}
@@ -86,48 +70,34 @@ const MediaReferencePage = ({ data }) => {

German Locale

{germanEntries.map( - ({ contentful_id, title, one, oneLocalized, many, manyLocalized }) => { + ({ sys: { id }, title, one, oneLocalized, many, manyLocalized }) => { const slug = slugify(title, { strict: true, lower: true }) let content = null if (manyLocalized) { content = manyLocalized.map(imageData => ( - {title} + {title} )) } if (oneLocalized) { content = ( - {title} + {title} ) } if (many) { content = many.map(imageData => ( - {title} + {title} )) } if (one) { - content = ( - {title} - ) + content = {title} } return ( -
+

{title}

{content}
@@ -142,80 +112,75 @@ export default MediaReferencePage export const pageQuery = graphql` query MediaReferenceQuery { - default: allContentfulMediaReference( - sort: { fields: title } - filter: { title: { glob: "!*Localized*" }, node_locale: { eq: "en-US" } } + default: allContentfulContentTypeMediaReference( + sort: { title: ASC } + filter: { + title: { glob: "!*Localized*" } + sys: { locale: { eq: "en-US" } } + } ) { nodes { title - contentful_id + sys { + id + } one { - file { - url - } + url } many { - file { - url - } + url } } } - english: allContentfulMediaReference( - sort: { fields: title } - filter: { title: { glob: "*Localized*" }, node_locale: { eq: "en-US" } } + english: allContentfulContentTypeMediaReference( + sort: { title: ASC } + filter: { + title: { glob: "*Localized*" } + sys: { locale: { eq: "en-US" } } + } ) { nodes { title - contentful_id + sys { + id + } one { - file { - url - } + url } many { - file { - url - } + url } oneLocalized { - file { - url - } + url } manyLocalized { - file { - url - } + url } } } - german: allContentfulMediaReference( - sort: { fields: title } - filter: { title: { glob: "*Localized*" }, node_locale: { eq: "de-DE" } } + german: allContentfulContentTypeMediaReference( + sort: { title: ASC } + filter: { + title: { glob: "*Localized*" } + sys: { locale: { eq: "de-DE" } } + } ) { nodes { title - contentful_id + sys { + id + } one { - file { - url - } + url } many { - file { - url - } + url } oneLocalized { - file { - url - } + url } manyLocalized { - file { - url - } + url } } } diff --git a/e2e-tests/contentful/src/pages/number.js b/e2e-tests/contentful/src/pages/number.js index 5eff2ed704bba..57af867470887 100644 --- a/e2e-tests/contentful/src/pages/number.js +++ b/e2e-tests/contentful/src/pages/number.js @@ -53,9 +53,12 @@ export default NumberPage export const pageQuery = graphql` query NumberQuery { - default: allContentfulNumber( - sort: { fields: contentful_id } - filter: { title: { glob: "!*Localized*" }, node_locale: { eq: "en-US" } } + default: allContentfulContentTypeNumber( + sort: { sys: { id: ASC } } + filter: { + title: { glob: "!*Localized*" } + sys: { locale: { eq: "en-US" } } + } ) { nodes { title @@ -63,9 +66,12 @@ export const pageQuery = graphql` decimal } } - english: allContentfulNumber( - sort: { fields: contentful_id } - filter: { title: { glob: "*Localized*" }, node_locale: { eq: "en-US" } } + english: allContentfulContentTypeNumber( + sort: { sys: { id: ASC } } + filter: { + title: { glob: "*Localized*" } + sys: { locale: { eq: "en-US" } } + } ) { nodes { title @@ -73,9 +79,12 @@ export const pageQuery = graphql` decimalLocalized } } - german: allContentfulNumber( - sort: { fields: contentful_id } - filter: { title: { glob: "*Localized*" }, node_locale: { eq: "de-DE" } } + german: allContentfulContentTypeNumber( + sort: { sys: { id: ASC } } + filter: { + title: { glob: "*Localized*" } + sys: { locale: { eq: "de-DE" } } + } ) { nodes { title diff --git a/e2e-tests/contentful/src/pages/rich-text.js b/e2e-tests/contentful/src/pages/rich-text.js index b064694b7469b..a8bd18af36031 100644 --- a/e2e-tests/contentful/src/pages/rich-text.js +++ b/e2e-tests/contentful/src/pages/rich-text.js @@ -4,7 +4,7 @@ import * as React from "react" import slugify from "slugify" import { BLOCKS, MARKS, INLINES } from "@contentful/rich-text-types" -import { renderRichText } from "gatsby-source-contentful/rich-text" +import { renderRichText } from "gatsby-source-contentful" import Layout from "../components/layout" @@ -20,13 +20,13 @@ function renderReferencedComponent(ref) { return } -const options = { +const makeOptions = ({ assetBlockMap, entryBlockMap, entryInlineMap }) => ({ renderMark: { [MARKS.BOLD]: text => {text}, }, renderNode: { [BLOCKS.EMBEDDED_ASSET]: node => { - const asset = node.data.target + const asset = assetBlockMap.get(node?.data?.target?.sys.id) if (asset.gatsbyImageData) { return } @@ -40,7 +40,7 @@ const options = { ) }, [BLOCKS.EMBEDDED_ENTRY]: node => { - const entry = node?.data?.target + const entry = entryBlockMap.get(node?.data?.target?.sys.id) if (!entry) { throw new Error( `Entity not available for node:\n${JSON.stringify(node, null, 2)}` @@ -49,11 +49,11 @@ const options = { return renderReferencedComponent(entry) }, [INLINES.EMBEDDED_ENTRY]: node => { - const entry = node.data.target - if (entry.__typename === "ContentfulText") { + const entry = entryInlineMap.get(node?.data?.target?.sys.id) + if (entry.__typename === "ContentfulContentTypeText") { return ( - [Inline-ContentfulText] {entry.title}: {entry.short} + [Inline-ContentfulContentTypeText] {entry.short} ) } @@ -64,7 +64,7 @@ const options = { ) }, }, -} +}) const RichTextPage = ({ data }) => { const defaultEntries = data.default.nodes @@ -77,7 +77,7 @@ const RichTextPage = ({ data }) => { return (

{title}

- {renderRichText(richText, options)} + {renderRichText(richText, makeOptions)}
) @@ -89,7 +89,7 @@ const RichTextPage = ({ data }) => { return (

{title}

- {renderRichText(richTextLocalized, options)} + {renderRichText(richTextLocalized, makeOptions)}
) @@ -101,7 +101,7 @@ const RichTextPage = ({ data }) => { return (

{title}

- {renderRichText(richTextLocalized, options)} + {renderRichText(richTextLocalized, makeOptions)}
) @@ -114,99 +114,145 @@ export default RichTextPage export const pageQuery = graphql` query RichTextQuery { - default: allContentfulRichText( - sort: { fields: title } + default: allContentfulContentTypeRichText( + sort: { title: ASC } filter: { title: { glob: "!*Localized*|*Validated*" } - node_locale: { eq: "en-US" } + sys: { locale: { eq: "en-US" } } } ) { nodes { id title richText { - raw - references { - __typename - ... on ContentfulAsset { - contentful_id - gatsbyImageData(width: 200) - } - ... on ContentfulText { - contentful_id - title - short - } - ... on ContentfulLocation { - contentful_id - location { - lat - lon + json + links { + assets { + block { + sys { + id + } + gatsbyImageData(width: 200) } } - ... on ContentfulContentReference { - contentful_id - title - one { - ... on ContentfulContentReference { - contentful_id - title - content_reference { - ... on ContentfulContentReference { - contentful_id - title - } - } + entries { + block { + __typename + sys { + id + type } - } - many { - ... on ContentfulText { - contentful_id + ... on ContentfulContentTypeText { title short } - ... on ContentfulNumber { - contentful_id - title - integer + ... on ContentfulContentTypeLocation { + location { + lat + lon + } } - ... on ContentfulContentReference { - contentful_id + ... on ContentfulContentTypeContentReference { title - content_reference { - ... on ContentfulContentReference { - id + one { + __typename + ... on ContentfulEntry { + sys { + id + } + } + ... on ContentfulContentTypeText { title + short + } + ... on ContentfulContentTypeContentReference { + title + one { + ... on ContentfulContentTypeContentReference { + title + } + } + many { + ... on ContentfulContentTypeContentReference { + title + } + } + } + } + many { + __typename + ... on ContentfulEntry { + sys { + id + } + } + ... on ContentfulContentTypeText { + title + short + } + ... on ContentfulContentTypeNumber { + title + integer + } + ... on ContentfulContentTypeContentReference { + title + one { + ... on ContentfulContentTypeContentReference { + title + } + } + many { + ... on ContentfulContentTypeContentReference { + title + } + } } } } } + inline { + __typename + sys { + id + type + } + ... on ContentfulContentTypeText { + title + short + } + } } } } } } - english: allContentfulRichText( - sort: { fields: title } - filter: { title: { glob: "*Localized*" }, node_locale: { eq: "en-US" } } + english: allContentfulContentTypeRichText( + sort: { title: ASC } + filter: { + title: { glob: "*Localized*" } + sys: { locale: { eq: "en-US" } } + } ) { nodes { id title richTextLocalized { - raw + json } } } - german: allContentfulRichText( - sort: { fields: title } - filter: { title: { glob: "*Localized*" }, node_locale: { eq: "de-DE" } } + german: allContentfulContentTypeRichText( + sort: { title: ASC } + filter: { + title: { glob: "*Localized*" } + sys: { locale: { eq: "de-DE" } } + } ) { nodes { id title richTextLocalized { - raw + json } } } diff --git a/e2e-tests/contentful/src/pages/tags.js b/e2e-tests/contentful/src/pages/tags.js index 9d752ee279ba8..ab4ee7d44a68c 100644 --- a/e2e-tests/contentful/src/pages/tags.js +++ b/e2e-tests/contentful/src/pages/tags.js @@ -55,18 +55,18 @@ const TagsPage = ({ data }) => { data-cy-assets style={{ display: "flex", justifyContent: "space-between" }} > - {assets.map(({ title, file, metadata }) => { + {assets.map(({ title, url, contentfulMetadata }) => { const slug = slugify(title, { strict: true, lower: true }) return (

{title}

{title}
- {metadata.tags.map(({ name }) => ( + {contentfulMetadata.tags.map(({ name }) => ( { ) } - export default TagsPage export const pageQuery = graphql` query TagsQuery { - tags: allContentfulTag(sort: { fields: contentful_id }) { + tags: allContentfulTag(sort: { id: ASC }) { nodes { name contentful_id } } - integers: allContentfulNumber( - sort: { fields: contentful_id } + integers: allContentfulContentTypeNumber( + sort: { sys: { id: ASC } } filter: { - metadata: { + contentfulMetadata: { tags: { elemMatch: { contentful_id: { eq: "numberInteger" } } } } - node_locale: { eq: "en-US" } + sys: { locale: { eq: "en-US" } } } ) { nodes { @@ -113,13 +112,13 @@ export const pageQuery = graphql` integer } } - decimals: allContentfulNumber( - sort: { fields: contentful_id } + decimals: allContentfulContentTypeNumber( + sort: { sys: { id: ASC } } filter: { - metadata: { + contentfulMetadata: { tags: { elemMatch: { contentful_id: { eq: "numberDecimal" } } } } - node_locale: { eq: "en-US" } + sys: { locale: { eq: "en-US" } } } ) { nodes { @@ -128,18 +127,18 @@ export const pageQuery = graphql` } } assets: allContentfulAsset( - sort: { fields: contentful_id } + sort: { sys: { id: ASC } } filter: { - metadata: { tags: { elemMatch: { contentful_id: { eq: "animal" } } } } - node_locale: { eq: "en-US" } + contentfulMetadata: { + tags: { elemMatch: { contentful_id: { eq: "animal" } } } + } + sys: { locale: { eq: "en-US" } } } ) { nodes { title - file { - url - } - metadata { + url + contentfulMetadata { tags { name } diff --git a/e2e-tests/contentful/src/pages/text.js b/e2e-tests/contentful/src/pages/text.js index bc39dd5ddfa6e..b2e10ceeef546 100644 --- a/e2e-tests/contentful/src/pages/text.js +++ b/e2e-tests/contentful/src/pages/text.js @@ -33,7 +33,7 @@ const TextPage = ({ data }) => {

Long (Plain):

-

{longPlain.longPlain.longPlain}

+

{longPlain.longPlain.raw}

Markdown (Simple):

{

Long (Plain):

-

{longEnglish.longLocalized.longLocalized}

+

{longEnglish.longLocalized.raw}

German Locale

@@ -67,7 +67,7 @@ const TextPage = ({ data }) => {

Long (Plain):

-

{longGerman.longLocalized.longLocalized}

+

{longGerman.longLocalized.raw}

) @@ -77,29 +77,25 @@ export default TextPage export const pageQuery = graphql` query TextQuery { - short: contentfulText( - node_locale: { eq: "en-US" } - contentful_id: { eq: "5ZtcN1o7KpN7J7xgiTyaXo" } + short: contentfulContentTypeText( + sys: { id: { eq: "5ZtcN1o7KpN7J7xgiTyaXo" }, locale: { eq: "en-US" } } ) { short } - shortList: contentfulText( - node_locale: { eq: "en-US" } - contentful_id: { eq: "7b5U927WTFcQXO2Gewwa2k" } + shortList: contentfulContentTypeText( + sys: { id: { eq: "7b5U927WTFcQXO2Gewwa2k" }, locale: { eq: "en-US" } } ) { shortList } - longPlain: contentfulText( - node_locale: { eq: "en-US" } - contentful_id: { eq: "6ru8cSC9hZi3Ekvtw7P77S" } + longPlain: contentfulContentTypeText( + sys: { id: { eq: "6ru8cSC9hZi3Ekvtw7P77S" }, locale: { eq: "en-US" } } ) { longPlain { - longPlain + raw } } - longMarkdownSimple: contentfulText( - node_locale: { eq: "en-US" } - contentful_id: { eq: "NyPJw0mcSuCwY2gV0zYny" } + longMarkdownSimple: contentfulContentTypeText( + sys: { id: { eq: "NyPJw0mcSuCwY2gV0zYny" }, locale: { eq: "en-US" } } ) { longMarkdown { childMarkdownRemark { @@ -107,9 +103,8 @@ export const pageQuery = graphql` } } } - longMarkdownComplex: contentfulText( - node_locale: { eq: "en-US" } - contentful_id: { eq: "3pwKS9UWsYmOguo4UdE1EB" } + longMarkdownComplex: contentfulContentTypeText( + sys: { id: { eq: "3pwKS9UWsYmOguo4UdE1EB" }, locale: { eq: "en-US" } } ) { longMarkdown { childMarkdownRemark { @@ -117,32 +112,28 @@ export const pageQuery = graphql` } } } - shortEnglish: contentfulText( - node_locale: { eq: "en-US" } - contentful_id: { eq: "2sQRyOLUexvWZj9nkzS3nN" } + shortEnglish: contentfulContentTypeText( + sys: { id: { eq: "2sQRyOLUexvWZj9nkzS3nN" }, locale: { eq: "en-US" } } ) { shortLocalized } - shortGerman: contentfulText( - node_locale: { eq: "de-DE" } - contentful_id: { eq: "2sQRyOLUexvWZj9nkzS3nN" } + shortGerman: contentfulContentTypeText( + sys: { id: { eq: "2sQRyOLUexvWZj9nkzS3nN" }, locale: { eq: "de-DE" } } ) { shortLocalized } - longEnglish: contentfulText( - node_locale: { eq: "en-US" } - contentful_id: { eq: "5csovkwdDBqTKwSblAOHvd" } + longEnglish: contentfulContentTypeText( + sys: { id: { eq: "5csovkwdDBqTKwSblAOHvd" }, locale: { eq: "en-US" } } ) { longLocalized { - longLocalized + raw } } - longGerman: contentfulText( - node_locale: { eq: "de-DE" } - contentful_id: { eq: "5csovkwdDBqTKwSblAOHvd" } + longGerman: contentfulContentTypeText( + sys: { id: { eq: "5csovkwdDBqTKwSblAOHvd" }, locale: { eq: "de-DE" } } ) { longLocalized { - longLocalized + raw } } } diff --git a/examples/using-contentful/package.json b/examples/using-contentful/package.json index f1a66f4698077..a063844460597 100644 --- a/examples/using-contentful/package.json +++ b/examples/using-contentful/package.json @@ -5,13 +5,13 @@ "version": "1.0.0", "author": "Marcus Ericsson (mericsson.com)", "dependencies": { - "gatsby": "next", - "gatsby-core-utils": "next", - "gatsby-plugin-image": "next", - "gatsby-plugin-sharp": "next", - "gatsby-plugin-typography": "next", - "gatsby-source-contentful": "next", - "gatsby-transformer-remark": "next", + "gatsby": "^5.14.0-next.0", + "gatsby-core-utils": "^4.14.0-next.0", + "gatsby-plugin-image": "^3.14.0-next.0", + "gatsby-plugin-sharp": "^5.14.0-next.0", + "gatsby-plugin-typography": "^5.14.0-next.0", + "gatsby-source-contentful": "^8.99.0-next.0", + "gatsby-transformer-remark": "^6.14.0-next.0", "prop-types": "^15.7.2", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -24,9 +24,6 @@ ], "license": "MIT", "main": "n/a", - "resolutions": { - "contentful": "6.1.3" - }, "scripts": { "develop": "gatsby develop", "build": "gatsby build", diff --git a/examples/using-contentful/src/layouts/index.js b/examples/using-contentful/src/layouts/index.js index d8dfe39dd2a82..4608bb54abe5b 100644 --- a/examples/using-contentful/src/layouts/index.js +++ b/examples/using-contentful/src/layouts/index.js @@ -12,7 +12,7 @@ const DefaultLayout = ({ children }) => ( <>

Products

    - {product && - product.map((p, i) => ( + {ContentfulContentTypeProduct.length && + ContentfulContentTypeProduct.map((p, i) => (
  • - {p.productName.productName} + {p.productName.raw}
  • ))}
@@ -60,19 +60,21 @@ CategoryTemplate.propTypes = propTypes export default CategoryTemplate export const pageQuery = graphql` - query($id: String!) { - contentfulCategory(id: { eq: $id }) { + query ($id: String!) { + contentfulContentTypeCategory(id: { eq: $id }) { title { - title + raw } icon { gatsbyImageData(layout: FIXED, width: 75) } - product { - gatsbyPath(filePath: "/products/{ContentfulProduct.id}") - id - productName { - productName + linkedFrom { + ContentfulContentTypeProduct { + gatsbyPath(filePath: "/products/{ContentfulContentTypeProduct.id}") + id + productName { + raw + } } } } diff --git a/examples/using-contentful/src/pages/image-api.js b/examples/using-contentful/src/pages/image-api.js index fb1104abe76ad..2641241d03335 100644 --- a/examples/using-contentful/src/pages/image-api.js +++ b/examples/using-contentful/src/pages/image-api.js @@ -316,8 +316,8 @@ const ImageAPI = props => { export default ImageAPI export const pageQuery = graphql` - query { - allContentfulAsset(filter: { node_locale: { eq: "en-US" } }) { + { + allContentfulAsset(filter: { sys: { locale: { eq: "en-US" } } }) { edges { node { title diff --git a/examples/using-contentful/src/pages/index.js b/examples/using-contentful/src/pages/index.js index 3f9ef900e998c..e4c4fc9966c2c 100644 --- a/examples/using-contentful/src/pages/index.js +++ b/examples/using-contentful/src/pages/index.js @@ -85,14 +85,16 @@ IndexPage.propTypes = propTypes export default IndexPage export const pageQuery = graphql` - query { - us: allContentfulProduct(filter: { node_locale: { eq: "en-US" } }) { + { + us: allContentfulContentTypeProduct( + filter: { sys: { locale: { eq: "en-US" } } } + ) { edges { node { id - gatsbyPath(filePath: "/products/{ContentfulProduct.id}") + gatsbyPath(filePath: "/products/{ContentfulContentTypeProduct.id}") productName { - productName + raw } image { gatsbyImageData(layout: FIXED, width: 75) @@ -100,13 +102,15 @@ export const pageQuery = graphql` } } } - german: allContentfulProduct(filter: { node_locale: { eq: "de" } }) { + german: allContentfulContentTypeProduct( + filter: { sys: { locale: { eq: "de" } } } + ) { edges { node { id - gatsbyPath(filePath: "/products/{ContentfulProduct.id}") + gatsbyPath(filePath: "/products/{ContentfulContentTypeProduct.id}") productName { - productName + raw } image { gatsbyImageData(layout: FIXED, width: 75) diff --git a/examples/using-contentful/src/pages/products/{ContentfulProduct.id}.js b/examples/using-contentful/src/pages/products/{ContentfulContentTypeProduct.id}.js similarity index 82% rename from examples/using-contentful/src/pages/products/{ContentfulProduct.id}.js rename to examples/using-contentful/src/pages/products/{ContentfulContentTypeProduct.id}.js index 14f574714916d..8a46b44cad269 100644 --- a/examples/using-contentful/src/pages/products/{ContentfulProduct.id}.js +++ b/examples/using-contentful/src/pages/products/{ContentfulContentTypeProduct.id}.js @@ -13,9 +13,9 @@ const propTypes = { class ProductTemplate extends React.Component { render() { - const product = this.props.data.contentfulProduct + const product = this.props.data.contentfulContentTypeProduct const { - productName: { productName }, + productName: { raw: productName }, productDescription, price, image, @@ -39,7 +39,7 @@ class ProductTemplate extends React.Component { )}

{productName}

-

Made by {brand.companyName.companyName}

+

Made by {brand.companyName.raw}

Price: ${price}
(
  • - {category.title.title} + {category.title.raw}
  • ))} @@ -70,10 +70,10 @@ ProductTemplate.propTypes = propTypes export default ProductTemplate export const pageQuery = graphql` - query($id: String!) { - contentfulProduct(id: { eq: $id }) { + query ($id: String!) { + contentfulContentTypeProduct(id: { eq: $id }) { productName { - productName + raw } productDescription { childMarkdownRemark { @@ -86,14 +86,14 @@ export const pageQuery = graphql` } brand { companyName { - companyName + raw } } categories { id - gatsbyPath(filePath: "/categories/{ContentfulCategory.id}") + gatsbyPath(filePath: "/categories/{ContentfulContentTypeCategory.id}") title { - title + raw } } } diff --git a/packages/gatsby-codemods/package.json b/packages/gatsby-codemods/package.json index 0d2ae76a4fc3a..47d7dfeeac88f 100644 --- a/packages/gatsby-codemods/package.json +++ b/packages/gatsby-codemods/package.json @@ -32,6 +32,7 @@ "execa": "^5.1.1", "graphql": "^16.6.0", "jscodeshift": "0.13.1", + "lodash": "^4.17.21", "recast": "0.20.5" }, "devDependencies": { diff --git a/packages/gatsby-codemods/src/bin/__tests__/gatsby-codemods-test.js b/packages/gatsby-codemods/src/bin/__tests__/gatsby-codemods-test.js index f65ad483dd485..6cf8c28ee3980 100644 --- a/packages/gatsby-codemods/src/bin/__tests__/gatsby-codemods-test.js +++ b/packages/gatsby-codemods/src/bin/__tests__/gatsby-codemods-test.js @@ -66,7 +66,7 @@ describe("transform", () => { run() expect(console.log).toBeCalledWith( - `You have passed in invalid codemod name: does-not-exist. Please pass in one of the following: gatsby-plugin-image, global-graphql-calls, import-link, navigate-calls, rename-bound-action-creators, sort-and-aggr-graphql.` + `You have passed in invalid codemod name: does-not-exist. Please pass in one of the following: gatsby-source-contentful, gatsby-plugin-image, global-graphql-calls, import-link, navigate-calls, rename-bound-action-creators, sort-and-aggr-graphql.` ) }) }) diff --git a/packages/gatsby-codemods/src/bin/cli.js b/packages/gatsby-codemods/src/bin/cli.js index 589762d6a1298..9c3b7e061d7f6 100644 --- a/packages/gatsby-codemods/src/bin/cli.js +++ b/packages/gatsby-codemods/src/bin/cli.js @@ -2,6 +2,7 @@ import path from "path" import execa from "execa" const codemods = [ + `gatsby-source-contentful`, `gatsby-plugin-image`, `global-graphql-calls`, `import-link`, diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/content-types-typescript.input.ts b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/content-types-typescript.input.ts new file mode 100644 index 0000000000000..d06d64244e704 --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/content-types-typescript.input.ts @@ -0,0 +1,3 @@ +interface Data { + allContentfulTemplatePage: FooConnection +} \ No newline at end of file diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/content-types-typescript.output.ts b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/content-types-typescript.output.ts new file mode 100644 index 0000000000000..cf15eeef36404 --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/content-types-typescript.output.ts @@ -0,0 +1,3 @@ +interface Data { + allContentfulContentTypeTemplatePage: FooConnection +} \ No newline at end of file diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/content-types.input.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/content-types.input.js new file mode 100644 index 0000000000000..c4e29d055ab67 --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/content-types.input.js @@ -0,0 +1,10 @@ +const demo = [ + ...data.allContentfulFoo.nodes, + ...data.allContentfulBar.nodes, +] +const content = data.contentfulPage.content +const { + data: { + allContentfulTemplatePage: { nodes: templatePages }, + }, +} = await graphql(``) \ No newline at end of file diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/content-types.output.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/content-types.output.js new file mode 100644 index 0000000000000..97d0afc1e4848 --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/content-types.output.js @@ -0,0 +1,10 @@ +const demo = [ + ...data.allContentfulContentTypeFoo.nodes, + ...data.allContentfulContentTypeBar.nodes, +] +const content = data.contentfulContentTypePage.content +const { + data: { + allContentfulContentTypeTemplatePage: { nodes: templatePages }, + }, +} = await graphql(``) \ No newline at end of file diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/contentful-asset.input.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/contentful-asset.input.js new file mode 100644 index 0000000000000..34b8bac2c5301 --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/contentful-asset.input.js @@ -0,0 +1,7 @@ +import React from "react" + +export default () => ( + +) \ No newline at end of file diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/contentful-asset.output.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/contentful-asset.output.js new file mode 100644 index 0000000000000..330c083d93f19 --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/contentful-asset.output.js @@ -0,0 +1,7 @@ +import React from "react" + +export default () => ( + +) \ No newline at end of file diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/contentful-sys.input.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/contentful-sys.input.js new file mode 100644 index 0000000000000..e6e4e4696953a --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/contentful-sys.input.js @@ -0,0 +1,11 @@ +const res1 = allContentfulPage.nodes.contentful_id +const res2 = allContentfulPage.nodes.sys.contentType.__typename +const { contentful_id, createdAt, updatedAt } = allContentfulPage.nodes +const { title, metaDescription, metaImage, content } = data.contentfulPage +const { foo } = result.data.allContentfulPage.nodes[0] +const { + revision, + sys: { + contentType: { __typename }, + }, +} = allContentfulPage.nodes diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/contentful-sys.output.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/contentful-sys.output.js new file mode 100644 index 0000000000000..3162c4b43f8b1 --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/contentful-sys.output.js @@ -0,0 +1,17 @@ +const res1 = allContentfulPage.nodes.sys.id +const res2 = allContentfulPage.nodes.sys.contentType.name +const { + sys: { + id: contentful_id, + firstPublishedAt: createdAt, + publishedAt: updatedAt + } +} = allContentfulPage.nodes +const { title, metaDescription, metaImage, content } = data.contentfulContentTypePage +const { foo } = result.data.allContentfulContentTypePage.nodes[0] +const { + sys: { + publishedVersion: revision, + contentType: { name } + } +} = allContentfulPage.nodes diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/gatsby-node.input.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/gatsby-node.input.js new file mode 100644 index 0000000000000..0c603f87e12d8 --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/gatsby-node.input.js @@ -0,0 +1,14 @@ +exports.createSchemaCustomization = null +export const createSchemaCustomization = null + +// export function createResolvers(actions) { +// actions.createResolvers({ +// ContentfulFoo: {}, +// }) +// } + +export const createResolvers = (actions) => { + actions.createResolvers({ + ContentfulFoo: {}, + }) +} \ No newline at end of file diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/gatsby-node.output.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/gatsby-node.output.js new file mode 100644 index 0000000000000..b4b44cc19fab2 --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/gatsby-node.output.js @@ -0,0 +1,14 @@ +exports.createSchemaCustomization = null +export const createSchemaCustomization = null + +// export function createResolvers(actions) { +// actions.createResolvers({ +// ContentfulFoo: {}, +// }) +// } + +export const createResolvers = (actions) => { + actions.createResolvers({ + ContentfulContentTypeFoo: {}, + }) +} \ No newline at end of file diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-all.input.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-all.input.js new file mode 100644 index 0000000000000..c66bf5eaf0890 --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-all.input.js @@ -0,0 +1,9 @@ +const result = await graphql` + { + allContentfulPage(limit: 1000) { + nodes { + id + } + } + } +` diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-all.output.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-all.output.js new file mode 100644 index 0000000000000..10a770abaddc4 --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-all.output.js @@ -0,0 +1,7 @@ +const result = await graphql`{ + allContentfulContentTypePage(limit: 1000) { + nodes { + id + } + } +}` diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-fragment.input.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-fragment.input.js new file mode 100644 index 0000000000000..a368562c6f3ca --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-fragment.input.js @@ -0,0 +1,34 @@ +export const ExampleFragment = graphql` + fragment Example on ContentfulExample { + title + contentful_id + logo { + file { + url + fileName + contentType + details { + size + image { + width + height + } + } + } + } + } + { + allContentfulFoo { + nodes { + ... on ContentfulExample { + contentful_id + logo { + file { + url + } + } + } + } + } + } +` \ No newline at end of file diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-fragment.output.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-fragment.output.js new file mode 100644 index 0000000000000..2fedb08a9113a --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-fragment.output.js @@ -0,0 +1,29 @@ +export const ExampleFragment = graphql`fragment Example on ContentfulContentTypeExample { + title + sys { + id + } + logo { + url + fileName + contentType + size + width + height + } +} + +{ + allContentfulContentTypeFoo { + nodes { + ... on ContentfulContentTypeExample { + sys { + id + } + logo { + url + } + } + } + } +}` diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-single.input.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-single.input.js new file mode 100644 index 0000000000000..6f24e1c68328e --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-single.input.js @@ -0,0 +1,7 @@ +const result = await graphql(` + { + contentfulPage { + id + } + } +`) diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-single.output.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-single.output.js new file mode 100644 index 0000000000000..799e641992d3e --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-single.output.js @@ -0,0 +1,5 @@ +const result = await graphql(`{ + contentfulContentTypePage { + id + } +}`) diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-assets.input.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-assets.input.js new file mode 100644 index 0000000000000..248f80f3c80e5 --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-assets.input.js @@ -0,0 +1,37 @@ +const result = await graphql(` + { + allContentfulPage( + filter: { logo: { file: { url: { ne: null } } } }, + sort: [{createdAt: ASC}, {logo: {file: {fileName: ASC}}}] + ) { + nodes { + id + logo { + file { + url + fileName + contentType + details { + size + image { + width + height + } + } + } + } + } + } + allContentfulAsset( + filter: {file: { url: { ne: null } }}, + sort: [{createdAt: ASC}, {file: {fileName: ASC}}] + ) { + nodes { + id + } + } + contentfulAsset(file: { url: { ne: null } }) { + id + } + } +`) diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-assets.output.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-assets.output.js new file mode 100644 index 0000000000000..98f4790628c6e --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-assets.output.js @@ -0,0 +1,29 @@ +const result = await graphql(`{ + allContentfulContentTypePage( + filter: {logo: {url: {ne: null}}} + sort: [{sys: {firstPublishedAt: ASC}}, {logo: {fileName: ASC}}] + ) { + nodes { + id + logo { + url + fileName + contentType + size + width + height + } + } + } + allContentfulAsset( + filter: {url: {ne: null}} + sort: [{sys: {firstPublishedAt: ASC}}, {fileName: ASC}] + ) { + nodes { + id + } + } + contentfulAsset(url: {ne: null}) { + id + } +}`) diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-metadata.input.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-metadata.input.js new file mode 100644 index 0000000000000..a946df7fd715a --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-metadata.input.js @@ -0,0 +1,29 @@ +const result = await graphql(` + { + allContentfulTag(sort: { fields: contentful_id }) { + nodes { + name + contentful_id + } + } + allContentfulNumber( + sort: { fields: contentful_id } + filter: { + metadata: { + tags: { elemMatch: { contentful_id: { eq: "numberInteger" } } } + } + } + ) { + nodes { + title + integer + metadata { + tags { + name + contentful_id + } + } + } + } + } +`) diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-metadata.output.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-metadata.output.js new file mode 100644 index 0000000000000..e9e6e1b952b4c --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-metadata.output.js @@ -0,0 +1,23 @@ +const result = await graphql(`{ + allContentfulTag(sort: {fields: contentful_id}) { + nodes { + name + contentful_id + } + } + allContentfulContentTypeNumber( + sort: {fields: contentful_id} + filter: {contentfulMetadata: {tags: {elemMatch: {contentful_id: {eq: "numberInteger"}}}}} + ) { + nodes { + title + integer + contentfulMetadata { + tags { + name + contentful_id + } + } + } + } +}`) diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-sys.input.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-sys.input.js new file mode 100644 index 0000000000000..6a2dbc9d3103e --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-sys.input.js @@ -0,0 +1,45 @@ +const result = await graphql(` + { + allContentfulPage(limit: 1000) { + nodes { + contentful_id + customName: node_locale + createdAt + updatedAt + revision + spaceId + sys { + type + contentType { + __typename + } + } + } + } + contentfulPage { + contentful_id + node_locale + createdAt + updatedAt + revision + spaceId + sys { + type + contentType { + __typename + } + } + } + allContentfulPage( + filter: { slug: { eq: "blog" }, node_locale: { eq: $locale } } + sort: { updatedAt: DESC } + ) { + nodes { + id + } + } + contentfulPage(slug: { eq: "blog" }, node_locale: { eq: $locale }) { + id + } + } +`) diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-sys.output.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-sys.output.js new file mode 100644 index 0000000000000..a12f1e1e4998c --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-sys.output.js @@ -0,0 +1,43 @@ +const result = await graphql(`{ + allContentfulContentTypePage(limit: 1000) { + nodes { + sys { + id + customName: locale + firstPublishedAt + publishedAt + publishedVersion + spaceId + type + contentType { + name + } + } + } + } + contentfulContentTypePage { + sys { + id + locale + firstPublishedAt + publishedAt + publishedVersion + spaceId + type + contentType { + name + } + } + } + allContentfulContentTypePage( + filter: {slug: {eq: "blog"}, sys: {locale: {eq: $locale}}} + sort: {sys: {publishedAt: DESC}} + ) { + nodes { + id + } + } + contentfulContentTypePage(slug: {eq: "blog"}, sys: {locale: {eq: $locale}}) { + id + } +}`) diff --git a/packages/gatsby-codemods/src/transforms/__tests__/gatsby-source-contentful.js b/packages/gatsby-codemods/src/transforms/__tests__/gatsby-source-contentful.js new file mode 100644 index 0000000000000..f9a7c86eb4d56 --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__tests__/gatsby-source-contentful.js @@ -0,0 +1,27 @@ +const tests = [ + `content-types`, + `content-types-typescript`, + `contentful-asset`, + `contentful-sys`, + `gatsby-node`, + `graphql-content-type-all`, + `graphql-content-type-fragment`, + `graphql-content-type-single`, + `graphql-contentful-assets`, + `graphql-contentful-metadata`, + `graphql-contentful-sys`, +] + +const defineTest = require(`jscodeshift/dist/testUtils`).defineTest + +describe(`codemods`, () => { + tests.forEach(test => + defineTest( + __dirname, + `gatsby-source-contentful`, + null, + `gatsby-source-contentful/${test}`, + { parser: test.indexOf(`typescript`) !== -1 ? `ts` : `js` } + ) + ) +}) diff --git a/packages/gatsby-codemods/src/transforms/gatsby-source-contentful.js b/packages/gatsby-codemods/src/transforms/gatsby-source-contentful.js new file mode 100644 index 0000000000000..9e77eeba1b580 --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/gatsby-source-contentful.js @@ -0,0 +1,734 @@ +import * as graphql from "graphql" +import { parse, print } from "recast" +import { transformFromAstSync, parseSync } from "@babel/core" +import { cloneDeep } from "lodash" + +export default function jsCodeShift(file) { + if ( + file.path.includes(`node_modules`) || + file.path.includes(`.cache`) || + file.path.includes(`public`) + ) { + return file.source + } + const transformedSource = babelRecast(file.source, file.path) + return transformedSource +} + +export function babelRecast(code, filePath) { + const transformedAst = parse(code, { + parser: { + parse: source => runParseSync(source, filePath), + }, + }) + + const changedTracker = { hasChanged: false, filename: filePath } // recast adds extra semicolons that mess with diffs and we want to avoid them + + const options = { + cloneInputAst: false, + code: false, + ast: true, + plugins: [[updateImport, changedTracker]], + } + + const { ast } = transformFromAstSync(transformedAst, code, options) + + if (changedTracker.hasChanged) { + return print(ast, { lineTerminator: `\n` }).code + } + return code +} + +const CONTENT_TYPE_SELECTOR_REGEX = /^(allContentful|[cC]ontentful)([A-Z0-9].+)/ +const CONTENT_TYPE_SELECTOR_BLACKLIST = [`Asset`, `Reference`, `Id`, `Tag`] +const SYS_FIELDS_TRANSFORMS = new Map([ + [`node_locale`, `locale`], + [`contentful_id`, `id`], + [`spaceId`, `spaceId`], + [`createdAt`, `firstPublishedAt`], + [`updatedAt`, `publishedAt`], + [`revision`, `publishedVersion`], +]) + +const isContentTypeSelector = selector => { + if (!selector) { + return false + } + const res = selector.match(CONTENT_TYPE_SELECTOR_REGEX) + return res && !CONTENT_TYPE_SELECTOR_BLACKLIST.includes(res[2]) +} +const updateContentfulSelector = selector => + selector.replace(`ontentful`, `ontentfulContentType`) + +const renderFilename = (path, state) => + `${state.opts.filename} (Line ${path.node.loc.start.line})` + +const injectNewFields = (selections, newFields, fieldToReplace) => { + if (!fieldToReplace) { + return [...selections, ...newFields] + } + + const fieldIndex = selections.findIndex( + ({ name }) => name?.value === fieldToReplace + ) + + return [ + ...selections.slice(0, fieldIndex), + ...newFields, + ...selections.slice(fieldIndex + 1), + ] +} + +function getNestedMemberExpressionProperties(node, t) { + const properties = [] + let current = node + while (t.isMemberExpression(current)) { + if (t.isIdentifier(current.property)) { + properties.push(current.property.name) + } + current = current.object + } + return properties.reverse() +} + +export function updateImport(babel) { + const { types: t } = babel + // Stack to keep track of nesting + const stack = [] + // Flag to indicate whether we are inside createResolvers function + let insideCreateResolvers = false + return { + visitor: { + Identifier(path, state) { + if ( + path.node.name === `createSchemaCustomization` && + state.opts.filename.match(/gatsby-node/) + ) { + console.log( + `${renderFilename( + path, + state + )}: Check your custom schema customizations if you patch or adjust schema related to Contentful. You probably can remove it now.` + ) + } + + // Rename content type identifiers within createResolvers + if (insideCreateResolvers) { + const variableName = path.node.name + + // Check if the variable name matches the regex + if (isContentTypeSelector(variableName)) { + path.node.name = updateContentfulSelector(variableName) + state.opts.hasChanged = true + } + } + }, + ObjectPattern: { + enter(path) { + // Push this ObjectPattern onto the stack as we enter it + stack.push(path.node.properties.map(prop => prop.key?.name)) + }, + exit(path, state) { + // Check if the variable name matches the regex + path.node.properties.forEach(prop => { + if (isContentTypeSelector(prop.key?.name)) { + prop.key.name = updateContentfulSelector(prop.key.name) + state.opts.hasChanged = true + } + }) + + // Rename contentType.__typename to contentType.name + if ( + JSON.stringify([[`sys`], [`contentType`], [`__typename`]]) === + JSON.stringify(stack) + ) { + const typenameProp = path.node.properties.find( + prop => prop.key.name === `__typename` + ) + if (typenameProp) { + typenameProp.key = t.identifier(`name`) + typenameProp.value = t.identifier(`name`) + } + // Merge old sys fields into new structure + } else { + const transformedSysProperties = [] + path.node.properties.forEach(property => { + if (SYS_FIELDS_TRANSFORMS.has(property.key?.name)) { + const transformedProp = { + ...property, + key: { + ...property.key, + name: SYS_FIELDS_TRANSFORMS.get(property.key.name), + }, + } + transformedSysProperties.push(transformedProp) + } + }) + if (transformedSysProperties.length) { + const sysField = { + type: `Property`, + key: { + type: `Identifier`, + name: `sys`, + }, + value: { + type: `ObjectPattern`, + properties: transformedSysProperties, + }, + } + path.node.properties = injectSysField( + sysField, + path.node.properties + ) + + state.opts.hasChanged = true + } + } + + // Pop this ObjectPattern off the stack as we exit it + stack.pop() + }, + }, + MemberExpression(path, state) { + const nestedProperties = getNestedMemberExpressionProperties( + path.node, + t + ) + + const assetFlatStructure = new Map([ + [`url`, [`url`, `file`]], + [`fileName`, [`fileName`, `file`]], + [`contentType`, [`contentType`, `file`]], + [`size`, [`size`, `details`, `file`]], + [`width`, [`width`, `image`, `details`, `file`]], + [`height`, [`height`, `image`, `details`, `file`]], + ]) + + for (const [newProp, oldProps] of assetFlatStructure) { + if ( + nestedProperties.slice(-oldProps.length).join(`.`) === + oldProps.reverse().join(`.`) + ) { + // We found a matching nested property. + // Rebuild the MemberExpression with the new structure. + let baseExpression = path.node + for (let i = 0; i < oldProps.length; i++) { + baseExpression = baseExpression.object + } + const newExpression = t.memberExpression( + baseExpression, + t.identifier(newProp) + ) + path.replaceWith(newExpression) + state.opts.hasChanged = true + return + } + } + + // Identify MemberExpression for `allContentfulPage.nodes.contentfulId` + const propName = path.node.property.name + const replacement = SYS_FIELDS_TRANSFORMS.get(propName) + + if (replacement) { + // Rewrite the MemberExpression with the new property + path.node.property = t.identifier(replacement) + + // Also rewrite the parent node to `.sys.` if it's not already + if (path.node.object.property?.name !== `sys`) { + path.node.object = t.memberExpression( + path.node.object, + t.identifier(`sys`) + ) + } + state.opts.hasChanged = true + } + + // Rename sys.contentType.__typename to sys.contentType.name + if ( + propName === `__typename` && + t.isMemberExpression(path.node.object) && + t.isIdentifier(path.node.object.property, { name: `contentType` }) && + t.isMemberExpression(path.node.object.object) && + t.isIdentifier(path.node.object.object.property, { name: `sys` }) + ) { + path.node.property = t.identifier(`name`) + return + } + + if (isContentTypeSelector(path.node.property?.name)) { + if ( + path.node.object?.name === `data` || + path.node.object.property?.name === `data` + ) { + path.node.property.name = updateContentfulSelector( + path.node.property.name + ) + state.opts.hasChanged = true + } else { + console.log( + `${renderFilename(path, state)}: You might need to change "${ + path.node.property?.name + }" to "${updateContentfulSelector(path.node.property.name)}"` + ) + } + } + }, + ExportNamedDeclaration: { + enter(path) { + const declaration = path.node.declaration + + // For "export function createResolvers() {}" + if ( + t.isFunctionDeclaration(declaration) && + t.isIdentifier(declaration.id, { name: `createResolvers` }) + ) { + insideCreateResolvers = true + } + + // For "export const createResolvers = function() {}" or "export const createResolvers = () => {}" + else if (t.isVariableDeclaration(declaration)) { + const declarators = declaration.declarations + for (const declarator of declarators) { + if ( + t.isIdentifier(declarator.id, { name: `createResolvers` }) && + (t.isFunctionExpression(declarator.init) || + t.isArrowFunctionExpression(declarator.init)) + ) { + insideCreateResolvers = true + } + } + } + }, + exit() { + insideCreateResolvers = false + }, + }, + TSInterfaceDeclaration(path, state) { + path.node.body.body.forEach(property => { + if ( + t.isTSPropertySignature(property) && + isContentTypeSelector(property.key.name) + ) { + property.key.name = updateContentfulSelector(property.key.name) + state.opts.hasChanged = true + } + }) + }, + TaggedTemplateExpression({ node }, state) { + if (node.tag.name !== `graphql`) { + return + } + const query = node.quasi?.quasis?.[0]?.value?.raw + if (query) { + const { ast: transformedGraphQLQuery, hasChanged } = + processGraphQLQuery(query, state) + + if (hasChanged) { + node.quasi.quasis[0].value.raw = graphql.print( + transformedGraphQLQuery + ) + state.opts.hasChanged = true + } + } + }, + CallExpression({ node }, state) { + if (node.callee.name !== `graphql`) { + return + } + const query = node.arguments?.[0].quasis?.[0]?.value?.raw + + if (query) { + const { ast: transformedGraphQLQuery, hasChanged } = + processGraphQLQuery(query, state) + + if (hasChanged) { + node.arguments[0].quasis[0].value.raw = graphql.print( + transformedGraphQLQuery + ) + state.opts.hasChanged = true + } + } + }, + }, + } +} + +// Locate a subfield within a selection set or fields +function locateSubfield(node, fieldName) { + const subFields = Array.isArray(node) + ? node + : node.selectionSet?.selections || node.value?.fields + if (!subFields) { + return null + } + return subFields.find(({ name }) => name?.value === fieldName) +} + +// Replace first old field occurence with new sys field +const injectSysField = (sysField, selections) => { + let sysInjected = false + + selections = selections + .map(field => { + const fieldName = field.name?.value || field.key?.name + if (fieldName === `sys`) { + const existingSysFields = ( + field.selectionSet?.selections || field.value.properties + ).map(subField => { + const kind = subField.kind || subField.type + // handle contentType rename + if ( + (kind === `ObjectProperty` + ? subField.key.name + : subField.name.value) === `contentType` + ) { + const subfields = + kind === `ObjectProperty` + ? subField.value.properties + : subField.selectionSet.selections + + subfields.map(contentTypeField => { + const fieldName = + kind === `ObjectProperty` + ? contentTypeField.key.name + : contentTypeField.name.value + + if (fieldName === `__typename`) { + if (kind === `ObjectProperty`) { + contentTypeField.key.name = `name` + } else { + contentTypeField.name.value = `name` + } + } + }) + } + return subField + }) + if (sysField?.type === `Property`) { + sysField.value.properties.push(...existingSysFields) + } else { + sysField.selectionSet.selections.push(...existingSysFields) + } + return null + } + return field + }) + .filter(Boolean) + + // Replace first old field occurence with new sys field + return selections + .map(field => { + const fieldName = field.name?.value || field.key?.name + if (SYS_FIELDS_TRANSFORMS.has(fieldName)) { + if (!sysInjected) { + // Inject for first occurence of a sys field + sysInjected = true + return sysField + } + // Remove all later fields + return null + } + // Keep non-sys fields as they are + return field + }) + .filter(Boolean) +} + +// Flatten the old deeply nested Contentful asset structure +const flattenAssetFields = node => { + const flatAssetFields = [] + + // Flatten asset file field + const fileField = locateSubfield(node, `file`) + + if (fileField) { + // Top level file fields + const urlField = locateSubfield(fileField, `url`) + if (urlField) { + flatAssetFields.push(urlField) + } + const fileNameField = locateSubfield(fileField, `fileName`) + if (fileNameField) { + flatAssetFields.push(fileNameField) + } + const contentTypeField = locateSubfield(fileField, `contentType`) + if (contentTypeField) { + flatAssetFields.push(contentTypeField) + } + + // details subfield with size and dimensions + const detailsField = locateSubfield(fileField, `details`) + if (detailsField) { + const sizeField = locateSubfield(detailsField, `size`) + if (sizeField) { + flatAssetFields.push(sizeField) + } + // width & height from image subfield + const imageField = locateSubfield(detailsField, `image`) + if (imageField) { + const widthField = locateSubfield(imageField, `width`) + if (widthField) { + flatAssetFields.push(widthField) + } + const heightField = locateSubfield(imageField, `height`) + if (heightField) { + flatAssetFields.push(heightField) + } + } + } + } + return flatAssetFields +} + +function createNewSysField(fields, fieldType = `Field`) { + const kind = fieldType === `Argument` ? `Argument` : `Field` + const subKindValue = fieldType === `Argument` ? `ObjectValue` : `SelectionSet` + const subKindIndex = fieldType === `Argument` ? `value` : `selectionSet` + const subKindIndex2 = fieldType === `Argument` ? `fields` : `selections` + + const contentfulSysFields = fields.filter(({ name }) => + SYS_FIELDS_TRANSFORMS.has(name?.value) + ) + + if (contentfulSysFields.length) { + const transformedSysFields = cloneDeep(contentfulSysFields).map(field => { + return { + ...field, + name: { + ...field.name, + value: SYS_FIELDS_TRANSFORMS.get(field.name.value), + }, + } + }) + + const sysField = { + kind: kind, + name: { + kind: `Name`, + value: `sys`, + }, + [subKindIndex]: { + kind: subKindValue, + [subKindIndex2]: transformedSysFields, + }, + } + return sysField + } + return null +} + +function processGraphQLQuery(query) { + try { + let hasChanged = false // this is sort of a hack, but print changes formatting and we only want to use it when we have to + const ast = graphql.parse(query) + + function processArguments(node) { + // flatten Contentful Asset filters + // Queries directly on allContentfulAssets + const flatAssetFields = flattenAssetFields(node) + if (flatAssetFields.length) { + node.value.fields = injectNewFields( + node.value.fields, + flatAssetFields, + `file` + ) + hasChanged = true + } + // Subfields that might be asset fields + node.value.fields.forEach((field, fieldIndex) => { + const flatAssetFields = flattenAssetFields(field) + if (flatAssetFields.length) { + node.value.fields[fieldIndex].value.fields = injectNewFields( + node.value.fields[fieldIndex].value.fields, + flatAssetFields, + `file` + ) + hasChanged = true + } + }) + + // Rename metadata -> contentfulMetadata + node.value.fields.forEach(field => { + if (field.name.value === `metadata`) { + field.name.value = `contentfulMetadata` + hasChanged = true + } + }) + + const sysField = createNewSysField(node.value.fields, `Argument`) + if (sysField) { + node.value.fields = injectSysField(sysField, node.value.fields) + hasChanged = true + } + } + + graphql.visit(ast, { + Argument(node) { + // Update filters and sort of collection endpoints + if ([`filter`, `sort`].includes(node.name.value)) { + if (node.value.kind === `ListValue`) { + node.value.values.forEach(node => processArguments({ value: node })) + return + } + processArguments(node) + } + }, + SelectionSet(node) { + // Rename content type node selectors + node.selections.forEach(field => { + if (isContentTypeSelector(field.name?.value)) { + field.name.value = updateContentfulSelector(field.name.value) + hasChanged = true + } + }) + }, + InlineFragment(node) { + if (isContentTypeSelector(node.typeCondition.name?.value)) { + node.typeCondition.name.value = updateContentfulSelector( + node.typeCondition.name.value + ) + hasChanged = true + + const sysField = createNewSysField(node.selectionSet.selections) + if (sysField) { + node.selectionSet.selections = injectSysField( + sysField, + node.selectionSet.selections + ) + hasChanged = true + } + } + }, + FragmentDefinition(node) { + if (isContentTypeSelector(node.typeCondition.name?.value)) { + node.typeCondition.name.value = updateContentfulSelector( + node.typeCondition.name.value + ) + hasChanged = true + + const sysField = createNewSysField(node.selectionSet.selections) + if (sysField) { + node.selectionSet.selections = injectSysField( + sysField, + node.selectionSet.selections + ) + hasChanged = true + } + } + }, + Field(node) { + // Flatten asset fields + if (node.name.value === `contentfulAsset`) { + const flatAssetFields = flattenAssetFields({ + value: { fields: node.arguments }, + }) + + node.arguments = injectNewFields( + node.arguments, + flatAssetFields, + `file` + ) + + hasChanged = true + } + + // Rename metadata -> contentfulMetadata + if (node.name.value === `metadata`) { + const tagsField = locateSubfield(node, `tags`) + if (tagsField) { + node.name.value = `contentfulMetadata` + hasChanged = true + } + } + + if (isContentTypeSelector(node.name.value)) { + // Move sys properties into new sys property + const nodesField = + node.name.value.indexOf(`all`) === 0 && + locateSubfield(node, `nodes`) + const rootNode = nodesField || node + + const sysField = createNewSysField(rootNode.selectionSet.selections) + if (sysField) { + rootNode.selectionSet.selections = injectSysField( + sysField, + rootNode.selectionSet.selections + ) + hasChanged = true + } + + const filterNode = + node.name.value.indexOf(`all`) === 0 && + locateSubfield(node.arguments, `filter`) + + const filterFields = filterNode?.value?.fields || node.arguments + + if (filterFields && filterFields.length) { + const sysField = createNewSysField(filterFields, `Argument`) + // Inject the new sys at the first occurence of any old sys field + if (sysField) { + if (node.name.value.indexOf(`all`) === 0) { + const filterFieldIndex = node.arguments.findIndex( + field => field.name?.value === `filter` + ) + node.arguments[filterFieldIndex].value.fields = injectSysField( + sysField, + node.arguments[filterFieldIndex].value.fields + ) + } else { + node.arguments = injectSysField(sysField, filterFields) + } + hasChanged = true + } + } + } + + // Flatten asset file field + const flatAssetFields = flattenAssetFields(node) + if (flatAssetFields.length) { + node.selectionSet.selections = injectNewFields( + node.selectionSet.selections, + flatAssetFields, + `file` + ) + hasChanged = true + } + }, + }) + return { ast, hasChanged } + } catch (err) { + throw new Error( + `GatsbySourceContentfulCodemod: GraphQL syntax error in query:\n\n${query}\n\nmessage:\n\n${err}` + ) + } +} + +function runParseSync(source, filePath) { + let ast + try { + ast = parseSync(source, { + plugins: [ + `@babel/plugin-syntax-jsx`, + `@babel/plugin-proposal-class-properties`, + ], + overrides: [ + { + test: [`**/*.ts`, `**/*.tsx`], + plugins: [[`@babel/plugin-syntax-typescript`, { isTSX: true }]], + }, + ], + filename: filePath, + parserOpts: { + tokens: true, // recast uses this + }, + }) + } catch (e) { + console.error(e) + } + if (!ast) { + console.log( + `The codemod was unable to parse ${filePath}. If you're running against the '/src' directory and your project has a custom babel config, try running from the root of the project so the codemod can pick it up.` + ) + } + return ast +} diff --git a/packages/gatsby-source-contentful/.babelrc b/packages/gatsby-source-contentful/.babelrc deleted file mode 100644 index ac0ad292bb087..0000000000000 --- a/packages/gatsby-source-contentful/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": [["babel-preset-gatsby-package"]] -} diff --git a/packages/gatsby-source-contentful/.babelrc.js b/packages/gatsby-source-contentful/.babelrc.js new file mode 100644 index 0000000000000..1e7ab4a86bfca --- /dev/null +++ b/packages/gatsby-source-contentful/.babelrc.js @@ -0,0 +1,4 @@ +module.exports = { + presets: [["babel-preset-gatsby-package", { esm: true }]], + plugins: ["@babel/plugin-transform-modules-commonjs"], +} diff --git a/packages/gatsby-source-contentful/.gitignore b/packages/gatsby-source-contentful/.gitignore index 1a46274e77c91..e69787c28dfd7 100644 --- a/packages/gatsby-source-contentful/.gitignore +++ b/packages/gatsby-source-contentful/.gitignore @@ -1,4 +1,35 @@ -/__tests__ -/yarn.lock -/*.js +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git +node_modules + +decls +dist +*.js +!gatsby-node.js +!.babelrc.js +!jest.config.js !index.js diff --git a/packages/gatsby-source-contentful/MIGRATION.md b/packages/gatsby-source-contentful/MIGRATION.md new file mode 100644 index 0000000000000..3e6b44c4b3a3b --- /dev/null +++ b/packages/gatsby-source-contentful/MIGRATION.md @@ -0,0 +1,440 @@ +# Migration Guide for `gatsby-source-contentful` v9 + +The v9 release of `gatsby-source-contentful` brings significant improvements, focusing on stability and enhancing the developer experience. + +
    +Table of contents + +- [Migration Guide for `gatsby-source-contentful` v9](#migration-guide-for-gatsby-source-contentful-v9) + - [Core Changes, Updates and new Features](#core-changes-updates-and-new-features) + - [1. Introduction](#1-introduction) + - [2. Automated Migration with Codemods](#2-automated-migration-with-codemods) + - [3. Manual Changes and Additional Updates](#3-manual-changes-and-additional-updates) + - [Breaking Changes](#breaking-changes) + - [1. Remove Your Workarounds](#1-remove-your-workarounds) + - [2. New content type naming pattern](#2-new-content-type-naming-pattern) + - [3. Metadata / Data from Contentful's `sys` field](#3-metadata--data-from-contentfuls-sys-field) + - [Sorting by `sys`](#sorting-by-sys) + - [Filtering by `sys`](#filtering-by-sys) + - [4. Fields](#4-fields) + - [Text fields](#text-fields) + - [JSON fields](#json-fields) + - [Rich Text field](#rich-text-field) + - [Schema Comparison](#schema-comparison) + - [Query Updates](#query-updates) + - [Rendering changes for Rich Text](#rendering-changes-for-rich-text) + - [5. Assets](#5-assets) + - [Old GraphlQL schema for assets](#old-graphlql-schema-for-assets) + - [New GraphlQL schema for assets](#new-graphlql-schema-for-assets) + - [6. Using the Contentful Preview API (CPA)](#6-using-the-contentful-preview-api-cpa) + - [Conclusion and Support](#conclusion-and-support) + +
    + +## Core Changes, Updates and new Features + +- **Dynamic Schema Generation**: Schema types are now dynamically generated based on the Contentful content model, eliminating site breakages due to empty content fields. +- **Rich Text and JSON Field Enhancements**: Improved handling of Rich Text and JSON fields for more accurate processing, querying, and rendering. +- **Schema and Performance Optimizations**: An optimized, leaner schema reduces node count and improves build times, especially for larger projects. +- **Contentful GraphQL API Alignment**: Enhanced alignment with Contentful's GraphQL API enables more intuitive and consistent querying. +- **Removed Content Type Name Restrictions**: New naming patterns now allow all content type names, including previously restricted names like `entity` and `reference`. +- **Updated Field Name Restrictions**: Fields can now be named `contentful_id`, with restrictions now applied to names like `sys`, `contentfulMetadata`, and `linkedFrom`. +- **Refined Backlinks**: Backlinks/references are now located in the `linkedFrom` field, aligning with Contentful's GraphQL API structure. +- **Expanded Configuration Options**: Additional [configuration options](#advanced-configuration) provide greater control and customization for your specific project needs. + +## 1. Introduction + +Version 9 of the `gatsby-source-contentful` plugin marks a significant evolution, introducing changes mainly in the structure of GraphQL queries. + +This guide helps you to navigate through these updates, ensuring a straightforward migration to the latest version. We'll cover both automated and manual steps to adapt your project seamlessly to v9's advancements. + +## 2. Automated Migration with Codemods + +Before you start manual updates, take advantage of our codemods that automate most of the transition process. You can use one of the following options to apply the codemods to your project: + +**Option A:** (Direct Execution) + +```bash +npx gatsby-codemods@ctf-next gatsby-source-contentful . +``` + +**Hint:** If you use `.mjs` files, rename them to `.js` or `.ts` first. + +**Option B:** (Install, Execute, Uninstall) + +```bash +# Install codemods +npm install -D jscodeshift gatsby-codemods@ctf-next + +# Execute codemods +npx jscodeshift -t ./node_modules/gatsby-codemods/transforms/gatsby-source-contentful.js . + +# Uninstall codemods +npm remove jscodeshift gatsby-codemods +``` + +**Handling Large Codebases:** If your project is particularly large, you may need to increase the maximum memory allocation for the process: + +```bash +NODE_OPTIONS=--max_old_space_size=10240 npx ... +``` + +## 3. Manual Changes and Additional Updates + +After applying the codemods, review your project for any additional changes that need to be made manually. The following sections detail specific areas that require attention. + +## Breaking Changes + +With v9, the schema generated by `gatsby-source-contentful` is now closely aligned with Contentful's GraphQL API. This significant alignment means several changes in how you structure your GraphQL queries: + +### 1. Remove Your Workarounds + +If you previously implemented workarounds, such as freezing your schema with `gatsby-plugin-schema-snapshot` or using `createSchemaCustomization` to address issues in your Contentful GraphQL schema, you can safely remove these. + +The enhancements in v9 have been designed to eliminate the need for such workarounds, offering a more reliable schema generation process that mirrors the structure and functionality of Contentful's native GraphQL API. + +### 2. New content type naming pattern + +The old naming pattern for generated node types based on your Contentful content model was pretty simple. This gave us shorter queries, but came with restrictions for content type names. Content type names like `entity`, `reference`, `tag`, `asset` did cause problems or simply did not work. + +1. Generated GraphQL node types are now prefixed with `ContentfulContentType` instead of `Contentful` +2. Hardcoded types and interfaces stay short. For example: `ContentfulEntry`, `ContentfulAsset`, `ContentfulLocation` or + `ContentfulRichText` + +This is close to the [behaviour of the Contentful GraphQL API](https://www.contentful.com/developers/docs/references/graphql/#/reference/schema-generation/types): + +> If the generated name starts with a number or collides with a reserved type name, it gets prefixed with 'ContentType' + +Instead of doing this on a collision, we always prefix for consistency. + +```diff +export const pageQuery = graphql` + query { +- allContentfulProduct { ++ allContentfulContentTypeProduct { + ... + } + } +` +``` + +### 3. Metadata / Data from Contentful's `sys` field + +The `sys` field now reflects the structure of the sys field in the Contentful [GraphQL API](https://www.contentful.com/developers/docs/references/graphql/#/reference/schema-generation/sys-field) + +| Old Path | New Path | Comment | +| --------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| node_locale | sys.locale | this does not exists in Contentful's sys but we need it (for now) [as Contentful GraphQL handles locales different](https://www.contentful.com/developers/docs/references/graphql/#/reference/locale-handling) | +| contentful_id | sys.id | +| spaceId | sys.spaceId | +| createdAt | sys.firstPublishedAt | +| updatedAt | sys.publishedAt | +| revision | sys.publishedVersion | +| | sys.environmentId | This is **new** | +| sys.contentType | sys.contentType | This is now a real reference to the Content Type node. If you used this field, you will probably need to change the`__typename` subfield to `name` | + +#### Sorting by `sys` + +```graphql +allContentfulPage(sort: { fields: contentful_id }) { ... } +``` + +becomes + +```graphql +allContentfulContentTypePage(sort: { fields: sys___id }) { ... } +``` + +#### Filtering by `sys` + +```graphql +contentfulPage( + contentful_id: { + eq: "38akBjGb3T1t4AjB87wQjo" + } +) { ... } +``` + +becomes + +```graphql +contentfulContentTypePage( + sys: { + id: { + eq: "38akBjGb3T1t4AjB87wQjo" + } + } +) { ... } +``` + +### 4. Fields + +#### Text fields + +- The raw field value is now stored in `FIELD_NAME { raw }` instead of `FIELD_NAME { FIELD_NAME }` +- This helps for consistency on your side, while reducing node type bload in the Gatsby backend + +#### JSON fields + +1. You can no more queries sub-fields, which ensures your queries won't break on changed content. This means you now have to check if the results have a value on your own. + - You can achieve this with a [custom Field Editor](https://www.contentful.com/blog/apps-and-open-source-guide-to-making-contentful-truly-yours/) in Contentful. +2. Spaces within keys of JSON fields are now kept, means your data is accessible in you code the same way it is entered in contentful. + +```jsx +

    Born at: {actor.Born_At}

    +``` + +becomes + +```jsx +

    Born at: {actor["Born At"]}

    +``` + +#### Rich Text field + +The schema for Rich Text fields in `gatsby-source-contentful` v9 has been updated to align with Contentful's GraphQL API. This change enhances the flexibility and consistency of querying Rich Text content. You will need to adjust your GraphQL queries and rendering code accordingly. + +Key Changes: + +1. **Unified Link Subfield**: You can now query all linked Contentful content types within the `links` subfield of Rich Text, irrespective of whether they are directly linked in the current content or not. +2. **Direct Entry ID Querying**: Instead of navigating through various fields, the entry ID can now be queried directly as a child field of `links`. This simplification makes your queries more concise. + +##### Schema Comparison + +**Old Schema:** + +```graphql +type ContentfulRichTextRichText { + raw: String + references: [ContentfulAssetContentfulContentReferenceContentfulLocationContentfulTextUnion] + @link(by: "id", from: "references___NODE") +} +union ContentfulAssetContentfulContentReferenceContentfulLocationContentfulTextUnion = + ContentfulAsset + | ContentfulContentReference + | ContentfulLocation + | ContentfulText +``` + +**New Schema:** + +```graphql +type ContentfulNodeTypeRichText @dontInfer { + json: JSON + links: ContentfulNodeTypeRichTextLinks +} + +type ContentfulNodeTypeRichTextLinks { + assets: ContentfulNodeTypeRichTextAssets + entries: ContentfulNodeTypeRichTextEntries +} + +type ContentfulNodeTypeRichTextAssets { + block: [ContentfulAsset]! + hyperlink: [ContentfulAsset]! +} + +type ContentfulNodeTypeRichTextEntries { + inline: [ContentfulEntry]! + block: [ContentfulEntry]! + hyperlink: [ContentfulEntry]! +} +``` + +##### Query Updates + +Update your Rich Text queries to reflect the new schema: + +**Old Example Query:** + +```graphql +query pageQuery { + allContentfulRichText { + nodes { + richText { + raw + references { + ... on ContentfulAsset { + url + } + ... on ContentfulText { + id + } + } + } + } + } +} +``` + +**New Example Query:** + +```graphql +query pageQuery { + allContentfulContentTypeRichText { + nodes { + richText { + json + links { + assets { + block { + url + } + } + entries { + block { + ... on ContentfulContentTypeText { + id + } + } + } + } + } + } + } +} +``` + +**Notes**: + +- The `raw` field is now `json`. +- The `references` field is replaced by `links`, providing a more structured and granular approach to handling linked content. + +##### Rendering changes for Rich Text + +Instead of passing your option object into `renderRichText()` you now pass a option factory. This allows you to access the data you queried for all the linked entities (`assetBlockMap`, `entryBlockMap`, `entryInlineMap`). + +**Old rendering logic:** + +```tsx +import { renderRichText } from "gatsby-source-contentful/rich-text" +import { Options } from "@contentful/rich-text-react-renderer" +import { BLOCKS, MARKS } from "@contentful/rich-text-types" + +const options: Options = { + renderNode: { + [BLOCKS.EMBEDDED_ASSET]: node => { + const image = getImage(node.data.target) + return image ? ( + + ) : null + }, + }, +} + +;
    {renderRichText(richText, options)}
    +``` + +**New rendering logic:** + +```tsx +import { renderRichText, MakeOptions } from "gatsby-source-contentful" +import { BLOCKS, MARKS } from "@contentful/rich-text-types" + +const makeOptions: MakeOptions = ({ + assetBlockMap, + assetHyperlinkMap, + entryBlockMap, + entryInlineMap, + entryHyperlinkMap, +}) => ({ + renderNode: { + [BLOCKS.EMBEDDED_ASSET]: node => { + const image = assetBlockMap.get(node.data.target.sys.id) + return image && image.gatsbyImageData ? ( + + ) : null + }, + }, +}) + +;
    {renderRichText(richText, makeOptions)}
    +``` + +Find more details abour [rendering Rich Text in our README file section](./README.md#leveraging-contentful-rich-text-with-gatsby). + +### 5. Assets + +Assets got simplified a lot and aligned to the [Contentful GraphQL API](https://www.contentful.com/developers/docs/references/graphql/#/reference/schema-generation/assets). + +#### Old GraphlQL schema for assets + +```graphql +type ContentfulAsset implements ContentfulInternalReference & Node @dontInfer { + file: ContentfulAssetFile + title: String + description: String + sys: ContentfulInternalSys +} + +type ContentfulAssetFile { + url: String + details: ContentfulAssetFileDetails + fileName: String + contentType: String +} + +type ContentfulAssetFileDetails { + size: Int + image: ContentfulAssetFileDetailsImage +} + +type ContentfulAssetFileDetailsImage { + width: Int + height: Int +} +``` + +#### New GraphlQL schema for assets + +```graphql +type ContentfulAsset implements ContentfulInternalReference & Node @dontInfer { + sys: ContentfulInternalSys + title: String + description: String + mimeType: String + fileName: String + url: String + size: Int + width: Int + height: Int +} +``` + +### 6. Using the Contentful Preview API (CPA) + +In version 9, fields marked as required in Contentful are automatically treated as non-nullable in Gatsby's GraphQL schema. This aligns the GraphQL schema more closely with your Contentful content model, enhancing type safety and predictability in your Gatsby project. + +However, when using the Contentful Preview API (CPA), you might encounter scenarios where unpublished content doesn't yet fulfill all required fields. To accommodate this, `gatsby-source-contentful` introduces the `enforceRequiredFields` configuration option. + +- **Configuration**: By default, `enforceRequiredFields` is `true`, enforcing the non-nullability of required fields. To override this behavior, particularly in development or preview environments, set `enforceRequiredFields` to `false`: + +```javascript +// In your gatsby-config.js +{ + resolve: `gatsby-source-contentful`, + options: { + spaceId: process.env.CONTENTFUL_SPACE_ID, + accessToken: process.env.CONTENTFUL_ACCESS_TOKEN, + enforceRequiredFields: process.env.NODE_ENV !== 'production', // Example condition + }, +} +``` + +- **Environment Variables**: You can control this setting through environment variables, enabling non-nullable enforcement in production while disabling it in development or preview environments where you might be working with incomplete content. + +- **Impact on TypeScript**: For projects using TypeScript, changing the `enforceRequiredFields` setting will alter the generated types. With `enforceRequiredFields` set to `false`, fields that are required in Contentful but may be missing in the preview content will be nullable in the GraphQL schema. As a result, TypeScript users should ensure their code can handle potentially null values in these fields. + +## Conclusion and Support + +We understand that the changes introduced in `gatsby-source-contentful` v9 are significant. These updates were necessary to resolve architectural issues from the early stages of the plugin and to align it more closely with the evolving capabilities of Contentful and Gatsby. + +As we step into this new era, our focus is on ensuring that you experience the most stable, efficient, and future-proof integration when using Contentful with Gatsby. While we've made every effort to simplify the migration process, we also recognize that transitions like these can be challenging. + +If you encounter any difficulties or have questions, we encourage you to reach out for support: + +- For detailed discussions and sharing experiences: [Join our GitHub Discussion](https://github.com/gatsbyjs/gatsby/discussions/38585). +- To report bugs or specific issues: [Submit an Issue on GitHub](https://github.com/gatsbyjs/gatsby/issues). diff --git a/packages/gatsby-source-contentful/README.md b/packages/gatsby-source-contentful/README.md index 819ff78ad3467..eaaaa34557445 100644 --- a/packages/gatsby-source-contentful/README.md +++ b/packages/gatsby-source-contentful/README.md @@ -1,209 +1,416 @@ -# gatsby-source-contentful +# gatsby-source-contentful v9 + +`gatsby-source-contentful` is a powerful Gatsby plugin that brings [Contentful's rich content management capabilities](https://www.contentful.com/) into the Gatsby ecosystem. It enables developers to seamlessly integrate Contentful's Content Delivery API with their Gatsby sites, allowing for efficient content retrieval and dynamic site generation based on Contentful's structured content.
    Table of contents -- [gatsby-source-contentful](#gatsby-source-contentful) - - [Install](#install) - - [Setup Instructions](#setup-instructions) - - [How to use](#how-to-use) - - [Restrictions and limitations](#restrictions-and-limitations) +- [gatsby-source-contentful v9](#gatsby-source-contentful-v9) + - [Core Features](#core-features) + - [What's New in Version v9](#whats-new-in-version-v9) + - [Restrictions and Limitations](#restrictions-and-limitations) + - [Installation](#installation) + - [Migration](#migration) + - [Configuration Instructions](#configuration-instructions) - [Using Delivery API](#using-delivery-api) - [Using Preview API](#using-preview-api) - - [Offline](#offline) - - [Configuration options](#configuration-options) - - [How to query for nodes](#how-to-query-for-nodes) - - [Query for all nodes](#query-for-all-nodes) - - [Query for a single node](#query-for-a-single-node) - - [A note about LongText fields](#a-note-about-longtext-fields) - - [Duplicated entries](#duplicated-entries) - - [Query for Assets in ContentType nodes](#query-for-assets-in-contenttype-nodes) - - [More on Queries with Contentful and Gatsby](#more-on-queries-with-contentful-and-gatsby) - - [Displaying responsive image with gatsby-plugin-image](#displaying-responsive-image-with-gatsby-plugin-image) - - [Building images on the fly via `useContentfulImage`](#building-images-on-the-fly-via-usecontentfulimage) - - [On-the-fly image options:](#on-the-fly-image-options) - - [Contentful Tags](#contentful-tags) - - [List available tags](#list-available-tags) - - [Filter content by tags](#filter-content-by-tags) - - [Contentful Rich Text](#contentful-rich-text) - - [Query Rich Text content and references](#query-rich-text-content-and-references) - - [Rendering](#rendering) - - [Embedding an image in a Rich Text field](#embedding-an-image-in-a-rich-text-field) - - [Download assets for static distribution](#download-assets-for-static-distribution) - - [Enable the feature with the `downloadLocal: true` option.](#enable-the-feature-with-the-downloadlocal-true-option) - - [Updating Queries for downloadLocal](#updating-queries-for-downloadlocal) - - [Sourcing From Multiple Contentful Spaces](#sourcing-from-multiple-contentful-spaces) + - [Get Started Guide](#get-started-guide) + - [Step 0: Our Contentful Data Model](#step-0-our-contentful-data-model) + - [Step 1: Creating a Gatsby Page with Contentful Data](#step-1-creating-a-gatsby-page-with-contentful-data) + - [Step 2: Dynamic Page Creation with `gatsby-node.js`](#step-2-dynamic-page-creation-with-gatsby-nodejs) + - [Step 3: Using Markdown to Render the Content](#step-3-using-markdown-to-render-the-content) + - [Further Tutorials and Documentation](#further-tutorials-and-documentation) + - [Advanced configuration](#advanced-configuration) + - [Offline Mode](#offline-mode) + - [Querying Contentful Data in Gatsby](#querying-contentful-data-in-gatsby) + - [Overview of Querying Nodes](#overview-of-querying-nodes) + - [Querying All Nodes](#querying-all-nodes) + - [Querying Single Nodes](#querying-single-nodes) + - [Handling Long Text Fields](#handling-long-text-fields) + - [Addressing Duplicated Entries Caused by Internationalization](#addressing-duplicated-entries-caused-by-internationalization) + - [Querying Assets and Tags within ContentType Nodes](#querying-assets-and-tags-within-contenttype-nodes) + - [Advanced Queries with Contentful and Gatsby](#advanced-queries-with-contentful-and-gatsby) + - [Working with Images and Contentful](#working-with-images-and-contentful) + - [Displaying Responsive Images with `gatsby-plugin-image`](#displaying-responsive-images-with-gatsby-plugin-image) + - [Building Images on the Fly with `useContentfulImage`](#building-images-on-the-fly-with-usecontentfulimage) + - [Leveraging Contentful Rich Text with Gatsby](#leveraging-contentful-rich-text-with-gatsby) + - [Querying Rich Text Content](#querying-rich-text-content) + - [Basic Rich Text Rendering](#basic-rich-text-rendering) + - [Embedding Images in Rich Text with Gatsby Plugin Image](#embedding-images-in-rich-text-with-gatsby-plugin-image) + - [Embedding Entries in Rich Text](#embedding-entries-in-rich-text) + - [Further Contentful Rich Text resources](#further-contentful-rich-text-resources) + - [Downloading Assets for Static Distribution](#downloading-assets-for-static-distribution) + - [Benefits of `downloadLocal`](#benefits-of-downloadlocal) + - [Trade-offs](#trade-offs) + - [Enabling `downloadLocal`](#enabling-downloadlocal) + - [Updating GraphQL Queries](#updating-graphql-queries) + - [Troubleshooting](#troubleshooting) + - [Multi-Space Sourcing: Example for Sourcing from Multiple Contentful Spaces](#multi-space-sourcing-example-for-sourcing-from-multiple-contentful-spaces) + - [Current Limitations](#current-limitations)
    -## Install +## Core Features -```shell -npm install gatsby-source-contentful gatsby-plugin-image -``` +- **Comprehensive Content Integration**: Easily connect your Contentful space with Gatsby, ensuring all content types, references, and custom fields are handled effectively. +- **Advanced Contentful Features Support**: Harness the full power of Contentful's offerings, including the Image API for creating responsive images and the comprehensive Rich Text feature for versatile content representation. +- **Efficient Content Synchronization**: Utilizes Contentful's Sync API for incremental updates, significantly speeding up build times. +- **Aligned Data Structure**: Data in Gatsby closely mirrors the structure of Contentful's GraphQL API, with minimal discrepancies, for a consistent development experience. +- **Preview API Support**: Leverage Contentful's Preview API for content previews before publishing, with some limitations due to incremental sync constraints. Refer to [Restrictions & Limitations](#restrictions-and-limitations) for more details. -## Setup Instructions +### What's New in Version v9 -To get setup quickly with a new site and have Netlify do the heavy lifting, [deploy a new Gatsby Contentful site with just a few clicks on netlify.com](https://app.netlify.com/start/deploy?repository=https://github.com/contentful/starter-gatsby-blog). +The v9 release of `gatsby-source-contentful` brings significant improvements, focusing on stability and enhancing the developer experience. -## How to use +- **Dynamic Schema Generation**: Schema types are now dynamically generated based on the Contentful content model, eliminating site breakages due to empty content fields. +- **Rich Text and JSON Field Enhancements**: Improved handling of Rich Text and JSON fields for more accurate processing, querying, and rendering. +- **Schema and Performance Optimizations**: An optimized, leaner schema reduces node count and improves build times, especially for larger projects. +- **Contentful GraphQL API Alignment**: Enhanced alignment with Contentful's GraphQL API enables more intuitive and consistent querying. +- **Removed Content Type Name Restrictions**: New naming patterns now allow all content type names, including previously restricted names like `entity` and `reference`. +- **Updated Field Name Restrictions**: Fields can now be named `contentful_id`, with restrictions now applied to names like `sys`, `contentfulMetadata`, and `linkedFrom`. +- **Refined Backlinks**: Backlinks/references are now located in the `linkedFrom` field, aligning with Contentful's GraphQL API structure. +- **Expanded Configuration Options**: Additional [configuration options](#advanced-configuration) provide greater control and customization for your specific project needs. +- **Updated @link Directive Usage**: The new version of the "gatsby-source-contentful" plugin adopts the @link directive, eliminating the warnings in the build log about the deprecated \_\_\_NODE convention in Gatsby v5. -First, you need a way to pass environment variables to the build process, so secrets and other secured data aren't committed to source control. We recommend using [`dotenv`][dotenv] which will then expose environment variables. [Read more about `dotenv` and using environment variables here][envvars]. Then we can _use_ these environment variables and configure our plugin. +For a detailed migration guide and to leverage these improvements, refer to the [Migration Guide](./MIGRATION.md) section. -## Restrictions and limitations +## Restrictions and Limitations -This plugin has several limitations, please be aware of these: +Please note the following limitations when using `gatsby-source-contentful`: -1. At the moment, fields that do not have at least one populated instance will not be created in the GraphQL schema. This can break your site when field values get removed. You may workaround with an extra content entry with all fields filled out. +1. **Environment Access**: Your access token must have permissions for both your desired environment and the `master` environment. +2. **Preview API Usage**: While using Contentful's Preview API, content may become out-of-sync over time as incremental syncing is not officially supported. Regular cache clearing is recommended. +3. **Restricted Content Type Names**: Content type names such as `entity` and `reference` are not allowed. +4. **Field Name Prefixes**: Certain field names are restricted and will be automatically prefixed for consistency, including `id`, `children`, `parent`, `fields`, `internal`, `sys`, `contentfulMetadata`, and `linkedFrom`. -2. When using reference fields, be aware that this source plugin will automatically create the reverse reference. You do not need to create references on both content types. +Your revisions to the installation section are clear and informative, effectively guiding users through the initial setup process. The emphasis on `gatsby-plugin-image` for leveraging Gatsby and Contentful's image capabilities is a valuable recommendation. Here's a slightly refined version for clarity and flow: -3. When working with environments, your access token has to have access to your desired environment and the `master` environment. +## Installation + +To install the plugin, run the following command in your Gatsby project: + +```shell +npm install gatsby-source-contentful gatsby-plugin-image +``` -4. Using the preview functionality might result in broken content over time, as syncing data on preview is not officially supported by Contentful. Make sure to regularly clean your cache when using Contentful's preview API. +While `gatsby-plugin-image` is optional, it is highly recommended to fully utilize the benefits of [Gatsby's Image Plugin](https://www.gatsbyjs.com/plugins/gatsby-plugin-image/) and [Contentful's Images API](https://www.contentful.com/developers/docs/references/images-api/), enhancing your site's image handling and performance. -5. The following content type names are not allowed: `entity`, `reference` +## Migration -6. The following field names are restricted and will be prefixed: `children`, `contentful_id`, `fields`, `id`, `internal`, `parent`, +Please see our [Migration Guide](./MIGRATION.md) -7. The Plugin has a dependency on `gatsby-plugin-image` which itself has dependencies. Check [Displaying responsive image with gatsby-plugin-image](#displaying-responsive-image-with-gatsby-plugin-image) to determine which additional plugins you'll need to install. +## Configuration Instructions ### Using Delivery API +To use Contentful's Delivery API, which retrieves published content, follow these steps: + +1. **Set Environment Variables**: Create a file named `.env.development` in your project's root with the following variables: + +```sh +# .env.development +CONTENTFUL_SPACE_ID=your_space_id +CONTENTFUL_ACCESS_TOKEN=your_delivery_api_access_token +CONTENTFUL_ENVIRONMENT=master # or your custom environment +``` + +Replace `your_space_id` and `your_delivery_api_access_token` with your actual Contentful Space ID and Delivery API access token. To create access tokens, open the Contentful [UI](https://app.contentful.com/), click `Settings` in the top right corner, and select `API keys`. 2. **Production Environment Variables**: Duplicate the `.env.development` file and rename it to `.env.production`. This ensures your Gatsby site connects to Contentful's Delivery API in both development and production builds. + +3. **Plugin Configuration**: In your `gatsby-config.js`, add the `gatsby-source-contentful` plugin with the necessary options: + ```javascript -// In your gatsby-config.js +// gatsby-config.js +require("dotenv").config({ + path: `.env.${process.env.NODE_ENV}`, +}) + module.exports = { plugins: [ { resolve: `gatsby-source-contentful`, options: { - spaceId: `your_space_id`, - // Learn about environment variables: https://gatsby.dev/env-vars + spaceId: process.env.CONTENTFUL_SPACE_ID, accessToken: process.env.CONTENTFUL_ACCESS_TOKEN, + environment: process.env.CONTENTFUL_ENVIRONMENT, }, }, `gatsby-plugin-image`, + // ... other plugins ... ], } ``` ### Using Preview API +To configure the plugin for Contentful's Preview API, which allows you to see unpublished content. This will configure the plugin for Contentful's Preview API for development mode, while maintaining the Contentful Content API for production builds. + +1. **Update Environment Variables**: In your `.env.development` file, update `CONTENTFUL_ACCESS_TOKEN` to use an access token for the Contentful Preview API. + +2. **Adjust Plugin Configuration**: Modify your `gatsby-config.js` to conditionally set the host based on the environment: + ```javascript -// In your gatsby-config.js +// gatsby-config.js module.exports = { plugins: [ { resolve: `gatsby-source-contentful`, options: { - spaceId: `your_space_id`, - // Learn about environment variables: https://gatsby.dev/env-vars - accessToken: process.env.CONTENTFUL_ACCESS_TOKEN, - host: `preview.contentful.com`, + // ... other plugin options ... + host: + process.env.NODE_ENV === `production` + ? `cdn.contentful.com` + : `preview.contentful.com`, }, }, - `gatsby-plugin-image`, + // ... other plugins ... ], } ``` -### Offline - -If you don't have internet connection you can add `export GATSBY_CONTENTFUL_OFFLINE=true` to tell the plugin to fallback to the cached data, if there is any. - -### Configuration options - -**`spaceId`** [string][required] - -Contentful space ID - -**`accessToken`** [string][required] - -Contentful delivery API key, when using the Preview API use your Preview API key - -**`host`** [string][optional] [default: `'cdn.contentful.com'`] - -The base host for all the API requests, by default it's `'cdn.contentful.com'`, if you want to use the Preview API set it to `'preview.contentful.com'`. You can use your own host for debugging/testing purposes as long as you respect the same Contentful JSON structure. - -**`environment`** [string][optional] [default: 'master'] - -The environment to pull the content from, for more info on environments check out this [Guide](https://www.contentful.com/developers/docs/concepts/multiple-environments/). - -**`downloadLocal`** [boolean][optional] [default: `false`] - -Downloads and caches `ContentfulAsset`'s to the local filesystem. Allows you to query a `ContentfulAsset`'s `localFile` field, which is not linked to Contentful's CDN. See [Download assets for static distribution](#download-assets-for-static-distribution) for more information on when and how to use this feature. - -**`localeFilter`** [function][optional] [default: `() => true`] - -Possibility to limit how many locales/nodes are created in GraphQL. This can limit the memory usage by reducing the amount of nodes created. Useful if you have a large space in Contentful and only want to get the data from one selected locale. - -For example, to filter locales on only germany `localeFilter: locale => locale.code === 'de-DE'` - -List of locales and their codes can be found in Contentful app -> Settings -> Locales - -**`proxy`** [object][optional] [default: `undefined`] - -Axios proxy configuration. See the [axios request config documentation](https://github.com/mzabriskie/axios#request-config) for further information about the supported values. - -**`useNameForId`** [boolean][optional] [default: `true`] - -Use the content's `name` when generating the GraphQL schema e.g. a content type called `[Component] Navigation bar` will be named `contentfulComponentNavigationBar`. - -When set to `false`, the content's internal ID will be used instead e.g. a content type with the ID `navigationBar` will be called `contentfulNavigationBar`. - -Using the ID is a much more stable property to work with as it will change less often. However, in some scenarios, content types' IDs will be auto-generated (e.g. when creating a new content type without specifying an ID) which means the name in the GraphQL schema will be something like `contentfulC6XwpTaSiiI2Ak2Ww0oi6qa`. This won't change and will still function perfectly as a valid field name but it is obviously pretty ugly to work with. - -If you are confident your content types will have natural-language IDs (e.g. `blogPost`), then you should set this option to `false`. If you are unable to ensure this, then you should leave this option set to `true` (the default). - -**`pageLimit`** [number][optional] [default: `1000`] - -Number of entries to retrieve from Contentful at a time. Due to some technical limitations, the response payload should not be greater than 7MB when pulling content from Contentful. If you encounter this issue you can set this param to a lower number than 100, e.g `50`. - -**`assetDownloadWorkers`** [number][optional] [default: `50`] - -Number of workers to use when downloading Contentful assets. Due to technical limitations, opening too many concurrent requests can cause stalled downloads. If you encounter this issue you can set this param to a lower number than 50, e.g 25. - -**`contentfulClientConfig`** [object][optional] [default: `{}`] - -Additional config which will get passed to [Contentfuls JS SDK](https://github.com/contentful/contentful.js#configuration). - -Use this with caution, you might override values this plugin does set for you to connect to Contentful. +## Get Started Guide -**`enableTags`** [boolean][optional] [default: `false`] +This guide is a straightforward path to see how Gatsby and Contentful can bring your content to life, ideal for those just starting out. + +### Step 0: Our Contentful Data Model + +Before we jump into Gatsby, let's define our Contentful data model. We're working with a Page content type, which includes the following fields: + +- **Title**: The headline of your page. +- **Slug**: A unique URL segment based on the page's title. +- **Content**: The primary content or body of your page. + +### Step 1: Creating a Gatsby Page with Contentful Data + +Begin by fetching data from a single entry of our `Page` content type and displaying it on a Gatsby page. + +1. **Create a Page Component**: In your `src/pages` directory, add a new file, such as `contentful-page.js`. +2. **Load data with GraphQL**: Use Gatsby's page query to retrieve data from Contentful. For a `Page` content type with `title`, `slug`, and `content` fields. +3. **Render data with Gatsby**: The result of the page query is available through the `data` prop. + + Your file should look like this: + + ```jsx + // src/pages/contentful-page.js + import React from "react" + import { graphql } from "gatsby" + + export default function ContentfulPage({ data }) { + return ( +
    +

    {data.contentfulPage.title}

    +

    {data.contentfulPage.content.raw}

    +
    + ) + } + + export const pageQuery = graphql` + query { + contentfulPage(slug: { eq: "your-slug" }) { + title + content { + raw + } + } + } + ` + ``` + + **Important:** Replace `"your-slug"` with the actual slug of the page you want to display. At this stage, the content will be displayed as raw text. + +### Step 2: Dynamic Page Creation with `gatsby-node.js` + +Now, create a subpage for each `Page` content type entry. + +1. **Rename and Move Page Component**: Rename `src/pages/contentful-page.js` to `src/templates/contentful-page-template.js`. This file becomes your template for dynamically created pages. + +2. **Implement Dynamic Page Creation**: In your `gatsby-node.js`, use the `createPages` API to dynamically generate pages for each entry of the `Page` content type from Contentful. + + ```javascript + // gatsby-node.js + const path = require("path") + + exports.createPages = async ({ graphql, actions }) => { + const { createPage } = actions + + // Query data of all Contentful pages + const result = await graphql(` + query { + allContentfulPage { + edges { + node { + slug + } + } + } + } + `) + + // Dynamically create a page for each Contentful entry, passing the slug for URL generation + result.data.allContentfulPage.edges.forEach(({ node }) => { + createPage({ + path: node.slug, + component: path.resolve(`./src/templates/contentful-page-template.js`), + context: { + slug: node.slug, + }, + }) + }) + } + ``` + + This code will create a new page for each `Page` entry in Contentful, using the slug as the page path. + +3. **Update Template with Slug Filter**: In your template file (`src/templates/contentful-page-template.js`), update the GraphQL query to filter by the provided slug. + + ```jsx + // src/templates/contentful-page-template.js + import React from "react" + import { graphql } from "gatsby" + + export default function ContentfulPageTemplate({ data }) { + return ( +
    +

    {data.contentfulPage.title}

    +

    {data.contentfulPage.content.raw}

    +
    + ) + } + + export const pageQuery = graphql` + query contentfulPageQuery($slug: String!) { + contentfulPage(slug: { eq: $slug }) { + title + content { + raw + } + } + } + ` + ``` + + **Hint:** The `$slug` variable is provided via the context in `gatsby-node.js`. + +### Step 3: Using Markdown to Render the Content + +> **Note on Markdown**: Markdown is a lightweight markup language that allows you to write content using an easy-to-read, easy-to-write plain text format, which is then converted into HTML. The `gatsby-transformer-remark` plugin enables this transformation within Gatsby. [Learn more about Markdown](https://www.markdownguide.org/getting-started/). + +To add Markdown rendering to your Contentful content: + +1. **Install `gatsby-transformer-remark`**: Run `npm install gatsby-transformer-remark`. + +2. **Update Gatsby Config**: Add the `gatsby-transformer-remark` plugin to your `gatsby-config.js`. + + ```javascript + // gatsby-config.js + module.exports = { + plugins: [ + // ... other plugins ... + `gatsby-transformer-remark`, + ], + } + ``` + +3. **Adjust the Template for Markdown**: Modify your template (`src/templates/contentful-page-template.js`) to render the markdown content. Update the GraphQL query and rendering logic to handle the transformed markdown. + + ```jsx + // src/templates/contentful-page-template.js + import React from "react" + import { graphql } from "gatsby" + + export default function ContentfulPageTemplate({ data }) { + return ( +
    +

    {data.contentfulPage.title}

    + {/* Here we render the generated HTML */} +
    +
    + ) + } + + export const pageQuery = graphql` + query contentfulPageQuery($slug: String!) { + contentfulPage(slug: { eq: $slug }) { + title + content { + # The childMarkdownRemark is created by gatsby-transformer-remark + childMarkdownRemark { + html + } + } + } + } + ` + ``` + +You now have a foundational grasp of `gatsby-source-contentful` and its potential to power your Gatsby projects with Contentful's dynamic content. + +### Further Tutorials and Documentation + +For more in-depth exploration and advanced concepts, check out these valuable resources: + +- **Contentful Gatsby Starter Guide**: A comprehensive guide to kickstart your Gatsby projects with Contentful. [Explore the guide](https://www.contentful.com/gatsby-starter-guide/). +- **Contentful Blog Example**: An example Gatsby project integrated with Contentful, ideal for blog-like websites. [View on GitHub](https://github.com/contentful/starter-gatsby-blog). +- **Contentful & Gatsby in 5 Minutes**: A quick tutorial for setting up a Gatsby site with Contentful. [Start learning](https://www.contentful.com/help/gatsbyjs-and-contentful-in-five-minutes/). +- **Gatsby's Quick Guide to Contentful**: Gatsby's own guide to integrating Contentful for content management. [Read the guide](https://www.gatsbyjs.com/blog/a-quick-start-guide-to-gatsby-and-contentful/). +- **Video Tutorial by Khaled and Jason**: Though a bit dated, this informative video remains a great resource for understanding Gatsby and Contentful integration. [Watch on YouTube](https://www.youtube.com/watch?v=T9hLWjIN-pY). + +## Advanced configuration + +Here's the revised table for the `gatsby-source-contentful` configuration options, including the suggested updates and corrections: + +| Option | Type | Default Value | Description | +| ---------------------------------- | ------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `spaceId` | string | (required) | Your Contentful space ID. | +| `accessToken` | string | (required) | Access token for the Content Delivery API. Use the Preview API key for the Contentful Preview API. | +| `host` | string | `cdn.contentful.com` | Base host for API requests. Default is for the Delivery API; use `preview.contentful.com` for the Preview API. Custom hosts can be used for debugging/testing. | +| `environment` | string | `master` | The environment in Contentful to pull content from. [Guide](https://www.contentful.com/developers/docs/concepts/multiple-environments/) | +| `downloadLocal` | boolean | `false` | Downloads and caches `ContentfulAsset` to the local filesystem. Use `localFile` to access the local files. See [Download assets for static distribution](#download-assets-for-static-distribution) | +| `useNameForId` | boolean | `true` | Determines whether the content's `name` or internal ID is used for generating GraphQL schema types. Using `name` can make type names more readable but can be unstable if names change. Using the internal ID ensures stability as IDs are less likely to change, but may result in less readable types, especially when auto-generated. | +| `enforeRequiredFields` (New in v9) | boolean | `true` | Fields required in Contentful will also be required in Gatsby. If you are using Contentfuls Preview API (CPA), you may want to disable this conditionally. | + +| `enableMarkdownDetection` (New in v9) | boolean | `true` | Assumes all long text fields in Contentful are markdown fields. Requires `gatsby-transformer-remark`. Can be a performance issue in large projects. Set to `false` and use `markdownFields` to specify markdown fields. | +| `markdownFields` (New in v9) | array | `[]` | Specify which fields contain markdown content. Effective only when `enableMarkdownDetection` is `false`. Format: array of pairs (content type ID and array of field IDs). Example: `[["product", ["description", "summary"]], ["otherContentTypeId", ["someMarkdownFieldId"]]]` | +| `contentTypePrefix` (Renamed in v9) | string | `ContentfulContentType` | Prefix for the generated GraphQL types. Formerly known as `typePrefix`. | +| `localeFilter` | function | `() => true` | Function to filter which locales/nodes are created in GraphQL, reducing memory usage by limiting nodes. | +| `contentTypeFilter` | function | `() => true` | Function to filter which contentType/nodes are created in GraphQL, reducing memory usage by limiting nodes. | +| `pageLimit` | number | `1000` | Number of entries to retrieve from Contentful at a time. Adjust if the payload size exceeds 7MB. | +| `assetDownloadWorkers` | number | `50` | Number of workers to use when downloading Contentful assets. Adjust to prevent stalled downloads due to too many concurrent requests. | +| `proxy` | object | (none) | Axios proxy configuration. See the [axios request config documentation](https://github.com/mzabriskie/axios#request-config) for further information about the supported values. | +| `contentfulClientConfig` | object | `{}` | Additional config passed to Contentful's JS SDK. Use with caution to avoid overriding plugin-set values. | -Enable the new [tags feature](https://www.contentful.com/blog/2021/04/08/governance-tagging-metadata/). This will disallow the content type name `tags` till the next major version of this plugin. +### Offline Mode -Learn how to use them at the [Contentful Tags](#contentful-tags) section. +For development without internet access, you can enable the Offline Mode of `gatsby-source-contentful`. Simply set the environment variable `GATSBY_CONTENTFUL_OFFLINE`, for example by adding `GATSBY_CONTENTFUL_OFFLINE=true` to your `.env.development`. This mode uses cached data from previous builds. -**`contentTypeFilter`** [function][optional] [default: () => true] +**Note:** Changing `gatsby-config.js` or `package.json` will clear the cache, requiring internet access for the next build. -Possibility to limit how many contentType/nodes are created in GraphQL. This can limit the memory usage by reducing the amount of nodes created. Useful if you have a large space in Contentful and only want to get the data from certain content types. +## Querying Contentful Data in Gatsby -For example, to exclude content types starting with "page" `contentTypeFilter: contentType => !contentType.sys.id.startsWith('page')` +### Overview of Querying Nodes -**`typePrefix`** [string][optional] [default: `Contentful`] +In Gatsby, Contentful provides three primary node types for querying: `Asset`, `ContentType`, and `Tag`. These nodes allow you to access various types of content from your Contentful space: -Prefix for the type names created in GraphQL. This can be used to avoid conflicts with other plugins, or if you want more than one instance of this plugin in your project. For example, if you set this to `Blog`, the type names will be `BlogAsset` and `allBlogAsset`. - -## How to query for nodes - -Two standard node types are available from Contentful: `Asset` and `ContentType`. - -`Asset` nodes will be created in your site's GraphQL schema under `contentfulAsset` and `allContentfulAsset`. - -`ContentType` nodes are a little different - their exact name depends on what you called them in your Contentful data models. The nodes will be created in your site's GraphQL schema under `contentful${entryTypeName}` and `allContentful${entryTypeName}`. - -In all cases querying for nodes like `contentfulX` will return a single node, and nodes like `allContentfulX` will return all nodes of that type. - -### Query for all nodes - -You might query for **all** of a type of node: +- `Asset` nodes are accessible under `contentfulAsset` and `allContentfulAsset`. +- `ContentType` nodes, following the new naming pattern, are available as `contentfulContentType${entryTypeName}` and `allContentfulContentType${entryTypeName}`. +- `Tag` nodes are found under `contentfulTag` and `allContentfulTag`. + +Querying `contentfulX` retrieves a single node, while `allContentfulX` fetches all nodes of that type. + +### Querying All Nodes + +To query all nodes of a specific type: ```graphql { allContentfulAsset { nodes { - contentful_id + sys { + id + } title description } @@ -211,133 +418,91 @@ You might query for **all** of a type of node: } ``` -You might do this in your `gatsby-node.js` using Gatsby's [`createPages`](https://gatsbyjs.com/docs/node-apis/#createPages) Node API. +### Querying Single Nodes -### Query for a single node - -To query for a single `image` asset with the title `'foo'` and a width of 1600px: - -```javascript -export const assetQuery = graphql` - { - contentfulAsset(title: { eq: "foo" }) { - contentful_id - title - description - file { - fileName - url - contentType - details { - size - image { - height - width - } - } - } - } - } -` -``` - -To query for a single `CaseStudy` node with the short text properties `title` and `subtitle`: +For querying a single node: ```graphql - { - contentfulCaseStudy(filter: { title: { eq: 'bar' } }) { - title - subtitle +{ + contentfulAsset(title: { eq: "foo" }) { + sys { + id } + title + description + // ... other fields ... } -``` - -You might query for a **single** node inside a component in your `src/components` folder, using [Gatsby's `StaticQuery` component](https://www.gatsbyjs.com/docs/static-query/). - -#### A note about LongText fields - -On Contentful, a "Long text" field uses Markdown by default. The field is exposed as an object, while the raw Markdown is exposed as a child node. +} -```graphql { - contentfulCaseStudy { - body { - body - } + contentfulContentTypeCaseStudy(slug: { eq: "example-slug" }) { + title + slug + // ... other fields ... } } ``` -Unless the text is Markdown-free, you cannot use the returned value directly. In order to handle the Markdown content, you must use a transformer plugin such as [`gatsby-transformer-remark`](https://www.gatsbyjs.com/plugins/gatsby-transformer-remark/). The transformer will create a `childMarkdownRemark` on the "Long text" field and expose the generated html as a child node: +### Handling Long Text Fields + +Long text fields often use Markdown. For Markdown processing, use `gatsby-transformer-remark`. See [Step 3 in the Getting Started Guide](#step-3-using-markdown-to-render-the-content). ```graphql { - contentfulCaseStudy { + contentfulContentTypeCaseStudy { body { + raw # Raw Markdown childMarkdownRemark { - html + html # HTML from Markdown } } } } ``` -You can then insert the returned HTML inline in your JSX: - -```jsx -
    -``` - -#### Duplicated entries +### Addressing Duplicated Entries Caused by Internationalization -When Contentful pulls the data, all localizations will be pulled. Therefore, if you have a localization active, it will duplicate the entries. Narrow the search by filtering the query with `node_locale` filter: +Due to Contentful's data retrieval process, each localization creates a new node, potentially leading to duplicates. Include the `node_locale` in queries to avoid this: ```graphql { - allContentfulCaseStudy(filter: { node_locale: { eq: "en-US" } }) { - edges { - node { - id - slug - title - subtitle - body { - body - } - } - } + allContentfulContentTypeCaseStudy( + filter: { node_locale: { eq: "en-US" } } + ) { + # ... query fields ... } } ``` -### Query for Assets in ContentType nodes - -More typically your `Asset` nodes will be mixed inside of your `ContentType` nodes, so you'll query them together. All the same formatting rules for `Asset` and `ContentType` nodes apply. +### Querying Assets and Tags within ContentType Nodes -To get **all** the `CaseStudy` nodes with `ShortText` fields `id`, `slug`, `title`, `subtitle`, `LongText` field `body` and `heroImage` asset field, we use `allContentful${entryTypeName}` to return all instances of that `ContentType`: +Assets and tags are typically queried alongside ContentType nodes: ```graphql { - allContentfulCaseStudy { + allContentfulContentTypeCaseStudy { edges { node { - id + sys { + id + } slug title subtitle body { body } + # Asset heroImage { title description gatsbyImageData(layout: CONSTRAINED) - # Further below in this doc you can learn how to use these response images + } + # Tags + contentfulMetadata { + tags { + name + } } } } @@ -345,163 +510,156 @@ To get **all** the `CaseStudy` nodes with `ShortText` fields `id`, `slug`, `titl } ``` -### More on Queries with Contentful and Gatsby +### Advanced Queries with Contentful and Gatsby -It is strongly recommended that you take a look at how data flows in a real Contentful and Gatsby application to fully understand how the queries, Node.js functions and React components all come together. Check out the example site at -[using-contentful.gatsbyjs.org](https://using-contentful.gatsbyjs.org/). +Explore your GraphQL schema and data with GraphiQL, a browser-based IDE available at http://localhost:8000/\_\_\_graphql during Gatsby development. This tool is essential for experimenting with queries and understanding the Contentful data structure in your project. -## Displaying responsive image with gatsby-plugin-image +## Working with Images and Contentful -To use it: +Contentful and Gatsby together provide a powerful combination for managing and displaying images. Leveraging Contentful's Images API and Gatsby's image handling capabilities, you can create responsive, optimized images for your site. Here's how to get the most out of this integration: -1. Install the required plugins: +Please check out the [Readme](https://www.gatsbyjs.com/plugins/gatsby-plugin-image/) and [Reference Guide](https://www.gatsbyjs.com/docs/reference/built-in-components/gatsby-plugin-image/) of `gatsby-plugin-image` for more detailed information about the possibilities of it. -```shell -npm install gatsby-plugin-image gatsby-plugin-sharp -``` +### Displaying Responsive Images with `gatsby-plugin-image` -2. Add the plugins to your `gatsby-config.js`: +1. **Install Required Plugins**: If you haven't already, install `gatsby-plugin-image`: -```javascript -module.exports = { - plugins: [ - `gatsby-plugin-sharp`, - `gatsby-plugin-image`, - // ...etc - ], -} -``` + ```shell + npm install gatsby-plugin-image + ``` -3. You can then query for image data using the new `gatsbyImageData` resolver: +2. **Configure Gatsby**: If you haven't already, add it to your `gatsby-config.js`: -```graphql -{ - allContentfulBlogPost { - nodes { - heroImage { - gatsbyImageData(layout: FULL_WIDTH) - } + ```javascript + module.exports = { + plugins: [ + `gatsby-plugin-image`, + // ... other plugins ... + ], } - } -} -``` + ``` -4. Your query will return a dynamic image. Check the [documentation of gatsby-plugin-image](https://www.gatsbyjs.com/plugins/gatsby-plugin-image/#dynamic-images) to learn how to render it on your website. +3. **Querying Images**: Use the `gatsbyImageData` resolver in your GraphQL queries to fetch image data on any Contentful Asset you have linked in a Contentful Field: -Check the [Reference Guide of gatsby-plugin-image](https://www.gatsbyjs.com/docs/reference/built-in-components/gatsby-plugin-image/) to get a deeper insight on how this works. + ```graphql + query contentfulPageQuery($slug: String!) { + contentfulPage(slug: { eq: $slug }) { + # Your other fields + heroImage { + gatsbyImageData(layout: FULL_WIDTH) + } + } + } + ``` + +4. **Rendering Images**: Use the `` component from `gatsby-plugin-image` to display the images in your components: + + ```jsx + export default function ContentfulPageTemplate({ data }) { + return ( +
    + {/* Render logic of the rest of your content type */} + +
    + ) + } + ``` -### Building images on the fly via `useContentfulImage` + Replace `imageData` with the image data fetched from your GraphQL query. -With `useContentfulImage` and the URL to the image on the Contentful Image API you can create dynamic images on the fly: + Certainly! Here's a rephrased version of the fifth step for using images with `gatsby-plugin-image`, aligned with the style and tone of the other steps: -```js -import { GatsbyImage } from "gatsby-plugin-image" -import * as React from "react" -import { useContentfulImage } from "gatsby-source-contentful/hooks" - -const MyComponent = () => { - const dynamicImage = useContentfulImage({ - image: { - url: "//images.ctfassets.net/k8iqpp6u0ior/3BSI9CgDdAn1JchXmY5IJi/f97a2185b3395591b98008647ad6fd3c/camylla-battani-AoqgGAqrLpU-unsplash.jpg", - width: 2000, - height: 1000, - }, - }) +5. **Optimize Image Delivery Speed**: To enhance the performance of images served from Contentful's Image CDN, consider adding `preconnect` and `dns-prefetch` metatags to your website. These tags help accelerate the process of connecting to the CDN, leading to faster image rendering. - return -} -``` + Implement this optimization using Gatsby's [Head API](https://www.gatsbyjs.com/docs/reference/built-in-components/gatsby-head/) in your global layout component: -#### On-the-fly image options: + ```jsx + export const Head = () => ( + <> + + + + ) + ``` -This hook accepts the same parameters as the `gatsbyImageData` field in your GraphQL queries. They are automatically translated to the proper Contentful Image API parameters. + By incorporating these tags, you ensure a quicker DNS resolution and connection establishment with the Contentful Image CDN. -Here are the most relevant ones: +### Building Images on the Fly with `useContentfulImage` -- width: maximum 4000 -- height: maximum 4000 -- toFormat: defaults to the actual image format -- jpegProgressive: set to `progressive` to enable -- quality: between 1 and 100 -- resizingBehavior: https://www.contentful.com/developers/docs/references/images-api/#/reference/resizing-&-cropping/change-the-resizing-behavior -- cropFocus: https://www.contentful.com/developers/docs/references/images-api/#/reference/resizing-&-cropping/specify-focus-area -- background: background color in format `rgb:9090ff` -- cornerRadius: https://www.contentful.com/developers/docs/references/images-api/#/reference/resizing-&-cropping/crop-rounded-corners-&-circle-elipsis +For more dynamic image handling, `useContentfulImage` allows you to build images on the fly using Contentful's Image API: -## [Contentful Tags](https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/content-tags) +1. **Use the Hook**: In your component, use the `useContentfulImage` hook to create an image dynamically: -You need to set the `enableTags` flag to `true` to use this new feature. + ```jsx + import { useContentfulImage } from "gatsby-source-contentful" -### List available tags + const MyComponent = () => { + const dynamicImage = useContentfulImage({ + image: { + url: "URL_of_the_image_on_Contentful", + width: 800, + height: 600, + }, + }) -This example lists all available tags. The sorting is optional. + return + } + ``` -```graphql -query TagsQuery { - allContentfulTag(sort: { fields: contentful_id }) { - nodes { - name - contentful_id - } - } -} -``` + Replace `URL_of_the_image_on_Contentful` with the actual URL of the image you want to display. The hook accepts the same parameters as the `gatsbyImageData` field in your GraphQL queries. -### Filter content by tags +2. **Customizing Images**: You can customize the images using various options such as `width`, `height`, `format`, `quality`, etc. These options align with [Contentful's Image API parameters](https://www.contentful.com/developers/docs/references/images-api/#/reference). -This filters content entries that are tagged with the `numberInteger` tag. +Can you help me to improve this section of my readme for a new version of the gatsby plugin gatsby-source-contentful? -```graphql -query FilterByTagsQuery { - allContentfulNumber( - sort: { fields: contentful_id } - filter: { - metadata: { - tags: { elemMatch: { contentful_id: { eq: "numberInteger" } } } - } - } - ) { - nodes { - title - integer - } - } -} -``` +## Leveraging Contentful Rich Text with Gatsby + +The integration of Contentful's Rich Text fields in the `gatsby-source-contentful` plugin allows for a dynamic and rich content experience in Gatsby projects. This feature supports a variety of content compositions, including text, assets, and embedded entries, offering a powerful tool for developers and content creators alike. -## [Contentful Rich Text](https://www.contentful.com/developers/docs/concepts/rich-text/) +The latest `gatsby-source-contentful` updates enhance this integration, closely aligning with Contentful's GraphQL API and the `@contentful/rich-text-react-renderer`. -Rich Text feature is supported in this source plugin, you can use the following query to get the JSON output: +### Querying Rich Text Content -**Note:** In our example Content Model the field containing the Rich Text data is called `bodyRichText`. Make sure to use your field name within the Query instead of `bodyRichText` +The Rich Text field in Gatsby reflects the structure provided by Contentful's API. -### Query Rich Text content and references +Example GraphQL query for a Rich Text field: ```graphql -query pageQuery($id: String!) { - contentfulBlogPost(id: { eq: $id }) { - title - slug - # This is the rich text field, the name depends on your field configuration in Contentful - bodyRichText { - raw - references { - ... on ContentfulAsset { - # You'll need to query contentful_id in each reference - contentful_id - __typename - fixed(width: 1600) { - width - height - src - srcSet +# This is a extended example, see below for a step-by-step introduction +{ + allContentfulContentTypePage { + nodes { + content { + json + links { + assets { + block { + sys { + id + } # Required for link resolution + gatsbyImageData(width: 200) + } + # There is also hyperlink {} + } + entries { + block { + # Generic props and fields + __typename + sys { + id + type + } # Required for link resolution + # Content Type specific fields + ... on ContentfulContentTypeSomethingElse { + body + } + } + # Include inline and hyperlink as needed } - } - ... on ContentfulBlogPost { - contentful_id - __typename - title - slug } } } @@ -509,143 +667,215 @@ query pageQuery($id: String!) { } ``` -### Rendering +This query retrieves Rich Text JSON content (`json`) along with linked assets and entries, now organized under `assets` and `entries` within the `links` object. + +### Basic Rich Text Rendering + +To render Rich Text, use `gatsby-source-contentful`'s `renderRichText` function with a custom rendering setup for various embedded entries and assets: ```jsx +// src/templates/contentful-page-template.js +import { renderRichText } from "gatsby-source-contentful" import { BLOCKS, MARKS } from "@contentful/rich-text-types" -import { renderRichText } from "gatsby-source-contentful/rich-text" -const Bold = ({ children }) => {children} -const Text = ({ children }) =>

    {children}

    - -const options = { +// This factory provides a dynamic configuration for each rich text rendering +const makeOptions = ({ assetBlockMap, entryBlockMap, entryInlineMap }) => ({ renderMark: { - [MARKS.BOLD]: text => {text}, + [MARKS.BOLD]: text => {text}, }, renderNode: { - [BLOCKS.PARAGRAPH]: (node, children) => {children}, - [BLOCKS.EMBEDDED_ASSET]: node => { - return ( - <> -

    Embedded Asset

    -
    -            {JSON.stringify(node, null, 2)}
    -          
    - - ) - }, + [BLOCKS.PARAGRAPH]: (node, children) =>

    {children}

    , }, +}) + +export default function ContentfulPageTemplate({ data }) { + return ( +
    + {/* Your other fields */} +
    {renderRichText(data.content, makeOptions)}
    +
    + ) } -function BlogPostTemplate({ data }) { - const { bodyRichText } = data.contentfulBlogPost - - return
    {bodyRichText && renderRichText(bodyRichText, options)}
    -} +export const pageQuery = graphql` + query contentfulPageQuery($slug: String!) { + contentfulPage(slug: { eq: $slug }) { + title + content { + json + } + } + } +` ``` -**Note:** The `contentful_id` and `__typename` fields must be queried on rich-text references in order for the `renderNode` to receive the correct data. +### Embedding Images in Rich Text with Gatsby Plugin Image -### Embedding an image in a Rich Text field +For optimal image display, use `gatsby-plugin-image` to render embedded images: -**Import** +```jsx +// src/templates/contentful-page-template.js +import { GatsbyImage } from "gatsby-plugin-image" -```js -import { renderRichText } from "gatsby-source-contentful/rich-text" -``` +const makeOptions = ({ assetBlockMap, entryBlockMap, entryInlineMap }) => ({ + renderNode: { + [BLOCKS.EMBEDDED_ASSET]: node => { + const asset = assetBlockMap.get(node?.data?.target?.sys.id) + if (asset.gatsbyImageData) { + return + } + }, + }, +}) -**GraphQL** +// No changes are required to the ContentfulPageTemplate page component -```graphql -mainContent { - raw - references { - ... on ContentfulAsset { - contentful_id - __typename - gatsbyImageData +export const pageQuery = graphql` + query contentfulPageQuery($slug: String!) { + contentfulPage(slug: { eq: $slug }) { + title + content { + json + links { + assets { + block { + sys { + # Make sure to query the id for link resolution + id + } + # You have full access to gatsby-plugin-image + gatsbyImageData(width: 200) + } + } + } + } } } -} +` ``` -**Options** +### Embedding Entries in Rich Text + +Enhance the renderer to include other entries within Rich Text: ```jsx -const options = { +// src/templates/contentful-page-template.js +import { GatsbyImage } from "gatsby-plugin-image" + +const makeOptions = ({ assetBlockMap, entryBlockMap, entryInlineMap }) => ({ renderNode: { - "embedded-asset-block": node => { - const { gatsbyImageData } = node.data.target - if (!gatsbyImageData) { - // asset is not an image - return null + [BLOCKS.EMBEDDED_ASSET]: node => { + const asset = assetBlockMap.get(node?.data?.target?.sys.id) + if (asset.gatsbyImageData) { + return + } + }, + [BLOCKS.EMBEDDED_ENTRY]: node => { + const entry = entryBlockMap.get(node?.data?.target?.sys.id) + if (entry.__typename === "ContentfulContentTypeSomethingElse") { + return } - return + // Add more conditions for other content types }, }, -} -``` +}) -**Render** +// No changes are required to the ContentfulPageTemplate page component -```jsx -
    - {blogPost.mainContent && renderRichText(blogPost.mainContent, options)} -
    +export const pageQuery = graphql` + query contentfulPageQuery($slug: String!) { + contentfulPage(slug: { eq: $slug }) { + title + content { + json + links { + assets { + block { + sys { + id + } + gatsbyImageData(width: 200) + } + } + entries { + block { + __typename + sys { + # These two fields are required, + # make sure to include them in your sys + id + type + } + ... on ContentfulContentTypeSomethingElse { + body + } + } + } + } + } + } + } +` ``` -Check out the examples at [@contentful/rich-text-react-renderer](https://github.com/contentful/rich-text/tree/master/packages/rich-text-react-renderer). +### Further Contentful Rich Text resources + +1. [Contentful Rich Text Field Documentation](https://www.contentful.com/developers/docs/concepts/rich-text/): A comprehensive guide to understanding and utilizing Rich Text fields in Contentful. +2. [Contentful Rich Text Rendering Library](https://github.com/contentful/rich-text): Explore the official Contentful library for rendering Rich Text content. +3. [Using Rich Text Fields in Gatsby](https://www.contentful.com/developers/docs/javascript/tutorials/rendering-contentful-rich-text-with-javascript/): An in-depth tutorial on rendering Rich Text fields with JavaScript, easily adaptable for Gatsby projects. + +## Downloading Assets for Static Distribution -## Download assets for static distribution +The `downloadLocal` feature is a valuable addition for managing Contentful assets, enabling local storage and access. This option can significantly enhance your project’s performance and flexibility in handling media files. -When you set `downloadLocal: true` in your config, the plugin will download and cache Contentful assets to the local filesystem. +### Benefits of `downloadLocal` -There are two main reasons you might want to use this option: +1. **Improved Performance**: By caching assets locally, you reduce the dependency on external network requests to Contentful's CDN, potentially speeding up load times. +2. **Enhanced Image Processing**: Local assets allow for more control over image processing, enabling the use of advanced features in `gatsby-plugin-image`, such as generating AVIF formats. -- Avoiding the extra network handshake required to the Contentful CDN for images hosted there -- Gain more control over how images are processed or rendered (ie, providing AVIF with gatsby-plugin-image) +### Trade-offs -The default setting for this feature is `false`, as there are certain tradeoffs for using this feature: +However, consider the following trade-offs when enabling this feature: -- All assets in your Contentful space will be downloaded during builds -- Your build times and build output size will increase -- Bandwidth going through your CDN will increase (since images are no longer being served from the Contentful CDN) -- You will have to change how you query images and which image fragments you use. +- **Increased Build Times**: Downloading all assets during build time can increase the duration of your build process. +- **Larger Build Output Size**: The local storage of assets will increase the size of your build output. +- **Higher Bandwidth Usage**: Since images are not served from Contentful's CDN, your CDN might experience increased bandwidth usage. -### Enable the feature with the `downloadLocal: true` option. +### Enabling `downloadLocal` + +To activate this feature, set the `downloadLocal` option to `true` in your `gatsby-config.js`: ```javascript -// In your gatsby-config.js +// gatsby-config.js module.exports = { plugins: [ { resolve: `gatsby-source-contentful`, options: { spaceId: `your_space_id`, - // Learn about environment variables: https://gatsby.app/env-vars accessToken: process.env.CONTENTFUL_ACCESS_TOKEN, downloadLocal: true, }, }, + // ... other plugins ... ], } ``` -### Updating Queries for downloadLocal +### Updating GraphQL Queries + +Once `downloadLocal` is enabled, your GraphQL queries will need adjustment to work with the locally downloaded assets: + +- Query the `localFile` field on `ContentfulAsset` to access the local file. +- Utilize `gatsby-plugin-image` to process and display images. -When using the `downloadLocal` setting, you'll need to update your codebase to be working with the locally downloaded images, rather than the default Contentful Nodes. Query a `ContentfulAsset`'s `localFile` field in GraphQL to gain access to the common fields of the `gatsby-source-filesystem` `File` node. This is not a Contentful node, so usage for `gatsby-plugin-image` is different: +Example Query: ```graphql query MyQuery { - # Example is for a `ContentType` with a `ContentfulAsset` field - # You could also query an asset directly via - # `allContentfulAsset { edges{ node { } } }` - # or `contentfulAsset(contentful_id: { eq: "contentful_id here" } ) { }` - contentfulMyContentType { + contentfulContentTypePage { myContentfulAssetField { - # Query for locally stored file(e.g. An image) - `File` node localFile { - # Use `gatsby-plugin-image` to create the image data childImageSharp { gatsbyImageData(formats: AVIF) } @@ -655,35 +885,37 @@ query MyQuery { } ``` -Note: This feature downloads any file from a `ContentfulAsset` node that `gatsby-source-contentful` provides. They are all copied over from `./cache/gatsby-source-filesystem/` to the sites build location `./public/static/`. +### Troubleshooting -For any troubleshooting related to this feature, first try clearing your `./cache/` directory. `gatsby-source-contentful` will acquire fresh data, and all `ContentfulAsset`s will be downloaded and cached again. +- **Cache Clearing**: If you encounter issues with this feature, start by clearing the Gatsby cache by executing `npx gatsby clear` in your root directory. This ensures fresh data from Contentful and re-downloads assets. -## Sourcing From Multiple Contentful Spaces +## Multi-Space Sourcing: Example for Sourcing from Multiple Contentful Spaces -To source from multiple Contentful environments/spaces, add another configuration for `gatsby-source-contentful` in `gatsby-config.js`: +To source content from multiple Contentful spaces within a Gatsby project, you can define multiple instances of the `gatsby-source-contentful` plugin in your `gatsby-config.js`. Each instance should be configured with its respective Contentful space ID and access token. Here's an example: ```javascript -// In your gatsby-config.js +// gatsby-config.js module.exports = { plugins: [ { resolve: `gatsby-source-contentful`, options: { - spaceId: `your_space_id`, - accessToken: process.env.CONTENTFUL_ACCESS_TOKEN, + spaceId: `first_space_id`, + accessToken: process.env.FIRST_SPACE_ACCESS_TOKEN, }, }, { resolve: `gatsby-source-contentful`, options: { - spaceId: `your_second_space_id`, - accessToken: process.env.SECONDARY_CONTENTFUL_ACCESS_TOKEN, + spaceId: `second_space_id`, + accessToken: process.env.SECOND_SPACE_ACCESS_TOKEN, }, }, + // ... additional plugin instances for other spaces ... ], } ``` -[dotenv]: https://github.com/motdotla/dotenv -[envvars]: https://gatsby.dev/env-vars +### Current Limitations + +As of now, the `gatsby-source-contentful` plugin does not support the [Contentful feature for linking content across multiple spaces](https://www.contentful.com/help/link-content-across-multiple-spaces/). This means that while you can source content from different spaces, any links or references to content residing in separate spaces won't be resolved automatically. It's important to plan your content architecture accordingly, keeping this limitation in mind. diff --git a/packages/gatsby-source-contentful/gatsby-node.js b/packages/gatsby-source-contentful/gatsby-node.js new file mode 100644 index 0000000000000..2a15db4534328 --- /dev/null +++ b/packages/gatsby-source-contentful/gatsby-node.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +module.exports = require("./dist/gatsby-node"); diff --git a/packages/gatsby-source-contentful/index.d.ts b/packages/gatsby-source-contentful/index.d.ts deleted file mode 100644 index 542b1609adf03..0000000000000 --- a/packages/gatsby-source-contentful/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module "gatsby-source-contentful" {} diff --git a/packages/gatsby-source-contentful/index.js b/packages/gatsby-source-contentful/index.js index c3b803dbe9304..d21b571a9a46c 100644 --- a/packages/gatsby-source-contentful/index.js +++ b/packages/gatsby-source-contentful/index.js @@ -1 +1 @@ -module.exports.schemes = require(`./schemes.js`) +module.exports = require(`./dist/index.js`) diff --git a/packages/gatsby-source-contentful/jest.config.js b/packages/gatsby-source-contentful/jest.config.js new file mode 100644 index 0000000000000..afbc44fb86c08 --- /dev/null +++ b/packages/gatsby-source-contentful/jest.config.js @@ -0,0 +1,10 @@ +/** @type {import('jest').Config} */ +const config = { + snapshotFormat: { + escapeString: true, + printBasicPrototype: true, + }, + snapshotSerializers: [`jest-serializer-path`], +} + +module.exports = config \ No newline at end of file diff --git a/packages/gatsby-source-contentful/package.json b/packages/gatsby-source-contentful/package.json index df8089455fa1e..da5a0e400dd92 100644 --- a/packages/gatsby-source-contentful/package.json +++ b/packages/gatsby-source-contentful/package.json @@ -1,25 +1,28 @@ { "name": "gatsby-source-contentful", "description": "Gatsby source plugin for building websites using the Contentful CMS as a data source", - "version": "8.14.0-next.2", - "author": "Marcus Ericsson (mericsson.com)", + "version": "8.99.0-next.0", + "author": "Marcus Ericsson (mericsson.com), Benedikt Rötsch (hashbite.net)", + "main": "index.js", + "types": "dist/index.d.ts", "bugs": { "url": "https://github.com/gatsbyjs/gatsby/issues" }, "dependencies": { "@babel/runtime": "^7.20.13", "@contentful/rich-text-react-renderer": "^15.17.0", - "@contentful/rich-text-types": "^15.15.1", - "@hapi/joi": "^15.1.1", + "@contentful/rich-text-links": "^16.1.1", + "@contentful/rich-text-types": "^16.2.0", "@vercel/fetch-retry": "^5.1.3", "chalk": "^4.1.2", "common-tags": "^1.8.2", - "contentful": "^9.3.5", - "fs-extra": "^11.2.0", - "gatsby-core-utils": "^4.14.0-next.2", - "gatsby-plugin-utils": "^4.14.0-next.2", - "gatsby-source-filesystem": "^5.14.0-next.2", + "contentful": "^10.2.3", + "fs-extra": "^11.1.1", + "gatsby-core-utils": "^4.14.0-next.0", + "gatsby-plugin-utils": "^4.14.0-next.0", + "gatsby-source-filesystem": "^5.14.0-next.0", "is-online": "^9.0.1", + "joi": "^17.9.2", "json-stringify-safe": "^5.0.1", "lodash": "^4.17.21", "node-fetch": "^2.6.12", @@ -31,7 +34,10 @@ "@babel/core": "^7.20.12", "babel-preset-gatsby-package": "^3.14.0-next.2", "cross-env": "^7.0.3", - "nock": "^13.3.1" + "del-cli": "^5.0.0", + "gatsby-plugin-sharp": "^5.14.0-next.0", + "nock": "^13.3.1", + "typescript": "^5.0.4" }, "homepage": "https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-source-contentful#readme", "keywords": [ @@ -42,9 +48,7 @@ "license": "MIT", "peerDependencies": { "gatsby": "^5.0.0-next", - "gatsby-plugin-image": "^3.0.0-next", - "gatsby-plugin-sharp": "^5.0.0-next", - "sharp": "^0.32.6" + "gatsby-plugin-image": "^3.0.0-next" }, "repository": { "type": "git", @@ -52,12 +56,14 @@ "directory": "packages/gatsby-source-contentful" }, "scripts": { - "build": "babel src --out-dir . --ignore **/__tests__ --ignore **/__fixtures__", - "prepare": "cross-env NODE_ENV=production npm run build", - "watch": "babel -w src --out-dir . --ignore **/__tests__ --ignore **/__fixtures__" + "build": "babel src --out-dir dist/ --ignore \"**/__tests__\" --ignore \"**/__mocks__\" --extensions \".ts\"", + "typegen": "tsc --emitDeclarationOnly --declaration --declarationDir dist/", + "prepare": "cross-env NODE_ENV=production npm run clean && npm run build && npm run typegen", + "watch": "babel -w src --out-dir dist/ --ignore \"**/__tests__\" --extensions \".ts\"", + "test": "jest", + "clean": "del-cli dist/*" }, "engines": { "node": ">=18.0.0" - }, - "types": "index.d.ts" + } } diff --git a/packages/gatsby-source-contentful/rich-text.d.ts b/packages/gatsby-source-contentful/rich-text.d.ts deleted file mode 100644 index 03ec178b54811..0000000000000 --- a/packages/gatsby-source-contentful/rich-text.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ReactNode } from "react" -import { Options } from "@contentful/rich-text-react-renderer" - -interface ContentfulRichTextGatsbyReference { - /** - * Either ContentfulAsset for assets or ContentfulYourContentTypeName for content types - */ - __typename: string - contentful_id: string -} - -interface RenderRichTextData { - raw?: string | null - references?: T[] | null -} - -export function renderRichText< - TReference extends ContentfulRichTextGatsbyReference ->(data: RenderRichTextData, options?: Options): ReactNode diff --git a/packages/gatsby-source-contentful/src/__fixtures__/content-types.js b/packages/gatsby-source-contentful/src/__fixtures__/content-types.js new file mode 100644 index 0000000000000..0687d22eb19a8 --- /dev/null +++ b/packages/gatsby-source-contentful/src/__fixtures__/content-types.js @@ -0,0 +1,786 @@ +export const contentTypes = [ + { + sys: { + space: { + sys: { + type: `Link`, + linkType: `Space`, + id: `k8iqpp6u0ior`, + }, + }, + id: `number`, + type: `ContentType`, + createdAt: `2021-03-01T16:59:23.010Z`, + updatedAt: `2021-05-21T11:20:58.851Z`, + environment: { + sys: { + id: `master`, + type: `Link`, + linkType: `Environment`, + }, + }, + revision: 5, + }, + displayField: `title`, + name: `Number`, + description: ``, + fields: [ + { + id: `title`, + name: `Title`, + type: `Symbol`, + localized: false, + required: true, + disabled: false, + omitted: false, + }, + { + id: `integer`, + name: `Integer`, + type: `Integer`, + localized: false, + required: false, + disabled: false, + omitted: false, + }, + { + id: `integerLocalized`, + name: `Integer Localized`, + type: `Integer`, + localized: true, + required: false, + disabled: false, + omitted: false, + }, + { + id: `decimal`, + name: `Decimal`, + type: `Number`, + localized: false, + required: false, + disabled: false, + omitted: false, + }, + { + id: `decimalLocalized`, + name: `Decimal Localized`, + type: `Number`, + localized: true, + required: false, + disabled: false, + omitted: false, + }, + ], + }, + { + sys: { + space: { + sys: { + type: `Link`, + linkType: `Space`, + id: `k8iqpp6u0ior`, + }, + }, + id: `text`, + type: `ContentType`, + createdAt: `2021-03-01T17:02:35.612Z`, + updatedAt: `2021-05-21T10:48:06.210Z`, + environment: { + sys: { + id: `master`, + type: `Link`, + linkType: `Environment`, + }, + }, + revision: 3, + }, + displayField: `title`, + name: `Text`, + description: ``, + fields: [ + { + id: `title`, + name: `Title`, + type: `Symbol`, + localized: false, + required: true, + disabled: false, + omitted: false, + }, + { + id: `short`, + name: `Short`, + type: `Symbol`, + localized: false, + required: false, + disabled: false, + omitted: false, + }, + { + id: `shortLocalized`, + name: `Short Localized`, + type: `Symbol`, + localized: true, + required: false, + disabled: false, + omitted: false, + }, + { + id: `shortList`, + name: `Short List`, + type: `Array`, + localized: false, + required: false, + disabled: false, + omitted: false, + items: { + type: `Symbol`, + validations: [], + }, + }, + { + id: `shortListLocalized`, + name: `Short List Localized`, + type: `Array`, + localized: true, + required: false, + disabled: false, + omitted: false, + items: { + type: `Symbol`, + validations: [], + }, + }, + { + id: `longPlain`, + name: `Long Plain`, + type: `Text`, + localized: false, + required: false, + disabled: false, + omitted: false, + }, + { + id: `longMarkdown`, + name: `Long Markdown`, + type: `Text`, + localized: false, + required: false, + disabled: false, + omitted: false, + }, + { + id: `longLocalized`, + name: `Long Localized`, + type: `Text`, + localized: true, + required: false, + disabled: false, + omitted: false, + }, + ], + }, + { + sys: { + space: { + sys: { + type: `Link`, + linkType: `Space`, + id: `k8iqpp6u0ior`, + }, + }, + id: `mediaReference`, + type: `ContentType`, + createdAt: `2021-03-01T17:03:21.639Z`, + updatedAt: `2021-05-21T11:11:20.265Z`, + environment: { + sys: { + id: `master`, + type: `Link`, + linkType: `Environment`, + }, + }, + revision: 8, + }, + displayField: `title`, + name: `Media Reference`, + description: ``, + fields: [ + { + id: `title`, + name: `Title`, + type: `Symbol`, + localized: false, + required: true, + disabled: false, + omitted: false, + }, + { + id: `one`, + name: `One`, + type: `Link`, + localized: false, + required: false, + disabled: false, + omitted: false, + linkType: `Asset`, + }, + { + id: `oneLocalized`, + name: `One Localized`, + type: `Link`, + localized: true, + required: false, + disabled: false, + omitted: false, + linkType: `Asset`, + }, + { + id: `many`, + name: `Many`, + type: `Array`, + localized: false, + required: false, + disabled: false, + omitted: false, + items: { + type: `Link`, + validations: [], + linkType: `Asset`, + }, + }, + { + id: `manyLocalized`, + name: `Many Localized`, + type: `Array`, + localized: true, + required: false, + disabled: false, + omitted: false, + items: { + type: `Link`, + validations: [], + linkType: `Asset`, + }, + }, + ], + }, + { + sys: { + space: { + sys: { + type: `Link`, + linkType: `Space`, + id: `k8iqpp6u0ior`, + }, + }, + id: `boolean`, + type: `ContentType`, + createdAt: `2021-03-01T17:05:40.030Z`, + updatedAt: `2021-05-21T10:32:45.505Z`, + environment: { + sys: { + id: `master`, + type: `Link`, + linkType: `Environment`, + }, + }, + revision: 3, + }, + displayField: `title`, + name: `Boolean`, + description: ``, + fields: [ + { + id: `title`, + name: `Title`, + type: `Symbol`, + localized: false, + required: false, + disabled: false, + omitted: false, + }, + { + id: `boolean`, + name: `Boolean`, + type: `Boolean`, + localized: false, + required: false, + disabled: false, + omitted: false, + }, + { + id: `booleanLocalized`, + name: `Boolean Localized`, + type: `Boolean`, + localized: true, + required: false, + disabled: false, + omitted: false, + }, + ], + }, + { + sys: { + space: { + sys: { + type: `Link`, + linkType: `Space`, + id: `k8iqpp6u0ior`, + }, + }, + id: `date`, + type: `ContentType`, + createdAt: `2021-03-01T17:07:02.629Z`, + updatedAt: `2021-05-20T18:16:28.584Z`, + environment: { + sys: { + id: `master`, + type: `Link`, + linkType: `Environment`, + }, + }, + revision: 2, + }, + displayField: `title`, + name: `Date`, + description: ``, + fields: [ + { + id: `title`, + name: `Title`, + type: `Symbol`, + localized: false, + required: false, + disabled: false, + omitted: false, + }, + { + id: `date`, + name: `Date`, + type: `Date`, + localized: false, + required: false, + disabled: false, + omitted: false, + }, + { + id: `dateTime`, + name: `Date Time`, + type: `Date`, + localized: false, + required: false, + disabled: false, + omitted: false, + }, + { + id: `dateTimeTimezone`, + name: `Date Time Timezone`, + type: `Date`, + localized: false, + required: false, + disabled: false, + omitted: false, + }, + { + id: `dateLocalized`, + name: `Date Localized`, + type: `Date`, + localized: true, + required: false, + disabled: false, + omitted: false, + }, + ], + }, + { + sys: { + space: { + sys: { + type: `Link`, + linkType: `Space`, + id: `k8iqpp6u0ior`, + }, + }, + id: `location`, + type: `ContentType`, + createdAt: `2021-03-01T17:09:20.579Z`, + updatedAt: `2021-05-21T10:37:52.205Z`, + environment: { + sys: { + id: `master`, + type: `Link`, + linkType: `Environment`, + }, + }, + revision: 2, + }, + displayField: `title`, + name: `Location`, + description: ``, + fields: [ + { + id: `title`, + name: `Title`, + type: `Symbol`, + localized: false, + required: true, + disabled: false, + omitted: false, + }, + { + id: `location`, + name: `Location`, + type: `Location`, + localized: false, + required: false, + disabled: false, + omitted: false, + }, + { + id: `locationLocalized`, + name: `Location Localized`, + type: `Location`, + localized: true, + required: false, + disabled: false, + omitted: false, + }, + ], + }, + { + sys: { + space: { + sys: { + type: `Link`, + linkType: `Space`, + id: `k8iqpp6u0ior`, + }, + }, + id: `json`, + type: `ContentType`, + createdAt: `2021-03-01T17:09:56.970Z`, + updatedAt: `2021-05-21T10:36:57.432Z`, + environment: { + sys: { + id: `master`, + type: `Link`, + linkType: `Environment`, + }, + }, + revision: 2, + }, + displayField: `title`, + name: `JSON`, + description: ``, + fields: [ + { + id: `title`, + name: `Title`, + type: `Symbol`, + localized: false, + required: false, + disabled: false, + omitted: false, + }, + { + id: `json`, + name: `JSON`, + type: `Object`, + localized: false, + required: false, + disabled: false, + omitted: false, + }, + { + id: `jsonLocalized`, + name: `JSON Localized`, + type: `Object`, + localized: true, + required: false, + disabled: false, + omitted: false, + }, + ], + }, + { + sys: { + space: { + sys: { + type: `Link`, + linkType: `Space`, + id: `k8iqpp6u0ior`, + }, + }, + id: `richText`, + type: `ContentType`, + createdAt: `2021-03-01T17:11:01.406Z`, + updatedAt: `2021-11-05T12:56:39.942Z`, + environment: { + sys: { + id: `master`, + type: `Link`, + linkType: `Environment`, + }, + }, + revision: 7, + }, + displayField: `title`, + name: `Rich Text`, + description: ``, + fields: [ + { + id: `title`, + name: `Title`, + type: `Symbol`, + localized: false, + required: false, + disabled: false, + omitted: false, + }, + { + id: `richText`, + name: `Rich Text`, + type: `RichText`, + localized: false, + required: false, + disabled: false, + omitted: false, + }, + { + id: `richTextLocalized`, + name: `Rich Text Localized`, + type: `RichText`, + localized: true, + required: false, + disabled: false, + omitted: false, + }, + { + id: `richTextValidated`, + name: `Rich Text Validated`, + type: `RichText`, + localized: false, + required: false, + disabled: false, + omitted: false, + }, + ], + }, + { + sys: { + space: { + sys: { + type: `Link`, + linkType: `Space`, + id: `k8iqpp6u0ior`, + }, + }, + id: `contentReference`, + type: `ContentType`, + createdAt: `2021-03-02T09:17:08.210Z`, + updatedAt: `2021-05-21T10:36:34.506Z`, + environment: { + sys: { + id: `master`, + type: `Link`, + linkType: `Environment`, + }, + }, + revision: 7, + }, + displayField: `title`, + name: `Content Reference`, + description: ``, + fields: [ + { + id: `title`, + name: `Title`, + type: `Symbol`, + localized: false, + required: false, + disabled: false, + omitted: false, + }, + { + id: `one`, + name: `One`, + type: `Link`, + localized: false, + required: false, + disabled: false, + omitted: false, + linkType: `Entry`, + }, + { + id: `oneLocalized`, + name: `One Localized`, + type: `Link`, + localized: true, + required: false, + disabled: false, + omitted: false, + linkType: `Entry`, + }, + { + id: `many`, + name: `Many`, + type: `Array`, + localized: false, + required: false, + disabled: false, + omitted: false, + items: { + type: `Link`, + validations: [ + { + linkContentType: [`contentReference`, `number`, `text`], + }, + ], + linkType: `Entry`, + }, + }, + { + id: `manyLocalized`, + name: `Many Localized`, + type: `Array`, + localized: true, + required: false, + disabled: false, + omitted: false, + items: { + type: `Link`, + validations: [], + linkType: `Entry`, + }, + }, + ], + }, + { + sys: { + space: { + sys: { + type: `Link`, + linkType: `Space`, + id: `k8iqpp6u0ior`, + }, + }, + id: `validatedContentReference`, + type: `ContentType`, + createdAt: `2021-05-20T15:29:49.734Z`, + updatedAt: `2021-05-20T15:33:26.620Z`, + environment: { + sys: { + id: `master`, + type: `Link`, + linkType: `Environment`, + }, + }, + revision: 4, + }, + displayField: `title`, + name: `ValidatedContentReference`, + description: ``, + fields: [ + { + id: `title`, + name: `Title`, + type: `Symbol`, + localized: false, + required: false, + disabled: false, + omitted: false, + }, + { + id: `oneItemSingleType`, + name: `One Item: Single Type`, + type: `Link`, + localized: false, + required: false, + disabled: false, + omitted: false, + linkType: `Entry`, + validations: [ + { + linkContentType: [`text`], + }, + ], + }, + { + id: `oneItemManyTypes`, + name: `One Item: Many Types`, + type: `Link`, + localized: false, + required: false, + disabled: false, + omitted: false, + linkType: `Entry`, + validations: [ + { + linkContentType: [`number`, `text`], + }, + ], + }, + { + id: `oneItemAllTypes`, + name: `One Item: All Types`, + type: `Link`, + localized: false, + required: false, + disabled: false, + omitted: false, + linkType: `Entry`, + }, + { + id: `multipleItemsSingleType`, + name: `Multiple Items: Single Type`, + type: `Array`, + localized: false, + required: false, + disabled: false, + omitted: false, + items: { + type: `Link`, + validations: [ + { + linkContentType: [`text`], + }, + ], + linkType: `Entry`, + }, + }, + { + id: `multipleItemsManyTypes`, + name: `Multiple Items: Many Types`, + type: `Array`, + localized: false, + required: false, + disabled: false, + omitted: false, + items: { + type: `Link`, + validations: [ + { + linkContentType: [`number`, `text`], + }, + ], + linkType: `Entry`, + }, + }, + { + id: `multipleItemsAllTypes`, + name: `Multiple Items: All Types`, + type: `Array`, + localized: false, + required: false, + disabled: false, + omitted: false, + items: { + type: `Link`, + validations: [], + linkType: `Entry`, + }, + }, + ], + }, +] diff --git a/packages/gatsby-source-contentful/src/__fixtures__/restricted-content-type.js b/packages/gatsby-source-contentful/src/__fixtures__/restricted-content-type.js deleted file mode 100644 index 8adda16238442..0000000000000 --- a/packages/gatsby-source-contentful/src/__fixtures__/restricted-content-type.js +++ /dev/null @@ -1,80 +0,0 @@ -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: { - entries: [], - assets: [], - deletedEntries: [], - deletedAssets: [], - nextSyncToken: `12345`, - }, - defaultLocale: `en-US`, - locales: [ - { - code: `en-US`, - name: `English (United States)`, - default: true, - fallbackCode: null, - sys: { - id: `1uSElBQA68GRKF30tpTxxT`, - type: `Locale`, - version: 1, - }, - }, - ], - space: { - sys: { type: `Space`, id: `uzfinxahlog0` }, - name: `Starter Gatsby Blog`, - locales: [ - { - code: `en-US`, - default: true, - name: `English (United States)`, - fallbackCode: null, - }, - ], - }, - tagItems: [], - } -} 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 0be1eaa13ee55..d26d3c97acc6c 100644 --- a/packages/gatsby-source-contentful/src/__fixtures__/rich-text-data.js +++ b/packages/gatsby-source-contentful/src/__fixtures__/rich-text-data.js @@ -496,6 +496,7 @@ exports.initialSync = () => { }, }, }, + metadata: { tags: [] }, }, { sys: { @@ -658,6 +659,7 @@ exports.initialSync = () => { }, }, }, + metadata: { tags: [] }, }, ], assets: [ @@ -702,6 +704,7 @@ exports.initialSync = () => { }, }, }, + metadata: { tags: [] }, }, { sys: { @@ -744,6 +747,7 @@ exports.initialSync = () => { }, }, }, + metadata: { tags: [] }, }, ], deletedEntries: [], @@ -794,82 +798,3 @@ exports.initialSync = () => { tagItems: [], } } - -// @todo this fixture is unused -exports.deleteLinkedPage = () => { - return { - currentSyncData: { - entries: [], - assets: [], - deletedEntries: [ - { - sys: { - type: `DeletedEntry`, - id: `7oHxo6bs0us9wIkq27qdyK`, - space: { - sys: { - type: `Link`, - linkType: `Space`, - id: `ahntqop9oi7x`, - }, - }, - environment: { - sys: { - id: `master`, - type: `Link`, - linkType: `Environment`, - }, - }, - revision: 1, - createdAt: `2020-10-16T12:29:38.094Z`, - updatedAt: `2020-10-16T12:29:38.094Z`, - deletedAt: `2020-10-16T12:29:38.094Z`, - }, - }, - ], - deletedAssets: [], - nextSyncToken: `FEnChMOBwr1Yw4TCqsK2LcKpCH3CjsORIyLDrGbDtgozw6xreMKCwpjCtlxATw3CqcO3w6XCrMKuITDDiEoQSMKvIMOYwrzCn3sHPH3CvsK3w4A9w6LCjsOVwrjCjGwbw4rCl0fDl8OhU8Oqw67DhMOCwozDmxrChsOtRD4`, - }, - defaultLocale: `en-US`, - locales: [ - { - code: `en-US`, - name: `English (United States)`, - default: true, - fallbackCode: null, - sys: { - id: `1uSElBQA68GRKF30tpTxxT`, - type: `Locale`, - version: 1, - }, - }, - { - code: `nl`, - name: `Dutch`, - default: false, - fallbackCode: `en-US`, - sys: { - id: `2T7M2OzIrvE8cOCOF1HMuY`, - type: `Locale`, - version: 1, - }, - }, - ], - space: { - sys: { - type: `Space`, - id: `ahntqop9oi7x`, - }, - name: `Rich Text`, - locales: [ - { - code: `en-US`, - default: true, - name: `English (United States)`, - fallbackCode: null, - }, - ], - }, - tagItems: [], - } -} 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 60dc97f683950..b2d94d44dde96 100644 --- a/packages/gatsby-source-contentful/src/__fixtures__/starter-blog-data.js +++ b/packages/gatsby-source-contentful/src/__fixtures__/starter-blog-data.js @@ -289,6 +289,7 @@ exports.initialSync = () => { publishDate: { "en-US": `2017-05-12T00:00+02:00` }, tags: { "en-US": [`javascript`] }, }, + metadata: { tags: [] }, }, { sys: { @@ -349,6 +350,7 @@ exports.initialSync = () => { publishDate: { "en-US": `2017-05-15T00:00+02:00` }, tags: { "en-US": [`general`] }, }, + metadata: { tags: [] }, }, { sys: { @@ -409,6 +411,7 @@ exports.initialSync = () => { publishDate: { "en-US": `2017-05-16T00:00+02:00` }, tags: { "en-US": [`javascript`, `static-sites`] }, }, + metadata: { tags: [] }, }, { sys: { @@ -461,6 +464,7 @@ exports.initialSync = () => { }, }, }, + metadata: { tags: [] }, }, ], assets: [ @@ -734,6 +738,7 @@ exports.createBlogPost = () => { }, publishDate: { "en-US": `2020-04-01T00:00+02:00` }, }, + metadata: { tags: [] }, }, ], assets: [ @@ -887,6 +892,7 @@ exports.updateBlogPost = () => { }, publishDate: { "en-US": `2020-05-15T00:00+02:00` }, }, + metadata: { tags: [] }, }, ], assets: [], diff --git a/packages/gatsby-source-contentful/src/__tests__/.gitignore b/packages/gatsby-source-contentful/src/__tests__/.gitignore new file mode 100644 index 0000000000000..c94e3760cc971 --- /dev/null +++ b/packages/gatsby-source-contentful/src/__tests__/.gitignore @@ -0,0 +1 @@ +*/*.png \ No newline at end of file diff --git a/packages/gatsby-source-contentful/src/__tests__/23df7612602d69edfc3eaf89c8c02248/347966-contentful-logo-wordmark-dark (1)-4cd185-original-1582664935 (1).png b/packages/gatsby-source-contentful/src/__tests__/23df7612602d69edfc3eaf89c8c02248/347966-contentful-logo-wordmark-dark (1)-4cd185-original-1582664935 (1).png deleted file mode 100644 index cf451192b4723..0000000000000 Binary files a/packages/gatsby-source-contentful/src/__tests__/23df7612602d69edfc3eaf89c8c02248/347966-contentful-logo-wordmark-dark (1)-4cd185-original-1582664935 (1).png and /dev/null differ diff --git a/packages/gatsby-source-contentful/src/__tests__/47098b484f55425af2625391283f7f32/347966-contentful-logo-wordmark-dark (1)-4cd185-original-1582664935 (1).png b/packages/gatsby-source-contentful/src/__tests__/47098b484f55425af2625391283f7f32/347966-contentful-logo-wordmark-dark (1)-4cd185-original-1582664935 (1).png deleted file mode 100644 index 29d5701a9ebd8..0000000000000 Binary files a/packages/gatsby-source-contentful/src/__tests__/47098b484f55425af2625391283f7f32/347966-contentful-logo-wordmark-dark (1)-4cd185-original-1582664935 (1).png and /dev/null differ diff --git a/packages/gatsby-source-contentful/src/__tests__/85c405f72e68f1e90176d3220d41c5bf/347966-contentful-logo-wordmark-dark (1)-4cd185-original-1582664935 (1).png b/packages/gatsby-source-contentful/src/__tests__/85c405f72e68f1e90176d3220d41c5bf/347966-contentful-logo-wordmark-dark (1)-4cd185-original-1582664935 (1).png deleted file mode 100644 index 5ab7dde5cba71..0000000000000 Binary files a/packages/gatsby-source-contentful/src/__tests__/85c405f72e68f1e90176d3220d41c5bf/347966-contentful-logo-wordmark-dark (1)-4cd185-original-1582664935 (1).png and /dev/null differ diff --git a/packages/gatsby-source-contentful/src/__tests__/__snapshots__/create-schema-customization.js.snap b/packages/gatsby-source-contentful/src/__tests__/__snapshots__/create-schema-customization.js.snap new file mode 100644 index 0000000000000..95097fbf722d4 --- /dev/null +++ b/packages/gatsby-source-contentful/src/__tests__/__snapshots__/create-schema-customization.js.snap @@ -0,0 +1,991 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`create-schema-customization builds schema based on Contentful Content Model 1`] = ` +Array [ + Array [ + Object { + "extensions": Object { + "dontInfer": Object {}, + }, + "fields": Object { + "description": Object { + "type": "String!", + }, + "displayField": Object { + "type": "String!", + }, + "id": Object { + "type": "ID!", + }, + "name": Object { + "type": "String!", + }, + }, + "interfaces": Array [ + "Node", + ], + "name": "ContentfulContentType", + }, + ], + Array [ + Object { + "extensions": Object { + "dontInfer": Object {}, + }, + "fields": Object { + "contentType": Object { + "extensions": Object { + "link": Object { + "by": "id", + "from": "contentType", + }, + }, + "type": "ContentfulContentType", + }, + "environmentId": Object { + "type": " String!", + }, + "firstPublishedAt": Object { + "extensions": Object { + "dateformat": Object {}, + }, + "type": " Date!", + }, + "id": Object { + "type": " String!", + }, + "locale": Object { + "type": " String!", + }, + "publishedAt": Object { + "extensions": Object { + "dateformat": Object {}, + }, + "type": " Date!", + }, + "publishedVersion": Object { + "type": " Int!", + }, + "spaceId": Object { + "type": " String!", + }, + "type": Object { + "type": " String!", + }, + }, + "interfaces": Array [ + "Node", + ], + "name": "ContentfulSys", + }, + ], + Array [ + Object { + "extensions": Object { + "dontInfer": Object {}, + }, + "fields": Object { + "contentful_id": Object { + "type": "String!", + }, + "id": Object { + "type": "ID!", + }, + "name": Object { + "type": "String!", + }, + }, + "interfaces": Array [ + "Node", + ], + "name": "ContentfulTag", + }, + ], + Array [ + Object { + "extensions": Object { + "dontInfer": Object {}, + }, + "fields": Object { + "tags": Object { + "extensions": Object { + "link": Object { + "by": "id", + "from": "tags", + }, + }, + "type": "[ContentfulTag]!", + }, + }, + "name": "ContentfulMetadata", + }, + ], + Array [ + Object { + "extensions": Object { + "dontInfer": Object {}, + }, + "fields": Object { + "ContentfulContentTypeContentReference": Object { + "extensions": Object { + "link": Object { + "by": "id", + "from": "ContentfulContentTypeContentReference", + }, + }, + "type": "[ContentfulContentTypeContentReference]", + }, + "ContentfulContentTypeMediaReference": Object { + "extensions": Object { + "link": Object { + "by": "id", + "from": "ContentfulContentTypeMediaReference", + }, + }, + "type": "[ContentfulContentTypeMediaReference]", + }, + "ContentfulContentTypeRichText": Object { + "extensions": Object { + "link": Object { + "by": "id", + "from": "ContentfulContentTypeRichText", + }, + }, + "type": "[ContentfulContentTypeRichText]", + }, + "ContentfulContentTypeValidatedContentReference": Object { + "extensions": Object { + "link": Object { + "by": "id", + "from": "ContentfulContentTypeValidatedContentReference", + }, + }, + "type": "[ContentfulContentTypeValidatedContentReference]", + }, + }, + "name": "ContentfulLinkedFrom", + }, + ], + Array [ + Object { + "extensions": Object { + "dontInfer": Object {}, + }, + "fields": Object { + "contentType": Object { + "type": "String!", + }, + "contentfulMetadata": Object { + "type": "ContentfulMetadata!", + }, + "description": Object { + "type": "String", + }, + "filename": Object { + "type": "String!", + }, + "gatsbyImageData": Object { + "args": Object { + "aspectRatio": Object { + "description": "If set along with width or height, this will set the value of the other dimension to match the provided aspect ratio, cropping the image if needed. +If neither width or height is provided, height will be set based on the intrinsic width of the source image.", + "type": "Float", + }, + "backgroundColor": Object { + "description": "Background color applied to the wrapper, or when \\"letterboxing\\" an image to another aspect ratio.", + "type": "String", + }, + "breakpoints": Object { + "description": "Specifies the image widths to generate. You should rarely need to change this. For FIXED and CONSTRAINED images it is better to allow these to be determined automatically, +based on the image size. For FULL_WIDTH images this can be used to override the default, which is [750, 1080, 1366, 1920]. +It will never generate any images larger than the source.", + "type": "[Int]", + }, + "cornerRadius": Object { + "defaultValue": 0, + "description": "Desired corner radius in pixels. Results in an image with rounded corners. +Pass \`-1\` for a full circle/ellipse.", + "type": "Int", + }, + "cropFocus": Object { + "type": "ContentfulImageCropFocus", + }, + "formats": Object { + "defaultValue": Array [ + "", + "webp", + ], + "description": "The image formats to generate. Valid values are AUTO (meaning the same format as the source image), JPG, PNG, WEBP and AVIF. +The default value is [AUTO, WEBP], and you should rarely need to change this. Take care if you specify JPG or PNG when you do +not know the formats of the source images, as this could lead to unwanted results such as converting JPEGs to PNGs. Specifying +both PNG and JPG is not supported and will be ignored.", + "type": "[GatsbyImageFormat]", + }, + "height": Object { + "description": "If set, the height of the generated image. If omitted, it is calculated from the supplied width, matching the aspect ratio of the source image.", + "type": "Int", + }, + "jpegProgressive": Object { + "defaultValue": true, + "type": "Boolean", + }, + "layout": Object { + "description": "The layout for the image. +FIXED: A static image sized, that does not resize according to the screen width +FULL_WIDTH: The image resizes to fit its container. Pass a \\"sizes\\" option if it isn't going to be the full width of the screen. +CONSTRAINED: Resizes to fit its container, up to a maximum width, at which point it will remain fixed in size.", + "type": "GatsbyImageLayout", + }, + "outputPixelDensities": Object { + "description": "A list of image pixel densities to generate for FIXED and CONSTRAINED images. You should rarely need to change this. It will never generate images larger than the source, and will always include a 1x image. +Default is [ 1, 2 ] for fixed images, meaning 1x, 2x, 3x, and [0.25, 0.5, 1, 2] for fluid. In this case, an image with a fluid layout and width = 400 would generate images at 100, 200, 400 and 800px wide.", + "type": "[Float]", + }, + "placeholder": Object { + "description": "Format of generated placeholder image, displayed while the main image loads. +BLURRED: a blurred, low resolution image, encoded as a base64 data URI. +DOMINANT_COLOR: a solid color, calculated from the dominant color of the image (default). +TRACED_SVG: deprecated. Will use DOMINANT_COLOR. +NONE: no placeholder. Set the argument \\"backgroundColor\\" to use a fixed background color.", + "type": "GatsbyImagePlaceholder", + }, + "quality": Object { + "defaultValue": 50, + "type": "Int", + }, + "resizingBehavior": Object { + "type": "ImageResizingBehavior", + }, + "sizes": Object { + "description": "The \\"sizes\\" property, passed to the img tag. This describes the display size of the image. +This does not affect the generated images, but is used by the browser to decide which images to download. You can leave this blank for fixed images, or if the responsive image +container will be the full width of the screen. In these cases we will generate an appropriate value.", + "type": "String", + }, + "width": Object { + "description": "The display width of the generated image for layout = FIXED, and the display width of the largest image for layout = CONSTRAINED. +The actual largest image resolution will be this value multiplied by the largest value in outputPixelDensities +Ignored if layout = FLUID.", + "type": "Int", + }, + }, + "resolve": [Function], + "type": "JSON", + }, + "height": Object { + "type": "Int", + }, + "id": Object { + "type": "ID!", + }, + "linkedFrom": Object { + "type": "ContentfulLinkedFrom", + }, + "mimeType": Object { + "type": "String!", + }, + "size": Object { + "type": "Int", + }, + "sys": Object { + "type": "ContentfulSys!", + }, + "title": Object { + "type": "String", + }, + "url": Object { + "type": "String!", + }, + "width": Object { + "type": "Int", + }, + }, + "interfaces": Array [ + "ContentfulEntity", + "Node", + "RemoteFile", + ], + "name": "ContentfulAsset", + }, + ], + Array [ + Object { + "fields": Object { + "block": Object { + "resolve": [Function], + "type": "[ContentfulAsset]!", + }, + "hyperlink": Object { + "resolve": [Function], + "type": "[ContentfulAsset]!", + }, + }, + "name": "ContentfulRichTextAssets", + }, + ], + Array [ + Object { + "fields": Object { + "block": Object { + "resolve": [Function], + "type": "[ContentfulEntry]!", + }, + "hyperlink": Object { + "resolve": [Function], + "type": "[ContentfulEntry]!", + }, + "inline": Object { + "resolve": [Function], + "type": "[ContentfulEntry]!", + }, + }, + "name": "ContentfulRichTextEntries", + }, + ], + Array [ + Object { + "fields": Object { + "assets": Object { + "resolve": [Function], + "type": "ContentfulRichTextAssets", + }, + "entries": Object { + "resolve": [Function], + "type": "ContentfulRichTextEntries", + }, + }, + "name": "ContentfulRichTextLinks", + }, + ], + Array [ + Object { + "extensions": Object { + "dontInfer": Object {}, + }, + "fields": Object { + "json": Object { + "resolve": [Function], + "type": "JSON", + }, + "links": Object { + "resolve": [Function], + "type": "ContentfulRichTextLinks", + }, + }, + "name": "ContentfulRichText", + }, + ], + Array [ + Object { + "extensions": Object { + "dontInfer": Object {}, + }, + "fields": Object { + "lat": Object { + "type": "Float!", + }, + "lon": Object { + "type": "Float!", + }, + }, + "name": "ContentfulLocation", + }, + ], + Array [ + Object { + "extensions": Object { + "dontInfer": Object {}, + }, + "fields": Object { + "raw": "String!", + }, + "interfaces": Array [ + "Node", + ], + "name": "ContentfulMarkdown", + }, + ], + Array [ + Object { + "extensions": Object { + "dontInfer": Object {}, + }, + "fields": Object { + "contentfulMetadata": Object { + "type": "ContentfulMetadata!", + }, + "decimal": Object { + "type": "Float", + }, + "decimalLocalized": Object { + "type": "Float", + }, + "id": Object { + "type": "ID!", + }, + "integer": Object { + "type": "Int", + }, + "integerLocalized": Object { + "type": "Int", + }, + "linkedFrom": "ContentfulLinkedFrom", + "sys": Object { + "type": "ContentfulSys!", + }, + "title": Object { + "type": "String!", + }, + }, + "interfaces": Array [ + "ContentfulEntity", + "ContentfulEntry", + "Node", + ], + "name": "ContentfulContentTypeNumber", + }, + ], + Array [ + Object { + "extensions": Object { + "dontInfer": Object {}, + }, + "fields": Object { + "contentfulMetadata": Object { + "type": "ContentfulMetadata!", + }, + "id": Object { + "type": "ID!", + }, + "linkedFrom": "ContentfulLinkedFrom", + "longLocalized": Object { + "extensions": Object { + "link": Object { + "by": "id", + "from": "longLocalized", + }, + }, + "type": "ContentfulMarkdown", + }, + "longMarkdown": Object { + "extensions": Object { + "link": Object { + "by": "id", + "from": "longMarkdown", + }, + }, + "type": "ContentfulMarkdown", + }, + "longPlain": Object { + "extensions": Object { + "link": Object { + "by": "id", + "from": "longPlain", + }, + }, + "type": "ContentfulMarkdown", + }, + "short": Object { + "type": "String", + }, + "shortList": Object { + "type": "[String]", + }, + "shortListLocalized": Object { + "type": "[String]", + }, + "shortLocalized": Object { + "type": "String", + }, + "sys": Object { + "type": "ContentfulSys!", + }, + "title": Object { + "type": "String!", + }, + }, + "interfaces": Array [ + "ContentfulEntity", + "ContentfulEntry", + "Node", + ], + "name": "ContentfulContentTypeText", + }, + ], + Array [ + Object { + "extensions": Object { + "dontInfer": Object {}, + }, + "fields": Object { + "contentfulMetadata": Object { + "type": "ContentfulMetadata!", + }, + "id": Object { + "type": "ID!", + }, + "linkedFrom": "ContentfulLinkedFrom", + "many": Object { + "extensions": Object { + "link": Object { + "by": "id", + "from": "many", + }, + }, + "type": "[ContentfulAsset]", + }, + "manyLocalized": Object { + "extensions": Object { + "link": Object { + "by": "id", + "from": "manyLocalized", + }, + }, + "type": "[ContentfulAsset]", + }, + "one": Object { + "extensions": Object { + "link": Object { + "by": "id", + "from": "one", + }, + }, + "type": "ContentfulAsset", + }, + "oneLocalized": Object { + "extensions": Object { + "link": Object { + "by": "id", + "from": "oneLocalized", + }, + }, + "type": "ContentfulAsset", + }, + "sys": Object { + "type": "ContentfulSys!", + }, + "title": Object { + "type": "String!", + }, + }, + "interfaces": Array [ + "ContentfulEntity", + "ContentfulEntry", + "Node", + ], + "name": "ContentfulContentTypeMediaReference", + }, + ], + Array [ + Object { + "extensions": Object { + "dontInfer": Object {}, + }, + "fields": Object { + "boolean": Object { + "type": "Boolean", + }, + "booleanLocalized": Object { + "type": "Boolean", + }, + "contentfulMetadata": Object { + "type": "ContentfulMetadata!", + }, + "id": Object { + "type": "ID!", + }, + "linkedFrom": "ContentfulLinkedFrom", + "sys": Object { + "type": "ContentfulSys!", + }, + "title": Object { + "type": "String", + }, + }, + "interfaces": Array [ + "ContentfulEntity", + "ContentfulEntry", + "Node", + ], + "name": "ContentfulContentTypeBoolean", + }, + ], + Array [ + Object { + "extensions": Object { + "dontInfer": Object {}, + }, + "fields": Object { + "contentfulMetadata": Object { + "type": "ContentfulMetadata!", + }, + "date": Object { + "extensions": Object { + "dateformat": Object {}, + }, + "type": "Date", + }, + "dateLocalized": Object { + "extensions": Object { + "dateformat": Object {}, + }, + "type": "Date", + }, + "dateTime": Object { + "extensions": Object { + "dateformat": Object {}, + }, + "type": "Date", + }, + "dateTimeTimezone": Object { + "extensions": Object { + "dateformat": Object {}, + }, + "type": "Date", + }, + "id": Object { + "type": "ID!", + }, + "linkedFrom": "ContentfulLinkedFrom", + "sys": Object { + "type": "ContentfulSys!", + }, + "title": Object { + "type": "String", + }, + }, + "interfaces": Array [ + "ContentfulEntity", + "ContentfulEntry", + "Node", + ], + "name": "ContentfulContentTypeDate", + }, + ], + Array [ + Object { + "extensions": Object { + "dontInfer": Object {}, + }, + "fields": Object { + "contentfulMetadata": Object { + "type": "ContentfulMetadata!", + }, + "id": Object { + "type": "ID!", + }, + "linkedFrom": "ContentfulLinkedFrom", + "location": Object { + "type": "ContentfulLocation", + }, + "locationLocalized": Object { + "type": "ContentfulLocation", + }, + "sys": Object { + "type": "ContentfulSys!", + }, + "title": Object { + "type": "String!", + }, + }, + "interfaces": Array [ + "ContentfulEntity", + "ContentfulEntry", + "Node", + ], + "name": "ContentfulContentTypeLocation", + }, + ], + Array [ + Object { + "extensions": Object { + "dontInfer": Object {}, + }, + "fields": Object { + "contentfulMetadata": Object { + "type": "ContentfulMetadata!", + }, + "id": Object { + "type": "ID!", + }, + "json": Object { + "type": "JSON", + }, + "jsonLocalized": Object { + "type": "JSON", + }, + "linkedFrom": "ContentfulLinkedFrom", + "sys": Object { + "type": "ContentfulSys!", + }, + "title": Object { + "type": "String", + }, + }, + "interfaces": Array [ + "ContentfulEntity", + "ContentfulEntry", + "Node", + ], + "name": "ContentfulContentTypeJson", + }, + ], + Array [ + Object { + "extensions": Object { + "dontInfer": Object {}, + }, + "fields": Object { + "contentfulMetadata": Object { + "type": "ContentfulMetadata!", + }, + "id": Object { + "type": "ID!", + }, + "linkedFrom": "ContentfulLinkedFrom", + "richText": Object { + "resolve": [Function], + "type": "ContentfulRichText", + }, + "richTextLocalized": Object { + "resolve": [Function], + "type": "ContentfulRichText", + }, + "richTextValidated": Object { + "resolve": [Function], + "type": "ContentfulRichText", + }, + "sys": Object { + "type": "ContentfulSys!", + }, + "title": Object { + "type": "String", + }, + }, + "interfaces": Array [ + "ContentfulEntity", + "ContentfulEntry", + "Node", + ], + "name": "ContentfulContentTypeRichText", + }, + ], + Array [ + Object { + "extensions": Object { + "dontInfer": Object {}, + }, + "fields": Object { + "contentfulMetadata": Object { + "type": "ContentfulMetadata!", + }, + "id": Object { + "type": "ID!", + }, + "linkedFrom": "ContentfulLinkedFrom", + "many": Object { + "extensions": Object { + "link": Object { + "by": "id", + "from": "many", + }, + }, + "type": "[UnionContentfulContentReferenceNumberText]", + }, + "manyLocalized": Object { + "extensions": Object { + "link": Object { + "by": "id", + "from": "manyLocalized", + }, + }, + "type": "[ContentfulEntry]", + }, + "one": Object { + "extensions": Object { + "link": Object { + "by": "id", + "from": "one", + }, + }, + "type": "ContentfulEntry", + }, + "oneLocalized": Object { + "extensions": Object { + "link": Object { + "by": "id", + "from": "oneLocalized", + }, + }, + "type": "ContentfulEntry", + }, + "sys": Object { + "type": "ContentfulSys!", + }, + "title": Object { + "type": "String", + }, + }, + "interfaces": Array [ + "ContentfulEntity", + "ContentfulEntry", + "Node", + ], + "name": "ContentfulContentTypeContentReference", + }, + ], + Array [ + Object { + "extensions": Object { + "dontInfer": Object {}, + }, + "fields": Object { + "contentfulMetadata": Object { + "type": "ContentfulMetadata!", + }, + "id": Object { + "type": "ID!", + }, + "linkedFrom": "ContentfulLinkedFrom", + "multipleItemsAllTypes": Object { + "extensions": Object { + "link": Object { + "by": "id", + "from": "multipleItemsAllTypes", + }, + }, + "type": "[ContentfulEntry]", + }, + "multipleItemsManyTypes": Object { + "extensions": Object { + "link": Object { + "by": "id", + "from": "multipleItemsManyTypes", + }, + }, + "type": "[UnionContentfulNumberText]", + }, + "multipleItemsSingleType": Object { + "extensions": Object { + "link": Object { + "by": "id", + "from": "multipleItemsSingleType", + }, + }, + "type": "[ContentfulContentTypeText]", + }, + "oneItemAllTypes": Object { + "extensions": Object { + "link": Object { + "by": "id", + "from": "oneItemAllTypes", + }, + }, + "type": "ContentfulEntry", + }, + "oneItemManyTypes": Object { + "extensions": Object { + "link": Object { + "by": "id", + "from": "oneItemManyTypes", + }, + }, + "type": "UnionContentfulNumberText", + }, + "oneItemSingleType": Object { + "extensions": Object { + "link": Object { + "by": "id", + "from": "oneItemSingleType", + }, + }, + "type": "ContentfulContentTypeText", + }, + "sys": Object { + "type": "ContentfulSys!", + }, + "title": Object { + "type": "String", + }, + }, + "interfaces": Array [ + "ContentfulEntity", + "ContentfulEntry", + "Node", + ], + "name": "ContentfulContentTypeValidatedContentReference", + }, + ], +] +`; + +exports[`create-schema-customization builds schema based on Contentful Content Model 2`] = ` +Array [ + Array [ + Object { + "fields": Object { + "contentfulMetadata": Object { + "type": "ContentfulMetadata!", + }, + "id": Object { + "type": "ID!", + }, + "sys": Object { + "type": "ContentfulSys!", + }, + }, + "interfaces": Array [ + "Node", + ], + "name": "ContentfulEntity", + }, + ], + Array [ + Object { + "fields": Object { + "contentfulMetadata": Object { + "type": "ContentfulMetadata!", + }, + "id": Object { + "type": "ID!", + }, + "linkedFrom": Object { + "type": "ContentfulLinkedFrom", + }, + "sys": Object { + "type": "ContentfulSys!", + }, + }, + "interfaces": Array [ + "ContentfulEntity", + "Node", + ], + "name": "ContentfulEntry", + }, + ], +] +`; + +exports[`create-schema-customization builds schema based on Contentful Content Model 3`] = ` +Array [ + Array [ + Object { + "name": "UnionContentfulContentReferenceNumberText", + "types": Array [ + "ContentfulContentTypeContentReference", + "ContentfulContentTypeNumber", + "ContentfulContentTypeText", + ], + }, + ], + Array [ + Object { + "name": "UnionContentfulNumberText", + "types": Array [ + "ContentfulContentTypeNumber", + "ContentfulContentTypeText", + ], + }, + ], +] +`; diff --git a/packages/gatsby-source-contentful/src/__tests__/__snapshots__/gatsby-node.js.snap b/packages/gatsby-source-contentful/src/__tests__/__snapshots__/gatsby-node.js.snap index 4961649230900..3bfb5088c2279 100644 --- a/packages/gatsby-source-contentful/src/__tests__/__snapshots__/gatsby-node.js.snap +++ b/packages/gatsby-source-contentful/src/__tests__/__snapshots__/gatsby-node.js.snap @@ -1,17 +1,801 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`gatsby-node stores rich text as raw with references attached 1`] = ` -Array [ - "ahntqop9oi7x___7oHxo6bs0us9wIkq27qdyK___Entry", - "ahntqop9oi7x___6KpLS2NZyB3KAvDzWf4Ukh___Entry", - "ahntqop9oi7x___4ZQrqcrTunWiuNaavhGYNT___Asset", -] +exports[`gatsby-node stores rich text as JSON 1`] = ` +Object { + "content": Array [ + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "This is the homepage", + }, + ], + "data": Object {}, + "nodeType": "paragraph", + }, + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "Heading 1", + }, + ], + "data": Object {}, + "nodeType": "heading-1", + }, + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "Heading 2", + }, + ], + "data": Object {}, + "nodeType": "heading-2", + }, + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "Heading 3", + }, + ], + "data": Object {}, + "nodeType": "heading-3", + }, + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "Heading 4", + }, + ], + "data": Object {}, + "nodeType": "heading-4", + }, + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "Heading 5", + }, + ], + "data": Object {}, + "nodeType": "heading-5", + }, + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "Heading 6", + }, + ], + "data": Object {}, + "nodeType": "heading-6", + }, + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "This is ", + }, + Object { + "data": Object {}, + "marks": Array [ + Object { + "type": "bold", + }, + ], + "nodeType": "text", + "value": "bold ", + }, + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "and ", + }, + Object { + "data": Object {}, + "marks": Array [ + Object { + "type": "italic", + }, + ], + "nodeType": "text", + "value": "italic", + }, + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": " and ", + }, + Object { + "data": Object {}, + "marks": Array [ + Object { + "type": "bold", + }, + Object { + "type": "italic", + }, + ], + "nodeType": "text", + "value": "both", + }, + ], + "data": Object {}, + "nodeType": "paragraph", + }, + Object { + "content": Array [ + Object { + "content": Array [ + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "Very", + }, + ], + "data": Object {}, + "nodeType": "paragraph", + }, + ], + "data": Object {}, + "nodeType": "list-item", + }, + Object { + "content": Array [ + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "useful", + }, + ], + "data": Object {}, + "nodeType": "paragraph", + }, + ], + "data": Object {}, + "nodeType": "list-item", + }, + Object { + "content": Array [ + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "list", + }, + ], + "data": Object {}, + "nodeType": "paragraph", + }, + ], + "data": Object {}, + "nodeType": "list-item", + }, + ], + "data": Object {}, + "nodeType": "unordered-list", + }, + Object { + "content": Array [ + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "This is a quote", + }, + ], + "data": Object {}, + "nodeType": "paragraph", + }, + ], + "data": Object {}, + "nodeType": "blockquote", + }, + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "Reference tests:", + }, + ], + "data": Object {}, + "nodeType": "heading-2", + }, + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "Inline Link: ", + }, + Object { + "content": Array [], + "data": Object { + "target": Object { + "sys": Object { + "id": "7oHxo6bs0us9wIkq27qdyK", + "linkType": "Entry", + "type": "Link", + }, + }, + }, + "nodeType": "embedded-entry-inline", + }, + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "", + }, + ], + "data": Object {}, + "nodeType": "paragraph", + }, + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "Link in list:", + }, + ], + "data": Object {}, + "nodeType": "paragraph", + }, + Object { + "content": Array [ + Object { + "content": Array [ + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "", + }, + Object { + "content": Array [], + "data": Object { + "target": Object { + "sys": Object { + "id": "6KpLS2NZyB3KAvDzWf4Ukh", + "linkType": "Entry", + "type": "Link", + }, + }, + }, + "nodeType": "embedded-entry-inline", + }, + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "", + }, + ], + "data": Object {}, + "nodeType": "paragraph", + }, + ], + "data": Object {}, + "nodeType": "list-item", + }, + ], + "data": Object {}, + "nodeType": "ordered-list", + }, + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "Embedded Entity:", + }, + ], + "data": Object {}, + "nodeType": "paragraph", + }, + Object { + "content": Array [], + "data": Object { + "target": Object { + "sys": Object { + "id": "7oHxo6bs0us9wIkq27qdyK", + "linkType": "Entry", + "type": "Link", + }, + }, + }, + "nodeType": "embedded-entry-block", + }, + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "", + }, + ], + "data": Object {}, + "nodeType": "paragraph", + }, + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "Embedded Asset:", + }, + ], + "data": Object {}, + "nodeType": "heading-2", + }, + Object { + "content": Array [], + "data": Object { + "target": Object { + "sys": Object { + "id": "4ZQrqcrTunWiuNaavhGYNT", + "linkType": "Asset", + "type": "Link", + }, + }, + }, + "nodeType": "embedded-asset-block", + }, + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "", + }, + ], + "data": Object {}, + "nodeType": "paragraph", + }, + ], + "data": Object {}, + "nodeType": "document", +} `; -exports[`gatsby-node stores rich text as raw with references attached 2`] = ` -Array [ - "ahntqop9oi7x___7oHxo6bs0us9wIkq27qdyK___Entry___nl", - "ahntqop9oi7x___6KpLS2NZyB3KAvDzWf4Ukh___Entry___nl", - "ahntqop9oi7x___4ZQrqcrTunWiuNaavhGYNT___Asset___nl", -] +exports[`gatsby-node stores rich text as JSON 2`] = ` +Object { + "content": Array [ + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "This is the homepage", + }, + ], + "data": Object {}, + "nodeType": "paragraph", + }, + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "Heading 1", + }, + ], + "data": Object {}, + "nodeType": "heading-1", + }, + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "Heading 2", + }, + ], + "data": Object {}, + "nodeType": "heading-2", + }, + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "Heading 3", + }, + ], + "data": Object {}, + "nodeType": "heading-3", + }, + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "Heading 4", + }, + ], + "data": Object {}, + "nodeType": "heading-4", + }, + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "Heading 5", + }, + ], + "data": Object {}, + "nodeType": "heading-5", + }, + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "Heading 6", + }, + ], + "data": Object {}, + "nodeType": "heading-6", + }, + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "This is ", + }, + Object { + "data": Object {}, + "marks": Array [ + Object { + "type": "bold", + }, + ], + "nodeType": "text", + "value": "bold ", + }, + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "and ", + }, + Object { + "data": Object {}, + "marks": Array [ + Object { + "type": "italic", + }, + ], + "nodeType": "text", + "value": "italic", + }, + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": " and ", + }, + Object { + "data": Object {}, + "marks": Array [ + Object { + "type": "bold", + }, + Object { + "type": "italic", + }, + ], + "nodeType": "text", + "value": "both", + }, + ], + "data": Object {}, + "nodeType": "paragraph", + }, + Object { + "content": Array [ + Object { + "content": Array [ + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "Very", + }, + ], + "data": Object {}, + "nodeType": "paragraph", + }, + ], + "data": Object {}, + "nodeType": "list-item", + }, + Object { + "content": Array [ + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "useful", + }, + ], + "data": Object {}, + "nodeType": "paragraph", + }, + ], + "data": Object {}, + "nodeType": "list-item", + }, + Object { + "content": Array [ + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "list", + }, + ], + "data": Object {}, + "nodeType": "paragraph", + }, + ], + "data": Object {}, + "nodeType": "list-item", + }, + ], + "data": Object {}, + "nodeType": "unordered-list", + }, + Object { + "content": Array [ + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "This is a quote", + }, + ], + "data": Object {}, + "nodeType": "paragraph", + }, + ], + "data": Object {}, + "nodeType": "blockquote", + }, + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "Reference tests:", + }, + ], + "data": Object {}, + "nodeType": "heading-2", + }, + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "Inline Link: ", + }, + Object { + "content": Array [], + "data": Object { + "target": Object { + "sys": Object { + "id": "7oHxo6bs0us9wIkq27qdyK", + "linkType": "Entry", + "type": "Link", + }, + }, + }, + "nodeType": "embedded-entry-inline", + }, + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "", + }, + ], + "data": Object {}, + "nodeType": "paragraph", + }, + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "Link in list:", + }, + ], + "data": Object {}, + "nodeType": "paragraph", + }, + Object { + "content": Array [ + Object { + "content": Array [ + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "", + }, + Object { + "content": Array [], + "data": Object { + "target": Object { + "sys": Object { + "id": "6KpLS2NZyB3KAvDzWf4Ukh", + "linkType": "Entry", + "type": "Link", + }, + }, + }, + "nodeType": "embedded-entry-inline", + }, + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "", + }, + ], + "data": Object {}, + "nodeType": "paragraph", + }, + ], + "data": Object {}, + "nodeType": "list-item", + }, + ], + "data": Object {}, + "nodeType": "ordered-list", + }, + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "Embedded Entity:", + }, + ], + "data": Object {}, + "nodeType": "paragraph", + }, + Object { + "content": Array [], + "data": Object { + "target": Object { + "sys": Object { + "id": "7oHxo6bs0us9wIkq27qdyK", + "linkType": "Entry", + "type": "Link", + }, + }, + }, + "nodeType": "embedded-entry-block", + }, + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "", + }, + ], + "data": Object {}, + "nodeType": "paragraph", + }, + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "Embedded Asset:", + }, + ], + "data": Object {}, + "nodeType": "heading-2", + }, + Object { + "content": Array [], + "data": Object { + "target": Object { + "sys": Object { + "id": "4ZQrqcrTunWiuNaavhGYNT", + "linkType": "Asset", + "type": "Link", + }, + }, + }, + "nodeType": "embedded-asset-block", + }, + Object { + "content": Array [ + Object { + "data": Object {}, + "marks": Array [], + "nodeType": "text", + "value": "", + }, + ], + "data": Object {}, + "nodeType": "paragraph", + }, + ], + "data": Object {}, + "nodeType": "document", +} `; diff --git a/packages/gatsby-source-contentful/src/__tests__/__snapshots__/hooks.js.snap b/packages/gatsby-source-contentful/src/__tests__/__snapshots__/hooks.js.snap index b496f44c4a4dd..ec7854ee019fc 100644 --- a/packages/gatsby-source-contentful/src/__tests__/__snapshots__/hooks.js.snap +++ b/packages/gatsby-source-contentful/src/__tests__/__snapshots__/hooks.js.snap @@ -2,7 +2,7 @@ exports[`useContentfulImage constrained: width and aspectRatio 1`] = ` Object { - "backgroundColor": null, + "backgroundColor": undefined, "height": 337, "images": Object { "fallback": Object { @@ -21,7 +21,7 @@ https://images.ctfassets.net/foo/bar/baz/image.jpg?w=600&h=337 600w", exports[`useContentfulImage constrained: width and height 1`] = ` Object { - "backgroundColor": null, + "backgroundColor": undefined, "height": 480, "images": Object { "fallback": Object { @@ -40,7 +40,7 @@ https://images.ctfassets.net/foo/bar/baz/image.jpg?w=600&h=480 600w", exports[`useContentfulImage fixed: width and height 1`] = ` Object { - "backgroundColor": null, + "backgroundColor": undefined, "height": 480, "images": Object { "fallback": Object { @@ -57,7 +57,7 @@ Object { exports[`useContentfulImage fullWidth: aspect ratio 1`] = ` Object { - "backgroundColor": null, + "backgroundColor": undefined, "height": 0.5625, "images": Object { "fallback": Object { @@ -85,7 +85,7 @@ https://images.ctfassets.net/foo/bar/baz/image.jpg?w=4096&h=2304 4096w", exports[`useContentfulImage fullWidth: aspectRatio, maxWidth 1`] = ` Object { - "backgroundColor": null, + "backgroundColor": undefined, "height": 0.5625, "images": Object { "fallback": Object { @@ -113,7 +113,7 @@ https://images.ctfassets.net/foo/bar/baz/image.jpg?w=4096&h=2304 4096w", exports[`useContentfulImage fullWidth: width, height & maxWidth 1`] = ` Object { - "backgroundColor": null, + "backgroundColor": undefined, "height": 0.75, "images": Object { "fallback": Object { diff --git a/packages/gatsby-source-contentful/src/__tests__/a578a7a05e25c710a5d1ec09ab5469c5/347966-contentful-logo-wordmark-dark__1_-4cd185-original-1582664935__1_.png b/packages/gatsby-source-contentful/src/__tests__/a578a7a05e25c710a5d1ec09ab5469c5/347966-contentful-logo-wordmark-dark__1_-4cd185-original-1582664935__1_.png deleted file mode 100644 index 88648b0401005..0000000000000 Binary files a/packages/gatsby-source-contentful/src/__tests__/a578a7a05e25c710a5d1ec09ab5469c5/347966-contentful-logo-wordmark-dark__1_-4cd185-original-1582664935__1_.png and /dev/null differ diff --git a/packages/gatsby-source-contentful/src/__tests__/baec3798e6d3e16f3183ed0feb8a4023/347966-contentful-logo-wordmark-dark__1_-4cd185-original-1582664935__1_.png b/packages/gatsby-source-contentful/src/__tests__/baec3798e6d3e16f3183ed0feb8a4023/347966-contentful-logo-wordmark-dark__1_-4cd185-original-1582664935__1_.png deleted file mode 100644 index d213f2d823b09..0000000000000 Binary files a/packages/gatsby-source-contentful/src/__tests__/baec3798e6d3e16f3183ed0feb8a4023/347966-contentful-logo-wordmark-dark__1_-4cd185-original-1582664935__1_.png and /dev/null differ diff --git a/packages/gatsby-source-contentful/src/__tests__/create-schema-customization.js b/packages/gatsby-source-contentful/src/__tests__/create-schema-customization.js new file mode 100644 index 0000000000000..09a7f3cf829e7 --- /dev/null +++ b/packages/gatsby-source-contentful/src/__tests__/create-schema-customization.js @@ -0,0 +1,51 @@ +// @ts-check +import { createSchemaCustomization } from "../create-schema-customization" +import { contentTypes } from "../__fixtures__/content-types" +import { defaultOptions } from "../plugin-options" + +const createMockCache = () => { + return { + get: jest.fn(key => contentTypes), + } +} + +describe(`create-schema-customization`, () => { + const actions = { createTypes: jest.fn() } + const schema = { + buildObjectType: jest.fn(config => { + return { + config, + } + }), + buildInterfaceType: jest.fn(), + buildUnionType: jest.fn(), + } + const cache = createMockCache() + const reporter = { + info: jest.fn(), + verbose: jest.fn(), + panic: jest.fn(), + activityTimer: () => { + return { start: jest.fn(), end: jest.fn() } + }, + } + + beforeEach(() => { + cache.get.mockClear() + process.env.GATSBY_WORKER_ID = `mocked` + }) + + it(`builds schema based on Contentful Content Model`, async () => { + await createSchemaCustomization( + { schema, actions, reporter, cache }, + { ...defaultOptions, spaceId: `testSpaceId` } + ) + + expect(schema.buildObjectType).toHaveBeenCalled() + expect(schema.buildObjectType.mock.calls).toMatchSnapshot() + expect(schema.buildInterfaceType).toHaveBeenCalled() + expect(schema.buildInterfaceType.mock.calls).toMatchSnapshot() + expect(schema.buildUnionType).toHaveBeenCalled() + expect(schema.buildUnionType.mock.calls).toMatchSnapshot() + }) +}) diff --git a/packages/gatsby-source-contentful/src/__tests__/df7930af6b8c743c76c43b86ffe5af75/347966-contentful-logo-wordmark-dark (1)-4cd185-original-1582664935 (1).png b/packages/gatsby-source-contentful/src/__tests__/df7930af6b8c743c76c43b86ffe5af75/347966-contentful-logo-wordmark-dark (1)-4cd185-original-1582664935 (1).png deleted file mode 100644 index f6829df9c4a97..0000000000000 Binary files a/packages/gatsby-source-contentful/src/__tests__/df7930af6b8c743c76c43b86ffe5af75/347966-contentful-logo-wordmark-dark (1)-4cd185-original-1582664935 (1).png and /dev/null differ 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 132bf18831a7a..e6c496acc7f84 100644 --- a/packages/gatsby-source-contentful/src/__tests__/download-contentful-assets.js +++ b/packages/gatsby-source-contentful/src/__tests__/download-contentful-assets.js @@ -1,9 +1,6 @@ // @ts-check import { downloadContentfulAssets } from "../download-contentful-assets" import { createAssetNodes } from "../normalize" -import { createPluginConfig } from "../plugin-options" - -const pluginConfig = createPluginConfig({}) jest.mock(`gatsby-source-filesystem`, () => { return { @@ -16,6 +13,8 @@ jest.mock(`gatsby-source-filesystem`, () => { }) const reporter = { + info: jest.fn(), + warn: jest.fn(), createProgress: jest.fn(() => { return { start: jest.fn(), @@ -24,25 +23,44 @@ const reporter = { }), } +const getNode = jest.fn() +const touchNode = jest.fn() +const createNodeField = jest.fn() + +const mockedContentfulEntity = { + sys: { id: `mocked` }, +} + const fixtures = [ { - sys: { - id: `idJjXOxmNga8CSnQGEwTw`, - type: `Asset`, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), + id: `aa1beda4-b14a-50f5-89a8-222992a46a41`, + internal: { + owner: `gatsby-source-contentful`, + type: `ContentfulAsset`, }, fields: { + title: { "en-US": `TundraUS`, fr: `TundraFR` }, file: { "en-US": { url: `//images.ctfassets.net/testing/us-image.jpeg`, + details: { size: 123, image: { width: 123, height: 123 } }, + }, + fr: { + url: `//images.ctfassets.net/testing/fr-image.jpg`, + details: { size: 123, image: { width: 123, height: 123 } }, }, }, }, - title: { - "en-US": `TundraUS`, - fr: `TundraFR`, + sys: { + id: `idJjXOxmNga8CSnQGEwTw`, + type: `Asset`, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + space: mockedContentfulEntity, + environment: mockedContentfulEntity, + revision: 123, }, + metadata: { tags: [] }, }, ] @@ -52,15 +70,14 @@ describe(`downloadContentfulAssets`, () => { const createNodeId = jest.fn(id => id) const defaultLocale = `en-US` const locales = [{ code: `en-US` }, { code: `fr`, fallbackCode: `en-US` }] - const space = { - sys: { - id: `1234`, - }, - } + const space = mockedContentfulEntity const cache = { get: jest.fn(() => Promise.resolve(null)), set: jest.fn(() => Promise.resolve(null)), + directory: __dirname, + name: `mocked`, + del: jest.fn(() => Promise.resolve()), } const assetNodes = [] @@ -73,25 +90,23 @@ describe(`downloadContentfulAssets`, () => { defaultLocale, locales, space, - pluginConfig, })) ) } - await downloadContentfulAssets({ - actions: { touchNode: jest.fn() }, + await downloadContentfulAssets( + { createNodeId, cache, reporter, getNode }, + { createNode, touchNode, createNodeField }, assetNodes, - cache, - assetDownloadWorkers: 50, - reporter, - }) + 50 + ) assetNodes.forEach(n => { expect(cache.get).toHaveBeenCalledWith( - `contentful-asset-${n.contentful_id}-${n.node_locale}` + `contentful-asset-${n.sys.id}-${n.sys.locale}` ) expect(cache.set).toHaveBeenCalledWith( - `contentful-asset-${n.contentful_id}-${n.node_locale}`, + `contentful-asset-${n.sys.id}-${n.sys.locale}`, expect.anything() ) }) diff --git a/packages/gatsby-source-contentful/src/__tests__/e97c003d47d50d52c7e2759a2e4712b2/347966-contentful-logo-wordmark-dark__1_-4cd185-original-1582664935__1_.png b/packages/gatsby-source-contentful/src/__tests__/e97c003d47d50d52c7e2759a2e4712b2/347966-contentful-logo-wordmark-dark__1_-4cd185-original-1582664935__1_.png deleted file mode 100644 index aa5c716ab32d7..0000000000000 Binary files a/packages/gatsby-source-contentful/src/__tests__/e97c003d47d50d52c7e2759a2e4712b2/347966-contentful-logo-wordmark-dark__1_-4cd185-original-1582664935__1_.png and /dev/null differ diff --git a/packages/gatsby-source-contentful/src/__tests__/f6853f3e258800aa68463fa9565a9d80/347966-contentful-logo-wordmark-dark (1)-4cd185-original-1582664935 (1).png b/packages/gatsby-source-contentful/src/__tests__/f6853f3e258800aa68463fa9565a9d80/347966-contentful-logo-wordmark-dark (1)-4cd185-original-1582664935 (1).png deleted file mode 100644 index 1429459d19b2e..0000000000000 Binary files a/packages/gatsby-source-contentful/src/__tests__/f6853f3e258800aa68463fa9565a9d80/347966-contentful-logo-wordmark-dark (1)-4cd185-original-1582664935 (1).png and /dev/null differ diff --git a/packages/gatsby-source-contentful/src/__tests__/fetch-backoff.js b/packages/gatsby-source-contentful/src/__tests__/fetch-backoff.js index f9c951e172bd6..6ca289df23b9a 100644 --- a/packages/gatsby-source-contentful/src/__tests__/fetch-backoff.js +++ b/packages/gatsby-source-contentful/src/__tests__/fetch-backoff.js @@ -83,6 +83,12 @@ describe(`fetch-backoff`, () => { `/spaces/${options.spaceId}/environments/master/sync?initial=true&limit=444` ) .reply(200, { items: [] }) + // Tags + .get( + `/spaces/${options.spaceId}/environments/master/tags?skip=0&limit=1000` + ) + .reply(200, { items: [] }) + await fetchContent({ pluginConfig, reporter, syncToken: null }) expect(reporter.panic).not.toBeCalled() @@ -120,6 +126,11 @@ describe(`fetch-backoff`, () => { `/spaces/${options.spaceId}/environments/master/sync?initial=true&limit=1000` ) .reply(200, { items: [] }) + // Tags + .get( + `/spaces/${options.spaceId}/environments/master/tags?skip=0&limit=1000` + ) + .reply(200, { items: [] }) await fetchContent({ pluginConfig, reporter, syncToken: null }) 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 623402d38d2fb..9bd7a8741eb83 100644 --- a/packages/gatsby-source-contentful/src/__tests__/fetch-network-errors.js +++ b/packages/gatsby-source-contentful/src/__tests__/fetch-network-errors.js @@ -66,6 +66,11 @@ describe(`fetch-retry`, () => { `/spaces/${options.spaceId}/environments/master/sync?initial=true&limit=1000` ) .reply(200, { items: [] }) + // Tags + .get( + `/spaces/${options.spaceId}/environments/master/tags?skip=0&limit=1000` + ) + .reply(200, { items: [] }) await fetchContent({ pluginConfig, reporter, syncToken: null }) @@ -108,7 +113,7 @@ describe(`fetch-retry`, () => { const msg = expect(e.context.sourceMessage) msg.toEqual( expect.stringContaining( - `Fetching contentful data failed: ERR_BAD_RESPONSE 500 MockedContentfulError` + `Fetching contentful data failed: 500 MockedContentfulError Mocked message of Contentful error` ) ) msg.toEqual(expect.stringContaining(`Request ID: 123abc`)) diff --git a/packages/gatsby-source-contentful/src/__tests__/fetch.js b/packages/gatsby-source-contentful/src/__tests__/fetch.js index 017ff4b39ce56..bba516bf061a7 100644 --- a/packages/gatsby-source-contentful/src/__tests__/fetch.js +++ b/packages/gatsby-source-contentful/src/__tests__/fetch.js @@ -47,6 +47,9 @@ const mockClient = { }), } +mockClient.withAllLocales = mockClient +mockClient.withoutLinkResolution = mockClient + jest.mock(`contentful`, () => { return { createClient: jest.fn(() => mockClient), @@ -60,10 +63,6 @@ jest.mock(`../plugin-options`, () => { } }) -// jest so test output is not filled with contentful plugin logs -// @ts-ignore -global.console = { log: jest.fn(), time: jest.fn(), timeEnd: jest.fn() } - const proxyOption = { host: `localhost`, port: 9001, @@ -113,6 +112,9 @@ beforeEach(() => { global.process.exit.mockClear() reporter.panic.mockClear() mockClient.getLocales.mockClear() + mockClient.getContentTypes.mockClear() + mockClient.getSpace.mockClear() + mockClient.getTags.mockClear() // @ts-ignore formatPluginOptionsForCLI.mockClear() // @ts-ignore @@ -124,7 +126,7 @@ afterAll(() => { }) it(`calls contentful.createClient with expected params`, async () => { - await fetchContent({ pluginConfig, reporter, syncToken: null }) + await fetchContent({ pluginConfig, reporter, syncToken: `` }) expect(reporter.panic).not.toBeCalled() expect(createClient).toBeCalledWith( expect.objectContaining({ @@ -144,7 +146,7 @@ it(`calls contentful.createClient with expected params and default fallbacks`, a spaceId: `rocybtov1ozk`, }), reporter, - syncToken: null, + syncToken: ``, }) expect(reporter.panic).not.toBeCalled() @@ -170,7 +172,6 @@ it(`calls contentful.getContentTypes with default page limit`, async () => { expect(reporter.panic).not.toBeCalled() expect(mockClient.getContentTypes).toHaveBeenCalledWith({ limit: 1000, - order: `sys.createdAt`, skip: 0, }) }) @@ -188,13 +189,12 @@ it(`calls contentful.getContentTypes with custom plugin option page limit`, asyn expect(reporter.panic).not.toBeCalled() expect(mockClient.getContentTypes).toHaveBeenCalledWith({ limit: 50, - order: `sys.createdAt`, skip: 0, }) }) describe(`Tags feature`, () => { - it(`tags are disabled by default`, async () => { + it(`calls contentful.getTags`, async () => { await fetchContent({ pluginConfig: createPluginConfig({ accessToken: `6f35edf0db39085e9b9c19bd92943e4519c77e72c852d961968665f1324bfc94`, @@ -202,28 +202,12 @@ describe(`Tags feature`, () => { pageLimit: 50, }), reporter, - syncToken: null, - }) - - expect(reporter.panic).not.toBeCalled() - expect(mockClient.getTags).not.toBeCalled() - }) - it(`calls contentful.getTags when enabled`, async () => { - await fetchContent({ - pluginConfig: createPluginConfig({ - accessToken: `6f35edf0db39085e9b9c19bd92943e4519c77e72c852d961968665f1324bfc94`, - spaceId: `rocybtov1ozk`, - pageLimit: 50, - enableTags: true, - }), - reporter, - syncToken: null, + syncToken: ``, }) expect(reporter.panic).not.toBeCalled() expect(mockClient.getTags).toHaveBeenCalledWith({ limit: 50, - order: `sys.createdAt`, skip: 0, }) }) @@ -235,7 +219,7 @@ describe(`Displays troubleshooting tips and detailed plugin options on contentfu throw new Error(`error`) }) - await fetchContent({ pluginConfig, reporter, syncToken: null }) + await fetchContent({ pluginConfig, reporter, syncToken: `` }) expect(reporter.panic).toBeCalledWith( expect.objectContaining({ @@ -273,7 +257,7 @@ describe(`Displays troubleshooting tips and detailed plugin options on contentfu throw err }) - await fetchContent({ pluginConfig, reporter, syncToken: null }) + await fetchContent({ pluginConfig, reporter, syncToken: `` }) expect(reporter.panic).toBeCalledWith( expect.objectContaining({ @@ -314,7 +298,7 @@ describe(`Displays troubleshooting tips and detailed plugin options on contentfu await fetchContent({ pluginConfig: masterConfig, reporter, - syncToken: null, + syncToken: ``, }) expect(reporter.panic).toBeCalledWith( @@ -354,7 +338,7 @@ describe(`Displays troubleshooting tips and detailed plugin options on contentfu throw err }) - await fetchContent({ pluginConfig, reporter, syncToken: null }) + await fetchContent({ pluginConfig, reporter, syncToken: `` }) expect(reporter.panic).toBeCalledWith( expect.objectContaining({ @@ -395,7 +379,7 @@ describe(`Displays troubleshooting tips and detailed plugin options on contentfu throw err }) - await fetchContent({ pluginConfig, reporter, syncToken: null }) + await fetchContent({ pluginConfig, reporter, syncToken: `` }) expect(reporter.panic).toBeCalledWith( expect.objectContaining({ @@ -432,7 +416,6 @@ describe(`Displays troubleshooting tips and detailed plugin options on contentfu expect(mockClient.sync).toHaveBeenCalledWith({ initial: true, limit: 1000, - resolveLinks: false, }) mockClient.sync.mockClear() @@ -440,7 +423,6 @@ describe(`Displays troubleshooting tips and detailed plugin options on contentfu expect(mockClient.sync).toHaveBeenCalledWith({ nextSyncToken: `mocked`, - resolveLinks: false, }) }) }) diff --git a/packages/gatsby-source-contentful/src/__tests__/gatsby-node.js b/packages/gatsby-source-contentful/src/__tests__/gatsby-node.js index b366f3902c5fb..a2d90a773dcf3 100644 --- a/packages/gatsby-source-contentful/src/__tests__/gatsby-node.js +++ b/packages/gatsby-source-contentful/src/__tests__/gatsby-node.js @@ -6,13 +6,13 @@ import { sourceNodes, onPreInit, } from "../gatsby-node" -import { existingNodes, is, memoryNodeCounts } from "../backreferences" +import { existingNodes, is } from "../backreferences" import { fetchContent, fetchContentTypes } from "../fetch" -import { makeId } from "../normalize" +import { makeId, makeTypeName } from "../normalize" +import { defaultOptions } from "../plugin-options" import startersBlogFixture from "../__fixtures__/starter-blog-data" import richTextFixture from "../__fixtures__/rich-text-data" -import restrictedContentTypeFixture from "../__fixtures__/restricted-content-type" import unpublishedFieldDelivery from "../__fixtures__/unpublished-fields-delivery" import unpublishedFieldPreview from "../__fixtures__/unpublished-fields-preview" import preserveBackLinks from "../__fixtures__/preserve-back-links" @@ -27,7 +27,11 @@ jest.mock(`gatsby-core-utils`, () => { } }) -const defaultPluginOptions = { spaceId: `testSpaceId` } +/** @type {import("gatsby-plugin-utils/types").IPluginInfoOptions} */ +const defaultPluginOptions = { + ...defaultOptions, + spaceId: `testSpaceId`, +} // @ts-ignore fetchContentTypes.mockImplementation(() => @@ -96,16 +100,24 @@ describe(`gatsby-node`, () => { touchNode: jest.fn(), enableStatefulSourceNodes: jest.fn(), } + + const schemaCustomizationTypes = [] const schema = { - buildObjectType: jest.fn(() => { - return { - config: { - interfaces: [], - }, - } + buildObjectType: jest.fn(config => { + schemaCustomizationTypes.push({ + typeOrTypeDef: { config }, + plugin: { name: `gatsby-source-contentful` }, + }) + return { config } + }), + buildInterfaceType: jest.fn(config => { + schemaCustomizationTypes.push({ + typeOrTypeDef: { config }, + plugin: { name: `gatsby-source-contentful` }, + }) }), - buildInterfaceType: jest.fn(), } + let pluginStatus = {} const resetPluginStatus = () => { pluginStatus = {} @@ -114,6 +126,7 @@ describe(`gatsby-node`, () => { getState: jest.fn(() => { return { program: { directory: process.cwd() }, + schemaCustomization: { types: schemaCustomizationTypes }, status: { plugins: { [`gatsby-source-contentful`]: pluginStatus, @@ -122,10 +135,12 @@ describe(`gatsby-node`, () => { } }), } + const cache = createMockCache() const getCache = jest.fn(() => cache) const reporter = { info: jest.fn(), + warn: jest.fn(), verbose: jest.fn(), panic: jest.fn(), activityTimer: () => { @@ -180,6 +195,7 @@ describe(`gatsby-node`, () => { getCache, reporter, parentSpan, + schema, }, pluginOptions ) @@ -247,7 +263,7 @@ describe(`gatsby-node`, () => { type: value.sys.linkType || value.sys.type, }) ) - matchedObject[`${field}___NODE`] = linkId + matchedObject[field] = linkId if ( value.sys.type !== `Asset` && @@ -257,7 +273,7 @@ describe(`gatsby-node`, () => { references.set(linkId, {}) } - const referenceKey = `${currentContentType.name.toLowerCase()}___NODE` + const referenceKey = currentContentType.name.toLowerCase() const reference = references.get(linkId) const linkedNode = getNode(linkId) reference[referenceKey] = @@ -270,8 +286,8 @@ describe(`gatsby-node`, () => { break } case `Text`: { - const linkId = createNodeId(`${nodeId}${field}TextNode`) - matchedObject[`${field}___NODE`] = linkId + const linkId = createNodeId(`${nodeId}${field}MarkdownNode`) + matchedObject[field] = linkId break } default: @@ -283,13 +299,23 @@ describe(`gatsby-node`, () => { }) // update all matchedObjets with our backreferences - for (const [nodeId, value] of references) { - const matchedObject = { - ...nodeMap.get(nodeId), - ...value, + if (references.size) { + for (const [nodeId, value] of references) { + const matchedObject = nodeMap.get(nodeId) + if (matchedObject) { + matchedObject.linkedFrom = {} + + for (const k of Object.keys(value)) { + const typeName = makeTypeName(k) + if (matchedObject[`linkedFrom`][typeName]) { + matchedObject[`linkedFrom`][typeName].push(...value[k]) + } else { + matchedObject[`linkedFrom`][typeName] = value[k] + } + } + nodeMap.set(nodeId, matchedObject) + } } - - nodeMap.set(nodeId, matchedObject) } // match all entry nodes @@ -320,7 +346,7 @@ describe(`gatsby-node`, () => { // check if all references got removed references should be removed for (const value of currentNodeMap.values()) { Object.keys(value).forEach(field => { - if (field.endsWith(`___NODE`)) { + if (![`id`, `parent`].includes(field)) { expect([].concat(value[field])).not.toContain(nodeId) } }) @@ -395,7 +421,12 @@ describe(`gatsby-node`, () => { true, noLocaleFallback ) || ``, - file, + mimeType: file.contentType, + filename: file.fileName, + url: `https:` + file.url, + size: file.details.size, + width: file.details?.image?.width || null, + height: file.details?.image?.height || null, }) }) }) @@ -443,6 +474,7 @@ describe(`gatsby-node`, () => { cache.set.mockClear() reporter.info.mockClear() reporter.panic.mockClear() + schemaCustomizationTypes.length = 0 }) let hasImported = false @@ -622,12 +654,14 @@ describe(`gatsby-node`, () => { // @ts-ignore fetchContent.mockImplementationOnce(startersBlogFixture.initialSync) schema.buildObjectType.mockClear() - // @ts-ignore + await simulateGatsbyBuild({ - typePrefix: `CustomPrefix`, + ...defaultPluginOptions, + // @ts-ignore + contentTypePrefix: `CustomPrefix`, }) - expect(schema.buildObjectType).toHaveBeenCalledTimes(3) + expect(schema.buildObjectType).toHaveBeenCalledTimes(14) expect(schema.buildObjectType).toHaveBeenCalledWith( expect.objectContaining({ name: `CustomPrefixPerson`, @@ -638,27 +672,32 @@ describe(`gatsby-node`, () => { name: `CustomPrefixBlogPost`, }) ) - expect(schema.buildObjectType).toHaveBeenCalledWith( + + expect(schema.buildObjectType).not.toHaveBeenCalledWith( expect.objectContaining({ - name: `CustomPrefixAsset`, + name: `ContentfulContentTypePerson`, }) ) expect(schema.buildObjectType).not.toHaveBeenCalledWith( expect.objectContaining({ - name: `ContentfulPerson`, + name: `ContentfulContentTypeBlogPost`, }) ) - expect(schema.buildObjectType).not.toHaveBeenCalledWith( + expect(actions.createNode).not.toHaveBeenCalledWith( expect.objectContaining({ - name: `ContentfulBlogPost`, + internal: expect.objectContaining({ + type: `ContentfulContentTypeBlogPost`, + }), }) ) - expect(schema.buildObjectType).not.toHaveBeenCalledWith( + expect(actions.createNode).not.toHaveBeenCalledWith( expect.objectContaining({ - name: `ContentfulAsset`, + internal: expect.objectContaining({ + type: `ContentfulContentTypePerson`, + }), }) ) }) @@ -708,7 +747,7 @@ describe(`gatsby-node`, () => { createdBlogEntryIds.forEach(blogEntryId => { const blogEntry = getNode(blogEntryId) - expect(getNode(blogEntry[`author___NODE`])).toBeTruthy() + expect(getNode(blogEntry[`author`])).toBeTruthy() }) expect(actions.createNode).toHaveBeenCalledTimes(46) @@ -798,7 +837,7 @@ describe(`gatsby-node`, () => { updatedBlogEntryIds.forEach(blogEntryId => { const blogEntry = getNode(blogEntryId) expect(blogEntry.title).toBe(`Hello world 1234`) - expect(getNode(blogEntry[`author___NODE`])).toBeTruthy() + expect(getNode(blogEntry[`author`])).toBeTruthy() }) expect(actions.createNode).toHaveBeenCalledTimes(54) @@ -866,8 +905,8 @@ describe(`gatsby-node`, () => { for (const author of getNodes().filter( n => n.internal.type === `ContentfulPerson` )) { - expect(author[`blog post___NODE`].length).toEqual(3) - expect(author[`blog post___NODE`]).toEqual( + expect(author[`blog post`].length).toEqual(3) + expect(author[`blog post`]).toEqual( expect.not.arrayContaining([ makeId({ spaceId: removedBlogEntry.sys.space.sys.id, @@ -887,15 +926,15 @@ describe(`gatsby-node`, () => { // check if blog post exists removedBlogEntryIds.forEach(entryId => { const blogEntry = getNode(entryId) - authorIds.push(blogEntry[`author___NODE`]) + authorIds.push(blogEntry[`author`]) expect(blogEntry).not.toBeUndefined() }) for (const author of getNodes().filter( n => n.internal.type === `ContentfulPerson` )) { - expect(author[`blog post___NODE`].length).toEqual(4) - expect(author[`blog post___NODE`]).toEqual( + expect(author[`blog post`].length).toEqual(4) + expect(author[`blog post`]).toEqual( expect.arrayContaining([ makeId({ spaceId: removedBlogEntry.sys.space.sys.id, @@ -933,8 +972,8 @@ describe(`gatsby-node`, () => { for (const author of getNodes().filter( n => n.internal.type === `ContentfulPerson` )) { - expect(author[`blog post___NODE`].length).toEqual(3) - expect(author[`blog post___NODE`]).toEqual( + expect(author[`blog post`].length).toEqual(3) + expect(author[`blog post`]).toEqual( expect.not.arrayContaining([ makeId({ spaceId: removedBlogEntry.sys.space.sys.id, @@ -949,14 +988,14 @@ describe(`gatsby-node`, () => { // check if references are gone authorIds.forEach(authorId => { - expect(getNode(authorId)[`blog post___NODE`]).toEqual( + expect(getNode(authorId)[`blog post`]).toEqual( expect.not.arrayContaining(deletedEntryIds) ) }) - expect(actions.createNode).toHaveBeenCalledTimes(52) + expect(actions.createNode).toHaveBeenCalledTimes(48) expect(actions.deleteNode).toHaveBeenCalledTimes(2) - expect(actions.touchNode).toHaveBeenCalledTimes(2) + expect(actions.touchNode).toHaveBeenCalledTimes(0) expect(reporter.info.mock.calls).toMatchInlineSnapshot(` Array [ Array [ @@ -1040,9 +1079,9 @@ describe(`gatsby-node`, () => { locales ) - expect(actions.createNode).toHaveBeenCalledTimes(54) + expect(actions.createNode).toHaveBeenCalledTimes(48) expect(actions.deleteNode).toHaveBeenCalledTimes(2) - expect(actions.touchNode).toHaveBeenCalledTimes(2) + expect(actions.touchNode).toHaveBeenCalledTimes(0) expect(reporter.info.mock.calls).toMatchInlineSnapshot(` Array [ Array [ @@ -1073,7 +1112,7 @@ describe(`gatsby-node`, () => { `) }) - it(`stores rich text as raw with references attached`, async () => { + it(`stores rich text as JSON`, async () => { // @ts-ignore fetchContent.mockImplementationOnce(richTextFixture.initialSync) // @ts-ignore @@ -1085,15 +1124,53 @@ describe(`gatsby-node`, () => { const initNodes = getNodes() const homeNodes = initNodes.filter( - ({ contentful_id: id }) => id === `6KpLS2NZyB3KAvDzWf4Ukh` + ({ sys: { id } }) => id === `6KpLS2NZyB3KAvDzWf4Ukh` ) expect(homeNodes).toHaveLength(2) homeNodes.forEach(homeNode => { - expect(homeNode.content.references___NODE).toStrictEqual([ - ...new Set(homeNode.content.references___NODE), - ]) - expect(homeNode.content.references___NODE).toMatchSnapshot() + expect(homeNode.content).toMatchSnapshot() + }) + }) + + it(`should ignore markdown conversion when disabled in config`, async () => { + // @ts-ignore + fetchContent.mockImplementationOnce(startersBlogFixture.initialSync) + schema.buildObjectType.mockClear() + + await simulateGatsbyBuild({ + ...defaultPluginOptions, + enableMarkdownDetection: false, }) + expect(actions.createNode).toHaveBeenCalledTimes(18) + expect(actions.deleteNode).toHaveBeenCalledTimes(0) + expect(actions.touchNode).toHaveBeenCalledTimes(0) + + expect( + actions.createNode.mock.calls.filter( + call => call[0].internal.type == `ContentfulMarkdown` + ) + ).toHaveLength(0) + }) + + it(`should be able to create markdown nodes from a predefined list in config`, async () => { + // @ts-ignore + fetchContent.mockImplementationOnce(startersBlogFixture.initialSync) + schema.buildObjectType.mockClear() + + await simulateGatsbyBuild({ + ...defaultPluginOptions, + enableMarkdownDetection: false, + markdownFields: [[`blogPost`, [`body`]]], + }) + expect(actions.createNode).toHaveBeenCalledTimes(24) + expect(actions.deleteNode).toHaveBeenCalledTimes(0) + expect(actions.touchNode).toHaveBeenCalledTimes(0) + + expect( + actions.createNode.mock.calls.filter( + call => call[0].internal.type == `ContentfulMarkdown` + ) + ).toHaveLength(6) }) it(`is able to render unpublished fields in Delivery API`, async () => { @@ -1331,56 +1408,6 @@ describe(`gatsby-node`, () => { ) }) - it(`panics when response contains restricted content types`, async () => { - // @ts-ignore - fetchContent.mockImplementationOnce( - restrictedContentTypeFixture.initialSync - ) - // @ts-ignore - fetchContentTypes.mockImplementationOnce( - restrictedContentTypeFixture.contentTypeItems - ) - - await simulateGatsbyBuild() - - expect(reporter.panic).toBeCalledWith( - expect.objectContaining({ - context: { - sourceMessage: `Restricted ContentType name found. The name "reference" is not allowed.`, - }, - }) - ) - }) - - it(`panics when response contains content type Tag while enableTags is true`, async () => { - // @ts-ignore - fetchContent.mockImplementationOnce( - restrictedContentTypeFixture.initialSync - ) - const contentTypesWithTag = () => { - const manipulatedContentTypeItems = - restrictedContentTypeFixture.contentTypeItems() - manipulatedContentTypeItems[0].name = `Tag` - return manipulatedContentTypeItems - } - // @ts-ignore - fetchContentTypes.mockImplementationOnce(contentTypesWithTag) - - await simulateGatsbyBuild({ - spaceId: `mocked`, - enableTags: true, - useNameForId: true, - }) - - expect(reporter.panic).toBeCalledWith( - expect.objectContaining({ - context: { - sourceMessage: `Restricted ContentType name found. The name "tag" is not allowed.`, - }, - }) - ) - }) - it(`should preserve back reference when referencing entry wasn't touched`, async () => { // @ts-ignore fetchContentTypes.mockImplementation(preserveBackLinks.contentTypeItems) @@ -1394,33 +1421,33 @@ describe(`gatsby-node`, () => { await simulateGatsbyBuild() blogPostNodes = getNodes().filter( - node => node.internal.type === `ContentfulBlogPost` + node => node.internal.type === `ContentfulContentTypeBlogPost` ) blogCategoryNodes = getNodes().filter( - node => node.internal.type === `ContentfulBlogCategory` + node => node.internal.type === `ContentfulContentTypeBlogCategory` ) expect(blogPostNodes.length).toEqual(1) expect(blogCategoryNodes.length).toEqual(1) - expect(blogCategoryNodes[0][`blog post___NODE`]).toEqual([ - blogPostNodes[0].id, - ]) + expect( + blogCategoryNodes[0][`linkedFrom`][`ContentfulContentTypeBlogPost`] + ).toEqual([blogPostNodes[0].id]) expect(blogCategoryNodes[0][`title`]).toEqual(`CMS`) await simulateGatsbyBuild() blogPostNodes = getNodes().filter( - node => node.internal.type === `ContentfulBlogPost` + node => node.internal.type === `ContentfulContentTypeBlogPost` ) blogCategoryNodes = getNodes().filter( - node => node.internal.type === `ContentfulBlogCategory` + node => node.internal.type === `ContentfulContentTypeBlogCategory` ) expect(blogPostNodes.length).toEqual(1) expect(blogCategoryNodes.length).toEqual(1) - expect(blogCategoryNodes[0][`blog post___NODE`]).toEqual([ - blogPostNodes[0].id, - ]) + expect( + blogCategoryNodes[0][`linkedFrom`][`ContentfulContentTypeBlogPost`] + ).toEqual([blogPostNodes[0].id]) expect(blogCategoryNodes[0][`title`]).toEqual(`CMS edit #1`) }) @@ -1442,10 +1469,10 @@ describe(`gatsby-node`, () => { await simulateGatsbyBuild() blogPostNodes = getNodes().filter( - node => node.internal.type === `ContentfulBlogPost` + node => node.internal.type === `ContentfulContentTypeBlogPost` ) blogCategoryNodes = getNodes().filter( - node => node.internal.type === `ContentfulBlogCategory` + node => node.internal.type === `ContentfulContentTypeBlogCategory` ) blogCategoryChildNodes = blogCategoryNodes.flatMap(node => node.children.map(childId => getNode(childId)) @@ -1459,16 +1486,16 @@ describe(`gatsby-node`, () => { expect(blogCategoryNodes[0][`title`]).toEqual(`CMS`) // `body` field on child node is concrete value and not a link - expect(blogCategoryChildNodes[0][`body`]).toEqual(`cms`) - expect(blogCategoryChildNodes[0][`body___NODE`]).toBeUndefined() + expect(blogCategoryChildNodes[0][`raw`]).toEqual(`cms`) + expect(blogCategoryChildNodes[0][`raw___NODE`]).toBeUndefined() await simulateGatsbyBuild() blogPostNodes = getNodes().filter( - node => node.internal.type === `ContentfulBlogPost` + node => node.internal.type === `ContentfulContentTypeBlogPost` ) blogCategoryNodes = getNodes().filter( - node => node.internal.type === `ContentfulBlogCategory` + node => node.internal.type === `ContentfulContentTypeBlogCategory` ) blogCategoryChildNodes = blogCategoryNodes.flatMap(node => node.children.map(childId => getNode(childId)) @@ -1478,13 +1505,13 @@ describe(`gatsby-node`, () => { expect(blogCategoryNodes.length).toEqual(1) expect(blogCategoryChildNodes.length).toEqual(1) // backref was added when entries were linked - expect(blogCategoryNodes[0][`blog post___NODE`]).toEqual([ - blogPostNodes[0].id, - ]) + expect( + blogCategoryNodes[0].linkedFrom.ContentfulContentTypeBlogPost + ).toEqual([blogPostNodes[0].id]) expect(blogCategoryNodes[0][`title`]).toEqual(`CMS`) // `body` field on child node is concrete value and not a link - expect(blogCategoryChildNodes[0][`body`]).toEqual(`cms`) - expect(blogCategoryChildNodes[0][`body___NODE`]).toBeUndefined() + expect(blogCategoryChildNodes[0][`raw`]).toEqual(`cms`) + expect(blogCategoryChildNodes[0][`raw___NODE`]).toBeUndefined() }) }) diff --git a/packages/gatsby-source-contentful/src/__tests__/gatsby-plugin-image.js b/packages/gatsby-source-contentful/src/__tests__/gatsby-plugin-image.js index 033ea67336beb..9f75d655e25a2 100644 --- a/packages/gatsby-source-contentful/src/__tests__/gatsby-plugin-image.js +++ b/packages/gatsby-source-contentful/src/__tests__/gatsby-plugin-image.js @@ -1,12 +1,12 @@ +// @ts-check /** * @jest-environment node */ -// @ts-check import fs from "fs-extra" import { setPluginOptions } from "gatsby-plugin-sharp/plugin-options" import _ from "lodash" import { resolve } from "path" -import { setFieldsOnGraphQLNodeType } from "../extend-node-type" +import { createSchemaCustomization } from "../create-schema-customization" import { generateImageSource } from "../gatsby-plugin-image" import * as gatsbyCoreUtils from "gatsby-core-utils" import * as pluginSharp from "gatsby-plugin-sharp" @@ -38,31 +38,53 @@ const createMockCache = () => { const cache = createMockCache() const exampleImage = { - defaultLocale: `en-US`, - file: { - url: `//images.ctfassets.net:443/k8iqpp6u0ior/3ljGfnpegOnBTFGhV07iC1/94257340bda15ad4ca8462da3a8afa07/347966-contentful-logo-wordmark-dark__1_-4cd185-original-1582664935__1_.png`, - fileName: `347966-contentful-logo-wordmark-dark (1)-4cd185-original-1582664935 (1).png`, - contentType: `image/png`, - details: { - size: 123456, - image: { - width: `1646`, - height: `338`, - }, - }, + sys: { + locale: `en-US`, }, + url: `https://images.ctfassets.net:443/k8iqpp6u0ior/3ljGfnpegOnBTFGhV07iC1/94257340bda15ad4ca8462da3a8afa07/347966-contentful-logo-wordmark-dark__1_-4cd185-original-1582664935__1_.png`, + filename: `347966-contentful-logo-wordmark-dark (1)-4cd185-original-1582664935 (1).png`, + mimeType: `image/png`, + size: 123456, + width: `1646`, + height: `338`, internal: { contentDigest: `unique`, }, } describe(`gatsby-plugin-image`, () => { - let extendedNodeType + let contentfulAsset beforeAll(async () => { - extendedNodeType = await setFieldsOnGraphQLNodeType({ - type: { name: `ContentfulAsset` }, - cache, + const actions = { createTypes: jest.fn() } + const schema = { + buildObjectType: jest.fn(config => { + return { + config, + } + }), + buildInterfaceType: jest.fn(), + buildUnionType: jest.fn(), + } + const cache = createMockCache() + const reporter = { + info: jest.fn(), + verbose: jest.fn(), + panic: jest.fn(), + activityTimer: () => { + return { start: jest.fn(), end: jest.fn() } + }, + } + + await createSchemaCustomization( + { schema, actions, reporter, cache }, + { spaceId: `testSpaceId`, accessToken: `mocked` } + ) + + schema.buildObjectType.mock.calls.forEach(call => { + if (call[0].name === `ContentfulAsset`) { + contentfulAsset = call[0].fields + } }) }) @@ -76,7 +98,7 @@ describe(`gatsby-plugin-image`, () => { Object { "format": "webp", "height": 210, - "src": "https://test.png?w=420&h=210&fm=webp", + "src": "//test.png?w=420&h=210&fm=webp", "width": 420, } `) @@ -90,7 +112,7 @@ describe(`gatsby-plugin-image`, () => { Object { "format": "webp", "height": 210, - "src": "https://test.png?w=420&h=210&fm=webp&r=10", + "src": "//test.png?w=420&h=210&fm=webp&r=10", "width": 420, } `) @@ -104,7 +126,7 @@ describe(`gatsby-plugin-image`, () => { Object { "format": "webp", "height": 210, - "src": "https://test.png?w=420&h=210&fm=webp&r=max", + "src": "//test.png?w=420&h=210&fm=webp&r=max", "width": 420, } `) @@ -116,7 +138,7 @@ describe(`gatsby-plugin-image`, () => { Object { "format": "webp", "height": 210, - "src": "https://test.png?w=420&h=210&fm=webp", + "src": "//test.png?w=420&h=210&fm=webp", "width": 420, } `) @@ -129,12 +151,9 @@ describe(`gatsby-plugin-image`, () => { }) it(`default`, async () => { - const resp = await extendedNodeType.gatsbyImageData.resolve( + const resp = await contentfulAsset.gatsbyImageData.resolve( exampleImage, - // @ts-ignore - {}, - null, - null + {} ) expect(resp.images.sources[0].srcSet).toContain(`q=50`) expect(resp.images.sources[0].srcSet).toContain(`fm=webp`) @@ -145,7 +164,7 @@ describe(`gatsby-plugin-image`, () => { }) it(`force format`, async () => { - const resp = await extendedNodeType.gatsbyImageData.resolve( + const resp = await contentfulAsset.gatsbyImageData.resolve( exampleImage, // @ts-ignore { @@ -162,7 +181,7 @@ describe(`gatsby-plugin-image`, () => { }) it(`custom width`, async () => { - const resp = await extendedNodeType.gatsbyImageData.resolve( + const resp = await contentfulAsset.gatsbyImageData.resolve( exampleImage, // @ts-ignore { @@ -180,14 +199,12 @@ describe(`gatsby-plugin-image`, () => { expect(resp).toMatchSnapshot() }) it(`custom quality`, async () => { - const resp = await extendedNodeType.gatsbyImageData.resolve( + const resp = await contentfulAsset.gatsbyImageData.resolve( exampleImage, // @ts-ignore { quality: 90, - }, - null, - null + } ) expect(resp.images.sources[0].srcSet).toContain(`q=90`) expect(resp.images.sources[0].srcSet).toContain(`fm=webp`) @@ -196,14 +213,12 @@ describe(`gatsby-plugin-image`, () => { expect(resp).toMatchSnapshot() }) it(`layout fixed`, async () => { - const resp = await extendedNodeType.gatsbyImageData.resolve( + const resp = await contentfulAsset.gatsbyImageData.resolve( exampleImage, // @ts-ignore { layout: `fixed`, - }, - null, - null + } ) expect(resp.images.sources[0].srcSet).not.toContain(`,`) expect(resp.images.sources[0].sizes).not.toContain(`,`) @@ -212,14 +227,12 @@ describe(`gatsby-plugin-image`, () => { expect(resp).toMatchSnapshot() }) it(`layout full width`, async () => { - const resp = await extendedNodeType.gatsbyImageData.resolve( + const resp = await contentfulAsset.gatsbyImageData.resolve( exampleImage, // @ts-ignore { layout: `fullWidth`, - }, - null, - null + } ) expect(resp.images.sources[0].srcSet).toContain(`,`) expect(resp.images.sources[0].sizes).toEqual(`100vw`) @@ -229,28 +242,24 @@ describe(`gatsby-plugin-image`, () => { }) it(`placeholder blurred`, async () => { - const resp = await extendedNodeType.gatsbyImageData.resolve( + const resp = await contentfulAsset.gatsbyImageData.resolve( exampleImage, // @ts-ignore { placeholder: `blurred`, - }, - null, - null + } ) expect(resp.backgroundColor).toEqual(undefined) expect(resp.placeholder.fallback).toMatch(/^data:image\/png;base64,.+/) expect(resp).toMatchSnapshot() }) it(`placeholder traced svg (falls back to DOMINANT_COLOR)`, async () => { - const resp = await extendedNodeType.gatsbyImageData.resolve( + const resp = await contentfulAsset.gatsbyImageData.resolve( exampleImage, // @ts-ignore { placeholder: `tracedSVG`, - }, - null, - null + } ) expect(resp.backgroundColor).toEqual(`#080808`) expect(resp.placeholder).not.toBeDefined() @@ -266,12 +275,10 @@ describe(`gatsby-plugin-image`, () => { }, }) - const resp = await extendedNodeType.gatsbyImageData.resolve( + const resp = await contentfulAsset.gatsbyImageData.resolve( exampleImage, // @ts-ignore - {}, - null, - null + {} ) expect(resp.images.sources[0].srcSet).toContain(`q=42`) expect(resp.images.fallback.srcSet).toContain(`q=42`) @@ -289,12 +296,10 @@ describe(`gatsby-plugin-image`, () => { }, }) - const resp = await extendedNodeType.gatsbyImageData.resolve( + const resp = await contentfulAsset.gatsbyImageData.resolve( exampleImage, // @ts-ignore - {}, - null, - null + {} ) expect(resp.images.sources[0].srcSet).toContain(`q=42`) expect(resp.images.fallback.srcSet).toContain(`q=60`) @@ -311,12 +316,10 @@ describe(`gatsby-plugin-image`, () => { }, }) - const resp = await extendedNodeType.gatsbyImageData.resolve( + const resp = await contentfulAsset.gatsbyImageData.resolve( exampleImage, // @ts-ignore - {}, - null, - null + {} ) expect(resp.backgroundColor).toEqual(`#080808`) expect(resp.placeholder).not.toBeDefined() @@ -333,12 +336,10 @@ describe(`gatsby-plugin-image`, () => { }, }) - const resp = await extendedNodeType.gatsbyImageData.resolve( + const resp = await contentfulAsset.gatsbyImageData.resolve( exampleImage, // @ts-ignore - {}, - null, - null + {} ) expect(resp.placeholder.fallback).toMatch(/^data:image\/png;base64,.+/) expect(resp).toMatchSnapshot() @@ -352,12 +353,10 @@ describe(`gatsby-plugin-image`, () => { }, }) - const resp = await extendedNodeType.gatsbyImageData.resolve( + const resp = await contentfulAsset.gatsbyImageData.resolve( exampleImage, // @ts-ignore - {}, - null, - null + {} ) expect(resp.backgroundColor).toEqual(`#663399`) expect(resp).toMatchSnapshot() diff --git a/packages/gatsby-source-contentful/src/__tests__/hooks.js b/packages/gatsby-source-contentful/src/__tests__/hooks.js index 1f184dbe1d6bf..f3d325231c389 100644 --- a/packages/gatsby-source-contentful/src/__tests__/hooks.js +++ b/packages/gatsby-source-contentful/src/__tests__/hooks.js @@ -10,7 +10,7 @@ jest.mock(`react`, () => { describe(`useContentfulImage`, () => { const consoleWarnSpy = jest.spyOn(console, `warn`) - const image = { url: `//images.ctfassets.net/foo/bar/baz/image.jpg` } + const image = { url: `https://images.ctfassets.net/foo/bar/baz/image.jpg` } beforeEach(() => { consoleWarnSpy.mockClear() diff --git a/packages/gatsby-source-contentful/src/__tests__/image-helpers.js b/packages/gatsby-source-contentful/src/__tests__/image-helpers.js index 80b58997348f4..71d0ed6081202 100644 --- a/packages/gatsby-source-contentful/src/__tests__/image-helpers.js +++ b/packages/gatsby-source-contentful/src/__tests__/image-helpers.js @@ -9,14 +9,12 @@ describe(`Contentful Image API helpers`, () => { it(`allows you to create URls`, () => { expect( createUrl(`//images.contentful.com/dsf/bl.jpg`, { width: 100 }) - ).toMatchInlineSnapshot( - `"https://images.contentful.com/dsf/bl.jpg?w=100"` - ) + ).toMatchInlineSnapshot(`"//images.contentful.com/dsf/bl.jpg?w=100"`) }) it(`ignores options it doesn't understand`, () => { expect( createUrl(`//images.contentful.com/dsf/bl.jpg`, { happiness: 100 }) - ).toMatchInlineSnapshot(`"https://images.contentful.com/dsf/bl.jpg?"`) + ).toMatchInlineSnapshot(`"//images.contentful.com/dsf/bl.jpg?"`) }) }) }) diff --git a/packages/gatsby-source-contentful/src/__tests__/normalize.js b/packages/gatsby-source-contentful/src/__tests__/normalize.js index 2e1f2be440e17..102727a43b652 100644 --- a/packages/gatsby-source-contentful/src/__tests__/normalize.js +++ b/packages/gatsby-source-contentful/src/__tests__/normalize.js @@ -1,11 +1,11 @@ // @ts-check import { buildEntryList, - buildResolvableSet, + buildFallbackChain, buildForeignReferenceMap, - createNodesForContentType, + buildResolvableSet, createAssetNodes, - buildFallbackChain, + createNodesForContentType, getLocalizedField, makeId, } from "../normalize" @@ -19,19 +19,10 @@ const { space, } = require(`./data.json`) -const conflictFieldPrefix = `contentful_test` -// restrictedNodeFields from here https://www.gatsbyjs.com/docs/node-interface/ -const restrictedNodeFields = [ - `id`, - `children`, - `contentful_id`, - `parent`, - `fields`, - `internal`, -] - -const pluginConfig = createPluginConfig({}) - +const pluginConfig = createPluginConfig({ + accessToken: `mocked`, + spaceId: `mocked`, +}) const unstable_createNodeManifest = jest.fn() // Counts the created nodes per node type @@ -48,7 +39,12 @@ function countCreatedNodeTypesFromMock(mock) { return nodeTypeCounts } +const createNodeId = jest.fn(id => id) + describe(`generic`, () => { + beforeEach(() => { + createNodeId.mockClear() + }) it(`builds entry list`, () => { const entryList = buildEntryList({ currentSyncData, @@ -82,12 +78,15 @@ describe(`generic`, () => { const resolvable = buildResolvableSet({ assets: currentSyncData.assets, entryList, + spaceId: space.sys.id, }) const allNodes = [...currentSyncData.entries, ...currentSyncData.assets] allNodes.forEach(node => - expect(resolvable).toContain(`${node.sys.id}___${node.sys.type}`) + expect(resolvable).toContain( + `${space.sys.id}___${node.sys.id}___${node.sys.type}` + ) ) }) it(`builds foreignReferenceMap`, () => { @@ -99,6 +98,7 @@ describe(`generic`, () => { const resolvable = buildResolvableSet({ assets: currentSyncData.assets, entryList, + spaceId: space.sys.id, }) const foreignReferenceMapState = buildForeignReferenceMap({ @@ -110,23 +110,24 @@ describe(`generic`, () => { useNameForId: true, previousForeignReferenceMapState: undefined, deletedEntries: [], + createNodeId, }) const referenceKeys = Object.keys(foreignReferenceMapState.backLinks) const expectedReferenceKeys = [ - `2Y8LhXLnYAYqKCGEWG4EKI___Asset`, - `3wtvPBbBjiMKqKKga8I2Cu___Asset`, - `4LgMotpNF6W20YKmuemW0a___Entry`, - `4zj1ZOfHgQ8oqgaSKm4Qo2___Asset`, - `6m5AJ9vMPKc8OUoQeoCS4o___Asset`, - `6t4HKjytPi0mYgs240wkG___Asset`, - `7LAnCobuuWYSqks6wAwY2a___Entry`, - `10TkaLheGeQG6qQGqWYqUI___Asset`, - `24DPGBDeGEaYy8ms4Y8QMQ___Entry`, - `651CQ8rLoIYCeY6G0QG22q___Entry`, - `JrePkDVYomE8AwcuCUyMi___Entry`, - `KTRF62Q4gg60q6WCsWKw8___Asset`, - `wtrHxeu3zEoEce2MokCSi___Asset`, - `Xc0ny7GWsMEMCeASWO2um___Asset`, + `rocybtov1ozk___2Y8LhXLnYAYqKCGEWG4EKI___Asset`, + `rocybtov1ozk___3wtvPBbBjiMKqKKga8I2Cu___Asset`, + `rocybtov1ozk___4LgMotpNF6W20YKmuemW0a___Entry`, + `rocybtov1ozk___4zj1ZOfHgQ8oqgaSKm4Qo2___Asset`, + `rocybtov1ozk___6m5AJ9vMPKc8OUoQeoCS4o___Asset`, + `rocybtov1ozk___6t4HKjytPi0mYgs240wkG___Asset`, + `rocybtov1ozk___7LAnCobuuWYSqks6wAwY2a___Entry`, + `rocybtov1ozk___10TkaLheGeQG6qQGqWYqUI___Asset`, + `rocybtov1ozk___24DPGBDeGEaYy8ms4Y8QMQ___Entry`, + `rocybtov1ozk___651CQ8rLoIYCeY6G0QG22q___Entry`, + `rocybtov1ozk___JrePkDVYomE8AwcuCUyMi___Entry`, + `rocybtov1ozk___KTRF62Q4gg60q6WCsWKw8___Asset`, + `rocybtov1ozk___wtrHxeu3zEoEce2MokCSi___Asset`, + `rocybtov1ozk___Xc0ny7GWsMEMCeASWO2um___Asset`, ] expect(referenceKeys).toHaveLength(expectedReferenceKeys.length) expect(referenceKeys).toEqual(expect.arrayContaining(expectedReferenceKeys)) @@ -135,10 +136,10 @@ describe(`generic`, () => { expect(resolvable).toContain(referenceId) let expectedLength = 1 - if (referenceId === `651CQ8rLoIYCeY6G0QG22q___Entry`) { + if (referenceId === `rocybtov1ozk___651CQ8rLoIYCeY6G0QG22q___Entry`) { expectedLength = 2 } - if (referenceId === `7LAnCobuuWYSqks6wAwY2a___Entry`) { + if (referenceId === `rocybtov1ozk___7LAnCobuuWYSqks6wAwY2a___Entry`) { expectedLength = 3 } expect(foreignReferenceMapState.backLinks[referenceId]).toHaveLength( @@ -158,6 +159,7 @@ describe(`Process contentful data (by name)`, () => { const resolvable = buildResolvableSet({ assets: currentSyncData.assets, entryList, + spaceId: space.sys.id, }) const foreignReferenceMapState = buildForeignReferenceMap({ @@ -169,17 +171,20 @@ describe(`Process contentful data (by name)`, () => { useNameForId: true, previousForeignReferenceMapState: undefined, deletedEntries: [], + createNodeId, }) expect( - foreignReferenceMapState.backLinks[`24DPGBDeGEaYy8ms4Y8QMQ___Entry`][0] - .name - ).toBe(`product___NODE`) + foreignReferenceMapState.backLinks[ + `rocybtov1ozk___24DPGBDeGEaYy8ms4Y8QMQ___Entry` + ][0].name + ).toBe(`ContentfulContentTypeProduct`) expect( - foreignReferenceMapState.backLinks[`2Y8LhXLnYAYqKCGEWG4EKI___Asset`][0] - .name - ).toBe(`brand___NODE`) + foreignReferenceMapState.backLinks[ + `rocybtov1ozk___2Y8LhXLnYAYqKCGEWG4EKI___Asset` + ][0].name + ).toBe(`ContentfulContentTypeBrand`) }) it(`creates nodes for each entry`, () => { @@ -191,6 +196,7 @@ describe(`Process contentful data (by name)`, () => { const resolvable = buildResolvableSet({ assets: currentSyncData.assets, entryList, + spaceId: space.sys.id, }) const foreignReferenceMap = buildForeignReferenceMap({ @@ -202,16 +208,14 @@ describe(`Process contentful data (by name)`, () => { useNameForId: true, previousForeignReferenceMapState: undefined, deletedEntries: [], + createNodeId, }) const createNode = jest.fn() - const createNodeId = jest.fn(id => id) const getNode = jest.fn(() => undefined) // All nodes are new contentTypeItems.forEach((contentTypeItem, i) => { createNodesForContentType({ contentTypeItem, - restrictedNodeFields, - conflictFieldPrefix, entries: entryList[i], createNode, createNodeId, @@ -229,40 +233,32 @@ describe(`Process contentful data (by name)`, () => { const nodeTypeCounts = countCreatedNodeTypesFromMock(createNode.mock) - expect(Object.keys(nodeTypeCounts)).toHaveLength(15) - expect(nodeTypeCounts).toEqual( expect.objectContaining({ + ContentfulContentType: contentTypeItems.length, + // Generated markdown child entities + ContentfulMarkdown: 38, // 3 Brand Contentful entries - ContentfulBrand: 6, - contentfulBrandCompanyDescriptionTextNode: 6, - contentfulBrandCompanyNameTextNode: 6, + ContentfulContentTypeBrand: 6, // 2 Category Contentful entries - ContentfulCategory: 4, - contentfulCategoryCategoryDescriptionTextNode: 4, - contentfulCategoryTitleTextNode: 4, - ContentfulContentType: contentTypeItems.length, + ContentfulContentTypeCategory: 4, // 1 JSON Test Contentful entry - ContentfulJsonTest: 2, - contentfulJsonTestJsonStringTestJsonNode: 2, - contentfulJsonTestJsonTestJsonNode: 2, + ContentfulContentTypeJsonTest: 2, // 4 Product Contentful entries - ContentfulProduct: 8, - contentfulProductProductDescriptionTextNode: 8, - contentfulProductProductNameTextNode: 8, + ContentfulContentTypeProduct: 8, // 1 Remark Test Contentful entry - ContentfulRemarkTest: 2, - contentfulRemarkTestContentTextNode: 2, + ContentfulContentTypeRemarkTest: 2, }) ) + expect(Object.keys(nodeTypeCounts)).toHaveLength(7) + // Relevant to compare to compare warm and cold situation - expect(createNode.mock.calls.length).toBe(69) // "cold build entries" count + expect(createNode.mock.calls.length).toBe(65) // "cold build entries" count }) it(`creates nodes for each asset`, () => { const createNode = jest.fn(() => Promise.resolve()) - const createNodeId = jest.fn(id => id) const assets = currentSyncData.assets assets.forEach(assetItem => { createAssetNodes({ @@ -272,7 +268,6 @@ describe(`Process contentful data (by name)`, () => { defaultLocale, locales, space, - pluginConfig, }) }) const nodeTypeCounts = countCreatedNodeTypesFromMock(createNode.mock) @@ -292,6 +287,7 @@ describe(`Process existing mutated nodes in warm build`, () => { const resolvable = buildResolvableSet({ assets: currentSyncData.assets, entryList, + spaceId: space.sys.id, }) const foreignReferenceMap = buildForeignReferenceMap({ @@ -303,10 +299,10 @@ describe(`Process existing mutated nodes in warm build`, () => { useNameForId: true, previousForeignReferenceMapState: undefined, deletedEntries: [], + createNodeId, }) const createNode = jest.fn() - const createNodeId = jest.fn(id => id) let doReturn = true const getNode = jest.fn(id => { if (doReturn) { @@ -317,7 +313,7 @@ describe(`Process existing mutated nodes in warm build`, () => { return { id, internal: { - contentDigest: entryList[0][0].sys.updatedAt + `changed`, + contentDigest: entryList[0][0].sys.publishedAt + `changed`, }, } } @@ -327,8 +323,6 @@ describe(`Process existing mutated nodes in warm build`, () => { contentTypeItems.forEach((contentTypeItem, i) => { createNodesForContentType({ contentTypeItem, - restrictedNodeFields, - conflictFieldPrefix, entries: entryList[i], createNode, createNodeId, @@ -346,36 +340,28 @@ describe(`Process existing mutated nodes in warm build`, () => { const nodeTypeCounts = countCreatedNodeTypesFromMock(createNode.mock) - expect(Object.keys(nodeTypeCounts)).toHaveLength(15) - expect(nodeTypeCounts).toEqual( expect.objectContaining({ + ContentfulContentType: contentTypeItems.length, + // Markdown child entities + ContentfulMarkdown: 38, // 3 Brand Contentful entries - ContentfulBrand: 6, - contentfulBrandCompanyDescriptionTextNode: 6, - contentfulBrandCompanyNameTextNode: 6, + ContentfulContentTypeBrand: 6, // 2 Category Contentful entries - ContentfulCategory: 4, - contentfulCategoryCategoryDescriptionTextNode: 4, - contentfulCategoryTitleTextNode: 4, - ContentfulContentType: contentTypeItems.length, + ContentfulContentTypeCategory: 4, // 1 JSON Test Contentful entry - ContentfulJsonTest: 2, - contentfulJsonTestJsonStringTestJsonNode: 2, - contentfulJsonTestJsonTestJsonNode: 2, + ContentfulContentTypeJsonTest: 2, // 4 Product Contentful entries - ContentfulProduct: 8, - contentfulProductProductDescriptionTextNode: 8, - contentfulProductProductNameTextNode: 8, + ContentfulContentTypeProduct: 8, // 1 Remark Test Contentful entry - ContentfulRemarkTest: 2, - contentfulRemarkTestContentTextNode: 2, + ContentfulContentTypeRemarkTest: 2, }) ) + expect(Object.keys(nodeTypeCounts)).toHaveLength(7) // Relevant to compare to compare warm and cold situation // This number ought to be the same as the cold build - expect(createNode.mock.calls.length).toBe(69) // "warm build where entry was changed" count + expect(createNode.mock.calls.length).toBe(65) // "warm build where entry was changed" count }) }) @@ -388,6 +374,7 @@ describe(`Process contentful data (by id)`, () => { const resolvable = buildResolvableSet({ assets: currentSyncData.assets, entryList, + spaceId: space.sys.id, }) const foreignReferenceMapState = buildForeignReferenceMap({ contentTypeItems, @@ -398,17 +385,20 @@ describe(`Process contentful data (by id)`, () => { useNameForId: false, previousForeignReferenceMapState: undefined, deletedEntries: [], + createNodeId, }) expect( - foreignReferenceMapState.backLinks[`24DPGBDeGEaYy8ms4Y8QMQ___Entry`][0] - .name - ).toBe(`2pqfxujwe8qsykum0u6w8m___NODE`) + foreignReferenceMapState.backLinks[ + `rocybtov1ozk___24DPGBDeGEaYy8ms4Y8QMQ___Entry` + ][0].name + ).toBe(`ContentfulContentType2PqfXuJwE8QSyKuM0U6W8M`) expect( - foreignReferenceMapState.backLinks[`2Y8LhXLnYAYqKCGEWG4EKI___Asset`][0] - .name - ).toBe(`sfztzbsum8coewygeuyes___NODE`) + foreignReferenceMapState.backLinks[ + `rocybtov1ozk___2Y8LhXLnYAYqKCGEWG4EKI___Asset` + ][0].name + ).toBe(`ContentfulContentTypeSFzTZbSuM8CoEwygeUYes`) }) it(`creates nodes for each entry`, () => { @@ -419,6 +409,7 @@ describe(`Process contentful data (by id)`, () => { const resolvable = buildResolvableSet({ assets: currentSyncData.assets, entryList, + spaceId: space.sys.id, }) const foreignReferenceMap = buildForeignReferenceMap({ contentTypeItems, @@ -429,16 +420,14 @@ describe(`Process contentful data (by id)`, () => { useNameForId: false, previousForeignReferenceMapState: undefined, deletedEntries: [], + createNodeId, }) const createNode = jest.fn() - const createNodeId = jest.fn(id => id) const getNode = jest.fn(() => undefined) // All nodes are new contentTypeItems.forEach((contentTypeItem, i) => { createNodesForContentType({ contentTypeItem, - restrictedNodeFields, - conflictFieldPrefix, entries: entryList[i], createNode, createNodeId, @@ -455,32 +444,23 @@ describe(`Process contentful data (by id)`, () => { }) const nodeTypeCounts = countCreatedNodeTypesFromMock(createNode.mock) - expect(Object.keys(nodeTypeCounts)).toHaveLength(15) - expect(nodeTypeCounts).toEqual( expect.objectContaining({ + ContentfulContentType: contentTypeItems.length, // 3 Brand Contentful entries - ContentfulSFzTZbSuM8CoEwygeUYes: 6, - contentfulSFzTZbSuM8CoEwygeUYesCompanyDescriptionTextNode: 6, - contentfulSFzTZbSuM8CoEwygeUYesCompanyNameTextNode: 6, + ContentfulContentTypeSFzTZbSuM8CoEwygeUYes: 6, // 2 Category Contentful entries - Contentful6XwpTaSiiI2Ak2Ww0Oi6Qa: 4, - contentful6XwpTaSiiI2Ak2Ww0Oi6QaCategoryDescriptionTextNode: 4, - contentful6XwpTaSiiI2Ak2Ww0Oi6QaTitleTextNode: 4, - ContentfulContentType: contentTypeItems.length, + ContentfulContentType6XwpTaSiiI2Ak2Ww0Oi6Qa: 4, // 1 JSON Test Contentful entry - ContentfulJsonTest: 2, - contentfulJsonTestJsonStringTestJsonNode: 2, - contentfulJsonTestJsonTestJsonNode: 2, + ContentfulContentTypeJsonTest: 2, // 4 Product Contentful entries - Contentful2PqfXuJwE8QSyKuM0U6W8M: 8, - contentful2PqfXuJwE8QSyKuM0U6W8MProductDescriptionTextNode: 8, - contentful2PqfXuJwE8QSyKuM0U6W8MProductNameTextNode: 8, + ContentfulContentType2PqfXuJwE8QSyKuM0U6W8M: 8, // 1 Remark Test Contentful entry - ContentfulRemarkTest: 2, - contentfulRemarkTestContentTextNode: 2, + ContentfulContentTypeRemarkTest: 2, }) ) + + expect(Object.keys(nodeTypeCounts)).toHaveLength(7) }) }) diff --git a/packages/gatsby-source-contentful/src/__tests__/rich-text.js b/packages/gatsby-source-contentful/src/__tests__/rich-text.js index 46ad833a3bd55..9df80a17a5a7a 100644 --- a/packages/gatsby-source-contentful/src/__tests__/rich-text.js +++ b/packages/gatsby-source-contentful/src/__tests__/rich-text.js @@ -5,12 +5,13 @@ // @ts-check import React from "react" import { render } from "@testing-library/react" -import { renderRichText } from "gatsby-source-contentful/rich-text" +import { renderRichText } from "../rich-text" import { BLOCKS, INLINES } from "@contentful/rich-text-types" +import { getRichTextEntityLinks } from "@contentful/rich-text-links" import { initialSync } from "../__fixtures__/rich-text-data" import { cloneDeep } from "lodash" -const raw = JSON.stringify({ +const json = { nodeType: `document`, data: {}, content: [ @@ -406,120 +407,132 @@ const raw = JSON.stringify({ data: {}, }, ], -}) +} const fixtures = initialSync().currentSyncData +const fixturesEntriesMap = new Map() +const fixturesAssetsMap = new Map() -const references = [ - ...fixtures.entries.map(entity => { - return { - sys: entity.sys, - contentful_id: entity.sys.id, - __typename: `ContentfulContent`, - ...entity.fields, - } - }), - ...fixtures.assets.map(entity => { - return { - sys: entity.sys, - contentful_id: entity.sys.id, - __typename: `ContentfulAsset`, - ...entity.fields, - } - }), -] +fixtures.entries.forEach(entity => + fixturesEntriesMap.set(entity.sys.id, { sys: entity.sys, ...entity.fields }) +) +fixtures.assets.forEach(entity => + fixturesAssetsMap.set(entity.sys.id, { sys: entity.sys, ...entity.fields }) +) + +const links = { + assets: { + block: getRichTextEntityLinks(json, `embedded-asset-block`)[`Asset`].map( + entity => fixturesAssetsMap.get(entity.id) + ), + hyperlink: getRichTextEntityLinks(json, `asset-hyperlink`)[`Asset`].map( + entity => fixturesAssetsMap.get(entity.id) + ), + }, + entries: { + inline: getRichTextEntityLinks(json, `embedded-entry-inline`)[`Entry`].map( + entity => fixturesEntriesMap.get(entity.id) + ), + block: getRichTextEntityLinks(json, `embedded-entry-block`)[`Entry`].map( + entity => fixturesEntriesMap.get(entity.id) + ), + hyperlink: getRichTextEntityLinks(json, `entry-hyperlink`)[`Entry`].map( + entity => fixturesEntriesMap.get(entity.id) + ), + }, +} describe(`rich text`, () => { test(`renders with default options`, () => { const { container } = render( - <> - {renderRichText({ - raw: cloneDeep(raw), - references: cloneDeep(references), - })} - + <>{renderRichText({ json: cloneDeep(json), links: cloneDeep(links) })} ) expect(container).toMatchSnapshot() }) test(`renders with custom options`, () => { - const options = { - renderNode: { - [INLINES.EMBEDDED_ENTRY]: node => { - if (!node.data.target) { - return ( - - Unresolved INLINE ENTRY: {JSON.stringify(node, null, 2)} - - ) - } - return ( - - Resolved inline Entry ({node.data.target.contentful_id}) - - ) - }, - [INLINES.ENTRY_HYPERLINK]: node => { - if (!node.data.target) { - return ( - - Unresolved ENTRY HYPERLINK: {JSON.stringify(node, null, 2)} - - ) - } - return ( - - Resolved entry Hyperlink ({node.data.target.contentful_id}) - - ) - }, - [INLINES.ASSET_HYPERLINK]: node => { - if (!node.data.target) { - return ( - - Unresolved ASSET HYPERLINK: {JSON.stringify(node, null, 2)} - - ) - } - return ( - - Resolved asset Hyperlink ({node.data.target.contentful_id}) - - ) - }, - [BLOCKS.EMBEDDED_ENTRY]: node => { - if (!node.data.target) { + const makeOptions = ({ + assetBlockMap, + assetHyperlinkMap, + entryBlockMap, + entryInlineMap, + entryHyperlinkMap, + }) => { + return { + renderNode: { + [INLINES.EMBEDDED_ENTRY]: node => { + const entry = entryInlineMap.get(node?.data?.target?.sys.id) + if (!entry) { + return ( + + Unresolved INLINE ENTRY:{` `} + {JSON.stringify({ node, entryInlineMap }, null, 2)} + + ) + } + return Resolved inline Entry ({entry.sys.id}) + }, + [INLINES.ENTRY_HYPERLINK]: node => { + const entry = entryHyperlinkMap.get(node?.data?.target?.sys.id) + if (!entry) { + return ( + + Unresolved ENTRY HYPERLINK: {JSON.stringify(node, null, 2)} + + ) + } + return Resolved entry Hyperlink ({entry.sys.id}) + }, + [INLINES.ASSET_HYPERLINK]: node => { + const entry = assetHyperlinkMap.get(node?.data?.target?.sys.id) + if (!entry) { + return ( + + Unresolved ASSET HYPERLINK: {JSON.stringify(node, null, 2)} + + ) + } + return Resolved asset Hyperlink ({entry.sys.id}) + }, + [BLOCKS.EMBEDDED_ENTRY]: node => { + const entry = entryBlockMap.get(node?.data?.target?.sys.id) + if (!entry) { + return ( +
    + Unresolved ENTRY !!!!": {JSON.stringify(node, null, 2)} +
    + ) + } return ( -
    Unresolved ENTRY !!!!": {JSON.stringify(node, null, 2)}
    +

    + Resolved embedded Entry: {entry.title[`en-US`]} ({entry.sys.id}) +

    ) - } - return ( -

    - Resolved embedded Entry: {node.data.target.title[`en-US`]} ( - {node.data.target.contentful_id}) -

    - ) - }, - [BLOCKS.EMBEDDED_ASSET]: node => { - if (!node.data.target) { + }, + [BLOCKS.EMBEDDED_ASSET]: node => { + const entry = assetBlockMap.get(node?.data?.target?.sys.id) + if (!entry) { + return ( +
    + Unresolved ASSET !!!!": {JSON.stringify(node, null, 2)} +
    + ) + } return ( -
    Unresolved ASSET !!!!": {JSON.stringify(node, null, 2)}
    +

    + Resolved embedded Asset: {entry.title[`en-US`]} ({entry.sys.id}) +

    ) - } - return ( -

    - Resolved embedded Asset: {node.data.target.title[`en-US`]} ( - {node.data.target.contentful_id}) -

    - ) + }, }, - }, + } } + const { container } = render( <> {renderRichText( - { raw: cloneDeep(raw), references: cloneDeep(references) }, - options + { json: cloneDeep(json), links: cloneDeep(links) }, + makeOptions )} ) diff --git a/packages/gatsby-source-contentful/src/backreferences.js b/packages/gatsby-source-contentful/src/backreferences.ts similarity index 55% rename from packages/gatsby-source-contentful/src/backreferences.js rename to packages/gatsby-source-contentful/src/backreferences.ts index c5feef0c54aab..f505920ad0bc5 100644 --- a/packages/gatsby-source-contentful/src/backreferences.js +++ b/packages/gatsby-source-contentful/src/backreferences.ts @@ -1,10 +1,16 @@ -// @ts-check import { hasFeature } from "gatsby-plugin-utils/index" -import { getDataStore } from "gatsby/dist/datastore" import { untilNextEventLoopTick } from "./utils" +import type { IContentfulEntry } from "./types/contentful" + +// @ts-ignore this is not available (yet) in typegen phase +import { getDataStore } from "gatsby/dist/datastore" +// @ts-ignore this is not available (yet) in typegen phase +import type { IGatsbyNode } from "gatsby/dist/redux/types" +import type { Actions, Node, NodePluginArgs } from "gatsby" +import type { IProcessedPluginOptions } from "./types/plugin" // Array of all existing Contentful nodes. Make it global and incrementally update it because it's hella slow to recreate this on every data update for large sites. -export const existingNodes = new Map() +export const existingNodes = new Map() let allNodesLoopCount = 0 @@ -13,17 +19,36 @@ export const is = { firstSourceNodesCallOfCurrentNodeProcess: true, } +interface IMemoryNodesCountsBySysType { + Asset: number + Entry: number +} + +const memoryNodeCountsBySysType: IMemoryNodesCountsBySysType = { + Asset: 0, + Entry: 0, +} + export async function getExistingCachedNodes({ actions, getNode, pluginConfig, -}) { +}: { + actions: Actions + getNode: NodePluginArgs["getNode"] + pluginConfig: IProcessedPluginOptions +}): Promise<{ + existingNodes: Map + memoryNodeCountsBySysType: IMemoryNodesCountsBySysType +}> { const { touchNode } = actions const needToTouchNodes = !hasFeature(`stateful-source-nodes`) && is.firstSourceNodesCallOfCurrentNodeProcess + const needToTouchLocalFileNodes = pluginConfig.get(`downloadLocal`) + if (existingNodes.size === 0) { memoryNodeCountsBySysType.Asset = 0 memoryNodeCountsBySysType.Entry = 0 @@ -34,7 +59,9 @@ export async function getExistingCachedNodes({ for (const typeName of allNodeTypeNames) { const typeNodes = dataStore.iterateNodesByType(typeName) - const firstNodeOfType = Array.from(typeNodes.slice(0, 1))[0] + const firstNodeOfType = Array.from( + typeNodes.slice(0, 1) + )[0] as unknown as IGatsbyNode if ( !firstNodeOfType || @@ -46,12 +73,27 @@ export async function getExistingCachedNodes({ for (const node of typeNodes) { if (needToTouchNodes) { touchNode(node) - - if (node?.fields?.includes(`localFile`)) { - // Prevent GraphQL type inference from crashing on this property - const fullNode = getNode(node.id) - const localFileNode = getNode(fullNode.fields.localFile) - touchNode(localFileNode) + } + // Handle nodes created by downloadLocal + if ( + needToTouchLocalFileNodes && + node?.fields && + Object.keys(node?.fields).includes(`localFile`) + ) { + // Prevent GraphQL type inference from crashing on this property + interface INodeWithLocalFile extends Node { + fields: { + localFile: string + [key: string]: unknown + } + } + const fullNode = getNode(node.id) + if (fullNode) { + const localFileFullNode = fullNode as INodeWithLocalFile + const localFileNode = getNode(localFileFullNode.fields.localFile) + if (localFileNode) { + touchNode(localFileNode) + } } } @@ -60,7 +102,7 @@ export async function getExistingCachedNodes({ await untilNextEventLoopTick() } - addNodeToExistingNodesCache(node) + addNodeToExistingNodesCache(node as unknown as IContentfulEntry) } // dont block the event loop @@ -76,14 +118,9 @@ export async function getExistingCachedNodes({ } } -const memoryNodeCountsBySysType = { - Asset: 0, - Entry: 0, -} - // store only the fields we need to compare to reduce memory usage. if a node is updated we'll use getNode to grab the whole node before updating it -export function addNodeToExistingNodesCache(node) { - if (node.internal.type === `ContentfulTag`) { +export function addNodeToExistingNodesCache(node: IContentfulEntry): void { + if (!node.sys?.type) { return } @@ -97,15 +134,18 @@ export function addNodeToExistingNodesCache(node) { const cacheNode = { id: node.id, - contentful_id: node.contentful_id, sys: { + id: node.sys.id, type: node.sys.type, + locale: node.sys.locale, + spaceId: node.sys.spaceId, }, node_locale: node.node_locale, children: node.children, internal: { - owner: node.internal.owner, + owner: `gatsby-source-contentful`, }, + linkedFrom: node.linkedFrom, __memcache: true, } @@ -115,10 +155,10 @@ export function addNodeToExistingNodesCache(node) { } } - existingNodes.set(node.id, cacheNode) + existingNodes.set(node.id, cacheNode as unknown as IContentfulEntry) } -export function removeNodeFromExistingNodesCache(node) { +export function removeNodeFromExistingNodesCache(node: IContentfulEntry): void { if (node.internal.type === `ContentfulTag`) { return } diff --git a/packages/gatsby-source-contentful/src/config.ts b/packages/gatsby-source-contentful/src/config.ts new file mode 100644 index 0000000000000..1bcec72be805b --- /dev/null +++ b/packages/gatsby-source-contentful/src/config.ts @@ -0,0 +1,14 @@ +export const conflictFieldPrefix = `contentful` + +export const restrictedNodeFields = [ + // restrictedNodeFields from here https://www.gatsbyjs.org/docs/node-interface/ + `id`, + `children`, + `parent`, + `fields`, + `internal`, + // Contentful Common resource attributes: https://www.contentful.com/developers/docs/references/content-delivery-api/#/introduction/common-resource-attributes + `sys`, + `contentfulMetadata`, + `linkedFrom`, +] diff --git a/packages/gatsby-source-contentful/src/create-schema-customization.js b/packages/gatsby-source-contentful/src/create-schema-customization.js deleted file mode 100644 index 314f6305e3251..0000000000000 --- a/packages/gatsby-source-contentful/src/create-schema-customization.js +++ /dev/null @@ -1,212 +0,0 @@ -// @ts-check -import _ from "lodash" -import { fetchContentTypes } from "./fetch" -import { createPluginConfig } from "./plugin-options" -import { CODES } from "./report" -import { resolveGatsbyImageData } from "./gatsby-plugin-image" -import { ImageCropFocusType, ImageResizingBehavior } from "./schemes" -import { stripIndent } from "common-tags" -import { addRemoteFilePolyfillInterface } from "gatsby-plugin-utils/polyfill-remote-file" -import { makeTypeName } from "./normalize" - -async function getContentTypesFromContentful({ - cache, - reporter, - pluginConfig, -}) { - // Get content type items from Contentful - const allContentTypeItems = await fetchContentTypes({ - pluginConfig, - reporter, - }) - - const contentTypeFilter = pluginConfig.get(`contentTypeFilter`) - - const contentTypeItems = allContentTypeItems.filter(contentTypeFilter) - - if (contentTypeItems.length === 0) { - reporter.panic({ - id: CODES.ContentTypesMissing, - context: { - sourceMessage: `Please check if your contentTypeFilter is configured properly. Content types were filtered down to none.`, - }, - }) - } - - // 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(`tag`) - } - - 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 = contentTypeItem.sys.id - if (useNameForId) { - contentTypeItemId = contentTypeItem.name.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, store, reporter, cache }, - pluginOptions -) { - const { createTypes } = actions - - const pluginConfig = createPluginConfig(pluginOptions) - - const typePrefix = pluginConfig.get(`typePrefix`) - const useNameForId = pluginConfig.get(`useNameForId`) - - 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 { getGatsbyImageFieldConfig } = await import( - `gatsby-plugin-image/graphql-utils` - ) - - 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 }, - }), - ] - - contentfulTypes.push( - addRemoteFilePolyfillInterface( - schema.buildObjectType({ - name: makeTypeName(`Asset`, typePrefix), - fields: { - contentful_id: { type: `String!` }, - id: { type: `ID!` }, - gatsbyImageData: getGatsbyImageFieldConfig( - async (...args) => resolveGatsbyImageData(...args, { cache }), - { - jpegProgressive: { - type: `Boolean`, - defaultValue: true, - }, - resizingBehavior: { - type: ImageResizingBehavior, - }, - cropFocus: { - type: ImageCropFocusType, - }, - cornerRadius: { - type: `Int`, - defaultValue: 0, - description: stripIndent` - Desired corner radius in pixels. Results in an image with rounded corners. - Pass \`-1\` for a full circle/ellipse.`, - }, - quality: { - type: `Int`, - defaultValue: 50, - }, - } - ), - ...(pluginConfig.get(`downloadLocal`) - ? { - localFile: { - type: `File`, - extensions: { - link: { - from: `fields.localFile`, - }, - }, - }, - } - : {}), - }, - interfaces: [`ContentfulReference`, `Node`, `RemoteFile`], - }), - { - schema, - actions, - store, - } - ) - ) - - // Create types for each content type - contentTypeItems.forEach(contentTypeItem => - contentfulTypes.push( - schema.buildObjectType({ - name: makeTypeName( - useNameForId ? contentTypeItem.name : contentTypeItem.sys.id, - typePrefix - ), - 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: makeTypeName(`Tag`, typePrefix), - fields: { - name: { type: `String!` }, - contentful_id: { type: `String!` }, - id: { type: `ID!` }, - }, - interfaces: [`Node`], - extensions: { infer: false }, - }) - ) - } - - createTypes(contentfulTypes) -} diff --git a/packages/gatsby-source-contentful/src/create-schema-customization.ts b/packages/gatsby-source-contentful/src/create-schema-customization.ts new file mode 100644 index 0000000000000..f5343924629a6 --- /dev/null +++ b/packages/gatsby-source-contentful/src/create-schema-customization.ts @@ -0,0 +1,813 @@ +import type { + GatsbyNode, + NodePluginSchema, + CreateSchemaCustomizationArgs, + IGatsbyResolverContext, +} from "gatsby" +import { + GraphQLFieldConfig, + GraphQLFloat, + GraphQLInt, + GraphQLString, + GraphQLType, +} from "gatsby/graphql" +import type { ObjectTypeComposerArgumentConfigMapDefinition } from "graphql-compose" +import { getRichTextEntityLinks } from "@contentful/rich-text-links" +import { stripIndent } from "common-tags" +import { addRemoteFilePolyfillInterface } from "gatsby-plugin-utils/polyfill-remote-file" + +import { fetchContentTypes } from "./fetch" +import { createPluginConfig } from "./plugin-options" +import { CODES } from "./report" +import { resolveGatsbyImageData } from "./gatsby-plugin-image" +import { makeTypeName } from "./normalize" +import { ImageCropFocusType, ImageResizingBehavior } from "./schemes" +import type { IPluginOptions, MarkdownFieldDefinition } from "./types/plugin" +import type { ContentType, ContentTypeField } from "contentful" + +import type { + IContentfulAsset, + IContentfulEntry, + IContentfulImageAPITransformerOptions, +} from "./types/contentful" +import { detectMarkdownField, makeContentTypeIdMap } from "./utils" +import { restrictedNodeFields } from "./config" +import { Document } from "@contentful/rich-text-types" + +type CreateTypes = CreateSchemaCustomizationArgs["actions"]["createTypes"] + +interface IContentfulGraphQLField + extends Omit< + Partial< + GraphQLFieldConfig< + IContentfulEntry, + IGatsbyResolverContext + > + >, + "type" + > { + type: string | GraphQLType + id?: string +} + +interface IRichTextFieldStructure { + richTextData: Document + spaceId: string +} + +// Contentful content type schemas +const ContentfulDataTypes: Map< + string, + (field: IContentfulGraphQLField) => IContentfulGraphQLField +> = new Map([ + [ + `Symbol`, + (): IContentfulGraphQLField => { + return { type: GraphQLString } + }, + ], + [ + `Text`, + (): IContentfulGraphQLField => { + return { type: GraphQLString } + }, + ], + [ + `Markdown`, + (field): IContentfulGraphQLField => { + return { + type: `ContentfulMarkdown`, + extensions: { + link: { by: `id`, from: field.id }, + }, + } + }, + ], + [ + `Integer`, + (): IContentfulGraphQLField => { + return { type: GraphQLInt } + }, + ], + [ + `Number`, + (): IContentfulGraphQLField => { + return { type: GraphQLFloat } + }, + ], + [ + `Date`, + (): IContentfulGraphQLField => { + return { + type: `Date`, + extensions: { + dateformat: {}, + }, + } + }, + ], + [ + `Object`, + (): IContentfulGraphQLField => { + return { type: `JSON` } + }, + ], + [ + `Boolean`, + (): IContentfulGraphQLField => { + return { type: `Boolean` } + }, + ], + [ + `Location`, + (): IContentfulGraphQLField => { + return { type: `ContentfulLocation` } + }, + ], + [ + `RichText`, + (): IContentfulGraphQLField => { + return { + type: `ContentfulRichText`, + resolve: (source, args, context, info): IRichTextFieldStructure => { + const richTextData = context.defaultFieldResolver( + source, + args, + context, + info + ) + return { richTextData, spaceId: source.sys.spaceId } + }, + } + }, + ], +]) + +const unionsNameSet = new Set() +const getLinkFieldType = ( + linkType: string | undefined, + field: ContentTypeField, + schema: NodePluginSchema, + createTypes: CreateTypes, + contentTypeIdMap: Map +): IContentfulGraphQLField => { + // Check for validations + const validations = + field.type === `Array` ? field.items?.validations : field?.validations + + if (validations) { + // We only handle content type validations + const linkContentTypeValidation = validations.find( + ({ linkContentType }) => !!linkContentType + ) + if ( + linkContentTypeValidation && + linkContentTypeValidation.linkContentType + ) { + const { linkContentType } = linkContentTypeValidation + const contentTypes = Array.isArray(linkContentType) + ? linkContentType + : [linkContentType] + + // We need to remove non existant content types from outdated validations to avoid broken unions + const filteredTypes = contentTypes.filter(typeId => + contentTypeIdMap.has(typeId) + ) + + // Full type names for union members, shorter variant for the union type name + const translatedTypeNames = filteredTypes.map( + typeId => contentTypeIdMap.get(typeId) as string + ) + const shortTypeNames = filteredTypes + .map(typeName => makeTypeName(typeName, ``)) + .sort() + + // Single content type + if (translatedTypeNames.length === 1) { + const typeName = translatedTypeNames.shift() + if (!typeName || !field.id) { + throw new Error( + `Translated type name can not be empty. A field id is required as well. ${JSON.stringify( + { typeName, field, translatedTypeNames, shortTypeNames }, + null, + 2 + )}` + ) + } + return { + type: typeName, + extensions: { + link: { by: `id`, from: field.id }, + }, + } + } + + // Multiple content types + const unionName = [`UnionContentful`, ...shortTypeNames].join(``) + + if (!unionsNameSet.has(unionName)) { + unionsNameSet.add(unionName) + createTypes( + schema.buildUnionType({ + name: unionName, + types: translatedTypeNames, + }) + ) + } + + return { + type: unionName, + extensions: { + link: { by: `id`, from: field.id }, + }, + } + } + } + + return { + type: `Contentful${linkType}`, + extensions: { + link: { by: `id`, from: field.id }, + }, + } +} +// Translate Contentful field types to GraphQL field types +const translateFieldType = ( + field: ContentTypeField, + contentTypeItem: ContentType, + schema: NodePluginSchema, + createTypes: CreateTypes, + enableMarkdownDetection: boolean, + enforceRequiredFields: boolean, + markdownFields: MarkdownFieldDefinition, + contentTypeIdMap: Map +): GraphQLFieldConfig => { + let fieldType + if (field.type === `Array`) { + if (!field.items) { + throw new Error( + `Invalid content type field definition:\n${JSON.stringify( + field, + null, + 2 + )}` + ) + } + // Arrays of Contentful Links or primitive types + const fieldData = + field.items?.type === `Link` + ? getLinkFieldType( + field.items.linkType, + field, + schema, + createTypes, + contentTypeIdMap + ) + : translateFieldType( + field.items as ContentTypeField, + contentTypeItem, + schema, + createTypes, + enableMarkdownDetection, + enforceRequiredFields, + markdownFields, + contentTypeIdMap + ) + + fieldType = { ...fieldData, type: `[${fieldData.type}]` } + } else if (field.type === `Link`) { + // Contentful Link (reference) field types + fieldType = getLinkFieldType( + field.linkType, + field as ContentTypeField, + schema, + createTypes, + contentTypeIdMap + ) + } else { + // Detect markdown in text fields + const typeName = detectMarkdownField( + field as ContentTypeField, + contentTypeItem, + enableMarkdownDetection, + markdownFields + ) + + // Primitive field types + const primitiveType = ContentfulDataTypes.get(typeName) + if (!primitiveType) { + throw new Error(`Contentful field type ${field.type} is not supported.`) + } + fieldType = primitiveType(field) + } + + // To support Contentful's CPA (Content Preview API), required fields can be disabled by config. + if (enforceRequiredFields && field.required) { + fieldType.type = `${fieldType.type}!` + } + + return fieldType +} + +async function getContentTypesFromContentful({ + cache, + reporter, + pluginConfig, +}): Promise> { + // Get content type items from Contentful + const allContentTypeItems = await fetchContentTypes({ + pluginConfig, + reporter, + }) + + if (!allContentTypeItems) { + throw new Error( + `Could not locate any content types in Contentful space ${pluginConfig.get( + `spaceId` + )}.` + ) + } + const contentTypeFilter = pluginConfig.get(`contentTypeFilter`) + + const contentTypeItems = allContentTypeItems.filter(contentTypeFilter) + + if (contentTypeItems.length === 0) { + reporter.panic({ + id: CODES.ContentTypesMissing, + context: { + sourceMessage: `Please check if your contentTypeFilter is configured properly. Content types were filtered down to none.`, + }, + }) + } + + // 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 const createSchemaCustomization: GatsbyNode["createSchemaCustomization"] = + async function ({ schema, actions, store, reporter, cache }, pluginOptions) { + const { createTypes } = actions + + const pluginConfig = createPluginConfig(pluginOptions as IPluginOptions) + + const contentTypePrefix = pluginConfig.get(`contentTypePrefix`) + const useNameForId = pluginConfig.get(`useNameForId`) + const enableMarkdownDetection: boolean = pluginConfig.get( + `enableMarkdownDetection` + ) + const enforceRequiredFields: boolean = pluginConfig.get( + `enforceRequiredFields` + ) + const markdownFields: MarkdownFieldDefinition = new Map( + pluginConfig.get(`markdownFields`) + ) + + let contentTypeItems: Array + 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, + }) + } + + // Generic Types + createTypes( + schema.buildInterfaceType({ + name: `ContentfulEntity`, + fields: { + id: { type: `ID!` }, + sys: { type: `ContentfulSys!` }, + contentfulMetadata: { type: `ContentfulMetadata!` }, + }, + interfaces: [`Node`], + }) + ) + + createTypes( + schema.buildInterfaceType({ + name: `ContentfulEntry`, + fields: { + id: { type: `ID!` }, + sys: { type: `ContentfulSys!` }, + contentfulMetadata: { type: `ContentfulMetadata!` }, + linkedFrom: { type: `ContentfulLinkedFrom` }, + }, + interfaces: [`ContentfulEntity`, `Node`], + }) + ) + + createTypes( + schema.buildObjectType({ + name: `ContentfulContentType`, + fields: { + id: { type: `ID!` }, + name: { type: `String!` }, + displayField: { type: `String!` }, + description: { type: `String!` }, + }, + interfaces: [`Node`], + extensions: { dontInfer: {} }, + }) + ) + + createTypes( + schema.buildObjectType({ + name: `ContentfulSys`, + fields: { + id: { type: ` String!` }, + type: { type: ` String!` }, + spaceId: { type: ` String!` }, + environmentId: { type: ` String!` }, + contentType: { + type: `ContentfulContentType`, + extensions: { + link: { by: `id`, from: `contentType` }, + }, + }, + firstPublishedAt: { + type: ` Date!`, + extensions: { + dateformat: {}, + }, + }, + publishedAt: { + type: ` Date!`, + extensions: { + dateformat: {}, + }, + }, + publishedVersion: { type: ` Int!` }, + locale: { type: ` String!` }, + }, + interfaces: [`Node`], + extensions: { dontInfer: {} }, + }) + ) + + createTypes( + schema.buildObjectType({ + name: `ContentfulTag`, + fields: { + name: { type: `String!` }, + contentful_id: { type: `String!` }, + id: { type: `ID!` }, + }, + interfaces: [`Node`], + extensions: { dontInfer: {} }, + }) + ) + + createTypes( + schema.buildObjectType({ + name: `ContentfulMetadata`, + fields: { + tags: { + type: `[ContentfulTag]!`, + extensions: { + link: { by: `id`, from: `tags` }, + }, + }, + }, + extensions: { dontInfer: {} }, + }) + ) + + const { getGatsbyImageFieldConfig } = await import( + `gatsby-plugin-image/graphql-utils.js` + ) + + const reverseLinkFields = {} + const contentTypeIdMap = makeContentTypeIdMap( + contentTypeItems, + contentTypePrefix, + useNameForId + ) + + contentTypeItems.forEach(contentType => { + const contentTypeItemId = contentTypeIdMap.get(contentType.sys.id) + + if (!contentTypeItemId) { + throw new Error( + `Could not locate id for content type ${contentType.sys.id} (${contentType.name})` + ) + } + + if ( + contentType.fields.some( + field => + field.linkType || field.items?.linkType || field.type === `RichText` + ) + ) { + reverseLinkFields[contentTypeItemId] = { + type: `[${contentTypeItemId}]`, + extensions: { + link: { by: `id`, from: contentTypeItemId }, + }, + } + } + }) + + // When nothing is set up to be linked in the Contentful Content Model, schema building fails + // as empty fields get removed by graphql-compose + if (Object.keys(reverseLinkFields).length === 0) { + contentTypeItems.forEach(contentType => { + const contentTypeItemId = contentTypeIdMap.get(contentType.sys.id) + + if (contentTypeItemId) { + reverseLinkFields[contentTypeItemId] = { + type: `[${contentTypeItemId}]`, + extensions: { + link: { by: `id`, from: contentTypeItemId }, + }, + } + } + }) + } + + const linkedFromName = `ContentfulLinkedFrom` + createTypes( + schema.buildObjectType({ + name: linkedFromName, + fields: reverseLinkFields, + extensions: { dontInfer: {} }, + }) + ) + + // Assets + const gatsbyImageData = getGatsbyImageFieldConfig< + IContentfulAsset, + null, + IContentfulImageAPITransformerOptions + >( + async (image, options) => + resolveGatsbyImageData(image, options, { cache }), + { + jpegProgressive: { + type: `Boolean`, + defaultValue: true, + }, + resizingBehavior: { + type: ImageResizingBehavior, + }, + cropFocus: { + type: ImageCropFocusType, + }, + cornerRadius: { + type: `Int`, + defaultValue: 0, + description: stripIndent` + Desired corner radius in pixels. Results in an image with rounded corners. + Pass \`-1\` for a full circle/ellipse.`, + }, + quality: { + type: `Int`, + defaultValue: 50, + }, + } as unknown as ObjectTypeComposerArgumentConfigMapDefinition + ) + gatsbyImageData.type = `JSON` + createTypes( + addRemoteFilePolyfillInterface( + schema.buildObjectType({ + name: `ContentfulAsset`, + fields: { + id: { type: `ID!` }, + sys: { type: `ContentfulSys!` }, + contentfulMetadata: { type: `ContentfulMetadata!` }, + gatsbyImageData, + ...(pluginConfig.get(`downloadLocal`) + ? { + localFile: { + type: `File`, + extensions: { + link: { + from: `fields.localFile`, + }, + }, + }, + } + : {}), + title: { type: `String` }, + description: { type: `String` }, + contentType: { type: `String!` }, + mimeType: { type: `String!` }, + filename: { type: `String!` }, + url: { type: `String!` }, + size: { type: `Int` }, + width: { type: `Int` }, + height: { type: `Int` }, + linkedFrom: { type: linkedFromName }, + }, + interfaces: [`ContentfulEntity`, `Node`], + extensions: { dontInfer: {} }, + }), + { + schema, + actions, + store, + } + ) + ) + + // Rich Text + const makeRichTextLinksResolver = + (nodeType, entityType) => + async ( + source: IRichTextFieldStructure, + _args, + context + ): Promise | null> => { + const links = getRichTextEntityLinks(source.richTextData, nodeType)[ + entityType + ].map(({ id }) => id) + + const res = await context.nodeModel.findAll({ + query: { + filter: { + sys: { + id: { + in: links, + }, + spaceId: { eq: source.spaceId }, + }, + }, + }, + type: `Contentful${entityType}`, + }) + + return res.entries + } + + // Contentful specific types + createTypes( + schema.buildObjectType({ + name: `ContentfulRichTextAssets`, + fields: { + block: { + type: `[ContentfulAsset]!`, + resolve: makeRichTextLinksResolver(`embedded-asset-block`, `Asset`), + }, + hyperlink: { + type: `[ContentfulAsset]!`, + resolve: makeRichTextLinksResolver(`asset-hyperlink`, `Asset`), + }, + }, + }) + ) + + createTypes( + schema.buildObjectType({ + name: `ContentfulRichTextEntries`, + fields: { + inline: { + type: `[ContentfulEntry]!`, + resolve: makeRichTextLinksResolver( + `embedded-entry-inline`, + `Entry` + ), + }, + block: { + type: `[ContentfulEntry]!`, + resolve: makeRichTextLinksResolver(`embedded-entry-block`, `Entry`), + }, + hyperlink: { + type: `[ContentfulEntry]!`, + resolve: makeRichTextLinksResolver(`entry-hyperlink`, `Entry`), + }, + }, + }) + ) + + createTypes( + schema.buildObjectType({ + name: `ContentfulRichTextLinks`, + fields: { + assets: { + type: `ContentfulRichTextAssets`, + resolve(source) { + return source + }, + }, + entries: { + type: `ContentfulRichTextEntries`, + resolve(source) { + return source + }, + }, + }, + }) + ) + + createTypes( + schema.buildObjectType({ + name: `ContentfulRichText`, + fields: { + json: { + type: `JSON`, + resolve(source: IRichTextFieldStructure) { + return source.richTextData + }, + }, + links: { + type: `ContentfulRichTextLinks`, + resolve(source) { + return source + }, + }, + }, + extensions: { dontInfer: {} }, + }) + ) + + // Location + createTypes( + schema.buildObjectType({ + name: `ContentfulLocation`, + fields: { + lat: { type: `Float!` }, + lon: { type: `Float!` }, + }, + extensions: { + dontInfer: {}, + }, + }) + ) + + // Text (Markdown, as text is a pure string) + createTypes( + schema.buildObjectType({ + name: `ContentfulMarkdown`, + fields: { + raw: `String!`, + }, + interfaces: [`Node`], + extensions: { + dontInfer: {}, + }, + }) + ) + + // Content types + for (const contentTypeItem of contentTypeItems) { + try { + const fields = {} + contentTypeItem.fields.forEach(field => { + if (field.omitted) { + return + } + if (restrictedNodeFields.includes(field.id)) { + // Throw error on reserved field names as the Contenful GraphQL API does: + // https://www.contentful.com/developers/docs/references/graphql/#/reference/schema-generation/fields + throw new Error( + `Unfortunately the field name ${field.id} is reserved. ${contentTypeItem.name}@${contentTypeItem.sys.id}` + ) + } + fields[field.id] = translateFieldType( + field, + contentTypeItem, + schema, + createTypes, + enableMarkdownDetection, + enforceRequiredFields, + markdownFields, + contentTypeIdMap + ) + }) + + const type = contentTypeIdMap.get(contentTypeItem.sys.id) as string + + createTypes( + schema.buildObjectType({ + name: type, + fields: { + id: { type: `ID!` }, + sys: { type: `ContentfulSys!` }, + contentfulMetadata: { type: `ContentfulMetadata!` }, + ...fields, + linkedFrom: linkedFromName, + }, + interfaces: [`ContentfulEntity`, `ContentfulEntry`, `Node`], + extensions: { dontInfer: {} }, + }) + ) + } catch (err) { + err.message = `Unable to create schema for Contentful Content Type ${ + contentTypeItem.name || contentTypeItem.sys.id + }:\n${err.message}` + console.log(err.stack) + throw err + } + } + } diff --git a/packages/gatsby-source-contentful/src/download-contentful-assets.js b/packages/gatsby-source-contentful/src/download-contentful-assets.ts similarity index 59% rename from packages/gatsby-source-contentful/src/download-contentful-assets.js rename to packages/gatsby-source-contentful/src/download-contentful-assets.ts index 93834e9d7b13d..955bf91967325 100644 --- a/packages/gatsby-source-contentful/src/download-contentful-assets.js +++ b/packages/gatsby-source-contentful/src/download-contentful-assets.ts @@ -1,17 +1,16 @@ -// @ts-check +import type { Actions, SourceNodesArgs } from "gatsby" import { createRemoteFileNode } from "gatsby-source-filesystem" -import { createUrl } from "./image-helpers" +import type { IContentfulAsset } from "./types/contentful" /** * @name distributeWorkload * @param workers A list of async functions to complete * @param {number} count The number of task runners to use (see assetDownloadWorkers in config) */ - -async function distributeWorkload(workers, count = 50) { +async function distributeWorkload(workers, count = 50): Promise { const methods = workers.slice() - async function task() { + async function task(): Promise { while (methods.length > 0) { await methods.pop()() } @@ -20,24 +19,24 @@ async function distributeWorkload(workers, count = 50) { await Promise.all(new Array(count).fill(undefined).map(() => task())) } +interface IRemoteData { + fileNodeID: string +} + /** * @name downloadContentfulAssets * @description Downloads Contentful assets to the local filesystem. * The asset files will be downloaded and cached. Use `localFile` to link to them * @param gatsbyFunctions - Gatsby's internal helper functions */ - -export async function downloadContentfulAssets(gatsbyFunctions) { - const { - actions: { createNode, touchNode, createNodeField }, - createNodeId, - store, - cache, - reporter, - assetDownloadWorkers, - getNode, - assetNodes, - } = gatsbyFunctions +export async function downloadContentfulAssets( + gatsbyFunctions: SourceNodesArgs, + actions: Actions, + assetNodes: Array, + assetDownloadWorkers: number +): Promise { + const { createNodeId, cache, reporter, getNode } = gatsbyFunctions + const { createNode, touchNode, createNodeField } = actions // Any ContentfulAsset nodes will be downloaded, cached and copied to public/static // regardless of if you use `localFile` to link an asset or not. @@ -48,30 +47,29 @@ export async function downloadContentfulAssets(gatsbyFunctions) { ) bar.start() await distributeWorkload( - assetNodes.map(node => async () => { + assetNodes.map(assetNode => async (): Promise => { let fileNodeID - const { contentful_id: id, node_locale: locale } = node + const { + sys: { id, locale }, + url, + } = assetNode const remoteDataCacheKey = `contentful-asset-${id}-${locale}` - const cacheRemoteData = await cache.get(remoteDataCacheKey) - if (!node.file) { - reporter.log(id, locale) - reporter.warn(`The asset with id: ${id}, contains no file.`) - return Promise.resolve() - } - if (!node.file.url) { - reporter.warn( - `The asset with id: ${id} has a file but the file contains no url.` - ) + const cacheRemoteData: IRemoteData = await cache.get(remoteDataCacheKey) + + if (!url) { + reporter.warn(`The asset with id: ${id} has no url.`) return Promise.resolve() } - const url = createUrl(node.file.url) // Avoid downloading the asset again if it's been cached // Note: Contentful Assets do not provide useful metadata // to compare a modified asset to a cached version? if (cacheRemoteData) { - fileNodeID = cacheRemoteData.fileNodeID // eslint-disable-line prefer-destructuring - touchNode(getNode(cacheRemoteData.fileNodeID)) + fileNodeID = cacheRemoteData.fileNodeID + const existingNode = getNode(cacheRemoteData.fileNodeID) + if (existingNode) { + touchNode(existingNode) + } } // If we don't have cached data, download the file @@ -92,10 +90,13 @@ export async function downloadContentfulAssets(gatsbyFunctions) { } if (fileNodeID) { - createNodeField({ node, name: `localFile`, value: fileNodeID }) + createNodeField({ + node: assetNode, + name: `localFile`, + value: fileNodeID, + }) } - - return node + return Promise.resolve() }), assetDownloadWorkers ) diff --git a/packages/gatsby-source-contentful/src/extend-node-type.js b/packages/gatsby-source-contentful/src/extend-node-type.js deleted file mode 100644 index 1129c33e70daa..0000000000000 --- a/packages/gatsby-source-contentful/src/extend-node-type.js +++ /dev/null @@ -1,62 +0,0 @@ -// @ts-check -import { stripIndent } from "common-tags" -import { GraphQLBoolean, GraphQLInt } from "gatsby/graphql" -import { hasFeature } from "gatsby-plugin-utils" - -import { resolveGatsbyImageData } from "./gatsby-plugin-image" -import { ImageCropFocusType, ImageResizingBehavior } from "./schemes" -import { makeTypeName } from "./normalize" - -export async function setFieldsOnGraphQLNodeType( - { type, cache }, - { typePrefix = `Contentful` } = {} -) { - if (type.name !== makeTypeName(`Asset`, typePrefix)) { - return {} - } - - // gatsby-plugin-image - const getGatsbyImageData = async () => { - const { getGatsbyImageFieldConfig } = await import( - `gatsby-plugin-image/graphql-utils` - ) - - const fieldConfig = getGatsbyImageFieldConfig( - async (...args) => resolveGatsbyImageData(...args, { cache }), - { - jpegProgressive: { - type: GraphQLBoolean, - defaultValue: true, - }, - resizingBehavior: { - type: ImageResizingBehavior, - }, - cropFocus: { - type: ImageCropFocusType, - }, - cornerRadius: { - type: GraphQLInt, - defaultValue: 0, - description: stripIndent` - Desired corner radius in pixels. Results in an image with rounded corners. - Pass \`-1\` for a full circle/ellipse.`, - }, - quality: { - type: GraphQLInt, - }, - } - ) - - fieldConfig.type = hasFeature(`graphql-typegen`) - ? `GatsbyImageData` - : `JSON` - - return fieldConfig - } - - const gatsbyImageData = await getGatsbyImageData() - - return { - gatsbyImageData, - } -} diff --git a/packages/gatsby-source-contentful/src/fetch.js b/packages/gatsby-source-contentful/src/fetch.ts similarity index 69% rename from packages/gatsby-source-contentful/src/fetch.js rename to packages/gatsby-source-contentful/src/fetch.ts index 8d15ca1f6b721..c68e7ad1e72f2 100644 --- a/packages/gatsby-source-contentful/src/fetch.js +++ b/packages/gatsby-source-contentful/src/fetch.ts @@ -1,20 +1,38 @@ -// @ts-check import chalk from "chalk" -import { createClient } from "contentful" +import { + ContentfulClientApi, + ContentTypeCollection, + createClient, + CreateClientParams, +} from "contentful" +import type { Reporter } from "gatsby" import _ from "lodash" +import type { Span } from "opentracing" + import { formatPluginOptionsForCLI } from "./plugin-options" import { CODES } from "./report" +import type { + ContentType, + Locale, + Space, + SyncCollection, + Tag, + EntrySkeletonType, + ContentfulCollection, +} from "contentful" +import type { IProcessedPluginOptions } from "./types/plugin" +import type { IProgressReporter } from "gatsby-cli/lib/reporter/reporter-progress" /** * Generate a user friendly error message. * * Contentful's API has its own error message structure, which might change depending of internal server or authentification errors. */ -const createContentfulErrorMessage = e => { +const createContentfulErrorMessage = (e: any): string => { // Handle axios error messages if (e.isAxiosError) { const axiosErrorMessage = [e.code, e.status] - const axiosErrorDetails = [] + const axiosErrorDetails: Array = [] if (e.response) { axiosErrorMessage.push(e.response.status) @@ -48,25 +66,24 @@ const createContentfulErrorMessage = e => { return axiosErrorMessage.filter(Boolean).join(` `) } + interface IContentfulAPIError { + status?: number + statusText?: string + requestId?: string + message: string + details: Record + request?: Record + } + // If it is not an axios error, we assume that we got a Contentful SDK error and try to parse it const errorMessage = [e.name] - const errorDetails = [] + const errorDetails: Array = [] try { /** * Parse stringified error data from message * https://github.com/contentful/contentful-sdk-core/blob/4cfcd452ba0752237a26ce6b79d72a50af84d84e/src/error-handler.ts#L71-L75 - * - * @todo properly type this with TS - * type { - * status?: number - * statusText?: string - * requestId?: string - * message: string - * !details: Record - * !request?: Record - * } - */ - const errorData = JSON.parse(e.message) + **/ + const errorData: IContentfulAPIError = JSON.parse(e.message) errorMessage.push(errorData.status && String(errorData.status)) errorMessage.push(errorData.statusText) errorMessage.push(errorData.message) @@ -98,11 +115,15 @@ const createContentfulErrorMessage = e => { function createContentfulClientOptions({ pluginConfig, reporter, - syncProgress = { total: 0, tick: a => a }, -}) { + syncProgress, +}: { + pluginConfig: IProcessedPluginOptions + reporter: Reporter + syncProgress?: IProgressReporter +}): CreateClientParams { let syncItemCount = 0 - const contentfulClientOptions = { + const contentfulClientOptions: CreateClientParams = { space: pluginConfig.get(`spaceId`), accessToken: pluginConfig.get(`accessToken`), host: pluginConfig.get(`host`), @@ -110,7 +131,7 @@ function createContentfulClientOptions({ proxy: pluginConfig.get(`proxy`), integration: `gatsby-source-contentful`, responseLogger: response => { - function createMetadataLog(response) { + function createMetadataLog(response): string | null { if (!response.headers) { return null } @@ -130,7 +151,8 @@ function createContentfulClientOptions({ if ( response.config.url === `sync` && !response.isAxiosError && - response?.data.items + response?.data.items && + syncProgress ) { syncItemCount += response.data.items.length syncProgress.total = syncItemCount @@ -162,7 +184,12 @@ function handleContentfulError({ reporter, contentfulClientOptions, pluginConfig, -}) { +}: { + e: any + reporter: Reporter + contentfulClientOptions: CreateClientParams + pluginConfig: IProcessedPluginOptions +}): void { let details let errors if (e.code === `ENOTFOUND`) { @@ -214,6 +241,7 @@ function handleContentfulError({ } reporter.panic({ + id: CODES.GenericContentfulError, context: { sourceMessage: `Accessing your Contentful space failed: ${createContentfulErrorMessage( e @@ -227,19 +255,41 @@ ${formatPluginOptionsForCLI(pluginConfig.getOriginalPluginOptions(), errors)}`, }) } +interface IFetchResult { + currentSyncData: SyncCollection< + EntrySkeletonType, + "WITH_ALL_LOCALES" | "WITHOUT_LINK_RESOLUTION" + > + tagItems: Array + defaultLocale: string + locales: Array + space: Space +} + /** * Fetches: * * Locales with default locale * * Entries and assets * * Tags */ -export async function fetchContent({ syncToken, pluginConfig, reporter }) { +export async function fetchContent({ + syncToken, + pluginConfig, + reporter, + parentSpan, +}: { + syncToken: string + pluginConfig: IProcessedPluginOptions + reporter: Reporter + parentSpan?: Span +}): Promise { // Fetch locales and check connectivity const contentfulClientOptions = createContentfulClientOptions({ pluginConfig, reporter, }) - const client = createClient(contentfulClientOptions) + const client = createClient(contentfulClientOptions).withAllLocales + .withoutLinkResolution // 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. @@ -264,12 +314,18 @@ export async function fetchContent({ syncToken, pluginConfig, reporter }) { } // Fetch entries and assets via Contentful CDA sync API - const pageLimit = pluginConfig.get(`pageLimit`) - reporter.verbose(`Contentful: Sync ${pageLimit} items per page.`) + const pageLimit = pluginConfig.get(`pageLimit`) || 100 + const ctfEnvironment = pluginConfig.get(`environment`) || `master` + reporter.verbose( + `Contentful: Sync ${pageLimit} items per page from environment ${ctfEnvironment}.` + ) const syncProgress = reporter.createProgress( `Contentful: ${syncToken ? `Sync changed items` : `Sync all items`}`, pageLimit, - 0 + 0, + { + parentSpan, + } ) syncProgress.start() const contentfulSyncClientOptions = createContentfulClientOptions({ @@ -277,19 +333,29 @@ export async function fetchContent({ syncToken, pluginConfig, reporter }) { reporter, syncProgress, }) - const syncClient = createClient(contentfulSyncClientOptions) - - let currentSyncData + const syncClient = createClient(contentfulSyncClientOptions).withAllLocales + .withoutLinkResolution + + let currentSyncData: + | SyncCollection< + EntrySkeletonType, + "WITH_ALL_LOCALES" | "WITHOUT_LINK_RESOLUTION" + > + | undefined let currentPageLimit = pageLimit - let lastCurrentPageLimit + let lastCurrentPageLimit = 0 let syncSuccess = false try { while (!syncSuccess) { try { const query = syncToken - ? { nextSyncToken: syncToken, resolveLinks: false } - : { initial: true, limit: currentPageLimit, resolveLinks: false } - currentSyncData = await syncClient.sync(query) + ? { nextSyncToken: syncToken } + : { initial: true as const, limit: currentPageLimit } + + currentSyncData = (await syncClient.sync(query)) as SyncCollection< + EntrySkeletonType, + "WITH_ALL_LOCALES" | "WITHOUT_LINK_RESOLUTION" + > syncSuccess = true } catch (e) { // Back off page limit if responses content length exceeds Contentfuls limits. @@ -329,38 +395,44 @@ export async function fetchContent({ syncToken, pluginConfig, reporter }) { } finally { // Fix output when there was no new data in Contentful if ( - !currentSyncData?.entries.length && - !currentSyncData?.assets.length && - !currentSyncData?.deletedEntries.length && - !currentSyncData?.deletedAssets.length + currentSyncData && + currentSyncData?.entries.length + + currentSyncData?.assets.length + + currentSyncData?.deletedEntries.length + + currentSyncData?.deletedAssets.length === + 0 ) { syncProgress.tick() syncProgress.total = 1 } - - syncProgress.done() + syncProgress.end() } // We need to fetch tags with the non-sync API as the sync API doesn't support this. - let tagItems = [] - if (pluginConfig.get(`enableTags`)) { - try { - const tagsResult = await pagedGet(client, `getTags`, pageLimit) - tagItems = tagsResult.items - reporter.verbose(`Tags fetched ${tagItems.length}`) - } catch (e) { - reporter.panic({ - id: CODES.FetchTags, - context: { - sourceMessage: `Error fetching tags: ${createContentfulErrorMessage( - e - )}`, - }, - }) + let tagItems + try { + const tagsResult = await pagedGet(client, `getTags`, pageLimit) + if (!tagsResult) { + throw new Error() } + tagItems = tagsResult.items + reporter.verbose(`Tags fetched ${tagItems.length}`) + } catch (e) { + reporter.panic({ + id: CODES.FetchTags, + context: { + sourceMessage: `Error fetching tags: ${createContentfulErrorMessage( + e + )}`, + }, + }) + } + + if (!currentSyncData) { + throw new Error(`Unable to sync data from Contentful API`) } - const result = { + const result: IFetchResult = { currentSyncData, tagItems, defaultLocale, @@ -375,24 +447,42 @@ export async function fetchContent({ syncToken, pluginConfig, reporter }) { * Fetches: * * Content types */ -export async function fetchContentTypes({ pluginConfig, reporter }) { +export async function fetchContentTypes({ + pluginConfig, + reporter, +}: { + pluginConfig: IProcessedPluginOptions + reporter: Reporter +}): Promise | null> { const contentfulClientOptions = createContentfulClientOptions({ pluginConfig, reporter, }) - const client = createClient(contentfulClientOptions) - const pageLimit = pluginConfig.get(`pageLimit`) + const client = createClient(contentfulClientOptions).withAllLocales + .withoutLinkResolution + const pageLimit = pluginConfig.get(`pageLimit`) || 100 const sourceId = `${pluginConfig.get(`spaceId`)}-${pluginConfig.get( `environment` )}` - let contentTypes = null + + let contentTypes: Array = [] try { reporter.verbose(`Fetching content types (${sourceId})`) // Fetch content types from CDA API try { - contentTypes = await pagedGet(client, `getContentTypes`, pageLimit) + const contentTypeCollection = await pagedGet( + client, + `getContentTypes`, + pageLimit + ) + if (contentTypeCollection) { + reporter.verbose( + `Content types fetched ${contentTypeCollection.items.length} (${sourceId})` + ) + contentTypes = contentTypeCollection.items as Array + } } catch (e) { reporter.panic({ id: CODES.FetchContentTypes, @@ -403,11 +493,6 @@ export async function fetchContentTypes({ pluginConfig, reporter }) { }, }) } - reporter.verbose( - `Content types fetched ${contentTypes.items.length} (${sourceId})` - ) - - contentTypes = contentTypes.items } catch (e) { handleContentfulError(e) } @@ -420,19 +505,22 @@ export async function fetchContentTypes({ pluginConfig, reporter }) { * The first call will have no aggregated response. Subsequent calls will * concatenate the new responses to the original one. */ -function pagedGet( - client, - method, - pageLimit, +function pagedGet( + client: ContentfulClientApi<"WITH_ALL_LOCALES" | "WITHOUT_LINK_RESOLUTION">, + method: "getContentTypes" | "getTags", + pageLimit: number, query = {}, skip = 0, - aggregatedResponse = null -) { + aggregatedResponse?: ContentfulCollection +): Promise | ContentTypeCollection | undefined> { + if (!client[method]) { + throw new Error(`Contentful Client does not support the method ${method}`) + } + return client[method]({ ...query, skip: skip, limit: pageLimit, - order: `sys.createdAt`, }).then(response => { if (!aggregatedResponse) { aggregatedResponse = response @@ -440,7 +528,7 @@ function pagedGet( aggregatedResponse.items = aggregatedResponse.items.concat(response.items) } if (skip + pageLimit <= response.total) { - return pagedGet( + return pagedGet( client, method, pageLimit, diff --git a/packages/gatsby-source-contentful/src/gatsby-node.js b/packages/gatsby-source-contentful/src/gatsby-node.ts similarity index 80% rename from packages/gatsby-source-contentful/src/gatsby-node.js rename to packages/gatsby-source-contentful/src/gatsby-node.ts index 8f582ab7a9916..f9d2ae97a2d45 100644 --- a/packages/gatsby-source-contentful/src/gatsby-node.js +++ b/packages/gatsby-source-contentful/src/gatsby-node.ts @@ -1,19 +1,20 @@ -// @ts-check -import _ from "lodash" +import type { GatsbyNode } from "gatsby" import origFetch from "node-fetch" import fetchRetry from "@vercel/fetch-retry" import { polyfillImageServiceDevRoutes } from "gatsby-plugin-utils/polyfill-remote-file" -export { setFieldsOnGraphQLNodeType } from "./extend-node-type" -import { CODES } from "./report" +import { hasFeature } from "gatsby-plugin-utils/has-feature" +import { CODES } from "./report" import { maskText } from "./plugin-options" - +import type { IPluginOptions } from "./types/plugin" export { createSchemaCustomization } from "./create-schema-customization" export { sourceNodes } from "./source-nodes" const fetch = fetchRetry(origFetch) -const validateContentfulAccess = async pluginOptions => { +const validateContentfulAccess = async ( + pluginOptions: IPluginOptions +): Promise => { if (process.env.NODE_ENV === `test`) return undefined await fetch( @@ -43,14 +44,26 @@ const validateContentfulAccess = async pluginOptions => { return undefined } -export const onPreInit = async ( +export const onPreInit: GatsbyNode["onPreInit"] = async ( { store, reporter, actions }, pluginOptions ) => { + // gatsby version is too old + if (!hasFeature(`track-inline-object-opt-out`)) { + reporter.panic({ + id: CODES.GatsbyPluginMissing, + context: { + // TODO update message to reflect the actual version with track-inline-object-opt-out support + sourceMessage: `Used gatsby version is too old and doesn't support required features. Please update to gatsby@>=5.X.0`, + }, + }) + } + // if gatsby-plugin-image is not installed try { - await import(`gatsby-plugin-image/graphql-utils`) + await import(`gatsby-plugin-image/graphql-utils.js`) } catch (err) { + console.log(err) reporter.panic({ id: CODES.GatsbyPluginMissing, context: { @@ -80,7 +93,9 @@ export const onPreInit = async ( } } -export const pluginOptionsSchema = ({ Joi }) => +export const pluginOptionsSchema: GatsbyNode["pluginOptionsSchema"] = ({ + Joi, +}) => Joi.object() .keys({ accessToken: Joi.string() @@ -118,7 +133,7 @@ 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), + .default(() => (): boolean => true), typePrefix: Joi.string() .description(`Prefix for Contentful node types`) .default(`Contentful`), @@ -127,7 +142,21 @@ List of locales and their codes can be found in Contentful app -> Settings -> Lo `Possibility to limit how many contentType/nodes are created in GraphQL. This can limit the memory usage by reducing the amount of nodes created. Useful if you have a large space in Contentful and only want to get the data from certain content types. For example, to exclude content types starting with "page" \`contentTypeFilter: contentType => !contentType.sys.id.startsWith('page')\`` ) - .default(() => () => true), + .default(() => (): boolean => true), + enableMarkdownDetection: Joi.boolean() + .description( + `Assumes that every long text field in Contentful is a markdown field. Can be a performance bottle-neck on big projects. Requires gatsby-transformer-remark.` + ) + .default(true), + markdownFields: Joi.array() + .description( + `List of text fields that contain markdown content. Needs gatsby-transformer-remark.` + ) + .default([]) + .example([ + [`product`, [`description`, `summary`]], + [`otherContentTypeId`, [`someMarkdownFieldId`]], + ]), pageLimit: Joi.number() .integer() .description( @@ -153,11 +182,11 @@ For example, to exclude content types starting with "page" \`contentTypeFilter: .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() + enforceRequiredFields: Joi.boolean() .description( - `Enable the new tags feature. This will disallow the content type name "tags" till the next major version of this plugin.` + `Fields required in Contentful will also be required in Gatsby. If you are using Contentfuls Preview API (CPA), you may want to disable this conditionally.` ) - .default(false), + .default(true), 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\`. @@ -181,7 +210,9 @@ For example, to exclude content types starting with "page" \`contentTypeFilter: }) .external(validateContentfulAccess) -/** @type {import('gatsby').GatsbyNode["onCreateDevServer"]} */ -export const onCreateDevServer = ({ app, store }) => { +export const onCreateDevServer: GatsbyNode["onCreateDevServer"] = ({ + app, + store, +}) => { polyfillImageServiceDevRoutes(app, store) } diff --git a/packages/gatsby-source-contentful/src/gatsby-plugin-image.js b/packages/gatsby-source-contentful/src/gatsby-plugin-image.ts similarity index 68% rename from packages/gatsby-source-contentful/src/gatsby-plugin-image.js rename to packages/gatsby-source-contentful/src/gatsby-plugin-image.ts index 963df00d8b013..5f30b9aee8fc4 100644 --- a/packages/gatsby-source-contentful/src/gatsby-plugin-image.js +++ b/packages/gatsby-source-contentful/src/gatsby-plugin-image.ts @@ -1,7 +1,15 @@ -// @ts-check import fs from "fs-extra" +import type { GatsbyCache } from "gatsby" import { fetchRemoteFile } from "gatsby-core-utils/fetch-remote-file" +import { + Fit, + IGatsbyImageData, + IGatsbyImageHelperArgs, + ImageFormat, +} from "gatsby-plugin-image" +import type { ImageFit } from "gatsby-plugin-utils/polyfill-remote-file/types" import path from "path" + import { createUrl, isImage, @@ -9,6 +17,11 @@ import { validImageFormats, CONTENTFUL_IMAGE_MAX_SIZE, } from "./image-helpers" +import type { + contentfulImageApiBackgroundColor, + IContentfulAsset, + IContentfulImageAPITransformerOptions, +} from "./types/contentful" // Promises that rejected should stay in this map. Otherwise remove promise and put their data in resolvedBase64Cache const inFlightBase64Cache = new Map() @@ -17,7 +30,15 @@ const inFlightBase64Cache = new Map() const resolvedBase64Cache = new Map() // Note: this may return a Promise, body (sync), or null -const getBase64Image = (imageProps, cache) => { +const getBase64Image = ( + imageProps: { + image: IContentfulAsset + baseUrl: string + options: IContentfulImageAPITransformerOptions + aspectRatio: number + }, + cache: GatsbyCache +): string | null | Promise => { if (!imageProps) { return null } @@ -27,15 +48,18 @@ const getBase64Image = (imageProps, cache) => { return null } + const placeholderWidth = imageProps.options.blurredOptions?.width || 20 // Keep aspect ratio, image format and other transform options const { aspectRatio } = imageProps - const originalFormat = imageProps.image.file.contentType.split(`/`)[1] - const toFormat = imageProps.options.toFormat + const originalFormat = imageProps.image.mimeType.split(`/`)[1] + const toFormat = + imageProps.options.blurredOptions?.toFormat || imageProps.options.toFormat + const imageOptions = { ...imageProps.options, toFormat, - width: 20, - height: Math.floor(20 / aspectRatio), + width: placeholderWidth, + height: Math.floor(placeholderWidth / aspectRatio), } const requestUrl = createUrl(imageProps.baseUrl, imageOptions) @@ -52,12 +76,10 @@ const getBase64Image = (imageProps, cache) => { return inFlight } - const loadImage = async () => { - const { - file: { contentType }, - } = imageProps.image + const loadImage = async (): Promise => { + const { mimeType } = imageProps.image - const extension = mimeTypeExtensions.get(contentType) + const extension = mimeTypeExtensions.get(mimeType) const absolutePath = await fetchRemoteFile({ url: requestUrl, @@ -80,42 +102,15 @@ const getBase64Image = (imageProps, cache) => { }) } -const getTracedSVG = async ({ image, options, cache }) => { - const { traceSVG } = await import(`gatsby-plugin-sharp`) - - const { - file: { contentType, url: imgUrl, fileName }, - } = image - - if (contentType.indexOf(`image/`) !== 0) { - return null - } - - const extension = mimeTypeExtensions.get(contentType) - const url = createUrl(imgUrl, options) - const name = path.basename(fileName, extension) - - const absolutePath = await fetchRemoteFile({ - url, - name, - directory: cache.directory, - ext: extension, - cacheKey: image.internal.contentDigest, - }) - - return traceSVG({ - file: { - internal: image.internal, - name: image.file.fileName, - extension, - absolutePath, - }, - args: { toFormat: ``, ...options.tracedSVGOptions }, - fileArgs: options, - }) -} - -const getDominantColor = async ({ image, options, cache }) => { +const getDominantColor = async ({ + image, + options, + cache, +}: { + image: IContentfulAsset + options: IContentfulImageAPITransformerOptions + cache: GatsbyCache +}): Promise => { let pluginSharp try { @@ -129,12 +124,10 @@ const getDominantColor = async ({ image, options, cache }) => { } try { - const { - file: { contentType, url: imgUrl, fileName }, - } = image + const { mimeType, url: imgUrl, filename } = image - if (contentType.indexOf(`image/`) !== 0) { - return null + if (mimeType.indexOf(`image/`) !== 0) { + return `rgba(0,0,0,0.5)` } // 256px should be enough to properly detect the dominant color @@ -142,9 +135,9 @@ const getDominantColor = async ({ image, options, cache }) => { options.width = 256 } - const extension = mimeTypeExtensions.get(contentType) + const extension = mimeTypeExtensions.get(mimeType) const url = createUrl(imgUrl, options) - const name = path.basename(fileName, extension) + const name = path.basename(filename, extension) const absolutePath = await fetchRemoteFile({ url, @@ -172,33 +165,39 @@ const getDominantColor = async ({ image, options, cache }) => { } } -function getBasicImageProps(image, args) { - let aspectRatio +function getBasicImageProps( + image, + args +): { + baseUrl: string + mimeType: string + aspectRatio: number + height: number + width: number +} { + let { width, height } = image if (args.width && args.height) { - aspectRatio = args.width / args.height - } else { - aspectRatio = - image.file.details.image.width / image.file.details.image.height + width = args.width + height = args.height } return { - baseUrl: image.file.url, - contentType: image.file.contentType, - aspectRatio, - width: image.file.details.image.width, - height: image.file.details.image.height, + baseUrl: image.url, + mimeType: image.mimeType, + aspectRatio: width / height, + width, + height, } } - // Generate image source data for gatsby-plugin-image export function generateImageSource( - filename, - width, - height, - toFormat, - _fit, // We use resizingBehavior instead - imageTransformOptions -) { + filename: string, + width: number, + height: number, + toFormat: "gif" | ImageFormat, + _fit: ImageFit | null, + imageTransformOptions: IContentfulImageAPITransformerOptions +): { width: number; height: number; format: string; src: string } | undefined { const imageFormatDefaults = imageTransformOptions[`${toFormat}Options`] if ( @@ -231,7 +230,7 @@ export function generateImageSource( height = CONTENTFUL_IMAGE_MAX_SIZE } - if (!validImageFormats.has(toFormat)) { + if (toFormat && !validImageFormats.has(toFormat)) { console.warn( `[gatsby-source-contentful] Invalid image format "${toFormat}". Supported types are jpg, png, webp and avif"` ) @@ -243,7 +242,10 @@ export function generateImageSource( height, toFormat, resizingBehavior, - background: backgroundColor?.replace(`#`, `rgb:`), + background: backgroundColor?.replace( + `#`, + `rgb:` + ) as contentfulImageApiBackgroundColor, quality, jpegProgressive, cropFocus, @@ -254,18 +256,16 @@ export function generateImageSource( let didShowTraceSVGRemovalWarning = false export async function resolveGatsbyImageData( - image, - options, - context, - info, - { cache } -) { + image: IContentfulAsset, + options: IContentfulImageAPITransformerOptions, + { cache }: { cache: GatsbyCache } +): Promise { if (!isImage(image)) return null const { generateImageData } = await import(`gatsby-plugin-image`) const { getPluginOptions, doMergeDefaults } = await import( - `gatsby-plugin-sharp/plugin-options` + `gatsby-plugin-sharp/plugin-options.js` ) const sharpOptions = getPluginOptions() @@ -297,17 +297,19 @@ export async function resolveGatsbyImageData( options.placeholder = `dominantColor` } - const { baseUrl, contentType, width, height } = getBasicImageProps( + const { baseUrl, mimeType, width, height, aspectRatio } = getBasicImageProps( image, options ) - let [, format] = contentType.split(`/`) - if (format === `jpeg`) { - format = `jpg` + let [, fileFormat] = mimeType.split(`/`) + if (fileFormat === `jpeg`) { + fileFormat = `jpg` } + const format: ImageFormat = fileFormat as ImageFormat + // Translate Contentful resize parameter to gatsby-plugin-image css object fit - const fitMap = new Map([ + const fitMap: Map = new Map([ [`pad`, `contain`], [`fill`, `cover`], [`scale`, `fill`], @@ -320,12 +322,13 @@ export async function resolveGatsbyImageData( pluginName: `gatsby-source-contentful`, sourceMetadata: { width, height, format }, filename: baseUrl, - generateImageSource, + generateImageSource: + generateImageSource as unknown as IGatsbyImageHelperArgs["generateImageSource"], fit: fitMap.get(options.resizingBehavior), - options, + options: options as unknown as Record, }) - let placeholderDataURI = null + let placeholderDataURI: string | null = null if (options.placeholder === `dominantColor`) { imageProps.backgroundColor = await getDominantColor({ @@ -341,15 +344,12 @@ export async function resolveGatsbyImageData( baseUrl, image, options, + aspectRatio, }, cache ) } - if (options.placeholder === `tracedSVG`) { - console.error(`this shouldn't happen`) - } - if (placeholderDataURI) { imageProps.placeholder = { fallback: placeholderDataURI } } diff --git a/packages/gatsby-source-contentful/src/hooks.js b/packages/gatsby-source-contentful/src/hooks.js deleted file mode 100644 index 109e4f110217a..0000000000000 --- a/packages/gatsby-source-contentful/src/hooks.js +++ /dev/null @@ -1,22 +0,0 @@ -// @ts-check -import { getImageData } from "gatsby-plugin-image" -import { useMemo } from "react" - -import { createUrl } from "./image-helpers" - -export function useContentfulImage({ image, ...props }) { - return useMemo( - () => - getImageData({ - baseUrl: image.url, - sourceWidth: image.width, - sourceHeight: image.height, - backgroundColor: null, - urlBuilder: ({ baseUrl, width, height, options, format }) => - createUrl(baseUrl, { ...options, height, width, toFormat: format }), - pluginName: `gatsby-source-contentful`, - ...props, - }), - [image.url, image.width, image.height, props] - ) -} diff --git a/packages/gatsby-source-contentful/src/hooks.ts b/packages/gatsby-source-contentful/src/hooks.ts new file mode 100644 index 0000000000000..939500bfc4b7b --- /dev/null +++ b/packages/gatsby-source-contentful/src/hooks.ts @@ -0,0 +1,38 @@ +import { getImageData, IGatsbyImageData } from "gatsby-plugin-image" +import { useMemo } from "react" + +import { createUrl } from "./image-helpers" +import type { + IContentfulAsset, + IContentfulImageAPITransformerOptions, +} from "./types/contentful" + +interface IUseContentfulImageArgs { + image: IContentfulAsset + [key: string]: unknown +} + +export function useContentfulImage({ + image, + ...props +}: IUseContentfulImageArgs): IGatsbyImageData { + return useMemo( + () => + getImageData({ + baseUrl: image.url, + sourceWidth: image.width, + sourceHeight: image.height, + backgroundColor: undefined, + urlBuilder: ({ baseUrl, width, height, options, format }) => + createUrl(baseUrl, { + ...options, + height, + width, + toFormat: format, + }), + pluginName: `gatsby-source-contentful`, + ...props, + }), + [image.url, image.width, image.height, props] + ) +} diff --git a/packages/gatsby-source-contentful/src/image-helpers.js b/packages/gatsby-source-contentful/src/image-helpers.ts similarity index 83% rename from packages/gatsby-source-contentful/src/image-helpers.js rename to packages/gatsby-source-contentful/src/image-helpers.ts index 3530c96b09ed2..79e2995cf4bab 100644 --- a/packages/gatsby-source-contentful/src/image-helpers.js +++ b/packages/gatsby-source-contentful/src/image-helpers.ts @@ -1,5 +1,8 @@ -// @ts-check import { URLSearchParams } from "url" +import type { + IContentfulAsset, + IContentfulImageAPIUrlBuilderOptions, +} from "./types/contentful" // Maximum value for size parameters in Contentful Image API // @see https://www.contentful.com/developers/docs/references/images-api/#/reference/resizing-&-cropping/specify-width-&-height @@ -19,12 +22,15 @@ export const mimeTypeExtensions = new Map([ ]) // Check if Contentful asset is actually an image -export function isImage(image) { - return mimeTypeExtensions.has(image?.file?.contentType) +export function isImage(image: IContentfulAsset): boolean { + return mimeTypeExtensions.has(image?.mimeType) } // Create a Contentful Image API url -export function createUrl(imgUrl, options = {}) { +export function createUrl( + imgUrl: string, + options: IContentfulImageAPIUrlBuilderOptions = {} +): string { // If radius is -1, we need to pass `max` to the API const cornerRadius = options.cornerRadius === -1 ? `max` : options.cornerRadius @@ -61,5 +67,5 @@ export function createUrl(imgUrl, options = {}) { } } - return `https:${imgUrl}?${searchParams.toString()}` + return `${imgUrl}?${searchParams.toString()}` } diff --git a/packages/gatsby-source-contentful/src/index.ts b/packages/gatsby-source-contentful/src/index.ts new file mode 100644 index 0000000000000..cc861147de119 --- /dev/null +++ b/packages/gatsby-source-contentful/src/index.ts @@ -0,0 +1,8 @@ +import * as importedSchemes from "./schemes" +export * from "./hooks" +export * from "./rich-text" +export * from "./types/contentful" +export * from "./image-helpers" +export type { IPluginOptions, IProcessedPluginOptions } from "./types/plugin" + +export const schemes = importedSchemes diff --git a/packages/gatsby-source-contentful/src/normalize.js b/packages/gatsby-source-contentful/src/normalize.js deleted file mode 100644 index c63e61904ad6a..0000000000000 --- a/packages/gatsby-source-contentful/src/normalize.js +++ /dev/null @@ -1,902 +0,0 @@ -// @ts-check -import stringify from "json-stringify-safe" -import _ from "lodash" -import { getGatsbyVersion } from "gatsby-core-utils" -import { lt, prerelease } from "semver" -import fastq from "fastq" - -/** - * @param {string} type - * @param {string} typePrefix - */ -export const makeTypeName = (type, typePrefix) => - _.upperFirst(_.camelCase(`${typePrefix} ${type}`)) - -const GATSBY_VERSION_MANIFEST_V2 = `4.3.0` -const gatsbyVersion = - (typeof getGatsbyVersion === `function` && getGatsbyVersion()) || `0.0.0` -const gatsbyVersionIsPrerelease = prerelease(gatsbyVersion) -const shouldUpgradeGatsbyVersion = - lt(gatsbyVersion, GATSBY_VERSION_MANIFEST_V2) && !gatsbyVersionIsPrerelease - -export const getLocalizedField = ({ field, locale, localesFallback }) => { - if (!field) { - return null - } - if (!_.isUndefined(field[locale.code])) { - return field[locale.code] - } else if ( - !_.isUndefined(locale.code) && - !_.isUndefined(localesFallback[locale.code]) - ) { - return getLocalizedField({ - field, - locale: { code: localesFallback[locale.code] }, - localesFallback, - }) - } else { - return null - } -} -export const buildFallbackChain = locales => { - const localesFallback = {} - _.each( - locales, - locale => (localesFallback[locale.code] = locale.fallbackCode) - ) - return localesFallback -} -const makeGetLocalizedField = - ({ locale, localesFallback }) => - field => - getLocalizedField({ field, locale, localesFallback }) - -export const makeId = ({ spaceId, id, currentLocale, defaultLocale, type }) => { - const normalizedType = type.startsWith(`Deleted`) - ? type.substring(`Deleted`.length) - : type - return currentLocale === defaultLocale - ? `${spaceId}___${id}___${normalizedType}` - : `${spaceId}___${id}___${normalizedType}___${currentLocale}` -} - -const makeMakeId = - ({ currentLocale, defaultLocale, createNodeId }) => - (spaceId, id, type) => - createNodeId(makeId({ spaceId, id, currentLocale, defaultLocale, type })) - -export const buildEntryList = ({ contentTypeItems, currentSyncData }) => { - // Create buckets for each type sys.id that we care about (we will always want an array for each, even if its empty) - const map = new Map( - contentTypeItems.map(contentType => [contentType.sys.id, []]) - ) - // Now fill the buckets. Ignore entries for which there exists no bucket. (This happens when filterContentType is used) - currentSyncData.entries.map(entry => { - const arr = map.get(entry.sys.contentType.sys.id) - if (arr) { - arr.push(entry) - } - }) - // Order is relevant, must map 1:1 to contentTypeItems array - return contentTypeItems.map(contentType => map.get(contentType.sys.id)) -} - -export const buildResolvableSet = ({ - entryList, - existingNodes = new Map(), - assets = [], -}) => { - const resolvable = new Set() - existingNodes.forEach(node => { - // We need to add only root level resolvable (assets and entries) - // Derived nodes (markdown or JSON) will be recreated if needed. - resolvable.add(`${node.contentful_id}___${node.sys.type}`) - }) - - entryList.forEach(entries => { - entries.forEach(entry => - resolvable.add(`${entry.sys.id}___${entry.sys.type}`) - ) - }) - - assets.forEach(assetItem => - resolvable.add(`${assetItem.sys.id}___${assetItem.sys.type}`) - ) - - return resolvable -} - -function cleanupReferencesFromEntry(foreignReferenceMapState, entry) { - const { links, backLinks } = foreignReferenceMapState - const entryId = entry.sys.id - - const entryLinks = links[entryId] - if (entryLinks) { - entryLinks.forEach(link => { - const backLinksForLink = backLinks[link] - if (backLinksForLink) { - const newBackLinks = backLinksForLink.filter(({ id }) => id !== entryId) - if (newBackLinks.lenth > 0) { - backLinks[link] = newBackLinks - } else { - delete backLinks[link] - } - } - }) - } - - delete links[entryId] -} - -export const buildForeignReferenceMap = ({ - contentTypeItems, - entryList, - resolvable, - defaultLocale, - space, - useNameForId, - previousForeignReferenceMapState, - deletedEntries, -}) => { - const foreignReferenceMapState = previousForeignReferenceMapState || { - links: {}, - backLinks: {}, - } - - const { links, backLinks } = foreignReferenceMapState - - for (const deletedEntry of deletedEntries) { - // remove stored entries from entry that is being deleted - cleanupReferencesFromEntry(foreignReferenceMapState, deletedEntry) - } - - contentTypeItems.forEach((contentTypeItem, i) => { - // 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() - } - - entryList[i].forEach(entryItem => { - // clear links added in previous runs for given entry, as we will recreate them anyway - cleanupReferencesFromEntry(foreignReferenceMapState, entryItem) - - const entryItemFields = entryItem.fields - Object.keys(entryItemFields).forEach(entryItemFieldKey => { - if (entryItemFields[entryItemFieldKey]) { - const entryItemFieldValue = - entryItemFields[entryItemFieldKey][defaultLocale] - // If this is an array of single reference object - // add to the reference map, otherwise ignore. - if (Array.isArray(entryItemFieldValue)) { - if ( - entryItemFieldValue[0] && - entryItemFieldValue[0].sys && - entryItemFieldValue[0].sys.type && - entryItemFieldValue[0].sys.id - ) { - entryItemFieldValue.forEach(v => { - const key = `${v.sys.id}___${v.sys.linkType || v.sys.type}` - // Don't create link to an unresolvable field. - if (!resolvable.has(key)) { - return - } - - if (!backLinks[key]) { - backLinks[key] = [] - } - backLinks[key].push({ - name: `${contentTypeItemId}___NODE`, - id: entryItem.sys.id, - spaceId: space.sys.id, - type: entryItem.sys.type, - }) - - if (!links[entryItem.sys.id]) { - links[entryItem.sys.id] = [] - } - - links[entryItem.sys.id].push(key) - }) - } - } else if ( - entryItemFieldValue?.sys?.type && - entryItemFieldValue.sys.id - ) { - const key = `${entryItemFieldValue.sys.id}___${ - entryItemFieldValue.sys.linkType || entryItemFieldValue.sys.type - }` - // Don't create link to an unresolvable field. - if (!resolvable.has(key)) { - return - } - - if (!backLinks[key]) { - backLinks[key] = [] - } - backLinks[key].push({ - name: `${contentTypeItemId}___NODE`, - id: entryItem.sys.id, - spaceId: space.sys.id, - type: entryItem.sys.type, - }) - - if (!links[entryItem.sys.id]) { - links[entryItem.sys.id] = [] - } - - links[entryItem.sys.id].push(key) - } - } - }) - }) - }) - - return foreignReferenceMapState -} - -function prepareTextNode(id, node, key, text) { - const str = _.isString(text) ? text : `` - const textNode = { - id, - parent: node.id, - children: [], - [key]: str, - internal: { - type: _.camelCase(`${node.internal.type} ${key} TextNode`), - mediaType: `text/markdown`, - content: str, - // entryItem.sys.updatedAt is source of truth from contentful - contentDigest: node.updatedAt, - }, - sys: { - type: `TextNode`, - }, - } - - node.children = node.children.concat([id]) - - return textNode -} - -function prepareJSONNode(id, node, key, content) { - const str = JSON.stringify(content) - const JSONNode = { - ...(_.isPlainObject(content) ? { ...content } : { content: content }), - id, - parent: node.id, - children: [], - internal: { - type: _.camelCase(`${node.internal.type} ${key} JSONNode`), - mediaType: `application/json`, - content: str, - // entryItem.sys.updatedAt is source of truth from contentful - contentDigest: node.updatedAt, - }, - sys: { - type: `JsonNode`, - }, - } - - node.children = node.children.concat([id]) - - return JSONNode -} - -let numberOfContentSyncDebugLogs = 0 -const maxContentSyncDebugLogTimes = 50 - -let warnOnceForNoSupport = false -let warnOnceToUpgradeGatsby = false - -/** - * This fn creates node manifests which are used for Gatsby Cloud Previews via the Content Sync API/feature. - * Content Sync routes a user from Contentful to a page created from the entry data they're interested in previewing. - */ - -function contentfulCreateNodeManifest({ - pluginConfig, - entryItem, - entryNode, - space, - unstable_createNodeManifest, -}) { - const isPreview = pluginConfig.get(`host`) === `preview.contentful.com` - - const createNodeManifestIsSupported = - typeof unstable_createNodeManifest === `function` - - const shouldCreateNodeManifest = isPreview && createNodeManifestIsSupported - - const updatedAt = entryItem.sys.updatedAt - - const manifestId = `${space.sys.id}-${entryItem.sys.id}-${updatedAt}` - - if ( - process.env.CONTENTFUL_DEBUG_NODE_MANIFEST === `true` && - numberOfContentSyncDebugLogs <= maxContentSyncDebugLogTimes - ) { - numberOfContentSyncDebugLogs++ - - console.info( - JSON.stringify({ - isPreview, - createNodeManifestIsSupported, - shouldCreateNodeManifest, - manifestId, - entryItemSysUpdatedAt: updatedAt, - }) - ) - } - - if (shouldCreateNodeManifest) { - if (shouldUpgradeGatsbyVersion && !warnOnceToUpgradeGatsby) { - console.warn( - `Your site is doing more work than it needs to for Preview, upgrade to Gatsby ^${GATSBY_VERSION_MANIFEST_V2} for better performance` - ) - warnOnceToUpgradeGatsby = true - } - - unstable_createNodeManifest({ - manifestId, - node: entryNode, - updatedAtUTC: updatedAt, - }) - } else if ( - isPreview && - !createNodeManifestIsSupported && - !warnOnceForNoSupport - ) { - console.warn( - `Contentful: Your version of Gatsby core doesn't support Content Sync (via the unstable_createNodeManifest action). Please upgrade to the latest version to use Content Sync in your site.` - ) - warnOnceForNoSupport = true - } -} - -function makeQueuedCreateNode({ nodeCount, createNode }) { - if (nodeCount > 5000) { - let createdNodeCount = 0 - - const createNodesQueue = fastq((node, cb) => { - function runCreateNode() { - const maybeNodePromise = createNode(node) - - // checking for `.then` is vastly more performant than using `instanceof Promise` - if (`then` in maybeNodePromise) { - maybeNodePromise.then(() => { - cb(null) - }) - } else { - cb(null) - } - } - - if (++createdNodeCount % 100 === 0) { - setImmediate(() => { - runCreateNode() - }) - } else { - runCreateNode() - } - }, 10) - - const queueFinished = new Promise(resolve => { - createNodesQueue.drain = () => { - resolve(null) - } - }) - - return { - create: (node, callback) => createNodesQueue.push(node, callback), - createNodesPromise: queueFinished, - } - } else { - const nodePromises = [] - const queueFinished = () => Promise.all(nodePromises) - - return { - create: node => nodePromises.push(createNode(node)), - createNodesPromise: queueFinished(), - } - } -} - -export const createNodesForContentType = async ({ - contentTypeItem, - restrictedNodeFields, - conflictFieldPrefix, - entries, - unstable_createNodeManifest, - createNode, - createNodeId, - getNode, - resolvable, - foreignReferenceMap, - defaultLocale, - locales, - space, - useNameForId, - pluginConfig, -}) => { - const { create, createNodesPromise } = makeQueuedCreateNode({ - nodeCount: entries.length, - createNode, - }) - - const typePrefix = pluginConfig.get(`typePrefix`) - - // 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 - } else { - contentTypeItemId = contentTypeItem.sys.id - } - - // Create a node for the content type - const contentTypeNode = { - id: createNodeId(contentTypeItemId), - parent: null, - children: [], - name: contentTypeItem.name, - displayField: contentTypeItem.displayField, - description: contentTypeItem.description, - internal: { - type: `${makeTypeName(`ContentType`, typePrefix)}`, - contentDigest: contentTypeItem.sys.updatedAt, - }, - sys: { - type: contentTypeItem.sys.type, - }, - } - - create(contentTypeNode) - - locales.forEach(locale => { - const localesFallback = buildFallbackChain(locales) - const mId = makeMakeId({ - currentLocale: locale.code, - defaultLocale, - createNodeId, - }) - const getField = makeGetLocalizedField({ - locale, - localesFallback, - }) - - // Warn about any field conflicts - const conflictFields = [] - contentTypeItem.fields.forEach(contentTypeItemField => { - const fieldName = contentTypeItemField.id - if (restrictedNodeFields.includes(fieldName)) { - console.log( - `Restricted field found for ContentType ${contentTypeItemId} and field ${fieldName}. Prefixing with ${conflictFieldPrefix}.` - ) - conflictFields.push(fieldName) - } - }) - - const childrenNodes = [] - - // First create nodes for each of the entries of that content type - const entryNodes = entries.map(entryItem => { - const entryNodeId = mId( - space.sys.id, - entryItem.sys.id, - entryItem.sys.type - ) - - const existingNode = getNode(entryNodeId) - if (existingNode?.updatedAt === entryItem.sys.updatedAt) { - // The Contentful model has `.sys.updatedAt` leading for an entry. If the updatedAt value - // of an entry did not change, then we can trust that none of its children were changed either. - return null - } - - // Get localized fields. - const entryItemFields = _.mapValues(entryItem.fields, (v, k) => { - const fieldProps = contentTypeItem.fields.find(field => field.id === k) - - const localizedField = fieldProps.localized - ? getField(v) - : v[defaultLocale] - - return localizedField - }) - - // Prefix any conflicting fields - // https://github.com/gatsbyjs/gatsby/pull/1084#pullrequestreview-41662888 - conflictFields.forEach(conflictField => { - entryItemFields[`${conflictFieldPrefix}${conflictField}`] = - entryItemFields[conflictField] - delete entryItemFields[conflictField] - }) - - // Add linkages to other nodes based on foreign references - Object.keys(entryItemFields).forEach(entryItemFieldKey => { - if (entryItemFields[entryItemFieldKey]) { - const entryItemFieldValue = entryItemFields[entryItemFieldKey] - if (Array.isArray(entryItemFieldValue)) { - if (entryItemFieldValue[0]?.sys?.type === `Link`) { - // Check if there are any values in entryItemFieldValue to prevent - // creating an empty node field in case when original key field value - // is empty due to links to missing entities - const resolvableEntryItemFieldValue = entryItemFieldValue - .filter(function (v) { - return resolvable.has( - `${v.sys.id}___${v.sys.linkType || v.sys.type}` - ) - }) - .map(function (v) { - return mId( - space.sys.id, - v.sys.id, - v.sys.linkType || v.sys.type - ) - }) - if (resolvableEntryItemFieldValue.length !== 0) { - entryItemFields[`${entryItemFieldKey}___NODE`] = - resolvableEntryItemFieldValue - } - - delete entryItemFields[entryItemFieldKey] - } - } else if (entryItemFieldValue?.sys?.type === `Link`) { - if ( - resolvable.has( - `${entryItemFieldValue.sys.id}___${ - entryItemFieldValue.sys.linkType || - entryItemFieldValue.sys.type - }` - ) - ) { - entryItemFields[`${entryItemFieldKey}___NODE`] = mId( - space.sys.id, - entryItemFieldValue.sys.id, - entryItemFieldValue.sys.linkType || entryItemFieldValue.sys.type - ) - } - delete entryItemFields[entryItemFieldKey] - } - } - }) - - // Add reverse linkages if there are any for this node - const foreignReferences = - foreignReferenceMap[`${entryItem.sys.id}___${entryItem.sys.type}`] - if (foreignReferences) { - foreignReferences.forEach(foreignReference => { - const existingReference = entryItemFields[foreignReference.name] - if (existingReference) { - // If the existing reference is a string, we're dealing with a - // many-to-one reference which has already been recorded, so we can - // skip it. However, if it is an array, add it: - if (Array.isArray(existingReference)) { - entryItemFields[foreignReference.name].push( - mId( - foreignReference.spaceId, - foreignReference.id, - foreignReference.type - ) - ) - } - } else { - // If there is one foreign reference, there can be many. - // Best to be safe and put it in an array to start with. - entryItemFields[foreignReference.name] = [ - mId( - foreignReference.spaceId, - foreignReference.id, - foreignReference.type - ), - ] - } - }) - } - - let entryNode = { - id: entryNodeId, - spaceId: space.sys.id, - contentful_id: entryItem.sys.id, - createdAt: entryItem.sys.createdAt, - updatedAt: entryItem.sys.updatedAt, - parent: null, - children: [], - internal: { - type: `${makeTypeName(contentTypeItemId, typePrefix)}`, - }, - sys: { - type: entryItem.sys.type, - }, - } - - contentfulCreateNodeManifest({ - pluginConfig, - entryItem, - entryNode, - space, - unstable_createNodeManifest, - }) - - // Revision applies to entries, assets, and content types - if (entryItem.sys.revision) { - entryNode.sys.revision = entryItem.sys.revision - } - - // Content type applies to entries only - if (entryItem.sys.contentType) { - entryNode.sys.contentType = entryItem.sys.contentType - } - - // Replace text fields with text nodes so we can process their markdown - // into HTML. - Object.keys(entryItemFields).forEach(entryItemFieldKey => { - // Ignore fields with "___node" as they're already handled - // and won't be a text field. - if (entryItemFieldKey.includes(`___`)) { - return - } - - const fieldType = contentTypeItem.fields.find( - f => - (restrictedNodeFields.includes(f.id) - ? `${conflictFieldPrefix}${f.id}` - : f.id) === entryItemFieldKey - ).type - if (fieldType === `Text`) { - const textNodeId = createNodeId( - `${entryNodeId}${entryItemFieldKey}TextNode` - ) - - // The Contentful model has `.sys.updatedAt` leading for an entry. If the updatedAt value - // of an entry did not change, then we can trust that none of its children were changed either. - // (That's why child nodes use the updatedAt of the parent node as their digest, too) - const existingNode = getNode(textNodeId) - if (existingNode?.updatedAt !== entryItem.sys.updatedAt) { - const textNode = prepareTextNode( - textNodeId, - entryNode, - entryItemFieldKey, - entryItemFields[entryItemFieldKey] - ) - - childrenNodes.push(textNode) - } - - entryItemFields[`${entryItemFieldKey}___NODE`] = textNodeId - delete entryItemFields[entryItemFieldKey] - } else if ( - fieldType === `RichText` && - _.isPlainObject(entryItemFields[entryItemFieldKey]) - ) { - const fieldValue = entryItemFields[entryItemFieldKey] - - const rawReferences = [] - - // Locate all Contentful Links within the rich text data - const traverse = obj => { - // eslint-disable-next-line guard-for-in - for (const k in obj) { - const v = obj[k] - if (v && v.sys && v.sys.type === `Link`) { - rawReferences.push(v) - } else if (v && typeof v === `object`) { - traverse(v) - } - } - } - - traverse(fieldValue) - - // Build up resolvable reference list - const resolvableReferenceIds = new Set() - rawReferences - .filter(function (v) { - return resolvable.has( - `${v.sys.id}___${v.sys.linkType || v.sys.type}` - ) - }) - .forEach(function (v) { - resolvableReferenceIds.add( - mId(space.sys.id, v.sys.id, v.sys.linkType || v.sys.type) - ) - }) - - entryItemFields[entryItemFieldKey] = { - raw: stringify(fieldValue), - references___NODE: [...resolvableReferenceIds], - } - } else if ( - fieldType === `Object` && - _.isPlainObject(entryItemFields[entryItemFieldKey]) - ) { - const jsonNodeId = createNodeId( - `${entryNodeId}${entryItemFieldKey}JSONNode` - ) - - // The Contentful model has `.sys.updatedAt` leading for an entry. If the updatedAt value - // of an entry did not change, then we can trust that none of its children were changed either. - // (That's why child nodes use the updatedAt of the parent node as their digest, too) - const existingNode = getNode(jsonNodeId) - if (existingNode?.updatedAt !== entryItem.sys.updatedAt) { - const jsonNode = prepareJSONNode( - jsonNodeId, - entryNode, - entryItemFieldKey, - entryItemFields[entryItemFieldKey] - ) - childrenNodes.push(jsonNode) - } - - entryItemFields[`${entryItemFieldKey}___NODE`] = jsonNodeId - delete entryItemFields[entryItemFieldKey] - } else if ( - fieldType === `Object` && - _.isArray(entryItemFields[entryItemFieldKey]) - ) { - entryItemFields[`${entryItemFieldKey}___NODE`] = [] - - entryItemFields[entryItemFieldKey].forEach((obj, i) => { - const jsonNodeId = createNodeId( - `${entryNodeId}${entryItemFieldKey}${i}JSONNode` - ) - - // The Contentful model has `.sys.updatedAt` leading for an entry. If the updatedAt value - // of an entry did not change, then we can trust that none of its children were changed either. - // (That's why child nodes use the updatedAt of the parent node as their digest, too) - const existingNode = getNode(jsonNodeId) - if (existingNode?.updatedAt !== entryItem.sys.updatedAt) { - const jsonNode = prepareJSONNode( - jsonNodeId, - entryNode, - entryItemFieldKey, - obj - ) - childrenNodes.push(jsonNode) - } - - entryItemFields[`${entryItemFieldKey}___NODE`].push(jsonNodeId) - }) - - delete entryItemFields[entryItemFieldKey] - } - }) - - entryNode = { - ...entryItemFields, - ...entryNode, - node_locale: locale.code, - } - - // The content of an entry is guaranteed to be updated if and only if the .sys.updatedAt field changed - entryNode.internal.contentDigest = entryItem.sys.updatedAt - - // Link tags - if (pluginConfig.get(`enableTags`)) { - entryNode.metadata = { - tags___NODE: entryItem.metadata.tags.map(tag => - createNodeId(`ContentfulTag__${space.sys.id}__${tag.sys.id}`) - ), - } - } - - return entryNode - }) - - entryNodes.forEach((entryNode, index) => { - // entry nodes may be undefined here if the node was previously already created - if (entryNode) { - create(entryNode, () => { - entryNodes[index] = undefined - }) - } - }) - childrenNodes.forEach((entryNode, index) => { - // entry nodes may be undefined here if the node was previously already created - if (entryNode) { - create(entryNode, () => { - childrenNodes[index] = undefined - }) - } - }) - }) - - return createNodesPromise -} - -export const createAssetNodes = async ({ - assetItem, - createNode, - createNodeId, - defaultLocale, - locales, - space, - pluginConfig, -}) => { - const { create, createNodesPromise } = makeQueuedCreateNode({ - createNode, - nodeCount: locales.length, - }) - - const assetNodes = [] - - locales.forEach(locale => { - const localesFallback = buildFallbackChain(locales) - const mId = makeMakeId({ - currentLocale: locale.code, - defaultLocale, - createNodeId, - }) - const getField = makeGetLocalizedField({ - locale, - localesFallback, - }) - - const file = getField(assetItem.fields?.file) ?? null - - // Skip empty and unprocessed assets in Preview API - if (!file || !file.url || !file.contentType || !file.fileName) { - return - } - - const assetNode = { - contentful_id: assetItem.sys.id, - spaceId: space.sys.id, - id: mId(space.sys.id, assetItem.sys.id, assetItem.sys.type), - createdAt: assetItem.sys.createdAt, - updatedAt: assetItem.sys.updatedAt, - parent: null, - children: [], - file, - title: assetItem.fields.title ? getField(assetItem.fields.title) : ``, - description: assetItem.fields.description - ? getField(assetItem.fields.description) - : ``, - node_locale: locale.code, - internal: { - type: makeTypeName(`Asset`, pluginConfig.get(`typePrefix`)), - }, - sys: { - type: assetItem.sys.type, - }, - url: `https:${file.url}`, - placeholderUrl: `https:${file.url}?w=%width%&h=%height%`, - // These fields are optional for edge cases in the Preview API and Contentfuls asset processing - mimeType: file.contentType, - filename: file.fileName, - width: file.details?.image?.width ?? null, - height: file.details?.image?.height ?? null, - size: file.details?.size ?? null, - } - - // Link tags - if (pluginConfig.get(`enableTags`)) { - assetNode.metadata = { - tags___NODE: assetItem.metadata.tags.map(tag => - createNodeId(`ContentfulTag__${space.sys.id}__${tag.sys.id}`) - ), - } - } - - // Revision applies to entries, assets, and content types - if (assetItem.sys.revision) { - assetNode.sys.revision = assetItem.sys.revision - } - - // The content of an entry is guaranteed to be updated if and only if the .sys.updatedAt field changed - assetNode.internal.contentDigest = assetItem.sys.updatedAt - - assetNodes.push(assetNode) - create(assetNode) - }) - - await createNodesPromise - return assetNodes -} diff --git a/packages/gatsby-source-contentful/src/normalize.ts b/packages/gatsby-source-contentful/src/normalize.ts new file mode 100644 index 0000000000000..31c1eaf39f74e --- /dev/null +++ b/packages/gatsby-source-contentful/src/normalize.ts @@ -0,0 +1,991 @@ +import type { Actions, Node, SourceNodesArgs } from "gatsby" +import _ from "lodash" +import { getGatsbyVersion } from "gatsby-core-utils" +import { lt, prerelease } from "semver" +import fastq from "fastq" +import type { queue, done } from "fastq" + +import { restrictedNodeFields, conflictFieldPrefix } from "./config" +import type { + IContentfulAsset, + IContentfulEntry, + IContentfulLink, + ILocalizedField, + IEntryWithAllLocalesAndWithoutLinkResolution, +} from "./types/contentful" +import type { + SyncCollection, + Asset, + ContentType, + Space, + Locale, + LocaleCode, + AssetFile, + FieldsType, + EntrySkeletonType, + DeletedEntry, + EntitySys, + ContentTypeField, +} from "contentful" +import type { + IProcessedPluginOptions, + MarkdownFieldDefinition, +} from "./types/plugin" +import { detectMarkdownField, makeContentTypeIdMap } from "./utils" + +export const makeTypeName = ( + type: string, + typePrefix = `ContentfulContentType` +): string => _.upperFirst(_.camelCase(`${typePrefix} ${type}`)) + +const GATSBY_VERSION_MANIFEST_V2 = `4.3.0` +const gatsbyVersion = + (typeof getGatsbyVersion === `function` && getGatsbyVersion()) || `0.0.0` +const gatsbyVersionIsPrerelease = prerelease(gatsbyVersion) +const shouldUpgradeGatsbyVersion = + lt(gatsbyVersion, GATSBY_VERSION_MANIFEST_V2) && !gatsbyVersionIsPrerelease + +interface IContententfulLocaleFallback { + [key: string]: LocaleCode +} + +export const getLocalizedField = ({ + field, + locale, + localesFallback, +}: { + field: ILocalizedField + locale: Locale + localesFallback: IContententfulLocaleFallback +}): T | null => { + if (!field) { + return null + } + if (!_.isUndefined(field[locale.code])) { + return field[locale.code] as T + } else if ( + !_.isUndefined(locale.code) && + !_.isUndefined(localesFallback[locale.code]) + ) { + return getLocalizedField({ + field, + locale: { ...locale, code: localesFallback[locale.code] }, + localesFallback, + }) + } else { + return null + } +} +export const buildFallbackChain = ( + locales: Array +): IContententfulLocaleFallback => { + const localesFallback = {} + _.each( + locales, + locale => (localesFallback[locale.code] = locale.fallbackCode) + ) + return localesFallback +} +const makeGetLocalizedField = + ({ locale, localesFallback }) => + (field: ILocalizedField): T | null => + getLocalizedField({ field, locale, localesFallback }) + +export const makeId = ({ + spaceId, + id, + currentLocale, + defaultLocale, + type, +}: { + spaceId: string + id: string + currentLocale: string + defaultLocale: string + type: string +}): string => { + const normalizedType = type.startsWith(`Deleted`) + ? type.substring(`Deleted`.length) + : type + return currentLocale === defaultLocale + ? `${spaceId}___${id}___${normalizedType}` + : `${spaceId}___${id}___${normalizedType}___${currentLocale}` +} + +const makeMakeId = + ({ currentLocale, defaultLocale, createNodeId }) => + (spaceId, id, type): string => + createNodeId(makeId({ spaceId, id, currentLocale, defaultLocale, type })) + +// Generates an unique id per space for reference resolving +export const createRefId = ( + node: + | IEntryWithAllLocalesAndWithoutLinkResolution + | IContentfulEntry + | Asset, + spaceId: string +): string => `${spaceId}___${node.sys.id}___${node.sys.type}` + +export const createLinkRefId = ( + link: IContentfulLink, + spaceId: string +): string => `${spaceId}___${link.sys.id}___${link.sys.linkType}` + +export const buildEntryList = ({ + contentTypeItems, + currentSyncData, +}: { + contentTypeItems: Array + currentSyncData: SyncCollection< + EntrySkeletonType, + "WITH_ALL_LOCALES" | "WITHOUT_LINK_RESOLUTION" + > +}): Array< + Array> +> => { + // Create buckets for each type sys.id that we care about (we will always want an array for each, even if its empty) + const map: Map< + string, + Array> + > = new Map(contentTypeItems.map(contentType => [contentType.sys.id, []])) + // Now fill the buckets. Ignore entries for which there exists no bucket. (This happens when filterContentType is used) + currentSyncData.entries.map(entry => { + const arr = map.get(entry.sys.contentType.sys.id) + if (arr) { + arr.push(entry) + } + }) + // Order is relevant, must map 1:1 to contentTypeItems array + return contentTypeItems.map(contentType => map.get(contentType.sys.id) || []) +} + +export const buildResolvableSet = ({ + entryList, + existingNodes = new Map(), + assets = [], + spaceId, +}: { + entryList: Array< + Array> + > + existingNodes: Map + assets: Array + spaceId: string +}): Set => { + const resolvable: Set = new Set() + existingNodes.forEach(node => { + if (node.internal.owner === `gatsby-source-contentful` && node?.sys?.id) { + // We need to add only root level resolvable (assets and entries) + // Derived nodes (markdown or JSON) will be recreated if needed. + resolvable.add(createRefId(node, spaceId)) + } + }) + + entryList.forEach(entries => { + entries.forEach(entry => resolvable.add(createRefId(entry, spaceId))) + }) + + assets.forEach(assetItem => resolvable.add(createRefId(assetItem, spaceId))) + + return resolvable +} + +interface IForeignReference { + name: string + id: string + spaceId: string + type: string // Could be based on constances? +} + +interface IForeignReferenceMap { + [key: string]: Array +} + +interface IForeignReferenceMapState { + links: { [key: string]: Array } + backLinks: IForeignReferenceMap +} + +function cleanupReferencesFromEntry( + foreignReferenceMapState: IForeignReferenceMapState, + entry: { sys: EntitySys } +): void { + const { links, backLinks } = foreignReferenceMapState + const entryId = entry.sys.id + + const entryLinks = links[entryId] + if (entryLinks) { + entryLinks.forEach(link => { + const backLinksForLink = backLinks[link] + if (backLinksForLink) { + const newBackLinks = backLinksForLink.filter(({ id }) => id !== entryId) + if (newBackLinks.length > 0) { + backLinks[link] = newBackLinks + } else { + delete backLinks[link] + } + } + }) + } + + delete links[entryId] +} + +export const buildForeignReferenceMap = ({ + contentTypeItems, + entryList, + resolvable, + defaultLocale, + space, + useNameForId, + contentTypePrefix, + previousForeignReferenceMapState, + deletedEntries, +}: { + contentTypeItems: Array + entryList: Array< + Array> + > + resolvable: Set + defaultLocale: string + space: Space + useNameForId: boolean + contentTypePrefix: string + previousForeignReferenceMapState?: IForeignReferenceMapState + deletedEntries: Array + createNodeId +}): IForeignReferenceMapState => { + const foreignReferenceMapState: IForeignReferenceMapState = + previousForeignReferenceMapState || { + links: {}, + backLinks: {}, + } + + const { links, backLinks } = foreignReferenceMapState + + for (const deletedEntry of deletedEntries) { + // remove stored entries from entry that is being deleted + cleanupReferencesFromEntry(foreignReferenceMapState, deletedEntry) + } + + const contentTypeIdMap = makeContentTypeIdMap( + contentTypeItems, + contentTypePrefix, + useNameForId + ) + + contentTypeItems.forEach((contentTypeItem, i) => { + const contentTypeItemId = contentTypeIdMap.get( + contentTypeItem.sys.id + ) as string + + entryList[i].forEach(entryItem => { + // clear links added in previous runs for given entry, as we will recreate them anyway + cleanupReferencesFromEntry(foreignReferenceMapState, entryItem) + + const entryItemFields = entryItem.fields + Object.keys(entryItemFields).forEach(entryItemFieldKey => { + if (entryItemFields[entryItemFieldKey]) { + const entryItemFieldValue = + entryItemFields[entryItemFieldKey][defaultLocale] + // If this is an array of single reference object + // add to the reference map, otherwise ignore. + if (Array.isArray(entryItemFieldValue)) { + if ( + entryItemFieldValue[0] && + entryItemFieldValue[0].sys && + entryItemFieldValue[0].sys.type && + entryItemFieldValue[0].sys.id + ) { + entryItemFieldValue.forEach(v => { + const key = createLinkRefId(v, space.sys.id) + // Don't create link to an unresolvable field. + if (!resolvable.has(key)) { + return + } + + if (!backLinks[key]) { + backLinks[key] = [] + } + backLinks[key].push({ + name: contentTypeItemId, + id: entryItem.sys.id, + spaceId: space.sys.id, + type: entryItem.sys.type, + }) + + if (!links[entryItem.sys.id]) { + links[entryItem.sys.id] = [] + } + + links[entryItem.sys.id].push(key) + }) + } + } else if ( + entryItemFieldValue?.sys?.type && + entryItemFieldValue.sys.id + ) { + const key = createLinkRefId(entryItemFieldValue, space.sys.id) + // Don't create link to an unresolvable field. + if (!resolvable.has(key)) { + return + } + + if (!backLinks[key]) { + backLinks[key] = [] + } + backLinks[key].push({ + name: contentTypeItemId, + id: entryItem.sys.id, + spaceId: space.sys.id, + type: entryItem.sys.type, + }) + + if (!links[entryItem.sys.id]) { + links[entryItem.sys.id] = [] + } + + links[entryItem.sys.id].push(key) + } + } + }) + }) + }) + + return foreignReferenceMapState +} + +function prepareMarkdownNode( + id: string, + node: IContentfulEntry, + _key: string, + text: unknown +): Node { + const str = _.isString(text) ? text : `` + const markdownNode: Node = { + id, + parent: node.id, + raw: str, + internal: { + type: `ContentfulMarkdown`, + mediaType: `text/markdown`, + content: str, + // entryItem.sys.publishedAt is source of truth from contentful + contentDigest: node.sys.publishedAt, + }, + children: [], + } + + return markdownNode +} + +let numberOfContentSyncDebugLogs = 0 +const maxContentSyncDebugLogTimes = 50 + +let warnOnceForNoSupport = false +let warnOnceToUpgradeGatsby = false + +/** + * This fn creates node manifests which are used for Gatsby Cloud Previews via the Content Sync API/feature. + * Content Sync routes a user from Contentful to a page created from the entry data they're interested in previewing. + */ + +function contentfulCreateNodeManifest({ + pluginConfig, + entryItem, + entryNode, + space, + // eslint-disable-next-line @typescript-eslint/naming-convention + unstable_createNodeManifest, +}: { + pluginConfig: IProcessedPluginOptions + entryItem: IEntryWithAllLocalesAndWithoutLinkResolution + entryNode: IContentfulEntry + space: Space + unstable_createNodeManifest: SourceNodesArgs["unstable_createNodeManifest"] +}): void { + const isPreview = pluginConfig.get(`host`) === `preview.contentful.com` + + const createNodeManifestIsSupported = + typeof unstable_createNodeManifest === `function` + + const shouldCreateNodeManifest = isPreview && createNodeManifestIsSupported + + const updatedAt = entryItem.sys.updatedAt + + const manifestId = `${space.sys.id}-${entryItem.sys.id}-${updatedAt}` + + if ( + process.env.CONTENTFUL_DEBUG_NODE_MANIFEST === `true` && + numberOfContentSyncDebugLogs <= maxContentSyncDebugLogTimes + ) { + numberOfContentSyncDebugLogs++ + + console.info( + JSON.stringify({ + isPreview, + createNodeManifestIsSupported, + shouldCreateNodeManifest, + manifestId, + entryItemSysUpdatedAt: updatedAt, + }) + ) + } + + if (shouldCreateNodeManifest) { + if (shouldUpgradeGatsbyVersion && !warnOnceToUpgradeGatsby) { + console.warn( + `Your site is doing more work than it needs to for Preview, upgrade to Gatsby ^${GATSBY_VERSION_MANIFEST_V2} for better performance` + ) + warnOnceToUpgradeGatsby = true + } + + unstable_createNodeManifest({ + manifestId, + node: entryNode, + updatedAtUTC: updatedAt, + }) + } else if ( + isPreview && + !createNodeManifestIsSupported && + !warnOnceForNoSupport + ) { + console.warn( + `Contentful: Your version of Gatsby core doesn't support Content Sync (via the unstable_createNodeManifest action). Please upgrade to the latest version to use Content Sync in your site.` + ) + warnOnceForNoSupport = true + } +} + +interface ICreateNodesForContentTypeArgs + extends Omit, + SourceNodesArgs { + contentTypeItem: ContentType + entries: Array< + IEntryWithAllLocalesAndWithoutLinkResolution + > + resolvable: Set + foreignReferenceMap: IForeignReferenceMap + defaultLocale: string + locales: Array + space: Space + useNameForId: boolean + pluginConfig: IProcessedPluginOptions + createNode: (node: Node) => void | Promise +} + +function makeQueuedCreateNode({ nodeCount, createNode }): { + create: (node: Node, cb?: done) => unknown + createNodesPromise: Promise +} { + if (nodeCount > 5000) { + let createdNodeCount = 0 + + const createNodesQueue: queue = fastq((node, cb) => { + function runCreateNode(): void { + const maybeNodePromise = createNode(node) + + // checking for `.then` is vastly more performant than using `instanceof Promise` + if (`then` in maybeNodePromise) { + maybeNodePromise.then(() => { + cb(null) + }) + } else { + cb(null) + } + } + + if (++createdNodeCount % 100 === 0) { + setImmediate(() => { + runCreateNode() + }) + } else { + runCreateNode() + } + }, 10) + + const queueFinished = new Promise(resolve => { + createNodesQueue.drain = (): void => { + resolve(null) + } + }) + + return { + create: (node, callback): void => createNodesQueue.push(node, callback), + createNodesPromise: queueFinished, + } + } else { + const nodePromises: Array> = [] + const queueFinished = (): Promise> => + Promise.all(nodePromises) + + return { + create: (node): number => nodePromises.push(createNode(node)), + createNodesPromise: queueFinished(), + } + } +} + +export const createNodesForContentType = ({ + contentTypeItem, + entries, + // eslint-disable-next-line @typescript-eslint/naming-convention + unstable_createNodeManifest, + createNode, + createNodeId, + getNode, + resolvable, + foreignReferenceMap, + defaultLocale, + locales, + space, + useNameForId, + pluginConfig, + reporter, +}: ICreateNodesForContentTypeArgs): Promise => { + const { create, createNodesPromise } = makeQueuedCreateNode({ + nodeCount: entries.length, + createNode, + }) + + const enableMarkdownDetection: boolean = pluginConfig.get( + `enableMarkdownDetection` + ) + const markdownFields: MarkdownFieldDefinition = new Map( + pluginConfig.get(`markdownFields`) + ) + const contentTypePrefix: string = pluginConfig.get(`contentTypePrefix`) + + // 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 + } else { + contentTypeItemId = contentTypeItem.sys.id + } + + // Create a node for the content type + const contentTypeNode: Node = { + id: createNodeId(contentTypeItemId), + parent: null, + children: [], + name: contentTypeItem.name, + displayField: contentTypeItem.displayField, + description: contentTypeItem.description, + internal: { + type: `ContentfulContentType`, + contentDigest: contentTypeItem.sys.updatedAt, + }, + // https://www.contentful.com/developers/docs/references/content-delivery-api/#/introduction/common-resource-attributes + // https://www.contentful.com/developers/docs/references/graphql/#/reference/schema-generation/sys-field + sys: { + type: contentTypeItem.sys.type, + id: contentTypeItem.sys.id, + spaceId: contentTypeItem.sys.space.sys.id, + environmentId: contentTypeItem.sys.environment.sys.id, + firstPublishedAt: contentTypeItem.sys.createdAt, + publishedAt: contentTypeItem.sys.updatedAt, + publishedVersion: contentTypeItem.sys.revision, + }, + } + + create(contentTypeNode) + + const fieldMap: Map = new Map() + contentTypeItem.fields.forEach(f => { + const fieldName = restrictedNodeFields.includes(f.id) + ? `${conflictFieldPrefix}${f.id}` + : f.id + fieldMap.set(fieldName, f) + }) + + locales.forEach(locale => { + const localesFallback = buildFallbackChain(locales) + const mId = makeMakeId({ + currentLocale: locale.code, + defaultLocale, + createNodeId, + }) + const getField = makeGetLocalizedField({ + locale, + localesFallback, + }) + + // Warn about any field conflicts + const conflictFields: Array = [] + contentTypeItem.fields.forEach(contentTypeItemField => { + const fieldName = contentTypeItemField.id + if (restrictedNodeFields.includes(fieldName)) { + console.log( + `Restricted field found for ContentType ${contentTypeItemId} and field ${fieldName}. Prefixing with ${conflictFieldPrefix}.` + ) + conflictFields.push(fieldName) + } + }) + + const childrenNodes: Array = [] + + const warnForUnresolvableLink = ( + entryItem: IEntryWithAllLocalesAndWithoutLinkResolution< + FieldsType, + string + >, + fieldName: string, + fieldValue: unknown + ): void => { + reporter.warn( + `Unable to find linked content in ${entryItem.sys.id}!\nContent Type: ${ + contentTypeItem.name + }, Field: ${fieldName}, Locale: ${locale.code}\n${JSON.stringify( + fieldValue, + null, + 2 + )}` + ) + } + + // First create nodes for each of the entries of that content type + const entryNodes: Array = entries.map( + entryItem => { + const entryNodeId = mId( + space.sys.id, + entryItem.sys.id, + entryItem.sys.type + ) + + const existingNode = getNode(entryNodeId) + if (existingNode?.updatedAt === entryItem.sys.updatedAt) { + // The Contentful model has `.sys.updatedAt` leading for an entry. If the updatedAt value + // of an entry did not change, then we can trust that none of its children were changed either. + return null + } + + // Get localized fields. + const entryItemFields = _.mapValues(entryItem.fields, (v, k) => { + const fieldProps = contentTypeItem.fields.find( + field => field.id === k + ) + + if (!fieldProps) { + throw new Error(`Unable to translate field ${k}`) + } + + const localizedField = fieldProps.localized + ? getField(v) + : v[defaultLocale] + + return localizedField + }) + + // Prefix any conflicting fields + // https://github.com/gatsbyjs/gatsby/pull/1084#pullrequestreview-41662888 + conflictFields.forEach(conflictField => { + entryItemFields[`${conflictFieldPrefix}${conflictField}`] = + entryItemFields[conflictField] + delete entryItemFields[conflictField] + }) + + // Add linkages to other nodes based on foreign references + Object.keys(entryItemFields).forEach(entryItemFieldKey => { + if (entryItemFields[entryItemFieldKey]) { + const entryItemFieldValue = entryItemFields[entryItemFieldKey] + const field = fieldMap.get(entryItemFieldKey) + if ( + field?.type === `Link` || + (field?.type === `Array` && field.items?.type === `Link`) + ) { + if (Array.isArray(entryItemFieldValue)) { + // Check if there are any values in entryItemFieldValue to prevent + // creating an empty node field in case when original key field value + // is empty due to links to missing entities + const resolvableEntryItemFieldValue = entryItemFieldValue + .filter(v => { + const isResolvable = resolvable.has( + createLinkRefId(v, space.sys.id) + ) + if (!isResolvable) { + warnForUnresolvableLink(entryItem, entryItemFieldKey, v) + } + return isResolvable + }) + .map(function (v) { + return mId( + space.sys.id, + v.sys.id, + v.sys.linkType || v.sys.type + ) + }) + + entryItemFields[entryItemFieldKey] = + resolvableEntryItemFieldValue + } else { + if ( + resolvable.has( + createLinkRefId(entryItemFieldValue, space.sys.id) + ) + ) { + entryItemFields[entryItemFieldKey] = mId( + space.sys.id, + entryItemFieldValue.sys.id, + entryItemFieldValue.sys.linkType || + entryItemFieldValue.sys.type + ) + } else { + entryItemFields[entryItemFieldKey] = null + warnForUnresolvableLink( + entryItem, + entryItemFieldKey, + entryItemFieldValue + ) + } + } + } + } + }) + + // Add reverse linkages if there are any for this node + const foreignReferences = + foreignReferenceMap[createRefId(entryItem, space.sys.id)] + const linkedFromFields = {} + if (foreignReferences) { + foreignReferences.forEach(foreignReference => { + const existingReference = linkedFromFields[foreignReference.name] + if (existingReference) { + // If the existing reference is a string, we're dealing with a + // many-to-one reference which has already been recorded, so we can + // skip it. However, if it is an array, add it: + if (Array.isArray(existingReference)) { + linkedFromFields[foreignReference.name].push( + mId( + foreignReference.spaceId, + foreignReference.id, + foreignReference.type + ) + ) + } + } else { + // If there is one foreign reference, there can be many. + // Best to be safe and put it in an array to start with. + linkedFromFields[foreignReference.name] = [ + mId( + foreignReference.spaceId, + foreignReference.id, + foreignReference.type + ), + ] + } + }) + } + + // Create actual entry node + let entryNode: IContentfulEntry = { + id: entryNodeId, + parent: contentTypeItemId, + children: [], + internal: { + type: makeTypeName(contentTypeItemId, contentTypePrefix), + // The content of an entry is guaranteed to be updated if and only if the .sys.updatedAt field changed + contentDigest: entryItem.sys.updatedAt as string, + trackInlineObjects: false, + }, + // https://www.contentful.com/developers/docs/references/content-delivery-api/#/introduction/common-resource-attributes + // https://www.contentful.com/developers/docs/references/graphql/#/reference/schema-generation/sys-field + sys: { + type: entryItem.sys.type, + id: entryItem.sys.id, + locale: locale.code, + spaceId: entryItem.sys.space.sys.id, + environmentId: entryItem.sys.environment.sys.id, + contentType: createNodeId(contentTypeItemId), + firstPublishedAt: entryItem.sys.createdAt, + publishedAt: entryItem.sys.updatedAt, + publishedVersion: entryItem.sys.revision, + }, + contentfulMetadata: { + tags: entryItem.metadata.tags.map(tag => + createNodeId(`ContentfulTag__${space.sys.id}__${tag.sys.id}`) + ), + }, + linkedFrom: linkedFromFields, + } + + contentfulCreateNodeManifest({ + pluginConfig, + entryItem, + entryNode, + space, + // eslint-disable-next-line @typescript-eslint/naming-convention + unstable_createNodeManifest, + }) + + // Replace text fields with text nodes so we can process their markdown + // into HTML. + const children: Array = [] + Object.keys(entryItemFields).forEach(entryItemFieldKey => { + const field = fieldMap.get(entryItemFieldKey) + if (field?.type === `Text`) { + const fieldType = detectMarkdownField( + field, + contentTypeItem, + enableMarkdownDetection, + markdownFields + ) + + if (fieldType == `Markdown`) { + const textNodeId = createNodeId( + `${entryNodeId}${entryItemFieldKey}${fieldType}Node` + ) + + // The Contentful model has `.sys.updatedAt` leading for an entry. If the updatedAt value + // of an entry did not change, then we can trust that none of its children were changed either. + // (That's why child nodes use the updatedAt of the parent node as their digest, too) + const existingNode = getNode(textNodeId) + if (existingNode?.updatedAt !== entryItem.sys.updatedAt) { + const textNode = prepareMarkdownNode( + textNodeId, + entryNode, + entryItemFieldKey, + entryItemFields[entryItemFieldKey] + ) + + childrenNodes.push(textNode) + children.push(textNodeId) + } + + entryItemFields[entryItemFieldKey] = textNodeId + } + } + }) + + entryNode = { + ...entryItemFields, + ...entryNode, + children, + } + return entryNode + } + ) + + entryNodes.forEach((entryNode, index) => { + // entry nodes may be undefined here if the node was previously already created + if (entryNode) { + create(entryNode, () => { + entryNodes[index] = undefined + }) + } + }) + childrenNodes.forEach((entryNode, index) => { + // entry nodes may be undefined here if the node was previously already created + if (entryNode) { + create(entryNode, () => { + childrenNodes[index] = undefined + }) + } + }) + }) + + return createNodesPromise +} + +export const createAssetNodes = async ({ + assetItem, + createNode, + createNodeId, + defaultLocale, + locales, + space, +}: { + assetItem + createNode + createNodeId + defaultLocale + locales: Array + space: Space +}): Promise> => { + const { create, createNodesPromise } = makeQueuedCreateNode({ + createNode, + nodeCount: locales.length, + }) + + const assetNodes: Array = [] + + locales.forEach(locale => { + const localesFallback = buildFallbackChain(locales) + const mId = makeMakeId({ + currentLocale: locale.code, + defaultLocale, + createNodeId, + }) + const getField = makeGetLocalizedField({ + locale, + localesFallback, + }) + + const fileRes = getField(assetItem.fields?.file) + + if (!fileRes) { + return + } + + const file = fileRes as unknown as AssetFile + + // Skip empty and unprocessed assets in Preview API + if (!file || !file.url || !file.contentType || !file.fileName) { + return + } + + const assetNode: IContentfulAsset = { + id: mId(space.sys.id, assetItem.sys.id, assetItem.sys.type), + parent: null, + children: [], + file, + internal: { + type: `ContentfulAsset`, + // The content of an asset is guaranteed to be updated if and only if the .sys.updatedAt field changed + contentDigest: assetItem.sys.updatedAt, + trackInlineObjects: false, + }, + // https://www.contentful.com/developers/docs/references/content-delivery-api/#/introduction/common-resource-attributes + // https://www.contentful.com/developers/docs/references/graphql/#/reference/schema-generation/sys-field + sys: { + type: assetItem.sys.type, + id: assetItem.sys.id, + locale: locale.code, + spaceId: assetItem.sys.space.sys.id, + environmentId: assetItem.sys.environment.sys.id, + firstPublishedAt: assetItem.sys.createdAt, + publishedAt: assetItem.sys.updatedAt, + publishedVersion: assetItem.sys.revision, + }, + contentType: `ContentfulAsset`, + placeholderUrl: `https:${file.url}?w=%width%&h=%height%`, + url: `https:${file.url}`, + // These fields are optional for edge cases in the Preview API and Contentfuls asset processing + width: file.details?.image?.width ?? undefined, + height: file.details?.image?.height ?? undefined, + size: file.details?.size ?? null, + contentfulMetadata: { + tags: assetItem.metadata.tags.map(tag => + createNodeId(`ContentfulTag__${space.sys.id}__${tag.sys.id}`) + ), + }, + title: assetItem.fields.title + ? getField(assetItem.fields.title) || `` + : ``, + description: assetItem.fields.description + ? getField(assetItem.fields.description) || `` + : ``, + // Satisfy the Gatsby ImageCDN feature + mimeType: file.contentType, + filename: file.fileName, + } + + assetNodes.push(assetNode) + create(assetNode) + }) + + await createNodesPromise + return assetNodes +} diff --git a/packages/gatsby-source-contentful/src/plugin-options.js b/packages/gatsby-source-contentful/src/plugin-options.ts similarity index 58% rename from packages/gatsby-source-contentful/src/plugin-options.js rename to packages/gatsby-source-contentful/src/plugin-options.ts index d315ab8324ca4..1e34dc781c1a8 100644 --- a/packages/gatsby-source-contentful/src/plugin-options.js +++ b/packages/gatsby-source-contentful/src/plugin-options.ts @@ -1,10 +1,11 @@ -// @ts-check import chalk from "chalk" import _ from "lodash" +import type { IPluginOptions, IProcessedPluginOptions } from "./types/plugin" + const DEFAULT_PAGE_LIMIT = 1000 -const defaultOptions = { +const defaultOptions: Omit = { host: `cdn.contentful.com`, environment: `master`, downloadLocal: false, @@ -12,30 +13,53 @@ const defaultOptions = { contentTypeFilter: () => true, pageLimit: DEFAULT_PAGE_LIMIT, useNameForId: true, - enableTags: false, - typePrefix: `Contentful`, + contentTypePrefix: `ContentfulContentType`, + enableMarkdownDetection: true, + markdownFields: [], + enforceRequiredFields: true, +} + +/** + * Mask majority of input to not leak any secrets + * @param {string} input + * @returns {string} masked text + */ +const maskText = (input: string): string => { + // show just 25% of string up to 4 characters + const hiddenCharactersLength = + input.length - Math.min(4, Math.floor(input.length * 0.25)) + + return `${`*`.repeat(hiddenCharactersLength)}${input.substring( + hiddenCharactersLength + )}` } -const createPluginConfig = pluginOptions => { +const createPluginConfig = ( + pluginOptions: Partial +): IProcessedPluginOptions => { const conf = { ...defaultOptions, ...pluginOptions } return { - get: key => conf[key], - getOriginalPluginOptions: () => pluginOptions, + get: (key): unknown => conf[key], + getOriginalPluginOptions: (): IPluginOptions => + pluginOptions as IPluginOptions, } } const maskedFields = [`accessToken`, `spaceId`] -const formatPluginOptionsForCLI = (pluginOptions, errors = {}) => { +const formatPluginOptionsForCLI = ( + pluginOptions: IPluginOptions, + errors = {} +): string => { const optionKeys = new Set( Object.keys(pluginOptions) .concat(Object.keys(defaultOptions)) .concat(Object.keys(errors)) ) - const getDisplayValue = key => { - const formatValue = value => { + const getDisplayValue = (key: string): string => { + const formatValue = (value: unknown): string => { if (_.isFunction(value)) { return `[Function]` } else if (maskedFields.includes(key) && typeof value === `string`) { @@ -53,42 +77,27 @@ const formatPluginOptionsForCLI = (pluginOptions, errors = {}) => { return chalk.dim(`undefined`) } - const lines = [] + const lines: Array = [] optionKeys.forEach(key => { if (key === `plugins`) { // skip plugins field automatically added by gatsby return } - lines.push( - `${key}${ - typeof pluginOptions[key] === `undefined` && - typeof defaultOptions[key] !== `undefined` - ? chalk.dim(` (default value)`) - : `` - }: ${getDisplayValue(key)}${ - typeof errors[key] !== `undefined` ? ` - ${chalk.red(errors[key])}` : `` - }` - ) + const defaultValue = + typeof pluginOptions[key] === `undefined` && + typeof defaultOptions[key] !== `undefined` + ? chalk.dim(` (default value)`) + : `` + const displayValue = getDisplayValue(key) + const errorValue = + typeof errors[key] !== `undefined` ? ` - ${chalk.red(errors[key])}` : `` + + lines.push(`${key}${defaultValue}: ${displayValue}${errorValue}`) }) return lines.join(`\n`) } -/** - * Mask majority of input to not leak any secrets - * @param {string} input - * @returns {string} masked text - */ -const maskText = input => { - // show just 25% of string up to 4 characters - const hiddenCharactersLength = - input.length - Math.min(4, Math.floor(input.length * 0.25)) - - return `${`*`.repeat(hiddenCharactersLength)}${input.substring( - hiddenCharactersLength - )}` -} - export { defaultOptions, formatPluginOptionsForCLI, diff --git a/packages/gatsby-source-contentful/src/report.js b/packages/gatsby-source-contentful/src/report.ts similarity index 73% rename from packages/gatsby-source-contentful/src/report.js rename to packages/gatsby-source-contentful/src/report.ts index afffe7faa83d3..dbd796ae78c3e 100644 --- a/packages/gatsby-source-contentful/src/report.js +++ b/packages/gatsby-source-contentful/src/report.ts @@ -1,4 +1,4 @@ -// @ts-check +import type { IErrorMapEntry } from "gatsby-cli/lib/structured-errors/error-map" export const CODES = { /* Fetch errors */ @@ -8,9 +8,16 @@ export const CODES = { FetchContentTypes: `111004`, GatsbyPluginMissing: `111005`, ContentTypesMissing: `111006`, + FetchTags: `111007`, + GenericContentfulError: `111008`, + GatsbyTooOld: `111009`, } -export const ERROR_MAP = { +interface IErrorMap { + [code: string]: Omit +} + +export const ERROR_MAP: IErrorMap = { [CODES.LocalesMissing]: { text: context => context.sourceMessage, level: `ERROR`, @@ -41,12 +48,17 @@ export const ERROR_MAP = { level: `ERROR`, category: `THIRD_PARTY`, }, - [CODES.FetchTags]: { + [CODES.GatsbyPluginMissing]: { + text: context => context.sourceMessage, + level: `ERROR`, + category: `USER`, + }, + [CODES.GenericContentfulError]: { text: context => context.sourceMessage, level: `ERROR`, category: `THIRD_PARTY`, }, - [CODES.GatsbyPluginMissing]: { + [CODES.GatsbyTooOld]: { text: context => context.sourceMessage, level: `ERROR`, category: `USER`, diff --git a/packages/gatsby-source-contentful/src/rich-text.js b/packages/gatsby-source-contentful/src/rich-text.js deleted file mode 100644 index 052b8ada4304e..0000000000000 --- a/packages/gatsby-source-contentful/src/rich-text.js +++ /dev/null @@ -1,46 +0,0 @@ -// @ts-check -import { documentToReactComponents } from "@contentful/rich-text-react-renderer" -import resolveResponse from "contentful-resolve-response" - -export function renderRichText({ raw, references }, options = {}) { - const richText = JSON.parse(raw || null) - - // If no references are given, there is no need to resolve them - if (!references || !references.length) { - return documentToReactComponents(richText, options) - } - - // Create dummy response so we can use official libraries for resolving the entries - const dummyResponse = { - items: [ - { - sys: { type: `Entry` }, - richText, - }, - ], - includes: { - Entry: references - .filter(({ __typename }) => __typename !== `ContentfulAsset`) - .map(reference => { - return { - ...reference, - sys: { type: `Entry`, id: reference.contentful_id }, - } - }), - Asset: references - .filter(({ __typename }) => __typename === `ContentfulAsset`) - .map(reference => { - return { - ...reference, - sys: { type: `Asset`, id: reference.contentful_id }, - } - }), - }, - } - - const resolved = resolveResponse(dummyResponse, { - removeUnresolved: true, - }) - - return documentToReactComponents(resolved[0].richText, options) -} diff --git a/packages/gatsby-source-contentful/src/rich-text.ts b/packages/gatsby-source-contentful/src/rich-text.ts new file mode 100644 index 0000000000000..bf8fc064c7d5c --- /dev/null +++ b/packages/gatsby-source-contentful/src/rich-text.ts @@ -0,0 +1,111 @@ +import { + documentToReactComponents, + Options, +} from "@contentful/rich-text-react-renderer" +import { Document } from "@contentful/rich-text-types" +import React from "react" +import type { IContentfulEntry, IContentfulAsset } from "./types/contentful" + +interface IContentfulRichTextLinksAssets { + block?: Array + hyperlink?: Array +} + +interface IContentfulRichTextLinksEntries { + inline?: Array + block?: Array + hyperlink?: Array +} + +interface IContentfulRichTextLinks { + assets?: IContentfulRichTextLinksAssets + entries?: IContentfulRichTextLinksEntries +} + +type AssetBlockMap = Map> +type AssetHyperlinkMap = Map> +type EntryBlockMap = Map> +type EntryInlineMap = Map> +type EntryHyperlinkMap = Map> + +interface IMakeOptions { + assetBlockMap: AssetBlockMap + assetHyperlinkMap: AssetHyperlinkMap + entryBlockMap: EntryBlockMap + entryInlineMap: EntryInlineMap + entryHyperlinkMap: EntryHyperlinkMap +} +export type MakeOptions = (referenceMaps: IMakeOptions) => Options + +export function renderRichText( + { + json, + links, + }: { + json: unknown + links?: unknown + }, + makeOptions?: MakeOptions | Options +): React.ReactNode { + const options = + typeof makeOptions === `function` + ? makeOptions(generateLinkMaps(links as IContentfulRichTextLinks)) + : makeOptions + + return documentToReactComponents(json as Document, options) +} + +/** + * Helper function to simplify Rich Text rendering. Based on: + * https://www.contentful.com/blog/2021/04/14/rendering-linked-assets-entries-in-contentful/ + */ +function generateLinkMaps(links: IContentfulRichTextLinks): { + assetBlockMap: AssetBlockMap + assetHyperlinkMap: AssetHyperlinkMap + entryBlockMap: EntryBlockMap + entryInlineMap: EntryInlineMap + entryHyperlinkMap: EntryHyperlinkMap +} { + const assetBlockMap = new Map() + if (links?.assets?.block) { + for (const asset of links.assets.block) { + assetBlockMap.set(asset.sys.id, asset) + } + } + + const assetHyperlinkMap = new Map() + if (links?.assets?.hyperlink) { + for (const asset of links.assets.hyperlink) { + assetHyperlinkMap.set(asset.sys.id, asset) + } + } + + const entryBlockMap = new Map() + if (links?.entries?.block) { + for (const entry of links.entries.block) { + entryBlockMap.set(entry.sys.id, entry) + } + } + + const entryInlineMap = new Map() + if (links?.entries?.inline) { + for (const entry of links.entries.inline) { + entryInlineMap.set(entry.sys.id, entry) + } + } + + const entryHyperlinkMap = new Map() + if (links?.entries?.hyperlink) { + for (const entry of links.entries.hyperlink) { + entryHyperlinkMap.set(entry.sys.id, entry) + } + } + + return { + assetBlockMap, + assetHyperlinkMap, + entryBlockMap, + entryInlineMap, + entryHyperlinkMap, + } +} diff --git a/packages/gatsby-source-contentful/src/schemes.js b/packages/gatsby-source-contentful/src/schemes.ts similarity index 99% rename from packages/gatsby-source-contentful/src/schemes.js rename to packages/gatsby-source-contentful/src/schemes.ts index abc4db100761a..a297e4d4c5094 100644 --- a/packages/gatsby-source-contentful/src/schemes.js +++ b/packages/gatsby-source-contentful/src/schemes.ts @@ -1,4 +1,3 @@ -// @ts-check import { GraphQLEnumType } from "gatsby/graphql" export const ImageFormatType = new GraphQLEnumType({ diff --git a/packages/gatsby-source-contentful/src/source-nodes.js b/packages/gatsby-source-contentful/src/source-nodes.js deleted file mode 100644 index baea1ca1aa215..0000000000000 --- a/packages/gatsby-source-contentful/src/source-nodes.js +++ /dev/null @@ -1,595 +0,0 @@ -// @ts-check -import isOnline from "is-online" -import _ from "lodash" -import { - addNodeToExistingNodesCache, - getExistingCachedNodes, - removeNodeFromExistingNodesCache, -} from "./backreferences" -import { untilNextEventLoopTick } from "./utils" - -import { downloadContentfulAssets } from "./download-contentful-assets" -import { fetchContent } from "./fetch" -import { - buildEntryList, - buildForeignReferenceMap, - buildResolvableSet, - createAssetNodes, - createNodesForContentType, - makeId, -} from "./normalize" -import { createPluginConfig } from "./plugin-options" -import { CODES } from "./report" -import { hasFeature } from "gatsby-plugin-utils/has-feature" - -const conflictFieldPrefix = `contentful` - -// restrictedNodeFields from here https://www.gatsbyjs.com/docs/node-interface/ -const restrictedNodeFields = [ - `children`, - `contentful_id`, - `fields`, - `id`, - `internal`, - `parent`, -] - -const CONTENT_DIGEST_COUNTER_SEPARATOR = `_COUNT_` - -/*** - * 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, - createNodeId, - store, - cache, - getCache, - reporter, - parentSpan, - }, - pluginOptions -) { - const { - createNode: originalCreateNode, - touchNode, - deleteNode: originalDeleteNode, - unstable_createNodeManifest, - enableStatefulSourceNodes, - } = actions - - if (hasFeature(`stateful-source-nodes`)) { - enableStatefulSourceNodes() - } - - const pluginConfig = createPluginConfig(pluginOptions) - - // wrap createNode so we can cache them in memory for faster lookups when finding backreferences - const createNode = node => { - addNodeToExistingNodesCache(node) - - return originalCreateNode(node) - } - - const deleteNode = node => { - removeNodeFromExistingNodesCache(node) - - return originalDeleteNode(node) - } - - // Array of all existing Contentful nodes - const { existingNodes, memoryNodeCountsBySysType } = - await getExistingCachedNodes({ - actions, - getNode, - pluginConfig, - }) - - // 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 ( - !(await isOnline()) && - 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 - } else 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.` - ) - } - - const sourceId = `${pluginConfig.get(`spaceId`)}-${pluginConfig.get( - `environment` - )}` - - const fetchActivity = reporter.activityTimer(`Contentful: Fetch data`, { - parentSpan, - }) - - fetchActivity.start() - - const CACHE_SYNC_TOKEN = `contentful-sync-token-${sourceId}` - const CACHE_CONTENT_TYPES = `contentful-content-types-${sourceId}` - const CACHE_FOREIGN_REFERENCE_MAP_STATE = `contentful-foreign-reference-map-state-${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 - ] - const isCachedBuild = !!syncToken - - // 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.`, - }, - }) - } - - // Update syncToken - const nextSyncToken = currentSyncData?.nextSyncToken - - 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`, - { - parentSpan, - } - ) - processingActivity.start() - - // Report existing, new and updated nodes - const nodeCounts = { - newEntry: 0, - newAsset: 0, - updatedEntry: 0, - updatedAsset: 0, - deletedEntry: currentSyncData?.deletedEntries?.length || 0, - deletedAsset: currentSyncData?.deletedAssets?.length || 0, - } - - currentSyncData?.entries?.forEach(entry => - entry.sys.revision === 1 ? nodeCounts.newEntry++ : nodeCounts.updatedEntry++ - ) - currentSyncData?.assets?.forEach(asset => - asset.sys.revision === 1 ? nodeCounts.newAsset++ : nodeCounts.updatedAsset++ - ) - - reporter.info(`Contentful: ${nodeCounts.newEntry} new entries`) - reporter.info(`Contentful: ${nodeCounts.updatedEntry} updated entries`) - reporter.info(`Contentful: ${nodeCounts.deletedEntry} deleted entries`) - reporter.info( - `Contentful: ${ - memoryNodeCountsBySysType.Entry / locales.length - } cached entries` - ) - reporter.info(`Contentful: ${nodeCounts.newAsset} new assets`) - reporter.info(`Contentful: ${nodeCounts.updatedAsset} updated assets`) - reporter.info( - `Contentful: ${ - memoryNodeCountsBySysType.Asset / locales.length - } cached assets` - ) - reporter.info(`Contentful: ${nodeCounts.deletedAsset} deleted assets`) - - reporter.verbose(`Building Contentful reference map`) - - const entryList = buildEntryList({ contentTypeItems, currentSyncData }) - const { assets } = currentSyncData - - // Create map of resolvable ids so we can check links against them while creating - // links. - const resolvable = buildResolvableSet({ - existingNodes, - entryList, - assets, - }) - - const previousForeignReferenceMapState = await cache.get( - CACHE_FOREIGN_REFERENCE_MAP_STATE - ) - // Build foreign reference map before starting to insert any nodes - const foreignReferenceMapState = buildForeignReferenceMap({ - contentTypeItems, - entryList, - resolvable, - defaultLocale, - space, - useNameForId: pluginConfig.get(`useNameForId`), - previousForeignReferenceMapState, - deletedEntries: currentSyncData?.deletedEntries, - }) - - await cache.set(CACHE_FOREIGN_REFERENCE_MAP_STATE, foreignReferenceMapState) - const foreignReferenceMap = foreignReferenceMapState.backLinks - - reporter.verbose(`Resolving Contentful references`) - - let newOrUpdatedEntries = new Set() - entryList.forEach(entries => { - entries.forEach(entry => { - newOrUpdatedEntries.add(`${entry.sys.id}___${entry.sys.type}`) - }) - }) - - const { deletedEntries, deletedAssets } = currentSyncData - const deletedEntryGatsbyReferenceIds = new Set() - - 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( - makeId({ - spaceId: space.sys.id, - id: node.sys.id, - type: normalizedType, - currentLocale: locale.code, - defaultLocale, - }) - ) - // Gather deleted node ids to remove them later on - deletedEntryGatsbyReferenceIds.add(nodeId) - return getNode(nodeId) - }) - .filter(node => node) - - localizedNodes.forEach(node => { - // touchNode first, to populate typeOwners & avoid erroring - touchNode(node) - deleteNode(node) - }) - } - - if (deletedEntries.length || deletedAssets.length) { - const deletionActivity = reporter.activityTimer( - `Contentful: Deleting nodes and assets`, - { - parentSpan, - } - ) - deletionActivity.start() - deletedEntries.forEach(deleteContentfulNode) - deletedAssets.forEach(deleteContentfulNode) - deletionActivity.end() - } - - // Update existing entry nodes that weren't updated but that need reverse links added or removed. - let existingNodesThatNeedReverseLinksUpdateInDatastore = new Set() - - if (isCachedBuild) { - existingNodes.forEach(n => { - if ( - !( - n.sys.type === `Entry` && - !newOrUpdatedEntries.has(`${n.id}___${n.sys.type}`) && - !deletedEntryGatsbyReferenceIds.has(n.id) - ) - ) { - return - } - - if ( - n.contentful_id && - foreignReferenceMap[`${n.contentful_id}___${n.sys.type}`] - ) { - foreignReferenceMap[`${n.contentful_id}___${n.sys.type}`].forEach( - foreignReference => { - const { name, id: contentfulId, type, spaceId } = foreignReference - - const nodeId = createNodeId( - makeId({ - spaceId, - id: contentfulId, - type, - currentLocale: n.node_locale, - defaultLocale, - }) - ) - - // Create new reference field when none exists - if (!n[name]) { - existingNodesThatNeedReverseLinksUpdateInDatastore.add(n) - n[name] = [nodeId] - return - } - - // Add non existing references to reference field - if (n[name] && !n[name].includes(nodeId)) { - existingNodesThatNeedReverseLinksUpdateInDatastore.add(n) - n[name].push(nodeId) - } - } - ) - } - - // Remove references to deleted nodes - if (n.contentful_id && deletedEntryGatsbyReferenceIds.size) { - Object.keys(n).forEach(name => { - // @todo Detect reference fields based on schema. Should be easier to achieve in the upcoming version. - if (!name.endsWith(`___NODE`)) { - return - } - if (Array.isArray(n[name])) { - n[name] = n[name].filter(referenceId => { - const shouldRemove = - deletedEntryGatsbyReferenceIds.has(referenceId) - if (shouldRemove) { - existingNodesThatNeedReverseLinksUpdateInDatastore.add(n) - } - return !shouldRemove - }) - } else { - const referenceId = n[name] - if (deletedEntryGatsbyReferenceIds.has(referenceId)) { - existingNodesThatNeedReverseLinksUpdateInDatastore.add(n) - n[name] = null - } - } - }) - } - }) - } - - // allow node to gc if it needs to - // @ts-ignore - newOrUpdatedEntries = undefined - await untilNextEventLoopTick() - - // We need to call `createNode` on nodes we modified reverse links on, - // otherwise changes won't actually persist - if (existingNodesThatNeedReverseLinksUpdateInDatastore.size) { - let existingNodesLoopCount = 0 - for (const node of existingNodesThatNeedReverseLinksUpdateInDatastore) { - function addChildrenToList(node, nodeList = [node]) { - for (const childNodeId of node?.children ?? []) { - const childNode = getNode(childNodeId) - if ( - childNode && - childNode.internal.owner === `gatsby-source-contentful` - ) { - nodeList.push(childNode) - addChildrenToList(childNode) - } - } - return nodeList - } - - const nodeAndDescendants = addChildrenToList(node) - for (const nodeToUpdateOriginal of nodeAndDescendants) { - // We should not mutate original node as Gatsby will still - // compare against what's in in-memory weak cache, so we - // clone original node to ensure reference identity is not possible - const nodeToUpdate = nodeToUpdateOriginal.__memcache - ? getNode(nodeToUpdateOriginal.id) - : nodeToUpdateOriginal - - let counter - const [initialContentDigest, counterStr] = - nodeToUpdate.internal.contentDigest.split( - CONTENT_DIGEST_COUNTER_SEPARATOR - ) - - if (counterStr) { - counter = parseInt(counterStr, 10) - } - - if (!counter || isNaN(counter)) { - counter = 1 - } else { - counter++ - } - - const newNode = { - ...nodeToUpdate, - internal: { - ...nodeToUpdate.internal, - // We need to remove properties from existing fields - // that are reserved and managed by Gatsby (`.internal.owner`, `.fields`). - // Gatsby automatically will set `.owner` it back - owner: undefined, - // We add or modify counter postfix to contentDigest - // to make sure Gatsby treat this as data update - contentDigest: `${initialContentDigest}${CONTENT_DIGEST_COUNTER_SEPARATOR}${counter}`, - }, - // `.fields` need to be created with `createNodeField` action, we can't just re-add them. - // Other plugins (or site itself) will have opportunity to re-generate them in `onCreateNode` lifecycle. - // Contentful content nodes are not using `createNodeField` so it's safe to delete them. - // (Asset nodes DO use `createNodeField` for `localFile` and if we were updating those, then - // we would also need to restore that field ourselves after re-creating a node) - fields: undefined, // plugin adds node field on asset nodes which don't have reverse links - } - - // memory cached nodes are mutated during back reference checks - // so we need to carry over the changes to the updated node - if (nodeToUpdateOriginal.__memcache) { - for (const key of Object.keys(nodeToUpdateOriginal)) { - if (!key.endsWith(`___NODE`)) { - continue - } - - newNode[key] = nodeToUpdateOriginal[key] - } - } - - createNode(newNode) - - if (existingNodesLoopCount++ % 2000 === 0) { - // dont block the event loop - await untilNextEventLoopTick() - } - } - } - } - - // allow node to gc if it needs to - // @ts-ignore - existingNodesThatNeedReverseLinksUpdateInDatastore = undefined - await untilNextEventLoopTick() - - const creationActivity = reporter.activityTimer(`Contentful: Create nodes`, { - 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 - await createNodesForContentType({ - contentTypeItem, - restrictedNodeFields, - conflictFieldPrefix, - entries: entryList[i], - createNode, - createNodeId, - getNode, - resolvable, - foreignReferenceMap, - defaultLocale, - locales, - space, - useNameForId: pluginConfig.get(`useNameForId`), - pluginConfig, - unstable_createNodeManifest, - }) - - // allow node to garbage collect these items if it needs to - contentTypeItems[i] = undefined - entryList[i] = undefined - await untilNextEventLoopTick() - } - - if (assets.length) { - reporter.info(`Creating ${assets.length} Contentful asset nodes`) - } - - const assetNodes = [] - for (let i = 0; i < assets.length; i++) { - // We wait for each asset to be process until handling the next one. - assetNodes.push( - ...(await createAssetNodes({ - assetItem: assets[i], - createNode, - createNodeId, - defaultLocale, - locales, - space, - pluginConfig, - })) - ) - - assets[i] = undefined - if (i % 1000 === 0) { - await untilNextEventLoopTick() - } - } - - await untilNextEventLoopTick() - - // 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() - - // Download asset files to local fs - if (pluginConfig.get(`downloadLocal`)) { - await downloadContentfulAssets({ - assetNodes, - actions, - createNodeId, - store, - cache, - getCache, - getNode, - reporter, - assetDownloadWorkers: pluginConfig.get(`assetDownloadWorkers`), - }) - } -} diff --git a/packages/gatsby-source-contentful/src/source-nodes.ts b/packages/gatsby-source-contentful/src/source-nodes.ts new file mode 100644 index 0000000000000..18192f332e325 --- /dev/null +++ b/packages/gatsby-source-contentful/src/source-nodes.ts @@ -0,0 +1,639 @@ +import type { GatsbyNode, Node } from "gatsby" +import { hasFeature } from "gatsby-plugin-utils/has-feature" +import isOnline from "is-online" + +import { downloadContentfulAssets } from "./download-contentful-assets" +import { fetchContent } from "./fetch" +import { + buildEntryList, + buildForeignReferenceMap, + buildResolvableSet, + createAssetNodes, + createNodesForContentType, + createRefId, + makeId, +} from "./normalize" +import { + addNodeToExistingNodesCache, + getExistingCachedNodes, + removeNodeFromExistingNodesCache, +} from "./backreferences" +import type { ContentType } from "contentful" +import { createPluginConfig } from "./plugin-options" +import { CODES } from "./report" +import { untilNextEventLoopTick } from "./utils" +import type { IPluginOptions } from "./types/plugin" +import type { IContentfulAsset } from "./types/contentful" + +const CONTENT_DIGEST_COUNTER_SEPARATOR = `_COUNT_` + +/*** + * 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 const sourceNodes: GatsbyNode["sourceNodes"] = + async function sourceNodes(args, pluginOptions) { + const { + actions, + getNode, + createNodeId, + store, + cache, + reporter, + parentSpan, + } = args + + const { + createNode: originalCreateNode, + deleteNode: originalDeleteNode, + enableStatefulSourceNodes, + } = actions + + if (hasFeature(`stateful-source-nodes`) && enableStatefulSourceNodes) { + enableStatefulSourceNodes() + } + + const pluginConfig = createPluginConfig(pluginOptions as IPluginOptions) + + // wrap createNode so we can cache them in memory for faster lookups when finding backreferences + const createNode = (node): void | Promise => { + addNodeToExistingNodesCache(node) + + return originalCreateNode(node) + } + + const deleteNode = (node): void | Promise => { + removeNodeFromExistingNodesCache(node) + + return originalDeleteNode(node) + } + + // Array of all existing Contentful nodes + const { existingNodes, memoryNodeCountsBySysType } = + await getExistingCachedNodes({ + actions, + getNode, + pluginConfig, + }) + + // 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 ( + !(await isOnline()) && + 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 + } else 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.` + ) + } + + const sourceId = `${pluginConfig.get(`spaceId`)}-${pluginConfig.get( + `environment` + )}` + + const fetchActivity = reporter.activityTimer(`Contentful: Fetch data`, { + parentSpan, + }) + + fetchActivity.start() + + const CACHE_SYNC_TOKEN = `contentful-sync-token-${sourceId}` + const CACHE_CONTENT_TYPES = `contentful-content-types-${sourceId}` + const CACHE_FOREIGN_REFERENCE_MAP_STATE = `contentful-foreign-reference-map-state-${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 + ] + const isCachedBuild = !!syncToken + + // Actual fetch of data from Contentful + const { + currentSyncData, + tagItems, + defaultLocale, + locales: allLocales = [], + space, + } = await fetchContent({ syncToken, pluginConfig, reporter, parentSpan }) + + const contentTypeItems = (await cache.get( + CACHE_CONTENT_TYPES + )) as Array + + 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.`, + }, + }) + } + + // Update syncToken + const nextSyncToken = currentSyncData?.nextSyncToken + + 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`, + { + parentSpan, + } + ) + processingActivity.start() + + // Report existing, new and updated nodes + const nodeCounts = { + newEntry: 0, + newAsset: 0, + updatedEntry: 0, + updatedAsset: 0, + deletedEntry: currentSyncData?.deletedEntries?.length || 0, + deletedAsset: currentSyncData?.deletedAssets?.length || 0, + } + + currentSyncData?.entries?.forEach(entry => + entry.sys.revision === 1 + ? nodeCounts.newEntry++ + : nodeCounts.updatedEntry++ + ) + currentSyncData?.assets?.forEach(asset => + asset.sys.revision === 1 + ? nodeCounts.newAsset++ + : nodeCounts.updatedAsset++ + ) + + reporter.info(`Contentful: ${nodeCounts.newEntry} new entries`) + reporter.info(`Contentful: ${nodeCounts.updatedEntry} updated entries`) + reporter.info(`Contentful: ${nodeCounts.deletedEntry} deleted entries`) + reporter.info( + `Contentful: ${ + memoryNodeCountsBySysType.Entry / locales.length + } cached entries` + ) + reporter.info(`Contentful: ${nodeCounts.newAsset} new assets`) + reporter.info(`Contentful: ${nodeCounts.updatedAsset} updated assets`) + reporter.info( + `Contentful: ${ + memoryNodeCountsBySysType.Asset / locales.length + } cached assets` + ) + reporter.info(`Contentful: ${nodeCounts.deletedAsset} deleted assets`) + + reporter.verbose(`Building Contentful reference map`) + + const entryList = buildEntryList({ + currentSyncData, + contentTypeItems, + }) + const { assets } = currentSyncData + + // Create map of resolvable ids so we can check links against them while creating + // links. + const resolvable = buildResolvableSet({ + existingNodes, + entryList, + assets, + spaceId: space.sys.id, + }) + + // Build foreign reference map before starting to insert any nodes + const previousForeignReferenceMapState = await cache.get( + CACHE_FOREIGN_REFERENCE_MAP_STATE + ) + const useNameForId: boolean = pluginConfig.get(`useNameForId`) + const contentTypePrefix: string = pluginConfig.get(`contentTypePrefix`) + + const foreignReferenceMapState = buildForeignReferenceMap({ + contentTypeItems, + entryList, + resolvable, + defaultLocale, + space, + useNameForId, + contentTypePrefix, + previousForeignReferenceMapState, + deletedEntries: currentSyncData?.deletedEntries, + createNodeId, + }) + await cache.set(CACHE_FOREIGN_REFERENCE_MAP_STATE, foreignReferenceMapState) + const foreignReferenceMap = foreignReferenceMapState.backLinks + + reporter.verbose(`Resolving Contentful references`) + + let newOrUpdatedEntries: Set | undefined = new Set() + entryList.forEach(entries => { + entries.forEach(entry => { + if (entry) { + newOrUpdatedEntries?.add(createRefId(entry, space.sys.id)) + } + }) + }) + + const { deletedEntries, deletedAssets } = currentSyncData + const deletedEntryGatsbyReferenceIds = new Set() + + function deleteContentfulNode(node): void { + const normalizedType = node.sys.type.startsWith(`Deleted`) + ? node.sys.type.substring(`Deleted`.length) + : node.sys.type + + const localizedNodes = locales.map(locale => { + const nodeId = createNodeId( + makeId({ + spaceId: space.sys.id, + id: node.sys.id, + type: normalizedType, + currentLocale: locale.code, + defaultLocale, + }) + ) + // Gather deleted node ids to remove them later on + deletedEntryGatsbyReferenceIds.add(nodeId) + return getNode(nodeId) + }) + + localizedNodes.forEach(node => { + if (node) { + deleteNode(node) + } + }) + } + + if (deletedEntries.length || deletedAssets.length) { + const deletionActivity = reporter.activityTimer( + `Contentful: Deleting nodes and assets`, + { + parentSpan, + } + ) + deletionActivity.start() + deletedEntries.forEach(deleteContentfulNode) + deletedAssets.forEach(deleteContentfulNode) + deletionActivity.end() + } + + // Create map of reference fields to properly delete stale references + const referenceFieldMap = new Map() + for (const contentTypeItem of contentTypeItems) { + if (!contentTypeItem) { + continue + } + const referenceFields = contentTypeItem.fields.filter(field => { + if (field.disabled || field.omitted) { + return false + } + + return ( + field.type === `Link` || + (field.type === `Array` && field.items?.type === `Link`) + ) + }) + if (referenceFields.length) { + referenceFieldMap.set( + contentTypeItem.name, + referenceFields.map(field => field.id) + ) + } + } + + const reverseReferenceFields = contentTypeItems.map(contentTypeItem => + useNameForId + ? contentTypeItem?.name.toLowerCase() + : contentTypeItem?.sys.id.toLowerCase() + ) + + // Update existing entry nodes that weren't updated but that need reverse links added or removed. + let existingNodesThatNeedReverseLinksUpdateInDatastore: Set = + new Set() + + const removeReferencesToDeletedNodes = (fieldValue, node): void => { + if (Array.isArray(fieldValue)) { + fieldValue = fieldValue.filter(referenceId => { + const shouldRemove = deletedEntryGatsbyReferenceIds.has(referenceId) + if (shouldRemove) { + existingNodesThatNeedReverseLinksUpdateInDatastore.add(node) + } + return !shouldRemove + }) + } else { + if (deletedEntryGatsbyReferenceIds.has(fieldValue)) { + existingNodesThatNeedReverseLinksUpdateInDatastore.add(node) + fieldValue = null + } + } + } + + if (isCachedBuild) { + existingNodes.forEach(n => { + if ( + !( + n.sys.type === `Entry` && + !newOrUpdatedEntries?.has(`${n.id}___${n.sys.type}`) && + !deletedEntryGatsbyReferenceIds.has(n.id) + ) + ) { + return + } + const refId = createRefId(n, space.sys.id) + if (n.sys.id && foreignReferenceMap[refId]) { + foreignReferenceMap[refId].forEach(foreignReference => { + const { name, id, type, spaceId } = foreignReference + + const nodeId = createNodeId( + makeId({ + spaceId, + id, + type, + currentLocale: n.sys.locale, + defaultLocale, + }) + ) + + // Create new reference field when none exists + if (!n.linkedFrom[name]) { + existingNodesThatNeedReverseLinksUpdateInDatastore.add(n) + n.linkedFrom[name] = [nodeId] + return + } + + // Add non existing references to reference field + const field = n.linkedFrom[name] + if (field && Array.isArray(field) && !field.includes(nodeId)) { + existingNodesThatNeedReverseLinksUpdateInDatastore.add(n) + field.push(nodeId) + } + }) + } + + // Remove references to deleted nodes + if ( + n.sys.id && + deletedEntryGatsbyReferenceIds.size && + referenceFieldMap.has(n.sys.contentType) + ) { + referenceFieldMap.get(n.sys.contentType).forEach(name => { + removeReferencesToDeletedNodes(n[name], n) + }) + + reverseReferenceFields.forEach(name => { + removeReferencesToDeletedNodes(n.linkedFrom[name], n) + }) + } + }) + } + + // allow node to gc if it needs to + // @ts-ignore avoid complex typing as this helps with GC + newOrUpdatedEntries = undefined + await untilNextEventLoopTick() + + // We need to call `createNode` on nodes we modified reverse links on, + // otherwise changes won't actually persist + if (existingNodesThatNeedReverseLinksUpdateInDatastore.size) { + let existingNodesLoopCount = 0 + for (const node of existingNodesThatNeedReverseLinksUpdateInDatastore) { + function addChildrenToList( + node: Node, + nodeList: Array = [node] + ): Array { + for (const childNodeId of node?.children ?? []) { + const childNode = getNode(childNodeId) + if ( + childNode && + childNode.internal.owner === `gatsby-source-contentful` + ) { + nodeList.push(childNode) + addChildrenToList(childNode) + } + } + return nodeList + } + + const nodeAndDescendants = addChildrenToList(node) + for (const nodeToUpdateOriginal of nodeAndDescendants) { + // We should not mutate original node as Gatsby will still + // compare against what's in in-memory weak cache, so we + // clone original node to ensure reference identity is not possible + const nodeToUpdate = nodeToUpdateOriginal.__memcache + ? getNode(nodeToUpdateOriginal.id) + : nodeToUpdateOriginal + + if (!nodeToUpdate) { + continue + } + + // We add or modify counter postfix to contentDigest + // to make sure Gatsby treat this as data update + let counter + const [initialContentDigest, counterStr] = + nodeToUpdate.internal.contentDigest.split( + CONTENT_DIGEST_COUNTER_SEPARATOR + ) + + if (counterStr) { + counter = parseInt(counterStr, 10) + } + + if (!counter || isNaN(counter)) { + counter = 1 + } else { + counter++ + } + + const newNode = { + ...nodeToUpdate, + internal: { + ...nodeToUpdate.internal, + // We need to remove properties from existing fields + // that are reserved and managed by Gatsby (`.internal.owner`, `.fields`). + // Gatsby automatically will set `.owner` it back + owner: undefined, + // We add or modify counter postfix to contentDigest + // to make sure Gatsby treat this as data update + contentDigest: `${initialContentDigest}${CONTENT_DIGEST_COUNTER_SEPARATOR}${counter}`, + }, + // `.fields` need to be created with `createNodeField` action, we can't just re-add them. + // Other plugins (or site itself) will have opportunity to re-generate them in `onCreateNode` lifecycle. + // Contentful content nodes are not using `createNodeField` so it's safe to delete them. + // (Asset nodes DO use `createNodeField` for `localFile` and if we were updating those, then + // we would also need to restore that field ourselves after re-creating a node) + fields: undefined, // plugin adds node field on asset nodes which don't have reverse links + } + + // memory cached nodes are mutated during back reference checks + // so we need to carry over the changes to the updated node + if (nodeToUpdateOriginal.__memcache) { + for (const key of Object.keys(nodeToUpdateOriginal)) { + if (!key.endsWith(`___NODE`)) { + continue + } + + newNode[key] = nodeToUpdateOriginal[key] + } + } + + await createNode(newNode) + + if (existingNodesLoopCount++ % 2000 === 0) { + // dont block the event loop + await untilNextEventLoopTick() + } + } + } + } + + // allow node to gc if it needs to + // @ts-ignore avoid complex typing + existingNodesThatNeedReverseLinksUpdateInDatastore = undefined + await untilNextEventLoopTick() + + const creationActivity = reporter.activityTimer( + `Contentful: Create nodes`, + { + parentSpan, + } + ) + creationActivity.start() + + // Create nodes for each entry of each content type + for (let i = 0; i < contentTypeItems.length; i++) { + const contentTypeItem = contentTypeItems[i] + + if (entryList[i].length) { + reporter.info( + `Creating ${entryList[i].length} Contentful ${ + 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 + await createNodesForContentType({ + contentTypeItem, + entries: entryList[i], + resolvable, + foreignReferenceMap, + defaultLocale, + locales, + space, + useNameForId, + pluginConfig, + ...actions, + ...args, + createNode, + }) + + // allow node to garbage collect these items if it needs to + // @ts-ignore avoid complex typing as this helps with GC + contentTypeItems[i] = undefined + // @ts-ignore avoid complex typing as this helps with GC + entryList[i] = undefined + await untilNextEventLoopTick() + } + + if (assets.length) { + reporter.info(`Creating ${assets.length} Contentful asset nodes`) + } + + const assetNodes: Array = [] + for (let i = 0; i < assets.length; i++) { + // We wait for each asset to be process until handling the next one. + assetNodes.push( + ...(await createAssetNodes({ + assetItem: assets[i], + createNode, + createNodeId, + defaultLocale, + locales, + space, + })) + ) + + // @ts-ignore avoid complex typing as this helps with GC + assets[i] = undefined + if (i % 1000 === 0) { + await untilNextEventLoopTick() + } + } + + await untilNextEventLoopTick() + + // 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, + parent: null, + children: [], + internal: { + type: `ContentfulTag`, + contentDigest: tag.sys.updatedAt, + }, + }) + } + } + + creationActivity.end() + + // Download asset files to local fs + if (pluginConfig.get(`downloadLocal`)) { + const assetDownloadWorkers = pluginConfig.get(`assetDownloadWorkers`) + await downloadContentfulAssets( + args, + actions, + assetNodes, + assetDownloadWorkers + ) + } + } diff --git a/packages/gatsby-source-contentful/src/types/contentful.ts b/packages/gatsby-source-contentful/src/types/contentful.ts new file mode 100644 index 0000000000000..df62864685797 --- /dev/null +++ b/packages/gatsby-source-contentful/src/types/contentful.ts @@ -0,0 +1,148 @@ +import { Node } from "gatsby" +import { IGatsbyImageData, ImageFormat } from "gatsby-plugin-image" +import { + FieldsType, + LocaleCode, + EntrySys, + Metadata, + EntryLink, + EntryFields, +} from "contentful" + +// Generic Types +export interface IContentfulContentType { + id: string + name: string + displayField: string + description: string +} +export interface IContentfulSys { + id: string + type: string + spaceId: string + environmentId: string + contentType?: string + firstPublishedAt: string + publishedAt: string + publishedVersion: number + locale: string +} + +export interface ILocalizedField { + [locale: string]: unknown +} + +interface IContentfulMetadata { + tags: Array +} + +interface IContentfulLinkSys { + id: string + linkType: string +} +export interface IContentfulLink { + sys: IContentfulLinkSys +} + +interface IContentfulEntity extends Node { + id: string + sys: IContentfulSys + contentfulMetadata: IContentfulMetadata +} + +export interface IContentfulEntry extends IContentfulEntity { + linkedFrom: { + [referenceId: string]: Array + } +} + +export interface IContentfulAsset extends IContentfulEntity { + gatsbyImageData?: IGatsbyImageData + localFile?: { + excludeByMimeTypes?: Array + maxFileSizeBytes?: number + requestConcurrency?: number + } + title: string + description: string + contentType: string + mimeType: string + filename: string + url: string + size?: number + width?: number + height?: number + fields?: { localFile?: string } +} + +// Image API +type Enumerate< + N extends number, + Acc extends Array = [] +> = Acc["length"] extends N + ? Acc[number] + : Enumerate + +type IntRange = Exclude< + Enumerate, + Enumerate +> + +export type contentfulImageApiBackgroundColor = `rgb:${string}` + +type contentfulCropFocus = + | "top" + | "top_left" + | "top_right" + | "bottom" + | "bottom_left" + | "bottom_right" + | "right" + | "left" + | "face" + | "faces" + | "center" + +export interface IContentfulImageAPITransformerOptions { + cornerRadius?: number | "max" + width?: number + height?: number + toFormat?: ImageFormat | "auto" | "jpg" | "png" | "webp" | "gif" | "avif" + jpegProgressive?: number + quality?: IntRange<0, 100> + resizingBehavior?: "pad" | "fill" | "scale" | "crop" | "thumb" + cropFocus?: contentfulCropFocus + backgroundColor?: string + placeholder?: "dominantColor" | "blurred" | "tracedSVG" + blurredOptions?: { width?: number; toFormat: ImageFormat } + tracedSVGOptions?: { [key: string]: unknown } +} + +export interface IContentfulImageAPIUrlBuilderOptions { + width?: number + height?: number + toFormat?: ImageFormat | "auto" | "jpg" | "png" | "webp" | "gif" | "avif" + resizingBehavior?: "pad" | "fill" | "scale" | "crop" | "thumb" + background?: contentfulImageApiBackgroundColor + quality?: IntRange<0, 100> + jpegProgressive?: number + cropFocus?: contentfulCropFocus + cornerRadius?: number | "max" +} + +export interface IEntryWithAllLocalesAndWithoutLinkResolution< + Fields extends FieldsType, + Locales extends LocaleCode +> { + sys: EntrySys + metadata: Metadata + fields: { + [FieldName in keyof Fields]: { + [LocaleName in Locales]?: Fields[FieldName] extends EntryFields.Link + ? EntryLink + : Fields[FieldName] extends Array> + ? Array + : Fields[FieldName] + } + } +} diff --git a/packages/gatsby-source-contentful/src/types/plugin.ts b/packages/gatsby-source-contentful/src/types/plugin.ts new file mode 100644 index 0000000000000..8625fab893400 --- /dev/null +++ b/packages/gatsby-source-contentful/src/types/plugin.ts @@ -0,0 +1,21 @@ +import { PluginOptions } from "gatsby" + +export interface IPluginOptions extends Partial { + accessToken: string + spaceId: string + host?: string + environment?: string + downloadLocal?: boolean + localeFilter?: () => boolean + contentTypeFilter?: () => boolean + pageLimit?: number + useNameForId?: boolean + contentTypePrefix: string +} + +export interface IProcessedPluginOptions { + get: (key: keyof IPluginOptions) => any + getOriginalPluginOptions: () => IPluginOptions +} + +export type MarkdownFieldDefinition = Map diff --git a/packages/gatsby-source-contentful/src/utils.js b/packages/gatsby-source-contentful/src/utils.js deleted file mode 100644 index ff33ce49aed33..0000000000000 --- a/packages/gatsby-source-contentful/src/utils.js +++ /dev/null @@ -1,9 +0,0 @@ -// When iterating on tons of objects, we don't want to block the event loop -// this helper function returns a promise that resolves on the next tick so that the event loop can continue before we continue running blocking code -export function untilNextEventLoopTick() { - return new Promise(res => { - setImmediate(() => { - res(null) - }) - }) -} diff --git a/packages/gatsby-source-contentful/src/utils.ts b/packages/gatsby-source-contentful/src/utils.ts new file mode 100644 index 0000000000000..10a9df7ae1903 --- /dev/null +++ b/packages/gatsby-source-contentful/src/utils.ts @@ -0,0 +1,56 @@ +import { ContentType, ContentTypeField } from "contentful" +import { MarkdownFieldDefinition } from "./types/plugin" +import { makeTypeName } from "./normalize" + +// When iterating on tons of objects, we don't want to block the event loop +// this helper function returns a promise that resolves on the next tick so that the event loop can continue before we continue running blocking code +export function untilNextEventLoopTick(): Promise { + return new Promise(res => { + setImmediate(() => { + res(null) + }) + }) +} + +export function detectMarkdownField( + field: ContentTypeField, + contentTypeItem: ContentType, + enableMarkdownDetection: boolean, + markdownFields: MarkdownFieldDefinition +): string { + let typeName = field.type as string + + if (typeName == `Text` && enableMarkdownDetection) { + typeName = `Markdown` + } + + // Detect markdown based on given field ids + const markdownFieldDefinitions = markdownFields.get(contentTypeItem.sys.id) + if (markdownFieldDefinitions && markdownFieldDefinitions.includes(field.id)) { + typeName = `Markdown` + } + + return typeName +} + +// Establish identifier for content type based on plugin options +// Use `name` if specified, otherwise, use internal id (usually a natural-language constant, +// but sometimes a base62 uuid generated by Contentful, hence the option) +export function makeContentTypeIdMap( + contentTypeItems: Array, + contentTypePrefix: string, + useNameForId: boolean +): Map { + const contentTypeIdMap: Map = new Map() + + contentTypeItems.forEach(contentType => { + let contentTypeItemId + if (useNameForId) { + contentTypeItemId = makeTypeName(contentType.name, contentTypePrefix) + } else { + contentTypeItemId = makeTypeName(contentType.sys.id, contentTypePrefix) + } + contentTypeIdMap.set(contentType.sys.id, contentTypeItemId) + }) + return contentTypeIdMap +} diff --git a/packages/gatsby-source-contentful/tsconfig.json b/packages/gatsby-source-contentful/tsconfig.json new file mode 100644 index 0000000000000..185104ef5de84 --- /dev/null +++ b/packages/gatsby-source-contentful/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src"], + "exclude": [ + "node_modules", + "src/__tests__", + "src/__mocks__", + "dist", + "./node.d.ts" + ] +} diff --git a/packages/gatsby-transformer-sqip/src/extend-node-type.js b/packages/gatsby-transformer-sqip/src/extend-node-type.js index bbef03954199e..4aba5b1090d2b 100644 --- a/packages/gatsby-transformer-sqip/src/extend-node-type.js +++ b/packages/gatsby-transformer-sqip/src/extend-node-type.js @@ -135,6 +135,8 @@ async function sqipSharp({ cache, getNodeAndSavePathDependency }) { async function sqipContentful({ cache }) { const { schemes: { ImageResizingBehavior, ImageCropFocusType }, + createUrl, + mimeTypeExtensions, } = require(`gatsby-source-contentful`) const cacheDir = path.resolve(`${cache.directory}/intermediate-files/`) @@ -183,16 +185,9 @@ async function sqipContentful({ cache }) { }, }, async resolve(asset, fieldArgs) { - const { - createUrl, - mimeTypeExtensions, - } = require(`gatsby-source-contentful/image-helpers`) - - const { - file: { contentType, url: imgUrl, fileName }, - } = asset + const { mimeType, url: imgUrl, filename } = asset - if (!contentType.includes(`image/`)) { + if (!mimeType.includes(`image/`)) { return null } @@ -220,9 +215,9 @@ async function sqipContentful({ cache }) { background, } - const extension = mimeTypeExtensions.get(contentType) + const extension = mimeTypeExtensions.get(mimeType) const url = createUrl(imgUrl, options) - const name = path.basename(fileName, extension) + const name = path.basename(filename, extension) const absolutePath = await fetchRemoteFile({ url, diff --git a/packages/gatsby/index.d.ts b/packages/gatsby/index.d.ts index 6ef0ef1a31c02..7e33201c4c329 100644 --- a/packages/gatsby/index.d.ts +++ b/packages/gatsby/index.d.ts @@ -23,6 +23,7 @@ export type AvailableFeatures = | "content-file-path" | "stateful-source-nodes" | "adapters" + | "track-inline-object-opt-out" export { Link, @@ -34,6 +35,8 @@ export { export * from "gatsby-script" +export { IGatsbyResolverContext } from "./dist/schema/type-definitions" + export { AdapterInit, IAdapter, @@ -1777,6 +1780,7 @@ export interface NodeInput { contentDigest: string description?: string contentFilePath?: string + trackInlineObjects?: boolean } [key: string]: unknown } @@ -1785,7 +1789,7 @@ export interface Node extends NodeInput { parent: string | null children: string[] internal: NodeInput["internal"] & { - owner: string + owner?: string } [key: string]: unknown } diff --git a/packages/gatsby/scripts/__tests__/api.js b/packages/gatsby/scripts/__tests__/api.js index dc1c6ce7ca41e..665dd8db02cb0 100644 --- a/packages/gatsby/scripts/__tests__/api.js +++ b/packages/gatsby/scripts/__tests__/api.js @@ -37,6 +37,7 @@ it("generates the expected api output", done => { "slices", "stateful-source-nodes", "adapters", + "track-inline-object-opt-out", ], "node": Object { "createPages": Object {}, diff --git a/packages/gatsby/scripts/output-api-file.js b/packages/gatsby/scripts/output-api-file.js index 8f3cf1b5f7c32..826a04429c123 100644 --- a/packages/gatsby/scripts/output-api-file.js +++ b/packages/gatsby/scripts/output-api-file.js @@ -41,7 +41,7 @@ async function outputFile() { }, {}) /** @type {Array} */ - output.features = ["image-cdn", "graphql-typegen", "content-file-path", "slices", "stateful-source-nodes", "adapters"]; + output.features = ["image-cdn", "graphql-typegen", "content-file-path", "slices", "stateful-source-nodes", "adapters", "track-inline-object-opt-out"]; return fs.writeFile( path.resolve(OUTPUT_FILE_NAME), diff --git a/packages/gatsby/src/joi-schemas/joi.ts b/packages/gatsby/src/joi-schemas/joi.ts index cb5402fd55295..352e31ee49899 100644 --- a/packages/gatsby/src/joi-schemas/joi.ts +++ b/packages/gatsby/src/joi-schemas/joi.ts @@ -177,6 +177,7 @@ export const nodeSchema: Joi.ObjectSchema = Joi.object() ignoreType: Joi.boolean(), counter: Joi.number(), contentFilePath: Joi.string(), + trackInlineObjects: Joi.boolean(), }) .unknown(false), // Don't allow non-standard fields }) diff --git a/packages/gatsby/src/schema/__tests__/__snapshots__/build-schema.js.snap b/packages/gatsby/src/schema/__tests__/__snapshots__/build-schema.js.snap index afeb90d39d48f..1cbf2b5b40dcd 100644 --- a/packages/gatsby/src/schema/__tests__/__snapshots__/build-schema.js.snap +++ b/packages/gatsby/src/schema/__tests__/__snapshots__/build-schema.js.snap @@ -2016,6 +2016,7 @@ type Internal { owner: String! type: String! contentFilePath: String + trackInlineObjects: Boolean } \\"\\"\\" @@ -2356,6 +2357,7 @@ input InternalFilterInput { owner: StringQueryOperatorInput type: StringQueryOperatorInput contentFilePath: StringQueryOperatorInput + trackInlineObjects: BooleanQueryOperatorInput } input BooleanQueryOperatorInput { @@ -2452,6 +2454,7 @@ input InternalFieldSelector { owner: FieldSelectorEnum type: FieldSelectorEnum contentFilePath: FieldSelectorEnum + trackInlineObjects: FieldSelectorEnum } type FileGroupConnection { @@ -2566,6 +2569,7 @@ input InternalSortInput { owner: SortOrderEnum type: SortOrderEnum contentFilePath: SortOrderEnum + trackInlineObjects: SortOrderEnum } type DirectoryConnection { diff --git a/packages/gatsby/src/schema/__tests__/__snapshots__/rebuild-schema.js.snap b/packages/gatsby/src/schema/__tests__/__snapshots__/rebuild-schema.js.snap index e8ca70911a884..a59ab4f0debc2 100644 --- a/packages/gatsby/src/schema/__tests__/__snapshots__/rebuild-schema.js.snap +++ b/packages/gatsby/src/schema/__tests__/__snapshots__/rebuild-schema.js.snap @@ -215,6 +215,7 @@ type Internal { owner: String! type: String! contentFilePath: String + trackInlineObjects: Boolean } \\"\\"\\" @@ -555,6 +556,7 @@ input InternalFilterInput { owner: StringQueryOperatorInput type: StringQueryOperatorInput contentFilePath: StringQueryOperatorInput + trackInlineObjects: BooleanQueryOperatorInput } input BooleanQueryOperatorInput { @@ -651,6 +653,7 @@ input InternalFieldSelector { owner: FieldSelectorEnum type: FieldSelectorEnum contentFilePath: FieldSelectorEnum + trackInlineObjects: FieldSelectorEnum } type FileGroupConnection { @@ -765,6 +768,7 @@ input InternalSortInput { owner: SortOrderEnum type: SortOrderEnum contentFilePath: SortOrderEnum + trackInlineObjects: SortOrderEnum } type DirectoryConnection { diff --git a/packages/gatsby/src/schema/node-model.js b/packages/gatsby/src/schema/node-model.js index b7e5824745a03..649137161b74d 100644 --- a/packages/gatsby/src/schema/node-model.js +++ b/packages/gatsby/src/schema/node-model.js @@ -498,6 +498,9 @@ class LocalNodeModel { * @param {Node} node Root Node */ trackInlineObjectsInRootNode(node) { + if (node.internal.trackInlineObjects === false) { + return + } if (!this._trackedRootNodes.has(node)) { addRootNodeToInlineObject( this._rootNodeMap, diff --git a/packages/gatsby/src/schema/types/node-interface.ts b/packages/gatsby/src/schema/types/node-interface.ts index 5769947f3381e..4f5f6019a2b99 100644 --- a/packages/gatsby/src/schema/types/node-interface.ts +++ b/packages/gatsby/src/schema/types/node-interface.ts @@ -27,6 +27,7 @@ const getOrCreateNodeInterface = ( owner: `String!`, type: `String!`, contentFilePath: `String`, + trackInlineObjects: `Boolean`, }) // TODO: Can be removed with graphql-compose 5.11 tc.getInputTypeComposer() diff --git a/yarn.lock b/yarn.lock index a2341802939ed..2f15e126f4038 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1414,6 +1414,13 @@ resolved "https://registry.yarnpkg.com/@builder.io/partytown/-/partytown-0.7.5.tgz#f501e3db37a5ac659f21ba0c2e61b278e58b64b9" integrity sha512-Zbr2Eo0AQ4yzmQr/36/h+6LKjmdVBB3Q5cGzO6rtlIKB/IOpbQVUZW+XAnhpJmJr9sIF97OZjgbhG9k7Sjn4yw== +"@contentful/rich-text-links@^16.1.1": + version "16.1.1" + resolved "https://registry.yarnpkg.com/@contentful/rich-text-links/-/rich-text-links-16.1.1.tgz#d4fe5d77d0fc7c7b8a1185613910c11b6a1400ff" + integrity sha512-WSxtDeq1maWGSGJ5f8psRdERmXzvIhJn7jQB+H1QIfP7C50dh/67CV6ABSB2GdfcXQxVX21f4KptS+5IpBetUA== + dependencies: + "@contentful/rich-text-types" "^16.2.0" + "@contentful/rich-text-react-renderer@^15.17.0": version "15.17.0" resolved "https://registry.yarnpkg.com/@contentful/rich-text-react-renderer/-/rich-text-react-renderer-15.17.0.tgz#88b6c707eec5d0d45491be830f0cfb1a0fae9472" @@ -1421,11 +1428,6 @@ dependencies: "@contentful/rich-text-types" "^16.2.0" -"@contentful/rich-text-types@^15.15.1": - version "15.15.1" - resolved "https://registry.yarnpkg.com/@contentful/rich-text-types/-/rich-text-types-15.15.1.tgz#96835cf0d0eba9e54f92ee43a4a1ce2a74014b53" - integrity sha512-oheW0vkxWDuKBIIXDeJfZaRYo+NzKbC4gETMhH+MGJd4nfL9cqrOvtRxZBgnhICN4vDpH4my/zUIZGKcFqGSjQ== - "@contentful/rich-text-types@^16.0.2", "@contentful/rich-text-types@^16.2.0": version "16.2.0" resolved "https://registry.yarnpkg.com/@contentful/rich-text-types/-/rich-text-types-16.2.0.tgz#5a98cb119340f4da46555216913e96d7ce58df70" @@ -2037,7 +2039,7 @@ resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.1.0.tgz#6c9eafc78c1529248f8f4d92b0799a712b6052c6" integrity sha512-i9YbZPN3QgfighY/1X1Pu118VUz2Fmmhd6b2n0/O8YVgGGfw0FbUYoA97k7FkpGJ+pLCFEDLUmAPPV4D1kpeFw== -"@hapi/joi@^15.0.0", "@hapi/joi@^15.1.1": +"@hapi/joi@^15.0.0": version "15.1.1" resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-15.1.1.tgz#c675b8a71296f02833f8d6d243b34c57b8ce19d7" integrity sha512-entf8ZMOK8sc+8YfeOlM8pCfg3b5+WZIKBfUaaJT8UsjAAPjartzxIYm3TIbjvA4u+u++KbcXD38k682nVHDAQ== @@ -6592,13 +6594,12 @@ axios-rate-limit@^1.3.0: resolved "https://registry.yarnpkg.com/axios-rate-limit/-/axios-rate-limit-1.3.0.tgz#03241d24c231c47432dab6e8234cfde819253c2e" integrity sha512-cKR5wTbU/CeeyF1xVl5hl6FlYsmzDVqxlN4rGtfO5x7J83UxKDckudsW0yW21/ZJRcO0Qrfm3fUFbhEbWTLayw== -axios@^0.27.0: - version "0.27.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" - integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== +axios@^0.26.1: + version "0.26.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9" + integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA== dependencies: - follow-redirects "^1.14.9" - form-data "^4.0.0" + follow-redirects "^1.14.8" axios@^1.6.2, axios@^1.6.4: version "1.6.4" @@ -8418,17 +8419,17 @@ content-type@~1.0.4, content-type@~1.0.5: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== -contentful-resolve-response@^1.3.12: - version "1.3.12" - resolved "https://registry.yarnpkg.com/contentful-resolve-response/-/contentful-resolve-response-1.3.12.tgz#8d1c37e4c084d87eba825a1f376e3562cb834385" - integrity sha512-fe2dsACyV3jzRjHcoeAa4bBt06YwkdsY+kdQwFCcdETyBQIxifuyGamIF4NmKcDqvZ9Yhw7ujjPCadzHb9jmKw== +contentful-resolve-response@^1.3.6: + version "1.7.0" + resolved "https://registry.yarnpkg.com/contentful-resolve-response/-/contentful-resolve-response-1.7.0.tgz#0431d222a474e60a397b050a883bb3df61d9a378" + integrity sha512-NIaQkXAdNWf/1wp13aqtrmHWHy71oCIIbynojGs9ONDtA7hBXC2yCEf2IjzfgOkrkEfOMvwDQHizD0WmL6FR8w== dependencies: fast-copy "^2.1.7" -contentful-sdk-core@^7.0.5: - version "7.0.6" - resolved "https://registry.yarnpkg.com/contentful-sdk-core/-/contentful-sdk-core-7.0.6.tgz#bd6be54f166b5cddf7a5132f8087b22d8bb8d477" - integrity sha512-xG4+a4p7VGCuxxUWh8t3O3V6gEcPP/aSE/KkvPRMYkm8PbxWYTAYG3c5pn5lmtj1QKcsY7yjiLWRXtP4qzem3Q== +contentful-sdk-core@^7.0.2: + version "7.1.0" + resolved "https://registry.yarnpkg.com/contentful-sdk-core/-/contentful-sdk-core-7.1.0.tgz#74d3ef8b167c5ac390fbb5db2d6e433731e05115" + integrity sha512-RzTPnRsbCdVAhyka3wa9sDsAu9YsxoerNgaMqd63Ljb7qpG2zkdHcP7NTfyIbuHDJNJdAQdifyafxfEEwP+q/w== dependencies: fast-copy "^2.1.7" lodash.isplainobject "^4.0.6" @@ -8436,17 +8437,17 @@ contentful-sdk-core@^7.0.5: p-throttle "^4.1.1" qs "^6.9.4" -contentful@^9.3.5: - version "9.3.5" - resolved "https://registry.yarnpkg.com/contentful/-/contentful-9.3.5.tgz#4644461f7b34a99ed64a10511743e7c278a71a00" - integrity sha512-QVXHwD9nxREBpcemC6Po2LUYStmBBHPyVbN3SKzkR+WmIZhflF6x+TDmmz2jcCg/RSN+INDZbhe8FQ1S/zTE8w== +contentful@^10.2.3: + version "10.2.3" + resolved "https://registry.yarnpkg.com/contentful/-/contentful-10.2.3.tgz#b29ef8a71f75eb19ff66fa98e74dcf2400b86daf" + integrity sha512-1GqJYwuQBl744ssyadZjeHrQAKBlOG3dxhcYBxfPi4iMzfGjMh+WiN0WeH0yIunKfOn5aIxEeYxZ6yexbP08zA== dependencies: "@contentful/rich-text-types" "^16.0.2" - axios "^0.27.0" - contentful-resolve-response "^1.3.12" - contentful-sdk-core "^7.0.5" - fast-copy "^2.1.7" + axios "^0.26.1" + contentful-resolve-response "^1.3.6" + contentful-sdk-core "^7.0.2" json-stringify-safe "^5.0.1" + type-fest "^3.1.0" continuable-cache@^0.3.1: version "0.3.1" @@ -11814,10 +11815,10 @@ flush-write-stream@^1.0.0, flush-write-stream@^1.0.2: inherits "^2.0.1" readable-stream "^2.0.4" -follow-redirects@^1.14.9: - version "1.14.9" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" - integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== +follow-redirects@^1.14.8: + version "1.15.5" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020" + integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== follow-redirects@^1.15.4: version "1.15.4" @@ -12085,7 +12086,7 @@ fs-extra@^10.0.0, fs-extra@^10.1.0: jsonfile "^6.0.1" universalify "^2.0.0" -fs-extra@^11.2.0: +fs-extra@^11.1.1, fs-extra@^11.2.0: version "11.2.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== @@ -24309,6 +24310,11 @@ type-fest@^2.11.2, type-fest@^2.19.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== +type-fest@^3.1.0: + version "3.11.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.11.0.tgz#e78ea6b50d6a6b1e4609035fb9ea8f1e3c328194" + integrity sha512-JaPw5U9ixP0XcpUbQoVSbxSDcK/K4nww20C3kjm9yE6cDRRhptU28AH60VWf9ltXmCrIfIbtt9J+2OUk2Uqiaw== + type-is@^1.6.4, type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -24386,7 +24392,7 @@ typescript@^4.1.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== -typescript@^5.1.6: +typescript@^5.0.4, typescript@^5.1.6: version "5.1.6" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==