UNPKG

@oracle/gatsby-source-oce

Version:

Gatsby source plugin for using Oracle Content Management as a data source

316 lines (275 loc) 9.72 kB
/* eslint-disable no-plusplus */ /** * Copyright (c) 2021, 2022, Oracle and/or its affiliates. * Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. */ /* eslint-disable no-await-in-loop */ /* eslint-disable no-param-reassign */ /* eslint-disable no-console */ /* eslint-disable no-shadow */ const { createRemoteFileNode } = require('gatsby-source-filesystem'); const fs = require('fs').promises; const { createWriteStream } = require('fs'); const { Headers } = require('node-fetch'); const fetch = require('node-fetch'); exports.cleanUp = (entities) => entities.map((item) => { delete item.links; delete item.createdDate; return item; }); /** * A utility function to reformat dates for storage * @private */ const isDate = (v) => (typeof v === 'object') && (typeof v.value === 'string' && typeof v.timezone === 'string'); const isDigitalAsset = (e) => (e.typeCategory === 'DigitalAssetType' || (!e.typeCategory && e.oceType === 'DigitalAsset')); // Standardize dates on ISO 8601 version. exports.standardizeDates = (entities) => { const fixDate = (aggr, kv) => { const [key, value] = kv; if (value) { // could be null if (Array.isArray(value)) { aggr[key] = value.map((v) => (isDate(v) ? v.value : v)); } else if (isDate(value)) { aggr[key] = value.value; } else { aggr[key] = value; } } return aggr; }; return entities.map((e) => { // if value has value && timezone e.updatedDate = e.updatedDate.value; // handle field values e.fields = Object.entries(e.fields).reduce(fixDate, {}); return e; }); }; exports.normalizeDigitalAsset = (entities) => entities.map((e) => { if (isDigitalAsset(e)) { // console.log(e.id, Object.keys(e.fields)) e.fields.native = e.fields.native.links .filter((f) => f.rel === 'self') .reduce((_, f) => f.href, ''); } return e; }); /** * Every Gatsby asset has to have a unique id. This function generates the * id based on the OCE asset id and channel id * @private * @param {*} createNodeId action passed in from Gatsby * @param {*} entities An array of asset records to process */ exports.createGatsbyIds = (createNodeId, entities, channelToken) => entities.map((e) => { e.oceId = e.id; e.id = createNodeId(`oracleoce-${e.id}-${channelToken}`); return e; }); exports.FixTypeDefinitions = (entities) => entities.map((e) => { // We want to index assets under the generic oceAsset type to make OCE-style queries easier e.oceType = e.type; e.oceFields = e.fields; e.type = 'oceAsset'; return e; }); exports.moveFieldsUp = (entities) => entities .map(({ fields, ...other }) => ({ ...other, ...fields })); // const pretty = (o) => JSON.stringify(o, null, 2); const getMediaList = (element, renditions, staticRootDir = null, staticUrlPrefix = '') => { const result = []; const baseKey = `oce-media-${element.id}`; // Get the asset name less the extension const name = element.name.replace(/\....$/, ''); // We always get the native rendition. The name of the native rendition is the asset name result.push({ key: baseKey.concat('-native'), name: `${name}`, staticName: element.name, staticSubDir: '', url: element.native, reference: element, }); // Only do this if we are downloading the asset to be referenced as a public url in the site if (staticRootDir) { element.staticURL = `${staticUrlPrefix}/${staticRootDir}/${element.name}`; } // Now loop through renditions and grab the first version of each. The name of the rendition // is the asset name with a suffix of -renditionType (eq -Thumbnail) if (renditions !== 'none') { element.renditions.forEach((e) => { // We only download system renditions when specifically required if ((renditions === 'all') || (renditions === 'custom' && e.type === 'customrendition')) { result.push({ key: baseKey.concat(`${e.name}`), name: `${e.name}-${name}`, staticName: element.name, staticSubDir: `/${e.name}`, url: `${e.formats[0].links[0].href}`, reference: e, }); // Only do this if we are downloading the asset to be referenced as a public url in the site if (staticRootDir) { e.staticURL = `${staticUrlPrefix}/${staticRootDir}/${e.name}/${element.name}`; } } }); } return result; }; // This is used to create the directory used to store assets downloaded from the server. exports.prepareForStaticDownload = async ({ staticAssetRootDir }) => { const fullPath = `./public/${staticAssetRootDir}`; console.log(`Creating static path${fullPath}`); try { await fs.mkdir(fullPath, { recursive: true }); } catch (e) { console.log("Couldn't create storage dir"); } }; // Async file download to public folder. async function downloadToPublic( fileUrl, storagePath, storageName, oAuthStr, ) { // Make the subdirectory to store the file try { await fs.mkdir(storagePath, { recursive: true }); } catch (e) { console.log(`Couldn't create dir${storagePath}`); } // Download the file const headers = new Headers({ Authorization: `${oAuthStr}`, Accept: '*/*', Connection: 'keep-alive', 'User-Agent': 'oracle/gatsby-source-oce', }); console.log(`Downloading file ${fileUrl}`); const res = await fetch(fileUrl, { headers }); return new Promise((resolve, reject) => { const fileStream = createWriteStream(`${storagePath}/${storageName}`); res.body.pipe(fileStream); res.body.on('error', (err) => { console.log(`Error downloading file ${fileUrl} ||${storageName}`); reject(err); }); fileStream.on('finish', () => { console.log(`Finished downloading file ${fileUrl} || ${storageName}\n\n`); resolve(); }); }); } // Used to download digital asset data to the static directory processed by gatsby-file exports.downloadMediaFilesToStaticDir = async ({ entities, staticAssetRootDir, staticUrlPrefix, renditions, oAuthStr, }) => { const dataItemMap = new Map(); // Build a map of files that we need to download for (let idx = 0; idx < entities.length; idx++) { const e = entities[idx]; if (isDigitalAsset(e)) { const dataItemList = getMediaList(e, renditions, staticAssetRootDir, staticUrlPrefix); for (let mediaIndex = 0; mediaIndex < dataItemList.length; mediaIndex++) { const mediaObject = dataItemList[mediaIndex]; const storagePath = `./public/${staticAssetRootDir}/${mediaObject.staticSubDir}`; const storageName = `${mediaObject.staticName}`; const fileUrl = mediaObject.url; dataItemMap.set(fileUrl, { fileUrl, storagePath, storageName }); } } } const promises = []; dataItemMap.forEach(async (value) => { promises.push(downloadToPublic(value.fileUrl, value.storagePath, value.storageName, oAuthStr)); }); await Promise.all(promises); }; // Used to download digital asset data to the cache exports.downloadMediaFiles = async ({ entities, store, cache, createNode, createNodeId, touchNode, renditions, oAuthStr, }) => Promise.all( entities.map(async (e) => { if (isDigitalAsset(e)) { // Call new function to get all renditions const dataItemList = getMediaList(e, renditions); let fileNodeID = null; for (let x = 0; x < dataItemList.length; x += 1) { const mediaObject = dataItemList[x]; fileNodeID = null; const cacheMediaData = await cache.get(mediaObject.key); // If we have cached media data and it wasn't modified, reuse // previously created file node to not try to re-download if (cacheMediaData && e.updatedDate === cacheMediaData.updatedDate) { fileNodeID = cacheMediaData.fileNodeID; touchNode({ nodeId: cacheMediaData.fileNodeID }); } // If we don't have cached data, download the file if (!fileNodeID) { try { console.log(`downloading mediaFile ${mediaObject.url}`); let fileNode = null; // If oAuthStr is defined, the download will be from a preview (management) call or // from a secure channel. let httpHeaders = ''; if (oAuthStr !== '') { httpHeaders = { Authorization: `${oAuthStr}` }; } fileNode = await createRemoteFileNode({ url: mediaObject.url, httpHeaders, store, cache, createNode, createNodeId, name: mediaObject.name, }); if (fileNode) { fileNodeID = fileNode.id; await cache.set(mediaObject.key, { fileNodeID, updatedDate: e.updatedDate, }); } } catch (error) { console.log(`ERROR: Downloading media file !${error}`); // Ignore } } if (fileNodeID) { if ('reference' in mediaObject) { mediaObject.reference.gatsbyFileNodeId = fileNodeID; } } } } return e; }), ); exports.createNodesFromEntities = async ({ entities, createNode, createContentDigest }) => { await entities.forEach(async (e) => { const { type, ...entity } = e; // eslint-disable-line no-unused-vars const children = []; const node = { ...entity, children, parent: null, internal: { type, contentDigest: createContentDigest(JSON.stringify(entity)), }, }; await createNode(node); }); };