UNPKG

gatsby-source-contentful

Version:

Gatsby source plugin for building websites using the Contentful CMS as a data source

476 lines (451 loc) 18.9 kB
"use strict"; exports.__esModule = true; exports.sourceNodes = sourceNodes; var _backreferences = require("./backreferences"); var _utils = require("./utils"); var _downloadContentfulAssets = require("./download-contentful-assets"); var _fetch = require("./fetch"); var _normalize = require("./normalize"); var _pluginOptions = require("./plugin-options"); var _report = require("./report"); var _hasFeature = require("gatsby-plugin-utils/has-feature"); // @ts-check 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_`; async function isOnline() { return (await import(`is-online`)).default(); } /*** * 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. */ async function sourceNodes({ actions, getNode, createNodeId, store, cache, getCache, reporter, parentSpan }, pluginOptions) { var _store$getState$statu, _store$getState$statu2, _currentSyncData$dele, _currentSyncData$dele2, _currentSyncData$entr, _currentSyncData$asse; const { createNode: originalCreateNode, touchNode, deleteNode: originalDeleteNode, unstable_createNodeManifest, enableStatefulSourceNodes } = actions; if ((0, _hasFeature.hasFeature)(`stateful-source-nodes`)) { enableStatefulSourceNodes(); } const pluginConfig = (0, _pluginOptions.createPluginConfig)(pluginOptions); // wrap createNode so we can cache them in memory for faster lookups when finding backreferences const createNode = node => { (0, _backreferences.addNodeToExistingNodesCache)(node); return originalCreateNode(node); }; const deleteNode = node => { (0, _backreferences.removeNodeFromExistingNodesCache)(node); return originalDeleteNode(node); }; // Array of all existing Contentful nodes const { existingNodes, memoryNodeCountsBySysType } = await (0, _backreferences.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$statu = store.getState().status.plugins) === null || _store$getState$statu === void 0 ? void 0 : (_store$getState$statu2 = _store$getState$statu[`gatsby-source-contentful`]) === null || _store$getState$statu2 === void 0 ? void 0 : _store$getState$statu2[CACHE_SYNC_TOKEN]; const isCachedBuild = !!syncToken; // Actual fetch of data from Contentful const { currentSyncData, tagItems, defaultLocale, locales: allLocales = [], space } = await (0, _fetch.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: _report.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 === null || currentSyncData === void 0 ? void 0 : 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 === null || currentSyncData === void 0 ? void 0 : (_currentSyncData$dele = currentSyncData.deletedEntries) === null || _currentSyncData$dele === void 0 ? void 0 : _currentSyncData$dele.length) || 0, deletedAsset: (currentSyncData === null || currentSyncData === void 0 ? void 0 : (_currentSyncData$dele2 = currentSyncData.deletedAssets) === null || _currentSyncData$dele2 === void 0 ? void 0 : _currentSyncData$dele2.length) || 0 }; currentSyncData === null || currentSyncData === void 0 ? void 0 : (_currentSyncData$entr = currentSyncData.entries) === null || _currentSyncData$entr === void 0 ? void 0 : _currentSyncData$entr.forEach(entry => entry.sys.revision === 1 ? nodeCounts.newEntry++ : nodeCounts.updatedEntry++); currentSyncData === null || currentSyncData === void 0 ? void 0 : (_currentSyncData$asse = currentSyncData.assets) === null || _currentSyncData$asse === void 0 ? void 0 : _currentSyncData$asse.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 = (0, _normalize.buildEntryList)({ contentTypeItems, currentSyncData }); const { assets } = currentSyncData; // Create map of resolvable ids so we can check links against them while creating // links. const resolvable = (0, _normalize.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 = (0, _normalize.buildForeignReferenceMap)({ contentTypeItems, entryList, resolvable, defaultLocale, space, useNameForId: pluginConfig.get(`useNameForId`), previousForeignReferenceMapState, deletedEntries: currentSyncData === null || currentSyncData === void 0 ? void 0 : 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((0, _normalize.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((0, _normalize.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 (0, _utils.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 = node === null || node === void 0 ? void 0 : node.children) !== null && _node$children !== void 0 ? _node$children : []) { var _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 (0, _utils.untilNextEventLoopTick)(); } } } } // allow node to gc if it needs to // @ts-ignore existingNodesThatNeedReverseLinksUpdateInDatastore = undefined; await (0, _utils.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 (0, _normalize.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 (0, _utils.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 (0, _normalize.createAssetNodes)({ assetItem: assets[i], createNode, createNodeId, defaultLocale, locales, space, pluginConfig }))); assets[i] = undefined; if (i % 1000 === 0) { await (0, _utils.untilNextEventLoopTick)(); } } await (0, _utils.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 (0, _downloadContentfulAssets.downloadContentfulAssets)({ assetNodes, actions, createNodeId, store, cache, getCache, getNode, reporter, assetDownloadWorkers: pluginConfig.get(`assetDownloadWorkers`) }); } }